Skip to content

Commit 812eb69

Browse files
committed
add clients doc
1 parent afa0e64 commit 812eb69

File tree

5 files changed

+375
-141
lines changed

5 files changed

+375
-141
lines changed
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
# Object Proxy
2+
3+
[API Reference](../../api-reference/clients/object-proxy.md)
4+
5+
`Thing` objects can be consumed using an `ObjectProxy` instance per protocol, where the interactions with
6+
a property, action or event are abstracted as operations like:
7+
8+
- Read/Write/Observe Property
9+
- Invoke Action
10+
- Subscribe/Unsubscribe Event
11+
12+
To instantiate an `ObjectProxy`, use the `ClientFactory`:
13+
14+
=== "HTTP"
15+
16+
```py title="HTTP Client" linenums="1" hl_lines="7-10"
17+
from hololinked.client import ClientFactory
18+
19+
thing = ClientFactory.http(url="http://localhost:8000/my-thing/resources/wot-td")
20+
```
21+
22+
One needs to append `/resources/wot-td` to the URL to load a [Thing Description](https://www.w3.org/TR/wot-thing-description11/#introduction-td)
23+
of the `Thing`. The `Thing Description` contains the metadata of the `Thing` like available properties, actions and events,
24+
their data types, forms (protocols and endpoints) etc. which can be used to create the `ObjectProxy`.
25+
26+
=== "ZMQ"
27+
28+
```py title="ZMQ Client" linenums="1" hl_lines="7-10"
29+
from hololinked.client import ClientFactory
30+
31+
thing = ClientFactory.zmq(server_id="test-server", thing_id="my-thing", access_point="tcp://localhost:5555")
32+
```
33+
34+
For `IPC`:
35+
36+
```py title="ZMQ Client IPC" linenums="1" hl_lines="7-10"
37+
thing = ClientFactory.zmq(server_id="test-server", thing_id="my-thing", access_point="IPC")
38+
```
39+
40+
When using TCP, on the server side one may choose the address as `access_point="tcp://*:5555"`.
41+
On the client side, however, one must use the explicit address, like `access_point="tcp://my-pc:5555"` or
42+
`access_point="tcp://localhost:5555"`.
43+
44+
The `Thing Description` is fetched automatically from the server.
45+
46+
47+
!!! Note
48+
49+
Only one protocol is allowed per client.
50+
51+
### read and write properties
52+
53+
To read and write properties by name, one can use `read_property` and `write_property`, or the dot operator:
54+
55+
```py title="read and write property" linenums="1"
56+
--8<-- "docs/beginners-guide/code/object_proxy/sync.py:8:17"
57+
```
58+
59+
To read and write multiple properties:
60+
61+
```py title="read and write multiple properties" linenums="1"
62+
--8<-- "docs/beginners-guide/code/object_proxy/sync.py:48:55"
63+
```
64+
65+
### invoke actions
66+
67+
One can also access actions with dot operator and supply positional and keyword arguments:
68+
69+
```py title="invoke actions with dot operator" linenums="1"
70+
--8<-- "docs/beginners-guide/code/object_proxy/sync.py:21:30"
71+
```
72+
73+
One can also use `invoke_action` to invoke an action by name
74+
75+
```py title="invoke_action()" linenums="1"
76+
--8<-- "docs/beginners-guide/code/object_proxy/sync.py:34:45"
77+
```
78+
79+
### oneway scheduling
80+
81+
`oneway` scheduling do not fetch return value and exceptions that might occur while executing a property or an action.
82+
The server schedules the operation and returns an empty response to the client, allowing it to process further logic.
83+
It is possible to set a property, set multiple or all properties or invoke an action in
84+
oneway. Other operations are not supported.
85+
86+
```py title="oneway=True" linenums="1"
87+
--8<-- "docs/beginners-guide/code/object_proxy/sync.py:58:73"
88+
```
89+
90+
Simply provide the keyword argument `oneway=True` to the operation method.
91+
92+
Importantly, one cannot have an action argument or a property on the server named `oneway` as it is a
93+
reserved keyword argument to such methods on the client. At least they become inaccessible on the `ObjectProxy`.
94+
95+
### no-block scheduling
96+
97+
`noblock` allows scheduling a property or action but collecting the reply later:
98+
99+
```py title="noblock=True" linenums="1"
100+
--8<-- "docs/beginners-guide/code/object_proxy/sync.py:77:101"
101+
```
102+
103+
When using `read_reply()`, `noblock` calls raise exception on the client if the server raised its own exception or fetch the return value .
104+
105+
<!-- We supported this before, but not now, TODO renable -->
106+
<!-- Timeout exceptions are raised when there is no reply within timeout specified.
107+
108+
.. literalinclude:: code/object_proxy/sync.py
109+
:language: python
110+
:linenos:
111+
:lines: 95-98 -->
112+
113+
!!! Note
114+
115+
One cannot combine `oneway` and `noblock` - `oneway` takes precedence over `noblock`.
116+
117+
### async client-side scheduling
118+
119+
All operations on the `ObjectProxy` can also be invoked in an asynchronous manner within an `async` function.
120+
Simply prefix `async_` to the method name, like `async_read_property`, `async_write_property`, `async_invoke_action` etc.:
121+
122+
```py title="asyncio" linenums="1"
123+
--8<-- "docs/beginners-guide/code/object_proxy/async.py:12:28"
124+
```
125+
126+
There is no support for dot operator based access. One may also note that `async` operations
127+
do not change the nature of the execution on the server side.
128+
`asyncio` on `ObjectProxy` is purely a client-side non-blocking network call, so that one can
129+
simultaneously perform other (async) operations while the client is waiting for the network operation to complete.
130+
131+
!!! Note
132+
133+
`oneway` and `noblock` are not supported for async calls due to the asynchronous nature of the
134+
operation themselves.
135+
136+
### subscribe and unsubscribe events
137+
138+
To subscribe to an event, use `subscribe_event` method and pass a callback function that accepts a single argument, the event data:
139+
140+
```py title="subscribe_event()" linenums="1"
141+
from hololinked.client.abstractions import SSE
142+
143+
def update_plot(event: SSE):
144+
plt.clf() # Clear the current figure
145+
plt.plot(x_axis, event.data["spectrum"], color='red', linewidth=2)
146+
plt.title(f'Live Spectrum - {event.data["timestamp"]} UTC')
147+
# assuming event data is a dictionary with keys spectrum and timestamp
148+
149+
spectrometer.subscribe_event("intensity_measurement_event", callbacks=update_plot)
150+
```
151+
152+
To unsubscribe from an event, use `unsubscribe_event` method:
153+
154+
```py title="unsubscribe_event()" linenums="1"
155+
spectrometer.unsubscribe_event("intensity_measurement_event")
156+
```
157+
158+
One can also supply multiple callbacks to be executed in series or concurrently, schedule an async callback etc., see [events section](./events.md#subscription) for further details.
159+
160+
### customizations
161+
162+
##### foreign attributes on client
163+
164+
Normally, there cannot be user defined attributes on the `ObjectProxy` as the attributes on the client
165+
must mimic the available properties, actions and events on the server. An accidental setting of an unknown
166+
property must raise an `AttributeError` when not found on the server, instead of silently going through and setting
167+
said property on the client object itself:
168+
169+
```py title="foreign attributes raise AttributeError" linenums="1"
170+
--8<-- "docs/beginners-guide/code/object_proxy/customizations.py:3:5"
171+
```
172+
173+
One can overcome this by setting `allow_foreign_attributes` to `True`:
174+
175+
```py title="foreign attributes allowed" linenums="1"
176+
--8<-- "docs/beginners-guide/code/object_proxy/customizations.py:7:12"
177+
```
178+
179+
##### controlling timeouts for non-responsive server
180+
181+
For invoking any operation (say property read/write & action call), two types of timeouts can be configured:
182+
183+
- `invokation_timeout` - the amount of time the server has to wait for an operation to be scheduled
184+
- `execution_timeout` - the amount of time the server has to complete the operation once scheduled
185+
186+
When the `invokation_timeout` expires, the operation is guaranteed to be never scheduled. When the `execution_timeout` expires, the operation is scheduled but returns without the expected response. In both cases, a `TimeoutError` is raised on the client side specifying the timeout type. If an operation is scheduled but not completed within the `execution_timeout`, the server may still complete the operation and there can be unknown side effects or client does not know about it.
187+
188+
```py title="timeout specification" linenums="1"
189+
--8<-- "docs/beginners-guide/code/object_proxy/customizations.py:20:27"
190+
```
191+
192+
!!! Note
193+
194+
Currently only a global specification is supported. In future, one may be able to specify timeouts per operation.
195+
196+
197+
<!-- #### change handshake timeout
198+
199+
Before sending the first message to the server, a handshake is always done explicitly to not loose messages on the socket.
200+
This is an artificat of ZMQ (which also does its own handshake). `handshake_timeout` controls how long to look for the server,
201+
in case the server takes a while to boot.
202+
203+
```py title="timeout" linenums="1"
204+
--8<-- "docs/beginners-guide/code/object_proxy/customizations.py:10:11"
205+
```
206+
207+
Default value is 1 minute. A `ConnectionError` is raised if the server cannot be contacted.
208+
209+
One can also delay contacting the server by setting `load_thing` to False. But one has to manually performing the `handshake` later
210+
before loading the server resources:
211+
212+
```py title="timeout" linenums="1"
213+
--8<-- "docs/beginners-guide/code/object_proxy/customizations.py:12:17"
214+
```
215+
216+
If one is completely sure that server is online, one may drop the manual handshake. -->
217+
Lines changed: 42 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,56 @@
11
from hololinked.client import ObjectProxy
22

3-
spectrometer_proxy = ObjectProxy(instance_name='spectrometer',
4-
protocol='IPC', async_mixin=True)
3+
spectrometer = ObjectProxy(server_id="spectrometer", thing_id="spectrometer", access_point="IPC")
54
# synchronous call is always available, not optional
6-
spectrometer_proxy.serial_number = 'USB2+H15897'
7-
spectrometer_proxy.invoke_action('connect', trigger_mode=2, integration_time=1000)
8-
spectrometer_proxy.set_properties(
9-
integration_time=100,
10-
nonlinearity_correction=False
11-
)
12-
spectrometer_proxy.disconnect()
5+
spectrometer.serial_number = "USB2+H15897"
6+
spectrometer.invoke_action("connect", trigger_mode=2, integration_time=1000)
7+
spectrometer.set_properties(integration_time=100, nonlinearity_correction=False)
8+
spectrometer.disconnect()
139

1410

15-
#----------------------------
11+
# ----------------------------
1612
# async calls example
1713
import asyncio
18-
# invoke action
19-
asyncio.run(spectrometer_proxy.async_invoke_action('connect',
20-
trigger_mode=2, integration_time=1000))
21-
# get multiple properties
22-
asyncio.run(spectrometer_proxy.async_write_multiple_properties(
23-
integration_time=100,
24-
nonlinearity_correction=False
25-
))
26-
# set multiple properties
27-
asyncio.run(spectrometer_proxy.async_read_multiple_properties(
28-
names=["integration_time", "trigger_mode"]))
29-
# get single property
30-
asyncio.run(spectrometer_proxy.async_read_property('serial_number'))
31-
# set single property
32-
asyncio.run(spectrometer_proxy.async_write_property('serial_number',
33-
'USB2+H15897'))
34-
35-
#----------------------------
14+
15+
spectrometer = ObjectProxy(server_id="spectrometer", thing_id="spectrometer", access_point="IPC")
16+
17+
18+
async def setup_spectrometer():
19+
# write a property
20+
await spectrometer.async_write_property("serial_number", "USB2+H15897")
21+
# invoke action
22+
await spectrometer.async_invoke_action("connect", trigger_mode=2, integration_time=1000)
23+
# write multiple properties
24+
await spectrometer.async_write_multiple_properties(background_correction="AUTO", nonlinearity_correction=False)
25+
await spectrometer.async_invoke_action("start_acquisition")
26+
27+
28+
asyncio.run(setup_spectrometer())
29+
30+
31+
# ----------------------------
3632
# running multiple clients
37-
async def async_example(spectrometer : ObjectProxy):
33+
async def async_example(spectrometer: ObjectProxy):
3834
await spectrometer.async_invoke_action("start_acquisition")
3935
for i in range(1000):
4036
await spectrometer.async_read_property("last_intensity")
4137
await asyncio.sleep(0.025)
4238
await spectrometer.async_invoke_action("stop_acquisition")
4339

44-
spectrometer1_proxy = ObjectProxy(instance_name='spectrometer1',
45-
protocol='IPC', async_mixin=True)
46-
spectrometer2_proxy = ObjectProxy(instance_name='spectrometer2',
47-
protocol='IPC', async_mixin=True)
48-
spectrometer3_proxy = ObjectProxy(instance_name='spectrometer3',
49-
protocol='IPC', async_mixin=True)
50-
51-
asyncio.get_event_loop().run_until_complete(asyncio.gather(
52-
*[async_example(spectrometer) for spectrometer in [
53-
spectrometer1_proxy, spectrometer2_proxy, spectrometer3_proxy]]
54-
))
40+
41+
spectrometer1_proxy = ClientFactory.zmq(instance_name="spectrometer1", protocol="IPC", async_mixin=True)
42+
spectrometer2_proxy = ClientFactory.zmq(instance_name="spectrometer2", protocol="IPC", async_mixin=True)
43+
spectrometer3_proxy = ClientFactory.zmq(instance_name="spectrometer3", protocol="IPC", async_mixin=True)
44+
45+
asyncio.get_event_loop().run_until_complete(
46+
asyncio.gather(
47+
*[
48+
async_example(spectrometer)
49+
for spectrometer in [
50+
spectrometer1_proxy,
51+
spectrometer2_proxy,
52+
spectrometer3_proxy,
53+
]
54+
]
55+
)
56+
)
Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,27 @@
1-
from hololinked.client import ObjectProxy
1+
from hololinked.client import ClientFactory
22

3-
spectrometer_proxy = ObjectProxy(instance_name='spectrometer')
4-
spectrometer_proxy.serial_num = 5 # raises AttributeError
3+
spectrometer = ClientFactory.zmq(server_id="test-server", thing_id="my-thing", access_point="IPC")
4+
spectrometer.serial_num = 5 # raises AttributeError
55
# when server does not have property serial_num
66

7-
spectrometer_proxy = ObjectProxy(instance_name='spectrometer')
8-
spectrometer_proxy.serial_num = 5 # OK!!
7+
spectrometer_proxy = ClientFactory.zmq(
8+
server_id="test-server", thing_id="my-thing", access_point="IPC", allow_foreign_attributes=True
9+
)
10+
spectrometer_proxy.serial_num = 5 # OK!!
11+
# even when the server does not have property serial_num
912

10-
spectrometer_proxy = ObjectProxy(instance_name='spectrometer',
11-
protocol='IPC', handshake_timeout=10)
12-
spectrometer_proxy = ObjectProxy(instance_name='spectrometer',
13-
protocol='IPC', load_thing=False)
14-
spectrometer_proxy.serial_number = 'USB2+H15897' # raises AttributeError
13+
spectrometer_proxy = ObjectProxy(instance_name="spectrometer", protocol="IPC", handshake_timeout=10)
14+
spectrometer_proxy = ObjectProxy(instance_name="spectrometer", protocol="IPC", load_thing=False)
15+
spectrometer_proxy.serial_number = "USB2+H15897" # raises AttributeError
1516
spectrometer_proxy.zmq_client.handshake()
16-
spectrometer_proxy.load_thing()
17-
spectrometer_proxy.serial_number = 'USB2+H15897' # now OK!!
17+
spectrometer_proxy.load_thing()
18+
spectrometer_proxy.serial_number = "USB2+H15897" # now OK!!
1819

1920
# wait only for 10 seconds for server to respond per call
20-
spectrometer_proxy = ObjectProxy(instance_name='spectrometer',
21-
protocol='IPC', invokation_timeout=10)
22-
21+
spectrometer_proxy = ClientFactory.zmq(
22+
server_id="test-server",
23+
thing_id="my-thing",
24+
access_point="IPC",
25+
invokation_timeout=10,
26+
execution_timeout=10,
27+
)

0 commit comments

Comments
 (0)