Comunicação serial com o Arduino em Go

By: TsukiGva2 (published on: )

Comunicação serial

Nas últimas semanas, em meio ao caos do trabalho e uma refatoração surpresa, daquelas que se provam necessárias após um susto, ou quando algo para de funcionar inesperadamente, me encontrei repensando toda a minha arquitetura relacionada à comunicação Serial.

Neste post, iremos mergulhar no desenvolvimento de um programa em Golang que envia dados estruturados via comunicação serial.

Introdução:

Code Snippet

Este código (um pouco redundante, preste atenção nas variáveis buf e imm), lida com dados recebidos via comunicação serial com o computador. O computador, no caso, é o nosso MiniPC, que, no momento, considere como um dispositivo genérico com um sistema Linux compatível com POSIX, um gerenciador de pacotes moderno, e a linguagem Go, que vamos utilizar em breve para escrever nosso módulo.

Primeiro, vamos estudar este código, passo a passo:

Variáveis/Constantes relevantes:

Neste pequeno trecho, utilizamos duas constantes que não estão presentes na imagem, apresentadas abaixo:

  • START_DELIMITER, valor: 0x3C ( '<' )
  • END_DELIMITER, valor 0x3E ( '>' )

duas definições, que representam os nosso marcadores de início e fim do recebimento de dados, essas variáveis são utilizadas para delimitar os dados recebidos via Serial, para garantir que sejam lidos apenas os dados necessários, nada além disso.

Também definimos algumas variáveis que iremos utilizar na função, são essas:

  • O vetor buf, que irá conter os dados resultantes da leitura serial;
  • O vetor imm, que será o intermediário entre a leitura e o vetor resultante, buf;
  • Os inteiros c e i, que representam, respectivamente, o último caractere lido e o número total de caracteres lidos;
  • A constante CAP, que é simplesmente a capacidade de nossos vetores, pode ser ajustada de acordo com a necessidade.

Ao código!

while (Serial.available() > 0)
	if (Serial.read() == START_DELIMITER) goto read_delimited;
return;
read_delimited:

Nessa seção, utilizamos um tipo de guarda, para impedir a leitura de dados que não iniciam com o nosso delimitador de inicio. O código acima consome dados vindos da entrada Serial até que não hajam mais dados disponíveis OU até que encontre o delimitador de início, no nosso caso, 0x3C ( '<' ). Caso ele leia todos os dados disponíveis, e não encontre o delimitador, ele retorna da função com o comando return.

for (i = 0; i < CAP && Serial.available() > 0 && (c = Serial.read()) != END_DELIMITER; i++) imm[i] = c;
if (c != END_DELIMITER) return;
memcpy(buf, imm, i);
buf[i] = '\0';
parse_data(buf);

O segredo desta segunda parte do código está neste laço for, que eu admito que faz um pouco mais do que deveria. Ele inicia com o valor de i (o nosso contador) = 0, e continua iterando de acordo com as seguintes condições:

  • i < CAP, isto é, a quantidade de caracteres lidos deve ser menor que a capacidade dos nossos vetores, para que a leitura prossiga;
  • Serial.available() > 0, ou, ainda existem caracteres para leitura na entrada Serial.
  • ( c = Serial.read() ) != END_DELIMITER, este trecho tem muita informação, mas, basicamente, toda vez que utilizamos (variável=valor) (incluindo os parênteses), na linguagem C, isto não apenas atribui aquele valor à variável, assim como RETORNA o valor atribuído, isto é útil para que possamos realizar operações compostas, como essa, que atribui o caractere lido à variável c e ao mesmo tempo checa se este valor é o nosso delimitador de fim.

E então, a cada iteração, atribuímos o caractere lido ao nosso buffer intermediário, imm.

Após o laço, temos uma linha muito importante, que confirma se o valor final de c, ou seja, o último caractere lido pelo laço, foi o nosso delimitador de fim. Caso o valor final de c não seja o delimitador de fim, isto significa que o laço foi encerrado devido à algum outro problema de leitura, seja o estouro da capacidade, ou o fim inesperado dos caracteres disponíveis para leitura, quebrando a integridade dos dados.

Após a leitura bem sucedida dos dados, chamamos a função parse_data com o valor do buffer resultante, o que essa função faz será explicado no próximo post, sobre tratamento de dados utilizando a função sscanf, da biblioteca padrão da linguagem C. Por enquanto, assuma que os dados devem popular uma estrutura parecida com a seguinte:

typedef struct __attribute__((packed)) PCData
{
  int32_t tags       ;
  int32_t unique_tags;
  bool    comm_status;
  bool    wifi_status;
  bool    lte4_status;
  bool    rfid_status;
  int32_t sys_version;
  int32_t backups    ;
  int32_t envios     ;
} PCData;

O formato utilizado nesse post, será o seguinte:

<tags;unique_tags;comm_status;wifi_status;lte4_status;rfid_status;sys_version;backups;envios>

Já contendo os nossos delimitadores de início/fim. No momento, implementar a função parse_data pode servir como um exercício para o leitor, contudo, ela não tem tanta importância para o assunto deste post.

caso você decida implementar a função parse_data, utilize os seguintes casos de teste (lembrando que não são necessários os delimitadores, pois eles são consumidos pela função de comunicação Serial):

  • "1;1;1;1;1;1" (dados insuficientes)
  • "1;0;1;1;1;1;1;1;1" (correto, o valor de todos os campos deve ser 1, exceto o campo unique_tags)
  • "99999999999;0;1;1;1;0;0;0;0" (algo bem estranho vai acontecer com o campo tags, devido ao fato da função sscanf não lidar com o overflow, será que você consegue resolver esse problema implementando a função parse_data de outra forma?)

Desafio extra: implemente utilizando a função strtok.

Tente pensar em outros tipos de entrada para a sua função, e lembre-se de tentar outras soluções também!

Comunicação Serial em Go

Finalmente chegamos na parte mais esperada, a programação!

É uma prática muito importante, principalmente em um ambiente profissional, a leitura de código. Eu mesmo, passo horas e horas dos meus dias de trabalho, percorrendo linhas e linhas de código (normalmente muito antigas), anotando, recompilando mentalmente e fazendo muitas perguntas, e não há nenhuma vergonha nisso, é comum fazermos perguntas, inclusive, é comum até termos muito mais perguntas do que respostas, é assim que se aprende!

(Atenção: todo o código deste post está disponível em github

Primeiramente, criamos uma nova pasta e iniciamos um novo módulo Go. Para seguir estes passos, apenas entre na pasta utilizando um prompt de comando (ou um terminal no Linux), e digite o comando

go mod init seu_nome/comunica_serial