🤖Made by a dirty clanker: This issue was fully researched and written by OpenAI GPT-5.6 Sol High.
Summary
PR #474 fixed the original problem reported here: RF Fabric transmissions are now attributed to the physical radio that carried them, and airtime is calculated from that radio's own modulation settings. Issue #516 can therefore remain closed.
The remaining issue is that duty-cycle accounting scope is hard-coded. AirtimeBudgets currently keys a ledger by:
(frequency,bandwidth)
Consequently:
two physical transmitters configured for the same channel share one budget;
radios on different channels receive independent budgets;
duty_cycle.shared_budget: true changes this to one budget for the whole node.
Those are useful policies, but neither is universally correct. The appropriate scope can depend on jurisdiction, frequency allocation, equipment conformity, deployment policy, and whether several radios form one certified product. Making the accounting scope configurable would let each deployment select the policy that fits its jurisdiction, equipment, and operational needs.
Why configurability would be useful
Different valid models exist:
Per physical transmitter
Each radio_id has its own rolling budget, even when two radios use the same frequency and bandwidth. This matches regulatory language that evaluates the on-time of a single transmitter device in some jurisdictions.
Per channel
Radios with the same (frequency, bandwidth) share a budget. This is the current behavior and is a useful conservative channel-occupancy policy.
One shared node budget
Every radio shares one allowance. This is already available through duty_cycle.shared_budget and may be appropriate for conservative deployments or a particular conformity assessment.
Explicit accounting domains
Operators assign radios to named budget groups. Radios in the same group share a ledger; radios in different groups do not. This covers regulatory frequency bands, certified equipment groups, or deployment-specific policy without hard-coding one country's band table into the repeater.
Giving two radios the same budget_group intentionally makes them share accounting. Giving them different groups keeps them independent, regardless of whether their channel settings happen to match.
Regulatory examples, not hard-coded policy
Swedish and EU rules illustrate why a channel key is not a universal abstraction. PTSFS 2025:1 defines duty cycle over a one-hour period and attaches limits or alternative spectrum-access requirements to frequency bands:
Other jurisdictions or equipment classes may use a different scope. These sources are included only as motivation; the proposed feature does not assume that Swedish/EU band boundaries apply worldwide.
Proposed configuration
Introduce an explicit accounting mode, for example:
duty_cycle:budget_scope:channel # radio | channel | shared | explicit
Suggested meanings:
radio: key the ledger by physical radio_id;
channel: key it by the effective (frequency, bandwidth), preserving current behavior;
shared: use one ledger for the complete repeater;
explicit: key it by each radio's duty_cycle.budget_group.
The exact key name is open to review. Exposing the selected policy in configuration would make the behavior explicit instead of inferring it from geography.
One possible backward-compatible design:
absence of the new setting retains the current channel behavior;
shared_budget: true maps to budget_scope: shared;
conflicting legacy and new settings produce an actionable validation error;
configuration import/export and the example configuration show the resolved mode.
When several radios share a ledger but specify different limits, the safest deterministic rule is to use the strictest limit, as the current shared-ledger implementation does.
State and refresh behavior
A robust implementation should avoid granting fresh airtime or multiplying recorded history when configuration changes:
an unchanged accounting key retains its rolling history;
moving a radio to another key selects that key's existing ledger, if any;
a retired key remains until its rolling window expires so moving away and back cannot reset the allowance;
splitting a shared ledger avoids cloning its history into multiple ledgers and double-counting it if they are merged again;
changing only modulation settings changes airtime calculation for future transmissions but does not rewrite historical airtime.
Relationship to airtime calculation
Accounting scope and airtime calculation are separate concerns:
radio_id -> that radio's SF/BW/CR/preamble -> packet airtime
configured budget_scope + radio/domain/channel -> rolling ledger
The first relationship is already implemented and is why #516 remains resolved. This issue concerns only selection and lifecycle of the ledger that receives that correctly calculated airtime.
Spectrum-access mechanisms such as LBT/AFA are also separate. Where equipment relies on an approved mitigation technique instead of a duty-cycle ceiling, the UI could distinguish the internal airtime policy from a legal limit.
Possible acceptance criteria
duty_cycle.budget_scope supports radio, channel, shared, and explicit accounting.
radio gives each physical radio_id an independent ledger, including radios on identical channels.
channel preserves the current (frequency, bandwidth) grouping.
shared preserves the existing node-wide shared-budget behavior.
explicit groups radios solely by their configured budget_group and validates missing groups.
Each ledger/domain can enforce its applicable max_airtime_per_minute; shared groups use a documented deterministic limit.
Refreshes and scope changes cannot reset, duplicate, or inflate airtime inside the rolling window.
TX is gated and debited against the actual transmitting radio and the ledger selected by the configured scope.
Existing radio-specific airtime calculation and RX/TX attribution remain unchanged.
Legacy configurations remain backward compatible, with shared_budget mapped or rejected clearly when conflicting.
Stats expose the resolved accounting scope and group/key sufficiently for an operator to understand why radios share or do not share a budget.
> [!NOTE]
> 🤖 **Made by a dirty clanker:** This issue was fully researched and written by OpenAI GPT-5.6 Sol High.
## Summary
PR #474 fixed the original problem reported here: RF Fabric transmissions are now attributed to the physical radio that carried them, and airtime is calculated from that radio's own modulation settings. Issue #516 can therefore remain closed.
The remaining issue is that duty-cycle **accounting scope is hard-coded**. `AirtimeBudgets` currently keys a ledger by:
```python
(frequency, bandwidth)
```
Consequently:
- two physical transmitters configured for the same channel share one budget;
- radios on different channels receive independent budgets;
- `duty_cycle.shared_budget: true` changes this to one budget for the whole node.
Those are useful policies, but neither is universally correct. The appropriate scope can depend on jurisdiction, frequency allocation, equipment conformity, deployment policy, and whether several radios form one certified product. Making the accounting scope configurable would let each deployment select the policy that fits its jurisdiction, equipment, and operational needs.
## Why configurability would be useful
Different valid models exist:
### Per physical transmitter
Each `radio_id` has its own rolling budget, even when two radios use the same frequency and bandwidth. This matches regulatory language that evaluates the on-time of a single transmitter device in some jurisdictions.
### Per channel
Radios with the same `(frequency, bandwidth)` share a budget. This is the current behavior and is a useful conservative channel-occupancy policy.
### One shared node budget
Every radio shares one allowance. This is already available through `duty_cycle.shared_budget` and may be appropriate for conservative deployments or a particular conformity assessment.
### Explicit accounting domains
Operators assign radios to named budget groups. Radios in the same group share a ledger; radios in different groups do not. This covers regulatory frequency bands, certified equipment groups, or deployment-specific policy without hard-coding one country's band table into the repeater.
For example:
```yaml
duty_cycle:
budget_scope: explicit
radios:
- id: local
duty_cycle:
budget_group: local-transmitter-eu868
max_airtime_per_minute: 3600
- id: link
duty_cycle:
budget_group: backhaul-transmitter
max_airtime_per_minute: 600
```
Giving two radios the same `budget_group` intentionally makes them share accounting. Giving them different groups keeps them independent, regardless of whether their channel settings happen to match.
## Regulatory examples, not hard-coded policy
Swedish and EU rules illustrate why a channel key is not a universal abstraction. PTSFS 2025:1 defines duty cycle over a one-hour period and attaches limits or alternative spectrum-access requirements to frequency bands:
https://pts.se/contentassets/b05c8a7d01a64783aafc6a19e1b78590/ptsfs-2025-1-foreskrifter-om-undantag-fran-tillstandsplikt-for-anvandning-av-vissa-radiosandare.pdf
The consolidated EU short-range-device decision describes the on-time of a single transmitter device, measured over the applicable frequency band:
https://eur-lex.europa.eu/eli/dec/2006/771(2)/2022-02-10/eng
ETSI EN 300 220-1 assesses overall duty cycle over the operational frequency band rather than granting a separate allowance to every channel:
https://www.etsi.org/deliver/etsi_en/300200_300299/30022001/03.01.01_60/en_30022001v030101p.pdf
Other jurisdictions or equipment classes may use a different scope. These sources are included only as motivation; the proposed feature does not assume that Swedish/EU band boundaries apply worldwide.
## Proposed configuration
Introduce an explicit accounting mode, for example:
```yaml
duty_cycle:
budget_scope: channel # radio | channel | shared | explicit
```
Suggested meanings:
- `radio`: key the ledger by physical `radio_id`;
- `channel`: key it by the effective `(frequency, bandwidth)`, preserving current behavior;
- `shared`: use one ledger for the complete repeater;
- `explicit`: key it by each radio's `duty_cycle.budget_group`.
The exact key name is open to review. Exposing the selected policy in configuration would make the behavior explicit instead of inferring it from geography.
One possible backward-compatible design:
- absence of the new setting retains the current `channel` behavior;
- `shared_budget: true` maps to `budget_scope: shared`;
- conflicting legacy and new settings produce an actionable validation error;
- configuration import/export and the example configuration show the resolved mode.
When several radios share a ledger but specify different limits, the safest deterministic rule is to use the strictest limit, as the current shared-ledger implementation does.
## State and refresh behavior
A robust implementation should avoid granting fresh airtime or multiplying recorded history when configuration changes:
- an unchanged accounting key retains its rolling history;
- moving a radio to another key selects that key's existing ledger, if any;
- a retired key remains until its rolling window expires so moving away and back cannot reset the allowance;
- splitting a shared ledger avoids cloning its history into multiple ledgers and double-counting it if they are merged again;
- changing only modulation settings changes airtime calculation for future transmissions but does not rewrite historical airtime.
## Relationship to airtime calculation
Accounting scope and airtime calculation are separate concerns:
```text
radio_id -> that radio's SF/BW/CR/preamble -> packet airtime
configured budget_scope + radio/domain/channel -> rolling ledger
```
The first relationship is already implemented and is why #516 remains resolved. This issue concerns only selection and lifecycle of the ledger that receives that correctly calculated airtime.
Spectrum-access mechanisms such as LBT/AFA are also separate. Where equipment relies on an approved mitigation technique instead of a duty-cycle ceiling, the UI could distinguish the internal airtime policy from a legal limit.
## Possible acceptance criteria
1. `duty_cycle.budget_scope` supports `radio`, `channel`, `shared`, and `explicit` accounting.
2. `radio` gives each physical `radio_id` an independent ledger, including radios on identical channels.
3. `channel` preserves the current `(frequency, bandwidth)` grouping.
4. `shared` preserves the existing node-wide shared-budget behavior.
5. `explicit` groups radios solely by their configured `budget_group` and validates missing groups.
6. Each ledger/domain can enforce its applicable `max_airtime_per_minute`; shared groups use a documented deterministic limit.
7. Refreshes and scope changes cannot reset, duplicate, or inflate airtime inside the rolling window.
8. TX is gated and debited against the actual transmitting radio and the ledger selected by the configured scope.
9. Existing radio-specific airtime calculation and RX/TX attribution remain unchanged.
10. Legacy configurations remain backward compatible, with `shared_budget` mapped or rejected clearly when conflicting.
11. Stats expose the resolved accounting scope and group/key sufficiently for an operator to understand why radios share or do not share a budget.
## Relevant files
- `repeater/airtime.py`
- `repeater/config.py`
- `repeater/config_manager.py`
- `repeater/engine.py`
- `config.yaml.example`
- `tests/test_per_radio_duty_cycle.py`
## Related
- #383 - RF Fabric / multi-radio work
- #474 - introduced the current per-radio calculation and channel-keyed ledgers
- #516 - radio-specific airtime calculation; correctly remains closed
<!-- github-sync-id:5752838152 -->
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
PR #474 fixed the original problem reported here: RF Fabric transmissions are now attributed to the physical radio that carried them, and airtime is calculated from that radio's own modulation settings. Issue #516 can therefore remain closed.
The remaining issue is that duty-cycle accounting scope is hard-coded.
AirtimeBudgetscurrently keys a ledger by:Consequently:
duty_cycle.shared_budget: truechanges this to one budget for the whole node.Those are useful policies, but neither is universally correct. The appropriate scope can depend on jurisdiction, frequency allocation, equipment conformity, deployment policy, and whether several radios form one certified product. Making the accounting scope configurable would let each deployment select the policy that fits its jurisdiction, equipment, and operational needs.
Why configurability would be useful
Different valid models exist:
Per physical transmitter
Each
radio_idhas its own rolling budget, even when two radios use the same frequency and bandwidth. This matches regulatory language that evaluates the on-time of a single transmitter device in some jurisdictions.Per channel
Radios with the same
(frequency, bandwidth)share a budget. This is the current behavior and is a useful conservative channel-occupancy policy.One shared node budget
Every radio shares one allowance. This is already available through
duty_cycle.shared_budgetand may be appropriate for conservative deployments or a particular conformity assessment.Explicit accounting domains
Operators assign radios to named budget groups. Radios in the same group share a ledger; radios in different groups do not. This covers regulatory frequency bands, certified equipment groups, or deployment-specific policy without hard-coding one country's band table into the repeater.
For example:
Giving two radios the same
budget_groupintentionally makes them share accounting. Giving them different groups keeps them independent, regardless of whether their channel settings happen to match.Regulatory examples, not hard-coded policy
Swedish and EU rules illustrate why a channel key is not a universal abstraction. PTSFS 2025:1 defines duty cycle over a one-hour period and attaches limits or alternative spectrum-access requirements to frequency bands:
https://pts.se/contentassets/b05c8a7d01a64783aafc6a19e1b78590/ptsfs-2025-1-foreskrifter-om-undantag-fran-tillstandsplikt-for-anvandning-av-vissa-radiosandare.pdf
The consolidated EU short-range-device decision describes the on-time of a single transmitter device, measured over the applicable frequency band:
https://eur-lex.europa.eu/eli/dec/2006/771(2)/2022-02-10/eng
ETSI EN 300 220-1 assesses overall duty cycle over the operational frequency band rather than granting a separate allowance to every channel:
https://www.etsi.org/deliver/etsi_en/300200_300299/30022001/03.01.01_60/en_30022001v030101p.pdf
Other jurisdictions or equipment classes may use a different scope. These sources are included only as motivation; the proposed feature does not assume that Swedish/EU band boundaries apply worldwide.
Proposed configuration
Introduce an explicit accounting mode, for example:
Suggested meanings:
radio: key the ledger by physicalradio_id;channel: key it by the effective(frequency, bandwidth), preserving current behavior;shared: use one ledger for the complete repeater;explicit: key it by each radio'sduty_cycle.budget_group.The exact key name is open to review. Exposing the selected policy in configuration would make the behavior explicit instead of inferring it from geography.
One possible backward-compatible design:
channelbehavior;shared_budget: truemaps tobudget_scope: shared;When several radios share a ledger but specify different limits, the safest deterministic rule is to use the strictest limit, as the current shared-ledger implementation does.
State and refresh behavior
A robust implementation should avoid granting fresh airtime or multiplying recorded history when configuration changes:
Relationship to airtime calculation
Accounting scope and airtime calculation are separate concerns:
The first relationship is already implemented and is why #516 remains resolved. This issue concerns only selection and lifecycle of the ledger that receives that correctly calculated airtime.
Spectrum-access mechanisms such as LBT/AFA are also separate. Where equipment relies on an approved mitigation technique instead of a duty-cycle ceiling, the UI could distinguish the internal airtime policy from a legal limit.
Possible acceptance criteria
duty_cycle.budget_scopesupportsradio,channel,shared, andexplicitaccounting.radiogives each physicalradio_idan independent ledger, including radios on identical channels.channelpreserves the current(frequency, bandwidth)grouping.sharedpreserves the existing node-wide shared-budget behavior.explicitgroups radios solely by their configuredbudget_groupand validates missing groups.max_airtime_per_minute; shared groups use a documented deterministic limit.shared_budgetmapped or rejected clearly when conflicting.Relevant files
repeater/airtime.pyrepeater/config.pyrepeater/config_manager.pyrepeater/engine.pyconfig.yaml.exampletests/test_per_radio_duty_cycle.pyRelated