Everything asked for and not delivered last time.
The sky plot moves to its own column on the LEFT of the map. It was
stacked in the readout column, where a plot that wants to be square
competed for width with the numbers and pushed the pass table off the
bottom of the screen — while the margin on the other side of the map sat
empty throughout. Its own width, dragged from its right edge, with
double-click to reset.
Every block of the readout column gets a heading and a chevron: the
pass, the position, what to tune, the pass list. Each remembers its own
state, because an operator working FM birds never looks at the linear
passband and one watching a schedule never looks at the range rate.
There were no headings at all before, which cost twice — nothing said
what a group of numbers was, and there was nowhere to put the control
that shuts it. A shut block keeps the one number it exists for in its
heading, and drops that badge again when open rather than repeating the
body a line below.
The tune panel now leads with the CENTRE of the passband. The
Doppler-corrected figure was the wrong number for a reference panel: it
moves every second, it is different for every operator, and it is not
what the frequency plan, the AMSAT tables or anybody on the air calls
the satellite's frequency. What the radio is actually on belongs beside
Tracking, where the radio is — it is already there — so this keeps the
correction only as the offset that explains the difference.
Badges rather than rows of grey words, in the app's own status tones, on
the things that decide something: the band of each frequency, the
Doppler offset, an inverting transponder, the passband width, and the
peak elevation of a pass.
And the ground track. It was `color: 'var(--info)'` on a map that
renders with preferCanvas, and a CSS variable is not a colour a canvas
strokeStyle can take — so the browser kept whatever was set last and the
track came out a pale near-white that vanished over the imagery and the
deserts alike. This is the same trap internal to GridSquareMap's own
comment: every other map in this app passes hex. Now a real colour over
a dark casing, the way a road is drawn, because one line cannot hold up
over both a pale sea and a dark continent but a line with an outline
can.
Filled, with a hairline white edge — the same trick the home marker
uses. A ring is an outline drawn over whatever is beneath it, and eight
pixels of one over the satellite imagery was barely visible, which is
the whole reason the layer exists to be looked at.
Its own colour, in its own key, beside the switch that turns the layer
on. Separate from the decode colour deliberately: that one exists
because the band palette disappears over some basemaps, and cyan over a
pale sea has exactly the same problem for exactly the same reason. One
control for both would have forced the two layers into a single colour,
which is the distinction it took a shape to draw in the first place.
The picker only appears while the layer is on, since it configures that
layer and nothing else, and it is a portable UI pref like the other map
colours, so it travels with data/.
Two things the first version got wrong.
The arcs. One line per reporting station made a fan out of a single
square, and with a few dozen receivers it buried the decode arcs it was
drawn beside. They earned nothing either: an arc's job on the decode
layer is to say WHICH station a path belongs to, and here every path
starts in the same place. So marks only, and the mark is a hollow
diamond faded by age over the window. Shape rather than colour, because
the decode dots are filled circles and the arcs already use fourteen
colours — a fifteenth reads as another band. A divIcon, canvas being
able to draw circles and nothing else; affordable here at a few dozen
nodes where it would not be against three hundred arcs.
The controls. The basemap row was at left-12, which is 48 px, against a
zoom control that is 30 px of buttons plus its 10 px margin and a
border — so it overlapped, and the minus button took clicks meant for
Street. Then the colour input and the reverse-layer switch were added to
the same row, which grew until it reached the middle of a world map:
covering the Atlantic to spare a top-right corner that was empty
throughout, Leaflet putting nothing there but an attribution that lives
at the bottom. Basemaps stay left at left-16, the other two move right.
The map drew one direction of every path on it: what this receiver
decoded. The reverse — which stations are reporting our own
transmissions — is the half an operator cannot see from their own
radio, and on FT8 it is the half that decides whether calling is worth
the cycle.
It is the narrowest slice of the PSK Reporter feed there is. The v2
topic is
pskr/filter/v2/<band>/<mode>/<tx call>/<rx call>/…
so putting the operator's callsign in the TRANSMIT level makes the
broker send nothing else. For scale, from internal/pskr's own measured
numbers: four bands unfiltered is 83 messages a second, filtered on the
receiver's square 0.2 to 1.2 a second — one callsign in the transmit
level is a handful per FT8 cycle however open the band is. Both grids
are in the payload, so the arc is arithmetic and there is no lookup.
internal/pskrme, with its own connection, for the same reason
internal/pskrtgt has its own: the three want slices of the feed that
cannot be filtered out of one another. It also means this keeps working
with the band-opening watch off — hanging it off that feed's lifecycle
would have made it fail silently for anyone not chasing openings.
Nothing is persisted. One entry per STATION inside a fifteen-minute
window, carrying its freshest report: PSK Reporter's uploaders batch,
many every five minutes, so a tighter window would show a fraction of
who actually heard the last few calls. Stop clears the window, or
switching the layer back on would redraw who heard us before it was on.
On the map the layer is dashed and single-coloured. Solid is what we
decoded, dashed is somebody decoding us; colour alone could not carry
that distinction next to fourteen band colours. The receivers are rings
rather than filled dots for the same reason. Per profile, since the
callsign IS the subscription — a switch resubscribes rather than going
on reporting who hears the previous station.
The direction test that kept the wind out was applied to staying in as
well, and there it was wrong. Its unit is a four-degree step, which is
four seconds of travel at a degree a second — far longer than the 1600 ms
settle window — so between two accepted steps nothing said the mast was
still turning, the timer expired, and Stop went dark and lit again the
whole way round.
Entry stays strict: two consecutive steps of four degrees the same way,
which is what weather cannot produce. Staying in is now sensitive
instead — any continued progress in the direction already established
re-arms the timer, down to a single degree, because one degree the same
way is not a gust when the rotor is demonstrably under power. That
demonstration is exactly what the strict entry provides, so the loose
exit costs nothing: wind still cannot start it, and once stopped the
sensitive path is disarmed with the direction.
The settle window goes to 3000 ms so it outlasts one degree of progress
on a slow mast rather than four degrees on a fast one. Measuring
progress needs the PREVIOUS reading, which movementReferenceRef is not:
it deliberately holds still through sub-threshold steps so they can
accumulate into one.
"Later" set updateInfo to null and nothing else, while the check behind
it runs on a five-minute interval — so the notice came back four times
an hour, all evening, for a version the operator had already declined.
The button was doing exactly what it said and was useless anyway.
It now offers 1, 4, 12 or 24 hours, and the deferral is recorded with
the VERSION it applies to. That scoping is the part that matters: a
release newer than the one put off is different news and appears at
once, so a snooze can defer an update but never bury one.
The cross hides it for an hour — the shortest of the four — rather than
for the five minutes until the next poll, which is what made the notice
feel broken. The background check honours the deferral; checkUpdateNow,
behind About, deliberately does not, because asking is asking.
Kept out of the portable UI prefs: a machine told to wait four hours has
said nothing about the operator's other machines.
The band palette is calibrated against a plain basemap. Over the Esri
imagery the 60m navy and the 70cm olive all but disappear, and 20m
yellow goes over the deserts — and the palette only earns its keep for
somebody watching several bands at once, which most operators are not.
So a colour input beside the basemap buttons, where it belongs: the two
are one decision, since the palette that reads well on the plain map is
the one that vanishes over the ground. Empty means per band, which stays
the default, and the reset arrow appears only once something is set. It
opens on the crimson the home marker already uses, that being chosen to
hold up on any basemap.
With a colour forced the legend keeps the band NAMES and drops the
swatches: which bands are up is still worth knowing, a colour key that
maps to nothing is not.
Stored as a portable UI pref, like the grid map's own fills, so it
travels with data/.
The tab read the followed list, the horizon and the locator once, when it
was mounted. It is normally open BEHIND the settings window while those
are edited, so un-following QO-100 left it in the pass table and in the
dropdown until the tab was reopened — indistinguishable, from the
outside, from a setting that had not saved at all.
SaveSatSettings now emits sat:settings, and the panel reloads the birds
and the passes on it, the same way it already does on sat:tle. One event
for every key: they all change what the tab should be showing. The
selection needs nothing new — a satellite that has stopped being
followed drops out of the dropdown, and the effect that keeps the
selection inside that list moves to the first one remaining.
The FTx button is gone. It was the wrong grain in both directions: the
Digital class already sat a contest RTTY square next to an FT8 one, and
FTx then sat FT8 next to FT4, when what this map answers is where ONE
mode has been heard. The four classes stay as buttons and a dropdown
beside them offers a single mode.
Its contents come from the log, not from a list in the code. That is
what settles FT2: it is not a registered ADIF mode yet, so a hardcoded
list meant either leaving out the operators already using it or shipping
a mode that does not officially exist. A query does neither, and needs
no change here the day it is registered. The mode offered is the SUBMODE
where there is one, because ADIF files PSK63 under PSK and "PSK" is not
the name anybody is looking for.
The band list is the station's own, unioned with anything worked outside
it so nothing in the log is unreachable, ordered by frequency through
the band plan's index — sorting the names puts 10m between 1.25m and
12m. The satellite list is drawn from SAT_NAME on the squares
themselves and the control is absent altogether on a terrestrial log: a
VHF square worked through AO-91 and one worked line-of-sight are not the
same achievement, and until now nothing separated them.
The mode dropdown shares the scope with the class buttons rather than
narrowing on top of them — mode is one question, and two controls both
answering it is how a map ends up showing PHONE ∩ FT8 and nothing else.
A stored FTX preference is read as Digital, so it cannot leave the map
filtered by something no control shows as selected.
Settings ▸ FlexRadio stores the band→antenna map keyed by the band name
in capitals — that is what the panel writes and what the entry form reads
back. applySatRadio looked its two bands up through bandForHz, which
returns the band plan's own spelling ("70cm"), so both lookups missed,
both antennas came back empty, and the early return left the pass on
whichever antenna the radio was last used on. An operator with XVTA on
2 m and XVTB on 70 cm had configured exactly the thing being ignored, and
nothing said so: the downlink ran through the wrong transverter in
silence.
The key is now computed by flexBandAntKey, which exists so the next
caller cannot make the same mistake, and a test pins the case in the
direction that broke. Both outcomes are logged — the resolved antennas,
or the bands nothing was configured for — because from the outside a
setting never made and a lookup that missed look identical.
Alongside, three things the same pass made obvious:
- Tracking shows the satellite's own azimuth, elevation, distance and
altitude beside the frequencies. The strip held where the ANTENNA was
pointing but not where the bird was, which is what says whether a pass
is worth calling on. Elevation dims below the horizon so a satellite
followed before its lever cannot be read as workable.
- Both satellite lists in the settings are sorted by name with the
numbers taken as numbers. The available column followed the order
birds.json happens to be written in and the followed column the order
of the clicks, so finding one bird among sixteen meant reading all
sixteen.
- A followed satellite that has since been renamed is resolved through
the plan's aliases. LILACSAT-2 became LO-90, and the followed list is
stored as plain text, so the bird the operator had chosen appeared as
having no elements while the same satellite sat in the available
column under its new name. Resolved in GetSatSettings rather than
satSettings, which is read at startup before the plan is loaded.
"I thought we had fixed this" — and we had not, we had only raised the number.
The antenna moves a few degrees in the wind and the Stop button lights up and
goes out, all evening, on a mast that has not turned.
A threshold cannot tell the two apart at any setting. A gust pushes a beam past
four degrees and brings it back — 100°, 105°, 100°, 106° — and every one of those
excursions cleared the threshold. Raising it only raises the wind speed it takes,
and blunts the detection of a rotor genuinely creeping.
What separates a rotation from the weather is not amplitude, it is SIGN. A rotor
under power advances; a gust reverses. So a step is only movement when the
previous step went the same way: wind gives +5, −5, +5 and never two in a row,
while a rotor gives +4, +4, +4 and is announced on the second — one poll, about
a second, on a mast that takes half a minute to cross a pass.
The threshold stays, doing the job it is actually good at: ignoring the degree
or two of potentiometer noise that is not a step at all.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
A G-2800 sitting at 020° turned anticlockwise reports 020, 010, 000, 359, 358 …
340. The dial fed those straight into a CSS rotate, so the transition travelled
from 20deg to 359deg the long way: a complete revolution on screen while the
mast moved forty degrees in the opposite direction. On a rotator with an overlap
that happens on every pass through north, which is where the antenna spends much
of its time.
unwrapRotation was already there and already did the right thing — it was only
wired to the HOVER beam, where the mouse crossing north had made it obvious.
The antenna's own beam used the raw azimuth.
So the antenna angle is accumulated across renders too: 020 → 000 → −001 →
−020, which is the way the mast is moving. Both lobes of a bidirectional antenna
get their own accumulator, since they cross north at different moments, and the
ref only advances when the input changes so a re-render for any other reason
cannot make the beam creep.
The classic dial is unaffected: it draws its needle from SVG coordinates, with
no transition to send anywhere.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Calibrate was in the protocol notes at the top of the package ('V') and nowhere
else. It drives every element to its end stop so the controller re-learns where
zero is, and it is the cure for the one SteppIR fault an operator cannot reason
about: an antenna that tunes to the wrong length. The controller counts steps
from a remembered position, so once that memory is wrong — a power cut in
mid-move, elements pushed by hand, a motor that slipped — every frequency after
it is wrong by the same amount, silently.
It asks before it runs. Minutes, every element travelling its full length, and
the antenna unusable until it finishes: right answer to a mistuned beam, wrong
answer to a stray click during a contest.
It sits beside Retract as a two-column row rather than a second full-width
button. The docked widget is narrow and already tall, and the pair reads as what
it is — the two commands that move every element at once.
Retract (Home) was already there and worked on a SteppIR; it now carries a
tooltip saying what it is for, because "retract elements" does not tell an
operator that this is the storage position and that the next tune brings them
back out unaided.
An Ultrabeam controller has no calibration command, and the adapter says so
instead of pretending — the antenna interface is shared, and a button that
silently does nothing is worse than one that explains itself.
Untested on hardware: the byte is from SteppIR's own Transceiver Interface note,
which is the same source the rest of this package was cross-checked against.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
An operator spent three hours setting up, accepted the update, and reopened a
program that had forgotten everything. It is the second such report.
The updater is not the culprit — it touches its own exe and nothing else. The
pointer is. config.json is the only record of WHERE the database is, and it was
written with os.WriteFile: truncate, then fill. A process that stops between
those two steps — a crash, a power cut, an update's watchdog force-exiting the
old instance — leaves the file empty. readBootstrap then swallowed the parse
error, returned an empty pointer, and startup read that as "no database chosen":
it created a NEW one at the default path and opened it. Three hours of work
still on disk, and an application presenting itself as freshly installed.
Three changes, in the order they defend:
- The write is atomic. A temporary file, fsync'd, renamed into place — and a
rename within a volume cannot publish half a file. The previous contents are
kept as config.json.bak, because a pointer is a few dozen bytes and an
evening of configuration is not.
- A pointer that EXISTS and cannot be read is no longer treated as no pointer.
It is restored from the backup, and when there is nothing to restore from
the broken file is KEPT as config.json.broken — it is evidence, and it may
still be readable by hand.
- Creating a new, empty settings database in a folder that already holds a
full one is now said out loud, in the startup log and on screen. Nothing is
deleted and nothing is guessed — guessing which file is theirs is how the
wrong one gets opened — but the message names the other file, which is where
their settings still are.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Correcting the previous commit, which offered a 450° rotator range for the
Rotator Genius. It cannot do it.
The evidence is the operator's own box and 4O3A's manual together. His Rotator
Configuration reads "Limits: 5 to 4" — that is where the mechanical stop sits
within ONE turn, a dead zone at four and a half degrees, not a range of travel.
The manual is unambiguous about what happens past it: "you will not be able to
give it a target beyond the limits". A 450° mast on a Rotator Genius is a 450°
mast used as a 360° one, and that limit belongs to the controller.
So the setting goes. Offering an operator a 450° option that the box can only
ever refuse is worse than not offering one — it spends their evening proving
that the software was wrong about their station.
What stays is the half that was genuinely ours: GoTo no longer clamps to 360
before sending, and the limits the Genius reports on every heading query are now
read instead of skipped over. The overlap branch is driven entirely by what the
device answers — no setting, no assumption — so a controller that one day
reports a range past 360 is driven through it without a line changing here.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
An operator with a 450° mast watched the Genius stop at 359 and had to press
"clockwise" by hand to get through north. Half of that was ours: GoTo clamped
every target to 360 before sending it, although the wire command carries three
digits and always could have said 370.
So the clamp goes to 450, the rotator-range setting is offered for the Rotator
Genius like the other backends it applies to, and when a bearing can be reached
two ways the nearer one is taken — 010° sent as 370° when the antenna is already
at 350°, which is the entire point of having an overlap.
THE GENIUS DECIDES WHAT IS REACHABLE. It reports the limits it is configured
with, and they are the truth about what is bolted to the tower. This particular
station's box says "5 to 4" — the factory 360° range — and would refuse 370,
turning a working command into a rejected one. So the overlap is used only when
the Genius itself says it has one, and when OpsLog is set to 450 while the Genius
is not, the log says so once a minute and names the dialog to change: the setting
lives in the Genius's own Rotator Configuration, and nothing here can reach past
its limits.
The |h reply always carried those limits and they were being skipped over.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
THE FLICKER was a real bug and not a rotator problem. The rotator is asked where
it is at most every three seconds — a controller query binds a socket and waits —
but satTrackStep built a fresh status every second and carried over only Radio
and Error. So the antenna readout was filled in on one tick in three and blank
on the other two, which on screen is a rotator that keeps disconnecting. The
status is now rebuilt FROM the previous one, so a field nobody wrote this tick
keeps the value somebody wrote last tick.
THE HEADER now carries the two frequencies and the antenna bearing, beside the
button that started tracking. During a pass an operator watches the radio and
the antenna, not a column on the far side of the window — and that column is the
first thing they hide to get the map full width, which until now took the
numbers with it. The compass spins while the antenna is still on its way: a mast
takes tens of seconds to cross a pass, and "moving" against "stuck" is the whole
reason to look at it, which a number alone cannot show.
THE PRECISION drops from one hertz to a hundred. The Doppler moves about sixty
hertz a second on 70 cm, so the last two digits changed on every tick and the
display was a blur that could not be read and did not need to be. The radio
still gets the whole figure — the correction is computed and sent to the hertz —
this is only how much of it is worth putting in front of somebody. The shift
beside it says "+9.7 kHz" rather than "+9741 Hz", which is how it is read aloud.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Two birds are often up at once, and switching between them left the frequencies
on the first. The selection on the page is the DISPLAY's; the tracker held its
own name and went on following whatever it was started with. The way through was
to stop tracking and start it again, which is how it was found — and which is
also how a Flex throws away and rebuilds both its slices for no reason.
RetargetSatelliteTracking changes what is being followed without letting go of
the radio or the rotator. Everything derived from the old satellite is cleared so
the next step sets it afresh: the frequencies, the mode on both slices (set once
per satellite, not per tick), the antennas and the CTCSS tone — the new bird may
be U/V where the old one was V/U, which swaps which slice sits on which band.
And it happens at once rather than at the next tick. The loop gained a wake
channel: a second of the previous satellite's frequencies is a second of the
wrong pass, and the antenna would otherwise wait for the new bird to drift a
step away from where the old one happened to be.
Selecting a satellite with the radio idle is unchanged — a look, not a command.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Asked why the satellite dropdown exists when the passes are on the right, and
the honest answer was that the table was incomplete. Three kinds of satellite
never appeared in it: QO-100, which has no pass because it never sets; one whose
elements have not arrived, which cannot be predicted at all; and one whose next
pass falls beyond the prediction window. Following a satellite and not finding it
in the list reads as OpsLog having lost it.
They are listed at the end now, each saying which of the three it is, and
clicking one selects it exactly like a pass row. The table stops being "what is
coming" and becomes "what you follow, soonest first" — which is what an operator
was scanning it for anyway.
The dropdown stays, and now for one reason rather than by default: it is the
only selector left when the readout column is hidden, which is what an operator
does when they want the map full width during a pass.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
A satellite tracker that insists on an elevation motor is a tracker switched off
for nearly everybody. A pass at the edge of the footprint — which is most of
them — never climbs above ten or fifteen degrees for its whole length, and a
yagi's beamwidth swallows that: the bearing alone is enough, and it is how most
stations that work satellites are actually built. The same switch rescues an
az/el station whose elevation motor has failed.
So it is an option, not a silent fallback, because it does cost something: a
bird straight overhead is a moving azimuth and a bearing that means nothing, and
whether to accept that is the operator's call. With it on, any rotor in the list
can be chosen — the PstRotator, the Rotator Genius, the ARCO, the tower already
turned for HF.
That works because the per-backend command dispatch moved out of the three
RotatorGoTo/Stop/Heading methods into linkGoTo/linkStop/linkHeading, so the
satellite tracker drives any of the seven backends through the same code the
compass uses instead of a second implementation of each. GetRotatorHeading loses
sixty lines of near-duplicate switch in the process, and a rotor with no
elevation axis now says so (HasElevation) rather than reporting a zero that
looks like a real bearing.
One trap, with a test on it: the step check compared both axes, so with the
elevation never commanded its difference stayed above the step for the whole
pass and every tick ordered the antenna to the bearing it was already on. A mast
has a finite number of turns in it.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Both were already on screen, in a grey eleven-pixel footnote under the
frequencies, which is not where either of them belongs.
FM and SSB are two different evenings. One is a channel, a tone and a handheld;
the other is a passband, a beam and a VFO walked as the Doppler moves. So the
mode is a coloured badge — in the readout, in the transponder list where a
satellite offering both makes the choice matter, and in the header so it
survives hiding the readout column, which is exactly what an operator does when
they want the map full width during a pass.
The tone gets the weight of a frequency because that is what it is worth: a
repeater called without it does not answer, and what the operator hears is an
empty channel and concludes the satellite is not up. And when a bird needs no
tone it now says so — a blank line could not tell "no tone" from "OpsLog does
not know", and the difference is a pass.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
A footprint is thousands of kilometres across. A dozen of them overlap into a
wash of translucent circles that hides the coastline, the ground track and the
satellites themselves — and the question a footprint answers, "can I hear it",
is only ever asked about the bird being worked.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
An unselected satellite was a four-pixel dot with a one-pixel white edge, and
the basemap decides whether that is visible at all: a pale marker vanishes into
pale terrain, a grey one into a dark ocean, and the operator can switch between
the two in one session. So: a dark halo under a white ring around a larger dot.
Two rings, because no single colour reads on both.
The ones above the horizon now carry their name. Not all of them — a dozen
labels is a map nobody can read — but the two or three an operator is choosing
between right now, which is what saves hovering each grey dot in turn to find
them.
Hovering said "name · elevation · altitude", none of which decides anything.
What decides whether to reach for the radio is how long is left, how high it
will get and where to point, so the tooltip now carries the pass: elevation and
azimuth with its compass point, distance with an arrow for closing or receding,
rise and set with a countdown and a direction, and the peak. A bird already in
view shows its SET countdown instead of its rise — that is the number that
matters at that moment. No pass in the window says so, because a blank reads as
a fault. It costs no extra prediction: the pass list on screen is indexed by
name, and the first entry for a name is its next pass.
The tooltip also had to stop closing itself. The layer is rebuilt every five
seconds as the birds move, and a rebuilt marker is a new marker, so the detail
being read disappeared mid-sentence. The map now tracks the pointer and reopens
the tooltip of the dot it is still on — that one only, so nothing hangs open
once the mouse has moved away.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The satellite page configured its own EasyComm or PstRotator link while five
other backends were configured in the rotator list. An operator with one az/el
mast therefore described it twice, and could describe it differently the second
time — a station that works on HF and not on a pass, for no reason visible
anywhere on screen.
Now every interface lives in Settings ▸ Rotator, once, and the satellite page
stores only a KEY into that list plus the tracking policy that is genuinely its
own (minimum elevation, step, park). The key and not the index: deleting the
first rotor must not silently point the tracker at a different mast.
migrateSatRotator() turns an existing satellite link into a real entry in the
list, selects it, and clears the old keys so it cannot run twice.
Which rotors have an elevation axis is now a question with one answer, in Go:
rotatorTypes plus rotorHasElevation, exposed to the panel by GetRotatorTypes.
The dropdown, the labels, each backend's default port and default baud all come
from there, so TypeScript no longer keeps a second copy of the same knowledge to
drift out of step. Three cases do not follow from the type alone and are treated
as such: PstRotator forwards elevation to a mast that may not have any, so the
operator says; a SPID's dialect decides (Rot1Prog has no elevation in its reply
format); and an ARCO and an ERC-M speak the same GS-232 while only one of them
lifts.
Each interface carries an Az / Az+El badge beside it. The satellite rotor
dropdown LISTS the azimuth-only ones, disabled, rather than hiding them: an
operator who owns one rotator and does not see it concludes OpsLog cannot find
it, where a greyed row saying "azimuth only" teaches the actual thing.
ERC-M by DF9GR is new — the az/el interface for a Yaesu G-5500. It emulates
GS-232, so internal/rotator/gs232 grew the elevation half: W for a two-axis
move, C2 to read both, falling back to C+B for the firmware that answers C2 with
the azimuth alone. That fallback is the point of the parser tests: reading such
a reply as "elevation zero" would put the antenna on the horizon, which is the
one wrong answer that looks plausible.
EasyComm II is promoted to an ordinary rotator interface, so it can also turn
the antenna from the compass and from a spot click.
The ERC-M is UNTESTED on hardware. Its Test button reads BOTH axes rather than
just the azimuth, so a controller wired for azimuth alone says so there instead
of during a pass.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The dial painted its background as a full-bleed square, so inside the
rotor widget — which is already a card — it read as a hole punched in the
panel rather than an instrument sitting on it. It is a circle now, at the
radius the map already used, and the corners are left to whatever it is
drawn on. The wrapper loses its own border and background for the same
reason: one card, not two.
And the continents were barely there. At #202832 on a #0B1015 ground the
land was some eight per cent brighter than the sea — technically a map,
practically a dark square with a suggestion in it. The new shades read as
coastlines while staying well under the beams, which are what the dial is
actually for.
The palette stays deliberately unthemed. That was never the problem: a
map that repaints itself in four colour schemes stops being readable, and
the beams' green, orange and yellow have to mean the same thing in every
one of them.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The rigs and antennas are already declared once, in Settings ▸ Operating
conditions — a station per rig with its antennas hanging off it — and
then typed again into every contact. That is work, and it is a source of
spellings that do not match: "IC-7610", "IC 7610" and "ic7610" are three
different rigs to an award, to a filter, and to anyone reading the log
later.
Both fields now offer that list, in the entry form and in the QSO editor,
and the antenna field offers the antennas of the rig that was picked
because that is the structure the tree already has. It falls back to all
of them for a rig the tree does not know, so an operator typing a
borrowed rig is still offered their own antennas rather than nothing.
Free text stays allowed throughout — a contact made from somebody else's
station, or imported from another logger, carries a rig that was never in
this tree and must still be loggable. Same rule the satellite-name field
follows.
And the decodes list gains a ceiling of two thousand rows. The rolling
half hour was never a limit on a crowded evening — three decoders put
several thousand rows inside it — and the panel slows down long before
the age cut removes any of them, because each row is a layout, a status
lookup and a distance. Past the ceiling the oldest go: what has already
been scrolled past, rather than the period being read.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
A Flex 6700 owner's log: no connection attempt of any kind, and six
saves each answered with
cat: settings saved, link unchanged - staying connected
His settings were right — FlexRadio selected, 192.168.1.154, port 4992,
and the detector had found and named his radio back at him. What was off
was the master CAT switch, which sits above the radio dropdown.
Three things, because each of them failed him on its own.
The log line was false on both counts: a disabled CAT has the signature
"off", so every save took the unchanged branch and announced a link that
was staying connected when nothing was connected at all. It now says CAT
is switched off, which is the fact he needed and the only place it could
have reached him.
The panel says it too, where he was actually looking: a notice under the
switch while it is off, because everything below it can be perfectly
right and still connect to nothing.
And choosing a radio now ticks the switch. Picking a brand, typing an
address, running a detector and clicking the radio it found are all one
gesture — "connect to this" — and making the operator find a separate
master switch afterwards is a trap. Nothing here ever turns CAT off on
its own; they can still untick it.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Reported on an IC-7610 shared through OmniRig: clicking a CW spot put
the radio in CW-R every time, and the only way out was to edit the
OmniRig rig file by hand.
OmniRig's model has two CW modes, PM_CW_U and PM_CW_L, and nothing in it
says which one an .ini calls plain CW. Icom rig files disagree with each
other — on some PM_CW_U is CI-V mode 0x03 (CW), on others 0x07 (CW-R) —
so asking for "CW" is a question with two right answers and OpsLog was
only ever giving one of them.
Settings ▸ CAT ▸ OmniRig now carries the answer for THIS rig, and it is
pushed to a radio that is already connected rather than waiting for a
reconnect: which bit a mode maps to is not worth dropping the link, and
with it WSJT-X's rigctl session. Same shape as the Yaesu RTTY sideband,
for the same reason.
Nothing changes for anyone whose rig file already agrees with us.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Once the callsign stopped giving up its width, the RST boxes were the
next widest things in the row and became the ones that shifted when the
date field appeared. Both are now a notch narrower and shrink-0, like the
callsign; "59+20" is the longest report either ever holds and still fits.
Its row gains a date field for a manual entry, and a flex row makes room
by shrinking its children — so the widest box, which is the one the eye
is on while typing, was the one that visibly narrowed.
A notch narrower and shrink-0: the size it will always be, with or
without the date, and the slack now comes from the boxes beside it.
Preferences is one component holding two hundred pieces of state, and its
biggest panels are eight hundred lines of form — external services, CAT.
A plain controlled input sends every keystroke into that state, so every
character re-rendered the whole dialog, and the letter arrived after the
finger had left the key.
BufferedInput keeps the text where it is typed and hands it up 120 ms
later. A drop-in for Input, so the change is one import rather than a
hundred call sites — which means it has to be right in every shape those
call sites take:
• Blur flushes at once, so clicking Save cannot lose the last word, and
so does unmounting: a panel changed mid-word still hands up what was
there.
• A value that comes back DIFFERENT from what was sent up is adopted,
even with the focus in the field. That is what keeps the fields that
normalise as you type working — the callsign box that upper-cases,
the port box that drops everything but digits. They echo a corrected
value, and the correction wins.
• A value changed from outside while the field is idle wins too: that
is how loading the settings, or switching profile, refills the form.
• Checkboxes, colour pickers and file fields pass straight through.
Three things reported together.
Preferences said the section name twice: a small line above every panel
repeated the heading right underneath it — "GENERAL" over "General" —
while the sidebar next to them already shows which section is open,
highlighted. Two lines and a highlight for one fact; the small line goes.
The band matrix opens on the digital row the operator chooses. Its
digital row has always rotated — DIGI, then each digital mode in their
own list — but it always STARTED on DIGI, so somebody who only works FT8
clicked through to their own mode on every callsign. Settings ▸ General
now says where the rotation starts, and the dropdown offers exactly the
modes the matrix rotates through (the phone-mode rule is now shared
rather than copied, so the two cannot drift). DIGI stays the default: it
is the right answer for anyone working several digital modes.
And the MQTT chip is gone from the status bar. That is the name of a
message protocol, not of anything an operator has — a chip in the status
bar has to say what it is about, and this one told nobody anything. What
it carried is shown in the Chase New panel, which is the place that uses
it; its poller went with it.
Logging a contact from a piece of paper — a contest sheet, a friend's
report, a QSO worked on another radio — means the frequency, the band,
the mode, the date and both times all have to stop following the rig and
the clock at once. That is a single decision, and it was five clicks in
five different places, each of which had to be found first.
The padlock beside Start UTC now holds all five and releases all five,
with the same behaviour as before: locking pre-fills the times so the
fields are not empty, unlocking hands them back to the clock and snaps a
QSO already in progress to now.
The five per-field locks stay underneath. Everything downstream reads
them, and individually they say the right thing — "this value is
decoupled from the rig". Only the control is one.
A backdrop-filter covers the whole window and is recomputed every time
anything above it repaints. Behind these dialogs is an application that
never stops moving — CAT polls four times a second, spots arrive, meters
sweep, maps redraw — so the filter was being recomputed continuously, and
each keystroke's repaint dragged a full-window blur with it. That is why
the lag was felt in Preferences and nowhere else, and why fixing the
cluster macros did not end it.
Worse in one place: the cluster server editor opens FROM Preferences, so
its overlay was the SECOND full-window filter stacked over the first.
That is exactly where the delay was first reported.
The dialogs an operator types in for minutes — Preferences, the cluster
editor, the QSO editor, bulk edit, alert rules, award definitions — now
dim the background harder instead of blurring it. Everything else keeps
the blur: a confirmation you click through in a second costs nothing.
This tab began as the relay and rotator dashboard and stopped there: the
three things an operator touches most — the radio, the CW keyer, the
voice keyer — were the ones missing from the page that claims to show the
station.
The radio card carries the frequency and the mode large, because that is
what gets glanced at, and the split pair only when there IS a split: a
second frequency shown at all times is one more number to read past. When
CAT is down it says which kind of down — switched off, or on and not
answering.
The CW keyer card carries the speed, which is the control an operator
reaches for mid-QSO when a station comes back faster than expected, and
Stop beside it because a message going to the wrong callsign has to end
now. The voice keyer card carries the recorded messages themselves: a
card that only said "idle" would be a light, not a control.
Each polls its own binding and holds its own state, like the supply card
above them, so they drop into the grid and reorder with everything else.
The two keyers appear only when there is something behind them — a port
configured, a message actually recorded — because an operator who works
neither should not be handed two dead cards. That question is asked once
on opening the tab: a keyer is bought and wired, not something that
appears mid-session.
Typing a cluster macro cost two things per character. The state lived on
SettingsModal, so every keystroke in one of those twenty-four boxes
re-rendered the WHOLE preferences dialog — every list, every form, every
panel. And the save wrote through to Go and into the database on each
one: a round trip per character.
The editor is now its own module-scoped component with its own state, so
a keystroke re-renders twelve rows. And writeUiPrefDebounced holds the
database write until the typing stops, while the local cache — which is
what everything reads back — is still written at once. Pending values are
flushed when the page goes away, so typing and immediately closing does
not lose the last word.
The behaviour is unchanged: still saved as you type, still no Save
button, because a text box whose contents only take effect on some other
button is how work gets lost.
Also folds the satellite changelog into one [NEW] entry. Satellites are
new in this version — nobody reading the notes has seen any of it — so a
running account of how it was built, tab then tracking then rotator then
where the settings moved to, is the wrong shape. One entry saying what it
does.
Setting up a telnet cluster is the step operators get stuck on. The
address and the port are two pieces of information nobody has to hand,
and a typo in either looks exactly like a node that is down — there is
nothing to read, only a connection that never comes.
So the editor offers a list, and choosing one fills the fields. Six to
begin with: F4BPO, DXFun, SOTA, POTA, and the two Reverse Beacon feeds —
listed separately and named for what they carry, because they are one
network on two ports where 7000 is CW and RTTY and 7001 is FT8 and FT4,
and no amount of trying will tell you which is which.
The preset then gets out of the way. Everything stays editable, the name
is only filled when the operator has not chosen one of their own, and a
node typed in by hand behaves exactly the same. Reopening a node created
from a preset shows it selected, so the list also answers "which one is
this".
Click the heading. Within each PERIOD and never across them: the slots
are what this panel is — what was on the air in one fifteen-second
window — and a list sorted end to end would mix three minutes of decodes
into one column of numbers with no way to tell which window any of them
came from.
One click sorts the way that column is worth reading — strongest signal,
lowest frequency, furthest DX, A to Z, most wanted — the second reverses
it, and the third gives arrival order back. Arrival order stays the
default and stays one click away, because it mirrors the decoder's own
window line for line, which is what makes the two screens comparable at a
glance.
Status ranks by the cluster's own order, so the two views rank the same
things the same way, with the markers that are orthogonal to the entity —
a new county on a worked country — sorted above the plain duplicates.
A station that never sent a grid cannot be placed, and an unresolved
country is not a name: both sort to the end whichever way the column
goes, rather than pretending to a distance of zero and heading the list
under "nearest first".
The map answers "where is the satellite over the earth". This answers
"where do I look", which during a pass is the question that matters.
The projection is the one every tracker uses and every operator already
reads: the centre is the zenith, the rim is the horizon, north is up. So
the radius is (90 − elevation), not the elevation — a bird overhead is a
dot in the middle, and a pass that hugs the rim is one that never rises.
Whether it comes over the roof or along the treeline is something no
amount of azimuth and elevation digits conveys, and one glance settles.
The whole pass is drawn: a dashed track with arrowheads for the direction
of travel, a hollow circle where it rises, a filled one where it sets,
and a cross where the satellite is now — green above the horizon, grey
below, because the numbers are still right down there and nothing can be
worked through the earth.
The track is fetched once a minute, not once a second: the SHAPE of a
pass does not change while it happens. Only the marker moves, and that
rides on the tuning poll that was already running.
Headings, because a column of numbers with nothing above it makes an
operator work out what they are looking at every time. A real table, so
the satellite column takes the width the longest name needs — "ZHUHAI-1
OVS-1A" was cut to eight characters in a fixed one.
Colour where it carries meaning, and nowhere else. The maximum elevation
is the quality of the pass, so it is coloured like one: a bird 70°
overhead and one scraping 12° along the horizon are not the same evening,
and the table should say so without the operator reading every number. A
pass in progress is green, one starting within five minutes is amber. And
a dot for the mode: FM and SSB call for a completely different set-up,
and which the next pass is decides whether you reach for a handheld or
for the whole station.
The rotator's baud rate is a dropdown, like every other one in OpsLog.
Three things reported together from the tab.
The shipped frequency plan went from eleven satellites to twenty-five:
the eight Tevel FM cubesats, EO-88, AO-109, CAS-4A and 4B, TO-108,
GreenCube's single-frequency digipeater, and QO-100's wideband
transponder beside its narrowband one. It remains a starting point in a
file the operator can correct — a transponder gets switched and no
release should be needed to follow it — and the picker still lists every
bird in the element set when the "with a plan" filter is unticked.
The readout column drags to any width between 240 and 720 pixels,
double-clicks back to its default, and folds away entirely. How much map
against how much detail is the operator's call: watching a footprint
cross an ocean and working a pass want opposite things.
And the locator is no longer asked for twice. Passes are predicted from
the station locator, which is set once in Station information; the field
here was only ever for an antenna at another site, so it says so and sits
folded away. Nobody should have to wonder which of two locators is in
use.
Also: "Driven by" is two columns wide. "OpsLog (EasyComm II)" did not fit
in a third of the row, and a truncated choice is a choice that cannot be
read.
The world map and the grid-square map shared a single key, so picking
satellite imagery to look at grids repainted the main map too, and there
was no way to have terrain on one and streets on the other. They are
different maps answering different questions, and the imagery that suits
one is not the imagery that suits the next.
Four keys now, one per map, in lib/mapBase beside the remembered views —
named in one place so a rename cannot silently orphan somebody's choice —
and portable, so a copied data folder brings them along. The grid map
inherits whatever was set under the old shared key rather than being
reset to the default: an operator who chose imagery there keeps it.