Sensor Architecture
The sensor framework is built around a double-dispatch system that allows type-safe, extensible sensor implementations across multiple Modbus backends.
Why Tag-Based + Backend Double Dispatch?
This design allows you to:
- Add new measurement types without modifying the Sensor base class
- Support multiple Modbus backends (minimalmodbus, pymodbus, etc.) without changing sensor logic
- Ensure type-safe tag validation at the sensor level
- Keep sensor-specific and backend-specific read logic isolated and testable
- Share the same tag types across different sensor models AND backends (e.g., CO2Tag works with any CO2 sensor using any supported Modbus backend)
Core Components
classDiagram
%% Backend abstraction
class ModbusBackendTag {
<<abstract>>
+identify backend type
}
class MinimalmodbusBackendTag {
}
%% Core classes
class Sensor {
-instrument: Instrument
-backend_tag: ModbusBackendTag
-_valid_tags: List~ReadTag~
-_namespace: dict
+read(tags) dict~ReadTag, Any~
+write(settings) None
+get_valid_tags() List~ReadTag~
}
class ReadTag {
<<abstract>>
+metadata: ReadTagMetadata
}
class sensor_namespace {
<<per-sensor>>
+_read(instrument, tag, backend_tag)
+_write(instrument, tag, value, backend_tag)
}
class modbusbackend_namespace {
<<global>>
+_read_float()
+_write_register()
}
%% Relationships
ModbusBackendTag <|-- MinimalmodbusBackendTag
Sensor o-- ModbusBackendTag : owns
Sensor o-- ReadTag : validates and owns
Sensor o-- sensor_namespace : owns
Sensor ..> sensor_namespace : dispatches to
sensor_namespace ..> modbusbackend_namespace : dispatches to
sensor_namespace ..> ModbusBackendTag : dispatches on
Sensor ..> ReadTag : dispatches on
ReadTag ..> sensor_namespace : selects implementation
How It Works
The architecture uses a two-level dispatch system:
1. Initialization
When creating a sensor, the Sensor class uses the global modbusbackend_namespace to create and configure the instrument:
- _create_instrument() dispatches on the backend_tag type to instantiate the appropriate Modbus client
- _setup_instrument() dispatches on the instrument type to configure serial communication parameters
2. Reading/Writing
When calling sensor.read([CO2, TEMPERATURE]):
1. The Sensor validates that each tag is in its _valid_tags list
2. For each tag, retrieves the _read dispatcher from its sensor-specific namespace
3. Calls _read(self.instrument, tag, self.backend_tag) which performs double dispatch:
- First dispatch on tag type (e.g., CO2Tag vs TemperatureTag) - determines WHAT to read
- Second dispatch on backend tag type (e.g., MinimalmodbusBackendTag vs PymodbusBackendTag) - determines HOW to read
4. The appropriate sensor-specific, backend-specific implementation executes
3. Namespace Isolation
- Global
modbusbackend_namespace: Shared across all sensors for backend lifecycle management - Per-sensor namespaces: Each sensor (GMP252, GMM222, etc.) has its own
_readand_writedispatchers to avoid conflicts
This design decouples sensor logic (register addresses, data decoding) from backend logic (communication protocol), allowing any sensor to work with any backend through proper registration. The tag acts as a measurement type selector, while the backend tag acts as a backend implementation selector.