embedded-lab-control 0.2.0__py3-none-any.whl

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.
Files changed (64) hide show
  1. embedded_lab_control-0.2.0.dist-info/METADATA +855 -0
  2. embedded_lab_control-0.2.0.dist-info/RECORD +64 -0
  3. embedded_lab_control-0.2.0.dist-info/WHEEL +5 -0
  4. embedded_lab_control-0.2.0.dist-info/entry_points.txt +2 -0
  5. embedded_lab_control-0.2.0.dist-info/licenses/LICENSE +21 -0
  6. embedded_lab_control-0.2.0.dist-info/top_level.txt +1 -0
  7. labctl/__init__.py +8 -0
  8. labctl/actuators/__init__.py +56 -0
  9. labctl/actuators/base.py +183 -0
  10. labctl/actuators/lcus1.py +232 -0
  11. labctl/actuators/mock.py +81 -0
  12. labctl/actuators/runtime.py +1178 -0
  13. labctl/cli.py +7485 -0
  14. labctl/core/__init__.py +36 -0
  15. labctl/core/admin_files.py +388 -0
  16. labctl/core/audit.py +269 -0
  17. labctl/core/claim_keepalive.py +342 -0
  18. labctl/core/config.py +1068 -0
  19. labctl/core/database.py +880 -0
  20. labctl/core/images.py +503 -0
  21. labctl/core/manager.py +2815 -0
  22. labctl/core/models.py +815 -0
  23. labctl/core/safe_open.py +109 -0
  24. labctl/extras.py +128 -0
  25. labctl/health/__init__.py +43 -0
  26. labctl/health/alerts.py +275 -0
  27. labctl/health/checks.py +533 -0
  28. labctl/health/daemon.py +397 -0
  29. labctl/mcp_server.py +5254 -0
  30. labctl/power/__init__.py +13 -0
  31. labctl/power/base.py +145 -0
  32. labctl/power/kasa.py +446 -0
  33. labctl/power/shelly.py +89 -0
  34. labctl/power/tasmota.py +95 -0
  35. labctl/sdwire/__init__.py +10 -0
  36. labctl/sdwire/controller.py +1116 -0
  37. labctl/sdwire/power_guard.py +190 -0
  38. labctl/serial/__init__.py +18 -0
  39. labctl/serial/boot_test.py +342 -0
  40. labctl/serial/capture.py +430 -0
  41. labctl/serial/keys.py +89 -0
  42. labctl/serial/proxy.py +802 -0
  43. labctl/serial/ser2net.py +338 -0
  44. labctl/serial/udev.py +273 -0
  45. labctl/services.py +210 -0
  46. labctl/web/__init__.py +9 -0
  47. labctl/web/activity_broadcaster.py +204 -0
  48. labctl/web/api.py +1003 -0
  49. labctl/web/app.py +202 -0
  50. labctl/web/auth.py +97 -0
  51. labctl/web/claims.py +232 -0
  52. labctl/web/static/css/style.css +477 -0
  53. labctl/web/static/js/app.js +76 -0
  54. labctl/web/templates/activity.html +202 -0
  55. labctl/web/templates/base.html +49 -0
  56. labctl/web/templates/claim_refused.html +38 -0
  57. labctl/web/templates/console.html +237 -0
  58. labctl/web/templates/dashboard.html +100 -0
  59. labctl/web/templates/login.html +25 -0
  60. labctl/web/templates/sbc_detail.html +306 -0
  61. labctl/web/templates/settings.html +78 -0
  62. labctl/web/templates/status_history.html +65 -0
  63. labctl/web/views.py +496 -0
  64. labctl/web/websocket.py +208 -0
@@ -0,0 +1,855 @@
1
+ Metadata-Version: 2.4
2
+ Name: embedded-lab-control
3
+ Version: 0.2.0
4
+ Summary: Centralized management system for embedded development lab resources
5
+ Author: John Jezl
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/johnjezl/Embedded-Lab-Control
8
+ Project-URL: Repository, https://github.com/johnjezl/Embedded-Lab-Control
9
+ Project-URL: Issues, https://github.com/johnjezl/Embedded-Lab-Control/issues
10
+ Project-URL: Changelog, https://github.com/johnjezl/Embedded-Lab-Control/blob/main/CHANGELOG.md
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: System :: Hardware
21
+ Classifier: Topic :: Utilities
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: click>=8.0
26
+ Requires-Dist: pyyaml>=6.0
27
+ Requires-Dist: requests>=2.28
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=7.0; extra == "dev"
30
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
31
+ Requires-Dist: black==26.5.1; extra == "dev"
32
+ Requires-Dist: isort==9.0.1; extra == "dev"
33
+ Requires-Dist: flake8==7.4.1; extra == "dev"
34
+ Provides-Extra: web
35
+ Requires-Dist: flask>=3.0; extra == "web"
36
+ Requires-Dist: flask-socketio>=5.0; extra == "web"
37
+ Provides-Extra: mcp
38
+ Requires-Dist: mcp<3,>=2.2; extra == "mcp"
39
+ Provides-Extra: sdwire
40
+ Requires-Dist: sdwire>=0.3; python_version >= "3.12" and extra == "sdwire"
41
+ Provides-Extra: kasa
42
+ Requires-Dist: python-kasa>=0.12; python_version >= "3.11" and extra == "kasa"
43
+ Requires-Dist: python-kasa<0.8,>=0.7.7; python_version < "3.11" and extra == "kasa"
44
+ Provides-Extra: actuators
45
+ Requires-Dist: pyserial>=3.5; extra == "actuators"
46
+ Dynamic: license-file
47
+
48
+ # Embedded Lab Control (labctl)
49
+
50
+ Centralized management system for embedded development lab resources. Provides deterministic USB serial access, power control, and resource tracking for multiple single-board computers (SBCs).
51
+
52
+ ## Features
53
+
54
+ - **Deterministic USB Serial Naming** - udev rules create stable `/dev/lab/<sbc>` symlinks
55
+ - **Remote Serial Console** - TCP access via ser2net with multi-client support
56
+ - **Smart Plug Power Control** - Supports Tasmota, Kasa, and Shelly devices
57
+ - **Web Dashboard** - Browser-based monitoring and control
58
+ - **REST API** - Programmatic access to all features
59
+ - **Health Monitoring** - Automated ping, serial, and power checks
60
+ - **Activity Stream** - Unified audit trail with CLI tailing, live web feed, and API queries
61
+ - **Hardware Claims** - Exclusive-access coordination for long-running SBC workflows
62
+ - **Session Logging** - Capture serial output with rotation and compression
63
+ - **Authentication** - Session-based web login and API key auth (optional, disabled by default)
64
+ - **Claude Code Skill** - an example `/deploy-and-test` skill orchestrates build, deploy, boot, and serial capture over MCP ([install](#claude-code-skill-deploy-and-test))
65
+
66
+ ## Installation
67
+
68
+ ### Prerequisites
69
+
70
+ - Linux, Python 3.10+. Some extras need newer Python: Kasa devices that use
71
+ KLAP v2 authentication need 3.11+ (`python-kasa` 0.12), and the `sdwire`
72
+ extra installs nothing below 3.12 (SDWire commands then say so).
73
+ - `ser2net` for remote serial consoles (`sudo apt install ser2net`).
74
+
75
+ ### Install from PyPI
76
+
77
+ The distribution is `embedded-lab-control`; the command is `labctl`.
78
+ With [pipx](https://pipx.pypa.io/) (its own virtual environment, `labctl`
79
+ on your `PATH`):
80
+
81
+ ```bash
82
+ pipx install "embedded-lab-control[web,mcp,kasa,sdwire,actuators]"
83
+ ```
84
+
85
+ or with pip, in a virtual environment:
86
+
87
+ ```bash
88
+ python3 -m venv ~/.venvs/labctl
89
+ ~/.venvs/labctl/bin/pip install "embedded-lab-control[web,mcp,kasa,sdwire,actuators]"
90
+ ```
91
+
92
+ Pick the extras you need; the core package (SBC records, serial ports,
93
+ ser2net, Tasmota and Shelly plugs, claims) needs none:
94
+
95
+ | Extra | Enables | Installs |
96
+ |-------|---------|----------|
97
+ | `web` | `labctl web`: dashboard and REST API; `labctl user hash-password`, `add` and `verify` (password hashing for web logins and API-key users) | Flask, Flask-SocketIO |
98
+ | `mcp` | `labctl mcp`: MCP server for AI tools | `mcp` SDK 2.x |
99
+ | `kasa` | TP-Link Kasa plugs and strips | `python-kasa` (0.12+ on Python 3.11+, 0.7.7 on 3.10: no KLAP v2) |
100
+ | `sdwire` | SDWire SD card muxes (`labctl sdwire ...`) | `sdwire`, Python 3.12+ only |
101
+ | `actuators` | USB relay boards (LCUS-1) | `pyserial` |
102
+
103
+ A command whose extra is missing says which one to install, e.g.
104
+ `pip install "embedded-lab-control[web]"`, or with pipx,
105
+ `pipx inject embedded-lab-control "flask>=3.0" "flask-socketio>=5.0"`.
106
+
107
+ labctl runs on built-in defaults; to configure it, start from
108
+ [`config/labctl.yaml.example`](https://github.com/johnjezl/Embedded-Lab-Control/blob/main/config/labctl.yaml.example) (also in the
109
+ source distribution) saved as `~/.config/labctl/config.yaml` (see
110
+ [Config file search order](#config-file-search-order)).
111
+
112
+ The PyPI package is the CLI and services' code only. The shared,
113
+ system-wide install with systemd services (`scripts/install-services.sh`)
114
+ runs from a git checkout; see [Systemd Services](#systemd-services).
115
+
116
+ ### Install from source
117
+
118
+ ```bash
119
+ # System packages
120
+ sudo apt install python3-venv ser2net
121
+
122
+ # Clone repository
123
+ git clone https://github.com/johnjezl/Embedded-Lab-Control.git
124
+ cd Embedded-Lab-Control
125
+
126
+ # Create virtual environment
127
+ python3 -m venv .venv
128
+ source .venv/bin/activate
129
+
130
+ # Install with the extras you need (see the table above)
131
+ pip install -e ".[web,mcp,kasa,sdwire,actuators]"
132
+
133
+ # Copy example config (a personal config; see "Config file search order")
134
+ mkdir -p ~/.config/labctl
135
+ cp config/labctl.yaml.example ~/.config/labctl/config.yaml
136
+ ```
137
+
138
+ ### Serial device names
139
+
140
+ Deterministic `/dev/lab/<name>` serial names come from udev rules generated
141
+ from the adapters you register:
142
+
143
+ ```bash
144
+ labctl serial discover # find adapters and their USB paths
145
+ labctl serial add board1-console 1-2.1 # name the adapter on USB path 1-2.1
146
+ labctl serial udev > 99-lab-serial.rules # generate the rules
147
+ cat 99-lab-serial.rules # check: rules, not a message
148
+ sudo install -m 644 99-lab-serial.rules /etc/udev/rules.d/
149
+ sudo udevadm control --reload-rules && sudo udevadm trigger
150
+ ```
151
+
152
+ The devices are `root:dialout 0660`: join `dialout` to open them directly
153
+ (`sudo usermod -aG dialout $USER`, then log in again). On a system-wide
154
+ install, `install-services.sh` makes the rules file writable by the
155
+ `labctl` group (`root:labctl 0664`), and members of that group can update
156
+ it with `labctl serial udev --install --reload`, without `sudo`; on such a
157
+ host use that instead of the `sudo install` above, which resets the file
158
+ to `root:root 0644`. Prefer `--install` wherever you can write the file:
159
+ it refuses to remove or re-point a link a board claimed by someone else
160
+ uses, while installing a file by hand checks nothing, so look at
161
+ `labctl claims list` first.
162
+
163
+ [docs/HARDWARE_MAP.md](https://github.com/johnjezl/Embedded-Lab-Control/blob/main/docs/HARDWARE_MAP.md) walks through finding USB
164
+ paths, registering adapters, installing the rules and troubleshooting.
165
+
166
+ ### Config file search order
167
+
168
+ labctl uses the first of these that loads. A file in the search path that
169
+ exists but can't be loaded (bad YAML or a bad value) is skipped with a
170
+ warning, and the next one is used; a file named with `-c` or
171
+ `$LABCTL_CONFIG` is refused instead. `labctl web` and `labctl mcp --http`
172
+ refuse to start on any config that didn't load.
173
+
174
+ 1. `-c/--config PATH` (only that file)
175
+ 2. `$LABCTL_CONFIG`
176
+ 3. `~/.config/labctl/config.yaml` (or `$XDG_CONFIG_HOME/labctl/config.yaml`)
177
+ 4. `/etc/labctl/config.yaml`
178
+ 5. Built-in defaults
179
+
180
+ > **Warning:** a personal `~/.config/labctl/config.yaml` wins over the shared
181
+ > `/etc/labctl/config.yaml`. On a system install, a personal config left over
182
+ > from a personal install silently points labctl at a different database than the
183
+ > services use. Remove it, or pass `-c /etc/labctl/config.yaml`. Either way,
184
+ > you must be in the `labctl` group: `install-services.sh` makes
185
+ > `/etc/labctl` readable only by it (`sudo usermod -aG labctl $USER`, then
186
+ > log in again). Without that, labctl can't read the shared config and falls
187
+ > back to the built-in defaults and a personal database. To write the
188
+ > services' database too, its file and directory must be writable by the
189
+ > group. A new install's database (`/var/lib/labctl/db/labctl.db`) is; move
190
+ > an older one there (see [Moving the database](#moving-the-database)), or
191
+ > run commands as `sudo -u labctl labctl ...`.
192
+
193
+ ## Quick Start
194
+
195
+ ```bash
196
+ # Add an SBC
197
+ labctl add rpi4 --description "Raspberry Pi 4"
198
+
199
+ # Assign its console serial port (type: console, jtag or debug)
200
+ labctl port assign rpi4 console /dev/lab/rpi4 --baud 115200
201
+
202
+ # Assign a power plug (Tasmota example)
203
+ labctl plug assign rpi4 tasmota 192.168.1.100
204
+
205
+ # View status
206
+ labctl status
207
+
208
+ # Connect to serial console
209
+ labctl console rpi4
210
+
211
+ # Control power
212
+ labctl power on rpi4
213
+ labctl power cycle rpi4 --delay 5
214
+
215
+ # Start web dashboard
216
+ labctl web --port 5000
217
+ ```
218
+
219
+ ## CLI Reference
220
+
221
+ Every command has `--help`. This section lists the commands; see
222
+ [docs/OPERATIONS.md](https://github.com/johnjezl/Embedded-Lab-Control/blob/main/docs/OPERATIONS.md) for what each one changes.
223
+
224
+ ### Global Options
225
+
226
+ | Option | Description |
227
+ |--------|-------------|
228
+ | `-v`, `--verbose` | Enable verbose (DEBUG) output |
229
+ | `-q`, `--quiet` | Suppress non-essential output |
230
+ | `-d`, `--delay <seconds>` | Wait before executing the command (put it before the command name) |
231
+ | `-c`, `--config <path>` | Use this config file only (see [search order](#config-file-search-order)) |
232
+ | `--version` | Show the version and exit |
233
+
234
+ ### Common Flags
235
+
236
+ | Flag | Description |
237
+ |------|-------------|
238
+ | `-y`, `--yes` | Skip the confirmation on destructive commands. The question is asked only when stdin is a terminal, so cron, CI, pipes and agents are never prompted. `remove`, `power-all`, `sdwire remove`, `serial remove` and `actuator remove` also ask on a terminal, but without one they refuse unless `-y` is given. |
239
+ | `--override-claim` | Act on a board another user has claimed (or that shares hardware with one). The override is recorded in the activity log. Without it, such commands refuse. |
240
+ | `--dry-run` | On `power cycle`, `sdwire flash` and `sdwire update`: print the plan and change nothing |
241
+
242
+ ### SBC Management
243
+
244
+ | Command | Description |
245
+ |---------|-------------|
246
+ | `labctl add <name>` | Add a new SBC |
247
+ | `labctl remove <name>` | Remove an SBC |
248
+ | `labctl list` | List all SBCs |
249
+ | `labctl info <name>` | Show SBC details |
250
+ | `labctl edit <name>` | Edit SBC properties (--rename, --project, etc.) |
251
+ | `labctl edit <name> --cycle-delay N` | Per-SBC off→on delay in seconds (0–86400); the default and minimum for every power cycle of the board: `power cycle`, the reboot after `sdwire flash`/`update`, `boot-test`, recovery, and MCP/REST/web cycles. Without one, 3 s is the default and minimum. Boards sharing a plug outlet are cycled together, so the largest of their minimums applies, a board without a stored delay counting as 3 s (negative clears it) |
252
+ | `labctl network set <sbc> ethernet\|wifi <ip>` | Set an SBC's IP address (`-m` MAC, `-h` hostname); `network remove` deletes it |
253
+ | `labctl ssh <sbc>` | SSH to the SBC's configured IP (`-u` user) |
254
+ | `labctl status` | Show status overview |
255
+ | `labctl status --watch` | Continuous status monitoring |
256
+ | `labctl status --fast` | Skip live power probes; show the monitor daemon's cached power state |
257
+ | `labctl export [-f yaml\|json] [-o file]` | Export SBCs with their ports, network addresses, plugs and SDWires, plus serial adapters |
258
+ | `labctl import <file> [--update]` | Import an export file (skips existing SBCs unless `--update`) |
259
+
260
+ ### Activity Stream
261
+
262
+ | Command | Description |
263
+ |---------|-------------|
264
+ | `labctl activity tail` | Show recent activity events |
265
+ | `labctl activity tail --follow` | Stream new activity events by polling the database |
266
+ | `labctl activity tail --actor <actor>` | Filter events by actor |
267
+ | `labctl activity tail --source <source>` | Filter events by source (`cli`, `mcp`, `api`, `web`, `daemon`, `internal`) |
268
+ | `labctl activity tail --result <result>` | Filter events by result (`ok`, `error`, `forbidden`) |
269
+ | `labctl activity tail --sbc <name>` | Filter events for one SBC |
270
+ | `labctl activity export --format ndjson` | Export activity events as NDJSON |
271
+
272
+ ### Claims (Exclusive Access Coordination)
273
+
274
+ | Command | Description |
275
+ |---------|-------------|
276
+ | `labctl claim <sbc> -r "<reason>" [-d 2h]` | Claim exclusive access to an SBC (`--reason` is required) |
277
+ | `labctl release <sbc>` | Release your active claim |
278
+ | `labctl renew <sbc>` | Extend your active claim |
279
+ | `labctl force-release <sbc> -r "<reason>"` | Override: release the active claim, whoever holds it (yours too) |
280
+ | `labctl request-release <sbc> -r "<reason>"` | Politely ask the claimant to release |
281
+ | `labctl claims list` | List all active claims |
282
+ | `labctl claims show <sbc>` | Show the active claim on one SBC |
283
+ | `labctl claims history <sbc>` | Show released claim history for one SBC |
284
+ | `labctl claims expire` | Run one expiry/dead-session sweep (also prunes old released claims) |
285
+ | `labctl claims stats` | Show aggregate claim statistics |
286
+
287
+ A claim's deadline moves each time you use the board. Long operations
288
+ keep a claim you already hold alive while they run (CLI `boot-test`; MCP
289
+ `boot_test`, `serial_capture`, `sdwire_update`, `flash_image`) and never
290
+ claim a board for you. A boot test stops between runs once your claim is
291
+ lost or someone else claims the board, reporting `Result: INCOMPLETE,
292
+ ...` (or `Result: STOPPED before the first run ...`) instead of a pass
293
+ rate; claim it for the whole test.
294
+
295
+ ### SDWire (SD Card Multiplexer)
296
+
297
+ | Command | Description |
298
+ |---------|-------------|
299
+ | `labctl sdwire discover` | Scan for connected SDWire devices |
300
+ | `labctl sdwire add <name> <serial>` | Register an SDWire device |
301
+ | `labctl sdwire remove <name>` | Unregister an SDWire device |
302
+ | `labctl sdwire list` | List all registered SDWire devices |
303
+ | `labctl sdwire assign <sbc> <device>` | Assign SDWire to an SBC |
304
+ | `labctl sdwire unassign <sbc>` | Remove SDWire assignment |
305
+ | `labctl sdwire dut <sbc>` | Switch SD card to SBC (boot from SD) |
306
+ | `labctl sdwire host <sbc>` | Switch SD card to host (for flashing). Only when the plug reports the board off: refuses when it's on, has no plug, or its state can't be read (`--force` skips the check) |
307
+ | `labctl sdwire flash <sbc> <image>` | Power off, flash full image to SD card, power-cycle (`--no-reboot` leaves the SBC off). A board with no power plug is refused unless `--no-plug-ok` (you've made sure it is off) |
308
+ | `labctl sdwire update <sbc> -p N -c src:dest -r old:new -d file` | Power off, then copy, rename, or delete files on a partition (`--reboot` to power-cycle; without it the SBC is left off). No plug: as `flash` (`--no-plug-ok`) |
309
+ | `labctl sdwire ls <sbc> -p N [--path /] [--recursive] [--force]` | List directory contents on a partition. A card already on the host is read in place; otherwise the board must be confirmed off, as for `sdwire host` (`--force` skips the check) |
310
+ | `labctl sdwire cat <sbc> -p N --path /file [--encoding text|base64|hex] [--force]` | Read a file from a partition (as `ls`) |
311
+ | `labctl sdwire info <sbc> [--force]` | Show partition table and filesystem metadata (as `ls`) |
312
+
313
+ Partition numbers are 1-based (e.g., `-p 1` for the first partition, which maps to `/dev/sdb1`).
314
+ `ls`, `cat` and `info` borrow the card: they switch it to the host and back,
315
+ and refuse while the SBC is powered on.
316
+
317
+ SDWire operations shell out (via `sudo`) to `mount`, `umount`, `dd`, `sync`,
318
+ `partprobe`, `parted` and `blkid`. They do not use `sd-mux-ctrl`, and they do
319
+ not require a fixed privileged mountpoint such as `/mnt/sdwire`.
320
+
321
+ To reduce friction for interactive use and MCP/service flows, add a narrowly
322
+ scoped sudoers rule. Use its own file: `scripts/install-services.sh` rewrites
323
+ `/etc/sudoers.d/labctl`.
324
+ ```bash
325
+ echo '<user> ALL=(root) NOPASSWD: /usr/bin/mount, /usr/bin/umount, /usr/bin/dd, /bin/dd, /usr/bin/sync, /bin/sync, /usr/sbin/partprobe, /sbin/partprobe, /usr/sbin/parted, /sbin/parted, /usr/sbin/blkid, /sbin/blkid' | sudo tee /etc/sudoers.d/labctl-sdwire
326
+ sudo chmod 440 /etc/sudoers.d/labctl-sdwire
327
+ ```
328
+
329
+ Keep this scoped to the exact commands above. `NOPASSWD` sudo is still privileged;
330
+ the point here is to allow SDWire workflows without granting broader root access.
331
+
332
+ ### Serial Device Management
333
+
334
+ | Command | Description |
335
+ |---------|-------------|
336
+ | `labctl serial discover` | Scan for connected USB-serial adapters |
337
+ | `labctl serial add <name> <usb_path>` | Register a USB-serial adapter |
338
+ | `labctl serial remove <name>` | Unregister an adapter (refused when a board someone else has claimed has a port on it; `--override-claim`) |
339
+ | `labctl serial list` | List all registered adapters |
340
+ | `labctl serial rename <name> <new>` | Rename an adapter |
341
+ | `labctl serial udev --install --reload` | Generate and apply udev rules (refused when they would remove or re-point a `/dev/lab` link a board someone else has claimed uses; `--override-claim`) |
342
+ | `labctl serial repair [--apply]` | Re-link port assignments to registered adapters (dry run by default) |
343
+ | `labctl serial capture <port> --timeout N --until <pattern>` | Capture serial output |
344
+ | `labctl serial send <port> <data> --capture N` | Send data (plus CRLF unless `--raw`) to a serial port |
345
+ | `labctl ser2net generate [--install] [--override-claim]` | Generate the ser2net config from the database. Without `--install`, ports with no TCP port are left out and the port each would get is shown; nothing is saved. `--install` first assigns and saves a TCP port for each such port (with `port assign`'s claim checks), then writes `ser2net.config_file` (default `/etc/ser2net.yaml`): atomically when labctl can create files in its directory (e.g. with sudo), otherwise rewritten in place. It refuses, saving and writing nothing, if such a port's board (or one sharing its console device) is claimed by another user, unless `--override-claim`; a port whose save fails is left out of the config and the command exits 1 |
346
+ | `labctl ser2net reload` | Restart the ser2net service |
347
+
348
+ ### Boot Testing
349
+
350
+ | Command | Description |
351
+ |---------|-------------|
352
+ | `labctl boot-test <sbc> -i <image> -d <dest> -e <pattern> -r N` | Deploy a file to the SD card, then power-cycle N times and count boots that print the pattern |
353
+ | `labctl boot-test <sbc> --no-deploy -e <pattern> -r N` | Same, without deploying |
354
+
355
+ ### Serial Console
356
+
357
+ | Command | Description |
358
+ |---------|-------------|
359
+ | `labctl port assign <sbc> <type> [device]` | Assign a `console`, `jtag` or `debug` port (`-a/--alias`, `-s/--serial-device`, `-t/--tcp-port`, `-b/--baud`) |
360
+ | `labctl port remove <sbc> <type>` | Remove a port assignment |
361
+ | `labctl port list` | List port assignments |
362
+ | `labctl port list --unassigned` | Show unassigned /dev/lab/* devices |
363
+ | `labctl ports [--all]` | List `/dev/lab` symlinks and their ser2net TCP ports; `--all` also lists USB serial devices (`/dev/ttyUSB*`, `/dev/ttyACM*`) no symlink points at |
364
+ | `labctl connect <alias\|sbc\|device>` | Connect by alias, SBC name, or device |
365
+ | `labctl console <sbc>` | Connect to serial console |
366
+ | `labctl log <sbc>` | Capture serial output to a log file until Ctrl+C (`-n N` to stop after N lines) |
367
+ | `labctl log <sbc> --follow` | Same, also echoing each line to the terminal |
368
+
369
+ ### Power Control
370
+
371
+ | Command | Description |
372
+ |---------|-------------|
373
+ | `labctl plug assign <sbc> <type> <addr>` | Assign power plug (--index for strips) |
374
+ | `labctl plug remove <sbc>` | Remove the plug assignment |
375
+ | `labctl power on <sbc>` | Turn on (sets pre_power relays first; see below) |
376
+ | `labctl power off <sbc>` | Turn off |
377
+ | `labctl power cycle <sbc> [--delay N] [--dry-run]` | Off, wait, set pre_power relays, on. The delay is 0–86400 s (negative is an error); the default and minimum are the SBC's `--cycle-delay`, otherwise 3 s, or a larger one of a board sharing its plug outlet (smaller values are raised to it, with a warning naming that board). |
378
+ | `labctl power status <sbc>` | Check power state |
379
+ | `labctl power-all on/off` | Control all SBCs (claimed boards are skipped) |
380
+
381
+ ### Relays, Bindings and Recovery
382
+
383
+ USB relays ("actuators") can drive board straps and buttons: recovery mode,
384
+ boot select, reset. An actuator channel is *bound* to an (SBC, purpose) pair.
385
+ Only the `lcus1_serial` driver is implemented so far; it needs the
386
+ `actuators` extra (pyserial).
387
+
388
+ | Command | Description |
389
+ |---------|-------------|
390
+ | `labctl actuator add <name> --driver lcus1_serial --device <path> --channels N` | Register a relay board |
391
+ | `labctl actuator list` / `probe <name>` / `remove <name>` | List, check reachability (read-only), or remove (with its bindings) |
392
+ | `labctl actuator set <name> <channel> open\|closed` | Drive a channel directly, for checking wiring |
393
+ | `labctl bind <sbc> <purpose> --actuator A --channel N --mode latch\|momentary [--phase pre-power]` | Bind a channel (`--pulse-ms` for momentary; `--active-when` for polarity) |
394
+ | `labctl unbind <sbc> <purpose>` | Remove a binding |
395
+ | `labctl bindings list [--target <sbc>]` / `status <sbc> <purpose>` | Show bindings and their desired vs. last state |
396
+ | `labctl bindings actuate\|release <sbc> <purpose>` | Assert or release a latch binding |
397
+ | `labctl bindings press <sbc> <purpose>` | Pulse a momentary binding |
398
+ | `labctl enter-recovery <sbc>` | Power off, wait, assert the `recovery_mode` binding, power on |
399
+ | `labctl exit-recovery <sbc>` | Same, releasing the strap for a normal boot |
400
+
401
+ **pre_power bindings** (`--phase pre-power`) are straps that must be in place
402
+ before the board gets power. Every power-on (`power on`, `power cycle`,
403
+ `power-all on`, the reboot after `sdwire flash`/`update`, `boot-test`, MCP and
404
+ REST) first checks those relays on the board and on every board sharing its
405
+ outlet. If a relay can't be reached, nothing is switched. labctl then drives
406
+ each relay to its desired state and only then applies power. If a strap fails
407
+ after a cycle's power-off, the boards are left off rather than booting
408
+ without it. There is no `--phase post-power` yet (bindings stored with it
409
+ by older versions are listed as inactive). When the
410
+ monitor daemon starts, it returns unheld relay channels to their default
411
+ state and leaves asserted bindings alone.
412
+
413
+ ### Multi-Client Access
414
+
415
+ Use `labctl proxy start <sbc>` when several people need to watch the same
416
+ console. The proxy keeps viewers attached across normal SBC restarts and is
417
+ view-only unless you opt in with `--allow-write`. While a proxy is active,
418
+ `labctl connect` and `labctl console` refuse direct access for that SBC so
419
+ people do not silently bypass the shared session.
420
+
421
+ | Command | Description |
422
+ |---------|-------------|
423
+ | `labctl proxy start <sbc>` | Start a shared serial-console proxy for one SBC; reconnects across SBC restarts and is read-only by default (`--allow-write` to enable input, `--exit-on-disconnect` to stop instead) |
424
+ | `labctl proxy list` | Explain the current shared-console proxy mode |
425
+ | `labctl sessions <sbc>` | Show shared-console clients once proxy daemon mode exists |
426
+
427
+ ### Health Monitoring
428
+
429
+ | Command | Description |
430
+ |---------|-------------|
431
+ | `labctl health-check` | Run all health checks |
432
+ | `labctl health-check --type ping` | Ping check only |
433
+ | `labctl health-check --sbc <name>` | Check single SBC |
434
+ | `labctl monitor` | Run the monitoring daemon in the foreground (`-i` interval, `--no-alerts`) |
435
+ | `labctl services status` | Show the state of `labctl-monitor`, `labctl-mcp`, `labctl-web` and `ser2net` (exit 1 if any is not running) |
436
+
437
+ ### User Management
438
+
439
+ | Command | Description |
440
+ |---------|-------------|
441
+ | `labctl user hash-password` | Generate password hash for config |
442
+ | `labctl user generate-key` | Generate random API key |
443
+ | `labctl user add <username>` | Interactive user creation with YAML output |
444
+ | `labctl user verify <username>` | Verify password against config |
445
+
446
+ ### MCP Server (AI Integration)
447
+
448
+ | Command | Description |
449
+ |---------|-------------|
450
+ | `labctl mcp` | Start MCP server (stdio transport) |
451
+ | `labctl mcp --http 8080` | Start MCP server (HTTP transport, binds 127.0.0.1) |
452
+ | `labctl mcp --http 8080 --host 0.0.0.0` | Listen on other interfaces (requires `auth.enabled` with API keys) |
453
+ | `labctl mcp --http 8080 --cert C --key K` | HTTP transport over TLS |
454
+ | `labctl mcp [--http 8080 ...] --check` | Run the startup checks and exit: whether clients need API keys, or why the server would refuse to start |
455
+
456
+ ### Shell Completion
457
+
458
+ ```bash
459
+ eval "$(labctl completion bash)" # or zsh; fish: labctl completion fish > ~/.config/fish/completions/labctl.fish
460
+ ```
461
+
462
+ ### Aliases
463
+
464
+ For convenience, common aliases are supported:
465
+ - `ls` → `list`
466
+ - `rm`, `delete` → `remove`
467
+ - `show` → `info`
468
+ - `on` → `power on`
469
+ - `off` → `power off`
470
+
471
+ ## Configuration
472
+
473
+ Default config location: `~/.config/labctl/config.yaml`
474
+
475
+ System installs use `/etc/labctl/config.yaml`. That file contains credentials and
476
+ is intended to be group-readable by the `labctl` group, not world-readable. Any
477
+ user who should run `labctl` against the shared lab inventory must be added to
478
+ the `labctl` group and start a new login session afterward.
479
+
480
+ ```yaml
481
+ serial:
482
+ dev_dir: /dev/lab # udev symlink directory
483
+ base_tcp_port: 4000 # lowest auto-assigned ser2net TCP port
484
+ default_baud: 115200
485
+
486
+ ser2net:
487
+ config_file: /etc/ser2net.yaml
488
+ enabled: true
489
+
490
+ # Authentication (disabled by default)
491
+ auth:
492
+ enabled: false
493
+ secret_key: "generate-a-random-string"
494
+ session_lifetime_minutes: 480
495
+ users:
496
+ - username: admin
497
+ password_hash: "generate with: labctl user hash-password"
498
+ api_key: "generate with: labctl user generate-key"
499
+
500
+ # HTTPS (disabled by default)
501
+ web:
502
+ cert_file: "/etc/labctl/ssl/cert.pem"
503
+ key_file: "/etc/labctl/ssl/key.pem"
504
+
505
+ # TP-Link Kasa credentials (for KLAP-authenticated devices)
506
+ kasa:
507
+ username: "your-tplink-email@example.com"
508
+ password: "your-tplink-password"
509
+
510
+ database_path: ~/.config/labctl/labctl.db
511
+ log_level: WARNING
512
+
513
+ # SQLite tuning. WAL journal mode is always on; this controls how long
514
+ # a transaction will wait for an exclusive lock before giving up.
515
+ database:
516
+ timeout_seconds: 10.0
517
+ ```
518
+
519
+ > **Note on backups:** WAL mode creates `-wal` and `-shm` sidecar files
520
+ > next to `labctl.db`. Backup scripts should copy all three together,
521
+ > or quiesce the daemon and run `sqlite3 labctl.db 'PRAGMA wal_checkpoint(TRUNCATE)'`
522
+ > before copying just the main file.
523
+
524
+ ## Web Interface
525
+
526
+ Start the web server:
527
+
528
+ ```bash
529
+ labctl web --port 5000
530
+ ```
531
+
532
+ Access at `http://localhost:5000`
533
+
534
+ Features:
535
+ - Dashboard with SBC status overview
536
+ - Power control buttons
537
+ - Web-based serial console (xterm.js)
538
+ - Live `/activity` page backed by Server-Sent Events
539
+ - Claim badges on the dashboard and SBC detail pages; changes to a board
540
+ someone else has claimed are refused (operators may override, logged)
541
+ - Settings and configuration view
542
+ - Optional login authentication (enable `auth.enabled` in config)
543
+ - Optional HTTPS with SSL/TLS certificates
544
+
545
+ ### Enabling HTTPS
546
+
547
+ labctl supports native HTTPS via SSL/TLS certificates. This is recommended when
548
+ authentication is enabled, to protect credentials in transit.
549
+
550
+ **1. Generate a self-signed certificate:**
551
+
552
+ ```bash
553
+ sudo mkdir -p /etc/labctl/ssl
554
+ sudo openssl req -x509 -newkey rsa:4096 -nodes \
555
+ -keyout /etc/labctl/ssl/key.pem \
556
+ -out /etc/labctl/ssl/cert.pem \
557
+ -days 365 \
558
+ -subj "/CN=labctl"
559
+ ```
560
+
561
+ To include additional hostnames or IPs (recommended for avoiding browser warnings
562
+ on your local network):
563
+
564
+ ```bash
565
+ sudo openssl req -x509 -newkey rsa:4096 -nodes \
566
+ -keyout /etc/labctl/ssl/key.pem \
567
+ -out /etc/labctl/ssl/cert.pem \
568
+ -days 365 \
569
+ -subj "/CN=labctl" \
570
+ -addext "subjectAltName=DNS:labctl,DNS:tarrasque,DNS:tarrasque.local,IP:192.168.1.100"
571
+ ```
572
+
573
+ **2a. Use via CLI flags (ad-hoc):**
574
+
575
+ ```bash
576
+ labctl web --cert /etc/labctl/ssl/cert.pem --key /etc/labctl/ssl/key.pem
577
+ ```
578
+
579
+ **2b. Or configure in config.yaml (persistent, recommended for systemd):**
580
+
581
+ ```yaml
582
+ web:
583
+ cert_file: "/etc/labctl/ssl/cert.pem"
584
+ key_file: "/etc/labctl/ssl/key.pem"
585
+ ```
586
+
587
+ **3. Set permissions for the systemd service:**
588
+
589
+ ```bash
590
+ sudo chown root:labctl /etc/labctl/ssl/key.pem
591
+ sudo chmod 640 /etc/labctl/ssl/key.pem
592
+ sudo systemctl restart labctl-web
593
+ ```
594
+
595
+ Access at `https://localhost:5000`. Your browser will warn about the self-signed
596
+ certificate — this is expected for lab use.
597
+
598
+ ## REST API
599
+
600
+ Base URL: `http://localhost:5000/api` (or `https://` when SSL is configured)
601
+
602
+ When authentication is enabled, API requests require an `X-API-Key` header:
603
+
604
+ ```bash
605
+ curl -H "X-API-Key: your-api-key" https://localhost:5000/api/sbcs
606
+ ```
607
+
608
+ For self-signed certificates, use `curl -k` or `--cacert cert.pem` to bypass
609
+ certificate verification:
610
+
611
+ ```bash
612
+ curl -k -H "X-API-Key: your-api-key" https://localhost:5000/api/sbcs
613
+ ```
614
+
615
+ Key activity and claims endpoints:
616
+
617
+ | Endpoint | Description |
618
+ |----------|-------------|
619
+ | `GET /api/activity` | Query recent activity events with `limit`, `actor`, `source`, `sbc`, `result`, `since`, and `after_id` filters |
620
+ | `GET /api/claims` | List all active claims |
621
+ | `GET /api/claims/<sbc>` | Show the current claim on one SBC |
622
+ | `GET /api/claims/<sbc>/history` | Show released claim history for one SBC |
623
+ | `POST /api/claims/<sbc>` | Create a claim (`duration_minutes`, `reason`, `agent_name`; with `claims.require_agent_name` and auth off, `agent_name` is required) |
624
+ | `POST /api/claims/<sbc>/renew` | Renew a claim held by the current caller |
625
+ | `POST /api/claims/<sbc>/release` | Release a claim held by the current caller |
626
+ | `POST /api/claims/<sbc>/force-release` | Operator override for an active claim |
627
+ | `POST /api/claims/<sbc>/request-release` | Record a polite release request for the current claimant |
628
+
629
+ | Endpoint | Method | Description |
630
+ |----------|--------|-------------|
631
+ | `/sbcs` | GET | List all SBCs |
632
+ | `/sbcs/<name>` | GET | Get SBC details |
633
+ | `/sbcs` | POST | Create SBC |
634
+ | `/sbcs/<name>` | PUT | Update SBC |
635
+ | `/sbcs/<name>` | DELETE | Remove SBC |
636
+ | `/sbcs/<name>/power` | GET | Get power state |
637
+ | `/sbcs/<name>/power` | POST | Control power (see below) |
638
+ | `/sbcs/<name>/ports` | POST | Assign a serial port |
639
+ | `/sbcs/<name>/console/info` | GET | Console connection info (TCP port, baud rate, and the proxy *settings* from config: `port_base`, write policy, client limit; not a running proxy's port) |
640
+ | `/sbcs/<name>/history` | GET | Status history |
641
+ | `/sbcs/<name>/uptime` | GET | Uptime statistics |
642
+ | `/ports` | GET | List all serial port assignments |
643
+ | `/status` | GET | Status overview of all SBCs |
644
+ | `/health/check` | GET, POST | Run a health check now (both methods; nothing is cached): pings, opens console ports and queries plugs |
645
+ | `/health` | GET | Service liveness and version (always open, no auth required) |
646
+
647
+ `POST /sbcs/<name>/power` takes `{"action": "on"|"off"|"cycle"}`, plus an
648
+ optional `delay` for a cycle (seconds, 0–86400; the default and the
649
+ minimum are the largest stored cycle delay among the boards on the plug
650
+ outlet, a board without one counting as 3.0) and
651
+ `"override_claim": true` to act on a board another user has claimed.
652
+ Power-on and cycle set the pre_power relays first
653
+ (see [Relays, Bindings and Recovery](#relays-bindings-and-recovery)). Responses:
654
+
655
+ | Status | Meaning |
656
+ |--------|---------|
657
+ | 200 | Done; the body includes the new `state` (and `delay_seconds` for a cycle) |
658
+ | 400 | Bad `action` or `delay`, an out-of-range stored cycle delay, or no plug assigned |
659
+ | 403 | An `agent`-role user tried `override_claim` (`not_operator`) |
660
+ | 404 | No such SBC |
661
+ | 409 | Refused with nothing switched: the board is claimed (`sbc_claimed`) or must be claimed first (`claim_required`), `on`/`off` while a cycle of the board's outlet is running, or a pre_power relay couldn't be checked or set before power-on (`{"error": ..., "switched": false}`) |
662
+ | 429 | A cycle requested while another cycle of the same outlet is running, or within 5 s of the outlet's last one |
663
+ | 500 | The plug failed, or a strap failed after a cycle's power-off (the board may be off) |
664
+
665
+ See [docs/OPERATIONS.md](https://github.com/johnjezl/Embedded-Lab-Control/blob/main/docs/OPERATIONS.md) for the full claim and REST
666
+ error codes.
667
+
668
+ ## MCP Server (AI Integration)
669
+
670
+ labctl includes an MCP (Model Context Protocol) server for AI assistant integration.
671
+ This allows tools like Claude Desktop and Claude Code to manage lab resources directly.
672
+
673
+ Install the MCP dependency from a checkout (the `[mcp]` extra):
674
+
675
+ ```bash
676
+ pip install -e ".[mcp]"
677
+ # or from PyPI: pip install 'embedded-lab-control[mcp]'
678
+ ```
679
+
680
+ **Resources** (read-only data): `lab://sbcs`, `lab://sbcs/{name}`, `lab://power/{name}`,
681
+ `lab://serial-devices`, `lab://sdwire-devices`, `lab://ports`, `lab://health/{name}`,
682
+ `lab://status`, `lab://claims`, `lab://claims/{name}`, `lab://claims/history/{name}`,
683
+ `lab://claims/metrics`, `lab://activity/recent`, `lab://activity/{name}`
684
+
685
+ **Tools** (actions): `power_on`, `power_off`, `power_cycle`, `run_health_check`,
686
+ `add_sbc`, `remove_sbc`, `update_sbc`, `assign_serial_port`, `assign_power_plug`,
687
+ `set_network_address`, `sdwire_to_dut`, `sdwire_to_host`, `sdwire_update`,
688
+ `flash_image`, `serial_capture`, `serial_send`, `boot_test`, `list_images`,
689
+ `delete_image` (and more: see the docs)
690
+
691
+ **Uploads**: clients upload builds over HTTP to the MCP server
692
+ (`PUT /images/<name>`, same API key) and pass the bare name to
693
+ `flash_image`, `sdwire_update` or `boot_test`, so an agent can build on
694
+ another machine ([Uploading images](https://github.com/johnjezl/Embedded-Lab-Control/blob/main/docs/MCP_SERVER.md#uploading-images)).
695
+
696
+ **Prompts**: `debug_sbc` (guided SBC debugging), `lab_report` (comprehensive status)
697
+
698
+ See `docs/MCP_SERVER.md` for full tool parameters and usage.
699
+
700
+ **Safety:** destructive tools (power off, flashing, `serial_send`, deletions,
701
+ …) act only when called a second time with a confirmation token; host files
702
+ are reachable only under directories you allowlist in `mcp.allowed_*_paths`
703
+ (none by default); the HTTP transport binds loopback and requires the web
704
+ users' API keys (`Authorization: Bearer`) when `auth.enabled` is set. See
705
+ [Security Considerations](https://github.com/johnjezl/Embedded-Lab-Control/blob/main/docs/MCP_SERVER.md#security-considerations).
706
+
707
+ ### Claude Desktop / Claude Code Configuration
708
+
709
+ For several Claude Code agents sharing the lab (each with its own API key,
710
+ so claims keep them apart), see
711
+ [Connecting Claude Code agents](https://github.com/johnjezl/Embedded-Lab-Control/blob/main/docs/CLAUDE_CODE_AGENTS.md).
712
+
713
+ For a single local client over stdio, add to your MCP client configuration
714
+ (reading `/etc/labctl/config.yaml` requires the `labctl` group; otherwise
715
+ point `-c` at your own config):
716
+
717
+ ```json
718
+ {
719
+ "mcpServers": {
720
+ "labctl": {
721
+ "command": "/opt/labctl/venv/bin/labctl",
722
+ "args": ["-c", "/etc/labctl/config.yaml", "mcp"]
723
+ }
724
+ }
725
+ }
726
+ ```
727
+
728
+ ### Claude Code skill: /deploy-and-test
729
+
730
+ `skills/deploy-and-test.md` is an example skill that builds, deploys over
731
+ SDWire, power-cycles and checks the serial console using the labctl MCP
732
+ tools. It is not active in this repository. Claude Code loads a skill from a
733
+ `SKILL.md` file, with `name`/`description` frontmatter, inside a directory
734
+ named after the skill. To install it for one project (or for all your
735
+ projects, under `~/.claude/skills/`):
736
+
737
+ ```bash
738
+ mkdir -p <your-project>/.claude/skills/deploy-and-test
739
+ cp skills/deploy-and-test.md <your-project>/.claude/skills/deploy-and-test/SKILL.md
740
+ ```
741
+
742
+ It expects a connected labctl MCP server, and reads per-target settings from
743
+ `.claude/deploy-config.json`.
744
+
745
+ ## Systemd Services
746
+
747
+ Install labctl system-wide with systemd services:
748
+
749
+ ```bash
750
+ sudo ./scripts/install-services.sh
751
+
752
+ # Optional: MCP server over HTTP on 127.0.0.1:8080
753
+ sudo systemctl enable --now labctl-mcp
754
+ ```
755
+
756
+ `install-services.sh` creates the `labctl` user and group and installs
757
+ labctl with the `web,mcp,kasa,sdwire` extras into `/opt/labctl/venv`, linked as
758
+ `/usr/local/bin/labctl`. It sets up `/etc/labctl/config.yaml` (root:labctl,
759
+ 640) and `/var/lib/labctl`, and makes the udev rules file,
760
+ `/etc/ser2net.yaml` and the services' database writable by the `labctl`
761
+ group. A new config puts the database in `/var/lib/labctl/db` (setgid
762
+ 2775, the file 664); if `database_path` names another file, the script
763
+ leaves it alone and prints how to move it there. It writes
764
+ `/etc/sudoers.d/labctl` for `udevadm` and `systemctl restart ser2net`.
765
+ It enables and starts `labctl-web` and `labctl-monitor`; `labctl-mcp` is
766
+ installed but not enabled. For USB relays, also install the `actuators` extra
767
+ into the venv (`sudo /opt/labctl/venv/bin/pip install "<checkout>[actuators]"`).
768
+
769
+ `labctl-mcp` runs `labctl mcp --http 8080`, which binds 127.0.0.1. To
770
+ reach it from other machines, enable `auth.enabled` with API keys and
771
+ override the command in a drop-in (`sudo systemctl edit labctl-mcp`). The
772
+ drop-in must clear `ExecStart` before setting it again:
773
+
774
+ ```ini
775
+ [Service]
776
+ ExecStart=
777
+ ExecStart=/opt/labctl/venv/bin/labctl -c /etc/labctl/config.yaml mcp --http 8080 --host 0.0.0.0
778
+ ```
779
+
780
+ To update an existing install after `git pull` (keeps config and database):
781
+
782
+ ```bash
783
+ sudo ./scripts/update.sh
784
+ ```
785
+
786
+ When the configured database is `/var/lib/labctl/db/labctl.db` (also when
787
+ written through a symlinked `/var/lib/labctl`), both scripts keep it (and
788
+ its directory) writable by the `labctl` group, and make sure group members
789
+ can traverse `/var/lib/labctl`: they add group search (`g+x`) when it
790
+ belongs to the `labctl` group, and warn when members still can't reach it
791
+ (e.g. it belongs to another group). Otherwise they print a note and leave
792
+ the database alone.
793
+
794
+ ### Moving the database
795
+
796
+ Installs from before the `/var/lib/labctl/db` directory keep the database
797
+ elsewhere (e.g. `/var/lib/labctl/labctl.db` or
798
+ `/var/lib/labctl/.config/labctl/labctl.db`), which only the `labctl` user
799
+ can write. To let `labctl` group members write it from the CLI, move it:
800
+
801
+ Every step runs as root: your own account may not be able to see into
802
+ `/var/lib/labctl` at all.
803
+
804
+ ```bash
805
+ sudo systemctl stop labctl-web labctl-monitor labctl-mcp
806
+ OLD=/var/lib/labctl/labctl.db # your current database_path (named labctl.db)
807
+ sudo install -d -o labctl -g labctl -m 2775 /var/lib/labctl/db
808
+ # A clean stop normally leaves no -wal/-shm; move them too if present.
809
+ sudo sh -c 'for f in "$1" "$1-wal" "$1-shm"; do
810
+ if [ -e "$f" ]; then mv -v "$f" /var/lib/labctl/db/; fi
811
+ done' sh "$OLD"
812
+ # Check the database arrived before going on: if it isn't there,
813
+ # update.sh creates a new, EMPTY database at the new path.
814
+ sudo ls -l /var/lib/labctl/db/labctl.db
815
+ # Set database_path: /var/lib/labctl/db/labctl.db, then copy the config to
816
+ # the service copy (update.sh refuses to run while the two differ):
817
+ sudoedit /etc/labctl/config.yaml
818
+ sudo -u labctl cp /etc/labctl/config.yaml /var/lib/labctl/.config/labctl/config.yaml
819
+ sudo ./scripts/update.sh # or install-services.sh: sets 2775/664, restarts
820
+ sudo systemctl start labctl-web labctl-monitor # and labctl-mcp if you use it
821
+ ```
822
+
823
+ `update.sh` restarts the enabled services itself; the last line covers any
824
+ you started by hand. As a group member, a write such as
825
+ `labctl claim <sbc> -r test` then `labctl release <sbc>` should now work
826
+ without `sudo -u labctl`.
827
+
828
+ ## Development
829
+
830
+ ```bash
831
+ # Run tests
832
+ pytest tests/ -v
833
+
834
+ # Format code
835
+ black src/ tests/
836
+ isort src/ tests/
837
+
838
+ # Lint
839
+ flake8 src/ tests/
840
+ ```
841
+
842
+ ## Documentation
843
+
844
+ - [Implementation Plan](https://github.com/johnjezl/Embedded-Lab-Control/blob/main/docs/IMPLEMENTATION.md) - Development roadmap and status
845
+ - [Mapping USB Serial Adapters](https://github.com/johnjezl/Embedded-Lab-Control/blob/main/docs/HARDWARE_MAP.md) - Stable `/dev/lab` names from USB paths
846
+ - [Decision Log](https://github.com/johnjezl/Embedded-Lab-Control/blob/main/docs/DECISIONS.md) - Architecture decisions
847
+ - [MCP Server](https://github.com/johnjezl/Embedded-Lab-Control/blob/main/docs/MCP_SERVER.md) - AI integration, security model
848
+ - [Claude Code Agents](https://github.com/johnjezl/Embedded-Lab-Control/blob/main/docs/CLAUDE_CODE_AGENTS.md) - One API key per agent, setup and launch
849
+ - [Operations](https://github.com/johnjezl/Embedded-Lab-Control/blob/main/docs/OPERATIONS.md) - What every command and tool changes
850
+ - [Security Policy](https://github.com/johnjezl/Embedded-Lab-Control/blob/main/SECURITY.md) - Reporting vulnerabilities
851
+ - [Releasing](https://github.com/johnjezl/Embedded-Lab-Control/blob/main/docs/RELEASING.md) - Publishing a version to PyPI
852
+
853
+ ## License
854
+
855
+ MIT