Reject WROOM pintables taken from the bare ESP32 die map.

Module pad 1 is GND; extracting ANT/XTAL as pin 1 made every ground look like a shorted antenna.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-28 02:55:12 +02:00
co-authored by Cursor
parent e84418f975
commit ed75aabaf0
5 changed files with 46 additions and 2 deletions
+5 -1
View File
@@ -148,8 +148,12 @@ async def run_skill_locally(
errors: list[str] = [] errors: list[str] = []
if validator is not None: if validator is not None:
check = dict(payload)
mpn_hint = re.search(r"MPN:\s*(\S+)", user_text or "", re.I)
if mpn_hint and "mpn" not in check:
check["mpn"] = mpn_hint.group(1).rstrip(".,;")
try: try:
errors = list(validator(payload) or []) errors = list(validator(check) or [])
except Exception as exc: except Exception as exc:
log.warning( log.warning(
"Skill %s validate.py raised: %s", skill_name, exc, "Skill %s validate.py raised: %s", skill_name, exc,
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"default_model_version": "1.5.0", "default_model_version": "1.6.0",
"extract-pintable": { "extract-pintable": {
"skill_id": "skill_01VMWPZuvuZAe4LmLbmsNWNY", "skill_id": "skill_01VMWPZuvuZAe4LmLbmsNWNY",
"latest_version": "1784798970179642", "latest_version": "1784798970179642",
+3
View File
@@ -30,6 +30,9 @@ Rules for pin extraction:
- For multiplexed pins, put the primary name in `name` and alternates in `functions` - For multiplexed pins, put the primary name in `name` and alternates in `functions`
- If the datasheet has separate tables for different packages, extract for the package matching the MPN - If the datasheet has separate tables for different packages, extract for the package matching the MPN
- Pay careful attention to pin numbering — off-by-one errors here break everything downstream - Pay careful attention to pin numbering — off-by-one errors here break everything downstream
- **Modules vs bare die (critical).** MPNs containing `WROOM`, `WROVER`, `MODULE`, `MOD-`, or `SIP` are *modules*. Extract the **module landing-pad table** (connector pins the schematic uses). Do **not** extract the SoC/QFN ball map from a nested chip chapter or a sibling chip-only PDF.
- Espressif WROOM: pin 1 is GND (often a group of GND pads). Pin 1 named `ANT`, `CHIP_PU`, or `XTAL_*` means you grabbed the bare ESP32 die table — that will mark every module GND as “antenna shorted” and is invalid.
- Crystal, RF antenna, and flash on a WROOM module are **inside the can**; they must not appear as schematic pin numbers.
### 3. Extract package info ### 3. Extract package info
+19
View File
@@ -2,6 +2,7 @@
"""Validate extraction output against the pintable schema.""" """Validate extraction output against the pintable schema."""
import json import json
import re
import sys import sys
from pathlib import Path from pathlib import Path
@@ -47,6 +48,24 @@ def validate(data: dict) -> list[str]:
if dupes: if dupes:
errors.append(f"Duplicate pin numbers: {dupes}") errors.append(f"Duplicate pin numbers: {dupes}")
names = {
str(p.get("number")): str(p.get("name") or "").upper()
for p in pins if "number" in p
}
pin1 = names.get("1", "")
looks_like_rf_die = bool(
re.search(r"\bANT\b|^CHIP_PU$|^XTAL", pin1)
and any("XTAL" in n for n in names.values())
)
mpn = str(data.get("mpn") or "")
is_module_mpn = bool(re.search(r"WROOM|WROVER|\bMODULE\b|\bSIP\b", mpn, re.I))
if looks_like_rf_die and is_module_mpn:
errors.append(
"Pin 1 looks like a bare RF SoC ball (ANT/CHIP_PU) with XTAL "
"pins in the table. Module footprints (WROOM) use pad 1 = GND; "
"extract the module landing-pad table, not the die map."
)
if "absolute_maximum_ratings" in data: if "absolute_maximum_ratings" in data:
ratings = data["absolute_maximum_ratings"] ratings = data["absolute_maximum_ratings"]
if ratings is not None and not isinstance(ratings, list): if ratings is not None and not isinstance(ratings, list):
+18
View File
@@ -212,6 +212,7 @@ def test_local_skills_load():
validate = load_skill_validator("extract-pintable") validate = load_skill_validator("extract-pintable")
assert validate is not None assert validate is not None
errors = validate({ errors = validate({
"mpn": "MSPM0G3507SPTR",
"component_subtype": "ic.mcu", "component_subtype": "ic.mcu",
"component_subtype_description": "MCU", "component_subtype_description": "MCU",
"package_info": {"base_family": "MSPM0", "package": "LQFP-48", "pin_count": 2}, "package_info": {"base_family": "MSPM0", "package": "LQFP-48", "pin_count": 2},
@@ -223,6 +224,23 @@ def test_local_skills_load():
assert errors == [] assert errors == []
def test_wroom_rejects_bare_soc_pin1_ant():
from backend.services.llm.local_skill import load_skill_validator
validate = load_skill_validator("extract-pintable")
errors = validate({
"mpn": "ESP32-S31-WROOM-3",
"component_subtype": "ic.mcu",
"package_info": {"base_family": "ESP32-S31", "package": "module", "pin_count": 2},
"pintable": [
{"number": 1, "name": "ANT"},
{"number": 2, "name": "CHIP_PU"},
{"number": 78, "name": "XTAL_N"},
{"number": 79, "name": "XTAL_P"},
],
})
assert any("module" in e.lower() or "pad" in e.lower() or "SoC" in e or "WROOM" in e for e in errors)
def test_factory_routes_deepseek(monkeypatch): def test_factory_routes_deepseek(monkeypatch):
monkeypatch.setattr(settings, "deepseek_api_key", "sk-test") monkeypatch.setattr(settings, "deepseek_api_key", "sk-test")
from backend.services.llm.factory import get_provider_by_name from backend.services.llm.factory import get_provider_by_name