refactor lora payload timing
Bump the batch payload codec to schema v4 with separate meter-time and UTC anchors, then use meter seconds for sparse batch slotting and receiver reconstruction. Update the current 868 MHz bench configuration, allow ACKs from configured receiver short IDs, improve AP-to-STA recovery, quiet the test build, and document the changed protocol in the README.
This commit is contained in:
@@ -9,13 +9,14 @@ Firmware for LilyGO T3 v1.6.1 (`ESP32 + SX1276 + SSD1306`) that runs in two role
|
||||
- Single codebase, role selected at boot by `detect_role()` (`src/config.cpp`).
|
||||
- LoRa transport is wrapped with firmware-level CRC16-CCITT (`src/lora_transport.cpp`).
|
||||
- Sender meter ingest is decoupled from LoRa waits via FreeRTOS meter reader task + queue on ESP32 (`src/sender_state_machine.cpp`).
|
||||
- Batch payload codec is schema `v3` with a 30-bit `present_mask` over `[t_last-29, t_last]` (`lib/dd3_legacy_core/src/payload_codec.cpp`).
|
||||
- Batch payload codec is schema `v4` with a 30-bit `present_mask` over `[meter_t_last-29, meter_t_last]` and a separate UTC anchor (`lib/dd3_legacy_core/src/payload_codec.cpp`).
|
||||
- Sender retries reuse cached encoded payload bytes (no re-encode on retry path).
|
||||
- Sender ACK receive windows adapt from observed ACK RTT + miss streak.
|
||||
- Sender catch-up mode drains backlog with immediate extra sends when more than one batch is queued (still ACK-gated, single inflight batch).
|
||||
- Sender only starts normal metering/transmit flow after valid time bootstrap from receiver ACK.
|
||||
- Sender fault counters are reset at first valid time sync and again at each UTC hour boundary.
|
||||
- Receiver runs STA mode if stored config is valid and connects, otherwise AP fallback.
|
||||
- Current bench defaults in `include/config.h`: 868 MHz LoRa, sender short-ID `0x6540`, receiver short-ID `0x7EB4`.
|
||||
|
||||
## LoRa Protocol
|
||||
|
||||
@@ -35,12 +36,14 @@ Transport layer chunks payload into:
|
||||
|
||||
Receiver reassembles all chunks before decode.
|
||||
|
||||
Payload codec (`schema=3`, magic `0xDDB3`) carries:
|
||||
- metadata: sender ID, batch ID, `t_last`, `present_mask`, battery mV, error counters
|
||||
Payload codec (`schema=4`, magic `0xDDB3`) carries:
|
||||
- metadata: sender ID, batch ID, `meter_t_last`, `ts_utc_last`, `present_mask`, battery mV, error counters
|
||||
- arrays per present sample: `energy_wh[]`, `p1_w[]`, `p2_w[]`, `p3_w[]`
|
||||
|
||||
`n == 0` with `present_mask == 0` is valid and used for sync request packets.
|
||||
|
||||
Schema `v4` is not wire-compatible with schema `v3`: both sender and receiver must be flashed with matching firmware.
|
||||
|
||||
### AckDown (7 bytes payload)
|
||||
|
||||
`[flags:1][batch_id_be:2][epoch_utc_be:4]`
|
||||
@@ -90,7 +93,8 @@ Timestamp derivation:
|
||||
- sample epoch: `ts_utc = meter_seconds + epoch_offset`
|
||||
- jump checks: rollback, wall-time delta mismatch, anchor drift
|
||||
|
||||
Sender builds sparse 30-slot windows and sends every `METER_SEND_INTERVAL_MS` (`30s`).
|
||||
Sender builds sparse 30-slot windows in meter-seconds space and sends every `METER_SEND_INTERVAL_MS` (`30s`).
|
||||
Samples without a valid meter seconds value are rejected for normal batch transmission.
|
||||
When backlog is present (`batch_q > 1`), sender transmits additional queued batches immediately after ACK to reduce lag, while keeping stop-and-wait ACK semantics.
|
||||
|
||||
Sender diagnostics (serial debug mode):
|
||||
@@ -111,11 +115,13 @@ For decoded `BatchUp`:
|
||||
5. Track duplicates per configured sender.
|
||||
6. If duplicate: update duplicate counters/time, skip data write/publish.
|
||||
7. If `n==0`: sync request path only.
|
||||
8. Else reconstruct each sample timestamp from `t_last + present_mask`, then:
|
||||
8. Else reconstruct each sample from `meter_t_last + present_mask` and `ts_utc_last + present_mask`, then:
|
||||
- append to SD CSV
|
||||
- publish MQTT state
|
||||
- update web status and last batch table
|
||||
|
||||
ACK validation accepts only configured sender IDs, the sender's own short-ID, or configured receiver short-IDs (`EXPECTED_RECEIVER_IDS`) so receiver-originated ACKs are allowed without accepting arbitrary device IDs.
|
||||
|
||||
## MQTT
|
||||
|
||||
State topic:
|
||||
@@ -154,6 +160,7 @@ Home Assistant discovery:
|
||||
- Default web credentials: `admin/admin`.
|
||||
- AP auth requirement is controlled by `WEB_AUTH_REQUIRE_AP` (default `true`).
|
||||
- STA auth requirement is controlled by `WEB_AUTH_REQUIRE_STA` (default `true`).
|
||||
- If the receiver boots into AP mode with saved Wi-Fi and MQTT config, it periodically retries STA mode and reinitializes NTP, MQTT, and the web server after a successful reconnect.
|
||||
|
||||
Web timestamp display:
|
||||
- human-facing timestamps show `epoch (HH:MM:SS TZ)` in local configured timezone.
|
||||
@@ -176,19 +183,14 @@ OLED duplicate display:
|
||||
## Build Environments
|
||||
|
||||
From `platformio.ini`:
|
||||
- `lilygo-t3-v1-6-1`
|
||||
- `lilygo-t3-v1-6-1-test`
|
||||
- `lilygo-t3-v1-6-1-868`
|
||||
- `lilygo-t3-v1-6-1-868-test`
|
||||
- `lilygo-t3-v1-6-1-payload-test`
|
||||
- `lilygo-t3-v1-6-1-868-payload-test`
|
||||
- `lilygo-t3-v1-6-1-prod`
|
||||
- `lilygo-t3-v1-6-1-868-prod`
|
||||
- `production`: serial debug off, light sleep on
|
||||
- `debug`: serial diagnostics on, real meter and real LoRa
|
||||
- `test`: synthetic meter samples and payload codec self-test, serial debug off
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
python -m platformio run -e lilygo-t3-v1-6-1
|
||||
python -m platformio run -e production
|
||||
```
|
||||
|
||||
## Test Mode
|
||||
|
||||
+17
-5
@@ -17,16 +17,22 @@ enum class BatchRetryPolicy : uint8_t {
|
||||
// =============================================================================
|
||||
|
||||
// LoRa frequency — uncomment ONE line:
|
||||
#define LORA_FREQUENCY_HZ 433E6 // 433 MHz (EU ISM, default)
|
||||
// #define LORA_FREQUENCY_HZ 868E6 // 868 MHz (EU SRD)
|
||||
// #define LORA_FREQUENCY_HZ 433E6 // 433 MHz (EU ISM, default)
|
||||
#define LORA_FREQUENCY_HZ 868E6 // 868 MHz (EU SRD)
|
||||
// #define LORA_FREQUENCY_HZ 915E6 // 915 MHz (US ISM)
|
||||
|
||||
// Expected sender device IDs (short-IDs). The receiver will only accept
|
||||
// batches from these senders. Add one entry per physical sender board.
|
||||
constexpr uint8_t NUM_SENDERS = 1;
|
||||
inline constexpr uint16_t EXPECTED_SENDER_IDS[NUM_SENDERS] = {
|
||||
0xF19C // TTGO #1 – 433 MHz sender
|
||||
// 0x7EB4 // TTGO #2 – 868 MHz sender (uncomment & adjust NUM_SENDERS)
|
||||
0x6540 // TTGO #1 – detected 868 MHz sender on /dev/ttyACM1
|
||||
};
|
||||
|
||||
// Receiver short-ID(s) allowed to originate AckDown packets. On the current
|
||||
// bench setup the receiver board on /dev/ttyACM0 identifies as 0x7EB4.
|
||||
constexpr uint8_t NUM_RECEIVERS = 1;
|
||||
inline constexpr uint16_t EXPECTED_RECEIVER_IDS[NUM_RECEIVERS] = {
|
||||
0x7EB4 // TTGO receiver – detected 868 MHz receiver on /dev/ttyACM0
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
@@ -79,12 +85,17 @@ constexpr uint8_t METER_BATCH_MAX_SAMPLES = 30;
|
||||
constexpr uint8_t BATCH_QUEUE_DEPTH = 10;
|
||||
constexpr BatchRetryPolicy BATCH_RETRY_POLICY = BatchRetryPolicy::Keep;
|
||||
constexpr uint32_t WATCHDOG_TIMEOUT_SEC = 120;
|
||||
constexpr uint32_t WIFI_RECONNECT_INTERVAL_MS = 60000; // WiFi reconnection retry interval (1 minute)
|
||||
constexpr uint32_t WIFI_RECONNECT_INTERVAL_MS = 80UL * 1000UL; // WiFi reconnection retry interval (80 seconds)
|
||||
constexpr bool ENABLE_HA_DISCOVERY = true;
|
||||
#ifndef SERIAL_DEBUG_MODE_FLAG
|
||||
#define SERIAL_DEBUG_MODE_FLAG 0
|
||||
#endif
|
||||
constexpr bool SERIAL_DEBUG_MODE = SERIAL_DEBUG_MODE_FLAG != 0;
|
||||
#if defined(DD3_DEBUG)
|
||||
constexpr bool CONFIG_FLOW_DEBUG = SERIAL_DEBUG_MODE;
|
||||
#else
|
||||
constexpr bool CONFIG_FLOW_DEBUG = false;
|
||||
#endif
|
||||
constexpr bool SERIAL_DEBUG_DUMP_JSON = false;
|
||||
constexpr bool LORA_SEND_BYPASS = false;
|
||||
|
||||
@@ -130,6 +141,7 @@ constexpr bool WEB_AUTH_REQUIRE_AP = true;
|
||||
// the web config page (/wifi). The first-boot AP forces password change.
|
||||
constexpr const char *WEB_AUTH_DEFAULT_USER = "admin";
|
||||
constexpr const char *WEB_AUTH_DEFAULT_PASS = "admin";
|
||||
|
||||
inline constexpr char HA_MANUFACTURER[] = "AcidBurns";
|
||||
static_assert(
|
||||
HA_MANUFACTURER[0] == 'A' &&
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
struct BatchInput {
|
||||
uint16_t sender_id;
|
||||
uint16_t batch_id;
|
||||
uint32_t t_last;
|
||||
uint32_t meter_t_last;
|
||||
uint32_t ts_utc_last;
|
||||
uint32_t present_mask;
|
||||
uint8_t n;
|
||||
uint16_t battery_mV;
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
#include <limits.h>
|
||||
|
||||
static constexpr uint16_t kMagic = 0xDDB3;
|
||||
// Breaking change: schema v3 replaces fixed dt_s spacing with a 30-bit present_mask.
|
||||
static constexpr uint8_t kSchema = 3;
|
||||
// Breaking change: schema v4 uses a 30-bit present_mask in meter_seconds space
|
||||
// plus a ts_utc anchor for receiver-side wall-clock reconstruction.
|
||||
static constexpr uint8_t kSchema = 4;
|
||||
static constexpr uint8_t kFlags = 0x01;
|
||||
static constexpr size_t kMaxSamples = 30;
|
||||
static constexpr uint32_t kPresentMaskValidBits = 0x3FFFFFFFUL;
|
||||
@@ -125,7 +126,7 @@ bool encode_batch(const BatchInput &in, uint8_t *out, size_t out_cap, size_t *ou
|
||||
return false;
|
||||
}
|
||||
size_t pos = 0;
|
||||
if (!ensure_capacity(24, out_cap, pos)) {
|
||||
if (!ensure_capacity(28, out_cap, pos)) {
|
||||
return false;
|
||||
}
|
||||
write_u16_le(&out[pos], kMagic);
|
||||
@@ -136,7 +137,9 @@ bool encode_batch(const BatchInput &in, uint8_t *out, size_t out_cap, size_t *ou
|
||||
pos += 2;
|
||||
write_u16_le(&out[pos], in.batch_id);
|
||||
pos += 2;
|
||||
write_u32_le(&out[pos], in.t_last);
|
||||
write_u32_le(&out[pos], in.meter_t_last);
|
||||
pos += 4;
|
||||
write_u32_le(&out[pos], in.ts_utc_last);
|
||||
pos += 4;
|
||||
write_u32_le(&out[pos], in.present_mask);
|
||||
pos += 4;
|
||||
@@ -207,7 +210,7 @@ bool decode_batch(const uint8_t *buf, size_t len, BatchInput *out) {
|
||||
return false;
|
||||
}
|
||||
size_t pos = 0;
|
||||
if (len < 24) {
|
||||
if (len < 28) {
|
||||
return false;
|
||||
}
|
||||
uint16_t magic = read_u16_le(&buf[pos]);
|
||||
@@ -221,7 +224,9 @@ bool decode_batch(const uint8_t *buf, size_t len, BatchInput *out) {
|
||||
pos += 2;
|
||||
out->batch_id = read_u16_le(&buf[pos]);
|
||||
pos += 2;
|
||||
out->t_last = read_u32_le(&buf[pos]);
|
||||
out->meter_t_last = read_u32_le(&buf[pos]);
|
||||
pos += 4;
|
||||
out->ts_utc_last = read_u32_le(&buf[pos]);
|
||||
pos += 4;
|
||||
out->present_mask = read_u32_le(&buf[pos]);
|
||||
pos += 4;
|
||||
@@ -319,7 +324,8 @@ bool payload_codec_self_test() {
|
||||
BatchInput in = {};
|
||||
in.sender_id = 1;
|
||||
in.batch_id = 42;
|
||||
in.t_last = 1700000000;
|
||||
in.meter_t_last = 123456789;
|
||||
in.ts_utc_last = 1700000000;
|
||||
in.present_mask = (1UL << 0) | (1UL << 2) | (1UL << 3) | (1UL << 10) | (1UL << 29);
|
||||
in.n = 5;
|
||||
in.battery_mV = 3750;
|
||||
@@ -362,7 +368,8 @@ bool payload_codec_self_test() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (out.sender_id != in.sender_id || out.batch_id != in.batch_id || out.t_last != in.t_last ||
|
||||
if (out.sender_id != in.sender_id || out.batch_id != in.batch_id || out.meter_t_last != in.meter_t_last ||
|
||||
out.ts_utc_last != in.ts_utc_last ||
|
||||
out.present_mask != in.present_mask || out.n != in.n || out.battery_mV != in.battery_mV ||
|
||||
out.err_m != in.err_m || out.err_d != in.err_d || out.err_tx != in.err_tx || out.err_last != in.err_last ||
|
||||
out.err_rx_reject != in.err_rx_reject) {
|
||||
|
||||
+1
-3
@@ -50,8 +50,6 @@ build_flags =
|
||||
[env:test]
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-DSERIAL_DEBUG_MODE_FLAG=1
|
||||
-DSERIAL_DEBUG_MODE_FLAG=0
|
||||
-DENABLE_TEST_MODE
|
||||
-DPAYLOAD_CODEC_TEST
|
||||
-DDEBUG_METER_DIAG
|
||||
-DDD3_DEBUG
|
||||
|
||||
@@ -112,6 +112,14 @@ void setup() {
|
||||
display_set_sender_statuses(g_receiver_shared.sender_statuses, NUM_SENDERS);
|
||||
|
||||
bool has_cfg = wifi_load_config(g_cfg);
|
||||
if (CONFIG_FLOW_DEBUG) {
|
||||
serial_debug_printf("cfg: has=%u ssid=%s mqtt=%s:%u valid=%u",
|
||||
has_cfg ? 1U : 0U,
|
||||
g_cfg.ssid.c_str(),
|
||||
g_cfg.mqtt_host.c_str(),
|
||||
static_cast<unsigned>(g_cfg.mqtt_port),
|
||||
g_cfg.valid ? 1U : 0U);
|
||||
}
|
||||
|
||||
// Store WiFi config in shared state for later reconnection attempts
|
||||
g_receiver_shared.wifi_config = g_cfg;
|
||||
@@ -142,6 +150,7 @@ void setup() {
|
||||
if (g_cfg.ntp_server_2.isEmpty()) {
|
||||
g_cfg.ntp_server_2 = "time.nist.gov";
|
||||
}
|
||||
mqtt_init(g_cfg, g_device_id);
|
||||
web_server_set_config(g_cfg);
|
||||
web_server_set_sender_faults(g_receiver_shared.sender_faults_remote, g_receiver_shared.sender_last_error_remote);
|
||||
web_server_begin_ap(g_receiver_shared.sender_statuses, NUM_SENDERS);
|
||||
|
||||
+24
-2
@@ -39,6 +39,12 @@ void mqtt_init(const WifiMqttConfig &config, const char *device_id) {
|
||||
} else {
|
||||
g_client_id = String("dd3-bridge-") + String(random(0xffff), HEX);
|
||||
}
|
||||
if (CONFIG_FLOW_DEBUG) {
|
||||
Serial.printf("mqtt_init: host=%s port=%u client=%s\n",
|
||||
g_cfg.mqtt_host.c_str(),
|
||||
static_cast<unsigned>(g_cfg.mqtt_port),
|
||||
g_client_id.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
static bool mqtt_connect() {
|
||||
@@ -46,10 +52,26 @@ static bool mqtt_connect() {
|
||||
return true;
|
||||
}
|
||||
String client_id = g_client_id.length() > 0 ? g_client_id : String("dd3-bridge-") + String(random(0xffff), HEX);
|
||||
bool connected = false;
|
||||
if (g_cfg.mqtt_user.length() > 0) {
|
||||
return mqtt_client.connect(client_id.c_str(), g_cfg.mqtt_user.c_str(), g_cfg.mqtt_pass.c_str());
|
||||
connected = mqtt_client.connect(client_id.c_str(), g_cfg.mqtt_user.c_str(), g_cfg.mqtt_pass.c_str());
|
||||
} else {
|
||||
connected = mqtt_client.connect(client_id.c_str());
|
||||
}
|
||||
return mqtt_client.connect(client_id.c_str());
|
||||
if (CONFIG_FLOW_DEBUG) {
|
||||
if (connected) {
|
||||
Serial.printf("mqtt: connected host=%s port=%u client=%s\n",
|
||||
g_cfg.mqtt_host.c_str(),
|
||||
static_cast<unsigned>(g_cfg.mqtt_port),
|
||||
client_id.c_str());
|
||||
} else {
|
||||
Serial.printf("mqtt: connect failed host=%s port=%u state=%d\n",
|
||||
g_cfg.mqtt_host.c_str(),
|
||||
static_cast<unsigned>(g_cfg.mqtt_port),
|
||||
mqtt_client.state());
|
||||
}
|
||||
}
|
||||
return connected;
|
||||
}
|
||||
|
||||
void mqtt_loop() {
|
||||
|
||||
+2
-1
@@ -5,7 +5,8 @@
|
||||
struct BatchInput {
|
||||
uint16_t sender_id;
|
||||
uint16_t batch_id;
|
||||
uint32_t t_last;
|
||||
uint32_t meter_t_last;
|
||||
uint32_t ts_utc_last;
|
||||
uint32_t present_mask;
|
||||
uint8_t n;
|
||||
uint16_t battery_mV;
|
||||
|
||||
+23
-21
@@ -308,43 +308,42 @@ static bool process_batch_packet(const LoraPacket &pkt, BatchInput &out_batch, b
|
||||
// to recover from temporary WiFi outages
|
||||
static void try_wifi_reconnect_if_in_ap_mode() {
|
||||
if (!g_ap_mode) {
|
||||
// Already in STA mode, no need to reconnect
|
||||
return;
|
||||
}
|
||||
|
||||
if (!g_shared || g_shared->wifi_config.ssid.length() == 0) {
|
||||
// No valid WiFi config to reconnect with
|
||||
if (!g_shared || g_shared->wifi_config.ssid.length() == 0 || g_shared->wifi_config.mqtt_host.length() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t now_ms = millis();
|
||||
if (g_shared->last_wifi_reconnect_attempt_ms != 0 &&
|
||||
now_ms - g_shared->last_wifi_reconnect_attempt_ms < WIFI_RECONNECT_INTERVAL_MS) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_shared->last_wifi_reconnect_attempt_ms == 0 ||
|
||||
now_ms - g_shared->last_wifi_reconnect_attempt_ms >= WIFI_RECONNECT_INTERVAL_MS) {
|
||||
|
||||
// Update the last attempt time
|
||||
g_shared->last_wifi_reconnect_attempt_ms = now_ms;
|
||||
|
||||
if (SERIAL_DEBUG_MODE) {
|
||||
serial_debug_printf("wifi_reconnect: attempting to reconnect from AP mode");
|
||||
serial_debug_printf("wifi_reconnect: attempting STA reconnect from AP mode");
|
||||
}
|
||||
|
||||
// Try to reconnect with 10 second timeout
|
||||
if (wifi_try_reconnect_sta(g_shared->wifi_config, 10000)) {
|
||||
// Reconnection successful!
|
||||
g_ap_mode = false;
|
||||
time_receiver_init(g_shared->wifi_config.ntp_server_1.c_str(), g_shared->wifi_config.ntp_server_2.c_str());
|
||||
mqtt_init(g_shared->wifi_config, g_device_id);
|
||||
web_server_set_config(g_shared->wifi_config);
|
||||
web_server_begin_sta(g_sender_statuses, NUM_SENDERS);
|
||||
if (SERIAL_DEBUG_MODE) {
|
||||
serial_debug_printf("wifi_reconnect: reconnection successful, switching from AP to STA mode");
|
||||
serial_debug_printf("wifi_reconnect: STA reconnect successful");
|
||||
}
|
||||
} else {
|
||||
// Reconnection failed, restore AP mode to ensure web interface is available
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_shared->ap_ssid[0] != '\0') {
|
||||
wifi_restore_ap_mode(g_shared->ap_ssid, g_shared->ap_password);
|
||||
}
|
||||
if (SERIAL_DEBUG_MODE) {
|
||||
serial_debug_printf("wifi_reconnect: reconnection failed, restored AP mode");
|
||||
}
|
||||
}
|
||||
}
|
||||
serial_debug_printf("wifi_reconnect: reconnect failed, AP restored");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -400,7 +399,7 @@ static void receiver_loop() {
|
||||
}
|
||||
uint32_t duplicate_ts = time_get_utc();
|
||||
if (duplicate_ts == 0) {
|
||||
duplicate_ts = batch.t_last;
|
||||
duplicate_ts = batch.ts_utc_last;
|
||||
}
|
||||
status.rx_last_duplicate_ts_utc = duplicate_ts;
|
||||
}
|
||||
@@ -429,12 +428,13 @@ static void receiver_loop() {
|
||||
if (short_id == 0) {
|
||||
short_id = short_id_from_sender_id(batch.sender_id);
|
||||
}
|
||||
if (batch.t_last < static_cast<uint32_t>(METER_BATCH_MAX_SAMPLES - 1) || batch.t_last < MIN_ACCEPTED_EPOCH_UTC) {
|
||||
if (batch.meter_t_last < static_cast<uint32_t>(METER_BATCH_MAX_SAMPLES - 1) || batch.ts_utc_last < static_cast<uint32_t>(METER_BATCH_MAX_SAMPLES - 1) || batch.ts_utc_last < MIN_ACCEPTED_EPOCH_UTC) {
|
||||
note_fault(g_receiver_faults, g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms, FaultType::Decode);
|
||||
display_set_last_error(g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms);
|
||||
goto receiver_loop_done;
|
||||
}
|
||||
const uint32_t window_start = batch.t_last - static_cast<uint32_t>(METER_BATCH_MAX_SAMPLES - 1);
|
||||
const uint32_t meter_window_start = batch.meter_t_last - static_cast<uint32_t>(METER_BATCH_MAX_SAMPLES - 1);
|
||||
const uint32_t ts_utc_window_start = batch.ts_utc_last - static_cast<uint32_t>(METER_BATCH_MAX_SAMPLES - 1);
|
||||
|
||||
MeterData samples[METER_BATCH_MAX_SAMPLES];
|
||||
float bat_v = batch.battery_mV > 0 ? static_cast<float>(batch.battery_mV) / 1000.0f : NAN;
|
||||
@@ -456,7 +456,9 @@ static void receiver_loop() {
|
||||
} else {
|
||||
snprintf(data.device_id, sizeof(data.device_id), "dd3-0000");
|
||||
}
|
||||
data.ts_utc = window_start + static_cast<uint32_t>(slot);
|
||||
data.meter_seconds = meter_window_start + static_cast<uint32_t>(slot);
|
||||
data.meter_seconds_valid = true;
|
||||
data.ts_utc = ts_utc_window_start + static_cast<uint32_t>(slot);
|
||||
if (data.ts_utc < MIN_ACCEPTED_EPOCH_UTC) {
|
||||
note_fault(g_receiver_faults, g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms, FaultType::Decode);
|
||||
display_set_last_error(g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms);
|
||||
|
||||
@@ -161,7 +161,7 @@ struct MeterSampleEvent {
|
||||
static QueueHandle_t g_meter_sample_queue = nullptr;
|
||||
static TaskHandle_t g_meter_reader_task = nullptr;
|
||||
static bool g_meter_reader_task_running = false;
|
||||
static constexpr UBaseType_t METER_SAMPLE_QUEUE_LEN = 8;
|
||||
static constexpr UBaseType_t METER_SAMPLE_QUEUE_LEN = 32;
|
||||
static constexpr uint32_t METER_READER_TASK_STACK_WORDS = 4096;
|
||||
static constexpr UBaseType_t METER_READER_TASK_PRIORITY = 2;
|
||||
static constexpr BaseType_t METER_READER_TASK_CORE = 0;
|
||||
@@ -778,6 +778,15 @@ static uint16_t short_id_from_sender_id(uint16_t sender_id) {
|
||||
return EXPECTED_SENDER_IDS[sender_id - 1];
|
||||
}
|
||||
|
||||
static bool is_expected_receiver_short_id(uint16_t short_id) {
|
||||
for (uint8_t i = 0; i < NUM_RECEIVERS; ++i) {
|
||||
if (EXPECTED_RECEIVER_IDS[i] == short_id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static uint32_t kwh_to_wh_from_float(float value) {
|
||||
if (isnan(value)) {
|
||||
return 0;
|
||||
@@ -1001,7 +1010,9 @@ static bool send_inflight_batch(uint32_t ts_for_display) {
|
||||
BatchInput input = {};
|
||||
input.sender_id = sender_id_from_short_id(g_short_id);
|
||||
input.batch_id = g_inflight_batch_id;
|
||||
input.t_last = g_inflight_sync_request ? time_get_utc() :
|
||||
input.meter_t_last = g_inflight_sync_request ? 0 :
|
||||
g_inflight_samples[g_inflight_count - 1].meter_seconds;
|
||||
input.ts_utc_last = g_inflight_sync_request ? time_get_utc() :
|
||||
g_inflight_samples[g_inflight_count - 1].ts_utc;
|
||||
input.present_mask = 0;
|
||||
input.n = 0;
|
||||
@@ -1018,22 +1029,23 @@ static bool send_inflight_batch(uint32_t ts_for_display) {
|
||||
uint8_t ts_collapsed = 0;
|
||||
|
||||
if (!g_inflight_sync_request) {
|
||||
if (input.t_last < static_cast<uint32_t>(METER_BATCH_MAX_SAMPLES - 1)) {
|
||||
if (!g_inflight_samples[g_inflight_count - 1].meter_seconds_valid ||
|
||||
input.meter_t_last < static_cast<uint32_t>(METER_BATCH_MAX_SAMPLES - 1)) {
|
||||
g_last_tx_build_error = TxBuildError::Encode;
|
||||
return false;
|
||||
}
|
||||
const uint32_t window_start = input.t_last - static_cast<uint32_t>(METER_BATCH_MAX_SAMPLES - 1);
|
||||
const uint32_t window_start = input.meter_t_last - static_cast<uint32_t>(METER_BATCH_MAX_SAMPLES - 1);
|
||||
MeterData slot_samples[METER_BATCH_MAX_SAMPLES];
|
||||
bool slot_used[METER_BATCH_MAX_SAMPLES] = {};
|
||||
for (uint8_t i = 0; i < g_inflight_count; ++i) {
|
||||
const MeterData &sample = g_inflight_samples[i];
|
||||
if (sample.ts_utc < window_start || sample.ts_utc > input.t_last) {
|
||||
if (!sample.meter_seconds_valid || sample.meter_seconds < window_start || sample.meter_seconds > input.meter_t_last) {
|
||||
if (ts_dropped < 255) {
|
||||
ts_dropped++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
uint8_t slot = static_cast<uint8_t>(sample.ts_utc - window_start);
|
||||
uint8_t slot = static_cast<uint8_t>(sample.meter_seconds - window_start);
|
||||
if (slot_used[slot] && ts_collapsed < 255) {
|
||||
ts_collapsed++;
|
||||
}
|
||||
@@ -1308,7 +1320,11 @@ static void sender_loop() {
|
||||
data.ts_utc = sample_ts_utc;
|
||||
data.valid = has_snapshot;
|
||||
|
||||
bool appended = append_meter_sample(data, meter_ok, has_snapshot);
|
||||
if (!data.meter_seconds_valid) {
|
||||
data.valid = false;
|
||||
}
|
||||
|
||||
bool appended = append_meter_sample(data, meter_ok, has_snapshot && data.meter_seconds_valid);
|
||||
(void)appended;
|
||||
display_set_last_meter(data);
|
||||
display_set_last_read(meter_ok, data.ts_utc);
|
||||
@@ -1426,8 +1442,10 @@ static void sender_loop() {
|
||||
ack_id);
|
||||
}
|
||||
} else if (sender_id_from_short_id(ack_pkt.device_id_short) == 0 &&
|
||||
ack_pkt.device_id_short != g_short_id) {
|
||||
// Reject ACKs from unknown device IDs to prevent spoofing.
|
||||
ack_pkt.device_id_short != g_short_id &&
|
||||
!is_expected_receiver_short_id(ack_pkt.device_id_short)) {
|
||||
// Reject ACKs from unknown device IDs to prevent spoofing, but allow
|
||||
// ACKs from configured receiver short-IDs.
|
||||
sender_note_rx_reject(RxRejectReason::DeviceIdMismatch, "ack");
|
||||
if (SERIAL_DEBUG_MODE) {
|
||||
serial_debug_printf("ack: reject device_id=%04X (unknown)",
|
||||
@@ -1656,7 +1674,7 @@ SenderStats SenderStateMachine::stats() const {
|
||||
}
|
||||
|
||||
void SenderStateMachine::handleMeterRead(uint32_t now_ms) {
|
||||
meter_reader_pump(now_ms);
|
||||
(void)now_ms;
|
||||
}
|
||||
|
||||
void SenderStateMachine::maybeSendBatch(uint32_t now_ms) {
|
||||
|
||||
@@ -127,6 +127,15 @@ bool wifi_connect_sta(const WifiMqttConfig &config, uint32_t timeout_ms) {
|
||||
bool connected = WiFi.status() == WL_CONNECTED;
|
||||
if (connected) {
|
||||
esp_wifi_set_ps(WIFI_PS_MIN_MODEM);
|
||||
if (CONFIG_FLOW_DEBUG) {
|
||||
Serial.printf("wifi_sta: connected ssid=%s ip=%s\n",
|
||||
config.ssid.c_str(),
|
||||
WiFi.localIP().toString().c_str());
|
||||
}
|
||||
} else if (CONFIG_FLOW_DEBUG) {
|
||||
Serial.printf("wifi_sta: failed ssid=%s status=%d\n",
|
||||
config.ssid.c_str(),
|
||||
static_cast<int>(WiFi.status()));
|
||||
}
|
||||
return connected;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ static void fill_sparse_batch(BatchInput &in) {
|
||||
memset(&in, 0, sizeof(in));
|
||||
in.sender_id = 1;
|
||||
in.batch_id = 42;
|
||||
in.t_last = 1700000000;
|
||||
in.meter_t_last = 123456789;
|
||||
in.ts_utc_last = 1700000000;
|
||||
in.present_mask = (1UL << 0) | (1UL << 2) | (1UL << 3) | (1UL << 10) | (1UL << 29);
|
||||
in.n = 5;
|
||||
in.battery_mV = 3750;
|
||||
@@ -45,7 +46,8 @@ static void fill_full_batch(BatchInput &in) {
|
||||
memset(&in, 0, sizeof(in));
|
||||
in.sender_id = 1;
|
||||
in.batch_id = 0xBEEF;
|
||||
in.t_last = 1769904999;
|
||||
in.meter_t_last = 1769904999 - 29;
|
||||
in.ts_utc_last = 1769904999;
|
||||
in.present_mask = 0x3FFFFFFFUL;
|
||||
in.n = kMaxSamples;
|
||||
in.battery_mV = 4095;
|
||||
@@ -65,7 +67,8 @@ static void fill_full_batch(BatchInput &in) {
|
||||
static void assert_batch_equals(const BatchInput &expected, const BatchInput &actual) {
|
||||
TEST_ASSERT_EQUAL_UINT16(expected.sender_id, actual.sender_id);
|
||||
TEST_ASSERT_EQUAL_UINT16(expected.batch_id, actual.batch_id);
|
||||
TEST_ASSERT_EQUAL_UINT32(expected.t_last, actual.t_last);
|
||||
TEST_ASSERT_EQUAL_UINT32(expected.meter_t_last, actual.meter_t_last);
|
||||
TEST_ASSERT_EQUAL_UINT32(expected.ts_utc_last, actual.ts_utc_last);
|
||||
TEST_ASSERT_EQUAL_UINT32(expected.present_mask, actual.present_mask);
|
||||
TEST_ASSERT_EQUAL_UINT8(expected.n, actual.n);
|
||||
TEST_ASSERT_EQUAL_UINT16(expected.battery_mV, actual.battery_mV);
|
||||
@@ -89,14 +92,14 @@ static void assert_batch_equals(const BatchInput &expected, const BatchInput &ac
|
||||
}
|
||||
}
|
||||
|
||||
static void test_encode_decode_roundtrip_schema_v3() {
|
||||
static void test_encode_decode_roundtrip_schema_v4() {
|
||||
BatchInput in = {};
|
||||
fill_sparse_batch(in);
|
||||
|
||||
uint8_t encoded[256] = {};
|
||||
size_t encoded_len = 0;
|
||||
TEST_ASSERT_TRUE(encode_batch(in, encoded, sizeof(encoded), &encoded_len));
|
||||
TEST_ASSERT_TRUE(encoded_len > 24);
|
||||
TEST_ASSERT_TRUE(encoded_len > 28);
|
||||
|
||||
BatchInput out = {};
|
||||
TEST_ASSERT_TRUE(decode_batch(encoded, encoded_len, &out));
|
||||
@@ -199,12 +202,22 @@ static void test_encode_rejects_invalid_n_and_regression_cases() {
|
||||
}
|
||||
|
||||
static const uint8_t VECTOR_SYNC_EMPTY[] = {
|
||||
0xB3, 0xDD, 0x03, 0x01, 0x01, 0x00, 0x34, 0x12, 0xE4, 0x97, 0x7E, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA6, 0x0E,
|
||||
0xB3, 0xDD, 0x04, 0x01, 0x01, 0x00, 0x34, 0x12,
|
||||
0x15, 0xCD, 0x5B, 0x07,
|
||||
0xE4, 0x97, 0x7E, 0x69,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00,
|
||||
0xA6, 0x0E,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
static const uint8_t VECTOR_SPARSE_5[] = {
|
||||
0xB3, 0xDD, 0x03, 0x01, 0x01, 0x00, 0x2A, 0x00, 0x00, 0xF1, 0x53, 0x65, 0x0D, 0x04, 0x00, 0x20, 0x05, 0xA6, 0x0E,
|
||||
0x02, 0x01, 0x03, 0x02, 0x01, 0xA0, 0x86, 0x01, 0x00, 0x01, 0x31, 0x00, 0x96, 0x01, 0x88, 0xFF, 0x3C, 0xA0, 0x1F,
|
||||
0xB3, 0xDD, 0x04, 0x01, 0x01, 0x00, 0x2A, 0x00,
|
||||
0x15, 0xCD, 0x5B, 0x07,
|
||||
0x00, 0xF1, 0x53, 0x65,
|
||||
0x0D, 0x04, 0x00, 0x20,
|
||||
0x05, 0xA6, 0x0E,
|
||||
0x02, 0x01, 0x03, 0x02, 0x01,
|
||||
0xA0, 0x86, 0x01, 0x00, 0x01, 0x31, 0x00, 0x96, 0x01, 0x88, 0xFF, 0x3C, 0xA0, 0x1F,
|
||||
0x9F, 0x1F, 0x9C, 0x09, 0x32, 0x00, 0x9F, 0x1F, 0xB4, 0x1F, 0xA0, 0x1F, 0xAB, 0x20, 0x00, 0x00, 0x14, 0x9F, 0x1F,
|
||||
0xA0, 0x1F, 0x14};
|
||||
|
||||
@@ -224,7 +237,8 @@ static void test_payload_golden_vectors() {
|
||||
BatchInput expected_sync = {};
|
||||
expected_sync.sender_id = 1;
|
||||
expected_sync.batch_id = 0x1234;
|
||||
expected_sync.t_last = 1769904100;
|
||||
expected_sync.meter_t_last = 123456789;
|
||||
expected_sync.ts_utc_last = 1769904100;
|
||||
expected_sync.present_mask = 0;
|
||||
expected_sync.n = 0;
|
||||
expected_sync.battery_mV = 3750;
|
||||
@@ -267,7 +281,7 @@ static void test_payload_golden_vectors() {
|
||||
void setup() {
|
||||
dd3_legacy_core_force_link();
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_encode_decode_roundtrip_schema_v3);
|
||||
RUN_TEST(test_encode_decode_roundtrip_schema_v4);
|
||||
RUN_TEST(test_decode_rejects_bad_magic_schema_flags);
|
||||
RUN_TEST(test_decode_rejects_truncated_and_length_mismatch);
|
||||
RUN_TEST(test_encode_and_decode_reject_invalid_present_mask);
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
static void test_decode_batch_null_args() {
|
||||
uint8_t dummy[32] = {};
|
||||
BatchInput out = {};
|
||||
TEST_ASSERT_FALSE(decode_batch(nullptr, 24, &out));
|
||||
TEST_ASSERT_FALSE(decode_batch(dummy, 24, nullptr));
|
||||
TEST_ASSERT_FALSE(decode_batch(nullptr, 28, &out));
|
||||
TEST_ASSERT_FALSE(decode_batch(dummy, 28, nullptr));
|
||||
TEST_ASSERT_FALSE(decode_batch(nullptr, 0, nullptr));
|
||||
}
|
||||
|
||||
@@ -29,90 +29,103 @@ static void test_decode_batch_zero_length() {
|
||||
}
|
||||
|
||||
static void test_decode_batch_minimal_valid_sync() {
|
||||
// Sync-only (n=0) payload: 24 bytes header, no samples.
|
||||
uint8_t buf[24] = {};
|
||||
// Sync-only (n=0) payload: 28 bytes header, no samples.
|
||||
uint8_t buf[28] = {};
|
||||
// magic 0xDDB3 LE
|
||||
buf[0] = 0xB3; buf[1] = 0xDD;
|
||||
buf[2] = 3; // schema
|
||||
buf[2] = 4; // schema
|
||||
buf[3] = 0x01; // flags
|
||||
// sender_id=1
|
||||
buf[4] = 0x01; buf[5] = 0x00;
|
||||
// batch_id=1
|
||||
buf[6] = 0x01; buf[7] = 0x00;
|
||||
// t_last=1769904000 LE
|
||||
uint32_t t = 1769904000UL;
|
||||
buf[8] = t & 0xFF; buf[9] = (t >> 8) & 0xFF;
|
||||
buf[10] = (t >> 16) & 0xFF; buf[11] = (t >> 24) & 0xFF;
|
||||
// meter_t_last=123456789 LE
|
||||
uint32_t meter_t = 123456789UL;
|
||||
buf[8] = meter_t & 0xFF; buf[9] = (meter_t >> 8) & 0xFF;
|
||||
buf[10] = (meter_t >> 16) & 0xFF; buf[11] = (meter_t >> 24) & 0xFF;
|
||||
// ts_utc_last=1769904000 LE
|
||||
uint32_t utc_t = 1769904000UL;
|
||||
buf[12] = utc_t & 0xFF; buf[13] = (utc_t >> 8) & 0xFF;
|
||||
buf[14] = (utc_t >> 16) & 0xFF; buf[15] = (utc_t >> 24) & 0xFF;
|
||||
// present_mask=0
|
||||
buf[12] = 0; buf[13] = 0; buf[14] = 0; buf[15] = 0;
|
||||
buf[16] = 0; buf[17] = 0; buf[18] = 0; buf[19] = 0;
|
||||
// n=0
|
||||
buf[16] = 0;
|
||||
buf[20] = 0;
|
||||
// battery_mV=3750 LE
|
||||
buf[17] = 0xA6; buf[18] = 0x0E;
|
||||
buf[21] = 0xA6; buf[22] = 0x0E;
|
||||
// err fields
|
||||
buf[19] = 0; buf[20] = 0; buf[21] = 0; buf[22] = 0; buf[23] = 0;
|
||||
buf[23] = 0; buf[24] = 0; buf[25] = 0; buf[26] = 0; buf[27] = 0;
|
||||
|
||||
BatchInput out = {};
|
||||
TEST_ASSERT_TRUE(decode_batch(buf, 24, &out));
|
||||
TEST_ASSERT_TRUE(decode_batch(buf, 28, &out));
|
||||
TEST_ASSERT_EQUAL_UINT8(0, out.n);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, out.present_mask);
|
||||
}
|
||||
|
||||
static void test_decode_batch_n_exceeds_30() {
|
||||
// Forge a header with n=31, which should be rejected.
|
||||
uint8_t buf[24] = {};
|
||||
uint8_t buf[28] = {};
|
||||
buf[0] = 0xB3; buf[1] = 0xDD;
|
||||
buf[2] = 3; buf[3] = 0x01;
|
||||
buf[2] = 4; buf[3] = 0x01;
|
||||
buf[4] = 0x01; buf[5] = 0x00;
|
||||
buf[6] = 0x01; buf[7] = 0x00;
|
||||
uint32_t t = 1769904000UL;
|
||||
buf[8] = t & 0xFF; buf[9] = (t >> 8) & 0xFF;
|
||||
buf[10] = (t >> 16) & 0xFF; buf[11] = (t >> 24) & 0xFF;
|
||||
buf[12] = 0xFF; buf[13] = 0xFF; buf[14] = 0xFF; buf[15] = 0x3F; // all 30 bits set
|
||||
buf[16] = 31; // n=31 → must reject
|
||||
buf[17] = 0xA6; buf[18] = 0x0E;
|
||||
buf[19] = 0; buf[20] = 0; buf[21] = 0; buf[22] = 0; buf[23] = 0;
|
||||
uint32_t meter_t = 123456789UL;
|
||||
buf[8] = meter_t & 0xFF; buf[9] = (meter_t >> 8) & 0xFF;
|
||||
buf[10] = (meter_t >> 16) & 0xFF; buf[11] = (meter_t >> 24) & 0xFF;
|
||||
uint32_t utc_t = 1769904000UL;
|
||||
buf[12] = utc_t & 0xFF; buf[13] = (utc_t >> 8) & 0xFF;
|
||||
buf[14] = (utc_t >> 16) & 0xFF; buf[15] = (utc_t >> 24) & 0xFF;
|
||||
buf[16] = 0xFF; buf[17] = 0xFF; buf[18] = 0xFF; buf[19] = 0x3F; // all 30 bits set
|
||||
buf[20] = 31; // n=31 → must reject
|
||||
buf[21] = 0xA6; buf[22] = 0x0E;
|
||||
buf[23] = 0; buf[24] = 0; buf[25] = 0; buf[26] = 0; buf[27] = 0;
|
||||
|
||||
BatchInput out = {};
|
||||
TEST_ASSERT_FALSE(decode_batch(buf, 24, &out));
|
||||
TEST_ASSERT_FALSE(decode_batch(buf, 28, &out));
|
||||
}
|
||||
|
||||
static void test_decode_batch_present_mask_n_mismatch() {
|
||||
// present_mask has 3 bits but n=5 → must reject.
|
||||
uint8_t buf[24] = {};
|
||||
uint8_t buf[28] = {};
|
||||
buf[0] = 0xB3; buf[1] = 0xDD;
|
||||
buf[2] = 3; buf[3] = 0x01;
|
||||
buf[2] = 4; buf[3] = 0x01;
|
||||
buf[4] = 0x01; buf[5] = 0x00;
|
||||
buf[6] = 0x01; buf[7] = 0x00;
|
||||
uint32_t t = 1769904000UL;
|
||||
buf[8] = t & 0xFF; buf[9] = (t >> 8) & 0xFF;
|
||||
buf[10] = (t >> 16) & 0xFF; buf[11] = (t >> 24) & 0xFF;
|
||||
buf[12] = 0x07; buf[13] = 0; buf[14] = 0; buf[15] = 0; // 3 bits
|
||||
buf[16] = 5; // n=5 but only 3 mask bits
|
||||
buf[17] = 0xA6; buf[18] = 0x0E;
|
||||
buf[19] = 0; buf[20] = 0; buf[21] = 0; buf[22] = 0; buf[23] = 0;
|
||||
uint32_t meter_t = 123456789UL;
|
||||
buf[8] = meter_t & 0xFF; buf[9] = (meter_t >> 8) & 0xFF;
|
||||
buf[10] = (meter_t >> 16) & 0xFF; buf[11] = (meter_t >> 24) & 0xFF;
|
||||
uint32_t utc_t = 1769904000UL;
|
||||
buf[12] = utc_t & 0xFF; buf[13] = (utc_t >> 8) & 0xFF;
|
||||
buf[14] = (utc_t >> 16) & 0xFF; buf[15] = (utc_t >> 24) & 0xFF;
|
||||
buf[16] = 0x07; buf[17] = 0; buf[18] = 0; buf[19] = 0; // 3 bits
|
||||
buf[20] = 5; // n=5 but only 3 mask bits
|
||||
buf[21] = 0xA6; buf[22] = 0x0E;
|
||||
buf[23] = 0; buf[24] = 0; buf[25] = 0; buf[26] = 0; buf[27] = 0;
|
||||
|
||||
BatchInput out = {};
|
||||
TEST_ASSERT_FALSE(decode_batch(buf, 24, &out));
|
||||
TEST_ASSERT_FALSE(decode_batch(buf, 28, &out));
|
||||
}
|
||||
|
||||
static void test_decode_batch_reserved_mask_bits() {
|
||||
// Bit 30 or 31 set → must reject (only bits 0-29 valid).
|
||||
uint8_t buf[24] = {};
|
||||
uint8_t buf[28] = {};
|
||||
buf[0] = 0xB3; buf[1] = 0xDD;
|
||||
buf[2] = 3; buf[3] = 0x01;
|
||||
buf[2] = 4; buf[3] = 0x01;
|
||||
buf[4] = 0x01; buf[5] = 0x00;
|
||||
buf[6] = 0x01; buf[7] = 0x00;
|
||||
uint32_t t = 1769904000UL;
|
||||
buf[8] = t & 0xFF; buf[9] = (t >> 8) & 0xFF;
|
||||
buf[10] = (t >> 16) & 0xFF; buf[11] = (t >> 24) & 0xFF;
|
||||
buf[12] = 0x01; buf[13] = 0; buf[14] = 0; buf[15] = 0x40; // bit 30
|
||||
buf[16] = 1;
|
||||
buf[17] = 0xA6; buf[18] = 0x0E;
|
||||
buf[19] = 0; buf[20] = 0; buf[21] = 0; buf[22] = 0; buf[23] = 0;
|
||||
uint32_t meter_t = 123456789UL;
|
||||
buf[8] = meter_t & 0xFF; buf[9] = (meter_t >> 8) & 0xFF;
|
||||
buf[10] = (meter_t >> 16) & 0xFF; buf[11] = (meter_t >> 24) & 0xFF;
|
||||
uint32_t utc_t = 1769904000UL;
|
||||
buf[12] = utc_t & 0xFF; buf[13] = (utc_t >> 8) & 0xFF;
|
||||
buf[14] = (utc_t >> 16) & 0xFF; buf[15] = (utc_t >> 24) & 0xFF;
|
||||
buf[16] = 0x01; buf[17] = 0; buf[18] = 0; buf[19] = 0x40; // bit 30
|
||||
buf[20] = 1;
|
||||
buf[21] = 0xA6; buf[22] = 0x0E;
|
||||
buf[23] = 0; buf[24] = 0; buf[25] = 0; buf[26] = 0; buf[27] = 0;
|
||||
|
||||
BatchInput out = {};
|
||||
TEST_ASSERT_FALSE(decode_batch(buf, 24, &out));
|
||||
TEST_ASSERT_FALSE(decode_batch(buf, 28, &out));
|
||||
}
|
||||
|
||||
// ---- uleb128_decode: negative tests ----
|
||||
@@ -308,7 +321,8 @@ static void test_decode_batch_byte_flip_fuzz() {
|
||||
BatchInput in = {};
|
||||
in.sender_id = 1;
|
||||
in.batch_id = 42;
|
||||
in.t_last = 1769904000UL;
|
||||
in.meter_t_last = 123456789UL;
|
||||
in.ts_utc_last = 1769904000UL;
|
||||
in.present_mask = 0x07; // bits 0-2
|
||||
in.n = 3;
|
||||
in.battery_mV = 3750;
|
||||
|
||||
Reference in New Issue
Block a user