Connector Binding#
This page explains how the middleware wires itself to external systems — devices, peer middleware instances, and the knowledge graph itself. It covers the recognition chain that turns ontology declarations into live connections, how to add support for a protocol this library does not ship, and the failure modes that produce no exception.
Terms this page assumes
The Recognition Chain#
A semantic connector registers itself from the knowledge graph. The chain runs once, at middleware construction:
Domain property — a complex property in your ClassScope, e.g.
tu:hasConveyorSpeed.Interface property — the property’s
rdfs:subPropertyOf*ancestry is queried against the connector registry. If it descends frominf:isInterfaceAccessibleMQTTParameter, the MQTT binding matches.Binding descriptor — the registry returns the descriptor keyed on that interface property.
Metadata fetch — the binding reads the connection-metadata properties it declared (e.g.
inf:hasMQTTTopic,inf:hasMQTTBrokerIP) from the graph.Connector build — the binding yields one or more
Registrationobjects, each naming aconnector_cls, aConnectionInfo, aSyncDirection, and a formatter.
Recognition runs over the ClassSpec and the graph, not over materialized instance data. Everything it needs — which properties are COMPLEX, which match an interface property — is available before any instance data is fetched.
# The SPARQL pattern that decides a match
ASK { tu:hasConveyorSpeed rdfs:subPropertyOf* inf:isInterfaceAccessibleMQTTParameter }
Registration Happens at Construction#
Critical: Bindings are built and registered in the middleware constructor. Registering a connector later — during on_start_up or datamodel materialization — means the framework never calls connect() on it. Inbound traffic dies silently: the listener task never starts, the queue is never fed, and receive() blocks forever. Outbound may limp along because consume() reconnects on failure, making the fault one-directional and quiet.
# Correct: bindings register during SemanticMiddleware.__init__
mw = SemanticMiddleware(
mode="resource",
resource_iri=tui.TransferUnit1,
autoregister_connectors=True,
connector_sync_direction=SyncDirection.BIDIRECTIONAL,
)
# By this point, all connectors are already in the framework's connection registry.
The framework calls connect() on everything in the connection registry before it runs on_start_up callbacks. Registration in the constructor avoids the silent failure entirely.
Adding a Protocol#
To support a protocol this library does not ship, build a binding descriptor and inject it into the connector registry. A descriptor references its connector class rather than subclassing it, so a connector nobody here owns can still be made semantic.
from kapps_semantic_middleware.connectors.semantic import semantic_connector, Registration
from kapps_semantic_middleware.vocabulary import INF
from transitional_sync_middleware.middleware.sync.synced_connector import SyncDirection
@semantic_connector
class VendorBinding:
connector_cls = SomeVendorConnector
interface_property = INF.isInterfaceAccessibleVendorParameter
connection_metadata = (INF.hasVendorAddress, INF.hasVendorChannel)
@staticmethod
def build(metadata, conn_info, direction):
address = metadata[INF.hasVendorAddress]
channel = metadata[INF.hasVendorChannel]
# Formatter reassembles static facets (unit, access mode) into the payload
formatter = ...
yield Registration(
SomeVendorConnector(address, channel),
conn_info,
SyncDirection.TO_PERSISTENCE,
formatter,
float
)
if direction is SyncDirection.BIDIRECTIONAL:
yield Registration(
SomeVendorConnector(address, channel + "_set"),
conn_info,
SyncDirection.FROM_PERSISTENCE,
formatter,
float
)
# Inject after init
mw.connector_registry[INF.isInterfaceAccessibleVendorParameter] = VendorBinding
One binding may yield two connectors against one binding target — a read topic and a write topic — differing only in sync_direction. The framework’s ConnectionRegistry.connections is Dict[ConnectionInfo, List[str]], so both bind to one ConnectionInfo.
Connector Wiring#
An instance is described by its connector’s protocol and direction, and by nothing else. These are constructor parameters of resource mode:
Wiring |
|
|
Behavior |
|---|---|---|---|
Driving |
|
|
Reads live values and drives the device |
Observing |
|
|
Reads live values, structurally unable to write |
Inspecting |
|
(ignored) |
Nothing connected; structure and graph content only |
Direction is the most restrictive of two constraints: a parameter’s inf:accessMode and the instance’s wiring. A read registration is always emitted. A write registration is emitted only when the parameter is readwrite and the wiring permits a write. A monitor can never drive a writable belt, and a controller can never write a sensor — structurally, not by convention. An absent or unrecognized accessMode yields read-only.
Shipped Protocols#
Two bindings ship in the library:
MQTT (connectors/mqtt_binding.py) — reaches a device. Recognized by inf:hasMQTTTopic on the parameter blanknode. Metadata: inf:hasMQTTBrokerIP, inf:hasMQTTTopic, inf:hasMQTTSetTopic (present iff accessMode is readwrite), optional inf:hasMQTTValuePath, optional inf:hasMQTTBrokerPort (absent means 1883). Topic scheme: TransferUnit<n>/<component>/<position>/<param>, and a setpoint appends _set. Payload is raw scalar by default; the parameter’s ontology datatype parses it. With inf:hasMQTTValuePath, a JSON envelope is read and written at that path.
REST (connectors/rest_binding.py) — reaches another middleware instance over its generated REST routes. Recognized by the resource’s Service carrying an svc:address; no parameter-local marker is needed. A peer middleware is a device as far as the seam is concerned. The route is structural: address plus structural path derived from the datamodel tree is a complete binding. See Views and Projection for the route shape.
Connection Metadata and Projection#
A binding descriptor declares the connection-metadata properties its protocol needs. The same set is what gets hidden from the northbound surface. The Projection removes protocol properties from the ClassSpec before fetching, so a peer cannot learn the broker address and bypass the middleware. What counts as protocol metadata is read from the ontology at every startup: everything contributed by an Interface property strictly between the Parameter’s own property and inf:isInterfaceAccessibleParameter.
See Views and Projection for the hiding mechanism.
Transport#
A transport is what a connector dials — an MQTT broker, and nothing else so far. A Parameter’s connection metadata names a transport by address; it never provides one. This library ships no transport implementation. The middleware may be asked to ensure a transport exists at a declared address before it registers the first connector aimed there:
def my_starter(host: str, port: int) -> None:
# Start amqtt broker, or verify one is reachable, or ...
...
mw = SemanticMiddleware(..., ensure_transport=my_starter)
The hook is called once per distinct (host, port) during construction. The library never starts a broker, never probes one, never stops one, and never reads a return value. It states a need; the deployment meets it.
The Graph-Facing Connector#
Framework constructs (workflows, synced connectors) read and write the knowledge graph through KnowledgeGraphConnector, not by calling ogm.fetch/ogm.commit directly. This connector implements transitional_sync_middleware’s Connector protocol (connect/disconnect/provide/consume), wrapping OGM operations. Any future MES-style synchronization pattern (SyncedConnector, SyncRole/SyncDirection) that transitional_sync_middleware provides for other connectors becomes available for knowledge-graph synchronization without new plumbing.
# Internal: all graph access goes through this connector
from kapps_semantic_middleware.connectors.knowledge_graph_connector import KnowledgeGraphConnector
This also becomes the extension point for @state’s value source. Any transitional_sync_middleware connector can back a StateProperty’s getter — an OpcUaConnector reads a PLC register, an MqttClientConnector subscribes to a topic. Reuse the existing IT/OT bridge directly.