♻️ refactor(compat): 替换 MoveJoint 时间律为解析式 7 阶平滑函数并添加离散限位校验

* 将预捕获 alpha 数据表替换为解析式 7 阶平滑点到点时间律
  s(u)=35u⁴-84u⁵+70u⁶-20u⁷,形状系数按 1~3 阶导数最大值重算
* 新增离散限位校验:按真实 8ms 采样点反算速度/加速度/jerk,
  不满足时自动拉长总时长后重采样,最多迭代 10000 次
* 实发轨迹落盘:ActualSendJointTraj.txt(角度制)、
  ActualSendJerkStats.txt(点间跃度统计),按时间目录归档
* J519 AcceptsCommand 门控:只有机器人就绪时才发送下一帧,
  减少无效下发;状态日志附带最近发送目标关节轴
* FanucControllerRuntime 构造函数改为必选 ILogger 注入,
  确保 DI 解析时稳定拿到日志实例
* LegacyHttpApiController 移除已废弃的 ConnectServer 调用,
  EnableRobot 参数从 2 改为 4
* 新增跃度报警分析文档和六轴限值表,补充反馈远离拒绝测试

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-05-06 09:06:28 +08:00
parent af65ca03a0
commit b1710e5d01
13 changed files with 1654 additions and 163 deletions

View File

@@ -2,6 +2,7 @@ using System.Buffers.Binary;
using System.Net;
using System.Net.Sockets;
using Flyshot.Runtime.Fanuc.Protocol;
using Microsoft.Extensions.Logging;
namespace Flyshot.Core.Tests;
@@ -287,6 +288,31 @@ public sealed class FanucJ519ClientTests : IDisposable
});
}
/// <summary>
/// 验证状态变化日志会附带最近一次实际发送的目标关节轴,便于联调时对照控制目标。
/// </summary>
[Fact]
public async Task ReceiveLoop_LogsLastSentTargetJointsWhenStatusChanges()
{
var logger = new CapturingLogger<FanucJ519Client>();
using var client = new FanucJ519Client(logger);
await client.ConnectAsync("127.0.0.1", Port, _cts.Token);
var initResult = await _server.ReceiveAsync(_cts.Token);
client.UpdateCommand(new FanucJ519Command(sequence: 1, targetJoints: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]));
client.StartMotion();
await SendStatusPacketAsync(initResult.RemoteEndPoint, sequence: 42);
_ = await _server.ReceiveAsync(_cts.Token);
await Task.Delay(200, _cts.Token);
Assert.Contains(
logger.Entries,
entry => entry.Level == LogLevel.Information
&& entry.Message.Contains("J519 最后一条发送目标关节轴", StringComparison.Ordinal)
&& entry.Message.Contains("1.000, 2.000, 3.000, 4.000, 5.000, 6.000", StringComparison.Ordinal));
}
/// <summary>
/// 向被测 J519 客户端发送一帧最小状态包,用机器人侧 status sequence 驱动下一帧命令。
/// </summary>
@@ -299,4 +325,68 @@ public sealed class FanucJ519ClientTests : IDisposable
responsePacket[0x0c] = 15;
await _server.SendAsync(responsePacket, clientEndpoint, _cts.Token);
}
/// <summary>
/// 收集测试过程中的结构化日志,便于断言运行期输出内容。
/// </summary>
private sealed class CapturingLogger<T> : ILogger<T>
{
/// <summary>
/// 获取已记录的日志条目。
/// </summary>
public List<LogEntry> Entries { get; } = [];
/// <summary>
/// 开始日志作用域;当前测试无需作用域,直接返回空对象。
/// </summary>
public IDisposable BeginScope<TState>(TState state)
where TState : notnull
{
return NullScope.Instance;
}
/// <summary>
/// 指示所有日志级别均启用,便于测试完整捕获输出。
/// </summary>
public bool IsEnabled(LogLevel logLevel)
{
return true;
}
/// <summary>
/// 记录一条格式化后的日志消息。
/// </summary>
public void Log<TState>(
LogLevel logLevel,
EventId eventId,
TState state,
Exception? exception,
Func<TState, Exception?, string> formatter)
{
Entries.Add(new LogEntry(logLevel, formatter(state, exception)));
}
/// <summary>
/// 表示一次日志记录。
/// </summary>
public sealed record LogEntry(LogLevel Level, string Message);
/// <summary>
/// 提供空日志作用域,避免测试中额外分配。
/// </summary>
private sealed class NullScope : IDisposable
{
/// <summary>
/// 获取单例空作用域。
/// </summary>
public static NullScope Instance { get; } = new();
/// <summary>
/// 释放空作用域;无需实际动作。
/// </summary>
public void Dispose()
{
}
}
}
}