Read the weather sensors on 433 MHz, and let them be given names

A consumer weather station is two things.  The display on the kitchen wall is
one of them; the other is a plastic box on a fence post that says what it can
see every sixteen seconds, in the clear, to anyone who happens to be
listening.  This reads the box.

A section of its own, like the aircraft one, and for the same reason: it does
not fit through the scanner.  A sensor message is a burst of a carrier
switched on and off, a fifth of a second long, and the scan path is a squelch
and a recorder -- it would record the bursts as clicks in a WAV file and
decode nothing.  `bandsaunter weather` listens, `bandsaunter readings` reads
a log back, `bandsaunter sensors` says what is out there.  Item 6 in the main
menu is the same thing without a command line.

Five families: the Tower 592TXR, the 5-in-1, the 6045M lightning detector,
the 609TXC and the 606TX.  Temperature, humidity, wind speed and direction,
rainfall, strike counts, how far off the storm is, and battery state from all
of them.  Every one is implemented from its published description and checked
against frames built from the same description, which proves the framing, the
parity, the checksums and the arithmetic and is not the same as having held
one of each.

The naming is the point.  A sensor broadcasts an identity, and that identity
is a number that came out of a hat in a factory; it tells one sensor from
another and is no use at all for telling which is which.  So press n while
listening: the display comes down, the sensors are listed, you name one, and
it goes back up, with the receiver running throughout.  That is the moment it
is possible -- the sensor is on the screen saying 3.1 degrees, and the person
watching is the one who knows that the cold one is the shed.  An hour later it
is a list of hexadecimal again.  Names are written the instant they are given
rather than at exit, to a neighbouring file renamed over the old one, and one
given before a sensor has ever been heard waits under its identity and moves
across when the first message says which model it is.

Four things keep the neighbours' doorbells off the display.  The checks the
message carries; a second copy, for the two models that carry only one byte
of check between them; a plausibility range, because a checksum can be
satisfied by a message the hardware could not send; and where in the burst
the message sits.  That last one is the one that is easy to miss: a seven-byte
message read out of the front of a real eight-byte one is made of that
message's own payload bytes, whose parity is already correct, so the parity
bits contribute nothing and one byte of sum is all that is left -- and
corroboration cannot help, the three copies being identical.  What gives that
window away every time is that it ends a whole byte before the burst does.

The Atlas is nine bytes like the lightning detector and lays its payload out
differently, so every decoder insists on a message type it knows.  Anything
else that frames correctly is reported with its identity and no weather,
because wrong weather under somebody's sensor name is a worse answer than
none.

ism.py now delegates to this rather than keeping a second implementation of
the tower sensor, which fixes the channel letters -- A is 3, B is 2, C is 0,
and there is no D -- and the battery bit, which is set while the battery is
good.  The two thinly-checked models are not reported from a scan at all: a
scan hears one burst, and they need two.

The option menus are now handed the module that owns the options rather than
importing the aircraft one, so one set of screens drives both sections and
will drive a third.

169 new tests, checked against nineteen deliberately broken builds; two of the
tests were too weak to notice their own mutation and were rewritten.  Full
suite 2252 passed.  Built as 2026-09-07_01.

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-07 13:40:33 -07:00
parent f01de4117f
commit 65cc03b78d
18 changed files with 6006 additions and 123 deletions

View file

@ -1,5 +1,5 @@
.\" Generated by packaging/make-man.py -- do not edit by hand.
.TH BANDSAUNTER 1 "2026-09-06" "bandsaunter 2026-09-06_04" "User Commands"
.TH BANDSAUNTER 1 "2026-09-07" "bandsaunter 2026-09-07_01" "User Commands"
.SH NAME
bandsaunter \- scan, record and identify radio signals with an RTL-SDR
.SH SYNOPSIS
@ -85,6 +85,20 @@ animation. See
.B AIRCRAFT
below.
.TP
.B weather
Listen to the AcuRite weather sensors on 433.92 MHz, and name them as they
arrive. See
.B WEATHER SENSORS
below.
.TP
.B readings
Read a weather log back: the report, and a spreadsheet. See
.B WEATHER SENSORS
below.
.TP
.B sensors
List every weather sensor heard, and give them names.
.TP
.B analyze
Identify a signal in an already-recorded file, decode Morse from it, or write
out the picture it turns out to be.
@ -2148,6 +2162,289 @@ sideband by which way the signal's energy leans, so
.B \-\-mode usb
is not needed. The frequency in the filename is the carrier \[em] the
frequency to dial into a radio.
.SH WEATHER SENSORS
A consumer weather station is two things. The display on the kitchen wall is
one of them; the other is a plastic box on a fence post that says what it can
see every sixteen seconds, in the clear, on 433.92 MHz, to anyone who happens
to be listening.
.B bandsaunter weather
reads the box.
.PP
It is a mode of its own, like the aircraft one, and for the same reason: it
does not fit through the scanner. A sensor message is a burst of a carrier
switched on and off, a fifth of a second long, and the scan path is a squelch
and a recorder \[em] it would record the bursts as clicks in a WAV file and
decode nothing.
.SS What it reads
Five families, each with its own framing and its own check.
.TP
.B "Tower 592TXR / 06002RM"
Seven bytes: temperature and humidity.
.TP
.B "5-in-1 06014RM / VN1TXC"
Eight bytes, in two kinds sent alternately: wind speed with wind direction and
rainfall, or wind speed with temperature and humidity. It has more to say than
fits in one message, so the display keeps the newest value of each quantity
rather than the newest message.
.TP
.B "Lightning 6045M"
Nine bytes: temperature, humidity, the cumulative strike count, and how far
off the storm is. A bit set when the detector believes it is being interfered
with is shown too, because a strike count that climbs while it is set is not
lightning.
.TP
.B 609TXC
Five bytes: temperature and humidity.
.TP
.B 606TX
Four bytes: temperature, and nothing else at all.
.PP
Battery state comes from all of them. The Atlas, the 986 and 515 fridge
thermometers, the 00275rm room monitor and the 899 standalone rain gauge are
on the same band and are not decoded; a message from one whose framing happens
to match is reported as an unknown message type with its identity and nothing
else, rather than guessed at.
.PP
These formats are implemented from their published descriptions and are
checked against frames built from the same descriptions. That proves the
framing, the parity, the checksums and the arithmetic; it is not the same as
having held every one of these sensors.
.SS Naming a sensor
A sensor broadcasts an identity, and that identity is a number that came out
of a hat in a factory \[em] or a different number out of the same hat the next
time the batteries were changed. It tells one sensor from another and is no
use at all for telling which is which.
.PP
So press
.B n
while listening. The display comes down, the sensors are listed with numbers,
you pick one and type a name, and it goes back up. The receiver keeps running
throughout: a slow typist loses a few seconds of weather and nothing else.
That is the moment it is possible to do \[em] the sensor is on the screen
saying 3.1 degrees, and the person watching is the one who knows that the cold
one is the shed.
.PP
Names can also be given with
.BI \-\-name " ID=NAME"
on
.B "bandsaunter weather"
or
.BR "bandsaunter sensors" ,
before or after anything has been heard: a name given before the sensor has
ever been received waits under its identity alone, because nothing yet knows
which model it is, and moves across the moment the first message arrives. They
live in
.I sensors.yaml
beside the settings, are written the moment they are given rather than when
the program exits, and are written to a neighbouring file which is renamed
over the old one, so a machine losing power halfway through leaves either the
old names or the new ones and never half of each. Nothing is ever dropped for
being stale.
.SS Why nothing false gets through
433 MHz is a crowded band \[em] doorbells, car keys, tyre-pressure sensors,
garage doors \[em] and a decoder that looks at every bit offset of every burst
will find a message in noise if it is allowed to. Four things stop it.
.PP
The three newer models carry an eight-bit sum plus odd parity in the top bit
of every payload byte, which is twelve to fourteen bits of check.
.PP
The two older models carry one byte of check between them, which is one false
message in two hundred and fifty-six, so those two are only believed when the
same message arrives twice. It costs nothing: these sensors send everything
three times in a row, for exactly this reason.
.PP
Nothing outside what the hardware can report is accepted \[em] no temperature
beyond \-40 to 70 \[de]C, no humidity above 100 per cent, no wind the
anemometer cannot physically produce.
.PP
And a message must sit where a message sits. A seven-byte message read out of
the front of a real eight-byte one is made of that message's own payload
bytes, whose parity is already correct, so the parity bits contribute nothing
and one byte of sum is all that is left. Corroboration does not help either,
the copies of a message being identical. What gives that window away every
time is that it ends a whole byte before the burst does.
.SS Getting it off the air
The receiver is tuned a little to one side of 433.92 MHz, because every
RTL-SDR puts a spike of its own at whatever it is tuned to, and a spike
sitting on top of a signal that works by being switched on and off is the one
thing that stops it being off. The sensors are shifted back to the middle in
software, which puts the spike out at the edge instead.
.B \-\-offset 0
tunes straight at them, which is worth trying once to see what the spike was
costing.
.PP
A running average of the complex samples then rejects the spike, and only
after that is the magnitude taken \[em] filtering before detection rather than
after is what keeps the neighbours out of the envelope of the sensor.
.PP
Slicing the envelope into bits never measures anything against a clock. The
newer sensors vary the length of the pulse and keep the gaps even; the two
older ones keep the pulse even and vary the gap. Both readings of the same
pulses are tried and the checksums say which it was. A transmitter running ten
per cent fast is read correctly and never noticed, which matters: these are
unlocked and drift with the temperature, and an outdoor sensor in January is
not the one that was on the fence in July.
.SS Afterwards
When the listening stops, two tables. The first is about reception \[em] who,
how often, how well \[em] and is the one to look at when something is missing:
these transmit on a fixed cycle, so a gap of thirty seconds from a sensor that
sends every sixteen means half of them are being missed, and that is an aerial
problem rather than a weather one. The second is the first, last, lowest and
highest of everything each sensor reported.
.PP
There is no average, deliberately. These arrive every sixteen seconds when the
sensor is in range and not at all when it is not, and rain and cold both
shorten the range of a 433 MHz transmitter, so the mean of what was received
is the mean of a sample whose gaps are themselves the weather. A bearing gets
no lowest or highest either: north is 0 and also 360.
.PP
.B \-\-csv
writes a column per quantity and a row per reading, with the sensor's name in
the second column and the unit in the heading rather than beside every number.
.B "bandsaunter readings \-\-csv"
does the same to an old log, and takes
.BI \-\-sensor " NAME"
to narrow it to one sensor.
.PP
The log keeps the raw bytes of every message underneath whatever was made of
them, because the message is the evidence and the rest of the line is an
opinion about it. Readings are converted once, on the way in, to Celsius,
kilometres an hour, millimetres and kilometres \[em] different models report
in different units \[em] so
.B \-\-units imperial
changes only what is shown, and can be changed afterwards on an old log.
.SS Without a sensor
.B \-\-simulate
puts six sensors on a fence that does not exist, one of every model,
transmitting real messages with real checksums, keyed on and off as a real one
does, through the real filter, the real slicer and the real decoders. Nothing
touches the receiver.
.SS If nothing is heard
These are a few milliwatts. A quarter-wave whip for 433.92 MHz is 17 cm of
wire, and the stock telescopic aerial set to about that length works well;
indoors, behind a wall, with the dongle in the back of a machine, is usually
the problem. Try
.B \-\-gain 40
if the automatic gain control is not finding them, and
.B \-\-messages
to watch individual receptions arrive while moving the aerial about.
.SH WEATHER OPTIONS
Every option the weather side takes, in the four groups the menu shows them
in. Each is a flag here and a line in the menu, and both come from one table
in the program, so they cannot disagree.
.SS Receiver
.TP
.B --device
Receiver \[em] which receiver to use, when more than one is plugged in.
.br
Setting name \fBdevice\fR, default \fB0\fR.
.br
Accepts: at least 0.
.TP
.B --gain
Gain \[em] tuner gain in dB, or automatic.
.br
Setting name \fBgain\fR, default \fBauto\fR.
.RS
.PP
Leave it automatic first. If a sensor you know is there never appears, try 40 or so.
.RE
.TP
.B --rate
Sample rate \[em] how fast to sample; a quarter of a megasample is the minimum (Hz).
.br
Setting name \fBrate\fR, default \fB1.024 MHz\fR.
.br
Accepts: at least 250000.
.TP
.B --frequency --freq
Sensors on \[em] where the sensors transmit (Hz).
.br
Setting name \fBfrequency\fR, default \fB433.92 MHz\fR.
.br
Accepts: at least 3e+08, at most 1e+09.
.TP
.B --offset
Tuning offset \[em] how far to one side of the sensors to tune the receiver (Hz).
.br
Setting name \fBoffset\fR, default \fB250 kHz\fR.
.br
Accepts: at least 0.
.RS
.PP
A quarter of the sample rate is right and is the default. Only set it to zero to see what the spike was costing.
.RE
.TP
.B --simulate / --no-simulate
Invent a garden \[em] put imaginary sensors on an imaginary fence.
.br
Setting name \fBsimulate\fR, default \fBno\fR.
.RS
.PP
Turn this on to see what the whole thing does without hardware. Turn it off to hear real sensors.
.RE
.PP
.SS Listening
.TP
.B --seconds
Listen for \[em] how long to listen before stopping (0 = until interrupted) (s).
.br
Setting name \fBseconds\fR, default \fBuntil stopped\fR.
.br
Accepts: at least 0.
.TP
.B --log / --no-log
Write a log \[em] write every message down as it arrives.
.br
Setting name \fBlog\fR, default \fByes\fR.
.TP
.B --messages / --no-messages
Print every message \[em] one line per message instead of a table that updates in place.
.br
Setting name \fBmessages\fR, default \fBno\fR.
.TP
.B --hold
Keep on screen for \[em] how long a sensor stays on the display after its last message (s).
.br
Setting name \fBhold\fR, default \fB1800 s\fR.
.br
Accepts: at least 1.
.PP
.SS Sensors
.TP
.B --units
Show readings in \[em] metric or imperial, for the display and the export.
.br
Setting name \fBunits\fR, default \fBmetric\fR.
.br
Accepts: one of: metric, imperial.
.TP
.B --only-named / --all-sensors
Only named sensors \[em] ignore sensors that have not been given a name.
.br
Setting name \fBonly_named\fR, default \fBno\fR.
.RS
.PP
Leave it off until you have named things, or there will be nothing to name.
.RE
.TP
.B --unknown / --no-unknown
Show unreadable models \[em] list sensors whose messages framed correctly and were not understood.
.br
Setting name \fBunknown\fR, default \fByes\fR.
.PP
.SS Afterwards
.TP
.B --report / --no-report
Report at the end \[em] print what each sensor said when the listening stops.
.br
Setting name \fBreport\fR, default \fByes\fR.
.TP
.B --csv / --no-csv
Also write a spreadsheet \[em] write the readings as CSV beside the log.
.br
Setting name \fBcsv\fR, default \fBno\fR.
.PP
.SH FILES
.TP
.I ~/.config/bandsaunter/config.yaml
@ -2156,6 +2453,13 @@ The settings every run starts from.
.I ~/.config/bandsaunter/aircraft.yaml
The aircraft options, as saved from the menus.
.TP
.I ~/.config/bandsaunter/weather.yaml
The weather options, as saved from the menus.
.TP
.I ~/.config/bandsaunter/sensors.yaml
What each weather sensor is called. The only file here holding anything a
person typed; safe to edit by hand.
.TP
.I ~/.config/bandsaunter/*.yaml
Named profiles.
.TP
@ -2164,6 +2468,11 @@ Every ADS-B frame heard in one listening session, with a
.I .txt
report and any picture drawn from it beside it.
.TP
.IR weather_ * .jsonl
Every weather sensor message heard in one listening session, with a
.I .csv
of the readings beside it where one was asked for.
.TP
.I ~/bandsaunter/
Where recordings, transcripts and logs are written, unless
.B \-\-output

View file

@ -57,15 +57,29 @@ def settings_section() -> list[str]:
def aircraft_section() -> list[str]:
"""Every ADS-B option, from the same table the menu and flags come from.
Written out rather than described in prose, so that an option added to
the program cannot quietly fail to appear in its manual.
"""
"""Every ADS-B option, from the same table the menu and flags come from."""
from bandsaunter import aircraft as air
return options_section(air)
def weather_section() -> list[str]:
"""Every weather option, from the same table."""
from bandsaunter import weather as wx
return options_section(wx)
def options_section(air) -> list[str]:
"""One section's options, written out from the table the program uses.
Written out rather than described in prose, so that an option added to
the program cannot quietly fail to appear in its manual. Both sections
describe their options in the same shape, so this does not need to know
which one it has been handed.
"""
out = []
defaults = air.AircraftOptions()
defaults = air.defaults()
for group in air.OPTION_GROUPS:
out.append(f'.SS {esc(group)}')
for o in air.in_group(group):
@ -180,6 +194,20 @@ animation. See
.B AIRCRAFT
below.
.TP
.B weather
Listen to the AcuRite weather sensors on 433.92 MHz, and name them as they
arrive. See
.B WEATHER SENSORS
below.
.TP
.B readings
Read a weather log back: the report, and a spreadsheet. See
.B WEATHER SENSORS
below.
.TP
.B sensors
List every weather sensor heard, and give them names.
.TP
.B analyze
Identify a signal in an already-recorded file, decode Morse from it, or write
out the picture it turns out to be.
@ -1246,6 +1274,177 @@ sideband by which way the signal's energy leans, so
.B \-\-mode usb
is not needed. The frequency in the filename is the carrier \[em] the
frequency to dial into a radio.
.SH WEATHER SENSORS
A consumer weather station is two things. The display on the kitchen wall is
one of them; the other is a plastic box on a fence post that says what it can
see every sixteen seconds, in the clear, on 433.92 MHz, to anyone who happens
to be listening.
.B bandsaunter weather
reads the box.
.PP
It is a mode of its own, like the aircraft one, and for the same reason: it
does not fit through the scanner. A sensor message is a burst of a carrier
switched on and off, a fifth of a second long, and the scan path is a squelch
and a recorder \[em] it would record the bursts as clicks in a WAV file and
decode nothing.
.SS What it reads
Five families, each with its own framing and its own check.
.TP
.B "Tower 592TXR / 06002RM"
Seven bytes: temperature and humidity.
.TP
.B "5-in-1 06014RM / VN1TXC"
Eight bytes, in two kinds sent alternately: wind speed with wind direction and
rainfall, or wind speed with temperature and humidity. It has more to say than
fits in one message, so the display keeps the newest value of each quantity
rather than the newest message.
.TP
.B "Lightning 6045M"
Nine bytes: temperature, humidity, the cumulative strike count, and how far
off the storm is. A bit set when the detector believes it is being interfered
with is shown too, because a strike count that climbs while it is set is not
lightning.
.TP
.B 609TXC
Five bytes: temperature and humidity.
.TP
.B 606TX
Four bytes: temperature, and nothing else at all.
.PP
Battery state comes from all of them. The Atlas, the 986 and 515 fridge
thermometers, the 00275rm room monitor and the 899 standalone rain gauge are
on the same band and are not decoded; a message from one whose framing happens
to match is reported as an unknown message type with its identity and nothing
else, rather than guessed at.
.PP
These formats are implemented from their published descriptions and are
checked against frames built from the same descriptions. That proves the
framing, the parity, the checksums and the arithmetic; it is not the same as
having held every one of these sensors.
.SS Naming a sensor
A sensor broadcasts an identity, and that identity is a number that came out
of a hat in a factory \[em] or a different number out of the same hat the next
time the batteries were changed. It tells one sensor from another and is no
use at all for telling which is which.
.PP
So press
.B n
while listening. The display comes down, the sensors are listed with numbers,
you pick one and type a name, and it goes back up. The receiver keeps running
throughout: a slow typist loses a few seconds of weather and nothing else.
That is the moment it is possible to do \[em] the sensor is on the screen
saying 3.1 degrees, and the person watching is the one who knows that the cold
one is the shed.
.PP
Names can also be given with
.BI \-\-name " ID=NAME"
on
.B "bandsaunter weather"
or
.BR "bandsaunter sensors" ,
before or after anything has been heard: a name given before the sensor has
ever been received waits under its identity alone, because nothing yet knows
which model it is, and moves across the moment the first message arrives. They
live in
.I sensors.yaml
beside the settings, are written the moment they are given rather than when
the program exits, and are written to a neighbouring file which is renamed
over the old one, so a machine losing power halfway through leaves either the
old names or the new ones and never half of each. Nothing is ever dropped for
being stale.
.SS Why nothing false gets through
433 MHz is a crowded band \[em] doorbells, car keys, tyre-pressure sensors,
garage doors \[em] and a decoder that looks at every bit offset of every burst
will find a message in noise if it is allowed to. Four things stop it.
.PP
The three newer models carry an eight-bit sum plus odd parity in the top bit
of every payload byte, which is twelve to fourteen bits of check.
.PP
The two older models carry one byte of check between them, which is one false
message in two hundred and fifty-six, so those two are only believed when the
same message arrives twice. It costs nothing: these sensors send everything
three times in a row, for exactly this reason.
.PP
Nothing outside what the hardware can report is accepted \[em] no temperature
beyond \-40 to 70 \[de]C, no humidity above 100 per cent, no wind the
anemometer cannot physically produce.
.PP
And a message must sit where a message sits. A seven-byte message read out of
the front of a real eight-byte one is made of that message's own payload
bytes, whose parity is already correct, so the parity bits contribute nothing
and one byte of sum is all that is left. Corroboration does not help either,
the copies of a message being identical. What gives that window away every
time is that it ends a whole byte before the burst does.
.SS Getting it off the air
The receiver is tuned a little to one side of 433.92 MHz, because every
RTL-SDR puts a spike of its own at whatever it is tuned to, and a spike
sitting on top of a signal that works by being switched on and off is the one
thing that stops it being off. The sensors are shifted back to the middle in
software, which puts the spike out at the edge instead.
.B \-\-offset 0
tunes straight at them, which is worth trying once to see what the spike was
costing.
.PP
A running average of the complex samples then rejects the spike, and only
after that is the magnitude taken \[em] filtering before detection rather than
after is what keeps the neighbours out of the envelope of the sensor.
.PP
Slicing the envelope into bits never measures anything against a clock. The
newer sensors vary the length of the pulse and keep the gaps even; the two
older ones keep the pulse even and vary the gap. Both readings of the same
pulses are tried and the checksums say which it was. A transmitter running ten
per cent fast is read correctly and never noticed, which matters: these are
unlocked and drift with the temperature, and an outdoor sensor in January is
not the one that was on the fence in July.
.SS Afterwards
When the listening stops, two tables. The first is about reception \[em] who,
how often, how well \[em] and is the one to look at when something is missing:
these transmit on a fixed cycle, so a gap of thirty seconds from a sensor that
sends every sixteen means half of them are being missed, and that is an aerial
problem rather than a weather one. The second is the first, last, lowest and
highest of everything each sensor reported.
.PP
There is no average, deliberately. These arrive every sixteen seconds when the
sensor is in range and not at all when it is not, and rain and cold both
shorten the range of a 433 MHz transmitter, so the mean of what was received
is the mean of a sample whose gaps are themselves the weather. A bearing gets
no lowest or highest either: north is 0 and also 360.
.PP
.B \-\-csv
writes a column per quantity and a row per reading, with the sensor's name in
the second column and the unit in the heading rather than beside every number.
.B "bandsaunter readings \-\-csv"
does the same to an old log, and takes
.BI \-\-sensor " NAME"
to narrow it to one sensor.
.PP
The log keeps the raw bytes of every message underneath whatever was made of
them, because the message is the evidence and the rest of the line is an
opinion about it. Readings are converted once, on the way in, to Celsius,
kilometres an hour, millimetres and kilometres \[em] different models report
in different units \[em] so
.B \-\-units imperial
changes only what is shown, and can be changed afterwards on an old log.
.SS Without a sensor
.B \-\-simulate
puts six sensors on a fence that does not exist, one of every model,
transmitting real messages with real checksums, keyed on and off as a real one
does, through the real filter, the real slicer and the real decoders. Nothing
touches the receiver.
.SS If nothing is heard
These are a few milliwatts. A quarter-wave whip for 433.92 MHz is 17 cm of
wire, and the stock telescopic aerial set to about that length works well;
indoors, behind a wall, with the dongle in the back of a machine, is usually
the problem. Try
.B \-\-gain 40
if the automatic gain control is not finding them, and
.B \-\-messages
to watch individual receptions arrive while moving the aerial about.
.SH WEATHER OPTIONS
Every option the weather side takes, in the four groups the menu shows them
in. Each is a flag here and a line in the menu, and both come from one table
in the program, so they cannot disagree.
.WEATHER_OPTIONS_HERE
.SH FILES
.TP
.I ~/.config/bandsaunter/config.yaml
@ -1254,6 +1453,13 @@ The settings every run starts from.
.I ~/.config/bandsaunter/aircraft.yaml
The aircraft options, as saved from the menus.
.TP
.I ~/.config/bandsaunter/weather.yaml
The weather options, as saved from the menus.
.TP
.I ~/.config/bandsaunter/sensors.yaml
What each weather sensor is called. The only file here holding anything a
person typed; safe to edit by hand.
.TP
.I ~/.config/bandsaunter/*.yaml
Named profiles.
.TP
@ -1262,6 +1468,11 @@ Every ADS-B frame heard in one listening session, with a
.I .txt
report and any picture drawn from it beside it.
.TP
.IR weather_ * .jsonl
Every weather sensor message heard in one listening session, with a
.I .csv
of the readings beside it where one was asked for.
.TP
.I ~/bandsaunter/
Where recordings, transcripts and logs are written, unless
.B \-\-output
@ -1373,6 +1584,8 @@ def main() -> int:
text = "\n".join(out)
text = text.replace(".AIRCRAFT_OPTIONS_HERE",
"\n".join(aircraft_section()))
text = text.replace(".WEATHER_OPTIONS_HERE",
"\n".join(weather_section()))
text = text.replace("\n\n", "\n") # troff dislikes blank lines
target = Path(sys.argv[1] if len(sys.argv) > 1
else Path(__file__).parent / "bandsaunter.1")