Привет, я Павел Семенищев, сетевой инженер в Yandex Infrastructure. В команде Network Operations Center (NOC) мы отвечаем не только за магистральные и дата‑центровые сети, но и за офисные, а также сети складов и дарксторов Яндекс Лавки. А это ОЧЕНЬ много удалённых точек присутствия, и при проблемах с Wi‑Fi на каждую сетевика не отправишь.

Для быстрого сканирования параметров сети на местах я создал WiProber под Android и WiFi Prober под iOS — получился сетевой «комбайн» для инженера, который сначала был нашим внутренним инструментом, а теперь есть и в общем доступе. Под катом расскажу, что умеют эти приложения, и какие ограничения операционных систем удалось обойти при их создании.

Для чего нужен свой сканер и что он может

В нашу зону ответственности входят около 700 дарксторов, разбросанных по всей стране, — и нам нужно было разобраться, как работает Wi‑Fi‑сеть в каждой отдельной точке. В меньших масштабах решить такие проблемы помогает радиообследование: с использованием профессионального оборудования вроде Ekahau Sidekick инженер может точечно найти неисправности Wi‑Fi и оптимизировать покрытие сети. 

Но в нашем случае такой вариант был нереальным: логистика сложная, дорогого оборудования и ресурсов инженеров в таком объёме просто нет. При этом диагностику провести всё‑таки надо. Тогда и появилась идея: сделать приложение, с которым любой человек на месте сможет пройтись по складу, а мы получим валидный инженерный отчёт.

Первый год я занимался разработкой под Android, поскольку на местах, как правило, всегда есть сотрудники или смежники с самыми обычными Android‑смартфонами. А уже затем появилось отдельное приложение для отображения параметров подключения в App Store. Функциональность на разных платформах немного отличается, поэтому сначала покажу, что умеет каждое из приложений, а потом расскажу подробнее про их создание.

WiProber v2.0: Android-приложение для радиообследования

В радиообследовании есть два метода для сканирования эфира: 

  • Stop-and-Go — останавливаемся, измеряем, делаем пару шагов, снова останавливаемся и т.д.

  • Continuous — непрерывно двигаемся и делаем измерения параллельно.

Нам хотелось реализовать именно второй режим. 

Не менее важным условием был экспорт в формат Ekahau (.esx), поскольку это стандарт для отчётов после радиообследования. Так полученный файл можно открыть и проанализировать в профессиональном софте Ekahau AI Pro, которым пользуются многие сетевые инженеры. 

Исходя из вводных, у приложения две главных киллер-фичи: 

  1. Режим Continuous: не надо тыкать в карту каждые 5 метров. Нажал «Старт», прошёл по маршруту, нажал «Стоп», а приложение тем временем само пишет трек и данные.

  2. Полная совместимость с Ekahau: на выходе получаем файл .esx, который открывается в Ekahau AI Pro.

Для Wi‑Fi‑инженера эти две фичи закрывают вполне конкретные боли. Continuous‑режим — привычный по Ekahau Pro «walking survey»: идёшь по объекту в нормальном темпе, а не дёргаешься в Stop‑and‑Go каждые пару метров, и при этом получаешь плотный трек измерений вдоль реального маршрута. Экспорт в.esx означает, что переучиваться не надо — открываешь результат в Ekahau AI Pro и получаешь все привычные артефакты: тепловые карты покрытия RSSI и SNR, наложение измерений на план. По сути, телефон становится дешёвым «полевым датчиком», а аналитика и оформление отчёта остаются в том же приложении, к которому инженер уже привык.

Эту разработку я выложил в опенсорс. После перевода на английский у приложения появилась дополнительная аудитория из разных стран, которая оставляет полезную обратную связь. 

Как это выглядит на телефоне, и как проект с тепловыми картами открывается в Ekahau AI Pro
Как это выглядит на телефоне, и как проект с тепловыми картами открывается в Ekahau AI Pro

WiFi Prober: приложение под iOS для отображения параметров подключения

На iPhone тоже хотелось иметь инструмент уровня «достал из кармана — продиагностировал проблему». Поэтому в App Store я выложил отдельное приложение. Вот что оно умеет: 

  • Deep Signal Analysis. Графики RSSI и Noise Floor в реальном времени. Можно увидеть историю сигнала, а не просто текущее значение, а также удобно мотать историю измерений и видеть события роуминга на графике.

  • Roaming Log. Приложение ловит моменты переключения клиента между точками доступа (BSSID) и каналами. Незаменимо при отладке «липких» клиентов и бесшовного роуминга. Данные об уровне сигнала, BSSID, канале и timestamp позволяют отслеживать залипание клиента, корректность выбора кандидата и работу 802.11k/v.

  • Latency Monitor. Честный TCP‑пинг. Приложение само определяет шлюз (Gateway) и пингует его параллельно с внешним хостом (ya.ru). Сразу видно, где проблема — во внутренней сети или у провайдера. 

  • Phy Specs. Вычисляет ширину канала (20/40/80 MHz) и MCS Index на лету. 

  • OUI Database. Умеет определять производителя оборудования, благодаря встроенной офлайн‑базе OUI на 45 000 записей. Видно не только MAC, но и железо (Cisco, Mikrotik, Ubiquiti, Apple). 

Killer Feature — AR Mode: включаете камеру, и приложение накладывает параметры сети поверх реального мира (HUD). Удобно делать радиообследование и записывать видеоотчёты прохода по объекту. Это walking survey без планшета и без Ekahau Sidekick: проход по объекту с видеозаписью, где параметры наложены на реальный мир — готовый визуальный отчёт заказчику.

.esx изнутри: два месяца на реверс бинарного формата

Прежде чем рассказывать про засады ОС, стоит сказать пару слов про формат, под который, собственно, всё это пишется. 

Файл.esx — это обычный zip‑архив, внутри которого лежит десятка полтора JSON‑файлов: project.json, floorPlans.json, accessPoints.json, measuredRadios.json, accessPointMeasurements.json, surveyLookups.json, survey-{uuid}.json, notes.json, pictureNotes.json, images.json, projectHistorys.json, wifiAdapterInformations.json — и набор справочников типа wallTypes.json, deviceProfiles.json, floorTypes.json и т.д. Всё это связано между собой через UUID: у точки доступа есть id, на него ссылается запись из measuredRadios.json через accessPointId, заметки держат список imageIds из images.json, и так далее. JSON‑часть реверсится быстро: распаковал zip — и видишь модель проекта почти один в один.

А вот сырые результаты измерений — RSSI‑семплы по каждой AP в каждой временной точке маршрута — лежат не в JSON, а в отдельных бинарных файлах внутри того же архива: по файлу на survey, с именами вида track-{uuid}.bin. И вот тут начинается настоящий хардкор: формат закрытый, документации нет, единственный источник истины — поведение Ekahau AI Pro, которое нужно объяснить наблюдением. На реверс ушло около двух месяцев в режиме «диффов»: создаю в AI Pro проект с одним survey и одним семплом, экспортирую в.esx, распаковываю, смотрю байты. Потом добавляю ещё один семпл — смотрю, что поменялось. Смотрю на результат сканирования сети в Ekahau в той же точке пространства с тем же адаптером с байтами бинарника, делаю предположения, что есть что.

В итоге картина оказалась такой: порядок байтов — big‑endian (а не little‑endian, как обычно ждёшь от современных форматов), файл начинается с двух байт глобального заголовка 02 01, а дальше идут блоки сканирований. Внутри каждого блока — однобайтовый маркер начала, целочисленный индекс скана, затем переменное число записей по AP, и однобайтовый «футер» в конце блока. Каждая запись по AP — это последовательность «тег‑байт + значение»: 0x01 — начало AP‑блока, 0x02 за которым Int32 относительного таймстемпа в миллисекундах, 0x04 за которым Int16 индекса AP в глобальном списке accessPointMeasurements, 0x05 за которым Int8 RSSI, 0x11 за которым Int32 частоты в МГц, и 0x09 — конец AP‑блока. Никаких magic‑строк, никакого CRC, длины блока нигде явно не записаны — длина AP‑записи фиксированная (17 байт), длина блока скана выводится только по маркеру конца. Именно это и съело основное время: без явных длин любая ошибка в одном поле уводит парсер по байтам, и весь следующий блок «съезжает».

GLOBAL_HEADER
├─ 0x02
└─ 0x01


SCAN
├─ 0x00
├─ scanId (4 bytes)
├─ AP
│  ├─ 0x01
│  ├─ 0x02 + timestamp(4)
│  ├─ 0x04 + apIndex(2)
│  ├─ 0x05 + rssi(1)
│  ├─ 0x11 + frequency(4)
│  └─ 0x09
├─ AP
│  ...
└─ 0x10


SCAN
...

Вот как это выглядит в реальном коде (фрагмент BinaryDataSerializer.kt из репозитория, слегка обрезанный):

private val BYTE_ORDER = ByteOrder.BIG_ENDIAN

fun serialize(measurements: List<MeasurementEntry>): ByteArray {
    val groupedScans = measurements
        .groupBy { it.relTimestamp }
        .toSortedMap()
    // ... расчёт totalSize: 2 байта header + (5 + N*17 + 1) на каждый скан
    val buffer = ByteBuffer.allocate(totalSize).order(BYTE_ORDER)

    buffer.put(0x02.toByte()); buffer.put(0x01.toByte())   // global header
    var scanIndex = 0
    groupedScans.forEach { (_, apList) ->
        buffer.put(0x00.toByte()); buffer.putInt(scanIndex) // scan id
        apList.forEach { item ->
            buffer.put(0x01.toByte())                                  // AP start
            buffer.put(0x02.toByte()); buffer.putInt(item.relTimestamp) // ts, ms
            buffer.put(0x04.toByte()); buffer.putShort(item.apIndex.toShort())
            buffer.put(0x05.toByte()); buffer.put(item.network.level.toByte()) // RSSI
            buffer.put(0x11.toByte()); buffer.putInt(item.network.frequency)
            buffer.put(0x09.toByte())                                  // AP end
        }
        buffer.put(0x10.toByte())                                       // scan footer
        scanIndex++
    }
    return buffer.array()
}

Финальный результат: WiProber собирает в cacheDir всю JSON-часть (project.json, floorPlans.json, accessPoints.json, measuredRadios.json, surveys.json, accessPointMeasurements.json и компанию), складывает рядом файлы track-{surveyId}.bin с этим бинарным форматом и упаковывает всё в один zip с расширением .esx. Ekahau AI Pro открывает такой файл «как родной»: видит план, AP, привязанные к ним замеры, рисует heatmap’ы покрытия RSSI и SNR и показывает трек walking survey прямо поверх планов — ровно так же, как если бы проект изначально был сделан в Sidekick.

Разработка под Android: что было трудно и как это лечилось

Первый год я не только разбирал структуру файлов Ekahau и писал алгоритмы, но и боролся с ограничениями ОС — Android очень не любит, когда приложения часто сканируют сети. Если коротко, меня поджидали три засады: разрешения, scan throttling и режим Continuous, в котором нужно сканировать так часто, как только система разрешит, и при этом не разорвать текущую ассоциацию с точкой доступа. Расскажу про каждую.

1. Разрешения

Геолокация ради SSID. Первая проблема любого Wi‑Fi‑сканера на современном Android — получение из WifiManager.scanResults хоть чего‑либо кроме MAC‑адреса своей же сети, приложению нужно разрешение ACCESS_FINE_LOCATION плюс включённая системная служба геолокации. Без неё scanResults возвращает пустой список — даже если разрешение выдано. Поэтому в манифесте пришлось запросить и fine, и coarse, а в рантайме я делаю двухступенчатую проверку: сначала разрешения, потом — статус самого класса LocationManager, который предоставляет доступ к системным службам геолокации, и только после этого жму на «скан».

<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
private fun checkPermissionsAndScan() {
    if (!WifiScanner.isWifiEnabled()) { /* открыть системную панель Wi-Fi */ return }

    val permissions = arrayOf(
        Manifest.permission.ACCESS_FINE_LOCATION,
        Manifest.permission.CHANGE_WIFI_STATE,
        Manifest.permission.ACCESS_WIFI_STATE
    )
    val allGranted = permissions.all {
        ActivityCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED
    }
    if (allGranted) {
        if (isLocationServiceEnabled()) scanWifi() else showLocationServiceDialog()
    } else {
        requestPermissionLauncher.launch(permissions)
    }
}

Скан — асинхронный: вызов и результат разнесены во времени. Метод wifiManager.startScan() — обычная функция, но она лишь только говорит системе «попробуй посканировать» и сразу возвращает управление; результаты приходят позже как отдельный broadcast SCAN_RESULTS_AVAILABLE_ACTION. И здесь оговорюсь: в Android «broadcast» означает не сетевой broadcast‑пакет, а внутренний механизм рассылки сообщений между процессами: ОС публикует Intent, а все приложения, которые подписались на это событие через BroadcastReceiver, его получают. Так что наш сценарий устроен в два шага: дёрнули функцию startScan() — сидим ждём, пока система через broadcast не скажет, что результаты готовы.
Причём метод официально deprecated с API 28, но альтернативы для произвольного приложения нет: companion‑device API под радиообследование не годится. Поэтому я аккуратно зарегистрировал receiver — с учётом нового флага RECEIVER_NOT_EXPORTED, который Google потребовал начиная с Android 13:

private val wifiScanReceiver = object : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        val success = intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, false)
        if (success && hasFineLocation(context)) {
            onScanResults?.invoke(wifiManager.scanResults)
        } else {
            onScanFailure?.invoke()
        }
    }
}

fun initialize(context: Context) {
    wifiManager = context.applicationContext
        .getSystemService(Context.WIFI_SERVICE) as WifiManager
    val filter = IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
        context.registerReceiver(wifiScanReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
    } else {
        @Suppress("DEPRECATION")
        context.registerReceiver(wifiScanReceiver, filter)
    }
}

Дальше — главная боль.

2. Scan throttling

Начиная с Android 9, система ограничивает приложение четырьмя сканированиями за две минуты — и это касается даже foreground‑приложений. Для радиообследования, где инженер ходит по складу и ожидает 100+ замеров на маршруте, это приговор. Полностью обойти ограничение нельзя (это сделано осознанно, ради аккумулятора), но можно:

  • Честно определить, включён ли throttling, через WifiManager.isScanThrottleEnabled на API 30+ либо через Settings.Global на более старых билдах.

  • Аккуратно вести собственный учёт «свежих» сканов, чтобы предсказуемо сообщать пользователю, сколько секунд ему ждать.

  • Подсказать инженеру отключить throttle в «Параметрах разработчика» — на корпоративных Android‑смартфонах это легитимный и быстрый путь.

В WiProber я использовал все три подхода. Приложение само проверяет, включен ли throttle на устройстве, и ведёт собственное окно из timestamp последних четырёх сканов: пока в окне меньше четырех записей за две минуты — пускаю скан, иначе показываю сообщение с обратным отсчётом «подождите N секунд». А также отдельно подсказываю: один раз снять ограничение в разделе для разработчиков, и сканирование пойдет с аппаратной скоростью.

private fun isSystemThrottlingEnabled(): Boolean {
    val wifi = applicationContext
        .getSystemService(Context.WIFI_SERVICE) as? WifiManager ?: return true
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) return wifi.isScanThrottleEnabled
    return try {
        Settings.Global.getInt(contentResolver, "wifi_scan_throttle_enabled") == 1
    } catch (_: Exception) { true }
}

private fun canScan(): Boolean {
   if (!isSystemThrottlingEnabled()) return true

   val twoMinutesInMillis = 2 * 60 * 1000
   val currentTime = System.currentTimeMillis()
   viewModel.recentScanTimestamps.removeAll { timestamp -> currentTime - timestamp > twoMinutesInMillis }

   if (viewModel.recentScanTimestamps.size < 4) {
       return true
   } else {
       val oldestRecentScan = viewModel.recentScanTimestamps.minOrNull() ?: currentTime
       val timeToWait = (oldestRecentScan + twoMinutesInMillis) - currentTime
       val secondsLeft = (timeToWait / 1000).toInt() + 1
       if (currentTime - lastThrottlingToastTime > 5000) {
           showThrottlingLimitDialog(secondsLeft)
           lastThrottlingToastTime = currentTime
       }
       return false
   }
}

3. Режим Continuous и нюанс с реассоциацией

В Stop‑and‑Go‑режиме перед каждым сканом я принудительно вызываю wifiManager.disconnect() — это даёт более точную картину эфира, потому что радиомодуль не «зацеплен» на текущую AP. Но в Continuous, где пользователь идёт по маршруту, рвать ассоциацию каждые несколько секунд недопустимо: и аккумулятор сядет, и работающие на телефоне сетевые сервисы будут страдать. Поэтому у startScan появился флаг shouldDisconnect, а цикл Continuous‑скана — это самозапланированный хвост: следующий запрос на скан стартует прямо из колбэка, как только пришли результаты, либо после короткой задержки в случае ошибки. Получается простая кооперативная петля на корутинах lifecycleScope, которую легко гасить флагом viewModel.isTracking.

WifiScanner.startScan(
    onResults = { results ->
        val duration = System.currentTimeMillis() - startTime
        viewModel.addContinuousScanResult(mapScanResults(results), duration)
        if (viewModel.isTracking.value == true) startContinuousScanningLoop()
    },
    onFailure = {
        lifecycleScope.launch {
            delay(1000)
            if (viewModel.isTracking.value == true) startContinuousScanningLoop()
        }
    },
    shouldDisconnect = false
)

В сумме это даёт скорость скана, близкую к аппаратной, без обрыва соединения и без «зависающих» состояний UI, если пользователь резко завершил сессию.

Где разработчика зажимает iOS и как из этого выкручиваться

С «яблочной» техникой были свои заморочки: на iOS все «гайки закручены» — система не отдаёт детальную статистику сигнала сторонним приложениям (Sandbox). 

На iOS архитектура ровно противоположна Android: системные API расскажут тебе про твою же ассоциацию, но не про эфир вокруг, а sandbox запрещает почти любое прямое общение с радиомодулем. Под капотом получилось три истории.

1. Откуда брать RSSI/Noise/MCS, если приложениям этого не дают

Apple убрала CNCopyCurrentNetworkInfo для большинства сценариев, а более низкоуровневых публичных API для статистики Wi‑Fi у third‑party‑приложений просто нет. Зато есть Shortcuts: системное действие Get Network Details видит и WiFiSig, и WiFiNoise, и WiFiTxRate/RxRate, и стандарт, и BSSID. 

Так и родилась связка: пользователь один раз ставит шорткат, который циклом читает параметры сети и через App Intents «стримит» их обратно в приложение. App Intent на стороне Swift получает JSON‑файл от шортката, парсит и публикует точку в реактивный поток Combine, на который уже подписана ViewModel и графики SwiftUI.

struct StreamDataToApp: AppIntent {
    static var title: LocalizedStringResource = "Stream Data to WiFi Monitor"
    static var openAppWhenRun: Bool = false

    @Parameter(title: "WiFi Parameters")
    var wifiParametersFile: IntentFile

    @MainActor
    func perform() async throws -> some IntentResult {
        let data = wifiParametersFile.data
        guard let dict = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
            throw IntentError.parsingFailed
        }
        let point = WiFiDataPoint(
            id: UUID(), timestamp: Date(),
            rssi:    dict["WiFiSig"]     as? Int    ?? 0,
            noise:   dict["WiFiNoise"]   as? Int    ?? 0,
            standard:dict["WiFiStd"]     as? String ?? "N/A",
            channel: dict["WiFiCh"]      as? Int    ?? 0,
            txRate: (dict["WiFiTxRate"]  as? NSNumber)?.doubleValue ?? 0,
            rxRate: (dict["WiFiRxRate"]  as? NSNumber)?.doubleValue ?? 0,
            ssid:    dict["WiFiName"]    as? String ?? "N/A",
            bssid:   dict["WiFiBSSID"]   as? String ?? "N/A"
        )
        DataStream.subject.send(point)
        return .result()
    }
}

Обратный канал — «пора остановиться» — сделан как ещё один интент ShouldStopMonitoring, читающий флаг из общего App Group UserDefaults. Шорткат опрашивает его на каждой итерации и сам корректно выходит из цикла, как только пользователь нажал «Stop» в приложении. Получается полудуплексная связка без нативного фонового сервиса (которого приложению‑инструменту в iOS никто и не даст).

2. Roaming Log на коленке

Раз уж точки сигнала всё равно прилетают в поток, ловить роуминг — это просто сравнение текущего BSSID с предыдущим. Событие фиксируется только если BSSID реально сменился и оба значения не пустые. К событию я тут же кладу RSSI «до/после» и канал — этого хватает, чтобы потом в CSV‑отчёте увидеть, где клиент «прыгнул» с диапазона 5 GHz на 2.4 GHz, и понять, не залип ли он на дальней AP.

if let last = lastBSSID,
   !last.isEmpty, !point.bssid.isEmpty,
   last != point.bssid {
    let event = RoamingEvent(
        timestamp: point.timestamp,
        fromBSSID: last,         toBSSID: point.bssid,
        fromRSSI: lastRSSI ?? 0, toRSSI: point.rssi,
        fromChannel: lastChannel ?? 0, toChannel: point.channel
    )
    roamingEvents.insert(event, at: 0)
    if roamingEvents.count > 50 { roamingEvents.removeLast() }
}
lastBSSID = point.bssid
lastRSSI  = point.rssi
lastChannel = point.channel

3. Latency Monitor: TCP-пинг и поиск IP-адреса шлюза

ICMP‑сокеты на iOS требуют либо raw‑сокетов, либо стороннего SimplePing — и то и другое для инструмента из App Store избыточно. Я обошёлся TCP‑пингом через Network.framework: измеряю время от старта NWConnection до состояния .ready на 443/53/80 — это близкая к реальной latency, потому что в неё входит и SYN/SYN‑ACK, и TLS‑инициализация при HTTPS. Чтобы пинговать оба адреса параллельно — внешний (ya.ru / 77.88.44.242) и внутренний шлюз, — нужен сам IP-адрес шлюза. CoreLocation/NetworkExtension его не отдают, поэтому пришлось спуститься в Darwin и аккуратно прочитать таблицу маршрутов через sysctl(CTL_NET, PF_ROUTE, NET_RT_FLAGS, …). Кусок самый «СИшный» во всём проекте, но обёрнут он буквально в одну функцию GatewayTools.getGateways(), возвращающую IPv4- и IPv6-адреса.

static func ping(host: String, port: UInt16 = 443) async -> Double? {
    let start = ProcessInfo.processInfo.systemUptime
    return await withCheckedContinuation { cont in
        let endpoint = NWEndpoint.hostPort(
            host: NWEndpoint.Host(host),
            port: NWEndpoint.Port(integerLiteral: port))
        let conn = NWConnection(to: endpoint, using: .tcp)
        let lock = OSAllocatedUnfairLock(initialState: false)
        let finish = { (rtt: Double?) in
            let go = lock.withLock { resumed -> Bool in
                if resumed { return false }
                resumed = true; return true
            }
            if go { conn.cancel(); cont.resume(returning: rtt) }
        }
        conn.stateUpdateHandler = { state in
            switch state {
            case .ready:               finish((ProcessInfo.processInfo.systemUptime - start) * 1000)
            case .failed, .cancelled:  finish(nil)
            default: break
            }
        }
        conn.start(queue: .global())
        DispatchQueue.global().asyncAfter(deadline: .now() + 2) { finish(nil) }
    }
}
private static func getGateway(family: Int32) -> String? {
    var mib: [Int32] = [CTL_NET, PF_ROUTE, 0, family, NET_RT_FLAGS, RTF_GATEWAY]
    var len = 0
    if sysctl(&mib, 6, nil, &len, nil, 0) < 0 { return nil }
    var buf = [UInt8](repeating: 0, count: len)
    if sysctl(&mib, 6, &buf, &len, nil, 0) < 0 { return nil }
    // … парсим RTM-сообщения и достаём адрес шлюза через getnameinfo()
}

В результате без приватных API получился рабочий «карманный» инструмент: графики RSSI/Noise — через Shortcuts‑мост, roaming‑лог — поверх потока данных, ping и AR‑режим — нативно. С точки зрения App Store‑ревью это абсолютно «белая» комбинация: я не лезу в радиомодуль, я лишь красиво пакую и визуализирую то, что система согласна мне дать.

Статус проекта

Приложение прошло ревью Apple и доступно в App Store. Инструменты типа Ping и AR работают сразу, а для работы графиков нужно будет один раз из приложения нажать кнопку установки вспомогательного шортката.

WiProber под Android выложен в опенсорсе. Буду рад, если инструмент пригодится в работе или личных целях и вам. Исходный код открыт — заходите, смотрите, приносите идеи или сразу пул‑реквесты, если знакомы с Kotlin.

А ещё больше про разработки Yandex Infrastructure — в канале нашей команды.

Комментарии (6)


  1. Tirarex
    05.06.2026 07:39

    Этакий Ubiquiti wifiman но который не умеет строить карты в AR режиме.


    1. htechno Автор
      05.06.2026 07:39

      WiFiMan классное приложение. Правда не всегда стабильно работает, часты вылетает, ну да ладно. Я добавлял в приложения те функции, которые нужны именно мне как WiFi инженеру в своей работе.


    1. exhalace
      05.06.2026 07:39

      WiFiMan умеет выгружать результаты в *.esx (Ekahau) формат?


  1. Belkau77
    05.06.2026 07:39

    100% пригодится. сохранил себе.
    п.с. не было идей запилить или изолировать какие то функции на платной основе?
    гипотетически спрашиваю, очень полезный инструмент явно...


    1. htechno Автор
      05.06.2026 07:39

      Была такая мимолетная идея, но я её сразу отмёл. Целевая аудитория слишком маленькая чтобы заработать, есть подобные бесплатные решения, это мой Pet-проект для работы, а не способ заработка.


  1. RFAREAS
    05.06.2026 07:39

    Интересно. Большая работа проделана. Респект.