dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
DSH 嵌入式硬件沙盒插件:在 3D 面包板场景里摆放 ESP32 / BME280 / PIR / LED 等器件、按协议与电压校验接线;让真实 Python 项目代码在环运行、经 Python shim 读虚拟 I2C/GPIO 的寄存器级读数,支持故障注入(断线 / NACK / busy / 强制读数);联网设备模拟器可向真实 HTTP 后端周期上报(已在真实"智能选座"系统全链路验证);还可只读观察真机状态,并用数据驱动的型号定义支持新硬件。随插件带 16 个 hw_* 工具与一份 SKILL.md。
Web profile plugin — host half + browser half.
English: README.md | 中文: README.zh.md
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:
| Requirement | Where | Note |
|---|---|---|
repo topic dsh-plugin | GitHub repository topics | makes the repo discoverable in community lists |
a dsh.bundle manifest | package.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 |
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:
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).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.
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.
The interface is currently Chinese only — the labels below are the literal ones on screen.
| Panel | What 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 component | click a model key to drop it into the scene (collapsed by default) |
| ② Re-skin models | search 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) |
| Statistics | render statistics (draw calls / triangles / FPS) — pure read, off by default |
| Center 3D view | placed 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 box | send 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:
<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.
hw_* toolsThis 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.
| Tool | What it does | When to use it |
|---|---|---|
hw_list_projects | list built-in hardware projects: parts, wires, network config, and whether each is currently wired end to end | before hand-placing anything — a blueprint is one call, hand-assembly is four steps and a missed step fails silently |
hw_import_project | import a project: clears the current assembly first, then places parts, wires and network bindings | switching the whole hardware set; note it replaces, it does not add |
hw_save_project | create 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 |
| Tool | What it does | When to use it |
|---|---|---|
hw_get_assembly | read the SSOT summary: revision, components, connections, warnings (with their message, not just the code), power summary, virtual devices | before every edit — you need the real port ids; port ids are pin identities (E4), not silkscreen names (GPIO23) |
hw_edit_assembly | one tool over the contract's ClientAction union: place / move / rotate / connect / disconnect / remove / pin | any change to the scene. It takes an action object, so read the samples in the tool description first |
| Tool | What it does | When to use it |
|---|---|---|
hw_run_simulation | run a real Python project on virtual hardware; duration is virtual seconds; returns project output plus virtual/wall elapsed time | the 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_log | read the last (or in-progress) run's project output, with an optional tail | when hw_run_simulation's output was truncated |
hw_inject_fault | inject disconnect / nack / busy / set_reading / clear against a device id or a model:address target | verifying that the project's error handling really observes the fault (e.g. an OSError after a disconnect) instead of assuming it does |
| Tool | What it does | When to use it |
|---|---|---|
hw_network_add | add a networked device (protocol adapter + endpoint + self-reported device id). It does not start | wiring a device emulator into your backend for the first time |
hw_network_devices | list devices: protocol, endpoint, online state, report/failure counts, current readings, server-pushed config | confirming the device is actually connected and actually reporting — an added-but-idle device looks "fine" otherwise |
hw_network_control | start / stop / disconnect (offline fault) / clear | only start makes requests go out — intentional, so nothing has external side effects by default; disconnect tests whether the far side notices |
hw_network_set_reading | set 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_occlusion | occlude / un-occlude a placed part and propagate the effect along the real wires to a networked device's readings | testing the hardware path: if the PIR is not wired, or wired to the wrong pin, nothing happens — exactly like the real board |
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.
| Code | Meaning | What to do |
|---|---|---|
bad_params | the 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_conflict | two current directions fight (output to output / two supplies tied together) | rearrange per current direction: at least one end must become an input |
protocol_mismatch | the two ports carry different signal kinds | use a matching port pair; check which signal that pin actually supports |
port_occupied | a different cable already owns that port | unplug the other cable (disconnect with its cableId), or use another pin |
voltage_mismatch | port voltages are not compatible | connect voltage-compatible ports, or add level shifting |
power_exceeded | the assembly's power demand exceeds its supply budget | add supply or reduce draw |
i2c_address_conflict | two devices on the bus share an I2C address | change one device's address (SDO/AD0 pin) |
i2c_pullup_missing | the I2C bus has no pull-ups | add pull-ups on SDA/SCL |
unconnected_port | a port has no connection at all | connect it as needed — and check you did not forget the other wire of a pair |
unknown_hardware_model | no such model key | check the key against hw_get_assembly's existing components |
component_pinned | the component is pinned; the move was explicitly refused | unpin it first (right-click menu, or set_pinned: false) |
already_connected | that very pair of ports is already connected | do 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.already_connected.ok: true does not mean the effect happened. Every write should be followed by reading the
assembly again.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.
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:
| state | meaning | where to look |
|---|---|---|
ok | seen, data fresh | — |
stale | seen, but the data is old | the board really is offline — check power/network |
error | we cannot reach the system | the URL and the credentials, not the hardware |
not_found | the system answers but has no such device | the device id, or it has never been online |
idle | not 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.
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
<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.| modelKey | Origin | In the bundle | Why |
|---|---|---|---|
bme280 | self-produced | yes | register-level device model + demo assembly |
led-5v | self-produced | yes | simplest possible output part |
hc-sr501 | self-produced | yes | the demo assembly uses 2 of them (PIR) |
esp32-seat-sensor | self-produced | yes | the networked-device demo |
rpi-4b | imported from step.parts (third party) | no | third-party geometry — not redistributed by this plugin; import it yourself from the UI when you need it |
mpl3115a2 | imported from step.parts (third party) | no | same |
breadboard | self-produced (no ports, passive) | no geometry | renders 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.
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:
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.# 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.
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.
Honest list; none of these are hidden behind a default that pretends otherwise.
npm install line to offer.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.transport: 'network') and fully drivable by tools, but the front end does not draw them.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.| File | Contents |
|---|---|
| docs/00-宿主API核实.md | host API verification (slot tree, route registration, pitfalls) |
| docs/01-项目定位.md | project positioning, locked scope |
| docs/02-步骤①-虚拟时钟内核.md | the virtual clock kernel |
| docs/03-模型库可行性验证.md | model-library feasibility study (why not a s |