串口连接

最简形式

#include <dlt698/app.hpp>
#include <iostream>

int main() {
    using namespace dlt698;
    app::ClientOptions options;
    options.protocol.server.bytes = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
    app::Client client(options);
    // Windows 用 COM 名,POSIX 用设备路径。
    auto connected = client.open_serial("COM3", 9600);
    if (!connected) {
        std::cerr << connected.error().context << '\n';
        return 1;
    }
    auto value = client.get({0x200F, 2, 0});
    // ... 检查 value
    return client.disconnect() ? 0 : 1;
}

服务端对应 server.start_serial("COM4", 9600)。串口一端接采集终端,另一端接你的程序。

字格式

open_serial(path, baud) 用的是默认 8E1。需要非默认参数时用完整重载:

transport::SerialOptions serial;
serial.baud_rate = 2400;
serial.data_bits = 8;
serial.parity = transport::SerialParity::even;      // none / odd / even
serial.stop_bits = transport::SerialStopBits::one;  // one / one_point_five / two
serial.flow_control = transport::SerialFlowControl::none;  // none / software / hardware

transport::SerialLinkOptions link;
link.baud_rate = 2400;
link.bits_per_character = 11;  // 1 起始 + 8 数据 + 1 校验 + 1 停止
link.set_transmit = [](bool sending) { /* 切 RS-485 方向 */ return Result<void>{}; };
link.async_drain = [](IChannel::WriteHandler done) { /* 真实排空后回调 done */ };

auto connected = client.open_serial("COM3", serial, link,
                                    app::ConnectionProfile::local_public);

bits_per_character 含起始、数据、校验和停止位。它影响帧间隔计算,必须和实际字格式一致:8E1 是 11 位,8N1 是 10 位,8E2 是 12 位。填错会导致帧间隔估算偏差。

one_point_five 停止位会被拒绝,返回配置错误。

帧间隔和 FE

DL/T 698.45 的串行链路有两个要求:每帧前加四个 FE(前导字节 0xFE),相邻帧之间保留至少 33 位时间的间隔。open_serial 和 start_serial 内部的 SerialLinkChannel 会自动处理,不需要自己加。

RS-485 方向切换

半双工 RS-485 需要在发送和接收之间切换方向。两种模式:

自动方向适配器(推荐用于 USB 转 RS-485) —— 硬件自己控制方向,程序不需要配置:

transport::SerialLinkOptions link;  // 不设置 set_transmit

此时 SerialLinkChannel 只按字节数和波特率估算发送时间。这个估算不保证硬件时序,USB 转串口芯片的内部缓冲和操作系统的排队都会引入延迟。现场可靠性要求高时,需要用带自动方向控制的适配器并实测。

手动方向切换 —— 程序控制 DE 脚,必须提供真实排空回调:

transport::SerialLinkOptions link;
link.set_transmit = [&](bool sending) -> Result<void> {
    // sending=true 切到发送,false 切到接收。幂等。
    return Result<void>{};
};
link.async_drain = [&](transport::IChannel::WriteHandler done) {
    // 必须等最后一个停止位真正发出去(通常是 DE 脚实际落下)再回调,
    // 不能只是"调用了 write"或"数据拷进了驱动缓冲区"。
    drain_hardware(done);
};
手动方向切换缺

async_drain 会在构造时失败 配置了 set_transmit 却没有 async_drain,SerialLinkChannel::wrap 抛 std::invalid_argument。这不是可以忽略的警告——没有真实排空就切回接收,最后一个字节会被丢掉,表现为间歇性的帧错误和 CRC 失败。

为什么必须真实排空:async_write 成功只表示字节全部交给了驱动,不代表物理线路已经发完。如果不等最后停止位发出就切方向,对端收到的帧被截断,表现为 FCS 校验失败。这类故障时好时坏,很难定位。

关联模式

串口默认 local_public:不发 LINK,直接 CONNECT。本地表计一般不需要远程登录。

// 需要 LINK 登录的串口设备
client.open_serial("COM3", 9600, app::ConnectionProfile::remote_public);
// 已确认预设关联
client.open_serial("COM3", 9600, app::ConnectionProfile::local_preset);

判断依据和 TCP 一样,见TCP 连接的三种关联模式。本地 RS-485 按标准通常具有预连接通道,很多表计既不发 LINK 也不需要 CONNECT。

通道预算

transport::ChannelOptions channel;
channel.read_chunk_bytes = 4096;          // 单次读取缓冲,1~1 MiB
channel.max_pending_write_bytes = 1024 * 1024;
channel.max_pending_writes = 128;
serial.channel = channel;

read_chunk_bytes 是接收缓冲大小,和协议帧长度无关。一帧可能被拆成多次读取,也可能一次读取包含多帧,流解析由库处理。

写入预算是背压保护。发送速度快于对端响应时,写队列满会返回 resource_limit。

现场排查

打不开串口。 确认端口号正确、设备未被其他程序占用、当前用户有权限、串口确实存在(Windows 上设备管理器确认 COM 号;Linux 上 /dev/ttyUSB* 路径和拨号用户)。

能打开但收不到任何字节。 先确认波特率和字格式匹配。RS-485 的 A/B(D+/D-)接反也很常见,表现为完全无响应。

收到数据但校验失败。 优先怀疑方向切换时序(没有真实排空)、字格式不符(校验位错)、或 bits_per_character 与实际不一致。

偶发 CRC 失败、长报文更容易失败。 典型的方向切换时序问题或线路干扰。缩短线路、降低波特率、加屏蔽,或者改成带自动方向控制的适配器。

request_timeout 要放宽。 串口比 TCP 慢得多。2400 波特率下,一个 300 字节的记录响应需要好几秒。

options.protocol.request_timeout = std::chrono::milliseconds{15000};

操作系统同步打开串口的耗时不受 transport_timeout 控制。 open_serial 是同步调用,打开端口的耗时取决于驱动和设备,不归这个 5 秒时限管。

下一步