Treat KiCad PNM as MPN and skip missing U* as INSUFFICIENT_EVIDENCE.
HubAudio CSV has no U13–U39 rows; those refs stay without an invented part number. IC Value fills MPN when PNM is blank. Progress no longer labels netlist-only ICs as “no MPN in BOM”.
This commit is contained in:
@@ -16,6 +16,69 @@ NetlistFormat = Literal["pads", "edif", "kicad_xml", "kicad_sexp", "kicad_sch"]
|
||||
_GND_EXACT = {"GND", "AGND", "DGND", "PGND", "VSS", "0V"}
|
||||
_DNP_TRUE = {"1", "y", "yes", "true", "dnp", "dni", "x"}
|
||||
_FITTED_FALSE = {"0", "n", "no", "false"}
|
||||
# KiCad exports often label the part column PNM (not MPN).
|
||||
_MPN_ALIASES = (
|
||||
"PNM",
|
||||
"MPN",
|
||||
"Manufacturer Part Number",
|
||||
"Manufacturer Part",
|
||||
"Part Number",
|
||||
"Mfr. Part",
|
||||
"Mfg Part #",
|
||||
)
|
||||
|
||||
|
||||
def _row_get(row: dict[str, str | None], *names: str) -> str:
|
||||
keys = {str(k).lower(): k for k in row if k is not None}
|
||||
for name in names:
|
||||
key = keys.get(name.lower())
|
||||
if key is None:
|
||||
continue
|
||||
val = (row.get(key) or "").strip()
|
||||
if val:
|
||||
return val
|
||||
return ""
|
||||
|
||||
|
||||
def resolve_row_mpn(
|
||||
row: dict[str, str | None],
|
||||
refs: list[str],
|
||||
mpn_col: str = "Manufacturer Part Number",
|
||||
) -> str | None:
|
||||
"""PNM/MPN aliases, then IC Value when the part column is blank. Never invent."""
|
||||
mpn = _row_get(row, mpn_col, *_MPN_ALIASES)
|
||||
if mpn:
|
||||
return mpn
|
||||
if any(re.match(r"^U\d", r, re.I) for r in refs):
|
||||
value = _row_get(row, "Value", "Comment")
|
||||
if value:
|
||||
return value
|
||||
return None
|
||||
|
||||
|
||||
def ic_mpn_skip_reason(
|
||||
ref: str,
|
||||
*,
|
||||
mpn: str | None,
|
||||
value: str | None,
|
||||
footprint: str | None = None,
|
||||
in_bom: bool,
|
||||
bom_value: str | None = None,
|
||||
) -> str | None:
|
||||
"""Progress/not_reviewed text when an IC has no part number evidence."""
|
||||
got = (mpn or "").strip() or (value or "").strip()
|
||||
if got:
|
||||
return None
|
||||
fp = (footprint or "").strip() or "empty"
|
||||
if not in_bom:
|
||||
return (
|
||||
f"INSUFFICIENT_EVIDENCE: {ref} not in BOM "
|
||||
f"(Value={(value or '').strip() or 'empty'}; Footprint={fp})"
|
||||
)
|
||||
return (
|
||||
f"INSUFFICIENT_EVIDENCE: {ref} BOM PNM/MPN empty "
|
||||
f"(Value={bom_value!r})"
|
||||
)
|
||||
|
||||
|
||||
def detect_netlist_format(content: bytes | str) -> NetlistFormat:
|
||||
@@ -180,17 +243,13 @@ def parse_bom(
|
||||
has_variant_col = bool(colnames & {"variant"})
|
||||
|
||||
for row in reader:
|
||||
refs_raw = row.get(reference_col, "")
|
||||
value = row.get("Value", "") or row.get("Comment", "")
|
||||
footprint = row.get("Footprint", "")
|
||||
mpn = (row.get(mpn_col, "") or "").strip() or None
|
||||
lcsc = row.get("LCSC", "") or None
|
||||
datasheet_url = (row.get("Datasheet", "") or "").strip() or None
|
||||
refs_raw = _row_get(row, reference_col) or (row.get(reference_col, "") or "")
|
||||
value = _row_get(row, "Value", "Comment")
|
||||
footprint = _row_get(row, "Footprint")
|
||||
refs = [r.strip() for r in refs_raw.split(",") if r.strip()]
|
||||
if not mpn:
|
||||
mpn = (row.get("PNM", "") or "").strip() or None
|
||||
if not mpn and any(re.match(r"^U\d", r, re.I) for r in refs):
|
||||
mpn = (value or "").strip() or None
|
||||
mpn = resolve_row_mpn(row, refs, mpn_col=mpn_col)
|
||||
lcsc = _row_get(row, "LCSC") or None
|
||||
datasheet_url = _row_get(row, "Datasheet") or None
|
||||
dnp_raw = (row.get("DNP") or row.get("DNI") or "").strip().lower()
|
||||
fitted_raw = (row.get("Fitted") or row.get("Populate") or "").strip().lower()
|
||||
variant = (row.get("Variant") or row.get("variant") or "").strip() or None
|
||||
|
||||
@@ -9,6 +9,7 @@ from backend.periscopex.cad_bridge import annotate_findings_cad, cad_index_from_
|
||||
from backend.periscopex.finding_engine import complete_findings
|
||||
from backend.periscopex.functional_groups import FunctionalGroupsReport
|
||||
from backend.periscopex.models import ComponentType, DesignGraph, Finding, LayoutGraph
|
||||
from backend.periscopex.parsers import ic_mpn_skip_reason
|
||||
from backend.periscopex.pcb_inventory import PcbInventoryReport
|
||||
from backend.periscopex.pcb_review import (
|
||||
PCB_SYSTEM_PROMPT,
|
||||
@@ -67,8 +68,16 @@ async def review_pcb_ics(
|
||||
if comp.component_type != ComponentType.IC:
|
||||
continue
|
||||
mpn = (comp.mpn or "").strip() or (comp.value or "").strip()
|
||||
if not mpn:
|
||||
skipped.append({"designator": ref, "reason": "no MPN in BOM"})
|
||||
skip = ic_mpn_skip_reason(
|
||||
ref,
|
||||
mpn=comp.mpn,
|
||||
value=comp.value,
|
||||
footprint=comp.footprint,
|
||||
in_bom=ref in (graph.bom_fields or {}),
|
||||
bom_value=(graph.bom_fields or {}).get(ref, {}).get("value"),
|
||||
)
|
||||
if skip:
|
||||
skipped.append({"designator": ref, "reason": skip})
|
||||
continue
|
||||
cons = _match_constraints(mpn, constraints_map)
|
||||
if not _has_library_extraction(cons):
|
||||
|
||||
@@ -28,6 +28,7 @@ from backend.periscopex.led_current_check import check_led_current
|
||||
from backend.periscopex.lifecycle import check_lifecycle, load_lifecycle_dir
|
||||
from backend.periscopex.models import ComponentType, DesignGraph, Finding, ValidationReport
|
||||
from backend.periscopex.nc_pin_check import check_nc_pins
|
||||
from backend.periscopex.parsers import ic_mpn_skip_reason
|
||||
from backend.periscopex.passive_rail_check import (
|
||||
check_i2c_pullups,
|
||||
check_reset_pullups,
|
||||
@@ -150,10 +151,18 @@ async def validate_design_async(
|
||||
if comp.component_type != ComponentType.IC:
|
||||
continue
|
||||
mpn = (comp.mpn or "").strip() or (comp.value or "").strip()
|
||||
if not mpn:
|
||||
not_reviewed.append({"designator": ref, "reason": "no MPN in BOM"})
|
||||
skip = ic_mpn_skip_reason(
|
||||
ref,
|
||||
mpn=comp.mpn,
|
||||
value=comp.value,
|
||||
footprint=comp.footprint,
|
||||
in_bom=ref in (graph.bom_fields or {}),
|
||||
bom_value=(graph.bom_fields or {}).get(ref, {}).get("value"),
|
||||
)
|
||||
if skip:
|
||||
not_reviewed.append({"designator": ref, "reason": skip})
|
||||
if on_progress:
|
||||
await on_progress(ref, 0, "skipped", "no MPN in BOM")
|
||||
await on_progress(ref, 0, "skipped", skip)
|
||||
continue
|
||||
pdf = _find_pdf(mpn, pdf_dir_path, storage=storage)
|
||||
if pdf:
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
|
||||
What's new in Periscope.
|
||||
|
||||
## 2.60.4 — 2026-09-21 — KiCad PNM and honest missing-MPN skips
|
||||
|
||||
KiCad BOM column `PNM` is an MPN alias. Grouped `Reference` lists still split. IC `Value` fills MPN when PNM is blank (PCA9534ARGTR). Netlist-only U* with no BOM row are `INSUFFICIENT_EVIDENCE` (quoted Value/Footprint), not a silent drop and not an invented part number.
|
||||
|
||||
- [Fixed] Progress skip text no longer says “no MPN in BOM” when the designator is absent from the CSV.
|
||||
- [New] HubAudio KiCad BOM fixture: PNM header, U2/U3 Value fallback, L4 bead PNM, U13…U39 not invented.
|
||||
|
||||
## 2.60.3 — 2026-09-21 — Ferrite Z from library MPN datasheet
|
||||
|
||||
graph_build looks up the bead MPN PDF in project `uploads/datasheets` and `library/datasheets` (named files and blob refs). Z is filled only when the PDF states impedance (e.g. 120 Ω @ 100 MHz). Missing Z stays omitted; graph_build does not abort.
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
"Reference","Qty","Value","DNP","Exclude from BOM","Exclude from Board","Footprint","Datasheet","Description","PNM"
|
||||
"BOOT1,RESET1","2","TS3425UA-3x4x2.5-160","","","","MIKILAB_TS3425UA_3x4x2_5_160:TS3425UA-3x4x2.5-160","","SMD tactile switch, 4-pin, 1-2 common and 3-4 common, 160gf","TS3425UA-3x4x2.5-160"
|
||||
"C21,C22,C33,C47,C48,C49,C59,C60,C61,C65,C68,C70,C72,C73,C74,C75,C76,C77,C78,C79,C80,C86,C88,C119,C129,C134,C135,C162,C163,C172,C173,C174,C191,C192,C193,C194,C202","37","100nF","","","","Capacitor_SMD:C_0805_2012Metric","","GRM21BR71H104KA01L","GRM21BR71H104KA01L"
|
||||
"C23,C118","2","47uF","","","","Capacitor_SMD:C_1206_3216Metric","","CL32A476KOJNNNE","CL32A476KOJNNNE"
|
||||
"C24,C38,C44","3","1uF","","","","Capacitor_SMD:C_1206_3216Metric","","12063C105KAT2A","12063C105KAT2A"
|
||||
"C25,C26,C34,C36,C39,C40,C41,C45,C51,C52,C55,C56,C58,C121,C122,C158,C175,C176,C178,C179","20","10uF","","","","Capacitor_SMD:C_1206_3216Metric","","GRM31CR71E106KA12L","GRM31CR71E106KA12L"
|
||||
"C27,C43,C53,C57,C63,C67,C105,C108,C111,C114,C117,C123,C165,C166,C167,C168,C169,C170,C171,C180,C182,C183,C190","23","1uF","","","","Capacitor_SMD:C_0805_2012Metric","","GCM21BR71E105KA56L","GCM21BR71E105KA56L"
|
||||
"C29,C30","2","22pF","","","","Capacitor_SMD:C_0805_2012Metric","","CL21C220JBANNNC","CL21C220JBANNNC"
|
||||
"C31,C32,C84,C85,C136,C164","6","10uF","","","","Capacitor_SMD:C_0805_2012Metric","","GRM21BR61E106KA73L","GRM21BR61E106KA73L"
|
||||
"C37,C42","2","1nF","","","","Capacitor_SMD:C_0805_2012Metric","","CC0805KRX7R9BB102","CC0805KRX7R9BB102"
|
||||
"C62","1","470pF","","","","Capacitor_SMD:C_0805_2012Metric","","GRM2165C1H471JA01D","GRM2165C1H471JA01D"
|
||||
"C64,C189,C201,C203,C210,C211","6","22uF","","","","Capacitor_SMD:C_0805_2012Metric","","GRM21BZ71A226ME15L","GRM21BZ71A226ME15L"
|
||||
"C81","1","150pF","","","","Capacitor_SMD:C_0805_2012Metric","","08055A151FAT2A","08055A151FAT2A"
|
||||
"C82","1","5.6nF","","","","Capacitor_SMD:C_0805_2012Metric","","GCM2195C1K562FA16D","GCM2195C1K562FA16D"
|
||||
"C100","1","33pF","","","","Capacitor_SMD:C_0603_1608Metric","","GQM1875C2E330JB12D","GQM1875C2E330JB12D"
|
||||
"C101","1","2.7pF","","","","Capacitor_SMD:C_0603_1608Metric","","GQM1875C2E2R7BB12D","GQM1875C2E2R7BB12D"
|
||||
"C103,C106,C109,C112","4","8.2pF","","","","Capacitor_SMD:C_0805_2012Metric","","GQM2195C2E8R2CB12D","GQM2195C2E8R2CB12D"
|
||||
"C104,C107,C110,C113","4","2.2nF","","","","Capacitor_SMD:C_0805_2012Metric","","GRM2165C1H222JA01D","GRM2165C1H222JA01D"
|
||||
"C115,C116","2","15pF","","","","Capacitor_SMD:C_0805_2012Metric","","GQM2195C2E150JB12D","GQM2195C2E150JB12D"
|
||||
"C132,C133","2","20pF","","","","Capacitor_SMD:C_0805_2012Metric","","GRM21A5C2J200JA01","GRM21A5C2J200JA01"
|
||||
"C195,C196,C197,C198,C199,C200,C204,C205,C206,C207,C208,C209","12","2.2uF","","","","Capacitor_SMD:C_0805_2012Metric","","CL21B225KPFNNNE","CL21B225KPFNNNE"
|
||||
"D1,D4","2","D_TVS","","","","Diode_SMD:D_SOD-882","","PESD5V0X1BL",""
|
||||
"D2","1","ESD9B5.0ST5G","","","","Diode_SMD:D_SOD-923","https://www.onsemi.com/pub/Collateral/ESD9B-D.PDF","ESD9B5.0ST5G","ESD9B5.0ST5G"
|
||||
"D3","1","LED","","","","LED_SMD:LED_0805_2012Metric","","LTST-C170TBKT","LTST-C170TBKT"
|
||||
"FB1","1","BLM18BB600SN1","","","","Inductor_SMD:L_0603_1608Metric","","BLM18BB600SN1","BLM18BB600SN1"
|
||||
"J1","1","RJ45_Abracon_ARJP11A-MASA-B-A-EMU2","","","","Connector_RJ:RJ45_Abracon_ARJP11A-MA_Horizontal","https://abracon.com/Magnetics/lan/ARJP11A.PDF","RJ45 PoE 10/100 Base-TX Jack with Magnetic Module","RJ45_Abracon_ARJP11A-MASA-B-A-EMU2"
|
||||
"J2","1","USB_C_Receptacle_USB2.0_16P","","","","Connector_USB:USB_C_Receptacle_HRO_TYPE-C-31-M-12","https://www.usb.org/sites/default/files/documents/usb_type-c.zip","TYPE-C-31-M-12","TYPE-C-31-M-12"
|
||||
"J3","1","AFBR-1624Z","","","","OptoDevice:Broadcom_AFBR-16xxZ_Horizontal","https://docs.broadcom.com/docs/AV02-4369EN","AFBR-1624Z","AFBR-1624Z"
|
||||
"J4","1","AFBR-2529Z","","","","OptoDevice:Broadcom_AFBR-16xxZ_Horizontal","https://docs.broadcom.com/doc/AV02-4369EN","AFBR-2529Z","AFBR-2529Z"
|
||||
"J5","1","RF/COAXIAL SMA BHD JACK STR 50OHM SOLDER","","","","Connector_Coaxial:SMA_Amphenol_132289_EdgeMount","","RF/COAXIAL SMA BHD JACK STR 50OHM SOLDER","RF/COAXIAL SMA BHD JACK STR 50OHM SOLDER"
|
||||
"J6,J8,J9","3","SJ-3506-SMT-TR","","","","MIKILAB_SJ_3506_SMT_TR:CUI_SJ-3506-SMT-TR","","SJ-3506-SMT-TR","SJ-3506-SMT-TR"
|
||||
"J_BAT2,J_POWER1","2","Conn_01x02","","","","Connector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_Horizontal","","Conn_01x02","Conn_01x02"
|
||||
"L4,L6,L8,L18,L21,L23,L24,L26","8","120 ohm","","","","Inductor_SMD:L_0805_2012Metric","","BLM21PG121SN1D","BLM21PG121SN1D"
|
||||
"L5,L10","2","47uH","","","","Inductor_SMD:L_0603_1608Metric","","LSQNB160808T470M","LSQNB160808T470M"
|
||||
"L12,L19,L25","3","1uH","","","","Inductor_SMD:L_0806_2016Metric","","WPN201610M1R0MT","WPN201610M1R0MT"
|
||||
"L13","1","18nH","","","","Inductor_SMD:L_0603_1608Metric","","LQW18AN18NJ00D","LQW18AN18NJ00D"
|
||||
"L14","1","120nH","","","","Inductor_SMD:L_0603_1608Metric","","LQW18ANR12J00D","LQW18ANR12J00D"
|
||||
"L15","1","22nH","","","","Inductor_SMD:L_0603_1608Metric","","LQW18AN22NH00D","LQW18AN22NH00D"
|
||||
"R1","1","510","","","","Resistor_SMD:R_0805_2012Metric","","RC0805FR-07510RL","RC0805FR-07510RL"
|
||||
"R2,R30","2","330","","","","Resistor_SMD:R_0805_2012Metric","","RC0805FR-07330RL","RC0805FR-07330RL"
|
||||
"R15,R16","2","100m","","","","Resistor_SMD:R_1206_3216Metric","","WSL1206R1000FEA","WSL1206R1000FEA"
|
||||
"R18,R19","2","1k","","","","Resistor_SMD:R_0805_2012Metric","","RC0805FR-071KL","RC0805FR-071KL"
|
||||
"R34","1","12.1K","","","","Resistor_SMD:R_0805_2012Metric","","FRC0805F1212TS","FRC0805F1212TS"
|
||||
"R35,R36,R44,R45,R52","5","4.7K","","","","Resistor_SMD:R_0805_2012Metric","","RC0805FR-074K7L","RC0805FR-074K7L"
|
||||
"R41,R42","2","5.1K","","","","Resistor_SMD:R_0805_2012Metric","","RC0805FR-075K1L","RC0805FR-075K1L"
|
||||
"R43,R47,R48,R58,R59,R72,R75,R76","8","10K","","","","Resistor_SMD:R_0805_2012Metric","","RC0805FR-0710KL","RC0805FR-0710KL"
|
||||
"R46","1","4.3K","","","","Resistor_SMD:R_0805_2012Metric","","0805W8F4301T5E","0805W8F4301T5E"
|
||||
"R51","1","100","","","","Resistor_SMD:R_0603_1608Metric","","RC0603FR-07100RL","RC0603FR-07100RL"
|
||||
"R73,R74","2","2.2K","","","","Resistor_SMD:R_0805_2012Metric","","0805W8F2201T5E","0805W8F2201T5E"
|
||||
"R95,R96,R97,R98,R100","5","33","","","","Resistor_SMD:R_0805_2012Metric","","RC0805FR-0733RL — 33 Ω — 1% — 0805","RC0805FR-0733RL — 33 Ω — 1% — 0805"
|
||||
"RN10,RN14,RN15,RN16,RN18","5","4x10k","","","","Resistor_SMD:R_Array_Convex_4x0402","","YC124-JR-0710KL",""
|
||||
"RN11","1","49.9","","","","Resistor_SMD:R_Array_Convex_4x1206","","CAT16-49R9F4LF",""
|
||||
"RN12","1","33","","","","Resistor_SMD:R_Array_Convex_2x0402","","2D02WGJ0330TCE",""
|
||||
"RN13","1","10K","","","","Resistor_SMD:R_Array_Convex_4x0402","","YC124-FR-0710KL",""
|
||||
"U1","1","TPS22965DSGR","","","","MIKILAB_TPS22965DSGR:SON50P200X200X80-9N","","TPS22965DSGR","TPS22965DSGR"
|
||||
"U2,U3","2","PCA9534ARGTR","","","","Package_DFN_QFN:VQFN-16-1EP_3x3mm_P0.5mm_EP1.68x1.68mm","http://www.ti.com/lit/ds/symlink/tca9534.pdf","8-bit I/O Expander, I2C / SMBus interface, interrupt output, fixed address bits 0b0100xxx, TSSOP-16 / SOIC-16",""
|
||||
"U4,U8","2","INA228","","","","Package_SO:TSSOP-10_3x3mm_P0.5mm","https://www.ti.com/lit/ds/symlink/ina228.pdf","INA228AQDGSRQ1","INA228AQDGSRQ1"
|
||||
"U5","1","XTAC5212IRGER","","","","MIKILAB_XTAC5212IRGER:VQFN24_RGE_CORNERPADS_TEX","https://www.ti.com/lit/gpn/tac5212","XTAC5212IRGER","XTAC5212IRGER"
|
||||
"U9,U14,U26","3","TPD2E007DCKR","","","","MIKILAB_TPD2E007DCKR:SOT65P210X110-3N","","TPD2E007DCKR","TPD2E007DCKR"
|
||||
"U10","1","W25Q128JVS","","","","Package_SO:SOIC-8_5.3x5.3mm_P1.27mm","http://www.winbond.com/resource-files/w25q128jv_dtr%20revc%2003272018%20plus.pdf","W25Q128JVS","W25Q128JVS"
|
||||
"U11","1","ESP32-S31-WROOM-3","","","","MIKILAB_ESP32_S31_WROOM_3:ESP32-S31-WROOM-3","https://documentation.espressif.com/esp32-s31-wroom-3_datasheet_en.html#pin_layout","ESP32-S31-WROOM-3","ESP32-S31-WROOM-3"
|
||||
"U12","1","Si4684-A10-GM","","","","Package_DFN_QFN:QFN-48-1EP_7x7mm_P0.5mm_EP5.3x5.3mm_ThermalVias","https://www.skyworksinc.com/-/media/SkyWorks/SL/documents/public/product-summaries/Si4684-A10_PS.pdf","Si4684-A10-GM","Si4684-A10-GM"
|
||||
"U16","1","AXP2101","","","","Package_DFN_QFN:QFN-40-1EP_5x5mm_P0.4mm_EP3.6x3.6mm_ThermalVias","https://www.lcsc.com/datasheet/C3036461.pdf","AXP2101","AXP2101"
|
||||
"U17","1","24AA025E64","","","","Package_SO:SOIC-8_3.9x4.9mm_P1.27mm","https://ww1.microchip.com/downloads/aemDocuments/documents/MPD/ProductDocuments/DataSheets/2-Kbit-I2C-Serial-EEPROMs-+EUI-48-or-EUI-64-Node-20002124.pdf","24AA025E64","24AA025E64"
|
||||
"U18","1","ADAU1467WBCPZ300R","","","","MIKILAB_ADAU1467WBCPZ300RL:ADAU1467WBCPZ300","https://www.analog.com/media/en/technical-documentation/data-sheets/adau1463-1467.pdf","ADAU1467WBCPZ300R","ADAU1467WBCPZ300R"
|
||||
"U19","1","LAN8720A","","","","Package_DFN_QFN:VQFN-24-1EP_4x4mm_P0.5mm_EP2.5x2.5mm_ThermalVias","http://ww1.microchip.com/downloads/en/DeviceDoc/8720a.pdf","LAN8720A","LAN8720A"
|
||||
"U24","1","25AA1024-I_SM","","","","Package_SO:SOIC-8_5.3x5.3mm_P1.27mm","","25AA1024-I_SM","25AA1024-I_SM"
|
||||
"U27","1","W25Q16JW","","","","Package_SO:SOIC-8_5.3x5.3mm_P1.27mm","https://www.winbond.com/hq/support/documentation/levelOne.jsp?__locale=en&DocNo=DA00-W25Q16JV.1","W25Q16JW","W25Q16JW"
|
||||
"U31,U32,U33,U37","4","TMP117NAIDRVR","","","","Package_SON:WSON-6-1EP_2x2mm_P0.65mm_EP1x1.6mm","https://www.ti.com/lit/ds/symlink/tmp117.pdf","TMP117NAIDRVR","TMP117NAIDRVR"
|
||||
"U38","1","FSC-BT1058","","","","MIKILAB_FSC_BT1058:FSC-BT1058","https://document.feasycom.com/docs/datasheet/BT1058/FSC-BT1058_Datasheet_EN.pdf","FSC-BT1058","FSC-BT1058"
|
||||
"U40","1","SC16IS740IPW,128","","","","Package_SO:TSSOP-16_4.4x5mm_P0.65mm","https://www.nxp.com/docs/en/data-sheet/SC16IS740_750_760.pdf","SC16IS740IPW,128","SC16IS740IPW,128"
|
||||
"X1","1","ASE-50.000MHZ-LC-T","","","","Oscillator:Oscillator_SMD_Abracon_ASE-4Pin_3.2x2.5mm","http://www.abracon.com/Oscillators/ASV.pdf","3.3V CMOS SMD Crystal Clock Oscillator, Abracon","ASE-50.000MHZ-LC-T"
|
||||
"Y1","1","ABM3B-12.288MHZ-10-1-U-T","","","","Crystal:Crystal_SMD_5032-4Pin_5.0x3.2mm","","Four pin crystal, GND on pins 2 and 4","ABM3B-12.288MHZ-10-1-U-T"
|
||||
"Y2","1","ABM8-19.200MHZ-10-1-U-T","","","","Crystal:Crystal_SMD_Abracon_ABM8AIG-4Pin_3.2x2.5mm","","Four pin crystal, GND on pins 2 and 4","ABM8-19.200MHZ-10-1-U-T"
|
||||
"Y4","1","12,288 MHz","","","","Crystal:Crystal_SMD_2520-4Pin_2.5x2.0mm","","FH1220004","FH1220004"
|
||||
|
@@ -0,0 +1,47 @@
|
||||
"""HubAudio KiCad BOM: PNM column, grouped refs, no invented U13…U39 MPNs."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from backend.periscopex.parsers import ic_mpn_skip_reason, parse_bom
|
||||
|
||||
FIXTURE = Path(__file__).parent / "fixtures" / "hubaudio_kicad_bom.csv"
|
||||
SKIPPED = ("U13", "U15", "U25", "U28", "U34", "U35", "U36", "U39")
|
||||
|
||||
|
||||
def test_hubaudio_header_is_pnm_not_mpn():
|
||||
header = FIXTURE.read_text(encoding="utf-8-sig").splitlines()[0]
|
||||
assert "PNM" in header
|
||||
assert "Manufacturer Part Number" not in header
|
||||
|
||||
|
||||
def test_hubaudio_pnm_and_value_fallback():
|
||||
bom = parse_bom(FIXTURE, mpn_col="Manufacturer Part Number")
|
||||
assert bom["U1"]["mpn"] == "TPS22965DSGR"
|
||||
assert bom["U10"]["mpn"] == "W25Q128JVS"
|
||||
assert bom["U11"]["mpn"] == "ESP32-S31-WROOM-3"
|
||||
# PNM blank, Value is the orderable MPN; grouped Reference list.
|
||||
assert bom["U2"]["mpn"] == "PCA9534ARGTR"
|
||||
assert bom["U3"]["mpn"] == "PCA9534ARGTR"
|
||||
assert bom["U2"]["value"] == "PCA9534ARGTR"
|
||||
assert (bom["U4"]["mpn"] == "INA228AQDGSRQ1") and bom["U8"]["mpn"] == "INA228AQDGSRQ1"
|
||||
assert bom["L4"]["mpn"] == "BLM21PG121SN1D"
|
||||
assert bom["L4"]["value"] == "120 ohm"
|
||||
|
||||
|
||||
def test_hubaudio_skipped_refs_absent_no_invented_mpn():
|
||||
bom = parse_bom(FIXTURE, mpn_col="Manufacturer Part Number")
|
||||
for ref in SKIPPED:
|
||||
assert ref not in bom
|
||||
reason = ic_mpn_skip_reason(
|
||||
ref, mpn=None, value="", footprint="MIKILAB_TPD2E007DCKR:SOT65P210X110-3N",
|
||||
in_bom=False,
|
||||
)
|
||||
assert reason and reason.startswith("INSUFFICIENT_EVIDENCE")
|
||||
assert "not in BOM" in reason
|
||||
|
||||
|
||||
def test_ic_skip_empty_pnm_quotes_value():
|
||||
reason = ic_mpn_skip_reason(
|
||||
"U99", mpn=None, value="", footprint="QFN", in_bom=True, bom_value="",
|
||||
)
|
||||
assert reason and "PNM/MPN empty" in reason
|
||||
@@ -77,3 +77,7 @@ async def test_deterministic_findings_seeded_and_not_reviewed(tmp_path):
|
||||
assert data["summary"]["ERROR"] >= 1
|
||||
|
||||
assert {x["designator"] for x in data["not_reviewed"]} == {"U3", "U6"}
|
||||
by_ref = {x["designator"]: x["reason"] for x in data["not_reviewed"]}
|
||||
assert by_ref["U3"] == "no datasheet PDF"
|
||||
assert by_ref["U6"].startswith("INSUFFICIENT_EVIDENCE")
|
||||
assert "not in BOM" in by_ref["U6"]
|
||||
|
||||
Reference in New Issue
Block a user