Files
periscope/docs/piano-pcb-review.md
T
michele 721ede4243 Add PCB review plan: verify placement and routing, no auto-place.
Documents a parallel MODE=pcb exam job (inventory + datasheet-gated
checks) that reuses placement_check, si_check, and the placement job
pattern. Parks F2 packing/export.
2026-09-19 21:41:27 +02:00

285 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Piano — PCB review (controllo placement + routing)
Documento **solo di piano**. Non è un implementazione sprint e **non** è un auto-placer.
Prodotto: **Periscope Layout review** — esaminare un board KiCad già sbrogliato (o parzialmente sbrogliato) e dire *dove* placement e routing violano geometria + datasheet.
Contratto rispetto a [`docs/piano-implementazione.md`](piano-implementazione.md):
| | Schema (già in produzione) | PCB review (questo piano) | Placement F2 pack (fuori da questo piano) |
| --- | --- | --- | --- |
| Promessa | Lo schema rispetta il datasheet | Il rame e le posizioni **esistenti** rispettano datasheet + geometria | Proporre xy nuovi |
| Trigger | BOM + netlist / `.kicad_sch` | + `.kicad_pcb` | PCB + `layout_rules` numerici |
| Output | Finding pin/net, derating, review LLM | Finding mm / net / Z0 / skew / via EP, `rule_id` `PE-PLC*` / `PE-SI*` / `PE-LAY*` | `placement_pack.json` proposte |
| Job | `MODE=run` (`status`) | Job parallelo `MODE=pcb` (`pcb_status`) | già `MODE=placement` (`placement_status`) |
Senza `.kicad_pcb` il progetto resta un Periscope schema **completo**. Il report schema **non** si riempie di `PE-PLC` / `PE-SI`.
---
## 0. Postura: esame, non progettazione
### Placement = verifica
Si misura ciò che è **già** sul board:
- distanza footprint/pad (euclidea e shortest-path sui segmenti);
- layer (F.Cu vs B.Cu) vs `same_layer`;
- via nel courtyard del pad termico vs `min_via_count`;
- keepout: net estraneo nel courtyard;
- crystal: load cap vs pin XIN/XOUT con la stessa metrica.
Non si spostano footprint. Non si scrive `.kicad_pcb`. Non si usa `placement_pack` / `build_placement_pack` come obiettivo di questo piano.
`placement_check` (`backend/periscopex/placement_check.py`, `PE-PLC-001``004`) è già questa postura. Resta il nucleo placement del job PCB.
La pipeline Placement esistente (`POST …/placement/start`, `placement_plan.json`) è **topologia routing-first** (domini, satelliti, `role_hint`). Utile come contesto (“questo C è decoupling di U1”) ma **non** è auto-placement. F2 packing (`placement_pack.json`, collisioni, export pcbnew) resta **parcheggiato**.
### Routing = la stessa postura
Si misura ciò che è **già** in rame:
- lunghezza net / coppia / bus (somma segmenti, non folklore USB);
- intra-pair skew vs `layout_rules.kind=length_match` **solo se** c’è un mm nel datasheet (`si_check`, `PE-SI-001`);
- Z0 sulle tracce dove stackup + larghezza esistono (`impedance_traces.py` + ImpedanceFinder);
- mismatch pad PCB vs pin schema (net name su pad vs `DesignGraph.pin_net`);
- elenco tracce / bus per il report (inventario, poi check).
Non si sbroglia. Non si cambia larghezza. Non si inventano 3W, creepage IEC, o target 50 Ω se il datasheet/stackup non li danno.
### Datasheet compliance (layout)
Solo vincoli **strutturati** già estratti in `ComponentConstraints.layout_rules` (skill C4, `layout_rules.py`):
| `kind` | Check PCB | Skip se |
| --- | --- | --- |
| `decoupling_proximity` | distanza / path vs `max_distance_mm`; crystal stesso | `max_distance_mm` null |
| `thermal_via` | via in courtyard vs `min_via_count` | courtyard o count assenti |
| `keepout` | endpoint net estraneo in courtyard | courtyard assente |
| `length_match` | skew coppia vs `max_distance_mm` | mm assente |
Niente default 3 mm. Niente “foto del layout TI vs gerber”. Citazione: `source_page` sul finding, come oggi.
---
## 1. Come sta in parallelo alla pipeline schema
Oggi i check layout (`placement_check`, `si_check`) girano **dentro** la review schema (`backend/services/validation.py``_run_deterministic_checks`) se esiste `layout_graph.json`. Funziona, ma mescola due prodotti nello stesso `report.json` e nello stesso `status`.
Target:
```
┌─ MODE=run → status → report.json (schema)
Project files ───┼─ MODE=placement → placement_status → placement_plan.json
└─ MODE=pcb → pcb_status → pcb_report.json
```
Stesso pattern job/stage/findings della Placement sibling, **non** un secondo auto-placer.
### Riuso obbligatorio (non reinventare)
| Pezzo | Path | Ruolo nel job PCB |
| --- | --- | --- |
| Workspace / SSE broker | `backend/services/pipeline.py` (`PipelineWorkspace`, `broker`) | Download upload, `layout_graph.json`, eventi |
| Job dispatch | `backend/services/job_runner.py`, `backend/pipeline_worker.py` | Nuovo `MODE=pcb` accanto a `run` / `regen` / `placement` |
| API sibling | `backend/routers/pipeline.py` (`placement/start|cancel|events`) | `pcb/start`, `pcb/cancel`, `pcb/events` |
| Meta progetto | `backend/services/projects.py` (`placement_status*`) | `pcb_status`, `pcb_state`, `pcb_execution_name`, `pcb_cancel_requested` |
| Parser board | `backend/periscopex/parsers_kicad_pcb.py` | `LayoutGraph` (footprint, pad net, segmenti, via, zone, stackup) |
| Grafo schema | `design_graph.json` / `build_graph(..., pcb_path=)` | Pin↔net; `capacitors_on_net`; CAD index |
| Placement verify | `placement_check.py` | PE-PLC-* |
| SI verify | `si_check.py` | PE-SI-001 |
| BOM/schema | `bom_match_check.py` | Resta **schema**. Sul PCB: analogo **pad-net match** (nuovo check piccolo, stesso schema `Finding`) |
| Cad bridge | `cad_bridge.py` (`target: pcb` per `PE-PLC`/`PE-SI`/`PE-LAY`) | Plugin pcbnew focus |
| Validation helpers | `assign_finding_ids`, fail-soft per check, `annotate_findings_cad` | IDs + CAD |
| Impedenza tracce | `impedance_traces.py`, `routers/impedance.py`, `impedance_nets.json` | Inventario Z0; finding solo se target numerico esiste |
| UI | `finding-card`, report filters, `types.ts` / `api.ts`, `pcb-upload.tsx`, pagine/hooks placement come stampo | Tab Layout + findings nel report |
| Test | `tests/test_kicad_pcb.py`, `test_placement_check.py`, `test_si_check.py`, `test_placement_pipeline.py` | Fixtures + job smoke |
### Contratto job (come Placement)
- **Non** tocca `ProjectMeta.status` dellanalisi schema.
- Soft-cancel: flag `pcb_cancel_requested` (come `placement_cancel_requested`).
- Mutex: non partire se analisi `queued|running` **o** placement `queued|running` **o** pcb già attivo (levent log è condiviso e Placement fa `clear_history`).
- SSE: eventi `pcb_step_update` / `pcb_complete` / `pcb_error` / `pcb_cancelled`. Lo stream schema ignora `pcb_*` (come già ignora `placement_*`).
- Locale: `enqueue` con `proc_key=pcb:{id}`, `execution_name=local/pcb/{id}` (stesso trick di `local/placement/`).
- Heal zombie: `heal_if_pcb_stuck` speculare a `heal_if_placement_stuck` (evento terminale, o artefatto `pcb_report.json` se il worker è morto dopo la scrittura).
- Billing: job **free** (come placement). Nessun LLM in v1.
### Stages del job PCB (v1)
1. **`ensure_graph`** — riusa `design_graph.json`; se manca, `graph_build` da BOM+netlist (stesso helper Placement). Senza grafo: 400, non inventare connettività dal solo PCB.
2. **`parse_pcb`** — riusa `layout_graph.json` o `parse_kicad_pcb(uploads/pcb.kicad_pcb)` e upload. Fail → `pcb_status=error` (a differenza dello schema, qui il PCB è lingresso).
3. **`inventory`** — lista net con rame, lunghezze, coppie `_DP/_DM` `_P/_N`, bus omonimi, Z0 dove stackup+width ci sono. Artefatto `pcb_inventory.json` (leggibile in UI). Nessun finding da soli numeri.
4. **`checks`** — funzioni pure in `periscopex/` (fail-soft per check). Vedi §3.
5. **`write_report`** — `pcb_report.json` (`ValidationReport`), cad-bridge merge, IDs `PCB-{designator}-{001}` per non collidere con lo schema.
Senza datasheet / `layout_rules`: linventory c’è comunque; i finding datasheet-gated sono lista vuota, non errore.
---
## 2. Ingressi
| Ingresso | Dove | Obbligatorio v1 |
| --- | --- | --- |
| `.kicad_pcb` | `uploads/pcb.kicad_pcb`, `has_pcb` | Sì |
| Design graph | `design_graph.json` (da BOM+netlist / KiCad sch flatten) | Sì (per pin↔net e MPN) |
| Estrazioni IC | `extracted/` + library | Per `layout_rules` / pintable pin names |
| Datasheet PDF | library blobs | Solo per `source_page` in UI; i check v1 sono deterministici |
| Stackup nel PCB | `LayoutGraph.stackup` (εr, h, t, layer names) | Per Z0; skip se assente |
| `layout_rules` | JSON MPN, `KNOWN_KINDS` | Per PE-PLC / PE-SI; skip se mm/count null |
Non in v1: Gerber come ingresso, EasyEDA JSON, PADS dump come layout, zip gerarchico PCB (il board non è gerarchico).
Allineamento net: i nomi net del PCB devono combaciare con lo schema **dopo** flatten gerarchico (`U1.4` ↔ pad). Un mismatch è un finding (`PE-LAY-001`), non un crash.
---
## 3. Cosa elenca e cosa controlla questa stage
Due uscite distinte: **inventario** (sempre, se il parse riesce) e **findings** (solo regola + numero).
### 3.1 Inventario (report Layout, non finding)
Per ogni net con rame:
- nome, `NetType` dallo schema se noto;
- lunghezza mm (somma segmenti, come `si_check.net_length_mm`);
- layer usati, min/max width, n. via;
- coppia differenziale se suffisso noto;
- Z0 stimata se stackup+width (riuso `impedance_traces.analyze_where_needed` / `impedance_nets.json`);
- appartenenza a bus (prefisso comune: `D[0-7]`, `A[0-15]`, `USB_DP/DM`) — lunghezze affiancate, **senza** soglia inventata.
UI: tabella “Traces / buses” sulla pagina PCB o tab Layout. Serve a *vedere* il board prima dei finding.
### 3.2 Check deterministici (findings, stesso modello `Finding`)
Registrati in un unico `run_pcb_checks(graph, constraints_map, layout)` (nuovo wrapper sottile; **non** SDK LLM in `periscopex/`).
| ID | Source | Cosa | Gate |
| --- | --- | --- | --- |
| `PE-LAY-001` | nuovo `pcb_net_match` (specchio di `bom_match_check`) | Pad net ≠ pin net schema, stesso ref+pin | Pad ha `net` e lo schema ha `pin_net` |
| `PE-LAY-002` | stesso modulo | Ref in schema senza footprint PCB (o viceversa, skip `#PWR`) | Graph + footprints |
| `PE-PLC-001` | `placement_check` | Decoupling / crystal oltre `max_distance_mm` (path se esiste, senno euclidea) | regola + mm |
| `PE-PLC-002` | idem | Via termiche < `min_via_count` | courtyard + count |
| `PE-PLC-003` | idem | `same_layer: true` e cap sullaltro lato senza via sotto il pin | boolean regola + layer footprint |
| `PE-PLC-004` | idem | Keepout: net estraneo in courtyard | courtyard |
| `PE-SI-001` | `si_check` | Intra-pair skew > `length_match` mm | coppia nominale **e** mm datasheet |
| `PE-Z0-001` (fase 2) | da `impedance_traces` | Z0 fuori target | target in `layout_rules` **o** netclass KiCad esplicita; mai 50 Ω di default |
Campi finding (già sul modello): `rule_id`, `net`, `pins[]`, `source`, `cad_sheet` / `cad_uuid` se in `cad_index`, `status` ERROR/WARNING, `source_page` dalla regola.
Normalization: **downgrade-only** se un giorno si aggiunge un pass; v1 non alza severità.
### 3.3 Fuori dai finding (skip esplicito, log)
- 3W / crosstalk senza distanza datasheet o calcolatore.
- Creepage/clearance IEC 62368 senza V/mm nel datasheet.
- Isolation barrier / HV courtyard senza keepout HV.
- CPWG se il vendor ImpedanceFinder non lo dà.
- Length-match “USB spec” / 150 mil folklore.
- Confronto visivo application-circuit vs PCB.
Queste restano Wave G residue in `piano-implementazione.md`, non questo job.
---
## 4. UI
Gating: `hasPcb`. Senza PCB, nessun bottone “Run PCB review”, nessun finding layout nel report schema.
| Superficie | Comportamento |
| --- | --- |
| Project hub | Accanto a “Build placement plan”: **Run PCB review** (verify). Upload già `PcbUploadButton`. |
| `/project/[id]/pcb` | Stampo di `/placement`: stepper SSE (`ensure_graph``parse_pcb``inventory``checks``write_report`), cancel, errore, link al report. |
| Sidebar | Voce **Layout** (`/pcb` o `report?domain=layout`). Nested route = `forceDocument` come placement. |
| Report | `GET /report/{id}` **merge** `report.json` + `pcb_report.json`. Filtro URL `?domain=schema\|layout\|all`. Finding card: badge “Layout” + `rule_id` (`PE-PLC-001`) oltre “Automated check”. |
| Inventory | Tabella tracce/bus/Z0 sulla pagina PCB (dati `pcb_inventory.json` / `impedance_nets.json`). |
| Plugin | `periscope-findings.json` include finding `target: pcb` dopo il job PCB (cad-bridge già distingue prefissi). |
Commenti / review_states restano keyed by `finding_id` su `report.json` (o dict parallelo); il merge in GET non deve perdere commenti schema. IDs prefissati `PCB-` evitano collisioni `U3-001`.
Re-run schema **non** cancella `pcb_report.json`. Re-run PCB **non** tocca findings LLM.
---
## 5. Fasi
Ordine: non parallelizzare con auto-pack. Non mischiare changelog Core vs Layout.
### Fase 0 — Confine (doc + split)
- Questo documento = contratto.
- Togliere `placement_check` / `si_check` da `_run_deterministic_checks` della review schema (restano no-op senza layout; lo split evita PE-PLC nello schema).
- Eval harness schema (`eval_report.py`) continua a chiamarli con `layout=None` (no-op) **oppure** li sposta in eval PCB dedicato.
**Done when:** run schema senza PCB = zero `PE-PLC`/`PE-SI` nel report.
### Fase 1 — Job parallelo + inventory + check già esistenti (slice usabile)
- Meta `pcb_*`, `enqueue` `MODE=pcb`, router start/cancel/events, worker.
- Stages §1; artefatti `layout_graph.json`, `pcb_inventory.json`, `pcb_report.json`.
- Check: `pcb_net_match` + `check_placement` + `check_si`.
- UI: start, progress, merge findings nel report, filtro Layout, `rule_id` sulla card.
- Test: wrapper `run_pcb_checks` su fixture (decoupling 15 mm → `PE-PLC-001`; 1 mm → niente); pad net mismatch → `PE-LAY-001`; job busy helpers come `test_placement_pipeline.py`; pytest API start 400 senza PCB.
**Done when:** upload `.kicad_pcb` → start PCB → finding visibili nel report; schema invariato.
### Fase 2 — Inventario ricco (tracce, bus, Z0)
- Tabella lunghezze + coppie + bus.
- Z0 per net signal con stackup (già `impedance_traces`); finding `PE-Z0-001` **solo** con target numerico (regola o netclass).
- Collegare `impedance_nets.json` al job PCB (oggi è side-effect dello schema `graph_build`).
**Done when:** board con stackup mostra Z0 in UI; senza stackup, skip loggato, zero finding inventati.
### Fase 3 — Plugin + eval PCB
- Cad-bridge dopo `write_report`; click finding centra footprint in pcbnew (`plugins/kicad/periscope_plugin.py`).
- Golden keys su fixture (USB skew voluta → `PE-SI-001`; simple_project senza PCB → vuoto).
- Changelog Layout (voce distinta, non “un po di Periscope in più”).
### Fase 4 — Non fare (parcheggio)
- `placement_pack` / collisioni courtyard / export xy / muovere rame.
- Auto-place antenna in `.kicad_pcb`.
- 3W, creepage IEC, CPWG, isolation HV senza numero.
- LLM che “legge” il layout come review schema.
- Secondo ingresso Gerber/EasyEDA.
---
## 6. Fuori scope (esplicito)
- Auto-placement, packing mm, write-back pcbnew, “migliora il floorplan”.
- Autore di sbroglio / change width / length-tune.
- Simulazione SPICE / IBIS / EM / VSWR.
- Inventare millimetri, Ω, o V/mm.
- Plugin EasyEDA; parser JSON EasyEDA.
- Layout da `!PADS-POWERPCB`.
- Unire forzatamente job PCB nella run schema.
- Toccare ssh / keys / `sshd` (irrilevante a questo piano).
---
## 7. Come si misura il “done”
Fixture minima (non un USB inventato in produzione, ma un `.kicad_pcb` di test in `tests/`):
1. C di decoupling a 15 mm da VDD con regola 2 mm → `PE-PLC-001`.
2. Stesso C a 1 mm → nessun finding proximity.
3. Coppia `USB_DP`/`USB_DM` con skew > `length_match``PE-SI-001`.
4. Pad `U1.1` net `+3V3` vs schema `GND``PE-LAY-001`.
5. Progetto senza PCB: job PCB rifiutato; report schema pulito.
Niente LLM. Niente coordinate proposte.
---
## 8. Relazione col piano esistente
`piano-implementazione.md` Wave G (G0 ingest, G1 SI gated, G2 placement vs datasheet) è **questo** prodotto, riletto come job parallelo di **esame**.
La roadmap Placement F2 (pack v1/v2/export) **non** entra in queste fasi. `placement_check` resta verifica; `placement_pack` resta skeleton gated e non si estende qui.
Chats Cursor precedenti: nessuna decisione di “PCB error pipeline” come job; solo note RF (“auto-place rame nel PCB = dopo”). Questo piano è la decisione: **review**, non place.