Protocol Adapters
Extending SPX with a new protocol adapter lets you bridge simulations to bespoke transports—legacy serial buses, proprietary gateways, or vendor-specific SDKs. This guide distils common patterns from the built-in adapters (ascii, http_endpoint, modbus_slave, mqtt, ble) so you can author your own protocol component with minimal guesswork.
Anatomy of a protocol component
Every adapter is a subclass of spx_sdk.communication.protocol.Protocol and registered with the SDK so YAML definitions can instantiate it:
from spx_sdk.communication.protocol import Protocol
from spx_sdk.registry import register_class
@register_class(name="my_transport")
class MyTransport(Protocol):
def _populate(self, definition: dict):
super()._populate(definition)
# Parse configuration, allocate state, validate inputs
def prepare(self, *args, **kwargs) -> bool:
# Establish connections, warm up caches
return super().prepare(*args, **kwargs)
def run(self, *args, **kwargs) -> bool:
# Perform a single tick (poll remote, push outbound data)
return super().run(*args, **kwargs)
def stop(self, *args, **kwargs) -> bool:
# Stop background workers, close sockets
return super().stop(*args, **kwargs)Key responsibilities:
Configuration parsing in
_populate– validate user input and provide sensible defaults.Lifecycle control – implement
prepare/start/run/stop/detach/attach/releaseas needed to manage resources cleanly. See the Modbus and HTTP adapters for reference.Attribute binding – wire SPX attributes to protocol data structures (topics, registers, BLE state) using helper functions in
spx_sdk.attributes.Threading and IO – background threads (
mqtt,ble), event loops (http_endpoint), or socket servers (ascii) keep traffic flowing.Status reporting – expose a
statusproperty (typically an enum) so operators can inspect health via the API/UI.
Designing the configuration schema
Aim for a declarative shape that mirrors the target protocol. Borrow ideas from:
mqtt:broker, credentials, QoS, unifiedbindingssection with direction.http_endpoint: route definitions with delay/jitter knobs.ble: nestedadapter,device,codecs,services,bindings.
Checklist:
Define required keys and raise
ValueErrorearly when they are missing.Surface optional tuning knobs (timeouts, jitter, polling interval) with defaults.
Support environment overrides for secrets (see
mqtt’senv_username_var/env_password_var).When migrating from older schemas, provide compatibility shims (e.g., MQTT’s legacy
publishers/subscribersconversion).
Example _populate fragment:
Attribute binding patterns
Most adapters maintain a bridge between SPX attribute references and protocol-specific payloads:
Resolve references with
resolve_attribute_reference_hierarchical(self.parent, ref)to support$in(...),$out(...), or#attr(...).Encapsulate the binding in small classes (
Bindingin BLE,SubscriptionBinding/PublicationBindingin MQTT).Track last values to suppress redundant updates (
_value_changedlogic in BLE).Provide codecs or conversion hooks where necessary (string encode/decode, register packing).
Sample binding builder:
Use these bindings inside your worker loop to push/pull data reliably.
Lifecycle management blueprint
Each protocol aligns with the same lifecycle contract:
prepare() — one-time setup, allocate sessions, load configs. Do not start threads here.
start() — launch background workers (threads, asyncio servers). Guard against double start.
run() — optional per-tick hook for polling-style adapters (BLE, MQTT) when SPX runs in deterministic steps.
stop() — terminate workers and close connections.
detach()/attach() — optional; typically used to temporarily pause the adapter while keeping state intact (HTTP, BLE).
release()/destroy() — final clean-up invoked when the component is removed.
Mirror the defensive coding seen in http_endpoint and modbus_slave: always handle repeated calls, and log failures with enough context to debug (host, port, broker address).
Example: WebSocket echo protocol (conceptual)
Below is a minimal skeleton that broadcasts attribute changes over WebSockets and forwards inbound messages to SPX attributes. It demonstrates the concepts above without diving into transport specifics.
This skeleton focuses on structure: configuration parsing, attribute resolution, lifecycle management, and the run() method pushing outbound updates. Replace the transport internals with your own library calls.
Integrating the protocol in YAML
Once registered, the protocol becomes available under communication: in your model definitions:
The SPX loader instantiates the protocol component alongside other communication blocks, and the lifecycle control (prepare/start/run/stop) is driven automatically.
Testing and validation
Unit tests – isolate helper functions (binding builders, codecs) and use dependency injection for networking pieces. The ASCII and MQTT modules include pure-python tests for message parsing.
Integration tests – use
spx_python.initplus helper utilities (wait_for_attribute,ensure_instance) to run end-to-end scenarios. Mirror the approach inspx-examples/tests/*to validate bidirectional flows.Deterministic runs – when possible, expose deterministic modes (e.g., disable polling jitter) so CI tests are stable.
Fault observability – log errors with context (host, port, topic, correlation IDs) and expose counters through the protocol component’s state so operators can inspect issues via
/api/v3/system.
Best practices checklist
Keep the adapter self-contained: configuration parsing, lifecycle, and bindings should live in one module.
Guard against partial configuration by raising informative
ValueError/RuntimeErrorduring_populateorprepare.Clean up all resources in
stop()/release()to avoid zombie threads or lingering sockets.Consider exposing metrics attributes (counters, status enums) so dashboards can track connectivity.
Document the YAML schema inline and in the docs (see BLE Adapter for a comprehensive example).
Following these conventions will ensure your custom protocol behaves like a first-class citizen alongside the built-in adapters. Once the module is importable, users can combine it with other communication blocks, scenarios, and automation tooling without additional integration work.
Last updated

