2.0协议完善加简单交管配置
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<RootNamespace>StandardScene.Magnetic.Tests</RootNamespace>
|
||||
<AssemblyName>StandardScene.Magnetic.Tests</AssemblyName>
|
||||
<IsPackable>false</IsPackable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StandardScene.Magnetic\StandardScene.Magnetic.csproj" />
|
||||
<ProjectReference Include="..\StandardScene.Fass2Simulator\StandardScene.Fass2Simulator.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,164 @@
|
||||
using StandardScene.Magnetic.Protocol;
|
||||
using StandardScene.Magnetic.Tasking;
|
||||
using System.Collections.Generic;
|
||||
using Xunit;
|
||||
|
||||
namespace StandardScene.Magnetic.Tests.Tasking
|
||||
{
|
||||
public class Fass2ControlAreaGateTests
|
||||
{
|
||||
[Fact]
|
||||
public void Resolve_WithoutArea_UsesCurrentSite()
|
||||
{
|
||||
var info = Fass2ControlAreaGate.Resolve(new Fass2SiteActionData(), 17);
|
||||
|
||||
Assert.Equal(new[] { 17 }, info.AreaSiteIds);
|
||||
Assert.Equal("17", info.AreaId);
|
||||
Assert.Equal(1, info.Capacity);
|
||||
Assert.Equal(Fass2TaskBuilder.StartStopControlStart, info.ReleaseStartStop);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_ParsesSiteArray_AndReleasePass()
|
||||
{
|
||||
var info = Fass2ControlAreaGate.Resolve(new Fass2SiteActionData
|
||||
{
|
||||
ControlArea = "[10, 11, 12]",
|
||||
ControlCapacity = 2,
|
||||
ControlReleaseStartStop = Fass2TaskBuilder.StartStopPass
|
||||
}, 8);
|
||||
|
||||
Assert.Equal(new[] { 10, 11, 12 }, info.AreaSiteIds);
|
||||
Assert.Equal("10,11,12", info.AreaId);
|
||||
Assert.Equal(2, info.Capacity);
|
||||
Assert.Equal(Fass2TaskBuilder.StartStopPass, info.ReleaseStartStop);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseSiteIds_CommaAndSemicolon()
|
||||
{
|
||||
var ids = Fass2ControlAreaGate.ParseSiteIds("10;11|12", 8);
|
||||
Assert.Equal(new[] { 10, 11, 12 }, ids);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LimitWindow_StopsAtFirstUnreleasedControlStop()
|
||||
{
|
||||
var nodes = new[]
|
||||
{
|
||||
Node(1, Fass2TaskBuilder.StartStopPass),
|
||||
Node(2, Fass2TaskBuilder.StartStopControlStop),
|
||||
Node(3, Fass2TaskBuilder.StartStopPass),
|
||||
Node(4, Fass2TaskBuilder.StartStopPass)
|
||||
};
|
||||
|
||||
var count = Fass2ControlAreaGate.LimitWindowCount(nodes, 0, 4, releasedIndex: -1);
|
||||
|
||||
Assert.Equal(2, count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LimitWindow_AfterRelease_IncludesFollowingNodes()
|
||||
{
|
||||
var nodes = new[]
|
||||
{
|
||||
Node(1, Fass2TaskBuilder.StartStopPass),
|
||||
Node(2, Fass2TaskBuilder.StartStopControlStart),
|
||||
Node(3, Fass2TaskBuilder.StartStopPass)
|
||||
};
|
||||
|
||||
var count = Fass2ControlAreaGate.LimitWindowCount(nodes, 1, 3, releasedIndex: 1);
|
||||
|
||||
Assert.Equal(2, count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_ExcludesSelf_AllowsFirstCar()
|
||||
{
|
||||
var area = Area(1, 10, 11);
|
||||
var occupancies = new List<Fass2ControlOccupancy>
|
||||
{
|
||||
new Fass2ControlOccupancy(1, new[] { 10, 11 })
|
||||
};
|
||||
|
||||
var check = Fass2ControlAreaGate.Evaluate(1, area, occupancies);
|
||||
|
||||
Assert.True(check.CanRelease);
|
||||
Assert.Equal(0, check.OthersInArea);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_OtherCarOnListedSite_EqualsCapacity_Blocks()
|
||||
{
|
||||
var area = Area(1, 10, 11);
|
||||
var occupancies = new List<Fass2ControlOccupancy>
|
||||
{
|
||||
new Fass2ControlOccupancy(1, new[] { 8 }),
|
||||
new Fass2ControlOccupancy(2, new[] { 11 })
|
||||
};
|
||||
|
||||
var check = Fass2ControlAreaGate.Evaluate(1, area, occupancies);
|
||||
|
||||
Assert.False(check.CanRelease);
|
||||
Assert.Equal(1, check.OthersInArea);
|
||||
Assert.Contains("capacity=1", check.Reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_OtherCarOutsideList_DoesNotBlock()
|
||||
{
|
||||
var area = Area(1, 10, 11);
|
||||
var occupancies = new List<Fass2ControlOccupancy>
|
||||
{
|
||||
new Fass2ControlOccupancy(2, new[] { 99 })
|
||||
};
|
||||
|
||||
var check = Fass2ControlAreaGate.Evaluate(1, area, occupancies);
|
||||
|
||||
Assert.True(check.CanRelease);
|
||||
Assert.Equal(0, check.OthersInArea);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_CapacityTwo_AllowsSecondCar()
|
||||
{
|
||||
var area = Area(2, 10, 11);
|
||||
var occupancies = new List<Fass2ControlOccupancy>
|
||||
{
|
||||
new Fass2ControlOccupancy(1, new[] { 10 }),
|
||||
new Fass2ControlOccupancy(2, new[] { 11 })
|
||||
};
|
||||
|
||||
var check = Fass2ControlAreaGate.Evaluate(2, area, occupancies);
|
||||
|
||||
Assert.True(check.CanRelease);
|
||||
Assert.Equal(1, check.OthersInArea);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ActionResolver_ControlStop_IsNeverComplete()
|
||||
{
|
||||
var expected = Node(5, Fass2TaskBuilder.StartStopControlStop);
|
||||
var actual = Node(5, Fass2TaskBuilder.StartStopControlStop);
|
||||
|
||||
Assert.True(Fass2ActionResolver.RequiresActionWait(expected));
|
||||
Assert.False(Fass2ActionResolver.IsStationActionComplete(expected, actual, vehicleState: 2));
|
||||
}
|
||||
|
||||
private static Fass2ControlAreaInfo Area(int capacity, params int[] siteIds)
|
||||
{
|
||||
return new Fass2ControlAreaInfo
|
||||
{
|
||||
AreaSiteIds = siteIds,
|
||||
AreaId = string.Join(",", siteIds),
|
||||
Capacity = capacity,
|
||||
ReleaseStartStop = 11
|
||||
};
|
||||
}
|
||||
|
||||
private static Fass2NodeMessage Node(ushort id, byte startStop)
|
||||
{
|
||||
return new Fass2NodeMessage { Node = id, StartStop = startStop };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
using StandardScene.Magnetic.Protocol;
|
||||
using StandardScene.Magnetic.Tasking;
|
||||
using System.Collections.Generic;
|
||||
using Xunit;
|
||||
|
||||
namespace StandardScene.Magnetic.Tests.Tasking
|
||||
{
|
||||
public class Fass2ControlStopStateMachineTests
|
||||
{
|
||||
[Fact]
|
||||
public void ControlStop_TruncatesWindow_WaitsOccupancy_ThenReleases()
|
||||
{
|
||||
Fass2NodeMessage[] lastWindow = null;
|
||||
Fass2NodeMessage lastPatch = null;
|
||||
var canRelease = false;
|
||||
var nodes = new List<Fass2NodeMessage>
|
||||
{
|
||||
new Fass2NodeMessage { Node = 1, StartStop = Fass2TaskBuilder.StartStopPass, Distance = 100 },
|
||||
new Fass2NodeMessage { Node = 2, StartStop = Fass2TaskBuilder.StartStopControlStop },
|
||||
new Fass2NodeMessage { Node = 3, StartStop = Fass2TaskBuilder.StartStopPass }
|
||||
};
|
||||
var plan = new Fass2TaskPlan
|
||||
{
|
||||
StartSiteId = 10,
|
||||
GoalSiteId = 12,
|
||||
SiteIds = new List<int>(),
|
||||
Nodes = nodes,
|
||||
FieldsSignature = "keep"
|
||||
};
|
||||
|
||||
var machine = new Fass2TaskStateMachine(new Fass2TaskCallbacks
|
||||
{
|
||||
BuildPlan = (_, __, ___) => plan,
|
||||
SendNodes = (window, _) => lastWindow = window,
|
||||
SendAction = (patch, _) => lastPatch = patch,
|
||||
SendControl = (_, __) => { },
|
||||
ResolveNodeId = id => (ushort)id,
|
||||
AllocateTaskId = () => 1UL,
|
||||
AllocateActionId = () => 9UL,
|
||||
CheckControlRelease = _ => new Fass2ControlReleaseCheck
|
||||
{
|
||||
CanRelease = canRelease,
|
||||
AreaId = "DockA",
|
||||
Capacity = 1,
|
||||
OthersInArea = canRelease ? 0 : 1,
|
||||
ReleaseStartStop = Fass2TaskBuilder.StartStopControlStart,
|
||||
Reason = canRelease ? "area clear" : "area occupied"
|
||||
}
|
||||
})
|
||||
{
|
||||
StartBeforeMove = false,
|
||||
LockCount = 4,
|
||||
ResendIntervalMs = 60_000,
|
||||
ActionRetryIntervalMs = 0
|
||||
};
|
||||
|
||||
machine.Begin(10, 12);
|
||||
machine.Tick(Report(1, moving: true, startStop: 1));
|
||||
|
||||
Assert.Equal(Fass2TaskPhase.Moving, machine.Context.Phase);
|
||||
Assert.NotNull(lastWindow);
|
||||
Assert.Equal(2, lastWindow.Length);
|
||||
Assert.Equal(Fass2TaskBuilder.StartStopControlStop, lastWindow[1].StartStop);
|
||||
|
||||
machine.Tick(Report(2, moving: false, startStop: 12));
|
||||
Assert.Equal(Fass2TaskPhase.AtStation, machine.Context.Phase);
|
||||
Assert.Equal(1, machine.Context.CurrentIndex);
|
||||
|
||||
lastPatch = null;
|
||||
machine.Tick(Report(2, moving: false, startStop: 12));
|
||||
Assert.True(machine.Context.WaitingForRelease);
|
||||
Assert.Null(lastPatch);
|
||||
Assert.Equal(-1, machine.Context.ControlReleasedIndex);
|
||||
|
||||
canRelease = true;
|
||||
machine.Tick(Report(2, moving: false, startStop: 12));
|
||||
Assert.NotNull(lastPatch);
|
||||
Assert.Equal(Fass2TaskBuilder.StartStopControlStart, lastPatch.StartStop);
|
||||
Assert.Equal(1, machine.Context.ControlReleasedIndex);
|
||||
Assert.Equal(Fass2TaskBuilder.StartStopControlStart, nodes[1].StartStop);
|
||||
Assert.True(lastWindow.Length >= 2);
|
||||
Assert.Equal((ushort)3, lastWindow[lastWindow.Length - 1].Node);
|
||||
|
||||
machine.Tick(Report(2, moving: false, startStop: 11));
|
||||
Assert.Equal(Fass2TaskPhase.AtStation, machine.Context.Phase);
|
||||
Assert.Equal(1, machine.Context.CurrentIndex);
|
||||
}
|
||||
|
||||
private static Fass2StateReport Report(ushort node, bool moving, byte startStop)
|
||||
{
|
||||
return new Fass2StateReport
|
||||
{
|
||||
State = (byte)(moving ? 1 : 2),
|
||||
Node = new Fass2NodeMessage
|
||||
{
|
||||
Node = node,
|
||||
StartStop = startStop
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using StandardScene.Magnetic.Tasking;
|
||||
using System.Collections.Generic;
|
||||
using Xunit;
|
||||
|
||||
namespace StandardScene.Magnetic.Tests.Tasking
|
||||
{
|
||||
public class Fass2ReconnectChainSelectorTests
|
||||
{
|
||||
[Fact]
|
||||
public void PreferGoal_OnRemainingPath_Wins()
|
||||
{
|
||||
var matches = new List<Fass2ChainMatch>
|
||||
{
|
||||
Match(1, start: 6, target: 10, site: 8, index: 1, remain: new[] { 8, 9, 10 }, prio: 9),
|
||||
Match(2, start: 1, target: 5, site: 8, index: 3, remain: new[] { 8, 4, 5 }, prio: 1)
|
||||
};
|
||||
|
||||
var ok = Fass2ReconnectChainSelector.TrySelect(matches, preferGoalSiteId: 5, out var selected, out var reason);
|
||||
|
||||
Assert.True(ok);
|
||||
Assert.Equal(5, selected.TargetSiteId);
|
||||
Assert.Contains("preferGoal=5", reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StartPoint_Beats_Priority_WhenGoalNotOnPath()
|
||||
{
|
||||
var matches = new List<Fass2ChainMatch>
|
||||
{
|
||||
Match(1, start: 8, target: 12, site: 8, index: 0, remain: new[] { 8, 12 }, prio: 1, atStart: true),
|
||||
Match(2, start: 6, target: 10, site: 8, index: 1, remain: new[] { 8, 9, 10 }, prio: 9)
|
||||
};
|
||||
|
||||
var ok = Fass2ReconnectChainSelector.TrySelect(matches, preferGoalSiteId: 99, out var selected, out _);
|
||||
|
||||
Assert.True(ok);
|
||||
Assert.Equal(12, selected.TargetSiteId);
|
||||
Assert.True(selected.IsAtStartPoint);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ambiguous_DifferentTargets_SameScore_Fails()
|
||||
{
|
||||
var matches = new List<Fass2ChainMatch>
|
||||
{
|
||||
Match(1, start: 1, target: 5, site: 3, index: 1, remain: new[] { 3, 4, 5 }, prio: 1),
|
||||
Match(2, start: 9, target: 7, site: 3, index: 1, remain: new[] { 3, 6, 7 }, prio: 1)
|
||||
};
|
||||
|
||||
var ok = Fass2ReconnectChainSelector.TrySelect(matches, preferGoalSiteId: null, out var selected, out var reason);
|
||||
|
||||
Assert.False(ok);
|
||||
Assert.Null(selected);
|
||||
Assert.Contains("ambiguous", reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Empty_Fails()
|
||||
{
|
||||
var ok = Fass2ReconnectChainSelector.TrySelect(new List<Fass2ChainMatch>(), null, out _, out var reason);
|
||||
Assert.False(ok);
|
||||
Assert.Equal("no chain", reason);
|
||||
}
|
||||
|
||||
private static Fass2ChainMatch Match(
|
||||
int taskId, int start, int target, int site, int index, int[] remain, int prio, bool atStart = false)
|
||||
{
|
||||
return new Fass2ChainMatch
|
||||
{
|
||||
TaskId = taskId,
|
||||
TaskPriority = prio,
|
||||
CurrentSiteId = site,
|
||||
TargetSiteId = target,
|
||||
IndexInPath = index,
|
||||
DistanceToTarget = remain.Length > 0 ? remain.Length - 1 : 0,
|
||||
IsAtStartPoint = atStart || index == 0,
|
||||
IsAtEndPoint = remain.Length <= 1,
|
||||
RemainingPath = remain,
|
||||
FullPath = remain
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using StandardScene.Fass2Simulator;
|
||||
using Xunit;
|
||||
|
||||
namespace StandardScene.Magnetic.Tests.Tasking
|
||||
{
|
||||
public class Fass2SimControlStopTests
|
||||
{
|
||||
[Fact]
|
||||
public void ControlStop_StaysUntilReleasePatch()
|
||||
{
|
||||
var config = new Fass2SimConfig
|
||||
{
|
||||
InitialNode = 1,
|
||||
DefaultSegmentDistance = 200,
|
||||
DefaultSpeed = 1000,
|
||||
AutoContinueOnPass = true,
|
||||
AutoCompleteStationActions = false,
|
||||
ActionDelayMs = 0
|
||||
};
|
||||
var (vehicle, motion, actions) = Fass2SimBootstrap.CreateTestStack(config);
|
||||
vehicle.State = 1;
|
||||
|
||||
motion.OnNodesCommand(3001, new[]
|
||||
{
|
||||
new Fass2SimNodeMessage { Node = 1, StartStop = 1, Distance = 200 },
|
||||
new Fass2SimNodeMessage { Node = 2, StartStop = 12 }
|
||||
});
|
||||
|
||||
for (var i = 0; i < 20 && vehicle.Node != 2; i++)
|
||||
{
|
||||
motion.Tick(50);
|
||||
actions.Tick(50);
|
||||
}
|
||||
|
||||
Assert.Equal((ushort)2, vehicle.Node);
|
||||
Assert.Equal((byte)2, vehicle.State);
|
||||
Assert.Equal((byte)12, vehicle.StartStop);
|
||||
Assert.False(actions.CanLeaveStation());
|
||||
|
||||
motion.OnNodesCommand(3001, new[]
|
||||
{
|
||||
new Fass2SimNodeMessage { Node = 2, StartStop = 11, Distance = 200 },
|
||||
new Fass2SimNodeMessage { Node = 3, StartStop = 1 }
|
||||
});
|
||||
actions.OnActionCommand(9, new Fass2SimNodeMessage
|
||||
{
|
||||
Node = 2,
|
||||
StartStop = 11
|
||||
});
|
||||
actions.Tick(10);
|
||||
motion.OnStationActionsCompleted();
|
||||
|
||||
for (var i = 0; i < 30 && vehicle.Node != 3; i++)
|
||||
{
|
||||
motion.Tick(50);
|
||||
actions.Tick(50);
|
||||
}
|
||||
|
||||
Assert.Equal((ushort)3, vehicle.Node);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using StandardScene.Magnetic.Protocol;
|
||||
using System;
|
||||
|
||||
namespace StandardScene.Magnetic.Tests.TestHelpers
|
||||
{
|
||||
internal static class Fass2TestFrameBuilder
|
||||
{
|
||||
public static byte[] BuildStateFrame(
|
||||
ushort car,
|
||||
byte state,
|
||||
ushort nodeId,
|
||||
ulong taskId = 0,
|
||||
byte command = 0)
|
||||
{
|
||||
var frame = new byte[Fass2Protocol.StateFrameLength];
|
||||
frame[0] = Fass2Protocol.Begin;
|
||||
frame[1] = command;
|
||||
WriteUInt16(frame, 2, car);
|
||||
WriteUInt16(frame, 4, 1200);
|
||||
WriteUInt16(frame, 6, 800);
|
||||
frame[28] = 100;
|
||||
frame[29] = 100;
|
||||
frame[36] = state;
|
||||
WriteUInt64(frame, 45, taskId);
|
||||
|
||||
var node = new Fass2NodeMessage
|
||||
{
|
||||
Node = nodeId,
|
||||
StartStop = 1,
|
||||
Distance = 500,
|
||||
Speed = 120
|
||||
};
|
||||
Buffer.BlockCopy(node.ToBytes(), 0, frame, 53, 25);
|
||||
|
||||
frame[98] = Fass2Protocol.Xor(frame, 1, 97);
|
||||
frame[99] = Fass2Protocol.End;
|
||||
return frame;
|
||||
}
|
||||
|
||||
public static string ToHex(byte[] bytes)
|
||||
{
|
||||
if (bytes == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var parts = new string[bytes.Length];
|
||||
for (var i = 0; i < bytes.Length; i++)
|
||||
{
|
||||
parts[i] = bytes[i].ToString("X2");
|
||||
}
|
||||
|
||||
return string.Join(" ", parts);
|
||||
}
|
||||
|
||||
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,64 @@
|
||||
using StandardScene.Magnetic.Protocol;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
|
||||
namespace StandardScene.Magnetic.Tests.TestHelpers
|
||||
{
|
||||
internal sealed class TestUdpCar : IFass2UdpCar, IDisposable
|
||||
{
|
||||
private readonly UdpClient _ackListener;
|
||||
private readonly object _syncRoot = new object();
|
||||
|
||||
public TestUdpCar(ushort vehicleCode, int remotePort)
|
||||
{
|
||||
VehicleCode = vehicleCode;
|
||||
RemotePort = remotePort;
|
||||
_ackListener = new UdpClient(new IPEndPoint(IPAddress.Loopback, remotePort));
|
||||
_ackListener.Client.ReceiveTimeout = 500;
|
||||
}
|
||||
|
||||
public ushort VehicleCode { get; }
|
||||
|
||||
public string RemoteAddress => "127.0.0.1";
|
||||
|
||||
public int RemotePort { get; }
|
||||
|
||||
public List<Fass2StateReport> Reports { get; } = new List<Fass2StateReport>();
|
||||
|
||||
public void OnUdpStateReceived(Fass2StateReport report)
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
Reports.Add(report);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] WaitForAck(int timeoutMs = 2000)
|
||||
{
|
||||
var deadline = Environment.TickCount64 + timeoutMs;
|
||||
while (Environment.TickCount64 < deadline)
|
||||
{
|
||||
try
|
||||
{
|
||||
var remote = new IPEndPoint(IPAddress.Any, 0);
|
||||
return _ackListener.Receive(ref remote);
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
Thread.Sleep(20);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_ackListener?.Close();
|
||||
_ackListener?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user