from dataclasses import dataclass, field from typing import List, Optional def decode_hamming_8_4(byte_val): # Extract data bits: bits 1, 3, 5, 7 return ((byte_val >> 1) & 1) | \ (((byte_val >> 3) & 1) << 1) | \ (((byte_val >> 5) & 1) << 2) | \ (((byte_val >> 7) & 1) << 3) @dataclass class Packet: """ Represents a single Teletext packet (Row). In T42, we have 42 bytes: Byte 0-1: Magazine and Row Address (Hamming 8/4 encoding) Byte 2-41: Data bytes (40 bytes) """ original_data: bytes magazine: int = field(init=False) row: int = field(init=False) data: bytearray = field(init=False) def __post_init__(self): if len(self.original_data) != 42: raise ValueError(f"Packet must be 42 bytes, got {len(self.original_data)}") # Parse Magazine and Row from the first 2 bytes (Hamming 8/4) # MRAG (Magazine + Row Address Group) # Byte 0: P1 D1 P2 D2 P3 D3 P4 D4 # Byte 1: P1 D1 P2 D2 P3 D3 P4 D4 b1 = self.original_data[0] b2 = self.original_data[1] # De-interleave Hamming bits to get M (3 bits) and R (5 bits) d1 = decode_hamming_8_4(b1) d2 = decode_hamming_8_4(b2) # Magazine is 3 bits (Logic is specific: Mag 8 is encoded as 0) # Row is 5 bits. # According to Spec (ETSI EN 300 706): # b1 encoded: M1 M2 M3 R1 # b2 encoded: R2 R3 R4 R5 self.magazine = (d1 & 0b0111) if self.magazine == 0: self.magazine = 8 row_low_bit = (d1 >> 3) & 1 row_high_bits = d2 self.row = (row_high_bits << 1) | row_low_bit self.data = bytearray(self.original_data[2:]) @property def is_header(self): return self.row == 0 @dataclass class Page: """ Represents a Teletext Page (e.g., 100). Can have multiple subpages. """ magazine: int page_number: int # 00-99 (Hex storage: 0x00-0xFF) sub_code: int = 0 # 13-bit subcode (0000 to 3F7F hex) # Control bits C4-C14 control_bits: int = 0 language: int = 0 # National Option (0-7, from C12-C14) packets: List[Packet] = field(default_factory=list) @property def full_page_number(self): # Format as Hex to support A-F pages return f"{self.magazine}{self.page_number:02X}" def get_control_bit(self, n: int) -> bool: """ Returns value of control bit Cn (4-14) """ if 4 <= n <= 14: return bool((self.control_bits >> (n - 4)) & 1) return False def set_control_bit(self, n: int, value: bool): """ Sets value of control bit Cn (4-14) """ if 4 <= n <= 14: if value: self.control_bits |= (1 << (n - 4)) else: self.control_bits &= ~(1 << (n - 4)) def calculate_crc(self) -> int: """ Calculates the CRC-16 checksum for the page. According to ETSI EN 300 706 (Section 9.6.1 & Figure 13): - G(x) = x^16 + x^12 + x^9 + x^7 + 1 (Poly 0x1281) - Initial value: 0. - Processed bits b8 to b1 (MSB first for stored bytes). - Total 1024 bytes (32 packets * 32 bytes). - Packet X/0: Bytes 14 to 37 (24 bytes) + 8 spaces. - Packets X/1 to X/25: Bytes 14 to 45 (32 bytes). - Packets X/26 to X/31: 32 spaces each. """ crc = 0 poly = 0x1281 # Helper to update CRC with a byte (MSB first) def update_crc(c, val): v = (val << 8) & 0xFFFF for _ in range(8): if (c ^ v) & 0x8000: c = (c << 1) ^ poly else: c = c << 1 v <<= 1 c &= 0xFFFF return c # Organize packets by row rows = {p.row: p for p in self.packets} for r in range(32): # Process 32 slots (0-31) start, end = 0, 0 padding = 0 if r == 0: # Row 0: Bytes 14-37 (24 bytes) start, end = 8, 32 # data[8..31] padding = 8 elif 1 <= r <= 25: # Rows 1-25: Bytes 14-45 (32 bytes) start, end = 8, 40 # data[8..39] padding = 0 else: # Rows 26-31: 32 spaces each padding = 32 # Process packet data if available if r in rows and start < end: p_data = rows[r].data for i in range(start, end): byte_val = (p_data[i] & 0x7F) if i < len(p_data) else 0x20 crc = update_crc(crc, byte_val) elif start < end: # Missing packet but slot exists for _ in range(start, end): crc = update_crc(crc, 0x20) # Add padding for this slot for _ in range(padding): crc = update_crc(crc, 0x20) return crc def get_stored_crc(self) -> Optional[int]: """ Attempts to retrieve the stored CRC from Packet 27/0 if present. Returns None if not found. """ # Look for Packet 27 for p in self.packets: if p.row == 27: # Check Designation Code (Byte 0) try: if len(p.data) >= 40: b0 = p.data[0] # Decode Hamming 8/4 designation = decode_hamming_8_4(b0) # Packets X/27/0 to X/27/3 exist, but only X/27/0 has the CRC. # We also check if b0 is raw 0 as a fallback for some captures. if designation == 0 or b0 == 0: # Packet 27/0 # Checksum is in bytes 38 and 39 (TBytes 44 and 45). hi = p.data[38] lo = p.data[39] crc = (hi << 8) | lo return crc except: pass return None @dataclass class TeletextService: """ Container for all pages. """ pages: List[Page] = field(default_factory=list) # We also keep a flat list of all packets to preserve order on save all_packets: List[Packet] = field(default_factory=list)