Files
StandardSence/StandardScene.Magnetic/CarTypes/Mag2Car.cs
T

1898 lines
66 KiB
C#

using LessokajiWeaverUtilities.Utilities;
using SimpleCore;
using SimpleCore.Compiler;
using SimpleCore.BasicProps;
using SimpleCore.Library;
using SimpleCore.PropType;
using SimpleCore.Traffic;
using SimpleLite.Props;
using SimpleLite.RCS;
using SimpleLite.RCS.CarTypes;
using SimpleLite.UI;
using StandardScene.Magnetic.Protocol;
using StandardScene.Magnetic.Tasking;
using StandardScene;
using System;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
namespace StandardScene.CarTypes
{
/// <summary>
/// 基于 FASS 2.0 通用控制接口实现的磁导航小车类型。
/// 支持 PLC/TCP(主动查询)与 PCB/UDP(被动收状态 + 0x10 应答)两种通讯模式。
/// 路径段通过 0xB1 下发,业务逻辑对齐 <c>FASS.Scheduler</c> Fairyland 调度实现。
/// </summary>
[TemplateTrackCoderSettings(
priority = 1,
templateString = "agv.Mag2Go(${src.id},${dst.id},${track.Speed});",
blockVerb = "true",
trackFields = typeof(BasicTrackFields))]
[CarType(Name = "Mag2Car")]
[I18N.DocumentTranslation(Name = "Mag2Car", locale = "en")]
[EnvelopConfig(lengthX = 1200, lengthY = 800, centerX = 0, centerY = 0)]
public class Mag2Car : GhostCar, IFass2UdpCar, IFass2LoopCar
{
private readonly object _syncRoot = new object();
private TcpClient _persistentClient;
private DateTime _lastPollTime = DateTime.MinValue;
private DateTime _lastUdpReportTime = DateTime.MinValue;
private bool _udpRegistered;
private ushort _registeredVehicleCode = ushort.MaxValue;
private long _nextTaskId = 1;
private long _nextActionId = 1;
private Fass2TaskStateMachine _taskStateMachine;
private Fass2TrafficLocker _trafficLocker;
private bool _taskRestoreAttempted;
private bool _wasOffline;
private int _reconnectWaitSiteId = -1;
private int _lastOffPathHandledSiteId = -1;
private ushort _lastMappedNode;
private ushort _lastMappedDistance;
[FieldMember] public Fass2CommMode CommMode = Fass2CommMode.Udp;
[FieldMember] public int Port = 5000;
[FieldMember] public int ListenPort = 20103;
[FieldMember] public ushort VehicleCode = 0;
[FieldMember] public int ConnectTimeoutMs = 500;
[FieldMember] public int SendTimeoutMs = 500;
[FieldMember] public int ReceiveTimeoutMs = 500;
[FieldMember] public int PollIntervalMs = 1000;
[FieldMember] public int UdpOfflineTimeoutMs = 15000;
[FieldMember] public int MoveTimeoutSeconds = 120;
[FieldMember] public int LockCount = 4;
[FieldMember] public bool EnableTrafficControl = true;
[FieldMember] public bool VerifyTrajectoryFieldsOnStation = false;
[FieldMember] public int TaskResendIntervalMs = 500;
[FieldMember] public int ActionRetryIntervalMs = 1000;
[FieldMember] public bool EnableTaskStateMachine = true;
[FieldMember] public bool EnableLoopTaskDrive = true;
[FieldMember] public bool StartBeforeMove = true;
[FieldMember] public bool UseTagValueAsNode = false;
[FieldMember] public bool UsePersistentConnection = true;
[FieldMember] public bool EnableDetailLog = true;
[FieldMember] public bool LogRawFrame = true;
[FieldMember] public bool EnableFileLog = true;
[FieldMember] public string LogDirectory = "logs";
[FieldMember] public float CarLength = 1200;
[FieldMember] public float CarWidth = 800;
public Fass2StateReport LastReport { get; private set; } = new Fass2StateReport();
public bool IsTaskIdle => _taskStateMachine?.IsIdle ?? true;
public Fass2TaskPhase TaskPhase => _taskStateMachine?.Context.Phase ?? Fass2TaskPhase.Idle;
public bool HasGoalSite => TryGetGoalSiteId(out _);
public int? GoalSiteId => TryGetGoalSiteId(out var goalSiteId) ? goalSiteId : null;
public event Action<int> TaskCompleted;
string IFass2UdpCar.RemoteAddress => address;
int IFass2UdpCar.RemotePort => Port;
ushort IFass2UdpCar.VehicleCode => VehicleCode;
void IFass2UdpCar.OnUdpStateReceived(Fass2StateReport report)
{
lock (_syncRoot)
{
_lastUdpReportTime = DateTime.Now;
ApplyReport(report);
TryRecoverAfterReconnectIfNeeded(report);
SyncUdpOnlineTags(true);
}
TickTaskStateMachine(report);
}
public new static async Task<Mag2Car> Create()
{
var car = new Mag2Car
{
lstatus = "连接中",
address = "127.0.0.1",
name = "Mag2Car",
haveCoordination = true,
speed = 1
};
car.EnsureTaskStateMachine();
Mag2CarFileLogger.Configure(car.LogDirectory, car.EnableFileLog);
return car;
}
protected override void draw(Graphics eGraphics)
{
var halfLength = CarLength / 2;
var halfWidth = CarWidth / 2;
eGraphics.FillRectangle(Brushes.DimGray, -halfLength, -halfWidth, CarLength, CarWidth);
eGraphics.DrawRectangle(Pens.White, -halfLength, -halfWidth, CarLength, CarWidth);
using (var orientPen = new Pen(Color.Cyan, 3))
{
orientPen.CustomEndCap = new AdjustableArrowCap(6, 6, true);
orientPen.StartCap = LineCap.RoundAnchor;
eGraphics.DrawLine(orientPen, 0, 0, halfLength, 0);
}
}
public override string SetDisplayInfo()
{
try
{
var report = LastReport;
var state = Fass2Protocol.StateText(report.State);
var alarm = report.Alarm == 0 ? "" : $"|alarm:0x{report.Alarm:X}";
return $"{name}({id})\n{state}|site:{siteID}|soc:{report.BatteryCharge}|node:{report.Node.Node}{alarm}";
}
catch
{
return $"{name}({id})\n状态未知";
}
}
public override void keepAlive()
{
if ((DateTime.Now - _lastPollTime).TotalMilliseconds < PollIntervalMs)
{
return;
}
_lastPollTime = DateTime.Now;
if (CommMode == Fass2CommMode.Udp)
{
try
{
EnsureUdpSession();
TryRestoreTaskFromTags();
if (_lastUdpReportTime != DateTime.MinValue &&
(DateTime.Now - _lastUdpReportTime).TotalMilliseconds <= UdpOfflineTimeoutMs)
{
haveCoordination = true;
SyncUdpOnlineTags(true);
TryLogLoopAssignmentToFile();
var report = LastReport;
DetailLog($"keepAlive udp ok, node={report.Node.Node}, state={Fass2Protocol.StateText(report.State)}, soc={report.BatteryCharge}, taskPhase={TaskPhase}");
}
else
{
lstatus = "离线";
_wasOffline = true;
SyncUdpOnlineTags(false);
DetailLog($"keepAlive udp waiting, listen={ListenPort}, remote={address}:{Port}");
}
}
catch (Exception ex)
{
lstatus = "离线";
_wasOffline = true;
SyncUdpOnlineTags(false);
DetailLog($"keepAlive udp failed: {ex.Message}");
}
return;
}
try
{
DetailLog($"keepAlive poll begin, endpoint={address}:{Port}");
var report = RefreshState();
haveCoordination = true;
DetailLog($"keepAlive ok, node={report.Node.Node}, state={Fass2Protocol.StateText(report.State)}, soc={report.BatteryCharge}");
}
catch (Exception ex)
{
lstatus = IsBenignSocketFault(ex) ? lstatus : "离线";
if (!IsBenignSocketFault(ex))
{
_wasOffline = true;
DetailLog($"keepAlive failed: {ex.Message}");
}
}
}
private bool _running;
private int _lastSyncedTrafficSiteId = -1;
private int _lastFileLoggedGoalSite = -1;
public override async Task actualSendScript(string script)
{
if (_running)
{
throw new InvalidOperationException($"Mag2Car {id} already running script");
}
if (ShouldDriveLoopGoalAsTrackedTask(script))
{
_running = true;
try
{
DetailLog($"loop goal drive intercept, goal={GoalSiteId}, scriptLen={(script ?? string.Empty).Length}");
await ExecuteGoalTaskAsync();
DetailLog("loop goal drive completed");
}
catch (Exception ex)
{
DetailLog($"loop goal drive failed: {ExceptionFormatter.FormatEx(ex)}");
RecoverAfterGoalDriveFailure();
throw;
}
finally
{
_running = false;
}
return;
}
_running = true;
try
{
DetailLog($"script begin, length={(script ?? string.Empty).Length}");
var agv = new Mag2CarInterface(id);
var tcs = new TaskCompletionSource<int>();
new Thread(() =>
{
try
{
SelfEvaluating(agv, script);
tcs.SetResult(1);
}
catch (Exception ex)
{
tcs.SetException(ex);
}
})
{ Name = $"eva_{name}({id}):{status.programs.now.name}" }.Start();
await tcs.Task;
await agv.WaitAsync();
DetailLog("script completed");
}
catch (Exception ex)
{
DetailLog($"script failed: {ExceptionFormatter.FormatEx(ex)}");
throw;
}
finally
{
_running = false;
}
}
[MethodMember(Name = "查询状态", Description = "按FASS 2.0协议查询AGV状态")]
public void QueryState()
{
RefreshState();
}
[MethodMember(Name = "启动", Description = "发送启动指令(0x01)")]
public void StartMag2Car()
{
SendControl(Fass2Protocol.CmdStart, 0);
}
[MethodMember(Name = "停止", Description = "发送停止指令(0x02)")]
public void StopMag2Car()
{
SendControl(Fass2Protocol.CmdStop, 0);
}
[MethodMember(Name = "急停", Description = "发送急停指令(0x03)")]
public void EmergencyStopMag2Car()
{
SendControl(Fass2Protocol.CmdEmergencyStop, 0);
}
[MethodMember(Name = "进入小车远程", Description = "进入小车远程桌面")]
[I18N.DocumentTranslation(Name = "Open remote desktop", Description = "Open the car's remote desktop", locale = "en")]
public void Mstsc()
{
Process.Start(new ProcessStartInfo
{
FileName = "mstsc",
Arguments = $"/v:{address}",
UseShellExecute = false,
CreateNoWindow = true
});
}
public void EmergencyStop(string reason)
{
Diagnosis.Post($"car{name}:Mag2Car EmergencyStop {reason}");
SendControl(Fass2Protocol.CmdEmergencyStop, 0);
}
public void EmergencyRelease()
{
Diagnosis.Post($"car{name}:Mag2Car EmergencyRelease");
SendControl(Fass2Protocol.CmdStart, 0);
}
[MethodMember(Name = "重置UDP监听", Description = "重新注册 UDP 会话(PCB 模式)")]
public void ResetUdpSession()
{
lock (_syncRoot)
{
if (_udpRegistered)
{
Fass2UdpHub.Unregister(_registeredVehicleCode, this);
_udpRegistered = false;
_registeredVehicleCode = ushort.MaxValue;
}
_lastUdpReportTime = DateTime.MinValue;
}
EnsureUdpSession();
DetailLog($"UDP session reset, listen={ListenPort}, remote={address}:{Port}");
}
[MethodMember(Name = "重置TCP连接", Description = "关闭当前长连接,下次命令自动重连(PLC 模式)")]
public void ResetTcpConnection()
{
lock (_syncRoot)
{
ClosePersistentConnection("manual reset");
}
}
[MethodMember(Name = "重置环线联调", Description = "清空 Loop/FASS2 标签、交管锁和脚本错误,恢复可再分配")]
public void ResetLoopDriveState()
{
lock (_syncRoot)
{
_running = false;
_lastSyncedTrafficSiteId = -1;
_taskRestoreAttempted = false;
_wasOffline = false;
_reconnectWaitSiteId = -1;
_lastOffPathHandledSiteId = -1;
if (_taskStateMachine != null && !_taskStateMachine.IsIdle)
{
_taskStateMachine.Cancel("manual loop reset");
}
ClearTaskPersistence();
ClearLoopDriveTags();
_lastFileLoggedGoalSite = -1;
var site = ResolveSite(LastReport.Node.Node);
if (site == null && GetLastSite() > 0)
{
site = SimpleLib.GetSite(GetLastSite());
}
if (site == null && siteID > 0)
{
site = SimpleLib.GetSite(siteID);
}
if (site != null)
{
TrafficReset(site, true, strict: false);
siteID = site.id;
x = site.x;
y = site.y;
_lastSyncedTrafficSiteId = site.id;
haveCoordination = true;
}
status.programs.task = Task.CompletedTask;
SyncUdpOnlineTags(IsUdpSessionOnline());
DetailLog(
$"loop drive state reset, site={(site?.id.ToString() ?? "-")}, holding=[{string.Join(",", status.holdingLocks)}], pending=[{string.Join(",", status.pendingLocks)}]");
}
}
private void ClearLoopDriveTags()
{
if (tags == null)
{
return;
}
Commons.DeleteTag(tags, "goalSite");
Commons.DeleteTag(tags, "occupied");
Commons.DeleteTag(tags, "loopAssigned");
Commons.DeleteTag(tags, "dest");
Commons.DeleteTag(tags, "deliver");
Commons.DeleteTag(tags, "changePriority");
Commons.DeleteTag(tags, "priority");
Commons.DeleteTag(tags, "currentWorkStep");
Fass2TaskPersistence.Clear(tags);
}
/// <summary>
/// 任务失败/超时后清 pending、复位本站交管锁,去掉 occupied,保留 goalSite 供环线重派。
/// </summary>
private void RecoverAfterGoalDriveFailure()
{
try
{
if (_taskStateMachine != null && !_taskStateMachine.IsIdle)
{
_taskStateMachine.Cancel("recover after failure");
}
}
catch
{
// ignore
}
try
{
ClearTaskPersistence();
Commons.DeleteTag(tags, "occupied");
var site = ResolveSite(LastReport.Node.Node);
if (site == null && siteID > 0)
{
site = SimpleLib.GetSite(siteID);
}
if (site == null && GetLastSite() > 0)
{
site = SimpleLib.GetSite(GetLastSite());
}
if (site != null)
{
TrafficReset(site, true, strict: false);
siteID = site.id;
_lastSyncedTrafficSiteId = site.id;
haveCoordination = true;
}
else
{
status.pendingLocks = Array.Empty<int>();
status.seqScope = Array.Empty<int>();
status.seqPtr = 0;
}
SyncUdpOnlineTags(IsUdpSessionOnline());
DetailLog(
$"goal drive recover done, keepGoal={GoalSiteId}, site={(site?.id.ToString() ?? "-")}, holding=[{string.Join(",", status.holdingLocks)}], pending=[{string.Join(",", status.pendingLocks)}]");
}
catch (Exception ex)
{
DetailLog($"goal drive recover failed: {ex.Message}");
}
}
private bool IsUdpSessionOnline()
{
return CommMode == Fass2CommMode.Udp &&
_lastUdpReportTime != DateTime.MinValue &&
(DateTime.Now - _lastUdpReportTime).TotalMilliseconds <= UdpOfflineTimeoutMs;
}
public Fass2StateReport RefreshState()
{
if (CommMode == Fass2CommMode.Udp)
{
EnsureUdpSession();
if (_lastUdpReportTime == DateTime.MinValue ||
(DateTime.Now - _lastUdpReportTime).TotalMilliseconds > UdpOfflineTimeoutMs)
{
throw new TimeoutException(
$"Mag2Car {name}({id}) UDP state timeout ({UdpOfflineTimeoutMs}ms), listen={ListenPort}");
}
return LastReport;
}
var request = Fass2Protocol.BuildControl(Fass2Protocol.CmdQuery, VehicleCode, 0);
var response = SendRequest(request, Fass2Protocol.StateFrameLength, "Query(0x00)");
var report = Fass2Protocol.ParseState(response);
ApplyReport(report);
TryRecoverAfterReconnectIfNeeded(report);
return report;
}
public void SendControl(byte command, ushort param)
{
var request = Fass2Protocol.BuildControl(command, VehicleCode, param);
SendRequest(request, Fass2Protocol.StateFrameLength, CommandName(command));
}
public void SendPathNodes(Fass2NodeMessage[] nodes)
{
var taskId = (ulong)Interlocked.Increment(ref _nextTaskId);
SendPathNodes(nodes, taskId);
}
public void SendPathNodes(Fass2NodeMessage[] nodes, ulong taskId)
{
var request = Fass2Protocol.BuildNodes(VehicleCode, taskId, nodes);
DetailLog($"send nodes 0xB1, task={taskId}, count={nodes.Length}");
SendRequest(request, Fass2Protocol.StateFrameLength, "Nodes(0xB1)");
}
public Fass2TaskPlan BuildTaskPlan(int startSiteId, int goalSiteId, double defaultSpeed = -1)
{
return Fass2TaskBuilder.BuildPath(startSiteId, goalSiteId, CreateTaskBuildOptions(defaultSpeed), ResolveNodeId, this);
}
public void SendTaskPlan(Fass2TaskPlan plan)
{
if (plan == null || plan.Nodes == null || plan.Nodes.Count == 0)
{
throw new ArgumentException("Fass2TaskPlan requires at least one node");
}
var taskId = (ulong)Interlocked.Increment(ref _nextTaskId);
DetailLog(
$"send task plan 0xB1, task={taskId}, sites={plan.SiteIds.Count}, nodes={plan.Nodes.Count}, batches={plan.Batches.Count}, sig={plan.FieldsSignature}");
foreach (var batch in plan.Batches)
{
SendPathNodes(batch, taskId);
}
}
public void BeginTrackedTask(int startSiteId, int goalSiteId, double defaultSpeed = -1)
{
EnsureTaskStateMachine();
_taskStateMachine.LockCount = LockCount;
_taskStateMachine.ResendIntervalMs = TaskResendIntervalMs;
_taskStateMachine.ActionRetryIntervalMs = ActionRetryIntervalMs;
_taskStateMachine.StartBeforeMove = StartBeforeMove;
Fass2ActionResolver.VerifyTrajectoryFields = VerifyTrajectoryFieldsOnStation;
_taskStateMachine.Begin(startSiteId, goalSiteId, defaultSpeed);
var tick = _taskStateMachine.Tick(LastReport);
DetailLog($"tracked task bootstrap tick: {tick.Message}");
}
public async Task WaitTrackedTaskAsync(int timeoutMs)
{
EnsureTaskStateMachine();
var waitMs = timeoutMs <= 0 ? MoveTimeoutSeconds * 1000 : timeoutMs;
if (CommMode == Fass2CommMode.Udp)
{
await _taskStateMachine.WaitForCompletionAsync(waitMs);
return;
}
await _taskStateMachine.WaitAsync(
waitMs,
RefreshState,
Math.Max(50, Math.Min(PollIntervalMs, 500)));
}
async Task IFass2LoopCar.ExecuteGoalTaskAsync(int? goalSiteId, double defaultSpeed,
CancellationToken cancellationToken)
{
await ExecuteGoalTaskAsync(goalSiteId, defaultSpeed, cancellationToken);
}
public async Task ExecuteGoalTaskAsync(int? goalSiteId = null, double defaultSpeed = -1,
CancellationToken cancellationToken = default)
{
if (!EnableTaskStateMachine)
{
throw new InvalidOperationException("ExecuteGoalTaskAsync requires EnableTaskStateMachine=true");
}
var goalId = goalSiteId ?? GoalSiteId;
if (goalId == null || goalId.Value <= 0)
{
throw new InvalidOperationException("Mag2Car goalSite is not set");
}
var startId = ResolvePhysicalSiteId();
if (startId <= 0)
{
throw new InvalidOperationException($"Mag2Car {name}({id}) cannot resolve start site for loop task");
}
var lockedSite = status.holdingLocks.Length > 0 ? status.holdingLocks[0] : -1;
if (lockedSite > 0 && lockedSite != startId)
{
DetailLog($"ExecuteGoalTaskAsync normalize start, holding={lockedSite}, reportStart={startId}");
}
DetailLog($"ExecuteGoalTaskAsync start={startId}, goal={goalId.Value}, speed={defaultSpeed}, traffic={EnableTrafficControl}");
BeginTrackedTask(startId, goalId.Value, defaultSpeed);
await WaitTrackedTaskAsync(MoveTimeoutSeconds * 1000);
TaskCompleted?.Invoke(goalId.Value);
}
/// <summary>
/// 环线联调以 UDP 上报节点映射的站点为准,避免 FindRoute/forecast 污染 holdingLocks 后 GetLastSite 变成目标站。
/// </summary>
private int ResolvePhysicalSiteId()
{
if (LastReport.Node.Node != 0)
{
var fromReport = ResolveSite(LastReport.Node.Node);
if (fromReport != null)
{
return fromReport.id;
}
}
if (GetLastSite() > 0)
{
return GetLastSite();
}
return siteID > 0 ? siteID : -1;
}
private bool ShouldDriveLoopGoalAsTrackedTask(string script)
{
if (!EnableLoopTaskDrive || !EnableTaskStateMachine || !HasGoalSite)
{
return false;
}
return !string.IsNullOrEmpty(script) &&
script.Contains("Mag2Go", StringComparison.Ordinal);
}
private bool TryGetGoalSiteId(out int goalSiteId)
{
goalSiteId = 0;
if (tags == null || !tags.TryGetValue("goalSite", out var goalText) ||
!int.TryParse(goalText, out goalSiteId))
{
return false;
}
return goalSiteId > 0;
}
private void EnsureTaskStateMachine()
{
if (_taskStateMachine != null)
{
return;
}
_trafficLocker = new Fass2TrafficLocker(this, message => DetailLog(message));
_taskStateMachine = new Fass2TaskStateMachine(new Fass2TaskCallbacks
{
BuildPlan = BuildTaskPlan,
SendNodes = (nodes, taskId) => SendPathNodes(nodes, taskId),
SendAction = SendAction,
SendControl = SendControl,
ResolveNodeId = ResolveNodeId,
ResolveSite = ResolveSite,
AllocateTaskId = () => (ulong)Interlocked.Increment(ref _nextTaskId),
AllocateActionId = () => (ulong)Interlocked.Increment(ref _nextActionId),
Persist = PersistTaskContext,
ClearPersisted = ClearTaskPersistence,
Log = message => DetailLog($"task-sm {message}"),
PrepareTraffic = (siteIds, fromIndex) =>
{
if (!EnableTrafficControl)
{
return;
}
_trafficLocker.PrepareSequence(siteIds, fromIndex);
},
EnsureTrafficWindow = (siteIds, fromIndex, wantWindow) =>
{
if (!EnableTrafficControl)
{
return wantWindow;
}
return _trafficLocker.EnsureWindowLocked(siteIds, fromIndex, wantWindow);
},
LeaveTraffic = siteId =>
{
if (!EnableTrafficControl)
{
return;
}
_trafficLocker.LeavePassed(siteId);
},
ReleaseTrafficBehind = (siteIds, keepFromIndex) =>
{
if (!EnableTrafficControl)
{
return;
}
_trafficLocker.ReleaseLocksBehind(siteIds, keepFromIndex);
},
RebaseTraffic = (siteIds, fromIndex) =>
{
if (!EnableTrafficControl)
{
return;
}
_trafficLocker.RebaseFrom(siteIds, fromIndex, LockCount);
},
FinalizeTraffic = siteId =>
{
if (!EnableTrafficControl)
{
return;
}
_trafficLocker.FinalizeAtSite(siteId);
},
CheckControlRelease = siteId =>
{
try
{
return Fass2ControlAreaOccupancy.Check(this, siteId);
}
catch (Exception ex)
{
DetailLog($"control occupancy failed: site={siteId}, {ex.Message}");
return new Fass2ControlReleaseCheck
{
CanRelease = false,
Reason = ex.Message
};
}
}
});
}
private void TickTaskStateMachine(Fass2StateReport report)
{
if (!EnableTaskStateMachine || _taskStateMachine == null || !_taskStateMachine.IsRunning)
{
return;
}
var tick = _taskStateMachine.Tick(report);
if (!string.IsNullOrEmpty(tick.Message) &&
(tick.Dispatched || tick.Rebuilt || tick.ActionSent || tick.Advanced || tick.Completed || tick.Faulted))
{
DetailLog($"task-sm tick phase={tick.Phase}, {tick.Message}");
}
UpdateTaskStatusEnums();
}
private void TryRestoreTaskFromTags()
{
if (!EnableTaskStateMachine || _taskRestoreAttempted || tags == null)
{
return;
}
_taskRestoreAttempted = true;
if (!Fass2TaskPersistence.TryLoad(tags, out var saved))
{
return;
}
EnsureTaskStateMachine();
if (_taskStateMachine.TryRestore(saved))
{
DetailLog(
$"task restored from tags, phase={saved.Phase}, goal={saved.GoalSiteId}, index={saved.CurrentIndex}, task={saved.TaskId}");
UpdateTaskStatusEnums();
}
}
private void TryRecoverAfterReconnectIfNeeded(Fass2StateReport report)
{
var site = report?.Node != null ? ResolveSite(report.Node.Node) : null;
if (site == null)
{
if (_wasOffline || _reconnectWaitSiteId > 0)
{
DetailLog($"reconnect skip, unmapped node={report?.Node.Node}");
}
return;
}
EnsureTaskStateMachine();
var running = _taskStateMachine != null && _taskStateMachine.IsRunning;
var onRoute = running && _taskStateMachine.TryFindIndexOnRoute(site.id, out _);
var offlineRecover = _wasOffline || _reconnectWaitSiteId > 0;
var offPathRecover = running && !onRoute && _lastOffPathHandledSiteId != site.id;
if (!offlineRecover && !offPathRecover)
{
return;
}
if (_reconnectWaitSiteId > 0 && _reconnectWaitSiteId != site.id)
{
_reconnectWaitSiteId = -1;
}
_wasOffline = false;
var holding = status.holdingLocks == null || status.holdingLocks.Length == 0
? string.Empty
: string.Join(",", status.holdingLocks);
var routeText = FormatRouteSites();
DetailLog(
$"reconnect site={site.id}, onRoute={onRoute}, offPath={offPathRecover}, taskPhase={TaskPhase}, holding=[{holding}], goal={GoalSiteId?.ToString() ?? "-"}, route=[{routeText}]");
if (!running)
{
_reconnectWaitSiteId = -1;
DetailLog("reconnect idle, keep SyncTrafficFromReport");
return;
}
if (!TryClaimReportedSite(site))
{
_reconnectWaitSiteId = site.id;
DetailLog($"reconnect wait, site={site.id} held by another car");
return;
}
_reconnectWaitSiteId = -1;
try
{
if (onRoute)
{
if (_taskStateMachine.IsAtRouteEnd(site.id))
{
TrafficResetToReportedSite(site);
_taskStateMachine.TryCompleteAtSite(site.id);
DetailLog($"reconnect rebase complete at end site={site.id}");
return;
}
if (_taskStateMachine.RebaseToSite(site.id))
{
_lastSyncedTrafficSiteId = site.id;
DetailLog($"reconnect rebase site={site.id}");
}
return;
}
_lastOffPathHandledSiteId = site.id;
RecoverOffOriginalRoute(site);
}
catch (Exception ex)
{
DetailLog($"reconnect recover failed: {ex.Message}");
}
}
/// <summary>
/// 上报站不在当前任务 Plan/RouteSiteIds 上:按 tasklist 归属链路重下,不再用原 goal 续跑。
/// </summary>
private void RecoverOffOriginalRoute(Site site)
{
var originalGoal = _taskStateMachine.Context.GoalSiteId > 0
? _taskStateMachine.Context.GoalSiteId
: GoalSiteId ?? 0;
var matches = Mag2LoopMission.FindChainsContaining(site.id);
if (originalGoal > 0 && matches.Count > 0)
{
matches = matches.Where(m => m != null && m.TargetSiteId != originalGoal).ToList();
}
if (Fass2ReconnectChainSelector.TrySelect(matches, preferGoalSiteId: null, out var chain, out var reason)
&& chain != null)
{
DetailLog($"reconnect off-path chain={chain}, reason={reason}, dropGoal={originalGoal}");
if (chain.IsAtEndPoint || chain.TargetSiteId == site.id)
{
HandOffToAutoLoop(site);
_taskStateMachine.TryCompleteAtSite(site.id);
DetailLog($"reconnect chain end site={site.id}, goal={chain.TargetSiteId}");
return;
}
SetGoalSiteTag(chain.TargetSiteId);
if (_taskStateMachine.RestartFromSite(site.id, chain.TargetSiteId))
{
TrafficResetToReportedSite(site);
_lastSyncedTrafficSiteId = site.id;
DetailLog($"reconnect restart start={site.id}, goal={chain.TargetSiteId}");
return;
}
}
DetailLog(
$"reconnect off-path unmatched site={site.id}, dropGoal={originalGoal}, matches={matches.Count}, {reason ?? "no chain"}");
if (!EnableLoopTaskDrive && originalGoal > 0 && originalGoal != site.id)
{
try
{
var probe = BuildTaskPlan(site.id, originalGoal);
if (probe?.SiteIds != null && probe.SiteIds.Count > 0)
{
SetGoalSiteTag(originalGoal);
if (_taskStateMachine.RestartFromSite(site.id, originalGoal))
{
TrafficResetToReportedSite(site);
_lastSyncedTrafficSiteId = site.id;
DetailLog($"reconnect restart keepGoal (non-loop) start={site.id}, goal={originalGoal}");
return;
}
}
}
catch (Exception ex)
{
DetailLog($"reconnect keepGoal plan failed: {ex.Message}");
}
}
DetailLog($"reconnect unmatched site={site.id}, handoff AutoLoop");
HandOffToAutoLoop(site);
if (!_taskStateMachine.IsIdle)
{
_taskStateMachine.Cancel("reconnect unmatched site");
}
}
private string FormatRouteSites()
{
var sites = _taskStateMachine?.Context?.Plan?.SiteIds;
if (sites == null || sites.Count == 0)
{
sites = _taskStateMachine?.Context?.RouteSiteIds;
}
if (sites == null || sites.Count == 0)
{
return string.Empty;
}
return string.Join(",", sites);
}
private bool TryClaimReportedSite(Site site)
{
if (site == null)
{
return false;
}
var holder = FindOtherCarHoldingSite(site.id);
if (holder == null)
{
return true;
}
if (TryReleaseStaleHolder(holder, site.id))
{
DetailLog($"reconnect released stale holder car={holder.id}, site={site.id}");
return true;
}
return false;
}
private void TrafficResetToReportedSite(Site site)
{
if (site == null)
{
return;
}
try
{
TrafficReset(site, true, strict: false);
siteID = site.id;
_lastSyncedTrafficSiteId = site.id;
haveCoordination = true;
}
catch (Exception ex)
{
DetailLog($"reconnect TrafficReset failed: site={site.id}, {ex.Message}");
}
}
private void HandOffToAutoLoop(Site site)
{
Commons.DeleteTag(tags, "occupied");
Commons.DeleteTag(tags, "goalSite");
Commons.DeleteTag(tags, "loopAssigned");
ClearTaskPersistence();
TrafficResetToReportedSite(site);
}
private void SetGoalSiteTag(int goalSiteId)
{
if (tags == null || goalSiteId <= 0)
{
return;
}
Commons.AddOrUpdateTag(tags, "goalSite", goalSiteId.ToString());
}
private void PersistTaskContext(Fass2TaskContext context)
{
if (tags == null || context == null)
{
return;
}
Fass2TaskPersistence.Save(tags, context);
UpdateTaskStatusEnums();
}
private void ClearTaskPersistence()
{
if (tags == null)
{
return;
}
Fass2TaskPersistence.Clear(tags);
UpdateTaskStatusEnums();
}
private void UpdateTaskStatusEnums()
{
if (_taskStateMachine == null)
{
status.enums["TaskPhase"] = Fass2TaskPhase.Idle.ToString();
return;
}
var context = _taskStateMachine.Context;
status.enums["TaskPhase"] = context.Phase.ToString();
status.enums["TaskGoal"] = context.GoalSiteId.ToString();
status.enums["TaskIndex"] = context.CurrentIndex.ToString();
status.enums["TaskFieldsSig"] = context.FieldsSignature ?? string.Empty;
if (!string.IsNullOrEmpty(context.FaultReason))
{
status.enums["TaskFault"] = context.FaultReason;
}
else
{
status.enums.Remove("TaskFault");
}
}
private Fass2TaskBuildOptions CreateTaskBuildOptions(double defaultSpeed = -1)
{
return new Fass2TaskBuildOptions
{
UseTagValueAsNode = UseTagValueAsNode,
DefaultSpeed = defaultSpeed > 0 ? defaultSpeed : 12,
CarSpeed = speed
};
}
public void SendAction(Fass2NodeMessage node, ulong actionId = 0)
{
var request = Fass2Protocol.BuildAction(VehicleCode, actionId, node);
DetailLog($"send action 0xA1, actionId={actionId}, node={node.Node}");
SendRequest(request, Fass2Protocol.ActionFrameLength, "Action(0xA1)");
}
private byte[] SendRequest(byte[] request, int responseLength, string commandName)
{
DetailLog($"TX {commandName}, len={request.Length}, mode={CommMode}");
if (LogRawFrame)
{
DetailLog($"TX raw {Fass2Protocol.ToHex(request)}");
}
if (CommMode == Fass2CommMode.Udp)
{
EnsureUdpSession();
Fass2UdpHub.SendToCar(this, request);
DetailLog($"UDP TX {commandName} done (async, no sync RX)");
return Array.Empty<byte>();
}
byte[] response;
lock (_syncRoot)
{
response = SendAndReceive(request, responseLength);
}
DetailLog($"RX {commandName}, bytes={response.Length}");
if (LogRawFrame)
{
DetailLog($"RX raw {Fass2Protocol.ToHex(response)}");
}
if (response.Length >= Fass2Protocol.StateFrameLength)
{
ApplyReport(Fass2Protocol.ParseState(response));
}
return response;
}
private void EnsureUdpSession()
{
if (string.IsNullOrWhiteSpace(address))
{
throw new InvalidOperationException("Mag2Car address is empty for UDP mode");
}
if (!_udpRegistered || _registeredVehicleCode != VehicleCode)
{
if (_udpRegistered && _registeredVehicleCode != VehicleCode)
{
Fass2UdpHub.Unregister(_registeredVehicleCode, this);
_udpRegistered = false;
}
Fass2UdpHub.Register(this);
Fass2UdpHub.EnsureStarted(ListenPort);
_udpRegistered = true;
_registeredVehicleCode = VehicleCode;
DetailLog($"UDP registered, VehicleCode={VehicleCode}, listen={ListenPort}, remote={address}:{Port}");
}
}
private byte[] SendAndReceive(byte[] request, int responseLength)
{
return UsePersistentConnection
? SendAndReceivePersistent(request, responseLength)
: SendAndReceiveShort(request, responseLength);
}
private byte[] SendAndReceivePersistent(byte[] request, int responseLength)
{
try
{
EnsurePersistentConnection();
var stream = _persistentClient.GetStream();
stream.Write(request, 0, request.Length);
stream.Flush();
return ReadExact(stream, responseLength);
}
catch (Exception ex)
{
ClosePersistentConnection($"persistent send/read failed: {ex.Message}");
throw;
}
}
private void EnsurePersistentConnection()
{
if (IsPersistentConnected())
{
return;
}
ClosePersistentConnection("prepare reconnect");
DetailLog($"persistent connect begin {address}:{Port}, timeout={ConnectTimeoutMs}ms");
var client = new TcpClient();
try
{
ConnectWithTimeout(client);
client.SendTimeout = SendTimeoutMs;
client.ReceiveTimeout = ReceiveTimeoutMs;
_persistentClient = client;
DetailLog($"persistent connect ok {address}:{Port}");
}
catch
{
try { client.Close(); } catch { }
_persistentClient = null;
throw;
}
}
private bool IsPersistentConnected()
{
try
{
return _persistentClient != null && _persistentClient.Connected;
}
catch
{
return false;
}
}
private void ClosePersistentConnection(string reason)
{
var client = _persistentClient;
if (client == null)
{
return;
}
_persistentClient = null;
DetailLog($"persistent connection closed, reason={reason}");
try
{
if (client.Connected)
{
client.Client?.Shutdown(SocketShutdown.Both);
}
}
catch (Exception ex) when (IsBenignSocketFault(ex)) { }
catch { }
try { client.Close(); }
catch (Exception ex) when (IsBenignSocketFault(ex)) { }
catch { }
}
private byte[] SendAndReceiveShort(byte[] request, int responseLength)
{
using (var client = new TcpClient())
{
DetailLog($"short connect begin {address}:{Port}, timeout={ConnectTimeoutMs}ms");
ConnectWithTimeout(client);
DetailLog($"short connect ok {address}:{Port}");
client.SendTimeout = SendTimeoutMs;
client.ReceiveTimeout = ReceiveTimeoutMs;
using (var stream = client.GetStream())
{
stream.Write(request, 0, request.Length);
stream.Flush();
return ReadExact(stream, responseLength);
}
}
}
private void ConnectWithTimeout(TcpClient client)
{
if (string.IsNullOrWhiteSpace(address))
{
throw new InvalidOperationException("Mag2Car address is empty");
}
using var cts = new CancellationTokenSource(ConnectTimeoutMs);
try
{
client.ConnectAsync(address, Port, cts.Token).GetAwaiter().GetResult();
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
AbortClientQuietly(client);
throw new TimeoutException($"connect {address}:{Port} timeout ({ConnectTimeoutMs}ms)");
}
catch (Exception ex) when (IsBenignSocketFault(ex) && cts.IsCancellationRequested)
{
AbortClientQuietly(client);
throw new TimeoutException($"connect {address}:{Port} timeout ({ConnectTimeoutMs}ms)", ex);
}
}
private static void AbortClientQuietly(TcpClient client)
{
if (client == null) return;
try { client.Client?.Close(); } catch (Exception ex) when (IsBenignSocketFault(ex)) { } catch { }
try { client.Close(); } catch (Exception ex) when (IsBenignSocketFault(ex)) { } catch { }
}
private static bool IsBenignSocketFault(Exception ex)
{
for (var current = ex; current != null; current = current.InnerException)
{
if (current is ObjectDisposedException) return true;
if (current is SocketException socketEx)
{
switch (socketEx.SocketErrorCode)
{
case SocketError.OperationAborted:
case SocketError.Interrupted:
case SocketError.ConnectionAborted:
case SocketError.ConnectionReset:
case SocketError.Shutdown:
return true;
}
}
if (current is IOException ioEx)
{
var message = ioEx.Message;
if (!string.IsNullOrEmpty(message) &&
(message.Contains("已中止 I/O", StringComparison.Ordinal) ||
message.Contains("I/O operation", StringComparison.OrdinalIgnoreCase) ||
message.Contains("operation was aborted", StringComparison.OrdinalIgnoreCase)))
{
return true;
}
}
}
return false;
}
private static byte[] ReadExact(NetworkStream stream, int length)
{
var buffer = new byte[length];
var offset = 0;
try
{
while (offset < length)
{
var read = stream.Read(buffer, offset, length - offset);
if (read == 0)
{
throw new IOException("remote closed before full FASS2 response");
}
offset += read;
}
return buffer;
}
catch (Exception ex) when (IsBenignSocketFault(ex))
{
throw new IOException("remote closed before full FASS2 response", ex);
}
}
/// <summary>
/// 按 FASS2 调度器 <c>GetSendNodes</c> 规则构建 src→dst 路径段(含中间站与站点/边 fields)。
/// </summary>
internal Fass2NodeMessage[] BuildMoveNodes(int srcId, int dstId, double trackSpeed = -1)
{
return Fass2TaskBuilder.BuildSegment(srcId, dstId, CreateTaskBuildOptions(trackSpeed), ResolveNodeId, this);
}
private void WaitUntilArrived(ushort targetNode, int timeoutMs)
{
var deadline = DateTime.Now.AddMilliseconds(timeoutMs <= 0 ? MoveTimeoutSeconds * 1000 : timeoutMs);
while (DateTime.Now < deadline)
{
var report = RefreshState();
if (report.Node.Node == targetNode && report.State != 1)
{
DetailLog($"arrived node={targetNode}, state={Fass2Protocol.StateText(report.State)}");
return;
}
Thread.Sleep(Math.Max(50, Math.Min(PollIntervalMs, 500)));
}
throw new TimeoutException(
$"Mag2Car {name}({id}) move timeout, target node={targetNode}, current={LastReport.Node.Node}, state={Fass2Protocol.StateText(LastReport.State)}");
}
private void ApplyReport(Fass2StateReport report)
{
LastReport = report;
lstatus = report.Alarm != 0 ? "报警" : report.State switch
{
0 => "未准备",
4 => "故障",
3 => "急停",
_ => "上线"
};
th = report.HeadingAngle;
status.enums["Protocol"] = CommMode == Fass2CommMode.Udp ? "FASS2.0/UDP" : "FASS2.0/TCP";
status.enums["CurrentNode"] = report.Node.Node.ToString();
status.enums["Alarm"] = $"0x{report.Alarm:X}";
status.enums["State"] = Fass2Protocol.StateText(report.State);
status.enums["Soc"] = report.BatteryCharge.ToString();
status.enums["BatteryHealth"] = report.BatteryHealth.ToString();
status.enums["ElectricCurrent"] = report.BatteryCurrent.ToString();
status.enums["Voltage"] = report.BatteryVoltage.ToString();
status.enums["HeadingAngle"] = report.HeadingAngle.ToString();
status.enums["Task"] = report.Task.ToString();
status.enums["StartStop"] = report.Node.StartStop.ToString();
status.enums["Lift"] = report.Node.Lift.ToString();
status.enums["Roll"] = report.Node.Roll.ToString();
UpdateTaskStatusEnums();
var site = ResolveSite(report.Node.Node);
if (site == null)
{
haveCoordination = false;
DetailLog($"report applied without mapped site, node={report.Node.Node}, state={Fass2Protocol.StateText(report.State)}");
return;
}
haveCoordination = true;
ApplyMapCoordinates(site, report);
SyncTrafficFromReport(site, report);
DetailLog(
$"report applied, node={report.Node.Node}, site={siteID}, x={x:0.###}, y={y:0.###}, dist={report.Node.Distance}, state={Fass2Protocol.StateText(report.State)}, soc={report.BatteryCharge}, task={report.Task}");
}
/// <summary>
/// 路段内按 Distance 在起止站点间插值,避免地图只在 node 跳变时才移动。
/// <para>
/// 协议行驶中 Node 一直是出发站、Distance 递增;到站才改 Node。
/// 若中途 0xB1 重下/task 重建把 Distance 清 0,旧逻辑会把车坐标打回出发站中心 → 看起来“闪回上一站”。
/// </para>
/// </summary>
private void ApplyMapCoordinates(Site site, Fass2StateReport report)
{
siteID = site.id;
var node = report.Node.Node;
var dist = report.Node.Distance;
var moving = Fass2ActionResolver.IsVehicleMoving(report.State);
// 同站行驶中 Distance 被清零:保留当前插值坐标,不要拽回站心。
if (dist <= 0 && moving &&
_lastMappedNode == node && _lastMappedDistance > 100)
{
DetailLog(
$"map coord keep (dist cleared while moving), node={node}, lastDist={_lastMappedDistance}, site={site.id}");
_lastMappedDistance = 0;
return;
}
if (dist <= 0 ||
!TryResolveMotionSegment(node, out var fromSite, out var toSite, out var segmentLenMm))
{
x = site.x;
y = site.y;
_lastMappedNode = node;
_lastMappedDistance = dist;
return;
}
var progress = Math.Min(1.0, dist / Math.Max(1.0, segmentLenMm));
var dx = toSite.x - fromSite.x;
var dy = toSite.y - fromSite.y;
var mapLen = Math.Sqrt(dx * dx + dy * dy);
if (mapLen >= 1.0)
{
x = (float)(fromSite.x + dx * progress);
y = (float)(fromSite.y + dy * progress);
_lastMappedNode = node;
_lastMappedDistance = dist;
return;
}
var track = FindTrackBetween(fromSite.id, toSite.id);
if (track != null)
{
var trackDx = toSite.x - fromSite.x;
var trackDy = toSite.y - fromSite.y;
if (Math.Abs(trackDx) + Math.Abs(trackDy) >= 1.0)
{
x = (float)(fromSite.x + trackDx * progress);
y = (float)(fromSite.y + trackDy * progress);
_lastMappedNode = node;
_lastMappedDistance = dist;
return;
}
}
// 地图起止站点坐标重合时,用协议距离沿 X 轴做最小可视化偏移。
x = (float)(fromSite.x + dist);
y = fromSite.y;
_lastMappedNode = node;
_lastMappedDistance = dist;
}
private bool TryResolveMotionSegment(ushort currentNode, out Site fromSite, out Site toSite, out double segmentLenMm)
{
fromSite = null;
toSite = null;
segmentLenMm = 0;
fromSite = ResolveSite(currentNode);
if (fromSite == null)
{
return false;
}
var plan = _taskStateMachine?.Context?.Plan;
if (plan?.Nodes == null || plan.Nodes.Count < 2)
{
return false;
}
for (var i = 0; i < plan.Nodes.Count - 1; i++)
{
if (plan.Nodes[i].Node != currentNode)
{
continue;
}
var targetNode = plan.Nodes[i + 1].Node;
toSite = ResolveSite(targetNode);
if (toSite == null)
{
return false;
}
// Distance 挂在出发站节点上(边长),不是下一站。
segmentLenMm = plan.Nodes[i].Distance;
if (segmentLenMm <= 0)
{
var dx = toSite.x - fromSite.x;
var dy = toSite.y - fromSite.y;
segmentLenMm = Math.Max(1.0, Math.Sqrt(dx * dx + dy * dy));
}
return true;
}
return false;
}
private static Track FindTrackBetween(int fromSiteId, int toSiteId)
{
foreach (var track in SimpleLib.GetAllTracks())
{
if (track.direction == 0)
{
if ((track.siteA == fromSiteId && track.siteB == toSiteId) ||
(track.siteB == fromSiteId && track.siteA == toSiteId))
{
return track;
}
continue;
}
if (track.direction == 1 && track.siteA == fromSiteId && track.siteB == toSiteId)
{
return track;
}
if (track.direction == 2 && track.siteB == fromSiteId && track.siteA == toSiteId)
{
return track;
}
}
return null;
}
private void SyncUdpOnlineTags(bool isOnline)
{
if (CommMode != Fass2CommMode.Udp)
{
return;
}
if (isOnline)
{
Commons.AddOrUpdateTag(tags, "Online", "true");
Commons.DeleteTag(tags, "agvOffline");
return;
}
Commons.DeleteTag(tags, "Online");
}
private void SyncTrafficFromReport(Site site, Fass2StateReport report)
{
if (site == null)
{
return;
}
var taskRunning = _running || (_taskStateMachine?.IsRunning ?? false);
if (taskRunning)
{
var lockedSite = status.holdingLocks.Length > 0 ? status.holdingLocks[0] : -1;
if (lockedSite == site.id)
{
_lastSyncedTrafficSiteId = site.id;
}
else
{
DetailLog(
$"traffic sync skipped during task, lock={lockedSite}, reportSite={site.id}, node={report.Node.Node}");
}
return;
}
var idleLockedSite = status.holdingLocks.Length > 0 ? status.holdingLocks[0] : -1;
if (idleLockedSite == site.id)
{
_lastSyncedTrafficSiteId = site.id;
return;
}
if (_lastSyncedTrafficSiteId == site.id && idleLockedSite == site.id)
{
return;
}
// 双车叠在同一站时,后到车不能 TrafficReset 抢锁;离线/错站的陈旧占锁则回收后重占。
var holder = FindOtherCarHoldingSite(site.id);
if (holder != null)
{
if (TryReleaseStaleHolder(holder, site.id))
{
DetailLog(
$"traffic sync released stale holder car={holder.id}, site={site.id}, node={report.Node.Node}");
}
else
{
if (_lastSyncedTrafficSiteId != -site.id)
{
DetailLog(
$"traffic sync skip, site={site.id} held by car={holder.id}, selfLock={idleLockedSite}, node={report.Node.Node}");
_lastSyncedTrafficSiteId = -site.id;
}
return;
}
}
try
{
DetailLog($"traffic sync TrafficReset, lock={idleLockedSite}, targetSite={site.id}, node={report.Node.Node}");
TrafficReset(site, true, strict: false);
_lastSyncedTrafficSiteId = site.id;
}
catch (Exception ex)
{
DetailLog($"traffic sync failed: {ex.Message}");
}
}
private Car FindOtherCarHoldingSite(int siteId)
{
foreach (var other in SimpleLib.GetAllCars().OfType<Car>())
{
if (other == null || other.id == id)
{
continue;
}
if (other.status?.holdingLocks != null &&
Array.IndexOf(other.status.holdingLocks, siteId) >= 0)
{
return other;
}
}
return null;
}
/// <summary>
/// 他车离线,或上报站点已离开,却仍占着本站锁 → 视为陈旧占锁,允许回收。
/// </summary>
private bool TryReleaseStaleHolder(Car other, int siteId)
{
if (other == null)
{
return false;
}
var online = other.tags?.Contains("Online") == true;
var physicalSiteId = other is Mag2Car mag2 && mag2.siteID > 0
? mag2.siteID
: other.GetLastSite();
var stale = !online || (physicalSiteId > 0 && physicalSiteId != siteId);
if (!stale)
{
return false;
}
try
{
TrafficControl.Leave(other, siteId);
return Array.IndexOf(other.status.holdingLocks ?? Array.Empty<int>(), siteId) < 0;
}
catch (Exception ex)
{
DetailLog($"traffic sync release stale holder failed: car={other.id}, site={siteId}, {ex.Message}");
return false;
}
}
private Site ResolveSite(ushort node)
{
if (node == 0)
{
return null;
}
if (UseTagValueAsNode)
{
var byTagValue = SimpleLib.GetAllSites().FirstOrDefault(site =>
site.fields.TryGetValue("TagValue", out var tag) &&
int.TryParse(tag, out var tagValue) &&
tagValue == node);
if (byTagValue != null)
{
return byTagValue;
}
}
return SimpleLib.GetAllSites().FirstOrDefault(site => site.id == node);
}
internal ushort ResolveNodeId(int siteId)
{
var site = SimpleLib.GetSite(siteId);
if (UseTagValueAsNode &&
site.fields.TryGetValue("TagValue", out var tag) &&
ushort.TryParse(tag, out var tagValue))
{
return tagValue;
}
return (ushort)Clamp(siteId, 0, ushort.MaxValue);
}
private static int Clamp(int value, int min, int max)
{
if (value < min) return min;
return value > max ? max : value;
}
private void DetailLog(string message)
{
WriteFileLog(message);
if (!EnableDetailLog)
{
return;
}
var text = $"[Mag2Car:{name}({id})] {DateTime.Now:HH:mm:ss.fff} {message}";
AppendDebug(text);
Diagnosis.Post(text, "Mag2Car", true);
}
private void WriteFileLog(string message)
{
if (!EnableFileLog)
{
return;
}
var logVehicleCode = ResolveLogVehicleCode();
if (logVehicleCode == 0)
{
return;
}
Mag2CarFileLogger.Configure(LogDirectory, true);
Mag2CarFileLogger.Write(logVehicleCode, message);
}
private ushort ResolveLogVehicleCode()
{
if (VehicleCode != 0)
{
return VehicleCode;
}
if (id <= 0 || id > ushort.MaxValue)
{
return 0;
}
return (ushort)id;
}
private void TryLogLoopAssignmentToFile()
{
if (!EnableFileLog || !TryGetGoalSiteId(out var goalSiteId))
{
_lastFileLoggedGoalSite = -1;
return;
}
if (_lastFileLoggedGoalSite == goalSiteId)
{
return;
}
_lastFileLoggedGoalSite = goalSiteId;
var startSite = GetLastSite() > 0 ? GetLastSite() : siteID;
WriteFileLog($"AutoLoop: 车辆 {name} 从站点 {startSite} 自动分配到目标站点 {goalSiteId}");
}
private static string CommandName(byte command)
{
switch (command)
{
case Fass2Protocol.CmdQuery: return "Query(0x00)";
case Fass2Protocol.CmdStart: return "Start(0x01)";
case Fass2Protocol.CmdStop: return "Stop(0x02)";
case Fass2Protocol.CmdEmergencyStop: return "EmergencyStop(0x03)";
case Fass2Protocol.CmdReset: return "Reset(0x04)";
case Fass2Protocol.CmdRest: return "Rest(0x05)";
case Fass2Protocol.CmdShutdown: return "Shutdown(0x06)";
default: return $"Unknown(0x{command:X2})";
}
}
internal sealed class Mag2CarInterface : AGVInterface
{
private readonly Mag2Car _car;
public Mag2CarInterface(int id)
{
_car = (Mag2Car)SimpleLib.GetCar(id);
}
public override bool TryLock(int siteId)
{
if (!_car.status.usage.Get().scheduling)
{
throw new Exception("abandoned");
}
if (_car.status.pendingLocks.Length > 0 && _car.status.pendingLocks[0] != siteId)
{
throw new Exception("lock not according to sequence");
}
return TrafficControl.TryLock(_car, siteId);
}
public override void Leave(int siteId)
{
TrafficControl.Leave(_car, siteId);
}
public void Mag2Go(int srcId, int dstId, double speed = -1)
{
var promise = new TaskCompletionSource<int>();
Queue(async () =>
{
_car.DetailLog($"Mag2Go src={srcId}, dst={dstId}, speed={speed}, traffic={_car.EnableTrafficControl}");
if (_car.EnableTaskStateMachine)
{
_car.BeginTrackedTask(srcId, dstId, speed);
await _car.WaitTrackedTaskAsync(_car.MoveTimeoutSeconds * 1000);
}
else
{
if (_car.StartBeforeMove)
{
_car.SendControl(Fass2Protocol.CmdStart, 0);
}
var plan = _car.BuildTaskPlan(srcId, dstId, speed);
_car.DetailLog(
$"Mag2Go plan sites={plan.SiteIds.Count}, nodes={plan.Nodes.Count}, batches={plan.Batches.Count}");
_car.SendTaskPlan(plan);
_car.WaitUntilArrived(_car.ResolveNodeId(dstId), _car.MoveTimeoutSeconds * 1000);
}
promise.SetResult(1);
}, async () =>
{
await promise.Task;
});
}
}
}
}