using System; using System.Linq; namespace StandardScene.Magnetic.Protocol { /// /// FASS 2.0 通用控制接口(PLC)报文编解码,对齐 /// FASS.Extend.Car.Fairyland.Plc 与调度器 CarRequestService 逻辑。 /// 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; } /// /// PCB/UDP 模式:收到 AGV 主动上报的 100B 状态后,回 0x10(50B)时间戳应答。 /// 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(); } }