Skip to content

Gateway Protocol Driver Development ​

Development preview

This page is based on the driver source code at commit b1a16cbc and the project's driver development guide. The driver framework and several driver implementations genuinely exist, but the current application layer does not register configured drivers with the TagEngine (see Section 9), so bringing up a new driver requires removing that wiring blocker first.

1. Pluggable Driver Architecture ​

All industrial protocols are implemented through the unified IProtocolDriver interface, with two integration modes:

  • Static registration: the driver is compiled into the main program and registered automatically at startup.
  • Dynamic loading: the driver is compiled as a .so/.dll shared library and hot-loaded at runtime via dlopen/LoadLibrary.
cpp
class IProtocolDriver {
public:
    virtual ~IProtocolDriver() = default;

    /// Initialize the driver (establish the connection, load configuration)
    virtual bool init(const DriverConfig& config) = 0;

    /// Batch-read Tags
    virtual ReadResult readTags(const std::vector<TagConfig>& tags) = 0;

    /// Write a single Tag
    virtual WriteResult writeTag(const TagConfig& tag, const TagValue& value) = 0;

    /// Get the protocol name (e.g. "modbus", "opcua")
    virtual std::string getProtocolName() const = 0;

    /// Get the driver version
    virtual std::string getVersion() const = 0;

    /// Whether it is currently connected
    virtual bool isConnected() const = 0;

    /// Disconnect and release resources
    virtual void disconnect() = 0;
};
MethodWhen it is calledDescription
init()Gateway startup / driver loadEstablishes the connection to field devices and initializes internal state
readTags()TagEngine scheduled pollingBatch-reads the current values of a group of Tags
writeTag()Cloud command delivery / manual debuggingWrites a single Tag value to the device
getProtocolName()At registrationReturns the protocol identifier (lowercase, e.g. "modbus")
isConnected()Status checksReturns the device connection state
disconnect()Gateway shutdown / driver unloadReleases all resources

DriverConfig carries the connection parameters: TCP (host/port) or an RTU serial port (serialDevice/baudRate/parity/dataBits/stopBits), plus the generic timeoutMs (default 3000), retryCount (3), pollIntervalMs (1000), and a driver-specific extension key-value table extra. TagValue is a variant (bool/int64/uint64/double/string/bytes) with a timestamp and a quality code (0=good; see Configuration for details).

2. DriverFactory Registration Mechanism ​

DriverFactory is a thread-safe singleton factory providing registerDriver() / loadFromFile() / create() / listDrivers() and related capabilities.

Option 1: static registration macro (recommended) — use the macro in the driver's .cpp; registration happens automatically at program startup:

cpp
FASTBEE_REGISTER_DRIVER("myprotocol", MyProtocolDriver)

Option 2: dynamic loading (hot load) — loaded from a .so at runtime; the shared library must export two C functions:

cpp
extern "C" {
    IProtocolDriver* createDriver();
    void destroyDriver(IProtocolDriver* driver);
}

3. Steps to Add a New Driver ​

  1. Create src/drivers/myprotocol/MyProtocolDriver.h/.cpp and implement the IProtocolDriver interface.
  2. Export the extern "C" createDriver() / destroyDriver() factory functions.
  3. Register with the FASTBEE_REGISTER_DRIVER("myprotocol", MyProtocolDriver) macro.
  4. Add the driver's build rule in the FASTBEE_BUILD_DRIVERS section of CMakeLists.txt (add_library(myprotocol_driver SHARED ...)).
  5. Add the driver configuration to the drivers array in gateway.json, then add devices and Tags.
  6. Write unit tests under test/unit/ and verify through the debugging interfaces.

Notes for driver implementations:

  • Thread safety: readTags() and writeTag() may be called from different threads; the driver must handle synchronization internally.
  • Exception handling: the driver should catch all exceptions internally and never throw to the caller.
  • Connection management: the driver should handle disconnect/reconnect logic itself and honor the timeoutMs setting.
  • Resource release: disconnect() must release all resources (close sockets, free memory, etc.).

4. Modbus Driver ​

The most complete reference implementation (src/drivers/modbus/), based on libmodbus, supporting both RTU and TCP.

Supported function codes:

Function codeDescriptionRead/Write
01Read CoilsRead
02Read Discrete InputsRead
03Read Holding RegistersRead
04Read Input RegistersRead
05Write Single CoilWrite
06Write Single RegisterWrite
15Write Multiple CoilsWrite
16Write Multiple RegistersWrite

Data type mapping (design):

dataTypeModbus registersC++ type
bool1 bitbool (coil/discrete input)
int16 / uint161int16_t / uint16_t
int32 / uint322int32_t / uint32_t
float2float (IEEE 754 single precision)
double4double (IEEE 754 double precision)

The byte-order design supports ABCD (big-endian) / CDAB (word swap) / BADC / DCBA. Note that the current TagConfig::fromJson() does not parse the dataType, functionCode, or byteOrder fields (byte order is always the big-endian default); Tag fields are governed by the runtime parser (see the Tag section of Configuration).

5. OPC UA Driver ​

A dual-mode implementation: when FASTBEE_ENABLE_OPCUA=ON and open62541 is found, a real Client performs reads and writes; by default (OFF) it runs in Stub mode, generating simulated data from the nodeId hash.

  • Node ID format: ns=<namespace>;<type>=<identifier>, e.g. ns=2;s=Temperature (string) or ns=0;i=2258 (numeric). The Tag's nodeId field stores this string.
  • Endpoint: defaults to opc.tcp://<host>:<port> and can be overridden with extra.endpointUrl; extra.securityMode selects the security mode.
  • Type mapping: Boolean→bool, Int16/32/64→int64, UInt16/32/64→uint64, Float/Double→double, String→string.
  • After reading, scale/offset scaling is applied; Tags that fail to read are marked with the BAD quality code while the remaining Tags continue to be processed.

6. BACnet Driver ​

Currently a Stub implementation: the interface is defined and awaits integration of the BACnet Stack. Design highlights:

  • Data points are identified by "object type + instance number + property ID": objectType (0=analog-input, 3=binary-input, etc.), instanceId, propertyId (85=present-value is the most common).
  • Supports both BIP (BACnet/IP, default port 47808) and MS/TP (RS485 serial) links.
  • Device discovery uses Who-Is / I-Am; reads use ReadProperty request-response.

7. Siemens S7 Driver ​

Supports Ethernet communication with S7-200/300/1200/1500 series PLCs (default port 102). Dual mode: define HAS_SNAP7 and link snap7 for real communication; the default is Stub simulation.

Address formats:

FormatDescriptionExample
DB<n>.DBW<offset>Data block word (2 bytes)DB1.DBW0
DB<n>.DBD<offset>Data block double word (4 bytes)DB1.DBD4
DB<n>.DBX<byte>.<bit>Data block bitDB1.DBX0.0
MB<n> / M<byte>.<bit>Flag byte/bitMB0 / M0.0
I<byte>.<bit> / Q<byte>.<bit>Input/output bitI0.0 / Q0.0

8. DLT645 Meter Driver ​

Supports both the DLT645-1997 and DLT645-2007 versions of the Chinese meter protocol, with a self-developed frame format implementation (68H + address(6B BCD) + 68H + control code + length + data(+0x33) + checksum + 16H).

Common data items:

Tag IDData item2007 encodingUnitScale
FE_totalTotal forward active energy0x00010000kWh0.01
V_APhase A voltage0x02010000V0.1
I_APhase A current0x02020000A0.001
P_totalInstantaneous active power0x02030000kW0.0001

Serial parameters default to 2400 baud; extra.meterAddress configures the meter address and extra.version selects the protocol version. The real serial link is still being brought up, so treat it as Stub-bounded by default.

9. IEC 60870-5-104 Driver ​

A power telecontrol protocol over TCP (port 2404), event-driven (general interrogation + spontaneous reporting) rather than polling. Currently a Stub: the frame format and 28 typical substation data points are complete, awaiting integration of lib60870 (the HAS_LIB60870 macro enables real mode).

Supported type identifications (partial):

TypeIDNameDescription
M_SP_NA_1 (1)Single-point informationSwitch positions, alarm signals
M_ME_NC_1 (13)Measured value, short floating pointVoltage/current/power
M_IT_NA_1 (15)Integrated totalsEnergy meter readings
C_SC_NA_1 (45)Single commandSwitch open/close
C_IC_NA_1 (100)Interrogation commandFetch the full data set

Tag ID naming rule: yx_NNNN for remote signalling, yc_NNNN for remote measurement, ym_NNNN for remote pulse, yk_NNNN for remote control (NNNN is the IOA information object address).

10. Implemented Driver List and Current Wiring Status ​

DriverDirectoryStatus
Modbus RTU/TCPsrc/drivers/modbus/Complete: full libmodbus function codes, batch-read optimization
OPC UAsrc/drivers/opcua/Dual mode: real open62541 implementation / Stub
Siemens S7src/drivers/s7/Dual mode: real Snap7 implementation / Stub
DLT645src/drivers/dlt645/Frame format complete; serial link bring-up in progress
IEC 60870-5-104src/drivers/iec104/Stub: frame format + 28 data points
BACnet IPsrc/drivers/bacnet/Stub: interface defined
GPIOsrc/hal/GpioManagerComplete: DI/DO/AI, based on libgpiod

Current wiring status (the single most important fact for driver developers): GatewayApp::initDrivers() only loads driver configurations and records their count; it does not call DriverFactory::create() to create instances, nor tagEngine_->registerDriver() to register them. Therefore, even if a new driver compiles and registers successfully, the acquisition engine will never invoke it — before joint debugging, the application layer must first complete the wiring of "read configuration → create driver → init → register with TagEngine", which is one of the core development tasks of the project at this stage.

11. Driver Health Monitoring and Reconnection ​

All drivers have their health managed centrally by DriverHealthMonitor, with a 6-state state machine DISCONNECTED → CONNECTING → CONNECTED → RECONNECTING → FAILED → ISOLATED: when consecutive errors reach maxConsecutiveErrors (default 10) the driver enters FAILED; a persistent timeout moves it to ISOLATED; afterwards it automatically attempts recovery every autoRecoverMs (default 60 seconds). The health summary is exposed via GET /api/drivers/health.

ExponentialBackoff provides standardized reconnect delays (base 1s, cap 60s, ×2 backoff, ±20% jitter), shared by the MQTT client and driver reconnections.

12. Tag Aggregation and Engineering Value Conversion ​

  • TagAggregator: sliding-window aggregation over Tags (MAX/MIN/AVG/TOTAL/DELTA/RATE); results are queried via GET /api/aggregator/rules/:id/result. There is currently no API entry point for creating rules.
  • TagScaling: raw value ↔ engineering value conversion. LINEAR (engineering value = raw value × scale + offset) supports reverse conversion (engineering value → raw value when issuing control commands); SQRT (√raw value × sqrtFactor) is used for differential-pressure flowmeter scenarios.

13. Unit Testing ​

Tests for a new driver go under test/unit/ and use GoogleTest; when real hardware is not required, inject data through simulated backends/Stubs:

bash
cd build
cmake .. -DFASTBEE_BUILD_TESTS=ON -DFASTBEE_BUILD_DRIVERS=ON
cmake --build . -j"$(nproc)"
ctest --output-on-failure
# Run specific test cases
./bin/gateway_unit_tests --gtest_filter="MyProtocolDriver*"

The project currently contains 585 test cases (555 unit + 30 integration); a new driver must not introduce regressions.

FastBee | fastbee.cn