2026-06-05 13:44:39 +09:00

1025 lines
41 KiB
C#

using Aurora.Api.Devices;
using Hermes;
using Opc.Ua;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
namespace AuroraLaser
{
// -----------------------------------------------------------------------------------------------------------------
// PUBLIC SURFACE
// -----------------------------------------------------------------------------------------------------------------
public interface ILaserDiagnostics
{
Dictionary<string, double> GetLaserTemperatures(); // °C (0.1)
Dictionary<string, double> GetLaserCurrents(); // A (0.1)
Dictionary<string, double> GetLaserVoltages(); // V (0.1) - 추정 범용 파서
Dictionary<string, double> GetExtendedLaserCurrents(); // 확장 전류 (필요 시 채널명 매핑)
uint GetExternalTriggerRateHz(); // Hz
double GetOperatingHours(); // Hours
string GetSerialNumber();
string GetPartNumber();
string GetFirmwareDetails();
string GetLaserDescription();
}
public interface ILaserPulse
{
bool SetPulseWaveform(ushort index);
(ushort waveform, uint prf0) GetPulseWaveform();
bool SetPulseRate(uint prf);
uint GetPulseRate();
bool SetPulseBurstLength(uint pulses); // 0=continuous
uint GetPulseBurstLength();
bool SetPumpDutyFactor(ushort permille); // 0~1000
ushort GetPumpDutyFactor();
bool SetAllPulseGenParams(PulseGenParams p);
PulseGenParams GetAllPulseGenParams();
bool RestartPulseGenerator();
}
public struct PulseGenParams
{
public ushort Waveform; // 0~63
public uint PulseRate; // prf0
public uint BurstLength; // 0=continuous
public ushort DutyPermille; // 0~1000
}
public interface ILaserControl
{
bool SetInterfaceMode(byte mode); // 0=Local,1=Remote
byte GetInterfaceMode();
bool SetControlSignal(bool laserEnable, bool pilotEnable, bool cwPulse);
(bool laserEnable, bool pilotEnable, bool cwPulse) GetControlSignal();
bool SetAnalogueSetPoint(ushort channel, ushort value);
(ushort channel, ushort value) GetAnalogueSignal(ushort channel);
}
public interface ILaserSafety
{
bool ResetCriticalError();
bool ShutdownLaser();
bool RestoreDefaultSettings();
bool SaveCurrentSettings();
}
// -----------------------------------------------------------------------------------------------------------------
// IMPLEMENTATION (SocketConn은 기존 베이스를 사용)
// -----------------------------------------------------------------------------------------------------------------
public class TruPulse : SocketConn, ILaser, ILaserControl, ILaserDiagnostics, ILaserPulse, ILaserSafety, IExternalController
{
private readonly object _execLock = new object();
private readonly List<byte> _rxBuffer = new List<byte>();
public Func<bool, int> LaserEnableFunc { get; set; }
public Func<bool, int> LaserDisableFunc { get; set; }
public Func<bool, int> PilotOnOffFunc { get; set; }
public bool LaserReady { get; private set; }
public bool IsEmissionReady { get; private set; }
public bool HasAlarm { get; private set; }
//Hardware Interface Status
public bool TaskActive { get; private set; }
public bool LaserIsOn { get; private set; }
public bool LaserEmissionWarning { get; private set; }
public bool Laser_Deactivated { get; private set; }
public bool SystemAlarm { get; private set; }
public bool BeamDelivery { get; private set; }
public bool LaserTemperature { get; private set; }
public bool Alarm { get; private set; }
public bool Monitor { get; private set; }
public bool LaserEmissionGate { get; private set; }
public bool LaserDisable { get; private set; }
public bool LaserPulseCW { get; private set; }
public bool PilotLaserEnable { get; private set; }
public bool LaserEnable { get; private set; }
public int SelectedWaveForm { get; private set; }
public uint ActiveAnalog_mV { get; private set; }
public uint SimmerAnalog_mV { get; private set; }
private object locker = new object();
public List<string> AlarmReasons { get; private set; } = new List<string>();
//~Hardware Interface Status
private bool _IsSimul;
private Thread _statusThread;
private bool _stop = false;
public TruPulse(string deviceId, bool isSimul) : base(deviceId, isSimul)
{
_IsSimul = isSimul;
}
// ---------------- Connection / Thread ----------------
public int Open(LaserConfig cfg)
{
if (_IsSimul) return E1.OK;
int ret = Connect(cfg.SocketConfig);
if (ret == E1.OK) {
_stop = false;
_statusThread = new Thread(StatusLoop) { IsBackground = true };
_statusThread.Start();
}
return ret;
}
public void DisConnect()
{
if (_IsSimul) return;
_stop = true;
if (_statusThread != null && _statusThread.IsAlive) {
if (!_statusThread.Join(800)) _statusThread.Interrupt();
}
if (IsConnected()) Close();
}
private void StatusLoop()
{
while (!_stop) {
try {
var (statusWord, alarmCodes) = QueryStatusLinesAndAlarms();
Thread.Sleep(100);
UpdateAlarmCodes(alarmCodes);
var (Di1, Di2, activeAnal, SimmerAnal) = QueryStatusHwInterface();
TaskActive = (Di1 & (1 << 8)) != 0;
LaserIsOn = (Di1 & (1 << 7)) != 0;
LaserEmissionWarning = (Di1 & (1 << 6)) != 0;
Laser_Deactivated = (Di1 & (1 << 5)) == 0; // Active Low
SystemAlarm = (Di1 & (1 << 4)) == 0; // Active Low
BeamDelivery = (Di1 & (1 << 3)) == 0; // Active Low
LaserTemperature = (Di1 & (1 << 2)) == 0; // Active Low
Alarm = (Di1 & (1 << 1)) == 0; // Active Low
Monitor = (Di1 & (1 << 0)) == 0; // Active Low
LaserEmissionGate = (Di2 & (1 << 12)) != 0;
LaserDisable = (Di2 & (1 << 11)) != 0;
LaserPulseCW = (Di2 & (1 << 10)) != 0;
PilotLaserEnable = (Di2 & (1 << 9)) != 0;
LaserEnable = (Di2 & (1 << 8)) != 0;
SelectedWaveForm = (byte)(Di2 & 0xFF);
ActiveAnalog_mV = activeAnal;
SimmerAnalog_mV = SimmerAnal;
} catch (Exception ex) {
Debug.WriteLine($"StatusLoop: {ex.Message}");
}
Thread.Sleep(400);
}
}
private void UpdateAlarmCodes(List<ushort> alarmCodes)
{
lock (locker) {
if (alarmCodes != null && alarmCodes.Count > 0) {
var newReasons = new List<string>();
foreach (var code in alarmCodes) {
if (AlarmReasonTable.TryGetValue(code, out var reason))
newReasons.Add($"{code}: {reason}");
else
newReasons.Add($"{code}: Unknown alarm code");
}
if (!Enumerable.SequenceEqual(AlarmReasons, newReasons)) {
AlarmReasons = newReasons;
}
}
else {
if (AlarmReasons.Count > 0) {
AlarmReasons.Clear();
}
}
}
}
public string GetAlarmReasonsString(List<ushort> alarmCodes)
{
if (alarmCodes == null || alarmCodes.Count == 0)
return "No active alarms";
List<string> reasons = new List<string>();
foreach (var code in alarmCodes) {
if (AlarmReasonTable.TryGetValue(code, out var reason))
reasons.Add($"{code}: {reason}");
else if (code >= 100)
reasons.Add($"{code}: System fault: internal laser fault");
else
reasons.Add($"{code}: Unknown alarm code");
}
return string.Join(", ", reasons);
}
// ---------------- Masks (요약본) ----------------
private readonly Dictionary<string, uint> StatusMask = new Dictionary<string, uint>()
{
{ "LaserEmissionSlowSignal", 0x00000001 },
{ "LaserReady", 0x00000008 },
{ "ChillerReady", 0x00000400 },
{ "ErrorOccurred", 0x00002000 },
{ "BeamSwitchReady", 0x00004000 },
{ "ShutdownState", 0x10000000 },
{ "StandbyState", 0x40000000 },
};
private readonly Dictionary<string, uint> AlarmMask = new Dictionary<string, uint>()
{
{ "laserFiberInterlock", 0x00000001 },
{ "laserOverheating", 0x00000002 },
{ "HighBackReflection", 0x00000004 },
{ "LaserModuleFailed", 0x00000020 },
{ "ChillerError", 0x00800000 },
{ "CriticalErrorLaser", 0x02000000 },
{ "EStopButtonPressed", 0x20000000 },
};
private static readonly Dictionary<ushort, string> AlarmReasonTable = new Dictionary<ushort, string>()
{
{ 40, "System fault: diode driver current" },
{ 41, "System fault: diode driver current" },
{ 42, "System fault: diode driver current" },
{ 43, "System fault: diode driver current" },
{ 44, "System fault: diode driver current" },
{ 45, "System fault: diode driver current" },
{ 46, "System fault: diode driver current" },
{ 47, "System fault: diode driver current" },
{ 48, "System fault: diode driver current" },
{ 49, "System fault: diode driver current" },
{ 50, "System fault: seed laser" },
{ 51, "System fault: seed laser" },
{ 52, "System fault: seed laser" },
{ 53, "System fault: seed laser" },
{ 65, "Beam delivery temperature sensor fault" },
{ 66, "Beam delivery temperature alarm" },
{ 80, "Base plate temperature alarm" },
{ 82, "Base plate temperature sensor fault" },
{ 93, "Power supply alarm" },
{ 95, "Fan alarm" },
{ 99, "Emergency stop alarm" },
};
// -----------------------------------------------------------------------------------------------------------------
// CONTROL (0x14, 0x15, 0x1A, 0x1B, 0x1C, 0x1D)
// -----------------------------------------------------------------------------------------------------------------
public bool SetInterfaceMode(byte mode)
{
var ok = ExecAck(eFunctionCommand.SetInterfaceMode, new byte[] { mode });
return ok;
}
public byte GetInterfaceMode()
{
var recv = Exec(eFunctionCommand.GetInterfaceMode, null);
// 응답 Data[0] = 현재 모드
return recv.DataByte(0);
}
public bool SetControlSignal(bool laserEnable, bool pilotEnable, bool cwPulse)
{
ushort bits = 0;
if (laserEnable) bits |= 1 << 0;
if (pilotEnable) bits |= 1 << 1;
if (cwPulse) bits |= 1 << 2;
byte[] args = U16BE(bits);
return ExecAck(eFunctionCommand.SetControlSignal, args);
}
public (bool laserEnable, bool pilotEnable, bool cwPulse) GetControlSignal()
{
try {
var r = Exec(eFunctionCommand.GetControlSignal, null);
byte b = r.DataByte(0);
bool le = (b & (1 << 0)) != 0;
bool pe = (b & (1 << 1)) != 0;
bool cw = (b & (1 << 2)) != 0;
return (le, pe, cw);
}
catch (Exception ex) {
Log.E($"GetControlSignal + {ex.Message}");
return (false, false, false);
}
}
// channel: 0=active current, 1=simmer (매뉴얼 값)
public bool SetAnalogueSetPoint(ushort channel, ushort value)
{
byte[] args = new byte[4];
Array.Copy(U16BE(channel), 0, args, 0, 2);
Array.Copy(U16BE(value), 0, args, 2, 2);
return ExecAck(eFunctionCommand.SetAnalogueSetPoint, args);
}
public (ushort channel, ushort value) GetAnalogueSignal(ushort channel)
{
try {
byte[] arg = U16BE(channel);
var r = Exec(eFunctionCommand.GetAnalogueSignal, arg);
// r.Data[0]=0x00, r.Data[1]=response code(0=OK), r.Data[2..3]=값(0-4095 등)
ushort val = r.DataU16BE(2);
return (channel, val);
}
catch (Exception ex) {
Log.E($"GetAnaloguSignal + {ex.Message}");
return (0, 0);
}
}
// -----------------------------------------------------------------------------------------------------------------
// PULSE GENERATOR (0x20~0x27, 0x2C, 0x2D, 0x2F)
// -----------------------------------------------------------------------------------------------------------------
public bool SetPulseWaveform(ushort index)
{
try {
return ExecAck(eFunctionCommand.SetPulseWaveForm, U16BE(index));
}
catch (Exception ex) {
Log.E($"SetPulseWaveform + {ex.Message}");
return false;
}
}
public (ushort waveform, uint prf0) GetPulseWaveform()
{
try {
var r = Exec(eFunctionCommand.GetPulseWaveForm, null);
ushort wf = r.DataU16BE(0);
uint prf = r.DataU32BE(2);
return (wf, prf);
}
catch (Exception ex) {
Log.E($"GetPulseWaveform + {ex.Message}");
return (0,0);
}
}
public bool SetPulseRate(uint prf)
{
return ExecAck(eFunctionCommand.SetPulseRate, U32BE(prf));
}
public uint GetPulseRate()
{
var r = Exec(eFunctionCommand.GetPulseRate, null);
return r.DataU32BE(0);
}
public bool SetPulseBurstLength(uint pulses)
{
return ExecAck(eFunctionCommand.SetPulseBurstLength, U32BE(pulses));
}
public uint GetPulseBurstLength()
{
var r = Exec(eFunctionCommand.GetPulseBurstLength, null);
return r.DataU32BE(0);
}
public bool SetPumpDutyFactor(ushort permille)
{
return ExecAck(eFunctionCommand.SetPumpDutyFactor, U16BE(permille));
}
public ushort GetPumpDutyFactor()
{
var r = Exec(eFunctionCommand.GetPumpDutyFactor, null);
return r.DataU16BE(0);
}
public bool SetAllPulseGenParams(PulseGenParams p)
{
// 매뉴얼 규격: (파라미터 패킹 순서/크기 확인 필요)
// 여기서는 [Waveform(2) + Rate(4) + Burst(4) + Duty(2)]로 패킹
var buf = new List<byte>(12);
buf.AddRange(U16BE(p.Waveform));
buf.AddRange(U32BE(p.PulseRate));
buf.AddRange(U32BE(p.BurstLength));
buf.AddRange(U16BE(p.DutyPermille));
return ExecAck(eFunctionCommand.SetAllPulseGeneratorParameters, buf.ToArray());
}
public PulseGenParams GetAllPulseGenParams()
{
var r = Exec(eFunctionCommand.GetAllPulseGeneratorParameters, null);
PulseGenParams p = new PulseGenParams();
p.Waveform = r.DataU16BE(0);
p.PulseRate = r.DataU32BE(2);
p.BurstLength = r.DataU32BE(6);
p.DutyPermille = r.DataU16BE(10);
return p;
}
public bool RestartPulseGenerator()
{
return ExecAck(eFunctionCommand.RestartPulseGenerator, null);
}
// -----------------------------------------------------------------------------------------------------------------
// MONITORING / DIAGNOSTICS (0x50~0x57)
// -----------------------------------------------------------------------------------------------------------------
public (ushort statusWord, List<ushort> alarmCodes) QueryStatusLinesAndAlarms()
{
var r = Exec(eFunctionCommand.QueryStatusLinesAndAlarms, null);
ushort status = r.DataU16BE(0);
List<ushort> alarms = new List<ushort>();
for (int i = 2; i < r.DataLength; i += 2)
alarms.Add(r.DataU16BE(i));
return (status, alarms);
}
public Dictionary<string, double> GetLaserTemperatures()
{
var r = Exec(eFunctionCommand.QueryLaserTemperatures, null);
// 2B*4, 0.1°C
return new Dictionary<string, double> {
["Module1"] = r.DataU16BE(0) / 10.0,
["Module2"] = r.DataU16BE(2) / 10.0,
["Combiner"] = r.DataU16BE(4) / 10.0,
["Controller"] = r.DataU16BE(6) / 10.0
};
}
public Dictionary<string, double> GetLaserCurrents()
{
var r = Exec(eFunctionCommand.QueryLaserCurrents, null);
// 2B*3, 0.1A
return new Dictionary<string, double> {
["Pump1"] = r.DataU16BE(0) / 10.0,
["Pump2"] = r.DataU16BE(2) / 10.0,
["SeedLaser"] = r.DataU16BE(4) / 10.0,
};
}
public Dictionary<string, double> GetLaserVoltages()
{
var r = Exec(eFunctionCommand.QueryLaserVoltages, null);
// 일반적으로 2B*3, 0.1V 가 많음(모델 의존). 안전하게 길이에 맞춰 group 파싱.
return ParsePairsAsDictionary(r, scale: 0.1, prefix: "V");
}
public (uint DI1, uint DI2, uint ActiveAnalog, uint SimmerAnalog) QueryStatusHwInterface()
{
try {
var r = Exec(eFunctionCommand.QueryHardwareInterface, null);
uint DI1 = r.DataU16BE(0);
uint DI2 = r.DataU16BE(2);
uint ActiveAnalog = r.DataU16BE(4);
uint SimmerAnalog = r.DataU16BE(6);
return (DI1, DI2, ActiveAnalog, SimmerAnalog);
} catch (Exception ex) {
Log.E($"QueryStatusLinesAndAlarms : {ex.Message}");
return (0, 0, 0, 0);
}
}
public uint GetExternalTriggerRateHz()
{
var r = Exec(eFunctionCommand.QueryExternalPulseTriggerRepetitionRate, null);
return r.DataU32BE(0);
}
public Dictionary<string, double> GetExtendedLaserCurrents()
{
var r = Exec(eFunctionCommand.QueryExtendedLaserCurrents, null);
return ParsePairsAsDictionary(r, scale: 0.1, prefix: "Iext");
}
// -----------------------------------------------------------------------------------------------------------------
// DIAG / MAINT (0x60~0x65)
// -----------------------------------------------------------------------------------------------------------------
public bool SetLaserIntoDiagnosisState()
{
return ExecAck(eFunctionCommand.SetLaserIntoDiagnosisState, null);
}
public bool ResetLaserFromDiagnosis()
{
// 일부 펌웨어에서 0x61 존재(메뉴얼 변형 시). 없으면 Set→모드 종료 절차 필요.
return ExecAck(eFunctionCommand.ResetLaserFromDiagnosis, null);
}
public string GetSerialNumber() => ExecAscii(eFunctionCommand.GetLaserSerialNumber);
public string GetPartNumber() => ExecAscii(eFunctionCommand.GetLaserPartNumber);
public string GetFirmwareDetails() => ExecAscii(eFunctionCommand.GetLaserFirmwareDetails);
public string GetLaserDescription() => ExecAscii(eFunctionCommand.GetLaserDescription);
// -----------------------------------------------------------------------------------------------------------------
// ERROR / CONTROL (0x70~0x73)
// -----------------------------------------------------------------------------------------------------------------
public bool ResetCriticalError() => ExecAck(eFunctionCommand.ResetCriticalError, null);
public bool ShutdownLaser() => ExecAck(eFunctionCommand.ShutdownLaser, null);
public bool RestoreDefaultSettings() => ExecAck(eFunctionCommand.RestoreDefaultSettings, null);
public bool SaveCurrentSettings() => ExecAck(eFunctionCommand.SaveCurrentSettings, null);
// -----------------------------------------------------------------------------------------------------------------
// ASCII 응답을 반환하는 명령(GetLaserSerialNumber 등)을 처리
// -----------------------------------------------------------------------------------------------------------------
private string ExecAscii(eFunctionCommand cmd)
{
// 1. 기본 송수신 수행
var r = Exec(cmd, null);
// 2. 유효 데이터 검증
if (r.Data == null || r.Data.Length == 0)
return string.Empty;
// 3. 종료문자(0x00) 또는 0xFF 패딩 제거
int len = Array.FindIndex(r.Data, b => b == 0x00);
if (len < 0) len = r.Data.Length; // 없으면 전체 길이 사용
// 4. ASCII 디코딩
string ascii = Encoding.ASCII.GetString(r.Data, 0, len);
// 5. 가끔 들어오는 제어문자, 공백, CRLF 제거
ascii = ascii.Replace("\r", "").Replace("\n", "").Trim();
// 6. 필요시 로그 출력 (디버깅용)
//Debug.WriteLine($"[ExecAscii] CMD={cmd} -> \"{ascii}\"");
return ascii;
}
// -----------------------------------------------------------------------------------------------------------------
// CORE TX/RX + PARSERS
// -----------------------------------------------------------------------------------------------------------------
private bool ExecAck(eFunctionCommand cmd, byte[] args)
{
var r = Exec(cmd, args);
return r.ResponseOk;
}
private Recv Exec(eFunctionCommand cmd, byte[] args)
{
const int OverallTimeoutMs = 1000; // 전체 응답 대기 시간 (필요시 조절)
const int ChunkTimeoutMs = 100; // Recieve 한 번당 최대 대기 시간
lock (_execLock) {
// ---------------------------
// 1. 패킷 구성
// ---------------------------
int dataLen = 2 + (args?.Length ?? 0); // 0x09 + CMD + args...
int plen = 6 + (args?.Length ?? 0); // Start,N,0x09,CMD,args...,0x0D,CS
byte[] pkt = new byte[plen];
int i = 0;
pkt[i++] = 0x1B; // Start
pkt[i++] = (byte)dataLen; // N
pkt[i++] = 0x09; // CmdStart
pkt[i++] = (byte)cmd; // Command
if (args != null && args.Length > 0) {
Buffer.BlockCopy(args, 0, pkt, i, args.Length);
i += args.Length;
}
pkt[i++] = 0x0D; // Stop
pkt[i++] = Checksum(pkt, i - 1); // CS (Start~Stop까지 합의 LSB)
// ---------------------------
// 2. 송신
// ---------------------------
int sendRet = Send(pkt);
if (sendRet != 0)
throw new InvalidOperationException("Send failed");
// ---------------------------
// 3. 응답 수신 + 프레임 조립
// ---------------------------
int startTick = Environment.TickCount;
while (true) {
// 3-1. 버퍼에서 프레임이 완성됐는지 먼저 확인
if (TryParseOneFrameFromBuffer(out Recv parsed)) {
if (!parsed.ProtocolOk) {
string err = DecodeResponseError(parsed.ResponseValue,
parsed.Command,
parsed.DataByte(0));
Debug.WriteLine($"[Laser Error] {cmd} => {err}");
throw new InvalidOperationException(err);
}
// 내가 방금 보낸 cmd 에 대한 응답이면 OK
if (parsed.Command == (byte)cmd || parsed.Command == 0x00)
return parsed;
// 아니면 이전 명령의 늦게 도착한 응답 or 기타 프레임 → 무시만 하고 계속 대기
Debug.WriteLine($"[Warn] Unexpected frame for {cmd}, got CMD=0x{parsed.Command:X2}, ignore.");
// 다시 while 루프 계속
continue;
}
// 3-2. 아직 프레임이 안 만들어졌다면, 추가 수신 시도
int elapsed = Environment.TickCount - startTick;
if (elapsed >= OverallTimeoutMs)
throw new TimeoutException($"Timeout waiting for response of CMD=0x{(byte)cmd:X2}");
// SocketConn.Recieve 호출 (Chunk 단위로 조금씩 기다리기)
if (Recieve(out var chunk, ChunkTimeoutMs) == 0 && chunk != null && chunk.Length > 0) {
_rxBuffer.AddRange(chunk);
// 바로 다음 루프에서 TryParseOneFrameFromBuffer 재시도
} else {
// 이번 chunk 타임아웃(데이터 없음) → 다시 while로 가서 전체 타임아웃 체크
}
}
}
}
private bool TryParseOneFrameFromBuffer(out Recv parsed)
{
parsed = default;
while (true) {
// 최소 Start(1) + N(1)은 있어야 함
if (_rxBuffer.Count < 2)
return false;
// 1) Start 바이트(0x1B) 위치 찾기
int startIndex = _rxBuffer.FindIndex(b => b == 0x1B);
if (startIndex < 0) {
// Start 못 찾으면 버퍼 전체 폐기
_rxBuffer.Clear();
return false;
}
// Start 이전의 쓰레기 데이터 제거
if (startIndex > 0)
_rxBuffer.RemoveRange(0, startIndex);
if (_rxBuffer.Count < 2)
return false;
byte N = _rxBuffer[1];
// 전체 프레임 길이 = 2(Start+N) + N + 2(Stop+CS) = N + 4
int frameLen = N + 4;
if (frameLen < 7) {
// 명세상 최소 7바이트보다 짧으면 잘못된 길이 → Start 하나 날리고 다시
_rxBuffer.RemoveAt(0);
continue;
}
if (_rxBuffer.Count < frameLen) {
// 아직 전체 프레임이 다 안 들어옴
return false;
}
// 여기까지 왔으면 프레임 하나 분량은 버퍼에 존재
byte[] raw = _rxBuffer.GetRange(0, frameLen).ToArray();
_rxBuffer.RemoveRange(0, frameLen);
try {
parsed = Recv.Parse(raw);
return true;
} catch (Exception ex) {
// 이 프레임이 잘못된 경우 → 버리고 다음 Start 탐색
Debug.WriteLine($"[Warn] Drop invalid frame: {ex.Message}");
// 계속 while 루프 돌아서 다음 Start 탐색
continue;
}
}
}
private static byte Checksum(byte[] data, int lengthInclusive)
{
uint s = 0;
for (int k = 0; k <= lengthInclusive; k++) s += data[k];
return (byte)(s & 0xFF);
}
private static byte[] U16BE(ushort v) => new byte[] { (byte)(v >> 8), (byte)(v & 0xFF) };
private static byte[] U32BE(uint v) => new byte[] { (byte)(v >> 24), (byte)((v >> 16) & 0xFF), (byte)((v >> 8) & 0xFF), (byte)(v & 0xFF) };
private Dictionary<string, double> ParsePairsAsDictionary(Recv r, double scale, string prefix)
{
var dict = new Dictionary<string, double>();
int pairs = r.DataLength / 2;
for (int n = 0; n < pairs; n++) {
ushort raw = r.DataU16BE(n * 2);
dict[$"{prefix}{n + 1}"] = raw * scale / 1.0;
}
return dict;
}
// -----------------------------------------------------------------------------------------------------------------
// RECV STRUCT
// -----------------------------------------------------------------------------------------------------------------
private struct Recv
{
public byte Start; // 0x1B
public byte N;
public byte CmdStart; // 0x09 or 0x00(err)
public byte ResponseValue;// R
public byte Command; // Echoed CMD (or 0x00+X1 error)
public byte[] Data; // payload y bytes
public byte Stop; // 0x0D
public byte Csum;
public bool ProtocolOk => CmdStart == 0x09 && ResponseValue == 0x00;
public bool ResponseOk => ProtocolOk && (DataLength == 0 || DataByte(0) == 0x00);
public int DataLength => Data?.Length ?? 0;
public byte DataByte(int idx) => (Data != null && idx < Data.Length) ? Data[idx] : (byte)0x00;
public ushort DataU16BE(int idx)
{
if (Data == null || Data.Length < idx + 2) return 0;
return (ushort)((Data[idx] << 8) | Data[idx + 1]);
}
public uint DataU32BE(int idx)
{
if (Data == null || Data.Length < idx + 4) return 0;
return (uint)((Data[idx] << 24) | (Data[idx + 1] << 16) | (Data[idx + 2] << 8) | Data[idx + 3]);
}
public static Recv Parse(byte[] raw)
{
if (raw == null)
throw new ArgumentNullException(nameof(raw));
if (raw.Length < 7)
throw new ArgumentException("Short RX");
Recv r = new Recv();
int i = 0;
r.Start = raw[i++];
if (r.Start != 0x1B)
throw new ArgumentException("Invalid start byte");
r.N = raw[i++];
r.CmdStart = raw[i++];
r.ResponseValue = raw[i++];
r.Command = raw[i++];
int dataCount = r.N - 3; // N = (0x09 + R + CMD + yData) => yData = N - 3
if (dataCount < 0)
throw new ArgumentException("Invalid length field");
int expectedLen = 2 + r.N + 2; // Start(1) + N(1) + N bytes + Stop + Csum
if (raw.Length != expectedLen)
throw new ArgumentException($"Frame length mismatch. Expected {expectedLen}, got {raw.Length}");
r.Data = new byte[dataCount];
if (dataCount > 0) {
Buffer.BlockCopy(raw, i, r.Data, 0, dataCount);
i += dataCount;
}
r.Stop = raw[i++];
if (r.Stop != 0x0D)
throw new ArgumentException("Invalid stop byte");
r.Csum = raw[i++];
// Checksum 검증 (Start~Stop까지 합의 LSB)
byte calc = Checksum(raw, expectedLen - 2); // Stop 위치 = expectedLen-2
if (calc != r.Csum)
throw new ArgumentException("Checksum mismatch");
return r;
}
}
public int LaserOn()
{
var ret = LaserDisableFunc(false);
Thread.Sleep(100);
ret += LaserEnableFunc(true);
Thread.Sleep(100);
return ret == E1.OK ? E1.OK: E1.NETWORK;
return SetControlSignal(laserEnable:true, pilotEnable:false, cwPulse:false) ? E1.OK : E2.ABORTED;
}
public int LaserOff()
{
return LaserEnableFunc(false);
return SetControlSignal(laserEnable:false, pilotEnable:false, cwPulse:false) ? E1.OK : E2.ABORTED;
}
public int PilotLaserOn()
{
return PilotOnOffFunc(true);
return SetControlSignal(laserEnable:false, pilotEnable:true, cwPulse:false) ? E1.OK : E2.ABORTED;
}
public int PilotLaserOff()
{
return PilotOnOffFunc(false);
return SetControlSignal(laserEnable: false, pilotEnable: false, cwPulse: false) ? E1.OK : E2.ABORTED;
}
public int CwPulseOn()
{
return SetControlSignal(laserEnable: false, pilotEnable: false, cwPulse: true) ? E1.OK : E2.ABORTED;
}
public int CwPulseOff()
{
return SetControlSignal(laserEnable: false, pilotEnable: false, cwPulse: false) ? E1.OK : E2.ABORTED;
}
public bool GetInitCommunication() => true;
public bool GetEmissionStatus() => IsEmissionReady;
public bool GetLaserReady() {
return LaserEnable;
}
/// <summary>
/// 1) Laser_Disable = 1 → 문제 해결
/// 2) Laser_Disable = 0
/// 3) Laser_Enable = 0 → 1 (필요시 2회 반복)
/// 4) ResetCriticalError(0x70)
///
/// 1) OFF Shutdown 알람 상태 / PSU 전원 차단
/// 2) ON Wait for Reset 인터락 클리어 중(ResetCriticalError 수행)
/// 3) OFF Standby PSU 재시작 준비
/// 4) ON Ready → Emission Ready 정상 기동 완료
/// 5) ResetCriticalError(0x70)
/// </summary>
/// <returns></returns>
public bool AlarmReset()
{
try {
if (!IsConnected())
return false;
if (AlarmReasons.Count > 0) {
LaserDisableFunc(false);
LaserEnableFunc(false);
Thread.Sleep(300);
LaserEnableFunc(true);
Thread.Sleep(200);
LaserEnableFunc(false);
Thread.Sleep(200);
LaserEnableFunc(true);
//SetControlSignal(laserEnable: false, pilotEnable: false, cwPulse: false);
//Thread.Sleep(200);
//SetControlSignal(laserEnable: true, pilotEnable: false, cwPulse: false);
//Thread.Sleep(300);
//SetControlSignal(laserEnable: false, pilotEnable: false, cwPulse: false);
//Thread.Sleep(200);
//SetControlSignal(laserEnable: true, pilotEnable: false, cwPulse: false);
//Thread.Sleep(200);
ResetCriticalError();
Thread.Sleep(200);
LaserEnableFunc(false);
}
return true;
}
catch (Exception ex) {
Log.E($"Laser Reset Fail : {ex.Message}");
return false;
}
}
public bool GetLaserError() => AlarmReasons.Count > 0;
public bool GetWariningStatus() => false; // 필요 시 알람/워닝 분리
public bool GetPilotLaserStatus() {
return PilotLaserEnable;
}
public string GetControlMode() => GetInterfaceMode() == 1 ? "Remote" : "Local";
public string GetPowerCenter() => ""; // 장비 모델별 확장 시 채움
public string GetPowerRing() => ""; // 장비 모델별 확장 시 채움
public bool GetStartLaserSetBitStatus() => false;
public bool GetLaserStartSetBitStatus() => GetControlSignal().laserEnable;
public bool GetPilotLaserSetBitStatus() {
return PilotLaserEnable;
}
public bool GetLaserResetSetBitStatus() => false;
public bool GetControlModeSetStatus() => GetInterfaceMode() == 1;
public bool GetConnected() => IsConnected();
public int SetData(int nTarget, int value) => 0;
public string GetData(int nTarget) => string.Empty;
public double GetOperatingHours() => 0.0;
private string DecodeResponseError(byte responseCode, byte laserCmd, byte dataX1)
{
// R = Response Value (4번째 바이트)
// laserCmd = Command (5번째 바이트)
// dataX1 = 첫 데이터 바이트 (X₁)
if (responseCode == 0x00) {
// 레이저 명령 처리 실패 (Command = 0)
switch (dataX1) {
case 0: return "Command not recognized (unknown or unsupported)";
case 2: return "Too few data bytes (missing parameters)";
case 3: return "Too many data bytes (payload too long)";
default: return $"Protocol error code {dataX1}";
}
}
switch (responseCode) {
case 0x09: return "OK";
case 0x00: return "Protocol error: missing start byte (0x09)";
case 2: return "Unknown command";
case 3: return "Invalid stop byte (expected 0x0D)";
case 4: return "Checksum error";
case 5: return "Payload too long";
case 6: return "Timeout: incomplete command received";
default: return $"Unknown protocol error R={responseCode}";
}
}
public void SetMode4LaserOn(ushort simmer_mA = 100, ushort analog_mA = 2500)
{
// 1) Remote 제어 모드
if(GetInterfaceMode() != 4)
SetInterfaceMode(4);
// 2) Mode 4 (Digital Modulation)
SetPulseWaveform(0);
// 3) Simmer 설정 (예: 100mA)
SetAnalogueSetPoint(1, simmer_mA);
// 4) Active current 설정 (예: 2500mA)
SetAnalogueSetPoint(0, analog_mA);
// 5) Laser Enable ON
SetControlSignal(laserEnable: true, pilotEnable: false, cwPulse: false);
// 6) LaserOn() 실행
LaserOn();
}
}
// -----------------------------------------------------------------------------------------------------------------
// ENUM 확장 (전체)
// -----------------------------------------------------------------------------------------------------------------
public enum eFunctionCommand : byte
{
// Communication & Interface
SetBaudRate = 0x10,
GetBaudRate = 0x11,
SetInterfaceMode = 0x14,
GetInterfaceMode = 0x15,
// Control / Operation
SetControlSignal = 0x1A,
GetControlSignal = 0x1B,
SetAnalogueSetPoint = 0x1C,
GetAnalogueSignal = 0x1D,
// Pulse Generator
SetPulseWaveForm = 0x20,
GetPulseWaveForm = 0x21,
SetPulseRate = 0x22,
GetPulseRate = 0x23,
SetPulseBurstLength = 0x24,
GetPulseBurstLength = 0x25,
SetPumpDutyFactor = 0x26,
GetPumpDutyFactor = 0x27,
SetAllPulseGeneratorParameters = 0x2C,
GetAllPulseGeneratorParameters = 0x2D,
RestartPulseGenerator = 0x2F,
// Monitoring / Diagnostics
QueryStatusLinesAndAlarms = 0x50,
QueryLaserTemperatures = 0x51,
QueryLaserCurrents = 0x52,
QueryLaserVoltages = 0x53,
QueryHardwareInterface = 0x54,
QueryOperatingHours = 0x55,
QueryExternalPulseTriggerRepetitionRate = 0x56,
QueryExtendedLaserCurrents = 0x57,
// Diagnostic / Maintenance
SetLaserIntoDiagnosisState = 0x60,
ResetLaserFromDiagnosis = 0x61, // 펌웨어에 따라 미구현 가능
GetLaserSerialNumber = 0x62,
GetLaserPartNumber = 0x63,
GetLaserFirmwareDetails = 0x64,
GetLaserDescription = 0x65,
// Error / Control
ResetCriticalError = 0x70,
ShutdownLaser = 0x71,
RestoreDefaultSettings = 0x72,
SaveCurrentSettings = 0x73,
}
}