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
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;
|
|
}
|
|
|
|
}
|
|
}
|
|
|