Skip to content

Commit 16c4eec

Browse files
baggepinnenclaude
andcommitted
Add steerable 3D bot with cascade balance/position control and yaw controller
Add SteerableDyadBot3D, a variant of RollingDyadBot3D with individually driven wheels and odometric position/velocity plus yaw angle/rate outputs, together with a control architecture for it: - ControlMixer combines the total drive torque and a differential yaw torque into the two wheel torque commands. - YawController (PID on the heading angle) produces the differential torque; the yaw dynamics through the mixer are approximately a double integrator. - YawControlledDyadBot3D closes both loops: the existing CascadeController holds the robot upright at position zero while the yaw controller tracks a heading reference that steps by 90 degrees at t = 5, making the robot spin in place to the new heading. The cascade position loop uses the odometric path position (average wheel arc length) rather than the world-frame x coordinate, since the latter is no longer aligned with the driving direction once the robot has yawed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 89d5512 commit 16c4eec

19 files changed

Lines changed: 1194 additions & 2 deletions

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ Plant models:
1717
wheel axle (`MultibodyComponents.RollingWheelSet`). Same interface and
1818
parameters as `PlanarDyadBot`; for motion in the vertical plane it is
1919
dynamically equivalent to the planar model.
20+
- `SteerableDyadBot3D`: like `RollingDyadBot3D` but with individually driven
21+
wheels, so the robot can be steered by differential torque. Outputs
22+
odometric path position/velocity, tilt angle/rate and yaw (heading)
23+
angle/rate.
2024

2125
Closed-loop models around the planar plant:
2226

@@ -37,6 +41,11 @@ Closed-loop models around the 3D plant:
3741
`AngleController` as `AngleControlledDyadBot`; with in-plane motion the
3842
closed-loop response is identical to the planar model
3943
(`test/test_stabilization.jl` verifies this).
44+
- `YawControlledDyadBot3D`: `SteerableDyadBot3D` with the balance/position
45+
cascade (`CascadeController`, position reference held at zero) and a
46+
separate `YawController` tracking a heading reference that steps at t = 5;
47+
a `ControlMixer` combines drive and yaw torque into the two wheel torque
48+
commands, so the robot spins in place to the new heading.
4049

4150
## Controller tuning scripts (`scripts/`)
4251

dyad/segway_3d.dyad

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,212 @@ metadata {
190190
}
191191
end
192192

193+
"""
194+
Three-dimensional model of a two-wheeled balancing robot with ideal rolling
195+
contact and individually driven wheels.
196+
197+
Same mechanics as `RollingDyadBot3D` (ideal no-slip rolling axle with an
198+
inverted-pendulum body on a revolute joint about the axle axis), but each
199+
wheel has its own motor torque input, so the robot can be steered by applying
200+
a differential torque. Each motor torque acts between the body and its wheel.
201+
202+
Inputs and outputs:
203+
- `torque_left`, `torque_right`: motor torques applied between body and each wheel
204+
- `pos`, `vel`: odometric path position and velocity (average wheel arc
205+
length; equals the distance driven along the — possibly curved — path)
206+
- `theta`, `theta_dot`: tilt angle and tilt rate of the body (zero when upright)
207+
- `yaw`, `yaw_dot`: heading angle and rate (positive counterclockwise seen from above)
208+
209+
The model must be accompanied by a `MultibodyComponents.World` component in
210+
the top level of the enclosing model.
211+
"""
212+
component SteerableDyadBot3D
213+
wheels = MultibodyComponents.RollingWheelSet(radius = R, m_wheel = m / 2, I_axis = Iw / 2, I_long = Iw / 4, track = track) {^wheels}
214+
"Body tilt joint about the axle axis"
215+
pitch = MultibodyComponents.Revolute(n = [0, 0, 1]) {^pitch}
216+
body = MultibodyComponents.BodyShape(color = [0.2, 0.2, 0.2, 0.9], m = M, r = [0, 2 * L, 0], r_cm = [0, L, 0], I_11 = Ib, I_22 = Ib / 10, I_33 = Ib, radius = 0.03) {^body}
217+
# wheels.axis1 is the wheel on the +z side of the axle, which is the right
218+
# wheel when facing the forward x direction with y up
219+
torquesource_right = RotationalComponents.Sources.TorqueSource() {^torquesource_right}
220+
torquesource_left = RotationalComponents.Sources.TorqueSource() {^torquesource_left}
221+
damper_right = RotationalComponents.Components.Damper(d = d / 2) {^damper_right}
222+
damper_left = RotationalComponents.Components.Damper(d = d / 2) {^damper_left}
223+
"Left-wheel motor torque command"
224+
torque_left = Dyad.RealInput() {^torque_left}
225+
"Right-wheel motor torque command"
226+
torque_right = Dyad.RealInput() {^torque_right}
227+
"Odometric path position (average wheel arc length)"
228+
pos = Dyad.RealOutput() {^pos}
229+
"Body tilt angle"
230+
theta = Dyad.RealOutput() {^theta}
231+
"Odometric path velocity"
232+
vel = Dyad.RealOutput() {^vel}
233+
"Body tilt rate"
234+
theta_dot = Dyad.RealOutput() {^theta_dot}
235+
"Yaw (heading) angle"
236+
yaw = Dyad.RealOutput() {^yaw}
237+
"Yaw rate"
238+
yaw_dot = Dyad.RealOutput() {^yaw_dot}
239+
"Body mass"
240+
parameter M::Mass = 0.1
241+
"Total wheel mass (split between the two wheels)"
242+
parameter m::Mass = 0.05
243+
"Wheel radius"
244+
parameter R::Length = 0.04
245+
"Distance from wheel axis to body center of mass"
246+
parameter L::Length = 0.08
247+
"Body moment of inertia about the axle axis"
248+
parameter Ib::Inertia = 2.5e-4
249+
"Total wheel + motor moment of inertia about the spin axis (split between the two wheels)"
250+
parameter Iw::Inertia = 1e-4
251+
"Motor viscous friction (split between the two wheels)"
252+
parameter d::RotationalDampingConstant = 0.0001
253+
"Initial tilt angle of the body"
254+
parameter phi0::Angle = 0.0
255+
"Distance between the wheels"
256+
parameter track::Length = 0.13
257+
relations
258+
initial pitch.phi = phi0
259+
initial pitch.w = 0
260+
# Wheel spin theta and forward position have opposite signs (positive wheel
261+
# torque drives the robot toward negative x, like in PlanarDyadBot)
262+
pos = -R * (wheels.theta1 + wheels.theta2) / 2
263+
vel = der(pos)
264+
theta = pitch.phi
265+
theta_dot = pitch.w
266+
yaw = wheels.phi
267+
yaw_dot = der(wheels.phi)
268+
connect(body.frame_a, pitch.frame_b) {^id14}
269+
connect(pitch.frame_a, wheels.frame_middle) {^id15}
270+
connect(torquesource_right.spline, wheels.axis1) {^id16}
271+
connect(torquesource_left.spline, wheels.axis2) {^id17}
272+
connect(damper_left.spline_b, wheels.axis2) {^id18}
273+
connect(damper_right.spline_b, wheels.axis1) {^id19}
274+
connect(torque_right, torquesource_right.tau) {^id20}
275+
connect(torque_left, torquesource_left.tau) {^id21}
276+
connect(pitch.axis, torquesource_right.support, damper_right.spline_a, torquesource_left.support, damper_left.spline_a) {^id22}
277+
metadata {
278+
"Dyad": {"icons": {"default": "dyad://DyadBotComponents/DyadBot3D.svg"}},
279+
"_links": {
280+
"wheels": {
281+
"Dyad": {
282+
"placement": {
283+
"diagram": {"iconName": "default", "x1": 400, "y1": 620, "x2": 600, "y2": 820, "rot": 0}
284+
},
285+
"tags": []
286+
}
287+
},
288+
"pitch": {
289+
"Dyad": {
290+
"placement": {"diagram": {"x1": 450, "y1": 320, "x2": 550, "y2": 420, "rot": 270}}
291+
}
292+
},
293+
"body": {
294+
"Dyad": {
295+
"placement": {"diagram": {"x1": 350, "y1": -10, "x2": 650, "y2": 290, "rot": 270}}
296+
}
297+
},
298+
"torquesource_right": {
299+
"Dyad": {"placement": {"diagram": {"x1": 150, "y1": 620, "x2": 250, "y2": 720}}}
300+
},
301+
"torquesource_left": {
302+
"Dyad": {"placement": {"diagram": {"x1": 850, "y1": 620, "x2": 750, "y2": 720}}}
303+
},
304+
"damper_right": {
305+
"Dyad": {"placement": {"diagram": {"x1": 150, "y1": 760, "x2": 250, "y2": 860}}}
306+
},
307+
"damper_left": {
308+
"Dyad": {"placement": {"diagram": {"x1": 850, "y1": 760, "x2": 750, "y2": 860}}}
309+
},
310+
"torque_left": {
311+
"Dyad": {
312+
"placement": {
313+
"diagram": {"iconName": "default", "x1": 20, "y1": 560, "x2": 100, "y2": 640, "rot": 0}
314+
},
315+
"tags": []
316+
}
317+
},
318+
"torque_right": {
319+
"Dyad": {
320+
"placement": {
321+
"diagram": {"iconName": "default", "x1": 20, "y1": 440, "x2": 100, "y2": 520, "rot": 0}
322+
},
323+
"tags": []
324+
}
325+
},
326+
"pos": {
327+
"Dyad": {
328+
"placement": {
329+
"diagram": {"iconName": "default", "x1": 880, "y1": 180, "x2": 960, "y2": 260, "rot": 0}
330+
},
331+
"tags": []
332+
}
333+
},
334+
"theta": {
335+
"Dyad": {
336+
"placement": {
337+
"diagram": {"iconName": "default", "x1": 880, "y1": 300, "x2": 960, "y2": 380, "rot": 0}
338+
},
339+
"tags": []
340+
}
341+
},
342+
"vel": {
343+
"Dyad": {
344+
"placement": {
345+
"diagram": {"iconName": "default", "x1": 880, "y1": 420, "x2": 960, "y2": 500, "rot": 0}
346+
},
347+
"tags": []
348+
}
349+
},
350+
"theta_dot": {
351+
"Dyad": {
352+
"placement": {
353+
"diagram": {"iconName": "default", "x1": 880, "y1": 540, "x2": 960, "y2": 620, "rot": 0}
354+
},
355+
"tags": []
356+
}
357+
},
358+
"yaw": {
359+
"Dyad": {
360+
"placement": {
361+
"diagram": {"iconName": "default", "x1": 880, "y1": 660, "x2": 960, "y2": 740, "rot": 0}
362+
},
363+
"tags": []
364+
}
365+
},
366+
"yaw_dot": {
367+
"Dyad": {
368+
"placement": {
369+
"diagram": {"iconName": "default", "x1": 880, "y1": 780, "x2": 960, "y2": 860, "rot": 0}
370+
},
371+
"tags": []
372+
}
373+
},
374+
"id14": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}], "renderStyle": "standard"}},
375+
"id15": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}], "renderStyle": "standard"}},
376+
"id16": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}], "renderStyle": "standard"}},
377+
"id17": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}], "renderStyle": "standard"}},
378+
"id18": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}], "renderStyle": "standard"}},
379+
"id19": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}], "renderStyle": "standard"}},
380+
"id20": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}], "renderStyle": "standard"}},
381+
"id21": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}], "renderStyle": "standard"}},
382+
"id22": {
383+
"Dyad": {
384+
"edges": [
385+
{"S": 1, "M": [], "E": -1},
386+
{"S": -1, "M": [], "E": 2},
387+
{"S": 3, "M": [], "E": -1},
388+
{"S": 4, "M": [], "E": -1},
389+
{"S": 5, "M": [], "E": -1}
390+
],
391+
"junctions": [{"x": 500, "y": 900}],
392+
"renderStyle": "standard"
393+
}
394+
}
395+
}
396+
}
397+
end
398+
193399
"""
194400
Three-dimensional model of a two-wheeled balancing robot with ideal rolling
195401
contact.

0 commit comments

Comments
 (0)