using SimpleCore.Library; using System; using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Threading; namespace StandardScene.Magnetic.Protocol { /// /// FASS 2.0 PCB/UDP 共享监听枢纽。 /// AGV 主动上报 100B 状态 → 解析并分发给对应车辆 → 回 0x10 时间戳应答到车辆 address:Port。 /// 下发 0xB1/0xA1/0x01 等命令亦通过本枢纽单播到车辆 address:Port。 /// internal static class Fass2UdpHub { private static readonly object SyncRoot = new object(); private static readonly Dictionary CarsByCode = new Dictionary(); private static UdpClient _listener; private static Thread _receiveThread; private static volatile bool _running; private static int _listenPort = 20103; private static readonly Dictionary _lastUnknownCarLogUtcTicks = new Dictionary(); public static void Register(IFass2UdpCar car) { if (car == null) { return; } lock (SyncRoot) { CarsByCode[car.VehicleCode] = car; } } public static void Unregister(IFass2UdpCar car) { if (car == null) { return; } Unregister(car.VehicleCode, car); } public static void Unregister(ushort vehicleCode, IFass2UdpCar car) { if (car == null) { return; } lock (SyncRoot) { if (CarsByCode.TryGetValue(vehicleCode, out var existing) && ReferenceEquals(existing, car)) { CarsByCode.Remove(vehicleCode); } } } public static void EnsureStarted(int listenPort) { if (listenPort > 0) { _listenPort = listenPort; } lock (SyncRoot) { if (_running) { return; } _listener = new UdpClient(_listenPort); _running = true; _receiveThread = new Thread(ReceiveLoop) { IsBackground = true, Name = $"Fass2UdpHub:{_listenPort}" }; _receiveThread.Start(); Diagnosis.Post($"Fass2UdpHub 已启动,监听 UDP {_listenPort}", "Fass2UdpHub", true); } } public static void Stop() { lock (SyncRoot) { _running = false; try { _listener?.Close(); } catch { } _listener = null; } try { _receiveThread?.Join(1000); } catch { } _receiveThread = null; } public static void SendToCar(IFass2UdpCar car, byte[] payload) { if (car == null || payload == null || payload.Length == 0) { return; } if (string.IsNullOrWhiteSpace(car.RemoteAddress) || car.RemotePort <= 0) { throw new InvalidOperationException( $"Fass2UdpHub send failed: car VehicleCode={car.VehicleCode} remote endpoint not configured"); } using var sender = new UdpClient(); var remote = new IPEndPoint(IPAddress.Parse(car.RemoteAddress), car.RemotePort); sender.Send(payload, payload.Length, remote); } private static void ReceiveLoop() { while (_running) { try { var remote = new IPEndPoint(IPAddress.Any, 0); var packet = _listener.Receive(ref remote); ProcessPacket(packet, remote); } catch (SocketException) when (!_running) { break; } catch (ObjectDisposedException) when (!_running) { break; } catch (Exception ex) { Diagnosis.Post($"Fass2UdpHub receive error: {ex.Message}", "Fass2UdpHub", true); } } } private static void ProcessPacket(byte[] packet, IPEndPoint remote) { if (!Fass2Protocol.TryExtractStateFrame(packet, packet.Length, out var frame)) { return; } Fass2StateReport report; try { report = Fass2Protocol.ParseState(frame); } catch (Exception ex) { Diagnosis.Post($"Fass2UdpHub parse error from {remote}: {ex.Message}", "Fass2UdpHub", true); return; } IFass2UdpCar car; lock (SyncRoot) { if (!CarsByCode.TryGetValue(report.Car, out car)) { LogUnknownCar(report.Car); return; } } try { car.OnUdpStateReceived(report); } catch (Exception ex) { Diagnosis.Post($"Fass2UdpHub dispatch error car={report.Car}: {ex.Message}", "Fass2UdpHub", true); } try { var ack = Fass2Protocol.BuildStateResponse(report.Car, Fass2Protocol.BeijingUnixTimeMs()); SendToCar(car, ack); } catch (Exception ex) { Diagnosis.Post($"Fass2UdpHub 0x10 reply failed car={report.Car}: {ex.Message}", "Fass2UdpHub", true); } } private static void LogUnknownCar(ushort carCode) { var nowTicks = DateTime.UtcNow.Ticks; lock (SyncRoot) { if (_lastUnknownCarLogUtcTicks.TryGetValue(carCode, out var lastTicks) && nowTicks - lastTicks < TimeSpan.FromSeconds(5).Ticks) { return; } _lastUnknownCarLogUtcTicks[carCode] = nowTicks; } var registered = string.Join(",", CarsByCode.Keys); if (string.IsNullOrEmpty(registered)) { registered = "(无)"; } Diagnosis.Post( $"Fass2UdpHub 收到 Car={carCode} 的状态,但未注册该编号(已注册: {registered})。请检查 MagFass2Car.VehicleCode 与模拟器车号是否一致,并重新启动场景。", "Fass2UdpHub", true); } } }