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

1441 lines
50 KiB
C#
Raw Permalink Normal View History

2026-06-09 13:59:11 +08:00
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;
}