app
AI Flow: Reverse Engineering
Public Made by Adomby adom
Photos of a PCB to a tagged KiCad copy: parts placed, copper and nets with their evidence, a schematic, a measured confidence score, bench tests and a video of the run.
main
Caleb
Publish 0.5.0
1984b40
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
{
"name": "reverse",
"description": "Reverse engineer a PCB from photos. The program measures (board outline, straightening, scale from hole pitches and part bodies, part bodies and packages, hole rows, every island of copper on both faces, vias, what each pad and via joins) and generates the KiCad copy with the photographed pours and tracks. Connections nobody can see are left unrouted, each with its evidence tag, and the layer count is stated from what is known: outside evidence when given, otherwise UNKNOWN with its test. Routing the hidden connections is an optional later stage. The AI names (part identities, markings, nets, functions). Every claim carries an evidence tag, and every stage records its interactive PCB or schematic in the flow window. `auto` runs every program-only step and stops when the AI's input is needed; `next` names the one next action.",
"steps": [
{
"name": "intake",
"done": [
"state.json",
"checks/prep.ok"
],
"do": "Run `init --photos <folder or photo...>` with every photo of the board, unsorted (close-up tiles, whole-board overviews, both faces, no labels), start the flow window (`serve` in the background, open its URL in a Hydrogen webview), then `prep`. On an unsorted set `prep` first runs `stitch`: it works out which photos show the same face, which are close-up tiles and which are overviews, which face is the top, and builds one mosaic per face at the close-ups' own resolution (photos/top.jpg, photos/bottom.jpg). Read stitch.json and look at stitch/*-sources.jpg (what each part of the mosaic came from), stitch/*-seams.jpg (the joins at full size) and stitch/*-overlap.jpg before going on: areas filled from the overview are low resolution, areas nothing covers are black, and both are listed. (`init --top <photo...> --bottom <photo...>` still starts a run from one photo per face.) Then `prep` finds the board outline in every photo (connectors hanging over an edge are left out of the fit), straightens each photo, rates its sharpness, finds the plated holes and rows of holes, finds part bodies on the top, and works out how the bottom photo maps onto the top. Look at evidence/outline-*.jpg and evidence/prep-*.jpg. Run `check prep`. If a photo is soft or a marking is unreadable, ask the human for another shot now."
},
{
"name": "scale",
"done": [
"scale.json",
"board/top.jpg"
],
"do": "Run `scale`: it solves pixels per mm from the hole-row pitches (the scale at which every row lands on a standard pitch) and lists the candidates. Confirm it with a body whose size a datasheet gives: `scale --body <prep body id>=<length>x<width> --source \"<datasheet, package drawing>\"` (the body ids are on evidence/prep-<top photo>.jpg). It then writes both faces into one board frame (board/top.jpg, board/bottom.jpg, gridded copies for sketching) and aligns the bottom to the top on their shared holes. A board with no rows of holes and four mounting holes at a spacing a document gives: `scale --holes <x>x<y> --board <x>x<y> --source \"...\"` frames each photo through its own four holes and takes the flip from what shows through both faces."
},
{
"name": "parts",
"done": [
"checks/parts.ok"
],
"do": "STEP 1: identify as many parts as possible. `detect parts` measures every body and hole row on both faces in board mm, matches a package by size, proposes a reference and rotation, and writes crops (evidence/parts/) and contact sheets. Chip capacitors and resistors from 0201 to 1206 are read body first (candidates with `method`: `body`, drawn as outlines on evidence/parts-map.jpg and parts-map-bottom.jpg): the place is the body's centre and the package is from the body's own size, so check the package against the crop; detect/parts.json `passives.leftOut` lists those left out as lying inside a bigger part, and `passives.levels` the colour levels each face was read at: measured on that photo's own chip bodies, or, when it shows too few, another board's numbers scaled to it, which misses more. Dark resistor bodies with pale ends are often not read body first: look for them among the other candidates. Every candidate carries a `confidence` from 0 to 1 and, where something counts against it, a `doubt`; those under 0.5 are small grey rings on the map. `parts init` turns the candidates rated 0.5 or more into parts.json and lists the rest in `unassigned`, most sure first: real parts hide there too, so read down that list. `parts snap` measures each part's place from its pads; a chip part changes size only to what its own body measures on the photo, or, where no body reads, to pads that fit far better, and its `sizeCheck` says which. Then read every crop: set the reference, value (`decode <marking>` reads resistor codes), marking, MPN, function, footprint where the guess is wrong, and the pin-1 mark for every part that fits one way; tag each field by how it is known. Dismiss each candidate that is not a part (silkscreen, logo, reflection) in parts.json `dismissed` with the reason. Run `check parts`."
},
{
"name": "blocks",
"done": [
"checks/blocks.ok"
],
"do": "STEP 2: identify the functional blocks. `blocks propose` groups parts that sit together; name each block, say what it does, move parts between blocks where the topology says so, and write blocks.json. Run `check blocks`."
},
{
"name": "copper",
"done": [
"checks/copper.ok"
],
"do": "STEP 3: identify zones and routing. `copper prep` writes gap maps, gridded board images and the vias read from the photos (detect/domes.json, detect/vias.json, each with a measured position and diameter). `copper trace` reads every island of copper on both faces from the photos: copper is lighter than laminate relative to its own neighbourhood, and a dark-line filter adds the thin gaps. Each island becomes a pour (a zone with its exact copper) or a track (a centre line at its measured width). Pads and vias are joined to the islands they touch, screw lugs name their pours, and through vias carry nets to unnamed islands. It writes copper.json, detect/copper-measured.json (groups, disagreements with the nets so far, and whether both photos can show a via's ring at all) and evidence/copper-islands-*.jpg. Then `connect measure` and `connect attribute` give every pour, track and via a net with graded evidence. They run here, before anything is built, because attribution writes nets into copper.json: run later, it makes the build, `check mimic` and the snapshot stale, and they have to be done again (`auto` and `next` do send you back by themselves). `connect attribute` starts nets.json empty when there is none. Look at the evidence images against the photos, and at the disagreements. Run `check copper`."
},
{
"name": "mimic",
"done": [
"checks/mimic.ok"
],
"do": "STEP 4: copy everything visible. `build` writes the KiCad project: KiCad library footprints at the measured positions, generated lug footprints, the outline with its corner radius, the traced pours filled with the copper the photo shows, tracks and vias, all at the rules set at `init`. It registers the project with the flow window. Run `check mimic --pcb <board>`: every part within 0.5 mm of where it was measured, every zone on the board and filled."
},
{
"name": "snapshot",
"done": [
"snapshots/mimic/manifest.json"
],
"do": "STEP 5a: save the project as copied, before anything inferred is added: `snapshot --name mimic --dir <kicad project folder>`. The snapshot records copper.json and parts.json by hash: when either changes, this step shows stale and later steps wait. Then `build`, `check mimic` and run the same snapshot command again; the old one is kept as mimic.old<n>."
},
{
"name": "connect",
"done": [
"checks/nets.ok",
"checks/layers.ok",
"checks/schematic.ok"
],
"do": "STEP 5b: state what is known of the connections nobody can see, and of the layer count, without inventing either. Add pin names from datasheets (tag DATASHEET, with the source) or from the circuit (tag INFERRED, with a test) to nets.json; `connect attribute` then runs again, and a net the copper contradicts becomes a finding with a test in its `contradiction`, never a silent retag. `layers state` writes layers.json: a count from outside evidence when you have it (`--count <n> --source \"<document>\"`, or `--count <n> --evidence <file> --kind edge-micrograph|cross-section|xray|ct`), otherwise UNKNOWN with test T-layers and the minimum the photos prove or suggest. The hidden connections stay unrouted: `build` leaves each as a ratsnest line in a net class named for its evidence, and lists them in unrouted.json. Run `check nets` and `check layers`. The schematic follows the nets: `build` and `schematic` write a root sheet with the hierarchy and one sheet per block from parts.json, blocks.json, nets.json and net-evidence.json (solid wire: the pad was seen joined to the net's copper; dashed: inferred; a ? marker: net not known). For every IC you identified, name its KiCad library symbol in parts.json `symbol` (\"Library:Name\", used only when its pin numbers are the footprint's pads) or its datasheet pin names in `pins`, and list pins a datasheet says are unused in nets.json `noConnect`. Then `check schematic`: KiCad's ERC with zero errors, KiCad's netlist equal to nets.json pin by pin, and the sheet rules (it needs a kicad-cli: ADOM_AIFLOW_KICAD_CLI). The sheets are drawn and checked again whenever parts.json, blocks.json or nets.json change. Optional, later, and never required by `next`: `connect route [--layers 4,6]` routes the hidden connections on the program's own router and records each stackup in layers.json. Its routes are one way to make the connections, not what is inside the board, and they prove no layer count; a count stated from outside evidence stays as stated."
},
{
"name": "review",
"done": [
"checks/review.ok"
],
"do": "STEP 6: review in a measured loop until the photo score reaches the target (default 90). Run `review pass`. It re-measures, computes a score from 0 to 100 per category, draws the audits still needed and answers CONTINUE, TARGET MET, STALLED or STOPPED. There are two overall figures. The photo score is the lowest of what photos can earn (identity, values of marked parts, blocks, placement, copper, vias, the joins a crop shows, the drawn layers against the photos): the target applies to it. The bench score is the lowest of what needs the real board or outside documents (values of unmarked parts, pins and connections no photo shows joined, the net of copper that reaches no pin, the layer count): it is reported beside the photo score with what would raise it, and the loop does not wait for it. No score is typed. On CONTINUE do what it names: have an auditor who did not produce this run judge every crop in audits/<category>/sheet.md and sheet-*.jpg with `audit verdict` (a sample counts only when complete, cannot_tell counts against it, and an ok on a planted false claim voids it), or fix the method where a measurement is the limit, then `review pass` again. A full pass that raises no photo figure is STALLED: it names each category below the target, what limits it and the one thing most likely to raise it. `confidence` prints every score's inputs. `check review` passes only on TARGET MET or an honestly stated stall."
},
{
"name": "tests",
"done": [
"checks/tests.ok"
],
"do": "STEP 7: `tests generate` writes a test for every INFERRED or UNKNOWN item (continuity for nets and pin maps, raking-light photos for markings, an edge view or X-ray for layers and planes, rail voltages for regulators), with the expected result and what changes if it differs. Edit what the generator could not know, then `check tests`."
},
{
"name": "report",
"done": [
"report.md",
"checks/report.ok"
],
"do": "Run `report` for the generated tables, write report.md around them (what the board is, the blocks, the layer statement and its evidence, both scores per category, copied versus inferred, the hidden connections left unrouted, the tests), with the computed confidence line word for word, and run `report` again: it refuses a report without that line or with a claim above the real numbers. `report --draft` writes a starting report.md from the tables when there is none. `auto` then builds the two minute run video (`video build`: reverse-flow/video/<project>-run.mp4 with captions and video.json; you may write reverse-flow/video-notes.json first, one line naming the board and one per main chip); a failed video build is reported and never holds the run up. Hand the KiCad project, the report and the video to the human. After that, optional and never waited for: `bench plan` writes ordered instructions for a person with the real board and a multimeter (reverse-flow/bench/plan.md), and `bench ingest --file <results.json>` records their answers as MEASURED, so the bench score moves by computation (docs/bench.md)."
}
]
}