Skip to content

Repository files navigation

tait-codeplug

Read, decode, edit and program a Tait TM8100 / TM8200 codeplug over the serial programming interface, on Linux, macOS or Windows, without the Windows CPS.

Reverse-engineered from Free Serial Analyzer captures of the CPS and hardware-validated against a real TM8100: a same-image write round-trips every writable record byte-identical, and the field map is validated field-by-field against real-radio CPS saves.

Two things ship from this repo, at the same version:

tait-codeplug the CLI, as a self-contained single-file binary for six platforms - latest release
M0LTE.Tait.Codeplug the library behind it, on nuget.org

Install the CLI

Each binary embeds the .NET runtime and the native serial library, so there is nothing else to install.

Debian, Ubuntu and Raspberry Pi OS: apt

The recommended route, because apt upgrade then keeps it current along with everything else:

curl -fsSL https://packet-net.github.io/apt/pubkey.asc | sudo gpg --dearmor -o /usr/share/keyrings/packet-net.gpg
echo "deb [signed-by=/usr/share/keyrings/packet-net.gpg] https://packet-net.github.io/apt ./" | sudo tee /etc/apt/sources.list.d/packet-net.list
sudo apt update
sudo apt install tait-codeplug

amd64, arm64 and armhf. The same packet-net apt repository carries the rest of the Packet.NET packages, so the three lines above are worth having anyway.

On armhf this needs Debian 12 (bookworm) or newer, which for a Raspberry Pi means a 32-bit image of Raspberry Pi OS bookworm or later. .NET 10's 32-bit ARM runtime requires glibc 2.34, and Debian 11 (bullseye) has 2.31, so the binary cannot start there at all. The package says so in its dependencies, so apt on an older machine declines with unmet dependencies rather than installing something that then dies in the dynamic loader. A 64-bit image is the better answer where the Pi supports one. amd64 and arm64 are not affected and still install on bullseye.

An apt-installed copy is upgraded with apt, not with --upgrade: run sudo apt update && sudo apt install --only-upgrade tait-codeplug. --upgrade will notice and tell you so rather than overwriting a file dpkg owns.

Everything else: download the binary

Grab the one for your platform from the latest release:

curl -LO https://github.com/M0LTE/tait-codeplug/releases/latest/download/tait-codeplug-<version>-linux-x64
chmod +x tait-codeplug-<version>-linux-x64
./tait-codeplug-<version>-linux-x64

Assets: linux-x64, linux-arm64, linux-arm (armv7 / 32-bit Pi), win-x64, osx-x64 (Intel), osx-arm64 (Apple Silicon). SHA256SUMS covers every asset. A binary installed this way updates itself with tait-codeplug --upgrade.

Or build it yourself: dotnet run --project src/M0LTE.Tait.Codeplug.Cli -- <verb> ... (.NET 10 SDK).

If it goes wrong

Interactive mode writes a crash report to the temporary directory (/tmp/tait-codeplug-crash-<timestamp>.log on Linux) and prints the same thing to the terminal once it has handed the screen back. Paste that into an issue: it names the version, the platform, what the tool was doing and the full error. The one-shot verbs print their errors straight to the terminal, so running the same operation as read or patch rather than in interactive mode is also a quick way to see what a failure actually says.

Interactive mode

Run it with no arguments and you get a screen instead of a verb: pick a port, read the radio, edit the packet-relevant essentials, write it back.

╭┤tait-codeplug 0.4.1 - Tait TM8100/TM8200 codeplug editor├────────────────────────────────╮
│╭┤Radio├─────────────────────────────────────────────────────────────────────────────────╮│
││ Port:                           (no serial ports detected)                             ││
││                                                                                        ││
││ ⟦ Read from radio ⟧▖ ⟦ Write to radio ⟧▖ DBVer 0095, 169 records                       ││
│╰────────────────────────────────────────────────────────────────────────────────────────╯│
│╭┤Channels - Enter or F3 to edit├────────────────────────────╮╭┤PDN preset├──────────────╮│
││ #   RX (MHz)      TX (MHz)      Bandwidth  Power           ││ ◉ none                   ││
││ 0   144.812500    (= RX)        Narrow     High            ││ ○ audio-and-ptt          ││
││                                                            ││ ○ pdn-basic              ││
││                                                            ││ ○ pdn-extra              ││
││                                                            ││ ○ pdn-internal           ││
││                                                            ││                          ││
││                                                            ││ Applied when you         ││
││                                                            ││ write. No preset         ││
││                                                            ││ touches RF or channel    ││
││                                                            ││ config.                  ││
│╰────────────────────────────────────────────────────────────╯╰──────────────────────────╯│
│╭┤Log├───────────────────────────────────────────────────────────────────────────────────╮│
││ 22:41:05  loaded /home/tf/packet.net/tait-programming-research/tait-gps-customer-identi││
││ 22:41:05  DBVer 0095, 1 channel(s) decoded.                                            ││
││ 22:41:11  channel 0 edited (not yet written to the radio).                             ││
││                                                                                        ││
││                                                                                        ││
││                                                                                        ││
││                                                                                        ││
│ F10  Quit │ F3  Edit channel │ F5  Read │ F2  Write                                      │
╰──────────────────────────────────────────────────────────────────────────────────────────╯

F6 moves between panels and Tab moves within one; the panel holding the keyboard lights its border. F5 reads the radio (it prompts you to power-cycle it), F3 edits the selected channel, F7 adds one, F8 deletes one, F2 writes back, F10 quits. The PDN preset is staged and applied when you write, so choosing one changes nothing until you commit. A write always snapshots the pre-change codeplug to a tait-codeplug-backup-<timestamp>.m8p first.

The radio work runs off the UI thread, so the screen stays live through the ~25s read and the 90s the connect will wait for your power-cycle.

Left alone, it goes quiet: the main loop steps down after ten seconds untouched and again after a minute, so an editor left open over SSH is not writing to your terminal 25 times a second all afternoon. Typing is unaffected; the one key that wakes it after a long pause can take up to a quarter of a second to register, and everything after it is normal.

F5 and F2 put "power-cycle the radio now" on the screen rather than in the log, and take it down again by themselves once the radio answers. Cancel or Esc abandons the operation instead of waiting out the full 90 seconds. Both show a progress bar while they run.

Typing feels slow over SSH

It is, and it is worth knowing why before you go looking for a fault at your end. Terminal.Gui repaints the whole screen for every character typed into a text box - about 7-8 bytes per cell on screen, so 22 KB on a 100x30 terminal and 82 KB at 200x50, per keystroke. A minimal Terminal.Gui app does the same, so it is the library rather than this tool, and there is nothing to configure around it.

How much that costs you depends on the console and on which of Terminal.Gui's three drivers is in front of it, and the difference between them is large. Measure it on your own machine rather than trusting a number from someone else's:

tait-codeplug tui --driver list      # what this platform offers
tait-codeplug tui --bench            # time one repaint on the default driver
tait-codeplug tui --bench --driver ansi

Under about 30ms per repaint feels instant; a few hundred milliseconds is the editor feeling sluggish. If another driver is quicker, use it: tait-codeplug tui --driver ansi radio.m8p.

Beyond that:

  • Make the terminal window smaller while you are editing: 80x24 costs a sixth of what 200x50 does.
  • Skip the editor for a single value: tait-codeplug patch /dev/ttyUSB0 ch0.rxfreq 144.812500 does a read-modify-write with no typing in a UI at all.
  • Over SSH, run the tool on the machine the radio is plugged into rather than across the link.

Colours are true-colour: a dark slate palette, green for read, amber for write (it is the one that changes your radio), red for errors. Terminal.Gui maps them down on a 16- or 256-colour terminal, so it stays legible on a plain console.

To try the editor without a radio on the bench, open a saved codeplug: tait-codeplug tui radio.m8p.

Use it from the command line

# decode - the source is an .m8p file OR a serial port (reads the live radio)
tait-codeplug parse   <file.m8p | port>            verify checksums + print the section map
tait-codeplug dump    <file.m8p | port>            decode every mapped field
tait-codeplug get     <file.m8p | port> [field]    read one field, or all as name=value
tait-codeplug set     <file.m8p> <field> <value>   set one field and save (e.g. ch0.bandwidth Wide)
tait-codeplug set     <file.m8p> profile <name>    apply an upgrade profile to a file

# hardware (radio latched into programming mode on <port>: power-cycle it as you trigger)
tait-codeplug version <port>                       interrogate: model / firmware / serial
tait-codeplug read    <port> [out.m8p]             read the codeplug (to a file, or stdout if omitted)
tait-codeplug patch   <port> <field> <value>       live-set one field (backs up first)
tait-codeplug patch   <port> profile <name>        live-apply an upgrade profile
tait-codeplug channel add    <file.m8p>            append a channel (a copy of the last one)
tait-codeplug channel delete <file.m8p> <n>       remove channel n, shifting the rest down
tait-codeplug tui     [file.m8p]                  interactive mode, optionally on a saved codeplug
tait-codeplug --upgrade                           replace this binary with the latest release

--upgrade fetches the release build for your platform, checks it against the release's own SHA256SUMS, and renames it over the running binary. Nothing is replaced unless the checksum matches, and the swap is a rename, so a failure at any point leaves what you have working. It refuses early if it cannot write where the binary lives, rather than downloading 40 MB first to find out.

The radio must be latched into programming mode: power-cycle it as the command connects. Progress and prompts go to stderr, so read <port> > radio.m8p gives you a clean .m8p on stdout.

Upgrade profiles

audio-and-ptt, pdn-basic, pdn-extra and pdn-internal upgrade a radio to the Packet.NET feature set - CCDI telemetry and control, and the TNC-less internal FFSK packet modem plus SDM mode signalling - without touching RF config (channels, frequencies, power), so they layer safely onto a radio already provisioned for its environment. See the library README for exactly what each one sets.

They nest, so apply the one profile that describes the radio and it carries the rest.

audio-and-ptt is the modem wiring for the auxiliary connector - a soundcard interface or TNC: Rx tap-out R1, type Split, unmuted except on PTT; EPTT1 tap-in T13; AUX_GPI1 as an active-low External PTT 1 input; and External PTT 1 transmitting Data from the Audio Tap In instead of Voice from the aux mic. Those three records come out byte-identical to a CPS save of the same configuration on a default TM8100 codeplug. pdn-basic and pdn-extra both include it, so this one is for a radio that needs the wiring and nothing else - one whose data settings are already right, or one being set up for an external modem without the CCDI side. The PTT sources are settable individually too: set radio.m8p ptt.eptt1 DataFromAudioTapIn (or Voice); get radio.m8p | grep ptt. lists all three.

It also programs the front-panel F1 key to Squelch Override, so you can open the speaker and hear what is really on the channel whatever the squelch and subaudible signalling are doing. Every profile passes through audio-and-ptt, so every profile carries it. That is an operator convenience rather than part of the modem wiring, so note it if you apply audio-and-ptt for the wiring alone. The keys are settable individually: set radio.m8p key.f1 SquelchOverride (or Unassigned); get radio.m8p | grep key. lists all four. Like the digital I/O lines, a key is written as a whole pattern validated against a CPS save, and a function this tool has not captured reads as Other and is left untouched.

And it winds the Tx timer out to 250 seconds, the CPS's maximum. A default codeplug drops the carrier after 60, which is short enough to cut a long packet transmission in half; every profile carries the longer timer for the same reason it carries the key. It is settable on its own in seconds: set radio.m8p txtimer 250, and 0 means no time-out at all. get radio.m8p txtimer reads it back. It is the first network's Tx Timer Duration on the CPS's Networks > Basic Settings page.

pdn-basic adds the CCDI command channel on top of that wiring - CCDI on, the radio in Command mode at power-up, progress messages on, the command baud at 28800, and the data port on Mic, the front-panel connector the host's serial lead plugs into - and pdn-extra adds the transparent FFSK modem and SDM signalling on top of that.

pdn-internal is the one for a radio with a Packet.NET internal options board fitted: pdn-extra with the data port moved to Internal Options, the tap-out moved to R2 for a sound-card modem, and IOP_GPIO1 programmed as an active-low External PTT 1 input for the board's PTT line. It releases AUX_GPI1 again, so only the board can key the radio. Every PTT line is settable on its own: set radio.m8p gpio.iop_gpio1 ExternalPtt1Input (or Unassigned, or BusyStatusOutput on a line that can be an output); get radio.m8p | grep gpio lists every line.

Safety

  1. patch snapshots the current codeplug to a backup file before writing. Keep it.
  2. Codeplug region only. This never writes firmware.
  3. Version-pinned: the write path refuses a radio whose database version is not in its validated set (currently 0094 / 0095), because the field offsets are version-specific.
  4. The field map enforces the CPS's own input rules, so the tool will not write a state the CPS rejects.
  5. Bench on a sacrificial radio first, and re-read after a power-cycle to verify a write.

No RF is involved in any of this, and no part of it transmits.

Protocol and provenance

The protocol write-up is docs/research/tait-codeplug-protocol.md and the programming brief is docs/research/tait-codeplug-programming-brief.md, both in packet-net/packet.net, where this code was developed before moving here. Its history came with it. That repo also holds Packet.Radio.Tait, the runtime CCDI/transparent-mode driver these profiles provision a radio for.

Releasing

Add a section to CHANGELOG.md for the version first: its bullets become the "What's changed" list at the top of the GitHub release, above the install instructions. If you forget, the release falls back to the commit subjects since the previous tag and the run logs a warning.

A v* tag runs .github/workflows/publish.yml: it gates on the test suite, pushes M0LTE.Tait.Codeplug to nuget.org via trusted publishing (OIDC, no stored API key), then cross-publishes the six CLI binaries, builds the three .deb packages (packaging/build-deb.sh) and attaches them all plus SHA256SUMS to a GitHub Release.

Last of all it POSTs a release-published repository_dispatch at packet-net/apt, which rebuilds its index and picks the new .debs up within a minute or so. That needs an APT_DISPATCH_TOKEN secret on this repo: a fine-grained PAT with Contents: Read and write on packet-net/apt and nothing else. Without it the release still ships and the run logs a warning; the apt repo's hourly cron then finds the release on its own.

Note that a release marked prerelease or draft never reaches apt: the apt repo reads releases/latest, which ignores both. That is deliberate.

git tag -a v0.3.0 -m "v0.3.0 - <one-line summary>" && git push origin v0.3.0

Licence

AGPL-3.0-or-later. See LICENSE.

About

Read, decode, edit and program a Tait TM8100/TM8200 codeplug over serial without the Windows CPS. Cross-platform CLI + the M0LTE.Tait.Codeplug library. AGPL-3.0-or-later.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages