✨ feat(server): 添加服务启动自动初始化机器人功能
* 在 ControllerClientCompatOptions 中新增 AutoInitOptions 配置模型 * 创建 AutoInitRobotHostedService 实现启动时自动初始化链路 * 在 appsettings.json 的 ControllerClientCompat.AutoInit 节配置 RobotName 和 RobotIp * 启动时依次执行 SetUpRobot → SetActiveController → Connect → EnableRobot;配置缺失或连接失败不阻止服务启动 * 更新 README.md 和现场工作流文档
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
- 2026-05-11 对 `UTTC_MS11` 的独立 `ConfigRoot` 重新生成确认:flyshot 链路若继续用离散 `jerk` 作为阻断条件,会把新结果拉长到约 `16.576s / 33.152s`;只保留速度、加速度校验并跳过 flyshot jerk 阻断后,新 `1.0x / 0.5x` 时长回落到约 `7.552s / 15.104s`,与旧抓包主段 `7.408s / 14.808s` 的差异已很小,当前按可忽略处理。详见 `docs/uttc-ms11-jerk-validation-decision-20260511.md`。
|
||||
- `MoveJoint` 已按 `2026042802-mvpoint*.pcap` 复刻为点到点临时轨迹:当前关节到目标关节的关节空间直线,五次 smoothstep 起停,按 `status=15` 运动窗口复现 `40/55/77` 点,并由 J519 层完成 `rad -> deg` 下发。
|
||||
- 单程序只对应一台机器人,上传/删除/恢复飞拍轨迹统一读写运行目录 `Config/RobotConfig.json`,不再创建独立轨迹存储文件。
|
||||
- 服务启动时支持自动初始化机器人:在 `appsettings.json` 的 `ControllerClientCompat.AutoInit` 节配置 `RobotName` 和 `RobotIp`,启动后会自动执行 `SetUpRobot → SetActiveController → Connect → EnableRobot`;配置缺失或连接失败不会阻止服务启动。
|
||||
|
||||
单位约定总览:
|
||||
|
||||
@@ -71,6 +72,7 @@
|
||||
- [x] 补齐 `Get/SetSpeedRatio`、`Get/SetTCP`、`Get/SetIO` 真机命令体与响应解析
|
||||
- [x] 保留新 HTTP 接口路线,明确不再实现旧 `50001/TCP+JSON` 网关
|
||||
- [x] 将飞拍轨迹持久化收敛到运行目录 `Config/RobotConfig.json`
|
||||
- [x] 服务启动自动初始化机器人:通过 `ControllerClientCompat.AutoInit` 配置节指定 `RobotName` 和 `RobotIp`,启动时自动执行初始化链路
|
||||
|
||||
剩余 Todo:
|
||||
|
||||
|
||||
@@ -159,7 +159,8 @@ POST /disconnect_robot/
|
||||
|
||||
真机模式下:
|
||||
|
||||
- `StopMove()` 取消当前稠密轨迹生成任务并停止 J519 状态包驱动发送。
|
||||
- 正常飞拍、MoveJoint、MovePose 结束时,默认在 J519 队列末尾追加 `LastData=1` 结束帧;结束帧发出后进入仅监控状态,继续接收 J519 状态包,不再回发保姿命令。
|
||||
- `StopMove()` 取消当前稠密轨迹生成任务并走强制停止路径,可停止 J519 状态包驱动发送。
|
||||
- `DisableRobot()` 发送 J519 packet type 2 状态输出停止包,然后 `StopProg("RVBUSTSM")`。
|
||||
- `Disconnect()` 关闭状态、命令和 J519 三条通道,并清理本地运行状态。
|
||||
|
||||
@@ -178,5 +179,5 @@ POST /disconnect_robot/
|
||||
- 主运行窗口命令序号连续,无重复 seq;J519 客户端单元测试覆盖按最新 status sequence 回发命令。
|
||||
- 响应 `status=15` 段覆盖主运行窗口,响应相对命令滞后 2 到 8 帧。
|
||||
- 实发点位相对重采样期望的全局 RMS 小于 `0.012deg`,最大绝对误差小于 `0.07deg`。
|
||||
- `lastData=0`,结束运动当前依赖 J519 packet type 2 状态输出停止包;`../j519 协议.pcap` 中另有 1 个 `LastData=1` 后紧跟 type 2 的样本,停止语义后续单独验证。
|
||||
- 历史抓包主运行窗口 `lastData=0`;当前实现默认在生成的新 J519 队列末尾追加一帧 `LastData=1` 结束命令,随后保留状态监控且不发送 packet type 2。packet type 2 仅用于禁用、异常停止、断开等强制停止路径;如现场需要旧保持行为,可通过内部开关关闭结束帧路径。
|
||||
- IO 脉冲数量和 mask 集合 `10/12/14` 与抓包一致。
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
|
||||
文档把它定义为“停止状态包输出”的控制包,不是命令流正常终止的首选动作。命令流结束应通过 command packet 的 `Last data=1` 表达。
|
||||
|
||||
当前实现:`FanucJ519Client.StopMotionAsync()` 当前会停止状态包驱动发送并发送 packet type `2`,而稠密轨迹执行期间保持 `LastData=0`。这是与 FANUC 文档最明显的语义差异之一;已有多数 UTTC 抓包显示主运行窗口 `LastData=0`,但 `../j519 协议.pcap` 中存在 1 个 `LastData=1` 后紧跟 packet type `2` 的样本,后续应单独校准停止语义。
|
||||
当前实现:`FanucJ519Client.StopMotionAsync()` 仍保留 packet type `2` 作为禁用、异常中断或断开前的强制停止路径。正常稠密运动结束由内部开关 `EndCommandStreamAfterQueueDrained` 控制,默认会在队列末尾追加一帧 `LastData=1` 的结束命令;该帧发出后客户端进入仅监控状态,继续接收 J519 状态包用于位置监控,不再持续回发保姿命令。关闭该开关时保留旧行为:队列结束后继续保持最后目标或反馈位置。`LastData` 是单帧 command packet 字段,不是全局状态;下一轮运动重新装载的新队列会从 `LastData=0` 的普通命令帧开始。
|
||||
|
||||
## 4. 通信时序重点
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
1. `FanucJ519Client` 已改为收到机器人 132B status 包后立即回发当前命令,不再由上位机本地固定 8ms 发送循环主动发包。
|
||||
2. 命令包 sequence 已按刚收到的 status packet sequence 写入,避免第一帧从本地 `0` 起步。
|
||||
3. `FanucControllerRuntime.ExecuteTrajectory()` 启动前会检查已有 J519 响应中的 `AcceptsCommand` 和 `SystemReady`;但如果还没收到状态包,则会放行,后续命令仍要等第一帧 status 到达才会发出。
|
||||
4. 当前稠密轨迹结束不发送 `LastData=1`,而是依赖停止 J519 状态包驱动发送和 packet type `2` stop 控制包。
|
||||
4. 默认新路径会在稠密轨迹队列末尾追加独立的 `LastData=1` 结束帧;结束后只监控状态,不发送 packet type `2`。旧保持路径仍可通过内部开关回退。
|
||||
|
||||
序号和节拍已经按手册方向校准;停止语义仍需在真实 R30iB 联调中继续确认。
|
||||
|
||||
@@ -220,13 +220,13 @@
|
||||
主要差异/风险:
|
||||
|
||||
1. 当前未实现命令缓冲预填,也未读取 `$PKT_STACK / $START_MOVE`。
|
||||
2. 当前停止运动依赖 packet type `2` stop 控制包,没有稳定发送 `LastData=1` 的最后 command packet;这与手册标准结束语义不同。
|
||||
2. 默认正常运动结束已经通过追加 `LastData=1` 结束帧靠近手册语义;packet type `2` 仍仅作为强制停止状态输出路径。
|
||||
3. 当前未实现 packet type `3` 的速度/加速度/jerk 限制表查询,也未实现 payload/speed 20 档动态限制算法。
|
||||
4. 当前没有自动校验 `AUTO / 100% OVERRIDE / brake control / resume offset / payload` 等控制器前置状态。
|
||||
5. 当前没有报警码读取和 `MOTN-* / PRIO-*` 映射。
|
||||
|
||||
建议后续联调优先级:
|
||||
|
||||
1. 验证运动结束是否必须补 `LastData=1`;如果当前 stop 控制包能稳定工作,也应在文档中标为现场兼容路径,而不是手册标准路径。
|
||||
1. 联调验证 `LastData=1` 后机器人是否稳定退出命令消费并继续输出状态包;如现场程序仍要求持续命令包,可通过内部开关回退旧保持路径。
|
||||
2. 抓一次报警现场包,确认 `MOTN-600/603/617` 等是否能从包序号与状态位直接定位。
|
||||
3. 如果后续追求更稳的真实机运行,补 packet type `3` 限制表查询,并把规划器的速度、加速度、jerk 校验与 FANUC 手册算法靠近。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "8.0.420",
|
||||
"rollForward": "disable"
|
||||
"version": "8.0.422",
|
||||
"rollForward": "latestPatch"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,11 @@ public sealed class ControllerClientCompatOptions
|
||||
/// </summary>
|
||||
public string? WorkspaceRoot { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置服务启动时自动初始化机器人的配置;为 null 时跳过自动初始化。
|
||||
/// </summary>
|
||||
public AutoInitOptions? AutoInit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 解析运行配置根目录,确保运行时默认不再依赖源码仓库位置。
|
||||
/// </summary>
|
||||
@@ -44,3 +49,19 @@ public sealed class ControllerClientCompatOptions
|
||||
: Path.GetFullPath(WorkspaceRoot);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示服务启动时自动初始化机器人的配置项。
|
||||
/// </summary>
|
||||
public sealed class AutoInitOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置机器人名称,用于加载机器人配置文件(如 LR_Mate_200iD)。
|
||||
/// </summary>
|
||||
public string RobotName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置机器人 IP 地址,用于连接真机或仿真控制器。
|
||||
/// </summary>
|
||||
public string RobotIp { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
@@ -802,6 +802,18 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
|
||||
sample.TrajectoryTime);
|
||||
}
|
||||
}
|
||||
if (_j519Client.EndCommandStreamAfterQueueDrained && commands.Count > 0)
|
||||
{
|
||||
// 正常结束命令流时追加独立结束帧,避免把最后一个真实轨迹采样点和结束语义混在一起。
|
||||
commands.Add(new FanucJ519Command(
|
||||
sequence: 0,
|
||||
targetJoints: commands[^1].TargetJoints,
|
||||
lastData: 1,
|
||||
writeIoType: 2,
|
||||
writeIoIndex: 1,
|
||||
writeIoMask: 0,
|
||||
writeIoValue: 0));
|
||||
}
|
||||
TryWriteDenseSendArtifacts(result, outputDir, sentJointRows, sentTimingRows, sentJerkRows, triggerBindings);
|
||||
|
||||
// 上层只负责生成完整目标序列,真实出队节拍交给 J519 状态包驱动。
|
||||
@@ -865,6 +877,19 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
|
||||
commands.Add(command);
|
||||
sentPoseRows.Add(BuildDenseSendPoseRow(row[0], pose));
|
||||
}
|
||||
if (_j519Client.EndCommandStreamAfterQueueDrained && commands.Count > 0)
|
||||
{
|
||||
// 直角轨迹结束帧沿用最终目标位姿,并明确清空 IO 输出。
|
||||
commands.Add(new FanucJ519Command(
|
||||
sequence: 0,
|
||||
targetValues: commands[^1].TargetValues,
|
||||
lastData: 1,
|
||||
dataStyle: 0,
|
||||
writeIoType: 2,
|
||||
writeIoIndex: 1,
|
||||
writeIoMask: 0,
|
||||
writeIoValue: 0));
|
||||
}
|
||||
|
||||
TryWriteDenseCartesianSendArtifacts(outputDir, sentPoseRows);
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Flyshot.ControllerClientCompat;
|
||||
using Flyshot.Server.Host;
|
||||
using Flyshot.Server.Host.Middleware;
|
||||
using Flyshot.Server.Host.Services;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using NLog.Web;
|
||||
@@ -23,6 +24,10 @@ try
|
||||
builder.Services.Configure<HostSwaggerOptions>(builder.Configuration.GetSection("Swagger"));
|
||||
builder.Services.Configure<HostCorsOptions>(builder.Configuration.GetSection("Cors"));
|
||||
builder.Services.AddControllerClientCompat(builder.Configuration);
|
||||
|
||||
// 注册服务启动时自动初始化机器人的托管服务;配置缺失时自动跳过。
|
||||
builder.Services.AddHostedService<AutoInitRobotHostedService>();
|
||||
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
using Flyshot.ControllerClientCompat;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Flyshot.Server.Host.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 服务启动时自动初始化机器人的托管服务。
|
||||
/// 从配置文件读取 robot_name 和 robot_ip,依次执行 SetUpRobot、SetActiveController、Connect、EnableRobot。
|
||||
/// 初始化失败不会阻止服务启动,仅记录警告日志。
|
||||
/// </summary>
|
||||
public sealed class AutoInitRobotHostedService : IHostedService
|
||||
{
|
||||
private readonly IControllerClientCompatService _compatService;
|
||||
private readonly ControllerClientCompatOptions _options;
|
||||
private readonly ILogger<AutoInitRobotHostedService> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化自动初始化机器人托管服务的实例。
|
||||
/// </summary>
|
||||
/// <param name="compatService">控制器兼容服务,用于执行机器人初始化操作。</param>
|
||||
/// <param name="options">控制器兼容层配置,包含自动初始化参数。</param>
|
||||
/// <param name="logger">日志记录器。</param>
|
||||
public AutoInitRobotHostedService(
|
||||
IControllerClientCompatService compatService,
|
||||
IOptions<ControllerClientCompatOptions> options,
|
||||
ILogger<AutoInitRobotHostedService> logger)
|
||||
{
|
||||
_compatService = compatService;
|
||||
_options = options.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 服务启动时执行自动初始化。若配置中未指定 AutoInit 或参数为空,则跳过。
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>异步任务。</returns>
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var autoInit = _options.AutoInit;
|
||||
|
||||
// 未配置 AutoInit 或参数为空时,跳过自动初始化
|
||||
if (autoInit == null
|
||||
|| string.IsNullOrWhiteSpace(autoInit.RobotName)
|
||||
|| string.IsNullOrWhiteSpace(autoInit.RobotIp))
|
||||
{
|
||||
_logger.LogInformation("未配置 AutoInit,跳过服务启动自动初始化。");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"开始自动初始化机器人: robot_name={RobotName}, robot_ip={RobotIp}",
|
||||
autoInit.RobotName, autoInit.RobotIp);
|
||||
|
||||
try
|
||||
{
|
||||
// 步骤1:加载机器人配置
|
||||
_compatService.SetUpRobot(autoInit.RobotName);
|
||||
if (!_compatService.IsSetUp)
|
||||
{
|
||||
_logger.LogWarning("自动初始化失败: SetUpRobot 返回 IsSetUp=false,robot_name={RobotName}", autoInit.RobotName);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// 步骤2:设置控制器模式(默认非仿真)
|
||||
_compatService.SetActiveController(sim: false);
|
||||
|
||||
// 步骤3:连接机器人控制器
|
||||
_compatService.Connect(autoInit.RobotIp);
|
||||
|
||||
// 步骤4:使能机器人(上伺服)
|
||||
_compatService.EnableRobot(8);
|
||||
|
||||
_logger.LogInformation("自动初始化成功: robot_name={RobotName}", autoInit.RobotName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 初始化失败不阻止服务启动,仅记录警告
|
||||
_logger.LogWarning(ex, "自动初始化机器人失败,服务将继续启动。robot_name={RobotName}", autoInit.RobotName);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 服务停止时的清理操作。当前无需额外清理。
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>异步任务。</returns>
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,11 @@
|
||||
}
|
||||
},
|
||||
"ControllerClientCompat": {
|
||||
"ServerVersion": "flyshot-replacement-controller-client-compat/0.1.0"
|
||||
"ServerVersion": "flyshot-replacement-controller-client-compat/0.1.0",
|
||||
"AutoInit": {
|
||||
"RobotName": "LR_Mate_200iD",
|
||||
"RobotIp": "127.0.0.1"
|
||||
}
|
||||
},
|
||||
"Swagger": {
|
||||
"Enabled": true,
|
||||
|
||||
@@ -60,15 +60,20 @@ public sealed class FanucControllerRuntimeDenseTests
|
||||
WaitUntilIdle(runtime);
|
||||
|
||||
var commands = j519Client.GetCommandHistoryForTests();
|
||||
Assert.Equal(3, commands.Count);
|
||||
Assert.Equal(4, commands.Count);
|
||||
Assert.All(commands, static command => Assert.Equal(0u, command.Sequence));
|
||||
var expectedJ1Targets = new[] { 0.0, 0.08, 0.16 };
|
||||
var actualJ1Targets = commands.Select(static command => command.TargetJoints[0]).ToArray();
|
||||
var actualJ1Targets = commands.Take(3).Select(static command => command.TargetJoints[0]).ToArray();
|
||||
Assert.Equal(expectedJ1Targets.Length, actualJ1Targets.Length);
|
||||
for (var index = 0; index < expectedJ1Targets.Length; index++)
|
||||
{
|
||||
Assert.Equal(expectedJ1Targets[index], actualJ1Targets[index], precision: 6);
|
||||
}
|
||||
Assert.All(commands.Take(3), static command => Assert.Equal(0, command.LastData));
|
||||
Assert.Equal(1, commands[^1].LastData);
|
||||
Assert.Equal(0, commands[^1].WriteIoMask);
|
||||
Assert.Equal(0, commands[^1].WriteIoValue);
|
||||
Assert.Equal(0.16, commands[^1].TargetJoints[0], precision: 6);
|
||||
Assert.False(j519Client.IsCommandQueueDrainedForTests());
|
||||
}
|
||||
|
||||
@@ -114,12 +119,12 @@ public sealed class FanucControllerRuntimeDenseTests
|
||||
denseJointTrajectory: denseTrajectory);
|
||||
|
||||
var executeTask = Task.Run(() => runtime.ExecuteTrajectory(result, [DegreesToRadians(0.16), 0.0, 0.0, 0.0, 0.0, 0.0]), cts.Token);
|
||||
await WaitUntilAsync(() => j519Client.GetCommandHistoryForTests().Count == 3, cts.Token);
|
||||
await WaitUntilAsync(() => j519Client.GetCommandHistoryForTests().Count == 4, cts.Token);
|
||||
|
||||
// 只有机器人状态包把队列全部取出后,ExecuteTrajectory 才能向上层返回。
|
||||
Assert.False(executeTask.IsCompleted);
|
||||
|
||||
for (uint sequence = 100; sequence < 103; sequence++)
|
||||
for (uint sequence = 100; sequence < 104; sequence++)
|
||||
{
|
||||
await SendStatusPacketAsync(server, initResult.RemoteEndPoint, sequence, cts.Token);
|
||||
_ = await server.ReceiveAsync(cts.Token);
|
||||
@@ -190,9 +195,9 @@ public sealed class FanucControllerRuntimeDenseTests
|
||||
var timingLines = File.ReadAllLines(timingPath);
|
||||
var jerkLines = File.ReadAllLines(jerkPath);
|
||||
var shotEventsJson = File.ReadAllText(shotEventsPath);
|
||||
Assert.Equal(commands.Count, pointLines.Length);
|
||||
Assert.Equal(commands.Count, timingLines.Length);
|
||||
Assert.Equal(Math.Max(0, commands.Count - 1), jerkLines.Length);
|
||||
Assert.Equal(denseTrajectory.Length, pointLines.Length);
|
||||
Assert.Equal(denseTrajectory.Length, timingLines.Length);
|
||||
Assert.Equal(Math.Max(0, denseTrajectory.Length - 1), jerkLines.Length);
|
||||
|
||||
var firstColumns = ParseColumns(pointLines[0]);
|
||||
var secondColumns = ParseColumns(pointLines[1]);
|
||||
@@ -216,7 +221,7 @@ public sealed class FanucControllerRuntimeDenseTests
|
||||
Assert.Equal(1.0, secondTimingColumns[0], precision: 6);
|
||||
Assert.Equal(0.008, secondTimingColumns[1], precision: 6);
|
||||
Assert.Equal(0.008, secondTimingColumns[2], precision: 6);
|
||||
Assert.Equal(commands.Count - 1, lastTimingColumns[0], precision: 6);
|
||||
Assert.Equal(denseTrajectory.Length - 1, lastTimingColumns[0], precision: 6);
|
||||
Assert.Equal(0.016, lastTimingColumns[2], precision: 6);
|
||||
|
||||
var firstJerkColumns = ParseColumns(jerkLines[0]);
|
||||
@@ -395,10 +400,11 @@ public sealed class FanucControllerRuntimeDenseTests
|
||||
|
||||
var commands = j519Client.GetCommandHistoryForTests();
|
||||
Assert.NotEmpty(commands);
|
||||
Assert.Equal(preparedExecution.Samples.Count, commands.Count);
|
||||
Assert.Equal(speedRatio, preparedExecution.FinalSpeedRatio, precision: 6);
|
||||
Assert.Equal(preparedExecution.Samples.Count + 1, commands.Count);
|
||||
Assert.Equal(speedRatio, preparedExecution.RequestSpeedRatio, precision: 6);
|
||||
Assert.Equal(0, preparedExecution.StretchIterationCount);
|
||||
Assert.Equal(preparedExecution.Samples[0].JointsDegrees, commands[0].TargetJoints);
|
||||
Assert.Equal(1, commands[^1].LastData);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -469,8 +475,9 @@ public sealed class FanucControllerRuntimeDenseTests
|
||||
WaitUntilIdle(runtime);
|
||||
|
||||
var commands = j519Client.GetCommandHistoryForTests();
|
||||
Assert.Equal(preparedSamples.Length, commands.Count);
|
||||
Assert.Equal([1.0, 2.0, 3.0], commands.Select(static command => command.TargetJoints[0]));
|
||||
Assert.Equal(preparedSamples.Length + 1, commands.Count);
|
||||
Assert.Equal([1.0, 2.0, 3.0], commands.Take(preparedSamples.Length).Select(static command => command.TargetJoints[0]));
|
||||
Assert.Equal(1, commands[^1].LastData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -523,17 +530,57 @@ public sealed class FanucControllerRuntimeDenseTests
|
||||
WaitUntilIdle(runtime);
|
||||
|
||||
var commands = j519Client.GetCommandHistoryForTests();
|
||||
Assert.Equal(expectedResult.DenseJointTrajectory!.Count, commands.Count);
|
||||
Assert.Equal(expectedResult.DenseJointTrajectory!.Count + 1, commands.Count);
|
||||
AssertJointDegreesEqual(startJoints, commands[0].TargetJoints);
|
||||
AssertJointDegreesEqual(targetJoints, commands[^1].TargetJoints);
|
||||
Assert.Equal(1, commands[^1].LastData);
|
||||
Assert.All(commands.Take(commands.Count - 1), static command => Assert.Equal(0, command.LastData));
|
||||
|
||||
var middleAlpha = ComputeLineAlpha(commands[commands.Count / 2].TargetJoints, startJoints, targetJoints);
|
||||
var middleAlpha = ComputeLineAlpha(commands[(commands.Count - 1) / 2].TargetJoints, startJoints, targetJoints);
|
||||
Assert.InRange(middleAlpha, 0.45, 0.55);
|
||||
|
||||
var earlyAlpha = ComputeLineAlpha(commands[Math.Min(5, commands.Count - 1)].TargetJoints, startJoints, targetJoints);
|
||||
Assert.InRange(earlyAlpha, 0.0, 0.02);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证关闭命令流结束开关时,MoveJoint 保持旧队列长度和最终目标保持语义。
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MoveJoint_RealMode_WithEndCommandStreamDisabled_KeepsLegacyQueueShape()
|
||||
{
|
||||
using var commandClient = new FanucCommandClient();
|
||||
using var stateClient = new FanucStateClient();
|
||||
using var j519Client = new FanucJ519Client(endCommandStreamAfterQueueDrained: false);
|
||||
using var runtime = new FanucControllerRuntime(commandClient, stateClient, j519Client);
|
||||
var service = CreateCompatService(runtime);
|
||||
var startJoints = new[] { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 };
|
||||
var targetJoints = new[] { 0.2, -0.1, 0.05, 0.0, 0.0, 0.1 };
|
||||
|
||||
service.SetUpRobot("FANUC_LR_Mate_200iD");
|
||||
j519Client.EnableCommandHistoryForTests();
|
||||
ForceRealModeEnabled(runtime, speedRatio: 1.0);
|
||||
SetPrivateField(runtime, "_jointPositions", startJoints);
|
||||
|
||||
var options = new ControllerClientCompatOptions
|
||||
{
|
||||
ConfigRoot = TestRobotFactory.GetConfigRoot()
|
||||
};
|
||||
var robot = new ControllerClientCompatRobotCatalog(options, new RobotModelLoader())
|
||||
.LoadProfile("FANUC_LR_Mate_200iD", accLimitScale: 1.0, jerkLimitScale: 1.0);
|
||||
var expectedResult = MoveJointTrajectoryGenerator.CreateResult(robot, startJoints, targetJoints, speedRatio: 1.0);
|
||||
|
||||
service.MoveJoint(targetJoints);
|
||||
WaitUntilIdle(runtime);
|
||||
|
||||
var commands = j519Client.GetCommandHistoryForTests();
|
||||
Assert.Equal(expectedResult.DenseJointTrajectory!.Count, commands.Count);
|
||||
Assert.All(commands, static command => Assert.Equal(0, command.LastData));
|
||||
var currentCommand = j519Client.GetCurrentCommand();
|
||||
Assert.NotNull(currentCommand);
|
||||
AssertJointDegreesEqual(targetJoints, currentCommand.TargetJoints);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MoveJointTrajectoryGenerator_LowerSpeedUsesFixedServoPeriodAndLongerPlannedDuration()
|
||||
{
|
||||
@@ -629,6 +676,10 @@ public sealed class FanucControllerRuntimeDenseTests
|
||||
Assert.All(commands, static command => Assert.Equal(0, command.DataStyle));
|
||||
AssertPoseEqual(startPose, commands[0].TargetValues.Take(6).ToArray());
|
||||
AssertPoseEqual(targetPose, commands[^1].TargetValues.Take(6).ToArray());
|
||||
Assert.All(commands.Take(commands.Count - 1), static command => Assert.Equal(0, command.LastData));
|
||||
Assert.Equal(1, commands[^1].LastData);
|
||||
Assert.Equal(0, commands[^1].WriteIoMask);
|
||||
Assert.Equal(0, commands[^1].WriteIoValue);
|
||||
Assert.All(commands, static command =>
|
||||
{
|
||||
Assert.Equal(0.0, command.TargetValues[6], precision: 6);
|
||||
@@ -677,8 +728,9 @@ public sealed class FanucControllerRuntimeDenseTests
|
||||
WaitUntilIdle(runtime);
|
||||
|
||||
var commands = j519Client.GetCommandHistoryForTests();
|
||||
Assert.Equal(3, commands.Count);
|
||||
Assert.Equal(4, commands.Count);
|
||||
Assert.Equal(0.16, commands[^1].TargetJoints[0], precision: 6);
|
||||
Assert.Equal(1, commands[^1].LastData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -782,9 +834,12 @@ public sealed class FanucControllerRuntimeDenseTests
|
||||
WaitUntilIdle(runtime);
|
||||
|
||||
var commands = j519Client.GetCommandHistoryForTests();
|
||||
Assert.Equal(4, commands.Count);
|
||||
Assert.Equal([(ushort)0, (ushort)5, (ushort)5, (ushort)5], commands.Select(static command => command.WriteIoMask));
|
||||
Assert.Equal([(ushort)0, (ushort)5, (ushort)5, (ushort)0], commands.Select(static command => command.WriteIoValue));
|
||||
Assert.Equal(5, commands.Count);
|
||||
Assert.Equal([(ushort)0, (ushort)5, (ushort)5, (ushort)5], commands.Take(4).Select(static command => command.WriteIoMask));
|
||||
Assert.Equal([(ushort)0, (ushort)5, (ushort)5, (ushort)0], commands.Take(4).Select(static command => command.WriteIoValue));
|
||||
Assert.Equal(1, commands[^1].LastData);
|
||||
Assert.Equal(0, commands[^1].WriteIoMask);
|
||||
Assert.Equal(0, commands[^1].WriteIoValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -837,9 +892,12 @@ public sealed class FanucControllerRuntimeDenseTests
|
||||
WaitUntilIdle(runtime);
|
||||
|
||||
var commands = j519Client.GetCommandHistoryForTests();
|
||||
Assert.Equal(4, commands.Count);
|
||||
Assert.Equal([(ushort)0, (ushort)0, (ushort)1, (ushort)1], commands.Select(static command => command.WriteIoMask));
|
||||
Assert.Equal([(ushort)0, (ushort)0, (ushort)1, (ushort)0], commands.Select(static command => command.WriteIoValue));
|
||||
Assert.Equal(5, commands.Count);
|
||||
Assert.Equal([(ushort)0, (ushort)0, (ushort)1, (ushort)1], commands.Take(4).Select(static command => command.WriteIoMask));
|
||||
Assert.Equal([(ushort)0, (ushort)0, (ushort)1, (ushort)0], commands.Take(4).Select(static command => command.WriteIoValue));
|
||||
Assert.Equal(1, commands[^1].LastData);
|
||||
Assert.Equal(0, commands[^1].WriteIoMask);
|
||||
Assert.Equal(0, commands[^1].WriteIoValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user