mirror of
https://github.com/pyMC-dev/pyMC_Repeater.git
synced 2026-08-07 01:13:11 +02:00
fix(cli): accept radio parameters in MHz and stage them until restart
set freq and set radio stored the CLI's MHz/kHz inputs directly into radio.frequency and radio.bandwidth, which the rest of the stack treats as Hz — a freq change tuned the radio to a few hundred hertz. Convert to Hz on write and validate set radio with the firmware gate (freq 150-2500, bw 7-500, sf 5-12, cr 5-8, same error string; set freq stays unvalidated like firmware). Radio changes are now saved without a live apply so the reply's restart-to-apply contract is real: a live retune would cut off the remote admin mid-session, and the all-or-nothing live radio path would also have dragged staged frequency changes along with a tx tweak.
This commit is contained in:
@@ -45,7 +45,8 @@ class MeshCLI:
|
||||
``ConfigManager.save_to_file`` returns a bare bool; the tuple form is
|
||||
tolerated for older manager doubles. Live update only runs after a
|
||||
successful save so a failed write never half-applies a change. Pass
|
||||
no sections to stage a change: saved to disk, applied on restart.
|
||||
no sections to stage a change: saved to disk, applied on restart
|
||||
(radio parameters, matching firmware's reboot-to-apply).
|
||||
"""
|
||||
result = self.config_manager.save_to_file()
|
||||
saved = result[0] if isinstance(result, tuple) else bool(result)
|
||||
@@ -660,37 +661,45 @@ class MeshCLI:
|
||||
return "OK"
|
||||
|
||||
elif key == "radio":
|
||||
# Format: freq bw sf cr
|
||||
# Format: freq(MHz) bw(kHz) sf cr — the config stores Hz.
|
||||
radio_parts = value.split()
|
||||
if len(radio_parts) != 4:
|
||||
return "Error: Expected freq bw sf cr"
|
||||
|
||||
if "radio" not in self.config:
|
||||
self.config["radio"] = {}
|
||||
freq_mhz = float(radio_parts[0])
|
||||
bw_khz = float(radio_parts[1])
|
||||
sf = int(radio_parts[2])
|
||||
cr = int(radio_parts[3])
|
||||
if not (
|
||||
150.0 <= freq_mhz <= 2500.0
|
||||
and 7.0 <= bw_khz <= 500.0
|
||||
and 5 <= sf <= 12
|
||||
and 5 <= cr <= 8
|
||||
):
|
||||
return "Error, invalid radio params"
|
||||
|
||||
self.config["radio"]["frequency"] = float(radio_parts[0])
|
||||
self.config["radio"]["bandwidth"] = float(radio_parts[1])
|
||||
self.config["radio"]["spreading_factor"] = int(radio_parts[2])
|
||||
self.config["radio"]["coding_rate"] = int(radio_parts[3])
|
||||
if not self._save_config_and_apply(["radio"]):
|
||||
radio_config = self.config.setdefault("radio", {})
|
||||
radio_config["frequency"] = int(round(freq_mhz * 1_000_000))
|
||||
radio_config["bandwidth"] = int(round(bw_khz * 1_000))
|
||||
radio_config["spreading_factor"] = sf
|
||||
radio_config["coding_rate"] = cr
|
||||
if not self._save_config_and_apply():
|
||||
return "Error: Failed to save config"
|
||||
return "OK - restart repeater to apply"
|
||||
|
||||
elif key == "freq":
|
||||
if "radio" not in self.config:
|
||||
self.config["radio"] = {}
|
||||
self.config["radio"]["frequency"] = float(value)
|
||||
if not self._save_config_and_apply(["radio"]):
|
||||
# CLI input is MHz (firmware parity); the config stores Hz.
|
||||
freq_mhz = float(value)
|
||||
self.config.setdefault("radio", {})["frequency"] = int(round(freq_mhz * 1_000_000))
|
||||
if not self._save_config_and_apply():
|
||||
return "Error: Failed to save config"
|
||||
return "OK - restart repeater to apply"
|
||||
|
||||
elif key == "tx":
|
||||
if "radio" not in self.config:
|
||||
self.config["radio"] = {}
|
||||
self.config["radio"]["tx_power"] = int(value)
|
||||
if not self._save_config_and_apply(["radio"]):
|
||||
self.config.setdefault("radio", {})["tx_power"] = int(value)
|
||||
if not self._save_config_and_apply():
|
||||
return "Error: Failed to save config"
|
||||
return "OK"
|
||||
return "OK - restart repeater to apply"
|
||||
|
||||
elif key == "guest.password":
|
||||
# LoginHelper authenticates from repeater.security.guest_password.
|
||||
|
||||
@@ -189,11 +189,19 @@ def test_cmd_set_updates_and_validation_errors():
|
||||
assert cli._cmd_set("repeat off").endswith("OFF")
|
||||
assert cfg["repeater"]["mode"] == "monitor"
|
||||
|
||||
assert cli._cmd_set("radio 900000000 250000 9 6").startswith("OK")
|
||||
assert cfg["radio"]["frequency"] == 900000000.0
|
||||
# CLI input is MHz/kHz (firmware parity); the config stores Hz.
|
||||
assert cli._cmd_set("radio 900 250 9 6").startswith("OK")
|
||||
assert cfg["radio"]["frequency"] == 900000000
|
||||
assert cfg["radio"]["bandwidth"] == 250000
|
||||
assert cfg["radio"]["spreading_factor"] == 9
|
||||
assert cfg["radio"]["coding_rate"] == 6
|
||||
assert cli._cmd_set("radio 100 250 9 6") == "Error, invalid radio params"
|
||||
assert cli._cmd_set("radio 900 250 4 6") == "Error, invalid radio params"
|
||||
|
||||
assert cli._cmd_set("freq 868000000").startswith("OK")
|
||||
assert cli._cmd_set("tx 17") == "OK"
|
||||
assert cli._cmd_set("freq 868.5").startswith("OK")
|
||||
assert cfg["radio"]["frequency"] == 868500000
|
||||
assert cli._cmd_set("tx 17") == "OK - restart repeater to apply"
|
||||
assert cfg["radio"]["tx_power"] == 17
|
||||
assert cli._cmd_set("guest.password g") == "OK"
|
||||
assert cfg["repeater"]["security"]["guest_password"] == "g"
|
||||
assert cfg["security"]["guest_password"] == "stale-top-level"
|
||||
@@ -505,3 +513,18 @@ def test_cli_password_change_applies_to_live_repeater_acl(tmp_path):
|
||||
|
||||
assert cli._cmd_set("allow.read.only off") == "OK"
|
||||
assert acl.allow_read_only is False
|
||||
|
||||
|
||||
def test_cmd_set_radio_commands_stage_without_live_apply():
|
||||
"""Radio changes match firmware reboot-to-apply: saved to disk, never
|
||||
live-applied (a live retune would cut off the admin mid-session)."""
|
||||
cfg = _base_config()
|
||||
mgr = _cfg_mgr()
|
||||
cli = MeshCLI("/tmp/cfg.yaml", cfg, mgr)
|
||||
|
||||
assert cli._cmd_set("radio 900 250 9 6") == "OK - restart repeater to apply"
|
||||
assert cli._cmd_set("freq 868.5") == "OK - restart repeater to apply"
|
||||
assert cli._cmd_set("tx 17") == "OK - restart repeater to apply"
|
||||
|
||||
assert mgr.save_to_file.call_count == 3
|
||||
mgr.live_update_daemon.assert_not_called()
|
||||
|
||||
Reference in New Issue
Block a user