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.
Files changed (171) hide show
  1. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/CHANGELOG.md +352 -3
  2. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/PKG-INFO +1 -1
  3. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/analyzer.md +27 -0
  4. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/architecture.md +16 -12
  5. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/device/base_device.md +52 -1
  6. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/device/param_system.md +80 -2
  7. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/device/test_helper.md +92 -4
  8. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/device/traffic_manager.md +6 -0
  9. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/index.md +8 -5
  10. plesty_lib-0.3.6/docs/service.md +112 -0
  11. plesty_lib-0.3.6/docs/sim.md +94 -0
  12. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/toc.yaml +2 -0
  13. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/gl-secret-detection-report.json +7 -7
  14. plesty_lib-0.3.6/plesty/lib/device/device_utils.py +231 -0
  15. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/device/funcs.py +52 -2
  16. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/device/params.py +209 -2
  17. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/service/__init__.py +44 -1
  18. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/service/tcp_ip_server.py +213 -5
  19. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/test/client_field_test.py +88 -9
  20. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/test/field_test.py +330 -24
  21. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/test/report_artifact.py +7 -0
  22. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/traffic/__init__.py +26 -3
  23. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/traffic/apt.py +9 -0
  24. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/traffic/serial.py +33 -3
  25. plesty_lib-0.3.6/plesty/lib/traffic/serial_ports.py +86 -0
  26. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/traffic/visa.py +417 -27
  27. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/ui/qt/plot.py +2 -2
  28. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/ui/qt/shell.py +1 -1
  29. plesty_lib-0.3.6/tests/manual/stop_closes_the_device.py +116 -0
  30. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_client_field_test.py +123 -0
  31. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_device_funcs.py +170 -1
  32. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_device_params.py +215 -1
  33. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_field_test.py +250 -2
  34. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_misc.py +12 -2
  35. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_report_artifact.py +7 -0
  36. plesty_lib-0.3.6/tests/test_service_loop_factory.py +78 -0
  37. plesty_lib-0.3.6/tests/test_tcp_ip_orphan_reply.py +93 -0
  38. plesty_lib-0.3.6/tests/test_tcp_ip_stop_signal.py +85 -0
  39. plesty_lib-0.3.6/tests/test_tcp_ip_traffic.py +196 -0
  40. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_traffic_drivers.py +180 -0
  41. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_visa_discovery.py +308 -0
  42. plesty_lib-0.3.6/tests/test_visa_mav_gating.py +139 -0
  43. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/uv.lock +39 -2
  44. plesty_lib-0.3.5.dev2/plesty/lib/device/device_utils.py +0 -108
  45. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/.gitignore +0 -0
  46. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/.gitlab-ci.yml +0 -0
  47. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/LICENSE +0 -0
  48. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/LICENSES/LGPL-3.0-or-later.txt +0 -0
  49. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/README.md +0 -0
  50. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/REUSE.toml +0 -0
  51. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/api_reference.md +0 -0
  52. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/contributing.md +0 -0
  53. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/data_schemas.md +0 -0
  54. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/device/cmd_solver.md +0 -0
  55. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/device/composite_device.md +0 -0
  56. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/device/error_handling.md +0 -0
  57. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/device/func_system.md +0 -0
  58. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/device/index.md +0 -0
  59. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/device/logging_system.md +0 -0
  60. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/experiment.md +0 -0
  61. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/imgs/device_layers.svg +0 -0
  62. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/imgs/device_standard.svg +0 -0
  63. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/imgs/plesty_framework.svg +0 -0
  64. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/monitor.md +0 -0
  65. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/quickstart.md +0 -0
  66. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/docs/ui.md +0 -0
  67. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/__init__.py +0 -0
  68. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/analyzer/__init__.py +0 -0
  69. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/analyzer/base_analyzer.py +0 -0
  70. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/data/__init__.py +0 -0
  71. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/data/array.py +0 -0
  72. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/data/ctype_manager.py +0 -0
  73. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/data/io.py +0 -0
  74. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/data/table.py +0 -0
  75. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/data/types.py +0 -0
  76. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/data/units.py +0 -0
  77. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/device/__init__.py +0 -0
  78. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/device/async_wrapper.py +0 -0
  79. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/device/base_apt_device.py +0 -0
  80. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/device/base_device_sync.py +0 -0
  81. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/device/base_tcp_scpi_device.py +0 -0
  82. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/device/base_visa_scpi_device.py +0 -0
  83. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/device/composite_device.py +0 -0
  84. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/device/doc.py +0 -0
  85. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/device/telemetry.py +0 -0
  86. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/experiment/__init__.py +0 -0
  87. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/experiment/assets/preflight.yaml +0 -0
  88. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/experiment/base_experiment.py +0 -0
  89. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/experiment/journal.py +0 -0
  90. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/experiment/preflight.py +0 -0
  91. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/experiment/runs.py +0 -0
  92. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/experiment/schedule.py +0 -0
  93. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/monitor/__init__.py +0 -0
  94. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/monitor/assets/viz.yaml +0 -0
  95. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/monitor/base_monitor.py +0 -0
  96. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/monitor/sources.py +0 -0
  97. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/monitor/viz.py +0 -0
  98. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/service/resource_manager.py +0 -0
  99. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/service/tcp_ip_client.py +0 -0
  100. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/sim/__init__.py +0 -0
  101. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/sim/apt.py +0 -0
  102. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/sim/data_generator.py +0 -0
  103. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/sim/demo_device.py +0 -0
  104. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/solver/__init__.py +0 -0
  105. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/solver/iceblock.py +0 -0
  106. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/solver/scpi.py +0 -0
  107. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/test/__init__.py +0 -0
  108. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/test/analyzer_pipeline.py +0 -0
  109. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/test/device_func_system.py +0 -0
  110. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/test/device_param_system.py +0 -0
  111. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/test/device_pipeline.py +0 -0
  112. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/test/experiment_pipeline.py +0 -0
  113. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/test/field_test_concurrency.py +0 -0
  114. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/test/grouped_param_keys.py +0 -0
  115. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/test/resource_allocation.py +0 -0
  116. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/test/schema_params.py +0 -0
  117. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/test/schema_refresh.py +0 -0
  118. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/traffic/apt_protocol.py +0 -0
  119. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/traffic/tcp_ip.py +0 -0
  120. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/traffic/usb_utils.py +0 -0
  121. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/traffic/utils.py +0 -0
  122. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/ui/__init__.py +0 -0
  123. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/ui/assets/palette.json +0 -0
  124. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/ui/assets/shell.qss +0 -0
  125. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/ui/monitor_panel.py +0 -0
  126. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/ui/panel.py +0 -0
  127. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/ui/qt/__init__.py +0 -0
  128. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/ui/qt/app.py +0 -0
  129. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/ui/qt/recorder.py +0 -0
  130. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/ui/theme.py +0 -0
  131. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/utils/__init__.py +0 -0
  132. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/utils/config.py +0 -0
  133. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/utils/dll_utils.py +0 -0
  134. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/utils/error_utils.py +0 -0
  135. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/utils/logger.py +0 -0
  136. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/utils/registry.py +0 -0
  137. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/plesty/lib/utils/settings.py +0 -0
  138. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/pyproject.toml +0 -0
  139. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_analyzer.py +0 -0
  140. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_analyzer_pipeline.py +0 -0
  141. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_async_wrapper_threading.py +0 -0
  142. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_composite_config.py +0 -0
  143. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_config.py +0 -0
  144. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_data_array.py +0 -0
  145. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_data_io_hdf5.py +0 -0
  146. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_data_types.py +0 -0
  147. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_demo_device.py +0 -0
  148. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_device_apt.py +0 -0
  149. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_device_base.py +0 -0
  150. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_device_data_path.py +0 -0
  151. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_device_pipeline.py +0 -0
  152. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_device_scpi.py +0 -0
  153. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_device_telemetry.py +0 -0
  154. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_experiment.py +0 -0
  155. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_experiment_pipeline.py +0 -0
  156. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_experiment_preflight.py +0 -0
  157. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_experiment_runs.py +0 -0
  158. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_monitor.py +0 -0
  159. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_schema_params.py +0 -0
  160. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_schema_refresh.py +0 -0
  161. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_service_manager.py +0 -0
  162. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_settings.py +0 -0
  163. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_shipped_imports.py +0 -0
  164. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_sim.py +0 -0
  165. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_solver.py +0 -0
  166. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_tcp_ip_resources.py +0 -0
  167. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_traffic_orphan.py +0 -0
  168. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_traffic_utils.py +0 -0
  169. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_ui.py +0 -0
  170. {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.6}/tests/test_utils.py +0 -0
  171. {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.4.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.4.0 the API diff against 0.3.4 (the
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) is a minor bump.
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: plesty-lib
3
- Version: 0.3.5.dev2
3
+ Version: 0.3.6
4
4
  Summary: A library for the Plesty ecosystem.
5
5
  Author: Plesty Development Team
6
6
  Author-email: Yunshuang Yuan <yunshuang.yuan@fkp.uni-hannover.de>
@@ -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` schema-driven synthetic data for mock solvers and tests. |
28
- | `plesty.lib.test` | Shipped test helpers and the standard contract pipelines (`DevicePipeline`, `ExperimentPipeline`, `AnalyzerPipeline`) that hub modules import. |
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` (ten
147
- gates, SDK gate d1), `ExperimentPipeline` (five hardware-free gates, SDK
148
- gate E1), and `AnalyzerPipeline` (five gates on schema-generated synthetic
149
- inputs, groundwork for SDK gate a1) define what every hub module must prove:
150
- schema integrity, mock round-trips, lifecycle, resource contention (device
151
- tier); deterministic plans, resolvable ops, serializability (experiment
152
- tier); analyze-signature match, round-trip validation, provenance (analyzer
153
- tier). Hub modules expose one pytest function per gate. See
154
- [Test helpers](device/test_helper.md).
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"`). Per-parameter and return
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. Human-readable summaries (`param_summary`).
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
- If needed, provide a custom parser in parameter metadata via `parser`.
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