Skip to main content

Business Plugin Development

This document uses "business plugin" as a collective term for data forwarding targets derived from DataForwardBase, protocol servers, database writers, cloud integrations, and synchronization bridges. Business plugins do not collect raw data from field devices. They consume collected variables, device states, alarms, or plugin events and write them to external systems or expose server-side protocol capabilities.

To extend drivers for PLCs, instruments, OPC, MQTT collection, or other devices, see Collection Plugin Development.

Source Code Entry

EntryFunction
ThingsGatewayRuntime.Application/Driver/DataForward/DataForwardBase.csruntime base class for all business plugins.
ThingsGatewayRuntime.Application/Driver/DataForward/DataForwardPropertyBase.csRoot-level plugin property class.
ThingsGatewayRuntime.Application/Driver/DataForward/DataForwardVariablePropertyBase.csRoot class of the target variable attribute.
ThingsGatewayRuntime.Application/Driver/DataForward/DataForwardChannelPropertyBase.csrequires TCP, serial port, DTU, SSL target attribute base class.
ThingsGatewayRuntime.Application/Driver/DataForward/Cache/*Cache queue, offline file cache, topic template, script model, batch upload base class.
ThingsGatewayRuntime.Application/Task/DataForward/DataForwardMange/DataForwardMange.csForwards groups, target launches, event distribution, group-level data production , redundant switching scheduling entry.
ThingsGatewayRuntime.Application/Model/DataForwardGroupRuntime.csforwards the range-parsed runtime variables and device indexes of the group.
ThingsGatewayRuntime.Plugin/Plugin/*MQTT, Kafka, RabbitMQ, Webhook, ModbusSlave, SyncBridge, etc. implementations.
ThingsGatewayRuntime.NOAOTPlugin/Plugin/OpcUa/OpcUaServerOPC UA Server business plugin implementation.
ThingsGatewayRuntimePRO/src/ThingsGatewayRuntime.NOAOTPROPlugin/Plugin/*Implemented in professional versions such as IEC104, IEC61850 Server, ZeroMQ, etc.

Boundaries between Business Plugins and Collection Plugins

ProjectCollection PluginsBusiness Plugins
Base ClassesCollectBaseDataForwardBase
Configuration LocationCollection DevicesData forwarding objectives
Data sourcesPLC, instruments, external reporting, virtual computingRuntime variables, device status, alarms, plugin events
Main tasksRead and write on-site locationsUpload, storage, server-side protocols, bridging, write-back
variable rangeVariable enabled under the deviceRange of variables parsed by the forwarding group
Write entryInvokeWriteAsync, InvokeMethodAsyncAfter writing the target protocol, the acquisition side RPC is usually called again.

Do not write the business plugin as "then scan all variables globally." The variable range is determined by the forwarding group. Business plugins should read the currently visible data from the target via GetVariables(), IdVariableRuntimes, CollectDevices or the group production entry.

Plugin Discovery Rules

RulesDescription
Inherit DataForwardBasePluginService Only recognize non-abstract derived classes of DataForwardBase as data forwarding plugins.
Publicly disclose the parameterless constructorCreated at runtime via Activator.CreateInstance, with each target having an independent instance.
The full name of the plugin is the configuration keyData forwarding target storage type FullName. Renaming the namespace or class name will affect the old configuration.
Inject the target property first, then initializeDataForwardMange.StartTargetAsync Call SetDriverProperties, then InitTarget, InitAsync, StartAsync.
The target variable attribute does not determine the variable rangeThe target variable attribute only stores plugin-specific configurations such as external mappings, permissions, and data types. Whether a variable enters the target depends on the range of the forwarded group and the relationship between group variables.
UsesGroupDataProducer determines the data entry pointcached targets, usually true, consuming data produced by the unified forwarding group; The server-side protocol class is usually false, which directly handles variable changes.

Base Class Selection

Base ClassSuitable ScenarioFocus must be placed onexisting references
DataForwardBaseprotocol server, synchronization bridge, and targets requiring self-managed memory mapping or connection loops.TargetProperties,TargetPropertyType, optional VariablePropertyType, ProtectedInitAsync, AfterVariablesChangedAsync, OnVariableChanged, ProtectedExecuteAsync, IsConnected.ModbusSlave, OpcUaServer, IEC104Slave, IEC61850Server, SyncBridge.
DataForwardBaseWithCacherequires offline caching, but the trigger model does not fully correspond to the target uploaded by periodic variables.DataForwardPropertyWithCacheDataForwardPropertyWithCache, enabled models, Update*Model or AcceptProduced*.HisAlarmForwardTarget.
DataForwardBaseWithCacheIntervalConsumes data produced by the forwarding group uniformly, supporting periodic/variable/batch processing and offline caching.DataForwardPropertyWithCacheInterval, Model Switch, AcceptProduced* or Update*Model.HisDataForwardTarget, RealDataForwardTarget, ThingsBoardClientProducer.
DataForwardBaseWithCacheIntervalScriptRequires topic templates, entity scripts, and custom upload templates, but only wants to reuse conversion capabilities.After inheriting, call methods such as GetVariableBasicDataTopicArray by model.Parent class of the script upload class target.
DataForwardBaseWithCacheIntervalScriptAllMQTT, Kafka, RabbitMQ, Webhook, ZeroMQ and similar "Topic/Payload Generated and Uploaded" targets.Implement Upload(TopicArray, CancellationToken), initialize client connections, and override IsConnected.MqttClientProducer, KafkaProducer, RabbitMQProducer, Webhook, ZeroMQProducer.
DataForwardChannelPropertyBasetarget requires TCP client/server, serial port, DTU, and SSL parameters.property class inherits it and calls ProtectedInitAsync in InitChannelAsync.ModbusSlaveProperty, IEC104SlaveProperty.

If the goal is simply to "convert variables to JSON and send to a certain system," prioritize using DataForwardBaseWithCacheIntervalScriptAll. If the goal is to simulate a protocol server externally and allow external systems to actively read and write memory point tables, prioritize using DataForwardBase.

Target Attributes and Variable Attributes

TypesFunctions
DataForwardPropertyBaseGoal-level property root class. Connection address, authentication, table name, cache, and template are all target attributes.
DataForwardVariablePropertyBaseRoot class of the target variable-level property. The external address, data type, write permissions, etc., of a single variable under a certain target fall under this category.
DataForwardVariablePropertyGeneral reserved field Data 1 to Data 10, suitable for simple external mapping.
DataForwardPropertyWithCacheOffline caching, memory queue limit, upload sharding, offline data filtering, upload concurrency.
DataForwardPropertyWithCacheIntervalInherits cache properties; the trigger mode and cycle are uniformly determined by the forwarding group.
DataForwardPropertyWithCacheIntervalScriptAdds detailed logs, JSON format, list/dictionary upload, topic template, entity script, and upload template.
DataForwardChannelPropertyBaseAdds channel type, remote address, local binding, SSL, serial port, heartbeat, DTU, concurrency, connection timeout, etc.

When implementing the target attribute, ensure that both the instance and type of the target attribute are consistent:

private readonly MyTargetProperty _properties = new();

public override DataForwardPropertyBase TargetProperties => _properties;

public override Type TargetPropertyType { get; } = typeof(MyTargetProperty);

If the plugin has variable configurations, then provide:

private readonly MyVariableProperty _variableProperties = new();

public override DataForwardVariablePropertyBase VariablePropertys => _variableProperties;

public override Type? VariablePropertyType { get; } = typeof(MyVariableProperty);

DataForwardVariablePropertyBase.Enable is not a dynamic property, so the page will not have a second enable switch. Whether to forward a variable is determined by the range of group variables, the enabled status of variables within the group, the enabled status of the target, and the trigger conditions.

DynamicProperty Contract

business plugins and collection plugins use the same set of dynamic property rules.

ProjectInstructions
Only [DynamicProperty] will be displayed and savedDo not tag runtime connection objects, client objects, cache fields, or temporary indexes.
The default value will enter the new formDefault port, Topic, table name, and cache off/enable policies should be conservative.
CertificatePurpose for certificate dropdownCertificate fields such as MQTT, OPC UA, IEC61850 TLS should indicate Client, Server, or CA.
Remark Specify the unit and format clearlysuch as milliseconds, seconds, topic templates, SQL table names, and concatenation string formats.
Complex objects can also be used as propertiessuch as upload template configuration or SOE configuration, but JSON serialization and import/export must be restored.

Lifecycle Hooks

PhaseRuntime ActionsPlugin Development Notes
ConstructorCreate the target plugin instance.Only initialize lightweight fields, do not connect to external systems.
Property injectionSetDriverProperties(forwarder.TargetProperties, forwarder.TargetPropertyType, target.TargetPropertys).ProtectedInitAsync can read the full target property.
InitTargetmounts CurrentGroup,CurrentTarget, logs, target.Forwarder, calls ProtectedInitTarget.You can cache basic information of groups/targets, but do not start a connection.
InitAsynccall ProtectedInitAsync, then AfterVariablesChangedAsync.Parsing properties, initializing caches, creating client configurations, and building variable mappings.
StartAsynccalls ProtectedStartAsync, controlled by StartTimeout. After success, set IsStarted.When connecting to an external system or starting a server listener, if a failure fails, throw an exception or return the failure state.
GetTasksCreate a target scheduling loop. By default, ProtectedExecuteAsync is called by group cycle.Most cached targets reuse parent class tasks; The server goal can be to refresh memory or check connections during execution.
Initial SnapshotAfter the target is launched, the forwarding manager pushes the current snapshot to the group production target.Don't assume you have to wait for the next variable change before data comes in.
Variable/Device/Alarm/Event ChangesBased on UsesGroupDataProducer AcceptProduced* or On*Changed.Cached targets should not repeatedly subscribe to global events themselves.
StopAsync/SafetyDisposeAsyncStop scheduling loops to release channels, logs, caches, clients, and servers.Connection must be closed, cache released, and subscription canceled to avoid repeated resending after restarting.

The forwarding group is the only entry point to the variable range

DataForwardGroupRuntime.RebuildVariables() generates two runtime indexes based on the group configuration.

IndexDescription
IdVariableRuntimesThe variable after parsing within the current group range, with the key being variable Id.
CollectDevicesCollection devices that are derived from variables within the current group range.

Business plugins should read data through these entry points:

entrysuitable scenario
GetVariables()traverses the visible variable of the current target, which is the most commonly used.
IdVariableRuntimesQuickly search by variable ID.
CollectDevicesGenerate device snapshots, protocol server nodes, or device connection status.
GetVariableProperty<TProperty>(variable)Read the strong-type target variable attribute of a variable under the current target.
GetVariablePropertyValue(variable, propertyName)Read a single field of the target variable property; if not configured, it falls back to the default property instance.
TryGetVariablePropertyValue(...)Used when you need to distinguish between "not configured" and "configured as null".

The target variable attribute cannot "pull" the variable into the forwarding range. When "variable has value but target hasn't been sent" appears on site, first check the forwarding group range, variable enabled within the group, trigger mode, target enabled, and target connection.

Data Entry: Direct Events and Group Production

PatternUsesGroupDataProducerData EntryPlugin Fit
Direct EventfalseOnVariableChanged, OnDeviceChanged, OnAlarmChanged, OnPluginEventChangedModbusSlave, IEC104Slave, OPC UA Server, IEC61850Server, SyncBridge.
Group ProductiontrueAcceptProducedVariableChange, AcceptProducedVariableSnapshot, AcceptProducedDeviceSnapshot, AcceptProducedAlarm, AcceptProducedPluginEventMQTT, Kafka, RabbitMQ, Webhook, ZeroMQ, historical data, real-time data, historical alarms.

cached base classes fix UsesGroupDataProducer to true. The forwarding manager unifies production data based on group trigger mode, batch mode, and maximum batch size, then calls AcceptProduced*. This way, multiple objectives won't have to perform scope parsing and batch processing separately.

Caching and Failure Semantics

DataForwardBaseWithCache Maintain independent memory queues and file caches for each model.

PropertiesDescription
CacheEnableWhether offline file caching is enabled. When closing the shutdown, the failed data is discarded semantics using at-most-once.
CacheFileMaxLengthmaximum row length per cache file; if exceeded, old data is deleted.
SplitSizebatch split size when uploading or reposting.
QueueMaxCountMemory queue limit; if exceeded, orders are placed first; if no order is possible, old data is discarded.
OnlineFilterwhether to filter offline variables.
ConcurrencyTopic: The number of concurrent uploads is implemented by the specific plugin. When the

sending method fails, the cache base class will decide whether to place the disk based on CacheEnable. The script upload target also writes the generated but unconfirmed TopicArray into the Topic outbox. After connection recovery, it ReplayTopicUploadCache first reissue the old data and then process the new data.

Topic, Script, and Upload Template

DataForwardBaseWithCacheIntervalScriptAll has handled these tasks:

CapabilityDescription
Topic TemplateThingsGateway/Variable/${DeviceName} These templates are grouped and replaced by entity attributes.
Entity scriptsvariables, devices, alarms, and plugin events can all be reshaped through dynamic model scripts.
Upload TemplateAfter configuring the content template, use ${property-name} to generate a custom payload.
List/Item by ItemIsVariableList, IsDeviceList, etc. to control whether to merge or send each item.
Dictionary uploadvariable/alarm can be converted into the dictionary structure of DeviceName -> Name -> entity.
JSON formatIndentation and ignoring null are controlled by properties.

These plugins usually only need to implement Upload(TopicArray topicArray, CancellationToken cancellationToken).

Script Upload Target Template

public sealed class MyProducer : DataForwardBaseWithCacheIntervalScriptAll
{
private readonly MyProducerProperty _properties = new();
private readonly DataForwardVariableProperty _variableProperties = new();
private MyClient? _client;
private bool _success = true;

protected override DataForwardPropertyWithCacheIntervalScript DataForwardPropertyWithCacheIntervalScript => _properties;

public override Type TargetPropertyType { get; } = typeof(MyProducerProperty);

public override Type VariablePropertyType { get; } = typeof(DataForwardVariableProperty);

public override DataForwardVariablePropertyBase VariablePropertys => _variableProperties;

public override bool IsConnected() => _success && _client?.Connected == true;

protected override async Task ProtectedInitAsync(CancellationToken cancellationToken)
{
_client = new MyClient(_properties.Endpoint, _properties.Token);
await base.ProtectedInitAsync(cancellationToken).ConfigureAwait(false);
}

protected override async Task ProtectedStartAsync(CancellationToken cancellationToken)
{
await _client!.ConnectAsync(cancellationToken).ConfigureAwait(false);
await base.ProtectedStartAsync(cancellationToken).ConfigureAwait(false);
}

protected override async ValueTask<OperResult> Upload(TopicArray topicArray, CancellationToken cancellationToken)
{
try
{
await _client!.PublishAsync(topicArray.Topic, topicArray.Payload.Memory, cancellationToken).ConfigureAwait(false);
_success = true;
return OperResult.Success;
}
catch (Exception ex)
{
_success = false;
return new OperResult(ex);
}
}

protected override async Task SafetyDisposeAsync(bool disposing)
{
if (_client != null)
await _client.DisposeAsync().ConfigureAwait(false);
await base.SafetyDisposeAsync(disposing).ConfigureAwait(false);
}
}

public sealed class MyProducerProperty : DataForwardPropertyWithCacheIntervalScript
{
[DynamicProperty("Service address")]
public string Endpoint { get; set; } = "http://127.0.0.1:8080";

[DynamicProperty("Access token")]
public string Token { get; set; } = string.Empty;
}

Protocol Server Target Template

Protocol server targets usually do not use script upload base classes, but instead maintain an external protocol memory mapping.

public sealed class MyServerTarget : DataForwardBase
{
private readonly MyServerProperty _properties = new();
private readonly MyServerVariableProperty _variableProperties = new();
private readonly ConcurrentQueue<VariableRuntime> _changedVariables = new();
private MyServer? _server;

public override DataForwardPropertyBase TargetProperties => _properties;

public override Type TargetPropertyType { get; } = typeof(MyServerProperty);

public override DataForwardVariablePropertyBase VariablePropertys => _variableProperties;

public override Type VariablePropertyType { get; } = typeof(MyServerVariableProperty);

public override bool IsConnected() => _server?.IsRunning == true;

protected override async Task ProtectedInitAsync(CancellationToken cancellationToken)
{
_server = new MyServer(_properties.BindUrl);
await base.ProtectedInitAsync(cancellationToken).ConfigureAwait(false);
}

public override async Task AfterVariablesChangedAsync(CancellationToken cancellationToken)
{
await base.AfterVariablesChangedAsync(cancellationToken).ConfigureAwait(false);
foreach (var variable in GetVariables())
{
var map = GetVariableProperty<MyServerVariableProperty>(variable);
if (map != null)
_server!.Register(map.Address, variable.DataType);
}
}

protected override async Task ProtectedStartAsync(CancellationToken cancellationToken)
{
await _server!.StartAsync(cancellationToken).ConfigureAwait(false);
await base.ProtectedStartAsync(cancellationToken).ConfigureAwait(false);
}

public override void OnVariableChanged(VariableRuntime variableRuntime, VariableBasicData variableData, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
return;
_changedVariables.Enqueue(variableRuntime);
}

protected override Task ProtectedExecuteAsync(object? state, CancellationToken cancellationToken)
{
while (_changedVariables.TryDequeue(out var variable))
{
var map = GetVariableProperty<MyServerVariableProperty>(variable);
if (map != null)
_server!.SetValue(map.Address, variable.Value);
}
return Task.CompletedTask;
}
}

ModbusSlave, IEC104Slave, OpcUaServer, and IEC61850Server all follow this model: variable changes first enter a mapping or queue maintained by the plugin, then the protocol server exposes read/write services.

Current Source Code Business Plugin Reference Matrix

The table below checks 15 business plugins based on the current source code. Before developing new goals, prioritize looking for the type closest to your reference.

PluginBase ClassTarget AttributesVariable AttributesKey Points
HisDataForwardTargetDataForwardBaseWithCacheIntervalHisDataForwardPropertyHisDataForwardVariablePropertyHistorical data entry, sampling strategies, conditional expressions, tabs, retention days.
RealDataForwardTargetDataForwardBaseWithCacheIntervalRealDataForwardPropertyNoneReal-time table overwrite, period/change trigger, database writer.
HisAlarmForwardTargetDataForwardBaseWithCacheHisAlarmForwardPropertyNoneHistorical alarm entry, alarm level filtering, cache pruning.
ModbusSlaveDataForwardBaseModbusSlavePropertyModbusSlaveVariablePropertyModbus slave station memory mapping, channel properties, variable addresses, RPC write permissions.
MqttClientProducerDataForwardBaseWithCacheIntervalScriptAllMqttClientProducerPropertyMqttClientProducerVariablePropertyMQTT client upload, topic/payload template, subscription RPC write.
MqttServerProducerDataForwardBaseWithCacheIntervalScriptAllMqttServerProducerPropertyMqttServerProducerVariablePropertyMQTT Server upload, client connection management, topic/payload templates.
ThingsBoardClientProducerDataForwardBaseWithCacheIntervalThingsBoardClientProducerPropertyThingsBoardClientProducerVariablePropertyThingsBoard Gateway protocol, telemetry upload, device connection/disconnection, RPC Write.
KafkaProducerDataForwardBaseWithCacheIntervalScriptAllKafkaProducerPropertyDataForwardVariablePropertyKafka Producer initialization, TopicArray upload, concurrency, and caching.
RabbitMQProducerDataForwardBaseWithCacheIntervalScriptAllRabbitMQProducerPropertyDataForwardVariablePropertyRabbitMQ connection, Exchange/RoutingKey, TopicArray upload.
WebhookDataForwardBaseWithCacheIntervalScriptAllWebhookPropertyDataForwardVariablePropertyHTTP Webhook, request method, header, payload template, failure buffer.
SyncBridgeDataForwardBase, IRpcDriverSyncBridgePropertySyncBridgeVariablePropertyGateway synchronization bridge, variable change queue, RPC invert proxy.
OpcUaServerDataForwardBaseOpcUaServerPropertyOpcUaServerVariablePropertyOPC UA server nodes, certificates, security policies, variable write permissions.
IEC104SlaveDataForwardBaseIEC104SlavePropertyIEC104SlaveVariablePropertyIEC104 Slave Station, Telesignal/Telemetry/Bit Array Mapping, Channel Properties.
IEC61850ServerDataForwardBaseIEC61850ServerPropertyIEC61850ServerVariablePropertyIEC61850 Server modeling, dataset, write index, object type conversion.
ZeroMQProducerDataForwardBaseWithCacheIntervalScriptAllZeroMQProducerPropertyDataForwardVariablePropertyZeroMQ Push/Pub/Dealer upload, binding mode, TopicArray, and caching.

Plugin Type References

TypesRecommended References
External Message System UploadsMqttClientProducer,KafkaProducer,RabbitMQProducer, ZeroMQProducer.
HTTP/REST pushWebhook.
Industrial protocol serverModbusSlave,IEC104Slave,OpcUaServer, IEC61850Server.
Database storageHisDataForwardTarget,RealDataForwardTarget, HisAlarmForwardTarget.
Platform-specific protocolThingsBoardClientProducer.
Gateway bridging and write-back proxySyncBridge.

Common Development Errors

ErrorsConsequencesCorrect Practices
When connecting to external systems with constructors,attributes have not yet been injected, so logs and cancel tokens are not available.Connect to ProtectedStartAsync, and client configuration to ProtectedInitAsync.
TargetProperties and TargetPropertyTypeMisalignment of form fields and runtime objects, import and export exceptions.Both always correspond to the same strong type attribute.
Treat the target variable attributes as range filteringVariable changes will not enter the target or troubleshoot direction errors.The range of variables is determined only by the forwarding group, and the target variable attributes are only mapped externally.
Cached targets subscribe to global variable eventsRepeated sending, batch processing, and trigger mode failure.After inheriting the cache base class, use AcceptProduced* and Update*Model.
Send failed but still returns successOffline cache cannot take over, data is lost and logs are misleading.Send failed Return failure OperResult.
IsConnected always returns truefrontend state and cache reissue distortion judgments.Returns based on the actual state of the client, socket, server, or channel.
Ignore CancellationTokenFreezes when stopping targets, switching redundancy, or refreshing configurations.All connections, sending, waiting, and resending pass cancel tokens.
Do not close client/serverport occupancy, duplicate connections, or file cache not released when releasing.SafetyDisposeAsync, then call the base.
Uploading templates does not release TopicArrayHigh-frequency uploads cause memory pressure.Use the parent class UpdateTopicArrays and do not bypass its release logic.

Validation Checklist

ScenarioValidation Point
Plugin DiscoveryPlugin Management and Data Forwarding Target dropdown shows plugins of the data forwarding type.
Attribute FormsTarget Properties, Variable Properties, Default Values, Certificates, Enumerations, and Import/Export are all correct.
Range AnalysisIn Manual, All, CollectDevice, CollectGroup modes, the GetVariables() quantity meets expectations.
Change triggersSingle point change can enter the target, group variable participates group triggers, update mode takes effect.
Cycle triggercycle snapshots are executed by group interval, batch mode, and maximum batch execution.
External connectionWhen the target connection fails, LastErrorMessage is clear; after recovery, reconnection or restart can be successful.
Offline CachingWhen the external system is disconnected, failed data enters the cache and is reissued batch after recovery.
Write-Back/RPCServer protocol or platform RPC write-back can be applied to the collected variable and is rejected when permission is disabled.
Restart the targetModify property saving, no duplicate subscriptions after multiple start-stops, port occupancy, or repeated sending.
Redundancy TargetsWhen redundancy is enabled, master-standby switching does not lose runtime references, and failed targets can be cleaned up.