feat(server): 添加静态状态页与调试入口

- 将状态页、调试页改为 `wwwroot` 静态资源
  - 补充调试配置接口与前端脚本
  - 为兼容层、规划层和运行时补充日志
  - 更新集成测试覆盖新入口
This commit is contained in:
2026-04-29 14:05:02 +08:00
parent 0724efebed
commit c38faddbf0
27 changed files with 2630 additions and 1894 deletions

View File

@@ -1,6 +1,7 @@
using Flyshot.Core.Config;
using Flyshot.Core.Domain;
using Flyshot.Runtime.Common;
using Microsoft.Extensions.Logging;
namespace Flyshot.ControllerClientCompat;
@@ -17,6 +18,7 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
private readonly ControllerClientTrajectoryOrchestrator _trajectoryOrchestrator;
private readonly RobotConfigLoader _configLoader;
private readonly IFlyshotTrajectoryStore _trajectoryStore;
private readonly ILogger<ControllerClientCompatService>? _logger;
private RobotProfile? _activeRobotProfile;
private string? _configuredRobotName;
private CompatibilityRobotSettings? _robotSettings;
@@ -35,13 +37,15 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
/// <param name="trajectoryOrchestrator">轨迹规划与触发编排器。</param>
/// <param name="configLoader">旧版 RobotConfig.json 加载器。</param>
/// <param name="trajectoryStore">已上传轨迹持久化存储。</param>
/// <param name="logger">日志记录器;允许测试直接构造时传入 null。</param>
public ControllerClientCompatService(
ControllerClientCompatOptions options,
ControllerClientCompatRobotCatalog robotCatalog,
IControllerRuntime runtime,
ControllerClientTrajectoryOrchestrator trajectoryOrchestrator,
RobotConfigLoader configLoader,
IFlyshotTrajectoryStore trajectoryStore)
IFlyshotTrajectoryStore trajectoryStore,
ILogger<ControllerClientCompatService>? logger = null)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_robotCatalog = robotCatalog ?? throw new ArgumentNullException(nameof(robotCatalog));
@@ -49,6 +53,7 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
_trajectoryOrchestrator = trajectoryOrchestrator ?? throw new ArgumentNullException(nameof(trajectoryOrchestrator));
_configLoader = configLoader ?? throw new ArgumentNullException(nameof(configLoader));
_trajectoryStore = trajectoryStore ?? throw new ArgumentNullException(nameof(trajectoryStore));
_logger = logger;
}
/// <inheritdoc />
@@ -90,6 +95,8 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
_connectedServerIp = serverIp;
_connectedServerPort = port;
}
_logger?.LogInformation("ConnectServer 完成: {ServerIp}:{Port}", serverIp, port);
}
/// <inheritdoc />
@@ -107,6 +114,8 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
/// <inheritdoc />
public void SetUpRobot(string robotName)
{
_logger?.LogInformation("SetUpRobot 开始: robotName={RobotName}", robotName);
var robotSettings = TryLoadRobotSettings() ?? CreateDefaultRobotSettings();
var robotProfile = _robotCatalog.LoadProfile(
robotName,
@@ -129,6 +138,14 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
_uploadedTrajectories[saved.Key] = saved.Value;
}
}
_logger?.LogInformation(
"SetUpRobot 完成: robotName={RobotName}, dof={Dof}, accLimit={AccLimit}, jerkLimit={JerkLimit}, 恢复轨迹数={TrajCount}",
robotName,
robotProfile.DegreesOfFreedom,
robotSettings.AccLimitScale,
robotSettings.JerkLimitScale,
_uploadedTrajectories.Count);
}
/// <inheritdoc />
@@ -184,11 +201,15 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
throw new ArgumentException("控制器 IP 不能为空。", nameof(robotIp));
}
_logger?.LogInformation("Connect 开始: robotIp={RobotIp}", robotIp);
lock (_stateLock)
{
EnsureRobotSetup();
_runtime.Connect(robotIp);
}
_logger?.LogInformation("Connect 完成: robotIp={RobotIp}", robotIp);
}
/// <inheritdoc />
@@ -204,31 +225,37 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
/// <inheritdoc />
public void EnableRobot(int bufferSize)
{
_logger?.LogInformation("EnableRobot 开始: bufferSize={BufferSize}", bufferSize);
lock (_stateLock)
{
EnsureRobotSetup();
_runtime.EnableRobot(bufferSize);
}
_logger?.LogInformation("EnableRobot 完成");
}
/// <inheritdoc />
public void DisableRobot()
{
_logger?.LogInformation("DisableRobot 开始");
lock (_stateLock)
{
EnsureRobotSetup();
_runtime.DisableRobot();
}
_logger?.LogInformation("DisableRobot 完成");
}
/// <inheritdoc />
public void StopMove()
{
_logger?.LogInformation("StopMove 开始");
lock (_stateLock)
{
EnsureRobotSetup();
_runtime.StopMove();
}
_logger?.LogInformation("StopMove 完成");
}
/// <inheritdoc />
@@ -335,6 +362,9 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
{
ArgumentNullException.ThrowIfNull(jointPositions);
_logger?.LogInformation("MoveJoint 开始: 目标关节数={JointCount}", jointPositions.Count);
_logger?.LogDebug("MoveJoint 目标关节: {Joints}", string.Join(", ", jointPositions.Select(j => j.ToString("F4"))));
lock (_stateLock)
{
var robot = RequireActiveRobot();
@@ -345,8 +375,15 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
var speedRatio = _runtime.GetSnapshot().SpeedRatio;
var moveResult = MoveJointTrajectoryGenerator.CreateResult(robot, currentJointPositions, jointPositions, speedRatio);
_logger?.LogInformation(
"MoveJoint 规划完成: 当前速度倍率={SpeedRatio}, 规划时长={Duration}s, 采样点数={SampleCount}",
speedRatio,
moveResult.Duration.TotalSeconds,
moveResult.DenseJointTrajectory?.Count ?? 0);
_runtime.ExecuteTrajectory(moveResult, jointPositions);
}
_logger?.LogInformation("MoveJoint 完成");
}
/// <inheritdoc />
@@ -359,6 +396,11 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
throw new ArgumentException("轨迹路点不能为空。", nameof(waypoints));
}
_logger?.LogInformation("ExecuteTrajectory 开始: 路点数={WaypointCount}, method={Method}, saveTraj={SaveTraj}",
waypoints.Count, options.Method, options.SaveTrajectory);
_logger?.LogDebug("ExecuteTrajectory 路点详情: {Waypoints}",
string.Join(" | ", waypoints.Select(wp => $"[{string.Join(", ", wp.Select(j => j.ToString("F4")))}]")));
lock (_stateLock)
{
var robot = RequireActiveRobot();
@@ -366,9 +408,17 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
// 普通轨迹必须按调用方指定 method 规划,再把规划结果交给运行时执行。
var bundle = _trajectoryOrchestrator.PlanOrdinaryTrajectory(robot, waypoints, options);
_logger?.LogInformation(
"ExecuteTrajectory 规划完成: method={Method}, 时长={Duration}s, 有效={IsValid}, 采样点数={SampleCount}",
bundle.Result.Method,
bundle.Result.Duration.TotalSeconds,
bundle.Result.IsValid,
bundle.Result.DenseJointTrajectory?.Count ?? 0);
var finalJointPositions = bundle.PlannedTrajectory.PlannedWaypoints[^1].Positions;
_runtime.ExecuteTrajectory(bundle.Result, finalJointPositions);
}
_logger?.LogInformation("ExecuteTrajectory 完成");
}
/// <inheritdoc />
@@ -386,6 +436,12 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
{
ArgumentNullException.ThrowIfNull(trajectory);
_logger?.LogInformation(
"UploadTrajectory 开始: name={Name}, waypoints={WaypointCount}, shotFlags={ShotCount}",
trajectory.Name,
trajectory.Waypoints.Count,
trajectory.ShotFlags.Count(static f => f));
lock (_stateLock)
{
EnsureRuntimeEnabled();
@@ -395,6 +451,8 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
var settings = _robotSettings ?? CreateDefaultRobotSettings();
_trajectoryStore.Save(robotName, settings, trajectory);
}
_logger?.LogInformation("UploadTrajectory 完成: name={Name}", trajectory.Name);
}
/// <inheritdoc />
@@ -415,6 +473,10 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
throw new ArgumentException("轨迹名称不能为空。", nameof(name));
}
_logger?.LogInformation(
"ExecuteTrajectoryByName 开始: name={Name}, method={Method}, moveToStart={MoveToStart}, useCache={UseCache}",
name, options.Method, options.MoveToStart, options.UseCache);
lock (_stateLock)
{
var robot = RequireActiveRobot();
@@ -422,24 +484,37 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
if (!_uploadedTrajectories.TryGetValue(name, out var trajectory))
{
_logger?.LogWarning("ExecuteTrajectoryByName 失败: 轨迹不存在 name={Name}", name);
throw new InvalidOperationException("FlyShot trajectory does not exist.");
}
if (trajectory.Waypoints.Count == 0)
{
_logger?.LogWarning("ExecuteTrajectoryByName 失败: 轨迹无路点 name={Name}", name);
throw new InvalidOperationException("FlyShot trajectory contains no waypoints.");
}
// 已上传飞拍轨迹必须按调用方指定 method 生成 shot timeline 后再交给运行时。
var bundle = _trajectoryOrchestrator.PlanUploadedFlyshot(robot, trajectory, options, RequireRobotSettings());
_logger?.LogInformation(
"ExecuteTrajectoryByName 规划完成: name={Name}, method={Method}, 时长={Duration}s, 触发事件数={TriggerCount}, 使用缓存={UsedCache}",
name,
bundle.Result.Method,
bundle.Result.Duration.TotalSeconds,
bundle.Result.TriggerTimeline.Count,
bundle.Result.UsedCache);
if (options.MoveToStart)
{
_logger?.LogInformation("ExecuteTrajectoryByName 先移动到起点");
_runtime.ExecuteTrajectory(CreateImmediateMoveResult(), bundle.PlannedTrajectory.PlannedWaypoints[0].Positions);
}
var finalJointPositions = bundle.PlannedTrajectory.PlannedWaypoints[^1].Positions;
_runtime.ExecuteTrajectory(bundle.Result, finalJointPositions);
}
_logger?.LogInformation("ExecuteTrajectoryByName 完成: name={Name}", name);
}
/// <inheritdoc />
@@ -450,11 +525,14 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
throw new ArgumentException("轨迹名称不能为空。", nameof(name));
}
_logger?.LogInformation("SaveTrajectoryInfo 开始: name={Name}, method={Method}", name, method);
lock (_stateLock)
{
var robot = RequireActiveRobot();
if (!_uploadedTrajectories.TryGetValue(name, out var trajectory))
{
_logger?.LogWarning("SaveTrajectoryInfo 失败: 轨迹不存在 name={Name}", name);
throw new InvalidOperationException("FlyShot trajectory does not exist.");
}
@@ -469,6 +547,8 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
var settings = _robotSettings ?? CreateDefaultRobotSettings();
_trajectoryStore.Save(robotName, settings, trajectory);
}
_logger?.LogInformation("SaveTrajectoryInfo 完成: name={Name}", name);
}
/// <inheritdoc />
@@ -479,11 +559,14 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
throw new ArgumentException("轨迹名称不能为空。", nameof(name));
}
_logger?.LogInformation("IsFlyshotTrajectoryValid 开始: name={Name}, method={Method}", name, method);
lock (_stateLock)
{
var robot = RequireActiveRobot();
if (!_uploadedTrajectories.TryGetValue(name, out var trajectory))
{
_logger?.LogWarning("IsFlyshotTrajectoryValid 失败: 轨迹不存在 name={Name}", name);
throw new InvalidOperationException("FlyShot trajectory does not exist.");
}
@@ -494,6 +577,9 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
RequireRobotSettings());
duration = bundle.Result.Duration;
_logger?.LogInformation(
"IsFlyshotTrajectoryValid 结果: name={Name}, valid={Valid}, duration={Duration}s",
name, bundle.Result.IsValid, duration.TotalSeconds);
return bundle.Result.IsValid;
}
}
@@ -506,16 +592,21 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
throw new ArgumentException("轨迹名称不能为空。", nameof(name));
}
_logger?.LogInformation("DeleteTrajectory 开始: name={Name}", name);
lock (_stateLock)
{
if (!_uploadedTrajectories.Remove(name))
{
_logger?.LogWarning("DeleteTrajectory 失败: 轨迹不存在 name={Name}", name);
throw new InvalidOperationException("DeleteFlyShotTraj failed");
}
var robotName = _configuredRobotName ?? throw new InvalidOperationException("Robot has not been setup.");
_trajectoryStore.Delete(robotName, name);
}
_logger?.LogInformation("DeleteTrajectory 完成: name={Name}", name);
}
/// <inheritdoc />