from __future__ import annotations import hashlib import json from collections import defaultdict from collections.abc import Mapping from pathlib import Path from typing import Any from ..formatting import h16 from .memory import MemoryMap from .peripherals.x24164 import ( X24164_FACTORY_DEFAULT_BYTES, X24164_LOGICAL_PAGE_COUNT, X24164_LOGICAL_PAGE_SIZE, X24164_LOGICAL_SIZE, factory_default_words_from_rom, ) JsonObject = dict[str, Any] SELECTOR_MAP_BASE = 0xC564 SELECTOR_MAP_COUNT = 0x0200 RECORD_BYTES = 8 SHADOW_BASE = 0xF400 def build_eeprom_snapshot( memory: MemoryMap, *, rom_bytes: bytes | None = None, include_image_hex: bool = False, ) -> JsonObject: image = memory.dump_eeprom_image() selectors_by_offset = _selectors_by_offset(rom_bytes) factory_image = _factory_image(rom_bytes) write_events = _write_events(memory, selectors_by_offset) write_words = _coalesced_write_words(memory, selectors_by_offset) factory_diffs = _factory_diffs(image, factory_image, selectors_by_offset) report: JsonObject = { "kind": "emulator_eeprom_snapshot", "summary": { "logical_size": len(image), "logical_size_hex": f"0x{len(image):04X}", "sha256": hashlib.sha256(image).hexdigest(), "write_byte_count": len(write_events), "write_word_count": len(write_words), "factory_diff_word_count": len(factory_diffs), "record_count": X24164_LOGICAL_PAGE_COUNT, }, "records": _records(image), "write_events": write_events, "write_word_events": write_words, "factory_diffs": factory_diffs, "shadow_f400": _shadow_summary(memory, rom_bytes, selectors_by_offset), } if include_image_hex: report["image_hex"] = image.hex() return report def format_eeprom_snapshot(report: Mapping[str, Any], *, limit: int = 80) -> str: summary = report["summary"] lines = [ "Emulator EEPROM Snapshot", "", f"size={summary['logical_size_hex']} sha256={summary['sha256']}", ( f"writes: bytes={summary['write_byte_count']} words={summary['write_word_count']} " f"factory_diff_words={summary['factory_diff_word_count']}" ), "", "Persistent Records:", ] for record in report.get("records", []): lines.append( f"- page {record['page_hex']} EEPROM {record['range_hex']} " f"bytes={record['bytes_hex']} text={record['ascii']!r}" ) lines.extend(["", "EEPROM Word Writes:"]) word_events = list(report.get("write_word_events", [])) if not word_events: lines.append("- none since EEPROM setup/load") for event in word_events[:limit]: suffix = _event_suffix(event) lines.append( f"- {event['address_hex']} page={event['page_hex']} offset={event['offset_hex']} " f"{event['old_word_hex']}->{event['new_word_hex']} source={event['source']}{suffix}" ) if len(word_events) > limit: lines.append(f"- ... {len(word_events) - limit} more word writes omitted") lines.extend(["", "Factory Diffs:"]) diffs = list(report.get("factory_diffs", [])) if not diffs: lines.append("- current EEPROM image matches ROM factory/default image") for diff in diffs[:limit]: suffix = _event_suffix(diff) lines.append( f"- {diff['address_hex']} page={diff['page_hex']} offset={diff['offset_hex']} " f"expected={diff['expected_word_hex']} actual={diff['actual_word_hex']}{suffix}" ) if len(diffs) > limit: lines.append(f"- ... {len(diffs) - limit} more factory diffs omitted") shadow = report.get("shadow_f400", {}) lines.extend(["", "F400 Shadow Diffs:"]) shadow_diffs = list(shadow.get("diffs", [])) if isinstance(shadow, Mapping) else [] if not shadow_diffs: lines.append("- F400-F4FF shadow matches ROM factory words or no ROM factory baseline was supplied") for diff in shadow_diffs[:limit]: suffix = _event_suffix(diff) lines.append( f"- {diff['address_hex']} offset={diff['offset_hex']} " f"expected={diff['expected_word_hex']} actual={diff['actual_word_hex']}{suffix}" ) if len(shadow_diffs) > limit: lines.append(f"- ... {len(shadow_diffs) - limit} more shadow diffs omitted") return "\n".join(lines).rstrip() + "\n" def write_eeprom_snapshot( memory: MemoryMap, output_path: Path, *, rom_bytes: bytes | None = None, as_json: bool = False, include_image_hex: bool = False, ) -> JsonObject: report = build_eeprom_snapshot(memory, rom_bytes=rom_bytes, include_image_hex=include_image_hex) output_path.parent.mkdir(parents=True, exist_ok=True) if as_json: output_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") else: output_path.write_text(format_eeprom_snapshot(report), encoding="utf-8") return report def _write_events(memory: MemoryMap, selectors_by_offset: Mapping[int, list[int]]) -> list[JsonObject]: events = [] for index, event in enumerate(memory.p9_bus.x24164_bus.write_events): item = _address_info(event.logical_address, selectors_by_offset) item.update( { "index": index, "device": event.device, "device_offset": event.device_offset, "device_offset_hex": f"0x{event.device_offset:03X}", "old_value": event.old_value, "old_value_hex": f"0x{event.old_value & 0xFF:02X}", "new_value": event.new_value, "new_value_hex": f"0x{event.new_value & 0xFF:02X}", "source": event.source, } ) events.append(item) return events def _coalesced_write_words(memory: MemoryMap, selectors_by_offset: Mapping[int, list[int]]) -> list[JsonObject]: events = memory.p9_bus.x24164_bus.write_events words: list[JsonObject] = [] index = 0 while index < len(events): event = events[index] next_event = events[index + 1] if index + 1 < len(events) else None if ( next_event is not None and (event.logical_address & 1) == 0 and next_event.logical_address == ((event.logical_address + 1) & 0x0FFF) and next_event.device == event.device and next_event.source == event.source ): old_word = ((event.old_value & 0xFF) << 8) | (next_event.old_value & 0xFF) new_word = ((event.new_value & 0xFF) << 8) | (next_event.new_value & 0xFF) item = _address_info(event.logical_address, selectors_by_offset) item.update( { "index": index, "device": event.device, "old_word": old_word, "old_word_hex": f"0x{old_word:04X}", "new_word": new_word, "new_word_hex": f"0x{new_word:04X}", "source": event.source, } ) words.append(item) index += 2 else: index += 1 return words def _factory_diffs( image: bytes, factory_image: bytes | None, selectors_by_offset: Mapping[int, list[int]], ) -> list[JsonObject]: if factory_image is None: return [] diffs = [] for address in range(0, min(len(image), len(factory_image)), 2): expected = (factory_image[address] << 8) | factory_image[address + 1] actual = (image[address] << 8) | image[address + 1] if expected == actual: continue item = _address_info(address, selectors_by_offset) item.update( { "expected_word": expected, "expected_word_hex": f"0x{expected:04X}", "actual_word": actual, "actual_word_hex": f"0x{actual:04X}", } ) diffs.append(item) return diffs def _shadow_summary( memory: MemoryMap, rom_bytes: bytes | None, selectors_by_offset: Mapping[int, list[int]], ) -> JsonObject: if rom_bytes is None: return {"diffs": [], "note": "no ROM factory baseline supplied"} factory_words = dict(factory_default_words_from_rom(rom_bytes)) diffs = [] for offset in range(0, X24164_FACTORY_DEFAULT_BYTES, 2): expected = factory_words[offset] address = SHADOW_BASE + offset high = memory.external.get(address & 0xFFFF, 0xFF) low = memory.external.get((address + 1) & 0xFFFF, 0xFF) actual = ((high & 0xFF) << 8) | (low & 0xFF) if expected == actual: continue item = _address_info(offset, selectors_by_offset) item.update( { "address": address, "address_hex": h16(address), "expected_word": expected, "expected_word_hex": f"0x{expected:04X}", "actual_word": actual, "actual_word_hex": f"0x{actual:04X}", } ) diffs.append(item) return {"diffs": diffs, "diff_count": len(diffs)} def _records(image: bytes) -> list[JsonObject]: records = [] for page in range(X24164_LOGICAL_PAGE_COUNT): base = page * X24164_LOGICAL_PAGE_SIZE data = image[base : base + RECORD_BYTES] records.append( { "page": page, "page_hex": f"0x{page:X}", "address": base, "address_hex": f"0x{base:03X}", "range_hex": f"0x{base:03X}-0x{base + RECORD_BYTES - 1:03X}", "bytes_hex": data.hex(" ").upper(), "words_hex": [ f"0x{((data[index] << 8) | data[index + 1]):04X}" for index in range(0, len(data), 2) ], "ascii": _ascii(data), "is_blank_spaces": data == (b" " * RECORD_BYTES), } ) return records def _factory_image(rom_bytes: bytes | None) -> bytes | None: if rom_bytes is None: return None image = bytearray([0xFF] * X24164_LOGICAL_SIZE) for offset, word in factory_default_words_from_rom(rom_bytes): for page in range(X24164_LOGICAL_PAGE_COUNT): address = (page * X24164_LOGICAL_PAGE_SIZE) + offset image[address] = (word >> 8) & 0xFF image[address + 1] = word & 0xFF for page in range(1, X24164_LOGICAL_PAGE_COUNT): base = page * X24164_LOGICAL_PAGE_SIZE for offset in range(0, RECORD_BYTES, 2): image[base + offset] = 0x20 image[base + offset + 1] = 0x20 return bytes(image) def _selectors_by_offset(rom_bytes: bytes | None) -> dict[int, list[int]]: if rom_bytes is None: return {} result: dict[int, list[int]] = defaultdict(list) for selector in range(SELECTOR_MAP_COUNT): address = SELECTOR_MAP_BASE + selector * 2 if address + 1 >= len(rom_bytes): break low = rom_bytes[address + 1] if low: result[low & 0xFE].append(selector) return dict(result) def _address_info(address: int, selectors_by_offset: Mapping[int, list[int]]) -> JsonObject: address &= 0x0FFF page = (address // X24164_LOGICAL_PAGE_SIZE) & 0x0F offset = address & 0xFF aligned_offset = offset & 0xFE selectors = selectors_by_offset.get(aligned_offset, []) return { "address": address, "address_hex": f"0x{address:03X}", "page": page, "page_hex": f"0x{page:X}", "offset": offset, "offset_hex": f"0x{offset:02X}", "aligned_offset": aligned_offset, "aligned_offset_hex": f"0x{aligned_offset:02X}", "record_byte": offset if offset < RECORD_BYTES else None, "role": "record_header_or_label" if offset < RECORD_BYTES else "factory_shadow_offset", "mapped_selectors": selectors[:24], "mapped_selectors_hex": [f"0x{selector:03X}" for selector in selectors[:24]], } def _event_suffix(event: Mapping[str, Any]) -> str: parts = [] if event.get("role"): parts.append(str(event["role"])) selectors = event.get("mapped_selectors_hex") if isinstance(selectors, list) and selectors: parts.append("selectors=" + ",".join(str(item) for item in selectors[:6])) return f" ({'; '.join(parts)})" if parts else "" def _ascii(data: bytes) -> str: return "".join(chr(value) if 0x20 <= value <= 0x7E else "." for value in data) __all__ = [ "build_eeprom_snapshot", "format_eeprom_snapshot", "write_eeprom_snapshot", ]