Contest operating goes outside the band edges. Someone answers a CQ at 14352, or a radio reports a frequency a few hundred hertz off, and Bands.ForFrequency returned null: the contact scored nothing, missed the per-band dupe check and exported with no band. N1MM buckets a frequency by its whole megahertz — its BandFor divides the kilohertz by 1000 and switches on the result, with 1 and 2 both 160M, 3 and 4 both 80M, 28 and 29 both 10M — and never looks at a band edge. Same rule here, except that the real edges are checked first: 2190M and 630M are both under 1 MHz and share the bucket, so nothing else can tell them apart. A frequency under 1 MHz that misses both edge ranges still has no band, as it does in N1MM. The bandmap clamped its slice to the band edges, so a radio past the edge left the receiver bar off the top of the scale. The slice now stretches to wherever the radio is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
314 lines
15 KiB
Markdown
314 lines
15 KiB
Markdown
# Nonemm
|
|
|
|
A contest logger for amateur radio: a from-scratch reimplementation of
|
|
[N1MM Logger+](https://n1mmwp.hamdocs.com/) in C#, running on Linux and Windows.
|
|
|
|
The contest rules, log formats and database schema are written from their
|
|
published definitions. What Nonemm keeps is **interoperability**: the log
|
|
database is N1MM's `.s3db` in N1MM's schema, user-defined contests are N1MM's
|
|
`.udc` files, and a log either program writes opens in the other.
|
|
|
|
## Running it
|
|
|
|
The SDK lives at `~/.dotnet` on this machine, so a shell that has not been set
|
|
up needs:
|
|
|
|
```sh
|
|
export DOTNET_ROOT=$HOME/.dotnet PATH="$HOME/.dotnet:$PATH"
|
|
```
|
|
|
|
Then:
|
|
|
|
```sh
|
|
dotnet run --project src/Nonemm.App # start the logger
|
|
dotnet test Nonemm.slnx # run every test
|
|
```
|
|
|
|
`./build.sh` does the same with the environment already set: `./build.sh test
|
|
Nonemm.slnx`.
|
|
|
|
To work a contest: **Config → Station** for your callsign and zones, then
|
|
**File → New Database** and **File → New Contest**. In the entry window, type a
|
|
callsign and press **space** to move to the exchange, then **Enter** to log.
|
|
Type a frequency into the callsign box and press Enter to change band. **View**
|
|
opens the log, check, bandmap, score and packet windows.
|
|
|
|
Everything that judges a station — the bar under the callsign box, a row in the
|
|
check window, a spot on the bandmap, a line in the log — is coloured by the same
|
|
scorer: red for a dupe, green for a new multiplier, blue for points.
|
|
|
|
## What it does
|
|
|
|
| | |
|
|
|---|---|
|
|
| Contests | CQ WW, CQ WPX, ARRL DX, IARU HF, Sweepstakes, RTTY Roundup, NAQP, general logging, and user-defined `.udc` contests |
|
|
| Log | N1MM `.s3db`, Cabrillo 3.0 out, ADIF in and out |
|
|
| While typing | dupe check, multiplier check, points, country and zone from the country file, exchange filled from a call history file |
|
|
| Windows | entry, log, check, bandmap, available mults and Qs, score summary, packet |
|
|
| Editing | double-click a cell in the log, or open the whole contact with Ctrl+Y; Delete removes it. All of it goes out to the other stations |
|
|
| Radio | one or two radios over hamlib `rigctld`, split, reconnecting on its own |
|
|
| Cluster | DX cluster over telnet, spots feeding the bandmap, Alt+P to spot a station |
|
|
| Bandmap | drawn like N1MM's: a frequency scale with the receiver on it and callsigns beside it, joined by leader lines |
|
|
| Network | contacts shared with the other stations of a multi-operator entry, in N1MM's own contact message |
|
|
| Keying | CW through `cwdaemon` or a WinKeyer, with N1MM's message macros |
|
|
|
|
### The country file and the callsign database
|
|
|
|
Neither is bundled. **Config → Download Country File** and **Download Check
|
|
Partial File** fetch them from where they are published, which is where N1MM
|
|
fetches them from too —
|
|
[`country-files.com/cty/wl_cty.dat`](https://www.country-files.com/cty/wl_cty.dat)
|
|
and
|
|
[`supercheckpartial.com/MASTER.SCP`](https://www.supercheckpartial.com/MASTER.SCP).
|
|
The country file is `wl_cty.dat` rather than plain `cty.dat`: same format, with
|
|
the WAE entities listed separately, which is what CQ WW counts.
|
|
|
|
A download that fails changes nothing. The file is fetched, checked that it
|
|
parses as what it claims to be, and only then put in place, so a site that
|
|
answers with an apology page instead of a country file cannot cost an operator
|
|
their multipliers mid-contest.
|
|
|
|
Both files can also be dropped into `SupportFiles` under the configuration
|
|
directory (`~/.config/nonemm` on Linux, `Documents\Nonemm` on Windows).
|
|
User-defined contests go in `UserDefinedContests` under the same directory.
|
|
|
|
Without a country file the program still runs; country- and continent-scored
|
|
contests lose accuracy.
|
|
|
|
### Radio control
|
|
|
|
The logger reads and tunes the radio through
|
|
[hamlib](https://hamlib.github.io/)'s `rigctld`, started separately for whichever
|
|
radio is on the desk:
|
|
|
|
```sh
|
|
rigctld -m 2028 -r /dev/ttyUSB0 # -m is the hamlib model number; rigctl -l lists them
|
|
```
|
|
|
|
Then **Config → Radio**. With no radio connected nothing changes: frequency and
|
|
mode stay where they were last typed.
|
|
|
|
### CW
|
|
|
|
**Config → Keyer and messages** picks `cwdaemon` (a UDP port, usually 6789) or a
|
|
WinKeyer (a serial port), sets the speed, and edits the twelve function key
|
|
messages for CW and for phone. The macros are N1MM's: `{MYCALL}`, `{CALL}`,
|
|
`{EXCH}`, `{SENTRST}`, `{SENTNR}`, `#` for the serial number, and `{SENTRSTCUT}`
|
|
for cut numbers. Escape stops sending.
|
|
|
|
### Editing the log
|
|
|
|
Double-click a cell in the log window to change it. The columns follow the
|
|
contest exchange, so CQ WW shows a Zone column and Sweepstakes shows Nr, Prec,
|
|
Ck and Sec. A value the contest does not accept — zone 41, an ARRL section that
|
|
does not exist, a callsign with a space in it — is refused and the reason
|
|
appears under the log. Changing the callsign looks the country up again.
|
|
|
|
Delete, or the right-click menu, removes the selected contact after a
|
|
confirmation. Either way the whole log is scored again, so a multiplier the
|
|
removed contact was holding passes to the next contact that claims it.
|
|
|
|
**Edit → Edit Last Contact**, Ctrl+Y, or Enter on a row in the log opens the
|
|
whole contact in one dialog. That is where the fields with no log column live:
|
|
QSX frequency, name, QTH, comment, grid, power, radio number, run position, and
|
|
the country and WPX prefixes. ◀ and ▶ walk the log without closing, offering to
|
|
save first. Points and the multiplier flags are shown but cannot be typed in —
|
|
they are worked out from the rules every time the log changes, so anything typed
|
|
there would be overwritten on the next edit.
|
|
|
|
Correcting the country prefix by hand changes the score. The country file is a
|
|
best guess for calls it has no rule for, so what the contact says now wins over
|
|
what the file says.
|
|
|
|
### Call history files
|
|
|
|
**Config → Call History File…** points at one of the files published before a
|
|
contest. It is N1MM's format, so a file written for N1MM is read as it is:
|
|
`#` comments, `!!Order!!` naming the columns of the lines after it, semicolons
|
|
in preference to commas, and the directives that change what is stored —
|
|
`!!FourCharGridSq!!`, `!!NoLoc2AltGrid!!`, `!!MapStateToSect!!`,
|
|
`!!AppendUserText!!` and `!!NoAppendUserText!!`.
|
|
|
|
Leaving the callsign box fills what the file knows into the exchange boxes that
|
|
are still empty. A box with something in it is left alone: what the other
|
|
station actually sends beats what somebody published months ago. The check
|
|
window gets a History column of matching calls.
|
|
|
|
The section-validating directives — `!!Validate50State!!`,
|
|
`!!ValidateArrlSection!!`, `!!MapOnSection!!`, `!!GTA2GH_NT2TER!!` — are read
|
|
and passed over rather than acted on. Throwing data away on a section list this
|
|
program does not hold would be worse than keeping it.
|
|
|
|
### The check window
|
|
|
|
Five columns, kept apart because they answer different questions: **Log** is
|
|
what this station copied itself, **Master** is a guess about the whole world,
|
|
**History** is what was published before the contest, **Bandmap** is somebody
|
|
else's claim, and **Exchange** offers the values the exchange box with the
|
|
cursor in it can hold — the ARRL sections, or the states and provinces. A
|
|
serial number or a report has no list behind it, so that column stays empty
|
|
rather than guessing.
|
|
|
|
### Available mults and Qs
|
|
|
|
**View → Available Mults and Qs** lists what is spotted and not worked yet, a
|
|
column per band, multipliers first and then the stations that are only points.
|
|
Clicking one puts the radio there with the call in the entry window, the same as
|
|
clicking a spot on the bandmap. **Mults only** hides everything that brings no
|
|
multiplier.
|
|
|
|
A spot carries no exchange, so a station is only known to be a multiplier where
|
|
the multiplier follows from the callsign — a DXCC entity — or where the call
|
|
history file says what the station sends. That is why CQ WW shows the country
|
|
but not the zone: the zone that counts is the one the station sends, and the
|
|
country file's guess is wrong for the large countries. A call history file with
|
|
a `CQZone` column fills that in.
|
|
|
|
A spot carries no mode either. Stations are judged in the mode the radio is in,
|
|
so in a mixed-mode contest the answer follows the operator.
|
|
|
|
### Radios
|
|
|
|
**Config → Radios** takes a `rigctld` address per radio. Each radio needs its
|
|
own `rigctld`, started for whichever rig is on that port:
|
|
|
|
rigctld -m 2028 -r /dev/ttyUSB0 -t 4532
|
|
rigctld -m 1035 -r /dev/ttyUSB1 -t 4533
|
|
|
|
Commands go out with a `+` in front, which asks `rigctld` for its extended
|
|
answer: named fields ended by an `RPRT` line. The raw answer is bare values with
|
|
no terminator, so the client has to know how many lines each command returns,
|
|
and one wrong count leaves the connection reading every later answer against
|
|
the wrong command.
|
|
|
|
Split is read from the radio and recorded: the contact stores where we
|
|
transmitted in N1MM's QSX column, the entry window shows `14008.00 ▸ 14020.0`,
|
|
and the bandmap draws a red bar at the transmit frequency. A radio that cannot
|
|
do split answers `RPRT -11`; that is an answer, not a broken connection, and
|
|
everything else it reported still counts.
|
|
|
|
A second radio makes the station SO2R and opens a second entry window. The two
|
|
windows share one log, one score and one run of serial numbers, so a station
|
|
worked on radio 1 is a dupe on radio 2. What is typed, the frequency, the mode
|
|
and whether you are running belong to each radio on its own.
|
|
|
|
| Key | What it does |
|
|
|---|---|
|
|
| Ctrl+Tab | moves you to the other radio: the keyboard, the entry window and the box all follow |
|
|
| Ctrl+Shift+Tab | puts both radios in the headphones, and back |
|
|
|
|
Both radios show on the bandmap — the one you are on in green, the other in
|
|
orange — but only the one you are on drives its entry window, so the second
|
|
radio moving cannot drag you off the station you are working.
|
|
|
|
An SO2R box speaking OTRSP routes the key and the headphones. Name its serial
|
|
port in **Config → Radios** and it follows: `TX2` when you move to radio 2,
|
|
`RX1S` when you ask for both radios, and `TX` again before every message goes
|
|
out, so a message cannot go out of the radio you have just left. A command that
|
|
would change nothing is not sent, because the box works relays. Without a box
|
|
the logger just keeps track of which radio it is on and you switch by hand.
|
|
|
|
Not there yet: alternating CQ, and voice keying on the second radio.
|
|
|
|
### The bandmap
|
|
|
|
A frequency scale down the left with the stations written out beside it. Each
|
|
callsign sits level with its frequency, and when two stations are too close to
|
|
write one above the other the lower one moves down and a leader line runs back
|
|
to where it really is. That is what N1MM does, and the stacking follows its
|
|
rule: place the label centred on its frequency, and push it below the one above
|
|
if it would cover it.
|
|
|
|
The slice shown follows the receiver — 10, 20, 40 or 100 kHz, or the whole band
|
|
— and stretches past the band edge when the radio is past it.
|
|
Callsigns are coloured by the same scorer as the entry window, so a dupe reads
|
|
as a dupe here too. Clicking a callsign puts the radio there with the call
|
|
already in the entry window; clicking anywhere else just moves the radio.
|
|
|
|
The bars on the scale are as wide as the mode passes: green where you are
|
|
listening, red where the radio transmits when working split, orange for the
|
|
other radio of a two-radio station.
|
|
|
|
Band-plan colouring of the scale is not there. The segments differ by ITU region
|
|
and the program has no band-plan table, so it would be guesswork.
|
|
|
|
### Band edges
|
|
|
|
A contact just outside a band edge keeps its band. `Bands.ForFrequency` first
|
|
checks the real edges, and for anything outside them falls back to the whole
|
|
megahertz: 14400 kHz is 20M, 7305 kHz is 40M. This is N1MM's rule, and it is
|
|
there because contest operating goes past the edges — someone answers a CQ at
|
|
14352, or a radio reports a frequency a few hundred hertz off — and a contact
|
|
with no band scores nothing and exports wrong. Below 1 MHz the megahertz is
|
|
always 0, so 2190M and 630M are told apart by their edges alone.
|
|
|
|
### The DX cluster
|
|
|
|
**Config → Cluster** takes the node's address, a password for the few nodes that
|
|
ask for one, and the commands to send after login. Filters are the node's
|
|
business, so whatever goes in the command box is sent as typed and left alone.
|
|
|
|
The client speaks telnet properly: it answers the option negotiation instead of
|
|
letting the control bytes turn up in the first lines of text, and it reads the
|
|
login prompt out of a partial line, because nodes write `login: ` with no line
|
|
ending. If no prompt arrives within ten seconds the callsign goes out anyway,
|
|
which is what N1MM does. A connection that has heard nothing for four minutes
|
|
gets a blank line, so the node does not drop it as idle. A dropped connection is
|
|
retried.
|
|
|
|
Alt+P, or **Edit → Spot It**, puts the call being typed on the cluster at the
|
|
current frequency; with nothing typed it spots the last contact logged. The spot
|
|
also goes straight onto our own bandmap rather than waiting to come back round
|
|
from the node.
|
|
|
|
### Networked stations
|
|
|
|
**Config → Network** names this station and lists the others. Each contact is
|
|
sent to them as it is logged, in N1MM's `contactinfo` message, so an N1MM
|
|
station on the same network sees them too. Editing a contact sends
|
|
`contactreplace` and deleting one sends `contactdelete`, the same as N1MM. With
|
|
no addresses listed the messages are broadcast. A contact that arrives is scored
|
|
again here from the rules rather than trusted.
|
|
|
|
An incoming edit or delete is matched by contact id first. N1MM does not know
|
|
our ids, so the call and the timestamp are the fallback — that is the pair N1MM
|
|
itself keys a contact on.
|
|
|
|
## Layout
|
|
|
|
| Project | What it holds |
|
|
|---|---|
|
|
| `Nonemm.Core` | Frequencies, bands, modes, callsigns, grid squares, the country file and the callsign database |
|
|
| `Nonemm.Contests` | Contest rules, the scoring engine, `.udc` files |
|
|
| `Nonemm.Formats` | Cabrillo out, ADIF in and out |
|
|
| `Nonemm.Storage` | The N1MM-compatible `.s3db` |
|
|
| `Nonemm.Rig` | Radio control over `rigctld`, and the SO2R box |
|
|
| `Nonemm.Spotting` | Spots, the bandmap, the DX cluster client |
|
|
| `Nonemm.Network` | Contacts shared between the stations of a multi-operator entry |
|
|
| `Nonemm.Keying` | CW through `cwdaemon` or a WinKeyer |
|
|
| `Nonemm.Session` | The contest in progress, and one entry position per radio — no UI toolkit |
|
|
| `Nonemm.App` | The Avalonia windows |
|
|
|
|
The split at `Nonemm.Session` is the important one: it references no UI
|
|
framework, so what space does, when a dupe fires and what a contact scores are
|
|
covered by plain unit tests.
|
|
|
|
## Where it stands
|
|
|
|
Working: logging a contest end to end, live dupe and multiplier checking, eight
|
|
built-in contests plus user-defined ones, Cabrillo and ADIF export, ADIF import,
|
|
the log, check, graphical bandmap, score and packet windows, editing and deleting logged
|
|
contacts, radio control, DX cluster spots, contacts shared between networked
|
|
stations, and CW keying.
|
|
|
|
Checked against N1MM 1.0.11031: a log this program wrote opens in N1MM, which
|
|
reads the contest, its categories and the contacts. See
|
|
[`docs/n1mm-interop.md`](docs/n1mm-interop.md) for what that took.
|
|
|
|
Nothing here has been run against a real radio, keyer, SO2R box or cluster node.
|
|
Every one of those is tested against a fake that speaks the same protocol, which
|
|
proves the two agree with each other and nothing more.
|
|
|
|
[`docs/unfinished.md`](docs/unfinished.md) lists what is half-built and what has
|
|
never been tested against the real thing.
|