Commit Graph
100 Commits
Author SHA1 Message Date
erikbrockoandClaude Sonnet 5.5 8c80a46e5e Refresh the windows once after typing, not per letter
Each key press in an entry box ran Refresh, which redraws the entry window
and every open tool window between two key presses. The refresh is now
posted at background priority, so the key presses already queued are
handled first and a burst of letters costs one refresh.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 11:09:50 +00:00
erikbrockoandClaude Sonnet 5.5 826aa0ef97 Commit resync writes in one transaction
Each insert, delete and rescore update was its own commit to disk, so a
resync of about a thousand QSOs froze the window thread. LogStore has a
new Batch() scope that commits its writes together. ContestSession uses
it in AddRangeFromNetwork, ClearFromNetwork and Rescore.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 11:01:05 +00:00
erikbrockoandClaude Sonnet 5.5 36af4340cf Add settings to minimize and raise the windows together
Both are checkboxes in Manage Skins, Colors and Fonts and default to on.
The entry window minimizes and restores the other windows with itself.
Activating any window raises the others that are not minimized.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 10:56:53 +00:00
erikbrockoandClaude Sonnet 5.5 ab81476e27 Show tool windows without an owner so they can go behind the entry window
An owned window is transient-for its owner on X11, so the window manager
kept every tool window above the entry window. The windows now have no
owner. The entry window closes them when it closes, and minimizes or
restores them when its own state changes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 10:50:02 +00:00
erikbrockoandClaude Sonnet 5.5 71c11a9316 Replace the Networked stations window with Options tab fields
Station Number, Port and Version to claim are now on the Options tab of
Network Status and save when a box loses focus. The other settings
(station name, listen port, peers, link on/off) are in a Stations
Configuration window opened from a button on the same tab. The explanation
texts and the Config menu item are removed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 10:43:23 +00:00
erikbrockoandClaude Sonnet 5.5 209c557112 Skip Run QSOs on the bandmap and refocus the callsign box after Alt+W
A QSO logged in Run mode is no longer added to the bandmap as a local
spot, including when recent contacts are re-spotted at startup.

Releasing Alt moves focus to the menu bar. Alt+W now sets a flag, and
the Alt key-up handler puts focus back on the callsign box.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 13:30:12 +00:00
erikbrockoandClaude Sonnet 5.5 d22787fb66 Rework the telnet window's cluster favourites and login tabs
Favourites show the callsign in bold, the address and the sysop's note,
and each keeps its own password, commands, logon call and note. Clicking
one fills the boxes for editing. Autoconnect marks the favourite to
connect to at start and replaces the old reconnect-to-last-node setting.
A new published list refreshes the notes of the favourites in it. The
published list now spans the window, and the Spot comment tab becomes
Login & Spots with the logon call and keep-alive.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 13:03:13 +00:00
erikbrockoandClaude Sonnet 5.5 7be51b72c7 Reduce the padding of list rows
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 12:48:33 +00:00
erikbrockoandClaude Sonnet 5.5 f31ee5bc03 Move the bandmap settings to a Configure Bandmap window
The spot timeouts, the QSY spot and CQ frequency distances and the
recent contact minutes leave the telnet Filters tab for a window under
Config. Each is a numeric box with a range.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 12:31:34 +00:00
erikbrockoandClaude Sonnet 5.5 9d0de3b129 Set the bandmap spot timeout separately for CW/digital and phone
CW and digital spots time out after 10 minutes and phone spots after 30
by default, both settings on the telnet Filters tab. The mode comes from
the band plan; a frequency it has no boundaries for counts as phone.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 12:27:55 +00:00
erikbrockoandClaude Sonnet 5.5 7c25816b74 Put the contacts of the last 20 minutes on the bandmap as local spots
A contest opening and contacts arriving from the other stations, a
resync included, spot the contacts logged in the last few minutes (a
setting on the telnet Filters tab, 0 for none). A station the bandmap
already holds on that band keeps its spot. Nothing is sent out. Spots
that come from the log are drawn in italics.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 12:27:55 +00:00
erikbrockoandClaude Sonnet 5.5 cf39b5af9c Show only the newest SFI, A and K in the solar weather bar
For sh/wwv, only the first record under the header is used, since the
table lists the newest first. The date, time, forecast and logger are
dropped, and the status bar reads "SFI 140  A 5  K 2". The line the node
sent is the tool tip. Announcement lines give the same three values.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 12:17:44 +00:00
erikbrockoandClaude Sonnet 5.5 e55159ba49 Show the cluster's solar weather in a bandmap status bar
The reader follows N1MM's rules for WWV and WCY lines and the sh/wwv
table. The bar is hidden until the cluster has sent some.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 12:12:35 +00:00
erikbrockoandClaude Sonnet 5.5 1e0fb3eb08 Mark the CQ frequency on the bandmap and leave Run when the radio moves off it
Starting Run stores the frequency in the position. The bandmap draws a
purple, bold "CQ Frequency" pseudospot there, which is not added to the
shared bandmap, and a click on it tunes back and sets Run. A move of more
than 3 kHz (a setting on the telnet Filters tab) while running switches
to search and pounce.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 12:12:35 +00:00
erikbrockoandClaude Sonnet 5.5 ab98c1b871 Leave the cluster's time out of the bandmap hover comment
A cluster line ends with the time of the spot, which the age on the
first line already says.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 12:12:35 +00:00
erikbrockoandClaude Sonnet 5.5 7139bda1fc Halve the extra reach of the bandmap carrier tick to the right
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 12:12:35 +00:00
erikbrockoandClaude Sonnet 5.5 b32c09aeef Add a 60 kHz bandmap zoom, a mode-wide zoom and denser scale ticks
The zoom list gains 60 kHz and a "mode" level that shows the CW, digital
or phone part of the band the receiver is in, before "band". For views
from 40 to 200 kHz wide, the scale picks the smallest round step that
keeps three lines of text between ticks. The font size buttons are
removed; Ctrl with the wheel or plus and minus still change the size.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 12:12:35 +00:00
erikbrockoandClaude Sonnet 5.5 a189f63732 Show local spots in bold and name the computer that made them
A spot an operator put on a bandmap, here or on another computer, is
drawn in bold. Its hover text names the computer instead of the
spotter's callsign. A spot from another computer carries the name from
the ADDSPOT message.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 11:51:13 +00:00
erikbrockoandClaude Sonnet 5.5 0c61401c2d Show tool tips at once and widen the bandmap carrier tick
The tool tip has no show delay and the theme's fade is removed. The
carrier tick reaches 30 px further to the right.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 11:51:13 +00:00
erikbrockoandClaude Sonnet 5.5 58f417a1fb Shorten the bandmap hover text to frequency, spotter, age and comment
The first line reads "14025.0 by K1TTT, 41m ago". The comment follows
on a second line and is left out when it is only LSB, USB or SSB.
Control characters are removed from the comment: cluster lines end in
bell and carriage-return characters, which the tooltip drew as boxes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 11:51:13 +00:00
erikbrockoandClaude Sonnet 5.5 70c16a563f Measure a QSY spot from where the call was first seen
The radio reports the frequency in small steps as the knob turns, and
each step was compared with the one before, so no step passed the
threshold. The move is now measured from the frequency the radio had
when the call appeared in the entry window. The threshold is a box on
the Filters tab of the telnet window.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 11:51:05 +00:00
erikbrockoandClaude Sonnet 5.5 45040dff83 Zoom the text of the log, check, available and bandmap windows
Ctrl+wheel and Ctrl+plus / Ctrl+minus change the text size of the
window in front, and Ctrl+0 puts it back. Each size is kept in the
settings file.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 11:37:35 +00:00
erikbrockoandClaude Sonnet 5.5 0b2f8e2820 Show scroll bars at full width, drop the log status bar
- Turn off scroll bar auto-hide for ScrollViewer and DataGrid.
- Make the splitter between the two log grids 5 px, in the grid line color.
- Remove the log window status bar. The stats are in the entry window.
- Show a refused log edit in an alert dialog.
- Keep the log grid's rows in an ObservableCollection, so a new contact
  does not rebuild the list and move the view.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 11:28:01 +00:00
erikbrockoandClaude Sonnet 5.5 f784a73115 Spot a call locally and wipe when the radio moves far on a QSY
With a call in the entry window, a move of more than 2 kHz on the same
band puts the call on the bandmap where it was heard and wipes the boxes,
as N1MM does. The spot goes to the other stations as ADDSPOT and to the
Spots broadcast, never to the cluster. ADDSPOT from other stations is
added to the bandmap.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 05:50:20 +00:00
erikbrockoandClaude Sonnet 5.5 986f359a40 Read the zone lists in the country file
The # blocks in wl_cty.dat list CQ and ITU zones by call area and the
first letter after the digit, written with the main prefix letters. A call
now takes them whatever its own prefix letters are, so RA9Y and RM9I get
what UA9Y gets, as in N1MM. A call whose own prefix rule gives a zone, and
a whole-call entry, are left as they are.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 05:50:20 +00:00
erikbrockoandClaude Sonnet 5.5 3a39b40edb Fill the zone box from the log and the country file on space
Without a call history file, the CQ zone box stayed empty after the
callsign. It now takes the zone from the call history file, else from the
last logged contact with the call, else from the country file, as N1MM
does. A box that already holds text is left alone.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 05:24:56 +00:00
erikbrockoandClaude Sonnet 5.5 072c04883e Mirror a radio from another computer's broadcast data
Listen for Radio broadcast data from another Nonemm or N1MM computer and
record its frequency, mode and split as this computer's own radio. No
radio is controlled. The settings are in the Configure Radios dialog.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 05:16:27 +00:00
erikbrockoandClaude Sonnet 5.5 75fc6c02d2 Stop the recorder from hiding text with a null foreground
Setting Foreground to null is a local value of no brush, so the Record
button, connected devices in the list and the status line had no text
colour. Clear the value instead. The bitrate step is now 4 kbps.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 03:55:20 +00:00
erikbrockoandClaude Sonnet 5.5 b8007355e4 Keep N1MM's message columns when the contest definition is saved again
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 03:50:24 +00:00
erikbrockoandClaude Sonnet 5.5 393b73f87a Write N1MM's own Contest table rows for the built-in contests
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 03:47:29 +00:00
erikbrockoandClaude Sonnet 5.5 79c65c92a7 Write N1MM's own multiplier names into the Contest table
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 03:45:14 +00:00
erikbrockoandClaude Sonnet 5.5 65178b2d89 Add the Broadcast Data dialog and send N1MM's score breakdown
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 03:42:19 +00:00
erikbrockoandClaude Sonnet 5.5 ec12fb1d0f Start the station link on launch and beacon on every interface
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 03:22:34 +00:00
erikbrockoandClaude Sonnet 5.5 c8bc141829 Improve the bandmap window
Remove the status bar and the band name, add the 75 and 150 kHz spans,
add buttons for the callsign size (saved in the settings), draw the scale
in the form's text colour so it shows in the dark theme, mark the carrier
on the VFO bar and place a sideband bar on its side of the carrier, and
show who spotted a station, when, and the comment when the pointer is on it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 01:21:45 +00:00
erikbrockoandClaude Sonnet 5.5 7cf8cf29ec Draw the line under the selected tab in the text colour
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 01:14:21 +00:00
erikbrockoandClaude Sonnet 5.5 2c1e18803d Make the selected tab stand out
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 01:11:47 +00:00
erikbrockoandClaude Sonnet 5.5 3e1d3fe183 Restore the entry window's saved width
The entry window sizes its height to its content, and Place skipped the
saved size for any window that sizes itself on either side.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 01:11:18 +00:00
erikbrockoandClaude Sonnet 5.5 29d3a0f016 Name a monospace font list instead of the generic monospace
The generic name only resolves through fontconfig, so on Windows the
windows fell back to the proportional default.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 01:07:47 +00:00
erikbrockoandClaude Sonnet 5.5 ae37e0be53 Write an update archive with only the files changed since the last publish
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 01:07:47 +00:00
erikbrockoandClaude Sonnet 5.5 c6eb1355a8 Make the tab headers smaller and use the normal text colour
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 01:07:47 +00:00
erikbrockoandClaude Sonnet 5.5 047a3612cb Zoom the bandmap with the mouse wheel and add a Centre on VFO toggle
The wheel steps along the span list and keeps the frequency under the
pointer in place. With Centre on VFO off, the slice stays put until the
receiver nears an edge. The span and the toggle are saved in the settings.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 00:52:06 +00:00
erikbrockoandClaude Sonnet 5.5 476844a9f0 Make the telnet traffic a compact terminal view
Lines were as tall as a list item, with padding of more than the text. A
line is now as tall as its text, the view always follows the newest line
instead of pausing under the pointer, and a burst of lines is scrolled
once. The scrollback is a thousand lines.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 00:46:54 +00:00
erikbrockoandClaude Sonnet 5.5 f336b89c37 Send contacts in the layout N1MM actually reads
QsoRecord followed MultiOpManager.QSOString, a helper N1MM does not use
for the network. The messages use Contact.QSOString: 43 fields, the
continent before the contact type, and flags as numbers. N1MM converts
the multiplier flags with ToShort and threw on 'True', so every contact
logged here raised an error at the other computer.

The id now goes out as a GUID and is kept on arrival, which is what the
other station's checksum of a resync block is made over.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 00:41:51 +00:00
erikbrockoandClaude Sonnet 5.5 0a423e9e1f Choose the telnet window's colours on the window thread
The window picked a brush on the socket thread, and Avalonia crashed the
program the next time it painted that line. A line that arrives is now
posted to the window thread first. Spots no longer show the bell
character as a box, and an unexpected error in the cluster loop is
shown instead of ending it silently.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 00:40:11 +00:00
erikbrockoandClaude Sonnet 5.5 085584a281 Fix the crash on changing network settings, and apply resynced contacts in batches
ApplyNetworkSettings disposed the station link without clearing it, so
ApplyStationLinkSettings disposed it a second time and threw. Only the
second function touches the link now, and StationLink.Dispose can be
called twice.

Contacts from the network are queued and applied together on the window
thread. Each one used to rescore the whole log and redraw the log window,
which kept the window thread busy for minutes during a resync.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 00:26:03 +00:00
erikbrockoandClaude Sonnet 5.5 0e5aa3238b Publish the logger and the recorder in one archive
publish.sh takes a runtime id and publishes both programs into
dist/nonemm-<rid>, which holds the runtime and the Avalonia files once.
The archive is a zip for Windows and a tar.gz for the rest.
publish-windows.sh and publish-linux.sh call it with their default id.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 23:57:47 +00:00
erikbrockoandClaude Sonnet 5.5 c01032a2cc Add a sound recorder that writes several devices to one Opus file
nonemm-recorder is a separate program (src/Nonemm.Recorder.App) over a
library (src/Nonemm.Recorder). Each row in its list is one channel of one
device, and each row becomes one channel in an Ogg Opus file: 60 ms frames,
complexity 9, 32 kbps per channel by default.

- A device that is not connected keeps its row, in red, and writes silence.
  It is recorded from the moment it returns, also during a recording.
- Output devices are recorded as loopback: WASAPI on Windows, the
  "Monitor of" sources on PulseAudio and PipeWire.
- A headphone button plays one channel, in mono, on the default device.
- The rows, bitrate and folder are saved when the window closes.

SoundFlow 1.4.2 numbers its backend enum one higher than the miniaudio it
ships, so asking for PulseAudio gave ALSA and Wasapi gave DirectSound.
SoundFlowBackend lowers the value by one and checks which backend started.

Output devices and live capture are not tested on real hardware. The
tests use a fake sound system and decode the written file.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 23:47:45 +00:00
erikbrockoandClaude Sonnet 5.5 c18d79ba5a Run the digital bridge on Windows
The bridge launched its helper only through Wine. It now starts any
process, so on Windows it runs the helper directly. WineBridgeChannel is
renamed ProcessBridgeChannel.

WindowsControlSetup sets up the control side on Windows. The digital
dialog, AppSession, README and docs/digital-bridge.md describe the
Windows setup. publish-windows.sh builds a Windows release.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 21:56:47 +00:00
erikbrockoandClaude Sonnet 5.5 0ad894bfae Write LOCATION to Cabrillo, and give CQ WW M/2 eight band changes an hour
CQ WW lists LOCATION as required, and the export never wrote it. It now
carries the ARRL section, state or province, or DX.

CQ WW M/2 has no stay on a band: it may make eight band changes in a clock
hour. It was getting the ten-minute stay that M/S has.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 14:18:42 +00:00
erikbrockoandClaude Opus 5 b5043506ae Fill in the macros from N1MM's function key page
Going down N1MM's published list, the ones a contest operator would miss:

| | |
|---|---|
| `{LASTEXCH}` | what the last station sent, built from the boxes the contest asks for |
| `{ROVERQTH}` | where a rover is now |
| `{TIME}`, `{DATE}`, `{TIMEGMT}`, `{DATEGMT}` | the clock and the calendar |
| `{ESC}`, `{CTRL-A}` to `{CTRL-Z}` | the control characters a TNC reads as commands |
| `{CTRLHOME}` | the cursor back in the callsign box |
| `{F1}` to `{F12}` | another of this radio's keys, where `{CTRLF1}` is the other radio's |
| `{CQ}` | makes any key a CQ key: it starts running and carries alternating CQ on |
| `{DIGQTCS}`, `{DIGQTCR}` | opens the WAE QTC window the way asked for |
| `{CLEARRIT}` | the receive offset back to zero, which is rigctld's `J 0` |
| `{FORCELOG}` | logs what is in the boxes, holes and all |

`{FORCELOG}` is the one that gives something up. `LogContact` refuses an entry
that is short or holds a value the contest will not take, so that a caller who
has not checked cannot log nonsense; `LogContactAnyway` is the way past it,
because a contact in the log with a hole in it can be filled in afterwards and
one that was never logged cannot. It still needs a callsign, and the status
line says the row will need fixing before the log goes in.

A key that names itself would send for ever, so a key sending another key
ignores the macro a second time down.

What is still passed over is written down in the README and unfinished.md.
Advanced voicing is the one that matters: N1MM says a callsign or a serial by
playing a recording per letter out of `LettersFiles`, which is why `!` and `#`
say nothing on phone here and why F5 is empty in N1MM's own SSB file too. The
three packet BBS date formats are passed over because there is nothing here to
check a guess at them against.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-11 08:24:34 +00:00
erikbrockoandClaude Opus 5 31ad6fb192 Record a message with Ctrl+Shift+F, and open the editor with Alt+K
N1MM records a phone message from the entry window: Ctrl+Shift+F1 to
Ctrl+Shift+F12 records into the file that key plays, and the same pair again
stops and saves it. Escape and sending anything stop one too — there is one
microphone, and it cannot be recorded and transmitted at the same time. The
status line says what is being recorded and what was saved, which is where
N1MM says it.

The recording being made is the session's rather than the window's, for the
same reason: the voice keyer window and these keys record into the same
microphone, and now each can see what the other started.

Alt+K opens the function key editor for the mode the radio is on, which is
N1MM's shortcut for what right-clicking a key button already did.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-11 08:24:21 +00:00
erikbrockoandClaude Opus 5 082fa75a40 Play a recording on phone, and record it in a window
A phone function key names a wav file instead of words, which is what N1MM's
own SSB file holds: `F1 CQ,{WAVDIR}\Cq.wav`. `{WAVDIR}` is the folder named
after whoever is at the microphone, a name is taken relative to `Wav`, and
commas play several files in a row. An SSB `.mc` file written for N1MM plays
here.

Window ▸ Voice Keyer lists every recording the messages name, says which of
them are there for the operator picked at the top, plays one, and records over
one. The recording is written beside the wanted file and moved over it at the
end, so a recorder that will not start takes nothing away.

`CommandSoundCard` runs `aplay` and `arecord`, and both commands are settings:
the audio path belongs to whatever the station runs, and a library would pick
one of them for the operator. A recorder is stopped with an interrupt so it
closes its wav file, and killed only if it ignores that. PTT is cwdaemon's
`<ESC>a` for as long as the recording plays, off for a station on VOX.

Two things follow. The QTC window sends on SSB: N1MM's four recordings on the
Ready, Agn, Cfm and Close buttons, with the lines still read out by the
operator. And alternating CQ duels on phone, because the voice keyer says when
a recording has finished — which it had to, since the CQ message is picked by
the radio's mode and would otherwise have been keyed as Morse.

Nothing here has been played on a radio: this machine has no sound card, so
what is tested is the program being run, the file filled in, the exit code and
the interrupt, with shell commands standing in for the two.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-11 07:51:58 +00:00
erikbrockoandClaude Opus 5 dae1f4f197 Clear the stale items out of the docs
The scoring audit says which of its findings are fixed and which are still
open, its line numbers point at where the code is now, and the order of what
to do next is what is left rather than what is done.

In unfinished.md: Send All was listed as missing 500 lines above the section
that describes it, three TypeAhead methods had been renamed, the station
number clash is guarded and the paragraph saying it is not was next to the one
saying it is, and the registry holds twenty-six contests rather than
twenty-five. The claim that every instance of the station's logs scores
contact for contact is replaced by what the replay of 2026-09-06 actually
found.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-11 07:51:45 +00:00
erikbrockoandClaude Opus 5 b5d9f1e21c Name the All Asian multiplier after what it counts
The score window and the stored Contest.Multiplier1Name row both said
"Prefixes" for an Asian entrant, which counts DXCC entities.

Contest.MultiplierNames is now MultiplierNamesFor(StationInfo me), the
same shape as ExchangeFieldsFor. Every other contest returns the same
list it did before; All Asian returns "Countries" from Asia and
"Prefixes" from everywhere else. ContestDefinitions.For takes the
station as well, because that name is written to the log for N1MM.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-10 20:47:51 +00:00
erikbrockoandClaude Opus 5 f5e3d9ed8a Score All Asian from the entrant's own side
All Asian is two contests in one class and only one of them was written.
An Asian entrant was paid three times the band table for every contact,
including Asian ones, could work its own country, and counted WPX
prefixes where the rules count DXCC entities.

AllAsian now asks where the entrant is. An Asian station works anybody
outside its own country, gets the band table once for Asia and three
times for the rest, and counts entities. Everybody else works Asia only
and counts prefixes, as before.

Maritime mobiles follow the JARL rule rather than N1MM, which applies it
to non-Asian entrants alone: a station at sea makes a valid contact with
Asian stations only, is paid the Asian rate wherever it is, and counts
for no multiplier either way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-10 20:43:43 +00:00
erikbrockoandClaude Opus 5 04b6f14639 Mark the Oceania finding fixed
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-10 20:38:07 +00:00
erikbrockoandClaude Opus 5 5a3b2db135 Pay Oceania's high bands what they are worth
The band table stopped after 40 metres and gave every other band one point.
It should be 20 on 160, 10 on 80, 5 on 40, 1 on 20, 2 on 15 and 3 on 10:
from Oceania the high bands are the hard ones, so they pay more than 20
metres does. The WIA scoring table and N1MM's COceania.ComputePoints agree
on all six.

The catch-all hid a second fault. N1MM's table ends without one, so a band
that is not in the contest — the WARC bands, 6 metres — scores nothing;
here it scored a point.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-10 20:38:00 +00:00
erikbrockoandClaude Opus 5 211298e04c Let the walk and the completeness check read one rule
A box the walk between the entry boxes steps over cannot be typed in, so it
must not hold the contact back either. `EntryFields.IsComplete` counted every
box marked required, while `MoveFocus` stepped over the ones the contest does
not ask this station for, so a contest with a box for some stations only
could lock the operator out of his own log.

The seven contests the scoring audit named do not do that any more —
`bae40bb` gave each of them the boxes N1MM shows, and fifteen reproductions
across all seven and both station types log cleanly — but the rule they broke
was still there to break again. Completeness now takes the same
applicability rule the cursor uses, and `OperatingPosition` answers it in one
place for the entry window, the ESM window and logging alike.

The audit is marked up for the three findings that are already fixed and for
item 14, which turns out to be what N1MM does rather than a fault.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-10 20:33:30 +00:00
erikbrockoandClaude Opus 5 e56d6096f3 Say in the docs that the RTTY QTC messages are there now
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-10 20:04:19 +00:00
erikbrockoandClaude Opus 5 2079bf3f59 Paint a WAE station by the traffic left with it, as N1MM does
N1MM has four colours for a station in WAE rather than the usual three, and
the manual and the RTTY tutorial both spell them out. Red is a new
multiplier, as everywhere else. Green is a new station with QTC traffic
still to exchange, which takes the more-than-one-multiplier colour because
WAE has no second multiplier to use it. Black is a station already worked
that still has traffic left — worth working for the QTC points alone — and
it is a colour of its own, N1MM's "Color of a QTC callsign in the WAE
contest". Grey is a dupe with nothing left, the same as in other contests.

The verdict carries how much traffic is left with the station, so the
callsign box, the check window, the bandmap and everything else that colours
a station say the same thing. The contest works the number out: ten with a
station on the other side of the contest, none with one on our own
continent, and none once the ten have gone. Every other contest answers
nought. The log counts the QTC rows per station as they are indexed rather
than counting them again for each station judged, because the bandmap judges
every spot it holds on every pass.

The verdict line says it in words as well, so a dupe reads `dupe · 7 QTCs
left` rather than `dupe`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-10 20:03:32 +00:00
erikbrockoandClaude Opus 5 f24cab0ae5 Work QTC traffic on RTTY the way N1MM does
The QTC window put nothing on the air on RTTY except the lines themselves,
so the operator typed the rest into the digital window's transmit pane and
copied the lines by hand. N1MM sends a message for each step and fills the
lines in from a click. Both are here now, with N1MM's messages, N1MM's
defaults and N1MM's settings behind them.

The messages, all of them settings in the QTC window's setup area:

- RX Ready, `WAERXReadyText`, `{ENTERLF}I AM QRV BK`
- All Agn, `WAEAllAgnText`, `{ENTERLF}ALL AGN ALL AGN BK`
- Agn n, `WAEAGNText`, `{ENTERLF}AGN QTC: $ $ $ `, where `$` is the line
- Save, `WAESaveQTCText`, `{ENTERLF}R R R ALL OK BK`
- R U QRV, `WAERUQRVText`, `{ENTERLF}R U QRV BK`
- Save, reading traffic out, `WAESSaveQTCText`, `{ENTERLF}GL DE {MYCALL}`

Each keys the transmitter, sends the text and drops it again, and an empty
one sends nothing. The line spacing default is N1MM's `{ENTERLF}` rather
than `{ENTER}`, which was wrong.

The buttons: R U QRV and Snd Hdr were hidden while reading traffic out on
RTTY, though that is where N1MM starts — R U QRV is the first button
pressed. All Agn is new, and so is the S button on each line, which strikes
a line through a QTC that arrived too broken to read so the rows after it
keep their places; Agn n clears the strike and asks for that line again.

Clicking a line in the digital window fills the next row, which is how the
series is copied on RTTY: the other station sends the header and all ten
lines in one transmission. A click anywhere in a line hands the whole line
to the QTC window, which reads the time, the callsign and the serial number
out of it by shape and flashes the line to show it was taken. A line that is
not a QTC is left to mean what a click always means there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-10 20:03:18 +00:00
erikbrockoandClaude Opus 5 e0f307968e Mark a dupe in the contact type column, as N1MM does
N1MM writes a contact type on every contact it logs: `D` for a station
already worked, `N` for one the entry may not work at all, and empty for an
ordinary contact. Nothing here wrote the column, so a dupe counted as a
contact worth no points instead of dropping out of the score, and the two
places that read the column did nothing: the QTC pile offered dupes that
N1MM leaves out, and the statistics window could not leave dupes out either.

The marking is in one place, `ContestLog.ApplyVerdict`, and every contest is
marked the same way. What changes between contests is what counts as a dupe
and which stations are workable, and both come from the contest, as they do
in N1MM's `Contact.IsDupe`. No contest module in N1MM marks differently. Our
own callsign is not workable either, which is what N1MM adds on top of the
contest rule.

Two details follow N1MM as well: QTC traffic keeps an empty column, because
its `IsDupe` only marks a row while the program is logging contacts; and a
mark that came with the contact stands, because N1MM marks a contact when it
logs or edits one and never clears marks in a pass over the log. Editing a
contact clears the column so it is worked out again, which is what N1MM does
at the same point: an edit can move a contact to a band where it is no
longer a dupe.

A dupe is now out of the score altogether — contacts, points and multipliers
— which is N1MM's `ScoreClass.AddContact`. The QSO count still counts WAE
QTC rows as contacts; that is the other half of the scoring audit's item 14.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-10 19:38:57 +00:00
erikbrockoandClaude Opus 5 0d9b4a713e Export WAE QTC traffic to ADIF, and read it back
ADIF has no record for QTC traffic, which is why N1MM leaves it out and why
a WAE log cannot be moved between databases as ADIF. The plain export now
writes the contacts alone, as N1MM does: it used to write the QTC rows as
contacts, with the time of the traffic in RST_SENT and the reported callsign
in RST_RCVD.

File > Export > Export ADIF with QTC traffic writes the whole log instead.
A QTC row goes out with the station, time, band and mode, and the traffic in
application-defined fields — APP_NONEMM_QTC and friends, the prefix the ADIF
specification keeps for a program's own data — with the line in the comment
as well so the file can be read without this program. The report columns are
left off that record, because on a QTC row they hold the time and callsign
of the contact being reported. Reading such a file back rebuilds the rows,
so a WAE log survives the round trip whole. The item is only in the menu for
a contest with traffic.

Another program reading that file sees each QTC row as an ordinary contact
with the station the traffic was exchanged with and ignores the extra
fields. There is no way around that in ADIF, which is why the plain export
is still the one to hand over.

Contacts now also carry their row kind in APP_NONEMM_EXCHANGE1. Without it
a WAE log read back holds contacts with no kind at all, which N1MM's own
queries do not find.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-10 19:31:04 +00:00
erikbrockoandClaude Opus 5 c686a86dcc Log the contact when Ctrl+Z opens the QTC window, ESM or not
Ctrl+Z logged the contact by calling the Enter handler. With ESM switched
on, Enter sends the next message of the contact instead of logging it, so
the QTC window opened over an unlogged contact and the traffic went into the
log in front of the contact it belongs to.

It now calls LogContact, which is what N1MM calls at the same point. A
station already worked is logged again as a dupe, worth no points, which is
what the WAE rules expect: the second contact does not count but the traffic
does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-10 19:30:54 +00:00
erikbrockoandClaude Opus 5 9b3e10d127 Take a QTC header that is only a series number as ten lines
An operator copying a header off the air often types the series number and
nothing else. That read as a header the program could not parse, so the box
went yellow and Close refused to log the series.

N1MM fills the count in: "If he only types the batch number without the
forward slash and count, the program will assume the count is 10."

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-10 19:30:47 +00:00
erikbrockoandClaude Opus 5 265f90ccd7 Keep QTC traffic out of the serial numbers
A QTC row holds the serial of the contact it reports, which the station
being reported sent, not us. The serial pool was filled from every row in
the log, so reporting a station that sent us 1800 pushed our own next serial
to 1801, and reopening the log read it back from the rows again.

The pool now takes the sent number of contacts alone, when a contact is
logged, when one arrives from another station of the entry, and when the
numbers are seeded from the log at startup. N1MM leaves both QTC kinds out
of the same query, in QSONrsByBand.

The test opens a real log file, sends a series, closes the store and opens
it again, so the series numbering and the contacts already reported are
checked across a restart as well.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-10 19:30:32 +00:00
erikbrockoandClaude Opus 5 3e326053a0 Say how many QTCs are left with the station being worked
WAE gives a point for every QTC line, so an operator needs to know what is
still there to exchange. The entry window now carries N1MM's QTCinfo line in
WAE: how many QTC lines have already gone with the station in the callsign
box, how many more may go, and how many contacts are left to report to
anybody. The QTC window shows the same line.

The numbers come from the log rows, the way N1MM works them out:

- exchanged is every QTC row with that call, both directions and all bands,
  and ten is the lot
- available is every contact not reported to anybody yet, without the dupes
  and without the station itself, which is N1MM's QTCRecordsetCount
- what is left with the station is what is free of its ten, and no more than
  the contacts there are to report when we are the one reporting

Europe on CW and SSB only takes traffic down, so its line leaves the last
number out, as N1MM's does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-10 19:30:25 +00:00
erikbrockoandClaude Opus 5 00bbb0e6b3 Mark the picked line in the log only while the window is in front
A marked line in a window behind says nothing about where the keyboard
is. The log takes the marks off as another window is activated and puts
them back when it comes forward; the contacts stay picked in between, so
"Change Operator…" and Delete still act on what the operator picked.

The frame around the cell the keyboard is on goes with them. The theme
draws that frame with a rectangle inside the cell, not with the cell's
own border, so it takes a style on the template.

Quick edit moves the picked line: Ctrl+Q and Ctrl+A step through the log
from the entry window, and the contact in the entry boxes is the picked
line here and is scrolled to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-10 17:22:54 +00:00
erikbrockoandClaude Opus 5 606c43e4a6 Take half the room either side of the text in a log cell
The values sit closer to the line between the columns, and a column that
is as wide as its values is fifteen pixels narrower.

A header takes much more room besides its text than a cell does — its
own padding, and the room the theme keeps for the sort arrow — so the
two are now measured with an allowance each. One allowance for both
clipped "Mode" and "CQ-Zone" to the column width the values needed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-10 17:02:15 +00:00
erikbrockoandClaude Opus 5 836624376e Change the operator of the contacts picked in the log
A multi-operator station logs a whole shift under whoever was at the
radio, so a shift logged under the wrong callsign had to be put right a
contact at a time. "Change Operator…" in the log's right-click menu asks
for one callsign and writes it against every contact picked in the grid.

The dialog offers the operators the program already knows in a drop-down
and takes any other callsign typed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-10 16:56:04 +00:00
erikbrockoandClaude Opus 5 56c7bcef21 Fit more of the log on screen and edit a cell without fighting it
The theme gave each cell 32 pixels of height, which fits about seven
contacts in the window. A row is now as tall as its text and one pixel
either side, so about fifteen fit.

Editing a cell:

- the columns bind one way, and the value is written back when the edit
  is taken, on Enter or a click elsewhere. A two-way binding wrote on
  every key press, which took the edit as the first character was typed
  and closed the box.
- Enter on the current cell opens the box. The handler runs before the
  grid's own, which reads Enter as "move down a line".
- Left, Right, Home and End stay in the box. The grid read a key the box
  left alone as "go to the next cell", and the box leaves a key alone
  when the caret cannot move; End also landed at the start of the text.
- the box takes the height and the left inset of the text it covers, so
  nothing moves as it opens.

An edit no longer rebuilds the list. The lines on screen are given the
contact again, so the view stays where the operator put it instead of
jumping to the top and back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-10 16:55:59 +00:00
erikbrockoandClaude Opus 5 f2d9e331b4 Give each operator his windows back
The program stored an operator's window positions but never used them at
startup: the entry window was not placed and no window was reopened. It
now restores the whole screen both at startup and on a change of
operator: the windows that operator had open, where he had them, and the
ones he did not have closed.

The second radio's entry window no longer saves the layout. It has none
of the other windows open, so saving from it dropped every window the
first one had.

Ctrl+O gets six buttons for the operators last at the radio, newest
first. One click changes to that operator.

The recordings folder is no longer asked for. It is `Wav/<callsign>`
under the data folder, which is where N1MM plays them from, so
`StoredOperator.WavPath` is gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-10 13:58:12 +00:00
erikbrockoandClaude Opus 5 66e9a0ab65 Say what the serial number messages do
The README and the comments said the pool "goes out" and named it in
places where the reader cannot tell what is sent. They now name the
message and what it holds: the highest number taken on each band.

One of them was also wrong. The pool has not been sent on a reset since
the reset started counting from the log, and the README still said it was.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
2026-09-10 12:54:18 +00:00
erikbrockoandClaude Opus 5 980ddb586f Count from the log again on Reset Serial Numbers
The button started the numbers again at one. N1MM's manual says it "sets
serial numbers on all computers to the highest value in any of the logs,
plus 1", and its own handler re-reads the highest number per band out of
the log, so a mixed network came apart at exactly the point the button is
meant to repair: the N1MM stations counted on from the highest number and
this one started at one, sending numbers that had already gone out.

ContestSession.RestartNumbers fills the pool from the log, which is where
the reset now lands. SerialNumbersFrom is gone with it, and so is the pool
this station sent afterwards: N1MM sends none, because each station works
the same number out of its own copy of the log.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
2026-09-10 12:28:03 +00:00
erikbrockoandClaude Opus 5 7ba0b44fc1 Take serial numbers from a pool the whole entry shares
Each station worked out its next number as the highest in its log plus one,
so two operators typing at the same moment both sent the same number and
one of the two contacts was wrong in the sponsor's log.

SerialNumberPool holds the highest number taken per band, which is N1MM's
QSONumbers array. A number is taken at the first character of the callsign,
as N1MM takes it, and ReserveNr tells the other stations before the contact
is worked. A number at or below one this station has used is answered with
the whole pool, N1MM's QSONRS, and the station that is behind counts on from
there. The pool also goes out when a contest is opened and when the numbers
are started again.

Contest.NumbersByBand says whether the numbers run per band or as one
sequence for the station. CQ WPX runs one per band for a multi-two and a
multi-multi; every other contest here runs one sequence. The band index in
the message is N1MM's, so the array lines up with theirs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
2026-09-10 11:56:49 +00:00
erikbrockoandClaude Opus 5 33c7631e17 Let the operator pick, order and sort the log columns
The log showed the contest's columns and nothing else, with the country,
points and multiplier columns fixed in the XAML. LogColumns now holds every
field the log can show and the settings file keeps the names, left to right.

A right click on a header adds or removes a column and offers the contest's
own set back, a header can be dragged to move its column, and a click sorts
on it. Sorting uses the field, not the text of the cell, so 9,000 kHz comes
before 14,000 and the serial numbers do not run 1, 10, 2. Both grids get the
same columns, so a value in the pane below sits under the column it belongs
to. Removing the last column is refused, otherwise nothing could be selected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
2026-09-10 11:38:30 +00:00
erikbrockoandClaude Opus 5 d5788da240 Wipe on Alt+W as well as put the boxes back
Alt+W only put the boxes back, so wiping needed Ctrl+W or the button.
It now goes both ways, the way N1MM's Wipe button does: with something in
the boxes it clears them, and pressed again on empty boxes it puts back
what it cleared.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
2026-09-10 11:30:07 +00:00
erikbrockoandClaude Opus 5 24542b8355 Put the wiped entry boxes back on Alt+W
N1MM wipes on Ctrl+W and puts the boxes back on Alt+W, which this had no
key for. A wipe now keeps the call, the exchange, the name, the comment
and the sent time, and Alt+W fills them in again.

Wiping empty boxes keeps what the wipe before it held, and the boxes are
only filled in again when they are empty, so nothing typed since the wipe
is thrown away. A call moved onto the call stack is not kept: it has not
gone anywhere and putting it back would make a second copy of it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
2026-09-10 11:17:41 +00:00
erikbrockoandClaude Opus 5 ab9730b26d Check the entry boxes by the rules the log window uses
The entry window took any text and only the log window checked it, so the
same value was accepted in one place and refused in the other. The rules
now live in one file, ExchangeCheck, which both windows call.

OperatingPosition.EntryProblem names the first box that cannot be logged
and why. LogContact and SaveQuickEdit throw on one, so a caller that has
not checked cannot write nonsense to the log. The entry window checks
before it logs, puts the reason in the status line and the cursor in the
box to fix, which covers Enter, the Log It button, ESM and {LOG}.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
2026-09-10 11:13:38 +00:00
erikbrockoandClaude Opus 5 95e1405be1 Check the signal report before it goes in the log
The log window took any text in the two report columns. It now checks the
report the same way it checks the zone or the section: RST on most modes,
and a signal-to-noise ratio in decibels on FT8, FT4, JT65, Q65 and MSK144.
A WAE QTC row is skipped, because it keeps the time sent and the other
station's call in those two columns.

The grid column is checked on every contest now, not only when the contest
asks for a locator, and the value is stored as JN88dd.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
2026-09-10 11:07:09 +00:00
erikbrockoandClaude Opus 5 7ca2c5cd52 Walk the QTC window the way N1MM walks it
Four changes to the QTC window, three of them N1MM's behaviour and one an
addition.

Tab, enter and space all move to the next box the operator types in, and shift
and tab move back. The walk covers those boxes and nothing else. N1MM handles
the three keys in every box itself — `QTCLinesA_KeyDown` and its two siblings,
with `PreviewKeyDown` claiming Tab — rather than leaving them to the tab order,
so the cursor never lands on a switched-off line or on a button beside one.
Tab was going to Avalonia's default walk, which covered every control in the
window.

The header now says how many lines take traffic. `QTCHeader_LostFocus` reads
the count after the last `/` and hands it to `ColorizeCWRXFields`, which enables
that many lines and switches off the rest, capped by how many of the station's
ten are still free. All ten were live whatever the header said. Nothing already
typed is cleared, which N1MM does under `DontErase`.

A time typed short is filled in from the line above as the cursor leaves the
box: `52` under `1148` reads as `1152`. `QTCLinesA_LostFocus` takes as many
characters off the front of the line above as the typed part is missing, and
drops a colon whatever the length. `QtcTraffic.FillTime` is that rule.

The addition: while the cursor is in a QTC callsign box the check window
answers for that call rather than the entry window's, so a call taken down in
traffic is checked the same way one being worked is. `CheckWindowSources`
takes the call from `Elsewhere` when the QTC window has set it.

Checked by driving the window: header `4/5` left five lines live and read
"1 of 5", tab walked header to time to call to serial to the next line's time,
shift and tab walked back, `52` on the second line became `1152`, and the check
window listed W1AW while `W1A` was typed in a QTC line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
2026-09-09 21:17:03 +00:00
erikbrockoandClaude Opus 5 d4d0375f1f Get WAE ready: the score split, the Cabrillo layouts and the QTC list
The score summary now splits the way N1MM's does. `ContestLog` keeps a tally
per band, and per kind of row where the contest has kinds, and the score window
reads those instead of adding the rows up itself. Three things were wrong with
the old sum:

- The band rows counted a multiplier as one whatever the band. WAE weights it
  by band — `ScoreRow.AddContact` adds four on 3.5, three on 7 and two on 14,
  21 and 28 — so the band rows did not add up to the total.
- The band rows counted contacts left out of the claimed score.
- There was no split by kind. `CWAE.LogInfo` and `ScoreClass.Description` give
  WAE a `Q/QTC` column and a row per band and kind, so contacts, traffic sent
  and traffic received are read apart.

The Cabrillo writer now pads the record name to seven characters, which is what
`ConvertCabrilloStructStdToString` does and why: `X-QSO:` is six characters and
a space, so every record name puts the frequency in the same column. Nonemm
wrote five, and the `X-QSO:` lines added last commit did not line up with the
rest.

WAE's own layouts follow `CWAE.CabrilloString`: three spaces after the sent
serial rather than the one every other column gets, and the transmitter column
only for a multi-operator entry.

`QtcTraffic.ToSend` no longer offers a contact marked as a dupe, which is what
`DBSupport.QTCRecordsetCount` leaves out with `Trim(ContactType) = ''`.

Checked by running the program against a WAE SSB log of six contacts and a
series of ten received: the band rows read 4, 3, 2 and 2 and add up to the 11
in the total row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
2026-09-09 20:25:45 +00:00
erikbrockoandClaude Opus 5 de8edafa0c Add the scoring review against om5m.s3db
The review of 6e24e00 against the decompiled reference and a replay of the
56,284 rows of om5m.s3db, with the per-instance comparison in the CSV.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
2026-09-09 20:12:51 +00:00
erikbrockoandClaude Opus 5 a15bc8a6a2 Keep an excluded contact out of the score the way N1MM does
Three things were wrong with a contact the operator takes out of the claimed
score. The decompiled reference gives the rule in four places:

- `Contest.cs:437` returns zero points when `IsClaimedQso` is 0.
- `ContestInstance.cs:307` builds the where clause `CLAIMEDQSO = 1`, which the
  dupe search (`DBSupport.cs:1189`) and the multiplier search
  (`DBSupport.cs:1327`) both use, so an unclaimed contact takes nothing away
  from a later one.
- The score query (`DBSupport.cs:1807`) adds `ContactType <> 'D'`, which the
  other two do not. A contact marked as a dupe is therefore still searched for
  dupes and multipliers, and is left out of the score alone.
- `Contact.cs:1563`, `3307` and `3881` write `X-QSO:` rather than dropping the
  contact from the Cabrillo file.

`ContestLog` scored an unclaimed contact in full, and the Cabrillo writer left
it out of the file. `Qso.IsInScore` now holds the score rule and
`StatisticsPivot` uses it instead of its own copy.

The same reference also fixes the sent serial in three Cabrillo exporters.
ARRL 10M, Ukrainian DX and YO DX HF formatted the sent exchange from the
station rather than the contact, so a DX entrant exported `001` on every line.
N1MM reads the contact's own number: `CArrl10m.cs:122` writes it unpadded,
`CUkrainian.cs:232` and `CYOHFDX.cs:167` pad it to three digits.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
2026-09-09 20:12:51 +00:00
erikbrockoandClaude Opus 5 bae40bb04e Show the boxes N1MM shows for each contest
The entry window had a fixed set of boxes with a Name and Comment box beside
the exchange. N1MM lays out one row of boxes per contest and nothing else, so
this does the same: general logging keeps Name and Comment, and a contest gets
the boxes it exchanges.

Two new members on `Contest` carry the cases where the boxes and the log
columns are not the same list:

- `FromExchange1` spreads what was typed in one box across the log columns.
  Where the other station sends a serial or a district code depending on where
  it is, N1MM gives it one box and sorts the value into the number, section or
  zone column from there, so the score and the Cabrillo file read it from the
  same column N1MM does.
- `LogFieldsFor` gives the log window its own column list. Sweepstakes takes
  the whole exchange in one box and logs it in four columns; `SweepstakesExchange`
  parses `72 A 74 SCV` in any order, reading from the last word back as N1MM
  does.

The sent serial is now marked as not required and space steps over it along
with the two report boxes, because nobody types the number going out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
2026-09-09 20:12:37 +00:00
erikbrockoandClaude Opus 5 6e24e00ec8 Copy the statistics table and graph to the clipboard
Copy All takes whichever tab is open, as N1MM's does: the table off the table
tab, a picture of the graph off the graph tab. Copy Table as Text and Copy
Graph as Image do either one from either tab.

`StatisticsTable` lays the pivot out as the window prints it — the headings,
the rows, the Total line — and pads it into columns for the text form, so the
grid, the clipboard and Print to File all say the same thing.

X11 asks for a picture when the other program pastes rather than when it is
copied, so the bitmap given to the clipboard is held until the clipboard is
given a different one. Disposing it at the end of the copy killed the program
on the next paste; the table pasted into a second Avalonia program reads back
lined up, and the picture reads back as a 916x438 bitmap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
2026-09-04 07:59:22 +00:00
erikbrockoandClaude Opus 5 acd22a9b5e Count the log under any two of its fields
N1MM's statistics window. `StatisticsPivot` counts the contacts under one row
field and one column field, with the row total, the running total and the
column totals N1MM prints. The fields are N1MM's own two lists: 29 down the
rows, 20 of them across the columns, sorted the way N1MM's SQL sorts them, so
zone 10 comes after zone 2 and 80M before 20M.

The window has N1MM's two tabs. The table is the grid with `Tot` and `Accum`
and a `Total` line; the graph is a stacked column per row, one segment per
column, in one of N1MM's twelve palettes. "Include Dupes & X-QSOs" counts the
contacts left out of the claimed score and the ones marked as dupes.

Checked against a 3775-contact CQ WW SSB log: 3762 counted, which is what
`select count(*) ... where CLAIMEDQSO = 1 and ContactType <> 'D'` returns, and
the per-band totals match band by band.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
2026-09-04 07:47:30 +00:00
erikbrockoandClaude Opus 5 4eb8071d57 Say when the entry has two signals on the air
N1MM blocks the transmitter when a multi-operator entry is about to break
its own class. This program does not touch the transmitter: a logger that
decides for itself when the operator may not send loses a contact every
time it is wrong, and it is wrong whenever it has stale information about
the other stations — which, on a network, is whenever a laptop has just
been unplugged.

So the entry window says so instead. While this station is transmitting
and the entry is over what it is allowed, a red box goes across the top of
the window naming the station and the band. It is above the frequency and
under the menu, so nothing it covers is anything the operator needs while
he is sending, and it is big enough to read from the other side of the
room.

`TransmitRules` is what it asks:

| Entry | Allowed |
|---|---|
| one transmitter | one signal |
| two transmitters | two signals, on different bands |
| anything else | one signal per band |

Two on one band is wrong in every multi-operator class, so it is checked
whatever the category says. A station that has not said which band it is on
counts as somewhere else: a guess there puts the box in front of an
operator doing nothing wrong.

This is also the first thing that sends `XMIT`. The message was written and
nothing called it, so the transmit column of everybody's network status
window stayed empty and no station knew when another was sending. The entry
window now says when its key goes down and what it is sending.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
2026-09-03 23:23:46 +00:00
erikbrockoandClaude Opus 5 0b92bfc6ee Fill in the rest of N1MM's network status window
Seven tabs now, in N1MM's order, and the messages behind them.

**Options** holds what there is something behind: Log QSOs at all Stations,
which decides whether the contacts the others log are kept here; the
message trace, moved off the Messages tab where N1MM does not keep it; and
Run1/Run2, which used to be guessed from the radio number and is now the
operator's choice, sent in every `STATUS` and every `XMIT`. The transmit
interlocks stay out: this program does not block a transmitter, and a
station that claimed it did would be waited on for nothing.

**Call Stack Targets.** A callsign heard at one station and handed to the
position that will work it — `STACKANOTHERCALL` with the call alone,
`STACKCALL` with the contact behind it. The target is picked from the tab
or from the station list, and what arrives goes on the call stack of the
radio the operator is on, sorted by whether it is a multiplier here.

**The master** is the station the entry follows for cluster spots and the
time. `MASTER` names it to everybody, so the column agrees everywhere.

**The clock.** `TIME` carries a station's clock and Force Time Sync sends
it. N1MM sets the clock from it when it is running as an administrator and
says how far out it is when it is not; this program always does the second,
because setting the clock needs root and a contest logger has no business
asking for it. Anything over two seconds is called out.

**Serial numbers.** Reset Serial Numbers at all Stations sends
`RESETQSONRS` and every station starts again at one. The contacts already
logged keep the numbers they were sent with.

**What the entry agrees on** is now checked rather than assumed:

| What | How |
|---|---|
| the contest | `CONTESTNAME` as soon as a connection opens, and again in every `CHECKSUM` |
| the country file | its issue date — the publisher's `=VER20260826` — rides in `STATUS` |
| the computer name | longer than N1MM's fifteen characters is named at startup; a second computer answering to this one's name is named when its beacon arrives |
| the station number | two stations on the same number are named when the second says so |

`CountryFile` reads its issue date, which nothing had asked it for before.

Left for later: the master relaying its cluster spots, and the shared
serial-number pool where stations claim numbers in advance.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
2026-09-03 23:15:44 +00:00
erikbrockoandClaude Opus 5 b5d86c1677 Send the messages N1MM actually reads
Three of the messages this program sends were its own invention or in the
wrong units, so an N1MM beside it read them wrongly or not at all.

| Message | N1MM | What was sent |
|---|---|---|
| `XMIT` | transmitting, the function key's caption, Run2, F1 pressed, stop sending | transmitting, the radio number |
| `PASSFREQ` | the frequency in hundredths of a kilohertz, and nothing else | the frequency in kilohertz, and a callsign |
| `FREQ` | the frequency in hundredths of a kilohertz | `BANDMAP`, which N1MM has no such message for |

So N1MM showed a radio number where the other operators read what a
station is sending, read a pass on 14205.5 as 140.25, and never saw this
station move at all. `BANDMAP` is gone: the frequency goes out in `FREQ`
when the radio moves and the mode, run and the rest ride in the `STATUS`
that follows it, which is where N1MM carries them.

The `Msg` column now shows what a station is sending, which is what the
caption is for. `PassCall` and `RadioNumber` are gone from the station:
neither is on the wire, so neither was ever anything but this program
talking to itself.

**A station that reconnects resynchronises by itself.** N1MM sends
`LASTQAT` — when I last worked anybody — as soon as a connection opens,
and a station holding nothing that late asks for the hours in between. An
empty log asks for nothing: a program that has just been started has no
business pulling the whole contest across, and N1MM suppresses it the same
way. The answer goes over the connection this station opens, which is not
always up when the message arrives, so a resync that has nothing to ask
over waits for it rather than being lost — and one that still cannot send
stops and says so instead of leaving the row resynchronising for the rest
of the contest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
2026-09-03 22:54:16 +00:00
erikbrockoandClaude Opus 5 2a21c772ad Resynchronize a log with the other stations, and rescore it
A station that was away — a laptop that lost the network, one that was
switched off — comes back missing whatever the others logged while it was
gone. N1MM fills that in by walking the log an hour at a time, and this is
that walk, on the same messages:

| Message | What it says |
|---|---|
| `CHECKSUM` | here is what I hold for your contacts in this hour, and the contest and callsign I am logging |
| `DELETEQS` | drop your copies of my contacts in that hour, I am sending them again |
| `RESYNCQSO` | one of those contacts |
| `CONFIRMED` | here is what the hour hashes to at my end — which also asks for the next hour |

Each station is the authority on the contacts it made, so an hour that
disagrees is replaced rather than merged. The answer to one block asks for
the next, and the walk ends when the block starts after the end of the
period. The Resynchronize tab counts the hours that agreed, the hours that
were sent again and the hours that still disagreed, which are N1MM's
columns.

**`LogChecksum`** is N1MM's: a CRC32 over the `DXLOG` columns of the block,
ordered by time and then callsign, leaving out what each station works out
for itself — points, the three multiplier flags, the country prefix, the
continent, the WPX prefix and `IsOriginal`. Two stations may disagree about
those and still hold the same log. A checksum that disagrees for any other
reason costs one block of traffic, which is what a resync is for.

**The link still holds no log.** It asks one through `ContactLog` for the
contacts of a computer in a block of time, and `SessionContactLog` answers
on the window thread, because that is where the log is touched.

**Every contact logged here now carries the computer that made it.** N1MM
stamps `NetBiosName` and `NetworkedCompNr` on every row and this program
did not, so its own contacts belonged to no station and a resync could not
ask for them.

**Rescore** writes the points and multiplier flags of a period back to the
database. The log in memory scores itself whenever it changes; the stored
rows do not, and N1MM reads them rather than working them out. A contact
that arrives from another station after the one that took a multiplier
leaves those columns behind. A station can also ask the others to rescore
when its resync finishes, which is the flag on the end of `CHECKSUM`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
2026-09-03 21:52:09 +00:00
erikbrockoandClaude Opus 5 68b29d722a Give the network status window N1MM's tabs and columns
The window was one flat table of columns this program made up. N1MM's has
tabs and a fixed set of columns, and an operator who knows one has to be
able to read the other.

**The window** now has Stations, Actions and Messages. Stations is a grid
with N1MM's headers in N1MM's order — Computer, IP Address, Pass, Run,
10, 100, Freq, Op, Msg, Send, Receive, Band, R/M, Mode — and a row per
station, this computer included. Right-click a row to pass the contact
being worked to that station, to talk, to show the connection log, or to
close and open its connection. Actions holds the talk line, the pass
frequency, the echo and the networking switch; Messages holds the chat,
the connection failures, the echo round trips and a trace of every
message that arrives.

**`STATUS`** is what fills those columns. N1MM sends it every few seconds
whether anything has changed or not — the pass frequency, the frequency,
run or search and pounce, the operator, the rates over the last ten and
the last hundred contacts, the mode, the categories and the version — so
a station sitting on one frequency for an hour still has a current row.
`BANDMAP` only says a station moved. The five transmit-interlock flags
are sent as false: this program does not block a transmitter.

**`Rates.PerHour`** reads the two rate columns out of the log. Count back
over the last N contacts, stop at a gap longer than the contest's minimum
off time, and treat a span under a minute as a minute, so three contacts
in five seconds read as 180 an hour rather than 2160.

A connection closed by hand stays closed. Without that flag the next
beacon opens it again within ten seconds, which makes the menu item look
broken.

Left out, because there is nothing behind them: N1MM's Resynchronize and
Rescore tabs, the transmit interlocks of its Options tab, the Master
column and the call-stacking target tab.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
2026-09-03 21:39:42 +00:00
erikbrockoandClaude Opus 5 6f2f39768c Show the other computers, and apply the ten-minute rule
Three pieces on top of the link:

**The network status window**, on the entry window's Window menu. A row
per station, this computer included the way N1MM shows it: number,
address, operator, band, mode, run, transmit, pass frequency, the last
message type, how long ago it arrived, the counts each way and the echo
round trip. A station that broadcast the wrong version is named under the
table in red — that is the one fault where everything looks connected
and nothing arrives. The box at the bottom sends a line of chat, and
Echo asks every station whether it is there.

**Where this station is** goes out once a second when it has changed.
The frequency, the mode and run have a dozen places they can change from
— the radio moving, a band button, a QSY typed into the callsign box —
so it is read and compared rather than announced from each of them.

**The ten-minute rule.** `BandChangeRules` already counted changes and
the stay on a band, and the entry window already showed the countdown,
but only a user-defined contest carried a rule, so every built-in
contest allowed anything. `ForCategory` gives a multi-operator entry
with one or two transmitters a ten-minute stay, which is N1MM's fallback
in `ContestInstance.BandChangeTimerDuration` for every contest that does
not name its own. Its per-contest table is a few hundred cases in a
decompiled hash switch and is not repeated.

Config ▸ Edit Networked-Computer Names now covers both networks: the
12060 broadcast to other programs and the 12070 link, with the station
number, the port, the version to claim and stations named by address for
a network where a broadcast does not reach.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
2026-09-03 15:26:42 +00:00
erikbrockoandClaude Opus 5 c0e1bc1e1b Talk to the other logging computers on N1MM's own port
N1MM has two networks and this program had only one of them. Port 12060
carries XML to other programs — a spot tool, a score poster — and
`StationNetwork` already writes that. Computer to computer, N1MM uses
port 12070 and a different protocol, which is what this adds.

The wire format, from `MultiOpManager` and `MultiStation`:

    DATA__07%SHACK-PC%QSO%2026-09-03 12:34:56%DL1ABC%…~__DATA

the sending station's number, its computer name, the message type and
the fields of that type. `%` and `~` cannot appear in a field, so N1MM
writes `!` in their place. `StationRecord` reads and writes that, and
holds what has arrived until a `~` says a message is whole: TCP hands
over half a message as often as two.

Discovery is a UDP broadcast to the same port, six fields wide.
**N1MM refuses a station whose version is not its own** — it says
"Software versions must match" and drops it — so the version this
program broadcasts is a setting rather than its own version, and a
station that says something else is kept in the list with `Refused` set
so the operator can see why nothing is arriving.

`StationLink` runs both sockets, opens a connection to every station it
hears, and reports what arrives. `QsoRecord` is the thirty-five fields
of a contact in N1MM's order, from `MultiOpManager.cs:1007`; points and
multiplier flags are read but the log works them out again from the
rules, so two stations cannot disagree about a score. `AddStation` names
a station by hand, for a network where a broadcast does not reach.

A station can also be named rather than heard, and one that is connected
may say nothing for a while, so `NetworkedStation` is heard-from as soon
as it exists. Left at nothing, the sweep that drops quiet stations read
every new station as quiet since the beginning of time and closed the
connection the moment it opened.

Tested as bytes and over loopback. Nothing has talked to a real N1MM.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
2026-09-03 15:19:03 +00:00
erikbrockoandClaude Opus 5 9f635550bd Read a QTC series out on RTTY
N1MM's Send All: the whole series as one message — the heading, every
line with the operator's spacing between them, then the ending, with
`{TX}` and `{RX}` around it so it keys the transmitter and drops it
again. The shape is N1MM's, from `QTCWindow.cs:3609`. A line is the three
fields joined with hyphens, which is how N1MM writes them, and Snd n
sends one line again for a station that missed it.

One message rather than a button per line because a series is close to a
minute of transmission at 45 baud: the type-ahead buffer holds it and
paces the engine.

Three settings with N1MM's names and defaults — `WAESendAllHeadingText`,
`WAESendAllEndingText` and `WAESQTCSpacing` — on the QTC window's setup
dialog. `{ENTERLF}` is new to the expander and stands for a carriage
return and a line feed; `{QTC}` is filled in by the window, which is the
only place that knows which series is going out.

The messages are tested as text. Nothing has gone on the air.

Left out: N1MM's four SSB recordings, which need a voice keyer, and its
RTTY messages for the station taking traffic down, which are typed into
the digital window's transmit pane here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
2026-09-03 15:02:08 +00:00
erikbrockoandClaude Opus 5 0fc8d7e405 Write down what starts the transmit pane again
Two things do, not one: the engine reporting the drop, and a new message
being keyed. The second was missing from the text, which is the fault the
last commit fixed.

The claim that the bridge does not forward MMTTY's window message 32772
is dropped. `docs/digital-bridge.md` argues the control cannot raise
`OnTranslateMessage` for a message it has an event for, `OnPttEvent` does
report drops here, and the two files disagreed. Says what is known and
what is not instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
2026-09-03 14:46:37 +00:00
erikbrockoandClaude Opus 5 f49a8c10fd Start the transmit pane again on every new message
The macro buttons went wrong at the end of a transmission, both faults in
DigitalEngineSender:

- The engine reports the transmitter drop on its own thread.
  WhenTransmitChanged took the state lock, decided the message had ended,
  released the lock, and only then called Buffer.Ended(), which clears
  what has gone to the engine. A macro pressed on the last character got
  through StartAsync in that gap and had already flushed its own text into
  Sent, so Ended() wiped the new text off the pane while the engine
  transmitted it. Ended() is now called inside the same lock.

- The pane only started again when the engine reported a drop. Two macros
  in a row keep the transmitter up, so that report never came and Sent
  grew with every press. Everything in Sent is locked, because it is in
  the engine and cannot be taken back, so the whole pane became
  read-only. TypeAhead.Started() drops the last message's sent text and
  keeps what was typed ahead, and StartAsync calls it whenever it keys a
  new transmission.

The rest of this commit is the digital transmit work these fixes sit on:
the pane as one coloured box, the sender's three keying states, the
type-ahead feeder paced by the clock with the engine's count as a brake,
{RX} flushing what is left in one piece, and the entry window's function
keys reading the digital macros on a digital mode.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
2026-09-03 14:40:30 +00:00
erikbrockoandClaude Opus 5 7ae60be4a0 Write down what the count lag and the symbol count measure
The third probe run puts numbers on both. TxBufLen lags by 100 to 150 ms, about
one character at 45.45 baud: twenty-one characters pushed at 2496 ms read 0 at
2497, 2547 and 2597, then 26 at 2647. It moves once per symbol transmitted, so
readings 50 ms apart repeat.

It read 26 for 21 characters because it counts Baudot symbols, and the message
carried two digits: a shift to figures and a shift back each time, plus one at
the start. That is the case the cap was needed for and I had not seen. A contest
exchange is mostly digits, so the engine always has more to transmit than the
clock thinks, and the clock alone would run ahead of it on every QSO.

Slack of three characters covers both, so nothing changes in the pump. The
count reaching 0 does not cut the end of a message off either: it emptied 350 ms
before the last character was decoded back and the engine held the transmitter
up for another 900 ms.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RtspmWmS7f8kUvcyaHpRWZ
2026-09-01 23:20:11 +00:00
erikbrockoandClaude Opus 5 5370ad3f6d Do not trust the engine's count as the pace
The second probe run read TxBufLen as 0 one millisecond and again fifty
milliseconds after twenty-one characters had been pushed, while those characters
were already going out: the first of them was decoded back off the air 420 ms
later. The number is right when it is a second old and wrong when it is fresh,
so it runs behind the engine.

A pump that fed on it would have handed over the whole message in half a second
and put all of it beyond reach, which is worse than the clock it replaced. So
the clock is the pace again, and the count is a check on it: the engine is never
given more than Lead + Slack characters however fast the clock says to feed, and
a message ends when the count and the clock estimate both say it has. Characters
fed since the last answer are added to it, so a stale answer cannot be spent
twice.

The engine is set to Character out: ABCD with no space after it went out at
once. Word out would have held it, so the holding case stays, but it now needs
the count to be neither going down nor being added to, since an engine kept
exactly at the cap has a count that does not move either.

The probe writes down every reading for two seconds after a push and does not
believe an empty one in that window, which is what the run needed to measure the
lag and did not do.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RtspmWmS7f8kUvcyaHpRWZ
2026-09-01 23:13:28 +00:00
erikbrockoandClaude Opus 5 4118cd6d33 Pace the pump on what MMTTY says it has left
The engine probe answered two questions on the air, and they point opposite
ways.

TxBufLen is the number of characters left to transmit. Seventeen characters
pushed, five decoded back, and it read 12, counting down to 0 as the message
went out. So the pump no longer counts character times off the clock: it asks
the engine, feeds while the answer is under Lead, and asks again. The baud rate
is now only the fallback for an engine that will not answer.

A backspace is not an edit. Pushed in as a character it made the count go up by
four and the text went out unchanged, so text the engine has been given cannot
be taken back, only aborted. EngineTypeAhead was built on the opposite belief
and is gone, along with the setting that chose it and the interface that existed
to switch between the two.

A count that stops going down means the engine is holding what it has: MMTTY set
to Word out keeps a word until the space after it. The pump feeds one character
per look while that lasts, so the space arrives and the word goes out rather
than the message sitting there.

The probe kept two bugs of its own that this run showed: it read the count a
millisecond after pushing, saw 0 and called the message finished, and it started
a step while the previous unkey was still in flight, so that step ran with the
engine down. It now needs two empty answers in a row and waits for the engine to
say it has stopped. It also asks what {RX} does to a word the engine is holding,
which decides whether a macro without a trailing space loses its last word.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RtspmWmS7f8kUvcyaHpRWZ
2026-09-01 23:02:00 +00:00