Files
vt1200/harness/udpstream/tiledict.py
T

131 lines
5.7 KiB
Python

"""Chunked upload of the 32x32 tile dictionary.
The dictionary is 256 entries of 128 bytes -- 32 KB -- and it does not fit in
one datagram. :func:`dict_chunks` splits it into independently framed
``KIND_DICT_CHUNK`` packets, each carrying at most :data:`MAX_PAYLOAD` payload
bytes: a 2-byte little-endian **offset** followed by a slice of the dictionary.
[ 16-byte header ][ offset u16 ][ dictionary bytes ... ]
The returned packets are frame buffers (header plus payload). Wrapping one in
an Ethernet frame is the caller's job, exactly as for
:meth:`harness.udpstream.vidsrc.StripSource.encode`, and the helpers below take
that frame buffer, not the padded Ethernet envelope.
The offset is the dictionary position of the first body byte, so a receiver
can detect a gap, a duplicate or a reordered packet without keeping a stream
state machine (Task 3 requires ascending offsets with no gaps). A 32 KB
dictionary needs 23 packets: 22 of 1,448 body bytes (31,856 bytes) and a
912-byte tail.
Every packet has its own frame header and therefore its own header CRC; the
CRC covers header bytes 0..11 only (ruling R14), exactly as for a strip frame.
There is no body checksum -- the Ethernet FCS of each datagram is the check for
the payload bytes.
``rows`` and ``stretch`` are zero because a dictionary chunk has no geometry,
and ``frame_no`` is zero because an upload is not one of the video's numbered
frames. The ``frame_no`` field is not what orders the chunks; the offset is.
The payload's ``frame_parse.h`` mirrors :data:`harness.udpstream.frame.KINDS`,
which is where ``KIND_DICT_CHUNK`` is defined.
Pure standard library.
"""
import struct
from harness.udpstream.frame import (HEADER_SIZE, KIND_DICT_CHUNK, MAGIC,
VERSION, pack_header, unpack_header)
__all__ = [
"MAX_BODY", "MAX_DICT_BYTES", "MAX_PAYLOAD", "OFFSET_SIZE",
"dict_chunks", "payload_body", "payload_offset",
]
#: Largest chunk payload in one packet, offset included. The same 1450-byte
#: budget as a strip datagram's chunk (:data:`harness.udpstream.vidsrc.CHUNK`).
MAX_PAYLOAD = 1450
#: The offset field: a u16 little-endian dictionary position.
OFFSET_SIZE = 2
#: Most dictionary bytes one packet can carry after its offset.
MAX_BODY = MAX_PAYLOAD - OFFSET_SIZE # 1,448
#: A u16 offset can name positions 0..65,535, so a dictionary ending at
#: offset 65,536 is still addressable: its last chunk's offset fits.
MAX_DICT_BYTES = 0x10000
_OFFSET = struct.Struct("<H")
def _dict_payload(packet: bytes) -> bytes:
"""Return a dictionary chunk's payload, or raise ValueError.
Validates what the helpers rely on: a whole ``VS`` frame of the right
version, the dictionary-chunk kind, a length that matches the packet, and
room for the offset. The header CRC is *not* rejected here -- a caller
that wants to count bad headers can read ``unpack_header`` itself -- but a
truncated or foreign packet is an error rather than a silent misread.
"""
try:
header = unpack_header(packet)
except ValueError as exc:
raise ValueError("not a dictionary chunk: %s" % exc)
if header["magic"] != MAGIC or header["version"] != VERSION:
raise ValueError("not a VS frame (magic %r, version %d)"
% (header["magic"], header["version"]))
if header["kind"] != KIND_DICT_CHUNK:
raise ValueError("kind %d is not a dictionary chunk (%d)"
% (header["kind"], KIND_DICT_CHUNK))
if len(packet) != HEADER_SIZE + header["payload_len"]:
raise ValueError("packet is %d bytes but the header describes %d"
% (len(packet), HEADER_SIZE + header["payload_len"]))
if header["payload_len"] < OFFSET_SIZE:
raise ValueError("a dictionary chunk payload is at least %d bytes, "
"got %d" % (OFFSET_SIZE, header["payload_len"]))
return packet[HEADER_SIZE:HEADER_SIZE + header["payload_len"]]
def dict_chunks(dictionary: bytes, offset_base: int = 0) -> list[bytes]:
"""Split ``dictionary`` into framed ``KIND_DICT_CHUNK`` packets.
``offset_base`` is added to every offset, for placing the upload at a
non-zero position in the receiver's buffer. The dictionary must be
non-empty and must end within the u16 offset space; those are errors here
rather than a wrap on the wire.
"""
if not isinstance(dictionary, (bytes, bytearray, memoryview)):
raise TypeError("a dictionary is bytes-like, got %s"
% type(dictionary).__name__)
data = bytes(dictionary)
if not data:
raise ValueError("a dictionary upload needs at least one byte")
if (not isinstance(offset_base, int) or isinstance(offset_base, bool)
or not 0 <= offset_base <= 0xFFFF):
raise ValueError("offset_base is 0..65535, got %r" % (offset_base,))
end = offset_base + len(data)
if end > MAX_DICT_BYTES:
raise ValueError("a %d-byte dictionary at offset %d ends at %d, past "
"the u16 offset space (%d)"
% (len(data), offset_base, end, MAX_DICT_BYTES))
packets = []
for start in range(0, len(data), MAX_BODY):
body = data[start:start + MAX_BODY]
payload = _OFFSET.pack(offset_base + start) + body
packets.append(pack_header(KIND_DICT_CHUNK, 0, len(payload), 0, 0,
payload=payload) + payload)
return packets
def payload_offset(packet: bytes) -> int:
"""Dictionary position of the first body byte in ``packet``."""
return _OFFSET.unpack_from(_dict_payload(packet), 0)[0]
def payload_body(packet: bytes) -> bytes:
"""The dictionary bytes ``packet`` carries, offset excluded."""
return _dict_payload(packet)[OFFSET_SIZE:]