feat(gps): add UM982 GPS driver replacing broken TinyGPS++
Implement a complete UM982 GNSS driver (um982_gps.h/.c) with: - NMEA parser for GGA, RMC, THS, VTG with multi-talker support (GP/GN/GL/GA/GB) - Correct coordinate parsing using decimal-point-based degree detection (fixes PR #68 bug: 3-digit longitude degrees) - Checksum verification on all incoming sentences - Non-blocking line assembler with ring buffer - Init sequence: UNLOG, HEADING FIXLENGTH, baseline config, NMEA enables, VERSIONA handshake (no SAVECONFIG to avoid NVM wear) - Validity/age checks with configurable timeouts Integration into main.cpp: - Replace TinyGPSPlus with UM982_GPS_t, UART5 baud 9600->115200 - Non-blocking um982_process() in main loop (single-byte UART reads) - GPS heading override with magnetometer fallback - Health check using um982_position_age() Test infrastructure: - 49 unit tests covering checksums, coordinate parsing, all sentence types, talker IDs, feed/assembly, validity, init sequence, edge cases - Mock HAL_UART_Receive with per-UART ring buffer for integration tests - All 72 MCU tests passing (23 existing + 49 new) Fixes all 12 bugs identified in PR #68 analysis (5 compile errors + 7 functional).
This commit is contained in:
@@ -27,6 +27,10 @@ CXX_LIB_DIR := ../9_1_1_C_Cpp_Libraries
|
||||
CXX_SRCS := $(CXX_LIB_DIR)/ADAR1000_AGC.cpp $(CXX_LIB_DIR)/ADAR1000_Manager.cpp
|
||||
CXX_OBJS := ADAR1000_AGC.o ADAR1000_Manager.o
|
||||
|
||||
# GPS driver source
|
||||
GPS_SRC := ../9_1_3_C_Cpp_Code/um982_gps.c
|
||||
GPS_OBJ := um982_gps.o
|
||||
|
||||
# Real source files compiled against mock headers
|
||||
REAL_SRC := ../9_1_1_C_Cpp_Libraries/adf4382a_manager.c
|
||||
|
||||
@@ -73,7 +77,10 @@ TESTS_WITH_PLATFORM := test_bug11_platform_spi_transmit_only
|
||||
# C++ tests (AGC outer loop)
|
||||
TESTS_WITH_CXX := test_agc_outer_loop
|
||||
|
||||
ALL_TESTS := $(TESTS_WITH_REAL) $(TESTS_MOCK_ONLY) $(TESTS_STANDALONE) $(TESTS_WITH_PLATFORM) $(TESTS_WITH_CXX)
|
||||
# GPS driver tests (need mocks + GPS source + -lm)
|
||||
TESTS_GPS := test_um982_gps
|
||||
|
||||
ALL_TESTS := $(TESTS_WITH_REAL) $(TESTS_MOCK_ONLY) $(TESTS_STANDALONE) $(TESTS_WITH_PLATFORM) $(TESTS_WITH_CXX) $(TESTS_GPS)
|
||||
|
||||
.PHONY: all build test clean \
|
||||
$(addprefix test_,bug1 bug2 bug3 bug4 bug5 bug6 bug7 bug8 bug9 bug10 bug11 bug12 bug13 bug14 bug15) \
|
||||
@@ -189,6 +196,20 @@ test_agc_outer_loop: test_agc_outer_loop.cpp $(CXX_OBJS) $(MOCK_OBJS)
|
||||
test_agc: test_agc_outer_loop
|
||||
./test_agc_outer_loop
|
||||
|
||||
# --- GPS driver rules ---
|
||||
|
||||
$(GPS_OBJ): $(GPS_SRC)
|
||||
$(CC) $(CFLAGS) $(INCLUDES) -I../9_1_3_C_Cpp_Code -c $< -o $@
|
||||
|
||||
# Note: test includes um982_gps.c directly for white-box testing (static fn access)
|
||||
test_um982_gps: test_um982_gps.c $(MOCK_OBJS)
|
||||
$(CC) $(CFLAGS) $(INCLUDES) -I../9_1_3_C_Cpp_Code $< $(MOCK_OBJS) -lm -o $@
|
||||
|
||||
# Convenience target
|
||||
.PHONY: test_gps
|
||||
test_gps: test_um982_gps
|
||||
./test_um982_gps
|
||||
|
||||
# --- Individual test targets ---
|
||||
|
||||
test_bug1: test_bug1_timed_sync_init_ordering
|
||||
|
||||
@@ -21,6 +21,7 @@ SPI_HandleTypeDef hspi4 = { .id = 4 };
|
||||
I2C_HandleTypeDef hi2c1 = { .id = 1 };
|
||||
I2C_HandleTypeDef hi2c2 = { .id = 2 };
|
||||
UART_HandleTypeDef huart3 = { .id = 3 };
|
||||
UART_HandleTypeDef huart5 = { .id = 5 }; /* GPS UART */
|
||||
ADC_HandleTypeDef hadc3 = { .id = 3 };
|
||||
TIM_HandleTypeDef htim3 = { .id = 3 };
|
||||
|
||||
@@ -34,6 +35,26 @@ uint32_t mock_tick = 0;
|
||||
/* ========================= Printf control ========================= */
|
||||
int mock_printf_enabled = 0;
|
||||
|
||||
/* ========================= Mock UART TX capture =================== */
|
||||
uint8_t mock_uart_tx_buf[MOCK_UART_TX_BUF_SIZE];
|
||||
uint16_t mock_uart_tx_len = 0;
|
||||
|
||||
/* ========================= Mock UART RX buffer ==================== */
|
||||
#define MOCK_UART_RX_SLOTS 8
|
||||
|
||||
static struct {
|
||||
uint32_t uart_id;
|
||||
uint8_t buf[MOCK_UART_RX_BUF_SIZE];
|
||||
uint16_t head;
|
||||
uint16_t tail;
|
||||
} mock_uart_rx[MOCK_UART_RX_SLOTS];
|
||||
|
||||
void mock_uart_tx_clear(void)
|
||||
{
|
||||
mock_uart_tx_len = 0;
|
||||
memset(mock_uart_tx_buf, 0, sizeof(mock_uart_tx_buf));
|
||||
}
|
||||
|
||||
/* ========================= Mock GPIO read ========================= */
|
||||
#define GPIO_READ_TABLE_SIZE 32
|
||||
static struct {
|
||||
@@ -49,6 +70,9 @@ void spy_reset(void)
|
||||
mock_tick = 0;
|
||||
mock_printf_enabled = 0;
|
||||
memset(gpio_read_table, 0, sizeof(gpio_read_table));
|
||||
memset(mock_uart_rx, 0, sizeof(mock_uart_rx));
|
||||
mock_uart_tx_len = 0;
|
||||
memset(mock_uart_tx_buf, 0, sizeof(mock_uart_tx_buf));
|
||||
}
|
||||
|
||||
const SpyRecord *spy_get(int index)
|
||||
@@ -185,6 +209,83 @@ HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pD
|
||||
.value = Timeout,
|
||||
.extra = huart
|
||||
});
|
||||
/* Capture TX data for test inspection */
|
||||
for (uint16_t i = 0; i < Size && mock_uart_tx_len < MOCK_UART_TX_BUF_SIZE; i++) {
|
||||
mock_uart_tx_buf[mock_uart_tx_len++] = pData[i];
|
||||
}
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
/* ========================= Mock UART RX helpers ====================== */
|
||||
|
||||
/* find_rx_slot, mock_uart_rx_load, etc. use the mock_uart_rx declared above */
|
||||
|
||||
static int find_rx_slot(UART_HandleTypeDef *huart)
|
||||
{
|
||||
if (huart == NULL) return -1;
|
||||
/* Find existing slot */
|
||||
for (int i = 0; i < MOCK_UART_RX_SLOTS; i++) {
|
||||
if (mock_uart_rx[i].uart_id == huart->id && mock_uart_rx[i].head != mock_uart_rx[i].tail) {
|
||||
return i;
|
||||
}
|
||||
if (mock_uart_rx[i].uart_id == huart->id) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
/* Find empty slot */
|
||||
for (int i = 0; i < MOCK_UART_RX_SLOTS; i++) {
|
||||
if (mock_uart_rx[i].uart_id == 0) {
|
||||
mock_uart_rx[i].uart_id = huart->id;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void mock_uart_rx_load(UART_HandleTypeDef *huart, const uint8_t *data, uint16_t len)
|
||||
{
|
||||
int slot = find_rx_slot(huart);
|
||||
if (slot < 0) return;
|
||||
mock_uart_rx[slot].uart_id = huart->id;
|
||||
for (uint16_t i = 0; i < len; i++) {
|
||||
uint16_t next = (mock_uart_rx[slot].head + 1) % MOCK_UART_RX_BUF_SIZE;
|
||||
if (next == mock_uart_rx[slot].tail) break; /* Buffer full */
|
||||
mock_uart_rx[slot].buf[mock_uart_rx[slot].head] = data[i];
|
||||
mock_uart_rx[slot].head = next;
|
||||
}
|
||||
}
|
||||
|
||||
void mock_uart_rx_clear(UART_HandleTypeDef *huart)
|
||||
{
|
||||
int slot = find_rx_slot(huart);
|
||||
if (slot < 0) return;
|
||||
mock_uart_rx[slot].head = 0;
|
||||
mock_uart_rx[slot].tail = 0;
|
||||
}
|
||||
|
||||
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData,
|
||||
uint16_t Size, uint32_t Timeout)
|
||||
{
|
||||
(void)Timeout;
|
||||
int slot = find_rx_slot(huart);
|
||||
if (slot < 0) return HAL_TIMEOUT;
|
||||
|
||||
for (uint16_t i = 0; i < Size; i++) {
|
||||
if (mock_uart_rx[slot].head == mock_uart_rx[slot].tail) {
|
||||
return HAL_TIMEOUT; /* No more data */
|
||||
}
|
||||
pData[i] = mock_uart_rx[slot].buf[mock_uart_rx[slot].tail];
|
||||
mock_uart_rx[slot].tail = (mock_uart_rx[slot].tail + 1) % MOCK_UART_RX_BUF_SIZE;
|
||||
}
|
||||
|
||||
spy_push((SpyRecord){
|
||||
.type = SPY_UART_RX,
|
||||
.port = NULL,
|
||||
.pin = Size,
|
||||
.value = Timeout,
|
||||
.extra = huart
|
||||
});
|
||||
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
|
||||
@@ -105,6 +105,7 @@ typedef struct {
|
||||
extern SPI_HandleTypeDef hspi1, hspi4;
|
||||
extern I2C_HandleTypeDef hi2c1, hi2c2;
|
||||
extern UART_HandleTypeDef huart3;
|
||||
extern UART_HandleTypeDef huart5; /* GPS UART */
|
||||
extern ADC_HandleTypeDef hadc3;
|
||||
extern TIM_HandleTypeDef htim3; /* Timer for DELADJ PWM */
|
||||
|
||||
@@ -139,6 +140,7 @@ typedef enum {
|
||||
SPY_TIM_SET_COMPARE,
|
||||
SPY_SPI_TRANSMIT_RECEIVE,
|
||||
SPY_SPI_TRANSMIT,
|
||||
SPY_UART_RX,
|
||||
} SpyCallType;
|
||||
|
||||
typedef struct {
|
||||
@@ -187,6 +189,23 @@ void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);
|
||||
uint32_t HAL_GetTick(void);
|
||||
void HAL_Delay(uint32_t Delay);
|
||||
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size, uint32_t Timeout);
|
||||
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
|
||||
|
||||
/* ========================= Mock UART RX buffer ======================= */
|
||||
|
||||
/* Inject bytes into the mock UART RX buffer for a specific UART handle.
|
||||
* HAL_UART_Receive will return these bytes one at a time. */
|
||||
#define MOCK_UART_RX_BUF_SIZE 2048
|
||||
|
||||
void mock_uart_rx_load(UART_HandleTypeDef *huart, const uint8_t *data, uint16_t len);
|
||||
void mock_uart_rx_clear(UART_HandleTypeDef *huart);
|
||||
|
||||
/* Capture buffer for UART TX data (to verify commands sent to GPS module) */
|
||||
#define MOCK_UART_TX_BUF_SIZE 2048
|
||||
|
||||
extern uint8_t mock_uart_tx_buf[MOCK_UART_TX_BUF_SIZE];
|
||||
extern uint16_t mock_uart_tx_len;
|
||||
void mock_uart_tx_clear(void);
|
||||
|
||||
/* ========================= SPI stubs ============================== */
|
||||
|
||||
|
||||
BIN
9_Firmware/9_1_Microcontroller/tests/test_agc_outer_loop
Executable file
BIN
9_Firmware/9_1_Microcontroller/tests/test_agc_outer_loop
Executable file
Binary file not shown.
BIN
9_Firmware/9_1_Microcontroller/tests/test_gap3_emergency_state_ordering
Executable file
BIN
9_Firmware/9_1_Microcontroller/tests/test_gap3_emergency_state_ordering
Executable file
Binary file not shown.
BIN
9_Firmware/9_1_Microcontroller/tests/test_gap3_emergency_stop_rails
Executable file
BIN
9_Firmware/9_1_Microcontroller/tests/test_gap3_emergency_stop_rails
Executable file
Binary file not shown.
BIN
9_Firmware/9_1_Microcontroller/tests/test_gap3_idq_periodic_reread
Executable file
BIN
9_Firmware/9_1_Microcontroller/tests/test_gap3_idq_periodic_reread
Executable file
Binary file not shown.
BIN
9_Firmware/9_1_Microcontroller/tests/test_gap3_iwdg_config
Executable file
BIN
9_Firmware/9_1_Microcontroller/tests/test_gap3_iwdg_config
Executable file
Binary file not shown.
BIN
9_Firmware/9_1_Microcontroller/tests/test_gap3_overtemp_emergency_stop
Executable file
BIN
9_Firmware/9_1_Microcontroller/tests/test_gap3_overtemp_emergency_stop
Executable file
Binary file not shown.
BIN
9_Firmware/9_1_Microcontroller/tests/test_gap3_temperature_max
Executable file
BIN
9_Firmware/9_1_Microcontroller/tests/test_gap3_temperature_max
Executable file
Binary file not shown.
BIN
9_Firmware/9_1_Microcontroller/tests/test_um982_gps
Executable file
BIN
9_Firmware/9_1_Microcontroller/tests/test_um982_gps
Executable file
Binary file not shown.
853
9_Firmware/9_1_Microcontroller/tests/test_um982_gps.c
Normal file
853
9_Firmware/9_1_Microcontroller/tests/test_um982_gps.c
Normal file
@@ -0,0 +1,853 @@
|
||||
/*******************************************************************************
|
||||
* test_um982_gps.c -- Unit tests for UM982 GPS driver
|
||||
*
|
||||
* Tests NMEA parsing, checksum validation, coordinate parsing, init sequence,
|
||||
* and validity tracking. Uses the mock HAL infrastructure for UART.
|
||||
*
|
||||
* Build: see Makefile target test_um982_gps
|
||||
* Run: ./test_um982_gps
|
||||
******************************************************************************/
|
||||
#include "stm32_hal_mock.h"
|
||||
#include "../9_1_3_C_Cpp_Code/um982_gps.h"
|
||||
#include "../9_1_3_C_Cpp_Code/um982_gps.c" /* Include .c directly for white-box testing */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#include <math.h>
|
||||
|
||||
/* ========================= Test helpers ============================== */
|
||||
|
||||
static int tests_passed = 0;
|
||||
static int tests_failed = 0;
|
||||
|
||||
#define TEST(name) \
|
||||
do { printf(" [TEST] %-55s ", name); } while(0)
|
||||
|
||||
#define PASS() \
|
||||
do { printf("PASS\n"); tests_passed++; } while(0)
|
||||
|
||||
#define FAIL(msg) \
|
||||
do { printf("FAIL: %s\n", msg); tests_failed++; } while(0)
|
||||
|
||||
#define ASSERT_TRUE(expr, msg) \
|
||||
do { if (!(expr)) { FAIL(msg); return; } } while(0)
|
||||
|
||||
#define ASSERT_FALSE(expr, msg) \
|
||||
do { if (expr) { FAIL(msg); return; } } while(0)
|
||||
|
||||
#define ASSERT_EQ_INT(a, b, msg) \
|
||||
do { if ((a) != (b)) { \
|
||||
char _buf[256]; \
|
||||
snprintf(_buf, sizeof(_buf), "%s (got %d, expected %d)", msg, (int)(a), (int)(b)); \
|
||||
FAIL(_buf); return; \
|
||||
} } while(0)
|
||||
|
||||
#define ASSERT_NEAR(a, b, tol, msg) \
|
||||
do { if (fabs((double)(a) - (double)(b)) > (tol)) { \
|
||||
char _buf[256]; \
|
||||
snprintf(_buf, sizeof(_buf), "%s (got %.8f, expected %.8f)", msg, (double)(a), (double)(b)); \
|
||||
FAIL(_buf); return; \
|
||||
} } while(0)
|
||||
|
||||
#define ASSERT_NAN(val, msg) \
|
||||
do { if (!isnan(val)) { FAIL(msg); return; } } while(0)
|
||||
|
||||
static UM982_GPS_t gps;
|
||||
|
||||
static void reset_gps(void)
|
||||
{
|
||||
spy_reset();
|
||||
memset(&gps, 0, sizeof(gps));
|
||||
gps.huart = &huart5;
|
||||
gps.heading = NAN;
|
||||
gps.heading_mode = 'V';
|
||||
gps.rmc_status = 'V';
|
||||
}
|
||||
|
||||
/* ========================= Checksum tests ============================ */
|
||||
|
||||
static void test_checksum_valid(void)
|
||||
{
|
||||
TEST("checksum: valid GGA");
|
||||
ASSERT_TRUE(um982_verify_checksum(
|
||||
"$GNGGA,001043.00,4404.14036,N,12118.85961,W,1,12,0.98,1113.0,M,-21.3,M*47"),
|
||||
"should be valid");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_checksum_valid_ths(void)
|
||||
{
|
||||
TEST("checksum: valid THS");
|
||||
ASSERT_TRUE(um982_verify_checksum("$GNTHS,341.3344,A*1F"),
|
||||
"should be valid");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_checksum_invalid(void)
|
||||
{
|
||||
TEST("checksum: invalid (wrong value)");
|
||||
ASSERT_FALSE(um982_verify_checksum("$GNTHS,341.3344,A*FF"),
|
||||
"should be invalid");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_checksum_missing_star(void)
|
||||
{
|
||||
TEST("checksum: missing * marker");
|
||||
ASSERT_FALSE(um982_verify_checksum("$GNTHS,341.3344,A"),
|
||||
"should be invalid");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_checksum_null(void)
|
||||
{
|
||||
TEST("checksum: NULL input");
|
||||
ASSERT_FALSE(um982_verify_checksum(NULL), "should be false");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_checksum_no_dollar(void)
|
||||
{
|
||||
TEST("checksum: missing $ prefix");
|
||||
ASSERT_FALSE(um982_verify_checksum("GNTHS,341.3344,A*1F"),
|
||||
"should be invalid without $");
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ========================= Coordinate parsing tests ================== */
|
||||
|
||||
static void test_coord_latitude_north(void)
|
||||
{
|
||||
TEST("coord: latitude 4404.14036 N");
|
||||
double lat = um982_parse_coord("4404.14036", 'N');
|
||||
/* 44 + 04.14036/60 = 44.069006 */
|
||||
ASSERT_NEAR(lat, 44.069006, 0.000001, "latitude");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_coord_latitude_south(void)
|
||||
{
|
||||
TEST("coord: latitude 3358.92500 S (negative)");
|
||||
double lat = um982_parse_coord("3358.92500", 'S');
|
||||
ASSERT_TRUE(lat < 0.0, "should be negative for S");
|
||||
ASSERT_NEAR(lat, -(33.0 + 58.925/60.0), 0.000001, "latitude");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_coord_longitude_3digit(void)
|
||||
{
|
||||
TEST("coord: longitude 12118.85961 W (3-digit degrees)");
|
||||
double lon = um982_parse_coord("12118.85961", 'W');
|
||||
/* 121 + 18.85961/60 = 121.314327 */
|
||||
ASSERT_TRUE(lon < 0.0, "should be negative for W");
|
||||
ASSERT_NEAR(lon, -(121.0 + 18.85961/60.0), 0.000001, "longitude");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_coord_longitude_east(void)
|
||||
{
|
||||
TEST("coord: longitude 11614.19729 E");
|
||||
double lon = um982_parse_coord("11614.19729", 'E');
|
||||
ASSERT_TRUE(lon > 0.0, "should be positive for E");
|
||||
ASSERT_NEAR(lon, 116.0 + 14.19729/60.0, 0.000001, "longitude");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_coord_empty(void)
|
||||
{
|
||||
TEST("coord: empty string returns NAN");
|
||||
ASSERT_NAN(um982_parse_coord("", 'N'), "should be NAN");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_coord_null(void)
|
||||
{
|
||||
TEST("coord: NULL returns NAN");
|
||||
ASSERT_NAN(um982_parse_coord(NULL, 'N'), "should be NAN");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_coord_no_dot(void)
|
||||
{
|
||||
TEST("coord: no decimal point returns NAN");
|
||||
ASSERT_NAN(um982_parse_coord("440414036", 'N'), "should be NAN");
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ========================= GGA parsing tests ========================= */
|
||||
|
||||
static void test_parse_gga_full(void)
|
||||
{
|
||||
TEST("GGA: full sentence with all fields");
|
||||
reset_gps();
|
||||
mock_set_tick(1000);
|
||||
|
||||
um982_parse_sentence(&gps,
|
||||
"$GNGGA,001043.00,4404.14036,N,12118.85961,W,1,12,0.98,1113.0,M,-21.3,M*47");
|
||||
|
||||
ASSERT_NEAR(gps.latitude, 44.069006, 0.0001, "latitude");
|
||||
ASSERT_NEAR(gps.longitude, -(121.0 + 18.85961/60.0), 0.0001, "longitude");
|
||||
ASSERT_EQ_INT(gps.fix_quality, 1, "fix quality");
|
||||
ASSERT_EQ_INT(gps.num_satellites, 12, "num sats");
|
||||
ASSERT_NEAR(gps.hdop, 0.98, 0.01, "hdop");
|
||||
ASSERT_NEAR(gps.altitude, 1113.0, 0.1, "altitude");
|
||||
ASSERT_NEAR(gps.geoid_sep, -21.3, 0.1, "geoid sep");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_parse_gga_rtk_fixed(void)
|
||||
{
|
||||
TEST("GGA: RTK fixed (quality=4)");
|
||||
reset_gps();
|
||||
|
||||
um982_parse_sentence(&gps,
|
||||
"$GNGGA,023634.00,4004.73871635,N,11614.19729418,E,4,28,0.7,61.0988,M,-8.4923,M,,*5D");
|
||||
|
||||
ASSERT_EQ_INT(gps.fix_quality, 4, "RTK fixed");
|
||||
ASSERT_EQ_INT(gps.num_satellites, 28, "num sats");
|
||||
ASSERT_NEAR(gps.latitude, 40.0 + 4.73871635/60.0, 0.0000001, "latitude");
|
||||
ASSERT_NEAR(gps.longitude, 116.0 + 14.19729418/60.0, 0.0000001, "longitude");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_parse_gga_no_fix(void)
|
||||
{
|
||||
TEST("GGA: no fix (quality=0)");
|
||||
reset_gps();
|
||||
|
||||
/* Compute checksum for this sentence */
|
||||
um982_parse_sentence(&gps,
|
||||
"$GNGGA,235959.00,,,,,0,00,99.99,,,,,,*79");
|
||||
|
||||
ASSERT_EQ_INT(gps.fix_quality, 0, "no fix");
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ========================= RMC parsing tests ========================= */
|
||||
|
||||
static void test_parse_rmc_valid(void)
|
||||
{
|
||||
TEST("RMC: valid position and speed");
|
||||
reset_gps();
|
||||
mock_set_tick(2000);
|
||||
|
||||
um982_parse_sentence(&gps,
|
||||
"$GNRMC,001031.00,A,4404.13993,N,12118.86023,W,0.146,,100117,,,A*7B");
|
||||
|
||||
ASSERT_EQ_INT(gps.rmc_status, 'A', "status");
|
||||
ASSERT_NEAR(gps.latitude, 44.0 + 4.13993/60.0, 0.0001, "latitude");
|
||||
ASSERT_NEAR(gps.longitude, -(121.0 + 18.86023/60.0), 0.0001, "longitude");
|
||||
ASSERT_NEAR(gps.speed_knots, 0.146, 0.001, "speed");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_parse_rmc_void(void)
|
||||
{
|
||||
TEST("RMC: void status (no valid fix)");
|
||||
reset_gps();
|
||||
gps.latitude = 12.34; /* Pre-set to check it doesn't get overwritten */
|
||||
|
||||
um982_parse_sentence(&gps,
|
||||
"$GNRMC,235959.00,V,,,,,,,100117,,,N*64");
|
||||
|
||||
ASSERT_EQ_INT(gps.rmc_status, 'V', "void status");
|
||||
ASSERT_NEAR(gps.latitude, 12.34, 0.001, "lat should not change on void");
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ========================= THS parsing tests ========================= */
|
||||
|
||||
static void test_parse_ths_autonomous(void)
|
||||
{
|
||||
TEST("THS: autonomous heading 341.3344");
|
||||
reset_gps();
|
||||
mock_set_tick(3000);
|
||||
|
||||
um982_parse_sentence(&gps, "$GNTHS,341.3344,A*1F");
|
||||
|
||||
ASSERT_NEAR(gps.heading, 341.3344, 0.001, "heading");
|
||||
ASSERT_EQ_INT(gps.heading_mode, 'A', "mode");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_parse_ths_not_valid(void)
|
||||
{
|
||||
TEST("THS: not valid mode");
|
||||
reset_gps();
|
||||
|
||||
um982_parse_sentence(&gps, "$GNTHS,,V*10");
|
||||
|
||||
ASSERT_NAN(gps.heading, "heading should be NAN when empty");
|
||||
ASSERT_EQ_INT(gps.heading_mode, 'V', "mode V");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_parse_ths_zero(void)
|
||||
{
|
||||
TEST("THS: heading exactly 0.0000");
|
||||
reset_gps();
|
||||
|
||||
um982_parse_sentence(&gps, "$GNTHS,0.0000,A*19");
|
||||
|
||||
ASSERT_NEAR(gps.heading, 0.0, 0.001, "heading zero");
|
||||
ASSERT_EQ_INT(gps.heading_mode, 'A', "mode A");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_parse_ths_360_boundary(void)
|
||||
{
|
||||
TEST("THS: heading near 360");
|
||||
reset_gps();
|
||||
|
||||
um982_parse_sentence(&gps, "$GNTHS,359.9999,D*13");
|
||||
|
||||
ASSERT_NEAR(gps.heading, 359.9999, 0.001, "heading near 360");
|
||||
ASSERT_EQ_INT(gps.heading_mode, 'D', "mode D");
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ========================= VTG parsing tests ========================= */
|
||||
|
||||
static void test_parse_vtg(void)
|
||||
{
|
||||
TEST("VTG: course and speed");
|
||||
reset_gps();
|
||||
|
||||
um982_parse_sentence(&gps,
|
||||
"$GPVTG,220.86,T,,M,2.550,N,4.724,K,A*34");
|
||||
|
||||
ASSERT_NEAR(gps.course_true, 220.86, 0.01, "course");
|
||||
ASSERT_NEAR(gps.speed_knots, 2.550, 0.001, "speed knots");
|
||||
ASSERT_NEAR(gps.speed_kmh, 4.724, 0.001, "speed kmh");
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ========================= Talker ID tests =========================== */
|
||||
|
||||
static void test_talker_gp(void)
|
||||
{
|
||||
TEST("talker: GP prefix parses correctly");
|
||||
reset_gps();
|
||||
|
||||
um982_parse_sentence(&gps, "$GPTHS,123.4567,A*07");
|
||||
|
||||
ASSERT_NEAR(gps.heading, 123.4567, 0.001, "heading with GP");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_talker_gl(void)
|
||||
{
|
||||
TEST("talker: GL prefix parses correctly");
|
||||
reset_gps();
|
||||
|
||||
um982_parse_sentence(&gps, "$GLTHS,123.4567,A*1B");
|
||||
|
||||
ASSERT_NEAR(gps.heading, 123.4567, 0.001, "heading with GL");
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ========================= Feed / line assembly tests ================ */
|
||||
|
||||
static void test_feed_single_sentence(void)
|
||||
{
|
||||
TEST("feed: single complete sentence with CRLF");
|
||||
reset_gps();
|
||||
mock_set_tick(5000);
|
||||
|
||||
const char *data = "$GNTHS,341.3344,A*1F\r\n";
|
||||
um982_feed(&gps, (const uint8_t *)data, (uint16_t)strlen(data));
|
||||
|
||||
ASSERT_NEAR(gps.heading, 341.3344, 0.001, "heading");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_feed_multiple_sentences(void)
|
||||
{
|
||||
TEST("feed: multiple sentences in one chunk");
|
||||
reset_gps();
|
||||
mock_set_tick(5000);
|
||||
|
||||
const char *data =
|
||||
"$GNTHS,100.0000,A*18\r\n"
|
||||
"$GNGGA,001043.00,4404.14036,N,12118.85961,W,1,12,0.98,1113.0,M,-21.3,M*47\r\n";
|
||||
um982_feed(&gps, (const uint8_t *)data, (uint16_t)strlen(data));
|
||||
|
||||
ASSERT_NEAR(gps.heading, 100.0, 0.01, "heading from THS");
|
||||
ASSERT_EQ_INT(gps.fix_quality, 1, "fix from GGA");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_feed_partial_then_complete(void)
|
||||
{
|
||||
TEST("feed: partial bytes then complete");
|
||||
reset_gps();
|
||||
mock_set_tick(5000);
|
||||
|
||||
const char *part1 = "$GNTHS,200.";
|
||||
const char *part2 = "5000,A*1E\r\n";
|
||||
um982_feed(&gps, (const uint8_t *)part1, (uint16_t)strlen(part1));
|
||||
/* Heading should not be set yet */
|
||||
ASSERT_NAN(gps.heading, "should be NAN before complete");
|
||||
|
||||
um982_feed(&gps, (const uint8_t *)part2, (uint16_t)strlen(part2));
|
||||
ASSERT_NEAR(gps.heading, 200.5, 0.01, "heading after complete");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_feed_bad_checksum_rejected(void)
|
||||
{
|
||||
TEST("feed: bad checksum sentence is rejected");
|
||||
reset_gps();
|
||||
mock_set_tick(5000);
|
||||
|
||||
const char *data = "$GNTHS,999.0000,A*FF\r\n";
|
||||
um982_feed(&gps, (const uint8_t *)data, (uint16_t)strlen(data));
|
||||
|
||||
ASSERT_NAN(gps.heading, "heading should remain NAN");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_feed_versiona_response(void)
|
||||
{
|
||||
TEST("feed: VERSIONA response sets flag");
|
||||
reset_gps();
|
||||
|
||||
const char *data = "#VERSIONA,79,GPS,FINE,2326,378237000,15434,0,18,889;\"UM982\"\r\n";
|
||||
um982_feed(&gps, (const uint8_t *)data, (uint16_t)strlen(data));
|
||||
|
||||
ASSERT_TRUE(gps.version_received, "version_received should be true");
|
||||
ASSERT_TRUE(gps.initialized, "VERSIONA should mark communication alive");
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ========================= Validity / age tests ====================== */
|
||||
|
||||
static void test_heading_valid_within_timeout(void)
|
||||
{
|
||||
TEST("validity: heading valid within timeout");
|
||||
reset_gps();
|
||||
mock_set_tick(10000);
|
||||
|
||||
um982_parse_sentence(&gps, "$GNTHS,341.3344,A*1F");
|
||||
|
||||
/* Still at tick 10000 */
|
||||
ASSERT_TRUE(um982_is_heading_valid(&gps), "should be valid");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_heading_invalid_after_timeout(void)
|
||||
{
|
||||
TEST("validity: heading invalid after 2s timeout");
|
||||
reset_gps();
|
||||
mock_set_tick(10000);
|
||||
|
||||
um982_parse_sentence(&gps, "$GNTHS,341.3344,A*1F");
|
||||
|
||||
/* Advance past timeout */
|
||||
mock_set_tick(12500);
|
||||
ASSERT_FALSE(um982_is_heading_valid(&gps), "should be invalid after 2.5s");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_heading_invalid_mode_v(void)
|
||||
{
|
||||
TEST("validity: heading invalid with mode V");
|
||||
reset_gps();
|
||||
mock_set_tick(10000);
|
||||
|
||||
um982_parse_sentence(&gps, "$GNTHS,,V*10");
|
||||
|
||||
ASSERT_FALSE(um982_is_heading_valid(&gps), "mode V is invalid");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_position_valid(void)
|
||||
{
|
||||
TEST("validity: position valid with fix quality 1");
|
||||
reset_gps();
|
||||
mock_set_tick(10000);
|
||||
|
||||
um982_parse_sentence(&gps,
|
||||
"$GNGGA,001043.00,4404.14036,N,12118.85961,W,1,12,0.98,1113.0,M,-21.3,M*47");
|
||||
|
||||
ASSERT_TRUE(um982_is_position_valid(&gps), "should be valid");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_position_invalid_no_fix(void)
|
||||
{
|
||||
TEST("validity: position invalid with no fix");
|
||||
reset_gps();
|
||||
mock_set_tick(10000);
|
||||
|
||||
um982_parse_sentence(&gps,
|
||||
"$GNGGA,235959.00,,,,,0,00,99.99,,,,,,*79");
|
||||
|
||||
ASSERT_FALSE(um982_is_position_valid(&gps), "no fix = invalid");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_position_age_uses_last_valid_fix(void)
|
||||
{
|
||||
TEST("age: position age uses last valid fix, not no-fix GGA");
|
||||
reset_gps();
|
||||
|
||||
mock_set_tick(10000);
|
||||
um982_parse_sentence(&gps,
|
||||
"$GNGGA,001043.00,4404.14036,N,12118.85961,W,1,12,0.98,1113.0,M,-21.3,M*47");
|
||||
|
||||
mock_set_tick(12000);
|
||||
um982_parse_sentence(&gps,
|
||||
"$GNGGA,235959.00,,,,,0,00,99.99,,,,,,*79");
|
||||
|
||||
mock_set_tick(12500);
|
||||
ASSERT_EQ_INT(um982_position_age(&gps), 2500, "age should still be from last valid fix");
|
||||
ASSERT_FALSE(um982_is_position_valid(&gps), "latest no-fix GGA should invalidate position");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_heading_age(void)
|
||||
{
|
||||
TEST("age: heading age computed correctly");
|
||||
reset_gps();
|
||||
mock_set_tick(10000);
|
||||
|
||||
um982_parse_sentence(&gps, "$GNTHS,341.3344,A*1F");
|
||||
|
||||
mock_set_tick(10500);
|
||||
uint32_t age = um982_heading_age(&gps);
|
||||
ASSERT_EQ_INT(age, 500, "age should be 500ms");
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ========================= Send command tests ======================== */
|
||||
|
||||
static void test_send_command_appends_crlf(void)
|
||||
{
|
||||
TEST("send_command: appends \\r\\n");
|
||||
reset_gps();
|
||||
|
||||
um982_send_command(&gps, "GPGGA COM2 1");
|
||||
|
||||
/* Check that TX buffer contains "GPGGA COM2 1\r\n" */
|
||||
const char *expected = "GPGGA COM2 1\r\n";
|
||||
ASSERT_TRUE(mock_uart_tx_len == strlen(expected), "TX length");
|
||||
ASSERT_TRUE(memcmp(mock_uart_tx_buf, expected, strlen(expected)) == 0,
|
||||
"TX content should be 'GPGGA COM2 1\\r\\n'");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_send_command_null_safety(void)
|
||||
{
|
||||
TEST("send_command: NULL gps returns false");
|
||||
ASSERT_FALSE(um982_send_command(NULL, "RESET"), "should return false");
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ========================= Init sequence tests ======================= */
|
||||
|
||||
static void test_init_sends_correct_commands(void)
|
||||
{
|
||||
TEST("init: sends correct command sequence");
|
||||
spy_reset();
|
||||
mock_uart_tx_clear();
|
||||
|
||||
/* Pre-load VERSIONA response so init succeeds */
|
||||
const char *ver_resp = "#VERSIONA,79,GPS,FINE,2326,378237000,15434,0,18,889;\"UM982\"\r\n";
|
||||
mock_uart_rx_load(&huart5, (const uint8_t *)ver_resp, (uint16_t)strlen(ver_resp));
|
||||
|
||||
UM982_GPS_t init_gps;
|
||||
bool ok = um982_init(&init_gps, &huart5, 50.0f, 3.0f);
|
||||
|
||||
ASSERT_TRUE(ok, "init should succeed");
|
||||
ASSERT_TRUE(init_gps.initialized, "should be initialized");
|
||||
|
||||
/* Verify TX buffer contains expected commands */
|
||||
const char *tx = (const char *)mock_uart_tx_buf;
|
||||
ASSERT_TRUE(strstr(tx, "UNLOG\r\n") != NULL, "should send UNLOG");
|
||||
ASSERT_TRUE(strstr(tx, "CONFIG HEADING FIXLENGTH\r\n") != NULL, "should send CONFIG HEADING");
|
||||
ASSERT_TRUE(strstr(tx, "CONFIG HEADING LENGTH 50 3\r\n") != NULL, "should send LENGTH");
|
||||
ASSERT_TRUE(strstr(tx, "GPGGA COM2 1\r\n") != NULL, "should enable GGA");
|
||||
ASSERT_TRUE(strstr(tx, "GPRMC COM2 1\r\n") != NULL, "should enable RMC");
|
||||
ASSERT_TRUE(strstr(tx, "GPTHS COM2 0.2\r\n") != NULL, "should enable THS at 5Hz");
|
||||
ASSERT_TRUE(strstr(tx, "SAVECONFIG\r\n") == NULL, "should NOT save config (NVM wear)");
|
||||
ASSERT_TRUE(strstr(tx, "VERSIONA\r\n") != NULL, "should query version");
|
||||
|
||||
/* Verify command order: UNLOG should come before GPGGA */
|
||||
const char *unlog_pos = strstr(tx, "UNLOG\r\n");
|
||||
const char *gpgga_pos = strstr(tx, "GPGGA COM2 1\r\n");
|
||||
ASSERT_TRUE(unlog_pos < gpgga_pos, "UNLOG should precede GPGGA");
|
||||
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_init_no_baseline(void)
|
||||
{
|
||||
TEST("init: baseline=0 skips LENGTH command");
|
||||
spy_reset();
|
||||
mock_uart_tx_clear();
|
||||
|
||||
const char *ver_resp = "#VERSIONA,79,GPS,FINE,2326,378237000,15434,0,18,889;\"UM982\"\r\n";
|
||||
mock_uart_rx_load(&huart5, (const uint8_t *)ver_resp, (uint16_t)strlen(ver_resp));
|
||||
|
||||
UM982_GPS_t init_gps;
|
||||
um982_init(&init_gps, &huart5, 0.0f, 0.0f);
|
||||
|
||||
const char *tx = (const char *)mock_uart_tx_buf;
|
||||
ASSERT_TRUE(strstr(tx, "CONFIG HEADING LENGTH") == NULL, "should NOT send LENGTH");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_init_fails_no_version(void)
|
||||
{
|
||||
TEST("init: fails if no VERSIONA response");
|
||||
spy_reset();
|
||||
mock_uart_tx_clear();
|
||||
|
||||
/* Don't load any RX data — init should timeout */
|
||||
UM982_GPS_t init_gps;
|
||||
bool ok = um982_init(&init_gps, &huart5, 50.0f, 3.0f);
|
||||
|
||||
ASSERT_FALSE(ok, "init should fail without version response");
|
||||
ASSERT_FALSE(init_gps.initialized, "should not be initialized");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_nmea_traffic_sets_initialized_without_versiona(void)
|
||||
{
|
||||
TEST("init state: supported NMEA traffic sets initialized");
|
||||
reset_gps();
|
||||
|
||||
ASSERT_FALSE(gps.initialized, "should start uninitialized");
|
||||
um982_parse_sentence(&gps, "$GNTHS,341.3344,A*1F");
|
||||
ASSERT_TRUE(gps.initialized, "supported NMEA should mark communication alive");
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ========================= Edge case tests =========================== */
|
||||
|
||||
static void test_empty_fields_handled(void)
|
||||
{
|
||||
TEST("edge: GGA with empty lat/lon fields");
|
||||
reset_gps();
|
||||
gps.latitude = 99.99;
|
||||
gps.longitude = 99.99;
|
||||
|
||||
/* GGA with empty position fields (no fix) */
|
||||
um982_parse_sentence(&gps,
|
||||
"$GNGGA,235959.00,,,,,0,00,99.99,,,,,,*79");
|
||||
|
||||
ASSERT_EQ_INT(gps.fix_quality, 0, "no fix");
|
||||
/* Latitude/longitude should not be updated (fields are empty) */
|
||||
ASSERT_NEAR(gps.latitude, 99.99, 0.01, "lat unchanged");
|
||||
ASSERT_NEAR(gps.longitude, 99.99, 0.01, "lon unchanged");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_sentence_too_short(void)
|
||||
{
|
||||
TEST("edge: sentence too short to have formatter");
|
||||
reset_gps();
|
||||
/* Should not crash */
|
||||
um982_parse_sentence(&gps, "$GN");
|
||||
um982_parse_sentence(&gps, "$");
|
||||
um982_parse_sentence(&gps, "");
|
||||
um982_parse_sentence(&gps, NULL);
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_line_overflow(void)
|
||||
{
|
||||
TEST("edge: oversized line is dropped");
|
||||
reset_gps();
|
||||
|
||||
/* Create a line longer than UM982_LINE_BUF_SIZE */
|
||||
char big[200];
|
||||
memset(big, 'X', sizeof(big));
|
||||
big[0] = '$';
|
||||
big[198] = '\n';
|
||||
big[199] = '\0';
|
||||
|
||||
um982_feed(&gps, (const uint8_t *)big, 199);
|
||||
/* Should not crash, heading should still be NAN */
|
||||
ASSERT_NAN(gps.heading, "no valid data from overflow");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_process_via_mock_uart(void)
|
||||
{
|
||||
TEST("process: reads from mock UART RX buffer");
|
||||
reset_gps();
|
||||
mock_set_tick(5000);
|
||||
|
||||
/* Load data into mock UART RX */
|
||||
const char *data = "$GNTHS,275.1234,D*18\r\n";
|
||||
mock_uart_rx_load(&huart5, (const uint8_t *)data, (uint16_t)strlen(data));
|
||||
|
||||
/* Call process() which reads from UART */
|
||||
um982_process(&gps);
|
||||
|
||||
ASSERT_NEAR(gps.heading, 275.1234, 0.001, "heading via process()");
|
||||
ASSERT_EQ_INT(gps.heading_mode, 'D', "mode D");
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ========================= PR #68 bug regression tests =============== */
|
||||
|
||||
/* These tests specifically verify the bugs found in the reverted PR #68 */
|
||||
|
||||
static void test_regression_sentence_id_with_gn_prefix(void)
|
||||
{
|
||||
TEST("regression: GN-prefixed GGA is correctly identified");
|
||||
reset_gps();
|
||||
|
||||
/* PR #68 bug: strncmp(sentence, "GGA", 3) compared "GNG" vs "GGA" — never matched.
|
||||
* Our fix: skip 2-char talker ID, compare at sentence+3. */
|
||||
um982_parse_sentence(&gps,
|
||||
"$GNGGA,001043.00,4404.14036,N,12118.85961,W,1,12,0.98,1113.0,M,-21.3,M*47");
|
||||
|
||||
ASSERT_EQ_INT(gps.fix_quality, 1, "GGA should parse with GN prefix");
|
||||
ASSERT_NEAR(gps.latitude, 44.069006, 0.001, "latitude should be parsed");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_regression_longitude_3digit_degrees(void)
|
||||
{
|
||||
TEST("regression: 3-digit longitude degrees parsed correctly");
|
||||
reset_gps();
|
||||
|
||||
/* PR #68 bug: hardcoded 2-digit degrees for longitude.
|
||||
* 12118.85961 should be 121° 18.85961' = 121.314327° */
|
||||
um982_parse_sentence(&gps,
|
||||
"$GNGGA,001043.00,4404.14036,N,12118.85961,W,1,12,0.98,1113.0,M,-21.3,M*47");
|
||||
|
||||
ASSERT_NEAR(gps.longitude, -(121.0 + 18.85961/60.0), 0.0001,
|
||||
"longitude 121° should not be parsed as 12°");
|
||||
ASSERT_TRUE(gps.longitude < -100.0, "longitude should be > 100 degrees");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_regression_hemisphere_no_ptr_corrupt(void)
|
||||
{
|
||||
TEST("regression: hemisphere parsing doesn't corrupt field pointer");
|
||||
reset_gps();
|
||||
|
||||
/* PR #68 bug: GGA/RMC hemisphere cases manually advanced ptr,
|
||||
* desynchronizing from field counter. Our parser uses proper tokenizer. */
|
||||
um982_parse_sentence(&gps,
|
||||
"$GNGGA,001043.00,4404.14036,N,12118.85961,W,1,12,0.98,1113.0,M,-21.3,M*47");
|
||||
|
||||
/* After lat/lon, remaining fields should be correct */
|
||||
ASSERT_EQ_INT(gps.num_satellites, 12, "sats after hemisphere");
|
||||
ASSERT_NEAR(gps.hdop, 0.98, 0.01, "hdop after hemisphere");
|
||||
ASSERT_NEAR(gps.altitude, 1113.0, 0.1, "altitude after hemisphere");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_regression_rmc_also_parsed(void)
|
||||
{
|
||||
TEST("regression: RMC sentence is actually parsed (not dead code)");
|
||||
reset_gps();
|
||||
|
||||
/* PR #68 bug: identifySentence never matched GGA/RMC, so position
|
||||
* parsing was dead code. */
|
||||
um982_parse_sentence(&gps,
|
||||
"$GNRMC,001031.00,A,4404.13993,N,12118.86023,W,0.146,,100117,,,A*7B");
|
||||
|
||||
ASSERT_TRUE(gps.latitude > 44.0, "RMC lat should be parsed");
|
||||
ASSERT_TRUE(gps.longitude < -121.0, "RMC lon should be parsed");
|
||||
ASSERT_NEAR(gps.speed_knots, 0.146, 0.001, "RMC speed");
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ========================= Main ====================================== */
|
||||
|
||||
int main(void)
|
||||
{
|
||||
printf("=== UM982 GPS Driver Tests ===\n\n");
|
||||
|
||||
printf("--- Checksum ---\n");
|
||||
test_checksum_valid();
|
||||
test_checksum_valid_ths();
|
||||
test_checksum_invalid();
|
||||
test_checksum_missing_star();
|
||||
test_checksum_null();
|
||||
test_checksum_no_dollar();
|
||||
|
||||
printf("\n--- Coordinate Parsing ---\n");
|
||||
test_coord_latitude_north();
|
||||
test_coord_latitude_south();
|
||||
test_coord_longitude_3digit();
|
||||
test_coord_longitude_east();
|
||||
test_coord_empty();
|
||||
test_coord_null();
|
||||
test_coord_no_dot();
|
||||
|
||||
printf("\n--- GGA Parsing ---\n");
|
||||
test_parse_gga_full();
|
||||
test_parse_gga_rtk_fixed();
|
||||
test_parse_gga_no_fix();
|
||||
|
||||
printf("\n--- RMC Parsing ---\n");
|
||||
test_parse_rmc_valid();
|
||||
test_parse_rmc_void();
|
||||
|
||||
printf("\n--- THS Parsing ---\n");
|
||||
test_parse_ths_autonomous();
|
||||
test_parse_ths_not_valid();
|
||||
test_parse_ths_zero();
|
||||
test_parse_ths_360_boundary();
|
||||
|
||||
printf("\n--- VTG Parsing ---\n");
|
||||
test_parse_vtg();
|
||||
|
||||
printf("\n--- Talker IDs ---\n");
|
||||
test_talker_gp();
|
||||
test_talker_gl();
|
||||
|
||||
printf("\n--- Feed / Line Assembly ---\n");
|
||||
test_feed_single_sentence();
|
||||
test_feed_multiple_sentences();
|
||||
test_feed_partial_then_complete();
|
||||
test_feed_bad_checksum_rejected();
|
||||
test_feed_versiona_response();
|
||||
|
||||
printf("\n--- Validity / Age ---\n");
|
||||
test_heading_valid_within_timeout();
|
||||
test_heading_invalid_after_timeout();
|
||||
test_heading_invalid_mode_v();
|
||||
test_position_valid();
|
||||
test_position_invalid_no_fix();
|
||||
test_position_age_uses_last_valid_fix();
|
||||
test_heading_age();
|
||||
|
||||
printf("\n--- Send Command ---\n");
|
||||
test_send_command_appends_crlf();
|
||||
test_send_command_null_safety();
|
||||
|
||||
printf("\n--- Init Sequence ---\n");
|
||||
test_init_sends_correct_commands();
|
||||
test_init_no_baseline();
|
||||
test_init_fails_no_version();
|
||||
test_nmea_traffic_sets_initialized_without_versiona();
|
||||
|
||||
printf("\n--- Edge Cases ---\n");
|
||||
test_empty_fields_handled();
|
||||
test_sentence_too_short();
|
||||
test_line_overflow();
|
||||
test_process_via_mock_uart();
|
||||
|
||||
printf("\n--- PR #68 Regression ---\n");
|
||||
test_regression_sentence_id_with_gn_prefix();
|
||||
test_regression_longitude_3digit_degrees();
|
||||
test_regression_hemisphere_no_ptr_corrupt();
|
||||
test_regression_rmc_also_parsed();
|
||||
|
||||
printf("\n===============================================\n");
|
||||
printf(" Results: %d passed, %d failed (of %d total)\n",
|
||||
tests_passed, tests_failed, tests_passed + tests_failed);
|
||||
printf("===============================================\n");
|
||||
|
||||
return tests_failed > 0 ? 1 : 0;
|
||||
}
|
||||
Reference in New Issue
Block a user