Manual

do

Maker

.

com

Serial com Python, Arduino e HC12

Serial com Python, Arduino e HC12

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!

Protocolo de comunicação

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.

Definição do protocolo de 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:

  • Caractere de início de mensagem
  • Endereço do rádio que deve receber a mensagem
  • Somatória de todos os bytes enviados
  • Passos do motor (2 bytes)
  • Direção a girar
  • Fim de mensagem

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().

Parsing de mensagem

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.

Structs em C/C++

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.

Enum em C/C++

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.

Serial com Python

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.

Array de bytes serial com Python

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:

teste-codigo.png

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.

list-append.jpg

E assim temos a comunicação para teste, de forma rápida e eficiente!

Bitwise

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!

Vídeo mostrando a serial com Python e overview

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.

Nome do Autor

Djames Suhanko

Autor do blog "Do bit Ao Byte / Manual do Maker".

Viciado em embarcados desde 2006.
LinuxUser 158.760, desde 1997.