🐛 fix(fanuc): 修复 J519 队列等待与日志注入
* 为 FANUC 命令、状态和 J519 客户端注入独立日志记录器 * 补充不可接收命令状态下队列完成等待的回归测试 * 调整 J519 命令流结束后的状态处理语义
This commit is contained in:
@@ -39,23 +39,49 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
|
|||||||
/// 初始化 FANUC 控制器运行时。
|
/// 初始化 FANUC 控制器运行时。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="logger">运行时日志记录器。</param>
|
/// <param name="logger">运行时日志记录器。</param>
|
||||||
|
/// <param name="commandLogger">FANUC 10012 命令通道客户端日志记录器。</param>
|
||||||
|
/// <param name="stateLogger">FANUC 10010 状态通道客户端日志记录器。</param>
|
||||||
/// <param name="j519Logger">J519 客户端日志记录器。</param>
|
/// <param name="j519Logger">J519 客户端日志记录器。</param>
|
||||||
public FanucControllerRuntime(ILogger<FanucControllerRuntime> logger, ILogger<FanucJ519Client> j519Logger)
|
public FanucControllerRuntime(
|
||||||
|
ILogger<FanucControllerRuntime> logger,
|
||||||
|
ILogger<FanucCommandClient> commandLogger,
|
||||||
|
ILogger<FanucStateClient> stateLogger,
|
||||||
|
ILogger<FanucJ519Client> j519Logger)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(logger);
|
ArgumentNullException.ThrowIfNull(logger);
|
||||||
|
ArgumentNullException.ThrowIfNull(commandLogger);
|
||||||
|
ArgumentNullException.ThrowIfNull(stateLogger);
|
||||||
ArgumentNullException.ThrowIfNull(j519Logger);
|
ArgumentNullException.ThrowIfNull(j519Logger);
|
||||||
|
|
||||||
_commandClient = new FanucCommandClient();
|
_commandClient = new FanucCommandClient(commandLogger);
|
||||||
_stateClient = new FanucStateClient();
|
_stateClient = new FanucStateClient(new FanucStateClientOptions(), stateLogger);
|
||||||
_j519Client = new FanucJ519Client(j519Logger);
|
_j519Client = new FanucJ519Client(j519Logger);
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化 FANUC 控制器运行时,并仅显式传入运行时与 J519 日志记录器。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">运行时日志记录器。</param>
|
||||||
|
/// <param name="j519Logger">J519 客户端日志记录器。</param>
|
||||||
|
public FanucControllerRuntime(ILogger<FanucControllerRuntime> logger, ILogger<FanucJ519Client> j519Logger)
|
||||||
|
: this(
|
||||||
|
logger,
|
||||||
|
NullLogger<FanucCommandClient>.Instance,
|
||||||
|
NullLogger<FanucStateClient>.Instance,
|
||||||
|
j519Logger)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 初始化一份无日志的 FANUC 控制器运行时,供离线测试或手工构造场景使用。
|
/// 初始化一份无日志的 FANUC 控制器运行时,供离线测试或手工构造场景使用。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public FanucControllerRuntime()
|
public FanucControllerRuntime()
|
||||||
: this(NullLogger<FanucControllerRuntime>.Instance, NullLogger<FanucJ519Client>.Instance)
|
: this(
|
||||||
|
NullLogger<FanucControllerRuntime>.Instance,
|
||||||
|
NullLogger<FanucCommandClient>.Instance,
|
||||||
|
NullLogger<FanucStateClient>.Instance,
|
||||||
|
NullLogger<FanucJ519Client>.Instance)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -737,18 +763,20 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
|
|||||||
sample.TrajectoryTime);
|
sample.TrajectoryTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (_j519Client.EndCommandStreamAfterQueueDrained && commands.Count > 0)
|
//TODO 不进行结束,一致发送最后一帧
|
||||||
{
|
|
||||||
// 正常结束命令流时追加独立结束帧,避免把最后一个真实轨迹采样点和结束语义混在一起。
|
// if (_j519Client.EndCommandStreamAfterQueueDrained && commands.Count > 0)
|
||||||
commands.Add(new FanucJ519Command(
|
// {
|
||||||
sequence: 0,
|
// // 正常结束命令流时追加独立结束帧,避免把最后一个真实轨迹采样点和结束语义混在一起。
|
||||||
targetJoints: commands[^1].TargetJoints,
|
// commands.Add(new FanucJ519Command(
|
||||||
lastData: 1,
|
// sequence: 0,
|
||||||
writeIoType: 2,
|
// targetJoints: commands[^1].TargetJoints,
|
||||||
writeIoIndex: 1,
|
// lastData: 1,
|
||||||
writeIoMask: 0,
|
// writeIoType: 2,
|
||||||
writeIoValue: 0));
|
// writeIoIndex: 1,
|
||||||
}
|
// writeIoMask: 0,
|
||||||
|
// writeIoValue: 0));
|
||||||
|
// }
|
||||||
TryWriteDenseSendArtifacts(result, outputDir, sentJointRows, sentTimingRows, sentJerkRows, triggerBindings);
|
TryWriteDenseSendArtifacts(result, outputDir, sentJointRows, sentTimingRows, sentJerkRows, triggerBindings);
|
||||||
|
|
||||||
// 上层只负责生成完整目标序列,真实出队节拍交给 J519 状态包驱动。
|
// 上层只负责生成完整目标序列,真实出队节拍交给 J519 状态包驱动。
|
||||||
|
|||||||
@@ -609,8 +609,8 @@ public sealed class FanucJ519Client : IDisposable
|
|||||||
if (_endCommandStreamAfterQueueDrained && command.LastData == 1)
|
if (_endCommandStreamAfterQueueDrained && command.LastData == 1)
|
||||||
{
|
{
|
||||||
// LastData 是命令流的正常结束语义;结束后保留状态接收,但不继续回发保姿包。
|
// LastData 是命令流的正常结束语义;结束后保留状态接收,但不继续回发保姿包。
|
||||||
_monitorOnlyAfterLastData = true;
|
// _monitorOnlyAfterLastData = true;
|
||||||
_motionStarted = false;
|
// _motionStarted = false;
|
||||||
CompleteCommandQueueLocked();
|
CompleteCommandQueueLocked();
|
||||||
}
|
}
|
||||||
else if (willDrainQueue && _commandQueue is { Count: 0 })
|
else if (willDrainQueue && _commandQueue is { Count: 0 })
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Flyshot.Runtime.Common;
|
using Flyshot.Runtime.Common;
|
||||||
using Flyshot.Runtime.Fanuc;
|
using Flyshot.Runtime.Fanuc;
|
||||||
|
using Flyshot.Runtime.Fanuc.Protocol;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
@@ -14,7 +15,7 @@ public sealed class FanucControllerRuntimeLoggingRegistrationTests
|
|||||||
/// 验证宿主按 IControllerRuntime 解析 FANUC 运行时时,不会因为可选日志参数而退回到无日志实例。
|
/// 验证宿主按 IControllerRuntime 解析 FANUC 运行时时,不会因为可选日志参数而退回到无日志实例。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ServiceProvider_Resolves_FanucControllerRuntime_WithLogger()
|
public void ServiceProvider_Resolves_FanucControllerRuntime_WithAllClientLoggers()
|
||||||
{
|
{
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
services.AddLogging();
|
services.AddLogging();
|
||||||
@@ -28,5 +29,27 @@ public sealed class FanucControllerRuntimeLoggingRegistrationTests
|
|||||||
|
|
||||||
Assert.NotNull(loggerField);
|
Assert.NotNull(loggerField);
|
||||||
Assert.NotNull(loggerField!.GetValue(concreteRuntime));
|
Assert.NotNull(loggerField!.GetValue(concreteRuntime));
|
||||||
|
|
||||||
|
AssertClientLoggerIsInjected<FanucCommandClient>(concreteRuntime, "_commandClient");
|
||||||
|
AssertClientLoggerIsInjected<FanucStateClient>(concreteRuntime, "_stateClient");
|
||||||
|
AssertClientLoggerIsInjected<FanucJ519Client>(concreteRuntime, "_j519Client");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 断言运行时持有的底层协议客户端已注入对应类型的日志记录器。
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TClient">待检查的协议客户端类型。</typeparam>
|
||||||
|
/// <param name="runtime">已经通过依赖注入解析出的 FANUC 运行时。</param>
|
||||||
|
/// <param name="clientFieldName">运行时内部保存该客户端实例的字段名。</param>
|
||||||
|
private static void AssertClientLoggerIsInjected<TClient>(FanucControllerRuntime runtime, string clientFieldName)
|
||||||
|
{
|
||||||
|
var clientField = typeof(FanucControllerRuntime).GetField(clientFieldName, BindingFlags.Instance | BindingFlags.NonPublic);
|
||||||
|
|
||||||
|
Assert.NotNull(clientField);
|
||||||
|
var client = Assert.IsType<TClient>(clientField!.GetValue(runtime));
|
||||||
|
var loggerField = typeof(TClient).GetField("_logger", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||||
|
|
||||||
|
Assert.NotNull(loggerField);
|
||||||
|
Assert.NotNull(loggerField!.GetValue(client));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -279,6 +279,34 @@ public sealed class FanucJ519ClientTests : IDisposable
|
|||||||
Assert.True(client.IsCommandQueueDrainedForTests());
|
Assert.True(client.IsCommandQueueDrainedForTests());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证机器人持续不可接收命令时,后续装载的命令队列会被完成,避免执行请求永久等待。
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task LoadCommandQueue_WhenLatestStatusDoesNotAcceptCommands_CompletesQueueWait()
|
||||||
|
{
|
||||||
|
using var client = new FanucJ519Client();
|
||||||
|
await client.ConnectAsync("127.0.0.1", Port, _cts.Token);
|
||||||
|
var initResult = await _server.ReceiveAsync(_cts.Token);
|
||||||
|
|
||||||
|
client.StartMotion();
|
||||||
|
await SendStatusPacketAsync(initResult.RemoteEndPoint, sequence: 900, status: 0x04);
|
||||||
|
await Task.Delay(100, _cts.Token);
|
||||||
|
|
||||||
|
client.LoadCommandQueue(
|
||||||
|
[
|
||||||
|
new FanucJ519Command(sequence: 0, targetJoints: [1.0, 0.0, 0.0, 0.0, 0.0, 0.0])
|
||||||
|
]);
|
||||||
|
await SendStatusPacketAsync(initResult.RemoteEndPoint, sequence: 901, status: 0x04);
|
||||||
|
|
||||||
|
await client.WaitForCommandQueueDrainedAsync(_cts.Token).WaitAsync(TimeSpan.FromSeconds(1), _cts.Token);
|
||||||
|
await Assert.ThrowsAsync<TimeoutException>(
|
||||||
|
() => _server.ReceiveAsync(_cts.Token).AsTask().WaitAsync(TimeSpan.FromMilliseconds(120)));
|
||||||
|
|
||||||
|
Assert.True(client.IsCommandQueueDrainedForTests());
|
||||||
|
await client.StopMotionAsync(_cts.Token);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 验证启用命令流结束开关后,LastData 结束帧发送完成会进入仅监控状态,不再继续回发保姿命令。
|
/// 验证启用命令流结束开关后,LastData 结束帧发送完成会进入仅监控状态,不再继续回发保姿命令。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -572,13 +600,14 @@ public sealed class FanucJ519ClientTests : IDisposable
|
|||||||
private async Task SendStatusPacketAsync(
|
private async Task SendStatusPacketAsync(
|
||||||
IPEndPoint clientEndpoint,
|
IPEndPoint clientEndpoint,
|
||||||
uint sequence,
|
uint sequence,
|
||||||
IReadOnlyList<double>? jointDegrees = null)
|
IReadOnlyList<double>? jointDegrees = null,
|
||||||
|
byte status = 15)
|
||||||
{
|
{
|
||||||
var responsePacket = new byte[FanucJ519Protocol.ResponsePacketLength];
|
var responsePacket = new byte[FanucJ519Protocol.ResponsePacketLength];
|
||||||
BinaryPrimitives.WriteUInt32BigEndian(responsePacket.AsSpan(0x00, 4), 0);
|
BinaryPrimitives.WriteUInt32BigEndian(responsePacket.AsSpan(0x00, 4), 0);
|
||||||
BinaryPrimitives.WriteUInt32BigEndian(responsePacket.AsSpan(0x04, 4), 1);
|
BinaryPrimitives.WriteUInt32BigEndian(responsePacket.AsSpan(0x04, 4), 1);
|
||||||
BinaryPrimitives.WriteUInt32BigEndian(responsePacket.AsSpan(0x08, 4), sequence);
|
BinaryPrimitives.WriteUInt32BigEndian(responsePacket.AsSpan(0x08, 4), sequence);
|
||||||
responsePacket[0x0c] = 15;
|
responsePacket[0x0c] = status;
|
||||||
|
|
||||||
if (jointDegrees is not null)
|
if (jointDegrees is not null)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user