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;
}
}
}
@@ -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();
}
}
}