AGENT NETWORK · DOCS
教程

教程七:把真实设备接进来

ANetLink:14 个协议适配器,而 daemon 仍然不知道"设备"是什么

场景与前置条件

你有一台机器连着真实设备:一台 Modbus PLC、一根 Zigbee 协调器棒、一个已经跑着的 Home Assistant、几台 IP 摄像头。你想让网络另一端的 agent 能调用它们,并且拿回的 不是"我发了命令",而是可核对的效果记录。

这一页做三件事:先在一台机器上把 ANetLink 单独跑通(不需要 daemon,不需要 hub), 再接一台真实设备,最后把它接进 anet daemon,让另一台机器经 hub 委派它。

开始之前需要:

  • 一台装了 Go 1.26.6+ 的机器(go.mod 声明 go 1.26.6)。ANetLink 只能从源码构建 —— 仓库里既没有发布脚本也没有 release workflow,.github/workflows 下只有 ci.yml。 CGO_ENABLED=0,无需 C 编译器。
  • 一个 anet daemon(第三步才用到)。版本 0.1.7,一行安装见 安装。
  • 第三步还需要另一台装了 anet 的机器作为调用方,两边都能出站访问同一个 hub。 本页用官方 hub https://hub.agentnetwork.org.cn。

第一步和第二步与 anet 完全无关。ANetLink 是一个能单独使用的运行时:装上它加一个 适配器,你本机的 Claude Code 就能经 MCP 直接控设备,一个联系人都不需要。

ANetLink 是 ANet 面向物理世界的窄腰。它南向说各种设备协议,北向只暴露一个能力面。

三个模型撑起这件事:

DeviceProfile —— 一台设备"能做什么"的机器可读描述,是内容寻址的 CoreDet-CBOR 对象,有稳定 CID。它由三部分组成:

组成含义
能力列表点分类法 id,如 light.onoff、sensor.temperature、ptz.move
访问掩码三比特:STATE=1(主动状态流)、SET=2(执行)、GET=4(按需读)。可读可写的开关 = 6
信任阶梯两条正交的轴:MaxTrust(V0–V4,这次执行是否真的发生了)与 AuthTrust(A0–A4,这台设备是谁)

Effect —— 每次调用返回一个效果,状态与度量严格分离。这是 ANetLink 的诚实核心: sdk.EffectBuilder 在 Verified() 没有配套的观测(Metric() 的度量,或 Snapshot() / StreamURI() 的制品)时,Build() 自动降级为 UNVERIFIED。适配器在类型层面无法假报 OK。runtime 再钳一道:MaxTrust < V2 的设备,即使适配器声称 OK 也强制降为 UNVERIFIED。信任只能钳、不能抬,适配器和 quirk 都无权抬高。

Adapter / Backend —— 适配器说一个协议族,实现 C4 合同;一个适配器可以有多个后端, 启动时按优先序探测。

北向两个消费口,同一批设备与效果:MCP(给 Claude / IDE)与 C1(给 anet daemon)。

双后端制:探测优先,永远兜底

设计纪律写在 sdk/probe.go 的 Select 里:

  • 有 backend_override 时用指定的后端,但探测仍然必须通过 —— 强制指定却探测失败 是配置错误,启动即报,不静默降级;
  • 没有 override 时按声明顺序探测,取第一个可用的;
  • 全部不可用时,错误信息里带每个后端各自的失败原因,而不是一句"不可用"。

意图是:宿主设施(已装的 BlueZ、已跑的 OTBR、已跑的 zigbee2mqtt 或 Home Assistant) 活着就骑上去,不活着就落到自研兜底路径,永远都要兜底。

按当前代码,这条纪律只有 thread 适配器真正落成了两个后端(host-otbr 与 managed-otbr)。其余 13 个适配器的 Backends() 各自只返回一个后端。

兜底在适配器一层存在而不在后端一层:Zigbee 生态有两条路(骑 zigbee2mqtt 的 mqtt 桥,和直驱协调器棒的原生 zigbee),但它们是两个适配器,不是同一个适配器的两个后端。 这一点值得如实说明,不要按"每个适配器都有双后端"去理解现状。

14 个适配器与它们的 build tag

适配器全部是编译期插件,注册在 cmd/anetlinkd/plugin_<name>.go 里。有两套 tag 约定, 用哪一套本身说明了适配器的性质。

减法 tag(默认编入,-tags no_<name> 去掉) —— 对本地网络讲开放标准、不依赖发行版 之外的东西:

适配器目录配置键build tag说明
simsim!no_sim内建模拟灯。默认注册,裸 anetlinkd 也有东西可看
modbusmodbus!no_modbusModbus/TCP,纯 Go 客户端,写后读回
mqttbridgemqtt!no_mqttzigbee2mqtt / Tasmota 桥,骑在已跑的 broker 上
habridgehomeassistant!no_haHome Assistant WebSocket 桥
opcuaopcua!no_opcuaOPC UA,工业自动化
bacnetbacnet!no_bacnetBACnet/IP,楼宇自动化,纯 Go ASHRAE 135 编解码
cancan!no_canCAN 总线,over SocketCAN
zigbeezigbee!no_zigbee原生 Z-Stack 协调器,自研 UNPI + ZCL
bleble!no_ble纯 Go 直连内核 HCI socket,不需要 BlueZ / D-Bus / cgo
btmeshbtmesh!no_btmesh蓝牙 Mesh,自实现 AES-CMAC / AES-CCM 与密钥调度
threadthread!no_threadThread 承载,host OTBR 或打包 otbr-agent 受管子进程

加法 tag(默认不编入,-tags <name> 才有) —— 经厂商 HTTP 接口触达设备、要持有凭据:

适配器目录配置键build tag说明
onvifonvifonvifONVIF IP 摄像头,标准路径,带 WS-Discovery
hikvisionhikvisionhikvision海康 ISAPI
dahuadahuadahua大华 CGI

方向反过来的理由与 anet 的 shell tag 一致:两种 tag 写错的代价不对称。减法 tag 漏了, 是某人想去掉的模块没去掉;加法 tag 漏了,是一个没人启用过的攻击面进了所有构建。

另有一个加法 tag adap,是编译期之外的逃生口:经 UDS 托管一个不在本仓构建的 C4 适配器(可以是别的语言写的)。一个 socket 一个适配器,不做复用。

# 默认发行版(无摄像头、无厂商接口)
CGO_ENABLED=0 go build -o anetlinkd ./cmd/anetlinkd
 
# 安防网关:全部摄像头能力
CGO_ENABLED=0 go build -tags onvif,hikvision,dahua -o anetlinkd ./cmd/anetlinkd
 
# 边缘盒子:裁掉 mqtt / HA / sim
CGO_ENABLED=0 go build -tags no_mqtt,no_ha,no_sim -o anetlinkd-min ./cmd/anetlinkd

配置里写了没编入的适配器,启动响亮报错,并且指向正确的修法 —— 加法的说 "用 -tags onvif 重新构建",减法的说"是不是带了 no_x"。裁掉的能力绝不静默消失。

第一步:单机跑通

构建并空跑

git clone https://github.com/ANetResearch/ANetLink.git
cd ANetLink
CGO_ENABLED=0 go build -o anetlinkd ./cmd/anetlinkd
./anetlinkd --demo

实际输出:

2026/09/03 17:49:35 vendor corrections loaded: 7 quirk(s)
2026/09/03 17:49:35 adapters compiled+configured: sim
2026/09/03 17:49:35 device sim/lamp-1 online=true trust=T4 profile=bafyreifz4fnvfxulvtxllo3nkahcrbpeyklonfpbahl6ocnx25s2quot5i
demo: light.onoff on=true → status=OK verifiable=true metrics=map[power_state:1]

四行分别是:加载的厂商修正数量、这个二进制里编进并且配置好的适配器、设备清单 (键 = <适配器>/<设备id>,带信任级与 Profile CID)、一次真实调用的效果。

第二行只有 sim,不代表这个构建里只有 sim。默认构建把 11 个减法 tag 的适配器全都链接 进来了 —— go tool nm ./anetlinkd | grep -c adapters/modbus 是正数,adapters/onvif 是 0(加法 tag,没带 -tags onvif)。这一行列的是注册进 runtime 的适配器,而只有 sim 是 defaultOn:其余的没写配置段就不注册。

sim 的 status=OK verifiable=true 是真的:它是进程内闭环,MaxTrust=V4、 AuthTrust=A4,并且带回了度量 power_state=1。换成一台云端设备,同样一句 Verified() 会被 runtime 钳成 UNVERIFIED。

挂到 Claude / IDE(MCP)

./anetlinkd --mcp

在 stdio 上起一个 MCP server,四个工具:

工具作用
devices_list列出全部设备:键、协议、在线态、V/A 双轴信任、可调能力、Profile CID
device_invoke调用一个能力,返回诚实效果(status / verifiable / metrics / message / evidence)
devices_discover扫描已连接的网络,返回发现结果与可直接粘贴的配置片段
devices_pair开放入网窗口(默认 60 秒,上限 254 秒 —— Zigbee 的限制),报告谁加入了

--mcp 模式下 stdout 归 MCP 会话,日志走 stderr。

在 Claude Code 里注册这个 MCP server 之后,就可以直接说"把客厅的灯关掉"或 "我刚买了个传感器",分别落到 device_invoke 与 devices_pair。

第二步:接一台真实设备

先让它自己找

./anetlinkd --scan

没有可扫的东西时输出是这样(这是裸构建的真实输出):

found 0 device(s)

no devices discovered.
  (the compiled-in adapters either need configuration first —
   a coordinator port, a broker URL, a Home Assistant token —
   or their networks are empty.)

14 个适配器里有 5 个实现了自动发现(sdk.Discoverer):zigbee(ZDO 邻居表)、 bacnet(Who-Is 广播)、mqttbridge(读 zigbee2mqtt 的 retained 设备表)、 habridge(枚举 HA 实体)、onvif(WS-Discovery)。其余九个是静态配置 —— Modbus、OPC UA、CAN 这类协议本身没有发现机制,只能写地址。

扫出来的结果可以直接落成配置文件:

./anetlinkd --scan --scan-out config.json

已存在的文件不会被覆盖,会报错让你先挪开。

写配置

config.json,所有段可选;不写某适配器段即不注册它。

{
  "backend_override": { "thread": "host-otbr" },
  "quirk_dir": "/etc/anetlink/quirks.d",
  "adapters": {
    "sim": { "enabled": false },
 
    "modbus": {
      "devices": [
        {
          "id": "plc-1",
          "addr": "192.168.1.50:502",
          "unitid": 1,
          "vendor": "acme",
          "model": "rtu-8",
          "caps": [
            { "id": "switch.onoff", "kind": "coil", "address": 3, "access": 6 },
            { "id": "sensor.temperature", "kind": "holding", "address": 7,
              "access": 4, "scale": 0.1, "unit": "°C" }
          ]
        }
      ]
    },
 
    "homeassistant": {
      "url": "ws://homeassistant.local:8123/api/websocket",
      "token": "长期访问令牌"
    }
  }
}

要点:

  • access 是三比特整数按位或:STATE=1、SET=2、GET=4。可写可读 = 6,只读 = 4。
  • modbus 段的键名来自 Go 结构体字段名,该结构体没有 json tag (adapters/modbus/modbus.go 的 DeviceConfig)。encoding/json 的兜底匹配不区分大小写 但不认下划线,所以 unit id 那一项是 unitid,写成 unit_id 会被静默忽略、落成 0。 (ANetLink/docs/MANUAL-zh.md §5 的样例写的是 unit_id,那一处是错的。)
  • backend_override 强制某适配器用指定后端。强制但探测失败 = 配置错误,启动即报。
  • quirk_dir 放站点自己的厂商修正,叠在随构建发行的那批之上(裸构建默认加载 7 条)。 修正只改数值,永远不能抬信任,并且被应用的 quirk id 会写进效果的 evidence —— 一个被修正过的读数不是设备原样报出的读数,消费方必须能看出来。

买了新设备就开窗口

./anetlinkd --config config.json --pair 60s

目前只有 zigbee 适配器实现了 sdk.Pairer。窗口结束后网络自动关闭。

启动

./anetlinkd --config config.json --mcp

启动日志会打印:加载的厂商修正数量、编进并配置好的适配器、设备清单(每台带在线态、 信任级与 Profile CID)。

选中的后端不在这份日志里。runtime 记着它(Runtime.Backend(<适配器名>) 能问出来), 但 cmd/anetlinkd/main.go 没有把它打出来 —— 眼下要确认某个适配器骑上了哪个后端,只能靠 探测失败时的报错,或者读 backend_override。

第三步:接进 ANet

到这一步为止,设备只对本机可见。接上 daemon 之后,它们变成这个节点在 hub 上可被 委派的能力。

让 anetlinkd 服务 C1 wire

./anetlinkd --config config.json --c1-socket /run/anetlink.sock

这条 wire 是 HTTP over Unix socket,三个端点(默认 JSON,带 Accept: application/cbor 时返回 CoreDet-CBOR):

GET  /v0/health        → 204
GET  /v0/capabilities  → {"capabilities": ["light.onoff@sim/lamp-1", ...]}
GET  /v0/describe      → {"cid": ""}          # DeviceCatalog CAS 对象未做,当前恒为空
POST /v0/invoke        {"capability":"...","args":{...},"call_id":"...","caller_aid":"..."}
                       → {"status":"...","metrics":{...},"message":"...","evidence":{...}}

不接 daemon 也可以直接核一遍:

curl -s --unix-socket /run/anetlink.sock http://anetlink/v0/capabilities
curl -s -o /dev/null -w 'health=%{http_code}\n' --unix-socket /run/anetlink.sock http://anetlink/v0/health
curl -s --unix-socket /run/anetlink.sock -X POST -H 'Content-Type: application/json' \
  -d '{"capability":"light.onoff@sim/lamp-1","args":{"on":true}}' \
  http://anetlink/v0/invoke

实际返回:

{"capabilities":["light.onoff@sim/lamp-1"]}
health=204
{"status":"OK","metrics":{"power_state":1}}
anet version
anet 0.1.7 (commit unknown, built unknown)
modules: anetlink,blackboard,cas,mcp,org,p2p,service,taskboard,x402

modules: 这一行列的是这个二进制里实际链接进来的模块。v0.1.7 的发布件每个平台只有 两个 —— anet-<plat> 与 anet-shell-<plat>(deploy/release/build-release.sh 的 build_one "$plat" "" "anet" 与 build_one "$plat" "shell" "anet-shell"),一行安装拿到的 是前者:不带任何减法 tag,所以 anetlink 在内。

min/standard/paid/agent/p2p 那几档确实带了 no_anetlink,但它们是 docs/DISTRIBUTIONS-zh.md 里的构建配方,没有对应的发布件 —— 要那几档得自己构建。 详见发行版总览。

判据是符号数,不是运行时开关:

go tool nm ./anet | grep -c module/anetlink   # 默认构建 > 0
go build -tags no_anetlink -o /tmp/anet-lean ./cmd/anet
go tool nm /tmp/anet-lean | grep -c module/anetlink   # 必须 0

配置 daemon 指向那个 socket

编辑 ~/.anet/config.json:

{
  "modules": {
    "anetlink": {
      "socket": "/run/anetlink.sock",
      "timeout_s": 5
    }
  }
}

socket 必填,timeout_s 限定启动时那次注册握手(缺省 5 秒)。

先起 anetlinkd,再起 daemon。 模块编译进来了、配置块也写了、但 socket 连不上, daemon 会启动失败并报 register provider on /run/anetlink.sock: ...,而不是带着 一份缺了能力的注册表照常起来。这是故意的:后者属于"沉默的失败",会在几天后由一次 "莫名其妙找不到 provider"的委派发现。

也存在一个旧的等价写法 providers.anetlink.socket(只有 socket 一个字段), daemon 会把它投影成同一个模块配置块。新配置用 modules.anetlink。

anet stop
anet up

把能力发布到 hub

anet hub-register https://hub.agentnetwork.org.cn --name lab-gateway
anet status

daemon 在注册时会把 provider 注册表里实际服务的能力 id 折进要发布的 caps 列表, 和运营者手写的标签并存。也就是说 light.onoff@sim/lamp-1 这样的 id 是 daemon 自己 放进去的,不需要你手抄一遍。

daemon 每次启动也做同一次合并并重新注册一遍(internal/daemon/daemon.go 的 refreshRegistration,0.1.7 起)。所以设备清单变了(新配了一台 PLC、换了适配器 tag) 之后,anet stop && anet up 就够,不必再显式 hub-register;显式注册仍然可用, 改展示名或手写 caps 时用它。两条路径共用一把锁,后到的不会被过期快照覆盖。

这次刷新在后台跑、失败不致命:hub 不可达时 daemon 照常起来,只在 daemon.log 里留一行 anet: could not refresh this node's registration with <hub>: …。真的改了清单时会留一行 anet: refreshed capabilities at <hub>: [...]。看不到设备能力上目录,先查这两行。

能力的"加"跟着重启走,"减"不一定。 显式 hub-register 会把合并后的 caps 写回 config.json,所以一个曾经显式注册过的设备 id 会留在本机 caps 里;拔掉那台设备再重启, 启动时的刷新报的仍然是它。要真的撤下来,手工删掉 config.json 里 caps 数组的那几条再重启。

另一台机器委派它

调用方装默认版即可,不需要 ANetLink,也不需要知道设备的存在:

anet find --cap 'light.onoff@sim/lamp-1'
anet find --cap 'light.*'

--cap 走 hub 的能力索引做精确匹配,* 结尾按前缀匹配一个族,逗号分隔是或。 它和不带 --cap 的 anet find 是两个问题:后者搜的是散文,会返回一个只是"提到了" 这个词的 agent。

拿到对方 AID 之后:

anet delegate <provider-aid> --capability 'light.onoff@sim/lamp-1' --args '{"on":true}'
anet results

--capability 的委派不经过任何模型:provider 端的 daemon 用注册表解析这个 id, 确定性执行,把效果作为交付物返回,附一份签名收据,并写进本地证据链。

交付物的形状(capabilityResult):

{
  "capability": "switch.onoff@modbus/plc-1",
  "status": "OK",
  "verifiable": true,
  "metrics": { "switch_onoff": 1 },
  "evidence": {
    "requested": "switch.onoff=true",
    "protocol": "modbus",
    "native_ack": true,
    "observed_state": "true",
    "latency_ms": 12,
    "verify_trust": 2,
    "auth_trust": 1
  }
}

evidence 一路从适配器穿过 C1 wire 到达委派方,包括 quirk 字段 —— 一个被厂商修正 改过的数值,消费方能看出它被改过。这份 provenance 也在 provider 自己的证据链里, 两侧对同一次执行的记述不能不一致。

哪些 evidence 字段有值取决于适配器。sim 只填度量、不填 evidence,所以拿它做的那次 /v0/invoke 返回是 {"status":"OK","metrics":{"power_state":1}},没有 evidence 段 —— 这是诚实的:一个进程内模拟器没有"协议"也没有"设备 ack"可报。

C1 红线:能力 id 里带着设备,daemon 不知道设备是什么

light.onoff@sim/lamp-1 这个 id 的形状是 <能力>@<适配器>/<设备>。它只在 ANetLink 一侧铸造和解析 —— northbound/c1serv 的 CapabilityID 与 ParseCapabilityID 是全 系统唯一认识这个形状的两个函数。

过了 socket 之后,它在 daemon 眼里是一个不透明字符串:

  • provider/anetlink 只负责把它放进 HTTP body;
  • provider.Registry 把它当 map 的键;
  • internal/daemon/capability.go 把它写进 TaskDoc 的 Requires,签名、投递、记账。

这几层里没有一行代码去看 @ 右边是什么。daemon 因此不需要有"设备"这个概念, 也不需要有"设备表""在线状态""配对窗口"。它只知道"某个 id 可以被调用,调用会返回一个 效果"。

这条红线的实用后果是:ANetLink 可以长出第 15、第 20 个适配器,daemon 一行都不用改; 而 daemon 也不会慢慢长成第二个设备管理系统。同一道门里还进来别的东西 —— cas、shell、service、taskboard 的能力,走的是同一个 provider.CapabilityProvider 接口。

诚实效果:UNVERIFIED 不是失败

设备这一侧最容易出错的地方,是把"我发了命令"当成"我确认了效果"。ANetLink 用两条机制 挡这件事:

结构上挡 —— EffectBuilder.Verified() 没有配套的度量或制品时,Build() 降级为 UNVERIFIED。适配器作者写不出一个没有观测支撑的 OK。

运行时钳 —— runtime 对 MaxTrust < V2 的设备把 OK 强制降为 UNVERIFIED。 云 API 封顶在 V1(只有传输层 ack),所以任何走厂商云的路径都不可能报出可验证的成功。

各适配器如实标注自己能验证到哪一级:

适配器VerifyTrustAuthTrust验证手段
simV4A4进程内闭环
modbusV2A1写后读回,读回值即 metric;相悖报 FAILED
opcuaV2A2写后读回
homeassistantV2A1等 state_changed 事件;幂等执行回退到活读
mqttV2A1等设备状态回声;静默 = UNVERIFIED
bacnetV2A1写后读回;BACnet/IP 无设备身份,认证轴止于 A1
zigbeeV2A2加密网络 + 每设备链路密钥,读回属性
btmeshV2A2AppKey 认证每条消息;未收到 status 的 set 报 UNVERIFIED
threadV2A2CoAP PUT 后 GET 读回
can读 V2 / 写 V1A0传统 CAN 无应用层 ack、无设备身份,写入不得假装验证成功
bleV2A0广播即观测值,但无身份、无 ack;写入被拒绝而不是假装成功

摄像头这一族有自己的红线,因为它们是第一类"结果不是数字"的设备:

  • ptz.move 的速度契约统一为 [-1,1],厂商标度在适配器内部换算(海康 [-100,100]、 大华离散方向码加 1–8 速度)。调用方不需要知道这是谁家的相机。
  • 每次 move 都是有界的,发完必停。ONVIF 的 continuous move 不停就一直转。
  • 位置回读是 PTZ 的验证手段。相机到机械限位时会照常 ack 却纹丝不动;只信 ack 就会 报告一次没发生的转动。这种情况报 UNVERIFIED 并说明"位置未变"。
  • verify_stream 打开后,取流会真做一次 RTSP DESCRIBE 拿到 SDP 才算验证通过。 GetStreamUri 只是返回一个字符串,相机会为一条编码器没起来的流照样发 URI。

五种效果状态的完整语义见效果状态参考。

已知缺口

这些是当前代码的实际状态,不是路线图上的措辞。

Matter 栈没有接成适配器。 matter/ 下九个包齐备(TLV、mDNS 发现、SPAKE2+、PASE、 帧、exchange/MRP、加密会话、交互模型、入网序列),SPAKE2+ 对 RFC 9383 与 connectedhomeip 官方向量双钉,PASE 与一只 matter.js 虚拟灯完成了完整握手、双方导出 同一会话密钥(那是真实的 Matter 协议实现,但不是硬件设备;硬件握手没有记录)。但 cmd/anetlinkd/ 下没有 plugin_matter.go —— 这套栈今天还不能作为设备出现在 runtime 的设备表里, 也不会出现在 --scan 的结果中。

厂商云适配器为零。 adapters/ 下 14 个适配器全部走本地网络或局域网内的厂商 HTTP 接口。没有任何一个适配器接厂商云(小米、涂鸦、Aqara 之类)。这意味着一批只提供 云 API 的设备目前接不进来 —— 并且即使接进来,它们的效果也封顶在 V1,永远报不出可验证 的 OK。

摄像头的 PTZ / 事件 / 抓拍没有真机验证记录。 L0 假件覆盖了全部路径(真 SOAP 与 ISAPI/CGI 线格式、真 multipart 事件流),L1 用 onvif-server 覆盖发现、能力投影与 RTSP 探测 —— 但 onvif-server 只实现 Profile S,PTZ、事件、JPEG 抓拍这三样必须真机。海康 ISAPI 与 大华 CGI 是厂商私有协议,不存在模拟器,整条路径都只能在 L2 跑。相应的 -tags realworld 测试写好了(TestRealPTZ、TestRealSnapshotIsRealBytes、 TestRealStreamProbe),等的是把 ANETLINK_HIKVISION_ADDR 之类指向真机。

TestRealPTZ 会真的转动云台(跑完会尽量转回去)。不要对着此刻有人在看的画面跑。

已经验证过的那部分要一并说清楚:ANetLink 的 ONVIF 适配器驱动 ANetMock 里的模拟海康球机 跑通过 —— 绝对定位经位置回读确认(pan 0 → 0.6)、抓拍拿到 2391 字节真 JPEG 并内容寻址、 RTSP DESCRIBE 得到 H265。这三个数字取自 ANetMock/README.md 的自述,本页没有重跑这条链路。 ANetMock 说真实线协议且状态闭环,但它不是真机;两者不能互相替代。

自动发现覆盖 5/14。 见上文第二步的清单。其余适配器要手写地址与寄存器/节点映射。

排查

症状原因与处置
adapter "x" configured but not compiled into this build (built with no_x?)config 引用了被减法 tag 裁掉的适配器。重构建不带那个 no_x,或删掉 config 段
adapter "x" is configured but not compiled in — rebuild with -tags x引用了加法 tag 的适配器(onvif / hikvision / dahua / adap)。按提示加 tag 重构建
sdk: forced backend "y" unavailable: ...backend_override 指定的后端探测失败。检查宿主设施是否真在跑
sdk: no backend available — ...该适配器所有后端都探测失败,错误里逐个列了原因
daemon 启动失败 register provider on ...anetlinkd 没起、socket 路径不对、或权限不足。先起 anetlinkd 再起 daemon
hub 上看不到设备能力接完 anetlink 之后 daemon 没重启(模块只在启动时构建),或者重启时 hub 不可达 —— 启动时那次注册刷新失败只写一行日志。anet logs 里找 could not refresh this node's registration,修好后重启,或跑一次 anet hub-register
anet find --cap 找不到,但 anet find 能搜到前者是精确/前缀匹配。确认 id 一字不差,包括 @ 后面的适配器名与设备 id
效果总是 UNVERIFIED设备 MaxTrust < V2,或读回/事件没到。这是诚实行为不是缺陷 —— 命令发了,但这一级验证不了
--demo 输出 no devicessim 被 no_sim 裁掉或 config 里关了。这是预期的优雅退化
MQTT 适配器发现 0 设备broker 通但没有 zigbee2mqtt 的 retained bridge/devices。确认 z2m 在同一个 broker 上

更多见排查手册。

接下来