Dando continuidade ao meu tutorial do protocolo UBX, eu procurarei tentar responder as seguintes perguntas de modo a poder realmente utilizar o protocolo UBX com as informações julgadas necessárias para meu navegador inercial. Repito-as aqui:
- Como enviar dados para o receptor, caso ele seja do tipo get/set?
- Caso, a mensagem seja da classe CFG, por exemplo, como receber os dados de UBX-ACK e processá-los para confirmar se deu tudo certo (ACK) ou se deu errado (NAK).
- Como configurar as mensagens que eu desejo receber rotineiramente por polling?
- Como eu faço para ativar e/ou desativar determinadas mensagens para envio pelo receptor? Ou seja, se eu quiser fazer meu receptor enviar apenas mensagens do tipo UBX e não mais do protocolo NMEA (como esse estudo de caso se propõe).
- Como fazer o parsing da mensagens de modo a eu conseguir trabalhar com dados de engenharia.
Para a primeira pergunta, eu segui as diretrizes presentes no próprio documento da u-blox. Eu preciso especificar cada um dos itens da mensagem e ao final ter o checksum perfeitinho. Para configurar isso direitinho, eu fiz a seguinte função em C/C++:
// =============================================
// Aqui vai ao sabor do cliente: a depender de
// qual serial ele ira utilizar de modo dedicado
// =============================================
// #define NEO_M8N Serial2
// #define NEO_M8N Serial1
void UBS_send_message(const uint8_t CLASS,
const uint8_t ID,
uint16_t Lenght,
const uint8_t *Payload) {
// Header -- sempre inicia assim.
NEO_M8N.write(0xB5);
NEO_M8N.write(0x62);
// Envia CLASS, ID, Lenght e Payload.
NEO_M8N.write(CLASS);
NEO_M8N.write(ID);
NEO_M8N.write(static_cast<uint8_t>(Lenght & 0xFF)); // LSB
NEO_M8N.write(static_cast<uint8_t>((Lenght >> 8) & 0xFF)); // MSB
for (size_t i = 0 ; i < Lenght ; i++){
NEO_M8N.write(Payload[i]);
}
// Calcula Checksum e os envia.
uint8_t checksum_a, checksum_b;
UBX_checksum(CLASS, ID, Lenght, Payload, checksum_a, checksum_b);
NEO_M8N.write(checksum_a);
NEO_M8N.write(checksum_b);
}
Aqui eu meio que repito a função UBX_checksum já apresentada na primeira parte deste tutorial:
uint8_t UBX_checksum(const uint8_t CLASS,
const uint8_t ID,
uint16_t Lenght,
const uint8_t *Payload,
uint8_t &checksum_a,
uint8_t &checksum_b) {
// Inicia as variáveis de saída.
checksum_a = 0; checksum_b = 0;
// CLASS
checksum_a = checksum_a + CLASS;
checksum_b = checksum_b + checksum_a;
// ID
checksum_a = checksum_a + ID;
checksum_b = checksum_b + checksum_a;
// Lenght -- lembre-se de que é 16 bits.
checksum_a = checksum_a + static_cast<uint8_t>(Lenght & 0xFF); // LSB
checksum_b = checksum_b + checksum_a;
checksum_a = checksum_a + static_cast<uint8_t>((Lenght >> 8) & 0xFF); // MSB
checksum_b = checksum_b + checksum_a;
// Realiza as operações conforme datasheet
for (size_t i = 0 ; i < Lenght ; i++) {
checksum_a = checksum_a + Payload[i];
checksum_b = checksum_b + checksum_a;
}
return 0;
}
Para verificar as mensagens que chegam do sistema, tem que se ter em mente que toda mensagem trocada possui um header. Será com base nesse header que se verifica se há mensagem nova chegando ou não. No caso de se enviar informações referentes à configuração, por exemplo, o dispositivo (receiver) sempre responde com uma informaçõ de ACK ou de NAK. Para configurar uma recepção de menagem de um ACK (1) ou de NAK (0), a gente fará o seguinte:
- Assim que entrar na função, a gente marca um t0. Servirá de base numérica para um timeout para a recepção;
- Procura guardar todos os dados em um buffer. A dimensão do buffer tem por base os tamanhos máximos das mensagens de ACK e/ou NAK;
- Caso a mensagem recebida seja menor do que esperado, a gente retorna falha;
- Verifica se os parâmetros iniciais são do header, caso contrário retorna falha;
- Verifica se o parâmetros class é referente ao ACK/NAK, caso contrátio retorna falha;
- Destrincha a mensagem e verifica se o "assunto" foi recebido é referente ao "assunto" que foi enviado; caso contrário retorna falha; e
- Faz o checksum e se tudo estiver dentro dos conformes, recebe-se a mensagem ACK (1).
bool UBX_request_acknowledge(uint8_t CLASS_expected,
uint8_t ID_expected,
uint32_t timeout_ms = 500){
// Para contador de timeout
const uint32_t start = millis();
// Recebe todos os dados e os guarda em um buffer.
uint8_t buffer[10];
size_t cont = 0;
while ( (millis() - start < timeout_ms) && (cont < sizeof(buffer)) ){
if(NEO_M8N.available()) {
buffer[cont++] = NEO_M8N.read();
}
}
// Situação de falha por dados incompletos.
if (cont < sizeof(buffer)) {
return false;
}
// Verifica o Header.
if ( !(buffer[0] == 0xB5 && buffer[1] == 0x62) ){
return false;
}
// CLASS e ID
uint8_t CLASS = buffer[2]; // deve ser 0x05 para ACK/NAK
uint8_t ID = buffer[3]; // 0x01 = ACK, 0x00 = NAK
if (CLASS != 0x05){
return false;
}
// Lenght
uint16_t Lenght = static_cast<uint16_t>(buffer[4]) | (static_cast<uint16_t>(buffer[5]) << 8);
if (Lenght != 2) {
return false;
}
// Payload
uint8_t Payload[2];
Payload[0] = buffer[6];
Payload[1] = buffer[7];
// Verifica se é resposta à mensagem esperada
if ( (Payload[0] != CLASS_expected) || (Payload[1] != ID_expected) ) {
return false;
}
// Checksum recebido
uint8_t checksum_a_received = buffer[8];
uint8_t checksum_b_received = buffer[9];
// Checksum calculado
uint8_t checksum_a_calc = 0;
uint8_t checksum_b_calc = 0;
UBX_checksum(CLASS, ID, Lenght, Payload, checksum_a_calc, checksum_b_calc);
// Checksum incorreto
if ( (checksum_a_received != checksum_a_calc) || (checksum_b_received != checksum_b_calc) ) {
return false;
}
// Se tudo não deu errado (NAK), retorna a mensagem recebida OK (ACK).
return (ID == 0x01);
}
Filosofia muito similar é empregada quando na leitura de mensagens que chegarão por polling, por exemplo. Ideia similar a esta utilizada pela mensagem de ACK/NAK será utilizada. O que diferirá será a verificação do header, sem a necessidade de a gente precisar criar um buffer. Isso decorre do fato de que as mensagens possuem tamanhos distintos. Receber a mensagem inteira, de tamanho desconhecido, para depois verificar se o que chegou é válido e depois fazer o processamento dela não é a coisa mais sensata, sobretudo se realizada diversas vezes por segundo. O que a gente irá fazer é receber os primeiros bytes do envio (header) e verifica se é ou não uma mensagem válida. Caso positivo, a gente simplesmente vai salvando toda a mensagem e com base nela a gente destrinchará o que vem dela. Sequencialmente, tem-se que:
// ===================================================
// (*) Lê um pacote UBX completo, ou seja:
//
// 1. sincroniza;
// 2. lê cabeçalho;
// 3. payload; e
// 4. valida checksum.
//
// (*) Retorna TRUE se o pacote foi lido com sucesso e
// checksum bateu.
//
// (*) Importante detacar que buffer 'Payload' deve ter
// espaço suficiente para 'Length' bytes.
// ===================================================
bool UBX_read_message(uint8_t &CLASS,
uint8_t &ID,
uint8_t *Payload,
uint16_t &Length,
uint8_t &checksum_a,
uint8_t &checksum_b,
uint32_t timeout_ms = 500) {
// Inicia o contador de timeout.
const uint32_t start = millis();
// ===================================
// 1. Sincroniza no header '0xB5 0x62'
// ===================================
bool sinc = false;
while (millis() - start < timeout_ms) {
// Se não chegar mensagem, volta ao while.
if (NEO_M8N.available() == 0){
continue;
}
// Chegou mensagem (byte), lê.
uint8_t b = NEO_M8N.read();
if ( !sinc ) {
if (b == 0xB5) {
sinc = true;
}
} else {
if (b == 0x62) {
break; // header completo, sai do 'while'.
}
// Falso início: se for outro 0xB5, permanece
// no estado; senão reseta.
sinc = (b == 0xB5);
}
}
// Timeout sem sincronizar
if (millis() - start >= timeout_ms) {
return false;
}
// ================================================
// 2. Lê class, id e Length (2 bytes little-endian)
// ================================================
while (NEO_M8N.available() < 4) {
if (millis() - start >= timeout_ms) {
return false;
}
}
CLASS = NEO_M8N.read();
ID = NEO_M8N.read();
uint8_t len_l = NEO_M8N.read();
uint8_t len_h = NEO_M8N.read();
Length = static_cast<uint16_t>(len_l) | (static_cast<uint16_t>(len_h) << 8);
// sanity check razoável (vai que dá uma merda)
if (Length > 1024) {
return false;
}
// =======================
// 3. Lê Payload
// =======================
uint16_t received = 0;
while (received < Length) {
if (NEO_M8N.available()) {
Payload[received++] = NEO_M8N.read();
}
if (millis() - start >= timeout_ms){
return false;
}
}
// ======================
// 4. Valida o checksum
// ======================
while (NEO_M8N.available() < 2) {
if (millis() - start >= timeout_ms) return false;
}
uint8_t checksum_a_rec = NEO_M8N.read();
uint8_t checksum_b_rec = NEO_M8N.read();
// Calcula checksum esperado
uint8_t calc_a = 0, calc_b = 0;
UBX_checksum(CLASS, ID, Length, (Length ? Payload : nullptr), calc_a, calc_b);
// expõe os valores calculados se o chamador quiser inspecionar
checksum_a = calc_a;
checksum_b = calc_b;
// Valida
if (checksum_a_rec != calc_a || checksum_b_rec != calc_b) {
return false;
}
return true;
}
Com base nessas informações e dados, a gente precisa agora entender como que configura o receptor para trabalhar com as mensagens UBX apenas. Isso ficará para um próximo post.
Nenhum comentário:
Postar um comentário