Skip to content

Object Proxy

API Reference

ObjectProxy is a procedural client meant to consume a Thing instance where the interactions with a property, action or event can be abstracted as operations like:

  • Read/Write/Observe Property
  • Invoke Action
  • Subscribe/Unsubscribe Event

One would require a Thing Description to construct the client. The ThingDescription contains the metadata of the Thing like available properties, actions and events, their data types, their protocols and endpoints (called forms), among other metadata, in JSON format.

Info

See some online hosted examples at the examples website - Camera | Spectrometer | Oscilloscope

The purpose of this JSON metadata is to provide both a human and machine readable description of the Thing and its capabilities, so that clients can automatically discover and interact with it without prior knowledge. In hololinked, this JSON document is automatically generated and served by the server protocols. There is lesser requirement for manually creating said ThingDescription, unless one wants to highly customize it.

To instantiate an ObjectProxy, use the ClientFactory for one protocol at a time:

Note

Only one protocol is allowed per ObjectProxy client. You can always create multiple clients if you need multiple protocols.

HTTP Client
1
2
3
from hololinked.client import ClientFactory

thing = ClientFactory.http(url="http://localhost:8000/my-thing/resources/wot-td")
For HTTP, one needs to append /resources/wot-td to the URL to load an automatically generated Thing Description from the HTTP server serving the Thing.

For TCP:

ZMQ Client TCP
1
2
3
4
5
6
7
from hololinked.client import ClientFactory

thing = ClientFactory.zmq(
    server_id="test-server",
    thing_id="my-thing",
    access_point="tcp://localhost:5555"
)

For IPC:

ZMQ Client IPC
1
2
3
4
5
thing = ClientFactory.zmq(
    server_id="test-server",
    thing_id="my-thing",
    access_point="IPC"
)

For ZMQ, one needs to specify the server_id, thing_id and the access_point (say, TCP or IPC) where the server is accessible. These values are customizable while instantiating an instance of the ZMQServer. If the run() method on the Thing instance was used, the server_id defaults to thing_id.

When using ZMQ-TCP, on the server side one may specify the address as access_point="tcp://*:5555" to bind on all interfaces. On the client side, however, one must use the explicit address containing the machine hostname, like access_point="tcp://my-raspberry-pi:5555" or access_point="tcp://localhost:5555".

The Thing Description is fetched automatically from the server while mediating the connection.

MQTT Consumer
1
2
3
4
5
6
7
8
from hololinked.client import ClientFactory

thing = ClientFactory.mqtt(
    hostname="mqtt://my-mqtt-broker.com",
    thing_id="my-thing",
    username=os.getenv("USERNAME"),
    password=os.getenv("PASSWORD")
)

The Thing Description is published to the MQTT Broker under the topic <thing_id>/thing-description by the server, and the ClientFactory subsribes to the Thing Description and constructs the ObjectProxy.

MQTT currently supports only events and properties that publish change events.

read and write properties

To read and write properties by name, one can use read_property and write_property, or the dot operator:

read and write property
# create client
spectrometer = ClientFactory.http(
    url="http://localhost:8000/spectrometer/resources/wot-td"
)
# setting property by name
spectrometer.write_property("serial_number", "USB2+H15897")
# setting property with dot operator leads to the same effect
spectrometer.serial_number = "USB2+H15897"

# similar API for reading property
print(spectrometer.serial_number)  # prints 'USB2+H15897'
print(spectrometer.read_property("serial_number"))  # prints 'USB2+H15897'

To read and write multiple properties:

read and write multiple properties
# read and write multiple properties
print(
    spectrometer.read_multiple_properties(
        names=["integration_time", "trigger_mode"]
    )
)
spectrometer.write_multiple_properties(
    integration_time=100, nonlinearity_correction=False
)  # pass properties as keyword arguments
print(
    spectrometer.read_multiple_properties(
        names=[
            "state",
            "nonlinearity_correction",
            "integration_time",
            "trigger_mode",
        ]
    )
)

invoke actions

One can also access actions with dot operator and supply positional and keyword arguments:

invoke actions with dot operator
# normal action call
# with keyword arguments
spectrometer.connect(trigger_mode=2, integration_time=1000)
spectrometer.disconnect()
# with positional arguments
spectrometer.connect(2, 1000)
spectrometer.disconnect()
# with both positional and keyword arguments
spectrometer.connect(2, integration_time=1000)
spectrometer.disconnect()

One can also use invoke_action to invoke an action by name

invoke_action()
# using invoke_action
spectrometer.connect()
spectrometer.invoke_action("disconnect")
# keyword arguments
spectrometer.invoke_action("connect", trigger_mode=2, integration_time=1000)
spectrometer.invoke_action("disconnect")
# positional arguments
spectrometer.invoke_action("connect", 2, 1000)
spectrometer.invoke_action("disconnect")
# with both positional and keyword arguments
spectrometer.invoke_action("connect", 2, integration_time=1000)
spectrometer.invoke_action("disconnect")

oneway scheduling

oneway scheduling do not fetch return value and exceptions that might occur while executing a property or an action. The server schedules the operation and returns an empty response to the client, allowing it to process further logic. It is possible to set a property, set multiple or all properties or invoke an action in oneway. Other operations are not supported.

oneway=True
# oneway action call
spectrometer.invoke_action(
    "connect", trigger_mode=2, integration_time=1000, oneway=True
)
spectrometer.invoke_action("disconnect", oneway=True)
# oneway action with positional arguments
spectrometer.invoke_action("connect", 2, 1000, oneway=True)
# write multiple properties one way
spectrometer.write_multiple_properties(
    integration_time=100, nonlinearity_correction=False, oneway=True
)
# read multiple properties one way not supported
# as return value is mandatory
print(
    spectrometer.read_multiple_properties(
        names=[
            "state",
            "nonlinearity_correction",
            "integration_time",
            "trigger_mode",
        ]
    )
)
# write property one way
spectrometer.write_property("integration_time", 100, oneway=True)

Simply provide the keyword argument oneway=True to the operation method.

oneway must be always specified as a keyword argument. Due to this reason, one cannot have an action argument or a property on the server named oneway as it is a reserved keyword argument to such methods on the client. At least they become inaccessible on the ObjectProxy.

no-block scheduling

noblock allows scheduling a property or action and collecting the reply later:

noblock=True
# no block calls
spectrometer1_proxy = ClientFactory.zmq(
    server_id="server1",
    thing_id="spectrometer1",
    access_point="tcp://mypc1:8000",
)
spectrometer2_proxy = ClientFactory.http(
    url="http://mypc2:8000/spectrometer2/resources/wot-td"
)

reply_ids = []

for client in [spectrometer1_proxy, spectrometer2_proxy]:
    reply_id = client.write_property(
        "background_correction", "AUTO", noblock=True
    )
    reply_ids.append(reply_id)

# no return value expected, exceptions will be raised on read_reply and will not be None
assert all(client.read_reply(reply_id) is None for reply_id in reply_ids)
reply_ids = []

for client in [spectrometer1_proxy, spectrometer2_proxy]:
    reply_id = client.invoke_action(
        "start_acquisition",
        trigger_mode=2,
        integration_time=1000,
        noblock=True,
    )
    reply_ids.append(reply_id)

# no return value expected, exceptions will be raised on read_reply and will not be None
assert all(client.read_reply(reply_id) is None for reply_id in reply_ids)

When using read_reply(), noblock calls raise exception on the client if the server raised its own exception or fetch the return value .

Note

One cannot combine oneway and noblock - oneway takes precedence over noblock.

async client-side scheduling

All operations on the ObjectProxy can also be invoked in an asynchronous manner within an async function. Simply prefix async_ to the method name, like async_read_property, async_write_property, async_invoke_action etc.:

asyncio
import asyncio

spectrometer1 = ObjectProxy.zmq(
    server_id="spectrometer", thing_id="spectrometer1", access_point="IPC"
)
spectrometer2 = ObjectProxy.http(
    url="http://localhost:8000/spectrometer2/resources/wot-td"
)


async def setup_spectrometer(spectrometer: ObjectProxy, serial_number: str):
    # write a property
    await spectrometer.async_write_property("serial_number", serial_number)
    # invoke action
    await spectrometer.async_invoke_action(
        "connect", trigger_mode=2, integration_time=1000
    )
    # write multiple properties
    await spectrometer.async_write_multiple_properties(
        background_correction="AUTO", nonlinearity_correction=False
    )
    await spectrometer.async_invoke_action("start_acquisition")


asyncio.run(
    asyncio.gather(
        setup_spectrometer(spectrometer1, "USB2+H15897"),
        setup_spectrometer(spectrometer2, "USB2+H15898"),
    )
)

There is no support for dot operator based access for asyncio. One may also note that async operations
do not change the nature of the execution on the server side. asyncio on ObjectProxy is purely a client-side non-blocking network call, so that one can simultaneously perform other async operations while the client is waiting for said network operation to complete.

Note

oneway and noblock are not supported for async calls due to the asynchronous nature of the operation themselves.

subscribe and unsubscribe events

To subscribe to an event, use subscribe_event method and pass a callback function that accepts a single argument, the event data:

subscribe_event()
1
2
3
4
5
6
7
8
9
from hololinked.client.abstractions import SSE

def update_plot(event: SSE):
    plt.clf()  # Clear the current figure
    plt.plot(x_axis, event.data["spectrum"], color='red', linewidth=2)
    plt.title(f'Live Spectrum - {event.data["timestamp"]} UTC')
    # assuming event data is a dictionary with keys spectrum and timestamp

spectrometer.subscribe_event("intensity_measurement_event", callbacks=update_plot)

It is possible to also use bound methods as callbacks. To unsubscribe from an event, use unsubscribe_event method:

unsubscribe_event()
spectrometer.unsubscribe_event("intensity_measurement_event")

One can also supply multiple callbacks to be executed in series or concurrently, schedule an async callback etc., see events section for further details.

observe and unobserve properties

Observing properties work similar to event subcriptions, provided the property was specified as observable on the server side.

To observe a property:

observe_property()
1
2
3
4
5
6
def update_plot(event: SSE):
    plt.clf()  # Clear the current figure
    plt.plot(x_axis, event.data, color='red', linewidth=2)
    plt.title(f'Live Spectrum - last updated {datetime.now().isoformat()}')

spectrometer.observe_property("last_intensity", callback=update_plot)

To unobserve a property:

unobserve_property()
spectrometer.unobserve_property("last_intensity")

Once again, to customize callback scheduling, see events section for further details.

customizations

foreign attributes on client

Normally, there cannot be user defined attributes on the ObjectProxy as the attributes on the client must mimic the available properties, actions and events on the server. An accidental setting of an unknown property must raise an AttributeError, when not found on the server, instead of silently setting said property on the client itself:

foreign attributes raise AttributeError
1
2
3
4
5
spectrometer = ClientFactory.zmq(
    server_id="test-server", thing_id="my-thing", access_point="IPC"
)
spectrometer.serial_num = 5  # raises AttributeError
# when server does not have property serial_num

The requirement for this behaviour is due to python's duck typing. If one intends to set a property named foo, and instead types it as fooo (misspelt), it is better to raise an error instead of silently setting a new attribute fooo on the client.

One can overcome this by setting allow_foreign_attributes to True:

foreign attributes allowed
1
2
3
4
5
6
7
8
spectrometer_proxy = ClientFactory.zmq(
    server_id="test-server",
    thing_id="my-thing",
    access_point="IPC",
    allow_foreign_attributes=True,
)
spectrometer_proxy.serial_num = 5  # OK!!
# even when the server does not have property serial_num
controlling timeouts for non-responsive server

For invoking any operation (say property read/write & action call), two types of timeouts can be configured:

  • invokation_timeout - the amount of time the server has to wait for an operation to be scheduled
  • execution_timeout - the amount of time the server has to complete the operation once scheduled

When the invokation_timeout expires, the operation is guaranteed to be never executed. 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 specifying the timeout type. If an operation is scheduled but not completed within the execution_timeout, the server may still complete the operation but client does not know about it.

timeout specification
1
2
3
4
5
6
7
8
# wait only for 10 seconds for server to respond per call
spectrometer_proxy = ClientFactory.zmq(
    server_id="test-server",
    thing_id="my-thing",
    access_point="IPC",
    invokation_timeout=10,
    execution_timeout=10,
)

Currently only a global customization of these values are supported. In future, one may be able to specify timeouts per operation.