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/.dllshared library and hot-loaded at runtime viadlopen/LoadLibrary.
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;
};| Method | When it is called | Description |
|---|---|---|
init() | Gateway startup / driver load | Establishes the connection to field devices and initializes internal state |
readTags() | TagEngine scheduled polling | Batch-reads the current values of a group of Tags |
writeTag() | Cloud command delivery / manual debugging | Writes a single Tag value to the device |
getProtocolName() | At registration | Returns the protocol identifier (lowercase, e.g. "modbus") |
isConnected() | Status checks | Returns the device connection state |
disconnect() | Gateway shutdown / driver unload | Releases 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:
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:
extern "C" {
IProtocolDriver* createDriver();
void destroyDriver(IProtocolDriver* driver);
}3. Steps to Add a New Driver
- Create
src/drivers/myprotocol/MyProtocolDriver.h/.cppand implement theIProtocolDriverinterface. - Export the
extern "C" createDriver() / destroyDriver()factory functions. - Register with the
FASTBEE_REGISTER_DRIVER("myprotocol", MyProtocolDriver)macro. - Add the driver's build rule in the
FASTBEE_BUILD_DRIVERSsection ofCMakeLists.txt(add_library(myprotocol_driver SHARED ...)). - Add the driver configuration to the
driversarray ingateway.json, then add devices and Tags. - Write unit tests under
test/unit/and verify through the debugging interfaces.
Notes for driver implementations:
- Thread safety:
readTags()andwriteTag()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
timeoutMssetting. - 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 code | Description | Read/Write |
|---|---|---|
| 01 | Read Coils | Read |
| 02 | Read Discrete Inputs | Read |
| 03 | Read Holding Registers | Read |
| 04 | Read Input Registers | Read |
| 05 | Write Single Coil | Write |
| 06 | Write Single Register | Write |
| 15 | Write Multiple Coils | Write |
| 16 | Write Multiple Registers | Write |
Data type mapping (design):
| dataType | Modbus registers | C++ type |
|---|---|---|
bool | 1 bit | bool (coil/discrete input) |
int16 / uint16 | 1 | int16_t / uint16_t |
int32 / uint32 | 2 | int32_t / uint32_t |
float | 2 | float (IEEE 754 single precision) |
double | 4 | double (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) orns=0;i=2258(numeric). The Tag'snodeIdfield stores this string. - Endpoint: defaults to
opc.tcp://<host>:<port>and can be overridden withextra.endpointUrl;extra.securityModeselects the security mode. - Type mapping: Boolean→bool, Int16/32/64→int64, UInt16/32/64→uint64, Float/Double→double, String→string.
- After reading,
scale/offsetscaling is applied; Tags that fail to read are marked with theBADquality 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:
| Format | Description | Example |
|---|---|---|
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 bit | DB1.DBX0.0 |
MB<n> / M<byte>.<bit> | Flag byte/bit | MB0 / M0.0 |
I<byte>.<bit> / Q<byte>.<bit> | Input/output bit | I0.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 ID | Data item | 2007 encoding | Unit | Scale |
|---|---|---|---|---|
FE_total | Total forward active energy | 0x00010000 | kWh | 0.01 |
V_A | Phase A voltage | 0x02010000 | V | 0.1 |
I_A | Phase A current | 0x02020000 | A | 0.001 |
P_total | Instantaneous active power | 0x02030000 | kW | 0.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):
| TypeID | Name | Description |
|---|---|---|
| M_SP_NA_1 (1) | Single-point information | Switch positions, alarm signals |
| M_ME_NC_1 (13) | Measured value, short floating point | Voltage/current/power |
| M_IT_NA_1 (15) | Integrated totals | Energy meter readings |
| C_SC_NA_1 (45) | Single command | Switch open/close |
| C_IC_NA_1 (100) | Interrogation command | Fetch 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
| Driver | Directory | Status |
|---|---|---|
| Modbus RTU/TCP | src/drivers/modbus/ | Complete: full libmodbus function codes, batch-read optimization |
| OPC UA | src/drivers/opcua/ | Dual mode: real open62541 implementation / Stub |
| Siemens S7 | src/drivers/s7/ | Dual mode: real Snap7 implementation / Stub |
| DLT645 | src/drivers/dlt645/ | Frame format complete; serial link bring-up in progress |
| IEC 60870-5-104 | src/drivers/iec104/ | Stub: frame format + 28 data points |
| BACnet IP | src/drivers/bacnet/ | Stub: interface defined |
| GPIO | src/hal/GpioManager | Complete: 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:
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.

