From 577caba6354ee8341e732bf2916f820679f36e7f Mon Sep 17 00:00:00 2001 From: John Poole Date: Mon, 25 May 2026 14:56:23 -0700 Subject: [PATCH] After time changes, but before testing trial #2 --- exercises/26_Bluetooth_discover/.gitignore | 2 + .../Codex_2_timing_prompt.md | 24 ++++ exercises/26_Bluetooth_discover/README.md | 46 ++++--- .../scripts/create_exercise_26_ble_schema.sql | 34 +++++ .../scripts/import_exercise_26_ble_log.pl | 41 ++++-- exercises/26_Bluetooth_discover/src/main.cpp | 118 ++++++++++++++---- 6 files changed, 217 insertions(+), 48 deletions(-) create mode 100644 exercises/26_Bluetooth_discover/.gitignore create mode 100644 exercises/26_Bluetooth_discover/Codex_2_timing_prompt.md mode change 100644 => 100755 exercises/26_Bluetooth_discover/scripts/import_exercise_26_ble_log.pl diff --git a/exercises/26_Bluetooth_discover/.gitignore b/exercises/26_Bluetooth_discover/.gitignore new file mode 100644 index 0000000..47e0292 --- /dev/null +++ b/exercises/26_Bluetooth_discover/.gitignore @@ -0,0 +1,2 @@ +*.sqlite + diff --git a/exercises/26_Bluetooth_discover/Codex_2_timing_prompt.md b/exercises/26_Bluetooth_discover/Codex_2_timing_prompt.md new file mode 100644 index 0000000..703a725 --- /dev/null +++ b/exercises/26_Bluetooth_discover/Codex_2_timing_prompt.md @@ -0,0 +1,24 @@ +4. Clarification on runtime clock validity: + +Do not require continuous GPS visibility to keep the clock valid. Use a holdover model. + +At startup: +- Require GPS/PPS/RTC discipline before entering BLE survey mode. +- Set: + g_clockValid = true + g_lastDisciplineEpochMs = currentEpochMs() + g_lastDisciplineMillis = millis() + +During runtime: +- clock_valid remains true while: + millis() - g_lastDisciplineMillis <= kMaxDisciplineAgeMs +- gps_valid is evaluated separately from the latest GPS fix age: + rx_gps_age_ms <= kMaxGpsFixAgeMs + +If the unit later moves under trees or near buildings and loses satellite visibility: +- Do not immediately invalidate clock_valid. +- Let gps_valid become false when rx_gps_age_ms exceeds kMaxGpsFixAgeMs. +- Continue OLED BLE display if possible. +- Do not write normal observation rows unless both clock_valid and gps_valid are true, or else write rows with explicit validity fields so analysis can filter them. + +The important point is: a temporary GPS outage should stale the coordinates, not necessarily the disciplined clock. diff --git a/exercises/26_Bluetooth_discover/README.md b/exercises/26_Bluetooth_discover/README.md index 9fda38e..1d25217 100644 --- a/exercises/26_Bluetooth_discover/README.md +++ b/exercises/26_Bluetooth_discover/README.md @@ -26,12 +26,12 @@ pio device monitor -b 115200 -p /dev/ttytBOB ## Behavior -- Advertises manufacturer data in this format: `B2|NODE|seq|tx_epoch_ms`. +- Advertises manufacturer data in this format: `B2|NODE|seq|tx_payload_epoch_ms`. - Accepts current `B2` advertisements and legacy `TBMSND|1|NODE|seq|uptime` advertisements from known nodes in `AMY, BOB, CY, DAN, ED, FLO, GUY`. - Displays heard nodes sorted by rolling RSSI average, strongest first. - Drops stale entries after 20 seconds. -- Logs accepted advertisements to SD as `/logs/YYYYMMDD_HHMISS_NODE_ble_search.log`. -- Refreshes the receiver GPS position at 1 Hz and logs the last known receiver coordinates plus GPS fix age. +- Logs valid accepted advertisements to SD as `/logs/YYYYMMDD_HHMISS_NODE_ble_search.log`. +- Refreshes the receiver GPS position at 1 Hz and logs the last known receiver coordinates plus GPS fix age. Observation rows are written only while the holdover clock and GPS fix age are both valid. - Starts a WiFi AP and HTTP web service during boot. This does not wait for GPS. Default web addresses: @@ -85,7 +85,7 @@ jlpoole@jp ~/work/tbeam/logs $ ls -lath flo/*_16* jlpoole@jp ~/work/tbeam/logs $ ``` -Here are start and end samples from an earlier trial. These show the previous 12-column schema; current firmware uses the 14-column schema described below. +Here are start and end samples from an earlier trial. These show the previous 12-column schema; current firmware uses the 18-column schema described below. ```bash jlpoole@jp ~/work/tbeam/logs $ cat -n ed/20260525_162217_ED_ble_search.log|head -n 3 1 human_time,epoch_ms,receiver,lat,lon,heard,rssi,avg_rssi,age_s,count,seq,payload @@ -110,22 +110,26 @@ The header represents: | 3 | `receiver` | Unit that wrote the log row. | | 4 | `rx_lat` | Receiver GPS latitude from the latest valid local GPS fix. | | 5 | `rx_lon` | Receiver GPS longitude from the latest valid local GPS fix. | -| 6 | `rx_gps_age_ms` | Age of the receiver GPS fix in milliseconds when the row was written. If GPS becomes unavailable, the last known coordinates remain and this age grows. | -| 7 | `heard` | Remote unit heard in the BLE advertisement. | -| 8 | `rssi` | RSSI measured by the receiver for this advertisement. | -| 9 | `avg_rssi` | Receiver-calculated arithmetic mean of the most recent RSSI measurements for this heard unit, using up to the last 5 accepted advertisements and rounded to the nearest integer. The window size is `kRssiWindow = 5` in `main.cpp`. | -| 10 | `age_s` | Age in seconds of the displayed/heard entry. | -| 11 | `count` | Number of accepted advertisements from that heard unit. | -| 12 | `seq` | Sequence number advertised by the heard unit. | -| 13 | `tx_epoch_ms` | Sender timestamp from the BLE payload as Unix epoch milliseconds, when available. Legacy v1 payloads report `0` here. | -| 14 | `payload` | Raw BLE manufacturer-data string. See Payload Definition below. | +| 6 | `gps_fix_age_ms` | Age of the receiver GPS fix in milliseconds when the row was written. If GPS becomes unavailable, the last known coordinates remain and this age grows. | +| 7 | `clock_valid` | `1` when the startup-disciplined clock is still inside the configured holdover window. | +| 8 | `gps_valid` | `1` when the receiver GPS fix age is inside the configured freshness window. | +| 9 | `discipline_age_ms` | Milliseconds since the startup GPS/PPS/RTC discipline event. | +| 10 | `last_discipline_epoch_ms` | Epoch milliseconds assigned to the PPS edge used for startup discipline. | +| 11 | `heard` | Remote unit heard in the BLE advertisement. | +| 12 | `rssi` | RSSI measured by the receiver for this advertisement. | +| 13 | `avg_rssi` | Receiver-calculated arithmetic mean of the most recent RSSI measurements for this heard unit, using up to the last 5 accepted advertisements and rounded to the nearest integer. The window size is `kRssiWindow = 5` in `main.cpp`. | +| 14 | `age_s` | Age in seconds of the displayed/heard entry. | +| 15 | `count` | Number of accepted advertisements from that heard unit. | +| 16 | `seq` | Sequence number advertised by the heard unit. | +| 17 | `tx_payload_epoch_ms` | Sender payload generation timestamp from the BLE payload as Unix epoch milliseconds, when available. Legacy v1 payloads report `0` here. | +| 18 | `payload` | Raw BLE manufacturer-data string. See Payload Definition below. | ## Payload definition `payload` is the exact BLE manufacturer-data string received from the other unit. Current firmware advertises a compact v2 text payload: ```text -B2|NODE|SEQ|TX_EPOCH_MS +B2|NODE|SEQ|TX_PAYLOAD_EPOCH_MS ``` Example: @@ -141,7 +145,7 @@ Meaning: | `B2` | `B2` | Compact Exercise 26 payload prefix and version. | | `NODE` | `FLO` | Sending unit name. Receiver accepts only known units and ignores itself. | | `SEQ` | `0611` | Sender’s advertisement sequence number, zero-padded, wraps every 10,000 advertisements. | -| `TX_EPOCH_MS` | `1779727662217` | Sender timestamp as Unix epoch milliseconds, derived from the disciplined local clock. | +| `TX_PAYLOAD_EPOCH_MS` | `1779727662217` | Sender payload generation timestamp as Unix epoch milliseconds, derived from the disciplined local clock. This is not a per-advertisement RF transmit timestamp. | The receiver also accepts the legacy v1 payload used by earlier Exercise 26 firmware: @@ -155,4 +159,14 @@ Constraints currently enforced by the receiver: - Prefix must be current `B2` or legacy `TBMSND` version `1`. - Node must be one of `AMY, BOB, CY, DAN, ED, FLO, GUY`. - Node must not be the receiver’s own name. -- Current `B2` payloads provide `SEQ` and `TX_EPOCH_MS`; legacy v1 payloads provide `SEQ` only and log `tx_epoch_ms` as `0`. +- Current `B2` payloads provide `SEQ` and `TX_PAYLOAD_EPOCH_MS`; legacy v1 payloads provide `SEQ` only and log `tx_payload_epoch_ms` as `0`. + +## Clock Discipline and Timestamp Semantics + +BLE survey advertising starts only after startup GPS UTC, GPS coordinates, and PPS-backed RTC discipline have succeeded. Before that point the OLED remains on a blocking GPS/clock status page and the unit does not enter active BLE survey mode. + +Observation logging requires both `clock_valid=1` and `gps_valid=1`. The clock uses a holdover model: startup discipline sets `last_discipline_epoch_ms`, and `clock_valid` remains true until `discipline_age_ms` exceeds the configured holdover limit. Temporary GPS loss does not immediately invalidate the clock. GPS validity is tracked separately with `gps_fix_age_ms`; if the fix becomes stale, observation rows are suppressed until GPS freshness returns. + +`rx_epoch_ms` is the receiver's local disciplined-clock time at the moment it writes the observation row. `tx_payload_epoch_ms` is the sender's local disciplined-clock time when it generated and installed that BLE advertising payload. A single payload may be observed many times before the sender replaces it, so `tx_payload_epoch_ms` is useful for correlating logs but is not an RF transmit timestamp. + +`rx_epoch_ms - tx_payload_epoch_ms` is diagnostic for clock alignment, scheduling, payload age, and repeated-observation behavior. It is not BLE propagation delay. The status fields that prove a row was written under valid survey conditions are `clock_valid`, `gps_valid`, `gps_fix_age_ms`, `discipline_age_ms`, and `last_discipline_epoch_ms`. diff --git a/exercises/26_Bluetooth_discover/scripts/create_exercise_26_ble_schema.sql b/exercises/26_Bluetooth_discover/scripts/create_exercise_26_ble_schema.sql index 4d5b544..717441f 100644 --- a/exercises/26_Bluetooth_discover/scripts/create_exercise_26_ble_schema.sql +++ b/exercises/26_Bluetooth_discover/scripts/create_exercise_26_ble_schema.sql @@ -376,4 +376,38 @@ WHERE tx_epoch_ms IS NOT NULL AND tx_epoch_ms > 0 GROUP BY trial_id, receiver, heard; +-- +-- Additiona views created after initial import of 1st test set +-- and anomolies seen +-- +CREATE VIEW IF NOT EXISTS v_rx_tx_delta_by_seq AS +SELECT + trial_id, + receiver, + heard, + payload_seq, + COUNT(*) AS observations, + MIN(rx_tx_delta_ms) AS min_delta_ms, + AVG(rx_tx_delta_ms) AS avg_delta_ms, + MAX(rx_tx_delta_ms) AS max_delta_ms, + MIN(rx_epoch_ms) AS first_rx_epoch_ms, + MAX(rx_epoch_ms) AS last_rx_epoch_ms +FROM ble_observation +WHERE tx_epoch_ms IS NOT NULL + AND tx_epoch_ms > 0 +GROUP BY trial_id, receiver, heard, payload_seq; + +CREATE VIEW IF NOT EXISTS v_payload_seq_summary AS +SELECT + trial_id, + receiver, + heard, + MIN(payload_seq) AS min_payload_seq, + MAX(payload_seq) AS max_payload_seq, + COUNT(DISTINCT payload_seq) AS distinct_payload_seq, + COUNT(*) AS observations, + COUNT(*) - COUNT(DISTINCT payload_seq) AS repeated_seq_observations +FROM ble_observation +GROUP BY trial_id, receiver, heard; + COMMIT; diff --git a/exercises/26_Bluetooth_discover/scripts/import_exercise_26_ble_log.pl b/exercises/26_Bluetooth_discover/scripts/import_exercise_26_ble_log.pl old mode 100644 new mode 100755 index b6a9d7e..7d0feea --- a/exercises/26_Bluetooth_discover/scripts/import_exercise_26_ble_log.pl +++ b/exercises/26_Bluetooth_discover/scripts/import_exercise_26_ble_log.pl @@ -4,16 +4,19 @@ # # Example: # -# sqlite3 ble_fieldtest_20260525_1945_ed_flo.sqlite < create_exercise_26_ble_schema.sql -# -# ./import_exercise_26_ble_log.pl \ -# --db ble_fieldtest_20260525_1945_ed_flo.sqlite \ -# --log "$FLO_LOG" -# -# ./import_exercise_26_ble_log.pl \ -# --db ble_fieldtest_20260525_1945_ed_flo.sqlite \ -# --log "$ED_LOG" -# + +=pod + sqlite3 ble_fieldtest_20260525_1945_ed_flo.sqlite < create_exercise_26_ble_schema.sql + + ./import_exercise_26_ble_log.pl \ + --db ble_fieldtest_20260525_1945_ed_flo.sqlite \ + --log "$FLO_LOG" + + ./import_exercise_26_ble_log.pl \ + --db ble_fieldtest_20260525_1945_ed_flo.sqlite \ + --log "$ED_LOG" +=cut + # Manifest: # # The importer expects a manifest file with the same name as the log plus @@ -35,6 +38,24 @@ # receiver_notes=FLO was hand-carried north and across the street, then returned to base. # test_notes=Two-unit BLE discovery test. ED stationary, FLO moving. # + +=pod +An after import smoke test: + +sqlite3 -header -column ble_fieldtest_20260525_1945_ed_flo.sqlite \ + 'SELECT * FROM v_log_file_summary;' + +sqlite3 -header -column ble_fieldtest_20260525_1945_ed_flo.sqlite \ + 'SELECT * FROM v_receiver_gps_summary;' + +sqlite3 -header -column ble_fieldtest_20260525_1945_ed_flo.sqlite \ + 'SELECT * FROM v_link_rssi_summary;' + +sqlite3 -header -column ble_fieldtest_20260525_1945_ed_flo.sqlite \ + 'SELECT * FROM v_rx_tx_timing_summary;' + +=cut + # Purpose: # # Import Exercise 26 BLE Discovery logs into SQLite. diff --git a/exercises/26_Bluetooth_discover/src/main.cpp b/exercises/26_Bluetooth_discover/src/main.cpp index 04bc8dd..7e518d5 100644 --- a/exercises/26_Bluetooth_discover/src/main.cpp +++ b/exercises/26_Bluetooth_discover/src/main.cpp @@ -74,6 +74,8 @@ static constexpr uint32_t kScanWindowSeconds = 2; static constexpr uint32_t kLogFlushPeriodMs = 5000; static constexpr uint32_t kStartupStatusPeriodMs = 1000; static constexpr uint32_t kGpsRefreshPeriodMs = 1000; +static constexpr uint32_t kMaxGpsFixAgeMs = 5000; +static constexpr uint32_t kMaxDisciplineAgeMs = 4UL * 60UL * 60UL * 1000UL; static constexpr uint8_t kRssiWindow = 5; struct NodeState { @@ -89,7 +91,7 @@ struct NodeState { uint32_t seenCount = 0; uint32_t lastSeenMs = 0; uint32_t lastSeq = 0; - int64_t lastTxEpochMs = 0; + int64_t lastTxPayloadEpochMs = 0; }; XPowersLibInterface* g_pmu = nullptr; @@ -111,6 +113,7 @@ NodeState g_nodes[] = { }; bool g_disciplined = false; +bool g_clockValid = false; bool g_bleStarted = false; bool g_sdReady = false; bool g_logReady = false; @@ -127,6 +130,8 @@ uint32_t g_lastFlushMs = 0; uint32_t g_lastGpsRefreshMs = 0; int64_t g_epochBase = 0; uint32_t g_epochBaseMs = 0; +int64_t g_lastDisciplineEpochMs = 0; +uint32_t g_lastDisciplineMillis = 0; double g_latitude = 0.0; double g_longitude = 0.0; uint32_t g_gpsFixMs = 0; @@ -183,7 +188,7 @@ int64_t currentEpoch() { } int64_t currentEpochMs() { - if (!g_disciplined || g_epochBase <= 0) { + if (!g_clockValid || g_epochBase <= 0) { return 0; } return (g_epochBase * 1000LL) + (int64_t)(millis() - g_epochBaseMs); @@ -196,6 +201,26 @@ uint32_t gpsAgeMs(uint32_t now) { return now - g_gpsFixMs; } +uint32_t disciplineAgeMs(uint32_t now) { + if (!g_clockValid || g_lastDisciplineMillis == 0) { + return UINT32_MAX; + } + return now - g_lastDisciplineMillis; +} + +bool clockValidNow(uint32_t now) { + g_clockValid = g_disciplined && g_lastDisciplineMillis > 0 && (uint32_t)(now - g_lastDisciplineMillis) <= kMaxDisciplineAgeMs; + return g_clockValid; +} + +bool gpsValidNow(uint32_t now) { + return g_hasLocation && gpsAgeMs(now) <= kMaxGpsFixAgeMs; +} + +bool fieldDataValid(uint32_t now) { + return clockValidNow(now) && gpsValidNow(now); +} + void refreshGpsPosition(bool force = false) { const uint32_t now = millis(); if (!force && (uint32_t)(now - g_lastGpsRefreshMs) < kGpsRefreshPeriodMs) { @@ -307,6 +332,10 @@ void printBootBanner() { (unsigned long)kScanWindowSeconds, (unsigned long)kScanPeriodMs); Serial.printf("advertise_period_ms=%lu\n", (unsigned long)kAdvertisePeriodMs); + Serial.printf("clock_disciplined=%u time_source=GPS_PPS_RTC_REQUIRED\n", g_clockValid ? 1U : 0U); + Serial.printf("max_gps_fix_age_ms=%lu max_discipline_age_ms=%lu\n", + (unsigned long)kMaxGpsFixAgeMs, + (unsigned long)kMaxDisciplineAgeMs); Serial.printf("web_url=http://192.168.%u.1/\n", (unsigned)LOG_AP_IP_OCTET); } @@ -348,6 +377,9 @@ bool disciplineStartupClock() { if (g_clock.disciplineFromGnss(sample, waitForPps, nullptr, disciplined, hadPriorRtc, driftSeconds)) { g_epochBase = ClockDiscipline::toEpochSeconds(disciplined); g_epochBaseMs = g_lastPpsMs > 0 ? g_lastPpsMs : millis(); + g_clockValid = true; + g_lastDisciplineMillis = g_epochBaseMs; + g_lastDisciplineEpochMs = currentEpochMs(); g_latitude = sample.latitude; g_longitude = sample.longitude; g_gpsFixMs = sample.sampleMillis; @@ -355,11 +387,12 @@ bool disciplineStartupClock() { g_lastGpsRefreshMs = millis(); char iso[32]; ClockDiscipline::formatIsoUtc(disciplined, iso, sizeof(iso)); - Serial.printf("clock_disciplined node=%s utc=%s prior_rtc=%u drift_s=%lld lat=%.7f lon=%.7f\n", + Serial.printf("clock_disciplined node=%s utc=%s prior_rtc=%u drift_s=%lld last_discipline_epoch_ms=%lld lat=%.7f lon=%.7f\n", NODE_NAME, iso, hadPriorRtc ? 1U : 0U, (long long)driftSeconds, + (long long)g_lastDisciplineEpochMs, g_latitude, g_longitude); return true; @@ -372,7 +405,7 @@ bool disciplineStartupClock() { } bool openDatedLog() { - if (!g_disciplined || !g_storage.ready()) { + if (!fieldDataValid(millis()) || !g_storage.ready()) { return false; } char stamp[24]; @@ -386,7 +419,7 @@ bool openDatedLog() { Serial.printf("sd_log_open_failed path=%s err=%s\n", g_logPath, g_storage.lastError()); return false; } - g_storage.println("human_time,rx_epoch_ms,receiver,rx_lat,rx_lon,rx_gps_age_ms,heard,rssi,avg_rssi,age_s,count,seq,tx_epoch_ms,payload"); + g_storage.println("human_time,rx_epoch_ms,receiver,rx_lat,rx_lon,gps_fix_age_ms,clock_valid,gps_valid,discipline_age_ms,last_discipline_epoch_ms,heard,rssi,avg_rssi,age_s,count,seq,tx_payload_epoch_ms,payload"); g_storage.flush(); Serial.printf("sd_log_open path=%s\n", g_logPath); g_logReady = true; @@ -448,14 +481,14 @@ void updateAdvertisement() { g_lastAdvertiseMs = millis(); } -bool parsePayload(const std::string& data, char* outName, size_t outNameSize, uint32_t& outSeq, int64_t& outTxEpochMs, char* outPayload, size_t outPayloadSize) { +bool parsePayload(const std::string& data, char* outName, size_t outNameSize, uint32_t& outSeq, int64_t& outTxPayloadEpochMs, char* outPayload, size_t outPayloadSize) { if (outNameSize == 0 || outPayloadSize == 0) { return false; } outName[0] = '\0'; outPayload[0] = '\0'; outSeq = 0; - outTxEpochMs = 0; + outTxPayloadEpochMs = 0; if (data.empty() || data.size() >= outPayloadSize) { return false; } @@ -494,22 +527,41 @@ bool parsePayload(const std::string& data, char* outName, size_t outNameSize, ui strlcpy(outName, name, outNameSize); outSeq = (uint32_t)strtoul(seq, nullptr, 10); if (txEpochMs && txEpochMs[0]) { - outTxEpochMs = (int64_t)strtoll(txEpochMs, nullptr, 10); + outTxPayloadEpochMs = (int64_t)strtoll(txEpochMs, nullptr, 10); } return true; } void logAcceptedAdvertisement(const NodeState& node, const char* payload) { + const uint32_t now = millis(); + const bool clockValid = clockValidNow(now); + const uint32_t fixAge = gpsAgeMs(now); + const bool gpsValid = gpsValidNow(now); + const uint32_t discAge = disciplineAgeMs(now); + if (!clockValid || !gpsValid) { + static uint32_t lastSuppressedMs = 0; + if ((uint32_t)(now - lastSuppressedMs) >= 1000U) { + lastSuppressedMs = now; + Serial.printf("observation_suppressed node=%s heard=%s clock_valid=%u gps_valid=%u gps_fix_age_ms=%lu discipline_age_ms=%lu\n", + NODE_NAME, + node.name, + clockValid ? 1U : 0U, + gpsValid ? 1U : 0U, + (unsigned long)fixAge, + (unsigned long)discAge); + } + return; + } + const int avg = averageRssi(node); - const uint32_t age = ageSeconds(node, millis()); + const uint32_t age = ageSeconds(node, now); const int64_t epochMs = currentEpochMs(); const int64_t epoch = epochMs / 1000LL; - const uint32_t fixAge = gpsAgeMs(millis()); char human[32]; formatDateTime(epoch, human, sizeof(human)); Serial.printf("%lu,%s,%s,%d,%d,%lu,%lu,%lu,%lld,%s\n", - (unsigned long)millis(), + (unsigned long)now, NODE_NAME, node.name, node.lastRssi, @@ -517,33 +569,37 @@ void logAcceptedAdvertisement(const NodeState& node, const char* payload) { (unsigned long)age, (unsigned long)node.seenCount, (unsigned long)node.lastSeq, - (long long)node.lastTxEpochMs, + (long long)node.lastTxPayloadEpochMs, payload); if (g_logReady && g_storage.isLogOpen()) { - char line[256]; + char line[320]; snprintf(line, sizeof(line), - "%s,%lld,%s,%.7f,%.7f,%lu,%s,%d,%d,%lu,%lu,%lu,%lld,%s", + "%s,%lld,%s,%.7f,%.7f,%lu,%u,%u,%lu,%lld,%s,%d,%d,%lu,%lu,%lu,%lld,%s", human, (long long)epochMs, NODE_NAME, g_latitude, g_longitude, (unsigned long)fixAge, + clockValid ? 1U : 0U, + gpsValid ? 1U : 0U, + (unsigned long)discAge, + (long long)g_lastDisciplineEpochMs, node.name, node.lastRssi, avg, (unsigned long)age, (unsigned long)node.seenCount, (unsigned long)node.lastSeq, - (long long)node.lastTxEpochMs, + (long long)node.lastTxPayloadEpochMs, payload); g_storage.println(line); } } -void acceptAdvertisement(const char* name, int rssi, uint32_t seq, int64_t txEpochMs, const char* payload) { +void acceptAdvertisement(const char* name, int rssi, uint32_t seq, int64_t txPayloadEpochMs, const char* payload) { const int idx = nodeIndexFor(name); if (idx < 0) { return; @@ -559,7 +615,7 @@ void acceptAdvertisement(const char* name, int rssi, uint32_t seq, int64_t txEpo ++node.seenCount; node.lastSeenMs = millis(); node.lastSeq = seq; - node.lastTxEpochMs = txEpochMs; + node.lastTxPayloadEpochMs = txPayloadEpochMs; logAcceptedAdvertisement(node, payload); } @@ -572,11 +628,11 @@ class DiscoveryAdvertisedCallbacks : public BLEAdvertisedDeviceCallbacks { char name[8]; char payload[48]; uint32_t seq = 0; - int64_t txEpochMs = 0; - if (!parsePayload(advertisedDevice.getManufacturerData(), name, sizeof(name), seq, txEpochMs, payload, sizeof(payload))) { + int64_t txPayloadEpochMs = 0; + if (!parsePayload(advertisedDevice.getManufacturerData(), name, sizeof(name), seq, txPayloadEpochMs, payload, sizeof(payload))) { return; } - acceptAdvertisement(name, advertisedDevice.getRSSI(), seq, txEpochMs, payload); + acceptAdvertisement(name, advertisedDevice.getRSSI(), seq, txPayloadEpochMs, payload); } }; @@ -594,6 +650,10 @@ void startBle() { void pollBle() { const uint32_t now = millis(); + if (!clockValidNow(now)) { + BLEDevice::getAdvertising()->stop(); + return; + } if ((uint32_t)(now - g_lastAdvertiseMs) >= kAdvertisePeriodMs) { updateAdvertisement(); } @@ -623,6 +683,20 @@ void renderDisplay() { char title[32]; makeTitle(title, sizeof(title)); + const bool clockValid = clockValidNow(now); + const uint32_t fixAge = gpsAgeMs(now); + const bool gpsValid = gpsValidNow(now); + + if (!clockValid) { + showLines(title, "CLOCK STALE", "BLE survey paused", "logging paused"); + return; + } + if (!gpsValid) { + char ageLine[32]; + snprintf(ageLine, sizeof(ageLine), "gps age:%lus", fixAge == UINT32_MAX ? 9999UL : (unsigned long)(fixAge / 1000UL)); + showLines(title, "GPS STALE", ageLine, "logging paused"); + return; + } if (g_displayMode == 1) { char rows[5][32] = {}; @@ -658,7 +732,7 @@ void renderDisplay() { char l3[32]; char l4[32]; snprintf(l2, sizeof(l2), "fresh:%lu total:%lu", (unsigned long)freshTotal, (unsigned long)heardTotal); - snprintf(l3, sizeof(l3), "sd:%s web:%s log:%s", g_sdReady ? "Y" : "N", g_webReady ? "Y" : "N", g_logReady ? "Y" : "N"); + snprintf(l3, sizeof(l3), "clk:%u gps:%u log:%u", clockValid ? 1U : 0U, gpsValid ? 1U : 0U, g_logReady ? 1U : 0U); snprintf(l4, sizeof(l4), "log:%s", g_logPath[0] ? g_logPath + 6 : "none"); showLines(title, "DIAG", l2, l3, l4); return; @@ -704,7 +778,7 @@ void pollStorageWeb() { g_sdReady = true; Serial.println("sd_mounted"); } - if (g_disciplined && g_storage.ready() && !g_logReady) { + if (fieldDataValid(millis()) && g_storage.ready() && !g_logReady) { g_sdReady = true; g_logReady = openDatedLog(); }