Expose Python Classes
Subclass from Thing
Normally, the hardware is interfaced with a computer through Ethernet, USB, an embedded module etc., and one would write a class to encapsulate its properties and commands. Exposing this class to the network or other processes provides access to the hardware for multiple use cases in a client-server model. Such remotely visible Python objects are to be made by subclassing from Thing:
id is a unique name recognising the instantiated object, allowing multiple instances of the same class to have a remote interface. It is therefore a mandatory argument to be supplied to the Thing parent. id should be a URI compatible string; non-experts may use strings composed of characters, numbers, forward slashes etc., which look like a part of a browser URL:
| Thing ID | |
|---|---|
Properties
For attributes (like serial number above), if one requires them to be exposed on the network, one should use "properties" defined in hololinked.core.properties to "type define" the attributes of the object (in a python-idiomatic sense):
Apart from predefined attributes like String, Number, List etc., it is possible to create custom properties with pydantic or JSON schema. One could also use python native types with pydantic. Only properties defined in hololinked.core.properties or subclass of Property object (note the captial 'P') can be exposed to the network, not normal python attributes or python's own property.
Actions
For methods to be exposed on the network, one can use the action decorator:
Properties usually model settings, captured data etc., which have a read-write operation (also read-only or read-write-delete operations) and usually a specific type. Actions are supposed to model activities in the physical world, like executing a control routine, start/stop measurement etc.
Both properties and actions are symmetric - they can be invoked from within the object and externally by a client and expected to behave similarly, except when they are constrained by a state machine.
Actions can take arbitrary signature or the arguments can be constrained again using pydantic or JSON schema.
Serve the Object
To start a server, say a HTTP server, one can call the run_with_http_server method after instantiating the Thing:
| HTTP Server | |
|---|---|
The exposed properties, actions and events (events are discussed below) are independent of protocol implementations, therefore, one can start one or multiple protocols to serve the Thing:
| Multiple Protocols | |
|---|---|
See the protocols section for more options to serve the Thing.
All requests to properties and actions are generally queued as the domain of operation under the hood is remote procedure calls (RPC) mediated completely by ZMQ. Therefore, only one request is executed at a time as it is assumed that the hardware normally responds to only one (physical-)operation at a time.
This is only an assumption to simplify the programming model, given multiple protocols and to avoid unintended race conditions, both logical and in the physical world. One could override them explicitly using threaded or async methods.
It is also expected that the internal state of the python object is not inadvertently affected by running multiple requests at once to different properties or actions. If a single request or operation takes 5-10ms, one can still run 100s of operations per second. More often than not, the requirement of parallel operations is never the bottleneck in hardware control.
Overloaded Properties
To overload the get-set of properties to directly apply property values onto devices, one may supply a custom getter & setter method:
Properties follow the python descriptor protocol. In non expert terms, when a custom get-set method is not provided, properties look like class attributes however their data containers are instantiated at object instance level by default. For example, the serial_number property defined previously as String, whenever set/written, will be complied to a string and assigned as an attribute to each instance of the Thing class. This is done with an internally generated name. It is not necessary to know this internally generated name as the property
value can be accessed again in any python logic using the dot operator, say,
self.device = Spectrometer.from_serial_number(self.serial_number)
However, to avoid generating such an internal data container and instead apply the value on the device, one must supply custom get-set methods. This is generally useful as the hardware is a better source of truth about the value of a property. Further, the write value of a property may not always correspond to a read value due to hardware limitations. Say, the write value of referencing_run_frequency requested by the user is 1050, however, the device adjusted it to 1000 automatically. This is dependent on hardware behaviour.
Publish Events
Events can asynchronously push data to clients. For example, one can supply clients with the measured data using events:
Data may also be polled by the client repeatedly but events save network time or allow sending data which cannot be timed, like alarm messages. Arbitrary payloads are supported, as long as the data is serializable and one can also specify the payload structure using pydantic or JSON schema.
Events follow a PUB-SUB model, through any protocol, despite being broker-mediated like MQTT or brokerless through HTTP server sent events or ZMQ pub-sub. They follow a different channel compared to properties and actions and are not blocked by them. The events are emitted very close to execution to the push(), usually only with microseconds of delay.
To start the capture method defined above which will publish the events, one may thread it as follows to send it to the background: