@@ -190,6 +190,212 @@ metadata {
190190}
191191end
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"""
194400Three-dimensional model of a two-wheeled balancing robot with ideal rolling
195401contact.
0 commit comments