|
|
|
|
using System;
|
|
|
|
|
using System.Diagnostics;
|
|
|
|
|
using System.Globalization;
|
|
|
|
|
using System.Text.RegularExpressions;
|
|
|
|
|
using leak_test_project.Models;
|
|
|
|
|
|
|
|
|
|
namespace leak_test_project.Utils
|
|
|
|
|
{
|
|
|
|
|
public static class SentinelParser
|
|
|
|
|
{
|
|
|
|
|
// 실시간 스트리밍 데이터 정규식 (헤더 이후 본문 예: LR 0.123456 sccm)
|
|
|
|
|
private static readonly Regex BodyValueRegex = new Regex(@"(?<id>[A-Z]{2})\s+(?<value>-?\d+\.?\d*)\s+(?<unit>\w+)", RegexOptions.Compiled);
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// C28 헤더 추출 (매뉴얼 기준: XXYYZZZ + Tab/Space + H + Tab + Body)
|
|
|
|
|
/// XX: 8-Bit CRC, YY: Sequence Code, ZZZ: Data Length, H: Data Type Code
|
|
|
|
|
/// </summary>
|
|
|
|
|
public static string ExtractBody(string input, out char typeCode)
|
|
|
|
|
{
|
|
|
|
|
typeCode = '\0';
|
|
|
|
|
if (string.IsNullOrWhiteSpace(input)) return input;
|
|
|
|
|
|
|
|
|
|
// 정규식을 통해 "XXYYZZZ(7글자) + 스페이스/탭 + H(1글자) + 스페이스/탭 + 데이터" 패턴 매칭
|
|
|
|
|
// (명령-응답 프로토콜을 사용할 때 들어오는 헤더)
|
|
|
|
|
if (input.Length >= 8)
|
|
|
|
|
{
|
|
|
|
|
var match = Regex.Match(input.TrimStart(), @"^.{7}[\s\t]+([a-zA-Z])[\s\t]+(.*)$", RegexOptions.Singleline);
|
|
|
|
|
if (match.Success)
|
|
|
|
|
{
|
|
|
|
|
typeCode = char.ToUpper(match.Groups[1].Value[0]);
|
|
|
|
|
string body = match.Groups[2].Value.Trim();
|
|
|
|
|
Debug.WriteLine($"[C28 Parser] Header Match: Type={typeCode}, Body={body}");
|
|
|
|
|
return body;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 폴백(Fallback): 통신 규격 헤더 없이 'Auto Result' 설정으로 바로 데이터가 날아오는 경우
|
|
|
|
|
|
|
|
|
|
var tabs = input.Split('\t');
|
|
|
|
|
|
|
|
|
|
// 1. 탭(\t)이 7개 이상 포함되어 있다면, 이는 표 규격의 '최종 결과(Final Result)'임
|
|
|
|
|
if (tabs.Length > 7)
|
|
|
|
|
{
|
|
|
|
|
typeCode = 'R'; // Result
|
|
|
|
|
Debug.WriteLine($"[C28 Parser] Final Result pattern matched.");
|
|
|
|
|
return input.Trim();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. 레거시 포맷인 경우 (탭 분할 후 인덱스 2에 유효 데이터가 있는 형태)
|
|
|
|
|
if (tabs.Length > 2)
|
|
|
|
|
{
|
|
|
|
|
string text2 = tabs[2];
|
|
|
|
|
while (text2.IndexOf(" ") >= 0) text2 = text2.Replace(" ", " ");
|
|
|
|
|
if (text2.Trim().Split(' ').Length >= 4)
|
|
|
|
|
{
|
|
|
|
|
typeCode = 'R'; // Result
|
|
|
|
|
Debug.WriteLine($"[C28 Parser] Legacy Final Result pattern matched.");
|
|
|
|
|
return input.Trim();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. 탭이 적고 "LR 0.123 sccm" 형태의 스트리밍 값 패턴을 가진 경우
|
|
|
|
|
if (BodyValueRegex.IsMatch(input))
|
|
|
|
|
{
|
|
|
|
|
typeCode = 'S'; // Streaming
|
|
|
|
|
Debug.WriteLine($"[C28 Parser] Streaming pattern matched.");
|
|
|
|
|
return input.Trim();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 그 외 판별 불가 데이터는 일반 메시지로 간주
|
|
|
|
|
typeCode = 'M';
|
|
|
|
|
return input.Trim();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 스트리밍 값 파싱
|
|
|
|
|
/// </summary>
|
|
|
|
|
public static ParsedData ParseStreamingValue(string input)
|
|
|
|
|
{
|
|
|
|
|
var result = new ParsedData { RawData = input };
|
|
|
|
|
string body = ExtractBody(input, out char type);
|
|
|
|
|
|
|
|
|
|
// 스트리밍 데이터는 본문 내에도 탭이나 콤마가 있을 수 있음
|
|
|
|
|
var match = BodyValueRegex.Match(body);
|
|
|
|
|
if (match.Success)
|
|
|
|
|
{
|
|
|
|
|
result.MeasuredValue = double.Parse(match.Groups["value"].Value, CultureInfo.InvariantCulture);
|
|
|
|
|
result.Unit = match.Groups["unit"].Value;
|
|
|
|
|
Debug.WriteLine($"[C28 Parser] Streaming Value: {result.MeasuredValue} {result.Unit}");
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 최종 결과 데이터 파싱 (C28 매뉴얼 Appendix D 참조)
|
|
|
|
|
/// 형식: Channel# Port# Program# LinkInfo Time Date UniqueID Evaluation SPCFlag TestType TestEval TestData1 ...
|
|
|
|
|
/// 구분자: TAB (\t)
|
|
|
|
|
/// </summary>
|
|
|
|
|
public static ParsedData ParseFinalResult(string input)
|
|
|
|
|
{
|
|
|
|
|
var result = new ParsedData { RawData = input };
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
// 헤더 제거 및 필드 분리 (탭 기준)
|
|
|
|
|
string body = ExtractBody(input, out char type);
|
|
|
|
|
var fields = body.Split(new[] { '\t' }, StringSplitOptions.None);
|
|
|
|
|
|
|
|
|
|
if (fields.Length >= 8)
|
|
|
|
|
{
|
|
|
|
|
result.ChannelNo = fields[0].Trim(); // C##
|
|
|
|
|
result.ProgramNo = fields[2].Trim(); // P##
|
|
|
|
|
result.TestTime = fields[4].Trim(); // HH:MM:SS.XXX
|
|
|
|
|
result.TestDate = fields[5].Trim(); // MM/DD/YY
|
|
|
|
|
result.UniqueId = fields[6].Trim(); // ##########
|
|
|
|
|
result.SerialNo = result.UniqueId.Length > 5 ? result.UniqueId.Substring(result.UniqueId.Length - 5) : result.UniqueId;
|
|
|
|
|
|
|
|
|
|
// 판정 코드 매핑 (A=Accept, R=Reject)
|
|
|
|
|
string eval = fields[7].Trim();
|
|
|
|
|
result.Judgment = (eval == "A") ? "OK" : "NG";
|
|
|
|
|
result.SensorJudgment = eval; // 센서 원본 판정 코드 보존 (교차 검증용)
|
|
|
|
|
|
|
|
|
|
Debug.WriteLine($"[C28 Parser] Parsed Final Result - CH: {result.ChannelNo}, Prog: {result.ProgramNo}, ID: {result.SerialNo}, Eval: {result.Judgment}");
|
|
|
|
|
|
|
|
|
|
// 측정값 필드 검색 (TestData 1, 2...)
|
|
|
|
|
// 매뉴얼 예시: PLR, P, LR 0.123456 sccm
|
|
|
|
|
for (int i = 9; i < fields.Length; i++)
|
|
|
|
|
{
|
|
|
|
|
var match = BodyValueRegex.Match(fields[i]);
|
|
|
|
|
if (match.Success)
|
|
|
|
|
{
|
|
|
|
|
result.MeasuredValue = double.Parse(match.Groups["value"].Value, CultureInfo.InvariantCulture);
|
|
|
|
|
result.Unit = match.Groups["unit"].Value;
|
|
|
|
|
Debug.WriteLine($"[C28 Parser] Extracted Final Value: {result.MeasuredValue} {result.Unit}");
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// 레거시 파싱 로직 (ClsLeakSensor.cs 대응)
|
|
|
|
|
// ExtractBody나 input.Trim() 등에 의해 앞선 탭(\t)이 제거되어 fields 배열의 길이가 짧을 수 있음.
|
|
|
|
|
// 원래 데이터가 fields[2]에 있었거나, 파싱되어 fields[0] 또는 마지막 요소에 남은 경우를 대비.
|
|
|
|
|
string text2 = fields.Length > 2 ? fields[2] : fields[fields.Length - 1];
|
|
|
|
|
|
|
|
|
|
while (text2.IndexOf(" ") >= 0)
|
|
|
|
|
{
|
|
|
|
|
text2 = text2.Replace(" ", " ");
|
|
|
|
|
}
|
|
|
|
|
text2 = text2.Trim();
|
|
|
|
|
string[] array2 = text2.Split(' ');
|
|
|
|
|
|
|
|
|
|
if (array2.Length >= 4)
|
|
|
|
|
{
|
|
|
|
|
result.SensorJudgment = array2[1];
|
|
|
|
|
// 레거시는 보통 P/F 를 사용하거나 A/R 을 사용 (P, A 모두 OK 처리)
|
|
|
|
|
result.Judgment = (result.SensorJudgment == "P" || result.SensorJudgment == "A") ? "OK" : "NG";
|
|
|
|
|
|
|
|
|
|
if (double.TryParse(array2[3], NumberStyles.Any, CultureInfo.InvariantCulture, out double measuredVal))
|
|
|
|
|
{
|
|
|
|
|
result.MeasuredValue = measuredVal;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (array2.Length > 4)
|
|
|
|
|
{
|
|
|
|
|
result.Unit = array2[4];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Debug.WriteLine($"[C28 Parser] Parsed Legacy Final Result - Eval: {result.Judgment}, Value: {result.MeasuredValue}");
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
result.SensorJudgment = "F";
|
|
|
|
|
result.Judgment = "NG";
|
|
|
|
|
result.MeasuredValue = -999.9;
|
|
|
|
|
Debug.WriteLine($"[C28 Parser] Parsed Legacy Final Result (Fail Fallback) - Eval: {result.Judgment}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine($"[C28 Parse Error] {ex.Message}");
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static string EvaluateJudgment(double value, double ul, double ll)
|
|
|
|
|
{
|
|
|
|
|
if (value >= ll && value <= ul) return "OK";
|
|
|
|
|
return "NG";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
public static class SentinelCrc8
|
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 입력된 데이터의 8비트 CRC 값을 HEX 문자열로 계산함.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="data">CRC를 계산할 문자열</param>
|
|
|
|
|
/// <returns>2자리의 HEX 문자열 (예: "A5")</returns>
|
|
|
|
|
public static string CalculateHex(string data)
|
|
|
|
|
{
|
|
|
|
|
// PDF 4페이지: 8-Bit CRC in HEX. Used for error checking.
|
|
|
|
|
// 주의: 제조사에서 제공하는 특정 다항식(Polynomial)이 있는 경우,
|
|
|
|
|
// 아래의 간단한 XOR 방식이 아닌 해당 알고리즘으로 교체해야 함.
|
|
|
|
|
byte crc = 0;
|
|
|
|
|
foreach (char c in data)
|
|
|
|
|
{
|
|
|
|
|
crc ^= (byte)c; // 현재는 기본 XOR 기반의 간단한 체크섬 예시
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2자리 16진수 문자열로 반환 (대문자)
|
|
|
|
|
return crc.ToString("X2");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|