After time changes, but before testing trial #2
This commit is contained in:
parent
aaa765adf8
commit
577caba635
6 changed files with 217 additions and 48 deletions
2
exercises/26_Bluetooth_discover/.gitignore
vendored
Normal file
2
exercises/26_Bluetooth_discover/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
*.sqlite
|
||||
|
||||
24
exercises/26_Bluetooth_discover/Codex_2_timing_prompt.md
Normal file
24
exercises/26_Bluetooth_discover/Codex_2_timing_prompt.md
Normal 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.
|
||||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
41
exercises/26_Bluetooth_discover/scripts/import_exercise_26_ble_log.pl
Normal file → Executable file
41
exercises/26_Bluetooth_discover/scripts/import_exercise_26_ble_log.pl
Normal file → Executable 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.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue