Compare commits

2 Commits
161 changed files with 8434 additions and 132 deletions
+3 -1
View File
@@ -87,4 +87,6 @@ _ReSharper*/
*.db
*.sqlite
*.sqlite3
*.sqlite3
*.csv
+3 -3
View File
@@ -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>
+589 -95
View File
@@ -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
+457 -23
View File
@@ -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
}
+1 -1
View File
@@ -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;
+394
View File
@@ -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("\"", "\"\"") +
"\"";
}
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
Binary file not shown.
@@ -1 +1 @@
3920568398d269aea7b71fb4581ad111777c15ca3f2f2bb272ee9e0a989215c4
e972a413d047c4137a8ce86cbff54a8d2e2558806d9d974d3d6312467ee8ba4d
Binary file not shown.
Binary file not shown.
@@ -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;
}
}
File diff suppressed because it is too large Load Diff
@@ -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);
}
}
}
+3 -5
View File
@@ -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>
Binary file not shown.
@@ -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": ""
}
}
}
+4
View File
@@ -119,6 +119,10 @@ namespace MyParking.Shared
// 禁用旧版DirectionAngle/ZeroDirection坐标偏置,
// 保证SendXYThSpeed直接使用真实车体坐标系。
ResetToBodyFrame();
// 停车机器人优先保持当前机械舵角,通过反转轮速表达反向运动,
// 避免蟹行正反切换时四个舵轮无意义地旋转180°。
_chassis.PreferMinimumSteeringTravel = true;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

+100
View File
@@ -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()
+177
View File
@@ -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()
+172
View File
@@ -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()
+706
View File
@@ -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()
Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Some files were not shown because too many files have changed in this diff Show More