Skip to content

Writing Port Drivers

Calin Crisan edited this page May 2, 2020 · 13 revisions

A Very Simple Port

Port drivers are Python classes that inherit qtoggleserver.core.ports.Port. A very simple driver can be written as follows:

simpleport.py:
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:

qtoggleserver.conf:
...
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 

Port Initialization Parameters

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.

simpleport.py:
...
    def __init__(self, number, def_value=None):
        super().__init__(port_id=f'simple_port{number}')
        self._value = def_value
...
qtoggleserver.conf:
...
ports = [
    {
        driver = "simpleport.SimplePort"
        number = 1
        def_value = true
    }
]
...

Making Port Read-only

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:

simpleport.py:
...
class SimplePort(ports.Port):
    TYPE = ports.TYPE_BOOLEAN
    WRITABLE = False
...

An Additional Attribute

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:

simpleport.py:
...
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.

Another Additional 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.

simpleport.py:
...
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.

Attribute Getters & Setters

Whether we're talking about standard or additional port attributes, the procedure used to obtain their values is the same:

  1. 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 than None, the returned value is considered the attribute value.
  2. Then, a port property called _<name> is looked up and, if it exists and is not None, it's considered the attribute value.
  3. 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 than None, the returned value is considered the attribute value.
  4. Lastly, a port property called is looked up and, if it exists and is not None, it's considered the attribute value.
  5. 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:

  1. First, if a port method having the name attr_set_<name> exists, it is called with the new value as parameter.
  2. 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.

Port Tweaks

The following port class-level constants affect the functioning of ports:

  • WRITE_VALUE_QUEUE_SIZE defaults to 16 and controls the maximum number of queued values to be written to the port. Set it to 1 if you don't want value write queuing.

Further Reading

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.