forked from arti/vt1200
131 lines
5.7 KiB
Python
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:]
|