리크 테스트 gui
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

196 lines
7.7 KiB

using System;
using System.Diagnostics;
using System.Timers;
1 month ago
using Timer = System.Timers.Timer;
using leak_test_project.Infrastructure;
using leak_test_project.Utils;
namespace leak_test_project.Services
{
/// <summary>
/// Sentinel C28 기기와의 통신 프로토콜을 관리하는 서비스.
/// 자동 재연결(Auto-Reconnect) 및 예외 처리 포함.
/// </summary>
public class SentinelC28Service : IDisposable
{
private readonly ICommunication _communication;
private int _sequence = 1;
private Timer _reconnectTimer;
private bool _shouldBeConnected = false;
public event EventHandler<string> RawDataReceived;
public event EventHandler<string> ResultReceived;
public event EventHandler<string> StreamingReceived;
public event EventHandler<bool> ConnectionChanged;
/// <summary> 파싱된 최종 검사 결과 알림 </summary>
public event EventHandler<Models.ParsedData> OnFinalResultParsed;
/// <summary> 파싱된 실시간 스트리밍 데이터 알림 </summary>
public event EventHandler<Models.ParsedData> OnStreamingParsed;
public SentinelC28Service(ICommunication communication)
{
_communication = communication;
_communication.DataReceived += OnDataReceived;
2 weeks ago
_communication.ConnectionStatusChanged += OnConnectionStatusChanged;
// 1초(1000ms)마다 연결 상태를 확인하고 재연결 시도
_reconnectTimer = new Timer(1000);
_reconnectTimer.AutoReset = false; // 재진입 방지
_reconnectTimer.Elapsed += (s, e) => {
if (_shouldBeConnected && !_communication.IsOpen)
{
Debug.WriteLine($"[SentinelC28 Service] Attempting to reconnect to {_communication.Name}...");
Console.WriteLine($"[Service] Attempting to reconnect to {_communication.Name}...");
if (!_communication.Open())
{
// 실패 시 다시 타이머 시작
if (_shouldBeConnected) _reconnectTimer.Start();
}
}
else if (_shouldBeConnected)
{
_reconnectTimer.Start();
}
};
}
2 weeks ago
private void OnConnectionStatusChanged(object sender, bool isConnected)
{
ConnectionChanged?.Invoke(this, isConnected);
if (!isConnected && _shouldBeConnected) StartReconnectTimer();
}
public bool Connect()
{
_shouldBeConnected = true;
bool opened = _communication.Open();
if (!opened) StartReconnectTimer();
return opened;
}
public void Disconnect()
{
_shouldBeConnected = false;
_reconnectTimer.Stop();
_communication.Close();
}
private void StartReconnectTimer()
{
if (!_reconnectTimer.Enabled) _reconnectTimer.Start();
}
public void SendCommand(string command, string dataTypeCode)
{
if (!_communication.IsOpen) return;
try {
string sequenceHex = _sequence.ToString("X2");
string lengthHex = command.Length.ToString("X3");
string payload = $"{sequenceHex}{lengthHex} {dataTypeCode}\t{command}";
string crc = SentinelCrc8.CalculateHex(payload);
string fullCommand = $"{crc}{payload}\r\n";
Debug.WriteLine($"[SentinelC28 Service] Sending Command: {fullCommand.TrimEnd()}");
if (!_communication.Write(fullCommand))
{
FileLogger.Log("ERROR", "[SentinelC28] Failed to send command: Communication channel closed.");
}
_sequence = (_sequence >= 255) ? 1 : _sequence + 1;
} catch (Exception ex) {
FileLogger.Log("ERROR", $"[SentinelC28] Error sending command: {ex.Message}");
}
}
2 weeks ago
private readonly System.Text.StringBuilder _receiveBuffer = new System.Text.StringBuilder(8192);
private const int MaxBufferSize = 65536; // 64KB
private void OnDataReceived(object sender, string rawData)
{
try {
Debug.WriteLine($"[SentinelC28 Service] Raw Data Received: {rawData.Replace("\r", "\\r").Replace("\n", "\\n").Replace("\t", "\\t")}");
RawDataReceived?.Invoke(this, rawData);
if (string.IsNullOrEmpty(rawData)) return;
2 weeks ago
lock (_receiveBuffer)
{
2 weeks ago
if (_receiveBuffer.Length + rawData.Length > MaxBufferSize)
{
_receiveBuffer.Clear();
FileLogger.Log("WARNING", "[SentinelC28] Buffer size limit exceeded. Buffer cleared to prevent OOM.");
}
_receiveBuffer.Append(rawData);
string content = _receiveBuffer.ToString();
int splitIndex;
while ((splitIndex = GetFirstDelimiterIndex(content)) >= 0)
{
2 weeks ago
string frame = content.Substring(0, splitIndex);
content = content.Substring(splitIndex + 1);
// 버퍼 갱신 (처리한 데이터 잘라냄)
_receiveBuffer.Clear();
_receiveBuffer.Append(content);
if (string.IsNullOrWhiteSpace(frame)) continue;
// 헤더 분석 및 본문 추출
string body = SentinelParser.ExtractBody(frame, out char typeCode);
switch (typeCode)
{
case 'R': // 최종 결과 (Result Value)
ResultReceived?.Invoke(this, frame);
var finalParsed = SentinelParser.ParseFinalResult(frame);
OnFinalResultParsed?.Invoke(this, finalParsed);
break;
case 'S': // 스트리밍 데이터 (Streaming Value)
StreamingReceived?.Invoke(this, frame);
var streamParsed = SentinelParser.ParseStreamingValue(frame);
OnStreamingParsed?.Invoke(this, streamParsed);
break;
case 'M': // 일반 메시지
FileLogger.Log("INFO", $"[SentinelC28 Message] {body}");
break;
}
}
}
} catch (Exception ex) {
FileLogger.Log("ERROR", $"[SentinelC28] Error parsing received data: {ex.Message}");
}
}
2 weeks ago
private int GetFirstDelimiterIndex(string text)
{
int indexN = text.IndexOf('\n');
int indexR = text.IndexOf('\r');
if (indexN >= 0 && indexR >= 0) return Math.Min(indexN, indexR);
return indexN >= 0 ? indexN : indexR;
}
public void Dispose()
{
Disconnect();
2 weeks ago
// [Memory Leak 방지] 이벤트 핸들러 명시적 구독 해제
if (_communication != null)
{
_communication.DataReceived -= OnDataReceived;
_communication.ConnectionStatusChanged -= OnConnectionStatusChanged;
}
_reconnectTimer?.Stop();
_reconnectTimer?.Dispose();
_reconnectTimer = null;
}
}
}