mirror of
https://github.com/adafruit/Adafruit_GPS.git
synced 2026-10-26 22:10:17 +00:00
Merge pull request #186 from ladyada-eagleclaw/main
Add exact decimals and NMEA command construction
This commit is contained in:
4 files changed
+376
-2
No files matched your search
@@ -3,6 +3,26 @@ name: Arduino Library CI
|
||||
on: [pull_request, push, repository_dispatch]
|
||||
|
||||
jobs:
|
||||
nmea-tests:
|
||||
name: NMEA regression tests
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Run host tests with sanitizers
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$RUNNER_TEMP/nmea-tests"
|
||||
for test in nmea_validate nmea_fields nmea_utilities; do
|
||||
echo "Running $test"
|
||||
g++ -std=c++11 -Wall -Wextra -Werror -g \
|
||||
-fsanitize=address,undefined -fno-sanitize-recover=all \
|
||||
-fno-omit-frame-pointer -Isrc src/Adafruit_NMEA.cpp \
|
||||
"extras/tests/$test/$test.cpp" -o "$RUNNER_TEMP/nmea-tests/$test"
|
||||
"$RUNNER_TEMP/nmea-tests/$test"
|
||||
done
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
/* Host regression for decimal parsing and command construction:
|
||||
g++ -std=c++11 -Wall -Wextra -Werror -fsanitize=address,undefined -Isrc \
|
||||
src/Adafruit_NMEA.cpp extras/tests/nmea_utilities/nmea_utilities.cpp \
|
||||
-o /tmp/nmea_utilities
|
||||
/tmp/nmea_utilities
|
||||
*/
|
||||
|
||||
#include "Adafruit_NMEA.h"
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static void expectNumber(nmea_span_t field, nmea_number_status_t status,
|
||||
int64_t coefficient = 0, uint8_t decimalPlaces = 0);
|
||||
static void expectCommand(const char *body, const char *expected);
|
||||
static void expectRejected(const char *body, size_t length);
|
||||
|
||||
int main() {
|
||||
struct {
|
||||
const char *text;
|
||||
int64_t coefficient;
|
||||
uint8_t decimalPlaces;
|
||||
} numbers[] = {{"0", 0, 0},
|
||||
{"-0.000", 0, 3},
|
||||
{"-123.4500", -1234500, 4},
|
||||
{"+12", 12, 0},
|
||||
{"-.5", -5, 1},
|
||||
{"1.", 1, 0},
|
||||
{"00012.0300", 120300, 4},
|
||||
{"4807.038123456", INT64_C(4807038123456), 9},
|
||||
{"9223372036854775807", INT64_MAX, 0},
|
||||
{"-9223372036854775808", INT64_MIN, 0},
|
||||
{"922337203685477580.7", INT64_MAX, 1},
|
||||
{"-922337203685477580.8", INT64_MIN, 1}};
|
||||
for (const auto &number : numbers)
|
||||
expectNumber({number.text, strlen(number.text)}, NMEA_NUMBER_VALID,
|
||||
number.coefficient, number.decimalPlaces);
|
||||
puts("PASS: exact decimals, retained zeros, and signed 64-bit limits");
|
||||
|
||||
const char *badNumbers[] = {
|
||||
".", "+", "-", "+.", "--1", "1.2.3", " 1",
|
||||
"1 ", "1e2", "NaN", "inf", "1,2", "1*00", "9223372036854775808x"};
|
||||
for (const char *text : badNumbers)
|
||||
expectNumber({text, strlen(text)}, NMEA_NUMBER_BAD_FORMAT);
|
||||
const char *overflow[] = {"9223372036854775808", "-9223372036854775809",
|
||||
"99999999999999999999999999"};
|
||||
for (const char *text : overflow)
|
||||
expectNumber({text, strlen(text)}, NMEA_NUMBER_OUT_OF_RANGE);
|
||||
expectNumber({NULL, 100}, NMEA_NUMBER_MISSING);
|
||||
expectNumber({"1", 0}, NMEA_NUMBER_EMPTY);
|
||||
const char embeddedNul[] = {'1', 0, '2'};
|
||||
expectNumber({embeddedNul, sizeof(embeddedNul)}, NMEA_NUMBER_BAD_FORMAT);
|
||||
const char highByte[] = {'1', (char)0x80};
|
||||
expectNumber({highByte, sizeof(highByte)}, NMEA_NUMBER_BAD_FORMAT);
|
||||
puts("PASS: missing, empty, malformed, and overflowing numbers are distinct");
|
||||
|
||||
const char bounded[] = {'-', '1', '.', '2'};
|
||||
expectNumber({bounded, sizeof(bounded)}, NMEA_NUMBER_VALID, -12, 1);
|
||||
expectNumber({"12x", 2}, NMEA_NUMBER_VALID, 12, 0);
|
||||
char fraction[258];
|
||||
memset(fraction, '0', sizeof(fraction));
|
||||
fraction[1] = '.';
|
||||
expectNumber({fraction, 257}, NMEA_NUMBER_VALID, 0, 255);
|
||||
expectNumber({fraction, sizeof(fraction)}, NMEA_NUMBER_OUT_OF_RANGE);
|
||||
char zeros[300];
|
||||
memset(zeros, '0', sizeof(zeros));
|
||||
expectNumber({zeros, sizeof(zeros)}, NMEA_NUMBER_VALID);
|
||||
puts("PASS: bounded numeric input and decimal-place limits");
|
||||
|
||||
expectCommand("PQTMVERNO", "$PQTMVERNO*58\r\n");
|
||||
expectCommand("PQTMVERNO,", "$PQTMVERNO,*74\r\n");
|
||||
expectCommand("PAIR002", "$PAIR002*38\r\n");
|
||||
expectCommand("PAIR001,004,0", "$PAIR001,004,0*3F\r\n");
|
||||
expectCommand("PMTK220,1000", "$PMTK220,1000*1F\r\n");
|
||||
puts("PASS: known command checksums and every output capacity boundary");
|
||||
|
||||
const char *badBodies[] = {"", ",", ",A", "A B", "$A", "!A",
|
||||
"A*41", "A\r", "A\n", "A,\t", "A,\x7f", "A,\x80"};
|
||||
for (const char *body : badBodies)
|
||||
expectRejected(body, strlen(body));
|
||||
expectRejected(NULL, 10);
|
||||
expectRejected("A", SIZE_MAX);
|
||||
expectRejected(embeddedNul, sizeof(embeddedNul));
|
||||
assert(Adafruit_NMEA::buildCommand(NULL, 100, "A", 1) == 0);
|
||||
puts("PASS: malformed commands and invalid arguments produce no partial "
|
||||
"output");
|
||||
|
||||
char overlap[40];
|
||||
memset(overlap, 'A', sizeof(overlap));
|
||||
assert(Adafruit_NMEA::buildCommand(overlap, 20, overlap + 4, 7) == 0);
|
||||
assert(overlap[0] == 0);
|
||||
for (size_t i = 1; i < sizeof(overlap); i++)
|
||||
assert(overlap[i] == 'A');
|
||||
memset(overlap, 'A', sizeof(overlap));
|
||||
assert(Adafruit_NMEA::buildCommand(overlap + 4, 20, overlap, 7) == 0);
|
||||
for (size_t i = 0; i < sizeof(overlap); i++) {
|
||||
if (i == 4)
|
||||
assert(overlap[i] == 0);
|
||||
else
|
||||
assert(overlap[i] == 'A');
|
||||
}
|
||||
memset(overlap, 'A', sizeof(overlap));
|
||||
assert(Adafruit_NMEA::buildCommand(overlap, 20, overlap, 7) == 0);
|
||||
puts("PASS: overlapping source and destination ranges are rejected");
|
||||
|
||||
char adjacent[13];
|
||||
memcpy(adjacent + 10, "A,1", 3);
|
||||
assert(Adafruit_NMEA::buildCommand(adjacent, 10, adjacent + 10, 3) == 9);
|
||||
assert(strcmp(adjacent, "$A,1*5C\r\n") == 0);
|
||||
assert(memcmp(adjacent + 10, "A,1", 3) == 0);
|
||||
memcpy(adjacent, "A,1", 3);
|
||||
assert(Adafruit_NMEA::buildCommand(adjacent + 3, 10, adjacent, 3) == 9);
|
||||
assert(strcmp(adjacent + 3, "$A,1*5C\r\n") == 0);
|
||||
assert(memcmp(adjacent, "A,1", 3) == 0);
|
||||
puts("PASS: adjacent buffers and nonterminated command bodies are supported");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void expectNumber(nmea_span_t field, nmea_number_status_t status,
|
||||
int64_t coefficient, uint8_t decimalPlaces) {
|
||||
nmea_decimal_t result = Adafruit_NMEA::parseDecimal(field);
|
||||
assert(result.status == status);
|
||||
assert(result.coefficient == coefficient);
|
||||
assert(result.decimalPlaces == decimalPlaces);
|
||||
}
|
||||
|
||||
static void expectCommand(const char *body, const char *expected) {
|
||||
size_t required = strlen(expected) + 1;
|
||||
for (size_t capacity = 0; capacity <= required + 1; capacity++) {
|
||||
size_t allocated = capacity;
|
||||
if (!allocated)
|
||||
allocated = 1;
|
||||
char *output = (char *)malloc(allocated);
|
||||
assert(output != NULL);
|
||||
memset(output, '?', allocated);
|
||||
size_t length =
|
||||
Adafruit_NMEA::buildCommand(output, capacity, body, strlen(body));
|
||||
if (capacity < required) {
|
||||
assert(length == 0);
|
||||
if (capacity)
|
||||
assert(output[0] == 0);
|
||||
else
|
||||
assert(output[0] == '?');
|
||||
for (size_t i = 1; i < allocated; i++)
|
||||
assert(output[i] == '?');
|
||||
} else {
|
||||
assert(length == required - 1);
|
||||
assert(strcmp(output, expected) == 0);
|
||||
assert(Adafruit_NMEA::validate(output, length).status ==
|
||||
NMEA_FRAME_VALID);
|
||||
if (capacity > required)
|
||||
assert(output[required] == '?');
|
||||
}
|
||||
free(output);
|
||||
}
|
||||
}
|
||||
|
||||
static void expectRejected(const char *body, size_t length) {
|
||||
char output[64];
|
||||
memset(output, '?', sizeof(output));
|
||||
assert(Adafruit_NMEA::buildCommand(output, sizeof(output), body, length) ==
|
||||
0);
|
||||
assert(output[0] == 0);
|
||||
for (size_t i = 1; i < sizeof(output); i++)
|
||||
assert(output[i] == '?');
|
||||
}
|
||||
+165
-1
@@ -1,13 +1,14 @@
|
||||
/**************************************************************************/
|
||||
/*!
|
||||
@file Adafruit_NMEA.cpp
|
||||
@brief Hardware-independent NMEA sentence validation.
|
||||
@brief Hardware-independent NMEA sentence and field utilities.
|
||||
|
||||
Written for Adafruit Industries. BSD license; see license.txt.
|
||||
*/
|
||||
/**************************************************************************/
|
||||
|
||||
#include "Adafruit_NMEA.h"
|
||||
#include <string.h>
|
||||
|
||||
/**************************************************************************/
|
||||
/*!
|
||||
@@ -151,3 +152,166 @@ nmea_span_t Adafruit_NMEA::nextField(nmea_span_t &remaining) {
|
||||
}
|
||||
return field;
|
||||
}
|
||||
|
||||
/**************************************************************************/
|
||||
/*!
|
||||
@brief Convert a complete field to an exact signed decimal value.
|
||||
@param field Borrowed readable text, with no NUL terminator required.
|
||||
@return Status, integer coefficient, and decimal-place count. Both numeric
|
||||
members are zero on failure. NULL data is MISSING; zero length is EMPTY.
|
||||
|
||||
Accepts an optional sign, digits, and at most one decimal point. At least
|
||||
one digit is required. Whitespace, exponents, and non-digit suffixes are
|
||||
rejected. The supplied fractional digit count is retained, including zeros.
|
||||
Coefficients must fit int64_t and decimal-place counts must fit uint8_t.
|
||||
Malformed syntax takes precedence over overflow. No rounding, floating-point
|
||||
conversion, allocation, or input modification occurs.
|
||||
*/
|
||||
/**************************************************************************/
|
||||
nmea_decimal_t Adafruit_NMEA::parseDecimal(nmea_span_t field) {
|
||||
nmea_decimal_t result = {NMEA_NUMBER_BAD_FORMAT, 0, 0};
|
||||
if (!field.data) {
|
||||
result.status = NMEA_NUMBER_MISSING;
|
||||
return result;
|
||||
}
|
||||
if (!field.length) {
|
||||
result.status = NMEA_NUMBER_EMPTY;
|
||||
return result;
|
||||
}
|
||||
|
||||
size_t start = 0;
|
||||
bool negative = false;
|
||||
if (field.data[0] == '-' || field.data[0] == '+') {
|
||||
negative = field.data[0] == '-';
|
||||
start = 1;
|
||||
}
|
||||
|
||||
// Accumulate negatively: INT64_MIN has no positive int64_t counterpart.
|
||||
int64_t limit = -INT64_MAX;
|
||||
if (negative)
|
||||
limit = INT64_MIN;
|
||||
int64_t cutoff = limit / 10;
|
||||
uint8_t lastDigitLimit = (uint8_t)(-(limit % 10));
|
||||
int64_t coefficient = 0;
|
||||
uint8_t decimalPlaces = 0;
|
||||
bool decimalPoint = false;
|
||||
bool hasDigit = false;
|
||||
bool overflow = false;
|
||||
for (size_t i = start; i < field.length; i++) {
|
||||
char c = field.data[i];
|
||||
if (c == '.' && !decimalPoint) {
|
||||
decimalPoint = true;
|
||||
continue;
|
||||
}
|
||||
if (c < '0' || c > '9')
|
||||
return result;
|
||||
hasDigit = true;
|
||||
uint8_t digit = c - '0';
|
||||
if (decimalPoint) {
|
||||
if (decimalPlaces == UINT8_MAX)
|
||||
overflow = true;
|
||||
else
|
||||
decimalPlaces++;
|
||||
}
|
||||
if (!overflow) {
|
||||
if (coefficient < cutoff ||
|
||||
(coefficient == cutoff && digit > lastDigitLimit))
|
||||
overflow = true;
|
||||
else
|
||||
coefficient = coefficient * 10 - digit;
|
||||
}
|
||||
}
|
||||
if (!hasDigit)
|
||||
return result;
|
||||
if (overflow) {
|
||||
result.status = NMEA_NUMBER_OUT_OF_RANGE;
|
||||
return result;
|
||||
}
|
||||
|
||||
result.status = NMEA_NUMBER_VALID;
|
||||
result.coefficient = coefficient;
|
||||
if (!negative)
|
||||
result.coefficient = -coefficient;
|
||||
result.decimalPlaces = decimalPlaces;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**************************************************************************/
|
||||
/*!
|
||||
@brief Build a checksummed command in caller-provided storage.
|
||||
@param output Writable buffer for the complete command and NUL terminator.
|
||||
@param capacity Size of output in bytes, including space for NUL.
|
||||
@param body Readable address and optional comma-separated fields, without
|
||||
'$', '!', '*', control characters, or non-ASCII bytes. No NUL is required.
|
||||
@param bodyLength Number of bytes in body, excluding any NUL terminator.
|
||||
@return Bytes written excluding NUL, or zero on invalid input, overlapping
|
||||
buffers, or insufficient capacity. On failure, output[0] is cleared if
|
||||
output is non-NULL and capacity is nonzero; no other output bytes are
|
||||
changed.
|
||||
|
||||
Produces "$<body>*HH\r\n" followed by NUL, using uppercase checksum digits.
|
||||
Requires bodyLength + NMEA_COMMAND_OVERHEAD bytes of storage. The address
|
||||
must be nonempty and ASCII alphanumeric. Zero-field commands are allowed.
|
||||
Arguments, capacity, and body syntax are checked before constructing output.
|
||||
No heap allocation or transport I/O occurs.
|
||||
*/
|
||||
/**************************************************************************/
|
||||
size_t Adafruit_NMEA::buildCommand(char *output, size_t capacity,
|
||||
const char *body, size_t bodyLength) {
|
||||
bool valid = output && body && bodyLength &&
|
||||
capacity >= NMEA_COMMAND_OVERHEAD &&
|
||||
bodyLength <= capacity - NMEA_COMMAND_OVERHEAD;
|
||||
if (valid) {
|
||||
// Compare address differences so checking overlap cannot overflow an end
|
||||
// address. Reject overlap with the caller's entire writable output span.
|
||||
uintptr_t outputAddress = (uintptr_t)output;
|
||||
uintptr_t bodyAddress = (uintptr_t)body;
|
||||
if (outputAddress <= bodyAddress)
|
||||
valid = bodyAddress - outputAddress >= capacity;
|
||||
else
|
||||
valid = outputAddress - bodyAddress >= bodyLength;
|
||||
}
|
||||
|
||||
uint8_t checksum = 0;
|
||||
bool inAddress = true;
|
||||
if (valid) {
|
||||
for (size_t i = 0; i < bodyLength; i++) {
|
||||
uint8_t c = (uint8_t)body[i];
|
||||
if (c < ' ' || c > '~' || c == '$' || c == '!' || c == '*') {
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
if (inAddress) {
|
||||
if (c == ',' && i > 0) {
|
||||
inAddress = false;
|
||||
} else if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
|
||||
(c >= '0' && c <= '9'))) {
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
checksum ^= c;
|
||||
}
|
||||
}
|
||||
if (!valid) {
|
||||
if (output && capacity)
|
||||
output[0] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
output[0] = '$';
|
||||
memcpy(output + 1, body, bodyLength);
|
||||
size_t end = bodyLength + 1;
|
||||
output[end++] = '*';
|
||||
uint8_t digits[] = {(uint8_t)(checksum / 16), (uint8_t)(checksum % 16)};
|
||||
for (uint8_t i = 0; i < 2; i++) {
|
||||
if (digits[i] < 10)
|
||||
output[end++] = '0' + digits[i];
|
||||
else
|
||||
output[end++] = 'A' + digits[i] - 10;
|
||||
}
|
||||
output[end++] = '\r';
|
||||
output[end++] = '\n';
|
||||
output[end] = '\0';
|
||||
return end;
|
||||
}
|
||||
+24
-1
@@ -1,7 +1,7 @@
|
||||
/**************************************************************************/
|
||||
/*!
|
||||
@file Adafruit_NMEA.h
|
||||
@brief Hardware-independent NMEA sentence validation.
|
||||
@brief Hardware-independent NMEA sentence and field utilities.
|
||||
|
||||
Written for Adafruit Industries. BSD license; see license.txt.
|
||||
*/
|
||||
@@ -13,6 +13,8 @@
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define NMEA_COMMAND_OVERHEAD 7 ///< Bytes for '$', '*HH', CR/LF, and NUL.
|
||||
|
||||
/** Result of framing or validating a sentence. Recognition belongs to GNSS
|
||||
* or receiver-specific decoders: an unknown address can still be VALID. */
|
||||
typedef enum : uint8_t {
|
||||
@@ -43,11 +45,32 @@ typedef struct {
|
||||
nmea_span_t fields; ///< Text after the first comma and before '*'.
|
||||
} nmea_sentence_t;
|
||||
|
||||
/// Result of converting one complete field to a decimal number.
|
||||
typedef enum : uint8_t {
|
||||
NMEA_NUMBER_VALID, ///< The complete field is a representable decimal.
|
||||
NMEA_NUMBER_MISSING, ///< Field is absent.
|
||||
NMEA_NUMBER_EMPTY, ///< Field is present but empty.
|
||||
NMEA_NUMBER_BAD_FORMAT, ///< Invalid character or decimal syntax.
|
||||
NMEA_NUMBER_OUT_OF_RANGE ///< Coefficient or decimal-place count overflowed.
|
||||
} nmea_number_status_t;
|
||||
|
||||
/** Exact decimal value: coefficient / 10^decimalPlaces, with no float step.
|
||||
* Coordinate interpretation and conversion to degrees belong to Adafruit_GNSS.
|
||||
* On any conversion failure, coefficient and decimalPlaces are both zero. */
|
||||
typedef struct {
|
||||
nmea_number_status_t status; ///< Result of conversion.
|
||||
int64_t coefficient; ///< Signed integer containing the decimal digits.
|
||||
uint8_t decimalPlaces; ///< Number of digits following the decimal point.
|
||||
} nmea_decimal_t;
|
||||
|
||||
/// Hardware-independent NMEA utilities shared by receiver classes.
|
||||
class Adafruit_NMEA {
|
||||
public:
|
||||
static nmea_sentence_t validate(const char *data, size_t length);
|
||||
static nmea_span_t nextField(nmea_span_t &remaining);
|
||||
static nmea_decimal_t parseDecimal(nmea_span_t field);
|
||||
static size_t buildCommand(char *output, size_t capacity, const char *body,
|
||||
size_t bodyLength);
|
||||
};
|
||||
|
||||
#endif // ADAFRUIT_NMEA_H
|
||||
Reference in new issue
Block a user