Feature request: configurable RF Fabric duty-cycle accounting scope #164

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

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:

(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:

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:

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

  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
> [!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 -->
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#164