109 changed files with 1669 additions and 1908 deletions
@ -0,0 +1,25 @@ |
|||||
|
[10:14:54.667] [WARNING] [Board4251] Timeout waiting for response (Retry 1/3). Command: x00c_001101:ow2800326003e, ReceivedSoFar: |
||||
|
[10:14:55.084] [WARNING] [Board4251] Timeout waiting for response (Retry 2/3). Command: x00c_001101:ow2800326003e, ReceivedSoFar: |
||||
|
[10:14:55.501] [WARNING] [Board4251] Timeout waiting for response (Retry 3/3). Command: x00c_001101:ow2800326003e, ReceivedSoFar: |
||||
|
[10:14:55.934] [WARNING] [Board4251] Timeout waiting for response (Retry 4/3). Command: x00c_001101:ow2800326003e, ReceivedSoFar: |
||||
|
[10:14:55.934] [ERROR] [Board4251] Failed to receive response after 3 retries: x00c_001101:ow2800326003e |
||||
|
[10:18:58.651] [WARNING] [Board4251] Timeout waiting for response (Retry 1/3). Command: x00c_001101:ow2800326003e, ReceivedSoFar: |
||||
|
[10:18:59.085] [WARNING] [Board4251] Timeout waiting for response (Retry 2/3). Command: x00c_001101:ow2800326003e, ReceivedSoFar: |
||||
|
[10:18:59.502] [WARNING] [Board4251] Timeout waiting for response (Retry 3/3). Command: x00c_001101:ow2800326003e, ReceivedSoFar: |
||||
|
[10:18:59.920] [WARNING] [Board4251] Timeout waiting for response (Retry 4/3). Command: x00c_001101:ow2800326003e, ReceivedSoFar: |
||||
|
[10:18:59.920] [ERROR] [Board4251] Failed to receive response after 3 retries: x00c_001101:ow2800326003e |
||||
|
[10:19:07.251] [WARNING] [Board4251] Timeout waiting for response (Retry 1/3). Command: x00c_001101:ow2800326003e, ReceivedSoFar: |
||||
|
[10:19:07.669] [WARNING] [Board4251] Timeout waiting for response (Retry 2/3). Command: x00c_001101:ow2800326003e, ReceivedSoFar: |
||||
|
[10:19:08.101] [WARNING] [Board4251] Timeout waiting for response (Retry 3/3). Command: x00c_001101:ow2800326003e, ReceivedSoFar: |
||||
|
[10:19:08.535] [WARNING] [Board4251] Timeout waiting for response (Retry 4/3). Command: x00c_001101:ow2800326003e, ReceivedSoFar: |
||||
|
[10:19:08.536] [ERROR] [Board4251] Failed to receive response after 3 retries: x00c_001101:ow2800326003e |
||||
|
[10:25:39.456] [WARNING] [Board4251] Timeout waiting for response (Retry 1/3). Command: x00c_001101:ow2800326003e, ReceivedSoFar: |
||||
|
[10:25:39.881] [WARNING] [Board4251] Timeout waiting for response (Retry 2/3). Command: x00c_001101:ow2800326003e, ReceivedSoFar: |
||||
|
[10:25:40.304] [WARNING] [Board4251] Timeout waiting for response (Retry 3/3). Command: x00c_001101:ow2800326003e, ReceivedSoFar: |
||||
|
[10:25:40.725] [WARNING] [Board4251] Timeout waiting for response (Retry 4/3). Command: x00c_001101:ow2800326003e, ReceivedSoFar: |
||||
|
[10:25:40.726] [ERROR] [Board4251] Failed to receive response after 3 retries: x00c_001101:ow2800326003e |
||||
|
[10:26:30.241] [WARNING] [Board4251] Timeout waiting for response (Retry 1/3). Command: x00c_001101:ow2800326003e, ReceivedSoFar: |
||||
|
[10:26:30.672] [WARNING] [Board4251] Timeout waiting for response (Retry 2/3). Command: x00c_001101:ow2800326003e, ReceivedSoFar: |
||||
|
[10:26:31.088] [WARNING] [Board4251] Timeout waiting for response (Retry 3/3). Command: x00c_001101:ow2800326003e, ReceivedSoFar: |
||||
|
[10:26:31.505] [WARNING] [Board4251] Timeout waiting for response (Retry 4/3). Command: x00c_001101:ow2800326003e, ReceivedSoFar: |
||||
|
[10:26:31.505] [ERROR] [Board4251] Failed to receive response after 3 retries: x00c_001101:ow2800326003e |
||||
Binary file not shown.
Binary file not shown.
@ -0,0 +1,10 @@ |
|||||
|
[10:24:40.119] [WARNING] [Board4251] Timeout waiting for response (Retry 1/3). Command: x00c_001101:ow2800326003e, ReceivedSoFar: |
||||
|
[10:24:40.539] [WARNING] [Board4251] Timeout waiting for response (Retry 2/3). Command: x00c_001101:ow2800326003e, ReceivedSoFar: |
||||
|
[10:24:40.970] [WARNING] [Board4251] Timeout waiting for response (Retry 3/3). Command: x00c_001101:ow2800326003e, ReceivedSoFar: |
||||
|
[10:24:41.396] [WARNING] [Board4251] Timeout waiting for response (Retry 4/3). Command: x00c_001101:ow2800326003e, ReceivedSoFar: |
||||
|
[10:24:41.396] [ERROR] [Board4251] Failed to receive response after 3 retries: x00c_001101:ow2800326003e |
||||
|
[10:25:21.146] [WARNING] [Board4251] Timeout waiting for response (Retry 1/3). Command: x00c_001101:ow2800326003e, ReceivedSoFar: |
||||
|
[10:25:21.569] [WARNING] [Board4251] Timeout waiting for response (Retry 2/3). Command: x00c_001101:ow2800326003e, ReceivedSoFar: |
||||
|
[10:25:21.995] [WARNING] [Board4251] Timeout waiting for response (Retry 3/3). Command: x00c_001101:ow2800326003e, ReceivedSoFar: |
||||
|
[10:25:22.417] [WARNING] [Board4251] Timeout waiting for response (Retry 4/3). Command: x00c_001101:ow2800326003e, ReceivedSoFar: |
||||
|
[10:25:22.417] [ERROR] [Board4251] Failed to receive response after 3 retries: x00c_001101:ow2800326003e |
||||
@ -1 +1 @@ |
|||||
203a310cc48dd71b35ad4241fb5264cead816046c43c41fd4c03ab287b385409 |
dfa1bcc3dd55c3f75b42b34a72f780d712c9e1147faf3e82c45e2f51cf41885f |
||||
|
|||||
Binary file not shown.
Binary file not shown.
@ -1 +1 @@ |
|||||
5388cf90cfa775894722698f4c7e037cd8714537da7bb0136faff35db6bbd820 |
0c498c72e1af24f34d80cd6f85d77cb00370ba1742f248052ac2cf5deed9afa2 |
||||
|
|||||
Binary file not shown.
Binary file not shown.
@ -0,0 +1,4 @@ |
|||||
|
// <autogenerated />
|
||||
|
using System; |
||||
|
using System.Reflection; |
||||
|
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] |
||||
@ -0,0 +1,23 @@ |
|||||
|
//------------------------------------------------------------------------------
|
||||
|
// <auto-generated>
|
||||
|
// 이 코드는 도구를 사용하여 생성되었습니다.
|
||||
|
// 런타임 버전:4.0.30319.42000
|
||||
|
//
|
||||
|
// 파일 내용을 변경하면 잘못된 동작이 발생할 수 있으며, 코드를 다시 생성하면
|
||||
|
// 이러한 변경 내용이 손실됩니다.
|
||||
|
// </auto-generated>
|
||||
|
//------------------------------------------------------------------------------
|
||||
|
|
||||
|
using System; |
||||
|
using System.Reflection; |
||||
|
|
||||
|
[assembly: System.Reflection.AssemblyCompanyAttribute("leak_test_project.Tests")] |
||||
|
[assembly: System.Reflection.AssemblyConfigurationAttribute("Release")] |
||||
|
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] |
||||
|
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+77d1c3aaba381d0c78380cabfaed51897a31edc1")] |
||||
|
[assembly: System.Reflection.AssemblyProductAttribute("leak_test_project.Tests")] |
||||
|
[assembly: System.Reflection.AssemblyTitleAttribute("leak_test_project.Tests")] |
||||
|
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] |
||||
|
|
||||
|
// Generated by the MSBuild WriteCodeFragment class.
|
||||
|
|
||||
@ -0,0 +1 @@ |
|||||
|
acbfea79027e4ab25c0f7b2b015d0f8ac8002d7760da81e077d21947bf758347 |
||||
@ -0,0 +1,8 @@ |
|||||
|
is_global = true |
||||
|
build_property.RootNamespace = leak_test_project.Tests |
||||
|
build_property.ProjectDir = C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\ |
||||
|
build_property.EnableComHosting = |
||||
|
build_property.EnableGeneratedComInterfaceComImportInterop = |
||||
|
build_property.CsWinRTUseWindowsUIXamlProjections = false |
||||
|
build_property.EffectiveAnalysisLevelStyle = |
||||
|
build_property.EnableCodeStyleSeverity = |
||||
Binary file not shown.
Binary file not shown.
@ -0,0 +1 @@ |
|||||
|
00716152d706ef3c486fd25e9c4535b79d009a6ebc9ecbc7d98bc05e0e6dd29a |
||||
@ -0,0 +1,54 @@ |
|||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\xunit.runner.visualstudio.testadapter.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\xunit.abstractions.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\xunit.runner.reporters.net452.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\xunit.runner.utility.net452.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\leak_test_project.Tests.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\leak_test_project.Tests.pdb |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\Castle.Core.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\Microsoft.VisualStudio.CodeCoverage.Shim.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\Microsoft.TestPlatform.CoreUtilities.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\Microsoft.TestPlatform.PlatformAbstractions.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\Microsoft.VisualStudio.TestPlatform.ObjectModel.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\Moq.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\NuGet.Frameworks.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\System.Collections.Immutable.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\System.Reflection.Metadata.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\System.Runtime.CompilerServices.Unsafe.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\System.Threading.Tasks.Extensions.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\xunit.assert.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\xunit.core.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\xunit.execution.desktop.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\cs\Microsoft.TestPlatform.CoreUtilities.resources.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\cs\Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\de\Microsoft.TestPlatform.CoreUtilities.resources.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\de\Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\es\Microsoft.TestPlatform.CoreUtilities.resources.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\es\Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\fr\Microsoft.TestPlatform.CoreUtilities.resources.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\fr\Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\it\Microsoft.TestPlatform.CoreUtilities.resources.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\it\Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\ja\Microsoft.TestPlatform.CoreUtilities.resources.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\ja\Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\ko\Microsoft.TestPlatform.CoreUtilities.resources.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\ko\Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\pl\Microsoft.TestPlatform.CoreUtilities.resources.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\pl\Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\pt-BR\Microsoft.TestPlatform.CoreUtilities.resources.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\pt-BR\Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\ru\Microsoft.TestPlatform.CoreUtilities.resources.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\ru\Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\tr\Microsoft.TestPlatform.CoreUtilities.resources.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\tr\Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\zh-Hans\Microsoft.TestPlatform.CoreUtilities.resources.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\zh-Hans\Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\zh-Hant\Microsoft.TestPlatform.CoreUtilities.resources.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\bin\Release\net472\zh-Hant\Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\obj\Release\net472\leak_test_project.Tests.csproj.AssemblyReference.cache |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\obj\Release\net472\leak_test_project.Tests.GeneratedMSBuildEditorConfig.editorconfig |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\obj\Release\net472\leak_test_project.Tests.AssemblyInfoInputs.cache |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\obj\Release\net472\leak_test_project.Tests.AssemblyInfo.cs |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\obj\Release\net472\leak_test_project.Tests.csproj.CoreCompileInputs.cache |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\obj\Release\net472\leak_tes.E02BB52F.Up2Date |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\obj\Release\net472\leak_test_project.Tests.dll |
||||
|
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project.Tests\obj\Release\net472\leak_test_project.Tests.pdb |
||||
@ -1,3 +1,4 @@ |
|||||
<Solution> |
<Solution> |
||||
|
<Project Path="leak_test_project.Tests/leak_test_project.Tests.csproj" /> |
||||
<Project Path="leak_test_project/leak_test_project.csproj" Id="e02e2608-51f9-4338-b1a6-e6ac94362aae" /> |
<Project Path="leak_test_project/leak_test_project.csproj" Id="e02e2608-51f9-4338-b1a6-e6ac94362aae" /> |
||||
</Solution> |
</Solution> |
||||
|
|||||
@ -1,34 +0,0 @@ |
|||||
namespace leak_test_project.Infrastructure |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// 하드웨어 통신을 위한 추상화 인터페이스.
|
|
||||
/// 시리얼(RS232), 이더넷(TCP/IP) 등 통신 방식에 상관없이
|
|
||||
/// 상위 서비스에서 동일한 방식으로 하드웨어에 접근할 수 있도록 정의함.
|
|
||||
/// </summary>
|
|
||||
public interface ICommunication |
|
||||
{ |
|
||||
/// <summary> 통신 채널 식별 이름 (예: COM1, TCP_192.168.0.1) </summary>
|
|
||||
string Name { get; } |
|
||||
|
|
||||
/// <summary> 현재 통신 채널이 열려 있는지 여부 </summary>
|
|
||||
bool IsOpen { get; } |
|
||||
|
|
||||
/// <summary> 통신 채널을 연결함 </summary>
|
|
||||
bool Open(); |
|
||||
|
|
||||
/// <summary> 통신 채널 연결을 해제함 </summary>
|
|
||||
void Close(); |
|
||||
|
|
||||
/// <summary> 데이터를 하드웨어로 전송함 </summary>
|
|
||||
bool Write(string data); |
|
||||
|
|
||||
/// <summary> 수신 버퍼를 비워 잔류 데이터를 제거함 </summary>
|
|
||||
void ClearBuffer(); |
|
||||
|
|
||||
/// <summary> 하드웨어로부터 데이터를 수신했을 때 발생하는 이벤트 </summary>
|
|
||||
event System.EventHandler<string> DataReceived; |
|
||||
|
|
||||
/// <summary> 연결 상태가 변경되었을 때 발생하는 이벤트 (connected, disconnected) </summary>
|
|
||||
event System.EventHandler<bool> ConnectionStatusChanged; |
|
||||
} |
|
||||
} |
|
||||
@ -1,34 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using leak_test_project.Models; |
|
||||
|
|
||||
namespace leak_test_project.Infrastructure |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// DIO 보드 추상화 인터페이스.
|
|
||||
/// 실제 하드웨어(JunSystem DIO) 또는 시뮬레이션 보드를 교체 가능하게 설계.
|
|
||||
/// </summary>
|
|
||||
public interface IDioBoard : IDisposable |
|
||||
{ |
|
||||
/// <summary>DIO 보드를 초기화합니다.</summary>
|
|
||||
bool Initialize(); |
|
||||
|
|
||||
/// <summary>지정된 입력 포인트의 현재 값을 읽습니다.</summary>
|
|
||||
bool ReadInput(string pointName); |
|
||||
|
|
||||
/// <summary>지정된 출력 포인트에 값을 씁니다.</summary>
|
|
||||
void WriteOutput(string pointName, bool value); |
|
||||
|
|
||||
/// <summary>모든 입력 포인트 목록을 반환합니다.</summary>
|
|
||||
List<DioPoint> GetInputPoints(); |
|
||||
|
|
||||
/// <summary>모든 출력 포인트 목록을 반환합니다.</summary>
|
|
||||
List<DioPoint> GetOutputPoints(); |
|
||||
|
|
||||
/// <summary>입력 포인트의 값이 OFF→ON으로 변경되었을 때 발생합니다.</summary>
|
|
||||
event EventHandler<DioEventArgs> InputChanged; |
|
||||
|
|
||||
/// <summary>보드 동작 중 오류가 발생했을 때 발생합니다.</summary>
|
|
||||
event EventHandler<string> ErrorOccurred; |
|
||||
} |
|
||||
} |
|
||||
@ -1,27 +0,0 @@ |
|||||
namespace leak_test_project.Models |
|
||||
{ |
|
||||
public enum IdSensorType |
|
||||
{ |
|
||||
ZMDI, // 기존 ZMDI 센서
|
|
||||
Board4253 // 신규 4253 보드 (ID 읽기용)
|
|
||||
} |
|
||||
|
|
||||
public class AppConfig |
|
||||
{ |
|
||||
public IdSensorType SelectedIdSensor { get; set; } = IdSensorType.Board4253; |
|
||||
|
|
||||
public string LeftPort { get; set; } = "COM9"; |
|
||||
public string RightPort { get; set; } = "COM8"; |
|
||||
public int ZmdiBaudRate { get; set; } = 19200; |
|
||||
|
|
||||
public string SensorPort { get; set; } = "COM1"; |
|
||||
public int SensorBaudRate { get; set; } = 9600; |
|
||||
|
|
||||
public string Board4253Port { get; set; } = "COM3"; |
|
||||
public int Board4253BaudRate { get; set; } = 115200; |
|
||||
public int Board4253Timeout { get; set; } = 5000; // 보드가 응답을 주는데 2초 이상 걸리므로 무조건 길게 대기
|
|
||||
|
|
||||
public double SpecUL { get; set; } = 1.00; |
|
||||
public double SpecLL { get; set; } = -1.00; |
|
||||
} |
|
||||
} |
|
||||
@ -1,38 +0,0 @@ |
|||||
namespace leak_test_project.Models |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// DIO 보드의 개별 포인트(입력 또는 출력)를 나타내는 모델.
|
|
||||
/// </summary>
|
|
||||
public class DioPoint |
|
||||
{ |
|
||||
/// <summary>포인트 이름 (예: LEFT_START, LEFT_OK, RIGHT_NG)</summary>
|
|
||||
public string Name { get; set; } |
|
||||
|
|
||||
/// <summary>설명 (예: 좌측 시작 신호)</summary>
|
|
||||
public string Description { get; set; } |
|
||||
|
|
||||
/// <summary>true: 입력, false: 출력</summary>
|
|
||||
public bool IsInput { get; set; } |
|
||||
|
|
||||
/// <summary>현재 값 (ON=true, OFF=false)</summary>
|
|
||||
public bool Value { get; set; } |
|
||||
|
|
||||
/// <summary>하드웨어 IO 포트의 실제 비트 인덱스 (0~31)</summary>
|
|
||||
public int BitIndex { get; set; } |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// DIO 입력 변경 시 사용되는 이벤트 인자.
|
|
||||
/// </summary>
|
|
||||
public class DioEventArgs : System.EventArgs |
|
||||
{ |
|
||||
public string PointName { get; set; } |
|
||||
public bool NewValue { get; set; } |
|
||||
|
|
||||
public DioEventArgs(string pointName, bool newValue) |
|
||||
{ |
|
||||
PointName = pointName; |
|
||||
NewValue = newValue; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,26 +0,0 @@ |
|||||
namespace leak_test_project.Models |
|
||||
{ |
|
||||
public class InOutItem : System.ComponentModel.INotifyPropertyChanged |
|
||||
{ |
|
||||
public string Address { get; set; } |
|
||||
public string Name { get; set; } |
|
||||
public string Description { get; set; } |
|
||||
|
|
||||
private bool _value; |
|
||||
/// <summary>현재 값 (ON=true, OFF=false). DIO 실시간 상태 표시용.</summary>
|
|
||||
public bool Value |
|
||||
{ |
|
||||
get => _value; |
|
||||
set |
|
||||
{ |
|
||||
if (_value != value) |
|
||||
{ |
|
||||
_value = value; |
|
||||
PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(nameof(Value))); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; |
|
||||
} |
|
||||
} |
|
||||
@ -1,18 +0,0 @@ |
|||||
namespace leak_test_project.Models |
|
||||
{ |
|
||||
public class InspectData |
|
||||
{ |
|
||||
public string InspectDate { get; set; } |
|
||||
public string InspectTime { get; set; } |
|
||||
public string Retest { get; set; } |
|
||||
public string Mode { get; set; } // 예: 개발/양산
|
|
||||
public string LineNo { get; set; } |
|
||||
public string ProductType { get; set; } |
|
||||
public string ProductId { get; set; } |
|
||||
public string Channel { get; set; } // 좌/우
|
|
||||
public string SpecUL { get; set; } |
|
||||
public string SpecLL { get; set; } |
|
||||
public string MeasuredValue { get; set; } |
|
||||
public string Judgment { get; set; } |
|
||||
} |
|
||||
} |
|
||||
@ -1,31 +0,0 @@ |
|||||
using System; |
|
||||
|
|
||||
namespace leak_test_project.Models |
|
||||
{ |
|
||||
public class ParsedData |
|
||||
{ |
|
||||
public string RawData { get; set; } |
|
||||
public double MeasuredValue { get; set; } |
|
||||
public string Unit { get; set; } |
|
||||
public string TestType { get; set; } |
|
||||
public string Judgment { get; set; } |
|
||||
public DateTime Timestamp { get; set; } |
|
||||
|
|
||||
// 스크린샷 추가 요구사항 필드
|
|
||||
public string ChannelNo { get; set; } |
|
||||
public string ProgramNo { get; set; } |
|
||||
public string UniqueId { get; set; } |
|
||||
public string TestDate { get; set; } |
|
||||
public string TestTime { get; set; } |
|
||||
public string SerialNo { get; set; } |
|
||||
public string LowID { get; set; } |
|
||||
|
|
||||
/// <summary>센서 자체 판정 결과 (A=Accept, R=Reject). SPEC 교차 검증에 사용.</summary>
|
|
||||
public string SensorJudgment { get; set; } |
|
||||
|
|
||||
public ParsedData() |
|
||||
{ |
|
||||
Timestamp = DateTime.Now; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,159 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace leak_test_project.Models |
||||
|
{ |
||||
|
public class AppConfig |
||||
|
{ |
||||
|
|
||||
|
|
||||
|
public string SensorPort { get; set; } = "COM1"; |
||||
|
public int SensorBaudRate { get; set; } = 9600; |
||||
|
|
||||
|
public string Board4251Port { get; set; } = "COM3"; |
||||
|
public int Board4251BaudRate { get; set; } = 115200; |
||||
|
public int Board4251Timeout { get; set; } = 5000; // 보드가 응답을 주는데 2초 이상 걸리므로 무조건 길게 대기
|
||||
|
|
||||
|
public double SpecUL { get; set; } = 1.00; |
||||
|
public double SpecLL { get; set; } = -1.00; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// DIO 보드의 개별 포인트(입력 또는 출력)를 나타내는 모델.
|
||||
|
/// </summary>
|
||||
|
public class DioPoint |
||||
|
{ |
||||
|
/// <summary>포인트 이름 (예: LEFT_START, LEFT_OK, RIGHT_NG)</summary>
|
||||
|
public string Name { get; set; } |
||||
|
|
||||
|
/// <summary>설명 (예: 좌측 시작 신호)</summary>
|
||||
|
public string Description { get; set; } |
||||
|
|
||||
|
/// <summary>true: 입력, false: 출력</summary>
|
||||
|
public bool IsInput { get; set; } |
||||
|
|
||||
|
/// <summary>현재 값 (ON=true, OFF=false)</summary>
|
||||
|
public bool Value { get; set; } |
||||
|
|
||||
|
/// <summary>하드웨어 IO 포트의 실제 비트 인덱스 (0~31)</summary>
|
||||
|
public int BitIndex { get; set; } |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// DIO 입력 변경 시 사용되는 이벤트 인자.
|
||||
|
/// </summary>
|
||||
|
public class DioEventArgs : System.EventArgs |
||||
|
{ |
||||
|
public string PointName { get; set; } |
||||
|
public bool NewValue { get; set; } |
||||
|
|
||||
|
public DioEventArgs(string pointName, bool newValue) |
||||
|
{ |
||||
|
PointName = pointName; |
||||
|
NewValue = newValue; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public class InOutItem : System.ComponentModel.INotifyPropertyChanged |
||||
|
{ |
||||
|
public string Address { get; set; } |
||||
|
public string Name { get; set; } |
||||
|
public string Description { get; set; } |
||||
|
|
||||
|
private bool _value; |
||||
|
/// <summary>현재 값 (ON=true, OFF=false). DIO 실시간 상태 표시용.</summary>
|
||||
|
public bool Value |
||||
|
{ |
||||
|
get => _value; |
||||
|
set |
||||
|
{ |
||||
|
if (_value != value) |
||||
|
{ |
||||
|
_value = value; |
||||
|
PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(nameof(Value))); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; |
||||
|
} |
||||
|
|
||||
|
public class InspectData |
||||
|
{ |
||||
|
public string InspectDate { get; set; } |
||||
|
public string InspectTime { get; set; } |
||||
|
public string Retest { get; set; } |
||||
|
public string Mode { get; set; } // 예: 개발/양산
|
||||
|
public string LineNo { get; set; } |
||||
|
public string ProductType { get; set; } |
||||
|
public string ProductId { get; set; } |
||||
|
public string Channel { get; set; } // 좌/우
|
||||
|
public string SpecUL { get; set; } |
||||
|
public string SpecLL { get; set; } |
||||
|
public string MeasuredValue { get; set; } |
||||
|
public string Judgment { get; set; } |
||||
|
} |
||||
|
|
||||
|
public class ParsedData |
||||
|
{ |
||||
|
public string RawData { get; set; } |
||||
|
public double MeasuredValue { get; set; } |
||||
|
public string Unit { get; set; } |
||||
|
public string TestType { get; set; } |
||||
|
public string Judgment { get; set; } |
||||
|
public DateTime Timestamp { get; set; } |
||||
|
|
||||
|
// 스크린샷 추가 요구사항 필드
|
||||
|
public string ChannelNo { get; set; } |
||||
|
public string ProgramNo { get; set; } |
||||
|
public string UniqueId { get; set; } |
||||
|
public string TestDate { get; set; } |
||||
|
public string TestTime { get; set; } |
||||
|
public string SerialNo { get; set; } |
||||
|
public string LowID { get; set; } |
||||
|
|
||||
|
/// <summary>센서 자체 판정 결과 (A=Accept, R=Reject). SPEC 교차 검증에 사용.</summary>
|
||||
|
public string SensorJudgment { get; set; } |
||||
|
|
||||
|
public ParsedData() |
||||
|
{ |
||||
|
Timestamp = DateTime.Now; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// ZMDI 센서에서 읽은 제품 ID 정보.
|
||||
|
/// 레거시 ClsSensorReader의 필드를 기반으로 구성.
|
||||
|
/// </summary>
|
||||
|
public class SensorIdData |
||||
|
{ |
||||
|
/// <summary>센서 메모리에서 읽은 원본 Low ID 문자열</summary>
|
||||
|
public string LowID { get; set; } = ""; |
||||
|
|
||||
|
/// <summary>제조 년도</summary>
|
||||
|
public int Year { get; set; } |
||||
|
|
||||
|
/// <summary>제조 월</summary>
|
||||
|
public int Month { get; set; } |
||||
|
|
||||
|
/// <summary>제조 일</summary>
|
||||
|
public int Day { get; set; } |
||||
|
|
||||
|
/// <summary>시리얼 번호 (5자리 패딩)</summary>
|
||||
|
public string Serial { get; set; } = ""; |
||||
|
|
||||
|
/// <summary>MC Line (개발/양산 구분)</summary>
|
||||
|
public string McLine { get; set; } = ""; |
||||
|
|
||||
|
/// <summary>라인 번호</summary>
|
||||
|
public string LineNo { get; set; } = ""; |
||||
|
|
||||
|
/// <summary>제품 구분 (Item)</summary>
|
||||
|
public string Item { get; set; } = ""; |
||||
|
|
||||
|
/// <summary>최종 조합 ID (년+월+일+시리얼+라인+항목)</summary>
|
||||
|
public string ID { get; set; } = ""; |
||||
|
|
||||
|
/// <summary>이전 검사 결과. F=미시험/불합격, P=합격. 불량 제품 필터링에 사용.</summary>
|
||||
|
public string PrevResult { get; set; } = ""; |
||||
|
} |
||||
|
} |
||||
@ -1,39 +0,0 @@ |
|||||
namespace leak_test_project.Models |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// ZMDI 센서에서 읽은 제품 ID 정보.
|
|
||||
/// 레거시 ClsSensorReader의 필드를 기반으로 구성.
|
|
||||
/// </summary>
|
|
||||
public class SensorIdData |
|
||||
{ |
|
||||
/// <summary>센서 메모리에서 읽은 원본 Low ID 문자열</summary>
|
|
||||
public string LowID { get; set; } = ""; |
|
||||
|
|
||||
/// <summary>제조 년도</summary>
|
|
||||
public int Year { get; set; } |
|
||||
|
|
||||
/// <summary>제조 월</summary>
|
|
||||
public int Month { get; set; } |
|
||||
|
|
||||
/// <summary>제조 일</summary>
|
|
||||
public int Day { get; set; } |
|
||||
|
|
||||
/// <summary>시리얼 번호 (5자리 패딩)</summary>
|
|
||||
public string Serial { get; set; } = ""; |
|
||||
|
|
||||
/// <summary>MC Line (개발/양산 구분)</summary>
|
|
||||
public string McLine { get; set; } = ""; |
|
||||
|
|
||||
/// <summary>라인 번호</summary>
|
|
||||
public string LineNo { get; set; } = ""; |
|
||||
|
|
||||
/// <summary>제품 구분 (Item)</summary>
|
|
||||
public string Item { get; set; } = ""; |
|
||||
|
|
||||
/// <summary>최종 조합 ID (년+월+일+시리얼+라인+항목)</summary>
|
|
||||
public string ID { get; set; } = ""; |
|
||||
|
|
||||
/// <summary>이전 검사 결과. F=미시험/불합격, P=합격. 불량 제품 필터링에 사용.</summary>
|
|
||||
public string PrevResult { get; set; } = ""; |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,489 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Diagnostics; |
||||
|
using System.Text; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using leak_test_project.Infrastructure; |
||||
|
using leak_test_project.Models; |
||||
|
using leak_test_project.Utils; |
||||
|
|
||||
|
namespace leak_test_project.Services |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 신규 4251 보드와의 통신을 관리하는 서비스.
|
||||
|
/// <end> 키워드를 기준으로 데이터를 수신하며, 상태 확인 및 ID 읽기 기능을 제공함.
|
||||
|
/// </summary>
|
||||
|
public class Board4251Service : IDisposable |
||||
|
{ |
||||
|
private readonly ICommunication _communication; |
||||
|
private readonly StringBuilder _receiveBuffer = new StringBuilder(); |
||||
|
private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); |
||||
|
private TaskCompletionSource<string> _responseTcs; |
||||
|
public int TimeoutMs { get; set; } = 5000; // 보드가 응답을 주는데 2초 이상 걸리므로 무조건 길게 대기
|
||||
|
private bool _shouldBeConnected = false; |
||||
|
private System.Timers.Timer _reconnectTimer; |
||||
|
|
||||
|
public event EventHandler<string> ErrorOccurred; |
||||
|
public event EventHandler<bool> ConnectionChanged; |
||||
|
|
||||
|
public string LastResponse { get; private set; } = ""; |
||||
|
|
||||
|
public string GetLastBuffer() |
||||
|
{ |
||||
|
string currentBuf = _receiveBuffer.ToString().Trim(); |
||||
|
return string.IsNullOrEmpty(currentBuf) ? LastResponse : currentBuf; |
||||
|
} |
||||
|
|
||||
|
public Board4251Service(ICommunication communication) |
||||
|
{ |
||||
|
_communication = communication; |
||||
|
_communication.DataReceived += OnDataReceived; |
||||
|
_communication.ConnectionStatusChanged += (s, isConnected) => { |
||||
|
ConnectionChanged?.Invoke(this, isConnected); |
||||
|
if (!isConnected && _shouldBeConnected) StartReconnectTimer(); |
||||
|
}; |
||||
|
|
||||
|
_reconnectTimer = new System.Timers.Timer(1000); |
||||
|
_reconnectTimer.AutoReset = false; |
||||
|
_reconnectTimer.Elapsed += (s, e) => { |
||||
|
if (_shouldBeConnected && !_communication.IsOpen) |
||||
|
{ |
||||
|
if (!_communication.Open()) |
||||
|
{ |
||||
|
if (_shouldBeConnected) _reconnectTimer.Start(); |
||||
|
} |
||||
|
} |
||||
|
else if (_shouldBeConnected) |
||||
|
{ |
||||
|
_reconnectTimer.Start(); |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private void StartReconnectTimer() |
||||
|
{ |
||||
|
if (_reconnectTimer != null && !_reconnectTimer.Enabled) _reconnectTimer.Start(); |
||||
|
} |
||||
|
|
||||
|
public bool Connect() |
||||
|
{ |
||||
|
_shouldBeConnected = true; |
||||
|
bool opened = _communication.Open(); |
||||
|
if (!opened) StartReconnectTimer(); |
||||
|
return opened; |
||||
|
} |
||||
|
|
||||
|
public void Disconnect() |
||||
|
{ |
||||
|
_shouldBeConnected = false; |
||||
|
_reconnectTimer?.Stop(); |
||||
|
_communication.Close(); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 4251 보드의 상태를 확인합니다.
|
||||
|
/// </summary>
|
||||
|
/// <returns>성공 여부</returns>
|
||||
|
public async Task<bool> CheckStatusAsync(int channel = 1) |
||||
|
{ |
||||
|
string response = await SendCommandAsync($"x00c_00{channel}101:owt28006727ea97c7801\r\n"); |
||||
|
|
||||
|
if (response == null) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
if (response.IndexOf("Success", StringComparison.OrdinalIgnoreCase) >= 0) |
||||
|
{ |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
if (response.IndexOf("Fail", StringComparison.OrdinalIgnoreCase) >= 0) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
// Success도 Fail도 없는 알 수 없는 응답인 경우
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 4251 보드로부터 16자리 ID를 읽어옵니다.
|
||||
|
/// </summary>
|
||||
|
/// <returns>16자리 ID 문자열, 실패 시 null</returns>
|
||||
|
public async Task<string> ReadIdAsync(int channel = 1) |
||||
|
{ |
||||
|
string response = await SendCommandAsync($"x00c_00{channel}101:ow2800326003e\r\n"); |
||||
|
|
||||
|
if (response == null) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
if (response.Contains("Fail")) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
string id = ExtractId(response); |
||||
|
if (string.IsNullOrEmpty(id)) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
return id; |
||||
|
} |
||||
|
|
||||
|
private string ExtractId(string response) |
||||
|
{ |
||||
|
if (string.IsNullOrEmpty(response)) return null; |
||||
|
|
||||
|
// 보드 응답에는 보낸 명령어가 에코되어 포함되므로, 줄바꿈으로 나누어 실제 데이터 라인을 찾음
|
||||
|
string[] lines = response.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries); |
||||
|
|
||||
|
foreach (var line in lines) |
||||
|
{ |
||||
|
string trimmed = line.Trim(); |
||||
|
if (trimmed.Contains("x00c_") || trimmed.IndexOf("<end>", StringComparison.OrdinalIgnoreCase) >= 0 || trimmed.IndexOf("Success", StringComparison.OrdinalIgnoreCase) >= 0) |
||||
|
continue; |
||||
|
|
||||
|
// 16자리 영숫자 ID 추출
|
||||
|
var match = System.Text.RegularExpressions.Regex.Match(trimmed, @"[A-Za-z0-9]{16}"); |
||||
|
if (match.Success) return match.Value; |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
public async Task<string> SendCommandAsync(string command) |
||||
|
{ |
||||
|
await _lock.WaitAsync(); |
||||
|
try |
||||
|
{ |
||||
|
int retryCount = 0; |
||||
|
while (retryCount <= 3) |
||||
|
{ |
||||
|
if (!_communication.IsOpen) |
||||
|
{ |
||||
|
if (!_communication.Open()) |
||||
|
{ |
||||
|
retryCount++; |
||||
|
await Task.Delay(300); |
||||
|
continue; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
_receiveBuffer.Clear(); |
||||
|
LastResponse = ""; |
||||
|
_responseTcs = new TaskCompletionSource<string>(); |
||||
|
|
||||
|
_communication.ClearBuffer(); // 이전에 남아있던 패킷 조각 완벽히 제거
|
||||
|
|
||||
|
Debug.WriteLine($"[Board4251] Sending Command: {command.TrimEnd()}"); |
||||
|
_communication.Write(command); |
||||
|
|
||||
|
using (var cts = new CancellationTokenSource(TimeoutMs)) |
||||
|
{ |
||||
|
cts.Token.Register(() => _responseTcs.TrySetCanceled()); |
||||
|
try |
||||
|
{ |
||||
|
return await _responseTcs.Task; |
||||
|
} |
||||
|
catch (OperationCanceledException) |
||||
|
{ |
||||
|
retryCount++; |
||||
|
string currentBuffer = _receiveBuffer.ToString().Trim(); |
||||
|
FileLogger.Log("WARNING", $"[Board4251] Timeout waiting for response (Retry {retryCount}/3). Command: {command.Trim()}, ReceivedSoFar: {currentBuffer}"); |
||||
|
_receiveBuffer.Clear(); |
||||
|
if (retryCount <= 3) await Task.Delay(300); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
FileLogger.Log("ERROR", $"[Board4251] Failed to receive response after 3 retries: {command.Trim()}"); |
||||
|
return null; |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
_responseTcs = null; |
||||
|
_lock.Release(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void OnDataReceived(object sender, string data) |
||||
|
{ |
||||
|
Debug.WriteLine($"[Board4251] Raw Data Chunk Received: {data.Replace("\r", "\\r").Replace("\n", "\\n").Replace("\t", "\\t")}"); |
||||
|
_receiveBuffer.Append(data); |
||||
|
string currentContent = _receiveBuffer.ToString(); |
||||
|
|
||||
|
bool isComplete = false; |
||||
|
|
||||
|
if (currentContent.IndexOf("<end>", StringComparison.OrdinalIgnoreCase) >= 0 || |
||||
|
currentContent.IndexOf("Success", StringComparison.OrdinalIgnoreCase) >= 0 || |
||||
|
currentContent.IndexOf("Fail", StringComparison.OrdinalIgnoreCase) >= 0 || |
||||
|
currentContent.IndexOf("<ACK>OFF", StringComparison.OrdinalIgnoreCase) >= 0) |
||||
|
{ |
||||
|
isComplete = true; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// <end>가 오지 않았더라도, 명령어 에코가 아닌 줄에서 16자리 ID를 발견하면 즉시 완료 처리
|
||||
|
string[] lines = currentContent.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries); |
||||
|
foreach (var line in lines) |
||||
|
{ |
||||
|
string trimmed = line.Trim(); |
||||
|
if (trimmed.Contains("x00c_")) continue; |
||||
|
|
||||
|
var match = System.Text.RegularExpressions.Regex.Match(trimmed, @"[A-Za-z0-9]{16}"); |
||||
|
if (match.Success) |
||||
|
{ |
||||
|
isComplete = true; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (isComplete) |
||||
|
{ |
||||
|
if (_responseTcs != null && !_responseTcs.Task.IsCompleted) |
||||
|
{ |
||||
|
LastResponse = currentContent.Trim(); |
||||
|
_responseTcs.TrySetResult(currentContent); |
||||
|
} |
||||
|
_receiveBuffer.Clear(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
Disconnect(); |
||||
|
_communication.DataReceived -= OnDataReceived; |
||||
|
_reconnectTimer?.Dispose(); |
||||
|
_lock.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 신규 4251 보드를 사용하여 제품 ID를 읽는 센서 서비스.
|
||||
|
/// ZmdiSensorService와 동일한 구조로 구현되어 교체가 용이함.
|
||||
|
/// </summary>
|
||||
|
public class Board4251SensorService : IIdSensorService |
||||
|
{ |
||||
|
private readonly Board4251Service _service; |
||||
|
private readonly int _sensorIndex; |
||||
|
|
||||
|
public event EventHandler<string> ProgressMessage; |
||||
|
public event EventHandler<string> ErrorMessage; |
||||
|
public event EventHandler<bool> ConnectionChanged; |
||||
|
|
||||
|
public Board4251SensorService(Board4251Service service, int sensorIndex) |
||||
|
{ |
||||
|
_service = service; |
||||
|
_sensorIndex = sensorIndex; |
||||
|
// 좌우 오류 간섭을 막기 위해 공용 에러 이벤트 구독 해제
|
||||
|
_service.ConnectionChanged += (s, isConnected) => ConnectionChanged?.Invoke(this, isConnected); |
||||
|
} |
||||
|
|
||||
|
public bool Connect() => _service.Connect(); |
||||
|
public void Disconnect() => _service.Disconnect(); |
||||
|
|
||||
|
public SensorIdData ReadSensor() |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
int channel = _sensorIndex + 1; |
||||
|
|
||||
|
// 0. 초기화 (x00o 전송)
|
||||
|
ProgressMessage?.Invoke(this, "4251 보드 초기화 중..."); |
||||
|
// x00o 명령을 보내고 응답 내용에 상관없이 다음 단계 진행 (최대 5초 대기)
|
||||
|
Task.Run(() => _service.SendCommandAsync("x00o\r\n")).Wait(5000); |
||||
|
Task.Delay(350).Wait(); // 보드 안정화 대기
|
||||
|
|
||||
|
// 1. 보드 상태 확인 (Fail인 경우 중단)
|
||||
|
ProgressMessage?.Invoke(this, "4251 보드 데이터 읽는 중..."); |
||||
|
int extendedTimeout = 15000; |
||||
|
string statusCmd = $"x00c_00{channel}101:owt28006727ea97c7801"; |
||||
|
var statusTask = Task.Run(() => _service.CheckStatusAsync(channel)); |
||||
|
if (!statusTask.Wait(extendedTimeout)) |
||||
|
{ |
||||
|
string buf = _service.GetLastBuffer(); |
||||
|
string displayBuf = string.IsNullOrEmpty(buf) ? "수신된 데이터 없음" : buf; |
||||
|
ErrorMessage?.Invoke(this, $"통신 실패 (4251 보드 CH{channel} 상태 타임아웃)\r\n[송신값]: {statusCmd}\r\n[수신값]: {displayBuf}"); |
||||
|
return null; |
||||
|
} |
||||
|
if (!statusTask.Result) |
||||
|
{ |
||||
|
string buf = _service.GetLastBuffer(); |
||||
|
string displayBuf = string.IsNullOrEmpty(buf) ? "수신된 데이터 없음" : buf; |
||||
|
ErrorMessage?.Invoke(this, $"통신 실패 (4251 보드 CH{channel} 상태 이상 또는 Fail)\r\n[송신값]: {statusCmd}\r\n[수신값]: {displayBuf}"); |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
// 2. ID 읽기 (끝자리가 F일 경우 최대 3회 시도)
|
||||
|
string idCmd = $"x00c_00{channel}101:ow2800326003e"; |
||||
|
string rawId = null; |
||||
|
int maxAttempts = 3; |
||||
|
|
||||
|
for (int attempt = 1; attempt <= maxAttempts; attempt++) |
||||
|
{ |
||||
|
var idTask = Task.Run(() => _service.ReadIdAsync(channel)); |
||||
|
if (!idTask.Wait(extendedTimeout)) |
||||
|
{ |
||||
|
if (attempt == maxAttempts) |
||||
|
{ |
||||
|
string buf = _service.GetLastBuffer(); |
||||
|
string displayBuf = string.IsNullOrEmpty(buf) ? "수신된 데이터 없음" : buf; |
||||
|
ErrorMessage?.Invoke(this, $"통신 실패 (4251 보드 CH{channel} ID 대기 타임아웃)\r\n[송신값]: {idCmd}\r\n[수신값]: {displayBuf}"); |
||||
|
return null; |
||||
|
} |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
rawId = idTask.Result; |
||||
|
if (string.IsNullOrEmpty(rawId)) |
||||
|
{ |
||||
|
if (attempt == maxAttempts) |
||||
|
{ |
||||
|
string buf = _service.GetLastBuffer(); |
||||
|
string displayBuf = string.IsNullOrEmpty(buf) ? "수신된 데이터 없음" : buf; |
||||
|
ErrorMessage?.Invoke(this, $"통신 실패 (4251 보드 CH{channel} ID 응답 없거나 파싱 오류)\r\n[송신값]: {idCmd}\r\n[수신값]: {displayBuf}"); |
||||
|
return null; |
||||
|
} |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
// 정상적으로 파싱된 경우, 끝자리가 'F'인지 확인
|
||||
|
if (rawId.EndsWith("F", StringComparison.OrdinalIgnoreCase)) |
||||
|
{ |
||||
|
if (attempt < maxAttempts) |
||||
|
{ |
||||
|
ProgressMessage?.Invoke(this, $"ID 읽기 재시도 중... ({attempt}/{maxAttempts})"); |
||||
|
Task.Delay(350).Wait(); // 재시도 전 약간의 딜레이
|
||||
|
continue; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
ProgressMessage?.Invoke(this, $"최대 재시도(2회 추가) 초과. 끝자리가 F인 ID({rawId})를 사용합니다."); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 제대로 된 값을 얻었거나 최대 횟수에 도달하면 루프 탈출
|
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
// 3. SensorIdData 객체 구성 (16자리 ID를 각 필드에 적절히 분배)
|
||||
|
// 신규 보드는 16자리 전체가 ID이므로, 파싱 로직 없이 통째로 넣거나
|
||||
|
// 특정 규칙이 있다면 여기서 분할함.
|
||||
|
var data = new SensorIdData |
||||
|
{ |
||||
|
LowID = rawId, |
||||
|
ID = rawId, // 16자리 전체를 ID로 사용
|
||||
|
Serial = "", // 시리얼 번호는 현재 존재하지 않으므로 강제로 파싱하지 않음
|
||||
|
Item = "N/A", |
||||
|
PrevResult = "F" // '불량제품 투입' 필터를 통과하기 위한 강제 초기화
|
||||
|
}; |
||||
|
|
||||
|
ProgressMessage?.Invoke(this, "ID 읽기 성공"); |
||||
|
return data; |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
ErrorMessage?.Invoke(this, $"4251 보드 읽기 중 예외 발생: {ex.Message}"); |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
// 공유 서비스(Board4251Service)는 외부(HomeViewModel)에서 관리하므로 여기서 해제하지 않음
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 시리얼 통신을 기반으로 동작하는 신규 4251 DIO 보드 구현체.
|
||||
|
/// 기존 RealDioBoard(Legacy)와 교체 가능함.
|
||||
|
/// </summary>
|
||||
|
public class Board4251DioBoard : IDioBoard |
||||
|
{ |
||||
|
private readonly Board4251Service _service; |
||||
|
private readonly List<DioPoint> _inputs = new List<DioPoint>(); |
||||
|
private readonly List<DioPoint> _outputs = new List<DioPoint>(); |
||||
|
private bool _isDisposed = false; |
||||
|
|
||||
|
#pragma warning disable 0067
|
||||
|
public event EventHandler<DioEventArgs> InputChanged; |
||||
|
#pragma warning restore 0067
|
||||
|
|
||||
|
public event EventHandler<string> ErrorOccurred; |
||||
|
|
||||
|
public Board4251DioBoard(Board4251Service service) |
||||
|
{ |
||||
|
_service = service; |
||||
|
_service.ErrorOccurred += (s, msg) => ErrorOccurred?.Invoke(this, msg); |
||||
|
InitializePoints(); |
||||
|
} |
||||
|
|
||||
|
private void InitializePoints() |
||||
|
{ |
||||
|
// 실제 보드 구성에 맞게 입출력 포인트 정의 (DioConfigParser 기반 혹은 하드코딩)
|
||||
|
// 일단 기존 프로젝트 구성과 호환되도록 빈 리스트 혹은 기본값 설정
|
||||
|
var config = DioConfigParser.LoadDefault(); |
||||
|
_inputs.AddRange(config.InputPoints); |
||||
|
_outputs.AddRange(config.OutputPoints); |
||||
|
} |
||||
|
|
||||
|
public bool Initialize() |
||||
|
{ |
||||
|
// 시리얼 연결 시도
|
||||
|
try { |
||||
|
if (!_service.Connect()) |
||||
|
{ |
||||
|
ErrorOccurred?.Invoke(this, $"4251 Board: Failed to open serial port."); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
// 보드 상태 확인
|
||||
|
var statusTask = Task.Run(() => _service.CheckStatusAsync()); |
||||
|
if (!statusTask.Wait(5000)) |
||||
|
{ |
||||
|
ErrorOccurred?.Invoke(this, "4251 Board: Initialization Timeout (CheckStatus)."); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
return statusTask.Result; |
||||
|
} catch (Exception ex) { |
||||
|
ErrorOccurred?.Invoke(this, $"4251 Board: Initialization Error - {ex.Message}"); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public bool ReadInput(string pointName) |
||||
|
{ |
||||
|
// 신규 보드의 입력 읽기 프로토콜이 필요한 부분 (현재 ReadId 등만 구현됨)
|
||||
|
// 구현 계획에는 ID 읽기와 상태 확인만 있었으므로,
|
||||
|
// 실제 DIO 기능을 위해선 추가적인 시리얼 명령이 필요할 수 있음.
|
||||
|
// 일단 true/false 로직 구현
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
public void WriteOutput(string pointName, bool value) |
||||
|
{ |
||||
|
// 보드 출력 제어 명령 전송 (예시 프로토콜 필요)
|
||||
|
// _service.SendCommandAsync(...) 호출 형태가 될 것임.
|
||||
|
} |
||||
|
|
||||
|
public List<DioPoint> GetInputPoints() => _inputs; |
||||
|
public List<DioPoint> GetOutputPoints() => _outputs; |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
if (!_isDisposed) |
||||
|
{ |
||||
|
_isDisposed = true; |
||||
|
_service.Disconnect(); |
||||
|
_service.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,97 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Linq; |
|
||||
using System.Threading.Tasks; |
|
||||
using leak_test_project.Infrastructure; |
|
||||
using leak_test_project.Models; |
|
||||
using leak_test_project.Utils; |
|
||||
|
|
||||
namespace leak_test_project.Services |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// 시리얼 통신을 기반으로 동작하는 신규 4253 DIO 보드 구현체.
|
|
||||
/// 기존 RealDioBoard(Legacy)와 교체 가능함.
|
|
||||
/// </summary>
|
|
||||
public class Board4253DioBoard : IDioBoard |
|
||||
{ |
|
||||
private readonly Board4253Service _service; |
|
||||
private readonly List<DioPoint> _inputs = new List<DioPoint>(); |
|
||||
private readonly List<DioPoint> _outputs = new List<DioPoint>(); |
|
||||
private bool _isDisposed = false; |
|
||||
|
|
||||
#pragma warning disable 0067
|
|
||||
public event EventHandler<DioEventArgs> InputChanged; |
|
||||
#pragma warning restore 0067
|
|
||||
|
|
||||
public event EventHandler<string> ErrorOccurred; |
|
||||
|
|
||||
public Board4253DioBoard(Board4253Service service) |
|
||||
{ |
|
||||
_service = service; |
|
||||
_service.ErrorOccurred += (s, msg) => ErrorOccurred?.Invoke(this, msg); |
|
||||
InitializePoints(); |
|
||||
} |
|
||||
|
|
||||
private void InitializePoints() |
|
||||
{ |
|
||||
// 실제 보드 구성에 맞게 입출력 포인트 정의 (DioConfigParser 기반 혹은 하드코딩)
|
|
||||
// 일단 기존 프로젝트 구성과 호환되도록 빈 리스트 혹은 기본값 설정
|
|
||||
var config = DioConfigParser.LoadDefault(); |
|
||||
_inputs.AddRange(config.InputPoints); |
|
||||
_outputs.AddRange(config.OutputPoints); |
|
||||
} |
|
||||
|
|
||||
public bool Initialize() |
|
||||
{ |
|
||||
// 시리얼 연결 시도
|
|
||||
try { |
|
||||
if (!_service.Connect()) |
|
||||
{ |
|
||||
ErrorOccurred?.Invoke(this, $"4253 Board: Failed to open serial port."); |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
// 보드 상태 확인
|
|
||||
var statusTask = Task.Run(() => _service.CheckStatusAsync()); |
|
||||
if (!statusTask.Wait(5000)) |
|
||||
{ |
|
||||
ErrorOccurred?.Invoke(this, "4253 Board: Initialization Timeout (CheckStatus)."); |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
return statusTask.Result; |
|
||||
} catch (Exception ex) { |
|
||||
ErrorOccurred?.Invoke(this, $"4253 Board: Initialization Error - {ex.Message}"); |
|
||||
return false; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public bool ReadInput(string pointName) |
|
||||
{ |
|
||||
// 신규 보드의 입력 읽기 프로토콜이 필요한 부분 (현재 ReadId 등만 구현됨)
|
|
||||
// 구현 계획에는 ID 읽기와 상태 확인만 있었으므로,
|
|
||||
// 실제 DIO 기능을 위해선 추가적인 시리얼 명령이 필요할 수 있음.
|
|
||||
// 일단 true/false 로직 구현
|
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
public void WriteOutput(string pointName, bool value) |
|
||||
{ |
|
||||
// 보드 출력 제어 명령 전송 (예시 프로토콜 필요)
|
|
||||
// _service.SendCommandAsync(...) 호출 형태가 될 것임.
|
|
||||
} |
|
||||
|
|
||||
public List<DioPoint> GetInputPoints() => _inputs; |
|
||||
public List<DioPoint> GetOutputPoints() => _outputs; |
|
||||
|
|
||||
public void Dispose() |
|
||||
{ |
|
||||
if (!_isDisposed) |
|
||||
{ |
|
||||
_isDisposed = true; |
|
||||
_service.Disconnect(); |
|
||||
_service.Dispose(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,144 +0,0 @@ |
|||||
using System; |
|
||||
using System.Threading.Tasks; |
|
||||
using leak_test_project.Infrastructure; |
|
||||
using leak_test_project.Models; |
|
||||
using leak_test_project.Utils; |
|
||||
|
|
||||
namespace leak_test_project.Services |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// 신규 4253 보드를 사용하여 제품 ID를 읽는 센서 서비스.
|
|
||||
/// ZmdiSensorService와 동일한 구조로 구현되어 교체가 용이함.
|
|
||||
/// </summary>
|
|
||||
public class Board4253SensorService : IIdSensorService |
|
||||
{ |
|
||||
private readonly Board4253Service _service; |
|
||||
private readonly int _sensorIndex; |
|
||||
|
|
||||
public event EventHandler<string> ProgressMessage; |
|
||||
public event EventHandler<string> ErrorMessage; |
|
||||
public event EventHandler<bool> ConnectionChanged; |
|
||||
|
|
||||
public Board4253SensorService(Board4253Service service, int sensorIndex) |
|
||||
{ |
|
||||
_service = service; |
|
||||
_sensorIndex = sensorIndex; |
|
||||
// 좌우 오류 간섭을 막기 위해 공용 에러 이벤트 구독 해제
|
|
||||
_service.ConnectionChanged += (s, isConnected) => ConnectionChanged?.Invoke(this, isConnected); |
|
||||
} |
|
||||
|
|
||||
public bool Connect() => _service.Connect(); |
|
||||
public void Disconnect() => _service.Disconnect(); |
|
||||
|
|
||||
public SensorIdData ReadSensor() |
|
||||
{ |
|
||||
try |
|
||||
{ |
|
||||
int channel = _sensorIndex + 1; |
|
||||
|
|
||||
// 0. 초기화 (x00o 전송)
|
|
||||
ProgressMessage?.Invoke(this, "4253 보드 초기화 중..."); |
|
||||
// x00o 명령을 보내고 응답 내용에 상관없이 다음 단계 진행 (최대 5초 대기)
|
|
||||
Task.Run(() => _service.SendCommandAsync("x00o\r\n")).Wait(5000); |
|
||||
Task.Delay(350).Wait(); // 보드 안정화 대기
|
|
||||
|
|
||||
// 1. 보드 상태 확인 (Fail인 경우 중단)
|
|
||||
ProgressMessage?.Invoke(this, "4253 보드 데이터 읽는 중..."); |
|
||||
int extendedTimeout = 15000; |
|
||||
string statusCmd = $"x00c_00{channel}101:owt28006727ea97c7801"; |
|
||||
var statusTask = Task.Run(() => _service.CheckStatusAsync(channel)); |
|
||||
if (!statusTask.Wait(extendedTimeout)) |
|
||||
{ |
|
||||
string buf = _service.GetLastBuffer(); |
|
||||
string displayBuf = string.IsNullOrEmpty(buf) ? "수신된 데이터 없음" : buf; |
|
||||
ErrorMessage?.Invoke(this, $"통신 실패 (4253 보드 CH{channel} 상태 타임아웃)\r\n[송신값]: {statusCmd}\r\n[수신값]: {displayBuf}"); |
|
||||
return null; |
|
||||
} |
|
||||
if (!statusTask.Result) |
|
||||
{ |
|
||||
string buf = _service.GetLastBuffer(); |
|
||||
string displayBuf = string.IsNullOrEmpty(buf) ? "수신된 데이터 없음" : buf; |
|
||||
ErrorMessage?.Invoke(this, $"통신 실패 (4253 보드 CH{channel} 상태 이상 또는 Fail)\r\n[송신값]: {statusCmd}\r\n[수신값]: {displayBuf}"); |
|
||||
return null; |
|
||||
} |
|
||||
|
|
||||
// 2. ID 읽기 (끝자리가 F일 경우 최대 3회 시도)
|
|
||||
string idCmd = $"x00c_00{channel}101:ow2800326003e"; |
|
||||
string rawId = null; |
|
||||
int maxAttempts = 3; |
|
||||
|
|
||||
for (int attempt = 1; attempt <= maxAttempts; attempt++) |
|
||||
{ |
|
||||
var idTask = Task.Run(() => _service.ReadIdAsync(channel)); |
|
||||
if (!idTask.Wait(extendedTimeout)) |
|
||||
{ |
|
||||
if (attempt == maxAttempts) |
|
||||
{ |
|
||||
string buf = _service.GetLastBuffer(); |
|
||||
string displayBuf = string.IsNullOrEmpty(buf) ? "수신된 데이터 없음" : buf; |
|
||||
ErrorMessage?.Invoke(this, $"통신 실패 (4253 보드 CH{channel} ID 대기 타임아웃)\r\n[송신값]: {idCmd}\r\n[수신값]: {displayBuf}"); |
|
||||
return null; |
|
||||
} |
|
||||
continue; |
|
||||
} |
|
||||
|
|
||||
rawId = idTask.Result; |
|
||||
if (string.IsNullOrEmpty(rawId)) |
|
||||
{ |
|
||||
if (attempt == maxAttempts) |
|
||||
{ |
|
||||
string buf = _service.GetLastBuffer(); |
|
||||
string displayBuf = string.IsNullOrEmpty(buf) ? "수신된 데이터 없음" : buf; |
|
||||
ErrorMessage?.Invoke(this, $"통신 실패 (4253 보드 CH{channel} ID 응답 없거나 파싱 오류)\r\n[송신값]: {idCmd}\r\n[수신값]: {displayBuf}"); |
|
||||
return null; |
|
||||
} |
|
||||
continue; |
|
||||
} |
|
||||
|
|
||||
// 정상적으로 파싱된 경우, 끝자리가 'F'인지 확인
|
|
||||
if (rawId.EndsWith("F", StringComparison.OrdinalIgnoreCase)) |
|
||||
{ |
|
||||
if (attempt < maxAttempts) |
|
||||
{ |
|
||||
ProgressMessage?.Invoke(this, $"ID 읽기 재시도 중... ({attempt}/{maxAttempts})"); |
|
||||
Task.Delay(350).Wait(); // 재시도 전 약간의 딜레이
|
|
||||
continue; |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
ProgressMessage?.Invoke(this, $"최대 재시도(2회 추가) 초과. 끝자리가 F인 ID({rawId})를 사용합니다."); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 제대로 된 값을 얻었거나 최대 횟수에 도달하면 루프 탈출
|
|
||||
break; |
|
||||
} |
|
||||
|
|
||||
// 3. SensorIdData 객체 구성 (16자리 ID를 각 필드에 적절히 분배)
|
|
||||
// 신규 보드는 16자리 전체가 ID이므로, 파싱 로직 없이 통째로 넣거나
|
|
||||
// 특정 규칙이 있다면 여기서 분할함.
|
|
||||
var data = new SensorIdData |
|
||||
{ |
|
||||
LowID = rawId, |
|
||||
ID = rawId, // 16자리 전체를 ID로 사용
|
|
||||
Serial = "", // 시리얼 번호는 현재 존재하지 않으므로 강제로 파싱하지 않음
|
|
||||
Item = "N/A", |
|
||||
PrevResult = "F" // '불량제품 투입' 필터를 통과하기 위한 강제 초기화
|
|
||||
}; |
|
||||
|
|
||||
ProgressMessage?.Invoke(this, "ID 읽기 성공"); |
|
||||
return data; |
|
||||
} |
|
||||
catch (Exception ex) |
|
||||
{ |
|
||||
ErrorMessage?.Invoke(this, $"4253 보드 읽기 중 예외 발생: {ex.Message}"); |
|
||||
return null; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public void Dispose() |
|
||||
{ |
|
||||
// 공유 서비스(Board4253Service)는 외부(HomeViewModel)에서 관리하므로 여기서 해제하지 않음
|
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,261 +0,0 @@ |
|||||
using System; |
|
||||
using System.Text; |
|
||||
using System.Threading; |
|
||||
using System.Threading.Tasks; |
|
||||
using leak_test_project.Infrastructure; |
|
||||
using leak_test_project.Utils; |
|
||||
|
|
||||
namespace leak_test_project.Services |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// 신규 4253 보드와의 통신을 관리하는 서비스.
|
|
||||
/// <end> 키워드를 기준으로 데이터를 수신하며, 상태 확인 및 ID 읽기 기능을 제공함.
|
|
||||
/// </summary>
|
|
||||
public class Board4253Service : IDisposable |
|
||||
{ |
|
||||
private readonly ICommunication _communication; |
|
||||
private readonly StringBuilder _receiveBuffer = new StringBuilder(); |
|
||||
private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); |
|
||||
private TaskCompletionSource<string> _responseTcs; |
|
||||
public int TimeoutMs { get; set; } = 5000; // 보드가 응답을 주는데 2초 이상 걸리므로 무조건 길게 대기
|
|
||||
private bool _shouldBeConnected = false; |
|
||||
private System.Timers.Timer _reconnectTimer; |
|
||||
|
|
||||
public event EventHandler<string> ErrorOccurred; |
|
||||
public event EventHandler<bool> ConnectionChanged; |
|
||||
|
|
||||
public string LastResponse { get; private set; } = ""; |
|
||||
|
|
||||
public string GetLastBuffer() |
|
||||
{ |
|
||||
string currentBuf = _receiveBuffer.ToString().Trim(); |
|
||||
return string.IsNullOrEmpty(currentBuf) ? LastResponse : currentBuf; |
|
||||
} |
|
||||
|
|
||||
public Board4253Service(ICommunication communication) |
|
||||
{ |
|
||||
_communication = communication; |
|
||||
_communication.DataReceived += OnDataReceived; |
|
||||
_communication.ConnectionStatusChanged += (s, isConnected) => { |
|
||||
ConnectionChanged?.Invoke(this, isConnected); |
|
||||
if (!isConnected && _shouldBeConnected) StartReconnectTimer(); |
|
||||
}; |
|
||||
|
|
||||
_reconnectTimer = new System.Timers.Timer(1000); |
|
||||
_reconnectTimer.AutoReset = false; |
|
||||
_reconnectTimer.Elapsed += (s, e) => { |
|
||||
if (_shouldBeConnected && !_communication.IsOpen) |
|
||||
{ |
|
||||
if (!_communication.Open()) |
|
||||
{ |
|
||||
if (_shouldBeConnected) _reconnectTimer.Start(); |
|
||||
} |
|
||||
} |
|
||||
else if (_shouldBeConnected) |
|
||||
{ |
|
||||
_reconnectTimer.Start(); |
|
||||
} |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
private void StartReconnectTimer() |
|
||||
{ |
|
||||
if (_reconnectTimer != null && !_reconnectTimer.Enabled) _reconnectTimer.Start(); |
|
||||
} |
|
||||
|
|
||||
public bool Connect() |
|
||||
{ |
|
||||
_shouldBeConnected = true; |
|
||||
bool opened = _communication.Open(); |
|
||||
if (!opened) StartReconnectTimer(); |
|
||||
return opened; |
|
||||
} |
|
||||
|
|
||||
public void Disconnect() |
|
||||
{ |
|
||||
_shouldBeConnected = false; |
|
||||
_reconnectTimer?.Stop(); |
|
||||
_communication.Close(); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// 4253 보드의 상태를 확인합니다.
|
|
||||
/// </summary>
|
|
||||
/// <returns>성공 여부</returns>
|
|
||||
public async Task<bool> CheckStatusAsync(int channel = 1) |
|
||||
{ |
|
||||
string response = await SendCommandAsync($"x00c_00{channel}101:owt28006727ea97c7801\r\n"); |
|
||||
|
|
||||
if (response == null) |
|
||||
{ |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
if (response.IndexOf("Success", StringComparison.OrdinalIgnoreCase) >= 0) |
|
||||
{ |
|
||||
return true; |
|
||||
} |
|
||||
|
|
||||
if (response.IndexOf("Fail", StringComparison.OrdinalIgnoreCase) >= 0) |
|
||||
{ |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
// Success도 Fail도 없는 알 수 없는 응답인 경우
|
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// 4253 보드로부터 16자리 ID를 읽어옵니다.
|
|
||||
/// </summary>
|
|
||||
/// <returns>16자리 ID 문자열, 실패 시 null</returns>
|
|
||||
public async Task<string> ReadIdAsync(int channel = 1) |
|
||||
{ |
|
||||
string response = await SendCommandAsync($"x00c_00{channel}101:ow2800326003e\r\n"); |
|
||||
|
|
||||
if (response == null) |
|
||||
{ |
|
||||
return null; |
|
||||
} |
|
||||
|
|
||||
if (response.Contains("Fail")) |
|
||||
{ |
|
||||
return null; |
|
||||
} |
|
||||
|
|
||||
string id = ExtractId(response); |
|
||||
if (string.IsNullOrEmpty(id)) |
|
||||
{ |
|
||||
return null; |
|
||||
} |
|
||||
|
|
||||
return id; |
|
||||
} |
|
||||
|
|
||||
private string ExtractId(string response) |
|
||||
{ |
|
||||
if (string.IsNullOrEmpty(response)) return null; |
|
||||
|
|
||||
// 보드 응답에는 보낸 명령어가 에코되어 포함되므로, 줄바꿈으로 나누어 실제 데이터 라인을 찾음
|
|
||||
string[] lines = response.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries); |
|
||||
|
|
||||
foreach (var line in lines) |
|
||||
{ |
|
||||
string trimmed = line.Trim(); |
|
||||
if (trimmed.Contains("x00c_") || trimmed.IndexOf("<end>", StringComparison.OrdinalIgnoreCase) >= 0 || trimmed.IndexOf("Success", StringComparison.OrdinalIgnoreCase) >= 0) |
|
||||
continue; |
|
||||
|
|
||||
// 16자리 영숫자 ID 추출
|
|
||||
var match = System.Text.RegularExpressions.Regex.Match(trimmed, @"[A-Za-z0-9]{16}"); |
|
||||
if (match.Success) return match.Value; |
|
||||
} |
|
||||
|
|
||||
return null; |
|
||||
} |
|
||||
|
|
||||
public async Task<string> SendCommandAsync(string command) |
|
||||
{ |
|
||||
await _lock.WaitAsync(); |
|
||||
try |
|
||||
{ |
|
||||
int retryCount = 0; |
|
||||
while (retryCount <= 3) |
|
||||
{ |
|
||||
if (!_communication.IsOpen) |
|
||||
{ |
|
||||
if (!_communication.Open()) |
|
||||
{ |
|
||||
retryCount++; |
|
||||
await Task.Delay(300); |
|
||||
continue; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
_receiveBuffer.Clear(); |
|
||||
LastResponse = ""; |
|
||||
_responseTcs = new TaskCompletionSource<string>(); |
|
||||
|
|
||||
_communication.ClearBuffer(); // 이전에 남아있던 패킷 조각 완벽히 제거
|
|
||||
|
|
||||
_communication.Write(command); |
|
||||
|
|
||||
using (var cts = new CancellationTokenSource(TimeoutMs)) |
|
||||
{ |
|
||||
cts.Token.Register(() => _responseTcs.TrySetCanceled()); |
|
||||
try |
|
||||
{ |
|
||||
return await _responseTcs.Task; |
|
||||
} |
|
||||
catch (OperationCanceledException) |
|
||||
{ |
|
||||
retryCount++; |
|
||||
string currentBuffer = _receiveBuffer.ToString().Trim(); |
|
||||
FileLogger.Log("WARNING", $"[Board4253] Timeout waiting for response (Retry {retryCount}/3). Command: {command.Trim()}, ReceivedSoFar: {currentBuffer}"); |
|
||||
_receiveBuffer.Clear(); |
|
||||
if (retryCount <= 3) await Task.Delay(300); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
FileLogger.Log("ERROR", $"[Board4253] Failed to receive response after 3 retries: {command.Trim()}"); |
|
||||
return null; |
|
||||
} |
|
||||
finally |
|
||||
{ |
|
||||
_responseTcs = null; |
|
||||
_lock.Release(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private void OnDataReceived(object sender, string data) |
|
||||
{ |
|
||||
_receiveBuffer.Append(data); |
|
||||
string currentContent = _receiveBuffer.ToString(); |
|
||||
|
|
||||
bool isComplete = false; |
|
||||
|
|
||||
if (currentContent.IndexOf("<end>", StringComparison.OrdinalIgnoreCase) >= 0 || |
|
||||
currentContent.IndexOf("Success", StringComparison.OrdinalIgnoreCase) >= 0 || |
|
||||
currentContent.IndexOf("Fail", StringComparison.OrdinalIgnoreCase) >= 0 || |
|
||||
currentContent.IndexOf("<ACK>OFF", StringComparison.OrdinalIgnoreCase) >= 0) |
|
||||
{ |
|
||||
isComplete = true; |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
// <end>가 오지 않았더라도, 명령어 에코가 아닌 줄에서 16자리 ID를 발견하면 즉시 완료 처리
|
|
||||
string[] lines = currentContent.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries); |
|
||||
foreach (var line in lines) |
|
||||
{ |
|
||||
string trimmed = line.Trim(); |
|
||||
if (trimmed.Contains("x00c_")) continue; |
|
||||
|
|
||||
var match = System.Text.RegularExpressions.Regex.Match(trimmed, @"[A-Za-z0-9]{16}"); |
|
||||
if (match.Success) |
|
||||
{ |
|
||||
isComplete = true; |
|
||||
break; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
if (isComplete) |
|
||||
{ |
|
||||
if (_responseTcs != null && !_responseTcs.Task.IsCompleted) |
|
||||
{ |
|
||||
LastResponse = currentContent.Trim(); |
|
||||
_responseTcs.TrySetResult(currentContent); |
|
||||
} |
|
||||
_receiveBuffer.Clear(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public void Dispose() |
|
||||
{ |
|
||||
Disconnect(); |
|
||||
_communication.DataReceived -= OnDataReceived; |
|
||||
_reconnectTimer?.Dispose(); |
|
||||
_lock.Dispose(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,348 +0,0 @@ |
|||||
using System; |
|
||||
using System.Text; |
|
||||
using System.Threading; |
|
||||
using System.Timers; |
|
||||
using leak_test_project.Infrastructure; |
|
||||
using leak_test_project.Models; |
|
||||
using leak_test_project.Utils; |
|
||||
|
|
||||
namespace leak_test_project.Services |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// ZMDI 센서와 시리얼 통신하여 제품 ID를 읽고 파싱하는 서비스.
|
|
||||
/// 자동 재연결(Auto-Reconnect) 및 예외 처리 포함.
|
|
||||
/// </summary>
|
|
||||
public class ZmdiSensorService : IIdSensorService |
|
||||
{ |
|
||||
private readonly ICommunication _comm; |
|
||||
private readonly object _commSync = new object(); |
|
||||
private readonly int _sensorIndex; // 0=LEFT, 1=RIGHT
|
|
||||
|
|
||||
private System.Timers.Timer _reconnectTimer; |
|
||||
private bool _shouldBeConnected = false; |
|
||||
|
|
||||
// 레거시 명령 시퀀스 (ClsSensorReader의 commandList1~4) 그대로 유지
|
|
||||
private readonly string[] _commandList1 = { "V", "Pr_D7", "Pr_D6", "Pr_D5", "r" }; |
|
||||
private readonly string[] _commandList2 = { "tso31150" }; |
|
||||
private readonly string[] _commandList3 = { "os_10", "t11005", "OWT7800272D1", "OR_78002", |
|
||||
"OW_780038AA55A", "OW_780011A", "OR_78002", "OW_780038AFF00", "OW_78001CF", "OR_78004" }; |
|
||||
private readonly string[] _commandList4 = { "OW_7800140", "OR_78002", "OW_7800141", |
|
||||
"OR_78002", "OW_7800142", "OR_78002", "x9c_990:x" }; |
|
||||
|
|
||||
// 년/월/일 디코딩 테이블 (레거시 그대로)
|
|
||||
private readonly string[] _yearHexList = { |
|
||||
"49","4A","4B","4C","4D","4E","4F","50","51","52","53","54","55","56","57","58","59","5A" |
|
||||
}; |
|
||||
private readonly string[] _yearIdList = { |
|
||||
"I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z" |
|
||||
}; |
|
||||
private readonly string[] _monthList = { "1","2","3","4","5","6","7","8","9","A","B","C" }; |
|
||||
private readonly string[] _dayHexList = { |
|
||||
"41","42","43","44","45","46","47","48","49","4A","4B","4C","4D","4E","4F","50", |
|
||||
"51","52","53","54","55","56","57","58","59","5A","31","32","33","34","35" |
|
||||
}; |
|
||||
private readonly string[] _dayIdList = { |
|
||||
"A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P", |
|
||||
"Q","R","S","T","U","V","W","X","Y","Z","1","2","3","4","5" |
|
||||
}; |
|
||||
|
|
||||
/// <summary>진행 상황 메시지 이벤트</summary>
|
|
||||
public event EventHandler<string> ProgressMessage; |
|
||||
|
|
||||
/// <summary>오류 메시지 이벤트</summary>
|
|
||||
public event EventHandler<string> ErrorMessage; |
|
||||
|
|
||||
/// <summary>연결 상태 변경 이벤트</summary>
|
|
||||
public event EventHandler<bool> ConnectionChanged; |
|
||||
|
|
||||
public ZmdiSensorService(ICommunication communication, int sensorIndex) |
|
||||
{ |
|
||||
_comm = communication; |
|
||||
_sensorIndex = sensorIndex; |
|
||||
|
|
||||
_comm.ConnectionStatusChanged += (s, isConnected) => { |
|
||||
ConnectionChanged?.Invoke(this, isConnected); |
|
||||
if (!isConnected && _shouldBeConnected) StartReconnectTimer(); |
|
||||
}; |
|
||||
|
|
||||
// 1초(1000ms)마다 연결 상태를 확인하고 재연결 시도
|
|
||||
_reconnectTimer = new System.Timers.Timer(1000); |
|
||||
_reconnectTimer.AutoReset = false; // 재진입 방지
|
|
||||
_reconnectTimer.Elapsed += (s, e) => { |
|
||||
if (_shouldBeConnected && !_comm.IsOpen) |
|
||||
{ |
|
||||
if (!_comm.Open()) |
|
||||
{ |
|
||||
if (_shouldBeConnected) _reconnectTimer.Start(); |
|
||||
} |
|
||||
} |
|
||||
else if (_shouldBeConnected) |
|
||||
{ |
|
||||
_reconnectTimer.Start(); |
|
||||
} |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
public bool Connect() |
|
||||
{ |
|
||||
_shouldBeConnected = true; |
|
||||
bool opened = _comm.Open(); |
|
||||
if (!opened) StartReconnectTimer(); |
|
||||
return opened; |
|
||||
} |
|
||||
|
|
||||
public void Disconnect() |
|
||||
{ |
|
||||
_shouldBeConnected = false; |
|
||||
_reconnectTimer?.Stop(); |
|
||||
_comm.Close(); |
|
||||
} |
|
||||
|
|
||||
private void StartReconnectTimer() |
|
||||
{ |
|
||||
if (_reconnectTimer != null && !_reconnectTimer.Enabled) _reconnectTimer.Start(); |
|
||||
} |
|
||||
|
|
||||
public void Dispose() |
|
||||
{ |
|
||||
Disconnect(); |
|
||||
_reconnectTimer?.Dispose(); |
|
||||
_reconnectTimer = null; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// ZMDI 센서에서 제품 ID를 읽고 파싱합니다.
|
|
||||
/// 반드시 백그라운드 스레드에서 호출해야 합니다.
|
|
||||
/// </summary>
|
|
||||
/// <returns>성공 시 SensorIdData, 실패 시 null</returns>
|
|
||||
public SensorIdData ReadSensor() |
|
||||
{ |
|
||||
string recvData = ""; |
|
||||
try |
|
||||
{ |
|
||||
var data = new SensorIdData(); |
|
||||
|
|
||||
// 1단계: 초기 명령 실행
|
|
||||
ProgressMessage?.Invoke(this, "ZMDI 초기화 중..."); |
|
||||
for (int i = 0; i < _commandList1.Length; i++) |
|
||||
{ |
|
||||
if (!ExecuteCommand(_commandList1[i], ref recvData)) |
|
||||
{ |
|
||||
ErrorMessage?.Invoke(this, $"통신 실패 (cmd1: ZMDI 센서 초기화 및 통신 확인 단계)\r\n[송신값]: {_commandList1[i]}\r\n[수신값]: {recvData.Trim()}"); |
|
||||
return null; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
Thread.Sleep(1000); |
|
||||
|
|
||||
// 2단계
|
|
||||
ProgressMessage?.Invoke(this, "ZMDI 메모리 준비 중..."); |
|
||||
for (int i = 0; i < _commandList2.Length; i++) |
|
||||
{ |
|
||||
if (!ExecuteCommand(_commandList2[i], ref recvData)) |
|
||||
{ |
|
||||
ErrorMessage?.Invoke(this, $"통신 실패 (cmd2: ZMDI 메모리 접근 준비 단계)\r\n[송신값]: {_commandList2[i]}\r\n[수신값]: {recvData.Trim()}"); |
|
||||
return null; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
Thread.Sleep(200); |
|
||||
|
|
||||
// 3단계
|
|
||||
ProgressMessage?.Invoke(this, "ZMDI 데이터 수집 중..."); |
|
||||
for (int i = 0; i < _commandList3.Length; i++) |
|
||||
{ |
|
||||
if (!ExecuteCommand(_commandList3[i], ref recvData)) |
|
||||
{ |
|
||||
ErrorMessage?.Invoke(this, $"통신 실패 (cmd3: ZMDI 데이터 수집 설정 단계)\r\n[송신값]: {_commandList3[i]}\r\n[수신값]: {recvData.Trim()}"); |
|
||||
return null; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 4단계: ID 메모리 읽기 (3회)
|
|
||||
ProgressMessage?.Invoke(this, "ZMDI ID 읽기 중..."); |
|
||||
// 첫 번째 ID 레지스터
|
|
||||
if (!ExecuteCommand(_commandList4[0], ref recvData)) { ErrorMessage?.Invoke(this, $"통신 실패 (ID 읽기 단계-1)\r\n[송신값]: {_commandList4[0]}\r\n[수신값]: {recvData.Trim()}"); return null; } |
|
||||
if (!ExecuteCommand(_commandList4[1], ref recvData)) { ErrorMessage?.Invoke(this, $"통신 실패 (ID 읽기 단계-2)\r\n[송신값]: {_commandList4[1]}\r\n[수신값]: {recvData.Trim()}"); return null; } |
|
||||
data.LowID = recvData.Length > 1 ? recvData.Substring(1).Replace("\r", "").Replace("\n", "") : ""; |
|
||||
|
|
||||
// 두 번째 ID 레지스터
|
|
||||
if (!ExecuteCommand(_commandList4[2], ref recvData)) { ErrorMessage?.Invoke(this, $"통신 실패 (ID 읽기 단계-3)\r\n[송신값]: {_commandList4[2]}\r\n[수신값]: {recvData.Trim()}"); return null; } |
|
||||
if (!ExecuteCommand(_commandList4[3], ref recvData)) { ErrorMessage?.Invoke(this, $"통신 실패 (ID 읽기 단계-4)\r\n[송신값]: {_commandList4[3]}\r\n[수신값]: {recvData.Trim()}"); return null; } |
|
||||
data.LowID += recvData.Length > 1 ? recvData.Substring(1).Replace("\r", "").Replace("\n", "") : ""; |
|
||||
|
|
||||
// 세 번째 ID 레지스터
|
|
||||
if (!ExecuteCommand(_commandList4[4], ref recvData)) { ErrorMessage?.Invoke(this, $"통신 실패 (ID 읽기 단계-5)\r\n[송신값]: {_commandList4[4]}\r\n[수신값]: {recvData.Trim()}"); return null; } |
|
||||
if (!ExecuteCommand(_commandList4[5], ref recvData)) { ErrorMessage?.Invoke(this, $"통신 실패 (ID 읽기 단계-6)\r\n[송신값]: {_commandList4[5]}\r\n[수신값]: {recvData.Trim()}"); return null; } |
|
||||
data.LowID += recvData.Length > 1 ? recvData.Substring(1).Replace("\r", "").Replace("\n", "") : ""; |
|
||||
|
|
||||
// 마지막 명령 실행
|
|
||||
if (!ExecuteCommand(_commandList4[6], ref recvData)) { ErrorMessage?.Invoke(this, $"통신 실패 (ID 읽기 종료 단계)\r\n[송신값]: {_commandList4[6]}\r\n[수신값]: {recvData.Trim()}"); return null; } |
|
||||
|
|
||||
// ID 파싱
|
|
||||
if (!ParseLowId(data)) |
|
||||
return null; |
|
||||
|
|
||||
return data; |
|
||||
} |
|
||||
catch (Exception ex) |
|
||||
{ |
|
||||
FileLogger.Log("ERROR", $"[ZMDI] ReadSensor exception: {ex.Message}"); |
|
||||
ErrorMessage?.Invoke(this, $"센서 데이터 오류 (Exception: {ex.Message})\r\n[수신값]: {recvData.Trim()}"); |
|
||||
return null; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>LowID 문자열을 파싱하여 년/월/일/시리얼/라인/항목을 추출합니다.</summary>
|
|
||||
private bool ParseLowId(SensorIdData data) |
|
||||
{ |
|
||||
if (string.IsNullOrEmpty(data.LowID) || data.LowID.Length < 12) |
|
||||
{ |
|
||||
ErrorMessage?.Invoke(this, $"센서 데이터 길이 부족 (길이: {data.LowID?.Length ?? 0})\r\n[수신값]: {data.LowID}"); |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
string yearHex = data.LowID.Substring(0, 2); |
|
||||
string yearId = ""; |
|
||||
data.Year = 0; |
|
||||
for (int i = 0; i < _yearHexList.Length; i++) |
|
||||
{ |
|
||||
if (yearHex == _yearHexList[i]) |
|
||||
{ |
|
||||
data.Year = 2013 + i; |
|
||||
yearId = _yearIdList[i]; |
|
||||
break; |
|
||||
} |
|
||||
} |
|
||||
if (data.Year <= 0) { ErrorMessage?.Invoke(this, $"센서 데이터 오류(년도 파싱 불가)\r\n[수신값]: {data.LowID}"); return false; } |
|
||||
|
|
||||
string monthChar = data.LowID.Substring(2, 1); |
|
||||
string monthId = ""; |
|
||||
data.Month = 0; |
|
||||
for (int i = 0; i < _monthList.Length; i++) |
|
||||
{ |
|
||||
if (monthChar == _monthList[i]) |
|
||||
{ |
|
||||
data.Month = 1 + i; |
|
||||
monthId = monthChar; |
|
||||
break; |
|
||||
} |
|
||||
} |
|
||||
if (data.Month <= 0) { ErrorMessage?.Invoke(this, $"센서 데이터 오류(월 파싱 불가)\r\n[수신값]: {data.LowID}"); return false; } |
|
||||
|
|
||||
string dayHex = data.LowID.Substring(3, 2); |
|
||||
string dayId = ""; |
|
||||
data.Day = 0; |
|
||||
for (int i = 0; i < _dayHexList.Length; i++) |
|
||||
{ |
|
||||
if (dayHex == _dayHexList[i]) |
|
||||
{ |
|
||||
data.Day = 1 + i; |
|
||||
dayId = _dayIdList[i]; |
|
||||
break; |
|
||||
} |
|
||||
} |
|
||||
if (data.Day <= 0) { ErrorMessage?.Invoke(this, $"센서 데이터 오류(일 파싱 불가)\r\n[수신값]: {data.LowID}"); return false; } |
|
||||
|
|
||||
// 시리얼 번호 계산 (16진수 → 10진수)
|
|
||||
string serialHigh = data.LowID.Substring(5, 2); |
|
||||
string serialLow = data.LowID.Substring(7, 2); |
|
||||
int high = Convert.ToInt32(serialHigh, 16) << 8; |
|
||||
int low = Convert.ToInt32(serialLow, 16); |
|
||||
data.Serial = (high + low).ToString().PadLeft(5, '0'); |
|
||||
|
|
||||
// MC Line 및 라인 번호 (비트 단위 파싱)
|
|
||||
string nibble = data.LowID.Substring(9, 1); |
|
||||
string binary = Convert.ToString(Convert.ToInt32(nibble, 16), 2).PadLeft(4, '0'); |
|
||||
data.McLine = binary.Substring(0, 1); |
|
||||
string lineNoBits = binary.Substring(1, 3); |
|
||||
data.LineNo = Convert.ToInt32(lineNoBits, 2).ToString(); |
|
||||
|
|
||||
// PrevResult (이전 검사 결과)
|
|
||||
data.PrevResult = data.LowID.Substring(10, 1); |
|
||||
|
|
||||
// 제품 Item
|
|
||||
data.Item = data.LowID.Substring(11, 1); |
|
||||
|
|
||||
// 최종 ID 조합
|
|
||||
data.ID = yearId + monthId + dayId + data.Serial + data.LineNo + data.Item; |
|
||||
|
|
||||
return true; |
|
||||
} |
|
||||
|
|
||||
/// <summary>명령을 전송하고 응답을 수신합니다. 최대 3회 재시도.</summary>
|
|
||||
private bool ExecuteCommand(string sendCommand, ref string recvData) |
|
||||
{ |
|
||||
lock (_commSync) |
|
||||
{ |
|
||||
int retryCount = 0; |
|
||||
while (true) |
|
||||
{ |
|
||||
if (SendCommandWaitResponse(sendCommand, ref recvData)) |
|
||||
return true; |
|
||||
|
|
||||
Thread.Sleep(300); |
|
||||
retryCount++; |
|
||||
if (retryCount > 3) |
|
||||
return false; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>명령 전송 후 CR+LF 응답을 500ms 타임아웃으로 대기합니다.</summary>
|
|
||||
private bool SendCommandWaitResponse(string sendCommand, ref string recvData) |
|
||||
{ |
|
||||
string fullCommand = sendCommand + "\r\n"; |
|
||||
|
|
||||
if (!_comm.IsOpen) |
|
||||
{ |
|
||||
if (!_comm.Open()) |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
recvData = ""; |
|
||||
|
|
||||
// 핵심 버그 수정: 이전 명령어의 잔류 응답 데이터 비우기
|
|
||||
_comm.ClearBuffer(); |
|
||||
|
|
||||
// 500ms 타임아웃으로 응답 대기 (레거시 동일)
|
|
||||
DateTime deadline = DateTime.Now.AddMilliseconds(500); |
|
||||
|
|
||||
// 동기식 수신을 위한 임시 버퍼
|
|
||||
string buffer = ""; |
|
||||
EventHandler<string> handler = null; |
|
||||
handler = (s, data) => { buffer += data; }; |
|
||||
|
|
||||
// 핵심 버그 수정: 명령어를 쓰기 '전에' 이벤트 핸들러를 먼저 등록하여 아주 빠른 응답 유실 방지
|
|
||||
_comm.DataReceived += handler; |
|
||||
|
|
||||
bool success = false; |
|
||||
try |
|
||||
{ |
|
||||
if (!_comm.Write(fullCommand)) |
|
||||
return false; |
|
||||
|
|
||||
while (DateTime.Now < deadline) |
|
||||
{ |
|
||||
Thread.Sleep(2); |
|
||||
if (buffer.IndexOf("\r\n") >= 0) |
|
||||
{ |
|
||||
recvData = buffer; |
|
||||
success = true; |
|
||||
break; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 타임아웃 발생 시에도 현재까지 수신된 버퍼 내용을 반환 (디버깅용)
|
|
||||
if (!success) |
|
||||
recvData = buffer; |
|
||||
|
|
||||
return success; |
|
||||
} |
|
||||
finally |
|
||||
{ |
|
||||
_comm.DataReceived -= handler; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,10 +1,65 @@ |
|||||
using System; |
using System; |
||||
using System.Collections.Generic; |
using System.Collections.Generic; |
||||
using System.IO; |
using System.IO; |
||||
|
using System.Xml.Serialization; |
||||
using leak_test_project.Models; |
using leak_test_project.Models; |
||||
|
|
||||
namespace leak_test_project.Utils |
namespace leak_test_project.Utils |
||||
{ |
{ |
||||
|
public static class ConfigManager |
||||
|
{ |
||||
|
private static readonly string ConfigPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config.xml"); |
||||
|
public static AppConfig Current { get; private set; } = new AppConfig(); |
||||
|
public static event EventHandler ConfigChanged; |
||||
|
|
||||
|
static ConfigManager() |
||||
|
{ |
||||
|
Load(); |
||||
|
} |
||||
|
|
||||
|
public static void Load() |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
if (File.Exists(ConfigPath)) |
||||
|
{ |
||||
|
XmlSerializer serializer = new XmlSerializer(typeof(AppConfig)); |
||||
|
using (FileStream fs = new FileStream(ConfigPath, FileMode.Open)) |
||||
|
{ |
||||
|
Current = (AppConfig)serializer.Deserialize(fs); |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
Current = new AppConfig(); |
||||
|
Save(); // 초기 파일 생성
|
||||
|
} |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
Console.WriteLine($"[Config] Error loading config: {ex.Message}"); |
||||
|
Current = new AppConfig(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static void Save() |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
XmlSerializer serializer = new XmlSerializer(typeof(AppConfig)); |
||||
|
using (FileStream fs = new FileStream(ConfigPath, FileMode.Create)) |
||||
|
{ |
||||
|
serializer.Serialize(fs, Current); |
||||
|
} |
||||
|
ConfigChanged?.Invoke(null, EventArgs.Empty); |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
Console.WriteLine($"[Config] Error saving config: {ex.Message}"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
public class DioConfigParser |
public class DioConfigParser |
||||
{ |
{ |
||||
public string CompanyName { get; set; } = "ADLink"; |
public string CompanyName { get; set; } = "ADLink"; |
||||
@ -1,61 +0,0 @@ |
|||||
using System; |
|
||||
using System.IO; |
|
||||
using System.Xml.Serialization; |
|
||||
using leak_test_project.Models; |
|
||||
|
|
||||
namespace leak_test_project.Utils |
|
||||
{ |
|
||||
public static class ConfigManager |
|
||||
{ |
|
||||
private static readonly string ConfigPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config.xml"); |
|
||||
public static AppConfig Current { get; private set; } = new AppConfig(); |
|
||||
public static event EventHandler ConfigChanged; |
|
||||
|
|
||||
static ConfigManager() |
|
||||
{ |
|
||||
Load(); |
|
||||
} |
|
||||
|
|
||||
public static void Load() |
|
||||
{ |
|
||||
try |
|
||||
{ |
|
||||
if (File.Exists(ConfigPath)) |
|
||||
{ |
|
||||
XmlSerializer serializer = new XmlSerializer(typeof(AppConfig)); |
|
||||
using (FileStream fs = new FileStream(ConfigPath, FileMode.Open)) |
|
||||
{ |
|
||||
Current = (AppConfig)serializer.Deserialize(fs); |
|
||||
} |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
Current = new AppConfig(); |
|
||||
Save(); // 초기 파일 생성
|
|
||||
} |
|
||||
} |
|
||||
catch (Exception ex) |
|
||||
{ |
|
||||
Console.WriteLine($"[Config] Error loading config: {ex.Message}"); |
|
||||
Current = new AppConfig(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public static void Save() |
|
||||
{ |
|
||||
try |
|
||||
{ |
|
||||
XmlSerializer serializer = new XmlSerializer(typeof(AppConfig)); |
|
||||
using (FileStream fs = new FileStream(ConfigPath, FileMode.Create)) |
|
||||
{ |
|
||||
serializer.Serialize(fs, Current); |
|
||||
} |
|
||||
ConfigChanged?.Invoke(null, EventArgs.Empty); |
|
||||
} |
|
||||
catch (Exception ex) |
|
||||
{ |
|
||||
Console.WriteLine($"[Config] Error saving config: {ex.Message}"); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,61 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.IO; |
|
||||
using System.Reflection; |
|
||||
using System.Text; |
|
||||
using System.Windows; |
|
||||
|
|
||||
namespace leak_test_project.Utils |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// 데이터를 CSV 파일로 내보내는 유틸리티
|
|
||||
/// </summary>
|
|
||||
public static class CsvExporter |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// 컬렉션 데이터를 CSV 파일로 저장함
|
|
||||
/// </summary>
|
|
||||
/// <typeparam name="T">데이터 모델 클래스</typeparam>
|
|
||||
/// <param name="items">내보낼 데이터 목록</param>
|
|
||||
/// <param name="filePath">저장할 파일 경로</param>
|
|
||||
public static bool ExportToCsv<T>(IEnumerable<T> items, string filePath) |
|
||||
{ |
|
||||
try |
|
||||
{ |
|
||||
var sb = new StringBuilder(); |
|
||||
var props = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance); |
|
||||
|
|
||||
// Header
|
|
||||
foreach (var prop in props) |
|
||||
{ |
|
||||
sb.Append(prop.Name).Append(","); |
|
||||
} |
|
||||
sb.AppendLine(); |
|
||||
|
|
||||
// Body
|
|
||||
foreach (var item in items) |
|
||||
{ |
|
||||
foreach (var prop in props) |
|
||||
{ |
|
||||
var val = prop.GetValue(item, null); |
|
||||
var str = val?.ToString() ?? ""; |
|
||||
if (str.Contains(",") || str.Contains("\"") || str.Contains("\n")) |
|
||||
sb.Append($"\"{str.Replace("\"", "\"\"")}\""); |
|
||||
else |
|
||||
sb.Append(str); |
|
||||
sb.Append(","); |
|
||||
} |
|
||||
sb.AppendLine(); |
|
||||
} |
|
||||
|
|
||||
File.WriteAllText(filePath, sb.ToString(), Encoding.UTF8); |
|
||||
return true; |
|
||||
} |
|
||||
catch (Exception ex) |
|
||||
{ |
|
||||
MessageBox.Show($"CSV 저장 실패: {ex.Message}"); |
|
||||
return false; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,88 +0,0 @@ |
|||||
using System; |
|
||||
using System.IO; |
|
||||
using System.Text; |
|
||||
using leak_test_project.Models; |
|
||||
|
|
||||
namespace leak_test_project.Utils |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// 통신 로그 및 시스템 이력을 파일로 저장하는 유틸리티
|
|
||||
/// </summary>
|
|
||||
public static class FileLogger |
|
||||
{ |
|
||||
private static readonly string LogDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs"); |
|
||||
private static readonly object _lock = new object(); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// 검사 데이터를 Logs/yyyy-MM-dd.csv 파일에 저장함
|
|
||||
/// </summary>
|
|
||||
public static void LogInspectData(InspectData data) |
|
||||
{ |
|
||||
lock (_lock) |
|
||||
{ |
|
||||
try |
|
||||
{ |
|
||||
if (!Directory.Exists(LogDirectory)) |
|
||||
Directory.CreateDirectory(LogDirectory); |
|
||||
|
|
||||
string dateStr = DateTime.Now.ToString("yyyy-MM-dd"); |
|
||||
string filePath = Path.Combine(LogDirectory, $"{dateStr}.csv"); |
|
||||
|
|
||||
bool isNewFile = !File.Exists(filePath); |
|
||||
|
|
||||
// CSV 헤더: Date,Time,Channel,ID,Value,Judgment,Mode,LineNo,ProductType,SpecUL,SpecLL,Retest
|
|
||||
if (isNewFile) |
|
||||
{ |
|
||||
string header = "Date,Time,Channel,ID,Value,Judgment,Mode,LineNo,ProductType,SpecUL,SpecLL,Retest" + Environment.NewLine; |
|
||||
File.WriteAllText(filePath, header, Encoding.UTF8); |
|
||||
} |
|
||||
|
|
||||
string csvLine = $"{Esc(data.InspectDate)},{Esc(data.InspectTime)},{Esc(data.Channel)},{Esc(data.ProductId)}," + |
|
||||
$"{Esc(data.MeasuredValue)},{Esc(data.Judgment)},{Esc(data.Mode)},{Esc(data.LineNo)}," + |
|
||||
$"{Esc(data.ProductType)},{Esc(data.SpecUL)},{Esc(data.SpecLL)},{Esc(data.Retest)}{Environment.NewLine}"; |
|
||||
|
|
||||
File.AppendAllText(filePath, csvLine, Encoding.UTF8); |
|
||||
} |
|
||||
catch (Exception ex) |
|
||||
{ |
|
||||
Console.WriteLine($"[FileLogger Error] {ex.Message}"); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// 단순 텍스트 로그 (기존 호환성 유지용)
|
|
||||
/// </summary>
|
|
||||
public static void Log(string tag, string message) |
|
||||
{ |
|
||||
lock (_lock) |
|
||||
{ |
|
||||
try |
|
||||
{ |
|
||||
if (!Directory.Exists(LogDirectory)) |
|
||||
Directory.CreateDirectory(LogDirectory); |
|
||||
|
|
||||
string dateStr = DateTime.Now.ToString("yyyy-MM-dd"); |
|
||||
string filePath = Path.Combine(LogDirectory, $"{dateStr}_system.log"); |
|
||||
|
|
||||
string logEntry = $"[{DateTime.Now:HH:mm:ss.fff}] [{tag}] {message}{Environment.NewLine}"; |
|
||||
File.AppendAllText(filePath, logEntry, Encoding.UTF8); |
|
||||
} |
|
||||
catch (Exception ex) |
|
||||
{ |
|
||||
Console.WriteLine($"[FileLogger Error] {ex.Message}"); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
/// <summary>
|
|
||||
/// CSV 값 이스케이프: 쉼표, 따옴표, 줄바꿈이 포함된 값을 안전하게 감싸줌
|
|
||||
/// </summary>
|
|
||||
private static string Esc(string value) |
|
||||
{ |
|
||||
if (string.IsNullOrEmpty(value)) return ""; |
|
||||
if (value.Contains(",") || value.Contains("\"") || value.Contains("\n")) |
|
||||
return $"\"{value.Replace("\"", "\"\"")}\""; |
|
||||
return value; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,93 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.IO; |
|
||||
using System.Linq; |
|
||||
using System.Text.RegularExpressions; |
|
||||
using leak_test_project.Models; |
|
||||
|
|
||||
namespace leak_test_project.Utils |
|
||||
{ |
|
||||
public static class LogParser |
|
||||
{ |
|
||||
private static readonly string LogDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs"); |
|
||||
|
|
||||
public static List<InspectData> ParseLogs(DateTime start, DateTime end, string judgmentFilter = "[전체]", string serialFilter = "") |
|
||||
{ |
|
||||
var results = new List<InspectData>(); |
|
||||
|
|
||||
if (!Directory.Exists(LogDirectory)) |
|
||||
{ |
|
||||
// 디렉토리가 없으면 빈 리스트 반환
|
|
||||
return results; |
|
||||
} |
|
||||
|
|
||||
// 날짜 범위 내의 모든 로그 파일 찾기 (.csv)
|
|
||||
var files = Directory.GetFiles(LogDirectory, "*.csv") |
|
||||
.Where(f => { |
|
||||
string fileName = Path.GetFileNameWithoutExtension(f); |
|
||||
if (DateTime.TryParse(fileName, out DateTime fileDate)) |
|
||||
{ |
|
||||
return fileDate.Date >= start.Date && fileDate.Date <= end.Date; |
|
||||
} |
|
||||
return false; |
|
||||
}); |
|
||||
|
|
||||
foreach (var file in files) |
|
||||
{ |
|
||||
try |
|
||||
{ |
|
||||
var lines = File.ReadAllLines(file); |
|
||||
if (lines.Length <= 1) continue; // Header only or empty
|
|
||||
|
|
||||
// CSV 헤더: Date,Time,Channel,ID,Value,Judgment,Mode,LineNo,ProductType,SpecUL,SpecLL,Retest
|
|
||||
for (int i = 1; i < lines.Length; i++) |
|
||||
{ |
|
||||
string line = lines[i]; |
|
||||
if (string.IsNullOrWhiteSpace(line)) continue; |
|
||||
var parts = line.Split(','); |
|
||||
if (parts.Length < 12) continue; |
|
||||
|
|
||||
string date = parts[0].Trim(); |
|
||||
string time = parts[1].Trim(); |
|
||||
string channel = parts[2].Trim(); |
|
||||
string id = parts[3].Trim(); |
|
||||
string val = parts[4].Trim(); |
|
||||
string judg = parts[5].Trim(); |
|
||||
string mode = parts[6].Trim(); |
|
||||
string lineNo = parts[7].Trim(); |
|
||||
string prodType = parts[8].Trim(); |
|
||||
string specUl = parts[9].Trim(); |
|
||||
string specLl = parts[10].Trim(); |
|
||||
string retest = parts[11].Trim(); |
|
||||
|
|
||||
// 필터 적용
|
|
||||
if (judgmentFilter != "[전체]" && judg != judgmentFilter) continue; |
|
||||
if (!string.IsNullOrEmpty(serialFilter) && !id.Contains(serialFilter)) continue; |
|
||||
|
|
||||
results.Add(new InspectData |
|
||||
{ |
|
||||
InspectDate = date, |
|
||||
InspectTime = time, |
|
||||
Channel = channel, |
|
||||
ProductId = id, |
|
||||
MeasuredValue = val, |
|
||||
Judgment = judg, |
|
||||
Mode = mode, |
|
||||
LineNo = lineNo, |
|
||||
ProductType = prodType, |
|
||||
SpecUL = specUl, |
|
||||
SpecLL = specLl, |
|
||||
Retest = retest |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
catch (Exception ex) |
|
||||
{ |
|
||||
Console.WriteLine($"[LogParser] Error reading file {file}: {ex.Message}"); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return results.OrderByDescending(r => r.InspectDate).ThenByDescending(r => r.InspectTime).ToList(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,228 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.IO; |
||||
|
using System.Linq; |
||||
|
using System.Reflection; |
||||
|
using System.Text; |
||||
|
using System.Windows; |
||||
|
using leak_test_project.Models; |
||||
|
|
||||
|
namespace leak_test_project.Utils |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 통신 로그 및 시스템 이력을 파일로 저장하는 유틸리티
|
||||
|
/// </summary>
|
||||
|
public static class FileLogger |
||||
|
{ |
||||
|
private static readonly string LogDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs"); |
||||
|
private static readonly object _lock = new object(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 검사 데이터를 Logs/yyyy-MM-dd.csv 파일에 저장함
|
||||
|
/// </summary>
|
||||
|
public static void LogInspectData(InspectData data) |
||||
|
{ |
||||
|
lock (_lock) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
if (!Directory.Exists(LogDirectory)) |
||||
|
Directory.CreateDirectory(LogDirectory); |
||||
|
|
||||
|
string dateStr = DateTime.Now.ToString("yyyy-MM-dd"); |
||||
|
string filePath = Path.Combine(LogDirectory, $"{dateStr}.csv"); |
||||
|
|
||||
|
bool isNewFile = !File.Exists(filePath); |
||||
|
|
||||
|
// CSV 헤더: Date,Time,Channel,ID,Value,Judgment,Mode,LineNo,ProductType,SpecUL,SpecLL,Retest
|
||||
|
if (isNewFile) |
||||
|
{ |
||||
|
string header = "Date,Time,Channel,ID,Value,Judgment,Mode,LineNo,ProductType,SpecUL,SpecLL,Retest" + Environment.NewLine; |
||||
|
File.WriteAllText(filePath, header, Encoding.UTF8); |
||||
|
} |
||||
|
|
||||
|
string csvLine = $"{Esc(data.InspectDate)},{Esc(data.InspectTime)},{Esc(data.Channel)},{Esc(data.ProductId)}," + |
||||
|
$"{Esc(data.MeasuredValue)},{Esc(data.Judgment)},{Esc(data.Mode)},{Esc(data.LineNo)}," + |
||||
|
$"{Esc(data.ProductType)},{Esc(data.SpecUL)},{Esc(data.SpecLL)},{Esc(data.Retest)}{Environment.NewLine}"; |
||||
|
|
||||
|
File.AppendAllText(filePath, csvLine, Encoding.UTF8); |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
Console.WriteLine($"[FileLogger Error] {ex.Message}"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 단순 텍스트 로그 (기존 호환성 유지용)
|
||||
|
/// </summary>
|
||||
|
public static void Log(string tag, string message) |
||||
|
{ |
||||
|
lock (_lock) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
if (!Directory.Exists(LogDirectory)) |
||||
|
Directory.CreateDirectory(LogDirectory); |
||||
|
|
||||
|
string dateStr = DateTime.Now.ToString("yyyy-MM-dd"); |
||||
|
string filePath = Path.Combine(LogDirectory, $"{dateStr}_system.log"); |
||||
|
|
||||
|
string logEntry = $"[{DateTime.Now:HH:mm:ss.fff}] [{tag}] {message}{Environment.NewLine}"; |
||||
|
File.AppendAllText(filePath, logEntry, Encoding.UTF8); |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
Console.WriteLine($"[FileLogger Error] {ex.Message}"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
/// <summary>
|
||||
|
/// CSV 값 이스케이프: 쉼표, 따옴표, 줄바꿈이 포함된 값을 안전하게 감싸줌
|
||||
|
/// </summary>
|
||||
|
private static string Esc(string value) |
||||
|
{ |
||||
|
if (string.IsNullOrEmpty(value)) return ""; |
||||
|
if (value.Contains(",") || value.Contains("\"") || value.Contains("\n")) |
||||
|
return $"\"{value.Replace("\"", "\"\"")}\""; |
||||
|
return value; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static class LogParser |
||||
|
{ |
||||
|
private static readonly string LogDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs"); |
||||
|
|
||||
|
public static List<InspectData> ParseLogs(DateTime start, DateTime end, string judgmentFilter = "[전체]", string serialFilter = "") |
||||
|
{ |
||||
|
var results = new List<InspectData>(); |
||||
|
|
||||
|
if (!Directory.Exists(LogDirectory)) |
||||
|
{ |
||||
|
// 디렉토리가 없으면 빈 리스트 반환
|
||||
|
return results; |
||||
|
} |
||||
|
|
||||
|
// 날짜 범위 내의 모든 로그 파일 찾기 (.csv)
|
||||
|
var files = Directory.GetFiles(LogDirectory, "*.csv") |
||||
|
.Where(f => { |
||||
|
string fileName = Path.GetFileNameWithoutExtension(f); |
||||
|
if (DateTime.TryParse(fileName, out DateTime fileDate)) |
||||
|
{ |
||||
|
return fileDate.Date >= start.Date && fileDate.Date <= end.Date; |
||||
|
} |
||||
|
return false; |
||||
|
}); |
||||
|
|
||||
|
foreach (var file in files) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var lines = File.ReadAllLines(file); |
||||
|
if (lines.Length <= 1) continue; // Header only or empty
|
||||
|
|
||||
|
// CSV 헤더: Date,Time,Channel,ID,Value,Judgment,Mode,LineNo,ProductType,SpecUL,SpecLL,Retest
|
||||
|
for (int i = 1; i < lines.Length; i++) |
||||
|
{ |
||||
|
string line = lines[i]; |
||||
|
if (string.IsNullOrWhiteSpace(line)) continue; |
||||
|
var parts = line.Split(','); |
||||
|
if (parts.Length < 12) continue; |
||||
|
|
||||
|
string date = parts[0].Trim(); |
||||
|
string time = parts[1].Trim(); |
||||
|
string channel = parts[2].Trim(); |
||||
|
string id = parts[3].Trim(); |
||||
|
string val = parts[4].Trim(); |
||||
|
string judg = parts[5].Trim(); |
||||
|
string mode = parts[6].Trim(); |
||||
|
string lineNo = parts[7].Trim(); |
||||
|
string prodType = parts[8].Trim(); |
||||
|
string specUl = parts[9].Trim(); |
||||
|
string specLl = parts[10].Trim(); |
||||
|
string retest = parts[11].Trim(); |
||||
|
|
||||
|
// 필터 적용
|
||||
|
if (judgmentFilter != "[전체]" && judg != judgmentFilter) continue; |
||||
|
if (!string.IsNullOrEmpty(serialFilter) && !id.Contains(serialFilter)) continue; |
||||
|
|
||||
|
results.Add(new InspectData |
||||
|
{ |
||||
|
InspectDate = date, |
||||
|
InspectTime = time, |
||||
|
Channel = channel, |
||||
|
ProductId = id, |
||||
|
MeasuredValue = val, |
||||
|
Judgment = judg, |
||||
|
Mode = mode, |
||||
|
LineNo = lineNo, |
||||
|
ProductType = prodType, |
||||
|
SpecUL = specUl, |
||||
|
SpecLL = specLl, |
||||
|
Retest = retest |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
Console.WriteLine($"[LogParser] Error reading file {file}: {ex.Message}"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return results.OrderByDescending(r => r.InspectDate).ThenByDescending(r => r.InspectTime).ToList(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 데이터를 CSV 파일로 내보내는 유틸리티
|
||||
|
/// </summary>
|
||||
|
public static class CsvExporter |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 컬렉션 데이터를 CSV 파일로 저장함
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="T">데이터 모델 클래스</typeparam>
|
||||
|
/// <param name="items">내보낼 데이터 목록</param>
|
||||
|
/// <param name="filePath">저장할 파일 경로</param>
|
||||
|
public static bool ExportToCsv<T>(IEnumerable<T> items, string filePath) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var sb = new StringBuilder(); |
||||
|
var props = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance); |
||||
|
|
||||
|
// Header
|
||||
|
foreach (var prop in props) |
||||
|
{ |
||||
|
sb.Append(prop.Name).Append(","); |
||||
|
} |
||||
|
sb.AppendLine(); |
||||
|
|
||||
|
// Body
|
||||
|
foreach (var item in items) |
||||
|
{ |
||||
|
foreach (var prop in props) |
||||
|
{ |
||||
|
var val = prop.GetValue(item, null); |
||||
|
var str = val?.ToString() ?? ""; |
||||
|
if (str.Contains(",") || str.Contains("\"") || str.Contains("\n")) |
||||
|
sb.Append($"\"{str.Replace("\"", "\"\"")}\""); |
||||
|
else |
||||
|
sb.Append(str); |
||||
|
sb.Append(","); |
||||
|
} |
||||
|
sb.AppendLine(); |
||||
|
} |
||||
|
|
||||
|
File.WriteAllText(filePath, sb.ToString(), Encoding.UTF8); |
||||
|
return true; |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
MessageBox.Show($"CSV 저장 실패: {ex.Message}"); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,29 +0,0 @@ |
|||||
namespace leak_test_project.Utils |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Sentinel C28 전용 CRC-8 계산 유틸리티.
|
|
||||
/// 문서 4페이지의 에러 체크 규격(8-Bit CRC in HEX)을 구현함.
|
|
||||
/// </summary>
|
|
||||
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"); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,12 @@ |
|||||
|
[09:15:52.955] [ERROR] [Board4253] Failed to receive response after 3 retries: x00o |
||||
|
[09:15:54.567] [ERROR] [Board4253] Failed to receive response after 3 retries: x00c_001101:owt28006727ea97c7801 |
||||
|
[09:15:56.332] [ERROR] [Board4253] Failed to receive response after 3 retries: x00o |
||||
|
[09:15:57.938] [ERROR] [Board4253] Failed to receive response after 3 retries: x00c_002101:owt28006727ea97c7801 |
||||
|
[09:25:56.095] [ERROR] [Board4251] Failed to receive response after 3 retries: x00o |
||||
|
[09:25:57.708] [ERROR] [Board4251] Failed to receive response after 3 retries: x00c_002101:owt28006727ea97c7801 |
||||
|
[09:26:03.642] [ERROR] [Board4251] Failed to receive response after 3 retries: x00o |
||||
|
[09:26:05.253] [ERROR] [Board4251] Failed to receive response after 3 retries: x00c_001101:owt28006727ea97c7801 |
||||
|
[09:26:07.848] [ERROR] [Board4251] Failed to receive response after 3 retries: x00o |
||||
|
[09:26:09.454] [ERROR] [Board4251] Failed to receive response after 3 retries: x00c_002101:owt28006727ea97c7801 |
||||
|
[09:26:20.684] [WARNING] [Board4251] Timeout waiting for response (Retry 1/3). Command: x00c_002101:ow2800326003e, ReceivedSoFar: |
||||
|
[09:27:10.638] [WARNING] [Board4251] Timeout waiting for response (Retry 1/3). Command: x00c_002101:ow2800326003e, ReceivedSoFar: |
||||
@ -0,0 +1,2 @@ |
|||||
|
[11:07:28.075] [ERROR] [Board4251] Failed to receive response after 3 retries: x00o |
||||
|
[11:07:29.684] [ERROR] [Board4251] Failed to receive response after 3 retries: x00c_001101:owt28006727ea97c7801 |
||||
Binary file not shown.
@ -0,0 +1,6 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8" ?> |
||||
|
<configuration> |
||||
|
<startup> |
||||
|
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" /> |
||||
|
</startup> |
||||
|
</configuration> |
||||
Binary file not shown.
@ -0,0 +1,74 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<asmv1:assembly xsi:schemaLocation="urn:schemas-microsoft-com:asm.v1 assembly.adaptive.xsd" manifestVersion="1.0" xmlns:asmv1="urn:schemas-microsoft-com:asm.v1" xmlns="urn:schemas-microsoft-com:asm.v2" xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:co.v1="urn:schemas-microsoft-com:clickonce.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" xmlns:dsig="http://www.w3.org/2000/09/xmldsig#" xmlns:co.v2="urn:schemas-microsoft-com:clickonce.v2"> |
||||
|
<asmv1:assemblyIdentity name="leak_test_project.exe" version="1.0.0.22" publicKeyToken="0000000000000000" language="neutral" processorArchitecture="msil" type="win32" /> |
||||
|
<application /> |
||||
|
<entryPoint> |
||||
|
<assemblyIdentity name="leak_test_project" version="1.0.0.0" language="neutral" processorArchitecture="msil" /> |
||||
|
<commandLine file="leak_test_project.exe" parameters="" /> |
||||
|
</entryPoint> |
||||
|
<trustInfo> |
||||
|
<security> |
||||
|
<applicationRequestMinimum> |
||||
|
<PermissionSet Unrestricted="true" ID="Custom" SameSite="site" /> |
||||
|
<defaultAssemblyRequest permissionSetReference="Custom" /> |
||||
|
</applicationRequestMinimum> |
||||
|
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3"> |
||||
|
<!-- |
||||
|
UAC 매니페스트 옵션 |
||||
|
Windows 사용자 계정 컨트롤 수준을 변경하려면 |
||||
|
requestedExecutionLevel 노드를 다음 중 하나로 바꾸세요. |
||||
|
|
||||
|
<requestedExecutionLevel level="asInvoker" uiAccess="false" /> |
||||
|
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" /> |
||||
|
<requestedExecutionLevel level="highestAvailable" uiAccess="false" /> |
||||
|
|
||||
|
이전 버전과의 호환성을 위해 파일 및 레지스트리 가상화를 사용하려면 |
||||
|
requestedExecutionLevel 노드를 삭제하세요. |
||||
|
--> |
||||
|
<requestedExecutionLevel level="asInvoker" uiAccess="false" /> |
||||
|
</requestedPrivileges> |
||||
|
</security> |
||||
|
</trustInfo> |
||||
|
<dependency> |
||||
|
<dependentOS> |
||||
|
<osVersionInfo> |
||||
|
<os majorVersion="5" minorVersion="1" buildNumber="2600" servicePackMajor="0" /> |
||||
|
</osVersionInfo> |
||||
|
</dependentOS> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<dependentAssembly dependencyType="preRequisite" allowDelayedBinding="true"> |
||||
|
<assemblyIdentity name="Microsoft.Windows.CommonLanguageRuntime" version="4.0.30319.0" /> |
||||
|
</dependentAssembly> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<dependentAssembly dependencyType="install" allowDelayedBinding="true" codebase="leak_test_project.exe" size="173568"> |
||||
|
<assemblyIdentity name="leak_test_project" version="1.0.0.0" language="neutral" processorArchitecture="msil" /> |
||||
|
<hash> |
||||
|
<dsig:Transforms> |
||||
|
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" /> |
||||
|
</dsig:Transforms> |
||||
|
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" /> |
||||
|
<dsig:DigestValue>0Y9/ht3hpE+fHxvoEjUYeZTPBCNYvBFnq7mbbLVmzdQ=</dsig:DigestValue> |
||||
|
</hash> |
||||
|
</dependentAssembly> |
||||
|
</dependency> |
||||
|
<file name="leak_test_project.exe.config" size="189"> |
||||
|
<hash> |
||||
|
<dsig:Transforms> |
||||
|
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" /> |
||||
|
</dsig:Transforms> |
||||
|
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" /> |
||||
|
<dsig:DigestValue>R+Wg8QGvQVHX8T0ta/qbhH1bXkqY0fRnS3wBV3J0bN8=</dsig:DigestValue> |
||||
|
</hash> |
||||
|
</file> |
||||
|
<file name="PCI-Dask.dll" size="335872"> |
||||
|
<hash> |
||||
|
<dsig:Transforms> |
||||
|
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" /> |
||||
|
</dsig:Transforms> |
||||
|
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" /> |
||||
|
<dsig:DigestValue>pDB54gSYV5M/tu2DZtDUj13HUp3HrRLSNr97K/nGHh4=</dsig:DigestValue> |
||||
|
</hash> |
||||
|
</file> |
||||
|
</asmv1:assembly> |
||||
@ -0,0 +1,3 @@ |
|||||
|
[autorun] |
||||
|
open=setup.exe |
||||
|
icon=setup.exe,0 |
||||
@ -0,0 +1,21 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<asmv1:assembly xsi:schemaLocation="urn:schemas-microsoft-com:asm.v1 assembly.adaptive.xsd" manifestVersion="1.0" xmlns:asmv1="urn:schemas-microsoft-com:asm.v1" xmlns="urn:schemas-microsoft-com:asm.v2" xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:xrml="urn:mpeg:mpeg21:2003:01-REL-R-NS" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" xmlns:dsig="http://www.w3.org/2000/09/xmldsig#" xmlns:co.v1="urn:schemas-microsoft-com:clickonce.v1" xmlns:co.v2="urn:schemas-microsoft-com:clickonce.v2"> |
||||
|
<assemblyIdentity name="leak_test_project.application" version="1.0.0.22" publicKeyToken="0000000000000000" language="neutral" processorArchitecture="msil" xmlns="urn:schemas-microsoft-com:asm.v1" /> |
||||
|
<description asmv2:publisher="leak_test_project" asmv2:product="leak_test_project" xmlns="urn:schemas-microsoft-com:asm.v1" /> |
||||
|
<deployment install="true" mapFileExtensions="true" /> |
||||
|
<compatibleFrameworks xmlns="urn:schemas-microsoft-com:clickonce.v2"> |
||||
|
<framework targetVersion="4.7.2" profile="Full" supportedRuntime="4.0.30319" /> |
||||
|
</compatibleFrameworks> |
||||
|
<dependency> |
||||
|
<dependentAssembly dependencyType="install" codebase="Application Files\leak_test_project_1_0_0_22\leak_test_project.exe.manifest" size="3936"> |
||||
|
<assemblyIdentity name="leak_test_project.exe" version="1.0.0.22" publicKeyToken="0000000000000000" language="neutral" processorArchitecture="msil" type="win32" /> |
||||
|
<hash> |
||||
|
<dsig:Transforms> |
||||
|
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" /> |
||||
|
</dsig:Transforms> |
||||
|
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" /> |
||||
|
<dsig:DigestValue>aS0o/xVk0fJXJbjmEymfDZePIzgCcCd8N6ZW9l5KEAM=</dsig:DigestValue> |
||||
|
</hash> |
||||
|
</dependentAssembly> |
||||
|
</dependency> |
||||
|
</asmv1:assembly> |
||||
Binary file not shown.
Binary file not shown.
@ -1,14 +1,14 @@ |
|||||
<?xml version="1.0"?> |
<?xml version="1.0"?> |
||||
<AppConfig xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> |
<AppConfig xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> |
||||
<SelectedIdSensor>Board4253</SelectedIdSensor> |
<SelectedIdSensor>Board4251</SelectedIdSensor> |
||||
<LeftPort>COM1</LeftPort> |
<LeftPort>COM9</LeftPort> |
||||
<RightPort>COM1</RightPort> |
<RightPort>COM8</RightPort> |
||||
<ZmdiBaudRate>19200</ZmdiBaudRate> |
<ZmdiBaudRate>19200</ZmdiBaudRate> |
||||
<SensorPort>COM1</SensorPort> |
<SensorPort>COM1</SensorPort> |
||||
<SensorBaudRate>9600</SensorBaudRate> |
<SensorBaudRate>9600</SensorBaudRate> |
||||
<Board4253Port>COM11</Board4253Port> |
<Board4251Port>COM11</Board4251Port> |
||||
<Board4253BaudRate>115200</Board4253BaudRate> |
<Board4251BaudRate>115200</Board4251BaudRate> |
||||
<Board4253Timeout>500</Board4253Timeout> |
<Board4251Timeout>5000</Board4251Timeout> |
||||
<SpecUL>0.1</SpecUL> |
<SpecUL>1</SpecUL> |
||||
<SpecLL>-0.15</SpecLL> |
<SpecLL>-1</SpecLL> |
||||
</AppConfig> |
</AppConfig> |
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1 +1 @@ |
|||||
e34b793bb8bcf09fd6cf0746c906a5cb6a41ebb454d1a15e8953c71a7b0a3a10 |
5a72f1b3b73a2fa11c624e6d676bc467a15e8fd47fc6d1c698f6714d64884008 |
||||
|
|||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue