Remember runtime lock-outs, and let a lock-out be a span
Pressing `l` during a scan locked a frequency out for that run only, so the same birdie had to be locked out again on every later one. It now writes back to the settings file the run started from -- only that one key, since a scan's config also holds whatever was passed on the command line for this run and saving all of it would quietly make those permanent. The file is read, its lock-outs replaced, the rest left as it was. `save_lockouts` turns it off for anyone who would rather their config were never touched. Lock-outs were also single frequencies only. They are now a list of frequencies and spans -- "162.55M, 450M-455M, 88M to 108M" -- which is what a pager band or a noisy stretch of spectrum actually is. A point is still widened by the lock-out width; a span is taken exactly as written, because whoever typed it already said how wide it is. The scanner matched lock-outs by rounding a frequency into a bucket of the lock-out width, which cannot express a span and was never exact at the edges. It now holds intervals and tests them directly. Settings files that predate this hold a bare number per lock-out, and still mean the same thing: Lockout.coerce takes numbers, strings, pairs and dicts, so old profiles load untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
b69d0b7a26
commit
44c98b11e3
9 changed files with 319 additions and 46 deletions
|
|
@ -8,12 +8,12 @@ from pathlib import Path
|
|||
|
||||
import yaml
|
||||
|
||||
from .ranges import ScanRange, parse_range_list
|
||||
from .ranges import Lockout, ScanRange, parse_range_list
|
||||
|
||||
__all__ = ["ScanConfig", "DEFAULT_CONFIG_DIR", "DEFAULT_CONFIG_PATH",
|
||||
"load_config", "save_config", "list_profiles", "profile_path",
|
||||
"load_default", "save_default", "delete_profile",
|
||||
"is_first_run", "DEFAULT_OUTPUT_DIR"]
|
||||
"is_first_run", "DEFAULT_OUTPUT_DIR", "remember_lockouts"]
|
||||
|
||||
DEFAULT_CONFIG_DIR = Path(
|
||||
os.environ.get("BANDSAUNTER_CONFIG_DIR",
|
||||
|
|
@ -120,8 +120,9 @@ class ScanConfig:
|
|||
transcribe_min_seconds: float = 1.0
|
||||
|
||||
# -- behaviour ------------------------------------------------------
|
||||
lockout: list[float] = field(default_factory=list)
|
||||
lockout: list[Lockout] = field(default_factory=list)
|
||||
lockout_width: float = 12_500.0
|
||||
save_lockouts: bool = True # write runtime lock-outs back to disk
|
||||
revisit_seconds: float = 8.0 # ignore a frequency again this soon
|
||||
max_cycles: int = 0 # 0 = run forever
|
||||
max_runtime_seconds: float = 0.0 # 0 = no limit
|
||||
|
|
@ -130,6 +131,12 @@ class ScanConfig:
|
|||
log_file: str = "scan_log.jsonl"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
def __post_init__(self):
|
||||
# Lock-outs arrive as bare numbers from older settings files, as
|
||||
# dicts from newer ones, and as either from callers. One type from
|
||||
# here on.
|
||||
self.lockout = [Lockout.coerce(v) for v in self.lockout]
|
||||
|
||||
def validate(self) -> list[str]:
|
||||
"""Return a list of human-readable problems (empty when config is sane)."""
|
||||
errs = []
|
||||
|
|
@ -270,6 +277,37 @@ def save_default(cfg: "ScanConfig", directory: Path | None = None) -> Path:
|
|||
return path
|
||||
|
||||
|
||||
def remember_lockouts(cfg: "ScanConfig",
|
||||
directory: Path | None = None) -> Path | None:
|
||||
"""Write the lock-out list back to the settings file it came from.
|
||||
|
||||
Only that one key: a scan's config also holds whatever was passed on the
|
||||
command line for this run, and saving all of it would quietly make those
|
||||
one-off options permanent. So the file on disk is read, its lock-outs
|
||||
replaced, and the rest left exactly as it was.
|
||||
|
||||
Returns where it was written, or None if it could not be -- locking out a
|
||||
birdie must never be the thing that ends a scan.
|
||||
"""
|
||||
path = getattr(cfg, "_source_path", None)
|
||||
path = Path(path) if path else (Path(directory or DEFAULT_CONFIG_DIR)
|
||||
/ "config.yaml")
|
||||
try:
|
||||
data = {}
|
||||
if path.exists():
|
||||
with open(path) as fh:
|
||||
data = yaml.safe_load(fh) or {}
|
||||
data["lockout"] = [lk.to_dict() for lk in cfg.lockout]
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = path.with_suffix(path.suffix + ".tmp")
|
||||
with open(tmp, "w") as fh:
|
||||
yaml.safe_dump(data, fh, sort_keys=False, default_flow_style=False)
|
||||
tmp.replace(path)
|
||||
return path
|
||||
except (OSError, yaml.YAMLError):
|
||||
return None
|
||||
|
||||
|
||||
def delete_profile(name: str, directory: Path | None = None) -> bool:
|
||||
path = profile_path(name, directory)
|
||||
if path.exists():
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue