Pular para o conteúdo principal
N

dsh-breadboard

noob-stupid/dsh-breadboard

DSH hardware sandbox plugin: assemble a 3D scene of ESP32 / BME280 / PIR / LED components with protocol- and voltage-checked port wiring, run real Python project code in the loop against simulated I2C/GPIO hardware with fault injection, let networked device simulators report over HTTP to a real backend (validated end-to-end against a live seat-reservation system), observe a real device read-only and map its state onto the scene, and add support for new boards through data-driven pin definitions. Ships 16 hw_* agent tools plus a bundled SKILL.md.

Instalar

dsh plugin --profile web add github:noob-stupid/dsh-breadboard

README

Web profile plugin — host half + browser half.

English: README.md | 中文: README.zh.md


DSH Hardware Sandbox

A 3D hardware sandbox for DSH: build the assembly, wire it up, and run your project's own code against virtual hardware.

Install (from GitHub; this package is not published on npm)

dsh plugin --profile web add github:Noob-stupid/dsh-breadboard

Then reload the page. The sandbox registers into the main slot (the 3D view) and the sidebar.panellist slot (the panels); skill/SKILL.md ships with the plugin, so an agent that has it installed already knows how to drive it.

Like it? Open this plugin's page in the Plugins manager — its own bottom-right corner carries a small, plain-text link to give it a star on GitHub. It is one transparent line, no box, no banner, and it only ever appears on this plugin's page.

Three things make that one line work — all three are in this repository:

RequirementWhereNote
repo topic dsh-pluginGitHub repository topicsmakes the repo discoverable in community lists
a dsh.bundle manifestpackage.json → "dsh": { "bundle": { "patch": "./cordis.patch.yml" } }without it dsh plugin add installs nothing — and the UI symptom is misleading: the plugin list says "no composition bundle declared", while what actually broke is the assembly, so every /api/* returns 404 and the panel simply never appears
an exported apply(ctx)src/index.ts (host) / src/client/index.ts (browser)host half registers routes, tools and the simulation engine; browser half registers the two slots above

What it is / what it solves

Normally the parts of a hardware project you can only try on the real board are the wiring itself, the sensor semantics at register level, and the failure handling. This plugin moves those into a virtual scene that actually runs:

  • 3D assembly — drop parts from the built-in model library (ESP32, Raspberry Pi 4B, BME280, HC-SR501 PIR, LED module, MPL3115A2, breadboard), wire them port by port, and get a live SSOT (single source of truth) assembly state with topology and electrical checks.
  • Project-level hardware-in-the-loop — a real Python process runs your project's code. A Python shim intercepts its smbus/I2C access and answers from virtual devices, so the project under test reads register-level values (e.g. it performs the real BME280 compensation math on data coming from the virtual chip).
  • Networked-device emulation (a second, separate class of virtual hardware) — a device that actively POSTs to your external HTTP backend on a real cadence: register → pull config → periodic report. Verified end-to-end against a real seat-selection system (device → HTTP → backend state machine → database).
  • Failure injection — unplug a wire, return NACK, force a reading, take a device offline, and watch whether the project's error handling actually catches it.
  • Project-level convenience — scan a real project folder to learn which hardware it needs, and switch the entire hardware setup in one click.

Requires DSH with the web profile and webServer, tools and subprocess services. The host half declares them as hard inject dependencies on purpose: if any of them is missing the plugin does not activate at all, rather than activating with tools silently unregistered, a blank panel, or a simulation engine that cannot spawn.

See it without a real project

examples/zhizuo-virtual-esp32/ is a standalone script version of the virtual ESP32 seat sensor. hw_list_projects lists the built-in demo project (zhizuo-seat-node) together with whether it is currently wired end to end; hw_import_project puts its parts, wires and network binding in place in one call.

UI tour

The interface is currently Chinese only — the labels below are the literal ones on screen.

PanelWhat it does
③ Project (one-click hardware swap)the main workflow. Per project body: file count (N files (M skipped while sampling) when sampling skipped files), Scan requirements, Delete; plus + Import project body (pick folder) and + Import model bundle (pick folder)
① Add componentclick a model key to drop it into the scene (collapsed by default)
② Re-skin modelssearch for a model / import a file, and re-skin one of the model keys above; a model key with no geometry renders as a placeholder box (collapsed by default)
Statisticsrender statistics (draw calls / triangles / FPS) — pure read, off by default
Center 3D viewplaced components with world transforms, blue cables between ports, spatial warning boxes drawn on the components that have warn/error warnings, gizmo/right-click menu (pin, delete), port tooltips
Panel overlay input boxsend a message to the current session without leaving the panel; if the host exposes no agents service the box is hidden (capabilities.chat is absent)

Two of those deserve emphasis:

  • Scan requirements is a UI button only. It is not exposed as an agent tool — the agent side has no equivalent. What the agent can do is everything after that: place parts, wire them, build a project blueprint, run simulations.
  • Deleting a project only deletes the project definition (that one JSON). It does not touch the scene, so removing a project never wipes the hardware you have on screen.

Quick start: three steps

  1. Import the project body — + Import project body (pick folder), point it at a complete project folder (frontend + backend + hardware code). It is imported without models; the host reads it to work out which hardware it needs.
  2. Scan what hardware it needs — the Scan requirements button in the ③ panel (UI only, see above). The report lists each candidate model key with the evidence — which files it was seen in — so you can judge it yourself; when the scan had to sample, it says so instead of implying it read everything.
  3. Import a model bundle — + Import model bundle (pick folder), a folder laid out as <modelKey>/model.glb. Whatever you skip keeps rendering as a placeholder box.

Or skip steps 1–3 entirely: describe the system to the agent and let it call hw_save_project to write a blueprint, then hw_import_project to place every part, wire and network binding in one go.

Agent tools: 16 hw_* tools

This is the actual registered list (nothing added, nothing omitted). The when to use column is the point of the table: these tools are not interchangeable.

Projects

ToolWhat it doesWhen to use it
hw_list_projectslist built-in hardware projects: parts, wires, network config, and whether each is currently wired end to endbefore hand-placing anything — a blueprint is one call, hand-assembly is four steps and a missed step fails silently
hw_import_projectimport a project: clears the current assembly first, then places parts, wires and network bindingsswitching the whole hardware set; note it replaces, it does not add
hw_save_projectcreate or overwrite a project blueprint (stored under $DSH_HOME/dsh-hardware-sandbox/projects/)the user described a system in words — write it down as a blueprint, then import it

Assembly

ToolWhat it doesWhen to use it
hw_get_assemblyread the SSOT summary: revision, components, connections, warnings (with their message, not just the code), power summary, virtual devicesbefore every edit — you need the real port ids; port ids are pin identities (E4), not silkscreen names (GPIO23)
hw_edit_assemblyone tool over the contract's ClientAction union: place / move / rotate / connect / disconnect / remove / pinany change to the scene. It takes an action object, so read the samples in the tool description first

Simulation

ToolWhat it doesWhen to use it
hw_run_simulationrun a real Python project on virtual hardware; duration is virtual seconds; returns project output plus virtual/wall elapsed timethe actual hardware-in-the-loop run. accelerated (default) finishes far faster than real time; realtime paces to the wall clock for observation
hw_get_sim_logread the last (or in-progress) run's project output, with an optional tailwhen hw_run_simulation's output was truncated
hw_inject_faultinject disconnect / nack / busy / set_reading / clear against a device id or a model:address targetverifying that the project's error handling really observes the fault (e.g. an OSError after a disconnect) instead of assuming it does

Networked devices

ToolWhat it doesWhen to use it
hw_network_addadd a networked device (protocol adapter + endpoint + self-reported device id). It does not startwiring a device emulator into your backend for the first time
hw_network_deviceslist devices: protocol, endpoint, online state, report/failure counts, current readings, server-pushed configconfirming the device is actually connected and actually reporting — an added-but-idle device looks "fine" otherwise
hw_network_controlstart / stop / disconnect (offline fault) / clearonly start makes requests go out — intentional, so nothing has external side effects by default; disconnect tests whether the far side notices
hw_network_set_readingset a physical reading (e.g. { ir_front: true, ir_back: true }, true = occluded)testing whether the protocol layer is right, independently of the hardware path. The protocol translates the reading into a level (ir_active_high etc.), so polarity mistakes stay on the protocol side where they belong
hw_set_occlusionocclude / un-occlude a placed part and propagate the effect along the real wires to a networked device's readingstesting the hardware path: if the PIR is not wired, or wired to the wrong pin, nothing happens — exactly like the real board

Reason codes and what to do about them

Failed assembly operations come back with a reason code (the contract's WarningCode, 12 of them, in src/contracts/assembly.ts).

Read the code. Change that thing. Do not guess-and-retry.

CodeMeaningWhat to do
bad_paramsthe action's parameters are unusable (e.g. a rotation angle of NaN / Infinity)fix the numbers — the problem is the value, not the component id
direction_conflicttwo current directions fight (output to output / two supplies tied together)rearrange per current direction: at least one end must become an input
protocol_mismatchthe two ports carry different signal kindsuse a matching port pair; check which signal that pin actually supports
port_occupieda different cable already owns that portunplug the other cable (disconnect with its cableId), or use another pin
voltage_mismatchport voltages are not compatibleconnect voltage-compatible ports, or add level shifting
power_exceededthe assembly's power demand exceeds its supply budgetadd supply or reduce draw
i2c_address_conflicttwo devices on the bus share an I2C addresschange one device's address (SDO/AD0 pin)
i2c_pullup_missingthe I2C bus has no pull-upsadd pull-ups on SDA/SCL
unconnected_porta port has no connection at allconnect it as needed — and check you did not forget the other wire of a pair
unknown_hardware_modelno such model keycheck the key against hw_get_assembly's existing components
component_pinnedthe component is pinned; the move was explicitly refusedunpin it first (right-click menu, or set_pinned: false)
already_connectedthat very pair of ports is already connecteddo not go looking for a cable that does not exist; to re-route, disconnect this one first

Two notes worth keeping:

  • port_occupied and already_connected are separate on purpose. Merged into one code, the user goes hunting for a cable that does not exist.
  • Multiple different ports connecting to the same port is legal (that is an I2C bus) and does not raise already_connected.
  • ok: true does not mean the effect happened. Every write should be followed by reading the assembly again.

Model definitions: teaching the plugin a new board

Appearance and pin data are two different things. Model libraries (step.parts, GrabCAD, SnapEDA…) provide appearance — the .glb. They do not provide pin data: even SnapEDA's symbol library explicitly carries "no bus protocol or voltage", and that semantics is exactly what the validators check.

{
  "key": "stm32f103c8t6",                              // identity; cannot shadow a built-in
  "label": "STM32F103C8T6 (Blue Pill)",
  "size": { "x": 0.0533, "y": 0.011, "z": 0.0229 },    // metres
  "ports": [
    { "portId": "P1", "name": "VBAT", "protocol": "power", "voltage": 3.3, "direction": "in" }
  ],
  "portLayout": { "edge": "+z", "pitch": 0.00254, "count": 40, "inset": 0.001, "rows": 2 }
}

Install it from the panel (+ 添加型号定义) or have an agent write it (hw_save_model_definition). Validation happens before anything is written, and a valid definition takes effect immediately without reloading the plugin.

Start from an authoritative source, not from a pinout image. hw_scan_board_pins reads the chain PlatformIO itself uses — boards/<board>.json → build.variant → variants/<variant>/pins_arduino.h, the file the compiler translates SDA into GPIO21 with — and returns a draft with its provenance. It returns a draft rather than a finished definition on purpose, because that file cannot tell you which pin sits where on the header, which pin is 5V or needs a pull-up, or what is printed on the board. Those gaps come back with every draft, and an agent that cannot source them is told to ask rather than invent them.

portId is identity, name is the silkscreen. A board has several GND pins; naming ports after their function collides silently. voltage: 0 is valid — that is a ground pin, not a missing field.

The rule that matters: a pin table must come from an authoritative source — the vendor datasheet, PlatformIO's variants/<board>/pins_arduino.h (the mapping the compiler actually uses), or the project's own files (platformio.ini, .ioc, sdkconfig). A hand-typed pin table is worse than none, because it yields validation that looks right and connects the wrong pin, and nothing can detect it. An agent that cannot find a source is told to stop and ask rather than invent a plausible pin order.

Real-device observation: reading a board into the scene

hw_watch_device polls an external system (read-only) and maps a real device onto a scene component. It is the mirror image of hw_network_add: that one impersonates a device and sends requests, this one asks about a real board and sends nothing.

Five states are kept apart, because collapsing them produces plausible but wrong conclusions:

statemeaningwhere to look
okseen, data fresh—
staleseen, but the data is oldthe board really is offline — check power/network
errorwe cannot reach the systemthe URL and the credentials, not the hardware
not_foundthe system answers but has no such devicethe device id, or it has never been online
idlenot polled yet—

Polling runs on wall-clock time with a two-second floor, which protects the remote server. Only the host polls; the panel reads the host's cache.

Model bundle format

The folder you hand to + Import model bundle (and the layout the exporter round-trips through) is:

<modelKey>/
  model.glb      the geometry
  meta.json      optional metadata
  • Only GLB / GLTF are loaded for rendering. The importer's format detection is content-based (not extension-based) and accepts glb / gltf / stl / obj / ply / 3mf / dae / fbx / wrl, but STEP / IGES is refused outright rather than silently stored — three.js has no loader for it, so a stored STEP would never render.
  • Internally each stored model is one directory <modelKey>/{model.bin, meta.json}, with model.bin holding the geometry bytes (meta.json records format, nodes, bytes, generator). Storing the bytes with a neutral name is deliberate: it keeps the layout free of extension ambiguity and keeps meta bound to its bytes. Exported bundles use the <modelKey>/model.glb form above, so export and import are the same layout.
  • Shipped with the package (seeded into the user model directory at startup, only keys that are missing — a model you imported is never overwritten):
modelKeyOriginIn the bundleWhy
bme280self-producedyesregister-level device model + demo assembly
led-5vself-producedyessimplest possible output part
hc-sr501self-producedyesthe demo assembly uses 2 of them (PIR)
esp32-seat-sensorself-producedyesthe networked-device demo
rpi-4bimported from step.parts (third party)nothird-party geometry — not redistributed by this plugin; import it yourself from the UI when you need it
mpl3115a2imported from step.parts (third party)nosame
breadboardself-produced (no ports, passive)no geometryrenders as the procedural placeholder box (sized from the contract)

Why the split: the four self-produced models are ours to ship, so the plugin is usable out of the box (the demo assembly is esp32-seat-sensor + 2 × hc-sr501, all shipped). The third-party models are somebody else's assets with their own terms, so the plugin does not redistribute them — the UI gives you search entry points and you import the file yourself. Port semantics (I2C address, protocol, voltage, direction) are always produced here, because no model library sells them — see docs/05.

Wall-clock trap: why networked devices ignore virtual time

The virtual clock is accelerating: advance(60) burns 60 virtual seconds in tens of milliseconds. A passive bus device is fine on it (deterministic, purely synchronous). A networked device is not — it reports periodically on its own initiative, so a single fast-forward would fire dozens of real HTTP requests at somebody's backend and hammer it.

So networked devices always run on the wall clock, decoupled from virtual time; their snapshot carries wallClockDriven: true, and accelerated mode means nothing for them. Two consequences:

  • Accelerated mode does not apply to networked devices. They keep reporting at their real interval regardless.
  • An added networked device sits in idle until you explicitly start it — by default it produces no external side effects at all. One of the structural rules of this plugin: default behaviour must not reach outside the machine.

Build and development

# 1. install (also links the framework peers the build needs)
npm install

# 2. build: src/ -> lib/ (needs bash + a DSH source checkout)
$env:DSH_CHECKOUT='D:\dsh\deepseek-harness'; bash scripts/build.sh

# 3. tests and type checks (measured on 2026-10-11)
npm test                  # node --test, 308 tests, 0 failures
npm run typecheck         # tsc -p tsconfig.json --noEmit          -> exit 0
npm run typecheck:client  # tsc -p tsconfig.client.json --noEmit   -> exit 0

scripts/build.sh does three things beyond compiling: it links the build dependencies (scripts/link-deps.mjs, also wired as postinstall because npm install wipes junctions it does not recognize), it copies project-shim/ into lib/project-shim/, and it writes lib/build-stamp.json. That copy is functional, not cosmetic — SimEngine points the Python process's PYTHONPATH at the shim (smbus + RPi.GPIO + sitecustomize), and since package.json's files only ships lib, a build without it publishes a package that cannot run.

scripts/prepare.mjs makes the git install work: lib/ is gitignored, so a git install would have no entry point. It builds only when lib/index.js is missing, and a failed build only warns (exit 0) — a build failure usually means the machine lacks bash or devDependencies, and failing the whole install would blame npm for it.

Release artifact: npm pack → a .tgz containing lib, cordis.patch.yml, skill, docs, models and scripts.

Self-check: is the live build the build you think it is?

curl http://127.0.0.1:<port>/@noob-stupid/dsh-breadboard/api/capabilities

All UI and API live under the route prefix /@noob-stupid/dsh-breadboard/. GET …/api/capabilities returns build (the build timestamp of the currently live code) and simTools (the tools that actually registered). Reach for it first when something looks wrong: tool registration failure is completely silent — the model just says it does not have that capability and the UI shows nothing unusual — and a hot reload can report success while the old module keeps serving.

Known limitations

Honest list; none of these are hidden behind a default that pretends otherwise.

  • This package is not published on npm. Install from GitHub. A prebuilt tarball exists as a release artifact, but there is no npm install line to offer.
  • The UI is Chinese only. There is no locale switch in the panel today, and no English UI strings to promise.
  • Reading GPIO from the project's Python code is not implemented. hw_set_occlusion propagates occlusion along the real wires into a networked device's readings, but the reverse direction — project code calling RPi.GPIO.input() through the shim — raises NotImplementedError, and the bridge's gpio_read / gpio_write are declared in the contract but not implemented ("protocol defined, behaviour model not implemented yet"). So a firmware that polls a pin and then reports cannot be exercised end to end yet; a firmware that reports over HTTP can.
  • Networked devices are not rendered in the 3D scene yet. They are in the SSOT (transport: 'network') and fully drivable by tools, but the front end does not draw them.
  • Only measured / verified paths are claimed. The networked-device chain was verified against a real backend; an I2C-shaped real project has not been adapted yet — the I2C half is verified by this repository's own test projects.
  • Model size vs geometric fidelity is an open either/or (documented in docs/06 §6): scaling an imported model to the declared real-board size slightly deforms it. Today the plugin keeps real board dimensions and aligns ports to the deformed pitch.
  • hw_set_occlusion only reaches networked devices. If the occluded part is wired to something that is not a networked device with a pinMap, nothing propagates — and the tool says so in its reason instead of silently succeeding.
  • The port count on some imported models is still converging (e.g. a handful of Raspberry Pi header pins land on the PCB rather than on a pin mesh). Ports are usable; a few anchors are off by fractions of a millimetre.

Documentation

FileContents
docs/00-宿主API核实.mdhost API verification (slot tree, route registration, pitfalls)
docs/01-项目定位.mdproject positioning, locked scope
docs/02-步骤①-虚拟时钟内核.mdthe virtual clock kernel
docs/03-模型库可行性验证.mdmodel-library feasibility study (why not a scraper)
docs/04-失败族-看起来正常实际全错.mdthe "looks fine, actually all wrong" failure family (20 cases)
docs/05-模型来源与导入方案.mdmodel sources and the import decision
docs/06-状态与交接.mdstatus / handover sheet
docs/07-联网设备-第二类虚拟硬件.mdnetworked devices, the second class of virtual hardware

The docs are written in Chinese (they are working documents, and translating them would create two copies to keep in sync). This README and README.zh.md are maintained as a matched pair.

License

BSD-3-Clause.


dsh-breadboard

Plugins relacionados