feat(reticulum-daemon): airtime-limit args + radio_state RPC for live read-back
Groundwork for the .126 LoRa settings panel (operator top priority):
- --airtime-limit-short/--airtime-limit-long (percent duty-cycle locks,
e.g. EU868 25/10) written into the RNode interface config when set;
default None writes nothing — identical to older daemons.
- New socket RPC {"cmd":"radio_state"} returns the live RNodeInterface
state: requested config values AND the radio-confirmed r_* values
(r_frequency/r_bandwidth/r_txpower/r_sf/r_cr/r_st_alock/r_lt_alock,
online, port, airtime utilisation). The r_* values are what the RADIO
reported after detect/configure — the settings panel's proof that the
device is actually using what was applied.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
44522fc94a
commit
2afeafc92e
@@ -108,6 +108,13 @@ def _write_rns_config(
|
||||
f" spreadingfactor = {lora['spreadingfactor']}\n"
|
||||
f" codingrate = {lora['codingrate']}\n"
|
||||
)
|
||||
# Regulatory duty-cycle limits (percent). Only written when set —
|
||||
# absent keys keep RNS's own default (no software airtime lock),
|
||||
# matching every daemon built before these args existed.
|
||||
if lora.get("airtime_limit_short") is not None:
|
||||
interfaces += f" airtime_limit_short = {lora['airtime_limit_short']}\n"
|
||||
if lora.get("airtime_limit_long") is not None:
|
||||
interfaces += f" airtime_limit_long = {lora['airtime_limit_long']}\n"
|
||||
elif tcp_listen or tcp_connect:
|
||||
parts = []
|
||||
if tcp_listen:
|
||||
@@ -188,6 +195,8 @@ class ReticulumDaemon:
|
||||
"txpower": self.args.txpower,
|
||||
"spreadingfactor": self.args.spreadingfactor,
|
||||
"codingrate": self.args.codingrate,
|
||||
"airtime_limit_short": self.args.airtime_limit_short,
|
||||
"airtime_limit_long": self.args.airtime_limit_long,
|
||||
},
|
||||
no_radio=self.args.no_radio,
|
||||
tcp_listen=self.args.tcp_listen,
|
||||
@@ -358,6 +367,8 @@ class ReticulumDaemon:
|
||||
self.announce()
|
||||
elif cmd == "status":
|
||||
self._broadcast(self._status())
|
||||
elif cmd == "radio_state":
|
||||
self._broadcast(self._radio_state())
|
||||
elif cmd == "send_resource":
|
||||
self._send_resource(req)
|
||||
elif cmd == "shutdown":
|
||||
@@ -374,6 +385,48 @@ class ReticulumDaemon:
|
||||
return {"event": "status", "connected": self.router is not None,
|
||||
"dest_hash": self.dest_hash_hex, "interfaces": ifaces}
|
||||
|
||||
def _radio_state(self) -> dict:
|
||||
"""Radio-confirmed RNode parameters, straight from the live
|
||||
RNodeInterface object. The r_* attributes are what the RADIO reported
|
||||
after detect/configure (RNS/Interfaces/RNodeInterface.py) — this is
|
||||
the read-back the settings panel shows as proof the device is
|
||||
actually using the applied values, as opposed to what the config
|
||||
asked for. Absent radio (TCP/no-radio builds) → configured=False."""
|
||||
state = {"event": "radio_state", "configured": False, "online": False}
|
||||
try:
|
||||
import RNS
|
||||
for iface in list(RNS.Transport.interfaces):
|
||||
if type(iface).__name__ != "RNodeInterface":
|
||||
continue
|
||||
state.update({
|
||||
"configured": True,
|
||||
"online": bool(getattr(iface, "online", False)),
|
||||
"port": getattr(iface, "port", None),
|
||||
# Requested (config) values…
|
||||
"frequency": getattr(iface, "frequency", None),
|
||||
"bandwidth": getattr(iface, "bandwidth", None),
|
||||
"txpower": getattr(iface, "txpower", None),
|
||||
"spreadingfactor": getattr(iface, "sf", None),
|
||||
"codingrate": getattr(iface, "cr", None),
|
||||
"airtime_limit_short": getattr(iface, "st_alock", None),
|
||||
"airtime_limit_long": getattr(iface, "lt_alock", None),
|
||||
# …and what the radio itself confirmed it is running.
|
||||
"r_frequency": getattr(iface, "r_frequency", None),
|
||||
"r_bandwidth": getattr(iface, "r_bandwidth", None),
|
||||
"r_txpower": getattr(iface, "r_txpower", None),
|
||||
"r_spreadingfactor": getattr(iface, "r_sf", None),
|
||||
"r_codingrate": getattr(iface, "r_cr", None),
|
||||
"r_airtime_limit_short": getattr(iface, "r_st_alock", None),
|
||||
"r_airtime_limit_long": getattr(iface, "r_lt_alock", None),
|
||||
# Live utilisation, when the interface tracks it.
|
||||
"airtime_short": getattr(iface, "airtime_short", None),
|
||||
"airtime_long": getattr(iface, "airtime_long", None),
|
||||
})
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
return state
|
||||
|
||||
def _send(self, req: dict):
|
||||
import RNS
|
||||
import LXMF
|
||||
@@ -643,6 +696,11 @@ def _parse_args(argv):
|
||||
p.add_argument("--txpower", type=int, default=17)
|
||||
p.add_argument("--spreadingfactor", type=int, default=8)
|
||||
p.add_argument("--codingrate", type=int, default=5)
|
||||
# Regulatory duty-cycle locks (percent of airtime, e.g. EU868 short=25
|
||||
# long=10). None (the default) writes no config line, so RNS applies no
|
||||
# software airtime lock — identical to daemons built before these existed.
|
||||
p.add_argument("--airtime-limit-short", type=float, default=None)
|
||||
p.add_argument("--airtime-limit-long", type=float, default=None)
|
||||
p.add_argument("--enable-transport", action="store_true",
|
||||
help="run as an RNS transport node: relay traffic and rebroadcast "
|
||||
"announces so nodes beyond direct RF range discover each other "
|
||||
|
||||
Reference in New Issue
Block a user