using System; using System.Linq; using System.Text; namespace StandardScene.Fass2Simulator { /// /// FASS 2.0 PCB/UDP 报文编解码,字段布局对齐 /// StandardScene.Magnetic.Protocol.Fass2Protocol.ParseState 与 backend PCB 仿真器。 /// public static class Fass2SimProtocol { 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[] BuildState(Fass2SimVehicleSnapshot snapshot) { var frame = new byte[StateFrameLength]; frame[0] = Begin; frame[1] = snapshot.LastCommand; WriteUInt16(frame, 2, snapshot.Car); WriteUInt16(frame, 4, snapshot.Length); WriteUInt16(frame, 6, snapshot.Width); var carType = Encoding.ASCII.GetBytes("Fass2Sim".PadRight(16, '\0')); Buffer.BlockCopy(carType, 0, frame, 12, Math.Min(16, carType.Length)); frame[28] = snapshot.BatteryCharge; frame[29] = snapshot.BatteryHealth; WriteUInt16(frame, 30, snapshot.BatteryCurrent); WriteUInt16(frame, 32, snapshot.BatteryVoltage); WriteUInt16(frame, 34, snapshot.HeadingAngle); frame[36] = snapshot.State; WriteUInt64(frame, 37, snapshot.Alarm); WriteUInt64(frame, 45, snapshot.Task); var nodeBytes = BuildNodeBytes(snapshot); Buffer.BlockCopy(nodeBytes, 0, frame, 53, 25); frame[98] = Xor(frame, 1, 97); frame[99] = End; return frame; } public static byte[] BuildNodeBytes(Fass2SimVehicleSnapshot snapshot) { var bytes = new byte[25]; WriteUInt16(bytes, 0, snapshot.Node); WriteUInt16(bytes, 2, snapshot.Distance); bytes[4] = snapshot.StartStop; bytes[5] = snapshot.Direction; bytes[6] = snapshot.Orientation; bytes[7] = snapshot.Byroad; WriteUInt16(bytes, 8, snapshot.Speed); bytes[10] = snapshot.Obstacle; bytes[11] = snapshot.Audio; bytes[12] = snapshot.Light; bytes[13] = snapshot.Charge; bytes[14] = snapshot.Rest; bytes[15] = snapshot.Lift; bytes[16] = snapshot.Clamp; bytes[17] = snapshot.Tray; bytes[18] = snapshot.Roll; bytes[19] = snapshot.Shutdown; return bytes; } public static bool TryGetCommand(byte[] packet, out byte command, out int expectedLength) { command = 0; expectedLength = 0; if (packet == null || packet.Length < 2 || packet[0] != Begin) { return false; } command = packet[1]; expectedLength = ResolvePacketLength(command); return expectedLength > 0; } public static int ResolvePacketLength(byte command) { switch (command) { case CmdQuery: case CmdStart: case CmdStop: case CmdEmergencyStop: case CmdReset: case CmdRest: case CmdShutdown: case CmdStateResponse: return ControlFrameLength; case CmdAction: return ActionFrameLength; case CmdNodes: return NodesFrameLength; default: return 0; } } public static string CommandName(byte command) { switch (command) { case CmdQuery: return "Query(0x00)"; case CmdStart: return "Start(0x01)"; case CmdStop: return "Stop(0x02)"; case CmdEmergencyStop: return "EmergencyStop(0x03)"; case CmdReset: return "Reset(0x04)"; case CmdRest: return "Rest(0x05)"; case CmdShutdown: return "Shutdown(0x06)"; case CmdAction: return "Action(0xA1)"; case CmdNodes: return "Nodes(0xB1)"; case CmdStateResponse: return "StateResponse(0x10)"; default: return $"Unknown(0x{command: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})"; } } /// 协议速度(0.1 m/min) → mm/s。 public static double ProtocolSpeedToMmPerSec(ushort protocolSpeed) { if (protocolSpeed == 0) { return 0; } return protocolSpeed * 0.1 * 1000.0 / 60.0; } public static string ToHex(byte[] bytes) { return bytes == null ? string.Empty : string.Join(" ", bytes.Select(b => b.ToString("X2"))); } 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 bool TryParseNodes(byte[] packet, out ulong taskId, out Fass2SimNodeMessage[] nodes) { taskId = 0; nodes = Array.Empty(); if (packet == null || packet.Length < NodesFrameLength || packet[1] != CmdNodes) { return false; } taskId = BitConverter.ToUInt64(packet, 4); var count = BitConverter.ToUInt16(packet, 12); if (count == 0 || count > 10) { return false; } var requiredLength = 14 + count * 25 + 2; if (packet.Length < requiredLength) { return false; } nodes = new Fass2SimNodeMessage[count]; for (var i = 0; i < count; i++) { nodes[i] = Fass2SimNodeMessage.FromBytes(packet, 14 + i * 25); } return true; } public static bool TryParseAction(byte[] packet, out ulong actionId, out Fass2SimNodeMessage node) { actionId = 0; node = null; if (packet == null || packet.Length < ActionFrameLength || packet[1] != CmdAction) { return false; } actionId = BitConverter.ToUInt64(packet, 4); node = Fass2SimNodeMessage.FromBytes(packet, 14); return true; } 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); } } }