diff --git a/zephcore/app/CompanionMesh.cpp b/zephcore/app/CompanionMesh.cpp index c62ae16..962293b 100644 --- a/zephcore/app/CompanionMesh.cpp +++ b/zephcore/app/CompanionMesh.cpp @@ -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); } diff --git a/zephcore/app/CompanionMesh.h b/zephcore/app/CompanionMesh.h index 1311cdc..43281de 100644 --- a/zephcore/app/CompanionMesh.h +++ b/zephcore/app/CompanionMesh.h @@ -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); diff --git a/zephcore/app/ObserverMesh.cpp b/zephcore/app/ObserverMesh.cpp index ebb46eb..9f53f27 100644 --- a/zephcore/app/ObserverMesh.cpp +++ b/zephcore/app/ObserverMesh.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include 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"); diff --git a/zephcore/app/RepeaterMesh.cpp b/zephcore/app/RepeaterMesh.cpp index 5724282..594c66c 100644 --- a/zephcore/app/RepeaterMesh.cpp +++ b/zephcore/app/RepeaterMesh.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -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; diff --git a/zephcore/helpers/MeshcoreJson.h b/zephcore/helpers/MeshcoreJson.h new file mode 100644 index 0000000..3b8efc9 --- /dev/null +++ b/zephcore/helpers/MeshcoreJson.h @@ -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 +#include +#include +#include + +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); +} diff --git a/zephcore/include/mesh/Mesh.h b/zephcore/include/mesh/Mesh.h index ee2f093..d53fcad 100644 --- a/zephcore/include/mesh/Mesh.h +++ b/zephcore/include/mesh/Mesh.h @@ -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; } diff --git a/zephcore/src/Mesh.cpp b/zephcore/src/Mesh.cpp index ef31cbc..de3e610 100644 --- a/zephcore/src/Mesh.cpp +++ b/zephcore/src/Mesh.cpp @@ -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;