app
LAN7800 EEPROM Programmer
Public Made by Adomby adom
MPLAB-Connect-parity EEPROM tool for the Microchip LAN7800 USB-3.1 to Gigabit-Ethernet controller (VID 0x0424 / PID 0x7800). Read an adapter's EEPROM to a .bin, edit MAC / VID / PID / bcdDevice / strings / serial and LED/GPIO with a byte-exact preview, and program hardware with verify-after-write. Reads are free; physical writes are gated behind --force-physical-write. Ships an AI-oriented CLI (single source of truth) plus a Hydrogen webapp that shells out to it.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
"""Offline round-trip tests for the LAN7800 image model. No hardware.
Run: python3 -m pytest cli/tests (or) python3 cli/tests/test_eeprom.py
"""
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from lan7800.eeprom import Eeprom, EepromError, LAN7800_VID, LAN7800_PID # noqa
from tests.make_fixture import build # noqa
def _fx():
return Eeprom.from_bytes(build())
def test_signature_and_size():
e = _fx()
assert e.size == 512
assert e.has_signature and e.signature == 0xA5
def test_mac_exact_bytes_natural_order():
# RESOLVED assumption: EEPROM offset 0x01 = addr[0] (natural order).
# Entering 00:80:0F:78:00:00 must store bytes 00 80 0F 78 00 00 at 0x01,
# so the lan78xx driver reads the intended MAC back (not the byte-reverse).
from lan7800.eeprom import MAC_OFFSET
e = Eeprom.blank(512)
e.set_mac("00:80:0F:78:00:00")
assert bytes(e.data[MAC_OFFSET:MAC_OFFSET + 6]) == \
bytes([0x00, 0x80, 0x0F, 0x78, 0x00, 0x00])
assert e.mac == "00:80:0F:78:00:00"
# and the reverse would have been the bug we fixed
assert e.mac != "00:00:78:0F:80:00"
def test_mac_roundtrip():
e = _fx()
assert e.mac == "00:80:0F:11:22:33"
e.set_mac("AA:BB:CC:DD:EE:FF")
assert e.mac == "AA:BB:CC:DD:EE:FF"
# re-parse persisted bytes to prove octet order survives a save/load
e2 = Eeprom.from_bytes(e.to_bytes())
assert e2.mac == "AA:BB:CC:DD:EE:FF"
def test_vid_pid_across_all_blocks():
e = _fx()
for d in e.descriptors():
assert d.present and d.looks_valid
assert d.vid == LAN7800_VID and d.pid == LAN7800_PID
touched = e.set_vid(0x1234)
assert set(touched) == {"ss", "hs", "fs"}
e.set_pid(0xABCD)
e2 = Eeprom.from_bytes(e.to_bytes())
for d in e2.descriptors():
assert d.vid == 0x1234 and d.pid == 0xABCD
def test_bcd_device():
e = _fx()
e.set_bcd_device(0x0410)
e2 = Eeprom.from_bytes(e.to_bytes())
assert all(d.bcd_device == 0x0410 for d in e2.descriptors() if d.present)
def test_strings_read():
e = _fx()
s = {x.name: x.value for x in e.strings() if x.present}
assert s["manufacturer"] == "Microchip"
assert s["product"] == "LAN7800 USB 3.1 GbE"
assert s["serial"] == "SN0000000001"
def test_serial_inplace_shorter_ok():
e = _fx()
e.set_string_inplace("serial", "SN42") # shorter -> fits
e2 = Eeprom.from_bytes(e.to_bytes())
assert e2.string("serial").value == "SN42"
def test_serial_inplace_too_long_refused():
e = _fx()
try:
e.set_string_inplace("serial", "X" * 40) # far larger than slot
except EepromError:
return
raise AssertionError("expected EepromError for oversized in-place string")
def test_set_string_absent_refused():
e = _fx()
try:
e.set_string_inplace("interface", "eth") # absent in fixture
except EepromError:
return
raise AssertionError("expected EepromError setting an absent string")
def test_signature_setter():
e = Eeprom.blank(512)
assert not e.has_signature
e.set_signature()
assert e.has_signature
def test_bad_mac_rejected():
e = _fx()
for bad in ("00:11:22:33:44", "zz:11:22:33:44:55", "1:2:3:4:5:6:7"):
try:
e.set_mac(bad)
except EepromError:
continue
raise AssertionError(f"expected rejection of {bad!r}")
def test_led_roundtrip():
from lan7800 import regs
e = _fx()
regs.set_led_mode(e, 0, 10); regs.set_led_enable(e, 0, True)
regs.set_led_mode(e, 3, 15); regs.set_led_blink(e, 3)
e2 = Eeprom.from_bytes(e.to_bytes())
assert regs.get_led(e2, 0)["mode"] == 10 and regs.get_led(e2, 0)["enabled"]
assert regs.get_led(e2, 3)["mode_name"] == "Force On"
assert regs.get_led_blink(e2)["blink_rate"] == "20Hz"
def test_gpio_roundtrip():
from lan7800 import regs
e = _fx()
regs.set_gpio(e, 5, direction="out", drive="open-drain", output_value=1,
wake_enabled=True, wake_active_high=False)
e2 = Eeprom.from_bytes(e.to_bytes())
g = regs.get_gpio(e2, 5)
assert g["direction"] == "out" and g["drive"] == "open-drain"
assert g["output_value"] == 1 and g["wake_enabled"] and not g["wake_active_high"]
def test_regs_bounds():
from lan7800 import regs
e = _fx()
for bad in (-1, 4, 99):
try:
regs.set_led_mode(e, bad, 0)
except EepromError:
continue
raise AssertionError("expected LED index rejection")
def test_default_template():
e = Eeprom.default_template()
assert e.has_signature
assert e.mac == "00:80:0F:00:00:00"
ds = [d for d in e.descriptors() if d.present]
assert len(ds) == 3
assert all(d.vid == LAN7800_VID and d.pid == LAN7800_PID for d in ds)
from lan7800 import regs
assert regs.region_overlaps(e) == [] # descriptors clear of LED/GPIO offsets
def test_overlap_guard():
from lan7800 import regs
from lan7800.eeprom import DESC_PTRS
e = _fx()
# Point the SS descriptor at 0x48 (word 0x24) so its 18-byte block covers
# the fixed GPIO config region, and make it look like a real descriptor.
e.data[DESC_PTRS["ss"]] = 0x24
e.data[DESC_PTRS["ss"] + 1] = 0x00
e.data[0x48] = 0x12
e.data[0x49] = 0x01
overlaps = regs.region_overlaps(e)
assert overlaps, "region_overlaps should detect the collision"
try:
regs.set_gpio(e, 5, output_value=1)
except EepromError:
return
raise AssertionError("guard should block a GPIO write into a descriptor block")
def _run():
fns = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
passed = 0
for fn in fns:
fn()
passed += 1
print(f" ok {fn.__name__}")
print(f"\n{passed}/{len(fns)} passed")
if __name__ == "__main__":
_run()