feat: implement Packet 26 enhancement support with Hamming 24/18 decoding
All checks were successful
Build Linux / Build Linux (push) Successful in 1m30s
Build Windows / Build Windows (push) Successful in 4m50s

This commit is contained in:
2026-02-21 12:54:41 +01:00
parent 6b9bf47504
commit 8dba051ab4
3 changed files with 111 additions and 2 deletions

View File

@@ -1,6 +1,6 @@
import os import os
from typing import List, Callable, Optional from typing import List, Callable, Optional
from .models import Packet, Page, TeletextService, decode_hamming_8_4 from .models import Packet, Page, TeletextService, decode_hamming_8_4, decode_hamming_24_18, Packet26Enhancement
def load_t42(file_path: str, progress_callback: Optional[Callable[[int, int], None]] = None) -> TeletextService: def load_t42(file_path: str, progress_callback: Optional[Callable[[int, int], None]] = None) -> TeletextService:
service = TeletextService() service = TeletextService()
@@ -70,6 +70,35 @@ def load_t42(file_path: str, progress_callback: Optional[Callable[[int, int], No
# Update tracking # Update tracking
current_pages_by_mag[mag] = new_page current_pages_by_mag[mag] = new_page
last_row_by_mag[mag] = 0 last_row_by_mag[mag] = 0
elif row == 26:
# Enhancement packet
target_page = current_pages_by_mag.get(mag)
if target_page:
target_page.packets.append(packet)
# Track active row within this packet/page for enhancements
# Default active row is usually the last one?
# Spec: "The active row is set to 0 at the beginning of each page."
# We should probably keep state in the Page object during loading.
if not hasattr(target_page, '_active_row'):
target_page._active_row = 0
for i in range(13):
b1 = packet.data[1 + i*3]
b2 = packet.data[2 + i*3]
b3 = packet.data[3 + i*3]
triplet = decode_hamming_24_18(b1, b2, b3)
if triplet is not None:
address = triplet & 0x3F
mode = (triplet >> 6) & 0x1F
data = (triplet >> 11) & 0x7F
if 0x01 <= mode <= 0x07:
# Set Active Row (bits 0-4 of data = row number)
target_page._active_row = data & 0x1F
elif mode == 0x00:
# Overwrite character at (active_row, address)
enh = Packet26Enhancement(row=target_page._active_row, col=address, mode=mode, data=data)
target_page.packet26_enhancements.append(enh)
else: else:
# Add to the "current" page of this magazine. # Add to the "current" page of this magazine.
target_page = current_pages_by_mag.get(mag) target_page = current_pages_by_mag.get(mag)

View File

@@ -39,6 +39,52 @@ def decode_hamming_8_4(byte_val):
# Return 4 data bits: D1, D2, D3, D4 # Return 4 data bits: D1, D2, D3, D4
return b[1] | (b[3] << 1) | (b[5] << 2) | (b[7] << 3) return b[1] | (b[3] << 1) | (b[5] << 2) | (b[7] << 3)
def decode_hamming_24_18(b1, b2, b3):
"""
Decodes a 24/18 Hamming triplet.
Returns 18 bits of data or None if uncorrectable error.
"""
v = b1 | (b2 << 8) | (b3 << 16)
syndrome = 0
for i in range(5):
check = 0
for j in range(23):
if ((j + 1) >> i) & 1:
check ^= (v >> j) & 1
if check:
syndrome |= (1 << i)
overall_parity = 0
for j in range(24):
overall_parity ^= (v >> j) & 1
if overall_parity == 0:
if syndrome != 0:
return None # Double error
else:
if syndrome != 0:
v ^= (1 << (syndrome - 1))
# Extract 18 data bits
d_indices = [2, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14, 16, 17, 18, 19, 20, 21, 22]
res = 0
for i, idx in enumerate(d_indices):
if (v >> idx) & 1:
res |= (1 << i)
return res
@dataclass
class Packet26Enhancement:
"""
Represents an enhancement from Packet 26.
Usually a character replacement or attribute at a specific (row, col).
"""
row: int
col: int
mode: int
data: int
@dataclass @dataclass
class Packet: class Packet:
""" """
@@ -100,6 +146,7 @@ class Page:
sub_code: int = 0 # Subpage code (0000 to 3F7F hex usually, simplest is 0-99 equivalent) sub_code: int = 0 # Subpage code (0000 to 3F7F hex usually, simplest is 0-99 equivalent)
language: int = 0 # National Option (0-7) language: int = 0 # National Option (0-7)
packets: List[Packet] = field(default_factory=list) packets: List[Packet] = field(default_factory=list)
packet26_enhancements: List[Packet26Enhancement] = field(default_factory=list)
@property @property
def full_page_number(self): def full_page_number(self):

View File

@@ -333,9 +333,28 @@ class TeletextCanvas(QWidget):
byte_val = ord(header_prefix[c]) byte_val = ord(header_prefix[c])
else: else:
byte_val = data[c] if c < len(data) else 0x20 byte_val = data[c] if c < len(data) else 0x20
byte_val &= 0x7F # Strip parity byte_val &= 0x7F # Strip parity
# Packet 26 Overwrite?
# Check if there is an enhancement for this (row, col)
p26_data = None
if self.page:
for enh in self.page.packet26_enhancements:
if enh.row == row and enh.col == c:
if enh.mode == 0x00:
# Overwrite character
p26_data = enh.data
break
# If we have P26 overwrite, we use it for display, but it doesn't change
# the serial attribute processing of the 'original' byte_val if it was a control code?
# Actually, P26 overwrites a displayable character.
# If the original byte_val was a control code, does P26 replace it?
# Usually yes, it's a "display at" operation.
display_byte = p26_data if p26_data is not None else byte_val
# Control Codes # Control Codes
is_control = False is_control = False
@@ -460,6 +479,20 @@ class TeletextCanvas(QWidget):
if drcs_char: if drcs_char:
self.draw_drcs(painter, x, y, drcs_char, fg, double_height) self.draw_drcs(painter, x, y, drcs_char, fg, double_height)
elif p26_data is not None:
# Level 2.5 character from G2 supplementary set
# (Technically P26 data can point to other sets, but G2 is common)
from .charsets import get_char_g2
char = get_char_g2(p26_data)
painter.setPen(fg)
if double_height:
painter.save()
painter.translate(x, y)
painter.scale(1, 2)
painter.drawText(QRect(0, 0, self.cell_w, self.cell_h), Qt.AlignmentFlag.AlignCenter, char)
painter.restore()
else:
painter.drawText(QRect(x, y, self.cell_w, self.cell_h), Qt.AlignmentFlag.AlignCenter, char)
elif graphics_mode: elif graphics_mode:
# Mosaic Graphics # Mosaic Graphics
h_mos = self.cell_h * 2 if double_height else self.cell_h h_mos = self.cell_h * 2 if double_height else self.cell_h