We revive the hexapod. Part one

In a previous article, we shared our experience in creating a hexapod using 3D printing technology. Now we will talk about the software component, which allowed him to revive.

It was originally planned to present all the information in one article, but in the process of writing it became clear that such a presentation would be superficial and uninformative. Therefore, it was decided to write several articles with a more detailed presentation of the topic.

Hexapod device


Currently, the UNO R3 board with Wi-Fi ESP8266 is used as the main controller . In fact, this board with two controllers on board, interacting with each other via a UART interface.



Despite the fact that Uno has a rather limited amount of computing resources, it is enough to teach the robot how to perform basic commands:

  • straight line movement with a given speed and duration
  • circular movement to the left or right (turn in place)
  • take preset limb positions

The ESP8266 is responsible for organizing the wireless communication channel and serves as a gateway through which Uno receives control commands.

The robot can be controlled via the local network within the telnet session established with it or via a wired connection to the controller (for firmware or debugging). For convenience, we also wrote a simple android application that implements minimal interactive functionality for controlling the robot.



The figure below schematically shows the hexapod structure.



All servos are connected to the Multiservo Shield expansion card , which allows you to control 18 servos. Her communication with Arduino is via the IΒ²C bus. Therefore, even with 18 servos steering at the same time, almost all Arduino pins will remain free.
It should be noted that the expansion board has a connector for powering the connected servos. But the maximum allowable current for which the board is designed is about 10A, which is not enough to power the MG996R servo drives, the total maximum current consumption of which can exceed the specified value. Therefore, in our version, each servo was connected to a separate power line, bypassing the expansion board.

Hexapod management


The hexapod limb control logic is implemented in the program using the GeksaFoot class .

class GeksaFoot
class GeksaFoot {
private:
//         
  Vector3D m_p0;
//          
  Vector3D m_r0;
//  Multiservo,    
  Multiservo m_coxaServo;   //  
  Multiservo m_femoraServo; //  
  Multiservo m_tibiaServo;  //  
public:
  GeksaFoot(Vector3D p0,Vector3D r0);
//   
  void begin(int coxaPin, int femoraPin, int tibiaPin);
//   
  void end();   

//   

  void coxaAngle(int);      //      (-90 .. 90 )
  int coxaAngle();          //     

  void femoraAngle(int);    //       (-90 .. 90 )
  int femoraAngle();        //     

  void tibiaAngle(int);     //       (-90 .. 90 )
  int tibiaAngle();         //     

//         

  //         
  int getAngles(Vector3D p, int& coxaAngle, int& femoraAngle, int& tibiaAngle);
  //       
  int getPoint(int coxaAngle, int femoraAngle, int tibiaAngle, Vector3D& p);
};


The methods coxaAngle , femoraAngle , tibiaAngle allow you to set or find out the angle of rotation of an individual leg joint. The auxiliary methods getAngles and getPoint implement the calculation logic of direct and inverse kinematics, with which you can calculate the value of the leg angles for a given point in the space of its limb. Or vice versa, a space point for the current angle values.

The average position of each joint corresponds to a zero value of the angle, and the range of rotation of the joint lies in the range from -90 to 90 degrees.

The top level class is the Geksapod class. It implements the logic of the entire robot. Each hexapod leg is included in this class as a separate instance of the GeksaFoot class .

class geksapod
class Geksapod: public AJobManager {
  friend class MotionJob;
  friend CommandProcessorJob;
  //  
  GeksaFoot m_LeftFrontFoot;
  GeksaFoot m_LeftMidleFoot;
  GeksaFoot m_LeftBackFoot;
  GeksaFoot m_RigthFrontFoot;
  GeksaFoot m_RigthMidleFoot;
  GeksaFoot m_RigthBackFoot;
  // ,    
  MotionJob m_MotionJob;
private:
  //       
  //       
  //     
  int _setPose(int idx, int ca, int fa, int ta);
  int _setPose(int[FOOTS_COUNT][3]);  
  int _setPose(Vector3D points[FOOTS_COUNT]); 
protected:
  //          
  int setPose(int idx, int ca, int fa, int ta, int actionTime); 
  int setPose(int pose[FOOTS_COUNT][3], int actionTime);
  int setPose(int idx, Vector3D p, int actionTime);
  int setPose(Vector3D points[FOOTS_COUNT], int actionTime = 0);
  int setPose(int ca, int fa, int ta, int actionTime);
  //      
  void getPose(int idx, int& ca, int& fa, int& ta);
  void getPose(int pose[FOOTS_COUNT][3]);
  void getPose(int idx, Vector3D& p);
  void getPose(Vector3D points[FOOTS_COUNT]);
  //    
  int execute(Motion* pMotion);
public:
  Geksapod();
  void setup();
  //   
  int move(int speed, int time);    //   
  int rotate(int speed, int time);  //   
  void stop();                      //  
  //         
  int getAngles(int idx, Vector3D p, int& ca, int& fa, int& ta);
  int getPoint(int idx, int coxaAngle, int femoraAngle, int tibiaAngle, Vector3D& p);
  int getAngles(Vector3D points[FOOTS_COUNT], int pose[FOOTS_COUNT][3]);
  int getPoints(int pose[FOOTS_COUNT][3], Vector3D points[FOOTS_COUNT]);
};


The overloaded getPose and setPose methods are intended for internal use and allow you to get the current position of the limbs of the robot or set a new one. In this case, the position of the legs is set in the form of a set of values ​​of the rotation angles of each joint or as a set of coordinates of the end points of the limbs of the robot relative to its center.
For smooth limb movement when calling setPose methods, you can specify the time ( actionTime parameter ) after which the legs should reach the specified position.
The robot is controlled by the public methods move , rotate and stop .

Multitask emulation


The Geksapod class inherits the implementation of the AJobManager class and contains an instance of the MotionJob class , which in turn inherits from the AJob class . These classes allow you to implement the so-called non-preemptive multitasking, allowing you to abstract from the linearity of programs and perform several tasks at the same time.

class ajob
class AJob {
  friend class AJobManager;
private:
  AJobManager* m_pAJobManager;   
  AJob* mJobNext;                      
  unsigned long m_counter;         //    onRun
  unsigned long m_previousMillis; //     onRun
  unsigned long m_currentMillis;  //   
  unsigned long m_delayMillis;    //    onRun
  void run();
public:
  AJob(AJobManager*, unsigned long delay = 0L);
  ~AJob();
                  
  void finish();  //     
  long counter(); //    onRun    
  long setDelay(unsigned long); //    onRun
  unsigned long previousMillis();//      onRun
  unsigned long currentMillis(); //    
                              
  virtual void onInit();  //      
  virtual void onRun();   //     
  virtual void onDone();  //        finish
};


The AJob class is the base class for all tasks that require simultaneous execution. Its heirs should override the onRun method , which implements the logic of the task being performed. Given the specifics of preemptive multitasking, calling this method should not be too time consuming. It is recommended to break the task logic into several lighter subtasks, each of which will be performed for a separate onRun call .

class AJobManager
class AJobManager {
  friend class AJob;
  AJob* mJobFirst;  //      
  void attach(AJob*);   //    
  void dettach(AJob*); //    
  void dettachAll();   //   
public:  
  AJobManager();
  ~AJobManager();
  void setup();
  void loop();
};


The AJobManager class has a more modest declaration and contains only two public methods: setup and loop . The setup method must be called once before starting the main program loop. In it, one by one initialization of all tasks occurs, by calling the corresponding onInit method for each task from the list.
A task is added to the list automatically when its constructor is called and can be deleted by calling the public finish method of the task itself. Loop

methodIt is called repeatedly in the main program cycle and is responsible for sequentially executing the logic of each task from the list at specified intervals (if installed).

Thus, when creating an instance of the Geksapod class inherited from the AJobManager class , we get at our disposal a convenient multitasking tool.

Movement implementation


Any movement of the body can be described by some function that determines its position at a given point in time. Such a function can be composite, that is, represent a set of functions, each of which is applicable only for a certain period of time.

The various types of movement of the limbs of the hexapod are defined and can be extended using classes inherited from the Motion class .

class Motion
class Motion {
  friend class MotionJob;
protected:
  long m_MaxTime;     //      
  long m_TotalTime;   //      
  bool m_IsLooped;    //   
  Motion* m_pNext;    //     
public:  
  Motion(long maxTime, bool isLooped, long totalTime = -1, Motion* pNext = NULL);
  ~Motion();
  
  inline long maxTime() { return m_MaxTime; }
  inline long totalTime() { return m_TotalTime; }
  inline bool isLooped() { return m_IsLooped; }

  //          

  //       0 <= time <= m_MaxTime 
  virtual int getPose(long time, Vector3D points[FOOTS_COUNT]) { return E_NOT_IMPL; };  
  //        0 <= time <= m_MaxTime
  virtual int getPose(long time, int pose[FOOTS_COUNT][3]) { return E_NOT_IMPL; };       
};


To implement the movement, the overridden getPose method must return the position of the limbs of the robot for a given period of time time in the range from 0 to mMaxTime (in milliseconds). In the case when the movement is looped ( m_IsLooped == true) , the movement time can be limited by setting the duration in m_TotalTime . And finally, you can organize a sequence of movements by combining them into a list.

Thus, we have the opportunity to describe the movements of the robot. But the description itself (in our case, some instance of the class inherited from Motion) will not make the robot move. We need a mechanism that will rearrange the legs of the hexapod, guided by the description given to it.

And this mechanism is an instance of the MotionJob class declared in the Geksapod class .

class MotionJob
class MotionJob: public AJob {
  enum STATUS { 
    NONE, RUNING, STOPING 
   } m_Status;
  
  Geksapod* m_pGeksapod;
  Motion* m_pMotion;
  long m_MotionTime;
  long m_TotalTime;
public:  
  MotionJob(Geksapod* pGeksapod);
  int execute(Motion* pMotion);
  void onRun();
};


An instance of the MotionJob class, inherited from AJob, is a task in which the onRun method is called at regular intervals . It also implements a mechanism that forces our robot to perform movements. We just need to tell him how to move, indicating a description of the movement when calling the execute method. That's all for now. There are still many unlit issues that I will try to write about in the next article. I hope I haven’t tired the readers with too much code. Ready to answer all your questions. To be continued ... PS In the process of preparing the next article, I slightly changed the structure of the visibility of methods in the AJob class . OnInit Methods







, onRun and onDone do not need public access, since their call comes from the friendly AJobManager class . Considering that these methods should be overlapped in the heirs, it is enough to place them in the protected section .

class ajob
class AJob {
  friend class AJobManager;
private:
  AJobManager* m_pAJobManager;   
  AJob* mJobNext;                      
  unsigned long m_counter;         //    onRun
  unsigned long m_previousMillis; //     onRun
  unsigned long m_currentMillis;  //   
  unsigned long m_delayMillis;    //    onRun
  void run();
protected:
  virtual void onInit();  //      
  virtual void onRun();   //     
  virtual void onDone();  //        finish
public:
  AJob(AJobManager*, unsigned long delay = 0L);
  ~AJob();

  void finish();  //     
  long counter(); //    onRun    
  long setDelay(unsigned long); //    onRun
  unsigned long previousMillis();//      onRun
  unsigned long currentMillis(); //    
};


All Articles