After time changes, but before testing trial #2

This commit is contained in:
John Poole 2026-05-25 14:56:23 -07:00
commit 577caba635
6 changed files with 217 additions and 48 deletions

View file

@ -0,0 +1,2 @@
*.sqlite

View file

@ -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.

View file

@ -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` | Senders 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 receivers 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`.

View file

@ -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;

View file

@ -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.

View file

@ -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();
}