DCS/ruiyiweiUX/Assets/Scripts/Services/DataService.cs

1441 lines
50 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<float> OnBFIValueUpdated;
// public event Action<float> OnBloodFlowValueUpdated;
// public event Action<float> 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<BFIDataPoint> _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<IDataService>(this);
Debug.Log("DataService: 已注册到ServiceLocator");
}
void Start()
{
// 初始化安卓平台配置
InitializeAndroidConfig();
_currentRecordingJournalPath = ResolveCurrentRecordingJournalPath();
RecoverCurrentRecordingJournalIfNeeded("程序启动");
// 初始化串口通信服务
InitializeSerialCommunication();
// 订阅患者信息变更事件
var patientService = ServiceLocator.Get<IPatientInfoService>();
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;
}
}
/// <summary>
/// 初始化串口通信服务
/// </summary>
private void InitializeSerialCommunication()
{
try
{
_serialCommunication = ServiceLocator.Get<ISerialCommunicationService>();
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;
}
}
/// <summary>
/// 处理设备状态数据(心跳报告)
/// 自动开始记录:收到第一个心跳就开始记录
/// </summary>
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);
}
}
/// <summary>
/// 处理通信错误
/// </summary>
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();
}
}
}
/// <summary>
/// 握手完成处理
/// </summary>
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<IPatientInfoService>();
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}");
}
}
/// <summary>
/// 初始化安卓平台配置
/// </summary>
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
/// <summary>
/// 根据平台获取合适的串口名称
/// </summary>
/// <returns>串口名称</returns>
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
}
/// <summary>
/// 根据平台获取合适的串口波特率
/// </summary>
/// <returns>波特率</returns>
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
/// <summary>
/// 患者信息变更处理
/// </summary>
private void OnPatientInfoChanged(PatientInfo patient)
{
if (_isRecordingTest && _currentTestRecord != null && patient != null)
{
UpdateCurrentRecordPatientInfo(patient.HospitalId, patient.Name, patient.Gender, patient.Age, patient.Height, patient.Weight);
}
}
/// <summary>
/// 自动开始记录(收到心跳时触发)
/// </summary>
private void AutoStartRecording()
{
if (_isRecordingTest)
{
Debug.Log("已有测试正在记录中,无需重复开始");
return;
}
// 获取当前患者信息
var patientService = ServiceLocator.Get<IPatientInfoService>();
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}");
}
/// <summary>
/// 心跳监控协程
/// </summary>
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();
}
/// <summary>
/// 更新当前记录的患者信息(当用户在记录过程中设置患者信息时)
/// </summary>
public void UpdateCurrentRecordPatientInfo(string patientId, string patientName)
{
UpdateCurrentRecordPatientInfo(patientId, patientName, string.Empty, 0, 0f, 0f);
}
/// <summary>
/// 更新当前记录的完整患者信息(当用户在记录过程中更新患者信息时)
/// </summary>
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测试记录管理
/// <summary>
/// 开始BFI测试记录
/// </summary>
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<BFIDataPoint>()
};
_currentTestData = new List<BFIDataPoint>();
_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}");
}
}
/// <summary>
/// 停止BFI测试记录并保存
/// </summary>
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<BFIDataPoint>(_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}");
}
}
/// <summary>
/// 记录BFI数据点
/// </summary>
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<BFIDataPoint>(_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<BFIRecordingJournalEntry>(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<BFIDataPoint>()
};
}
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<BFIDataPoint>();
}
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
};
}
/// <summary>
/// 获取当前测试记录状态
/// </summary>
public bool IsRecordingBFITest()
{
return _isRecordingTest;
}
/// <summary>
/// 获取当前测试记录信息
/// </summary>
public BFITestRecord GetCurrentTestRecord()
{
return _currentTestRecord;
}
/// <summary>
/// 获取当前测试数据点数量
/// </summary>
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;
}