Revivemos o hexapod. Parte um

Em um artigo anterior, compartilhamos nossa experiência na criação de um hexapod usando a tecnologia de impressão 3D. Agora vamos falar sobre o componente de software, o que lhe permitiu reviver.

Foi originalmente planejado apresentar todas as informações em um artigo, mas, no processo de redação, ficou claro que tal apresentação seria superficial e pouco informativa. Por isso, decidiu-se escrever vários artigos com uma apresentação mais detalhada do tópico.

Dispositivo hexapod


Atualmente, a placa UNO R3 com Wi-Fi ESP8266 é usada como o controlador principal . De fato, esta placa possui dois controladores a bordo, interagindo entre si por meio de uma interface UART.



Apesar do Uno ter uma quantidade bastante limitada de recursos de computação, é suficiente ensinar ao robô como executar comandos básicos:

  • movimento em linha reta com uma determinada velocidade e duração
  • movimento circular para a esquerda ou direita (gire no lugar)
  • tomar posições predefinidas dos membros

O ESP8266 é responsável pela organização do canal de comunicação sem fio e serve como um gateway através do qual o Uno recebe comandos de controle.

O robô pode ser controlado pela rede local na sessão de telnet estabelecida com ele ou por uma conexão com fio ao controlador (para firmware ou depuração). Por conveniência, também escrevemos um aplicativo Android simples que implementa uma funcionalidade interativa mínima para controlar o robô.



A figura abaixo mostra esquematicamente a estrutura hexapod.



Todos os servos estão conectados à placa de expansão Multiservo Shield , que permite controlar 18 servos. Sua comunicação com o Arduino é via barramento I²C. Portanto, mesmo com 18 servos direcionando ao mesmo tempo, quase todos os pinos do Arduino permanecerão livres.
Note-se que a placa de expansão possui um conector para alimentar os servos conectados. Mas a corrente máxima permitida para a qual a placa foi projetada é de cerca de 10A, o que não é suficiente para alimentar os servoconversores MG996R, cujo consumo máximo total de corrente pode exceder o valor especificado. Portanto, em nossa versão, cada servo foi conectado a uma linha de energia separada, ignorando a placa de expansão.

Gerenciamento hexapod


A lógica de controle hexapod limb é implementada no programa usando a classe GeksaFoot .

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


Os métodos coxaAngle , femoraAngle , tibiaAngle permitem definir ou descobrir o ângulo de rotação de uma articulação individual da perna. Os métodos auxiliares getAngles e getPoint implementam a lógica de cálculo da cinemática direta e inversa, com a qual você pode calcular o valor dos ângulos da perna para um determinado ponto no espaço do membro. Ou vice-versa, um ponto de espaço para os valores atuais do ângulo.

A posição média de cada junta corresponde a um valor zero do ângulo e a faixa de rotação da junta situa-se na faixa de -90 a 90 graus.

A classe de nível superior é a classe Geksapod. Ele implementa a lógica de todo o robô. Cada perna hexapod é incluída nesta classe como uma instância separada da classe GeksaFoot .

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


Os métodos getPose e setPose sobrecarregados destinam-se a uso interno e permitem obter a posição atual dos membros do robô ou definir um novo. Nesse caso, a posição das pernas é definida na forma de um conjunto de valores dos ângulos de rotação de cada articulação ou como um conjunto de coordenadas dos pontos finais dos membros do robô em relação ao seu centro. Para um movimento suave dos membros ao chamar os métodos setPose , você pode especificar o tempo (parâmetro actionTime ) após o qual as pernas devem atingir a posição especificada. O robô é controlado pelos métodos públicos mover , girar e parar .



Emulação multitarefa


A classe Geksapod herda a implementação da classe AJobManager e contém uma instância da classe MotionJob , que por sua vez herda da classe AJob . Essas classes permitem implementar a chamada multitarefa não-preemptiva, permitindo abstrair da linearidade dos programas e executar várias tarefas ao mesmo tempo.

classe 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
};


A classe AJob é a classe base para todas as tarefas que requerem execução simultânea. Seus herdeiros devem substituir o método onRun , que implementa a lógica da tarefa que está sendo executada. Dadas as especificidades da multitarefa preventiva, chamar esse método não deve consumir muito tempo. É recomendável dividir a lógica da tarefa em várias subtarefas mais leves, cada uma das quais será executada para uma chamada onRun separada .

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


A classe AJobManager possui uma declaração mais modesta e contém apenas dois métodos públicos: setup e loop . O método de instalação deve ser chamado uma vez antes de iniciar o loop principal do programa. Nele, uma inicialização por todas as tarefas ocorre, chamando o método onInit correspondente para cada tarefa da lista.
Uma tarefa é adicionada à lista automaticamente quando seu construtor é chamado e pode ser excluído chamando o método de conclusão pública da própria tarefa.

Método de loopÉ chamado repetidamente no ciclo principal do programa e é responsável pela execução seqüencial da lógica de cada tarefa da lista em intervalos especificados (se instalado).

Assim, ao criar uma instância da classe Geksapod herdada da classe AJobManager , temos à nossa disposição uma conveniente ferramenta multitarefa.

Implementação do movimento


Qualquer movimento do corpo pode ser descrito por alguma função que determina sua posição em um determinado momento. Essa função pode ser composta, ou seja, pode ser um conjunto de funções, cada uma das quais aplicável apenas por um determinado período de tempo.

Os vários tipos de movimento dos membros do hexápode são definidos e podem ser estendidos usando classes herdadas da classe Motion .

classe 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; };       
};


Para implementar o movimento, o método getPose substituído deve retornar a posição dos membros do robô por um determinado período de tempo, no intervalo de 0 a mMaxTime (em milissegundos). No caso em que o movimento é executado em loop ( m_IsLooped == true) , o tempo do movimento pode ser limitado, definindo a duração em m_TotalTime . E, finalmente, você pode organizar uma sequência de movimentos combinando-os em uma lista.

Assim, temos a oportunidade de descrever os movimentos do robô. Mas a descrição em si (no nosso caso, alguma instância da classe herdada do Motion) não fará o robô se mover. Precisamos de um mecanismo que reorganize as pernas do hexápode, guiado pela descrição dada a ele.

E esse mecanismo é uma instância da classe MotionJob declarada na classe Geksapod .

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


Uma instância da classe MotionJob , herdada do AJob, é uma tarefa na qual o método onRun é chamado em intervalos regulares . Também implementa um mecanismo que força nosso robô a realizar movimentos. Só precisamos dizer a ele como se mover, indicando uma descrição do movimento ao chamar o método execute. É tudo por agora. Ainda existem muitos problemas apagados sobre os quais tentarei escrever no próximo artigo. Espero não ter cansado os leitores com muito código. Pronto para responder todas as suas perguntas. Para continuar ... PS: No processo de preparação do próximo artigo, alterei levemente a estrutura da visibilidade dos métodos na classe AJob . Métodos OnInit







, onRun e onDone não precisam de acesso público, pois sua chamada vem da classe amigável AJobManager . Considerando que esses métodos devem ser sobrepostos nos herdeiros, basta colocá-los na seção protegida .

classe 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