* 新增 `MovePose` 方法,支持以直角坐标执行点到点移动。 * 引入 `LegacyCartesianPoseRequest` 类,处理直角位姿请求体的解析与验证。 * 更新 `LegacyHttpApiController`,实现 `/move_pose/` 路由以支持新功能。 * 增强状态快照元数据,提供机器人初始化状态与已上传轨迹信息。 * 更新前端状态页面,增加直角坐标点动控制面板与步长设置选项。 * 相关文档与测试用例同步更新,确保新功能的完整性与稳定性。
328 lines
11 KiB
C#
328 lines
11 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,
|
|
int triggerSampleIndexOffsetCycles = 0,
|
|
IEnumerable<IReadOnlyList<double>>? denseJointTrajectory = null,
|
|
FlyshotPreparedExecution? preparedFlyshotExecution = null,
|
|
IEnumerable<IReadOnlyList<double>>? denseCartesianTrajectory = 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.");
|
|
}
|
|
|
|
if (triggerSampleIndexOffsetCycles < 0)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(triggerSampleIndexOffsetCycles), "Trigger sample index offset cycles must be zero or positive.");
|
|
}
|
|
|
|
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();
|
|
var copiedDenseCartesianTrajectory = denseCartesianTrajectory?.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;
|
|
TriggerSampleIndexOffsetCycles = triggerSampleIndexOffsetCycles;
|
|
DenseJointTrajectory = copiedDenseJointTrajectory;
|
|
DenseCartesianTrajectory = copiedDenseCartesianTrajectory;
|
|
PreparedFlyshotExecution = preparedFlyshotExecution;
|
|
}
|
|
|
|
/// <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 configured command-sample offset applied after trigger binding picks the best sample.
|
|
/// </summary>
|
|
[JsonPropertyName("triggerSampleIndexOffsetCycles")]
|
|
public int TriggerSampleIndexOffsetCycles { 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>
|
|
/// 获取稠密直角坐标轨迹采样点,每行格式为 [time, x, y, z, w, p, r]。
|
|
/// Null 表示本结果不是直角流式运动。
|
|
/// </summary>
|
|
[JsonPropertyName("denseCartesianTrajectory")]
|
|
public IReadOnlyList<IReadOnlyList<double>>? DenseCartesianTrajectory { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the prepared flyshot execution queue when the flyshot chain has already built the final 8ms send sequence.
|
|
/// </summary>
|
|
[JsonIgnore]
|
|
public FlyshotPreparedExecution? PreparedFlyshotExecution { 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,
|
|
IEnumerable<double>? referenceJointsDegrees = null)
|
|
{
|
|
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));
|
|
ReferenceJointsDegrees = referenceJointsDegrees?.ToArray();
|
|
}
|
|
|
|
/// <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>
|
|
/// Gets the teach waypoint joints converted to degrees.
|
|
/// This is used to select the closest real send sample inside the trigger time window.
|
|
/// </summary>
|
|
[JsonPropertyName("referenceJointsDegrees")]
|
|
public IReadOnlyList<double>? ReferenceJointsDegrees { 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
|
|
}
|