-
Notifications
You must be signed in to change notification settings - Fork 3
Writing Port Drivers
- A Very Simple Port
- Port Initialization Parameters
- Making Port Read-only
- An Additional Attribute
- Another Additional Attribute
- Attribute Getters & Setters
- Port Tweaks
- Further Reading
Port drivers are Python classes that inherit qtoggleserver.core.ports.Port
. A very simple driver can be written as follows:
from qtoggleserver.core import ports
class SimplePort(ports.Port):
TYPE = ports.TYPE_BOOLEAN
def __init__(self):
super().__init__(port_id='simple_port1')
self._value = None
def read_value(self):
return self._value
def write_value(self, value):
self.debug('writting value %s' % value)
self._value = value
Assuming that you placed your simpleport.py
somewhere in the Python path, add the port in your qtoggleserver.conf
file:
...
ports = [
{
driver = "simpleport.SimplePort"
}
]
...
To get you started with port driver development, you can run qToggleServer with a custom Python path set to your working directory:
$ PYTHONPATH=/path/to/your/simpleport/dir qtoggleserver -c /path/to/qtoggleserver.conf
Now let's add some parameters that will be passed to the port upon initialization. number
allows choosing the port id, while def_value
allows supplying a default initial value.
...
def __init__(self, number, def_value=None):
super().__init__(port_id=f'simple_port{number}')
self._value = def_value
...
...
ports = [
{
driver = "simpleport.SimplePort"
number = 1
def_value = true
}
]
...
Suppose now that we want to make our port read-only. This basically boils down to setting the common port attribute writable
to false
. This can be done by simply hardcoding the value to a class attribute having the corresponding uppercase name:
...
class SimplePort(ports.Port):
TYPE = ports.TYPE_BOOLEAN
WRITABLE = False
...
What if we want to allow the user to control the writable flag of our port? We can define an additional port attribute called allow_writing
that controls it:
...
class SimplePort(ports.Port):
TYPE = ports.TYPE_BOOLEAN
ADDITIONAL_ATTRDEFS = {
'allow_writing': {
'display_name': 'Allow Writing',
'description': 'Controls the port writable flag.',
'type': 'boolean',
'modifiable': True
}
}
...
async def attr_is_writable(self):
return await self.get_attr('allow_writing')
...
...
While the ADDITIONAL_ATTRDEFS
part might be quite straight-forward, the new attr_is_writable
method surely needs some explanations. First off, all attribute getters and setters are expected to be async
; they also need to start with attr_
. Boolean getters are prefixed with is_
, while numeric and string getters would have a get_
prefix. The name of the attribute, in lowercase, comes in last (in this case, writable
). The attr_is_writable
method simply says that the standard writable
attribute will have the same value as the user-defined allow_writing
attribute.
Let's now define a second additional attribute, of type string
and with some choices. Supposing that our port comes in three models called Model A
, Model B
and Model C
, we'll call our attribute simply model
.
...
class SimplePort(ports.Port):
ADDITIONAL_ATTRDEFS = {
...
'model': {
'display_name': 'Model',
'description': 'Choose the model of your port.',
'type': 'string',
'modifiable': True,
'choices': [
{'value': 'a', 'display_name': 'Model A'},
{'value': 'b', 'display_name': 'Model B'},
{'value': 'c', 'display_name': 'Model C'}
]
}
}
...
async def attr_get_model(self):
# Determine your attribute value here
self.debug('returning hardcoded model "b"')
return 'b'
async def attr_set_model(self, value):
# Do what you need with your new attribute value
self.debug('setting model to %s', value)
...
...
Defining the getter/setter pair allows us to implement custom logic when reading and writing the attribute value.
Whether we're talking about standard or additional port attributes, the procedure used to obtain their values is the same:
- First, if a port method having the name
attr_get_<name>
(attr_is_<name>
for booleans) exists, it is called and, if it returns a value other thanNone
, the returned value is considered the attribute value. - Then, a port property called
_<name>
is looked up and, if it exists and is notNone
, it's considered the attribute value. - Then, if a port method having the name
attr_get_default_<name>
(attr_is_default_<name>
for booleans) exists, it is called and, if it returns a value other thanNone
, the returned value is considered the attribute value. - Lastly, a port property called is looked up and, if it exists and is not
None
, it's considered the attribute value. - If none of the above provided a value, the port is considered as not having that attribute.
Here's the procedure used to set attribute values:
- First, if a port method having the name
attr_set_<name>
exists, it is called with the new value as parameter. - Otherwise, a port property called
_<name>
is looked up and, if it exists, it is assigned the new value.
Properties in uppercase, following <NAME>
pattern are never changed, being considered constants and often used to provide default attribute values.
The following port class-level constants affect the functioning of ports:
-
WRITE_VALUE_QUEUE_SIZE
defaults to16
and controls the maximum number of queued values to be written to the port. Set it to1
if you don't want value write queuing.
Once you know how to write a basic port driver, you might be interested in writing complex ports for peripherals.
Packaging your port driver into an add-on might also be something of interest.