用Qt和nmcli打造Linux嵌入式WiFi连接界面 简介在Linux下使用QT框架编写Wi-Fi连接程序常需兼顾界面设计与底层网络管理。这份工程示例面向嵌入式、桌面开发的初中级学习者基于Ubuntu 16.04环境演示如何结合NetworkManager、wpa_supplicant等工具实现Wi-Fi扫描、连接与状态反馈。压缩包共12个文件包含4个cpp源文件、3个头文件、3个ui界面文件以及pro工程配置和user用户配置源码结构清晰便于直接导入QT Creator阅读修改。包体仅9KB轻量且聚焦核心逻辑。目前已有4523人学习浏览适合想快速上手Linux无线网络编程的开发者。通过该工程读者可以掌握QNetworkConfigurationManager扫描热点、QNetworkConfiguration激活连接的方法并了解密码输入、连接状态提示等交互设计思路还可学习将网络操作封装为独立管理类以降低界面与业务逻辑耦合的组织方式对后续开发跨平台GUI或嵌入式联网应用有直接参考价值。1. Linux 下的 Qt WiFi 连接程序把命令行变成图形界面一台跑着 Linux 的 ARM 工控机要连产线 WiFi现场工人总不能每次都在设备旁边开终端敲 wpa_cli。这个 Qt 写的 WiFi 连接程序解决的就是操作入口扫描热点、输密码、连接、显示信号、掉线自动重连。底层用 QProcess 调 nmcli让 NetworkManager 和 wpa_supplicant 干活界面用 Qt Widgets 画。核心只有三个类WifiManager 封装扫描、连接和状态监测WifiListModel 给 QListView 供数据MainWindow 管交互与密码框。代码量不大但牵扯 QProcess 异步、字符编码、系统服务和交叉编译坑不少。适合给嵌入式设备做 HMI 的开发者、在工控机上要快速切网络的集成商以及想在 Qt 应用里集成 WiFi 能力的人。下面按技术选型、核心模块、编译运行、避坑、二次开发的顺序拆。2. 技术选型与架构设计QProcess 调 nmcli还是 D-Bus 直连 NetworkManager动手写代码前先得想清楚一件事程序跟系统里的 WiFi 管理栈怎么对话。Linux 下这块的常规链路是「网卡驱动 → wpa_supplicant → NetworkManager → 上层工具」Qt 程序可以选择在这一层的不同位置接入。选错位置后面全是返工。2.1 三条技术路线与取舍路线 A 是 QProcess 调 nmcli。nmcli 是 NetworkManager 自带的命令行客户端扫描、连接、看状态都有现成命令程序里只需把 stdout 拿回来解析。开发量最小跟系统服务天然同步缺点是拿到的是一坨文本SSID 里的特殊字符需要自己处理。路线 B 是 QtDBus 直连 NetworkManager 的 D-Bus 接口。NetworkManager 把网卡、连接、访问点都暴露成 D-Bus 对象可以拿到结构化数据也能订阅信号实时感知网络变化比如断线瞬间就能收到通知。代价是要理解 org.freedesktop.NetworkManager 的对象路径和接口语义。路线 C 是绕开 NetworkManager直接用控制接口驱动 wpa_supplicant。适合目标系统里压根没有 NetworkManager 的最小嵌入式环境但需要自己维护配置文件、处理关联状态、手动起 DHCP工作量翻倍。这个工程选的是路线 A。目标板系统里 NetworkManager 是现成的产品要的是「快速交付、能连上、别捣乱」nmcli 路径最务实。同时把 WifiManager 的接口设计成与具体实现解耦将来要换成 D-Bus 或 wpa_supplicant只改一个类内部就能完成。三条路线的对比路线对外依赖开发量数据结构适用场景QProcess nmcliNetworkManager小文本行需自解析系统已有 NetworkManager求快QtDBus 直连NetworkManager D-Bus中对象化含信号通知需要精细监听网络事件wpa_supplicant 直连仅 wpa_supplicant大socket 文本协议最小系统无 NetworkManager如果以后走路线 B入口大概是这样的// 通过系统总线拿到 NetworkManager 的接口对象 QDBusInterface nm(org.freedesktop.NetworkManager, /org/freedesktop/NetworkManager, org.freedesktop.NetworkManager, QDBusConnection::systemBus()); // 调用 GetDevices 拿到网卡对象路径列表 QDBusReplyQListQDBusObjectPath reply nm.call(GetDevices);这段代码说明两点D-Bus 调用能直接得到对象路径而不是文本方便后续操作具体网卡但前提是程序运行在系统总线有权限的环境里后面避坑章节会讲权限问题。2.2 工程目录界面与逻辑分开整个工程按「核心逻辑 界面」分层目录结构如下wifi-manager/ ├── wifi-manager.pro # qmake 工程文件 ├── main.cpp # 程序入口负责初始化 QApplication ├── core/ │ ├── wifimanager.h # 网络动作封装扫描/连接/状态轮询 │ ├── wifimanager.cpp │ ├── wifimodel.h # QAbstractListModel 子类供热点列表展示 │ └── wifimodel.cpp └── ui/ ├── mainwindow.h # 主窗口列表、扫描按钮、状态栏 ├── mainwindow.cpp ├── connectdialog.h # 密码输入对话框 └── connectdialog.cppcore 目录下是两个纯逻辑类不依赖任何 QWidget只有线程和进程相关的 Qt 模块。ui 目录下的类只管界面拿到核心层发来的信号后刷新视图。这样分层的好处是如果产品里要多一个命令行版本或者把 WiFi 逻辑做成服务跑在后台直接复用 core 就行。2.3 数据流与状态机数据流是单向的界面按钮触发 WifiManager 的公开方法WifiManager 通过 QProcess 启动 nmclinmcli 经 D-Bus 让 NetworkManager 干活返回时 stdout 文本被 QProcess 异步读回解析成 WifiInfo 结构体列表再通过信号发给界面。整个过程里UI 线程永远不等待。程序内部维护一个简单的状态机用来决定当前收到的输出属于哪条命令enum class WifiState { Idle, // 空闲什么动作都没有 Scanning, // 正在执行扫描命令 Connecting, // 正在执行连接命令 Connected, // 已连接且状态轮询确认过 Reconnecting // 检测到断线正在重连 };状态的迁移规则直接对应界面表现当前状态触发条件界面表现Idle程序启动列表清空扫描按钮可用Scanning用户点扫描或重连前需要重新扫列表置灰转圈Connecting用户输入密码点连接密码框禁用显示连接中Connectednmcli 返回成功或轮询发现已连列表高亮当前网络Reconnecting轮询发现断线且存在保存的连接状态栏提示重连中很多初版代码没有这个状态机结果就是扫描命令的输出和连接命令的输出混在一起解析逻辑写得像一团乱麻。有了状态每个 QProcess 回调里先判断当前状态再决定怎么处理逻辑一下就清晰了。3. 核心模块拆解从扫描到连接的完整链路这一章把最核心的三段代码拿出来逐行讲。整个 WiFi 流程可以压缩成三句话让系统扫一遍热点并拿回列表拿着用户选的 SSID 和密码去关联然后盯着连接状态防止掉线。3.1 热点扫描先 rescan 再 list扫描不能上来直接拉列表。nmcli 的dev wifi list读的是 NetworkManager 里的访问点缓存缓存可能是几分钟前的旧数据。正确姿势是先主动触发一次扫描等网卡把结果收回来再调 list 读取。这里用两个 QProcess 串行处理// wifimanager.cpp void WifiManager::startScan() { // 如果上一个扫描进程还在跑先杀掉避免两个 nmcli 抢 stdout if (m_scanProc-state() ! QProcess::NotRunning) { m_scanProc-kill(); m_scanProc-waitForFinished(300); } m_state WifiState::Scanning; // 第一步主动触发一次网络扫描 m_scanProc-start(nmcli, {dev, wifi, rescan}); } void WifiManager::onRescanFinished() { // rescan 是异步触发的没有“扫描完成”回调 // 等 2 秒让网卡把探测帧收回来再拉列表 QTimer::singleShot(2000, this, [this]() { m_listProc-start(nmcli, {-t, --escape, no, -f, SSID,SIGNAL,SECURITY,ACTIVE, dev, wifi, list}); }); }-t是 terse 简化输出每行一条、字段间用冒号分隔方便程序解析--escape no表示不做转义处理否则中文 SSID 会被转成\x开头的一串-f指定只需要 SSID、信号强度、加密方式和是否已连接这四列。rescan和list各用独立的 QProcess是为了避免前一个命令的输出串到后一个命令的读取回调里。列表拿回来后解析函数从后往前拆字段这是为了避免 SSID 本身带冒号导致错位void WifiManager::parseScanOutput(const QString text) { m_networks.clear(); const QStringList lines text.split(\n, QString::SkipEmptyParts); for (const QString line : lines) { const QStringList fields line.split(:); if (fields.size() 4) continue; // 至少要有 SSID/SIGNAL/SECURITY/ACTIVE 四段 WifiInfo info; // ACTIVE 是最后一列值为 yes 或 no info.active (fields.last() yes); // SECURITY 是倒数第二列SIGNAL 是倒数第三列 info.security fields.at(fields.size() - 2); info.signal fields.at(fields.size() - 3).toInt(); // 剩下的字段是 SSID用冒号重新拼回去 const QStringList ssidParts fields.mid(0, fields.size() - 3); info.ssid ssidParts.join(:); if (info.ssid.isEmpty()) continue; // 隐藏热点SSID 为空不展示 m_networks.append(info); } emit scanFinished(m_networks); }信号强度这一列 nmcli 给的是 0 到 100 的百分比不是 dBm。界面上要显示信号格数直接按 20 分一档切成五格即可。解析完成后发一个scanFinished信号WifiListModel 拿到列表刷新 QListView。提示--escape no和「从后往前拆字段」是配套的。如果不开这个参数SSID 里的冒号会被转义split 之后字段数就不会错位但中文会被打成\x逃逸串开了它就必须靠拆分顺序来兜底。这两个方案二选一。3.2 连接动作密码传递与安全边界连接的核心是执行nmcli dev wifi connect SSID password 密码。有密码和没密码是两种情形开放热点不需要 password 参数。工程里的实现如下void WifiManager::connectToWifi(const QString ssid, const QString password) { m_state WifiState::Connecting; m_connectingSsid ssid; QStringList args; args dev wifi connect ssid; if (!password.isEmpty()) { args password password; } // name 参数给连接起个固定名字之后查状态、改 autoconnect 都靠它 args name ssid; m_connectProc-start(nmcli, args); } void WifiManager::onConnectFinished(int exitCode) { const QString err QString::fromLocal8Bit(m_connectProc-readAllStandardError()); if (exitCode 0) { m_lastSsid m_connectingSsid; m_state WifiState::Connected; emit connectResult(true, 连接成功); } else { m_state WifiState::Idle; emit connectResult(false, err); } }这里最关键的一点必须用QProcess::start(nmcli, args)这种传参数数组的方式把 SSID 和密码当作独立的 argv 元素传给进程。这样密码里的$、空格、反引号都不会被 shell 解释因为 QProcess 不会经过 shell。绝不能图省事拼成nmcli dev wifi connect ssid password password再丢给system()执行那样密码里只要有一个空格或$命令就废了。连接成功后 nmcli 会在 NetworkManager 里保存一份连接配置默认autoconnect是开启的所以下次开机系统会自动重连。这也是程序里name参数用 SSID 命名的原因方便用nmcli connection show去查这份配置。失败时 stderr 里的错误信息比如密码错误、热点未找到原样带回界面提示用户能直接看到问题。3.3 状态监测与自动重连连接不是一次性的程序得时刻知道当前连没连、连的是哪个。做法是起一个定时器周期性调用nmcli dev wifi list并看哪一行的 ACTIVE 字段是 yesvoid WifiManager::startStatusMonitor(int intervalMs) { if (!m_statusTimer) { m_statusTimer new QTimer(this); connect(m_statusTimer, QTimer::timeout, this, [this]() { // 复用 list 同样带 ACTIVE 列解析出当前连接 m_statusProc-start(nmcli, {-t, --escape, no, -f, SSID,SIGNAL,ACTIVE, dev, wifi, list}); }); } m_statusTimer-start(intervalMs); }拿到返回结果后找到 ACTIVE 为 yes 的那一行取出 SSID 和信号值发给界面更新状态栏。如果发现当前 ACTIVE 里没有任何网络而程序刚才是连着的说明掉线了此时触发重连void WifiManager::onStatusParsed(const QString currentSsid, int signal) { if (currentSsid.isEmpty()) { // 掉线了。如果有保存过的连接直接用 connection up 拉起 if (!m_lastSsid.isEmpty()) { m_state WifiState::Reconnecting; m_connectProc-start(nmcli, {connection, up, m_lastSsid}); } } else { emit connectionChanged(true, currentSsid, signal); } }这里重连用的是nmcli connection up 名字而不是重新走一遍connect加密码的流程。因为第一次连接成功后 NetworkManager 已经保存了带密码的配置connection up会直接沿用程序里因此不需要长期保存明文密码这是刻意的设计取舍。状态轮询的间隔我一般设 5 秒。太短会频繁唤醒网卡、增加功耗太长则掉线后界面反应迟钝5 秒在工控机上是个稳妥的折中值。4. 编译与运行从开发机交叉编译到 ARM 板写好的代码最终要跑在 ARM 板上。这一章说清楚环境怎么搭、工程怎么编、产物怎么部署以及上板后第一件事该验证什么。4.1 环境准备开发机是常见的桌面 Linux 发行版装了 Qt 5.12 以上版本和 Qt Creator。目标板是一块 ARM Cortex-A7 核心板板卡厂商会随开发包附一个交叉编译 SDK里面含编译器、sysroot 根文件系统和环境变量脚本。交叉编译的思路是在 x86 机器上调用 arm 编译器链接目标板 sysroot 里的 Qt 运行库产出 ARM 格式的二进制。开发机上需要先装交叉工具链以 Debian 系发行版为例# 安装 armhf 交叉编译器 sudo apt install gcc-arm-linux-gnueabihf g-arm-linux-gnueabihf同时要确认目标板系统里 WiFi 相关组件齐全缺了后面全白搭# 在目标板的串口终端里执行 nmcli -v # 确认 NetworkManager 客户端存在 pgrep -a wpa_supplicant # 确认 wpa_supplicant 进程在跑 ls /sys/class/net/ # 确认 wlan0 网卡节点存在pgrep -a wpa_supplicant这条命令值得说一句很多板子默认没有把 wpa_supplicant 拉起来NetworkManager 装了声称支持 wifi但实际扫描永远是空问题就在这个进程没起来。这个验证步骤我每次上板必做。4.2 qmake 工程与交叉编译工程根目录的wifi-manager.pro长这样QT core gui widgets TARGET wifi-manager TEMPLATE app CONFIG c11 SOURCES main.cpp \ core/wifimanager.cpp \ core/wifimodel.cpp \ ui/mainwindow.cpp \ ui/connectdialog.cpp HEADERS core/wifimanager.h \ core/wifimodel.h \ ui/mainwindow.h \ ui/connectdialog.h target.path /usr/bin INSTALLS targetTARGET是输出二进制名INSTALLS target配合make install可以指定部署路径后面会用到。交叉编译时先加载板卡 SDK 的环境脚本再用 SDK 自带的 qmake 而不是开发机原生的 qmake# 加载目标板 SDK 环境变量会替换 CC/CXX/qmake 指向 source /opt/board-sdk/environment-setup-armv7at2hf-neon # 用 SDK 里的 qmake 生成 Makefile qmake wifi-manager.pro # 编译-j4 并行加快速度 make -j4 # file 命令确认产物确实是 ARM 格式 file wifi-managerfile wifi-manager输出应该包含ARM字样而不是x86-64这是判断交叉编译是否生效最直观的办法。如果发现编出来是 x86 格式多半是环境脚本没 source 成功或者 Makefile 没重新生成清掉 build 目录重来一遍。编译通过只是第一步链接时如果提示找不到 libQt5Widgets.so说明 SDK 的 sysroot 里没有目标板的 Qt 运行库。通常板卡 SDK 里会带一份 Qt 的 sysroot如果厂商没给就得自己去交叉编译一遍 Qt 源码那是另一个大工程这里不展开。4.3 部署与启动验证产物部署到目标板最常见的方式是 scp 拷贝加串口终端验证# 二进制拷到板子 scp wifi-manager root192.168.1.10:/usr/bin/ # 如果板子没有 Qt 运行库把 SDK sysroot 里的 Qt 动态库一起拷过去 scp -r /opt/board-sdk/sysroot/usr/lib/libQt5*.so* root192.168.1.10:/usr/lib/ # 串口终端里执行指定 framebuffer 平台插件 export LD_LIBRARY_PATH/usr/lib ./wifi-manager -platform linuxfb-platform linuxfb是针对没有 X 环境的嵌入式板子Qt 直接写 /dev/fb0 显示。如果板子有 GPU可以换成-platform eglfs界面渲染走硬件加速刷新更快。开发机上调试时用的是 xcb 插件代码里不用为平台差异做任何改动这正是 Qt 抽象层带来的好处。上板后的验证顺序我建议固定成一套先用nmcli dev status看网卡有没有被 NetworkManager 接管再用nmcli connection show --active看有没有已经建立的连接最后才启动程序点扫描。这一步一步排查能快速定位问题是出在系统层还是程序层。部署涉及的文件清单如下部署物作用目标路径wifi-manager主程序二进制/usr/bin/libQt5Core.so.5 等Qt 运行库板子没有时必须带/usr/lib/platforms/libqlinuxfb.soframebuffer 显示插件/usr/lib/qt5/plugins/wpa_supplicant.conf兜底无线配置NetworkManager 异常时用/etc/5. 排查与避坑热点扫不到、连不上、界面卡死的五个典型问题WiFi 这块的报错信息常常语焉不详很多问题不走到现场根本想不到下面五条是血泪经验里最典型的按「现象 → 原因 → 解决」写。5.1 扫描结果为空或中文 SSID 变乱码现象程序列表一片空白或者热点名称显示成\xE6\xB5\x8B\xE8这样的转义串有时是方块乱码。原因分两种。空列表通常是扫描命令执行太快NetworkManager 的访问点缓存还没刷新rescan刚发出去就立刻list拿回来的是空表或旧表。乱码则是 locale 问题当系统 locale 是 C/POSIX 时nmcli 对非 UTF-8 字节不会原样输出而是转成\xNN形式或者程序用QString::fromLocal8Bit解码时假设了错误编码。解决扫描必须走「rescan → 延时 2 秒 → list」的串行流程就是 3.1 里写的那样解码统一用QString::fromUtf8并且在上板前 exportLC_ALLzh_CN.UTF-8。如果已经是 UTF-8 环境还出现\x转义说明 SSID 本身不是合法 UTF-8某些老旧 AP 用 GBK 甚至自定义编码这种情况要么在解析层手动 unescape要么干脆放弃显示、只留信号强度列。5.2 密码含$或空格导致连接失败现象密码里带$、反引号或空格的网络程序报连接失败但在终端里手工敲同样的命令却能连上。原因代码里把密码拼进了命令行字符串。拼出来的nmcli dev wifi connect XXX password $abc被交给system()或QProcess::execute执行时shell 会把$abc当变量展开反引号更是直接被当成命令执行密码根本不是原来那个。解决只允许用一种方式传参QProcess::start(nmcli, args)密码作为独立字符串放进 QStringList。Qt 的 start 方法不经过 shellargv 里有什么字符就原样传给进程$、空格、引号都不是问题。代码评审时我会专门盯着这一处凡是出现「字符串拼接 网络相关命令」的地方一律打回。5.3 点扫描后界面卡死几秒甚至假死现象点击扫描按钮整个窗口无响应有的机器能卡五六秒热点多的时候更久。原因扫描回调里用了waitForFinished()这个函数会阻塞当前线程直到子进程退出。nmcli 的 scan 本来就慢扫描动作叠加网络探测经常要几秒在 GUI 线程里等它等于自杀。解决所有 nmcli 调用一律走信号槽异步。QProcess 的finished、readyReadStandardOutput信号在 Qt 事件循环里分发不阻塞 UI。如果某个地方确实需要同步等待把它挪到QtConcurrent::run的工作线程里结果通过信号回到主线程绝不在 UI 线程里同步等进程。5.4 掉线后不会自动重连现象程序连接成功但现场网络抖动断了之后状态栏一直显示断开不会自己恢复。原因NetworkManager 默认的 autoconnect 是针对系统启动场景的运行期间断线后它有自己的退避策略重连周期可能长达几分钟甚至不主动重连另外如果程序里没有状态轮询掉了也没人知道。解决程序内部自己盯。用 QTimer 每 5 秒查一次 ACTIVE 列发现断线立即nmcli connection up 连接名拉起。同时上板后人工验证一次拔掉网线或关掉 AP观察 10 秒内程序是否自动恢复这是验收标准之一。5.5 重连时报 Connection already active现象掉线后自动重连触发多次某个时刻报Error: Connection already active而且界面状态被搞乱。原因NetworkManager 可能已经自己把连接拉起来了程序的重连命令后到一步系统就报已经激活。多个重连路径叠加竞态轮询定时器连续触发两次也会这样。解决重连前先检查当前是否有 ACTIVE 为 yes 的连接有就跳过重连重连命令发起后如果收到 already active 错误不当作失败处理而是重新查询一次状态。另外重连期间把状态机置为 Reconnecting防止同一时刻发出第二条重连命令这也是 2.3 状态机存在的原因之一。6. 二次开发把 WiFi 模块拆成组件再扩出热点模式到这个阶段程序已经在板子上跑通了。接下来最值得做的两件事把 core 层抽成可复用的库以及在这个框架上扩展新功能。6.1 把 core 拆成静态库现在工程是单一 app如果产品线里有多台设备、多个 Qt 程序都要用到 WiFi 管理把 WifiManager、WifiModel、WifiInfo 打成静态库更合适。工程改成 subdirs 模式# wifi-manager.pro 改成顶层聚合工程 TEMPLATE subdirs SUBDIRS wifi_core appwifi_core/wifi_core.pro里声明为静态库QT core network TEMPLATE lib CONFIG staticlib c11 TARGET wifi_core SOURCES wifimanager.cpp wifimodel.cpp HEADERS wifimanager.h wifimodel.happ 子工程只依赖wifi_coremainwindow 调用的是库的公开接口。以后就算把底层实现从 nmcli 换成 D-Bus界面层一行不用动。这个替换成本在架构阶段就预留了现在验证它只需要改 wifi_core 内部代码重新编库。6.2 扩展热点分享与 WPS基于现有 WifiManager加功能基本都是加一个公开方法的事。开热点用 nmcli 的 hotspot 子命令void WifiManager::startHotspot(const QString ssid, const QString password) { // 把 wlan0 变成 APSSID 和密码由参数传入 m_proc-start(nmcli, {dev, wifi, hotspot, ifname, wlan0, ssid, ssid, password, password}); }WPS 一键连接对现场设备很实用加一个带 PIN 码的连接入口nmcli dev wifi connect SSID --wps-pin PIN复现 3.2 的连接流程只是参数变了。这些扩展都不需要动状态机框架Scanning、Connecting、Connected 三个状态照样覆盖这些命令的执行过程。6.3 把 nmcli 原始输出纳入日志二次开发阶段我最推荐立刻做的一件事给 WifiManager 加一个日志开关把每次发给 QProcess 的参数组和返回的 stdout 原文写进文件用qInstallMessageHandler统一导向/var/log/wifi-manager.log。WiFi 问题在工控现场特别玄学有了原始输出用户报障时直接把日志拷回来不用远程复现就能定位是程序解析错了还是系统层压根没扫到。从那以后我每次拿到一块新板子第一件事不是画界面而是先在板上敲一遍nmcli dev status、pgrep -a wpa_supplicant、echo $LANG确认无线链路三件套齐了再让 Qt 程序上场。希望这篇笔记帮到你。本文还有配套的精品资源点击获取