using Flyshot.Core.Config; using Flyshot.Core.Domain; using Flyshot.Core.Planning; using Flyshot.Core.Planning.Sampling; using Flyshot.Core.Triggering; using Microsoft.Extensions.Logging; namespace Flyshot.ControllerClientCompat; /// /// 负责把 ControllerClient 兼容层的轨迹输入转换为规划结果和触发时间轴。 /// public sealed class ControllerClientTrajectoryOrchestrator { private readonly ICspPlanner _icspPlanner; private readonly SelfAdaptIcspPlanner _selfAdaptIcspPlanner; private readonly ShotTimelineBuilder _shotTimelineBuilder = new(new WaypointTimestampResolver()); private readonly Dictionary _flyshotCache = new(StringComparer.Ordinal); private readonly ILogger? _logger; /// /// 初始化轨迹编排器。 /// /// 日志记录器;允许 null。 public ControllerClientTrajectoryOrchestrator(ILogger? logger = null) { _logger = logger; _icspPlanner = new(logger: null); _selfAdaptIcspPlanner = new(logger: null); } /// /// 对普通轨迹执行 ICSP 规划。 /// /// 当前机器人配置。 /// 普通轨迹关节路点。 /// 包含规划轨迹、空触发时间轴和执行结果的结果包。 public PlannedExecutionBundle PlanOrdinaryTrajectory( RobotProfile robot, IReadOnlyList> waypoints, TrajectoryExecutionOptions? options = null, double planningSpeedScale = 1.0) { ArgumentNullException.ThrowIfNull(robot); ArgumentNullException.ThrowIfNull(waypoints); options ??= new TrajectoryExecutionOptions(); var planningRobot = ApplyPlanningSpeedScale(robot, planningSpeedScale); _logger?.LogInformation( "PlanOrdinaryTrajectory 开始: 路点数={WaypointCount}, method={Method}, planningSpeedScale={PlanningSpeedScale}", waypoints.Count, options.Method, planningSpeedScale); var program = CreateProgram( name: "ordinary-trajectory", waypoints: waypoints, shotFlags: Enumerable.Repeat(false, waypoints.Count), offsetValues: Enumerable.Repeat(0, waypoints.Count), addressGroups: Enumerable.Range(0, waypoints.Count).Select(static _ => Array.Empty())); var method = ParseOrdinaryMethod(options.Method); var request = new TrajectoryRequest( robot: planningRobot, program: program, method: method, saveTrajectoryArtifacts: options.SaveTrajectory); var plannedTrajectory = PlanByMethod(request, method); var shotTimeline = new ShotTimeline(Array.Empty(), Array.Empty()); var result = CreateResult(plannedTrajectory, shotTimeline, usedCache: false, shapeTrajectoryEdges: false); _logger?.LogInformation( "PlanOrdinaryTrajectory 完成: 时长={Duration}s, 采样点数={SampleCount}", result.Duration.TotalSeconds, result.DenseJointTrajectory?.Count ?? 0); return new PlannedExecutionBundle(plannedTrajectory, shotTimeline, result); } /// /// 对已经上传的飞拍轨迹执行自适应 ICSP 规划并生成触发时间轴。 /// /// 当前机器人配置。 /// 兼容层保存的上传轨迹。 /// 包含规划轨迹、触发时间轴和执行结果的结果包。 public PlannedExecutionBundle PlanUploadedFlyshot( RobotProfile robot, ControllerClientCompatUploadedTrajectory uploaded, FlyshotExecutionOptions? options = null, CompatibilityRobotSettings? settings = null, double? planningSpeedScale = null) { ArgumentNullException.ThrowIfNull(robot); ArgumentNullException.ThrowIfNull(uploaded); options ??= new FlyshotExecutionOptions(); settings ??= CreateDefaultRobotSettings(); var effectivePlanningSpeedScale = planningSpeedScale ?? settings.PlanningSpeedScale; var planningRobot = ApplyPlanningSpeedScale(robot, effectivePlanningSpeedScale); _logger?.LogInformation( "PlanUploadedFlyshot 开始: name={Name}, waypoints={WaypointCount}, method={Method}, useCache={UseCache}, planningSpeedScale={PlanningSpeedScale}", uploaded.Name, uploaded.Waypoints.Count, options.Method, options.UseCache, effectivePlanningSpeedScale); var program = CreateProgram( name: uploaded.Name, waypoints: uploaded.Waypoints, shotFlags: uploaded.ShotFlags, offsetValues: uploaded.OffsetValues, addressGroups: uploaded.AddressGroups); var method = ParseFlyshotMethod(options.Method); var cacheKey = CreateFlyshotCacheKey(planningRobot, uploaded, options, settings, effectivePlanningSpeedScale); if (options.UseCache && _flyshotCache.TryGetValue(cacheKey, out var cachedBundle)) { _logger?.LogInformation("PlanUploadedFlyshot 命中缓存: name={Name}, cacheKey={CacheKey}", uploaded.Name, cacheKey); var executionTrajectory = ApplySmoothStartStopTiming(cachedBundle.PlannedTrajectory); var executionTimeline = _shotTimelineBuilder.Build( executionTrajectory, holdCycles: settings.IoKeepCycles, samplePeriod: planningRobot.ServoPeriod, useDo: settings.UseDo); // 命中缓存时只替换 TrajectoryResult 的 usedCache 标志,规划轨迹和触发时间轴保持不可变复用。 return new PlannedExecutionBundle( cachedBundle.PlannedTrajectory, executionTimeline, CreateResult(executionTrajectory, executionTimeline, usedCache: true, shapeTrajectoryEdges: false)); } var request = new TrajectoryRequest( robot: planningRobot, program: program, method: method, moveToStart: options.MoveToStart, saveTrajectoryArtifacts: options.SaveTrajectory, useCache: options.UseCache); var plannedTrajectory = PlanByMethod(request, method, settings); var smoothedExecutionTrajectory = ApplySmoothStartStopTiming(plannedTrajectory); var shotTimeline = _shotTimelineBuilder.Build( smoothedExecutionTrajectory, holdCycles: settings.IoKeepCycles, samplePeriod: planningRobot.ServoPeriod, useDo: settings.UseDo); var result = CreateResult(smoothedExecutionTrajectory, shotTimeline, usedCache: false, shapeTrajectoryEdges: false); var bundle = new PlannedExecutionBundle(plannedTrajectory, shotTimeline, result); _logger?.LogInformation( "PlanUploadedFlyshot 完成: name={Name}, 时长={Duration}s, 触发事件数={TriggerCount}, 采样点数={SampleCount}", uploaded.Name, result.Duration.TotalSeconds, result.TriggerTimeline.Count, result.DenseJointTrajectory?.Count ?? 0); if (options.UseCache) { _flyshotCache[cacheKey] = bundle; } return bundle; } /// /// 按普通轨迹执行接口约束解析 method 参数。 /// /// 旧 SDK 传入的方法名。 /// 领域层规划方法。 private static PlanningMethod ParseOrdinaryMethod(string method) { var normalized = NormalizeMethod(method); return normalized switch { "icsp" => PlanningMethod.Icsp, "doubles" => PlanningMethod.Doubles, _ => throw new ArgumentException($"Unsupported ExecuteTrajectory method: {method}", nameof(method)) }; } /// /// 按飞拍轨迹执行接口约束解析 method 参数。 /// /// 旧 SDK 传入的方法名。 /// 领域层规划方法。 private static PlanningMethod ParseFlyshotMethod(string method) { var normalized = NormalizeMethod(method); return normalized switch { "icsp" => PlanningMethod.Icsp, "self-adapt-icsp" => PlanningMethod.SelfAdaptIcsp, "doubles" => PlanningMethod.Doubles, _ => throw new ArgumentException($"Unsupported ExecuteFlyShotTraj method: {method}", nameof(method)) }; } /// /// 按领域枚举分派到当前已经落地的规划器。 /// /// 规划请求。 /// 规划方法。 /// 规划轨迹。 private PlannedTrajectory PlanByMethod(TrajectoryRequest request, PlanningMethod method, CompatibilityRobotSettings? settings = null) { return method switch { PlanningMethod.Icsp => _icspPlanner.Plan(request), PlanningMethod.SelfAdaptIcsp => _selfAdaptIcspPlanner.Plan(request, settings?.AdaptIcspTryNum ?? 5), PlanningMethod.Doubles => throw new NotSupportedException("doubles 轨迹规划尚未落地。"), _ => throw new ArgumentOutOfRangeException(nameof(method), method, "未知轨迹规划方法。") }; } /// /// 归一化旧 SDK 的 method 字符串。 /// /// 原始方法名。 /// 小写短横线方法名。 private static string NormalizeMethod(string method) { if (string.IsNullOrWhiteSpace(method)) { return "icsp"; } return method.Trim().ToLowerInvariant(); } /// /// 为已上传飞拍轨迹构造包含参数和轨迹内容的缓存键,避免同名覆盖后误用旧规划结果。 /// /// 机器人配置。 /// 上传轨迹。 /// 执行参数。 /// 缓存键。 private static string CreateFlyshotCacheKey( RobotProfile robot, ControllerClientCompatUploadedTrajectory uploaded, FlyshotExecutionOptions options, CompatibilityRobotSettings settings, double planningSpeedScale) { var hash = new HashCode(); hash.Add(robot.Name, StringComparer.Ordinal); hash.Add(planningSpeedScale); hash.Add(uploaded.Name, StringComparer.Ordinal); hash.Add(NormalizeMethod(options.Method), StringComparer.Ordinal); hash.Add(options.MoveToStart); hash.Add(options.SaveTrajectory); hash.Add(settings.UseDo); hash.Add(settings.IoKeepCycles); hash.Add(settings.AdaptIcspTryNum); foreach (var limit in robot.JointLimits) { hash.Add(limit.JointName, StringComparer.Ordinal); hash.Add(limit.VelocityLimit); hash.Add(limit.AccelerationLimit); hash.Add(limit.JerkLimit); } foreach (var waypoint in uploaded.Waypoints) { foreach (var value in waypoint) { hash.Add(value); } } foreach (var flag in uploaded.ShotFlags) { hash.Add(flag); } foreach (var offset in uploaded.OffsetValues) { hash.Add(offset); } foreach (var group in uploaded.AddressGroups) { foreach (var address in group) { hash.Add(address); } } return hash.ToHashCode().ToString("X8"); } /// /// 构造编排器直接调用时的默认兼容配置,保持既有单元测试中的 DO 生成行为。 /// /// 默认机器人兼容配置。 private static CompatibilityRobotSettings CreateDefaultRobotSettings() { return new CompatibilityRobotSettings( useDo: true, ioAddresses: Array.Empty(), ioKeepCycles: 0, accLimitScale: 1.0, jerkLimitScale: 1.0, adaptIcspTryNum: 5); } /// /// 按规划全局速度倍率生成规划专用机器人约束。 /// /// 原始机器人约束。 /// 规划阶段的全局速度倍率,1.0 表示不额外缩放。 /// 已按速度倍率缩放后的规划机器人约束。 private static RobotProfile ApplyPlanningSpeedScale(RobotProfile robot, double planningSpeedScale) { if (double.IsNaN(planningSpeedScale) || double.IsInfinity(planningSpeedScale) || planningSpeedScale <= 0.0) { throw new ArgumentOutOfRangeException(nameof(planningSpeedScale), "规划速度倍率必须是有限正数。"); } if (Math.Abs(planningSpeedScale - 1.0) < 1e-12) { return robot; } // RVBUST 规划阶段会用独立限速倍率缩放有效限制;运行时 speedRatio 仍只负责 J519 下发重采样。 var scaledLimits = robot.JointLimits .Select(limit => new JointLimit( limit.JointName, limit.VelocityLimit * planningSpeedScale, limit.AccelerationLimit * planningSpeedScale * planningSpeedScale, limit.JerkLimit * planningSpeedScale * planningSpeedScale * planningSpeedScale)) .ToArray(); return new RobotProfile( name: robot.Name, modelPath: robot.ModelPath, degreesOfFreedom: robot.DegreesOfFreedom, jointLimits: scaledLimits, jointCouplings: robot.JointCouplings, servoPeriod: robot.ServoPeriod, triggerPeriod: robot.TriggerPeriod); } /// /// 把兼容层输入数组转换成领域层 FlyshotProgram。 /// /// 轨迹名称。 /// 关节路点。 /// 拍照标志。 /// 偏移周期。 /// IO 地址组。 /// 领域层飞拍程序。 private static FlyshotProgram CreateProgram( string name, IEnumerable> waypoints, IEnumerable shotFlags, IEnumerable offsetValues, IEnumerable> addressGroups) { return new FlyshotProgram( name: name, waypoints: waypoints.Select(static waypoint => new JointWaypoint(waypoint)).ToArray(), shotFlags: shotFlags.ToArray(), offsetValues: offsetValues.ToArray(), addressGroups: addressGroups.Select(static group => new IoAddressGroup(group)).ToArray()); } /// /// 从规划轨迹和触发时间轴构造运行时可消费的稳定结果对象。 /// /// 规划后的轨迹。 /// 触发时间轴。 /// 运行时执行结果描述。 private static TrajectoryResult CreateResult( PlannedTrajectory plannedTrajectory, ShotTimeline shotTimeline, bool usedCache, bool shapeTrajectoryEdges) { var denseJointTrajectory = TrajectorySampler.SampleJointTrajectory( plannedTrajectory, samplePeriod: plannedTrajectory.Robot.ServoPeriod.TotalSeconds, smoothStartStop: shapeTrajectoryEdges); return new TrajectoryResult( programName: plannedTrajectory.OriginalProgram.Name, method: plannedTrajectory.Method, isValid: true, duration: TimeSpan.FromSeconds(plannedTrajectory.WaypointTimes[^1]), shotEvents: shotTimeline.ShotEvents, triggerTimeline: shotTimeline.TriggerTimeline, artifacts: Array.Empty(), failureReason: null, usedCache: usedCache, originalWaypointCount: plannedTrajectory.OriginalWaypointCount, plannedWaypointCount: plannedTrajectory.PlannedWaypointCount, denseJointTrajectory: denseJointTrajectory); } /// /// 为飞拍执行生成一条平滑起停的时间轴。 /// 保持路点位置不变,只重映射路点时刻,让起点和终点附近的速度自然收敛。 /// private static PlannedTrajectory ApplySmoothStartStopTiming(PlannedTrajectory plannedTrajectory) { var originalTimes = plannedTrajectory.WaypointTimes; if (originalTimes.Count < 3) { return plannedTrajectory; } var totalDuration = originalTimes[^1]; if (totalDuration <= 0.0) { return plannedTrajectory; } var smoothedTimes = new double[originalTimes.Count]; smoothedTimes[0] = 0.0; smoothedTimes[^1] = totalDuration; for (var index = 1; index < originalTimes.Count - 1; index++) { var normalizedProgress = originalTimes[index] / totalDuration; smoothedTimes[index] = totalDuration * InvertSmoothStartStopProgress(normalizedProgress); } var segmentDurations = new double[smoothedTimes.Length - 1]; for (var index = 0; index < segmentDurations.Length; index++) { segmentDurations[index] = smoothedTimes[index + 1] - smoothedTimes[index]; } return new PlannedTrajectory( robot: plannedTrajectory.Robot, originalProgram: plannedTrajectory.OriginalProgram, plannedWaypoints: plannedTrajectory.PlannedWaypoints, waypointTimes: smoothedTimes, segmentDurations: segmentDurations, segmentScales: plannedTrajectory.SegmentScales, method: plannedTrajectory.Method, iterations: plannedTrajectory.Iterations, threshold: plannedTrajectory.Threshold); } /// /// 反解 7 次 smootherstep 的时间进度,用二分法把原始线性进度映射成平滑时间轴。 /// private static double InvertSmoothStartStopProgress(double normalizedProgress) { var target = Math.Clamp(normalizedProgress, 0.0, 1.0); var low = 0.0; var high = 1.0; for (var iteration = 0; iteration < 40; iteration++) { var middle = (low + high) / 2.0; var progress = EvaluateSmoothStartStopProgress(middle); if (progress < target) { low = middle; } else { high = middle; } } return (low + high) / 2.0; } /// /// 计算 7 次 smootherstep 进度值,用于整段平滑起停时间律。 /// private static double EvaluateSmoothStartStopProgress(double normalizedTime) { var u = Math.Clamp(normalizedTime, 0.0, 1.0); var u2 = u * u; var u3 = u2 * u; var u4 = u3 * u; var u5 = u4 * u; var u6 = u5 * u; var u7 = u6 * u; return (35.0 * u4) - (84.0 * u5) + (70.0 * u6) - (20.0 * u7); } }