Arduino UNO Q 通信机制源码跟踪

/ 0评 / 0

这段时间好好分析了一轮,发现这东西非常非常长,所以这一篇写了很久很久,大概一周多.

手工分析可能存在一些问题,原本是写在Notion的,导出到这里可能有些排版问题,同时软件不断更新的.

重点回答这几件事:

我这个分析就跟着代码走,需要读的库包含以下.

上面提到的所有库都在 https://github.com/arduino/ 可以下载到,为了更清晰,我这里每一步都写了编号.

1. 从 Bridge.provide() 开始

文件: Arduino_RouterBridge/src/bridge.h

template<typename F>
bool provide(const MsgPack::str_t& name, F&& func) {
   k_mutex_lock(&bridge_mutex, K_FOREVER);
   bool res;
   bool out = call(BIND_METHOD, name).result(res) && res && server->bind(name, func);
   k_mutex_unlock(&bridge_mutex);
   return out;
}

1.1 这个函数的两个入参是什么意思

provide() 的模板参数和函数参数分别表示:

这里为什么不用String和char[],主要是为了后续序列化.

换句话说,provide("set_led", set_led) 的语义不是"把 C++ 函数名发给 Linux",而是:

  1. 告诉 router: "我这条连接现在提供一个名叫 set_led 的 RPC 方法"
  2. 在 MCU 本地 dispatcher 上,把字符串 "set_led" 绑定到 set_led 这个回调

2. 先看 provide() 这一行里三段表达式分别是什么

还是这一行:

bool out = call(BIND_METHOD, name).result(res) && res && server->bind(name, func);

它其实是三个阶段串在一起:

  1. call(BIND_METHOD, name).result(res)
    向 router 发起一次 RPC 调用,请求注册方法名
  2. res
    这是 router 返回的业务结果,表示 router 是否接受了这次注册
  3. server->bind(name, func)
    在 MCU 本地把方法名和回调绑起来

也就是:

注意这意味着顺序不是"先本地 bind,再远端 register",而是反过来.


3. BIND_METHOD 是什么

文件: Arduino_RouterBridge/src/bridge.h

#define BIND_METHOD "$/register"

所以:

call(BIND_METHOD, name)

等价于:

call("$/register", name)

这不是调用用户方法,而是在调用 router 内建的一个控制 RPC.


4. 继续跟进 call(BIND_METHOD, name)

文件: Arduino_RouterBridge/src/bridge.h

template<typename... Args>
RpcCall<Args...> call(const MsgPack::str_t& method, Args&&... args) {
   return RpcCall<Args...>(method, client, &read_mutex, &write_mutex, std::forward<Args>(args)...);
}

4.1 这里的入参分别是什么

call(BIND_METHOD, name)

那么这里会推导成:

4.2 这个函数本身做了什么

它没有发包.

它只是构造并返回一个 RpcCall<Args...> 对象,把这些信息先存起来:

所以 Bridge.call() 本质上是"创建一个待执行的 RPC 调用对象",不是"立即同步执行".


5. 继续跟进 RpcCall(...) 构造函数

文件: Arduino_RouterBridge/src/bridge.h

RpcCall(const MsgPack::str_t& m, RPCClient* c, struct k_mutex* rm, struct k_mutex* wm, Args&&... args)
    : method(m), client(c), read_mutex(rm), write_mutex(wm),
      callback_params(std::forward_as_tuple(std::forward<Args>(args)...)) {
    k_mutex_init(&call_mutex);
    setError(GENERIC_ERR, "This call is not yet executed");
}

5.1 构造时保存了什么

也就是说,call() 返回以后,这个对象里已经带着"将来要发的那次 RPC 调用"的全部上下文.

5.2 为什么初始 error 是 "This call is not yet executed"

因为这时候请求还没真正发出去.

也就是:


6. 继续跟进 .result(res)

文件: Arduino_RouterBridge/src/bridge.h

template<typename RType> bool result(RType& result) {

    if (!atomic_cas(&_executed, 0, 1)){
        setError(GENERIC_ERR, "This call is no longer available");
        return false;
    }

    while (true) {
        if (k_mutex_lock(write_mutex, K_MSEC(10)) == 0) {
            std::apply([this](const auto&... elems) {
                client->send_rpc(method, msg_id_wait, elems...);
            }, callback_params);
            k_mutex_unlock(write_mutex);
            break;
        } else {
            k_yield();
        }
    }

    while(true) {
        if (k_mutex_lock(read_mutex, K_MSEC(10)) == 0 ) {
            RpcError temp_err;
            if (client->get_response(msg_id_wait, result, temp_err)) {
                k_mutex_unlock(read_mutex);
                setError(temp_err.code, temp_err.traceback);
                break;
            }
            k_mutex_unlock(read_mutex);
            k_msleep(1);
        } else {
            k_yield();
        }
    }

    return !isError();
}

这段是整条链路里最关键的一段.

6.1 第一件事: 保证只执行一次

if (!atomic_cas(&_executed, 0, 1))

意思是:

所以 RpcCall 是"一次性消费对象".

这也解释了为什么 README 里说 .result() 只能成功取一次结果.

6.2 第二件事: 真正发请求

client->send_rpc(method, msg_id_wait, elems...);

注意这里才第一次真正调用 RPCClient 发包.

也就是说:

6.3 第三件事: 等待响应

client->get_response(msg_id_wait, result, temp_err)

这里按消息 id 等待对应 response.

如果响应到了:

所以 call() 不是同步返回,而是 RpcCall,就是为了把:

分成两步.


7. 继续跟进 client->send_rpc(...)

文件: Arduino_RPCLite/src/client.h

template<typename... Args>
bool send_rpc(const MsgPack::str_t& method, uint32_t& wait_id, Args&&... args) {
    uint32_t msg_id;
    if (decoder->send_call(CALL_MSG, method, msg_id, std::forward<Args>(args)...)) {
        wait_id = msg_id;
        return true;
    }
    return false;
}

7.1 这里做了什么

这个 msg_id 很重要,因为后面 get_response(wait_id, ...) 要靠它对响应做匹配.


8. 继续跟进 decoder->send_call(...)

文件: Arduino_RPCLite/src/decoder.h

template<typename... Args>
bool send_call(const int call_type, const MsgPack::str_t& method, uint32_t& msg_id, Args&&... args) {

    if (call_type!=CALL_MSG && call_type!=NOTIFY_MSG) return false;

    MsgPack::Packer packer;
    packer.clear();

    if (call_type == CALL_MSG){
        msg_id = _msg_id;
        MsgPack::arr_size_t call_size(REQUEST_SIZE);
        packer.serialize(call_size, call_type, msg_id, method);
    } else {
        MsgPack::arr_size_t call_size(NOTIFY_SIZE);
        packer.serialize(call_size, call_type, method);
    }

    MsgPack::arr_size_t arg_size(sizeof...(args));
    packer.serialize(arg_size, std::forward<Args>(args)...);

    if (send(reinterpret_cast<const uint8_t*>(packer.data()), packer.size()) == packer.size()){
        _msg_id++;
        return true;
    }
    return false;
}

8.1 这里开始进入协议层

这段代码把 RPC 调用编码成 MsgPack RPC 数组.

CALL_MSG 而言,包结构是:

[0, msg_id, method, [args...]]

其中:

如果现在调用的是:

call("$/register", name).result(res)

那么发出去的逻辑包大致就是:

[0, <某个msg_id>, "$/register", ["set_led"]]

这里 set_led 只是例子,真实值取决于 name.

8.2 为什么 RPCClientRPCServer 都用同一个 decoder

文件: [Arduino_RPCLite/src/decoder_manager.h]

static RpcDecoder<>& getDecoder(ITransport& transport)

RPCClientRPCServer 都通过 RpcDecoderManager 按 transport 取同一个 RpcDecoder 实例.

这意味着:

这也是 Bridge 里两个 mutex 存在的根本原因之一.

8.3 协议可靠性约束

transport.hdecoder.h 的实现来看,这套 RPC 协议没有任何应用层可靠性机制.

具体来说:

没有帧定界/字节填充:

SerialTransport 直接 read() / write(),没有 SLIP 编码或任何帧封装.

消息边界完全依赖 MsgPack 自身的长度前缀.decoder 的 parse_packet() 通过解析 MsgPack array header 来确定一条完整消息的起止位置.

没有校验和/CRC:

没有任何消息完整性校验.串口传输如果出现比特错误,只能靠 MsgPack 反序列化失败来间接发现.

没有流控/重传:

没有 ACK/NACK,没有应用层重传,没有背压机制.send() 假设 transport 总能成功写入.

固定 buffer 上限:

文件: Arduino_RPCLite/src/decoder.h

template<size_t BufferSize = DECODER_BUFFER_SIZE>

其中 DECODER_BUFFER_SIZE = 1024.超过这个大小的单条消息会出问题.

另外 RPCRequest<> 的默认 buffer 大小是:

#define DEFAULT_RPC_BUFFER_SIZE  (DECODER_BUFFER_SIZE / 4)

即 256 字节.这意味着请求/响应的有效载荷空间其实是比较有限的.

错误恢复机制:

decoder 内部有一个 _discarded_packets 计数器.当解析失败时,包会被丢弃并计数,然后 decoder 会在 buffer 里扫描寻找下一个合法的 MsgPack 包头.

也就是说,如果串口丢字节导致 buffer 状态错位,decoder 只能"丢弃并向前扫描",不能回溯重传.

这些约束不是 bug,而是嵌入式场景下"用复杂度换确定性"的合理取舍.但它们直接影响"什么时候这套系统可能出问题"的判断.


9. 请求发出去以后,Linux 侧谁接收

router 侧入口在 [arduino-router/main.go] 和 [arduino-router/internal/msgpackrouter/router.go]

router 会把串口包装成一个 connection:

wr := &MsgpackDebugStream{Name: cfg.SerialPortAddr, Upstream: serialPort}
routerExit := router.Accept(wr)

也就是说 MCU 串口这一端,在 router 看来也是一个普通 MsgPack RPC client connection.


10. router 收到 $/register 后做什么

文件: [arduino-router/internal/msgpackrouter/router.go]

switch method {
case "$/register":
    if len(params) != 1 {
        ...
    } else if methodToRegister, ok := params[0].(string); !ok {
        ...
    } else if err := r.registerMethod(methodToRegister, msgpackconn); err != nil {
        ...
    } else {
        fwdRes(true, nil)
        return
    }

10.1 它检查什么

对于 $/register:

注册动作在:

文件: arduino-router/internal/msgpackrouter/router.go

func (r *Router) registerMethod(method string, conn *msgpackrpc.Connection) error {
    ...
    r.routes[method] = conn
    return nil
}

也就是说 router 并不知道 set_led 对应哪个 C++ 函数.

router 只知道:

这就是 Linux 侧"发现" MCU 方法的真实含义:


11. router 返回的 res 是什么

回到这一句:

call(BIND_METHOD, name).result(res)

这里的 res 是一个 bool,它接收的是 router 对 $/register 的 RPC 返回值.

当 router 成功注册时,返回:

fwdRes(true, nil)

也就是:

所以:

这两个都要成功,后面的 && res 才会继续走到 server->bind(name, func).


12. 然后才执行本地 server->bind(name, func)

文件: [Arduino_RPCLite/src/server.h]

template<typename F>
bool bind(const MsgPack::str_t& name, F&& func, MsgPack::str_t tag=""){
    return dispatcher.bind(name, func, tag);
}

继续往下:

文件: [Arduino_RPCLite/src/dispatcher.h]

template<typename F>
bool bind(MsgPack::str_t name, F&& f, MsgPack::str_t tag="") {
    if (_count >= N) return false;

    if (isBound(name)) return false;

    _entries[_count++] = {name, tag, wrap(std::forward<F>(f))};
    return true;
}

12.1 这里本地到底存了什么

dispatcher 里每个 entry 有三项:

这一步才是 MCU 本地"方法名 -> C++ 回调"的真实绑定位置.

所以 provide() 成功以后,系统中会同时存在两份状态:


13. Linux 侧以后调用 set_led 时,router 怎么处理

文件: arduino-router/internal/msgpackrouter/router.go

client, ok := r.getConnectionForMethod(method)
if !ok {
    fwdRes(nil, routerError(ErrCodeMethodNotAvailable, fmt.Sprintf("method %s not available", method)))
    return
}

err := client.SendRequestWithAsyncResult(
    fwdRes,
    method, params...)

13.1 这里说明了什么

对于普通 method:

  1. router 先查 routes[method]
  2. 找到就把请求转发到对应 connection
  3. 找不到就直接返回
method xxx not available

所以这条错误的直接含义是:

router 当前查不到这个 method 对应的连接

它首先是 router 路由表层面的错误,不是 MCU 本地 dispatcher 抛出的错误.


14. MCU 收到转发请求后,怎么挑出对应方法

在 bridge 后台线程里,会不断跑:

文件: Arduino_RouterBridge/src/bridge.h

inline void updateEntryPoint(void *, void *, void *){
    while (true) {
        if (Bridge) {
            Bridge.update();
        }
        k_yield();
    }
}

update() 的主体是:

文件: Arduino_RouterBridge/src/bridge.h

RPCRequest<> req;
if (!server->get_rpc(req)) {
    ...
    return;
}

server->process_request(req);
server->send_response(req);

继续看 get_rpc(req):

文件: Arduino_RPCLite/src/server.h

decoder->decode();

const MsgPack::str_t method = decoder->fetch_rpc_method();

if (method == "" || !dispatcher.hasTag(method, tag)) return false;

req.size = decoder->get_request(req.buffer, RpcSize);
return req.size > 0;

14.1 这里做了哪几步

  1. decoder->decode()
    从 transport 继续读串口数据,尝试解析一个完整 MsgPack 包
  2. decoder->fetch_rpc_method()
    从包头里先把 method 字符串取出来
  3. dispatcher.hasTag(method, tag)
    检查这个 method 是否存在,并且 tag 是否匹配当前这条处理通道
  4. decoder->get_request(...)
    真正把整包取出来填到 req.buffer

所以 MCU 不是"先完整解所有包,再决定谁来处理",而是:


14.5 补齐: wrap() 到底把回调变成了什么

前面 Section 12 看到 server->bind(name, func) 最终存的是:

_entries[_count++] = {name, tag, wrap(std::forward<F>(f))};

这里的 wrap(...) 做了非常精巧的事情.

文件: Arduino_RPCLite/src/wrapper.h

template<typename F, typename Signature = typename arx::function_traits<typename std::decay<F>::type>::function_type>
auto wrap(F&& f) -> RpcFunctionWrapper<Signature>* {
    return new RpcFunctionWrapper<Signature>(std::forward<F>(f));
};

14.5.1 编译期签名提取

关键在第二个模板参数:

typename arx::function_traits<typename std::decay<F>::type>::function_type

这行做的事情是:

  1. std::decay<F>::type 去掉引用和 cv 限定
  2. arx::function_traits<...> 提取函数特征(返回类型,参数类型列表)
  3. ::function_type 把它转成 std::function<R(Args...)> 的形式

所以不管你传进来的是普通函数,lambda,函数对象还是 std::function,wrap() 都能在编译期推导出它的签名.

14.5.2 RpcFunctionWrapper 是什么

文件: Arduino_RPCLite/src/wrapper.h

template<typename R, typename... Args>
class RpcFunctionWrapper<std::function<R(Args...)>>: public IFunctionWrapper {

它继承自 IFunctionWrapper:

class IFunctionWrapper {
public:
    virtual ~IFunctionWrapper() = default;
    virtual bool operator()(MsgPack::Unpacker& unpacker, MsgPack::Packer& packer) = 0;
};

这是一个类型擦除接口.

所有不同签名的回调,在 dispatcher 表里都以 IFunctionWrapper* 存储.

但每个 RpcFunctionWrapper<R(Args...)> 实例内部,仍然完整保留了原始签名信息.

14.5.3 调用时发生了什么

当 wrapper 的 operator() 被调用时(传入 unpacker 和 packer),它做了这几步:

  1. 检查参数数组大小是否匹配 sizeof...(Args)
  2. 调用 deserialize_tuple(unpacker, args) 从 MsgPack 流里反序列化出参数 tuple
  3. 调用 invoke_with_tuple(_func, args, ...) 把参数展开,调用真实回调
  4. 如果返回类型是 void:packer.serialize(nil, nil) → 回包里 result 是 nil
  5. 如果返回类型非 void:packer.serialize(nil, out) → 回包里 result 是返回值

也就是说,wrap() 把"任意签名的 C++ 回调"变成了一个统一接口:

MsgPack 字节流 → 反序列化参数 → 调用真实函数 → 序列化返回值 → MsgPack 字节流

这就是 dispatcher 表里存的东西:不是裸函数指针,而是"知道怎么和 MsgPack 对接的类型擦除包装器".


14.6 补齐: process_request()send_response() 的完整流程

回到 update() 里的调用序列:

server->get_rpc(req);       // Section 14 已分析
server->process_request(req);  // 这里展开
server->send_response(req);    // 这里展开

14.6.1 process_request(req) 做了什么

文件: Arduino_RPCLite/src/server.h

void process_request(RPCRequest<RpcSize>& req) {

    if (!req.unpack_request_headers()) {
        req.reset();
        return;
    }

    req.pack_response_headers();

    dispatcher.call(req.method, req.unpacker, req.packer);
}

分三步:

第一步: req.unpack_request_headers()

文件: Arduino_RPCLite/src/request.h

req.buffer 里的原始字节流喂给 unpacker,解出:

同时 unpacker 的读取位置停在了"参数区域"的开头,等待后续消费.

第二步: req.pack_response_headers()

文件: Arduino_RPCLite/src/request.h

如果 type == CALL_MSG,向 packer 写入 response 包头:

packer.serialize(resp_size, RESP_MSG, msg_id);

[RESPONSE_SIZE, 1, msg_id].

此时 packer 里的内容是:

[4, 1, msg_id, ???, ???]
         ^后续由 dispatcher 填入 result 和 error

如果 type == NOTIFY_MSG,不写 response header(因为 notify 不需要回复).

第三步: dispatcher.call(req.method, req.unpacker, req.packer)

文件: Arduino_RPCLite/src/dispatcher.h

bool call(const MsgPack::str_t& name, MsgPack::Unpacker& unpacker, MsgPack::Packer& packer) {
    for (size_t i = 0; i < _count; ++i) {
        if (_entries[i].name == name) {
            return (*_entries[i].fn)(unpacker, packer);
        }
    }
    // handler not found
    MsgPack::object::nil_t nil;
    packer.serialize(RpcError(FUNCTION_NOT_FOUND_ERR, name), nil);
    return false;
}

遍历表,找到 name 匹配的 entry,调用它的 wrapper operator()(unpacker, packer).

这就是 Section 14.5 分析的 RpcFunctionWrapper 被真正调用的地方.

wrapper 内部会:

如果找不到匹配的 method,直接写入 FUNCTION_NOT_FOUND_ERR 错误.

14.6.2 send_response(req) 做了什么

文件: Arduino_RPCLite/src/server.h

bool send_response(const RPCRequest<RpcSize>& req) const {

    if (req.type == NO_MSG || req.packer.size() == 0) {
        return true; // No response to send
    }

    if (req.type == NOTIFY_MSG) return true;

    return decoder->send_response(req.packer);
}

三个出口:

  1. 如果 type == NO_MSG 或 packer 是空的 → 不发(无效请求或处理失败被 reset 了)
  2. 如果 type == NOTIFY_MSG → 不发(通知不需要回复)
  3. 否则 → 调 decoder->send_response(req.packer) 把 packer 里的字节流发到串口

此时 packer 里装的是完整的 response:

[RESPONSE_SIZE, RESP_MSG, msg_id, result_or_nil, error_or_nil]

这就是 Linux 侧最终收到的 RPC 回包.

14.6.3 把执行链路压成一句话

get_rpc(req)       → 从串口读一个完整包,窥探 method 名,匹配 tag
process_request()  → 解包头,dispatcher 查表,wrapper 反序列化参数+调用回调+序列化结果
send_response()    → 把 packer 里的回包发回串口

前面笔记跟到了 get_rpc() 就停了.现在补齐后,MCU 侧的完整"收请求 → 执行 → 回包"链路就清楚了.


15. provide_safe() 为什么和线程有关

看定义:

文件: Arduino_RouterBridge/src/bridge.h

template<typename F>
bool provide_safe(const MsgPack::str_t& name, F&& func) {
    k_mutex_lock(&bridge_mutex, K_FOREVER);
    bool res;
    bool out = call(BIND_METHOD, name).result(res) && res && server->bind(name, func, "__safe__");
    k_mutex_unlock(&bridge_mutex);
    return out;
}

provide() 的唯一区别,是本地 bind 多传了一个 tag:

"__safe__"

15.1 这个 tag 后面在哪里被用到

后台线程 update() 调:

server->get_rpc(req)

默认 tag 是 "".

而主线程安全路径 update_safe() 调:

文件: Arduino_RouterBridge/src/bridge.h

if (!server->get_rpc(req, "__safe__")) {
    ...
    return;
}

update_safe() 又是从主 loop 线程的 hook 里被调:

文件: Arduino_RouterBridge/src/bridge.h

void __attribute__((weak)) __loopHook(void){
    k_yield();
    safeUpdate();
}

所以:

router 并不知道 safesafe.
这个差异完全发生在 MCU 本地 dispatcher 选路这一层.


16. method xxx not available 到底表示什么

前面已经看到,这条错误是在 router 里生成的:

文件: arduino-router/internal/msgpackrouter/router.go

client, ok := r.getConnectionForMethod(method)
if !ok {
    fwdRes(nil, routerError(ErrCodeMethodNotAvailable, fmt.Sprintf("method %s not available", method)))
    return
}

所以这条错误最准确的含义是:

router 当前没有找到这个 method 对应的 connection

它通常对应这些情况:

它不直接等价于:

如果请求已经成功路由到 MCU,但 MCU 本地 dispatcher 没找到 handler,那么本地会走:

文件: Arduino_RPCLite/src/dispatcher.h

packer.serialize(RpcError(FUNCTION_NOT_FOUND_ERR, name), nil);

这时返回的是 RPCLite 的 FUNCTION_NOT_FOUND_ERR,不是 router 的 method xxx not available.


17. 一个完整时序例子: Bridge.provide("set_led", set_led)

把前面的链路压成顺序:

  1. 用户代码调用:
Bridge.provide("set_led", set_led);
  1. provide() 先进入:
call("$/register", "set_led").result(res)
  1. call() 只构造 RpcCall
  2. .result(res) 才真正:
  1. router 收到 $/register
  1. .result(res) 在 MCU 侧收到响应
  1. provide() 继续执行:
server->bind("set_led", set_led)
  1. MCU 本地 dispatcher 保存:
name = "set_led"
tag  = ""
fn   = wrapped(set_led)

到这里,这个方法才算真正"注册完成".


18. 一个完整时序例子: Linux 调 MCU 的 set_led

  1. Linux 某 client 向 router 发:
[REQUEST, x, "set_led", [true]]
  1. router 查 routes["set_led"]
  1. router 把请求转发给 MCU
  2. MCU 的 update()update_safe() 读到包
  3. server->get_rpc(req, tag):
  1. server->process_request(req):
  1. server->send_response(req) 发回 router
  2. router 再把 response 回给最初发请求的 Linux client

19. 回到最开始的问题,给一句最短结论

provide(name, func) 的本质不是"把函数给 Linux".

它实际做的是:

  1. name 向 router 注册一个可路由的方法名
  2. func 在 MCU 本地绑定这个方法名对应的实现

所以:

call() 返回 RpcCall,是为了把"构造调用"和"真正阻塞执行"分开;
provide_safe() 则是在 MCU 本地多打一个 tag,让 method 只能在主线程那条处理路径里执行.


20. 关键文件索引


21. RpcCall 里的 client 是哪来的

前面在 provide() 里看到:

call(BIND_METHOD, name).result(res)

再跟到:

template<typename... Args>
RpcCall<Args...> call(const MsgPack::str_t& method, Args&&... args) {
   return RpcCall<Args...>(method, client, &read_mutex, &write_mutex, std::forward<Args>(args)...);
}

这里传给 RpcCallclient 不是临时创建的,而是 BridgeClass 的成员:

文件: Arduino_RouterBridge/src/bridge.h

RPCClient* client = nullptr;
RPCServer* server = nullptr;

它们是在 Bridge.begin() 里预先初始化好的:

文件: Arduino_RouterBridge/src/bridge.h

serial_ptr->begin(baud);
...
transport = new SerialTransport(*serial_ptr);

client = new RPCClient(*transport);
server = new RPCServer(*transport);

所以初始化链是:

  1. Bridge.begin()
  2. 打开 Serial1
  3. 创建 SerialTransport
  4. 用同一个 transport 创建 RPCClient
  5. 用同一个 transport 创建 RPCServer
  6. 后续 Bridge.call() 只是把这个已经存在的 client 借给 RpcCall

结论:


22. 为什么 clientserver 能共用一条串口

这点如果不展开,看 Bridge 很容易误以为:

实际上它们底层共享的是同一个 transport,对应同一个 decoder 管理器.

文件: Arduino_RPCLite/src/client.h

explicit RPCClient(ITransport& t) : decoder(&RpcDecoderManager<>::getDecoder(t)) {}

文件: Arduino_RPCLite/src/server.h

explicit RPCServer(ITransport& t) : decoder(&RpcDecoderManager<>::getDecoder(t)) {}

文件: Arduino_RPCLite/src/decoder_manager.h

static RpcDecoder<>& getDecoder(ITransport& transport)

这意味着:

这就是为什么 Bridge 里必须有:

否则:

就会直接抢同一份 decoder 状态.

从这个角度看,Bridge 更像一个"在单条双向链路上复用 client/server 语义的复合端点".


23. atomic_cas 在这里到底防什么

文件: Arduino_RouterBridge/src/bridge.h

if (!atomic_cas(&_executed, 0, 1)){
    setError(GENERIC_ERR, "This call is no longer available");
    return false;
}

这里的 atomic_cas 可以理解成:

如果 _executed == 0,就原子地把它改成 1,并返回成功;
如果 _executed != 0,就不改,返回失败.

在这个场景里它的作用是:

所以它不是协议层逻辑,而是 RpcCall 这个一次性对象的并发保护.


24. router 这层天然是"多连接,多端点"模型

如果只看 Bridge,很容易觉得这套东西是"单 MCU <-> 单 Linux".

但如果看 router 这层的数据结构,就会发现它本来就是多连接模型.

文件: arduino-router/internal/msgpackrouter/router.go

type Router struct {
    routesLock     sync.Mutex
    routes         map[string]*msgpackrpc.Connection
    routesInternal map[string]RouterRequestHandler
}

再看注册函数:

文件: arduino-router/internal/msgpackrouter/router.go

func (r *Router) registerMethod(method string, conn *msgpackrpc.Connection) error

这里 router 注册的不是"某块固定 MCU 的方法",而是:

所以从设计上说,这套路由本来就能扩展到:

router 并不关心对端是什么实体,它只关心:

哪条 connection 声明了哪个 method

因此它更像一个:

而不是:


25. 当前方案是"按 method 名暴露能力"

现在这套设计的核心是:

所以完整查找过程是两级:

  1. router 全局查找
method -> 哪条connection
  1. MCU 本地查找
method -> 哪个wrapped callback

这是一个典型的"细粒度方法路由"设计.

它的好处是:

它的代价是:


26. 另一种可能的方案是"总入口 + 参数 dispatch"

从当前实现出发,可以推导出另一种架构方案:

然后把真实目标放到参数里,例如:

service_id
command_id
payload
flags
version

这样 router 只需要知道很少几个固定入口.

这类方案的特点是:

但代价也很明显:

所以两种设计更适合不同目标:

可以把它们粗略理解成:


27. 为什么当前实现更容易写

结合上面两节,可以得出一个比较务实的结论.

Arduino 用户来说,当前设计:

Bridge.provide("set_led", set_led);
Bridge.provide("get_temp", get_temp);

明显比下面这种更容易接受:

Bridge.provide("dispatch", dispatch);

然后所有请求都由 dispatch(payload) 再人工分流.

也就是说,现在这套 API 优先优化的是:

而不是优先优化:

这也是为什么从当前代码可以看出,它更像一个"开发者可直接使用的 RPC bridge",而不是一个"严格能力封装的系统调用门".


28. 专门跟踪 provide_safe()

前面已经知道:

但如果要真正理解它为什么能保证"在主线程执行",还得把整条链路按代码继续跟下去.

我们从定义开始.

文件: Arduino_RouterBridge/src/bridge.h

template<typename F>
bool provide_safe(const MsgPack::str_t& name, F&& func) {
    k_mutex_lock(&bridge_mutex, K_FOREVER);
    bool res;
    bool out = call(BIND_METHOD, name).result(res) && res && server->bind(name, func, "__safe__");
    k_mutex_unlock(&bridge_mutex);
    return out;
}

28.1 先看它和 provide() 唯一的代码差异

对比 provide():

server->bind(name, func);

provide_safe():

server->bind(name, func, "__safe__");

唯一差别就是多了一个第三个参数:

"__safe__"

所以:

这说明:

safe 不是 router 的属性,而是 MCU 本地 dispatcher 的属性.


29. 继续跟踪 server->bind(name, func, "__safe__")

文件: Arduino_RPCLite/src/server.h

template<typename F>
bool bind(const MsgPack::str_t& name, F&& func, MsgPack::str_t tag=""){
    return dispatcher.bind(name, func, tag);
}

再往下:

文件: Arduino_RPCLite/src/dispatcher.h

template<typename F>
bool bind(MsgPack::str_t name, F&& f, MsgPack::str_t tag="") {
    if (_count >= N) return false;

    if (isBound(name)) return false;

    _entries[_count++] = {name, tag, wrap(std::forward<F>(f))};
    return true;
}

29.1 provide_safe() 最终往表里写了什么

如果用户写的是:

Bridge.provide_safe("greet", greet);

那么 MCU 本地 dispatcher 最终存进去的是一条类似:

name = "greet"
tag  = "__safe__"
fn   = wrapped(greet)

而不是:

name = "greet"
tag  = ""

所以 provide_safe() 的本质可以先记成一句话:

它不是换了一套 router 注册机制,而是在 MCU 本地把这个 method 打上 "__safe__" 标签.


30. 接下来问题变成: 谁会消费 "__safe__" 这个 tag

先看 Bridge 里有哪几条收包处理路径.

30.1 后台线程路径: update()

文件: Arduino_RouterBridge/src/bridge.h

void update() {

    if (k_mutex_lock(&read_mutex, K_MSEC(10)) != 0 ) return;

    RPCRequest<> req;
    if (!server->get_rpc(req)) {
        k_mutex_unlock(&read_mutex);
        k_msleep(1);
        return;
    }

    k_mutex_unlock(&read_mutex);

    server->process_request(req);

    while (true) {
        if (k_mutex_lock(&write_mutex, K_MSEC(10)) == 0){
            server->send_response(req);
            k_mutex_unlock(&write_mutex);
            break;
        } else {
            k_yield();
        }
    }
}

这里调用的是:

server->get_rpc(req)

也就是没有显式传 tag,因此用的是默认值 "".

30.2 主线程路径: update_safe()

文件: Arduino_RouterBridge/src/bridge.h

void update_safe() {

    if (k_mutex_lock(&read_mutex, K_MSEC(10)) != 0 ) return;

    RPCRequest<> req;
    if (!server->get_rpc(req, "__safe__")) {
        k_mutex_unlock(&read_mutex);
        k_msleep(1);
        return;
    }

    k_mutex_unlock(&read_mutex);

    server->process_request(req);

    while (true) {
        if (k_mutex_lock(&write_mutex, K_MSEC(10)) == 0){
            server->send_response(req);
            k_mutex_unlock(&write_mutex);
            break;
        } else {
            k_yield();
        }
    }
}

这里调用的是:

server->get_rpc(req, "__safe__")

也就是说:

到这里已经能看出来:
同一条物理链路上的 request,会被哪条执行路径消费,取决于 method 在 dispatcher 里绑定的 tag.


31. update_safe() 又是谁调用的

这是理解"为什么它是主线程安全"的关键.

先看后台线程入口.

文件: Arduino_RouterBridge/src/bridge.h

inline void updateEntryPoint(void *, void *, void *){
    while (true) {
        if (Bridge) {
            Bridge.update();
        }
        k_yield();
    }
}

而这个线程是在 Bridge.begin() 里创建的:

文件: Arduino_RouterBridge/src/bridge.h

upd_tid = k_thread_create(&upd_thread_data, upd_stack_area,
                        UPDATE_THREAD_STACK_SIZE,
                        updateEntryPoint,
                        NULL, NULL, NULL,
                        UPDATE_THREAD_PRIORITY, 0, K_NO_WAIT);

所以:

再看 update_safe() 这边.

文件: Arduino_RouterBridge/src/bridge.h

static void safeUpdate(){
    BridgeClassUpdater::safeUpdate(&Bridge);
}

safeUpdate() 又在这里被调:

文件: Arduino_RouterBridge/src/bridge.h

void __attribute__((weak)) __loopHook(void){
    k_yield();
    safeUpdate();
}

31.1 为什么说这是主 loop 线程路径

__loopHook() 被设计成 Arduino 主循环上下文里的 hook.

README 也明确写了:

所以当前设计意图是:

这条链路可以直接写成:

__loopHook()
  -> safeUpdate()
    -> BridgeClassUpdater::safeUpdate(&Bridge)
      -> Bridge.update_safe()
        -> server->get_rpc(req, "__safe__")

于是 "__safe__" tag 的 method,就被强制放到了主 loop 线程处理.


32. 继续跟踪 server->get_rpc(req, "__safe__")

文件: Arduino_RPCLite/src/server.h

template<size_t RpcSize = DEFAULT_RPC_BUFFER_SIZE>
bool get_rpc(RPCRequest<RpcSize>& req, MsgPack::str_t tag="") {
    decoder->decode();

    const MsgPack::str_t method = decoder->fetch_rpc_method();

    if (method == "" || !dispatcher.hasTag(method, tag)) return false;

    req.size = decoder->get_request(req.buffer, RpcSize);
    return req.size > 0;
}

这里逻辑要拆开看.

32.1 decoder->decode()

继续从共享 transport 里读字节,尝试解析出一条完整包.

32.2 decoder->fetch_rpc_method()

先从包头里把 method 名字读出来,但还没把这个包正式交给 handler.

32.3 dispatcher.hasTag(method, tag)

这是关键选择点.

它问的是:

当前这个 method,在本地 dispatcher 表里,是否存在一条 name == method && tag == 当前tag 的记录?

如果答案是否定的,那么:

所以 tag 的作用不是"给 handler 打个说明",而是直接参与"这条请求应不应该由当前线程处理"的判定.


33. 继续跟踪 dispatcher.hasTag(method, tag)

文件: Arduino_RPCLite/src/dispatcher.h

bool hasTag(const MsgPack::str_t& name, MsgPack::str_t& tag) const {
    for (size_t i = 0; i < _count; ++i) {
        if (_entries[i].name == name && _entries[i].tag == tag) {
            return true;
        }
    }
    return false;
}

这就是最终判定规则,没有隐藏逻辑.

要命中,必须同时满足:

因此:

这也解释了为什么 provide_safe() 本质上是在做"线程选路".


34. provide_safe() 的一次完整时序

现在把整条链收束成一个完整过程.

假设用户写:

Bridge.provide_safe("greet", greet);

34.1 注册阶段

  1. provide_safe("greet", greet)
  2. 先执行:
call("$/register", "greet").result(res)
  1. router 注册:
routes["greet"] = 这条MCU连接
  1. router 回复 true
  2. MCU 本地执行:
server->bind("greet", greet, "__safe__")
  1. dispatcher 存下:
name = "greet"
tag  = "__safe__"
fn   = wrapped(greet)

34.2 调用阶段

  1. Linux 某 client 调用:
"greet"
  1. router 查全局表:
routes["greet"] -> 这条MCU连接
  1. 请求被转发到 MCU 串口
  2. MCU 后台线程 update() 先看到这个包
  3. 它调用:
server->get_rpc(req)

也就是 tag 为 ""

  1. dispatcher.hasTag("greet", "") 返回 false
  2. 所以后台线程不会处理它
  3. 主 loop 线程跑到 __loopHook()
  4. __loopHook()safeUpdate()
  5. safeUpdate()Bridge.update_safe()
  6. update_safe() 调:
server->get_rpc(req, "__safe__")
  1. dispatcher.hasTag("greet", "__safe__") 返回 true
  2. 于是这次 request 被主线程路径取走
  3. server->process_request(req) 最终执行 greet()
  4. server->send_response(req) 把结果发回去

最终效果就是:

method 名还是同一个 greet,
router 看不见任何 safe 信息,
但 MCU 本地用 tag 把它强制导向主 loop 线程处理.


35. provide_safe() 的设计本质

到这里可以把它压缩成一句更准确的话:

provide_safe() 不是"更安全的 router 注册",也不是"另一种 RPC 协议",而是:

在 MCU 本地 dispatcher 上给 method 打上 "__safe__" 标签,
再让主 loop 线程的 update_safe() 专门只消费这个标签的方法.

所以它保护的不是:

它保护的是:


36. 一个重要推论: safe 不是权限模型,而是执行模型

这点很容易误解.

当前代码里的 "__safe__":

它只是本地 dispatcher 的路由条件.

所以:

因此 provide_safe() 可以理解成:

同名 RPC 的"本地执行线程选择器"

而不是:

对外暴露不同接口


37. 和 provide() 做一个并排对照

37.1 provide("foo", fn)

注册结果:

dispatcher entry = { name="foo", tag="" }

消费路径:

updateEntryPoint()
  -> Bridge.update()
    -> server->get_rpc(req)
      -> dispatcher.hasTag("foo", "")

执行线程:

bridge 后台线程

37.2 provide_safe("foo", fn)

注册结果:

dispatcher entry = { name="foo", tag="__safe__" }

消费路径:

__loopHook()
  -> safeUpdate()
    -> Bridge.update_safe()
      -> server->get_rpc(req, "__safe__")
        -> dispatcher.hasTag("foo", "__safe__")

执行线程:

Arduino 主 loop 线程

所以本质差异只有一句:

两者对 router 来说完全一样,
只是在 MCU 本地选择由哪条线程路径来消费 request.


38. 启动时序与故障规避

结合前面的链路,可以把当前实现对启动顺序的依赖明确记下来.

38.1 MCU 重启后的旧注册问题

Bridge.begin() 里有这一句:

文件: Arduino_RouterBridge/src/bridge.h

started = call(RESET_METHOD).result(res) && res;

其中:

#define RESET_METHOD "$/reset"

也就是说,MCU 在 bridge 初始化时会先向 router 发一个:

$/reset

作用是:

所以第一类问题:

MCU 已经复位,但 router 还以为它之前注册的方法仍然有效

当前设计有两条清理路径:

  1. MCU 主动发 $/reset(如上所述)
  2. 连接断开时 router 自动清理

文件: arduino-router/internal/msgpackrouter/router.go

// Unregister the methods when the connection is terminated
r.removeMethodsFromConnection(msgpackconn)
msgpackconn.Close()

这是在 Accept() 的连接循环尾部执行的.当串口连接物理断开(比如 MCU 复位导致串口消失,或者线被拔掉),msgpackconn.Run() 会因 io.EOF 或其他错误退出循环,然后自动执行:

r.removeMethodsFromConnection(msgpackconn)

这个方法会遍历整个 routes 表,把所有属于这条 connection 的 method 都删掉.

所以旧注册残留的防护是双保险的:

但如果场景是"MCU 软复位,串口链路没有物理断开",那么 router 不会自动清理,这时候就只能靠 $/reset.

38.2 但对端未就绪问题没有被协议层彻底解决

另一类问题是:

这时就会出现:

从当前代码看,没有一套完整的:

也就是说,当前设计更依赖:

而不是由 RPC 协议层自动兜底.

38.3 当前部署顺序下暴露出的设计风险

根据当前项目使用方式,有一个需要明确记录的风险顺序:

  1. 先下载 MCU 程序
  2. 释放复位,MCU 开始运行
  3. 之后才启动容器
  4. Python 脚本运行在容器内

在这个顺序下,MCU 很可能先于 Linux 侧业务环境开始执行 Bridge.begin(),provide() 或后续 RPC.

这会带来一个现实问题:

因此可以明确记为一个设计风险:

当前系统如果采用"MCU 先启动,容器后启动"的顺序,那么 bridge 初始化和早期 RPC 依赖时序,存在启动窗口问题.

这个问题不是 $/reset 能解决的,因为:

38.4 工程上的规避方式

在当前实现不改协议的前提下,比较现实的规避方式是:

换句话说:

38.5 当前结论

可以把这一点直接记成一句判断:

当前 bridge / router 设计对启动时序敏感.
$/reset 只解决旧注册残留,不解决对端未 ready 的问题.
如果部署顺序是"MCU 先运行,容器后启动",那么这是一个真实的系统设计缺陷,需要靠上层时序控制或额外握手补齐.


39. 一个容易漏掉但很重要的点: RpcCall 不是"纯惰性对象"

前面为了说明主链,重点强调了:

这个主判断没错,但还需要补两条"隐式执行"语义,否则读代码时容易误判.

文件: Arduino_RouterBridge/src/bridge.h

bool result() {
    MsgPack::object::nil_t nil;
    return result(nil);
}

~RpcCall(){
    result();
}

operator bool() {
    return result();
}

39.1 析构函数会触发一次执行

这意味着:

所以更准确地说:

39.2 if (Bridge.call(...)) 也会触发执行

因为定义了:

operator bool() {
    return result();
}

所以像 README 里的这种写法:

if (!Bridge.call("send_greeting", "Hello Friend")) {
    ...
}

本质不是"判断对象是否有效",而是:

39.3 因此建议把 RpcCall 的语义写成一句更准确的话

Bridge.call() 返回的是"延迟到 result() / operator bool() / 析构时才真正发出的单次 RPC 调用对象".


40. notify()call() 的边界也值得单独记一下

文件: Arduino_RouterBridge/src/bridge.h

template<typename... Args>
RpcCall<Args...> call(const MsgPack::str_t& method, Args&&... args) {
   return RpcCall<Args...>(method, client, &read_mutex, &write_mutex, std::forward<Args>(args)...);
}

template<typename... Args>
void notify(const MsgPack::str_t method, Args&&... args)  {
    while (true) {
        if (k_mutex_lock(&write_mutex, K_MSEC(10)) == 0) {
            client->notify(method, std::forward<Args>(args)...);
            k_mutex_unlock(&write_mutex);
            break;
        }
        k_yield();
    }
}

这里建议你补一个对照表:

这点和 Monitor.write() 的实现正好对应.

文件: Arduino_RouterBridge/src/monitor.h

if (_compatibility_mode) {
    bridge->call(MON_WRITE_METHOD, send_buffer).result(written);
} else {
    bridge->notify(MON_WRITE_METHOD, send_buffer);
    written = size;
}

也就是:

这能帮助读者理解:不是所有 Bridge API 都建立在"请求-响应"之上.


41. router 实际不只有 $/register

如果这份笔记的目标是"UNO Q 通信机制全景",那现在还缺一个"控制面/API 面总览".

41.1 router 内建控制 RPC

文件: arduino-router/internal/msgpackrouter/router.go

当前 router 至少显式处理了这些控制方法:

另外 $/version 不是在 router.go 里硬编码,而是在启动阶段注册进去:

文件: arduino-router/main.go

if err := router.RegisterMethod("$/version", ...); err != nil {
    ...
}

所以从系统视角看,BridgeClass 当前实际依赖的 router 控制面至少有:

而不是只有一个 $/register.

41.2 串口控制 RPC 也属于系统通信面的一部分

文件: arduino-router/main.go

router 还注册了:

这部分虽然不是 Bridge.provide() 主链的一环,但它说明:

41.3 $/serial/open$/serial/close 的动态串口管理

文件: arduino-router/main.go

这两个方法允许外部通过 RPC 动态打开和关闭串口连接.

$/serial/open 做的是:

err := router.RegisterMethod("$/serial/open", func(_ *msgpackrpc.Connection, params []any, res msgpackrouter.RouterResponseHandler) {
    address, ok := params[0].(string)
    // 验证地址是否匹配配置
    // 通知后台 goroutine 打开串口
    serialCloseSignal = make(chan struct{})
    serialOpened.Broadcast()
    res(true, nil)
})

$/serial/close 做的是:

err = router.RegisterMethod("$/serial/close", func(_ *msgpackrpc.Connection, params []any, res msgpackrouter.RouterResponseHandler) {
    address, ok := params[0].(string)
    // 验证地址是否匹配配置
    close(serialCloseSignal)
    serialCloseSignal = nil
    serialClosed.Wait()
    res(true, nil)
})

关键设计点:

另外,当串口连接意外断开时,这个 goroutine 会自动尝试重连:

for {
    serialOpened.L.Lock()
    for serialCloseSignal == nil {
        serialClosed.Broadcast()
        serialOpened.Wait()  // 等待 open 信号
    }
    serialOpened.L.Unlock()
    // 打开串口,连接到 router...
    // 如果连接断开,循环回去等待
}

所以串口管理的设计是:

这意味着"串口什么时候可用"不完全取决于物理连接,还取决于有没有人调过 $/serial/open.


42. Arduino_RouterBridge 里其实还有好几个不同的通信子系统

现在把"RPC method 暴露/调用主链"打通了,但 Arduino_RouterBridge 这个库本身不只暴露 Bridge.

42.1 Monitor

文件: Arduino_RouterBridge/src/monitor.h

它实际对应的是一组 monitor RPC:

本质上这是把"串口监视器语义"包装成一组 RPC.

也就是说:

42.2 BridgeTCPClient / BridgeTCPServer

文件: Arduino_RouterBridge/src/tcp_client.h
文件: Arduino_RouterBridge/src/tcp_server.h

它们对应的 method 包括:

这说明一个重要事实:

Arduino_RouterBridge 不只是"让 Linux 调 MCU 方法",它还让 MCU 把 Linux 当成网络代理,借 router 去做 TCP/TLS/监听/accept.

42.3 BridgeUDP

文件: Arduino_RouterBridge/src/udp_bridge.h

对应的 method 包括:

这条链路和 TCP 不同,它是"按数据报包边界"组织的,所以如果你后续补文档,建议专门把这几个状态量讲清楚:

因为 UDP 这里不是简单的字节流转发,而是"router 侧 packet + MCU 侧 packet state"配合.

42.4 BridgeHCI

文件: Arduino_RouterBridge/src/hci.h

它对应的是:

这条链说明 UNO Q 这套 bridge 不只代理"应用层 RPC",还在尝试把 Linux 主机上的蓝牙 HCI 通道通过 RPC 暴露给 MCU 侧.

如果你想把"通信库"分析做完整,这块很值得单列,因为它已经不是普通业务 RPC,而是"主机设备接口代理".


43. Linux 侧还有三个真正的后端 API 模块

文件: arduino-router/main.go

当前启动时会注册三类内部 API:

43.1 你现在主线实际只详细分析了 msgpackrouter

这当然是对的,因为它是"method 路由内核".

但如果问题改成:

当前通信库里还有哪些没分析到

那么至少还包括:

也就是说,msgpackrouter 负责"谁该收到请求",而这些 internal API 模块负责"请求到了以后具体怎么碰 Linux 资源".


44. 如果把"当前仓库里的通信库"拉成清单,还缺这些层

这个仓库里和"通信"有关的内容,严格来说可以分四层:

44.1 主机桥接/RPC 层

这一层你已经分析了核心主链,但还没补完 Monitor/TCP/UDP/HCI 四个子面.

44.2 router 内部资源代理层

这层你目前基本还没展开.

44.3 Zephyr 网络抽象层

文件:

这层的意义是:

暂时不考虑补了,除非你要这么做:

那么这一层就必须补.

44.4 板级总线通信层

文件:

这层不是 host-bridge 网络通信,而是板级外设总线:

如果你说的"通信库"包含板级总线,那它们也还没分析.
如果你说的"通信库"只指 UNO Q 和 Linux 主机之间的通信,那这层可以放到次级优先级.


45. 分析其他Call差异

算了,以后再说,看着很乱.


46. 一句话总结这次补充

你现在这份笔记已经把:

这一条主链分析清楚了.

这里暂时不分析的三大片:


47. 专门展开 Monitor

Monitor 这条链很值得单独分析,因为它看起来像普通 Serial/Stream,但实际上已经不是"本地 UART",而是:

MCU 上的 Stream 包装
-> Bridge RPC
-> router 的 monitorapi
-> Linux 侧 monitor TCP socket

47.1 Monitor.begin() 做了三件事

文件: Arduino_RouterBridge/src/monitor.h

bool begin(...) {
    ...
    if (!bridge_started) {
        bridge_started = bridge->begin();
    }
    ...
    _connected = bridge->call(MON_CONNECTED_METHOD).result(out) && out;
    MsgPack::str_t ver;
    _compatibility_mode = !bridge->getRouterVersion(ver);
    ...
}

这里实际做的是:

  1. 确保 Bridge 已经起来
  2. mon/connected 问 Linux 侧当前是否有 monitor 客户端连上
  3. $/version 判断 router 是否支持较新的 monitor 行为

所以 Monitor.begin() 本质不是"初始化一个本地串口",而是"探测并挂接一条经 router 转发的监控流".

47.2 Monitor.write() 的真实路径

文件: Arduino_RouterBridge/src/monitor.h

if (_compatibility_mode) {
    bridge->call(MON_WRITE_METHOD, send_buffer).result(written);
} else {
    bridge->notify(MON_WRITE_METHOD, send_buffer);
    written = size;
}

对应 Linux 侧:

文件: arduino-router/internal/monitorapi/monitor-api.go

monitorapi.write() 会:

所以这条链可以记成:

47.3 Monitor.read() 的真实路径

MCU 侧不是每次直接向 Linux 要一个字符,而是:

文件: Arduino_RouterBridge/src/monitor.h
文件: arduino-router/internal/monitorapi/monitor-api.go

这说明:

47.4 monitorapi 不是走串口,而是额外开了一个 TCP 监听口

文件: arduino-router/internal/monitorapi/monitor-api.go

listener, err := net.Listen("tcp", addr)
...
_ = router.RegisterMethod("mon/connected", connected)
_ = router.RegisterMethod("mon/read", read)
_ = router.RegisterMethod("mon/write", write)
_ = router.RegisterMethod("mon/reset", reset)

所以 monitor 这条链不是:

而是:

这个拓扑如果不单独说出来,读者很容易把 Monitor 误解成"另一个普通串口对象".


48. TCP

BridgeTCPClient / BridgeTCPServer 的本质不是 MCU 自己有 TCP 协议栈去直接连网,而是:

MCU 把 TCP 操作编码成 RPC
router 在 Linux 上真正持有 net.Conn / net.Listener
MCU 只拿一个 connection_id / listener_id

48.1 BridgeTCPClient.connect()

文件: Arduino_RouterBridge/src/tcp_client.h

const bool ok = _connected || bridge->call(TCP_CONNECT_METHOD, hostname, port).result(connection_id);

对应 Linux 侧:

文件: arduino-router/internal/network-api/network-api.go

tcpConnect() 做的是真正的:

所以 MCU 侧拿到的 connection_id 本质是:

48.2 BridgeTCPClient.read()/write()

写路径:

读路径:

文件: Arduino_RouterBridge/src/tcp_client.h
文件: arduino-router/internal/network-api/network-api.go

这里最值得记的一句话是:

MCU 看到的是 Arduino Client 接口;实际持有 TCP 连接状态的是 Linux 侧 networkapi.

48.3 connectSSL() 其实是让 Linux 去做 TLS

文件: Arduino_RouterBridge/src/tcp_client.h
文件: arduino-router/internal/network-api/network-api.go

BridgeTCPClient.connectSSL() 最终调用 tcp/connectSSL,Linux 侧用的是:

conn, err := tls.Dial("tcp", serverAddr, tlsConfig)

所以 TLS 的真正终止点在 Linux,不在 MCU.

这点非常重要,因为它解释了:

48.4 BridgeTCPServer 其实是"让 Linux 代持 listener"

文件: Arduino_RouterBridge/src/tcp_server.h

_listening = bridge->call(TCP_LISTEN_METHOD, hostname, _port).result(listener_id);

对应 Linux 侧:

文件: arduino-router/internal/network-api/network-api.go

所以这里的 server 语义是:

这和通常"MCU 本地起 server"是完全不同的架构.


49. UDP

UDP 这条链比 TCP 更值得重点写,因为它不是单纯字节流,而是显式保留了"一个数据报包"的边界.

49.1 BridgeUDP.begin() 并不是连远端,而是在 Linux 上开一个 UDP socket

文件: Arduino_RouterBridge/src/udp_bridge.h

ok = bridge->call(UDP_CONNECT_METHOD, hostname, port).result(connection_id);

对应 Linux 侧:

文件: arduino-router/internal/network-api/network-api.go

udpConn, err := net.ListenUDP("udp", udpAddr)

这里其实更接近:

所以 udp/connect 在这里更像"打开/绑定一个 UDP 通道",不是真正意义上的 connected UDP peer.

49.2 发包是三段式

MCU 侧:

对应 Linux 侧:

文件: Arduino_RouterBridge/src/udp_bridge.h
文件: arduino-router/internal/network-api/network-api.go

所以 UDP 发包不是"每次 write 就发网包",而是:

49.3 收包是两阶段

第一阶段:等一个完整 packet 到达

文件: Arduino_RouterBridge/src/udp_bridge.h

bridge->call(UDP_AWAIT_PACKET_METHOD, connection_id, read_timeout).result(packet_meta);

Linux 侧 udpAwaitPacket() 会:

文件: arduino-router/internal/network-api/network-api.go

第二阶段:MCU 再分批把这一包读完

49.4 dropPacket() 的设计意图

文件: Arduino_RouterBridge/src/udp_bridge.h

它的语义是:

所以 BridgeUDP 实际上是:

Linux 侧负责"按包收进来并缓存完整 datagram"
MCU 侧负责"把这个 datagram 分批消费完"

这是它和 TCP 的根本差异.


50. 专门展开 HCI

HCI 这条链虽然小,但它其实是这套 bridge 里最"硬核"的一个代理面,因为它碰到的是 Linux 主机蓝牙 HCI raw socket.

50.1 MCU 侧接口长什么样

文件: Arduino_RouterBridge/src/hci.h

BridgeHCI 暴露的核心操作就是:

这就是一个"二进制设备通道代理".

50.2 为什么这里专门做了 BinaryView

文件: Arduino_RouterBridge/src/hci.h

struct BinaryView {
    const uint8_t *data;
    size_t size;
    void to_msgpack(MsgPack::Packer &packer) const {
        packer.pack(data, size);
    }
};

这里不是转成字符串,也不是逐字节数组,而是明确告诉 MsgPack:

这说明 HCI 这条链的关注点是:

50.3 Linux 侧到底做了什么

文件: arduino-router/internal/hciapi/hci-api_linux.go

hci/open 会:

hci/send

hci/recv

hci/avail

这说明:

BridgeHCI 不是普通业务 RPC;它是把 Linux 内核设备接口通过 RPC 映射到 MCU 侧.

50.4 平台约束

文件: arduino-router/internal/hciapi/hci-api_default.go

BridgeHCI 依赖 Linux 侧 hciapi,不是跨平台恒成立能力.


实际这几个库比上面分析的功能多,累了不分析了.

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注