Rename the core package to periscopex, update UI/docs/Docker/deploy defaults to periscope.michelebigi.it, and keep legacy version/storage key aliases so existing projects keep working. Co-authored-by: Cursor <cursoragent@cursor.com>
131 lines
3.8 KiB
Python
131 lines
3.8 KiB
Python
"""DNP variant: fitted enable without pull/driver is ERROR."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from backend.periscopex.dnp_check import check_dnp_enables
|
|
from backend.periscopex.models import (
|
|
Component,
|
|
ComponentConstraints,
|
|
ComponentType,
|
|
DesignGraph,
|
|
Net,
|
|
NetType,
|
|
Pin,
|
|
PinConnection,
|
|
ResistorSpecs,
|
|
)
|
|
from backend.periscopex.parsers import parse_bom
|
|
|
|
|
|
def _graph(components, nets, bom_fields=None):
|
|
net_objs = {
|
|
name: Net(
|
|
name=name, net_type=ntype,
|
|
pins=[PinConnection(component_ref=r, pin_number=str(p)) for r, p in conns],
|
|
)
|
|
for name, (ntype, conns) in nets.items()
|
|
}
|
|
return DesignGraph(components=components, nets=net_objs, bom_fields=bom_fields or {})
|
|
|
|
|
|
def _cons():
|
|
return {
|
|
"LDOX": ComponentConstraints(
|
|
mpn="LDOX",
|
|
pintable=[
|
|
Pin(number=1, name="VIN"),
|
|
Pin(number=2, name="VOUT"),
|
|
Pin(number=3, name="EN"),
|
|
Pin(number=4, name="GND"),
|
|
],
|
|
absolute_maximum_ratings=[], rules=[],
|
|
)
|
|
}
|
|
|
|
|
|
def _ldo():
|
|
return Component(
|
|
reference="U1", value="", footprint="",
|
|
component_type=ComponentType.IC, mpn="LDOX",
|
|
pins={"1": "VIN", "2": "VOUT", "3": "EN_NET", "4": "GND"},
|
|
)
|
|
|
|
|
|
def _r(dnp_net="EN_NET"):
|
|
return Component(
|
|
reference="R1", value="10k", footprint="",
|
|
component_type=ComponentType.RESISTOR, mpn="R1",
|
|
pins={"1": dnp_net, "2": "VIN"},
|
|
specs=ResistorSpecs(value_ohms=10000, value_formatted="10k"),
|
|
)
|
|
|
|
|
|
def test_dnp_pull_leaves_enable_floating():
|
|
g = _graph(
|
|
{"U1": _ldo(), "R1": _r()},
|
|
{
|
|
"VIN": (NetType.POWER, [("U1", "1"), ("R1", "2")]),
|
|
"VOUT": (NetType.POWER, [("U1", "2")]),
|
|
"EN_NET": (NetType.SIGNAL, [("U1", "3"), ("R1", "1")]),
|
|
"GND": (NetType.GROUND, [("U1", "4")]),
|
|
},
|
|
bom_fields={
|
|
"U1": {"mpn": "LDOX", "value": "", "dnp": False},
|
|
"R1": {"mpn": "R1", "value": "10k", "dnp": True},
|
|
},
|
|
)
|
|
findings = check_dnp_enables(g, _cons())
|
|
assert len(findings) == 1
|
|
assert findings[0].rule_id == "PE-DNP-001"
|
|
assert findings[0].status == "ERROR"
|
|
|
|
|
|
def test_fitted_pull_is_silent():
|
|
g = _graph(
|
|
{"U1": _ldo(), "R1": _r()},
|
|
{
|
|
"VIN": (NetType.POWER, [("U1", "1"), ("R1", "2")]),
|
|
"VOUT": (NetType.POWER, [("U1", "2")]),
|
|
"EN_NET": (NetType.SIGNAL, [("U1", "3"), ("R1", "1")]),
|
|
"GND": (NetType.GROUND, [("U1", "4")]),
|
|
},
|
|
bom_fields={
|
|
"U1": {"mpn": "LDOX", "value": "", "dnp": False},
|
|
"R1": {"mpn": "R1", "value": "10k", "dnp": False},
|
|
},
|
|
)
|
|
assert check_dnp_enables(g, _cons()) == []
|
|
|
|
|
|
def test_no_dnp_column_skips_floating_enable():
|
|
g = _graph(
|
|
{"U1": _ldo()},
|
|
{
|
|
"VIN": (NetType.POWER, [("U1", "1")]),
|
|
"VOUT": (NetType.POWER, [("U1", "2")]),
|
|
"EN_NET": (NetType.SIGNAL, [("U1", "3")]),
|
|
"GND": (NetType.GROUND, [("U1", "4")]),
|
|
},
|
|
bom_fields={"U1": {"mpn": "LDOX", "value": ""}},
|
|
)
|
|
assert check_dnp_enables(g, _cons()) == []
|
|
|
|
|
|
def test_parse_bom_reads_dnp_and_skips_when_column_absent(tmp_path: Path):
|
|
with_dnp = tmp_path / "dnp.csv"
|
|
with_dnp.write_text(
|
|
"Reference,Value,DNP,Manufacturer Part Number\n"
|
|
"U1,LDO,,LDOX\n"
|
|
"R1,10k,1,Rpull\n"
|
|
)
|
|
bom = parse_bom(with_dnp)
|
|
assert bom["U1"]["dnp"] is False
|
|
assert bom["R1"]["dnp"] is True
|
|
|
|
no_col = tmp_path / "plain.csv"
|
|
no_col.write_text("Reference,Value,Manufacturer Part Number\nU1,LDO,LDOX\n")
|
|
bom2 = parse_bom(no_col)
|
|
assert "dnp" not in bom2["U1"]
|