Using the SmarterLink Library
This document describes how to configure and use the SmarterLink .NET library to build participants that communicate over the SmarterLink protocol.
The library must be configured and connected to an MQTT broker before use. This is handled by bootstrapping either via the SmarterLinkBootstrapper class for standalone apps, or via DI extension methods for Generic Host / ASP.NET Core apps. Once bootstrapped, each participant interacts with the bus through a role client that matches its SmarterLink role.
| Client | Role | Obtain via bootstrapper | Obtain via DI |
|---|---|---|---|
HmiClient |
Human-Machine Interface | bootstrapper.GetHmiClient() |
Inject HmiClient |
DataProducerClient |
Data Producer | bootstrapper.GetDataProducerClient() |
Inject DataProducerClient |
DataProcessorClient |
Data Processor | bootstrapper.GetDataProcessorClient() |
Inject DataProcessorClient |
DataStoreClient |
Data Store | bootstrapper.GetDataStoreClient() |
Inject DataStoreClient |
FileTransferClient |
— | bootstrapper.GetFileTransferClient() |
Inject FileTransferClient |
Bootstrapping
Option A: Generic Host / ASP.NET Core (DI)
Use the DI extension methods when using Microsoft.Extensions.Hosting or ASP.NET Core.
1. Register services:
builder.Services.AddMessagingServices(); // registers IMessageBus and MQTT transport
builder.Services.AddSmarterLinkClients(); // registers all role clients and NotificationClient
2. Configure MQTT via appsettings.json:
{
"SmarterLink": {
"Mqtt": {
"ClientId": "my-participant",
"Host": "localhost",
"Port": 1883
},
"Topic": {
"Site": "site-x",
"Area": "area-1",
"SubArea": "sub-1",
"Station": "station-a"
}
}
}
Alternatively, pass the MQTT client ID directly to AddMessagingServices:
builder.Services.AddMessagingServices(mqttClientId: "my-participant");
3. Start the host:
await host.RunWithMessagingServices();
RunWithMessagingServices() connects the message bus to the broker before starting the host. The equivalent of calling ConnectAsync() on IMessageBus manually.
4. Inject clients:
public class MyService(DataProducerClient producer)
{
// ...
}
All clients are registered as transient. NotificationClient is also transient and is injected into each client automatically.
Option B: Standalone Bootstrapper
Use SmarterLinkBootstrapper when not using a Generic Host (e.g. console apps or embedded scenarios).
var config = new SmarterLinkConfig(
TopicConfig: new TopicConfig(
Site: "site-x",
Area: "area-1",
SubArea: "sub-1",
Station: "station-a"),
MqttConfig: new MqttConfig(
ClientId: "my-participant",
Host: "localhost"));
var bootstrapper = new SmarterLinkBootstrapper(config);
bootstrapper.AddConsoleLogging(); // optional
await bootstrapper.StartAsync();
// Obtain clients after starting
var producerClient = bootstrapper.GetDataProducerClient();
var processorClient = bootstrapper.GetDataProcessorClient();
var hmiClient = bootstrapper.GetHmiClient();
var storeClient = bootstrapper.GetDataStoreClient();
StartAsync() must be called before any client is obtained. Calling a Get*Client() method before StartAsync() throws an InvalidOperationException.
Configuration
Regardless of which bootstrapping approach you use, SmarterLink requires two pieces of configuration:
TopicConfig -- describes the location of the station in the topic hierarchy:
| Property | Description |
|---|---|
Site |
Site or facility identifier |
Area |
Area within the site |
SubArea |
Sub-area within the area |
Station |
Station or work cell |
MqttConfig -- MQTT broker connection settings:
| Property | Required | Default | Description |
|---|---|---|---|
ClientId |
yes | - | Unique MQTT client ID for this process |
Host |
no | localhost |
Broker hostname |
Port |
no | 1883 |
Broker port |
Username |
no | - | Broker username |
Password |
no | - | Broker password |
Packages
| Package | Description |
|---|---|
SmarterLink.Bootstrapping |
Standalone bootstrapper for console/simple apps |
SmarterLink.Clients |
Role clients and notification client |
SmarterLink.Messaging |
DI extensions for Generic Host / ASP.NET Core apps |
SmarterLink.Core |
Domain models and protocol types (transitive dependency) |
Role Clients
All role clients expose a Notifications property (a NotificationClient) for subscribing to incoming events, and a NotifyCustomPayload method for publishing application-defined payloads outside the standard event schema.
HmiClient
Publishes job events on behalf of an HMI participant.
// Signal that the active job has changed
await hmiClient.NotifyJobEvent(
sourceParticipantId: "hmi-01",
payload: new JobEvents.ActiveJobUpdated(jobId: "job-2024-001"));
// Signal that a job file is available
await hmiClient.NotifyJobEvent(
sourceParticipantId: "hmi-01",
payload: new JobEvents.JobFileAvailable(
jobId: "job-2024-001",
fileId: "recipe.json",
location: new Location(new Uri("http://datastore/jobs/job-2024-001/config"))));
DataProducerClient
Publishes inspection events and job events on behalf of a Data Producer participant.
// Signal that an image has been acquired
await producerClient.NotifyInspectionEvent(
sourceParticipantId: "cam-01",
payload: new InspectionEvents.ComponentImageAcquired(
componentId: "Comp-042",
fileId: "Comp-042-01.png",
location: new Location(new Uri("http://datastore/components/Comp-042/images/Comp-042-01.png")),
jobId: "job-2024-001"));
// Signal system status
await producerClient.NotifyInspectionEvent(
sourceParticipantId: "cam-01",
payload: new InspectionEvents.InspectionSystemStatusAcquired(
participantId: "cam-01",
information: new Dictionary<string, string> { ["temperature"] = "42°C" }));
DataProcessorClient
Publishes data processor events and job events on behalf of a Data Processor participant.
// Publish an analysis result
await processorClient.NotifyDataProcessorEvent(
sourceParticipantId: "classifier-a",
payload: new DataProcessorEvents.ComponentAnalysisUpdated(
componentId: "Comp-042",
fileId: "Comp-042-analysis-a.json",
location: new Location(new Uri("http://datastore/components/Comp-042/analyses/Comp-042-analysis-a.json")),
jobId: "job-2024-001"));
// Publish a consolidated report
await processorClient.NotifyDataProcessorEvent(
sourceParticipantId: "report-gen",
payload: new DataProcessorEvents.ComponentReportUpdated(
componentId: "Comp-042",
fileId: "Comp-042-report.json",
location: new Location(new Uri("http://datastore/components/Comp-042/reports/Comp-042-report.json")),
jobId: "job-2024-001"));
DataStoreClient
The DataStoreClient has no role-specific publish methods.
Subscribing to Events
Every role client exposes a Notifications property of type NotificationClient. Use it to register handlers for incoming events.
Standard event types are defined in SmarterLink.Core under the SmarterLink.Core.Protocol.Events namespace:
| Class | Events |
|---|---|
JobEvents |
ActiveJobUpdated, JobFileAvailable |
InspectionEvents |
ComponentImageAcquired, UserInformationAcquired, InspectionSystemStatusAcquired |
DataProcessorEvents |
ComponentAnalysisUpdated, ComponentReportUpdated |
Each handler method is constrained to the appropriate base class (e.g. RegisterInspectionEventHandler<TEvent> requires TEvent : InspectionEvents), so the compiler will catch mismatches at build time.
Subscribing to job events
client.Notifications.RegisterJobEventHandler<JobEvents.ActiveJobUpdated>(
async (payload, userData) =>
{
Console.WriteLine($"Active job changed to: {payload.JobId}");
});
Subscribing to Data Producer events
// From any producer
client.Notifications.RegisterInspectionEventHandler<InspectionEvents.ComponentImageAcquired>(
async (payload, userData) =>
{
Console.WriteLine($"Image acquired for {payload.ComponentId}: {payload.Location}");
});
// From a specific producer only
client.Notifications.RegisterInspectionEventHandler<InspectionEvents.ComponentImageAcquired>(
handler: async (payload, userData) => { /* ... */ },
participantId: "cam-01");
Subscribing to Data Processor events
client.Notifications.RegisterDataProcessorEventHandler<DataProcessorEvents.ComponentReportUpdated>(
async (payload, userData) =>
{
Console.WriteLine($"Report available for {payload.ComponentId} at {payload.Location}");
});
Catch-all handler
To receive every standard event on the bus (useful for audit logging or diagnostics):
client.Notifications.RegisterCatchAllStandardEventHandler(
async (payload, userData) =>
{
Console.WriteLine($"Event received: {payload.GetType().Name}");
});
Custom payload handler
To receive application-defined payloads that fall outside the standard event schema:
client.Notifications.RegisterCustomPayloadHandler(
topicFilter: "smarter-link/site-x/area-1/sub-1/station-a/#",
handler: async (topic, header, customPayload) =>
{
Console.WriteLine($"Custom payload on {topic}: {customPayload}");
});
File Transfers
FileTransferClient is available via DI (Inject FileTransferClient) or via the bootstrapper pattern alongside the role clients. It implements the SmarterLink File Transfer Protocol directly over MQTT.
Sending a file
Create a File Send Descriptor (FSD) to make a file available for download. The descriptor subscribes to the appropriate MQTT topics and handles incoming metadata and transfer requests automatically. Embed the descriptor's Location in a SmarterLink event payload so receivers know where to find the file.
await using var stream = File.OpenRead("image.png");
await using var fsd = await fileTransferClient.CreateFileSendDescriptorAsync(
senderId: "cam-01",
descriptorId: "fs:00",
stream: stream,
filename: "image.png",
expiry: DateTimeOffset.UtcNow.AddMinutes(5),
supportedHashingFunctions: HashingFunction.Sha256);
// Embed fsd.Location in a SmarterLink event
await producerClient.NotifyInspectionEvent(
sourceParticipantId: "cam-01",
payload: new InspectionEvents.ComponentImageAcquired(
componentId: "Comp-042",
fileId: "image.png",
location: fsd.Location,
jobId: "job-2024-001"));
// Keep the descriptor alive until the TTL expires
await fsd.ExpiryCompletion;
Receiving a file
When a Location is received in an event payload, use FileTransferClient to fetch metadata, create a File Receive Descriptor (FRD), and request the transfer.
// 1. Fetch metadata from the sender
var metadata = await fileTransferClient.GetMetadataAsync(location);
if (metadata is null) return; // sender not available
// 2. Create a receive descriptor
await using var frd = await fileTransferClient.CreateFileReceiveDescriptorAsync(
senderId: "cam-01",
descriptorId: "fr:00",
metadata: metadata);
// 3. Request the transfer
var accepted = await fileTransferClient.RequestTransferAsync(
fileLocation: location,
chunkSize: 64 * 1024,
receiveDescriptor: frd);
if (!accepted) return; // sender rejected the request
// 4. Read chunks as they arrive
await using var output = File.Create("received-image.png");
await foreach (var chunk in frd.GetChunksAsync())
{
await output.WriteAsync(chunk);
}
Progress can be tracked via frd.ReceivedSize and frd.TotalSize during the transfer.
Testing
For unit and integration tests, register InMemoryTransport instead of the real MQTT transport. This runs the entire message bus in-process with no broker required.
services.AddMessagingServicesForTesting();
services.AddSmarterLinkClients();
Everything else (clients, handlers, topic routing) works identically to the production setup.