dsh-labscript 0.0.0-stage → 0.2.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0
4
+
5
+ - Safe default: backend is mock-labscript unless the config explicitly sets labscript.
6
+ - labs_abort bypasses the serial queue, so it is never blocked behind labs_wait.
7
+ - Pending/last-run state is persisted under DSH_HOME and reloaded after a restart.
8
+ - labs_doctor: read-only diagnostic for python, numpy/h5py, labscript/runmanager, paths and versions.
9
+ - Minimal Plugins-page configuration page (backend, python, runmanager, protocolsDir) + doctor results.
10
+ - Python environment requirements documented per backend.
11
+
12
+ ## 0.1.0
13
+
14
+ - Eight labs_* tools: status, set, engage, wait, abort, data, protocol, restore.
15
+ - Fixed-operation YAML protocols with postprocess write-back and panel restore.
16
+ - Panel snapshots before writes plus an events.jsonl action log.
17
+ - Built-in mock-labscript backend for tests, examples and hardware-free development.
18
+ - Runmanager (labscript) backend built on the stock remote API and shot HDF5.
19
+ - Stalled-queue diagnosis, selective panel restore and accurate abort semantics.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dsh-labscript contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,154 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # dsh-labscript
2
+
3
+ Control a [labscript-suite](https://labscriptsuite.org/) experiment from DeepSeek Harness (DSH).
4
+
5
+ The plugin exposes the stock labscript control surface only: the runmanager remote API
6
+ (panel globals, n_shots, engage, abort, run_shots, error_in_globals) plus the shot HDF5
7
+ files. It does not require any custom BLACS/lyse patch. Everything lab-specific
8
+ (analysis, protocols, hard limits) is supplied by configuration.
9
+
10
+ ## Status
11
+
12
+ v0.2.0. The mock-labscript backend, the protocol/postprocess flow and the
13
+ snapshot/diagnosis behaviour are covered by node and python tests. A client
14
+ configuration page (Plugins page) and a read-only `labs_doctor` are included. The real
15
+ labscript backend is implemented against the stock runmanager remote API but has
16
+ not yet been validated on laboratory hardware. Start real-machine work with a
17
+ read-only status call, then a single-key write/restore, then a few shots.
18
+
19
+ ## Requirements
20
+
21
+ - DSH >= 0.2 and Node.js ^22.19 or >= 24.
22
+ - Python >= 3.9. Mock mode needs only numpy >= 1.23 and h5py >= 3.8; real
23
+ hardware needs the labscript-suite Python environment (labscript, runmanager,
24
+ labscript_utils, zprocess importable). Tested with Python 3.11.0,
25
+ numpy 2.2.5, h5py 3.13.0. The plugin never stores credentials; zprocess
26
+ authentication is read by labscript from labconfig.
27
+
28
+ ## Install
29
+
30
+ Clone this repository, add it as a `link:` dependency in your DSH profile and
31
+ `pnpm install`; docs/INSTALL.zh.md walks through it. The backend defaults to
32
+ `mock-labscript`; real hardware is only used when the config sets
33
+ `backend: labscript` explicitly. Connection settings can be edited in
34
+ Plugins -> dsh-labscript after install (advanced keys stay in the profile
35
+ config). Run `labs_doctor` first if anything looks misconfigured.
36
+
37
+ ## Tools
38
+
39
+ | Tool | What it does |
40
+ |---|---|
41
+ | labs_doctor | Read-only configuration diagnostic: python, numpy/h5py, labscript/runmanager, paths, approval, versions. |
42
+ | labs_status | Read the panel: labscript, globals, n_shots, error_in_globals, run_shots, pending run progress, snapshot directory |
43
+ | labs_set | Write panel globals (or raw expressions, or the repetition counter). Snapshots the panel first. Does not run. |
44
+ | labs_engage | Submit the current panel for a run; returns immediately with n_shots_at_submit and expected_n_shots |
45
+ | labs_wait | Wait (blocking, with timeout) for the pending run; finalizes a pending protocol: postprocess, finish write-back, restore |
46
+ | labs_abort | Abort the current submission |
47
+ | labs_data | Read the run folder HDF5: first-shot globals and, with scan, a per-shot table |
48
+ | labs_protocol | list / show / run YAML fixed-operation protocols |
49
+ | labs_restore | Restore the panel from the latest snapshot taken before a write |
50
+
51
+ A runtime skill named labscript carries the workflow rules (status first, fixed
52
+ protocols for stable flows, submit then end the turn, one run at a time, never
53
+ guess panel state).
54
+
55
+ ## Fixed-operation protocols
56
+
57
+ name: demo_scan
58
+ labscript: /path/to/your/sequence.py # or: joint_script: registry_key
59
+ set:
60
+ do_acquire: true
61
+ amplitude: 0.5
62
+ scan:
63
+ duration_test_us: np.linspace(0.0, 8.0, 5) # expression, number or list
64
+ reps: 2
65
+ postprocess: demo.peaks # entry configured once
66
+ finish:
67
+ restore: true
68
+ panel:
69
+ duration_test_us: fit.center # written back to the panel
70
+ history:
71
+ demo.peaks.center_us: fit.center # appended to the calibration jsonl
72
+
73
+ labs_protocol show demo_scan # validates the plan and hard limits
74
+ labs_protocol run demo_scan # set + scan + reps + engage, returns immediately
75
+ labs_wait # waits, then postprocess -> finish -> restore
76
+
77
+ ## Configuration
78
+
79
+ Add one row to your DSH profile patch (see examples/config.example.yml):
80
+
81
+ - id: dsh-labscript
82
+ name: dsh-labscript
83
+ config:
84
+ python: /path/to/labscript-suite/python
85
+ backend: labscript
86
+ runmanager: { host: 127.0.0.1, port: 42523, timeoutS: 10 }
87
+ protocolsDir: /path/to/protocols
88
+ scriptsFile: /path/to/scripts.json
89
+ hardLimitsFile: /path/to/hard_limits.json
90
+ calibrationLog: /path/to/calibration.jsonl
91
+ pathMap:
92
+ - { remotePrefix: "E:/Experiments", localPrefix: "//lab-pc/Experiments" }
93
+ reps: { global: dummy_iterator }
94
+ postprocess:
95
+ entry: my_lab.analysis:analyze
96
+ pythonPaths: [/path/to/my_lab]
97
+ approval: { engage: true, protocolRun: true }
98
+
99
+ The python interpreter must be able to import numpy and h5py; the real backend
100
+ also needs runmanager (labscript-suite). The plugin never stores credentials:
101
+ zprocess authentication is read by labscript itself from labconfig.
102
+
103
+ ### Mock backend
104
+
105
+ backend: mock-labscript
106
+
107
+ runs a built-in simulator (fake panel, fake HDF5, persistent state under
108
+ mock.stateDir). It exists for tests, examples and hardware-free development; it
109
+ is not a physics simulation.
110
+
111
+ ## Install (local)
112
+
113
+ git clone <this repo> && cd dsh-labscript
114
+ npm install
115
+ # add "dsh-labscript": "link:/path/to/dsh-labscript" to the profile package.json
116
+ # add the bundle name and the cordis.patch.yml row; restart DSH
117
+
118
+ ## Development
119
+
120
+ npm install # only runtime dependency: yaml
121
+ npm test # node --test, 18 tests (fake bridge, no python needed)
122
+ python python/tests/run_tests.py # 8 tests (needs numpy + h5py)
123
+ LABSCRIPT_PYTHON=/path/to/python3 node scripts/demo.mjs # end-to-end mock demo
124
+
125
+ A fresh clone must run `npm install` before `npm test`; the CI workflow does this.
126
+
127
+ ## Docs
128
+
129
+ - docs/README.zh.md - Chinese guide
130
+ - docs/INSTALL.zh.md - local DSH profile install
131
+
132
+ ## Boundaries
133
+
134
+ - Real hardware: engage/protocol run ask for a DSH approval (one-shot) when the
135
+ backend is labscript; abort and panel writes do not. The plugin is not a
136
+ hardware interlock.
137
+ - Completion is detected from the run folder on the filesystem (HDF5 count,
138
+ stable file sizes, and a readable last shot); stock labscript has no
139
+ run-state query.
140
+ - Cancelling labs_wait only stops waiting: the run keeps going. Call labs_abort
141
+ explicitly when something must stop. labscript abort stops compiling/submitting new
142
+ shots; shots already queued in BLACS or running are not stopped.
143
+ - labs_wait reports a diagnosis (complete, no_shots, compiling, compiled_not_executed,
144
+ partial). compiled_not_executed means shots exist but show no acquisition data yet;
145
+ BLACS may be paused or not executing the queue.
146
+ - Real runmanager engage does not return a run folder; the bridge captures the
147
+ output folder before engage and reports it as the run folder.
148
+ - The panel is snapshotted before every write; labs_restore restores only the keys the
149
+ write touched (full=true restores the whole panel), so unrelated concurrent edits survive.
150
+ - Actions are logged to events.jsonl next to the snapshots.
151
+
152
+ ---
153
+
154
+ Development note: implemented with assistance from a DeepSeek Harness coding agent.
@@ -0,0 +1,6 @@
1
+ # dsh-labscript -- DSH bundle patch
2
+ #
3
+ # Declares the plugin row. The "name" must match the installed package name.
4
+ - insert:
5
+ - id: dsh-labscript
6
+ name: dsh-labscript
@@ -0,0 +1,54 @@
1
+ # 安装(DSH desktop profile)
2
+
3
+ ## 0. 环境要求
4
+
5
+ - DSH >= 0.2,Node.js ^22.19 或 >= 24。
6
+ - Python >= 3.9。
7
+ - mock 模式:只需 numpy >= 1.23、h5py >= 3.8;
8
+ - 真机模式:用 labscript-suite 的 Python 环境(labscript、runmanager、
9
+ labscript_utils、zprocess 可导入)。
10
+ - 开发时实测:Python 3.11.0、numpy 2.2.5、h5py 3.13.0。
11
+
12
+ ## 1. 安装插件
13
+
14
+ 把仓库放到本机任意目录(下称 `<repo>`),在 DSH desktop profile 里 link 安装:
15
+
16
+ 1. 编辑 `%USERPROFILE%\.dsh\profiles\desktop\package.json`:
17
+
18
+ "dependencies": {
19
+ ...,
20
+ "dsh-labscript": "link:<repo>"
21
+ }
22
+
23
+ 并在 `dsh.profile.bundles` 里加入 `"dsh-labscript"`。
24
+
25
+ 2. 在 profile 目录执行 `pnpm install`,重启 DSH。
26
+ 3. 新会话里应出现 9 个 `labs_*` 工具和 `labscript` skill。
27
+
28
+ 实验机建议固定 tag:`git checkout v0.2.0` 后再安装,避免 main 变动影响硬件行为。
29
+
30
+ ## 2. 配置
31
+
32
+ 优先在 DSH 里配置:侧边栏 → 插件 → dsh-labscript,填 backend、python、
33
+ runmanager host/port、protocolsDir,保存后下一次工具调用生效(不用重启)。
34
+
35
+ 高级配置(scriptsFile、hard limits、postprocess、approval、pathMap、超时等)
36
+ 仍写在 profile 的 `cordis.patch.yml`:
37
+
38
+ - id: dsh-labscript
39
+ name: dsh-labscript
40
+ config:
41
+ scriptsFile: <path>/scripts.json
42
+ hardLimitsFile: <path>/hard_limits.json
43
+ postprocess:
44
+ entry: my_lab.analysis:analyze
45
+ pythonPaths: [<path>/my_lab]
46
+ approval: { engage: true, protocolRun: true }
47
+
48
+ ## 3. 冒烟顺序
49
+
50
+ 1. `labs_doctor`:确认 python/依赖/路径/版本身份;
51
+ 2. mock:`labs_status` → `labs_protocol list` → `labs_protocol run demo_scan` → `labs_wait`;
52
+ 3. 真机:只读 `labs_status` → 单键 `labs_set` + `labs_restore` → ≤10 shots →
53
+ `labs_data` → 一条协议。
54
+
@@ -0,0 +1,57 @@
1
+ # dsh-labscript(中文说明)
2
+
3
+ 在 DeepSeek Harness(DSH)里控制 labscript-suite 实验。只用 stock 接口:
4
+ runmanager remote(面板 globals、n_shots、engage、abort、run_shots、
5
+ error_in_globals)+ shot HDF5。不需要改 BLACS/lyse;实验室私有的分析、协议、
6
+ hard limits 全部通过配置接入。
7
+
8
+ ## 工具(9 个)
9
+
10
+ | 工具 | 作用 |
11
+ |---|---|
12
+ | labs_doctor | 只读诊断:python、numpy/h5py、labscript/runmanager、路径、审批、版本 |
13
+ | labs_status | 读面板:labscript、globals、n_shots、error_in_globals、run_shots、pending 进度 |
14
+ | labs_set | 写面板(标量 / raw 表达式 / reps),写前全量快照;不运行 |
15
+ | labs_engage | 提交当前面板,立即返回 n_shots_at_submit 与 expected_n_shots |
16
+ | labs_wait | 等待 pending run(可超时);若是协议则收尾:postprocess → finish 写回 → restore |
17
+ | labs_abort | 中止当前提交(不排队,不会被 labs_wait 堵住) |
18
+ | labs_data | 读 run 目录 HDF5:首发 globals、按扫描轴的逐发表格 |
19
+ | labs_protocol | list / show / run YAML 固定操作协议 |
20
+ | labs_restore | 用最近一次写前快照恢复面板(默认只恢复该次写过的键) |
21
+
22
+ 配套 runtime skill(labscript):先看现场、稳定流程优先用协议、提交后可以结束回合、
23
+ 同一时间只跑一个、不猜面板状态;新装或报配置错误时先跑 labs_doctor。
24
+
25
+ ## 安装与配置
26
+
27
+ - 要求:DSH >= 0.2、Node ^22.19/24;Python >= 3.9。mock 模式只要 numpy+h5py;
28
+ 真机用 labscript-suite 环境。详见 docs/INSTALL.zh.md。
29
+ - 默认 backend 是 mock-labscript;真机必须显式配置 backend: labscript。
30
+ - 安装后在 DSH 插件页填 backend、python、runmanager host/port、protocolsDir;
31
+ 其余高级配置(scriptsFile、hard limits、postprocess、approval、pathMap)写在
32
+ profile 的 cordis.patch.yml。
33
+
34
+ ## 协议 YAML
35
+
36
+ name: demo_scan
37
+ labscript: /path/to/sequence.py # 或 joint_script: 登记名
38
+ set: { do_acquire: true, amplitude: 0.5 }
39
+ scan: { duration_test_us: np.linspace(0, 8.0, 5) } # 表达式/数字/列表
40
+ reps: 2
41
+ postprocess: demo.peaks # 全局配置一次,协议里只写名字
42
+ finish:
43
+ restore: true
44
+ panel: { duration_test_us: fit.center }
45
+ history: { demo.peaks.center_us: fit.center }
46
+
47
+ postprocess 是后处理:跑完后调用配置的 Python 函数(entry + pythonPaths),
48
+ 返回 dict 供 finish 写回。旧协议里的 analysis: 字段当作 postprocess 别名。
49
+
50
+ ## 边界
51
+
52
+ - 真机 engage / protocol run 会请求 DSH 审批(一次),abort 和面板写入不请求;
53
+ 插件不是硬件联锁。
54
+ - labscript abort 只停止编译/提交新的 shot,已进 BLACS 队列或正在跑的 shot 不会被停。
55
+ - 完成判定基于 HDF5 数量、大小稳定和最后一发可读;BLACS paused 会报
56
+ compiled_not_executed 诊断。
57
+
@@ -0,0 +1,33 @@
1
+ # Two ways to configure dsh-labscript in a DSH profile.
2
+ #
3
+ # 1) Hardware-free development / examples (built-in simulator):
4
+ - id: dsh-labscript
5
+ name: dsh-labscript
6
+ config:
7
+ python: /path/to/python3
8
+ backend: mock-labscript
9
+ protocolsDir: /path/to/your/protocols
10
+ mock:
11
+ stateDir: ~/.dsh-labscript/mock
12
+ postprocess:
13
+ entry: postprocess_demo:analyze
14
+ pythonPaths: [/path/to/dsh-labscript/examples]
15
+
16
+ # 2) Real labscript-suite (adjust host/port to your runmanager):
17
+ # - id: dsh-labscript
18
+ # name: dsh-labscript
19
+ # config:
20
+ # python: /path/to/labscript-suite/python
21
+ # backend: labscript
22
+ # runmanager: { host: 127.0.0.1, port: 42523, timeoutS: 10 }
23
+ # protocolsDir: /path/to/your/protocols
24
+ # scriptsFile: /path/to/scripts.json
25
+ # hardLimitsFile: /path/to/hard_limits.json
26
+ # calibrationLog: /path/to/calibration.jsonl
27
+ # pathMap:
28
+ # - { remotePrefix: "E:/Experiments", localPrefix: "//lab-pc/Experiments" }
29
+ # reps: { global: dummy_iterator }
30
+ # postprocess:
31
+ # entry: my_lab.analysis:analyze
32
+ # pythonPaths: [/path/to/my_lab]
33
+ # approval: { engage: true, protocolRun: true }
@@ -0,0 +1,19 @@
1
+ """Example postprocess entry: after a run, return the numbers that finish.panel
2
+ and finish.history write back. Replace the fake model with your lab analysis.
3
+
4
+ Signature: analyze(run_folder=..., scan_global=..., reps=..., name=...)
5
+ Returns a JSON-serializable dict.
6
+ """
7
+
8
+
9
+ def analyze(run_folder, scan_global=None, reps=None, name=None):
10
+ # Placeholder numbers so the write-back path is demonstrable end to end.
11
+ return {
12
+ "fit": {"center": 2.5, "pi_duration_us": 1.25, "r2": 0.99},
13
+ "meta": {
14
+ "run_folder": run_folder,
15
+ "scan_global": scan_global,
16
+ "reps": reps,
17
+ "name": name,
18
+ },
19
+ }
@@ -0,0 +1,18 @@
1
+ # Demo fixed-operation protocol. It runs against the built-in mock-labscript
2
+ # backend without hardware; replace labscript with your sequence path and the
3
+ # panel keys with your own globals for real experiments.
4
+ name: demo_scan
5
+ labscript: /path/to/your/sequence.py
6
+ set:
7
+ do_acquire: true
8
+ amplitude: 0.5
9
+ scan:
10
+ duration_test_us: np.linspace(0.0, 8.0, 5)
11
+ reps: 2
12
+ postprocess: demo.peaks
13
+ finish:
14
+ restore: true
15
+ panel:
16
+ duration_test_us: fit.center
17
+ history:
18
+ demo.peaks.center_us: fit.center
package/lib/client.js ADDED
@@ -0,0 +1,193 @@
1
+ /**
2
+ * dsh-labscript -- browser half.
3
+ *
4
+ * A pre-built client bundle: DSH serves it as-is and expects the
5
+ * window.__ModuleLoader__.load({ id, factory }) wrapper. It registers one small
6
+ * configuration page into the official Plugins page (plugins.bundle.config).
7
+ * The page only edits backend, python, runmanager host/port and protocolsDir;
8
+ * everything else stays in the profile config. It talks to this plugin's own
9
+ * routes under /dsh-labscript/api (GET/POST config, POST doctor).
10
+ */
11
+ window.__ModuleLoader__.load({
12
+ id: 'dsh-labscript',
13
+ factory: (require) => {
14
+ const module = { exports: {} }
15
+ const exports = module.exports
16
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
17
+
18
+ const React = require('react')
19
+ const h = React.createElement
20
+ const { useEffect, useState } = React
21
+
22
+ const API = '/dsh-labscript/api'
23
+ const STYLE_ID = 'dsh-labscript/plugin-config.css'
24
+
25
+ const CSS = [
26
+ '.lsc{max-width:720px;display:flex;flex-direction:column;gap:12px;font-size:13px;line-height:20px}',
27
+ '.lsc-section{border:1px solid #d9d9d9;border-radius:8px;padding:12px;display:flex;flex-direction:column;gap:8px}',
28
+ '.lsc-section h3{margin:0;font-size:14px;font-weight:600}',
29
+ '.lsc-row{display:flex;flex-direction:column;gap:4px}',
30
+ '.lsc label{font-weight:500}',
31
+ '.lsc input,.lsc select{width:100%;box-sizing:border-box;padding:6px 8px;border-radius:6px;border:1px solid #d9d9d9;background:#fff;color:inherit;font:inherit}',
32
+ '.lsc .hint{color:#8a8a8a;font-size:12px;line-height:18px}',
33
+ '.lsc-actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center}',
34
+ '.lsc button{padding:5px 12px;border-radius:6px;border:1px solid #d9d9d9;background:#f5f5f5;color:inherit;cursor:pointer;font:inherit}',
35
+ '.lsc button.primary{background:#2b6cb0;border-color:transparent;color:#fff}',
36
+ '.lsc button:disabled{opacity:.55;cursor:not-allowed}',
37
+ '.lsc-msg{padding:8px 10px;border-radius:6px;white-space:pre-wrap;font-size:12px;line-height:18px}',
38
+ '.lsc-msg.ok{background:rgba(22,163,74,.1);color:#16a34a}',
39
+ '.lsc-msg.err{background:rgba(220,38,38,.1);color:#dc2626}',
40
+ '.lsc-check{display:flex;gap:8px;font-size:12px;line-height:18px}',
41
+ '.lsc-check b{min-width:88px;font-weight:600}',
42
+ '.lsc-check.ok b{color:#16a34a}',
43
+ '.lsc-check.warn b{color:#ea580c}',
44
+ '.lsc-check.fail b{color:#dc2626}',
45
+ ].join(String.fromCharCode(10))
46
+
47
+ function ensureStyle() {
48
+ if (document.getElementById(STYLE_ID) !== null) return
49
+ const style = document.createElement('style')
50
+ style.id = STYLE_ID
51
+ style.textContent = CSS
52
+ document.head.appendChild(style)
53
+ }
54
+
55
+ async function apiFetch(path, options) {
56
+ const response = await fetch(API + path, Object.assign({ headers: { 'Content-Type': 'application/json' } }, options || {}))
57
+ let payload
58
+ try {
59
+ payload = await response.json()
60
+ } catch (error) {
61
+ throw new Error('接口返回了非 JSON 内容(HTTP ' + response.status + ')')
62
+ }
63
+ if (!response.ok || payload.ok === false) throw new Error(payload.error || ('请求失败(HTTP ' + response.status + ')'))
64
+ return payload
65
+ }
66
+
67
+ function Row(props) {
68
+ return h('div', { className: 'lsc-row' },
69
+ h('label', null, props.label),
70
+ props.children,
71
+ props.hint === undefined ? null : h('span', { className: 'hint' }, props.hint),
72
+ )
73
+ }
74
+
75
+ function ConfigPage() {
76
+ const [loading, setLoading] = useState(true)
77
+ const [configFile, setConfigFile] = useState('')
78
+ const [version, setVersion] = useState('')
79
+ const [backend, setBackend] = useState('mock-labscript')
80
+ const [python, setPython] = useState('')
81
+ const [host, setHost] = useState('127.0.0.1')
82
+ const [port, setPort] = useState(42523)
83
+ const [protocolsDir, setProtocolsDir] = useState('')
84
+ const [busy, setBusy] = useState('')
85
+ const [message, setMessage] = useState(null)
86
+ const [checks, setChecks] = useState(null)
87
+
88
+ const load = async () => {
89
+ setLoading(true)
90
+ try {
91
+ const payload = await apiFetch('/config')
92
+ setVersion(payload.version || '')
93
+ setConfigFile(payload.configFile || '')
94
+ const current = payload.config || {}
95
+ setBackend(current.backend || 'mock-labscript')
96
+ setPython(current.python || '')
97
+ setHost((current.runmanager && current.runmanager.host) || '127.0.0.1')
98
+ setPort((current.runmanager && current.runmanager.port) || 42523)
99
+ setProtocolsDir(current.protocolsDir || '')
100
+ } catch (error) {
101
+ setMessage({ kind: 'err', text: error.message })
102
+ } finally {
103
+ setLoading(false)
104
+ }
105
+ }
106
+
107
+ useEffect(() => {
108
+ ensureStyle()
109
+ load()
110
+ }, [])
111
+
112
+ const save = async () => {
113
+ setBusy('save')
114
+ setMessage(null)
115
+ try {
116
+ await apiFetch('/config', {
117
+ method: 'POST',
118
+ body: JSON.stringify({
119
+ backend,
120
+ python,
121
+ runmanager: { host, port: Number(port) },
122
+ protocolsDir,
123
+ }),
124
+ })
125
+ setMessage({ kind: 'ok', text: '已保存;下一次工具调用生效,不需要重启。' })
126
+ } catch (error) {
127
+ setMessage({ kind: 'err', text: error.message })
128
+ } finally {
129
+ setBusy('')
130
+ }
131
+ }
132
+
133
+ const doctor = async () => {
134
+ setBusy('doctor')
135
+ setMessage(null)
136
+ setChecks(null)
137
+ try {
138
+ const payload = await apiFetch('/doctor', { method: 'POST', body: '{}' })
139
+ setChecks(payload.checks || [])
140
+ } catch (error) {
141
+ setMessage({ kind: 'err', text: error.message })
142
+ } finally {
143
+ setBusy('')
144
+ }
145
+ }
146
+
147
+ const checkRows = (checks || []).map((check, index) => h('div', { key: index, className: 'lsc-check ' + check.status },
148
+ h('b', null, check.status),
149
+ h('span', null, check.name + ': ' + check.detail + (check.hint ? ' -- ' + check.hint : '')),
150
+ ))
151
+
152
+ return h('div', { className: 'lsc' },
153
+ h('div', null, h('h3', null, 'dsh-labscript'), h('span', { className: 'hint' }, 'v' + version + (configFile ? ' · ' + configFile : ''))),
154
+ h('section', { className: 'lsc-section' },
155
+ h('h3', null, '连接'),
156
+ h(Row, { label: 'backend' },
157
+ h('select', { value: backend, onChange: (event) => setBackend(event.target.value) },
158
+ h('option', { value: 'mock-labscript' }, 'mock-labscript(模拟器)'),
159
+ h('option', { value: 'labscript' }, 'labscript(真实硬件)'),
160
+ ),
161
+ ),
162
+ h(Row, { label: 'python', hint: '真机填 labscript-suite 环境的 python;模拟器只需 numpy+h5py。' }, h('input', { type: 'text', value: python, onChange: (event) => setPython(event.target.value) })),
163
+ h(Row, { label: 'runmanager host' }, h('input', { type: 'text', value: host, onChange: (event) => setHost(event.target.value) })),
164
+ h(Row, { label: 'runmanager port' }, h('input', { type: 'number', value: port, onChange: (event) => setPort(event.target.value) })),
165
+ ),
166
+ h('section', { className: 'lsc-section' },
167
+ h('h3', null, '实验'),
168
+ h(Row, { label: 'protocolsDir', hint: '协议 YAML 目录;留空则协议列表为空。' }, h('input', { type: 'text', value: protocolsDir, onChange: (event) => setProtocolsDir(event.target.value) })),
169
+ h('span', { className: 'hint' }, 'scriptsFile、hard limits、postprocess、approval、pathMap 等高级配置仍写在 profile 的 cordis.patch.yml。'),
170
+ ),
171
+ h('div', { className: 'lsc-actions' },
172
+ h('button', { className: 'primary', disabled: loading || busy !== '', onClick: save }, busy === 'save' ? '保存中…' : '保存'),
173
+ h('button', { disabled: loading || busy !== '', onClick: doctor }, busy === 'doctor' ? '诊断中…' : '运行诊断'),
174
+ ),
175
+ message === null ? null : h('div', { className: 'lsc-msg ' + message.kind }, message.text),
176
+ checks === null ? null : h('div', { className: 'lsc-section' }, h('h3', null, '诊断结果'), checkRows),
177
+ )
178
+ }
179
+
180
+ function apply(ctx) {
181
+ ctx.effect(
182
+ () => ctx.slots.inject('plugins.bundle.config', () =>
183
+ ctx.slots.register({ name: 'plugins.bundle.config', key: 'dsh-labscript' }, ConfigPage)),
184
+ 'dsh-labscript: plugin configuration page',
185
+ )
186
+ }
187
+
188
+ exports.apply = apply
189
+ exports.inject = ['slots']
190
+ exports.ConfigPage = ConfigPage
191
+ return module.exports
192
+ },
193
+ })