Skip to content
Merged
61 changes: 61 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -577,8 +577,13 @@ set(CORE_SOURCES
src/core/MemoryRecallPolicy.cpp
src/core/tnc/AetherAx25LibmodemShim.cpp
src/core/tnc/Ax25FrameFormatter.cpp
src/core/tnc/Ax25.cpp
src/core/tnc/Ax25Connection.cpp
src/core/tnc/HeardList.cpp
src/core/tnc/KissFraming.cpp
src/core/tnc/KissTncServer.cpp
src/core/tnc/TncTerminal.cpp
src/core/pms/PmsMailbox.cpp
)

if(APPLE)
Expand Down Expand Up @@ -1938,6 +1943,7 @@ add_executable(ax25_libmodem_shim_test
tests/ax25_libmodem_shim_test.cpp
src/core/tnc/AetherAx25LibmodemShim.cpp
src/core/tnc/Ax25FrameFormatter.cpp
src/core/tnc/Ax25.cpp
src/core/tnc/KissFraming.cpp
# LogManager.cpp provides lcAx25 (the shim's qCDebug category, #2763);
# LogManager.cpp depends on AsyncLogWriter via the m_writer member, so
Expand All @@ -1951,6 +1957,61 @@ target_include_directories(ax25_libmodem_shim_test PRIVATE src)
target_link_libraries(ax25_libmodem_shim_test PRIVATE Qt6::Core aether_libmodem_core)
add_test(NAME ax25_libmodem_shim_test COMMAND ax25_libmodem_shim_test)

# Offline AX.25 decode diagnostic: replays a captured WAV through the decoder.
# Not a ctest (needs an input file); built on demand for troubleshooting.
add_executable(ax25_replay EXCLUDE_FROM_ALL
tools/ax25_replay.cpp
src/core/tnc/AetherAx25LibmodemShim.cpp
src/core/tnc/Ax25FrameFormatter.cpp
src/core/tnc/KissFraming.cpp
src/core/LogManager.cpp
src/core/AsyncLogWriter.cpp
src/core/AppSettings.cpp
)
target_include_directories(ax25_replay PRIVATE src)
target_link_libraries(ax25_replay PRIVATE Qt6::Core aether_libmodem_core)

add_executable(ax25_session_analyze EXCLUDE_FROM_ALL
tools/ax25_session_analyze.cpp
src/core/tnc/AetherAx25LibmodemShim.cpp
src/core/tnc/Ax25FrameFormatter.cpp
src/core/tnc/Ax25.cpp
src/core/tnc/Ax25Connection.cpp
src/core/tnc/KissFraming.cpp
src/core/LogManager.cpp
src/core/AsyncLogWriter.cpp
src/core/AppSettings.cpp
)
target_include_directories(ax25_session_analyze PRIVATE src)
target_link_libraries(ax25_session_analyze PRIVATE Qt6::Core aether_libmodem_core)

add_executable(pms_mailbox_test
tests/pms_mailbox_test.cpp
src/core/tnc/Ax25.cpp
src/core/tnc/Ax25Connection.cpp
src/core/pms/PmsMailbox.cpp
src/core/AppSettings.cpp
)
target_include_directories(pms_mailbox_test PRIVATE src)
target_link_libraries(pms_mailbox_test PRIVATE Qt6::Core)
add_test(NAME pms_mailbox_test COMMAND pms_mailbox_test)

add_executable(tnc_terminal_test
tests/tnc_terminal_test.cpp
src/core/tnc/Ax25.cpp
src/core/tnc/Ax25Connection.cpp
src/core/tnc/HeardList.cpp
src/core/tnc/TncTerminal.cpp
# HeardList now emits qCWarning(lcAx25) on persistence failure; pull in
# LogManager + its deps so the category symbol resolves.
src/core/LogManager.cpp
src/core/AsyncLogWriter.cpp
src/core/AppSettings.cpp
)
target_include_directories(tnc_terminal_test PRIVATE src)
target_link_libraries(tnc_terminal_test PRIVATE Qt6::Core)
add_test(NAME tnc_terminal_test COMMAND tnc_terminal_test)

add_executable(cwx_panel_test
tests/cwx_panel_test.cpp
src/gui/CwxPanel.cpp
Expand Down
64 changes: 62 additions & 2 deletions docs/MODEM.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,58 @@ problem can be triaged as client-side (connect/parse/backlog) vs RF-side
(decode/level/gate). The TNC STATUS panel shows listening port, client count,
and RX/TX frame counters.

## Personal Mailbox System (PMS)

The **Mailbox** tab turns AetherModem into a compact, Kantronics-KPC-3-style
Personal Mailbox System (PBBS). A single remote caller can connect over
**1200-baud AX.25 connected mode** and read, list, and send messages, see who
has been heard, then disconnect.

**Connected-mode data link.** Unlike the KISS/UI paths (connectionless), the PMS
needs AX.25 v2.0 connected mode (LAPB, mod-8). Two reusable, RF-agnostic,
unit-tested layers provide it:

- `src/core/tnc/Ax25.{h,cpp}` — frame primitives: callsign/SSID `Address`
encode/decode and `Frame` parse/build for I, RR/RNR/REJ, SABM, DISC, DM, UA,
FRMR, and UI frames (address..info, no FCS — the same convention as
`buildTransmitAudioFromFrame`).
- `src/core/tnc/Ax25Connection.{h,cpp}` — a single-connection state machine:
accepts an inbound SABM (→ UA), tracks V(S)/V(R)/V(A), acknowledges with RR,
segments outbound data into I-frames (≤ paclen), and retransmits unacked
I-frames on the T1 timeout up to N2 tries before declaring link failure.
Defaults (T1 6 s, N2 8, paclen 128, window 4) are sized for 1200-baud FM with
PTT overhead, so a lossy link recovers via REJ/T1 rather than dropping.

**The mailbox service** is `src/core/pms/PmsMailbox.{h,cpp}` (one file pair). It
owns the `Ax25Connection`, greets a caller by callsign with the AetherMailbox SID
and version, runs a line-oriented command interpreter, and persists state as JSON
under `~/.config/AetherSDR/pms/` (`messages.json`, `callers.json`,
`heard.json`). Decoded frames are fed in via `onAirFrame()`; everything it emits
on `transmitFrame()` is keyed through the existing one-at-a-time TX queue. The
**heard list** is updated for *all* received frames (not just mailbox traffic) so
callers can discover other PMS/BBS stations nearby, and an optional **hourly UI
beacon** announces the mailbox is online and how to connect.

**Commands** (Kantronics subset; first letter or full word):
`H(elp)`, `B(ye)`, `I(nfo)`, `J(heard)`, `L(ist)`, `LM` (list mine),
`R(ead) n`, `K(ill) n`, `S(end)`/`SP call`, `SB cat`, `U(sers)`. A message is
entered after `SUBJECT:` and terminated with `/EX` or Ctrl-Z on its own line.
Use `ALL` as the recipient for a public message.

The **Mailbox** config tab exposes Enable PMS, the **listen callsign** the
mailbox answers on (full `CALL-SSID`, e.g. `KI6BCJ-10`), an optional **vanity
alias** it also answers on (e.g. `AETBBS` — AX.25 limits a callsign to 6
characters plus an optional `-SSID`), a welcome/PTEXT line, the
hourly-beacon toggle and text, plus a stats row with **Statistics** on the left
and the **last callers** on the right. When a caller dials the alias, the whole
session (UA, greeting, every reply) uses the alias address. All settings persist
in `AppSettings` (`AetherModemPms*` keys) across restarts; enabling the PMS turns
the modem on. The bottom of the window is a slim status bar showing modem state,
gain, and a compact packet-activity strip.

These layers are intentionally split so the planned APRS/AX.25 **digipeater** can
reuse `Ax25`/`Ax25Connection` and the heard list directly.

## Open Work

The remaining missed packets are mostly AX.25-looking candidates that fail FCS. That means the decoder is often finding packet structure but still has symbol/bit errors before CRC.
Expand All @@ -216,5 +268,13 @@ Next work should focus on:
- validating over-the-air AetherModem TX level, timing, and FCS decode with a
second receiver

Out of scope remains APRS-IS, maps, digipeating, and connected-mode AX.25.
(KISS-over-TCP is now implemented — see the KISS TNC section above.)
Out of scope remains APRS-IS and maps. (KISS-over-TCP, connected-mode AX.25, and
a Personal Mailbox System are now implemented — see the sections above. A future
APRS/AX.25 digipeater can reuse the new `Ax25`/`Ax25Connection` primitives.)

PMS follow-ups worth tracking:

- On-air validation of connected-mode RX/TX at 1200 baud with a second TNC
(the protocol layer is unit-tested; RF round-trip needs a real radio).
- Multi-connect (the current PMS answers one caller at a time, by design).
- A local operator terminal tab to use/test the mailbox without a radio.
Loading
Loading