using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Text; using UnityEngine; using GeneralTools; public class DataService : MonoBehaviour, IDataService { public static DataService Instance { get; private set; } public event Action OnBFIValueUpdated; // public event Action OnBloodFlowValueUpdated; // public event Action OnBatteryLevelUpdated; public float CurrentBFI { get; private set; } public float CurrentBloodFlow { get; private set; } public float CurrentBatteryLevel { get; private set; } = 80f; // 初始电量80% public bool IsCollecting { get; private set; } private Coroutine _dataCollectionCoroutine; private Coroutine _batteryDrainCoroutine; // 串口通信服务 private ISerialCommunicationService _serialCommunication; private bool _useSerialCommunication = true; // 控制是否使用串口通信 private bool _enableSimulatedData = false; // 禁用模拟数据,改为使用真实串口数据 // BFI测试记录相关 private BFITestRecord _currentTestRecord; private List _currentTestData; private bool _isRecordingTest = false; private const float MIN_VALID_BFI = 0f; private const float MAX_VALID_BFI = 1000f; // 自动记录相关 private DateTime _lastHeartbeatTime = DateTime.MinValue; private const float HEARTBEAT_TIMEOUT_SECONDS = 5f; // 5秒没有心跳只标记断开,不切分记录 private static readonly TimeSpan MAX_RECORDING_DURATION = TimeSpan.FromHours(24); private bool _hasReceivedHeartbeat = false; private bool _autoRecordingEnabled = false; private bool _heartbeatTimeoutLogged = false; private Coroutine _heartbeatMonitorCoroutine; private DateTime _lastCurrentRecordSnapshotTime = DateTime.MinValue; private int _lastCurrentRecordSnapshotPointCount = 0; private long _lastJournaledPointTicks = 0; private float _lastJournaledPointBFI = float.NaN; private static readonly TimeSpan CURRENT_RECORD_SNAPSHOT_INTERVAL = TimeSpan.FromMinutes(5); private const string CURRENT_RECORDING_JOURNAL_FILE_NAME = "bfi_current_recording.jsonl"; private const string JOURNAL_ENTRY_START = "start"; private const string JOURNAL_ENTRY_POINT = "point"; private const string JOURNAL_ENTRY_PATIENT = "patient"; private const string JOURNAL_ENTRY_END = "end"; private string _currentRecordingJournalPath; private void Awake() { Instance = this; ServiceLocator.Register(this); Debug.Log("DataService: 已注册到ServiceLocator"); } void Start() { // 初始化安卓平台配置 InitializeAndroidConfig(); _currentRecordingJournalPath = ResolveCurrentRecordingJournalPath(); RecoverCurrentRecordingJournalIfNeeded("程序启动"); // 初始化串口通信服务 InitializeSerialCommunication(); // 订阅患者信息变更事件 var patientService = ServiceLocator.Get(); if (patientService != null) { patientService.OnPatientInfoChanged += OnPatientInfoChanged; } // 启动电池消耗协程 // _batteryDrainCoroutine = StartCoroutine(BatteryDrainCoroutine()); // 根据是否有串口通信决定数据采集方式 if (_enableSimulatedData && (!_useSerialCommunication || _serialCommunication == null)) { // 只有明确启用模拟数据时才使用模拟数据采集 Debug.Log("DataService: 启用模拟数据采集"); StartDataCollection(); } else { Debug.Log("DataService: 等待真实串口数据,模拟数据已禁用"); } // 启动心跳监控协程 _heartbeatMonitorCoroutine = StartCoroutine(MonitorHeartbeat()); } public void StartDataCollection() { if (IsCollecting) return; IsCollecting = true; } public void StopDataCollection() { if (!IsCollecting) return; IsCollecting = false; if (_dataCollectionCoroutine != null) { StopCoroutine(_dataCollectionCoroutine); _dataCollectionCoroutine = null; } } /// /// 初始化串口通信服务 /// private void InitializeSerialCommunication() { try { _serialCommunication = ServiceLocator.Get(); if (_serialCommunication == null) { Debug.LogWarning("未找到串口通信服务,将使用模拟数据"); _useSerialCommunication = false; return; } // 订阅串口通信事件 _serialCommunication.OnDeviceStatusReceived += OnDeviceStatusReceived; // _serialCommunication.OnDeviceDataReceived += OnDeviceDataReceived; // _serialCommunication.OnAlarmStatusReceived += OnAlarmStatusReceived; _serialCommunication.OnCommunicationError += OnCommunicationError; _serialCommunication.OnHandshakeCompleted += OnHandshakeCompleted; // 从配置系统读取串口参数 string portName = GetPlatformSerialPortName(); //GetSerialPortName(); int baudRate = GetPlatformSerialBaudRate(); //GetSerialBaudRate(); Debug.Log($"尝试连接串口: {portName} @ {baudRate}"); // 尝试连接串口 if (!_serialCommunication.Connect(portName, baudRate)) { Debug.LogWarning($"串口连接失败 ({portName}),将使用模拟数据"); _useSerialCommunication = false; } else { Debug.Log("串口通信服务初始化成功"); } } catch (Exception ex) { Debug.LogError($"初始化串口通信服务失败: {ex.Message}"); _useSerialCommunication = false; } } /// /// 处理设备状态数据(心跳报告) /// 自动开始记录:收到第一个心跳就开始记录 /// private void OnDeviceStatusReceived(DeviceStatusData status) { CurrentBFI = status.BFI; CurrentBatteryLevel = status.BatteryLevel; if (_autoRecordingEnabled && !_isRecordingTest) { AutoStartRecording(); } // 更新心跳时间。心跳恢复时只继续向当前主界面会话记录写点,不新建记录。 _lastHeartbeatTime = DateTime.Now; _hasReceivedHeartbeat = true; _heartbeatTimeoutLogged = false; OnBFIValueUpdated?.Invoke(CurrentBFI); // OnBatteryLevelUpdated?.Invoke(CurrentBatteryLevel); // 记录关键状态变化到文件日志 #if UNITY_ANDROID && !UNITY_EDITOR // 电量低于10%时记录关键信息 if (status.BatteryLevel <= 10 && status.PowerType == 1) { AndroidFileLogger.Instance?.LogError("DeviceStatus", $"电量严重不足: {status.BatteryLevel}%, BFI: {status.BFI:F2}"); } // BFI异常值时记录 else if (status.BFI <= 0 || status.BFI > 999 || float.IsNaN(status.BFI)) { AndroidFileLogger.Instance?.LogError("DeviceStatus", $"BFI值异常: {status.BFI}, 电量: {status.BatteryLevel}%"); } // 正常情况下只偶尔记录 else if (Time.time % 60f < 1f) // 每分钟记录一次状态 { AndroidFileLogger.Instance?.LogInfo("DeviceStatus", $"设备正常: BFI={status.BFI:F2}, 电量={status.BatteryLevel}%"); } #endif // 只有在记录测试时才记录BFI数据,避免不必要的内存占用 if (_autoRecordingEnabled && _isRecordingTest) { RolloverRecordingIfNeeded("记录超过24小时"); RecordBFIData(status.BFI); } // 通过DCSAlarmManager检查设备状态 if (DCSAlarmManager.Instance != null) { DCSAlarmManager.Instance.CheckAndSendAlarms(status); DCSAlarmManager.Instance.CheckBFIAlarm(CurrentBFI); } } /// /// 处理通信错误 /// private void OnCommunicationError() { // 触发通信异常报警(代码0x34) DCSAlarmManager.Instance?.SendAlarmCommand(0x34, AlarmPriority.High, true); #if UNITY_ANDROID && !UNITY_EDITOR AndroidFileLogger.Instance?.LogError("Communication", $"串口通信异常,时间: {System.DateTime.Now:HH:mm:ss}"); #endif // 通信错误时的处理(不再自动切换到模拟模式) if (_useSerialCommunication) { Debug.LogWarning("通信异常,等待重新连接"); _useSerialCommunication = false; // 只有明确启用模拟数据时才启动模拟采集 if (_enableSimulatedData) { Debug.Log("启用模拟数据作为备用"); StartDataCollection(); } } } /// /// 握手完成处理 /// private void OnHandshakeCompleted() { Debug.Log("设备握手完成,开始接收数据"); IsCollecting = true; } void OnDestroy() { // 停止数据采集 StopDataCollection(); // 如果有正在进行的BFI测试记录,保存数据 SaveCurrentRecordingSnapshotIfNeeded(true, "程序退出"); SaveCurrentRecordingIfNeeded("程序退出"); if (_batteryDrainCoroutine != null) { StopCoroutine(_batteryDrainCoroutine); } // 停止心跳监控协程 if (_heartbeatMonitorCoroutine != null) { StopCoroutine(_heartbeatMonitorCoroutine); } // 取消患者信息事件订阅 var patientService = ServiceLocator.Get(); if (patientService != null) { patientService.OnPatientInfoChanged -= OnPatientInfoChanged; } // 清理串口通信服务 if (_serialCommunication != null) { _serialCommunication.OnDeviceStatusReceived -= OnDeviceStatusReceived; // _serialCommunication.OnDeviceDataReceived -= OnDeviceDataReceived; // _serialCommunication.OnAlarmStatusReceived -= OnAlarmStatusReceived; _serialCommunication.OnCommunicationError -= OnCommunicationError; _serialCommunication.OnHandshakeCompleted -= OnHandshakeCompleted; _serialCommunication.Disconnect(); } } private void OnApplicationPause(bool pauseStatus) { if (pauseStatus) { SaveCurrentRecordingSnapshotIfNeeded(true, "应用进入后台"); SaveCurrentRecordingIfNeeded("应用进入后台"); } } private void OnApplicationQuit() { SaveCurrentRecordingSnapshotIfNeeded(true, "应用退出"); SaveCurrentRecordingIfNeeded("应用退出"); } public void SaveCurrentRecordingIfNeeded(string reason) { if (_isRecordingTest) { Debug.Log($"[{reason}] 检测到正在进行的BFI测试记录,自动保存数据"); StopBFITestRecording(); } } public void SetAutoBFIRecordingEnabled(bool enabled, string reason = "") { if (_autoRecordingEnabled == enabled) { if (enabled && !_isRecordingTest) { AutoStartRecording(); } return; } _autoRecordingEnabled = enabled; if (!enabled) { SaveCurrentRecordingIfNeeded(string.IsNullOrEmpty(reason) ? "关闭自动记录" : reason); _hasReceivedHeartbeat = false; _lastHeartbeatTime = DateTime.MinValue; Debug.Log($"[自动记录] 已关闭自动BFI记录: {reason}"); } else { _hasReceivedHeartbeat = false; _lastHeartbeatTime = DateTime.MinValue; _heartbeatTimeoutLogged = false; if (!_isRecordingTest) { AutoStartRecording(); } Debug.Log($"[自动记录] 已启用自动BFI记录: {reason}"); } } /// /// 初始化安卓平台配置 /// private void InitializeAndroidConfig() { #if UNITY_ANDROID && !UNITY_EDITOR try { Debug.Log("开始初始化安卓配置..."); // 确保目标目录存在 var persistentPath = Application.persistentDataPath; if (!Directory.Exists(persistentPath)) { Directory.CreateDirectory(persistentPath); } // 复制安卓配置文件 var sourceConfigPath = Path.Combine(Application.streamingAssetsPath, "config_android.ini"); var targetConfigPath = Path.Combine(persistentPath, "config_android.ini"); // 如果目标文件不存在,则从StreamingAssets复制 if (!File.Exists(targetConfigPath)) { if (File.Exists(sourceConfigPath)) { File.Copy(sourceConfigPath, targetConfigPath, true); Debug.Log($"安卓配置文件已复制到: {targetConfigPath}"); } else { Debug.LogWarning("未找到安卓配置文件,创建默认配置"); CreateDefaultAndroidConfig(targetConfigPath); } } Debug.Log("安卓配置初始化完成"); } catch (Exception ex) { Debug.LogError($"安卓配置初始化失败: {ex.Message}"); } #endif } #if UNITY_ANDROID && !UNITY_EDITOR private void CreateDefaultAndroidConfig(string configPath) { try { var defaultConfig = @"[SerialPort] serialPortName=/dev/ttyS4 serialBaudRate=115200 serialDataBits=8 serialStopBits=1 serialParity=None serialFlowControl=false heartbeatInterval=1000 communicationTimeout=5000 autoReconnect=true maxReconnectAttempts=5 shutdownBroadcastAction=com.dcx.ruiyiweiux.ACTION_SHUTDOWN"; File.WriteAllText(configPath, defaultConfig); Debug.Log($"创建默认安卓配置文件: {configPath}"); } catch (Exception ex) { Debug.LogError($"创建默认安卓配置文件失败: {ex.Message}"); } } #endif /// /// 根据平台获取合适的串口名称 /// /// 串口名称 public string GetPlatformSerialPortName() { // 优先从配置文件读取 try { #if UNITY_ANDROID && !UNITY_EDITOR // 安卓平台串口配置 var androidConfigPath = Path.Combine(Application.persistentDataPath, "config_android.ini"); if (File.Exists(androidConfigPath)) { var lines = File.ReadAllLines(androidConfigPath); foreach (var line in lines) { if (line.StartsWith("serialPortName=")) { var portName = line.Substring("serialPortName=".Length).Trim(); if (!string.IsNullOrEmpty(portName)) { Debug.Log($"从安卓配置文件读取串口名称: {portName}"); return portName; } } } } #endif // 桌面平台配置文件读取 var configPath = Path.Combine(Application.streamingAssetsPath, "config.ini"); if (File.Exists(configPath)) { var lines = File.ReadAllLines(configPath); foreach (var line in lines) { if (line.StartsWith("serialPortName=")) { var portName = line.Substring("serialPortName=".Length).Trim(); if (!string.IsNullOrEmpty(portName)) { Debug.Log($"从配置文件读取串口名称: {portName}"); return portName; } } } } } catch (Exception ex) { Debug.LogWarning($"读取串口配置失败: {ex.Message}"); } // 如果配置文件不存在或读取失败,使用平台默认值 #if UNITY_ANDROID && !UNITY_EDITOR // 安卓平台常用串口设备路径 string[] androidPorts = { "/dev/ttyS4", "/dev/ttyUSB1", "/dev/ttyACM0", "/dev/ttyACM1" }; foreach (var port in androidPorts) { if (File.Exists(port)) { Debug.Log($"检测到安卓串口设备: {port}"); return port; } } Debug.LogWarning("未检测到安卓串口设备,使用默认路径"); return "/dev/ttyS4"; #elif UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN // Windows平台默认串口 return "COM1"; #elif UNITY_STANDALONE_LINUX || UNITY_EDITOR_LINUX // Linux平台默认串口 return "/dev/ttyS4"; #elif UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX // macOS平台默认串口 return "/dev/cu.usbserial-1410"; #else // 其他平台默认 return "COM1"; #endif } /// /// 根据平台获取合适的串口波特率 /// /// 波特率 private int GetPlatformSerialBaudRate() { try { #if UNITY_ANDROID && !UNITY_EDITOR // 安卓平台配置读取 var androidConfigPath = Path.Combine(Application.persistentDataPath, "config_android.ini"); if (File.Exists(androidConfigPath)) { var lines = File.ReadAllLines(androidConfigPath); foreach (var line in lines) { if (line.StartsWith("serialBaudRate=")) { var baudRateStr = line.Substring("serialBaudRate=".Length).Trim(); if (int.TryParse(baudRateStr, out int baudRate)) { Debug.Log($"从安卓配置文件读取波特率: {baudRate}"); return baudRate; } } } } #endif // 桌面平台配置读取 var configPath = Path.Combine(Application.streamingAssetsPath, "config.ini"); if (File.Exists(configPath)) { var lines = File.ReadAllLines(configPath); foreach (var line in lines) { if (line.StartsWith("baudRate=")) { var baudRateStr = line.Substring("baudRate=".Length).Trim(); if (int.TryParse(baudRateStr, out int baudRate)) { Debug.Log($"从配置文件读取波特率: {baudRate}"); return baudRate; } } } } } catch (Exception ex) { Debug.LogWarning($"读取波特率配置失败: {ex.Message}"); } // 默认波特率(DCS协议标准) return 115200; } #region 自动记录管理 /// /// 患者信息变更处理 /// private void OnPatientInfoChanged(PatientInfo patient) { if (_isRecordingTest && _currentTestRecord != null && patient != null) { UpdateCurrentRecordPatientInfo(patient.HospitalId, patient.Name, patient.Gender, patient.Age, patient.Height, patient.Weight); } } /// /// 自动开始记录(收到心跳时触发) /// private void AutoStartRecording() { if (_isRecordingTest) { Debug.Log("已有测试正在记录中,无需重复开始"); return; } // 获取当前患者信息 var patientService = ServiceLocator.Get(); var patient = patientService?.GetCurrentPatient(); string patientId = ""; string patientName = "未知患者"; string patientGender = ""; int patientAge = 0; float patientHeight = 0f; float patientWeight = 0f; if (patient != null) { patientId = patient.HospitalId; patientName = patient.Name; patientGender = patient.Gender; patientAge = patient.Age; patientHeight = patient.Height; patientWeight = patient.Weight; } string testName = $"BFI测试_{DateTime.Now:yyyyMMdd_HHmmss}"; StartBFITestRecording(testName, patientId, patientName, patientGender, patientAge, patientHeight, patientWeight); Debug.Log($"[自动记录] 收到心跳,开始记录 - 患者: {patientName}"); } /// /// 心跳监控协程 /// private IEnumerator MonitorHeartbeat() { while (true) { yield return new WaitForSeconds(1f); // 每秒检查一次 if (_autoRecordingEnabled && _isRecordingTest) { RolloverRecordingIfNeeded("记录超过24小时"); FinalizeLastJournalPoint(); // 检查心跳超时 var timeSinceLastHeartbeat = (DateTime.Now - _lastHeartbeatTime).TotalSeconds; if (_hasReceivedHeartbeat && timeSinceLastHeartbeat > HEARTBEAT_TIMEOUT_SECONDS && !_heartbeatTimeoutLogged) { Debug.LogWarning($"[心跳监控] 心跳超时 ({timeSinceLastHeartbeat:F1}秒),保持当前BFI记录,等待心跳恢复"); _heartbeatTimeoutLogged = true; } } } } private void RolloverRecordingIfNeeded(string reason) { if (!_autoRecordingEnabled || !_isRecordingTest || _currentTestRecord == null) { return; } if (DateTime.Now - _currentTestRecord.StartTime < MAX_RECORDING_DURATION) { return; } Debug.Log($"[自动记录] {reason},保存当前BFI记录并继续新记录"); StopBFITestRecording(); AutoStartRecording(); } /// /// 更新当前记录的患者信息(当用户在记录过程中设置患者信息时) /// public void UpdateCurrentRecordPatientInfo(string patientId, string patientName) { UpdateCurrentRecordPatientInfo(patientId, patientName, string.Empty, 0, 0f, 0f); } /// /// 更新当前记录的完整患者信息(当用户在记录过程中更新患者信息时) /// public void UpdateCurrentRecordPatientInfo(string patientId, string patientName, string patientGender, int patientAge, float patientHeight, float patientWeight) { if (_isRecordingTest && _currentTestRecord != null) { _currentTestRecord.PatientId = patientId; _currentTestRecord.PatientName = patientName; _currentTestRecord.PatientGender = patientGender; _currentTestRecord.PatientAge = patientAge; _currentTestRecord.PatientHeight = patientHeight; _currentTestRecord.PatientWeight = patientWeight; AppendCurrentRecordingJournalEntry(CreateJournalPatientEntry(_currentTestRecord)); SaveCurrentRecordingSnapshotIfNeeded(true, "更新患者信息"); Debug.Log($"[自动记录] 更新患者信息: {patientName} ({patientId})"); } } #endregion #region BFI测试记录管理 /// /// 开始BFI测试记录 /// public void StartBFITestRecording(string testName = "", string patientId = "", string patientName = "", string patientGender = "", int patientAge = 0, float patientHeight = 0f, float patientWeight = 0f) { try { if (_isRecordingTest) { Debug.LogWarning("已有测试正在记录中,先停止当前测试"); StopBFITestRecording(); } // 创建新的测试记录 var startTime = DateTime.Now; _currentTestRecord = new BFITestRecord { TestId = Guid.NewGuid().ToString(), TestName = string.IsNullOrEmpty(testName) ? $"BFI测试_{startTime:yyyyMMdd_HHmmss}" : testName, PatientId = patientId, PatientName = patientName, PatientGender = patientGender, PatientAge = patientAge, PatientHeight = patientHeight, PatientWeight = patientWeight, StartTime = startTime, StartTimeTicks = startTime.Ticks, EndTime = DateTime.MinValue, EndTimeTicks = 0, DataPoints = new List() }; _currentTestData = new List(); _isRecordingTest = true; _lastCurrentRecordSnapshotTime = DateTime.MinValue; _lastCurrentRecordSnapshotPointCount = 0; _lastJournaledPointTicks = 0; _lastJournaledPointBFI = float.NaN; ResetCurrentRecordingJournal(); AppendCurrentRecordingJournalEntry(CreateJournalStartEntry(_currentTestRecord)); Debug.Log($"开始BFI测试记录: {_currentTestRecord.TestName}"); } catch (Exception ex) { Debug.LogError($"开始BFI测试记录失败: {ex.Message}"); } } /// /// 停止BFI测试记录并保存 /// public void StopBFITestRecording() { try { if (!_isRecordingTest || _currentTestRecord == null) { Debug.LogWarning("没有正在进行的测试记录"); return; } // 完成测试记录 var endTime = DateTime.Now; _currentTestRecord.EndTime = endTime; _currentTestRecord.EndTimeTicks = endTime.Ticks; _currentTestRecord.DataPoints = new List(_currentTestData); // 计算统计信息 _currentTestRecord.EndTest(endTime); FinalizeLastJournalPoint(); AppendCurrentRecordingJournalEntry(CreateJournalEndEntry(_currentTestRecord)); // 保存到历史记录 bool savedToHistory = false; if (BFIHistoryManager.Instance != null) { bool savedToDisk = BFIHistoryManager.Instance.AddTestRecord(_currentTestRecord); var savedRecord = BFIHistoryManager.Instance.GetRecord(_currentTestRecord.TestId); int expectedPointCount = _currentTestData?.Count ?? 0; savedToHistory = savedToDisk && savedRecord != null && (savedRecord.DataPoints?.Count ?? 0) >= expectedPointCount && BFIHistoryManager.Instance.IsRecordPersisted(_currentTestRecord.TestId, expectedPointCount); if (savedToHistory) { Debug.Log($"BFI测试记录已保存: {_currentTestRecord.TestName}, 数据点: {_currentTestData.Count}"); } else { Debug.LogWarning($"BFI测试记录保存失败或未确认落盘: {_currentTestRecord.TestName}, 数据点: {_currentTestData.Count}"); } } else { Debug.LogWarning("BFIHistoryManager未找到,无法保存测试记录"); } if (savedToHistory) { DeleteCurrentRecordingJournal(); } else { Debug.LogWarning("BFI测试记录未确认写入历史索引,保留实时日志以便下次启动恢复"); } // 清理 _isRecordingTest = false; _currentTestRecord = null; _currentTestData?.Clear(); _lastCurrentRecordSnapshotTime = DateTime.MinValue; _lastCurrentRecordSnapshotPointCount = 0; _lastJournaledPointTicks = 0; _lastJournaledPointBFI = float.NaN; } catch (Exception ex) { Debug.LogError($"停止BFI测试记录失败: {ex.Message}"); } } /// /// 记录BFI数据点 /// private void RecordBFIData(float bfiValue) { if (!_isRecordingTest || _currentTestData == null) return; try { if (float.IsNaN(bfiValue) || float.IsInfinity(bfiValue) || bfiValue < MIN_VALID_BFI || bfiValue > MAX_VALID_BFI) { Debug.LogWarning($"[自动记录] 丢弃异常BFI数据点: {bfiValue}"); return; } // 记录粒度按秒:同一秒内只保留一个数据点(更新为最新值) var timestamp = TruncateToSecond(DateTime.Now); if (_currentTestData.Count > 0) { var lastPoint = _currentTestData[_currentTestData.Count - 1]; if (lastPoint != null && lastPoint.Timestamp == timestamp) { lastPoint.BFI = bfiValue; lastPoint.Status = "正常"; lastPoint.AlarmLevel = 0; lastPoint.TimestampTicks = timestamp.Ticks; return; } FinalizeJournalPoint(lastPoint); } var dataPoint = new BFIDataPoint { Timestamp = timestamp, TimestampTicks = timestamp.Ticks, BFI = bfiValue, Status = "正常", AlarmLevel = 0 }; _currentTestData.Add(dataPoint); AppendJournalPointIfChanged(dataPoint); SaveCurrentRecordingSnapshotIfNeeded(false, "新增BFI数据点"); // 严格限制数据点数量,防止安卓板内存溢出 const int MAX_DATA_POINTS = 90000; // 24小时每秒1点约86400个,保留完整日记录 if (_currentTestData.Count > MAX_DATA_POINTS) { // 移除前1/3数据,保留后2/3,减少频繁操作 int removeCount = _currentTestData.Count / 3; _currentTestData.RemoveRange(0, removeCount); #if UNITY_ANDROID && !UNITY_EDITOR AndroidFileLogger.Instance?.LogInfo("DataService", $"BFI数据清理: 删除{removeCount}个旧数据点,剩余{_currentTestData.Count}个"); #endif // 减少日志输出频率,避免内存积压 if (_currentTestData.Count % 300 == 0) // 每300个数据点记录一次 { Debug.LogWarning($"[内存管理] BFI数据达到{MAX_DATA_POINTS}个,已清理{removeCount}个旧数据点"); } } } catch (Exception ex) { Debug.LogError($"记录BFI数据失败: {ex.Message}"); #if UNITY_ANDROID && !UNITY_EDITOR AndroidFileLogger.Instance?.LogError("DataService", $"记录BFI数据异常: {ex.Message}"); #endif } } private DateTime TruncateToSecond(DateTime time) { return new DateTime(time.Year, time.Month, time.Day, time.Hour, time.Minute, time.Second, time.Kind); } private void FinalizeLastJournalPoint() { if (_currentTestData == null || _currentTestData.Count == 0) { return; } FinalizeJournalPoint(_currentTestData[_currentTestData.Count - 1]); } private void FinalizeJournalPoint(BFIDataPoint point) { AppendJournalPointIfChanged(point); } private void AppendJournalPointIfChanged(BFIDataPoint point) { if (point == null || point.TimestampTicks == 0) { return; } if (point.TimestampTicks == _lastJournaledPointTicks && Mathf.Approximately(point.BFI, _lastJournaledPointBFI)) { return; } AppendCurrentRecordingJournalEntry(CreateJournalPointEntry(point)); _lastJournaledPointTicks = point.TimestampTicks; _lastJournaledPointBFI = point.BFI; } private void SaveCurrentRecordingSnapshotIfNeeded(bool force, string reason) { if (!_isRecordingTest || _currentTestRecord == null || _currentTestData == null || _currentTestData.Count == 0) { return; } DateTime now = DateTime.Now; bool intervalReached = now - _lastCurrentRecordSnapshotTime >= CURRENT_RECORD_SNAPSHOT_INTERVAL; bool pointCountChanged = _currentTestData.Count != _lastCurrentRecordSnapshotPointCount; if (!force && (!intervalReached || !pointCountChanged)) { return; } try { var snapshot = CreateCurrentRecordingSnapshot(now); bool saved = force ? BFIHistoryManager.Instance.AddTestRecord(snapshot) : BFIHistoryManager.Instance.AddTestRecordSnapshot(snapshot); if (!saved) { Debug.LogWarning($"[自动记录] 当前BFI记录快照未确认写入磁盘: {reason}"); _lastCurrentRecordSnapshotTime = now; return; } _lastCurrentRecordSnapshotTime = now; _lastCurrentRecordSnapshotPointCount = _currentTestData.Count; if (force || _currentTestData.Count % 300 == 0) { Debug.Log($"[自动记录] 当前BFI记录快照已保存: {reason}, 数据点: {_currentTestData.Count}"); } } catch (Exception ex) { Debug.LogWarning($"[自动记录] 保存当前BFI记录快照失败: {ex.Message}"); } } public bool FlushCurrentBFIRecordingSnapshot(string reason = "") { int beforePointCount = _lastCurrentRecordSnapshotPointCount; SaveCurrentRecordingSnapshotIfNeeded(true, string.IsNullOrEmpty(reason) ? "手动刷新当前BFI记录" : reason); return _lastCurrentRecordSnapshotPointCount >= beforePointCount && _currentTestData != null && _lastCurrentRecordSnapshotPointCount == _currentTestData.Count; } public void RecoverPendingBFIRecordingJournal(string reason = "") { RecoverCurrentRecordingJournalIfNeeded(string.IsNullOrEmpty(reason) ? "手动恢复当前BFI实时日志" : reason); } public void ResetCurrentBFIRecordingAfterHistoryClear(string reason = "") { bool shouldContinueRecording = _autoRecordingEnabled && _isRecordingTest; _isRecordingTest = false; _currentTestRecord = null; _currentTestData?.Clear(); _lastCurrentRecordSnapshotTime = DateTime.MinValue; _lastCurrentRecordSnapshotPointCount = 0; _lastJournaledPointTicks = 0; _lastJournaledPointBFI = float.NaN; DeleteCurrentRecordingJournal(); Debug.Log($"[自动记录] 已重置当前BFI记录缓存: {reason}"); if (shouldContinueRecording) { AutoStartRecording(); } } private BFITestRecord CreateCurrentRecordingSnapshot(DateTime snapshotTime) { var snapshot = new BFITestRecord { TestId = _currentTestRecord.TestId, TestName = _currentTestRecord.TestName, PatientId = _currentTestRecord.PatientId, PatientName = _currentTestRecord.PatientName, PatientGender = _currentTestRecord.PatientGender, PatientAge = _currentTestRecord.PatientAge, PatientHeight = _currentTestRecord.PatientHeight, PatientWeight = _currentTestRecord.PatientWeight, LowThreshold = _currentTestRecord.LowThreshold, HighThreshold = _currentTestRecord.HighThreshold, AlarmPriority = _currentTestRecord.AlarmPriority, AlarmEnabled = _currentTestRecord.AlarmEnabled, StartTime = _currentTestRecord.StartTime, StartTimeTicks = _currentTestRecord.StartTimeTicks, EndTime = snapshotTime, EndTimeTicks = snapshotTime.Ticks, DataPoints = new List(_currentTestData) }; snapshot.EndTest(snapshotTime); return snapshot; } private string ResolveCurrentRecordingJournalPath() { return Path.Combine(Application.persistentDataPath, CURRENT_RECORDING_JOURNAL_FILE_NAME); } private void RecoverCurrentRecordingJournalIfNeeded(string reason) { try { if (string.IsNullOrEmpty(_currentRecordingJournalPath)) { _currentRecordingJournalPath = ResolveCurrentRecordingJournalPath(); } if (_isRecordingTest || !File.Exists(_currentRecordingJournalPath)) { return; } if (!TryReadCurrentRecordingJournal(out var recoveredRecord)) { Debug.LogWarning($"[自动记录] 未能恢复当前BFI实时日志,保留文件等待下次尝试: {_currentRecordingJournalPath}"); return; } int pointCount = recoveredRecord.DataPoints?.Count ?? 0; if (pointCount == 0) { DeleteCurrentRecordingJournal(); Debug.Log("[自动记录] 当前BFI实时日志没有数据点,已清理"); return; } bool savedToDisk = BFIHistoryManager.Instance.AddTestRecord(recoveredRecord); var savedRecord = BFIHistoryManager.Instance.GetRecord(recoveredRecord.TestId); if (savedToDisk && savedRecord != null && (savedRecord.DataPoints?.Count ?? 0) >= pointCount && BFIHistoryManager.Instance.IsRecordPersisted(recoveredRecord.TestId, pointCount)) { DeleteCurrentRecordingJournal(); Debug.Log($"[自动记录] 已从实时日志恢复BFI记录: {reason}, 数据点: {pointCount}"); } else { Debug.LogWarning($"[自动记录] 实时日志恢复后未确认写入历史索引,继续保留日志: {_currentRecordingJournalPath}"); } } catch (Exception ex) { Debug.LogWarning($"[自动记录] 恢复当前BFI实时日志失败: {ex.Message}"); } } private bool TryReadCurrentRecordingJournal(out BFITestRecord record) { record = null; long endTimeTicks = 0; try { foreach (var line in File.ReadLines(_currentRecordingJournalPath, Encoding.UTF8)) { if (string.IsNullOrWhiteSpace(line)) { continue; } BFIRecordingJournalEntry entry; try { entry = JsonUtility.FromJson(line); } catch { // 突然断电可能留下半行,跳过无法解析的最后一行。 continue; } if (entry == null || string.IsNullOrEmpty(entry.Type)) { continue; } if (entry.Type == JOURNAL_ENTRY_START) { record = CreateRecordFromJournalStart(entry); } else if (entry.Type == JOURNAL_ENTRY_PATIENT) { if (record == null) { record = CreateRecordFromJournalStart(entry); } ApplyJournalPatientInfo(record, entry); } else if (entry.Type == JOURNAL_ENTRY_POINT) { if (record == null) { record = CreateRecordFromJournalStart(entry); } AddOrUpdateJournalPoint(record, entry); } else if (entry.Type == JOURNAL_ENTRY_END) { endTimeTicks = entry.EndTimeTicks; } } if (record == null) { return false; } record.RestoreAfterDeserialization(); DateTime endTime = ResolveRecoveredEndTime(record, endTimeTicks); record.EndTest(endTime); return true; } catch (Exception ex) { Debug.LogWarning($"[自动记录] 读取当前BFI实时日志失败: {ex.Message}"); record = null; return false; } } private BFITestRecord CreateRecordFromJournalStart(BFIRecordingJournalEntry entry) { long startTicks = entry.StartTimeTicks > 0 ? entry.StartTimeTicks : DateTime.Now.Ticks; var startTime = new DateTime(startTicks, DateTimeKind.Local); return new BFITestRecord { TestId = string.IsNullOrEmpty(entry.TestId) ? Guid.NewGuid().ToString() : entry.TestId, TestName = string.IsNullOrEmpty(entry.TestName) ? $"BFI测试_{startTime:yyyyMMdd_HHmmss}" : entry.TestName, PatientId = entry.PatientId ?? string.Empty, PatientName = entry.PatientName ?? string.Empty, PatientGender = entry.PatientGender ?? string.Empty, PatientAge = entry.PatientAge, PatientHeight = entry.PatientHeight, PatientWeight = entry.PatientWeight, LowThreshold = entry.LowThreshold, HighThreshold = entry.HighThreshold, AlarmPriority = entry.AlarmPriority, AlarmEnabled = entry.AlarmEnabled, StartTime = startTime, StartTimeTicks = startTicks, EndTime = DateTime.MinValue, EndTimeTicks = 0, DataPoints = new List() }; } private void ApplyJournalPatientInfo(BFITestRecord record, BFIRecordingJournalEntry entry) { record.PatientId = entry.PatientId ?? string.Empty; record.PatientName = entry.PatientName ?? string.Empty; record.PatientGender = entry.PatientGender ?? string.Empty; record.PatientAge = entry.PatientAge; record.PatientHeight = entry.PatientHeight; record.PatientWeight = entry.PatientWeight; } private void AddOrUpdateJournalPoint(BFITestRecord record, BFIRecordingJournalEntry entry) { if (record.DataPoints == null) { record.DataPoints = new List(); } long timestampTicks = entry.TimestampTicks > 0 ? entry.TimestampTicks : DateTime.Now.Ticks; var existingPoint = record.DataPoints.Find(point => point != null && point.TimestampTicks == timestampTicks); if (existingPoint != null) { existingPoint.BFI = entry.BFI; existingPoint.Status = string.IsNullOrEmpty(entry.Status) ? "正常" : entry.Status; existingPoint.Notes = entry.Notes ?? string.Empty; existingPoint.AlarmLevel = entry.AlarmLevel; existingPoint.Timestamp = new DateTime(timestampTicks, DateTimeKind.Local); return; } record.DataPoints.Add(new BFIDataPoint { Timestamp = new DateTime(timestampTicks, DateTimeKind.Local), TimestampTicks = timestampTicks, BFI = entry.BFI, Status = string.IsNullOrEmpty(entry.Status) ? "正常" : entry.Status, Notes = entry.Notes ?? string.Empty, AlarmLevel = entry.AlarmLevel }); } private DateTime ResolveRecoveredEndTime(BFITestRecord record, long endTimeTicks) { if (endTimeTicks > 0) { return new DateTime(endTimeTicks, DateTimeKind.Local); } if (record.DataPoints != null && record.DataPoints.Count > 0) { var lastPoint = record.DataPoints[record.DataPoints.Count - 1]; if (lastPoint != null && lastPoint.TimestampTicks > 0) { return new DateTime(lastPoint.TimestampTicks, DateTimeKind.Local); } } return DateTime.Now; } private void ResetCurrentRecordingJournal() { DeleteCurrentRecordingJournal(); } private void DeleteCurrentRecordingJournal() { try { if (string.IsNullOrEmpty(_currentRecordingJournalPath)) { _currentRecordingJournalPath = ResolveCurrentRecordingJournalPath(); } if (File.Exists(_currentRecordingJournalPath)) { File.Delete(_currentRecordingJournalPath); } } catch (Exception ex) { Debug.LogWarning($"[自动记录] 删除当前BFI实时日志失败: {ex.Message}"); } } private void AppendCurrentRecordingJournalEntry(BFIRecordingJournalEntry entry) { try { if (entry == null) { return; } if (string.IsNullOrEmpty(_currentRecordingJournalPath)) { _currentRecordingJournalPath = ResolveCurrentRecordingJournalPath(); } string directory = Path.GetDirectoryName(_currentRecordingJournalPath); if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) { Directory.CreateDirectory(directory); } string line = JsonUtility.ToJson(entry, false) + Environment.NewLine; byte[] bytes = Encoding.UTF8.GetBytes(line); using (var stream = new FileStream(_currentRecordingJournalPath, FileMode.Append, FileAccess.Write, FileShare.Read, 4096)) { stream.Write(bytes, 0, bytes.Length); FlushJournalStream(stream); } } catch (Exception ex) { Debug.LogWarning($"[自动记录] 写入当前BFI实时日志失败: {ex.Message}"); } } private void FlushJournalStream(FileStream stream) { try { stream.Flush(true); } catch { stream.Flush(); } } private BFIRecordingJournalEntry CreateJournalStartEntry(BFITestRecord record) { return new BFIRecordingJournalEntry { Type = JOURNAL_ENTRY_START, TestId = record.TestId, TestName = record.TestName, PatientId = record.PatientId, PatientName = record.PatientName, PatientGender = record.PatientGender, PatientAge = record.PatientAge, PatientHeight = record.PatientHeight, PatientWeight = record.PatientWeight, LowThreshold = record.LowThreshold, HighThreshold = record.HighThreshold, AlarmPriority = record.AlarmPriority, AlarmEnabled = record.AlarmEnabled, StartTimeTicks = record.StartTimeTicks }; } private BFIRecordingJournalEntry CreateJournalPointEntry(BFIDataPoint point) { return new BFIRecordingJournalEntry { Type = JOURNAL_ENTRY_POINT, TestId = _currentTestRecord?.TestId, StartTimeTicks = _currentTestRecord?.StartTimeTicks ?? 0, TimestampTicks = point.TimestampTicks, BFI = point.BFI, Status = point.Status, Notes = point.Notes, AlarmLevel = point.AlarmLevel }; } private BFIRecordingJournalEntry CreateJournalPatientEntry(BFITestRecord record) { return new BFIRecordingJournalEntry { Type = JOURNAL_ENTRY_PATIENT, TestId = record.TestId, StartTimeTicks = record.StartTimeTicks, PatientId = record.PatientId, PatientName = record.PatientName, PatientGender = record.PatientGender, PatientAge = record.PatientAge, PatientHeight = record.PatientHeight, PatientWeight = record.PatientWeight }; } private BFIRecordingJournalEntry CreateJournalEndEntry(BFITestRecord record) { return new BFIRecordingJournalEntry { Type = JOURNAL_ENTRY_END, TestId = record.TestId, StartTimeTicks = record.StartTimeTicks, EndTimeTicks = record.EndTimeTicks }; } /// /// 获取当前测试记录状态 /// public bool IsRecordingBFITest() { return _isRecordingTest; } /// /// 获取当前测试记录信息 /// public BFITestRecord GetCurrentTestRecord() { return _currentTestRecord; } /// /// 获取当前测试数据点数量 /// public int GetCurrentTestDataCount() { return _currentTestData?.Count ?? 0; } #endregion } [System.Serializable] public class BFIRecordingJournalEntry { public string Type; public string TestId; public string TestName; public string PatientId; public string PatientName; public string PatientGender; public int PatientAge; public float PatientHeight; public float PatientWeight; public float LowThreshold; public float HighThreshold; public int AlarmPriority; public bool AlarmEnabled; public long StartTimeTicks; public long EndTimeTicks; public long TimestampTicks; public float BFI; public string Status; public string Notes; public int AlarmLevel; }