Skip to main content

Python library to use the OndoSense RS485 sensor family in your Python code.

Project description

ondosense-connect

ondosense-connect provides an easy and simple way to use the OndoSense RS485 RADAR sensor family in Python.

TLDR; Quick Start

Note: For the best Developer Experience, please install the ondosense-connect-api package, which provides classes for all Commands and Parameters you can use with the RS485 sensor family.

The following examples cover some common use cases like

  • (1) taking a simple distance measurement,
  • (2) configuring the sensor and persisting the configuration,
  • (3) saving the data of a burst measurement,
  • (4) selecting additional result data selectors
  • (5) and resetting the sensor to factory settings.

(1) Take a single measurement

This is the most basic use case, assuming you have a sensor with factory settings:

from ondosense.connect.rs485.sensor import SerialSensor
import ondosense.connect.parameters as p

# Adapt the serial device name according to your setup.
port = 'COM6'

with (SerialSensor(port) as sensor):
    # Let the sensor find the best amplifier gain values for the current target
    # Please make sure, your sensor is properly aligned to the target you want to measure.
    sensor.autoset_amplifier_gains()

    # Get one measurement
    measurement = sensor.get_measurement()

    # Print the measured distance in "m"
    print(f"Distance      : {measurement.distance.distance_in_m} m")

If you run this code you should get an output like that:

Protocol startup
Driver connected
1.701446
Remaining bytes in read buffer: 0
26 bytes written to sensor.
10 bytes read from sensor.
Driver disconnected
Protocol shutdown

(2) Configuring the sensor and persisting the configuration

Int his example, we'll configure the sensor and make these changes persistent so the next time the sensor starts up, you don't need to configure it again.

Please note: The baud rate won't be persisted when saving parameters. The sensor will always start up with the default baud rate of 19200 baud.

from ondosense.connect.rs485.sensor import SerialSensor
import ondosense.connect.parameters as p

# Adapt the serial device name according to your setup.
port = 'COM6'

with (SerialSensor(port) as sensor):
    (
        sensor
        # Increase baudrate to speed up the communication with the sensor
        .set_baudrate(921600, True)
        # Set the minimum distance you expect your target in
        .set_parameter(p.MinDistanceInMm(500))
        # Set the maximum distance you expect your target in
        .set_parameter(p.MaxDistanceInMm(5000))
        # Let the sensor find the best amplifier gain values for the current target
        # Please make sure, your sensor is properly aligned to the target you want to measure. 
        .autoset_amplifier_gains()
        # Persist the configuration changes
        .save_parameters()
    )

(3) Saving the measurement data of a burst measurement into a JSON file

In this example, we will configure the sensor, perform a burst measurement and then save the measurement data into a JSON file.

from ondosense.connect.rs485.sensor import SerialSensor
import ondosense.connect.parameters as p

# Adapt the serial device name according to your setup.
port = 'COM6'

with (SerialSensor(port) as sensor):
    (
        sensor
        # Increase baudrate to speed up the communication with the sensor
        .set_baudrate(921600, True)
        # Set the minimum distance you expect your target in
        .set_parameter(p.MinDistanceInMm(500))
        # Set the maximum distance you expect your target in
        .set_parameter(p.MaxDistanceInMm(5000))
        # Let the sensor find the best amplifier gain values for the current target
        # Please make sure, your sensor is properly aligned to the target you want to measure. 
        .autoset_amplifier_gains()
    )

    # Get a burst of 100 measurements
    measurements = sensor.get_burst_measurement(100)

    with open('result_data.json', 'w', encoding='utf8') as file:
        file.write(measurements.to_json())

SerialSensor.get_measurement() and SerialSensor.get_burst_measurement() return a Measurement respectively a MeasurementIterator object which both provide a .to_json() method.

The .to_json() method returns one or multiple measurements as JSON string which can easily be saved into a file.

Note: .to_json() accepts the indent argument. Per default, .to_json() will return a compact JSON string. To make its output human-readable, you can specify a value greater than zero, e.g. measurement.to_json(indent = 2).

(4) Selecting additional Result Data Selectors

from ondosense.connect.rs485.sensor import SerialSensor
import ondosense.connect.parameters as p
import ondosense.connect.result_data_selectors as rds

# Adapt the serial device name according to your setup.
port = 'COM6'

with (SerialSensor(port) as sensor):
    # Configure the sensor
    (
        sensor
        # Increase baudrate to speed up the communication with the sensor
        .set_baudrate(921600, True)
        # Set the minimum distance you expect your target in
        .set_parameter(p.MinDistanceInMm(500))
        # Set the maximum distance you expect your target in
        .set_parameter(p.MaxDistanceInMm(5000))

        # Set desired measurement type
        .set_result_data_selector(
            (
                rds.DistanceSelector()
                + rds.PeakSelector()
            ).value
        )

        .autoset_amplifier_gains()  # Let the sensor find the best amplifier gain values for the current target
    )

    measurement = sensor.get_measurement()

    print(f"Distance      : {measurement.distance.distance_in_m} m")
    print(f"Peak Amplitude: {measurement.peak.amplitude_in_ou}")

(5) resetting the Sensor to Factory Settings

from ondosense.connect.rs485.sensor import SerialSensor

# Adapt the serial device name according to your setup.
port = 'COM6'

with (SerialSensor(port) as sensor):
    sensor.execute_factory_reset()

Note: After a factory reset, all parameter values are reset to their factory default. The sensor's baud rate is reset to 19200.

Advanced library features

SerialSensor initializer

SerialSensor's initializer accepts at least the serial device name (argument port) and several optional arguments:

SerialSensor(port: str, baudrate: int = 19200, timeout: float = 1, plugins: list | None = None,
    logger: logging.Logger | None = None, power_on_grace_time: float = 0.0,
    power_off_grace_time: float = 0.0)

Context Manager vs. manual serial connection control

In the examples above, we used Python's context manager to create and handle the SerialSensor instance. This is the recommended usage, as it ensures the serial connection is properly established and released.

from ondosense.connect.rs485.sensor import SerialSensor
from ondosense.connect.parameters import *
from ondosense.connect.result_data_selectors import *

my_serial_device = 'COM6'

with (SerialSensor(my_serial_device) as sensor):
    # Configure the sensor
    (
        sensor
        .set_baudrate(921600, pray_it_works=True) # Increase communication baud rate 
        .set_parameter(MinDistanceInMm(500))  # Set minimum distance range
        .set_parameter(MaxDistanceInMm(5000))  # Set maximum distance range
        .set_result_data_selector(DistanceSelector().value)  # Set desired measurement type
        .autoset_amplifier_gains()  # Let the sensor find the best amplifier gain values for the current target
    )

    # Get a row measurement
    measurements = sensor.get_burst_measurement(100)

    with open('my_measurements.json', 'w', encoding='utf8') as file:
        file.write(measurements.to_json())

However, the library also allows to manually control the serial connection, by using the SerialSensor.connect() and SerialSensor.disconnect() methods:

from ondosense.connect.rs485.sensor import SerialSensor

sensor = SerialSensor('COM6')
sensor.connect()

measurement = sensor.get_measurement()
print(measurement.distance.distance_in_m)

sensor.disconnect()

While this allows for manual connection establishment and release in cases the use of the context manager is undesirable, it poses the risk of forgetting to properly release the serial connection.

Working with Parameters

Parameters can be read and written. To do so, SerialSensor provides the set_parameter() and get_parameter() methods. Both methods can be used with the named Parameter classes from the ondosense-connect-api package or with numeric values.

Using numeric parameters

from ondosense.connect.rs485.sensor import SerialSensor

with SerialSensor('COM6') as sensor:
    # Parameter "0x44" is "MinDistanceInMm"
    sensor.set_parameter(0x44, 300)  
    print(sensor.get_parameter(0x44))

will print

300

Using Parameter objects

Note: Parameter and Command classes are provided by the ondosense-connect-api package.

from ondosense.connect.parameters import MinDistanceInMm
from ondosense.connect.rs485.sensor import SerialSensor

with SerialSensor('COM6') as sensor:
    sensor.set_parameter(MinDistanceInMm(300))
    print(sensor.get_parameter(MinDistanceInMm))

will print

<class 'ondosense.connect.parameters.MinDistance'>(id=0x44, name=MIN_DISTANCE, status=1, value=300)

Please note, the return value of SerialSensor.get_parameter() depends on the type of its input argument. If it has been a numeric value, the method will return a numeric value. If it has been a Parameter object / class, it will return a Parameter object.

Please also note, that SerialSensor.set_parameter() accepts a Parameter object (class instance) and also returns a Parameter object, while SerialSensor.get_parameter() accepts a Parameter class and returns a Parameter object!

There is a special Parameter class GenericParameter. Its purpose is to enable the developer to use parameter objects without the need to actually know the named Parameter classes:

from ondosense.connect.contracts import GenericParameter
from ondosense.connect.rs485.sensor import SerialSensor

with SerialSensor('COM6') as sensor:
    sensor.set_parameter(GenericParameter(0x44, 500))
    sensor.get_parameter(GenericParameter(0x44))

This generic implementation can later be replaced with the named parameters if needed by a simple search & replace.

Working with Commands

For Command execution SerialSensor.execute_command() must be used.

Similar to Parameters, Commands can be executed by specifying their numeric value or by using a named Command class.

Commands may have a body or not. When using numeric values, please refer to the RS485 API description. When using Command objects, they will provide the structure of the command request and response data.

Using numeric commands

Command without body.

from ondosense.connect.rs485.sensor import SerialSensor

with SerialSensor('COM6') as sensor:
    sensor.execute_command(0x07)  # Set auto amplifier gains

If the Command has a body, the body data must be provided as bytes():

from ondosense.connect.rs485.sensor import SerialSensor

with SerialSensor('COM6') as sensor:
    sensor.execute_command(0xff, (353350403412).to_bytes(5, byteorder="big", signed=False))  # Factory reset

Using Command objects

Note: Parameter and Command classes are provided by the ondosense-connect-api package.

Using Command objects is much easier:

from ondosense.connect.rs485.sensor import SerialSensor
from ondosense.connect.commands import *

with SerialSensor('COM6') as sensor:
    cmd = GetRadarProfiles()
    sensor.execute_command(cmd)

    print(cmd.response_data)

will print

...
{'count': 2, 'profiles': [3, 16]}
... 

A Command object has the properties serializiation_hints and deserialization_hints which can be used to determine the structure of the command request and response body:

from ondosense.connect.commands import GetRadarProfiles
from pprint import pprint

pprint(GetRadarProfiles.deserialization_hint)

prints

{'count': {'data_type': {'C': 'uint32_t',
                         'CS': 'UInt32',
                         '_type': 'data_type',
                         'python': 'int'},
           'length': 4,
           'signed': False},
 'profiles': {'count': 'count',
              'data_type': {'C': 'uint32_t',
                            'CS': 'UInt32',
                            '_type': 'data_type',
                            'python': 'int'},
              'length': 4,
              'signed': False},
 'status': {'data_type': {'C': 'int8_t',
                          'CS': 'SByte',
                          '_type': 'data_type',
                          'python': 'int'},
            'length': 1,
            'signed': True}}

Restoring saved measurements

In section (3) of the Quick Start you learned about saving measurements to a file. Now we have a look at loading these measurements into the MeasurementIterator object so you can use them as if you had a sensor.

import json
from ondosense.connect.rs485.measurement import SerialMeasurementIterator

with open('result_data.json', 'r', encoding='utf8') as file:
    data = json.load(file)
    measurements = SerialMeasurementIterator().hydrate(data)

for meas in measurements:
    print(meas.distance.distance_in_m)

The essential part is using the hydrate() method on the SerialMeasurementIterator. From this step onwards, the SerialMeasurementItzerator will behave exactly like real time sensor measurements.

Logging

SerialSensor's initializer accepts an optional logger argument, which must be a logging.Logger instance.

If the logger argument is not provided, SerialSensor will automatically create an internal logger for outputting status information during runtime. This default logger logs all events above and including logging.INFO and uses logging.StreamHandler(sys.stdout) to output to the system console.

The used Logger can be retrieved using SerialSensor.get_logger().

Internally, SerialSensor creates several Loggers in this hierarchy:

  • root (default: serial_sensor)
    • protocol
      • driver
      • serializer

Whether a logger is explicitly passed or the default logger is used, SerialSensor takes care of creating this logger hierarchy.

Using the default Logger

from ondosense.connect.rs485.sensor import SerialSensor

import logging


# No logger is provided via the initializer
with SerialSensor('COM6') as sensor:
    logger = sensor.get_logger()  # get the logger
    logger.setLevel(logging.DEBUG)  # reconfigure the loglevel

Using an external logger

from ondosense.connect.rs485.sensor import SerialSensor

import sys
import logging

# Creating and configuring a Logger instance
handler = logging.StreamHandler(sys.stdout)
formatter = logging.Formatter('%(asctime)s: %(message)s')
handler.setFormatter(formatter)

logger = logging.getLogger('my_logger')
logger.addHandler(handler)
logger.setLevel(logging.INFO)

# a Logger instance is passed via the logger argument
with SerialSensor('COM6', logger=logger) as sensor:
    print(sensor.get_parameter(0x44))

prints

2025-09-29 11:58:36,243: Protocol startup
2025-09-29 11:58:36,273: Driver connected
100
2025-09-29 11:58:36,294: Remaining bytes in read buffer: 0
2025-09-29 11:58:36,411: 2 bytes written to sensor.
2025-09-29 11:58:36,412: 5 bytes read from sensor.
2025-09-29 11:58:36,412: Driver disconnected
2025-09-29 11:58:36,412: Protocol shutdown

Select log level of logging

SerialSensor's default logger (when none has been passed to the initializer) will log all messages including and above level loggin.INFO. To change this behavior during runtime, you can can do something like that:

from ondosense.connect.rs485.sensor import SerialSensor
import logging

with SerialSensor('COM6') as sensor:
    # from now on, only warnings will be logged
    sensor.get_logger().setLevel(logging.WARNING)

Disabling logging altogether

When logging is not desired at all, you can provide a Logger which uses the NullHandler. This effectively disables logging to the console.

from ondosense.connect.rs485.sensor import SerialSensor

import logging

logger = logging.getLogger('my_logger')
logger.addHandler(logging.NullHandler())

with SerialSensor('COM6', logger=logger) as sensor:
    print(sensor.get_parameter(0x44))

prints

100

Software controlled power supply

SerialSensor supports switching a sensor on and off. To use this feature, you must use the USB/RS485 Connector Board starting from Revision 03.

If you do not use this hardware, you can skip this chapter.

Note: To enable the support of software controlled power supply, you must enable this feature on the PCB with a jumper. Please refer to the USB/RS485 Connector Board documentation for details.

To switching the sensor on and off, you can provide two additional arguments to SerialSensor's initializer: power_on_grace_time and power_off_grace_time:

from ondosense.connect.rs485.sensor import SerialSensor
from ondosense.connect.parameters_intern import *

# This switches the sensor on, waits 5 seconds, reads the MinDistanceInMm parameter
# and then shuts the sensor off and waits another second before continuing.
with SerialSensor('COM6', power_on_grace_time=5.0, power_off_grace_time=1.0) as sensor:
    print(sensor.get_parameter(MinDistanceInMm).value)

print('Sensor has been shut off.')

If power_on_grace_time's value is greater than zero, SerialSensor will automatically try to switch on the sensor and wait the specified time in seconds before trying to communicate with the sensor. This ensures, the sensor has fully started up and is ready to communicate.

If power_off_grace_time's value is greater than zeo, SerialSensor will wait the specified time in seconds before powering off the sensor. This ensures the sensor can finish pending actions before beeing shut down.

Plugins

SerialSensor offers a plugin interface to interact with the sensor communication on serial level.

The abstract base class for these plugins is defined by DriverPlugin. You can find it in the module ondosense.connect.generic.driver.

All plugins must inherit from this class and implement the methods on_write() and on_read().

Furthermore, it is highly recommended to also implement the methods enable() -> Self und disable() -> Self. These can be used to enabled or disable a plugin during runtime.

You can find an example implementation (PrintDriverCommunicationPlugin) in the module ondosense.connect.generic.driver:

from ondosense.connect.generic.driver import DriverPlugin

from typing import Self


class PrintDriverCommunicationPlugin(DriverPlugin):
    def __init__(self, read_prefix: str = "R: ", write_prefix: str = "W: "):
        super().__init__()

        self._enabled: bool = True
        self._read_prefix: str = read_prefix
        self._write_prefix: str = write_prefix

    def enable(self) -> Self:
        self._enabled = True
        return self

    def disable(self) -> Self:
        self._enabled = False
        return self

    def on_read(self, data: bytes) -> bytes:
        if self._enabled:
            hex_string = ' '.join(format(byte, '02X') for byte in data)
            print(f'{self._read_prefix}{hex_string}')

        return data

    def on_write(self, data: bytes) -> bytes:
        if self._enabled:
            hex_string = ' '.join(format(byte, '02X') for byte in data)
            print(f'{self._write_prefix}{hex_string}')

        return data

Working with plugins

Plugins are passed in the plugins argument of SerialSensor's initializer:

from ondosense.connect.rs485.sensor import SerialSensor
from ondosense.connect.parameters import SensorType, ExactSensorType
from ondosense.connect.plugin.driver import PrintDriverCommunicationPlugin

print_plugin = PrintDriverCommunicationPlugin()
driver_plugins = [print_plugin]

with SerialSensor('COM6', plugins=driver_plugins) as sensor:
    sensor_type = sensor.get_parameter(SensorType)
    print(f'Type........: {str(sensor_type)}')

    # Disable the plugin. From now on, the PrintDriverCommunicationPlugin does not print anything anymore.
    print_plugin.disable()

    exact_sensor_type = sensor.get_parameter(ExactSensorType)
    print(f'Exact Type..: {str(sensor_type)}')

prints

Protocol startup
Driver connected
W: 01
W: 52
R: 01
R: 00 00 00 11
Type........: apex
Exact Type..: apex
Remaining bytes in read buffer: 0
4 bytes written to sensor.
10 bytes read from sensor.
Driver disconnected
Protocol shutdown

Available Plugins

PrintDriverCommunicationPlugin

This is the example plugin to demonstrate the implementation of a plugin. For real world use cases, the LoggingDriverCommunicationPlugin should be preferred.

LoggingDriverCommunicationPlugin

This plugin uses a logging.Logger instance to log the sensor's serial communication.

The plugin can be enabled and disabled during runtime, using enable() und disable().

The default log level of LoggingDriverCommunicationPlugin is logging.DEBUG.

for details regarding logging in general, please refer to the official logging component documentation.

Using an external Logger

from ondosense.connect.rs485.sensor import SerialSensor
from ondosense.connect.plugin.driver import LoggingDriverCommunicationPlugin

import logging

logger = logging.getLogger('my_logger')

stream_handler = logging.StreamHandler()
logger.addHandler(stream_handler)
logger.setLevel(logging.INFO)

plugin = LoggingDriverCommunicationPlugin(logger, loglevel=logging.INFO)

with SerialSensor('COM6', plugins=[plugin]) as sensor:
   ...

Using the default logger

from ondosense.connect.rs485.sensor import SerialSensor
from ondosense.connect.plugin.driver import LoggingDriverCommunicationPlugin

import logging

plugin = LoggingDriverCommunicationPlugin()

with SerialSensor('COM4', 19200, plugins=[plugin]) as sensor:
    sensor.get_logger().setLevel(logging.DEBUG)
    ...

Note: This works, because LoggingDriverCommunicationPlugin extends the LoggerAwareDriverPlugin. Plugins extending this base class will receive a logging.Logger instance when passed to SerialSensor as initializer argument.

For developers

High level architecture

There are four components, playing together:

  • A Sensor class working as facade and central entry point for every interaction with the sensor
  • A Protocol class, flanked by the Serializer class, which takes care of data transformation from and to the sensor
  • A Driver class handling the actual communication over the given medium

Currently, ondosense-connect implements the serial/RS485 interface.

Tests

Tests are implemented using pytest. Requirements for the test suite are defined in requirements.txt.

All test cases can be found in the /test directory.

To specify the serial device name to run the tests against, please use the CLI argument --serial-device=<device>.

Test Groups

The test cases are organized in groups, using custom pytest marks. There are tests which require sensor communication. These are marked with the marker com. Other tests do not require sensor communication and are marked with nocom.

Invocation examples:

pytest --m "nocom" # Run all tests marked with "nocom"
pytest --m "com" # Run all tests marked with "com"

Error Handling

The ondosense-connect library will raise different error types depending on cause and underlying component.

All custom error classes extend ondosense.connect.generic.error.OndosenseConnectError. This enabled the use of a try-except matching OndosenseConnectError to detect errors during communication with the sensor.

Warning: There are exceptions to this rule. Internal errors (e.g. invalid (de)serialization-hints) raise ValueError or KeyErrors. However, as these errors are not expected to occur, using these untyped "low level" errors helps during debugging.

Generic base error classes:

  • ondosense.connect.generic.driver
    • DriverError Generic base class indicating errors on the low-level communication layer
  • ondosense.connect.generic.protocol
    • ProtocolError Generic base class indicating errors on the low-level protocol layer
  • ondosense.connect.generic.sensor
    • SensorError Generic base class indicating usage errors
  • ondosense.connect.generic.serializer
    • SerializationError Generic base class indicating errors during (de)serialization of data

The RS485/Serial implementation derives some more fine-grained exceptions:

  • ondosense.connect.rs485.driver
    • SerialError Generic base class for errors in the serial communication layer
    • ResponseTooShortError
    • NoResponseError
  • ondosense.connect.rs485.protocol
    • SerialProtocolError Generic base class for errors on the protocol layer
    • ParameterError Base class indicating an error when dealing with parameters
    • ParameterWriteError
    • ParameterReadError
    • ExecuteCommandError

Event Handling

As the sensor may provide responses (e.g. a Measurement) with dynamic lengths, the deserialization component must be able to control the number of bytes to be read from the serial interface.

To enable the serialization component to do this without (totally) breaking the separation of concerns, the driver component listens to ReadEvent and WriteEvent events which can be emitted by the SerialSerializer. The request and response data is then transported via the events, without the need to inject the SerialDriver instance into the SerialSerializer.

The event handling component lives in the ondosense.eventhandling package. This component provides base implementations for Event classes, an EventSubscriber and ean EventDispatcher.

The concrete implementation can be seen in the ondosense.connect.rs485.driver component.

Known Issues / Limitations

  • This library is not suited for timing critical applications (down to the ms) as it cannot ensure timings at OS and hardware level. However, for general purpose applications it is stable and well tested.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ondosense_connect-0.19.6-py3-none-any.whl (37.4 kB view details)

Uploaded Python 3

File details

Details for the file ondosense_connect-0.19.6-py3-none-any.whl.

File metadata

File hashes

Hashes for ondosense_connect-0.19.6-py3-none-any.whl
Algorithm Hash digest
SHA256 f59c7a770da12c79a60621b705276344c4557d41294f52c1b877c96f98ce6965
MD5 0a92e96a671da58f21e87d7e235342b9
BLAKE2b-256 23de4e44d6924b153b891dde9f31905a09b8e80a035693378bea2319317dc5d6

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page