test: align test mode with ACK/time flow and expose ack metrics
This commit is contained in:
@@ -50,6 +50,7 @@ struct MeterData {
|
||||
struct SenderStatus {
|
||||
MeterData last_data;
|
||||
uint32_t last_update_ts_utc;
|
||||
uint16_t last_acked_batch_id;
|
||||
bool has_data;
|
||||
};
|
||||
|
||||
|
||||
@@ -133,6 +133,7 @@ static void init_sender_statuses() {
|
||||
g_sender_statuses[i] = {};
|
||||
g_sender_statuses[i].has_data = false;
|
||||
g_sender_statuses[i].last_update_ts_utc = 0;
|
||||
g_sender_statuses[i].last_acked_batch_id = 0;
|
||||
g_sender_statuses[i].last_data.short_id = EXPECTED_SENDER_IDS[i];
|
||||
snprintf(g_sender_statuses[i].last_data.device_id, sizeof(g_sender_statuses[i].last_data.device_id), "dd3-%04X", EXPECTED_SENDER_IDS[i]);
|
||||
g_sender_faults_remote[i] = {};
|
||||
@@ -1056,6 +1057,7 @@ static void receiver_loop() {
|
||||
}
|
||||
|
||||
if (sender_idx >= 0) {
|
||||
g_sender_statuses[sender_idx].last_acked_batch_id = batch_id;
|
||||
web_server_set_last_batch(static_cast<uint8_t>(sender_idx), samples, count);
|
||||
for (size_t s = 0; s < count; ++s) {
|
||||
mqtt_publish_state(samples[s]);
|
||||
|
||||
+142
-6
@@ -12,8 +12,93 @@
|
||||
|
||||
static uint32_t g_last_test_ms = 0;
|
||||
static uint16_t g_test_code_counter = 1000;
|
||||
static uint16_t g_test_batch_id = 1;
|
||||
static uint16_t g_test_last_acked_batch_id = 0;
|
||||
static constexpr uint32_t TEST_SEND_INTERVAL_MS = 30000;
|
||||
|
||||
static void write_u16_be(uint8_t *dst, uint16_t value) {
|
||||
dst[0] = static_cast<uint8_t>((value >> 8) & 0xFF);
|
||||
dst[1] = static_cast<uint8_t>(value & 0xFF);
|
||||
}
|
||||
|
||||
static uint16_t read_u16_be(const uint8_t *src) {
|
||||
return static_cast<uint16_t>(src[0] << 8) | static_cast<uint16_t>(src[1]);
|
||||
}
|
||||
|
||||
static void write_u32_be(uint8_t *dst, uint32_t value) {
|
||||
dst[0] = static_cast<uint8_t>((value >> 24) & 0xFF);
|
||||
dst[1] = static_cast<uint8_t>((value >> 16) & 0xFF);
|
||||
dst[2] = static_cast<uint8_t>((value >> 8) & 0xFF);
|
||||
dst[3] = static_cast<uint8_t>(value & 0xFF);
|
||||
}
|
||||
|
||||
static uint32_t read_u32_be(const uint8_t *src) {
|
||||
return (static_cast<uint32_t>(src[0]) << 24) |
|
||||
(static_cast<uint32_t>(src[1]) << 16) |
|
||||
(static_cast<uint32_t>(src[2]) << 8) |
|
||||
static_cast<uint32_t>(src[3]);
|
||||
}
|
||||
|
||||
static uint32_t ack_window_ms() {
|
||||
uint32_t air_ms = lora_airtime_ms(lora_frame_size(LORA_ACK_DOWN_PAYLOAD_LEN));
|
||||
uint32_t window_ms = air_ms + 300;
|
||||
if (window_ms < 1200) {
|
||||
window_ms = 1200;
|
||||
}
|
||||
if (window_ms > 4000) {
|
||||
window_ms = 4000;
|
||||
}
|
||||
return window_ms;
|
||||
}
|
||||
|
||||
static bool receive_ack_for_batch(uint16_t batch_id, uint8_t &time_valid, uint32_t &ack_epoch, int16_t &rssi_dbm, float &snr_db) {
|
||||
LoraPacket ack_pkt = {};
|
||||
uint32_t window_ms = ack_window_ms();
|
||||
bool got_ack = lora_receive_window(ack_pkt, window_ms);
|
||||
if (!got_ack) {
|
||||
got_ack = lora_receive_window(ack_pkt, window_ms / 2);
|
||||
}
|
||||
if (!got_ack || ack_pkt.msg_kind != LoraMsgKind::AckDown || ack_pkt.payload_len < LORA_ACK_DOWN_PAYLOAD_LEN) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t ack_id = read_u16_be(&ack_pkt.payload[1]);
|
||||
if (ack_id != batch_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
time_valid = ack_pkt.payload[0] & 0x01;
|
||||
ack_epoch = read_u32_be(&ack_pkt.payload[3]);
|
||||
rssi_dbm = ack_pkt.rssi_dbm;
|
||||
snr_db = ack_pkt.snr_db;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void send_test_ack(uint16_t self_short_id, uint16_t batch_id, uint8_t &time_valid, uint32_t &ack_epoch) {
|
||||
ack_epoch = time_get_utc();
|
||||
time_valid = (time_is_synced() && ack_epoch >= MIN_ACCEPTED_EPOCH_UTC) ? 1 : 0;
|
||||
if (!time_valid) {
|
||||
ack_epoch = 0;
|
||||
}
|
||||
|
||||
LoraPacket ack = {};
|
||||
ack.msg_kind = LoraMsgKind::AckDown;
|
||||
ack.device_id_short = self_short_id;
|
||||
ack.payload_len = LORA_ACK_DOWN_PAYLOAD_LEN;
|
||||
ack.payload[0] = time_valid;
|
||||
write_u16_be(&ack.payload[1], batch_id);
|
||||
write_u32_be(&ack.payload[3], ack_epoch);
|
||||
|
||||
uint8_t repeats = ACK_REPEAT_COUNT == 0 ? 1 : ACK_REPEAT_COUNT;
|
||||
for (uint8_t i = 0; i < repeats; ++i) {
|
||||
lora_send(ack);
|
||||
if (i + 1 < repeats && ACK_REPEAT_DELAY_MS > 0) {
|
||||
delay(ACK_REPEAT_DELAY_MS);
|
||||
}
|
||||
}
|
||||
lora_receive_continuous();
|
||||
}
|
||||
|
||||
void test_sender_loop(uint16_t short_id, const char *device_id) {
|
||||
if (millis() - g_last_test_ms < TEST_SEND_INTERVAL_MS) {
|
||||
return;
|
||||
@@ -36,11 +121,13 @@ void test_sender_loop(uint16_t short_id, const char *device_id) {
|
||||
uint32_t now_utc = time_get_utc();
|
||||
uint32_t ts = now_utc > 0 ? now_utc : millis() / 1000;
|
||||
|
||||
StaticJsonDocument<128> doc;
|
||||
StaticJsonDocument<192> doc;
|
||||
doc["id"] = device_id;
|
||||
doc["role"] = "sender";
|
||||
doc["test_code"] = code;
|
||||
doc["ts"] = ts;
|
||||
doc["batch_id"] = g_test_batch_id;
|
||||
doc["last_acked"] = g_test_last_acked_batch_id;
|
||||
char bat_buf[8];
|
||||
snprintf(bat_buf, sizeof(bat_buf), "%.2f", data.battery_voltage_v);
|
||||
doc["bat_v"] = serialized(bat_buf);
|
||||
@@ -60,11 +147,32 @@ void test_sender_loop(uint16_t short_id, const char *device_id) {
|
||||
pkt.device_id_short = short_id;
|
||||
pkt.payload_len = json.length();
|
||||
memcpy(pkt.payload, json.c_str(), pkt.payload_len);
|
||||
lora_send(pkt);
|
||||
if (!lora_send(pkt)) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t time_valid = 0;
|
||||
uint32_t ack_epoch = 0;
|
||||
int16_t ack_rssi = 0;
|
||||
float ack_snr = 0.0f;
|
||||
if (receive_ack_for_batch(g_test_batch_id, time_valid, ack_epoch, ack_rssi, ack_snr)) {
|
||||
if (time_valid == 1 && ack_epoch >= MIN_ACCEPTED_EPOCH_UTC) {
|
||||
time_set_utc(ack_epoch);
|
||||
}
|
||||
g_test_last_acked_batch_id = g_test_batch_id;
|
||||
g_test_batch_id++;
|
||||
if (SERIAL_DEBUG_MODE) {
|
||||
Serial.printf("test ack: batch=%u time_valid=%u epoch=%lu rssi=%d snr=%.1f\n",
|
||||
static_cast<unsigned>(g_test_last_acked_batch_id),
|
||||
static_cast<unsigned>(time_valid),
|
||||
static_cast<unsigned long>(ack_epoch),
|
||||
static_cast<int>(ack_rssi),
|
||||
static_cast<double>(ack_snr));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void test_receiver_loop(SenderStatus *statuses, uint8_t count, uint16_t self_short_id) {
|
||||
(void)self_short_id;
|
||||
LoraPacket pkt = {};
|
||||
if (!lora_receive(pkt, 0)) {
|
||||
return;
|
||||
@@ -73,22 +181,28 @@ void test_receiver_loop(SenderStatus *statuses, uint8_t count, uint16_t self_sho
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t decompressed[160];
|
||||
uint8_t decompressed[192];
|
||||
if (pkt.payload_len >= sizeof(decompressed)) {
|
||||
return;
|
||||
}
|
||||
memcpy(decompressed, pkt.payload, pkt.payload_len);
|
||||
decompressed[pkt.payload_len] = '\0';
|
||||
|
||||
StaticJsonDocument<128> doc;
|
||||
StaticJsonDocument<192> doc;
|
||||
if (deserializeJson(doc, reinterpret_cast<const char *>(decompressed)) != DeserializationError::Ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char *id = doc["id"] | "";
|
||||
const char *code = doc["test_code"] | "";
|
||||
uint16_t batch_id = static_cast<uint16_t>(doc["batch_id"] | 0);
|
||||
uint32_t ts = doc["ts"] | 0;
|
||||
float bat_v = doc["bat_v"] | NAN;
|
||||
|
||||
uint8_t time_valid = 0;
|
||||
uint32_t ack_epoch = 0;
|
||||
send_test_ack(self_short_id, batch_id, time_valid, ack_epoch);
|
||||
|
||||
for (uint8_t i = 0; i < count; ++i) {
|
||||
if (strncmp(statuses[i].last_data.device_id, id, sizeof(statuses[i].last_data.device_id)) == 0) {
|
||||
display_set_test_code_for_sender(i, code);
|
||||
@@ -96,12 +210,34 @@ void test_receiver_loop(SenderStatus *statuses, uint8_t count, uint16_t self_sho
|
||||
statuses[i].last_data.battery_voltage_v = bat_v;
|
||||
statuses[i].last_data.battery_percent = battery_percent_from_voltage(bat_v);
|
||||
}
|
||||
statuses[i].last_data.link_valid = true;
|
||||
statuses[i].last_data.link_rssi_dbm = pkt.rssi_dbm;
|
||||
statuses[i].last_data.link_snr_db = pkt.snr_db;
|
||||
statuses[i].last_data.ts_utc = ts;
|
||||
statuses[i].last_acked_batch_id = batch_id;
|
||||
statuses[i].has_data = true;
|
||||
statuses[i].last_update_ts_utc = time_get_utc();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
mqtt_publish_test(id, String(reinterpret_cast<const char *>(decompressed)));
|
||||
StaticJsonDocument<256> mqtt_doc;
|
||||
mqtt_doc["id"] = id;
|
||||
mqtt_doc["role"] = "receiver";
|
||||
mqtt_doc["test_code"] = code;
|
||||
mqtt_doc["ts"] = ts;
|
||||
mqtt_doc["batch_id"] = batch_id;
|
||||
mqtt_doc["acked_batch_id"] = batch_id;
|
||||
if (!isnan(bat_v)) {
|
||||
mqtt_doc["bat_v"] = bat_v;
|
||||
}
|
||||
mqtt_doc["rssi"] = pkt.rssi_dbm;
|
||||
mqtt_doc["snr"] = pkt.snr_db;
|
||||
mqtt_doc["time_valid"] = time_valid;
|
||||
mqtt_doc["ack_epoch"] = ack_epoch;
|
||||
|
||||
String mqtt_payload;
|
||||
serializeJson(mqtt_doc, mqtt_payload);
|
||||
mqtt_publish_test(id, mqtt_payload);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -363,6 +363,7 @@ static String render_sender_block(const SenderStatus &status) {
|
||||
s += " RSSI:" + String(status.last_data.link_rssi_dbm) + " SNR:" + String(status.last_data.link_snr_db, 1);
|
||||
}
|
||||
if (status.has_data) {
|
||||
s += " ack:" + String(status.last_acked_batch_id);
|
||||
s += " err_tx:" + String(status.last_data.err_lora_tx);
|
||||
s += " err_last:" + String(static_cast<uint8_t>(status.last_data.last_error));
|
||||
s += " (" + String(fault_text(status.last_data.last_error)) + ")";
|
||||
|
||||
Reference in New Issue
Block a user