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]>
25 satellites, typed by hand and never revisited. Eight of them were the
first-generation Tevel constellation, which re-entered in 2024; four more had
come down too; and the nine Tevel-2 satellites that replaced them, the Chinese
space station, AO-27, AO-123 and twenty others were simply absent. So: 44
satellites now, and a generator instead of a memory.
cmd/satgen joins three public sources on the NORAD catalog number — Celestrak's
amateur group and PE0SAT's mirror for which birds OpsLog can actually get
elements for, and SatNOGS DB for the transmitters. It is a one-shot tool, run by
hand, the same arrangement as cmd/cntygen, and it is deliberately conservative:
- It never destroys a curated entry. The hand-written plans hold things
SatNOGS does not reliably carry — a CTCSS tone, the QO-100 passband as
operators describe it — so an existing bird keeps its data and only gains
its catalog number.
- It prunes on the re-entry date, which is a fact SatNOGS publishes rather
than a judgement about which of the missing satellites are missing for good.
- It refuses a digital uplink that does not say what it is. A GMSK uplink is a
command channel far more often than a digipeater, and shipping the wrong one
invites somebody to transmit on a control frequency. An analog uplink with
both ends is a contact by construction, which is what catches the repeaters
that describe themselves only as "Mode V/U FM".
- Its output is deterministic. One satellite can hold two catalog entries —
GreenCube is 53106 in one feed and 53109 in the other — and iterating a map
picked a different one each run.
A bird now carries its NORAD number, and that is how its elements are found.
Names were the only join before, and they are written differently by every party
involved: "TIANYAN 01" and "TO-108" are one satellite that had never once met,
so TO-108 tracked nothing at all.
And the plan now reaches a station that has already run OpsLog. The editable
copy was written on the first launch and was the operator's list for ever after,
so a release adding nine satellites reached nobody who had opened the tab. It is
merged on each load instead: a satellite they already have is untouched, edits
and corrections included, and only the ones they have never seen are added.
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]>
Measured rather than guessed: the whole repository was cross-compiled for
linux/amd64 and the gaps closed one by one. There were fewer than expected.
Flex and TCI were never Windows-specific — they carried //go:build windows by
inheritance and import nothing but net and gorilla/websocket. Untagged, no code
change. The two backends a Linux operator is most likely to own were already
portable.
Audio was 560 lines, not 2287: only devices.go and engine.go touch WASAPI, while
manager.go, recorder.go, wav.go and mp3.go were pure Go wearing the tag by
association. The whole platform surface is seven functions, now implemented a
second time on PulseAudio through github.com/jfreymuth/pulse — pure Go over the
server socket, so the no-cgo rule survives, and PipeWire answers the same
protocol. The fixed 16 kHz mono format and the server-side resampling mirror
what AUTOCONVERTPCM does on Windows, for the same reason.
OmniRig is the only real loss, and its backend still EXISTS off Windows rather
than being compiled out of app.go: a settings database is portable, so an
operator moving a profile across keeps "omnirig" saved and must be told to pick
a native backend instead of meeting a nil one.
The parts where Linux is not Windows, and where a compile-only stub would have
been a silent bug:
- data dir: still beside the binary, but ~/.local/share/OpsLog/data when that
folder belongs to the system — decided by trying the write, because /opt and
/usr/local are writable on some stations and not others.
- single instance: an flock, not a pid file. The kernel drops it however the
process dies, so a crash leaves nothing to delete by hand. This is the guard
that stops two instances fighting over the rig frequency.
- update: simpler here. Unix renames over a running binary, so the deferred
swap the Windows path needs a detached helper for is unreachable.
- tasklist/taskkill become /proc and SIGTERM; the boot log moves out of /tmp,
which is wiped exactly when the evidence is wanted.
- serial ports sorted naturally: /dev/ttyUSB10 was landing between USB1 and
USB2, the same trap COM10 fell into.
release.ps1 now cross-builds and vets for linux before it builds the exe, and
refuses the release if that fails — a port rots one unguarded x/sys/windows call
at a time.
Nothing has been executed on Linux yet: Wails needs webkit2gtk and cgo there, so
the binary must be built on Linux. scripts/linux-setup.sh checks the machine and
does it; BUILDING-LINUX.md is the manual version.
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]>
Six outbound services, five documented. The missing one is the answer to
the problem the digital-modes page had just finished describing: WSJT-X
sends to one address, so without a relay the choice is between OpsLog and
GridTracker rather than both — and OpsLog has been able to be that relay
all along.
Connections now lists it and explains the loop guard (pointing it at one
of OpsLog's own ports would feed the stream back into itself, and it
refuses) and that datagrams are passed on verbatim, with no origin header
of the kind the receiving side has had to write code to survive.
Digital Modes and GridTracker gains it as the unicast answer, which is
better than the one it gave: OpsLog relaying to GridTracker on 2238 needs
nothing from GridTracker and no multicast anywhere.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Documentation and releases move at different speeds. A page corrected on
a Sunday should not have to wait for the next version, and it should
certainly not be a reason to cut one.
The wiki step moves out of release.ps1 into publish-wiki.ps1, and the
release calls that same file — one implementation rather than two that
drift. Run it from VS Code (Tasks: Run Task -> Publish wiki) or directly;
it clones the wiki repo, copies the pages in and pushes only if something
changed, listing what it is about to commit so the wiki's own history
says which pages moved rather than a column of identical lines.
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]>
HAMLOG.online was added after most profiles were configured, so it had no
entry in the shipped defaults and no stored value either: it came back
blank, and blank is not a status anybody chose. Every service now starts
the same way — the sent side at R, the received side at N — and a blank
left by a service that did not exist when the operator last saved is
filled in from that. A status they chose themselves is untouched.
Two tests hold the line: no sent side may default to Y, and no field may
be left without a default. Y means "already sent", so it makes the
uploader skip the contact for ever — an operator with eQSL Sent at Y had
a logbook that never reached eQSL, and the only trace was one line in the
application log.
Wiki, both from operator reports:
QSL Management opens with Confirmations — what the page actually is (the
status stamped on every new QSO, not an action), what each status does,
and the warning about Y in the plainest words available, because it fails
silently and by design.
Digital Modes and GridTracker is new. Unicast and multicast explained
from the operating problem rather than the networking: one letterbox that
two programs watch, against a broadcast everyone can tune to. It carries
the real evidence — two starts of one station an hour apart, decodes in
the second and none in the first, the only difference being whether
GridTracker or OpsLog reached port 2237 first — then the settings for
WSJT-X, JTDX, MSHV, GridTracker and OpsLog, the 127.0.0.1-in-the-group-box
mistake, what to do if unicast is unavoidable, and how to check it from
the log.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Reported from a station whose two WSJT-X rows had been dead for weeks:
both were ticked multicast with 127.0.0.1 in the group box, and both
failed the join on every interface with
setsockopt: l'adresse demandée n'est pas valide dans son contexte
which names nothing the operator typed and does not say what is wrong
with it. A multicast group runs 224.0.0.0 to 239.255.255.255; 127.0.0.1
is loopback unicast, and it is an understandable thing to type — it is
the address every other field in every other program wants.
The address is now checked before the join. When it is not a multicast
one the row listens on unicast instead, which is what such an address
means, and the log says why it is not multicast. The row works, and the
reason is in a sentence rather than in a kernel error code.
0.27.18 went out before it was committed, so it does not belong in that
block — and it is the one entry an operator on 0.27.18 needs, since it is
their update that will not come back.
The relaunch after an update stopped working, and the regression is mine:
removing the PowerShell helper — which is what Defender was reading as a
dropper — also removed the wait it was doing. Nothing took over the job.
The numbers made it certain rather than unlucky. The instance being
replaced is allowed THIRTY seconds to shut down (armExitWatchdog forces
it out at that point) because it closes a remote logbook, a CAT session
and sometimes a backup. The new instance was patient with the
single-instance mutex for TWENTY. On any station where shutting down ran
past that, the new process gave up and exited in silence: no window after
an update, and the previous OpsLog still in the task manager. Exactly the
report.
Both relaunch paths now pass --wait-pid, and the new process waits on
that process's handle — a plain kernel wait, which ends the instant the
old one ends, however long or short that is, and looks nothing like a
script starting another program. The mutex retry stays as a backstop and
goes to forty-five seconds, so it is longer than the wait it exists for
rather than shorter.
And when the old process really has not gone, the message says that
instead of "OpsLog is already running" — after an update the operator did
not start a second copy, and what they need to know is which one to
close.
A test keeps the two spawn sites honest: a relaunch added without
--wait-pid is this bug again.
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.
"I changed an award and the column stayed empty" cannot be answered
without knowing whether the recompute ran at all, how long it took, and
whether it changed anything — and zero rows changed is a real answer
(the definition matches nothing in the log) that looks exactly like a
recompute which never happened.
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.
0.27.17 went out, so everything committed after its release commit needs
a block of its own — the padlock, the duplicated section name, the
digital row of the matrix and the MQTT chip.
The backdrop-blur fix goes with them rather than staying in 0.27.17. It
had been folded into that version's typing-lag entry, but it is not in
the binary 0.27.17 shipped: what that release actually carries is the
cluster-macro half, which is what its entry now says again.
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.
Reported from a station running MSHV, WSJT-X and JTDX together: click a
call in MSHV and the entry field filled, emptied, refilled — once a
second — with the map zooming in and out to match.
Two causes, both about reading one program's statement as another's.
The clear was tracked per LISTENER. Several decoders commonly share one
multicast group, so an idle WSJT-X reporting no DX Call — which is simply
true, and which it repeats every second — was read as MSHV abandoning the
station it was calling. "The operator cleared the DX Call" is a statement
about one program, never about a socket, so it is now tracked per
program, and a clear carries the id of whoever made it.
And nothing arbitrated between them. The program that announces a station
now holds the entry field, and the others cannot touch it until it lets
go: it clears its own call, it stops sending (closed), or the QSO is
logged. That is the operator's own suggestion, and it is the right one —
between overs there is no way to tell "I have nothing" from "I am not the
one you are working" except by remembering who was.
Refusing another program's callsign is logged once per focus, not once a
second: an operator whose second decoder "stopped filling the call" needs
something to 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.
It handles azimuth and elevation, and a great many stations already run
it in front of a controller OpsLog has never heard of. For those,
OpsLog talking to the controller itself would be a second program
fighting PstRotator over the same cable — so it hands over the bearing
instead, and lets PstRotator turn the mast.
Both kinds sit behind one small interface, chosen in Settings. Neither is
more correct than the other: the right one is whichever the station
already has working.
The 450° overlap is deliberately NOT applied on the PstRotator path.
PstRotator knows which machine is on the other end and does its own; two
programs each deciding to go the long way round is exactly how an antenna
unwinds in the middle of a pass.
Position queries are asked at most every three seconds rather than on
every tick. A PstRotator query binds a socket and waits up to a second
and a half, and many setups answer nothing at all — so one silence is
enough and it stops asking, reporting the commanded position instead and
saying that is what it is.
Settings ▸ Lists ▸ Satellites is gone. It was a text box an operator
typed their birds into by hand, and it had nothing to do with the
satellites the tracker knew: the same station kept two lists of the same
satellites and they drifted apart. The SAT_NAME box on the entry form now
offers the followed set — or every satellite with a frequency plan when
none is followed — merged with whatever that old list still holds, read
and never written, because what it holds is somebody's past work.
And the satellite section moved out of Hardware, which it never was.
Which birds you chase, where your antenna stands and how old your
elements are is operating; the rotator inside it is the only hardware
there, and one block does not make a section a device.
Two things belong in different places, and they were in one.
Settings → Satellites now holds the setup: which satellites to follow —
the same two-column shape as the awards, for the same reason, since a
feed carries two hundred birds and an operator works six — and the
orbital elements, their age, the fetch, and pasting your own. Following
none still means every satellite with both elements and a frequency plan,
so somebody who has not chosen yet is not handed an empty tab.
The panel keeps only what a pass needs. A countdown to AOS, or to LOS
once it is up, because that is the number that decides whether you sit
down; a bar for where in the pass you are, since mid-pass the useful
question is not the clock but whether you are past the peak; rise, peak
and set with compass directions, because "rises SW" is a direction to
look in and 213° is arithmetic. Distance, altitude and footprint. And
approaching or receding, which is the sign of the whole Doppler
correction and the only thing that explains why the frequencies are
moving the way they are.
The countdowns run in the browser from two timestamps. Predicting a pass
steps the orbit across a day thirty seconds at a time, which is not
something to do once a second for a clock the page can keep itself.
Two things reported together on the new tab.
The locator: the station's is on the ACTIVE PROFILE, not in a settings
key. keyStationMyGrid is a legacy key that EnsureDefault migrated into
the profile long ago and nothing writes any more, so reading it told an
operator with a perfectly good locator on screen that he had not set one
— and refused every pass prediction on the strength of it.
The map: Leaflet stacks its panes and controls up to z-index 1000, which
without a stacking context of their own float over the whole application
— Preferences opened behind the map, its Save and Close buttons under it.
The other three maps in OpsLog each carry `isolate` for exactly this
reason; this one was missing it.
EasyComm is what satellite rotator controllers agreed on, so a box that
works with SatPC32, Gpredict or Hamlib works here. Serial or TCP, and its
own settings rather than the HF rotator's: an az/el pair is a different
machine on a different port, and an operator who has both must not have
to choose.
A great many EasyComm controllers — the Arduino trackers above all —
accept commands and never say a word back. That is legal and common, so a
silent controller is not treated as a broken one: it is still driven, and
the last commanded position is reported in its place, marked as commanded
rather than read. A stuck rotator must not be able to hide behind an
order it never carried out, which is why the panel shows the antenna's
position beside the satellite's.
The 450° overlap is the reason a satellite rotator is worth having, so it
is used: a pass crossing north continues past 360 instead of unwinding
three quarters of a turn with the antenna sweeping the ground. Below the
configured elevation the mast is left alone — the numbers are right all
the way round the orbit, but a rotator that chases a satellite through
the far side of the earth spends the night turning, and a mast has a
finite number of turns in it.
The hard part of satellite tuning is not the arithmetic, it is deciding
who owns the dial. A tracker that forces both frequencies fights the
operator every time they turn the knob to follow a station across a
linear transponder; one that never touches the receiver leaves them
chasing a signal that slides nine kilohertz across a 70 cm pass.
So the operator owns the receiver and the tracker follows them. Every
second it asks the radio where the receiver actually is. Where it put it,
nothing has changed. Further than a dial-turn's tolerance, and the
operator has chosen a station: what they landed on is converted back into
a nominal frequency, and the transmitter is derived from that. Which is
the division of labour on a linear bird — the operator listens, the radio
does the sums.
Three ways to reach the radio, because a satellite pair is a shape of
operating rather than a manufacturer's feature. An IC-9700 or IC-9100 is
asked for its OWN satellite mode: it pairs main and sub, gives full
duplex, and keeps the dials linked the way its designers meant, which is
always better than an imitation built out of split. A Flex gets two
slices, A the downlink and B the uplink, created when missing, because
"slice B does not exist" is not something to make an operator fix at the
start of a ten-minute pass. Everything else gets the downlink, and is
told so — half the job announced beats half the job hidden.
What goes in the log is the NOMINAL pair. Two stations working each other
through a transponder read different numbers off their dials at the same
instant; the only figure they can both agree on is the transponder's own.
FREQ is the uplink and FREQ_RX the downlink — the one place a satellite
QSO differs from every other kind, and the reason FREQ alone cannot
describe one.
Three questions answered at once, because on a pass there is no time to
go looking for any of them: where the bird is, when the next one comes,
and what to tune. The map draws each satellite's footprint — the honest
answer to "can I hear it", since everything inside the circle has the
satellite above its horizon — and the selected one's path over the
ground. The pass list is every favourite in time order, the one in
progress in green.
The readout shows the corrected frequency large and the nominal one
beneath it. Only one of them, and an operator cannot tell a Doppler
correction from a mistuned transponder.
The map opens on the station rather than the Atlantic, and remembers
where it was left like the others. The panel is mounted only while its
tab is visible: it asks for the tuning once a second, and there is no
reason to compute an orbit nobody is looking at.
What internal/sat could not know: where the antenna is, which birds the
operator cares about, and where the files live.
Startup reads the cached elements and the frequency plan from disk and
nothing else — one file and a few hundred parses, so the tab is full the
moment it is opened, on a shack PC with no internet as much as on one
with. Fetching is the slow, optional half and never blocks a launch; it
happens on its own only when the set is stale and the operator asked for
it.
Elements pasted in by hand go in their own file. The feed cache is
replaced wholesale on every refresh, so a freshly launched satellite —
whose elements circulate on a mailing list days before any feed carries
it, which is exactly the week everybody wants to hear it — would
otherwise be wiped by the first automatic update.
The list joins both halves and shows what is missing on either side. A
bird with elements and no plan is one the operator can still track; a
bird with a plan and no elements is the visible symptom of an element set
that is too old. Dropping either turns a fixable configuration problem
into a satellite that "does not exist".
GetSatelliteTuning is the working answer, and everything that will later
drive a radio is built on top of it rather than beside it, so the display
and the rig can never disagree. It keeps the operator's frequency
nominal and applies Doppler only on the way out: on a linear pass the
station being answered stays put on the dial while both radios chase the
shift. A geostationary bird is corrected by nothing at all.
Two things the tracker cannot work without, both kept apart from the
orbital maths on purpose.
The elements come from Celestrak's amateur group, with PE0SAT as the
fallback for the hour when Celestrak is rate-limiting a hundred trackers
at once. A malformed satellite is skipped rather than fatal — a feed of
two hundred birds with one bad checksum must still give the operator the
other hundred and ninety-nine — and the count is returned so the app can
say so. The cache is plain TLE text in the data directory, written
beside and renamed, and only replaced once a feed has produced usable
elements: a captive portal must not take away the set the station
already had. Loading it first is what makes the satellite tab full on a
shack PC with no internet.
The frequency plan is separate because it changes for different reasons:
elements every few days from a feed, a transponder when the satellite is
commanded into another mode. The shipped list is a starting point, copied
to the data directory on first use and read from there afterwards, so an
operator can correct a frequency without waiting for a release and keep
the correction across updates — and a file they have broken is reported,
not overwritten.
UplinkFor is the part that matters on the air. On an inverting linear
transponder, tuning up the downlink means going down the uplink; get it
backwards and you transmit at the far end of the passband from the
station you can hear, which is the classic first evening on a linear
bird.
Names are matched on letters and digits alone. Celestrak says
"RADFXSAT (FOX-1B)" where every operator says AO-91, and nobody spells
Es'hail the same way twice.
The foundation of the satellite branch, and nothing above it yet: where a
satellite is (SGP4 from akhenakh/sgp4, Apache-2.0 and pure Go, so the
no-cgo rule holds), where it will be (passes with an elevation floor,
because a three-degree scrape is a line in a table that will never be a
QSO), and what its motion does to a frequency.
The Doppler pair is the part worth being careful about: the two
corrections go in OPPOSITE directions. The downlink arrives shifted and
we tune to meet it; the uplink must LEAVE shifted the other way to land
on the transponder's nominal input. A test pins the signs and the size —
7 km/s on 2 m is about 3.4 kHz.
Elements keep their raw lines beside the parsed form: that is what the
cache stores and what an operator pastes by hand for a bird no feed
carries yet, which is exactly when everyone wants to hear it.
Ticking "RTTY on USB" changed nothing: the flag is deliberately absent
from catLinkSig — none of these preferences is worth dropping the CAT
link, and with it WSJT-X's rigctl session, to apply — so saving the
settings left the link alone and the running client kept its old answer
until the next launch.
Preferences that the link does not depend on are now pushed to the
connected rig when the settings are saved, through the Yaesu escape on
the manager. SetRTTYUpper joins the controller interface for that: it is
a preference rather than a command, but it has to be reachable on a rig
that is already talking.