Add protocol-certification M6/M7 USB-C Ethernet HDMI packs (2.73.0).

USB 2.0 is protocol, Type-C is connector, USB 2.0 over Type-C is the
implementation. Ethernet/HDMI skeletons stay UNKNOWN with needed_document.
No invented ohms; Mini HDMI is connector-only; RJ45 is not 1000BASE-T.
This commit is contained in:
2026-09-22 13:40:37 +02:00
parent 7baffadd6b
commit 949f5f1541
9 changed files with 8262 additions and 374 deletions
@@ -123,6 +123,7 @@ class ProtocolConstraint(BaseModel):
limit_kind: LimitKind | None = None
value_origin: ValueOrigin = "SPEC"
typical: bool = False
needed_document: str | None = None
@field_validator("source_type")
@classmethod
@@ -155,6 +156,11 @@ class ProtocolConstraint(BaseModel):
raise ProtocolCatalogError(
f"{self.id}: non-NUMERIC value_kind cannot carry a number"
)
if self.value_kind in {"UNKNOWN", "MISSING_SOURCE"}:
if not (self.needed_document or "").strip():
raise ProtocolCatalogError(
f"{self.id}: UNKNOWN/MISSING_SOURCE needs needed_document"
)
return self
def is_fail_mandatory(self) -> bool:
@@ -185,6 +191,7 @@ class PhysicalInterface(BaseModel):
required_checks: list[str] = Field(default_factory=list)
constraints: list[ProtocolConstraint] = Field(default_factory=list)
notes: str = ""
incomplete: bool = False
@model_validator(mode="after")
def _logical_differs_from_physical(self) -> PhysicalInterface:
@@ -298,8 +305,11 @@ def _c(
mandatory: MandatoryClass = "MANDATORY",
source_type: SourceType = "STANDARD",
source_class: SourceClass = "NORMATIVE",
value_origin: ValueOrigin = "SPEC",
typical: bool = False,
conditions: list[str] | None = None,
measurement_method: MeasurementMethod | None = None,
needed_document: str | None = None,
) -> dict[str, Any]:
row: dict[str, Any] = {
"id": f"{iface_id}-{parameter}",
@@ -317,6 +327,13 @@ def _c(
}
if measurement_method:
row["measurement_method"] = measurement_method
if value_kind in {"UNKNOWN", "MISSING_SOURCE"}:
row["needed_document"] = (
needed_document
or "Official specification not on file (cite before NUMERIC)"
)
elif needed_document:
row["needed_document"] = needed_document
return row
@@ -328,6 +345,7 @@ def _skel_constraints(
mandatory: MandatoryClass = "MANDATORY",
source_type: SourceType = "STANDARD",
source_class: SourceClass = "NORMATIVE",
needed_document: str | None = None,
) -> list[dict[str, Any]]:
"""Named checks with no numbers. Cite later; never invent ohm/dB."""
return [
@@ -336,6 +354,7 @@ def _skel_constraints(
mandatory=mandatory,
source_type=source_type,
source_class=source_class,
needed_document=needed_document,
)
for p in parameters
]
@@ -353,6 +372,7 @@ def _iface(
connector: str = "none",
hints: list[str] | None = None,
notes: str = "",
incomplete: bool = False,
) -> dict[str, Any]:
return {
"id": pid,
@@ -365,34 +385,110 @@ def _iface(
"required_checks": checks,
"constraints": constraints,
"notes": notes,
"incomplete": incomplete,
}
USB2_CHECKS = [
"signaling",
"data_rate",
"electrical_levels",
"differential_impedance",
"impedance_tolerance",
"rise_time",
"fall_time",
"intra_pair_skew",
"max_length",
"min_length",
"connector_requirements",
"termination",
"pull_up",
"pull_down",
"vbus_requirements",
"dc_resistance",
"topology",
"length",
"stubs",
"vias",
"return_path",
"test_method",
"compliance_test",
"length",
]
USB_C_CHECKS = [
"rp",
"rd",
"ra",
"cc",
"vbus_requirements",
"current_advertisement",
"voltage",
"timing",
"electrical_thresholds",
"termination",
"connector_requirements",
"usb2_channel_requirements",
]
ETH_CHECKS = [
"data_rate",
"signaling",
"differential_impedance",
"pair_skew",
"length",
"magnetics",
"return_path",
"impedance_tolerance",
"intra_pair_skew",
"pair_to_pair_skew",
"maximum_channel_length",
"insertion_loss",
"return_loss",
"crosstalk",
"magnetics_requirements",
"termination",
"common_mode",
"connector_requirements",
"phy_requirements",
"return_path",
"vias",
"topology",
"cable_channel",
]
HDMI_CHECKS = [
"differential_impedance",
"pair_skew",
"lane_relationships",
"impedance_tolerance",
"intra_pair_skew",
"inter_lane_skew",
"clock_data_relationship",
"data_rate",
"transition_rate",
"channel_loss",
"insertion_loss",
"return_loss",
"crosstalk",
"vias",
"connector",
"stubs",
"connector_contribution",
"topology",
"return_path",
"ac_coupling",
"esd",
"ddc",
"cec",
"hpd",
]
DOC_USB_BASE = (
"USB-IF Base Specification (revision to fix when PDF is provided)"
)
DOC_USB_ELEC = (
"USB-IF Electrical Compliance Specification (revision to fix when PDF is provided)"
)
DOC_USB_CABLE = (
"USB-IF Cable and Connector Specification (revision to fix when PDF is provided)"
)
DOC_USB_CTS = (
"USB-IF Compliance Test Specification (revision to fix when PDF is provided)"
)
DOC_USB_ECN = "USB-IF ECN applicable to the revision used"
DOC_IEEE_8023 = "IEEE 802.3 (clause for this variant; not on file)"
DOC_HDMI_ADOPTER = (
"HDMI Adopter specification (not on file; 1.4 vs 2.x vs FRL must not be mixed)"
)
DDR_CHECKS = [
"byte_lane_mapping",
"dq_to_dqs_skew",
@@ -486,13 +582,36 @@ def build_m0_catalog() -> dict[str, Any]:
"id": pid, "name": name, "bus_type": bus, "family": family, "notes": notes,
})
L("usb2-ls-fs", "USB 2.0 Low/Full Speed", "SERIAL", "USB")
L("usb2-ls-fs", "USB 2.0 Low/Full Speed (umbrella)", "SERIAL", "USB",
"Umbrella. Prefer usb2-ls / usb2-fs when speed is known.")
L("usb2-ls", "USB 2.0 Low Speed", "SERIAL", "USB")
L("usb2-fs", "USB 2.0 Full Speed", "SERIAL", "USB")
L("usb2-hs", "USB 2.0 High Speed", "SERIAL", "USB")
L("usb-c-usb2", "USB Type-C carrying USB 2.x only", "SERIAL", "USB")
L("usb-c", "USB Type-C connector/interface system", "SERIAL", "USB",
"Connector/interface system, not a data protocol. Not USB3. Not USB4.")
L("usb-c-usb2", "USB 2.0 over USB Type-C (physical implementation name)", "SERIAL", "USB",
"Physical implementation USB 2.0 over Type-C. Protocol is USB 2.0; connector is Type-C.")
L("usb-c-pd", "USB Power Delivery on CC", "SERIAL", "USB")
L("usb3-x", "USB 3.x (logical)", "SERIAL", "USB",
"Distinct from USB 2.0 and from Type-C connector. Numbers UNKNOWN until USB-IF USB3 spec.")
L("usb4", "USB4 (logical)", "SERIAL", "USB",
"Distinct from USB 3.x and Type-C. Numbers UNKNOWN until USB-IF USB4 spec.")
L("100base-tx", "100BASE-TX Fast Ethernet", "SERIAL", "ETHERNET")
L("10base-t", "10BASE-T", "SERIAL", "ETHERNET")
L("1000base-t", "1000BASE-T", "SERIAL", "ETHERNET")
L("2.5gbase-t", "2.5GBASE-T", "SERIAL", "ETHERNET")
L("5gbase-t", "5GBASE-T", "SERIAL", "ETHERNET")
L("10gbase-t", "10GBASE-T", "SERIAL", "ETHERNET")
L("sgmii", "SGMII", "SERIAL", "ETHERNET")
L("1000base-x", "1000BASE-X", "SERIAL", "ETHERNET")
L("100base-fx", "100BASE-FX", "SERIAL", "ETHERNET")
L("10gbase-r", "10GBASE-R", "SERIAL", "ETHERNET")
L("ethernet-unspecified", "Ethernet unspecified (RJ45 alone)", "SERIAL", "ETHERNET",
"RJ45 is a connector, not 100BASE-TX or 1000BASE-T.")
L("hdmi-1.4", "HDMI 1.4 TMDS", "SERIAL", "HDMI")
L("hdmi-2.0", "HDMI 2.0 TMDS", "SERIAL", "HDMI")
L("hdmi-frl", "HDMI FRL mode", "SERIAL", "HDMI",
"FRL is distinct from TMDS. Do not copy TMDS numbers onto FRL.")
L("ddr-family", "DDR family (abstract)", "MEMORY", "DDR")
for gen, title in (
("ddr1", "DDR1"), ("ddr2", "DDR2"), ("ddr3", "DDR3"),
@@ -548,92 +667,214 @@ def build_m0_catalog() -> dict[str, Any]:
L("can", "CAN / CAN FD", "PERIPHERAL", "CAN")
L("rgmii", "RGMII MAC-PHY", "PERIPHERAL", "ETHERNET")
missing_usb = [
_c("usb2-hs-dpair", "differential_impedance", "MISSING_SOURCE",
measurement_method="PCB_STACKUP"),
_c("usb2-hs-dpair", "intra_pair_skew", "MISSING_SOURCE",
measurement_method="CALCULATION"),
_c("usb2-hs-dpair", "topology", "UNKNOWN"),
_c("usb2-hs-dpair", "length", "UNKNOWN"),
_c("usb2-hs-dpair", "vias", "UNKNOWN"),
_c("usb2-hs-dpair", "return_path", "UNKNOWN", mandatory="RECOMMENDED",
source_class="VENDOR"),
]
def _usb2_cons(pid: str) -> list[dict[str, Any]]:
docs = {
"differential_impedance": DOC_USB_ELEC,
"impedance_tolerance": DOC_USB_ELEC,
"electrical_levels": DOC_USB_ELEC,
"rise_time": DOC_USB_ELEC,
"fall_time": DOC_USB_ELEC,
"intra_pair_skew": DOC_USB_ELEC,
"termination": DOC_USB_ELEC,
"dc_resistance": DOC_USB_ELEC,
"connector_requirements": DOC_USB_CABLE,
"vbus_requirements": DOC_USB_BASE,
"pull_up": DOC_USB_BASE,
"pull_down": DOC_USB_BASE,
"signaling": DOC_USB_BASE,
"data_rate": DOC_USB_BASE,
"test_method": DOC_USB_CTS,
"compliance_test": DOC_USB_CTS,
}
rec = {"return_path"}
out = []
for p in USB2_CHECKS:
mand: MandatoryClass = "RECOMMENDED" if p in rec else "MANDATORY"
out.append(_c(
pid, p, "UNKNOWN",
mandatory=mand,
needed_document=docs.get(p, DOC_USB_BASE),
))
return out
def _usbc_cons(pid: str) -> list[dict[str, Any]]:
cable = {"rp", "rd", "ra", "cc", "connector_requirements", "current_advertisement"}
out = []
for p in USB_C_CHECKS:
doc = DOC_USB_CABLE if p in cable else DOC_USB_ELEC
if p == "usb2_channel_requirements":
doc = DOC_USB_ELEC
out.append(_c(pid, p, "UNKNOWN", needed_document=doc))
return out
def _eth_cons(pid: str, *, phy: bool = False) -> list[dict[str, Any]]:
out = []
for p in ETH_CHECKS:
if p in {"phy_requirements", "magnetics_requirements"} or phy:
if p in {"phy_requirements", "magnetics_requirements"}:
out.append(_c(
pid, p, "PHY_DEPENDENT",
source_type="PHY", source_class="VENDOR",
needed_document="PHY vendor datasheet (not IEEE-universal)",
))
continue
out.append(_c(pid, p, "UNKNOWN", needed_document=DOC_IEEE_8023))
return out
def _hdmi_cons(pid: str) -> list[dict[str, Any]]:
return [
_c(pid, p, "UNKNOWN", needed_document=DOC_HDMI_ADOPTER)
for p in HDMI_CHECKS
]
for lid, pid, hints, note in (
("usb2-ls", "usb2-ls-dpair", ["USB_D+", "USB_LS"], "USB 2.0 Low Speed. No invented Z."),
("usb2-fs", "usb2-fs-dpair", ["USB_D+", "USB_FS"], "USB 2.0 Full Speed. No invented Z."),
("usb2-ls-fs", "usb2-ls-fs-dpair", ["USB_D+", "USB_D-", "USB_DP", "USB_DM", "D+", "D-"],
"LS/FS umbrella pair. No invented Z."),
("usb2-hs", "usb2-hs-dpair", ["USB_D+", "USB_D-", "USB_HS"],
"USB 2.0 High Speed. USB-IF cite required before NUMERIC Z."),
):
physical.append(_iface(
pid, lid, "SERIAL", "SERIAL_DIFFERENTIAL", "YES",
USB2_CHECKS, _usb2_cons(pid),
hints=hints, notes=note,
))
physical.append(_iface(
"usb2-ls-fs-dpair", "usb2-ls-fs", "SERIAL", "SERIAL_DIFFERENTIAL", "YES",
USB2_CHECKS,
[
_c("usb2-ls-fs-dpair", "differential_impedance", "MISSING_SOURCE",
measurement_method="PCB_STACKUP"),
_c("usb2-ls-fs-dpair", "intra_pair_skew", "MISSING_SOURCE"),
_c("usb2-ls-fs-dpair", "topology", "UNKNOWN"),
],
hints=["USB_D+", "USB_D-", "USB_DP", "USB_DM", "D+", "D-"],
notes="LS/FS pair. No invented Z.",
))
physical.append(_iface(
"usb2-hs-dpair", "usb2-hs", "SERIAL", "SERIAL_DIFFERENTIAL", "YES",
USB2_CHECKS, missing_usb,
hints=["USB_D+", "USB_D-", "USB_HS"],
notes="HS pair. Official USB-IF cite required before NUMERIC Z.",
))
physical.append(_iface(
"usb-c-usb2-receptacle", "usb-c-usb2", "SERIAL", "MIXED", "YES",
USB2_CHECKS + ["cc_rd_rp", "vbus_gnd", "superspeed_absence"],
[
_c("usb-c-usb2-receptacle", "differential_impedance", "MISSING_SOURCE"),
_c("usb-c-usb2-receptacle", "cc_termination", "MISSING_SOURCE",
source_type="CONNECTOR", source_class="NORMATIVE"),
_c("usb-c-usb2-receptacle", "superspeed_pairs", "NOT_APPLICABLE",
mandatory="INFORMATIONAL",
conditions=["usb2_only"]),
],
"usb-c-receptacle", "usb-c", "SERIAL", "MIXED", "YES",
USB_C_CHECKS, _usbc_cons("usb-c-receptacle"),
connector="Type-C",
hints=["CC1", "CC2", "USB_C", "TYPE_C", "A5", "B5"],
notes="Type-C carrying USB2. Electrical USB2 + CC. No invented Rd ohms.",
hints=["USB_C", "TYPE_C", "CC1", "CC2"],
notes=(
"USB Type-C connector/interface system. Not USB 2.0/3.x/USB4 protocol. "
"No invented Rd/Rp ohms."
),
))
usb2_over_c = _usb2_cons("usb-c-usb2-receptacle")
seen_p = {c["parameter"] for c in usb2_over_c}
for c in _usbc_cons("usb-c-usb2-receptacle"):
if c["parameter"] not in seen_p:
usb2_over_c.append(c)
seen_p.add(c["parameter"])
usb2_over_c.append(_c(
"usb-c-usb2-receptacle", "cc_rd_rp", "UNKNOWN",
source_type="CONNECTOR", needed_document=DOC_USB_CABLE,
))
usb2_over_c.append(_c(
"usb-c-usb2-receptacle", "superspeed_pairs", "NOT_APPLICABLE",
mandatory="INFORMATIONAL", conditions=["usb2_only"],
))
physical.append(_iface(
"usb-c-usb2-receptacle", "usb2-hs", "SERIAL", "MIXED", "YES",
USB2_CHECKS + [p for p in USB_C_CHECKS if p not in USB2_CHECKS] + [
"cc_rd_rp", "vbus_gnd", "superspeed_absence",
],
usb2_over_c,
connector="Type-C",
hints=["CC1", "CC2", "USB_C", "TYPE_C", "A5", "B5", "USB_D+", "USB_D-"],
notes=(
"Physical implementation: USB 2.0 over USB Type-C. "
"Protocol=USB 2.0, connector=Type-C. Not USB3. Not USB4. No invented Rd."
),
))
physical.append(_iface(
"usb3-over-usb-c", "usb3-x", "SERIAL", "SERIAL_DIFFERENTIAL", "YES",
["superspeed_pairs", "differential_impedance", "intra_pair_skew"],
_skel_constraints(
"usb3-over-usb-c",
["superspeed_pairs", "differential_impedance", "intra_pair_skew"],
"UNKNOWN",
needed_document="USB-IF USB 3.x specification (not on file)",
),
connector="Type-C",
hints=["SSTX", "SSRX", "USB3"],
notes="USB 3.x over Type-C. Distinct from USB 2.0 and USB4. Numbers UNKNOWN.",
incomplete=True,
))
physical.append(_iface(
"usb4-over-usb-c", "usb4", "SERIAL", "SERIAL_DIFFERENTIAL", "YES",
["superspeed_pairs", "differential_impedance"],
_skel_constraints(
"usb4-over-usb-c",
["superspeed_pairs", "differential_impedance"],
"UNKNOWN",
needed_document="USB-IF USB4 specification (not on file)",
),
connector="Type-C",
hints=["USB4", "TBT4"],
notes="USB4 over Type-C. Not implied by Type-C connector. Numbers UNKNOWN.",
incomplete=True,
))
physical.append(_iface(
"usb-c-pd-cc", "usb-c-pd", "SERIAL", "SERIAL_SINGLE_ENDED", "CONDITIONAL",
["cc_pd_contract"],
[_c("usb-c-pd-cc", "pd_contract", "MISSING_SOURCE",
source_type="STANDARD", conditions=["pd_present"])],
[_c("usb-c-pd-cc", "pd_contract", "UNKNOWN",
source_type="STANDARD", conditions=["pd_present"],
needed_document="USB-IF USB Power Delivery specification (not on file)")],
connector="Type-C",
hints=["CC1", "CC2", "USB_PD"],
notes="PD pack is separate; Type-C connector does not imply PD numbers.",
incomplete=True,
))
physical.append(_iface(
"100base-tx-mdi", "100base-tx", "SERIAL", "SERIAL_DIFFERENTIAL", "YES",
ETH_CHECKS,
[
_c("100base-tx-mdi", "differential_impedance", "MISSING_SOURCE",
source_type="STANDARD"),
_c("100base-tx-mdi", "pair_skew", "MISSING_SOURCE"),
_c("100base-tx-mdi", "magnetics", "PHY_DEPENDENT",
source_type="PHY", source_class="VENDOR"),
_c("100base-tx-mdi", "bob_smith", "UNKNOWN", mandatory="RECOMMENDED",
source_type="PHY", source_class="VENDOR"),
],
"ethernet-rj45-unspecified", "ethernet-unspecified", "SERIAL", "MIXED", "YES",
["connector_requirements"],
[_c("ethernet-rj45-unspecified", "connector_requirements", "UNKNOWN",
source_type="CONNECTOR", needed_document=DOC_IEEE_8023)],
connector="RJ45",
hints=["TD+", "TD-", "RD+", "RD-", "MDI", "RJ45", "100BASE"],
notes="IEEE numbers stay MISSING_SOURCE until the spec is on file.",
hints=["RJ45", "8P8C"],
notes="RJ45 connector alone is not 100BASE-TX or 1000BASE-T.",
incomplete=True,
))
for lid, pid, hints in (
("10base-t", "10base-t-mdi", ["10BASE", "TD+", "RD+"]),
("100base-tx", "100base-tx-mdi", ["TD+", "TD-", "RD+", "RD-", "MDI", "100BASE"]),
("1000base-t", "1000base-t-mdi", ["1000BASE", "GBE", "MDI"]),
("2.5gbase-t", "2.5gbase-t-mdi", ["2.5GBASE", "NBASE"]),
("5gbase-t", "5gbase-t-mdi", ["5GBASE"]),
("10gbase-t", "10gbase-t-mdi", ["10GBASE-T"]),
("sgmii", "sgmii-pcb", ["SGMII"]),
("1000base-x", "1000base-x-pcb", ["1000BASE-X", "SFP"]),
("100base-fx", "100base-fx-pcb", ["100BASE-FX", "FX"]),
("10gbase-r", "10gbase-r-pcb", ["10GBASE-R", "SFI"]),
):
conn = "none" if lid in {"sgmii", "1000base-x", "100base-fx", "10gbase-r"} else "RJ45"
physical.append(_iface(
pid, lid, "SERIAL", "SERIAL_DIFFERENTIAL", "YES",
ETH_CHECKS, _eth_cons(pid),
connector=conn, hints=hints,
notes=(
f"Incomplete skeleton: IEEE 802.3 not on file. "
f"IEEE vs PHY vs PCB vs magnetics are distinct. Not auto from RJ45."
),
incomplete=True,
))
for hid, lid, conn, note in (
("hdmi-1.4-tmds-type-a", "hdmi-1.4", "HDMI-A", "Type A land"),
("hdmi-1.4-tmds-type-a", "hdmi-1.4", "HDMI-A",
"HDMI 1.4 TMDS on Type A. Mini is a connector, not a protocol. UNKNOWN until Adopter spec."),
("hdmi-1.4-tmds-type-c-mini", "hdmi-1.4", "HDMI-C-mini",
"Mini Type C land; electrical HDMI 1.4, not a second standard"),
("hdmi-2.0-tmds-type-a", "hdmi-2.0", "HDMI-A", "Type A land"),
"Mini HDMI is CONNECTOR only. Electrical protocol remains HDMI 1.4 TMDS, not MINI-HDMI."),
("hdmi-2.0-tmds-type-a", "hdmi-2.0", "HDMI-A",
"HDMI 2.0 TMDS on Type A. Do not copy 1.4 numbers. UNKNOWN until Adopter spec."),
("hdmi-2.0-tmds-type-c-mini", "hdmi-2.0", "HDMI-C-mini",
"Mini Type C land; electrical HDMI 2.0"),
"Mini HDMI connector + HDMI 2.0 TMDS electrical. Not a separate protocol id."),
):
physical.append(_iface(
hid, lid, "SERIAL", "SERIAL_DIFFERENTIAL", "YES", HDMI_CHECKS,
[
_c(hid, "differential_impedance", "MISSING_SOURCE"),
_c(hid, "pair_skew", "MISSING_SOURCE"),
_c(hid, "insertion_loss", "MISSING_SOURCE", mandatory="RECOMMENDED"),
],
connector=conn, hints=["HDMI", "TMDS", "TX0", "TX1", "TX2", "CLK"],
_hdmi_cons(hid),
connector=conn,
hints=["HDMI", "TMDS", "TX0", "TX1", "TX2", "CLK"],
notes=note,
incomplete=True,
))
physical.append(_iface(
"hdmi-frl-pcb", "hdmi-frl", "SERIAL", "SERIAL_DIFFERENTIAL", "YES",
HDMI_CHECKS, _hdmi_cons("hdmi-frl-pcb"),
connector="HDMI-A",
hints=["FRL", "HDMI2.1", "HDMI_FRL"],
notes="HDMI FRL is distinct from TMDS. Do not apply TMDS rules. UNKNOWN until Adopter spec.",
incomplete=True,
))
def _mem_constraints(pid: str, kind: ValueKind, source_type: SourceType) -> list[dict[str, Any]]:
cls: SourceClass = "VENDOR" if source_type != "STANDARD" else "NORMATIVE"
File diff suppressed because it is too large Load Diff
@@ -210,6 +210,11 @@
"default": "",
"title": "Notes",
"type": "string"
},
"incomplete": {
"default": false,
"title": "Incomplete",
"type": "boolean"
}
},
"required": [
@@ -377,6 +382,18 @@
"default": false,
"title": "Typical",
"type": "boolean"
},
"needed_document": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Needed Document"
}
},
"required": [
@@ -11,7 +11,7 @@ from typing import Any, Literal
from pydantic import BaseModel, Field
from backend.periscopex.models import Component, DesignGraph
from backend.periscopex.models import Component, ComponentType, DesignGraph
from backend.periscopex.pcb_net_match import normalize_kicad_hierarchy_net
from backend.periscopex.protocol_catalog import (
ProtocolCatalog,
@@ -52,11 +52,24 @@ _EVIDENCE_SCORE: dict[str, float] = {
"pair_geometry": 0.4,
}
_USB_C_RE = re.compile(r"USB[\s_\-]*C\b|TYPE[\s_\-]*C|USB4085", re.I)
_USB_C_RE = re.compile(r"USB[\s_\-]*C(?:[\s_\-]|\b|$)|TYPE[\s_\-]*C|USB4085", re.I)
_USB2_RE = re.compile(r"USB[\s_\-]*2|16P", re.I)
_USB3_RE = re.compile(r"USB[\s_\-]*3|SUPER\s*SPEED|\bSS\b", re.I)
_USB_HS_RE = re.compile(r"HIGH\s*SPEED|\b480\b|\bHS\b", re.I)
_USB3_RE = re.compile(r"USB[\s_\-]*3|SUPER\s*SPEED|SSTX|SSRX|SS_T[XR]", re.I)
_USB4_RE = re.compile(r"USB[\s_\-]*4|\bUSB4\b|TBT4|THUNDERBOLT\s*4", re.I)
_USB_HS_RE = re.compile(r"HIGH\s*SPEED|\b480\b", re.I)
_USB_LSFS_RE = re.compile(r"LOW\s*SPEED|FULL\s*SPEED|\b12\s*MB|\b1\.5\s*MB", re.I)
_USB_SILICON_RE = re.compile(
r"CH340|CP210|FT232|USB\s*PHY|USB\s*CTRL|USB3300|TUSB|ISP150|MAX3421", re.I,
)
_RJ45_RE = re.compile(r"RJ45|8P8C|MAGJACK", re.I)
_ETH_PHY_RE = re.compile(
r"88E\d|LAN87|DP838|RTL821|KSZ90|BCM54|ETHERNET\s*PHY|\bPHY\b.*ETH|ETH.*PHY", re.I,
)
_ETH_MAG_RE = re.compile(r"MAGNETIC|PULSE\s*JACK|ETHERNET\s*XFR|LAN_XFR", re.I)
_HDMI_MINI_RE = re.compile(r"MINI\s*HDMI|HDMI[\s_\-]*C\b|HDMI[\s_\-]*MINI", re.I)
_HDMI_RE = re.compile(r"\bHDMI\b|TMDS", re.I)
_HDMI_FRL_RE = re.compile(r"\bFRL\b|HDMI\s*2\.1", re.I)
_HDMI_TMDS_NET = re.compile(r"TMDS|HDMI[_]?TX|HDMI[_]?CLK|HDMI[_]?D\d", re.I)
_DDR_GEN = [
("ddr5", re.compile(r"\bDDR5\b", re.I)),
("ddr4", re.compile(r"\bDDR4\b|MT41|K4A|IS43TR", re.I)),
@@ -109,6 +122,8 @@ class PhysicalBusInstance(BaseModel):
peer_refs: list[str] = Field(default_factory=list)
ambiguous_logical_ids: list[str] = Field(default_factory=list)
notes: str = ""
connector: str = ""
physical_implementation: str = ""
def _leaf(name: str) -> str:
@@ -267,6 +282,8 @@ def recognize_physical_buses(
out: list[PhysicalBusInstance] = []
out.extend(_recognize_declared(graph, ifaces, logical))
out.extend(_recognize_usb(graph, ifaces))
out.extend(_recognize_ethernet(graph, ifaces))
out.extend(_recognize_hdmi(graph, ifaces))
out.extend(_recognize_hbm(graph, ifaces))
out.extend(_recognize_ddr(graph, ifaces))
out.extend(_recognize_axi(graph, ifaces))
@@ -324,35 +341,42 @@ def _recognize_declared(
def _recognize_usb(graph: DesignGraph, ifaces: dict) -> list[PhysicalBusInstance]:
instances: list[PhysicalBusInstance] = []
connectors = []
type_c = []
other_usb_conn = []
for comp in graph.components.values():
if comp.component_type != ComponentType.CONNECTOR:
continue
blob = _blob(comp, graph)
if _USB_C_RE.search(blob) or _USB_C_RE.search(comp.footprint):
connectors.append(comp)
if _USB_C_RE.search(blob) or _USB_C_RE.search(comp.footprint or ""):
type_c.append(comp)
elif re.search(r"USB", blob, re.I) or re.search(r"USB", comp.footprint or "", re.I):
other_usb_conn.append(comp)
pairs = _usb_pairs(list(graph.nets))
if not connectors and not pairs:
silicon = [
c for c in graph.components.values()
if c.component_type == ComponentType.IC and (
_USB_SILICON_RE.search(_blob(c, graph))
or (pairs and any(
n in (c.pins or {}).values() for n in [p for pair in pairs for p in pair]
))
)
]
ss_nets = [n for n in graph.nets if _USB3_RE.search(_leaf(n)) or _USB3_RE.search(n)]
usb4_ev = any(_USB4_RE.search(_blob(c, graph)) for c in graph.components.values())
usb3_ev = bool(ss_nets) or any(
_USB3_RE.search(_blob(c, graph)) for c in [*type_c, *silicon]
)
if not type_c and not other_usb_conn and not pairs:
return []
usb3 = any(_USB3_RE.search(_blob(c, graph)) for c in connectors)
usb2_mark = any(_USB2_RE.search(_blob(c, graph)) for c in connectors)
hs = any(_USB_HS_RE.search(_blob(c, graph)) for c in connectors)
lsfs = any(_USB_LSFS_RE.search(_blob(c, graph)) for c in connectors)
ambiguous = False
notes = ""
if connectors and usb3 and usb2_mark:
ambiguous = True
notes = "USB2 vs USB3 on the same receptacle — REVIEW, not FAIL."
if not hs and not lsfs and pairs:
ambiguous = True
notes = (notes + " " if notes else "") + "USB2 speed not declared; HS vs LS/FS REVIEW."
hs = any(_USB_HS_RE.search(_blob(c, graph)) for c in [*type_c, *other_usb_conn, *silicon])
lsfs = any(_USB_LSFS_RE.search(_blob(c, graph)) for c in [*type_c, *other_usb_conn, *silicon])
if hs and not lsfs:
logical, phys = "usb2-hs", "usb2-hs-dpair"
usb2_log, usb2_phys = "usb2-hs", "usb2-hs-dpair"
elif lsfs and not hs:
logical, phys = "usb2-ls-fs", "usb2-ls-fs-dpair"
usb2_log, usb2_phys = "usb2-ls-fs", "usb2-ls-fs-dpair"
else:
logical, phys = "usb2-hs", "usb2-hs-dpair"
ambiguous = True
usb2_log, usb2_phys = "usb2-hs", "usb2-hs-dpair"
groups = [
SignalGroup(
@@ -364,50 +388,307 @@ def _recognize_usb(graph: DesignGraph, ifaces: dict) -> list[PhysicalBusInstance
for i, (a, b) in enumerate(pairs)
]
nets = sorted({n for g in groups for n in g.nets})
host = connectors[0].reference if connectors else (nets[0] if nets else "usb")
evidence: EvidenceKind = "part" if connectors else "net_name"
if connectors and any(_USB_C_RE.search(_blob(c, graph)) for c in connectors):
cphys = "usb-c-usb2-receptacle"
ciface = ifaces[cphys]
status: RecognitionStatus = "REVIEW" if ambiguous or usb3 else "RECOGNIZED"
if type_c:
host = type_c[0].reference
pin_notes = _usb_c_orientation_note(type_c[0], graph)
combo = bool(pairs) and bool(silicon)
# Connector alone does not declare USB 2.0 / 3.x / USB4.
if not combo:
iface = ifaces["usb-c-receptacle"]
instances.append(PhysicalBusInstance(
instance_id=f"usb-c-receptacle:{host}",
logical_protocol_id="usb-c",
physical_interface_id="usb-c-receptacle",
pcb_relevant=iface.pcb_relevant,
confidence=_EVIDENCE_SCORE["part"],
evidence_kind="part",
recognition_status="REVIEW",
nets=nets,
groups=groups,
host_ref=host,
peer_refs=[c.reference for c in type_c],
notes=(
"USB Type-C connector only; protocol not inferred "
"(need nets + silicon + topology). Not USB3. Not USB4. "
+ pin_notes
),
connector="Type-C",
physical_implementation="",
))
return instances
iface = ifaces["usb-c-usb2-receptacle"]
amb = [] if hs ^ lsfs else ["usb2-ls", "usb2-fs", "usb2-hs"]
status: RecognitionStatus = "REVIEW" if amb or usb3_ev else "RECOGNIZED"
notes = (
"USB 2.0 over USB Type-C (connector+nets+silicon). "
"Type-C is the connector, USB 2.0 is the protocol. "
+ pin_notes
)
if usb3_ev:
notes += " SuperSpeed evidence present — USB3 is a separate implementation, not implied by Type-C."
amb = list(dict.fromkeys(amb + ["usb3-x"]))
instances.append(PhysicalBusInstance(
instance_id=f"{cphys}:{host}",
logical_protocol_id="usb-c-usb2",
physical_interface_id=cphys,
pcb_relevant=ciface.pcb_relevant,
confidence=_EVIDENCE_SCORE[evidence],
evidence_kind=evidence,
instance_id=f"usb-c-usb2-receptacle:{host}",
logical_protocol_id=usb2_log,
physical_interface_id="usb-c-usb2-receptacle",
pcb_relevant=iface.pcb_relevant,
confidence=_EVIDENCE_SCORE["silicon"],
evidence_kind="silicon",
recognition_status=status,
nets=nets,
groups=groups,
host_ref=host,
peer_refs=[c.reference for c in connectors],
ambiguous_logical_ids=["usb-c-usb2", "usb2-hs"] if ambiguous else [],
peer_refs=[c.reference for c in type_c] + [s.reference for s in silicon],
ambiguous_logical_ids=amb,
notes=notes,
connector="Type-C",
physical_implementation="USB 2.0 over USB Type-C",
))
if usb3_ev and "usb3-over-usb-c" in ifaces:
instances.append(PhysicalBusInstance(
instance_id=f"usb3-over-usb-c:{host}",
logical_protocol_id="usb3-x",
physical_interface_id="usb3-over-usb-c",
pcb_relevant="YES",
confidence=_EVIDENCE_SCORE["net_name"],
evidence_kind="net_name",
recognition_status="REVIEW",
nets=sorted(ss_nets),
host_ref=host,
notes="USB 3.x over Type-C from SS evidence; not USB4; not implied by connector alone.",
connector="Type-C",
physical_implementation="USB 3.x over USB Type-C",
))
if usb4_ev and "usb4-over-usb-c" in ifaces:
instances.append(PhysicalBusInstance(
instance_id=f"usb4-over-usb-c:{host}",
logical_protocol_id="usb4",
physical_interface_id="usb4-over-usb-c",
pcb_relevant="YES",
confidence=_EVIDENCE_SCORE["part"],
evidence_kind="part",
recognition_status="REVIEW",
host_ref=host,
notes="USB4 evidence distinct from Type-C and USB3.",
connector="Type-C",
physical_implementation="USB4 over USB Type-C",
))
return instances
iface = ifaces[phys]
status = "REVIEW" if ambiguous else "RECOGNIZED"
amb = ["usb2-ls-fs", "usb2-hs"] if ambiguous else []
if not pairs:
return []
host = other_usb_conn[0].reference if other_usb_conn else (
silicon[0].reference if silicon else (nets[0] if nets else "usb")
)
if not other_usb_conn and not silicon:
# Pairs without connector or silicon: net-name only, REVIEW.
evidence: EvidenceKind = "net_name"
conf = _EVIDENCE_SCORE["net_name"]
status = "REVIEW"
notes = "USB 2.0 inferred from D+/D nets only; connector+silicon missing."
else:
evidence = "silicon" if silicon else "part"
conf = _EVIDENCE_SCORE[evidence]
status = "REVIEW" if not (hs ^ lsfs) else "RECOGNIZED"
notes = "USB 2.0 from connector+nets+silicon/topology. No invented Z."
iface = ifaces[usb2_phys]
instances.append(PhysicalBusInstance(
instance_id=f"{phys}:{host}",
logical_protocol_id=logical,
physical_interface_id=phys,
instance_id=f"{usb2_phys}:{host}",
logical_protocol_id=usb2_log,
physical_interface_id=usb2_phys,
pcb_relevant=iface.pcb_relevant,
confidence=_EVIDENCE_SCORE[evidence],
confidence=conf,
evidence_kind=evidence,
recognition_status=status,
nets=nets,
groups=groups,
host_ref=host,
peer_refs=[c.reference for c in connectors],
ambiguous_logical_ids=amb,
peer_refs=[c.reference for c in other_usb_conn] + [s.reference for s in silicon],
ambiguous_logical_ids=["usb2-ls-fs", "usb2-hs"] if status == "REVIEW" else [],
notes=notes,
connector="",
physical_implementation="USB 2.0",
))
return instances
def _usb_c_orientation_note(comp: Component, graph: DesignGraph) -> str:
"""D+/D from schematic pins; do not assume a mux mapping."""
a_side = []
b_side = []
for pin, net in (comp.pins or {}).items():
pu = pin.upper()
if pu in {"A6", "A7"} and net:
a_side.append(f"{pin}={net}")
if pu in {"B6", "B7"} and net:
b_side.append(f"{pin}={net}")
if a_side and b_side:
return (
f" D+/D both orientations on schematic ({', '.join(a_side + b_side)}); "
f"mux/switch not assumed."
)
if a_side or b_side:
return (
f" D+/D orientation from schematic pins {', '.join(a_side or b_side)}; "
f"opposite-side mux not assumed."
)
return " D+/D orientation taken from schematic; mux not assumed."
def _recognize_ethernet(graph: DesignGraph, ifaces: dict) -> list[PhysicalBusInstance]:
rj45 = [
c for c in graph.components.values()
if c.component_type == ComponentType.CONNECTOR
and (_RJ45_RE.search(_blob(c, graph)) or _RJ45_RE.search(c.footprint or ""))
]
phys = [
c for c in graph.components.values()
if c.component_type == ComponentType.IC and _ETH_PHY_RE.search(_blob(c, graph))
]
mags = [c for c in graph.components.values() if _ETH_MAG_RE.search(_blob(c, graph))]
named = " ".join(_blob(c, graph) for c in graph.components.values()) + " " + " ".join(graph.nets)
if not rj45 and not phys and not re.search(r"RGMII|SGMII|1000BASE|10BASE|100BASE", named, re.I):
return []
if rj45 and not phys and not mags and not re.search(
r"1000BASE-T|100BASE-TX|10BASE-T|SGMII|RGMII", named, re.I,
):
iface = ifaces.get("ethernet-rj45-unspecified")
if iface is None:
return []
host = rj45[0].reference
return [PhysicalBusInstance(
instance_id=f"ethernet-rj45-unspecified:{host}",
logical_protocol_id="ethernet-unspecified",
physical_interface_id="ethernet-rj45-unspecified",
pcb_relevant=iface.pcb_relevant,
confidence=_EVIDENCE_SCORE["part"],
evidence_kind="part",
recognition_status="REVIEW",
host_ref=host,
peer_refs=[c.reference for c in rj45],
notes="RJ45 connector alone is not 100BASE-TX or 1000BASE-T.",
connector="RJ45",
physical_implementation="",
)]
lid, pid = "100base-tx", "100base-tx-mdi"
if re.search(r"1000BASE-T|\bGBE\b|GIGABIT", named, re.I):
lid, pid = "1000base-t", "1000base-t-mdi"
elif re.search(r"10GBASE-T", named, re.I):
lid, pid = "10gbase-t", "10gbase-t-mdi"
elif re.search(r"2\.5GBASE", named, re.I):
lid, pid = "2.5gbase-t", "2.5gbase-t-mdi"
elif re.search(r"5GBASE-T", named, re.I):
lid, pid = "5gbase-t", "5gbase-t-mdi"
elif re.search(r"SGMII", named, re.I):
lid, pid = "sgmii", "sgmii-pcb"
elif re.search(r"1000BASE-X", named, re.I):
lid, pid = "1000base-x", "1000base-x-pcb"
elif re.search(r"100BASE-FX", named, re.I):
lid, pid = "100base-fx", "100base-fx-pcb"
elif re.search(r"10GBASE-R", named, re.I):
lid, pid = "10gbase-r", "10gbase-r-pcb"
elif re.search(r"10BASE-T", named, re.I):
lid, pid = "10base-t", "10base-t-mdi"
elif re.search(r"100BASE-TX", named, re.I):
lid, pid = "100base-tx", "100base-tx-mdi"
else:
return []
iface = ifaces.get(pid)
if iface is None:
return []
host = (phys[0].reference if phys else (rj45[0].reference if rj45 else lid))
return [PhysicalBusInstance(
instance_id=f"{pid}:{host}",
logical_protocol_id=lid,
physical_interface_id=pid,
pcb_relevant=iface.pcb_relevant,
confidence=_EVIDENCE_SCORE["silicon"] if phys else _EVIDENCE_SCORE["part"],
evidence_kind="silicon" if phys else "part",
recognition_status="REVIEW",
host_ref=host,
notes="Ethernet variant from PHY/magnetics/naming; IEEE vs PHY vs PCB vs magnetics stay distinct. Incomplete without IEEE 802.3 on file.",
connector=iface.connector,
physical_implementation=lid,
)]
def _recognize_hdmi(graph: DesignGraph, ifaces: dict) -> list[PhysicalBusInstance]:
conns = [
c for c in graph.components.values()
if c.component_type == ComponentType.CONNECTOR and (
_HDMI_RE.search(_blob(c, graph)) or _HDMI_RE.search(c.footprint or "")
or _HDMI_MINI_RE.search(_blob(c, graph))
)
]
tmds = [n for n in graph.nets if _HDMI_TMDS_NET.search(n) or _HDMI_TMDS_NET.search(_leaf(n))]
blob = " ".join(_blob(c, graph) for c in graph.components.values())
frl = bool(_HDMI_FRL_RE.search(blob) or any(_HDMI_FRL_RE.search(n) for n in graph.nets))
mini = any(
_HDMI_MINI_RE.search(_blob(c, graph)) or _HDMI_MINI_RE.search(c.footprint or "")
for c in conns
)
if not conns and not tmds and not frl:
return []
if conns and not tmds and not frl:
host = conns[0].reference
conn_name = "HDMI-C-mini" if mini else "HDMI-A"
pid = "hdmi-1.4-tmds-type-c-mini" if mini else "hdmi-1.4-tmds-type-a"
iface = ifaces.get(pid)
if iface is None:
return []
return [PhysicalBusInstance(
instance_id=f"{pid}:{host}",
logical_protocol_id="hdmi-1.4",
physical_interface_id=pid,
pcb_relevant=iface.pcb_relevant,
confidence=_EVIDENCE_SCORE["part"],
evidence_kind="part",
recognition_status="REVIEW",
host_ref=host,
notes=(
"HDMI connector only; Mini HDMI is CONNECTOR not a protocol. "
"Version/mode not assumed from connector. TMDS vs FRL not inferred."
),
connector=conn_name,
physical_implementation="",
)]
if frl:
lid, pid = "hdmi-frl", "hdmi-frl-pcb"
conn_name = "HDMI-C-mini" if mini else "HDMI-A"
else:
lid = "hdmi-1.4"
pid = "hdmi-1.4-tmds-type-c-mini" if mini else "hdmi-1.4-tmds-type-a"
conn_name = "HDMI-C-mini" if mini else "HDMI-A"
if re.search(r"HDMI\s*2\.0|HDMI2\.0", blob, re.I):
lid = "hdmi-2.0"
pid = "hdmi-2.0-tmds-type-c-mini" if mini else "hdmi-2.0-tmds-type-a"
iface = ifaces.get(pid)
if iface is None:
return []
host = conns[0].reference if conns else (tmds[0] if tmds else lid)
mode = "FRL" if frl else "TMDS"
return [PhysicalBusInstance(
instance_id=f"{pid}:{host}",
logical_protocol_id=lid,
physical_interface_id=pid,
pcb_relevant=iface.pcb_relevant,
confidence=_EVIDENCE_SCORE["net_name"],
evidence_kind="net_name",
recognition_status="REVIEW",
nets=tmds,
host_ref=host,
notes=(
f"HDMI {mode} electrical; connector={conn_name}. "
f"Mini HDMI is not a protocol id. TMDS rules are not applied to FRL."
),
connector=conn_name,
physical_implementation=f"HDMI {mode}",
)]
def _ddr_gen_from_text(text: str) -> str | None:
if _DDR4_PART.search(text):
return "ddr4"
@@ -2,6 +2,14 @@
What's new in Periscope.
## 2.73.0 — 2026-09-22 — Protocol certification M6/M7 (USB-C / Ethernet / HDMI packs)
USB 2.0 (LS/FS/HS) is the protocol; USB Type-C is a connector/interface system; USB 2.0 over Type-C is the physical implementation. Type-C does not imply USB3 or USB4. Mini HDMI is a connector, not a protocol. Ethernet variants (10/100/1000/2.5G/5G/10G, SGMII, 1000BASE-X, 100BASE-FX, 10GBASE-R) are incomplete skeletons. All listed numeric fields stay **UNKNOWN** with `needed_document` (USB-IF / IEEE 802.3 / HDMI Adopter). No invented ohms, no typical-as-standard, no OpenEMS.
- [Changed] `protocol_catalog.py` + `catalog.json`; `needed_document` on UNKNOWN/MISSING_SOURCE.
- [Changed] USB/Ethernet/HDMI recognition: connector alone is not the protocol.
- [New] pytest `tests/pcb/test_protocol_m6_m7.py`.
## 2.72.0 — 2026-09-22 — Protocol certification M10 (catalog skeletons)
Catalog skeletons and `required_checks` for **PCIe / CXL** (logical vs physical), **I2C**, **CAN**, **RGMII**, plus missing high-speed processor ids (PCI, PCI-X, CCIX, OpenCAPI, UPI, DMI, Infinity Fabric, HyperTransport, UCIe). Numbers stay **UNKNOWN / MISSING_SOURCE / PHY_DEPENDENT** until an official cite is in the pack. `pcb_relevant` is YES or CONDITIONAL as appropriate. No invented ohms, no USB-IF fill, no OpenEMS.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "periscope-web",
"version": "2.72.0",
"version": "2.73.0",
"private": true,
"scripts": {
"sync-version": "node scripts/sync-version.mjs",
+3 -2
View File
@@ -23,8 +23,9 @@ from backend.periscopex.protocol_catalog import (
REQUIRED_LOGICAL = {
"usb2-ls-fs", "usb2-hs", "usb-c-usb2", "100base-tx",
"hdmi-1.4", "hdmi-2.0",
"usb2-ls-fs", "usb2-ls", "usb2-fs", "usb2-hs", "usb-c", "usb-c-usb2",
"usb3-x", "usb4", "100base-tx", "10base-t", "1000base-t",
"hdmi-1.4", "hdmi-2.0", "hdmi-frl",
"ddr-family", "ddr4", "lpddr-family", "lpddr4",
"hbm-family", "hbm3", "hyperbus", "qspi", "octal-spi", "emmc", "ufs",
"axi4", "axi4-lite", "ahb", "apb", "wishbone", "avalon-mm", "avalon-st",
+1 -1
View File
@@ -102,7 +102,7 @@ def test_m10_no_invented_ohms_and_no_usbif_fill():
assert token not in blob
usb = next(p for p in loaded.physical_interfaces if p.id == "usb2-hs-dpair")
z = next(c for c in usb.constraints if c.parameter == "differential_impedance")
assert z.value_kind == "MISSING_SOURCE"
assert z.value_kind in {"MISSING_SOURCE", "UNKNOWN"}
assert z.value is None
assert z.source is None
+224
View File
@@ -0,0 +1,224 @@
"""M6/M7 USB-C/Ethernet/HDMI packs: UNKNOWN + needed_document, no invented ohms."""
from __future__ import annotations
import copy
import json
import pytest
from backend.periscopex.models import (
Component,
ComponentType,
DesignGraph,
Net,
NetType,
PinConnection,
)
from backend.periscopex.protocol_catalog import (
ProtocolCatalogError,
build_m0_catalog,
load_catalog,
parse_catalog,
)
from backend.periscopex.protocol_recognize import recognize_physical_buses
def _ic(ref: str, pins: dict[str, str], *, mpn: str = "", value: str = "") -> Component:
return Component(
reference=ref, value=value or mpn, footprint="",
component_type=ComponentType.IC, mpn=mpn or None, pins=pins,
)
def _net(name: str, *pairs: tuple[str, str]) -> Net:
return Net(
name=name, net_type=NetType.SIGNAL,
pins=[PinConnection(component_ref=r, pin_number=p) for r, p in pairs],
)
def test_unknown_requires_needed_document():
raw = build_m0_catalog()
iface = copy.deepcopy(raw["physical_interfaces"][0])
iface["id"] = "usb2-hs-no-doc"
bad = copy.deepcopy(iface["constraints"][0])
bad["id"] = "no-doc"
bad["value_kind"] = "UNKNOWN"
bad["value"] = None
bad["needed_document"] = None
iface["constraints"] = [bad]
raw["physical_interfaces"].append(iface)
with pytest.raises(ProtocolCatalogError, match="needed_document"):
parse_catalog(raw)
def test_reject_typical_as_standard_still_holds():
raw = build_m0_catalog()
iface = copy.deepcopy(raw["physical_interfaces"][0])
typical = copy.deepcopy(iface["constraints"][0])
typical["id"] = "bad-typical-m67"
typical["typical"] = True
typical["value_origin"] = "TYPICAL"
typical["source_type"] = "STANDARD"
typical["source_class"] = "NORMATIVE"
typical["mandatory"] = "MANDATORY"
typical["value_kind"] = "UNKNOWN"
typical["value"] = None
typical["needed_document"] = "USB-IF Base Specification"
iface["id"] = "usb2-hs-typical-m67"
iface["constraints"] = [typical]
raw["physical_interfaces"].append(iface)
with pytest.raises(ProtocolCatalogError, match="typical"):
parse_catalog(raw)
def test_usb2_and_usb_c_unknown_with_usbif_needed_document_no_90ohm():
cat = load_catalog()
logical = {p.id for p in cat.logical_protocols}
assert {"usb2-ls", "usb2-fs", "usb2-hs", "usb-c", "usb3-x", "usb4"} <= logical
assert "mini-hdmi" not in logical
usb_ifaces = [
p for p in cat.physical_interfaces
if p.logical_protocol_id in {"usb2-ls", "usb2-fs", "usb2-hs", "usb2-ls-fs", "usb-c", "usb3-x", "usb4"}
or p.id.startswith("usb-")
]
assert usb_ifaces
blob = json.dumps([c.model_dump() for p in usb_ifaces for c in p.constraints])
assert "90" not in blob
for p in usb_ifaces:
for c in p.constraints:
assert c.value is None
assert c.value_kind != "NUMERIC"
if c.value_kind in {"UNKNOWN", "MISSING_SOURCE"}:
assert c.needed_document
assert "USB-IF" in c.needed_document or "specification" in c.needed_document.lower()
over = next(p for p in cat.physical_interfaces if p.id == "usb-c-usb2-receptacle")
assert over.logical_protocol_id == "usb2-hs"
assert over.connector == "Type-C"
sys = next(p for p in cat.physical_interfaces if p.id == "usb-c-receptacle")
assert sys.logical_protocol_id == "usb-c"
assert sys.connector == "Type-C"
def test_usb_c_connector_alone_is_not_usb3_or_usb2_protocol():
g = DesignGraph(
components={
"J1": Component(
reference="J1", value="USB Type-C receptacle",
footprint="USB_C_Receptacle",
component_type=ComponentType.CONNECTOR,
pins={"A5": "CC1"},
),
},
nets={"CC1": _net("CC1", ("J1", "A5"))},
)
insts = recognize_physical_buses(g)
logs = {i.logical_protocol_id for i in insts}
assert "usb3-x" not in logs
assert "usb4" not in logs
assert "usb2-hs" not in logs
assert any(i.logical_protocol_id == "usb-c" for i in insts)
blob = json.dumps([i.model_dump() for i in insts])
assert "90" not in blob
def test_usb2_over_type_c_from_connector_nets_silicon_not_usb3():
g = DesignGraph(
components={
"U1": _ic("U1", {"1": "USB_D+", "2": "USB_D-"}, mpn="CH340E"),
"J2": Component(
reference="J2", value="USB_C_Receptacle_USB2.0_16P",
footprint="USB_C_Receptacle_USB2.0_16P",
component_type=ComponentType.CONNECTOR,
pins={"A6": "USB_D+", "A7": "USB_D-"},
),
},
nets={
"USB_D+": _net("USB_D+", ("U1", "1"), ("J2", "A6")),
"USB_D-": _net("USB_D-", ("U1", "2"), ("J2", "A7")),
},
)
insts = recognize_physical_buses(g)
usb = [i for i in insts if i.logical_protocol_id.startswith("usb2")]
assert usb
one = usb[0]
assert one.connector == "Type-C"
assert "USB 2.0 over USB Type-C" in one.physical_implementation
assert one.logical_protocol_id != "usb3-x"
assert all(i.logical_protocol_id != "usb3-x" for i in insts)
assert "mux not assumed" in one.notes.lower() or "mux/switch not assumed" in one.notes.lower()
blob = json.dumps(one.model_dump())
assert "90" not in blob
def test_rj45_alone_is_not_1000base_t():
g = DesignGraph(
components={
"J9": Component(
reference="J9", value="RJ45",
footprint="RJ45",
component_type=ComponentType.CONNECTOR,
pins={"1": "TD+"},
),
},
nets={"TD+": _net("TD+", ("J9", "1"))},
)
insts = recognize_physical_buses(g)
logs = {i.logical_protocol_id for i in insts}
assert "1000base-t" not in logs
assert "100base-tx" not in logs
assert any(i.logical_protocol_id == "ethernet-unspecified" for i in insts)
def test_mini_hdmi_is_connector_not_protocol_id():
cat = load_catalog()
logical = {p.id for p in cat.logical_protocols}
assert "mini-hdmi" not in logical
assert "hdmi-mini" not in logical
mini = next(p for p in cat.physical_interfaces if p.id == "hdmi-1.4-tmds-type-c-mini")
assert mini.logical_protocol_id == "hdmi-1.4"
assert mini.connector == "HDMI-C-mini"
assert "CONNECTOR" in mini.notes or "connector" in mini.notes.lower()
g = DesignGraph(
components={
"J8": Component(
reference="J8", value="Mini HDMI",
footprint="HDMI_C_Mini",
component_type=ComponentType.CONNECTOR,
pins={"1": "HPD"},
),
},
nets={"HPD": _net("HPD", ("J8", "1"))},
)
insts = recognize_physical_buses(g)
assert all(i.logical_protocol_id != "mini-hdmi" for i in insts)
hdmi = [i for i in insts if "hdmi" in i.logical_protocol_id]
assert hdmi
assert hdmi[0].connector == "HDMI-C-mini"
assert hdmi[0].logical_protocol_id.startswith("hdmi-")
def test_ethernet_hdmi_skeletons_unknown_needed_document():
cat = load_catalog()
for lid in (
"10base-t", "100base-tx", "1000base-t", "2.5gbase-t", "5gbase-t",
"10gbase-t", "sgmii", "1000base-x", "100base-fx", "10gbase-r",
"hdmi-1.4", "hdmi-2.0", "hdmi-frl",
):
ifaces = [p for p in cat.physical_interfaces if p.logical_protocol_id == lid]
assert ifaces, lid
assert all(p.incomplete for p in ifaces)
for p in ifaces:
for c in p.constraints:
assert c.value is None
assert c.value_kind != "NUMERIC"
if c.value_kind in {"UNKNOWN", "MISSING_SOURCE"}:
assert c.needed_document
if lid.startswith("hdmi"):
assert "HDMI" in c.needed_document
else:
assert "IEEE" in c.needed_document or "PHY" in (c.needed_document or "")
tmds = next(p for p in cat.physical_interfaces if p.id == "hdmi-1.4-tmds-type-a")
frl = next(p for p in cat.physical_interfaces if p.id == "hdmi-frl-pcb")
assert tmds.logical_protocol_id != frl.logical_protocol_id