2.0协议完善加简单交管配置
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user