mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-01 20:09:17 +00:00
simplify app layer: dedup response/telemetry/JSON builders
- CompanionMesh: sendPacketSent() helper, collapse sendFloodScoped
overloads, share self-telemetry LPP builder
- Mesh: shared computeAdaptive{Flood,Direct}Delay (was duplicated in
Companion + Repeater)
- ObserverMesh/RepeaterMesh: shared helpers/MeshcoreJson.h builders;
drop dead sign_input_len
This commit is contained in:
+157
-321
@@ -242,9 +242,8 @@ void CompanionMesh::sendFloodScoped(const TransportKey &scope, mesh::Packet *pkt
|
||||
}
|
||||
}
|
||||
|
||||
void CompanionMesh::sendFloodScoped(const ContactInfo &recipient, mesh::Packet *pkt, uint32_t delay_millis)
|
||||
void CompanionMesh::sendFloodScopedDefault(mesh::Packet *pkt, uint32_t delay_millis)
|
||||
{
|
||||
/* TODO: dynamic _send_scope, depending on recipient and current 'home' Region */
|
||||
if (_send_scope_force_unscoped) {
|
||||
TransportKey no_scope;
|
||||
memset(no_scope.key, 0, sizeof(no_scope.key));
|
||||
@@ -259,21 +258,18 @@ void CompanionMesh::sendFloodScoped(const ContactInfo &recipient, mesh::Packet *
|
||||
sendFloodScoped(scope, pkt, delay_millis);
|
||||
}
|
||||
|
||||
void CompanionMesh::sendFloodScoped(const ContactInfo &recipient, mesh::Packet *pkt, uint32_t delay_millis)
|
||||
{
|
||||
/* TODO: dynamic _send_scope, depending on recipient and current 'home' Region */
|
||||
(void)recipient;
|
||||
sendFloodScopedDefault(pkt, delay_millis);
|
||||
}
|
||||
|
||||
void CompanionMesh::sendFloodScoped(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t delay_millis)
|
||||
{
|
||||
/* TODO: have per-channel send_scope */
|
||||
if (_send_scope_force_unscoped) {
|
||||
TransportKey no_scope;
|
||||
memset(no_scope.key, 0, sizeof(no_scope.key));
|
||||
sendFloodScoped(no_scope, pkt, delay_millis);
|
||||
return;
|
||||
}
|
||||
|
||||
TransportKey default_scope;
|
||||
memcpy(default_scope.key, prefs.default_scope_key, sizeof(default_scope.key));
|
||||
|
||||
const TransportKey &scope = _send_scope.isNull() ? default_scope : _send_scope;
|
||||
sendFloodScoped(scope, pkt, delay_millis);
|
||||
(void)channel;
|
||||
sendFloodScopedDefault(pkt, delay_millis);
|
||||
}
|
||||
|
||||
bool CompanionMesh::onContactPathRecv(ContactInfo &from, uint8_t *in_path, uint8_t in_path_len,
|
||||
@@ -392,6 +388,16 @@ void CompanionMesh::sendPacketError(uint8_t code)
|
||||
writeFrame(rsp, sizeof(rsp));
|
||||
}
|
||||
|
||||
void CompanionMesh::sendPacketSent(uint8_t result, uint32_t tag, uint32_t est_timeout)
|
||||
{
|
||||
uint8_t rsp[10];
|
||||
rsp[0] = PACKET_SENT;
|
||||
rsp[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0;
|
||||
put_le32(&rsp[2], tag);
|
||||
put_le32(&rsp[6], est_timeout);
|
||||
writeFrame(rsp, sizeof(rsp));
|
||||
}
|
||||
|
||||
void CompanionMesh::sendPush(uint8_t code, const uint8_t *data, size_t len)
|
||||
{
|
||||
LOG_DBG("sendPush: code=0x%02x len=%u _push_cb=%s", code, (unsigned)len, _push_cb ? "set" : "NULL");
|
||||
@@ -1009,6 +1015,128 @@ void CompanionMesh::onChannelDataRecv(const mesh::GroupChannel &channel, mesh::P
|
||||
sendPush(PUSH_CODE_MSG_WAITING);
|
||||
}
|
||||
|
||||
int CompanionMesh::appendSelfTelemetry(uint8_t *reply, uint8_t permissions)
|
||||
{
|
||||
int i = 0;
|
||||
const uint8_t CH_SELF = 1;
|
||||
|
||||
// Battery voltage: [channel][LPP_VOLTAGE=116][2-byte 0.01V big-endian]
|
||||
uint16_t batt_mv = _batt_cb ? _batt_cb() : 0;
|
||||
reply[i++] = CH_SELF;
|
||||
reply[i++] = 116; // LPP_VOLTAGE
|
||||
uint16_t batt_scaled = batt_mv / 10;
|
||||
reply[i++] = (batt_scaled >> 8) & 0xFF;
|
||||
reply[i++] = batt_scaled & 0xFF;
|
||||
|
||||
// GPS position if authorized and available
|
||||
if (permissions & TELEM_PERM_LOCATION) {
|
||||
struct gps_position pos;
|
||||
if (gps_is_available() && gps_get_last_known_position(&pos)) {
|
||||
reply[i++] = CH_SELF;
|
||||
reply[i++] = 136; // LPP_GPS
|
||||
int32_t lat = (int32_t)(pos.latitude_ndeg / 100000);
|
||||
int32_t lon = (int32_t)(pos.longitude_ndeg / 100000);
|
||||
int32_t alt = pos.altitude_mm / 10;
|
||||
reply[i++] = (lat >> 16) & 0xFF;
|
||||
reply[i++] = (lat >> 8) & 0xFF;
|
||||
reply[i++] = lat & 0xFF;
|
||||
reply[i++] = (lon >> 16) & 0xFF;
|
||||
reply[i++] = (lon >> 8) & 0xFF;
|
||||
reply[i++] = lon & 0xFF;
|
||||
reply[i++] = (alt >> 16) & 0xFF;
|
||||
reply[i++] = (alt >> 8) & 0xFF;
|
||||
reply[i++] = alt & 0xFF;
|
||||
} else if (prefs.node_lat != 0 || prefs.node_lon != 0) {
|
||||
// Use configured position
|
||||
reply[i++] = CH_SELF;
|
||||
reply[i++] = 136; // LPP_GPS
|
||||
int32_t lat = (int32_t)(prefs.node_lat * 10000);
|
||||
int32_t lon = (int32_t)(prefs.node_lon * 10000);
|
||||
int32_t alt = 0;
|
||||
reply[i++] = (lat >> 16) & 0xFF;
|
||||
reply[i++] = (lat >> 8) & 0xFF;
|
||||
reply[i++] = lat & 0xFF;
|
||||
reply[i++] = (lon >> 16) & 0xFF;
|
||||
reply[i++] = (lon >> 8) & 0xFF;
|
||||
reply[i++] = lon & 0xFF;
|
||||
reply[i++] = (alt >> 16) & 0xFF;
|
||||
reply[i++] = (alt >> 8) & 0xFF;
|
||||
reply[i++] = alt & 0xFF;
|
||||
}
|
||||
}
|
||||
|
||||
// Environment sensors if authorized and available
|
||||
if (permissions & TELEM_PERM_ENVIRONMENT) {
|
||||
struct env_data env;
|
||||
if (env_sensors_read(&env) == 0) {
|
||||
if (env.has_temperature) {
|
||||
reply[i++] = CH_SELF;
|
||||
reply[i++] = LPP_TEMPERATURE;
|
||||
int16_t temp = (int16_t)(env.temperature_c * 10);
|
||||
reply[i++] = (temp >> 8) & 0xFF;
|
||||
reply[i++] = temp & 0xFF;
|
||||
} else if (env.has_mcu_temperature) {
|
||||
// MCU die temp as fallback when no external sensor
|
||||
reply[i++] = CH_SELF;
|
||||
reply[i++] = LPP_TEMPERATURE;
|
||||
int16_t temp = (int16_t)(env.mcu_temperature_c * 10);
|
||||
reply[i++] = (temp >> 8) & 0xFF;
|
||||
reply[i++] = temp & 0xFF;
|
||||
}
|
||||
if (env.has_humidity) {
|
||||
reply[i++] = CH_SELF;
|
||||
reply[i++] = LPP_RELATIVE_HUMIDITY;
|
||||
reply[i++] = (uint8_t)(env.humidity_pct * 2);
|
||||
}
|
||||
if (env.has_pressure) {
|
||||
reply[i++] = CH_SELF;
|
||||
reply[i++] = LPP_BAROMETRIC_PRESSURE;
|
||||
uint16_t press = (uint16_t)(env.pressure_hpa * 10);
|
||||
reply[i++] = (press >> 8) & 0xFF;
|
||||
reply[i++] = press & 0xFF;
|
||||
}
|
||||
}
|
||||
|
||||
// Power monitor telemetry (INA219/INA3221/ina2xx)
|
||||
if (power_sensors_available()) {
|
||||
struct power_data pwr;
|
||||
if (power_sensors_read(&pwr) == 0) {
|
||||
uint8_t ch = CH_SELF + 1;
|
||||
for (int j = 0; j < pwr.num_channels; j++) {
|
||||
if (pwr.channels[j].valid) {
|
||||
// Voltage: [ch][LPP_VOLTAGE=116][2-byte 0.01V]
|
||||
reply[i++] = ch;
|
||||
reply[i++] = 116;
|
||||
uint16_t v = (uint16_t)(pwr.channels[j].voltage_v * 100);
|
||||
reply[i++] = (v >> 8) & 0xFF;
|
||||
reply[i++] = v & 0xFF;
|
||||
// Current: [ch][LPP_CURRENT=117][2-byte 0.001A]
|
||||
reply[i++] = ch;
|
||||
reply[i++] = 117;
|
||||
uint16_t c = (uint16_t)(pwr.channels[j].current_a * 1000);
|
||||
reply[i++] = (c >> 8) & 0xFF;
|
||||
reply[i++] = c & 0xFF;
|
||||
// Power: [ch][LPP_POWER=128][2-byte 1W]
|
||||
reply[i++] = ch;
|
||||
reply[i++] = 128;
|
||||
uint16_t p = (uint16_t)(pwr.channels[j].power_w);
|
||||
reply[i++] = (p >> 8) & 0xFF;
|
||||
reply[i++] = p & 0xFF;
|
||||
ch++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger GPS wake for fresh fix on next request
|
||||
if (gps_is_available() && gps_is_enabled()) {
|
||||
gps_request_fresh_fix();
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
uint8_t CompanionMesh::onContactRequest(const ContactInfo &contact, uint32_t sender_timestamp,
|
||||
const uint8_t *data, uint8_t len, uint8_t *reply)
|
||||
{
|
||||
@@ -1047,130 +1175,11 @@ uint8_t CompanionMesh::onContactRequest(const ContactInfo &contact, uint32_t sen
|
||||
if (permissions & TELEM_PERM_BASE) {
|
||||
LOG_INF("onContactRequest: telemetry authorized (perms=0x%02x)", permissions);
|
||||
|
||||
// Build Cayenne LPP telemetry response
|
||||
int i = 0;
|
||||
|
||||
// Reflect sender_timestamp back as tag (4 bytes)
|
||||
// Build Cayenne LPP telemetry response: reflect sender_timestamp
|
||||
// back as a 4-byte tag, then append battery/GPS/env/power.
|
||||
memcpy(reply, &sender_timestamp, 4);
|
||||
i += 4;
|
||||
|
||||
const uint8_t CH_SELF = 1;
|
||||
|
||||
// Battery voltage: [channel][LPP_VOLTAGE=116][2-byte 0.01V big-endian]
|
||||
uint16_t batt_mv = _batt_cb ? _batt_cb() : 0;
|
||||
reply[i++] = CH_SELF;
|
||||
reply[i++] = 116; // LPP_VOLTAGE
|
||||
uint16_t batt_scaled = batt_mv / 10;
|
||||
reply[i++] = (batt_scaled >> 8) & 0xFF;
|
||||
reply[i++] = batt_scaled & 0xFF;
|
||||
|
||||
// GPS position if authorized and available
|
||||
if (permissions & TELEM_PERM_LOCATION) {
|
||||
struct gps_position pos;
|
||||
if (gps_is_available() && gps_get_last_known_position(&pos)) {
|
||||
reply[i++] = CH_SELF;
|
||||
reply[i++] = 136; // LPP_GPS
|
||||
int32_t lat = (int32_t)(pos.latitude_ndeg / 100000);
|
||||
int32_t lon = (int32_t)(pos.longitude_ndeg / 100000);
|
||||
int32_t alt = pos.altitude_mm / 10;
|
||||
reply[i++] = (lat >> 16) & 0xFF;
|
||||
reply[i++] = (lat >> 8) & 0xFF;
|
||||
reply[i++] = lat & 0xFF;
|
||||
reply[i++] = (lon >> 16) & 0xFF;
|
||||
reply[i++] = (lon >> 8) & 0xFF;
|
||||
reply[i++] = lon & 0xFF;
|
||||
reply[i++] = (alt >> 16) & 0xFF;
|
||||
reply[i++] = (alt >> 8) & 0xFF;
|
||||
reply[i++] = alt & 0xFF;
|
||||
} else if (prefs.node_lat != 0 || prefs.node_lon != 0) {
|
||||
// Use configured position
|
||||
reply[i++] = CH_SELF;
|
||||
reply[i++] = 136; // LPP_GPS
|
||||
int32_t lat = (int32_t)(prefs.node_lat * 10000);
|
||||
int32_t lon = (int32_t)(prefs.node_lon * 10000);
|
||||
int32_t alt = 0;
|
||||
reply[i++] = (lat >> 16) & 0xFF;
|
||||
reply[i++] = (lat >> 8) & 0xFF;
|
||||
reply[i++] = lat & 0xFF;
|
||||
reply[i++] = (lon >> 16) & 0xFF;
|
||||
reply[i++] = (lon >> 8) & 0xFF;
|
||||
reply[i++] = lon & 0xFF;
|
||||
reply[i++] = (alt >> 16) & 0xFF;
|
||||
reply[i++] = (alt >> 8) & 0xFF;
|
||||
reply[i++] = alt & 0xFF;
|
||||
}
|
||||
}
|
||||
|
||||
// Environment sensors if authorized and available
|
||||
if (permissions & TELEM_PERM_ENVIRONMENT) {
|
||||
struct env_data env;
|
||||
if (env_sensors_read(&env) == 0) {
|
||||
if (env.has_temperature) {
|
||||
reply[i++] = CH_SELF;
|
||||
reply[i++] = LPP_TEMPERATURE;
|
||||
int16_t temp = (int16_t)(env.temperature_c * 10);
|
||||
reply[i++] = (temp >> 8) & 0xFF;
|
||||
reply[i++] = temp & 0xFF;
|
||||
} else if (env.has_mcu_temperature) {
|
||||
// MCU die temp as fallback when no external sensor
|
||||
reply[i++] = CH_SELF;
|
||||
reply[i++] = LPP_TEMPERATURE;
|
||||
int16_t temp = (int16_t)(env.mcu_temperature_c * 10);
|
||||
reply[i++] = (temp >> 8) & 0xFF;
|
||||
reply[i++] = temp & 0xFF;
|
||||
}
|
||||
if (env.has_humidity) {
|
||||
reply[i++] = CH_SELF;
|
||||
reply[i++] = LPP_RELATIVE_HUMIDITY;
|
||||
reply[i++] = (uint8_t)(env.humidity_pct * 2);
|
||||
}
|
||||
if (env.has_pressure) {
|
||||
reply[i++] = CH_SELF;
|
||||
reply[i++] = LPP_BAROMETRIC_PRESSURE;
|
||||
uint16_t press = (uint16_t)(env.pressure_hpa * 10);
|
||||
reply[i++] = (press >> 8) & 0xFF;
|
||||
reply[i++] = press & 0xFF;
|
||||
}
|
||||
}
|
||||
|
||||
// Power monitor telemetry (INA219/INA3221/ina2xx)
|
||||
if (power_sensors_available()) {
|
||||
struct power_data pwr;
|
||||
if (power_sensors_read(&pwr) == 0) {
|
||||
uint8_t ch = CH_SELF + 1;
|
||||
for (int j = 0; j < pwr.num_channels; j++) {
|
||||
if (pwr.channels[j].valid) {
|
||||
// Voltage: [ch][LPP_VOLTAGE=116][2-byte 0.01V]
|
||||
reply[i++] = ch;
|
||||
reply[i++] = 116;
|
||||
uint16_t v = (uint16_t)(pwr.channels[j].voltage_v * 100);
|
||||
reply[i++] = (v >> 8) & 0xFF;
|
||||
reply[i++] = v & 0xFF;
|
||||
// Current: [ch][LPP_CURRENT=117][2-byte 0.001A]
|
||||
reply[i++] = ch;
|
||||
reply[i++] = 117;
|
||||
uint16_t c = (uint16_t)(pwr.channels[j].current_a * 1000);
|
||||
reply[i++] = (c >> 8) & 0xFF;
|
||||
reply[i++] = c & 0xFF;
|
||||
// Power: [ch][LPP_POWER=128][2-byte 1W]
|
||||
reply[i++] = ch;
|
||||
reply[i++] = 128;
|
||||
uint16_t p = (uint16_t)(pwr.channels[j].power_w);
|
||||
reply[i++] = (p >> 8) & 0xFF;
|
||||
reply[i++] = p & 0xFF;
|
||||
ch++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger GPS wake for fresh fix on next request
|
||||
if (gps_is_available() && gps_is_enabled()) {
|
||||
gps_request_fresh_fix();
|
||||
}
|
||||
|
||||
return i;
|
||||
int n = appendSelfTelemetry(&reply[4], permissions);
|
||||
return 4 + n;
|
||||
} else {
|
||||
LOG_INF("onContactRequest: telemetry denied for contact");
|
||||
}
|
||||
@@ -1448,32 +1457,12 @@ void CompanionMesh::onRawDataRecv(mesh::Packet *packet)
|
||||
|
||||
uint32_t CompanionMesh::getRetransmitDelay(const mesh::Packet *packet)
|
||||
{
|
||||
float factor = getContentionTracker().getFloodDelayFactor();
|
||||
uint32_t airtime = _radio->getEstAirtimeFor(
|
||||
packet->getPathByteLen() + packet->payload_len + 2);
|
||||
uint32_t max_jitter = (uint32_t)(5 * airtime * factor);
|
||||
/* Airtime-scaled ceiling: never exceed ~6 airtimes of spread. */
|
||||
uint32_t airtime_cap = 6 * airtime;
|
||||
if (max_jitter > airtime_cap) max_jitter = airtime_cap;
|
||||
/* Absolute cap: avoid excessive latency in very dense areas.
|
||||
* Reactive backoff will fine-tune further if needed. */
|
||||
if (max_jitter > 2000) max_jitter = 2000;
|
||||
/* Floor: give downstream nodes time to finish RX processing
|
||||
* and return to RX mode before we TX (~20ms settle) */
|
||||
return 20 + getRNG()->nextInt(0, max_jitter + 1);
|
||||
return computeAdaptiveFloodDelay(packet);
|
||||
}
|
||||
|
||||
uint32_t CompanionMesh::getDirectRetransmitDelay(const mesh::Packet *packet)
|
||||
{
|
||||
uint32_t airtime = _radio->getEstAirtimeFor(
|
||||
packet->getPathByteLen() + packet->payload_len + 2);
|
||||
/* Jitter around Arduino direct factor 0.3 using a per-packet factor
|
||||
* in the range [0.25, 0.40]. */
|
||||
uint32_t factor_milli = (uint32_t)getRNG()->nextInt(250, 401);
|
||||
uint32_t max_jitter = (airtime * factor_milli) / 1000;
|
||||
/* Floor: give downstream nodes time to finish RX processing
|
||||
* and return to RX mode before we TX (~20ms settle + jitter) */
|
||||
return 20 + getRNG()->nextInt(0, max_jitter + 1);
|
||||
return computeAdaptiveDirectDelay(packet);
|
||||
}
|
||||
|
||||
uint32_t CompanionMesh::getInitialFloodJitter(const mesh::Packet *packet)
|
||||
@@ -2002,12 +1991,7 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len)
|
||||
}
|
||||
|
||||
// Response: RESP_CODE_SENT + is_flood(1) + expected_ack(4) + est_timeout(4)
|
||||
uint8_t rsp[10];
|
||||
rsp[0] = PACKET_SENT;
|
||||
rsp[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0;
|
||||
put_le32(&rsp[2], expected_ack);
|
||||
put_le32(&rsp[6], est_timeout);
|
||||
writeFrame(rsp, sizeof(rsp));
|
||||
sendPacketSent(result, expected_ack, est_timeout);
|
||||
} else {
|
||||
sendPacketError(ERR_TABLE_FULL);
|
||||
}
|
||||
@@ -2528,12 +2512,7 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len)
|
||||
* clear the other pending fields manually — clearPendingReqs() would wipe it. */
|
||||
_pending_status = _pending_telemetry = _pending_discovery = _pending_req = 0;
|
||||
LOG_DBG("CMD_SEND_LOGIN: _pending_login set to %08x", _pending_login);
|
||||
uint8_t rsp[10];
|
||||
rsp[0] = PACKET_SENT;
|
||||
rsp[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0;
|
||||
memcpy(&rsp[2], &_pending_login, 4);
|
||||
put_le32(&rsp[6], est_timeout);
|
||||
writeFrame(rsp, sizeof(rsp));
|
||||
sendPacketSent(result, _pending_login, est_timeout);
|
||||
} else {
|
||||
sendPacketError(ERR_TABLE_FULL);
|
||||
}
|
||||
@@ -2554,12 +2533,7 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len)
|
||||
if (result != MSG_SEND_FAILED) {
|
||||
clearPendingReqs();
|
||||
memcpy(&_pending_status, contact->id.pub_key, 4); // legacy matching scheme
|
||||
uint8_t rsp[10];
|
||||
rsp[0] = PACKET_SENT;
|
||||
rsp[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0;
|
||||
put_le32(&rsp[2], tag);
|
||||
put_le32(&rsp[6], est_timeout);
|
||||
writeFrame(rsp, sizeof(rsp));
|
||||
sendPacketSent(result, tag, est_timeout);
|
||||
} else {
|
||||
sendPacketError(ERR_BAD_STATE);
|
||||
}
|
||||
@@ -2606,127 +2580,9 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len)
|
||||
memcpy(&rsp[i], self_id.pub_key, 6);
|
||||
i += 6; // pubkey prefix
|
||||
|
||||
// Channel 1 = TELEM_CHANNEL_SELF
|
||||
const uint8_t CH_SELF = 1;
|
||||
|
||||
// Battery voltage: [channel][LPP_VOLTAGE=116][2-byte 0.01V big-endian]
|
||||
uint16_t batt_mv = _batt_cb ? _batt_cb() : 0;
|
||||
rsp[i++] = CH_SELF;
|
||||
rsp[i++] = 116; // LPP_VOLTAGE
|
||||
uint16_t batt_scaled = batt_mv / 10; // 0.01V resolution
|
||||
rsp[i++] = (batt_scaled >> 8) & 0xFF; // Big-endian
|
||||
rsp[i++] = batt_scaled & 0xFF;
|
||||
|
||||
// GPS position if available: [channel][LPP_GPS=136][3-byte lat][3-byte lon][3-byte alt]
|
||||
// Use last known position even when GPS is sleeping (power-save mode)
|
||||
struct gps_position pos;
|
||||
if (gps_is_available() && gps_get_last_known_position(&pos)) {
|
||||
rsp[i++] = CH_SELF;
|
||||
rsp[i++] = 136; // LPP_GPS
|
||||
// Lat/lon in 0.0001 degrees (signed 24-bit)
|
||||
int32_t lat = (int32_t)(pos.latitude_ndeg / 100000); // nano-degrees to 0.0001 degrees
|
||||
int32_t lon = (int32_t)(pos.longitude_ndeg / 100000);
|
||||
int32_t alt = pos.altitude_mm / 10; // mm to 0.01m
|
||||
rsp[i++] = (lat >> 16) & 0xFF;
|
||||
rsp[i++] = (lat >> 8) & 0xFF;
|
||||
rsp[i++] = lat & 0xFF;
|
||||
rsp[i++] = (lon >> 16) & 0xFF;
|
||||
rsp[i++] = (lon >> 8) & 0xFF;
|
||||
rsp[i++] = lon & 0xFF;
|
||||
rsp[i++] = (alt >> 16) & 0xFF;
|
||||
rsp[i++] = (alt >> 8) & 0xFF;
|
||||
rsp[i++] = alt & 0xFF;
|
||||
} else if (prefs.node_lat != 0 || prefs.node_lon != 0) {
|
||||
// Use configured position
|
||||
rsp[i++] = CH_SELF;
|
||||
rsp[i++] = 136; // LPP_GPS
|
||||
int32_t lat = (int32_t)(prefs.node_lat * 10000);
|
||||
int32_t lon = (int32_t)(prefs.node_lon * 10000);
|
||||
int32_t alt = 0;
|
||||
rsp[i++] = (lat >> 16) & 0xFF;
|
||||
rsp[i++] = (lat >> 8) & 0xFF;
|
||||
rsp[i++] = lat & 0xFF;
|
||||
rsp[i++] = (lon >> 16) & 0xFF;
|
||||
rsp[i++] = (lon >> 8) & 0xFF;
|
||||
rsp[i++] = lon & 0xFF;
|
||||
rsp[i++] = (alt >> 16) & 0xFF;
|
||||
rsp[i++] = (alt >> 8) & 0xFF;
|
||||
rsp[i++] = alt & 0xFF;
|
||||
}
|
||||
|
||||
// Environment sensors
|
||||
{
|
||||
struct env_data env;
|
||||
if (env_sensors_read(&env) == 0) {
|
||||
// Temperature: [channel][LPP_TEMPERATURE=103][2-byte 0.1C signed big-endian]
|
||||
if (env.has_temperature) {
|
||||
rsp[i++] = CH_SELF;
|
||||
rsp[i++] = LPP_TEMPERATURE;
|
||||
int16_t temp = (int16_t)(env.temperature_c * 10);
|
||||
rsp[i++] = (temp >> 8) & 0xFF;
|
||||
rsp[i++] = temp & 0xFF;
|
||||
} else if (env.has_mcu_temperature) {
|
||||
// MCU die temp as fallback when no external sensor
|
||||
rsp[i++] = CH_SELF;
|
||||
rsp[i++] = LPP_TEMPERATURE;
|
||||
int16_t temp = (int16_t)(env.mcu_temperature_c * 10);
|
||||
rsp[i++] = (temp >> 8) & 0xFF;
|
||||
rsp[i++] = temp & 0xFF;
|
||||
}
|
||||
// Humidity: [channel][LPP_RELATIVE_HUMIDITY=104][1-byte 0.5%]
|
||||
if (env.has_humidity) {
|
||||
rsp[i++] = CH_SELF;
|
||||
rsp[i++] = LPP_RELATIVE_HUMIDITY;
|
||||
rsp[i++] = (uint8_t)(env.humidity_pct * 2);
|
||||
}
|
||||
// Pressure: [channel][LPP_BAROMETRIC_PRESSURE=115][2-byte 0.1hPa big-endian]
|
||||
if (env.has_pressure) {
|
||||
rsp[i++] = CH_SELF;
|
||||
rsp[i++] = LPP_BAROMETRIC_PRESSURE;
|
||||
uint16_t press = (uint16_t)(env.pressure_hpa * 10);
|
||||
rsp[i++] = (press >> 8) & 0xFF;
|
||||
rsp[i++] = press & 0xFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Power monitor telemetry (INA219/INA3221/ina2xx)
|
||||
if (power_sensors_available()) {
|
||||
struct power_data pwr;
|
||||
if (power_sensors_read(&pwr) == 0) {
|
||||
uint8_t ch = CH_SELF + 1;
|
||||
for (int j = 0; j < pwr.num_channels; j++) {
|
||||
if (pwr.channels[j].valid) {
|
||||
// Voltage: [ch][LPP_VOLTAGE=116][2-byte 0.01V]
|
||||
rsp[i++] = ch;
|
||||
rsp[i++] = 116;
|
||||
uint16_t v = (uint16_t)(pwr.channels[j].voltage_v * 100);
|
||||
rsp[i++] = (v >> 8) & 0xFF;
|
||||
rsp[i++] = v & 0xFF;
|
||||
// Current: [ch][LPP_CURRENT=117][2-byte 0.001A]
|
||||
rsp[i++] = ch;
|
||||
rsp[i++] = 117;
|
||||
uint16_t c = (uint16_t)(pwr.channels[j].current_a * 1000);
|
||||
rsp[i++] = (c >> 8) & 0xFF;
|
||||
rsp[i++] = c & 0xFF;
|
||||
// Power: [ch][LPP_POWER=128][2-byte 1W]
|
||||
rsp[i++] = ch;
|
||||
rsp[i++] = 128;
|
||||
uint16_t p = (uint16_t)(pwr.channels[j].power_w);
|
||||
rsp[i++] = (p >> 8) & 0xFF;
|
||||
rsp[i++] = p & 0xFF;
|
||||
ch++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Self-telemetry is unconditional (all permission bits set).
|
||||
i += appendSelfTelemetry(&rsp[i], TELEM_PERM_BASE | TELEM_PERM_LOCATION | TELEM_PERM_ENVIRONMENT);
|
||||
writeFrame(rsp, i);
|
||||
|
||||
// Trigger GPS wake so next telemetry request has fresh location
|
||||
if (gps_is_available() && gps_is_enabled()) {
|
||||
gps_request_fresh_fix();
|
||||
}
|
||||
} else if (len >= 4 + PUB_KEY_SIZE) {
|
||||
// Contact telemetry request: [cmd][3 reserved bytes][32-byte pubkey]
|
||||
ContactInfo *contact = lookupContactByPubKey(&data[4], PUB_KEY_SIZE);
|
||||
@@ -2736,12 +2592,7 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len)
|
||||
if (result != MSG_SEND_FAILED) {
|
||||
clearPendingReqs();
|
||||
_pending_telemetry = tag;
|
||||
uint8_t rsp[10];
|
||||
rsp[0] = PACKET_SENT;
|
||||
rsp[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0;
|
||||
put_le32(&rsp[2], tag);
|
||||
put_le32(&rsp[6], est_timeout);
|
||||
writeFrame(rsp, sizeof(rsp));
|
||||
sendPacketSent(result, tag, est_timeout);
|
||||
} else {
|
||||
sendPacketError(ERR_BAD_STATE);
|
||||
}
|
||||
@@ -2766,12 +2617,7 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len)
|
||||
if (result != MSG_SEND_FAILED) {
|
||||
clearPendingReqs();
|
||||
_pending_req = tag;
|
||||
uint8_t rsp[10];
|
||||
rsp[0] = PACKET_SENT;
|
||||
rsp[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0;
|
||||
put_le32(&rsp[2], tag);
|
||||
put_le32(&rsp[6], est_timeout);
|
||||
writeFrame(rsp, sizeof(rsp));
|
||||
sendPacketSent(result, tag, est_timeout);
|
||||
} else {
|
||||
sendPacketError(ERR_BAD_STATE);
|
||||
}
|
||||
@@ -2805,12 +2651,7 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len)
|
||||
if (result != MSG_SEND_FAILED) {
|
||||
clearPendingReqs();
|
||||
_pending_discovery = tag;
|
||||
uint8_t rsp[10];
|
||||
rsp[0] = PACKET_SENT;
|
||||
rsp[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0;
|
||||
put_le32(&rsp[2], tag);
|
||||
put_le32(&rsp[6], est_timeout);
|
||||
writeFrame(rsp, sizeof(rsp));
|
||||
sendPacketSent(result, tag, est_timeout);
|
||||
} else {
|
||||
sendPacketError(ERR_BAD_STATE);
|
||||
}
|
||||
@@ -3118,12 +2959,7 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len)
|
||||
if (result != MSG_SEND_FAILED) {
|
||||
clearPendingReqs();
|
||||
_pending_req = tag;
|
||||
uint8_t rsp[10];
|
||||
rsp[0] = PACKET_SENT;
|
||||
rsp[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; // is_flood
|
||||
put_le32(&rsp[2], tag);
|
||||
put_le32(&rsp[6], est_timeout);
|
||||
writeFrame(rsp, sizeof(rsp));
|
||||
sendPacketSent(result, tag, est_timeout);
|
||||
} else {
|
||||
sendPacketError(ERR_BAD_STATE);
|
||||
}
|
||||
|
||||
@@ -384,8 +384,19 @@ private:
|
||||
bool writeFrame(const uint8_t *data, size_t len);
|
||||
void sendPacketOk();
|
||||
void sendPacketError(uint8_t code);
|
||||
/* Emit the PACKET_SENT response: [PACKET_SENT][is_flood][tag:4][est_timeout:4]. */
|
||||
void sendPacketSent(uint8_t result, uint32_t tag, uint32_t est_timeout);
|
||||
void sendPush(uint8_t code, const uint8_t *data = nullptr, size_t len = 0);
|
||||
|
||||
/* Shared body for the recipient/channel sendFloodScoped overloads — both
|
||||
* resolve to the same default-scope logic (see the TODOs at each site). */
|
||||
void sendFloodScopedDefault(mesh::Packet *pkt, uint32_t delay_millis);
|
||||
|
||||
/* Append self-telemetry as Cayenne LPP into `out`, returning bytes written.
|
||||
* `permissions` gates the LOCATION and ENVIRONMENT sections (battery is
|
||||
* always included); pass all TELEM_PERM_* bits for unconditional output. */
|
||||
int appendSelfTelemetry(uint8_t *out, uint8_t permissions);
|
||||
|
||||
/** Serialize a ContactInfo into buf. If header != 0, prepend it.
|
||||
* Returns total bytes written. buf must be >= CONTACT_FRAME_SIZE. */
|
||||
static size_t serializeContact(uint8_t *buf, const ContactInfo &c, uint8_t header = 0);
|
||||
|
||||
+30
-129
@@ -9,6 +9,7 @@
|
||||
#include <mesh/Utils.h>
|
||||
#include <mesh/LoRaConfig.h>
|
||||
#include <adapters/radio/LoRaRadioBase.h>
|
||||
#include <helpers/MeshcoreJson.h>
|
||||
|
||||
#include <zephyr/logging/log.h>
|
||||
LOG_MODULE_REGISTER(zephcore_observer, CONFIG_ZEPHCORE_OBSERVER_LOG_LEVEL);
|
||||
@@ -96,14 +97,6 @@ void ObserverMesh::begin(RepeaterDataStore *store, struct ObserverCreds *creds)
|
||||
void ObserverMesh::buildStatusJson(const char *status, char *out, size_t out_size)
|
||||
{
|
||||
uint32_t now_epoch = _rtc ? _rtc->getCurrentTime() : 0;
|
||||
struct tm tm_now;
|
||||
time_t t = (time_t)now_epoch;
|
||||
gmtime_r(&t, &tm_now);
|
||||
|
||||
char ts_buf[48];
|
||||
snprintf(ts_buf, sizeof(ts_buf), "%04d-%02d-%02dT%02d:%02d:%02d.000000",
|
||||
tm_now.tm_year + 1900, tm_now.tm_mon + 1, tm_now.tm_mday,
|
||||
tm_now.tm_hour, tm_now.tm_min, tm_now.tm_sec);
|
||||
|
||||
char radio_buf[48];
|
||||
snprintf(radio_buf, sizeof(radio_buf), "%.3f,%.1f,%u,%u",
|
||||
@@ -117,32 +110,9 @@ void ObserverMesh::buildStatusJson(const char *status, char *out, size_t out_siz
|
||||
uptime_secs = 0;
|
||||
}
|
||||
|
||||
int noise_floor = ((LoRaRadioBase *)_radio)->getNoiseFloor();
|
||||
uint32_t recv_errors = ((LoRaRadioBase *)_radio)->getPacketsRecvErrors();
|
||||
|
||||
snprintf(out, out_size,
|
||||
"{"
|
||||
"\"status\":\"%s\","
|
||||
"\"timestamp\":\"%s\","
|
||||
"\"origin\":\"%s\","
|
||||
"\"origin_id\":\"%s\","
|
||||
"\"radio\":\"%s\","
|
||||
"\"model\":\"%s\","
|
||||
"\"firmware_version\":\"%s\","
|
||||
"\"client_version\":\"zephcoretomqtt/1.1\","
|
||||
"\"stats\":{"
|
||||
"\"battery_mv\":%u,"
|
||||
"\"uptime_secs\":%u,"
|
||||
"\"debug_flags\":%u,"
|
||||
"\"queue_len\":%u,"
|
||||
"\"noise_floor\":%d,"
|
||||
"\"tx_air_secs\":%u,"
|
||||
"\"rx_air_secs\":%u,"
|
||||
"\"recv_errors\":%u"
|
||||
"}"
|
||||
"}",
|
||||
struct MeshcoreStatusJson sj = {
|
||||
status,
|
||||
ts_buf,
|
||||
now_epoch,
|
||||
_prefs.node_name,
|
||||
_pubkey_hex,
|
||||
radio_buf,
|
||||
@@ -152,14 +122,16 @@ void ObserverMesh::buildStatusJson(const char *status, char *out, size_t out_siz
|
||||
"unknown",
|
||||
#endif
|
||||
FIRMWARE_VERSION,
|
||||
0u,
|
||||
0u, /* battery_mv (observer has none) */
|
||||
uptime_secs,
|
||||
0u,
|
||||
0u,
|
||||
noise_floor,
|
||||
0u,
|
||||
0u,
|
||||
recv_errors);
|
||||
0u, /* debug_flags */
|
||||
0u, /* queue_len */
|
||||
((LoRaRadioBase *)_radio)->getNoiseFloor(),
|
||||
0u, /* tx_air_secs */
|
||||
0u, /* rx_air_secs */
|
||||
((LoRaRadioBase *)_radio)->getPacketsRecvErrors(),
|
||||
};
|
||||
meshcore_build_status_json(out, out_size, &sj);
|
||||
}
|
||||
|
||||
void ObserverMesh::publishStatus(const char *status)
|
||||
@@ -212,65 +184,24 @@ void ObserverMesh::enqueuePacket(Packet *pkt)
|
||||
|
||||
/* Get current timestamp from RTC */
|
||||
uint32_t now_epoch = _rtc ? _rtc->getCurrentTime() : 0;
|
||||
struct tm tm_now;
|
||||
time_t t = (time_t)now_epoch;
|
||||
gmtime_r(&t, &tm_now);
|
||||
|
||||
/* Format ISO 8601 timestamp (microseconds always 0 — RTC has 1s resolution) */
|
||||
char ts_buf[48];
|
||||
snprintf(ts_buf, sizeof(ts_buf),
|
||||
"%04d-%02d-%02dT%02d:%02d:%02d.000000",
|
||||
tm_now.tm_year + 1900, tm_now.tm_mon + 1, tm_now.tm_mday,
|
||||
tm_now.tm_hour, tm_now.tm_min, tm_now.tm_sec);
|
||||
|
||||
/* Format time and date fields matching meshcoretomqtt */
|
||||
char time_buf[12], date_buf[32];
|
||||
snprintf(time_buf, sizeof(time_buf), "%02d:%02d:%02d",
|
||||
tm_now.tm_hour, tm_now.tm_min, tm_now.tm_sec);
|
||||
snprintf(date_buf, sizeof(date_buf), "%d/%d/%04d",
|
||||
tm_now.tm_mday, tm_now.tm_mon + 1, tm_now.tm_year + 1900);
|
||||
|
||||
/* Route letter: "F" = flood/transport-flood, "D" = direct/transport-direct */
|
||||
const char *route_str = pkt->isRouteDirect() ? "D" : "F";
|
||||
|
||||
/* score in meshcoretomqtt format: integer score * 1000 */
|
||||
int score_int = (int)(_last_score * 1000.0f);
|
||||
|
||||
/* Build JSON payload matching meshcoretomqtt packet format */
|
||||
static char json_buf[1024];
|
||||
int json_len = snprintf(json_buf, sizeof(json_buf),
|
||||
"{"
|
||||
"\"type\":\"PACKET\","
|
||||
"\"origin\":\"%s\","
|
||||
"\"origin_id\":\"%s\","
|
||||
"\"timestamp\":\"%s\","
|
||||
"\"direction\":\"rx\","
|
||||
"\"time\":\"%s\","
|
||||
"\"date\":\"%s\","
|
||||
"\"len\":\"%d\","
|
||||
"\"packet_type\":\"%u\","
|
||||
"\"route\":\"%s\","
|
||||
"\"payload_len\":\"%u\","
|
||||
"\"raw\":\"%s\","
|
||||
"\"SNR\":\"%d\","
|
||||
"\"RSSI\":\"%d\","
|
||||
"\"score\":\"%d\","
|
||||
"\"hash\":\"%s\""
|
||||
"}",
|
||||
struct MeshcorePacketJson pj = {
|
||||
_prefs.node_name,
|
||||
_pubkey_hex,
|
||||
ts_buf,
|
||||
time_buf,
|
||||
date_buf,
|
||||
now_epoch,
|
||||
_last_raw_len,
|
||||
(unsigned)pkt->getPayloadType(),
|
||||
route_str,
|
||||
pkt->isRouteDirect() ? "D" : "F",
|
||||
(unsigned)pkt->payload_len,
|
||||
raw_hex,
|
||||
(int)pkt->getSNR(),
|
||||
(int)_last_rssi,
|
||||
score_int,
|
||||
hash_hex);
|
||||
(int)(_last_score * 1000.0f),
|
||||
hash_hex,
|
||||
};
|
||||
int json_len = meshcore_build_packet_json(json_buf, sizeof(json_buf), &pj);
|
||||
|
||||
if (json_len < 0 || json_len >= (int)sizeof(json_buf)) {
|
||||
LOG_WRN("Packet JSON truncated (len=%d)", json_len);
|
||||
@@ -634,8 +565,6 @@ void ObserverMesh::publishSelfAdvert()
|
||||
int raw_len = pos;
|
||||
|
||||
/* Sign: pubkey + timestamp + appdata */
|
||||
int sign_input_len = PUB_KEY_SIZE + 4 + (appdata_off - ts_off - 4) + (raw_len - appdata_off);
|
||||
sign_input_len = PUB_KEY_SIZE + 4 + (raw_len - appdata_off);
|
||||
uint8_t sign_input[PUB_KEY_SIZE + 4 + 128]; /* generous upper bound */
|
||||
memcpy(sign_input, &raw[pubkey_off], PUB_KEY_SIZE);
|
||||
memcpy(sign_input + PUB_KEY_SIZE, &raw[ts_off], 4);
|
||||
@@ -643,54 +572,26 @@ void ObserverMesh::publishSelfAdvert()
|
||||
_self_id.sign(&raw[sig_off], sign_input, (size_t)(PUB_KEY_SIZE + 4 + raw_len - appdata_off));
|
||||
|
||||
/* ---- Build JSON ---- */
|
||||
uint32_t now_epoch = now_ts;
|
||||
struct tm tm_now;
|
||||
time_t t = (time_t)now_epoch;
|
||||
gmtime_r(&t, &tm_now);
|
||||
|
||||
char ts_buf[48];
|
||||
snprintf(ts_buf, sizeof(ts_buf), "%04d-%02d-%02dT%02d:%02d:%02d.000000",
|
||||
tm_now.tm_year + 1900, tm_now.tm_mon + 1, tm_now.tm_mday,
|
||||
tm_now.tm_hour, tm_now.tm_min, tm_now.tm_sec);
|
||||
char time_buf[12], date_buf[32];
|
||||
snprintf(time_buf, sizeof(time_buf), "%02d:%02d:%02d",
|
||||
tm_now.tm_hour, tm_now.tm_min, tm_now.tm_sec);
|
||||
snprintf(date_buf, sizeof(date_buf), "%d/%d/%04d",
|
||||
tm_now.tm_mday, tm_now.tm_mon + 1, tm_now.tm_year + 1900);
|
||||
|
||||
char raw_hex[sizeof(raw) * 2 + 1];
|
||||
Utils::toHex(raw_hex, raw, raw_len);
|
||||
raw_hex[raw_len * 2] = '\0';
|
||||
|
||||
static char json_buf[1024];
|
||||
int json_len = snprintf(json_buf, sizeof(json_buf),
|
||||
"{"
|
||||
"\"type\":\"PACKET\","
|
||||
"\"origin\":\"%s\","
|
||||
"\"origin_id\":\"%s\","
|
||||
"\"timestamp\":\"%s\","
|
||||
"\"direction\":\"rx\","
|
||||
"\"time\":\"%s\","
|
||||
"\"date\":\"%s\","
|
||||
"\"len\":\"%d\","
|
||||
"\"packet_type\":\"%u\","
|
||||
"\"route\":\"D\","
|
||||
"\"payload_len\":\"%d\","
|
||||
"\"raw\":\"%s\","
|
||||
"\"SNR\":\"0\","
|
||||
"\"RSSI\":\"0\","
|
||||
"\"score\":\"0\","
|
||||
"\"hash\":\"0000000000000000\""
|
||||
"}",
|
||||
struct MeshcorePacketJson pj = {
|
||||
name,
|
||||
_pubkey_hex,
|
||||
ts_buf,
|
||||
time_buf,
|
||||
date_buf,
|
||||
now_ts,
|
||||
raw_len,
|
||||
(unsigned)PAYLOAD_TYPE_ADVERT,
|
||||
raw_len - 2, /* payload_len = raw_len minus header and path_len_byte */
|
||||
raw_hex);
|
||||
"D",
|
||||
(unsigned)(raw_len - 2), /* payload_len = raw_len minus header + path_len_byte */
|
||||
raw_hex,
|
||||
0, /* SNR (locally originated) */
|
||||
0, /* RSSI */
|
||||
0, /* score */
|
||||
"0000000000000000", /* hash (not computed for self-advert) */
|
||||
};
|
||||
int json_len = meshcore_build_packet_json(json_buf, sizeof(json_buf), &pj);
|
||||
|
||||
if (json_len < 0 || json_len >= (int)sizeof(json_buf)) {
|
||||
LOG_WRN("publishSelfAdvert: JSON truncated");
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <mesh/Utils.h>
|
||||
#include <helpers/AdvertDataHelpers.h>
|
||||
#include <helpers/TxtDataHelpers.h>
|
||||
#include <helpers/MeshcoreJson.h>
|
||||
#include <adapters/radio/LoRaRadioBase.h>
|
||||
#include <adapters/sensors/SimpleLPP.h>
|
||||
#include <adapters/sensors/ZephyrEnvSensors.h>
|
||||
@@ -585,32 +586,11 @@ void RepeaterMesh::logTxFail(mesh::Packet* pkt, int len) {
|
||||
}
|
||||
|
||||
uint32_t RepeaterMesh::getRetransmitDelay(const mesh::Packet* packet) {
|
||||
float factor = getContentionTracker().getFloodDelayFactor();
|
||||
uint32_t airtime = _radio->getEstAirtimeFor(
|
||||
packet->getPathByteLen() + packet->payload_len + 2);
|
||||
uint32_t max_jitter = (uint32_t)(5 * airtime * factor);
|
||||
/* Airtime-scaled ceiling: never exceed ~6 airtimes of spread
|
||||
* (keeps SF7/narrow-BW configs from wasting time in oversized jitter windows). */
|
||||
uint32_t airtime_cap = 6 * airtime;
|
||||
if (max_jitter > airtime_cap) max_jitter = airtime_cap;
|
||||
/* Absolute cap: avoid excessive latency in very dense areas.
|
||||
* Reactive backoff will fine-tune further if needed. */
|
||||
if (max_jitter > 2000) max_jitter = 2000;
|
||||
/* Floor: give downstream nodes time to finish RX processing
|
||||
* and return to RX mode before we TX (~20ms settle) */
|
||||
return 20 + getRNG()->nextInt(0, max_jitter + 1);
|
||||
return computeAdaptiveFloodDelay(packet);
|
||||
}
|
||||
|
||||
uint32_t RepeaterMesh::getDirectRetransmitDelay(const mesh::Packet* packet) {
|
||||
uint32_t airtime = _radio->getEstAirtimeFor(
|
||||
packet->getPathByteLen() + packet->payload_len + 2);
|
||||
/* Jitter around Arduino direct factor 0.3 using a per-packet factor
|
||||
* in the range [0.25, 0.40]. */
|
||||
uint32_t factor_milli = (uint32_t)getRNG()->nextInt(250, 401);
|
||||
uint32_t max_jitter = (airtime * factor_milli) / 1000;
|
||||
/* Floor: give downstream nodes time to finish RX processing
|
||||
* and return to RX mode before we TX (~20ms settle + jitter). */
|
||||
return 20 + getRNG()->nextInt(0, max_jitter + 1);
|
||||
return computeAdaptiveDirectDelay(packet);
|
||||
}
|
||||
|
||||
bool RepeaterMesh::filterRecvFloodPacket(mesh::Packet* pkt) {
|
||||
@@ -1616,45 +1596,12 @@ void RepeaterMesh::publishUplinkPacket(mesh::Packet *pkt)
|
||||
hash_hex[MAX_HASH_SIZE * 2] = '\0';
|
||||
|
||||
uint32_t now_epoch = getRTCClock()->getCurrentTime();
|
||||
struct tm tm_now;
|
||||
time_t t = (time_t)now_epoch;
|
||||
gmtime_r(&t, &tm_now);
|
||||
|
||||
char ts_buf[48];
|
||||
char time_buf[12], date_buf[32];
|
||||
snprintf(ts_buf, sizeof(ts_buf), "%04d-%02d-%02dT%02d:%02d:%02d.000000",
|
||||
tm_now.tm_year + 1900, tm_now.tm_mon + 1, tm_now.tm_mday,
|
||||
tm_now.tm_hour, tm_now.tm_min, tm_now.tm_sec);
|
||||
snprintf(time_buf, sizeof(time_buf), "%02d:%02d:%02d",
|
||||
tm_now.tm_hour, tm_now.tm_min, tm_now.tm_sec);
|
||||
snprintf(date_buf, sizeof(date_buf), "%d/%d/%04d",
|
||||
tm_now.tm_mday, tm_now.tm_mon + 1, tm_now.tm_year + 1900);
|
||||
|
||||
static char json_buf[1024];
|
||||
int json_len = snprintf(json_buf, sizeof(json_buf),
|
||||
"{"
|
||||
"\"type\":\"PACKET\","
|
||||
"\"origin\":\"%s\","
|
||||
"\"origin_id\":\"%s\","
|
||||
"\"timestamp\":\"%s\","
|
||||
"\"direction\":\"rx\","
|
||||
"\"time\":\"%s\","
|
||||
"\"date\":\"%s\","
|
||||
"\"len\":\"%d\","
|
||||
"\"packet_type\":\"%u\","
|
||||
"\"route\":\"%s\","
|
||||
"\"payload_len\":\"%u\","
|
||||
"\"raw\":\"%s\","
|
||||
"\"SNR\":\"%d\","
|
||||
"\"RSSI\":\"%d\","
|
||||
"\"score\":\"%d\","
|
||||
"\"hash\":\"%s\""
|
||||
"}",
|
||||
struct MeshcorePacketJson pj = {
|
||||
_prefs.node_name,
|
||||
_uplink_pubkey_hex,
|
||||
ts_buf,
|
||||
time_buf,
|
||||
date_buf,
|
||||
now_epoch,
|
||||
_uplink_last_raw_len,
|
||||
(unsigned)pkt->getPayloadType(),
|
||||
pkt->isRouteDirect() ? "D" : "F",
|
||||
@@ -1663,7 +1610,9 @@ void RepeaterMesh::publishUplinkPacket(mesh::Packet *pkt)
|
||||
(int)pkt->getSNR(),
|
||||
(int)_uplink_last_rssi,
|
||||
(int)(_uplink_last_score * 1000.0f),
|
||||
hash_hex);
|
||||
hash_hex,
|
||||
};
|
||||
int json_len = meshcore_build_packet_json(json_buf, sizeof(json_buf), &pj);
|
||||
|
||||
if (json_len <= 0 || json_len >= (int)sizeof(json_buf)) {
|
||||
return;
|
||||
@@ -1678,14 +1627,6 @@ void RepeaterMesh::publishUplinkStatus(const char *status)
|
||||
|
||||
auto& radio_driver = getRadioDriver(_radio);
|
||||
uint32_t now_epoch = getRTCClock()->getCurrentTime();
|
||||
struct tm tm_now;
|
||||
time_t t = (time_t)now_epoch;
|
||||
gmtime_r(&t, &tm_now);
|
||||
|
||||
char ts_buf[48];
|
||||
snprintf(ts_buf, sizeof(ts_buf), "%04d-%02d-%02dT%02d:%02d:%02d.000000",
|
||||
tm_now.tm_year + 1900, tm_now.tm_mon + 1, tm_now.tm_mday,
|
||||
tm_now.tm_hour, tm_now.tm_min, tm_now.tm_sec);
|
||||
|
||||
char radio_buf[48];
|
||||
snprintf(radio_buf, sizeof(radio_buf), "%.3f,%.1f,%u,%u",
|
||||
@@ -1693,29 +1634,9 @@ void RepeaterMesh::publishUplinkStatus(const char *status)
|
||||
(unsigned)_prefs.sf, (unsigned)_prefs.cr);
|
||||
|
||||
static char json_buf[768];
|
||||
int json_len = snprintf(json_buf, sizeof(json_buf),
|
||||
"{"
|
||||
"\"status\":\"%s\","
|
||||
"\"timestamp\":\"%s\","
|
||||
"\"origin\":\"%s\","
|
||||
"\"origin_id\":\"%s\","
|
||||
"\"radio\":\"%s\","
|
||||
"\"model\":\"%s\","
|
||||
"\"firmware_version\":\"%s\","
|
||||
"\"client_version\":\"zephcoretomqtt/1.1\","
|
||||
"\"stats\":{"
|
||||
"\"battery_mv\":%u,"
|
||||
"\"uptime_secs\":%u,"
|
||||
"\"debug_flags\":%u,"
|
||||
"\"queue_len\":%u,"
|
||||
"\"noise_floor\":%d,"
|
||||
"\"tx_air_secs\":%u,"
|
||||
"\"rx_air_secs\":%u,"
|
||||
"\"recv_errors\":%u"
|
||||
"}"
|
||||
"}",
|
||||
struct MeshcoreStatusJson sj = {
|
||||
status,
|
||||
ts_buf,
|
||||
now_epoch,
|
||||
_prefs.node_name,
|
||||
_uplink_pubkey_hex,
|
||||
radio_buf,
|
||||
@@ -1732,7 +1653,9 @@ void RepeaterMesh::publishUplinkStatus(const char *status)
|
||||
_radio->getNoiseFloor(),
|
||||
(unsigned)(getTotalAirTime() / 1000),
|
||||
(unsigned)(getReceiveAirTime() / 1000),
|
||||
(unsigned)radio_driver.getPacketsRecvErrors());
|
||||
(unsigned)radio_driver.getPacketsRecvErrors(),
|
||||
};
|
||||
int json_len = meshcore_build_status_json(json_buf, sizeof(json_buf), &sj);
|
||||
|
||||
if (json_len <= 0 || json_len >= (int)sizeof(json_buf)) {
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* meshcoretomqtt-compatible JSON builders.
|
||||
*
|
||||
* Shared by ObserverMesh (mesh:: namespace) and the RepeaterMesh MQTT uplink
|
||||
* (global namespace). Header-only `static inline` so both translation units
|
||||
* can include it without a shared .cpp / CMake change. The packet and status
|
||||
* JSON shapes were byte-identical across the two; only the field *values*
|
||||
* differ, so callers fill the param struct and the format lives here once.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <time.h>
|
||||
|
||||
struct MeshcorePacketJson {
|
||||
const char *origin; /* node name */
|
||||
const char *origin_id; /* pubkey hex */
|
||||
uint32_t epoch; /* unix seconds (timestamp/time/date fields) */
|
||||
int raw_len;
|
||||
unsigned packet_type;
|
||||
const char *route; /* "D" or "F" */
|
||||
unsigned payload_len;
|
||||
const char *raw_hex;
|
||||
int snr;
|
||||
int rssi;
|
||||
int score_x1000;
|
||||
const char *hash_hex;
|
||||
};
|
||||
|
||||
static inline int meshcore_build_packet_json(char *out, size_t out_size,
|
||||
const struct MeshcorePacketJson *p)
|
||||
{
|
||||
struct tm tm_now;
|
||||
time_t t = (time_t)p->epoch;
|
||||
gmtime_r(&t, &tm_now);
|
||||
|
||||
char ts_buf[48], time_buf[12], date_buf[32];
|
||||
snprintf(ts_buf, sizeof(ts_buf), "%04d-%02d-%02dT%02d:%02d:%02d.000000",
|
||||
tm_now.tm_year + 1900, tm_now.tm_mon + 1, tm_now.tm_mday,
|
||||
tm_now.tm_hour, tm_now.tm_min, tm_now.tm_sec);
|
||||
snprintf(time_buf, sizeof(time_buf), "%02d:%02d:%02d",
|
||||
tm_now.tm_hour, tm_now.tm_min, tm_now.tm_sec);
|
||||
snprintf(date_buf, sizeof(date_buf), "%d/%d/%04d",
|
||||
tm_now.tm_mday, tm_now.tm_mon + 1, tm_now.tm_year + 1900);
|
||||
|
||||
return snprintf(out, out_size,
|
||||
"{"
|
||||
"\"type\":\"PACKET\","
|
||||
"\"origin\":\"%s\","
|
||||
"\"origin_id\":\"%s\","
|
||||
"\"timestamp\":\"%s\","
|
||||
"\"direction\":\"rx\","
|
||||
"\"time\":\"%s\","
|
||||
"\"date\":\"%s\","
|
||||
"\"len\":\"%d\","
|
||||
"\"packet_type\":\"%u\","
|
||||
"\"route\":\"%s\","
|
||||
"\"payload_len\":\"%u\","
|
||||
"\"raw\":\"%s\","
|
||||
"\"SNR\":\"%d\","
|
||||
"\"RSSI\":\"%d\","
|
||||
"\"score\":\"%d\","
|
||||
"\"hash\":\"%s\""
|
||||
"}",
|
||||
p->origin, p->origin_id, ts_buf, time_buf, date_buf,
|
||||
p->raw_len, p->packet_type, p->route, p->payload_len,
|
||||
p->raw_hex, p->snr, p->rssi, p->score_x1000, p->hash_hex);
|
||||
}
|
||||
|
||||
struct MeshcoreStatusJson {
|
||||
const char *status;
|
||||
uint32_t epoch;
|
||||
const char *origin;
|
||||
const char *origin_id;
|
||||
const char *radio;
|
||||
const char *model;
|
||||
const char *firmware_version;
|
||||
unsigned battery_mv;
|
||||
unsigned uptime_secs;
|
||||
unsigned debug_flags;
|
||||
unsigned queue_len;
|
||||
int noise_floor;
|
||||
unsigned tx_air_secs;
|
||||
unsigned rx_air_secs;
|
||||
unsigned recv_errors;
|
||||
};
|
||||
|
||||
static inline int meshcore_build_status_json(char *out, size_t out_size,
|
||||
const struct MeshcoreStatusJson *s)
|
||||
{
|
||||
struct tm tm_now;
|
||||
time_t t = (time_t)s->epoch;
|
||||
gmtime_r(&t, &tm_now);
|
||||
|
||||
char ts_buf[48];
|
||||
snprintf(ts_buf, sizeof(ts_buf), "%04d-%02d-%02dT%02d:%02d:%02d.000000",
|
||||
tm_now.tm_year + 1900, tm_now.tm_mon + 1, tm_now.tm_mday,
|
||||
tm_now.tm_hour, tm_now.tm_min, tm_now.tm_sec);
|
||||
|
||||
return snprintf(out, out_size,
|
||||
"{"
|
||||
"\"status\":\"%s\","
|
||||
"\"timestamp\":\"%s\","
|
||||
"\"origin\":\"%s\","
|
||||
"\"origin_id\":\"%s\","
|
||||
"\"radio\":\"%s\","
|
||||
"\"model\":\"%s\","
|
||||
"\"firmware_version\":\"%s\","
|
||||
"\"client_version\":\"zephcoretomqtt/1.1\","
|
||||
"\"stats\":{"
|
||||
"\"battery_mv\":%u,"
|
||||
"\"uptime_secs\":%u,"
|
||||
"\"debug_flags\":%u,"
|
||||
"\"queue_len\":%u,"
|
||||
"\"noise_floor\":%d,"
|
||||
"\"tx_air_secs\":%u,"
|
||||
"\"rx_air_secs\":%u,"
|
||||
"\"recv_errors\":%u"
|
||||
"}"
|
||||
"}",
|
||||
s->status, ts_buf, s->origin, s->origin_id, s->radio,
|
||||
s->model, s->firmware_version,
|
||||
s->battery_mv, s->uptime_secs, s->debug_flags, s->queue_len,
|
||||
s->noise_floor, s->tx_air_secs, s->rx_air_secs, s->recv_errors);
|
||||
}
|
||||
@@ -56,6 +56,11 @@ protected:
|
||||
virtual bool allowPacketForward(const Packet *packet);
|
||||
virtual uint32_t getRetransmitDelay(const Packet *packet);
|
||||
virtual uint32_t getDirectRetransmitDelay(const Packet *packet) { return 0; }
|
||||
/* Shared adaptive retransmit-delay math. CompanionMesh and RepeaterMesh
|
||||
* had byte-identical overrides of getRetransmitDelay/getDirectRetransmitDelay;
|
||||
* both now delegate here. */
|
||||
uint32_t computeAdaptiveFloodDelay(const Packet *packet);
|
||||
uint32_t computeAdaptiveDirectDelay(const Packet *packet);
|
||||
/* Passive contention tracking: if true, track heard floods we don't forward
|
||||
* (warms the contention EMA on nodes that don't relay, e.g. companions). */
|
||||
virtual bool passivelyTrackFloods() const { return false; }
|
||||
|
||||
@@ -71,6 +71,36 @@ uint32_t Mesh::getRetransmitDelay(const Packet *packet)
|
||||
return _rng->nextInt(0, 5) * t;
|
||||
}
|
||||
|
||||
uint32_t Mesh::computeAdaptiveFloodDelay(const Packet *packet)
|
||||
{
|
||||
float factor = getContentionTracker().getFloodDelayFactor();
|
||||
uint32_t airtime = _radio->getEstAirtimeFor(
|
||||
packet->getPathByteLen() + packet->payload_len + 2);
|
||||
uint32_t max_jitter = (uint32_t)(5 * airtime * factor);
|
||||
/* Airtime-scaled ceiling: never exceed ~6 airtimes of spread. */
|
||||
uint32_t airtime_cap = 6 * airtime;
|
||||
if (max_jitter > airtime_cap) max_jitter = airtime_cap;
|
||||
/* Absolute cap: avoid excessive latency in very dense areas.
|
||||
* Reactive backoff will fine-tune further if needed. */
|
||||
if (max_jitter > 2000) max_jitter = 2000;
|
||||
/* Floor: give downstream nodes time to finish RX processing
|
||||
* and return to RX mode before we TX (~20ms settle) */
|
||||
return 20 + _rng->nextInt(0, max_jitter + 1);
|
||||
}
|
||||
|
||||
uint32_t Mesh::computeAdaptiveDirectDelay(const Packet *packet)
|
||||
{
|
||||
uint32_t airtime = _radio->getEstAirtimeFor(
|
||||
packet->getPathByteLen() + packet->payload_len + 2);
|
||||
/* Jitter around Arduino direct factor 0.3 using a per-packet factor
|
||||
* in the range [0.25, 0.40]. */
|
||||
uint32_t factor_milli = (uint32_t)_rng->nextInt(250, 401);
|
||||
uint32_t max_jitter = (airtime * factor_milli) / 1000;
|
||||
/* Floor: give downstream nodes time to finish RX processing
|
||||
* and return to RX mode before we TX (~20ms settle + jitter) */
|
||||
return 20 + _rng->nextInt(0, max_jitter + 1);
|
||||
}
|
||||
|
||||
uint32_t Mesh::getCADFailRetryDelay() const
|
||||
{
|
||||
return _rng->nextInt(1, 4) * 120;
|
||||
|
||||
Reference in New Issue
Block a user