plesty-lib 0.3.5.dev2__tar.gz → 0.3.6__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.
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/CHANGELOG.md +352 -3
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/PKG-INFO +1 -1
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/analyzer.md +27 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/architecture.md +16 -12
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/device/base_device.md +52 -1
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/device/param_system.md +80 -2
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/device/test_helper.md +92 -4
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/device/traffic_manager.md +6 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/index.md +8 -5
- plesty_lib-0.3.6/docs/service.md +112 -0
- plesty_lib-0.3.6/docs/sim.md +94 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/toc.yaml +2 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/gl-secret-detection-report.json +7 -7
- plesty_lib-0.3.6/plesty/lib/device/device_utils.py +231 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/device/funcs.py +52 -2
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/device/params.py +209 -2
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/service/__init__.py +44 -1
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/service/tcp_ip_server.py +213 -5
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/test/client_field_test.py +88 -9
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/test/field_test.py +330 -24
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/test/report_artifact.py +7 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/traffic/__init__.py +26 -3
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/traffic/apt.py +9 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/traffic/serial.py +33 -3
- plesty_lib-0.3.6/plesty/lib/traffic/serial_ports.py +86 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/traffic/visa.py +417 -27
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/ui/qt/plot.py +2 -2
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/ui/qt/shell.py +1 -1
- plesty_lib-0.3.6/tests/manual/stop_closes_the_device.py +116 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_client_field_test.py +123 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_device_funcs.py +170 -1
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_device_params.py +215 -1
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_field_test.py +250 -2
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_misc.py +12 -2
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_report_artifact.py +7 -0
- plesty_lib-0.3.6/tests/test_service_loop_factory.py +78 -0
- plesty_lib-0.3.6/tests/test_tcp_ip_orphan_reply.py +93 -0
- plesty_lib-0.3.6/tests/test_tcp_ip_stop_signal.py +85 -0
- plesty_lib-0.3.6/tests/test_tcp_ip_traffic.py +196 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_traffic_drivers.py +180 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_visa_discovery.py +308 -0
- plesty_lib-0.3.6/tests/test_visa_mav_gating.py +139 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/uv.lock +39 -2
- plesty_lib-0.3.5.dev2/plesty/lib/device/device_utils.py +0 -108
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/.gitignore +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/.gitlab-ci.yml +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/LICENSE +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/LICENSES/LGPL-3.0-or-later.txt +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/README.md +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/REUSE.toml +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/api_reference.md +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/contributing.md +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/data_schemas.md +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/device/cmd_solver.md +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/device/composite_device.md +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/device/error_handling.md +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/device/func_system.md +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/device/index.md +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/device/logging_system.md +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/experiment.md +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/imgs/device_layers.svg +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/imgs/device_standard.svg +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/imgs/plesty_framework.svg +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/monitor.md +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/quickstart.md +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/ui.md +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/__init__.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/analyzer/__init__.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/analyzer/base_analyzer.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/data/__init__.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/data/array.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/data/ctype_manager.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/data/io.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/data/table.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/data/types.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/data/units.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/device/__init__.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/device/async_wrapper.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/device/base_apt_device.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/device/base_device_sync.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/device/base_tcp_scpi_device.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/device/base_visa_scpi_device.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/device/composite_device.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/device/doc.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/device/telemetry.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/experiment/__init__.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/experiment/assets/preflight.yaml +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/experiment/base_experiment.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/experiment/journal.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/experiment/preflight.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/experiment/runs.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/experiment/schedule.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/monitor/__init__.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/monitor/assets/viz.yaml +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/monitor/base_monitor.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/monitor/sources.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/monitor/viz.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/service/resource_manager.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/service/tcp_ip_client.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/sim/__init__.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/sim/apt.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/sim/data_generator.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/sim/demo_device.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/solver/__init__.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/solver/iceblock.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/solver/scpi.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/test/__init__.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/test/analyzer_pipeline.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/test/device_func_system.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/test/device_param_system.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/test/device_pipeline.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/test/experiment_pipeline.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/test/field_test_concurrency.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/test/grouped_param_keys.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/test/resource_allocation.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/test/schema_params.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/test/schema_refresh.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/traffic/apt_protocol.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/traffic/tcp_ip.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/traffic/usb_utils.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/traffic/utils.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/ui/__init__.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/ui/assets/palette.json +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/ui/assets/shell.qss +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/ui/monitor_panel.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/ui/panel.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/ui/qt/__init__.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/ui/qt/app.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/ui/qt/recorder.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/ui/theme.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/utils/__init__.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/utils/config.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/utils/dll_utils.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/utils/error_utils.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/utils/logger.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/utils/registry.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/utils/settings.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/pyproject.toml +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_analyzer.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_analyzer_pipeline.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_async_wrapper_threading.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_composite_config.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_config.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_data_array.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_data_io_hdf5.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_data_types.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_demo_device.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_device_apt.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_device_base.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_device_data_path.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_device_pipeline.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_device_scpi.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_device_telemetry.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_experiment.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_experiment_pipeline.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_experiment_preflight.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_experiment_runs.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_monitor.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_schema_params.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_schema_refresh.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_service_manager.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_settings.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_shipped_imports.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_sim.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_solver.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_tcp_ip_resources.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_traffic_orphan.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_traffic_utils.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_ui.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_utils.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_viz.py +0 -0
|
@@ -1,10 +1,358 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
- **A client holding an allocation can call schema operations again, and those
|
|
6
|
+
calls are now access-controlled.** The server appends the caller's allocation
|
|
7
|
+
to every `call` it forwards, but a schema operation is a generated closure
|
|
8
|
+
that validates its inputs strictly and so rejected the injected key by name:
|
|
9
|
+
`KeyError: measure_analog() got unexpected parameter(s): ['resources']`. Any
|
|
10
|
+
device that registers resources and exposes schema operations failed on
|
|
11
|
+
*every* operation call from an allocated client, which made per-client
|
|
12
|
+
locking unusable on schema-driven devices — that is, on all of them.
|
|
13
|
+
`@expose_to_api` methods escaped it only because a `**kwargs` absorbed the
|
|
14
|
+
key; synthetic operations have no signature to widen, and the strictness is
|
|
15
|
+
what enforces the schema's types and ranges.
|
|
16
|
+
|
|
17
|
+
`resources` is now taken out before validation and carried in the solver
|
|
18
|
+
request beside `func_meta`, where a solver can see the allocation alongside
|
|
19
|
+
the channels the arguments name.
|
|
20
|
+
|
|
21
|
+
The same gap left operations unguarded: `_user_permission_check` was
|
|
22
|
+
consulted in `write` and `query` only, so once the key stopped raising, a
|
|
23
|
+
client holding `ao0` could still run any operation on any line. Operations
|
|
24
|
+
now consult it too, before validating their arguments — a caller who may not
|
|
25
|
+
run one learns nothing about its inputs. A denied operation raises
|
|
26
|
+
`PermissionError` rather than returning `False` as `write` does, because an
|
|
27
|
+
operation's return value is arbitrary and a `False` denial is
|
|
28
|
+
indistinguishable from a legitimate result. A `FunctionSystem` used on its
|
|
29
|
+
own has no such policy and permits everything, as before.
|
|
30
|
+
|
|
31
|
+
- **`run_server()` picks the server's event loop explicitly, so a global
|
|
32
|
+
policy no longer has to.** Importing `plesty.lib.service` installs a Windows
|
|
33
|
+
loop policy as a side effect: that is what lets a bare `asyncio.run` in a
|
|
34
|
+
generated `__main__` get a selector loop, which pyzmq needs because
|
|
35
|
+
`ProactorEventLoop` has no `add_reader`. Both `asyncio.set_event_loop_policy`
|
|
36
|
+
and `WindowsSelectorEventLoopPolicy` are deprecated in Python 3.14 — two
|
|
37
|
+
warnings on every device server import on a bench — and removed in 3.16,
|
|
38
|
+
where the import would raise instead.
|
|
39
|
+
|
|
40
|
+
`plesty.lib.service.run_server(main())` replaces `asyncio.run(main())` and
|
|
41
|
+
hands the loop factory to the call site, which is the supported mechanism
|
|
42
|
+
and works on every Python this package targets. `server_loop_factory()`
|
|
43
|
+
exposes the same choice for callers that build their own runner.
|
|
44
|
+
`SelectorEventLoop` itself is not deprecated; only the policy machinery is.
|
|
45
|
+
|
|
46
|
+
The policy install stays for now, skipped only where it would raise rather
|
|
47
|
+
than warn (3.16+), because it is the one thing keeping an unmigrated entry
|
|
48
|
+
point working on Windows. Nothing on a bench changes behaviour: this adds
|
|
49
|
+
the supported path and leaves the old one standing until the entry points
|
|
50
|
+
move (sdk#35).
|
|
51
|
+
|
|
52
|
+
- **The Analyzer contract is locked, and now says so.** It shipped in 0.3.2
|
|
53
|
+
and has not changed since, the SDK's gate a1 and `plesty init analyzer`
|
|
54
|
+
generate against it, and the pilot migration put a real analysis module on
|
|
55
|
+
it without touching any analysis code. All of that was true and none of it
|
|
56
|
+
was written down where an analyzer author would look: `docs/analyzer.md`
|
|
57
|
+
described the interface without saying whether it was safe to depend on,
|
|
58
|
+
which is the one question a contributor has before writing the first
|
|
59
|
+
subclass. A new "Contract stability" section states what is fixed — the two
|
|
60
|
+
schemas, the single `analyze()`, the validating `__call__`, the provenance
|
|
61
|
+
stamp, the five pipeline gates — what may still be added, and what would
|
|
62
|
+
take a major version to change. Documentation only; no code changed.
|
|
63
|
+
|
|
64
|
+
- **Every served request now writes one record to the log**, so a monitor can
|
|
65
|
+
show what is on the wire without a new transport or a wire-format change.
|
|
66
|
+
The record carries what was asked, of what, by whom, how it ended and how
|
|
67
|
+
long it took — and never the payload: a result can be a spectrum or a stack
|
|
68
|
+
of frames, and a log is the wrong home for measurement data. A failure
|
|
69
|
+
records the exception *type*, not its message, which can carry a path or an
|
|
70
|
+
address. A reply that could not be routed is recorded as `delivered: false`
|
|
71
|
+
with its outcome intact — the client timed out and reconnected, the device
|
|
72
|
+
did the work anyway, and the caller is about to report a failure that did
|
|
73
|
+
not happen; that case is the one a monitor most needs to show.
|
|
74
|
+
|
|
75
|
+
A `describe` is recorded at `DEBUG` rather than `INFO` — liveness, not work.
|
|
76
|
+
A supervisor watching a bench polls it on a timer (plesty-bench's console
|
|
77
|
+
asks every declared device every three seconds, whether or not anyone is
|
|
78
|
+
looking) and each probe opens a fresh socket, so it arrives under a new
|
|
79
|
+
client identity every time. At `INFO` that is some 1,200 records per device
|
|
80
|
+
per hour on an idle bench, and a monitor would show almost nothing but the
|
|
81
|
+
console asking whether the server is alive, with the calls that moved the
|
|
82
|
+
instrument buried among them. Recorded, not dropped: raising the logger to
|
|
83
|
+
`DEBUG` brings them back.
|
|
84
|
+
|
|
85
|
+
Written through the `plesty.server.traffic` logger, prefixed `[TRAFFIC]`, so
|
|
86
|
+
it can be quieted or raised on its own:
|
|
87
|
+
`logging.getLogger("plesty.server.traffic").setLevel(logging.WARNING)`. One
|
|
88
|
+
line per request — a stage polled at 5 Hz writes roughly 2 MB an hour, and
|
|
89
|
+
the history reaches back exactly as far as the log does. Reporting never
|
|
90
|
+
raises: a malformed request or a throwing handler costs the record, not the
|
|
91
|
+
request. For plesty-bench#3.
|
|
92
|
+
|
|
93
|
+
- **A Windows stop now closes the device instead of killing the server.**
|
|
94
|
+
`CTRL_BREAK_EVENT` is the only console control event a supervisor can aim
|
|
95
|
+
at one process group, so it is what stops a single device server; Python
|
|
96
|
+
delivers it as `SIGBREAK`, whose default action is to terminate the process
|
|
97
|
+
outright (exit `0xC000013A`). Nothing installed a handler — not this
|
|
98
|
+
library, not the SDK scaffold — so every generated server was killed on
|
|
99
|
+
every stop: no traceback, no output, and the `finally` that closes the
|
|
100
|
+
device never ran. Whatever `__aexit__` releases was leaked each time. For a
|
|
101
|
+
spectrometer that was an orphaned `AddInProcess` still holding the camera,
|
|
102
|
+
which then refused the next start until an elevated `taskkill` or a power
|
|
103
|
+
cycle cleared it.
|
|
104
|
+
|
|
105
|
+
`DeviceTCPIPServer.run` now serves inside `stop_on_break()`, which hands
|
|
106
|
+
`SIGBREAK` to the interpreter's own `SIGINT` handler for the duration and
|
|
107
|
+
restores what it found afterwards. A stop raises `KeyboardInterrupt` in the
|
|
108
|
+
main thread on every platform, so the existing shutdown path runs and the
|
|
109
|
+
device is released. Servers get this by upgrading the library — no
|
|
110
|
+
regeneration from the template. Off Windows, and off the main thread where
|
|
111
|
+
handlers cannot be installed at all, it does nothing.
|
|
112
|
+
|
|
113
|
+
- **A missed disconnect acknowledgement and a lost answer no longer read as
|
|
114
|
+
the same event.** The ROUTER refuses to route a reply whose client has
|
|
115
|
+
gone, and one warning covered both. A `disconnect` ack that misses the
|
|
116
|
+
composite's one-second window is routine — the client asked to be released
|
|
117
|
+
and the server was a moment slow finishing an acquisition — and it happens
|
|
118
|
+
once per teardown, which is what made the log unreadable. It is now debug.
|
|
119
|
+
The other case is an answer to work the device has already done: the caller
|
|
120
|
+
saw a timeout and will report a failure that did not happen. That warns and
|
|
121
|
+
names what was asked for (`call acquire`, `write CameraSettings…`), because
|
|
122
|
+
a reply reported as merely lost is not something anyone can act on
|
|
123
|
+
afterwards; a request that cannot be parsed is assumed to matter rather
|
|
124
|
+
than dismissed.
|
|
125
|
+
|
|
126
|
+
- **`VI_ERROR_IN_PROGRESS` at open is no longer reported as a wedged
|
|
127
|
+
instrument.** The previous entry called it a finished diagnosis — wedged
|
|
128
|
+
before the session existed, every command will time out, only a power cycle
|
|
129
|
+
helps — and said so at error level. The logs do not support it. On one PM101R
|
|
130
|
+
it was the opening state of **ten of twelve** consecutive starts, and connect
|
|
131
|
+
succeeded on all twelve: identity, sensor query and limit reads all answered
|
|
132
|
+
normally. The message was telling people to unplug a working instrument,
|
|
133
|
+
which is the fastest way to spend the credibility the real remedy needs.
|
|
134
|
+
|
|
135
|
+
It is now recorded at debug and interpreted later. The diagnosis needs two
|
|
136
|
+
facts, and neither means anything alone: the clear-on-open found a transfer
|
|
137
|
+
in flight, **and** nothing on the session has ever been answered. One answer
|
|
138
|
+
proves the instrument was reachable, so a later failure is an ordinary fault
|
|
139
|
+
for the recovery ladder; a session that opened onto someone else's transfer
|
|
140
|
+
and has never been answered is the case worth a paragraph. Found by reading
|
|
141
|
+
the whole log rather than the starts we happened to trigger — a four-sample
|
|
142
|
+
read of the same data had suggested an alternating TMO/IN_PROGRESS pattern
|
|
143
|
+
that twelve samples show is simply IN_PROGRESS 10 of 12.
|
|
144
|
+
|
|
145
|
+
- **A query now waits for MAV before it reads, so a silent instrument strands
|
|
146
|
+
nothing.** This is the cause behind the entry below rather than another
|
|
147
|
+
symptom of it. A USBTMC query is two exchanges — the command out, then
|
|
148
|
+
`REQUEST_DEV_DEP_MSG_IN` and a bulk-in read — and an instrument is entitled
|
|
149
|
+
to stay silent: an undefined header is answered with `-113` in the error
|
|
150
|
+
queue and no response at all. When that happened the read timed out and we
|
|
151
|
+
walked away while the device still held the request. Nothing retracted it.
|
|
152
|
+
`viClear` cannot be queued behind a pending bulk-in, which is exactly the
|
|
153
|
+
`VI_ERROR_IN_PROGRESS` the ladder kept reporting, and reopening the session
|
|
154
|
+
replaces a host-side handle without touching the device — so the ladder's own
|
|
155
|
+
diagnosis was right and every rung it had was powerless against it. Five such
|
|
156
|
+
queries in one session stopped a PM101R and a PM400 servicing USB-TMC at all,
|
|
157
|
+
on different benches, and only unplugging them helped.
|
|
158
|
+
|
|
159
|
+
`_query_when_ready` reads the status byte first and issues the read only once
|
|
160
|
+
bit 4 says a message is waiting. Measured on the PM101R this bug had already
|
|
161
|
+
cost a power cycle: `MEAS:ENER?`, one of the five that wedged it, became a
|
|
162
|
+
four-second non-event — the status byte sat at `0x04` (EAV set, MAV clear: an
|
|
163
|
+
error queued, no answer coming) across forty polls, and the next three
|
|
164
|
+
queries answered in single-digit milliseconds. Reading the status byte costs
|
|
165
|
+
a control transfer and touches no bulk endpoint, which is what makes it safe
|
|
166
|
+
to ask before committing to a read.
|
|
167
|
+
|
|
168
|
+
Silence now raises `NoMessageError`, a `VisaIOError` carrying the timeout
|
|
169
|
+
code so existing callers are unchanged, and the ladder skips every rung for
|
|
170
|
+
it: there is no transfer to rescue, and a clear cannot conjure an answer out
|
|
171
|
+
of an instrument that has already declined to give one. Three timeouts and a
|
|
172
|
+
session teardown become one clean "it does not answer that".
|
|
173
|
+
|
|
174
|
+
Two guards, because a status byte that never sets MAV is ambiguous — the
|
|
175
|
+
instrument may have nothing to say, or may simply not maintain the bit. The
|
|
176
|
+
gate does not fire until MAV has been seen once on the session; before that a
|
|
177
|
+
silent byte falls back to reading anyway, which is precisely the old
|
|
178
|
+
behaviour and no worse, and an answer switches gating off with a warning. A
|
|
179
|
+
backend that cannot report a status byte at all falls back the same way.
|
|
180
|
+
|
|
181
|
+
**This is prevention, not recovery, and the difference matters.** Nothing
|
|
182
|
+
here implements the USBTMC abort sequence (`ABORT_BULK_IN` →
|
|
183
|
+
`CHECK_ABORT_BULK_IN_STATUS` → `INITIATE_CLEAR`), so a transfer that does get
|
|
184
|
+
stranded still needs the instrument to lose power. Three routes remain: the
|
|
185
|
+
first query of a session, which is unguarded until MAV has been proven; a
|
|
186
|
+
resource without a separable status read, which keeps the old path; and
|
|
187
|
+
`stb_gated_queries` switched off. The first is the one that matters — a
|
|
188
|
+
session whose *opening* query is speculative, which is exactly the
|
|
189
|
+
uncharacterised-head probe this fix exists for. Prove MAV with something the
|
|
190
|
+
instrument certainly answers before asking it anything it may not.
|
|
191
|
+
|
|
192
|
+
- **An instrument that arrived wedged now says so, on the first line.** The
|
|
193
|
+
clear-on-open is best effort and its failure was logged as such — but one
|
|
194
|
+
failure there is a diagnosis, not a symptom. `VI_ERROR_IN_PROGRESS` on a
|
|
195
|
+
session opened seconds ago cannot describe anything this process did: a
|
|
196
|
+
fresh session has issued no operation to still be in progress, so the
|
|
197
|
+
*instrument* was holding a stranded transfer before the session existed. It
|
|
198
|
+
is now reported at error level, saying what it means and what actually
|
|
199
|
+
clears it — because the two things anyone tries first both fail. Restarting
|
|
200
|
+
the server rebuilds a session against the same stuck endpoint, and a
|
|
201
|
+
driver-level re-enumeration (`Disable-PnpDevice`/`Enable-PnpDevice`,
|
|
202
|
+
`pnputil /restart-device`) resets the port without ever dropping bus power,
|
|
203
|
+
so a transfer held in the instrument's firmware rides straight through it.
|
|
204
|
+
Only power loss clears it, which for a bus-powered USB instrument means
|
|
205
|
+
unplugging the cable.
|
|
206
|
+
|
|
207
|
+
This came out of a PM101R that was diagnosed from thirty lines of identical
|
|
208
|
+
timeouts while the answer sat above them as a generic warning. Every other
|
|
209
|
+
clear failure — the timeout that is the documented benign case, an
|
|
210
|
+
instrument with no clear at all — stays the quiet warning it was, so the
|
|
211
|
+
new message means exactly one thing when it appears. The clear-on-open also
|
|
212
|
+
discards the host-side buffers first, as the recovery ladder's clear rung
|
|
213
|
+
already does.
|
|
214
|
+
|
|
215
|
+
- **The VISA recovery ladder no longer compounds a wedge.** A PM101R with no
|
|
216
|
+
sensor head answered `*IDN?` and took writes, but every sensor-dependent
|
|
217
|
+
query timed out — and the ladder then made it worse until the meter answered
|
|
218
|
+
nothing at all. Four faults, each fixed:
|
|
219
|
+
- A timeout can leave the session with a transfer still in flight. Every
|
|
220
|
+
operation on such a session reports `VI_ERROR_IN_PROGRESS`, the device
|
|
221
|
+
clear included, so the clear-and-retry rung could never succeed and only
|
|
222
|
+
spent a second timeout to say so. That error now goes straight to the
|
|
223
|
+
reopen, which is the only rung that clears the condition.
|
|
224
|
+
- A wedged session refuses to close, and the refusal was logged and the
|
|
225
|
+
reference dropped — which frees nothing. The backend kept the handle and
|
|
226
|
+
the instrument kept the interface, so each reopen added *another* live
|
|
227
|
+
session to the same device; two rungs left the meter holding three, at
|
|
228
|
+
which point `*IDN?` stopped answering on all of them. A refused close now
|
|
229
|
+
falls back to closing the underlying VISA handle, which does not go
|
|
230
|
+
through the session and so cannot be blocked by its state.
|
|
231
|
+
- The reopen called `open_resource` directly, bypassing the watchdog that
|
|
232
|
+
`_open` uses — on the one path that runs precisely when the instrument is
|
|
233
|
+
not answering, and where opening can therefore hang in native code. It now
|
|
234
|
+
opens through the same watchdog.
|
|
235
|
+
- A reopen that failed left the address registered to a session that no
|
|
236
|
+
longer existed, so `is_open` was false, every later command was refused,
|
|
237
|
+
and `open()` rejected every retry as "already in use" for the life of the
|
|
238
|
+
process. The address is now unclaimed before the release is attempted and
|
|
239
|
+
re-registered on success, so a failed reopen leaves the manager closed but
|
|
240
|
+
reopenable rather than needing a server restart.
|
|
241
|
+
|
|
242
|
+
The clear rung also discards the host-side buffers first: a timed-out
|
|
243
|
+
query's answer may still be half-arrived, and the clear reaches the
|
|
244
|
+
instrument but not the buffers, so the remnant became the reply to the next
|
|
245
|
+
command. `_close` releases through the same path, so a device server shutting
|
|
246
|
+
down on a wedged instrument no longer leaves the interface claimed.
|
|
247
|
+
|
|
248
|
+
None of this makes an absent sensor head measure anything — the underlying
|
|
249
|
+
query still fails. It stops one hardware fault from taking the session, and
|
|
250
|
+
then the process, down with it.
|
|
251
|
+
|
|
252
|
+
## 0.3.5 — 2026-08-24
|
|
253
|
+
|
|
254
|
+
The final release of the line begun in 0.3.5.dev1: everything in the three
|
|
255
|
+
pre-releases below ships here — the run convention (#34), bounded visible
|
|
256
|
+
connects, the rig preflight and `connect_rig` (#36, #37), default logging,
|
|
257
|
+
operation kinds driving the field test at both tiers (#39), and the pol-pl
|
|
258
|
+
rig findings (#35, #38) — plus the parser change below.
|
|
259
|
+
|
|
260
|
+
Note for consumers upgrading from 0.3.4: the run sources' `pattern=`
|
|
261
|
+
arguments and `data_dir` attributes are gone and `Experiment(run_root=)`
|
|
262
|
+
now defaults to the mount. Adjust those call sites.
|
|
263
|
+
|
|
264
|
+
- **A schema can name its response parser** (#40). `parser` was listed among
|
|
265
|
+
the schema keys but only ever held a `ResponseParser` object, so a name
|
|
266
|
+
written there was stored unchanged and *called* on the first reply. It is
|
|
267
|
+
now resolved to a parser when the parameter is registered — a misspelled
|
|
268
|
+
kind, an invalid pattern or an out-of-range capture group fails while the
|
|
269
|
+
schema loads, naming the parameter. Two built-in kinds cover the text
|
|
270
|
+
shapes every instrument has: `regex` (`pattern`, `group`) pulls the value
|
|
271
|
+
out of a reply that carries text around it, `delimited` (`index`,
|
|
272
|
+
`separator`) keeps one field of a record; both hand the result to
|
|
273
|
+
`AutoResponseParser`, so the parameter's `type` still does the conversion,
|
|
274
|
+
and both decode a `bytes` reply first. `auto` stays the default and
|
|
275
|
+
unchanged — a reply it cannot cast is still returned raw. A device whose
|
|
276
|
+
frame the built-ins cannot describe registers its own decoder once with
|
|
277
|
+
`register_parser_kind(name, factory)` and names it per parameter from the
|
|
278
|
+
schema; `parser_kinds()` lists what is nameable. Passing a
|
|
279
|
+
`ResponseParser` instance from Python is untouched. This retires the
|
|
280
|
+
private `pattern` key `plesty-spectrapro-hrs` had invented for exactly
|
|
281
|
+
this.
|
|
282
|
+
|
|
283
|
+
## 0.3.5.dev3 — 2026-08-18
|
|
284
|
+
|
|
285
|
+
Development pre-release for field-testing operation kinds (#39): operations
|
|
286
|
+
declare what they do and the field test drives a motion out and back, a
|
|
287
|
+
lifecycle op once, a control op at rest; plus the pol-pl rig findings #35
|
|
288
|
+
and #38. Same line as 0.3.5.dev2 — the final release is 0.3.5.
|
|
289
|
+
|
|
290
|
+
- **Operations declare what they do, and the field test drives them by
|
|
291
|
+
kind** (#39). `@expose_to_api(kind=…)` and a schema `kind` block record
|
|
292
|
+
one of `read | motion | acquire | lifecycle | control | configure`
|
|
293
|
+
(`plesty.lib.device.device_utils.OpKind`; `FuncDoc.kind`, the describe
|
|
294
|
+
payload and `function_docs()` carry it). `FieldTestPipeline` acts on it
|
|
295
|
+
instead of asking the developer per operation: a `motion` op is driven
|
|
296
|
+
one `step` out from what its `position_key` reports and put back (the
|
|
297
|
+
functions gate checks it came back; the stability gate repeats out and
|
|
298
|
+
back), a `lifecycle` op runs once when allow-listed and never on repeat,
|
|
299
|
+
a `control` op once at rest after the motions (skipped when nothing
|
|
300
|
+
moved), a `configure` op is never called. `op_kinds={…}` overrides the
|
|
301
|
+
module's declaration. On plesty-k10cr1 the operation every pol-pl row
|
|
302
|
+
calls could not be tested at all: `move_absolute(position)` was called
|
|
303
|
+
bare and failed on the missing argument.
|
|
304
|
+
- **The client tier drives operations by kind too.** `ClientFieldTest`
|
|
305
|
+
takes `op_kinds=` (the generated `field_test_client.py` passes the host
|
|
306
|
+
tier's): a motion is queried through `position_key`, moved one step out
|
|
307
|
+
and back over the wire as many pairs as the host tier's call count, a
|
|
308
|
+
lifecycle op once, a control op once after the motions, a configure op
|
|
309
|
+
never; a motion without `position_key`/`target`/`step` fails the gate.
|
|
310
|
+
- **A provocation that breaks is a failed gate, not a forced timeout.**
|
|
311
|
+
The drain and recovery gates called the slow op with the payload as one
|
|
312
|
+
positional dict (`getattr(device, slow_op)(dict(payload))`), so an
|
|
313
|
+
`@expose_to_api` slow op raised `TypeError` before any transport
|
|
314
|
+
traffic — and both gates counted the exception as an abandoned call and
|
|
315
|
+
passed. The call now goes through the operation's own call form, only a
|
|
316
|
+
timeout-class error counts as forced, and a motion slow op is a delta
|
|
317
|
+
from where the part is, moved back afterwards (`motion_restored` in the
|
|
318
|
+
evidence). The slow-op payload is prepared before the timeout is
|
|
319
|
+
shortened, so reading the start position cannot itself time out.
|
|
320
|
+
|
|
321
|
+
- **A serial port that is held says so** (#35). pyserial reports a wrong
|
|
322
|
+
port name and a port open in another process with the same
|
|
323
|
+
`SerialException`, and on the K10CR1 rig `Access is denied` on the right
|
|
324
|
+
`COM4` — Kinesis had the stage loaded — cost the session to driver and
|
|
325
|
+
VCP checks. `TrafficManager.open` now asks the subclass to explain a
|
|
326
|
+
failure (`_explain_open_error`, `None` by default) and appends the answer
|
|
327
|
+
to the raised message. `AptTrafficManager` and `SerialTrafficManager`
|
|
328
|
+
answer for EACCES/EBUSY: on Windows and for EBUSY, the port exists but is
|
|
329
|
+
open elsewhere — close Kinesis/APT (a loaded stage locks the FTDI device
|
|
330
|
+
via D2XX and blocks the VCP), stray interpreters, serial terminals, replug
|
|
331
|
+
to drop a stale handle; on Linux EACCES, add the user to `dialout`. The
|
|
332
|
+
port's `list_ports` description and hwid follow, so the user sees at once
|
|
333
|
+
whether the address is the intended device. Windows sets no `errno` on
|
|
334
|
+
the exception, so the number is read out of the message text. Other
|
|
335
|
+
failures (`ENOENT`, timeouts) are reported as before.
|
|
336
|
+
- `ModuleInfo.from_project` and the field-test version lookup no longer
|
|
337
|
+
answer for whatever repository `GIT_DIR` names: a git hook exports it for
|
|
338
|
+
the repository being pushed, and every `git -C <root>` in `report_artifact`
|
|
339
|
+
followed it — a field test run from a pre-push hook reported the hook's
|
|
340
|
+
remote and version, and the pre-push suite failed on the tmp-dir module
|
|
341
|
+
test. `GIT_*` is scrubbed from the subprocess environment.
|
|
342
|
+
|
|
343
|
+
- **Client-tier `functions` gate no longer demands a dict of every
|
|
344
|
+
operation** (#38). It required `isinstance(answer, dict)` for each
|
|
345
|
+
allow-listed operation, while the host tier asks that only of
|
|
346
|
+
schema-declared ones; on lightfield_spectrometer `acquire` (`None`) and
|
|
347
|
+
`get_recent_file` (`str`) came back 21 times each with no failure and
|
|
348
|
+
the gate still failed. A call that returns is the pass; the answer's
|
|
349
|
+
type is recorded in the evidence.
|
|
350
|
+
|
|
3
351
|
## 0.3.5.dev2 — 2026-08-17
|
|
4
352
|
|
|
5
353
|
Development pre-release for field-testing the rig start-up (#36, #37):
|
|
6
354
|
bounded, visible connects, the preflight, and default logging. Same line as
|
|
7
|
-
0.3.5.dev1 — the final release is 0.
|
|
355
|
+
0.3.5.dev1 — the final release is 0.3.5.
|
|
8
356
|
|
|
9
357
|
- **A run starts only on a rig that is really there** (pol-pl field
|
|
10
358
|
finding: a sweep went on with the powermeter server absent).
|
|
@@ -64,9 +412,10 @@ bounded, visible connects, the preflight, and default logging. Same line as
|
|
|
64
412
|
## 0.3.5.dev1 — 2026-08-17
|
|
65
413
|
|
|
66
414
|
Development pre-release for field-testing the run convention (#34); the
|
|
67
|
-
final release of this line is 0.
|
|
415
|
+
final release of this line is 0.3.5. The API diff against 0.3.4 — the
|
|
68
416
|
`pattern=` arguments and `data_dir` attributes of the run sources are
|
|
69
|
-
gone, `Experiment(run_root=)` defaults to the mount
|
|
417
|
+
gone, `Experiment(run_root=)` defaults to the mount — needs those call
|
|
418
|
+
sites adjusted.
|
|
70
419
|
|
|
71
420
|
- `plesty.lib.monitor.Viz` (#34): the `monitor` / `render` command line over
|
|
72
421
|
an experiment's panels. The experiment registers one function
|
|
@@ -54,6 +54,33 @@ Schema entries use the same dtype vocabulary as device schemas
|
|
|
54
54
|
`array_<numpy_dtype>`, `table2d`, … Arrays may declare a `shape` where `None`
|
|
55
55
|
marks a free dimension (`[None, 2]`); inputs may declare `required: False`.
|
|
56
56
|
|
|
57
|
+
## Contract stability
|
|
58
|
+
|
|
59
|
+
**This contract is locked.** It shipped in 0.3.2 and has not changed since.
|
|
60
|
+
Write an analyzer against what is described above and it will keep working.
|
|
61
|
+
|
|
62
|
+
Locked means the declared surface is fixed: the two class-level schemas, the
|
|
63
|
+
single `analyze()`, the validating `__call__` and its four steps, the
|
|
64
|
+
provenance stamp, and the five `AnalyzerPipeline` gates below. Names and
|
|
65
|
+
shapes here are not going to be rearranged under you.
|
|
66
|
+
|
|
67
|
+
It is locked on evidence rather than by declaration. The pilot migration
|
|
68
|
+
(`hub/analyzers/positionestimate`) put a real analysis module on the contract
|
|
69
|
+
without changing any of its analysis code — the facade was about 170 lines,
|
|
70
|
+
free-dimension shapes and optional inputs both behaved as specified, and all
|
|
71
|
+
five gates passed. The SDK then built on it: gate a1 and `plesty init
|
|
72
|
+
analyzer` generate and check against exactly this surface. A contract two
|
|
73
|
+
independent consumers already depend on is one that has been tested by use,
|
|
74
|
+
which is the only kind of locking worth stating.
|
|
75
|
+
|
|
76
|
+
What can still change: additions. A new optional schema key, a new dtype in
|
|
77
|
+
the shared vocabulary, a new gate — none of those invalidate an analyzer
|
|
78
|
+
written today. What cannot change without a major version and a deprecation
|
|
79
|
+
period: the four things in the paragraph above. If you find the contract
|
|
80
|
+
genuinely cannot express your transform, that is an issue on plesty-lib, not
|
|
81
|
+
a local workaround — the value of the stamp and the gates is that every
|
|
82
|
+
analyzer carries the same ones.
|
|
83
|
+
|
|
57
84
|
## Design rules
|
|
58
85
|
|
|
59
86
|
- **Synchronous by design.** Analysis is CPU-bound compute, unlike the I/O
|
|
@@ -16,7 +16,7 @@ Everything ships under the `plesty.lib` namespace package:
|
|
|
16
16
|
|---|---|
|
|
17
17
|
| `plesty.lib.utils` | Foundation utilities: logging setup, error handling, the process-wide `ResourceRegistry`, `EnvSettings`, the module/run configuration loader (`module_config`, `load_yaml`), DLL helpers. |
|
|
18
18
|
| `plesty.lib.data` | Data containers and persistence: `PlestyArray`, tables, units, dtype helpers, and the result store (`save_result` / `ResultDocument`, optional HDF5 export). |
|
|
19
|
-
| `plesty.lib.traffic` | Transport backends (traffic managers): Serial, VISA, USB utilities, raw TCP/IP. Moves bytes; knows nothing about parameters or operations. |
|
|
19
|
+
| `plesty.lib.traffic` | Transport backends (traffic managers): Serial, VISA, USB utilities, raw TCP/IP, and Thorlabs APT (binary framing, `apt_protocol`). Moves bytes; knows nothing about parameters or operations. |
|
|
20
20
|
| `plesty.lib.solver` | Protocol translation: the `CmdSolver` / `OpSolver` ABCs and concrete solvers (`SCPISolver`, `ICEBLOCKSolver`) that turn standardized calls into vendor commands. |
|
|
21
21
|
| `plesty.lib.device` | The core device tier: `BaseDeviceSyncModel` with its three system mixins (`ConfigSystem`, `FunctionSystem`, `TelemetrySystem`), the render-agnostic `doc_model()`, async wrappers, ready-made SCPI base devices, and `CompositeDevice`. |
|
|
22
22
|
| `plesty.lib.service` | Remote access: the ZMQ-based device TCP/IP server and client, plus the per-device `ResourceManager` for multi-client access control. |
|
|
@@ -24,8 +24,8 @@ Everything ships under the `plesty.lib` namespace package:
|
|
|
24
24
|
| `plesty.lib.experiment` | Reproducible orchestration: the async `Experiment` ABC with `Plan`/`Step` schedules and the crash-safe `Journal`. |
|
|
25
25
|
| `plesty.lib.monitor` | Live views: the `Monitor` ABC — schema-declared projections of arriving data onto declared traces — and the sources that feed it (`RunSource`, `TelemetrySource`, `PushSource`). Read-only and toolkit-free. |
|
|
26
26
|
| `plesty.lib.ui` | GUI framework: the `Panel` contract, the dockable `Shell` home window, the `Theme` carrying the plesty.net design tokens, and the PySide6/pyqtgraph backend behind the `gui` extra. |
|
|
27
|
-
| `plesty.lib.sim` | `DataGenerator`
|
|
28
|
-
| `plesty.lib.test` | Shipped test helpers and the standard contract pipelines
|
|
27
|
+
| `plesty.lib.sim` | Stand-ins for hardware: `DataGenerator` (schema-driven synthetic data for mock solvers and tests), `DemoDevice` (the standard demo device tests build on instead of hand-writing one), and `AptMotorSimulator` (a Thorlabs APT motor answering real protocol frames). |
|
|
28
|
+
| `plesty.lib.test` | Shipped test helpers and the standard contract pipelines that hub modules import: `DevicePipeline`, `ExperimentPipeline`, `AnalyzerPipeline` (mock, hardware-free), plus `FieldTestPipeline`, `client_field_test` and `field_test_concurrency` for what only an instrument can prove. |
|
|
29
29
|
|
|
30
30
|
### Layering
|
|
31
31
|
|
|
@@ -143,15 +143,19 @@ file is the commit record. `load_document` reads metadata without blob I/O;
|
|
|
143
143
|
`convert_to_hdf5` optionally packs documents into one archive. See
|
|
144
144
|
[Data & Metadata Schemas](data_schemas.md).
|
|
145
145
|
|
|
146
|
-
**Contract test pipelines** — `plesty.lib.test`. `DevicePipeline` (
|
|
147
|
-
gates, SDK gate d1), `ExperimentPipeline` (five hardware-free gates,
|
|
148
|
-
gate E1), and `AnalyzerPipeline` (five gates on schema-generated
|
|
149
|
-
inputs, groundwork for SDK gate a1) define what every hub module
|
|
150
|
-
schema integrity, mock round-trips, lifecycle
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
146
|
+
**Contract test pipelines** — `plesty.lib.test`. `DevicePipeline` (eight
|
|
147
|
+
mock gates, SDK gate d1), `ExperimentPipeline` (five hardware-free gates,
|
|
148
|
+
SDK gate E1), and `AnalyzerPipeline` (five gates on schema-generated
|
|
149
|
+
synthetic inputs, groundwork for SDK gate a1) define what every hub module
|
|
150
|
+
must prove: schema integrity, mock round-trips, lifecycle (device tier);
|
|
151
|
+
deterministic plans, resolvable ops, serializability (experiment tier);
|
|
152
|
+
analyze-signature match, round-trip validation, provenance (analyzer tier).
|
|
153
|
+
A device module exposes one pytest function calling `run_mock_pipeline()`;
|
|
154
|
+
experiment and analyzer modules expose one function per gate. What a mock
|
|
155
|
+
cannot reach — the command-format contract, transport pathologies, resource
|
|
156
|
+
contention — belongs to `FieldTestPipeline` on the instrument,
|
|
157
|
+
`client_field_test` over the wire, and `field_test_concurrency` for devices
|
|
158
|
+
sharing a host. See [Test helpers](device/test_helper.md).
|
|
155
159
|
|
|
156
160
|
## Design Decisions
|
|
157
161
|
|
|
@@ -122,6 +122,52 @@ class MyScpiSerialDevice(BaseDeviceSyncModel):
|
|
|
122
122
|
return self.traffic_manager is not None and self.traffic_manager.is_open
|
|
123
123
|
```
|
|
124
124
|
|
|
125
|
+
## Ready-Made Base Devices
|
|
126
|
+
|
|
127
|
+
Before subclassing `BaseDeviceSyncModel` directly, check whether one of the
|
|
128
|
+
shipped intermediates already covers your transport. Each wires up a traffic
|
|
129
|
+
manager and implements `connect`, `disconnect`, `_write_`, `_query_` and
|
|
130
|
+
`check_operatability`, so a module only declares its schemas and its
|
|
131
|
+
device-specific operations.
|
|
132
|
+
|
|
133
|
+
| Class | Location | For |
|
|
134
|
+
|---|---|---|
|
|
135
|
+
| `BaseVisaScpiDevice` | `plesty.lib.device.base_visa_scpi_device` | Any SCPI instrument reachable over VISA (USB-TMC, GPIB, serial-over-VISA). |
|
|
136
|
+
| `BaseTCPScpiDevice` | `plesty.lib.device.base_tcp_scpi_device` | A SCPI instrument that listens on a raw TCP socket rather than through VISA. |
|
|
137
|
+
| `BaseAptDevice` | `plesty.lib.device.base_apt_device` | Single-channel Thorlabs APT motion controllers — binary frames rather than text. |
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from plesty.lib.device.base_visa_scpi_device import BaseVisaScpiDevice
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class MyPowerMeter(BaseVisaScpiDevice):
|
|
144
|
+
def __init__(self, address: str):
|
|
145
|
+
super().__init__(
|
|
146
|
+
address,
|
|
147
|
+
timeout=5000,
|
|
148
|
+
param_schema="assets/param_schema.json",
|
|
149
|
+
op_schema="assets/op_schema.json",
|
|
150
|
+
)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Both SCPI bases take `write_termination` / `read_termination` (and an optional
|
|
154
|
+
`baud_rate` for serial-backed resources), pair themselves with
|
|
155
|
+
[`SCPISolver`](cmd_solver.md), and add `query_range`, `identity`,
|
|
156
|
+
`check_errors` and `reset` on top of the standard device API.
|
|
157
|
+
|
|
158
|
+
`BaseAptDevice` is the odd one out because APT is not a text protocol. It takes
|
|
159
|
+
a serial `port`, an `AptAxis` destination and a `channel`, and exposes the
|
|
160
|
+
exchange as messages rather than strings: `command(...)` sends one frame,
|
|
161
|
+
`request(...)` sends one and waits for its reply, `hw_info()` and `get_status()`
|
|
162
|
+
read the controller. Motion is handled in the controller's own count domain —
|
|
163
|
+
`home()`, `move_absolute_counts()`, `move_relative_counts()`, `stop()`,
|
|
164
|
+
`get_position_counts()`, `get_velocity_params()` / `set_velocity_params()` —
|
|
165
|
+
and the subclass converts counts to physical units in its user-facing
|
|
166
|
+
operations. Parameter keys are registered with `register_apt_param()` so the
|
|
167
|
+
config system can read and write them like any other. Passing `transport=` a
|
|
168
|
+
`plesty.lib.sim.apt.AptMotorSimulator` runs the whole class without a motor;
|
|
169
|
+
see [Simulation](../sim.md).
|
|
170
|
+
|
|
125
171
|
## State and Synchronization Helpers
|
|
126
172
|
|
|
127
173
|
Useful built-in helpers:
|
|
@@ -261,7 +307,12 @@ model.standard_methods # the common device API (connect/write/query/reset/...)
|
|
|
261
307
|
`functions` unifies the two ways a device gains operations: schema-registered
|
|
262
308
|
operations (`source="schema"`) and `@expose_to_api` methods
|
|
263
309
|
(`source="custom"`); framework plumbing is kept out and surfaced separately
|
|
264
|
-
under `standard_methods` (`source="standard"`).
|
|
310
|
+
under `standard_methods` (`source="standard"`). Each `FuncDoc` also carries
|
|
311
|
+
its `kind` — what the operation does to the world (`read`, `motion`,
|
|
312
|
+
`acquire`, `lifecycle`, `control`, `configure`), declared with
|
|
313
|
+
`@expose_to_api(kind=..., position_key=..., step=...)` or a `"kind"` block in
|
|
314
|
+
`schema_func.json` and used by the field test to drive the operation
|
|
315
|
+
(see *test_helper*, "Kinds"). Per-parameter and return
|
|
265
316
|
descriptions are parsed from Google-style docstring `Args:`/`Returns:`
|
|
266
317
|
sections — write those in your device methods and the generated documentation
|
|
267
318
|
inherits them.
|
|
@@ -16,7 +16,8 @@ Core implementation: `ConfigSystem` in `plesty.lib.device.params`.
|
|
|
16
16
|
4. Read/write permission flags (`read_only`, `write_only`).
|
|
17
17
|
5. Current value cache per parameter (`value`).
|
|
18
18
|
6. Automatic response casting through `AutoResponseParser`.
|
|
19
|
-
7.
|
|
19
|
+
7. Response parsers named from the schema (`parser`), including a device's own.
|
|
20
|
+
8. Human-readable summaries (`param_summary`).
|
|
20
21
|
|
|
21
22
|
## Main Data Structures
|
|
22
23
|
|
|
@@ -179,7 +180,83 @@ Examples:
|
|
|
179
180
|
3. `"true"/"1" -> bool` (via basic cast helper)
|
|
180
181
|
4. Delimited strings for typed list/tuple annotations
|
|
181
182
|
|
|
182
|
-
|
|
183
|
+
A reply the auto parser cannot cast is returned unchanged. When the value has to
|
|
184
|
+
be pulled out of the reply first, name a parser in the parameter's `parser` key.
|
|
185
|
+
|
|
186
|
+
### Naming a parser from a schema
|
|
187
|
+
|
|
188
|
+
`parser` takes a kind name, or a mapping of a `kind` and its configuration:
|
|
189
|
+
|
|
190
|
+
```json
|
|
191
|
+
{
|
|
192
|
+
"wavelength": {
|
|
193
|
+
"type": "float",
|
|
194
|
+
"unit": "nm",
|
|
195
|
+
"command": "?NM",
|
|
196
|
+
"parser": {"kind": "regex", "pattern": "(-?\\d+\\.\\d+) nm"}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
The kind is resolved to a `ResponseParser` when the parameter is registered, so a
|
|
202
|
+
misspelled kind, an invalid pattern or an out-of-range capture group fails while
|
|
203
|
+
the schema is being loaded rather than on the first query.
|
|
204
|
+
|
|
205
|
+
Built-in kinds:
|
|
206
|
+
|
|
207
|
+
| kind | configuration | reply → value |
|
|
208
|
+
|---|---|---|
|
|
209
|
+
| `auto` | — | `"3.14"` → `3.14` (the default) |
|
|
210
|
+
| `regex` | `pattern`, `group` (default `1`, a number or a group name), `encoding` | `"605.334 nm ok"` → `605.334` |
|
|
211
|
+
| `delimited` | `index` (default `0`, negatives count from the end), `separator` (default `,`), `encoding` | `"IDN,ACME,X100,1.2.3"` with `index: 2` → `"X100"` |
|
|
212
|
+
|
|
213
|
+
Both parsers hand their result to `AutoResponseParser`, so the parameter's `type`
|
|
214
|
+
still performs the conversion — the pattern or the separator only decides which
|
|
215
|
+
characters are the value. A `bytes` reply is decoded with `encoding` (default
|
|
216
|
+
`utf-8`) first.
|
|
217
|
+
|
|
218
|
+
Use `delimited` when the reply carries several fields and the parameter is one of
|
|
219
|
+
them. When the parameter's dtype is itself a list or tuple, `auto` already splits
|
|
220
|
+
the reply and no parser is needed.
|
|
221
|
+
|
|
222
|
+
### Registering a device's own parser
|
|
223
|
+
|
|
224
|
+
Binary frames and replies keyed by a mnemonic are the device's own business. The
|
|
225
|
+
module registers a parser once and every parameter then names it from the schema:
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
from plesty.lib.device.device_utils import ResponseParser
|
|
229
|
+
from plesty.lib.device.params import register_parser_kind
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class AptStatusParser(ResponseParser):
|
|
233
|
+
def __init__(self, field: str):
|
|
234
|
+
self.field = field
|
|
235
|
+
|
|
236
|
+
def parse(self, response, param=None, **kwargs):
|
|
237
|
+
return decode_apt_status(response)[self.field]
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
register_parser_kind("apt_status", AptStatusParser)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
```json
|
|
244
|
+
{
|
|
245
|
+
"position": {
|
|
246
|
+
"type": "float",
|
|
247
|
+
"command": "MGMSG_MOT_REQ_STATUSUPDATE",
|
|
248
|
+
"parser": {"kind": "apt_status", "field": "position"}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Schema keys other than `kind` are passed to the factory as keyword arguments.
|
|
254
|
+
Registering the same name twice with the same factory is fine; a different
|
|
255
|
+
factory under a name already in use raises. `parser_kinds()` lists what is
|
|
256
|
+
currently nameable.
|
|
257
|
+
|
|
258
|
+
Passing a `ResponseParser` instance directly to `register_config(parser=...)`
|
|
259
|
+
from Python keeps working unchanged.
|
|
183
260
|
|
|
184
261
|
## Useful API Methods
|
|
185
262
|
|
|
@@ -193,6 +270,7 @@ If needed, provide a custom parser in parameter metadata via `parser`.
|
|
|
193
270
|
8. `get_config_value(key)`
|
|
194
271
|
9. `set_config_min_max(key, min_value, max_value)`
|
|
195
272
|
10. `param_summary(constraints=True, description=False)`
|
|
273
|
+
11. `register_parser_kind(name, factory)` / `parser_kinds()` (module-level)
|
|
196
274
|
|
|
197
275
|
## Minimal End-to-End Example
|
|
198
276
|
|