✨ 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

@@ -279,6 +279,119 @@ public sealed class FanucJ519ClientTests : IDisposable
Assert.True(client.IsCommandQueueDrainedForTests());
}
/// <summary>
/// 验证启用命令流结束开关后,LastData 结束帧发送完成会进入仅监控状态,不再继续回发保姿命令。
/// </summary>
[Fact]
public async Task LoadCommandQueue_WithEndCommandStreamEnabled_StopsSendingAfterLastDataAndKeepsMonitoring()
{
using var client = new FanucJ519Client();
await client.ConnectAsync("127.0.0.1", Port, _cts.Token);
var initResult = await _server.ReceiveAsync(_cts.Token);
client.StartMotion();
client.LoadCommandQueue(
[
new FanucJ519Command(sequence: 0, targetJoints: [1.0, 0.0, 0.0, 0.0, 0.0, 0.0]),
new FanucJ519Command(sequence: 0, targetJoints: [2.0, 0.0, 0.0, 0.0, 0.0, 0.0], lastData: 1)
]);
await SendStatusPacketAsync(initResult.RemoteEndPoint, sequence: 800);
var first = await _server.ReceiveAsync(_cts.Token);
await SendStatusPacketAsync(initResult.RemoteEndPoint, sequence: 801);
var end = await _server.ReceiveAsync(_cts.Token);
await SendStatusPacketAsync(
initResult.RemoteEndPoint,
sequence: 802,
jointDegrees: [7.0, 8.0, 9.0, 10.0, 11.0, 12.0]);
await Assert.ThrowsAsync<TimeoutException>(
() => _server.ReceiveAsync(_cts.Token).AsTask().WaitAsync(TimeSpan.FromMilliseconds(120)));
var latest = client.GetLatestResponse();
Assert.Equal(FanucJ519Protocol.CommandPacketLength, first.Buffer.Length);
Assert.Equal(FanucJ519Protocol.CommandPacketLength, end.Buffer.Length);
Assert.Equal(0, first.Buffer[0x0c]);
Assert.Equal(1, end.Buffer[0x0c]);
Assert.NotNull(latest);
Assert.Equal(802u, latest.Sequence);
Assert.Equal(7.0, latest.JointDegrees[0], precision: 6);
await client.StopMotionAsync(_cts.Token);
}
/// <summary>
/// 验证禁用命令流结束开关后,即使队列里出现 LastData 也继续沿用旧的最后目标保持行为。
/// </summary>
[Fact]
public async Task LoadCommandQueue_WithEndCommandStreamDisabled_HoldsLastCommandAfterLastData()
{
using var client = new FanucJ519Client(endCommandStreamAfterQueueDrained: false);
await client.ConnectAsync("127.0.0.1", Port, _cts.Token);
var initResult = await _server.ReceiveAsync(_cts.Token);
client.StartMotion();
client.LoadCommandQueue(
[
new FanucJ519Command(sequence: 0, targetJoints: [1.0, 0.0, 0.0, 0.0, 0.0, 0.0]),
new FanucJ519Command(sequence: 0, targetJoints: [2.0, 0.0, 0.0, 0.0, 0.0, 0.0], lastData: 1)
]);
var packets = new List<byte[]>();
for (uint sequence = 810; sequence < 813; sequence++)
{
await SendStatusPacketAsync(initResult.RemoteEndPoint, sequence);
var result = await _server.ReceiveAsync(_cts.Token);
packets.Add(result.Buffer);
}
Assert.Equal([810u, 811u, 812u], packets
.Select(packet => BinaryPrimitives.ReadUInt32BigEndian(packet.AsSpan(0x08, 4)))
.ToArray());
Assert.Equal([1.0f, 2.0f, 2.0f], packets
.Select(packet => BinaryPrimitives.ReadSingleBigEndian(packet.AsSpan(0x1c, 4)))
.ToArray());
await client.StopMotionAsync(_cts.Token);
}
/// <summary>
/// 验证 LastData 后的仅监控状态不会阻止下一轮队列重新开始发送。
/// </summary>
[Fact]
public async Task LoadCommandQueue_AfterLastDataMonitoring_CanStreamNewQueue()
{
using var client = new FanucJ519Client();
await client.ConnectAsync("127.0.0.1", Port, _cts.Token);
var initResult = await _server.ReceiveAsync(_cts.Token);
client.StartMotion();
client.LoadCommandQueue(
[
new FanucJ519Command(sequence: 0, targetJoints: [1.0, 0.0, 0.0, 0.0, 0.0, 0.0]),
new FanucJ519Command(sequence: 0, targetJoints: [2.0, 0.0, 0.0, 0.0, 0.0, 0.0], lastData: 1)
]);
await SendStatusPacketAsync(initResult.RemoteEndPoint, sequence: 820);
_ = await _server.ReceiveAsync(_cts.Token);
await SendStatusPacketAsync(initResult.RemoteEndPoint, sequence: 821);
_ = await _server.ReceiveAsync(_cts.Token);
client.LoadCommandQueue(
[
new FanucJ519Command(sequence: 0, targetJoints: [3.0, 0.0, 0.0, 0.0, 0.0, 0.0]),
new FanucJ519Command(sequence: 0, targetJoints: [4.0, 0.0, 0.0, 0.0, 0.0, 0.0], lastData: 1)
]);
await SendStatusPacketAsync(initResult.RemoteEndPoint, sequence: 822);
var restarted = await _server.ReceiveAsync(_cts.Token);
Assert.Equal(FanucJ519Protocol.CommandPacketLength, restarted.Buffer.Length);
Assert.Equal(822u, BinaryPrimitives.ReadUInt32BigEndian(restarted.Buffer.AsSpan(0x08, 4)));
Assert.Equal(0, restarted.Buffer[0x0c]);
Assert.Equal(3.0f, BinaryPrimitives.ReadSingleBigEndian(restarted.Buffer.AsSpan(0x1c, 4)), precision: 6);
await client.StopMotionAsync(_cts.Token);
}
/// <summary>
/// 验证停止运动后可在同一连接内重启发送,命令序号仍由新的状态包决定。
/// </summary>