Punter C1 Protocol Reference
Punter C1 (“New Punter”) is the file-transfer protocol that CCGMS, Novaterm, and StrikeTerm speak natively on Commodore 64/128 BBSes — the one you reach for when the peer is a real C64.
Punter predates and is unrelated to the XMODEM family. Where XMODEM
sends a fixed 128-byte block and waits for a single
ACK/NAK, Punter C1 is built around a
three-token handshake per block
(GOO → ACK → S/B → block),
two independent per-block checksums, and a
two-phase transfer: a short type block that
announces the Commodore file type, then the data blocks
themselves. Each block also carries the size of the next block,
so the receiver always knows exactly how many bytes to read.
The gateway implements single-file C1 in src/punter.rs. It is
a clean-room Rust implementation written from Michael Steil's C1 wire
description (pagetable.com) and cross-checked against the Novaterm
9.6 / v10 punter.src 6502 source for the corners the prose
spec leaves ambiguous — the checksum loop bounds, the handshake
direction, and the end-off sequence in particular.
punter_hangup_on_failure under Tunables.
Every Punter C1 block is 7 to 255 bytes: a 7-byte header followed by up to 248 payload bytes.
offset 0 additive checksum (2 bytes, little-endian)
offset 2 cyclic checksum (2 bytes, little-endian)
offset 4 size of NEXT block (1 byte)
offset 5 block index (2 bytes, little-endian; high byte 0xFF = final)
offset 7 payload (0–248 bytes)
| Field | Offset | Size | Meaning |
|---|---|---|---|
| Additive checksum | 0 | 2 | 16-bit additive checksum of bytes 4…end (see Checksums). |
| Cyclic checksum | 2 | 2 | 16-bit cyclic (XOR + rotate) checksum of bytes 4…end. |
| Next-block size | 4 | 1 | Total length of the next block on the wire — how the receiver knows how many bytes to read for the next read. |
| Block index | 5 | 2 | Sequence number, little-endian. A high byte of 0xFF flags the final block of the phase. |
| Payload | 7 | 0–248 | File data (data phase) or the single file-type byte (type phase). |
Both checksums cover the block from offset 4 to end-of-block
— they deliberately skip themselves (the four checksum bytes).
Verified against the checksum routine in
punter.src: the loop runs from sizepos (offset 4)
up to the block's own length.
C1 protects every block with two independent 16-bit checksums computed over the same bytes — a corrupt block almost never satisfies both.
A plain wrapping 16-bit sum of the covered bytes. On the
6502 this is the clc / adc / bcc / inc sequence with carry
into the high byte; no carry ever leaves the 16-bit accumulator, so it is
exactly a 16-bit add that wraps on overflow.
Per byte: XOR the byte into the low byte of the 16-bit
accumulator, then rotate the whole 16-bit accumulator left by one
bit, circularly (bit 15 feeds back into bit 0). The
rotate happens after the XOR. This is the
eor-then-rol/rol pattern in the asm.
The reference implementation, byte for byte:
additive = 0
cyclic = 0
for b in bytes[4..end]:
additive = (additive + b) & 0xFFFF # wrapping add
cyclic = cyclic XOR b # XOR into low byte
cyclic = rotate_left_16(cyclic, 1) # circular, post-XOR
Both results are written little-endian into the block header
(additive at offset 0, cyclic at offset 2). On receive, the gateway
recomputes both over the received bytes 4…end and compares; a
mismatch on either checksum makes the block bad and triggers a
BAD resend request (see Recovery).
Five three-byte ASCII tokens carry all the control signalling. They travel UPPERCASE on the wire.
| Code | Bytes | Sent by | Meaning |
|---|---|---|---|
GOO | 47 4F 4F | Receiver | “Good — send the next block.” Also the per-block go-ahead. |
BAD | 42 41 44 | Receiver | “That block was corrupt — resend the same block.” |
ACK | 41 43 4B | Sender | Acknowledges the receiver's GOO/BAD. |
S/B | 53 2F 42 | Receiver | “Send Block” — the final go-ahead before the block bytes flow. |
SYN | 53 59 4E | Both | Synchronisation token exchanged during the end-off. |
.asc "goobadacks/bsyn", but a real C64 assembles that in the
default uppercase/graphics PETSCII charset, so the bytes that actually
leave the machine are 0x47 0x4F 0x4F… The gateway
therefore sends uppercase so real C64 peers recognise it,
and matches case-insensitively on receive so a peer that
happens to send lowercase still works. (The / in
S/B is 0x2F, unaffected by case.)
The asm is authoritative on direction (some prose write-ups get it
backwards): the receiver drives with
GOO/BAD and S/B; the
sender answers ACK and then transmits the
block. One data block looks like:
receiver -> GOO (or BAD to demand a resend of the same block)
sender -> ACK
receiver -> S/B
sender -> <block bytes>
A C1 transfer runs in two phases: a one-block type phase, then the data phase.
A single 8-byte block whose lone payload byte is the Commodore file type. It tells the receiver what kind of file is coming so the byte stream can be reconstructed faithfully on a CBM filesystem.
| Byte | Type | Meaning |
|---|---|---|
0 | PRG | Load-address-prefixed Commodore program. |
1 | SEQ | Flat sequential file. |
2 | USR | Flat user-defined file. |
3 | — | Unknown / none. |
Linux has no CBM directory entry to carry this, so the gateway maps it to
the filename extension. On send, the outbound type is
auto-detected from the extension (.prg / .seq /
.usr). On receive, the matching extension is
appended when the saved filename lacks one — preserving the
round-trip. A type byte outside 0..=3 is treated as
Unknown (left without a suffix) rather than silently
coerced to SEQ.
The file payload, carried in blocks of up to 248 data bytes each. The
first data block is the fixed 7-byte header-only block; every subsequent
block's length is announced by the previous block's
next-block size field. The final data block is
flagged by setting the block index's high byte to 0xFF, after
which the two sides run the end-off.
You pull a file from the gateway to your Commodore terminal. The gateway sends; your terminal's receiver drives with GOO / BAD / S/B.
# Phase A — type block
term <- GOO receiver ready
gateway -> ACK
term <- S/B
gateway -> [type block: 1 byte = PRG/SEQ/USR]
# Phase B — data blocks (repeat per block)
term <- GOO (BAD = resend the last block)
gateway -> ACK
term <- S/B
gateway -> [data block, up to 248 bytes] ... until the 0xFF-flagged final block
# End-off
both <-> SYN handshake
gateway -> S/B S/B S/B sender obligation (CCGMS needs all three)
GOO (up to punter_negotiation_timeout, default 45 s), then answers ACK; the receiver sends S/B and the gateway transmits the type block.GOO, the gateway answers ACK, the receiver sends S/B, and the gateway sends the block (header + up to 248 payload bytes, with both checksums and the next-block size filled in).BAD instead of GOO; the gateway re-sends the same block. There is no resend cap on a real C64 peer, so the gateway tolerates up to punter_max_bad_rounds (default 30) consecutive corrupt-block rounds before giving up.0xFF.SYN exchange, after which the gateway sends S/B three times. A real CCGMS receiver stalls if fewer than three arrive, so this is a firm sender obligation.You send a file from your Commodore terminal to the gateway. The gateway receives, so it drives with GOO / BAD / S/B.
# Phase A — type block
gateway -> GOO receiver ready
term <- ACK
gateway -> S/B
term <- [type block: 1 byte = PRG/SEQ/USR]
# Phase B — data blocks (repeat per block)
gateway -> GOO (BAD if the block failed a checksum)
term <- ACK
gateway -> S/B
term <- [data block] ... until the 0xFF-flagged final block
# End-off
both <-> SYN handshake (tolerant best-effort)
GOO; the sender answers ACK; the gateway sends S/B and reads the fixed 8-byte type block, recording the file type (PRG / SEQ / USR / Unknown).GOO, the sender answers ACK, the gateway sends S/B, and the sender transmits the block. The gateway reads exactly the number of bytes announced by the previous block's next-block-size field.BAD and the sender re-sends the same block; a good block clears the count. Up to punter_max_bad_rounds (default 30) consecutive bad rounds are tolerated before giving up.0xFF arrives, the gateway has the final block. It runs a tolerant best-effort end-off (ACK the final block, exchange SYN/S/B), sliding past any extra S/B the peer sends..prg/.seq/.usr extension appended if the type was known and the name had none.After the final (0xFF-flagged) block, both ends run a closing handshake. Getting it exactly right is what keeps CCGMS from hanging.
The end-off is a SYN exchange followed by the
sender transmitting S/B three times. This is
a sender obligation: a real CCGMS receiver stalls (the classic
“Downloading…” hang) if fewer than three
S/B arrive after the final block.
GOO twice after the type block — the block-ack
GOO and a second GOO before S/B —
and each needs its own ACK. The gateway's sender re-sends
ACK on a short cadence until S/B arrives; a single
slow ACK would strand CCGMS looping GOO forever.
S/B rule with fixed drain-reads.
It runs a tolerant best-effort re-handshake and lets its resync window
harmlessly slide past any extra S/B. Blind drain-reads would
swallow the opening signal of whatever comes next.
| Situation | What happens |
|---|---|
| Block fails a checksum (either additive or cyclic) | The receiver answers BAD instead of GOO; the sender re-sends the same block. Recovery is by resend, not by position or sequence skip. |
| Many consecutive corrupt blocks | Bounded by punter_max_bad_rounds (default 30) — the count of consecutive corrupt-block rounds tolerated before giving up. This is deliberately larger than punter_max_retries, because a real C64 peer never caps its own resends; a low cap would make the gateway quit first and strand the peer. |
| Stray / duplicated handshake code | accept_code's sliding resync window slides past it, so an extra ACK or S/B doesn't desync the conversation. |
| Silent peer (no response within the timeout) | Bounded waits (punter_negotiation_timeout for the opening handshake, punter_block_timeout per block) end the transfer rather than hanging forever. C1 has no “cancel” message, so a give-up is just a stop. |
| Give-up while a C64 is still waiting | If punter_hangup_on_failure is enabled, the gateway drops carrier so a stranded C64 sees loss-of-carrier instead of hanging. Off by default. |
| File exceeds 8 MB | The transfer is aborted — the same cap shared across XMODEM / YMODEM / ZMODEM / Kermit / Punter. |
max_retries vs. max_bad_rounds.
These are intentionally separate. punter_max_retries bounds
handshake-level retries (waiting for an expected code); the larger
punter_max_bad_rounds bounds how many corrupt-block resend
rounds the gateway will sit through before concluding the link is
hopeless. The earlier implementations conflated the two at a low cap,
which made the gateway give up while a healthy C64 peer was still happily
resending.
Punter has its own independent settings (separate from the XMODEM and ZMODEM families). Editable from the telnet Configuration → File Transfer menu, the GUI / web File Transfer popup, or egateway.conf — changes apply on the next transfer, no restart.
| Key | Default | Range / Meaning |
|---|---|---|
punter_block_size | 255 | 8–255 bytes total block size. Lower toward ~40 on a noisy line so a corrupt block costs fewer bytes to resend. |
punter_negotiation_timeout | 45 s | 1–300 s. Time to complete the opening (type-block) handshake. |
punter_negotiation_retry_interval | 5 s | 1–60 s. Gap between handshake re-sends during negotiation. |
punter_block_timeout | 20 s | 1–120 s. Per-block read timeout once the transfer is live. |
punter_max_retries | 10 | 1–100. Handshake-level retry cap (waiting for an expected code). |
punter_max_bad_rounds | 30 | Consecutive corrupt-block resend rounds tolerated before giving up — deliberately higher than max_retries (a real C64 peer never caps resends). |
punter_hangup_on_failure | false | Drop carrier on a give-up so a stranded C64 sees loss-of-carrier (C1 has no in-band abort). |
Deep-dive reference pages for every protocol and interface, plus the character-set tables and ANSI escape-sequence reference.
128-byte blocks; CRC-16 / checksum negotiation.
Block-0 metadata, batch, exact size truncation.
Streaming; ZDLE, CRC-32, autostart, resume.
Send-Init negotiation; F / A / D / Z / B transfer.
C1 dual checksum, two-phase, GOO/BAD/ACK.
Hayes command set, S-registers, +++ escape.
IAC negotiation, every option, the NVT data phase.
Server, gateway, host keys, TOFU, auth modes.
Hex tables for every encoding: ASCII, ANSI, PETSCII, ATASCII, Baudot/ITA2, ZX Spectrum, TRS-80.
Cursor, colour/SGR, erase, and screen-mode escape codes, with the raw hex bytes.