Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
583a7d00ca | ||
|
|
8e8af847db |
@@ -87,4 +87,6 @@ _ReSharper*/
|
||||
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
*.sqlite3
|
||||
|
||||
*.csv
|
||||
@@ -21,9 +21,6 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="CommonUsage">
|
||||
<HintPath>ref\CommonUsage.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="LessokajiWeaverUtilities">
|
||||
<HintPath>ref\LessokajiWeaverUtilities.dll</HintPath>
|
||||
</Reference>
|
||||
@@ -39,6 +36,9 @@
|
||||
<Reference Include="FundamentalLib">
|
||||
<HintPath>ref\RefFundamentalLib.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="CommonUsage">
|
||||
<HintPath>..\ref\CommonUsage.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -2,125 +2,619 @@ using ClumsyCore;
|
||||
using ClumsyCore.DTools;
|
||||
using ClumsyCore.Interfaces;
|
||||
using ClumsyCore.Pilot;
|
||||
using CommonUsage.Chassis;
|
||||
using MDCSToolBox.Commons.Controllers;
|
||||
using MDCSToolBox.Clumsy.Tracks;
|
||||
using MyParking.Shared;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using System.Threading;
|
||||
|
||||
namespace MultiWheelC
|
||||
{
|
||||
public abstract class DstTrackerTestBase : MovementTest
|
||||
internal static class MovementTestPreparation
|
||||
{
|
||||
public bool UseInteractivePick = true;
|
||||
public float srcX;
|
||||
public float srcY;
|
||||
public float dstX;
|
||||
public float dstY;
|
||||
public float carDirectionBias;
|
||||
|
||||
private readonly Painter _painter = UI.GetPainter("DstTrackerTest");
|
||||
private DriveTask _dt;
|
||||
|
||||
protected DstTrackerTestBase(float defaultCarDirectionBias)
|
||||
// 在测试正式开始前,将四个舵轮稳定回正到车体前向。
|
||||
public static bool AlignWheelsForward(
|
||||
ref DriveTask activeTask)
|
||||
{
|
||||
carDirectionBias = defaultCarDirectionBias;
|
||||
}
|
||||
var preparation = new PrepareWheelsForward();
|
||||
var task = new DriveTask(preparation.Get());
|
||||
activeTask = task;
|
||||
|
||||
public override void TestStop()
|
||||
{
|
||||
_dt?.Stop();
|
||||
_painter?.Clear();
|
||||
}
|
||||
|
||||
public override void Test()
|
||||
{
|
||||
Vector2 p1;
|
||||
Vector2 p2;
|
||||
if (UseInteractivePick)
|
||||
try
|
||||
{
|
||||
p1 = UI.GetPoint("point1");
|
||||
p2 = UI.GetPoint("point2");
|
||||
task.Wait();
|
||||
return preparation.Completed;
|
||||
}
|
||||
else
|
||||
{
|
||||
p1 = new Vector2(srcX, srcY);
|
||||
p2 = new Vector2(dstX, dstY);
|
||||
}
|
||||
|
||||
_painter.Clear();
|
||||
_dt = new DriveTask(new DstTracker
|
||||
{
|
||||
Src = p1,
|
||||
Dst = p2,
|
||||
CarDirectionBias = carDirectionBias,
|
||||
}.Get());
|
||||
_dt.Wait();
|
||||
}
|
||||
}
|
||||
|
||||
[MovementTest(name = "测试终点跟踪动作-前进")]
|
||||
public sealed class DstTrackerForward : DstTrackerTestBase
|
||||
{
|
||||
public DstTrackerForward() : base(0f) { }
|
||||
}
|
||||
|
||||
[MovementTest(name = "测试终点跟踪动作-后退")]
|
||||
public sealed class DstTrackerBackward : DstTrackerTestBase
|
||||
{
|
||||
public DstTrackerBackward() : base(180f) { }
|
||||
}
|
||||
|
||||
[MovementTest(name = "底盘旋转测试")]
|
||||
public class RotateToAngleTest : MovementTest
|
||||
{
|
||||
private DriveTask _dt;
|
||||
|
||||
// 停止当前正在执行的底盘原地旋转任务。
|
||||
public override void TestStop()
|
||||
{
|
||||
_dt?.Stop();
|
||||
}
|
||||
|
||||
// 交互输入目标角度后执行底盘原地旋转测试。
|
||||
public override void Test()
|
||||
{
|
||||
var input = UI.GetInput("输入旋转角度:");
|
||||
if (!float.TryParse(input, out var angleTarget))
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"旋转测试输入无效:{input}");
|
||||
$"测试前舵轮回正失败:{ex.Message}");
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
task.Stop();
|
||||
if (ReferenceEquals(activeTask, task))
|
||||
activeTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 只读取实际舵角,检查四个舵轮是否已与车头方向一致。
|
||||
public static bool AreWheelsForward(
|
||||
float toleranceDegrees = 2f)
|
||||
{
|
||||
var chassis =
|
||||
PilotDefinition.Chassis as MultiWheelChassis;
|
||||
if (chassis == null)
|
||||
{
|
||||
Console.WriteLine(
|
||||
"当前底盘不是MultiWheelChassis,无法检查舵轮方向。");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var adapter = new MultiWheelChassisAdapter(
|
||||
chassis,
|
||||
PilotDefinition.Self.CarNum);
|
||||
var toleranceRadians =
|
||||
toleranceDegrees * Math.PI / 180.0;
|
||||
|
||||
if (adapter.AreParallelWheelsAligned(
|
||||
0.0,
|
||||
toleranceRadians))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
Console.WriteLine(
|
||||
"四个舵轮尚未与车头方向一致,请先执行“准备:四个舵轮与车头方向一致”。");
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"检查舵轮方向失败:{ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[MovementTest(name = "准备:四个舵轮与车头方向一致")]
|
||||
public class AlignWheelsForwardTest : MovementTest
|
||||
{
|
||||
private DriveTask _task;
|
||||
|
||||
// 单独将四个舵轮转到车体前向0°并等待实际反馈稳定到位。
|
||||
public override void Test()
|
||||
{
|
||||
MovementTestPreparation.AlignWheelsForward(
|
||||
ref _task);
|
||||
}
|
||||
|
||||
// 停止正在执行的舵轮回正任务并清零底盘运动命令。
|
||||
public override void TestStop()
|
||||
{
|
||||
_task?.Stop();
|
||||
_task = null;
|
||||
}
|
||||
}
|
||||
|
||||
[MovementTest(name = "测试连续前进4m")]
|
||||
public class TestForward4m : MovementTest
|
||||
{
|
||||
public float DistanceMillimeters = 4000f; // 测试距离,单位mm。
|
||||
public float CruiseSpeed = 0.3f; // 巡航速度上限,单位m/s。
|
||||
public int TrialNumber = 1; // 重复实验编号。
|
||||
private DriveTask _task;
|
||||
private TrackingExperimentRecorder _recorder;
|
||||
// 从当前Detour位置沿车头方向生成4m连续直线并记录测试数据。
|
||||
public override void Test()
|
||||
{
|
||||
if (!MovementTestPreparation.AreWheelsForward())
|
||||
{
|
||||
return;
|
||||
}
|
||||
// 防止重复启动测试时,上一项旋转任务仍在运行。
|
||||
_dt?.Stop();
|
||||
var task = new DriveTask(
|
||||
new MultiWheelRotateInPlace
|
||||
{
|
||||
AngleTarget = angleTarget,
|
||||
|
||||
PidparamsRead = () => new PIDParams
|
||||
{
|
||||
Kp = PilotDefinition.Conf.InPlaceRotateKp,
|
||||
Ki = PilotDefinition.Conf.InPlaceRotateKi,
|
||||
Kd = PilotDefinition.Conf.InPlaceRotateKd,
|
||||
DeadZone = PilotDefinition.Conf.InPlaceRotateArriveDeg,
|
||||
SpeedAccPerSec = PilotDefinition.Conf.InPlaceRotateAcc,
|
||||
OutputUpperThreshold = PilotDefinition.Conf.InPlaceRotateMaxSpeed,
|
||||
MaxI = PilotDefinition.Conf.InPlaceRotateMaxI,
|
||||
}
|
||||
var location = DetourInterface.getCartLocation();
|
||||
if (double.IsNaN(location.x) ||
|
||||
double.IsInfinity(location.x) ||
|
||||
double.IsNaN(location.y) ||
|
||||
double.IsInfinity(location.y) ||
|
||||
double.IsNaN(location.th) ||
|
||||
double.IsInfinity(location.th))
|
||||
{
|
||||
Console.WriteLine(
|
||||
"Detour当前位姿无效,取消连续前进4m测试。");
|
||||
return;
|
||||
}
|
||||
var source = new Vector2((float)location.x, (float)location.y);
|
||||
// Detour航向单位是度,三角函数需要弧度。
|
||||
var headingRadians = location.th * Math.PI / 180.0;
|
||||
var destination = new Vector2(
|
||||
source.X + DistanceMillimeters * (float)Math.Cos(headingRadians),
|
||||
source.Y + DistanceMillimeters * (float)Math.Sin(headingRadians));
|
||||
_recorder =
|
||||
new TrackingExperimentRecorder(
|
||||
controllerName: "Stanley",
|
||||
trajectoryName: "Straight4m",
|
||||
trialNumber: TrialNumber,
|
||||
referenceStart: source,
|
||||
referenceEnd: destination,
|
||||
referenceSpeed: CruiseSpeed);
|
||||
_recorder.Start();
|
||||
try
|
||||
{
|
||||
_task = new DriveTask(
|
||||
new DstTracker
|
||||
{
|
||||
Src = source,
|
||||
Dst = destination,
|
||||
CarDirectionBias = 0f,
|
||||
MaxSpeed = CruiseSpeed
|
||||
}.Get());
|
||||
_dt = task;
|
||||
_task.Wait();
|
||||
// 保留少量停车后数据,便于观察速度是否回到零。
|
||||
Thread.Sleep(300);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_task?.Stop();
|
||||
_recorder?.UpdateCommand(0f, 0f);
|
||||
_recorder?.StopAndSave();
|
||||
_task = null;
|
||||
_recorder = null;
|
||||
}
|
||||
}
|
||||
|
||||
public override void TestStop()
|
||||
{
|
||||
_task?.Stop();
|
||||
_recorder?.UpdateCommand(0f, 0f);
|
||||
_recorder?.StopAndSave();
|
||||
}
|
||||
}
|
||||
|
||||
[MovementTest(name = "测试原地自转90°")]
|
||||
public class TestRotate90 : MovementTest
|
||||
{
|
||||
public float RelativeAngleDegrees = 90f; // 相对当前航向的旋转角度,逆时针为正。
|
||||
public float MaxAngularSpeedDegreesPerSecond = 20f; // PID输出的最大角速度。
|
||||
public int TrialNumber = 1; // 重复实验编号。
|
||||
|
||||
private DriveTask _task;
|
||||
private TrackingExperimentRecorder _recorder;
|
||||
|
||||
// 从当前Detour航向开始,原地相对旋转指定角度并记录实验数据。
|
||||
public override void Test()
|
||||
{
|
||||
if (float.IsNaN(RelativeAngleDegrees) ||
|
||||
float.IsInfinity(RelativeAngleDegrees) ||
|
||||
float.IsNaN(MaxAngularSpeedDegreesPerSecond) ||
|
||||
float.IsInfinity(MaxAngularSpeedDegreesPerSecond) ||
|
||||
MaxAngularSpeedDegreesPerSecond <= 0f)
|
||||
{
|
||||
Console.WriteLine("原地旋转测试参数无效。");
|
||||
return;
|
||||
}
|
||||
|
||||
var location = DetourInterface.getCartLocation();
|
||||
if (double.IsNaN(location.x) ||
|
||||
double.IsInfinity(location.x) ||
|
||||
double.IsNaN(location.y) ||
|
||||
double.IsInfinity(location.y) ||
|
||||
double.IsNaN(location.th) ||
|
||||
double.IsInfinity(location.th))
|
||||
{
|
||||
Console.WriteLine(
|
||||
"Detour当前位姿无效,取消原地旋转测试。");
|
||||
return;
|
||||
}
|
||||
|
||||
var rotationCenter =
|
||||
new Vector2((float)location.x, (float)location.y);
|
||||
var targetWorldAngle =
|
||||
NormalizeDegrees(
|
||||
(float)location.th + RelativeAngleDegrees);
|
||||
|
||||
_recorder = new TrackingExperimentRecorder(
|
||||
controllerName: "InPlaceRotatePID",
|
||||
trajectoryName: "Rotate90",
|
||||
trialNumber: TrialNumber,
|
||||
referenceStart: rotationCenter,
|
||||
referenceEnd: rotationCenter,
|
||||
referenceSpeed:
|
||||
MaxAngularSpeedDegreesPerSecond);
|
||||
_recorder.Start();
|
||||
|
||||
try
|
||||
{
|
||||
_task = new DriveTask(
|
||||
new MultiWheelRotateInPlace
|
||||
{
|
||||
// MultiWheelRotateInPlace接收世界坐标系绝对航向。
|
||||
AngleTarget = targetWorldAngle,
|
||||
PidparamsRead = () => new PIDParams
|
||||
{
|
||||
Kp =
|
||||
PilotDefinition.Conf.InPlaceRotateKp,
|
||||
Ki =
|
||||
PilotDefinition.Conf.InPlaceRotateKi,
|
||||
Kd =
|
||||
PilotDefinition.Conf.InPlaceRotateKd,
|
||||
DeadZone =
|
||||
PilotDefinition.Conf
|
||||
.InPlaceRotateArriveDeg,
|
||||
SpeedAccPerSec =
|
||||
PilotDefinition.Conf.InPlaceRotateAcc,
|
||||
OutputUpperThreshold =
|
||||
MaxAngularSpeedDegreesPerSecond,
|
||||
MaxI =
|
||||
PilotDefinition.Conf.InPlaceRotateMaxI
|
||||
},
|
||||
CommandAngularSpeedObserver =
|
||||
commandAngularSpeed =>
|
||||
_recorder?.UpdateCommand(
|
||||
0f,
|
||||
commandAngularSpeed)
|
||||
}.Get());
|
||||
|
||||
_task.Wait();
|
||||
|
||||
// 保留少量停止后的样本,用于观察角速度是否回到零。
|
||||
Thread.Sleep(300);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_task?.Stop();
|
||||
_recorder?.UpdateCommand(0f, 0f);
|
||||
_recorder?.StopAndSave();
|
||||
_task = null;
|
||||
_recorder = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 停止原地旋转并保存当前已经采集的实验数据。
|
||||
public override void TestStop()
|
||||
{
|
||||
_task?.Stop();
|
||||
_recorder?.UpdateCommand(0f, 0f);
|
||||
_recorder?.StopAndSave();
|
||||
}
|
||||
|
||||
// 将世界航向归一化到大约[-180°,180°]。
|
||||
private static float NormalizeDegrees(float angleDegrees)
|
||||
{
|
||||
return (float)(
|
||||
angleDegrees -
|
||||
Math.Round(angleDegrees / 360.0) * 360.0);
|
||||
}
|
||||
}
|
||||
|
||||
[MovementTest(name = "测试左转90°圆弧")]
|
||||
public class TestArcMovement : MovementTest
|
||||
{
|
||||
public float RadiusMillimeters = 2000f; // 左转圆的半径,单位mm。
|
||||
public float CruiseSpeed = 0.3f; // 圆周运动速度上限,单位m/s。
|
||||
public int TrialNumber = 1; // 重复实验编号。
|
||||
|
||||
private DriveTask _task;
|
||||
private TrackingExperimentRecorder _recorder;
|
||||
|
||||
// 从当前位姿开始,沿半径2m的圆弧向左转弯90°。
|
||||
public override void Test()
|
||||
{
|
||||
if (float.IsNaN(RadiusMillimeters) ||
|
||||
float.IsInfinity(RadiusMillimeters) ||
|
||||
RadiusMillimeters <= 0f ||
|
||||
float.IsNaN(CruiseSpeed) ||
|
||||
float.IsInfinity(CruiseSpeed) ||
|
||||
CruiseSpeed <= 0f)
|
||||
{
|
||||
Console.WriteLine("圆弧运动测试参数无效。");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!MovementTestPreparation.AreWheelsForward())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var location = DetourInterface.getCartLocation();
|
||||
if (double.IsNaN(location.x) ||
|
||||
double.IsInfinity(location.x) ||
|
||||
double.IsNaN(location.y) ||
|
||||
double.IsInfinity(location.y) ||
|
||||
double.IsNaN(location.th) ||
|
||||
double.IsInfinity(location.th))
|
||||
{
|
||||
Console.WriteLine(
|
||||
"Detour当前位姿无效,取消圆弧运动测试。");
|
||||
return;
|
||||
}
|
||||
|
||||
var source =
|
||||
new Vector2((float)location.x, (float)location.y);
|
||||
var headingRadians =
|
||||
location.th * Math.PI / 180.0;
|
||||
|
||||
// 根据世界航向求车体左法向,左转圆心位于车辆左侧。
|
||||
var center = new Vector2(
|
||||
source.X -
|
||||
RadiusMillimeters *
|
||||
(float)Math.Sin(headingRadians),
|
||||
source.Y +
|
||||
RadiusMillimeters *
|
||||
(float)Math.Cos(headingRadians));
|
||||
|
||||
// 从圆心指向车辆起点的极角,比车辆切线航向小90°。
|
||||
var startRadialAngleDegrees =
|
||||
(float)location.th - 90f;
|
||||
|
||||
var controller = new ChassisController
|
||||
{
|
||||
BaseSpeed = CruiseSpeed
|
||||
}.Get();
|
||||
controller.FinishSpeed = 0f;
|
||||
|
||||
var arc = new CircularArcTrack(
|
||||
center,
|
||||
RadiusMillimeters,
|
||||
startRadialAngleDegrees,
|
||||
startRadialAngleDegrees + 90f,
|
||||
direction: 1)
|
||||
{
|
||||
Speed = CruiseSpeed,
|
||||
CarDirectionBias = 0f
|
||||
};
|
||||
|
||||
if (!controller.AddTrack(arc, "LeftArc90Degrees"))
|
||||
{
|
||||
Console.WriteLine(
|
||||
"左转90°圆弧轨迹添加失败,取消测试。");
|
||||
return;
|
||||
}
|
||||
|
||||
_recorder = new TrackingExperimentRecorder(
|
||||
controllerName: "GeometricController",
|
||||
trajectoryName:
|
||||
$"LeftArc90_R{RadiusMillimeters:0}mm",
|
||||
trialNumber: TrialNumber,
|
||||
referenceStart: source,
|
||||
referenceEnd: source,
|
||||
referenceSpeed: CruiseSpeed);
|
||||
_recorder.Start();
|
||||
|
||||
try
|
||||
{
|
||||
_task = new DriveTask(controller.Track());
|
||||
_task.Wait();
|
||||
|
||||
// 保留少量停车后的样本,用于观察速度是否回到零。
|
||||
Thread.Sleep(300);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_task?.Stop();
|
||||
_recorder?.UpdateCommand(0f, 0f);
|
||||
_recorder?.StopAndSave();
|
||||
_task = null;
|
||||
_recorder = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 停止圆弧运动并保存当前已经采集的实验数据。
|
||||
public override void TestStop()
|
||||
{
|
||||
_task?.Stop();
|
||||
_recorder?.UpdateCommand(0f, 0f);
|
||||
_recorder?.StopAndSave();
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class ClampMovementTestBase : MovementTest
|
||||
{
|
||||
public float TimeoutSeconds = 30f; // 动作超时时间,单位s。
|
||||
|
||||
private DriveTask _task;
|
||||
protected abstract bool Close { get; }
|
||||
|
||||
// 根据派生测试类型驱动左右夹臂同步夹紧或打开。
|
||||
public override void Test()
|
||||
{
|
||||
var leftTarget = Close
|
||||
? PilotDefinition.Self.LeftArmUpperPos
|
||||
: PilotDefinition.Self.LeftArmLowerPos;
|
||||
var rightTarget = Close
|
||||
? PilotDefinition.Self.RightArmUpperPos
|
||||
: PilotDefinition.Self.RightArmLowerPos;
|
||||
|
||||
if (float.IsNaN(leftTarget) ||
|
||||
float.IsInfinity(leftTarget) ||
|
||||
float.IsNaN(rightTarget) ||
|
||||
float.IsInfinity(rightTarget))
|
||||
{
|
||||
Console.WriteLine(
|
||||
"夹臂目标位置无效,取消夹臂运动测试。");
|
||||
return;
|
||||
}
|
||||
|
||||
// 防止重复点击时上一项夹臂任务仍在运行。
|
||||
TestStop();
|
||||
Console.WriteLine(
|
||||
$"开始夹臂{(Close ? "夹紧" : "打开")}测试:" +
|
||||
$"左目标={leftTarget},右目标={rightTarget}");
|
||||
|
||||
var task = new DriveTask(
|
||||
new ClampToTarget
|
||||
{
|
||||
LeftClampTarget = leftTarget,
|
||||
RightClampTarget = rightTarget,
|
||||
TimeoutSeconds = TimeoutSeconds
|
||||
}.Get());
|
||||
_task = task;
|
||||
|
||||
try
|
||||
{
|
||||
task.Wait();
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 防止旧任务结束时,错误清除后来启动的新任务。
|
||||
if (ReferenceEquals(_dt, task))
|
||||
{
|
||||
_dt = null;
|
||||
}
|
||||
PilotDefinition.Self.SpeedLeftArm = 0f;
|
||||
PilotDefinition.Self.SpeedRightArm = 0f;
|
||||
if (ReferenceEquals(_task, task))
|
||||
_task = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 停止夹臂任务并立即清零左右夹臂下发速度。
|
||||
public override void TestStop()
|
||||
{
|
||||
_task?.Stop();
|
||||
_task = null;
|
||||
PilotDefinition.Self.SpeedLeftArm = 0f;
|
||||
PilotDefinition.Self.SpeedRightArm = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
[MovementTest(name = "夹臂关闭测试")]
|
||||
public sealed class TestClampOpenMovement
|
||||
: ClampMovementTestBase
|
||||
{
|
||||
protected override bool Close => false;
|
||||
}
|
||||
|
||||
[MovementTest(name = "夹臂启动测试")]
|
||||
public sealed class TestClampCloseMovement
|
||||
: ClampMovementTestBase
|
||||
{
|
||||
protected override bool Close => true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#region 旧版测试
|
||||
// public abstract class DstTrackerTestBase : MovementTest
|
||||
// {
|
||||
// public bool UseInteractivePick = true;
|
||||
// public float srcX;
|
||||
// public float srcY;
|
||||
// public float dstX;
|
||||
// public float dstY;
|
||||
// public float carDirectionBias;
|
||||
|
||||
// private readonly Painter _painter = UI.GetPainter("DstTrackerTest");
|
||||
// private DriveTask _dt;
|
||||
|
||||
// protected DstTrackerTestBase(float defaultCarDirectionBias)
|
||||
// {
|
||||
// carDirectionBias = defaultCarDirectionBias;
|
||||
// }
|
||||
|
||||
// public override void TestStop()
|
||||
// {
|
||||
// _dt?.Stop();
|
||||
// _painter?.Clear();
|
||||
// }
|
||||
|
||||
// public override void Test()
|
||||
// {
|
||||
// Vector2 p1;
|
||||
// Vector2 p2;
|
||||
// if (UseInteractivePick)
|
||||
// {
|
||||
// p1 = UI.GetPoint("point1");
|
||||
// p2 = UI.GetPoint("point2");
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// p1 = new Vector2(srcX, srcY);
|
||||
// p2 = new Vector2(dstX, dstY);
|
||||
// }
|
||||
|
||||
// _painter.Clear();
|
||||
// _dt = new DriveTask(new DstTracker
|
||||
// {
|
||||
// Src = p1,
|
||||
// Dst = p2,
|
||||
// CarDirectionBias = carDirectionBias,
|
||||
// }.Get());
|
||||
// _dt.Wait();
|
||||
// }
|
||||
// }
|
||||
|
||||
// [MovementTest(name = "测试终点跟踪动作-前进")]
|
||||
// public sealed class DstTrackerForward : DstTrackerTestBase
|
||||
// {
|
||||
// public DstTrackerForward() : base(0f) { }
|
||||
// }
|
||||
|
||||
// [MovementTest(name = "测试终点跟踪动作-后退")]
|
||||
// public sealed class DstTrackerBackward : DstTrackerTestBase
|
||||
// {
|
||||
// public DstTrackerBackward() : base(180f) { }
|
||||
// }
|
||||
|
||||
// [MovementTest(name = "底盘旋转测试")]
|
||||
// public class RotateToAngleTest : MovementTest
|
||||
// {
|
||||
// private DriveTask _dt;
|
||||
|
||||
// // 停止当前正在执行的底盘原地旋转任务。
|
||||
// public override void TestStop()
|
||||
// {
|
||||
// _dt?.Stop();
|
||||
// }
|
||||
|
||||
// // 交互输入目标角度后执行底盘原地旋转测试。
|
||||
// public override void Test()
|
||||
// {
|
||||
// var input = UI.GetInput("输入旋转角度:");
|
||||
// if (!float.TryParse(input, out var angleTarget))
|
||||
// {
|
||||
// Console.WriteLine(
|
||||
// $"旋转测试输入无效:{input}");
|
||||
// return;
|
||||
// }
|
||||
// // 防止重复启动测试时,上一项旋转任务仍在运行。
|
||||
// _dt?.Stop();
|
||||
// var task = new DriveTask(
|
||||
// new MultiWheelRotateInPlace
|
||||
// {
|
||||
// AngleTarget = angleTarget,
|
||||
|
||||
// PidparamsRead = () => new PIDParams
|
||||
// {
|
||||
// Kp = PilotDefinition.Conf.InPlaceRotateKp,
|
||||
// Ki = PilotDefinition.Conf.InPlaceRotateKi,
|
||||
// Kd = PilotDefinition.Conf.InPlaceRotateKd,
|
||||
// DeadZone = PilotDefinition.Conf.InPlaceRotateArriveDeg,
|
||||
// SpeedAccPerSec = PilotDefinition.Conf.InPlaceRotateAcc,
|
||||
// OutputUpperThreshold = PilotDefinition.Conf.InPlaceRotateMaxSpeed,
|
||||
// MaxI = PilotDefinition.Conf.InPlaceRotateMaxI,
|
||||
// }
|
||||
// }.Get());
|
||||
// _dt = task;
|
||||
// try
|
||||
// {
|
||||
// task.Wait();
|
||||
// }
|
||||
// finally
|
||||
// {
|
||||
// // 防止旧任务结束时,错误清除后来启动的新任务。
|
||||
// if (ReferenceEquals(_dt, task))
|
||||
// {
|
||||
// _dt = null;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -11,44 +11,92 @@ using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Numerics;
|
||||
using System.Threading;
|
||||
using FundamentalLib;
|
||||
using MyParking.Shared;
|
||||
|
||||
namespace MultiWheelC
|
||||
{
|
||||
public class DstTracker : MovementDefinition
|
||||
// C层测试准备:停车并等待四个舵轮稳定回到车体前向0°。
|
||||
public class PrepareWheelsForward : MovementDefinition
|
||||
{
|
||||
public Vector2 Src;
|
||||
public Vector2 Dst;
|
||||
public float CarDirectionBias = 0f;
|
||||
public Painter Painter = UI.GetPainter("DstTracker");
|
||||
public float ToleranceDegrees = 2f;
|
||||
public float StableSeconds = 0.3f;
|
||||
public float TimeoutSeconds = 10f;
|
||||
public bool Completed { get; private set; }
|
||||
|
||||
public override IEnumerable<bool> Get()
|
||||
{
|
||||
var chassis = (MultiWheelChassis)PilotDefinition.Chassis;
|
||||
DriveTask task = null;
|
||||
var chassis =
|
||||
PilotDefinition.Chassis as MultiWheelChassis;
|
||||
if (chassis == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"当前底盘不是MultiWheelChassis,无法执行舵轮回正。");
|
||||
}
|
||||
|
||||
var adapter = new MultiWheelChassisAdapter(
|
||||
chassis,
|
||||
PilotDefinition.Self.CarNum);
|
||||
var toleranceRadians =
|
||||
ToleranceDegrees * Math.PI / 180.0;
|
||||
var startTime = DateTime.UtcNow;
|
||||
DateTime? alignedSince = null;
|
||||
|
||||
Completed = false;
|
||||
if (!adapter.PrepareParallelDirection(0.0))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"无法将所有舵轮下发到车体前向0°。");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Console.WriteLine($"DstTracker src:({Src.X:F2}, {Src.Y:F2}) dst:({Dst.X:F2}, {Dst.Y:F2})");
|
||||
Painter.DrawLine(Color.Cyan, Src.X, Src.Y, Dst.X, Dst.Y, width: 3);
|
||||
|
||||
var tracker = new ChassisController().Get();
|
||||
var linePath = new LineTrack(Src, Dst)
|
||||
while (true)
|
||||
{
|
||||
CarDirectionBias = CarDirectionBias,
|
||||
Speed = PilotDefinition.Conf.DstTrackerMaxSpeed
|
||||
};
|
||||
tracker.AddTrack(linePath);
|
||||
task = new DriveTask(tracker.Track());
|
||||
task.Wait();
|
||||
yield return false;
|
||||
var aligned =
|
||||
adapter.AreParallelWheelsAligned(
|
||||
0.0,
|
||||
toleranceRadians);
|
||||
|
||||
if (aligned)
|
||||
{
|
||||
if (!alignedSince.HasValue)
|
||||
alignedSince = DateTime.UtcNow;
|
||||
|
||||
if ((DateTime.UtcNow -
|
||||
alignedSince.Value).TotalSeconds >=
|
||||
StableSeconds)
|
||||
{
|
||||
Completed = true;
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
alignedSince = null;
|
||||
}
|
||||
|
||||
if (TimeoutSeconds > 0f &&
|
||||
(DateTime.UtcNow - startTime).TotalSeconds >
|
||||
TimeoutSeconds)
|
||||
{
|
||||
throw new TimeoutException(
|
||||
$"舵轮回正超过{TimeoutSeconds:F1}s," +
|
||||
"测试已经取消。");
|
||||
}
|
||||
|
||||
yield return true;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
task?.Stop();
|
||||
chassis.SendXYThSpeed(0f, 0f, 0f);
|
||||
// 只清零驱动速度,保留已经下发的0°舵角。
|
||||
adapter.StopImmediately();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#region 功能项
|
||||
public class Sleep : MovementDefinition
|
||||
{
|
||||
public float Second = 2f;
|
||||
@@ -71,7 +119,303 @@ namespace MultiWheelC
|
||||
yield return false;
|
||||
}
|
||||
}
|
||||
public class DriverAble : MovementDefinition
|
||||
{
|
||||
public int WaitTimeoutMs = 2000;
|
||||
public int PollIntervalMs = 50;
|
||||
|
||||
// C层单车硬件:请求全部驱动轮复位并恢复使能。
|
||||
public override IEnumerable<bool> Get()
|
||||
{
|
||||
PilotDefinition.Self.ResetFromC = true;
|
||||
|
||||
try
|
||||
{
|
||||
var start = DateTime.Now;
|
||||
var timeoutMs = Math.Max(0, WaitTimeoutMs);
|
||||
var pollMs = Math.Max(1, PollIntervalMs);
|
||||
|
||||
// 至少保留一个调度周期,确保M层能收到复位请求。
|
||||
yield return true;
|
||||
|
||||
while (!PilotDefinition.Self.WheelAbleState &&
|
||||
(DateTime.Now - start).TotalMilliseconds < timeoutMs)
|
||||
{
|
||||
Thread.Sleep(pollMs);
|
||||
yield return true;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
PilotDefinition.Self.ResetFromC = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
public class DriverDisable : MovementDefinition
|
||||
{
|
||||
public int WaitTimeoutMs = 3000;
|
||||
public int PollIntervalMs = 20;
|
||||
|
||||
// C层单车硬件:请求驱动轮退出使能,并等待M层状态反馈。
|
||||
public override IEnumerable<bool> Get()
|
||||
{
|
||||
var timeoutMs = Math.Max(0, WaitTimeoutMs);
|
||||
var pollMs = Math.Max(1, PollIntervalMs);
|
||||
var startTime = DateTime.UtcNow;
|
||||
var success = false;
|
||||
|
||||
PilotDefinition.Self.DisableFromC = true;
|
||||
|
||||
try
|
||||
{
|
||||
// 至少保持一个C层调度周期,确保M层能收到下使能请求。
|
||||
yield return true;
|
||||
|
||||
success = !PilotDefinition.Self.WheelAbleState;
|
||||
|
||||
while (!success &&
|
||||
(DateTime.UtcNow - startTime).TotalMilliseconds <
|
||||
timeoutMs)
|
||||
{
|
||||
Thread.Sleep(pollMs);
|
||||
|
||||
success =
|
||||
!PilotDefinition.Self.WheelAbleState;
|
||||
|
||||
if (!success)
|
||||
{
|
||||
yield return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 无论正常完成、超时、异常还是任务被停止,都撤销请求。
|
||||
PilotDefinition.Self.DisableFromC = false;
|
||||
}
|
||||
if (success)
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"驱动器下使能完成," +
|
||||
$"WheelAbleState=" +
|
||||
$"{PilotDefinition.Self.WheelAbleState}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"驱动器下使能超时," +
|
||||
$"WheelAbleState=" +
|
||||
$"{PilotDefinition.Self.WheelAbleState}," +
|
||||
$"等待{timeoutMs}ms");
|
||||
}
|
||||
yield return false;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 直线运动
|
||||
//在世界坐标系下,从路径起点追踪到终点并停车
|
||||
public class DstTracker : MovementDefinition
|
||||
{
|
||||
public Vector2 Src;
|
||||
public Vector2 Dst;
|
||||
// 本次轨迹的巡航速度上限,单位m/s。
|
||||
public float MaxSpeed = PilotDefinition.Conf.DstTrackerMaxSpeed;
|
||||
public float CarDirectionBias = 0f;
|
||||
public Painter Painter = UI.GetPainter("DstTracker");
|
||||
public override IEnumerable<bool> Get()
|
||||
{
|
||||
var chassis = (MultiWheelChassis)PilotDefinition.Chassis;
|
||||
DriveTask task = null;
|
||||
try
|
||||
{
|
||||
Console.WriteLine($"DstTracker src:({Src.X:F2}, {Src.Y:F2}) dst:({Dst.X:F2}, {Dst.Y:F2})");
|
||||
Painter.DrawLine(Color.Cyan, Src.X, Src.Y, Dst.X, Dst.Y, width: 3);
|
||||
|
||||
var tracker = new ChassisController
|
||||
{
|
||||
BaseSpeed = MaxSpeed
|
||||
}.Get();
|
||||
// 要求路径末端速度下降到零。
|
||||
tracker.FinishSpeed = 0f;
|
||||
var linePath = new LineTrack(Src, Dst)
|
||||
{
|
||||
CarDirectionBias = CarDirectionBias,
|
||||
Speed = MaxSpeed
|
||||
};
|
||||
tracker.AddTrack(linePath);
|
||||
task = new DriveTask(tracker.Track());
|
||||
task.Wait();
|
||||
yield return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
task?.Stop();
|
||||
chassis.SendXYThSpeed(0f, 0f, 0f);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
//直线行走基于轮里程
|
||||
// C层单车底盘:按照车轮里程行驶指定的相对距离。
|
||||
public class LineTracking : MovementDefinition
|
||||
{
|
||||
// 相对动作启动位置的行驶距离,单位mm。
|
||||
// 正数表示前进,负数表示后退。
|
||||
public float TargetDistance;
|
||||
public float MaxSpeed = PilotDefinition.Conf.LineTrackMaxSpeed;
|
||||
public float Kp = PilotDefinition.Conf.LineTrackKp;
|
||||
public float Ki = PilotDefinition.Conf.LineTrackKi;
|
||||
public float Kd = PilotDefinition.Conf.LineTrackKd;
|
||||
public float DeadZone = PilotDefinition.Conf.LineTrackDeadZone;
|
||||
public int SrcId = -1;
|
||||
public int DstId = -1;
|
||||
public Action<int> LeaveSrcFunction;
|
||||
// 接近目标后是否保留速度,交给下一个动作接管。
|
||||
public bool EnableHandover;
|
||||
// 进入动作衔接的剩余距离,单位mm。
|
||||
public float HandoverDistance = 80f;
|
||||
// HandoverSpeed小于0时,使用MaxSpeed的此比例。
|
||||
public float HandoverSpeedRatio = 0.5f;
|
||||
// 大于等于0时,直接作为衔接速度,单位m/s。
|
||||
public float HandoverSpeed = -1f;
|
||||
public float MinHandoverSpeed = 0.05f;
|
||||
private PIDController _pid;
|
||||
// 读取当前单车直线行驶里程,单位mm。
|
||||
private static float ReadPosition()
|
||||
{
|
||||
return
|
||||
(PilotDefinition.Self.LFLActualPos + PilotDefinition.Self.LFRActualPos) / 2f;
|
||||
}
|
||||
|
||||
// 根据动作启动位置和目标距离执行直线里程闭环。
|
||||
public override IEnumerable<bool> Get()
|
||||
{
|
||||
if (float.IsNaN(TargetDistance) || float.IsInfinity(TargetDistance))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(TargetDistance),
|
||||
"目标行驶距离必须是有限值。");
|
||||
}
|
||||
|
||||
if (float.IsNaN(MaxSpeed) || float.IsInfinity(MaxSpeed) || MaxSpeed <= 0f)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(MaxSpeed),
|
||||
"最大速度必须是大于零的有限值。");
|
||||
}
|
||||
var chassis = (MultiWheelChassis)PilotDefinition.Chassis;
|
||||
// 每次启动动作时重新读取起始编码器位置。
|
||||
var startPosition = ReadPosition();
|
||||
// PID仍然控制绝对编码器位置,但绝对目标由动作自动计算。
|
||||
var targetPosition = startPosition + TargetDistance;
|
||||
_pid = new PIDController(ReadPosition, Kp, Ki, Kd, 0, DeadZone, MaxSpeed)
|
||||
{
|
||||
SpeedAccPerSec = Math.Abs(MaxSpeed) / 2f
|
||||
};
|
||||
var handoverRequested = false;
|
||||
var keepHandoverSpeed = false;
|
||||
DLog.Log(
|
||||
$"直线里程动作:" +
|
||||
$"起点={startPosition:F1}mm," +
|
||||
$"距离={TargetDistance:F1}mm," +
|
||||
$"目标={targetPosition:F1}mm",
|
||||
"straight_line");
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var currentPosition = ReadPosition();
|
||||
var remainingDistance = targetPosition - currentPosition;
|
||||
// 接近目标后,保留一定速度交给后续动作。
|
||||
if (EnableHandover && Math.Abs(remainingDistance) <= Math.Max(1f, HandoverDistance))
|
||||
{
|
||||
var direction = Math.Sign(remainingDistance);
|
||||
if (direction == 0)
|
||||
{
|
||||
direction = Math.Sign(TargetDistance);
|
||||
}
|
||||
var requestedSpeed = HandoverSpeed >= 0f ? Math.Abs(HandoverSpeed) : Math.Abs(MaxSpeed) * HandoverSpeedRatio;
|
||||
var maximumSpeed = Math.Abs(MaxSpeed);
|
||||
var minimumSpeed = Math.Min(Math.Abs(MinHandoverSpeed), maximumSpeed);
|
||||
var limitedSpeed = Math.Max(minimumSpeed, Math.Min(requestedSpeed, maximumSpeed));
|
||||
var handoverSpeed = limitedSpeed * direction;
|
||||
chassis.SendXYThSpeed(handoverSpeed, 0f, 0f);
|
||||
handoverRequested = true;
|
||||
// 保持一个调度周期,让速度命令实际生效。
|
||||
yield return true;
|
||||
break;
|
||||
}
|
||||
var speed = _pid.GetResponse(targetPosition);
|
||||
chassis.SendXYThSpeed(speed, 0f, 0f);
|
||||
if (_pid.IsArrived())
|
||||
{
|
||||
break;
|
||||
}
|
||||
yield return true;
|
||||
}
|
||||
if (SrcId != -1 &&
|
||||
LeaveSrcFunction != null)
|
||||
{
|
||||
LeaveSrcFunction(SrcId);
|
||||
DLog.Log($"释放放车点{SrcId}", "straight_line");
|
||||
}
|
||||
// 只有正常完成动作衔接时才允许保留非零速度。
|
||||
keepHandoverSpeed = handoverRequested;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 普通完成、人工停止或异常退出时都必须停车。
|
||||
if (!keepHandoverSpeed)
|
||||
{
|
||||
chassis.SendXYThSpeed(0f, 0f, 0f);
|
||||
}
|
||||
}
|
||||
yield return false;
|
||||
}
|
||||
}
|
||||
//直线行走基于detour
|
||||
public class LineTracking_based_detour : MovementDefinition
|
||||
{
|
||||
public float LineDistance = 1000f;
|
||||
public int SrcId = -1;
|
||||
public int DstId = -1;
|
||||
public Action<int> LeaveSrcFunction = null;
|
||||
public Painter painter = UI.GetPainter("Line", false);
|
||||
// C层单车轨迹:执行早期版本的两点直线跟踪动作。
|
||||
public override IEnumerable<bool> Get()
|
||||
{
|
||||
var curpose = DetourInterface.getCartLocation();
|
||||
Console.WriteLine($"curpose.th:{curpose.th}");
|
||||
var src = new Vector2((float)curpose.x, (float)curpose.y);
|
||||
var headingRadians = curpose.th * Math.PI / 180.0;
|
||||
var dst = new Vector2(
|
||||
(float)(curpose.x +
|
||||
LineDistance * Math.Cos(headingRadians)),
|
||||
(float)(curpose.y +
|
||||
LineDistance * Math.Sin(headingRadians)));
|
||||
// var dst = new Vector2((float)curpose.x + LineDistance * (float)Math.Cos(curpose.th),
|
||||
// (float)curpose.y + LineDistance * (float)Math.Sin(curpose.th));
|
||||
Console.WriteLine($"src:{src.X} {src.Y}");
|
||||
Console.WriteLine($"dst:{dst.X} {dst.Y}");
|
||||
painter.DrawLine(Color.Green, src.X, src.Y, dst.X, dst.Y, width: 3);
|
||||
|
||||
var tracker = new ChassisController().Get();
|
||||
var linePath = new LineTrack(src, dst) { CarDirectionBias = LineDistance > 0 ? 0 : 180 };
|
||||
tracker.AddTrack(linePath);
|
||||
var _dt = new DriveTask(tracker.Track());
|
||||
_dt.Wait();
|
||||
if (SrcId != -1 && LeaveSrcFunction != null)
|
||||
{
|
||||
LeaveSrcFunction(SrcId);
|
||||
DLog.Log($"释放放车点{SrcId}", "straight_line");
|
||||
}
|
||||
yield return false;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 旋转运动
|
||||
public class MultiWheelRotateInPlace : MovementDefinition
|
||||
{
|
||||
/// <summary>
|
||||
@@ -89,7 +433,10 @@ namespace MultiWheelC
|
||||
|
||||
public PIDController thPid;
|
||||
|
||||
// 将角度归一化到零到三百六十度范围内。
|
||||
// 将本周期PID角速度输出提供给实验记录器,单位deg/s。
|
||||
public Action<float> CommandAngularSpeedObserver;
|
||||
|
||||
// 归一化到大约 [-180°, 180°]
|
||||
private static float RangeAngle(float theta)
|
||||
{
|
||||
return (float)(theta - Math.Round(theta / 360.0f) * 360);
|
||||
@@ -110,6 +457,7 @@ namespace MultiWheelC
|
||||
{
|
||||
var s = thPid.GetResponse(targetAngle, true);
|
||||
Console.WriteLine($"s:{s} AngleTarget:{AngleTarget}");
|
||||
CommandAngularSpeedObserver?.Invoke(s);
|
||||
Chassis.SendXYThSpeed(0, 0, s);
|
||||
if (thPid.IsArrived()) break;
|
||||
yield return true;
|
||||
@@ -119,10 +467,96 @@ namespace MultiWheelC
|
||||
}
|
||||
finally
|
||||
{
|
||||
CommandAngularSpeedObserver?.Invoke(0f);
|
||||
Chassis.SendXYThSpeed(0, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 夹臂运动
|
||||
public class ClampToTarget : MovementDefinition
|
||||
{
|
||||
public float LeftClampTarget;
|
||||
public float RightClampTarget;
|
||||
public float MaxClampSpeed = PilotDefinition.Conf.MaxClampSpeed;
|
||||
public float ClampKp = PilotDefinition.Conf.ClampControlKp;
|
||||
public float ClampKi = PilotDefinition.Conf.ClampControlKi;
|
||||
public float ClampKd = PilotDefinition.Conf.ClampControlKd;
|
||||
public float ClampMaxI = PilotDefinition.Conf.ClampControlMaxI;
|
||||
public float ClampSpeedAcc = PilotDefinition.Conf.ClampControlSpeedAcc;
|
||||
public float ClampDeadZone = PilotDefinition.Conf.ClampControlDeadZone;
|
||||
public float TimeoutSeconds = 30f;
|
||||
private PIDController leftpid, rightpid;
|
||||
|
||||
// C层单车业务:驱动左右夹臂运动到夹紧或松开目标。
|
||||
public override IEnumerable<bool> Get()
|
||||
{
|
||||
try
|
||||
{
|
||||
leftpid = new PIDController(
|
||||
() => PilotDefinition.Self.ActualPosLeftArm,
|
||||
ClampKp, ClampKi, ClampKd, ClampMaxI,
|
||||
ClampDeadZone, MaxClampSpeed)
|
||||
{
|
||||
SpeedAccPerSec = ClampSpeedAcc
|
||||
};
|
||||
|
||||
rightpid = new PIDController(
|
||||
() => PilotDefinition.Self.ActualPosRightArm,
|
||||
ClampKp, ClampKi, ClampKd, ClampMaxI,
|
||||
ClampDeadZone, MaxClampSpeed)
|
||||
{
|
||||
SpeedAccPerSec = ClampSpeedAcc
|
||||
};
|
||||
|
||||
var startTime = DateTime.UtcNow;
|
||||
while (true)
|
||||
{
|
||||
if (TimeoutSeconds > 0f &&
|
||||
(DateTime.UtcNow - startTime).TotalSeconds >
|
||||
TimeoutSeconds)
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"夹臂运动超时({TimeoutSeconds:F1}s)," +
|
||||
"停止左右夹臂。");
|
||||
yield break;
|
||||
}
|
||||
|
||||
var leftspeed =
|
||||
leftpid.GetResponse(LeftClampTarget);
|
||||
var rightspeed =
|
||||
rightpid.GetResponse(RightClampTarget);
|
||||
Console.WriteLine(
|
||||
$"left arm speed:{leftspeed} " +
|
||||
$"right arm speed:{rightspeed}");
|
||||
|
||||
PilotDefinition.Self.SpeedLeftArm = leftspeed;
|
||||
PilotDefinition.Self.SpeedRightArm = rightspeed;
|
||||
|
||||
var leftArrived = leftpid.IsArrived();
|
||||
var rightArrived = rightpid.IsArrived();
|
||||
if (leftArrived)
|
||||
PilotDefinition.Self.SpeedLeftArm = 0f;
|
||||
if (rightArrived)
|
||||
PilotDefinition.Self.SpeedRightArm = 0f;
|
||||
|
||||
if (leftArrived && rightArrived)
|
||||
break;
|
||||
|
||||
yield return true;
|
||||
}
|
||||
|
||||
Console.WriteLine(
|
||||
$"left clamp to target:{LeftClampTarget} " +
|
||||
$"right clamp to target:{RightClampTarget}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
PilotDefinition.Self.SpeedLeftArm = 0f;
|
||||
PilotDefinition.Self.SpeedRightArm = 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ public class PilotConfig : MultiWheelPilotConfig
|
||||
|
||||
#region 单车-临时
|
||||
[FieldMember(desc = "原地旋转Kp")]
|
||||
public float InPlaceRotateKp = 0.05f;
|
||||
public float InPlaceRotateKp = 0.2f;
|
||||
|
||||
[FieldMember(desc = "原地旋转Ki")]
|
||||
public float InPlaceRotateKi = 0.01f;
|
||||
|
||||
@@ -0,0 +1,394 @@
|
||||
using ClumsyCore.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace MultiWheelC
|
||||
{
|
||||
// C层实验数据:保存一个采样时刻的定位与控制命令。
|
||||
public sealed class TrackingSample
|
||||
{
|
||||
public double ElapsedSeconds;
|
||||
|
||||
// Detour位置单位为mm,航向单位为deg。
|
||||
public double DetourX;
|
||||
public double DetourY;
|
||||
public double DetourTheta;
|
||||
|
||||
// 车体速度单位为m/s,角速度单位为deg/s。
|
||||
public float CommandSpeed;
|
||||
public float CommandVx;
|
||||
public float CommandVy;
|
||||
public float CommandAngularSpeed;
|
||||
}
|
||||
|
||||
// C层实验工具:统一采集并保存轨迹跟踪实验数据。
|
||||
public sealed class TrackingExperimentRecorder
|
||||
{
|
||||
private readonly string _controllerName;
|
||||
private readonly string _trajectoryName;
|
||||
private readonly int _trialNumber;
|
||||
private readonly Vector2 _referenceStart;
|
||||
private readonly Vector2 _referenceEnd;
|
||||
private readonly float _referenceSpeed;
|
||||
private readonly int _sampleIntervalMs;
|
||||
|
||||
private readonly List<TrackingSample> _samples =
|
||||
new List<TrackingSample>();
|
||||
|
||||
private readonly object _sampleSyncRoot =
|
||||
new object();
|
||||
|
||||
private readonly object _commandSyncRoot =
|
||||
new object();
|
||||
|
||||
private readonly Stopwatch _stopwatch =
|
||||
new Stopwatch();
|
||||
|
||||
private Thread _worker;
|
||||
private volatile bool _running;
|
||||
private int _started;
|
||||
private int _saved;
|
||||
|
||||
private bool _hasExternalCommand;
|
||||
private float _externalCommandSpeed;
|
||||
private float _externalCommandVx;
|
||||
private float _externalCommandVy;
|
||||
private float _externalCommandAngularSpeed;
|
||||
|
||||
public TrackingExperimentRecorder(
|
||||
string controllerName,
|
||||
string trajectoryName,
|
||||
int trialNumber,
|
||||
Vector2 referenceStart,
|
||||
Vector2 referenceEnd,
|
||||
float referenceSpeed,
|
||||
int sampleIntervalMs = 50)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(controllerName))
|
||||
throw new ArgumentException(
|
||||
"控制器名称不能为空。",
|
||||
nameof(controllerName));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(trajectoryName))
|
||||
throw new ArgumentException(
|
||||
"轨迹名称不能为空。",
|
||||
nameof(trajectoryName));
|
||||
|
||||
if (sampleIntervalMs <= 0)
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(sampleIntervalMs),
|
||||
"采样周期必须大于零。");
|
||||
|
||||
_controllerName = controllerName;
|
||||
_trajectoryName = trajectoryName;
|
||||
_trialNumber = trialNumber;
|
||||
_referenceStart = referenceStart;
|
||||
_referenceEnd = referenceEnd;
|
||||
_referenceSpeed = referenceSpeed;
|
||||
_sampleIntervalMs = sampleIntervalMs;
|
||||
}
|
||||
|
||||
// 保存成功后的CSV绝对路径;尚未保存时为空。
|
||||
public string SavedFilePath { get; private set; }
|
||||
|
||||
// 启动后台采样线程。
|
||||
public void Start()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _started, 1) != 0)
|
||||
return;
|
||||
|
||||
_stopwatch.Restart();
|
||||
_running = true;
|
||||
|
||||
// 立即保存起点静止状态,避免第一帧被后台线程延迟。
|
||||
CaptureSample();
|
||||
|
||||
_worker = new Thread(SamplingLoop)
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "TrackingExperimentRecorder"
|
||||
};
|
||||
_worker.Start();
|
||||
}
|
||||
|
||||
// 供Stanley/LQR控制器主动写入本周期最终速度命令。
|
||||
// 调用后优先记录该命令,不再使用底盘反解值。
|
||||
public void UpdateCommand(
|
||||
float commandSpeed,
|
||||
float commandAngularSpeed)
|
||||
{
|
||||
lock (_commandSyncRoot)
|
||||
{
|
||||
_externalCommandSpeed = commandSpeed;
|
||||
_externalCommandVx = commandSpeed;
|
||||
_externalCommandVy = 0f;
|
||||
_externalCommandAngularSpeed =
|
||||
commandAngularSpeed;
|
||||
_hasExternalCommand = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 供全向、蟹行和曲线控制器写入完整车体速度命令。
|
||||
public void UpdateBodyCommand(
|
||||
float commandVx,
|
||||
float commandVy,
|
||||
float commandAngularSpeed)
|
||||
{
|
||||
lock (_commandSyncRoot)
|
||||
{
|
||||
_externalCommandVx = commandVx;
|
||||
_externalCommandVy = commandVy;
|
||||
_externalCommandSpeed =
|
||||
(float)Math.Sqrt(
|
||||
commandVx * commandVx +
|
||||
commandVy * commandVy);
|
||||
_externalCommandAngularSpeed =
|
||||
commandAngularSpeed;
|
||||
_hasExternalCommand = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 停止采样并将本次实验保存为CSV;重复调用只保存一次。
|
||||
public void StopAndSave()
|
||||
{
|
||||
if (Volatile.Read(ref _started) == 0)
|
||||
return;
|
||||
|
||||
if (Interlocked.Exchange(ref _saved, 1) != 0)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
_running = false;
|
||||
|
||||
if (_worker != null &&
|
||||
_worker != Thread.CurrentThread)
|
||||
{
|
||||
_worker.Join(
|
||||
Math.Max(1000, _sampleIntervalMs * 4));
|
||||
}
|
||||
|
||||
// 保存停止时刻的最后一帧。
|
||||
CaptureSample();
|
||||
_stopwatch.Stop();
|
||||
SaveCsv();
|
||||
|
||||
Console.WriteLine(
|
||||
$"轨迹实验数据已保存:{SavedFilePath}");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 保存失败后允许调用者再次尝试。
|
||||
Interlocked.Exchange(ref _saved, 0);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// 按固定周期采集Detour位姿和控制命令。
|
||||
private void SamplingLoop()
|
||||
{
|
||||
while (_running)
|
||||
{
|
||||
Thread.Sleep(_sampleIntervalMs);
|
||||
|
||||
if (!_running)
|
||||
break;
|
||||
|
||||
CaptureSample();
|
||||
}
|
||||
}
|
||||
|
||||
// 采集一帧Detour位姿和控制命令。
|
||||
private void CaptureSample()
|
||||
{
|
||||
try
|
||||
{
|
||||
var location =
|
||||
DetourInterface.getCartLocation();
|
||||
|
||||
float commandSpeed;
|
||||
float commandVx;
|
||||
float commandVy;
|
||||
float commandAngularSpeed;
|
||||
|
||||
lock (_commandSyncRoot)
|
||||
{
|
||||
if (_hasExternalCommand)
|
||||
{
|
||||
commandSpeed =
|
||||
_externalCommandSpeed;
|
||||
commandVx =
|
||||
_externalCommandVx;
|
||||
commandVy =
|
||||
_externalCommandVy;
|
||||
commandAngularSpeed =
|
||||
_externalCommandAngularSpeed;
|
||||
}
|
||||
else
|
||||
{
|
||||
var command =
|
||||
PilotDefinition.Chassis
|
||||
.GetCarSpeed(false);
|
||||
|
||||
commandVx = command.Vx;
|
||||
commandVy = command.Vy;
|
||||
commandAngularSpeed = command.Vw;
|
||||
commandSpeed = (float)Math.Sqrt(
|
||||
commandVx * commandVx +
|
||||
commandVy * commandVy);
|
||||
}
|
||||
}
|
||||
|
||||
var sample = new TrackingSample
|
||||
{
|
||||
ElapsedSeconds =
|
||||
_stopwatch.Elapsed.TotalSeconds,
|
||||
DetourX = location.x,
|
||||
DetourY = location.y,
|
||||
DetourTheta = location.th,
|
||||
CommandSpeed = commandSpeed,
|
||||
CommandVx = commandVx,
|
||||
CommandVy = commandVy,
|
||||
CommandAngularSpeed =
|
||||
commandAngularSpeed
|
||||
};
|
||||
|
||||
lock (_sampleSyncRoot)
|
||||
{
|
||||
_samples.Add(sample);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 单帧读取失败不应终止车辆控制或整个记录线程。
|
||||
Console.WriteLine(
|
||||
$"轨迹实验采样失败:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// 将内存中的采样数据写入CSV。
|
||||
private void SaveCsv()
|
||||
{
|
||||
List<TrackingSample> snapshot;
|
||||
|
||||
lock (_sampleSyncRoot)
|
||||
{
|
||||
snapshot =
|
||||
new List<TrackingSample>(_samples);
|
||||
}
|
||||
|
||||
var outputDirectory = Path.Combine(
|
||||
AppContext.BaseDirectory,
|
||||
"TrackingExperiments");
|
||||
|
||||
Directory.CreateDirectory(outputDirectory);
|
||||
|
||||
var fileName =
|
||||
$"{DateTime.Now:yyyyMMdd_HHmmss_fff}_" +
|
||||
$"{SanitizeFileName(_controllerName)}_" +
|
||||
$"{SanitizeFileName(_trajectoryName)}_" +
|
||||
$"Trial{_trialNumber}.csv";
|
||||
|
||||
SavedFilePath = Path.Combine(
|
||||
outputDirectory,
|
||||
fileName);
|
||||
|
||||
using (var writer = new StreamWriter(
|
||||
SavedFilePath,
|
||||
false,
|
||||
new UTF8Encoding(true)))
|
||||
{
|
||||
writer.WriteLine(
|
||||
"ElapsedSeconds," +
|
||||
"ControllerName," +
|
||||
"TrajectoryName," +
|
||||
"TrialNumber," +
|
||||
"DetourX," +
|
||||
"DetourY," +
|
||||
"DetourTheta," +
|
||||
"CommandSpeed," +
|
||||
"CommandAngularSpeed," +
|
||||
"CommandVx," +
|
||||
"CommandVy," +
|
||||
"ReferenceStartX," +
|
||||
"ReferenceStartY," +
|
||||
"ReferenceEndX," +
|
||||
"ReferenceEndY," +
|
||||
"ReferenceSpeed");
|
||||
|
||||
foreach (var sample in snapshot)
|
||||
{
|
||||
writer.WriteLine(string.Join(
|
||||
",",
|
||||
Format(sample.ElapsedSeconds),
|
||||
EscapeCsv(_controllerName),
|
||||
EscapeCsv(_trajectoryName),
|
||||
_trialNumber.ToString(
|
||||
CultureInfo.InvariantCulture),
|
||||
Format(sample.DetourX),
|
||||
Format(sample.DetourY),
|
||||
Format(sample.DetourTheta),
|
||||
Format(sample.CommandSpeed),
|
||||
Format(sample.CommandAngularSpeed),
|
||||
Format(sample.CommandVx),
|
||||
Format(sample.CommandVy),
|
||||
Format(_referenceStart.X),
|
||||
Format(_referenceStart.Y),
|
||||
Format(_referenceEnd.X),
|
||||
Format(_referenceEnd.Y),
|
||||
Format(_referenceSpeed)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 将文件名中的非法字符替换为下划线。
|
||||
private static string SanitizeFileName(string value)
|
||||
{
|
||||
var result = value;
|
||||
|
||||
foreach (var invalidCharacter in
|
||||
Path.GetInvalidFileNameChars())
|
||||
{
|
||||
result = result.Replace(
|
||||
invalidCharacter,
|
||||
'_');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 按固定小数格式输出数值,避免系统区域设置改变CSV格式。
|
||||
private static string Format(double value)
|
||||
{
|
||||
return value.ToString(
|
||||
"0.######",
|
||||
CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
// 对CSV文本字段进行引号和逗号转义。
|
||||
private static string EscapeCsv(string value)
|
||||
{
|
||||
if (value == null)
|
||||
return string.Empty;
|
||||
|
||||
if (!value.Contains(",") &&
|
||||
!value.Contains("\"") &&
|
||||
!value.Contains("\r") &&
|
||||
!value.Contains("\n"))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
return
|
||||
"\"" +
|
||||
value.Replace("\"", "\"\"") +
|
||||
"\"";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ using System.Reflection;
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("ClumsyPilot")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+580a936a830dcb7a25ef327cf553341405264033")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("ClumsyPilot")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("ClumsyPilot")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
||||
@@ -1 +1 @@
|
||||
038d57c5b1714403d308c9343b385ef762951607b63a9fb275f4d7d2b8fd29cb
|
||||
5d77794fa0720c6591db5b06ac60427413c18989a6f7b64420ccb07d122d85bc
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
is_global = true
|
||||
build_property.RootNamespace = MultiWheelC
|
||||
build_property.ProjectDir = d:\MyParking\ClumsyPilot\
|
||||
build_property.ProjectDir = D:\Users\Desktop\入职培训\停车机器人\MyParking\ClumsyPilot\
|
||||
build_property.EnableComHosting =
|
||||
build_property.EnableGeneratedComInterfaceComImportInterop =
|
||||
build_property.CsWinRTUseWindowsUIXamlProjections = false
|
||||
|
||||
@@ -1 +1 @@
|
||||
3920568398d269aea7b71fb4581ad111777c15ca3f2f2bb272ee9e0a989215c4
|
||||
e972a413d047c4137a8ce86cbff54a8d2e2558806d9d974d3d6312467ee8ba4d
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
*.cs text eol=crlf
|
||||
.gitattributes text eol=lf
|
||||
@@ -0,0 +1,362 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# ASP.NET Scaffolding
|
||||
ScaffoldingReadMe.txt
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Coverlet is a free, cross platform Code Coverage Tool
|
||||
coverage*.json
|
||||
coverage*.xml
|
||||
coverage*.info
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
@@ -0,0 +1,126 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Numerics;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using FundamentalLib;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CommonUsage.Chassis
|
||||
{
|
||||
public abstract class AbstractChassis
|
||||
{
|
||||
protected AbstractChassis()
|
||||
{
|
||||
Valid = false;
|
||||
}
|
||||
|
||||
public abstract void Initialize();
|
||||
|
||||
public abstract void Visualize();
|
||||
|
||||
public abstract void AfterDirectionChanged();
|
||||
|
||||
/// <summary>
|
||||
/// 当前行进方向。
|
||||
/// </summary>
|
||||
[Obsolete]
|
||||
public float DirectionAngle
|
||||
{
|
||||
get => _originBiasTh;
|
||||
set
|
||||
{
|
||||
_originBiasTh = value;
|
||||
if (_originBiasTh != _lastDirectionAngle) AfterDirectionChanged();
|
||||
_lastDirectionAngle = _originBiasTh;
|
||||
}
|
||||
}
|
||||
|
||||
protected float _originBiasX = 0f, _originBiasY = 0f, _originBiasTh;
|
||||
|
||||
public Vector3 GetOriginBias()
|
||||
{
|
||||
return new Vector3(_originBiasX, _originBiasY, _originBiasTh);
|
||||
}
|
||||
|
||||
public class CarSpeed
|
||||
{
|
||||
public float Vx,Vy,Vw;
|
||||
}
|
||||
|
||||
public abstract CarSpeed GetCarSpeed(bool isActual = false);
|
||||
public List<GeometricControlPoint> GetGeometricControlPoints()
|
||||
{
|
||||
return GeometricControlPoints;
|
||||
}
|
||||
|
||||
public void ComputeWheelsGeometrically(float speed)
|
||||
{
|
||||
// 打印调用位置信息
|
||||
var stackTrace = new StackTrace(true);
|
||||
var callerFrame = stackTrace.GetFrame(1); // 获取调用者的帧
|
||||
if (callerFrame != null)
|
||||
{
|
||||
var fileName = callerFrame.GetFileName();
|
||||
var lineNumber = callerFrame.GetFileLineNumber();
|
||||
DLog.Log($"s:{speed:0.000} from {fileName} ln.{lineNumber}", $"WheelComputeCaller");
|
||||
}
|
||||
|
||||
DefineGeometricWheelComputation(speed);
|
||||
}
|
||||
|
||||
protected abstract void DefineGeometricWheelComputation(float speed);
|
||||
|
||||
public void DriveStop()
|
||||
{
|
||||
PredefinedDriveStop();
|
||||
CustomDriveStop?.Invoke();
|
||||
}
|
||||
|
||||
public abstract void PredefinedDriveStop();
|
||||
|
||||
public Action CustomDriveStop;
|
||||
|
||||
public abstract bool ComputeRotateWheels(float rotSpeed);
|
||||
|
||||
public abstract float CalculateTurningSpeedDecayFac(float turn);
|
||||
|
||||
public enum ChassisState
|
||||
{
|
||||
Standby,
|
||||
Running,
|
||||
AbnormalFeedback,
|
||||
ExceedMotionAbility,
|
||||
}
|
||||
|
||||
protected ChassisState State;
|
||||
|
||||
protected string StateDescription;
|
||||
|
||||
public (ChassisState State, string Description) GetChassisState()
|
||||
{
|
||||
return (State, StateDescription);
|
||||
}
|
||||
|
||||
public bool Debug = false;
|
||||
public float AccPerSecond = 0.2f;
|
||||
public float DeAccPerSecond = 0.2f;
|
||||
public float MaxSpeed = 1; // m/s
|
||||
public float MinTurnSpeedFac = 0.5f;
|
||||
public float MaxTurnThreshold = 90f;
|
||||
|
||||
public float GcpThetaPerSecond = 10f;
|
||||
|
||||
public DateTime LastMoveTime = DateTime.MinValue;
|
||||
|
||||
protected bool Valid = false;
|
||||
protected List<GeometricControlPoint> GeometricControlPoints = new();
|
||||
protected bool RotatingActive = false;
|
||||
protected bool GoingActive = false;
|
||||
protected bool GoingWheelAligned = false;
|
||||
|
||||
private float _lastDirectionAngle = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
|
||||
namespace CommonUsage.Chassis
|
||||
{
|
||||
public class DiffSteerWheel:SteerWheel
|
||||
{
|
||||
public DiffSteerWheel(float wheelDistance,Vector2 position, float angleLowerLimit, float angleUpperLimit, Action<float> speedWriter,
|
||||
Func<float> speedReader, Action<float> angleWriter, Func<float> angleReader, Action<float> leftSpeedWriter, Action<float> rightSpeedWriter,
|
||||
float angleLimitMarginDeg = 15f) : base(position,
|
||||
angleLowerLimit, angleUpperLimit, speedWriter, speedReader, angleWriter, angleReader, angleLimitMarginDeg)
|
||||
{
|
||||
_leftSpeedWriter = leftSpeedWriter;
|
||||
_rightSpeedWriter = rightSpeedWriter;
|
||||
WheelDistance = wheelDistance;
|
||||
}
|
||||
|
||||
public float GetLeftSendSpeed()
|
||||
{
|
||||
return _leftSendSpeed;
|
||||
}
|
||||
|
||||
public float GetRightSendSpeed()
|
||||
{
|
||||
return _rightSendSpeed;
|
||||
}
|
||||
|
||||
public void WriteLeftSpeed(float speed)
|
||||
{
|
||||
_leftSpeedWriter(_leftSendSpeed = speed);
|
||||
}
|
||||
|
||||
public void WriteRightSpeed(float speed)
|
||||
{
|
||||
_rightSpeedWriter(_rightSendSpeed = speed);
|
||||
}
|
||||
|
||||
public float WheelDistance;
|
||||
private readonly Action<float> _leftSpeedWriter;
|
||||
private readonly Action<float> _rightSpeedWriter;
|
||||
|
||||
private float _leftSendSpeed;
|
||||
private float _rightSendSpeed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
using FundamentalLib;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace CommonUsage.Chassis
|
||||
{
|
||||
public class DifferentialChassis : AbstractChassis
|
||||
{
|
||||
public void SetLeftRightWheels(Wheel wheelL, Wheel wheelR)
|
||||
{
|
||||
_leftWheel = wheelL;
|
||||
_rightWheel = wheelR;
|
||||
_halfWheelTrack = Math.Abs(_leftWheel.Position.Y);
|
||||
}
|
||||
|
||||
public override void Visualize()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public override CarSpeed GetCarSpeed(bool isActual = false)
|
||||
{
|
||||
if (!isActual)
|
||||
{
|
||||
return new CarSpeed()
|
||||
{
|
||||
Vx = (_speedL + _speedR) / 2f,
|
||||
Vw = (_speedR - _speedL) / Math.Abs(_leftWheel.Position.Y - _rightWheel.Position.Y) /
|
||||
(float)Math.PI * 180f * 1000f,
|
||||
Vy = 0
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
return new CarSpeed()
|
||||
{
|
||||
Vx = GetLinearSpeed(),
|
||||
Vw = (_rightWheel.ReadSpeed() - _leftWheel.ReadSpeed()) /
|
||||
Math.Abs(_leftWheel.Position.Y - _rightWheel.Position.Y) /
|
||||
(float)Math.PI * 180f * 1000f,
|
||||
Vy = 0
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public (Wheel,Wheel) GetWheels()
|
||||
{
|
||||
return (_leftWheel, _rightWheel);
|
||||
}
|
||||
|
||||
public float GetLinearSpeed()
|
||||
{
|
||||
return (_leftWheel.ReadSpeed() + _rightWheel.ReadSpeed()) / 2f;
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
GeometricControlPoints.Add(new GeometricControlPoint(new Vector2(0, 0)));
|
||||
Valid = true;
|
||||
}
|
||||
|
||||
public override void AfterDirectionChanged()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public override void PredefinedDriveStop()
|
||||
{
|
||||
if (!Valid) return;
|
||||
_sendSpeedL = _sendSpeedR = 0;
|
||||
_speedL = _speedR = 0;
|
||||
_leftWheel.WriteSpeed(_sendSpeedL);
|
||||
_rightWheel.WriteSpeed(_sendSpeedR);
|
||||
GoingActive = false;
|
||||
RotatingActive = false;
|
||||
}
|
||||
|
||||
protected override void DefineGeometricWheelComputation(float speed)
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
if (!GoingActive) LastMoveTime = now;
|
||||
|
||||
SendSpeed(speed, GeometricControlPoints[0].Theta, now - LastMoveTime);
|
||||
|
||||
GoingActive = true;
|
||||
RotatingActive = false;
|
||||
}
|
||||
|
||||
public override bool ComputeRotateWheels(float rotSpeed)
|
||||
{
|
||||
if (!RotatingActive) LastMoveTime = DateTime.Now;
|
||||
|
||||
SendSpeed(0, rotSpeed);
|
||||
|
||||
GoingActive = false;
|
||||
RotatingActive = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public override float CalculateTurningSpeedDecayFac(float turn)
|
||||
{
|
||||
return 1 - Math.Min(turn, MaxTurnThreshold) / MaxTurnThreshold * MinTurnSpeedFac;
|
||||
}
|
||||
|
||||
public void SendSpeed(float linearSpeed, float angularSpeed, TimeSpan? deltaTime = null)
|
||||
{
|
||||
var edgeLinearSpeed = (float)(angularSpeed / 180f * Math.PI * _halfWheelTrack / 1000);
|
||||
var vl = linearSpeed - edgeLinearSpeed;
|
||||
var vr = linearSpeed + edgeLinearSpeed;
|
||||
_speedL = vl;
|
||||
_speedR = vr;
|
||||
AccumulateSpeed(vl, vr, deltaTime);
|
||||
LastMoveTime = DateTime.Now;
|
||||
}
|
||||
|
||||
private void AccumulateSpeed(float vl, float vr, TimeSpan? deltaTime = null)
|
||||
{
|
||||
// var dTime = (float)(deltaTime ?? DateTime.Now - LastMoveTime).TotalSeconds;
|
||||
//
|
||||
// var speedSignL = Math.Sign(vl - _sendSpeedL);
|
||||
// var accL = Math.Abs(vl) > Math.Abs(_sendSpeedL) ? AccPerSecond : DeAccPerSecond;
|
||||
// _sendSpeedL += speedSignL * Math.Min(Math.Abs(vl - _sendSpeedL), accL * dTime);
|
||||
// _leftWheel.WriteSpeed(_sendSpeedL);
|
||||
//
|
||||
// var speedSignR = Math.Sign(vr - _sendSpeedR);
|
||||
// var accR = Math.Abs(vr) > Math.Abs(_sendSpeedR) ? AccPerSecond : DeAccPerSecond;
|
||||
// _sendSpeedR += speedSignR * Math.Min(Math.Abs(vr - _sendSpeedR), accR * dTime);
|
||||
// _rightWheel.WriteSpeed(_sendSpeedR);
|
||||
|
||||
// if (Debug)
|
||||
// Console.WriteLine($"DiffChassis, target:{v:0.00},send:{_sendSpeed:0.0}");
|
||||
// var dTime = (float)(deltaTime ?? DateTime.Now - LastMoveTime).TotalSeconds;
|
||||
var dTime = (float)(deltaTime ?? DateTime.Now - LastMoveTime).TotalSeconds;
|
||||
float diffL = vl - _sendSpeedL;
|
||||
float diffR = vr - _sendSpeedR;
|
||||
|
||||
float accL = Math.Abs(vl) > Math.Abs(_sendSpeedL) ? AccPerSecond : DeAccPerSecond;
|
||||
float accR = Math.Abs(vr) > Math.Abs(_sendSpeedR) ? AccPerSecond : DeAccPerSecond;
|
||||
|
||||
float maxDeltaL = accL * dTime;
|
||||
float maxDeltaR = accR * dTime;
|
||||
|
||||
float factorL = Math.Abs(diffL) > maxDeltaL ? maxDeltaL / Math.Abs(diffL) : 1.0f;
|
||||
float factorR = Math.Abs(diffR) > maxDeltaR ? maxDeltaR / Math.Abs(diffR) : 1.0f;
|
||||
|
||||
float factor = Math.Min(factorL, factorR);
|
||||
|
||||
_sendSpeedL += diffL * factor;
|
||||
_sendSpeedR += diffR * factor;
|
||||
|
||||
_leftWheel.WriteSpeed(_sendSpeedL);
|
||||
_rightWheel.WriteSpeed(_sendSpeedR);
|
||||
// Console.WriteLine($"DiffChassis, target:{vl:0.00},send:{_sendSpeedL:0.00} dTime:{dTime} diffL:{diffL} factor:{factor}" );
|
||||
}
|
||||
|
||||
private Wheel _leftWheel;
|
||||
private Wheel _rightWheel;
|
||||
private float _halfWheelTrack; // millimeter
|
||||
|
||||
private float _sendSpeedL;
|
||||
private float _sendSpeedR;
|
||||
private int _direction = 1; // 1 forward, -1 backward
|
||||
private float _speedL;
|
||||
private float _speedR;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using System.Diagnostics;
|
||||
using CommonUsage.Mathematics;
|
||||
using FundamentalLib;
|
||||
|
||||
namespace CommonUsage.Chassis
|
||||
{
|
||||
public class SingleSteerChassis : AbstractChassis
|
||||
{
|
||||
public void SetSteerWheel(SteerWheel wheel)
|
||||
{
|
||||
_steerWheel = wheel;
|
||||
}
|
||||
|
||||
public SteerWheel GetSteerWheel()
|
||||
{
|
||||
return _steerWheel;
|
||||
}
|
||||
|
||||
public override void Visualize()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public override CarSpeed GetCarSpeed(bool isActual = false)
|
||||
{
|
||||
if (!isActual)
|
||||
{
|
||||
var sendAngle = _steerWheel.GetSendAngle();
|
||||
var sendAngleRad = _steerWheel.GetSendAngle() / 180f * Math.PI;
|
||||
// VSteer* Cos = v;
|
||||
var vsteer = _sendSpeed / ((Math.Cos(Math.Abs(sendAngleRad)) + 0.000001));
|
||||
var vsteerY = vsteer * Math.Sin(sendAngleRad);
|
||||
// Console.WriteLine($"{vsteer} {vsteerY} {sendAngleRad} {_sendSpeed}");
|
||||
return new CarSpeed()
|
||||
{
|
||||
Vx = (float)(_sendSpeed * Math.Cos(Math.Abs(sendAngle) / 180f * Math.PI)),
|
||||
Vy = 0,
|
||||
Vw = (float)(_sendSpeed * Math.Sin(Math.Abs(sendAngle) / 180f * Math.PI) /
|
||||
Math.Abs(_steerWheel.Position.X / 1000f) / Math.PI * 180f)
|
||||
//阿克曼
|
||||
// Vx = (float)(_sendSpeed),
|
||||
// Vy = 0,
|
||||
// Vw = (float)(vsteerY / Math.Abs(_steerWheel.Position.X / 1000f) / Math.PI * 180f)
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
return new CarSpeed()
|
||||
{
|
||||
Vx = (float)(_steerWheel.ReadSpeed() *
|
||||
Math.Cos(Math.Abs(_steerWheel.ReadAngle()) / 180f * Math.PI)),
|
||||
Vy = 0,
|
||||
Vw = (float)(_steerWheel.ReadSpeed() *
|
||||
Math.Sin(Math.Abs(_steerWheel.ReadAngle()) / 180f * Math.PI) /
|
||||
Math.Abs(_steerWheel.Position.X / 1000f) / Math.PI * 180f)
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
GeometricControlPoints = new List<GeometricControlPoint>()
|
||||
{
|
||||
new (_steerWheel.Position),
|
||||
new (Vector2.Zero)
|
||||
};
|
||||
Valid = true;
|
||||
}
|
||||
|
||||
public override void AfterDirectionChanged()
|
||||
{
|
||||
if (Math.Abs(CommonMath.ThDiff(0, _originBiasTh)) > 90)
|
||||
{
|
||||
GeometricControlPoints = new List<GeometricControlPoint>()
|
||||
{
|
||||
new (-_steerWheel.Position),
|
||||
};
|
||||
_direction = -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
GeometricControlPoints = new List<GeometricControlPoint>()
|
||||
{
|
||||
new (_steerWheel.Position),
|
||||
};
|
||||
_direction = 1;
|
||||
}
|
||||
}
|
||||
|
||||
public override void PredefinedDriveStop()
|
||||
{
|
||||
if (!Valid) return;
|
||||
_sendSpeed = 0;
|
||||
_steerWheel.WriteSpeed(_sendSpeed);
|
||||
GoingActive = false;
|
||||
RotatingActive = false;
|
||||
}
|
||||
|
||||
protected override void DefineGeometricWheelComputation(float speed)
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
|
||||
if (!GoingActive)
|
||||
{
|
||||
LastMoveTime = now;
|
||||
GoingWheelAligned = false;
|
||||
}
|
||||
|
||||
SendSteerMotion(speed * _direction, GeometricControlPoints[0].Theta, now - LastMoveTime);
|
||||
|
||||
GoingActive = true;
|
||||
RotatingActive = false;
|
||||
}
|
||||
|
||||
public override bool ComputeRotateWheels(float rotSpeed)
|
||||
{
|
||||
if (!RotatingActive)
|
||||
{
|
||||
LastMoveTime = DateTime.Now;
|
||||
GoingWheelAligned = false;
|
||||
}
|
||||
|
||||
SendSteerMotion(rotSpeed, 90);
|
||||
|
||||
GoingActive = false;
|
||||
RotatingActive = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void SendSteerMotion(float speed, float theta, TimeSpan? deltaTime = null)
|
||||
{
|
||||
_steerWheel.WriteAngle(theta);
|
||||
|
||||
if (!GoingWheelAligned && Math.Abs(CommonMath.ThDiff(_steerWheel.ReadAngle(), theta)) < 1)
|
||||
GoingWheelAligned = true;
|
||||
if (!GoingWheelAligned) speed = 0;
|
||||
|
||||
var turnThresholdSpeed = CalculateTurningSpeedDecayFac(Math.Abs(theta)) * MaxSpeed;
|
||||
AccumulateSpeed(Math.Min(turnThresholdSpeed, Math.Abs(speed)) * Math.Sign(speed), deltaTime);
|
||||
|
||||
LastMoveTime = DateTime.Now;
|
||||
}
|
||||
|
||||
public override float CalculateTurningSpeedDecayFac(float turn)
|
||||
{
|
||||
return 1 - Math.Min(turn, MaxTurnThreshold) / MaxTurnThreshold * MinTurnSpeedFac;
|
||||
}
|
||||
|
||||
private void AccumulateSpeed(float v, TimeSpan? deltaTime = null)
|
||||
{
|
||||
// _targetSpeed = v;
|
||||
var speedSign = Math.Sign(v - _sendSpeed);
|
||||
var acc = Math.Abs(v) > Math.Abs(_sendSpeed) ? AccPerSecond : DeAccPerSecond;
|
||||
_sendSpeed += speedSign * Math.Min(Math.Abs(v - _sendSpeed),
|
||||
acc * (float)(deltaTime ?? DateTime.Now - LastMoveTime).TotalSeconds);
|
||||
_steerWheel.WriteSpeed(_sendSpeed);
|
||||
|
||||
if (Debug)
|
||||
Console.WriteLine($"SingleSteer, target:{v:0.00},send:{_sendSpeed:0.0}");
|
||||
}
|
||||
|
||||
private SteerWheel _steerWheel;
|
||||
private float _sendSpeed;
|
||||
private int _direction = 1; // 1 forward, -1 backward
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using CommonUsage.Mathematics;
|
||||
|
||||
namespace CommonUsage.Chassis
|
||||
{
|
||||
public class SteerWheel : Wheel
|
||||
{
|
||||
public SteerWheel(Vector2 position, float angleLowerLimit, float angleUpperLimit, Action<float> speedWriter,
|
||||
Func<float> speedReader, Action<float> angleWriter, Func<float> angleReader, float angleLimitMarginDeg = 15f) : base(position, speedWriter,
|
||||
speedReader)
|
||||
{
|
||||
_angleLowerLimit = angleLowerLimit;
|
||||
_angleUpperLimit = angleUpperLimit;
|
||||
_angleWriter = angleWriter;
|
||||
_angleReader = angleReader;
|
||||
_centerDistance = position.Length();
|
||||
AngleLimitMarginDeg = angleLimitMarginDeg;
|
||||
}
|
||||
|
||||
public float AngleLimitMarginDeg = 15f;
|
||||
|
||||
public bool TrySetDirection(bool allowReverse, ref float desireDirection, ref int dir)
|
||||
{
|
||||
if (TryNormalizeAngleInLimit(desireDirection, out var normalized))
|
||||
{
|
||||
dir = 1;
|
||||
desireDirection = normalized;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!allowReverse) return false;
|
||||
|
||||
var oppositeTh = (float)CommonMath.RoundTh(desireDirection + 180);
|
||||
if (TryNormalizeAngleInLimit(oppositeTh, out normalized))
|
||||
{
|
||||
dir = -1;
|
||||
desireDirection = normalized;
|
||||
return true;
|
||||
}
|
||||
|
||||
dir = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryNormalizeAngleInLimit(float angle, out float normalized)
|
||||
{
|
||||
var lower = CommonMath.RoundTh(_angleLowerLimit);
|
||||
var upper = CommonMath.RoundTh(_angleUpperLimit);
|
||||
while (upper < lower) upper += 360;
|
||||
|
||||
normalized = (float)CommonMath.RoundTh(angle);
|
||||
while (normalized < lower) normalized += 360;
|
||||
while (normalized > upper && normalized - 360 >= lower) normalized -= 360;
|
||||
|
||||
var margin = Math.Min(normalized - lower, upper - normalized);
|
||||
return normalized >= lower && normalized <= upper && margin >= Math.Max(0, AngleLimitMarginDeg);
|
||||
}
|
||||
|
||||
public float ReadAngle()
|
||||
{
|
||||
return _angleReader();
|
||||
}
|
||||
|
||||
public void WriteAngle(float angle)
|
||||
{
|
||||
_angleWriter.Invoke(_sendAngle = Math.Max(_angleLowerLimit, Math.Min(angle, _angleUpperLimit)));
|
||||
}
|
||||
|
||||
public float GetSendAngle()
|
||||
{
|
||||
return _sendAngle;
|
||||
}
|
||||
|
||||
public float GetAngleRelativeToChassis()
|
||||
{
|
||||
return ZeroDirection + _sendAngle;
|
||||
}
|
||||
|
||||
public float CenterDistance()
|
||||
{
|
||||
return _centerDistance;
|
||||
}
|
||||
|
||||
public float AngleLowerLimit => _angleLowerLimit;
|
||||
|
||||
public float AngleUpperLimit => _angleUpperLimit;
|
||||
|
||||
[JsonIgnore] private readonly Action<float> _angleWriter;
|
||||
[JsonIgnore] private readonly Func<float> _angleReader;
|
||||
|
||||
private float _angleLowerLimit = -90, _angleUpperLimit = 90;
|
||||
private float _centerDistance;
|
||||
|
||||
public float _sendAngle = 0f;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CommonUsage.Chassis
|
||||
{
|
||||
public class Wheel
|
||||
{
|
||||
public Wheel(Vector2 position, Action<float> speedWriter, Func<float> speedReader)
|
||||
{
|
||||
PhysicalPosition = Position = position;
|
||||
SpeedWriter = speedWriter;
|
||||
SpeedReader = speedReader;
|
||||
}
|
||||
|
||||
public void WriteSpeed(float speed)
|
||||
{
|
||||
SpeedWriter.Invoke(_sendSpeed = speed);
|
||||
}
|
||||
|
||||
public float GetSendSpeed()
|
||||
{
|
||||
return _sendSpeed;
|
||||
}
|
||||
|
||||
public float ReadSpeed()
|
||||
{
|
||||
return SpeedReader();
|
||||
}
|
||||
|
||||
// PhysicalPosition ===(chassis transform)===> Position
|
||||
// useful in dual agv coordination
|
||||
public readonly Vector2 PhysicalPosition;
|
||||
public Vector2 Position;
|
||||
|
||||
public float ZeroDirection = 0;
|
||||
|
||||
[JsonIgnore] public readonly Action<float> SpeedWriter;
|
||||
[JsonIgnore] public readonly Func<float> SpeedReader;
|
||||
|
||||
public float _sendSpeed;
|
||||
}
|
||||
|
||||
public class GeometricControlPoint
|
||||
{
|
||||
public GeometricControlPoint(Vector2 position)
|
||||
{
|
||||
Position = position;
|
||||
}
|
||||
|
||||
public Vector2 Position;
|
||||
public float Theta;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<DebugType>embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<DebugType>embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Hedingben.cs" />
|
||||
<Compile Remove="IPCConcurrentDictionary.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MQTTnet" Version="4.3.7.1207" />
|
||||
<PackageReference Include="MQTTnet.Extensions.ManagedClient" Version="4.3.7.1207" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="System.Buffers" Version="4.5.1" />
|
||||
<PackageReference Include="System.Numerics.Vectors" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="FundamentalLib">
|
||||
<HintPath>..\..\MedullaAdapter\ref\RefFundamentalLib.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="ClumsyCore">
|
||||
<HintPath>..\..\ClumsyPilot\ref\RefClumsyCore.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="CopyCommonUsageToMyParkingRef" AfterTargets="Build">
|
||||
<MakeDir Directories="..\..\ref" />
|
||||
<Copy SourceFiles="$(TargetPath)"
|
||||
DestinationFolder="..\..\ref" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,25 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.5.33424.131
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommonUsage", "CommonUsage.csproj", "{E1C5DEA8-3785-40A9-9965-E51E65BF5947}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{E1C5DEA8-3785-40A9-9965-E51E65BF5947}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E1C5DEA8-3785-40A9-9965-E51E65BF5947}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E1C5DEA8-3785-40A9-9965-E51E65BF5947}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E1C5DEA8-3785-40A9-9965-E51E65BF5947}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {709F9C19-45DB-46AD-B70A-0E1E3C6CFB0C}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -0,0 +1,93 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using static CommonUsage.Geometries.CircularArc;
|
||||
|
||||
namespace CommonUsage.Geometries
|
||||
{
|
||||
/// <summary>
|
||||
/// 便于直接创建几何形状并求几何形状的切点、切线等。
|
||||
/// </summary>
|
||||
public abstract class AbstractGeometry
|
||||
{
|
||||
protected AbstractGeometry()
|
||||
{
|
||||
PaddingType = Padding.StartExtendEndExtend;
|
||||
VisualizeOption = new VisualizeOption(Color.Red, Color.Gray);
|
||||
}
|
||||
|
||||
public Padding PaddingType;
|
||||
|
||||
public abstract (Vector2 Pt, float Angle, float Bias, float Position) QueryTangentPoint(Vector2 point);
|
||||
|
||||
public abstract void Visualize(Action<VisDot> processDot, Action<VisLine> processLine,
|
||||
bool visExtendedPart = false);
|
||||
|
||||
public VisualizeOption VisualizeOption;
|
||||
|
||||
/// <summary>
|
||||
/// 查询指定位置的曲率。
|
||||
/// </summary>
|
||||
/// <param name="position">从起点到查询位置的距离。</param>
|
||||
/// <returns></returns>
|
||||
public abstract float QueryCurvature(float position);
|
||||
|
||||
public abstract float Length();
|
||||
}
|
||||
|
||||
public class VisualizeOption
|
||||
{
|
||||
public VisualizeOption(Color mainColor, Color auxiliaryColor)
|
||||
{
|
||||
MainColor = mainColor;
|
||||
AuxiliaryColor = auxiliaryColor;
|
||||
}
|
||||
|
||||
public Color MainColor;
|
||||
public Color AuxiliaryColor;
|
||||
public bool DrawAuxiliary = true;
|
||||
public bool VisualizeDirection = true;
|
||||
}
|
||||
|
||||
public enum Padding
|
||||
{
|
||||
StartLineEndLine = 0b_0001_0001,
|
||||
StartLineEndExtend = 0b_0001_0010,
|
||||
StartExtendEndLine = 0b_0010_0001,
|
||||
StartExtendEndExtend = 0b_0010_0010,
|
||||
}
|
||||
|
||||
public class VisDot
|
||||
{
|
||||
public VisDot(Vector2 point, Color color)
|
||||
{
|
||||
Point = point;
|
||||
Color = color;
|
||||
}
|
||||
|
||||
public Vector2 Point;
|
||||
public Color Color;
|
||||
}
|
||||
|
||||
public class VisLine
|
||||
{
|
||||
public VisLine(Vector2 start, Vector2 end, bool startArrow, bool endArrow, Color color, float width = 1)
|
||||
{
|
||||
Start = start;
|
||||
End = end;
|
||||
StartArrow = startArrow;
|
||||
EndArrow = endArrow;
|
||||
Color = color;
|
||||
Width = width;
|
||||
}
|
||||
|
||||
public Vector2 Start;
|
||||
public Vector2 End;
|
||||
public bool StartArrow = false;
|
||||
public bool EndArrow = false;
|
||||
public Color Color;
|
||||
public float Width;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
using CommonUsage.Mathematics;
|
||||
|
||||
namespace CommonUsage.Geometries
|
||||
{
|
||||
public class BezierCurve : AbstractGeometry
|
||||
{
|
||||
|
||||
public BezierCurve(List<Vector2> controlPoints, int resolution = 100)
|
||||
{
|
||||
// Console.WriteLine($"BezierCurve1");
|
||||
// Console.WriteLine(string.Join(" ",controlPoints.Select(p=>$"{p.X:f2},{p.Y:f2}")));
|
||||
_controlPoints = controlPoints;
|
||||
_resolution = resolution;
|
||||
InitializeBezier();
|
||||
}
|
||||
|
||||
public override void Visualize(Action<VisDot> processDot, Action<VisLine> processLine, bool visExtendedPart = false)
|
||||
{
|
||||
if (VisualizeOption.DrawAuxiliary)
|
||||
for (var i = 0; i < _controlPoints.Count - 1; ++i)
|
||||
{
|
||||
processLine(new VisLine(_controlPoints[i], _controlPoints[i + 1],
|
||||
false, false, VisualizeOption.AuxiliaryColor));
|
||||
if (i == 0) continue;
|
||||
processDot(new VisDot(_controlPoints[i], VisualizeOption.AuxiliaryColor));
|
||||
}
|
||||
|
||||
for (var i = 0; i < _bezierPoints.Count - 1; ++i)
|
||||
{
|
||||
if (Direction == -1)
|
||||
{
|
||||
processLine(new VisLine(_bezierPoints[i + 1], _bezierPoints[i],
|
||||
false, i == (int)(_bezierPoints.Count / 2), VisualizeOption.MainColor, 2));
|
||||
}
|
||||
else
|
||||
{
|
||||
processLine(new VisLine(_bezierPoints[i], _bezierPoints[i + 1],
|
||||
false, i == (int)(_bezierPoints.Count / 2), VisualizeOption.MainColor, 2));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public (Vector2 Point, int Id) QueryPoint(Vector2 point)
|
||||
{
|
||||
var p = new Vector2();
|
||||
var id = -1;
|
||||
var bestDistance = float.MaxValue;
|
||||
|
||||
var hashes = _bias.Select(bb => CalculateHash(point, 100, bb.X, bb.Y)).ToList();
|
||||
|
||||
void TryQuery(Dictionary<uint, List<(Vector2 Point, int Id)>> dict, List<uint> hashList)
|
||||
{
|
||||
foreach (var hash in hashList)
|
||||
{
|
||||
if (!dict.TryGetValue(hash, out var ll)) continue;
|
||||
foreach (var (q, qId) in ll)
|
||||
{
|
||||
var d = Vector2.Distance(q, point);
|
||||
if (d < bestDistance)
|
||||
{
|
||||
p = q;
|
||||
id = qId;
|
||||
bestDistance = d;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//map目前有bug,取消cpu占用也不严重,必要时候在优化
|
||||
// TryQuery(_pointsMappingSmall, hashes);
|
||||
//
|
||||
// if (id == -1)
|
||||
// {
|
||||
// hashes = _bias.Select(bb => CalculateHash(point, 1000, bb.X, bb.Y)).ToList();
|
||||
// TryQuery(_pointsMappingBig, hashes);
|
||||
// }
|
||||
|
||||
if (id == -1)
|
||||
{
|
||||
// todo: improve the way to find closest point if mappings fail
|
||||
(p, id) = _bezierPoints.Select((p, i) => (p, i))
|
||||
.OrderBy(pair => CommonMath.dist(pair.p.X, pair.p.Y, point.X, point.Y)).First();
|
||||
}
|
||||
|
||||
return (p, id);
|
||||
}
|
||||
|
||||
public override (Vector2 Pt, float Angle, float Bias, float Position) QueryTangentPoint(Vector2 point)
|
||||
{
|
||||
var (p, id) = QueryPoint(point);
|
||||
var tangent = _tangents[id];
|
||||
var (bias, lp, fd) = CommonMath.Project2DLine(point, p, tangent);
|
||||
var next = fd > 0 ? id + 1 : id - 1;
|
||||
if (id == 0) next = 1;
|
||||
// Console.WriteLine($"id:{id} next:{next} tangent:{tangent} _tangents.Count:{_tangents.Count}");
|
||||
if (next > 0 && next < _tangents.Count)//线性插值
|
||||
{
|
||||
var (_, _, t) = CommonMath.Project2DLine(point, _bezierPoints[id], _bezierPoints[next]);
|
||||
var partial = t / Vector2.Distance(_bezierPoints[id], _bezierPoints[next]);
|
||||
if (partial >= 0 && partial <= 1)
|
||||
{
|
||||
tangent = CommonMath.RoundTh(_tangents[id] +
|
||||
partial * CommonMath.RoundTh(_tangents[next] - _tangents[id]));
|
||||
if (CommonMath.RoundTh(_tangents[next] - _tangents[id]) > 5)
|
||||
Console.WriteLine($"bezier tangents bug, tanget: {id}:{_tangents[id]} {next}:{_tangents[next]}");
|
||||
}
|
||||
// else Console.WriteLine("bezier tangents bug");
|
||||
}
|
||||
return (lp, tangent, bias, fd + _sumDistances[id]);
|
||||
}
|
||||
|
||||
public override float Length()
|
||||
{
|
||||
return _length;
|
||||
}
|
||||
|
||||
public override float QueryCurvature(float position)
|
||||
{
|
||||
int id = _sumDistances.Count - 1;
|
||||
if (position <= 0) id = 0;
|
||||
else
|
||||
{
|
||||
for (int i = 1; i < _sumDistances.Count; i++)
|
||||
{
|
||||
if (position > _sumDistances[i - 1] && position <= _sumDistances[i])
|
||||
{
|
||||
id = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var result = _curvatures[id];
|
||||
if (id > 0 && id < _sumDistances.Count - 1)//插值
|
||||
{
|
||||
var partial = (position - _sumDistances[id - 1]) / (_sumDistances[id] - _sumDistances[id - 1]);
|
||||
if (partial >= 0 && partial <= 1) result = (1 - partial) * _curvatures[id - 1] + partial * _curvatures[id];
|
||||
else Console.WriteLine("bezier curvature bug");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public Vector3 QueryBezierPointsById(int id)
|
||||
{
|
||||
if (id < 0 || id > Resolution)
|
||||
{
|
||||
Console.WriteLine($"QueryBezierPointsById out of range, Resolution:{Resolution},id:{id}.");
|
||||
return new Vector3(0, 0, 0);
|
||||
}
|
||||
|
||||
return new Vector3(_bezierPoints[id].X, _bezierPoints[id].Y, _tangents[id]);
|
||||
}
|
||||
|
||||
public List<Vector2> ControlPoints => _controlPoints;
|
||||
|
||||
public int Resolution => _resolution;
|
||||
/// <summary>
|
||||
/// 仅用于simple显示路径方向
|
||||
/// </summary>
|
||||
public int Direction = 1;
|
||||
public int Order => _order;
|
||||
|
||||
// public List<float> Tangents => _tangents;
|
||||
|
||||
public void UpdateControlPoint(int id, Vector2 point)
|
||||
{
|
||||
_controlPoints[id] = point;
|
||||
InitializeBezier();
|
||||
}
|
||||
|
||||
public void AddControlPoint(int id, Vector2 point)
|
||||
{
|
||||
_controlPoints.Insert(id, point);
|
||||
InitializeBezier();
|
||||
}
|
||||
|
||||
public void RemoveControlPoint(int id)
|
||||
{
|
||||
_controlPoints.RemoveAt(id);
|
||||
InitializeBezier();
|
||||
}
|
||||
|
||||
public Vector2 GetMidPoint()
|
||||
{
|
||||
return _bezierPoints[(int)Math.Ceiling(_resolution / 2d)];
|
||||
}
|
||||
|
||||
private void InitializeBezier()
|
||||
{
|
||||
_order = _controlPoints.Count - 1;
|
||||
// _bezierPoints = new List<Vector2>();
|
||||
var delta = 1.0f / _resolution;
|
||||
// for (int t = 0; t <= _resolution; t += 1)//下面循环算了,没必要先递归算一遍
|
||||
// _bezierPoints.Add(new Vector2(DeCasteljauX(_order, 0, t*delta), DeCasteljauY(_order, 0, t*delta)));
|
||||
var allPoints = new List<List<List<Vector2>>>();
|
||||
for (var i = 0; i < _order; i++)
|
||||
{
|
||||
var size = allPoints.Count;
|
||||
var morePoints = new List<List<Vector2>>();
|
||||
for (var j = 0; j < _order - i; j++)
|
||||
{
|
||||
var points = new List<Vector2>();
|
||||
for (int t = 0; t <= _resolution; t += 1)
|
||||
{
|
||||
float p0x;
|
||||
float p1x;
|
||||
float p0y;
|
||||
float p1y;
|
||||
var z = t;
|
||||
if (size > 0)
|
||||
{
|
||||
p0x = allPoints[i - 1][j][z].X;
|
||||
p1x = allPoints[i - 1][j + 1][z].X;
|
||||
p0y = allPoints[i - 1][j][z].Y;
|
||||
p1y = allPoints[i - 1][j + 1][z].Y;
|
||||
}
|
||||
else
|
||||
{
|
||||
p0x = _controlPoints[j].X;
|
||||
p1x = _controlPoints[j + 1].X;
|
||||
p0y = _controlPoints[j].Y;
|
||||
p1y = _controlPoints[j + 1].Y;
|
||||
}
|
||||
|
||||
var part = t * delta;
|
||||
points.Add(new Vector2((1 - part) * p0x + part * p1x, (1 - part) * p0y + part * p1y));
|
||||
}
|
||||
morePoints.Add(points);
|
||||
}
|
||||
allPoints.Add(morePoints);
|
||||
}
|
||||
|
||||
_bezierPoints = allPoints.Last().Last();
|
||||
_tangentInfo = allPoints;
|
||||
_tangents = Enumerable.Repeat(0f, _bezierPoints.Count).ToList();
|
||||
_curvatures = Enumerable.Repeat(0f, _bezierPoints.Count).ToList();
|
||||
var p2 = allPoints[Order - 2];
|
||||
for (var id = 0; id < _bezierPoints.Count; ++id)
|
||||
{
|
||||
_tangents[id] =
|
||||
(float)(Math.Atan2(p2[1][id].Y - p2[0][id].Y, p2[1][id].X - p2[0][id].X) / Math.PI * 180);
|
||||
if (id != 0) _curvatures[id] = (float)((CommonMath.ThDiff(_tangents[id], _tangents[id - 1]) / 180 * Math.PI)
|
||||
/ (Vector2.Distance(_bezierPoints[id], _bezierPoints[id - 1]) / 1000));
|
||||
}
|
||||
// Console.WriteLine($"{string.Join("\n", _tangents.Select((val, i) => $"{i}: {val}"))}");
|
||||
_tangents[0] = _tangents[1]; // todo: here is temporary fix
|
||||
_curvatures[0] = _curvatures[1];
|
||||
for (var id = 1; id < _bezierPoints.Count - 1; ++id)//前移0.5
|
||||
_curvatures[id] = (_curvatures[id] + _curvatures[id + 1]) / 2;
|
||||
|
||||
_remainDistances = Enumerable.Repeat(0f, _bezierPoints.Count).ToList();
|
||||
_sumDistances = Enumerable.Repeat(0f, _bezierPoints.Count).ToList();
|
||||
for (var i = _bezierPoints.Count - 2; i >= 0; --i)
|
||||
{
|
||||
_remainDistances[i] =
|
||||
_remainDistances[i + 1] + Vector2.Distance(_bezierPoints[i], _bezierPoints[i + 1]);
|
||||
}
|
||||
for (var i = 1; i < _bezierPoints.Count; ++i)
|
||||
{
|
||||
_sumDistances[i] =
|
||||
_sumDistances[i - 1] + Vector2.Distance(_bezierPoints[i], _bezierPoints[i - 1]);
|
||||
}
|
||||
_length = _sumDistances.Last();
|
||||
|
||||
_minX = _bezierPoints.Min(pp => pp.X);
|
||||
_minY = _bezierPoints.Min(pp => pp.Y);
|
||||
|
||||
var tmpList = _bezierPoints.Select((point, index) => (point, index)).ToList();
|
||||
return;
|
||||
void GenerateGridMapping(ref Dictionary<uint, List<(Vector2 Point, int Id)>> dict, float gSize)
|
||||
{
|
||||
dict = new Dictionary<uint, List<(Vector2 Point, int Id)>>();
|
||||
foreach (var (point, index) in tmpList)
|
||||
{
|
||||
var hash = CalculateHash(point, gSize);
|
||||
if (dict.TryGetValue(hash, out var ll))
|
||||
ll.Add((point, index));
|
||||
else dict[hash] = new List<(Vector2 Point, int Id)>() { (point, index) };
|
||||
}
|
||||
}
|
||||
|
||||
GenerateGridMapping(ref _pointsMappingSmall, 100);
|
||||
GenerateGridMapping(ref _pointsMappingBig, 1000);
|
||||
}
|
||||
|
||||
private uint CalculateHash(Vector2 point, float gridSize, int xBias = 0, int yBias = 0)
|
||||
{
|
||||
return (uint)(((int)((point.X - _minX) / gridSize) + xBias) << 16 + (((int)((point.Y - _minY) / gridSize) + yBias) & 0xffff));
|
||||
}
|
||||
|
||||
private readonly List<(int X, int Y)> _bias = new()
|
||||
{
|
||||
new(-1, -1), new(-1, 0), new(-1, 1),
|
||||
new(0, -1), new(0, 0), new(0, 1),
|
||||
new(1, -1), new(1, 0), new(1, 1),
|
||||
};
|
||||
|
||||
private float DeCasteljauX(int i, int j, float t)
|
||||
{
|
||||
if (i == 1)
|
||||
return (1 - t) * _controlPoints[j].X + t * _controlPoints[j + 1].X;
|
||||
return (1 - t) * DeCasteljauX(i - 1, j, t) + t * DeCasteljauX(i - 1, j + 1, t);
|
||||
}
|
||||
|
||||
private float DeCasteljauY(int i, int j, float t)
|
||||
{
|
||||
if (i == 1)
|
||||
return (1 - t) * _controlPoints[j].Y + t * _controlPoints[j + 1].Y;
|
||||
return (1 - t) * DeCasteljauY(i - 1, j, t) + t * DeCasteljauY(i - 1, j + 1, t);
|
||||
}
|
||||
|
||||
private int _order;
|
||||
private int _resolution;
|
||||
private List<Vector2> _controlPoints;
|
||||
private List<Vector2> _bezierPoints;
|
||||
private List<List<List<Vector2>>> _tangentInfo;
|
||||
private List<float> _tangents;
|
||||
private List<float> _remainDistances;
|
||||
private List<float> _sumDistances;
|
||||
private List<float> _curvatures;
|
||||
private Dictionary<uint, List<(Vector2 Point, int Id)>> _pointsMappingSmall;
|
||||
private Dictionary<uint, List<(Vector2 Point, int Id)>> _pointsMappingBig;
|
||||
private float _minX, _minY;
|
||||
private float _length;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using CommonUsage.Mathematics;
|
||||
|
||||
namespace CommonUsage.Geometries
|
||||
{
|
||||
public class CircularArc : AbstractGeometry
|
||||
{
|
||||
/// <summary>
|
||||
/// 以center为圆心、radius为半径,从angleStart逆时针转到angleEnd所构成的圆弧。direction表示圆弧走向。
|
||||
/// </summary>
|
||||
/// <param name="center"></param>
|
||||
/// <param name="radius"></param>
|
||||
/// <param name="angleStart"></param>
|
||||
/// <param name="angleEnd"></param>
|
||||
/// <param name="direction">表示圆弧走向,1为angleStart到angleEnd,-1为angleEnd到angleStart</param>
|
||||
public CircularArc(Vector2 center, float radius, float angleStart, float angleEnd, int direction, Padding paddingType)
|
||||
{
|
||||
_center = center;
|
||||
_radius = radius;
|
||||
_angleStart = angleStart;
|
||||
_angleEnd = angleEnd;
|
||||
_direction = direction;
|
||||
PaddingType = paddingType;
|
||||
|
||||
ChangeShape();
|
||||
CalculateVisPoints();
|
||||
}
|
||||
|
||||
public override void Visualize(Action<VisDot> processDot, Action<VisLine> processLine,
|
||||
bool visExtendedPart = false)
|
||||
{
|
||||
lock (_visPoints)
|
||||
{
|
||||
if (visExtendedPart)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
for (var i = 0; i < _visPoints.Length - 1; ++i)
|
||||
{
|
||||
if ((i == 0 || i == _visPoints.Length - 2) && !visExtendedPart) continue;
|
||||
var color = Color.Red;
|
||||
if (i == 0 || i == _visPoints.Length - 2) color = Color.Gray;
|
||||
processLine(new VisLine(_visPoints[i], _visPoints[i + 1],
|
||||
false, i == (_visPoints.Length - 1) / 2, color));
|
||||
}
|
||||
|
||||
if (visExtendedPart)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SwitchSide()
|
||||
{
|
||||
(_angleStart, _angleEnd) = (_angleEnd, _angleStart);
|
||||
ChangeShape();
|
||||
CalculateVisPoints();
|
||||
}
|
||||
|
||||
public float VisAngleResolution = 1;
|
||||
|
||||
public Vector2 Center
|
||||
{
|
||||
get => _center;
|
||||
set
|
||||
{
|
||||
_center = value;
|
||||
ChangeShape();
|
||||
CalculateVisPoints();
|
||||
}
|
||||
}
|
||||
|
||||
public float Radius
|
||||
{
|
||||
get => _radius;
|
||||
set
|
||||
{
|
||||
_radius = value;
|
||||
ChangeShape();
|
||||
CalculateVisPoints();
|
||||
}
|
||||
}
|
||||
|
||||
public float AngleStart
|
||||
{
|
||||
get => _angleStart;
|
||||
set
|
||||
{
|
||||
_angleStart = value;
|
||||
ChangeShape();
|
||||
CalculateVisPoints();
|
||||
}
|
||||
}
|
||||
|
||||
public float AngleEnd
|
||||
{
|
||||
get => _angleEnd;
|
||||
set
|
||||
{
|
||||
_angleEnd = value;
|
||||
ChangeShape();
|
||||
CalculateVisPoints();
|
||||
}
|
||||
}
|
||||
|
||||
public int Direction
|
||||
{
|
||||
get => _direction;
|
||||
set
|
||||
{
|
||||
_direction = value;
|
||||
ChangeShape();
|
||||
CalculateVisPoints();
|
||||
}
|
||||
}
|
||||
|
||||
public float AngleRange => _totalTh;
|
||||
|
||||
public Vector2 PointStart => _center + new Vector2(_radius * (float)Math.Cos(_angleStart / 180 * Math.PI),
|
||||
_radius * (float)Math.Sin(_angleStart / 180 * Math.PI));
|
||||
|
||||
public Vector2 PointEnd => _center + new Vector2(_radius * (float)Math.Cos(_angleEnd / 180 * Math.PI),
|
||||
_radius * (float)Math.Sin(_angleEnd / 180 * Math.PI));
|
||||
|
||||
public Vector2 Src => _src;
|
||||
|
||||
public Vector2 Dst => _dst;
|
||||
|
||||
public float TangentSrc => _tangentSrc;
|
||||
|
||||
public float TangentDst => _tangentDst;
|
||||
|
||||
public override (Vector2 Pt, float Angle, float Bias, float Position) QueryTangentPoint(Vector2 point)
|
||||
{
|
||||
var queryTh = (float)(Math.Atan2(point.Y - _center.Y, point.X - _center.X) / Math.PI * 180);
|
||||
|
||||
var p = new Vector2();
|
||||
var tangent = 0f;
|
||||
var pd = 0f;
|
||||
var bestBias = float.MaxValue;
|
||||
|
||||
if ((((int)PaddingType >> 4) & 0x1) == 1)
|
||||
{
|
||||
var (bias1, hPnt1, fd1) = CommonMath.Project2DLine(point, _beforeStartSrc, _src);
|
||||
if (fd1 <= 1000)
|
||||
{
|
||||
p = hPnt1;
|
||||
tangent = (_direction >= 0 ? _angleStart : _angleEnd) + 90 * _direction;
|
||||
pd = fd1;
|
||||
bestBias = bias1;
|
||||
}
|
||||
}
|
||||
|
||||
if (((int)PaddingType & 0x1) == 1)
|
||||
{
|
||||
var (bias2, hPnt2, fd2) = CommonMath.Project2DLine(point, _dst, _afterEndDst);
|
||||
if (fd2 >= 0 && Math.Abs(bias2) < Math.Abs(bestBias))
|
||||
{
|
||||
p = hPnt2;
|
||||
tangent = (_direction >= 0 ? _angleEnd : _angleStart) + 90 * _direction;
|
||||
pd = _totalLen + fd2;
|
||||
bestBias = bias2;
|
||||
}
|
||||
}
|
||||
|
||||
var th1 = _direction == 1 ? CommonMath.ThDiff(queryTh, _angleStart) : CommonMath.ThDiff(_angleEnd, queryTh);
|
||||
// todo: urgent bug! should use better strategy to prevent sign problem
|
||||
if (th1 < -55) th1 += 360;
|
||||
var arcBias = (_radius - Vector2.Distance(point, _center)) * _direction;
|
||||
if (Math.Abs(arcBias) < Math.Abs(bestBias))
|
||||
{
|
||||
p = _center + _radius * new Vector2((float)Math.Cos(queryTh / 180 * Math.PI),
|
||||
(float)Math.Sin(queryTh / 180 * Math.PI));
|
||||
tangent = queryTh + 90 * _direction;
|
||||
pd = _radius * th1 / 180 * (float)Math.PI;
|
||||
bestBias = arcBias;
|
||||
}
|
||||
|
||||
return (p, tangent, bestBias, pd);
|
||||
}
|
||||
|
||||
public override float QueryCurvature(float position)
|
||||
{
|
||||
// var theta = (float)(_angleEnd - position / _radius / Math.PI * 180f + Math.PI);
|
||||
// return Vectoriel.FromAngleLen(theta, 1f / _radius);
|
||||
return 1000f / _radius * _direction;
|
||||
}
|
||||
|
||||
public override float Length()
|
||||
{
|
||||
return _totalLen;
|
||||
}
|
||||
|
||||
private void CalculateVisPoints()
|
||||
{
|
||||
lock (_visPoints)
|
||||
{
|
||||
// todo: overlapping start and end is problematic
|
||||
var ptCnt = (int)Math.Ceiling((_angleEnd + 360 - _angleStart) % 360 / VisAngleResolution);
|
||||
_visPoints = new Vector2[ptCnt + 2];
|
||||
|
||||
var starting = _angleStart;
|
||||
if (_direction == -1) starting = _angleEnd;
|
||||
_visPoints[0] = _beforeStartSrc;
|
||||
|
||||
for (var j = 0; j < ptCnt; ++j)
|
||||
{
|
||||
var th = starting + j * VisAngleResolution * _direction;
|
||||
var radAngle = (float)(th / 180f * Math.PI);
|
||||
_visPoints[j + 1] = Center + new Vector2((float)Math.Cos(radAngle), (float)Math.Sin(radAngle)) * Radius;
|
||||
}
|
||||
|
||||
_visPoints[ptCnt + 1] = _afterEndDst;
|
||||
}
|
||||
}
|
||||
|
||||
private void ChangeShape()
|
||||
{
|
||||
_totalTh = CommonMath.ThDiff(_angleEnd, _angleStart);
|
||||
if (_totalTh < 0) _totalTh += 360;
|
||||
_totalLen = _radius * _totalTh / 180 * (float)Math.PI;
|
||||
|
||||
var radAngleStart = _angleStart / 180 * Math.PI;
|
||||
var radAngleEnd = _angleEnd / 180 * Math.PI;
|
||||
double srcAngle = radAngleStart, dstAngle = radAngleEnd;
|
||||
if (_direction == -1) (srcAngle, dstAngle) = (dstAngle, srcAngle);
|
||||
_src = _center + new Vector2((float)Math.Cos(srcAngle), (float)Math.Sin(srcAngle)) * _radius;
|
||||
_dst = _center + new Vector2((float)Math.Cos(dstAngle), (float)Math.Sin(dstAngle)) * _radius;
|
||||
_beforeStartSrc = CommonMath.Transform2D(_src,
|
||||
(_direction >= 0 ? _angleStart : _angleEnd) + 90 * _direction, new Vector2(-1000, 0));
|
||||
_afterEndDst = CommonMath.Transform2D(_dst, (_direction >= 0 ? _angleEnd : _angleStart) + 90 * _direction,
|
||||
new Vector2(1000, 0));
|
||||
_tangentSrc = QueryTangentPoint(_src).Angle;
|
||||
_tangentDst = QueryTangentPoint(_dst).Angle;
|
||||
}
|
||||
|
||||
private Vector2 _center;
|
||||
private float _radius, _angleStart, _angleEnd;
|
||||
private int _direction;
|
||||
|
||||
private float _totalTh, _totalLen;
|
||||
private Vector2 _src, _dst;
|
||||
private float _tangentSrc, _tangentDst;
|
||||
private Vector2 _beforeStartSrc, _afterEndDst;
|
||||
|
||||
private Vector2[] _visPoints = Array.Empty<Vector2>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
using CommonUsage.Mathematics;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace CommonUsage.Geometries
|
||||
{
|
||||
public class NurbsCurve : AbstractGeometry
|
||||
{
|
||||
|
||||
public NurbsCurve(List<Vector2> controlPoints, List<float> weights, List<float> knotVector, int frame = 100)
|
||||
{
|
||||
_controlPoints = controlPoints;
|
||||
_weights = weights;
|
||||
_knotVector = knotVector;
|
||||
_frame = frame;
|
||||
InitializeNurbs();
|
||||
}
|
||||
|
||||
public override void Visualize(Action<VisDot> processDot, Action<VisLine> processLine, bool visExtendedPart = false)
|
||||
{
|
||||
if (VisualizeOption.DrawAuxiliary)
|
||||
for (var i = 0; i < _controlPoints.Count - 1; ++i)
|
||||
{
|
||||
processLine(new VisLine(_controlPoints[i], _controlPoints[i + 1],
|
||||
false, false, VisualizeOption.AuxiliaryColor));
|
||||
if (i == 0) continue;
|
||||
processDot(new VisDot(_controlPoints[i], VisualizeOption.AuxiliaryColor));
|
||||
}
|
||||
|
||||
for (var i = 0; i < _nurbsPoints.Count - 1; ++i)
|
||||
{
|
||||
if (Direction == -1)
|
||||
{
|
||||
processLine(new VisLine(_nurbsPoints[i + 1], _nurbsPoints[i],
|
||||
false, i == (int)(_nurbsPoints.Count / 2), VisualizeOption.MainColor, 2));
|
||||
}
|
||||
else
|
||||
{
|
||||
processLine(new VisLine(_nurbsPoints[i], _nurbsPoints[i + 1],
|
||||
false, i == (int)(_nurbsPoints.Count / 2), VisualizeOption.MainColor, 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
public (Vector2 Point, int Id) QueryPoint(Vector2 point)
|
||||
{
|
||||
var p = new Vector2();
|
||||
var id = -1;
|
||||
var bestDistance = float.MaxValue;
|
||||
|
||||
var hashes = _bias.Select(bb => CalculateHash(point, 100, bb.X, bb.Y)).ToList();
|
||||
|
||||
void TryQuery(Dictionary<uint, List<(Vector2 Point, int Id)>> dict, List<uint> hashList)
|
||||
{
|
||||
foreach (var hash in hashList)
|
||||
{
|
||||
if (!dict.TryGetValue(hash, out var ll)) continue;
|
||||
foreach (var (q, qId) in ll)
|
||||
{
|
||||
var d = Vector2.Distance(q, point);
|
||||
if (d < bestDistance)
|
||||
{
|
||||
p = q;
|
||||
id = qId;
|
||||
bestDistance = d;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (id == -1)
|
||||
{
|
||||
// todo: improve the way to find closest point if mappings fail
|
||||
(p, id) = _nurbsPoints.Select((p, i) => (p, i))
|
||||
.OrderBy(pair => CommonMath.dist(pair.p.X, pair.p.Y, point.X, point.Y)).First();
|
||||
}
|
||||
|
||||
return (p, id);
|
||||
}
|
||||
|
||||
private uint CalculateHash(Vector2 point, float gridSize, int xBias = 0, int yBias = 0)
|
||||
{
|
||||
return (uint)(((int)((point.X - _minX) / gridSize) + xBias) << 16 + (((int)((point.Y - _minY) / gridSize) + yBias) & 0xffff));
|
||||
}
|
||||
|
||||
private readonly List<(int X, int Y)> _bias = new()
|
||||
{
|
||||
new(-1, -1), new(-1, 0), new(-1, 1),
|
||||
new(0, -1), new(0, 0), new(0, 1),
|
||||
new(1, -1), new(1, 0), new(1, 1),
|
||||
};
|
||||
|
||||
public override (Vector2 Pt, float Angle, float Bias, float Position) QueryTangentPoint(Vector2 point)
|
||||
{
|
||||
var (p, id) = QueryPoint(point);
|
||||
var tangent = _tangents[id];
|
||||
var (bias, lp, fd) = CommonMath.Project2DLine(point, p, tangent);
|
||||
var next = fd > 0 ? id + 1 : id - 1;
|
||||
if (next > 0 && next < _tangents.Count)//线性插值
|
||||
{
|
||||
var (_, _, t) = CommonMath.Project2DLine(point, _nurbsPoints[id], _nurbsPoints[next]);
|
||||
var partial = t / Vector2.Distance(_nurbsPoints[id], _nurbsPoints[next]);
|
||||
if (partial >= 0 && partial <= 1)
|
||||
{
|
||||
tangent = CommonMath.RoundTh(_tangents[id] +
|
||||
partial * CommonMath.RoundTh(_tangents[next] - _tangents[id]));
|
||||
if (CommonMath.RoundTh(_tangents[next] - _tangents[id]) > 5)
|
||||
Console.WriteLine($"Nurbs tangents bug, tanget: {id}:{_tangents[id]} {next}:{_tangents[next]}");
|
||||
}
|
||||
else Console.WriteLine("Nurbs tangents bug");
|
||||
}
|
||||
return (lp, tangent, bias, fd + _sumDistances[id]);
|
||||
}
|
||||
|
||||
public override float QueryCurvature(float position)
|
||||
{
|
||||
int id = _sumDistances.Count - 1;
|
||||
if (position <= 0) id = 0;
|
||||
else
|
||||
{
|
||||
for (int i = 1; i < _sumDistances.Count; i++)
|
||||
{
|
||||
if (position > _sumDistances[i - 1] && position <= _sumDistances[i])
|
||||
{
|
||||
id = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var result = _curvatures[id];
|
||||
if (id > 0 && id < _sumDistances.Count - 1)//插值
|
||||
{
|
||||
var partial = (position - _sumDistances[id - 1]) / (_sumDistances[id] - _sumDistances[id - 1]);
|
||||
if (partial >= 0 && partial <= 1) result = (1 - partial) * _curvatures[id - 1] + partial * _curvatures[id];
|
||||
else Console.WriteLine("Nurbs curvature bug");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public Vector3 QueryNurbsPointsById(int id)
|
||||
{
|
||||
if (id < 0 || id > Frame)
|
||||
{
|
||||
Console.WriteLine($"QueryBezierPointsById out of range, Resolution:{Frame},id:{id}.");
|
||||
return new Vector3(0, 0, 0);
|
||||
}
|
||||
|
||||
return new Vector3(_nurbsPoints[id].X, _nurbsPoints[id].Y, _tangents[id]);
|
||||
}
|
||||
public override float Length()
|
||||
{
|
||||
return _length;
|
||||
}
|
||||
|
||||
public int Order => _order;
|
||||
public List<Vector2> ControlPoints => _controlPoints;
|
||||
public List<float> Weights => _weights;
|
||||
public List<float> KnotVector => _knotVector;
|
||||
public int Frame => _frame;
|
||||
|
||||
public int Direction = 1;
|
||||
|
||||
|
||||
public void UpdateControlPoint(int id, Vector2 point)
|
||||
{
|
||||
_controlPoints[id] = point;
|
||||
InitializeNurbs();
|
||||
}
|
||||
public void UpdateNurbsWeihgts(int id, float weight)
|
||||
{
|
||||
_weights[id] = weight;
|
||||
InitializeNurbs();
|
||||
|
||||
}
|
||||
public void AddControlPoint(int id, Vector2 point)
|
||||
{
|
||||
_controlPoints.Insert(id, point);
|
||||
InitializeNurbs();
|
||||
}
|
||||
|
||||
public void RemoveControlPoint(int id)
|
||||
{
|
||||
_controlPoints.RemoveAt(id);
|
||||
InitializeNurbs();
|
||||
}
|
||||
|
||||
public Vector2 GetMidPoint()
|
||||
{
|
||||
return _nurbsPoints[(int)Math.Ceiling(_frame / 2d)];
|
||||
}
|
||||
|
||||
private void InitializeNurbs()
|
||||
{
|
||||
_order = _controlPoints.Count - 1;
|
||||
List<List<Vector2>> allpoints = new List<List<Vector2>>();
|
||||
List<Vector2> nurbsCurvePoints = new List<Vector2>();
|
||||
float delta = 1.0f / Frame;
|
||||
|
||||
for (float t = 0; t <= 1; t += delta)
|
||||
{
|
||||
var (point, tangent) = DeBoorAlgorithm(t);
|
||||
var points = new List<Vector2>
|
||||
{
|
||||
point,
|
||||
point + tangent // Tangent endpoint
|
||||
};
|
||||
allpoints.Add(points);
|
||||
nurbsCurvePoints.Add(point); // Store the curve point separately
|
||||
}
|
||||
|
||||
_nurbsPoints = nurbsCurvePoints;
|
||||
_tangents = Enumerable.Repeat(0f, _nurbsPoints.Count).ToList();
|
||||
_curvatures = Enumerable.Repeat(0f, _nurbsPoints.Count).ToList();
|
||||
|
||||
for (var id = 0; id < _nurbsPoints.Count - 1; ++id)
|
||||
{
|
||||
Vector2 p1 = _nurbsPoints[id];
|
||||
Vector2 p2 = _nurbsPoints[id + 1];
|
||||
|
||||
float tangentAngle = (float)Math.Atan2(p2.Y - p1.Y, p2.X - p1.X) * 180 / (float)Math.PI;
|
||||
_tangents[id] = tangentAngle;
|
||||
|
||||
// Calculate curvature using finite differences of tangent (second derivative approximation)
|
||||
if (id > 0)
|
||||
{
|
||||
float previousTangent = _tangents[id - 1];
|
||||
float curvature = (float)(CommonMath.ThDiff(tangentAngle, previousTangent) * Math.PI / 180) /
|
||||
(Vector2.Distance(p1, p2) / 1000);
|
||||
_curvatures[id] = curvature;
|
||||
}
|
||||
}
|
||||
|
||||
_curvatures.Insert(0, _curvatures[0]);
|
||||
for (var id = 1; id < _curvatures.Count - 1; ++id)
|
||||
{
|
||||
_curvatures[id] = (_curvatures[id] + _curvatures[id + 1]) / 2;
|
||||
}
|
||||
|
||||
_remainDistances = Enumerable.Repeat(0f, _nurbsPoints.Count).ToList();
|
||||
_sumDistances = Enumerable.Repeat(0f, _nurbsPoints.Count).ToList();
|
||||
_remainDistances[_nurbsPoints.Count - 1] = 0;
|
||||
|
||||
for (var i = _nurbsPoints.Count - 2; i >= 0; --i)
|
||||
{
|
||||
_remainDistances[i] = _remainDistances[i + 1] + Vector2.Distance(_nurbsPoints[i], _nurbsPoints[i + 1]);
|
||||
}
|
||||
|
||||
_sumDistances[0] = 0;
|
||||
for (var i = 1; i < _nurbsPoints.Count; ++i)
|
||||
{
|
||||
_sumDistances[i] = _sumDistances[i - 1] + Vector2.Distance(_nurbsPoints[i], _nurbsPoints[i - 1]);
|
||||
}
|
||||
|
||||
_length = _sumDistances.Last();
|
||||
|
||||
_minX = _nurbsPoints.Min(pp => pp.X);
|
||||
_minY = _nurbsPoints.Min(pp => pp.Y);
|
||||
|
||||
var tmpList = _nurbsPoints.Select((point, index) => (point, index)).ToList();
|
||||
// GenerateGridMapping(ref _pointsMappingSmall, 100, tmpList);
|
||||
// GenerateGridMapping(ref _pointsMappingBig, 1000, tmpList);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private float CalculateLength()
|
||||
{
|
||||
return _nurbsPoints.Zip(_nurbsPoints.Skip(1), Vector2.Distance).Sum();
|
||||
}
|
||||
|
||||
private (Vector2, Vector2) DeBoorAlgorithm(float t)
|
||||
{
|
||||
Vector2 numerator = Vector2.Zero;
|
||||
Vector2 tangentNumerator = Vector2.Zero;
|
||||
float denominator = 0f;
|
||||
|
||||
// Calculate the point on the curve
|
||||
for (int i = 0; i < ControlPoints.Count; ++i)
|
||||
{
|
||||
float basis = BasisFunction(i, _order, t) * Weights[i];
|
||||
numerator += basis * ControlPoints[i];
|
||||
denominator += basis;
|
||||
}
|
||||
|
||||
Vector2 point = numerator / denominator;
|
||||
|
||||
// Calculate the tangent vector using the analytical derivative
|
||||
for (int i = 0; i < ControlPoints.Count; ++i)
|
||||
{
|
||||
float basisDerivative = BasisFunctionDerivative(i, _order, t) * Weights[i];
|
||||
tangentNumerator += basisDerivative * ControlPoints[i];
|
||||
}
|
||||
Vector2 tangent = tangentNumerator / denominator;
|
||||
|
||||
return (point, tangent);
|
||||
}
|
||||
|
||||
private float BasisFunction(int i, int p, float t)
|
||||
{
|
||||
if (p == 0)
|
||||
return (KnotVector[i] <= t && t < KnotVector[i + 1]) ? 1.0f : 0.0f;
|
||||
|
||||
float denom1 = KnotVector[i + p] - KnotVector[i];
|
||||
float term1 = denom1 == 0 ? 0 : ((t - KnotVector[i]) / denom1) * BasisFunction(i, p - 1, t);
|
||||
|
||||
float denom2 = KnotVector[i + p + 1] - KnotVector[i + 1];
|
||||
float term2 = denom2 == 0 ? 0 : ((KnotVector[i + p + 1] - t) / denom2) * BasisFunction(i + 1, p - 1, t);
|
||||
|
||||
return term1 + term2;
|
||||
}
|
||||
|
||||
private float BasisFunctionDerivative(int i, int k, float t)
|
||||
{
|
||||
if (k == 0) return 0;
|
||||
|
||||
float denom1 = KnotVector[i + k] - KnotVector[i];
|
||||
float denom2 = KnotVector[i + k + 1] - KnotVector[i + 1];
|
||||
|
||||
float term1 = denom1 != 0 ? BasisFunction(i, k - 1, t) / denom1 : 0;
|
||||
float term2 = denom1 != 0 ? (t - KnotVector[i]) * BasisFunctionDerivative(i, k - 1, t) / denom1 : 0;
|
||||
|
||||
float term3 = denom2 != 0 ? -BasisFunction(i + 1, k - 1, t) / denom2 : 0;
|
||||
float term4 = denom2 != 0 ? (KnotVector[i + k + 1] - t) * BasisFunctionDerivative(i + 1, k - 1, t) / denom2 : 0;
|
||||
|
||||
return term1 + term2 + term3 + term4;
|
||||
}
|
||||
|
||||
private int _order;
|
||||
private List<Vector2> _controlPoints;
|
||||
private List<float> _weights;
|
||||
private List<float> _knotVector;
|
||||
private int _frame;
|
||||
private List<Vector2> _nurbsPoints;
|
||||
private List<List<List<Vector2>>> _tangentPoints;
|
||||
private List<float> _curvatures;
|
||||
private List<float> _sumDistances;
|
||||
private List<float> _remainDistances;
|
||||
private float _minX, _minY;
|
||||
private float _length;
|
||||
private Dictionary<uint, List<(Vector2 Point, int Id)>> _pointsMappingSmall;
|
||||
private Dictionary<uint, List<(Vector2 Point, int Id)>> _pointsMappingBig;
|
||||
private List<float> _tangents;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
|
||||
namespace CommonUsage.Geometries
|
||||
{
|
||||
/// <summary>
|
||||
/// MDCS数学类:向量。
|
||||
/// </summary>
|
||||
public class Vectoriel
|
||||
{
|
||||
public Vectoriel()
|
||||
{
|
||||
_vec2 = Vector2.Zero;
|
||||
_dir2 = Vector2.Normalize(_vec2);
|
||||
_len = _vec2.Length();
|
||||
_angle = (float)(Math.Atan2(_vec2.Y, _vec2.X) / Math.PI * 180f);
|
||||
}
|
||||
|
||||
public Vectoriel(Vector2 vec)
|
||||
{
|
||||
_vec2 = vec;
|
||||
_dir2 = Vector2.Normalize(_vec2);
|
||||
_len = _vec2.Length();
|
||||
_angle = (float)(Math.Atan2(_vec2.Y, _vec2.X) / Math.PI * 180f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过笛卡尔坐标系X和Y值构建向量。
|
||||
/// </summary>
|
||||
/// <param name="x"></param>
|
||||
/// <param name="y"></param>
|
||||
/// <returns></returns>
|
||||
public static Vectoriel FromXY(float x, float y)
|
||||
{
|
||||
return new Vectoriel(new Vector2(x, y));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过极坐标系的角度和距离值构建向量。
|
||||
/// </summary>
|
||||
/// <param name="angle"></param>
|
||||
/// <param name="len"></param>
|
||||
/// <returns></returns>
|
||||
public static Vectoriel FromAngleLen(float angle, float len)
|
||||
{
|
||||
var rad = angle / 180f * Math.PI;
|
||||
return new Vectoriel(new Vector2((float)Math.Cos(rad), (float)Math.Sin(rad)) * len);
|
||||
}
|
||||
|
||||
public static implicit operator Vector2(Vectoriel vec)
|
||||
{
|
||||
return vec._vec2;
|
||||
}
|
||||
|
||||
public static explicit operator Vectoriel(Vector2 vec)
|
||||
{
|
||||
return FromXY(vec.X, vec.Y);
|
||||
}
|
||||
|
||||
public Vector2 Direction => _dir2;
|
||||
|
||||
public float Length => _len;
|
||||
|
||||
public float Angle => _angle;
|
||||
|
||||
private Vector2 _vec2, _dir2;
|
||||
private float _len, _angle;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,774 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace CommonUsage.Mathematics
|
||||
{
|
||||
|
||||
using T3 = Tuple<float, float, float>;
|
||||
using D3 = Tuple<double, double, double>;
|
||||
|
||||
public class CommonMath
|
||||
{
|
||||
public class PrimeEnumerator<T>
|
||||
{
|
||||
public PrimeEnumerator(List<T> items, Func<T, bool> process)
|
||||
{
|
||||
_n = items.Count;
|
||||
_items = items;
|
||||
_process = process;
|
||||
|
||||
foreach (var pNum in _primes)
|
||||
{
|
||||
if (_n % pNum != 0)
|
||||
{
|
||||
_a = pNum;
|
||||
_b = 11;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Enumerate()
|
||||
{
|
||||
using (var enumerator = Get().GetEnumerator())
|
||||
{
|
||||
while (enumerator.MoveNext()) { }
|
||||
}
|
||||
}
|
||||
|
||||
private readonly int _n, _a, _b;
|
||||
private readonly int[] _primes = new[] { 29, 23, 19, 17, 13 };
|
||||
private List<T> _items;
|
||||
private readonly Func<T, bool> _process;
|
||||
|
||||
private IEnumerable<bool> Get()
|
||||
{
|
||||
for (var i = 0; i < _n; ++i)
|
||||
{
|
||||
var id = (i * _a + _b) % _n;
|
||||
yield return _process(_items[id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<IEnumerable<T>> GetPermutationsInternal<T>(IEnumerable<T> list, int length)
|
||||
{
|
||||
if (length == 1) return list.Select(t => new T[] { t });
|
||||
|
||||
return GetPermutationsInternal(list, length - 1)
|
||||
.SelectMany(t => list.Where(e => !t.Contains(e)),
|
||||
(t1, t2) => t1.Concat(new T[] { t2 }));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 得到一组数据的所有排列。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">元素数据类型</typeparam>
|
||||
/// <param name="list">所有待选元素</param>
|
||||
/// <param name="selectNum">所选出的元素数量</param>
|
||||
/// <returns></returns>
|
||||
public static List<List<T>> GetPermutations<T>(List<T> list, int selectNum)
|
||||
{
|
||||
return GetPermutationsInternal(list, selectNum).Select(ll => ll.ToList()).ToList();
|
||||
}
|
||||
|
||||
public static (float bias, Vector2 hPnt, float d) Project2DLine(Vector2 pnt, Vector2 segSt,
|
||||
Vector2 segEnd)
|
||||
{
|
||||
var dir = Vector2.Normalize(segEnd - segSt);
|
||||
var fd = Vector2.Dot(pnt - segSt, dir);
|
||||
var hPnt = segSt + fd * dir;
|
||||
var bias = dir.X * (pnt.Y-segSt.Y) - (pnt.X-segSt.X) * dir.Y;
|
||||
return (bias, hPnt, fd);
|
||||
}
|
||||
|
||||
public static (float bias, Vector2 hPnt, float fd) Project2DLine(Vector2 pnt, Vector2 segSt, float tangent)
|
||||
{
|
||||
var dir = new Vector2((float)System.Math.Cos(tangent / 180 * System.Math.PI), (float)System.Math.Sin(tangent / 180 * System.Math.PI));
|
||||
var fd = Vector2.Dot(pnt - segSt, dir);
|
||||
var hPnt = segSt + fd * dir;
|
||||
var bias = dir.X * (pnt.Y - segSt.Y) - (pnt.X - segSt.X) * dir.Y;
|
||||
return (bias, hPnt, fd);
|
||||
}
|
||||
|
||||
public class LineEqu
|
||||
{
|
||||
public double A, B, C, ln, dAB;
|
||||
public double px1, px2, py1, py2;
|
||||
public float midX;
|
||||
public float midY;
|
||||
}
|
||||
// Fit line with PCA.
|
||||
public LineEqu CalcLine(IEnumerable<Vector2> tls)
|
||||
{
|
||||
var lidarPoint2Ds = tls as Vector2[] ?? tls.ToArray();
|
||||
float fx = lidarPoint2Ds.Average(f => f.X);
|
||||
float fy = lidarPoint2Ds.Average(f => f.Y);
|
||||
float fxx = lidarPoint2Ds.Average(f => f.X * f.X);
|
||||
float fxy = lidarPoint2Ds.Average(f => f.X * f.Y);
|
||||
float fyy = lidarPoint2Ds.Average(f => f.Y * f.Y);
|
||||
float a = fxx - fx * fx, b = fxy - fx * fy, c = fyy - fy * fy;
|
||||
double sqt = System.Math.Sqrt((a - c) * (a - c) + 4 * b * b);
|
||||
double l1 = a + c + sqt;
|
||||
double l2 = a + c - sqt;
|
||||
double dx, dy;
|
||||
if (System.Math.Abs(a - l1 / 2) > System.Math.Abs(c - l1 / 2))
|
||||
{
|
||||
dy = l1 / 2 - a; dx = b;
|
||||
}
|
||||
else
|
||||
{
|
||||
dx = l1 / 2 - c; dy = b;
|
||||
}
|
||||
double norm = System.Math.Sqrt(dx * dx + dy * dy);
|
||||
dx /= norm; dy /= norm;
|
||||
double A = dy, B = -dx, C = dx * fy - dy * fx;
|
||||
double dAB = System.Math.Sqrt(A * A + B * B);
|
||||
|
||||
return new CommonMath.LineEqu
|
||||
{
|
||||
A = A,
|
||||
B = B,
|
||||
C = C,
|
||||
ln = lidarPoint2Ds.Average(p => System.Math.Abs(p.X * A + p.Y * B + C) / dAB),
|
||||
midX = fx,
|
||||
midY = fy
|
||||
};
|
||||
}
|
||||
|
||||
public static double QuadInterp3(double[] confsF)
|
||||
{
|
||||
if (confsF[0] > confsF[1] && confsF[0] > confsF[2])
|
||||
{
|
||||
//printf("left overflow...\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (confsF[1] > confsF[0] && confsF[1] > confsF[2])
|
||||
{
|
||||
return (-(confsF[2] - confsF[0]) / 2.0f / (confsF[0] + confsF[2] - 2.0f * confsF[1] + 0.0001f));
|
||||
}
|
||||
if (confsF[2] > confsF[0] && confsF[2] > confsF[1])
|
||||
{
|
||||
//printf("right overflow...\n");
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static double cross(PointF O, PointF A, PointF B)
|
||||
{
|
||||
return (A.X - O.X) * (B.Y - O.Y) - (A.Y - O.Y) * (B.X - O.X);
|
||||
}
|
||||
|
||||
public static List<PointF> GetConvexHull(List<PointF> points)
|
||||
{
|
||||
if (points == null)
|
||||
return null;
|
||||
|
||||
if (points.Count() <= 1)
|
||||
return points;
|
||||
|
||||
int n = points.Count(), k = 0;
|
||||
List<PointF> H = new List<PointF>(new PointF[2 * n]);
|
||||
|
||||
points.Sort((a, b) =>
|
||||
a.X == b.X ? a.Y.CompareTo(b.Y) : a.X.CompareTo(b.X));
|
||||
|
||||
// Build lower hull
|
||||
for (int i = 0; i < n; ++i)
|
||||
{
|
||||
while (k >= 2 && cross(H[k - 2], H[k - 1], points[i]) <= 0)
|
||||
k--;
|
||||
H[k++] = points[i];
|
||||
}
|
||||
|
||||
// Build upper hull
|
||||
for (int i = n - 2, t = k + 1; i >= 0; i--)
|
||||
{
|
||||
while (k >= t && cross(H[k - 2], H[k - 1], points[i]) <= 0)
|
||||
k--;
|
||||
H[k++] = points[i];
|
||||
}
|
||||
|
||||
return H.Take(k - 1).ToList();
|
||||
}
|
||||
|
||||
public static bool IsPointInPolygon4(PointF[] polygon, PointF testPoint)
|
||||
{
|
||||
// ray casting odd even test.
|
||||
bool result = false;
|
||||
int j = polygon.Count() - 1;
|
||||
for (int i = 0; i < polygon.Count(); i++)
|
||||
{
|
||||
if (polygon[i].Y < testPoint.Y && polygon[j].Y >= testPoint.Y ||
|
||||
polygon[j].Y < testPoint.Y && polygon[i].Y >= testPoint.Y)
|
||||
{
|
||||
if (polygon[i].X + (testPoint.Y - polygon[i].Y) / (polygon[j].Y - polygon[i].Y) *
|
||||
(polygon[j].X - polygon[i].X) < testPoint.X)
|
||||
{
|
||||
result = !result;
|
||||
}
|
||||
}
|
||||
|
||||
j = i;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static double Exp(double val)
|
||||
{
|
||||
if (val < -20) return 0.0000001;
|
||||
if (val > 20) return 99999999999999;
|
||||
long tmp = (long)(1512775 * val + 1072632447);
|
||||
return BitConverter.Int64BitsToDouble(tmp << 32);
|
||||
}
|
||||
public static double gaussmf(double x, double sig, double c)
|
||||
{
|
||||
return Exp(-(x - c) * (x - c) / (2 * sig * sig));
|
||||
}
|
||||
|
||||
public static float Exp(float x)
|
||||
{
|
||||
if (x < -10) return 0;
|
||||
if (x > 10) return 99999999999999;
|
||||
x = 1.0f + x / 64f;
|
||||
x *= x;
|
||||
x *= x;
|
||||
x *= x;
|
||||
x *= x;
|
||||
x *= x;
|
||||
x *= x;
|
||||
return x;
|
||||
}
|
||||
public static float gaussmf(float x, float sig, float c)
|
||||
{
|
||||
return Exp(-(x - c) * (x - c) / (2 * sig * sig));
|
||||
}
|
||||
|
||||
public static D3 Transform2D(D3 src, D3 t)
|
||||
{
|
||||
var rth = src.Item3 / 180.0 * System.Math.PI;
|
||||
var p1dtx = (src.Item1 + System.Math.Cos(rth) * t.Item1 -
|
||||
System.Math.Sin(rth) * t.Item2);
|
||||
var p1dty = (src.Item2 + System.Math.Sin(rth) * t.Item1 +
|
||||
System.Math.Cos(rth) * t.Item2);
|
||||
var p1dtth = src.Item3 + t.Item3;
|
||||
return Tuple.Create(p1dtx, p1dty, p1dtth);
|
||||
}
|
||||
|
||||
public struct LngLatToXY
|
||||
{
|
||||
public double scale;
|
||||
public double rad;
|
||||
public double biasX, biasY;
|
||||
}
|
||||
|
||||
public static LngLatToXY GetTransformLngLatToXY(Vector2 lnglat1, Vector2 xy1, Vector2 lnglat2, Vector2 xy2)
|
||||
{
|
||||
var scale = (xy1 - xy2).Length() / (lnglat1 - lnglat2).Length();
|
||||
var dxy = (xy1 - xy2);
|
||||
var dlnglat = lnglat1 - lnglat2;
|
||||
var rad = System.Math.Atan2(dxy.X, dxy.Y) - System.Math.Atan2(dlnglat.X, dlnglat.Y);
|
||||
var intm = lnglat1 * scale;
|
||||
var biasX = xy1.X - (intm.X * System.Math.Cos(rad) - intm.Y * System.Math.Sin(rad));
|
||||
var biasY = xy1.Y - (intm.X * System.Math.Sin(rad) + intm.Y * System.Math.Cos(rad));
|
||||
return new LngLatToXY {rad = rad, biasX = biasX, biasY = biasY, scale = scale};
|
||||
}
|
||||
|
||||
public Vector2 TransformLngLatToXY(Vector2 lnglat, LngLatToXY t)
|
||||
{
|
||||
var intm = lnglat * (float) t.scale;
|
||||
return new Vector2((float) (intm.X * System.Math.Cos(t.rad) - intm.Y * System.Math.Sin(t.rad) + t.biasX),
|
||||
(float) (intm.X * System.Math.Sin(t.rad) + intm.Y * System.Math.Cos(t.rad) + t.biasY));
|
||||
}
|
||||
|
||||
public static D3 ReverseTransform(D3 dest, D3 t)
|
||||
{
|
||||
var rth = (dest.Item3 - t.Item3) / 180.0 * System.Math.PI;
|
||||
var nxT = (dest.Item1 - System.Math.Cos(rth) * t.Item1 +
|
||||
System.Math.Sin(rth) * t.Item2);
|
||||
var nyT = (dest.Item2 - System.Math.Sin(rth) * t.Item1 -
|
||||
System.Math.Cos(rth) * t.Item2);
|
||||
var pth = dest.Item3 - t.Item3;
|
||||
return Tuple.Create(nxT, nyT, pth);
|
||||
}
|
||||
|
||||
public static D3 SolveTransform2D(D3 src, D3 dest)
|
||||
{
|
||||
var th = dest.Item3 - src.Item3;
|
||||
th = (th - System.Math.Round((th) / 360.0f) * 360);
|
||||
var rth = src.Item3 / 180.0 * System.Math.PI;
|
||||
var x = ((dest.Item1 - src.Item1) * System.Math.Cos(rth) +
|
||||
(dest.Item2 - src.Item2) * System.Math.Sin(rth));
|
||||
var y = (-(dest.Item1 - src.Item1) * System.Math.Sin(rth) +
|
||||
(dest.Item2 - src.Item2) * System.Math.Cos(rth));
|
||||
return Tuple.Create(x, y, th);
|
||||
}
|
||||
|
||||
public static T3 Transform2D(T3 src, T3 t)
|
||||
{
|
||||
var rth = src.Item3 / 180.0 * System.Math.PI;
|
||||
var p1dtx = (float)(src.Item1 + System.Math.Cos(rth) * t.Item1 -
|
||||
System.Math.Sin(rth) * t.Item2);
|
||||
var p1dty = (float)(src.Item2 + System.Math.Sin(rth) * t.Item1 +
|
||||
System.Math.Cos(rth) * t.Item2);
|
||||
var p1dtth = src.Item3 + t.Item3;
|
||||
return Tuple.Create(p1dtx, p1dty, p1dtth);
|
||||
}
|
||||
|
||||
public static Vector2 Transform2D(Vector3 src, Vector3 t)
|
||||
{
|
||||
var tup = Transform2D(Tuple.Create(src.X, src.Y, src.Z), Tuple.Create(t.X, t.Y, t.Z));
|
||||
return new Vector2(tup.Item1, tup.Item2);
|
||||
}
|
||||
|
||||
public static Vector2 Transform2D(Vector2 srcPos, float srcTh, Vector2 dt, float dth = 0)
|
||||
{
|
||||
var tup = Transform2D(Tuple.Create(srcPos.X, srcPos.Y, srcTh), Tuple.Create(dt.X, dt.Y, dth));
|
||||
return new Vector2(tup.Item1, tup.Item2);
|
||||
}
|
||||
|
||||
public static T3 ReverseTransform(T3 dest, T3 t)
|
||||
{
|
||||
var rth = (dest.Item3 - t.Item3) / 180.0 * System.Math.PI;
|
||||
var nxT = (float)(dest.Item1 - System.Math.Cos(rth) * t.Item1 +
|
||||
System.Math.Sin(rth) * t.Item2);
|
||||
var nyT = (float)(dest.Item2 - System.Math.Sin(rth) * t.Item1 -
|
||||
System.Math.Cos(rth) * t.Item2);
|
||||
var pth = dest.Item3 - t.Item3;
|
||||
return Tuple.Create(nxT, nyT, pth);
|
||||
}
|
||||
|
||||
public static T3 SolveTransform2D(T3 src, T3 dest)
|
||||
{
|
||||
var th = dest.Item3 - src.Item3;
|
||||
th = (float)(th - System.Math.Round((th) / 360.0f) * 360);
|
||||
var rth = src.Item3 / 180.0 * System.Math.PI;
|
||||
var x = (float)((dest.Item1 - src.Item1) * System.Math.Cos(rth) +
|
||||
(dest.Item2 - src.Item2) * System.Math.Sin(rth));
|
||||
var y = (float)(-(dest.Item1 - src.Item1) * System.Math.Sin(rth) +
|
||||
(dest.Item2 - src.Item2) * System.Math.Cos(rth));
|
||||
return Tuple.Create(x, y, th);
|
||||
}
|
||||
|
||||
public static Vector2 SolveTransform2D(Vector2 srcPos, float srcTh, Vector2 dt, float dth = 0)
|
||||
{
|
||||
var tup = SolveTransform2D(Tuple.Create(srcPos.X, srcPos.Y, srcTh), Tuple.Create(dt.X, dt.Y, dth));
|
||||
return new Vector2(tup.Item1, tup.Item2);
|
||||
}
|
||||
|
||||
public static double dist(double x1, double y1, double x2, double y2)
|
||||
{
|
||||
return System.Math.Sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
private struct FloatIntUnion
|
||||
{
|
||||
[FieldOffset(0)] public float f;
|
||||
|
||||
[FieldOffset(0)] public int tmp;
|
||||
}
|
||||
|
||||
public static float Sqrt(float z)
|
||||
{
|
||||
FloatIntUnion u;
|
||||
u.tmp = 0;
|
||||
u.f = z;
|
||||
u.tmp -= 1 << 23; /* Subtract 2^m. */
|
||||
u.tmp >>= 1; /* Divide by 2. */
|
||||
u.tmp += 1 << 29; /* Add ((b + 1) / 2) * 2^m. */
|
||||
return u.f;
|
||||
}
|
||||
|
||||
public static float dist2(float x1, float y1, float x2, float y2)
|
||||
{
|
||||
return ((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
|
||||
}
|
||||
|
||||
|
||||
public static float d2(float x1, float y1, float x2, float y2)
|
||||
{
|
||||
return (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2);
|
||||
}
|
||||
|
||||
public static float ThAverage(List<float> angles)
|
||||
{
|
||||
var anchor = angles[0];
|
||||
var diff = 0f;
|
||||
foreach (var angle in angles)
|
||||
diff += ThDiff(angle, anchor);
|
||||
return RoundTh(anchor + diff / angles.Count);
|
||||
}
|
||||
|
||||
public static float ThDiff(float th1, float th2)
|
||||
{
|
||||
return (float)(th1 - th2 -
|
||||
System.Math.Round((th1 - th2) / 360.0f) * 360);
|
||||
}
|
||||
|
||||
public static double ThDiff(double th1, double th2)
|
||||
{
|
||||
return th1 - th2 -
|
||||
System.Math.Round((th1 - th2) / 360.0f) * 360;
|
||||
}
|
||||
|
||||
public static double refine(double x)
|
||||
{
|
||||
if (x < 1 && x > -1) return x;
|
||||
if (x > 1)
|
||||
return (2 / (1 + System.Math.Exp(-((x - 1) * 2))));
|
||||
return (2 / (1 + System.Math.Exp(-((x + 1) * 2)))) - 2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 求点p到两点式直线p1p2的距离
|
||||
/// </summary>
|
||||
/// <param name="x">点p的x坐标</param>
|
||||
/// <param name="y">点p的y坐标</param>
|
||||
/// <param name="x1">直线点p1的x坐标</param>
|
||||
/// <param name="y1">直线点p1的y坐标</param>
|
||||
/// <param name="x2">直线点p2的x坐标</param>
|
||||
/// <param name="y2">直线点p2的y坐标</param>
|
||||
/// <returns></returns>
|
||||
public static double Point2LineDist(double x, double y, double x1, double y1, double x2, double y2)
|
||||
{
|
||||
double a1 = -(y1 - y2) / 10;
|
||||
double b1 = (x1 - x2) / 10;
|
||||
double c1 = (x1 * (y1 - y2) - y1 * (x1 - x2)) / 10;
|
||||
return System.Math.Abs(a1 * x + b1 * y + c1) / System.Math.Sqrt(a1 * a1 + b1 * b1);
|
||||
}
|
||||
|
||||
public static double Point2LineDist(Vector2 p, LineSegment ll)
|
||||
{
|
||||
double a1 = -(ll.Src.Y - ll.Dst.Y) / 10;
|
||||
double b1 = (ll.Src.X - ll.Dst.X) / 10;
|
||||
double c1 = (ll.Src.X * (ll.Src.Y - ll.Dst.Y) - ll.Src.Y * (ll.Src.X - ll.Dst.X)) / 10;
|
||||
return System.Math.Abs(a1 * p.X + b1 * p.Y + c1) / System.Math.Sqrt(a1 * a1 + b1 * b1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 两条两点式直线间的夹角
|
||||
/// </summary>
|
||||
/// <param name="x1"></param>
|
||||
/// <param name="y1"></param>
|
||||
/// <param name="x2"></param>
|
||||
/// <param name="y2"></param>
|
||||
/// <param name="x3"></param>
|
||||
/// <param name="y3"></param>
|
||||
/// <param name="x4"></param>
|
||||
/// <param name="y4"></param>
|
||||
/// <returns>角度制</returns>
|
||||
public static double AngleBetweenLines(double x1, double y1, double x2, double y2, double x3, double y3,
|
||||
double x4, double y4)
|
||||
{
|
||||
var vec1 = new Vector2((float)(x2 - x1), (float)(y2 - y1));
|
||||
var vec2 = new Vector2((float)(x4 - x3), (float)(y4 - y3));
|
||||
return System.Math.Acos(System.Math.Abs(Vector2.Dot(vec1, vec2) / vec1.Length() / vec2.Length())) / System.Math.PI * 180;
|
||||
}
|
||||
|
||||
public static double AngleBetweenLines(LineSegment ls1, LineSegment ls2)
|
||||
{
|
||||
return AngleBetweenLines(ls1.Src.X, ls1.Src.Y, ls1.Dst.X, ls1.Dst.Y, ls2.Src.X, ls2.Src.Y, ls2.Dst.X,
|
||||
ls2.Dst.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 两向量间夹角
|
||||
/// </summary>
|
||||
/// <param name="x1"></param>
|
||||
/// <param name="y1"></param>
|
||||
/// <param name="x2"></param>
|
||||
/// <param name="y2"></param>
|
||||
/// <param name="x3"></param>
|
||||
/// <param name="y3"></param>
|
||||
/// <param name="x4"></param>
|
||||
/// <param name="y4"></param>
|
||||
/// <returns>角度制</returns>
|
||||
public static double AngleBetweenVectors(double x1, double y1, double x2, double y2, double x3, double y3,
|
||||
double x4, double y4)
|
||||
{
|
||||
var vec1 = new Vector2((float)(x2 - x1), (float)(y2 - y1));
|
||||
var vec2 = new Vector2((float)(x4 - x3), (float)(y4 - y3));
|
||||
return System.Math.Acos(Vector2.Dot(vec1, vec2) / vec1.Length() / vec2.Length()) / System.Math.PI * 180;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 两向量间夹角.
|
||||
/// </summary>
|
||||
/// <param name="vec1"></param>
|
||||
/// <param name="vec2"></param>
|
||||
/// <returns>角度制</returns>
|
||||
public static double AngleBetweenVectors(Vector2 vec1, Vector2 vec2)
|
||||
{
|
||||
return System.Math.Acos(Vector2.Dot(vec1, vec2) / vec1.Length() / vec2.Length()) / System.Math.PI * 180;
|
||||
}
|
||||
|
||||
public static double AngleBetweenVectors(Vector3 vector1, Vector3 vector2)
|
||||
{
|
||||
float dotProduct = Vector3.Dot(vector1, vector2);
|
||||
float magnitude1 = vector1.Length();
|
||||
float magnitude2 = vector2.Length();
|
||||
float cosine = dotProduct / (magnitude1 * magnitude2);
|
||||
|
||||
return System.Math.Acos(cosine) / System.Math.PI * 180;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 求点到直线的垂足
|
||||
/// </summary>
|
||||
/// <param name="x"></param>
|
||||
/// <param name="y"></param>
|
||||
/// <param name="x1"></param>
|
||||
/// <param name="y1"></param>
|
||||
/// <param name="x2"></param>
|
||||
/// <param name="y2"></param>
|
||||
/// <returns></returns>
|
||||
public static (double, double) PerpendicularPoint(double x, double y, double x1, double y1, double x2, double y2)
|
||||
{
|
||||
double lx = x2 - x1, ly = y2 - y1, dAB = lx * lx + ly * ly;
|
||||
var u = ((x - x1) * lx + (y - y1) * ly) / dAB;
|
||||
return new(x1 + u * lx, y1 + u * ly);
|
||||
}
|
||||
|
||||
public static Vector2 PerpendicularPoint(Vector2 p, LineSegment ls)
|
||||
{
|
||||
double lx = ls.Dst.X - ls.Src.X, ly = ls.Dst.Y - ls.Src.Y, dAB = lx * lx + ly * ly;
|
||||
var u = ((p.X - ls.Src.X) * lx + (p.Y - ls.Src.Y) * ly) / dAB;
|
||||
return new Vector2((float)(ls.Src.X + u * lx), (float)(ls.Src.Y + u * ly));
|
||||
}
|
||||
|
||||
public static double PerpendicularPosition(double x, double y, double x1, double y1, double x2, double y2)
|
||||
{
|
||||
double lx = x2 - x1, ly = y2 - y1;
|
||||
var dAB = CommonMath.Sqrt((float)(lx * lx + ly * ly));
|
||||
lx /= dAB;
|
||||
ly /= dAB;
|
||||
return (x - x1) * lx + (y - y1) * ly;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 最小二乘法拟合直线,得到两点式。
|
||||
/// </summary>
|
||||
/// <param name="pts">待拟合的点集,应至少有2个点。</param>
|
||||
/// <param name="maxDist2Line">检查是否所有点距直线的距离均小于maxDist2Line,若为-1则不检查。</param>
|
||||
/// <returns>返回两点式的两个端点坐标。若坐标为全0,则拟合失败。</returns>
|
||||
public static (bool, Vector2, Vector2) FitLineSegment(List<Vector2> pts, double maxDist2Line = -1)
|
||||
{
|
||||
if (pts.Count < 2)
|
||||
{
|
||||
Console.WriteLine($"Points too Few! {pts.Count}! Cannot perform line fitting!",
|
||||
MethodBase.GetCurrentMethod()?.Name ?? "FitLine");
|
||||
return (false, Vector2.Zero, Vector2.Zero);
|
||||
};
|
||||
|
||||
// y = kx + b
|
||||
double A = 0, B = 0, C = 0, D = 0;
|
||||
foreach (var p in pts)
|
||||
{
|
||||
A += p.X * p.X;
|
||||
B += p.X;
|
||||
C += p.X * p.Y;
|
||||
D += p.Y;
|
||||
}
|
||||
|
||||
var tmp = A * pts.Count - B * B;
|
||||
var k = (C * pts.Count - B * D) / tmp;
|
||||
var b = (A * D - C * B) / tmp;
|
||||
|
||||
double x1 = 0,
|
||||
y1 = k * x1 + b,
|
||||
x2 = 1000,
|
||||
y2 = k * x2 + b;
|
||||
|
||||
double CalcDist(ref bool fail, ref Vector2 endP, ref Vector2 endQ)
|
||||
{
|
||||
double distSum = 0;
|
||||
double lx = x2 - x1, ly = y2 - y1, dAB = lx * lx + ly * ly;
|
||||
double minU = double.MaxValue, maxU = double.MinValue;
|
||||
|
||||
foreach (var p in pts)
|
||||
{
|
||||
var u = ((p.X - x1) * lx + (p.Y - y1) * ly) / dAB;
|
||||
var perp = new Vector2((float)(x1 + u * lx), (float)(y1 + u * ly));
|
||||
if (u < minU)
|
||||
{
|
||||
endP = perp;
|
||||
minU = u;
|
||||
}
|
||||
if (u > maxU)
|
||||
{
|
||||
endQ = perp;
|
||||
maxU = u;
|
||||
}
|
||||
|
||||
var curDist = dist(perp.X, perp.Y, p.X, p.Y);
|
||||
if (maxDist2Line > -1 && curDist > maxDist2Line) fail = true;
|
||||
distSum += curDist;
|
||||
}
|
||||
|
||||
return distSum;
|
||||
}
|
||||
|
||||
var kbFail = false;
|
||||
Vector2 endP1 = new Vector2(), endQ1 = new Vector2();
|
||||
double kbDist = CalcDist(ref kbFail, ref endP1, ref endQ1);
|
||||
|
||||
// x = my + n
|
||||
A = 0;
|
||||
B = 0;
|
||||
C = 0;
|
||||
D = 0;
|
||||
foreach (var p in pts)
|
||||
{
|
||||
A += p.X * p.Y;
|
||||
B += p.Y * p.Y;
|
||||
C += p.Y;
|
||||
D += p.X;
|
||||
}
|
||||
|
||||
tmp = C * C - B * pts.Count;
|
||||
var m = (C * D - A * pts.Count) / tmp;
|
||||
var n = (A * C - B * D) / tmp;
|
||||
y1 = 0;
|
||||
x1 = m * y1 + n;
|
||||
y2 = 1000;
|
||||
x2 = m * y2 + n;
|
||||
|
||||
var mnFail = false;
|
||||
Vector2 endP2 = new Vector2(), endQ2 = new Vector2();
|
||||
double mnDist = CalcDist(ref mnFail, ref endP2, ref endQ2);
|
||||
|
||||
Vector2 endP = endP1, endQ = endQ1;
|
||||
if (mnDist < kbDist)
|
||||
{
|
||||
if (mnFail) return (false, new Vector2(), new Vector2());
|
||||
endP = endP2;
|
||||
endQ = endQ2;
|
||||
}
|
||||
else if (kbFail) return (false, new Vector2(), new Vector2());
|
||||
|
||||
return (true, endP, endQ);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int toId(int x, int y, int z)
|
||||
{
|
||||
return (x * 1140671485 + 12820163) ^ (y * 134775813 + 1) ^ (z * 1103515245 + 12345);
|
||||
}
|
||||
|
||||
public class Clustering<T>
|
||||
{
|
||||
public int numIteration = 3;
|
||||
public int itemNumThreshold = 10;
|
||||
|
||||
public Func<T, T, bool> inRange;
|
||||
public Func<List<T>, T> average;
|
||||
private readonly List<T> _inputData;
|
||||
private Dictionary<T, List<T>> _clusters = new Dictionary<T, List<T>>();
|
||||
|
||||
public Clustering(List<T> data, Func<T, T, bool> inRange, Func<List<T>, T> average)
|
||||
{
|
||||
_inputData = data;
|
||||
this.inRange = inRange;
|
||||
this.average = average;
|
||||
}
|
||||
|
||||
public Dictionary<T, List<T>> GetClusters()
|
||||
{
|
||||
var tmp = new List<(T center, List<T> items)>();
|
||||
|
||||
for (var iter = 0; iter < numIteration; iter++)
|
||||
{
|
||||
tmp = tmp.Where(cluster => cluster.items.Count > itemNumThreshold)
|
||||
.Select(cluster => (average(cluster.items), new List<T>())).ToList();
|
||||
|
||||
foreach (var data in _inputData)
|
||||
{
|
||||
var added = false;
|
||||
foreach (var cluster in tmp)
|
||||
{
|
||||
if (inRange(cluster.center, data))
|
||||
{
|
||||
cluster.items.Add(data);
|
||||
added = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!added)
|
||||
tmp.Add((data, new List<T>() { data }));
|
||||
}
|
||||
}
|
||||
|
||||
_clusters = tmp.Where(cluster => cluster.items.Count > itemNumThreshold)
|
||||
.ToDictionary(cluster => cluster.center, cluster => cluster.items);
|
||||
return _clusters;
|
||||
}
|
||||
}
|
||||
|
||||
public static (bool, Vector2) TwoLinesIntersection(Vector2 A, Vector2 B, Vector2 C, Vector2 D)
|
||||
{
|
||||
// Line AB represented as a1x + b1y = c1
|
||||
double a1 = B.Y - A.Y;
|
||||
double b1 = A.X - B.X;
|
||||
double c1 = a1 * (A.X) + b1 * (A.Y);
|
||||
|
||||
// Line CD represented as a2x + b2y = c2
|
||||
double a2 = D.Y - C.Y;
|
||||
double b2 = C.X - D.X;
|
||||
double c2 = a2 * (C.X) + b2 * (C.Y);
|
||||
|
||||
double determinant = a1 * b2 - a2 * b1;
|
||||
|
||||
if (determinant == 0)
|
||||
{
|
||||
// The lines are parallel. This is simplified
|
||||
// by returning a pair of FLT_MAX
|
||||
return new(false, new Vector2());
|
||||
}
|
||||
else
|
||||
{
|
||||
double x = (b2 * c1 - b1 * c2) / determinant;
|
||||
double y = (a1 * c2 - a2 * c1) / determinant;
|
||||
return (true, new Vector2((float)x, (float)y));
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsAtLeft(Vector2 anchor, Vector2 dest, Vector2 p)
|
||||
{
|
||||
var v1 = new Vector3(anchor - p, 0);
|
||||
var v2 = new Vector3(dest - p, 0);
|
||||
return Vector3.Cross(v1, v2).Z > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将角度转化至-180到180度的范围内。
|
||||
/// </summary>
|
||||
/// <param name="th"></param>
|
||||
/// <returns></returns>
|
||||
public static double RoundTh(double th)
|
||||
{
|
||||
return th - System.Math.Round(th / 360) * 360;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将角度转化至-180到180度的范围内。
|
||||
/// </summary>
|
||||
/// <param name="th"></param>
|
||||
/// <returns></returns>
|
||||
public static float RoundTh(float th)
|
||||
{
|
||||
return th - (float)System.Math.Round(th / 360f) * 360f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace CommonUsage.Mathematics
|
||||
{
|
||||
/// <summary>
|
||||
/// 表示一条线段。
|
||||
/// </summary>
|
||||
public class LineSegment
|
||||
{
|
||||
/// <summary>
|
||||
/// 默认构造函数。所有坐标初始化为0。
|
||||
/// </summary>
|
||||
public LineSegment()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用两个端点初始化一段2D线段。
|
||||
/// </summary>
|
||||
/// <param name="src">线段起点。</param>
|
||||
/// <param name="dst">线段终点。</param>
|
||||
public LineSegment(Vector2 src, Vector2 dst)
|
||||
{
|
||||
Src = src;
|
||||
Dst = dst;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用两个端点初始化一段3D线段。
|
||||
/// </summary>
|
||||
/// <param name="src">线段起点。</param>
|
||||
/// <param name="dst">线段终点。</param>
|
||||
public LineSegment(Vector3 src, Vector3 dst)
|
||||
{
|
||||
Src3D = src;
|
||||
Dst3D = dst;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用两个端点初始化一条线段,2D。
|
||||
/// </summary>
|
||||
/// <param name="x1">线段起点x坐标。</param>
|
||||
/// <param name="y1">线段起点y坐标。</param>
|
||||
/// <param name="x2">线段终点x坐标。</param>
|
||||
/// <param name="y2">线段终点y坐标。</param>
|
||||
public LineSegment(double x1, double y1, double x2, double y2)
|
||||
{
|
||||
Src = new Vector2((float)x1, (float)y1);
|
||||
Dst = new Vector2((float)x2, (float)y2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用两个端点初始化一条线段,2D。
|
||||
/// </summary>
|
||||
/// <param name="x1">线段起点x坐标。</param>
|
||||
/// <param name="y1">线段起点y坐标。</param>
|
||||
/// <param name="z1">线段起点y坐标。</param>
|
||||
/// <param name="x2">线段终点x坐标。</param>
|
||||
/// <param name="y2">线段终点y坐标。</param>
|
||||
/// <param name="z2">线段终点y坐标。</param>
|
||||
public LineSegment(double x1, double y1, double z1, double x2, double y2, double z2)
|
||||
{
|
||||
Src3D = new Vector3((float)x1, (float)y1, (float)z1);
|
||||
Dst3D = new Vector3((float)x2, (float)y2, (float)z2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回线段长度,2D。
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public double Length()
|
||||
{
|
||||
return Vector2.Distance(Src, Dst);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回线段长度,3D。
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public double Length3D()
|
||||
{
|
||||
return Vector3.Distance(Src3D, Dst3D);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回线段与x轴正方向夹角度数,角度制。
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public double Angle()
|
||||
{
|
||||
return Math.Atan2(Dst.Y - Src.Y, Dst.X - Src.X) / Math.PI * 180;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回一段方向相反的线段。
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public LineSegment Reverse()
|
||||
{
|
||||
return new LineSegment(Dst3D, Src3D);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 线段2D起点。
|
||||
/// </summary>
|
||||
public Vector2 Src
|
||||
{
|
||||
get => new(_srcX, _srcY);
|
||||
set
|
||||
{
|
||||
_srcX = value.X;
|
||||
_srcY = value.Y;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 线段2D终点。
|
||||
/// </summary>
|
||||
public Vector2 Dst
|
||||
{
|
||||
get => new(_dstX, _dstY);
|
||||
set
|
||||
{
|
||||
_dstX = value.X;
|
||||
_dstY = value.Y;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 线段3D起点。
|
||||
/// </summary>
|
||||
public Vector3 Src3D
|
||||
{
|
||||
get => new(_srcX, _srcY, _srcZ);
|
||||
set
|
||||
{
|
||||
_srcX = value.X;
|
||||
_srcY = value.Y;
|
||||
_srcZ = value.Z;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 线段3D终点。
|
||||
/// </summary>
|
||||
public Vector3 Dst3D
|
||||
{
|
||||
get => new(_dstX, _dstY, _dstZ);
|
||||
set
|
||||
{
|
||||
_dstX = value.X;
|
||||
_dstY = value.Y;
|
||||
_dstZ = value.Z;
|
||||
}
|
||||
}
|
||||
|
||||
private float _srcX, _srcY, _srcZ, _dstX, _dstY, _dstZ;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"profiles": {
|
||||
"CommonUsage": {
|
||||
"commandName": "Project"
|
||||
},
|
||||
"配置文件 1": {
|
||||
"commandName": "Executable",
|
||||
"executablePath": "D:\\Code\\Core\\Medulla\\build\\Medulla.exe",
|
||||
"workingDirectory": "D:\\Code\\Core\\Medulla\\build\\"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CommonUsage.Protocols.VDA5050
|
||||
{
|
||||
public class CommunicationProtocolFactory
|
||||
{
|
||||
public static IVDACommunicationProtocol CreateProtocol(string protocolType, string host, int port)
|
||||
{
|
||||
return protocolType.ToLower() switch
|
||||
{
|
||||
"http" => new HTTPCommunication(host, port),
|
||||
"mqtt" => new MQTTCommunication(host, port),
|
||||
_ => throw new NotSupportedException($"Protocol {protocolType} is not supported")
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using CommonUsage.Protocols.VDA5050.Messages;
|
||||
using FundamentalLib;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CommonUsage.Protocols.VDA5050
|
||||
{
|
||||
public class HTTPCommunication : IVDACommunicationProtocol
|
||||
{
|
||||
private readonly string _host;
|
||||
private readonly int _port;
|
||||
|
||||
public HTTPCommunication(string host, int port)
|
||||
{
|
||||
_host = host;
|
||||
_port = port;
|
||||
}
|
||||
|
||||
public async Task PublishConnectionStatus(string status)
|
||||
{
|
||||
var message = new connectionMessage()
|
||||
{
|
||||
serialNumber = "test-01",
|
||||
headerId = 1,
|
||||
timestamp = DateTime.Now,
|
||||
connectionState = status
|
||||
};
|
||||
|
||||
await SendMessageAsync(message, "vda5050/connection");
|
||||
}
|
||||
|
||||
public void SetupOrderListener(Action<orderMessage> orderReceived)
|
||||
{
|
||||
PicoHttpServer.AddPostTextHandler("/order", new { }, (_, str) =>
|
||||
{
|
||||
var order = JsonConvert.DeserializeObject<orderMessage>(str);
|
||||
orderReceived(order);
|
||||
return "";
|
||||
});
|
||||
}
|
||||
|
||||
public void SetUpInstanActionListener(Action<instanceAction> onInstanceActionReceived)
|
||||
{
|
||||
PicoHttpServer.AddPostTextHandler("/instanceAction", new { }, (_, str) =>
|
||||
{
|
||||
var instanceAction = JsonConvert.DeserializeObject<instanceAction>(str);
|
||||
onInstanceActionReceived(instanceAction);
|
||||
return "";
|
||||
});
|
||||
}
|
||||
|
||||
public void SetUpImmediateCommandListener(Action<string> onChangeCarFieldsReceived)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public async Task SendMessageAsync<T>(T message, string topic)
|
||||
{
|
||||
try
|
||||
{
|
||||
var url = $"http://{_host}:{_port}/{topic}";
|
||||
using var client = new HttpClient();
|
||||
var response = await client.PostAsync(url, new StringContent(JsonConvert.SerializeObject(message), Encoding.UTF8, "application/json"));
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
Console.WriteLine($" >> Sending Message: Failed to send message. Status Code: {response.StatusCode}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error in sending message: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SendVisualizationMessageAsync<T>(T msg, string topic)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void SetupTestListener(Action<string> testMsg)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public async Task PublishFactSheet(factsheetMessage message)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using CommonUsage.Protocols.VDA5050.Messages;
|
||||
|
||||
namespace CommonUsage.Protocols.VDA5050
|
||||
{
|
||||
public interface IVDACommunicationProtocol
|
||||
{
|
||||
Task PublishConnectionStatus(string status);
|
||||
Task PublishFactSheet(factsheetMessage msg);
|
||||
void SetupOrderListener(Action<orderMessage> orderReceived);
|
||||
void SetUpInstanActionListener(Action<instanceAction> onInstanceActionReceived);
|
||||
void SetUpImmediateCommandListener(Action<string> onChangeCarFieldsReceived);
|
||||
Task SendMessageAsync<T>(T message, string topic);
|
||||
Task SendVisualizationMessageAsync<T>(T message, string topic);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
using System.IO;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommonUsage.Protocols.VDA5050.Messages;
|
||||
using MQTTnet;
|
||||
using MQTTnet.Client;
|
||||
using MQTTnet.Extensions.ManagedClient;
|
||||
using MQTTnet.Packets;
|
||||
using MQTTnet.Protocol;
|
||||
using MQTTnet.Server;
|
||||
using Newtonsoft.Json;
|
||||
using FundamentalLib;
|
||||
using CommonUsage.Protocols.VDA5050.Objects;
|
||||
using FundamentalLib.MiscHelpers;
|
||||
|
||||
namespace CommonUsage.Protocols.VDA5050
|
||||
{
|
||||
public class MQTTCommunication : IVDACommunicationProtocol
|
||||
{
|
||||
private readonly string _host;
|
||||
private readonly int _port;
|
||||
private readonly string _orderTopic = "vda5050/frldAGV/order";
|
||||
private readonly string _connectionTopic = "vda5050/frldAGV/connection";
|
||||
private readonly string _instanceAction = "vda5050/frldAGV/instantActions";
|
||||
private readonly string _factsheet = "vda5050/frldAGV/factsheet";
|
||||
private readonly string _changeCarFields = "vda5050/frldAGV/changeCarFields";
|
||||
|
||||
private IManagedMqttClient _client;
|
||||
private IManagedMqttClient _visualizationClient;
|
||||
|
||||
public MQTTCommunication(string host, int port)
|
||||
{
|
||||
_host = host;
|
||||
_port = port;
|
||||
InitializeClient();
|
||||
InitializeVisualizationClient();
|
||||
}
|
||||
|
||||
private void InitializeClient()
|
||||
{
|
||||
var willMessage = new connectionMessage()
|
||||
{
|
||||
headerId = 1,
|
||||
timestamp = DateTime.Now,
|
||||
version = "00",
|
||||
manufacturer = "frld",
|
||||
serialNumber = "test-01",
|
||||
connectionState = "CONNECTIONBROKEN"
|
||||
};
|
||||
|
||||
var mqttClientOptions = new MqttClientOptionsBuilder()
|
||||
.WithClientId("AGV-Client-frldAGV")
|
||||
.WithTcpServer(_host, _port)
|
||||
.WithWillTopic(_connectionTopic)
|
||||
.WithWillPayload(JsonConvert.SerializeObject(willMessage))
|
||||
.WithWillRetain(true)
|
||||
.Build();
|
||||
|
||||
var managedMqttClientOptions = new ManagedMqttClientOptionsBuilder()
|
||||
.WithClientOptions(mqttClientOptions)
|
||||
.WithMaxPendingMessages(20)
|
||||
.WithPendingMessagesOverflowStrategy(MqttPendingMessagesOverflowStrategy.DropOldestQueuedMessage)
|
||||
.Build();
|
||||
|
||||
_client = new MqttFactory().CreateManagedMqttClient();
|
||||
_client.StartAsync(managedMqttClientOptions).GetAwaiter().GetResult();
|
||||
Console.WriteLine($" >> MQTT client initialized and connected to broker at {_host} - {_port}");
|
||||
}
|
||||
|
||||
private void InitializeVisualizationClient()
|
||||
{
|
||||
var mqttClientOptions = new MqttClientOptionsBuilder()
|
||||
.WithClientId("AGV-Visualization")
|
||||
.WithTcpServer(_host, _port)
|
||||
.Build();
|
||||
|
||||
var managedMqttClientOptions = new ManagedMqttClientOptionsBuilder()
|
||||
.WithClientOptions(mqttClientOptions)
|
||||
.WithMaxPendingMessages(10) // Prevent overloading
|
||||
.WithPendingMessagesOverflowStrategy(MqttPendingMessagesOverflowStrategy.DropOldestQueuedMessage)
|
||||
.Build();
|
||||
|
||||
_visualizationClient = new MqttFactory().CreateManagedMqttClient();
|
||||
_visualizationClient.StartAsync(managedMqttClientOptions).GetAwaiter().GetResult();
|
||||
Console.WriteLine("MQTT Visualization client initialized.");
|
||||
}
|
||||
|
||||
public void SetUpImmediateCommandListener(Action<string> onChangeCarFieldsReceived)
|
||||
{
|
||||
Console.WriteLine($"Subscribed to the topic: {_changeCarFields}");
|
||||
_client.SubscribeAsync(_changeCarFields).GetAwaiter().GetResult();
|
||||
|
||||
_client.ApplicationMessageReceivedAsync += async e =>
|
||||
{
|
||||
if (e.ApplicationMessage.Topic == _changeCarFields)
|
||||
{
|
||||
var payload = Encoding.UTF8.GetString(e.ApplicationMessage.Payload);
|
||||
LogMessage($"RECEIVE-{e.ApplicationMessage.Topic}", e.ApplicationMessage.Topic, payload);
|
||||
//var script = JsonConvert.DeserializeObject<string>(payload);
|
||||
Console.WriteLine($"Change car field: {payload}");
|
||||
onChangeCarFieldsReceived(payload);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public void SetUpInstanActionListener(Action<instanceAction> onInstanceActionReceived)
|
||||
{
|
||||
Console.WriteLine($"Subscribed to the topic: {_instanceAction}");
|
||||
|
||||
// Subscribe to the instanceAction topic
|
||||
_client.SubscribeAsync(_instanceAction).GetAwaiter().GetResult();
|
||||
|
||||
_client.ApplicationMessageReceivedAsync += async e =>
|
||||
{
|
||||
if (e.ApplicationMessage.Topic == _instanceAction)
|
||||
{
|
||||
var payload = Encoding.UTF8.GetString(e.ApplicationMessage.Payload);
|
||||
LogMessage($"RECEIVE-{e.ApplicationMessage.Topic}", e.ApplicationMessage.Topic, payload);
|
||||
var actions = JsonConvert.DeserializeObject<instanceAction>(payload);
|
||||
Console.WriteLine($"Received instance action: Header ID = {actions.headerId}, Timestamp = {actions.timestamp}");
|
||||
foreach (var action in actions.actions)
|
||||
{
|
||||
Console.WriteLine($"Action ID: {action.actionId}, Type: {action.actionType}");
|
||||
}
|
||||
|
||||
onInstanceActionReceived(actions);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public async Task PublishConnectionStatus(string status)
|
||||
{
|
||||
var message = new connectionMessage()
|
||||
{
|
||||
headerId = 1,
|
||||
timestamp = DateTime.Now,
|
||||
version = "00",
|
||||
manufacturer = "frld",
|
||||
serialNumber = "test-01",
|
||||
connectionState = status
|
||||
};
|
||||
|
||||
var payload = JsonConvert.SerializeObject(message);
|
||||
|
||||
var content = new MqttApplicationMessageBuilder()
|
||||
.WithTopic(_connectionTopic)
|
||||
.WithPayload(payload)
|
||||
.WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce)
|
||||
.WithRetainFlag(true)
|
||||
.Build();
|
||||
|
||||
await _client.EnqueueAsync(content);
|
||||
LogMessage($"SEND-{_connectionTopic}", _connectionTopic, payload);
|
||||
|
||||
//await SendMessageAsync(message, _connectionTopic);
|
||||
}
|
||||
|
||||
public async Task PublishFactSheet(factsheetMessage message)
|
||||
{
|
||||
|
||||
//await SendMessageAsync(message, _factsheet);
|
||||
var payload = JsonConvert.SerializeObject(message);
|
||||
|
||||
var content = new MqttApplicationMessageBuilder()
|
||||
.WithTopic(_factsheet)
|
||||
.WithPayload(payload)
|
||||
.WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce)
|
||||
//.WithRetainFlag(true)
|
||||
.Build();
|
||||
|
||||
await _client.EnqueueAsync(content);
|
||||
LogMessage($"SEND-{_factsheet}", _factsheet, payload);
|
||||
|
||||
}
|
||||
|
||||
public void SetupOrderListener(Action<orderMessage> orderReceived)
|
||||
{
|
||||
// Subscribe to the orders topic
|
||||
_client.SubscribeAsync(_orderTopic, MqttQualityOfServiceLevel.AtMostOnce).GetAwaiter().GetResult();
|
||||
_client.ApplicationMessageReceivedAsync += async e =>
|
||||
{
|
||||
if (e.ApplicationMessage.Topic == _orderTopic)
|
||||
{
|
||||
var payload = Encoding.UTF8.GetString(e.ApplicationMessage.Payload);
|
||||
LogMessage($"RECEIVE-{e.ApplicationMessage.Topic}", e.ApplicationMessage.Topic, payload);
|
||||
var order = JsonConvert.DeserializeObject<orderMessage>(payload);
|
||||
|
||||
// Save the order message to a file for debugging
|
||||
// SaveOrderToFile(payload);
|
||||
orderReceived(order);
|
||||
}
|
||||
await Task.CompletedTask;
|
||||
};
|
||||
}
|
||||
|
||||
private void SaveOrderToFile(string orderJson)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Specify the file path (e.g., orders_log.txt in the current directory)
|
||||
string filePath = "orders_log.txt";
|
||||
|
||||
// Append the order JSON along with a timestamp
|
||||
File.AppendAllText(filePath, $"{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} - {orderJson}{Environment.NewLine}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Handle any exceptions that occur while writing to the file
|
||||
Console.WriteLine($"Failed to save order to file: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SendMessageAsync<T>(T message, string topic)
|
||||
{
|
||||
|
||||
var payload = JsonConvert.SerializeObject(message);
|
||||
|
||||
var content = new MqttApplicationMessageBuilder()
|
||||
.WithTopic(topic)
|
||||
.WithPayload(payload)
|
||||
.WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce)
|
||||
.Build();
|
||||
|
||||
await _client.EnqueueAsync(content);
|
||||
|
||||
if (topic != "vda5050/frldAGV/visualization")
|
||||
{
|
||||
|
||||
LogMessage($"SEND-{topic}", topic, payload);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public async Task SendVisualizationMessageAsync<T>(T message, string topic)
|
||||
{
|
||||
if (_visualizationClient == null) return; // Ensure client is initialized
|
||||
|
||||
var payload = JsonConvert.SerializeObject(message);
|
||||
|
||||
var content = new MqttApplicationMessageBuilder()
|
||||
.WithTopic(topic)
|
||||
.WithPayload(payload)
|
||||
.WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce) // QoS 0 for lightweight visualization
|
||||
.Build();
|
||||
|
||||
if (_visualizationClient.PendingApplicationMessagesCount < 5) // Prevent flooding
|
||||
{
|
||||
await _visualizationClient.EnqueueAsync(content);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Skipping visualization update to avoid MQTT congestion.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void LogMessage(string direction, string topic, string payload)
|
||||
{
|
||||
string formattedPayload = payload;
|
||||
string logDirectory = "Logs"; // Directory for log files
|
||||
var filePreName = direction;
|
||||
filePreName = filePreName.Replace("/", "_");
|
||||
string logFilePath = Path.Combine(logDirectory, filePreName + $"-{DateTime.Now:yyyy-MM-dd}.log");
|
||||
|
||||
if(!Directory.Exists(logDirectory)) Directory.CreateDirectory(logDirectory);
|
||||
// Try to parse the payload as JSON and pretty-print it
|
||||
try
|
||||
{
|
||||
var jsonObject = JsonConvert.DeserializeObject(payload);
|
||||
formattedPayload = JsonConvert.SerializeObject(jsonObject, Formatting.Indented);
|
||||
}
|
||||
catch (JsonReaderException)
|
||||
{
|
||||
// If the payload is not valid JSON, just leave it as is
|
||||
formattedPayload = payload;
|
||||
}
|
||||
|
||||
string logMessage = $"[{DateTime.Now:HH:mm:ss}] [{direction}] Topic: {topic}, Payload:\n{formattedPayload}\n";
|
||||
RotateLogFile(logFilePath, logDirectory);
|
||||
|
||||
try
|
||||
{
|
||||
File.AppendAllText(logFilePath, logMessage + Environment.NewLine);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error writing to log file: {ex.Message}");
|
||||
}
|
||||
// Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [{direction}] Topic: {topic}, Payload: {formattedPayload}");
|
||||
// DLog.Log($"[{DateTime.Now:HH:mm:ss}] [{direction}] Topic: {topic}, Payload: {formattedPayload}");
|
||||
}
|
||||
|
||||
|
||||
private void RotateLogFile(string logFilePath, string logDirectory)
|
||||
{
|
||||
const long maxFileSize = 10 * 1024 * 1024; // 10 MB in bytes
|
||||
|
||||
FileInfo fileInfo = new FileInfo(logFilePath);
|
||||
if (fileInfo.Exists && fileInfo.Length > maxFileSize)
|
||||
{
|
||||
string archivePath = Path.Combine(logDirectory, $"log_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.log");
|
||||
|
||||
try
|
||||
{
|
||||
File.Move(logFilePath, archivePath); // Rename the current log file
|
||||
Console.WriteLine($"Log file rotated: {archivePath}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error rotating log file: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CommonUsage.Protocols.VDA5050.Messages
|
||||
{
|
||||
public class connectionMessage
|
||||
{
|
||||
public int headerId;
|
||||
public DateTime timestamp;
|
||||
public string version = "";
|
||||
public string manufacturer = "";
|
||||
public string serialNumber = "";
|
||||
public string connectionState = ""; // Enum: {'ONLINE', 'OFFLINE', 'CONNECTIONBROKEN'}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CommonUsage.Protocols.VDA5050.Messages
|
||||
{
|
||||
public class errorMessage
|
||||
{
|
||||
public string serialNumber = "";
|
||||
public string errorCode = ""; // Unique error code
|
||||
public string description = ""; // Error description
|
||||
public string severity = ""; // Enum {'WARNING', 'FATAL'}
|
||||
public DateTime timestamp; // Time of the error
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using CommonUsage.Protocols.VDA5050.Objects;
|
||||
|
||||
namespace CommonUsage.Protocols.VDA5050.Messages
|
||||
{
|
||||
public class factsheetMessage
|
||||
{
|
||||
public int headerId;
|
||||
public DateTime timestamp;
|
||||
public string version = "";
|
||||
public string manufacturer = "";
|
||||
public string serialNumber = "";
|
||||
|
||||
public typeSpecification typeSpecification;
|
||||
public physicalParameters physicalParameters;
|
||||
public protocolLimits protocolLimits;
|
||||
public protocolFeatures protocolFeatures;
|
||||
public agvGeometry agvGeometry;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using CommonUsage.Protocols.VDA5050.Objects;
|
||||
|
||||
namespace CommonUsage.Protocols.VDA5050.Messages
|
||||
{
|
||||
public class instanceAction
|
||||
{
|
||||
|
||||
public uint headerId { get; set; } // Incremented for each new message.
|
||||
public string timestamp { get; set; } // ISO 8601 UTC timestamp.
|
||||
public string version { get; set; } // Protocol version.
|
||||
public string manufacturer { get; set; } // AGV manufacturer.
|
||||
public string serialNumber { get; set; } // Unique AGV serial number.
|
||||
|
||||
public List<actionState> actions { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using CommonUsage.Protocols.VDA5050.Objects;
|
||||
|
||||
namespace CommonUsage.Protocols.VDA5050.Messages
|
||||
{
|
||||
public class orderMessage
|
||||
{
|
||||
public uint headerId;
|
||||
public string timestamp = "";
|
||||
public string version = "";
|
||||
public string manufacturer = "";
|
||||
public string serialNumber = "";
|
||||
|
||||
public string orderId { get; set; }
|
||||
|
||||
public uint orderUpdateId { get; set; }
|
||||
|
||||
public node[] nodes { get; set; }
|
||||
|
||||
public edge[] edges { get; set; }
|
||||
|
||||
//public action[] action { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using CommonUsage.Protocols.VDA5050.Objects;
|
||||
|
||||
namespace CommonUsage.Protocols.VDA5050.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// 6.10 Topic: "state" (from AGV to master control)
|
||||
/// todo: complete all fields required by VDA5050
|
||||
/// </summary>
|
||||
public class stateMessage
|
||||
{
|
||||
public uint headerId;
|
||||
public string timestamp = "";
|
||||
public string version = "";
|
||||
public string manufacturer = "";
|
||||
public string serialNumber = "";
|
||||
/// <summary>
|
||||
/// Unique order identification of the current order or the previously finished order.
|
||||
/// The orderId is kept until a new order is received.
|
||||
/// Empty string (""), if no previous orderId is available.
|
||||
/// </summary>
|
||||
public string orderId = "";
|
||||
|
||||
/// <summary>
|
||||
/// Order update identification to identify, that an order update has been accepted by the AGV.
|
||||
/// "0" if no previous orderUpdateId is available.
|
||||
/// </summary>
|
||||
public uint orderUpdatedId = 0;
|
||||
|
||||
public string lastNodeId;
|
||||
|
||||
public uint lastNodeSequenceId;
|
||||
|
||||
/// <summary>
|
||||
/// Array of nodeState objects that need to be traversed for fulfilling the order (empty array if idle)
|
||||
/// </summary>
|
||||
public nodeState[] nodeStates = [];
|
||||
|
||||
/// <summary>
|
||||
/// Array of edgeState objects that need to be traversed for fulfilling the order (empty array if idle)
|
||||
/// </summary>
|
||||
public edgeState[] edgeStates = [];
|
||||
|
||||
public agvPosition agvPosition;
|
||||
|
||||
public velocity velocity;
|
||||
public load[] loads = [];
|
||||
|
||||
public bool driving;
|
||||
|
||||
public bool paused;
|
||||
|
||||
public bool newBaseRequest;
|
||||
|
||||
public double distanceSinceLastNode;
|
||||
|
||||
public batteryState batteryState;
|
||||
|
||||
public actionState[] actionStates = Array.Empty<actionState>();
|
||||
public string operatingMode = "";
|
||||
public List<errorState> errors { get; set; } = new List<errorState>(); // Array of errorState objects
|
||||
|
||||
public info[] information = [];
|
||||
public safetyState safetyState;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using CommonUsage.Protocols.VDA5050.Objects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CommonUsage.Protocols.VDA5050.Messages
|
||||
{
|
||||
public class visualizationMessage
|
||||
{
|
||||
public agvPosition agvPosition;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CommonUsage.Protocols.VDA5050.Objects
|
||||
{
|
||||
public class action
|
||||
{
|
||||
|
||||
public string actionId { get; set; }
|
||||
|
||||
public string actionType { get; set; }
|
||||
public string actionDescription { get; set; }
|
||||
|
||||
public string blockingType { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CommonUsage.Protocols.VDA5050.Objects
|
||||
{
|
||||
public class actionState
|
||||
{
|
||||
public actionState(action action,ActionStateEnum state)
|
||||
{
|
||||
actionId = action.actionId;
|
||||
actionDescription = action.actionDescription;
|
||||
actionType = action.actionType;
|
||||
actionStatus = state;
|
||||
}
|
||||
|
||||
public actionState()
|
||||
{
|
||||
}
|
||||
|
||||
public string actionId { get; set; }
|
||||
|
||||
public string actionType { get; set; }
|
||||
public string actionDescription { get; set; }
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public ActionStateEnum actionStatus { get; set; }
|
||||
|
||||
public string resultDescription { get; set; }
|
||||
|
||||
public enum ActionStateEnum
|
||||
{
|
||||
WAITING,
|
||||
INITIALIZING,
|
||||
RUNNING,
|
||||
PAUSED,
|
||||
FINISHED,
|
||||
FAILED
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using static CommonUsage.Protocols.VDA5050.Objects.agvGeometry;
|
||||
|
||||
namespace CommonUsage.Protocols.VDA5050.Objects
|
||||
{
|
||||
public class agvGeometry
|
||||
{
|
||||
// Wheel Definitions
|
||||
public List<wheelDefinition> wheelDefinitions { get; set; } = new List<wheelDefinition>();
|
||||
|
||||
// 2D Envelopes
|
||||
public List<envelope2D> envelopes2D { get; set; } = new List<envelope2D>();
|
||||
|
||||
// 3D Envelopes
|
||||
public List<envelope3D> envelopes3D { get; set; } = new List<envelope3D>();
|
||||
|
||||
public class wheelDefinition
|
||||
{
|
||||
public enum WheelType { DRIVE, CASTER, FIXED, MECANUM }
|
||||
|
||||
public WheelType type { get; set; }
|
||||
public bool isActiveDriven { get; set; }
|
||||
public bool isActiveSteered { get; set; }
|
||||
|
||||
// Wheel Position
|
||||
public double positionX { get; set; }
|
||||
public double positionY { get; set; }
|
||||
public double positionTheta { get; set; } // Required for fixed wheels
|
||||
|
||||
// Wheel Properties
|
||||
public double diameter { get; set; }
|
||||
public double width { get; set; }
|
||||
public double centerDisplacement { get; set; } = 0; // Default to 0 if not defined
|
||||
public string constraints { get; set; }
|
||||
}
|
||||
|
||||
public class envelope2D
|
||||
{
|
||||
public string set { get; set; }
|
||||
public List<polygonPoint> polygonPoints { get; set; } = new List<polygonPoint>();
|
||||
public string description { get; set; }
|
||||
|
||||
public class polygonPoint
|
||||
{
|
||||
public double x { get; set; }
|
||||
public double y { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
public class envelope3D
|
||||
{
|
||||
public string set { get; set; }
|
||||
public string format { get; set; }
|
||||
public object data { get; set; } // JSON object for 3D envelope data
|
||||
public string url { get; set; }
|
||||
public string description { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CommonUsage.Protocols.VDA5050.Objects
|
||||
{
|
||||
public class agvPosition
|
||||
{
|
||||
public bool positionInitialized;
|
||||
|
||||
public double x;
|
||||
|
||||
public double y;
|
||||
|
||||
public double theta;
|
||||
public double localizationScore;
|
||||
public double deviationRange;
|
||||
public string mapId = "";
|
||||
public string mapDescription = "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CommonUsage.Protocols.VDA5050.Objects
|
||||
{
|
||||
public class batteryState
|
||||
{
|
||||
public double batteryCharge;
|
||||
public double batteryVoltage;
|
||||
public double batteryHealth;
|
||||
public bool charging;
|
||||
public int reach;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CommonUsage.Protocols.VDA5050.Objects
|
||||
{
|
||||
public class boundingBoxReference
|
||||
{
|
||||
public double X { get; set; } // Reference point X in AGV coordinate system
|
||||
public double Y { get; set; } // Reference point Y in AGV coordinate system
|
||||
public double Z { get; set; } // Reference point Z in AGV coordinate system
|
||||
public double Theta { get; set; } // Orientation of the load bounding box
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CommonUsage.Protocols.VDA5050.Objects
|
||||
{
|
||||
public class controlPoint
|
||||
{
|
||||
public float x;
|
||||
|
||||
public float y;
|
||||
|
||||
public float weight;
|
||||
public controlPoint(float x, float y, float weight)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.weight = weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
|
||||
namespace CommonUsage.Protocols.VDA5050.Objects
|
||||
{
|
||||
public class edge : sequenceItem
|
||||
{
|
||||
public string edgeId;
|
||||
|
||||
public string edgeDescription;
|
||||
|
||||
public string startNodeId;
|
||||
|
||||
public string endNodeId;
|
||||
|
||||
public double maxSpeed;
|
||||
|
||||
public double orientation;
|
||||
|
||||
public trajectory? trajectory;
|
||||
|
||||
public float[] trackTypeInfo;
|
||||
// public List<Vector2> controlPoints;
|
||||
//
|
||||
// public List<float> weights;
|
||||
|
||||
public action[] action = [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CommonUsage.Protocols.VDA5050.Objects
|
||||
{
|
||||
public class edgeState : sequenceItem
|
||||
{
|
||||
public string edgeId;
|
||||
|
||||
public string edgeDescription;
|
||||
|
||||
public trajectory trajectory;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CommonUsage.Protocols.VDA5050.Objects
|
||||
{
|
||||
public class errorReference
|
||||
{
|
||||
public string referenceKey { get; set; } // Type of reference (e.g., nodeId, edgeId, actionId)
|
||||
public string referenceValue { get; set; } // Value corresponding to the referenceKey
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CommonUsage.Protocols.VDA5050.Objects
|
||||
{
|
||||
public class errorState
|
||||
{
|
||||
public List<errorReference> errorReferences { get; set; } = new List<errorReference>(); // Array of references
|
||||
public string errorType { get; set; } // Required: Type/name of the error
|
||||
public string errorDescription { get; set; } // Verbose description of the error
|
||||
public string errorHint { get; set; } // Hint for resolving the error
|
||||
public string errorLevel { get; set; } // Required: WARNING or FATAL
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CommonUsage.Protocols.VDA5050.Objects
|
||||
{
|
||||
public class info
|
||||
{
|
||||
public string infoType { get; set; } // Type/name of the information
|
||||
public List<infoReference> infoReferences { get; set; } = new List<infoReference>(); // List of references
|
||||
public string infoDescription { get; set; } // Description of the information
|
||||
public infoLevelEnum infoLevel { get; set; } // Debugging or visualization level
|
||||
|
||||
public class infoReference
|
||||
{
|
||||
public string ReferenceKey { get; set; } // Reference type (e.g., headerId, orderId)
|
||||
public string ReferenceValue { get; set; } // The actual referenced field value
|
||||
}
|
||||
|
||||
public enum infoLevelEnum
|
||||
{
|
||||
DEBUG, // Used for debugging
|
||||
INFO // Used for visualization
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CommonUsage.Protocols.VDA5050.Objects
|
||||
{
|
||||
public class load
|
||||
{
|
||||
public string loadId { get; set; } // Unique ID (barcode, RFID, etc.)
|
||||
public string loadType { get; set; } // Type of load
|
||||
public string loadPosition { get; set; } // Load handling position (e.g., "front", "back")
|
||||
public boundingBoxReference boundingBoxReference { get; set; } = new boundingBoxReference();
|
||||
public loadDimensions loadDimensions { get; set; } = new loadDimensions();
|
||||
public double weight { get; set; } // Weight of load in kg (0.0 to ∞)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CommonUsage.Protocols.VDA5050.Objects
|
||||
{
|
||||
public class loadDimensions
|
||||
{
|
||||
public double Length { get; set; } // Length of the bounding box
|
||||
public double Width { get; set; } // Width of the bounding box
|
||||
public double Height { get; set; } // Height of the bounding box (optional)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CommonUsage.Protocols.VDA5050.Objects
|
||||
{
|
||||
public class node : sequenceItem
|
||||
{
|
||||
public string nodeId;
|
||||
|
||||
public string nodeDescription;
|
||||
|
||||
public nodePosition nodePosition;
|
||||
|
||||
public action[] actions = [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CommonUsage.Protocols.VDA5050.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the position on a map in a global project-specific world coordinate system.
|
||||
/// Each floor has its own map.
|
||||
/// All maps shall use the same project-specific global origin.
|
||||
/// </summary>
|
||||
public class nodePosition
|
||||
{
|
||||
/// <summary>
|
||||
/// X-position on the map in reference to the map coordinate system.
|
||||
/// Precision is up to the specific implementation.
|
||||
/// </summary>
|
||||
public double x;
|
||||
|
||||
/// <summary>
|
||||
/// Y-position on the map in reference to the map coordinate system.
|
||||
/// Precision is up to the specific implementation.
|
||||
/// </summary>
|
||||
public double y;
|
||||
|
||||
/// <summary>
|
||||
/// Range: [-Pi ... Pi]
|
||||
/// Absolute orientation of the AGV on the node.
|
||||
/// Optional: vehicle can plan the path by itself. If defined, the AGV has to assume the theta angle on this node.
|
||||
/// If previous edge disallows rotation, the AGV shall rotate on the node.
|
||||
/// If following edge has a differing orientation defined but disallows rotation,
|
||||
/// the AGV is to rotate on the node to the edges desired rotation before entering the edge.
|
||||
/// </summary>
|
||||
public double theta;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CommonUsage.Protocols.VDA5050.Objects
|
||||
{
|
||||
public class nodeState : sequenceItem
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique node identification.
|
||||
/// </summary>
|
||||
public string nodeId;
|
||||
|
||||
/// <summary>
|
||||
/// Additional information on the node.
|
||||
/// </summary>
|
||||
public string nodeDescription;
|
||||
|
||||
/// <summary>
|
||||
/// Node position.
|
||||
/// The object is defined in 6.6 Topic: "order" (from master control to AGV)
|
||||
/// Optional: Master control has this information. Can be sent additionally, e.g., for debugging purposes.
|
||||
/// </summary>
|
||||
public nodePosition nodePosition;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CommonUsage.Protocols.VDA5050.Objects
|
||||
{
|
||||
public class physicalParameters
|
||||
{
|
||||
public double speedMin;
|
||||
public double speedMax;
|
||||
public double angularSpeedMin;
|
||||
public double angularSpeedMax;
|
||||
public double accelerationMax;
|
||||
public double decelerationMax;
|
||||
public double heightMin;
|
||||
public double heightMax;
|
||||
public double width;
|
||||
public double length;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CommonUsage.Protocols.VDA5050.Objects
|
||||
{
|
||||
public class protocolFeatures
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CommonUsage.Protocols.VDA5050.Objects
|
||||
{
|
||||
public class protocolLimits
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CommonUsage.Protocols.VDA5050.Objects
|
||||
{
|
||||
public class safetyState
|
||||
{
|
||||
public eStopEnum eStop { get; set; } // Emergency stop status
|
||||
public bool fieldViolation { get; set; } // "true" if a safety field is violated, "false" otherwise
|
||||
|
||||
public enum eStopEnum
|
||||
{
|
||||
AUTOACK, // Auto-acknowledged emergency stop (e.g., triggered by a bumper)
|
||||
MANUAL, // Manually confirmed emergency stop
|
||||
REMOTE, // Remote-confirmed emergency stop
|
||||
NONE // No emergency stop activated
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CommonUsage.Protocols.VDA5050.Objects
|
||||
{
|
||||
public class sequenceItem
|
||||
{
|
||||
public uint sequenceId;
|
||||
|
||||
public bool released;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CommonUsage.Protocols.VDA5050.Objects
|
||||
{
|
||||
public class trajectory
|
||||
{
|
||||
public float degree;
|
||||
|
||||
public float[] knotVector;
|
||||
|
||||
public controlPoint[] controlPoints;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CommonUsage.Protocols.VDA5050.Objects
|
||||
{
|
||||
public class typeSpecification
|
||||
{
|
||||
public string agvKinemantic = "";
|
||||
public string agvClass = "";
|
||||
public double maxLoadMass;
|
||||
public string[] localizationTypes; // Simplified description of localization type (e.g., NATURAL, REFLECTOR, RFID, DMC, GRID)
|
||||
public string[] navigationTypes; // Path planning types (e.g., 'AUTONOMOUS', 'VIRTUAL_LINE_GUIDED')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CommonUsage.Protocols.VDA5050.Objects
|
||||
{
|
||||
public class velocity
|
||||
{
|
||||
public double vx;
|
||||
|
||||
public double vy;
|
||||
|
||||
public double omega;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
//using CommonUsage.Protocols.VDA5050.Messages;
|
||||
//using System;
|
||||
//using System.Collections.Generic;
|
||||
//using System.Net.Http.Headers;
|
||||
//using System.Runtime.CompilerServices;
|
||||
//using System.Text;
|
||||
//using System.Threading;
|
||||
//using System.Threading.Tasks;
|
||||
//using CommonUsage.Protocols.VDA5050.Objects;
|
||||
//using ClumsyCore.Utilities;
|
||||
//using System.Linq;
|
||||
//using ClumsyCore.Pilot;
|
||||
//using System.Numerics;
|
||||
|
||||
//namespace CommonUsage.Protocols.VDA5050
|
||||
//{
|
||||
// public abstract class VDA5050Basic
|
||||
// {
|
||||
// protected static IVDACommunicationProtocol _communicationProtocol;
|
||||
|
||||
// public void Enable(IVDACommunicationProtocol protocol)
|
||||
// {
|
||||
// _communicationProtocol = protocol;
|
||||
// Task.Run(() => ManageConnection());
|
||||
// StartVisualizationLoop();
|
||||
// }
|
||||
// private void StartVisualizationLoop()
|
||||
// {
|
||||
// Console.WriteLine("Visualization in CommonUsage");
|
||||
// new Thread(async () =>
|
||||
// {
|
||||
// while (true)
|
||||
// {
|
||||
// var position = GetAGVPosition();
|
||||
// if (position != null)
|
||||
// {
|
||||
// var msg = new stateMessage()
|
||||
// {
|
||||
// serialNumber = "test-01",
|
||||
// agvPosition = new()
|
||||
// {
|
||||
// x = position.Value.X,
|
||||
// y = position.Value.Y,
|
||||
// theta = position.Value.Theta,
|
||||
// positionInitialized = true
|
||||
// }
|
||||
|
||||
// };
|
||||
// await _communicationProtocol.SendMessageAsync(msg, "vda5050/frldAGV/visualization");
|
||||
// }
|
||||
|
||||
// Thread.Sleep(100);
|
||||
// }
|
||||
// })
|
||||
// { Name = "VDA5050TopicVisualization" }.Start();
|
||||
// }
|
||||
|
||||
// private void ManageConnection()
|
||||
// {
|
||||
// while (true)
|
||||
// {
|
||||
// var status = CheckConnectionStatus() ? "ONLINE" : "OFFLINE";
|
||||
// _communicationProtocol.PublishConnectionStatus(status);
|
||||
// Thread.Sleep(1000);
|
||||
// }
|
||||
// }
|
||||
|
||||
// protected List<sequenceItem> OrganizeReceivedSequence(orderMessage order)
|
||||
// {
|
||||
// List<sequenceItem> receivedSequence = new();
|
||||
|
||||
// int ii = 0, jj = 0;
|
||||
// while (true)
|
||||
// {
|
||||
// var edge = order.edges[ii];
|
||||
// var node = order.nodes[jj];
|
||||
// var takeEdge = edge.sequenceId < node.sequenceId;
|
||||
|
||||
// if (takeEdge)
|
||||
// {
|
||||
// receivedSequence.Add(edge);
|
||||
// ii++;
|
||||
// if (ii == order.edges.Length) break;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// receivedSequence.Add(node);
|
||||
// jj++;
|
||||
// if (jj == order.nodes.Length) break;
|
||||
// }
|
||||
// }
|
||||
// for (var i = ii; i < order.edges.Length; ++i) receivedSequence.Add(order.edges[i]);
|
||||
// for (var j = jj; j < order.nodes.Length; ++j) receivedSequence.Add(order.nodes[j]);
|
||||
|
||||
// for (var i = 1; i < receivedSequence.Count; i++)
|
||||
// {
|
||||
// if (receivedSequence[i - 1].sequenceId + 1 != receivedSequence[i].sequenceId)
|
||||
// throw new Exception("stateMessage not continuous!");
|
||||
// }
|
||||
|
||||
// return receivedSequence;
|
||||
// }
|
||||
|
||||
// public virtual bool CheckConnectionStatus()
|
||||
// {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// protected abstract Vector3? GetAGVPosition();
|
||||
// public struct Vector3
|
||||
// {
|
||||
// public double X;
|
||||
// public double Y;
|
||||
// public double Theta;
|
||||
// }
|
||||
// }
|
||||
|
||||
//}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CommonUsage.Protocols.VDA5050
|
||||
{
|
||||
public class VDA5050Helper
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
|
||||
namespace CommonUsage
|
||||
{
|
||||
public class Visualizer
|
||||
{
|
||||
public Action<Color, Vector2, Vector2, bool, bool, int> LineAction;
|
||||
|
||||
public Action<Color, string, Vector2> TextAction;
|
||||
|
||||
public Action Clear;
|
||||
|
||||
public void DrawLine(Color color, Vector2 src, Vector2 dst, bool startArrow = false, bool endArrow = false,
|
||||
int width = 1)
|
||||
{
|
||||
LineAction?.Invoke(color, src, dst, startArrow, endArrow, width);
|
||||
}
|
||||
|
||||
public void DrawText(Color color, string text, Vector2 pos)
|
||||
{
|
||||
TextAction?.Invoke(color, text, pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,11 +27,6 @@
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
|
||||
<Reference Include="CommonUsage">
|
||||
<HintPath>ref\CommonUsage.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
|
||||
<Reference Include="MDCSToolBox">
|
||||
<HintPath>ref\MDCSToolBox.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
@@ -41,6 +36,9 @@
|
||||
<HintPath>ref\CycleGUI.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="CommonUsage">
|
||||
<HintPath>..\ref\CommonUsage.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -7,9 +7,20 @@
|
||||
"targets": {
|
||||
".NETCoreApp,Version=v8.0": {
|
||||
"MedullaAdapter/1.0.0": {
|
||||
"dependencies": {
|
||||
"CommonUsage": "1.0.0.0"
|
||||
},
|
||||
"runtime": {
|
||||
"MedullaAdapter.dll": {}
|
||||
}
|
||||
},
|
||||
"CommonUsage/1.0.0.0": {
|
||||
"runtime": {
|
||||
"CommonUsage.dll": {
|
||||
"assemblyVersion": "1.0.0.0",
|
||||
"fileVersion": "1.0.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -18,6 +29,11 @@
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"CommonUsage/1.0.0.0": {
|
||||
"type": "reference",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,6 +119,10 @@ namespace MyParking.Shared
|
||||
// 禁用旧版DirectionAngle/ZeroDirection坐标偏置,
|
||||
// 保证SendXYThSpeed直接使用真实车体坐标系。
|
||||
ResetToBodyFrame();
|
||||
|
||||
// 停车机器人优先保持当前机械舵角,通过反转轮速表达反向运动,
|
||||
// 避免蟹行正反切换时四个舵轮无意义地旋转180°。
|
||||
_chassis.PreferMinimumSteeringTravel = true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 285 KiB |
|
After Width: | Height: | Size: 247 KiB |
|
After Width: | Height: | Size: 116 KiB |
@@ -0,0 +1,100 @@
|
||||
"""绘制控制器下发角速度命令曲线。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
from plot_trajectory_comparison import (
|
||||
configure_matplotlib,
|
||||
discover_csv_files,
|
||||
load_and_resample,
|
||||
output_path,
|
||||
shade_localization_jump_windows,
|
||||
)
|
||||
|
||||
|
||||
def plot_angular_command(
|
||||
csv_path: Path,
|
||||
frequency_hz: float,
|
||||
filter_window_seconds: float,
|
||||
output_directory: str | None,
|
||||
show: bool,
|
||||
) -> Path:
|
||||
"""生成单份CSV的命令角速度曲线。"""
|
||||
frame, metadata = load_and_resample(
|
||||
csv_path,
|
||||
frequency_hz,
|
||||
filter_window_seconds,
|
||||
)
|
||||
time = frame["TimeSeconds"].to_numpy(dtype=float)
|
||||
angular_command = frame[
|
||||
"CommandAngularSpeedDegPerSec"
|
||||
].to_numpy(dtype=float)
|
||||
maximum = float(np.max(angular_command))
|
||||
minimum = float(np.min(angular_command))
|
||||
|
||||
fig, ax = plt.subplots(figsize=(10.0, 5.5))
|
||||
ax.plot(
|
||||
time,
|
||||
angular_command,
|
||||
color="tab:red",
|
||||
linewidth=1.6,
|
||||
label="CommandAngularSpeed",
|
||||
)
|
||||
ax.axhline(0.0, color="black", linewidth=0.8)
|
||||
shade_localization_jump_windows(ax, metadata)
|
||||
ax.set_xlabel("时间 / s")
|
||||
ax.set_ylabel("命令角速度 / (°/s)")
|
||||
ax.set_title(
|
||||
f"角速度指令曲线\n"
|
||||
f"{metadata['controller_name']} - "
|
||||
f"{metadata['trajectory_name']},"
|
||||
f"范围=[{minimum:.3f}, {maximum:.3f}]°/s"
|
||||
)
|
||||
ax.grid(True, alpha=0.3)
|
||||
ax.legend()
|
||||
fig.tight_layout()
|
||||
|
||||
destination = output_path(
|
||||
csv_path,
|
||||
output_directory,
|
||||
"angular_command",
|
||||
)
|
||||
fig.savefig(destination, dpi=300, bbox_inches="tight")
|
||||
if show:
|
||||
plt.show()
|
||||
plt.close(fig)
|
||||
return destination
|
||||
|
||||
|
||||
def main() -> None:
|
||||
configure_matplotlib()
|
||||
parser = argparse.ArgumentParser(
|
||||
description="绘制控制器下发角速度命令曲线。"
|
||||
)
|
||||
parser.add_argument("files", nargs="*", help="一个或多个CSV文件")
|
||||
parser.add_argument("--frequency", type=float, default=20.0)
|
||||
parser.add_argument("--window", type=float, default=0.55)
|
||||
parser.add_argument("--output-dir")
|
||||
parser.add_argument("--show", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
for csv_path in discover_csv_files(args.files):
|
||||
destination = plot_angular_command(
|
||||
csv_path,
|
||||
args.frequency,
|
||||
args.window,
|
||||
args.output_dir,
|
||||
args.show,
|
||||
)
|
||||
print(f"已生成:{destination}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,177 @@
|
||||
"""绘制控制器参考速度与Detour差分实际速度对比图。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
from plot_trajectory_comparison import (
|
||||
configure_matplotlib,
|
||||
discover_csv_files,
|
||||
load_and_resample,
|
||||
output_path,
|
||||
segmented_savgol,
|
||||
shade_localization_jump_windows,
|
||||
)
|
||||
|
||||
|
||||
def calculate_actual_speed_mps(
|
||||
frame,
|
||||
filter_window_seconds: float,
|
||||
) -> np.ndarray:
|
||||
"""使用Savitzky-Golay求位置导数并计算Detour实际合速度。"""
|
||||
time = frame["TimeSeconds"].to_numpy(dtype=float)
|
||||
dt = float(np.median(np.diff(time)))
|
||||
# 直接对固定频率重采样后的位置做SG求导,避免“先平滑再求导”
|
||||
# 造成两次滤波和过度削弱速度峰值。
|
||||
x_mm = frame["DetourXRawMm"].to_numpy(dtype=float)
|
||||
y_mm = frame["DetourYRawMm"].to_numpy(dtype=float)
|
||||
vx_mm_per_second = segmented_savgol(
|
||||
x_mm,
|
||||
dt,
|
||||
filter_window_seconds,
|
||||
derivative=1,
|
||||
)
|
||||
vy_mm_per_second = segmented_savgol(
|
||||
y_mm,
|
||||
dt,
|
||||
filter_window_seconds,
|
||||
derivative=1,
|
||||
)
|
||||
|
||||
speed = np.hypot(
|
||||
vx_mm_per_second,
|
||||
vy_mm_per_second,
|
||||
) / 1000.0
|
||||
speed[
|
||||
frame["InvalidNearLocalizationJump"].to_numpy(dtype=bool)
|
||||
] = np.nan
|
||||
return speed
|
||||
|
||||
|
||||
def plot_speed(
|
||||
csv_path: Path,
|
||||
frequency_hz: float,
|
||||
filter_window_seconds: float,
|
||||
output_directory: str | None,
|
||||
show: bool,
|
||||
) -> Path:
|
||||
"""生成单份CSV的参考/实际速度响应图。"""
|
||||
frame, metadata = load_and_resample(
|
||||
csv_path,
|
||||
frequency_hz,
|
||||
filter_window_seconds,
|
||||
)
|
||||
time = frame["TimeSeconds"].to_numpy(dtype=float)
|
||||
command_speed = frame["CommandSpeedMps"].to_numpy(dtype=float)
|
||||
actual_speed = calculate_actual_speed_mps(
|
||||
frame,
|
||||
filter_window_seconds,
|
||||
)
|
||||
is_in_place_rotation = (
|
||||
str(metadata["trajectory_name"])
|
||||
.lower()
|
||||
.startswith("rotate")
|
||||
)
|
||||
# 原地自转CSV中的ReferenceSpeed历史上保存的是角速度上限deg/s,
|
||||
# 不能作为线速度m/s使用;其参考线速度应为0。
|
||||
configured_speed = (
|
||||
0.0
|
||||
if is_in_place_rotation
|
||||
else float(metadata["reference_speed_mps"])
|
||||
)
|
||||
|
||||
moving = (
|
||||
(command_speed > max(0.02, configured_speed * 0.1)) &
|
||||
np.isfinite(actual_speed)
|
||||
)
|
||||
if np.any(moving):
|
||||
speed_rmse = float(
|
||||
np.sqrt(
|
||||
np.mean(
|
||||
(actual_speed[moving] - command_speed[moving]) ** 2
|
||||
)
|
||||
)
|
||||
)
|
||||
else:
|
||||
speed_rmse = float("nan")
|
||||
|
||||
fig, ax = plt.subplots(figsize=(10.0, 5.8))
|
||||
ax.plot(
|
||||
time,
|
||||
command_speed,
|
||||
linewidth=1.8,
|
||||
label="控制器参考/下发线速度",
|
||||
)
|
||||
ax.plot(
|
||||
time,
|
||||
actual_speed,
|
||||
linewidth=1.5,
|
||||
label="Detour差分实际线速度(SG求导)",
|
||||
)
|
||||
ax.axhline(
|
||||
configured_speed,
|
||||
linestyle=":",
|
||||
linewidth=1.3,
|
||||
color="tab:green",
|
||||
label=(
|
||||
"原地自转参考线速度 0 m/s"
|
||||
if is_in_place_rotation
|
||||
else f"配置巡航速度 {configured_speed:.3f} m/s"
|
||||
),
|
||||
)
|
||||
shade_localization_jump_windows(ax, metadata)
|
||||
ax.set_xlabel("时间 / s")
|
||||
ax.set_ylabel("线速度 / (m/s)")
|
||||
ax.set_title(
|
||||
f"参考速度与实际速度对比\n"
|
||||
f"{metadata['controller_name']} - "
|
||||
f"{metadata['trajectory_name']},"
|
||||
f"运动段RMSE={speed_rmse:.4f} m/s"
|
||||
)
|
||||
ax.grid(True, alpha=0.3)
|
||||
ax.legend()
|
||||
fig.tight_layout()
|
||||
|
||||
destination = output_path(
|
||||
csv_path,
|
||||
output_directory,
|
||||
"speed_response",
|
||||
)
|
||||
fig.savefig(destination, dpi=300, bbox_inches="tight")
|
||||
if show:
|
||||
plt.show()
|
||||
plt.close(fig)
|
||||
return destination
|
||||
|
||||
|
||||
def main() -> None:
|
||||
configure_matplotlib()
|
||||
parser = argparse.ArgumentParser(
|
||||
description="绘制参考速度与Detour差分实际速度对比图。"
|
||||
)
|
||||
parser.add_argument("files", nargs="*", help="一个或多个CSV文件")
|
||||
parser.add_argument("--frequency", type=float, default=20.0)
|
||||
parser.add_argument("--window", type=float, default=0.55)
|
||||
parser.add_argument("--output-dir")
|
||||
parser.add_argument("--show", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
for csv_path in discover_csv_files(args.files):
|
||||
destination = plot_speed(
|
||||
csv_path,
|
||||
args.frequency,
|
||||
args.window,
|
||||
args.output_dir,
|
||||
args.show,
|
||||
)
|
||||
print(f"已生成:{destination}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,172 @@
|
||||
"""绘制横向误差和航向误差随时间变化图。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
from plot_trajectory_comparison import (
|
||||
build_reference,
|
||||
configure_matplotlib,
|
||||
discover_csv_files,
|
||||
load_and_resample,
|
||||
output_path,
|
||||
shade_localization_jump_windows,
|
||||
)
|
||||
|
||||
|
||||
def plot_errors(
|
||||
csv_path: Path,
|
||||
frequency_hz: float,
|
||||
filter_window_seconds: float,
|
||||
output_directory: str | None,
|
||||
show: bool,
|
||||
) -> Path:
|
||||
"""生成单份CSV的横向/航向误差图。"""
|
||||
frame, metadata = load_and_resample(
|
||||
csv_path,
|
||||
frequency_hz,
|
||||
filter_window_seconds,
|
||||
)
|
||||
reference = build_reference(frame, metadata)
|
||||
time = frame["TimeSeconds"].to_numpy()
|
||||
lateral = np.asarray(reference["lateral_error_mm"])
|
||||
heading = np.asarray(reference["heading_error_degrees"])
|
||||
invalid = frame[
|
||||
"InvalidNearLocalizationJump"
|
||||
].to_numpy(dtype=bool)
|
||||
lateral_for_statistics = lateral.copy()
|
||||
heading_for_statistics = heading.copy()
|
||||
lateral_for_statistics[invalid] = np.nan
|
||||
heading_for_statistics[invalid] = np.nan
|
||||
|
||||
lateral_rmse = float(
|
||||
np.sqrt(np.nanmean(lateral_for_statistics**2))
|
||||
)
|
||||
heading_rmse = float(
|
||||
np.sqrt(np.nanmean(heading_for_statistics**2))
|
||||
)
|
||||
lateral_max = float(
|
||||
np.nanmax(np.abs(lateral_for_statistics))
|
||||
)
|
||||
heading_max = float(
|
||||
np.nanmax(np.abs(heading_for_statistics))
|
||||
)
|
||||
is_in_place_rotation = (
|
||||
reference["kind"] == "in_place_rotation"
|
||||
)
|
||||
|
||||
fig, axes = plt.subplots(
|
||||
2,
|
||||
1,
|
||||
figsize=(10.0, 7.0),
|
||||
sharex=True,
|
||||
)
|
||||
axes[0].plot(time, lateral, linewidth=1.5)
|
||||
axes[0].axhline(0.0, color="black", linewidth=0.8)
|
||||
if is_in_place_rotation:
|
||||
axes[0].set_ylabel("旋转中心位置漂移 / mm")
|
||||
axes[0].set_title(
|
||||
f"原地自转位置漂移:RMS={lateral_rmse:.2f} mm,"
|
||||
f"最大值={lateral_max:.2f} mm"
|
||||
)
|
||||
else:
|
||||
axes[0].set_ylabel("横向误差 / mm")
|
||||
axes[0].set_title(
|
||||
f"横向误差:RMSE={lateral_rmse:.2f} mm,"
|
||||
f"最大绝对值={lateral_max:.2f} mm"
|
||||
)
|
||||
shade_localization_jump_windows(axes[0], metadata)
|
||||
axes[0].grid(True, alpha=0.3)
|
||||
|
||||
axes[1].plot(
|
||||
time,
|
||||
heading,
|
||||
color="tab:orange",
|
||||
linewidth=1.5,
|
||||
)
|
||||
axes[1].axhline(0.0, color="black", linewidth=0.8)
|
||||
axes[1].set_xlabel("时间 / s")
|
||||
axes[1].set_ylabel(
|
||||
"目标角度剩余误差 / °"
|
||||
if is_in_place_rotation
|
||||
else "航向误差 / °"
|
||||
)
|
||||
axes[1].set_title(
|
||||
(
|
||||
f"目标角度剩余误差:RMSE={heading_rmse:.2f}°,"
|
||||
f"最大绝对值={heading_max:.2f}°"
|
||||
)
|
||||
if is_in_place_rotation
|
||||
else (
|
||||
f"航向误差:RMSE={heading_rmse:.2f}°,"
|
||||
f"最大绝对值={heading_max:.2f}°"
|
||||
)
|
||||
)
|
||||
shade_localization_jump_windows(axes[1], metadata)
|
||||
axes[1].grid(True, alpha=0.3)
|
||||
if metadata["localization_jump_events"]:
|
||||
axes[1].legend(loc="best")
|
||||
|
||||
fig.suptitle(
|
||||
f"横向/航向误差随时间变化\n"
|
||||
f"{metadata['controller_name']} - "
|
||||
f"{metadata['trajectory_name']}"
|
||||
)
|
||||
fig.tight_layout()
|
||||
|
||||
destination = output_path(
|
||||
csv_path,
|
||||
output_directory,
|
||||
"tracking_errors",
|
||||
)
|
||||
fig.savefig(destination, dpi=300, bbox_inches="tight")
|
||||
if show:
|
||||
plt.show()
|
||||
plt.close(fig)
|
||||
|
||||
if is_in_place_rotation:
|
||||
print(
|
||||
f"{csv_path.name}: position drift RMS="
|
||||
f"{lateral_rmse:.3f} mm, "
|
||||
f"target-angle error RMS={heading_rmse:.3f} deg"
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"{csv_path.name}: lateral RMSE="
|
||||
f"{lateral_rmse:.3f} mm, "
|
||||
f"heading RMSE={heading_rmse:.3f} deg"
|
||||
)
|
||||
return destination
|
||||
|
||||
|
||||
def main() -> None:
|
||||
configure_matplotlib()
|
||||
parser = argparse.ArgumentParser(
|
||||
description="绘制横向误差和航向误差随时间变化图。"
|
||||
)
|
||||
parser.add_argument("files", nargs="*", help="一个或多个CSV文件")
|
||||
parser.add_argument("--frequency", type=float, default=20.0)
|
||||
parser.add_argument("--window", type=float, default=0.55)
|
||||
parser.add_argument("--output-dir")
|
||||
parser.add_argument("--show", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
for csv_path in discover_csv_files(args.files):
|
||||
destination = plot_errors(
|
||||
csv_path,
|
||||
args.frequency,
|
||||
args.window,
|
||||
args.output_dir,
|
||||
args.show,
|
||||
)
|
||||
print(f"已生成:{destination}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,706 @@
|
||||
"""绘制理想轨迹与Detour实际轨迹对比图。
|
||||
|
||||
不传CSV路径时,默认处理本脚本目录下的全部CSV文件。
|
||||
本文件也提供其余三个绘图脚本共用的数据预处理函数。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from scipy.signal import savgol_filter
|
||||
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
REQUIRED_COLUMNS = {
|
||||
"ElapsedSeconds",
|
||||
"TrajectoryName",
|
||||
"DetourX",
|
||||
"DetourY",
|
||||
"DetourTheta",
|
||||
"CommandSpeed",
|
||||
"CommandAngularSpeed",
|
||||
"ReferenceStartX",
|
||||
"ReferenceStartY",
|
||||
"ReferenceEndX",
|
||||
"ReferenceEndY",
|
||||
"ReferenceSpeed",
|
||||
}
|
||||
|
||||
|
||||
def configure_matplotlib() -> None:
|
||||
"""配置中文字体和图片输出风格。"""
|
||||
matplotlib.rcParams["font.sans-serif"] = [
|
||||
"Microsoft YaHei",
|
||||
"SimHei",
|
||||
"Arial Unicode MS",
|
||||
"DejaVu Sans",
|
||||
]
|
||||
matplotlib.rcParams["axes.unicode_minus"] = False
|
||||
matplotlib.rcParams["figure.dpi"] = 120
|
||||
|
||||
|
||||
def _odd_window_length(
|
||||
sample_count: int,
|
||||
sample_interval: float,
|
||||
window_seconds: float,
|
||||
polynomial_order: int = 2,
|
||||
) -> int | None:
|
||||
"""计算不超过数据长度的Savitzky-Golay奇数窗口。"""
|
||||
requested = max(
|
||||
polynomial_order + 2,
|
||||
int(round(window_seconds / sample_interval)),
|
||||
)
|
||||
if requested % 2 == 0:
|
||||
requested += 1
|
||||
|
||||
maximum = sample_count if sample_count % 2 == 1 else sample_count - 1
|
||||
window = min(requested, maximum)
|
||||
minimum = polynomial_order + 2
|
||||
if minimum % 2 == 0:
|
||||
minimum += 1
|
||||
|
||||
return window if window >= minimum else None
|
||||
|
||||
|
||||
def wrap_degrees(angle_degrees: np.ndarray) -> np.ndarray:
|
||||
"""将角度差归一化到[-180°, 180°)。"""
|
||||
return (angle_degrees + 180.0) % 360.0 - 180.0
|
||||
|
||||
|
||||
def segmented_savgol(
|
||||
values: np.ndarray,
|
||||
sample_interval: float,
|
||||
window_seconds: float,
|
||||
derivative: int = 0,
|
||||
polynomial_order: int = 2,
|
||||
) -> np.ndarray:
|
||||
"""对含NaN断点的数据逐段执行SG滤波或求导。"""
|
||||
values = np.asarray(values, dtype=float)
|
||||
result = np.full_like(values, np.nan)
|
||||
finite_indices = np.flatnonzero(np.isfinite(values))
|
||||
if finite_indices.size == 0:
|
||||
return result
|
||||
|
||||
breaks = np.flatnonzero(np.diff(finite_indices) > 1)
|
||||
starts = np.r_[0, breaks + 1]
|
||||
ends = np.r_[breaks + 1, finite_indices.size]
|
||||
|
||||
for start_index, end_index in zip(starts, ends):
|
||||
indices = finite_indices[start_index:end_index]
|
||||
segment = values[indices]
|
||||
window = _odd_window_length(
|
||||
len(segment),
|
||||
sample_interval,
|
||||
window_seconds,
|
||||
polynomial_order,
|
||||
)
|
||||
if window is not None:
|
||||
result[indices] = savgol_filter(
|
||||
segment,
|
||||
window,
|
||||
polynomial_order,
|
||||
deriv=derivative,
|
||||
delta=sample_interval,
|
||||
mode="interp",
|
||||
)
|
||||
elif derivative == 0:
|
||||
result[indices] = segment
|
||||
elif len(segment) >= 2:
|
||||
result[indices] = np.gradient(segment, sample_interval)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def shade_localization_jump_windows(
|
||||
axis,
|
||||
metadata: dict[str, Any],
|
||||
) -> None:
|
||||
"""在时间曲线中标记不应参与车辆动力学评价的定位跳变窗口。"""
|
||||
for index, (start, end) in enumerate(
|
||||
metadata["jump_exclusion_windows"]
|
||||
):
|
||||
axis.axvspan(
|
||||
start,
|
||||
end,
|
||||
color="tab:red",
|
||||
alpha=0.12,
|
||||
label="Detour定位跳变排除窗口" if index == 0 else None,
|
||||
)
|
||||
|
||||
|
||||
def load_and_resample(
|
||||
csv_path: Path,
|
||||
frequency_hz: float = 20.0,
|
||||
filter_window_seconds: float = 0.55,
|
||||
) -> tuple[pd.DataFrame, dict[str, Any]]:
|
||||
"""压缩Detour保持帧,检测定位跳变,再分段重采样和平滑。"""
|
||||
if not np.isfinite(frequency_hz) or frequency_hz <= 0.0:
|
||||
raise ValueError("重采样频率必须是正有限值。")
|
||||
|
||||
raw = pd.read_csv(csv_path)
|
||||
missing = REQUIRED_COLUMNS.difference(raw.columns)
|
||||
if missing:
|
||||
raise ValueError(
|
||||
f"{csv_path.name}缺少列:{', '.join(sorted(missing))}"
|
||||
)
|
||||
|
||||
numeric_columns = [
|
||||
"ElapsedSeconds",
|
||||
"DetourX",
|
||||
"DetourY",
|
||||
"DetourTheta",
|
||||
"CommandSpeed",
|
||||
"CommandAngularSpeed",
|
||||
"ReferenceStartX",
|
||||
"ReferenceStartY",
|
||||
"ReferenceEndX",
|
||||
"ReferenceEndY",
|
||||
"ReferenceSpeed",
|
||||
]
|
||||
for column in numeric_columns:
|
||||
raw[column] = pd.to_numeric(raw[column], errors="coerce")
|
||||
|
||||
raw = (
|
||||
raw.dropna(subset=[
|
||||
"ElapsedSeconds",
|
||||
"DetourX",
|
||||
"DetourY",
|
||||
"DetourTheta",
|
||||
])
|
||||
.sort_values("ElapsedSeconds")
|
||||
.drop_duplicates("ElapsedSeconds", keep="last")
|
||||
.reset_index(drop=True)
|
||||
)
|
||||
if len(raw) < 5:
|
||||
raise ValueError(f"{csv_path.name}有效数据不足5行。")
|
||||
|
||||
time_raw = raw["ElapsedSeconds"].to_numpy(dtype=float)
|
||||
time_raw = time_raw - time_raw[0]
|
||||
raw["ElapsedSeconds"] = time_raw
|
||||
duration = float(time_raw[-1])
|
||||
sample_interval = 1.0 / frequency_hz
|
||||
time_uniform = np.arange(
|
||||
0.0,
|
||||
duration + sample_interval * 0.5,
|
||||
sample_interval,
|
||||
)
|
||||
|
||||
def interpolate_command(column: str) -> np.ndarray:
|
||||
values = raw[column].to_numpy(dtype=float)
|
||||
return np.interp(time_uniform, time_raw, values)
|
||||
|
||||
# 记录器频率高于Detour更新频率,会得到A,A,B,B形式的保持帧。
|
||||
# 速度估计前先保留真正发生位姿更新的样本。
|
||||
x_all = raw["DetourX"].to_numpy(dtype=float)
|
||||
y_all = raw["DetourY"].to_numpy(dtype=float)
|
||||
theta_all = raw["DetourTheta"].to_numpy(dtype=float)
|
||||
position_change = np.hypot(np.diff(x_all), np.diff(y_all))
|
||||
heading_change = np.abs(wrap_degrees(np.diff(theta_all)))
|
||||
update_mask = np.r_[
|
||||
True,
|
||||
(position_change > 1e-6) | (heading_change > 1e-6),
|
||||
]
|
||||
updates = raw.loc[update_mask].copy().reset_index(drop=True)
|
||||
if len(updates) < 3:
|
||||
raise ValueError(f"{csv_path.name}有效Detour更新点不足3个。")
|
||||
|
||||
update_time = updates["ElapsedSeconds"].to_numpy(dtype=float)
|
||||
update_x = updates["DetourX"].to_numpy(dtype=float)
|
||||
update_y = updates["DetourY"].to_numpy(dtype=float)
|
||||
update_theta = updates["DetourTheta"].to_numpy(dtype=float)
|
||||
update_command_speed = np.abs(
|
||||
updates["CommandSpeed"].to_numpy(dtype=float)
|
||||
)
|
||||
update_command_angular = np.abs(
|
||||
updates["CommandAngularSpeed"].to_numpy(dtype=float)
|
||||
)
|
||||
|
||||
# 自适应跳变阈值:正常移动允许达到参考位移的3倍并保留15mm余量;
|
||||
# 低速阶段仍至少允许30mm,防止把普通定位噪声误判为跳变。
|
||||
update_dt = np.diff(update_time)
|
||||
update_distance = np.hypot(np.diff(update_x), np.diff(update_y))
|
||||
expected_distance = (
|
||||
0.5 *
|
||||
(update_command_speed[1:] + update_command_speed[:-1]) *
|
||||
update_dt *
|
||||
1000.0
|
||||
)
|
||||
distance_threshold = np.maximum(
|
||||
30.0,
|
||||
expected_distance * 3.0 + 15.0,
|
||||
)
|
||||
update_heading_delta = np.abs(
|
||||
wrap_degrees(np.diff(update_theta))
|
||||
)
|
||||
expected_heading_delta = (
|
||||
0.5 *
|
||||
(update_command_angular[1:] + update_command_angular[:-1]) *
|
||||
update_dt
|
||||
)
|
||||
heading_threshold = np.maximum(
|
||||
5.0,
|
||||
expected_heading_delta * 3.0 + 2.0,
|
||||
)
|
||||
jump_before_current = (
|
||||
(update_distance > distance_threshold) |
|
||||
(update_heading_delta > heading_threshold)
|
||||
)
|
||||
jump_at_update = np.r_[False, jump_before_current]
|
||||
segment_ids = np.cumsum(jump_at_update.astype(int))
|
||||
|
||||
jump_events: list[dict[str, float]] = []
|
||||
for current_index in np.flatnonzero(jump_at_update):
|
||||
previous_index = current_index - 1
|
||||
jump_events.append({
|
||||
"time_seconds": float(update_time[current_index]),
|
||||
"distance_mm": float(update_distance[previous_index]),
|
||||
"heading_change_degrees":
|
||||
float(update_heading_delta[previous_index]),
|
||||
"before_x_mm": float(update_x[previous_index]),
|
||||
"before_y_mm": float(update_y[previous_index]),
|
||||
"after_x_mm": float(update_x[current_index]),
|
||||
"after_y_mm": float(update_y[current_index]),
|
||||
})
|
||||
|
||||
# 不跨越定位跳变插值。跳变前后之间保留NaN,使轨迹图自然断线,
|
||||
# 也防止SG滤波把坐标修正涂抹成车辆高速运动。
|
||||
x_resampled = np.full_like(time_uniform, np.nan)
|
||||
y_resampled = np.full_like(time_uniform, np.nan)
|
||||
theta_resampled = np.full_like(time_uniform, np.nan)
|
||||
update_theta_unwrapped = np.rad2deg(
|
||||
np.unwrap(np.deg2rad(update_theta))
|
||||
)
|
||||
maximum_segment_id = int(segment_ids[-1])
|
||||
for segment_id in range(maximum_segment_id + 1):
|
||||
segment_mask = segment_ids == segment_id
|
||||
segment_time = update_time[segment_mask]
|
||||
if segment_time.size == 0:
|
||||
continue
|
||||
|
||||
interval_start = (
|
||||
0.0 if segment_id == 0 else float(segment_time[0])
|
||||
)
|
||||
interval_end = (
|
||||
duration
|
||||
if segment_id == maximum_segment_id
|
||||
else float(segment_time[-1])
|
||||
)
|
||||
uniform_mask = (
|
||||
(time_uniform >= interval_start) &
|
||||
(time_uniform <= interval_end)
|
||||
)
|
||||
x_resampled[uniform_mask] = np.interp(
|
||||
time_uniform[uniform_mask],
|
||||
segment_time,
|
||||
update_x[segment_mask],
|
||||
)
|
||||
y_resampled[uniform_mask] = np.interp(
|
||||
time_uniform[uniform_mask],
|
||||
segment_time,
|
||||
update_y[segment_mask],
|
||||
)
|
||||
theta_resampled[uniform_mask] = np.interp(
|
||||
time_uniform[uniform_mask],
|
||||
segment_time,
|
||||
update_theta_unwrapped[segment_mask],
|
||||
)
|
||||
|
||||
x_filtered = segmented_savgol(
|
||||
x_resampled,
|
||||
sample_interval,
|
||||
filter_window_seconds,
|
||||
)
|
||||
y_filtered = segmented_savgol(
|
||||
y_resampled,
|
||||
sample_interval,
|
||||
filter_window_seconds,
|
||||
)
|
||||
theta_filtered = segmented_savgol(
|
||||
theta_resampled,
|
||||
sample_interval,
|
||||
filter_window_seconds,
|
||||
)
|
||||
|
||||
exclusion_half_width = max(
|
||||
0.30,
|
||||
filter_window_seconds * 0.5,
|
||||
)
|
||||
jump_exclusion_windows = [
|
||||
(
|
||||
max(0.0, event["time_seconds"] - exclusion_half_width),
|
||||
min(duration, event["time_seconds"] + exclusion_half_width),
|
||||
)
|
||||
for event in jump_events
|
||||
]
|
||||
invalid_near_jump = np.zeros(len(time_uniform), dtype=bool)
|
||||
for start, end in jump_exclusion_windows:
|
||||
invalid_near_jump |= (
|
||||
(time_uniform >= start) & (time_uniform <= end)
|
||||
)
|
||||
|
||||
frame = pd.DataFrame({
|
||||
"TimeSeconds": time_uniform,
|
||||
"DetourXRawMm": x_resampled,
|
||||
"DetourYRawMm": y_resampled,
|
||||
"DetourXFilteredMm": x_filtered,
|
||||
"DetourYFilteredMm": y_filtered,
|
||||
"DetourThetaUnwrappedDeg": theta_filtered,
|
||||
"DetourThetaDeg": wrap_degrees(theta_filtered),
|
||||
"CommandSpeedMps": interpolate_command("CommandSpeed"),
|
||||
"CommandAngularSpeedDegPerSec":
|
||||
interpolate_command("CommandAngularSpeed"),
|
||||
"InvalidNearLocalizationJump": invalid_near_jump,
|
||||
})
|
||||
|
||||
first = raw.iloc[0]
|
||||
metadata: dict[str, Any] = {
|
||||
"csv_path": csv_path,
|
||||
"trajectory_name": str(first["TrajectoryName"]),
|
||||
"controller_name": str(first.get("ControllerName", "")),
|
||||
"trial_number": str(first.get("TrialNumber", "")),
|
||||
"reference_start_mm": np.array(
|
||||
[first["ReferenceStartX"], first["ReferenceStartY"]],
|
||||
dtype=float,
|
||||
),
|
||||
"reference_end_mm": np.array(
|
||||
[first["ReferenceEndX"], first["ReferenceEndY"]],
|
||||
dtype=float,
|
||||
),
|
||||
"reference_speed_mps": float(first["ReferenceSpeed"]),
|
||||
# 圆弧构造时使用了测试开始处Detour航向,因此这里取首帧航向。
|
||||
"start_heading_degrees": float(first["DetourTheta"]),
|
||||
"sample_interval_seconds": sample_interval,
|
||||
"filter_window_seconds": filter_window_seconds,
|
||||
"raw_sample_count": len(raw),
|
||||
"detour_update_count": len(updates),
|
||||
"held_sample_count": int(len(raw) - len(updates)),
|
||||
"localization_jump_events": jump_events,
|
||||
"jump_exclusion_windows": jump_exclusion_windows,
|
||||
}
|
||||
return frame, metadata
|
||||
|
||||
|
||||
def build_reference(
|
||||
frame: pd.DataFrame,
|
||||
metadata: dict[str, Any],
|
||||
) -> dict[str, np.ndarray | float | str]:
|
||||
"""根据CSV元数据建立直线、圆弧或原地自转参考及误差。"""
|
||||
trajectory_name = str(metadata["trajectory_name"])
|
||||
start = np.asarray(metadata["reference_start_mm"], dtype=float)
|
||||
end = np.asarray(metadata["reference_end_mm"], dtype=float)
|
||||
actual = frame[
|
||||
["DetourXFilteredMm", "DetourYFilteredMm"]
|
||||
].to_numpy(dtype=float)
|
||||
actual_heading = frame["DetourThetaUnwrappedDeg"].to_numpy(dtype=float)
|
||||
|
||||
radius_match = re.search(
|
||||
r"LeftArc(?P<sweep>[0-9.]+)_R(?P<radius>[0-9.]+)mm",
|
||||
trajectory_name,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
if radius_match:
|
||||
radius = float(radius_match.group("radius"))
|
||||
sweep_degrees = float(radius_match.group("sweep"))
|
||||
start_heading = float(metadata["start_heading_degrees"])
|
||||
heading_radians = np.deg2rad(start_heading)
|
||||
center = start + radius * np.array(
|
||||
[-np.sin(heading_radians), np.cos(heading_radians)]
|
||||
)
|
||||
start_radial_degrees = start_heading - 90.0
|
||||
|
||||
radial = actual - center
|
||||
distance_to_center = np.linalg.norm(radial, axis=1)
|
||||
radial_angle_degrees = np.rad2deg(
|
||||
np.arctan2(radial[:, 1], radial[:, 0])
|
||||
)
|
||||
radial_angle_radians = np.deg2rad(radial_angle_degrees)
|
||||
reference_points = center + radius * np.column_stack([
|
||||
np.cos(radial_angle_radians),
|
||||
np.sin(radial_angle_radians),
|
||||
])
|
||||
# 对逆时针圆弧,正横向误差表示车辆位于轨迹左侧(圆内侧)。
|
||||
lateral_error = radius - distance_to_center
|
||||
reference_heading = radial_angle_degrees + 90.0
|
||||
heading_error = wrap_degrees(
|
||||
actual_heading - reference_heading
|
||||
)
|
||||
|
||||
plot_angles = np.deg2rad(
|
||||
np.linspace(
|
||||
start_radial_degrees,
|
||||
start_radial_degrees + sweep_degrees,
|
||||
361,
|
||||
)
|
||||
)
|
||||
ideal_plot = center + radius * np.column_stack([
|
||||
np.cos(plot_angles),
|
||||
np.sin(plot_angles),
|
||||
])
|
||||
return {
|
||||
"kind": "left_arc",
|
||||
"ideal_plot_mm": ideal_plot,
|
||||
"reference_points_mm": reference_points,
|
||||
"reference_heading_degrees": reference_heading,
|
||||
"lateral_error_mm": lateral_error,
|
||||
"heading_error_degrees": heading_error,
|
||||
"center_mm": center,
|
||||
"radius_mm": radius,
|
||||
}
|
||||
|
||||
line = end - start
|
||||
length = float(np.linalg.norm(line))
|
||||
if length <= 1e-6:
|
||||
rotation_match = re.search(
|
||||
r"Rotate(?P<angle>[+-]?[0-9.]+)",
|
||||
trajectory_name,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
if rotation_match:
|
||||
relative_angle_degrees = float(
|
||||
rotation_match.group("angle")
|
||||
)
|
||||
target_heading_degrees = (
|
||||
float(metadata["start_heading_degrees"]) +
|
||||
relative_angle_degrees
|
||||
)
|
||||
reference_points = np.repeat(
|
||||
start[np.newaxis, :],
|
||||
len(frame),
|
||||
axis=0,
|
||||
)
|
||||
position_drift = np.linalg.norm(
|
||||
actual - start,
|
||||
axis=1,
|
||||
)
|
||||
reference_heading = np.full(
|
||||
len(frame),
|
||||
target_heading_degrees,
|
||||
)
|
||||
heading_error = wrap_degrees(
|
||||
actual_heading - reference_heading
|
||||
)
|
||||
ideal_plot = np.repeat(
|
||||
start[np.newaxis, :],
|
||||
2,
|
||||
axis=0,
|
||||
)
|
||||
return {
|
||||
"kind": "in_place_rotation",
|
||||
"ideal_plot_mm": ideal_plot,
|
||||
"reference_points_mm": reference_points,
|
||||
"reference_heading_degrees": reference_heading,
|
||||
# 对原地自转,该字段表示偏离初始旋转中心的距离。
|
||||
"lateral_error_mm": position_drift,
|
||||
"heading_error_degrees": heading_error,
|
||||
"rotation_center_mm": start,
|
||||
"relative_angle_degrees": relative_angle_degrees,
|
||||
"target_heading_degrees": target_heading_degrees,
|
||||
}
|
||||
|
||||
raise ValueError(
|
||||
f"{trajectory_name}无法识别为圆弧,且参考直线长度为0。"
|
||||
)
|
||||
|
||||
tangent = line / length
|
||||
left_normal = np.array([-tangent[1], tangent[0]])
|
||||
displacement = actual - start
|
||||
progress = np.clip(displacement @ tangent, 0.0, length)
|
||||
reference_points = start + np.outer(progress, tangent)
|
||||
lateral_error = (actual - reference_points) @ left_normal
|
||||
reference_heading_scalar = np.rad2deg(
|
||||
np.arctan2(tangent[1], tangent[0])
|
||||
)
|
||||
reference_heading = np.full(len(frame), reference_heading_scalar)
|
||||
heading_error = wrap_degrees(
|
||||
actual_heading - reference_heading
|
||||
)
|
||||
ideal_plot = np.linspace(start, end, 361)
|
||||
return {
|
||||
"kind": "line",
|
||||
"ideal_plot_mm": ideal_plot,
|
||||
"reference_points_mm": reference_points,
|
||||
"reference_heading_degrees": reference_heading,
|
||||
"lateral_error_mm": lateral_error,
|
||||
"heading_error_degrees": heading_error,
|
||||
}
|
||||
|
||||
|
||||
def discover_csv_files(arguments: list[str]) -> list[Path]:
|
||||
"""解析命令行CSV;未指定时使用脚本目录下全部CSV。"""
|
||||
if arguments:
|
||||
files = [Path(item).expanduser().resolve() for item in arguments]
|
||||
else:
|
||||
files = sorted(SCRIPT_DIR.glob("*.csv"))
|
||||
if not files:
|
||||
raise FileNotFoundError("没有找到可处理的CSV文件。")
|
||||
return files
|
||||
|
||||
|
||||
def output_path(
|
||||
csv_path: Path,
|
||||
output_directory: str | None,
|
||||
suffix: str,
|
||||
) -> Path:
|
||||
"""构造图片输出路径并创建目录。"""
|
||||
directory = (
|
||||
Path(output_directory).expanduser().resolve()
|
||||
if output_directory
|
||||
else csv_path.parent / "plots"
|
||||
)
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
return directory / f"{csv_path.stem}_{suffix}.png"
|
||||
|
||||
|
||||
def plot_trajectory(
|
||||
csv_path: Path,
|
||||
frequency_hz: float,
|
||||
filter_window_seconds: float,
|
||||
output_directory: str | None,
|
||||
show: bool,
|
||||
) -> Path:
|
||||
"""生成单份CSV的理想/实际轨迹对比图。"""
|
||||
frame, metadata = load_and_resample(
|
||||
csv_path,
|
||||
frequency_hz,
|
||||
filter_window_seconds,
|
||||
)
|
||||
reference = build_reference(frame, metadata)
|
||||
|
||||
actual_x_m = frame["DetourXFilteredMm"].to_numpy() / 1000.0
|
||||
actual_y_m = frame["DetourYFilteredMm"].to_numpy() / 1000.0
|
||||
ideal_m = np.asarray(reference["ideal_plot_mm"]) / 1000.0
|
||||
|
||||
fig, ax = plt.subplots(figsize=(8.0, 7.0))
|
||||
ax.plot(
|
||||
ideal_m[:, 0],
|
||||
ideal_m[:, 1],
|
||||
"--",
|
||||
linewidth=2.2,
|
||||
label="理想轨迹",
|
||||
)
|
||||
ax.plot(
|
||||
actual_x_m,
|
||||
actual_y_m,
|
||||
linewidth=1.8,
|
||||
label="Detour实际轨迹(滤波后)",
|
||||
)
|
||||
if reference["kind"] == "in_place_rotation":
|
||||
ax.scatter(
|
||||
[ideal_m[0, 0]],
|
||||
[ideal_m[0, 1]],
|
||||
marker="*",
|
||||
s=100,
|
||||
label="理想旋转中心",
|
||||
zorder=5,
|
||||
)
|
||||
else:
|
||||
ax.scatter(
|
||||
[ideal_m[0, 0]],
|
||||
[ideal_m[0, 1]],
|
||||
marker="o",
|
||||
s=55,
|
||||
label="起点",
|
||||
zorder=5,
|
||||
)
|
||||
ax.scatter(
|
||||
[ideal_m[-1, 0]],
|
||||
[ideal_m[-1, 1]],
|
||||
marker="x",
|
||||
s=65,
|
||||
label="终点",
|
||||
zorder=5,
|
||||
)
|
||||
for event_index, event in enumerate(
|
||||
metadata["localization_jump_events"]
|
||||
):
|
||||
before = np.array([
|
||||
event["before_x_mm"],
|
||||
event["before_y_mm"],
|
||||
]) / 1000.0
|
||||
after = np.array([
|
||||
event["after_x_mm"],
|
||||
event["after_y_mm"],
|
||||
]) / 1000.0
|
||||
ax.scatter(
|
||||
[before[0], after[0]],
|
||||
[before[1], after[1]],
|
||||
marker="x",
|
||||
color="tab:red",
|
||||
s=55,
|
||||
zorder=6,
|
||||
label="Detour定位跳变前/后"
|
||||
if event_index == 0 else None,
|
||||
)
|
||||
ax.annotate(
|
||||
f"定位跳变 {event['distance_mm']:.1f} mm\n"
|
||||
f"t={event['time_seconds']:.2f} s",
|
||||
xy=(after[0], after[1]),
|
||||
xytext=(8, 8),
|
||||
textcoords="offset points",
|
||||
color="tab:red",
|
||||
fontsize=9,
|
||||
)
|
||||
ax.set_aspect("equal", adjustable="box")
|
||||
ax.set_xlabel("世界坐标 X / m")
|
||||
ax.set_ylabel("世界坐标 Y / m")
|
||||
ax.set_title(
|
||||
f"理想轨迹与实际轨迹对比\n"
|
||||
f"{metadata['controller_name']} - "
|
||||
f"{metadata['trajectory_name']}"
|
||||
)
|
||||
ax.grid(True, alpha=0.3)
|
||||
ax.legend()
|
||||
fig.tight_layout()
|
||||
|
||||
destination = output_path(
|
||||
csv_path,
|
||||
output_directory,
|
||||
"trajectory_comparison",
|
||||
)
|
||||
fig.savefig(destination, dpi=300, bbox_inches="tight")
|
||||
if show:
|
||||
plt.show()
|
||||
plt.close(fig)
|
||||
print(
|
||||
f"{csv_path.name}: 原始采样{metadata['raw_sample_count']}帧,"
|
||||
f"有效Detour更新{metadata['detour_update_count']}帧,"
|
||||
f"保持重复{metadata['held_sample_count']}帧,"
|
||||
f"定位跳变{len(metadata['localization_jump_events'])}次"
|
||||
)
|
||||
return destination
|
||||
|
||||
|
||||
def main() -> None:
|
||||
configure_matplotlib()
|
||||
parser = argparse.ArgumentParser(
|
||||
description="绘制理想轨迹与Detour实际轨迹对比图。"
|
||||
)
|
||||
parser.add_argument("files", nargs="*", help="一个或多个CSV文件")
|
||||
parser.add_argument("--frequency", type=float, default=20.0)
|
||||
parser.add_argument("--window", type=float, default=0.55)
|
||||
parser.add_argument("--output-dir")
|
||||
parser.add_argument("--show", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
for csv_path in discover_csv_files(args.files):
|
||||
destination = plot_trajectory(
|
||||
csv_path,
|
||||
args.frequency,
|
||||
args.window,
|
||||
args.output_dir,
|
||||
args.show,
|
||||
)
|
||||
print(f"已生成:{destination}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
After Width: | Height: | Size: 244 KiB |
|
After Width: | Height: | Size: 274 KiB |
|
After Width: | Height: | Size: 316 KiB |
|
After Width: | Height: | Size: 92 KiB |