Revivimos el hexápodo. Parte uno

En un artículo anterior, compartimos nuestra experiencia en la creación de un hexápodo utilizando la tecnología de impresión 3D. Ahora hablaremos sobre el componente de software, que le permitió revivir.

Originalmente se planeó presentar toda la información en un artículo, pero en el proceso de redacción quedó claro que dicha presentación sería superficial y poco informativa. Por lo tanto, se decidió escribir varios artículos con una presentación más detallada del tema.

Dispositivo hexápodo


Actualmente, la placa UNO R3 con Wi-Fi ESP8266 se utiliza como controlador principal . De hecho, esta placa con dos controladores a bordo, interactuando entre sí a través de una interfaz UART.



A pesar de que Uno tiene una cantidad bastante limitada de recursos informáticos, es suficiente para enseñarle al robot cómo ejecutar comandos básicos:

  • movimiento en línea recta con una velocidad y duración determinadas
  • movimiento circular hacia la izquierda o hacia la derecha (girar en su lugar)
  • tomar posiciones preestablecidas de las extremidades

El ESP8266 es responsable de organizar el canal de comunicación inalámbrica y sirve como una puerta de enlace a través de la cual Uno recibe los comandos de control.

El robot se puede controlar a través de la red local dentro de la sesión de telnet establecida con él o mediante una conexión por cable al controlador (para firmware o depuración). Para mayor comodidad, también escribimos una sencilla aplicación de Android que implementa una funcionalidad interactiva mínima para controlar el robot.



La siguiente figura muestra esquemáticamente la estructura hexapod.



Todos los servos están conectados a la tarjeta de expansión Multiservo Shield , que le permite controlar 18 servos. Su comunicación con Arduino es a través del bus I²C. Por lo tanto, incluso con 18 servos de dirección al mismo tiempo, casi todos los pines Arduino permanecerán libres.
Cabe señalar que la placa de expansión tiene un conector para alimentar los servos conectados. Pero la corriente máxima permitida para la cual está diseñada la placa es de aproximadamente 10 A, que no es suficiente para alimentar los servodriveres MG996R, cuyo consumo de corriente máximo total puede exceder el valor especificado. Por lo tanto, en nuestra versión, cada servo estaba conectado a una línea de alimentación separada, sin pasar por la placa de expansión.

Manejo de hexápodos


La lógica de control del miembro hexapod se implementa en el programa utilizando la clase GeksaFoot .

clase 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);
};


Los métodos coxaAngle , femoraAngle , tibiaAngle le permiten establecer o averiguar el ángulo de rotación de una articulación individual de la pierna. Los métodos auxiliares getAngles y getPoint implementan la lógica de cálculo de cinemática directa e inversa, con la cual puede calcular el valor de los ángulos de las piernas para un punto dado en el espacio de su extremidad. O viceversa, un punto espacial para los valores de ángulo actuales.

La posición promedio de cada junta corresponde a un valor cero del ángulo, y el rango de rotación de la junta se encuentra en el rango de -90 a 90 grados.

La clase de nivel superior es la clase Geksapod. Implementa la lógica de todo el robot. Cada pata hexapod se incluye en esta clase como una instancia separada de la clase GeksaFoot .

clase 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]);
};


Los métodos getPose y setPose sobrecargados están diseñados para uso interno y le permiten obtener la posición actual de las extremidades del robot o establecer una nueva. En este caso, la posición de las patas se establece en forma de un conjunto de valores de los ángulos de rotación de cada articulación o como un conjunto de coordenadas de los puntos finales de las extremidades del robot en relación con su centro.
Para un movimiento suave de la extremidad al llamar a los métodos setPose , puede especificar el tiempo (parámetro actionTime ) después del cual las piernas deben alcanzar la posición especificada.
El robot está controlado por los métodos públicos de movimiento , rotación y parada .

Emulación multitarea


La clase Geksapod hereda la implementación de la clase AJobManager y contiene una instancia de la clase MotionJob , que a su vez hereda de la clase AJob . Estas clases le permiten implementar la llamada multitarea no preventiva, lo que le permite abstraerse de la linealidad de los programas y realizar varias tareas al mismo tiempo.

trabajo de clase
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
};


La clase AJob es la clase base para todas las tareas que requieren ejecución simultánea. Sus herederos deben anular el método onRun , que implementa la lógica de la tarea que se realiza. Dados los detalles de la multitarea preventiva, llamar a este método no debería llevar demasiado tiempo. Se recomienda dividir la lógica de la tarea en varias subtareas más claras, cada una de las cuales se realizará para una llamada onRun separada .

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


La clase AJobManager tiene una declaración más modesta y contiene solo dos métodos públicos: configuración y bucle . El método de configuración debe llamarse una vez antes de iniciar el ciclo principal del programa. En él, se produce una inicialización de todas las tareas, llamando al método onInit correspondiente para cada tarea de la lista.
Una tarea se agrega a la lista automáticamente cuando se llama a su constructor y se puede eliminar llamando al método de finalización pública de la tarea en sí.

Método de bucleSe llama repetidamente en el ciclo principal del programa y es responsable de ejecutar secuencialmente la lógica de cada tarea de la lista a intervalos específicos (si está instalado).

Por lo tanto, al crear una instancia de la clase Geksapod heredada de la clase AJobManager , tenemos a nuestra disposición una conveniente herramienta multitarea.

Implementación del movimiento


Cualquier movimiento del cuerpo puede ser descrito por alguna función que determina su posición en un punto dado en el tiempo. Dicha función puede ser compuesta, es decir, puede ser un conjunto de funciones, cada una de las cuales es aplicable solo por un cierto período de tiempo.

Los diversos tipos de movimiento de las extremidades del hexapod se definen y pueden ampliarse utilizando clases heredadas de la clase Motion .

movimiento de clase
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; };       
};


Para implementar el movimiento, el método getPose anulado debe devolver la posición de las extremidades del robot durante un período de tiempo determinado en el rango de 0 a mMaxTime (en milisegundos). En el caso de que el movimiento se repita ( m_IsLooped == true) , el tiempo de movimiento puede limitarse configurando la duración en m_TotalTime . Y finalmente, puede organizar una secuencia de movimientos combinándolos en una lista.

Por lo tanto, tenemos la oportunidad de describir los movimientos del robot. Pero la descripción en sí (en nuestro caso, alguna instancia de la clase heredada de Motion) no hará que el robot se mueva. Necesitamos un mecanismo que reorganice las patas del hexapod, guiado por la descripción que se le da.

Y este mecanismo es una instancia de la clase MotionJob declarada en la clase Geksapod .

clase 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();
};


Una instancia de la clase MotionJob , heredada de AJob, es una tarea en la que se llama al método onRun a intervalos regulares . También implementa un mecanismo que obliga a nuestro robot a realizar movimientos. Solo necesitamos decirle cómo moverse, indicando una descripción del movimiento cuando se llama al método de ejecución. Eso es todo por ahora. Todavía hay muchos problemas no iluminados sobre los que intentaré escribir en el próximo artículo. Espero no haber cansado a los lectores con demasiado código. Listo para responder todas sus preguntas. Continuará ... PD: En el proceso de preparación del siguiente artículo, cambié ligeramente la estructura de la visibilidad de los métodos en la clase AJob . Métodos OnInit







, onRun y onDone no necesitan acceso público, ya que su llamada proviene de la clase amigable AJobManager . Teniendo en cuenta que estos métodos deben superponerse en los herederos, es suficiente colocarlos en la sección protegida .

trabajo de clase
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