plesty-lib 0.3.5.dev3__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 (170) hide show
  1. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/CHANGELOG.md +285 -4
  2. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/PKG-INFO +1 -1
  3. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/docs/analyzer.md +27 -0
  4. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/docs/architecture.md +16 -12
  5. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/docs/device/base_device.md +46 -0
  6. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/docs/device/param_system.md +80 -2
  7. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/docs/device/test_helper.md +67 -3
  8. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/docs/device/traffic_manager.md +6 -0
  9. {plesty_lib-0.3.5.dev3 → 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.dev3 → plesty_lib-0.3.6}/docs/toc.yaml +2 -0
  13. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/gl-secret-detection-report.json +7 -7
  14. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/device/funcs.py +34 -0
  15. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/device/params.py +209 -2
  16. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/service/__init__.py +44 -1
  17. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/service/tcp_ip_server.py +213 -5
  18. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/traffic/__init__.py +9 -2
  19. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/traffic/serial.py +28 -3
  20. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/traffic/visa.py +417 -27
  21. plesty_lib-0.3.6/tests/manual/stop_closes_the_device.py +116 -0
  22. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_device_funcs.py +106 -0
  23. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_device_params.py +215 -1
  24. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_misc.py +12 -2
  25. plesty_lib-0.3.6/tests/test_service_loop_factory.py +78 -0
  26. plesty_lib-0.3.6/tests/test_tcp_ip_orphan_reply.py +93 -0
  27. plesty_lib-0.3.6/tests/test_tcp_ip_stop_signal.py +85 -0
  28. plesty_lib-0.3.6/tests/test_tcp_ip_traffic.py +196 -0
  29. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_traffic_drivers.py +79 -0
  30. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_visa_discovery.py +308 -0
  31. plesty_lib-0.3.6/tests/test_visa_mav_gating.py +139 -0
  32. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/uv.lock +39 -2
  33. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/.gitignore +0 -0
  34. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/.gitlab-ci.yml +0 -0
  35. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/LICENSE +0 -0
  36. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/LICENSES/LGPL-3.0-or-later.txt +0 -0
  37. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/README.md +0 -0
  38. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/REUSE.toml +0 -0
  39. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/docs/api_reference.md +0 -0
  40. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/docs/contributing.md +0 -0
  41. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/docs/data_schemas.md +0 -0
  42. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/docs/device/cmd_solver.md +0 -0
  43. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/docs/device/composite_device.md +0 -0
  44. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/docs/device/error_handling.md +0 -0
  45. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/docs/device/func_system.md +0 -0
  46. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/docs/device/index.md +0 -0
  47. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/docs/device/logging_system.md +0 -0
  48. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/docs/experiment.md +0 -0
  49. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/docs/imgs/device_layers.svg +0 -0
  50. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/docs/imgs/device_standard.svg +0 -0
  51. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/docs/imgs/plesty_framework.svg +0 -0
  52. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/docs/monitor.md +0 -0
  53. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/docs/quickstart.md +0 -0
  54. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/docs/ui.md +0 -0
  55. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/__init__.py +0 -0
  56. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/analyzer/__init__.py +0 -0
  57. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/analyzer/base_analyzer.py +0 -0
  58. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/data/__init__.py +0 -0
  59. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/data/array.py +0 -0
  60. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/data/ctype_manager.py +0 -0
  61. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/data/io.py +0 -0
  62. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/data/table.py +0 -0
  63. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/data/types.py +0 -0
  64. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/data/units.py +0 -0
  65. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/device/__init__.py +0 -0
  66. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/device/async_wrapper.py +0 -0
  67. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/device/base_apt_device.py +0 -0
  68. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/device/base_device_sync.py +0 -0
  69. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/device/base_tcp_scpi_device.py +0 -0
  70. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/device/base_visa_scpi_device.py +0 -0
  71. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/device/composite_device.py +0 -0
  72. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/device/device_utils.py +0 -0
  73. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/device/doc.py +0 -0
  74. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/device/telemetry.py +0 -0
  75. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/experiment/__init__.py +0 -0
  76. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/experiment/assets/preflight.yaml +0 -0
  77. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/experiment/base_experiment.py +0 -0
  78. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/experiment/journal.py +0 -0
  79. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/experiment/preflight.py +0 -0
  80. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/experiment/runs.py +0 -0
  81. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/experiment/schedule.py +0 -0
  82. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/monitor/__init__.py +0 -0
  83. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/monitor/assets/viz.yaml +0 -0
  84. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/monitor/base_monitor.py +0 -0
  85. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/monitor/sources.py +0 -0
  86. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/monitor/viz.py +0 -0
  87. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/service/resource_manager.py +0 -0
  88. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/service/tcp_ip_client.py +0 -0
  89. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/sim/__init__.py +0 -0
  90. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/sim/apt.py +0 -0
  91. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/sim/data_generator.py +0 -0
  92. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/sim/demo_device.py +0 -0
  93. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/solver/__init__.py +0 -0
  94. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/solver/iceblock.py +0 -0
  95. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/solver/scpi.py +0 -0
  96. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/test/__init__.py +0 -0
  97. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/test/analyzer_pipeline.py +0 -0
  98. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/test/client_field_test.py +0 -0
  99. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/test/device_func_system.py +0 -0
  100. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/test/device_param_system.py +0 -0
  101. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/test/device_pipeline.py +0 -0
  102. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/test/experiment_pipeline.py +0 -0
  103. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/test/field_test.py +0 -0
  104. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/test/field_test_concurrency.py +0 -0
  105. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/test/grouped_param_keys.py +0 -0
  106. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/test/report_artifact.py +0 -0
  107. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/test/resource_allocation.py +0 -0
  108. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/test/schema_params.py +0 -0
  109. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/test/schema_refresh.py +0 -0
  110. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/traffic/apt.py +0 -0
  111. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/traffic/apt_protocol.py +0 -0
  112. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/traffic/serial_ports.py +0 -0
  113. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/traffic/tcp_ip.py +0 -0
  114. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/traffic/usb_utils.py +0 -0
  115. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/traffic/utils.py +0 -0
  116. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/ui/__init__.py +0 -0
  117. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/ui/assets/palette.json +0 -0
  118. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/ui/assets/shell.qss +0 -0
  119. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/ui/monitor_panel.py +0 -0
  120. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/ui/panel.py +0 -0
  121. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/ui/qt/__init__.py +0 -0
  122. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/ui/qt/app.py +0 -0
  123. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/ui/qt/plot.py +0 -0
  124. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/ui/qt/recorder.py +0 -0
  125. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/ui/qt/shell.py +0 -0
  126. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/ui/theme.py +0 -0
  127. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/utils/__init__.py +0 -0
  128. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/utils/config.py +0 -0
  129. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/utils/dll_utils.py +0 -0
  130. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/utils/error_utils.py +0 -0
  131. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/utils/logger.py +0 -0
  132. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/utils/registry.py +0 -0
  133. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/plesty/lib/utils/settings.py +0 -0
  134. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/pyproject.toml +0 -0
  135. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_analyzer.py +0 -0
  136. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_analyzer_pipeline.py +0 -0
  137. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_async_wrapper_threading.py +0 -0
  138. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_client_field_test.py +0 -0
  139. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_composite_config.py +0 -0
  140. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_config.py +0 -0
  141. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_data_array.py +0 -0
  142. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_data_io_hdf5.py +0 -0
  143. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_data_types.py +0 -0
  144. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_demo_device.py +0 -0
  145. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_device_apt.py +0 -0
  146. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_device_base.py +0 -0
  147. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_device_data_path.py +0 -0
  148. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_device_pipeline.py +0 -0
  149. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_device_scpi.py +0 -0
  150. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_device_telemetry.py +0 -0
  151. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_experiment.py +0 -0
  152. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_experiment_pipeline.py +0 -0
  153. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_experiment_preflight.py +0 -0
  154. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_experiment_runs.py +0 -0
  155. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_field_test.py +0 -0
  156. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_monitor.py +0 -0
  157. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_report_artifact.py +0 -0
  158. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_schema_params.py +0 -0
  159. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_schema_refresh.py +0 -0
  160. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_service_manager.py +0 -0
  161. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_settings.py +0 -0
  162. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_shipped_imports.py +0 -0
  163. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_sim.py +0 -0
  164. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_solver.py +0 -0
  165. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_tcp_ip_resources.py +0 -0
  166. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_traffic_orphan.py +0 -0
  167. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_traffic_utils.py +0 -0
  168. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_ui.py +0 -0
  169. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_utils.py +0 -0
  170. {plesty_lib-0.3.5.dev3 → plesty_lib-0.3.6}/tests/test_viz.py +0 -0
@@ -1,11 +1,291 @@
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
+
3
283
  ## 0.3.5.dev3 — 2026-08-18
4
284
 
5
285
  Development pre-release for field-testing operation kinds (#39): operations
6
286
  declare what they do and the field test drives a motion out and back, a
7
287
  lifecycle op once, a control op at rest; plus the pol-pl rig findings #35
8
- and #38. Same line as 0.3.5.dev2 — the final release is 0.4.0.
288
+ and #38. Same line as 0.3.5.dev2 — the final release is 0.3.5.
9
289
 
10
290
  - **Operations declare what they do, and the field test drives them by
11
291
  kind** (#39). `@expose_to_api(kind=…)` and a schema `kind` block record
@@ -72,7 +352,7 @@ and #38. Same line as 0.3.5.dev2 — the final release is 0.4.0.
72
352
 
73
353
  Development pre-release for field-testing the rig start-up (#36, #37):
74
354
  bounded, visible connects, the preflight, and default logging. Same line as
75
- 0.3.5.dev1 — the final release is 0.4.0.
355
+ 0.3.5.dev1 — the final release is 0.3.5.
76
356
 
77
357
  - **A run starts only on a rig that is really there** (pol-pl field
78
358
  finding: a sweep went on with the powermeter server absent).
@@ -132,9 +412,10 @@ bounded, visible connects, the preflight, and default logging. Same line as
132
412
  ## 0.3.5.dev1 — 2026-08-17
133
413
 
134
414
  Development pre-release for field-testing the run convention (#34); the
135
- 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
136
416
  `pattern=` arguments and `data_dir` attributes of the run sources are
137
- 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.
138
419
 
139
420
  - `plesty.lib.monitor.Viz` (#34): the `monitor` / `render` command line over
140
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.dev3
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:
@@ -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