新增1.0和2.0两种协议车型车型
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
namespace StandardScene.Magnetic.Protocol
|
||||
{
|
||||
/// <summary>
|
||||
/// FASS 2.0 通讯模式:PLC/TCP 主动查询,或 PCB/UDP 被动接收状态上报。
|
||||
/// </summary>
|
||||
public enum Fass2CommMode
|
||||
{
|
||||
/// <summary>PLC:TCP 同步收发,调度主动发 0x00 查询。</summary>
|
||||
Tcp = 0,
|
||||
|
||||
/// <summary>PCB:UDP 监听 AGV 上报,回 0x10,命令单向下发。</summary>
|
||||
Udp = 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace StandardScene.Magnetic.Protocol
|
||||
{
|
||||
/// <summary>
|
||||
/// FASS 2.0 通用控制接口(PLC)报文编解码,对齐
|
||||
/// <c>FASS.Extend.Car.Fairyland.Plc</c> 与调度器 <c>CarRequestService</c> 逻辑。
|
||||
/// </summary>
|
||||
internal static class Fass2Protocol
|
||||
{
|
||||
public const byte Begin = 0xBB;
|
||||
public const byte End = 0xEE;
|
||||
|
||||
public const byte CmdQuery = 0x00;
|
||||
public const byte CmdStart = 0x01;
|
||||
public const byte CmdStop = 0x02;
|
||||
public const byte CmdEmergencyStop = 0x03;
|
||||
public const byte CmdReset = 0x04;
|
||||
public const byte CmdRest = 0x05;
|
||||
public const byte CmdShutdown = 0x06;
|
||||
public const byte CmdAction = 0xA1;
|
||||
public const byte CmdNodes = 0xB1;
|
||||
public const byte CmdStateResponse = 0x10;
|
||||
|
||||
public const int ControlFrameLength = 50;
|
||||
public const int StateFrameLength = 100;
|
||||
public const int ActionFrameLength = 100;
|
||||
public const int NodesFrameLength = 300;
|
||||
|
||||
public static byte Xor(byte[] data, int start, int length)
|
||||
{
|
||||
byte xor = 0;
|
||||
for (var i = 0; i < length; i++)
|
||||
{
|
||||
xor ^= data[start + i];
|
||||
}
|
||||
return xor;
|
||||
}
|
||||
|
||||
public static byte[] BuildControl(byte command, ushort car, ushort param)
|
||||
{
|
||||
var frame = new byte[ControlFrameLength];
|
||||
frame[0] = Begin;
|
||||
frame[1] = command;
|
||||
WriteUInt16(frame, 2, car);
|
||||
WriteUInt16(frame, 4, param);
|
||||
frame[48] = Xor(frame, 1, 47);
|
||||
frame[49] = End;
|
||||
return frame;
|
||||
}
|
||||
|
||||
public static byte[] BuildNodes(ushort car, ulong taskId, Fass2NodeMessage[] nodes)
|
||||
{
|
||||
if (nodes == null || nodes.Length == 0)
|
||||
{
|
||||
throw new ArgumentException("nodes required");
|
||||
}
|
||||
if (nodes.Length > 10)
|
||||
{
|
||||
throw new ArgumentException("FASS2 nodes count must be <= 10");
|
||||
}
|
||||
|
||||
var frame = new byte[NodesFrameLength];
|
||||
frame[0] = Begin;
|
||||
frame[1] = CmdNodes;
|
||||
WriteUInt16(frame, 2, car);
|
||||
WriteUInt64(frame, 4, taskId);
|
||||
WriteUInt16(frame, 12, (ushort)nodes.Length);
|
||||
for (var i = 0; i < nodes.Length; i++)
|
||||
{
|
||||
Buffer.BlockCopy(nodes[i].ToBytes(), 0, frame, 14 + i * 25, 25);
|
||||
}
|
||||
frame[298] = Xor(frame, 1, 297);
|
||||
frame[299] = End;
|
||||
return frame;
|
||||
}
|
||||
|
||||
public static byte[] BuildAction(ushort car, ulong actionId, Fass2NodeMessage node)
|
||||
{
|
||||
if (node == null)
|
||||
{
|
||||
throw new ArgumentException("node required");
|
||||
}
|
||||
|
||||
var frame = new byte[ActionFrameLength];
|
||||
frame[0] = Begin;
|
||||
frame[1] = CmdAction;
|
||||
WriteUInt16(frame, 2, car);
|
||||
WriteUInt64(frame, 4, actionId);
|
||||
Buffer.BlockCopy(node.ToBytes(), 0, frame, 14, 25);
|
||||
frame[98] = Xor(frame, 1, 97);
|
||||
frame[99] = End;
|
||||
return frame;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PCB/UDP 模式:收到 AGV 主动上报的 100B 状态后,回 0x10(50B)时间戳应答。
|
||||
/// </summary>
|
||||
public static byte[] BuildStateResponse(ushort car, ulong timestampMs)
|
||||
{
|
||||
var frame = new byte[ControlFrameLength];
|
||||
frame[0] = Begin;
|
||||
frame[1] = CmdStateResponse;
|
||||
WriteUInt16(frame, 2, car);
|
||||
WriteUInt64(frame, 4, timestampMs);
|
||||
|
||||
var epoch = new DateTime(1970, 1, 1, 8, 0, 0, DateTimeKind.Unspecified);
|
||||
var date = epoch.AddMilliseconds(timestampMs > long.MaxValue ? long.MaxValue : (long)timestampMs);
|
||||
frame[12] = (byte)(date.Year - 1970);
|
||||
frame[13] = (byte)date.Month;
|
||||
frame[14] = (byte)date.Day;
|
||||
frame[15] = (byte)date.Hour;
|
||||
frame[16] = (byte)date.Minute;
|
||||
frame[17] = (byte)date.Second;
|
||||
WriteUInt16(frame, 18, (ushort)date.Millisecond);
|
||||
|
||||
frame[48] = Xor(frame, 1, 47);
|
||||
frame[49] = End;
|
||||
return frame;
|
||||
}
|
||||
|
||||
public static ulong BeijingUnixTimeMs()
|
||||
{
|
||||
var epoch = new DateTime(1970, 1, 1, 8, 0, 0, DateTimeKind.Unspecified);
|
||||
return (ulong)(DateTime.Now - epoch).TotalMilliseconds;
|
||||
}
|
||||
|
||||
public static bool TryExtractStateFrame(byte[] buffer, int length, out byte[] frame)
|
||||
{
|
||||
frame = null;
|
||||
if (buffer == null || length < StateFrameLength)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i <= length - StateFrameLength; i++)
|
||||
{
|
||||
if (buffer[i] != Begin || buffer[i + StateFrameLength - 1] != End)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
frame = new byte[StateFrameLength];
|
||||
Buffer.BlockCopy(buffer, i, frame, 0, StateFrameLength);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (length == StateFrameLength && buffer[0] == Begin && buffer[StateFrameLength - 1] == End)
|
||||
{
|
||||
frame = new byte[StateFrameLength];
|
||||
Buffer.BlockCopy(buffer, 0, frame, 0, StateFrameLength);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static Fass2StateReport ParseState(byte[] bytes)
|
||||
{
|
||||
if (bytes == null || bytes.Length < StateFrameLength)
|
||||
{
|
||||
throw new ArgumentException("FASS2 state frame must be 100 bytes");
|
||||
}
|
||||
if (bytes[0] != Begin || bytes[99] != End)
|
||||
{
|
||||
throw new ArgumentException($"invalid FASS2 state frame: begin=0x{bytes[0]:X2}, end=0x{bytes[99]:X2}");
|
||||
}
|
||||
|
||||
var nodeBytes = new byte[25];
|
||||
Buffer.BlockCopy(bytes, 53, nodeBytes, 0, 25);
|
||||
return new Fass2StateReport
|
||||
{
|
||||
Command = bytes[1],
|
||||
Car = ReadUInt16(bytes, 2),
|
||||
Length = ReadUInt16(bytes, 4),
|
||||
Width = ReadUInt16(bytes, 6),
|
||||
BatteryCharge = bytes[28],
|
||||
BatteryHealth = bytes[29],
|
||||
BatteryCurrent = ReadUInt16(bytes, 30),
|
||||
BatteryVoltage = ReadUInt16(bytes, 32),
|
||||
HeadingAngle = ReadUInt16(bytes, 34),
|
||||
State = bytes[36],
|
||||
Alarm = ReadUInt64(bytes, 37),
|
||||
Task = ReadUInt64(bytes, 45),
|
||||
Node = Fass2NodeMessage.FromBytes(nodeBytes)
|
||||
};
|
||||
}
|
||||
|
||||
public static string ToHex(byte[] bytes)
|
||||
{
|
||||
return bytes == null ? string.Empty : string.Join(" ", bytes.Select(b => b.ToString("X2")));
|
||||
}
|
||||
|
||||
public static string StateText(byte state)
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case 0: return "未准备";
|
||||
case 1: return "运行中";
|
||||
case 2: return "停止中";
|
||||
case 3: return "急停中";
|
||||
case 4: return "故障中";
|
||||
case 5: return "任务中";
|
||||
case 6: return "休眠中";
|
||||
case 7: return "关机中";
|
||||
case 8: return "充电中";
|
||||
default: return $"未知({state})";
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteUInt16(byte[] buffer, int offset, ushort value)
|
||||
{
|
||||
var bytes = BitConverter.GetBytes(value);
|
||||
buffer[offset] = bytes[0];
|
||||
buffer[offset + 1] = bytes[1];
|
||||
}
|
||||
|
||||
private static void WriteUInt64(byte[] buffer, int offset, ulong value)
|
||||
{
|
||||
var bytes = BitConverter.GetBytes(value);
|
||||
Buffer.BlockCopy(bytes, 0, buffer, offset, 8);
|
||||
}
|
||||
|
||||
private static ushort ReadUInt16(byte[] bytes, int offset)
|
||||
{
|
||||
return BitConverter.ToUInt16(bytes, offset);
|
||||
}
|
||||
|
||||
private static ulong ReadUInt64(byte[] bytes, int offset)
|
||||
{
|
||||
return BitConverter.ToUInt64(bytes, offset);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class Fass2NodeMessage
|
||||
{
|
||||
public ushort Node { get; set; }
|
||||
public ushort Distance { get; set; }
|
||||
public byte StartStop { get; set; }
|
||||
public byte Direction { get; set; }
|
||||
public byte Orientation { get; set; }
|
||||
public byte Byroad { get; set; }
|
||||
public ushort Speed { get; set; }
|
||||
public byte Obstacle { get; set; }
|
||||
public byte Audio { get; set; }
|
||||
public byte Light { get; set; }
|
||||
public byte Charge { get; set; }
|
||||
public byte Rest { get; set; }
|
||||
public byte Lift { get; set; }
|
||||
public byte Clamp { get; set; }
|
||||
public byte Tray { get; set; }
|
||||
public byte Roll { get; set; }
|
||||
public byte Shutdown { get; set; }
|
||||
|
||||
public byte[] ToBytes()
|
||||
{
|
||||
var bytes = new byte[25];
|
||||
WriteUInt16(bytes, 0, Node);
|
||||
WriteUInt16(bytes, 2, Distance);
|
||||
bytes[4] = StartStop;
|
||||
bytes[5] = Direction;
|
||||
bytes[6] = Orientation;
|
||||
bytes[7] = Byroad;
|
||||
WriteUInt16(bytes, 8, Speed);
|
||||
bytes[10] = Obstacle;
|
||||
bytes[11] = Audio;
|
||||
bytes[12] = Light;
|
||||
bytes[13] = Charge;
|
||||
bytes[14] = Rest;
|
||||
bytes[15] = Lift;
|
||||
bytes[16] = Clamp;
|
||||
bytes[17] = Tray;
|
||||
bytes[18] = Roll;
|
||||
bytes[19] = Shutdown;
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public static Fass2NodeMessage FromBytes(byte[] bytes)
|
||||
{
|
||||
if (bytes == null || bytes.Length < 25)
|
||||
{
|
||||
throw new ArgumentException("node frame must be 25 bytes");
|
||||
}
|
||||
return new Fass2NodeMessage
|
||||
{
|
||||
Node = ReadUInt16(bytes, 0),
|
||||
Distance = ReadUInt16(bytes, 2),
|
||||
StartStop = bytes[4],
|
||||
Direction = bytes[5],
|
||||
Orientation = bytes[6],
|
||||
Byroad = bytes[7],
|
||||
Speed = ReadUInt16(bytes, 8),
|
||||
Obstacle = bytes[10],
|
||||
Audio = bytes[11],
|
||||
Light = bytes[12],
|
||||
Charge = bytes[13],
|
||||
Rest = bytes[14],
|
||||
Lift = bytes[15],
|
||||
Clamp = bytes[16],
|
||||
Tray = bytes[17],
|
||||
Roll = bytes[18],
|
||||
Shutdown = bytes[19]
|
||||
};
|
||||
}
|
||||
|
||||
private static void WriteUInt16(byte[] buffer, int offset, ushort value)
|
||||
{
|
||||
var bytes = BitConverter.GetBytes(value);
|
||||
buffer[offset] = bytes[0];
|
||||
buffer[offset + 1] = bytes[1];
|
||||
}
|
||||
|
||||
private static ushort ReadUInt16(byte[] bytes, int offset)
|
||||
{
|
||||
return BitConverter.ToUInt16(bytes, offset);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class Fass2StateReport
|
||||
{
|
||||
public byte Command { get; set; }
|
||||
public ushort Car { get; set; }
|
||||
public ushort Length { get; set; }
|
||||
public ushort Width { get; set; }
|
||||
public byte BatteryCharge { get; set; }
|
||||
public byte BatteryHealth { get; set; }
|
||||
public ushort BatteryCurrent { get; set; }
|
||||
public ushort BatteryVoltage { get; set; }
|
||||
public ushort HeadingAngle { get; set; }
|
||||
public byte State { get; set; }
|
||||
public ulong Alarm { get; set; }
|
||||
public ulong Task { get; set; }
|
||||
public Fass2NodeMessage Node { get; set; } = new Fass2NodeMessage();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using StandardScene.Magnetic.Protocol;
|
||||
|
||||
namespace StandardScene.Magnetic.Protocol
|
||||
{
|
||||
/// <summary>
|
||||
/// 100B 状态帧编码,供模拟器或测试工具复用,字段布局与 <see cref="Fass2Protocol.ParseState"/> 一致。
|
||||
/// </summary>
|
||||
public static class Fass2StateCodec
|
||||
{
|
||||
public static byte[] BuildState(Fass2StateReport report, string carType = "MagFass2")
|
||||
{
|
||||
if (report == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(report));
|
||||
}
|
||||
|
||||
var frame = new byte[Fass2Protocol.StateFrameLength];
|
||||
frame[0] = Fass2Protocol.Begin;
|
||||
frame[1] = report.Command;
|
||||
WriteUInt16(frame, 2, report.Car);
|
||||
WriteUInt16(frame, 4, report.Length);
|
||||
WriteUInt16(frame, 6, report.Width);
|
||||
|
||||
var typeBytes = Encoding.ASCII.GetBytes((carType ?? string.Empty).PadRight(16, '\0'));
|
||||
Buffer.BlockCopy(typeBytes, 0, frame, 12, Math.Min(16, typeBytes.Length));
|
||||
|
||||
frame[28] = report.BatteryCharge;
|
||||
frame[29] = report.BatteryHealth;
|
||||
WriteUInt16(frame, 30, report.BatteryCurrent);
|
||||
WriteUInt16(frame, 32, report.BatteryVoltage);
|
||||
WriteUInt16(frame, 34, report.HeadingAngle);
|
||||
frame[36] = report.State;
|
||||
WriteUInt64(frame, 37, report.Alarm);
|
||||
WriteUInt64(frame, 45, report.Task);
|
||||
|
||||
var node = report.Node ?? new Fass2NodeMessage();
|
||||
Buffer.BlockCopy(node.ToBytes(), 0, frame, 53, 25);
|
||||
|
||||
frame[98] = Fass2Protocol.Xor(frame, 1, 97);
|
||||
frame[99] = Fass2Protocol.End;
|
||||
return frame;
|
||||
}
|
||||
|
||||
private static void WriteUInt16(byte[] buffer, int offset, ushort value)
|
||||
{
|
||||
var bytes = BitConverter.GetBytes(value);
|
||||
buffer[offset] = bytes[0];
|
||||
buffer[offset + 1] = bytes[1];
|
||||
}
|
||||
|
||||
private static void WriteUInt64(byte[] buffer, int offset, ulong value)
|
||||
{
|
||||
var bytes = BitConverter.GetBytes(value);
|
||||
Buffer.BlockCopy(bytes, 0, buffer, offset, 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
using SimpleCore.Library;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
|
||||
namespace StandardScene.Magnetic.Protocol
|
||||
{
|
||||
/// <summary>
|
||||
/// FASS 2.0 PCB/UDP 共享监听枢纽。
|
||||
/// AGV 主动上报 100B 状态 → 解析并分发给对应车辆 → 回 0x10 时间戳应答到车辆 address:Port。
|
||||
/// 下发 0xB1/0xA1/0x01 等命令亦通过本枢纽单播到车辆 address:Port。
|
||||
/// </summary>
|
||||
internal static class Fass2UdpHub
|
||||
{
|
||||
private static readonly object SyncRoot = new object();
|
||||
private static readonly Dictionary<ushort, IFass2UdpCar> CarsByCode = new Dictionary<ushort, IFass2UdpCar>();
|
||||
|
||||
private static UdpClient _listener;
|
||||
private static Thread _receiveThread;
|
||||
private static volatile bool _running;
|
||||
private static int _listenPort = 20103;
|
||||
private static readonly Dictionary<ushort, long> _lastUnknownCarLogUtcTicks = new Dictionary<ushort, long>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace StandardScene.Magnetic.Protocol
|
||||
{
|
||||
/// <summary>
|
||||
/// 由 <see cref="Fass2UdpHub"/> 回调的 UDP/PCB 车型处理接口。
|
||||
/// </summary>
|
||||
internal interface IFass2UdpCar
|
||||
{
|
||||
ushort VehicleCode { get; }
|
||||
|
||||
string RemoteAddress { get; }
|
||||
|
||||
int RemotePort { get; }
|
||||
|
||||
void OnUdpStateReceived(Fass2StateReport report);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user