From e5aeae9c33308054a8507334264351981c601f4e Mon Sep 17 00:00:00 2001 From: "ruifeng.zhou" Date: Fri, 19 Jun 2026 23:12:05 +0800 Subject: [PATCH] Initial tutorial project --- .gitignore | 55 +++++++++++ DiffWheelM/CartDefinition.cs | 32 ++++++ DiffWheelM/DiffWheelM.csproj | 31 ++++++ DiffWheelM/MotorRoutine.cs | 11 +++ DiffWheelM/Remote.cs | 31 ++++++ MultiWheelM/CartDefinition.cs | 175 +++++++++++++++++++++++++++++++++ MultiWheelM/MotorRoutine.cs | 11 +++ MultiWheelM/MultiWheelM.csproj | 34 +++++++ MultiWheelM/Remote.cs | 70 +++++++++++++ README.md | 72 ++++++++++++++ Tutorial.sln | 31 ++++++ 11 files changed, 553 insertions(+) create mode 100644 .gitignore create mode 100644 DiffWheelM/CartDefinition.cs create mode 100644 DiffWheelM/DiffWheelM.csproj create mode 100644 DiffWheelM/MotorRoutine.cs create mode 100644 DiffWheelM/Remote.cs create mode 100644 MultiWheelM/CartDefinition.cs create mode 100644 MultiWheelM/MotorRoutine.cs create mode 100644 MultiWheelM/MultiWheelM.csproj create mode 100644 MultiWheelM/Remote.cs create mode 100644 README.md create mode 100644 Tutorial.sln diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..42d05b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,55 @@ +# Build results +[Bb]in/ +[Oo]bj/ +build/ +artifacts/ + +# Visual Studio / Rider / VS Code +.vs/ +.idea/ +.vscode/ +*.user +*.suo +*.rsuser +*.userosscache +*.sln.docstates + +# .NET generated files +project.assets.json +project.nuget.cache +*.nuget.g.props +*.nuget.g.targets +*.AssemblyInfo.cs +*.GeneratedMSBuildEditorConfig.editorconfig +*.GlobalUsings.g.cs +*.FileListAbsolute.txt +*.CoreCompileInputs.cache +*.AssemblyReference.cache +*.assets.cache + +# Test and coverage output +TestResults/ +coverage/ +*.coverage +*.coveragexml +coverage*.json + +# Logs, diagnostics, and temporary files +*.log +*.tlog +*.binlog +*.pdb +*.cache +*.tmp +*.temp +*.swp +*.bak + +# Local runtime configuration +cartparams.json +appsettings.Development.json +*.local.json + +# OS files +Thumbs.db +.DS_Store diff --git a/DiffWheelM/CartDefinition.cs b/DiffWheelM/CartDefinition.cs new file mode 100644 index 0000000..72b93c7 --- /dev/null +++ b/DiffWheelM/CartDefinition.cs @@ -0,0 +1,32 @@ +using CartActivator; + +namespace DiffWheelM; + +[UseManualController(manualController = typeof(Remote))] +[UseLadderLogic(logic = typeof(MotorRoutine), scanInterval = 50)] +public class CartDefinition : CartActivator.CartDefinition +{ + [AsUpperIO(desc = "左轮下发速度", timeOutReset = true)] + [Sandbox] + public float SpeedLeft; + + [AsUpperIO(desc = "右轮下发速度", timeOutReset = true)] + [Sandbox] + public float SpeedRight; + + [AsLowerIO(desc = "左轮实际速度")] + [Sandbox] + public float ActualSpeedLeft; + + [AsLowerIO(desc = "右轮实际速度")] + [Sandbox] + public float ActualSpeedRight; + + [AsInitParam(desc = "手动最大轮速(m/s)")] + public float MaxManualSpeed = 0.3f; + + public override void Init() + { + Console.WriteLine("[DiffWheelM] Minimal Medulla cart initialized."); + } +} diff --git a/DiffWheelM/DiffWheelM.csproj b/DiffWheelM/DiffWheelM.csproj new file mode 100644 index 0000000..ae96fc9 --- /dev/null +++ b/DiffWheelM/DiffWheelM.csproj @@ -0,0 +1,31 @@ + + + + net8.0 + enable + enable + AnyCPU + false + ..\build\Medulla\plugins\ + + + + + D:\MDCS\Dependencies\Medulla\RefCartActivator.dll + False + + + D:\MDCS\Dependencies\Medulla\RefMedullaCore.dll + False + + + D:\MDCS\Dependencies\Medulla\RefMedulla2.dll + False + + + D:\MDCS\Dependencies\deps\RefFundamentalLib.dll + False + + + + diff --git a/DiffWheelM/MotorRoutine.cs b/DiffWheelM/MotorRoutine.cs new file mode 100644 index 0000000..308a4bd --- /dev/null +++ b/DiffWheelM/MotorRoutine.cs @@ -0,0 +1,11 @@ +using CartActivator; + +namespace DiffWheelM; + +public class MotorRoutine : LadderLogic +{ + public override void Operation(int iteration) + { + + } +} diff --git a/DiffWheelM/Remote.cs b/DiffWheelM/Remote.cs new file mode 100644 index 0000000..a46d9a2 --- /dev/null +++ b/DiffWheelM/Remote.cs @@ -0,0 +1,31 @@ +using CartActivator; + +namespace DiffWheelM; + +public class Remote : ManualController +{ + [AsControlItem(name = "速度摇杆", keyboard = "Up,Down,Left,Right", joystick = "Axis0,Axis1")] + public Stick Direction; + + [AsControlItem(name = "速度比例", keyboard = "Q,W")] + public Throttle SpeedRatio; + + public override void Operation() + { + var maxSpeed = Math.Max(0, cart.MaxManualSpeed); + var speedRatio = Math.Clamp(SpeedRatio.val, 0, 1); + + var left = Direction.y - Direction.x; + var right = Direction.y + Direction.x; + var wheelScale = Math.Max(1, Math.Max(Math.Abs(left), Math.Abs(right))); + + left = left / wheelScale * maxSpeed * speedRatio; + right = right / wheelScale * maxSpeed * speedRatio; + + cart.SpeedLeft = Math.Clamp(left, -maxSpeed, maxSpeed); + cart.SpeedRight = Math.Clamp(right, -maxSpeed, maxSpeed); + + statusText = $"L={cart.SpeedLeft:0.000}m/s, R={cart.SpeedRight:0.000}m/s"; + } +} + diff --git a/MultiWheelM/CartDefinition.cs b/MultiWheelM/CartDefinition.cs new file mode 100644 index 0000000..b3bb9f7 --- /dev/null +++ b/MultiWheelM/CartDefinition.cs @@ -0,0 +1,175 @@ +using CartActivator; +using CommonUsage.Chassis; +using System.Numerics; + +namespace MultiWheelM; + +[UseManualController(manualController = typeof(Remote))] +[UseLadderLogic(logic = typeof(MotorRoutine), scanInterval = 50)] +public class CartDefinition : CartActivator.CartDefinition +{ + [AsUpperIO(desc = "左前轮速度", timeOutReset = true)] + [Playground] + public float SpeedLeftFront; + + [AsUpperIO(desc = "左后轮速度", timeOutReset = true)] + [Playground] + public float SpeedLeftRear; + + [AsUpperIO(desc = "右前轮速度", timeOutReset = true)] + [Playground] + public float SpeedRightFront; + + [AsUpperIO(desc = "右后轮速度", timeOutReset = true)] + [Playground] + public float SpeedRightRear; + + [AsLowerIO(desc = "左前轮速度反馈")] + [Playground] + public float ActualSpeedLeftFront; + + [AsLowerIO(desc = "左后轮速度反馈")] + [Playground] + public float ActualSpeedLeftRear; + + [AsLowerIO(desc = "右前轮速度反馈")] + [Playground] + public float ActualSpeedRightFront; + + [AsLowerIO(desc = "右后轮速度反馈")] + [Playground] + public float ActualSpeedRightRear; + + [AsUpperIO(desc = "左前轮转向角度", timeOutReset = true)] + [Playground] + public float ThLeftFront; + + [AsUpperIO(desc = "左后轮转向角度", timeOutReset = true)] + [Playground] + public float ThLeftRear; + + [AsUpperIO(desc = "右前轮转向角度", timeOutReset = true)] + [Playground] + public float ThRightFront; + + [AsUpperIO(desc = "右后轮转向角度", timeOutReset = true)] + [Playground] + public float ThRightRear; + + [AsLowerIO(desc = "左前轮转向角度反馈")] + [Playground] + public float ActualThLeftFront; + + [AsLowerIO(desc = "左后轮转向角度反馈")] + [Playground] + public float ActualThLeftRear; + + [AsLowerIO(desc = "右前轮转向角度反馈")] + [Playground] + public float ActualThRightFront; + + [AsLowerIO(desc = "右后轮转向角度反馈")] + [Playground] + public float ActualThRightRear; + + [AsLowerIO(desc = "遥控模式")] + public int ManualMode = -1; + + [AsInitParam(desc = "手动最大速度(m/s)")] + public float MaxManualSpeed = 0.3f; + + [AsInitParam(desc = "手动最大自旋角速度(deg/s)")] + public float MaxManualAngularSpeed = 45f; + + [AsInitParam(desc = "手动最大转向角度(deg)")] + public float MaxManualTheta = 45f; + + [AsInitParam(desc = "遥控转向输入幂数")] + public float ManualThetaPow = 2f; + + [AsInitParam(desc = "舵轮前后半轴距(mm)")] + public float WheelBaseHalfLength = 750f; + + [AsInitParam(desc = "舵轮左右半轮距(mm)")] + public float WheelTrackHalfWidth = 500f; + + [AsInitParam(desc = "舵轮最小角度(deg)")] + public float WheelAngleLowerLimit = -120f; + + [AsInitParam(desc = "舵轮最大角度(deg)")] + public float WheelAngleUpperLimit = 120f; + + [AsInitParam(desc = "底盘速度加速度(m/s^2)")] + public float ChassisAccPerSecond = 0.3f; + + [AsInitParam(desc = "底盘速度减速度(m/s^2)")] + public float ChassisDeAccPerSecond = 0.5f; + + [AsInitParam(desc = "转向最低速度系数")] + public float ChassisMinTurnSpeedFac = 0.25f; + + [AsInitParam(desc = "几何控制点半径(mm)")] + public float ChassisControlPointRadius = 500f; + + [AsInitParam(desc = "几何控制点转角速度(deg/s)")] + public float ChassisGcpThetaPerSecond = 10f; + + [AsInitParam(desc = "阿克曼最小转向角(deg)")] + public float ChassisMinimumTurningAngleForAckermann = 60f; + + public MultiWheelChassis Chassis = null!; + + public override void Init() + { + Chassis = new MultiWheelChassis + { + MaxSpeed = MaxManualSpeed, + AccPerSecond = ChassisAccPerSecond, + DeAccPerSecond = ChassisDeAccPerSecond, + MinTurnSpeedFac = ChassisMinTurnSpeedFac, + ControlPointRadius = ChassisControlPointRadius, + GcpThetaPerSecond = ChassisGcpThetaPerSecond, + MinimumTurningAngleForAckermann = ChassisMinimumTurningAngleForAckermann + }; + + Chassis.AddWheel(new SteerWheel( + new Vector2(WheelBaseHalfLength, WheelTrackHalfWidth), + WheelAngleLowerLimit, + WheelAngleUpperLimit, + speed => SpeedLeftFront = speed, + () => ActualSpeedLeftFront, + angle => ThLeftFront = angle, + () => ActualThLeftFront)); + + Chassis.AddWheel(new SteerWheel( + new Vector2(-WheelBaseHalfLength, WheelTrackHalfWidth), + WheelAngleLowerLimit, + WheelAngleUpperLimit, + speed => SpeedLeftRear = speed, + () => ActualSpeedLeftRear, + angle => ThLeftRear = angle, + () => ActualThLeftRear)); + + Chassis.AddWheel(new SteerWheel( + new Vector2(WheelBaseHalfLength, -WheelTrackHalfWidth), + WheelAngleLowerLimit, + WheelAngleUpperLimit, + speed => SpeedRightFront = speed, + () => ActualSpeedRightFront, + angle => ThRightFront = angle, + () => ActualThRightFront)); + + Chassis.AddWheel(new SteerWheel( + new Vector2(-WheelBaseHalfLength, -WheelTrackHalfWidth), + WheelAngleLowerLimit, + WheelAngleUpperLimit, + speed => SpeedRightRear = speed, + () => ActualSpeedRightRear, + angle => ThRightRear = angle, + () => ActualThRightRear)); + + Chassis.Initialize(); + + Console.WriteLine("[MultiWheelM] Minimal multi-wheel Medulla cart initialized with CommonUsage chassis geometry."); + } +} diff --git a/MultiWheelM/MotorRoutine.cs b/MultiWheelM/MotorRoutine.cs new file mode 100644 index 0000000..4ffd363 --- /dev/null +++ b/MultiWheelM/MotorRoutine.cs @@ -0,0 +1,11 @@ +using CartActivator; + +namespace MultiWheelM; + +public class MotorRoutine : LadderLogic +{ + public override void Operation(int iteration) + { + + } +} diff --git a/MultiWheelM/MultiWheelM.csproj b/MultiWheelM/MultiWheelM.csproj new file mode 100644 index 0000000..a5a9b8b --- /dev/null +++ b/MultiWheelM/MultiWheelM.csproj @@ -0,0 +1,34 @@ + + + + net8.0 + enable + enable + AnyCPU + false + ..\build\Medulla\plugins\ + + + + + D:\MDCS\Release\Medulla\RefCartActivator.dll + False + + + D:\MDCS\Release\Medulla\RefMedullaCore.dll + False + + + D:\MDCS\Release\Medulla\RefMedulla2.dll + False + + + D:\MDCS\Dependencies\deps\RefFundamentalLib.dll + False + + + D:\MDCS\Dependencies\Commons\CommonUsage.dll + + + + diff --git a/MultiWheelM/Remote.cs b/MultiWheelM/Remote.cs new file mode 100644 index 0000000..dae9790 --- /dev/null +++ b/MultiWheelM/Remote.cs @@ -0,0 +1,70 @@ +using CartActivator; + +namespace MultiWheelM; + +public class Remote : ManualController +{ + [AsControlItem(name = "速度摇杆", keyboard = "Up,Down,Left,Right", joystick = "Axis0,Axis1")] + public Stick SpeedPad; + + [AsControlItem(name = "速度比例", keyboard = "Q,W")] + public Throttle SpeedRatio; + + [AsControlItem(name = "旋转模式", keyboard = "R")] + public Switch SpinMode; + + [AsControlItem(name = "横移模式", keyboard = "C")] + public Switch CrabMode; + + [AsControlItem(name = "斜行模式", keyboard = "S")] + public Switch SwayMode; + + [AsControlItem(name = "车头方向", keyboard = "A,D")] + public DThrottle FrontDirection; + + public override void Operation() + { + var speedThreshold = Math.Clamp(SpeedRatio.val, 0, 1) * Math.Max(0, cart.MaxManualSpeed); + var speed = Math.Clamp(SpeedPad.y, -1, 1) * speedThreshold; + var steerInput = Math.Clamp(SpeedPad.x, -1, 1); + var steering = (float)Math.Pow(Math.Abs(steerInput), cart.ManualThetaPow) * Math.Sign(steerInput); + var frontTh = -steering * cart.MaxManualTheta; + var rearTh = steering * cart.MaxManualTheta; + var frontDirection = Math.Clamp(FrontDirection.dval, -1, 1) * 180; + + if (SpinMode.on) + { + var direction = steerInput == 0 ? Math.Sign(speed) : steerInput; + cart.Chassis.SendRotateMotion(direction * cart.MaxManualAngularSpeed * Math.Clamp(SpeedRatio.val, 0, 1)); + cart.ManualMode = 1; + statusText = $"spin, w={direction * cart.MaxManualAngularSpeed:0.0}deg/s"; + return; + } + + if (CrabMode.on) + { + cart.Chassis.SetOriginBias(0, 0, 90); + cart.Chassis.SendMotion(speed, frontTh, rearTh); + cart.ManualMode = 2; + statusText = $"crab, v={speed:0.000}, front={frontTh:0.0}, rear={rearTh:0.0}"; + return; + } + + if (SwayMode.on) + { + cart.Chassis.SetOriginBias(0, 0, frontDirection); + cart.Chassis.SendMotion(speed, frontTh, -rearTh); + cart.ManualMode = 3; + statusText = $"sway, v={speed:0.000}, front={frontTh:0.0}, rear={-rearTh:0.0}"; + return; + } + + cart.Chassis.SetOriginBias(0, 0, frontDirection); + cart.Chassis.SendMotion(speed, frontTh, rearTh); + cart.ManualMode = 0; + statusText = $"normal, v={speed:0.000}, front={frontTh:0.0}, rear={rearTh:0.0}"; + } +} + + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..b67eb0b --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +# Tutorial Medulla Plugins + +本仓库用于放置最小 Medulla 插件示例,方便从产品化项目中抽离出底盘 IO 与遥控器逻辑。示例不引用 `MDCSToolBox`;其中 `MultiWheelM` 为了使用底盘几何模型,会额外引用 `CommonUsage`。 + +## 目录结构 + +```text +Tutorial/ + Tutorial.sln + DiffWheelM/ + 差速底盘最小 Medulla 插件 + MultiWheelM/ + 多舵轮底盘最小 Medulla 插件 +``` + +`build/`、`obj/` 和 `.vs/` 是本地构建或调试输出,已在 `.gitignore` 中忽略。 + +## DiffWheelM + +`DiffWheelM` 参考 `StandardKivaM`,保留差速底盘最小 IO: + +- `SpeedLeft` / `SpeedRight`: 左右轮下发速度,`AsUpperIO`,超时自动复位 +- `ActualSpeedLeft` / `ActualSpeedRight`: 左右轮实际速度反馈,`AsLowerIO` +- `MaxManualSpeed`: 手动最大轮速初始化参数,默认 `0.3m/s` + +遥控器使用 `Up,Down,Left,Right` 控制前后与转向,`Q,W` 控制速度比例。 + +## MultiWheelM + +`MultiWheelM` 参考 `StandardMultiWheelLifterM`,保留多舵轮底盘最小 IO: + +- 四个舵轮速度:`SpeedLeftFront`、`SpeedLeftRear`、`SpeedRightFront`、`SpeedRightRear` +- 四个舵轮转角:`ThLeftFront`、`ThLeftRear`、`ThRightFront`、`ThRightRear` +- 对应的实际速度与实际转角反馈 +- `MaxManualSpeed`、`MaxManualAngularSpeed`、`MaxManualTheta` 初始化参数 +- 舵轮几何参数:`WheelBaseHalfLength`、`WheelTrackHalfWidth`、`WheelAngleLowerLimit`、`WheelAngleUpperLimit` + +遥控器支持普通转向、旋转、横移和斜行几个最小模式: + +- `Up,Down,Left,Right`: 速度摇杆 +- `Q,W`: 速度比例 +- `R`: 旋转模式 +- `C`: 横移模式 +- `S`: 斜行模式 +- `A,D`: 车头方向偏置 + +当前多舵轮示例使用 `CommonUsage.Chassis.MultiWheelChassis` 做遥控几何分解,不接真实下位机,`MotorRoutine` 会把下发速度/角度镜像到实际反馈变量,便于 Medulla UI 或 Clumsy 侧观察。 + +## 构建 + +```powershell +dotnet build .\Tutorial.sln +``` + +默认输出目录: + +```text +build\Medulla\plugins\ +``` + +## 依赖 + +两个插件都只引用: + +- `D:\MDCS\Dependencies\Medulla\RefCartActivator.dll` +- `D:\MDCS\Dependencies\Medulla\RefMedullaCore.dll` +- `D:\MDCS\Dependencies\Medulla\RefMedulla2.dll` +- `D:\MDCS\Dependencies\deps\RefFundamentalLib.dll` + +`MultiWheelM` 额外引用: + +- `D:\MDCS\Dependencies\Commons\CommonUsage.dll` diff --git a/Tutorial.sln b/Tutorial.sln new file mode 100644 index 0000000..6f902f5 --- /dev/null +++ b/Tutorial.sln @@ -0,0 +1,31 @@ + +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}") = "DiffWheelM", "DiffWheelM\DiffWheelM.csproj", "{7C6F23A7-2B4D-45C2-98E9-6C55FBB0F1B8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MultiWheelM", "MultiWheelM\MultiWheelM.csproj", "{B7B53B6B-98B5-4B6F-9B09-598F7B8166C9}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7C6F23A7-2B4D-45C2-98E9-6C55FBB0F1B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C6F23A7-2B4D-45C2-98E9-6C55FBB0F1B8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C6F23A7-2B4D-45C2-98E9-6C55FBB0F1B8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C6F23A7-2B4D-45C2-98E9-6C55FBB0F1B8}.Release|Any CPU.Build.0 = Release|Any CPU + {B7B53B6B-98B5-4B6F-9B09-598F7B8166C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B7B53B6B-98B5-4B6F-9B09-598F7B8166C9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B7B53B6B-98B5-4B6F-9B09-598F7B8166C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B7B53B6B-98B5-4B6F-9B09-598F7B8166C9}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {EF6B92D5-5691-42DE-8B38-C17AE7DD7418} + EndGlobalSection +EndGlobal