diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 0031b9d9..31fb25bf 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -576,7 +576,7 @@ void MyMesh::onContactOverwrite(const uint8_t* pub_key) { ContactInfo* contact = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); if (contact) scheduleContactWriteAfterRelease(*contact); _store->deleteBlobByKey(pub_key, PUB_KEY_SIZE); // delete from storage - if (_serial->isConnected()) { + if (_serial != NULL && _serial->isConnected()) { out_frame[0] = PUSH_CODE_CONTACT_DELETED; memcpy(&out_frame[1], pub_key, PUB_KEY_SIZE); _serial->writeFrame(out_frame, 1 + PUB_KEY_SIZE); @@ -584,7 +584,7 @@ void MyMesh::onContactOverwrite(const uint8_t* pub_key) { } void MyMesh::onContactsFull() { - if (_serial->isConnected()) { + if (_serial != NULL && _serial->isConnected()) { out_frame[0] = PUSH_CODE_CONTACTS_FULL; _serial->writeFrame(out_frame, 1); } diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index e27a112c..98776f44 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -784,6 +784,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i uint8_t temp[166]; char *command = (char *) &data[5]; char *reply = (char *) &temp[5]; + *reply = 0; handleCommand(sender_timestamp, command, reply, i, packet->getPathHashSize()); diff --git a/examples/simple_sensor/main.cpp b/examples/simple_sensor/main.cpp index e8353d7d..9d9ed7d3 100644 --- a/examples/simple_sensor/main.cpp +++ b/examples/simple_sensor/main.cpp @@ -165,6 +165,7 @@ void loop() { if (len > 0 && command[len - 1] == '\r') { // received complete line command[len - 1] = 0; // replace newline with C string null terminator char reply[160]; + reply[0] = 0; the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial! if (reply[0]) { Serial.print(" -> "); Serial.println(reply); diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 8fb33461..26c97695 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -261,6 +261,11 @@ static bool configKeyMatches(const char* config, const char* key) { && (config[key_len] == 0 || config[key_len] == ' ' || config[key_len] == '.'); } +static bool configKeyEquals(const char* config, const char* key) { + return mesh::cli::matchNoArgCommand(config, key) + == mesh::cli::NoArgCommandMatch::Exact; +} + static bool isAdvancedRetryConfig(const char* config) { return configKeyMatches(config, "direct.retry.heard") || configKeyMatches(config, "direct.retry.margin") @@ -3594,16 +3599,11 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep savePrefs(); sprintf(reply, "OK - powersaving %s", enabled ? "on" : "off"); } else if (memcmp(config, "radio ", 6) == 0) { - strcpy(tmp, &config[6]); - const char *parts[4]; - int num = mesh::Utils::parseTextParts(tmp, parts, 4); float freq = 0.0f; float bw = 0.0f; - uint8_t sf = num > 2 ? atoi(parts[2]) : 0; - uint8_t cr = num > 3 ? atoi(parts[3]) : 0; - if (num == 4 - && mesh::cli::parseDecimalStrict(parts[0], freq) - && mesh::cli::parseDecimalStrict(parts[1], bw) + uint8_t sf = 0; + uint8_t cr = 0; + if (mesh::cli::parseRadioTupleStrict(&config[6], freq, bw, sf, cr) && freq >= 150.0f && freq <= 2500.0f && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && isValidLoRaBandwidth(bw)) { @@ -4315,7 +4315,7 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep mesh::cli::RecentRepeaterGetQuery recent_query; const mesh::cli::RecentRepeaterGetMatch recent_query_match = mesh::cli::parseRecentRepeaterGet(config, recent_query); - if (memcmp(config, "dutycycle", 9) == 0) { + if (configKeyEquals(config, "dutycycle")) { float dc = 100.0f / (_prefs->airtime_factor + 1.0f); int dc_int = (int)dc; int dc_frac = (int)((dc - dc_int) * 10.0f + 0.5f); @@ -4326,48 +4326,48 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep #else strcpy(reply, "Error: unsupported on this platform"); #endif - } else if (memcmp(config, "af", 2) == 0) { + } else if (configKeyEquals(config, "af")) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->airtime_factor)); - } else if (memcmp(config, "int.thresh", 10) == 0) { + } else if (configKeyEquals(config, "int.thresh")) { sprintf(reply, "> %d", (uint32_t) _prefs->interference_threshold); - } else if (memcmp(config, "cad", 3) == 0) { + } else if (configKeyEquals(config, "cad")) { sprintf(reply, "> %s. # channel busy: %u", _prefs->cad_enabled ? "on" : "off", _board->n_cad_busy); - } else if (memcmp(config, "agc.reset.interval", 18) == 0) { + } else if (configKeyEquals(config, "agc.reset.interval")) { sprintf(reply, "> %d", ((uint32_t) _prefs->agc_reset_interval) * 4); - } else if (memcmp(config, "multi.acks", 10) == 0) { + } else if (configKeyEquals(config, "multi.acks")) { sprintf(reply, "> %d", (uint32_t) _prefs->multi_acks); - } else if (memcmp(config, "allow.read.only", 15) == 0) { + } else if (configKeyEquals(config, "allow.read.only")) { sprintf(reply, "> %s", _prefs->allow_read_only ? "on" : "off"); - } else if (memcmp(config, "telemetry.access", 16) == 0) { + } else if (configKeyEquals(config, "telemetry.access")) { sprintf(reply, "> %s", _prefs->telemetry_access == TELEMETRY_ACCESS_ACL ? "acl" : "all"); - } else if (memcmp(config, "flood.advert.interval", 21) == 0) { + } else if (configKeyEquals(config, "flood.advert.interval")) { sprintf(reply, "> %d", ((uint32_t) _prefs->flood_advert_interval)); - } else if (memcmp(config, "advert.interval", 15) == 0) { + } else if (configKeyEquals(config, "advert.interval")) { sprintf(reply, "> %d", ((uint32_t) _prefs->advert_interval) * 2); - } else if (memcmp(config, "guest.password", 14) == 0) { + } else if (configKeyEquals(config, "guest.password")) { sprintf(reply, "> %s", _prefs->guest_password); - } else if (sender_timestamp == 0 && memcmp(config, "prv.key", 7) == 0) { // from serial command line only + } else if (sender_timestamp == 0 && configKeyEquals(config, "prv.key")) { // from serial command line only uint8_t prv_key[PRV_KEY_SIZE]; int len = _callbacks->getSelfId().writeTo(prv_key, PRV_KEY_SIZE); mesh::Utils::toHex(tmp, prv_key, len); sprintf(reply, "> %s", tmp); - } else if (memcmp(config, "name", 4) == 0) { + } else if (configKeyEquals(config, "name")) { sprintf(reply, "> %s", _prefs->node_name); - } else if (memcmp(config, "repeat", 6) == 0) { + } else if (configKeyEquals(config, "repeat")) { sprintf(reply, "> %s", _prefs->disable_fwd ? "off" : "on"); - } else if (memcmp(config, "lat", 3) == 0) { + } else if (configKeyEquals(config, "lat")) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lat)); - } else if (memcmp(config, "lon", 3) == 0) { + } else if (configKeyEquals(config, "lon")) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lon)); - } else if (memcmp(config, "radio.rxgain", 12) == 0) { + } else if (configKeyEquals(config, "radio.rxgain")) { sprintf(reply, "> %s", _prefs->rx_boosted_gain ? "on" : "off"); - } else if (memcmp(config, "radio.fem.rxgain", 16) == 0) { + } else if (configKeyEquals(config, "radio.fem.rxgain")) { if (!_board->canControlLoRaFemLna()) { strcpy(reply, "Error: unsupported"); } else { sprintf(reply, "> %s", _board->isLoRaFemLnaEnabled() ? "on" : "off"); } - } else if (memcmp(config, "radio.fem.txgain", 16) == 0) { + } else if (configKeyEquals(config, "radio.fem.txgain")) { if (!_board->canControlLoRaFemPaGain()) { strcpy(reply, "Error: unsupported"); } else { @@ -4391,22 +4391,22 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep (unsigned)_prefs->rx_ps_preamble, (unsigned long)_prefs->rx_ps_rx_us, (unsigned long)_prefs->rx_ps_sleep_us); - } else if (memcmp(config, "radio.rxps", 10) == 0) { // RX PowerSaving + } else if (configKeyEquals(config, "radio.rxps")) { // RX PowerSaving ensureRxPowerSavingDefaults(&_prefs->rx_ps_rx_us, &_prefs->rx_ps_sleep_us); sprintf(reply, "> %s,%lu,%lu", _prefs->rx_powersaving_enabled ? "on" : "off", (unsigned long)_prefs->rx_ps_rx_us, (unsigned long)_prefs->rx_ps_sleep_us); - } else if (memcmp(config, "rxps.wd", 7) == 0) { + } else if (configKeyEquals(config, "rxps.wd")) { uint32_t wd_soft, wd_hard; _callbacks->getRxPsWatchdogCounts(&wd_soft, &wd_hard); sprintf(reply, "> soft=%lu,hard=%lu", (unsigned long)wd_soft, (unsigned long)wd_hard); - } else if (memcmp(config, "radio", 5) == 0) { + } else if (configKeyEquals(config, "radio")) { char freq[16], bw[16]; strcpy(freq, StrHelper::ftoa(_prefs->freq)); strcpy(bw, StrHelper::ftoa3(_prefs->bw)); sprintf(reply, "> %s,%s,%d,%d", freq, bw, (uint32_t)_prefs->sf, (uint32_t)_prefs->cr); - } else if (memcmp(config, "rxdelay", 7) == 0) { + } else if (configKeyEquals(config, "rxdelay")) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->rx_delay_base)); - } else if (memcmp(config, "txdelay", 7) == 0) { + } else if (configKeyEquals(config, "txdelay")) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->tx_delay_factor)); } else if (strcmp(config, "flood.channel.data.hops") == 0) { char hops[8]; @@ -4416,59 +4416,60 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep char hops[8]; formatFloodChannelHops(hops, _prefs->flood_channel_data_max_hops); sprintf(reply, "> %s %s", _prefs->flood_channel_data_enabled ? "on" : "off", hops); - } else if (memcmp(config, "flood.max.advert", 16) == 0) { + } else if (configKeyEquals(config, "flood.max.advert")) { sprintf(reply, "> %d", (uint32_t)_prefs->flood_max_advert); - } else if (memcmp(config, "flood.max.unscoped", 18) == 0) { + } else if (configKeyEquals(config, "flood.max.unscoped")) { sprintf(reply, "> %d", (uint32_t)_prefs->flood_max_unscoped); - } else if (memcmp(config, "flood.max", 9) == 0) { + } else if (configKeyEquals(config, "flood.max")) { sprintf(reply, "> %d", (uint32_t)_prefs->flood_max); - } else if (memcmp(config, "direct.txdelay", 14) == 0) { + } else if (configKeyEquals(config, "direct.txdelay")) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->direct_tx_delay_factor)); - } else if (memcmp(config, "retry.preset", 12) == 0) { + } else if (configKeyEquals(config, "retry.preset")) { sprintf(reply, "> %s", retryPresetName(_prefs->retry_preset)); - } else if (memcmp(config, "direct.retry", 12) == 0 && (config[12] == 0 || config[12] == ' ')) { + } else if (configKeyEquals(config, "direct.retry")) { sprintf(reply, "> %s", _prefs->direct_retry_enabled ? "on" : "off"); - } else if (memcmp(config, "direct.retry.heard", 18) == 0) { + } else if (configKeyEquals(config, "direct.retry.heard")) { sprintf(reply, "> %s", _prefs->direct_retry_recent_enabled ? "on" : "off"); - } else if (memcmp(config, "direct.retry.margin", 19) == 0) { + } else if (configKeyEquals(config, "direct.retry.margin")) { char margin[12]; formatSnrDbX4(margin, sizeof(margin), _prefs->direct_retry_snr_margin_x4); sprintf(reply, "> %s", margin); - } else if (memcmp(config, "direct.retry.count", 18) == 0) { + } else if (configKeyEquals(config, "direct.retry.count")) { sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_attempts); - } else if (memcmp(config, "direct.retry.base", 17) == 0) { + } else if (configKeyEquals(config, "direct.retry.base")) { sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_base_ms); - } else if (memcmp(config, "direct.retry.step", 17) == 0) { + } else if (configKeyEquals(config, "direct.retry.step")) { sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_step_ms); - } else if (memcmp(config, "flood.retry.count", 17) == 0) { + } else if (configKeyEquals(config, "flood.retry.count")) { sprintf(reply, "> %d", (uint32_t)_prefs->flood_retry_attempts); - } else if (memcmp(config, "flood.retry.path", 16) == 0) { + } else if (configKeyEquals(config, "flood.retry.path")) { char path_gate[8]; formatFloodRetryPathGate(path_gate, _prefs->flood_retry_max_path); sprintf(reply, "> %s", path_gate); - } else if (memcmp(config, "flood.retry.group.path", 22) == 0) { + } else if (configKeyEquals(config, "flood.retry.group.path")) { char path_gate[8]; formatFloodRetryPathGate(path_gate, _prefs->flood_retry_group_max_path); sprintf(reply, "> %s", path_gate); - } else if (memcmp(config, "flood.retry.prefixes", 20) == 0) { + } else if (configKeyEquals(config, "flood.retry.prefixes")) { formatFloodRetryPrefixList(tmp, _prefs->flood_retry_prefixes, FLOOD_RETRY_PREFIX_SLOTS); sprintf(reply, "> %s", tmp[0] ? tmp : "none"); - } else if (memcmp(config, "flood.retry.ignore", 18) == 0) { + } else if (configKeyEquals(config, "flood.retry.ignore")) { formatFloodRetryPrefixList(tmp, _prefs->flood_retry_ignore_prefixes, FLOOD_RETRY_IGNORE_PREFIXES); sprintf(reply, "> %s", tmp[0] ? tmp : "none"); - } else if (memcmp(config, "flood.retry.advert", 18) == 0) { + } else if (configKeyEquals(config, "flood.retry.advert")) { sprintf(reply, "> %s", _prefs->flood_retry_advert_enabled ? "on" : "off"); - } else if (memcmp(config, "flood.retry.bridge", 18) == 0) { + } else if (configKeyEquals(config, "flood.retry.bridge")) { sprintf(reply, "> %s", _prefs->flood_retry_bridge_enabled ? "on" : "off"); } else if (memcmp(config, "flood.retry.bucket.", 19) == 0) { - uint8_t bucket = atoi(&config[19]); - if (bucket >= 1 && bucket <= FLOOD_RETRY_BRIDGE_BUCKETS) { + int32_t bucket = 0; + if (mesh::cli::parseIntegerStrict(&config[19], bucket) + && bucket >= 1 && bucket <= FLOOD_RETRY_BRIDGE_BUCKETS) { formatFloodRetryPrefixList(tmp, _prefs->flood_retry_bridge_buckets[bucket - 1], FLOOD_RETRY_BUCKET_PREFIXES); sprintf(reply, "> %s", tmp[0] ? tmp : "none"); } else { sprintf(reply, "Error, bucket 1-%d", FLOOD_RETRY_BRIDGE_BUCKETS); } - } else if (memcmp(config, "direct.retry.cr", 15) == 0) { + } else if (configKeyEquals(config, "direct.retry.cr")) { if (!_prefs->direct_retry_cr_enabled) { strcpy(reply, "> off"); } else if (!_callbacks->supportsAdvancedRetryConfig()) { @@ -4488,7 +4489,7 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep recent_query.search_prefix_len); } else if (recent_query_match == mesh::cli::RecentRepeaterGetMatch::Invalid) { strcpy(reply, "Error, use: get recent.repeaters [page] | search <2|4|6 hex> [page]"); - } else if (memcmp(config, "owner.info", 10) == 0) { + } else if (configKeyEquals(config, "owner.info")) { *reply++ = '>'; *reply++ = ' '; const char* sp = _prefs->owner_info; @@ -4497,9 +4498,9 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep sp++; } *reply = 0; // set null terminator - } else if (memcmp(config, "path.hash.mode", 14) == 0) { + } else if (configKeyEquals(config, "path.hash.mode")) { sprintf(reply, "> %d", (uint32_t)_prefs->path_hash_mode); - } else if (memcmp(config, "loop.detect", 11) == 0) { + } else if (configKeyEquals(config, "loop.detect")) { if (_prefs->loop_detect == LOOP_DETECT_OFF) { strcpy(reply, "> off"); } else if (_prefs->loop_detect == LOOP_DETECT_MINIMAL) { @@ -4509,16 +4510,16 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep } else { strcpy(reply, "> strict"); } - } else if (memcmp(config, "tx", 2) == 0 && (config[2] == 0 || config[2] == ' ')) { + } else if (configKeyEquals(config, "tx")) { sprintf(reply, "> %d", (int32_t) _prefs->tx_power_dbm); - } else if (memcmp(config, "freq", 4) == 0) { + } else if (configKeyEquals(config, "freq")) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->freq)); - } else if (memcmp(config, "public.key", 10) == 0) { + } else if (configKeyEquals(config, "public.key")) { strcpy(reply, "> "); mesh::Utils::toHex(&reply[2], _callbacks->getSelfId().pub_key, PUB_KEY_SIZE); - } else if (memcmp(config, "role", 4) == 0) { + } else if (configKeyEquals(config, "role")) { sprintf(reply, "> %s", _callbacks->getRole()); - } else if (memcmp(config, "bridge.type", 11) == 0) { + } else if (configKeyEquals(config, "bridge.type")) { sprintf(reply, "> %s", #ifdef WITH_RS232_BRIDGE "rs232" @@ -4531,26 +4532,26 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep #endif ); #ifdef WITH_BRIDGE - } else if (memcmp(config, "bridge.enabled", 14) == 0) { + } else if (configKeyEquals(config, "bridge.enabled")) { sprintf(reply, "> %s", _prefs->bridge_enabled ? "on" : "off"); - } else if (memcmp(config, "bridge.delay", 12) == 0) { + } else if (configKeyEquals(config, "bridge.delay")) { sprintf(reply, "> %d", (uint32_t)_prefs->bridge_delay); - } else if (memcmp(config, "bridge.source", 13) == 0) { + } else if (configKeyEquals(config, "bridge.source")) { sprintf(reply, "> %s", _prefs->bridge_pkt_src ? "logRx" : "logTx"); #endif #ifdef WITH_RS232_BRIDGE - } else if (memcmp(config, "bridge.baud", 11) == 0) { + } else if (configKeyEquals(config, "bridge.baud")) { sprintf(reply, "> %d", (uint32_t)_prefs->bridge_baud); - } else if (memcmp(config, "bridge.uart", 11) == 0) { + } else if (configKeyEquals(config, "bridge.uart")) { sprintf(reply, "> %d", (uint32_t)_prefs->bridge_uart); #endif #ifdef WITH_ESPNOW_BRIDGE - } else if (memcmp(config, "bridge.channel", 14) == 0) { + } else if (configKeyEquals(config, "bridge.channel")) { sprintf(reply, "> %d", (uint32_t)_prefs->bridge_channel); - } else if (memcmp(config, "bridge.secret", 13) == 0) { + } else if (configKeyEquals(config, "bridge.secret")) { sprintf(reply, "> %s", _prefs->bridge_secret); #endif - } else if (memcmp(config, "bootloader.ver", 14) == 0) { + } else if (configKeyEquals(config, "bootloader.ver")) { #ifdef NRF52_PLATFORM char ver[32]; if (_board->getBootloaderVersion(ver, sizeof(ver))) { @@ -4561,7 +4562,7 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep #else strcpy(reply, "Error: unsupported"); #endif - } else if (memcmp(config, "adc.multiplier", 14) == 0) { + } else if (configKeyEquals(config, "adc.multiplier")) { float adc_mult = _board->getAdcMultiplier(); if (adc_mult == 0.0f) { strcpy(reply, "Error: unsupported"); @@ -4569,29 +4570,29 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep sprintf(reply, "> %.3f", adc_mult); } // Power management commands - } else if (memcmp(config, "pwrmgt.support", 14) == 0) { + } else if (configKeyEquals(config, "pwrmgt.support")) { #ifdef NRF52_POWER_MANAGEMENT strcpy(reply, "> supported"); #else strcpy(reply, "> unsupported"); #endif - } else if (memcmp(config, "pwrmgt.source", 13) == 0) { + } else if (configKeyEquals(config, "pwrmgt.source")) { #ifdef NRF52_POWER_MANAGEMENT strcpy(reply, _board->isExternalPowered() ? "> external" : "> battery"); #else strcpy(reply, "ERROR: Power management not supported"); #endif - } else if (memcmp(config, "pwrmgt.bootreason", 17) == 0) { + } else if (configKeyEquals(config, "pwrmgt.bootreason")) { sprintf(reply, "> Reset: %s; Shutdown: %s", _board->getResetReasonString(_board->getResetReason()), _board->getShutdownReasonString(_board->getShutdownReason())); - } else if (memcmp(config, "pwrmgt.bootmv", 13) == 0) { + } else if (configKeyEquals(config, "pwrmgt.bootmv")) { #ifdef NRF52_POWER_MANAGEMENT sprintf(reply, "> %u mV", _board->getBootVoltage()); #else strcpy(reply, "ERROR: Power management not supported"); #endif - } else if (memcmp(config, "reboot.interval", 15) == 0) { + } else if (configKeyEquals(config, "reboot.interval")) { if (_prefs->reboot_interval == 0) { strcpy(reply, "disabled"); } else { diff --git a/src/helpers/sensors/GpsTimeValidation.h b/src/helpers/sensors/GpsTimeValidation.h new file mode 100644 index 00000000..4c599666 --- /dev/null +++ b/src/helpers/sensors/GpsTimeValidation.h @@ -0,0 +1,44 @@ +#pragma once + +#include + +namespace mesh { +namespace gps { + +inline bool isLeapYear(uint16_t year) { + return (year % 4U) == 0U + && ((year % 100U) != 0U || (year % 400U) == 0U); +} + +inline uint8_t daysInMonth(uint16_t year, uint8_t month) { + static const uint8_t days[] = { + 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, + }; + if (month < 1 || month > 12) return 0; + if (month == 2 && isLeapYear(year)) return 29; + return days[month - 1]; +} + +// Reject the zero/partial dates emitted before an NMEA receiver has acquired +// UTC. The upper bound also keeps LocationProvider's signed-long timestamp +// valid on 32-bit targets. +inline bool isValidNmeaDateTime(uint16_t year, uint8_t month, uint8_t day, + uint8_t hour, uint8_t minute, + uint8_t second) { + if (year < 2020 || year > 2038 + || month < 1 || month > 12 + || day < 1 || day > daysInMonth(year, month) + || hour > 23 || minute > 59 || second > 59) { + return false; + } + if (year < 2038) return true; + + // 2,147,483,647 is 2038-01-19 03:14:07 UTC. + if (month != 1 || day > 19) return false; + if (day < 19 || hour < 3) return true; + if (hour > 3 || minute > 14) return false; + return minute < 14 || second <= 7; +} + +} // namespace gps +} // namespace mesh diff --git a/src/helpers/sensors/MicroNMEALocationProvider.h b/src/helpers/sensors/MicroNMEALocationProvider.h index 8d882727..45882463 100644 --- a/src/helpers/sensors/MicroNMEALocationProvider.h +++ b/src/helpers/sensors/MicroNMEALocationProvider.h @@ -4,6 +4,11 @@ #include #include #include +#include + +#ifndef GPS_LOOP_MAX_BYTES + #define GPS_LOOP_MAX_BYTES 96 +#endif #ifndef GPS_EN #ifdef PIN_GPS_EN @@ -133,9 +138,20 @@ public : long satellitesCount() override { return nmea.getNumSatellites(); } bool isValid() override { return nmea.isValid(); } - long getTimestamp() override { - DateTime dt(nmea.getYear(), nmea.getMonth(),nmea.getDay(),nmea.getHour(),nmea.getMinute(),nmea.getSecond()); - return dt.unixtime(); + long getTimestamp() override { + const uint16_t year = nmea.getYear(); + const uint8_t month = nmea.getMonth(); + const uint8_t day = nmea.getDay(); + const uint8_t hour = nmea.getHour(); + const uint8_t minute = nmea.getMinute(); + const uint8_t second = nmea.getSecond(); + if (!mesh::gps::isValidNmeaDateTime(year, month, day, hour, minute, + second)) { + return 0; + } + DateTime dt(year, month, day, hour, minute, second); + const uint32_t timestamp = dt.unixtime(); + return timestamp <= 0x7FFFFFFFUL ? static_cast(timestamp) : 0; } void sendSentence(const char *sentence) override { @@ -144,12 +160,14 @@ public : void loop() override { - while (_gps_serial->available()) { + size_t processed = 0; + while (_gps_serial->available() && processed < GPS_LOOP_MAX_BYTES) { char c = _gps_serial->read(); #ifdef GPS_NMEA_DEBUG if (mesh::isUsbLoggingEnabled()) mesh::usbLoggingPort().print(c); #endif nmea.process(c); + processed++; } if (!isValid()) time_valid = 0; @@ -160,18 +178,21 @@ public : if (!_time_sync_needed && _clock != NULL && (millis() - _last_time_sync) > TIME_SYNC_INTERVAL) { _time_sync_needed = true; } + const long timestamp = getTimestamp(); + if (isValid() && satellitesCount() >= 5 && timestamp > 0) { + time_valid++; + } else { + time_valid = 0; + } if (_time_sync_needed && time_valid > 2) { if (_clock != NULL) { - _clock->setCurrentTime(getTimestamp()); + _clock->setCurrentTime(timestamp); markTimeSyncApplied(); _time_sync_needed = false; _last_time_sync = millis(); _last_valid_time_sync = _clock->getCurrentTime(); } } - if (isValid() && satellitesCount() >= 5) { - time_valid ++; - } } } }; diff --git a/test/README.md b/test/README.md index 3f2ac5ee..cd0c5c87 100644 --- a/test/README.md +++ b/test/README.md @@ -45,6 +45,7 @@ does not reflect the GoogleTest count -- run the built binary directly | `test_flood_filter_policy` | `src/helpers/FloodFilterPolicy.h` | unordered blacklist matching; ordered 1/2/3-byte pbyte rule prefixes; original incoming scope classes and canonical region-name identity; channel-authentication cache key comparison; priority ordering and terminal stop masks; bridge-bucket and regionless channel-target selector encoding; `require=region` and per-channel scope-gate truth tables; fast/slow timing; adding, replacing, and preserving packet scope | | `test_logical_message_cache` | `src/helpers/LogicalMessageCache.h` | bounded logical-message mapping; stable retry timestamps; exact older retries after newer messages; stale and same-timestamp mismatch rejection | | `test_cli_command_utils` | `src/helpers/CLICommandUtils.h`, `src/helpers/ContactListOrder.h`, `src/helpers/TerminalCommandTracker.h`, `src/helpers/TerminalDisplayFilter.h` | terminal verb/argument/path parsing; routed receive labels; quiet display defaults and independent emergency filtering; favorite-first contact ordering; single-command reply matching, round-trip timing, and rollover-safe expiration | +| `test_gps_time_validation` | `src/helpers/sensors/GpsTimeValidation.h` | complete NMEA UTC validation; leap years and calendar bounds; rejection of receiver-default and signed-32-bit-out-of-range dates | | `test_identity_generation` | `src/helpers/IdentityGeneration.h` | reserved-prefix rejection; bounded retries; final provisioned attempt; fail-closed exhaustion | | `test_remote_cli_reply_cache` | `src/helpers/RemoteCliReplyCache.h`, `src/helpers/RemoteCliRequest.h`, `src/helpers/RemoteCliTimeout.h` | authenticated logical-request matching; bounded recent-reply history; backward-compatible retry identity; 300% response timeout; empty-response completion; on-air truncation and clearing | | `test_companion_frame_queue` | `src/helpers/CompanionFrameQueue.h` | response/required/best-effort classification; reserved capacity; stable priority; safe eviction; message-waiting coalescing | diff --git a/test/test_gps_time_validation/test_gps_time_validation.cpp b/test/test_gps_time_validation/test_gps_time_validation.cpp new file mode 100644 index 00000000..65fc3916 --- /dev/null +++ b/test/test_gps_time_validation/test_gps_time_validation.cpp @@ -0,0 +1,33 @@ +#include + +#include + +TEST(GpsTimeValidation, AcceptsCompleteUtcDatesInSignedTimestampRange) { + EXPECT_TRUE(mesh::gps::isValidNmeaDateTime(2020, 1, 1, 0, 0, 0)); + EXPECT_TRUE(mesh::gps::isValidNmeaDateTime(2024, 2, 29, 23, 59, 59)); + EXPECT_TRUE(mesh::gps::isValidNmeaDateTime(2038, 1, 19, 3, 14, 7)); +} + +TEST(GpsTimeValidation, RejectsZeroPartialAndOutOfRangeDates) { + EXPECT_FALSE(mesh::gps::isValidNmeaDateTime(0, 0, 0, 0, 0, 0)); + EXPECT_FALSE(mesh::gps::isValidNmeaDateTime(2019, 12, 31, 23, 59, 59)); + EXPECT_FALSE(mesh::gps::isValidNmeaDateTime(2038, 1, 19, 3, 14, 8)); + EXPECT_FALSE(mesh::gps::isValidNmeaDateTime(2038, 1, 20, 0, 0, 0)); + EXPECT_FALSE(mesh::gps::isValidNmeaDateTime(2039, 1, 1, 0, 0, 0)); + EXPECT_FALSE(mesh::gps::isValidNmeaDateTime(2026, 13, 1, 0, 0, 0)); + EXPECT_FALSE(mesh::gps::isValidNmeaDateTime(2026, 1, 1, 24, 0, 0)); + EXPECT_FALSE(mesh::gps::isValidNmeaDateTime(2026, 1, 1, 0, 60, 0)); + EXPECT_FALSE(mesh::gps::isValidNmeaDateTime(2026, 1, 1, 0, 0, 60)); +} + +TEST(GpsTimeValidation, EnforcesCalendarDayBounds) { + EXPECT_FALSE(mesh::gps::isValidNmeaDateTime(2023, 2, 29, 12, 0, 0)); + EXPECT_TRUE(mesh::gps::isValidNmeaDateTime(2024, 2, 29, 12, 0, 0)); + EXPECT_FALSE(mesh::gps::isValidNmeaDateTime(2026, 4, 31, 12, 0, 0)); + EXPECT_TRUE(mesh::gps::isValidNmeaDateTime(2026, 4, 30, 12, 0, 0)); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/variants/heltec_rc32/platformio.ini b/variants/heltec_rc32/platformio.ini index 2777ca94..a8081e5c 100644 --- a/variants/heltec_rc32/platformio.ini +++ b/variants/heltec_rc32/platformio.ini @@ -6,6 +6,8 @@ platform_packages = framework-arduinoespressif32-libs @ https://github.com/espressif/arduino-esp32/releases/download/3.3.11/esp32-core-3.3.11-libs.tar.xz board = heltec-rc32 board_build.partitions = default_16MB.csv +monitor_rts = 0 +monitor_dtr = 0 build_flags = ${esp32_base.build_flags} ${sensor_base.build_flags} @@ -45,15 +47,9 @@ build_flags = -D PIN_TFT_BL=5 -D PIN_TFT_BL_ACTIVE=HIGH -D SPI_FREQUENCY=8000000 - -D PIN_GPS_TX=44 - -D PIN_GPS_RX=43 - -D PIN_GPS_EN=45 - -D PIN_GPS_EN_ACTIVE=HIGH - -D PIN_GPS_RESET=40 - -D PIN_GPS_RESET_ACTIVE=LOW - -D PIN_GPS_PPS=41 - -D GPS_BAUD_RATE=9600 - -D ENV_INCLUDE_GPS=1 + ; Heltec's RC32 board definition has no onboard GPS. In particular, GPIO45 + ; is an ESP32-S3 strapping pin and must not be driven as a guessed GPS enable. + -UENV_INCLUDE_GPS -D PIN_ADC_CTRL=15 -D PIN_VBAT_READ=7 -D ADC_CTRL_ENABLED=HIGH