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

320 lines
13 KiB

using System;
using System.Diagnostics;
using System.IO;
using System.IO.Ports;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace jig_test.Services
{
/// <summary>
/// Semi IO Lite 보드 (SIO-0201A) 제어 서비스 클래스
/// 이 클래스는 LS산전 SV-iG5 인버터 호환 프로토콜(ASCII)을 사용하여 릴레이 출력을 제어하고 입력을 읽습니다.
/// </summary>
public class SemiIOLiteController : IDisposable
{
private SerialPort _serialPort; // MainWindow에서 생성한 공유 시리얼 포트 인스턴스
private readonly string _stationId; // 기기의 고유 국번 (기본값 "04")
// 프로토콜 특수 문자 정의 (ASCII 제어 문자)
private const char ENQ = (char)0x05; // 시작 문자 (Enquiry) - 요청용
private const char EOT = (char)0x04; // 종료 문자 (End of Transmission)
private const char ACK = (char)0x06; // 응답 문자 (Acknowledge) - 응답용
/// <summary>
/// 릴레이 상태 정의 (비트 조합 방식)
/// 릴레이 1과 2의 조합으로 공정 상태(고정, 인가, 배기)를 결정합니다.
/// </summary>
public enum RelayState
{
None = 0, // 00: 모든 릴레이 OFF
Exhaust = 1, // 01: 릴레이 1번 ON (배기 밸브 작동)
Pressurize = 2, // 10: 릴레이 2번 ON (인가 밸브 작동)
Clamp = 3 // 11: 릴레이 1, 2번 모두 ON (지그 고정 및 폐쇄)
}
public SemiIOLiteController(SerialPort sharedPort, int stationId = 2)
{
_serialPort = sharedPort;
_stationId = stationId.ToString("D2"); // 정수 ID를 2자리 문자열로 변환 (02 등)
}
/// <summary>
/// 시리얼 포트가 닫혀있을 경우 오픈을 시도합니다.
/// </summary>
public bool Connect()
{
try
{
if (!_serialPort.IsOpen) _serialPort.Open();
return true;
}
catch (Exception ex) when (ex is UnauthorizedAccessException || ex is IOException || ex is InvalidOperationException)
{
Debug.WriteLine($"[IO Board] 연결 실패 (포트 접근/상태 오류): {ex.Message}");
return false;
}
catch (Exception ex)
{
Debug.WriteLine($"[IO Board] 연결 실패 (기타): {ex.Message}");
return false;
}
}
/// <summary>
/// 릴레이 상태를 물리적으로 변경하는 명령을 보냅니다.
/// 형식: ENQ + 국번 + W(Write) + 주소(0001) + 갯수(1) + 데이터(000X) + SUM + EOT
/// </summary>
public async Task<bool> SetStateAsync(RelayState state)
{
if (_serialPort == null || !_serialPort.IsOpen) return false;
string commandStr = BuildCommand(state);
try
{
_serialPort.DiscardInBuffer(); // 기존 버퍼 비우기
_serialPort.Write(commandStr);
// 최대 500ms 동안 응답 대기 및 ACK 확인
byte[] buffer = new byte[1024];
int totalRead = 0;
var sw = Stopwatch.StartNew();
while (sw.ElapsedMilliseconds < 500)
{
if (_serialPort.BytesToRead > 0)
{
int count = _serialPort.Read(buffer, totalRead, buffer.Length - totalRead);
totalRead += count;
if (Array.IndexOf(buffer, (byte)0x04, 0, totalRead) != -1) break; // EOT 감지
}
await Task.Delay(10);
}
if (totalRead == 0) return false;
string response = Encoding.ASCII.GetString(buffer, 0, totalRead);
return ValidateResponse(response);
}
catch (Exception ex) when (ex is UnauthorizedAccessException || ex is IOException || ex is InvalidOperationException)
{
Debug.WriteLine($"[IO Board] 쓰기 명령 전송 실패 (포트 오류): {ex.Message}");
return false;
}
catch (Exception ex)
{
Debug.WriteLine($"[IO Board] 쓰기 명령 전송 에러: {ex.Message}");
return false;
}
}
/// <summary>
/// 통신 유효성을 확인하기 위해 기기의 현재 상태를 읽어봅니다. (핸드셰이크)
/// </summary>
public async Task<bool> CheckConnectionAsync()
{
if (!_serialPort.IsOpen) return false;
try
{
// 주소 0001의 데이터를 1개 읽어오는 명령 생성
string readCmd = BuildReadCommand();
_serialPort.DiscardInBuffer(); // 기존 버퍼 비우기
_serialPort.Write(readCmd);
// 최대 500ms 동안 응답 대기
byte[] buffer = new byte[1024];
int totalRead = 0;
var sw = Stopwatch.StartNew(); // #13: DateTime.Now 대신 Stopwatch 사용
while (sw.ElapsedMilliseconds < 500)
{
if (_serialPort.BytesToRead > 0)
{
int count = _serialPort.Read(buffer, totalRead, buffer.Length - totalRead);
totalRead += count;
// EOT 문자가 들어오면 한 프레임 완성으로 간주
if (Array.IndexOf(buffer, (byte)0x04, 0, totalRead) != -1) break;
}
await Task.Delay(10);
}
if (totalRead == 0) return false;
string response = Encoding.ASCII.GetString(buffer, 0, totalRead);
return ValidateResponse(response);
}
catch (Exception ex) when (ex is UnauthorizedAccessException || ex is IOException || ex is InvalidOperationException)
{
Debug.WriteLine($"[IO Board] CheckConnection 포트 접속 에러: {ex.Message}");
return false;
}
catch (Exception ex)
{
Debug.WriteLine($"[IO Board] CheckConnection 에러: {ex.Message}");
return false;
}
}
/// <summary>
/// 읽기용 프로토콜 패킷을 조립합니다.
/// </summary>
private string BuildReadCommand()
{
string body = $"{_stationId}R00011"; // 국번 + R + 주소 + 갯수
int sum = 0;
foreach (char c in body) sum += (int)c;
return $"{ENQ}{body}{(sum & 0xFF).ToString("X2")}{EOT}";
}
/// <summary>
/// 응답 패킷의 무결성을 검증합니다.
/// IO 보드 응답은 ACK(0x06)로 시작하고 EOT(0x04)로 끝납니다.
/// </summary>
private bool ValidateResponse(string response)
{
if (string.IsNullOrEmpty(response) || response.Length < 7) return false;
try
{
// 응답은 ACK 또는 ENQ로 시작할 수 있음 (기기에 따라 다름)
int startIdx = response.IndexOf(ACK);
if (startIdx == -1) startIdx = response.IndexOf(ENQ);
int eotIdx = response.LastIndexOf(EOT); // 마지막 EOT를 찾음
return (startIdx != -1 && eotIdx != -1 && eotIdx > startIdx);
}
catch
{
return false;
}
}
/// <summary>
/// 쓰기용 프로토콜 패킷을 조립합니다. (국번 + W + 주소 + 갯수 + 데이터)
/// </summary>
private string BuildCommand(RelayState state)
{
string data = ((int)state).ToString("X1");
string body = $"{_stationId}W00011000{data}";
int sum = 0;
foreach (char c in body) sum += (int)c;
return $"{ENQ}{body}{(sum & 0xFF).ToString("X2")}{EOT}";
}
/// <summary>
/// 지그 앞의 물리 버튼(i1 단자)이 눌렸는지 확인하는 요청을 보냅니다.
/// 주소 0A07번의 데이터를 1개 읽어옵니다.
/// </summary>
public bool RequestInputState(Action<byte[]> onTx = null)
{
if (_serialPort == null || !_serialPort.IsOpen) return false;
try
{
// 입력 상태 읽기 (주소 0A07)
string body = $"{_stationId}R0A071";
int sum = 0;
foreach (char c in body) sum += (int)c;
// [ENQ] + body + [SUM(2자리)] + [EOT]
string command = $"{ENQ}{body}{(sum & 0xFF).ToString("X2")}{EOT}";
byte[] cmdBytes = Encoding.ASCII.GetBytes(command);
onTx?.Invoke(cmdBytes); // 송신 로그 콜백 호출
_serialPort.Write(command);
return true;
}
catch (Exception ex) when (ex is UnauthorizedAccessException || ex is IOException || ex is InvalidOperationException)
{
Debug.WriteLine($"[IO Board] RequestInputState 포트 에러: {ex.Message}");
return false;
}
catch (Exception ex)
{
Debug.WriteLine($"[IO Board] RequestInputState 기타 에러: {ex.Message}");
return false;
}
}
/// <summary>
/// 수신된 바이트 버퍼를 해석하여 디지털 입력(i1, i2) 상태를 반환합니다.
/// </summary>
public bool ParseInputBuffer(byte[] buffer, out bool i1, out bool i2)
{
i1 = i2 = false;
if (buffer == null || buffer.Length == 0) return false;
string res = Encoding.ASCII.GetString(buffer);
// 정규식을 활용하여 응답 포맷 검증
// 'R' 이후에 오는 3자리 Hex 다음 1자리 값만 추출
var match = Regex.Match(res, @"R[0-9A-Fa-f]{3}([0-9A-Fa-f])");
if (match.Success)
{
char val = match.Groups[1].Value[0];
if (val == '1' || val == '3') i1 = true;
if (val == '2' || val == '3') i2 = true;
return true;
}
return false;
}
/// <summary>
/// 16진수 문자열 형태(예: 05303257...)의 Raw 커맨드들을 직접 바이트 배열로 변환하여 전송합니다.
/// </summary>
public async Task<bool> SendRawHexCommandAsync(string hexString, Action<byte[]> onTx = null)
{
if (_serialPort == null || !_serialPort.IsOpen) return false;
try
{
// Hex 문자열을 바이트 배열로 변환
byte[] buffer = new byte[hexString.Length / 2];
for (int i = 0; i < buffer.Length; i++)
buffer[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16);
onTx?.Invoke(buffer);
_serialPort.DiscardInBuffer();
_serialPort.Write(buffer, 0, buffer.Length);
// EOT까지 응답을 대기 (간단한 형태)
byte[] rxBuffer = new byte[1024];
int totalRead = 0;
var sw = Stopwatch.StartNew();
while (sw.ElapsedMilliseconds < 500)
{
if (_serialPort.BytesToRead > 0)
{
int count = _serialPort.Read(rxBuffer, totalRead, rxBuffer.Length - totalRead);
totalRead += count;
if (Array.IndexOf(rxBuffer, (byte)0x04, 0, totalRead) != -1) break; // EOT 감지
}
await Task.Delay(10);
}
if (totalRead == 0) return false;
string response = Encoding.ASCII.GetString(rxBuffer, 0, totalRead);
return ValidateResponse(response);
}
catch (Exception ex)
{
Debug.WriteLine($"[IO Board] SendRawHexCommand 에러: {ex.Message}");
return false;
}
}
/// <summary>
/// 포트 수명은 호출자(MainViewModel)에서 관리하므로, 여기서는 참조만 정리합니다.
/// </summary>
public void Dispose()
{
_serialPort = null;
}
}
}