Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ What changed in each release. The section for a version is lifted into that vers

Newest first. Add a section before tagging.

## 0.15.0 - 2026-09-20

- **The Tx timer is readable and settable**: `txtimer`, the CPS's **Tx Timer Duration** on Networks > Basic Settings > Basic Network Settings - how long the radio may transmit before the timer drops the carrier. It reads and writes in seconds; **`0` means no time-out at all**, and `250` is the CPS's maximum. `get radio.m8p txtimer` reads it, `set radio.m8p txtimer 250` sets it, and `patch <port> txtimer 250` does it live. It is two fields of the network table (0x15) that the CPS keeps in step - an 8-bit duration at bit 52 of the first network's entry, and a flag at bit 7 that it clears when the duration is zero - so writing one maintains the other. Three CPS saves of one codeplug (the duration at its 60 s default, at 30 s, and at 0) differ in that record and in **no other byte of the file**, and setting the field on the first reproduces each of the other two byte for byte, and restores the first. Only the first network is mapped: a default codeplug carries exactly one, and no capture pins where the next entry begins.
- **Every profile now winds the Tx timer out to 250 seconds**, the maximum. A default codeplug drops the carrier after 60, which is short enough to cut a long packet transmission in half - so `audio-and-ptt` sets it, and `pdn-basic`, `pdn-extra` and `pdn-internal` all carry it the same way they carry the `F1` key. Like that key it is an operator setting rather than part of the modem wiring, so note it if you apply `audio-and-ptt` for the wiring alone: the profiles now touch the network table (0x15) as well. Still no RF or channel config touched.

## 0.14.1 - 2026-09-20

A packaging fix. The tool itself is unchanged.
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,12 @@ are settable individually: `set radio.m8p key.f1 SquelchOverride` (or `Unassigne
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
Expand Down
3 changes: 2 additions & 1 deletion src/M0LTE.Tait.Codeplug.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,8 @@ static void PrintUsage()
Console.WriteLine("upgrade profiles (leave RF/channels untouched; adjust the bauds for your setup):");
Console.WriteLine(" audio-and-ptt the aux-connector modem wiring: audio taps R1/split/except-on-PTT + T13,");
Console.WriteLine(" AUX_GPI1 = External PTT 1, EPTT1 transmits data from the tap in, F1 = squelch");
Console.WriteLine(" override. Carried by all three below, so every profile sets that key.");
Console.WriteLine(" override, Tx timer 250s (the max). Carried by all three below, so every");
Console.WriteLine(" profile sets that key and that timer.");
Console.WriteLine(" pdn-basic CCDI telemetry + control (RSSI, fwd/rev power, status, PTT, DCD) out of the");
Console.WriteLine(" Mic connector (data port Mic), plus audio-and-ptt");
Console.WriteLine(" pdn-extra pdn-basic + the TNC-less internal FFSK packet modem and SDM mode signalling");
Expand Down
2 changes: 1 addition & 1 deletion src/M0LTE.Tait.Codeplug.Cli/Tui.cs
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ private static Window Build()
Y = 5,
Width = Dim.Fill(),
Height = Dim.Fill(),
Text = "Applied when you\nwrite. No preset\ntouches RF or channel\nconfig. All set F1\nto squelch override.\n\n"
Text = "Applied when you\nwrite. No preset\ntouches RF or channel\nconfig. All set F1 to\nsquelch override and\nTx timer 250s.\n\n"
+ "audio-and-ptt: the\naux-connector modem\nwiring - audio taps\nand PTT line.\n\n"
+ "basic: that, plus\nCCDI control (RSSI,\npower, status, PTT,\nDCD) on the Mic\ndata port.\n\n"
+ "extra: adds the\nTNC-less FFSK modem\nand SDM signalling.\n\n"
Expand Down
8 changes: 8 additions & 0 deletions src/M0LTE.Tait.Codeplug/FieldConsole.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ public static class FieldConsole
rows.Add(("ctcsstable", string.Join(",", f.CtcssTable.Select(hz => hz.ToString("0.0", CultureInfo.InvariantCulture)))));
rows.Add(("dcstable", string.Join(",", f.DcsTable)));

// Networks > Basic Settings, Basic Network Settings tab (record 0x15; only if present)
if (f.HasNetworks)
{
rows.Add(("txtimer", f.TxTimerSeconds.ToString(CultureInfo.InvariantCulture)));
}

rows.Add(("sdm", f.SdmEnabled ? "true" : "false"));
rows.Add(("thsd", f.ThsdModemEnabled ? "true" : "false"));
rows.Add(("transparent", f.TransparentModeEnabled ? "true" : "false"));
Expand Down Expand Up @@ -201,6 +207,8 @@ public static void Set(CodeplugFields f, string name, string value)

switch (name.ToLowerInvariant())
{
// Networks > Basic Settings tab (seconds; 0 is no time-out, 250 the CPS's maximum)
case "txtimer": f.TxTimerSeconds = int.Parse(value, CultureInfo.InvariantCulture); return;
case "sdm": f.SdmEnabled = Bool(value); return;
case "thsd": f.ThsdModemEnabled = Bool(value); return;
case "transparent": f.TransparentModeEnabled = Bool(value); return;
Expand Down
65 changes: 61 additions & 4 deletions src/M0LTE.Tait.Codeplug/Fields/CodeplugFields.cs
Original file line number Diff line number Diff line change
Expand Up @@ -418,9 +418,11 @@ public void ApplyPacketAudioDefaults()
/// puts the modem's audio on air.
/// <para>
/// It also programs the front-panel <b>F1</b> key to Squelch Override, so the operator can open
/// the speaker and hear the channel whatever the squelch and subaudible signalling are doing.
/// That is an operator convenience rather than part of the modem wiring, but it is wanted on
/// every radio these profiles provision, and this is the one they all pass through.
/// the speaker and hear the channel whatever the squelch and subaudible signalling are doing, and
/// winds the <b>Tx timer</b> out to <see cref="MaxTxTimerSeconds"/> seconds, the CPS's maximum -
/// a default codeplug drops the carrier after 60, which is short enough to cut a packet
/// transmission in half. Neither is part of the modem wiring, but both are wanted on every radio
/// these profiles provision, and this is the one they all pass through.
/// </para>
/// <para>
/// <see cref="ApplyPdnBasic"/> applies it as part of a full packet upgrade; it is exposed on its
Expand All @@ -429,7 +431,8 @@ public void ApplyPacketAudioDefaults()
/// record nor the RF config: it changes the audio block (0x3B), one line of the digital I/O table
/// (0x37) and one entry of the PTT table (0x19) - those three reproduce, byte for byte, a CPS save
/// of that configuration on an otherwise default TM8100 codeplug (DBVer 0095) - plus the three key
/// items (0x0F, 0x03, 0x18), which likewise reproduce a CPS save of that key edit (DBVer 0094).
/// items (0x0F, 0x03, 0x18), which likewise reproduce a CPS save of that key edit (DBVer 0094),
/// and the first network's entry (0x15), which reproduces a CPS save of the timer edit.
/// </para>
/// </summary>
public void ApplyAudioAndPtt()
Expand All @@ -438,6 +441,7 @@ public void ApplyAudioAndPtt()
SetDigitalIoRole(DigitalIoLine.AuxGpi1, DigitalIoRole.ExternalPtt1Input);
SetPttTransmission(PttSource.ExternalPtt1, PttTransmission.DataFromAudioTapIn);
SetFunctionKeyRole(FunctionKey.F1, FunctionKeyRole.SquelchOverride);
TxTimerSeconds = MaxTxTimerSeconds;
}

/// <summary>
Expand Down Expand Up @@ -1732,6 +1736,59 @@ public void SetCustomerNetworkByte(int network, int index, byte value)
Image.Require(0x4D, (byte)(network - 1)).Data[index - 1] = value;
}

// ---- Networks > Basic Settings, Basic Network Settings tab (record 0x15/0) ----------
//
// Item 0x15 is the network table: one entry per network, packed LSB-first, the first entry
// starting at bit 0 of record 0x15/0. Only that first network is mapped - a factory-default
// codeplug carries exactly one, and no capture pins where the next one starts.

private const byte NetworkSection = 0x15;

// The transmit time-out timer is two fields the CPS keeps in step: the duration in seconds, and a
// flag set whenever a duration is programmed. Three CPS saves of the same codeplug - the duration
// at its 60 s default, at 30 s, and at 0 - differ in these bits and in no other byte of the file.
private const int NetworkTxTimerEnabled = 7; // 1 bit
private const int NetworkTxTimerDuration = 52; // 8 bits, seconds

/// <summary>The longest transmission <see cref="TxTimerSeconds"/> can be set to, in seconds: the
/// CPS's own maximum for the field.</summary>
public const int MaxTxTimerSeconds = 250;

/// <summary>True when the codeplug carries the network table (record 0x15/0).</summary>
public bool HasNetworks => HasRecord(NetworkSection, 0);

private byte[] Network => Image.Require(NetworkSection, 0).Data;

/// <summary>
/// How long the radio may transmit before the Tx timer drops the carrier, in seconds (the CPS
/// "Tx Timer Duration", Networks &gt; Basic Settings &gt; Basic Network Settings), for the first
/// network. <b>0 means no time-out at all</b>; <see cref="MaxTxTimerSeconds"/> is the CPS's
/// maximum, and a factory-default codeplug carries 60.
/// </summary>
/// <remarks>
/// The duration is a byte at bit 52 of the network entry. The CPS also clears a flag bit when the
/// duration is zero and sets it whenever one is programmed; writing here keeps the two in step, so
/// the result is byte-identical to the CPS's own save of the same edit.
/// </remarks>
/// <exception cref="ArgumentOutOfRangeException">The value is negative or above
/// <see cref="MaxTxTimerSeconds"/>.</exception>
public int TxTimerSeconds
{
get => (int)GetBits(Network, NetworkTxTimerDuration, 8);
set
{
if (value is < 0 or > MaxTxTimerSeconds)
{
throw new ArgumentOutOfRangeException(
nameof(value), value, $"0..{MaxTxTimerSeconds} s (0 is no time-out)");
}

byte[] p = Network;
SetBits(p, NetworkTxTimerDuration, 8, value);
SetBits(p, NetworkTxTimerEnabled, 1, value == 0 ? 0 : 1);
}
}

// ---- Audio tap block (record 0x3B/0) ------------------------------------------------

// ---- Programmable I/O: digital lines (record 0x37, item 55) ----------------------------------
Expand Down
36 changes: 31 additions & 5 deletions src/M0LTE.Tait.Codeplug/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ CodeplugImage live = programmer.ReadImage();
Composable patches that *upgrade a radio to the Packet.NET feature set* without touching its RF
config (channels, frequencies, power), so they layer safely onto a radio already provisioned for its
environment. They change the data record (0x09), the audio block (0x3B), the digital I/O lines they
program (0x37), the PTT table (0x19) and the three Key Settings items (0x0F, 0x03, 0x18). For a radio
arriving from a foreign application, prefer a clean flash of a full codeplug first, then apply a
profile.
program (0x37), the PTT table (0x19), the three Key Settings items (0x0F, 0x03, 0x18) and the network
table (0x15). For a radio arriving from a foreign application, prefer a clean flash of a full codeplug
first, then apply a profile.

They nest: `audio-and-ptt` is the aux-connector modem wiring, `pdn-basic` is that plus the CCDI
command channel, `pdn-extra` is that plus the FFSK modem, and `pdn-internal` is `pdn-extra` moved onto
Expand All @@ -76,8 +76,10 @@ settings are already right, or one being set up for an external modem without th
rather than Voice from the aux mic - without that last one the line keys the radio but puts the wrong
audio on air. Those three records - 0x19, 0x37 and 0x3B - come out byte-identical to a CPS save of the
same configuration on a default TM8100 codeplug. It never touches the data record. It also programs
the front-panel **F1 key to Squelch Override** (see [Function keys](#function-keys)) - not part of the
wiring, but wanted on every radio these profiles provision, and this is the one they all pass through.
the front-panel **F1 key to Squelch Override** (see [Function keys](#function-keys)) and winds the
**Tx timer out to its 250-second maximum** (see [Tx timer](#tx-timer)) - neither is part of the
wiring, but both are wanted on every radio these profiles provision, and this is the one they all
pass through.
- **`pdn-basic`** is `audio-and-ptt` plus the CCDI command channel that carries `Packet.Radio.Tait`'s
telemetry and control: averaged/instantaneous RSSI, forward/reverse power, PA temperature,
status/identity, transmitter keying, and the PROGRESS stream for carrier-sense (DCD) and external-PTT
Expand Down Expand Up @@ -159,6 +161,30 @@ Squelch Override onto another key.
`GetFunctionKeyRole` / `SetFunctionKeyRole` take a `FunctionKey`; the console names them `key.f1` ..
`key.f4`. `get radio.m8p | grep key.` lists all four; `set radio.m8p key.f1 SquelchOverride` sets one.

## Tx timer

`TxTimerSeconds` is the CPS's **Tx Timer Duration**, Networks > Basic Settings > Basic Network Settings
tab: how long the radio may transmit before the timer drops the carrier. It reads and writes in
**seconds**, `0` means **no time-out at all**, and `MaxTxTimerSeconds` (250) is the CPS's maximum. A
factory-default codeplug carries 60. The console names it `txtimer`.

Item 0x15 is the network table - one entry per network, packed LSB-first, the first starting at bit 0
of record 0x15/0. The timer is two fields of that entry, which the CPS keeps in step:

| field | where | what it holds |
|---|---|---|
| duration | bits 52..59 | the time-out in seconds, `0`..`250` |
| flag | bit 7 | set whenever a duration is programmed, cleared when it is zero |

Writing the duration maintains the flag, so the result is byte-identical to the CPS's own save of the
same edit. Three CPS saves of one TM8100 codeplug (DBVer 0095) - the duration at its 60 s default, at
30 s, and at 0 - differ in this record and in **no other byte of the file**, which is what pins both
fields; `set txtimer` on the first reproduces each of the other two exactly, and restores the first.

Only the **first network** is mapped. A default codeplug carries exactly one, and no capture pins where
the next network's entry starts, so a codeplug with several networks has only its first one read and
written here.

## Status and safety

The read + write path is hardware-validated against a real TM8100 (a same-image write round-tripped
Expand Down
32 changes: 26 additions & 6 deletions tests/M0LTE.Tait.Codeplug.Tests/Fixtures.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,16 +65,35 @@ internal static class Fixtures
/// codeplug carries no 0x18 record at all and an item count of zero.</summary>
public const string SquelchOverrideKeyList = "100000";

// The network table (0x15) as three CPS saves of one TM8100 codeplug (DBVer 0095) hold it - the
// Tx Timer Duration at its 60 s default, wound down to 30 s, and off. Those three files differ in
// this record and in no other byte, which is what makes them a byte-exact target for the timer.

/// <summary>Network table (0x15): one network, Tx timer at the 60 s factory default.</summary>
public const string DefaultNetworkTable =
"800017000008C403E0018218A478A00F0C0080001888130008";

/// <summary>The same table with the Tx timer wound down to 30 seconds.</summary>
public const string TxTimer30sNetworkTable =
"800017000008E401E0018218A478A00F0C0080001888130008";

/// <summary>The same table with the Tx timer at zero - no time-out. The CPS clears its flag bit
/// (byte 0) as well as the duration.</summary>
public const string TxTimerOffNetworkTable =
"0000170000080400E0018218A478A00F0C0080001888130008";

// Item index entries (7 bytes each) for the items the profiles touch: the function table
// (0x03, 14 bits x 65), the key table (0x0F, 20 bits x 4), the programmed-key list (0x18, 22 bits
// x 0), the audio block (0x3B, 95 bits x 4) and the digital line table (0x37, 132 bits x 15), as a
// real readout has them.
// (0x03, 14 bits x 65), the key table (0x0F, 20 bits x 4), the network table (0x15, one network),
// the programmed-key list (0x18, 22 bits x 0), the audio block (0x3B, 95 bits x 4) and the digital
// line table (0x37, 132 bits x 15), as a real readout has them.
private const string ItemIndex =
"030E0041000200" + "0F140004000100" + "18160000000300" + "3B5F0004000900" + "3784000F000600";
"030E0041000200" + "0F140004000100" + "15140101001200" +
"18160000000300" + "3B5F0004000900" + "3784000F000600";

/// <summary>A codeplug carrying every block the PDN profiles write: the data/signalling record,
/// the item index, the PTT table, the audio block, the digital I/O line table, the function table
/// and the front-panel key table. Every block but the data record is the real factory-default one.</summary>
/// the item index, the PTT table, the audio block, the digital I/O line table, the function table,
/// the front-panel key table and the network table. Every block but the data record is the real
/// factory-default one.</summary>
public static CodeplugFields Open(string digitalIoTableHex)
{
var image = new CodeplugImage(
Expand All @@ -83,6 +102,7 @@ [new KeyValuePair<string, string>("DBVer", "0095")],
new CodeplugRecord(0x01, 0, Convert.FromHexString(ItemIndex)),
new CodeplugRecord(0x09, 0, new byte[37]),
new CodeplugRecord(0x0F, 0, Convert.FromHexString(DefaultFunctionKeyTable)),
new CodeplugRecord(0x15, 0, Convert.FromHexString(DefaultNetworkTable)),
new CodeplugRecord(0x19, 0, Convert.FromHexString(DefaultPttTable)),
new CodeplugRecord(0x3B, 0, Convert.FromHexString(DefaultAudioBlock)),
]);
Expand Down
Loading
Loading