135 lines
5.0 KiB
C#
135 lines
5.0 KiB
C#
using Flyshot.Core.Domain;
|
|
|
|
namespace Flyshot.Core.Planning.Sampling;
|
|
|
|
/// <summary>
|
|
/// 负责把理论触发事件绑定到实际 J519 发送 sample。
|
|
/// 绑定规则为:先在理论触发时间前后固定时间窗内筛候选点,再优先选择关节差最小的 sample。
|
|
/// </summary>
|
|
public static class TriggerSampleBinder
|
|
{
|
|
/// <summary>
|
|
/// 当前触发绑定使用的固定近时窗半宽,单位为秒。
|
|
/// 即:以理论触发时间为中心,在前后各 100ms 的范围内寻找最优 sample。
|
|
/// </summary>
|
|
public const double TriggerBindingToleranceSeconds = 0.1;
|
|
|
|
/// <summary>
|
|
/// 把一组触发事件绑定到采样后的 J519 sample。
|
|
/// </summary>
|
|
/// <param name="triggers">待绑定的触发事件。</param>
|
|
/// <param name="samples">J519 实发 sample。</param>
|
|
/// <param name="sampleIndexOffsetCycles">在最佳绑定 sample 基础上继续向后偏移的命令周期数。</param>
|
|
/// <returns>按最终 sampleIndex 排序的绑定结果。</returns>
|
|
public static IReadOnlyList<TriggerSampleBinding> Bind(
|
|
IReadOnlyList<TrajectoryDoEvent> triggers,
|
|
IReadOnlyList<J519SendSample> samples,
|
|
int sampleIndexOffsetCycles = 0)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(triggers);
|
|
ArgumentNullException.ThrowIfNull(samples);
|
|
if (sampleIndexOffsetCycles < 0)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(sampleIndexOffsetCycles), "触发 sample 偏移周期不能为负数。");
|
|
}
|
|
|
|
var bindings = new List<TriggerSampleBinding>(triggers.Count);
|
|
if (triggers.Count == 0 || samples.Count == 0)
|
|
{
|
|
return bindings;
|
|
}
|
|
|
|
var lastSampleIndex = samples.Count - 1;
|
|
foreach (var trigger in triggers)
|
|
{
|
|
var fallbackIndex = FindClosestTrajectoryTimeSampleIndex(samples, trigger.TriggerTime);
|
|
var candidateStart = Math.Max(0, fallbackIndex - 1);
|
|
var candidateEnd = Math.Min(lastSampleIndex, fallbackIndex + 1);
|
|
var bestIndex = fallbackIndex;
|
|
var bestScore = double.PositiveInfinity;
|
|
var foundInWindow = false;
|
|
|
|
for (var sampleIndex = candidateStart; sampleIndex <= candidateEnd; sampleIndex++)
|
|
{
|
|
var sample = samples[sampleIndex];
|
|
if (Math.Abs(sample.TrajectoryTime - trigger.TriggerTime) > TriggerBindingToleranceSeconds)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foundInWindow = true;
|
|
var score = trigger.ReferenceJointsDegrees is { Count: > 0 }
|
|
? ComputeJointDifferenceScore(trigger.ReferenceJointsDegrees, sample.JointsDegrees)
|
|
: Math.Abs(sample.TrajectoryTime - trigger.TriggerTime);
|
|
if (score < bestScore)
|
|
{
|
|
bestScore = score;
|
|
bestIndex = sampleIndex;
|
|
}
|
|
}
|
|
|
|
var finalIndex = Math.Min(lastSampleIndex, bestIndex + sampleIndexOffsetCycles);
|
|
bindings.Add(new TriggerSampleBinding(trigger, samples[finalIndex], finalIndex, foundInWindow));
|
|
}
|
|
|
|
return bindings
|
|
.OrderBy(static binding => binding.SampleIndex)
|
|
.ThenBy(static binding => binding.Trigger.WaypointIndex)
|
|
.ToArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 按轨迹时间找到最接近理论触发时刻的 sample。
|
|
/// </summary>
|
|
public static int FindClosestTrajectoryTimeSampleIndex(IReadOnlyList<J519SendSample> samples, double triggerTime)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(samples);
|
|
|
|
var bestIndex = 0;
|
|
var bestDelta = double.PositiveInfinity;
|
|
for (var sampleIndex = 0; sampleIndex < samples.Count; sampleIndex++)
|
|
{
|
|
var delta = Math.Abs(samples[sampleIndex].TrajectoryTime - triggerTime);
|
|
if (delta < bestDelta)
|
|
{
|
|
bestDelta = delta;
|
|
bestIndex = sampleIndex;
|
|
}
|
|
}
|
|
|
|
return bestIndex;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 计算参考示教点与候选 sample 的关节差评分,使用平方和即可稳定排序。
|
|
/// </summary>
|
|
public static double ComputeJointDifferenceScore(
|
|
IReadOnlyList<double>? referenceJointsDegrees,
|
|
IReadOnlyList<double> sampleJointsDegrees)
|
|
{
|
|
if (referenceJointsDegrees is null || referenceJointsDegrees.Count == 0)
|
|
{
|
|
return 0.0;
|
|
}
|
|
|
|
var jointCount = Math.Min(referenceJointsDegrees.Count, sampleJointsDegrees.Count);
|
|
var score = 0.0;
|
|
for (var index = 0; index < jointCount; index++)
|
|
{
|
|
var delta = sampleJointsDegrees[index] - referenceJointsDegrees[index];
|
|
score += delta * delta;
|
|
}
|
|
|
|
return score;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 表示一个理论触发事件最终绑定到哪个 J519 sample 的结果。
|
|
/// </summary>
|
|
public sealed record TriggerSampleBinding(
|
|
TrajectoryDoEvent Trigger,
|
|
J519SendSample Sample,
|
|
int SampleIndex,
|
|
bool FoundInWindow);
|