手上有一台装了 Win10 的笔记本,桌角摆着一台 LoRa 网关和几个 LoRaWAN 节点,想把链路从“节点发包”一路打通到“应用收到数据”,中间必须得有一个网络服务器。ChirpStack 是这条链路上最常见的选择,问题在于它整套服务都跑在 Linux 上,而日常办公、串口调试、抓包工具又都堆在 Windows 里,于是“在 Win10 下搭建 Linux 环境的 LoRaWAN 服务器 ChirpStack”就成了很多人绕不开的一道坎。这篇内容就是把这个过程从头拆一遍:为什么这么选、环境怎么铺、容器怎么编排、网关怎么接进来、节点入网失败又该往哪儿查。
需要先说明这份内容的定位:它不是一份照抄就能跑通的官方文档,而是一个已经在 Win10 + Linux 混合环境下反复折腾过 LoRaWAN 端到端联调的人,把自己踩过的地方按顺序摆出来。跟着做的人,最好对 Docker、Linux 命令行有一点基础认知,但不需要事先懂 LoRaWAN 的协议细节——协议部分我会在使用到的地方顺手解释。整套东西能做什么:接收网关上报的 LoRaWAN 帧、完成 OTAA 入网、解密应用数据、把数据通过 MQTT 转发给你的业务程序、在 Web 控制台上管理网关、设备和密钥。适合搭建实验环境、做产品原型验证、或者给团队做内部演示的场合。
1. 方案选型:Win10 里套一层 Linux,账要先算清楚
1.1 先弄明白 ChirpStack 到底要几个服务
很多人第一次打开 ChirpStack 的部署说明就懵了,因为它的服务端不是一个进程,而是一组相互配合的组件。要跑起来至少需要这几样东西:负责处理 LoRaWAN 协议逻辑的网络服务器本体(v4 里叫 chirpstack)、负责把网关的私有协议翻译成 MQTT 的网关桥接(chirpstack-gateway-bridge)、一个 MQTT broker 用来做内部消息总线(通常用 mosquitto)、一个 PostgreSQL 存设备、密钥、会话和帧记录、一个 Redis 做缓存和调度。有些版本还会单独跑一个 REST 接口容器,方便用 HTTP 调试。
这套组合意味着内存占用不会太低。我实测下来,五个容器全开、没有设备流量的时候,常驻内存大概在 500MB 到 800MB 之间;节点和网关跑起来之后,PostgreSQL 的共享缓冲区会把内存往上推,900MB 到 1.2GB 是常态。所以如果那台 Win10 机器本身是 8GB 内存、还开着浏览器和 IDE,就有点挤了;16GB 以上会舒服很多。这个数字先摆在这里,是因为后面选方案的时候,虚拟机方案要额外吃掉 1GB 到 2GB 的固定开销,而容器方案相对轻。
还有个容易忽略的点:ChirpStack 对时间敏感。网关和服务器之间的帧时间戳、OTAA 会话里的计数、JWT 令牌的过期校验都依赖系统时钟。Linux 里的容器时钟跟随宿主,宿主又是从 WSL2 的虚拟时钟同步来的,Windows 睡眠、休眠之后 WSL2 的时钟漂移是很常见的坑,后面排查章节会专门讲。
1.2 三条落地路线的实测对比
Win10 上跑 Linux 服务,主流就这么几条路,我把它们放在一起对比过,结论是各有取舍,没有绝对赢家。
| 路线 | 网络接入难度 | 资源占用 | 启动速度 | 适合谁 |
|---|---|---|---|---|
| WSL2 + Docker Desktop | 低,端口自动发布到 Windows 网卡 | 中 | 快,秒级 | 一台机器兼顾办公和调试 |
| WSL2 发行版内装 Docker Engine | 高,UDP 端口要自己想办法转发 | 低 | 快 | 纯命令行党、不想装桌面软件 |
| 桌面虚拟化软件(VirtualBox、VMware) | 最低,桥接网卡后就是一台局域网主机 | 高 | 慢,分钟级 | 要做多套环境隔离、频繁重装 |
| Hyper-V 虚拟机 + 外部虚拟交换机 | 低 | 高 | 慢 | 已开启 Hyper-V 的机器 |
这里最值得展开的是第一条和第二条的区别,因为它直接决定网关能不能连进来。WSL2 本质上是一个轻量虚拟机,它有自己独立的虚拟网卡和 IP,Windows 主机和它之间是 NAT 关系。Windows 访问 WSL2 里监听的端口,靠的是 localhost 转发机制,这个默认是通的,所以你在浏览器里敲localhost:8080打开控制台没问题。但网关是外部物理设备,它往你的电脑 IP 发 UDP 包,这个包到了 Windows 网卡之后,默认不会被送进 WSL2 的虚拟网络里。
Docker Desktop 之所以省事,是因为它自带一个转发层,容器用-p 1700:1700/udp发布端口时,这个端口会真正绑定在 Windows 主机的网卡上,网关直接指过来就能通。而如果你在 WSL2 发行版里手动装 Docker Engine,容器端口只监听在 WSL 的虚拟网卡上,外部设备根本够不着。Windows 自带的netsh interface portproxy只支持 TCP,UDP 用不了,想转发 UDP 得借助额外工具,还得处理 WSL2 每次重启 IP 会变的问题。所以我的建议很直接:如果目标是把网关接进来跑通链路,优先用 Docker Desktop;如果只是自己拿虚拟节点做协议实验、不需要真实网关接入,那发行版内装 Docker Engine 完全够用,还更干净。
1.3 v3 与 v4 的架构差异,别照着旧教程抄
网上流传的教程里有一大半是 v3 时代的,配置文件的目录结构和 compose 文件跟现在的 v4 对不上,硬抄会在第一步就卡住。v3 的架构是拆开的:network-server 管协议,application-server 管业务和设备管理,gateway-bridge 管网关接入,三个服务各有一份配置文件,中间还夹着一个 app-server 的数据库。v4 把前两个合并成了单个 chirpstack 服务,配置只剩一份chirpstack.toml,数据库也合成一个。
这个变化带来的直接影响是:v4 的网关桥接里,MQTT 主题必须带区域前缀。因为一台服务器现在可以同时启用多个区域,网关上报的数据得靠主题前缀来区分是哪个频段。默认示例里写的是eu868,如果你把区域改成 470MHz 那一档,主题前缀不同步改,表现就是网关在线状态能显示,但收不到任何上行帧——这个问题我见过不止一次,后面排查章节会给出具体的定位方法。
版本号建议直接锁定大版本 tag,比如镜像写成chirpstack/chirpstack:4,不要用latest。原因是 v4 的小版本之间配置文件字段偶尔会调整,用latest拉下来一份新镜像,可能旧的 toml 里少了个必填项就起不来,而你根本不知道是哪次升级导致的。
2. 把 WSL2 和容器环境装到位
2.1 Windows 侧的前置开关
WSL2 对 Win10 版本有硬性要求,内部版本号得过 19041,也就是 2004 之后的版本。低于这个版本,wsl --install这类命令根本不存在。查版本的方式是在运行框里敲winver,看一眼版本号。
确认版本之后,需要打开两个 Windows 功能:适用于 Linux 的 Windows 子系统,以及虚拟机平台。图形界面里在“启用或关闭 Windows 功能”里勾选,命令行更方便:
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart执行完必须重启,不重启这两个功能只是标记为待生效,后面装发行版会报错。重启之后把默认版本设成 2:
wsl --set-default-version 2这里有个细节值得说一下:如果你的机器之前装过 WSL1 的发行版,默认版本改了也不会自动迁移,得对已有的发行版单独执行转换命令,否则你会发现 Docker 装完启动就报各种与 cgroup 相关的错误。这是我早期遇到过的一次,排查了半天才想起来版本没转。
2.2 发行版选择与基础工具
发行版我用的是 Ubuntu 22.04 LTS,理由很朴素:ChirpStack 官方镜像的构建基线、社区里绝大多数问题帖的环境、以及各类依赖包的版本都对得上,遇到问题搜索时命中率高。用更新的版本也可以,但要注意某些包的行为变化,比如新版对 cgroup v2 的默认处理,会影响到老版本容器的资源限制。
安装命令在较新的 Win10 上直接一条搞定:
wsl --list --online wsl --install -d Ubuntu-22.04装完首次进入会让你设用户名和密码,这个账号默认没有 root 权限,日常操作靠 sudo。进入系统后先做两件事:换一个顺手的软件源(如果你所在网络的默认源速度不理想),以及安装一批基础工具:
sudo apt-get update sudo apt-get install -y curl gnupg ca-certificates net-tools iproute2 jqnet-tools是为了ifconfig这种老命令,虽然新系统推荐ip addr,但排查网络时有些脚本还在用它;jq用来格式化 MQTT 报文里的 JSON,调试阶段非常省眼力。
WSL2 默认不启用 systemd,这意味着你用systemctl管理服务会失败。Ubuntu 22.04 配合较新的 WSL 版本可以在/etc/wsl.conf里打开:
[boot] systemd=true改完要在 Windows 侧执行wsl --shutdown让整个 WSL 实例彻底重启才生效,只重启发行版窗口是不够的。这一步如果漏了,后面装 Docker 时服务起不来,报错信息还很模糊。
2.3 Docker 装在哪:发行版内还是桌面版
如果选择 Docker Desktop 路线,装完之后在设置里确认“使用基于 WSL2 的引擎”是开着的,并且在“Resources → WSL Integration”里把你要用的那个发行版勾上。这样你在 WSL 终端里敲docker命令,实际操作的是 Docker Desktop 的后端,容器端口也会正确发布到 Windows 主机。这是个很妙的组合:命令行体验是纯 Linux 的,网络行为是桌面版帮你兜住的。
如果走发行版内自建路线,用官方源安装,别用系统仓库里那个老掉牙的版本:
sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | sudo tee /etc/apt/sources.list.d/docker.list sudo apt-get update sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin sudo usermod -aG docker $USER最后那句把当前用户加进 docker 组,执行完要退出终端重新登录才生效,否则你会一直看到“权限被拒绝”的提示。这个提示很误导人,很多人以为是 Docker 没装好。
2.4 网络形态决定网关能不能连进来
这一步是整个搭建过程中最容易翻车的地方,值得单独说透。先把 WSL2 的 IP 摸清楚:
ip addr show eth0 | grep inet你会看到一个 172 开头的地址,这是 WSL 在虚拟网络里的地址。Windows 主机在 WSL 视角下通常是这个网段的网关地址。关键结论:外部设备(网关)看到的只有 Windows 主机在局域网里的那个 IP(一般是 192.168 开头),它不知道也不关心 WSL 的存在。
用 Docker Desktop 时,容器端口发布在 Windows 主机的所有网卡上,你在浏览器敲http://<Windows局域网IP>:8080能打开控制台,网关把服务器地址填成同一个 IP、端口 1700,UDP 包也能进来。前提是 Windows 防火墙放行。第一次启动容器时系统会弹窗询问是否允许,如果当时点了取消,后面就得手动加规则:
New-NetFirewallRule -DisplayName "ChirpStack UDP 1700" -Direction Inbound -Protocol UDP -LocalPort 1700 -Action Allow New-NetFirewallRule -DisplayName "ChirpStack Web 8080" -Direction Inbound -Protocol TCP -LocalPort 8080 -Action Allow用发行版内自建 Docker 时,TCP 可以靠端口转发曲线救国:
netsh interface portproxy add v4tov4 listenport=8080 listenaddress=0.0.0.0 connectport=8080 connectaddress=172.x.x.x但 UDP 这条路走不通。能想到的替代办法有几种:把网关桥接这一个组件单独挪到 Windows 主机上跑(官方有 Windows 版可执行文件),让它去连 WSL 里的 MQTT;或者在 Windows 上跑一个 UDP 转发小工具,把 1700 端口收到的包转进 WSL。这两种做法都能跑,但配置成本明显高于直接用 Docker Desktop,所以我一般不在这一步上省事。
还有一个隐藏问题:WSL2 的虚拟 IP 在每次wsl --shutdown之后会变。如果你写死了端口转发规则指向旧 IP,重启后转发就失效了,表现是控制台打不开但容器明明在跑。这也是我不推荐在 Win10 上折腾裸 WSL2 端口转发的核心原因。
3. 编排 ChirpStack:compose 文件逐段拆解
3.1 目录结构与版本锁定
先建一个工作目录,把配置、数据、日志分开,别让容器写得到处都是:
mkdir -p ~/chirpstack/configuration/{chirpstack,chirpstack-gateway-bridge,mosquitto,postgresql/initdb} cd ~/chirpstack这个结构对应的是官方示例的惯例,好处是升级镜像时配置文件在宿主机上,容器随便删。postgresql/initdb这个目录尤其重要,PostgreSQL 官方镜像会在数据目录为空时自动执行里面的.sh和.sql脚本,用来创建数据库和角色。如果这个目录是空的,数据库容器能起来,但 chirpstack 连上去会报“数据库不存在”,日志里一长串连接失败。
版本方面,我会把镜像 tag 全部写死大版本号,数据库和缓存也指定具体的小版本,避免哪天上游推了新版本把环境搞崩。
3.2 六个容器的 compose 写法
下面这份docker-compose.yml是我实际在用的精简版,去掉了示例里一些不常用的部分:
services: chirpstack: image: chirpstack/chirpstack:4 command: -c /etc/chirpstack restart: unless-stopped volumes: - ./configuration/chirpstack:/etc/chirpstack depends_on: - postgres - redis - mosquitto environment: - MQTT_BROKER_HOST=mosquitto - REDIS_HOST=redis - POSTGRESQL_HOST=postgres ports: - "8080:8080" chirpstack-gateway-bridge: image: chirpstack/chirpstack-gateway-bridge:4 restart: unless-stopped volumes: - ./configuration/chirpstack-gateway-bridge:/etc/chirpstack-gateway-bridge ports: - "1700:1700/udp" depends_on: - mosquitto postgres: image: postgres:14-alpine restart: unless-stopped volumes: - ./configuration/postgresql/initdb:/docker-entrypoint-initdb.d - postgresqldata:/var/lib/postgresql/data environment: - POSTGRES_PASSWORD=chirpstack redis: image: redis:7-alpine restart: unless-stopped command: redis-server --save 300 1 --save 60 1 --save 10 1 volumes: - redisdata:/data mosquitto: image: eclipse-mosquitto:2 restart: unless-stopped volumes: - ./configuration/mosquitto/mosquitto.conf:/mosquitto/config/mosquitto.conf ports: - "1883:1883" volumes: postgresqldata: redisdata:几个值得解释的地方。command: -c /etc/chirpstack是告诉 chirpstack 去读哪个目录下的配置文件,v4 的镜像默认就是读这个路径,但写出来更清晰,也方便你以后挂载到别的位置。restart: unless-stopped保证 Docker 服务重启后容器自动起来,但你自己手动 stop 的不会被拉起,这个语义比较符合调试习惯。
Redis 那条--save参数是持久化策略,意思是 300 秒内有 1 次写入、或 60 秒内有 1 次、或 10 秒内有 1 次就落盘。ChirpStack 用 Redis 存设备的会话状态和调度队列,如果完全关掉持久化,容器重启后所有 OTAA 会话丢失,节点得重新入网。对实验环境这不算大事,但如果你在做长时间功耗测试,会话频繁丢失会污染数据,所以我把这条留着了。
mosquitto 的 1883 端口我发布出来了,理由是调试期间需要在宿主机上订阅主题看报文。如果只在容器网络内部用,可以去掉这个映射,安全性更好一些。
数据库初始化脚本configuration/postgresql/initdb/001-init-chirpstack.sh:
#!/bin/bash set -e psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL create role chirpstack with login password 'chirpstack'; create database chirpstack with owner chirpstack; EOSQL这个脚本只在数据库目录为空时执行一次。如果你改过里面的密码又重启容器,新密码不会生效,因为数据目录已经有内容了。想重来就得把 volume 删掉,这一点在做实验时经常忘记。
mosquitto 的配置configuration/mosquitto/mosquitto.conf:
listener 1883 allow_anonymous true匿名访问在本地实验环境里够用,省去建账号的麻烦。但如果这台机器在公共网络里,务必改成带认证的配置,并且在 chirpstack 和网关桥接的配置里同步填上用户名密码。我见过有人把这种裸奔的 broker 挂在办公网里,结果被其他同事的调试脚本误连,日志里全是无关主题。
3.3 三份配置文件的关键字段
chirpstack 的主配置configuration/chirpstack/chirpstack.toml,最小可用版本大概是这样:
[logging] level="info" [postgresql] dsn="postgres://chirpstack:chirpstack@postgres/chirpstack?sslmode=disable" [redis] url="redis://redis/" [network] net_id="000000" enabled_regions=["cn470"] [api] bind="0.0.0.0:8080" secret="change-this-to-something-random" [regions] [[regions]] id="cn470" description="CN470" common_name="CN470"net_id是 LoRaWAN 网络标识,实验环境随便填六位十六进制就行,但同一网络里所有设备要一致。enabled_regions必须和下面[[regions]]里的id对得上,写错了启动时容器直接退出,日志里会说区域未注册。api.secret是签发登录令牌用的密钥,别用默认值,虽然实验环境风险不大,但改一下是零成本的事。
470MHz 这一档要特别提醒:ChirpStack 的区域默认信道规划是完整的一套,而绝大多数网关只支持其中一段。如果你用的网关是常见的八信道机型,它的收发频点是固定的,必须在设备配置文件和网关侧对齐,否则节点发出去的信道服务器根本没监听。这个细节后面第四章会具体讲怎么配。
网关桥接的配置configuration/chirpstack-gateway-bridge/chirpstack-gateway-bridge.toml:
[backend.semtech_udp] udp_bind = "0.0.0.0:1700" [integration] enabled=["mqtt"] [integration.mqtt] event_topic_template="cn470/gateway/{{ .GatewayID }}/event/{{ .EventType }}" command_topic_template="cn470/gateway/{{ .GatewayID }}/command/#" [integration.mqtt.auth] type="generic" [integration.mqtt.auth.generic] servers=["tcp://mosquitto:1883"]udp_bind绑定 0.0.0.0 是为了容器外能访问,绑 127.0.0.1 的话端口映射会失效。主题模板里的cn470必须和 chirpstack 里区域 id 完全一致,大小写敏感。这一点踩过的人很多:网关状态能显示在线(因为状态主题是另一条路径,服务器会宽松处理),但上行帧一条都进不来,排查时会误以为是射频问题,其实只是主题前缀写错了。
command_topic_template末尾的#是通配符,必须保留,网关桥接要靠它订阅下行命令。这个符号漏掉,OTAA 入网时节点永远收不到入网接受帧,日志里能看到服务器已经生成了下行,但桥接那边没转发出去。
3.4 首次启动与自检
编排文件写完,先做语法校验再启动:
docker compose config docker compose up -d docker compose psconfig命令会把变量替换后的最终配置打印出来,能提前发现缩进错位这类问题。ps里重点看 postgres 和 chirpstack 两个容器的状态,如果是Restarting或者Exit,直接看日志:
docker compose logs -f chirpstack docker compose logs -f postgres最常遇到的启动失败是数据库连接被拒。日志里出现connection refused或者database "chirpstack" does not exist,八成是初始化脚本没执行。检查一下那个.sh文件有没有可执行权限,虽然官方镜像会执行它,但挂载进去的文件权限不对时也可能失败。
容器状态都正常之后,浏览器打开http://localhost:8080,能看到登录页说明主链路通了。初始账号密码是 admin/admin,登录后会强制改密码。如果你的机器上 8080 被别的服务占了(比如某些开发工具默认就用这个端口),改映射的左边那一半就行,容器内部端口不用动——不过要记得external_url之类的配置如果写了绝对地址,得同步改。
4. 控制台初始化与端到端联调
4.1 租户、应用、设备配置文件
登录之后的层级关系是:租户 → 应用 → 设备,设备要绑定一个设备配置文件。新装好的系统里除了管理员账号什么都没有,得先把这套骨架搭起来,否则添加设备时会发现下拉框是空的。
租户相当于一个组织单元,个人实验随便填个名字就行,关键是它下面的用户权限模型。应用是业务维度的容器,同一个应用下的设备上行数据会进同一个 MQTT 主题前缀,所以按项目划分比较合理,比如“温湿度试点”“水表批量测试”各建一个。
设备配置文件是真正决定射频行为的东西,几个参数必须和节点固件严格一致:
| 参数 | 常见取值 | 不匹配会怎样 |
|---|---|---|
| LoRaWAN MAC 版本 | 1.0.3 / 1.0.4 | 会话密钥推导方式不同,入网后数据解不开 |
| 区域参数修订版 | RP002-1.0.3 等 | 信道计划和最大占空比解释不一致 |
| 区域 | CN470 | 频点和数据速率完全对不上 |
| 设备类别 | Class A | Class B/C 的下行时机理解不同 |
| 上行数据速率上限 | DR0 到 DR5 | 节点用了超出上限的速率,服务器拒绝或丢弃 |
我的经验是:节点固件里用的是什么 MAC 版本和区域参数版本,就在这里选同样的,别凭感觉。尤其是 1.0.3 和 1.0.4,表面上只差一个小版本,但 1.0.4 引入了设备时间这类可选命令,某些节点栈在上行帧里带了相关字段,服务器不认就直接丢包。
4.2 网关注册与 UDP 接入
网关侧需要填的就三样:服务器地址(Windows 主机的局域网 IP)、上行端口 1700、下行端口 1700。有些网关的配置界面里叫 packet forwarder 设置,本质上是一套约定俗成的 UDP 报文格式,ChirpStack 的网关桥接就是按这个格式解析的。
服务器这边要先在控制台里添加网关,Gateway ID 填网关的 8 字节 EUI,十六进制、十六个字符。这个值在网关的标签或者管理页面上能找到,抄的时候注意别把大小写或者分隔符带进去。填错了的表现很有意思:网关在桥接那边能连上(因为桥接本身不校验 EUI),但服务器收到的帧找不到对应网关,直接丢弃,控制台里网关状态一直是灰色的“从未连接”。
v4 里有个开关叫是否允许未知网关接入,默认关着。这意味着你必须先添加网关,网关的数据才会被处理。这个设计在多人共用一台服务器时是好事,能防止别人家的网关乱入;但在单人调试时容易忘,添加完才想起还有这一步。
判断网关是否真的通了的办法很直接:在宿主机上订阅状态主题。
docker exec -it chirpstack-mosquitto-1 mosquitto_sub -t 'cn470/gateway/#' -v如果网关配置正确、网络通畅,每 30 秒左右会看到一条 stats 报文,里面有收发帧计数、温度、时间戳。看到这个就算物理链路通了,接下来才是节点的活。看不到的话,先确认网关里填的 IP 是 Windows 主机的局域网 IP 而不是 127.0.0.1,再确认防火墙放行了 UDP 1700,最后用抓包工具在 Windows 上看有没有包进来。
4.3 设备注册:OTAA 与 ABP 怎么选
OTAA 是空口入网,节点上电后发入网请求,服务器下发会话密钥,双方各自推导。这种方式密钥不在空口明文传输,安全性好,而且节点换网络时不用改固件。ABP 是预先烧录会话密钥,跳过了入网握手,上电就能发数据。
实验阶段我强烈建议用 OTAA,理由是它把问题暴露得更早更清楚。用 ABP 时如果密钥填错,表现是设备显示在线、数据也能收到,但解析出来全是乱码,你得去怀疑加密算法、帧计数器、端口号,排查面很宽。OTAA 则干脆利落:入网不成功就是没有会话,日志里直接告诉你哪一步失败。
注册设备要填三个值:DevEUI、JoinEUI(老版本叫 AppEUI)、AppKey。这三个值必须和节点固件里烧的一模一样,一个字节都不能差。实践中最容易出错的是字节序,某些厂商的文档里写的是小端序,而界面上要的是大端序展示。建议填完之后把界面上的值抄下来,和固件里的定义逐字节比对一次。
4.4 用 MQTT 订阅看真实报文
节点上电,观察上行主题:
docker exec -it chirpstack-mosquitto-1 mosquitto_sub -t 'cn470/gateway/+/event/up' -v这条命令看的是网关桥接的原始事件,能看到物理层参数:频率、扩频因子、带宽、信噪比、CRC 状态。如果节点在发但这里没有输出,说明网关没收到或者没转发,问题在射频侧或者网关配置侧。如果有输出但服务器没处理,问题在区域前缀或者网关注册。
再往上一层,看应用侧的数据:
docker exec -it chirpstack-mosquitto-1 mosquitto_sub -t 'application/#' -v这里能看到解密后的 payload(通常是 base64 或者 hex)、解析出的对象、以及设备的各种元数据。前提是你在应用里配置了 MQTT 集成,指向同一个 broker。这一步很多人会漏,做完之后发现应用里能看到数据,但自己的程序订阅不到,原因就是没开集成。
两层的对比定位法非常有效:原始事件有、应用数据没有,说明协议层有问题,重点看会话、密钥、帧计数;两层都没有,说明物理层或者网关链路有问题,重点看射频和网络。我排查时基本都先用这个二分法把范围砍一半。
5. 排查实录:从“网关不在线”到“join 一直失败”
5.1 网络与端口类问题
网关状态一直灰色。先看桥接容器日志里有没有收到包。有包但服务器没记录,检查网关 EUI 是否已在控制台注册、是否和网关上显示的一致。完全没包,从 Windows 侧抓包确认 UDP 是否到达,逐层往上找。见过一次是公司网络里交换机开了端口隔离,网关和电脑不在同一个广播域,改成同一网段就好了。
控制台能打开但局域网其他设备打不开。用 Docker Desktop 时端口发布在 0.0.0.0,理论上是通的,八成是防火墙。用裸 WSL2 时基本可以确定是 NAT 没转发,参考第二章的方案换掉。
容器全部正常但 MQTT 订阅没反应。检查 mosquitto 容器的日志,看有没有客户端连接被拒。如果 chirpstack 和桥接都连不上 broker,多半是容器网络里的服务名解析不到,确认 compose 里服务名拼写正确,以及它们确实在同一个默认网络里。
5.2 入网与频率类问题
入网请求发出去了,服务器也收到了,但节点收不到接受帧。这个现象几乎全部指向下行链路。检查三件事:网关桥接的 command 主题模板末尾有没有#;网关的收发频段是否覆盖服务器的下行频点(470MHz 这一档的下行通常在 500MHz 以上,有些网关的射频前端并不覆盖这一段);网关的下行发射功率设置是否过低。
入网成功但一断电重启就再也入不了网。这是 v4 的 DevNonce 校验引起的。服务器会记住设备用过的随机数,节点断电后随机数生成器从头开始,重复的值直接被拒。日志里能看到明确的 nonce 相关提示。解决办法是在设备详情页找到清空 nonce 的操作,或者确认节点固件是否支持持久化 nonce。做批量测试时这个坑特别烦,建议在脚本里留一步自动清理。
数据能收到但间隔忽长忽短。检查占空比限制和自适应速率。服务器会按区域规范限制下行占空比,如果节点配置的上行速率过高,服务器可能下发速率调整命令,节点改速率后重传,看起来就是间隔不稳定。另外检查网关是否有丢包,看 stats 报文里的收包计数和节点发送计数能不能对上。
5.3 常见问题速查表
| 现象 | 优先怀疑 | 快速验证 |
|---|---|---|
| 网关灰色,无 stats | IP/端口填错、防火墙拦截 | 宿主机订阅cn470/gateway/# |
| 有 stats,无上行 | 主题前缀与区域 id 不一致 | 核对 toml 里两个字符串 |
| 有上行,无应用数据 | 应用未配 MQTT 集成 | 订阅application/# |
| 入网无响应 | 下行主题模板缺#、频段不覆盖 | 看桥接日志有无 tx 记录 |
| 入网后数据乱码 | MAC 版本或密钥不一致 | 逐字节比对 DevEUI/AppKey |
| 重启后无法入网 | DevNonce 重复 | 清空设备 nonce 记录 |
| 容器重启后数据丢失 | 未挂载 volume 或初始化脚本未执行 | docker volume ls核对 |
还有一个不太常见但很难查的问题:容器跑了一段时间后 chirpstack 报数据库连接超时。如果不是资源不足,多半是 WSL2 的时钟漂移导致的,Windows 睡眠唤醒后 WSL 的时钟可能落后几分钟,PostgreSQL 的连接超时判断、令牌过期校验都会出问题。重启一次 WSL 实例通常能恢复,长期方案是在 Windows 侧安排一个定时校时任务,或者干脆别让主机长时间睡眠。
6. 长期跑的几点经验
6.1 数据落盘与备份
PostgreSQL 的 volume 是整个环境里唯一不能丢的东西,设备密钥、会话、帧记录全在里面。实验阶段我建议每天导出一次:
docker exec chirpstack-postgres-1 pg_dump -U chirpstack chirpstack > backup_$(date +%F).sql这个命令在容器名和 compose 服务名之间容易搞混,实际容器名是目录名-服务名-序号,先用docker ps看一眼再执行。导出文件放在 Windows 能访问的目录里更保险,WSL 的 ext4 虚拟磁盘文件如果因为意外损坏,里面所有东西都会一起没。
WSL2 的磁盘还有个特点:只增不减。你删掉大量文件之后,Windows 上的那个 vhdx 文件不会自动缩小,时间长了能占几十 GB。需要手动压缩,先wsl --shutdown再执行磁盘整理,具体命令跟系统版本有关,建议查一下当前版本支持的写法。
6.2 资源与时钟
给 WSL2 分配的资源默认是动态的,可能把你机器的内存吃满。在 Windows 用户目录下建一个.wslconfig文件限制上限:
[wsl2] memory=6GB processors=4 swap=2GB改完同样要wsl --shutdown。6GB 对这套服务来说比较宽裕,如果还要在同一台机器上跑其他开发工具,压到 4GB 也能勉强跑,但数据库的共享缓存会变小,查询会慢一些。
时钟问题的处理我在上一节提过,这里补一个更省事的办法:在 Windows 里加一个每小时的校时任务,指向系统自带的时间同步服务。WSL 的时钟跟随宿主,宿主准了,容器里的时间就基本不会偏太多。如果你在做需要精确时间戳的数据分析,这一点比想象中重要。
6.3 从练习环境走向长期运行
实验环境和长期运行的差别,主要在于你会不会在某天早上发现服务莫名其妙停了。几个能让它多撑一阵的做法:去掉 compose 里对单次挂载的强依赖,确保任何容器单独重启都不会导致整个链路断掉;把配置目录纳入版本管理(密钥相关的文件记得排除),改坏了能快速回滚;在宿主机上放一个每分钟检查一次控制台端口的小脚本,不通就自动重启容器。
不过说实话,Win10 加 WSL2 这套组合更适合做开发和演示,不适合当生产服务器。WSL 的生命周期跟着 Windows 用户会话走,注销、重启、系统更新都会打断它。真要把 ChirpStack 长期挂着给一批设备用,还是得挪到一台常开的 Linux 主机上,而你在 Win10 上磨合出来的这套 compose 和配置文件,可以原样搬过去,几乎不用改。
我自己在这个环境上花时间最多的部分,其实不是装,而是第一次让节点成功入网的那几个小时——所有容器都是绿的,网关在线,上行帧一条条进来,但节点就是收不到接受帧。最后发现问题出在那份网关桥接配置的主题模板末尾,#被我漏掉了。这个错误在日志里没有任何直接的提示,服务器认为自己已经把下行发出去了,桥接那边因为没订阅到命令主题,安静得像什么都没发生。从那之后我养成一个习惯:每次新建环境,先打开发送上行和下发命令两个日志窗口,各发一条测试帧,两边的计数都能对上,再去动设备配置文件。省下来的排查时间,比多读几篇文档都值。