feat: implement Packet 26 enhancement support with Hamming 24/18 decoding
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -336,6 +336,25 @@ class TeletextCanvas(QWidget):
|
|||||||
|
|
||||||
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
|
||||||
|
|||||||
Reference in New Issue
Block a user