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