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);
}
}