[[PageOutline(2-5, Table of Contents, floated)]] = Tutorial = First, you need to know that '''Py4bot''' is just a framework, not a ready-to-use software. So it is up to you to write the application matching your robot. But don't worry, it is very easy. The framework contains all base code to setup a working multi-legs robot. All we have to do is overwrite a few classes, and implement virtual methods. It means that you need to know how to code in Python, and what classes / methods are... If it is not the case, have a look [https://wiki.python.org/moin/BeginnersGuide/NonProgrammers here]. Before we dive into the tutorials, we need to install '''Py4bot''', as described in the [[InstallationGuide]]. It is strongly suggested to install from git, so we can keep it up-to-date very easily. Once it is done, we don't need to edit/modify the source code anymore; we will write our own code in a dedicated directory. == Vocabulary == First, lets define a few words. I tried to use commonly used vocabulary, and hope I did understand them correctly. Feel free to contact me if you find mistakes. * IK: Inverse Kinematic == Tutorial 1: First robot == ''Files for this tutorial are available in [https://framagit.org/fma38/Py4bot/tree/master/py4bot/examples/tutorial_1 py4bot/examples/tutorial_1/]. But don't just copy them here: write them from scratch, following the tutorial.'' First, we create a new empty dir in our home dir: {{{ $ mkdir tutorial_1 $ cd tutorial_1 }}} All usefull classes are available from the '''{{{api.py}}}''' module. So, we just have to import things from here. Let's create a '''{{{hexapod.py}}}''' file to put our code, and import what we are going to use in this tutorial: {{{ #!python # -*- coding: utf-8 -*- from py4bot.api import * }}} Now, let's create the robot! A multi-legs robot is mainly build from a body, and several legs. We also need to create an ''actuator pool'', which contains all legs joints actuators, in order to move them synchronized. In our example, actuators are standard servos, and we use the '''Veyron''' board, from '''DFRobot''', to drive them: {{{ #!python import settings class Hexapod(Robot): def _createBody(self): return Body(settings.LEGS_ORIGIN) def _createLegs(self): legs = {} legIk = {} for legIndex in settings.LEGS_INDEX: legs[legIndex] = Leg3Dof(legIndex, {'coxa': Coxa(), 'femur': Femur(), 'tibia': Tibia()}, settings.FEET_NEUTRAL[legIndex]) legIk[legIndex] = Leg3DofIk(settings.LEGS_GEOMETRY[legIndex]) return legs, legIk def _createActuatorPool(self): driver = Veyron() pool = ServoPool(driver) for leg in self._legs.values(): # Create joints actuators num = settings.LEGS_SERVOS_MAPPING[leg.index]['coxa'] servo = Servo(leg.coxa, num, **settings.SERVOS_CALIBRATION[num]) pool.add(servo) num = settings.LEGS_SERVOS_MAPPING[leg.index]['femur'] servo = Servo(leg.femur, num, **settings.SERVOS_CALIBRATION[num]) pool.add(servo) num = settings.LEGS_SERVOS_MAPPING[leg.index]['tibia'] servo = Servo(leg.tibia, num, **settings.SERVOS_CALIBRATION[num]) pool.add(servo) return pool }}} As you can see, we just implemented 3 virtual methods, {{{_createBody()}}}, {{{_createLegs()}}} and {{{_createActuatorPool()}}}. 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. 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: {{{ #!python class Gamepad(RemoteControl): def _createFrontend(self): return Thrustmaster(settings.THRUSTMASTER_PATH) def _buildComponents(self): self._addConfig("walk") self._addConfig("body") self._addComponent(Button, command=self.selectNextConfig, key="button_008", trigger="hold") self._addComponent(Button, command=self.selectPreviousConfig, key="button_009", trigger="hold") self._addComponent(Button, command=self.robot.incBodyPosition, key="button_004", mapper=MapperSetValue(dz=+5)) self._addComponent(Button, command=self.robot.incBodyPosition, key="button_005", mapper=MapperSetValue(dz=-5)) self._addComponent(Joystick, configs="walk", command=GaitSequencer().walk, keys=("analog_02", "analog_03", "analog_00"), mapper=MapperWalk()) self._addComponent(Analog, configs="body", command=self.robot.setBodyExtraPosition, key="analog_00", mapper=MapperSetMultiply('yaw', coef=-15)) self._addComponent(Analog, configs="body", command=self.robot.setBodyExtraPosition, key="analog_03", mapper=MapperSetMultiply('pitch', coef=-15)) self._addComponent(Analog, configs="body", command=self.robot.setBodyExtraPosition, key="analog_02", mapper=MapperSetMultiply('roll', coef=15)) }}} We won't deep into details, here; refer to the [[UserGuideGit#Controllers|documentation]] for more informations about remote controls. This is an important part of the framework, and you can do very smart things, like having several configurations, using several remotes for the same robot... Now, let's create our robot: {{{ #!python def main(): def addGait(gaitClass, gaitName): gait = gaitClass(gaitName, settings.GAIT_LEGS_GROUPS[gaitName], settings.GAIT_PARAMS[gaitName]) GaitManager().add(gait) addGait(GaitTripod, "tripod") addGait(GaitTetrapod, "tetrapod") addGait(GaitRiple, "riple") addGait(GaitWave, "metachronal") addGait(GaitWave, "wave") GaitManager().select("riple") robot = Hexapod() remote = Gamepad(robot) GaitSequencer().start() remote.start() robot.setBodyPosition(z=30) robot.mainLoop() remote.stop() remote.join() GaitSequencer().stop() GaitSequencer().join() if __name__ == "__main__": main() }}} First, we add a few pre-defined gaits, in order to make it walk. Then, we create the robot itself, the remote control, and start all this little world! === Settings === Let's discuss about settings used in the previous part. See the [[UserGuideGit#Geometrydefinition|user guide]] for the body/legs geometry conventions used. {{{ #!python LEGS_INDEX = ('RF', 'RM', 'RR', 'LR', 'LM', 'LF') }}} {{{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. {{{ #!python LEGS_GEOMETRY = { 'RM': {'coxa': 25, 'femur': 45, 'tibia': 65}, 'RF': {'coxa': 25, 'femur': 45, 'tibia': 65}, 'LF': {'coxa': 25, 'femur': 45, 'tibia': 65}, 'LM': {'coxa': 25, 'femur': 45, 'tibia': 65}, 'LR': {'coxa': 25, 'femur': 45, 'tibia': 65}, 'RR': {'coxa': 25, 'femur': 45, 'tibia': 65} } }}} {{{LEGS_GEOMETRY}}} dict contains the lengths of the different parts of the legs, in mm. {{{ #!python LEGS_ORIGIN = { 'RM': {'x': 50., 'y': 0., 'gamma0' : 0.}, 'RF': {'x': 35., 'y': 80., 'gamma0' : 30.}, 'LF': {'x': -35., 'y': 80., 'gamma0' : 150.}, 'LM': {'x': -50., 'y': 0., 'gamma0' : 180.}, 'LR': {'x': -35., 'y': -80., 'gamma0' : 210.}, 'RR': {'x': 35., 'y': -80., 'gamma0' : 330.}, } }}} {{{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. {{{ #!python FEET_NEUTRAL = { 'RM': LEGS_GEOMETRY['RM']['coxa'] + LEGS_GEOMETRY['RM']['femur'], 'RF': LEGS_GEOMETRY['RF']['coxa'] + LEGS_GEOMETRY['RF']['femur'], 'LF': LEGS_GEOMETRY['LF']['coxa'] + LEGS_GEOMETRY['LF']['femur'], 'LM': LEGS_GEOMETRY['LM']['coxa'] + LEGS_GEOMETRY['LM']['femur'], 'LR': LEGS_GEOMETRY['LR']['coxa'] + LEGS_GEOMETRY['LR']['femur'], 'RR': LEGS_GEOMETRY['RR']['coxa'] + LEGS_GEOMETRY['RR']['femur'], } }}} {{{FEET_NEUTRAL}}} dict contains the feet neutral positions af all legs. This is just the distance from the legs origins, in the ground plane, along leg X axis. {{{ #!python LEGS_SERVOS_MAPPING = { 'RF': {'coxa': 0, 'femur': 1, 'tibia': 2}, 'RM': {'coxa': 4, 'femur': 5, 'tibia': 6}, 'RR': {'coxa': 8, 'femur': 9, 'tibia': 10}, 'LR': {'coxa': 15, 'femur': 14, 'tibia': 13}, 'LM': {'coxa': 19, 'femur': 18, 'tibia': 17}, 'LF': {'coxa': 23, 'femur': 22, 'tibia': 21} } }}} {{{LEGS_SERVOS_MAPPING}}} is a table to map all joints to actuators nums. {{{ #!python SERVOS_CALIBRATION = { 0: {'offset': 0, 'neutral': 1500, 'ratio': 0.090}, # coxa leg RF 1: {'offset': 180, 'neutral': 1500, 'ratio': 0.090}, # femur leg RF 2: {'offset': 90, 'neutral': 1500, 'ratio': 0.090}, # tibia leg RF 4: {'offset': 0, 'neutral': 1500, 'ratio': 0.090}, # coxa leg RM 5: {'offset': 180, 'neutral': 1500, 'ratio': 0.090}, # femur leg RM 6: {'offset': 90, 'neutral': 1500, 'ratio': 0.090}, # tibia leg RM 8: {'offset': 0, 'neutral': 1500, 'ratio': 0.090}, # coxa leg RR 9: {'offset': 180, 'neutral': 1500, 'ratio': 0.090}, # femur leg RR 10: {'offset': 90, 'neutral': 1500, 'ratio': 0.090}, # tibia leg RR 15: {'offset': 0, 'neutral': 1500, 'ratio': 0.090}, # coxa leg LR 14: {'offset': 180, 'neutral': 1500, 'ratio': -0.090}, # femur leg LR 13: {'offset': 90, 'neutral': 1500, 'ratio': -0.090}, # tibia leg LR 19: {'offset': 0, 'neutral': 1500, 'ratio': 0.090}, # coxa leg LM 18: {'offset': 180, 'neutral': 1500, 'ratio': -0.090}, # femur leg LM 17: {'offset': 90, 'neutral': 1500, 'ratio': -0.090}, # tibia leg LM 23: {'offset': 0, 'neutral': 1500, 'ratio': 0.090}, # coxa leg LF 22: {'offset': 180, 'neutral': 1500, 'ratio': -0.090}, # femur leg LF 21: {'offset': 90, 'neutral': 1500, 'ratio': -0.090}, # tibia leg LF } }}} {{{SERVOS_CALIBRATION}}} contains some calibration: * {{{offset}}} is the angle between the servo reference and the real angle. See the [[FAQ#Howisgeometrydefined|FAQ]] for the real angles definition; * {{{neutral}}} is the pulse value for the neutral position of the servo; * {{{ratio}}} is angle per pulse width. Offsets may vary, depending how you mount the servos. Same, ratio sign may have to be inverted on one side, if you have a symmetrical design; all angle are always computed using trigonometric direction (CCW). See the [[UserGuideGit#Servoscalibration|user guide]] how to fine tune servos calibration. Finally: {{{ #!python GAIT_LEGS_GROUPS = { 'tripod': (('RM', 'LF', 'LR'), ('RF', 'LM', 'RR')), 'tetrapod': (('RR', 'LM'), ('RF', 'LR'), ('RM', 'LF')), 'riple': (('RR',), ('LM',), ('RF',), ('LR',), ('RM',), ('LF',)), 'metachronal': (('RR',), ('LM',), ('RF',), ('LR',), ('RM',), ('LF',)), 'wave': (('RR',), ('RM',), ('RF',), ('LR',), ('LM',), ('LF',)) } GAIT_PARAMS = { 'tripod': {'length': {'min': 4., 'max': 40.}, 'angle': {'min': 2., 'max': 10.}, 'height': {'min': 10, 'max': 40.}, 'speed': {'min': 50., 'max': 200.}}, 'tetrapod': {'length': {'min': 4., 'max': 40.}, 'angle': {'min': 2., 'max': 10.}, 'height': {'min': 10, 'max': 30.}, 'speed': {'min': 50., 'max': 200.}}, 'riple': {'length': {'min': 4., 'max': 40.}, 'angle': {'min': 2., 'max': 10.}, 'height': {'min': 10, 'max': 30.}, 'speed': {'min': 50., 'max': 300.}}, 'metachronal': {'length': {'min': 4., 'max': 40.}, 'angle': {'min': 2., 'max': 10.}, 'height': {'min': 10, 'max': 30.}, 'speed': {'min': 50., 'max': 200.}}, 'wave': {'length': {'min': 4., 'max': 40.}, 'angle': {'min': 2., 'max': 10.}, 'height': {'min': 10, 'max': 30.}, 'speed': {'min': 50., 'max': 200.}}, } }}} {{{GAIT_LEGS_GROUPS}}} contains the legs grouped together and controlled at the same time during the gait usage. Note that we need to define sequences, so don't forget the comma to define a tuple with a unique element. {{{GAIT_PARAMS}}} contains some additional gaits params: * {{{length}}} is the distance (range) each leg will move for an entire cycle when the robot translate at full speed; * {{{angle}}} is the angle (range) each leg will turn for an entire cycle when the robot rotate at full speed; * {{{height}}} is the height (range) the leg will lift up; * {{{speed}}} is the speed (range). These values must be adjusted for each robot, to avoid legs collisions. {{{ #!python THRUSTMASTER_PATH = "/dev/input/by-id/usb-Mega_World_Thrustmaster_dual_analog_3.2-event-joystick" }}} No need for further explanations. == Tutorial 2: Using the 3D simulator == ''Files for this tutorial are available in [https://framagit.org/fma38/Py4bot/tree/master/py4bot/examples/tutorial_2 py4bot/examples/tutorial_2/]'' Here, we will see how to use the 3D simulation stuff. As before, we create a new empty dir in our home dir: {{{ $ mkdir tutorial_2 $ cd tutorial_2 }}} {{{ #!python #!/usr/bin/env python # -*- coding: utf-8 -*- import math import visual from py4bot.api import * import settings }}} Nothing fancy, here. {{{ #!python class Hexapod3D(Robot3D): def _createBody(self): return Body(settings.LEGS_ORIGIN) def _createLegs(self): legs = {} legIk = {} for legIndex in settings.LEGS_INDEX: legs[legIndex] = Leg3Dof(legIndex, {'coxa': Coxa(), 'femur': Femur(), 'tibia': Tibia()}, settings.FEET_NEUTRAL[legIndex]) legIk[legIndex] = Leg3DofIk(settings.LEGS_GEOMETRY[legIndex]) return legs, legIk }}} Same as before, except that we inherits from {{{Robot3D}}}, instead of {{{Robot}}}. {{{ #!python def _createActuatorPool(self): driver = Driver3D() pool = ActuatorPool(driver) num = 0 for leg in self._legs.values(): # Create joints actuators coxa3D = Coxa3D(leg.coxa, num, settings.LEGS_GEOMETRY[leg.index]['coxa']) pool.add(coxa3D) num += 1 femur3D = Femur3D(leg.femur, num, settings.LEGS_GEOMETRY[leg.index]['femur']) pool.add(femur3D) num += 1 tibia3D = Tibia3D(leg.tibia, num, settings.LEGS_GEOMETRY[leg.index]['tibia']) pool.add(tibia3D) num += 1 # Init 3D view coxa3D.frame = self._body3D femur3D.frame = coxa3D tibia3D.frame = femur3D coxa3D.pos = (settings.LEGS_ORIGIN[leg.index]['x'], 0, -settings.LEGS_ORIGIN[leg.index]['y']) femur3D.pos = (settings.LEGS_GEOMETRY[leg.index]['coxa'], 0, 0) tibia3D.pos = (settings.LEGS_GEOMETRY[leg.index]['femur'], 0, 0) coxa3D.rotate(angle=math.radians(settings.LEGS_ORIGIN[leg.index]['gamma0']), axis=(0, 1, 0)) return pool }}} Here, we use specific 3D actuators: {{{Coxa3D}}}, {{{Femur3D}}}, {{{Tibia3D}}}; they all use the [http://vpython.org VPython] library to represent themselves on the screen. Note that we don't use a custom actuator mapping; nums are automatically incremented. We also need to configure the hierarchy, and the relative positions of the actuators. ''Note: this may change in the future; I'm working on a simpler way to do all this''. {{{ #!python class Gamepad(RemoteControl): def _createFrontend(self): return Thrustmaster(settings.THRUSTMASTER_PATH) def _buildComponents(self): self._addConfig("walk") self._addConfig("body") self._addComponent(Button, command=self.selectNextConfig, key="button_008", trigger="hold") self._addComponent(Button, command=self.selectPreviousConfig, key="button_009", trigger="hold") self._addComponent(Button, command=self.robot.incBodyPosition, key="button_004", mapper=MapperSetValue(dz=+5)) self._addComponent(Button, command=self.robot.incBodyPosition, key="button_005", mapper=MapperSetValue(dz=-5)) self._addComponent(Joystick, configs="walk", command=GaitSequencer().walk, keys=("analog_02", "analog_03", "analog_00"), mapper=MapperWalk()) self._addComponent(Analog, configs="body", command=self.robot.setBodyExtraPosition, key="analog_00", mapper=MapperSetMultiply('yaw', coef=-15)) self._addComponent(Analog, configs="body", command=self.robot.setBodyExtraPosition, key="analog_03", mapper=MapperSetMultiply('pitch', coef=-15)) self._addComponent(Analog, configs="body", command=self.robot.setBodyExtraPosition, key="analog_02", mapper=MapperSetMultiply('roll', coef=15)) }}} Same as before... {{{ #!python def main(): scene = visual.display(width=settings.SCENE_WIDTH, height=settings.SCENE_HEIGHT) ground = visual.box(axis=(0, 1, 0), pos=(0, 0, 0), length=0.1, height=500, width=500, color=visual.color.gray(0.75), opacity=0.5) def addGait(gaitClass, gaitName): gait = gaitClass(gaitName, settings.GAIT_LEGS_GROUPS[gaitName], settings.GAIT_PARAMS[gaitName]) GaitManager().add(gait) addGait(GaitTripod, "tripod") addGait(GaitTetrapod, "tetrapod") addGait(GaitRiple, "riple") addGait(GaitWave, "metachronal") addGait(GaitWave, "wave") GaitManager().select("riple") robot = Hexapod3D() remote = Gamepad(robot) GaitSequencer().start() remote.start() robot.setBodyPosition(z=30) robot.mainLoop() remote.stop() remote.join() GaitSequencer().stop() GaitSequencer().join() if __name__ == "__main__": main() }}} The only difference, here, is the scene size init, and the ground addition (optional). === Settings === {{{ #!python # -*- coding: utf-8 -*- from py4bot.common import config # Legs index LEGS_INDEX = ('RF', 'RM', 'RR', 'LR', 'LM', 'LF') # Legs geometry LEGS_GEOMETRY = { 'RM': {'coxa': 25, 'femur': 45, 'tibia': 65}, 'RF': {'coxa': 25, 'femur': 45, 'tibia': 65}, 'LF': {'coxa': 25, 'femur': 45, 'tibia': 65}, 'LM': {'coxa': 25, 'femur': 45, 'tibia': 65}, 'LR': {'coxa': 25, 'femur': 45, 'tibia': 65}, 'RR': {'coxa': 25, 'femur': 45, 'tibia': 65} } # Legs origin LEGS_ORIGIN = { 'RM': {'x': 35., 'y': 0., 'gamma0' : 0.}, 'RF': {'x': 35., 'y': 65., 'gamma0' : 30.}, 'LF': {'x': -35., 'y': 65., 'gamma0' : 150.}, 'LM': {'x': -35., 'y': 0., 'gamma0' : 180.}, 'LR': {'x': -35., 'y': -65., 'gamma0' : 210.}, 'RR': {'x': 35., 'y': -65., 'gamma0' : 330.}, } # Legs feet neutral position FEET_NEUTRAL = { 'RM': LEGS_GEOMETRY['RM']['coxa'] + LEGS_GEOMETRY['RM']['femur'], 'RF': LEGS_GEOMETRY['RF']['coxa'] + LEGS_GEOMETRY['RF']['femur'], 'LF': LEGS_GEOMETRY['LF']['coxa'] + LEGS_GEOMETRY['LF']['femur'], 'LM': LEGS_GEOMETRY['LM']['coxa'] + LEGS_GEOMETRY['LM']['femur'], 'LR': LEGS_GEOMETRY['LR']['coxa'] + LEGS_GEOMETRY['LR']['femur'], 'RR': LEGS_GEOMETRY['RR']['coxa'] + LEGS_GEOMETRY['RR']['femur'], } # Gaits GAIT_LEGS_GROUPS = { 'tripod': (('RM', 'LF', 'LR'), ('RF', 'LM', 'RR')), 'tetrapod': (('RR', 'LM'), ('RF', 'LR'), ('RM', 'LF')), 'riple': (('RR',), ('LM',), ('RF',), ('LR',), ('RM',), ('LF',)), 'metachronal': (('RR',), ('LM',), ('RF',), ('LR',), ('RM',), ('LF',)), 'wave': (('RR',), ('RM',), ('RF',), ('LR',), ('LM',), ('LF',)) } GAIT_PARAMS = { 'tripod': {'length': {'min': 4., 'max': 40.}, 'angle': {'min': 2., 'max': 10.}, 'height': {'min': 10, 'max': 40.}, 'speed': {'min': 50., 'max': 200.}}, 'tetrapod': {'length': {'min': 4., 'max': 40.}, 'angle': {'min': 2., 'max': 10.}, 'height': {'min': 10, 'max': 30.}, 'speed': {'min': 50., 'max': 200.}}, 'riple': {'length': {'min': 4., 'max': 40.}, 'angle': {'min': 2., 'max': 10.}, 'height': {'min': 10, 'max': 30.}, 'speed': {'min': 50., 'max': 300.}}, 'metachronal': {'length': {'min': 4., 'max': 40.}, 'angle': {'min': 2., 'max': 10.}, 'height': {'min': 10, 'max': 30.}, 'speed': {'min': 50., 'max': 200.}}, 'wave': {'length': {'min': 4., 'max': 40.}, 'angle': {'min': 2., 'max': 10.}, 'height': {'min': 10, 'max': 30.}, 'speed': {'min': 50., 'max': 200.}}, } # Gamepad path THRUSTMASTER_PATH = "/dev/input/by-id/usb-Mega_World_Thrustmaster_dual_analog_3.2-event-joystick" # VPython scene default size SCENE_WIDTH = 640 SCENE_HEIGHT = 480 }}} We removed the servos mapping/calibration, as we don't need them anymore, and we just added the scene default size. == Tutorial 3: Servos calibration == There are different reasons why we need to calibrate servos, depending on: * how servos are attached to the robot * what servos we actually use * how accurate servos are '''{{{py4bot-gui-servocal.py}}}''' script can be used to fine tune our servos calibration, and generate the {{{SERVOS_CALIBRATION}}} dict. [[Image(py4bot-gui-servocal.png, 300px)]] Most servos have a range of 180° (more or less), and their neutral position (mid-range) is around 1500µs. It is important to mount the servos on the robot so the we can reach the joints positions ranges we want. The following picture shows the usual joints positions when servos are at neutral, for a 3 DoF leg: [[Image(IK3DoF_neutral.png, 300px)]] Looking how '''Py4bot''' internally manages angles ([[UserGuideGit#a3DoFleg]]), we can see that matching joints positions are: * coxa = 0° * femur = 180° * tibia = 90° This means that there will be some offsets between the servo angles and the framework angles. Another thing to tune is the servo neutral position, to correct mechanical errors. Last thing to tune is the ratio between pulse variation and real servo displacement. This ratio varies a lot from a brand to another. And even for a same model, it varies from a servo to another. This is especially true for low cost servos. Offsets and ratios are defined in the {{{SERVO_CALIBRATION}}} table. Here is how to use '''{{{py4bot-gui-servocal.py}}}'''. We first need to enable the servo, by clicking the '''Enable''' checkbox. Then, we need to check if the servo rotates the right direction: increasing the pulse value should increase the joint angle (= make in turn in the trigonometric sense in the leg coordinate system). If it is not the case, we just click the '''Invert''' checkbox (usually, coxa joints are all the same, and other joints should be inverted for legs of one side, but it depends if there is a symmetry in the mechanics or not). Then, we can set the offset as shown above by moving the '''Offset''' slider. This has no impact on the servo position, but changes the table offset. Next, we need to fine tune the servo neutral position. To do this, we move the '''Neutral''' slider until we mechanically reach the correct neutral position. Once this is done, we can adjust the ratio. This step is a little bit harder, as we need to measure the angle the joint really moves. The best way is to move up to +-90°, as it is visually easy to see, but it is not always mechanically possible, and we can restrict the range to +-45°. But we have to keep in mind that the larger the angle is, the better the accuracy will be. So, we move the '''Test Angle''' slider to 45°. Then, we move the '''Ratio''' slider so the joint really reaches 45°. Finally, we can check if going to -45° moves the joint to the symmetrical position. We of course need to repeat the entire procedure for each servo/joint, which can be tedious for a 4 DoF hexapod :o/ But after that, our robot should walk better ;o) Note that if we already have a '''{{{settings.py}}}''' module, we can launch '''{{{py4bot-gui-servocal.py}}}''' from the directory containing that module in order to reload the previous values. And if the module contains a '''{{{LEGS_SERVOS_MAPPING}}}''', the script will show the legs/joints names. So, it is better to start writing this param, before launching the script. == Tutorial 4: Add support for a new Gamepad == ''Files for this tutorial are available in [https://framagit.org/fma38/Py4bot/tree/master/py4bot/examples/tutorial_4 py4bot/examples/tutorial_4/].'' Here, we will see how to create a custom input frontend for a new gamepad. Again, we create a new empty dir in our home dir: {{{ $ mkdir tutorial_4 $ cd tutorial_4 }}} ''TODO'' == Conclusion == Thanks for reading these tutorials! There are many additional things to do, and final implementation may change, according to feedback/suggestions I will get. But the core is there. Again, the goal of this framework is to provide a high level tool to build complete and powerfull applications to control multi-legs robots. Have a look at [https://framagit.org/fma38/Py4bot/tree/master/py4bot/examples/cronos py4bot/examples/cronos/], which contains the code for my 4DoF hexapod, and closely follows Py4bot devs.