Initial commit from MyParking project
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
// 驱动器、急停、夹臂等安全报警
|
||||
using MDCSToolBox.Medulla.Chassis.MultiWheel;
|
||||
|
||||
namespace MedullaAdapter
|
||||
{
|
||||
public class AlarmRoutine : MultiWheelAlarmRoutine<DiverCartDefinition>
|
||||
{
|
||||
// M层单车安全:汇总停车机器人夹臂等自定义报警状态。
|
||||
public override void SetOtherAlarms()
|
||||
{
|
||||
AddAlarm("左夹臂驱动报警", 2, () => cart.LeftArmErrorCode != 0);
|
||||
AddAlarm("右夹臂驱动报警", 2, () => cart.RightArmErrorCode != 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,532 @@
|
||||
// 定义车型、上下层IO、参数和MCU初始化
|
||||
using CartActivator;
|
||||
using MCUSerialBridgeCLR;
|
||||
using MDCSToolBox.Medulla.Chassis.MultiWheel;
|
||||
using Medulla.Types;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using MyParking.Shared;
|
||||
|
||||
namespace MedullaAdapter
|
||||
{
|
||||
[UseLadderLogic(logic = typeof(AlarmRoutine), scanInterval = 50)]
|
||||
[UseLadderLogic(logic = typeof(MotorRoutine), scanInterval = 50)]
|
||||
[UseLadderLogic(logic = typeof(MCURoutine), scanInterval = 20)]
|
||||
[UseManualController(manualController = typeof(Remote))]
|
||||
public class DiverCartDefinition : MultiWheelCartDefinition
|
||||
{
|
||||
#region 基本成员
|
||||
public MCUSerialBridge Bridge;
|
||||
|
||||
internal enum ManualControlMode
|
||||
{
|
||||
Normal = 0, // 正常模式
|
||||
Crab = 1, // 螃蟹模式
|
||||
Spin = 2, // 自旋模式
|
||||
}
|
||||
internal ManualControlMode TransmitterControlMode = ManualControlMode.Normal;
|
||||
internal DateTime TransmitterLastTime = DateTime.Now; // 物理遥控器计算两次实体遥控器指令之间的时间间隔
|
||||
private ManualControlMode? _pendingManualMode;
|
||||
private ManualControlMode? _activeManualMode;
|
||||
#endregion
|
||||
|
||||
#region AsUpperIO
|
||||
[AsUpperIO(desc = "从C上复位")] public bool ResetFromC;
|
||||
[AsUpperIO(desc = "从C将驱动轮下使能")] public bool DisableFromC;
|
||||
[AsUpperIO(desc = "左夹臂下发速度", timeOutReset = true)] public float SpeedLeftArm;
|
||||
[AsUpperIO(desc = "右夹臂下发速度", timeOutReset = true)] public float SpeedRightArm;
|
||||
[AsUpperIO(desc = "夹臂不同步报警")] public bool ClampOutOfSync;
|
||||
#endregion
|
||||
|
||||
#region AsLowerIO
|
||||
[AsLowerIO(desc = "左前左轮实际位置")] public float LFLActualPos;
|
||||
[AsLowerIO(desc = "左前右轮实际位置")] public float LFRActualPos;
|
||||
[AsLowerIO(desc = "右前左轮实际位置")] public float RFLActualPos;
|
||||
[AsLowerIO(desc = "右前右轮实际位置")] public float RFRActualPos;
|
||||
[AsLowerIO(desc = "左后左轮实际位置")] public float LRLActualPos;
|
||||
[AsLowerIO(desc = "左后右轮实际位置")] public float LRRActualPos;
|
||||
[AsLowerIO(desc = "右后左轮实际位置")] public float RRLActualPos;
|
||||
[AsLowerIO(desc = "右后右轮实际位置")] public float RRRActualPos;
|
||||
[AsLowerIO(desc = "左夹臂实际速度")] public float ActualSpeedLeftArm;
|
||||
[AsLowerIO(desc = "右夹臂实际速度")] public float ActualSpeedRightArm;
|
||||
[AsLowerIO(desc = "左夹臂状态字")] public int LeftArmStateCode;
|
||||
[AsLowerIO(desc = "右夹臂状态字")] public int RightArmStateCode;
|
||||
[AsLowerIO(desc = "左夹臂错误字")] public int LeftArmErrorCode;
|
||||
[AsLowerIO(desc = "右夹臂错误字")] public int RightArmErrorCode;
|
||||
[AsLowerIO(desc = "左夹臂电流")] public float LeftArmElectric;
|
||||
[AsLowerIO(desc = "右夹臂电流")] public float RightArmElectric;
|
||||
[AsLowerIO(desc = "左夹臂实际位置")] public float ActualPosLeftArm;
|
||||
[AsLowerIO(desc = "右夹臂实际位置")] public float ActualPosRightArm;
|
||||
[AsLowerIO(desc = "驱动轮使能状态")] public bool WheelAbleState = true;
|
||||
[AsLowerIO(desc = "电池健康状态")] public float SOH;
|
||||
[AsInitParam(desc = "车号")][AsLowerIO] public int CarNum = 1;
|
||||
#endregion
|
||||
|
||||
#region 初始参数
|
||||
[AsInitParam(desc = "MCU端口号")] public string MCUPort = "COM4";
|
||||
[AsInitParam(desc = "遥控器速度上限")] public float TransmitterSpeedUpperLimit = 1.0f;
|
||||
[AsInitParam(desc = "遥控器速度下限")] public float TransmitterSpeedLowerLimit = 0.0f;
|
||||
[AsInitParam(desc = "手动控制夹臂速度系数")] public float ManualArmSpeedFac = 1.0f;
|
||||
[AsInitParam(desc = "遥控转弯舵角同步限速宽度,单位为度")]
|
||||
public float ManualSteeringAlignmentSigmaDegrees = 8.0f;
|
||||
[AsInitParam(desc = "自转最大角速度,单位deg/s")]
|
||||
public float MaxSpinAngularSpeedDegreesPerSecond = 30f;
|
||||
[AsInitParam(desc = "轮速诊断日志相对目录")]
|
||||
public string WheelSpeedDiagnosticDirectory =
|
||||
@"logs\wheel-speed";
|
||||
[AsInitParam(desc = "左夹臂低限位")][AsLowerIO] public int LeftArmLowerPos = -10000;
|
||||
[AsInitParam(desc = "左夹臂高限位")][AsLowerIO] public int LeftArmUpperPos = 5927610;
|
||||
[AsInitParam(desc = "右夹臂低限位")][AsLowerIO] public int RightArmLowerPos = -17295;
|
||||
[AsInitParam(desc = "右夹臂高限位")][AsLowerIO] public int RightArmUpperPos = 5927610;
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region 监控参数
|
||||
[IOObjectMonitor(desc = "从M上复位")] public bool ResetFromM;
|
||||
[IOObjectMonitor(desc = "从M将驱动轮下使能")] public bool DisableFromM;
|
||||
[IOObjectMonitor(desc = "左前左轮PID修正后速度")] public float SpeedLFL;
|
||||
[IOObjectMonitor(desc = "左前右轮PID修正后速度")] public float SpeedLFR;
|
||||
[IOObjectMonitor(desc = "右前左轮PID修正后速度")] public float SpeedRFL;
|
||||
[IOObjectMonitor(desc = "右前右轮PID修正后速度")] public float SpeedRFR;
|
||||
[IOObjectMonitor(desc = "左后左轮PID修正后速度")] public float SpeedLRL;
|
||||
[IOObjectMonitor(desc = "左后右轮PID修正后速度")] public float SpeedLRR;
|
||||
[IOObjectMonitor(desc = "右后左轮PID修正后速度")] public float SpeedRRL;
|
||||
[IOObjectMonitor(desc = "右后右轮PID修正后速度")] public float SpeedRRR;
|
||||
[IOObjectMonitor(desc = "左前舵轮转向PID输出")] public float DiffSteerOutputLeftFront;
|
||||
[IOObjectMonitor(desc = "左后舵轮转向PID输出")] public float DiffSteerOutputLeftRear;
|
||||
[IOObjectMonitor(desc = "右前舵轮转向PID输出")] public float DiffSteerOutputRightFront;
|
||||
[IOObjectMonitor(desc = "右后舵轮转向PID输出")] public float DiffSteerOutputRightRear;
|
||||
[IOObjectMonitor(desc = "灯光模式")] public int LightMode = 0;
|
||||
[IOObjectMonitor(desc = "实体遥控器当前速度倍率")] public float TransmitterSpeed = 0.3f;
|
||||
[IOObjectMonitor(desc = "轮速诊断记录已启用")]
|
||||
public bool WheelSpeedDiagnosticEnabled;
|
||||
[IOObjectMonitor(desc = "轮速诊断记录状态")]
|
||||
public string WheelSpeedDiagnosticStatus = "未启动";
|
||||
[IOObjectMonitor(desc = "左前左驱动器远程帧701")] public byte LFLRemoteCode = 0;
|
||||
[IOObjectMonitor(desc = "左前右驱动器远程帧702")] public byte LFRRemoteCode = 0;
|
||||
[IOObjectMonitor(desc = "右前左驱动器远程帧703")] public byte RFLRemoteCode = 0;
|
||||
[IOObjectMonitor(desc = "右前右驱动器远程帧704")] public byte RFRRemoteCode = 0;
|
||||
[IOObjectMonitor(desc = "左后左驱动器远程帧705")] public byte LRLRemoteCode = 0;
|
||||
[IOObjectMonitor(desc = "左后右驱动器远程帧706")] public byte LRRRemoteCode = 0;
|
||||
[IOObjectMonitor(desc = "右后左驱动器远程帧707")] public byte RRLRemoteCode = 0;
|
||||
[IOObjectMonitor(desc = "右后右驱动器远程帧708")] public byte RRRRemoteCode = 0;
|
||||
[IOObjectMonitor(desc = "左夹臂驱动器远程帧709")] public byte LArmRemoteCode = 0;
|
||||
[IOObjectMonitor(desc = "右夹臂驱动器远程帧70A")] public byte RArmRemoteCode = 0;
|
||||
#endregion
|
||||
|
||||
#region 操作按钮
|
||||
// M层单车硬件:向驱动轮发送复位请求。
|
||||
[IOObjectUtility]
|
||||
public void WheelReset()
|
||||
{
|
||||
ResetFromM = true;
|
||||
}
|
||||
// M层单车硬件:向驱动轮发送下使能请求。
|
||||
[IOObjectUtility]
|
||||
public void WheelDisable()
|
||||
{
|
||||
DisableFromM = true;
|
||||
}
|
||||
|
||||
// M层诊断:请求开始保存CAN轮速事件和底盘周期快照。
|
||||
[IOObjectUtility]
|
||||
public void StartWheelSpeedDiagnostic()
|
||||
{
|
||||
WheelSpeedDiagnosticEnabled = true;
|
||||
WheelSpeedDiagnosticStatus = "等待创建日志文件";
|
||||
}
|
||||
|
||||
// M层诊断:请求停止轮速记录并刷新CSV文件。
|
||||
[IOObjectUtility]
|
||||
public void StopWheelSpeedDiagnostic()
|
||||
{
|
||||
WheelSpeedDiagnosticEnabled = false;
|
||||
WheelSpeedDiagnosticStatus = "等待停止并刷新日志";
|
||||
}
|
||||
#endregion
|
||||
|
||||
public override void CommunicationInit()
|
||||
{
|
||||
if (GhostMode) return;
|
||||
State = -1;
|
||||
Bridge = new MCUSerialBridge();
|
||||
//Step1:打开指定串口连接
|
||||
var err = Bridge.Open(MCUPort, 1000000u);
|
||||
if (err != MCUSerialBridgeError.OK)
|
||||
{
|
||||
Console.WriteLine($"MCU Open FAILED: {err.ToDescription()}");
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("MCU Open OK");
|
||||
}
|
||||
//Step2:远程复位MCU
|
||||
err = Bridge.Reset();
|
||||
if (err != MCUSerialBridgeError.OK)
|
||||
{
|
||||
Console.WriteLine($"MCU Reset FAILED: {err.ToDescription()}");
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
Thread.Sleep(500);
|
||||
Console.WriteLine("MCU Reset OK");
|
||||
}
|
||||
//Step3:获取MCU版本号
|
||||
err = Bridge.GetVersion(out var version, 100);
|
||||
if (err != MCUSerialBridgeError.OK)
|
||||
{
|
||||
Console.WriteLine($"MCU GetVersion FAILED: {err.ToDescription()}");
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"MCU GetVersion OK: {version}");
|
||||
}
|
||||
//Step4:获取MCU状态
|
||||
err = Bridge.GetState(out var state, 100);
|
||||
if (err != MCUSerialBridgeError.OK)
|
||||
{
|
||||
Console.WriteLine($"MCU GetState FAILED: {err.ToDescription()}");
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"MCU GetState OK: {state}");
|
||||
}
|
||||
//Step5:串口/CAN配置
|
||||
try
|
||||
{
|
||||
var ports = new List<PortConfig>();
|
||||
for (int i = 0; i < 1; i++)
|
||||
ports.Add(new CANPortConfig(500000, 10));
|
||||
for (int i = 0; i < 3; i++)
|
||||
ports.Add(new SerialPortConfig(9600, 10));
|
||||
Console.WriteLine("=== Port Configuration ===");
|
||||
for (int i = 0; i < ports.Count; i++)
|
||||
{
|
||||
if (ports[i] is SerialPortConfig s)
|
||||
Console.WriteLine($"Port {i}: Serial, Baud={s.Baud}, ReceiveFrameMs={s.ReceiveFrameMs}");
|
||||
else if (ports[i] is CANPortConfig c)
|
||||
Console.WriteLine($"Port {i}: CAN, Baud={c.Baud}, RetryTimeMs={c.RetryTimeMs}");
|
||||
}
|
||||
|
||||
var ret = Bridge.Configure(ports, 200);
|
||||
if (ret != MCUSerialBridgeError.OK)
|
||||
{
|
||||
Console.WriteLine($"MCU Configure FAILED: {ret.ToDescription()}");
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("MCU Configure OK");
|
||||
}
|
||||
Console.WriteLine("MCU Configure {0}", ret == MCUSerialBridgeError.OK ? "OK" : $"FAILED: 0x{(uint)ret:X8}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Configure Exception: {ex.Message}");
|
||||
return;
|
||||
}
|
||||
State = 0;
|
||||
}
|
||||
|
||||
internal void ManualControl(
|
||||
ManualControlMode mode,
|
||||
float x,
|
||||
float y,
|
||||
float frontDirection,
|
||||
float speedThreshold,
|
||||
TimeSpan? interval = null)
|
||||
{
|
||||
if (Chassis == null) return;
|
||||
|
||||
var adapter = GetChassisAdapter();
|
||||
if (adapter == null) return;
|
||||
|
||||
// 模式变化时先停车并下发舵轮准备角度;
|
||||
// 在实际舵角到位之前,不开放驱动速度。
|
||||
if (!EnsureManualModeReady(mode, interval))
|
||||
{
|
||||
adapter.StopImmediately();
|
||||
return;
|
||||
}
|
||||
|
||||
var speed = speedThreshold * y;
|
||||
var normalizedSteering =
|
||||
(float)Math.Pow(
|
||||
Math.Abs(x),
|
||||
ManualThetaPow) *
|
||||
Math.Sign(x);
|
||||
var steeringDegrees =
|
||||
-normalizedSteering * MaxManualTheta;
|
||||
var frontTh = steeringDegrees;
|
||||
var rearTh = -steeringDegrees;
|
||||
ManualMode = (int)mode;
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
case ManualControlMode.Normal:
|
||||
// 普通模式统一使用车体速度命令:
|
||||
// X向前,行驶中连续改变角速度时舵轮边转、车辆边走。
|
||||
// SendBodyCommand(
|
||||
// vx: speed,
|
||||
// vy: 0.0,
|
||||
// omegaRadiansPerSecond: omega,
|
||||
// interval);
|
||||
Chassis.SendMotion(
|
||||
speed,
|
||||
frontTh,
|
||||
rearTh,
|
||||
interval);
|
||||
break;
|
||||
case ManualControlMode.Crab:
|
||||
// 舵轮机械范围为[-120°,120°]。
|
||||
// 蟹行后虚拟轴距由原车宽度决定,比正常模式轴距短。
|
||||
// 按几何比例缩小转角,使相同摇杆输入获得接近一致的曲率。
|
||||
var normalSteeringRadians =
|
||||
AngleMath.DegreesToRadians(steeringDegrees);
|
||||
var geometryRatio =
|
||||
adapter.HalfTrackWidthMeters /
|
||||
adapter.HalfWheelBaseMeters;
|
||||
|
||||
// +90°运动坐标系已经把虚拟左侧映射为车体后方,
|
||||
// 此处保持普通模式的转向符号,避免再次取反导致左右颠倒。
|
||||
var crabSteeringRadians =
|
||||
Math.Atan(
|
||||
geometryRatio *
|
||||
Math.Tan(
|
||||
normalSteeringRadians));
|
||||
|
||||
// 蟹行转角最终限制为±30°,为±120°机械舵角保留余量。
|
||||
var maximumCrabSteeringRadians =
|
||||
AngleMath.DegreesToRadians(30.0);
|
||||
crabSteeringRadians = Math.Max(
|
||||
-maximumCrabSteeringRadians,
|
||||
Math.Min(
|
||||
maximumCrabSteeringRadians,
|
||||
crabSteeringRadians));
|
||||
|
||||
// 将车体左侧作为虚拟阿克曼车头,并在该运动坐标系中
|
||||
// 复用与普通模式相同的SendMotion前后控制点解算。
|
||||
if (!adapter.SendVirtualAckermannMotion(
|
||||
motionDirectionRadians:
|
||||
Math.PI / 2.0,
|
||||
speedMetersPerSecond:
|
||||
speed,
|
||||
steeringRadians:
|
||||
crabSteeringRadians,
|
||||
interval))
|
||||
{
|
||||
adapter.StopImmediately();
|
||||
|
||||
Console.WriteLine(
|
||||
"蟹行SendMotion命令分解失败,车辆已经停车:" +
|
||||
adapter.LastFailureReason);
|
||||
}
|
||||
break;
|
||||
case ManualControlMode.Spin:
|
||||
// 摇杆处于中位时只清零驱动速度,保持已经准备好的
|
||||
// 自转舵角;下次推动摇杆时仍会重新检查实际舵角。
|
||||
if (Math.Abs(speed) < 1e-6f)
|
||||
{
|
||||
adapter
|
||||
.StopXYThDrivePreserveSteeringState();
|
||||
break;
|
||||
}
|
||||
|
||||
// 自转时speed表示最外侧舵轮中心的目标切向速度,
|
||||
// 根据v=omega*r换算为SendXYThSpeed需要的角速度。
|
||||
var requestedSpinOmegaRadiansPerSecond =
|
||||
speed /
|
||||
adapter.MaximumWheelRadiusMeters;
|
||||
|
||||
// 对半径换算结果做正负对称限幅,防止遥控速度参数误设后自转过快。
|
||||
var maximumSpinOmegaRadiansPerSecond =
|
||||
AngleMath.DegreesToRadians(
|
||||
Math.Max(
|
||||
0f,
|
||||
MaxSpinAngularSpeedDegreesPerSecond));
|
||||
|
||||
var spinOmegaRadiansPerSecond =
|
||||
Math.Max(
|
||||
-maximumSpinOmegaRadiansPerSecond,
|
||||
Math.Min(
|
||||
maximumSpinOmegaRadiansPerSecond,
|
||||
requestedSpinOmegaRadiansPerSecond));
|
||||
|
||||
// 普通安全版SendXYThSpeed只下发角速度,
|
||||
// 四轮实际舵角未到位时不会开放驱动速度。
|
||||
if (!adapter.Send(
|
||||
new ChassisCommand(
|
||||
CarNum,
|
||||
new Twist2D(
|
||||
0.0,
|
||||
0.0,
|
||||
spinOmegaRadiansPerSecond)),
|
||||
interval))
|
||||
{
|
||||
adapter.StopImmediately();
|
||||
|
||||
Console.WriteLine(
|
||||
"SendXYThSpeed原地自转命令分解失败,车辆已经停车:" +
|
||||
adapter.LastFailureReason);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
ManualMode = -1;
|
||||
adapter.StopImmediately();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 停车后切换模式:先预转舵轮,实际角度到位后才允许发送运动命令。
|
||||
private bool EnsureManualModeReady(
|
||||
ManualControlMode mode,
|
||||
TimeSpan? interval)
|
||||
{
|
||||
var adapter = GetChassisAdapter();
|
||||
if (adapter == null)
|
||||
return false;
|
||||
|
||||
// 当前模式已经完成准备,可以直接接受运动命令。
|
||||
if (_activeManualMode == mode &&
|
||||
_pendingManualMode == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// 第一次收到新模式时,停车并下发一次舵轮准备姿态。
|
||||
if (_pendingManualMode != mode)
|
||||
{
|
||||
adapter.StopImmediately();
|
||||
// 所有模式的准备角度均按真实机械舵角表达;
|
||||
// 先退出上一模式的虚拟运动坐标系,再执行预对齐。
|
||||
adapter.ResetToBodyFrame();
|
||||
_activeManualMode = null;
|
||||
|
||||
var preparationAccepted = mode switch
|
||||
{
|
||||
ManualControlMode.Normal =>
|
||||
adapter.PrepareParallelDirection(0.0),
|
||||
|
||||
ManualControlMode.Crab =>
|
||||
adapter.PrepareParallelDirection(
|
||||
Math.PI / 2.0),
|
||||
|
||||
ManualControlMode.Spin =>
|
||||
adapter.PrepareSpin(interval),
|
||||
|
||||
_ => false
|
||||
};
|
||||
|
||||
if (!preparationAccepted)
|
||||
{
|
||||
_pendingManualMode = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
_pendingManualMode = mode;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 后续控制周期保持停车,并读取实际舵角判断是否到位。
|
||||
adapter.StopImmediately();
|
||||
|
||||
var toleranceRadians =
|
||||
AngleMath.DegreesToRadians(2.0);
|
||||
|
||||
bool aligned;
|
||||
|
||||
if (mode == ManualControlMode.Spin)
|
||||
{
|
||||
// 自转的四个舵轮目标角不同,等待期间持续刷新其目标。
|
||||
var preparationAccepted =
|
||||
adapter.PrepareSpin(interval);
|
||||
|
||||
aligned =
|
||||
preparationAccepted &&
|
||||
adapter.AreSpinWheelsAligned;
|
||||
}
|
||||
else
|
||||
{
|
||||
var targetDirection = mode ==
|
||||
ManualControlMode.Crab
|
||||
? Math.PI / 2.0
|
||||
: 0.0;
|
||||
|
||||
aligned =
|
||||
adapter.AreParallelWheelsAligned(
|
||||
targetDirection,
|
||||
toleranceRadians);
|
||||
}
|
||||
|
||||
if (!aligned)
|
||||
return false;
|
||||
|
||||
if (mode == ManualControlMode.Spin)
|
||||
{
|
||||
// 四轮实际舵角确认到位后只交接一次,保留PrepareSpin
|
||||
// 选定的机械舵角和轮速方向,避免首条XYTh命令重新选角。
|
||||
if (!adapter.AdoptPreparedSpinForXYTh(
|
||||
toleranceRadians))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// 蟹行轮子在真实车体系中到达机械+90°后,
|
||||
// 再将车体左侧激活为SendMotion的虚拟X正方向。
|
||||
else if (mode == ManualControlMode.Crab)
|
||||
{
|
||||
adapter.ActivateMotionFrame(
|
||||
Math.PI / 2.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
adapter.ResetToBodyFrame();
|
||||
}
|
||||
|
||||
_activeManualMode = mode;
|
||||
_pendingManualMode = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
private MultiWheelChassisAdapter _chassisAdapter;
|
||||
|
||||
private MultiWheelChassisAdapter GetChassisAdapter()
|
||||
{
|
||||
if (Chassis == null)
|
||||
return null;
|
||||
|
||||
if (_chassisAdapter == null ||
|
||||
_chassisAdapter.VehicleId != CarNum)
|
||||
{
|
||||
_chassisAdapter =
|
||||
new MultiWheelChassisAdapter(Chassis, CarNum);
|
||||
}
|
||||
|
||||
_chassisAdapter.SteeringAlignmentSigmaDegrees =
|
||||
Math.Max(
|
||||
ManualSteeringAlignmentSigmaDegrees,
|
||||
0.1f);
|
||||
|
||||
return _chassisAdapter;
|
||||
}
|
||||
|
||||
#region MCURoutine兼容参数(暂保留原硬件协议)
|
||||
|
||||
// 保存MCU读取到的原始输入字节,供M层监控和硬件排查使用。
|
||||
[AsLowerIO(desc = "MCU原始输入字节")]
|
||||
public float test;
|
||||
|
||||
// 保留原MCU灯光分支;单车默认值-1表示使用本车LightMode。
|
||||
[AsUpperIO(desc = "多车灯光同步兼容值,-1使用本车灯光")]
|
||||
public int MultiVehicleLightSync = -1;
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,928 @@
|
||||
// C#调用MCU通信桥
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace MCUSerialBridgeCLR
|
||||
{
|
||||
/// <summary>
|
||||
/// 辅助方法与内部 C 结构体封装,用于 P/Invoke 交互
|
||||
/// </summary>
|
||||
internal static class PortStructHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 将任意结构体序列化为字节数组
|
||||
/// </summary>
|
||||
/// <typeparam name="T">结构体类型</typeparam>
|
||||
/// <param name="str">要序列化的结构体</param>
|
||||
/// <returns>返回结构体对应的字节数组</returns>
|
||||
/// <remarks>使用 Marshal 分配内存并复制内容</remarks>
|
||||
public static byte[] StructToBytes<T>(T str)
|
||||
where T : struct
|
||||
{
|
||||
int size = Marshal.SizeOf<T>();
|
||||
byte[] arr = new byte[size];
|
||||
IntPtr ptr = Marshal.AllocHGlobal(size);
|
||||
try
|
||||
{
|
||||
Marshal.StructureToPtr(str, ptr, false);
|
||||
Marshal.Copy(ptr, arr, 0, size);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(ptr);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 串口端口配置的原生结构体(与 MCU C 层对应)
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public struct SerialPortConfigC
|
||||
{
|
||||
/// <summary>端口类型(0x01 = Serial)</summary>
|
||||
public byte port_type;
|
||||
|
||||
/// <summary>波特率</summary>
|
||||
public uint baud;
|
||||
|
||||
/// <summary>接收帧时间间隔</summary>
|
||||
public uint receive_frame_ms;
|
||||
|
||||
/// <summary>保留字节,填 0</summary>
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 7)]
|
||||
public byte[] reserved;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CAN 端口配置的原生结构体(与 MCU C 层对应)
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public struct CANPortConfigC
|
||||
{
|
||||
/// <summary>端口类型(0x02 = CAN)</summary>
|
||||
public byte port_type;
|
||||
|
||||
/// <summary>波特率</summary>
|
||||
public uint baud;
|
||||
|
||||
/// <summary>最大重发时间</summary>
|
||||
public uint retry_time_ms;
|
||||
|
||||
/// <summary>保留字节,填 0</summary>
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 7)]
|
||||
public byte[] reserved;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MCU 固件版本信息结构体
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
|
||||
public struct VersionInfo
|
||||
{
|
||||
/// <summary>产品型号</summary>
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 16)]
|
||||
public string ProductionName;
|
||||
|
||||
/// <summary>Git 标签</summary>
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 8)]
|
||||
public string GitTag;
|
||||
|
||||
/// <summary>Git commit 哈希值</summary>
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 8)]
|
||||
public string GitCommit;
|
||||
|
||||
/// <summary>编译时间(字符串)</summary>
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 24)]
|
||||
public string BuildTime;
|
||||
|
||||
/// <summary>
|
||||
/// 转换为可读字符串
|
||||
/// </summary>
|
||||
/// <returns>返回包含产品、Tag、Commit、BuildTime 的字符串</returns>
|
||||
// M层MCU适配:格式化固件版本信息便于日志显示。
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Product: {ProductionName}, Tag: {GitTag}, Commit: {GitCommit}, Built: {BuildTime}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MCU 当前运行状态
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
public struct MCUState
|
||||
{
|
||||
/// <summary>原始 32 位状态值</summary>
|
||||
[FieldOffset(0)]
|
||||
public uint RawValue;
|
||||
|
||||
/// <summary>状态子字段 0</summary>
|
||||
[FieldOffset(0)]
|
||||
public byte Substate0;
|
||||
|
||||
/// <summary>状态子字段 1</summary>
|
||||
[FieldOffset(1)]
|
||||
public byte Substate1;
|
||||
|
||||
/// <summary>状态子字段 2</summary>
|
||||
[FieldOffset(2)]
|
||||
public byte Substate2;
|
||||
|
||||
/// <summary>高字节模式标志</summary>
|
||||
[FieldOffset(3)]
|
||||
public byte Mode;
|
||||
|
||||
/// <summary>是否处于 Bridge 模式</summary>
|
||||
public bool IsBridge => (Mode & 0x80) == 0;
|
||||
|
||||
/// <summary>是否处于 DIVER 模式</summary>
|
||||
public bool IsDIVER => (Mode & 0x80) != 0;
|
||||
|
||||
/// <summary>
|
||||
/// 返回可读的状态字符串
|
||||
/// </summary>
|
||||
/// <returns>例如 "Bridge: Running" 或 "DIVER: Error"</returns>
|
||||
// M层MCU适配:格式化MCU运行状态便于日志显示。
|
||||
public override string ToString()
|
||||
{
|
||||
string modeStr = IsBridge ? "Bridge" : "DIVER";
|
||||
uint substate = (uint)(Substate0 | (Substate1 << 8) | (Substate2 << 16));
|
||||
|
||||
string subStr = substate switch
|
||||
{
|
||||
0x00000000 => "Idle",
|
||||
0x0000000F => "Running",
|
||||
0x000000FF => "Error",
|
||||
0x00000001 => "Configured", // DIVER specific
|
||||
0x8000000F => "Running",
|
||||
0x800000FF => "Error",
|
||||
_ => $"Unknown (0x{substate:X6})",
|
||||
};
|
||||
|
||||
return $"{modeStr}: {subStr}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 抽象端口配置基类
|
||||
/// </summary>
|
||||
public abstract class PortConfig
|
||||
{
|
||||
/// <summary>端口类型(由子类实现)</summary>
|
||||
public abstract byte PortType { get; }
|
||||
|
||||
/// <summary>序列化端口配置为字节数组(供 P/Invoke 使用)</summary>
|
||||
/// <returns>返回固定长度字节数组(16 bytes)</returns>
|
||||
// M层MCU适配:将端口配置序列化为原生接口字节。
|
||||
public abstract byte[] ToBytes();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 串口配置
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 构造函数
|
||||
/// </remarks>
|
||||
/// <param name="baud">波特率</param>
|
||||
/// <param name="receiveFrameMs">接收帧间隔</param>
|
||||
// M层MCU适配:创建串口通信参数配置。
|
||||
public class SerialPortConfig(uint baud, uint receiveFrameMs) : PortConfig
|
||||
{
|
||||
/// <summary>Serial 类型</summary>
|
||||
public override byte PortType => 0x01;
|
||||
|
||||
/// <summary>波特率</summary>
|
||||
public uint Baud { get; set; } = baud;
|
||||
|
||||
/// <summary>接收帧间隔</summary>
|
||||
public uint ReceiveFrameMs { get; set; } = receiveFrameMs;
|
||||
|
||||
/// <summary>
|
||||
/// 转换为字节数组
|
||||
/// </summary>
|
||||
/// <returns>16 字节数组</returns>
|
||||
// M层MCU适配:序列化串口波特率和组帧时间。
|
||||
public override byte[] ToBytes()
|
||||
{
|
||||
var c = new PortStructHelper.SerialPortConfigC
|
||||
{
|
||||
port_type = PortType,
|
||||
baud = Baud,
|
||||
receive_frame_ms = ReceiveFrameMs,
|
||||
reserved = new byte[7],
|
||||
};
|
||||
return PortStructHelper.StructToBytes(c);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CAN 端口配置
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 构造函数
|
||||
/// </remarks>
|
||||
/// <param name="baud">波特率</param>
|
||||
/// <param name="retryTimeMs">重发间隔</param>
|
||||
// M层MCU适配:创建CAN通信参数配置。
|
||||
public class CANPortConfig(uint baud, uint retryTimeMs) : PortConfig
|
||||
{
|
||||
/// <summary>CAN 类型</summary>
|
||||
public override byte PortType => 0x02;
|
||||
|
||||
/// <summary>波特率</summary>
|
||||
public uint Baud { get; set; } = baud;
|
||||
|
||||
/// <summary>重发间隔</summary>
|
||||
public uint RetryTimeMs { get; set; } = retryTimeMs;
|
||||
|
||||
/// <summary>
|
||||
/// 转换为字节数组
|
||||
/// </summary>
|
||||
/// <returns>16 字节数组</returns>
|
||||
// M层MCU适配:序列化CAN波特率和重试时间。
|
||||
public override byte[] ToBytes()
|
||||
{
|
||||
var c = new PortStructHelper.CANPortConfigC
|
||||
{
|
||||
port_type = PortType,
|
||||
baud = Baud,
|
||||
retry_time_ms = RetryTimeMs,
|
||||
reserved = new byte[7],
|
||||
};
|
||||
return PortStructHelper.StructToBytes(c);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CAN 帧结构(标准帧 11-bit ID + 1-bit RTR + 4-bit DLC + Payload)
|
||||
/// </summary>
|
||||
public class CANMessage
|
||||
{
|
||||
/// <summary>标准帧 ID(0~0x7FF,11 位)</summary>
|
||||
public ushort ID { get; set; }
|
||||
|
||||
/// <summary>远程帧标志:false = 数据帧,true = 远程帧</summary>
|
||||
public bool RTR { get; set; }
|
||||
|
||||
/// <summary>数据长度码:0~8</summary>
|
||||
public byte DLC { get; set; }
|
||||
|
||||
/// <summary>数据负载,长度必须严格等于 DLC(DLC=0 时可为 null)</summary>
|
||||
public byte[] Payload { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 序列化为 MCU 协议字节流
|
||||
/// </summary>
|
||||
/// <returns>返回字节数组:2 bytes header + Payload</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">如果 DLC > 8</exception>
|
||||
/// <exception cref="ArgumentException">如果 Payload 长度 != DLC</exception>
|
||||
// M层CAN适配:将标准或扩展CAN帧序列化为原生布局。
|
||||
public byte[] ToBytes()
|
||||
{
|
||||
if (DLC > 8)
|
||||
throw new ArgumentOutOfRangeException(nameof(DLC), "DLC must be 0-8");
|
||||
if (DLC > 0 && (Payload == null || Payload.Length != DLC))
|
||||
throw new ArgumentException("Payload length must equal DLC");
|
||||
|
||||
// 构造 2 字节 header
|
||||
ushort header = 0;
|
||||
header |= (ushort)(ID & 0x7FF); // bits 0-10
|
||||
if (RTR)
|
||||
header |= (1 << 11); // bit 11
|
||||
header |= (ushort)((DLC & 0xF) << 12); // bits 12-15
|
||||
|
||||
byte[] result = new byte[2 + DLC];
|
||||
byte[] headerBytes = BitConverter.GetBytes(header); // 小端序
|
||||
result[0] = headerBytes[0];
|
||||
result[1] = headerBytes[1];
|
||||
|
||||
if (DLC > 0)
|
||||
Buffer.BlockCopy(Payload, 0, result, 2, DLC);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 反序列化 MCU 协议字节流为 CANMessage
|
||||
/// </summary>
|
||||
/// <param name="data">原始字节数组</param>
|
||||
/// <param name="length">实际有效长度</param>
|
||||
/// <returns>CANMessage 实例</returns>
|
||||
/// <exception cref="ArgumentException">数据长度错误</exception>
|
||||
// M层CAN适配:从原生缓冲区还原CAN消息。
|
||||
public static CANMessage FromBytes(byte[] data, uint length)
|
||||
{
|
||||
if (data == null || length > data.Length || length < 2)
|
||||
throw new ArgumentException("Data must be at least 2 bytes");
|
||||
|
||||
ushort header = BitConverter.ToUInt16(data, 0);
|
||||
|
||||
var msg = new CANMessage
|
||||
{
|
||||
ID = (ushort)(header & 0x7FF),
|
||||
RTR = (header & (1 << 11)) != 0,
|
||||
DLC = (byte)((header >> 12) & 0xF),
|
||||
};
|
||||
|
||||
if (msg.DLC > 0)
|
||||
{
|
||||
if (length < 2 + msg.DLC)
|
||||
throw new ArgumentException("Data length less than DLC");
|
||||
msg.Payload = new byte[msg.DLC];
|
||||
Buffer.BlockCopy(data, 2, msg.Payload, 0, msg.DLC);
|
||||
}
|
||||
else
|
||||
{
|
||||
msg.Payload = Array.Empty<byte>();
|
||||
}
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
// M层CAN诊断:格式化CAN标识符和数据内容。
|
||||
public override string ToString()
|
||||
{
|
||||
string payloadStr =
|
||||
(Payload == null || Payload.Length == 0)
|
||||
? "[]"
|
||||
: "0x[" + string.Join(" ", Payload.Select(b => $"{b:X2}")) + "]";
|
||||
|
||||
return $"CANMessage(ID=0x{ID:X3}, RTR={RTR}, DLC={DLC}, Payload={payloadStr})";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 内部 P/Invoke 层,直接映射 C DLL 函数
|
||||
/// </summary>
|
||||
internal static class MCUSerialBridgeCoreAPI
|
||||
{
|
||||
private const string DLL = @"mcu_serial_bridge.dll";
|
||||
|
||||
[DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
|
||||
// M层原生接口:打开MCU串口桥设备。
|
||||
internal static extern MCUSerialBridgeError msb_open(
|
||||
out IntPtr handle,
|
||||
[MarshalAs(UnmanagedType.LPStr)] string port,
|
||||
uint baud
|
||||
);
|
||||
|
||||
[DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
|
||||
// M层原生接口:关闭MCU串口桥设备。
|
||||
internal static extern MCUSerialBridgeError msb_close(IntPtr handle);
|
||||
|
||||
[DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
|
||||
// M层原生接口:复位MCU串口桥。
|
||||
internal static extern MCUSerialBridgeError msb_reset(IntPtr handle, uint timeout_ms);
|
||||
|
||||
[DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
|
||||
// M层原生接口:读取MCU固件版本。
|
||||
public static extern MCUSerialBridgeError msb_version(
|
||||
IntPtr handle,
|
||||
out VersionInfo version,
|
||||
uint timeout_ms
|
||||
);
|
||||
|
||||
[DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
|
||||
// M层原生接口:读取MCU当前运行状态。
|
||||
public static extern MCUSerialBridgeError mcu_state(
|
||||
IntPtr handle,
|
||||
out MCUState state,
|
||||
uint timeout
|
||||
);
|
||||
|
||||
[DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
|
||||
// M层原生接口:下发串口桥端口配置。
|
||||
internal static extern MCUSerialBridgeError msb_configure(
|
||||
IntPtr handle,
|
||||
uint num_ports,
|
||||
IntPtr ports,
|
||||
uint timeout
|
||||
);
|
||||
|
||||
[DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
|
||||
// M层原生接口:读取MCU数字输入。
|
||||
internal static extern MCUSerialBridgeError msb_read_input(
|
||||
IntPtr handle,
|
||||
[Out] byte[] inputs,
|
||||
uint timeout_ms
|
||||
);
|
||||
|
||||
[DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
|
||||
// M层原生接口:写入MCU数字输出。
|
||||
internal static extern MCUSerialBridgeError msb_write_output(
|
||||
IntPtr handle,
|
||||
[In] byte[] outputs,
|
||||
uint timeout_ms
|
||||
);
|
||||
|
||||
[DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
|
||||
// M层原生接口:从指定串口或CAN端口读取数据。
|
||||
internal static extern MCUSerialBridgeError msb_read_port(
|
||||
IntPtr handle,
|
||||
byte port_index,
|
||||
[Out] byte[] dst_data,
|
||||
uint dst_capacity,
|
||||
out uint out_length,
|
||||
uint timeout_ms
|
||||
);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
// M层硬件桥接:定义串口或CAN端口收到原生数据时的回调签名。
|
||||
internal delegate void msb_on_port_data_callback_function_t(
|
||||
IntPtr dst_data,
|
||||
uint dst_data_size,
|
||||
IntPtr user_ctx
|
||||
);
|
||||
|
||||
[DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
|
||||
// M层原生接口:注册端口数据到达回调。
|
||||
internal static extern MCUSerialBridgeError msb_register_port_data_callback(
|
||||
IntPtr handle,
|
||||
byte port_index,
|
||||
msb_on_port_data_callback_function_t callback,
|
||||
IntPtr user_ctx
|
||||
);
|
||||
|
||||
[DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
|
||||
// M层原生接口:向指定串口或CAN端口写入数据。
|
||||
internal static extern MCUSerialBridgeError msb_write_port(
|
||||
IntPtr handle,
|
||||
byte port_index,
|
||||
[In] byte[] src_data,
|
||||
uint src_data_len,
|
||||
uint timeout_ms
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MCU 串口/端口操作托管封装类
|
||||
/// 实现 IDisposable 管理底层句柄生命周期
|
||||
/// </summary>
|
||||
public class MCUSerialBridge : IDisposable
|
||||
{
|
||||
public static uint MaxPortNumber = 16;
|
||||
|
||||
private IntPtr nativeHandle = IntPtr.Zero;
|
||||
|
||||
/// <summary>判断是否已打开</summary>
|
||||
public bool IsOpen => nativeHandle != IntPtr.Zero;
|
||||
|
||||
/// <summary>构造函数,初始化对象</summary>
|
||||
// M层MCU适配:创建串口桥包装器并固定原生回调委托。
|
||||
public MCUSerialBridge()
|
||||
{
|
||||
nativeHandle = IntPtr.Zero;
|
||||
}
|
||||
|
||||
/// <summary>析构函数</summary>
|
||||
// M层MCU适配:对象回收时兜底释放原生串口桥句柄。
|
||||
~MCUSerialBridge()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
/// <summary>显式释放资源</summary>
|
||||
// M层MCU适配:释放串口桥句柄和非托管资源。
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>内部释放资源方法</summary>
|
||||
/// <param name="disposing">true 表示手动释放,false 表示析构释放</param>
|
||||
// M层MCU适配:按托管或终结路径关闭原生句柄。
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (nativeHandle != IntPtr.Zero)
|
||||
{
|
||||
MCUSerialBridgeCoreAPI.msb_close(nativeHandle);
|
||||
nativeHandle = IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>打开串口</summary>
|
||||
/// <param name="portName">串口名,如 "COM3"</param>
|
||||
/// <param name="baud">波特率</param>
|
||||
/// <returns>错误码</returns>
|
||||
// M层单车通信:按端口名和波特率连接MCU串口桥。
|
||||
public MCUSerialBridgeError Open(string portName, uint baud)
|
||||
{
|
||||
return MCUSerialBridgeCoreAPI.msb_open(out nativeHandle, portName, baud);
|
||||
}
|
||||
|
||||
/// <summary>关闭串口</summary>
|
||||
/// <returns>错误码</returns>
|
||||
// M层单车通信:关闭当前MCU串口桥连接。
|
||||
public MCUSerialBridgeError Close()
|
||||
{
|
||||
if (nativeHandle == IntPtr.Zero)
|
||||
return MCUSerialBridgeError.Win_HandleNotFound;
|
||||
|
||||
MCUSerialBridgeError error = MCUSerialBridgeCoreAPI.msb_close(nativeHandle);
|
||||
nativeHandle = IntPtr.Zero;
|
||||
return error;
|
||||
}
|
||||
|
||||
/// <summary>MCU 复位</summary>
|
||||
/// <returns>错误码</returns>
|
||||
// M层单车通信:请求MCU复位并等待结果。
|
||||
public MCUSerialBridgeError Reset(uint timeout = 200)
|
||||
{
|
||||
if (nativeHandle == IntPtr.Zero)
|
||||
return MCUSerialBridgeError.Win_HandleNotFound;
|
||||
|
||||
return MCUSerialBridgeCoreAPI.msb_reset(nativeHandle, timeout);
|
||||
}
|
||||
|
||||
/// <summary>获取固件版本</summary>
|
||||
/// <param name="version">输出版本信息</param>
|
||||
/// <param name="timeout">超时时间(ms)</param>
|
||||
/// <returns>错误码</returns>
|
||||
// M层MCU诊断:读取串口桥固件版本。
|
||||
public MCUSerialBridgeError GetVersion(out VersionInfo version, uint timeout = 200)
|
||||
{
|
||||
version = new VersionInfo();
|
||||
if (nativeHandle == IntPtr.Zero)
|
||||
return MCUSerialBridgeError.Win_HandleNotFound;
|
||||
|
||||
return MCUSerialBridgeCoreAPI.msb_version(nativeHandle, out version, timeout);
|
||||
}
|
||||
|
||||
/// <summary>获取 MCU 当前状态</summary>
|
||||
/// <param name="state">输出状态</param>
|
||||
/// <param name="timeout">超时时间(ms)</param>
|
||||
/// <returns>错误码</returns>
|
||||
// M层MCU诊断:读取串口桥运行状态。
|
||||
public MCUSerialBridgeError GetState(out MCUState state, uint timeout = 200)
|
||||
{
|
||||
state = new MCUState();
|
||||
if (nativeHandle == IntPtr.Zero)
|
||||
return MCUSerialBridgeError.Win_HandleNotFound;
|
||||
|
||||
return MCUSerialBridgeCoreAPI.mcu_state(nativeHandle, out state, timeout);
|
||||
}
|
||||
|
||||
/// <summary>配置端口</summary>
|
||||
/// <param name="ports">端口集合</param>
|
||||
/// <param name="timeout">超时时间(ms)</param>
|
||||
/// <returns>错误码</returns>
|
||||
// M层MCU适配:批量配置CAN和串口通道参数。
|
||||
public MCUSerialBridgeError Configure(IEnumerable<PortConfig> ports, uint timeout = 200)
|
||||
{
|
||||
if (nativeHandle == IntPtr.Zero)
|
||||
return MCUSerialBridgeError.Win_HandleNotFound;
|
||||
|
||||
if (ports == null)
|
||||
return MCUSerialBridgeError.Win_InvalidParam;
|
||||
|
||||
// 转换成数组
|
||||
PortConfig[] portArray = ports as PortConfig[] ?? ports.ToArray();
|
||||
int count = portArray.Length;
|
||||
|
||||
// 分配连续原生内存
|
||||
int structSize = 16; // 每个 PortConfig 固定 16 字节
|
||||
IntPtr nativePorts = Marshal.AllocHGlobal(structSize * count);
|
||||
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
byte[] bytes = portArray[i].ToBytes();
|
||||
if (bytes.Length != structSize)
|
||||
return MCUSerialBridgeError.Win_InvalidParam;
|
||||
|
||||
Marshal.Copy(bytes, 0, nativePorts + i * structSize, structSize);
|
||||
}
|
||||
|
||||
// 调用底层 API
|
||||
return MCUSerialBridgeCoreAPI.msb_configure(
|
||||
nativeHandle,
|
||||
(uint)count,
|
||||
nativePorts,
|
||||
timeout
|
||||
);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return MCUSerialBridgeError.Win_InvalidParam;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(nativePorts);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>读取输入(4 字节)</summary>
|
||||
/// <param name="inputs">输出数组</param>
|
||||
/// <param name="timeout">超时(ms)</param>
|
||||
/// <returns>错误码</returns>
|
||||
// M层单车IO:读取MCU数字输入状态。
|
||||
public MCUSerialBridgeError ReadInput(out byte[] inputs, uint timeout = 100)
|
||||
{
|
||||
inputs = new byte[4];
|
||||
if (nativeHandle == IntPtr.Zero)
|
||||
return MCUSerialBridgeError.Win_HandleNotFound;
|
||||
|
||||
return MCUSerialBridgeCoreAPI.msb_read_input(nativeHandle, inputs, timeout);
|
||||
}
|
||||
|
||||
/// <summary>写输出(4 字节)</summary>
|
||||
/// <param name="outputs">数据数组</param>
|
||||
/// <param name="timeout">超时(ms)</param>
|
||||
/// <returns>错误码</returns>
|
||||
// M层单车IO:写入继电器、灯光等数字输出状态。
|
||||
public MCUSerialBridgeError WriteOutput(byte[] outputs, uint timeout = 100)
|
||||
{
|
||||
if (nativeHandle == IntPtr.Zero)
|
||||
return MCUSerialBridgeError.Win_HandleNotFound;
|
||||
|
||||
return MCUSerialBridgeCoreAPI.msb_write_output(nativeHandle, outputs, timeout);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取 Serial 端口的一帧数据。
|
||||
/// Serial 上报的数据按帧入队;本接口每次调用最多读取一帧。
|
||||
/// 注意,如果不及时调用该接口,数据可能会丢失。
|
||||
/// </summary>
|
||||
/// <param name="portIndex">Serial 端口索引</param>
|
||||
/// <param name="buffer">接收到的数据</param>
|
||||
/// <param name="timeout">
|
||||
/// 超时时间(毫秒)
|
||||
/// - 0:不等待,有数据立即返回,没有数据立即返回 MSB_Error_NoData
|
||||
/// - >0:若当前无数据,最多等待 timeout,期间有新帧到达则立即返回
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// 如果已经注册回调,本函数将始终返回 MSB_Error_NoData
|
||||
/// </remarks>
|
||||
/// <returns>
|
||||
/// 错误码 MCUSerialBridgeError
|
||||
/// - OK 成功读取一帧
|
||||
/// - NoData 当前无可读数据(仅在 timeout == 0 或等待超时)
|
||||
/// - Win_InvalidParam 参数错误
|
||||
/// </returns>
|
||||
// M层串口通信:同步读取指定MCU串口的数据。
|
||||
public MCUSerialBridgeError ReadSerial(byte portIndex, out byte[] buffer, uint timeout)
|
||||
{
|
||||
buffer = Array.Empty<byte>();
|
||||
|
||||
if (nativeHandle == IntPtr.Zero)
|
||||
return MCUSerialBridgeError.Win_HandleNotFound;
|
||||
|
||||
const int MAX_PORT_FRAME = 2048;
|
||||
byte[] tmp = new byte[MAX_PORT_FRAME];
|
||||
|
||||
var err = MCUSerialBridgeCoreAPI.msb_read_port(
|
||||
nativeHandle,
|
||||
portIndex,
|
||||
tmp,
|
||||
(uint)tmp.Length,
|
||||
out uint outLen,
|
||||
timeout
|
||||
);
|
||||
|
||||
if (err != MCUSerialBridgeError.OK)
|
||||
return err;
|
||||
|
||||
buffer = new byte[outLen];
|
||||
Buffer.BlockCopy(tmp, 0, buffer, 0, (int)outLen);
|
||||
|
||||
return MCUSerialBridgeError.OK;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写 Serial 端口数据。
|
||||
/// 注意:对于单个Serial端口,不支持多线程并行发送,不要在上一条数据没有发送完成之前调用该函数,否则有可能导致数据错误。
|
||||
/// 注意:超时时间一定要大于波特率和数据长度综合得出的帧时间
|
||||
/// </summary>
|
||||
/// <param name="portIndex">Serial 端口索引</param>
|
||||
/// <param name="data">待发送数据</param>
|
||||
/// <param name="timeout">超时时间(毫秒)</param>
|
||||
/// <returns>
|
||||
/// 错误码 MCUSerialBridgeError
|
||||
/// - OK 成功发送
|
||||
/// - 其他错误请查看 MCUSerialBridgeError
|
||||
/// </returns>
|
||||
// M层串口通信:向指定MCU串口发送数据。
|
||||
public MCUSerialBridgeError WriteSerial(byte portIndex, byte[] data, uint timeout)
|
||||
{
|
||||
if (nativeHandle == IntPtr.Zero)
|
||||
return MCUSerialBridgeError.Win_HandleNotFound;
|
||||
|
||||
if (data == null || data.Length == 0)
|
||||
return MCUSerialBridgeError.Win_InvalidParam;
|
||||
|
||||
return MCUSerialBridgeCoreAPI.msb_write_port(
|
||||
nativeHandle,
|
||||
portIndex,
|
||||
data,
|
||||
(uint)data.Length,
|
||||
timeout
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取 CAN 端口的一帧数据。
|
||||
/// CAN 上报的数据按帧入队;本接口每次调用最多读取一帧。
|
||||
/// 注意,如果不及时调用该接口,数据可能会丢失。
|
||||
/// </summary>
|
||||
/// <param name="portIndex">CAN 端口索引</param>
|
||||
/// <param name="message">输出 CAN 消息对象</param>
|
||||
/// <param name="timeout">超时时间(毫秒)</param>
|
||||
/// <remarks>
|
||||
/// 如果已经注册回调,本函数将始终返回 MSB_Error_NoData
|
||||
/// </remarks>
|
||||
/// <returns>
|
||||
/// 错误码 MCUSerialBridgeError
|
||||
/// - OK 成功读取一帧
|
||||
/// - NoData 当前无可读数据(仅在 timeout == 0 或等待超时)
|
||||
/// - Win_InvalidParam 参数错误
|
||||
/// - CAN_DataError CAN数据错误
|
||||
/// - Win_HandleNotFound 句柄无效
|
||||
/// </returns>
|
||||
// M层CAN通信:同步读取指定CAN通道的一帧消息。
|
||||
public MCUSerialBridgeError ReadCAN(byte portIndex, out CANMessage message, uint timeout)
|
||||
{
|
||||
message = null;
|
||||
|
||||
if (nativeHandle == IntPtr.Zero)
|
||||
return MCUSerialBridgeError.Win_HandleNotFound;
|
||||
|
||||
const int MAX_FRAME = 16;
|
||||
byte[] tmp = new byte[MAX_FRAME];
|
||||
|
||||
var err = MCUSerialBridgeCoreAPI.msb_read_port(
|
||||
nativeHandle,
|
||||
portIndex,
|
||||
tmp,
|
||||
(uint)tmp.Length,
|
||||
out uint outLen,
|
||||
timeout
|
||||
);
|
||||
if (err != MCUSerialBridgeError.OK)
|
||||
return err;
|
||||
|
||||
try
|
||||
{
|
||||
message = CANMessage.FromBytes(tmp, outLen);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return MCUSerialBridgeError.CAN_DataError;
|
||||
}
|
||||
return MCUSerialBridgeError.OK;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写 CAN 端口数据。
|
||||
/// 注意:CAN 支持多线程发送,最多可同时发送 16 个消息。
|
||||
/// 但是,多个CAN消息会进入排队队列等待发送,如果消息太多,可能引起超时。
|
||||
/// </summary>
|
||||
/// <param name="portIndex">CAN 端口索引</param>
|
||||
/// <param name="message">待发送 CAN 消息对象</param>
|
||||
/// <param name="timeout">超时时间(毫秒),默认 500ms</param>
|
||||
/// <returns>
|
||||
/// 错误码 MCUSerialBridgeError
|
||||
/// - OK 成功发送
|
||||
/// - Win_InvalidParam 参数错误
|
||||
/// - CAN_DataError CAN 数据错误
|
||||
/// - Win_HandleNotFound 句柄无效
|
||||
/// </returns>
|
||||
// M层CAN通信:向指定CAN通道发送一帧消息。
|
||||
public MCUSerialBridgeError WriteCAN(byte portIndex, CANMessage message, uint timeout)
|
||||
{
|
||||
if (nativeHandle == IntPtr.Zero)
|
||||
return MCUSerialBridgeError.Win_HandleNotFound;
|
||||
if (message == null)
|
||||
return MCUSerialBridgeError.Win_InvalidParam;
|
||||
try
|
||||
{
|
||||
byte[] buffer = message.ToBytes();
|
||||
return MCUSerialBridgeCoreAPI.msb_write_port(
|
||||
nativeHandle,
|
||||
portIndex,
|
||||
buffer,
|
||||
(uint)buffer.Length,
|
||||
timeout
|
||||
);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return MCUSerialBridgeError.CAN_DataError;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly Dictionary<
|
||||
byte,
|
||||
MCUSerialBridgeCoreAPI.msb_on_port_data_callback_function_t
|
||||
> _portCallbacks = [];
|
||||
|
||||
/// <summary>
|
||||
/// 注册指定端口(Serial)的回调函数
|
||||
/// </summary>
|
||||
/// <param name="portIndex">端口索引</param>
|
||||
/// <param name="callback">接收数据回调,byte[] 为接收到的原始数据</param>
|
||||
/// <returns>错误码</returns>
|
||||
/// <remarks>
|
||||
/// 注意事项:
|
||||
/// 1. 回调会在底层 C 层线程中直接调用,请**不要在回调内阻塞**,例如等待 I/O 或 Sleep。
|
||||
/// 2. 回调内**不能调用 WriteSerial/WriteCAN 等发送函数**,否则可能导致死锁或丢帧。
|
||||
/// 3. 回调内只能做轻量级操作,例如简单解析、统计或打标记。
|
||||
/// 4. 若需要复杂处理(例如长时间解析、解码、存储数据库等),请**将数据入队到另一个线程**,再在后台处理。
|
||||
/// 5. 数据可能随时到来,请保证回调尽快返回,避免影响后续帧接收。
|
||||
/// 6. 不要把其他类型的端口注册到这个接口,接口不对 portIndex 做类型检查。
|
||||
/// </remarks>
|
||||
// M层串口通信:注册指定串口的异步接收回调。
|
||||
public MCUSerialBridgeError RegisterSerialPortCallback(
|
||||
byte portIndex,
|
||||
Action<byte[]> callback
|
||||
)
|
||||
{
|
||||
if (callback == null)
|
||||
return MCUSerialBridgeError.Win_InvalidParam;
|
||||
|
||||
if (portIndex > MaxPortNumber)
|
||||
return MCUSerialBridgeError.Config_PortNumOver;
|
||||
|
||||
// 包装 C# 回调为 P/Invoke 委托
|
||||
// M层串口回调:复制原生缓存并转交托管回调处理。
|
||||
void del(IntPtr dst_data, uint dst_data_size, IntPtr user_ctx)
|
||||
{
|
||||
byte[] data = new byte[dst_data_size];
|
||||
Marshal.Copy(dst_data, data, 0, (int)dst_data_size);
|
||||
callback(data);
|
||||
}
|
||||
|
||||
// 保存引用,防止 GC 回收
|
||||
_portCallbacks[portIndex] = del;
|
||||
|
||||
// 调用 C 层注册
|
||||
return MCUSerialBridgeCoreAPI.msb_register_port_data_callback(
|
||||
nativeHandle,
|
||||
portIndex,
|
||||
_portCallbacks[portIndex],
|
||||
IntPtr.Zero
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册指定端口(CAN)的回调函数
|
||||
/// </summary>
|
||||
/// <param name="portIndex">端口索引</param>
|
||||
/// <param name="callback">接收数据回调,CANMessage 为接收到的原始数据</param>
|
||||
/// <returns>错误码</returns>
|
||||
/// <remarks>
|
||||
/// 注意事项:
|
||||
/// 1. 回调会在底层 C 层线程中直接调用,请**不要在回调内阻塞**,例如等待 I/O 或 Sleep。
|
||||
/// 2. 回调内**不能调用 WriteSerial/WriteCAN 等发送函数**,否则可能导致死锁或丢帧。
|
||||
/// 3. 回调内只能做轻量级操作,例如简单解析、统计或打标记。
|
||||
/// 4. 若需要复杂处理(例如长时间解析、解码、存储数据库等),请**将数据入队到另一个线程**,再在后台处理。
|
||||
/// 5. 数据可能随时到来,请保证回调尽快返回,避免影响后续帧接收。
|
||||
/// 6. 不要把其他类型的端口注册到这个接口,接口不对 portIndex 做类型检查。
|
||||
/// </remarks>
|
||||
// M层CAN通信:注册指定CAN通道的异步接收回调。
|
||||
public MCUSerialBridgeError RegisterCANPortCallback(
|
||||
byte portIndex,
|
||||
Action<CANMessage> callback
|
||||
)
|
||||
{
|
||||
if (callback == null)
|
||||
return MCUSerialBridgeError.Win_InvalidParam;
|
||||
|
||||
if (portIndex > MaxPortNumber)
|
||||
return MCUSerialBridgeError.Config_PortNumOver;
|
||||
|
||||
// 包装 C# 回调为 P/Invoke 委托
|
||||
// M层CAN回调:还原原生CAN帧并转交托管回调处理。
|
||||
void del(IntPtr dst_data, uint dst_data_size, IntPtr user_ctx)
|
||||
{
|
||||
try
|
||||
{
|
||||
byte[] data = new byte[dst_data_size];
|
||||
Marshal.Copy(dst_data, data, 0, (int)dst_data_size);
|
||||
CANMessage msg = CANMessage.FromBytes(data, dst_data_size);
|
||||
callback(msg);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 解析失败直接忽略,保证回调不会抛异常阻塞 C 层线程
|
||||
}
|
||||
}
|
||||
|
||||
// 保存引用,防止 GC 回收
|
||||
_portCallbacks[portIndex] = del;
|
||||
|
||||
// 调用 C 层注册
|
||||
return MCUSerialBridgeCoreAPI.msb_register_port_data_callback(
|
||||
nativeHandle,
|
||||
portIndex,
|
||||
_portCallbacks[portIndex],
|
||||
IntPtr.Zero
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// MCU通信错误码
|
||||
|
||||
namespace MCUSerialBridgeCLR
|
||||
{
|
||||
public enum MCUSerialBridgeError : uint
|
||||
{
|
||||
OK = 0x00000000, // Success
|
||||
NoData = 0x00000001, // Port no new data
|
||||
Win_Unknown = 0x80000001, // Unknown Windows error
|
||||
Win_InvalidParam = 0x80000002, // Invalid parameter
|
||||
Win_AllocFail = 0x80000003, // Memory allocation failed
|
||||
Win_HandleNotFound = 0x80000004, // Handle not found
|
||||
Win_ResourceBusy = 0x80000005, // Resource busy
|
||||
Win_BufferFull = 0x80000006, // Buffer is full
|
||||
Win_UserBufferTooSmall = 0x80000007, // Buffer is too small
|
||||
Win_CannotOpenPort = 0x80000010, // Cannot open port
|
||||
Win_CannotGetCommState = 0x80000011, // Cannot get comm state
|
||||
Win_CannotSetCommState = 0x80000012, // Cannot set comm state
|
||||
Win_CannotCreateThread = 0x80000013, // Cannot create thread
|
||||
Proto_Invalid = 0xE0000001, // Protocol invalid
|
||||
Proto_Checksum = 0xE0000002, // CRC check failed
|
||||
Proto_Timeout = 0xE0000003, // Protocol timeout
|
||||
Proto_FrameTooLong = 0xE0000004, // Frame too long
|
||||
Proto_UnknownCommand = 0xE0000005, // Unknown command
|
||||
Proto_InvalidPayload = 0xE0000006, // Invalid payload
|
||||
State_NotRunning = 0xF0000000, // Not running, configure
|
||||
State_Running = 0xF0000001, // Can not configure, already running
|
||||
Config_PortNumOver = 0xC0000000, // Port number over
|
||||
Config_SerialNumOver = 0xC0000001, // Serial port number over
|
||||
Config_CANNumOver = 0xC0000002, // CAN number over
|
||||
Config_UnknownPortType = 0xC0000010, // Unknown Port Type
|
||||
Serial_OpenFail = 0x01000001, // Serial open failed
|
||||
Serial_NotOpen = 0x01000002, // Serial not open
|
||||
Serial_ReadFail = 0x01000003, // Serial read failed
|
||||
Serial_WriteFail = 0x01000004, // Serial write failed
|
||||
Serial_Busy = 0x01000005, // Serial is busy
|
||||
CAN_DataError = 0x02000000, // CAN data error
|
||||
CAN_SendFail = 0x02000001, // CAN send failed
|
||||
CAN_RecvFail = 0x02000002, // CAN receive failed
|
||||
CAN_BufferFull = 0x02000003, // CAN buffer full
|
||||
CAN_NotInit = 0x02000004, // CAN not initialized
|
||||
Port_WriteBusy = 0x10000001, // Port is busy now
|
||||
MCU_Unknown = 0x00010001, // MCU unknown error
|
||||
MCU_IOSizeError = 0x00010002, // IO Should be 4 bytes
|
||||
MCU_OverTemperature = 0x00010010, // MCU over temperature
|
||||
}
|
||||
|
||||
public static class MCUSerialBridgeErrorExtensions
|
||||
{
|
||||
// M层MCU适配:把串口桥错误码转换为便于诊断的说明。
|
||||
public static string ToDescription(this MCUSerialBridgeError err)
|
||||
{
|
||||
return err switch
|
||||
{
|
||||
MCUSerialBridgeError.OK => "OK|Success",
|
||||
MCUSerialBridgeError.NoData => "NoData|Port no new data",
|
||||
MCUSerialBridgeError.Win_Unknown => "Win_Unknown|Unknown Windows error",
|
||||
MCUSerialBridgeError.Win_InvalidParam => "Win_InvalidParam|Invalid parameter",
|
||||
MCUSerialBridgeError.Win_AllocFail => "Win_AllocFail|Memory allocation failed",
|
||||
MCUSerialBridgeError.Win_HandleNotFound => "Win_HandleNotFound|Handle not found",
|
||||
MCUSerialBridgeError.Win_ResourceBusy => "Win_ResourceBusy|Resource busy",
|
||||
MCUSerialBridgeError.Win_BufferFull => "Win_BufferFull|Buffer is full",
|
||||
MCUSerialBridgeError.Win_UserBufferTooSmall => "Win_UserBufferTooSmall|Buffer is too small",
|
||||
MCUSerialBridgeError.Win_CannotOpenPort => "Win_CannotOpenPort|Cannot open port",
|
||||
MCUSerialBridgeError.Win_CannotGetCommState => "Win_CannotGetCommState|Cannot get comm state",
|
||||
MCUSerialBridgeError.Win_CannotSetCommState => "Win_CannotSetCommState|Cannot set comm state",
|
||||
MCUSerialBridgeError.Win_CannotCreateThread => "Win_CannotCreateThread|Cannot create thread",
|
||||
MCUSerialBridgeError.Proto_Invalid => "Proto_Invalid|Protocol invalid",
|
||||
MCUSerialBridgeError.Proto_Checksum => "Proto_Checksum|CRC check failed",
|
||||
MCUSerialBridgeError.Proto_Timeout => "Proto_Timeout|Protocol timeout",
|
||||
MCUSerialBridgeError.Proto_FrameTooLong => "Proto_FrameTooLong|Frame too long",
|
||||
MCUSerialBridgeError.Proto_UnknownCommand => "Proto_UnknownCommand|Unknown command",
|
||||
MCUSerialBridgeError.Proto_InvalidPayload => "Proto_InvalidPayload|Invalid payload",
|
||||
MCUSerialBridgeError.State_NotRunning => "State_NotRunning|Not running, configure",
|
||||
MCUSerialBridgeError.State_Running => "State_Running|Can not configure, already running",
|
||||
MCUSerialBridgeError.Config_PortNumOver => "Config_PortNumOver|Port number over",
|
||||
MCUSerialBridgeError.Config_SerialNumOver => "Config_SerialNumOver|Serial port number over",
|
||||
MCUSerialBridgeError.Config_CANNumOver => "Config_CANNumOver|CAN number over",
|
||||
MCUSerialBridgeError.Config_UnknownPortType => "Config_UnknownPortType|Unknown Port Type",
|
||||
MCUSerialBridgeError.Serial_OpenFail => "Serial_OpenFail|Serial open failed",
|
||||
MCUSerialBridgeError.Serial_NotOpen => "Serial_NotOpen|Serial not open",
|
||||
MCUSerialBridgeError.Serial_ReadFail => "Serial_ReadFail|Serial read failed",
|
||||
MCUSerialBridgeError.Serial_WriteFail => "Serial_WriteFail|Serial write failed",
|
||||
MCUSerialBridgeError.Serial_Busy => "Serial_Busy|Serial is busy",
|
||||
MCUSerialBridgeError.CAN_DataError => "CAN_DataError|CAN data error",
|
||||
MCUSerialBridgeError.CAN_SendFail => "CAN_SendFail|CAN send failed",
|
||||
MCUSerialBridgeError.CAN_RecvFail => "CAN_RecvFail|CAN receive failed",
|
||||
MCUSerialBridgeError.CAN_BufferFull => "CAN_BufferFull|CAN buffer full",
|
||||
MCUSerialBridgeError.CAN_NotInit => "CAN_NotInit|CAN not initialized",
|
||||
MCUSerialBridgeError.Port_WriteBusy => "Port_WriteBusy|Port is busy now",
|
||||
MCUSerialBridgeError.MCU_Unknown => "MCU_Unknown|MCU unknown error",
|
||||
MCUSerialBridgeError.MCU_IOSizeError => "MCU_IOSizeError|IO Should be 4 bytes",
|
||||
MCUSerialBridgeError.MCU_OverTemperature => "MCU_OverTemperature|MCU over temperature",
|
||||
_ => "Unknown Error",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
<Platforms>AnyCPU</Platforms>
|
||||
|
||||
<AssemblyName>MedullaAdapter</AssemblyName>
|
||||
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<OutputPath>build\Medulla\plugins\</OutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="CartActivator">
|
||||
<HintPath>ref\RefCartActivator.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="MedullaCore">
|
||||
<HintPath>ref\RefMedullaCore.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
|
||||
<Reference Include="FundamentalLib">
|
||||
<HintPath>ref\RefFundamentalLib.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
|
||||
<Reference Include="MDCSToolBox">
|
||||
<HintPath>ref\MDCSToolBox.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
|
||||
<!-- <Reference Include="CycleGUI">
|
||||
<HintPath>ref\CycleGUI.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference> -->
|
||||
<Reference Include="CommonUsage">
|
||||
<HintPath>..\ref\CommonUsage.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\Shared\Models\ChassisCommand.cs"
|
||||
Link="Shared\Models\ChassisCommand.cs" />
|
||||
|
||||
<Compile Include="..\Shared\Mathematics\FrameTransform2D.cs"
|
||||
Link="Shared\Mathematics\FrameTransform2D.cs" />
|
||||
|
||||
<Compile Include="..\Shared\Mathematics\AngleMath.cs"
|
||||
Link="Shared\Mathematics\AngleMath.cs" />
|
||||
|
||||
<Compile Include="..\Shared\Chassis\MultiWheelChassisAdapter.cs"
|
||||
Link="Shared\Chassis\MultiWheelChassisAdapter.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,364 @@
|
||||
// 计算8个驱动电机的目标速度和舵角PID
|
||||
using CartActivator;
|
||||
using FundamentalLib;
|
||||
using MDCSToolBox.Commons;
|
||||
using System;
|
||||
using static MDCSToolBox.Medulla.Chassis.BasicCartDefinition;
|
||||
|
||||
namespace MedullaAdapter
|
||||
{
|
||||
public class MotorRoutine : LadderLogic<DiverCartDefinition>
|
||||
{
|
||||
private bool _wasTransmitterControlling;
|
||||
private DateTime _lastMoveTime = DateTime.Now;
|
||||
public override void Operation(int iteration)
|
||||
{
|
||||
if (!cart.GhostMode && cart.State == -1) return;
|
||||
// SA稳定打开后,使能实体遥控器。
|
||||
TriggerOnce(
|
||||
cart.TransmitterConnected && cart.Transmitter_SA,
|
||||
300,
|
||||
() =>
|
||||
{
|
||||
cart.TransmitterControlEnable = true;
|
||||
cart.TransmitterLastTime = DateTime.Now;
|
||||
});
|
||||
// 遥控器断连或SA关闭时,立即撤销遥控使能。
|
||||
if (!cart.TransmitterConnected || !cart.Transmitter_SA)
|
||||
cart.TransmitterControlEnable = false;
|
||||
var transmitterSelected =
|
||||
cart.TransmitterConnected &&
|
||||
cart.TransmitterControlEnable &&
|
||||
cart.Transmitter_SA &&
|
||||
cart.Transmitter_SC == cart.CarNum;
|
||||
var transmitterControlling =
|
||||
transmitterSelected &&
|
||||
CartDefinition.testPriority(5, "TransmitterMode");
|
||||
if (transmitterControlling)
|
||||
{
|
||||
TransmitterChassisControl();
|
||||
cart.TransmitterLastTime = DateTime.Now;
|
||||
cart.CarStatu = "实体遥控器控制";
|
||||
}
|
||||
else
|
||||
{
|
||||
// 只在遥控器刚刚退出时发送一次停车,
|
||||
// 不能每周期停车,否则会覆盖C层轨迹控制。
|
||||
if (_wasTransmitterControlling)
|
||||
{
|
||||
cart.ManualControl(
|
||||
cart.TransmitterControlMode,
|
||||
0, 0, 0,
|
||||
cart.TransmitterSpeed,
|
||||
DateTime.Now - cart.TransmitterLastTime);
|
||||
|
||||
StopClampArms();
|
||||
cart.TransmitterLastTime = DateTime.Now;
|
||||
}
|
||||
|
||||
cart.CarStatu = "正常运行";
|
||||
}
|
||||
_wasTransmitterControlling = transmitterControlling;
|
||||
// 当前是否由C层控制。
|
||||
cart.ClumsyControl = CartDefinition.currentPriority == 0;
|
||||
// 计算四个舵轮PID和8个驱动电机最终速度。
|
||||
UpdateDiffSteerWheelSpeeds();
|
||||
// 平滑更新硬件速度限制。
|
||||
UpdateSendSpeedLimit();
|
||||
// 更新红黄绿灯状态。
|
||||
UpdateLightMode();
|
||||
}
|
||||
// 物理遥控器设置
|
||||
public void TransmitterChassisControl()
|
||||
{
|
||||
var interval = DateTime.Now - cart.TransmitterLastTime;
|
||||
switch (cart.Transmitter_SB)
|
||||
{
|
||||
case TransmitterState.Mode0:
|
||||
cart.TransmitterControlMode =
|
||||
DiverCartDefinition.ManualControlMode.Normal;
|
||||
break;
|
||||
case TransmitterState.Mode1:
|
||||
cart.TransmitterControlMode =
|
||||
DiverCartDefinition.ManualControlMode.Crab;
|
||||
break;
|
||||
case TransmitterState.Mode2:
|
||||
cart.TransmitterControlMode =
|
||||
DiverCartDefinition.ManualControlMode.Spin;
|
||||
break;
|
||||
default:
|
||||
cart.ManualControl(
|
||||
cart.TransmitterControlMode,
|
||||
0, 0, 0,
|
||||
cart.TransmitterSpeed,
|
||||
interval);
|
||||
StopClampArms();
|
||||
return;
|
||||
}
|
||||
// SA关闭后立即停车。
|
||||
if (!cart.Transmitter_SA)
|
||||
{
|
||||
cart.ManualControl(
|
||||
cart.TransmitterControlMode,
|
||||
0, 0, 0,
|
||||
cart.TransmitterSpeed,
|
||||
interval);
|
||||
StopClampArms();
|
||||
return;
|
||||
}
|
||||
// 限制实体遥控器的最大速度。
|
||||
cart.TransmitterSpeed = Math.Max(
|
||||
cart.TransmitterSpeedLowerLimit,
|
||||
Math.Min(
|
||||
cart.TransmitterSpeed,
|
||||
cart.TransmitterSpeedUpperLimit));
|
||||
// SD的Mode0作为底盘驾驶档。
|
||||
if (cart.Transmitter_SD == TransmitterState.Mode0)
|
||||
{
|
||||
// 底盘驾驶档不允许保留上一周期的夹臂速度。
|
||||
StopClampArms();
|
||||
|
||||
cart.ManualControl(
|
||||
cart.TransmitterControlMode,
|
||||
cart.TransmitterLeftJoystickValX,
|
||||
cart.TransmitterRightJoystickValY,
|
||||
0,
|
||||
cart.TransmitterSpeed,
|
||||
interval);
|
||||
|
||||
return;
|
||||
}
|
||||
if (cart.Transmitter_SD == TransmitterState.Mode1)
|
||||
{
|
||||
// 切换到夹臂档时,先确保底盘停止。
|
||||
cart.ManualControl(
|
||||
cart.TransmitterControlMode,
|
||||
0, 0, 0,
|
||||
cart.TransmitterSpeed,
|
||||
interval);
|
||||
|
||||
var armSpeed =
|
||||
cart.TransmitterRightJoystickValX *
|
||||
cart.ManualArmSpeedFac;
|
||||
|
||||
cart.SpeedLeftArm = armSpeed;
|
||||
cart.SpeedRightArm = armSpeed;
|
||||
return;
|
||||
}
|
||||
// 非驾驶档必须主动停车,防止上一条运动指令残留。
|
||||
cart.ManualControl(
|
||||
cart.TransmitterControlMode,
|
||||
0, 0, 0,
|
||||
cart.TransmitterSpeed,
|
||||
interval);
|
||||
StopClampArms();
|
||||
}
|
||||
|
||||
// M层单车夹臂安全:清除物理遥控器留下的左右夹臂速度命令。
|
||||
private void StopClampArms()
|
||||
{
|
||||
cart.SpeedLeftArm = 0;
|
||||
cart.SpeedRightArm = 0;
|
||||
}
|
||||
|
||||
// M层单车底盘:根据四个舵轮的目标角度和实际角度修正8个驱动电机速度。
|
||||
private void UpdateDiffSteerWheelSpeeds()
|
||||
{
|
||||
if (cart.LeftFrontPid == null ||
|
||||
cart.LeftRearPid == null ||
|
||||
cart.RightFrontPid == null ||
|
||||
cart.RightRearPid == null)
|
||||
{
|
||||
cart.SpeedLFL = 0;
|
||||
cart.SpeedLFR = 0;
|
||||
cart.SpeedRFL = 0;
|
||||
cart.SpeedRFR = 0;
|
||||
cart.SpeedLRL = 0;
|
||||
cart.SpeedLRR = 0;
|
||||
cart.SpeedRRL = 0;
|
||||
cart.SpeedRRR = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新左前舵轮PID参数。
|
||||
cart.LeftFrontPid.ChangeParameters(
|
||||
cart.DiffSteerKp,
|
||||
cart.DiffSteerKi,
|
||||
cart.DiffSteerKd,
|
||||
cart.DiffSteerMaxI,
|
||||
cart.DiffSteerDeadZone,
|
||||
cart.DiffSteerThresh,
|
||||
cart.DiffSteerSpeedAcc);
|
||||
|
||||
// 更新左后舵轮PID参数。
|
||||
cart.LeftRearPid.ChangeParameters(
|
||||
cart.DiffSteerKp,
|
||||
cart.DiffSteerKi,
|
||||
cart.DiffSteerKd,
|
||||
cart.DiffSteerMaxI,
|
||||
cart.DiffSteerDeadZone,
|
||||
cart.DiffSteerThresh,
|
||||
cart.DiffSteerSpeedAcc);
|
||||
|
||||
// 更新右前舵轮PID参数。
|
||||
cart.RightFrontPid.ChangeParameters(
|
||||
cart.DiffSteerKp,
|
||||
cart.DiffSteerKi,
|
||||
cart.DiffSteerKd,
|
||||
cart.DiffSteerMaxI,
|
||||
cart.DiffSteerDeadZone,
|
||||
cart.DiffSteerThresh,
|
||||
cart.DiffSteerSpeedAcc);
|
||||
|
||||
// 更新右后舵轮PID参数。
|
||||
cart.RightRearPid.ChangeParameters(
|
||||
cart.DiffSteerKp,
|
||||
cart.DiffSteerKi,
|
||||
cart.DiffSteerKd,
|
||||
cart.DiffSteerMaxI,
|
||||
cart.DiffSteerDeadZone,
|
||||
cart.DiffSteerThresh,
|
||||
cart.DiffSteerSpeedAcc);
|
||||
|
||||
// 根据实际舵角计算四条腿的差速修正量。
|
||||
var diffLf = cart.LeftFrontPid.GetResponse(
|
||||
cart.ThLeftFront, false, false, "LF");
|
||||
|
||||
var diffLr = cart.LeftRearPid.GetResponse(
|
||||
cart.ThLeftRear, false, false, "LR");
|
||||
|
||||
var diffRf = cart.RightFrontPid.GetResponse(
|
||||
cart.ThRightFront, false, false, "RF");
|
||||
|
||||
var diffRr = cart.RightRearPid.GetResponse(
|
||||
cart.ThRightRear, false, false, "RR");
|
||||
|
||||
// 保存四个转向PID的本周期修正量,供M层监控和舵轮响应CSV记录使用。
|
||||
cart.DiffSteerOutputLeftFront = diffLf;
|
||||
cart.DiffSteerOutputLeftRear = diffLr;
|
||||
cart.DiffSteerOutputRightFront = diffRf;
|
||||
cart.DiffSteerOutputRightRear = diffRr;
|
||||
|
||||
// 左前腿:左右电机施加方向相反的PID修正量。
|
||||
cart.SpeedLFL = cart.SpeedLeftFrontLeft - diffLf;
|
||||
cart.SpeedLFR = cart.SpeedLeftFrontRight + diffLf;
|
||||
|
||||
// 左后腿。
|
||||
cart.SpeedLRL = cart.SpeedLeftRearLeft - diffLr;
|
||||
cart.SpeedLRR = cart.SpeedLeftRearRight + diffLr;
|
||||
|
||||
// 右前腿。
|
||||
cart.SpeedRFL = cart.SpeedRightFrontLeft - diffRf;
|
||||
cart.SpeedRFR = cart.SpeedRightFrontRight + diffRf;
|
||||
|
||||
// 右后腿。
|
||||
cart.SpeedRRL = cart.SpeedRightRearLeft - diffRr;
|
||||
cart.SpeedRRR = cart.SpeedRightRearRight + diffRr;
|
||||
}
|
||||
|
||||
// M层单车限速:按照加速度和减速度平滑更新实际下发速度上限。
|
||||
private void UpdateSendSpeedLimit()
|
||||
{
|
||||
if (cart.Chassis == null)
|
||||
{
|
||||
cart.SendThresSpeed = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
var now = DateTime.Now;
|
||||
var elapsedSeconds = (float)(now - _lastMoveTime).TotalSeconds;
|
||||
_lastMoveTime = now;
|
||||
|
||||
// 防止调试暂停或线程卡顿后,一次产生过大的速度跳变。
|
||||
elapsedSeconds = Math.Clamp(elapsedSeconds, 0f, 0.2f);
|
||||
|
||||
// 限速值不允许小于零。
|
||||
var targetLimit = Math.Max(0f, cart.ThresSpeed);
|
||||
var currentLimit = Math.Max(0f, cart.SendThresSpeed);
|
||||
|
||||
// 增大速度上限时用加速度,减小时用减速度。
|
||||
var speedChangingRate =
|
||||
targetLimit > currentLimit
|
||||
? cart.Chassis.AccPerSecond
|
||||
: cart.Chassis.DeAccPerSecond;
|
||||
|
||||
speedChangingRate = Math.Max(0f, speedChangingRate);
|
||||
|
||||
var maxChange = speedChangingRate * elapsedSeconds;
|
||||
var speedDifference = targetLimit - currentLimit;
|
||||
|
||||
if (Math.Abs(speedDifference) <= maxChange)
|
||||
{
|
||||
currentLimit = targetLimit;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentLimit += Math.Sign(speedDifference) * maxChange;
|
||||
}
|
||||
|
||||
// 计算当前8个电机目标速度中的最大绝对值。
|
||||
var wheelMaxSpeed = 0f;
|
||||
|
||||
wheelMaxSpeed = Math.Max(wheelMaxSpeed, Math.Abs(cart.SpeedLFL));
|
||||
wheelMaxSpeed = Math.Max(wheelMaxSpeed, Math.Abs(cart.SpeedLFR));
|
||||
wheelMaxSpeed = Math.Max(wheelMaxSpeed, Math.Abs(cart.SpeedRFL));
|
||||
wheelMaxSpeed = Math.Max(wheelMaxSpeed, Math.Abs(cart.SpeedRFR));
|
||||
wheelMaxSpeed = Math.Max(wheelMaxSpeed, Math.Abs(cart.SpeedLRL));
|
||||
wheelMaxSpeed = Math.Max(wheelMaxSpeed, Math.Abs(cart.SpeedLRR));
|
||||
wheelMaxSpeed = Math.Max(wheelMaxSpeed, Math.Abs(cart.SpeedRRL));
|
||||
wheelMaxSpeed = Math.Max(wheelMaxSpeed, Math.Abs(cart.SpeedRRR));
|
||||
|
||||
// 没有必要让平滑限速值高于当前所有车轮需要的速度。
|
||||
if (targetLimit < wheelMaxSpeed &&
|
||||
currentLimit > wheelMaxSpeed)
|
||||
{
|
||||
currentLimit = wheelMaxSpeed;
|
||||
}
|
||||
|
||||
cart.SendThresSpeed = currentLimit;
|
||||
}
|
||||
|
||||
// M层单车灯光:根据报警、驱动器、限速和电量状态生成红黄绿灯模式。
|
||||
private void UpdateLightMode()
|
||||
{
|
||||
// 二级报警:红灯常亮。
|
||||
if (cart.AlarmLevel == 2)
|
||||
{
|
||||
cart.LightMode = 2;
|
||||
return;
|
||||
}
|
||||
|
||||
// 一级报警:黄灯常亮。
|
||||
if (cart.AlarmLevel == 1)
|
||||
{
|
||||
cart.LightMode = 3;
|
||||
return;
|
||||
}
|
||||
|
||||
// 驱动轮未使能:黄灯闪烁。
|
||||
if (!cart.WheelAbleState)
|
||||
{
|
||||
FlipFlop(ref cart.LightMode, 500, 0, 3);
|
||||
return;
|
||||
}
|
||||
|
||||
// C层正在进行限速:黄灯常亮。
|
||||
if (Math.Abs(cart.ThresSpeed - 1f) > 0.001f)
|
||||
{
|
||||
cart.LightMode = 3;
|
||||
return;
|
||||
}
|
||||
|
||||
// 电量低于预警值:黄灯常亮。
|
||||
if (cart.Soc <= cart.LowBatteryAlarmThreshold)
|
||||
{
|
||||
cart.LightMode = 3;
|
||||
return;
|
||||
}
|
||||
|
||||
// 正常运行:绿灯闪烁。
|
||||
FlipFlop(ref cart.LightMode, 500, 0, 1);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// Medulla虚拟遥控器和夹臂控制
|
||||
using MDCSToolBox.Medulla.Chassis.MultiWheel;
|
||||
using CartActivator;
|
||||
|
||||
namespace MedullaAdapter
|
||||
{
|
||||
// M层单车虚拟遥控器:使用父类提供的底盘控制界面。
|
||||
public class Remote : MultiWheelRemote<DiverCartDefinition>
|
||||
{
|
||||
// 将父类虚拟遥控器界面输入统一转发到本车的ManualControl。
|
||||
public override void ChassisOperation()
|
||||
{
|
||||
if (MultiVehicleMode.on)
|
||||
{
|
||||
MultiVehicleModeChassisLogic();
|
||||
statusText = "单车版本不支持多车联动遥控";
|
||||
return;
|
||||
}
|
||||
|
||||
// 当前单车适配层只定义Normal、Crab和Spin三种模式。
|
||||
// 禁止这些旧按钮绕过适配层直接修改底盘坐标偏置。
|
||||
if (AckermannMode.on ||
|
||||
SwayMode.on ||
|
||||
XYThMode.on)
|
||||
{
|
||||
cart.Chassis?.PredefinedDriveStop();
|
||||
statusText = "当前单车版本暂不支持阿克曼、斜行或全向模式";
|
||||
return;
|
||||
}
|
||||
|
||||
var mode = SpinMode.on
|
||||
? DiverCartDefinition.ManualControlMode.Spin
|
||||
: CrabMode.on
|
||||
? DiverCartDefinition.ManualControlMode.Crab
|
||||
: DiverCartDefinition.ManualControlMode.Normal;
|
||||
|
||||
cart.ManualControl(
|
||||
mode,
|
||||
SpeedPad.x,
|
||||
SpeedPad.y,
|
||||
FrontDirection.dval * 180,
|
||||
SpeedThreshold.val);
|
||||
|
||||
statusText =
|
||||
$"{mode}, x={SpeedPad.x:0.00}, " +
|
||||
$"y={SpeedPad.y:0.00}, " +
|
||||
$"speed={SpeedThreshold.val:0.00}";
|
||||
}
|
||||
[AsControlItem(name = "夹抱速度", LayoutRow = 0, LayoutCol = 4)]
|
||||
public Throttle ArmSpeed;
|
||||
|
||||
[AsControlItem(name = "夹抱打开", LayoutRow = 1, LayoutCol = 0)]
|
||||
public Button Open;
|
||||
|
||||
[AsControlItem(name = "夹抱关闭", LayoutRow = 1, LayoutCol = 2)]
|
||||
public Button Close;
|
||||
// M层虚拟遥控器:控制左右夹臂同步打开或关闭。
|
||||
public override void CustomOperation()
|
||||
{
|
||||
if (Open.pressed)
|
||||
{
|
||||
cart.SpeedLeftArm =
|
||||
-ArmSpeed.val * cart.ManualArmSpeedFac;
|
||||
cart.SpeedRightArm =
|
||||
-ArmSpeed.val * cart.ManualArmSpeedFac;
|
||||
}
|
||||
else if (Close.pressed)
|
||||
{
|
||||
cart.SpeedLeftArm =
|
||||
ArmSpeed.val * cart.ManualArmSpeedFac;
|
||||
cart.SpeedRightArm =
|
||||
ArmSpeed.val * cart.ManualArmSpeedFac;
|
||||
}
|
||||
else
|
||||
{
|
||||
cart.SpeedLeftArm = 0;
|
||||
cart.SpeedRightArm = 0;
|
||||
}
|
||||
}
|
||||
// 单车不支持多车联动,误打开多车开关时主动停车。
|
||||
public override void MultiVehicleModeChassisLogic()
|
||||
{
|
||||
cart.Chassis?.PredefinedDriveStop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace MedullaAdapter
|
||||
{
|
||||
/// <summary>
|
||||
/// 在后台保存驱动器CAN速度事件和底盘周期快照,避免文件IO阻塞CAN回调。
|
||||
/// </summary>
|
||||
internal sealed class WheelSpeedDiagnosticLogger : IDisposable
|
||||
{
|
||||
private readonly struct LogRecord
|
||||
{
|
||||
public LogRecord(bool isCanEvent, string line)
|
||||
{
|
||||
IsCanEvent = isCanEvent;
|
||||
Line = line;
|
||||
}
|
||||
|
||||
public bool IsCanEvent { get; }
|
||||
|
||||
public string Line { get; }
|
||||
}
|
||||
|
||||
private const int MaximumQueuedRecords = 100000;
|
||||
private const double SnapshotIntervalMilliseconds = 20.0;
|
||||
private readonly ConcurrentQueue<LogRecord> _records = new();
|
||||
private readonly AutoResetEvent _recordsAvailable = new(false);
|
||||
private readonly object _lifecycleLock = new();
|
||||
private Stopwatch _stopwatch;
|
||||
private Thread _writerThread;
|
||||
private StreamWriter _canWriter;
|
||||
private StreamWriter _snapshotWriter;
|
||||
private volatile bool _isRunning;
|
||||
private int _queuedRecordCount;
|
||||
private long _receiveSequence;
|
||||
private long _droppedRecordCount;
|
||||
private double _lastSnapshotMilliseconds = double.NegativeInfinity;
|
||||
|
||||
public bool IsRunning => _isRunning;
|
||||
|
||||
public string CanLogPath { get; private set; } = "";
|
||||
|
||||
public string SnapshotLogPath { get; private set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 创建本次诊断的两个CSV文件并启动后台写入线程。
|
||||
/// </summary>
|
||||
public void Start(string directory, int carNumber)
|
||||
{
|
||||
lock (_lifecycleLock)
|
||||
{
|
||||
if (_isRunning)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(directory))
|
||||
throw new ArgumentException(
|
||||
"轮速诊断目录不能为空。",
|
||||
nameof(directory));
|
||||
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
var filePrefix =
|
||||
$"{DateTime.Now:yyyyMMdd_HHmmss_fff}_Car{carNumber}";
|
||||
|
||||
CanLogPath = Path.Combine(
|
||||
directory,
|
||||
$"{filePrefix}_can.csv");
|
||||
|
||||
SnapshotLogPath = Path.Combine(
|
||||
directory,
|
||||
$"{filePrefix}_snapshot.csv");
|
||||
|
||||
_canWriter = CreateWriter(CanLogPath);
|
||||
_snapshotWriter = CreateWriter(SnapshotLogPath);
|
||||
|
||||
_canWriter.WriteLine(
|
||||
"ElapsedMs,ReceiveSequence,CanId,MotorName,RawRpm,SpeedMps");
|
||||
|
||||
_snapshotWriter.WriteLine(
|
||||
"ElapsedMs,CarNum,ManualControlMode,ManualMode,SendThresSpeed," +
|
||||
"DiffSteerKp,DiffSteerKi,DiffSteerKd,DiffSteerMaxI,DiffSteerDeadZone,DiffSteerThresh,DiffSteerSpeedAcc," +
|
||||
"PidOutLeftFront,PidOutLeftRear,PidOutRightFront,PidOutRightRear," +
|
||||
"CmdLFL,CmdLFR,CmdLRL,CmdLRR,CmdRFL,CmdRFR,CmdRRL,CmdRRR," +
|
||||
"PidLFL,PidLFR,PidLRL,PidLRR,PidRFL,PidRFR,PidRRL,PidRRR," +
|
||||
"ActualLFL,ActualLFR,ActualLRL,ActualLRR,ActualRFL,ActualRFR,ActualRRL,ActualRRR," +
|
||||
"ActualLeftFront,ActualLeftRear,ActualRightFront,ActualRightRear," +
|
||||
"TargetThLeftFront,TargetThLeftRear,TargetThRightFront,TargetThRightRear," +
|
||||
"ActualThLeftFront,ActualThLeftRear,ActualThRightFront,ActualThRightRear," +
|
||||
"ErrorThLeftFront,ErrorThLeftRear,ErrorThRightFront,ErrorThRightRear");
|
||||
|
||||
while (_records.TryDequeue(out _))
|
||||
{
|
||||
}
|
||||
|
||||
_queuedRecordCount = 0;
|
||||
_receiveSequence = 0;
|
||||
_droppedRecordCount = 0;
|
||||
_lastSnapshotMilliseconds =
|
||||
double.NegativeInfinity;
|
||||
_stopwatch = Stopwatch.StartNew();
|
||||
_isRunning = true;
|
||||
|
||||
_writerThread = new Thread(WriterLoop)
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "WheelSpeedDiagnosticWriter"
|
||||
};
|
||||
_writerThread.Start();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止记录并等待队列中的诊断数据写入磁盘。
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
Thread writerThread;
|
||||
|
||||
lock (_lifecycleLock)
|
||||
{
|
||||
if (!_isRunning &&
|
||||
_writerThread == null)
|
||||
return;
|
||||
|
||||
_isRunning = false;
|
||||
writerThread = _writerThread;
|
||||
_recordsAvailable.Set();
|
||||
}
|
||||
|
||||
writerThread?.Join(3000);
|
||||
|
||||
lock (_lifecycleLock)
|
||||
{
|
||||
_canWriter?.Flush();
|
||||
_snapshotWriter?.Flush();
|
||||
_canWriter?.Dispose();
|
||||
_snapshotWriter?.Dispose();
|
||||
_canWriter = null;
|
||||
_snapshotWriter = null;
|
||||
_writerThread = null;
|
||||
_stopwatch?.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将一帧驱动器速度反馈加入内存队列,不在CAN回调中执行文件写入。
|
||||
/// </summary>
|
||||
public void RecordCanFeedback(
|
||||
ushort canId,
|
||||
string motorName,
|
||||
float rawRpm,
|
||||
float speedMetersPerSecond)
|
||||
{
|
||||
if (!_isRunning)
|
||||
return;
|
||||
|
||||
var elapsedMilliseconds =
|
||||
_stopwatch.Elapsed.TotalMilliseconds;
|
||||
var receiveSequence =
|
||||
Interlocked.Increment(
|
||||
ref _receiveSequence);
|
||||
|
||||
var line = string.Join(
|
||||
",",
|
||||
Format(elapsedMilliseconds),
|
||||
receiveSequence.ToString(
|
||||
CultureInfo.InvariantCulture),
|
||||
$"0x{canId:X3}",
|
||||
motorName,
|
||||
Format(rawRpm),
|
||||
Format(speedMetersPerSecond));
|
||||
|
||||
Enqueue(new LogRecord(
|
||||
isCanEvent: true,
|
||||
line));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按最多50Hz记录一帧控制命令、PID输出、CAN反馈和舵角快照。
|
||||
/// </summary>
|
||||
public void RecordSnapshot(
|
||||
DiverCartDefinition cart)
|
||||
{
|
||||
if (!_isRunning || cart == null)
|
||||
return;
|
||||
|
||||
var elapsedMilliseconds =
|
||||
_stopwatch.Elapsed.TotalMilliseconds;
|
||||
|
||||
if (elapsedMilliseconds -
|
||||
_lastSnapshotMilliseconds <
|
||||
SnapshotIntervalMilliseconds)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastSnapshotMilliseconds =
|
||||
elapsedMilliseconds;
|
||||
|
||||
var line = string.Join(
|
||||
",",
|
||||
Format(elapsedMilliseconds),
|
||||
cart.CarNum.ToString(
|
||||
CultureInfo.InvariantCulture),
|
||||
Format((int)cart.TransmitterControlMode),
|
||||
Format(cart.ManualMode),
|
||||
Format(cart.SendThresSpeed),
|
||||
Format(cart.DiffSteerKp),
|
||||
Format(cart.DiffSteerKi),
|
||||
Format(cart.DiffSteerKd),
|
||||
Format(cart.DiffSteerMaxI),
|
||||
Format(cart.DiffSteerDeadZone),
|
||||
Format(cart.DiffSteerThresh),
|
||||
Format(cart.DiffSteerSpeedAcc),
|
||||
Format(cart.DiffSteerOutputLeftFront),
|
||||
Format(cart.DiffSteerOutputLeftRear),
|
||||
Format(cart.DiffSteerOutputRightFront),
|
||||
Format(cart.DiffSteerOutputRightRear),
|
||||
Format(cart.SpeedLeftFrontLeft),
|
||||
Format(cart.SpeedLeftFrontRight),
|
||||
Format(cart.SpeedLeftRearLeft),
|
||||
Format(cart.SpeedLeftRearRight),
|
||||
Format(cart.SpeedRightFrontLeft),
|
||||
Format(cart.SpeedRightFrontRight),
|
||||
Format(cart.SpeedRightRearLeft),
|
||||
Format(cart.SpeedRightRearRight),
|
||||
Format(cart.SpeedLFL),
|
||||
Format(cart.SpeedLFR),
|
||||
Format(cart.SpeedLRL),
|
||||
Format(cart.SpeedLRR),
|
||||
Format(cart.SpeedRFL),
|
||||
Format(cart.SpeedRFR),
|
||||
Format(cart.SpeedRRL),
|
||||
Format(cart.SpeedRRR),
|
||||
Format(cart.ActualSpeedLeftFrontLeft),
|
||||
Format(cart.ActualSpeedLeftFrontRight),
|
||||
Format(cart.ActualSpeedLeftRearLeft),
|
||||
Format(cart.ActualSpeedLeftRearRight),
|
||||
Format(cart.ActualSpeedRightFrontLeft),
|
||||
Format(cart.ActualSpeedRightFrontRight),
|
||||
Format(cart.ActualSpeedRightRearLeft),
|
||||
Format(cart.ActualSpeedRightRearRight),
|
||||
Format(cart.ActualSpeedLeftFront),
|
||||
Format(cart.ActualSpeedLeftRear),
|
||||
Format(cart.ActualSpeedRightFront),
|
||||
Format(cart.ActualSpeedRightRear),
|
||||
Format(cart.ThLeftFront),
|
||||
Format(cart.ThLeftRear),
|
||||
Format(cart.ThRightFront),
|
||||
Format(cart.ThRightRear),
|
||||
Format(cart.ActualThLeftFront),
|
||||
Format(cart.ActualThLeftRear),
|
||||
Format(cart.ActualThRightFront),
|
||||
Format(cart.ActualThRightRear),
|
||||
Format(cart.ThLeftFront - cart.ActualThLeftFront),
|
||||
Format(cart.ThLeftRear - cart.ActualThLeftRear),
|
||||
Format(cart.ThRightFront - cart.ActualThRightFront),
|
||||
Format(cart.ThRightRear - cart.ActualThRightRear));
|
||||
|
||||
Enqueue(new LogRecord(
|
||||
isCanEvent: false,
|
||||
line));
|
||||
}
|
||||
|
||||
private static StreamWriter CreateWriter(
|
||||
string path)
|
||||
{
|
||||
return new StreamWriter(
|
||||
path,
|
||||
append: false,
|
||||
new UTF8Encoding(
|
||||
encoderShouldEmitUTF8Identifier: true),
|
||||
bufferSize: 64 * 1024);
|
||||
}
|
||||
|
||||
private void Enqueue(LogRecord record)
|
||||
{
|
||||
var queuedCount =
|
||||
Interlocked.Increment(
|
||||
ref _queuedRecordCount);
|
||||
|
||||
if (queuedCount >
|
||||
MaximumQueuedRecords)
|
||||
{
|
||||
Interlocked.Decrement(
|
||||
ref _queuedRecordCount);
|
||||
Interlocked.Increment(
|
||||
ref _droppedRecordCount);
|
||||
return;
|
||||
}
|
||||
|
||||
_records.Enqueue(record);
|
||||
_recordsAvailable.Set();
|
||||
}
|
||||
|
||||
private void WriterLoop()
|
||||
{
|
||||
var lastFlushTime = DateTime.UtcNow;
|
||||
|
||||
try
|
||||
{
|
||||
while (_isRunning ||
|
||||
!_records.IsEmpty)
|
||||
{
|
||||
var wroteAnyRecord = false;
|
||||
|
||||
while (_records.TryDequeue(
|
||||
out var record))
|
||||
{
|
||||
Interlocked.Decrement(
|
||||
ref _queuedRecordCount);
|
||||
|
||||
if (record.IsCanEvent)
|
||||
_canWriter.WriteLine(record.Line);
|
||||
else
|
||||
_snapshotWriter.WriteLine(record.Line);
|
||||
|
||||
wroteAnyRecord = true;
|
||||
}
|
||||
|
||||
var shouldFlush =
|
||||
wroteAnyRecord &&
|
||||
(DateTime.UtcNow -
|
||||
lastFlushTime)
|
||||
.TotalMilliseconds >= 500.0;
|
||||
|
||||
if (shouldFlush)
|
||||
{
|
||||
_canWriter.Flush();
|
||||
_snapshotWriter.Flush();
|
||||
lastFlushTime = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
if (!wroteAnyRecord)
|
||||
_recordsAvailable.WaitOne(100);
|
||||
}
|
||||
|
||||
var dropped =
|
||||
Interlocked.Read(
|
||||
ref _droppedRecordCount);
|
||||
|
||||
if (dropped > 0)
|
||||
{
|
||||
_canWriter.WriteLine(
|
||||
$"# DroppedRecords={dropped}");
|
||||
_snapshotWriter.WriteLine(
|
||||
$"# DroppedRecords={dropped}");
|
||||
}
|
||||
|
||||
_canWriter.Flush();
|
||||
_snapshotWriter.Flush();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 后台日志失败不能终止车辆控制线程。
|
||||
Console.WriteLine(
|
||||
"轮速诊断后台写入失败:" +
|
||||
ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static string Format(
|
||||
double value)
|
||||
{
|
||||
return value.ToString(
|
||||
"0.######",
|
||||
CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
_recordsAvailable.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"runtimeTarget": {
|
||||
"name": ".NETCoreApp,Version=v8.0",
|
||||
"signature": ""
|
||||
},
|
||||
"compilationOptions": {},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"MedullaAdapter/1.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"CommonUsage/1.0.0.0": {
|
||||
"type": "reference",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user