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:
liquidraver
2026-05-29 12:54:52 +02:00
parent b1f1c77b88
commit 94dcf61715
7 changed files with 375 additions and 540 deletions
+157 -321
View File
@@ -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);
}
+11
View File
@@ -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
View File
@@ -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");
+13 -90
View File
@@ -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;
+129
View File
@@ -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);
}
+5
View File
@@ -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; }
+30
View File
@@ -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;