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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721
"""lan7800prog — LAN7800 EEPROM programmer CLI (Core v1, LAN7800 ONLY).
Design contract (Adom AI-oriented CLI):
* Every command prints a human line: `OK: ...`, or `ERROR: ...` + `Hint: ...`.
* `--json` additionally emits one machine-readable object on stdout.
* Reads are FREE. The ONLY commands that touch a physical adapter are
`program` and `erase`; both refuse unless `--force-physical-write` is
given. That flag is the safety catch — the operator/agent must have a
cleared `mesh escalate` PROCEED before setting it. The webapp never sets it
without an escalation.
Field edits produce a .bin image (file->file). Hardware programming is a
separate, explicit, gated step (load base -> edit -> preview .bin -> program).
"""
from __future__ import annotations
import argparse
import json
import sys
from .eeprom import (Eeprom, EepromError, parse_u16, LAN7800_VID, LAN7800_PID,
SIG_VALUE, SIZES, MAX_EEPROM_SIZE)
from .backends import (BinFileBackend, EthtoolBackend, UsbEepromBackend,
BackendError, udev_rule, udev_install_commands)
from .scan import scan_devices, diagnose_netdev
from . import regs
# ---- output helpers -------------------------------------------------------
def _emit(args, ok, payload=None, human=None, hint=None):
if getattr(args, "json", False):
obj = {"ok": ok}
if payload:
obj.update(payload)
if not ok and human:
obj["error"] = human
if hint:
obj["hint"] = hint
print(json.dumps(obj))
else:
if ok:
if human:
print(f"OK: {human}")
else:
print(f"ERROR: {human}", file=sys.stderr)
if hint:
print(f"Hint: {hint}", file=sys.stderr)
return 0 if ok else 1
def _fail(args, human, hint=None):
return _emit(args, False, human=human, hint=hint)
# ---- image sourcing -------------------------------------------------------
def _load_image_from_in(args) -> Eeprom:
if not getattr(args, "in_file", None):
raise EepromError("this command needs --in <base.bin>")
data = BinFileBackend(args.in_file).read_image()
return Eeprom.from_bytes(data)
def _read_device_image(iface: str, sudo: bool) -> Eeprom:
data = EthtoolBackend(iface, sudo=sudo).read_image()
return Eeprom.from_bytes(data)
def _describe(eep: Eeprom) -> dict:
descs = []
lan7800_seen = False
for d in eep.descriptors():
if d.present and d.vid == LAN7800_VID and d.pid == LAN7800_PID:
lan7800_seen = True
descs.append({
"kind": d.kind, "present": d.present, "looks_valid": d.looks_valid,
"ptr_word": d.ptr_word, "byte_offset": d.byte_offset,
"vid": (f"0x{d.vid:04X}" if d.vid is not None else None),
"pid": (f"0x{d.pid:04X}" if d.pid is not None else None),
"bcd_device": (f"0x{d.bcd_device:04X}" if d.bcd_device is not None else None),
})
strings = [{"name": s.name, "present": s.present, "value": s.value,
"byte_offset": s.byte_offset} for s in eep.strings()]
return {
"size": eep.size,
"signature": f"0x{eep.signature:02X}",
"signature_ok": eep.has_signature,
"mac": eep.mac,
"descriptors": descs,
"strings": strings,
"config_flags": [f"0x{v:08X}" for v in eep.config_flags()],
"lan7800_identity": lan7800_seen,
"regs": regs.describe_regs(eep),
}
# ---- commands -------------------------------------------------------------
def cmd_scan(args):
"""Enumerate attached LAN7800 adapters (FREE)."""
devices = scan_devices(probe_eeprom=not args.no_probe)
if not getattr(args, "json", False):
if not devices:
print("OK: no LAN7800 (0x0424/0x7800) adapters found")
return 0
print(f"OK: {len(devices)} LAN7800 adapter(s):")
for d in devices:
iface = d["iface"] or "(no netdev bound)"
print(f" usb {d['usb_path']} (bus {d['busnum']}/dev {d['devnum']}) "
f"{iface} driver={d['driver'] or '-'}")
print(f" MAC={d['mac'] or '?'} EEPROM={d['eeprom']} "
f"product={d['product'] or '?'}")
return 0
return _emit(args, True, {"count": len(devices), "devices": devices})
def cmd_read_device(args):
"""Read the CURRENTLY-CONNECTED LAN7800, auto-picking the backend.
netdev present -> ethtool; else -> libusb (driver-independent). FREE read.
"""
devices = scan_devices(probe_eeprom=False)
if not devices:
return _fail(args, "no LAN7800 (0x0424/0x7800) adapter connected",
hint="plug in the adapter, or run `scan`")
dev = devices[0]
data = None
backend_used = None
errors = []
# libusb is PRIMARY: no sudo, permissioned via the plugdev udev rule, and it
# works even when this flapping adapter has no stable lan78xx netdev. ethtool
# is only a fallback, tried when libusb is unavailable AND a netdev exists;
# its "needs sudo" failure is NOT surfaced as the headline error when libusb
# is the real path.
try:
backend_used = "libusb"
be = UsbEepromBackend(size=args.size)
try:
data = be.read_image()
finally:
be.close()
except BackendError as e:
errors.append(f"libusb: {e}")
data = None
if data is None and dev.get("iface"):
try:
backend_used = f"ethtool:{dev['iface']}"
data = EthtoolBackend(dev["iface"], sudo=args.sudo).read_image()
except BackendError as e:
errors.append(f"ethtool: {e}")
data = None
if data is None:
payload = {"device": dev, "errors": errors,
"udev_rule": udev_rule(),
"udev_install_commands": udev_install_commands(),
"netdev_diagnosis": diagnose_netdev(dev["usb_path"])}
return _emit(args, False, payload,
human="could not read the connected device (libusb primary)",
hint="libusb needs the plugdev udev rule "
"(udev_install_commands) then a replug; no sudo/root "
"required. ethtool is only a fallback and needs a "
"netdev + passwordless sudo.")
eep = Eeprom.from_bytes(data)
view = _describe(eep)
view["backend"] = backend_used
view["device"] = dev
view["raw_head_hex"] = data[:64].hex()
if not dev.get("iface"):
view["netdev_diagnosis"] = diagnose_netdev(dev["usb_path"])
if args.out:
BinFileBackend(args.out).write_image(data)
view["out"] = args.out
if not getattr(args, "json", False):
print(f"OK: read {len(data)}B from {dev['usb_path']} via {backend_used}"
f" sig={view['signature']} "
f"({'valid' if view['signature_ok'] else 'blank/invalid'})")
print(f" MAC: {view['mac']}")
return 0
return _emit(args, True, view)
def cmd_udev_rule(args):
return _emit(args, True,
{"path": "/etc/udev/rules.d/99-lan7800.rules",
"rule": udev_rule(),
"install_commands": udev_install_commands()},
human="udev rule for libusb access to 0x0424/0x7800 (run the "
"install_commands as the human, then replug)")
def cmd_new(args):
size = args.size
if size not in SIZES:
return _fail(args, f"size must be one of {SIZES}")
if args.blank:
eep = Eeprom.blank(size)
if args.sign:
eep.set_signature()
kind = "blank" + (" signed" if args.sign else "")
else:
eep = Eeprom.default_template(size) # signed default LAN7800 template
kind = "default LAN7800 template"
BinFileBackend(args.out).write_image(eep.to_bytes())
return _emit(args, True, {"out": args.out, "size": size, "kind": kind,
"preview": _describe(eep)},
human=f"wrote {kind} {size}-byte image to {args.out}")
def cmd_info(args):
try:
if args.iface:
eep = _read_device_image(args.iface, args.sudo)
source = f"iface:{args.iface}"
else:
eep = _load_image_from_in(args)
source = f"file:{args.in_file}"
except (EepromError, BackendError) as e:
return _fail(args, str(e), hint="give --in <base.bin> or --iface <name>")
view = _describe(eep)
view["source"] = source
if not getattr(args, "json", False):
sig_note = ("valid" if view["signature_ok"]
else "INVALID — device falls back to OTP/defaults")
print(f"OK: {source} size={view['size']} sig={view['signature']} "
f"({sig_note})")
print(f" MAC: {view['mac']}")
for d in view["descriptors"]:
if d["present"]:
print(f" {d['kind'].upper()} desc @0x{d['byte_offset']:X}: "
f"VID={d['vid']} PID={d['pid']} bcdDevice={d['bcd_device']}"
f"{'' if d['looks_valid'] else ' (!! not a std device desc)'}")
else:
print(f" {d['kind'].upper()} desc: absent")
for s in view["strings"]:
if s["present"]:
print(f" str[{s['name']}]: {s['value']!r}")
if not view["lan7800_identity"]:
print(" note: no descriptor advertises VID/PID 0x0424/0x7800 — "
"confirm this is a LAN7800 image (this tool is LAN7800-only)")
return 0
return _emit(args, True, view)
def _apply_edits(eep: Eeprom, args) -> list[str]:
changes = []
if args.mac is not None:
eep.set_mac(args.mac)
changes.append(f"mac={eep.mac}")
if args.vid is not None:
v = parse_u16(args.vid)
touched = eep.set_vid(v)
changes.append(f"vid=0x{v:04X}({'+'.join(touched)})")
if args.pid is not None:
p = parse_u16(args.pid)
touched = eep.set_pid(p)
changes.append(f"pid=0x{p:04X}({'+'.join(touched)})")
if args.bcd is not None:
b = parse_u16(args.bcd)
touched = eep.set_bcd_device(b)
changes.append(f"bcdDevice=0x{b:04X}({'+'.join(touched)})")
if args.serial is not None:
eep.set_string_inplace("serial", args.serial)
changes.append(f"serial={args.serial!r}")
for pair in (args.set_string or []):
if "=" not in pair:
raise EepromError(f"--set-string needs name=value, got {pair!r}")
name, value = pair.split("=", 1)
eep.set_string_inplace(name.strip(), value)
changes.append(f"str[{name.strip()}]={value!r}")
for spec in (args.set_led or []): # index=mode
idx, _, mode = spec.partition("=")
regs.set_led_mode(eep, int(idx), int(mode, 0))
changes.append(f"led{int(idx)}.mode={int(mode, 0)}")
for spec in (args.led_enable or []): # index=1|0
idx, _, on = spec.partition("=")
regs.set_led_enable(eep, int(idx), on.strip() in ("1", "on", "true"))
changes.append(f"led{int(idx)}.enable={on.strip()}")
if args.led_blink is not None:
regs.set_led_blink(eep, int(args.led_blink))
changes.append(f"led.blink={args.led_blink}")
for spec in (args.set_gpio or []): # index=dir,drive,out
idx, _, rest = spec.partition("=")
parts = [p.strip() for p in rest.split(",")]
kw = {}
if len(parts) > 0 and parts[0]:
kw["direction"] = parts[0]
if len(parts) > 1 and parts[1]:
kw["drive"] = parts[1]
if len(parts) > 2 and parts[2] != "":
kw["output_value"] = int(parts[2])
regs.set_gpio(eep, int(idx), **kw)
changes.append(f"gpio{int(idx)}={rest}")
for spec in (args.gpio_wake or []): # index=en,pol
idx, _, rest = spec.partition("=")
en, _, pol = rest.partition(",")
regs.set_gpio(eep, int(idx),
wake_enabled=en.strip() in ("1", "on", "true"),
wake_active_high=(pol.strip() in ("1", "on", "true")) if pol else None)
changes.append(f"gpio{int(idx)}.wake={rest}")
for spec in (args.set_configflag or []): # index=hexval e.g. 0=0x00000001
idx, _, val = spec.partition("=")
eep.set_config_flag(int(idx), int(val, 0))
changes.append(f"configflag{int(idx)}={int(val, 0):#010x}")
for spec in (args.set_raw or []): # offset=hexbytes e.g. 0x48=0103
off, _, hexb = spec.partition("=")
regs.set_region(eep, int(off, 0), bytes.fromhex(hexb.strip()))
changes.append(f"raw[{off}]={hexb.strip()}")
if args.ensure_signature:
eep.set_signature()
changes.append("signature=0xA5")
return changes
def cmd_edit(args):
try:
eep = _load_image_from_in(args) if args.in_file else Eeprom.blank(args.size)
changes = _apply_edits(eep, args)
except EepromError as e:
return _fail(args, str(e),
hint="VID/PID/strings need a base --in image that already "
"has descriptor blocks; MAC/signature work on blanks too")
if not changes:
return _fail(args, "no edits requested",
hint="pass --mac/--vid/--pid/--bcd/--serial/--set-string/"
"--ensure-signature")
out = args.out or args.in_file
if not out:
return _fail(args, "nowhere to write", hint="give --out <file.bin>")
BinFileBackend(out).write_image(eep.to_bytes())
payload = {"out": out, "changes": changes, "preview": _describe(eep)}
return _emit(args, True, payload,
human=f"applied [{', '.join(changes)}] -> {out}")
def cmd_read(args):
"""Read a device EEPROM to a .bin (FREE)."""
try:
data = EthtoolBackend(args.iface, sudo=args.sudo).read_image()
except BackendError as e:
return _fail(args, str(e))
BinFileBackend(args.out).write_image(data)
eep = Eeprom.from_bytes(data)
return _emit(args, True, {"out": args.out, "bytes": len(data),
"preview": _describe(eep)},
human=f"read {len(data)} bytes from {args.iface} -> {args.out}")
def _read_connected_image(args) -> bytes:
"""Read the connected device: libusb by default, ethtool if --iface given."""
if getattr(args, "iface", None):
return EthtoolBackend(args.iface, sudo=args.sudo).read_image()
be = UsbEepromBackend()
try:
return be.read_image()
finally:
be.close()
def cmd_verify(args):
"""Read device and compare against an expected image (FREE)."""
try:
expected = BinFileBackend(args.image).read_image()
actual = _read_connected_image(args)
except BackendError as e:
return _fail(args, str(e))
n = min(len(expected), len(actual))
diffs = [{"offset": i, "expected": expected[i], "actual": actual[i]}
for i in range(n) if expected[i] != actual[i]]
if len(expected) != len(actual):
diffs.append({"offset": None, "note":
f"length differs expected={len(expected)} actual={len(actual)}"})
ok = not diffs
return _emit(args, ok, {"match": ok, "diff_count": len(diffs),
"diffs": diffs[:32]},
human=("device matches image" if ok
else f"{len(diffs)} byte(s) differ"),
hint=None if ok else "re-program or investigate the mismatched offsets")
# ---- GATED hardware writers ----------------------------------------------
def _refuse_physical(args, action):
tgt = getattr(args, "iface", None) or "the connected 0424:7800 (libusb)"
return _emit(
args, False,
{"gated": True, "action": action},
human=f"REFUSED: {action} writes to {tgt}",
hint="physical writes are gated. The operator/agent must clear a "
"mesh escalate PROCEED, then re-run with --force-physical-write.")
def cmd_program(args):
if not args.force_physical_write:
return _refuse_physical(args, "program")
used = f"ethtool:{args.iface}" if args.iface else "libusb"
try:
image = BinFileBackend(args.image).read_image()
eep = Eeprom.from_bytes(image)
if not eep.has_signature and not args.allow_unsigned:
return _fail(args, "image byte0 != 0xA5 (unsigned)",
hint="run `edit --ensure-signature` first, or pass "
"--allow-unsigned to write it as-is")
if args.iface:
EthtoolBackend(args.iface, sudo=args.sudo).write_bytes(0, image)
else:
be = UsbEepromBackend(size=len(image)) # sudo-free, signature last
try:
be.program_image(image)
finally:
be.close()
except (BackendError, EepromError) as e:
return _fail(args, str(e))
result = {"backend": used, "bytes": len(image), "verified": None}
if not args.no_verify:
try:
actual = _read_connected_image(args)
except BackendError as e:
return _fail(args, f"programmed but could not read back to verify: {e}")
n = min(len(image), len(actual))
diffs = [i for i in range(n) if image[i] != actual[i]]
result["verified"] = (not diffs and len(image) == len(actual))
result["diff_count"] = len(diffs)
if not result["verified"]:
return _emit(args, False, result,
human=f"programmed but VERIFY FAILED ({len(diffs)} bytes)",
hint="do not trust this image; re-read and re-program")
return _emit(args, True, result,
human=f"programmed {len(image)} bytes via {used}"
+ ("" if args.no_verify else " (verified)")
+ "; replug/USB-reset to reload")
def cmd_erase(args):
if not args.force_physical_write:
return _refuse_physical(args, "erase")
used = f"ethtool:{args.iface}" if args.iface else "libusb"
payload = bytes([0x00]) if args.mode == "signature" else bytes([0xFF] * args.size)
off = 0
try:
if args.iface:
EthtoolBackend(args.iface, sudo=args.sudo).write_bytes(off, payload)
else:
be = UsbEepromBackend(size=args.size)
try:
be.write_bytes(off, payload)
finally:
be.close()
except BackendError as e:
return _fail(args, str(e))
touched = ("byte0 signature cleared (recoverable fallback)"
if args.mode == "signature"
else f"wrote 0xFF across {args.size} bytes")
return _emit(args, True, {"backend": used, "mode": args.mode},
human=f"erase ({args.mode}) via {used}: {touched}; replug to reload")
def _bulk_old_identity(args, dev):
"""Best-effort read of a connected unit's CURRENT MAC/serial for the record."""
try:
cur = Eeprom.from_bytes(_read_connected_image(args))
if cur.has_signature:
sv = cur.string("serial")
return cur.mac, (sv.value if sv.present else "")
except (BackendError, EepromError):
pass
return (dev.get("mac") if dev else None), ""
def cmd_bulk(args):
"""Bulk-program many adapters with an auto-incrementing MAC/serial and a
resumable manifest. Dry-run is the default; real writes are gated."""
from . import bulk
try:
seq = bulk.sequence(args.base, args.count, step=args.step,
local_admin=args.local_admin, scheme=args.scheme)
manifest = bulk.Manifest(args.manifest)
except EepromError as e:
return _fail(args, str(e))
start = manifest.next_index()
remaining = [u for u in seq if u["index"] >= start
and not manifest.has_mac(u["mac"])]
# DRY-RUN is the default AND the forced behaviour whenever a real write is
# not explicitly authorised -- so `bulk` never touches hardware by accident.
if args.dry_run or not args.force_physical_write:
for u in remaining:
manifest.append(index=u["index"], usb_path="", old_mac="",
old_serial="", new_mac=u["mac"],
new_serial=u["serial"], verify="DRYRUN")
preview = "\n".join(
f" [{u['index']:>3}] MAC {u['mac']} serial {u['serial']}"
for u in remaining) or " (nothing to do; manifest already complete)"
return _emit(args, True,
{"mode": "dry-run", "count": args.count, "step": args.step,
"start_index": start, "planned": remaining,
"manifest": manifest.csv_path},
human=(f"DRY-RUN preview: {len(remaining)} unit(s) "
f"(resume from index {start}); no device touched.\n"
f"{preview}\nManifest -> {manifest.csv_path}"),
hint="real burn: escalate a mesh session-scoped allow, then "
"re-run with --force-physical-write")
# REAL burn (gated). One unit per invocation by default (replug + re-run);
# --loop prompts between units in a single run for bench use.
try:
tmpl = (Eeprom.from_bytes(BinFileBackend(args.template).read_image())
if args.template else Eeprom.default_template())
except (BackendError, EepromError) as e:
return _fail(args, f"template load failed: {e}")
to_do = remaining if args.loop else remaining[:1]
burned = []
for u in to_do:
if args.loop:
try:
input(f"\n>> Insert unit for index {u['index']} "
f"(MAC {u['mac']}); press Enter (Ctrl-C to stop)... ")
except (EOFError, KeyboardInterrupt):
break
devs = scan_devices(probe_eeprom=False)
if not devs:
return _fail(args, f"no LAN7800 attached for index {u['index']}")
dev = devs[0]
old_mac, old_serial = _bulk_old_identity(args, dev)
# NOT-SWAPPED GUARD: if the connected unit's EEPROM already carries the
# identity we just wrote to the previous unit, barrett almost certainly
# did not swap adapters. Refuse to re-burn the same physical unit.
last = manifest.last_burned_mac()
if last and old_mac and old_mac.upper() == last.upper():
return _emit(args, False,
{"same_unit": True, "current_mac": old_mac,
"next_index": u["index"], "manifest": manifest.csv_path},
human=(f"This looks like the unit you just programmed "
f"(MAC {old_mac}). Unplug it and insert the next "
f"blank adapter, then retry."),
hint="not-swapped guard: refusing to re-burn the same unit")
try:
img = bulk.build_image(tmpl, u["mac"], u["serial"])
be = UsbEepromBackend(size=len(img))
try:
be.program_image(img) # LED-mux-gated, signature LAST
finally:
be.close()
actual = _read_connected_image(args)
ok = bytes(actual) == bytes(img)
except (BackendError, EepromError) as e:
manifest.append(index=u["index"], usb_path=dev.get("usb_path"),
old_mac=old_mac, old_serial=old_serial,
new_mac=u["mac"], new_serial=u["serial"], verify="FAIL")
return _fail(args, f"index {u['index']} program failed: {e}")
manifest.append(index=u["index"], usb_path=dev.get("usb_path"),
old_mac=old_mac, old_serial=old_serial,
new_mac=u["mac"], new_serial=u["serial"],
verify="PASS" if ok else "FAIL")
burned.append({"index": u["index"], "mac": u["mac"],
"serial": u["serial"], "verified": ok})
if not ok:
return _emit(args, False,
{"burned": burned, "manifest": manifest.csv_path},
human=f"index {u['index']} VERIFY FAILED after write")
return _emit(args, True,
{"mode": "burn", "burned": burned, "manifest": manifest.csv_path},
human=(f"burned {len(burned)} unit(s); manifest {manifest.csv_path}"
+ ("" if args.loop else "; replug next unit + re-run")))
# ---- argument parser ------------------------------------------------------
def build_parser():
p = argparse.ArgumentParser(
prog="lan7800prog",
description="LAN7800 EEPROM programmer (Core v1, LAN7800 only)")
p.add_argument("--json", action="store_true", help="emit machine-readable JSON")
sub = p.add_subparsers(dest="cmd", required=True)
def add_target(sp, iface=True, infile=True):
if infile:
sp.add_argument("--in", dest="in_file", help="base .bin image")
if iface:
sp.add_argument("--iface", help="network interface of the LAN7800")
sp.add_argument("--no-sudo", dest="sudo", action="store_false",
default=True, help="do not prefix ethtool with sudo")
sp = sub.add_parser("scan", help="enumerate attached LAN7800 adapters (FREE)")
sp.add_argument("--no-probe", action="store_true",
help="skip the ethtool EEPROM signature probe")
sp.set_defaults(func=cmd_scan)
sp = sub.add_parser("read-device",
help="read the connected LAN7800 (auto: netdev->ethtool "
"else libusb) (FREE)")
sp.add_argument("--out", help="also write the image to this .bin")
sp.add_argument("--size", type=int, default=MAX_EEPROM_SIZE, choices=SIZES)
sp.add_argument("--no-sudo", dest="sudo", action="store_false", default=True)
sp.set_defaults(func=cmd_read_device)
sp = sub.add_parser("udev-rule",
help="print the udev rule needed for libusb device access")
sp.set_defaults(func=cmd_udev_rule)
sp = sub.add_parser("new",
help="create a default LAN7800 template .bin "
"(or --blank for an empty image)")
sp.add_argument("--out", required=True)
sp.add_argument("--size", type=int, default=MAX_EEPROM_SIZE, choices=SIZES)
sp.add_argument("--blank", action="store_true",
help="all-zero image instead of the default template")
sp.add_argument("--sign", action="store_true",
help="with --blank: lay down the 0xA5 signature")
sp.set_defaults(func=cmd_new)
sp = sub.add_parser("info", help="parse & print an image or a live device")
add_target(sp)
sp.set_defaults(func=cmd_info)
sp = sub.add_parser("edit", help="edit fields, write a new .bin (no hardware)")
sp.add_argument("--in", dest="in_file", help="base .bin (required for VID/PID/strings)")
sp.add_argument("--out", help="output .bin (defaults to --in)")
sp.add_argument("--size", type=int, default=MAX_EEPROM_SIZE, choices=SIZES)
sp.add_argument("--mac")
sp.add_argument("--vid")
sp.add_argument("--pid")
sp.add_argument("--bcd", help="bcdDevice / DID")
sp.add_argument("--serial")
sp.add_argument("--set-string", action="append",
help="name=value (manufacturer/product/serial/configuration/interface)")
sp.add_argument("--set-led", action="append", metavar="I=MODE",
help="LED index=mode(0..15), e.g. 0=10")
sp.add_argument("--led-enable", action="append", metavar="I=1|0")
sp.add_argument("--led-blink", help="blink rate code 0=2.5Hz 1=5Hz 2=10Hz 3=20Hz")
sp.add_argument("--set-gpio", action="append", metavar="I=dir,drive,out",
help="e.g. 0=out,push-pull,1")
sp.add_argument("--gpio-wake", action="append", metavar="I=en,pol",
help="e.g. 0=1,1")
sp.add_argument("--set-configflag", action="append", metavar="I=HEX",
help="Config Flags 0..3, e.g. 0=0x00000001")
sp.add_argument("--set-raw", action="append", metavar="OFF=HEX",
help="raw bytes, e.g. 0x48=0103 (authoritative fallback)")
sp.add_argument("--ensure-signature", action="store_true")
sp.set_defaults(func=cmd_edit)
sp = sub.add_parser("read", help="read device EEPROM -> .bin (FREE)")
sp.add_argument("--iface", required=True)
sp.add_argument("--out", required=True)
sp.add_argument("--no-sudo", dest="sudo", action="store_false", default=True)
sp.set_defaults(func=cmd_read)
sp = sub.add_parser("verify",
help="compare connected device against a .bin (FREE; "
"libusb by default, --iface for ethtool)")
sp.add_argument("--iface", help="use ethtool via this iface (default: libusb)")
sp.add_argument("--image", required=True)
sp.add_argument("--no-sudo", dest="sudo", action="store_false", default=True)
sp.set_defaults(func=cmd_verify)
sp = sub.add_parser("program",
help="GATED: write a .bin to the connected device + verify "
"(libusb by default, --iface for ethtool)")
sp.add_argument("--iface", help="use ethtool via this iface (default: libusb)")
sp.add_argument("--image", required=True)
sp.add_argument("--force-physical-write", action="store_true",
help="required; set only after a cleared mesh escalation")
sp.add_argument("--allow-unsigned", action="store_true")
sp.add_argument("--no-verify", action="store_true")
sp.add_argument("--no-sudo", dest="sudo", action="store_false", default=True)
sp.set_defaults(func=cmd_program)
sp = sub.add_parser("erase", help="GATED: clear signature or full 0xFF erase")
sp.add_argument("--iface", help="use ethtool via this iface (default: libusb)")
sp.add_argument("--mode", choices=["signature", "full"], default="signature")
sp.add_argument("--size", type=int, default=MAX_EEPROM_SIZE, choices=SIZES)
sp.add_argument("--force-physical-write", action="store_true")
sp.add_argument("--no-sudo", dest="sudo", action="store_false", default=True)
sp.set_defaults(func=cmd_erase)
sp = sub.add_parser(
"bulk",
help="program many adapters with an auto-incrementing identity "
"(dry-run by default; real writes gated)")
sp.add_argument("--base", required=True,
help="base MAC, e.g. 00:80:0F:78:00:00 (OUI not hardcoded)")
sp.add_argument("--count", type=int, required=True, help="number of units")
sp.add_argument("--step", type=int, default=1, help="MAC increment (default 1)")
sp.add_argument("--manifest", required=True,
help="manifest path (.csv; a .json is written alongside)")
sp.add_argument("--scheme", default="mac-hex",
help="serial scheme (default mac-hex: iSerial = MAC hex)")
sp.add_argument("--local-admin", dest="local_admin", action="store_true",
help="force the locally-administered 02: bit on the base MAC")
sp.add_argument("--template",
help="base .bin (needs a serial slot); default: built-in template")
sp.add_argument("--dry-run", dest="dry_run", action="store_true",
help="compute+log the sequence, write DRYRUN rows; no device")
sp.add_argument("--loop", action="store_true",
help="interactive: prompt to insert each unit within one run")
sp.add_argument("--force-physical-write", dest="force_physical_write",
action="store_true",
help="authorize REAL writes (needs a mesh session-scoped allow)")
sp.add_argument("--iface", help="use ethtool via this iface instead of libusb")
sp.add_argument("--no-sudo", dest="sudo", action="store_false", default=True)
sp.set_defaults(func=cmd_bulk)
return p
def main(argv=None):
parser = build_parser()
args = parser.parse_args(argv)
try:
return args.func(args)
except (EepromError, BackendError) as e:
return _fail(args, str(e))
except (ValueError, KeyError) as e:
# user-input parse errors (int(), bytes.fromhex, dict lookups) — never
# leak a Python traceback / server paths to the caller or the webapp.
return _fail(args, f"invalid input: {e}",
hint="check numeric/hex arguments and field names")
except BrokenPipeError:
return 0
except Exception as e: # last-resort: clean message, no traceback
return _fail(args, f"unexpected error: {type(e).__name__}: {e}")
if __name__ == "__main__":
sys.exit(main())