@@ -192,24 +192,27 @@ end
192192
193193"""
194194Three-dimensional model of a two-wheeled balancing robot with ideal rolling
195- contact.
195+ contact and individually driven wheels .
196196
197197The two wheels form an ideal no-slip rolling axle
198198(`MultibodyComponents.RollingWheelSet`, which resolves the kinematic loop
199199through the two wheels internally), and the inverted-pendulum body is mounted
200- at the center of the axle through a revolute joint about the axle axis. The
201- motor torque acts between the body and the two wheels: half the commanded
202- torque drives each wheel and the reaction acts on the body. Viscous motor
203- friction acts between the body and each wheel in the same split.
200+ at the center of the axle through a revolute joint about the axle axis. Each
201+ wheel has its own motor torque input, so the robot can be steered by applying
202+ a differential torque; each motor torque acts between the body and its wheel.
204203
205- For motion in the vertical plane the model is dynamically equivalent to
206- `PlanarDyadBot`: the two wheels share the planar model's wheel mass `m`, spin
207- inertia `Iw` and motor friction `d`, and the body parameters are identical.
204+ With the same torque applied to both wheels the robot moves in the vertical
205+ plane and is dynamically equivalent to `PlanarDyadBot`: the two wheels share
206+ the planar model's wheel mass `m`, spin inertia `Iw` and motor friction `d`,
207+ and the body parameters are identical.
208208
209- Inputs and outputs (same interface as `PlanarDyadBot`):
210- - `torque`: torque applied by the motor between body and wheels
211- - `x`, `x_dot`: position and velocity of the wheel axis along the ground
209+ Inputs and outputs:
210+ - `torque_left`, `torque_right`: motor torques applied between body and each wheel
211+ - `x`, `x_dot`: odometric path position and velocity (average wheel arc
212+ length; equals the distance driven along the — possibly curved — path, and
213+ coincides with the world-frame x coordinate when driving straight)
212214- `theta`, `theta_dot`: tilt angle and tilt rate of the body (zero when upright)
215+ - `yaw`, `yaw_dot`: heading angle and rate (positive counterclockwise seen from above)
213216
214217The model must be accompanied by a `MultibodyComponents.World` component in
215218the top level of the enclosing model.
@@ -219,22 +222,28 @@ component RollingDyadBot3D
219222 "Body tilt joint about the axle axis"
220223 pitch = MultibodyComponents.Revolute(n = [0, 0, 1]) {^pitch}
221224 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}
222- "Splits the motor torque equally between the two wheels"
223- gain_half = BlockComponents.Math.Gain(k = 0.5) {^gain_half}
224- torque_left = RotationalComponents.Sources.TorqueSource() {^torque_left}
225- torque_right = RotationalComponents.Sources.TorqueSource() {^torque_right}
226- damper_left = RotationalComponents.Components.Damper(d = d / 2) {^damper_left}
227- damper_right = RotationalComponents.Components.Damper(d = d / 2) {^damper_right}
228- "Motor torque command"
229- torque = Dyad.RealInput() {^torque}
230- "Position of the wheel axis"
225+ # wheels.axis1 is the wheel on the +z side of the axle, which is the right
226+ # wheel when facing the forward x direction with y up
227+ torquesource1 = RotationalComponents.Sources.TorqueSource() {^torquesource1}
228+ torquesource2 = RotationalComponents.Sources.TorqueSource() {^torquesource2}
229+ damper1 = RotationalComponents.Components.Damper(d = d / 2) {^damper1}
230+ damper2 = RotationalComponents.Components.Damper(d = d / 2) {^damper2}
231+ "Left-wheel motor torque command"
232+ torque_left = Dyad.RealInput() {^torque_left}
233+ "Right-wheel motor torque command"
234+ torque_right = Dyad.RealInput() {^torque_right}
235+ "Odometric path position (average wheel arc length)"
231236 x = Dyad.RealOutput() {^x}
232237 "Body tilt angle"
233238 theta = Dyad.RealOutput() {^theta}
234- "Velocity of the wheel axis "
239+ "Odometric path velocity "
235240 x_dot = Dyad.RealOutput() {^x_dot}
236241 "Body tilt rate"
237242 theta_dot = Dyad.RealOutput() {^theta_dot}
243+ "Yaw (heading) angle"
244+ yaw = Dyad.RealOutput() {^yaw}
245+ "Yaw rate"
246+ yaw_dot = Dyad.RealOutput() {^yaw_dot}
238247 "Body mass"
239248 parameter M::Mass = 0.1
240249 "Total wheel mass (split between the two wheels)"
@@ -256,19 +265,23 @@ component RollingDyadBot3D
256265relations
257266 initial pitch.phi = phi0
258267 initial pitch.w = 0
259- x = wheels.x
268+ # Wheel spin and forward position have opposite signs (positive wheel torque
269+ # drives the robot toward negative x, like in PlanarDyadBot)
270+ x = -R * (wheels.theta1 + wheels.theta2) / 2
271+ x_dot = -R * (wheels.der_theta1 + wheels.der_theta2) / 2
260272 theta = pitch.phi
261- x_dot = der(wheels.x)
262273 theta_dot = pitch.w
274+ yaw = wheels.phi
275+ yaw_dot = der(wheels.phi)
263276 connect(body.frame_a, pitch.frame_b) {^id14}
264277 connect(pitch.frame_a, wheels.frame_middle) {^id15}
265- connect(torque_left .spline, wheels.axis1) {^id16}
266- connect(torque_right .spline, wheels.axis2) {^id17}
267- connect(damper_right .spline_b, wheels.axis2) {^id18}
268- connect(damper_left .spline_b, wheels.axis1) {^id19}
269- connect(torque_left.tau, torque_right.tau, gain_half.y ) {^id20}
270- connect(torque, gain_half.u ) {^id21}
271- connect(pitch.axis, torque_left .support, damper_left .spline_a, torque_right .support, damper_right .spline_a) {^id22}
278+ connect(torquesource1 .spline, wheels.axis1) {^id16}
279+ connect(torquesource2 .spline, wheels.axis2) {^id17}
280+ connect(damper2 .spline_b, wheels.axis2) {^id18}
281+ connect(damper1 .spline_b, wheels.axis1) {^id19}
282+ connect(torque_right, torquesource1.tau ) {^id20}
283+ connect(torque_left, torquesource2.tau ) {^id21}
284+ connect(pitch.axis, torquesource1 .support, damper1 .spline_a, torquesource2 .support, damper2 .spline_a) {^id22}
272285metadata {
273286 "Dyad": {"icons": {"default": "dyad://DyadBotComponents/DyadBot3D.svg"}},
274287 "_links": {
@@ -290,31 +303,34 @@ metadata {
290303 "placement": {"diagram": {"x1": 350, "y1": -10, "x2": 650, "y2": 290, "rot": 270}}
291304 }
292305 },
293- "gain_half": {
294- "Dyad": {
295- "placement": {"diagram": {"x1": 70, "y1": 650, "x2": 150, "y2": 730, "rot": 90}}
296- }
297- },
298- "torque_left": {
306+ "torquesource1": {
299307 "Dyad": {"placement": {"diagram": {"x1": 240, "y1": 670, "x2": 340, "y2": 570}}}
300308 },
301- "torque_right ": {
309+ "torquesource2 ": {
302310 "Dyad": {"placement": {"diagram": {"x1": 770, "y1": 670, "x2": 670, "y2": 570}}}
303311 },
304- "damper_left ": {
312+ "damper1 ": {
305313 "Dyad": {
306314 "placement": {"diagram": {"x1": 350, "y1": 490, "x2": 450, "y2": 590, "rot": 90}}
307315 }
308316 },
309- "damper_right ": {
317+ "damper2 ": {
310318 "Dyad": {
311319 "placement": {"diagram": {"x1": 650, "y1": 490, "x2": 550, "y2": 590, "rot": 270}}
312320 }
313321 },
314- "torque": {
322+ "torque_right": {
323+ "Dyad": {
324+ "placement": {
325+ "diagram": {"iconName": "default", "x1": 40, "y1": 580, "x2": 120, "y2": 660, "rot": 0}
326+ },
327+ "tags": []
328+ }
329+ },
330+ "torque_left": {
315331 "Dyad": {
316332 "placement": {
317- "diagram": {"iconName": "default", "x1": 20 , "y1": 460 , "x2": 100 , "y2": 540 , "rot": 0}
333+ "diagram": {"iconName": "default", "x1": 40 , "y1": 720 , "x2": 120 , "y2": 800 , "rot": 0}
318334 },
319335 "tags": []
320336 }
@@ -351,6 +367,22 @@ metadata {
351367 "tags": []
352368 }
353369 },
370+ "yaw": {
371+ "Dyad": {
372+ "placement": {
373+ "diagram": {"iconName": "default", "x1": 880, "y1": 780, "x2": 960, "y2": 860, "rot": 0}
374+ },
375+ "tags": []
376+ }
377+ },
378+ "yaw_dot": {
379+ "Dyad": {
380+ "placement": {
381+ "diagram": {"iconName": "default", "x1": 880, "y1": 900, "x2": 960, "y2": 980, "rot": 0}
382+ },
383+ "tags": []
384+ }
385+ },
354386 "id14": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
355387 "id15": {
356388 "Dyad": {
@@ -372,21 +404,23 @@ metadata {
372404 "id17": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
373405 "id18": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
374406 "id19": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
375- "id20": {
407+ "id20": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
408+ "id21": {
376409 "Dyad": {
377410 "edges": [
378- {"S": 1, "M": [{"x": 210, "y": 620}], "E": -1},
379411 {
380- "S": -1,
381- "M": [{"x": 210, "y": 880}, {"x": 800, "y": 880}, {"x": 800, "y": 620}],
412+ "S": 1,
413+ "M": [
414+ {"x": 160, "y": 760},
415+ {"x": 160, "y": 900},
416+ {"x": 820, "y": 900},
417+ {"x": 820, "y": 620}
418+ ],
382419 "E": 2
383- },
384- {"S": 3, "M": [{"x": 110, "y": 880}], "E": -1}
385- ],
386- "junctions": [{"x": 210, "y": 880}]
420+ }
421+ ]
387422 }
388423 },
389- "id21": {"Dyad": {"edges": [{"S": 1, "M": [{"x": 110, "y": 500}], "E": 2}]}},
390424 "id22": {
391425 "Dyad": {
392426 "edges": [
0 commit comments