Put the APRS channel on the front page, and find it for you
The region was in the menu, in the receiver group, between the sample rate and the invented channel. That is the wrong place for it. It is the setting that decides whether anything is heard at all, and being on the wrong one sounds exactly like having no aerial, so it does not belong a level down among the things that make a working receiver work slightly better. It is now on the front page of the APRS menu, named as well as numbered, and so is the Listen line: "north-america 144.39 MHz" rather than "144.39 MHz", because the number alone does not say whether it is the right one and the name alone does not say what will be tuned. A channel that is no region's says so rather than claiming one. The region and the frequency are separate settings -- somebody may want a local packet network on neither -- which means they can be made to disagree. Every place that chooses a region now goes through one function, so they cannot. And there is a search. `bandsaunter aprs --find-channel`, or f in the menu, listens on each region's channel in turn and prints what was on each, then offers to use the busiest. This answers the one question about APRS that cannot be answered on any single frequency, because the answer *is* a frequency: somebody who has just plugged a dongle in cannot tell a wrong channel from a dead aerial, and that is worth a minute of listening rather than an evening of doubt. What it does not do is claim more than it found. A quiet channel is not proof of an empty one -- a fixed station beacons every half hour -- so what it finds is traffic, and when every channel comes back silent it says that this is not the same as an empty band and points at the aerial instead. The invented channel now honours tuning, and only carries its stations on its own frequency. Without that the search would pass on a simulated band having never searched anything, which is the kind of test that is worse than none. Sixteen new tests against six deliberately broken builds. The three that searched a whole simulated band were doing the same expensive thing three times over and now build their results directly, leaving one real end-to-end search; that file went from nine and a half minutes to two. Full suite 2612 passed. Built as 2026-09-20_03. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016PsWPTweCT6pwxKngvVxcg
This commit is contained in:
parent
2b653c2c3e
commit
14ba77da9a
8 changed files with 553 additions and 20 deletions
|
|
@ -1036,6 +1036,14 @@ def aprs_menu(console: Console, cfg: ScanConfig) -> None:
|
|||
console.print(
|
||||
f"\n [cyan]l[/cyan] [bold green]Listen[/bold green]"
|
||||
f" [grey62]{ap.describe(options)}[/grey62]\n"
|
||||
# The channel is the setting that decides whether anything is
|
||||
# heard at all, and it cannot be discovered from the air on any
|
||||
# one frequency, so it sits here rather than a level down among
|
||||
# the gain and the sample rate.
|
||||
f" [cyan]c[/cyan] Channel / region "
|
||||
f"[bold]{ap.channel_text(options)}[/bold]\n"
|
||||
f" [cyan]f[/cyan] Find the channel [grey62]listen on each "
|
||||
f"region's in turn and see which has traffic[/grey62]\n"
|
||||
f" [cyan]r[/cyan] Read a log back [grey62]{kept} in "
|
||||
f"{cfg.output_dir}[/grey62]\n"
|
||||
f" [cyan]N[/cyan] open group N "
|
||||
|
|
@ -1050,6 +1058,10 @@ def aprs_menu(console: Console, cfg: ScanConfig) -> None:
|
|||
return
|
||||
if answer in ("l", "listen", "p"):
|
||||
_aprs_listen(console, cfg, options)
|
||||
elif answer in ("c", "channel", "region"):
|
||||
_aprs_channel(console, options)
|
||||
elif answer in ("f", "find", "search", "scan"):
|
||||
_aprs_find(console, options)
|
||||
elif answer in ("r", "read", "packets", "m"):
|
||||
_aprs_read(console, cfg, options, logs)
|
||||
elif answer == "s":
|
||||
|
|
@ -1071,8 +1083,8 @@ def aprs_menu(console: Console, cfg: ScanConfig) -> None:
|
|||
found = _find_options(answer, ap)
|
||||
if not found:
|
||||
console.print(f" [yellow]nothing matches {answer!r} \u2014 "
|
||||
f"enter a group number, or l, r, s, d or b"
|
||||
f"[/yellow]")
|
||||
f"enter a group number, or l, c, f, r, s, d "
|
||||
f"or b[/yellow]")
|
||||
elif len(found) == 1:
|
||||
_edit_option(console, options,
|
||||
str(ap.OPTIONS.index(found[0]) + 1), ap)
|
||||
|
|
@ -1082,6 +1094,92 @@ def aprs_menu(console: Console, cfg: ScanConfig) -> None:
|
|||
_pick_option(console, options, ap)
|
||||
|
||||
|
||||
def _aprs_channel(console: Console, options) -> None:
|
||||
"""Choose the region, which sets the frequency with it.
|
||||
|
||||
On the front page of the menu because it is the one setting that decides
|
||||
whether anything is heard at all, and because being on the wrong channel
|
||||
sounds exactly like having no aerial.
|
||||
"""
|
||||
from . import aprs as ap
|
||||
from .ax25 import APRS_CHANNELS
|
||||
|
||||
_rule(console, "APRS channel")
|
||||
console.print(Panel(Text.from_markup(
|
||||
"The frequency is agreed between amateurs rather than allocated, so "
|
||||
"it differs by region and there is no way to discover it from the "
|
||||
"air: on the wrong channel there is [bold]silence, not a bad "
|
||||
"signal[/bold].\n\n"
|
||||
"If you do not know which applies, [cyan]f[/cyan] on the previous "
|
||||
"screen listens on each in turn and tells you which has traffic."),
|
||||
border_style="blue", padding=(0, 1)))
|
||||
t = Table(box=None, header_style="bold", pad_edge=False)
|
||||
t.add_column("#", style="grey62", width=3, justify="right")
|
||||
t.add_column("region", width=15)
|
||||
t.add_column("frequency", justify="right", width=12)
|
||||
t.add_column("used in", style="grey62", overflow="fold")
|
||||
for i, (region, hz, where) in enumerate(APRS_CHANNELS, 1):
|
||||
here = abs(hz - options.frequency) < 1.0
|
||||
t.add_row(str(i),
|
||||
Text(region, style="bold cyan" if here else "white"),
|
||||
f"{hz / 1e6:.3f} MHz", where + (" ← now" if here else ""))
|
||||
console.print(t)
|
||||
console.print("\n[grey62]Enter a number, a frequency in MHz for a "
|
||||
"channel that is not a region's, or blank to go back."
|
||||
"[/grey62]")
|
||||
answer = _ask(console, " channel").strip()
|
||||
if not answer or answer in _BACK:
|
||||
return
|
||||
if answer.isdigit() and 1 <= int(answer) <= len(APRS_CHANNELS):
|
||||
ap.use_region(options, APRS_CHANNELS[int(answer) - 1][0])
|
||||
console.print(f" [green]{ap.channel_text(options)}[/green]")
|
||||
return
|
||||
try:
|
||||
megahertz = float(answer)
|
||||
except ValueError:
|
||||
console.print(" [yellow]enter a number from the list, or a "
|
||||
"frequency in MHz[/yellow]")
|
||||
return
|
||||
hz = megahertz * 1e6 if megahertz < 1e6 else megahertz
|
||||
options.frequency = hz
|
||||
console.print(f" [green]{ap.channel_text(options)}[/green]")
|
||||
|
||||
|
||||
def _aprs_find(console: Console, options) -> None:
|
||||
"""Listen on every region's channel and say which has traffic."""
|
||||
from . import aprs as ap
|
||||
|
||||
_rule(console, "find the channel")
|
||||
console.print(Panel(Text.from_markup(
|
||||
"Each region's channel in turn, for a few seconds each. This answers "
|
||||
"the one question about APRS that cannot be answered on any single "
|
||||
"frequency, because the answer [bold]is[/bold] a frequency.\n\n"
|
||||
"A quiet channel is not proof of an empty one \u2014 a fixed station "
|
||||
"beacons every half hour \u2014 so what this finds is traffic, and "
|
||||
"what it misses is only the absence of traffic while it listened."),
|
||||
border_style="blue", padding=(0, 1)))
|
||||
answer = _ask(console, " seconds on each", "20").strip()
|
||||
try:
|
||||
seconds = max(2.0, float(answer))
|
||||
except ValueError:
|
||||
console.print(" [yellow]that is not a number of seconds[/yellow]")
|
||||
return
|
||||
console.print(f"[grey62]about {seconds * len(ap.ax25.APRS_CHANNELS):.0f} "
|
||||
f"seconds altogether \u2014 control-C stops it[/grey62]")
|
||||
try:
|
||||
found = ap.find_channel(console, options, seconds)
|
||||
except Exception as exc: # a menu must survive it
|
||||
console.print(f" [red]{exc}[/red]")
|
||||
return
|
||||
best = ap.report_channels(console, found, seconds)
|
||||
if best is None:
|
||||
return
|
||||
if _confirm(f" listen on {best.region} from now on"):
|
||||
ap.use_region(options, best.region)
|
||||
console.print(f" [green]{ap.channel_text(options)}[/green] "
|
||||
f"[grey62]\u2014 press s to keep it[/grey62]")
|
||||
|
||||
|
||||
def _aprs_listen(console: Console, cfg: ScanConfig, options) -> None:
|
||||
from . import aprs as ap
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue