1025 lines
41 KiB
C#
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,
|
|
}
|
|
}
|