RF Fabric airtime calculation uses global radio parameters instead of target radio settings #165

Closed
opened 2026-10-27 09:35:58 +01:00 by adam · 0 comments
Owner

Note: This entire GitHub issue was researched, analyzed, and written by ChatGPT 5.6-Sol High after reviewing the current openhop_repeater codebase.

Summary

Code inspection indicates that RF Fabric supports physical radios with different LoRa air settings, while AirtimeManager currently keeps a single set of modulation parameters derived from the top-level radio configuration.

The TX airtime calculation examined does not receive the selected Fabric radio_id. This means airtime can be calculated using the global/default radio parameters rather than the parameters of the physical radio that will actually transmit the packet.

The RX path appears to have the same limitation: the engine can know the receiving radio, but that radio identity is not supplied to the airtime calculation.

If Fabric radios use different SF, bandwidth, coding rate, or preamble settings, this can result in airtime estimates that do not correspond to the radio that actually handled the packet.

Evidence from the current implementation

AirtimeManager stores one set of radio parameters

AirtimeManager initializes its radio configuration from the top-level radio section:

self.radio_config = config.get("radio", {})
self.refresh_radio_params(self.radio_config)

refresh_radio_params() then stores a single set of modulation parameters such as:

self.spreading_factor
self.bandwidth
self.coding_rate
self.preamble_length

The inspected implementation does not associate these cached values with a Fabric radio_id.

RF Fabric supports per-radio configuration

The multi-radio configuration path allows individual entries in radios[] to override their own radio section.

Conceptually, a configuration can therefore contain radios with different air settings, for example:

radio:
  spreading_factor: 8
  bandwidth: 62500

radios:
  - id: local
    radio:
      spreading_factor: 8
      bandwidth: 62500

  - id: link
    radio:
      spreading_factor: 11
      bandwidth: 62500

The physical radios constructed from these entries can therefore have different modulation settings.

TX airtime calculation does not identify the target radio

Before scheduling the transmission, airtime is calculated with:

airtime_ms = self.airtime_mgr.calculate_airtime(
    fwd_pkt.get_raw_length()
)

No radio_id is passed to calculate_airtime().

Later in the TX path, however, the selected Fabric radio can be explicitly supplied to the dispatcher:

await self.dispatcher.send_packet(
    fwd_pkt,
    wait_for_ack=False,
    radio_id=preferred_tx_radio_id,
)

This provides evidence that the physical TX radio is known by the transmission path but is not represented in the preceding airtime calculation.

Example consequence

Consider two Fabric radios configured with different spreading factors:

local: SF8
link:  SF11

Suppose a packet is selected for transmission on link.

The actual transmission can be directed to link using:

radio_id=preferred_tx_radio_id

but the airtime calculation has already occurred without that identifier:

calculate_airtime(payload_length)

If AirtimeManager currently holds the SF8 parameters from the top-level radio configuration, the estimated airtime would be based on SF8 even though the packet is subsequently transmitted using the SF11 radio.

Because LoRa airtime depends strongly on modulation parameters, the resulting difference can be significant.

The exact magnitude and direction of the error depends on the two radio configurations.

Effect on duty-cycle enforcement

The calculated airtime_ms is subsequently used by the duty-cycle gate:

can_tx_now, _ = self.airtime_mgr.can_transmit(airtime_ms)

and, after successful transmission, by the accounting path:

self.airtime_mgr.record_tx(airtime_ms)

Therefore, if airtime_ms was calculated using parameters that do not match the transmitting radio, the value used for duty-cycle enforcement and accounting can also differ from the estimated airtime of the actual transmission.

This is separate from #515, where multiple Fabric radios currently contribute to a shared duty-cycle history.

Even if the duty-cycle accounting described in #515 becomes radio-aware, enforcement can still be inaccurate if the airtime calculation itself does not use the target radio's modulation parameters.

RX appears to have the same limitation

The RX path calculates airtime using:

rx_airtime_ms = self.airtime_mgr.calculate_airtime(
    packet.get_raw_length()
)

and then records it with:

self.airtime_mgr.record_rx(rx_airtime_ms)

The airtime calculation does not include a Fabric radio identifier.

Elsewhere in the engine, the receiving radio can be represented by rx_radio_id.

This suggests that packets received through different physical radios are also evaluated using the same cached modulation parameters.

If the radios have different air settings, RX airtime statistics may therefore not correspond to the parameters of the radio that actually received the packet.

Live configuration path

The live radio configuration refresh also appears to update AirtimeManager using only the top-level radio configuration:

airtime_mgr.refresh_radio_params(
    self.config.get("radio", {}) or {}
)

This reinforces the current single-parameter-set model.

It is not clear from the inspected code how separate Fabric radio airtime parameters could be represented after a live configuration change.

Statistics

AirtimeManager.get_stats() exposes aggregate airtime values rather than a per-radio breakdown.

This is not necessarily incorrect by itself, but it makes it difficult to determine from the reported statistics which physical Fabric radio contributed a particular amount of airtime.

If radio-aware airtime accounting is introduced, per-radio statistics may also be useful.

Question about intended behavior

The main question for maintainers is:

Should airtime calculation in an RF Fabric configuration use the modulation parameters of the physical radio that actually transmits or receives the packet?

The current code inspected appears to use one cached parameter set regardless of Fabric radio_id.

If all Fabric radios are required to use identical air settings, the current calculation may be intentional, but that requirement should probably be documented and potentially validated.

If Fabric radios are expected to support different modulation settings, airtime calculation appears to need awareness of the selected physical radio.

Possible implementation direction

If per-radio modulation is intended to be supported, one possible API would be:

calculate_airtime(payload_len, radio_id=None)

AirtimeManager could then resolve the appropriate SF, bandwidth, coding rate, and preamble configuration for that radio.

For example:

local
  spreading_factor
  bandwidth
  coding_rate
  preamble_length

link
  spreading_factor
  bandwidth
  coding_rate
  preamble_length

The TX path already has preferred_tx_radio_id, and the RX path has access to rx_radio_id, so those identifiers could potentially be propagated into the airtime calculation.

This is only one possible implementation; the issue is primarily about ensuring that the airtime parameters correspond to the physical radio handling the packet.

Suggested regression tests

If different modulation settings per Fabric radio are intended to be supported, tests could verify that:

  1. Radio A and radio B can be configured with substantially different modulation parameters.
  2. The same payload produces different airtime values when calculated for A and B.
  3. TX airtime uses the parameters of preferred_tx_radio_id.
  4. Duty-cycle accounting receives the airtime calculated for the actual TX radio.
  5. RX airtime uses the parameters associated with rx_radio_id.
  6. Live configuration updates preserve the correct parameter set for each Fabric radio.
  7. Existing single-radio airtime calculations remain unchanged.

A useful regression case would be two radios with a large SF difference, such as SF7/SF8 versus SF12, because an incorrect parameter selection should then be easy to detect.

Relevant files

  • repeater/airtime.py
  • repeater/engine.py
  • repeater/config.py
  • repeater/config_manager.py
  • repeater/main.py
  • tests/test_airtime.py
  • tests/test_multi_radio_stack.py
  • tests/test_tx_lock.py

Related

Related to the broader RF Fabric / multi-radio work in #383.

This issue is directly related to #515, “RF Fabric radios share a single duty-cycle budget.”

The two issues describe separate but interacting parts of the airtime/duty-cycle path:

#515:
Multiple Fabric radios contribute to the same duty-cycle accounting state.

This issue:
Airtime calculation does not appear to use the modulation parameters
of the Fabric radio handling the packet.

This distinction is important because fixing #515 alone may not make duty-cycle enforcement accurate when Fabric radios use different modulation settings.

Even with separate per-radio or per-domain duty-cycle accounting, the value recorded against a radio can still be inaccurate if airtime_ms was calculated using another radio's SF, bandwidth, coding rate, or preamble settings.

Conversely, making airtime calculation radio-aware would improve the calculated value, but #515 would still cause that airtime to contribute to the shared duty-cycle state.

The two issues can therefore be addressed independently, but both need to be considered for correct multi-radio airtime accounting.

> **Note:** This entire GitHub issue was researched, analyzed, and written by **ChatGPT 5.6-Sol High** after reviewing the current `openhop_repeater` codebase. ## Summary Code inspection indicates that RF Fabric supports physical radios with different LoRa air settings, while `AirtimeManager` currently keeps a single set of modulation parameters derived from the top-level `radio` configuration. The TX airtime calculation examined does not receive the selected Fabric `radio_id`. This means airtime can be calculated using the global/default radio parameters rather than the parameters of the physical radio that will actually transmit the packet. The RX path appears to have the same limitation: the engine can know the receiving radio, but that radio identity is not supplied to the airtime calculation. If Fabric radios use different SF, bandwidth, coding rate, or preamble settings, this can result in airtime estimates that do not correspond to the radio that actually handled the packet. ## Evidence from the current implementation ### `AirtimeManager` stores one set of radio parameters `AirtimeManager` initializes its radio configuration from the top-level `radio` section: ```python self.radio_config = config.get("radio", {}) self.refresh_radio_params(self.radio_config) ``` `refresh_radio_params()` then stores a single set of modulation parameters such as: ```python self.spreading_factor self.bandwidth self.coding_rate self.preamble_length ``` The inspected implementation does not associate these cached values with a Fabric `radio_id`. ### RF Fabric supports per-radio configuration The multi-radio configuration path allows individual entries in `radios[]` to override their own `radio` section. Conceptually, a configuration can therefore contain radios with different air settings, for example: ```yaml radio: spreading_factor: 8 bandwidth: 62500 radios: - id: local radio: spreading_factor: 8 bandwidth: 62500 - id: link radio: spreading_factor: 11 bandwidth: 62500 ``` The physical radios constructed from these entries can therefore have different modulation settings. ### TX airtime calculation does not identify the target radio Before scheduling the transmission, airtime is calculated with: ```python airtime_ms = self.airtime_mgr.calculate_airtime( fwd_pkt.get_raw_length() ) ``` No `radio_id` is passed to `calculate_airtime()`. Later in the TX path, however, the selected Fabric radio can be explicitly supplied to the dispatcher: ```python await self.dispatcher.send_packet( fwd_pkt, wait_for_ack=False, radio_id=preferred_tx_radio_id, ) ``` This provides evidence that the physical TX radio is known by the transmission path but is not represented in the preceding airtime calculation. ## Example consequence Consider two Fabric radios configured with different spreading factors: ```text local: SF8 link: SF11 ``` Suppose a packet is selected for transmission on `link`. The actual transmission can be directed to `link` using: ```python radio_id=preferred_tx_radio_id ``` but the airtime calculation has already occurred without that identifier: ```python calculate_airtime(payload_length) ``` If `AirtimeManager` currently holds the SF8 parameters from the top-level `radio` configuration, the estimated airtime would be based on SF8 even though the packet is subsequently transmitted using the SF11 radio. Because LoRa airtime depends strongly on modulation parameters, the resulting difference can be significant. The exact magnitude and direction of the error depends on the two radio configurations. ## Effect on duty-cycle enforcement The calculated `airtime_ms` is subsequently used by the duty-cycle gate: ```python can_tx_now, _ = self.airtime_mgr.can_transmit(airtime_ms) ``` and, after successful transmission, by the accounting path: ```python self.airtime_mgr.record_tx(airtime_ms) ``` Therefore, if `airtime_ms` was calculated using parameters that do not match the transmitting radio, the value used for duty-cycle enforcement and accounting can also differ from the estimated airtime of the actual transmission. This is separate from #515, where multiple Fabric radios currently contribute to a shared duty-cycle history. Even if the duty-cycle accounting described in #515 becomes radio-aware, enforcement can still be inaccurate if the airtime calculation itself does not use the target radio's modulation parameters. ## RX appears to have the same limitation The RX path calculates airtime using: ```python rx_airtime_ms = self.airtime_mgr.calculate_airtime( packet.get_raw_length() ) ``` and then records it with: ```python self.airtime_mgr.record_rx(rx_airtime_ms) ``` The airtime calculation does not include a Fabric radio identifier. Elsewhere in the engine, the receiving radio can be represented by `rx_radio_id`. This suggests that packets received through different physical radios are also evaluated using the same cached modulation parameters. If the radios have different air settings, RX airtime statistics may therefore not correspond to the parameters of the radio that actually received the packet. ## Live configuration path The live radio configuration refresh also appears to update `AirtimeManager` using only the top-level `radio` configuration: ```python airtime_mgr.refresh_radio_params( self.config.get("radio", {}) or {} ) ``` This reinforces the current single-parameter-set model. It is not clear from the inspected code how separate Fabric radio airtime parameters could be represented after a live configuration change. ## Statistics `AirtimeManager.get_stats()` exposes aggregate airtime values rather than a per-radio breakdown. This is not necessarily incorrect by itself, but it makes it difficult to determine from the reported statistics which physical Fabric radio contributed a particular amount of airtime. If radio-aware airtime accounting is introduced, per-radio statistics may also be useful. ## Question about intended behavior The main question for maintainers is: **Should airtime calculation in an RF Fabric configuration use the modulation parameters of the physical radio that actually transmits or receives the packet?** The current code inspected appears to use one cached parameter set regardless of Fabric `radio_id`. If all Fabric radios are required to use identical air settings, the current calculation may be intentional, but that requirement should probably be documented and potentially validated. If Fabric radios are expected to support different modulation settings, airtime calculation appears to need awareness of the selected physical radio. ## Possible implementation direction If per-radio modulation is intended to be supported, one possible API would be: ```python calculate_airtime(payload_len, radio_id=None) ``` `AirtimeManager` could then resolve the appropriate SF, bandwidth, coding rate, and preamble configuration for that radio. For example: ```text local spreading_factor bandwidth coding_rate preamble_length link spreading_factor bandwidth coding_rate preamble_length ``` The TX path already has `preferred_tx_radio_id`, and the RX path has access to `rx_radio_id`, so those identifiers could potentially be propagated into the airtime calculation. This is only one possible implementation; the issue is primarily about ensuring that the airtime parameters correspond to the physical radio handling the packet. ## Suggested regression tests If different modulation settings per Fabric radio are intended to be supported, tests could verify that: 1. Radio A and radio B can be configured with substantially different modulation parameters. 2. The same payload produces different airtime values when calculated for A and B. 3. TX airtime uses the parameters of `preferred_tx_radio_id`. 4. Duty-cycle accounting receives the airtime calculated for the actual TX radio. 5. RX airtime uses the parameters associated with `rx_radio_id`. 6. Live configuration updates preserve the correct parameter set for each Fabric radio. 7. Existing single-radio airtime calculations remain unchanged. A useful regression case would be two radios with a large SF difference, such as SF7/SF8 versus SF12, because an incorrect parameter selection should then be easy to detect. ## Relevant files - `repeater/airtime.py` - `repeater/engine.py` - `repeater/config.py` - `repeater/config_manager.py` - `repeater/main.py` - `tests/test_airtime.py` - `tests/test_multi_radio_stack.py` - `tests/test_tx_lock.py` ## Related Related to the broader RF Fabric / multi-radio work in #383. This issue is directly related to #515, **“RF Fabric radios share a single duty-cycle budget.”** The two issues describe separate but interacting parts of the airtime/duty-cycle path: ```text #515: Multiple Fabric radios contribute to the same duty-cycle accounting state. This issue: Airtime calculation does not appear to use the modulation parameters of the Fabric radio handling the packet. ``` This distinction is important because fixing #515 alone may not make duty-cycle enforcement accurate when Fabric radios use different modulation settings. Even with separate per-radio or per-domain duty-cycle accounting, the value recorded against a radio can still be inaccurate if `airtime_ms` was calculated using another radio's SF, bandwidth, coding rate, or preamble settings. Conversely, making airtime calculation radio-aware would improve the calculated value, but #515 would still cause that airtime to contribute to the shared duty-cycle state. The two issues can therefore be addressed independently, but both need to be considered for correct multi-radio airtime accounting. <!-- github-sync-id:5752864568 -->
adam closed this issue 2026-10-27 09:35:58 +01:00
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: adam/pyMC_Repeater#165