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:
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:
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.
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:
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:
Radio A and radio B can be configured with substantially different modulation parameters.
The same payload produces different airtime values when calculated for A and B.
TX airtime uses the parameters of preferred_tx_radio_id.
Duty-cycle accounting receives the airtime calculated for the actual TX radio.
RX airtime uses the parameters associated with rx_radio_id.
Live configuration updates preserve the correct parameter set for each Fabric radio.
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 -->
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Code inspection indicates that RF Fabric supports physical radios with different LoRa air settings, while
AirtimeManagercurrently keeps a single set of modulation parameters derived from the top-levelradioconfiguration.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
AirtimeManagerstores one set of radio parametersAirtimeManagerinitializes its radio configuration from the top-levelradiosection:refresh_radio_params()then stores a single set of modulation parameters such as: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 ownradiosection.Conceptually, a configuration can therefore contain radios with different air settings, for example:
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:
No
radio_idis passed tocalculate_airtime().Later in the TX path, however, the selected Fabric radio can be explicitly supplied to the dispatcher:
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:
Suppose a packet is selected for transmission on
link.The actual transmission can be directed to
linkusing:but the airtime calculation has already occurred without that identifier:
If
AirtimeManagercurrently holds the SF8 parameters from the top-levelradioconfiguration, 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_msis subsequently used by the duty-cycle gate:and, after successful transmission, by the accounting path:
Therefore, if
airtime_mswas 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:
and then records it with:
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
AirtimeManagerusing only the top-levelradioconfiguration: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:
AirtimeManagercould then resolve the appropriate SF, bandwidth, coding rate, and preamble configuration for that radio.For example:
The TX path already has
preferred_tx_radio_id, and the RX path has access torx_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:
preferred_tx_radio_id.rx_radio_id.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.pyrepeater/engine.pyrepeater/config.pyrepeater/config_manager.pyrepeater/main.pytests/test_airtime.pytests/test_multi_radio_stack.pytests/test_tx_lock.pyRelated
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:
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_mswas 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.