* 建立 .NET 8 解决方案及分层项目结构 * 添加 Flyshot.Core.Domain 领域模型(机器人、轨迹、运动学) * 添加 Flyshot.Core.Planning 规划层(ICSP、CubicSpline、采样器) * 添加 Flyshot.Core.Triggering 触发时序与 IO 时间轴 * 添加 Flyshot.Core.Config 配置兼容与 .robot 解析 * 添加 Flyshot.Server.Host 最小宿主及 /healthz 端点 * 补充单元测试与集成测试项目 * 添加 CLAUDE.md、AGENTS.md、README.md 项目规范
176 lines
5.7 KiB
C#
176 lines
5.7 KiB
C#
using System.Text.Json.Serialization;
|
|
|
|
namespace Flyshot.Core.Domain;
|
|
|
|
/// <summary>
|
|
/// Represents an uploaded flyshot program built from teach points and shot metadata.
|
|
/// </summary>
|
|
public sealed class FlyshotProgram
|
|
{
|
|
/// <summary>
|
|
/// Initializes a validated flyshot program.
|
|
/// </summary>
|
|
public FlyshotProgram(
|
|
string name,
|
|
IEnumerable<JointWaypoint> waypoints,
|
|
IEnumerable<bool> shotFlags,
|
|
IEnumerable<int> offsetValues,
|
|
IEnumerable<IoAddressGroup> addressGroups)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(name))
|
|
{
|
|
throw new ArgumentException("Program name is required.", nameof(name));
|
|
}
|
|
|
|
ArgumentNullException.ThrowIfNull(waypoints);
|
|
ArgumentNullException.ThrowIfNull(shotFlags);
|
|
ArgumentNullException.ThrowIfNull(offsetValues);
|
|
ArgumentNullException.ThrowIfNull(addressGroups);
|
|
|
|
// Freeze the uploaded data so later orchestration cannot silently drift from the request payload.
|
|
var copiedWaypoints = waypoints.ToArray();
|
|
var copiedShotFlags = shotFlags.ToArray();
|
|
var copiedOffsetValues = offsetValues.ToArray();
|
|
var copiedAddressGroups = addressGroups.ToArray();
|
|
|
|
if (copiedWaypoints.Length == 0)
|
|
{
|
|
throw new ArgumentException("At least one waypoint is required.", nameof(waypoints));
|
|
}
|
|
|
|
if (copiedShotFlags.Length != copiedWaypoints.Length)
|
|
{
|
|
throw new ArgumentException("Shot flag count must match waypoint count.", nameof(shotFlags));
|
|
}
|
|
|
|
if (copiedOffsetValues.Length != copiedWaypoints.Length)
|
|
{
|
|
throw new ArgumentException("Offset value count must match waypoint count.", nameof(offsetValues));
|
|
}
|
|
|
|
if (copiedAddressGroups.Length != copiedWaypoints.Length)
|
|
{
|
|
throw new ArgumentException("Address group count must match waypoint count.", nameof(addressGroups));
|
|
}
|
|
|
|
var jointDimension = copiedWaypoints[0].Positions.Count;
|
|
if (copiedWaypoints.Any(waypoint => waypoint.Positions.Count != jointDimension))
|
|
{
|
|
throw new ArgumentException("All waypoints must share the same joint dimension.", nameof(waypoints));
|
|
}
|
|
|
|
Name = name;
|
|
Waypoints = copiedWaypoints;
|
|
ShotFlags = copiedShotFlags;
|
|
OffsetValues = copiedOffsetValues;
|
|
AddressGroups = copiedAddressGroups;
|
|
JointDimension = jointDimension;
|
|
ShotWaypointCount = copiedShotFlags.Count(flag => flag);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the program name used by cache and gateway lookups.
|
|
/// </summary>
|
|
[JsonPropertyName("name")]
|
|
public string Name { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the immutable teach waypoint list.
|
|
/// </summary>
|
|
[JsonPropertyName("waypoints")]
|
|
public IReadOnlyList<JointWaypoint> Waypoints { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the per-waypoint shot trigger flags.
|
|
/// </summary>
|
|
[JsonPropertyName("shotFlags")]
|
|
public IReadOnlyList<bool> ShotFlags { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the per-waypoint trigger offset values in servo cycles.
|
|
/// </summary>
|
|
[JsonPropertyName("offsetValues")]
|
|
public IReadOnlyList<int> OffsetValues { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the per-waypoint IO address groups.
|
|
/// </summary>
|
|
[JsonPropertyName("addressGroups")]
|
|
public IReadOnlyList<IoAddressGroup> AddressGroups { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the joint dimension shared by all waypoints.
|
|
/// </summary>
|
|
[JsonPropertyName("jointDimension")]
|
|
public int JointDimension { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the number of waypoints that request a shot trigger.
|
|
/// </summary>
|
|
[JsonPropertyName("shotWaypointCount")]
|
|
public int ShotWaypointCount { get; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents a single teach waypoint in joint space.
|
|
/// </summary>
|
|
public sealed class JointWaypoint
|
|
{
|
|
/// <summary>
|
|
/// Initializes a validated joint waypoint.
|
|
/// </summary>
|
|
public JointWaypoint(IEnumerable<double> positions)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(positions);
|
|
|
|
// Copy the input once so the waypoint remains immutable after upload.
|
|
var copiedPositions = positions.ToArray();
|
|
if (copiedPositions.Length == 0)
|
|
{
|
|
throw new ArgumentException("Joint waypoint must contain at least one joint value.", nameof(positions));
|
|
}
|
|
|
|
Positions = copiedPositions;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the immutable joint value vector.
|
|
/// </summary>
|
|
[JsonPropertyName("positions")]
|
|
public IReadOnlyList<double> Positions { get; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents the list of IO addresses that should fire at one logical trigger point.
|
|
/// </summary>
|
|
public sealed class IoAddressGroup
|
|
{
|
|
/// <summary>
|
|
/// Initializes a validated IO address group.
|
|
/// </summary>
|
|
public IoAddressGroup(IEnumerable<int> addresses)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(addresses);
|
|
|
|
// Preserve address order because some upper-layer tooling expects exported order stability.
|
|
var copiedAddresses = addresses.ToArray();
|
|
if (copiedAddresses.Length > 8)
|
|
{
|
|
throw new ArgumentException("A single trigger point can contain at most eight IO addresses.", nameof(addresses));
|
|
}
|
|
|
|
if (copiedAddresses.Any(address => address < 0))
|
|
{
|
|
throw new ArgumentException("IO addresses must be zero or positive.", nameof(addresses));
|
|
}
|
|
|
|
Addresses = copiedAddresses;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the immutable ordered IO address list.
|
|
/// </summary>
|
|
[JsonPropertyName("addresses")]
|
|
public IReadOnlyList<int> Addresses { get; }
|
|
}
|