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 const int MaxDenseLimitStretchIterations = 100; /// /// 每次离散限幅失败后统一放大的时间倍率。 /// private const double DenseLimitStretchFactor = 1.01; /// /// 平滑起停重定时混合系数:0 表示不平滑,1 表示完全使用平滑时间律。 /// 当前取值用于“弱平滑”,仅轻微拉伸首尾时间段,避免起停过慢。 /// private const double SmoothStartStopBlend = 0.60; 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。 /// 日志工厂;允许 null。 public ControllerClientTrajectoryOrchestrator( ILogger? logger = null, ILoggerFactory? loggerFactory = null) { _logger = logger; _icspPlanner = new(logger: loggerFactory?.CreateLogger()); _selfAdaptIcspPlanner = new(logger: loggerFactory?.CreateLogger()); } /// /// 对普通轨迹执行 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 executionTrajectory = plannedTrajectory; var denseJointTrajectory = CreateLimitCompliantDenseTrajectory(ref executionTrajectory, shapeTrajectoryEdges: false); var shotTimeline = new ShotTimeline(Array.Empty(), Array.Empty()); var result = CreateResult( executionTrajectory, shotTimeline, denseJointTrajectory, usedCache: false, triggerSampleIndexOffsetCycles: 0); _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}, smoothStartStopTiming={SmoothStartStopTiming}", uploaded.Name, uploaded.Waypoints.Count, options.Method, options.UseCache, effectivePlanningSpeedScale, settings.SmoothStartStopTiming); 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); // 命中缓存时只替换 TrajectoryResult 的 usedCache 标志,规划轨迹和触发时间轴保持不可变复用。 return cachedBundle; } 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 = ApplyExecutionTiming(plannedTrajectory, settings); var denseJointTrajectory = CreateLimitCompliantDenseTrajectory(ref smoothedExecutionTrajectory, shapeTrajectoryEdges: false); var shotTimeline = _shotTimelineBuilder.Build( smoothedExecutionTrajectory, holdCycles: settings.IoKeepCycles, samplePeriod: planningRobot.ServoPeriod, useDo: settings.UseDo); var result = CreateResult( smoothedExecutionTrajectory, shotTimeline, denseJointTrajectory, usedCache: false, triggerSampleIndexOffsetCycles: settings.TriggerSampleIndexOffsetCycles); 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.TriggerSampleIndexOffsetCycles); hash.Add(settings.AdaptIcspTryNum); hash.Add(settings.SmoothStartStopTiming); 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, triggerSampleIndexOffsetCycles: 7, accLimitScale: 1.0, jerkLimitScale: 1.0, adaptIcspTryNum: 5); } /// /// 按运行配置决定是否对规划结果做执行前时间轴重映射。 /// /// 规划阶段得到的轨迹。 /// 当前 RobotConfig.json 解析出的兼容设置。 /// 运行时真正用于采样和触发的轨迹。 private static PlannedTrajectory ApplyExecutionTiming(PlannedTrajectory plannedTrajectory, CompatibilityRobotSettings settings) { // legacy-fit 模式需要严格保留 waypoint.txt 反推出的节点时间,不能再二次改写时间轴。 return settings.SmoothStartStopTiming ? ApplySmoothStartStopTiming(plannedTrajectory) : plannedTrajectory; } /// /// 按规划全局速度倍率生成规划专用机器人约束。 /// /// 原始机器人约束。 /// 规划阶段的全局速度倍率,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, IReadOnlyList> denseJointTrajectory, bool usedCache, int triggerSampleIndexOffsetCycles) { 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, triggerSampleIndexOffsetCycles: triggerSampleIndexOffsetCycles, denseJointTrajectory: denseJointTrajectory); } /// /// 生成满足离散速度、加速度和 Jerk 限制的稠密执行轨迹。 /// private IReadOnlyList> CreateLimitCompliantDenseTrajectory( ref PlannedTrajectory executionTrajectory, bool shapeTrajectoryEdges) { for (var iteration = 0; iteration <= MaxDenseLimitStretchIterations; iteration++) { var denseJointTrajectory = TrajectorySampler.SampleJointTrajectory( executionTrajectory, samplePeriod: executionTrajectory.Robot.ServoPeriod.TotalSeconds, smoothStartStop: shapeTrajectoryEdges); try { TrajectoryLimitValidator.ValidateDenseJointTrajectory( executionTrajectory.Robot, denseJointTrajectory, trajectoryName: executionTrajectory.OriginalProgram.Name); return denseJointTrajectory; } catch (InvalidOperationException ex) when (iteration < MaxDenseLimitStretchIterations) { _logger?.LogWarning(ex, "稠密轨迹离散限幅校验失败,准备拉长时间轴重试"); // 离散差分超限时统一拉长时间轴,保持路点几何不变并降低速度、加速度和 Jerk。 executionTrajectory = StretchTrajectoryTiming(executionTrajectory, DenseLimitStretchFactor); _logger?.LogInformation( "离散差分超限,拉长时间轴,iteration={Iteration}, factor={StretchFactor}", iteration, DenseLimitStretchFactor); _logger?.LogInformation("拉长之后的总时间={TotalTime}", executionTrajectory.WaypointTimes[^1]); } } throw new InvalidOperationException("稠密轨迹离散限幅校验未能产生有效结果。"); } /// /// 按统一倍率拉长轨迹时间轴,保留原始路点和触发元数据。 /// private PlannedTrajectory StretchTrajectoryTiming(PlannedTrajectory trajectory, double stretchFactor) { var waypointTimes = trajectory.WaypointTimes.Select(time => time * stretchFactor).ToArray(); var segmentDurations = trajectory.SegmentDurations.Select(duration => duration * stretchFactor).ToArray(); var segmentScales = trajectory.SegmentScales.Select(scale => scale / stretchFactor).ToArray(); return new PlannedTrajectory( robot: trajectory.Robot, originalProgram: trajectory.OriginalProgram, plannedWaypoints: trajectory.PlannedWaypoints, waypointTimes: waypointTimes, segmentDurations: segmentDurations, segmentScales: segmentScales, method: trajectory.Method, iterations: trajectory.Iterations, threshold: trajectory.Threshold); } /// /// 为飞拍执行生成平滑起停时间轴(仅重定时,不改几何路点)。 /// 该方法保持 不变,只重映射 , /// 让轨迹在起点和终点附近具有更柔和的速度变化,从而降低“突然起步/突然收尾”带来的离散差分尖峰。 /// /// 规划器输出的原始轨迹(通常是线性时间轴)。 /// /// 若满足平滑条件,则返回新的重定时轨迹;若路点数量不足或总时长无效,则直接返回输入轨迹。 /// 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]; // 强制固定边界:起点仍在 0,终点仍在总时长,保证任务总耗时不变。 smoothedTimes[0] = 0.0; smoothedTimes[^1] = totalDuration; for (var index = 1; index < originalTimes.Count - 1; index++) { // 把原始时刻归一化到 [0, 1],用于统一时间律变换。 var normalizedProgress = originalTimes[index] / totalDuration; // 先求完整平滑时间律对应的时刻,再与原线性时刻按比例混合。 // 这里采用弱平滑(blend=0.35):保留大部分原节奏,仅轻微降低首尾突变。 var smoothedProgress = InvertSmoothStartStopProgress(normalizedProgress); var blendedProgress = ((1.0 - SmoothStartStopBlend) * normalizedProgress) + (SmoothStartStopBlend * smoothedProgress); smoothedTimes[index] = totalDuration * blendedProgress; } 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); } /// /// 反解平滑时间律进度:给定目标进度 p,求满足 f(t)=p 的 t。 /// 其中 f(t) 由 给出,是单调递增的 7 次 smootherstep 曲线, /// 通过二分法可稳定得到对应的归一化时间,避免显式求高次方程根带来的复杂性和数值不稳定。 /// /// 目标归一化进度,理论范围 [0, 1]。 /// 与目标进度对应的归一化时间,范围 [0, 1]。 private static double InvertSmoothStartStopProgress(double normalizedProgress) { // 防御式裁剪,避免上游浮点误差将进度推到区间外。 var target = Math.Clamp(normalizedProgress, 0.0, 1.0); var low = 0.0; var high = 1.0; // 固定迭代次数确保耗时可预测;40 次足以把区间收敛到工程可用精度。 for (var iteration = 0; iteration < 40; iteration++) { var middle = (low + high) / 2.0; var progress = EvaluateSmoothStartStopProgress(middle); // f(middle) 小于目标,说明根在右半区间;否则在左半区间。 if (progress < target) { low = middle; } else { high = middle; } } return (low + high) / 2.0; } /// /// 计算 7 次 smootherstep 的进度函数值 f(u)。 /// 该函数在 u=0 和 u=1 处具有更高阶导数连续性,可让起停段速度变化更平滑, /// 适合用于飞拍轨迹的时间重参数化,减少离散导数在边界处的突变。 /// /// 归一化时间 u,理论范围 [0, 1]。 /// 归一化进度 f(u),范围 [0, 1]。 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); } }