Controller Architecture¶
The IoT Controller is a headless Python agent that reads sensor data and pushes it to the backend.
Package Structure¶
controller/src/webmacs_controller/
├── __init__.py
├── __main__.py # Entry point (python -m webmacs_controller)
├── app.py # Main application loop
├── config.py # pydantic-settings configuration
├── schemas.py # Pydantic models for API payloads
└── services/
├── api_client.py # HTTPX client with retry + auto-reauth
├── actuator_manager.py # Actuator command handling
├── demo_seeder.py # Demo event seeding (dev mode)
├── hardware.py # HardwareInterface ABC + RevPi/Simulated impls ├── plugin_bridge.py # Load, start, and manage plugin instances ├── rule_engine.py # Valve cycling rule logic
├── sensor_manager.py # Sensor reading abstraction
└── telemetry.py # TelemetryTransport Protocol (HTTP / WebSocket)
Telemetry Modes¶
The controller supports two transport modes, selected via WEBMACS_TELEMETRY_MODE:
HTTP Mode (default)¶
sequenceDiagram
Controller->>+Backend: POST /api/v1/datapoints
Backend-->>-Controller: 201 Created
Each sensor reading → one HTTP POST request.
WebSocket Mode¶
sequenceDiagram
Controller->>+Backend: WS /ws/controller/telemetry
Note over Controller,Backend: Persistent connection
loop Every poll interval
Controller->>Backend: JSON message
Backend-->>Controller: ack
end
A single persistent WebSocket connection carries all telemetry.
Transport Protocol¶
The telemetry layer uses a Protocol for pluggability:
class TelemetryTransport(Protocol):
async def connect(self) -> None: ...
async def send(self, payload: dict) -> None: ...
async def disconnect(self) -> None: ...
Implementations:
HttpTelemetry— wrapsAPIClient.post()WebSocketTelemetry— async context manager overwebsocketslibrary
Auto-Reauthentication¶
The APIClient handles JWT lifecycle automatically:
- Login with configured credentials on startup
- Store JWT token
- On any 401 Unauthorized response → re-login and retry
- Exponential backoff on repeated failures
Sensor Manager¶
In development mode (WEBMACS_ENV=development), the SensorManager generates dummy data — sinusoidal waves, random noise, etc. In production mode, it reads from the RevPi via revpimodio2.
Configuration¶
class ControllerSettings(BaseSettings):
webmacs_env: str = "development"
webmacs_server_url: str = "http://localhost"
webmacs_server_port: int = 8000
webmacs_poll_interval: float = 1.0 # min 0.2 s
webmacs_max_batch_size: int = 100 # 1–500
webmacs_dedup_enabled: bool = False
webmacs_telemetry_mode: str = "http" # "http" | "websocket"
webmacs_admin_email: str
webmacs_admin_password: str
Plugin Bridge¶
The PluginBridge (plugin_bridge.py) loads and manages plugin instances on the controller:
- Discovery — scans installed packages for the
webmacs.pluginsentry-point group viaimportlib.metadata - Instantiation — creates plugin instances from the backend configuration (plugin ID, config JSON, demo mode)
- Lifecycle — starts, stops, and monitors plugins; reports status back to the backend
- Channel I/O — reads input channels and writes output channels each polling cycle, forwarding values through the telemetry pipeline
Plugins are Python packages that implement the DevicePlugin (async) or SyncDevicePlugin (blocking) abstract base class from the webmacs-plugins-core SDK.
flowchart LR
Registry["Plugin Registry"] -->|discover| EP["Entry Points"]
EP --> P1["Plugin A"]
EP --> P2["Plugin B"]
Bridge["Plugin Bridge"] -->|start / stop| P1
Bridge -->|start / stop| P2
P1 -->|channel data| TM["Telemetry"]
P2 -->|channel data| TM
See the Plugin Development Guide for writing custom plugins.
Next Steps¶
- WebSocket Design — full protocol specification
- Backend Architecture — how the backend processes data