Skip to content

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

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
class CustomBackendTag(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.

    Examples
    --------
    >>> from atmospyre.sensors.backends.custom import CustomBackendTag
    >>> backend = CustomBackendTag()
    >>> sensor = GMP252(port='/dev/ttyUSB0', backend_tag=backend)
    """

    pass

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
@dispatch(Byteorder, CustomBackendTag, namespace=modbusbackend_namespace)
def _get_byteorder(byteorder_enum: Byteorder, backend_tag: CustomBackendTag):
    """Map generic Byteorder enum to backend byte order constant.

    Parameters
    ----------
    byteorder_enum : Byteorder
        Generic byte order enumeration (ABCD, BADC, CDAB, DCBA)
    backend_tag : CustomBackendTag
        Backend tag for dispatch

    Returns
    -------
    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)
    """
    # TODO: Implement byte order mapping
    raise NotImplementedError("Implement _get_byteorder for your backend")
_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
@dispatch(ModbusMode, CustomBackendTag, namespace=modbusbackend_namespace)
def _get_modbus_mode(modbus_mode_enum: ModbusMode, backend_tag: CustomBackendTag):
    """Map generic ModbusMode enum to backend mode constant.

    Parameters
    ----------
    modbus_mode_enum : ModbusMode
        Generic Modbus mode (RTU or ASCII)
    backend_tag : CustomBackendTag
        Backend tag for dispatch

    Returns
    -------
    Any
        Your backend's Modbus mode constant

    Notes
    -----
    Map these values:
    - ModbusMode.RTU: Binary Modbus RTU mode
    - ModbusMode.ASCII: ASCII-encoded Modbus ASCII mode
    """
    # TODO: Implement Modbus mode mapping
    raise NotImplementedError("Implement _get_modbus_mode for your backend")
_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
@dispatch(Parity, CustomBackendTag, namespace=modbusbackend_namespace)
def _get_parity(parity_enum: Parity, backend_tag: CustomBackendTag):
    """Map generic Parity enum to backend parity constant.

    Parameters
    ----------
    parity_enum : Parity
        Generic parity mode (NONE, EVEN, ODD, MARK, SPACE)
    backend_tag : CustomBackendTag
        Backend tag for dispatch

    Returns
    -------
    Any
        Your backend's parity constant
    """
    # TODO: Implement parity mapping
    raise NotImplementedError("Implement _get_parity for your backend")
_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
@dispatch(Stopbits, CustomBackendTag, namespace=modbusbackend_namespace)
def _get_stopbits(stopbits_enum: Stopbits, backend_tag: CustomBackendTag):
    """Map generic Stopbits enum to backend stop bits constant.

    Parameters
    ----------
    stopbits_enum : Stopbits
        Generic stop bits (ONE, ONE_POINT_FIVE, TWO)
    backend_tag : CustomBackendTag
        Backend tag for dispatch

    Returns
    -------
    Any
        Your backend's stop bits constant
    """
    # TODO: Implement stop bits mapping
    raise NotImplementedError("Implement _get_stopbits for your backend")
_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
@dispatch(str, int, CustomBackendTag, namespace=modbusbackend_namespace)
def _create_instrument(port: str, slave_address: int, backend_tag: CustomBackendTag):
    """Create and return your backend's Modbus client instance.

    Called before each read/write operation.

    Parameters
    ----------
    port : str
        Serial port name (e.g., '/dev/ttyACM0', 'COM3')
    slave_address : int
        Modbus slave address (1-247)
    backend_tag : CustomBackendTag
        Backend tag for dispatch

    Returns
    -------
    Any
        Your backend's unconfigured client/instrument instance

    Notes
    -----
    Return a fresh instance - do not configure it here.
    Configuration happens in _setup_instrument.
    """
    # TODO: Create and return your backend's Modbus client
    raise NotImplementedError("Implement _create_instrument for your backend")
_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
@dispatch(
    object,
    int,
    Stopbits,
    int,
    Parity,
    ModbusMode,
    float,
    CustomBackendTag,
    namespace=modbusbackend_namespace,
)
def _setup_instrument(
    instrument,
    baudrate: int,
    stopbits: Stopbits,
    bytesize: int,
    parity: Parity,
    modbus_mode: ModbusMode,
    timeout: float,
    backend_tag: CustomBackendTag,
) -> None:
    """Configure instrument with serial communication parameters.

    Called immediately after _create_instrument.

    Parameters
    ----------
    instrument : Any
        Your backend's client instance (from _create_instrument)
    baudrate : int
        Serial baudrate (e.g., 9600, 19200, 115200)
    stopbits : Stopbits
        Number of stop bits
    bytesize : int
        Number of data bits (typically 8)
    parity : Parity
        Parity setting
    modbus_mode : ModbusMode
        Modbus mode (RTU or ASCII)
    timeout : float
        Serial timeout in seconds
    backend_tag : CustomBackendTag
        Backend tag for dispatch

    Important
    ---------
    MUST set equivalent of close_port_after_each_call=True to prevent
    serial port conflicts when multiple sensors share the same port.
    """
    # TODO: Configure your backend's instrument
    # Use mapping functions: _get_stopbits, _get_parity, _get_modbus_mode
    raise NotImplementedError("Implement _setup_instrument for your backend")
_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
@dispatch(object, CustomBackendTag, namespace=modbusbackend_namespace)
def _open_port(instrument, backend_tag: CustomBackendTag) -> None:
    """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
    ----------
    instrument : Any
        Your backend's client instance
    backend_tag : CustomBackendTag
        Backend tag for dispatch

    Raises
    ------
    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)

    Examples
    --------
    >>> # For minimalmodbus:
    >>> if instrument and hasattr(instrument, 'serial') and instrument.serial:
    ...     try:
    ...         if not instrument.serial.is_open:
    ...             instrument.serial.open()
    ...     except Exception as e:
    ...         logger.warning(f"Failed to open port: {e}")
    ...         raise
    """
    # TODO: Implement port opening for your backend
    raise NotImplementedError("Implement _open_port for your backend")
_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
@dispatch(object, CustomBackendTag, namespace=modbusbackend_namespace)
def _close_port(instrument, backend_tag: CustomBackendTag) -> None:
    """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
    ----------
    instrument : Any
        Your backend's client instance
    backend_tag : CustomBackendTag
        Backend tag for dispatch

    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

    Examples
    --------
    >>> # For minimalmodbus:
    >>> if instrument and hasattr(instrument, 'serial') and instrument.serial:
    ...     try:
    ...         if instrument.serial.is_open:
    ...             instrument.serial.close()
    ...     except Exception as e:
    ...         logger.warning(f"Failed to close port: {e}")
    """
    # TODO: Implement port closing for your backend
    raise NotImplementedError("Implement _close_port for your backend")
_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
@dispatch(object, CustomBackendTag, namespace=modbusbackend_namespace)
def _cleanup_instrument(instrument, backend_tag: CustomBackendTag) -> None:
    """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
    ----------
    instrument : Any
        Your backend's client instance
    backend_tag : CustomBackendTag
        Backend tag for dispatch

    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

    Examples
    --------
    >>> # For minimalmodbus:
    >>> if instrument is not None:
    ...     if hasattr(instrument, 'serial') and instrument.serial is not None:
    ...         if hasattr(instrument.serial, 'is_open'):
    ...             try:
    ...                 if instrument.serial.is_open:
    ...                     instrument.serial.close()
    ...             except Exception:
    ...                 pass  # Silently ignore cleanup errors
    """
    # TODO: Close and cleanup your backend's instrument
    raise NotImplementedError("Implement _cleanup_instrument for your backend")
_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
@dispatch(object, object, object, CustomBackendTag, namespace=modbusbackend_namespace)
def _read_bit(
    instrument, address: int, functioncode: int, backend_tag: CustomBackendTag
) -> int:
    """Read one bit from the slave (coil or discrete input).

    Parameters
    ----------
    instrument : Any
        Your backend's client instance
    address : int
        Bit register address (0-based)
    functioncode : int
        Modbus function code (1: read coils, 2: read discrete inputs)
    backend_tag : CustomBackendTag
        Backend tag for dispatch

    Returns
    -------
    int
        Bit value (0 or 1)

    Raises
    ------
    IOError
        If communication with device fails
    """
    # TODO: Implement single bit read
    raise NotImplementedError("Implement _read_bit for your backend")
_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
@dispatch(
    object, object, object, object, CustomBackendTag, namespace=modbusbackend_namespace
)
def _write_bit(
    instrument,
    address: int,
    value: int,
    functioncode: int,
    backend_tag: CustomBackendTag,
) -> None:
    """Write one bit to the slave (coil).

    Parameters
    ----------
    instrument : Any
        Your backend's client instance
    address : int
        Bit register address (0-based)
    value : int
        Value to write (0 or 1)
    functioncode : int
        Modbus function code (5: write single coil, 15: write multiple)
    backend_tag : CustomBackendTag
        Backend tag for dispatch

    Raises
    ------
    IOError
        If communication with device fails
    """
    # TODO: Implement single bit write
    raise NotImplementedError("Implement _write_bit for your backend")
_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
@dispatch(
    object, object, object, object, CustomBackendTag, namespace=modbusbackend_namespace
)
def _read_bits(
    instrument,
    address: int,
    number_of_bits: int,
    functioncode: int,
    backend_tag: CustomBackendTag,
) -> List[int]:
    """Read multiple bits from the slave (coils or discrete inputs).

    Parameters
    ----------
    instrument : Any
        Your backend's client instance
    address : int
        Starting bit register address (0-based)
    number_of_bits : int
        Number of bits to read
    functioncode : int
        Modbus function code (1: read coils, 2: read discrete inputs)
    backend_tag : CustomBackendTag
        Backend tag for dispatch

    Returns
    -------
    List[int]
        List of bit values (0 or 1)

    Raises
    ------
    IOError
        If communication with device fails
    """
    # TODO: Implement multiple bits read
    raise NotImplementedError("Implement _read_bits for your backend")
_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
@dispatch(object, object, object, CustomBackendTag, namespace=modbusbackend_namespace)
def _write_bits(
    instrument, address: int, values: List[int], backend_tag: CustomBackendTag
) -> None:
    """Write multiple bits to the slave (coils).

    Parameters
    ----------
    instrument : Any
        Your backend's client instance
    address : int
        Starting bit register address (0-based)
    values : List[int]
        List of values to write (0 or 1)
    backend_tag : CustomBackendTag
        Backend tag for dispatch

    Raises
    ------
    IOError
        If communication with device fails
    """
    # TODO: Implement multiple bits write
    raise NotImplementedError("Implement _write_bits for your backend")
_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
@dispatch(
    object,
    object,
    object,
    object,
    object,
    CustomBackendTag,
    namespace=modbusbackend_namespace,
)
def _read_register(
    instrument,
    address: int,
    number_of_decimals: int,
    functioncode: int,
    signed: bool,
    backend_tag: CustomBackendTag,
) -> Union[int, float]:
    """Read a 16-bit integer from a single register.

    Parameters
    ----------
    instrument : Any
        Your backend's client instance
    address : int
        Register address (0-based)
    number_of_decimals : int
        Decimal scaling (divide by 10^number_of_decimals)
    functioncode : int
        Modbus function code (3: holding, 4: input)
    signed : bool
        Interpret as signed (-32768 to 32767) or unsigned (0 to 65535)
    backend_tag : CustomBackendTag
        Backend tag for dispatch

    Returns
    -------
    int or float
        Register value (int if number_of_decimals=0, else float)

    Raises
    ------
    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
    """
    # TODO: Implement 16-bit register read with decimal scaling
    raise NotImplementedError("Implement _read_register for your backend")
_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
@dispatch(
    object,
    object,
    object,
    object,
    object,
    object,
    CustomBackendTag,
    namespace=modbusbackend_namespace,
)
def _write_register(
    instrument,
    address: int,
    value: Union[int, float],
    number_of_decimals: int,
    functioncode: int,
    signed: bool,
    backend_tag: CustomBackendTag,
) -> None:
    """Write a 16-bit integer to a single register.

    Parameters
    ----------
    instrument : Any
        Your backend's client instance
    address : int
        Register address (0-based)
    value : int or float
        Value to write
    number_of_decimals : int
        Decimal scaling (multiply by 10^number_of_decimals before writing)
    functioncode : int
        Modbus function code (6: single, 16: multiple)
    signed : bool
        Interpret as signed or unsigned
    backend_tag : CustomBackendTag
        Backend tag for dispatch

    Raises
    ------
    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
    """
    # TODO: Implement 16-bit register write with decimal scaling
    raise NotImplementedError("Implement _write_register for your backend")
_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
@dispatch(
    object, object, object, object, CustomBackendTag, namespace=modbusbackend_namespace
)
def _read_registers(
    instrument,
    address: int,
    number_of_registers: int,
    functioncode: int,
    backend_tag: CustomBackendTag,
) -> List[int]:
    """Read multiple 16-bit integers from consecutive registers.

    Parameters
    ----------
    instrument : Any
        Your backend's client instance
    address : int
        Starting register address (0-based)
    number_of_registers : int
        Number of registers to read (max ~125)
    functioncode : int
        Modbus function code (3: holding, 4: input)
    backend_tag : CustomBackendTag
        Backend tag for dispatch

    Returns
    -------
    List[int]
        List of register values (unsigned 16-bit: 0-65535)

    Raises
    ------
    IOError
        If communication with device fails
    """
    # TODO: Implement multiple registers read
    raise NotImplementedError("Implement _read_registers for your backend")
_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
@dispatch(object, object, object, CustomBackendTag, namespace=modbusbackend_namespace)
def _write_registers(
    instrument, address: int, values: List[int], backend_tag: CustomBackendTag
) -> None:
    """Write multiple 16-bit integers to consecutive registers.

    Parameters
    ----------
    instrument : Any
        Your backend's client instance
    address : int
        Starting register address (0-based)
    values : List[int]
        List of values to write (max ~123)
    backend_tag : CustomBackendTag
        Backend tag for dispatch

    Raises
    ------
    IOError
        If communication with device fails

    Notes
    -----
    Typically uses function code 16 (write multiple registers).
    """
    # TODO: Implement multiple registers write
    raise NotImplementedError("Implement _write_registers for your backend")
_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
@dispatch(
    object,
    object,
    object,
    object,
    object,
    object,
    CustomBackendTag,
    namespace=modbusbackend_namespace,
)
def _read_long(
    instrument,
    address: int,
    functioncode: int,
    signed: bool,
    byteorder: Byteorder,
    number_of_registers: int,
    backend_tag: CustomBackendTag,
) -> int:
    """Read a 32-bit or 64-bit integer from consecutive registers.

    Parameters
    ----------
    instrument : Any
        Your backend's client instance
    address : int
        Starting register address (0-based)
    functioncode : int
        Modbus function code (3: holding, 4: input)
    signed : bool
        Interpret as signed or unsigned
    byteorder : Byteorder
        Byte order (use _get_byteorder to convert)
    number_of_registers : int
        Number of registers (2: 32-bit, 4: 64-bit)
    backend_tag : CustomBackendTag
        Backend tag for dispatch

    Returns
    -------
    int
        32-bit or 64-bit integer value

    Raises
    ------
    IOError
        If communication with device fails

    Notes
    -----
    Use _get_byteorder to convert Byteorder enum to backend constant.
    """
    # TODO: Implement long integer read with byte order handling
    raise NotImplementedError("Implement _read_long for your backend")
_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
@dispatch(
    object,
    object,
    object,
    object,
    object,
    object,
    CustomBackendTag,
    namespace=modbusbackend_namespace,
)
def _write_long(
    instrument,
    address: int,
    value: int,
    signed: bool,
    byteorder: Byteorder,
    number_of_registers: int,
    backend_tag: CustomBackendTag,
) -> None:
    """Write a 32-bit or 64-bit integer to consecutive registers.

    Parameters
    ----------
    instrument : Any
        Your backend's client instance
    address : int
        Starting register address (0-based)
    value : int
        Value to write
    signed : bool
        Interpret as signed or unsigned
    byteorder : Byteorder
        Byte order (use _get_byteorder to convert)
    number_of_registers : int
        Number of registers (2: 32-bit, 4: 64-bit)
    backend_tag : CustomBackendTag
        Backend tag for dispatch

    Raises
    ------
    IOError
        If communication with device fails
    """
    # TODO: Implement long integer write with byte order handling
    raise NotImplementedError("Implement _write_long for your backend")
_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
@dispatch(
    object,
    object,
    object,
    object,
    object,
    CustomBackendTag,
    namespace=modbusbackend_namespace,
)
def _read_float(
    instrument,
    address: int,
    functioncode: int,
    number_of_registers: int,
    byteorder: Byteorder,
    backend_tag: CustomBackendTag,
) -> float:
    """Read a floating point number from consecutive registers.

    Parameters
    ----------
    instrument : Any
        Your backend's client instance
    address : int
        Starting register address (0-based)
    functioncode : int
        Modbus function code (3: holding, 4: input)
    number_of_registers : int
        Number of registers (2: 32-bit float, 4: 64-bit double)
    byteorder : Byteorder
        Byte order (use _get_byteorder to convert)
    backend_tag : CustomBackendTag
        Backend tag for dispatch

    Returns
    -------
    float
        Floating point value

    Raises
    ------
    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
    """
    # TODO: Implement float read with byte order handling
    raise NotImplementedError("Implement _read_float for your backend")
_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
@dispatch(
    object,
    object,
    object,
    object,
    object,
    CustomBackendTag,
    namespace=modbusbackend_namespace,
)
def _write_float(
    instrument,
    address: int,
    value: Union[int, float],
    number_of_registers: int,
    byteorder: Byteorder,
    backend_tag: CustomBackendTag,
) -> None:
    """Write a floating point number to consecutive registers.

    Parameters
    ----------
    instrument : Any
        Your backend's client instance
    address : int
        Starting register address (0-based)
    value : float or int
        Value to write
    number_of_registers : int
        Number of registers (2: 32-bit float, 4: 64-bit double)
    byteorder : Byteorder
        Byte order (use _get_byteorder to convert)
    backend_tag : CustomBackendTag
        Backend tag for dispatch

    Raises
    ------
    IOError
        If communication with device fails
    """
    # TODO: Implement float write with byte order handling
    raise NotImplementedError("Implement _write_float for your backend")
_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
@dispatch(
    object, object, object, object, CustomBackendTag, namespace=modbusbackend_namespace
)
def _read_string(
    instrument,
    address: int,
    number_of_registers: int,
    functioncode: int,
    backend_tag: CustomBackendTag,
) -> str:
    """Read an ASCII string from consecutive registers.

    Parameters
    ----------
    instrument : Any
        Your backend's client instance
    address : int
        Starting register address (0-based)
    number_of_registers : int
        Number of registers to read
    functioncode : int
        Modbus function code (3: holding, 4: input)
    backend_tag : CustomBackendTag
        Backend tag for dispatch

    Returns
    -------
    str
        ASCII string

    Raises
    ------
    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).
    """
    # TODO: Implement string read (2 chars per register)
    raise NotImplementedError("Implement _read_string for your backend")
_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.

Source code in atmospyre/sensors/backends/_template/backend_template.py
@dispatch(
    object, object, object, object, CustomBackendTag, namespace=modbusbackend_namespace
)
def _write_string(
    instrument,
    address: int,
    textstring: str,
    number_of_registers: int,
    backend_tag: CustomBackendTag,
) -> None:
    """Write an ASCII string to consecutive registers.

    Parameters
    ----------
    instrument : Any
        Your backend's client instance
    address : int
        Starting register address (0-based)
    textstring : str
        ASCII string to write (max 2*number_of_registers characters)
    number_of_registers : int
        Number of registers allocated for the string
    backend_tag : CustomBackendTag
        Backend tag for dispatch

    Raises
    ------
    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.
    """
    # TODO: Implement string write (2 chars per register)
    raise NotImplementedError("Implement _write_string for your backend")