1
0
forked from BRT/arc

Работа с WIFI

This commit is contained in:
slava 2026-06-17 19:30:24 +07:00
parent da72c4eda6
commit 21e7c210c1
16 changed files with 515 additions and 26 deletions

View File

@ -11,6 +11,8 @@
#include <QMutex>
#include <QDateTime>
#include <QHostAddress>
#include <QThreadPool>
#include <QtConcurrent>
#include "dump/TaskDumpRecorder.h"
@ -206,6 +208,104 @@ QList<QPair<QString, QString> > AdbUtils::getListDevices() {
return devices;
}
bool AdbUtils::adbPair(const QString &ip, int port, const QString &code) {
const QString endpoint = ip + ":" + QString::number(port);
QString out, err;
startProcess(adbPath(), {"pair", endpoint, code}, &out, &err);
const QString combined = out + err;
const bool ok = combined.contains("Successfully paired", Qt::CaseInsensitive);
if (ok) {
qInfo().noquote() << "[AdbUtils] adb pair" << endpoint << "→ ok";
} else {
qWarning().noquote() << "[AdbUtils] adb pair" << endpoint << "failed:" << combined.trimmed();
}
return ok;
}
QString AdbUtils::adbConnect(const QString &ip, int port) {
const QString endpoint = ip + ":" + QString::number(port);
QString out, err;
startProcess(adbPath(), {"connect", endpoint}, &out, &err);
const QString combined = out + err;
// adb connect возвращает exit 0 и при неудаче ("failed to connect"), поэтому
// ориентируемся на текст вывода.
if (combined.contains("connected to", Qt::CaseInsensitive)
|| combined.contains("already connected", Qt::CaseInsensitive)) {
qInfo().noquote() << "[AdbUtils] adb connect" << endpoint << "→ ok";
return endpoint;
}
qWarning().noquote() << "[AdbUtils] adb connect" << endpoint << "failed:" << combined.trimmed();
return {};
}
bool AdbUtils::adbDisconnect(const QString &endpoint) {
QString out, err;
startProcess(adbPath(), {"disconnect", endpoint}, &out, &err);
return true; // best-effort
}
QString AdbUtils::mdnsFindConnect(const QString &ip) {
QString out;
startProcess(adbPath(), {"mdns", "services"}, &out);
// Строки вида: adb-XXXX-YY._adb-tls-connect._tcp. 192.168.1.50:41555
static const QRegularExpression reEndpoint(R"((\d+\.\d+\.\d+\.\d+):(\d+))");
const QStringList lines = out.split('\n');
for (const QString &line : lines) {
if (!line.contains("_adb-tls-connect")) {
continue;
}
if (const auto m = reEndpoint.match(line); m.hasMatch() && m.captured(1) == ip) {
return m.captured(1) + ":" + m.captured(2);
}
}
return {};
}
QString AdbUtils::scanFindConnect(const QString &ip, int portStart, int portEnd) {
if (portStart < 1) portStart = 1;
if (portEnd > 65535) portEnd = 65535;
if (portEnd < portStart) return {};
// 1. Параллельно ищем открытые TCP-порты на IP телефона (сырой сокет, без adb).
QList<int> ports;
ports.reserve(portEnd - portStart + 1);
for (int p = portStart; p <= portEnd; ++p) ports.append(p);
QThreadPool pool;
pool.setMaxThreadCount(256);
const QList<int> openPorts = QtConcurrent::blockingFiltered(
&pool, ports, [&ip](int p) {
QTcpSocket s;
s.connectToHost(ip, static_cast<quint16>(p));
return s.waitForConnected(250);
});
if (openPorts.isEmpty()) {
qWarning().noquote() << "[AdbUtils] scanFindConnect: no open ports on" << ip;
return {};
}
qInfo().noquote() << "[AdbUtils] scanFindConnect: open ports on" << ip << ":" << openPorts;
// 2. На каждый открытый порт пробуем adb connect (порт спаривания откажет, connect-
// порт подключится — pairing уже есть, код не нужен).
for (const int p : openPorts) {
if (const QString ep = adbConnect(ip, p); !ep.isEmpty()) {
return ep;
}
}
return {};
}
QString AdbUtils::getAndroidId(const QString &deviceId) {
QString out;
if (!startProcess(adbPath(),
{"-s", deviceId, "shell", "settings", "get", "secure", "android_id"}, &out)) {
return {};
}
const QString id = out.trimmed();
return (id == "null" || id.isEmpty()) ? QString() : id;
}
QString AdbUtils::tryToGetRunningAppPid(const QString &deviceId, const QString &packageName) {
QProcess process;
process.setWorkingDirectory(adbDir());

View File

@ -13,6 +13,28 @@ public:
static QList<QPair<QString, QString> > getListDevices();
// --- Wi-Fi подключение (wireless debugging) ---
// Спаривание по коду: `adb pair IP:port code`. true при "Successfully paired".
static bool adbPair(const QString &ip, int port, const QString &code);
// `adb connect IP:port`. Возвращает рабочий "IP:port" при успехе, иначе пусто
// (adb может вернуть exit 0 и при неудаче — судим по тексту вывода).
static QString adbConnect(const QString &ip, int port);
// `adb disconnect <endpoint>` (best-effort).
static bool adbDisconnect(const QString &endpoint);
// Поиск connect-эндпоинта устройства по IP через `adb mdns services`
// (_adb-tls-connect._tcp). Возвращает "IP:port" или пусто.
static QString mdnsFindConnect(const QString &ip);
// android_id устройства (`settings get secure android_id`) — стабильная личность.
// Пусто при ошибке/таймауте.
static QString getAndroidId(const QString &deviceId);
// Fallback-поиск connect-порта, когда он сменился (тоггл/ребут wireless debugging),
// а mDNS в сети не работает: параллельно сканирует IP телефона на открытые порты и
// пробует `adb connect` (pairing сохранён → код не нужен). Возвращает рабочий
// "IP:port" или пусто. Тяжёлая операция — звать только как fallback.
static QString scanFindConnect(const QString &ip, int portStart = 30000, int portEnd = 50000);
static QString tryToGetRunningAppPid(const QString &deviceId, const QString &packageName);
static bool tryToStartApplication(const QString &deviceId, const QString &packageName);

View File

@ -260,16 +260,17 @@ void DatabaseManager::initSchema() {
))");
// Wi-Fi реквизиты подключения per-device (связь по android_id = devices.id).
// Задел под авто-реконнект по Wi-Fi: ip+port для adb connect, code — одноразовый
// pairing-код (нужен только для первого adb pair; реконнект идёт по ip:port).
// Авто-реконнект по Wi-Fi: ip+port для adb connect. Pairing-код НЕ храним — он
// одноразовый и для реконнекта не нужен (pairing сохраняется на устройстве).
q.exec(R"(CREATE TABLE IF NOT EXISTS wifi_connections (
android_id TEXT PRIMARY KEY,
ip TEXT,
port INTEGER,
code TEXT,
updated_at DATETIME,
FOREIGN KEY (android_id) REFERENCES devices (id) ON DELETE CASCADE
))");
// Миграция: убираем колонку code из старых БД (ошибка = колонки уже нет — норм).
q.exec("ALTER TABLE wifi_connections DROP COLUMN code");
db.commit();
}

View File

@ -13,28 +13,24 @@ WifiConnection parseRow(const QSqlQuery &query) {
wc.androidId = query.value("android_id").toString();
wc.ip = query.value("ip").toString();
wc.port = query.value("port").toInt();
wc.code = query.value("code").toString();
wc.updatedAt = query.value("updated_at").toString();
return wc;
}
} // namespace
bool WifiConnectionDAO::upsert(const QString &androidId, const QString &ip, int port,
const QString &code) {
bool WifiConnectionDAO::upsert(const QString &androidId, const QString &ip, int port) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
INSERT INTO wifi_connections (android_id, ip, port, code, updated_at)
VALUES (:android_id, :ip, :port, :code, :updated_at)
INSERT INTO wifi_connections (android_id, ip, port, updated_at)
VALUES (:android_id, :ip, :port, :updated_at)
ON CONFLICT(android_id) DO UPDATE SET
ip = excluded.ip,
port = excluded.port,
code = excluded.code,
updated_at = excluded.updated_at
)");
query.bindValue(":android_id", androidId);
query.bindValue(":ip", ip);
query.bindValue(":port", port);
query.bindValue(":code", code);
query.bindValue(":updated_at", QDateTime::currentDateTimeUtc().toString(Qt::ISODate));
if (!query.exec()) {
qWarning() << "WifiConnectionDAO::upsert error:" << query.lastError().text();
@ -43,6 +39,22 @@ bool WifiConnectionDAO::upsert(const QString &androidId, const QString &ip, int
return true;
}
bool WifiConnectionDAO::updateIp(const QString &androidId, const QString &ip) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
UPDATE wifi_connections SET ip = :ip, updated_at = :updated_at
WHERE android_id = :android_id
)");
query.bindValue(":ip", ip);
query.bindValue(":updated_at", QDateTime::currentDateTimeUtc().toString(Qt::ISODate));
query.bindValue(":android_id", androidId);
if (!query.exec()) {
qWarning() << "WifiConnectionDAO::updateIp error:" << query.lastError().text();
return false;
}
return true;
}
WifiConnection WifiConnectionDAO::getByAndroidId(const QString &androidId) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare("SELECT * FROM wifi_connections WHERE android_id = :android_id LIMIT 1");

View File

@ -8,8 +8,7 @@
struct WifiConnection {
QString androidId; // = devices.id
QString ip; // последний известный IP телефона в Wi-Fi
int port = 0; // порт для adb connect (5555 для legacy tcpip)
QString code; // одноразовый pairing-код (пусто для legacy tcpip)
int port = 0; // порт для adb connect
QString updatedAt; // ISO-время последнего обновления
};
@ -18,9 +17,13 @@ struct WifiConnection {
// поиска. В текущей итерации — только хранилище (воркер реконнекта отложен).
class WifiConnectionDAO {
public:
// Вставка/обновление по android_id (PRIMARY KEY).
static bool upsert(const QString &androidId, const QString &ip, int port,
const QString &code = {});
// Вставка/обновление по android_id (PRIMARY KEY). Pairing-код НЕ храним: он
// одноразовый и для реконнекта не нужен (pairing сохраняется на устройстве).
static bool upsert(const QString &androidId, const QString &ip, int port);
// Обновить ТОЛЬКО ip (и updated_at) существующей записи, не трогая port/code.
// Если записи нет — ничего не делает (порт/код задаёт онбординг, не фоновый IP-апдейт).
static bool updateIp(const QString &androidId, const QString &ip);
// Запись по android_id (пустой androidId в результате → не найдено).
[[nodiscard]] static WifiConnection getByAndroidId(const QString &androidId);

BIN
images/usb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
images/wifi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -56,6 +56,7 @@ static LONG WINAPI crashHandler(EXCEPTION_POINTERS *ep) {
#include "dao/GeneralLogDAO.h"
#include "dao/SettingsDAO.h"
#include "EventHandler.h"
#include "wifi/WifiAdbConnector.h"
#include "net/NetworkService.h"
#include "update/ApplyUpdate.h"
#include "black/PayByCardScript.h"
@ -126,6 +127,20 @@ void startEventHandler(const QCoreApplication &app) {
thread->start();
}
void startWifiAdbConnector(const QCoreApplication &app) {
auto *thread = new QThread;
auto *connector = new WifiAdbConnector;
connector->moveToThread(thread);
QObject::connect(thread, &QThread::started, connector, &WifiAdbConnector::start);
QObject::connect(connector, &WifiAdbConnector::finished, thread, &QThread::quit);
QObject::connect(connector, &WifiAdbConnector::finished, connector, &QObject::deleteLater);
QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
QObject::connect(&app, &QCoreApplication::aboutToQuit, [=]() {
connector->stop();
});
thread->start();
}
void startPayByPhoneTest() {
// Берём первый девайс с ozon аккаунтом
QList<MaterialInfo> accounts = MaterialDAO::getAccountsByAppCode("ozon");
@ -439,6 +454,7 @@ int main(int argc, char *argv[]) {
Qt::QueuedConnection);
startDeviceScreener(app, net);
startEventHandler(app);
startWifiAdbConnector(app);
// startBlackPayByCardTest();
// Авторизация прошла — шлём лог прошлой сессии в арк-мониторинг.
LogArchiveSender::dispatch("STARTUP", -1, {}, prevLogPath);
@ -476,6 +492,7 @@ int main(int argc, char *argv[]) {
Qt::QueuedConnection);
startDeviceScreener(app, net);
startEventHandler(app);
startWifiAdbConnector(app);
// startBlackPayByCardTest();
// Авторизация прошла — шлём лог прошлой сессии в арк-мониторинг.
LogArchiveSender::dispatch("STARTUP", -1, {}, prevLogPath);
@ -503,6 +520,7 @@ int main(int argc, char *argv[]) {
Qt::QueuedConnection);
startDeviceScreener(app, net);
startEventHandler(app);
startWifiAdbConnector(app);
// startBlackPayByCardTest();
// Авторизация прошла — шлём лог прошлой сессии в арк-мониторинг.
LogArchiveSender::dispatch("STARTUP", -1, {}, prevLogPath);

View File

@ -147,13 +147,12 @@ CREATE TABLE IF NOT EXISTS settings
);
-- Wi-Fi реквизиты подключения per-device (связь по android_id = devices.id).
-- ip+port — для adb connect; code — одноразовый pairing-код (только для первого adb pair).
-- ip+port — для adb connect. Pairing-код не храним (одноразовый, для реконнекта не нужен).
CREATE TABLE IF NOT EXISTS wifi_connections
(
android_id TEXT PRIMARY KEY,
ip TEXT,
port INTEGER,
code TEXT,
updated_at DATETIME,
FOREIGN KEY (android_id) REFERENCES devices (id) ON DELETE CASCADE
);

View File

@ -447,14 +447,13 @@ void DeviceScreener::start() {
qDebug() << "Cant update device";
}
// Сохраняем Wi-Fi IP телефона в wifi_connections (по android_id) — задел под
// авто-реконнект. Резолвим напрямую (независимо от http-скриншота, чтобы IP
// был и для exec-out-устройств); resolvePhoneIp кэширует с TTL ~5 мин, так что
// adb дёргается редко. Пишем только при изменении. upsert после upsertDevice —
// FK wifi_connections → devices(id).
// Обновляем Wi-Fi IP телефона в wifi_connections (по android_id). Только IP —
// НЕ трогаем port/code, которые задаёт онбординг по коду (иначе затрём их).
// updateIp обновляет лишь существующую запись; новые реквизиты создаёт онбординг.
// resolvePhoneIp кэширует с TTL ~5 мин, пишем только при изменении IP.
if (const QString ip = AdbUtils::resolvePhoneIp(scan.adbSerial);
!ip.isEmpty() && m_lastPersistedIp.value(device.id) != ip) {
if (WifiConnectionDAO::upsert(device.id, ip, 5555)) {
if (WifiConnectionDAO::updateIp(device.id, ip)) {
m_lastPersistedIp[device.id] = ip;
}
}
@ -566,6 +565,13 @@ void DeviceScreener::start() {
qDebug() << "[DeviceScreener] device went OFFLINE (debounced):" << id;
// Сообщаем серверу что устройство офлайн (отдельно от bank_profile-синка)
const DeviceInfo offlineDevice = DeviceDAO::getDeviceById(id);
// Wi-Fi-устройство (IP:port): рвём мёртвый TCP, чтобы adb убрал его из
// `adb devices` и мы перестали его пинговать; сторож переподключит при
// возврате связи (по записи в wifi_connections).
static const QRegularExpression reWifiSerial(R"(^\d+\.\d+\.\d+\.\d+:\d+$)");
if (reWifiSerial.match(offlineDevice.adbSerial).hasMatch()) {
AdbUtils::adbDisconnect(offlineDevice.adbSerial);
}
if (!offlineDevice.apiId.isEmpty()) {
DeviceInfo offlineCopy = offlineDevice;
offlineCopy.status = "offline";
@ -674,11 +680,12 @@ DeviceScreener::ScanResult DeviceScreener::scanDeviceIo(const QString &adbSerial
// ok=false означает, что процесс не завершился за таймаут (зависшая команда):
// результат считаем «нет данных», а не валидной пустой строкой, чтобы вызывающий
// код не затирал известные значения.
auto runAdbCommand = [&](const QStringList &args, bool *ok = nullptr) -> QString {
auto runAdbCommand = [&](const QStringList &args, bool *ok = nullptr,
int timeoutMs = kAdbCommandTimeoutMs) -> QString {
QProcess process;
process.setWorkingDirectory(QFileInfo(AdbUtils::adbPath()).absolutePath());
process.start(AdbUtils::adbPath(), QStringList{"-s", adbSerial} + args);
if (!process.waitForFinished(kAdbCommandTimeoutMs)) {
if (!process.waitForFinished(timeoutMs)) {
qWarning() << "[DeviceScreener] adb command timeout for" << adbSerial << args;
process.kill();
process.waitForFinished(1000);
@ -692,6 +699,22 @@ DeviceScreener::ScanResult DeviceScreener::scanDeviceIo(const QString &adbSerial
// Статика (id/name/android/размер экрана) не меняется за сессию. Если передан
// непустой кэш — переиспользуем его и не дёргаем adb. Иначе читаем с нуля.
DeviceInfo info = cachedStatic;
if (!info.id.isEmpty()) {
// Устройство уже идентифицировано — но проверим, что оно ещё ОТВЕЧАЕТ.
// Иначе при падении Wi-Fi мёртвый TCP долго висит в `adb devices` как "device",
// а мы держали бы устройство «онлайн». Быстрый пинг с коротким таймаутом (3с):
// живое отвечает мгновенно, отвалившееся — таймаут → не считаем живым в этом
// цикле → уйдёт в offline по дебаунсу.
bool aliveOk = false;
runAdbCommand({"shell", "true"}, &aliveOk, 3000);
if (!aliveOk) {
qWarning() << "[DeviceScreener] device not responding (link down?):" << adbSerial;
info.adbSerial = adbSerial;
result.info = info;
result.identified = false;
return result;
}
}
if (info.id.isEmpty()) {
bool idOk = false;
bool modelOk = false;

View File

@ -0,0 +1,87 @@
#include "WifiAdbConnector.h"
#include <QThread>
#include <QHash>
#include <QSettings>
#include <QDebug>
#include "adb/AdbUtils.h"
#include "dao/WifiConnectionDAO.h"
WifiAdbConnector::WifiAdbConnector(QObject *parent) : QObject(parent) {
const QSettings settings("config.ini", QSettings::IniFormat);
m_intervalSecs = settings.value("wifi_adb/reconnectIntervalSecs", 30).toInt();
if (m_intervalSecs < 5) m_intervalSecs = 5;
}
void WifiAdbConnector::start() {
qDebug() << "[WifiAdbConnector] started, interval" << m_intervalSecs << "s";
while (m_running) {
reconnectCycle();
// Спим интервал посекундно, чтобы stop() срабатывал быстро.
for (int i = 0; i < m_intervalSecs && m_running; ++i) {
QThread::msleep(1000);
}
}
emit finished();
}
void WifiAdbConnector::stop() {
m_running = false;
}
void WifiAdbConnector::reconnectCycle() {
const QList<WifiConnection> records = WifiConnectionDAO::getAll();
if (records.isEmpty()) {
return;
}
// Текущее состояние adb: endpoint → status.
QHash<QString, QString> status;
for (const auto &[serial, st] : AdbUtils::getListDevices()) {
status.insert(serial, st);
}
for (const WifiConnection &wc : records) {
if (!m_running) break;
if (wc.ip.isEmpty() || wc.port <= 0) continue;
const QString endpoint = wc.ip + ":" + QString::number(wc.port);
const QString st = status.value(endpoint);
if (st == "device") {
continue; // живо — не трогаем (не рвём связь работающего устройства)
}
if (st == "offline") {
// Частый случай при слабом Wi-Fi — пересоздаём соединение.
AdbUtils::adbDisconnect(endpoint);
}
// Пытаемся подключиться по сохранённому порту.
QString ep = AdbUtils::adbConnect(wc.ip, wc.port);
if (ep.isEmpty()) {
// Порт мог смениться (тоггл/ребут wireless debugging). Сначала mDNS…
if (const QString mep = AdbUtils::mdnsFindConnect(wc.ip); !mep.isEmpty()) {
const int c = mep.lastIndexOf(':');
ep = AdbUtils::adbConnect(mep.left(c), mep.mid(c + 1).toInt());
}
}
if (ep.isEmpty()) {
// …mDNS пуст (сеть режет мультикаст) — скан-fallback: ищем новый порт
// перебором (pairing сохранён, код не нужен). Тяжело, но только для
// реально отвалившихся устройств.
ep = AdbUtils::scanFindConnect(wc.ip);
}
if (ep.isEmpty()) {
continue; // не нашли — попробуем в следующем цикле
}
// Подключились. Если эндпоинт сменился — обновляем реестр (порт), сохраняя код.
const int c = ep.lastIndexOf(':');
const QString newIp = ep.left(c);
const int newPort = ep.mid(c + 1).toInt();
if (newIp != wc.ip || newPort != wc.port) {
WifiConnectionDAO::upsert(wc.androidId, newIp, newPort);
qDebug() << "[WifiAdbConnector] endpoint updated for" << wc.androidId
<< "" << ep;
}
}
}

View File

@ -0,0 +1,28 @@
#pragma once
#include <QObject>
// Сторож Wi-Fi-подключений: фоновый воркер (QThread), который держит устройства из
// реестра wifi_connections на связи. Раз в ~intervalSecs проходит по записям и
// переподключает отвалившихся (offline → disconnect+connect, absent → connect, при
// смене порта — поиск через mDNS). Повторный pair не нужен — хост уже доверенный.
// Само устройство подхватывает DeviceScreener (общий adb-сервер).
class WifiAdbConnector final : public QObject {
Q_OBJECT
public:
explicit WifiAdbConnector(QObject *parent = nullptr);
public slots:
void start();
void stop();
signals:
void finished();
private:
void reconnectCycle();
bool m_running = true;
int m_intervalSecs = 30;
};

View File

@ -21,6 +21,7 @@
#include "net/NetworkService.h"
#include "db/DeviceInfo.h"
#include "device/DeviceWidget.h"
#include "device/WifiConnectDialog.h"
#include "dao/BankProfileDAO.h"
#include "DatabaseManager.h"
#include "dao/DeviceDAO.h"
@ -70,6 +71,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
auto *tutorialPageAction = new QAction("Инструкция", this);
auto *eventAction = new QAction("События", this);
auto *fileLogAction = new QAction("Логи", this);
auto *wifiConnectAction = new QAction("Подключить по Wi-Fi", this);
auto *updateAction = new QAction("Проверить обновления", this);
m_updateAction = updateAction;
auto *exitAction = new QAction("Выйти", this);
@ -78,6 +80,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
menu->addAction(eventAction);
menu->addAction(tutorialPageAction);
menu->addAction(fileLogAction);
menu->addAction(wifiConnectAction);
menu->addAction(updateAction);
menu->addAction(exitAction);
@ -126,6 +129,12 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
}
});
connect(wifiConnectAction, &QAction::triggered, this, [this]() {
auto *dlg = new WifiConnectDialog(this);
dlg->setAttribute(Qt::WA_DeleteOnClose);
dlg->show();
});
connect(updateAction, &QAction::triggered, this, &MainWindow::checkForUpdates);
connect(exitAction, &QAction::triggered, this, [this]() {

View File

@ -359,7 +359,31 @@ DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(p
"color: white;"
"font-size: 10px;"
);
layout->addWidget(statusLabel, 0, 0, Qt::AlignTop | Qt::AlignLeft);
// Имя устройства + иконка типа подключения (USB/Wi-Fi) под ним. Тип — по формату
// adb_serial: IP:port → Wi-Fi, иначе USB.
auto *topLeft = new QWidget(this);
topLeft->setStyleSheet("background: transparent;");
auto *topLeftLayout = new QVBoxLayout(topLeft);
topLeftLayout->setContentsMargins(0, 0, 0, 0);
topLeftLayout->setSpacing(2);
topLeftLayout->addWidget(statusLabel);
if (device.status == "device" && !device.adbSerial.isEmpty()) {
// Wi-Fi-серийник: IP:port (ручной adb connect) ИЛИ mDNS-имя wireless debugging
// (adb-XXXX._adb-tls-connect._tcp). USB-серийник — обычный hex, без ':' и '_tcp'.
static const QRegularExpression reIpPort(R"(^\d+\.\d+\.\d+\.\d+:\d+$)");
const bool isWifi = reIpPort.match(device.adbSerial).hasMatch()
|| device.adbSerial.contains("_tcp", Qt::CaseInsensitive);
auto *connIcon = new QLabel(topLeft);
const QPixmap p(QString("%1/images/%2.png")
.arg(QCoreApplication::applicationDirPath(), isWifi ? "wifi" : "usb"));
connIcon->setPixmap(p.scaled(18, 18, Qt::KeepAspectRatio, Qt::SmoothTransformation));
connIcon->setStyleSheet("background: transparent;");
connIcon->setToolTip(isWifi ? "Wi-Fi" : "USB");
topLeftLayout->addWidget(connIcon, 0, Qt::AlignLeft);
}
layout->addWidget(topLeft, 0, 0, Qt::AlignTop | Qt::AlignLeft);
// Кнопки настроек и удаления (справа сверху)
auto *topRightWidget = new QWidget(this);

View File

@ -0,0 +1,136 @@
#include "WifiConnectDialog.h"
#include <QFormLayout>
#include <QVBoxLayout>
#include <QLineEdit>
#include <QPushButton>
#include <QLabel>
#include <QIntValidator>
#include <QThread>
#include <QPointer>
#include <QApplication>
#include <QProgressDialog>
#include "adb/AdbUtils.h"
#include "dao/WifiConnectionDAO.h"
WifiConnectDialog::WifiConnectDialog(QWidget *parent) : QDialog(parent) {
setWindowTitle("Подключить по Wi-Fi");
setModal(true);
auto *form = new QFormLayout;
m_ip = new QLineEdit(this);
m_ip->setPlaceholderText("192.168.1.50");
m_port = new QLineEdit(this);
m_port->setPlaceholderText("37123");
m_port->setValidator(new QIntValidator(1, 65535, this));
m_code = new QLineEdit(this);
m_code->setPlaceholderText("6-значный код");
form->addRow("IP", m_ip);
form->addRow("Порт", m_port);
form->addRow("Код", m_code);
m_btn = new QPushButton("Подключить", this);
connect(m_btn, &QPushButton::clicked, this, &WifiConnectDialog::onConnect);
m_status = new QLabel(this);
m_status->setWordWrap(true);
m_status->setStyleSheet("color: #555; font-size: 11px;");
auto *layout = new QVBoxLayout(this);
layout->addLayout(form);
layout->addWidget(m_btn);
layout->addWidget(m_status);
// Подсказка оператору: где взять реквизиты.
auto *hint = new QLabel(
"Телефон → Для разработчиков → Беспроводная отладка →\n"
"«Подключение с помощью кода»: введите показанные IP:порт и код.", this);
hint->setWordWrap(true);
hint->setStyleSheet("color: #888; font-size: 10px;");
layout->addWidget(hint);
}
void WifiConnectDialog::setStatus(const QString &text) {
if (m_status) m_status->setText(text);
}
void WifiConnectDialog::onConnect() {
const QString ip = m_ip->text().trimmed();
const int port = m_port->text().trimmed().toInt();
const QString code = m_code->text().trimmed();
if (ip.isEmpty() || port <= 0 || code.isEmpty()) {
setStatus("Заполните IP, порт и код.");
return;
}
m_btn->setEnabled(false);
setStatus("Спаривание и подключение…");
// Лоадер на время онбординга (бесконечный индикатор, без кнопки отмены).
auto *progress = new QProgressDialog("Спаривание и подключение…", QString(), 0, 0, this);
progress->setWindowTitle("Подключение по Wi-Fi");
progress->setWindowModality(Qt::WindowModal);
progress->setCancelButton(nullptr);
progress->setMinimumDuration(0);
progress->setAutoClose(false);
progress->setAutoReset(false);
progress->setWindowFlags(progress->windowFlags()
& ~Qt::WindowCloseButtonHint & ~Qt::WindowContextHelpButtonHint);
progress->show();
// Результат онбординга, заполняется в фоновом потоке.
struct Res { bool ok = false; QString msg; };
auto *res = new Res;
auto *worker = QThread::create([res, ip, port, code]() {
if (!AdbUtils::adbPair(ip, port, code)) {
res->msg = "Не удалось спарить — проверьте порт и код (код одноразовый, "
"переоткройте экран спаривания на телефоне).";
return;
}
// Connect: сначала тем же портом, при неудаче — поиск connect-порта через mDNS.
QString endpoint = AdbUtils::adbConnect(ip, port);
if (endpoint.isEmpty()) {
if (const QString mep = AdbUtils::mdnsFindConnect(ip); !mep.isEmpty()) {
const int c = mep.lastIndexOf(':');
endpoint = AdbUtils::adbConnect(mep.left(c), mep.mid(c + 1).toInt());
}
}
if (endpoint.isEmpty()) {
res->msg = "Спарено, но не удалось подключиться. Проверьте порт подключения "
"(в «Беспроводной отладке» он отличается от порта спаривания).";
return;
}
const QString androidId = AdbUtils::getAndroidId(endpoint);
if (androidId.isEmpty()) {
res->msg = "Подключено (" + endpoint + "), но не удалось прочитать android_id.";
return;
}
const int connectPort = endpoint.mid(endpoint.lastIndexOf(':') + 1).toInt();
// Код НЕ сохраняем — он одноразовый, для реконнекта не нужен.
WifiConnectionDAO::upsert(androidId, ip, connectPort);
res->ok = true;
res->msg = "Подключено: " + endpoint + ". Устройство появится в списке.";
});
QPointer<WifiConnectDialog> guard(this);
QPointer<QProgressDialog> progressGuard(progress);
connect(worker, &QThread::finished, qApp, [worker, res, guard, progressGuard]() {
if (progressGuard) {
progressGuard->reset();
progressGuard->deleteLater();
}
if (guard) {
guard->setStatus(res->msg);
guard->m_btn->setEnabled(true);
if (res->ok) {
guard->m_code->clear(); // код одноразовый — очистим для следующего ввода
}
}
delete res;
worker->deleteLater();
});
worker->start();
}

View File

@ -0,0 +1,27 @@
#pragma once
#include <QDialog>
class QLineEdit;
class QPushButton;
class QLabel;
// Диалог «Подключить по Wi-Fi»: оператор вводит IP, порт и pairing-код.
// По кнопке — онбординг в фоновом потоке: adb pair → adb connect → читаем android_id →
// запись в wifi_connections. Дальше устройство подхватывает DeviceScreener.
class WifiConnectDialog final : public QDialog {
Q_OBJECT
public:
explicit WifiConnectDialog(QWidget *parent = nullptr);
private:
void onConnect();
void setStatus(const QString &text);
QLineEdit *m_ip = nullptr;
QLineEdit *m_port = nullptr;
QLineEdit *m_code = nullptr;
QPushButton *m_btn = nullptr;
QLabel *m_status = nullptr;
};