Docker Applications
Docker applications are long-running, containerised programs that execute on Doovit devices (or any Docker-capable edge hardware). They form the primary way to deploy custom logic to the device layer of the Doover platform. Applications read sensors, control outputs, communicate with the cloud, and present interactive UIs to users in the web portal.
How Docker Applications Work
A Docker application runs as a container on the Doovit. It does not interact with hardware or the cloud directly. Instead, it communicates through three sidecar services, each exposed via gRPC on localhost:
| Service | Interface Class | Default Address | Purpose |
|---|---|---|---|
| Device Agent (DDA) | DeviceAgentInterface | 127.0.0.1:50051 | Cloud communication: channels, messages, events, tags |
| Platform Service | PlatformInterface | localhost:50053 | Hardware I/O: digital/analog inputs and outputs, pulse counters, system info |
| Modbus Service | ModbusInterface | 127.0.0.1:50054 | Modbus RTU and TCP communication with external devices |
The application container and these sidecar containers are orchestrated together on the Doovit. Your application code only needs to use the pydoover interface classes; all gRPC transport is handled internally.
The Application Base Class
Every Docker application is built by subclassing pydoover.docker.Application. The base class provides:
- Lifecycle management with
setup(),main_loop(), and shutdown hooks - Channel operations for subscribing to and publishing data
- Tag management for persistent key-value state
- Configuration loaded from the cloud or a local file
- UI management for declaring interactive web portal elements
- Notification sending for alerting users
- Hardware proxies that forward calls to the Platform and Modbus interfaces
Class Attributes
Three class attributes define the application's schema types:
from pydoover.docker import Application
from pydoover import config
from pydoover.ui import UI, NumericVariable
from pydoover.tags import Tags, Number
class MyConfig(config.Schema):
poll_interval = config.Number("Poll Interval", default=30, description="Seconds between polls")
class MyTags(Tags):
temperature = Number(default=0.0)
class MyUI(UI):
temp_display = NumericVariable("Temperature", units="C")
class MyApp(Application):
config_cls = MyConfig
tags_cls = MyTags
ui_cls = MyUI
These class attributes tell the framework how to parse configuration, initialise tags, and build the UI. The framework instantiates them during startup and makes them available as self.config, self.tags, and self.ui respectively.
Application Lifecycle
The lifecycle follows a linear sequence: initialise, set up, loop, and shut down.
1. Entry Point
Every Docker application uses run_app() as its entry point. This function parses command-line arguments, creates the interface instances, and starts the event loop.
from pydoover.docker import Application, run_app
class MyApp(Application):
def setup(self):
pass
def main_loop(self):
pass
if __name__ == "__main__":
run_app(MyApp)
The run_app() function extracts the app_key, gRPC URIs, healthcheck port, and config file path from the command line. You do not need to handle argument parsing yourself.
2. Setup
After the Device Agent becomes healthy and configuration is loaded, the framework calls setup(). Use this method for one-time initialisation: subscribing to channels, configuring Modbus buses, setting initial tag values, and preparing any state the main loop depends on.
def setup(self):
# Subscribe to a command channel from the cloud
self.subscribe("commands", events=["message_create"])
# Set initial tag values
self.set_tag("status", "starting")
The setup() method runs exactly once per application start. All interface instances (self.device_agent, self.platform_iface, self.modbus_iface) are available and connected when setup() is called.
3. Main Loop
After setup, the application enters a continuous loop. The main_loop() method is called once per iteration at a target period controlled by self.loop_target_period (default: 1 second).
def main_loop(self):
# Read a sensor
temp = self.platform_iface.fetch_ai(0)
# Update a tag (committed automatically after each iteration)
self.set_tag("temperature", temp)
# Publish data to a channel
self.create_message("telemetry", {"temperature": temp})
Tags are committed to the cloud automatically at the end of each loop iteration. If main_loop() raises an exception, the framework catches it, logs the traceback, and retries after a delay.
4. Shutdown
Shutdown is triggered by a shutdown_at tag timestamp, a system signal (SIGTERM/SIGINT), or a call to request_shutdown(). The application can defer shutdown by returning False from check_can_shutdown(), and perform cleanup in on_shutdown_at().
def check_can_shutdown(self):
if self.critical_operation_active:
return False
return True
def on_shutdown_at(self, dt):
self.set_tag("status", "offline")
For a complete lifecycle walkthrough, see Application Lifecycle.
Communication Architecture
The following describes how data flows between the application and the rest of the platform.
Cloud Communication (Device Agent)
The Device Agent service manages the connection to the Doover cloud. Through it, your application can:
- Subscribe to channels and receive events when messages arrive or aggregates change
- Create and update messages to publish data to the cloud
- Read channel aggregates to fetch current state
- Commit tags to persist key-value state across restarts
All cloud operations go through the DeviceAgentInterface. The application never connects to the cloud APIs directly; the Device Agent handles authentication, connection management, and offline buffering.
Hardware I/O (Platform Interface)
The Platform Interface provides access to the Doovit's physical I/O:
- Digital inputs/outputs for switches, relays, and binary sensors
- Analog inputs/outputs for voltage/current sensors and variable outputs
- Pulse counters for flow meters, encoders, and event counting
- System information including voltage, temperature, GPS location, and uptime
See Platform Interface for full details.
Modbus Communication
The Modbus Interface enables communication with external devices over Modbus RTU (serial) or Modbus TCP (network). It supports:
- Bus management for opening and closing serial or TCP connections
- Register reads and writes for input and holding registers
- Polling subscriptions for automatic periodic register reads with callbacks
See Modbus Interface for full details.
Configuration, Tags, and UI
Docker applications integrate with three declarative systems that bridge device-side code and the web portal.
Configuration defines what settings the application accepts. The framework auto-generates a settings form in the web portal from your Schema subclass. See Configuration Overview.
Tags provide persistent key-value state scoped to the application. Tags survive restarts and are synchronised to the cloud. Tag changes can trigger auto-logging and notifications. See Tags Overview.
UI elements define the interactive interface displayed in the web portal. Variables display data, interactions accept user input, and containers organise elements into groups. See UI Overview.
Test Mode
For local development without a real Doovit, applications support a test mode. When test_mode=True, the framework uses a MockDeviceAgentInterface that simulates cloud operations in memory.
app = MyApp(app_key="test", test_mode=True)
await app.setup()
# Advance one loop iteration
await app.next()
# Inspect state
print(app.tags.get("temperature"))
Test mode allows you to write unit tests and integration tests for your application logic without requiring hardware or cloud connectivity. The mock interface records all channel operations and tag commits for assertions.
Next Steps
- Application Class -- full API reference for the Application base class
- Device Agent Interface -- cloud communication bridge
- Platform Interface -- hardware I/O operations
- Modbus Interface -- serial and TCP Modbus communication
- Examples -- complete example applications