Trilha de aprendizado · Nível 11 · Tutorial 6

Mensagens UART com delimitação e timeout

Ao concluir, o aluno será capaz de receber mensagens UART delimitadas e tratar mensagens incompletas, excessivas ou ausentes sem bloquear indefinidamente o programa.

  • Nível: Intermediário
  • Duração: 25 min
  • 9 passos
Mensagens UART com delimitação e timeout

O que você vai percorrer

  1. Defina onde a mensagem termina Defina um contrato textual mínimo para mensagens UART e use LF como a única fronteira entre mensagens. 2 min
  2. Receba aos poucos, dentro do vetor Receba bytes UART de forma incremental em um vetor fixo, preservando espaço para o terminador nulo e concluindo a linha somente ao receber LF. 3 min
  3. Valide antes de agir Diferencie o fim de uma linha da validação completa do formato, para que somente mensagens permitidas possam produzir efeitos. 2 min
  4. Descarte o excesso e recupere a sincronização Recupere o enquadramento após uma mensagem UART longa demais, descartando seu restante até o LF sem interpretar o sufixo como outro comando. 3 min
  5. Dê um prazo para completar a linha Use um prazo de montagem para descartar linhas UART que ficam incompletas, sem interromper as demais tarefas do firmware. 3 min
  6. Envie mensagens curtas sem esperar por espaço Envie mensagens UART curtas inteiras apenas quando o buffer de transmissão tiver espaço suficiente, sem interromper o restante do loop. 2 min
  7. Espere por uma resposta, mas só até o prazo Implemente uma tentativa UART com envio pendente, prazo de resposta de 10 segundos e resultado definido mesmo sem resposta. 3 min
  8. Não confunda resposta atrasada com sucesso Evite que respostas antigas, inesperadas, tardias ou parciais sejam confundidas com o sucesso da tentativa atual. 2 min
  9. Teste as falhas e confirme a recuperação Teste o sketch integrado no Monitor Serial, force falhas de delimitação e prazo e confirme a recuperação sem interromper o firmware. 5 min

O que você vai aprender

  • Definir um formato simples de pedido e resposta com terminador explícito.
  • Receber e validar uma mensagem sem ultrapassar os limites do vetor.
  • Descartar mensagens incompletas ou longas demais e recuperar a sincronização.
  • Encerrar uma espera por resposta quando o prazo vencer.
  • Testar o comportamento com mensagens válidas, parciais e ausentes pelo monitor serial.

Antes de começar

  • Comunicação UART pelo monitor serial
  • Vetores de tamanho fixo em C/C++
  • Temporização sem bloqueio com millis
  • Máquinas de estados no firmware

Passo 1 de 9

Defina onde a mensagem termina

Defina um contrato textual mínimo para mensagens UART e use LF como a única fronteira entre mensagens.

Bytes precisam de um contrato

UART é fluxo, não mensagem

A UART entrega bytes em sequência. Embora cada byte viaje em um quadro UART, esses quadros não informam onde começa ou termina uma mensagem da aplicação.

Por exemplo, os bytes podem chegar como Q, :, 0, 1, \n, R, :, 0, 1, \n. Sem uma regra do protocolo, o firmware vê apenas um fluxo contínuo. Neste tutorial, a regra de fronteira será: LF (\n) termina uma mensagem.

Do fluxo às linhas

Nas sequências abaixo, \n e \r são representações visíveis dos caracteres LF e CR; eles ocupam um byte no fluxo real.

Uma única chegada de bytes pode conter linhas completas e também o começo de uma próxima linha.
Bytes recebidos em sequênciaLinhas já completas
`Q:01\nR:01\n``Q:01` e `R:01`
`Q:01`Nenhuma; linha incompleta
`R:02\r\n``R:02`
`Q:03\r`Nenhuma; CR não termina

Pedido e resposta no mesmo formato

Um formato curto e explícito

Use um contrato textual pequeno e restrito:

  • Pedido: Q:01\n
  • Resposta: R:01\n

Q significa pedido e R significa resposta. O caractere : ocupa sempre a segunda posição. Os dois últimos caracteres são o identificador da tentativa, sempre com dois algarismos.

A resposta repete o identificador do pedido: R:01 corresponde a Q:01. Durante os ensaios, não reutilize um identificador em outra tentativa. Isso permite distinguir uma resposta antiga da resposta esperada.

Formato do pedido e da resposta

O identificador liga pedido e resposta; o LF cria a fronteira entre as linhas.

Diagrama de fluxo serial com Q:01 e R:01 separados por LF, com os identificadores 01 ligados e um CR antes de LF mostrado como ignorado.

Os campos têm largura e significado definidos; o LF encerra cada mensagem.

Exemplo

Exemplo

Exemplo de troca:

O Uno envia Q:01\n. O interlocutor responde R:01\n para aquela tentativa.

R:02\n é uma resposta de outro identificador, e Q:1\n não respeita a largura de dois algarismos. Mais adiante, o firmware validará rigorosamente o conteúdo; por enquanto, a regra é reconhecer quando uma linha terminou.

CRLF não cria duas mensagens

LF fecha; CR é ignorado

No protocolo, o LF é o único terminador. Muitos monitores seriais enviam CRLF: nesse caso, o CR (\r) é ignorado e o LF (\n) conclui a mensagem.

  • Q:01\n → linha completa.
  • Q:01\r\n → a mesma linha completa.
  • Q:01\r → ainda incompleta.
  • Q:01 → ainda incompleta.

Ignorar CR impede que CRLF seja interpretado como duas mensagens. Um CR isolado nunca fecha a linha.

Dica

Fronteira não é validação

Delimitar uma linha não significa, ainda, aceitá-la como comando ou resposta. O LF informa apenas que chegou a fronteira; nos próximos passos, o conteúdo será guardado e validado antes de produzir qualquer efeito.

Pratique a regra de delimitação

Reconheça a fronteira da mensagem

Relacione cada sequência recebida ao estado de delimitação correto. Considere que as sequências são vistas exatamente como escritas, com \r e \n representando bytes CR e LF.

Toque em um item e depois no par correspondente.

Passo 2 de 9

Receba aos poucos, dentro do vetor

Receba bytes UART de forma incremental em um vetor fixo, preservando espaço para o terminador nulo e concluindo a linha somente ao receber LF.

Monte a linha sem parar o loop

Recepção incremental

A UART pode entregar uma linha em vários momentos. Em vez de esperar os próximos bytes, o loop() lê apenas o que já está disponível, guarda os caracteres de conteúdo e volta às outras tarefas.

Use um vetor de 16 posições: linha[0] até linha[14] recebem no máximo 15 caracteres; linha[15] fica reservado para \0. O índice n informa quantos caracteres de conteúdo já foram armazenados.

Diagrama de caracteres chegando aos poucos, sendo armazenados nas primeiras 15 posições de um vetor de 16, enquanto LF conclui a linha e a última posição recebe o terminador nulo.

O índice nunca pode avançar para a posição reservada ao terminador nulo.

Armazene e conclua com segurança

Dois finais diferentes

LF ('\n') pertence à mensagem recebida como delimitador, mas não é armazenado como conteúdo. Ao recebê-lo, o firmware escreve \0 no vetor para que a linha possa ser tratada como texto em C.

\0 não é um byte que o protocolo envia para terminar a linha: ele existe apenas na representação local. Já CR ('\r') é ignorado, conforme o contrato do passo anterior.

Atenda poucos bytes por passagem

Trecho para inserir em um sketch; processLine() representa o processamento da linha completa.

cpp
char linha[16];
uint8_t n = 0;
const uint8_t MAX_POR_PASSAGEM = 4;

void processLine(const char *texto) {
  Serial.print("Linha completa: ");
  Serial.println(texto);
}

void receberSerial() {
  uint8_t atendidos = 0;

  while (Serial.available() > 0 && atendidos < MAX_POR_PASSAGEM) {
    char c = (char)Serial.read();
    atendidos++;

    if (c == '\r') {
      continue;
    }

    if (c == '\n') {
      linha[n] = '\0';
      processLine(linha);  // use a linha antes de reutilizá-la
      n = 0;
      continue;
    }

    if (n < sizeof(linha) - 1) {
      linha[n] = c;
      n++;
    }
  }
}

void loop() {
  receberSerial();
  // Outras tarefas do firmware continuam aqui.
}

Dica

Ordem que protege o vetor

A condição é verificada antes da escrita: n < sizeof(linha) - 1. Assim, quando n vale 15, não há espaço para outro caractere de conteúdo, mas ainda há espaço para colocar \0 se o próximo byte for LF.

Neste trecho, o limite de quatro bytes é uma escolha de cooperação com o restante do firmware; não é uma exigência da UART. O tratamento correto de uma linha que ultrapassa o limite será definido no próximo passo.

Verifique o limite antes de escrever

Caso-limite: 15 caracteres e LF

Complete as lacunas para o caso em que os 15 caracteres permitidos já foram armazenados e o próximo byte é LF.

cpp
char linha[16];
uint8_t n = 15;
char c = '\n';

if (c == '\n') {
  linha[] = ;
  processLine(linha);
  n = 0;
} else if (c != '\r' && n  sizeof(linha) - 1) {
  linha[n++] = c;
}

Passo 3 de 9

Valide antes de agir

Diferencie o fim de uma linha da validação completa do formato, para que somente mensagens permitidas possam produzir efeitos.

Delimite primeiro; valide depois

Linha terminada não é comando válido

Receber LF só informa que uma linha terminou. Ainda falta decidir se os bytes formam uma mensagem permitida pelo contrato.

Neste tutorial, o conteúdo de uma mensagem tem exatamente quatro caracteres:

  • Q:01 — pedido
  • R:01 — resposta

O primeiro caractere só pode ser Q ou R; o segundo é obrigatoriamente :; os dois últimos são algarismos do identificador. O LF chega depois desses quatro caracteres e não faz parte do conteúdo armazenado.

Assim, Q:01 pode seguir para tratamento. Já Q:01X, Q:0, Z:01 e uma linha vazia chegam completos ao LF, mas não são mensagens válidas.

Duas decisões separadas

Diagrama mostrando que detectar o fim de linha acontece antes de conferir os quatro bytes exigidos pelo protocolo.

O LF cria uma candidata a mensagem; a validação decide se ela pertence ao protocolo.

Dica

Por que preservar o tamanho

A rotina de recepção já conhece a quantidade de caracteres armazenados em n. Passe esse valor junto com a linha completa para a validação.

Não use apenas strlen() para decidir o tamanho: um byte \0 recebido como dado poderia encerrar a leitura textual antes da inspeção. O tamanho registrado permite examinar todos os bytes recebidos antes do LF.

Aceite somente o contrato completo

Uma regra exata para o formato

A validação confere primeiro o tamanho exato e só então acessa os campos fixos. Depois, verifica o tipo, o separador e cada algarismo do identificador.

Nada deve ser executado enquanto os caracteres ainda chegam. Somente a rotina chamada após o LF recebe uma linha candidata.

O que passa pela validação

Exemplos de conteúdo já concluído por LF. O LF não aparece na coluna de conteúdo.

Linha vazia, tipo desconhecido, byte de controle e qualquer byte extra são rejeitados.
Conteúdo recebidoResultadoMotivo
Q:01AceitaTipo, separador e 2 dígitos corretos
R:99AceitaFormato completo permitido
Q:0RejeitaFalta um dígito
Q:01XRejeitaHá um sufixo extra
Z:01RejeitaTipo desconhecido
Q:\0RejeitaByte de controle no ID
RejeitaLinha vazia não é mensagem

Só trate após validar

Validação antes do tratamento

Troque a chamada feita quando chega LF por processarLinhaCompleta(linha, n). A função abaixo usa o tamanho recebido, não strlen(), e só produz efeito depois de uma validação completa.

Uma linha inválida é ignorada: ela não executa comando, não vira pedido e não representa resposta válida.

Valide todos os campos antes de tratar

Trecho para chamar somente quando o receptor encontrar LF. Ele mantém o contrato Q:01 e R:01.

cpp
bool formatoValido(const char *linha, uint8_t n) {
  return n == 4 &&
         (linha[0] == 'Q' || linha[0] == 'R') &&
         linha[1] == ':' &&
         linha[2] >= '0' && linha[2] <= '9' &&
         linha[3] >= '0' && linha[3] <= '9';
}

void processarLinhaCompleta(const char *linha, uint8_t n) {
  if (!formatoValido(linha, n)) {
    return;  // Linha vazia, desconhecida ou malformada: sem efeito.
  }

  if (linha[0] == 'Q') {
    Serial.println(F("Pedido reconhecido"));
  } else {
    Serial.println(F("Resposta reconhecida"));
  }
}

Exemplo

Conecte ao receptor incremental

No receptor do step anterior, a chamada no caso de LF passa a ser:

processarLinhaCompleta(linha, n);

Faça essa chamada antes de zerar n. O \0 local ainda pode ser escrito para exibir a linha, mas a decisão de validade usa n e as posições do vetor.

Pratique a validação estrita

Pratique a rejeição de linhas parecidas

Complete a condição inteira que aceita somente um pedido ou resposta com os quatro bytes previstos. Ela deve rejeitar tanto Q:01X quanto Q:\0, mesmo que o começo pareça correto.

Complete a validação

Preencha os elementos que tornam o formato estrito.

cpp
bool formatoValido(const char *linha, uint8_t n) {
  return n ==  &&
         (linha[0] == 'Q' || linha[0] == 'R') &&
         linha[1] ==  &&
         linha[2] >= '0' && linha[2] <=  &&
         linha[3] >= '0' && linha[3] <= '9';
}

Passo 4 de 9

Descarte o excesso e recupere a sincronização

Recupere o enquadramento após uma mensagem UART longa demais, descartando seu restante até o LF sem interpretar o sufixo como outro comando.

Invalide a linha inteira

Quando a linha ultrapassa o limite

O vetor de 16 posições aceita no máximo 15 caracteres de conteúdo; a última posição é reservada para o terminador local \0. Se chegar mais um caractere de conteúdo antes do LF, a linha inteira é inválida.

Não processe os 15 primeiros caracteres como se fossem uma mensagem. Eles podem ser apenas o prefixo de uma linha excessiva. Em vez disso, entre no estado DESCARTANDO: pare de gravar no vetor e ignore tudo até encontrar o próximo LF.

O LF é a fronteira de recuperação

Diagrama de um fluxo UART: quinze caracteres cabem no vetor, o seguinte provoca descarte, os bytes posteriores são ignorados até LF e uma nova mensagem curta começa em um vetor vazio.

Depois do excesso, apenas o LF conhecido separa a linha inválida da próxima linha.

Dica

Regra de enquadramento

O descarte não tenta corrigir uma mensagem longa nem aproveitar o seu final. Todos os bytes até o próximo LF ainda pertencem à mesma linha inválida.

O CR continua sendo ignorado. Somente LF fecha a linha e, durante o descarte, autoriza o retorno à recepção normal.

Consuma até o delimitador

Recepção com estado de descarte

Trecho para integrar ao receptor incremental dos steps anteriores. Chame receberByte(Serial.read()) para cada byte disponível. A função de processamento recebe o vetor e o comprimento explícito, preservando a validação estrita do step anterior.

cpp
enum EstadoRx { RECEBENDO, DESCARTANDO };

char linha[16];
byte indice = 0;
EstadoRx estadoRx = RECEBENDO;

void receberByte(int recebido) {
  char c = (char)recebido;

  if (estadoRx == DESCARTANDO) {
    if (c == '\n') {
      indice = 0;
      estadoRx = RECEBENDO;
    }
    return;  // nunca armazena trecho de linha inválida
  }

  if (c == '\r') {
    return;  // CR é ignorado
  }

  if (c == '\n') {
    linha[indice] = '\0';
    processarLinhaCompleta(linha, indice);
    indice = 0;
    return;
  }

  if (indice < sizeof(linha) - 1) {
    linha[indice++] = c;
  } else {
    indice = 0;                 // invalida o prefixo armazenado
    estadoRx = DESCARTANDO;
  }
}

Por que zerar não basta

A comparação de limite ocorre antes da escrita, portanto nenhuma posição além de linha[15] é acessada.

Zerar indice ao detectar o excesso só invalida a cópia local. Isso não permite iniciar outra mensagem imediatamente: DESCARTANDO impede que o sufixo da linha longa seja tratado como um novo comando. Quando chega LF, processarLinhaCompleta() não é chamada para a linha excessiva; a próxima linha começa do zero.

Dica

Preserve o comprimento recebido

A chamada mantém o comprimento explícito: processarLinhaCompleta(linha, indice). Esse valor é necessário para a validação conferir os quatro bytes de Q:01 ou R:01 sem depender apenas de texto terminado em nulo.

Pratique o ponto de recuperação

Complete a recuperação após excesso

Complete as lacunas que protegem o vetor e restabelecem a recepção somente após o delimitador. O caractere que excede o limite não é armazenado; a linha longa inteira não chega à validação.

Estados e limite

Preencha a condição de espaço e os estados da máquina de recepção.

cpp
if () {
  linha[indice++] = c;
} else {
  indice = 0;
  estadoRx = ;
}

if (estadoRx == DESCARTANDO && c == '\n') {
  indice = 0;
  estadoRx = ;
}

Verifique a ressincronização

Reconstrua a sequência

Considere uma linha que já ocupou os 15 caracteres permitidos. Chega um 16º caractere, depois mais bytes, depois LF. Em seguida, chega Q:01\n, que cabe no vetor e respeita o formato do protocolo. Ordene os eventos.

Uma linha longa seguida de uma válida

Coloque os eventos na ordem em que ocorrem.

  1. Os bytes de Q:01 são armazenados; seu LF permite concluir e validar essa nova linha.
  2. O 16º caractere de conteúdo não cabe; a linha é invalidada e o estado passa para DESCARTANDO.
  3. Os bytes restantes da mesma linha são consumidos sem gravar no vetor.
  4. O LF é recebido; o índice é reinicializado e o estado volta para RECEBENDO.

Atenção

Limite local não recupera bytes perdidos

Excesso no vetor da aplicação não é o mesmo que perda de bytes no buffer interno da UART. Aqui, o firmware ainda leu os bytes e decidiu ignorá-los até LF. Esse mecanismo não recupera bytes que tenham sido perdidos antes de Serial.read().

Portanto, a ressincronização recupera uma fronteira conhecida do protocolo; ela não reconstrói dados ausentes.

Passo 5 de 9

Dê um prazo para completar a linha

Use um prazo de montagem para descartar linhas UART que ficam incompletas, sem interromper as demais tarefas do firmware.

Defina o relógio da montagem

Uma linha também pode expirar

Uma linha em montagem não pode ocupar o receptor para sempre. Use um timeout de montagem de 3 segundos, contado a partir do primeiro caractere de conteúdo.

A marca de início é registrada uma única vez. Cada byte posterior apenas completa a mesma linha: ele não reinicia o prazo. O caractere CR ('\r') continua ignorado e também não inicia nem prolonga a contagem.

Assim, enviar Q:01 muito lentamente por mais de 3 segundos não torna a mensagem aceitável só porque o LF chegou no final. Os 3 segundos facilitam os ensaios manuais no Monitor Serial; não são um valor universal para todo protocolo.

O prazo começa uma única vez

Linha do tempo mostrando o primeiro byte iniciando um prazo único, bytes posteriores sem reinício, vencimento e descarte até LF.

O relógio começa no primeiro caractere de conteúdo e permanece correndo até a linha terminar ou expirar.

Vença e descarte até LF

Expire sem parar o firmware

Verifique o vencimento em todas as passagens pelo loop(), inclusive quando Serial.available() é zero. Não use delay() nem espere em um laço pela chegada do próximo byte.

Quando o prazo vence, o conteúdo parcial é invalidado: zere indice, desative linhaAtiva e entre em DESCARTANDO. Nesse estado, os bytes são consumidos sem armazenamento até chegar LF. Esse LF recupera a fronteira entre linhas, mas não valida a mensagem que expirou.

A política para linha vazia continua a mesma dos steps anteriores: quando LF chega durante a recepção normal, processarLinhaCompleta(linha, indice) é chamado mesmo se indice for zero. A validação então rejeita a linha vazia sem efeito.

Timeout integrado ao receptor

Acrescente o timeout ao receptor incremental dos steps anteriores. Mantenha o comprimento explícito ao entregar uma linha completa para a validação.

cpp
const unsigned long PRAZO_LINHA_MS = 3000UL;

bool linhaAtiva = false;
unsigned long inicioLinha;

void verificarTimeoutMontagem(unsigned long agora) {
  if (estadoRx == RECEBENDO && linhaAtiva &&
      (unsigned long)(agora - inicioLinha) >= PRAZO_LINHA_MS) {
    indice = 0;                 // invalida o conteúdo parcial
    linhaAtiva = false;
    estadoRx = DESCARTANDO;     // consome bytes até LF
  }
}

void receberByte(int recebido) {
  char c = (char)recebido;

  if (estadoRx == DESCARTANDO) {
    if (c == '\n') {
      indice = 0;
      estadoRx = RECEBENDO;
    }
    return;
  }

  if (c == '\r') return;       // CR não inicia nem prolonga prazo

  if (c == '\n') {
    linha[indice] = '\0';
    processarLinhaCompleta(linha, indice); // inclusive se indice for 0
    indice = 0;
    linhaAtiva = false;
    return;
  }

  if (!linhaAtiva) {
    linhaAtiva = true;
    inicioLinha = millis();     // somente o primeiro conteúdo marca o início
  }

  if (indice < sizeof(linha) - 1) {
    linha[indice++] = c;
  } else {
    indice = 0;
    linhaAtiva = false;
    estadoRx = DESCARTANDO;     // excesso também descarta até LF
  }
}

void loop() {
  verificarTimeoutMontagem(millis());

  uint8_t atendidos = 0;
  while (Serial.available() > 0 && atendidos < MAX_POR_PASSAGEM) {
    receberByte(Serial.read());
    atendidos++;
  }

  // Outras tarefas do firmware continuam aqui.
}

Dica

Descarte não é bloqueio

Se a linha expirar e nenhum LF chegar, o firmware permanece em DESCARTANDO, mas continua executando o restante do loop(). Um LF explícito, mesmo muito depois, encerra esse descarte e permite começar uma nova linha. Não há espera ativa nem tentativa de aproveitar o conteúdo vencido.

Verifique o comportamento após o prazo

Complete a condição de vencimento

Complete o limite da condição que detecta o timeout de uma linha em montagem.

cpp
if (estadoRx == RECEBENDO && linhaAtiva &&
    (unsigned long)(agora - inicioLinha) >= ) {
  indice = 0;
  linhaAtiva = false;
  estadoRx = DESCARTANDO;
}

Preveja a recuperação

O primeiro caractere de uma linha chega. Depois, o Monitor Serial não envia mais nada por mais de 3 segundos. Por fim, chega um LF. O que o receptor deve fazer?

Passo 6 de 9

Envie mensagens curtas sem esperar por espaço

Envie mensagens UART curtas inteiras apenas quando o buffer de transmissão tiver espaço suficiente, sem interromper o restante do loop.

Decida pelo pacote completo

Espaço antes do envio

A saída da UART também usa um buffer finito. Antes de colocar uma mensagem curta nele, consulte o espaço livre com Serial.availableForWrite().

A regra é enviar a mensagem inteira somente quando houver espaço para todos os bytes dela. Se faltar espaço, não espere em um laço e não envie apenas o começo: mantenha uma única saída pendente e tente novamente na próxima passagem pelo loop().

Assim, enquanto a UART libera espaço, o firmware continua atendendo a recepção incremental, os timeouts e as demais tarefas.

A mensagem inteira entra ou aguarda

Diagrama comparando uma mensagem de cinco bytes que entra no buffer de transmissão quando há cinco posições livres com a mesma mensagem mantida pendente quando há somente três posições.

Compare o espaço livre com todos os bytes da mensagem, incluindo o LF.

Conte os bytes transmitidos

A resposta do contrato deste tutorial é R:01\n. O LF faz parte do envio; o terminador nulo serve somente para representar a string na memória do C/C++.

A string local precisa de uma posição adicional para `\0`, mas ela não é transmitida.
ElementoQuantidadeVai pela UART?
`R`, `:`, `0`, `1`4 bytesSim
LF (`\n`)1 byteSim
Nulo (`\0`)1 byte localNão
Total exigido no buffer5 bytesSim

Adie sem perder a saída

Envio não bloqueante de uma saída pendente

Mantenha uma única mensagem pendente e chame atenderSaida() pelo fluxo principal a cada passagem do loop(). A string usa a mesma resposta R:01\n definida no protocolo.

cpp
const char resposta[] = "R:01\n";
const size_t bytesResposta = sizeof(resposta) - 1;
bool respostaPendente = true;

void atenderSaida() {
  if (!respostaPendente) {
    return;
  }

  if (Serial.availableForWrite() < bytesResposta) {
    return;  // adia; não espera e não envia um prefixo
  }

  Serial.write((const uint8_t *)resposta, bytesResposta);
  respostaPendente = false;
}

void loop() {
  atenderSaida();
  // Atendimento incremental da recepção e das demais tarefas.
}

Atenção

Aceitar no buffer não é entregar

Mantenha a consulta e a escrita no fluxo principal, com um único responsável por chamar Serial.write(). Outra escrita entre a consulta e o envio poderia ocupar o espaço que acabava de ser medido.

Quando Serial.write() aceita os bytes, eles foram colocados no buffer de transmissão. Isso não comprova que já saíram fisicamente pela UART nem que o interlocutor os recebeu. Não use Serial.flush() como solução: ele aguarda a transmissão terminar e pode bloquear o programa.

Pratique a decisão de envio

Envie ou adie sem bloquear

Complete a condição que preserva a mensagem inteira. Se não houver espaço para os cinco bytes de R:01\n, a função retorna e deixa respostaPendente verdadeira para uma passagem futura do loop().

Complete a decisão de transmissão

Preencha as duas chamadas da decisão de envio.

cpp
if (Serial.() < bytesResposta) {
  return;
}

Serial.((const uint8_t *)resposta, bytesResposta);
respostaPendente = false;

Passo 7 de 9

Espere por uma resposta, mas só até o prazo

Implemente uma tentativa UART com envio pendente, prazo de resposta de 10 segundos e resultado definido mesmo sem resposta.

Modele a espera como uma tentativa

Uma tentativa por vez

Uma tentativa usa um único pedido e tem três estados:

  • LIVRE: não há tentativa ativa.
  • ENVIO_PENDENTE: o pedido foi preparado e entrou na única fila de saída, mas ainda não foi aceito pelo buffer de transmissão.
  • AGUARDANDO: os cinco bytes de Q:nn\n já foram aceitos pelo buffer; agora existe uma resposta pendente.

O resultado final é SUCESSO ou SEM_RESPOSTA, e então o estado volta para LIVRE.

No Monitor Serial, Q:01 é um comando local de teste: ele solicita uma tentativa. O Uno transmite Q:01\n; depois, você digita R:01\n como interlocutor simulado. Use um identificador ainda não reutilizado em cada ensaio.

Fluxo de uma tentativa

Diagrama mostrando os estados de envio pendente, espera por resposta e os finais de sucesso ou timeout.

O relógio da resposta começa quando o pedido inteiro entra no buffer de transmissão, não quando o comando é digitado.

Dois relógios, dois problemas

Há dois prazos independentes. Eles resolvem problemas diferentes e não devem ser misturados.

Comparação entre o timeout da linha recebida e o timeout da tentativa.
PrazoComeça quandoAo vencer
Montagem: 3 sChega o primeiro caractere de conteúdoInvalida a linha parcial e descarta até LF
Resposta: 10 sO pedido inteiro entra no buffer de transmissãoEncerra a tentativa como sem resposta

Inicie o prazo após o envio inteiro

Prepare primeiro; conte depois

O prazo de resposta começa somente após Serial.write() aceitar todos os cinco bytes de Q:nn\n: Q, :, dois algarismos e LF. Enquanto a fila ainda não puder aceitar o pedido, ou enquanto o buffer UART não tiver espaço, a tentativa não passa a aguardar resposta.

O instante registrado inclui o tempo de transmissão física que ainda falta e o tempo para o interlocutor responder. Colocar bytes no buffer não prova que eles já chegaram ao outro lado.

Fila de saída e início seguro do prazo

Este bloco substitui a saída fixa do passo anterior por uma única fila que também identifica o tipo da mensagem. Ele pressupõe que formatoValido() e a recepção incremental já existem.

cpp
enum EstadoTentativa { LIVRE, ENVIO_PENDENTE, AGUARDANDO };
enum ResultadoTentativa { SEM_RESULTADO, SUCESSO, SEM_RESPOSTA };
enum TipoSaida { SAIDA_NENHUMA, SAIDA_PEDIDO };

const unsigned long PRAZO_RESPOSTA_MS = 10000UL;

char saida[16];
byte bytesSaida = 0;
bool saidaPendente = false;
TipoSaida tipoSaida = SAIDA_NENHUMA;

EstadoTentativa estadoTentativa = LIVRE;
ResultadoTentativa resultadoTentativa = SEM_RESULTADO;
unsigned long inicioResposta;
char pedido[6];       // Q:nn\n e \0
char idEsperado[2];   // os dois algarismos

bool enfileirar(const char *texto, TipoSaida tipo) {
  if (saidaPendente) return false;

  byte n = strlen(texto);
  if (n >= sizeof(saida)) return false;

  memcpy(saida, texto, n + 1);
  bytesSaida = n;
  tipoSaida = tipo;
  saidaPendente = true;
  return true;
}

void iniciarTentativa(const char *texto) {
  if (estadoTentativa != LIVRE) return;

  pedido[0] = 'Q'; pedido[1] = ':';
  pedido[2] = texto[2]; pedido[3] = texto[3];
  pedido[4] = '\n'; pedido[5] = '\0';

  // Sem espaço na única fila: não cria tentativa nem prazo fictício.
  if (!enfileirar(pedido, SAIDA_PEDIDO)) return;

  idEsperado[0] = texto[2];
  idEsperado[1] = texto[3];
  resultadoTentativa = SEM_RESULTADO;
  estadoTentativa = ENVIO_PENDENTE;
}

void atenderSaida() {
  if (!saidaPendente) return;
  if (Serial.availableForWrite() < bytesSaida) return;

  TipoSaida enviada = tipoSaida;
  Serial.write((const uint8_t *)saida, bytesSaida);
  saidaPendente = false;
  tipoSaida = SAIDA_NENHUMA;

  if (enviada == SAIDA_PEDIDO &&
      estadoTentativa == ENVIO_PENDENTE) {
    inicioResposta = millis();
    estadoTentativa = AGUARDANDO;
  }
}

Dica

O que não reinicia o prazo

Caracteres parciais, linhas inválidas ou qualquer outro dado recebido não reiniciam o prazo de resposta. Esses eventos pertencem ao receptor; eles não confirmam que o interlocutor respondeu ao pedido atual.

Conecte a resposta ao resultado

Sucesso exige uma resposta completa

Verifique o timeout de resposta em toda passagem pelo loop(), mesmo quando não chegou byte algum. Ao vencer, a tentativa recebe SEM_RESPOSTA e volta a LIVRE.

Uma linha só gera sucesso se já estiver completa e válida, a tentativa estiver em AGUARDANDO e os dois algarismos coincidirem com idEsperado. A recepção parcial continua sujeita ao timeout de montagem de 3 segundos implementado antes.

Encerre por sucesso ou ausência

Use estas funções junto à fila de saída e ao receptor já construído. tratarLinhaCompleta() deve ser chamado somente depois do LF, com o comprimento explícito da linha.

cpp
void verificarTimeoutResposta(unsigned long agora) {
  if (estadoTentativa == AGUARDANDO &&
      (unsigned long)(agora - inicioResposta) >= PRAZO_RESPOSTA_MS) {
    resultadoTentativa = SEM_RESPOSTA;
    estadoTentativa = LIVRE;
  }
}

void tratarLinhaCompleta(const char *texto, byte n) {
  verificarTimeoutResposta(millis());

  if (!formatoValido(texto, n)) return;

  if (texto[0] == 'Q') {
    iniciarTentativa(texto);
    return;
  }

  if (texto[0] == 'R' &&
      estadoTentativa == AGUARDANDO &&
      texto[2] == idEsperado[0] &&
      texto[3] == idEsperado[1]) {
    resultadoTentativa = SUCESSO;
    estadoTentativa = LIVRE;
  }
}

void loop() {
  verificarTimeoutResposta(millis());
  receberSerial();       // recepção incremental dos passos anteriores
  atenderSaida();        // pode iniciar AGUARDANDO após a escrita
  // As demais tarefas continuam aqui.
}

Dica

Ressincronizar não é confirmar resposta

O timeout de montagem e o timeout de resposta são independentes. Se uma linha parcial expirar, o receptor invalida essa linha e descarta bytes até LF para recuperar a fronteira. A tentativa que já aguarda resposta não recebe sucesso por isso: ela só termina por uma resposta correspondente ou pelo próprio prazo de 10 segundos.

Pratique as transições

Verifique as duas saídas

Complete as transições que encerram uma tentativa. A primeira condição representa ausência de resposta após 10 segundos. A segunda registra sucesso apenas para uma resposta completa, válida e com o identificador esperado.

Timeout ou sucesso

Preencha os estados e resultados das transições finais.

cpp
void verificarTimeoutResposta(unsigned long agora) {
  if (estadoTentativa ==  &&
      (unsigned long)(agora - inicioResposta) >= PRAZO_RESPOSTA_MS) {
    resultadoTentativa = ;
    estadoTentativa = LIVRE;
  }
}

if (linha[0] == 'R' &&
    estadoTentativa == AGUARDANDO &&
    linha[2] == idEsperado[0] && linha[3] == idEsperado[1]) {
  resultadoTentativa = ;
  estadoTentativa = ;
}

Passo 8 de 9

Não confunda resposta atrasada com sucesso

Evite que respostas antigas, inesperadas, tardias ou parciais sejam confundidas com o sucesso da tentativa atual.

Aceite somente a resposta da tentativa atual

A resposta precisa pertencer à tentativa atual

Uma resposta só representa sucesso quando três condições coincidem: existe uma tentativa em AGUARDANDO, os dois algarismos são iguais a idEsperado e o prazo de 10 segundos ainda não venceu.

Exemplo: se o Uno enviou Q:08\n, apenas R:08\n recebido a tempo encerra a tentativa com SUCESSO. Uma antiga R:07\n tem formato válido, mas é inesperada para a tentativa 08.

Use identificadores diferentes nos ensaios. Isso ajuda a rejeitar uma resposta antiga; não é uma garantia para uma sessão longa em que os identificadores sejam reutilizados.

Estado, identificador e prazo

Diagrama mostra que uma resposta é aceita apenas se corresponder ao identificador esperado e chegar antes do prazo; respostas diferentes ou atrasadas não chegam ao sucesso.

O formato da resposta não basta: o contexto da tentativa decide seu significado.

Mantenha o prazo intacto

Eventos não renovam a espera

Considere que Q:08\n já entrou inteiro no buffer de transmissão e a tentativa está em AGUARDANDO. O prazo de resposta foi iniciado nesse momento.

Somente sucesso ou vencimento encerram a tentativa. Os demais eventos não alteram `inicioResposta`.
EventoClassificaçãoEfeito
`R:08\n` antes do prazoResposta esperadaRegistra sucesso e encerra
`R:07\n` durante espera por 08InesperadaMantém a espera de 08
Linha inválida ou desconhecidaRejeitadaMantém a espera de 08
Nenhum byte recebidoSem eventoPrazo continua correndo
Prazo vencidoSem respostaEncerra a tentativa
`R:08\n` após vencimentoSem tentativa pendenteNão reabre o resultado

Dica

O prazo tem precedência

Cheque o vencimento antes de aceitar uma resposta. Assim, se o prazo já venceu quando uma linha R:08 é atendida, a tentativa termina como SEM_RESPOSTA; a linha não pode restaurar o estado AGUARDANDO nem produzir sucesso.

Depois de encerrada, uma resposta recebida sem tentativa pendente não muda resultadoTentativa. Ela pode ser considerada inesperada para fins de observação, mas não é uma confirmação tardia.

Atualize o tratamento sem trocar a arquitetura

Encerre uma vez e preserve o resultado

Substitua as funções de timeout e de tratamento de linha do step anterior por estas versões. Elas preservam EstadoTentativa, ResultadoTentativa, estadoTentativa, resultadoTentativa, inicioResposta, idEsperado e PRAZO_RESPOSTA_MS.

cpp
void encerrarSemResposta() {
  if (estadoTentativa != AGUARDANDO) return;

  resultadoTentativa = SEM_RESPOSTA;  // registro de um evento
  estadoTentativa = LIVRE;

  // Se havia uma linha ainda em montagem, ela não pode atravessar
  // a fronteira entre tentativas.
  if (estadoRx == RECEBENDO && linhaAtiva) {
    indice = 0;
    linhaAtiva = false;
    estadoRx = DESCARTANDO;
  }
}

void verificarTimeoutResposta(unsigned long agora) {
  if (estadoTentativa == AGUARDANDO &&
      (unsigned long)(agora - inicioResposta) >= PRAZO_RESPOSTA_MS) {
    encerrarSemResposta();
  }
}

void tratarLinhaValida(const char *linha, byte n) {
  verificarTimeoutResposta(millis());  // antes de aceitar sucesso
  if (!formatoValido(linha, n)) return;

  if (linha[0] == 'Q') {
    iniciarTentativa(linha);
    return;
  }

  if (linha[0] != 'R' || estadoTentativa != AGUARDANDO) {
    return;  // resposta sem tentativa pendente: não muda resultado
  }

  if (linha[2] != idEsperado[0] || linha[3] != idEsperado[1]) {
    return;  // resposta inesperada: não reinicia o prazo
  }

  resultadoTentativa = SUCESSO;  // registro de um único evento
  estadoTentativa = LIVRE;
}

Feche a linha antes de tratá-la

No ramo que trata LF do receptor, faça a limpeza do estado de montagem antes de chamar tratarLinhaValida(). Isso permite que um timeout de resposta detectado nessa função diferencie uma linha completa de uma linha ainda parcial.

cpp
if (c == '\n') {
  linha[indice] = '\0';
  byte nCompleta = indice;
  indice = 0;
  linhaAtiva = false;
  tratarLinhaValida(linha, nCompleta);
  return;
}

Uma linha parcial não atravessa o timeout

Chame verificarTimeoutResposta(millis()) em toda passagem pelo loop(), como no step anterior. Se ela vencer enquanto linhaAtiva for verdadeira, encerrarSemResposta() limpa o conteúdo parcial e coloca o receptor em DESCARTANDO. Os bytes restantes são consumidos até LF, sem serem interpretados como a próxima mensagem.

resultadoTentativa é alterado somente em transições: para SUCESSO ou SEM_RESPOSTA. Não envie continuamente textos como “aguardando” pelo loop(): mensagens repetidas podem ocupar o buffer de transmissão. Se você decidir exibir resultados, faça uma única emissão quando esse resultado mudar.

Verifique a coerência da tentativa

Classifique respostas antigas, tardias e parciais

Relacione cada evento ao tratamento correto. Em todos os casos, a tentativa 08 já havia sido iniciada por Q:08\n.

Toque em um item e depois no par correspondente.

Passo 9 de 9

Teste as falhas e confirme a recuperação

Teste o sketch integrado no Monitor Serial, force falhas de delimitação e prazo e confirme a recuperação sem interromper o firmware.

Monte o sketch integrado

Prepare o ensaio

Use somente o Arduino Uno R3 conectado por USB ao computador. Não conecte dispositivos externos aos pinos 0 e 1: a ponte USB–UART e o Monitor Serial serão o interlocutor.

Os três blocos a seguir compõem um único sketch, na ordem apresentada. O comando local Q:nn inicia uma tentativa; o Uno transmite Q:nn\n. Somente R:nn\n, com o mesmo identificador e antes de 10 s, registra sucesso.

O LED integrado pisca enquanto há uma linha em montagem, descarte, envio pendente ou espera por resposta. Isso oferece uma evidência visual de que o loop() continua sendo executado.

USB, Monitor Serial e LED

Diagrama de um Uno conectado por USB ao Monitor Serial, com fluxos de mensagens, caminho de descarte até LF e LED integrado de atividade.

Nesta prática, o computador simula o interlocutor UART. Os pinos 0 e 1 permanecem sem conexões externas.

Sketch integrado — estados, saída e validação

Bloco 1 de 3 — coloque no início de um arquivo novo na IDE. A fila de saída guarda também o tipo da mensagem: somente uma saída marcada como SAIDA_PEDIDO pode iniciar o prazo de resposta.

cpp
enum EstadoRx { RECEBENDO, DESCARTANDO };
enum EstadoTentativa { LIVRE, ENVIO_PENDENTE, AGUARDANDO };
enum ResultadoTentativa { SEM_RESULTADO, SUCESSO, SEM_RESPOSTA };
enum TipoSaida { SAIDA_NENHUMA, SAIDA_DIAGNOSTICO, SAIDA_PEDIDO };

const byte MAX_POR_PASSAGEM = 4;
const unsigned long PRAZO_LINHA_MS = 3000UL;
const unsigned long PRAZO_RESPOSTA_MS = 10000UL;

char linha[16];
byte indice = 0;
EstadoRx estadoRx = RECEBENDO;
bool linhaAtiva = false;
unsigned long inicioLinha;

char saida[16];
byte bytesSaida = 0;
bool saidaPendente = false;
TipoSaida tipoSaida = SAIDA_NENHUMA;

EstadoTentativa estadoTentativa = LIVRE;
ResultadoTentativa resultadoTentativa = SEM_RESULTADO;
unsigned long inicioResposta;
char pedido[6];
char idEsperado[2];

bool enfileirar(const char *texto, TipoSaida tipo) {
  if (saidaPendente) return false;

  byte n = strlen(texto);
  if (n >= sizeof(saida)) return false;

  memcpy(saida, texto, n + 1);
  bytesSaida = n;
  tipoSaida = tipo;
  saidaPendente = true;
  return true;
}

void diagnosticar(const char *texto) {
  enfileirar(texto, SAIDA_DIAGNOSTICO);
}

bool formatoValido(const char *texto, byte n) {
  return n == 4 &&
         (texto[0] == 'Q' || texto[0] == 'R') &&
         texto[1] == ':' &&
         texto[2] >= '0' && texto[2] <= '9' &&
         texto[3] >= '0' && texto[3] <= '9';
}

Dica

Proteção contra pedido não enviado

O comando que inicia a tentativa só muda o estado para ENVIO_PENDENTE se enfileirar() tiver aceitado o pedido. Além disso, a rotina de transmissão guarda o tipo que foi enviado e inicia inicioResposta apenas quando transmite aquele pedido. Uma mensagem de diagnóstico nunca pode iniciar esse relógio.

Complete o arquivo e confira a saída

Sketch integrado — tentativa, prazos e recepção

Bloco 2 de 3 — continue imediatamente após o bloco anterior.

cpp
void iniciarTentativa(const char *texto) {
  if (estadoTentativa != LIVRE) return;

  pedido[0] = 'Q'; pedido[1] = ':';
  pedido[2] = texto[2]; pedido[3] = texto[3];
  pedido[4] = '\n'; pedido[5] = '\0';
  idEsperado[0] = texto[2];
  idEsperado[1] = texto[3];

  // Sem espaço na única fila: não cria tentativa nem prazo fictício.
  if (!enfileirar(pedido, SAIDA_PEDIDO)) return;

  resultadoTentativa = SEM_RESULTADO;
  estadoTentativa = ENVIO_PENDENTE;
}

void encerrarSemResposta() {
  if (estadoTentativa != AGUARDANDO) return;

  resultadoTentativa = SEM_RESPOSTA;
  estadoTentativa = LIVRE;
  diagnosticar("SEM RESPOSTA\n");

  if (estadoRx == RECEBENDO && linhaAtiva) {
    indice = 0;
    linhaAtiva = false;
    estadoRx = DESCARTANDO;
  }
}

void verificarTimeouts(unsigned long agora) {
  if (estadoRx == RECEBENDO && linhaAtiva &&
      (unsigned long)(agora - inicioLinha) >= PRAZO_LINHA_MS) {
    indice = 0;
    linhaAtiva = false;
    estadoRx = DESCARTANDO;
    diagnosticar("LINHA EXPIROU\n");
  }

  if (estadoTentativa == AGUARDANDO &&
      (unsigned long)(agora - inicioResposta) >= PRAZO_RESPOSTA_MS) {
    encerrarSemResposta();
  }
}

void tratarLinhaCompleta(const char *texto, byte n) {
  verificarTimeouts(millis());  // vencimento tem precedência

  if (!formatoValido(texto, n)) {
    diagnosticar("INVALIDA\n");
    return;
  }

  if (texto[0] == 'Q') {
    iniciarTentativa(texto);
    return;
  }

  if (estadoTentativa != AGUARDANDO ||
      texto[2] != idEsperado[0] || texto[3] != idEsperado[1]) {
    diagnosticar("INESPERADA\n");
    return;
  }

  resultadoTentativa = SUCESSO;
  estadoTentativa = LIVRE;
  diagnosticar("SUCESSO\n");
}

void receberByte(int recebido) {
  char c = (char)recebido;

  if (estadoRx == DESCARTANDO) {
    if (c == '\n') {
      indice = 0;
      linhaAtiva = false;
      estadoRx = RECEBENDO;
    }
    return;
  }

  if (c == '\r') return;

  if (c == '\n') {
    linha[indice] = '\0';
    byte nCompleta = indice;
    indice = 0;
    linhaAtiva = false;
    tratarLinhaCompleta(linha, nCompleta);
    return;
  }

  if (!linhaAtiva) {
    linhaAtiva = true;
    inicioLinha = millis();
  }

  if (indice < sizeof(linha) - 1) {
    linha[indice++] = c;
  } else {
    indice = 0;
    linhaAtiva = false;
    estadoRx = DESCARTANDO;
    diagnosticar("LINHA LONGA\n");
  }
}

Sketch integrado — loop, transmissão e LED

Bloco 3 de 3 — finalize o mesmo arquivo, grave no Uno e abra o Monitor Serial em 9600 baud.

cpp
void receberSerial() {
  byte atendidos = 0;
  while (Serial.available() > 0 && atendidos < MAX_POR_PASSAGEM) {
    verificarTimeouts(millis());
    receberByte(Serial.read());
    atendidos++;
  }
}

void atenderSaida() {
  if (!saidaPendente) return;
  if (Serial.availableForWrite() < bytesSaida) return;

  TipoSaida enviada = tipoSaida;
  Serial.write((const uint8_t *)saida, bytesSaida);
  saidaPendente = false;
  tipoSaida = SAIDA_NENHUMA;

  // Só o pedido efetivamente aceito pelo buffer inicia a espera.
  if (enviada == SAIDA_PEDIDO && estadoTentativa == ENVIO_PENDENTE) {
    inicioResposta = millis();
    estadoTentativa = AGUARDANDO;
  }
}

void atualizarLed() {
  static unsigned long ultimoLed = 0;
  static bool nivelLed = false;
  bool ativo = linhaAtiva || estadoRx == DESCARTANDO ||
               estadoTentativa == ENVIO_PENDENTE ||
               estadoTentativa == AGUARDANDO;

  if (!ativo) {
    nivelLed = false;
    digitalWrite(LED_BUILTIN, LOW);
    return;
  }

  if ((unsigned long)(millis() - ultimoLed) >= 150UL) {
    ultimoLed = millis();
    nivelLed = !nivelLed;
    digitalWrite(LED_BUILTIN, nivelLed);
  }
}

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  Serial.begin(9600);
}

void loop() {
  verificarTimeouts(millis());
  receberSerial();
  verificarTimeouts(millis());
  atenderSaida();
  atualizarLed();
}
Saída esperada
Q:01
SUCESSO
LINHA EXPIROU
SEM RESPOSTA

A saída é representativa. Os textos dependem dos ensaios enviados e os timeouts dependem da espera real de 3 s ou 10 s.

Force falhas e observe a recuperação

Execute os ensaios em sequência

Abra o Monitor Serial em 9600 baud. Para os casos usuais, selecione Nova linha (LF). No ensaio CRLF, selecione CR e LF. Aguarde cada resultado final antes de começar a próxima tentativa.

No teste parcial, há dois eventos independentes: após 3 s, a linha parcial expira e o receptor entra em descarte; o LF posterior apenas recupera a fronteira. A tentativa iniciada antes continua aguardando até receber a resposta correta ou vencer o prazo de 10 s.

Matriz de comportamento esperado

Use um identificador novo para cada tentativa iniciada. As linhas de resposta só têm efeito quando correspondem a uma tentativa pendente.

O LED pisca durante montagem, descarte, envio pendente e espera por resposta. Ele apaga quando nenhuma atividade permanece pendente.
EnsaioAção no Monitor SerialResultado e recuperação esperados
Válido com LFCom LF, envie `Q:01`. Ao aparecer `Q:01`, envie `R:01`.Aparece `SUCESSO`; o LED para de piscar.
Válido com CRLFSelecione CR e LF. Envie `Q:02`; ao aparecer o pedido, envie `R:02`.Aparece `SUCESSO`. O CR não cria uma linha extra.
Conteúdo inválidoVolte para LF e envie `X:01`.Aparece `INVALIDA`. Nenhuma tentativa é criada.
Linha excessivaEnvie `1234567890123456` com LF. Depois, envie `Q:03` e responda `R:03`.Aparece `LINHA LONGA`; o LF recupera o receptor. A tentativa 03 termina em `SUCESSO`.
Parcial e timeoutEnvie `Q:04`; ao aparecer o pedido, envie `R:04` com Sem final de linha. Espere 3 s e envie somente LF. Depois aguarde.Aparece `LINHA EXPIROU`. O LF ressincroniza o receptor; posteriormente aparece `SEM RESPOSTA` para a tentativa 04.
Sem respostaEnvie `Q:05` e não envie resposta.Após cerca de 10 s aparece `SEM RESPOSTA`; o LED para.
Resposta tardiaDepois de `SEM RESPOSTA` da 05, envie `R:05` com LF.Aparece `INESPERADA`; a tentativa encerrada não é reaberta.
ID diferenteEnvie `Q:06`; ao aparecer o pedido, envie `R:07`. Aguarde o prazo.Aparece `INESPERADA`; depois aparece `SEM RESPOSTA`. O ID errado não renova o prazo.

Vincule o prazo ao pedido transmitido

Complete a condição que impede uma saída de diagnóstico de iniciar o prazo de resposta. O instante só deve ser registrado quando a mensagem transmitida for o pedido da tentativa que está em ENVIO_PENDENTE.

Inicie o prazo apenas para o pedido

Preencha a transição que ocorre depois de Serial.write().

cpp
TipoSaida enviada = tipoSaida;
Serial.write((const uint8_t *)saida, bytesSaida);
saidaPendente = false;
tipoSaida = SAIDA_NENHUMA;

if (enviada ==  &&
    estadoTentativa == ) {
  inicioResposta = ;
  estadoTentativa = ;
}

Registre a evidência do ensaio

Depois de executar ao menos um caso válido, uma linha longa ou inválida e o ensaio parcial, relate as evidências observadas. Diferencie claramente o LF que recupera o receptor do evento que encerra a tentativa.

Escreva pelo menos 120 caracteres (0/120).

Comprove e conclua

Resumo

Síntese final

  • O LF delimita a mensagem; CR é ignorado, permitindo LF ou CRLF sem criar duas linhas.
  • O receptor só trata uma linha após conferir o limite do vetor e o formato exato Q:nn ou R:nn.
  • Excesso e timeout de montagem invalidam a linha e descartam bytes até LF, sem bloquear o restante do firmware.
  • O LF após uma linha expirada recupera o receptor, mas não encerra automaticamente uma tentativa já pendente.
  • Uma resposta só produz sucesso se estiver completa, dentro do prazo e tiver o identificador esperado; respostas tardias ou com outro ID são inesperadas.
  • O prazo de resposta começa apenas depois de o pedido correto entrar no buffer de transmissão; uma mensagem de diagnóstico não pode criar uma tentativa fictícia.

Tutorial concluído

Parabéns! Você concluiu: Mensagens UART com delimitação e timeout

Você concluiu os ensaios de mensagens UART delimitadas com validação, descarte e timeout não bloqueantes.

Baixe o Aplicativo agora para ter acesso a + de 5000 cursos gratuitos, exercícios, certificado e muito conteúdo sem pagar nada!

  • Cursos online 100% gratuitos do início ao fim

    Milhares de cursos online em vídeo, ebooks e áudiobooks.

  • Mais de 60 mil exercícios gratuitos

    Para testar seus conhecimentos no decorrer dos cursos online

  • Certificado Digital gratuito válido em todo o Brasil

    Gerado diretamente na galeria de fotos do seu celular e enviado ao seu e-mail

Aplicativo Cursa na tela de ebook, na tela de curso em vídeo e na tela de exercícios do curso, mais o certificado de conclusão de curso