Overview

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 (GOOACKS/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.

Which protocol should I use? Punter is the right choice only when the other end is a Commodore terminal that speaks it (CCGMS, Novaterm, StrikeTerm). For modern terminals prefer ZMODEM; for the simplest possible transfer or the oldest hardware, XMODEM; for a 7-bit-clean link, Kermit. Punter is single-file — one file per transfer, no batch.
No in-band abort. The C1 wire protocol has no message that means “cancel.” A peer that stops responding simply times out. The gateway's only way to signal a hard failure back to a stranded C64 is to drop carrier — see punter_hangup_on_failure under Tunables.

Block Format

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)
FieldOffsetSizeMeaning
Additive checksum0216-bit additive checksum of bytes 4…end (see Checksums).
Cyclic checksum2216-bit cyclic (XOR + rotate) checksum of bytes 4…end.
Next-block size41Total length of the next block on the wire — how the receiver knows how many bytes to read for the next read.
Block index52Sequence number, little-endian. A high byte of 0xFF flags the final block of the phase.
Payload70–248File 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.

The “size of next block” trick. Because each block announces the length of the one after it, the receiver never has to guess or frame on a delimiter. That leaves one bootstrap problem: the first block of each phase has no predecessor to size it. C1 solves this with fixed, a-priori-known first-block lengths — 7 bytes (header only, no payload) for the first data block, and 8 bytes (header + one type byte) for the type block.

The Two Checksums

C1 protects every block with two independent 16-bit checksums computed over the same bytes — a corrupt block almost never satisfies both.

Additive

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.

Cyclic

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).


Handshake Codes

Five three-byte ASCII tokens carry all the control signalling. They travel UPPERCASE on the wire.

CodeBytesSent byMeaning
GOO47 4F 4FReceiver“Good — send the next block.” Also the per-block go-ahead.
BAD42 41 44Receiver“That block was corrupt — resend the same block.”
ACK41 43 4BSenderAcknowledges the receiver's GOO/BAD.
S/B53 2F 42Receiver“Send Block” — the final go-ahead before the block bytes flow.
SYN53 59 4EBothSynchronisation token exchanged during the end-off.
Why uppercase? Novaterm's source renders the codes as .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.)
Per-block exchange

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>

Two Phases & File Types

A C1 transfer runs in two phases: a one-block type phase, then the data phase.

Phase A — type block

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.

ByteTypeMeaning
0PRGLoad-address-prefixed Commodore program.
1SEQFlat sequential file.
2USRFlat user-defined file.
3Unknown / 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.

Phase B — data blocks

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.


Download — Gateway as Sender

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)

Step by step

  1. From the File Transfer menu press D, choose the file, then press P for Punter. The outbound file type (PRG / SEQ / USR) is auto-detected from the filename extension.
  2. The gateway waits for the receiver's opening GOO (up to punter_negotiation_timeout, default 45 s), then answers ACK; the receiver sends S/B and the gateway transmits the type block.
  3. For each data block the receiver sends 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).
  4. If a block arrives corrupt the receiver sends 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.
  5. The last block is flagged by setting its block-index high byte to 0xFF.
  6. The two sides run the end-off: a 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.

Upload — Gateway as Receiver

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)

Step by step

  1. Press U, enter the filename (max 64 chars; no path traversal), confirm overwrite if needed, then press P for Punter.
  2. The gateway sends the opening 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).
  3. For each data block the gateway sends 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.
  4. The gateway recomputes both checksums over bytes 4…end. If either fails it answers 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.
  5. When a block whose index high byte is 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.
  6. The file is saved under the name you entered, with a .prg/.seq/.usr extension appended if the type was known and the name had none.

The End-Off

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.

The double-GOO after the type block. CCGMS sends 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.
Receiver tolerance. The gateway's receiver does not mirror the three-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.

Error Recovery
SituationWhat 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 blocksBounded 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 codeaccept_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 waitingIf 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 MBThe 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.

Tunables

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.

KeyDefaultRange / Meaning
punter_block_size2558–255 bytes total block size. Lower toward ~40 on a noisy line so a corrupt block costs fewer bytes to resend.
punter_negotiation_timeout45 s1–300 s. Time to complete the opening (type-block) handshake.
punter_negotiation_retry_interval5 s1–60 s. Gap between handshake re-sends during negotiation.
punter_block_timeout20 s1–120 s. Per-block read timeout once the transfer is live.
punter_max_retries101–100. Handshake-level retry cap (waiting for an expected code).
punter_max_bad_rounds30Consecutive corrupt-block resend rounds tolerated before giving up — deliberately higher than max_retries (a real C64 peer never caps resends).
punter_hangup_on_failurefalseDrop carrier on a give-up so a stranded C64 sees loss-of-carrier (C1 has no in-band abort).
See also: the XMODEM, YMODEM, and ZMODEM references for the other transfer protocols, the File Transfer section of the User Manual for the menu walkthrough, and the Kermit Reference for the 7-bit-clean alternative. Punter is the right choice when the peer is a real Commodore terminal (CCGMS, Novaterm, StrikeTerm).

References

Deep-dive reference pages for every protocol and interface, plus the character-set tables and ANSI escape-sequence reference.

XMODEM

128-byte blocks; CRC-16 / checksum negotiation.

YMODEM

Block-0 metadata, batch, exact size truncation.

ZMODEM

Streaming; ZDLE, CRC-32, autostart, resume.

Kermit

Send-Init negotiation; F / A / D / Z / B transfer.

Punter (this page)

C1 dual checksum, two-phase, GOO/BAD/ACK.

AT Commands

Hayes command set, S-registers, +++ escape.

Telnet

IAC negotiation, every option, the NVT data phase.

SSH

Server, gateway, host keys, TOFU, auth modes.

Character Code Tables

Hex tables for every encoding: ASCII, ANSI, PETSCII, ATASCII, Baudot/ITA2, ZX Spectrum, TRS-80.

ANSI Escape Sequences

Cursor, colour/SGR, erase, and screen-mode escape codes, with the raw hex bytes.