mirror of
https://github.com/tiqi-group/pydase_service_base.git
synced 2025-06-06 12:40:41 +02:00
feat: adding ionizer_interface module
This commit is contained in:
parent
5f1e925c56
commit
2d7c7f9f1e
35
icon_service_base/ionizer_interface/README.md
Normal file
35
icon_service_base/ionizer_interface/README.md
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# Ionizer Interface
|
||||||
|
|
||||||
|
`IonizerServer` is a specialized server designed to extend `pydase` applications for seamless integration with the Ionizer system. By acting as a bridge between your `pydase` service and Ionizer, this server ensures that changes or events in your service are communicated betweeen pydase and Ionizer in real-time.
|
||||||
|
|
||||||
|
## How to Use
|
||||||
|
|
||||||
|
To deploy `IonizerServer` alongside your service, follow these steps:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from icon_service_base.ionizer_interface import IonizerServer
|
||||||
|
|
||||||
|
class YourServiceClass:
|
||||||
|
# your implementation...
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Instantiate your service
|
||||||
|
service = YourServiceClass()
|
||||||
|
|
||||||
|
# Start the main pydase server with IonizerServer as an additional server
|
||||||
|
Server(
|
||||||
|
service,
|
||||||
|
additional_servers=[
|
||||||
|
{
|
||||||
|
"server": IonizerServer,
|
||||||
|
"port": 8002,
|
||||||
|
"kwargs": {},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
).run()
|
||||||
|
```
|
||||||
|
|
||||||
|
This sets up your primary `pydase` server with `YourServiceClass` as the service. Additionally, it integrates the `IonizerServer` on port `8002`.
|
||||||
|
|
||||||
|
For further details, issues, or contributions, please refer to the [official documentation](https://pydase.readthedocs.io/en/latest/) or contact the maintainers.
|
3
icon_service_base/ionizer_interface/__init__.py
Normal file
3
icon_service_base/ionizer_interface/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from icon_service_base.ionizer_interface.ionizer_server import IonizerServer
|
||||||
|
|
||||||
|
__all__ = ["IonizerServer"]
|
50
icon_service_base/ionizer_interface/ionizer_server.py
Normal file
50
icon_service_base/ionizer_interface/ionizer_server.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
from enum import Enum
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pydase
|
||||||
|
import pydase.components
|
||||||
|
import pydase.units as u
|
||||||
|
import tiqi_rpc
|
||||||
|
from pydase.utils.helpers import get_object_attr_from_path
|
||||||
|
|
||||||
|
from icon_service_base.ionizer_interface.rpc_interface import RPCInterface
|
||||||
|
|
||||||
|
|
||||||
|
class IonizerServer:
|
||||||
|
def __init__(
|
||||||
|
self, service: pydase.DataService, port: int, host: str, **kwargs: Any
|
||||||
|
) -> None:
|
||||||
|
self.server = tiqi_rpc.Server(
|
||||||
|
RPCInterface(service, **kwargs), host=host, port=port # type: ignore
|
||||||
|
)
|
||||||
|
|
||||||
|
def notify_Ionizer(parent_path: str, attr_name: str, value: Any) -> None:
|
||||||
|
"""This function notifies Ionizer about changed values.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- parent_path (str): The parent path of the parameter.
|
||||||
|
- attr_name (str): The name of the changed parameter.
|
||||||
|
- value (Any): The value of the parameter.
|
||||||
|
"""
|
||||||
|
parent_path_list = parent_path.split(".")[1:] # without classname
|
||||||
|
name = ".".join(parent_path_list + [attr_name])
|
||||||
|
if isinstance(value, Enum):
|
||||||
|
value = value.value
|
||||||
|
if isinstance(value, u.Quantity):
|
||||||
|
value = value.m
|
||||||
|
if attr_name == "value":
|
||||||
|
parent_object = get_object_attr_from_path(service, parent_path_list)
|
||||||
|
if isinstance(parent_object, pydase.components.NumberSlider):
|
||||||
|
# removes the "value" from name -> Ionizer does not know about the
|
||||||
|
# internals of NumberSlider
|
||||||
|
name = ".".join(name.split(".")[:-1])
|
||||||
|
|
||||||
|
return self.server._handler.notify( # type: ignore
|
||||||
|
{"name": name, "value": value}
|
||||||
|
)
|
||||||
|
|
||||||
|
service._callback_manager.add_notification_callback(notify_Ionizer)
|
||||||
|
self.server.install_signal_handlers = lambda: None
|
||||||
|
|
||||||
|
async def serve(self) -> None:
|
||||||
|
await self.server.serve()
|
96
icon_service_base/ionizer_interface/rpc_interface.py
Normal file
96
icon_service_base/ionizer_interface/rpc_interface.py
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import inspect
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from pydase import DataService
|
||||||
|
from pydase.components import NumberSlider
|
||||||
|
from pydase.units import Quantity
|
||||||
|
from pydase.utils.helpers import get_object_attr_from_path
|
||||||
|
from pydase.version import __version__
|
||||||
|
|
||||||
|
|
||||||
|
class RPCInterface(object):
|
||||||
|
"""RPC interface to be passed to tiqi_rpc.Server to interface with Ionizer."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, service: DataService, info: dict[str, Any] = {}, *args: Any, **kwargs: Any
|
||||||
|
) -> None:
|
||||||
|
self._service = service
|
||||||
|
self._info = info
|
||||||
|
|
||||||
|
async def version(self) -> str:
|
||||||
|
return f"pydase v{__version__}"
|
||||||
|
|
||||||
|
async def name(self) -> str:
|
||||||
|
return self._service.__class__.__name__
|
||||||
|
|
||||||
|
async def info(self) -> dict:
|
||||||
|
return self._info
|
||||||
|
|
||||||
|
async def get_props(self, name: Optional[str] = None) -> dict[str, Any]:
|
||||||
|
if name is None:
|
||||||
|
return self._service.serialize()
|
||||||
|
return self._service.serialize()
|
||||||
|
|
||||||
|
async def get_param(self, full_access_path: str) -> Any:
|
||||||
|
"""Returns the value of the parameter given by the full_access_path.
|
||||||
|
|
||||||
|
This method is called when Ionizer initilizes the Plugin or refreshes. The
|
||||||
|
widgets need to store the full_access_path in their name attribute.
|
||||||
|
"""
|
||||||
|
param = get_object_attr_from_path(self._service, full_access_path.split("."))
|
||||||
|
if isinstance(param, NumberSlider):
|
||||||
|
return param.value
|
||||||
|
elif isinstance(param, DataService):
|
||||||
|
return param.serialize()
|
||||||
|
elif inspect.ismethod(param):
|
||||||
|
# explicitly serialize any methods that will be returned
|
||||||
|
full_access_path = param.__name__
|
||||||
|
args = inspect.signature(param).parameters
|
||||||
|
return f"{full_access_path}({', '.join(args)})"
|
||||||
|
elif isinstance(param, Enum):
|
||||||
|
return param.value
|
||||||
|
elif isinstance(param, Quantity):
|
||||||
|
return param.m
|
||||||
|
else:
|
||||||
|
return param
|
||||||
|
|
||||||
|
async def set_param(self, full_access_path: str, value: Any) -> None:
|
||||||
|
parent_path_list = full_access_path.split(".")[:-1]
|
||||||
|
parent_object = get_object_attr_from_path(self._service, parent_path_list)
|
||||||
|
attr_name = full_access_path.split(".")[-1]
|
||||||
|
# I don't want to trigger the execution of a property getter as this might take
|
||||||
|
# a while when connecting to remote devices
|
||||||
|
if not isinstance(
|
||||||
|
getattr(type(parent_object), attr_name, None),
|
||||||
|
property,
|
||||||
|
):
|
||||||
|
current_value = getattr(parent_object, attr_name, None)
|
||||||
|
if isinstance(current_value, Enum) and isinstance(value, int):
|
||||||
|
# Ionizer sets the enums using the position of the definition order
|
||||||
|
# This works as definition order is kept, see e.g.
|
||||||
|
# https://docs.python.org/3/library/enum.html#enum.EnumType.__iter__
|
||||||
|
# I need to use the name attribute as this is what
|
||||||
|
# DataService.__set_attribute_based_on_type expects
|
||||||
|
value = list(current_value.__class__)[value].name
|
||||||
|
elif isinstance(current_value, NumberSlider):
|
||||||
|
parent_path_list.append(attr_name)
|
||||||
|
attr_name = "value"
|
||||||
|
self._service.update_DataService_attribute(parent_path_list, attr_name, value)
|
||||||
|
|
||||||
|
async def remote_call(self, full_access_path: str, *args: Any) -> Any:
|
||||||
|
full_access_path_list = full_access_path.split(".")
|
||||||
|
method_object = get_object_attr_from_path(self._service, full_access_path_list)
|
||||||
|
return method_object(*args)
|
||||||
|
|
||||||
|
async def emit(self, message: str) -> None:
|
||||||
|
self.notify(message)
|
||||||
|
|
||||||
|
def notify(self, message: str) -> None:
|
||||||
|
"""
|
||||||
|
This method will be overwritten by the tiqi-rpc server.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message (str): Notification message.
|
||||||
|
"""
|
||||||
|
return
|
1089
poetry.lock
generated
1089
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -10,12 +10,12 @@ packages = [{ include = "icon_service_base" }]
|
|||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.10"
|
python = "^3.10"
|
||||||
loguru = "^0.7.0"
|
loguru = "^0.7.0"
|
||||||
kombu = "^5.2.4"
|
|
||||||
influxdb-client = "^1.36.1"
|
influxdb-client = "^1.36.1"
|
||||||
sqlmodel = "^0.0.8"
|
sqlmodel = "^0.0.8"
|
||||||
confz = "^2.0.0"
|
confz = "^2.0.0"
|
||||||
psycopg2-binary = "^2.9.6"
|
psycopg2-binary = "^2.9.6"
|
||||||
|
pydase = "0.1.0"
|
||||||
|
tiqi-rpc = {git = "ssh://git@gitlab.phys.ethz.ch/tiqi-projects/tiqi-rpc-python.git"}
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
black = "^23.1.0"
|
black = "^23.1.0"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user