简介:一套在Linux环境(如Ubuntu 16.04)下、使用Qt框架编写的Wi-Fi连接管理程序完整工程源码,面向需要学习Qt网络编程或掌握Linux无线网络配置的开发者,可解决通过图形界面完成热点扫描、密码认证与连接管理的问题。压缩包共12个文件,包含4个cpp源文件、3个头文件、3个ui界面文件以及pro工程文件,整体仅9KB;其中cpp实现核心逻辑,h声明类与接口,ui负责可视化布局,pro用于配置qmake构建。已有4523人浏览学习。程序基于QNetworkConfigurationManager完成Wi-Fi热点扫描,通过activate()方法触发连接,并封装独立网络管理类;界面包含Wi-Fi列表、密码输入框、连接/断开按钮,利用信号槽机制更新状态提示。项目结构清晰,适合初学者理解GUI与底层网络管理的结合,也可作为扩展自定义无线管理功能的起点。
1. 在Linux下用QT写WiFi连接:先把DBus这条主路走通
很多开发者在Linux下用QT写WiFi连接程序,第一反应是把iwlist和iw的输出拿来做解析,结果做出来的工具只能在root下跑,还经常和桌面环境的网络管理打架。实际Linux下QT WiFi程序的正确入口是NetworkManager的DBus接口,只要把扫描、连接、状态变化这三件事接进来,一个带界面、带状态提示的WiFi工具就能稳定复刻。这份资源适用于嵌入式设备调试工具、轻量桌面控制面板这类场景,也适合刚接触Linux网络管理的QT开发者。它解决的核心问题很明确:不写一堆命令行,在应用层把WiFi扫描、密码输入、连接状态跟踪全部图形化。
2. 先跑通NetworkManager底层:DBus接口的初始化与扫描
2.1 为什么选NetworkManager而不是iwlist
Linux下能拿到WiFi信息的方法很多,最常见的是iwlist和iw,但这两种拿到的数据是直接从内核无线子系统读出来的,绕过了NetworkManager(后面统一简称NM)。如果系统里NM已经接管了WiFi网卡,你在程序里再用root权限的子进程去调iwlist扫描,很容易造成网卡状态冲突:界面那边刚连上网,你这边的扫描结果还是旧的。
我一般会把实现方案按优先级排成三档:
| 方案 | 执行方式 | 权限需求 | 与NM冲突 | 适用场景 |
|---|---|---|---|---|
| iwlist/iw | 命令行子进程扫描 | root | 常见 | 无NM的极简板子 |
| nmcli | 子进程文本解析 | 普通用户 | 无 | 快速原型验证 |
| QDBus直连 | 进程内DBus调用 | 普通用户+polkit | 无 | 生产推荐 |
为什么推荐DBus直连?因为NM对所有连接统一管理,你在QT里做图形化时,能拿到“网卡状态变化”“扫描完成”“信号强度变化”这类事件,不需要自己轮询。省下来的不仅是代码量,还有大量排查网络状态的时间。nmcli虽然简单,但每次调用都要拉起一个子进程,频繁扫描时延迟明显,而且文本解析对隐藏SSID、特殊字符处理都容易翻车。
2.2 初始化DBus系统总线
第一步是工程配置。在QT的.pro文件里加:
QT += dbus如果工程用的是CMake,对应写法是:
find_package(Qt6 REQUIRED COMPONENTS DBus) target_link_libraries(your_app PRIVATE Qt6::DBus)工程配置好之后,连接系统总线并检查NetworkManager服务是否存在:
#include <QGuiApplication> #include <QDBusConnection> #include <QDBusInterface> #include <QDBusReply> int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); QDBusConnection bus = QDBusConnection::systemBus(); if (!bus.isConnected()) { qCritical() << "system bus unavailable"; return -1; } QDBusInterface nm("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager", "org.freedesktop.NetworkManager", bus); if (!nm.isValid()) { qCritical() << "NetworkManager service not found"; return -1; } return app.exec(); }这段代码看着简单,但有两个隐藏知识点。第一,QDBusInterface的四个参数分别对应服务名、对象路径、接口名、总线类型,服务名和对象路径都是org.freedesktop.NetworkManager开头,接口名如果写错,后面所有调用都会返回UnknownMethod。第二,isValid()只能证明DBus对象存在,不能证明NM已经完成WiFi设备初始化;系统刚启动时,可能出现服务注册了但无线网卡还没就绪的情况,后面要看设备状态来兜底。
我习惯在初始化后顺手读出NM版本,后续遇到接口行为差异时可以快速定位:
QDBusReply<QString> ver = nm.call("Get", "org.freedesktop.NetworkManager", "Version"); if (ver.isValid()) { qDebug() << "NM version" << ver.value(); }这里用属性读取方式拿到版本号,比直接找version()方法稳定,因为不同NM版本对属性列表的暴露程度不同。读版本号不是业务必须,但值得保留,很多“扫描没反应”的问题在不同NM版本上处理路径完全不同。
2.3 扫描请求的正确参数
扫描在NM里对应的接口是org.freedesktop.NetworkManager.Device.Wireless。先拿到无线网卡的DBus对象路径,再调用RequestScan。下面的代码演示了怎么枚举无线网卡并触发扫描:
QDBusInterface nm("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager", "org.freedesktop.NetworkManager", QDBusConnection::systemBus()); QDBusReply<QDBusObjectPath> reply = nm.call("GetDeviceByType", 2); // 2 = NM_DEVICE_TYPE_WIFI if (!reply.isValid()) { qWarning() << "no wifi device: " << reply.error().message(); return; } QString wifiDevicePath = reply.value().path(); QDBusInterface wlan("org.freedesktop.NetworkManager", wifiDevicePath, "org.freedesktop.NetworkManager.Device.Wireless", QDBusConnection::systemBus()); QVariantMap scanOptions; scanOptions.insert("ssid", QByteArray()); // 空值表示全频段扫描 QDBusMessage msg = wlan.call("RequestScan", scanOptions);参数说明:GetDeviceByType的2是NM_DEVICE_TYPE_WIFI的枚举值,QT侧没有现成的枚举头文件,直接写数字并注释含义。scanOptions里只放一个空字节数组的ssid,表示不限定网络,让网卡做全频段扫描。如果已经知道目标SSID,可以填具体字节数组,扫描会快一些,但常规场景还是传空。
需要注意的是,RequestScan调用成功只代表请求已经发给NM,不代表扫描完成。要等ScanDone信号。很多初写者在这里踩坑:调用完RequestScan立刻去GetAccessPoints,拿到的是上一次扫描的缓存数据。正确做法是先挂信号槽:
QDBusConnection::systemBus().connect( "org.freedesktop.NetworkManager", wifiDevicePath, "org.freedesktop.NetworkManager.Device.Wireless", "ScanDone", this, SLOT(onScanDone()));ScanDone信号没有参数,只表示这次请求的扫描流程已经结束。真正的热点列表要在这个槽里再读。
提示:
RequestScan里的ssid键不要随手删掉,部分NM版本要求必须携带这个键,空字节数组是最稳妥的参数。
2.4 先判断网卡状态再扫描
扫描并不是在任何网卡状态下都能做。网卡处于unmanaged或unavailable时,调RequestScan会直接报错。建议在点击“扫描”按钮前读一次设备状态:
QDBusInterface device("org.freedesktop.NetworkManager", wifiDevicePath, "org.freedesktop.NetworkManager.Device", QDBusConnection::systemBus()); QDBusReply<quint32> stateReply = device.call("Get", "org.freedesktop.NetworkManager.Device", "State"); quint32 state = stateReply.value(); if (state < 30 || state == 20) { qWarning() << "device not ready, state =" << state; return; }NM的设备状态枚举里,20是unmanaged,30是disconnected,40是connecting,100是activated。一般扫描至少要保证不是unmanaged和unavailable状态。实际调试时,如果设备停在unavailable,很可能被rfkill禁用了,不查底层日志是看不出来的。
从UI事件触发扫描,还要注意一个经验问题:NM对同一设备的扫描请求有最短间隔限制,连续点扫描按钮,第二次很可能直接失败。我一般做一个标志位防抖:
if (m_scanning) { qDebug() << "scan already in progress"; return; } m_scanning = true; // 这里调 RequestScan // 在 onScanDone 里再恢复 m_scanning = false这段逻辑放UI层,不放业务层,因为按钮置灰需要同步反馈。到这里,扫描链路的前置条件就算理清了。
3. 扫描结果在QT里的落地:AP对象、信号强度与列表刷新
3.1 读取AccessPoint列表并解析成自己的结构体
扫描完成后,用GetAccessPoints拿热点对象。这个方法的返回值不是简单字符串数组,而是ao类型的对象路径数组。我先把热点信息收敛成自己的结构体:
struct ApInfo { QString bssid; QString ssid; int strength; // 0~100 int frequency; // MHz };获取列表的代码:
#include <QtDBus/QDBusObjectPath> void WifiController::onScanDone() { if (!m_wirelessIface.isValid()) return; QDBusMessage msg = m_wirelessIface.call("GetAccessPoints"); QDBusReply<QList<QDBusObjectPath> > reply = msg; if (!reply.isValid()) { qWarning() << "GetAccessPoints failed" << reply.error().message(); return; } QList<ApInfo> points; const QList<QDBusObjectPath> paths = reply.value(); for (const QDBusObjectPath &path : paths) { ApInfo info; info.bssid = readApProperty(path.path(), "HwAddress").toString(); QByteArray rawSsid = readApProperty(path.path(), "Ssid").toByteArray(); info.ssid = decodeSsid(rawSsid); info.strength = readApProperty(path.path(), "Strength").toUInt(); info.frequency = readApProperty(path.path(), "Frequency").toUInt(); points.append(info); } emit apListReady(points); }readApProperty是对DBus属性读取的一次薄封装:
QVariant readApProperty(const QString &objPath, const QString &prop) { QDBusInterface iface("org.freedesktop.NetworkManager", objPath, "org.freedesktop.DBus.Properties", QDBusConnection::systemBus()); QDBusReply<QVariant> r = iface.call("Get", "org.freedesktop.NetworkManager.AccessPoint", prop); return r.isValid() ? r.value() : QVariant(); }这里要注意:GetAccessPoints返回的列表是瞬时状态,NM内部有缓存。如果你想要“所有频段历史结果”,用GetAllAccessPoints,它会包含当前缓存里所有BSSID;而GetAccessPoints只返回当前可见的那些。这两个方法名字很像,实际用途差异很大,漫游场景下GetAllAccessPoints的结果更能反映周围环境,但列表里会混入已经消失的热点,需要再做一次新鲜度过滤。
Ssid属性在DBus类型里是ay,也就是字节数组。不要用toString()强转,因为SSID不是严格的UTF-8文本,直接转会出现乱码,这一节后面单独讲。
3.2 decodeSsid:中文SSID与二进制SSID的处理
解码SSID我常用的做法是优先按UTF-8,非法字节再退回Latin-1:
QString decodeSsid(const QByteArray &raw) { QByteArray cleaned = raw; while (cleaned.endsWith('\0')) { cleaned.chop(1); } QString result = QString::fromUtf8(cleaned); if (!result.contains(QChar::ReplacementCharacter)) { return result; } return QString::fromLatin1(cleaned); }为什么还要处理结尾的\0?很多AP固件会在SSID后面补零对齐字节,不截掉的话,QString::fromUtf8会带上不可见字符,列表对比时明明同一个热点,字符串却不相等。中文环境里,如果AP用的是GBK编码,UTF-8解码会产生替换字符,这时退回Latin-1能显示出一个可辨识的近似结果。这不算完美,但比整行乱码强。
Qt6里QString::fromUtf8的行为和Qt5基本一致,不需要额外引入QTextCodec。如果你在维护的是老工程,注意QTextCodec::codecForName("UTF-8")在Qt6里已经移到了兼容模块,能用标准库字符串转换就尽量不引入额外依赖。
3.3 多频段去重与列表刷新机制
扫描结果里同一个SSID经常出现两条甚至三条,分别对应2.4GHz、5GHz等不同频段。要不要去重取决于你的业务场景:
- 只做“连接”场景,保留信号最强的一条就够了;
- 做网络诊断工具,要把每一条都展示出来,让用户看到频段和信道。
我常用按SSID保留最强信号的策略:
QList<ApInfo> deduplicateBySsid(const QList<ApInfo> &list) { QHash<QString, int> bestIndex; for (int i = 0; i < list.size(); ++i) { const QString key = list.at(i).ssid; if (!bestIndex.contains(key) || list.at(i).strength > list.at(bestIndex.value(key)).strength) { bestIndex.insert(key, i); } } QList<ApInfo> result; for (int idx : bestIndex.values()) { result.append(list.at(idx)); } return result; }去重后的列表再交给UI层。界面刷新这里有一个很重要的准则:不要在DBus回调里直接操作UI控件。DBus消息分发线程和UI线程不是同一个,直接往QListWidget里加行会偶发崩溃。用信号槽把apListReady信号连接到UI层槽函数时,连接方式用Qt::QueuedConnection,也不会出现跨线程操作UI的问题。
刷新方式上,热点数量几十条时,clear()再重建还能接受;热点上百条时,整表重建会让界面有明显的闪烁感。更稳的做法是只更新变化的行:
void WifiPanel::refreshList(const QList<ApInfo> &newList, const QList<ApInfo> &oldList) { QHash<QString, int> oldRows; for (int i = 0; i < oldList.size(); ++i) { oldRows.insert(oldList.at(i).bssid, i); } for (const ApInfo &info : newList) { if (oldRows.contains(info.bssid)) { int row = oldRows.value(info.bssid); model->item(row)->setData(info.strength, Qt::UserRole + 1); } else { addRow(info); } } }这里用BSSID做唯一键,比用SSID可靠。BSSID对应一块具体的无线网卡射频,不会因为同SSID多频段而冲突。
注意:如果你看过往版本会写
qDBusRegisterMetaType<QList<QDBusObjectPath> >(),那是因为模板参数类型没注册。用QDBusReply<QList<QDBusObjectPath> >时,建议在main函数里加一行qRegisterMetaType<QList<QDBusObjectPath> >("QList<QDBusObjectPath>"),避免信号队列传递时类型解析失败。
4. 连接流程的正确打开方式:从Connection到ActivateConnection
4.1 连接WiFi并不是“塞个密码”那么简单
很多第一次接触NM的人会以为,拿到了AP的BSSID和密码,调一个Connect方法就能连上。实际上NM没有这种高层方法,它的设计是把“连接配置”和“连接激活”分开:
AddConnection:把SSID、认证方式、密码、是否自动连接等信息保存成一条Connection;ActivateConnection:让NM用这条Connection去驱动网卡连接目标AP。
这个分离的好处是复用:同一条连接配置可以被不同网卡引用。坏处也很明显:不会构造Connection的嵌套字典,就连不上网。下面是一份最小化的连接配置:
QVariantMap connection; QVariantMap wireless; QVariantMap security; QVariantMap ipv4; QVariantMap ipv6; wireless["ssid"] = ssidBytes; wireless["mode"] = "infrastructure"; security["key-mgmt"] = "wpa-psk"; security["psk"] = password; connection["type"] = "802-11-wireless"; connection["id"] = ssid; connection["interface-name"] = "wlan0"; connection["autoconnect"] = true; QVariantMap settingsMap; settingsMap["connection"] = connection; settingsMap["802-11-wireless"] = wireless; settingsMap["802-11-wireless-security"] = security; settingsMap["ipv4"] = ipv4; settingsMap["ipv6"] = ipv6;这里有个参数细节:psk直接放进AddConnection的属性表,等于把密码明文提交给NM。开发调试这样最直观,但如果做的是团队内部工具,不希望把密钥落盘,要改用后面的密钥回调方案。
在DBus侧,这个嵌套字典对应的是a{sa{sv}}类型。QT的QVariantMap刚好能表达。最容易踩的坑是:把802-11-wireless和802-11-wireless-security写成了顶层键,结果NM把整条Connection判为类型无效。构造完成后做一次JSON序列化,肉眼确认嵌套层级,能省下不少调试时间。
4.2 AddConnection 代码与参数表
调用AddConnection的返回值是新建Connection的对象路径:
QDBusInterface settings("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager/Settings", "org.freedesktop.NetworkManager.Settings", QDBusConnection::systemBus()); QDBusReply<QDBusObjectPath> addReply = settings.call("AddConnection", settingsMap); if (!addReply.isValid()) { qWarning() << "AddConnection error:" << addReply.error().message(); return; } QString connPath = addReply.value().path();如果同一个SSID的连接已经存在,再AddConnection不会自动覆盖,而是会生成两条配置,激活时可能互相干扰。所以添加前要先遍历已保存的连接,查到同SSID就删掉旧的。删除连接用Delete:
QDBusInterface conn( "org.freedesktop.NetworkManager", connPath, "org.freedesktop.NetworkManager.Settings.Connection", QDBusConnection::systemBus()); conn.call("Delete");常用连接参数说明:
| 配置键 | 示例值 | 作用 |
|---|---|---|
| connection.type | 802-11-wireless | 指定无线连接 |
| connection.id | 显示名称 | 连接管理界面的标识 |
| connection.autoconnect | true | 开机或掉线后自动重连 |
| 802-11-wireless.ssid | 字节数组 | 目标网络名 |
| 802-11-wireless.mode | infrastructure | 基础架构模式 |
| 802-11-wireless-security.key-mgmt | wpa-psk | 加密管理方式 |
| 802-11-wireless-security.psk | 密码字符串 | WPA个人版密钥 |
| ipv4.method | auto | 自动获取IP |
interface-name这个键看情况写。写死wlan0在固定板卡上没问题,但换一台网卡名变成wlp2s0的设备后,这条Connection就废了。产品要求灵活性,就不要写死,让NM按SSID自动匹配。
4.3 通过密钥回调动态提供密码
如果不想让密码落盘,NM提供了一套“运行时提供密钥”的机制。QT里要做的是注册一个DBus对象,实现org.freedesktop.NetworkManager.SecretAgent接口。注册动作看下面这段:
bool registerSecretAgent() { QDBusConnection bus = QDBusConnection::systemBus(); QDBusObjectPath path("/com/example/SecretAgent"); bool ok = bus.registerObject( path.path(), this, QDBusConnection::ExportAdaptors); if (!ok) { return false; } QDBusInterface nm( "org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager", "org.freedesktop.NetworkManager", bus); QDBusReply<void> reply = nm.call("RegisterAgent", QVariant::fromValue(path), "all"); return reply.isValid(); }RegisterAgent的第一个参数是agent对象路径,第二个参数是注册类型,用"all"表示所有类型的密钥都走这个回调。NM在激活连接时发现缺密钥,会调用你注册对象的GetSecrets方法:
QVariantMap SecretAgentAdaptor::GetSecrets( const QVariantMap &connection, const QDBusObjectPath &connectionPath, const QDBusObjectPath &settingName, const QStringList &hints, uint flags) { QVariantMap result; QVariantMap secrets; secrets["psk"] = m_pendingPsk; // 内存里缓存的密码 result["802-11-wireless-security"] = secrets; return result; }这段实现里最需要注意的点是:GetSecrets是同步回调,NM在DBus消息上等你的返回值。如果你在这个回调里弹模态框、做耗时操作,DBus调用直接超时。正确的设计是:用户点击连接时先把密码存到内存成员变量,GetSecrets只做一次内存读取。
我自己的习惯是:开发调试阶段先走直接放psk的路线,能最快验证扫描和激活链路;产品化阶段再替换成密钥回调。两种方案切换成本不高,但逻辑上要分清“测试用”和“交付用”。
4.4 激活连接与状态跟踪
有了Connection对象路径后,把网卡设备路径一起交给ActivateConnection:
QDBusReply<QDBusObjectPath> actReply = nm.call("ActivateConnection", connPath, wifiDevicePath, "ignored_ap_path");第三个参数是AP对象路径。传空字符串时NM自己按SSID匹配热点;传具体AP路径时,NM只尝试连这一个BSSID。从扫描列表点选某个网络之后,建议把列表里选中行的ACE路径传进去,避免重叠覆盖区域自动选到信号弱的那个。
激活之后的状态变化是异步的,NM的ActiveConnection对象有一个State属性:
| State | 数值 | 含义 |
|---|---|---|
| Unknown | 0 | 未知 |
| Activating | 1 | 激活中 |
| Activated | 2 | 已连接 |
| Deactivating | 3 | 正在断开 |
| Deactivated | 4 | 已断开 |
监听状态变化最稳的办法,是监听org.freedesktop.NetworkManager.Connection.Active接口上的属性变化:
QDBusConnection::systemBus().connect( "org.freedesktop.NetworkManager", activeConnPath, "org.freedesktop.DBus.Properties", "PropertiesChanged", this, SLOT(onActivePropsChanged(QString,QVariantMap,QStringList)));槽函数里必须过滤接口名,因为PropertiesChanged会报告路由、IP配置等多类变化,不过滤会看到槽被频繁调用:
void WifiPanel::onActivePropsChanged( const QString &iface, const QVariantMap &changed, const QStringList &invalidated) { if (iface != "org.freedesktop.NetworkManager.Connection.Active") return; if (changed.contains("State")) { uint state = changed.value("State").toUInt(); if (state == 2) { m_stateLabel->setText("已连接"); } else if (state == 1) { m_stateLabel->setText("连接中..."); } else if (state == 4) { m_stateLabel->setText("已断开"); } } }连接失败的判定不能只靠状态,还要加超时。我一般启动一个20秒的QTimer,发出ActivateConnection后开始计时,进入Activated或Deactivated状态时停掉。超时没进入Activated就显示“检查密码/信号提示”,比让用户干等更友好。
5. 避坑:5个实际调试中绕不开的坑
5.1 RequestScan 返回成功却没有 ScanDone
现象:在某台设备上调用RequestScan后一直没有ScanDone信号,热点列表始终是旧的,但同一套代码在另一块板子上正常。
原因:不同NM版本对ScanDone信号的发出时机不同。有些版本在后台wpa_supplicant未就绪时,会把扫描请求静默吞掉;也有网卡驱动固件导致扫描中断的情况。只靠ScanDone一条路走,遇到这种设备就卡死了。
解决:我不再只依赖ScanDone,而是增加一个5秒超时兜底,超时后主动调一次GetAccessPoints,对比列表是否更新。如果没更新就在界面显示“扫描较慢,请重试”,不阻塞后续操作。超时逻辑要与扫描标志位配合,避免兜底查询还没结束用户又触发一次扫描。
5.2 连续点击扫描,第二次直接报错
现象:快速连点“扫描”按钮,第一次正常,第二次在RequestScan返回Device is not running或Scan request already in progress。
原因:NM对同一设备的扫描请求有互斥限制,一次扫描没结束就发第二次会被拒绝。UI层没做防抖就会出现这个现象。
解决:用一个bool m_scanInProgress标志位,发出请求时置true,ScanDone或超时兜底里置false。按钮的setEnabled直接绑定这个标志。最容易漏的点是忘了在超时兜底里复位标志,结果按钮永远置灰。测试用例里必须包含“扫描中强制再次点击”这一项,不然这个坑会在交付前突然冒出来。
5.3 密码正确却连接后几秒断开
现象:ActivateConnection返回正常,状态一度到了Activated,界面显示“已连接”,可三到五秒后状态又退回Deactivated。
原因:这类“先上去再掉下来”的现象,大多数不是信号问题,而是连接配置里少了key-mgmt或填错了。比如路由器是WPA2/WPA3,正确值应该是wpa-psk;填成wpa-none或空值时,NM默认按开放网络握手,路由器直接拒绝关联。
解决:把AddConnection实际提交的嵌套字典打出来,逐键对照路由器设置。普通家用路由基本用wpa-psk,企业网络则要补eap、identity等字段。扫描结果里的WpaFlags和RsnFlags做判断:有RsnFlags的基本都能用wpa-psk,只有个别WPA3-only网络需要额外配置。在实际设备上验证时,注意日志里wpa_supplicant的认证失败信息,它会直接告诉你握手停在哪一步。
5.4 中文SSID乱码,去重时出现两个不同key
现象:扫描到一个叫“办公室5G”的热点,界面上显示成乱码;部分温度设备显示正常,部分不正常。
原因:AP广播的SSID原始字节不一定是UTF-8编码。很多AP固件直接发GBK字节流,QT按UTF-8解码自然乱码。去重时因为解码结果不同,同一个热点被当成两条数据处理。
解决:按顺序尝试UTF-8解码,出现替换字符就退回Latin-1。中文环境里这种方法能覆盖绝大多数情况。另外,SSID解码结果不建议直接当唯一键,去重和内存比对优先用BSSID,BSSID是唯一的硬件地址。
5.5 换设备后“找不到旧连接”
现象:保存的Connection还在,激活时却报Connection is not available on this device。
原因:连接配置里interface-name写死了wlan0,换到另一台网卡名为wlp2s0的设备上就失效。部分情况下autoconnect被设成false,NM不会把它当作可激活连接。
解决:连接配置里不写死网卡名,让NM按SSID自动匹配。如果测试环境必须固定网卡,做一个启动时的映射表,从当前设备路径动态替换。这个坑通常在换机器时才暴露,开发机上都是自己的板卡,特别容易忽略。我在代码里给interface-name留一个空字符串的默认值,只在明确指定时才填充。
6. 进阶:信号强度概览与自动重连的收尾习惯
6.1 信号强度从百分比到信号格数的换算
NM返回的Strength是0到100的整数百分比。直接把数字显示在界面上不够直观,我习惯把它映射成5格信号,同时保留百分比用于调试。换算规则放在一个独立函数里:
int barsFromStrength(int strength) { if (strength >= 80) return 5; if (strength >= 60) return 4; if (strength >= 40) return 3; if (strength >= 20) return 2; return 1; }阈值用20 dB的线性划分,不是行业标准,但作为可视化的稳定粒度够用。如果要做成信号图标,可以把这个格数直接对应到不同的图标资源上。注意NM的Strength已经做过平滑处理,不需要在QT侧再加滑动平均,加了反而会让信号变化看起来迟钝。
6.2 断开自动重连的守卫条件
自动重连是一把双刃剑。连接配置里autoconnect=true时,NM自己已经做了重连,QT不需要重复实现。只有用户手动断开后,NM才不会自动连回去。如果你在做一个“恢复上次连接”的按钮,监听设备状态变化比轮询更合适:
void WifiController::onDeviceStateChanged(uint state) { if (state == 30 && m_enableAutoReconnect) { activateSavedConnection(m_lastConnPath); } }这里必须加防抖。设备在信号弱区反复断开时,状态会在disconnected和connected之间来回跳,不限制频率的话,重连请求会反复压进DBus消息队列。我加了一个30秒冷却时间,重连过一次后,30秒内不再触发下一次。这个习惯是从一次现场调试里学到的:当时没加冷却,设备每五秒断一次,QT侧每五秒重连一次,NM日志刷了一整屏。
从那以后,我每写一个WiFi工具,都会强制自己把状态驱动、去重键、超时兜底这三件事在代码里过一遍。特别是状态驱动,DBus信号能监听的就不要轮询,轮询在WiFi场景里只会带来更多偶发问题。这套经验都是从真实的Linux QT WiFi调试场景里磨出来的,希望帮到你。
本文还有配套的精品资源,点击获取