Files
FlyShotHost/src/Flyshot.Core.Domain/FlyshotProgram.cs
yunxiao.zhu 4eeaa3fef3 feat: 初始化飞拍替换方案仓库骨架
* 建立 .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 项目规范
2026-04-23 17:35:37 +08:00

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