Files
FlyShotHost/src/Flyshot.Core.Domain/TrajectoryResult.cs
yunxiao.zhu 390d066ece feat(runtime): 添加轨迹持久化与密集执行链路
* 新增飞拍轨迹文件存储,支持上传、加载与删除
* 接通 ControllerClientCompat 到运行时的轨迹编排
* 完善 FANUC 命令与 J519 客户端发送链路
* 补充密集轨迹执行、运行时编排和协议客户端测试
* 更新 README 与 AGENTS 中的当前实现状态
2026-04-26 17:14:17 +08:00

283 lines
8.8 KiB
C#

using System.Text.Json.Serialization;
namespace Flyshot.Core.Domain;
/// <summary>
/// Represents the stable planning result returned to orchestration, SDKs, and monitoring layers.
/// </summary>
public sealed class TrajectoryResult
{
/// <summary>
/// Initializes a validated trajectory result.
/// </summary>
public TrajectoryResult(
string programName,
PlanningMethod method,
bool isValid,
TimeSpan duration,
IEnumerable<ShotEvent> shotEvents,
IEnumerable<TrajectoryDoEvent> triggerTimeline,
IEnumerable<TrajectoryArtifact> artifacts,
string? failureReason,
bool usedCache,
int originalWaypointCount,
int plannedWaypointCount,
IEnumerable<IReadOnlyList<double>>? denseJointTrajectory = null)
{
if (string.IsNullOrWhiteSpace(programName))
{
throw new ArgumentException("Program name is required.", nameof(programName));
}
if (duration < TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(duration), "Duration must be zero or positive.");
}
if (originalWaypointCount < 0)
{
throw new ArgumentOutOfRangeException(nameof(originalWaypointCount), "Original waypoint count must be zero or positive.");
}
if (plannedWaypointCount < originalWaypointCount)
{
throw new ArgumentOutOfRangeException(nameof(plannedWaypointCount), "Planned waypoint count must be greater than or equal to the original waypoint count.");
}
ArgumentNullException.ThrowIfNull(shotEvents);
ArgumentNullException.ThrowIfNull(triggerTimeline);
ArgumentNullException.ThrowIfNull(artifacts);
// Materialize once so the result remains stable after the planner hands it off.
var copiedShotEvents = shotEvents.ToArray();
var copiedTriggerTimeline = triggerTimeline.ToArray();
var copiedArtifacts = artifacts.ToArray();
var copiedDenseJointTrajectory = denseJointTrajectory?.Select(static row => row.ToArray()).ToArray();
ProgramName = programName;
Method = method;
IsValid = isValid;
Duration = duration;
ShotEvents = copiedShotEvents;
TriggerTimeline = copiedTriggerTimeline;
Artifacts = copiedArtifacts;
FailureReason = failureReason;
UsedCache = usedCache;
OriginalWaypointCount = originalWaypointCount;
PlannedWaypointCount = plannedWaypointCount;
DenseJointTrajectory = copiedDenseJointTrajectory;
}
/// <summary>
/// Gets the source program name.
/// </summary>
[JsonPropertyName("programName")]
public string ProgramName { get; }
/// <summary>
/// Gets the method that produced the result.
/// </summary>
[JsonPropertyName("method")]
public PlanningMethod Method { get; }
/// <summary>
/// Gets a value indicating whether the result can be executed.
/// </summary>
[JsonPropertyName("isValid")]
public bool IsValid { get; }
/// <summary>
/// Gets the final trajectory duration.
/// </summary>
[JsonPropertyName("duration")]
public TimeSpan Duration { get; }
/// <summary>
/// Gets the sampled shot events exported for monitoring and reports.
/// </summary>
[JsonPropertyName("shotEvents")]
public IReadOnlyList<ShotEvent> ShotEvents { get; }
/// <summary>
/// Gets the trigger timeline that the runtime will inject into servo execution.
/// </summary>
[JsonPropertyName("triggerTimeline")]
public IReadOnlyList<TrajectoryDoEvent> TriggerTimeline { get; }
/// <summary>
/// Gets the exported trajectory artifacts associated with the result.
/// </summary>
[JsonPropertyName("artifacts")]
public IReadOnlyList<TrajectoryArtifact> Artifacts { get; }
/// <summary>
/// Gets the failure reason when the result is invalid.
/// </summary>
[JsonPropertyName("failureReason")]
public string? FailureReason { get; }
/// <summary>
/// Gets a value indicating whether the result reused cached planning data.
/// </summary>
[JsonPropertyName("usedCache")]
public bool UsedCache { get; }
/// <summary>
/// Gets the teach waypoint count before adaptive point insertion.
/// </summary>
[JsonPropertyName("originalWaypointCount")]
public int OriginalWaypointCount { get; }
/// <summary>
/// Gets the final waypoint count after planner preprocessing.
/// </summary>
[JsonPropertyName("plannedWaypointCount")]
public int PlannedWaypointCount { get; }
/// <summary>
/// Gets the dense joint trajectory samples where each row is [time, j1, j2, ...].
/// Null when dense sampling was not performed (e.g. simulation fallback).
/// </summary>
[JsonPropertyName("denseJointTrajectory")]
public IReadOnlyList<IReadOnlyList<double>>? DenseJointTrajectory { get; }
}
/// <summary>
/// Describes a trigger event that the runtime must inject into the servo timeline.
/// </summary>
public sealed class TrajectoryDoEvent
{
/// <summary>
/// Initializes a validated runtime trigger event.
/// </summary>
public TrajectoryDoEvent(int waypointIndex, double triggerTime, int offsetCycles, int holdCycles, IoAddressGroup addressGroup)
{
if (waypointIndex < 0)
{
throw new ArgumentOutOfRangeException(nameof(waypointIndex), "Waypoint index must be zero or positive.");
}
if (triggerTime < 0.0)
{
throw new ArgumentOutOfRangeException(nameof(triggerTime), "Trigger time must be zero or positive.");
}
if (holdCycles < 0)
{
throw new ArgumentOutOfRangeException(nameof(holdCycles), "Hold cycles must be zero or positive.");
}
WaypointIndex = waypointIndex;
TriggerTime = triggerTime;
OffsetCycles = offsetCycles;
HoldCycles = holdCycles;
AddressGroup = addressGroup ?? throw new ArgumentNullException(nameof(addressGroup));
}
/// <summary>
/// Gets the original teach-waypoint index that requested the event.
/// </summary>
[JsonPropertyName("waypointIndex")]
public int WaypointIndex { get; }
/// <summary>
/// Gets the theoretical trigger time before discretization.
/// </summary>
[JsonPropertyName("triggerTime")]
public double TriggerTime { get; }
/// <summary>
/// Gets the configured offset in servo cycles.
/// </summary>
[JsonPropertyName("offsetCycles")]
public int OffsetCycles { get; }
/// <summary>
/// Gets the configured hold duration in servo cycles.
/// </summary>
[JsonPropertyName("holdCycles")]
public int HoldCycles { get; }
/// <summary>
/// Gets the IO address group to fire.
/// </summary>
[JsonPropertyName("addressGroup")]
public IoAddressGroup AddressGroup { get; }
}
/// <summary>
/// Describes an exported artifact generated from a planned trajectory.
/// </summary>
public sealed class TrajectoryArtifact
{
/// <summary>
/// Initializes a validated trajectory artifact descriptor.
/// </summary>
public TrajectoryArtifact(TrajectoryArtifactKind kind, string logicalName, string relativePath)
{
if (string.IsNullOrWhiteSpace(logicalName))
{
throw new ArgumentException("Logical artifact name is required.", nameof(logicalName));
}
if (string.IsNullOrWhiteSpace(relativePath))
{
throw new ArgumentException("Relative artifact path is required.", nameof(relativePath));
}
Kind = kind;
LogicalName = logicalName;
RelativePath = relativePath;
}
/// <summary>
/// Gets the exported artifact kind.
/// </summary>
[JsonPropertyName("kind")]
public TrajectoryArtifactKind Kind { get; }
/// <summary>
/// Gets the logical artifact file name.
/// </summary>
[JsonPropertyName("logicalName")]
public string LogicalName { get; }
/// <summary>
/// Gets the artifact path relative to the service output root.
/// </summary>
[JsonPropertyName("relativePath")]
public string RelativePath { get; }
}
/// <summary>
/// Identifies the exported trajectory artifact category.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum TrajectoryArtifactKind
{
/// <summary>
/// Represents sparse joint teach points.
/// </summary>
JointTeachTrajectory,
/// <summary>
/// Represents sparse Cartesian teach points.
/// </summary>
CartesianTeachTrajectory,
/// <summary>
/// Represents dense joint trajectory samples.
/// </summary>
JointDenseTrajectory,
/// <summary>
/// Represents dense Cartesian trajectory samples.
/// </summary>
CartesianDenseTrajectory,
/// <summary>
/// Represents the exported shot-event mapping.
/// </summary>
ShotEventTimeline
}