AnasSarkiz/muse-voice-remote
Muse Voice Remote — bare ESP32-S3R8, 155 TOP JLCPCB parts with real supplier CAD, native 3D assembly and GLB exports. Revised 17 mm case allocation; 0.30 mm via drill /0.45 mm pad. Checked prototype PCB; mechanical, RF and MUSE qualification pending.
- Version
- 1.3.2
- License
- unset
- Stars
- 0
scripts/research-audit.py
#!/usr/bin/env python3
"""Summarize actual layout and procurement evidence without fabrication approval.
Distances are measurable lower bounds, not substitutes for switching-loop,
impedance, enclosure CAD or physical qualification.
"""
import collections
import datetime
import hashlib
import json
import math
from pathlib import Path
import pcbnew
root = Path(__file__).resolve().parents[1]
board_path = root / "hardware/MuseVoiceRemote.kicad_pcb"
board = pcbnew.LoadBoard(str(board_path))
footprints = {f.GetReference(): f for f in board.GetFootprints()}
checks = [
("charger input bypass", "U_CHARGE", "13", "C_CHG_IN", "1"),
("charger system bypass", "U_CHARGE", "10", "C_CHG_OUT", "1"),
("charger battery bypass", "U_CHARGE", "2", "C_CHG_BAT", "1"),
("regulator input bypass", "U_REG", "5", "C_REG_IN", "1"),
("regulator output bypass", "U_REG", "1", "C_REG_OUT1", "1"),
("regulator switching connection L1", "U_REG", "4", "L_REG", "1"),
("regulator switching connection L2", "U_REG", "2", "L_REG", "2"),
("backlight input bypass", "U_BACKLIGHT", "5", "C_BL_IN", "1"),
("backlight switch-to-inductor", "U_BACKLIGHT", "1", "L_BL", "2"),
("backlight switch-to-diode", "U_BACKLIGHT", "1", "D_BL", "2"),
("backlight output bypass", "D_BL", "1", "C_BL_OUT", "1"),
("backlight feedback", "U_BACKLIGHT", "3", "R_BL_FB", "1"),
("speaker supply bypass 100 nF", "U_AMP", "7", "C_AMP1", "1"),
("speaker supply bulk", "U_AMP", "7", "C_AMP2", "1"),
("microphone 1 bypass", "MIC1", "5", "C_MIC1", "1"),
("microphone 2 bypass", "MIC2", "5", "C_MIC2", "1"),
]
def pad(ref, number):
return next(p for p in footprints[ref].Pads() if p.GetNumber() == number)
placement = []
for title, ref, number, other, other_number in checks:
a, b = pad(ref, number).GetPosition(), pad(other, other_number).GetPosition()
distance = math.hypot(pcbnew.ToMM(a.x-b.x), pcbnew.ToMM(a.y-b.y))
placement.append(dict(check=title, from_pad=f"{ref}.{number}", to_pad=f"{other}.{other_number}",
straight_line_mm=round(distance, 3)))
routing = []
for net in ["USB_DM", "USB_DP", "USB_DM_MCU", "USB_DP_MCU", "REG_L1", "REG_L2", "BL_SW", "V3_3", "SYS", "BAT", "VBUS_FUSED"]:
items = [t for t in board.GetTracks() if t.GetNetname() == net]
tracks = [t for t in items if not isinstance(t, pcbnew.PCB_VIA)]
routing.append(dict(net=net, total_branch_length_mm=round(sum(pcbnew.ToMM(t.GetLength()) for t in tracks),3),
minimum_width_mm=min(pcbnew.ToMM(t.GetWidth()) for t in tracks),
routing_vias=sum(isinstance(t,pcbnew.PCB_VIA) for t in items),
layers=sorted(set(board.GetLayerName(t.GetLayer()) for t in tracks))))
selection = json.loads((root / "docs/jlcpcb-import-selection.json").read_text())
procurement = []
for selected in selection["parts"]:
raw = json.loads((root / selected["raw_response"]).read_text())
supplier = next(p for p in raw["data"]["componentPageInfo"]["list"] if p["componentCode"] == selected["lcsc"])
# Catalog quantities are conservative screening inputs. Actual checkout
# requirements and selected PCBA process remain authoritative.
count = selected["quantity"]
loss = supplier.get("lossNumber") or 0
needed = max(2*count+loss, supplier.get("leastPatchNumber") or 0, supplier.get("minPurchaseNum") or 0)
procurement.append(dict(lcsc=selected["lcsc"], mpn=selected["mpn"], per_board=count,
stock=supplier["stockCount"], supplier_loss_allowance=loss,
supplier_least_patch=supplier.get("leastPatchNumber"),
supplier_min_purchase=supplier.get("minPurchaseNum"),
two_pcb_screen_required=needed, two_pcb_screen_pass=supplier["stockCount"]>=needed,
category=supplier.get("componentTypeEn"), attributes=supplier.get("attributes",[]),
checked_at=selected["checked_at"]))
inner_signals = collections.Counter(t.GetNetname() for t in board.GetTracks()
if not isinstance(t,pcbnew.PCB_VIA) and t.GetLayer()==pcbnew.In1_Cu and t.GetNetname()!="GND")
result = dict(checked_at=datetime.datetime.now(datetime.timezone.utc).isoformat(),
board_sha256=hashlib.sha256(board_path.read_bytes()).hexdigest(),
finished_device_qualified=False,
prototype_manufacturing_verified=(root/"manufacturing/verification.json").exists() and json.loads((root/"manufacturing/verification.json").read_text()).get("source_native_sha256")==hashlib.sha256(board_path.read_bytes()).hexdigest(),
measurement_limits="Pad-centre distances are lower bounds; branch sums are not point-to-point lengths. No loop-area, impedance, thermal or enclosure simulation is claimed.",
placement_checks=placement, routing_metrics=routing,
inner_ground_layer_signal_segments=dict(inner_signals), procurement=procurement,
procurement_two_pcb_screen_pass=all(p["two_pcb_screen_pass"] for p in procurement),
calculated_nominals=dict(charge_current_A=0.890/4420, termination_current_A=0.030*2940/4420,
resistor_input_limit_A=1610/3240, charge_timer_hours=10*48*46.4/3600,
total_backlight_current_A=0.204/5.1, battery_adc_at_4V2_V=4.2/2,
xtal_load_pF=22/2+4),
load_budget_assumptions=dict(esp32_supply_design_A=0.5, flash_other_logic_A=0.07,
lcd_logic_A=0.04, backlight_input_A=4.5*(0.204/5.1)/(3.2*0.8),
motor_start_A=0.12,
note="Design allowances, not measured simultaneous loads; transducer, efficiency, battery and wiring limits remain to qualify."))
(root / "docs/research/board-audit.json").write_text(json.dumps(result,indent=2)+"\n")
print(f"Measured {len(placement)} placement paths and {len(routing)} routed nets; supplier screening {len(procurement)} MPNs. Finished-device qualification remains false; prototype manufacturing evidence is tracked separately.")