Skip to content

驱动开发

驱动是 IoT DC3 的南向 I/O 层:它把 Modbus、OPC UA、MQTT、S7、BACnet 等异构协议设备,统一接入到平台的数据平面和命令平面。本页带你从 dc3-driver-virtual 模板派生一个新协议驱动,并讲清驱动的生命周期、读/写调度与那条"不能随手改" 的路由约束——读完你能写出一个能注册、能采集、能接受命令的驱动。

你在这里:想为一种现成驱动还不支持的协议接入设备。只想使用已有驱动,请先看 操作手册快速开始。下一步可看 命令平面 理解读写命令如何流回设备。

除非特别说明,命令都在 iot-dc3 仓库根目录执行。

驱动是什么:一个聚合了 7 个 SPI 的 Spring Boot 服务

一个驱动本质上是一个独立的 Spring Boot 服务(dc3-driver-<protocol>)。它不直接和管理中心、数据中心打交道,而是构建在 dc3-common-driver 这个 SDK 之上——SDK 负责注册、调度、RabbitMQ 收发、gRPC 调用和租户上下文,你只需要实现协议逻辑

协议逻辑通过一个入口接口暴露:DriverCustomService。它本身不声明方法,而是聚合了 7 个职责单一的 SPI 子接口,一个驱动实现这一个接口,就等于把这 7 件事都接管了:

SPI 子接口你要回答的问题
DriverLifecycle进程启动时初始化什么(initial())?每个自定义周期做什么(schedule())?
DriverProtocol怎么从设备读一个位号(read(...))?怎么写一个位号(write(...))?
DriverCommand怎么执行模板里定义的自定义命令(execute(...))?
DriverMetadataListener设备/位号元数据变更时(event(...))如何刷新本地缓存?
DriverHealth驱动整体在线态是 ONLINE / OFFLINE / FAULT / MAINTAIN?
DeviceHealth单台设备的在线态如何判断?
DriverValidator驱动/位号配置是否合法(validate*)?能否生成仿真值?

源码入口:dc3-common/dc3-common-driver/.../service/DriverCustomService.java(一行 extends 把 7 个接口拼起来)。 dc3-driver-virtual 模板把这 7 个方法都给了可运行的示例实现,是新驱动最好的起点。

术语对齐

属性(Attribute) 来自驱动 application.ymldc3.driver.*-attribute,定义"这个驱动有哪些配置项";配置(Config) 是某台设备为这些属性填的具体值,存在管理中心。驱动启动时注册属性,运行时通过 Map<String, AttributeBO> 拿到某台设备的配置值。

生命周期:注册(带重试)→ initial → schedule

驱动进程启动后,DriverInitRunnerApplicationRunner)执行一段固定的引导序列:先向管理中心注册自己和全部属性定义,注册成功后调用你的 initial() 做一次性初始化,最后由 SDK 装配定时任务(读调度、自定义调度、设备健康检查)。

注册走 gRPC,而管理中心在驱动启动时未必就绪(滚动重启、Pod 重新调度)。所以注册不是"一锤子买卖": DriverInitRunner.registerWithRetry()带上限的指数退避重试——初始 2 秒,每次翻倍,封顶 30 秒,最多 30 次;全部失败才抛异常退出。没有它,管理中心的一次短暂抖动就会把驱动拖进 CrashLoopBackOff。

进程生命周期 · DriverInitRunner(ApplicationRunner 引导序列)在线态 · EntityStatusEnum(驱动 / 设备健康,租约模型)Spring Boot 启动注册成功initial() 一次性初始化指数退避 2s→30s · ≤30 次read cron 0/30s · custom 0/5shealth 0/15s · event(...) 元数据变更重试 30 次耗尽TTL 到期续租恢复进入维护维护完成恢复任意态 · 故障上报进程启动注册中registerWithRetry()已注册dc3_driver 落库运行中schedule 定时任务注册失败退出异常 → CrashLoopBackOffinitial() 只在启动时执行一次:建连接池 / 订阅关系等重资源ONLINE (0)在线 · 租约有效OFFLINE (1)租约到期未续MAINTAIN (2)维护中FAULT (3)故障正常状态离线 / 失败态维护态起点状态迁移故障上报

源码:dc3-common/dc3-common-driver/.../init/DriverInitRunner.javaREGISTER_MAX_ATTEMPTS=30REGISTER_INITIAL_BACKOFF=2sREGISTER_MAX_BACKOFF=30s)。initial() 只在启动时跑一次,适合建连接池、订阅关系; schedule()dc3.driver.schedule.custom 的 cron 周期触发。

从模板到新驱动:四步

新驱动的工作量集中在四处:拷贝模板、改 pom.xml、改 application.yml、实现 DriverCustomService。下图是整体路径,随后逐步展开。

步骤 1 · 拷贝模板并重命名步骤 2 · 接入父 POM步骤 3 · 配置 application.yml步骤 4 · 实现协议逻辑构建 · 运行 · 注册(SDK 编排,与协议无关)cp -r + 重命名包/启动类/实现类改名继承父模块聚合拷贝重命名构建接入配置注册jarRunner 启动gRPC 注册成功30 次耗尽dc3-driver-virtual模板 · 7 个 SPI 可运行示例dc3-driver-knxKnxDriverApplicationKnxDriverCustomServiceImpldc3-driver/pom.xmlmodules 登记新模块dc3-driver-knx/pom.xml协议依赖只放本模块如 calimero-core (KNX)父模块已引入 SDK 与插件dc3-common-driver · boot-mavendc3.driver 元数据tenant · name · code · typeschedule 调度read 0/30s · custom 0/5s · health 0/15sdriver-attributehost · port (设备级)point-attributegroupAddress (位号级)DriverCustomService实现一个接口 = 接管 7 件事7 个 SPI 子接口DriverLifecycle · initial / scheduleDriverProtocol · read / writeDriverCommand · executeDriverMetadataListener · eventDriverHealth · 驱动在线态DeviceHealth · 设备在线态DriverValidator · validate / 仿真mvn clean package-pl dc3-driver-knx -amjava -jardc3-driver-knx.jarDriverInitRunnerregisterWithRetry 2s→30s ×30dc3-center-manager登记驱动 + 属性定义注册成功Driver register succeeded注册失败退出CrashLoopBackOff模板 / 配置文件Java 代码 / SDK / 服务构建与运行失败路径主链路注册失败日志: Driver register failed on attempt n/30, retrying... · code 一旦投产不可改(稳定路由标识)

第 1 步:拷贝模板并重命名

驱动模块命名用 dc3-driver-<protocol>,协议名用 kebab-case:

bash
cp -r dc3-driver/dc3-driver-virtual dc3-driver/dc3-driver-knx

然后重命名 Java 包、启动类和自定义服务实现类。模板里两个关键类是:

说明
VirtualDriverApplicationSpring Boot 启动类
VirtualDriverCustomServiceImpl协议逻辑实现入口(implements DriverCustomService

新驱动应使用协议专用命名,例如 KnxDriverApplicationKnxDriverCustomServiceImpl,避免多个驱动出现重复类名。启动类与实现类放在同一父包下,确保组件扫描能找到带 @ServiceDriverCustomService 实现:

java
@SpringBootApplication
public class KnxDriverApplication {
    public static void main(String[] args) {
        SpringApplication.run(KnxDriverApplication.class, args);
    }
}

第 2 步:接入父 POM

dc3-driver/pom.xml<modules> 中登记新模块:

xml
<modules>
  <module>dc3-driver-knx</module>
  <!-- existing modules -->
</modules>

新模块自己的 pom.xml 通常只继承驱动父模块,并添加协议库依赖:

xml
<parent>
  <groupId>io.github.pnoker</groupId>
  <artifactId>dc3-driver</artifactId>
  <version>2026.9.22</version>
</parent>

<artifactId>dc3-driver-knx</artifactId>
<packaging>jar</packaging>

<dependencies>
  <!-- 协议库,例如 calimero-core (KNX)。重协议依赖只放这里,不要塞进 dc3-common-driver -->
</dependencies>

dc3-driver 父模块已引入 dc3-common-driver(SDK)和 Spring Boot Maven Plugin,你无需重复声明。

第 3 步:配置 application.yml

dc3.driver 是驱动最重要的用户可见配置。SDK 启动时读取它并注册到管理中心,管理侧据此渲染设备和位号的配置表单。下面以 dc3-driver-virtual 的真实结构为蓝本(把 virtual 的值换成 KNX 语义):

yaml
dc3:
  driver:
    tenant: default
    name: KNX Driver
    code: KnxDriver               # 稳定路由标识,详见下文约束
    type: DRIVER_CLIENT
    remark: @project.description@

    schedule:
      read:                       # 读调度:周期采集位号值
        enabled: true
        cron: '0/30 * * * * ?'    # 每 30 秒一轮
      custom:                     # 自定义调度:驱动 schedule() 回调
        enabled: true
        cron: '0/5 * * * * ?'
    health:
      device:                     # 设备健康上报
        enabled: true
        cron: '0/15 * * * * ?'
        timeout: 45               # 设备状态租约 TTL(秒)
        timeout-unit: SECONDS

    driver-attribute:             # 驱动级属性:每个设备实例填一份
      - attribute-name: Host
        attribute-code: host
        attribute-type-flag: STRING
        default-value: localhost
        remark: KNX/IP gateway host
      - attribute-name: Port
        attribute-code: port
        attribute-type-flag: INT
        default-value: 3671
        remark: KNX/IP gateway port

    point-attribute:              # 位号级属性:每个位号填一份
      - attribute-name: Group Address
        attribute-code: groupAddress
        attribute-type-flag: STRING
        default-value: 1/0/1
        remark: KNX group address

spring:
  application:
    name: @project.artifactId@
  profiles:
    active:
      - ${NODE_ENV:dev}

logging:
  file:
    name: dc3/logs/driver/knx/${spring.application.name}.log

各属性字段速查(含义上文已说明):

字段说明
attribute-nameUI 显示名,驱动元数据约定用英文
attribute-code协议实现读取的稳定 key,例如 hostportobjectType
attribute-type-flag属性类型,AttributeTypeEnum 共 8 值:STRING / BYTE / SHORT / INT / LONG / FLOAT / DOUBLE / BOOLEAN
default-value默认值
remark说明文字,建议英文

开关字段名是 enabled

调度开关字段名为 enabledDriverScheduleServiceImpl 读取 getRead().getEnabled() / getCustom().getEnabled() / device.getEnabled(),绑定到 DriverProperties 内的 private Boolean enabled。Spring 宽松绑定不会把 enable 映射到 enabled(属于不同属性名)。dc3-driver-virtual 模板里写的是 enable,该字段实际不会生效——新驱动请用 enabled。注意 device health 的 enabled 默认为 false,需显式置 true 才启用。

属性注册的链路是:application.ymldc3.driver → SDK 解析为 RegisterBO → 经 gRPC 提交到管理中心。下图给出这条注册流的实体关系:

启动期 · 属性注册链路(配置如何变成管理中心的表单)运行期 · 读调度(数据如何出站)提供设备与配置值(跨期通道)宽松绑定组装gRPC 提交落库表单定义配置值拉取 / 事件刷新遍历设备提交任务调用ReadPointValue消费application.ymldc3.driver.*DriverPropertiesSDK 宽松绑定RegisterBO驱动元数据 + 属性定义dc3-center-manager注册入口 :8400dc3_driver /dc3_driver_attributeWeb 配置表单按属性定义渲染 · 填设备配置值DriverMetadata 缓存运行时 Map<String, AttributeBO>DriverReadScheduleJobQuartz · cron 0/30 * * * * ?DriverMetadata设备 / 位号 / 配置缓存读线程池每设备一任务read(...)DriverCustomService 协议实现RabbitMQpoint_value 队列dc3-center-data消费 · TimescaleDB 落库read() / write() 抛异常是 SDK 约定的失败信号 —— 不要静默吞异常;单个位号读取失败不应拖垮整轮采集配置 / 协议实现SDK 与中心服务调度器消息总线持久化运行时数据通道

第 4 步:实现 DriverCustomService

核心协议逻辑放在 DriverCustomService 实现里。read(...) 返回一条 ReadPointValuewrite(...) 返回 Boolean ——这是协议契约的全部对外约定(源码 DriverProtocol.java):

java
@Slf4j
@Service
public class KnxDriverCustomServiceImpl implements DriverCustomService {

    @Resource
    private DriverMetadata driverMetadata;

    @Resource
    private DriverSenderService driverSenderService;

    @Override
    public void initial() {
        // 一次性初始化:建立协议栈、连接池、订阅关系
    }

    @Override
    public void schedule() {
        // 自定义周期任务,例如周期上报设备状态(带 TTL)
        driverMetadata.getDeviceIds().forEach(deviceId ->
                driverSenderService.deviceStatusSender(
                        deviceId, EntityStatusEnum.ONLINE, 45, TimeUnit.SECONDS));
    }

    @Override
    public void event(MetadataEventDTO metadataEvent) {
        // 响应设备/位号元数据变更(ADD/UPDATE/DELETE),刷新本地缓存或订阅
    }

    @Override
    public ReadPointValue read(Map<String, AttributeBO> driverConfig,
                               Map<String, AttributeBO> pointConfig,
                               DeviceBO device,
                               PointBO point) {
        String host = driverConfig.get("host").getValue(String.class);
        Integer port = driverConfig.get("port").getValue(Integer.class);
        String groupAddress = pointConfig.get("groupAddress").getValue(String.class);

        // 执行协议读取,返回原始字符串值(示例值 "0")
        return new ReadPointValue(device, point, "0");
    }

    @Override
    public Boolean write(Map<String, AttributeBO> driverConfig,
                         Map<String, AttributeBO> pointConfig,
                         DeviceBO device,
                         PointBO point,
                         WritePointValue writePointValue) {
        // 执行协议写入;仅当设备确认写成功才返回 true
        return true;
    }
}

读/写失败不要静默吞异常

read() / write() 抛异常是 SDK 约定的失败信号——SDK 会记录日志并对 RabbitMQ 上的命令做 ack/nack。写命令失败时结果不会回显写入值( responseValue=null),这是为了避免"假成功"。单个位号读取失败也不应拖垮整轮采集。

读/写调度:数据怎么出去、命令怎么进来

驱动有两条方向相反的数据流,都由 SDK 编排,你只填协议实现。

读(出站):Quartz 的 DriverReadScheduleJobdc3.driver.schedule.read 的 cron 触发,从 DriverMetadata 缓存遍历本驱动的设备,为每台设备提交读任务(线程池),调用你的 read() 拿到 ReadPointValue,再由 SDK 经 RabbitMQ 发往数据中心。你 不需要自己写 RabbitMQ 或 gRPC 管道。

写(入站):数据中心把读/写命令经 RabbitMQ 下发到本驱动的命令队列;PointCommandReceiver 做去重、按设备加锁后,反向调用你的 read()write(),结果再发回数据中心。

① 启动注册(带重试)② 一次性初始化 · 装配调度③ 读调度 · 数据上行④ 写命令 · 命令下行与回执落库 dc3_driver(_attribute)重试: 2s→30s ×30 · 日志 n/30一次性: 连接池 / 订阅关系装配 Quartz 定时任务 (read / custom / health)TimescaleDB 落库gRPC RegisterBO · 驱动 + 属性定义注册成功initial()read cron 0/30 * * * * ?read(driverConfig, pointConfig)ReadPointValue发布 point_value投递point_command (TTL 10s)PointCommandReceiver 消费校验 · 去重 · 加锁write(...)Boolean / 异常commandResult (失败不回显值)Quartz 调度SDK 内定时器DriverCustomService协议实现(你写的部分)dc3-common-driver驱动 SDKdc3-center-manager:8400 / gRPC :9400RabbitMQpoint_value / point_commanddc3-center-data:8500 / gRPC :9500上行 point_value 与下行 point_command 均由 SDK 编排 —— 协议实现只需填 read() 与 write(),不必自写 RabbitMQ / gRPC 管道调度器协议实现SDK 与中心服务消息总线调用 / 消息返回 / 回执

入站写命令在驱动侧的处理不是裸调用 write(),而是一条带校验、去重、加锁的流水线。下图展开 PointCommandReceiver 的处理管线(含错误路径):

入站命令流水线 · PointCommandReceiver(校验 → 去重 → 加锁 → 协议调用)健康上报 · 状态租约(TTL 必须大于读/上报周期)消费加锁后调用ReadPointValue回执是 · 作废是 · 丢弃抛异常失败也回执deviceStatusSender续租投递续租到期未续RabbitMQ 命令队列point_command · 本驱动队列过期?重复?按设备加锁同设备串行执行read() / write()协议实现 · 异常 = 失败信号RabbitMQresult 队列dc3-center-data命令历史落库 · 回执过期作废expireAt = now + 10s重复丢弃同命令已处理失败回执responseValue = null · 不回显health cron / schedule()0/15 * * * * ?DriverSenderServicedeviceStatusSender · TTL 45sRabbitMQ状态事件dc3_entity_state设备状态租约保持 ONLINETTL 45s > 读周期 30s判 OFFLINE · 反复掉线TTL < 周期 → flap写命令失败不回显值(responseValue=null,避免假成功);命令有效期 10s,超时未被驱动消费即作废;设备状态 TTL 建议 ≥ 45s消息队列SDK / 中心服务协议实现判定失败 / 异常路径命令主链路

发送侧统一走 DriverSenderService(源码 DriverSenderService.java),常用方法:

方法用途
pointValueSender(PointValue) / pointValueSender(List<PointValue>)发送单条 / 批量位号值
deviceStatusSender(deviceId, status)上报设备状态(默认 TTL)
deviceStatusSender(deviceId, status, timeout, unit)上报带 TTL 的设备状态
driverAlarmSender(String)上报驱动级告警
deviceAlarmSender(deviceId, String)上报设备级告警
eventReportSender(EventReportDTO)上报设备事件
pointCommandResultSender(...) / commandResultSender(...)回执命令结果

其中 status 取值见 EntityStatusEnumONLINE(0) / OFFLINE(1) / MAINTAIN(2) / FAULT(3)

设备状态上报 TTL 必须大于读周期

设备状态以"租约"形式上报:到期未续约就判离线。TTL 必须大于状态上报/读取周期,否则设备会在两次心跳之间被判离线、反复掉线(flap)。例如读 cron 为 0/30 * * * * ?(每 30 秒),TTL 必须大于 30 秒,建议 45 秒以上;模板默认设备健康 timeout: 45 秒,留足了余量。

命名与路由:哪个标识不能改

驱动路由涉及三个标识,区分清楚能避免投产后改不动的坑:

标识来源用途
dc3.driver.codeapplication.yml驱动类型唯一编码,管理中心据此识别驱动类型
dc3.driver.service自动派生或显式覆盖驱动实例路由标识,用于 RabbitMQ 命令队列和 routing key
spring.application.nameMaven artifactId日志文件名、Actuator 元数据等

dc3.driver.code 是稳定标识,变更需迁移

dc3.driver.code 一旦投产就不能随手改。它作为 driverCode 注册到管理中心、绑定该驱动类型的全部元数据——改了等于换了一个驱动类型,已接入的设备会全部失联,必须配套数据迁移方案。(RabbitMQ 命令队列与 routing key 由 dc3.driver.service 构建,不是 code,见下表。)

构建、运行与冒烟验证

本地运行先加载环境变量(让本地 Java 进程指向 Compose 发布到 localhost 的依赖端口):

bash
source dc3/env/dev.env.sh

构建新驱动及其依赖,然后运行:

bash
mvn -s .mvn/settings.xml clean package -pl dc3-driver/dc3-driver-knx -am
bash
java -jar dc3-driver/dc3-driver-knx/target/dc3-driver-knx.jar

开发环境下驱动会自动向管理中心注册。查看驱动日志确认出现类似 Driver register succeeded 的事件即表示注册成功(注册重试时会打印 Driver register failed on attempt n/30, retrying...)。

走通黄金路径做一次端到端冒烟(HTTP 路径与字段来自网关合约,示例值标注为示例):

  1. 管理侧建驱动、模板、位号、设备,并为驱动属性 host/port、位号属性 groupAddress 填配置值。
  2. 等待一个读周期(默认 30 秒)。
  3. 取最新位号值,确认 read() 的采集已落库:
bash
# 示例:deviceId/pointId 为示例值
curl -X POST http://localhost:8000/api/v3/data/point_value/latest \
  -H 'X-Auth-Tenant: default' \
  -H 'X-Auth-Login: dc3' \
  -H 'X-Auth-Token: {"salt":"<salt>","token":"<token>"}' \
  -H 'Content-Type: application/json' \
  -d '{"deviceId": 1, "pointId": 1, "page": {"current": 1, "size": 10}}'
  1. 对可写位号下发写命令,确认 write() 被调用并回执:
bash
curl -X POST http://localhost:8000/api/v3/data/point_command/write \
  -H 'X-Auth-Tenant: default' \
  -H 'X-Auth-Login: dc3' \
  -H 'X-Auth-Token: {"salt":"<salt>","token":"<token>"}' \
  -H 'Content-Type: application/json' \
  -d '{"deviceId": 1, "pointId": 1, "value": "42"}'

接口返回 commandId,再用它查询命令历史看执行状态(PointCommandHistoryVOstatusSUCCESS/FAILED 等,写成功时 responseValue 回显写入值):

bash
curl -X GET 'http://localhost:8000/api/v3/data/point_command_history/get_by_command_id?commandId=<commandId>' \
  -H 'X-Auth-Tenant: default' \
  -H 'X-Auth-Login: dc3' \
  -H 'X-Auth-Token: {"salt":"<salt>","token":"<token>"}'

完整的命令生命周期与回执语义见 命令平面

鉴权头怎么来

所有受保护接口都需要 X-Auth-Tenant / X-Auth-Login / X-Auth-Token。token 通过 POST /api/v3/auth/token/salt 取盐、 POST /api/v3/auth/token/generate 换取(有效期 12 小时)。详见 API 文档

常见问题

问题根因与处理
驱动编码冲突dc3.driver.code 重复——保持全局唯一且稳定,不要改已投产的 code
DriverCustomService 未加载实现类缺 @Service,或不在启动类组件扫描范围内
注册一直重试不成功管理中心未就绪或 gRPC 不通——看 Driver register failed on attempt n/30 日志,确认 CENTER_MANAGER_HOST 和管理中心健康态
read 返回空或异常不要静默吞异常,让日志暴露协议错误;单点失败不应拖垮整轮采集
设备频繁离线(flap)状态 TTL 小于读/上报周期——增大 TTL 或缩短调度周期
有读取但数据页无值检查 RabbitMQ 连通性、数据中心日志和租户上下文
元数据变更不生效event(...) 中更新本地协议客户端、订阅或缓存
协议依赖很重只放具体驱动模块的 pom.xml,不要放进 dc3-common-driver

延伸阅读

  • 命令平面 — 读/写命令如何下发、去重、加锁、回执,与本页的 read()/write() 对接
  • 模块地图 — 36 个驱动模块的全貌与 dc3-common-driver SDK 在依赖树中的位置
  • 领域模型 — Profile / Point / Device 与 Param/Attribute/Config 三层的字段与边界
  • API 文档 — 鉴权流程、网关合约与 OpenAPI
  • 故障排查 — 启动依赖、端口与环境变量类问题

基于 AGPL-3.0 协议发布