@tetherto/mdk-skill 0.0.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +133 -0
- package/dist/skills/mdk/SKILL.md +114 -0
- package/dist/skills/mdk/references/architecture.md +89 -0
- package/dist/skills/mdk/references/glossary.md +27 -0
- package/dist/skills/mdk/references/mdk-contract.schema.json +138 -0
- package/dist/skills/mdk/references/package-index.md +53 -0
- package/dist/skills/mdk/references/protocol.md +95 -0
- package/dist/skills/mdk-create-app/SKILL.md +231 -0
- package/dist/skills/mdk-deployment/SKILL.md +161 -0
- package/dist/skills/mdk-gateway-plugin/SKILL.md +192 -0
- package/dist/skills/mdk-gateway-plugin/references/controller-patterns.md +99 -0
- package/dist/skills/mdk-gateway-plugin/references/plugin-authoring.md +121 -0
- package/dist/skills/mdk-publish-security/SKILL.md +177 -0
- package/dist/skills/mdk-publish-security/references/vuln-classes.md +151 -0
- package/dist/skills/mdk-publish-security/scripts/scan-packages.mjs +544 -0
- package/dist/skills/mdk-site-sizing/SKILL.md +160 -0
- package/dist/skills/mdk-site-sizing/eval/held-out.json +104 -0
- package/dist/skills/mdk-site-sizing/eval/profiles/cap-100devices-2workers-mixed.json +24 -0
- package/dist/skills/mdk-site-sizing/eval/profiles/cap-100devices-2workers.json +23 -0
- package/dist/skills/mdk-site-sizing/eval/profiles/cap-10devices-1workers.json +23 -0
- package/dist/skills/mdk-site-sizing/eval/profiles/cap-15devices-2workers.json +24 -0
- package/dist/skills/mdk-site-sizing/eval/profiles/cap-200devices-4workers.json +23 -0
- package/dist/skills/mdk-site-sizing/eval/profiles/cap-500devices-4workers.json +23 -0
- package/dist/skills/mdk-site-sizing/eval/profiles/cap-50devices-2workers.json +23 -0
- package/dist/skills/mdk-site-sizing/references/envelope.json +284 -0
- package/dist/skills/mdk-site-sizing/references/envelope.md +42 -0
- package/dist/skills/mdk-site-sizing/scripts/compile-envelope.mjs +45 -0
- package/dist/skills/mdk-site-sizing/scripts/eval-held-out.mjs +50 -0
- package/dist/skills/mdk-site-sizing/scripts/lib.mjs +591 -0
- package/dist/skills/mdk-site-sizing/scripts/parse-site.mjs +25 -0
- package/dist/skills/mdk-site-sizing/scripts/size-site.mjs +49 -0
- package/dist/skills/mdk-ui-component/SKILL.md +207 -0
- package/dist/skills/mdk-ui-component/references/page-recipe.md +78 -0
- package/dist/skills/mdk-ui-component/references/ui-registry.json +1 -0
- package/dist/skills/mdk-worker-plugin/SKILL.md +198 -0
- package/dist/skills/mdk-worker-plugin/assets/mdk-contract.template.json +45 -0
- package/dist/skills/mdk-worker-plugin/assets/worker-template/README.md +93 -0
- package/dist/skills/mdk-worker-plugin/assets/worker-template/mdk-contract.json +26 -0
- package/dist/skills/mdk-worker-plugin/assets/worker-template/mock/server.js +93 -0
- package/dist/skills/mdk-worker-plugin/assets/worker-template/package.json +23 -0
- package/dist/skills/mdk-worker-plugin/assets/worker-template/smoke.config.js +41 -0
- package/dist/skills/mdk-worker-plugin/assets/worker-template/src/client.js +47 -0
- package/dist/skills/mdk-worker-plugin/assets/worker-template/src/commands/reboot.js +5 -0
- package/dist/skills/mdk-worker-plugin/assets/worker-template/src/commands/set-power-mode.js +10 -0
- package/dist/skills/mdk-worker-plugin/assets/worker-template/src/telemetry/hashrate-rt.js +5 -0
- package/dist/skills/mdk-worker-plugin/assets/worker-template/src/telemetry/power-mode.js +5 -0
- package/dist/skills/mdk-worker-plugin/assets/worker-template/src/telemetry/power.js +5 -0
- package/dist/skills/mdk-worker-plugin/assets/worker-template/src/telemetry/temperature.js +5 -0
- package/dist/skills/mdk-worker-plugin/references/contract-authoring.md +135 -0
- package/dist/skills/mdk-worker-plugin/references/device-families.md +96 -0
- package/dist/skills/mdk-worker-plugin/references/local-testing.md +134 -0
- package/dist/skills/mdk-worker-plugin/references/mdk-contract.schema.json +138 -0
- package/dist/skills/mdk-worker-plugin/references/worker-base-api.md +152 -0
- package/dist/skills/mdk-worker-plugin/scripts/validate-contract.mjs +129 -0
- package/dist/skills/mdk-worker-plugin/scripts/worker-smoke.mjs +256 -0
- package/package.json +47 -1
- package/src/assemble.mjs +11 -0
- package/src/index.d.ts +57 -0
- package/src/index.mjs +254 -0
- package/src/install.mjs +57 -0
- package/src/sources.map.json +34 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mdk-worker-plugin
|
|
3
|
+
description: >
|
|
4
|
+
Integrate a new device into MDK as a device Worker. Use for any task like
|
|
5
|
+
"new device / miner / power meter / sensor / container Worker", "build a
|
|
6
|
+
Worker", "integrate hardware", "wrap a device protocol (Modbus / CGMiner /
|
|
7
|
+
HTTP / MQTT)", or anything creating or editing an mdk-contract.json, a
|
|
8
|
+
Worker Plugin, or telemetry/command handlers under backend/workers/.
|
|
9
|
+
metadata:
|
|
10
|
+
suite: mdk-developer-skill
|
|
11
|
+
mdk_version: "0.7.0"
|
|
12
|
+
license: Apache-2.0
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Integrate a new MDK device Worker
|
|
16
|
+
|
|
17
|
+
A device Worker is a **Worker Plugin**: a package directory holding an
|
|
18
|
+
`mdk-contract.json` plus one small handler module per telemetry channel and
|
|
19
|
+
per command. It exports no module of its own. `WorkerRuntimeV2`
|
|
20
|
+
(`@tetherto/mdk-worker`) loads the directory and instantiates the plugin once
|
|
21
|
+
per device — handlers are plain `(params)` functions that read device context
|
|
22
|
+
from `require('@tetherto/mdk-worker/device')`. Workers are **site-agnostic**:
|
|
23
|
+
they know nothing about the site they run in, so everything here is testable
|
|
24
|
+
locally against a device mock before a Kernel ever exists.
|
|
25
|
+
|
|
26
|
+
The final package shape (this is `assets/worker-template/`, a verbatim copy of
|
|
27
|
+
`packages/cli/templates/worker/` — the same tree `mdk create worker` scaffolds):
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
<your-worker>/
|
|
31
|
+
├── mdk-contract.json # single source of truth (package root)
|
|
32
|
+
├── package.json # no main/exports — the runtime loads the dir
|
|
33
|
+
├── smoke.config.js # glue for scripts/worker-smoke.mjs
|
|
34
|
+
├── src/
|
|
35
|
+
│ ├── client.js # vendor-protocol I/O via ambient device context
|
|
36
|
+
│ ├── telemetry/<name>.js # one file per telemetry channel
|
|
37
|
+
│ └── commands/<name>.js # one file per command
|
|
38
|
+
└── mock/server.js # vendor firmware simulator
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Workflow
|
|
42
|
+
|
|
43
|
+
### 1. Start from the Worker template
|
|
44
|
+
|
|
45
|
+
The default starting point for every new Worker is `assets/worker-template/`
|
|
46
|
+
— a complete minimal Worker Plugin (contract + client + handlers + mock) with
|
|
47
|
+
no assumptions about your device category.
|
|
48
|
+
|
|
49
|
+
Do **not** model a new Worker on the MDK monorepo's shipped device families
|
|
50
|
+
(Whatsminer, Antminer, ABB, Seneca, …) by default — those still use the older
|
|
51
|
+
`plugin/` layout and are tightly coupled to the mining use cases they were
|
|
52
|
+
built for. Only when the user **explicitly asks** to follow the existing MDK
|
|
53
|
+
Worker families and their structure (e.g. contributing a Worker into this
|
|
54
|
+
monorepo, or deliberately mirroring a shipped family's protocol handling)
|
|
55
|
+
should you read [`references/device-families.md`](./references/device-families.md)
|
|
56
|
+
and copy from the named family instead.
|
|
57
|
+
|
|
58
|
+
### 2. Scaffold
|
|
59
|
+
|
|
60
|
+
In a CLI-managed project, scaffold and register in one step:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
mdk create worker <name>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
This copies the CLI Worker template into `workers/<name>`, links it as an npm
|
|
67
|
+
workspace member (root `npm install`, skip with `--no-install`), and appends
|
|
68
|
+
it under `mdk.yaml` → `spec.workers` with a `mock: true` seed device (skip
|
|
69
|
+
with `--no-stack-entry`) — so it is runnable immediately with
|
|
70
|
+
`mdk run worker <name>` before you change a line.
|
|
71
|
+
|
|
72
|
+
Outside the CLI (e.g. contributing inside the monorepo, or no `mdk.yaml`
|
|
73
|
+
project), copy `assets/worker-template/` to your Worker's location instead.
|
|
74
|
+
Either way, rename the template-specific parts (contract metadata, client
|
|
75
|
+
methods, mock behavior). Remember to adapt `smoke.config.js` (mock boot,
|
|
76
|
+
device opts, sample command params) and `package.json` (name, description).
|
|
77
|
+
The plugin is location-independent — put the package wherever your project
|
|
78
|
+
keeps packages. (Only Workers contributed to the MDK monorepo itself follow
|
|
79
|
+
its `backend/workers/<category>/<family>/` layout.)
|
|
80
|
+
|
|
81
|
+
### 3. Author `mdk-contract.json`
|
|
82
|
+
|
|
83
|
+
Start from [`assets/mdk-contract.template.json`](./assets/mdk-contract.template.json); the field-by-field guide is
|
|
84
|
+
[`references/contract-authoring.md`](./references/contract-authoring.md) — load it now. Non-negotiables:
|
|
85
|
+
|
|
86
|
+
- Every telemetry/command entry needs a `handler` path (e.g. `src/telemetry/<name>.js`)
|
|
87
|
+
— the loader hard-fails without it.
|
|
88
|
+
- **Every `number` command param must declare `min` and `max`.** The Kernel
|
|
89
|
+
enforces bounds only when declared (`ERR_PARAM_RANGE`); an undeclared bound
|
|
90
|
+
is an unprotected physical setpoint.
|
|
91
|
+
- Read-only devices (meters, sensors) declare `"commands": []`.
|
|
92
|
+
- `description` / `overview` / `constraints` are simultaneously machine docs
|
|
93
|
+
and AI operator context — write real semantics ("If 0, device is booting"),
|
|
94
|
+
and put safety thresholds in the contract, not in code comments.
|
|
95
|
+
|
|
96
|
+
### 4. Validate the contract — loop until clean
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
node <this-skill>/scripts/validate-contract.mjs <worker>/mdk-contract.json
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Validates against `references/mdk-contract.schema.json` plus semantic
|
|
103
|
+
checks (handler files exist, no duplicate names, numeric params bounded).
|
|
104
|
+
Fix and re-run until it exits 0. Handler files must exist first — stubs from
|
|
105
|
+
the template are enough at this stage.
|
|
106
|
+
|
|
107
|
+
### 5. Build the mock, then the device client
|
|
108
|
+
|
|
109
|
+
Write `mock/server.js` first — a standalone simulator of the vendor firmware
|
|
110
|
+
API that knows nothing about MDK. The template's self-contained HTTP JSON
|
|
111
|
+
mock is the pattern to follow; adapt it to your device's protocol. (Workers
|
|
112
|
+
living inside the MDK monorepo may instead reuse its shared mock framework in
|
|
113
|
+
[`backend/workers/mock/`](../../../../../backend/workers/mock/README.md).) Then write `src/client.js`: plain vendor-protocol
|
|
114
|
+
I/O. Bind to ambient device context
|
|
115
|
+
(`require('@tetherto/mdk-worker/device')` → `{ opts, env, config, logger }`)
|
|
116
|
+
rather than exporting a `createClient(opts)` factory — the runtime loads the
|
|
117
|
+
module fresh per device. Throw `ERR_*` errors. No envelopes, no base classes.
|
|
118
|
+
|
|
119
|
+
### 6. Wire the handlers
|
|
120
|
+
|
|
121
|
+
There is no `connect()` and no `plugin/index.js`. Each handler is
|
|
122
|
+
`async (params) => result` and talks to the device through `src/client.js`
|
|
123
|
+
(or by reading ambient context itself). Telemetry returns the channel value
|
|
124
|
+
(matching the contract's declared `type`); commands return a result object or
|
|
125
|
+
throw an `ERR_*` error. Full runtime semantics: [`references/worker-base-api.md`](references/worker-base-api.md).
|
|
126
|
+
|
|
127
|
+
Template pattern:
|
|
128
|
+
|
|
129
|
+
```js
|
|
130
|
+
'use strict'
|
|
131
|
+
const client = require('../client')
|
|
132
|
+
module.exports = async () => (await client.getSummary()).hashrate_ths
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### 7. Test locally — the four-step loop
|
|
136
|
+
|
|
137
|
+
Load [`references/local-testing.md`](./references/local-testing.md) for the full procedure. In order:
|
|
138
|
+
|
|
139
|
+
1. **Contract validation** — step 4 above (no runtime).
|
|
140
|
+
2. **In-process smoke** — `node <this-skill>/scripts/worker-smoke.mjs <worker-dir>`
|
|
141
|
+
boots your plugin against your mock (via `smoke.config.js`), asserts every
|
|
142
|
+
declared telemetry channel returns a value of the declared type, and that
|
|
143
|
+
out-of-bounds command params are rejected. No Kernel, no DHT.
|
|
144
|
+
3. **Standalone protocol check** — host the package on `WorkerRuntimeV2` with a
|
|
145
|
+
tiny caller and drive real envelopes through `handleRequest`.
|
|
146
|
+
4. **Site integration** — add the Worker to a site stack (e.g.
|
|
147
|
+
[`examples/mvp-site/deploy/run-process.js`](../../../../../examples/mvp-site/deploy/run-process.js))
|
|
148
|
+
and confirm a real Kernel registers it end-to-end.
|
|
149
|
+
|
|
150
|
+
### 8. Boot into a site
|
|
151
|
+
|
|
152
|
+
In a CLI-managed project this is just `mdk run worker <name>` (or `mdk run`
|
|
153
|
+
to boot the whole stack together) — see `mdk-deployment`. The rest of this
|
|
154
|
+
section is the manual path, for when nothing owns the runtime yet.
|
|
155
|
+
|
|
156
|
+
A caller process owns the runtime (the plugin package itself never does):
|
|
157
|
+
|
|
158
|
+
```js
|
|
159
|
+
const { WorkerRuntimeV2 } = require('@tetherto/mdk-worker')
|
|
160
|
+
const runtime = new WorkerRuntimeV2(pkgDir, {
|
|
161
|
+
workerId: 'my-worker-1',
|
|
162
|
+
devices: [{ deviceId: 'dev-0', opts: { host: '127.0.0.1', port: 18080 } }],
|
|
163
|
+
kernelTopic // hex topic → DHT announce; omit for local-only
|
|
164
|
+
})
|
|
165
|
+
await runtime.start()
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The Worker joins the DHT topic; the Kernel pulls identity + capabilities and
|
|
169
|
+
starts scheduling telemetry/health. **No Kernel changes are ever needed to add
|
|
170
|
+
a Worker.**
|
|
171
|
+
|
|
172
|
+
## Gotchas
|
|
173
|
+
|
|
174
|
+
- **Unidirectional protocol**: a Worker never calls the Kernel — it only
|
|
175
|
+
answers. If you think you need to push, you need a telemetry channel that
|
|
176
|
+
the Kernel pulls.
|
|
177
|
+
- **`deviceId` ownership is exclusive** per Worker; duplicate ids in
|
|
178
|
+
`opts.devices` throw `ERR_DEVICE_ID_DUPLICATE` at construction.
|
|
179
|
+
- One device offline must never affect siblings — failures stay inside that
|
|
180
|
+
device's instance (V2 has no boot-time probe; unreachable devices surface
|
|
181
|
+
as errors in telemetry payloads rather than `ERR_DEVICE_UNAVAILABLE`).
|
|
182
|
+
- Use `debug` / the ambient `logger`, never `console.log`, in Worker code.
|
|
183
|
+
Errors are `ERR_SCREAMING_SNAKE` strings.
|
|
184
|
+
- The published contract strips `handler` paths; everything else in the
|
|
185
|
+
contract is visible to sites and AI operators — write it accordingly.
|
|
186
|
+
|
|
187
|
+
## Hand-off
|
|
188
|
+
|
|
189
|
+
This skill stops at a working Worker Plugin + contract. For prompts that also
|
|
190
|
+
ask to **show** the data in a UI:
|
|
191
|
+
|
|
192
|
+
| Next need | Skill |
|
|
193
|
+
| -------------------------------------------- | -------------------- |
|
|
194
|
+
| Aggregate / expose HTTP API over this Worker | `mdk-gateway-plugin` |
|
|
195
|
+
| Register Worker in `mdk.yaml` and run it | `mdk-deployment` |
|
|
196
|
+
| Dashboard page / tiles / charts | `mdk-ui-component` |
|
|
197
|
+
|
|
198
|
+
See the composite routing table in [`../mdk/SKILL.md`](../mdk/SKILL.md).
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"metadata": {
|
|
3
|
+
"provider": "TODO-vendor-slug",
|
|
4
|
+
"deviceFamily": "TODO: miner | power-meter | sensor | container | minerpool",
|
|
5
|
+
"brand": "TODO-Brand",
|
|
6
|
+
"modelsSupported": ["TODO_MODEL"],
|
|
7
|
+
"overview": "TODO: what the worker controls, which vendor protocol it wraps, and any critical environmental constraint an AI operator must know."
|
|
8
|
+
},
|
|
9
|
+
"capabilities": {
|
|
10
|
+
"telemetry": [
|
|
11
|
+
{
|
|
12
|
+
"name": "temperature",
|
|
13
|
+
"unit": "C",
|
|
14
|
+
"type": "number",
|
|
15
|
+
"handler": "src/telemetry/temperature.js",
|
|
16
|
+
"description": "TODO: what this reads, plus semantic boundaries (e.g. 'If 0, device is booting')."
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"name": "status_text",
|
|
20
|
+
"type": "string",
|
|
21
|
+
"handler": "src/telemetry/status-text.js",
|
|
22
|
+
"description": "TODO: enumerate the possible values and what each means."
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"commands": [
|
|
26
|
+
{
|
|
27
|
+
"name": "setSetpoint",
|
|
28
|
+
"description": "TODO: what this does physically.",
|
|
29
|
+
"constraints": "TODO: rate limits / approval requirements, or delete this field.",
|
|
30
|
+
"handler": "src/commands/set-setpoint.js",
|
|
31
|
+
"params": [
|
|
32
|
+
{ "name": "value", "type": "number", "min": 0, "max": 100 }
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"health": {
|
|
37
|
+
"supportedStates": ["OK", "OFFLINE"],
|
|
38
|
+
"alerts": [],
|
|
39
|
+
"troubleshooting": []
|
|
40
|
+
},
|
|
41
|
+
"errors": {
|
|
42
|
+
"ERR_DEVICE_CALL_FAILED": "The device API call failed or returned an error."
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# __WORKER_NAME__
|
|
2
|
+
|
|
3
|
+
An MDK **Worker Plugin** — the code that translates between MDK and a family of
|
|
4
|
+
physical devices. It ships only an [`mdk-contract.json`](./mdk-contract.json) (its capability manifest)
|
|
5
|
+
plus [`src/`](./src/) handler files, a device "vendor SDK" client, and a mock device
|
|
6
|
+
server. It exports no module of its own; it is **directory-loaded** by
|
|
7
|
+
`WorkerRuntimeV2`, which the MDK CLI constructs (`mdk run worker <name>`) — the
|
|
8
|
+
package never constructs or depends on the runtime itself.
|
|
9
|
+
|
|
10
|
+
This scaffold is a working example that talks to a hypothetical device over a
|
|
11
|
+
tiny HTTP JSON API. Replace the device client, mock, contract, and handlers with
|
|
12
|
+
your own device's protocol.
|
|
13
|
+
|
|
14
|
+
## Layout
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
mdk-contract.json # capability manifest (telemetry channels + commands)
|
|
18
|
+
src/client.js # your device "vendor SDK" — binds to this device once, per instance
|
|
19
|
+
src/telemetry/*.js # telemetry handlers (metrics read live from the device)
|
|
20
|
+
src/commands/*.js # command handlers (reboot, set-power-mode, ...)
|
|
21
|
+
mock/server.js # a standalone device simulator (createServer)
|
|
22
|
+
smoke.config.js # the device spec `mdk validate worker --smoke` checks against
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Handlers are plain `(params)` functions — they call `require('../client')` for
|
|
26
|
+
their device's client, which itself binds once (at `require` time) to the
|
|
27
|
+
ambient `@tetherto/mdk-worker/device` for this instance's connection opts.
|
|
28
|
+
There is one such module registry per configured device, so each handler
|
|
29
|
+
always talks to its own device.
|
|
30
|
+
|
|
31
|
+
## Run it
|
|
32
|
+
|
|
33
|
+
Add the worker to your stack's `mdk.yaml` by **local path** and describe its
|
|
34
|
+
seed devices under `config`:
|
|
35
|
+
|
|
36
|
+
```yaml
|
|
37
|
+
spec:
|
|
38
|
+
workers:
|
|
39
|
+
- name: __WORKER_NAME__
|
|
40
|
+
package: ./workers/__WORKER_NAME__
|
|
41
|
+
config:
|
|
42
|
+
mock: true # start mock/server.js per device (dev only)
|
|
43
|
+
devices:
|
|
44
|
+
- id: dev-0
|
|
45
|
+
opts: { host: 127.0.0.1, port: 18080, serial: DEV-0 }
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Then:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
mdk run worker __WORKER_NAME__ # boots the worker (+ its mocks with mock: true)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
With `mock: true` the CLI starts one simulator from [`mock/server.js`](./mock/server.js) per device
|
|
55
|
+
so the worker talks to a simulator instead of hardware. Drop `mock` (or set it
|
|
56
|
+
`false`) and point `opts.host`/`opts.port` at real devices for production.
|
|
57
|
+
|
|
58
|
+
## Check it
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
mdk validate worker __WORKER_NAME__ # the contract and handler files, without running them
|
|
62
|
+
mdk validate worker --path . --smoke # also runs every handler against the simulator
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`--smoke` starts the simulator through [`smoke.config.js`](./smoke.config.js), calls every telemetry
|
|
66
|
+
handler and each sample command it lists, then stops the simulator and expects every handler to fail
|
|
67
|
+
with an `ERR_*` error code. Add a sample to `commands` for each command you add, unless it interrupts
|
|
68
|
+
the device.
|
|
69
|
+
|
|
70
|
+
## Builtin per-device health check
|
|
71
|
+
|
|
72
|
+
`WorkerRuntimeV2` answers a `telemetry.pull` query of `type: 'health'` for any
|
|
73
|
+
device automatically — no contract or handler changes needed. It works by
|
|
74
|
+
registering `health` as a plain handler in the same table any contract-declared
|
|
75
|
+
telemetry channel lives in, so it is dispatched with zero special-casing, the
|
|
76
|
+
same way `status`/`hashrate_rt`/etc. would be. That means it behaves exactly
|
|
77
|
+
like any other named channel:
|
|
78
|
+
|
|
79
|
+
- Requires a `deviceId` — a query with no `deviceId` returns
|
|
80
|
+
`{ error: 'ERR_DEVICE_ID_REQUIRED: health' }`, same as any other named channel.
|
|
81
|
+
- The response comes back in the standard named-channel shape,
|
|
82
|
+
`{ name: 'health', value: { status, id, opts, env, config, workerId } }` —
|
|
83
|
+
that device's own ambient config, useful for verifying that Gateway → Kernel
|
|
84
|
+
→ Worker routing reaches the right device instance (e.g. a query addressed
|
|
85
|
+
to `dev-3` always comes back with `dev-3`'s own `opts`, never a sibling
|
|
86
|
+
device's).
|
|
87
|
+
- It also shows up as a `health` key inside the default `type: 'metrics'`
|
|
88
|
+
aggregate for every device, since that aggregate iterates the very same
|
|
89
|
+
handler table.
|
|
90
|
+
|
|
91
|
+
If your own contract declares a `health` telemetry channel, that one is used
|
|
92
|
+
instead — the builtin never overrides a plugin-declared handler of the same
|
|
93
|
+
name.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"metadata": {
|
|
3
|
+
"provider": "demo",
|
|
4
|
+
"deviceFamily": "device",
|
|
5
|
+
"brand": "DEMO",
|
|
6
|
+
"modelsSupported": ["DEMO_V1"],
|
|
7
|
+
"overview": "Template Worker Plugin — talks to a device's HTTP JSON API over src/client.js. Replace the client, mock, contract and handlers with your own device's protocol."
|
|
8
|
+
},
|
|
9
|
+
"capabilities": {
|
|
10
|
+
"telemetry": [
|
|
11
|
+
{ "name": "hashrate_rt", "unit": "TH/s", "type": "number", "handler": "src/telemetry/hashrate-rt.js", "description": "Real-time throughput from the device's summary endpoint." },
|
|
12
|
+
{ "name": "power", "unit": "W", "type": "number", "handler": "src/telemetry/power.js", "description": "Current power draw." },
|
|
13
|
+
{ "name": "temperature", "unit": "C", "type": "number", "handler": "src/telemetry/temperature.js", "description": "Device temperature." },
|
|
14
|
+
{ "name": "power_mode", "type": "string", "handler": "src/telemetry/power-mode.js", "description": "Active power mode: eco, normal or high." }
|
|
15
|
+
],
|
|
16
|
+
"commands": [
|
|
17
|
+
{ "name": "reboot", "handler": "src/commands/reboot.js", "description": "Restarts the device controller.", "constraints": "Do not call more than once per 5 minutes.", "params": [] },
|
|
18
|
+
{ "name": "setPowerMode", "handler": "src/commands/set-power-mode.js", "description": "Changes the power mode: eco, normal or high (low is accepted as an alias for eco).", "params": [{ "name": "mode", "type": "string" }] }
|
|
19
|
+
],
|
|
20
|
+
"health": { "supportedStates": ["OK", "OFFLINE"] },
|
|
21
|
+
"errors": {
|
|
22
|
+
"ERR_BAD_POWER_MODE": "The device rejected the requested power mode.",
|
|
23
|
+
"ERR_DEVICE_CALL_FAILED": "The device's HTTP API call failed or returned an error."
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const http = require('http')
|
|
4
|
+
const debug = require('debug')('mdk:worker:__WORKER_NAME__:mock')
|
|
5
|
+
|
|
6
|
+
// A standalone device simulator: a small HTTP JSON API (GET /api/v3/summary,
|
|
7
|
+
// POST /api/v3/command) matching src/client.js. It knows nothing about MDK —
|
|
8
|
+
// it is the device-side surface the Worker Plugin translates against, the way
|
|
9
|
+
// a real device on the LAN would be. Replace with your own device's protocol.
|
|
10
|
+
|
|
11
|
+
const POWER_MODES = {
|
|
12
|
+
eco: { power: 0.8, hashrate: 0.85 },
|
|
13
|
+
normal: { power: 1, hashrate: 1 },
|
|
14
|
+
high: { power: 1.15, hashrate: 1.1 }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function jitter (value) {
|
|
18
|
+
return Math.round(value * (0.99 + Math.random() * 0.02) * 100) / 100
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createServer ({ host, port, serial, hashrateThs, powerW }) {
|
|
22
|
+
const state = {
|
|
23
|
+
serial: serial || 'DEV-0000',
|
|
24
|
+
baseHashrateThs: hashrateThs || 180,
|
|
25
|
+
basePowerW: powerW || 3400,
|
|
26
|
+
powerMode: 'normal',
|
|
27
|
+
bootTime: Date.now()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const summary = () => {
|
|
31
|
+
const scale = POWER_MODES[state.powerMode]
|
|
32
|
+
return {
|
|
33
|
+
serial: state.serial,
|
|
34
|
+
model: 'DEMO_V1',
|
|
35
|
+
firmware: 'v1.0.0',
|
|
36
|
+
uptime_s: Math.floor((Date.now() - state.bootTime) / 1000),
|
|
37
|
+
power_mode: state.powerMode,
|
|
38
|
+
hashrate_ths: jitter(state.baseHashrateThs * scale.hashrate),
|
|
39
|
+
power_w: jitter(state.basePowerW * scale.power),
|
|
40
|
+
board_temp_c: jitter(62 + (scale.power - 1) * 20)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const command = (body) => {
|
|
45
|
+
switch (body.cmd) {
|
|
46
|
+
case 'reboot':
|
|
47
|
+
state.bootTime = Date.now()
|
|
48
|
+
return { ok: true, rebooting: true }
|
|
49
|
+
case 'set-power-mode':
|
|
50
|
+
if (!POWER_MODES[body.mode]) throw new Error(`ERR_BAD_POWER_MODE: ${body.mode}`)
|
|
51
|
+
state.powerMode = body.mode
|
|
52
|
+
return { ok: true, power_mode: state.powerMode }
|
|
53
|
+
default:
|
|
54
|
+
throw new Error(`ERR_UNKNOWN_CMD: ${body.cmd}`)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const server = http.createServer((req, res) => {
|
|
59
|
+
const reply = (code, body) => {
|
|
60
|
+
res.writeHead(code, { 'content-type': 'application/json' })
|
|
61
|
+
res.end(JSON.stringify(body))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (req.method === 'GET' && req.url === '/api/v3/summary') return reply(200, summary())
|
|
65
|
+
|
|
66
|
+
if (req.method === 'POST' && req.url === '/api/v3/command') {
|
|
67
|
+
let buf = ''
|
|
68
|
+
req.on('data', (chunk) => { buf += chunk })
|
|
69
|
+
req.on('end', () => {
|
|
70
|
+
try {
|
|
71
|
+
reply(200, command(JSON.parse(buf || '{}')))
|
|
72
|
+
} catch (err) {
|
|
73
|
+
reply(400, { ok: false, error: err.message })
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
reply(404, { ok: false, error: 'ERR_NOT_FOUND' })
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
server.listen(port, host || '127.0.0.1', () => {
|
|
83
|
+
debug('device mock %s listening on %s:%d', state.serial, host, port)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
server,
|
|
88
|
+
state,
|
|
89
|
+
exit () { server.close() }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { createServer }
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__WORKER_NAME__",
|
|
3
|
+
"version": "0.12.1",
|
|
4
|
+
"description": "MDK Worker Plugin — an mdk-contract.json plus src/ handler files, loaded from this directory and instantiated per device by WorkerRuntimeV2 (see `mdk run worker`). It exports no module of its own.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mdk",
|
|
7
|
+
"mdk-worker"
|
|
8
|
+
],
|
|
9
|
+
"license": "Apache-2.0",
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=24"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"mock": "node -e \"require('./mock/server').createServer({ host: '127.0.0.1', port: 18080, serial: 'DEV-0' })\""
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@tetherto/mdk-worker": "^0.12.1",
|
|
18
|
+
"debug": "4.4.1"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"brittle": "3.18.0"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Smoke-harness glue for scripts/worker-smoke.mjs and `mdk validate worker
|
|
4
|
+
// --smoke`: setup() boots this worker's device mock and returns { config,
|
|
5
|
+
// commands, teardown }. Adapt all three when copying the template. `config`
|
|
6
|
+
// becomes device opts for WorkerRuntimeV2 (host/port for src/client.js).
|
|
7
|
+
//
|
|
8
|
+
// After teardown() every telemetry channel must fail with an ERR_*. A channel
|
|
9
|
+
// the worker serves from its own store (a `history` log, say) keeps answering
|
|
10
|
+
// instead: list it as `offlineTelemetry: ['history']` next to `teardown`.
|
|
11
|
+
|
|
12
|
+
const net = require('net')
|
|
13
|
+
|
|
14
|
+
const mock = require('./mock/server')
|
|
15
|
+
|
|
16
|
+
function freePort () {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const s = net.createServer()
|
|
19
|
+
s.once('error', reject)
|
|
20
|
+
s.listen(0, '127.0.0.1', () => {
|
|
21
|
+
const { port } = s.address()
|
|
22
|
+
s.close(() => resolve(port))
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
setup: async () => {
|
|
29
|
+
const port = await freePort()
|
|
30
|
+
const handle = mock.createServer({ host: '127.0.0.1', port, serial: 'DEV-SMOKE' })
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
config: { host: '127.0.0.1', port },
|
|
34
|
+
commands: {
|
|
35
|
+
reboot: {},
|
|
36
|
+
setPowerMode: { mode: 'eco' }
|
|
37
|
+
},
|
|
38
|
+
teardown: async () => { handle.exit() }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { opts, env, config, logger } = require('@tetherto/mdk-worker/device')
|
|
4
|
+
|
|
5
|
+
// The "vendor SDK" for your device's own HTTP JSON API. Plain device I/O — no
|
|
6
|
+
// MDK concepts, no base classes. This is all a plugin author writes to talk
|
|
7
|
+
// to their own device; replace it with your real protocol.
|
|
8
|
+
//
|
|
9
|
+
// The runtime loads this module fresh into a private registry per device, so
|
|
10
|
+
// binding directly to the ambient device module here — rather than exporting
|
|
11
|
+
// a `createClient(opts)` factory for a caller to invoke — already yields one
|
|
12
|
+
// client per device with no extra plumbing.
|
|
13
|
+
// Log only non-secret fields and key names: `env` carries credentials such as
|
|
14
|
+
// DEVICE_TOKEN, and `opts`/`config` may too — never log their values.
|
|
15
|
+
logger('config received: host=%s port=%s env keys=%o config keys=%o',
|
|
16
|
+
opts.host, opts.port, Object.keys(env || {}), Object.keys(config || {}))
|
|
17
|
+
|
|
18
|
+
const base = `http://${opts.host || '127.0.0.1'}:${opts.port}`
|
|
19
|
+
const auth = env.DEVICE_TOKEN ? { authorization: `Bearer ${env.DEVICE_TOKEN}` } : {}
|
|
20
|
+
|
|
21
|
+
const call = async (path, callOpts) => {
|
|
22
|
+
let res, body
|
|
23
|
+
try {
|
|
24
|
+
res = await fetch(base + path, { ...callOpts, headers: { ...auth, ...(callOpts && callOpts.headers) } })
|
|
25
|
+
body = await res.json()
|
|
26
|
+
} catch (err) {
|
|
27
|
+
// An unreachable device or a non-JSON reply: surface it as this plugin's
|
|
28
|
+
// own error code, not fetch's bare "fetch failed".
|
|
29
|
+
throw new Error(`ERR_DEVICE_CALL_FAILED: ${(err.cause && err.cause.code) || err.message}`)
|
|
30
|
+
}
|
|
31
|
+
if (!res.ok || body.ok === false) {
|
|
32
|
+
throw new Error(body.error || `ERR_DEVICE_CALL_FAILED: ${res.status}`)
|
|
33
|
+
}
|
|
34
|
+
return body
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const command = (cmd, args) => call('/api/v3/command', {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: { 'content-type': 'application/json' },
|
|
40
|
+
body: JSON.stringify({ cmd, ...args })
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
module.exports = {
|
|
44
|
+
getSummary: () => call('/api/v3/summary'),
|
|
45
|
+
reboot: () => command('reboot'),
|
|
46
|
+
setPowerMode: (mode) => command('set-power-mode', { mode })
|
|
47
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const client = require('../client')
|
|
4
|
+
|
|
5
|
+
module.exports = async (params) => {
|
|
6
|
+
// Example vendor-agnostic-to-firmware translation: this device's API calls
|
|
7
|
+
// low "eco". Adjust to your own device's vocabulary.
|
|
8
|
+
const mode = params.mode === 'low' ? 'eco' : params.mode
|
|
9
|
+
return client.setPowerMode(mode)
|
|
10
|
+
}
|