352 lines
12 KiB
Python
352 lines
12 KiB
Python
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",
|
|
]
|