Creating Your Own Sensor
This guide walks you through creating a custom modbus implementation for AtmosPyre.
What You'll Build
A interface to a modbus backend you found interesting.
Prerequisites
- Understanding of Sensor API and Modbus Backend
- Python 3.10+
Implement the Following Interfaces
Backend template for implementing custom Modbus communication.
This template provides the complete interface for implementing a new Modbus backend. Copy this file and implement all TODO sections with your backend library.
All functions use multipledispatch with a custom backend tag for type-based routing.
Classes
CustomBackendTag
Bases: ModbusBackendTag
Backend tag for your custom Modbus library.
This tag is used by multipledispatch to route operations to your backend implementation. Create one instance and pass it to sensors.
Source code in atmospyre/sensors/backends/_template/backend_template.py
Functions
_get_byteorder(byteorder_enum, backend_tag)
Map generic Byteorder enum to backend byte order constant.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
byteorder_enum
|
Byteorder
|
Generic byte order enumeration (ABCD, BADC, CDAB, DCBA) |
required |
backend_tag
|
CustomBackendTag
|
Backend tag for dispatch |
required |
Returns:
| Type | Description |
|---|---|
Any
|
Your backend's byte order constant |
Notes
Map these values: - Byteorder.ABCD: Big-endian (most significant byte first) - Byteorder.BADC: Big-endian with byte swap - Byteorder.CDAB: Little-endian with byte swap - Byteorder.DCBA: Little-endian (least significant byte first)
Source code in atmospyre/sensors/backends/_template/backend_template.py
_get_modbus_mode(modbus_mode_enum, backend_tag)
Map generic ModbusMode enum to backend mode constant.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
modbus_mode_enum
|
ModbusMode
|
Generic Modbus mode (RTU or ASCII) |
required |
backend_tag
|
CustomBackendTag
|
Backend tag for dispatch |
required |
Returns:
| Type | Description |
|---|---|
Any
|
Your backend's Modbus mode constant |
Notes
Map these values: - ModbusMode.RTU: Binary Modbus RTU mode - ModbusMode.ASCII: ASCII-encoded Modbus ASCII mode
Source code in atmospyre/sensors/backends/_template/backend_template.py
_get_parity(parity_enum, backend_tag)
Map generic Parity enum to backend parity constant.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
parity_enum
|
Parity
|
Generic parity mode (NONE, EVEN, ODD, MARK, SPACE) |
required |
backend_tag
|
CustomBackendTag
|
Backend tag for dispatch |
required |
Returns:
| Type | Description |
|---|---|
Any
|
Your backend's parity constant |
Source code in atmospyre/sensors/backends/_template/backend_template.py
_get_stopbits(stopbits_enum, backend_tag)
Map generic Stopbits enum to backend stop bits constant.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
stopbits_enum
|
Stopbits
|
Generic stop bits (ONE, ONE_POINT_FIVE, TWO) |
required |
backend_tag
|
CustomBackendTag
|
Backend tag for dispatch |
required |
Returns:
| Type | Description |
|---|---|
Any
|
Your backend's stop bits constant |
Source code in atmospyre/sensors/backends/_template/backend_template.py
_create_instrument(port, slave_address, backend_tag)
Create and return your backend's Modbus client instance.
Called before each read/write operation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
port
|
str
|
Serial port name (e.g., '/dev/ttyACM0', 'COM3') |
required |
slave_address
|
int
|
Modbus slave address (1-247) |
required |
backend_tag
|
CustomBackendTag
|
Backend tag for dispatch |
required |
Returns:
| Type | Description |
|---|---|
Any
|
Your backend's unconfigured client/instrument instance |
Notes
Return a fresh instance - do not configure it here. Configuration happens in _setup_instrument.
Source code in atmospyre/sensors/backends/_template/backend_template.py
_setup_instrument(instrument, baudrate, stopbits, bytesize, parity, modbus_mode, timeout, backend_tag)
Configure instrument with serial communication parameters.
Called immediately after _create_instrument.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instrument
|
Any
|
Your backend's client instance (from _create_instrument) |
required |
baudrate
|
int
|
Serial baudrate (e.g., 9600, 19200, 115200) |
required |
stopbits
|
Stopbits
|
Number of stop bits |
required |
bytesize
|
int
|
Number of data bits (typically 8) |
required |
parity
|
Parity
|
Parity setting |
required |
modbus_mode
|
ModbusMode
|
Modbus mode (RTU or ASCII) |
required |
timeout
|
float
|
Serial timeout in seconds |
required |
backend_tag
|
CustomBackendTag
|
Backend tag for dispatch |
required |
Important
MUST set equivalent of close_port_after_each_call=True to prevent serial port conflicts when multiple sensors share the same port.
Source code in atmospyre/sensors/backends/_template/backend_template.py
_open_port(instrument, backend_tag)
Open the serial port explicitly.
Called by the context manager before read/write operations. For backends with automatic port management, this can be a no-op.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instrument
|
Any
|
Your backend's client instance |
required |
backend_tag
|
CustomBackendTag
|
Backend tag for dispatch |
required |
Raises:
| Type | Description |
|---|---|
IOError
|
If opening the port fails |
Notes
- Check if port exists before opening
- Check if already open before attempting to open
- Log debug messages for troubleshooting
- Raise IOError if opening fails (don't silently fail)
Source code in atmospyre/sensors/backends/_template/backend_template.py
_close_port(instrument, backend_tag)
Close the serial port explicitly.
Called by the context manager after read/write operations. For backends with automatic port management, this can be a no-op.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instrument
|
Any
|
Your backend's client instance |
required |
backend_tag
|
CustomBackendTag
|
Backend tag for dispatch |
required |
Notes
- Check if port exists before closing
- Check if currently open before attempting to close
- Catch and ignore exceptions (cleanup should never fail)
- Log debug messages for troubleshooting
Source code in atmospyre/sensors/backends/_template/backend_template.py
_cleanup_instrument(instrument, backend_tag)
Close instrument and release serial port resources.
Called after each read/write operation in a finally block. Must be defensive and never raise exceptions.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instrument
|
Any
|
Your backend's client instance |
required |
backend_tag
|
CustomBackendTag
|
Backend tag for dispatch |
required |
Notes
- Check if instrument exists before accessing
- Check if port exists and is open before closing
- Catch and ignore all exceptions
- This allows other sensors to use the same port
- This is the final cleanup, called after _close_port
Source code in atmospyre/sensors/backends/_template/backend_template.py
_read_bit(instrument, address, functioncode, backend_tag)
Read one bit from the slave (coil or discrete input).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instrument
|
Any
|
Your backend's client instance |
required |
address
|
int
|
Bit register address (0-based) |
required |
functioncode
|
int
|
Modbus function code (1: read coils, 2: read discrete inputs) |
required |
backend_tag
|
CustomBackendTag
|
Backend tag for dispatch |
required |
Returns:
| Type | Description |
|---|---|
int
|
Bit value (0 or 1) |
Raises:
| Type | Description |
|---|---|
IOError
|
If communication with device fails |
Source code in atmospyre/sensors/backends/_template/backend_template.py
_write_bit(instrument, address, value, functioncode, backend_tag)
Write one bit to the slave (coil).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instrument
|
Any
|
Your backend's client instance |
required |
address
|
int
|
Bit register address (0-based) |
required |
value
|
int
|
Value to write (0 or 1) |
required |
functioncode
|
int
|
Modbus function code (5: write single coil, 15: write multiple) |
required |
backend_tag
|
CustomBackendTag
|
Backend tag for dispatch |
required |
Raises:
| Type | Description |
|---|---|
IOError
|
If communication with device fails |
Source code in atmospyre/sensors/backends/_template/backend_template.py
_read_bits(instrument, address, number_of_bits, functioncode, backend_tag)
Read multiple bits from the slave (coils or discrete inputs).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instrument
|
Any
|
Your backend's client instance |
required |
address
|
int
|
Starting bit register address (0-based) |
required |
number_of_bits
|
int
|
Number of bits to read |
required |
functioncode
|
int
|
Modbus function code (1: read coils, 2: read discrete inputs) |
required |
backend_tag
|
CustomBackendTag
|
Backend tag for dispatch |
required |
Returns:
| Type | Description |
|---|---|
List[int]
|
List of bit values (0 or 1) |
Raises:
| Type | Description |
|---|---|
IOError
|
If communication with device fails |
Source code in atmospyre/sensors/backends/_template/backend_template.py
_write_bits(instrument, address, values, backend_tag)
Write multiple bits to the slave (coils).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instrument
|
Any
|
Your backend's client instance |
required |
address
|
int
|
Starting bit register address (0-based) |
required |
values
|
List[int]
|
List of values to write (0 or 1) |
required |
backend_tag
|
CustomBackendTag
|
Backend tag for dispatch |
required |
Raises:
| Type | Description |
|---|---|
IOError
|
If communication with device fails |
Source code in atmospyre/sensors/backends/_template/backend_template.py
_read_register(instrument, address, number_of_decimals, functioncode, signed, backend_tag)
Read a 16-bit integer from a single register.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instrument
|
Any
|
Your backend's client instance |
required |
address
|
int
|
Register address (0-based) |
required |
number_of_decimals
|
int
|
Decimal scaling (divide by 10^number_of_decimals) |
required |
functioncode
|
int
|
Modbus function code (3: holding, 4: input) |
required |
signed
|
bool
|
Interpret as signed (-32768 to 32767) or unsigned (0 to 65535) |
required |
backend_tag
|
CustomBackendTag
|
Backend tag for dispatch |
required |
Returns:
| Type | Description |
|---|---|
int or float
|
Register value (int if number_of_decimals=0, else float) |
Raises:
| Type | Description |
|---|---|
IOError
|
If communication with device fails |
Notes
If number_of_decimals > 0, divide raw value by 10^number_of_decimals. Example: raw=235, decimals=1 → returns 23.5
Source code in atmospyre/sensors/backends/_template/backend_template.py
_write_register(instrument, address, value, number_of_decimals, functioncode, signed, backend_tag)
Write a 16-bit integer to a single register.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instrument
|
Any
|
Your backend's client instance |
required |
address
|
int
|
Register address (0-based) |
required |
value
|
int or float
|
Value to write |
required |
number_of_decimals
|
int
|
Decimal scaling (multiply by 10^number_of_decimals before writing) |
required |
functioncode
|
int
|
Modbus function code (6: single, 16: multiple) |
required |
signed
|
bool
|
Interpret as signed or unsigned |
required |
backend_tag
|
CustomBackendTag
|
Backend tag for dispatch |
required |
Raises:
| Type | Description |
|---|---|
IOError
|
If communication with device fails |
Notes
If number_of_decimals > 0, multiply value by 10^number_of_decimals. Example: value=23.5, decimals=1 → writes 235
Source code in atmospyre/sensors/backends/_template/backend_template.py
_read_registers(instrument, address, number_of_registers, functioncode, backend_tag)
Read multiple 16-bit integers from consecutive registers.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instrument
|
Any
|
Your backend's client instance |
required |
address
|
int
|
Starting register address (0-based) |
required |
number_of_registers
|
int
|
Number of registers to read (max ~125) |
required |
functioncode
|
int
|
Modbus function code (3: holding, 4: input) |
required |
backend_tag
|
CustomBackendTag
|
Backend tag for dispatch |
required |
Returns:
| Type | Description |
|---|---|
List[int]
|
List of register values (unsigned 16-bit: 0-65535) |
Raises:
| Type | Description |
|---|---|
IOError
|
If communication with device fails |
Source code in atmospyre/sensors/backends/_template/backend_template.py
_write_registers(instrument, address, values, backend_tag)
Write multiple 16-bit integers to consecutive registers.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instrument
|
Any
|
Your backend's client instance |
required |
address
|
int
|
Starting register address (0-based) |
required |
values
|
List[int]
|
List of values to write (max ~123) |
required |
backend_tag
|
CustomBackendTag
|
Backend tag for dispatch |
required |
Raises:
| Type | Description |
|---|---|
IOError
|
If communication with device fails |
Notes
Typically uses function code 16 (write multiple registers).
Source code in atmospyre/sensors/backends/_template/backend_template.py
_read_long(instrument, address, functioncode, signed, byteorder, number_of_registers, backend_tag)
Read a 32-bit or 64-bit integer from consecutive registers.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instrument
|
Any
|
Your backend's client instance |
required |
address
|
int
|
Starting register address (0-based) |
required |
functioncode
|
int
|
Modbus function code (3: holding, 4: input) |
required |
signed
|
bool
|
Interpret as signed or unsigned |
required |
byteorder
|
Byteorder
|
Byte order (use _get_byteorder to convert) |
required |
number_of_registers
|
int
|
Number of registers (2: 32-bit, 4: 64-bit) |
required |
backend_tag
|
CustomBackendTag
|
Backend tag for dispatch |
required |
Returns:
| Type | Description |
|---|---|
int
|
32-bit or 64-bit integer value |
Raises:
| Type | Description |
|---|---|
IOError
|
If communication with device fails |
Notes
Use _get_byteorder to convert Byteorder enum to backend constant.
Source code in atmospyre/sensors/backends/_template/backend_template.py
_write_long(instrument, address, value, signed, byteorder, number_of_registers, backend_tag)
Write a 32-bit or 64-bit integer to consecutive registers.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instrument
|
Any
|
Your backend's client instance |
required |
address
|
int
|
Starting register address (0-based) |
required |
value
|
int
|
Value to write |
required |
signed
|
bool
|
Interpret as signed or unsigned |
required |
byteorder
|
Byteorder
|
Byte order (use _get_byteorder to convert) |
required |
number_of_registers
|
int
|
Number of registers (2: 32-bit, 4: 64-bit) |
required |
backend_tag
|
CustomBackendTag
|
Backend tag for dispatch |
required |
Raises:
| Type | Description |
|---|---|
IOError
|
If communication with device fails |
Source code in atmospyre/sensors/backends/_template/backend_template.py
_read_float(instrument, address, functioncode, number_of_registers, byteorder, backend_tag)
Read a floating point number from consecutive registers.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instrument
|
Any
|
Your backend's client instance |
required |
address
|
int
|
Starting register address (0-based) |
required |
functioncode
|
int
|
Modbus function code (3: holding, 4: input) |
required |
number_of_registers
|
int
|
Number of registers (2: 32-bit float, 4: 64-bit double) |
required |
byteorder
|
Byteorder
|
Byte order (use _get_byteorder to convert) |
required |
backend_tag
|
CustomBackendTag
|
Backend tag for dispatch |
required |
Returns:
| Type | Description |
|---|---|
float
|
Floating point value |
Raises:
| Type | Description |
|---|---|
IOError
|
If communication with device fails |
Notes
Use _get_byteorder to convert Byteorder enum to backend constant. 32-bit (single precision): number_of_registers=2 64-bit (double precision): number_of_registers=4
Source code in atmospyre/sensors/backends/_template/backend_template.py
_write_float(instrument, address, value, number_of_registers, byteorder, backend_tag)
Write a floating point number to consecutive registers.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instrument
|
Any
|
Your backend's client instance |
required |
address
|
int
|
Starting register address (0-based) |
required |
value
|
float or int
|
Value to write |
required |
number_of_registers
|
int
|
Number of registers (2: 32-bit float, 4: 64-bit double) |
required |
byteorder
|
Byteorder
|
Byte order (use _get_byteorder to convert) |
required |
backend_tag
|
CustomBackendTag
|
Backend tag for dispatch |
required |
Raises:
| Type | Description |
|---|---|
IOError
|
If communication with device fails |
Source code in atmospyre/sensors/backends/_template/backend_template.py
_read_string(instrument, address, number_of_registers, functioncode, backend_tag)
Read an ASCII string from consecutive registers.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instrument
|
Any
|
Your backend's client instance |
required |
address
|
int
|
Starting register address (0-based) |
required |
number_of_registers
|
int
|
Number of registers to read |
required |
functioncode
|
int
|
Modbus function code (3: holding, 4: input) |
required |
backend_tag
|
CustomBackendTag
|
Backend tag for dispatch |
required |
Returns:
| Type | Description |
|---|---|
str
|
ASCII string |
Raises:
| Type | Description |
|---|---|
IOError
|
If communication with device fails |
Notes
Each 16-bit register holds 2 ASCII characters. To read N characters, use number_of_registers = ceil(N/2).
Source code in atmospyre/sensors/backends/_template/backend_template.py
_write_string(instrument, address, textstring, number_of_registers, backend_tag)
Write an ASCII string to consecutive registers.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instrument
|
Any
|
Your backend's client instance |
required |
address
|
int
|
Starting register address (0-based) |
required |
textstring
|
str
|
ASCII string to write (max 2*number_of_registers characters) |
required |
number_of_registers
|
int
|
Number of registers allocated for the string |
required |
backend_tag
|
CustomBackendTag
|
Backend tag for dispatch |
required |
Raises:
| Type | Description |
|---|---|
IOError
|
If communication with device fails |
ValueError
|
If textstring is too long for allocated registers |
Notes
Each register holds 2 characters. Shorter strings are typically padded with spaces.