| 72 | | Also note that we used some values from the '''{{{settings.py}}}''' module. This module is just a simple way to centralise the configuration of our hexapod. |
| 73 | | |
| 74 | | For now, it should contain something like: |
| 75 | | |
| 76 | | {{{ |
| 77 | | #!python |
| 78 | | |
| 79 | | # -*- coding: utf-8 -*- |
| 80 | | |
| 81 | | from py4bot.common import config |
| 82 | | |
| 83 | | |
| 84 | | # Legs index |
| | 73 | Also note that we used some values from the '''{{{settings.py}}}''' module. This module is just a simple way to centralise the configuration of our hexapod. We will describe this module later. |
| | 74 | |
| | 75 | To drive a robot, we need a remote control. In this tutorial, we are going to use an old gamepad, a [http://www.thrustmaster.com/en_IN/products/firestorm-dual-analog-3 Thrustmaster Firestorm Dual Analog 3]. As I own such gamepad, I already added support in Py4bot: |
| | 76 | |
| | 77 | {{{ |
| | 78 | #!python |
| | 79 | |
| | 80 | class Gamepad(RemoteControl): |
| | 81 | def _createFrontend(self): |
| | 82 | return Thrustmaster("/dev/input/by-id/usb-Mega_World_Thrustmaster_dual_analog_3.2-event-joystick") |
| | 83 | |
| | 84 | def _buildComponents(self): |
| | 85 | self._addConfig("walk") |
| | 86 | self._addConfig("body") |
| | 87 | |
| | 88 | self._addComponent(Button, command=self.selectNextConfig, key="button_08", trigger="hold") |
| | 89 | self._addComponent(Button, command=self.selectPreviousConfig, key="button_09", trigger="hold") |
| | 90 | |
| | 91 | self._addComponent(Button, command=self.robot.incBodyPosition, key="button_004", mapper=MapperSetValue(dz=+5)) |
| | 92 | self._addComponent(Button, command=self.robot.incBodyPosition, key="button_005", mapper=MapperSetValue(dz=-5)) |
| | 93 | self._addComponent(Joystick, configs="walk", command=GaitSequencer().walk, keys=("analog_02", "analog_03", "analog_00"), mapper=MapperWalk()) |
| | 94 | |
| | 95 | self._addComponent(Analog, configs="body", command=self.robot.setBodyExtraPosition, key="analog_00", mapper=MapperSetMultiply('yaw', coef=-15)) |
| | 96 | self._addComponent(Analog, configs="body", command=self.robot.setBodyExtraPosition, key="analog_03", mapper=MapperSetMultiply('pitch', coef=-15)) |
| | 97 | self._addComponent(Analog, configs="body", command=self.robot.setBodyExtraPosition, key="analog_02", mapper=MapperSetMultiply('roll', coef=15)) |
| | 98 | }}} |
| | 99 | |
| | 100 | We won't deep into details, here; refer to the [[documentation]] for more informations about remote controls. This is an important part of the framework, and you can do very smart things with it, like like having several configurations depending on the mode you want, using several remotes on the same robot... |
| | 101 | |
| | 102 | Now, let's create our robot: |
| | 103 | |
| | 104 | {{{ |
| | 105 | #!python |
| | 106 | |
| | 107 | def main(): |
| | 108 | def addGait(gaitClass, gaitName): |
| | 109 | gait = gaitClass(gaitName, settings.GAIT_LEGS_GROUPS[gaitName], settings.GAIT_PARAMS[gaitName]) |
| | 110 | GaitManager().add(gait) |
| | 111 | |
| | 112 | addGait(GaitTripod, "tripod") |
| | 113 | addGait(GaitTetrapod, "tetrapod") |
| | 114 | addGait(GaitRiple, "riple") |
| | 115 | addGait(GaitWave, "metachronal") |
| | 116 | addGait(GaitWave, "wave") |
| | 117 | GaitManager().select("riple") |
| | 118 | |
| | 119 | robot = Hexapod() |
| | 120 | |
| | 121 | remote = Gamepad(robot) |
| | 122 | |
| | 123 | GaitSequencer().start() |
| | 124 | remote.start() |
| | 125 | |
| | 126 | robot.setBodyPosition(z=30) |
| | 127 | |
| | 128 | robot.mainLoop() |
| | 129 | |
| | 130 | remote.stop() |
| | 131 | remote.join() |
| | 132 | GaitSequencer().stop() |
| | 133 | GaitSequencer().join() |
| | 134 | |
| | 135 | |
| | 136 | if __name__ == "__main__": |
| | 137 | main() |
| | 138 | }}} |
| | 139 | |
| | 140 | In addition to the robot itself, we added a few pre-defined gaits, in order to make it walk. |
| | 141 | |
| | 142 | === Settings === |
| | 143 | |
| | 144 | Let's discuss about settings used in the previous part. See the [[documentation]] for the body/legs geometry conventions used. |
| | 145 | |
| | 146 | {{{ |
| | 147 | #!python |
| | 148 | |
| 107 | | |
| 108 | | # Legs (feet) neutral position (femur horizontal, tibia vertical) |
| 109 | | LEGS_NEUTRAL = { |
| 110 | | 'RM': { |
| 111 | | 'l': LEGS_GEOMETRY['RM']['coxa'] + LEGS_GEOMETRY['RM']['femur'], |
| 112 | | 'z': LEGS_GEOMETRY['RM']['tibia'] |
| 113 | | }, |
| 114 | | 'RF': { |
| 115 | | 'l': LEGS_GEOMETRY['RF']['coxa'] + LEGS_GEOMETRY['RF']['femur'], |
| 116 | | 'z': LEGS_GEOMETRY['RF']['tibia'] |
| 117 | | }, |
| 118 | | 'LF': { |
| 119 | | 'l': LEGS_GEOMETRY['LF']['coxa'] + LEGS_GEOMETRY['LF']['femur'], |
| 120 | | 'z': LEGS_GEOMETRY['LF']['tibia'] |
| 121 | | }, |
| 122 | | 'LM': { |
| 123 | | 'l': LEGS_GEOMETRY['LM']['coxa'] + LEGS_GEOMETRY['LM']['femur'], |
| 124 | | 'z': LEGS_GEOMETRY['LM']['tibia'] |
| 125 | | }, |
| 126 | | 'LR': { |
| 127 | | 'l': LEGS_GEOMETRY['LR']['coxa'] + LEGS_GEOMETRY['LR']['femur'], |
| 128 | | 'z': LEGS_GEOMETRY['LR']['tibia'] |
| 129 | | }, |
| 130 | | 'RR': { |
| 131 | | 'l': LEGS_GEOMETRY['RR']['coxa'] + LEGS_GEOMETRY['RR']['femur'], |
| 132 | | 'z': LEGS_GEOMETRY['RR']['tibia'] |
| 133 | | } |
| 134 | | } |
| 135 | | config.initNeutral(LEGS_INDEX, LEGS_NEUTRAL) |
| 136 | | |
| 137 | | # Legs / actuators mapping |
| 138 | | LEGS_ACTUATORS_MAPPING = { |
| | 181 | }}} |
| | 182 | |
| | 183 | {{{LEGS_ORIGIN}}} dict contains the positions and orientation of the origin of the legs: {{{(x, y)}}} defines the center of rotation of '''coxa''' joint, and {{{gamma0}}} is the angle of the legs at neutral position. |
| | 184 | |
| | 185 | Note that we need to call the {{{initOrigin()}}} function of the '''{{{config.py}}}''' module, in order to initialize internal configuration (some matrix are defined, in order to avoid further maths). |
| | 186 | |
| | 187 | {{{ |
| | 188 | #!python |
| | 189 | |
| | 190 | LEGS_FEET_NEUTRAL = { |
| | 191 | 'RM': LEGS_GEOMETRY['RM']['coxa'] + LEGS_GEOMETRY['RM']['femur'], |
| | 192 | 'RF': LEGS_GEOMETRY['RF']['coxa'] + LEGS_GEOMETRY['RF']['femur'], |
| | 193 | 'LF': LEGS_GEOMETRY['LF']['coxa'] + LEGS_GEOMETRY['LF']['femur'], |
| | 194 | 'LM': LEGS_GEOMETRY['LM']['coxa'] + LEGS_GEOMETRY['LM']['femur'], |
| | 195 | 'LR': LEGS_GEOMETRY['LR']['coxa'] + LEGS_GEOMETRY['LR']['femur'], |
| | 196 | 'RR': LEGS_GEOMETRY['RR']['coxa'] + LEGS_GEOMETRY['RR']['femur'], |
| | 197 | } |
| | 198 | config.initFeetNeutral(LEGS_INDEX, LEGS_FEET_NEUTRAL) |
| | 199 | }}} |
| | 200 | |
| | 201 | {{{LEGS_FEET_NEUTRAL}}} dict contains the feet neutral positions af all legs. |
| | 202 | |
| | 203 | Here too, we need to call the {{{initFeetNeutral()}}} function of the '''{{{config.py}}}''' module, in order to initialize internal configuration. |
| | 204 | |
| | 205 | {{{ |
| | 206 | #!python |
| | 207 | |
| | 208 | LEGS_SERVOS_MAPPING = { |
| 146 | | }}} |
| 147 | | |
| 148 | | Some of these values seem obvious, some don't; we'll discuss all this later. |
| 149 | | |
| 150 | | Now, let's create our robot. Still in '''{{{hexapod.py}}}''', add: |
| 151 | | |
| 152 | | {{{ |
| 153 | | #!python |
| 154 | | |
| 155 | | def main(): |
| 156 | | robot = Hexapod() |
| 157 | | }}} |
| 158 | | |
| 159 | | The main interrest of a multi-legs robot is to make it walk! But by default, the {{{Robot}}} class does not know any '''gait''', so, we have to teach it how to actually walk. |
| 160 | | |
| 161 | | Again, as Py4bot is a framework, it comes with some pre-defined gaits. So, all we have to do is to tell our robot which gaits to use (at least one), and give some params: |
| 162 | | |
| 163 | | {{{ |
| 164 | | #!python |
| 165 | | |
| 166 | | gait = GaitRiple("riple", settings.GAIT_LEGS_GROUPS, settings.GAIT_PARAMS) |
| 167 | | GaitManager().add(gait) |
| 168 | | }}} |
| 169 | | |
| 170 | | Let's see what wee need to add in the '''{{{settings.py}}}''' module: |
| 171 | | |
| 172 | | {{{ |
| 173 | | #!python |
| 174 | | |
| 175 | | # Gaits |
| 176 | | GAIT_LEGS_GROUPS = (('RR',), ('LM',), ('RF',), ('LR',), ('RM',), ('LF',)) |
| | 216 | |
| | 217 | SERVOS_CALIBRATION = { |
| | 218 | 0: {'offset': 0., 'pulse90': 1500, 'ratio': 1000/90.}, # coxa leg RF |
| | 219 | 1: {'offset': -90., 'pulse90': 1500, 'ratio': 1000/90.}, # femur leg RF |
| | 220 | 2: {'offset': 0., 'pulse90': 1500, 'ratio': 1000/90.}, # tibia leg RF |
| | 221 | |
| | 222 | 4: {'offset': 0., 'pulse90': 1500, 'ratio': 1000/90.}, # coxa leg RM |
| | 223 | 5: {'offset': -90., 'pulse90': 1500, 'ratio': 1000/90.}, # femur leg RM |
| | 224 | 6: {'offset': 0., 'pulse90': 1500, 'ratio': 1000/90.}, # tibia leg RM |
| | 225 | |
| | 226 | 8: {'offset': 0., 'pulse90': 1500, 'ratio': 1000/90.}, # coxa leg RR |
| | 227 | 9: {'offset': -90., 'pulse90': 1500, 'ratio': 1000/90.}, # femur leg RR |
| | 228 | 10: {'offset': 0., 'pulse90': 1500, 'ratio': 1000/90.}, # tibia leg RR |
| | 229 | |
| | 230 | 15: {'offset': 0., 'pulse90': 1500, 'ratio': 1000/90.}, # coxa leg LR |
| | 231 | 14: {'offset': -90., 'pulse90': 1500, 'ratio': 1000/90.}, # femur leg LR |
| | 232 | 13: {'offset': 0., 'pulse90': 1500, 'ratio': 1000/90.}, # tibia leg LR |
| | 233 | |
| | 234 | 19: {'offset': 0., 'pulse90': 1505, 'ratio': 1000/90.}, # coxa leg LM |
| | 235 | 18: {'offset': -90., 'pulse90': 1500, 'ratio': 1000/90.}, # femur leg LM |
| | 236 | 17: {'offset': 0., 'pulse90': 1500, 'ratio': 1000/90.}, # tibia leg LM |
| | 237 | |
| | 238 | 23: {'offset': 0., 'pulse90': 1500, 'ratio': 1000/90.}, # coxa leg LF |
| | 239 | 22: {'offset': -90., 'pulse90': 1500, 'ratio': 1000/90.}, # femur leg LF |
| | 240 | 21: {'offset': 0., 'pulse90': 1500, 'ratio': 1000/90.}, # tibia leg LF |
| | 241 | } |
| | 242 | }}} |
| | 243 | |
| | 244 | {{{LEGS_SERVOS_MAPPING}}} is a table to map all joints to actuators nums. |
| | 245 | |
| | 246 | {{{SERVOS_CALIBRATION}}} contains some calibration: |
| | 247 | |
| | 248 | * {{{offset}}} is the angle between the servo reference and the real angle. See the [[FAQ#Howisgeometrydefined|FAQ]] for the real angles definition; |
| | 249 | * {{{pulse90}}} is the pulse value for the neutral position of the servo, which is usually 90°; |
| | 250 | * {{{ratio}}} is the pulse width per degree. |
| | 251 | |
| | 252 | Offsets may vary, depending how you mount the servos. Same, ratio sign may have to be inverted on one side, if you have a symetrical design; all angle are always computed using trigonometric direction (CCW). |
| | 253 | |
| | 254 | See the [[UserGuideGit#Servoscalibration|user guide]] how to fine tune servos calibration. |
| | 255 | |
| | 256 | Finally: |
| | 257 | |
| | 258 | {{{ |
| | 259 | #!python |
| | 260 | |
| | 261 | GAIT_LEGS_GROUPS = { |
| | 262 | 'tripod': (('RM', 'LF', 'LR'), ('RF', 'LM', 'RR')), |
| | 263 | 'tetrapod': (('RR', 'LM'), ('RF', 'LR'), ('RM', 'LF')), |
| | 264 | 'riple': (('RR',), ('LM',), ('RF',), ('LR',), ('RM',), ('LF',)), |
| | 265 | 'metachronal': (('RR',), ('LM',), ('RF',), ('LR',), ('RM',), ('LF',)), |
| | 266 | 'wave': (('RR',), ('RM',), ('RF',), ('LR',), ('LM',), ('LF',)) |
| | 267 | } |
| 179 | | 'length': 40., |
| 180 | | 'angle': 10., |
| 181 | | 'height': 30., |
| 182 | | 'minLength': 4., |
| 183 | | 'minAngle': 2., |
| 184 | | 'speedMin': 50., |
| 185 | | 'speedMax': 300. |
| 186 | | } |
| 187 | | }}} |
| 188 | | |
| 189 | | Final steps are: launch the gait sequencer, move a little bit the body position, ask the robot to start walking, and run the robot main loop: |
| 190 | | |
| 191 | | {{{ |
| 192 | | #!python |
| 193 | | |
| 194 | | GaitSequencer().start() |
| 195 | | |
| 196 | | robot.setBodyPosition(z=-30) |
| 197 | | |
| 198 | | GaitSequencer().walk(0.75, 90., 0., 1) |
| 199 | | |
| 200 | | robot.mainLoop(5) |
| 201 | | |
| 202 | | GaitSequencer().walkStop() |
| 203 | | time.sleep(5) |
| 204 | | |
| 205 | | GaitSequencer().stop() |
| 206 | | GaitSequencer().join() |
| 207 | | |
| 208 | | |
| 209 | | if __name__ == "__main__": |
| 210 | | main() |
| 211 | | }}} |
| 212 | | |
| 213 | | You should then see the robot walking for a few seconds: |
| 214 | | |
| 215 | | [[Image(tutorial_1.png, nolink)]] |
| 216 | | |
| 217 | | === Settings === |
| 218 | | |
| 219 | | Let's discuss about settings used in the previous part. See the [[FAQ#Bodylegsgeometry|FAQ]] for the Body/legs geometry conventions used. |
| 220 | | |
| 221 | | {{{ |
| 222 | | #!python |
| 223 | | |
| 224 | | LEGS_INDEX = ('RF', 'RM', 'RR', 'LR', 'LM', 'LF') |
| 225 | | }}} |
| 226 | | |
| 227 | | {{{LEGS_INDEX}}} contains the names used to define legs; they can be freely chosen, but these values must be used as keys for other params. |
| 228 | | |
| 229 | | {{{ |
| 230 | | #!python |
| 231 | | |
| 232 | | LEGS_GEOMETRY = { |
| 233 | | 'RM': {'coxa': 25, 'femur': 45, 'tibia': 50}, |
| 234 | | 'RF': {'coxa': 25, 'femur': 45, 'tibia': 50}, |
| 235 | | 'LF': {'coxa': 25, 'femur': 45, 'tibia': 50}, |
| 236 | | 'LM': {'coxa': 25, 'femur': 45, 'tibia': 50}, |
| 237 | | 'LR': {'coxa': 25, 'femur': 45, 'tibia': 50}, |
| 238 | | 'RR': {'coxa': 25, 'femur': 45, 'tibia': 50} |
| 239 | | } |
| 240 | | }}} |
| 241 | | |
| 242 | | {{{LEGS_GEOMETRY}}} dict contains the lengths of the differents parts of the legs, in mm. |
| 243 | | |
| 244 | | {{{ |
| 245 | | #!python |
| 246 | | |
| 247 | | LEGS_ORIGIN = { |
| 248 | | 'RM': {'x': 50., 'y': 0., 'gamma0' : 0.}, |
| 249 | | 'RF': {'x': 35., 'y': 80., 'gamma0' : 30.}, |
| 250 | | 'LF': {'x': -35., 'y': 80., 'gamma0' : 150.}, |
| 251 | | 'LM': {'x': -50., 'y': 0., 'gamma0' : 180.}, |
| 252 | | 'LR': {'x': -35., 'y': -80., 'gamma0' : 210.}, |
| 253 | | 'RR': {'x': 35., 'y': -80., 'gamma0' : 330.}, |
| 254 | | } |
| 255 | | config.initOrigin(LEGS_INDEX, LEGS_ORIGIN) |
| 256 | | }}} |
| 257 | | |
| 258 | | {{{LEGS_ORIGIN}}} dict contains the positions and orientation of the origin of the legs: {{{(x, y)}}} defines the center of rotation of '''coxa''' joint, and {{{gamma0}}} is the angle of the legs at neutral position. |
| 259 | | |
| 260 | | Note that we need to call the {{{initOrigin()}}} function of the '''{{{config.py}}}''' module, in order to initialize internal configuration (some matrix are defined, in order to avoid further maths). |
| 261 | | |
| 262 | | {{{ |
| 263 | | #!python |
| 264 | | |
| 265 | | LEGS_NEUTRAL = { |
| 266 | | 'RM': { |
| 267 | | 'l': LEGS_GEOMETRY['RM']['coxa'] + LEGS_GEOMETRY['RM']['femur'], |
| 268 | | 'z': LEGS_GEOMETRY['RM']['tibia'] |
| 269 | | }, |
| 270 | | 'RF': { |
| 271 | | 'l': LEGS_GEOMETRY['RF']['coxa'] + LEGS_GEOMETRY['RF']['femur'], |
| 272 | | 'z': LEGS_GEOMETRY['RF']['tibia'] |
| 273 | | }, |
| 274 | | 'LF': { |
| 275 | | 'l': LEGS_GEOMETRY['LF']['coxa'] + LEGS_GEOMETRY['LF']['femur'], |
| 276 | | 'z': LEGS_GEOMETRY['LF']['tibia'] |
| 277 | | }, |
| 278 | | 'LM': { |
| 279 | | 'l': LEGS_GEOMETRY['LM']['coxa'] + LEGS_GEOMETRY['LM']['femur'], |
| 280 | | 'z': LEGS_GEOMETRY['LM']['tibia'] |
| 281 | | }, |
| 282 | | 'LR': { |
| 283 | | 'l': LEGS_GEOMETRY['LR']['coxa'] + LEGS_GEOMETRY['LR']['femur'], |
| 284 | | 'z': LEGS_GEOMETRY['LR']['tibia'] |
| 285 | | }, |
| 286 | | 'RR': { |
| 287 | | 'l': LEGS_GEOMETRY['RR']['coxa'] + LEGS_GEOMETRY['RR']['femur'], |
| 288 | | 'z': LEGS_GEOMETRY['RR']['tibia'] |
| 289 | | } |
| 290 | | } |
| 291 | | config.initNeutral(LEGS_INDEX, LEGS_NEUTRAL) |
| 292 | | }}} |
| 293 | | |
| 294 | | {{{LEGS_NEUTRAL}}} dict contains the |
| 295 | | |
| 296 | | Here too, we need to call the {{{initNeutral()}}} function of the '''{{{config.py}}}''' module, in order to initialize internal configuration. |
| 297 | | |
| 298 | | {{{ |
| 299 | | #!python |
| 300 | | |
| 301 | | LEGS_ACTUATORS_MAPPING = { |
| 302 | | 'RF': {'coxa': 0, 'femur': 1, 'tibia': 2}, |
| 303 | | 'RM': {'coxa': 4, 'femur': 5, 'tibia': 6}, |
| 304 | | 'RR': {'coxa': 8, 'femur': 9, 'tibia': 10}, |
| 305 | | 'LR': {'coxa': 15, 'femur': 14, 'tibia': 13}, |
| 306 | | 'LM': {'coxa': 19, 'femur': 18, 'tibia': 17}, |
| 307 | | 'LF': {'coxa': 23, 'femur': 22, 'tibia': 21} |
| 308 | | } |
| 309 | | }}} |
| 310 | | |
| 311 | | {{{LEGS_ACTUATORS_MAPPING}}} dict contains a table to map all joints to actuators nums. |
| 312 | | |
| 313 | | {{{ |
| 314 | | #!python |
| 315 | | |
| 316 | | GAIT_LEGS_GROUPS = (('RR',), ('LM',), ('RF',), ('LR',), ('RM',), ('LF',)) |
| | 270 | 'tripod': {'length': 40., 'angle': 10., 'height': 40., 'minLength': 4., 'minAngle': 2., 'speedMin': 50., 'speedMax': 200.}, |
| | 271 | 'tetrapod': {'length': 40., 'angle': 10., 'height': 30., 'minLength': 4., 'minAngle': 2., 'speedMin': 50., 'speedMax': 200.}, |
| | 272 | 'riple': {'length': 40., 'angle': 10., 'height': 30., 'minLength': 4., 'minAngle': 2., 'speedMin': 50., 'speedMax': 300.}, |
| | 273 | 'metachronal': {'length': 40., 'angle': 10., 'height': 30., 'minLength': 4., 'minAngle': 2., 'speedMin': 50., 'speedMax': 200.}, |
| | 274 | 'wave': {'length': 40., 'angle': 10., 'height': 30., 'minLength': 4., 'minAngle': 2., 'speedMin': 50., 'speedMax': 200.} |
| | 275 | } |
| 352 | | |
| 353 | | In this tutorial, we are just going to add a remote control in order to drive our previous simulated robot. |
| 354 | | |
| 355 | | Let's use an old gamepad, a [http://www.thrustmaster.com/en_IN/products/firestorm-dual-analog-3 Thrustmaster Firestorm Dual Analog 3]. As I own such gamepad, I already added support in Py4bot. |
| 356 | | |
| 357 | | Add the following code between the {{{Hexapod}}} class, and the {{{main()}}} function: |
| 358 | | |
| 359 | | {{{ |
| 360 | | #!python |
| 361 | | |
| 362 | | class Gamepad(RemoteControl): |
| 363 | | def _createFrontend(self): |
| 364 | | return FrontendFactory().create("thrustmaster", path="/dev/input/by-id/usb-Mega_World_Thrustmaster_dual_analog_3.2-event-joystick") |
| 365 | | |
| 366 | | def _buildComponents(self): |
| 367 | | self._addComponent(Joystick, command=GaitSequencer().walk, keys=("analog_02", "analog_03", "analog_00"), mapper=MapperWalk()) |
| 368 | | self._addComponent(Button, command=GaitSequencer().walkStop, key="button_000") |
| 369 | | self._addComponent(Button, command=GaitSequencer().walkStep, key="button_003") |
| 370 | | self._addComponent(Button, command=self.robot.incBodyPosition, key="button_004", mapper=MapperSetValue(dz=+5)) |
| 371 | | self._addComponent(Button, command=self.robot.incBodyPosition, key="button_005", mapper=MapperSetValue(dz=-5)) |
| 372 | | }}} |