2.0协议完善加简单交管配置

This commit is contained in:
ykkokluo
2026-08-14 09:49:23 +08:00
parent a28bc68e23
commit 1e780c2b65
51 changed files with 4013 additions and 393 deletions
@@ -0,0 +1,171 @@
using StandardScene.Magnetic.Protocol;
using StandardScene.Magnetic.Tasking;
using StandardScene.Magnetic.Tests.TestHelpers;
using System;
using Xunit;
namespace StandardScene.Magnetic.Tests.Protocol
{
public sealed class Fass2ProtocolGoldenTests
{
[Fact]
public void BuildControl_Start_Car1_Param0_MatchesGoldenLayout()
{
var frame = Fass2Protocol.BuildControl(Fass2Protocol.CmdStart, 1, 0);
Assert.Equal(Fass2Protocol.ControlFrameLength, frame.Length);
Assert.Equal(Fass2Protocol.Begin, frame[0]);
Assert.Equal(Fass2Protocol.CmdStart, frame[1]);
Assert.Equal(0x01, frame[2]);
Assert.Equal(0x00, frame[3]);
Assert.Equal(0x00, frame[4]);
Assert.Equal(0x00, frame[5]);
Assert.Equal(Fass2Protocol.Xor(frame, 1, 47), frame[48]);
Assert.Equal(Fass2Protocol.End, frame[49]);
const string goldenPrefix = "BB 01 01 00 00 00";
Assert.StartsWith(goldenPrefix, Fass2TestFrameBuilder.ToHex(frame));
}
[Fact]
public void BuildNodes_TwoNodes_LittleEndianAndXor()
{
var nodes = new[]
{
new Fass2NodeMessage
{
Node = 1,
Distance = 500,
StartStop = 1,
Speed = 120
},
new Fass2NodeMessage
{
Node = 2,
StartStop = 2,
Lift = 1
}
};
var frame = Fass2Protocol.BuildNodes(1, 0x0102030405060708UL, nodes);
Assert.Equal(Fass2Protocol.NodesFrameLength, frame.Length);
Assert.Equal(Fass2Protocol.CmdNodes, frame[1]);
Assert.Equal(0x08, frame[4]);
Assert.Equal(0x01, frame[11]);
Assert.Equal(0x02, frame[12]);
Assert.Equal(0x00, frame[13]);
Assert.Equal(Fass2Protocol.Xor(frame, 1, 297), frame[298]);
Assert.Equal(Fass2Protocol.End, frame[299]);
var parsedNode0 = Fass2NodeMessage.FromBytes(CopyRange(frame, 14, 25));
Assert.Equal((ushort)1, parsedNode0.Node);
Assert.Equal((ushort)500, parsedNode0.Distance);
Assert.Equal((byte)1, parsedNode0.StartStop);
Assert.Equal((ushort)120, parsedNode0.Speed);
var parsedNode1 = Fass2NodeMessage.FromBytes(CopyRange(frame, 39, 25));
Assert.Equal((ushort)2, parsedNode1.Node);
Assert.Equal((byte)2, parsedNode1.StartStop);
Assert.Equal((byte)1, parsedNode1.Lift);
}
[Fact]
public void BuildAction_RoundTripsNodeBlock()
{
var node = new Fass2NodeMessage
{
Node = 3,
StartStop = 22,
Lift = 2,
Speed = 120
};
var frame = Fass2Protocol.BuildAction(2, 999, node);
Assert.Equal(Fass2Protocol.ActionFrameLength, frame.Length);
Assert.Equal(Fass2Protocol.CmdAction, frame[1]);
Assert.Equal(0x02, frame[2]);
Assert.Equal(0x00, frame[3]);
Assert.Equal(Fass2Protocol.Xor(frame, 1, 97), frame[98]);
var parsed = Fass2NodeMessage.FromBytes(CopyRange(frame, 14, 25));
Assert.Equal(node.Node, parsed.Node);
Assert.Equal(node.StartStop, parsed.StartStop);
Assert.Equal(node.Lift, parsed.Lift);
Assert.Equal(node.Speed, parsed.Speed);
}
[Fact]
public void BuildStateResponse_FixedTimestamp_EncodesBeijingCalendar()
{
var frame = Fass2Protocol.BuildStateResponse(1, 1_704_067_200_000UL);
Assert.Equal(Fass2Protocol.CmdStateResponse, frame[1]);
Assert.Equal(0x01, frame[2]);
Assert.Equal(0x00, frame[3]);
Assert.Equal(Fass2Protocol.Xor(frame, 1, 47), frame[48]);
Assert.Equal(Fass2Protocol.End, frame[49]);
}
[Fact]
public void ParseState_RoundTripsMinimalFrame()
{
var frame = Fass2TestFrameBuilder.BuildStateFrame(5, 2, 7, 42);
Assert.True(Fass2Protocol.TryExtractStateFrame(frame, frame.Length, out var extracted));
Assert.Equal(frame, extracted);
var report = Fass2Protocol.ParseState(frame);
Assert.Equal((ushort)5, report.Car);
Assert.Equal((byte)2, report.State);
Assert.Equal((ulong)42, report.Task);
Assert.Equal((ushort)7, report.Node.Node);
Assert.Equal((ushort)500, report.Node.Distance);
}
[Fact]
public void NodeMessage_ToBytes_FromBytes_RoundTrip()
{
var source = new Fass2NodeMessage
{
Node = 11,
Distance = 1234,
StartStop = 2,
Direction = 3,
Orientation = 64,
Byroad = 2,
Speed = 120,
Lift = 1,
Roll = 2,
Shutdown = 1
};
var roundTrip = Fass2NodeMessage.FromBytes(source.ToBytes());
Assert.Equal(source.Node, roundTrip.Node);
Assert.Equal(source.Distance, roundTrip.Distance);
Assert.Equal(source.StartStop, roundTrip.StartStop);
Assert.Equal(source.Direction, roundTrip.Direction);
Assert.Equal(source.Orientation, roundTrip.Orientation);
Assert.Equal(source.Byroad, roundTrip.Byroad);
Assert.Equal(source.Speed, roundTrip.Speed);
Assert.Equal(source.Lift, roundTrip.Lift);
Assert.Equal(source.Roll, roundTrip.Roll);
Assert.Equal(source.Shutdown, roundTrip.Shutdown);
}
[Fact]
public void SpeedToProtocol_UsesMPerMinTimesTen()
{
Assert.Equal((ushort)120, Fass2TaskBuilder.SpeedToProtocol(12));
Assert.Equal((ushort)120, Fass2TaskBuilder.SpeedToProtocol(0.2));
}
private static byte[] CopyRange(byte[] source, int offset, int length)
{
var copy = new byte[length];
Buffer.BlockCopy(source, offset, copy, 0, length);
return copy;
}
}
}
@@ -0,0 +1,63 @@
using StandardScene.Fass2Simulator;
using StandardScene.Magnetic.Protocol;
using Xunit;
namespace StandardScene.Magnetic.Tests.Protocol
{
/// <summary>
/// 验证 Magnetic 编解码与模拟器 100B 状态帧字段布局一致。
/// </summary>
public sealed class Fass2SimMagneticProtocolCrossTests
{
[Fact]
public void StateFrame_NodeBlockOffset_MatchesMagneticParser()
{
var vehicle = new Fass2SimVehicle(new Fass2SimConfig
{
VehicleCode = 1,
InitialNode = 9,
InitialState = 2
});
vehicle.StartStop = 2;
vehicle.Distance = 777;
vehicle.Speed = 120;
vehicle.Lift = 1;
vehicle.Task = 1001;
var simFrame = Fass2SimProtocol.BuildState(vehicle.Snapshot());
var report = Fass2Protocol.ParseState(simFrame);
Assert.Equal((ushort)1, report.Car);
Assert.Equal((byte)2, report.State);
Assert.Equal((ulong)1001, report.Task);
Assert.Equal((ushort)9, report.Node.Node);
Assert.Equal((ushort)777, report.Node.Distance);
Assert.Equal((byte)2, report.Node.StartStop);
Assert.Equal((ushort)120, report.Node.Speed);
Assert.Equal((byte)1, report.Node.Lift);
}
[Fact]
public void NodeBytes_Are25BytesAligned()
{
var node = new Fass2NodeMessage
{
Node = 4,
Distance = 100,
StartStop = 1,
Byroad = 2,
Speed = 60
};
var bytes = node.ToBytes();
Assert.Equal(25, bytes.Length);
var simNode = Fass2SimNodeMessage.FromBytes(bytes, 0);
Assert.Equal((ushort)4, simNode.Node);
Assert.Equal((ushort)100, simNode.Distance);
Assert.Equal((byte)1, simNode.StartStop);
Assert.Equal((byte)2, simNode.Byroad);
Assert.Equal((ushort)60, simNode.Speed);
}
}
}
@@ -0,0 +1,95 @@
using StandardScene.Magnetic.Protocol;
using StandardScene.Magnetic.Tests.TestHelpers;
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using Xunit;
namespace StandardScene.Magnetic.Tests.Protocol
{
public sealed class Fass2UdpHubMultiCarTests : IDisposable
{
private readonly int _hubPort;
private readonly TestUdpCar _car1;
private readonly TestUdpCar _car2;
public Fass2UdpHubMultiCarTests()
{
_hubPort = AllocateUdpPort();
_car1 = new TestUdpCar(1, AllocateUdpPort());
_car2 = new TestUdpCar(2, AllocateUdpPort());
Fass2UdpHub.Register(_car1);
Fass2UdpHub.Register(_car2);
Fass2UdpHub.EnsureStarted(_hubPort);
Thread.Sleep(100);
}
[Fact]
public void Hub_RoutesStateToRegisteredCar_AndRepliesWith0x10()
{
SendState(_car1.VehicleCode, 2, 10, 100);
var ack1 = _car1.WaitForAck();
Assert.NotNull(ack1);
Assert.Equal(Fass2Protocol.ControlFrameLength, ack1.Length);
Assert.Equal(Fass2Protocol.CmdStateResponse, ack1[1]);
Assert.Equal(0x01, ack1[2]);
Assert.Single(_car1.Reports);
Assert.Equal((ushort)10, _car1.Reports[0].Node.Node);
Assert.Empty(_car2.Reports);
}
[Fact]
public void Hub_DispatchesMultipleCarsIndependently()
{
SendState(1, 1, 11, 201);
SendState(2, 2, 22, 202);
Assert.NotNull(_car1.WaitForAck());
Assert.NotNull(_car2.WaitForAck());
Assert.Single(_car1.Reports);
Assert.Single(_car2.Reports);
Assert.Equal((ushort)11, _car1.Reports[0].Node.Node);
Assert.Equal((ushort)22, _car2.Reports[0].Node.Node);
Assert.Equal((ulong)201, _car1.Reports[0].Task);
Assert.Equal((ulong)202, _car2.Reports[0].Task);
}
[Fact]
public void Hub_IgnoresUnregisteredCarCode()
{
SendState(99, 2, 30, 0);
Thread.Sleep(300);
Assert.Empty(_car1.Reports);
Assert.Empty(_car2.Reports);
}
public void Dispose()
{
Fass2UdpHub.Unregister(_car1);
Fass2UdpHub.Unregister(_car2);
Fass2UdpHub.Stop();
_car1.Dispose();
_car2.Dispose();
}
private void SendState(ushort carCode, byte state, ushort nodeId, ulong taskId)
{
var frame = Fass2TestFrameBuilder.BuildStateFrame(carCode, state, nodeId, taskId);
using var sender = new UdpClient();
sender.Send(frame, frame.Length, new IPEndPoint(IPAddress.Loopback, _hubPort));
Thread.Sleep(80);
}
private static int AllocateUdpPort()
{
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
listener.Stop();
return port;
}
}
}