Architecture¶
Py ToloMEO organises microservice logic into four layers — service, plugin, messaging, and task. Each layer has a single responsibility and communicates with adjacent layers through well-defined interfaces.
The diagram below gives the high-level picture: how an inbound command travels from the ToloMEO cloud down to a plugin, and how outbound data and events travel back up to the cloud.
A service is configured declaratively through an inner Meta class — its
plugin class, messaging strategy, and commands. These settings are assembled
across the whole class hierarchy at class-definition time, so subclasses
extend behaviour by declaration rather than by overriding.
Service Layer¶
The service (built on ServiceBase / NATSService) is the top-level
orchestrator and owns four collaborators: the messaging strategy, a command
registry, a plugin registry, and a task manager. It:
- Subscribes to
commands.{name}.reqand decodes inbound commands - Runs a service-level command if one is registered, otherwise routes the
command to the plugin identified by the payload's
id - Publishes command responses to
events.params, sensor data toevents.data, and OTA/status events toevents.infos - Publishes a heartbeat to
heartbeat.{name}.service; sensor data and info arrive via push — plugins callpush_reading/push_info, which invoke a callback the service publishes from. There is no polling loop.
A service holds one plugin class but many plugin instances, keyed by plugin ID. SenML records are built in this layer before publishing — the messaging layer never sees SenML.
How plugins get acquired¶
setup registers the service's own commands and then calls acquire_plugins,
the single hook a service overrides to say which plugins it manages and how
it obtains them. The steps common to every service live in ServiceBase:
| Method | Responsibility |
|---|---|
acquire_plugins |
Which plugins to obtain, and how. Default: none |
new_plugin |
Instantiate Meta.plugin_class |
wire_plugin |
Bind the plugin's push callbacks. Default: none |
attach_plugin |
Wire, attach, and register — registering only if attach succeeds |
That last guarantee matters: a plugin whose connect failed never enters the
registry, so it is never served commands as though it were healthy.
OTAService attaches one plugin and SingleSensorService attaches one and wakes
the params loop, both through acquire_plugins. BLESensorService acquires later
instead, from start(): its addresses come from a configuration file it loads
there, and BLE discovery is slow enough that doing it during setup() would
delay startup.
The abstract bases declare no plugin_class
PluginBase, SensorPlugin, OTAPlugin and BLESensorPlugin are all
abstract, so SensorService, OTAService and BLESensorService deliberately
name no default plugin class — there is no instantiable one to name. Every
deployment declares its own, and forgetting fails at construction with a
RuntimeError naming Meta.plugin_class rather than later.
Plugin Layer¶
Plugins encapsulate domain logic and are decoupled from the transport. The
built-in base classes are SensorPlugin (continuous metric publishing),
BLESensorPlugin (a sensor over Bluetooth Low Energy) and OTAPlugin (firmware
update management). Each plugin:
- Declares its commands, input metrics, output metrics, and configuration options
in a
Metaclass (output metrics are namespaced with the plugin ID) - Manages its own connection lifecycle to hardware or external resources
- Runs background tasks (e.g. sensor read loops) on its own task manager and pushes readings to the service through a data-available callback — delivery is entirely push-driven, with no polling on the service side
Reusable behaviour lives in dependency-free mixins that the base classes combine
with PluginBase, which is what lets a plugin's logic be tested without a
service or a broker. All the base classes are abstract: hardware access is the
concrete plugin's to supply.
Messaging Layer¶
NATSMessageStrategy wraps the nats-py client and is pure transport: it
serialises payloads to JSON and publishes or subscribes on NATS subjects. It
performs no SenML encoding — that belongs to the service layer. The service
interacts with it only through the MessageStrategyABC interface. Swapping the
strategy class in Meta is the only change needed to use a different
transport.
ToloMEO Platform Context¶
A deployed Py ToloMEO microservice runs on an edge device. It connects to the
ToloMEO cloud platform through the connhex-edge-agent (a NATS proxy). The
platform sends commands inbound (operator triggers an OTA update) and receives
data outbound (sensor readings, OTA status events). Py ToloMEO handles the
edge side of this contract.
See also

