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.
- {bec_lib-4.3.2 → bec_lib-4.4.0}/PKG-INFO +1 -1
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/devicemanager.py +7 -5
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/endpoints.py +23 -1
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/messages.py +112 -3
- {bec_lib-4.3.2 → bec_lib-4.4.0}/pyproject.toml +1 -1
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_device_manager.py +34 -0
- bec_lib-4.4.0/tests/test_roi_endpoints.py +24 -0
- bec_lib-4.4.0/tests/test_roi_messages.py +93 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/.gitignore +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/README.md +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/__init__.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/_print_versions.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/acl_login.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/actors.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/alarm_handler.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/atlas_models.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/bec_errors.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/bec_service.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/bec_yaml_loader.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/bl_state_manager.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/bl_states.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/builtin_actor_hli.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/callback_handler.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/channel_monitor.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/client.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/codecs.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/config_helper.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/config_values.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/configs/__init__.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/configs/demo_config.yaml +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/configs/openapi_schema.json +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/connector.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/dap_plugin_objects.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/dap_plugins.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/device.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/file_utils.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/live_scan_data.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/lmfit_serializer.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/logger.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/macro_update_handler.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/messaging_hooks.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/messaging_services.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/metadata_schema.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/numpy_encoder.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/observer.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/path_optimization.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/pdf_writer.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/plugin_helper.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/position_generators.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/procedures/__init__.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/procedures/helper.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/procedures/hli.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/procedures/status.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/queue_items.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/redis_connector/__init__.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/redis_connector/constants.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/redis_connector/hli.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/redis_connector/managed_redis_connection.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/redis_connector/streams.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/redis_connector/validation.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/request_context.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/request_items.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/scan_args.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/scan_data_container.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/scan_history.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/scan_input_validator.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/scan_items.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/scan_manager.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/scan_number_container.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/scan_preview.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/scan_repeat.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/scan_report.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/scans.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/scibec_validator.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/script_executor.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/serialization.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/serialization_registry.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/service_config.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/signature_serializer.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/tests/__init__.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/tests/fixtures.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/tests/test_config.yaml +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/tests/test_service_config.yaml +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/tests/utils.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/user_macros.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/utils/__init__.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/utils/copier_jinja_filters.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/utils/deprecation.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/utils/import_utils.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/utils/json_extended.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/utils/plugin_manager/__init__.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/utils/plugin_manager/_constants.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/utils/plugin_manager/_util.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/utils/plugin_manager/create/__init__.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/utils/plugin_manager/create/device.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/utils/plugin_manager/create/scan.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/utils/plugin_manager/create/templates/scan.py.jinja +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/utils/plugin_manager/main.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/utils/proxy.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/utils/pydantic_pretty_print.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/utils/rpc_utils.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/utils/scan_utils.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/utils/threading_utils.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/bec_lib/utils/user_acls_test.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/__init__.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/_additional_for_signature_serializer_test.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/conftest.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_acl_login.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_alarm_handler.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_atlas_models.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_beamline_states.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_bec_logger.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_bec_messages.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_bec_service.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_callback_handler.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_channel_monitor.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_client.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_config_helper.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_core_utils.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_dap_plugins.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_data_container.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_deprecation.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_device_hashing.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_devices.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_file_utils.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_hli_connector_fakeredis.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_hli_redis_connector.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_import_utils.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_jinja_plugins.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_lmfit_serializer.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_managed_config_values.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_managed_redis_connection.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_messaging_hooks.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_messaging_service.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_metadata_schema.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_observer.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_path_optimization.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_pdf_writer.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_plugin_helper.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_plugin_manager.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_plugin_system.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_position_generators.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_procedure_submission.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_pydantic_pretty_print.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_queue_items.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_redis_connector_fakeredis.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_scan_context.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_scan_data.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_scan_history.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_scan_items.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_scan_manager.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_scan_object.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_scan_preview.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_scan_repeat.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_scan_report.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_scibec_validator.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_script_executor.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_serializer.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_service_config.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_signature_serializer.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_signature_serializer_with_future_import.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_stream_subs.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_user_macros.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_utils.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/tests/test_yaml_loader.py +0 -0
- {bec_lib-4.3.2 → bec_lib-4.4.0}/util_scripts/init_config.py +0 -0
|
@@ -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
|
-
|
|
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
|
|
60
|
-
"""
|
|
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:
|
|
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."""
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|