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.