using Microsoft.Extensions.Options; using Peak.Can.Basic.BackwardCompatibility; using SickBlazorApp.Models; using SickBlazorApp.Options; using System; using System.Collections.Generic; using System.IO; using System.Reflection; using System.Threading; using System.Threading.Tasks; namespace SickBlazorApp.Services.Windows; public sealed class PcanCanService : ICanBusService { private readonly CanBusOptions _options; private readonly EdsParser _edsParser; // Parser EDS mới private CancellationTokenSource? _cts; private bool _initialized; public event EventHandler? PositionReceived; private DateTime _lastHeartbeat = DateTime.MinValue; private readonly TimeSpan _heartbeatTimeout = TimeSpan.FromSeconds(2); public event EventHandler? NodeStateChanged; private DateTime _lastSeen = DateTime.MinValue; private CanNodeState _currentState = CanNodeState.Unknown; public event EventHandler? FrameReceived; private int _currentBitrate; public int CurrentBitrate { get; private set; } private bool _waitingStopAck = false; private bool _autoResumeEnabled = true; private DateTime _lastBootup = DateTime.MinValue; private byte _currentNodeId; public byte CurrentNodeId => _currentNodeId; public byte GetCurrentNodeId() => _currentNodeId; public event EventHandler? NodeIdChanged; private Dictionary _baudMap = new(); // Map baudrate động từ EDS public PcanCanService(IOptions options) { _options = options.Value; _edsParser = new EdsParser("SickBlazorApp.eds.AHM36_I_CO.eds"); // hard-code tên resource _options.PositionBaseCobId = _edsParser.GetTpdo1BaseCobId(); // vẫn động _options.PositionScale = _edsParser.GetPositionScale(); // động từ 230 mm/rev if (!_edsParser.IsPositionMappedToTpdo1()) throw new InvalidOperationException("Position mapping sai"); _baudMap = _edsParser.GetBaudrateMap(); CurrentBitrate = _currentBitrate; } // ================= INIT ================= public Task InitAsync() { ushort channel = ParseChannel(_options.Channel); if (_initialized) return Task.CompletedTask; var baud = MapBaudrate(_currentBitrate); var status = PCANBasic.Initialize(channel, baud); if (status != TPCANStatus.PCAN_ERROR_OK) throw new InvalidOperationException($"PCAN init failed: {status}"); _initialized = true; CurrentBitrate = _currentBitrate; return Task.CompletedTask; } // ================= START READ ================= public void Start() { if (_cts != null) return; _cts = new CancellationTokenSource(); ushort channel = ParseChannel(_options.Channel); uint pdoCobId = GetPositionCobId(); uint heartbeatCobId = 0x700u + _currentNodeId; double scale = _options.PositionScale; Task.Run(() => { while (!_cts!.IsCancellationRequested) { TPCANMsg msg; TPCANTimestamp ts; var result = PCANBasic.Read(channel, out msg, out ts); // ================= BOOT-UP DETECT ================= if (msg.ID >= 0x701 && msg.ID <= 0x77F && msg.LEN >= 1 && msg.DATA[0] == 0x00) { byte detectedNodeId = (byte)(msg.ID - 0x700); if (_currentNodeId != detectedNodeId) { _currentNodeId = detectedNodeId; NodeIdChanged?.Invoke(this, detectedNodeId); } } if (result != TPCANStatus.PCAN_ERROR_OK) { CheckHeartbeatTimeout(); Thread.Sleep(5); continue; } string dataHex = string.Join(" ", msg.DATA.Take(msg.LEN).Select(b => b.ToString("X2"))); FrameReceived?.Invoke(this, new CanFrame { CobId = msg.ID, Length = msg.LEN, DataHex = dataHex, Timestamp = DateTime.Now }); if (!msg.MSGTYPE.HasFlag(TPCANMessageType.PCAN_MESSAGE_STANDARD)) continue; // ================= HEARTBEAT ================= if (msg.ID >= 0x701 && msg.ID <= 0x77F && msg.LEN >= 1) { _lastHeartbeat = DateTime.Now; _lastSeen = DateTime.Now; byte nodeIdFromHb = (byte)(msg.ID - 0x700); byte hbValue = msg.DATA[0]; if (_currentNodeId != nodeIdFromHb) { _currentNodeId = nodeIdFromHb; NodeIdChanged?.Invoke(this, nodeIdFromHb); } var hbState = hbValue switch { 0x00 => CanNodeState.Bootup, 0x04 => CanNodeState.Stopped, 0x05 => CanNodeState.Operational, 0x7F => CanNodeState.PreOperational, _ => CanNodeState.Unknown }; UpdateState(hbState); if (_autoResumeEnabled && !_waitingStopAck && hbState == CanNodeState.Bootup) { if (DateTime.Now - _lastBootup > TimeSpan.FromSeconds(1)) { _lastBootup = DateTime.Now; Task.Run(async () => { await Task.Delay(500); SendNmtStart(_currentNodeId); }); } } if (_waitingStopAck && hbState == CanNodeState.Stopped) { _waitingStopAck = false; } continue; } // ================= POSITION PDO ================= if (msg.ID < 0x180 || msg.ID > 0x1FF || msg.LEN < 6) continue; _lastSeen = DateTime.Now; uint raw = (uint)( msg.DATA[0] | (msg.DATA[1] << 8) | (msg.DATA[2] << 16) | (msg.DATA[3] << 24) ); double positionMm = raw * scale; positionMm = Math.Min(positionMm, 3000.0); positionMm = Math.Max(positionMm, 0.0); PositionReceived?.Invoke(this, new PositionPdo { CobId = msg.ID, Length = msg.LEN, DataHex = dataHex, RawValue = raw, PositionMm = positionMm, Timestamp = DateTime.Now }); FrameReceived?.Invoke(this, new CanFrame { CobId = msg.ID, Length = msg.LEN, DataHex = dataHex, Timestamp = DateTime.Now, PositionMm = positionMm, Description = "TPDO1 Position" }); } }); } // ================= STOP ================= public void Stop() { if (_cts == null) return; _waitingStopAck = true; SendNmtStop(_currentNodeId); _cts.Cancel(); _cts = null; UpdateState(CanNodeState.Stopped); } public void Dispose() => Stop(); // ================= NMT ================= public void SendNmtStart(byte nodeId) { ushort channel = ParseChannel(_options.Channel); var msg = new TPCANMsg { ID = 0x000, LEN = 2, MSGTYPE = TPCANMessageType.PCAN_MESSAGE_STANDARD, DATA = new byte[8] }; msg.DATA[0] = 0x01; // NMT Start msg.DATA[1] = nodeId; for (int i = 0; i < 3; i++) { PCANBasic.Write(channel, ref msg); Thread.Sleep(50); } } public void SendNmtStop(byte nodeId) { ushort channel = ParseChannel(_options.Channel); var msg = new TPCANMsg { ID = 0x000, LEN = 2, MSGTYPE = TPCANMessageType.PCAN_MESSAGE_STANDARD, DATA = new byte[8] }; msg.DATA[0] = 0x02; // NMT Stop msg.DATA[1] = nodeId; PCANBasic.Write(channel, ref msg); } public void SendNmtReset(byte nodeId) { ushort channel = ParseChannel(_options.Channel); var msg = new TPCANMsg { ID = 0x000, LEN = 2, MSGTYPE = TPCANMessageType.PCAN_MESSAGE_STANDARD, DATA = new byte[8] }; msg.DATA[0] = 0x81; // Reset node msg.DATA[1] = nodeId; PCANBasic.Write(channel, ref msg); } // ================= HELPERS ================= private static ushort ParseChannel(string ch) => ch switch { "PCAN_USBBUS1" => 0x51, "PCAN_USBBUS2" => 0x52, _ => throw new ArgumentOutOfRangeException(nameof(ch)) }; private static TPCANBaudrate MapBaudrate(int b) => b switch { 1000000 => TPCANBaudrate.PCAN_BAUD_1M, 800000 => TPCANBaudrate.PCAN_BAUD_800K, 500000 => TPCANBaudrate.PCAN_BAUD_500K, 250000 => TPCANBaudrate.PCAN_BAUD_250K, 125000 => TPCANBaudrate.PCAN_BAUD_125K, 100000 => TPCANBaudrate.PCAN_BAUD_100K, 50000 => TPCANBaudrate.PCAN_BAUD_50K, 20000 => TPCANBaudrate.PCAN_BAUD_20K, _ => throw new ArgumentOutOfRangeException(nameof(b)) }; private uint GetPositionCobId() { return _options.PositionBaseCobId + _currentNodeId; } private void CheckHeartbeatTimeout() { if (_currentState == CanNodeState.PreOperational || _currentState == CanNodeState.Stopped) return; if (_lastHeartbeat == DateTime.MinValue) return; if (DateTime.Now - _lastHeartbeat > _heartbeatTimeout) { UpdateState(CanNodeState.Timeout); } } private void UpdateState(CanNodeState newState) { if (_currentState == newState) return; _currentState = newState; NodeStateChanged?.Invoke(this, newState); } public void ChangeBitrate(int bitrate) { if (_currentBitrate == bitrate) return; ushort channel = ParseChannel(_options.Channel); Stop(); if (_initialized) { PCANBasic.Uninitialize(channel); _initialized = false; } _currentBitrate = bitrate; } private void SendRaw(uint cobId, byte[] data) { ushort channel = ParseChannel(_options.Channel); var msg = new TPCANMsg { ID = cobId, LEN = (byte)data.Length, MSGTYPE = TPCANMessageType.PCAN_MESSAGE_STANDARD, DATA = new byte[8] }; Array.Copy(data, msg.DATA, data.Length); PCANBasic.Write(channel, ref msg); } private uint GetAccessCode() => _edsParser.GetAccessCode(); // 0x98127634 private byte GetBitrateIndex(int bitrate) { foreach (var kv in _baudMap) { if (kv.Value == bitrate) return kv.Key; } throw new ArgumentOutOfRangeException(nameof(bitrate), $"Bitrate {bitrate} not supported in EDS"); } public async Task ApplyBitrateAsync(byte nodeId, int bitrate) { // Pre-Operational SendRaw(0x000, new byte[] { 0x80, nodeId }); await Task.Delay(100); // Unlock access code (đọc động từ EDS/manual) uint accessCode = GetAccessCode(); SendRaw( (uint)(0x600 + nodeId), new byte[] { 0x23, 0x09, 0x20, 0x01, (byte)(accessCode & 0xFF), (byte)((accessCode >> 8) & 0xFF), (byte)((accessCode >> 16) & 0xFF), (byte)((accessCode >> 24) & 0xFF) } ); await Task.Delay(100); // Set bitrate (index từ EDS map) byte bitrateValue = GetBitrateIndex(bitrate); SendRaw( (uint)(0x600 + nodeId), new byte[] { 0x2F, 0x09, 0x20, 0x03, bitrateValue, 0x00, 0x00, 0x00 } ); await Task.Delay(100); // Save EEPROM SendRaw( (uint)(0x600 + nodeId), new byte[] { 0x23, 0x10, 0x10, 0x01, 0x73, 0x61, 0x76, 0x65 } ); await Task.Delay(100); // Reset node SendRaw(0x000u, new byte[] { 0x81, nodeId }); // Update runtime await Task.Delay(1200); ChangeBitrate(bitrate); CurrentBitrate = bitrate; } public async Task ApplyNodeIdAsync(byte oldNodeId, byte newNodeId) { if (newNodeId < 1 || newNodeId > 127) throw new ArgumentOutOfRangeException(nameof(newNodeId)); // Pre-Operational SendRaw(0x000, new byte[] { 0x80, oldNodeId }); await Task.Delay(100); // Unlock access code (động từ EDS) uint accessCode = GetAccessCode(); SendRaw( (uint)(0x600 + oldNodeId), new byte[] { 0x23, 0x09, 0x20, 0x01, (byte)(accessCode & 0xFF), (byte)((accessCode >> 8) & 0xFF), (byte)((accessCode >> 16) & 0xFF), (byte)((accessCode >> 24) & 0xFF) } ); await Task.Delay(100); // Set Node ID SendRaw( (uint)(0x600 + oldNodeId), new byte[] { 0x2F, 0x09, 0x20, 0x02, newNodeId, 0x00, 0x00, 0x00 } ); await Task.Delay(100); // Save EEPROM SendRaw( (uint)(0x600 + oldNodeId), new byte[] { 0x23, 0x10, 0x10, 0x01, 0x73, 0x61, 0x76, 0x65 } ); await Task.Delay(100); // Reset node SendRaw(0x000, new byte[] { 0x81, oldNodeId }); // Thông báo UI _currentNodeId = newNodeId; NodeIdChanged?.Invoke(this, newNodeId); } }