Three things stopped it. A contest is keyed by ContestNR, not by ContestID. ContestID is only the ContestInstance table's primary key; DXLOG.ContestNR refers to ContestNR, which is what N1MM's SQLWhereString matches on. The two agree in a fresh log and drift apart in one that has been used for a while, so we were reading one contest's header with another contest's contacts. In a real log of 56284 contacts, joining on ContestID disagreed with the contact's own ContestName 52029 times; joining on ContestNR disagreed 109 times. N1MM names a contest per mode: CQWWCW, CQWWSSB, CQWWRTTY, never plain CQWW. Our contests now use those names, and N1mmContestNames turns one back into a contest and a mode when a log is opened. The name carries the mode, so it beats the ModeCategory column. Plain CQWW still opens, because older versions wrote it that way. The log columns were sized by counting characters, which was too narrow for the headers: those are drawn in the theme's font, not the grid's monospace. The grid sizes them now, with the character count as a floor so a column of short values does not collapse. Bandmap spots now age from when they arrived rather than from the time written in them. A node with a wrong clock, or one replaying its backlog on connect, emptied the bandmap as fast as it filled it. Checked by opening a real 56284-contact N1MM log under Xvfb and reading the contest and contacts back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
82 lines
3.4 KiB
Markdown
82 lines
3.4 KiB
Markdown
# Opening a Nonemm log in N1MM
|
|
|
|
Checked on 2026-08-27 against N1MM Logger+ 1.0.11031 by writing a log here,
|
|
copying it to a Windows machine and opening it there. N1MM read the contest, its
|
|
categories and the contacts.
|
|
|
|
Three things had to be right, and none of them is obvious from the schema.
|
|
|
|
## The `Contest` table needs a row for the contest
|
|
|
|
`ContestInstance` says which contest a log is; the `Contest` table says what that
|
|
contest *is*. N1MM reads it in `Contest.FromRow` when it opens a log and throws
|
|
`InvalidOperationException: No current row` if the row is missing, which reaches
|
|
the operator as "A runtime error occurred".
|
|
|
|
So the definition row is written whenever a contest is opened, not only when it
|
|
is created — a log made before this was understood gets its row the next time it
|
|
is opened. `ContestDefinitions.For` builds the row from the contest's own rules:
|
|
the display and Cabrillo names, the mode, the dupe type and the multiplier
|
|
names.
|
|
|
|
## The overlay category cannot be empty
|
|
|
|
An empty `ContestInstance.OverlayCategory` is answered with "Invalid Overlay
|
|
Category:" and the log will not open. An entry with no overlay says `N/A`.
|
|
|
|
N1MM's list is not the Cabrillo specification's list. N1MM offers:
|
|
|
|
N/A, ROOKIE, BAND-LIMITED, TB-WIRES, OVER-50, HQ, NOVICE-TECH, EXPERT
|
|
|
|
so that is what the contest dialog offers.
|
|
|
|
## The sent exchange omits the report
|
|
|
|
N1MM's contest dialog says "Omit RST: CQWW: 05". `SentExchange` for CQ WW is
|
|
`14`, not `599 14`; for a serial number contest it is `001`. The report is fixed
|
|
for the whole contest and the entry window fills it in per contact.
|
|
|
|
## Getting the schema in the first place
|
|
|
|
N1MM ships no template database. `ham.s3db` is built at run time by applying SQL
|
|
migration files that N1MM writes out from string resources inside
|
|
`N1MMLogger.net.exe`. To read them: decompile the executable, parse
|
|
`N1MMLogger.Net.Resources.resx`, and take the untyped `<data>` entries named
|
|
`DXLogDDL_0001_initial_schema_and_data` through `DXLogDDL_0004_updates`. The QSO
|
|
table is `DXLOG` and the current `PRAGMA user_version` is 4.
|
|
|
|
`src/Nonemm.Storage/Schema.sql` is that schema, flattened to the state version 4
|
|
leaves behind.
|
|
|
|
## One thing to avoid
|
|
|
|
Do not replace the database file under a running N1MM. It does not notice and
|
|
throws on the next read.
|
|
|
|
## A contact belongs to a ContestNR, not to the table key
|
|
|
|
`ContestInstance` has two numbers: `ContestID`, the table's primary key, and
|
|
`ContestNR`. `DXLOG.ContestNR` refers to the second one. N1MM's
|
|
`ContestInstance.SQLWhereString` reads:
|
|
|
|
" ContestNR = " + this.ContestNR + " "
|
|
|
|
In a fresh log the two numbers match, so keying on either works. In a log N1MM
|
|
has been using for a while they drift apart. In one real log of 56284 contacts,
|
|
joining `DXLOG.ContestNR` to `ContestInstance.ContestID` disagreed with the
|
|
contact's own `ContestName` 52029 times; joining it to `ContestInstance.ContestNR`
|
|
disagreed 109 times.
|
|
|
|
So `ContestInstance.ContestNumber` here is `ContestNR`, and `ContestID` is
|
|
allocated separately when a contest is created.
|
|
|
|
## N1MM names a contest per mode
|
|
|
|
CQ WW is `CQWWCW`, `CQWWSSB` or `CQWWRTTY`, never plain `CQWW`. The full list
|
|
lives in the `Contest` table of any N1MM database. Nonemm writes the same names,
|
|
and `N1mmContestNames` turns one back into a contest and a mode when a log is
|
|
opened. The name carries the mode, so it beats whatever `ModeCategory` says.
|
|
|
|
Older N1MM versions wrote the CW running of CQ WW as plain `CQWW`. That name is
|
|
still in the registry, so those logs open.
|