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 GetLaserTemperatures(); // °C (0.1) Dictionary GetLaserCurrents(); // A (0.1) Dictionary GetLaserVoltages(); // V (0.1) - 추정 범용 파서 Dictionary 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 _rxBuffer = new List(); public Func LaserEnableFunc { get; set; } public Func LaserDisableFunc { get; set; } public Func 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 AlarmReasons { get; private set; } = new List(); //~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 alarmCodes) { lock (locker) { if (alarmCodes != null && alarmCodes.Count > 0) { var newReasons = new List(); 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 alarmCodes) { if (alarmCodes == null || alarmCodes.Count == 0) return "No active alarms"; List reasons = new List(); 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 StatusMask = new Dictionary() { { "LaserEmissionSlowSignal", 0x00000001 }, { "LaserReady", 0x00000008 }, { "ChillerReady", 0x00000400 }, { "ErrorOccurred", 0x00002000 }, { "BeamSwitchReady", 0x00004000 }, { "ShutdownState", 0x10000000 }, { "StandbyState", 0x40000000 }, }; private readonly Dictionary AlarmMask = new Dictionary() { { "laserFiberInterlock", 0x00000001 }, { "laserOverheating", 0x00000002 }, { "HighBackReflection", 0x00000004 }, { "LaserModuleFailed", 0x00000020 }, { "ChillerError", 0x00800000 }, { "CriticalErrorLaser", 0x02000000 }, { "EStopButtonPressed", 0x20000000 }, }; private static readonly Dictionary AlarmReasonTable = new Dictionary() { { 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(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 alarmCodes) QueryStatusLinesAndAlarms() { var r = Exec(eFunctionCommand.QueryStatusLinesAndAlarms, null); ushort status = r.DataU16BE(0); List alarms = new List(); for (int i = 2; i < r.DataLength; i += 2) alarms.Add(r.DataU16BE(i)); return (status, alarms); } public Dictionary GetLaserTemperatures() { var r = Exec(eFunctionCommand.QueryLaserTemperatures, null); // 2B*4, 0.1°C return new Dictionary { ["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 GetLaserCurrents() { var r = Exec(eFunctionCommand.QueryLaserCurrents, null); // 2B*3, 0.1A return new Dictionary { ["Pump1"] = r.DataU16BE(0) / 10.0, ["Pump2"] = r.DataU16BE(2) / 10.0, ["SeedLaser"] = r.DataU16BE(4) / 10.0, }; } public Dictionary 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 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 ParsePairsAsDictionary(Recv r, double scale, string prefix) { var dict = new Dictionary(); 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; } /// /// 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) /// /// 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, } }