✨ 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 项目规范
This commit is contained in:
226
src/Flyshot.Core.Domain/RobotProfile.cs
Normal file
226
src/Flyshot.Core.Domain/RobotProfile.cs
Normal file
@@ -0,0 +1,226 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Flyshot.Core.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// Describes the robot model contract consumed by planning and runtime orchestration.
|
||||
/// </summary>
|
||||
public sealed class RobotProfile
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new robot profile with validated joint limits and coupling metadata.
|
||||
/// </summary>
|
||||
public RobotProfile(
|
||||
string name,
|
||||
string modelPath,
|
||||
int degreesOfFreedom,
|
||||
IEnumerable<JointLimit> jointLimits,
|
||||
IEnumerable<JointCoupling> jointCouplings,
|
||||
TimeSpan servoPeriod,
|
||||
TimeSpan triggerPeriod)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new ArgumentException("Robot profile name is required.", nameof(name));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(modelPath))
|
||||
{
|
||||
throw new ArgumentException("Robot profile model path is required.", nameof(modelPath));
|
||||
}
|
||||
|
||||
if (degreesOfFreedom <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(degreesOfFreedom), "Degrees of freedom must be positive.");
|
||||
}
|
||||
|
||||
if (servoPeriod <= TimeSpan.Zero)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(servoPeriod), "Servo period must be positive.");
|
||||
}
|
||||
|
||||
if (triggerPeriod <= TimeSpan.Zero)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(triggerPeriod), "Trigger period must be positive.");
|
||||
}
|
||||
|
||||
ArgumentNullException.ThrowIfNull(jointLimits);
|
||||
ArgumentNullException.ThrowIfNull(jointCouplings);
|
||||
|
||||
// Snapshot the collections once so downstream layers cannot mutate domain state in place.
|
||||
var copiedJointLimits = jointLimits.ToArray();
|
||||
var copiedJointCouplings = jointCouplings.ToArray();
|
||||
|
||||
if (copiedJointLimits.Length != degreesOfFreedom)
|
||||
{
|
||||
throw new ArgumentException("Joint limit count must match degrees of freedom.", nameof(jointLimits));
|
||||
}
|
||||
|
||||
Name = name;
|
||||
ModelPath = modelPath;
|
||||
DegreesOfFreedom = degreesOfFreedom;
|
||||
JointLimits = copiedJointLimits;
|
||||
JointCouplings = copiedJointCouplings;
|
||||
ServoPeriod = servoPeriod;
|
||||
TriggerPeriod = triggerPeriod;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the robot profile name exposed to the rest of the runtime.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source path of the robot model file.
|
||||
/// </summary>
|
||||
[JsonPropertyName("modelPath")]
|
||||
public string ModelPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the active revolute degree-of-freedom count.
|
||||
/// </summary>
|
||||
[JsonPropertyName("degreesOfFreedom")]
|
||||
public int DegreesOfFreedom { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the validated per-joint kinematic limits.
|
||||
/// </summary>
|
||||
[JsonPropertyName("jointLimits")]
|
||||
public IReadOnlyList<JointLimit> JointLimits { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets optional joint coupling metadata parsed from the robot model.
|
||||
/// </summary>
|
||||
[JsonPropertyName("jointCouplings")]
|
||||
public IReadOnlyList<JointCoupling> JointCouplings { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the servo scheduling period used by the runtime.
|
||||
/// </summary>
|
||||
[JsonPropertyName("servoPeriod")]
|
||||
public TimeSpan ServoPeriod { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the trigger scheduling period used by shot-event alignment.
|
||||
/// </summary>
|
||||
[JsonPropertyName("triggerPeriod")]
|
||||
public TimeSpan TriggerPeriod { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes a single revolute joint limit set required by the planners.
|
||||
/// </summary>
|
||||
public sealed class JointLimit
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a validated joint limit record.
|
||||
/// </summary>
|
||||
public JointLimit(string jointName, double velocityLimit, double accelerationLimit, double jerkLimit)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(jointName))
|
||||
{
|
||||
throw new ArgumentException("Joint name is required.", nameof(jointName));
|
||||
}
|
||||
|
||||
if (velocityLimit <= 0.0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(velocityLimit), "Velocity limit must be positive.");
|
||||
}
|
||||
|
||||
if (accelerationLimit <= 0.0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(accelerationLimit), "Acceleration limit must be positive.");
|
||||
}
|
||||
|
||||
if (jerkLimit <= 0.0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(jerkLimit), "Jerk limit must be positive.");
|
||||
}
|
||||
|
||||
JointName = jointName;
|
||||
VelocityLimit = velocityLimit;
|
||||
AccelerationLimit = accelerationLimit;
|
||||
JerkLimit = jerkLimit;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the joint name associated with the limits.
|
||||
/// </summary>
|
||||
[JsonPropertyName("jointName")]
|
||||
public string JointName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the velocity limit in joint space units.
|
||||
/// </summary>
|
||||
[JsonPropertyName("velocityLimit")]
|
||||
public double VelocityLimit { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the acceleration limit in joint space units.
|
||||
/// </summary>
|
||||
[JsonPropertyName("accelerationLimit")]
|
||||
public double AccelerationLimit { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the jerk limit in joint space units.
|
||||
/// </summary>
|
||||
[JsonPropertyName("jerkLimit")]
|
||||
public double JerkLimit { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes a joint-coupling rule that must be applied before kinematics or planning.
|
||||
/// </summary>
|
||||
public sealed class JointCoupling
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a validated joint-coupling description.
|
||||
/// </summary>
|
||||
public JointCoupling(string slaveJointName, string masterJointName, double multiplier, double offset)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(slaveJointName))
|
||||
{
|
||||
throw new ArgumentException("Slave joint name is required.", nameof(slaveJointName));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(masterJointName))
|
||||
{
|
||||
throw new ArgumentException("Master joint name is required.", nameof(masterJointName));
|
||||
}
|
||||
|
||||
if (string.Equals(slaveJointName, masterJointName, StringComparison.Ordinal))
|
||||
{
|
||||
throw new ArgumentException("Slave and master joints must be different.");
|
||||
}
|
||||
|
||||
SlaveJointName = slaveJointName;
|
||||
MasterJointName = masterJointName;
|
||||
Multiplier = multiplier;
|
||||
Offset = offset;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the dependent joint name.
|
||||
/// </summary>
|
||||
[JsonPropertyName("slaveJointName")]
|
||||
public string SlaveJointName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source joint name.
|
||||
/// </summary>
|
||||
[JsonPropertyName("masterJointName")]
|
||||
public string MasterJointName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the coupling multiplier applied to the master joint angle.
|
||||
/// </summary>
|
||||
[JsonPropertyName("multiplier")]
|
||||
public double Multiplier { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the additive offset applied after the multiplier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("offset")]
|
||||
public double Offset { get; }
|
||||
}
|
||||
Reference in New Issue
Block a user