Skip to main content

Collection Plugin Development

This document is for developers extending ThingsGatewayRuntime collection protocols. A collection plugin reads PLCs, instruments, sensors, supervisory systems, or virtual data sources into runtime variables and, when permitted, writes external commands or RPC requests back to field devices.

To extend data forwarding, protocol servers, database writers, or cloud integrations, see Business Plugin Development.

Source Code Entry

EntryFunction
ThingsGatewayRuntime.Application/Driver/IDriver.csAll devices drive the runtime interface.
ThingsGatewayRuntime.Application/Driver/DriverBase.csDevice plugin lifecycle, logs, task scheduling, channel mounting, release logic.
ThingsGatewayRuntime.Application/Driver/Collect/CollectBase.csCollects the core base class of the plugin, responsible for variable packaging, scheduled reads, script variables, method variables, and write/RPC workflows.
ThingsGatewayRuntime.Application/Driver/Collect/CollectFoundationBase.csA master protocol template based on Foundation IDevice.
ThingsGatewayRuntime.Application/Driver/Collect/CollectReceivedFoundationBase.csBased on Foundation IReceivedDevice passive receiver class protocol template.
ThingsGatewayRuntime.Application/Task/Collect/DeviceManage/DeviceThreadManage.csDevice plugin creation, property injection, channel initialization, Scheduling entry for starting task cycles.
ThingsGatewayRuntime.Application/Service/Plugin/PluginService.csscans CollectBase Derived classes and generate a list of plugins.
ThingsGatewayRuntime.Plugin/Plugin/*Implemented as an open-source collection plugin.
ThingsGatewayRuntime.NOAOTPlugin/Plugin/*Non-AOT collection plugin implementations, such as OPC UA and OPC DA.
ThingsGatewayRuntimePRO/src/ThingsGatewayRuntime.NOAOTPROPlugin/Plugin/*Professional Edition Collection Plugin implementation.

Process Overview

Runtime processing of the collection plugin in the following order:

  1. PluginService scans all non-abstract CollectBase derived classes to form a plugin list.
  2. When the device starts, DeviceThreadManage.CreateDriver create instances using the full plugin name.
  3. DriverBase.InitDevice mounts DeviceRuntime, logs, and device.Driver.
  4. PluginServiceUtil.SetDriverProperties Writes the device plugin attribute dictionary back to the strong-type property object.
  5. DriverBase.InitChannelAsync DriverBase.InitChannelAsync and call AfterVariablesChangedAsync package variables.
  6. DriverBase.StartAsync Call the plugin's ProtectedStartAsync, then create and start TaskSchedulerLoop.
  7. CollectBase Read at variable intervals, retry on failure, go online after success, offline after failure.
  8. External write/RPC enters InvokeWriteAsync or InvokeMethodAsync, and the plugin drops to the protocol write.
  9. When stopping the device, call StopAsync, eventually entering SafetyDisposeAsync release channels, underlying protocol objects, logs, and locks.

Plugin Discovery Rules

Acquisition plugins must meet these conditions to appear in the collection device configuration.

RulesExplanation
Inheritance CollectBasePluginService only recognizes non-abstract derivative classes of CollectBase as collection plugins.
Exposing the parameterless constructorCreate instances at runtime via Activator.CreateInstance; No parameterless construct will fail to start.
Do not connect to the field device in the constructorThe constructor does not yet have device properties, channels, logs, and cancel tokens. Connections should be placed in InitChannelAsync or ProtectedStartAsync.
The full name of the plugin is the configuration keyThe device entity stores type FullName. Renaming the namespace or class name affects the old configuration.
[DisplayNonePlugin] hides the pluginMemoryDriver uses this tag as an internal memory device template and is not displayed as a regular field plugin.
[OnlyWindowsSupport] will restrict platformplugins with this property will not be displayed in non-Windows environments.
NOAOT assembly does not support AOTPluginService Supports SupportsAot, OPC/COM according to the assembly name label and similar plugins are usually located in the NOAOT assembly.

Base Class Selection

Base ClassSuitable ScenarioMust Be PrioritizedExisting References
CollectFoundationBaseProtocol can be abstracted into byte read/write bytes by address, such as Modbus, S7, DLT645, Omron, Melsec.FoundationDevice, CollectProperties, DriverPropertyType, InitChannelAsync, ProtectedLoadSourceReadAsync. Usually, you don't need to rewrite ReadSourceAsync.ModbusMaster, SiemensS7Master, ControlLogixMaster.
CollectReceivedFoundationBaseData is actively reported by the peer or maintained by the underlying device; normal scheduled reads do not apply.FoundationReceivedDevice, property types, reporting event mapping, and overriding AfterVariablesChangedAsync if necessary.HJ212Master, EDPF_NTMaster, KELID2008Master, LKSISMaster.
CollectBaseCustom protocols, subscription protocols, messaging protocols, virtual devices, scanners, CAN, OPC, IEC61850, ZeroMQ.ProtectedLoadSourceReadAsync,ReadSourceAsync,WriteValuesAsync,IsConnected, and rewrite if necessaryProtectedStartAsync, AfterVariablesChangedAsync.OpcUaMaster, CanMaster, IEC61850Master, MemoryDriver, MqttCollectBase.
MqttCollectBaseMQTT client/server collection, variable values are updated by topic messages.Derived classes handle connections, subscriptions, message reception, and online checks; The parent class is responsible for updating values based on variable relationships.MqttCollectClient, MqttCollectServer.

D on't inherit the base class from the higher layer just to "save code." If you can use CollectFoundationBase avoid handwriting read/write locks, retrys, and byte parsing; You must use CollectBase when you need to customize connections and subscriptions.

Attribute Model

Plugin attributes are divided into two layers: base class attributes handle common behavior, and specific attribute classes handle protocol parameters.

TypeFunction
DriverPropertyBaseAll plugin attribute root types. Only properties marked [DynamicProperty] are exposed to frontends, import/export, and runtime property injection.
CollectPropertyBaseCollects attribute root types, including internal fields such as concurrency, offline recovery interval, retry, read/write duty cycle, and write priority.
CollectPropertyRetryBaseExposure Failed Retries,Read/Write Duty Cycle, Write Priority.
CollectFoundationPropertyBaseIncrease read/write timeout,frame time,,String inverted bytes, data parsing order.
CollectFoundationPackPropertyBaseIncreases maximum packing length by.
CollectFoundationDtuPropertyBaseadds DTU ID.
CollectFoundationDtuPackPropertyBaseIncludes both the packing length and DTU ID.
CollectPropertyNonean internal plugin property without page attributes, such as MemoryDriver.

When implementing attributes, make sure that the following two members point to the same attribute type example:

private readonly MyDriverProperty _driverProperties = new();

public override CollectPropertyBase CollectProperties => _driverProperties;

public override Type DriverPropertyType { get; } = typeof(MyDriverProperty);

DriverPropertyType is used for dynamic frontend forms, Excel import/export, and property deserialization; CollectProperties is the object actually read at runtime. If they do not match, the fields shown on the page will be misaligned with the fields used by the plugin.

DynamicProperty Agreement

ProjectDescription
DescriptionThe page displays the name, which is also the default Chinese name when the localized resource is not configured.
RemarkPage prompt instructions, address format, organization, and notes that can be written.
GroupNameFrontend group name, recommended for many attributes.
ExpressionTypeIndicates script input type, commonly used for dynamic models and expression editors.
CertificatePurposeIdentifies the purpose of selecting certificates; the frontend provides dropdown options from certificate management.
Enumeration PropertiesIt is recommended to add JsonStringEnumConverter<T> or ensure the enumeration items have entered the source generation cache to avoid inconsistent import and export and JSON display.
Default ValuesNew frontend configuration will read the default value of the property instance, which must be a conservative value that can be used directly in the test environment.

Properties without [DynamicProperty] will not be saved by the page nor injected from the device property dictionary. Do not label runtime caches, connection objects, locks, and queues as dynamic properties.

Lifecycle Hooks

PhaseRuntime ActionsPlugin Development Notes
ConstructorActivator.CreateInstance Create an instance.Only initialize lightweight fields, do not read configurations, do not connect devices, and do not start threads.
InitDeviceSettings CurrentDevice, Logs, device.Driver, then call ProtectedInitDevice.To read the device's operating state, it can be accessed after ProtectedInitDevice; The attribute value has not yet been injected.
Property injectionPluginServiceUtil.SetDriverProperties Writes the [DynamicProperty][DynamicProperty].Specific protocol parameters should be read at subsequent stages rather than cached in constructors.
InitChannelAsyncSet ChannelObject, and if necessary, Channel.SetupAsync, and finally AfterVariablesChangedAsync.Foundation plugins typically reconstruct the underlying IDevice here, assign properties to underlying objects, and call InitChannel.
AfterVariablesChangedAsyncbase class rebuilding VariableSourceReads, script variables, method variables, and scheduled tasks.Variable addition, deletion, and attribute modification will all trigger. When rewriting, make sure to call base and clean up old subscriptions or mappings.
ProtectedStartAsynccalled before starting communication, controlled by StartTimeout.Connecting to remote ends, starting services, and subscribing messages can be placed here. Cancellation tokens must be respected.
ProtectedGetTasksBuilds scheduled tasks.CollectBase provides device status, online testing, and variable reading tasks. Only special protocols are rewritten.
Periodically readcall ReadSourceAsync; if failed, press RetryCount to retry.Success requires setting variable values or returning parsable bytes; Failure should return the failure result; do not accept exceptions and then fake success.
Write/RPCInvokeWriteAsync, InvokeMethodAsync.Custom writes use write locks, and write-back validation is performed if necessary.
StopAsync/SafetyDisposeAsyncStop task loops, release logs, underlying devices, event subscriptions, and locks.Event subscriptions must be canceled, socket/client/channel must be released to avoid repeated receptions after reboot.

Variable Reading Model

CollectBase.AfterVariablesChangedAsync divides enabled variables into three categories.

TypeDetection MethodOperation Mode
Theaddress of a normal source read variable is not DeviceStatus,Script, ScriptRead, and there is no OtherMethod.Enter ProtectedLoadSourceReadAsync, generate VariableSourceRead, and then the scheduled task calls ReadSourceAsync.
Script/Special Variableaddresses are DeviceStatus,Script, ScriptRead.Enter VariableScriptReads, updated by script tasks or special address logic.
Method VariableConfigured OtherMethod.Find the method based on [DynamicMethod], call the method on read, and pass parameters to the method at write.

ProtectedLoadSourceReadAsync is not to read data, but to organize variables into "packet reading." The Foundation plugin is usually written like this:

protected override Task<List<VariableSourceRead>> ProtectedLoadSourceReadAsync(List<VariableRuntime> deviceVariables)
{
List<VariableSourceRead> reads = new();
foreach (var group in deviceVariables.GroupBy(a => a.CollectGroup))
{
reads.AddRange(_plc.LoadSourceRead<VariableSourceRead, VariableRuntime>(
group,
_driverProperties.MaxPack,
CurrentDevice.IntervalTime));
}
return Task.FromResult(reads);
}

This approach has two benefits: the same collection group can be packaged independently, MaxPack limits the length of a single packet, and variables that maintain their own IntervalTime or the device's default interval remain active.

Read/Write Locks and Write-First

CollectBase Use AsyncReadWriteLock Manage read/write conflicts.

MechanismDescription
DutyCycleWhen writing is heavy, Read intermittently according to duty cycle to avoid long periods of writing without reading.
WritePriorityCancels waiting reads during write, allowing control commands to be sent first.
ReadSourceAsyncScheduled read to enter read lock. Custom reads should stop blocking threads for long periods.
WriteValuesAsyncThe write implementation should enter write lock, and the Foundation base class has already handled it; When directly inheriting CollectBase, you need to handle it yourself.
Checkis written successfully, the variable that enabled RpcWriteCheck will be checked back. Custom write implementations can reuse this method.

Do not perform irrelevant, time-consuming tasks in the write lock, such as waiting for external HTTP, writing large files, or starting threads. Locks only protect the critical zone for protocol read/write.

Connection and Online Status

MethodsInstructions
IsConnected()Used for frontend status, online testing, and device status checks. It must reflect the true connection status and cannot always return true unless permanently online like MemoryDriver.
ProtectedStartAsyncThe location where the service is first connected or started. If timeout is over, the startup failure will be recorded.
TestOnlineCollectReceivedFoundationBase By default, it attempts to reconnect and offlines the variable; Custom protocols can be overridden for lightweight detection.
SetDeviceStatusCollectBase Update device status based on connection status and variable availability. Special plugins can be rewritable, such as MemoryDriver permanently online.

"Equipment online" on site does not mean "every variable is online." If a read fails, CollectBase will set the read variable to offline and record the final error.

Special Addresses and Dynamic Methods

CapabilitySyntaxScenarios
Device Status VariableVariable Address Writing DeviceStatus.Treat the device's online/offline status as a regular variable for display or forwarding.
Script variableaddress writes Script or ScriptRead.Do not communicate on site; calculate values through expressions or scripts.
Dynamic MethodsMethod Marker [DynamicMethod("Notes", "Remarks").Read and write dates, call protocol-specific commands, execute object methods.
RPC writesexternal interface write variables.Enter InvokeWriteAsync or InvokeMethodAsync, then use the plugin to write to the field device.

The return value of the dynamic method must be convertible to OperResult or IOperResult<T>. After configuring OtherMethod on a variable, the base class will find the corresponding method by method name.

Foundation plugin template

CollectFoundationBase has implemented most of the fixed processes.

You need to writedescriptions
the underlying _plc fieldsusing protocol device classes in Foundation.
FoundationDevicereturns the current _plc.
InitChannelAsyncreconstruct _plc, write protocol parameters from the property object, and call _plc.InitChannel(channelObject, LogMessage), and finally call base.InitChannelAsync.
ProtectedLoadSourceReadAsyncGenerates VariableSourceRead using the address resolution/packaging capabilities of underlying devices.
Optional WriteValuesAsyncprotocol allows batch write, special data types, or rewriting when writing strings.
Optional [DynamicMethod]Exposing protocol-specific methods to variable or RPC.

ModbusMaster is the smallest Foundation template; SiemensS7Master demonstrates batch writes and dynamic methods; ControlLogixMaster demonstrates custom writes when the Foundation write path is insufficient.

Directly inheriting CollectBase templates

When directly inheriting from CollectBase, plugins must handle package reading, read, write, and connection status themselves.

[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
public sealed class MyCollectDriver : CollectBase
{
private readonly MyCollectProperty _properties = new();
private MyClient? _client;

public override CollectPropertyBase CollectProperties => _properties;

public override Type DriverPropertyType { get; } = typeof(MyCollectProperty);

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

public override ChannelTypeEnum[] SupportedChannelTypes() => [ChannelTypeEnum.Other];

public override DataTypeEnum[] SupportedDataTypes() =>
[DataTypeEnum.Boolean, DataTypeEnum.Int16, DataTypeEnum.Int32, DataTypeEnum.Float, DataTypeEnum.String];

protected override async Task ProtectedStartAsync(CancellationToken cancellationToken)
{
_client = new MyClient(_properties.Endpoint);
await _client.ConnectAsync(cancellationToken).ConfigureAwait(false);
}

protected override Task<List<VariableSourceRead>> ProtectedLoadSourceReadAsync(List<VariableRuntime> deviceVariables)
{
var reads = deviceVariables
.GroupBy(a => string.IsNullOrWhiteSpace(a.IntervalTime) ? CurrentDevice.IntervalTime : a.IntervalTime)
.Select(group =>
{
var read = new VariableSourceRead { IntervalTime = new(group.Key) };
read.AddVariableRange(group);
return read;
})
.ToList();

return Task.FromResult(reads);
}

protected override async ValueTask<OperResult<ReadOnlyMemory<byte>>> ReadSourceAsync(
VariableSourceRead sourceRead,
CancellationToken cancellationToken)
{
foreach (var variable in sourceRead.Variables)
{
var value = await _client!.ReadAsync(variable.RegisterAddress!, cancellationToken).ConfigureAwait(false);
variable.SetValue(value, DateTime.UtcNow);
}
return new OperResult<ReadOnlyMemory<byte>>();
}

protected override async ValueTask<Dictionary<string, OperResult>> WriteValuesAsync(
Dictionary<VariableRuntime, JsonElement> writeInfoLists,
CancellationToken cancellationToken)
{
using var writeLock = await ReadWriteLock.WriterLockAsync().ConfigureAwait(false);
var results = new Dictionary<string, OperResult>();

foreach (var (variable, value) in writeInfoLists)
{
results[variable.Name] = await _client!.WriteAsync(
variable.RegisterAddress!,
value.GetObjectFromJsonElement(),
cancellationToken).ConfigureAwait(false);
}

return results;
}

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

public sealed class MyCollectProperty : CollectPropertyRetryBase
{
[DynamicProperty("Connection address", Remark = "For example, 127.0.0.1:12345")]
public string Endpoint { get; set; } = "127.0.0.1:12345";
}

Current Source Code Collection Plugin Reference Matrix

The current source tree contains 109 collection implementations: 107 are exposed in the plugin catalog, while MemoryDriver and SyncBridgeMirrorDriver are runtime-only implementations hidden by DisplayNonePlugin. The table below is a representative matrix covering the main base classes, communication shapes, and lifecycle patterns rather than a complete plugin list. Before developing a new plugin, first choose the implementation closest to the required protocol shape.

PluginBase ClassAttribute ClassKey Reference
MemoryDriverCollectBaseCollectPropertyNoneMemory variables, expression triggers, global variable change events, no real channels.
SyncBridgeMirrorDriverCollectBaseCollectPropertyNoneHidden mirror container, inbound push updates, peer-session online state, and reverse-RPC boundaries.
ModbusMasterCollectFoundationBaseModbusMasterPropertyFoundation standard templates, DTU, site numbers, and maximum packing length.
SiemensS7MasterCollectFoundationBaseSiemensS7MasterPropertyS7 address packaging, batch write, and date/time dynamic methods.
Dlt645_2007MasterCollectFoundationBaseDlt645_2007MasterPropertyMeter address, password, operator code, precursor symbol.
Dlt645_1997MasterCollectFoundationBaseDlt645_1997MasterPropertyDL/T 645-1997 twelve-digit meter addresses, preamble bytes, DTU routing, and protocol data parsing.
OpcUaMasterCollectBaseOpcUaMasterPropertySubscribe/ Polling mixing, certificates, security policies, node type loading, subscription refresh.
OpcDaMasterCollectBaseOpcDaMasterPropertyWindows/COM OPC DA, subscription groups, reconnection check, server-side time.
MqttCollectClientMqttCollectBaseMqttCollectClientPropertyMQTT client connection, topic subscription, message mapping variables.
MqttCollectServerMqttCollectBaseMqttCollectServerPropertyMQTT server listening, client validation, message mapping variables.
GatewayMqttCollectClient / GatewayMqttCollectServerGatewayMqttCollectBaseGatewayMqttCollectClientProperty / GatewayMqttCollectServerPropertyFixed ThingsGateway protocol, one shared session, multiple RemoteKey sources, snapshots, variable synchronization, and reverse RPC.
CanMasterCollectBaseCanMasterPropertyCAN/CAN FD endpoints, frame assembly, hardware filtering, and full-frame slice reading.
CustomPacketMasterCollectFoundationBaseCustomPacketMasterPropertyStrongly typed complete-packet configuration, framing, response matching, checksums, read/write templates, and controlled raw debugging.
ControlLogixMasterCollectFoundationBaseControlLogixMasterPropertyAllenBradley CIP, Foundation package read, custom write.
PCCCMasterCollectFoundationBasePCCCMasterPropertyAllenBradley PCCC, Foundation read-write template.
DCONMasterCollectFoundationBaseDCONMasterPropertyDCON protocol, DTU, and packaging templates.
EDPF_NTMasterCollectReceivedFoundationBaseEDPF_NTUdpPropertyUDP passive reception, reconstructing the map when variables are refreshed, without active read/write implementation.
HJ212MasterCollectReceivedFoundationBaseHJ212MasterPropertyEco-friendly HJ212 reporting, address description, and reporting mapping.
IEC61850MasterCollectBaseIEC61850MasterPropertyMMS read/write, RCB reporting, GOOSE, SOE, TLS, complex subscription lifecycle.
InovanceMasterCollectFoundationBaseInovanceMasterPropertyInovance protocol, inheriting Foundation DTU packaged properties.
KELID2008MasterCollectReceivedFoundationBaseKELID2008MasterPropertyReporting Protocol, Address Description Extension, Active Read and Write Not Implemented.
LKSISMasterCollectReceivedFoundationBaseLKSISPropertyUDP reporting protocol, maintains receive mapping after variable changes.
Mc1E_BinaryMasterCollectFoundationBaseMc1E_BinaryMasterPropertyMitsubishi 1E binary, Foundation package template.
Mc3E_BinaryMasterCollectFoundationBaseMc3E_BinaryMasterPropertyMitsubishi 3E binary, Foundation package template.
ModbusC1MasterCollectFoundationBaseModbusC1MasterPropertyModbus C1, DTU packaging template.
ModbusC20MasterCollectFoundationBaseModbusC20MasterPropertyModbus C20, DTU packaging template.
OmronFinsMasterCollectFoundationBaseOmronFinsMasterPropertyOmron FINS, Foundation Packaging Template.
OpcAeMasterCollectBaseOpcAeMasterPropertyOPC AE event collection, event subscription conversion volume.
SECSMasterCollectReceivedFoundationBaseSECSMasterPropertySECS/GEM features both receiving devices and active read/write capabilities.
TIANXINMasterCollectFoundationBaseTIANXINMasterPropertyTianxin Instrument Class Protocol, DTU Packaging Template.
TS550MasterCollectFoundationBaseTS550MasterPropertyTS550 protocol, Foundation universal read/write properties.
USBScanerCollectBaseUSBScanerPropertyLocal scanner Hook, writes variables during scanning, does not perform active read/write.
VigorMasterCollectFoundationBaseVigorMasterPropertyVigor protocol, DTU packaging template.
ZeroMQCollectClientCollectBaseZeroMQCollectClientPropertyZeroMQ subscription/connection, topic prefix, high watermark, reconnection, and cleanup.

Common Development Errors

ErrorsConsequencesCorrect Practices
Constructor to read attributes or connect devicesAttributes haven't been injected yet, and logs and cancel tokens don't exist.The constructor only initializes fields, and the connection is placed in InitChannelAsync or ProtectedStartAsync.
CollectProperties and DriverPropertyType are inconsistentmisaligned page fields, import/export, and runtime read.Both point to the same strong attribute class.
After rewriting InitChannelAsync, do not call basevariables will not be packaged, and scheduled tasks will have no data source.After completing the initialization of the underlying object, call base.InitChannelAsync.
Not cleaning up old subscriptions after variable changesRepeated receipt, rewriting variables, memory leaks.Remove the old mapping first in AfterVariablesChangedAsync or release flow.
Read fails but still returns a successThe variable appears online but has an incorrect value.Returns the failed OperResult, offlined, and records the error.
Ignore CancellationTokenFreezes when stopping or restarting the device.All network, serial port, wait, and retry pass cancellation tokens.
Write without a write lockRead and write concurrent conflicts, with on-site protocol packets crossing.Customize WriteValuesAsync Use ReadWriteLock.WriterLockAsync.
Do not cancel event subscriptions upon releaseThe same data is processed multiple times after restarting.SafetyDisposeAsync.

Validation Checklist

Verify at least these scenarios after development is complete.

SceneVerification Point
Plugin DiscoveryPlugin Management shows plugins and selects them by dropping down the collection device , plugin type is collection.
Properties formAll [DynamicProperty] fields display, default values, enumerations, certificate dropdowns, import/export, all are correct.
Channel TypeSupportedChannelTypes() Consistent with the on-site connection method; If there is no normal channel, return Other.
Data TypesSupportedDataTypes() Do not expose types that the protocol cannot resolve.
Single Point ReadUse device debugging to read 3 to 5 points first, covering boolean, integer, floating-point, string, or protocol-specific types.
Batch readingpoint tables after importing and confirming that the package quantity, interval, and timeout meet expectations.
Write/RPCRead points cannot be written; writable points can be written; enabling readback verification returns an error if failed.
Disconnect and reconnectAfter disconnecting, shutting down the server, or disconnecting the serial port, the variable goes offline and can automatically reconnect after restoration.
Pause/ResumeTasks stop after pausing the device; resume collection after recovery.
Release rebootAfter multiple saves or service restarts without repeated subscriptions, port occupancy, or thread leaks.