diff --git a/src/main/fc/fc_msp.c b/src/main/fc/fc_msp.c index 8ac4b36301d..a4240986382 100644 --- a/src/main/fc/fc_msp.c +++ b/src/main/fc/fc_msp.c @@ -3357,6 +3357,7 @@ static mspResult_e mspFcProcessInCommand(uint16_t cmdMSP, sbuf_t *src) gpsSol.flags.validVelD = false; gpsSol.flags.validEPE = false; gpsSol.flags.validTime = false; + gpsSol.flags.validFixTime = false; gpsSol.numSat = sbufReadU8(src); gpsSol.llh.lat = sbufReadU32(src); gpsSol.llh.lon = sbufReadU32(src); @@ -4588,6 +4589,7 @@ static void readMspSimulatorValues(sbuf_t *src, const int dataSize, const uint8_ gpsSolDRV.fixType = sbufReadU8(src); gpsSolDRV.hdop = gpsSolDRV.fixType == GPS_NO_FIX ? 9999 : 100; gpsSolDRV.numSat = sbufReadU8(src); + gpsSolDRV.flags.validFixTime = false; if (gpsSolDRV.fixType != GPS_NO_FIX) { gpsSolDRV.flags.validVelNE = true; diff --git a/src/main/io/crsf_sensor.c b/src/main/io/crsf_sensor.c index c140fef6320..2fc8eae4fb1 100644 --- a/src/main/io/crsf_sensor.c +++ b/src/main/io/crsf_sensor.c @@ -136,6 +136,7 @@ static void crsfSensorHandleGPS(const uint8_t *payload) gpsSolDRV.flags.validVelD = false; gpsSolDRV.flags.validEPE = false; gpsSolDRV.flags.validTime = false; + gpsSolDRV.flags.validFixTime = false; gpsSolDRV.eph = gpsConstrainEPE(200); // default ~2m gpsSolDRV.epv = gpsConstrainEPE(400); // default ~4m diff --git a/src/main/io/gps.c b/src/main/io/gps.c index 5d34c97ec3c..f83707372d5 100755 --- a/src/main/io/gps.c +++ b/src/main/io/gps.c @@ -264,6 +264,7 @@ void processDisableGPSFix(void) gpsSol.flags.validVelD = false; gpsSol.flags.validEPE = false; gpsSol.flags.validTime = false; + gpsSol.flags.validFixTime = false; gpsSol.flags.validEllipsoidAltitude = false; gpsSol.flags.validSpeedAccuracy = false; gpsSol.flags.validHeadingAccuracy = false; @@ -313,6 +314,7 @@ void updateEstimatedGPSFix(void) gpsSol.flags.validVelD = false; //do not provide velocity.z gpsSol.flags.validEPE = true; gpsSol.flags.validTime = false; + gpsSol.flags.validFixTime = false; float speed = pidProfile()->fixedWingReferenceAirspeed; @@ -440,6 +442,7 @@ static void gpsResetSolution(gpsSolutionData_t* gpsSol) gpsSol->flags.validVelD = false; gpsSol->flags.validEPE = false; gpsSol->flags.validTime = false; + gpsSol->flags.validFixTime = false; gpsSol->flags.validEllipsoidAltitude = false; gpsSol->flags.validSpeedAccuracy = false; gpsSol->flags.validHeadingAccuracy = false; diff --git a/src/main/io/gps.h b/src/main/io/gps.h index ec531a5c6ba..66e204d96e8 100755 --- a/src/main/io/gps.h +++ b/src/main/io/gps.h @@ -132,6 +132,7 @@ typedef struct gpsSolutionData_s { bool validVelD; bool validEPE; // EPH/EPV values are valid - actual accuracy bool validTime; + bool validFixTime; // time is the fully resolved UTC of the same measurement as llh, with ms resolution bool validEllipsoidAltitude; bool validSpeedAccuracy; bool validHeadingAccuracy; @@ -155,6 +156,7 @@ typedef struct gpsSolutionData_s { uint16_t hdop; // generic HDOP value (*HDOP_SCALE) dateTime_t time; // GPS time in UTC + uint32_t fixTimeOfDayMs; // UTC time of day of the same measurement as llh in ms, valid when flags.validFixTime } gpsSolutionData_t; diff --git a/src/main/io/gps_dronecan.c b/src/main/io/gps_dronecan.c index 2c689d88724..584767ad3be 100644 --- a/src/main/io/gps_dronecan.c +++ b/src/main/io/gps_dronecan.c @@ -124,6 +124,7 @@ void dronecanGPSReceiveGNSSFix(const struct uavcan_equipment_gnss_Fix * pgnssFix // gpsSolDRV.time.millis = 0; gpsSolDRV.flags.validTime = 0; //(pkt->fixType >= 3); + gpsSolDRV.flags.validFixTime = false; gpsProcessNewDriverData(); newDataReady = true; @@ -173,6 +174,7 @@ void dronecanGPSReceiveGNSSFix2(const struct uavcan_equipment_gnss_Fix2 * pgnssF // gpsSolDRV.time.millis = 0; gpsSolDRV.flags.validTime = 0; //(pkt->fixType >= 3); + gpsSolDRV.flags.validFixTime = false; gpsProcessNewDriverData(); newDataReady = true; diff --git a/src/main/io/gps_fake.c b/src/main/io/gps_fake.c index da24e9c03c3..4d5bf87a591 100644 --- a/src/main/io/gps_fake.c +++ b/src/main/io/gps_fake.c @@ -80,6 +80,7 @@ void gpsFakeSet( gpsSolDRV.flags.validVelNE = true; gpsSolDRV.flags.validVelD = true; gpsSolDRV.flags.validEPE = true; + gpsSolDRV.flags.validFixTime = false; if (time) { struct tm* gTime = gmtime(&time); diff --git a/src/main/io/gps_msp.c b/src/main/io/gps_msp.c index 222e2651e7f..0c31eb8797a 100644 --- a/src/main/io/gps_msp.c +++ b/src/main/io/gps_msp.c @@ -111,6 +111,7 @@ void mspGPSReceiveNewData(const uint8_t * bufferPtr, unsigned int dataSize) gpsSolDRV.time.millis = 0; gpsSolDRV.flags.validTime = (pkt->fixType >= 3); + gpsSolDRV.flags.validFixTime = false; // MSP2_SENSOR_GPS is accepted with any gps_provider gpsProcessNewDriverData(); newDataReady = true; diff --git a/src/main/io/gps_ublox.c b/src/main/io/gps_ublox.c index 703242d10dd..b8ffe2fdc17 100755 --- a/src/main/io/gps_ublox.c +++ b/src/main/io/gps_ublox.c @@ -114,6 +114,12 @@ static bool _new_position; // do we have new speed information? static bool _new_speed; +// tAcc stays around 20 s while the leap seconds are unresolved, validTime alone does not exclude that +#define UBX_PVT_FIX_TIME_MAX_TACC_NS 1000000 +// validTime and the leap seconds can still change right after start, require a stable run of good epochs +#define UBX_PVT_FIX_TIME_STABLE_EPOCHS 5 +static uint8_t fixTimeGoodEpochs; + // Need this to determine if Galileo capable only static struct { uint8_t supported; @@ -626,6 +632,29 @@ static uint8_t gpsDecodeHardwareVersion(const char * szBuf, unsigned nBufSize) return UBX_HW_VERSION_UNKNOWN; } +static bool isPvtFixTimeExact(const ubx_nav_pvt *pvt, uint16_t payloadLength, uint8_t fixType) +{ + return payloadLength >= sizeof(ubx_nav_pvt) + && UBX_VALID_GPS_DATE_TIME(pvt->valid) + && UBX_VALID_GPS_FULLY_RESOLVED(pvt->valid) + && (!UBX_PVT_CONFIRMED_AVAILABLE(pvt->flags2) || UBX_PVT_CONFIRMED_TIME(pvt->flags2)) + && fixType == GPS_FIX_3D + && pvt->tAcc <= UBX_PVT_FIX_TIME_MAX_TACC_NS; +} + +// NAV-PVT hour..sec are rounded to 1/100 s, nano (-5..+995 ms) is the signed offset from them +static bool getPvtTimeOfDayMs(const ubx_nav_pvt *pvt, uint32_t *timeOfDayMs) +{ + const int32_t nanoMs = pvt->nano >= 0 ? (pvt->nano + 500000) / 1000000 : -((500000 - pvt->nano) / 1000000); + const int32_t ms = ((pvt->hour * 60 + pvt->min) * 60 + pvt->sec) * 1000 + nanoMs; + // outside hour..sec's day we cannot tell whether that day had a leap second + if (ms < 0 || (ms >= 86400000 && pvt->sec < 60)) { + return false; + } + *timeOfDayMs = ms; + return true; +} + static bool gpsParseFrameUBLOX(void) { switch (_msg_id) { @@ -638,6 +667,7 @@ static bool gpsParseFrameUBLOX(void) gpsSolDRV.epv = gpsConstrainEPE(_buffer.posllh.vertical_accuracy / 10); gpsSolDRV.flags.validEPE = true; gpsSolDRV.flags.validEllipsoidAltitude = true; + gpsSolDRV.flags.validFixTime = false; // fixTimeOfDayMs belongs to the last NAV-PVT, not to this position if (next_fix_type != GPS_NO_FIX) gpsSolDRV.fixType = next_fix_type; _new_position = true; @@ -684,6 +714,9 @@ static bool gpsParseFrameUBLOX(void) } break; case MSG_PVT: + if (_class != CLASS_NAV) { + break; + } { static int pvtCount = 0; DEBUG_SET(DEBUG_GPS, 0, pvtCount++); @@ -714,6 +747,12 @@ static bool gpsParseFrameUBLOX(void) gpsSolDRV.flags.validSpeedAccuracy = true; gpsSolDRV.flags.validHeadingAccuracy = true; + if (isPvtFixTimeExact(&_buffer.pvt, _payload_length, next_fix_type)) { + fixTimeGoodEpochs = MIN(fixTimeGoodEpochs + 1, UBX_PVT_FIX_TIME_STABLE_EPOCHS); + } else { + fixTimeGoodEpochs = 0; + } + if (UBX_VALID_GPS_DATE_TIME(_buffer.pvt.valid)) { gpsSolDRV.time.year = _buffer.pvt.year; gpsSolDRV.time.month = _buffer.pvt.month; @@ -724,8 +763,11 @@ static bool gpsParseFrameUBLOX(void) gpsSolDRV.time.millis = (uint16_t)(MAX(0, _buffer.pvt.nano) / (1000*1000)); gpsSolDRV.flags.validTime = true; + gpsSolDRV.flags.validFixTime = fixTimeGoodEpochs >= UBX_PVT_FIX_TIME_STABLE_EPOCHS + && getPvtTimeOfDayMs(&_buffer.pvt, &gpsSolDRV.fixTimeOfDayMs); } else { gpsSolDRV.flags.validTime = false; + gpsSolDRV.flags.validFixTime = false; } _new_position = true; @@ -860,7 +902,7 @@ static bool gpsParseFrameUBLOX(void) return false; } -static bool gpsNewFrameUBLOX(uint8_t data) +STATIC_UNIT_TESTED bool gpsNewFrameUBLOX(uint8_t data) { bool parsed = false; @@ -1320,6 +1362,8 @@ void gpsRestartUBLOX(void) satelites[i].gnssId = 0xFF; } + fixTimeGoodEpochs = 0; + ptSemaphoreInit(semNewDataReady); ptRestart(ptGetHandle(gpsProtocolReceiverThread)); ptRestart(ptGetHandle(gpsProtocolStateThread)); diff --git a/src/main/io/gps_ublox.h b/src/main/io/gps_ublox.h index 75f10901035..de9e788843d 100644 --- a/src/main/io/gps_ublox.h +++ b/src/main/io/gps_ublox.h @@ -61,6 +61,9 @@ STATIC_ASSERT(MAX_UBLOX_PAYLOAD_SIZE >= 256, ubx_size_too_small); #define UBX_VALID_GPS_DATE(valid) (valid & 1 << 0) #define UBX_VALID_GPS_TIME(valid) (valid & 1 << 1) #define UBX_VALID_GPS_DATE_TIME(valid) (UBX_VALID_GPS_DATE(valid) && UBX_VALID_GPS_TIME(valid)) +#define UBX_VALID_GPS_FULLY_RESOLVED(valid) (valid & 1 << 2) +#define UBX_PVT_CONFIRMED_AVAILABLE(flags2) (flags2 & 1 << 5) +#define UBX_PVT_CONFIRMED_TIME(flags2) (flags2 & 1 << 7) /* * hwVersion encoding (fits in uint8_t): @@ -436,7 +439,7 @@ typedef struct { int32_t nano; uint8_t fix_type; uint8_t fix_status; - uint8_t reserved1; + uint8_t flags2; uint8_t satellites; int32_t longitude; int32_t latitude; diff --git a/src/main/telemetry/crsf.c b/src/main/telemetry/crsf.c index b30819f185f..2f80acb26b8 100755 --- a/src/main/telemetry/crsf.c +++ b/src/main/telemetry/crsf.c @@ -80,6 +80,8 @@ #define CRSF_MSP_BUFFER_SIZE 96 #define CRSF_MSP_LENGTH_OFFSET 1 +#define CRSF_FRAME_GPS_TIME_TAIL_PAYLOAD_SIZE 5 + static uint8_t crsfCrc; static bool crsfTelemetryEnabled; static bool deviceInfoReplyPending; @@ -231,11 +233,20 @@ uint16_t Groundspeed ( km/h / 10 ) uint16_t GPS heading ( degree / 100 ) uint16 Altitude ( meter ­1000m offset ) uint8_t Satellites in use ( counter ) +Optional (extension, not part of the CRSF spec), only when the UTC time of this fix is exact +(u-blox NAV-PVT: date/time valid, fully resolved, confirmed if available, 3D fix, tAcc <= 1 ms, +all of it for 5 epochs in a row) and crsf_use_legacy_baro_packet is OFF: +uint32_t UTC time of day of this fix ( ms, rounded to the nearest ms ) +uint8_t Fix type ( always 2 = 3D while this tail is present ) */ static void crsfFrameGps(sbuf_t *dst) { + // UTC time of this fix in the same frame; a separate frame cannot be paired with it on the ground + const bool hasFixTime = gpsSol.flags.validFixTime && gpsSol.time.year != 0 + && !telemetryConfig()->crsf_use_legacy_baro_packet; + const uint8_t payloadSize = CRSF_FRAME_GPS_PAYLOAD_SIZE + (hasFixTime ? CRSF_FRAME_GPS_TIME_TAIL_PAYLOAD_SIZE : 0); // use sbufWrite since CRC does not include frame length - sbufWriteU8(dst, CRSF_FRAME_GPS_PAYLOAD_SIZE + CRSF_FRAME_LENGTH_TYPE_CRC); + sbufWriteU8(dst, payloadSize + CRSF_FRAME_LENGTH_TYPE_CRC); crsfSerialize8(dst, CRSF_FRAMETYPE_GPS); crsfSerialize32(dst, gpsSol.llh.lat); // CRSF and betaflight use same units for degrees crsfSerialize32(dst, gpsSol.llh.lon); @@ -243,6 +254,10 @@ static void crsfFrameGps(sbuf_t *dst) crsfSerialize16(dst, DECIDEGREES_TO_CENTIDEGREES(gpsSol.groundCourse)); // gpsSol.groundCourse is 0.1 degrees, need 0.01 deg crsfSerialize16(dst, (uint16_t)( (telemetryConfig()->crsf_use_legacy_baro_packet ? getEstimatedActualPosition(Z) : gpsSol.llh.alt ) / 100 + 1000) ); crsfSerialize8(dst, gpsSol.numSat); + if (hasFixTime) { + crsfSerialize32(dst, gpsSol.fixTimeOfDayMs); + crsfSerialize8(dst, gpsSol.fixType); + } } /* diff --git a/src/test/unit/CMakeLists.txt b/src/test/unit/CMakeLists.txt index 04a156b2298..1d18c3dbdb0 100644 --- a/src/test/unit/CMakeLists.txt +++ b/src/test/unit/CMakeLists.txt @@ -155,6 +155,11 @@ set_property(SOURCE osd_unittest.cc PROPERTY definitions OSD_UNIT_TEST USE_MSP_D set_property(SOURCE gps_ublox_unittest.cc PROPERTY depends "io/gps_ublox_utils.c") set_property(SOURCE gps_ublox_unittest.cc PROPERTY definitions GPS_UBLOX_UNIT_TEST) +set_property(SOURCE gps_ublox_pvt_unittest.cc PROPERTY depends "io/gps_ublox.c" "io/gps_ublox_utils.c") + +set_property(SOURCE telemetry_crsf_unittest.cc PROPERTY depends "telemetry/crsf.c" "common/crc.c" "common/streambuf.c") +set_property(SOURCE telemetry_crsf_unittest.cc PROPERTY definitions USE_SERIALRX_CRSF USE_TELEMETRY_CRSF) + set_property(SOURCE gps_null_port_unittest.cc PROPERTY depends "io/gps.c") set_property(SOURCE gps_null_port_unittest.cc PROPERTY definitions GPS_NULL_PORT_UNIT_TEST USE_GPS_PROTO_UBLOX) diff --git a/src/test/unit/gps_ublox_pvt_unittest.cc b/src/test/unit/gps_ublox_pvt_unittest.cc new file mode 100644 index 00000000000..675694b34f6 --- /dev/null +++ b/src/test/unit/gps_ublox_pvt_unittest.cc @@ -0,0 +1,411 @@ +/* + * This file is part of INAV. + * + * INAV is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * INAV is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with INAV. If not, see . + */ + +#include +#include + +#include "gtest/gtest.h" +#include "unittest_macros.h" + +extern "C" { +#include "platform.h" + +#include "build/debug.h" + +#include "common/typeconversion.h" + +#include "drivers/serial.h" +#include "drivers/time.h" + +#include "io/gps.h" +#include "io/gps_private.h" +#include "io/gps_ublox.h" +#include "io/serial.h" + +bool gpsNewFrameUBLOX(uint8_t data); + +gpsReceiverData_t gpsState; +gpsStatistics_t gpsStats; +gpsSolutionData_t gpsSolDRV; +gpsSolutionData_t gpsSol; +gpsConfig_t gpsConfig_System; +baudRate_e gpsToSerialBaudRate[GPS_BAUDRATE_COUNT]; +const uint32_t baudRates[] = { 0 }; + +int32_t debug[DEBUG32_VALUE_COUNT]; +uint8_t debugMode; + +// the parser under test only needs these to link, the serial and state paths are not exercised +timeMs_t millis(void) { return 0; } +float fastA2F(const char *p) { UNUSED(p); return 0; } +int gpsBaudRateToInt(gpsBaudRate_e baudrate) { UNUSED(baudrate); return 0; } +uint16_t gpsConstrainEPE(uint32_t epe) { return epe; } +uint16_t gpsConstrainHDOP(uint32_t hdop) { return hdop; } +void gpsProcessNewDriverData(void) {} +void gpsProcessNewSolutionData(bool timeout) { UNUSED(timeout); } +void gpsSetProtocolTimeout(timeMs_t timeoutMs) { UNUSED(timeoutMs); } +void gpsSetState(gpsState_e state) { UNUSED(state); } +bool isSerialTransmitBufferEmpty(const serialPort_t *instance) { UNUSED(instance); return true; } +void serialPrint(serialPort_t *instance, const char *str) { UNUSED(instance); UNUSED(str); } +uint8_t serialRead(serialPort_t *instance) { UNUSED(instance); return 0; } +uint32_t serialRxBytesWaiting(const serialPort_t *instance) { UNUSED(instance); return 0; } +void serialSetBaudRate(serialPort_t *instance, uint32_t baudRate) { UNUSED(instance); UNUSED(baudRate); } +void serialWriteBuf(serialPort_t *instance, const uint8_t *data, int count) +{ + UNUSED(instance); + UNUSED(data); + UNUSED(count); +} + +char *strnstr(const char *s, const char *find, size_t slen) +{ + const size_t len = strlen(find); + for (size_t i = 0; i + len <= slen && s[i]; i++) { + if (strncmp(s + i, find, len) == 0) { + return (char *)(s + i); + } + } + return NULL; +} +} + +#define NAV_PVT_PAYLOAD_SIZE 92 +#define STABLE_EPOCHS 5 + +static bool feedUbx(uint8_t msgClass, uint8_t msgId, const uint8_t *payload, uint16_t payloadLength) +{ + const uint8_t header[] = { PREAMBLE1, PREAMBLE2, msgClass, msgId, + (uint8_t)(payloadLength & 0xFF), (uint8_t)(payloadLength >> 8) }; + uint8_t ckA = 0; + uint8_t ckB = 0; + bool parsed = false; + + for (unsigned i = 0; i < sizeof(header); i++) { + if (i >= 2) { + ckA += header[i]; + ckB += ckA; + } + parsed |= gpsNewFrameUBLOX(header[i]); + } + for (unsigned i = 0; i < payloadLength; i++) { + ckA += payload[i]; + ckB += ckA; + parsed |= gpsNewFrameUBLOX(payload[i]); + } + parsed |= gpsNewFrameUBLOX(ckA); + parsed |= gpsNewFrameUBLOX(ckB); + return parsed; +} + +static bool feedPvt(uint8_t msgClass, uint16_t payloadLength, const ubx_nav_pvt *pvt) +{ + uint8_t payload[NAV_PVT_PAYLOAD_SIZE] = { 0 }; + memcpy(payload, pvt, sizeof(*pvt)); + return feedUbx(msgClass, MSG_PVT, payload, payloadLength); +} + +static bool feedPvtEpochs(uint8_t msgClass, uint16_t payloadLength, const ubx_nav_pvt *pvt, int count) +{ + bool parsed = true; + for (int i = 0; i < count; i++) { + parsed &= feedPvt(msgClass, payloadLength, pvt); + } + return parsed; +} + +static ubx_nav_pvt validPvt(uint8_t hour, uint8_t min, uint8_t sec, int32_t nano) +{ + ubx_nav_pvt pvt; + memset(&pvt, 0, sizeof(pvt)); + pvt.year = 2026; + pvt.month = 9; + pvt.day = 29; + pvt.hour = hour; + pvt.min = min; + pvt.sec = sec; + pvt.valid = 0x07; // validDate, validTime, fullyResolved + pvt.tAcc = 500000; // 500 us + pvt.nano = nano; + pvt.fix_type = FIX_3D; + pvt.fix_status = NAV_STATUS_FIX_VALID; + pvt.satellites = 12; + pvt.latitude = 470000000; + pvt.longitude = 80000000; + return pvt; +} + +static uint32_t timeOfDayMs(uint32_t hour, uint32_t min, uint32_t sec, uint32_t ms) +{ + return ((hour * 60 + min) * 60 + sec) * 1000 + ms; +} + +class GpsUbloxPvtTest : public ::testing::Test { +protected: + void SetUp() override + { + // one epoch without a valid time resets the driver's stability counter + ubx_nav_pvt reset = validPvt(0, 0, 0, 0); + reset.valid = 0; + feedPvt(CLASS_NAV, NAV_PVT_PAYLOAD_SIZE, &reset); + memset(&gpsSolDRV, 0, sizeof(gpsSolDRV)); + } + + void expectFixTime(const ubx_nav_pvt *pvt, uint32_t expectedTimeOfDayMs) + { + EXPECT_TRUE(feedPvtEpochs(CLASS_NAV, NAV_PVT_PAYLOAD_SIZE, pvt, STABLE_EPOCHS)); + EXPECT_TRUE(gpsSolDRV.flags.validFixTime); + EXPECT_EQ(expectedTimeOfDayMs, gpsSolDRV.fixTimeOfDayMs); + } + + void expectNoFixTime(uint8_t msgClass, uint16_t payloadLength, const ubx_nav_pvt *pvt) + { + feedPvtEpochs(msgClass, payloadLength, pvt, STABLE_EPOCHS + 1); + EXPECT_FALSE(gpsSolDRV.flags.validFixTime); + } + + void expectSingleEpoch(const ubx_nav_pvt *pvt, bool validFixTime, uint32_t expectedTimeOfDayMs = 0) + { + EXPECT_TRUE(feedPvt(CLASS_NAV, NAV_PVT_PAYLOAD_SIZE, pvt)); + EXPECT_EQ(validFixTime, gpsSolDRV.flags.validFixTime); + if (validFixTime) { + EXPECT_EQ(expectedTimeOfDayMs, gpsSolDRV.fixTimeOfDayMs); + } + } +}; + +TEST_F(GpsUbloxPvtTest, PositiveNanoRoundsToNearestMs) +{ + const ubx_nav_pvt pvt = validPvt(10, 20, 30, 199999990); + expectFixTime(&pvt, timeOfDayMs(10, 20, 30, 200)); + EXPECT_EQ(470000000, gpsSolDRV.llh.lat); + EXPECT_EQ(80000000, gpsSolDRV.llh.lon); +} + +TEST_F(GpsUbloxPvtTest, SmallNegativeNanoAtSecondBoundary) +{ + const ubx_nav_pvt pvt = validPvt(10, 20, 31, -10); + expectFixTime(&pvt, timeOfDayMs(10, 20, 31, 0)); +} + +TEST_F(GpsUbloxPvtTest, NegativeNanoGivesPreviousSecond) +{ + const ubx_nav_pvt pvt = validPvt(10, 20, 31, -3000000); + expectFixTime(&pvt, timeOfDayMs(10, 20, 30, 997)); +} + +TEST_F(GpsUbloxPvtTest, NanoRoundsHalfAwayFromZero) +{ + ubx_nav_pvt pvt = validPvt(10, 20, 31, 0); + expectFixTime(&pvt, timeOfDayMs(10, 20, 31, 0)); + + pvt.nano = -2600000; // round toward zero would give .998 + expectSingleEpoch(&pvt, true, timeOfDayMs(10, 20, 30, 997)); + + pvt.nano = -500000; + expectSingleEpoch(&pvt, true, timeOfDayMs(10, 20, 30, 999)); + + pvt.nano = -499999; + expectSingleEpoch(&pvt, true, timeOfDayMs(10, 20, 31, 0)); + + pvt.nano = 499999; + expectSingleEpoch(&pvt, true, timeOfDayMs(10, 20, 31, 0)); + + pvt.nano = 500000; + expectSingleEpoch(&pvt, true, timeOfDayMs(10, 20, 31, 1)); +} + +TEST_F(GpsUbloxPvtTest, TimeBeforeTheReportedDayIsNotExact) +{ + const ubx_nav_pvt pvt = validPvt(10, 20, 30, 0); + expectFixTime(&pvt, timeOfDayMs(10, 20, 30, 0)); + + // 23:59:59.997, or 23:59:60.997 after a leap second, of the previous day + const ubx_nav_pvt midnight = validPvt(0, 0, 0, -3000000); + expectSingleEpoch(&midnight, false); + + // a representation limit, not a receiver problem: the stable run continues + expectSingleEpoch(&pvt, true, timeOfDayMs(10, 20, 30, 0)); +} + +TEST_F(GpsUbloxPvtTest, TimeAfterTheReportedDayIsNotExact) +{ + const ubx_nav_pvt pvt = validPvt(10, 20, 30, 0); + expectFixTime(&pvt, timeOfDayMs(10, 20, 30, 0)); + + const ubx_nav_pvt pastMidnight = validPvt(23, 59, 59, 999600000); + expectSingleEpoch(&pastMidnight, false); + + const ubx_nav_pvt beforeMidnight = validPvt(23, 59, 59, 999400000); + expectSingleEpoch(&beforeMidnight, true, 86399999u); +} + +TEST_F(GpsUbloxPvtTest, LeapSecond) +{ + ubx_nav_pvt pvt = validPvt(23, 59, 60, 0); + expectFixTime(&pvt, 86400000u); + + pvt.nano = 995000000; + expectFixTime(&pvt, 86400995u); + + pvt.nano = -3000000; + expectFixTime(&pvt, 86399997u); +} + +TEST_F(GpsUbloxPvtTest, NotFullyResolved) +{ + ubx_nav_pvt pvt = validPvt(10, 20, 30, 0); + pvt.valid = 0x03; + expectNoFixTime(CLASS_NAV, NAV_PVT_PAYLOAD_SIZE, &pvt); + EXPECT_TRUE(gpsSolDRV.flags.validTime); +} + +TEST_F(GpsUbloxPvtTest, ConfirmedTime) +{ + ubx_nav_pvt pvt = validPvt(10, 20, 30, 0); + + pvt.flags2 = 0x20; // confirmedAvai without confirmedTime + expectNoFixTime(CLASS_NAV, NAV_PVT_PAYLOAD_SIZE, &pvt); + + pvt.flags2 = 0xA0; // confirmedAvai and confirmedTime + expectFixTime(&pvt, timeOfDayMs(10, 20, 30, 0)); + + pvt.flags2 = 0x20; + expectSingleEpoch(&pvt, false); + + pvt.flags2 = 0x00; // confirmation not available + expectFixTime(&pvt, timeOfDayMs(10, 20, 30, 0)); +} + +TEST_F(GpsUbloxPvtTest, WrongClassIsIgnored) +{ + const ubx_nav_pvt pvt = validPvt(10, 20, 30, 0); + + EXPECT_FALSE(feedPvtEpochs(0x02, NAV_PVT_PAYLOAD_SIZE, &pvt, STABLE_EPOCHS)); // RXM class, same id + EXPECT_FALSE(gpsSolDRV.flags.validFixTime); + EXPECT_FALSE(gpsSolDRV.flags.validTime); + EXPECT_EQ(0, gpsSolDRV.llh.lat); + EXPECT_EQ(0, gpsSolDRV.llh.lon); + + EXPECT_FALSE(feedPvtEpochs(0x29, NAV_PVT_PAYLOAD_SIZE, &pvt, STABLE_EPOCHS)); // NAV2-PVT, same id and layout + EXPECT_FALSE(gpsSolDRV.flags.validFixTime); + EXPECT_FALSE(gpsSolDRV.flags.validTime); + EXPECT_EQ(0, gpsSolDRV.llh.lat); + EXPECT_EQ(0, gpsSolDRV.llh.lon); +} + +TEST_F(GpsUbloxPvtTest, ShortPayload) +{ + const ubx_nav_pvt pvt = validPvt(10, 20, 30, 0); + expectNoFixTime(CLASS_NAV, sizeof(ubx_nav_pvt) - 4, &pvt); + EXPECT_TRUE(gpsSolDRV.flags.validTime); + + EXPECT_TRUE(feedPvtEpochs(CLASS_NAV, sizeof(ubx_nav_pvt), &pvt, STABLE_EPOCHS)); + EXPECT_TRUE(gpsSolDRV.flags.validFixTime); +} + +TEST_F(GpsUbloxPvtTest, DateOrTimeNotValid) +{ + ubx_nav_pvt pvt = validPvt(10, 20, 30, 0); + + pvt.valid = 0x06; // no validDate + expectNoFixTime(CLASS_NAV, NAV_PVT_PAYLOAD_SIZE, &pvt); + EXPECT_FALSE(gpsSolDRV.flags.validTime); + + pvt.valid = 0x05; // no validTime + expectNoFixTime(CLASS_NAV, NAV_PVT_PAYLOAD_SIZE, &pvt); + EXPECT_FALSE(gpsSolDRV.flags.validTime); +} + +TEST_F(GpsUbloxPvtTest, InvalidTimeClearsFixTime) +{ + ubx_nav_pvt pvt = validPvt(10, 20, 30, 0); + expectFixTime(&pvt, timeOfDayMs(10, 20, 30, 0)); + + pvt.valid = 0; + expectSingleEpoch(&pvt, false); + EXPECT_FALSE(gpsSolDRV.flags.validTime); +} + +TEST_F(GpsUbloxPvtTest, PosllhClearsFixTime) +{ + const ubx_nav_pvt pvt = validPvt(10, 20, 30, 0); + expectFixTime(&pvt, timeOfDayMs(10, 20, 30, 0)); + + ubx_nav_posllh posllh; + memset(&posllh, 0, sizeof(posllh)); + posllh.latitude = 470001000; + posllh.longitude = 80001000; + ASSERT_EQ(28u, sizeof(posllh)); + feedUbx(CLASS_NAV, MSG_POSLLH, (const uint8_t *)&posllh, sizeof(posllh)); + EXPECT_EQ(470001000, gpsSolDRV.llh.lat); + EXPECT_FALSE(gpsSolDRV.flags.validFixTime); +} + +TEST_F(GpsUbloxPvtTest, Requires3dFix) +{ + ubx_nav_pvt pvt = validPvt(10, 20, 30, 0); + + pvt.fix_type = FIX_2D; + expectNoFixTime(CLASS_NAV, NAV_PVT_PAYLOAD_SIZE, &pvt); + EXPECT_EQ(GPS_FIX_2D, gpsSolDRV.fixType); + + pvt.fix_type = FIX_3D; + pvt.fix_status = 0; // gnssFixOK not set + expectNoFixTime(CLASS_NAV, NAV_PVT_PAYLOAD_SIZE, &pvt); + EXPECT_EQ(GPS_NO_FIX, gpsSolDRV.fixType); +} + +TEST_F(GpsUbloxPvtTest, RequiresTimeAccuracy) +{ + ubx_nav_pvt pvt = validPvt(10, 20, 30, 0); + + pvt.tAcc = 2000000; // 2 ms + expectNoFixTime(CLASS_NAV, NAV_PVT_PAYLOAD_SIZE, &pvt); + + pvt.tAcc = 1000000; // 1 ms, the limit + expectFixTime(&pvt, timeOfDayMs(10, 20, 30, 0)); + + pvt.tAcc = 0xFFFFFFFF; // largest U4, far off like unresolved leap seconds + expectSingleEpoch(&pvt, false); + + pvt.tAcc = 500000; // 500 us + expectFixTime(&pvt, timeOfDayMs(10, 20, 30, 0)); +} + +TEST_F(GpsUbloxPvtTest, RequiresStableRunOfGoodEpochs) +{ + ubx_nav_pvt pvt = validPvt(10, 20, 30, 0); + + for (int epoch = 1; epoch <= STABLE_EPOCHS; epoch++) { + EXPECT_TRUE(feedPvt(CLASS_NAV, NAV_PVT_PAYLOAD_SIZE, &pvt)); + EXPECT_EQ(epoch == STABLE_EPOCHS, gpsSolDRV.flags.validFixTime) << "epoch " << epoch; + } + // 5 + 251 = 256 good epochs: a counter without saturation would wrap to 0 on the last one + for (int epoch = STABLE_EPOCHS + 1; epoch <= 256; epoch++) { + EXPECT_TRUE(feedPvt(CLASS_NAV, NAV_PVT_PAYLOAD_SIZE, &pvt)); + ASSERT_TRUE(gpsSolDRV.flags.validFixTime) << "epoch " << epoch; + } + + pvt.tAcc = 2000000; // one bad epoch resets the run + expectSingleEpoch(&pvt, false); + + pvt.tAcc = 500000; + for (int epoch = 1; epoch <= STABLE_EPOCHS; epoch++) { + EXPECT_TRUE(feedPvt(CLASS_NAV, NAV_PVT_PAYLOAD_SIZE, &pvt)); + EXPECT_EQ(epoch == STABLE_EPOCHS, gpsSolDRV.flags.validFixTime) << "epoch " << epoch; + } +} diff --git a/src/test/unit/target.h b/src/test/unit/target.h index 99ae99cb6ce..1e95d8269df 100755 --- a/src/test/unit/target.h +++ b/src/test/unit/target.h @@ -57,3 +57,7 @@ #define TARGET_IO_PORTB 0xffff #define TARGET_IO_PORTC 0xffff +#include +// glibc has no strnstr, newlib has it (see target/SITL/target.h) +char *strnstr(const char *s, const char *find, size_t slen); + diff --git a/src/test/unit/telemetry_crsf_unittest.cc b/src/test/unit/telemetry_crsf_unittest.cc new file mode 100644 index 00000000000..af50bb991e9 --- /dev/null +++ b/src/test/unit/telemetry_crsf_unittest.cc @@ -0,0 +1,208 @@ +/* + * This file is part of INAV. + * + * INAV is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * INAV is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with INAV. If not, see . + */ + +#include +#include + +#include "gtest/gtest.h" +#include "unittest_macros.h" + +extern "C" { +#include "platform.h" + +#include "common/maths.h" +#include "common/printf.h" + +#include "config/feature.h" + +#include "fc/rc_modes.h" +#include "fc/runtime_config.h" + +#include "flight/imu.h" + +#include "io/gps.h" + +#include "navigation/navigation.h" + +#include "rx/crsf.h" + +#include "sensors/battery.h" +#include "sensors/pitotmeter.h" +#include "sensors/sensors.h" +#include "sensors/temperature.h" + +#include "telemetry/crsf.h" +#include "telemetry/msp_shared.h" +#include "telemetry/telemetry.h" + +gpsSolutionData_t gpsSol; +telemetryConfig_t telemetryConfig_System; +navConfig_t navConfig_System; +attitudeEulerAngles_t attitude; +uint32_t armingFlags; +uint32_t stateFlags; +uint32_t flightModeFlags; + +static float estimatedAltitudeCm; + +// only the GPS frame is under test, the other frames and the receiver link just need to link +float getEstimatedActualPosition(int axis) { return axis == Z ? estimatedAltitudeCm : 0; } +float getEstimatedActualVelocity(int axis) { UNUSED(axis); return 0; } +bool feature(uint32_t mask) { UNUSED(mask); return false; } +bool sensors(uint32_t mask) { UNUSED(mask); return false; } +bool IS_RC_MODE_ACTIVE(boxId_e boxId) { UNUSED(boxId); return false; } +bool navigationRequiresAngleMode(void) { return false; } +bool isWaypointMissionRTHActive(void) { return false; } +int32_t constrain(int32_t amt, int32_t low, int32_t high) { return amt < low ? low : (amt > high ? high : amt); } +uint8_t calculateBatteryPercentage(void) { return 0; } +int16_t getAmperage(void) { return 0; } +int32_t getMAhDrawn(void) { return 0; } +uint16_t getBatteryVoltage(void) { return 0; } +uint16_t getBatteryAverageCellVoltage(void) { return 0; } +float getAirspeedEstimate(void) { return 0; } +bool getSensorTemperature(uint8_t sensorIndex, int16_t *temperature) +{ + UNUSED(sensorIndex); + UNUSED(temperature); + return false; +} +bool crsfRxIsActive(void) { return true; } +bool crsfRxIsTelemetryBufEmpty(void) { return true; } +void crsfRxSendTelemetryData(void) {} +void crsfRxWriteTelemetryData(const void *data, int len) { UNUSED(data); UNUSED(len); } +bool handleMspFrame(uint8_t *frameStart, int payloadLength) +{ + UNUSED(frameStart); + UNUSED(payloadLength); + return false; +} +bool sendMspReply(uint8_t payloadSize, mspResponseFnPtr responseFn) +{ + UNUSED(payloadSize); + UNUSED(responseFn); + return false; +} +int tfp_sprintf(char *s, const char *fmt, ...) { UNUSED(fmt); s[0] = 0; return 0; } +} + +#define GPS_FRAME_SIZE 19 // sync, length, type, 15 byte payload, CRC +#define GPS_FRAME_TAIL_SIZE 5 + +static uint8_t crc8DvbS2(const uint8_t *data, int len) +{ + uint8_t crc = 0; + for (int i = 0; i < len; i++) { + crc ^= data[i]; + for (int bit = 0; bit < 8; bit++) { + crc = (crc & 0x80) ? (uint8_t)((crc << 1) ^ 0xD5) : (uint8_t)(crc << 1); + } + } + return crc; +} + +static uint32_t readU32BigEndian(const uint8_t *p) +{ + return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3]; +} + +class TelemetryCrsfGpsTest : public ::testing::Test { +protected: + uint8_t frame[CRSF_FRAME_SIZE_MAX]; + + void SetUp() override + { + memset(frame, 0, sizeof(frame)); + memset(&telemetryConfig_System, 0, sizeof(telemetryConfig_System)); + memset(&gpsSol, 0, sizeof(gpsSol)); + gpsSol.llh.lat = 470000000; + gpsSol.llh.lon = 80000000; + gpsSol.llh.alt = 50000; // cm + gpsSol.groundSpeed = 500; // cm/s + gpsSol.groundCourse = 900; // decidegrees + gpsSol.numSat = 12; + gpsSol.fixType = GPS_FIX_3D; + gpsSol.time.year = 2026; + gpsSol.flags.validFixTime = true; + gpsSol.fixTimeOfDayMs = 0x01A2B3C4; + } + + void expectStandardPart(void) + { + EXPECT_EQ(CRSF_TELEMETRY_SYNC_BYTE, frame[0]); + EXPECT_EQ(CRSF_FRAMETYPE_GPS, frame[2]); + EXPECT_EQ(470000000u, readU32BigEndian(&frame[3])); + EXPECT_EQ(80000000u, readU32BigEndian(&frame[7])); + EXPECT_EQ(180, (frame[11] << 8) | frame[12]); // km/h * 10 + EXPECT_EQ(9000, (frame[13] << 8) | frame[14]); // centidegrees + EXPECT_EQ(1500, (frame[15] << 8) | frame[16]); // m + 1000 + EXPECT_EQ(12, frame[17]); + } + + void expectCrcOverTypeAndPayload(int frameSize) + { + // CRC covers type and payload: everything after sync and length, before the CRC byte + EXPECT_EQ(crc8DvbS2(&frame[2], frameSize - 3), frame[frameSize - 1]); + } +}; + +TEST_F(TelemetryCrsfGpsTest, TailWithExactFixTime) +{ + const int frameSize = getCrsfFrame(frame, CRSF_FRAMETYPE_GPS); + + EXPECT_EQ(GPS_FRAME_SIZE + GPS_FRAME_TAIL_SIZE, frameSize); + EXPECT_EQ(22, frame[1]); + expectStandardPart(); + EXPECT_EQ(0x01, frame[18]); // big-endian time of day + EXPECT_EQ(0xA2, frame[19]); + EXPECT_EQ(0xB3, frame[20]); + EXPECT_EQ(0xC4, frame[21]); + EXPECT_EQ(GPS_FIX_3D, frame[22]); + expectCrcOverTypeAndPayload(frameSize); +} + +TEST_F(TelemetryCrsfGpsTest, NoTailWithoutExactFixTime) +{ + gpsSol.flags.validFixTime = false; + const int frameSize = getCrsfFrame(frame, CRSF_FRAMETYPE_GPS); + + EXPECT_EQ(GPS_FRAME_SIZE, frameSize); + EXPECT_EQ(17, frame[1]); + expectStandardPart(); + expectCrcOverTypeAndPayload(frameSize); +} + +TEST_F(TelemetryCrsfGpsTest, NoTailWithoutYear) +{ + gpsSol.time.year = 0; + const int frameSize = getCrsfFrame(frame, CRSF_FRAMETYPE_GPS); + + EXPECT_EQ(GPS_FRAME_SIZE, frameSize); + EXPECT_EQ(17, frame[1]); +} + +TEST_F(TelemetryCrsfGpsTest, NoTailWithLegacyBaroPacket) +{ + // the altitude field then carries the current estimate, not the altitude of this fix + telemetryConfig_System.crsf_use_legacy_baro_packet = true; + estimatedAltitudeCm = 12300; + const int frameSize = getCrsfFrame(frame, CRSF_FRAMETYPE_GPS); + + EXPECT_EQ(GPS_FRAME_SIZE, frameSize); + EXPECT_EQ(17, frame[1]); + EXPECT_EQ(1123, (frame[15] << 8) | frame[16]); + expectCrcOverTypeAndPayload(frameSize); +}