Robô controlado por WiFi

Robô controlado por WiFi

Já escrevi dois artigos sobre seguidores de linha, sendo um deles feito com palitos de sorvete e o outro com o espetacular kit da Robocore. Mas já deu de seguidor de linha e desviador de obstáculo, hum? Por essa razão decidi fazer um robô controlado por WiFi, com movimentos pré-programados. E ficou interessante!

Robô controlado por WiFi ou LoRa?

Obviamente que controlado por LoRa o robô teria um alcance imenso, mas controlado por WiFi fica muito barato pra brincar dentro de casa. E dentro de casa tem obstáculos mais significativos, permitindo uma programação de movimento mais interessante.

Material utilizado no robô controlado por WiFi

O robô é desse kit da MASUGUX. Tem montes de vendedores que trabalham com esse kit, é muito barato, mas a MASUGUX é um dos mais baratos (se não for o mais barato), considerando a soma do produto mais o valor de entrega pelos correios. Mas ainda que ache por aí com uma diferença pífia, sugiro a compra com a MASUGUX por duas razões importantes: a qualidade no atendimento, e pela parceria de longa data com o blog, que permitiu a criação desse artigo e de muitos outros, lhe entregando prontamente o projeto para reprodução.

A placa utilizada para fazer o controle é a Waldunano, do nosso conhecido maker Waldyr. Essa placa possui slot para um Arduino Nano e para o ESP-01, onde o ESP-01 será o MASTER da comunicação, repassando a mensagem íntegra para o Arduino.

Robô controlado por WiFi com Waldunano

Deixei o conjunto de pilhas para alimentar a placa, mas poderia ser alimentado pelas baterias li-ion que atendem os motores.

Para controlar os motores DC, utilizei a ponte H L298N da Saravati (Ctrl+Chups from Google Images):

ponte H dos motores do Robô controlado por WiFiEla é extremamente simples de usar. Em ENTRADA controlamos os motores e a direção, conforme o estado dos pinos. Os motores são conectados nos bornes de 2 posições. A alimentação entra no borne de 3 posições, que tem uma saída 5V. Os jumpers devem estar fechando os pinos para ativação dos motores, nada mais além disso.

Alimentação dos motores

A ponte H L298N trabalha com tensões entre 6 e 32VDC. Tive que usar um conjunto combinado que fiz para tirar diferentes tensões com bateria li-ion. A saída ficou em pouco mais de 7V, suficiente para o projeto. No vídeo devo mostrar as conexões, é bastante simples.

Programação do robô controlado por WiFi

Nesse primeiro momento sugiro que experimente, depois faça suas próprias implementações com base no código disposto nesse artigo.

Temos duas programações a serem feitas; uma do ESP-01, outra do Arduino Nano. Eles se comunicarão entre si através da serial, mas o Arduino Nano tem apenas 1 serial. Solução: Usar a biblioteca SoftwareSerial para criar uma serial por software.

Apesar de parecer uma “camada de complicação”, é bastante simples intercomunicá-los. Primeiro fazemos a programação do ESP-01 para simplesmente repassar mensagem pela serial, depois fazemos a programação do Nano para receber essa informação pela serial. É importante garantir que estamos hábeis a fazer a comunicação antes de implementar os controles, já que a comunicação serial é a base.

Para isso, usei esse sketch no ESP-01:

unsigned long int last_time = millis(); //temporizador
uint8_t i = 0;   //exibe na serial

void setup(){
    Serial.begin(9600); //inicia a serial
    delay(2000); //pára por 2 segundos antes do loop
}

void loop(){
    //exibe um valor a cada 3 segundos.
    if ((millis()-last_time) > 3000){
        last_time = millis();
        Serial.println(i);
        i = i>8 ? 0 : i+1;
    }
}

Simplesmente enviando um byte a cada 3 segundos, mais nada. Agora certificamos a recepção desse byte no Nano:

#include <SoftwareSerial.h>

#define SOFT_TX 3
#define SOFT_RX 2

SoftwareSerial SerialESP(SOFT_RX,SOFT_TX);

String values;
uint8_t number = 0;

bool ok = false;

void setup(){
  SerialESP.begin(9600);
  Serial.begin(9600);
}

void loop(){
  while (SerialESP.available()){
    number = SerialESP.read();
    if (((number-48)%2) == 0 && number != '\n'){
      Serial.println("par");
      Serial.println(number-48);
    }
    else{
      Serial.println("...");
    }
  }
}

Aqui tem um pouco mais de coisas, mas é para experimentar algum tratamento. Quando o valor chega na serial ele é lido. Se não for Line Feed (LF, ou ‘\n’), tiramos o módulo. Se o valor for par, imprime na serial por hardware, de modo que poderemos ler como está acontecendo e “se” está acontecendo a comunicação entre o ESP-01 e o Arduino Nano.

Faça esse mesmo teste antes de iniciar a implementação.

Na Waldunano é necessário colocar os jumpers nos pinos que ficam embaixo do Arduino Nano, para fazer a comunicação serial através dos pinos 2 e 3 do Arduino. Isso significa que os pinos 2 e 3 do Arduino não poderão ser utilizados para outro propósito nesse robô.

Comunicação e formato da mensagem

Precisamos definir o formato da mensagem para a comunicação com o robô. Nada de strings nem montes de bytes. Não precisamos adicionar complexidade quando fazemos uma comunicação de segundo plano, sem interface com usuário. Desse modo, o mais adequado para compor a mensagem é um número de bytes mínimos, a ser lido pelo ESP-01, então repassado ao Nano.

O formato da mensagem mais simples que podemos fazer deverá conter:

  • 1 caractere de início
  • 1 caractere de velocidade do motor 1
  • 1 caractere de direção do motor 1
  • 1 caractere de velocidade do motor 2
  • 1 caractere de direção do motor 2
  • 1 fim de linha (‘\n’)

Não precisamos nem separar os campos, basta mandar em formato de bytes, e então, na leitura, separar cada byte. Como fazer isso? Fácil, inclusive já descrevi a maneira mais simples de compor uma boa mensagem nesse tutorial de LoRa.

Para escrever os bytes, utilizaremos Serial.write(), invés do tradicional Serial.print(), porque precisamos enviar dados binários, ao invés de caracteres representando dígitos. Se usarmos print, 255 será ‘2’, ‘5’, ‘5’. O que queremos é 1 byte para 1 valor. Com write 255 será simplesmente FF. Não precisamos nos preocupar com a representação hexadecimal, não se preocupe. Inclusive, sugiro fortemente a leitura do conceito nesse artigo.

Implementando o socket server

Para enviar os valores ao ESP-01, utilizaremos um servidor socket que receberá essa informação. O servidor socket com ESP8266 foi exemplificado nesse artigo. O processo é simples, mas como sempre, sugiro que faça o teste de comunicação antes de implementar o restante. Para testar o socket server no ESP8266, use o socket client em Python descrito nesse artigo. O título é sobre o server, mas nele disponho também o código do client. Seguindo os dois artigos desse parágrafo, será fácil comprovar o funcionamento. Após, siga daqui.

O código para o ESP8266 não precisaria fazer o tratamento, apenas fazer o papel de passthrough, repassando do jeito que chega, de modo transparente. De qualquer modo, garantir que a mensagem esteja no formato certo antes de passar adiante não faz mal nenhum, hum?

#include <ESP8266WiFi.h>

#define aSSID      "SuhankoFamily"
#define aPASSWD    "fsjmr112"
#define SOCK_PORT 123

uint8_t msg_pos = 0;
uint8_t value   = 0;
uint8_t msg[6]  = {0};

WiFiServer sockServer(SOCK_PORT);

void setup() {
  Serial.begin(9600); //inicia a serial
  delay(2000); //pára por 2 segundos antes do loop

  WiFi.begin(aSSID, aPASSWD);
  while (WiFi.status() != WL_CONNECTED) {
    delay(100);
  }
  Serial.print("IP: ");
  Serial.println(WiFi.localIP());
  sockServer.begin();
}

void loop() {
  msg_pos = 0;
  WiFiClient client = sockServer.available();
  if (client) {
    while (client.connected()) {
      while (client.available() > 0) {
        value  = client.read();
        msg[msg_pos] = value;
        //só incrementa se o primeiro byte for ^
        if (msg_pos < 6) {
            if (msg[0] == '^') {
            msg_pos++;
          }
        }
        //não deixa estourar o buffer se o último byte
        //não for o esperado
        else if (msg_pos == 5 && msg[5] != '\n') {
          msg_pos = 0;
        }
      }
      delay(10);
    }
    client.stop(); //acabou a leitura dos dados. Finaliza o client.
  }
   if (msg[0] == '^'){
       for (uint8_t i=0;i<6;i++){
           Serial.write(msg[i]);
       }
       memset(msg, 0, 6);
   }
}

Código para o Arduino Nano

O Arduino Nano é o que mais trabalha em nosso robô controlado por WiFi, apesar de ser slave. Isso porque os GPIO usados para controlar os motores estão nele, mas ele não é o ponto inicial da comunicação, por isso o ESP-01 foi promovido a master nessa comunicação. O código para o Arduino Nano ficou assim:

#include <Arduino.h>
#include <SoftPWM.h>
#include <SoftwareSerial.h>

//pinos da comunicação serial entre Arduino e ESP-01 da Waldunano
#define SOFT_TX 3
#define SOFT_RX 2

const uint8_t STOP     = 0;
const uint8_t FRONT    = 1;
const uint8_t BACK     = 2;
const uint8_t BUF_SZ   = 6;
const uint8_t IGNITION = 8;

const uint8_t ON  = HIGH;
const uint8_t OFF = LOW;

uint8_t serial_pos     = 0;  //marcador de posição da leitura do buffer
/*
buffer da leitura. Formato da msg: ^ABDC\n - onde A é velocidade esquerda;
 B é direção esquerda; C e D são o mesmo para direita.
 */
uint8_t serial_msg[BUF_SZ] = {0};  

//posicao de cada byte de instrução dos motores.
const uint8_t spd_lft = 1;
const uint8_t dir_lft = 2;
const uint8_t spd_rgt = 3;
const uint8_t dir_rgt = 4;

SoftwareSerial SerialESP(SOFT_RX,SOFT_TX); //serial de comunicação com o ESP-01

/*
Usando analógicos para os motores. Para fazer o PWM nesses pinos é necessário ter
uma biblioteca para fazê-lo por software. No caso, estamos usando a SoftPWM.
*/

uint8_t m1a = 4;
uint8_t m1b = 5;
uint8_t m2a = 6;
uint8_t m2b = 7;
uint8_t motors_pins[4] = {m1a,m1b,m2a,m2b};

void move_left(uint8_t speed, uint8_t direction){
    //A0 e A1
    /*
    SE FRENTE:
    LIGA A0 (speed), DESLIGA A1
    SE TRAS:
    DESLIGA A0, LIGA A1 (speed)
    */
   if (direction == FRONT){
       SoftPWMSet(m1b, 0);
       SoftPWMSet(m1a, speed);
       
       digitalWrite(IGNITION,ON);
       delay(100);
       digitalWrite(IGNITION,OFF);
   }
   else if (direction == BACK){
       SoftPWMSet(m1a, 0);
       SoftPWMSet(m1b, speed);

       digitalWrite(IGNITION,ON);
       delay(100);
       digitalWrite(IGNITION,OFF);
   }
   else if (direction == STOP){
       SoftPWMSet(m1b, 0);
       SoftPWMSet(m1a, 0);

       digitalWrite(IGNITION,ON);
       delay(100);
       digitalWrite(IGNITION,OFF);
   }

}

void move_right(uint8_t speed, uint8_t direction){
    //A2 e A3
    /*
    SE FRENTE:
    LIGA A2 (speed), DESLIGA A3
    SE TRAS:
    DESLIGA A2, LIGA A3 (speed)
    */
   if (direction == FRONT){
       SoftPWMSet(m2b, 0);
       SoftPWMSet(m2a, speed);
       digitalWrite(IGNITION,ON);
       delay(100);
       digitalWrite(IGNITION,OFF);
   }
   else if (direction == BACK){
       SoftPWMSet(m2a, 0);
       SoftPWMSet(m2b, speed);
       digitalWrite(IGNITION,ON);
       delay(100);
       digitalWrite(IGNITION,OFF);
   }
   else if (direction == STOP){
       SoftPWMSet(m2b, 0);
       SoftPWMSet(m2a, 0);
       digitalWrite(IGNITION,ON);
       delay(100);
       digitalWrite(IGNITION,OFF);
   }

}

void move_motors(uint8_t speed_left, uint8_t direction_left, uint8_t speed_right, uint8_t direction_right){
    move_left(speed_left,direction_left);
    move_right(speed_right,direction_right);
}

void setup() {
    Serial.begin(9600);
    SerialESP.begin(9600);

    pinMode(IGNITION,OUTPUT); //pino de sinal - tipo "ignição"
    digitalWrite(IGNITION,OFF);

    SoftPWMBegin(); //inicializa a biblioteca
    

  for (uint8_t i=0;i<4;i++){
      SoftPWMSet(motors_pins[i], 0); //configura os pinos analógicos para usar PWM.
      SoftPWMSetFadeTime(motors_pins[i], 30, 200);
  }
  
}

void loop() {
    //available diz o numero de bytes no buffer

    /*if serial > 0 : enquanto não for \n ou serial_pos < 5*/
    if (SerialESP.available() > 0){
        serial_msg[serial_pos] = SerialESP.read();
        if (serial_msg[serial_pos] == '^'){

        
            while (serial_msg[serial_pos] != '\n'){
                if (serial_pos == 6) {
                    break;
                }
                Serial.println(serial_msg[serial_pos]);
                if (serial_msg[0] == '^'){
                    serial_pos += 1;
                    if (SerialESP.available() > 0){
                        serial_msg[serial_pos] = SerialESP.read();
                    }
                }
                /*
                se já passou o tamanho do buffer e o último byte não foi o fim 
                da mensagem: zera o buffer
                */
                if (serial_pos > BUF_SZ-1 && serial_msg[serial_pos-1] != '\n'){
                    serial_pos = 0;
                }
            }
        }
        
    }
    /*como há um incremento após a leitura do último byte,
    a serial_pos será maior que o buffer, mas sem problemas, porque
    a mensagem já foi entregue, então podemos limpar a variável agora.
    */
    serial_pos = 0;

    /*
    apesar de estar em loop, não há problema em repetir o comando até que haja mudança,
    então não precisamos garantir uma execução única.
    */
   move_motors(serial_msg[spd_lft],serial_msg[dir_lft],serial_msg[spd_rgt],serial_msg[dir_rgt]);

   delay(100);

}

De quebra, fazemos o debug na serial USB, assim temos uma “radiografia” da mensagem que está sendo trocada entre as MCUs desse robô controlado por WiFi. A mensagem sempre começara com 94 e termina com 10. No caso, 10 é ‘\n’, que não é um caractere visual e tudo o que deve aparecer é uma linha em branco para o último byte:

debug serial

Aqui é a mensagem que enviei para parar os motores após o teste. Recapitulando:

  • 94 = ‘^’ – início de mensagem
  • 0 à 255 – velocidade do motor 1
  • 1 ou 2 – direção do motor 2
  • 0 à 255 – velocidade do motor 2
  • 1 ou 2 – direção do motor 2
  • 10 = ‘\n’ – fim de mensagem

Para testar, utilizei um socket client que escrevi em Python. Não está perfeito; a função chr lê apenas signed char, ou seja, os valores vão de -127 à 127. Mas é só usar uma velocidade dentro do valor positivo para testar, o programa de controle não é esse em Python. Para que você possa testar também, eis o programa:

#!/usr/bin/env python
import socket 
import time 
import struct
import argparse
import sys

ap = argparse.ArgumentParser()

ap.add_argument("-l","--frontleft",help="frente esquerdo",action='store_true',default=False)
ap.add_argument("-r","--frontright",action='store_true',help="frente direito",default=False)
ap.add_argument("-L","--backleft",action='store_true',help="tras esquerdo",default=False)
ap.add_argument("-R","--backright",action='store_true',help="tras esquerdo",default=False)
ap.add_argument("-b","--bothfront",action='store_true',help="ambos frente",default=False)
ap.add_argument("-B","--bothback",action='store_true',help="ambos tras",default=False)
ap.add_argument("-s","--stop",action='store_true',help="parar ambos",default=False)


#ip a se conectar
ip = "192.168.1.208"

#porta do socket server
port = 123 

addr = ((ip,port)) 

client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 
client_socket.connect(addr) 

msg = bytearray()

def stop():
   msg.append(94)
   msg.append(0)
   msg.append(1)
   msg.append(0)
   msg.append(1)
   msg.append(10)

def front_left():
   msg.append(94)
   msg.append(0)
   msg.append(1)
   msg.append(126)
   msg.append(1)
   msg.append(10)

def front_right():
   msg.append(94)
   msg.append(126)
   msg.append(1)
   msg.append(0)
   msg.append(1)
   msg.append(10)

def back_left():
   msg.append(94)
   msg.append(0)
   msg.append(1)
   msg.append(126)
   msg.append(2)
   msg.append(10)

def back_right():
   msg.append(94)
   msg.append(126)
   msg.append(2)
   msg.append(0)
   msg.append(1)
   msg.append(10)

def both_front():
   msg.append(94)
   msg.append(126)
   msg.append(1)
   msg.append(126)
   msg.append(1)
   msg.append(10)

def both_back():
   msg.append(94)
   msg.append(126)
   msg.append(2)
   msg.append(126)
   msg.append(2)
   msg.append(10)

args = vars(ap.parse_args())

if args['frontleft'] == True:
    front_left()
elif args['frontright'] == True:
    front_right()
elif args['backleft'] == True:
    back_left()
elif args['backright'] == True:
    back_right()
elif args['bothfront'] == True:
    both_front()
elif args['bothback'] == True:
    both_back()
elif args['stop'] == True:
    stop()

if len(msg) == 0:
    sys.exit(0)

for i in range(6):
#   client_socket.sendall(str(msg[i]).encode())
    client_socket.send(chr(msg[i]).encode())


time.sleep(1)

client_socket.close()

 

Está bem simples de usar, só executar python movebot.py –help  para pegar a lista de parâmetros e então para, por exemplo, mover o motor da esquerda para frente:

python movebot.py --frontleft

Use o parâmetro –stop para parar e assim por diante. Esse script foi feito para usar a partir do python 3.

O controle do robô está pronto, agora resta fazer o programa de interação. Vou deixar esse programa para outro artigo porque merece exclusividade.

Vídeo

Nesse breve vídeo demonstro o funcionamento e como testo o programa. Também apresento o robô e as partes que o compõe. Aproveito para convidá-lo a inscrever-se em nosso canal DobitaobyteBrasil no Youtube.

Te espero no próximo artigo!

 

Revisão: Ricardo Amaral de Andrade