{
  "title": "SSDV packet reference",
  "statusDate": "2026-09-01",
  "packetLengthBytes": 256,
  "normal": {
    "packetType": "0x66",
    "label": "Normal FEC packet",
    "payloadBytes": 205,
    "crcBytes": 4,
    "fecBytes": 32,
    "payloadRange": "15-219",
    "crcRange": "220-223",
    "fecRange": "224-255",
    "protectedRange": "1-223",
    "errorCorrection": "Reed-Solomon parity can correct up to 16 byte errors when byte positions are preserved."
  },
  "noFEC": {
    "packetType": "0x67",
    "label": "No-FEC packet",
    "payloadBytes": 237,
    "crcBytes": 4,
    "fecBytes": 0,
    "payloadRange": "15-251",
    "crcRange": "252-255",
    "fecRange": "none",
    "protectedRange": "1-251",
    "errorCorrection": "CRC detects corruption, but no Reed-Solomon parity is appended."
  },
  "packetFields": [
    {
      "offset": 0,
      "size": 1,
      "name": "Sync byte",
      "value": "0x55",
      "description": "Packet marker. A stream may contain extra sync bytes before a packet."
    },
    {
      "offset": 1,
      "size": 1,
      "name": "Packet type",
      "value": "0x66 or 0x67",
      "description": "0x66 identifies a normal packet with Reed-Solomon parity; 0x67 identifies the no-FEC layout."
    },
    {
      "offset": 2,
      "size": 4,
      "name": "Callsign",
      "value": "Base-40",
      "description": "Base-40 encoded callsign, normally derived from an alphanumeric callsign up to six characters."
    },
    {
      "offset": 6,
      "size": 1,
      "name": "Image ID",
      "value": "0-255",
      "description": "Sender-selected image identifier. Receivers use it with the callsign to separate one image from the next."
    },
    {
      "offset": 7,
      "size": 2,
      "name": "Packet ID",
      "value": "big-endian",
      "description": "Packet sequence number for the current image, beginning at 0."
    },
    {
      "offset": 9,
      "size": 1,
      "name": "Width",
      "value": "pixels / 16",
      "description": "Image width in MCU block units. A zero value is invalid."
    },
    {
      "offset": 10,
      "size": 1,
      "name": "Height",
      "value": "pixels / 16",
      "description": "Image height in MCU block units. A zero value is invalid."
    },
    {
      "offset": 11,
      "size": 1,
      "name": "Flags",
      "value": "00qqqexx",
      "description": "Quality, end-of-image, and chroma subsampling flags."
    },
    {
      "offset": 12,
      "size": 1,
      "name": "MCU offset",
      "value": "0-204 or 0xFF",
      "description": "Byte offset inside the payload where the first complete MCU begins, or 0xFF when no complete MCU starts in this packet."
    },
    {
      "offset": 13,
      "size": 2,
      "name": "MCU index",
      "value": "big-endian or 0xFFFF",
      "description": "Index of the MCU pointed to by MCU offset, or 0xFFFF when no complete MCU starts in this packet."
    },
    {
      "offset": 15,
      "size": 205,
      "name": "Payload",
      "value": "JPEG scan data",
      "description": "Normal-mode payload bytes. No-FEC packets extend the payload to byte 251."
    },
    {
      "offset": 220,
      "size": 4,
      "name": "CRC32 checksum",
      "value": "big-endian",
      "description": "CRC over the packet type through the end of the payload in the active layout."
    },
    {
      "offset": 224,
      "size": 32,
      "name": "Reed-Solomon parity",
      "value": "normal mode only",
      "description": "Parity bytes for normal packets. Not present in no-FEC packets."
    }
  ],
  "flagBits": [
    {
      "bits": "7-6",
      "name": "Reserved",
      "meaning": "Reserved bits, normally zero."
    },
    {
      "bits": "5-3",
      "name": "JPEG quality",
      "meaning": "Three-bit quality field stored as quality XOR 4, representing levels 0-7."
    },
    {
      "bits": "2",
      "name": "EOI",
      "meaning": "End-of-image flag, set only on the last packet for the image."
    },
    {
      "bits": "1-0",
      "name": "Subsampling mode",
      "meaning": "Published mode values identify 2x2, 1x2, 2x1, or 1x1 MCU/chroma layouts."
    }
  ],
  "imageConstraints": [
    "Input is JPEG scan data in an abbreviated JPEG model rather than a normal self-contained JPEG stream.",
    "Colour images use YCbCr; grayscale images are also supported by the reference encoder.",
    "Width and height are multiples of 16 pixels, with practical limits imposed by one-byte MCU-block dimensions.",
    "Baseline DCT is used; progressive JPEG and arbitrary custom JPEG tables are outside the checked reference profile.",
    "Quantisation and Huffman tables are fixed so the packet stream can omit normal JPEG table headers.",
    "The total MCU count is limited to 65535."
  ],
  "packetLayouts": [
    {
      "layout": "Normal FEC",
      "type": "0x66",
      "header": "15 bytes",
      "payload": "205 bytes",
      "crc": "4 bytes at 220-223",
      "fec": "32 bytes at 224-255",
      "typicalUse": "Unreliable RF links where byte errors are expected but framing can usually be recovered."
    },
    {
      "layout": "No-FEC",
      "type": "0x67",
      "header": "15 bytes",
      "payload": "237 bytes",
      "crc": "4 bytes at 252-255",
      "fec": "none",
      "typicalUse": "Links with their own robust FEC layer, or controlled fixtures where packet overhead matters."
    }
  ],
  "algorithmStages": [
    {
      "stage": "JPEG preparation",
      "encode": "Accept a baseline JPEG profile that can be represented with fixed quantisation and Huffman tables, then work with the scan data.",
      "decode": "Reconstruct a legal JPEG by writing the expected headers and fixed tables before emitting decoded scan data."
    },
    {
      "stage": "Packet identity",
      "encode": "Write callsign, image ID, packet ID, dimensions, quality, subsampling, and EOI state into each packet.",
      "decode": "Reject packets whose callsign, image ID, dimensions, quality, or subsampling do not match the current image session."
    },
    {
      "stage": "MCU resynchronisation",
      "encode": "Record the first complete MCU start inside the payload so a receiver can resume after a lost packet.",
      "decode": "Use MCU offset and MCU index to skip unusable bytes, close an incomplete MCU, and resume from the next aligned block."
    },
    {
      "stage": "Integrity check",
      "encode": "Write CRC32 over the active packet body.",
      "decode": "Validate CRC before accepting a no-FEC packet, or after Reed-Solomon correction for a normal packet."
    },
    {
      "stage": "Forward error correction",
      "encode": "For normal packets, append 32 Reed-Solomon parity bytes calculated over bytes 1-223.",
      "decode": "Correct byte errors where the receiver still knows the byte positions; insert padding before FEC when a demodulator detects dropped bytes."
    },
    {
      "stage": "Transport framing",
      "encode": "Send fixed-length packets over a modem or frame layer such as RTTY, AX.25, KISS, or mission-specific satellite downlink framing.",
      "decode": "Find packet boundaries, repair byte errors where possible, merge packets by callsign and image ID, and finalize the JPEG when EOI or end-of-stream is reached."
    }
  ],
  "transportProfiles": [
    {
      "name": "8-bit RTTY",
      "role": "Historical UKHAS balloon practice",
      "notes": "The UKHAS guide describes the implementation as RTTY at various baud rates and notes that better transports may be preferable."
    },
    {
      "name": "AX.25 / KISS packet",
      "role": "Amateur packet-radio carriage",
      "notes": "SSDV packets can be placed inside packet-radio frames when the modem/framing layer supplies reliable packet boundaries."
    },
    {
      "name": "Bell 202 AFSK 1200",
      "role": "Audio-path packet experiments",
      "notes": "Useful for sound-card tests and simple amateur packet links, usually with AX.25 framing around the SSDV bytes."
    },
    {
      "name": "9600 FSK/GFSK",
      "role": "Satellite-style packet downlinks",
      "notes": "Higher symbol rate paths need tighter frequency control and more careful demodulator recovery than basic AFSK audio."
    },
    {
      "name": "Mission FEC containers",
      "role": "Satellite payload-specific streams",
      "notes": "Some spacecraft wrap image packets in AO-40/FUNcube-style, IL2P, or other mission-specific telemetry/FEC framing before the SSDV layer is visible."
    }
  ],
  "sources": [
    {
      "title": "UKHAS SSDV guide",
      "url": "https://ukhas.org.uk/doku.php?id=guides:ssdv",
      "notes": "Packet offsets, JPEG constraints, Reed-Solomon notes, lost-packet behaviour, and historical update notes."
    },
    {
      "title": "fsphil/ssdv reference encoder",
      "url": "https://github.com/fsphil/ssdv",
      "notes": "Reference command-line encoder/decoder and README. The GitHub copy is archived and points to Codeberg as the continuing home."
    },
    {
      "title": "fsphil/ssdv header definitions",
      "url": "https://github.com/fsphil/ssdv/blob/master/ssdv.h",
      "notes": "Packet-size constants, header length, CRC length, Reed-Solomon length, callsign limit, packet types, and JPEG processor state."
    },
    {
      "title": "SSDV Habhub API documentation",
      "url": "https://ssdv.habhub.org/about.php",
      "notes": "Public packet field table and normal/no-FEC offset summary."
    },
    {
      "title": "SSDVKit local Swift package",
      "url": "https://sstv.org.uk/docs/ssdv/algorithm/",
      "notes": "Local Swift implementation notes are used only as implementation context; upstream protocol documents remain the public authority."
    }
  ]
}
