Tell the APRS map which band it is on, and where you are standing

Three things wrong with the window, all reported from the chair in front of
it, and all three the same kind of wrong: the map had been handed the aircraft
map's furniture and nobody had checked which bits of it were about aircraft.

The empty screen said "listening on 1090 MHz" and went on to explain that an
aeroplane is placed once an even and an odd position frame have both arrived.
That sentence was true of the window it was written for and false of every
word in this one.  What has to arrive before a mark can be placed is a fact
about the signal rather than about the window, so it is now a thing the window
is told: an aeroplane still says what an aeroplane needs, a station says that
most of them mention where they are every few minutes, and left unsaid the
words follow whatever channel the window was given rather than naming 1090
from memory.

There was no red flag.  The flag is drawn where the receiver was actually told
it is, and this section has its own --at that had never been filled in -- but
the aircraft section's had, by the same person, about the same aerial, on the
same roof.  One aerial does not move because the receiver was pointed at a
different band, so a position set on either side now serves both, this
section's own winning where it has one because two receivers in two places is
exactly what a separate setting is for.  It says out loud which it used, an
inherited position being a convenience right up until somebody has moved and
changed only one of them.  The flag, the range rings and every distance and
bearing all come from the same answer, so all four arrive together.

The boxes drew a station's position and never wrote it down.  A mark on a map
shows where something is; the figures are what gets read out over the air,
copied into a log or typed into something else.  They are also the only place
the doubt shows: a station that blanks its minutes is somewhere inside a
two-degree square, the diamond is as definite there as it is anywhere, and
only "49.0000N 72.0000W +/-340 km" admits it.

Twelve new tests against six deliberately broken builds.  One survived, and
it is the one that matters: with the drawing code changed back to print
aeroplanes at 1090 MHz, every test still passed, because they all read the
sentence off the object and none of them read the pixels.  That is the
reported fault exactly -- a window that holds one sentence and paints another
-- and the suite could not see it.  It reads the picture now, cropped below
the header, whose ticking clock has made a test pass for the wrong reason
twice already this session.  Full suite 2664 passed.  Built as
2026-09-21_02.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016PsWPTweCT6pwxKngvVxcg
This commit is contained in:
The Dust Council 2026-09-21 01:05:11 -07:00
parent 2c7b1ba25e
commit e05b66ec3d
8 changed files with 295 additions and 36 deletions

View file

@ -2480,19 +2480,31 @@ A diamond because it is the one shape on the picture with no front: a house
that beacons twice an hour is a place, and drawing it as a triangle would that beacons twice an hour is a place, and drawing it as a triangle would
have it pointing north for no reason. have it pointing north for no reason.
The box says what the station is, how far off and in which bearing, what it is The box says what the station is, **where it is in figures**, how far off and
doing if it is moving, its altitude, its weather, its status or comment, the in which bearing, what it is doing if it is moving, its altitude, its weather,
digipeaters it came through, how many packets and how many of those arrived its status or comment, the digipeaters it came through, how many packets and
directly, and how strongly. Boxes are placed where they cover nothing else and how many of those arrived directly, and how strongly. The mark shows where a
glide when their station moves, and where there is no room for one the mark is station is; the figures are what gets written down, read out over the air or
still drawn — with an evening's accumulation there are usually more marks than typed into something else — and where a station blanked its minutes, the
there is room for boxes, so the most recently heard get them. figures are the only place the vagueness shows (`49.0000N 72.0000W ±340 km`),
because a mark on a map is as definite at ten miles of doubt as at ten yards.
Boxes are placed where they cover nothing else and glide when their station
moves, and where there is no room for one the mark is still drawn — with an
evening's accumulation there are usually more marks than there is room for
boxes, so the most recently heard get them.
The same keys as the aircraft map: `d` cycles how much each box says, `t` The same keys as the aircraft map: `d` cycles how much each box says, `t`
trails, `g` the map underneath, `[` and `]` its brightness, `+`/`-` the range, trails, `g` the map underneath, `[` and `]` its brightness, `+`/`-` the range,
`q` quits. `--radius` sets how far it reaches to begin with, `--theme` picks `q` quits. `--radius` sets how far it reaches to begin with and `--theme` picks
from the same five, and `--at LAT,LON` is what puts the flag and the rings from the same five.
anywhere useful.
**Where the aerial is** puts the red flag on the map, centres the range rings
and gives every station a distance and a bearing. Set it with `--at LAT,LON`,
or in the menu under **Receiver at** — and if you have already told the
aircraft side where you are, that is used and nothing needs saying twice. One
aerial on one roof does not move because the receiver was pointed at a
different band. It says which it used, because an inherited position is a
convenience right up until you have moved and changed only one of them.
### Afterwards ### Afterwards
@ -2532,7 +2544,7 @@ version that can.
| Write a log | `--log` / `--no-log` | yes | one line of JSON per packet | | Write a log | `--log` / `--no-log` | yes | one line of JSON per packet |
| Print every packet | `--packets` / `--no-packets` | no | a stream of lines instead of a table | | Print every packet | `--packets` / `--no-packets` | no | a stream of lines instead of a table |
| Keep on screen for | `--hold` | 3600 s | how long a station stays after its last packet | | Keep on screen for | `--hold` | 3600 s | how long a station stays after its last packet |
| Receiver at | `--at` | blank | where the aerial is, for distances | | Receiver at | `--at` | the aircraft setting | where the aerial is: the flag, the rings and the distances |
| Show readings in | `--units` | metric | metric or imperial, for the display and the export | | Show readings in | `--units` | metric | metric or imperial, for the display and the export |
| — | `--window` | — | open the live map instead of a table | | — | `--window` | — | open the live map instead of a table |
| Map reaches | `--radius` | 50 km | how far around the aerial the window reaches | | Map reaches | `--radius` | 50 km | how far around the aerial the window reaches |

View file

@ -9,7 +9,7 @@ and transcribing speech.
# 2026-08-21_02 is the second build made on the 21st. The revision is padded # 2026-08-21_02 is the second build made on the 21st. The revision is padded
# to two digits so versions sort as text. # to two digits so versions sort as text.
VERSION_DATE = "2026-09-21" VERSION_DATE = "2026-09-21"
VERSION_REVISION = 1 VERSION_REVISION = 2
__version__ = f"{VERSION_DATE}_{VERSION_REVISION:02d}" __version__ = f"{VERSION_DATE}_{VERSION_REVISION:02d}"

View file

@ -35,7 +35,7 @@ from .settings import Setting, format_value
__all__ = ["AprsOptions", "OPTIONS", "OPTION_GROUPS", "defaults", "in_group", __all__ = ["AprsOptions", "OPTIONS", "OPTION_GROUPS", "defaults", "in_group",
"channel_text", "find_channel", "report_channels", "Found", "channel_text", "find_channel", "report_channels", "Found",
"use_region", "use_region", "receiver_at", "coordinates",
"by_key", "format_option", "describe", "summarise", "Station", "by_key", "format_option", "describe", "summarise", "Station",
"Net", "Heard", "listen", "open_device", "open_log", "pump", "Net", "Heard", "listen", "open_device", "open_log", "pump",
"finish", "report", "watch", "windowed", "blip_for", "finish", "report", "watch", "windowed", "blip_for",
@ -203,9 +203,13 @@ OPTIONS: tuple[Setting, ...] = (
unit="s", minimum=1.0, flags=("--hold",), example="3600"), unit="s", minimum=1.0, flags=("--hold",), example="3600"),
O("location", "Receiver at", "Listening", "text", O("location", "Receiver at", "Listening", "text",
"where the aerial is, as latitude,longitude (blank = no distances)", "where the aerial is, as latitude,longitude (blank = no distances)",
"Only used to work out how far away each station is and in which " "Puts a red flag on the map where you are, draws the range rings "
"direction. Left blank the positions are still recorded and drawn; " "round it, and gives every station a distance and a bearing. Left "
"there is simply nothing to measure them from.", "blank, whatever the aircraft side was told is used instead -- one "
"aerial on one roof does not move because the receiver was pointed at "
"a different band -- and if neither has been told, positions are "
"still recorded and drawn and there is simply nothing to measure them "
"from.",
flags=("--at",), example="47.55,-122.30", metavar="LAT,LON"), flags=("--at",), example="47.55,-122.30", metavar="LAT,LON"),
# -- what to show --------------------------------------------------- # -- what to show ---------------------------------------------------
@ -419,6 +423,26 @@ def coordinates(text: str) -> tuple[float, float] | None:
return (lat, lon) if abs(lat) <= 90 and abs(lon) <= 180 else None return (lat, lon) if abs(lat) <= 90 and abs(lon) <= 180 else None
def receiver_at(options: AprsOptions) -> tuple[float, float] | None:
"""Where the aerial is standing, from here or from the aircraft settings.
One aerial, one roof, one position. Which band it is pointed at today
does not move it, so somebody who told the aircraft side where they are
should not have to tell this side the same thing again -- and, having
already done it once, would reasonably expect the flag to be there.
This section's own setting wins where it has one, because two receivers
in two places is a thing that happens and is exactly what the separate
setting is for.
"""
here = coordinates(options.location)
if here is not None:
return here
from . import aircraft as air
return coordinates(air.load_options().location)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# How far off, and which way # How far off, and which way
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -974,6 +998,7 @@ def listen(console, options: AprsOptions, output_dir: str,
log = open_log(console, options, output_dir, started, log_path) log = open_log(console, options, output_dir, started, log_path)
net = Net() net = Net()
heard.net = net heard.net = net
_say_where(console, options, receiver_at(options))
console.print(f"[grey62]listening on {options.frequency / 1e6:g} MHz at " console.print(f"[grey62]listening on {options.frequency / 1e6:g} MHz at "
f"{options.rate / 1e6:g} MS/s — control-C to stop" f"{options.rate / 1e6:g} MS/s — control-C to stop"
"[/grey62]") "[/grey62]")
@ -1012,6 +1037,23 @@ def listen(console, options: AprsOptions, output_dir: str,
return finish(console, options, output_dir, heard) return finish(console, options, output_dir, heard)
def _say_where(console, options: AprsOptions, home) -> None:
"""Where the aerial is taken to be, and where that came from.
Said rather than assumed, because a position inherited from the aircraft
settings is a convenience right up until somebody has moved and only
changed one of them.
"""
if home is None:
console.print("[grey62]no receiver position set, so no flag, no "
"rings and no distances — `--at LAT,LON` gives all "
"three[/grey62]")
return
if not coordinates(options.location):
console.print(f"[grey62]receiver at {home[0]:.4f},{home[1]:.4f}, "
f"from the aircraft settings[/grey62]")
def _open_display(console, options: AprsOptions, started: float): def _open_display(console, options: AprsOptions, started: float):
"""A live table where there is a terminal to draw it on, else nothing.""" """A live table where there is a terminal to draw it on, else nothing."""
if options.packets_seen or not getattr(console, "is_terminal", False): if options.packets_seen or not getattr(console, "is_terminal", False):
@ -1023,7 +1065,7 @@ def _open_display(console, options: AprsOptions, started: float):
display = AprsDisplay(console, hold=options.hold, display = AprsDisplay(console, hold=options.hold,
imperial=options.imperial, imperial=options.imperial,
frequency=options.frequency, frequency=options.frequency,
home=coordinates(options.location)) home=receiver_at(options))
display.started = started display.started = started
live = Live(display.render(), console=console, refresh_per_second=2, live = Live(display.render(), console=console, refresh_per_second=2,
screen=False, transient=False, vertical_overflow="crop") screen=False, transient=False, vertical_overflow="crop")
@ -1100,7 +1142,7 @@ def report(console, net: Net, options: AprsOptions) -> None:
from .packets import height_text from .packets import height_text
from .acurite import Measure, format_measure from .acurite import Measure, format_measure
home = coordinates(options.location) home = receiver_at(options)
stations = sorted(net.all(), key=lambda s: (-s.packets, s.call)) stations = sorted(net.all(), key=lambda s: (-s.packets, s.call))
t = Table(box=None, header_style="bold", pad_edge=False, t = Table(box=None, header_style="bold", pad_edge=False,
@ -1377,6 +1419,13 @@ def station_lines(station: Station, home=None,
out.append(("placed by", station.object_of, "")) out.append(("placed by", station.object_of, ""))
if station.symbol: if station.symbol:
out.append(("symbol", station.symbol, "")) out.append(("symbol", station.symbol, ""))
if station.position is not None:
# Where it actually said it was, in figures. A mark on a map shows
# where a station is and a number is what gets written down, read
# out over the air, or typed into something else -- and where the
# station blanked its minutes, the figures are the only place the
# vagueness shows.
out.append(("position", station.position.describe(), ""))
away = station.away(home) away = station.away(home)
if away is not None: if away is not None:
out.append(("away", f"{format_measure(Measure('', away[0], 'km'), imperial)}" out.append(("away", f"{format_measure(Measure('', away[0], 'km'), imperial)}"
@ -1436,7 +1485,7 @@ def watch(console, options: AprsOptions, output_dir: str,
log = open_log(console, options, output_dir, started, log_path) log = open_log(console, options, output_dir, started, log_path)
net = Net() net = Net()
heard.net = net heard.net = net
home = coordinates(options.location) home = receiver_at(options)
# Set before the window is built, because the window reads its colours # Set before the window is built, because the window reads its colours
# out of the palette this writes. # out of the palette this writes.
set_theme(options.theme) set_theme(options.theme)
@ -1450,7 +1499,10 @@ def watch(console, options: AprsOptions, output_dir: str,
# a map, which is the whole point of pointing a window at this band. # a map, which is the whole point of pointing a window at this band.
fades=False, fades=False,
channel=f"{options.frequency / 1e6:g} MHz", channel=f"{options.frequency / 1e6:g} MHz",
subject="on the map", counted="packets") subject="on the map", counted="packets",
waiting=(f"listening on {options.frequency / 1e6:g} MHz\n\n"
"nothing placed yet — a station appears once it has said\n"
"where it is, which most do every few minutes"))
sky.started = started sky.started = started
sky.log_name = log.path.name if log is not None else "" sky.log_name = log.path.name if log is not None else ""
if options.simulate: if options.simulate:
@ -1481,6 +1533,7 @@ def watch(console, options: AprsOptions, output_dir: str,
daemon=True, name="aprs-basemap")) daemon=True, name="aprs-basemap"))
for thread in threads: for thread in threads:
thread.start() thread.start()
_say_where(console, options, home)
console.print(f"[grey62]listening on {options.frequency / 1e6:g} MHz — " console.print(f"[grey62]listening on {options.frequency / 1e6:g} MHz — "
"close the window to stop[/grey62]") "close the window to stop[/grey62]")
if log is not None: if log is not None:

View file

@ -382,7 +382,8 @@ class Sky:
box_opacity: float = 0.85, pulse: float = 0.0, box_opacity: float = 0.85, pulse: float = 0.0,
echo: float = 0.0, echo_reach: float = 0.0, echo: float = 0.0, echo_reach: float = 0.0,
fades: bool = True, channel: str = "1090 MHz", fades: bool = True, channel: str = "1090 MHz",
subject: str = "overhead", counted: str = "frames"): subject: str = "overhead", counted: str = "frames",
waiting: str = ""):
import threading import threading
self.unit = unit self.unit = unit
@ -399,6 +400,16 @@ class Sky:
self.channel = channel self.channel = channel
self.subject = subject self.subject = subject
self.counted = counted self.counted = counted
# What the empty picture says while there is nothing to draw on it.
# Said here rather than in the drawing, because what has to arrive
# before a mark can be placed is a fact about the signal and not
# about the window: an aeroplane needs two position frames of
# opposite parity, and an amateur station needs to have mentioned
# where it is, which not all of them ever do.
self.waiting = waiting or (
f"listening on {channel}\n\n"
"nothing placed yet — an aircraft is on the map once an even\n"
"and an odd position frame have both arrived")
self.hold = hold self.hold = hold
self.home = home self.home = home
self.radius_nm = radius_nm self.radius_nm = radius_nm
@ -982,10 +993,7 @@ def _build():
painter.setPen(rgb(INK)) painter.setPen(rgb(INK))
painter.drawText(self.rect(), _enum(Qt, "AlignmentFlag", painter.drawText(self.rect(), _enum(Qt, "AlignmentFlag",
"AlignCenter"), "AlignCenter"),
"listening on 1090 MHz\n\n" self.sky.waiting)
"nothing placed yet — an aircraft is on the map "
"once an even\nand an odd position frame have "
"both arrived")
def _draw_ground(self, painter, view) -> None: def _draw_ground(self, painter, view) -> None:
levels, box = self.sky.ground_covering(view.south, view.west, levels, box = self.sky.ground_covering(view.south, view.west,

View file

@ -1,5 +1,5 @@
.\" Generated by packaging/make-man.py -- do not edit by hand. .\" Generated by packaging/make-man.py -- do not edit by hand.
.TH BANDSAUNTER 1 "2026-09-21" "bandsaunter 2026-09-21_01" "User Commands" .TH BANDSAUNTER 1 "2026-09-21" "bandsaunter 2026-09-21_02" "User Commands"
.SH NAME .SH NAME
bandsaunter \- scan, record and identify radio signals with an RTL-SDR bandsaunter \- scan, record and identify radio signals with an RTL-SDR
.SH SYNOPSIS .SH SYNOPSIS
@ -2688,10 +2688,12 @@ the one shape on the picture with no front \[em] and coloured off the same
altitude ramp the aircraft use, that being the one set of colours every theme altitude ramp the aircraft use, that being the one set of colours every theme
defines, so a digipeater stays distinguishable from a car on all five. defines, so a digipeater stays distinguishable from a car on all five.
.PP .PP
The box says what the station is, how far off and in which bearing, what it is The box says what the station is, where it is in figures, how far off and in
doing if it is moving, its altitude, its weather, its status, the digipeaters which bearing, what it is doing if it is moving, its altitude, its weather, its
it came through, how many packets and how many arrived directly, and how status, the digipeaters it came through, how many packets and how many arrived
strongly. Boxes are placed where they cover nothing else and glide when their directly, and how strongly. The mark shows where a station is and the figures
are what gets written down \[em] and where a station blanked its minutes, the
figures are the only place that shows. Boxes are placed where they cover nothing else and glide when their
station moves; where there is no room for one the mark is still drawn, and the station moves; where there is no room for one the mark is still drawn, and the
most recently heard get the boxes. most recently heard get the boxes.
.PP .PP
@ -2712,8 +2714,14 @@ and
the range, and the range, and
.B q .B q
to quit. to quit.
.PP
.BI \-\-at " LAT,LON" .BI \-\-at " LAT,LON"
is what puts the flag and the rings anywhere useful. puts the red flag on the map, centres the range rings and gives every station a
distance and a bearing. Left unset, whatever the aircraft side was told is used
instead \[em] one aerial on one roof does not move because the receiver was
pointed at a different band \[em] and which was used is said out loud, an
inherited position being a convenience right up until somebody has moved and
changed only one of them.
.SS Afterwards .SS Afterwards
Three tables. Stations heard is about the band and the aerial: where each was, Three tables. Stations heard is about the band and the aerial: where each was,
how far off, how many packets, how many of those arrived directly rather than how far off, how many packets, how many of those arrived directly rather than

View file

@ -1686,10 +1686,12 @@ the one shape on the picture with no front \[em] and coloured off the same
altitude ramp the aircraft use, that being the one set of colours every theme altitude ramp the aircraft use, that being the one set of colours every theme
defines, so a digipeater stays distinguishable from a car on all five. defines, so a digipeater stays distinguishable from a car on all five.
.PP .PP
The box says what the station is, how far off and in which bearing, what it is The box says what the station is, where it is in figures, how far off and in
doing if it is moving, its altitude, its weather, its status, the digipeaters which bearing, what it is doing if it is moving, its altitude, its weather, its
it came through, how many packets and how many arrived directly, and how status, the digipeaters it came through, how many packets and how many arrived
strongly. Boxes are placed where they cover nothing else and glide when their directly, and how strongly. The mark shows where a station is and the figures
are what gets written down \[em] and where a station blanked its minutes, the
figures are the only place that shows. Boxes are placed where they cover nothing else and glide when their
station moves; where there is no room for one the mark is still drawn, and the station moves; where there is no room for one the mark is still drawn, and the
most recently heard get the boxes. most recently heard get the boxes.
.PP .PP
@ -1710,8 +1712,14 @@ and
the range, and the range, and
.B q .B q
to quit. to quit.
.PP
.BI \-\-at " LAT,LON" .BI \-\-at " LAT,LON"
is what puts the flag and the rings anywhere useful. puts the red flag on the map, centres the range rings and gives every station a
distance and a bearing. Left unset, whatever the aircraft side was told is used
instead \[em] one aerial on one roof does not move because the receiver was
pointed at a different band \[em] and which was used is said out loud, an
inherited position being a convenience right up until somebody has moved and
changed only one of them.
.SS Afterwards .SS Afterwards
Three tables. Stations heard is about the band and the aerial: where each was, Three tables. Stations heard is about the band and the aerial: where each was,
how far off, how many packets, how many of those arrived directly rather than how far off, how many packets, how many of those arrived directly rather than

View file

@ -1005,3 +1005,126 @@ def test_the_window_says_so_rather_than_failing_when_qt_is_missing(
heard = ap.watch(console, ap.AprsOptions(), str(tmp_path)) heard = ap.watch(console, ap.AprsOptions(), str(tmp_path))
assert heard.stations == 0 assert heard.stations == 0
assert "no window to open" in cap.get() assert "no window to open" in cap.get()
# ---------------------------------------------------------------------------
# Where the aerial is
# ---------------------------------------------------------------------------
def test_the_aerial_position_is_taken_from_the_aircraft_settings_if_unset(
tmp_path, monkeypatch):
"""One aerial, one roof, one position.
Which band it is pointed at today does not move it, so somebody who has
already told the aircraft side where they are should not have to say it
again -- and, having said it once, would reasonably expect the flag.
"""
from bandsaunter import aircraft
aircraft.save_options(
aircraft.AircraftOptions(location="47.55,-122.30"), tmp_path)
monkeypatch.setattr(aircraft, "options_path",
lambda directory=None: tmp_path / "aircraft.yaml")
assert ap.receiver_at(ap.AprsOptions()) == (47.55, -122.30)
def test_this_sections_own_position_wins_where_it_has_one(tmp_path,
monkeypatch):
"""Two receivers in two places is a thing that happens, and is exactly
what the separate setting is for."""
from bandsaunter import aircraft
aircraft.save_options(
aircraft.AircraftOptions(location="47.55,-122.30"), tmp_path)
monkeypatch.setattr(aircraft, "options_path",
lambda directory=None: tmp_path / "aircraft.yaml")
here = ap.receiver_at(ap.AprsOptions(location="51.5,-0.13"))
assert here == (51.5, -0.13)
def test_neither_side_knowing_is_no_position_rather_than_a_wrong_one(
tmp_path, monkeypatch):
from bandsaunter import aircraft
monkeypatch.setattr(aircraft, "options_path",
lambda directory=None: tmp_path / "none.yaml")
assert ap.receiver_at(ap.AprsOptions()) is None
def test_the_window_gets_the_position_so_the_flag_is_drawn(tmp_path,
monkeypatch):
from bandsaunter import aircraft, livemap
aircraft.save_options(
aircraft.AircraftOptions(location="47.55,-122.30"), tmp_path)
monkeypatch.setattr(aircraft, "options_path",
lambda directory=None: tmp_path / "aircraft.yaml")
built = {}
monkeypatch.setattr(livemap, "available", lambda: True)
monkeypatch.setattr(livemap, "show",
lambda sky, title: built.update(home=sky.home,
waiting=sky.waiting))
monkeypatch.setattr(ap, "open_device", lambda console, opts: Silence(1))
console = Console(width=120, force_terminal=False)
with console.capture():
ap.watch(console, ap.AprsOptions(rate=RATE, log=False, report=False,
basemap=False), str(tmp_path))
assert built["home"] == (47.55, -122.30)
# And the empty picture no longer talks about aeroplanes.
assert "1090" not in built["waiting"] and "144.39 MHz" in built["waiting"]
assert "aircraft" not in built["waiting"]
def test_it_says_where_the_position_came_from_rather_than_assuming(
tmp_path, monkeypatch):
"""A position inherited from another section is a convenience right up
until somebody has moved and changed only one of them."""
from bandsaunter import aircraft, livemap
aircraft.save_options(
aircraft.AircraftOptions(location="47.55,-122.30"), tmp_path)
monkeypatch.setattr(aircraft, "options_path",
lambda directory=None: tmp_path / "aircraft.yaml")
monkeypatch.setattr(livemap, "available", lambda: True)
monkeypatch.setattr(livemap, "show", lambda sky, title: None)
monkeypatch.setattr(ap, "open_device", lambda console, opts: Silence(1))
console = Console(width=120, force_terminal=False)
with console.capture() as cap:
ap.watch(console, ap.AprsOptions(rate=RATE, log=False, report=False,
basemap=False), str(tmp_path))
assert "from the aircraft settings" in cap.get()
def test_knowing_nowhere_says_what_that_costs(tmp_path, monkeypatch):
from bandsaunter import aircraft
monkeypatch.setattr(aircraft, "options_path",
lambda directory=None: tmp_path / "none.yaml")
monkeypatch.setattr(ap, "open_device", lambda console, opts: Silence(2))
console = Console(width=120, force_terminal=False)
with console.capture() as cap:
ap.listen(console, ap.AprsOptions(rate=RATE, log=False,
packets_seen=True), str(tmp_path))
out = cap.get()
assert "no flag" in out and "--at" in out
def test_the_box_gives_the_position_in_figures():
"""A mark shows where a station is; a number is what gets written down."""
rows = dict((label, value) for label, value, _f
in ap.station_lines(station_of()))
assert rows["position"] == "49.0583N 72.0292W"
def test_the_box_shows_a_vague_position_as_vague():
"""Blanking the minutes is a deliberate act by the operator, and the
figures are the only place that shows."""
rows = dict((label, value) for label, value, _f
in ap.station_lines(station_of("=49 . N/072 . W-vague")))
assert "±" in rows["position"]
def test_a_station_that_never_said_where_it_is_has_no_position_row():
rows = dict((label, value) for label, value, _f
in ap.station_lines(station_of(">Monitoring 146.52")))
assert "position" not in rows

View file

@ -2255,3 +2255,50 @@ def test_a_symbol_that_points_turns_with_its_heading(app):
north = _around_the_mark("vehicle", track=0.0) north = _around_the_mark("vehicle", track=0.0)
east = _around_the_mark("vehicle", track=90.0) east = _around_the_mark("vehicle", track=90.0)
assert int((north != east).any(axis=2).sum()) > 20 assert int((north != east).any(axis=2).sum()) > 20
@qt
def test_an_empty_picture_says_what_it_is_waiting_for(app):
"""What has to arrive before a mark can be placed is a fact about the
signal, not about the window: an aeroplane needs two position frames of
opposite parity, an amateur station needs to have mentioned where it is,
and the window had the first of those written into it."""
from bandsaunter.livemap import _build
plane = Sky()
assert "1090 MHz" in plane.waiting and "position frame" in plane.waiting
station = Sky(channel="144.39 MHz", fades=False,
waiting="listening on 144.39 MHz\n\nnothing placed yet")
assert "1090" not in station.waiting
# And with nowhere to centre on, that is what gets drawn.
assert station.centre() is None
view = _build()["SkyView"](station)
assert _painted(_rendered(view)) > 200
@qt
def test_the_waiting_words_follow_the_channel_when_none_are_given(app):
assert "144.39 MHz" in Sky(channel="144.39 MHz").waiting
@qt
def test_the_empty_picture_draws_the_words_it_was_given(app):
"""Holding the sentence is not the same as painting it. The complaint
that started this was about what the window said while it waited, and a
window that stores one sentence and draws another is that fault exactly
-- so the test has to read the pixels, not the attribute."""
from bandsaunter.livemap import _build
def below_the_header(sky):
# Cropped past the header, whose clock ticks: two pictures taken a
# moment apart differ up there for reasons that have nothing to do
# with the words in the middle.
return _rendered(_build()["SkyView"](sky))[60:, :, :]
plane = below_the_header(Sky())
assert not (plane != below_the_header(Sky())).any(), "not steady"
station = below_the_header(Sky(
channel="144.39 MHz", fades=False,
waiting="listening on 144.39 MHz\n\nnothing placed yet — a station "
"appears\nonce it has said where it is"))
assert int((plane != station).any(axis=2).sum()) > 200