plesty-lib 0.3.6__tar.gz → 0.4.0.dev1__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 (175) hide show
  1. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/CHANGELOG.md +101 -0
  2. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/PKG-INFO +1 -1
  3. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/docs/ui.md +95 -3
  4. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/gl-secret-detection-report.json +5 -5
  5. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/device/composite_device.py +11 -3
  6. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/device/device_utils.py +52 -0
  7. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/device/doc.py +22 -0
  8. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/device/funcs.py +74 -1
  9. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/device/params.py +25 -1
  10. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/service/tcp_ip_server.py +113 -22
  11. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/test/client_field_test.py +104 -9
  12. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/test/field_test.py +199 -13
  13. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/ui/__init__.py +24 -5
  14. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/ui/assets/shell.qss +34 -0
  15. plesty_lib-0.4.0.dev1/plesty/lib/ui/device_link.py +307 -0
  16. plesty_lib-0.4.0.dev1/plesty/lib/ui/device_panel.py +552 -0
  17. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/ui/panel.py +15 -0
  18. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/ui/qt/__init__.py +2 -0
  19. plesty_lib-0.4.0.dev1/plesty/lib/ui/qt/form.py +409 -0
  20. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/ui/qt/shell.py +1 -2
  21. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_client_field_test.py +63 -1
  22. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_composite_config.py +50 -0
  23. plesty_lib-0.4.0.dev1/tests/test_device_panel.py +461 -0
  24. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_field_test.py +78 -4
  25. plesty_lib-0.4.0.dev1/tests/test_tcp_ip_concurrent_serve.py +110 -0
  26. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_tcp_ip_orphan_reply.py +14 -0
  27. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/uv.lock +13 -13
  28. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/.gitignore +0 -0
  29. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/.gitlab-ci.yml +0 -0
  30. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/LICENSE +0 -0
  31. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/LICENSES/LGPL-3.0-or-later.txt +0 -0
  32. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/README.md +0 -0
  33. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/REUSE.toml +0 -0
  34. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/docs/analyzer.md +0 -0
  35. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/docs/api_reference.md +0 -0
  36. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/docs/architecture.md +0 -0
  37. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/docs/contributing.md +0 -0
  38. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/docs/data_schemas.md +0 -0
  39. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/docs/device/base_device.md +0 -0
  40. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/docs/device/cmd_solver.md +0 -0
  41. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/docs/device/composite_device.md +0 -0
  42. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/docs/device/error_handling.md +0 -0
  43. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/docs/device/func_system.md +0 -0
  44. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/docs/device/index.md +0 -0
  45. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/docs/device/logging_system.md +0 -0
  46. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/docs/device/param_system.md +0 -0
  47. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/docs/device/test_helper.md +0 -0
  48. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/docs/device/traffic_manager.md +0 -0
  49. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/docs/experiment.md +0 -0
  50. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/docs/imgs/device_layers.svg +0 -0
  51. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/docs/imgs/device_standard.svg +0 -0
  52. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/docs/imgs/plesty_framework.svg +0 -0
  53. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/docs/index.md +0 -0
  54. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/docs/monitor.md +0 -0
  55. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/docs/quickstart.md +0 -0
  56. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/docs/service.md +0 -0
  57. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/docs/sim.md +0 -0
  58. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/docs/toc.yaml +0 -0
  59. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/__init__.py +0 -0
  60. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/analyzer/__init__.py +0 -0
  61. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/analyzer/base_analyzer.py +0 -0
  62. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/data/__init__.py +0 -0
  63. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/data/array.py +0 -0
  64. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/data/ctype_manager.py +0 -0
  65. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/data/io.py +0 -0
  66. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/data/table.py +0 -0
  67. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/data/types.py +0 -0
  68. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/data/units.py +0 -0
  69. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/device/__init__.py +0 -0
  70. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/device/async_wrapper.py +0 -0
  71. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/device/base_apt_device.py +0 -0
  72. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/device/base_device_sync.py +0 -0
  73. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/device/base_tcp_scpi_device.py +0 -0
  74. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/device/base_visa_scpi_device.py +0 -0
  75. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/device/telemetry.py +0 -0
  76. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/experiment/__init__.py +0 -0
  77. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/experiment/assets/preflight.yaml +0 -0
  78. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/experiment/base_experiment.py +0 -0
  79. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/experiment/journal.py +0 -0
  80. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/experiment/preflight.py +0 -0
  81. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/experiment/runs.py +0 -0
  82. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/experiment/schedule.py +0 -0
  83. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/monitor/__init__.py +0 -0
  84. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/monitor/assets/viz.yaml +0 -0
  85. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/monitor/base_monitor.py +0 -0
  86. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/monitor/sources.py +0 -0
  87. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/monitor/viz.py +0 -0
  88. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/service/__init__.py +0 -0
  89. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/service/resource_manager.py +0 -0
  90. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/service/tcp_ip_client.py +0 -0
  91. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/sim/__init__.py +0 -0
  92. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/sim/apt.py +0 -0
  93. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/sim/data_generator.py +0 -0
  94. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/sim/demo_device.py +0 -0
  95. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/solver/__init__.py +0 -0
  96. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/solver/iceblock.py +0 -0
  97. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/solver/scpi.py +0 -0
  98. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/test/__init__.py +0 -0
  99. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/test/analyzer_pipeline.py +0 -0
  100. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/test/device_func_system.py +0 -0
  101. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/test/device_param_system.py +0 -0
  102. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/test/device_pipeline.py +0 -0
  103. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/test/experiment_pipeline.py +0 -0
  104. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/test/field_test_concurrency.py +0 -0
  105. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/test/grouped_param_keys.py +0 -0
  106. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/test/report_artifact.py +0 -0
  107. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/test/resource_allocation.py +0 -0
  108. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/test/schema_params.py +0 -0
  109. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/test/schema_refresh.py +0 -0
  110. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/traffic/__init__.py +0 -0
  111. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/traffic/apt.py +0 -0
  112. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/traffic/apt_protocol.py +0 -0
  113. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/traffic/serial.py +0 -0
  114. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/traffic/serial_ports.py +0 -0
  115. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/traffic/tcp_ip.py +0 -0
  116. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/traffic/usb_utils.py +0 -0
  117. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/traffic/utils.py +0 -0
  118. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/traffic/visa.py +0 -0
  119. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/ui/assets/palette.json +0 -0
  120. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/ui/monitor_panel.py +0 -0
  121. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/ui/qt/app.py +0 -0
  122. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/ui/qt/plot.py +0 -0
  123. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/ui/qt/recorder.py +0 -0
  124. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/ui/theme.py +0 -0
  125. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/utils/__init__.py +0 -0
  126. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/utils/config.py +0 -0
  127. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/utils/dll_utils.py +0 -0
  128. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/utils/error_utils.py +0 -0
  129. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/utils/logger.py +0 -0
  130. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/utils/registry.py +0 -0
  131. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/plesty/lib/utils/settings.py +0 -0
  132. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/pyproject.toml +0 -0
  133. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/manual/stop_closes_the_device.py +0 -0
  134. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_analyzer.py +0 -0
  135. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_analyzer_pipeline.py +0 -0
  136. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_async_wrapper_threading.py +0 -0
  137. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_config.py +0 -0
  138. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_data_array.py +0 -0
  139. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_data_io_hdf5.py +0 -0
  140. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_data_types.py +0 -0
  141. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_demo_device.py +0 -0
  142. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_device_apt.py +0 -0
  143. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_device_base.py +0 -0
  144. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_device_data_path.py +0 -0
  145. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_device_funcs.py +0 -0
  146. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_device_params.py +0 -0
  147. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_device_pipeline.py +0 -0
  148. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_device_scpi.py +0 -0
  149. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_device_telemetry.py +0 -0
  150. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_experiment.py +0 -0
  151. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_experiment_pipeline.py +0 -0
  152. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_experiment_preflight.py +0 -0
  153. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_experiment_runs.py +0 -0
  154. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_misc.py +0 -0
  155. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_monitor.py +0 -0
  156. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_report_artifact.py +0 -0
  157. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_schema_params.py +0 -0
  158. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_schema_refresh.py +0 -0
  159. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_service_loop_factory.py +0 -0
  160. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_service_manager.py +0 -0
  161. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_settings.py +0 -0
  162. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_shipped_imports.py +0 -0
  163. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_sim.py +0 -0
  164. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_solver.py +0 -0
  165. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_tcp_ip_resources.py +0 -0
  166. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_tcp_ip_stop_signal.py +0 -0
  167. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_tcp_ip_traffic.py +0 -0
  168. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_traffic_drivers.py +0 -0
  169. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_traffic_orphan.py +0 -0
  170. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_traffic_utils.py +0 -0
  171. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_ui.py +0 -0
  172. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_utils.py +0 -0
  173. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_visa_discovery.py +0 -0
  174. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_visa_mav_gating.py +0 -0
  175. {plesty_lib-0.3.6 → plesty_lib-0.4.0.dev1}/tests/test_viz.py +0 -0
@@ -2,6 +2,107 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - **`DevicePanel` builds a device's controls from its schema (#41).** `Panel`'s
6
+ docstring has named three implementations since it was written; only the live
7
+ view existed, so every module wanting controls in a window hand-wrote a panel
8
+ from `plesty.widgets` primitives — six of them, each restating what its
9
+ device already declares. The duplicate drifts, and it drifts in the direction
10
+ that matters: the hand-written panel for one spectrometer bounded its
11
+ exposure at 0.01–60000 ms and its hardware averaging at 1–1000, where the
12
+ device declares no upper bound on either. Limits that exist nowhere in the
13
+ instrument, invented by whoever wrote the GUI, silently constraining an
14
+ operator.
15
+
16
+ `controls_for()` derives them instead — `dtype` picks the control,
17
+ `min_value`/`max_value` bound it, `options` makes it a drop-down, `read_only`
18
+ a readout, `unit` the suffix, `description` the tooltip — and where the
19
+ device declares no bound the control has none. It is free of any toolkit and
20
+ tested without a display, so a drift fails a test rather than reaching a
21
+ bench. Operations become buttons, with a small form when they take arguments;
22
+ `lifecycle` ones (homing, reset, a dark reference) are left off unless named,
23
+ because they should not be one click away.
24
+
25
+ A panel starts **read-only**. A monitor holds no device and this one
26
+ necessarily does, so it may be pointed at an instrument an experiment is
27
+ driving; writes and operations wait behind *Take control*.
28
+
29
+ - **`describe` carries the device's documentation, not just its method names.**
30
+ It answered `{"methods": [...]}`, and `doc_model()` — though exposed to the
31
+ API — returns a dataclass holding `dtype: type` and a `ResponseParser`, which
32
+ the server refuses to serialize. So nothing across the wire could learn a
33
+ parameter's unit, bounds, options or description: a console or a remote
34
+ client had no way to render a control except by inventing its constraints,
35
+ which is the bug above with a network in the middle. `DeviceDocModel`,
36
+ `ConfigParameter`, `FuncDoc`, `FuncParam` and `FuncOutput` now have
37
+ `to_dict()`, and the reply carries it as `doc` **beside** the existing
38
+ `methods` key — a client written against the old shape is unaffected, and a
39
+ server older than the payload degrades to method names, which `DevicePanel`
40
+ reports rather than showing an empty form. A device whose model fails to
41
+ build still answers `describe`; it is what a supervisor uses to decide the
42
+ device is alive.
43
+
44
+ - **A panel can state its own tone.** `Panel.tone()` returns the state colour of
45
+ the title-bar dot; the shell hardcoded `running` whenever a panel had a status
46
+ line at all, so a panel that fails *while still running* — a device that
47
+ stopped answering — had no way to go red.
48
+
49
+ - **A long call holds the instrument, not the socket (#48).** The accept loop
50
+ answered one request at a time, inline, so nothing was even read while a
51
+ device call ran. A supervisor asks `describe` on a timer — the console does
52
+ it per device every three seconds — and every probe arriving during a 20 s
53
+ exposure queued behind it, timed out, reconnected, and had its reply dropped
54
+ as unroutable: a bench read as unreachable for the whole of every long
55
+ acquisition, and the log carried a warning per probe for work nobody had
56
+ lost. Requests are now answered in their own task, so the loop keeps
57
+ receiving. Device work stays serialized exactly as before — the async
58
+ wrapper holds a per-device lock around every call — and what overtakes it is
59
+ `describe`, which is introspection over the class and the op registry and
60
+ touches no hardware. Replies are serialized on their own lock, because a
61
+ reply is two frames on one shared ROUTER socket.
62
+
63
+ A lost *liveness* reply is also no longer a warning. `disconnect` was
64
+ already excluded for the same reason: the warning exists for an answer to
65
+ work the device has already done, and a probe that reads state and moves no
66
+ instrument loses nothing when its reply misses.
67
+
68
+ - **`CompositeDevice.identity()` runs on the sub-device's own thread (#49).**
69
+ It was the one path that called the client directly instead of going through
70
+ `submit` — `call`, `preflight`, `set_data_path` and `disconnect_all` all hop
71
+ first. Under `thread_affinity` a sub-device's ZMQ socket belongs to its
72
+ thread, so asking for the identity from another one put two threads on that
73
+ socket and the replies crossed: a magneto-PL rig whose power regulator was
74
+ sampling the meter got the identity string back from `measure_power`. The
75
+ crash was the lucky case — two calls of the same shape crossing swap two
76
+ readings and say nothing.
77
+
78
+ - **The functions gate now issues a control operation into a move, instead of
79
+ at a stage already parked.** #39 built the `control` kind but drove it at
80
+ rest: controls were sorted last and the stop went out after every motion had
81
+ finished and been put back. That proves the command is accepted, not that it
82
+ stops anything — and the case that matters is the other one. k10cr1#2, motion
83
+ methods returning before the stage had settled, was found by hand at a bench;
84
+ an in-flight stop is the gate that would have caught it.
85
+
86
+ The move is put in flight the way the drain gate provokes a slow call: the
87
+ transport's timeout is shortened so the command goes out and its answer is
88
+ abandoned, leaving the part travelling. Then the control is issued, and three
89
+ things have to hold — it returns while the move is still in flight,
90
+ `position_key` reads short of where the move was heading, and the part answers
91
+ afterwards and goes back where it started. The same shape runs over the wire
92
+ in `ClientFieldTest`, which takes `slow_op_payload` for it.
93
+
94
+ No worker thread drives the device. Servers pin device calls to one thread
95
+ because drivers turned out to have thread *affinity*, and a gate that called
96
+ one device from two threads would risk failing for a reason that has nothing
97
+ to do with the control operation.
98
+
99
+ **A module that stops late now fails a gate that passed before**, and a
100
+ control is skipped — with the reason — where no motion is allow-listed or no
101
+ `slow_op_payload` delta is declared, rather than being called at rest to no
102
+ purpose. The gate's evidence is also bound before the loop rather than after
103
+ it, so a run that fails still reports what it did, including whether a
104
+ stopped part was put back.
105
+
5
106
  - **A client holding an allocation can call schema operations again, and those
6
107
  calls are now access-controlled.** The server appends the caller's allocation
7
108
  to every `call` it forwards, but a schema operation is a generated closure
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: plesty-lib
3
- Version: 0.3.6
3
+ Version: 0.4.0.dev1
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>
@@ -2,8 +2,8 @@
2
2
 
3
3
  `plesty.lib.ui` is the GUI framework of the platform: **one home window that
4
4
  hosts any number of rearrangeable sub-windows**. It is deliberately generic —
5
- a live view is only the first kind of panel; a device control form or a run
6
- status board is the same contract in the same window.
5
+ a live view (`MonitorPanel`) and a device's controls (`DevicePanel`) are the
6
+ same contract in the same window, and a run status board would be a third.
7
7
 
8
8
  The framework is toolkit-free down to the last layer. `Panel`, `Theme`, and
9
9
  `MonitorPanel` import without a GUI installed; only the Qt backend needs the
@@ -72,11 +72,98 @@ Placement (`area`, `floating`, `closable`, `min_width`, `min_height`) is
72
72
  declared at construction and is only the *initial* arrangement — the operator
73
73
  owns it from there.
74
74
 
75
+ `status()` is the line in the title bar and `tone()` the colour of its dot —
76
+ `running`, `idle`, `warning`, `error`. The default reads a panel as working
77
+ whenever it has something to say, which is right for a view that only draws; a
78
+ panel that can fail *while still running* overrides it, so a device that
79
+ stopped answering is red from across the room.
80
+
75
81
  `MonitorPanel` is the bridge to the [monitor framework](monitor.md): it
76
82
  attaches a `PlotRenderer` to a monitor when it is built, and ticks it
77
83
  afterwards. Constructing and describing it needs no GUI, so a shell's
78
84
  composition is testable headless.
79
85
 
86
+ ## Device panels
87
+
88
+ A device already describes its controls completely, so `DevicePanel` builds
89
+ them instead of asking anyone to write them again:
90
+
91
+ ```python
92
+ from plesty.lib.ui import DevicePanel, LocalHandle, run
93
+
94
+ run([DevicePanel(LocalHandle(spectrometer), name="cct",
95
+ include=("exposure", "hardware_average"))])
96
+ ```
97
+
98
+ Every widget comes from the schema. Nothing is hand-placed, and nothing is
99
+ hand-bounded:
100
+
101
+ | The device declares | The panel renders |
102
+ |---|---|
103
+ | `dtype` `float`/`int` | spin box, adaptive step |
104
+ | `min_value` / `max_value` | its range — and where the device declares none, none |
105
+ | `options` | drop-down |
106
+ | `dtype` `bool` | check box |
107
+ | `read_only` | a readout, not an editor |
108
+ | `write_only` | an editor that is never polled |
109
+ | `unit` | the editor's suffix |
110
+ | `description` | the tooltip |
111
+ | an operation | a button, with a small form when it takes arguments |
112
+
113
+ That table is the whole point. A panel written by hand for one spectrometer
114
+ bounded its exposure at `0.01–60000 ms` where the device declares no upper
115
+ bound at all, and its hardware averaging at `1–1000` where the device declares
116
+ no limit — numbers that exist nowhere in the instrument, constraining whoever
117
+ used it. `controls_for()` derives them, is free of any toolkit, and is tested
118
+ without a display, so a drift like that fails a test instead of reaching a
119
+ bench.
120
+
121
+ **What stays out.** A spectrum, a beam image, a stage map are not derivable
122
+ from a schema — those are monitors, and a `MonitorPanel` docks beside this one.
123
+ Operations declared `lifecycle` (homing, reset, replacing a dark reference) are
124
+ left off unless named in `operations=`: they should not be one click away.
125
+
126
+ **A panel starts read-only.** A monitor holds no device; this one necessarily
127
+ does, and the instrument may be driven by a running experiment. Editors and
128
+ buttons stay disabled behind the panel's *Take control* toggle
129
+ (`set_writable()`).
130
+
131
+ ### Where the device is
132
+
133
+ `DevicePanel` never talks to a device directly — it drives a `DeviceHandle`,
134
+ which is four calls: `describe()`, `query(key)`, `write(key, value)` and
135
+ `call(op, **kwargs)`. That is what lets one panel serve three places:
136
+
137
+ | | The handle |
138
+ |---|---|
139
+ | A module's own GUI | `LocalHandle(device)` |
140
+ | A console beside the device server | `DeviceTCPIPClient` — it satisfies the protocol as written |
141
+ | Another machine | the same four calls, relayed by the bench agent |
142
+
143
+ `describe` carries the device's serialized `DeviceDocModel` — units, bounds,
144
+ options, descriptions — beside the method list it always returned, so a caller
145
+ that never imports plesty-lib reads what the device declared instead of
146
+ inventing it. A server older than that payload answers with method names only,
147
+ and the panel says so rather than showing an empty form.
148
+
149
+ `LocalHandle` goes through the same serialized payload the wire uses. That is
150
+ deliberate: a module's own GUI then exercises exactly what the console and a
151
+ remote client will see, so a hole in the payload shows up at the bench and not
152
+ only across a network.
153
+
154
+ ### Why it never reads on the GUI thread
155
+
156
+ Panels are ticked on the shell's clock, and a device read is a round trip to
157
+ hardware. Every call therefore goes to a `DeviceWorker` — one thread per panel
158
+ — and every result comes back through a queue the panel drains on its next
159
+ tick, so the shell's own clock does the hand-over and no Qt signal is involved.
160
+
161
+ One worker *per panel*, not per shell: a device server that has died answers
162
+ nothing until its client times out, and a shared worker would let that one
163
+ device stall every panel in the window. Requests are coalesced by tag, so a
164
+ device slower than the refresh interval drops the polls it cannot keep up with
165
+ instead of accumulating a backlog and showing readings from minutes ago.
166
+
80
167
  ## Theme
81
168
 
82
169
  Colours are data, not code. `Theme` reads `assets/palette.json` and renders
@@ -135,9 +222,14 @@ skipped where the `gui` extra is not installed, which is why CI (and any
135
222
  headless lab host) still runs the full suite:
136
223
 
137
224
  ```bash
138
- QT_QPA_PLATFORM=offscreen uv run pytest tests/test_ui.py
225
+ QT_QPA_PLATFORM=offscreen uv run pytest tests/test_ui.py tests/test_device_panel.py
139
226
  ```
140
227
 
228
+ A device panel is tested the same way, and further: `open()` starts its worker
229
+ without building a widget, so the whole device-facing half — the schema
230
+ arriving, polling, a refused write, a device that stops answering — is driven
231
+ with no display and no toolkit at all.
232
+
141
233
  ## See also
142
234
 
143
235
  - [Monitor framework](monitor.md) — what `MonitorPanel` shows.
@@ -19,19 +19,19 @@
19
19
  "version": "8.30.1"
20
20
  },
21
21
  "type": "secret_detection",
22
- "start_time": "2026-08-31T15:10:17",
23
- "end_time": "2026-08-31T15:10:17",
22
+ "start_time": "2026-09-03T11:41:33",
23
+ "end_time": "2026-09-03T11:41:34",
24
24
  "status": "success",
25
25
  "observability": {
26
26
  "events": [
27
27
  {
28
28
  "event": "collect_secrets_analyzer_scan_metrics_from_pipeline",
29
- "time_s": 0.468042116,
29
+ "time_s": 0.518565688,
30
30
  "exit_code": 0,
31
31
  "git_strategy": "FetchShallow",
32
- "repo_size_kb": 732,
32
+ "repo_size_kb": 767,
33
33
  "commit_count": 1,
34
- "bytes_scanned": 7570,
34
+ "bytes_scanned": 76550,
35
35
  "pipeline_type": "Tag"
36
36
  },
37
37
  {
@@ -719,12 +719,20 @@ class CompositeDevice:
719
719
  self._set_state(dev, ConnectionState.CONNECTED, "reconnected")
720
720
 
721
721
  def identity(self) -> dict[str, Any]:
722
- """Query the identity of each sub-device and return a mapping of results."""
722
+ """Query the identity of each sub-device and return a mapping of results.
723
+
724
+ Routed through :meth:`submit` like every other sub-device call, not
725
+ called on the client directly. Under ``thread_affinity`` a sub-device's
726
+ socket belongs to its own thread, and asking here from a foreign one
727
+ put a second thread on it: a magneto-PL rig whose power regulator was
728
+ sampling the meter got the identity string back from ``measure_power``
729
+ (2026-09-01). The crash was the lucky case — two calls of the same
730
+ shape crossing swap two readings and say nothing.
731
+ """
723
732
  identities = {}
724
733
  for device_name in self.devices:
725
- device = getattr(self, device_name)
726
734
  try:
727
- identities[device_name] = device.identity()
735
+ identities[device_name] = self.submit(device_name, "identity").result()
728
736
  except Exception as e:
729
737
  identities[device_name] = f"Error: {e}"
730
738
  return identities
@@ -31,6 +31,58 @@ class ResponseParser:
31
31
  )
32
32
 
33
33
 
34
+ def dtype_name(dtype: Any) -> str:
35
+ """Return the wire name of a declared data type.
36
+
37
+ A schema declares a type as a Python type, a typing construct, or already
38
+ as a string. A caller on the far side of the device protocol has to switch
39
+ on it, so it arrives as a name and never as a ``<class 'float'>`` repr.
40
+
41
+ Args:
42
+ dtype: The declared type, or ``None``.
43
+
44
+ Returns:
45
+ The type's name; ``"Any"`` when nothing was declared.
46
+ """
47
+ if dtype is None:
48
+ return "Any"
49
+ if isinstance(dtype, str):
50
+ return dtype
51
+ name = getattr(dtype, "__name__", None)
52
+ return name if name else str(dtype).replace("typing.", "")
53
+
54
+
55
+ def jsonable(value: Any) -> Any:
56
+ """Return *value* as something :func:`json.dumps` accepts.
57
+
58
+ Schema defaults, options and bounds are whatever the module declared — a
59
+ numpy scalar, an enum member, a tuple. They cross the wire as
60
+ documentation, so a value JSON has no form for is described rather than
61
+ dropped: a reader learns what the default is even when it cannot be one.
62
+
63
+ Args:
64
+ value: The value to convert.
65
+
66
+ Returns:
67
+ The value unchanged when JSON already holds it, a list or dict when it
68
+ is a container, else its ``str()``.
69
+ """
70
+ if value is None or isinstance(value, (bool, int, float, str)):
71
+ return value
72
+ if isinstance(value, (list, tuple, set, frozenset)):
73
+ return [jsonable(item) for item in value]
74
+ if isinstance(value, dict):
75
+ return {str(key): jsonable(item) for key, item in value.items()}
76
+ # A numpy scalar is not an int or a str but holds one.
77
+ item = getattr(value, "item", None)
78
+ if callable(item):
79
+ try:
80
+ return jsonable(item())
81
+ except Exception:
82
+ pass
83
+ return str(value)
84
+
85
+
34
86
  # define decoretor for device methods that require operatability check
35
87
  def operatable(func: Callable[..., Any]) -> Callable[..., Any]:
36
88
  """Decorator that checks device operatability before executing the wrapped function."""
@@ -14,6 +14,7 @@ it instead of re-deriving the metadata.
14
14
  """
15
15
 
16
16
  from dataclasses import dataclass
17
+ from typing import Any
17
18
 
18
19
  from plesty.lib.device.funcs import FuncDoc
19
20
  from plesty.lib.device.params import ConfigParameter
@@ -35,3 +36,24 @@ class DeviceDocModel:
35
36
  parameters: dict[str, list[ConfigParameter]]
36
37
  functions: list[FuncDoc]
37
38
  standard_methods: list[FuncDoc]
39
+
40
+ def to_dict(self) -> dict[str, Any]:
41
+ """Return the whole model as JSON primitives.
42
+
43
+ This is what leaves the device: the ``describe`` reply carries it, so a
44
+ caller that never imports plesty-lib — a console on another process, a
45
+ client on another machine — reads the same units, bounds, options and
46
+ descriptions the device declared, instead of inventing them.
47
+
48
+ Returns:
49
+ A mapping with ``parameters`` (grouped, as the model holds them),
50
+ ``functions`` and ``standard_methods``, all JSON primitives.
51
+ """
52
+ return {
53
+ "parameters": {
54
+ group: [param.to_dict() for param in params]
55
+ for group, params in self.parameters.items()
56
+ },
57
+ "functions": [func.to_dict() for func in self.functions],
58
+ "standard_methods": [func.to_dict() for func in self.standard_methods],
59
+ }
@@ -14,7 +14,13 @@ import re
14
14
  import reprlib
15
15
  import numpy as np
16
16
 
17
- from plesty.lib.device.device_utils import OpKind, ResponseParser, op_kind_of
17
+ from plesty.lib.device.device_utils import (
18
+ OpKind,
19
+ ResponseParser,
20
+ dtype_name,
21
+ jsonable,
22
+ op_kind_of,
23
+ )
18
24
  from plesty.lib.data import (
19
25
  PlestyArray,
20
26
  PlestyTable2D,
@@ -161,6 +167,26 @@ class FuncParam:
161
167
  item_dtype: Any = field(default=None)
162
168
  description: str = field(default="")
163
169
 
170
+ def to_dict(self) -> dict[str, Any]:
171
+ """Return this argument as JSON primitives.
172
+
173
+ Returns:
174
+ Name, type, unit, default and constraints — what a caller needs to
175
+ offer the argument and check it before sending.
176
+ """
177
+ return {
178
+ "name": self.name,
179
+ "dtype": dtype_name(self.dtype),
180
+ "unit": self.unit,
181
+ "default": jsonable(self.default),
182
+ "required": self.required,
183
+ "options": jsonable(self.options),
184
+ "range": jsonable(self.range),
185
+ "shape": jsonable(self.shape),
186
+ "item_dtype": dtype_name(self.item_dtype) if self.item_dtype is not None else None,
187
+ "description": self.description,
188
+ }
189
+
164
190
 
165
191
  @dataclass
166
192
  class FuncOutput:
@@ -177,6 +203,35 @@ class FuncOutput:
177
203
  headers: list[TableHeader] | None = field(default=None)
178
204
  description: str = field(default="")
179
205
 
206
+ def to_dict(self) -> dict[str, Any]:
207
+ """Return this output as JSON primitives.
208
+
209
+ Returns:
210
+ Name, type, unit and shape — what a caller needs to read the
211
+ result, including a table's column headers.
212
+ """
213
+ return {
214
+ "name": self.name,
215
+ "dtype": dtype_name(self.dtype),
216
+ "unit": self.unit,
217
+ "required": self.required,
218
+ "code_mapping": jsonable(self.code_mapping),
219
+ "range": jsonable(self.range),
220
+ "shape": jsonable(self.shape),
221
+ "item_dtype": dtype_name(self.item_dtype) if self.item_dtype is not None else None,
222
+ "headers": [
223
+ {
224
+ "name": header.name,
225
+ "dtype": dtype_name(header.dtype),
226
+ "unit": header.unit,
227
+ "description": header.description,
228
+ }
229
+ for header in self.headers or []
230
+ ]
231
+ or None,
232
+ "description": self.description,
233
+ }
234
+
180
235
 
181
236
  @dataclass
182
237
  class FuncDoc:
@@ -204,6 +259,24 @@ class FuncDoc:
204
259
  oparams: list["FuncOutput"]
205
260
  kind: OpKind = field(default_factory=OpKind)
206
261
 
262
+ def to_dict(self) -> dict[str, Any]:
263
+ """Return this operation as JSON primitives.
264
+
265
+ Returns:
266
+ The operation with its arguments, its outputs and its
267
+ :class:`~plesty.lib.device.device_utils.OpKind` — enough for a
268
+ caller to offer it, and to decide whether it should be one click
269
+ away.
270
+ """
271
+ return {
272
+ "name": self.name,
273
+ "source": self.source,
274
+ "description": self.description,
275
+ "iparams": [param.to_dict() for param in self.iparams],
276
+ "oparams": [output.to_dict() for output in self.oparams],
277
+ "kind": self.kind.to_dict(),
278
+ }
279
+
207
280
 
208
281
  #: Size-bounded repr for operation log lines, so array-valued parameters do
209
282
  #: not flood the log.
@@ -12,7 +12,7 @@ import logging
12
12
  import re
13
13
 
14
14
  from plesty.lib.utils.error_utils import handle_error
15
- from plesty.lib.device.device_utils import ResponseParser
15
+ from plesty.lib.device.device_utils import ResponseParser, dtype_name, jsonable
16
16
  from plesty.lib.data import resolve_dtype, istype, cast_basic_type
17
17
 
18
18
 
@@ -271,6 +271,30 @@ class ConfigParameter:
271
271
  parser: ResponseParser | None = None
272
272
  description: str | None = None
273
273
 
274
+ def to_dict(self) -> dict[str, Any]:
275
+ """Return the caller-facing description of this parameter.
276
+
277
+ Everything needed to render or bound the parameter, and nothing about
278
+ how it reaches the instrument: ``command`` and ``parser`` are this
279
+ device's own business, and neither has a JSON form.
280
+
281
+ Returns:
282
+ The parameter as JSON primitives.
283
+ """
284
+ return {
285
+ "name": self.name,
286
+ "dtype": dtype_name(self.dtype),
287
+ "unit": self.unit,
288
+ "default": jsonable(self.default),
289
+ "value": jsonable(self.value),
290
+ "read_only": self.read_only,
291
+ "write_only": self.write_only,
292
+ "min_value": jsonable(self.min_value),
293
+ "max_value": jsonable(self.max_value),
294
+ "options": jsonable(self.options),
295
+ "description": self.description,
296
+ }
297
+
274
298
 
275
299
  @dataclass
276
300
  class ConfigGroup: