labcode 0.8.0__tar.gz → 0.9.0__tar.gz

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 (112) hide show
  1. {labcode-0.8.0 → labcode-0.9.0}/.github/workflows/ci.yml +15 -0
  2. labcode-0.9.0/CHANGELOG.md +63 -0
  3. {labcode-0.8.0 → labcode-0.9.0}/PKG-INFO +63 -18
  4. {labcode-0.8.0 → labcode-0.9.0}/README.md +55 -16
  5. {labcode-0.8.0 → labcode-0.9.0/docs}/SPECIFICATIONS.md +161 -28
  6. {labcode-0.8.0 → labcode-0.9.0}/examples/README.md +62 -12
  7. labcode-0.9.0/examples/lads_plate_cycle.env.yaml +288 -0
  8. labcode-0.9.0/examples/lads_seal.env.yaml +100 -0
  9. {labcode-0.8.0 → labcode-0.9.0}/examples/preflight_sila2_env.py +24 -9
  10. {labcode-0.8.0 → labcode-0.9.0}/examples/render_sila2_plate_cycle.py +3 -2
  11. labcode-0.9.0/examples/run_all_lads_examples.py +93 -0
  12. {labcode-0.8.0 → labcode-0.9.0}/examples/run_all_sila2_examples.py +1 -1
  13. {labcode-0.8.0 → labcode-0.9.0}/examples/run_sila2_plate_cycle.py +9 -5
  14. {labcode-0.8.0 → labcode-0.9.0}/examples/run_sila2_plate_cycle_no_atc.py +5 -5
  15. {labcode-0.8.0 → labcode-0.9.0}/examples/run_sila2_seal.py +4 -3
  16. {labcode-0.8.0 → labcode-0.9.0}/examples/sila2_plate_cycle.boundary.yaml +1 -1
  17. {labcode-0.8.0 → labcode-0.9.0}/examples/sila2_plate_cycle.wrapped.env.yaml +7 -7
  18. {labcode-0.8.0 → labcode-0.9.0}/examples/sila2_plate_cycle_no_atc.boundary.yaml +1 -1
  19. {labcode-0.8.0 → labcode-0.9.0}/examples/sila2_plate_cycle_no_atc.wrapped.env.yaml +7 -7
  20. {labcode-0.8.0 → labcode-0.9.0}/examples/sila2_seal.boundary.yaml +1 -1
  21. {labcode-0.8.0 → labcode-0.9.0}/examples/sila2_seal.env.yaml +2 -2
  22. {labcode-0.8.0 → labcode-0.9.0}/examples/sila2_seal.wrapped.env.yaml +4 -4
  23. {labcode-0.8.0 → labcode-0.9.0}/labcode/backend.py +30 -19
  24. {labcode-0.8.0 → labcode-0.9.0}/labcode/cli.py +16 -1
  25. labcode-0.9.0/labcode/connections.py +257 -0
  26. {labcode-0.8.0 → labcode-0.9.0}/labcode/dialect.py +80 -34
  27. {labcode-0.8.0 → labcode-0.9.0}/labcode/extension.py +44 -7
  28. labcode-0.9.0/labcode/lads.py +181 -0
  29. labcode-0.9.0/labcode/lads_commands.py +167 -0
  30. labcode-0.9.0/labcode/lads_unit.py +331 -0
  31. {labcode-0.8.0 → labcode-0.9.0}/labcode/probe.py +4 -4
  32. labcode-0.9.0/labcode/sila2.py +154 -0
  33. {labcode-0.8.0 → labcode-0.9.0}/labcode/sila2_commands.py +1 -1
  34. {labcode-0.8.0 → labcode-0.9.0}/labcode.egg-info/PKG-INFO +63 -18
  35. {labcode-0.8.0 → labcode-0.9.0}/labcode.egg-info/SOURCES.txt +11 -1
  36. {labcode-0.8.0 → labcode-0.9.0}/labcode.egg-info/requires.txt +9 -1
  37. {labcode-0.8.0 → labcode-0.9.0}/labcode.egg-info/scm_file_list.json +11 -1
  38. labcode-0.9.0/labcode.egg-info/scm_version.json +8 -0
  39. {labcode-0.8.0 → labcode-0.9.0}/pyproject.toml +25 -8
  40. {labcode-0.8.0 → labcode-0.9.0}/tests/test_backend.py +74 -1
  41. {labcode-0.8.0 → labcode-0.9.0}/tests/test_cli.py +16 -1
  42. {labcode-0.8.0 → labcode-0.9.0}/tests/test_dialect.py +105 -3
  43. labcode-0.9.0/tests/test_lads.py +184 -0
  44. labcode-0.9.0/tests/test_lads_unit.py +396 -0
  45. {labcode-0.8.0 → labcode-0.9.0}/tests/test_sila2.py +2 -2
  46. labcode-0.8.0/labcode/sila2.py +0 -280
  47. labcode-0.8.0/labcode.egg-info/scm_version.json +0 -8
  48. {labcode-0.8.0 → labcode-0.9.0}/.github/workflows/publish.yml +0 -0
  49. {labcode-0.8.0 → labcode-0.9.0}/.gitignore +0 -0
  50. {labcode-0.8.0 → labcode-0.9.0}/LICENSE +0 -0
  51. {labcode-0.8.0 → labcode-0.9.0}/MANIFEST.in +0 -0
  52. {labcode-0.8.0 → labcode-0.9.0}/examples/outputs/plate_line.boundary.yaml +0 -0
  53. {labcode-0.8.0 → labcode-0.9.0}/examples/outputs/plate_line.observation.yaml +0 -0
  54. {labcode-0.8.0 → labcode-0.9.0}/examples/outputs/plate_line.plan.yaml +0 -0
  55. {labcode-0.8.0 → labcode-0.9.0}/examples/outputs/plate_line.svg +0 -0
  56. {labcode-0.8.0 → labcode-0.9.0}/examples/outputs/shared_bench.boundary.yaml +0 -0
  57. {labcode-0.8.0 → labcode-0.9.0}/examples/outputs/shared_bench.observation.yaml +0 -0
  58. {labcode-0.8.0 → labcode-0.9.0}/examples/outputs/shared_bench.plan.yaml +0 -0
  59. {labcode-0.8.0 → labcode-0.9.0}/examples/outputs/shared_bench.svg +0 -0
  60. {labcode-0.8.0 → labcode-0.9.0}/examples/outputs/sila2_plate_cycle.boundary.yaml +0 -0
  61. {labcode-0.8.0 → labcode-0.9.0}/examples/outputs/sila2_plate_cycle.observation.yaml +0 -0
  62. {labcode-0.8.0 → labcode-0.9.0}/examples/outputs/sila2_plate_cycle.plan.yaml +0 -0
  63. {labcode-0.8.0 → labcode-0.9.0}/examples/outputs/sila2_plate_cycle.svg +0 -0
  64. {labcode-0.8.0 → labcode-0.9.0}/examples/plate_line.boundary.yaml +0 -0
  65. {labcode-0.8.0 → labcode-0.9.0}/examples/plate_line.env.yaml +0 -0
  66. {labcode-0.8.0 → labcode-0.9.0}/examples/plate_line.workflow.yaml +0 -0
  67. {labcode-0.8.0 → labcode-0.9.0}/examples/render_plate_line.py +0 -0
  68. {labcode-0.8.0 → labcode-0.9.0}/examples/render_shared_bench.py +0 -0
  69. {labcode-0.8.0 → labcode-0.9.0}/examples/shared_bench.afternoon.boundary.yaml +0 -0
  70. {labcode-0.8.0 → labcode-0.9.0}/examples/shared_bench.env.yaml +0 -0
  71. {labcode-0.8.0 → labcode-0.9.0}/examples/shared_bench.morning.boundary.yaml +0 -0
  72. {labcode-0.8.0 → labcode-0.9.0}/examples/shared_bench.run.yaml +0 -0
  73. {labcode-0.8.0 → labcode-0.9.0}/examples/shared_bench.workflow.yaml +0 -0
  74. {labcode-0.8.0 → labcode-0.9.0}/examples/sila2_plate_cycle.workflow.yaml +0 -0
  75. {labcode-0.8.0 → labcode-0.9.0}/examples/sila2_plate_cycle_no_atc.workflow.yaml +0 -0
  76. {labcode-0.8.0 → labcode-0.9.0}/examples/sila2_seal.workflow.yaml +0 -0
  77. {labcode-0.8.0 → labcode-0.9.0}/labcode/__init__.py +0 -0
  78. {labcode-0.8.0 → labcode-0.9.0}/labcode/__main__.py +0 -0
  79. {labcode-0.8.0 → labcode-0.9.0}/labcode/_child.py +0 -0
  80. {labcode-0.8.0 → labcode-0.9.0}/labcode/idgen.py +0 -0
  81. {labcode-0.8.0 → labcode-0.9.0}/labcode/objectid.py +0 -0
  82. {labcode-0.8.0 → labcode-0.9.0}/labcode/otel.py +0 -0
  83. {labcode-0.8.0 → labcode-0.9.0}/labcode/otel_sila2.py +0 -0
  84. {labcode-0.8.0 → labcode-0.9.0}/labcode/py.typed +0 -0
  85. {labcode-0.8.0 → labcode-0.9.0}/labcode/record.py +0 -0
  86. {labcode-0.8.0 → labcode-0.9.0}/labcode/run_cli.py +0 -0
  87. {labcode-0.8.0 → labcode-0.9.0}/labcode/runner.py +0 -0
  88. {labcode-0.8.0 → labcode-0.9.0}/labcode/sila2_instrument.py +0 -0
  89. {labcode-0.8.0 → labcode-0.9.0}/labcode.egg-info/dependency_links.txt +0 -0
  90. {labcode-0.8.0 → labcode-0.9.0}/labcode.egg-info/entry_points.txt +0 -0
  91. {labcode-0.8.0 → labcode-0.9.0}/labcode.egg-info/top_level.txt +0 -0
  92. {labcode-0.8.0 → labcode-0.9.0}/setup.cfg +0 -0
  93. {labcode-0.8.0 → labcode-0.9.0}/tests/fixtures/device_script.env.yaml +0 -0
  94. {labcode-0.8.0 → labcode-0.9.0}/tests/fixtures/device_script.workflow.yaml +0 -0
  95. {labcode-0.8.0 → labcode-0.9.0}/tests/fixtures/joint.run.yaml +0 -0
  96. {labcode-0.8.0 → labcode-0.9.0}/tests/fixtures/replenishment.env.yaml +0 -0
  97. {labcode-0.8.0 → labcode-0.9.0}/tests/fixtures/reroute_device.env.yaml +0 -0
  98. {labcode-0.8.0 → labcode-0.9.0}/tests/fixtures/reroute_transporter.env.yaml +0 -0
  99. {labcode-0.8.0 → labcode-0.9.0}/tests/fixtures/transport.env.yaml +0 -0
  100. {labcode-0.8.0 → labcode-0.9.0}/tests/fixtures/transport.workflow.yaml +0 -0
  101. {labcode-0.8.0 → labcode-0.9.0}/tests/test_joint.py +0 -0
  102. {labcode-0.8.0 → labcode-0.9.0}/tests/test_objectid.py +0 -0
  103. {labcode-0.8.0 → labcode-0.9.0}/tests/test_otel.py +0 -0
  104. {labcode-0.8.0 → labcode-0.9.0}/tests/test_otel_child.py +0 -0
  105. {labcode-0.8.0 → labcode-0.9.0}/tests/test_otel_grpc.py +0 -0
  106. {labcode-0.8.0 → labcode-0.9.0}/tests/test_otel_sila2.py +0 -0
  107. {labcode-0.8.0 → labcode-0.9.0}/tests/test_probe.py +0 -0
  108. {labcode-0.8.0 → labcode-0.9.0}/tests/test_record.py +0 -0
  109. {labcode-0.8.0 → labcode-0.9.0}/tests/test_recording.py +0 -0
  110. {labcode-0.8.0 → labcode-0.9.0}/tests/test_run_cli.py +0 -0
  111. {labcode-0.8.0 → labcode-0.9.0}/tests/test_sila2_commands.py +0 -0
  112. {labcode-0.8.0 → labcode-0.9.0}/tests/test_sila2_instrument.py +0 -0
@@ -22,6 +22,21 @@ jobs:
22
22
  - run: pip install -e ".[test]"
23
23
  - run: pytest
24
24
 
25
+ test-export:
26
+ # `lc export` is the optional extra `labcode[export]`, so the matrix above runs
27
+ # without it and tests the "not installed" answer. This installs it the way a
28
+ # user would -- through the extra -- and tests that `lc export` reaches it.
29
+ runs-on: ubuntu-latest
30
+ steps:
31
+ - uses: actions/checkout@v7
32
+ - uses: actions/setup-python@v7
33
+ with:
34
+ python-version: "3.12"
35
+ cache: pip
36
+ - run: pip install -e ".[test,export]"
37
+ - run: python -c "import ofplang.export"
38
+ - run: pytest tests/test_cli.py
39
+
25
40
  lint:
26
41
  runs-on: ubuntu-latest
27
42
  steps:
@@ -0,0 +1,63 @@
1
+ # Changelog
2
+
3
+ What changed in each release of labcode, for someone deciding whether to upgrade. The
4
+ specification the section numbers refer to is [`docs/SPECIFICATIONS.md`](docs/SPECIFICATIONS.md).
5
+
6
+ Releases before 0.9.0 are described in the messages of their `Release vX.Y.Z` commits
7
+ (`git log --grep '^Release v'`).
8
+
9
+ ## 0.9.0 — 2026-09-28
10
+
11
+ labcode can now drive a lab over **LADS OPC UA** as well as SiLA2, and gains `lc export`.
12
+ Both are optional extras; nothing changes for an environment that uses neither.
13
+
14
+ ### Added
15
+
16
+ - **`flavor: lads`** — a script that is the commands alone, handed its LADS OPC UA clients
17
+ as a `sila2` script is handed SiLA2 ones (§1.10).
18
+ - A connection may say `kind: lads`, and may name the `device` and functional `unit` on the
19
+ server; left out, each is the only one there is (§1.5).
20
+ - The script sees `lads_clients` / `lads_client`: one functional unit per machine, with the
21
+ program runs, state-machine methods, cover movements and set-points LADS defines, each
22
+ waiting for what it starts, and `.raw` for the asyncua client itself.
23
+ - `labcode.lads_commands` holds the waits: a program run to its result, a state machine to
24
+ settle (§1.10.1). A timeout fails the operation and aborts nothing, as SiLA2's does.
25
+ - A script speaks one protocol. An operation may hold machines of both kinds, but a script
26
+ commands only its own flavor's; reaching for the other kind says why
27
+ (`lads_other_protocol` / `sila2_other_protocol`).
28
+ - The client library is the `lads` extra (`asyncua`). labcode imports it only when a `lads`
29
+ script connects.
30
+ - **`lc export`** — forwards to [ofplang-export](https://github.com/ofplang/export)'s CLI
31
+ unchanged, as `lc validate` and `lc schedule` do. `lc export view plan.yaml -o plan.html`
32
+ writes the workflow and its plan as one self-contained HTML file. It is the `export` extra;
33
+ without it `lc export` names the extra and exits 2.
34
+ - **LADS examples** — `lads_seal.env.yaml` and `lads_plate_cycle.env.yaml` run the
35
+ `sila2_seal` and `sila2_plate_cycle` workflows unchanged, with every machine reached over
36
+ LADS. `run_all_lads_examples.py` runs both against the reference lab,
37
+ [ofplang/mocklab](https://github.com/ofplang/mocklab).
38
+
39
+ ### Changed
40
+
41
+ - `SPECIFICATIONS.md` moved to `docs/SPECIFICATIONS.md`.
42
+ - The `ofplang-schedule` cap is `<0.13`, admitting schedule 0.12. The floor is unchanged.
43
+ - The front door checks a connecting script's connection **per kind**. A `sila2` or `lads`
44
+ script needs a machine of its own kind. An endpoint of the other kind draws a warning, as
45
+ an endpoint with no connection does. Messages that named only `sila2` now name the flavor.
46
+ - What a connecting flavor needs (planning the clients, holding them for one operation,
47
+ generating the wrapper) moved from `labcode.sila2` into `labcode.connections`. The `sila2`
48
+ flavor's behaviour, messages and reason codes are unchanged.
49
+
50
+ ### Fixed
51
+
52
+ - `examples/preflight_sila2_env.py` treated every declared connection as SiLA2. An
53
+ environment with a LADS machine would have reported it as a failing SiLA2 server. It now
54
+ checks SiLA2 machines only and lists the others as not checked.
55
+
56
+ ### Documentation
57
+
58
+ - The reference lab is named by its current name, ofplang/mocklab (formerly
59
+ ofplang-sila2-backend), and the examples record the commit they were last verified against.
60
+ - Section references written before probing took §1.6 pointed at the wrong sections and are
61
+ corrected. The `labcode.sila2_commands` section is numbered §1.7.1.
62
+ - The README lists the optional extras, lists every example, and links by absolute URL, so the
63
+ links also work on PyPI.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: labcode
3
- Version: 0.8.0
3
+ Version: 0.9.0
4
4
  Summary: labcode -- a dialect wrapper over the Object-Flow Programming Language toolchain
5
5
  Author-email: Kazunari Kaizu <kwaizu@gmail.com>
6
6
  License-Expression: MIT
@@ -21,20 +21,26 @@ Requires-Python: >=3.10
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
23
  Requires-Dist: ofplang-validate<0.3,>=0.2
24
- Requires-Dist: ofplang-schedule<0.12,>=0.9
24
+ Requires-Dist: ofplang-schedule<0.13,>=0.9
25
25
  Requires-Dist: ofplang-run<0.13,>=0.11
26
26
  Provides-Extra: test
27
27
  Requires-Dist: pytest>=7.0; extra == "test"
28
28
  Requires-Dist: opentelemetry-sdk>=1.20; extra == "test"
29
29
  Requires-Dist: opentelemetry-instrumentation-grpc[instruments]>=0.41b0; extra == "test"
30
+ Requires-Dist: asyncua>=2.0; extra == "test"
30
31
  Provides-Extra: dev
31
32
  Requires-Dist: pytest>=7.0; extra == "dev"
32
33
  Requires-Dist: ruff>=0.16; extra == "dev"
33
34
  Requires-Dist: mypy>=1.11; extra == "dev"
34
35
  Requires-Dist: opentelemetry-sdk>=1.20; extra == "dev"
35
36
  Requires-Dist: opentelemetry-instrumentation-grpc[instruments]>=0.41b0; extra == "dev"
37
+ Requires-Dist: asyncua>=2.0; extra == "dev"
36
38
  Provides-Extra: sila2
37
39
  Requires-Dist: sila2>=0.14; extra == "sila2"
40
+ Provides-Extra: lads
41
+ Requires-Dist: asyncua>=2.0; extra == "lads"
42
+ Provides-Extra: export
43
+ Requires-Dist: ofplang-export<0.2,>=0.1; extra == "export"
38
44
  Provides-Extra: otel
39
45
  Requires-Dist: opentelemetry-api>=1.20; extra == "otel"
40
46
  Requires-Dist: opentelemetry-sdk>=1.20; extra == "otel"
@@ -55,6 +61,7 @@ ofplang toolchain and exposes it under a single command:
55
61
  lc validate ... # check a workflow is well-formed portable v0
56
62
  lc schedule ... # compute a schedule for a workflow
57
63
  lc run ... # execute a workflow on the labcode backend
64
+ lc export ... # write documents out for people to read (optional: labcode[export])
58
65
  ```
59
66
 
60
67
  labcode is where a site-specific dialect and a custom runner (real lab hardware)
@@ -63,7 +70,7 @@ forward to the ofplang siblings unchanged; **`lc run` is the labcode dialect's o
63
70
  runner**: it drives the workflow on the labcode backend, running each device
64
71
  operation's script — supplied in the environment as an `x-labcode.script` extension
65
72
  on a process mode — out-of-process on a wall clock, so a long-running real operation
66
- never blocks the replan loop. See [`SPECIFICATIONS.md`](SPECIFICATIONS.md) for the
73
+ never blocks the replan loop. See [`docs/SPECIFICATIONS.md`](https://github.com/ofplang/labcode/blob/main/docs/SPECIFICATIONS.md) for the
67
74
  `x-labcode` extension.
68
75
 
69
76
  ```yaml
@@ -94,7 +101,25 @@ sibling packages unchanged; `lc run` is this package's own runner, built on them
94
101
  - [`ofplang-run`](https://github.com/ofplang/run) — the runner / simulator
95
102
 
96
103
  The language is defined in the [ofplang/spec](https://github.com/ofplang/spec)
97
- repository, and what labcode adds to it in [`SPECIFICATIONS.md`](SPECIFICATIONS.md).
104
+ repository, and what labcode adds to it in [`docs/SPECIFICATIONS.md`](https://github.com/ofplang/labcode/blob/main/docs/SPECIFICATIONS.md).
105
+
106
+ Talking to instruments, recording a run and exporting documents are optional, each an extra
107
+ installed into the interpreter that runs `lc` (labcode runs every script with that same
108
+ interpreter):
109
+
110
+ | extra | brings | needed for |
111
+ |---|---|---|
112
+ | `sila2` | the `sila2` client library | `flavor: sila2` scripts (§1.7) |
113
+ | `lads` | `asyncua`, an OPC UA client | `flavor: lads` scripts (§1.10) |
114
+ | `otel` | the OpenTelemetry SDK and exporter | `lc run --trace` |
115
+ | `export` | [`ofplang-export`](https://github.com/ofplang/export) | `lc export view` — the workflow and its plan as one self-contained HTML file |
116
+
117
+ ```sh
118
+ pip install 'labcode[sila2,lads]'
119
+ ```
120
+
121
+ Without an extra, labcode still runs; only what needs it — a script of that flavor,
122
+ `--trace`, or `lc export` — fails, with a message naming the extra to install.
98
123
 
99
124
  What `lc run` brings of its own, beyond dispatching:
100
125
 
@@ -114,12 +139,18 @@ What `lc run` brings of its own, beyond dispatching:
114
139
  - **object identity** — the reserved `_id` view key is declared on Object types and minted
115
140
  per object, so a physical thing can be followed through a run (§4).
116
141
  - **`flavor: sila2`** — a script that speaks SiLA2 gets its clients opened around it
117
- (§1.7). The client library itself is the `sila2` extra: `pip install labcode[sila2]`,
118
- installed into whichever interpreter runs the scripts.
142
+ (§1.7). The client library is the `sila2` extra.
143
+ - **`flavor: lads`** — the same for LADS OPC UA: a script is handed one LADS functional
144
+ unit per machine, already connected, with the program runs, state-machine methods and
145
+ cover movements LADS defines and the waits each needs (§1.10). A connection may name the
146
+ `device` and `unit` on the server, or leave them to be found. A script speaks one protocol;
147
+ an operation holding machines of both kinds commands only its own flavor's. The client
148
+ library is the `lads` extra.
119
149
  - **recording a run** — with `--trace`, what the run did is recorded as OpenTelemetry
120
150
  traces: one trace per run, a span per operation, and — measured inside the process that
121
151
  issued them — a span per SiLA2 connection, per command, and per gRPC call each of those
122
- made. Off by default; the extra is `pip install labcode[otel]`.
152
+ made. Off by default; it needs the `otel` extra. A `lads` operation is
153
+ recorded as its span alone for now.
123
154
 
124
155
  ## Usage
125
156
 
@@ -133,9 +164,11 @@ lc --version
133
164
 
134
165
  `lc` can also be run as a module: `python -m labcode <command> ...`.
135
166
 
136
- `lc validate` and `lc schedule` are the ofplang siblings' own CLIs unchanged, so their
137
- options are documented in those repositories. `lc run` is this package's own, and is
138
- described below.
167
+ `lc validate`, `lc schedule` and `lc export` are the ofplang siblings' own CLIs unchanged,
168
+ so their options are documented in those repositories — `lc export view plan.yaml -o
169
+ plan.html`, for one, writes the workflow and its plan as a single HTML file
170
+ ([ofplang/export](https://github.com/ofplang/export); needs `labcode[export]`). `lc run` is
171
+ this package's own, and is described below.
139
172
 
140
173
  ### `lc run`
141
174
 
@@ -267,9 +300,10 @@ duration and nothing is commanded. That is a real thing to write (an operator to
267
300
  stock up while the schedule waits for them) and an easy one to write by accident, so it is
268
301
  warned about.
269
302
 
270
- `flavor: sila2` is **refused on a refill route** for now: a sila2 script is handed clients,
271
- and which machine's clients a refill should receive — the replenisher's, or both ends' as a
272
- transport may ask for — is not settled. Use `python`.
303
+ `flavor: sila2` and `flavor: lads` are **refused on a refill route** for now: such a script
304
+ is handed clients, and which machine's clients a refill should receive — the replenisher's,
305
+ or both ends' as a transport may ask for — is not settled. Use `raw` (the default), which may
306
+ connect for itself.
273
307
 
274
308
  A refill holds the device it fills *and* the replenisher filling it, so it never overlaps
275
309
  the work it feeds. It is recorded (`--trace`) as a `replenishment` span naming both
@@ -332,11 +366,22 @@ OpenTelemetry's own export timeout allows: labcode sets no timeout of its own, s
332
366
 
333
367
  ## Examples
334
368
 
335
- [`examples/`](examples/README.md) holds three worked runs: `plate_line`, an
336
- Object-bearing line driven entirely by environment scripts and runnable with no
337
- hardware, and `sila2_seal` and `sila2_plate_cycle`, which drive the reference lab's
338
- SiLA2 servers for real. `sila2_seal` also walks through what a run does when a
339
- machine stops answering — before an operation, and in the middle of one.
369
+ [`examples/`](https://github.com/ofplang/labcode/blob/main/examples/README.md) holds worked runs of two kinds.
370
+
371
+ - **Runnable with nothing but Python** — `plate_line`, an Object-bearing line driven entirely
372
+ by environment scripts, and `shared_bench`, two jobs sharing one laboratory (`--jobs`).
373
+ - **Against real instrument servers** — `sila2_seal` (one instrument), `sila2_plate_cycle` (a
374
+ circuit through four instruments and an arm) and `sila2_plate_cycle_no_atc` (the same
375
+ circuit on a bench without a thermal cycler), over SiLA2; and `lads_seal` and
376
+ `lads_plate_cycle`, the first two workflows unchanged with every machine reached over LADS
377
+ OPC UA instead. `sila2_seal` also walks through what a run does when a machine stops
378
+ answering — before an operation, and in the middle of one.
379
+
380
+ The servers they are verified against are the reference lab,
381
+ [ofplang/mocklab](https://github.com/ofplang/mocklab): mock instruments serving SiLA2 or LADS
382
+ OPC UA by Docker Compose profile, over one simulated world. It is a reference, not a
383
+ requirement — the environments speak plain SiLA2 and LADS, so a bench is a change of hosts and
384
+ ports. `run_all_sila2_examples.py` and `run_all_lads_examples.py` run each set as a check.
340
385
 
341
386
  ## License
342
387
 
@@ -11,6 +11,7 @@ ofplang toolchain and exposes it under a single command:
11
11
  lc validate ... # check a workflow is well-formed portable v0
12
12
  lc schedule ... # compute a schedule for a workflow
13
13
  lc run ... # execute a workflow on the labcode backend
14
+ lc export ... # write documents out for people to read (optional: labcode[export])
14
15
  ```
15
16
 
16
17
  labcode is where a site-specific dialect and a custom runner (real lab hardware)
@@ -19,7 +20,7 @@ forward to the ofplang siblings unchanged; **`lc run` is the labcode dialect's o
19
20
  runner**: it drives the workflow on the labcode backend, running each device
20
21
  operation's script — supplied in the environment as an `x-labcode.script` extension
21
22
  on a process mode — out-of-process on a wall clock, so a long-running real operation
22
- never blocks the replan loop. See [`SPECIFICATIONS.md`](SPECIFICATIONS.md) for the
23
+ never blocks the replan loop. See [`docs/SPECIFICATIONS.md`](https://github.com/ofplang/labcode/blob/main/docs/SPECIFICATIONS.md) for the
23
24
  `x-labcode` extension.
24
25
 
25
26
  ```yaml
@@ -50,7 +51,25 @@ sibling packages unchanged; `lc run` is this package's own runner, built on them
50
51
  - [`ofplang-run`](https://github.com/ofplang/run) — the runner / simulator
51
52
 
52
53
  The language is defined in the [ofplang/spec](https://github.com/ofplang/spec)
53
- repository, and what labcode adds to it in [`SPECIFICATIONS.md`](SPECIFICATIONS.md).
54
+ repository, and what labcode adds to it in [`docs/SPECIFICATIONS.md`](https://github.com/ofplang/labcode/blob/main/docs/SPECIFICATIONS.md).
55
+
56
+ Talking to instruments, recording a run and exporting documents are optional, each an extra
57
+ installed into the interpreter that runs `lc` (labcode runs every script with that same
58
+ interpreter):
59
+
60
+ | extra | brings | needed for |
61
+ |---|---|---|
62
+ | `sila2` | the `sila2` client library | `flavor: sila2` scripts (§1.7) |
63
+ | `lads` | `asyncua`, an OPC UA client | `flavor: lads` scripts (§1.10) |
64
+ | `otel` | the OpenTelemetry SDK and exporter | `lc run --trace` |
65
+ | `export` | [`ofplang-export`](https://github.com/ofplang/export) | `lc export view` — the workflow and its plan as one self-contained HTML file |
66
+
67
+ ```sh
68
+ pip install 'labcode[sila2,lads]'
69
+ ```
70
+
71
+ Without an extra, labcode still runs; only what needs it — a script of that flavor,
72
+ `--trace`, or `lc export` — fails, with a message naming the extra to install.
54
73
 
55
74
  What `lc run` brings of its own, beyond dispatching:
56
75
 
@@ -70,12 +89,18 @@ What `lc run` brings of its own, beyond dispatching:
70
89
  - **object identity** — the reserved `_id` view key is declared on Object types and minted
71
90
  per object, so a physical thing can be followed through a run (§4).
72
91
  - **`flavor: sila2`** — a script that speaks SiLA2 gets its clients opened around it
73
- (§1.7). The client library itself is the `sila2` extra: `pip install labcode[sila2]`,
74
- installed into whichever interpreter runs the scripts.
92
+ (§1.7). The client library is the `sila2` extra.
93
+ - **`flavor: lads`** — the same for LADS OPC UA: a script is handed one LADS functional
94
+ unit per machine, already connected, with the program runs, state-machine methods and
95
+ cover movements LADS defines and the waits each needs (§1.10). A connection may name the
96
+ `device` and `unit` on the server, or leave them to be found. A script speaks one protocol;
97
+ an operation holding machines of both kinds commands only its own flavor's. The client
98
+ library is the `lads` extra.
75
99
  - **recording a run** — with `--trace`, what the run did is recorded as OpenTelemetry
76
100
  traces: one trace per run, a span per operation, and — measured inside the process that
77
101
  issued them — a span per SiLA2 connection, per command, and per gRPC call each of those
78
- made. Off by default; the extra is `pip install labcode[otel]`.
102
+ made. Off by default; it needs the `otel` extra. A `lads` operation is
103
+ recorded as its span alone for now.
79
104
 
80
105
  ## Usage
81
106
 
@@ -89,9 +114,11 @@ lc --version
89
114
 
90
115
  `lc` can also be run as a module: `python -m labcode <command> ...`.
91
116
 
92
- `lc validate` and `lc schedule` are the ofplang siblings' own CLIs unchanged, so their
93
- options are documented in those repositories. `lc run` is this package's own, and is
94
- described below.
117
+ `lc validate`, `lc schedule` and `lc export` are the ofplang siblings' own CLIs unchanged,
118
+ so their options are documented in those repositories — `lc export view plan.yaml -o
119
+ plan.html`, for one, writes the workflow and its plan as a single HTML file
120
+ ([ofplang/export](https://github.com/ofplang/export); needs `labcode[export]`). `lc run` is
121
+ this package's own, and is described below.
95
122
 
96
123
  ### `lc run`
97
124
 
@@ -223,9 +250,10 @@ duration and nothing is commanded. That is a real thing to write (an operator to
223
250
  stock up while the schedule waits for them) and an easy one to write by accident, so it is
224
251
  warned about.
225
252
 
226
- `flavor: sila2` is **refused on a refill route** for now: a sila2 script is handed clients,
227
- and which machine's clients a refill should receive — the replenisher's, or both ends' as a
228
- transport may ask for — is not settled. Use `python`.
253
+ `flavor: sila2` and `flavor: lads` are **refused on a refill route** for now: such a script
254
+ is handed clients, and which machine's clients a refill should receive — the replenisher's,
255
+ or both ends' as a transport may ask for — is not settled. Use `raw` (the default), which may
256
+ connect for itself.
229
257
 
230
258
  A refill holds the device it fills *and* the replenisher filling it, so it never overlaps
231
259
  the work it feeds. It is recorded (`--trace`) as a `replenishment` span naming both
@@ -288,11 +316,22 @@ OpenTelemetry's own export timeout allows: labcode sets no timeout of its own, s
288
316
 
289
317
  ## Examples
290
318
 
291
- [`examples/`](examples/README.md) holds three worked runs: `plate_line`, an
292
- Object-bearing line driven entirely by environment scripts and runnable with no
293
- hardware, and `sila2_seal` and `sila2_plate_cycle`, which drive the reference lab's
294
- SiLA2 servers for real. `sila2_seal` also walks through what a run does when a
295
- machine stops answering — before an operation, and in the middle of one.
319
+ [`examples/`](https://github.com/ofplang/labcode/blob/main/examples/README.md) holds worked runs of two kinds.
320
+
321
+ - **Runnable with nothing but Python** — `plate_line`, an Object-bearing line driven entirely
322
+ by environment scripts, and `shared_bench`, two jobs sharing one laboratory (`--jobs`).
323
+ - **Against real instrument servers** — `sila2_seal` (one instrument), `sila2_plate_cycle` (a
324
+ circuit through four instruments and an arm) and `sila2_plate_cycle_no_atc` (the same
325
+ circuit on a bench without a thermal cycler), over SiLA2; and `lads_seal` and
326
+ `lads_plate_cycle`, the first two workflows unchanged with every machine reached over LADS
327
+ OPC UA instead. `sila2_seal` also walks through what a run does when a machine stops
328
+ answering — before an operation, and in the middle of one.
329
+
330
+ The servers they are verified against are the reference lab,
331
+ [ofplang/mocklab](https://github.com/ofplang/mocklab): mock instruments serving SiLA2 or LADS
332
+ OPC UA by Docker Compose profile, over one simulated world. It is a reference, not a
333
+ requirement — the environments speak plain SiLA2 and LADS, so a bench is a change of hosts and
334
+ ports. `run_all_sila2_examples.py` and `run_all_lads_examples.py` run each set as a check.
296
335
 
297
336
  ## License
298
337
 
@@ -16,7 +16,7 @@ The extension answers two different questions, in two kinds of place. On a **pro
16
16
  mode**, a **transport route** and a **replenishment route** it says *what to run* (a
17
17
  `script`, §1.1–§1.4); on a **device**, a **transporter** and a **replenisher** it says
18
18
  *how to reach the machine* (a `connection`, §1.5), which is what lets a script be the
19
- commands alone (§1.7). The division is the same each time: the machine has an address,
19
+ commands alone (§1.7 for SiLA2, §1.10 for LADS OPC UA). The division is the same each time: the machine has an address,
20
20
  and the thing it does to something else has a procedure. Nowhere else — see §1.8.
21
21
 
22
22
  An environment process mode (§5) may carry an `x-labcode` mapping holding a `script`: the
@@ -46,17 +46,18 @@ still validates and schedules as plain v0. Only labcode interprets it.
46
46
  - `x-labcode.script`, if present, MUST be a mapping with:
47
47
  - `language`: MUST be `python`.
48
48
  - `code`: MUST be a string (an implementation-provided Python function body).
49
- - `flavor` (optional, default `raw`): MUST be `raw` or `sila2` — how `code` is meant to
50
- be run (§1.7). `sila2` is the **recommended** way to drive a SiLA2 lab: the code is the
51
- commands alone and labcode supplies the clients. `raw` is the whole function body,
52
- written by its author — the general escape hatch, and what a script that connects for
53
- itself (or speaks something other than SiLA2) uses. On a **replenishment route**
54
- `sila2` is an error in this version (§1.4).
49
+ - `flavor` (optional, default `raw`): MUST be `raw`, `sila2` or `lads` — how `code` is
50
+ meant to be run. `sila2` (§1.7) is the **recommended** way to drive a SiLA2 lab, and
51
+ `lads` (§1.10) a LADS OPC UA one: the code is the commands alone and labcode supplies
52
+ the clients. `raw` is the whole function body, written by its author — the general
53
+ escape hatch, and what a script that connects for itself (or speaks some other
54
+ protocol) uses. On a **replenishment route** `sila2` and `lads` are errors in this
55
+ version (§1.4).
55
56
  - `endpoints` (**transport routes only**, optional, default `false`): MUST be a boolean —
56
57
  whether this move is also given clients for the devices at either **end** of its route,
57
58
  not only its `transporter` (§1.7). A process mode may not declare it: a mode's machines
58
- are the ones it lists. A `sila2` script on a route with **no transporter** (§1.3) MUST
59
- declare it `true`: the ends are then the only machines there are, and a script is never
59
+ are the ones it lists. A `sila2` or `lads` script on a route with **no transporter**
60
+ (§1.3) MUST declare it `true`: the ends are then the only machines there are, and a script is never
60
61
  given a machine it did not ask for.
61
62
 
62
63
  **Unknown keys are an error** — in `x-labcode` at every position, and in the mappings it
@@ -134,7 +135,8 @@ by `(transporter, from, to)`, so a null one matches a null one, and the script's
134
135
  `transporter` local is `None`.
135
136
 
136
137
  Such a move is performed by the **devices at either end of the route** — for a move within
137
- one device, that device. So a `flavor: sila2` script on such a route must declare
138
+ one device, that device. So a `flavor: sila2` (or `lads`) script on such a route must
139
+ declare
138
140
  **`endpoints: true`** (§1.6), and at least one of those devices must declare a
139
141
  `connection`; either missing is a front-door error. The `endpoints` request is *required*
140
142
  rather than inferred: it is the author's statement of which machines the script drives, and
@@ -178,8 +180,8 @@ A replenishment script is **side-effect only**, as a transport's is: its return
178
180
  ignored and no output is verified. An exception is a graceful failure — the refill ends
179
181
  `failed` and the run stops, like any activity failure.
180
182
 
181
- `flavor: sila2` is **an error** on a replenishment route in this version. A `sila2` script
182
- is handed clients (§1.7), and which machine's clients a refill should receive — the
183
+ `flavor: sila2` is **an error** on a replenishment route in this version, and so is
184
+ `flavor: lads`. Such a script is handed clients (§1.7, §1.10), and which machine's clients a refill should receive — the
183
185
  replenisher's, or both ends' as a transport route may ask for — is not settled. Refusing
184
186
  says so; running the script without the clients it asked for would not. Use `raw` (the
185
187
  default), which may of course connect for itself.
@@ -204,6 +206,17 @@ devices:
204
206
  x-labcode:
205
207
  connection: { kind: sila2, host: 127.0.0.1, port: 50053, insecure: true }
206
208
 
209
+ - id: thermal_cycler
210
+ spots: [block]
211
+ x-labcode:
212
+ connection:
213
+ kind: lads
214
+ host: 127.0.0.1
215
+ port: 4844
216
+ insecure: true
217
+ device: ThermalCycler # optional: which LADS device on that server...
218
+ unit: Cycler # ...and which of its functional units (§1.10)
219
+
207
220
  transporters:
208
221
  - id: arm
209
222
  x-labcode:
@@ -212,13 +225,22 @@ transporters:
212
225
 
213
226
  | field | required | default | meaning |
214
227
  |---|---|---|---|
215
- | `kind` | no | `sila2` | the protocol; `sila2` is the only value this version defines |
228
+ | `kind` | no | `sila2` | the protocol: `sila2` or `lads` (LADS OPC UA) |
216
229
  | `host` | **yes** | — | a non-empty string |
217
230
  | `port` | **yes** | — | an integer in 1..65535 |
218
- | `insecure` | no | `false` | connect without TLS |
219
-
220
- **TLS is not supported in this version.** There is nowhere in the schema to put the
221
- credentials it needs (a root certificate, at least), so a `connection` whose effective
231
+ | `insecure` | no | `false` | connect without TLS (for `lads`: with OPC UA security mode None) |
232
+ | `device` | no | the only one | `lads` only: the browse name of the LADS device on the server |
233
+ | `unit` | no | the only one | `lads` only: the browse name of that device's functional unit |
234
+
235
+ `device` and `unit` are a **LADS** address and nothing else: on a `sila2` connection either
236
+ one is an error, and each MUST be a non-empty string. Omitted, they mean *the only one there
237
+ is* — a server with one device of one unit, which is the common case, needs neither — and
238
+ the omission is resolved when the script connects, not at the front door, since only the
239
+ server knows what it holds (§1.10).
240
+
241
+ **TLS is not supported in this version** — for `lads`, no OPC UA security mode other than
242
+ None. There is nowhere in the schema to put the credentials either needs (a root
243
+ certificate, a client certificate), so a `connection` whose effective
222
244
  `insecure` is false is rejected at the front door — including one that simply omits the
223
245
  key and takes the default — and refused again if a script reaches the connect helper
224
246
  directly. Every connection must therefore say `insecure: true` today.
@@ -226,12 +248,13 @@ The default stays `false` so that supporting TLS later is a pure addition: new f
226
248
  and the error goes away. The check applies to every declared `connection`, whether or not
227
249
  a script uses it.
228
250
 
229
- **A `sila2` script needs somewhere to connect** (checked at the front door):
251
+ **A `sila2` or `lads` script needs somewhere to connect** — a `connection` of its own
252
+ `kind` (checked at the front door):
230
253
 
231
254
  - a mode script with `flavor: sila2` requires **at least one** of that mode's `devices[]`
232
- to declare a `connection`;
233
- - a transport script with `flavor: sila2` requires that route's `transporter` to declare
234
- one — or, on a route with **no transporter** (§1.3), requires `endpoints: true` and at
255
+ to declare a `sila2` `connection`, and one with `flavor: lads` a `lads` one;
256
+ - a transport script requires that route's `transporter` to declare one of the script's
257
+ kind — or, on a route with **no transporter** (§1.3), requires `endpoints: true` and at
235
258
  least one of the devices at its ends to declare one, those being the machines that
236
259
  perform such a move.
237
260
 
@@ -240,7 +263,8 @@ either **end** of its route (§1.7), but those are *not* required to declare a `
240
263
  a route through a plain holding location is ordinary, and the end without an address is
241
264
  simply not connected to (a **warning** when *neither* end has one, since then the request
242
265
  does nothing — an **error** on a route with no transporter, which has nothing else to
243
- drive). The transporter is the one that must be reachable, because it is the machine that
266
+ drive). An end whose `connection` is of the **other** kind is not connected to either, and
267
+ is warned about: a script speaks one protocol (§1.10). The transporter is the one that must be reachable, because it is the machine that
244
268
  does the moving — and the one `sila2_client` names; where there is none, that is the source
245
269
  device, for the same reason. Asking a `raw` script for endpoint
246
270
  clients is an **error**: a raw script is handed no clients at all, so the request cannot be
@@ -420,7 +444,7 @@ x-labcode:
420
444
  more: waiting for an observable command to finish (the standard `sila2` polling pattern)
421
445
  belongs in the code, as it does in a `raw` script.
422
446
 
423
- #### 1.6.1 `labcode.sila2_commands` — the polling loop, written once
447
+ #### 1.7.1 `labcode.sila2_commands` — the polling loop, written once
424
448
 
425
449
  Waiting for an observable command is the same loop in every script that issues one, so
426
450
  labcode ships it. It is an **ordinary module**, reached by an ordinary import — nothing is
@@ -513,6 +537,108 @@ x-labcode:
513
537
  - `lc run` overrides it for one run: `--op-timeout SECONDS`, or `--no-op-timeout` for no
514
538
  limit at all. The order is flag, then document, then default.
515
539
 
540
+ ### 1.10 Calling convention (`flavor: lads`)
541
+
542
+ A `lads` script is the LADS OPC UA counterpart of a `sila2` one (§1.7), and everything §1.7
543
+ says of the flavor holds for it with the names changed: which machines an operation is
544
+ handed and in what order, the `endpoints` request on a transport, connections lasting one
545
+ operation and closed on any exit, a machine held without a client explaining itself when
546
+ indexed (`lads_not_connected`, `lads_endpoints_not_requested`), and the rest of the work
547
+ being the script's own. The two names it sees are:
548
+
549
+ | name | meaning |
550
+ |---|---|
551
+ | `lads_clients` | a `LadsUnit` for each machine, by **machine id**, in the order of §1.7 |
552
+ | `lads_client` | the first of them — for a transport its `transporter`, or the source device on a route with none |
553
+
554
+ Both are reserved, as `sila2_clients` / `sila2_client` are, and only in a `lads` script.
555
+
556
+ ```yaml
557
+ x-labcode:
558
+ script:
559
+ language: python
560
+ flavor: lads
561
+ code: |
562
+ # `lads_client` is the sealer's functional unit, already connected.
563
+ lads_client.write_target("SealingTemperature", 170)
564
+ lads_client.run_program("StartCycle") # returns once the run has ended
565
+ return {"cycle_count": int(lads_client.read("CycleCount"))} # a vendor variable
566
+ ```
567
+
568
+ - **What a script is handed is one functional unit, not a raw client.** A SiLA2 client is
569
+ typed by its server's features, so a `sila2` script reads well with nothing in between; an
570
+ OPC UA client is not, and a script driving LADS through it directly would be mostly
571
+ namespaces, NodeIds and Variants. A `LadsUnit` is a thin, synchronous view of the unit
572
+ named by the connection's `device` / `unit` (§1.5), built only on what the LADS companion
573
+ specification (OPC 30500, LADS 1.0.0) defines:
574
+
575
+ | call | LADS |
576
+ |---|---|
577
+ | `run_program(template_id, **properties)` | `StartProgram`, then wait for the run's result in `ProgramManager/ResultSet`; returns its properties (§1.10.1) |
578
+ | `start_program(template_id, **properties)` | `StartProgram` alone; returns the run id |
579
+ | `results()` | the result nodes in `ProgramManager/ResultSet` |
580
+ | `stop()`, `abort()`, `clear()` | the unit state machine's methods, each waiting until the unit has settled; returns that state |
581
+ | `reset()` | `abort()` then `clear()`, failing unless the unit ends `Stopped` — the counterpart of a SiLA2 `Reset` |
582
+ | `state(owner="FunctionalUnitState")` | a state machine's `CurrentState` (`Stopped`, `Running`, …) |
583
+ | `cover(name, action)` | a cover function's `Open` / `Close`, waiting until it has settled, and failing unless it ends `Opened` / `Closed` |
584
+ | `write_target(function, value)` | a control function's `TargetValue` |
585
+ | `read(path)`, `write(path, value)`, `child(path)` | any node below the unit, by browse names joined with `/`, in whichever namespace they live |
586
+ | `call(owner, method, *variants)` | any LADS method of the object at `owner` |
587
+ | `raw` | the asyncua client itself, for anything none of this covers |
588
+
589
+ A method call or write the server refuses fails the operation (`lads_call_failed`). Where
590
+ the server adds a `LastError` variable to the unit — not part of LADS; the reference lab's
591
+ servers do — the failure quotes it, since a StatusCode says *that* the server refused and
592
+ not *why*.
593
+ - **Which unit is resolved when connecting.** labcode looks for LADS devices under the
594
+ server's DI `DeviceSet` and for functional units in the chosen device's
595
+ `FunctionalUnitSet`, recognising each by its type (a subtype counts). A `device` or `unit`
596
+ that is named but absent fails the operation (`lads_target_not_found`), as does an omitted
597
+ one where the server has several (`lads_ambiguous_target`, naming them) and a server that
598
+ is not a LADS server at all (`lads_not_a_lads_server`). The front door cannot check any of
599
+ this: it needs the server.
600
+ - **One protocol per script.** An operation may hold machines of both kinds — a LADS sealer
601
+ loaded by a SiLA2 arm — but a script is handed clients of its own flavor only. A held
602
+ machine whose `connection` is of the other kind is treated as one held without a client,
603
+ and indexing it says so (`lads_other_protocol`, or `sila2_other_protocol` the other way
604
+ round). A mode none of whose devices is of the script's kind is an error at the front door
605
+ (§1.5); an operation that must command both kinds is written `raw`.
606
+ - **The client library is the `lads` extra** (`asyncua`), needed by the interpreter that
607
+ runs the scripts. labcode imports it only when a `lads` script connects, so it runs
608
+ without it otherwise; a `lads` script without it fails with `lads_unavailable`.
609
+ - **Probing is unchanged** (§1.6): it opens a TCP connection, whatever the `kind`.
610
+ - **A run's record** (`lc run --trace`) holds a `lads` operation's span, but nothing inside
611
+ it yet: the connection and the commands are traced for SiLA2 only (§5).
612
+
613
+ #### 1.10.1 `labcode.lads_commands` — the waits, written once
614
+
615
+ A LADS method returns once the server has **begun** carrying it out, and the effect follows:
616
+ `StartProgram` returns a run id while the program runs, `Stop` leaves the unit `Stopping`, a
617
+ cover passes through `Opening`. So the waits are the script's, as `settle` is in §1.7.1, and
618
+ labcode ships them in an ordinary module. A script normally reaches them through its unit
619
+ (`lads_client.run_program(...)`, `lads_client.stop()`), which calls them; importing them
620
+ reserves nothing, as with `settle`.
621
+
622
+ - `run_program(unit, template_id, *, timeout=3600.0, poll=1.0, **properties)` starts the
623
+ program and waits for the result whose `DeviceProgramRunId` (or, failing that, browse
624
+ name) is the run id. The **result** is the end of a run, not the unit's state returning to
625
+ `Stopped`: a server records it once the run is over and its effects are visible. The
626
+ properties are passed as LADS' `KeyValueType[]`, stringified.
627
+ - **A run that did not complete fails the operation** (`lads_program_failed`). LADS does not
628
+ say how a result reports failure, so labcode reads an `Outcome` property where the server
629
+ records one (anything but `Completed` is a failure) and otherwise the unit's state (a unit
630
+ left `Aborted` failed).
631
+ - `wait_for_state(unit, *, owner="FunctionalUnitState", label, timeout=600.0, poll=0.2)`
632
+ waits until a state machine has left every transient state (`Stopping`, `Aborting`,
633
+ `Clearing`, `Starting`, `Opening`, `Closing`) and returns the state it settled in, or fails
634
+ with `lads_transition_timeout`.
635
+ - **A timeout is not a cancel**, as in §1.7.1 — though here it could be: LADS has `Abort`.
636
+ A wait that times out (`lads_program_timeout`) still fails only the operation and leaves the
637
+ instrument as it is, so the same mistake leaves the lab in the same state whichever
638
+ protocol the instrument speaks. A script that wants the abort calls `abort()` itself.
639
+ - The timeouts are real seconds, the inner limit under `op_timeout` (§1.9), and unrelated to
640
+ a mode's `duration`, exactly as `settle`'s are.
641
+
516
642
  ## 2. Code source resolution and exclusivity
517
643
 
518
644
  For a dispatched `(process, mode)`, labcode resolves the code to run in this order:
@@ -646,13 +772,20 @@ ids per physical Object swaps in `RealUuid4Generator` (via
646
772
 
647
773
  ## 5. Not yet in this version (roadmap)
648
774
 
649
- - **`flavor: sila2` on a replenishment route** — refused today (§1.4). What has to be
650
- settled first is which machine's clients a refill script receives: the replenisher's
651
- alone, or both ends' as a transport route may ask for with `endpoints`. Until then a
652
- refill that must speak SiLA2 uses a `raw` script and connects for itself.
775
+ - **`flavor: sila2` or `lads` on a replenishment route** — refused today (§1.4). What has
776
+ to be settled first is which machine's clients a refill script receives: the
777
+ replenisher's alone, or both ends' as a transport route may ask for with `endpoints`.
778
+ Until then a refill that must speak SiLA2 or LADS uses a `raw` script and connects for
779
+ itself.
653
780
  - **A deeper probe** — asking a machine something (a SiLA2 property read) rather than only
654
781
  opening a connection to it, so "answering" can be checked and not just "listening"
655
782
  (§1.6). It would be an opt-in depth, since it costs a real exchange per check.
656
783
  - **Probing in parallel** — checking machines concurrently, so a lab with many unreachable
657
784
  machines does not pay for them one timeout at a time (§1.6).
658
- - **TLS** — the fields a secure connection needs, lifting the restriction in §1.5.
785
+ - **TLS** — the fields a secure connection needs, lifting the restriction in §1.5; for
786
+ LADS, an OPC UA security policy and the certificates it takes.
787
+ - **Tracing a `lads` operation** — spans for the connection and each method call inside an
788
+ operation's span, as a `sila2` one has (§1.10).
789
+ - **Aborting on a timeout, as an opt-in** — LADS can cancel a run where SiLA2 cannot, so a
790
+ `lads` wait could abort what it gave up on. It is off (§1.10.1) until it can be asked for
791
+ per call, since the state an abort leaves is not always better than the one a run leaves.