using Flyshot.Core.Domain; namespace Flyshot.Core.Planning.Sampling; /// /// 负责在飞拍进入运行时前构建最终 8ms 发送队列,并执行发送口径的离散限幅校验。 /// public static class FlyshotExecutionSendSequenceBuilder { /// /// 根据连续规划轨迹构建最终发送队列;请求 speedRatio 只作为诊断信息保留。 /// /// 机器人关节限值配置。 /// 规划阶段产出的连续轨迹。 /// 规划结果。 /// J519 物理发送周期,单位为秒。 /// 请求的执行倍率,必须已经在规划阶段折算进轨迹。 /// 最终队列采样倍率;为空时使用 1.0,表示直接采样已调速后的规划轨迹。 /// 通过离散校验后的飞拍最终发送结果。 public static FlyshotPreparedExecution Build( RobotProfile robot, PlannedTrajectory plannedTrajectory, TrajectoryResult result, double servoPeriodSeconds, double requestedSpeedRatio, double? samplingSpeedRatio = null) { ArgumentNullException.ThrowIfNull(robot); ArgumentNullException.ThrowIfNull(plannedTrajectory); ArgumentNullException.ThrowIfNull(result); if (requestedSpeedRatio <= 0.0 || double.IsNaN(requestedSpeedRatio) || double.IsInfinity(requestedSpeedRatio)) { throw new ArgumentOutOfRangeException(nameof(requestedSpeedRatio), "speed_ratio 必须是有限正数。"); } if (servoPeriodSeconds <= 0.0 || double.IsNaN(servoPeriodSeconds) || double.IsInfinity(servoPeriodSeconds)) { throw new ArgumentOutOfRangeException(nameof(servoPeriodSeconds), "伺服周期必须是有限正数。"); } // 未显式指定独立采样倍率时,最终发送队列必须原样使用请求倍率。 var finalSpeedRatio = samplingSpeedRatio ?? requestedSpeedRatio; if (finalSpeedRatio <= 0.0 || double.IsNaN(finalSpeedRatio) || double.IsInfinity(finalSpeedRatio)) { throw new ArgumentOutOfRangeException(nameof(samplingSpeedRatio), "最终队列采样倍率必须是有限正数。"); } var samples = J519SendTrajectorySampler.SamplePlannedTrajectory( plannedTrajectory, servoPeriodSeconds, finalSpeedRatio); try { TrajectoryLimitValidator.ValidateJ519SendSamples( robot, samples, trajectoryName: result.ProgramName, validateJerk: true, strictJerkTolerance: true); } catch (InvalidOperationException exception) { throw new InvalidOperationException( $"飞拍最终发送队列在请求 speed_ratio={requestedSpeedRatio:F6} 下离散限幅校验失败。" + "请降低 planning_speed_scale 或 speed_ratio 后重新规划;执行侧不会在发送阶段临场重采样。", exception); } return BuildPreparedExecution( result, samples, requestedSpeedRatio, finalSpeedRatio, stretchIterationCount: 0); } /// /// 将通过校验的 J519 采样点和触发绑定结果封装为稳定的飞拍执行结果。 /// private static FlyshotPreparedExecution BuildPreparedExecution( TrajectoryResult result, IReadOnlyList samples, double requestedSpeedRatio, double finalSpeedRatio, int stretchIterationCount) { var preparedSamples = samples.Select(static sample => new FlyshotPreparedSample( sample.SampleIndex, sample.SendTime, sample.TrajectoryTime, sample.SpeedRatio, sample.JointsDegrees)).ToArray(); var sampleLookup = preparedSamples.ToDictionary(static sample => sample.SampleIndex); var triggerBindings = TriggerSampleBinder.Bind( result.TriggerTimeline, samples, result.TriggerSampleIndexOffsetCycles) .Select(binding => new FlyshotPreparedTriggerBinding( binding.Trigger, sampleLookup[binding.Sample.SampleIndex], binding.SampleIndex, binding.FoundInWindow)) .ToArray(); var timingRows = samples .Select(sample => J519SendTrajectorySampler.BuildTimingRow(sample, requestedSpeedRatio, stretchIterationCount)) .ToArray(); var jerkRows = BuildJerkRows(samples); var finalDurationSeconds = preparedSamples.Length == 0 ? 0.0 : preparedSamples[^1].SendTime; return new FlyshotPreparedExecution( preparedSamples, triggerBindings, timingRows, jerkRows, requestedSpeedRatio, finalSpeedRatio, finalDurationSeconds, stretchIterationCount); } /// /// 为最终发送点列构建跃度诊断行,保证导出工件和运行时发送复用同一份数据。 /// private static IReadOnlyList> BuildJerkRows(IReadOnlyList samples) { var jerkRows = new List>(Math.Max(0, samples.Count - 1)); double[]? previousVelocity = null; double[]? previousAcceleration = null; for (var index = 1; index < samples.Count; index++) { jerkRows.Add(J519SendTrajectorySampler.BuildJerkRow( samples[index - 1].SendTime, samples[index].SendTime, samples[index - 1].JointsDegrees, samples[index].JointsDegrees, ref previousVelocity, ref previousAcceleration)); } return jerkRows; } }