app
Altium - the Altium Bridge
Public Made by Adomby adom
Adom Bridge for Altium Designer: a resident DelphiScript agent gives the AI eval, the 1,100-command process launcher, all 8 EDA surfaces with screenshots, compile/ERC, DRC, OutJob outputs, native SchLib/PcbLib authoring with STEP 3D bodies, and dialog handling by PID.
← Commit history
v0.1.17: layers_in_use - the board's real layer count, from primitive occupancy
10 files changed
+171−13
CHANGELOG.md+7@@ -1,5 +1,12 @@ # Changelog +## 0.1.17 — 2026-09-01+- **New verb `altium_layers_in_use`: how many layers the board ACTUALLY has.** Until now the only layer answer was "48", which is every copper layer Altium CAN address, not a property of the board. Altium's in-stack flag has no identifier proven on AD26 and a wrong guess halts the engine behind a modal, so the agent's new `layer_occupancy` counts primitives by layer in ONE pass (using only identifiers already shipped in this unit) and the bridge filters that tally to the copper layers. Verified live: LAN7800_test reports **1 copper layer in use (Top Layer)**, 7 layers carrying primitives, against 48 addressable.+- Read the result as a FLOOR: an inner plane carrying no primitives is invisible to a primitive count. The reply says so, and `copperLayersAddressable` is labelled as a capability rather than a board property.+- `read_board` gains the `occupancy` section (default on).+- The first cut of this view forgot to filter to copper and cheerfully reported a MECHANICAL layer (296 primitives) as the board's busiest layer. Now covered by a unit test against that exact shape.+- 96 verbs.+ ## Unreleased — bridge-rs only (no change to the shipped Python runtime) - **`check_dialogs` no longer sees other applications' windows.** The first cut matched window TITLES containing "Error"/"Warning" and picked up four *"RemoteApp security warning"* windows on John's desktop that had nothing to do with Altium — which `dismiss_dialogs` would then have clicked OK on. Pressing buttons in someone else's application is exactly what this bridge must never do. Now scoped by PID: `process_list` for Altium's pids, `desktop_enum_windows {pids}` for only those windows, then class + "not the main window". Also 300× faster (one enumeration instead of six title searches): **1 ms**. - Note for the Python bridge: it was never affected — `handlers/dialogs.py` has always attributed dialogs by pid.
adom-bridge-altium-manifest.json+4−4@@ -1,9 +1,9 @@ { "name": "altium",- "version": "0.1.16",- "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.1.16/adom-bridge-altium-v0.1.16.zip",- "sha256": "894e07d34e0182a0c730fd79f1d9b74b1836d1514f433a722d442e3fd3447deb",- "size": 105131,+ "version": "0.1.17",+ "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.1.17/adom-bridge-altium-v0.1.17.zip",+ "sha256": "b732f4be98cb86443f9665c0ad2329e59b8609296e35f79c68b705abedc5c758",+ "size": 106316, "verbPrefixes": [ "altium_" ],
adom-bridge-altium-v0.1.17.zipadded⋯ 1 unchanged line ⋯
bridge-rs/src/board.rs+57@@ -99,6 +99,35 @@ impl BoardData { json!(kinds) } + /// Every layer carrying a pad/track/arc, densest first — copper AND silkscreen AND mechanical.+ /// The agent tallies primitives by layer in one pass; this is the raw occupancy.+ pub fn layers_with_primitives(&self) -> Vec<Value> {+ let mut v = self.array("occupancy", "occupancy");+ v.sort_by_key(|l| std::cmp::Reverse(l.get("primitives").and_then(|p| p.as_i64()).unwrap_or(0)));+ v+ }++ /// The layer ids Altium reports as COPPER (top + mid + internal planes + bottom). Taken from+ /// the dump's own `copper` section rather than assumed from id ranges — the ranges are an+ /// implementation detail of the enum and not something to hardcode.+ fn copper_layer_ids(&self) -> std::collections::BTreeSet<i64> {+ self.copper_layers().iter().filter_map(|l| l.get("id").and_then(|i| i.as_i64())).collect()+ }++ /// COPPER layers that actually carry copper, densest first — the honest answer to "how many+ /// layers is this board".+ ///+ /// Two caveats the caller must not lose: an inner plane with no primitives is invisible here+ /// (so this is a floor, not the stack), and the first cut of this view forgot to filter by+ /// copper at all and cheerfully reported silkscreen and mechanical layers as board layers.+ pub fn copper_layers_in_use(&self) -> Vec<Value> {+ let copper = self.copper_layer_ids();+ self.layers_with_primitives()+ .into_iter()+ .filter(|l| l.get("layerId").and_then(|i| i.as_i64()).map(|i| copper.contains(&i)).unwrap_or(false))+ .collect()+ }+ /// One-screen summary: what is on this board. pub fn summary(&self) -> Value { let comps = self.components();@@ -117,6 +146,12 @@ impl BoardData { // 16 planes + bottom), not the stack actually in use — there is no "in stack" flag in // the read yet, so this is a capability count, not a layer count. Do not report it to // a user as "this board has N layers".+ // layers actually carrying copper (a floor — an empty inner plane is invisible here);+ // `copperLayersAddressable` is what Altium CAN address, which is not a board property+ "copperLayersInUse": self.copper_layers_in_use().len(),+ "copperLayersInUseNames": self.copper_layers_in_use().iter()+ .filter_map(|l| l.get("name").and_then(|n| n.as_str()).map(str::to_string)).collect::<Vec<_>>(),+ "layersWithPrimitives": self.layers_with_primitives().len(), "copperLayersAddressable": self.copper_layers().len(), "rules": rules.len(), "rulesEnabled": rules.iter().filter(|r| r.get("enabled").and_then(|b| b.as_bool()) != Some(false)).count(),@@ -219,6 +254,28 @@ mod tests { assert!(by.get("Clearance").is_some()); } + #[test]+ fn copper_layers_in_use_excludes_silkscreen_and_mechanical() {+ // measured on a live board: the raw tally is dominated by a MECHANICAL layer+ // (Adom.SolderBalls, 296 prims) — reporting that as a board layer would be wrong+ let d = BoardData::new(json!({+ "copper": {"ok": true, "layers": [{"id": 1, "name": "Top Layer"}, {"id": 32, "name": "Bottom Layer"}]},+ "occupancy": {"ok": true, "occupancy": [+ {"layerId": 65, "name": "Adom.SolderBalls", "primitives": 296},+ {"layerId": 1, "name": "Top Layer", "primitives": 51},+ {"layerId": 33, "name": "Top Overlay", "primitives": 9},+ {"layerId": 32, "name": "Bottom Layer", "primitives": 4}+ ]}+ }));+ assert_eq!(d.layers_with_primitives().len(), 4, "raw tally keeps everything");+ let cu = d.copper_layers_in_use();+ assert_eq!(cu.len(), 2, "only copper survives");+ assert_eq!(cu[0].get("name").unwrap(), "Top Layer", "densest copper first");+ let s = d.summary();+ assert_eq!(s.get("copperLayersInUse").and_then(|v| v.as_u64()), Some(2));+ assert_eq!(s.get("layersWithPrimitives").and_then(|v| v.as_u64()), Some(4));+ }+ #[test] fn an_unknown_ordinal_is_labelled_not_invented() { assert_eq!(rule_kind_name(0), "Clearance");
bridge-rs/src/verbs.rs+8−3@@ -10,7 +10,7 @@ const KNOWN: &[&str] = &[ "list_designators", "component_data", "component_pins", "list_nets", "net_connections", "layers", "copper_layers", "read_rules", "set_layer_visibility", "footprint_primitives", "schematic_data", "agent_start", "agent_stop", "agent_recover", "launch", "close", "check_dialogs", "dismiss_dialogs",- "read_board", "board_summary", "rules_by_kind",+ "read_board", "board_summary", "rules_by_kind", "layers_in_use", "describe", "uninstall", ]; @@ -216,9 +216,9 @@ pub fn dispatch(command: &str, args: &Value, _forwarded: &[(String, String)]) -> dialogs::dismiss(&button, titles.as_deref()) } // ---- board-read primitive: ONE agent round trip, views shaped here ----- "read_board" | "board_summary" | "rules_by_kind" => {+ "read_board" | "board_summary" | "rules_by_kind" | "layers_in_use" => { if let Some(g) = need_licensed() { return g; }- let sections = s(args, "sections").unwrap_or_else(|| "components,nets,layers,copper,rules".into());+ let sections = s(args, "sections").unwrap_or_else(|| "components,nets,layers,copper,rules,occupancy".into()); let r = agent::call("read_board", &[("sections", sections), ("limit", s(args, "limit").unwrap_or("2000".into()))], None, Duration::from_secs(timeout.max(120))); let data = r.get("data").cloned().unwrap_or(Value::Null);@@ -237,6 +237,11 @@ pub fn dispatch(command: &str, args: &Value, _forwarded: &[(String, String)]) -> "_hint": "One agent round trip; every view below is derived in the bridge."}), "rules_by_kind" => json!({"success": true, "board": d.board_path(), "rulesByKind": d.rules_by_kind(), "roundtripMs": r.get("roundtripMs")}),+ "layers_in_use" => json!({"success": true, "board": d.board_path(),+ "copperLayersInUse": d.copper_layers_in_use(),+ "layersWithPrimitives": d.layers_with_primitives(),+ "roundtripMs": r.get("roundtripMs"),+ "_hint": "copperLayersInUse = COPPER layers carrying copper, densest first — a floor, not the stack (an empty inner plane is invisible). layersWithPrimitives additionally counts silkscreen and mechanical."}), _ => json!({"success": true, "board": d.board_path(), "summary": d.summary(), "designators": d.designators(), "nets": d.nets(), "layers": d.layers(), "copperLayers": d.copper_layers(), "rules": d.rules(),
bridge.json+2−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "altium", "displayName": "Altium Designer",- "version": "0.1.16",+ "version": "0.1.17", "description": "Drive Altium Designer from Adom: a resident DelphiScript agent (eval + the 1,100-command process launcher), the X2 -R command line, UIA over the VCL shell, dialogs by PID, screenshots of every Altium window, and Altium 365.", "homepage": "https://wiki.adom.inc/adom/altium-bridge", "docs": "https://wiki.adom.inc/adom/altium-bridge",@@ -67,6 +67,7 @@ "altium_install_library_bundle", "altium_launch", "altium_layers",+ "altium_layers_in_use", "altium_license", "altium_list_commands", "altium_list_designators",
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "altium-bridge",- "version": "0.1.16",+ "version": "0.1.17", "type": "app", "description": "Skills for your container so your AI knows how to drive the Altium Designer bridge (altium_* verbs). The bridge runtime itself is the release zip; Adom Bridge loads that.", "tags": [
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-0.1.16+0.1.17
src/agent/AdomAgent.pas.tmpl+54−2@@ -3605,6 +3605,56 @@ Begin + '}'; End; +{ Which copper layers actually CARRY copper. The layer stack's "is this layer in the stack" flag has+ no identifier we have proven on this Altium build, and guessing one halts the engine behind a modal+ on the user's screen — so instead of asking the stack, count primitives. ONE pass over pads, tracks+ and arcs across all layers, tallied by Prim.Layer; every identifier here is already used elsewhere+ in this unit. An empty inner plane reports zero, which is why the caller must read this as+ "layers with copper on them", never as "the board's layer count". }+Function CmdLayerOccupancy(Lines : TStringList) : String;+Var+ Board : IPCB_Board;+ BIt : IPCB_BoardIterator;+ Prim : IPCB_Primitive;+ SL : TStringList;+ S, K : String;+ I, C, N : Integer;+Begin+ Board := PCBServer.GetCurrentPCBBoard;+ If Board = Nil Then+ Begin+ Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}';+ Exit;+ End;+ SL := TStringList.Create;+ N := 0;+ BIt := Board.BoardIterator_Create;+ BIt.AddFilter_ObjectSet(MkSet(ePadObject, eTrackObject, eArcObject));+ BIt.AddFilter_LayerSet(AllLayers);+ BIt.AddFilter_Method(eProcessAll);+ Prim := BIt.FirstPCBObject;+ While Prim <> Nil Do+ Begin+ K := IntToStr(Prim.Layer);+ C := StrToIntDef(SL.Values[K], 0);+ SL.Values[K] := IntToStr(C + 1);+ N := N + 1;+ Prim := BIt.NextPCBObject;+ End;+ Board.BoardIterator_Destroy(BIt);+ S := '';+ For I := 0 To SL.Count - 1 Do+ Begin+ If S <> '' Then S := S + ',';+ K := SL.Names[I];+ S := S + '{' + JRaw('layerId', K) + ',' ++ JStr('name', Board.LayerStack.LayerObject[StrToIntDef(K, 1)].Name) + ',' ++ JRaw('primitives', SL.Values[K]) + '}';+ End;+ SL.Free;+ Result := '{' + JRaw('ok','true') + ',' + JRaw('primitives', IntToStr(N)) + ',' + JRaw('occupancy', '[' + S + ']') + '}';+End;+ { ONE round trip to READ the whole board. Composed from the EXISTING readers on purpose: no new Altium API surface, so it cannot introduce an undeclared identifier (which halts the engine behind a modal). The bridge shapes these into typed views; recorded dumps are the unit-test fixtures. }@@ -3620,13 +3670,14 @@ Begin Exit; End; Sects := Param(Lines, 'sections');- If Sects = '' Then Sects := 'components,nets,layers,copper,rules';+ If Sects = '' Then Sects := 'components,nets,layers,copper,rules,occupancy'; S := ''; If Pos('components', Sects) > 0 Then S := S + ',' + JRaw('components', CmdComponentData(Lines)); If Pos('nets', Sects) > 0 Then S := S + ',' + JRaw('nets', CmdListNets(Lines)); If Pos('layers', Sects) > 0 Then S := S + ',' + JRaw('layers', CmdLayers(Lines)); If Pos('copper', Sects) > 0 Then S := S + ',' + JRaw('copper', CmdCopperLayers(Lines)); If Pos('rules', Sects) > 0 Then S := S + ',' + JRaw('rules', CmdReadRules(Lines));+ If Pos('occupancy', Sects) > 0 Then S := S + ',' + JRaw('occupancy', CmdLayerOccupancy(Lines)); Result := '{' + JRaw('ok','true') + ',' + JStr('board', Board.FileName) + ',' + JStr('sections', Sects) + S + '}'; End; @@ -3639,6 +3690,7 @@ Begin Else If Cmd = 'eval' Then Result := CmdEval(Lines, Payload) Else If Cmd = 'io_bench' Then Result := CmdIoBench(Lines) Else If Cmd = 'read_board' Then Result := CmdReadBoard(Lines)+ Else If Cmd = 'layer_occupancy' Then Result := CmdLayerOccupancy(Lines) Else If Cmd = 'lib_list' Then Result := CmdLibList(Lines) Else If Cmd = 'lib_select' Then Result := CmdLibSelect(Lines) Else If Cmd = 'compile' Then Result := CmdCompile(Lines)@@ -3689,7 +3741,7 @@ Begin gRunning := False; Result := '{' + JRaw('ok', 'true') + ',' + JStr('note', 'agent loop stopping') + '}'; End- Else Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'unknown command: ' + Cmd) + ',' + JStr('known', 'ping,state,run_process,open,eval,lib_list,lib_select,compile,violations,outjob,create_footprint,create_symbol,install_library,compile_intlib,extract_intlib,set_params,add_body,stop,list_designators,component_data,component_pins,list_nets,layers,net_connections,schematic_data,copper_layers,read_rules,set_layer_visibility,footprint_primitives,io_bench,read_board') + '}';+ Else Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'unknown command: ' + Cmd) + ',' + JStr('known', 'ping,state,run_process,open,eval,lib_list,lib_select,compile,violations,outjob,create_footprint,create_symbol,install_library,compile_intlib,extract_intlib,set_params,add_body,stop,list_designators,component_data,component_pins,list_nets,layers,net_connections,schematic_data,copper_layers,read_rules,set_layer_visibility,footprint_primitives,io_bench,read_board,layer_occupancy') + '}'; End; { ------------------------------------------------------------------ loop --- }
src/server.py+37−1@@ -1077,6 +1077,41 @@ def h_activate_document(args): return r ++def h_layers_in_use(args):+ """How many layers does this board actually have? — answered from primitives, not the stack.++ Altium's "is this layer in the stack" flag has no identifier proven on AD26, and a wrong guess+ halts the script engine behind a modal on the user's screen. So the agent tallies primitives by+ layer in ONE pass and this filters that tally to the layers Altium reports as copper.++ Read it as a FLOOR: an inner plane carrying no primitives is invisible here. `copperLayersAddressable`+ (48) is what Altium can address — never report that as a board property.++ NOTE: the same shaping exists in bridge-rs (`board.rs`). That duplication is deliberate and+ temporary — it goes away when the Rust bridge reaches parity and this runtime retires.+ """+ g = _need_licensed()+ if g:+ return g+ r = agent.call("read_board", {"sections": "copper,occupancy"}, timeout=float(args.get("timeout", 90)))+ data = r.get("data") or {}+ if not data.get("ok"):+ return r+ copper_ids = {l.get("id") for l in ((data.get("copper") or {}).get("layers") or [])}+ occupancy = sorted(((data.get("occupancy") or {}).get("occupancy") or []),+ key=lambda l: -(l.get("primitives") or 0))+ copper = [l for l in occupancy if l.get("layerId") in copper_ids]+ return {"success": True, "board": data.get("board"),+ "copperLayersInUse": copper,+ "copperLayerCount": len(copper),+ "layersWithPrimitives": occupancy,+ "roundtripMs": r.get("roundtripMs"),+ "_hint": "copperLayersInUse = COPPER layers carrying copper, densest first. A floor, not the "+ "stack: an empty inner plane is invisible. layersWithPrimitives also counts "+ "silkscreen and mechanical layers."}++ # ---- licence lifecycle: status, ensure-licensed, trial setup (John 2026-08-31) ------------ def h_license_status(args):@@ -1643,6 +1678,7 @@ _VERB_CATALOG = { "launch": "Start X2.exe and wait for the main window (~60s cold).", "close": "WM_CLOSE the main window; reports a save prompt instead of answering it; force:true taskkills.", "activate_document": "Make an ALREADY-OPEN document current by invoking its document-bar tab through UIA — no cursor, no foreground.",+ "layers_in_use": "How many layers the board ACTUALLY has: copper layers carrying primitives, densest first (a floor, not the stack).", "agent_start": "Install + bootstrap the resident DelphiScript agent via X2 -R RunScript; waits for heartbeat.", "agent_stop": "Drop the stop file; loop exits.", "agent_status": "Heartbeat age, request count, workspace.",@@ -1697,7 +1733,7 @@ COMMAND_HANDLERS = { "readiness": h_readiness, "status": h_status, "diagnostics": h_diagnostics, "window_info": h_window_info, "state": h_state, "check_dialogs": h_check_dialogs, "dismiss_dialogs": h_dismiss_dialogs, "launch": h_launch, "close": h_close,- "activate_document": h_activate_document,+ "activate_document": h_activate_document, "layers_in_use": h_layers_in_use, "agent_start": h_agent_start, "agent_stop": h_agent_stop, "agent_status": h_agent_status, "agent_recover": h_agent_recover, "eval": h_eval, "run_command": h_run_command, "run_script": h_run_script,