✨ feat(server): 添加服务启动自动初始化机器人功能

* 在 ControllerClientCompatOptions 中新增 AutoInitOptions 配置模型
* 创建 AutoInitRobotHostedService 实现启动时自动初始化链路
* 在 appsettings.json 的 ControllerClientCompat.AutoInit 节配置
  RobotName 和 RobotIp
* 启动时依次执行 SetUpRobot → SetActiveController → Connect →
  EnableRobot;配置缺失或连接失败不阻止服务启动
* 更新 README.md 和现场工作流文档
This commit is contained in:
2026-06-22 13:29:04 +08:00
parent 5c5eaf850d
commit ac41d04a54
12 changed files with 397 additions and 35 deletions

View File

@@ -12,6 +12,10 @@ public sealed class FanucJ519Client : IDisposable
private readonly object _commandLock = new();
private readonly object _responseLock = new();
private readonly ILogger<FanucJ519Client>? _logger;
/// <summary>
/// 指示预装队列发出 LastData 结束帧后是否进入仅监控状态,而不是继续发送保姿命令。
/// </summary>
private readonly bool _endCommandStreamAfterQueueDrained;
private UdpClient? _udpClient;
private CancellationTokenSource? _cts;
private Thread? _receiveThread;
@@ -25,8 +29,14 @@ public sealed class FanucJ519Client : IDisposable
private long _slowSendCount;
private long _maxReceiveToSendTicks;
private uint _sequenceBufferSize;
// 标记 StartMotion 前是否刚装载过新目标,用于区分新命令和上次运动残留目标。
/// <summary>
/// 在 StartMotion 前是否有待发送的新命令;如果为 true 则 StartMotion 不会清空当前目标,而是继续发送新目标。
/// </summary>
private bool _hasPendingCommandForStart;
/// <summary>
/// 标记命令流已通过 LastData 正常结束;该状态下仅接收状态包,不继续回发 hold 命令。
/// </summary>
private bool _monitorOnlyAfterLastData;
private bool _motionStarted;
private bool _disposed;
@@ -35,13 +45,22 @@ public sealed class FanucJ519Client : IDisposable
/// </summary>
public bool IsConnected => _udpClient is not null;
/// <summary>
/// 获取正常队列结束时是否通过 LastData 结束命令流并保留状态监控。
/// </summary>
internal bool EndCommandStreamAfterQueueDrained => _endCommandStreamAfterQueueDrained;
/// <summary>
/// 初始化 FANUC J519 客户端。
/// </summary>
/// <param name="logger">日志记录器;允许 null,供无日志场景使用。</param>
public FanucJ519Client(ILogger<FanucJ519Client>? logger = null)
/// <param name="endCommandStreamAfterQueueDrained">队列结束帧发出后是否停止命令流并保留状态监控。</param>
public FanucJ519Client(
ILogger<FanucJ519Client>? logger = null,
bool endCommandStreamAfterQueueDrained = true)
{
_logger = logger;
_endCommandStreamAfterQueueDrained = endCommandStreamAfterQueueDrained;
}
/// <summary>
@@ -113,6 +132,7 @@ public sealed class FanucJ519Client : IDisposable
}
_hasPendingCommandForStart = false;
_monitorOnlyAfterLastData = false;
_motionStarted = true;
}
@@ -156,6 +176,7 @@ public sealed class FanucJ519Client : IDisposable
{
_motionStarted = false;
_hasPendingCommandForStart = false;
_monitorOnlyAfterLastData = false;
CompleteCommandQueueLocked();
}
@@ -178,6 +199,7 @@ public sealed class FanucJ519Client : IDisposable
CompleteCommandQueueLocked();
_currentCommand = command;
_hasPendingCommandForStart = true;
_monitorOnlyAfterLastData = false;
_commandHistoryForTests?.Add(command);
}
@@ -211,6 +233,8 @@ public sealed class FanucJ519Client : IDisposable
// 队列耗尽后继续保持最后一帧目标,避免运动结束后回落到旧目标或空目标。
_currentCommand = commands[^1];
_hasPendingCommandForStart = true;
_monitorOnlyAfterLastData = false;
_motionStarted = true;
_commandQueueDrainedCompletion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
_commandHistoryForTests?.AddRange(commands);
}
@@ -337,6 +361,7 @@ public sealed class FanucJ519Client : IDisposable
CompleteCommandQueueLocked();
_commandHistoryForTests = null;
_hasPendingCommandForStart = false;
_monitorOnlyAfterLastData = false;
_motionStarted = false;
}
@@ -375,6 +400,7 @@ public sealed class FanucJ519Client : IDisposable
{
CompleteCommandQueueLocked();
_hasPendingCommandForStart = false;
_monitorOnlyAfterLastData = false;
}
}
@@ -526,9 +552,15 @@ public sealed class FanucJ519Client : IDisposable
{
FanucJ519Command? command;
var willDrainQueue = false;
var suppressHoldCommand = false;
lock (_commandLock)
{
if (!_motionStarted)
if (_monitorOnlyAfterLastData)
{
command = null;
suppressHoldCommand = true;
}
else if (!_motionStarted)
{
command = null;
}
@@ -546,7 +578,7 @@ public sealed class FanucJ519Client : IDisposable
}
}
if (command is null)
if (command is null && !suppressHoldCommand)
{
command = TryBuildHoldCommandFromLatestResponse(response);
}
@@ -574,7 +606,14 @@ public sealed class FanucJ519Client : IDisposable
lock (_commandLock)
{
_lastSentCommand = command;
if (willDrainQueue && _commandQueue is { Count: 0 })
if (_endCommandStreamAfterQueueDrained && command.LastData == 1)
{
// LastData 是命令流的正常结束语义;结束后保留状态接收,但不继续回发保姿包。
_monitorOnlyAfterLastData = true;
_motionStarted = false;
CompleteCommandQueueLocked();
}
else if (willDrainQueue && _commandQueue is { Count: 0 })
{
CompleteCommandQueueLocked();
}