Skip to content

Codec API

Tier: Stable Core

Encode and decode Click clipboard binary and program files.

laddercodec.encode

Ladder rung encoder — unified pipeline.

Definitions

Rung

One logical unit of ladder logic. A rung has 1..32 rows, each with 31 condition columns (A..AE) and 1 output column (AF). The rung may also carry a single plain-text comment.

Row

A horizontal slice of the rung grid. Row 0 is the topmost visible row. The grid starts at absolute offset 0x0A60 with a stride of 0x800 per row (32 columns x 64 bytes per cell).

Cell

A 64-byte (0x40) block within the grid. Addressed by (row, column). Contains wire flags, structural control bytes, and (for instruction- bearing rungs) stream-placement metadata.

Wire topology

The arrangement of horizontal wires ("-"), vertical pass-throughs ("|"), and junction-down points ("T") across the condition grid. Encoded via three per-cell flag bytes: +0x19 (segment), +0x1D (right), +0x21 (down). Conditions also set these flags (like "-").

Condition

An instruction placed on a condition column (A..AE). Includes Contact (NO/NC/edge) and CompareContact (GT/GE/LT/LE/EQ/NE). Condition cells are variable-length: a 0x25-byte header (wire flags, column, row, instruction index) followed by an instruction blob (class name, type marker, operand, func_code). Passed as Contact or CompareContact objects in the condition_rows grid.

NOP

The simplest AF-column instruction. At most one per rung. Encoded via a minimal byte model: col31 +0x1D = 1 (all rows), plus col0 +0x15 = 1 for non-first rows. Does not require an instruction stream entry.

Comment

Plain-text annotation on a rung. Stored as an RTF envelope (fixed prefix + cp1252 body + fixed suffix) in the payload region at 0x0298. The 4-byte payload length sits at 0x0294. The cell grid (always at 0x0A60 in a no-payload buffer) is pushed forward by payload_len bytes after insertion. Max 1400 bytes.

Page

A 0x1000 (4096) byte allocation unit. Buffer size is: pad_to_page(0x0A60 + logical_rows * 0x800 + payload_len).

Program header

A single 0x40-byte structure at 0x0254. Contains the row-count word (+0x00/+0x01 = (rows+1)*0x20) and other GUI state. Not a 32-entry table — the range 0x0294–0x0A5F is the payload region.

Supported checklist

Verified in Click (encode → paste → copy back → decode round-trip):

[x] Empty rung, 1..32 rows
[x] Wire topology, 1..32 rows (-, |, T in any valid position)
[x] NOP on any row (with col0 +0x15 enable for non-first rows)
[x] Plain comment, 1-row (empty, wire, NOP, max 1400 bytes)
[x] Plain comment, 2-row (empty, NOP, wire incl. col-A, max 1324)
[x] Plain comment, 3-row (empty, NOP, wire, mixed, max 1400)
[x] Plain comment, 4..32 rows (wire combos, scaling)
[x] Multi-line comment (\n → \par; verified 2026-03-12)
[x] Styled comments (bold/italic/underline via markdown → RTF groups; verified 2026-03-12)
[x] Contacts (NO, NC, edge, immediate — via Contact objects in condition_rows)
[x] Coils (out, latch, reset, immediate, range — via Coil objects in af_tokens)
[x] Comparison contacts (GT, GE, LT, LE, EQ, NE — via CompareContact objects)
[x] Timers (on_delay, off_delay, retentive — via Timer objects in af_tokens)
[ ] Full AF instruction set (counters, math, etc.)

Pipeline steps

1. Header   — load from synthesize_empty_multirow (includes row_word)
2. Grid     — build 32 cell objects per row (wire flags + NOP baked
              in), concatenate to form the grid bytes
3. Assemble — header[:0x0A60] + grid_bytes
4. Comment  — assemble RTF, insert at 0x0298, push grid forward
5. Pad      — to next 0x1000 page boundary

Cell objects are bytes blobs built by ClickCell.to_bytes(). Wire cells are 0x40 bytes. Instruction cells (contacts, coils, timers) are larger, and the concatenation model handles variable-length cells naturally — no fixed-offset assumptions in the grid.

encode_rung

encode_rung(
    logical_rows: int,
    condition_rows: Sequence[Sequence[ConditionToken]],
    af_tokens: Sequence[AfToken],
    comment: str | None = None,
    *,
    show_nicknames: bool = False,
) -> bytes

Encode a ladder rung to binary payload.

Parameters

logical_rows: Number of rung rows (1..32). condition_rows: Row-major token grid. Each row has 31 condition-column entries. Supported: "" blank, "-" horizontal wire, "|" vertical pass-through, "T" junction-down, or a Contact object. af_tokens: One per row. "NOP" encodes the NOP instruction on the AF column; "" leaves it blank; a Coil object encodes the coil instruction. comment: Optional comment text (max 1400 bytes after RTF encoding). Stored as an RTF envelope inserted at 0x0298; the cell grid is pushed forward by payload_len bytes automatically.

Inline styles (markdown syntax):
    ``**text**``  → bold
    ``__text__``  → underline
    ``*text*``    → italic (asterisk)
    ``_text_``    → italic (underscore)
Line breaks: ``\n`` becomes an RTF paragraph break (``\par``).
``\r\n`` and bare ``\r`` are normalized to ``\n`` first.
Returns

bytes Encoded binary payload ready for the target environment.

encode

encode(rungs, *, show_nicknames: bool = False)

Encode one or more rungs to clipboard binary.

Parameters

rungs: A single Rung object (single-rung encode) or a sequence of Rung objects (multi-rung encode). show_nicknames: When True, sets the nickname display flag on math instructions so Click shows project-level tag names instead of raw addresses. The nicknames must already be loaded in the Click project before pasting.

Returns

bytes Encoded binary payload.

laddercodec.decode

Ladder rung decoder — binary clipboard buffer to structured data.

Reads a Click clipboard binary and produces the same data structures that feed encode_rung() / encode_rungs().

Public API

decode_rung(data)        -> Rung
decode_rungs(data)       -> list[Rung]

Round-trip identity:

decode_rung(encode_rung(lr, cr, af, cmt))
    .logical_rows  == lr
    .conditions    == cr
    .instructions  == af
    .comment       == cmt

Instruction cells

Contacts and coils are composite: a horizontal wire ((1,1,0)) with instruction data layered on top. The instruction payload starts at cell offset +0x25 (UTF-16LE class name, type marker, operand, func code). Wire-only cells are exactly 0x40 bytes; instruction cells are larger.

Known instruction types are decoded into Contact (condition columns) or Coil (AF column) domain objects from model.py. Unrecognised cells fall back to UnknownCondition / UnknownInstruction with raw bytes preserved.

DecodeError

Bases: ValueError

Raised when a clipboard binary cannot be decoded.

UnknownCondition dataclass

Instruction cell on a condition column (A..AE).

The wire flags (always (1,1,0) for contacts) are implicit. raw carries the instruction-specific bytes from cell offset +0x25 to the cell boundary.

UnknownInstruction dataclass

Instruction cell on the AF column.

raw carries the instruction-specific bytes from cell offset +0x25 to the cell boundary.

Rung dataclass

Structured rung data — used for both decode output and encode input.

Attributes

logical_rows: Number of rung rows (1..32). conditions: Row-major token grid. Each row has 31 condition-column entries. Wire-only cells are strings ("" blank, "-" horizontal, "|" vertical, "T" junction-down). Contacts are Contact objects; unrecognised cells are UnknownCondition. instructions: One per row. "NOP" or "" for wire-only cells. Coils are Coil objects; unrecognised cells are UnknownInstruction. comment: Markdown text (for CSV export), or None. comment_rtf: Raw RTF payload bytes, or None. Preserves byte-exact fidelity for re-encoding.

CellDump dataclass

Raw byte dump of a single cell, for RE/debugging.

Attributes

rung: Rung index (0-based). row: Visual row within the rung (0-based). col: Column letter ("A".."AE" or "AF"). offset: Absolute byte offset in the buffer. size: Cell size in bytes (0x40 for wire-only, larger for instructions). raw: Raw cell bytes. flags: Flags (segment, right, down) read from +0x19/+0x1D/+0x21. token: Decoded wire/instruction token, or None if not decoded.

hex

hex(cols: int = 16) -> str

Return a formatted hex dump with offset labels.

decode

decode(data: bytes) -> Rung | list[Rung]

Decode a clipboard binary. Returns Rung for single-rung, list[Rung] for multi-rung.

Parameters

data: Raw clipboard bytes (page-aligned, starts with CLICK magic).

Returns

Rung | list[Rung] A single Rung for single-rung buffers, or a list of Rung objects for multi-rung buffers.

Raises

DecodeError If the buffer is invalid.

decode_rung

decode_rung(data: bytes) -> Rung

Decode a single-rung clipboard binary.

Parameters

data: Raw clipboard bytes (page-aligned, starts with CLICK magic).

Returns

Rung Decoded rung data matching encode_rung() input contract.

Raises

DecodeError If the buffer is invalid or contains multiple rungs.

decode_rungs

decode_rungs(data: bytes) -> list[Rung]

Decode a multi-rung clipboard binary.

Parameters

data: Raw clipboard bytes (page-aligned, starts with CLICK magic).

Returns

list[Rung] One entry per rung, in order.

Raises

DecodeError If the buffer is invalid or contains only a single rung.

inspect_cells

inspect_cells(
    data: bytes, cells: list[tuple[int, int, str]]
) -> list[CellDump]

Dump raw bytes for specific cells in a clipboard binary.

Parameters

data: Raw clipboard bytes. cells: List of (rung_index, visual_row, column_letter) tuples. Column is "A".."AE" for conditions or "AF" for output.

Returns

list[CellDump] One entry per requested cell, in the same order as cells.

Example::

dumps = inspect_cells(raw, [(0, 1, "A"), (0, 1, "B")])
for d in dumps:
    print(d)

laddercodec.decode_program

Program file decoder — Scr*.tmp binary to structured Rung data.

Reads Click Programming Software's internal temp files and produces the same Rung objects as the clipboard decoder.

Public API

decode_program(data)  -> Program

The SCR format is compact (~17x smaller than clipboard) and represents the full program as stored on disk. Instruction tag IDs and operand values are identical to clipboard format — only the framing differs.

decode_program

decode_program(data: bytes) -> Program

Decode an SC-SCR temp file into a Program.

Parameters

data: Raw bytes of a Scr*.tmp file (starts with SC-SCR magic).

Returns

Program Program with name, prog_idx, and rungs parsed from the file.

Raises

ValueError If the file cannot be parsed.

laddercodec.Rung dataclass

Structured rung data — used for both decode output and encode input.

Attributes

logical_rows: Number of rung rows (1..32). conditions: Row-major token grid. Each row has 31 condition-column entries. Wire-only cells are strings ("" blank, "-" horizontal, "|" vertical, "T" junction-down). Contacts are Contact objects; unrecognised cells are UnknownCondition. instructions: One per row. "NOP" or "" for wire-only cells. Coils are Coil objects; unrecognised cells are UnknownInstruction. comment: Markdown text (for CSV export), or None. comment_rtf: Raw RTF payload bytes, or None. Preserves byte-exact fidelity for re-encoding.

laddercodec.Program dataclass

A single PLC program (main or subroutine) with its rungs.

laddercodec.Project dataclass

A complete PLC project: main program plus subroutines.