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)
port: The serial device to use to communicate with the sensor.baudrate: Per default 19200. This is the sensor's default baud rate after powering on.timeout: The serial timeout in seconds, after the library assumes the sensor has not responded. Do not change.plugins: A list of plugins to be added to the SerialSensor instance. See chapter "Plugins".logger: You can specify alogging.Loggerinstance to be used bySerialSensor. See chapter "Logging".power_on_grace_time: Delay after sensor has been switched See chapter "Software controlled power supply"power_off_grace_time: Delay before sensor will bee switched off See chapter "Software controlled power supply"
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)protocoldriverserializer
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
Sensorclass working as facade and central entry point for every interaction with the sensor - A
Protocolclass, flanked by theSerializerclass, which takes care of data transformation from and to the sensor - A
Driverclass 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.driverDriverErrorGeneric base class indicating errors on the low-level communication layer
ondosense.connect.generic.protocolProtocolErrorGeneric base class indicating errors on the low-level protocol layer
ondosense.connect.generic.sensorSensorErrorGeneric base class indicating usage errors
ondosense.connect.generic.serializerSerializationErrorGeneric base class indicating errors during (de)serialization of data
The RS485/Serial implementation derives some more fine-grained exceptions:
ondosense.connect.rs485.driverSerialErrorGeneric base class for errors in the serial communication layerResponseTooShortErrorNoResponseError
ondosense.connect.rs485.protocolSerialProtocolErrorGeneric base class for errors on the protocol layerParameterErrorBase class indicating an error when dealing with parametersParameterWriteErrorParameterReadErrorExecuteCommandError
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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ondosense_connect-0.19.6-py3-none-any.whl.
File metadata
- Download URL: ondosense_connect-0.19.6-py3-none-any.whl
- Upload date:
- Size: 37.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f59c7a770da12c79a60621b705276344c4557d41294f52c1b877c96f98ce6965
|
|
| MD5 |
0a92e96a671da58f21e87d7e235342b9
|
|
| BLAKE2b-256 |
23de4e44d6924b153b891dde9f31905a09b8e80a035693378bea2319317dc5d6
|