bec-lib 4.3.2__tar.gz → 4.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. {bec_lib-4.3.2 → bec_lib-4.4.0}/PKG-INFO +1 -1
  2. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/devicemanager.py +7 -5
  3. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/endpoints.py +23 -1
  4. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/messages.py +112 -3
  5. {bec_lib-4.3.2 → bec_lib-4.4.0}/pyproject.toml +1 -1
  6. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_device_manager.py +34 -0
  7. bec_lib-4.4.0/tests/test_roi_endpoints.py +24 -0
  8. bec_lib-4.4.0/tests/test_roi_messages.py +93 -0
  9. {bec_lib-4.3.2 → bec_lib-4.4.0}/.gitignore +0 -0
  10. {bec_lib-4.3.2 → bec_lib-4.4.0}/README.md +0 -0
  11. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/__init__.py +0 -0
  12. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/_print_versions.py +0 -0
  13. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/acl_login.py +0 -0
  14. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/actors.py +0 -0
  15. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/alarm_handler.py +0 -0
  16. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/atlas_models.py +0 -0
  17. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/bec_errors.py +0 -0
  18. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/bec_service.py +0 -0
  19. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/bec_yaml_loader.py +0 -0
  20. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/bl_state_manager.py +0 -0
  21. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/bl_states.py +0 -0
  22. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/builtin_actor_hli.py +0 -0
  23. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/callback_handler.py +0 -0
  24. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/channel_monitor.py +0 -0
  25. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/client.py +0 -0
  26. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/codecs.py +0 -0
  27. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/config_helper.py +0 -0
  28. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/config_values.py +0 -0
  29. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/configs/__init__.py +0 -0
  30. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/configs/demo_config.yaml +0 -0
  31. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/configs/openapi_schema.json +0 -0
  32. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/connector.py +0 -0
  33. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/dap_plugin_objects.py +0 -0
  34. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/dap_plugins.py +0 -0
  35. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/device.py +0 -0
  36. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/file_utils.py +0 -0
  37. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/live_scan_data.py +0 -0
  38. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/lmfit_serializer.py +0 -0
  39. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/logger.py +0 -0
  40. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/macro_update_handler.py +0 -0
  41. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/messaging_hooks.py +0 -0
  42. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/messaging_services.py +0 -0
  43. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/metadata_schema.py +0 -0
  44. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/numpy_encoder.py +0 -0
  45. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/observer.py +0 -0
  46. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/path_optimization.py +0 -0
  47. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/pdf_writer.py +0 -0
  48. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/plugin_helper.py +0 -0
  49. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/position_generators.py +0 -0
  50. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/procedures/__init__.py +0 -0
  51. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/procedures/helper.py +0 -0
  52. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/procedures/hli.py +0 -0
  53. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/procedures/status.py +0 -0
  54. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/queue_items.py +0 -0
  55. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/redis_connector/__init__.py +0 -0
  56. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/redis_connector/constants.py +0 -0
  57. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/redis_connector/hli.py +0 -0
  58. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/redis_connector/managed_redis_connection.py +0 -0
  59. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/redis_connector/streams.py +0 -0
  60. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/redis_connector/validation.py +0 -0
  61. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/request_context.py +0 -0
  62. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/request_items.py +0 -0
  63. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/scan_args.py +0 -0
  64. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/scan_data_container.py +0 -0
  65. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/scan_history.py +0 -0
  66. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/scan_input_validator.py +0 -0
  67. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/scan_items.py +0 -0
  68. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/scan_manager.py +0 -0
  69. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/scan_number_container.py +0 -0
  70. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/scan_preview.py +0 -0
  71. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/scan_repeat.py +0 -0
  72. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/scan_report.py +0 -0
  73. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/scans.py +0 -0
  74. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/scibec_validator.py +0 -0
  75. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/script_executor.py +0 -0
  76. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/serialization.py +0 -0
  77. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/serialization_registry.py +0 -0
  78. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/service_config.py +0 -0
  79. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/signature_serializer.py +0 -0
  80. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/tests/__init__.py +0 -0
  81. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/tests/fixtures.py +0 -0
  82. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/tests/test_config.yaml +0 -0
  83. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/tests/test_service_config.yaml +0 -0
  84. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/tests/utils.py +0 -0
  85. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/user_macros.py +0 -0
  86. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/utils/__init__.py +0 -0
  87. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/utils/copier_jinja_filters.py +0 -0
  88. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/utils/deprecation.py +0 -0
  89. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/utils/import_utils.py +0 -0
  90. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/utils/json_extended.py +0 -0
  91. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/utils/plugin_manager/__init__.py +0 -0
  92. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/utils/plugin_manager/_constants.py +0 -0
  93. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/utils/plugin_manager/_util.py +0 -0
  94. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/utils/plugin_manager/create/__init__.py +0 -0
  95. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/utils/plugin_manager/create/device.py +0 -0
  96. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/utils/plugin_manager/create/scan.py +0 -0
  97. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/utils/plugin_manager/create/templates/scan.py.jinja +0 -0
  98. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/utils/plugin_manager/main.py +0 -0
  99. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/utils/proxy.py +0 -0
  100. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/utils/pydantic_pretty_print.py +0 -0
  101. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/utils/rpc_utils.py +0 -0
  102. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/utils/scan_utils.py +0 -0
  103. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/utils/threading_utils.py +0 -0
  104. {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/utils/user_acls_test.py +0 -0
  105. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/__init__.py +0 -0
  106. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/_additional_for_signature_serializer_test.py +0 -0
  107. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/conftest.py +0 -0
  108. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_acl_login.py +0 -0
  109. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_alarm_handler.py +0 -0
  110. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_atlas_models.py +0 -0
  111. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_beamline_states.py +0 -0
  112. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_bec_logger.py +0 -0
  113. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_bec_messages.py +0 -0
  114. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_bec_service.py +0 -0
  115. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_callback_handler.py +0 -0
  116. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_channel_monitor.py +0 -0
  117. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_client.py +0 -0
  118. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_config_helper.py +0 -0
  119. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_core_utils.py +0 -0
  120. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_dap_plugins.py +0 -0
  121. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_data_container.py +0 -0
  122. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_deprecation.py +0 -0
  123. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_device_hashing.py +0 -0
  124. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_devices.py +0 -0
  125. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_file_utils.py +0 -0
  126. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_hli_connector_fakeredis.py +0 -0
  127. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_hli_redis_connector.py +0 -0
  128. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_import_utils.py +0 -0
  129. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_jinja_plugins.py +0 -0
  130. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_lmfit_serializer.py +0 -0
  131. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_managed_config_values.py +0 -0
  132. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_managed_redis_connection.py +0 -0
  133. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_messaging_hooks.py +0 -0
  134. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_messaging_service.py +0 -0
  135. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_metadata_schema.py +0 -0
  136. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_observer.py +0 -0
  137. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_path_optimization.py +0 -0
  138. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_pdf_writer.py +0 -0
  139. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_plugin_helper.py +0 -0
  140. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_plugin_manager.py +0 -0
  141. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_plugin_system.py +0 -0
  142. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_position_generators.py +0 -0
  143. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_procedure_submission.py +0 -0
  144. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_pydantic_pretty_print.py +0 -0
  145. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_queue_items.py +0 -0
  146. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_redis_connector_fakeredis.py +0 -0
  147. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_scan_context.py +0 -0
  148. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_scan_data.py +0 -0
  149. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_scan_history.py +0 -0
  150. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_scan_items.py +0 -0
  151. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_scan_manager.py +0 -0
  152. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_scan_object.py +0 -0
  153. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_scan_preview.py +0 -0
  154. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_scan_repeat.py +0 -0
  155. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_scan_report.py +0 -0
  156. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_scibec_validator.py +0 -0
  157. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_script_executor.py +0 -0
  158. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_serializer.py +0 -0
  159. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_service_config.py +0 -0
  160. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_signature_serializer.py +0 -0
  161. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_signature_serializer_with_future_import.py +0 -0
  162. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_stream_subs.py +0 -0
  163. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_user_macros.py +0 -0
  164. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_utils.py +0 -0
  165. {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_yaml_loader.py +0 -0
  166. {bec_lib-4.3.2 → bec_lib-4.4.0}/util_scripts/init_config.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: bec_lib
3
- Version: 4.3.2
3
+ Version: 4.4.0
4
4
  Summary: BEC library
5
5
  Project-URL: Bug Tracker, https://github.com/bec-project/bec/issues
6
6
  Project-URL: Homepage, https://github.com/bec-project/bec
@@ -598,13 +598,15 @@ class DeviceManagerBase:
598
598
  )
599
599
  if "enabled" in config[dev]:
600
600
  self.devices[dev]._config["enabled"] = config[dev]["enabled"]
601
- if self.devices[dev].enabled:
602
- dev_info_msg = self._get_device_info(dev)
603
- self.devices[dev]._info = dev_info_msg.info
604
- if self._use_proxy_objects:
605
- self.devices[dev]._parse_info()
606
601
  status = "enabled" if self.devices[dev].enabled else "disabled"
607
602
  logger.info(f"Device {dev} has been {status}.")
603
+ if ("deviceConfig" in config[dev] or "enabled" in config[dev]) and self.devices[
604
+ dev
605
+ ].enabled:
606
+ dev_info_msg = self._get_device_info(dev)
607
+ self.devices[dev]._info = dev_info_msg.info
608
+ if self._use_proxy_objects:
609
+ self.devices[dev]._parse_info()
608
610
  if "readOnly" in config[dev]:
609
611
  self.devices[dev]._config["readOnly"] = config[dev]["readOnly"]
610
612
  if "userParameter" in config[dev]:
@@ -2166,10 +2166,32 @@ class MessageEndpoints:
2166
2166
  message_op=MessageOp.SET_PUBLISH,
2167
2167
  )
2168
2168
 
2169
+ @staticmethod
2170
+ def roi_config_request(device: str, signal: str, username: str) -> EndpointInfo:
2171
+ """Request an ROI configuration change, scoped to the submitting user by Redis ACLs.
2172
+
2173
+ Args:
2174
+ device (str): Name of the device.
2175
+ signal (str): Name of the signal.
2176
+ username (str): Username of the submitting user; subscribers may use ``*``.
2177
+
2178
+ Returns:
2179
+ EndpointInfo: Endpoint for ROI configuration requests.
2180
+ """
2181
+ return EndpointInfo(
2182
+ endpoint=(
2183
+ f"{EndpointType.PERSONAL.value}/{username}/devices/roi_config_request/"
2184
+ f"{device}/{signal}"
2185
+ ),
2186
+ message_type=messages.ROIConfigurationMessage,
2187
+ message_op=MessageOp.SEND,
2188
+ )
2189
+
2169
2190
  @staticmethod
2170
2191
  def roi_config(device: str, signal: str):
2171
2192
  """
2172
- Endpoint for ROI configuration. This endpoint is used to publish ROI configuration updates using a messages.ROIConfigurationMessage message.
2193
+ Current ROI configuration published by the device after applying a request.
2194
+ Clients send changes to roi_config_request instead.
2173
2195
 
2174
2196
  Args:
2175
2197
  device (str): Name of the device.
@@ -56,15 +56,116 @@ class BECStatus(Enum):
56
56
  ERROR = -1
57
57
 
58
58
 
59
- class ROIConfig(BaseModel):
60
- """Configuration for the ROI processing signal."""
59
+ class RectROIConfig(BaseModel):
60
+ """Axis-aligned rectangle in pixels, with x/y at the top-left corner."""
61
61
 
62
+ model_config = ConfigDict(extra="forbid")
63
+
64
+ type: Literal["rect"] = "rect"
62
65
  x: int
63
66
  y: int
64
67
  width: int
65
68
  height: int
66
69
 
67
70
 
71
+ class RotatedRectROIConfig(BaseModel):
72
+ """Rectangle in pixels, rotated about its unrotated bounding-box center.
73
+
74
+ x/y locate the top-left of the unrotated box. The rotation center is
75
+ (x + width / 2, y + height / 2). Positive angles are clockwise in degrees
76
+ in image coordinates (x right, y down).
77
+ """
78
+
79
+ model_config = ConfigDict(extra="forbid", allow_inf_nan=False)
80
+
81
+ type: Literal["rotated_rect"] = "rotated_rect"
82
+ x: float
83
+ y: float
84
+ width: float = Field(gt=0)
85
+ height: float = Field(gt=0)
86
+ angle: float
87
+
88
+
89
+ class CircleROIConfig(BaseModel):
90
+ """Circle in pixels; x/y locate the top-left of its bounding box.
91
+
92
+ Its center is (x + radius, y + radius) in image coordinates (x right, y down).
93
+ """
94
+
95
+ model_config = ConfigDict(extra="forbid", allow_inf_nan=False)
96
+
97
+ type: Literal["circle"] = "circle"
98
+ x: float
99
+ y: float
100
+ radius: float = Field(gt=0)
101
+
102
+
103
+ class EllipseROIConfig(BaseModel):
104
+ """Ellipse in pixels, rotated about the center of its unrotated bounding box.
105
+
106
+ x/y locate the top-left of that box; width/height are its full dimensions.
107
+ The rotation center is (x + width / 2, y + height / 2). Positive angles are
108
+ clockwise in degrees in image coordinates (x right, y down).
109
+ """
110
+
111
+ model_config = ConfigDict(extra="forbid", allow_inf_nan=False)
112
+
113
+ type: Literal["ellipse"] = "ellipse"
114
+ x: float
115
+ y: float
116
+ width: float = Field(gt=0)
117
+ height: float = Field(gt=0)
118
+ angle: float
119
+
120
+
121
+ class PixelMapROIConfig(BaseModel):
122
+ """Full-image mask indexed as mask[y][x]; True pixels belong to the ROI."""
123
+
124
+ model_config = ConfigDict(extra="forbid")
125
+
126
+ type: Literal["pixelmap"] = "pixelmap"
127
+ mask: list[list[bool]]
128
+
129
+ @field_validator("mask")
130
+ @classmethod
131
+ def validate_mask(cls, mask: list[list[bool]]) -> list[list[bool]]:
132
+ """Require a nonempty, rectangular two-dimensional mask."""
133
+ if not mask or not mask[0] or any(len(row) != len(mask[0]) for row in mask):
134
+ raise ValueError("Pixel map must be a nonempty rectangular 2D mask.")
135
+ return mask
136
+
137
+
138
+ class CustomAnchorROIConfig(BaseModel):
139
+ """Closed polygon of ordered (x, y) vertices in pixels.
140
+
141
+ The closing edge is implicit; the first vertex need not be repeated.
142
+ """
143
+
144
+ model_config = ConfigDict(extra="forbid", allow_inf_nan=False)
145
+
146
+ type: Literal["custom_anchor"] = "custom_anchor"
147
+ vertices: list[tuple[float, float]] = Field(min_length=3)
148
+
149
+ @field_validator("vertices")
150
+ @classmethod
151
+ def validate_vertices(cls, vertices: list[tuple[float, float]]) -> list[tuple[float, float]]:
152
+ """Require at least three distinct polygon vertices."""
153
+ if len(set(vertices)) < 3:
154
+ raise ValueError("A polygon requires at least three distinct vertices.")
155
+ return vertices
156
+
157
+
158
+ ROIConfiguration = Annotated[
159
+ RectROIConfig
160
+ | RotatedRectROIConfig
161
+ | CircleROIConfig
162
+ | EllipseROIConfig
163
+ | PixelMapROIConfig
164
+ | CustomAnchorROIConfig,
165
+ Field(discriminator="type"),
166
+ ]
167
+
168
+
68
169
  class ROIAnalysisConfig(BaseModel):
69
170
  """Configuration for the ROI analysis operation."""
70
171
 
@@ -158,10 +259,18 @@ class ROIConfigurationMessage(BECMessage):
158
259
  signal: str
159
260
  name: str
160
261
  active: bool
161
- roi_config: ROIConfig
262
+ roi_config: ROIConfiguration
162
263
  selected_operations: list[str]
163
264
  block_while_scanning: bool = True
164
265
 
266
+ @field_validator("roi_config", mode="before")
267
+ @classmethod
268
+ def accept_legacy_rectangle(cls, value: Any) -> Any:
269
+ """Interpret untagged rectangle payloads from older clients as rect ROIs."""
270
+ if isinstance(value, dict) and "type" not in value:
271
+ return {"type": "rect", **value}
272
+ return value
273
+
165
274
 
166
275
  class ROIAvailableAnalysisMessage(BECMessage):
167
276
  """Message for providing the available ROI analysis operations."""
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "bec_lib"
7
- version = "4.3.2"
7
+ version = "4.4.0"
8
8
  description = "BEC library"
9
9
  requires-python = ">=3.11"
10
10
  classifiers = [
@@ -691,3 +691,37 @@ def test_set_rpc_method_context(dm_with_devices):
691
691
  assert rpc_method_context.get() == dummy_method
692
692
  assert rpc_method_context.get() == another_dummy_method
693
693
  assert rpc_method_context.get() is None
694
+
695
+
696
+ def test_device_config_update_refreshes_saved_signal_metadata(dm_with_devices):
697
+ """A file-writer proxy must see changed persistence flags before the next scan."""
698
+ manager = dm_with_devices
699
+ device = manager.devices.samx
700
+ old_info = copy.deepcopy(device._info)
701
+ old_info.setdefault("signals", {})["roi.result_scalar"] = {
702
+ "signal_class": "DynamicSignal",
703
+ "describe": {"signal_info": {"saved": True}},
704
+ }
705
+ device._info = old_info
706
+ new_info = copy.deepcopy(old_info)
707
+ new_info["signals"]["roi.result_scalar"]["describe"]["signal_info"]["saved"] = False
708
+ msg = messages.DeviceConfigMessage(
709
+ action="update", config={"samx": {"deviceConfig": {"save_roi_data": False}}}
710
+ )
711
+ with (
712
+ mock.patch.object(
713
+ manager,
714
+ "_get_device_info",
715
+ return_value=messages.DeviceInfoMessage(device="samx", info=new_info),
716
+ ) as get_info,
717
+ mock.patch.object(device, "_parse_info") as parse_info,
718
+ ):
719
+ manager.parse_config_message(msg)
720
+ get_info.assert_called_once_with("samx")
721
+ parse_info.assert_called_once()
722
+ roi = next(
723
+ info
724
+ for name, component, info in manager.get_bec_signals("DynamicSignal")
725
+ if name == "samx" and component == "roi.result_scalar"
726
+ )
727
+ assert roi["describe"]["signal_info"]["saved"] is False
@@ -0,0 +1,24 @@
1
+ """ROI clients send requests; devices publish the current configuration."""
2
+
3
+ from bec_lib.endpoints import MessageEndpoints, MessageOp
4
+ from bec_lib.messages import ROIConfigurationMessage
5
+
6
+
7
+ def test_roi_request_and_current_configuration_are_separate():
8
+ request = MessageEndpoints.roi_config_request("camera", "roi_processing2", "alice")
9
+ current = MessageEndpoints.roi_config("camera", "roi_processing2")
10
+ assert request.endpoint != current.endpoint
11
+ assert request.endpoint == "personal/alice/devices/roi_config_request/camera/roi_processing2"
12
+ assert (
13
+ request.endpoint
14
+ != MessageEndpoints.roi_config_request("camera", "roi_processing2", "bob").endpoint
15
+ )
16
+ assert request.message_type is ROIConfigurationMessage
17
+ assert current.message_type is ROIConfigurationMessage
18
+ assert request.message_op == MessageOp.SEND
19
+ assert current.message_op == MessageOp.SET_PUBLISH
20
+
21
+
22
+ def test_roi_request_subscription_can_match_all_users():
23
+ request = MessageEndpoints.roi_config_request("camera", "roi_processing2", "*")
24
+ assert request.endpoint == "personal/*/devices/roi_config_request/camera/roi_processing2"
@@ -0,0 +1,93 @@
1
+ """ROI geometry validation and cross-service serialization compatibility."""
2
+
3
+ import pytest
4
+ from pydantic import BaseModel, ValidationError
5
+
6
+ from bec_lib import messages
7
+ from bec_lib.serialization import MsgpackSerialization, json_ext
8
+
9
+
10
+ def make_message(config):
11
+ """Build a configuration message for a geometry payload."""
12
+ return messages.ROIConfigurationMessage(
13
+ device="camera",
14
+ signal="roi",
15
+ name="sample",
16
+ active=True,
17
+ roi_config=config,
18
+ selected_operations=["sum"],
19
+ )
20
+
21
+
22
+ @pytest.mark.parametrize("serializer", [MsgpackSerialization, json_ext])
23
+ @pytest.mark.parametrize(
24
+ "config, model",
25
+ [
26
+ ({"type": "rect", "x": 1, "y": 2, "width": 3, "height": 4}, messages.RectROIConfig),
27
+ (
28
+ {"type": "rotated_rect", "x": 1.5, "y": 2, "width": 3, "height": 4, "angle": 30},
29
+ messages.RotatedRectROIConfig,
30
+ ),
31
+ ({"type": "circle", "x": 1, "y": 2, "radius": 3}, messages.CircleROIConfig),
32
+ (
33
+ {"type": "ellipse", "x": 1, "y": 2, "width": 3, "height": 4, "angle": -45},
34
+ messages.EllipseROIConfig,
35
+ ),
36
+ ({"type": "pixelmap", "mask": [[True, False], [False, True]]}, messages.PixelMapROIConfig),
37
+ (
38
+ {"type": "custom_anchor", "vertices": [[0, 0], [2, 0], [1, 3]]},
39
+ messages.CustomAnchorROIConfig,
40
+ ),
41
+ ],
42
+ )
43
+ def test_roi_geometries_roundtrip(config, model, serializer):
44
+ msg = make_message(config)
45
+ assert isinstance(msg.roi_config, model)
46
+ restored = serializer.loads(serializer.dumps(msg))
47
+ assert restored == msg
48
+ assert isinstance(restored.roi_config, model)
49
+ assert restored.roi_config.model_dump() == msg.roi_config.model_dump()
50
+ assert make_message(msg.roi_config).roi_config == msg.roi_config
51
+
52
+
53
+ def test_legacy_rectangle_payload_remains_supported():
54
+ class LegacyROIConfig(BaseModel):
55
+ """Rectangle schema used before the geometry discriminator was added."""
56
+
57
+ x: int
58
+ y: int
59
+ width: int
60
+ height: int
61
+
62
+ config = {"x": 1, "y": 2, "width": 3, "height": 4}
63
+ msg = make_message(config)
64
+ assert isinstance(msg.roi_config, messages.RectROIConfig)
65
+ assert msg.roi_config.type == "rect"
66
+ assert config == {"x": 1, "y": 2, "width": 3, "height": 4}
67
+ assert make_message(messages.RectROIConfig(**config)) == msg
68
+ # Older readers ignore the newly added rectangle discriminator.
69
+ assert LegacyROIConfig.model_validate(msg.roi_config.model_dump()).model_dump() == config
70
+
71
+
72
+ @pytest.mark.parametrize(
73
+ "config",
74
+ [
75
+ {"type": "unknown", "x": 1, "y": 2, "width": 3, "height": 4},
76
+ {"type": "rect", "x": 1, "y": 2, "width": 3, "height": 4, "angle": 30},
77
+ {"type": "rotated_rect", "x": 1, "y": 2, "width": 3, "height": 4},
78
+ {"type": "ellipse", "x": 1, "y": 2, "width": 0, "height": 4, "angle": 0},
79
+ {"type": "circle", "x": 1, "y": 2, "radius": -1},
80
+ {"type": "circle", "x": float("nan"), "y": 2, "radius": 1},
81
+ {"type": "pixelmap", "mask": []},
82
+ {"type": "pixelmap", "mask": [[]]},
83
+ {"type": "pixelmap", "mask": [[True], [True, False]]},
84
+ {"type": "pixelmap", "mask": [[2]]},
85
+ {"type": "custom_anchor", "vertices": [[0, 0], [1, 1]]},
86
+ {"type": "custom_anchor", "vertices": [[0, 0], [1, 1], [0, 0]]},
87
+ {"type": "custom_anchor", "vertices": [[0, 0, 1], [1, 1], [1, 0]]},
88
+ {"type": "custom_anchor", "vertices": [[0, 0], [1, 1], [float("inf"), 0]]},
89
+ ],
90
+ )
91
+ def test_invalid_roi_geometry_is_rejected(config):
92
+ with pytest.raises(ValidationError):
93
+ make_message(config)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes