0881c72c0f8534c2b75219afed61c5fa7fa8c192
451
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fba7e79a1c |
feat(decodes): answer a station on click, DT and Freq, badge filters
Clicking a decode now ANSWERS it. It sends WSJT-X/MSHV a Reply message (type 4), which is the same thing as double-clicking the line in their own Band Activity window: the application looks the decode up, sets its transmit frequency to the caller's and starts the exchange. It deliberately does not tune the radio, which is what it did before and why nothing happened. On FT8 the whole band sits inside one passband, so moving the dial changes nothing about who gets answered - the decision belongs to the decoding application, and the Reply is the only way to hand it over. Tuning would also just fight it for the VFO. The entry is still filled so the QSO can be logged here. The reply is routed by PROGRAM ID, not by listener: two receivers can share one multicast group, and answering a station heard on the 6 m instance by talking to the 20 m one would start a call on the wrong band. It goes to the address that instance's packets actually arrive from - a multicast listener must answer the sender, never the group. WSJT-X matches the reply against its own decode list, so the payload replays the decode field for field: time, snr, delta time, audio offset, mode and message text. Two columns added, DT and Freq - the audio offset inside the passband, not the RF frequency, which is the same for every station in the list and says nothing. Past about two seconds DT takes a warning tint: that station is drifting out of the window. The transmit strip. "You cannot see what you are sending, or who you are calling" - two separate faults. The message was only ever threaded into its period, and in FT8 you transmit in the slots you are NOT receiving in, so its period had no decodes and the whole line was dropped; a transmit slot now creates its period. And the state is a strip of its own at the top, because it is the one thing on the screen that is about the operator rather than the band. It is fed by every Status rather than only by one carrying transmit text, so it can still name the station being called on MSHV and older JTDX builds, which stop before tx_message in the Status payload. "New only" became per-category badges, in the colours and the vocabulary of the Chase New panel. None lit shows the whole band - this is a decode log first, and a panel that opened by hiding most of the traffic would be lying about what is on the air. |
||
|
|
4f77d51ffe |
refactor(decodes): real columns, spelled-out flags, bigger type
First pass on the panel from operating feedback. Columns are a grid template shared by the header row and every data row, so the two cannot drift and the eye has a rail to follow. It is capped at 1500 px and centred: free-flowing, a 2500 px window put the country a foot from the callsign it belonged to and left a hole in the middle of every line. "New" gets a COLUMN. It was only a coloured edge before, which says something is special without saying what — and every one of these is a reason to break off what you are doing and call. The entity verdict is a solid badge, the orthogonal ones (park, grid, prefix, county) are outlined in the colours markerColour already gives the cluster list and the band map, so a new park is the same green in all three. Applied inline because those are categorical --chart-* custom properties, which the theme does not expose as Tailwind colour utilities: written as border-chart-7 the badge would simply have had no colour. Band and mode selectors now appear only when the feed actually carries more than one of each. One MSHV is one band and one mode, so for most operators they were furniture; they show up the day a second instance puts a second band on the link, which is the only day they mean anything. Same rule for continent, and a receiver count when more than one instance is feeding. Added a LoTW-only filter, and raised the type throughout (call and message to 14 px, secondary to 12 px, badges to 11 px) with more room per row. The decode payload now carries the sending application's own id. It tells two receivers apart on one multicast group — and it is the address a WSJT-X Reply message would have to go back to, so it is carried now rather than requiring another trip through the parser later. |
||
|
|
a197d124dc |
feat(decodes): an FT decodes tab fed by the inbound UDP link
Every FTx decode WSJT-X, JTDX or MSHV puts on the wire, grouped by T/R
period. Optional and closable, from Tools -> FT decodes; its open state is
remembered, because an operator running digital modes leaves it open for
the session rather than consulting and closing it.
The period is the point, and what separates this from the cluster list.
FT8 is a sequence of fifteen-second slots and a band is read by watching
them go by: who called CQ this slot, who answered, what I was sending while
they did. A flat list sorted by time loses exactly that, so the list is
grouped one section per period, newest first, with the operator's own
transmission shown inside the slot it went out in.
Three fields had to be carried up from the wire to make it possible:
- the decode's OWN timestamp, which the parser read and threw away. It is
what assigns a slot: a period's decodes arrive in one burst a second or
two after it closes, so arrival time piles a whole period into the next
one. Rebuilt to UTC from milliseconds-since-midnight, with the
day-boundary case handled - a decode stamped 23:59:58 arriving at
00:00:01 would otherwise be dated a day ahead and sit at the top of the
list for the rest of the session.
- the decoded line itself. The exchange is what says where a station is in
a QSO, and no set of extracted fields reads like "R-09" does.
- tx_message and transmitting from Status, which nothing parsed before.
Recorded once per message rather than on every Status, which repeats it
about once a second for the whole over.
Also picked up on the way: is_new, low_confidence, off_air, the operator's
own call and grid, and the T/R period itself - better authority on slot
length than the mode name, which says nothing about a custom period. The
Status tail is read defensively: those fields were appended over successive
schema versions and JTDX and MSHV each stop at their own point, so a short
packet is normal and keeps whatever parsed.
Status flags come from ClusterSpotStatuses, the resolver the cluster list
and band map already use, filling the same cache. One verdict per call:
"new band" in this panel and plain worked in the cluster two seconds later
would be worse than no flag at all. Clicking a call goes through the same
handler as a cluster spot, so answering a station is one gesture whether it
came off telnet or off the receiver.
Filters: CQ only, new-anything only, band, mode, continent, an SNR floor
and a free search. The band, mode and continent choices are built from what
is actually on the feed - offering 160 m to a station whose receivers are
all on 6 m is noise.
Decodes are held in the frontend and pruned to a rolling half hour: they
are a live view, not data, nothing outside the panel reads them, and a
night of FT8 on 20 m would otherwise grow a list no filter can rescue.
Arrivals are staged on a 300 ms timer so a period landing as fifty packets
costs one status lookup and one render.
|
||
|
|
9599c3e0b9 | chore: release v0.25.9 | ||
|
|
a81125eab1 |
fix(icom): a set whose acknowledgement is lost is sent once more
Extends to frequency and mode what PTT already had. A missing FB is not a missing command: the rig acts on the frame as it decodes it, and what expires is our wait for the answer, on a bus shared with the rig's own transceive updates. JTDX in "Split: Fake It" moves the dial and the mode immediately before every key-down, so those acks queue behind each other. Losing one is fatal to the client rather than merely untidy: rigctld answers RPRT -9, JTDX reads that as losing rig control and tears the connection down mid-over. An operator's log shows three set_freq failures and one set_mode, each followed within 300 ms by a fresh rigctld client -- and shows the PTT resend rescuing an over that would otherwise have ended there. Opt-in per caller rather than folded into exec: only a command that says "be in this state" can be repeated safely, and a relative one must never come through here. The acknowledgement loss itself is still unexplained. Every failure in that log is preceded by a state read reporting SSB on a rig in DATA, which points at CI-V frame desync rather than a slow rig, and needs a trace to pin down. |
||
|
|
bbe1b3ce80 |
fix(tci): a refused un-key no longer leaves the rig keyed for good
The trx handler stamped its PTT cache BEFORE commanding the radio and left it in place when the command failed. An operator running JTDX over TCI with an Icom on CI-V lost an un-key to a lost acknowledgement: the cache recorded "off" regardless, and from then on every trx:0,false was dismissed as a repeat of a state the radio had never reached. The cache is per-server, not per-connection, so reconnecting JTDX changed nothing either -- the transmitter stayed keyed into the amplifier, with no drive, until the radio was switched off by hand. The cache is now written only on success, and a failure clears "known" outright so the next command reaches the radio whatever it is. Second guard: releasePTT drops a PTT this server asserted when the client disconnects, and when the server stops -- before the CAT backend goes down, while the rig is still reachable. rigctld has had that since a K3 sat in transmit for 29 s; the TCI server was written without it, so an operator moving from Hamlib to TCI silently lost the protection. A later log shows the rig keyed for 40 s across a JTDX reconnect for exactly that reason. |
||
|
|
7be6f64596 | chore: release v0.25.8 | ||
|
|
0bab7f05b9 |
feat(relays): accept a self-signed certificate on a generic HTTP board
HTTPS to a relay board could not work. Nearly every board that offers it signs its own certificate — there is no authority anywhere that could have signed it — so the request failed verification before it left. A checkbox, per board, off by default. Not a blanket switch, because the other HTTPS case is real and opposite: a board reached from outside through a proxy with a genuine certificate, where verification is the only thing standing between an antenna switch and the internet. Same setting, two boards, different answers. Off by default is only safe if the failure explains itself, so a certificate error now names the box to tick. Go's own "x509: certificate signed by unknown authority" is accurate and tells an operator nothing about what to do next. Shown only once an https:// URL is actually in the board's configuration. A board on plain HTTP has no certificate to argue about, and an option that cannot matter yet is one more thing to wonder about. The flag joins the driver cache key: ticking it has to rebuild the driver, or the cached one would go on refusing the certificate with the verifying client it already holds. The boards that take a bare host — WebSwitch, KMTronic — keep verification. An https:// typed there is the proxy case by construction, since they default to plain HTTP on the LAN. Three tests against a real self-signed TLS server: accepted with the box, refused with a message naming it without the box, and one board's setting not leaking into another's. |
||
|
|
21a0d560de |
feat(awards): a reference's number can be corrected in the editor
The one field the editor would not let you touch, and the one that was wrong on WAJA. Every other property of a reference — its name, pattern, entity list, validity window — was editable; the code was rendered readOnly, so correcting a number meant deleting all 47 references and importing a new list, throwing away anything the operator had adjusted in it. A rename in the store, not a delete plus an insert: everything the reference carries travels with it, which is the whole point of correcting a number rather than replacing an entry. A number already in use is refused — REPLACE INTO would have let one reference silently swallow another, discovered much later as a prefecture quietly missing from the list. The typed code is held apart from the selection. The list and every field patch key off the selected code, so editing it in place made the editor lose the reference mid-edit. SaveAwardReference now recomputes the log like Delete and Replace already did. A reference's name is what the award column SHOWS for awards displaying by name, and its pattern is part of what matches at all — so editing one changes rows, and the grid was left showing the old label until something else happened to trigger a pass. Changelog: the three TCI-sharing lines are merged into one. The server and the two fixes made to it while building are one unreleased feature, and an operator only ever meets the finished thing. The TCI-client PTT line stays separate — it is OpsLog driving a SunSDR, the other direction entirely. |
||
|
|
2941121f4b |
fix(awards): WAJA was numbered by the Japanese state, not by the JARL
The catalog carried Japan's civil prefecture code (JIS X 0401) — 01 Hokkaido, 02 Aomori, 03 Iwate, 04 Miyagi — where the award uses the JARL's own numbering. The two agree on the first three prefectures and then part company on 35 of the remaining 44: Tokyo is 10 to the JARL and 13 to the government, Niigata 08 against 15, Toyama 28 against 16. The names were right throughout, which is why nothing looked wrong: the award matches on the prefecture NAME in the QTH, so it counted exactly the right contacts. Only the number against each one was wrong — and that number is what an operator writes on a JARL claim. Renumbered by name from the official JARL list, so everything else each entry carried travels with it. That includes the Tokyo spelling pattern, which had to move from 13 to 10; left where it was it would have been matching QTHs for Saitama. Two digits throughout, as the JARL prints them. Not cosmetic: the codes are strings, so "1" sorts between "09" and "10" and the reference list appeared in an order no published list uses. Version 2 on the definition, so this actually reaches people. An operator who has not edited WAJA gets it at startup; one who has is offered it, since their work outranks ours. Nobody has to re-import by hand. Reported with the official JARL chart alongside the exported award. |
||
|
|
6c75ff345b |
fix(tci server): split armed on the frequency asked for, in either order
Audit prompted by "are we sure the commands are implemented — split, Fake It, Split rig?". The rigctl server is complete and hardened; the TCI one, three days old, had reintroduced a bug rigctld had already paid for. A client working split says two things — where to transmit, and that split is on — and nothing obliges it to say them in that order. A write to channel B while the rig was still simplex was DISCARDED, on the sound principle that preparing a transmit frequency is not a request to QSY. But then the split was armed on whatever the transmit VFO held, which is the receive frequency: the operator transmits straight onto the DX while their software shows exactly what they asked for. The frequency is now remembered and used when the split arrives, which is what rigctld does with set_split_vfo / set_split_freq. Two more from the same source: Asking for a split state the rig is already in touches nothing. A client in Fake It uses no split but still says so to be sure, and answering an error to a request that was already true is what made JTDX abandon a transmission a second into the frame through the rigctl server. A repeated PTT command is not re-sent. One client restated it sixteen times a second, and the Flex's own "xmit 1" was overwritten between two of them inside a millisecond. The same radio sits behind this server — the operator reporting this is on the Flex API backend. Fake It itself needs nothing but channel A, and now has a test saying so. |
||
|
|
d5e25244ee |
fix(tci server): announce transmit permission, and log what the client sends
MSHV's PTT test does nothing against the TCI sharing server. The initialisation block never carried TX_ENABLE. The document files it under unidirectional control rather than initialisation, so it was missed when the block was written from §4.1 — but its own note says it is "sent to the client when connected", and that is the point: a client that models transmit permission starts out assuming it may NOT transmit. Without it MSHV never even tries, so nothing arrives to relay and there is nothing to see at either end. Sent as true always. OpsLog is not what decides — the radio behind whichever backend is connected does, and its refusal already travels back through SetPTT into the log. TX_FREQUENCY goes with it, at connect and whenever the transmit frequency moves. It is the command a client showing "TX 14.200" reads; channel B alone left that stale. And every command a client sends is now logged. This is the only evidence there will ever be about a program on someone else's machine: "the PTT test does nothing" cannot be answered without knowing whether MSHV sent trx at all, and in what form. Cheap — TCI is event-driven, a client speaks when the operator does something — and capped at 200 lines per connection so one that does poll cannot quietly fill the log. If this was not the cause, the next report answers it in one line rather than another round of guessing. |
||
|
|
4095455e66 |
fix(tci): a refused PTT is reported instead of vanishing
"PTT via CAT does nothing on TCI." What OpsLog sends is right — trx:0,true; is the documented command and the same one the reference clients send — so the command was going out and the radio was discarding it. ExpertSDR announces transmit permission with TX_ENABLE: on connect, and again whenever the band changes, "in case transmitter permission was changed" (§4.3). While it is false the radio simply IGNORES trx. OpsLog never read that command, so there was nothing in the log, nothing on screen, and a dead key. Now the permission is tracked and SetPTT refuses out loud, naming where to look: the frequency must be inside a transmit band and TX enabled in ExpertSDR. The refusal travels the path that already exists — Manager.SetPTT to pttKey, which logs it and hands it to the UI. Silence is not a "no". A radio that never mentions TX_ENABLE — an older ExpertSDR, or another program speaking TCI — is not treated as refusing: we key and let it decide. Permission is also forgotten on connect, so a refusal remembered from a band since left cannot block PTT until a restart. This may not be the operator's own cause, and that is the other half of the change: if it is not, the log now settles it in one line. cat: TCI: → trx:0,true; present means the command left OpsLog and the radio ignored it for a reason it has not told us; absent means the fault is on this side. |
||
|
|
3f95763ca6 |
feat(lookup): a cache TTL of 0 switches the cache off
Wanted for the case where the answers are moving: an operator correcting their
own QRZ record, or chasing a DXpedition whose page changes during the operation,
otherwise waits out thirty days before OpsLog will ask again. Clearing the cache
by hand works once; this is the setting for a whole session.
Nothing is read from it and nothing is written to it — rows stored while it is
off would only sit there going stale, waiting for the day it comes back on.
Switching off is NOT clearing: what it already holds stays, and the Clear cache
button remains the way to throw that away.
Two distinctions the code now has to keep, both load-bearing:
An EXPLICIT stored zero is off; an ABSENT key is the thirty-day default. Every
operator who has never opened this setting has nothing stored, and reading that
blank as a zero would silently switch the cache off for all of them.
The CONSTRUCTOR's zero is still the default, not off. At startup the settings
have not been read yet, and beginning with no cache would hammer the provider
for the first seconds of every launch. Only SetTTL, called once the operator's
settings are known, can switch it off.
A negative lifetime is meaningless and is ignored rather than rounded into
either meaning.
The input had to change too: it derived its value from the stored number on
every keystroke, so the box could not be emptied — and 0 was unreachable
outright, since parseInt('0') || 30 is 30.
|
||
|
|
72ec3cbb97 |
feat(cat): share the rig over TCI as well as Hamlib — one or the other
internal/rigctld exists because Windows gives a COM port to ONE process: the moment OpsLog talks to the radio natively, nothing else can. It answers the programs that speak Hamlib NET rigctl. This answers the ones built around Expert Electronics' TCI instead — and it answers them whatever radio is connected, because it sits on the same backend-agnostic Rig interface. An operator with an Icom or a Yaesu can now hand a TCI-only program a working rig. One server or the other, never both. They answer the same questions about the same radio, nothing speaks both, and a second listener is only a second thing to go wrong. Written against the official TCI Protocol document (ExpertSDR3/TCI, 12 January 2024, MIT — downloaded and read, not recalled): the initialisation set of §4.1 in its documented order, and the argument order of every command from §4.2. A client will not proceed past connect without that block, which is why it is written out in full rather than stubbed. The one dangerous detail is the VFO mapping. TCI's channel A is where you LISTEN and channel B where you transmit — the opposite way round from OpsLog's RigState, which follows ADIF. Getting that backwards would put a station on the DX's own frequency, so it is pinned in both directions by a test, and RxFreq was added to the adapter rather than inferred. Writing channel B while the rig is simplex is ignored: the client asked to prepare a split transmit frequency, not to QSY, and a logger doing that on every spot click would drag the operator off the station they were listening to. A backend that cannot split still refuses out loud. Only changes are pushed. TCI clients redraw on each command, so re-sending an unchanged frequency four times a second makes a VFO readout flicker and fights the operator's own tuning. Nine tests, no socket needed — the protocol is the decision, not the transport. |
||
|
|
a8bca8c316 |
feat(awards): an entity that IS one island group fills its own IOTA reference
QRZ carries <iota> only for the operators who filled it in, and most have not. But for 99 entities the reference follows from the entity alone — a station in Ascension is on AF-003 whatever its callbook record says — and the entity is known for every callsign from cty.dat, with no callbook at all. So this reaches the operator with no QRZ subscription and the station that has never touched a callbook, and it lands before the contact is logged, which is when a reference is worth having. Filled ONLY when the callbook gave nothing: an operator who typed a reference knows something a table cannot — an IOTA-heavy entity, a rare island being activated — and that still wins, as does one picked by hand on the entry. The table is the IOTA programme's own dxcc_matches_one_iota.json, which lists exactly the entities that map to ONE reference. France is not among them: a French station is usually on the mainland and on no island, and guessing would put a reference on hundreds of contacts that earn none. Held as a table rather than a download — 3.5 kB that changes when an entity appears, so fetching it daily would buy nothing and would fail exactly where a portable station usually is. The header says where it came from and how to refresh it. Follows |
||
|
|
491546c7f4 |
fix(tci): one marker per callsign on the panorama, not one per spotter
Spot mirroring drew the same station two or three times, a few hertz apart. Not a loop in the spot pipeline — one cluster line produces exactly one SendSpot. It is that a popular DX station IS spotted two or three times, by different operators inside the same minute, and no two of them agree on the frequency to better than a few tens of hertz. This backend assumed ExpertSDR replaced a spot bearing a callsign it already held, and said so in a comment; it keys on the frequency too, so each of those became its own marker and stayed. The FlexRadio backend has always done this properly (spot remove before spot add, "one live spot per call"). TCI now does the same: the frequency drawn for each call is remembered, a re-spot within 500 Hz sends nothing at all, and one further away deletes the old marker before drawing the new one — so a station that really moves still moves, instead of collecting markers. SPOT_DELETE was not in the protocol document this backend was written from. Confirmed before use against ars-ka0s/eesdr-tci, an independent TCI library that lists SPOT with 5 arguments, SPOT_DELETE with 1 and SPOT_CLEAR with 0 — the other two matching exactly what already works here, which is what makes the third trustworthy. The memory is cleared where Connect sends spot_clear. Left standing, it would suppress the next spot for every remembered call as "already drawn" onto a panorama the radio had just emptied — spots would quietly stop appearing after a reconnect until each station changed frequency. Five tests on the decision, which is split out from SendSpot so it can be checked without a radio. |
||
|
|
2aceb99948 |
fix(lookup): a cached callsign kept its missing IOTA for thirty days
Reported with the evidence side by side: a QRZ record plainly carrying <iota>EU-048</iota>, and no IOTA on the entry. Adding a field to the lookup cache leaves every row already in it without that field — and the cache lasts thirty days. So the change worked perfectly for a callsign never looked up before, and did nothing at all for one already cached, which is every callsign an operator actually works. A row written before the column existed has NULL there, and is now treated as stale: one refetch per such callsign, the next time it is used. NULL and "" had to be made to mean different things for that. Put no longer passes the reference through nullable(), so an operator with NO island stores an empty string — otherwise every ordinary callsign would look unwritten and refetch for ever, turning a cache into a tax on every lookup. Both halves are tested. Same trap for web and zip, added the same way in 0026 and never noticed. |
||
|
|
c48294b0ca |
refactor(sync): no mass backfill — identities are stamped on what is touched
Correcting an over-design of my own. I had contacts given an identity in bulk the first time synchronisation was switched on, and a full dedupe-key map of the logbook rebuilt on every pass, both sized for 123 000 contacts. Neither is needed, because the change log starts EMPTY. Contacts logged before synchronisation was switched on are never in anyone's log and so are never exchanged; an identity is stamped only on a contact that is actually logged, edited or deleted from then on. Seeding a second machine with the existing history is a one-time copy of the database or an ADIF import — not something synchronisation should be doing, and not something it can do from a file that starts empty. One case survives, and it is why the dedupe key stays. Two machines can already hold the SAME old contact, the second seeded by that copy or import, with different row ids and no identity on either. The day one of them edits it, it stamps an identity and sends a change naming it; the other has never seen that identity and would insert a duplicate. Matching on the contact itself — callsign, minute, band, mode, the importer's own key — recognises it. So that became a targeted lookup through idx_qso_callsign, run only when an identity is unknown, instead of a whole-table map rebuilt each pass. Rare work, priced as rare work. |
||
|
|
747c2b9105 |
feat(lookup): read QRZ's island reference and fill the IOTA award
QRZ's XML carries <iota>EU-048</iota> for an operator on an island, and OpsLog read past it. Wired end to end: the provider, the lookup cache (a column, like web and zip before it), and the entry's award references. It matters more here than the same field would for another award. There is no live "who is on an island right now" feed anywhere — POTA has one and that is what OpsLog matches spots against; SOTA has one behind conditions; IOTA publishes only static lists. So the callbook record is the practical source, and it is known BEFORE the contact is logged, which is when a reference is useful. The reference goes in as an IOTA award reference, exactly as one picked by hand, so the existing path carries it to the qso.iota column on save. Two limits, both deliberate. A reference the operator typed or picked WINS: a callbook entry can be years out of date, and the operator in front of the radio has just been told where the station is. And the value must look like an IOTA reference — two letters, a hyphen, three digits — because writing anything else into the award makes a reference no list contains, which counts for nothing and has to be found by hand later. |
||
|
|
724e68b38d |
feat(sync): a stable identity per contact, indexed
Two PCs exchanging changes through a folder must be able to name the SAME contact in both logs. "the QSO with M0ABC at 14:32" is a guess and the two machines can disagree about which row that is, so an edit or a deletion cannot be addressed at all without an identity that travels with the record. A real indexed column, not a key inside extras_json: the identity is resolved once per incoming change, and scanning JSON for it would turn every sync into a full read of a 120 000-QSO logbook. A column costs one migration; the JSON would cost a scan every time. That was the decision to confirm, and it is confirmed. It follows the award_refs pattern — read in selectCols, absent from columnList, written only through its own methods. So an ordinary edit cannot clobber it, which matters: another machine addresses the contact by that id, and losing it makes the same QSO arrive again as a new one. Pinned by a test that saves an edit with the field deliberately blanked. Existing contacts are stamped in batches inside one transaction rather than one commit each — on a remote MySQL the round trip dominates, and 120 000 commits is the difference between a minute and an afternoon. And a dedupe-key map lets two machines that already hold the same imported log recognise each other's contacts instead of copying 120 000 of them across. MySQL nearly lost its logbook to this. It cannot index a TEXT column without a prefix length: the migration would fail with error 1170 and fail again on every startup, with no way out from the interface. The translator only emits VARCHAR for names listed by hand in varcharColumns. sync_uid is now listed — and a test reads the migrations, works out which indexed columns are TEXT, and fails if the list does not cover them, so the next one cannot reach a shared logbook. |
||
|
|
7d664bd1de |
feat(sync): the change-log core for one operator on several PCs
The shape we agreed: each PC keeps its own local SQLite and they exchange CHANGES through a folder the operator already has — Seafile, OneDrive, a NAS, a USB stick. Not the database file: SQLite over SMB or NFS corrupts, and in WAL mode the shared-memory index has no meaning across machines at all. The rule that makes a shared folder safe is one writer per file, append only. Each machine writes only its own <machine>.ndjson and never touches another's, so a sync client that replicates whole files can never merge two writers into one — there are never two. That is the exact opposite of putting the database there, and it is why it works. This is the half internal/offlineq deliberately refuses to be: its own doc says "no mirror, no pull, no merge, no tombstones". All four are here. Four decisions worth naming, each with a test: Tombstones. A delete is a record. Without one, a QSO removed on the laptop comes back on the next sync from the shack PC, for ever — and a contact logged again after a deletion has to come back, which the ordering also has to allow. Determinism. Last writer wins, ordered on (time, the writer's own counter, the writer's id) — not on time alone. Two PCs' clocks are never equal, so a bare timestamp is not even a total order: two machines merging the same pair in different orders could reach different answers and disagree for ever. Skew still decides WHICH edit wins and nothing can fix that; what this guarantees is that every machine agrees on the winner. Resumption. Readers resume at a byte offset, so a 40 000-contact file is read once and thereafter only its tail — and the offset advances only past COMPLETE lines, because a folder sync catches a file mid-upload sooner or later. A file that SHRANK was replaced rather than appended to, and is re-read from the start. Survivability. One unreadable line costs one record. A record stamped with a FUTURE format version is skipped, never guessed at. Core only: no UI and nothing wired to the logbook yet, so nothing is user-visible and there is no changelog entry. Next is the sync_uid column, the add/update/delete hooks and the settings panel. |
||
|
|
14ac73028e |
fix(worked-before): a prefixed call is one operator, not a whole country
Reported from a screenshot: working ZA/OE8NDR, the worked-before list showed ZA/IZ2DPX and ZA/IW2JOP beside him and the header counted all four as contacts "with this call". callMatch folded portable forms together by taking the part before the slash. That is right when the slash carries a suffix — RK3DWA/3, RK3DWA/P are one person — and exactly wrong when it carries a prefix: ZA/OE8NDR became the station "ZA", and the predicate `callsign LIKE 'ZA/%'` then selected every other visitor to Albania. The operator's own call is now picked out properly: drop the known qualifiers (P, M, MM, AM, QRP, a bare call-area digit) and of what remains take the longest part — a country prefix is short by nature, ZA, F, KH6, VP2E, and a callsign is not. The predicate matches the prefixed forms too, so ZA/OE8NDR and a plain OE8NDR still find each other, which is the whole point of the fold. A base under three characters falls back to an exact match: a malformed entry must not produce a LIKE that selects half the logbook. |
||
|
|
f405e71d7d |
fix(spid): a Rot1Prog takes three digits, so every target went the wrong way
Field report from a tower: on a RAK/RAU in Rot1Prog, every commanded heading made the antenna want to turn nearly a full circle ANTICLOCKWISE — 0°, 90°, any of them — while the heading readout, the stop button and everything else worked. BuildSet framed the four-digit Rot2Prog azimuth for both dialects. A Rot1Prog reads three: its replies are three digits in a five-byte frame, and its command field matches. So 90° went out as "0450" and was read as 045 — 45 − 360 = −315°. Every target landed 360° low, which is why it was always anticlockwise and always nearly a full turn. The operator's own guess, that OpsLog was in 720° mode, was the right instinct in the wrong place: the fault is a decimal shift, not a range. The round-trip test added here is the one that would have caught it without a tower — every degree of the circle through the command builder and back through the reply parser, which must return the degree that went in. The frame tests pinned the Rot2Prog form against the reference and said nothing about the other dialect. Two more from the same report. A rotator test that only READS the heading — SPID, ARCO, DCU-1 — said "Packet sent, the antenna should swing to north, check PstRotator's UDP listener", naming a program not in the path for a move never commanded; it now says the controller answered and nothing was moved. And the compass polled every three seconds, so a turning antenna moved the needle in steps of about thirteen degrees; the heading now has its own 700 ms tick while the relay boards and the antenna controller stay at three seconds. |
||
|
|
8683a450a7 |
feat(psu): switch a Modbus RTU bench supply from OpsLog
The manufacturer's document arrived, so this is no longer guesswork: 9600 8N1, function codes 03 and 06 only, and a register map with the output on/off at 0x0001, the measurements at 0x0010…0x0013 and the set points at 0x0030/0x0031. ONE REGISTER IS WRITTEN — 0x0001, the output. The map also exposes the voltage and current set points and the three protection trip levels as writable, and none of them belong to a logbook: a wrong value there is 30 V where a radio expected 13.8, or a trip level lifted on a supply feeding an amplifier. They are read and displayed instead, next to the measured values, which is also how an operator sees at a glance that the supply is on and the radio is drawing nothing. The wire layer is tested where it can be. CRC-16/MODBUS is pinned against its published check value — the CRC of "123456789" is 0x4B37 — which fixes the polynomial, the initial value, the reflection and the absence of a final xor all at once; the rest of the protocol is checked frame by frame against the manual, including the byte order of the CRC, an exception reply told apart from a broken line, and a reply from another slave on the bus refused. The write echo must match the value sent: it is the only confirmation the output really switched, and accepting the frame without it is how a radio ends up dark behind a green light. Framing follows the manual's own rules: 3.5 character times of silence between frames (4 ms at 9600), and a frame is over when the line falls quiet — Modbus RTU has no terminator, and a serial read that times out returns (0, nil) here, so the reader is built on a deadline and a quiet-time rather than on an error that never comes. Untested against hardware — nobody here has the supply. |
||
|
|
9f8e3c73d9 |
fix(udp): OpsLog was re-tuning its own rig from its own broadcasts
Reported as JTDX "Fake It" causing CAT disconnections. Fake It is not the
cause — it is what made an existing loop fire, and a regression of mine from
this morning is what closed that loop.
The station has an inbound remote-call row and an outbound N1MM RadioInfo row
on the same port, 2241, so every RadioInfo datagram OpsLog sends arrives
straight back on the loopback. The remote-call parser strips XML tags and takes
the last token as the callsign: for a RadioInfo that is
<ActiveRadioNr>1</ActiveRadioNr>, i.e. "1" — and <Freq> became a tune request
for the frequency the rig was already on.
That was harmless only while <Freq> was misread. It is in tens of Hz, the parser
assumed MHz, every echoed tune failed "out of the 11-digit CAT range", and the
loop died on the error. Teaching it the unit (
|
||
|
|
5770c40d89 |
test(adif): pin that an import invents no QSL routing
Reported as OpsLog defaulting QSL_SENT_VIA and QSL_RCVD_VIA to "E" on import. It does not: recordToQSO reads the field and normalises it, so an absent, empty, blank or unrecognised value gives an empty one. An "E" seen after an import came from the file — Log4OM writes QSL_SENT_VIA:1>E on every record whether a card was ever sent, which TestQSLSentViaDoesNotBecomeManager already pins. Nothing to change, so this pins the half nothing covered: the ABSENCE case. The opposite is an easy reach — electronic confirmation is the common case — and defaulting to it would quietly rewrite the operator's own record of how their cards travelled, which is the same class of fault as issue #16. Compiles and vets clean; not executed here — this machine's Application Control policy began blocking freshly built test binaries partway through the session. |
||
|
|
b49599150c |
fix(steppir): a silent controller wedged the driver for good
io.ReadFull cannot be used on a serial port, and it was.
On Windows a serial read that times out returns (0, nil) — on this transport a
timeout is not an error. io.ReadFull loops while err == nil, so a controller
that goes quiet for one poll, or answers with a truncated frame, spins it
forever. It holds ioMu throughout, and that is the whole failure the operator
sees:
- the poll goroutine never returns, so nothing is ever logged about a fault
and the cached status keeps the antenna looking connected;
- every command blocks on the same mutex, and the trace line sat AFTER the
lock, so even the attempt left no trace.
One dropped reply on a 4800-baud link therefore stopped the antenna responding
until OpsLog was restarted, with a log that showed the antenna starting, a few
status frames, and then nothing — which is exactly how it was reported.
Reads are now bounded: three seconds for an 11-byte frame that takes 23 ms on
the wire. Giving up returns an error, and the poll loop already knows what to do
with one — say so and reconnect. The command trace moved ahead of the lock, so
what the operator asked for is in the log even when the answer is not.
The SPE driver reads through a bufio.Reader, whose ErrNoProgress guard already
covers this; the ADIF parser reads a file. This was the only exposed one.
|
||
|
|
331db58705 |
fix(antenna,udp): name the reason tracking is off; infer the remote-tune unit
The SteppIR report was not a SteppIR fault. In the log the antenna starts, answers every poll, and sits at 21050 kHz while the rig works 21074 — and the status line only prints when the frame CHANGES, so a link that is alive and parked looks identical to one that died. The one line that explained it said "ultrabeam: follow loop stopped", which covers two quite different situations and, at startup where nothing was running, reads as a fault. Tracking was simply switched off. It now says which of the two it is, names the antenna type, and states the consequence — that the antenna will not follow the rig. Same log, a second and unrelated fault: every launch failed a tune request from DXHunter with "frequency 2107400000000 out of the 11-digit CAT range". <FREQ> was read as MHz, but DXHunter had echoed back the value OpsLog itself published in the N1MM RadioInfo broadcast, whose <Freq> is in tens of Hz. Both units come from the same peer, so the unit is now inferred: try MHz, kHz, Hz, tens of Hz, and keep the first that lands on an amateur band — anything a station is asked to tune to is in one. Hz before tens of Hz because their only overlap, 40 m against 4 m, is far more likely to be 40 m. Nothing plausible tunes nothing and says so, rather than sending the rig to a wrong band. |
||
|
|
480d24384b |
fix(relays): {value} is the relay's label, not a per-relay value
It was built the other way round on a misreading of two screenshots: the pattern
held {value} and the per-relay boxes held numbers to drop into it. The ask was
simpler and better — {value} is the name typed in Relay labels, so a switch
addressed by antenna name is one pattern instead of eight URLs:
http://10.10.10.100/relay?on={value} relay 1 named Ant1 → ?on=Ant1
Renaming the antenna re-addresses it, and the name on the button and the name on
the wire cannot drift apart because they are the same string. It works in the
per-relay URLs and in the patterns alike, so the per-relay boxes go back to
holding URLs and nothing about them changes meaning any more.
The label is percent-encoded with %20 rather than "+" for a space: "+" is a
space only in a query string and a literal plus in a path, and this can land in
either half of a URL.
{value} on a relay with no label would send "?on=", an empty parameter that most
boards answer with a cheerful 200 and no movement. The driver refuses it and
names the label as what is missing; the editor warns while it is being typed,
beside the empty box rather than after an antenna fails to switch. The labels
also join the driver's cache key — they are part of the wire format now.
|
||
|
|
fe2affc72f |
fix(relays): the generic HTTP board never sent its URLs; cluster filters start off
buildDeviceDriver had no case for "httpgen", so the generic board fell through
to the WebSwitch driver: it polled an address it had never been given, reported
itself offline, greyed out every relay button, and sent none of the configured
URLs. Nothing in the interface said so — the board was configured, saved and
listed, and simply did nothing. deviceKey did not cover the URLs either, so once
that is fixed, correcting a typo in one would still have handed back the cached
driver holding the old address until a restart.
Two shapes of home-made switch could not be described at all:
- a bit-mask board whose four URLs differ by one character
(/Set0/1, /Set0/2, /Set0/4, /Set0/8) — {value} in the pattern now takes the
number from the per-relay box, keeping the address in one place;
- a board numbering its channels from zero — {relay-1}, since giving up the
pattern for eight hand-typed URLs was the only alternative.
The pattern decides what the per-relay boxes hold, and the grid says which as
soon as {value} is typed: guessing per box ("does this look like a URL?") would
change meaning on a typo, which is not a thing to do to something wired to an
antenna. A URL typed without a scheme gets http:// like the named boards get
from relayBase; https:// is passed through untouched.
Host and the connection test are gone for this type. It has no address of its
own — its relays may each live on a different box — and no status to read, so a
test could only ever answer "OK, 4 relays". Save was greyed out without a host,
which made a complete configuration of four full URLs impossible to store.
Cluster filters no longer persist across launches. A band lock set weeks earlier
is invisible to whoever set it: the counter reads 76 spots live, the grid is
empty, and the search goes to the cluster instead. Nobody loses work by
re-ticking a chip. Grouping and the panel state still persist — they change how
spots look, never whether they appear.
|
||
|
|
be0326c862 |
fix(cluster): stop saying "waiting for spots" when 76 have arrived
An operator sent two screenshots: 76 LIVE in the counter, and the panel beside it reading "Waiting for spots… Spots will appear as the cluster sends them." Both his band and mode were locked to the rig — 20 m, SSB — so the filters were doing exactly their job and the empty state was describing a different problem entirely. He went looking for a connection fault. It now says how many arrived, names every filter currently narrowing the list, and offers one button to clear them all. The band and mode locks are named first: they follow the rig rather than a click, so they are the two nobody remembers switching on. Also times the connection. He reports the first launch taking a while to produce spots and a restart connecting instantly, which is the signature of a slow name resolution rather than a slow node — the OS caches the answer, so the second run skips it. The log now carries the dial duration and the delay to the first spot, which separates that from a node that simply had nothing to say. Hypothesis, not conclusion: the next log settles it. |
||
|
|
3def789742 |
fix(winkeyer): send the probe the way a working client sends it
Logger32's WinKeyer debug against the K3NG that will not answer OpsLog is a
capture of the exchange working, on the same keyer and the same port:
Sent: 13 13 13 00 04 55
Rcvd: 55 (72 ms)
Sent: 00 02 Host open
Rcvd: 23 (WK2 v23)
That is the sequence I already send. The difference is the grouping: Logger32
puts the three nulls and the echo probe in ONE write, and we split them with a
50 ms pause and a buffer purge in between. On a keyer that reboots when the
port opens, that pause is a window for it to come up mid-sequence and swallow
half of it — and there was nothing to wait for, since a null produces no reply.
The handshake bytes are now logged unconditionally, not behind the diagnostic
option. "No WinKeyer answered" cannot be told apart from a wrong port, a wrong
baud rate, a keyer still booting, or another program holding the line. The
bytes can, and it is four lines per connect attempt.
|
||
|
|
5bf7eb45d7 |
feat(rotator): native SPID / AlfaSpid, so PstRotator can go
An operator with a tower at each end and an AlfaSpid on both wanted OpsLog to talk to them directly. Multiple rotors were already there; the missing half was the protocol. Rot2Prog and Rot1Prog, over the controller's own COM port. The byte layout is in the package doc and pinned by table tests against Hamlib's spid.c, the reference implementation — including the one trap this protocol has: digits go out as ASCII and come back as raw bytes. Send raw and the controller ignores you; read as ASCII and every heading is wrong by a constant nobody would recognise as such. Two things the UI has to get right because they cannot be detected: the dialect (different reply length AND baud rate) and the baud list, which for a SPID is 600 or 1200 — offering the usual 4800-and-up would have left the controller permanently mute. Both are handled: picking SPID sets serial transport, 600 baud and Rot2Prog, and the baud dropdown changes to the rates these use. The connection test reads a status rather than moving anything, so a wrong dialect shows up there as a reply of the wrong length instead of as an antenna that behaves oddly an hour later. Untested against real hardware — I have none. The frames are pinned; the controller is the only thing that can confirm the rest. |
||
|
|
09f3ddaacb |
fix(confirmations): count a LoTW "V" everywhere, not only in the awards
An operator's screenshot had it side by side: the Awards panel showed Morocco validated on five bands, and the band/mode matrix two inches above showed the entity as merely worked. ADIF's QSL_Rcvd enumeration has both Y and V — "received" and "verified" — and V is what a LoTW download writes for a confirmation the ARRL has validated. The award engine's isYes accepted "Y" or "V". Everything else in the app compared against 'Y' alone: the worked-before status grid, the slot statistics, the row colouring by QSL status, the awards QSO list, the call history badges. So the confirmations an operator cares most about were the ones that did not count. Now one definition per side, named so the next reader finds the other: qso.ConfirmedValues on the Go side, used by the queries themselves, and isQSLConfirmed in lib/qsl on the frontend, which rowColors and the panels call instead of testing the letter. The Go test drives the real query shape against a real database with a 'V' row — the constant being right is not the point, the queries using it is. |
||
|
|
26b8dade20 |
feat(relays): generic HTTP relay board, and point band switching at it
An operator was about to build band→relay mapping into the custom URL rows.
It already exists, and better: relay auto-control has a per-relay "band" rule
driving webswitch, KMTronic, Dingtian, Denkovi and USB boards. It holds state
so a relay is only commanded when its wanted position changed, reads the
boards live before the first apply so it does not re-command one already in
place, and carries per-relay labels. A URL fired on a band change has none of
that, and would have been a second definition of "which antenna on which band"
— the drift that cost us a week on counties.
So the gap was only the hardware: a hand-made switch is none of the five named
types. It is now the sixth, "HTTP relay": an ON URL and an OFF URL, either as
one pattern with {relay} substituted, or one pair per relay. The per-relay
form is the reason it exists — these boxes often have URLs with nothing in
common between channels (…/FF0101 and …/FF0201), which no pattern can express.
Status is remembered rather than read: most have no endpoint worth trusting.
The cost is stated in the code and the panel — after a restart every relay is
re-commanded once, which is harmless on a board with no memory and far better
than assuming an antenna is already selected.
And the Connections panel now says so, exactly where the wrong choice is made:
picking a band change with a URL transport shows a note pointing at Station
Control. The URL transport stays — a lookup pushed to a webhook or a QSO to a
dashboard is an event, not a state, and Station Control has no place for it.
|
||
|
|
ffd6bcf54b |
fix(connections): log what was actually sent, and rename the section
Two answers to "can people debug this when it does not work".
They could not. A custom UDP row logged "sent 18 bytes to 127.0.0.1:12000"
and nothing about the message — which is a template the operator wrote, so
the byte count says nothing about whether the substitution came out as they
meant. The payload is now in the line, with the trigger that fired it, for
custom rows only: an ADIF record or a binary WSJT frame on every QSO would
bury the log rather than fill it.
Worse was the silent case. A template that is a single placeholder the trigger
does not carry — {freq} on a band change — renders empty, sends nothing, and
looked exactly like a row that had never fired. It now says so, throttled per
row, and points at the field list.
And a password in a URL is redacted before it reaches the file. Storing
http://admin:secret@switch/ as typed was a deliberate choice for LAN gear;
writing it into a log that gets sent to whoever is helping is a different
choice, and was never made.
The Settings section is "Connections" rather than "UDP integrations", since it
has not been UDP-only since the URL transport landed. The log prefix stays
"udp:" on purpose — renaming it would orphan every log an operator has already
sent and every note anyone has written against it.
|
||
|
|
6c2c1f106a |
feat(udp): custom outbound messages — a trigger, a template, UDP or a URL
The hard-coded emitters each speak one published format at one fixed moment.
This is the escape hatch, and it exists mainly for antenna switches: they are
driven by a URL, and the band change is the trigger they want.
Four triggers, each carrying its own field set: band change (the RADIO's band,
not the entry form — a switch follows the rig, not what is being typed), QSO
logged, rotator command and callsign lookup. The panel prints the fields the
selected trigger can fill, which is the point of the whole thing: a placeholder
the trigger does not carry renders as nothing, and without the list an operator
writes {freq} on a band change and has no way to learn why the switch never
moved.
Three things the panel cannot show, handled here:
- Escaping. A URL needs every value percent-encoded; a UDP payload must not
be touched. The first portable callsign, F4BPO/P, puts a path separator in
the middle of a query string otherwise — it works on the bench and fails on
the air.
- Blocking. An HTTP call to a switch that is unplugged would otherwise sit
for the operating system's timeout, on the path of a band change. It runs
in the background with a 3 s limit.
- Silence. A switch answering 404 after a firmware update fails exactly like
success looks from here, so the status code is logged, throttled per row.
The rotator trigger hooks the COMMAND rather than the SP/LP buttons, so the
compass and a spot click fire it too, and the path ("SP"/"LP") is passed
through because an azimuth alone cannot say which was taken — 137° is short
path to one station and long to another.
Credentials in a URL are stored as typed. That is the operator's call, on the
grounds that this is LAN gear, and the hint in the panel says so.
|
||
|
|
dc14990b44 |
fix(udp): send the structured QSO Logged too, not only the ADIF one
The ADIF datagram reached Logger32's socket and still nothing appeared in its log. WSJT-X sends TWO messages for every contact — QSO Logged (type 5), the structured one, and Logged ADIF (type 12) — and receivers differ on which they read: MacLoggerDX takes the structured one by default and offers the ADIF as an option. We were sending only the half Logger32 does not use. Both now go out on that row, in WSJT-X's own order. Nothing double-logs: any receiver built for WSJT-X already sees both from the real thing. Type 5 is read positionally, so the field order is pinned by a test against NetworkMessage.hpp — one field out of place shifts every one after it and the receiver files nonsense without complaining. QDateTime is Qt's own encoding: Julian day, milliseconds since midnight, then the time spec, which is sent as UTC (1) because a spec of 0 would have the receiver re-read the contact in its own zone. The Julian arithmetic is checked against known values; a day out there files every QSO on the wrong date. |
||
|
|
d95c998081 |
feat(udp): speak WSJT-X to Logger32, and lighten Sahara
"sent 1153 bytes to 127.0.0.1:2250" and nothing in Logger32. Both true: the line the operator screenshotted is titled "Setup additional WSJT/JTDX UDP sockets", and those receivers take UDP LOGGING PACKETS — the WSJT-X v2 protocol. A bare ADIF record posted to them is dropped without a word. So there is a new outbound service rather than a change to the existing one: "WSJT-X logged QSO (Logger32)" sends the same ADIF wrapped in a Logged ADIF datagram (magic 0xadbccbda, schema 2, type 12), defaulting to port 2250. The plain-text row stays for JTAlert/GridTracker-style receivers, because the two really are different wire formats and one row cannot be both. The id string is "OpsLog", not "WSJT-X": a receiver uses it to tell instances apart, and impersonating the real thing would make a second WSJT-X indistinguishable from us. Tested both ways — the header byte for byte, and a round trip back through our own ParseWSJT, since that is the same decoding every receiver applies. Sahara moves to the parchment an operator asked for: page #e6ded0, panels #f3eee2, toolbars #ded5c4, terracotta #c4501e on the buttons and the focus ring. Lighter and less saturated than the deep sand it was. |
||
|
|
c5c9d355ec |
fix(logging): name the antenna disagreement, and confirm an outbound send
Two silences from one operator's log. The SteppIR is not blocked — it is being overruled. Every tune is commanded, acknowledged on the next poll with the frequency asked for, then replaced on the poll after by a different one: 21075 asked, 21075 confirmed, 21050 reported, again and again, with the operator tuning once more each time. Two minutes of that reads as a dead link. It now says what it is, once, when the motors have stopped and the reported frequency is more than 10 kHz from the commanded one — a controller quantising to its own grid is not a fault, one sitting somewhere else entirely is. UDP outbound had no positive signal at all: only failures were logged. When an operator sets up an ADIF message to a second logger and nothing arrives, "OpsLog never sent it" and "the other program did not take it" looked exactly the same from here — and the first is the common case, because a row created as an inbound ADIF listener instead of an outbound ADIF message matches nothing and emits nothing. Successful sends are logged with their destination and service, and a QSO logged with no outbound row says so once a session. |
||
|
|
2cd1274975 |
fix(logging): say why the antenna stopped and what the ADIF port is receiving
Two problems from one operator's log, both of them the log's fault. SteppIR. The last line about it is "status query failed, reconnecting: Port has been closed" and then nothing — for the rest of the session. The poll loop does retry every two seconds, but a failed open was a bare `continue`: no line, ever. And startUltrabeam had three silent ways out — settings unreadable, the antenna turned off, no port or host configured — so a cleared field and a lost adapter produced exactly the same evidence, which is none. Both now say what happened, with the port or host named. Reopen failures are reported three times and then throttled (a port that is gone stays gone), and the recovery says how many attempts it took. FLDIGI. Four hundred "ADIF payload ignored" lines and nothing else legible. The cause is in the same log: two inbound listeners on the SAME multicast group and port, 239.255.0.1:2237 — one WSJT (MSHV), one ADIF (FLDIGI). Every WSJT-X packet is delivered to both, and an FT8 cycle is dozens of decodes every fifteen seconds; the bursts in the log are exactly 15 s apart. The ADIF listener now checks for the WSJT-X magic and says so once, with what to do about it, instead of rejecting each packet in writing. Anything else is described a few times with its payload and then goes quiet. |
||
|
|
9b1faada38 |
fix(chase-new): one badge, filters, a way in and a way out — and the frequency
Four things from an operator's first look at the panel, and one of them was mine. The frequency column showed "—" on every row. pskr.Spot.FreqHz was declared and documented in one edit and never assigned in the next, so the payload's "f" was parsed and dropped. A click therefore filled the callsign and left the rig where it was, which is half the point of the panel. Assigned, with a test that runs a real payload through and checks the value survives into the Spot — the field being declared is what made it look done. A row now carries ONE indication instead of stacking them. A station can be a new band and a new prefix at once; the row says the first that matters, in the order that would make an operator leave what they are doing: entity, band, mode, slot, then prefix, then square. Two badges on one line made the list unreadable at a glance, which is the only thing it is for. Each category has a filter chip in the header, in its own colour, remembered across sessions. A toolbar button shows the panel and a cross closes it — the same split the Super Check Partial panel uses, where the setting decides whether the feature exists and the button whether it is on screen. The panel is wider and the country column gets what is left, which is more than it was now that a row carries one badge. |
||
|
|
a9e3a159d9 |
feat(cluster): Chase New — a panel for what is new and audible here
PSK Reporter tells you what is actually being decoded in your region, which is a larger set than what somebody chose to spot: nobody spots the FT8 caller running ten watts from a rare square. Almost all of it existed. The MQTT payload already carries frequency, mode, transmitter and both grids; the watcher already drops any report collected further than NearKm from the operator, which is exactly the question worth asking — the station is being heard HERE, not in Japan; and with grid chasing on the subscription is already every band, filtered at the broker by receiver square, measured at 0.2 to 1.2 messages a second. This reads messages that were arriving and being discarded. "New" is not decided here. Every spot goes through ClusterSpotStatuses, the same function the DX cluster grid uses and the same cached index, so the two panels cannot drift apart the way the county columns did. Cost per message is map lookups behind an option cached in an atomic, because the MQTT goroutine must never wait on the settings store. The option is its own, not nested under grid chasing: chasing squares and chasing entities are different wants, and the feed now has three consumers, any one of which brings it up and none of which cuts the others loose when it goes down. The panel says "digital modes only" in its footer. An empty list has to mean "nothing new on FT8/FT4/JS8 near you", not "the band is dead" — it will never show a new entity on CW. |
||
|
|
3ce930e9cc |
refactor(winkeyer): learn the slow boot instead of naming the keyer
The K3NG entry added an hour ago is gone. It named one clone among many — WKmini, home-built Arduinos, unbranded boxes — for hardware that speaks exactly the same protocol, and it was the only line in the engine list that picked a boot delay rather than a protocol. An operator with an unlabelled clone would have had to guess. The delay is now learnt per port. The first connect finds out by failing the quick attempt and succeeding on the slow one; that fact is written to a global setting keyed by the port, and every connect afterwards goes straight to the slow attempt. Global rather than per profile on purpose: which keyer is plugged into COM3 belongs to the computer, and switching profiles for a different rig does not change the keyer on the desk. Two tests hold the contract from both sides — a slow keyer must be reported as slow, and a keyer that answers at once must not be, or every K1EL connect would inherit seconds it never needed. |
||
|
|
aca4dc5678 |
feat(winkeyer): K3NG keyers, and wait out their reboot
The operator's keyer is a K3NG — an Arduino running an emulation of the WinKeyer protocol — which changes the diagnosis and nearly broke it. Checked against K3NG's own source before anything else, because yesterday's handshake now FAILS a connect where it used to press on regardless. It is safe: k3ng_keyer.ino implements admin echo (0x04) and echoes the byte back, 0x13 is a documented no-op, and OPTION_WINKEY_STRICT_HOST_OPEN — on by default — ignores every byte except 0x00 before host open, so the resync nulls are dropped harmlessly and the echo probe still gets through. The real cause is in K3NG's options file, beside the feature itself: "disabling Automatic Software Reset is highly recommended", and an option to "discard errant serial port bytes at startup" for when it is not. On an Arduino, DTR is wired to reset through a capacitor: opening the port reboots the board into its bootloader. Ours spoke 400 ms later, to a keyer that was not running yet. So the retry now waits 2.5 s, which recovers it without an operator pressing connect twice, and the engine list gains a K3NG entry that skips the doomed fast attempt altogether. Everything else is identical to a K1EL — same settings panel, same protocol. |
||
|
|
1b736272c9 |
fix(winkeyer): perform K1EL's full opening handshake
An operator's log showed the whole fault in its first line: "connected on
COM3 — no reply — the keyer did not answer Host Open", followed by seven
configuration commands and two calls sent as Morse. Nothing was listening.
Reporting a link as up and then writing to it regardless is the part worth
fixing; the handshake is why it was down.
K1EL's Application Interface Guide gives the sequence, and we did one step
of it. Now all of it:
- DTR on, RTS OFF. K1EL's own init sets DTR_CONTROL_ENABLE with
RTS_CONTROL_DISABLE, and on a serial WinKeyer those lines ARE the power
supply — DTR feeds the 3.3 V regulator, RTS provides the negative rail.
go.bug.st/serial defaults both to true, so we drove RTS high on every
connect without a line of code saying so.
- 400 ms after the lines come up, for a WK1 still booting off DTR.
- Three 0x13 nulls to resync the command parser. A keyer left part-way
through a command by whoever spoke to it last would absorb Host Open as
a parameter — the everyday cause of a silent WinKeyer, and one the
operator can do nothing about from the outside.
- An echo test (0x00 0x04 0x55) before trusting the port at all. This is
the step that answers "is there a keyer here", and connecting now fails
on it, with the byte that came back when something else replied.
The whole handshake is retried once, since the first attempt's nulls are
what clear a confused parser. Tested against a fake port that reproduces
each failure: absent, mid-command, and echoing but versionless.
|
||
|
|
4e88bdfaa7 |
fix(qsl): separate the QSL manager from the routing method (#16)
QSL_VIA is the manager. QSL_SENT_VIA and QSL_RCVD_VIA are the ADIF "QSL Via"
enumeration — B bureau, D direct, E electronic, M manager (import-only) —
and say how a card travelled. OpsLog had one column for all three:
- the import folded QSL_SENT_VIA into QSL_VIA whenever QSL_VIA was empty,
which is exactly a Log4OM export (it defaults QSL_SENT_VIA to E), so
OE6CLD saw "E" everywhere OpsLog shows the manager;
- QSL_RCVD_VIA was listed in adifPromoted with no column behind it, so it
was not stored, not kept among the extras, and not exported — dropped
outright on import;
- neither was ever written on export, so an import followed by an export
destroyed both;
- and OpsLog polluted the field itself: the QSL Manager panel wrote
"Bureau" / "Direct" / "Electronic", in full words, into QSL_VIA.
Two columns added (migration 0027), carried through the five places a
promoted ADIF field has to touch, with round-trip tests pinning the reported
case. The QSL panel now offers Bureau / Direct / Electronic for each
direction and stores the enumeration; the manager field is labelled as the
manager and holds only that. M is kept when a file gives it and never
written back out.
Existing logs hold a mixture of the two in one column. The repair is offered,
not performed: the count is shown once per log with a plain question, and a
"no" is remembered. It moves only where QSL_SENT_VIA is still empty, and only
values that normalise to the enumeration — a manager is a callsign and can
never be one of those six words, which a test pins against real manager calls.
|
||
|
|
30143b01bf |
fix(counties): make the cluster and the entry panel agree on a US county
An operator asked why K1SEI showed "Middlesex" in Info (F2) and "Lower
Connecticut River Valley" in the cluster. Two sources: the entry panel has
a callbook answer, a spot carries only a callsign so the cluster derives one
from the FCC licence ZIP through GeoNames — and GeoNames has followed the
Census in replacing Connecticut's counties with the 2022 planning regions.
No award, callbook or log uses those, so every CT station matched nothing:
new county for ever, and counting toward nothing.
Measured against a full ULS import (1 556 444 US callsigns), 23 223 resolved
to a name the USA-CA reference does not contain. Three causes, three fixes:
- Spelling. "City and County of San Francisco", "Baltimore (city)",
"Nome (CA)", plus counties renamed since the award list was drawn
(Kusilvak, Oglala Lakota, the Valdez-Cordova split) and Alaska's four
"X City and Borough", whose reference codes read "JUNEAUCITYAND" because
the county-type suffix strip eats the wrong end. Normalised in
award.USCountyKey, which both sides already go through. 7 515 callsigns,
no re-download needed.
- Doña Ana, NM shipped into the reference as "NM/DO̱AANA" — mangled by a
non-UTF-8 CSV line, a code nothing could ever produce, so that county was
unwinnable and silent about it. Row repaired, cntygen now refuses such a
line, and a test makes every one of the 3 102 references reproduce its own
code from its own name.
- Connecticut. A planning region is drawn from towns in several counties, so
no name maps to a name — only the ZIP can resolve it. cmd/ctzipgen builds
the table from the Census 2020 crosswalk, filling PO-box-only ZIPs from the
nearest resolved centroid; all 11 ZIPs GeoNames still labels with a real
county agree with the result. 15 037 callsigns, applied at import, so the
store now carries a rules version and Settings says when a re-download is
needed.
Alignment itself is the last piece: a spot now shows the county the station is
logged with when we have one, and falls back to the ZIP-derived county only for
stations never worked.
|
||
|
|
4e2a5877d6 |
fix(rigctld): stop refusing two commands JTDX sends in Fake It
From a user's log, repeating throughout the session: rigctld: split off failed: cat: this radio's backend cannot set split rigctld: unimplemented command "X PKTUSB -1" Both are answered with an error, and JTDX treats a rig-control error as fatal: it abandoned a transmission 0.86 seconds into a 13.8-second frame. "S 0" — split OFF. Fake It uses no split, and JTDX still sends this to be sure. A backend that cannot SET split was refusing a request that was ALREADY satisfied. It now succeeds when the rig is simplex, without touching the rig at all. Arming still fails loudly: there the request is real and unmet, and a client that believes it transmits up the band while the radio sits on the DX's frequency is exactly what that refusal exists to prevent. "X <mode>" — set_split_mode. Sent during ordinary setup, Fake It included, where there is no second VFO to give a mode to. Accepted now: applied when a split is in force, and a plain success otherwise, because the transmit VFO already has that mode — it is the same VFO. get_split_mode answers to match. Both regressions fail without the fix with the exact codes from that log, RPRT -9 and RPRT -11. |