排查手册

按现象组织。先看症状,再跳到对应小节。

连接问题

连不上

connect_tcp 返回 io_error / timeout

按顺序检查:

检查项怎么确认
IP 和端口服务端是否真的在监听?Server::local_port() 返回实际端口
防火墙目标端口是否放行
关联模式配对服务端和客户端是否都用了 local_public?
SA 地址服务端的 server.bytes 是否与客户端配置一致
连接数上限TCP 超过 max_connections(默认 16)会拒绝
会话角色SessionOptions::role 与实际服务端行为是否匹配

关联被拒绝

connect 返回 association_failed

这是业务失败,不是传输失败 —— TCP 连上了,但 CONNECT 被设备拒绝。

ConnectResponse::result含义
1密码错或未授权
2逻辑地址不匹配
3通道忙
其他见设备文档
// 必须检查外层 Result 之后,再检查业务字段
auto connected = client.connect_tcp("127.0.0.1", 6980);
if (!connected) {
    std::cerr << "transport: " << connected.error().context << '\n';   // 连不上
    return 1;
}

busy

第二个并发请求返回 busy

库采用单在途事务策略:第二个请求直接返回 busy,不入队。

// 错误:以为会排队
auto a = client.get(oad1);
auto b = client.get(oad2);   // busy

// 正确:批量读
auto both = client.get_list({oad1, oad2});

如果确实需要并发读不同设备,为每个设备各建一个 Client。

超时

request_timeout(默认 5 秒)触发
超时会关闭物理通道

线上没有 generation 字段,无法区分迟到响应。库的取舍是:超时就断开,不做"再等一会儿看看有没有迟到响应"的尝试。

这意味着超时后必须重新连接,且超时前发出的写操作可能已经在设备侧执行。

常见原因:

原因处理
记录查询数据量大调大 request_timeout
设备响应慢(首次唤醒、通信模块休眠)调大超时,或先发一个轻量请求预热
网络丢包查链路质量
批量太大,分块收齐超时减少单次属性数

读取问题

返回 Result 成功但没数据

这是最容易误判的一点。外层 Result 成功只表示"请求-响应往返完成",业务结果在字段里。

操作业务结果在哪
getObjectValue = variant<uint8_t, Data>,uint8_t 分支是 DAR
get_list每个 AttributeResult::result
get_recordRecordResult::result
set返回值本身就是 uint8_t DAR
actionActionValue::dar
// get:正确
auto value = client.get(oad);
if (!value) {
    std::cerr << "transport: " << value.error().context << '\n';
} else if (const auto dar = std::get_if<std::uint8_t>(&value.value())) {
    std::cerr << "device DAR=" << unsigned(*dar) << '\n';   // 业务失败
} else {
    const auto& data = std::get<model::Data>(value.value());  // 成功
}

DAR 速查

DAR含义常见原因
1其他错误查设备文档
2请求数据不能接受Data 类型与属性定义不匹配
3请求被拒绝权限不足、状态不允许、选择器不支持
4对象不存在设备没有这个 OI —— 最常见
5属性不存在属性号写错
6属性不可写该属性在设备点表里是只读
7属性值超出范围值超出设备允许区间
8密码错 / 未授权权限相关
9密码重复连续写保护
10无效的逻辑地址SA 配置不对
11无空间存储已满
255其他查设备文档
DAR=4 多半是"设备不支持

库里定义了某个 OI,不代表你的设备实现了它。DL/T 698.45 定义了大量对象,实际电表只实现其中一部分。

库里内置的模拟设备(示例程序)覆盖的对象也比真实电表少。写代码前先确认目标设备的点表。

读到了属性但类型不对

data.type() == model::DataType::int32   // 期望 uint16

按精确类型访问:Data::as<T>() 在类型不符时抛 std::bad_variant_access。

// 先判类型再取值
if (data.type() == model::DataType::uint16) {
    std::cout << data.as<model::UInt16>().value << '\n';
} else {
    std::cerr << "expected uint16, got " << unsigned(data.type()) << '\n';
}

常见原因:

  • 索引 0(整体数组)和索引 1(单元素)类型不同
  • 索引 2、3 是相别,1 是总量 —— 别把 B 相当成总量
  • 谐波相别在属性号(2/3/4),不在索引
  • 记录列的 OAD 与普通点位 OAD 相同,但设备可能返回聚合类型

engineering_values 报错

报错原因处理
non-numeric standard attribute该属性无数值倍率直接用原始值,不要换算
unsupported_tag该属性是位串(如状态字)读原始 BitString,库不解释位含义

无倍率属性的典型例子:200D/5(最高谐波次数)、4010(计量元件数)。这些是计数值不是测量值,用原始整数即可。

resource_limit

resource_limit: ...

四种可能:

场景调什么
记录行数过多Limits::max_elements
属性数过多Limits::max_elements
帧太长Limits::max_frame_bytes(默认 16385)
Data 树太深/太大Limits::max_depth、Limits::max_data_bytes

记录查询给了行数据却给空表头时也返回 resource_limit —— 这是防止无法解释列含义的数据被误读,属正常保护。

数据不对

数值差一个数量级

十有八九是倍率换算搞反了。原始值是整数,工程值需要乘/除以倍率:

数据原始类型倍率例
电压UInt16-12413 → 241.3 V
电流Int32-35000 → 5.000 A
有功功率Int32-160000 → 6000 W
电能UInt32-2123456 → 1234.56 kWh
频率UInt16-25000 → 50.00 Hz

用 engineering_values + decimal_text 换算,不要自己乘除:

auto numbers = standard::engineering_values(oad, data, layout);
if (numbers && !numbers.value().empty())
    std::cout << standard::decimal_text(numbers.value().front()) << '\n';

各数据类别页有完整的倍率表。

符号错了

有符号量用二进制补码。协议原码是负数时(C++ 的 uint8_t 看不到负号):

数据类型负数示例
电流Int3205 FF FF FF F4 → -12
功率Int32高位为 1 → 负
功率因数Int16FC 90 → -880 → -0.880
温度Int16低温为负

用 Int32/Int16 类型的 Value 取值,不要按 UInt* 解读。

数组长度不对

数据长度取决于
电压、电流、功率DeviceLayout::wiring(三相 3,单相 1)
电能DeviceLayout::tariff_count + 1(索引 0 是总量)
谐波DeviceLayout::harmonic_order
时间区段表电表参数 400C 的时区时段数

两个决定长度的参数要从设备读:

参数OAD决定
时区时段数400C 第 4 字段电能数组长度(费率数)
最高谐波次数200D/5/0谐波数组长度

读不到这些参数时只能猜,猜错就会数组越界或长度不符。

需量结构取错字段

需量是 Array<Structure>,每个结构两个成员:数值 + DateTimeS 发生时间。

auto readings = standard::demand_values(oad, data, layout);
if (readings && !readings.value().empty()) {
    const auto& reading = readings.value().front();
    std::cout << standard::decimal_text(reading.number) << '\n';
    // DateTimeS 是 7 字节数组,不是具名字段
    const auto& t = reading.occurred_at.value;
    std::cout << (unsigned(t[0]) << 8 | t[1]) << '-' << unsigned(t[2]) << '-' << unsigned(t[3]) << '\n';
}
Danger

DateTimeS 没有 year/month/day 字段 它是 Value<date_time_s, std::array<std::uint8_t, 7>>:

下标含义
t[0], t[1]年,2 字节大端
t[2] t[3] t[4] t[5] t[6]月 日 时 分 秒 毫秒

写成 t.year 编译不过。

库里不做时区换算、不转 time_t、不校验日期合法性(记录查询的时间校验除外)。

时间对不上

检查项说明
时区设备时间可能是 UTC 或本地时间,库不换算
FF / FFFFDateTimeS 里全 FF 表示"未指定"字段
设备时钟错先读 4000/2/0 核对,见日期时间与校时
毫秒字段冻结和事件的时间精确到毫秒,肉眼对比看不出来

串口问题

现象原因处理
完全收不到数据波特率/校验位/停止位不匹配三方都要一致
偶发丢帧帧间隔不足至少 33 位时间;检查线缆
首字节丢设备需要唤醒时间先发前导字节
长帧被截断缓冲太小调大通道预算
RS-485 收到自己发的数据方向切换时机错必须用真实 async_drain,时间估算不够

详见串口连接。

记录查询问题

现象原因处理
DAR=5用了普通 GET记录型属性必须 get_record
unsupported_service on GET同上同上
返回 0 行设备没有那天的记录正常情况,不是错误
查不到应有数据冻结时刻未到、设备时钟错先读当前时钟
序号跳变换表、格式化、存储滚动以响应实际序号推进
序号区间无结果超出设备存储范围先读最大序号
resource_limit响应空表头设备实现问题,库的保护
invalid_value on 时间时间非法记录查询要求完整合法公历时间

排查流程

遇到问题时按这个顺序走:

  1. 抓包,用 dlt698_decode 确认链路层通不通
  2. 链路层通了 → 看 APDU 的服务号和选择子对不对
  3. 请求正确 → 看响应里的逐项 DAR
  4. DAR 是 0 但值不对 → 查类型和倍率
  5. 类型对值还不对 → 查数组长度和索引语义

大部分问题在前三步就能定位。

该报告什么

向库或设备方反馈问题时,提供这些信息能大幅加快定位:

信息怎么拿
完整报文dlt698_decode 的输入输出
精确 OAD打印 model::Oad 的三个字段
期望值与实际值包括类型和倍率
Result 与逐项 DAR两者都要
设备型号和点表版本设备方提供
连接参数关联模式、SA、波特率
Limits 配置非默认值要说明

常见失败速查

错误含义
io_error传输层失败
timeout超时,通道已关闭
cancelled被取消,通道已关闭
busy已有在途事务
not_associated未完成协议关联
association_failedCONNECT 被拒绝
address_mismatch响应地址与请求不符
direction_mismatch协议角色与传输方向不符
checksum_header / checksum_frame校验和错误
trailing_data帧后有多余数据
need_more_data数据不完整
resource_limit超出资源上限
unsupported_service服务或变体未实现
unsupported_tag数据类型不支持该操作
invalid_value值非法
bad_variant_accessas<T>() 类型不符(异常,非 Result)