✨ feat(fanuc): 添加直角坐标点动功能与相关接口
* 新增 `MovePose` 方法,支持以直角坐标执行点到点移动。 * 引入 `LegacyCartesianPoseRequest` 类,处理直角位姿请求体的解析与验证。 * 更新 `LegacyHttpApiController`,实现 `/move_pose/` 路由以支持新功能。 * 增强状态快照元数据,提供机器人初始化状态与已上传轨迹信息。 * 更新前端状态页面,增加直角坐标点动控制面板与步长设置选项。 * 相关文档与测试用例同步更新,确保新功能的完整性与稳定性。
This commit is contained in:
@@ -956,6 +956,111 @@ public sealed class RuntimeOrchestrationTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证飞拍执行阻塞在运行时时,状态页元数据快照仍能通过短锁快速返回。
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ControllerClientCompatService_GetStatusSnapshotMetadata_DoesNotWaitForRunningFlyshot()
|
||||
{
|
||||
var configRoot = CreateTempConfigRoot();
|
||||
try
|
||||
{
|
||||
WriteRobotConfigWithDemoTrajectory(configRoot);
|
||||
var options = new ControllerClientCompatOptions
|
||||
{
|
||||
ConfigRoot = configRoot
|
||||
};
|
||||
var runtime = new BlockingExecutionControllerRuntime([0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
|
||||
var service = new ControllerClientCompatService(
|
||||
options,
|
||||
new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
|
||||
runtime,
|
||||
new ControllerClientTrajectoryOrchestrator(),
|
||||
new RobotConfigLoader());
|
||||
|
||||
service.SetUpRobot("FANUC_LR_Mate_200iD");
|
||||
service.SetActiveController(sim: false);
|
||||
service.Connect("192.168.10.101");
|
||||
service.EnableRobot(2);
|
||||
service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot());
|
||||
|
||||
var executing = Task.Run(() => service.ExecuteTrajectoryByName(
|
||||
"demo-flyshot",
|
||||
new FlyshotExecutionOptions(moveToStart: false, method: "icsp", saveTrajectory: false, useCache: false, wait: true)));
|
||||
Assert.True(runtime.WaitForExecutionStarted(TimeSpan.FromSeconds(2)));
|
||||
|
||||
var metadataTask = Task.Run(() => service.GetStatusSnapshotMetadata());
|
||||
var completed = await Task.WhenAny(metadataTask, Task.Delay(TimeSpan.FromMilliseconds(150)));
|
||||
|
||||
runtime.ReleaseExecution();
|
||||
await executing;
|
||||
|
||||
Assert.Same(metadataTask, completed);
|
||||
var metadata = await metadataTask;
|
||||
Assert.True(metadata.IsSetup);
|
||||
Assert.Equal("FANUC_LR_Mate_200iD", metadata.RobotName);
|
||||
Assert.Equal(["demo-flyshot"], metadata.UploadedTrajectories);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(configRoot, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证两个飞拍执行命令必须串行进入 runtime,避免 J519 队列被并发执行覆盖。
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ControllerClientCompatService_ExecuteTrajectoryByName_SerializesConcurrentExecutionCommands()
|
||||
{
|
||||
var configRoot = CreateTempConfigRoot();
|
||||
try
|
||||
{
|
||||
WriteRobotConfigWithDemoTrajectory(configRoot);
|
||||
var options = new ControllerClientCompatOptions
|
||||
{
|
||||
ConfigRoot = configRoot
|
||||
};
|
||||
var runtime = new BlockingExecutionControllerRuntime([0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
|
||||
var service = new ControllerClientCompatService(
|
||||
options,
|
||||
new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
|
||||
runtime,
|
||||
new ControllerClientTrajectoryOrchestrator(),
|
||||
new RobotConfigLoader());
|
||||
|
||||
service.SetUpRobot("FANUC_LR_Mate_200iD");
|
||||
service.SetActiveController(sim: false);
|
||||
service.Connect("192.168.10.101");
|
||||
service.EnableRobot(2);
|
||||
service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot());
|
||||
|
||||
var first = Task.Run(() => service.ExecuteTrajectoryByName(
|
||||
"demo-flyshot",
|
||||
new FlyshotExecutionOptions(moveToStart: false, method: "icsp", saveTrajectory: false, useCache: false, wait: true)));
|
||||
Assert.True(runtime.WaitForExecutionStarted(TimeSpan.FromSeconds(2)));
|
||||
|
||||
var second = Task.Run(() => service.ExecuteTrajectoryByName(
|
||||
"demo-flyshot",
|
||||
new FlyshotExecutionOptions(moveToStart: false, method: "icsp", saveTrajectory: false, useCache: false, wait: true)));
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(100));
|
||||
|
||||
Assert.Equal(1, runtime.ExecuteCallCount);
|
||||
|
||||
runtime.ReleaseExecution();
|
||||
await first;
|
||||
Assert.True(runtime.WaitForExecutionStarted(TimeSpan.FromSeconds(2), expectedCallCount: 2));
|
||||
runtime.ReleaseExecution();
|
||||
await second;
|
||||
|
||||
Assert.Equal(2, runtime.ExecuteCallCount);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(configRoot, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证飞拍链路在进入运行时前就会准备最终发送队列,而不是把 speedRatio 重采样留给运行时临场处理。
|
||||
/// </summary>
|
||||
@@ -2005,6 +2110,12 @@ internal sealed class RecordingControllerRuntime : IControllerRuntime
|
||||
{
|
||||
LastExecutedResult = result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ExecuteCartesianTrajectory(TrajectoryResult result, IReadOnlyList<double> finalPose)
|
||||
{
|
||||
LastExecutedResult = result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -2166,6 +2277,182 @@ internal sealed class DelayedCompletionControllerRuntime : IControllerRuntime
|
||||
_jointPositions = finalJointPositions.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ExecuteCartesianTrajectory(TrajectoryResult result, IReadOnlyList<double> finalPose)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟 runtime 执行入口长期占用的测试运行时,用于验证兼容层锁边界。
|
||||
/// </summary>
|
||||
internal sealed class BlockingExecutionControllerRuntime : IControllerRuntime
|
||||
{
|
||||
private readonly object _lock = new();
|
||||
private readonly ManualResetEventSlim _executionStarted = new(false);
|
||||
private readonly ManualResetEventSlim _releaseExecution = new(false);
|
||||
private readonly double[] _jointPositions;
|
||||
private bool _isEnabled;
|
||||
private bool _isInMotion;
|
||||
private int _executeCallCount;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化一份会阻塞 ExecuteTrajectory 的测试运行时。
|
||||
/// </summary>
|
||||
/// <param name="initialJointPositions">初始关节反馈。</param>
|
||||
public BlockingExecutionControllerRuntime(IReadOnlyList<double> initialJointPositions)
|
||||
{
|
||||
_jointPositions = initialJointPositions.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 runtime 执行入口被调用的次数。
|
||||
/// </summary>
|
||||
public int ExecuteCallCount
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _executeCallCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 等待指定序号的执行调用进入 runtime。
|
||||
/// </summary>
|
||||
/// <param name="timeout">最长等待时间。</param>
|
||||
/// <param name="expectedCallCount">期望已经进入的执行次数。</param>
|
||||
/// <returns>是否在超时前等到。</returns>
|
||||
public bool WaitForExecutionStarted(TimeSpan timeout, int expectedCallCount = 1)
|
||||
{
|
||||
var deadline = DateTimeOffset.UtcNow.Add(timeout);
|
||||
while (DateTimeOffset.UtcNow < deadline)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_executeCallCount >= expectedCallCount)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
_executionStarted.Wait(TimeSpan.FromMilliseconds(10));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放当前阻塞的执行调用。
|
||||
/// </summary>
|
||||
public void ReleaseExecution()
|
||||
{
|
||||
_releaseExecution.Set();
|
||||
}
|
||||
|
||||
public void ResetRobot(RobotProfile robot, string robotName)
|
||||
{
|
||||
}
|
||||
|
||||
public void SetActiveController(bool sim)
|
||||
{
|
||||
}
|
||||
|
||||
public void Connect(string robotIp)
|
||||
{
|
||||
}
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
}
|
||||
|
||||
public void EnableRobot(int bufferSize)
|
||||
{
|
||||
_isEnabled = true;
|
||||
}
|
||||
|
||||
public void DisableRobot()
|
||||
{
|
||||
_isEnabled = false;
|
||||
}
|
||||
|
||||
public void StopMove()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_isInMotion = false;
|
||||
}
|
||||
|
||||
ReleaseExecution();
|
||||
}
|
||||
|
||||
public double GetSpeedRatio() => 1.0;
|
||||
|
||||
public void SetSpeedRatio(double ratio)
|
||||
{
|
||||
}
|
||||
|
||||
public IReadOnlyList<double> GetTcp() => [0.0, 0.0, 0.0];
|
||||
|
||||
public void SetTcp(double x, double y, double z)
|
||||
{
|
||||
}
|
||||
|
||||
public bool GetIo(int port, string ioType) => false;
|
||||
|
||||
public void SetIo(int port, bool value, string ioType)
|
||||
{
|
||||
}
|
||||
|
||||
public IReadOnlyList<double> GetJointPositions()
|
||||
{
|
||||
return _jointPositions.ToArray();
|
||||
}
|
||||
|
||||
public IReadOnlyList<double> GetPose() => [0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
|
||||
|
||||
public ControllerStateSnapshot GetSnapshot()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return new ControllerStateSnapshot(
|
||||
capturedAt: DateTimeOffset.UtcNow,
|
||||
connectionState: "Connected",
|
||||
isEnabled: _isEnabled,
|
||||
isInMotion: _isInMotion,
|
||||
speedRatio: 1.0,
|
||||
jointPositions: _jointPositions.ToArray(),
|
||||
cartesianPose: Array.Empty<double>(),
|
||||
activeAlarms: Array.Empty<RuntimeAlarm>());
|
||||
}
|
||||
}
|
||||
|
||||
public void ExecuteTrajectory(TrajectoryResult result, IReadOnlyList<double> finalJointPositions)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_executeCallCount++;
|
||||
_isInMotion = true;
|
||||
_executionStarted.Set();
|
||||
_releaseExecution.Reset();
|
||||
}
|
||||
|
||||
_releaseExecution.Wait();
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_isInMotion = false;
|
||||
_executionStarted.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
public void ExecuteCartesianTrajectory(TrajectoryResult result, IReadOnlyList<double> finalPose)
|
||||
{
|
||||
ExecuteTrajectory(result, _jointPositions);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -2292,4 +2579,8 @@ internal sealed class StickyFeedbackControllerRuntime : IControllerRuntime
|
||||
_jointPositions = finalJointPositions.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public void ExecuteCartesianTrajectory(TrajectoryResult result, IReadOnlyList<double> finalPose)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user