Boom-Monopod Model¶
The boom-monopod model simulates elements of legged locomotion using a single-legged hopping device on the end of a planarizing boom.
The base of the joint includes an unactuated universal joint which allow vertical boom motion and ‘forward’ travel in a circle around the base. In the reference pose, the boom is slanted to keep the base pivots low to the floor. Placing the pivot center near the contact plane minimizes radial foot motion during stance. At the end of boom is a nominally horizontal axis to allow body pitch.
In this implementation, the body pitch axis is actuated to allow controlling the leg angle with respect to the floor. This makes control much easier but diverges from an approximation of free locomotion.
The default controller produces a hopping motion at controlled speed. The leg spring is simulated using a linear leg actuator in force control mode. Constant thrust is provided by changing the spring setpoint during the takeoff portion of the gait cycle. The speed control applies linear feedback to the leg angle to adjust estimated speed to the reference speed.
Note: simulating the spring in the controller requires setting the WorldInfo basicTimeStep lower than normal. An alternate strategy might be to tune the PID control to simulate the physical leg spring.
This model is demonstrated in the boom-monopod.wbt
world available within the
Webots.zip archive.
Boom Monopod Proto¶
1#VRML_SIM R2023b utf8
2# Actuated monopod on the end of a passive boom. The boom base has NULL physics
3# so it will be fixed in place.
4# documentation url: https://courses.ideate.cmu.edu/16-375
5# license: No copyright, 2020-2024 Garth Zeglin. This file is explicitly placed in the public domain.
6# template language: javascript
7PROTO boom-monopod [
8 field SFVec3f translation 0 0 0
9 field SFRotation rotation 0 1 0 0
10 field SFFloat boomLength 1.0
11 field SFFloat legLength 0.2
12 field SFString controller "boom_monopod"
13 field SFString name "monopod"
14 field SFString customData ""
15]
16{
17 Robot {
18 # connect properties to user-visible data fields
19 translation IS translation
20 rotation IS rotation
21 controller IS controller
22 name IS name
23 customData IS customData
24
25 # calculate derived parameters
26 %<
27 let boomLength = fields.boomLength.value;
28 let legLength = fields.legLength.value;
29 let baseCylinderHeight = 0.050;
30 let boomYHeight = baseCylinderHeight + 0.050;
31 let halfBoomLen = 0.5*boomLength;
32 let halfLegLength = 0.5*legLength;
33 let boomRise = legLength - boomYHeight + 0.1050;
34 let boomAngle = Math.asin(boomRise/boomLength);
35 >%
36
37 # define the kinematic tree
38 children [
39
40 # add a default radio receiver and transmitter
41 Receiver {
42 channel 1
43 }
44 Emitter {
45 channel 1
46 }
47
48 # cylindrical base shape
49 Pose {
50 translation 0 0 %<= 0.5*baseCylinderHeight >%
51 children [
52 Shape {
53 appearance DEF boomAppearance PBRAppearance {
54 baseColor 0.21529 0.543008 0.99855
55 metalness 0.5
56 roughness 0.5
57 }
58 geometry Cylinder {
59 height %<= baseCylinderHeight >%
60 radius 0.2
61 }
62 }
63 ]
64 }
65 # define the base pivot joint connecting the base
66 # and the first link
67 HingeJoint {
68 jointParameters HingeJointParameters {
69 axis 0 0 1
70 }
71 device [
72 PositionSensor {
73 name "boom_z"
74 }
75 ]
76 # define the body in the center of the base universal joint
77 endPoint Solid {
78 translation 0 0 %<= baseCylinderHeight >%
79 physics Physics { }
80 # define a body to represent the joint center
81 children [
82 DEF blockShape Pose {
83 translation 0 0 0.025
84 children [
85 Shape {
86 appearance USE boomAppearance
87 geometry Box {
88 size 0.1 0.1 0.050
89 }
90 }
91 ]
92 }
93
94 # define the second axis of the base joint
95 HingeJoint {
96 jointParameters HingeJointParameters {
97 axis 0 1 0
98 anchor 0 0 0.050
99 }
100 device [
101 PositionSensor {
102 name "boom_y"
103 }
104 ]
105 # start definition of the boom link, with coordinates rotated so +X is along
106 # the tilted slope of the boom
107 endPoint Solid {
108 translation 0 0 0.050
109 rotation 0 -1 0 %<= boomAngle >%
110 physics Physics { density 300 }
111 children [
112 # boom cylinder shape
113 DEF boomShape Pose {
114 translation %<= halfBoomLen >% 0 0.0
115 rotation 0 1 0 1.5708
116 children [
117 Shape {
118 appearance USE boomAppearance
119 geometry Cylinder {
120 radius 0.025
121 height %<= fields.boomLength.value >%
122 }
123 }
124 ]
125 }
126 # define the boom_x joint at the end of the boom for body pitch rotation
127 # the axis is rotated so the +X is once again level
128 HingeJoint {
129 jointParameters HingeJointParameters {
130 axis %<= Math.cos(boomAngle) >% 0 %<= -Math.sin(boomAngle) >%
131 anchor %<= fields.boomLength.value >% 0 0
132 }
133 device [
134 PositionSensor {
135 name "boom_x"
136 }
137 RotationalMotor {
138 name "pitch"
139 }
140 ]
141 # define the block at the end of the boom representing the body mass
142 endPoint Solid {
143 translation %<= fields.boomLength.value >% 0 0
144 rotation 0 1 0 %<= boomAngle >%
145 physics Physics { density 300 }
146 children [
147 DEF boomEndShape Pose {
148 translation 0.050 0 0
149 children [
150 Shape {
151 appearance USE boomAppearance
152 geometry Box {
153 size 0.100 0.100 0.050
154 }
155 }
156 ]
157 } # end boomEndShape Pose
158
159 # define the 'monopod' vertical leg joint as a slider
160 # positive displacement lengthens the leg
161 SliderJoint {
162 jointParameters JointParameters {
163 axis 0 0 -1
164 minStop -0.2
165 maxStop 0.0
166 }
167 device [
168 PositionSensor {
169 name "leg_encoder"
170 }
171 LinearMotor {
172 name "leg_actuator"
173 maxForce 100
174 }
175 ]
176 # define the monopod leg and foot
177 # the coordinates are rotated so Z is vertical w.r.t world
178 endPoint Solid {
179 translation 0.1 0 0
180 physics Physics { density 200 }
181 children [
182 # vertical leg cylinder
183 Pose {
184 # translation 0 0 %<= -0.25 * legLength >%
185 children [
186 Shape {
187 appearance DEF legAppearance PBRAppearance {
188 baseColor 0.413001 1 0.33489
189 metalness 0.5
190 }
191 geometry Cylinder {
192 height %<= 2.0*legLength >%
193 radius 0.020
194 }
195 } # end leg cylinder Shape
196 ] # end children of leg Pose
197 } # end leg Pose
198 # smaller foot cylinder, turned on side
199 DEF footShape Pose {
200 translation 0 0 %<= -legLength - 0.050 >%
201 rotation 0 1 0 1.5708
202 children [
203 Shape {
204 appearance USE legAppearance
205
206 geometry Cylinder {
207 height 0.030
208 radius 0.050
209 }
210 } # end foot Shape
211 ] # end children of foot Pose
212 } # end foot Pose
213 ] # end children of monopod endPoint Solid
214 boundingObject USE footShape
215 } # end endPoint Solid for monopod
216 } # end monopod leg SliderJoint
217 ] # end children of endPoint Solid for boom end block
218 boundingObject USE boomEndShape
219 } # end endPoint Solid for boom end block
220 } # end boom_x HingeJoint
221 ] # end children of boom_y endpoint Solid
222 boundingObject USE boomShape
223 } # end boom_y endpoint Solid
224 } # end boom_y HingeJoint
225 ] # end children of boom_z endpoint Solid
226 boundingObject USE blockShape
227 } # end boom_z endpoint Solid
228 } # end boom_z HingeJoint
229 ] # end children of Robot
230 } # end Robot
231} # end body of proto
boom_monopod.py¶
1# boom_monopod
2# Copyright, 2024, Garth Zeglin.
3# Sample Webots controller file for driving a monopod mounted on a passive boom.
4
5print("boom_monopod.py waking up.")
6
7# Import standard Python libraries.
8import math
9
10# Import the Webots simulator API.
11from controller import Robot
12
13# Define the time step in milliseconds between controller updates.
14EVENT_LOOP_DT = 5
15
16################################################################
17class Monopod(Robot):
18 def __init__(self):
19
20 super(Monopod, self).__init__()
21 self.robot_name = self.getName()
22 print("%s: controller connected." % (self.robot_name))
23
24 # Connect to the radio receiver.
25 self.receiver = self.getDevice('receiver')
26 self.receiver.enable(100) # milliseconds of sampling period
27 self.message_base_address = "/" + self.robot_name
28
29 # Fetch handles for the motors.
30 self.pitch_motor = self.getDevice('pitch')
31 self.leg_actuator = self.getDevice('leg_actuator')
32
33 # Use the leg linear actuator in force mode.
34 self.leg_actuator.setPosition(math.inf)
35 self.leg_actuator.setForce(0)
36
37 # The controller uses a fixed thrust on takeoff with no altitude control.
38 # The units are meters of leg spring compression.
39 self.thrust = 0.040
40
41 # Target speed in boom_z radians/sec.
42 self.speed = 1.0
43
44 # Fetch handles for the joint sensors.
45 self.boom_z = self.getDevice('boom_z')
46 self.boom_y = self.getDevice('boom_y')
47 self.boom_x = self.getDevice('boom_x')
48 self.leg_len = self.getDevice('leg_encoder')
49
50 # Enable the sensors at full cycle rate.
51 for s in [self.boom_z, self.boom_y, self.boom_x, self.leg_len]:
52 s.enable(EVENT_LOOP_DT)
53
54 # Initialize state estimation.
55 self.delta_t = EVENT_LOOP_DT * 0.001
56 self.sim_time = 0.0
57 self.b_x, self.b_y, self.b_z = (0.0, 0.0, 0.0)
58 self.b_xd, self.b_yd, self.b_zd = (0.0, 0.0, 0.0)
59 self.b_x_last, self.b_y_last, self.b_z_last = (0.0, 0.0, 0.0)
60 self.leg_length = 0.0
61
62 # The pitch motor is driven in position mode.
63 self.pitch_motor.setPosition(0.0)
64
65 #================================================================
66 def poll_sensors(self):
67 """Read sensors and update filters."""
68
69 # Read the boom angles to determine the spatial position. A positive b_z is
70 # forward travel, a positive b_y is foot to ground contact, a positive b_x
71 # is negative pitch, i.e. foot placed forward.
72 self.b_z = self.boom_z.getValue()
73 self.b_y = self.boom_y.getValue()
74 self.b_x = self.boom_x.getValue()
75
76 # Calculate velocities using finite difference.
77 self.b_zd = (self.b_z - self.b_z_last) / self.delta_t
78 self.b_yd = (self.b_y - self.b_y_last) / self.delta_t
79 self.b_xd = (self.b_x - self.b_x_last) / self.delta_t
80
81 self.b_x_last, self.b_y_last, self.b_z_last = (self.b_x, self.b_y, self.b_z)
82
83 # Read the leg length sensor as a step toward simulating a leg spring. Leg
84 # length is positive as the leg extends, i.e., is normally decreasing during
85 # landing and increasing during takeoff.
86 self.leg_length = self.leg_len.getValue()
87 return
88
89 #================================================================
90 def poll_control(self):
91 """Update control model. Note that the leg spring is simulated using a
92 force actuator, so this also computes the passive leg spring physics."""
93
94 # Choose a pitch angle proportional to the speed error.
95 self.pitch_motor.setPosition(-0.3 * (self.speed - self.b_zd))
96
97 # Superimpose thrust force during liftoff phase. The condition
98 # is that the foot is in contact but the boom velocity is upwards.
99 if self.b_y > 0.0 and self.b_yd < 0.0:
100 neutral = self.thrust
101 else:
102 neutral = 0.0
103
104 # Apply simulated spring force. Positive forces extend the leg, i.e., press
105 # the foot against the ground. A typical equilibrium force is about 6N
106 # representing the weight of the boom. The spring constant is chosen k = F / d
107 # for roughly 50 mm of compression at equilibrium.
108 spring_force = -120.0 * (self.leg_length - neutral)
109 self.leg_actuator.setForce(spring_force)
110
111 # print(f"boom: y:{b_y}, z:{b_z} leg_length: {leg_length} neutral: {neutral} force: {spring_force}")
112 return
113
114 #================================================================
115 def poll_receiver(self):
116 """Process all available radio messages."""
117 while self.receiver.getQueueLength() > 0:
118 packet = self.receiver.getString()
119 tokens = packet.split()
120 if len(tokens) >= 2:
121 if tokens[0].startswith(self.message_base_address):
122 if tokens[0] == self.message_base_address + '/speed':
123 self.speed = float(tokens[1])
124 print("%s: set speed to %f" % (robot_name, self.speed))
125 # done with packet processing, advance to the next packet
126 self.receiver.nextPacket()
127
128 #================================================================
129 def run(self):
130 # Run loop to execute a periodic script until the simulation quits.
131 # If the controller returns -1, the simulator is quitting.
132 while self.step(EVENT_LOOP_DT) != -1:
133 # Read simulator clock time.
134 self.sim_time = self.getTime()
135
136 # Process sensor values.
137 self.poll_sensors()
138
139 # Poll the simulated radio receiver.
140 self.poll_receiver()
141
142 # Update control.
143 self.poll_control()
144
145
146################################################################
147# Start the script.
148robot = Monopod()
149robot.run()