Manual
do
Maker
.
com
Calma que antes de tudo - Não é Python no Arduino; é serial com Python, se comunicando com Arduino! E nesse artigo veremos a interessante comunicação serial entre o PC e o Arduino, utilizando o rádio 433MHz HC12, que é uma forma transparente para comunicar dispositivos entre si, ou interagir com eles através de um computador qualquer. Não precisa driver, basta iniciar uma serial e escrever nela!
Já tratei desse tema no artigo "ESP32 Lora - Tutorial simples". Como se tratava de LoRa, não ficou exatamente "simples" porque a parte de LoRa acabou tomando um pouco da cena. Mas quando trabalhamos com baixa frequência, devemos considerar coisas pertinentes à arquitetura; para comunicação machine-to-machine não faz muito sentido uma formatação de dados com leitura humana; e pior, tem pessoas que usam até json em comunicação serial.
O código que vou usar para exemplificar a comunicação é de um projeto de automação industrial que estou trabalhando. Costumo fazer implementações gradativas, mesmo que em alguns casos gere retrabalho, mas dessa maneira consigo fazer testes consistentes e reduzir bugs ocultos na lógica. Vamos à descrição da comunicação.
O objetivo da primeira fase é movimentar um motor, que tracionará uma mesa com o material que receberá enformação e corte laser. Como o tracionamento da mesa receberá um certo esforço, criei uma rampa para não "arrancar" na velocidade máxima, tirando o motor da inércia gentilmente. Haverá diversos rádios HC12 em diferentes partes do projeto e no momento atual da implementação, estou com a mensagem no seguinte formato:
Falta implementar uma camada de ofuscação da mensagem para aumentar minimamente a segurança. Também pode ser importante incluir o endereço de quem está enviando a mensagem, mas por enquanto são esses valores. Se estivéssemos mandando esses dados em formato ASCII, poderíamos ter algo como:
start=@,addr=1,sum=xx,steps=1230,dir=left,end=#
Parece um formato claro para leitura humana, mas cada caractere é 1 byte. Depois de recebido, ainda tem o trabalho de fazer parsing para extrair apenas as informações fundamentais. Essa é a "pior" maneira de se fazer a comunicação entre dispositivos, gerando volume na transmissão dos dados e processamento na filtragem da mensagem. Daí poderíamos optar por algo mais simples, como:
@,1,18,1230,left,#
Já diminui um bocado, certo? Só que essa não é uma boa forma de enviar mensagens também. E daí temos a última opção, que certamente é a mais eficiente: Mandar apenas os valores.
Como cada caractere é 1 byte, podemos mandar apenas os bytes, desse modo:
0x5e 0x01 0x07 0x01 0x0e 0x30 0x0a
São apenas os 7 bytes que compõe a mensagem. Escrevendo "bytes", 0x5e é apenas 1 valor, não 4 caracteres. Muito mais eficiente e fácil de receber e tratar!
Escrevi uma função para receber as mensagens, que fica desse jeito:
void hc12decodeBytes(){
if (Serial2.available()){
i = 0;
memset(msg_array,0,sizeof(msg_array));
}
while (Serial2.available()){
uint16_t buf = Serial2.read();
if (buf != hc12attr.start && i == 0){
Serial.println("noise");
break;
}
msg_array[i] = buf;
msg_len += 1;
Serial.print("buf - msg_array ");
Serial.print(buf);
Serial.print(" ");
Serial.println(msg_array[i]);
i+=1;
}
if (msg_array[hc12_sum] == msg_len && msg_array[hc12_sum] != 0){
Serial.println("tamanho correto");
setStepsAndCommand();
}
delay(1000);
}
A variável i é global. Depois temos um memset, que preenche com zeros o array msg_array, que guarda a mensagem recebida. A primeira coisa a validar é se o primeiro byte recebido é o identificador de início de mensagem, para não ler uma mensagem a partir de qualquer ponto e também não receber interferências. No caso, 0x5e, que é o circunflexo em ASCII. Usei o circunflexo porque em expressões regulares ele indica início de linha mesmo. A mensagem é lida byte a byte, então o parsing é feito fora do loop de recepção, após validar que o tamanho é condizente. A função que faz parsing é a setStepsAndCommand().
Temos um array de bytes chamado msg_array, que guarda os valores recebidos via HC12. Iniciei esse array com 10 bytes porque estava previsto seu crescimento, que inicialmente, como citei, continha uma funcionalidade básica de mover e parar o motor, sem determinar os passos.
Como o formato está bem definido, a atribuição das variáveis de comando ficou bastante fácil. Também, 2 bytes da mensagem se referem aos steps, que podem ir de 1 à 65535 (como citado - 2 bytes). Nesse caso, utilizei bitwise para somar os 2 bytes, como pode ser visto na função a seguir:
void setStepsAndCommand(){
hc12attr.steps = (msg_array[hc12_msb]<<8)|(msg_array[hc12_lsb]<<0);
hc12attr.command = msg_array[hc12_comm];
Serial.println(hc12attr.steps);
}
A struct hc12attr.steps recebe os 2 bytes. O mais significativo é deslocado para a esquerda e o menos significativo fica na direita. O print é para esse momento de implementação e teste, depois cai fora.
Para ficar mais clara a atribuição, vou eliminar as variáveis. Olhe novamente a mensagem mais acima. O que estou fazendo é:
hc12attr.steps = (0x01<<8)|(0x0e<<0);
Vamos transformar isso em binário para entender melhor:
256 14
000000001 000011110
O máximo que chegamos em 1 byte é 255. Como tivemos o estouro de base, precisamos de outro byte pra atribuir o valor. Isso significa que do lado esquerdo temos o valor 256. Do lado direito temos o valor 14. Com o deslocamento de bits que fizemos acima, já somando (|), temos o valor de 270 passos.
Se você é exclusivamente robista, talvez não tenha ainda utilizado uma struct. Olhe o código acima e repare que temos hc112attr.steps.
Uma struct é uma maneira de organizar um conjunto de dados pertinentes. Tudo o que é relacionado à mensagem foi adicionado à struct hc12attr. Sua forma de declaração é simples:
struct {
const uint16_t addr = 0x01; //receiver addr
const uint16_t start = 0x5e; //start char message
const uint16_t end = 0x0a; //end char message
const uint16_t left = 0x30; //run left char message
const uint16_t right = 0x31; //run right char message
const uint16_t stop = 0x32; //stop char message
const uint16_t msg_len = 0x07; //message lenght
uint16_t steps = 0x00; //read from HC12 always
int command = 0x32; //initial value is 'stop'
} hc12attr;
É bem mais fácil procurar o nome criado para uma variável em uma estrutura (usando VS Code, por exemplo, que mostra os componentes da struct assim que digitamos o ponto). Deixei todos os atributos da comunicação dentro dessa struct, inclusive os dados variáveis, como steps e command. Para ficar mais fácil visualizar, a estrutura vazia tem esse formato:
struct {
...
} nome_para_a_struct;
Tendo explicado o deslocamento de bits e a struct, deve ter ficado mais fácil entender agora a atribuição. Não se preocupe com o bitwise ainda, vou dar mais detalhes posteriormente.
A última coisa de interessante nesse código é o enumerador.
Usar valores explícitos em coisas posicionais nem sempre é viável. Por exemplo, usando o código do bitwise mais acima:
hc12attr.steps = (msg_array[3]<<8)|(msg_array[4]<<0);
Usando um valor estático para indicar a posição de um determinado byte não é adequado. Se mudássemos a mensagem, adicionando o endereço de origem após o endereço de destino, os bytes dos steps passariam a ser o 4 e 5. Com isso, teríamos que correr todo o código em busca dessas referências para corrigí-la. Para poucas variáveis (ou para variáveis dispersas), podemos usar defines ou const int. Mas há uma maneira prática de organizar informações posicionais, com o enum.
O enum pode ser utilizado de duas maneiras diferentes; auto-atribuição ou apontando o valor. Dessa maneira, não precisamos decorar as posições de nossa mensagem, basta chamar a variável do nome correspondente à posição. A mensagem acima ficou assim:
hc12attr.steps = (msg_array[hc12_msb]<<8)|(msg_array[hc12_lsb]<<0);
E o enumerador ficou dessa maneira:
enum {hc12_start, hc12_addr, hc12_sum, hc12_msb, hc12_lsb, hc12_comm, hc12_end};
Automaticamente os valores são atribuídos a partir de 0. Se quiséssemos valores diferentes do ordinal, bastaria definir um valor:
enum {hc12_etc = 25, hc12_foo = 3, hc12_bar = 44};
Como esse código vai mutar muito ainda, não há problema em disponibilizá-lo no estado atual, apenas para que possam ver a estrutura lógica disposta até aqui. A recepção está funcional e você pode experimentar esse código mesmo sem ter o HC12, basta comunicar 2 Arduino entre si via serial. Se for Arduino UNO, utilize a biblioteca softwareserial para criar a segunda serial; conecte TX a RX e vice versa, coloque ambas as MCUs para se comunicarem à 9600 bauds e não se esqueça de interconectar o GND entre as duas. Só isso. Se for comunicar o computador à MCU, utilize um adaptador USB-serial (FTDI, USB-UART ou o nome que quiser usar). Recomendo que conecte a segunda serial ao computador via FTDI para poder experimentar o código Python que vem a seguir. Se não tem um, recomendo esse adaptador que você encontra na Saravati.
Uma das maneiras de programar em Python no Windows (e talvez a mais prática) é utilizar o VS Code. No Linux o Python é nativo e gosto de utilizar o bpython para programação in-flow, que ajuda bastante a ver resultados com testes em fluxo. De qualquer modo, o que precisamos fazer é simples, mas a parte preciosa desse código está na conversão dos bytes que serão escritos via serial para o HC12.
Apesar de simples de executar, não é óbvio o procedimento. Isso porque em Arduino podemos usar Serial.print() para strings e Serial.write() para bytes. Já em Python, só tem write e nós precisamos definir o tipo de dado que será escrito. Para escrever a mensagem:
0x5e 0x01 0x07 0x01 0x0e 0x30 0x0a
Devemos escrever um código assim:
import serial
tracker = serial.Serial("/dev/ttyUSB1",9600,timeout=2)
#valor em hexa
cmd_hex = [0x5e,0x01,0x07,0x01,0x0e,0x31,0x0a]
#valor em bytes
cmd_bytes = bytes(cmd_hex)
#escrever o valor
tracker.write(cmd_bytes)
No meu caso, o dispositivo serial no Linux é /dev/ttyUSB1, mas em Windows será COMxx. O resto do código é igual. Simples ou não? - Só que eu sei, precisamos de flexibilidade para compor a mensagem em um código que não seja apenas teste, certo?
O resultado da comunicação:
Para criar uma lista dinâmica onde os valores possam ser inseridos durante o fluxo do programa, podemos fazer assim:
cmd_hex = []
cmd_hex.append(0x5e)
print(cmd_hex)
[94]
O valor foi impresso em decimal, não se espante. Aliás, tanto faz colocar o valor em hexa ou decimal, mas prefiro passar o valor na forma que eu imagino.
E assim temos a comunicação para teste, de forma rápida e eficiente!
Pra não ficar reescrevendo em um monte de artigos, sugiro a leitura do artigo Expansor de IO com PCF8575 e bitwise, que é bastante intuitivo e detalhado. O bitwise serve para qualquer linguagem, inclusive para compor dados para comunicação serial com Python - e até shell script!
Farei um vídeo mostrando o movimento do motor, os códigos e uma breve apresentação dos recursos descritos nesse artigo.
Inscreva-se no nosso canal Manual do Maker no YouTube.
Também estamos no Instagram.
Autor do blog "Do bit Ao Byte / Manual do Maker".
Viciado em embarcados desde 2006.
LinuxUser 158.760, desde 1997.