From 50be87526568d93de752bdd220f6ef0095623784 Mon Sep 17 00:00:00 2001
From: Andrei Ovcharenko
Date: Wed, 29 Jul 2026 13:20:19 +0300
Subject: [PATCH] bwl: import and reconcile connected clients on nl80211
The nl80211 flavor left pre/generate_connected_clients_events() as
stubs that report success, so stations already associated when the
agent starts never reach the controller: BML shows only clients that
(re)associate afterwards, keeps stale parents and reports RSSI -127.
Port the DWPAL station sweep to the nl80211 AP and monitor HALs: walk
every VAP with STA-FIRST/STA-NEXT over the hostapd control socket,
respect the iteration time budget, validate and deduplicate stations,
skip authenticated-but-not-associated entries, survive a station
vanishing mid-iteration (bounded VAP restart), and stop a failed VAP
without wedging the FSM. Station capabilities are parsed from the
hostapd station dump.
The AP HAL keeps the sweep idempotent: it tracks currently connected
stations from runtime events, emits an association only for new or
moved stations, and reconciles stale clients only for VAPs whose
enumeration reached a clean end-of-list (an empty reply; FAIL past the
restart budget leaves the VAP partially walked). A pre-sweep snapshot,
invalidated by runtime events, prevents a concurrent association from
being undone. The new supports_connected_clients_reconciliation()
capability lets the AP manager re-run the sweep every 60 seconds; HALs
without that guarantee keep the previous run-once behavior.
The whole series is publicly reviewable at
https://gitlab.com/kreout/prpl-mesh-mercusys/-/merge_requests/1; the
upstream GitLab only accepts merge requests from project members, so
the upstream submission itself goes through the prpl Foundation Jira.
Signed-off-by: Andrei Ovcharenko
---
.../ap_manager/ap_manager.cpp | 14 +
.../fronthaul_manager/ap_manager/ap_manager.h | 2 +
common/beerocks/bwl/include/bwl/ap_wlan_hal.h | 8 +
.../bwl/nl80211/ap_wlan_hal_nl80211.cpp | 337 +++++++++++++++++-
.../bwl/nl80211/ap_wlan_hal_nl80211.h | 17 +
.../bwl/nl80211/base_wlan_hal_nl80211.cpp | 19 +
.../bwl/nl80211/base_wlan_hal_nl80211.h | 3 +
.../bwl/nl80211/mon_wlan_hal_nl80211.cpp | 129 ++++++-
.../bwl/nl80211/mon_wlan_hal_nl80211.h | 12 +
9 files changed, 526 insertions(+), 15 deletions(-)
--- a/agent/src/beerocks/fronthaul_manager/ap_manager/ap_manager.cpp
+++ b/agent/src/beerocks/fronthaul_manager/ap_manager/ap_manager.cpp
@@ -747,6 +747,20 @@ bool ApManager::ap_manager_fsm(bool &con
m_generate_connected_clients_events = !is_finished_all_clients;
}
+ // Periodic reconciliation: HALs with an idempotent sweep re-run it to
+ // recover association events lost while the control socket was busy
+ // and to clean up stale clients.
+ if (!m_generate_connected_clients_events &&
+ ap_wlan_hal->supports_connected_clients_reconciliation() &&
+ now > m_next_client_reconciliation_time) {
+ m_next_client_reconciliation_time = now + std::chrono::seconds(60);
+ if (ap_wlan_hal->pre_generate_connected_clients_events()) {
+ m_generate_connected_clients_events = true;
+ } else {
+ LOG(WARNING) << "Failed to prepare connected clients reconciliation";
+ }
+ }
+
// Allow clients with expired blocking period timer
allow_expired_clients();
break;
--- a/agent/src/beerocks/fronthaul_manager/ap_manager/ap_manager.h
+++ b/agent/src/beerocks/fronthaul_manager/ap_manager/ap_manager.h
@@ -300,6 +300,8 @@ private:
bool m_generate_connected_clients_events = false;
std::chrono::steady_clock::time_point m_next_generate_connected_events_time =
std::chrono::steady_clock::time_point::min();
+ std::chrono::steady_clock::time_point m_next_client_reconciliation_time =
+ std::chrono::steady_clock::now() + std::chrono::seconds(60);
//Timer for triggering a CSA notification
void start_csa_notification_timer(
--- a/common/beerocks/bwl/include/bwl/ap_wlan_hal.h
+++ b/common/beerocks/bwl/include/bwl/ap_wlan_hal.h
@@ -442,6 +442,14 @@ public:
virtual bool pre_generate_connected_clients_events() = 0;
/**
+ * @brief Whether generate_connected_clients_events() is idempotent for
+ * this HAL (unchanged clients produce no duplicate events and stale
+ * clients are reported as disconnected), so the AP manager may re-run
+ * it periodically to reconcile missed association events.
+ */
+ virtual bool supports_connected_clients_reconciliation() const { return false; }
+
+ /**
* @brief Start WPS PBC procedure on a given VAP
*
* @param iface_name VAP interface on which to start WPS PBC
--- a/common/beerocks/bwl/nl80211/ap_wlan_hal_nl80211.cpp
+++ b/common/beerocks/bwl/nl80211/ap_wlan_hal_nl80211.cpp
@@ -22,6 +22,7 @@
#include
#include
#include
+#include
#include
//////////////////////////////////////////////////////////////////////////////
@@ -1291,18 +1292,344 @@ bool ap_wlan_hal_nl80211::set_disabled_s
return true;
}
+// IEEE 802.11 HT capability bits needed to interpret hostapd station dumps;
+// the stock build has no hostapd headers to take them from.
+static constexpr uint16_t NL80211_HT_CAP_SUPP_CHANNEL_WIDTH_SET = 0x0002;
+static constexpr uint16_t NL80211_HT_CAP_SMPS_MASK = 0x000C;
+static constexpr uint16_t NL80211_HT_CAP_SHORT_GI20MHZ = 0x0020;
+static constexpr uint16_t NL80211_HT_CAP_SHORT_GI40MHZ = 0x0040;
+
+static void sta_caps_from_ht(const int *HT_MCS, const std::string &ht_cap_str,
+ beerocks::message::sRadioCapabilities &sta_caps)
+{
+ sta_caps.ht_bw = beerocks::BANDWIDTH_UNKNOWN;
+
+ if (ht_cap_str.empty()) {
+ sta_caps.ant_num = 1;
+ return;
+ }
+
+ uint16_t ht_cap = uint16_t(std::strtoul(ht_cap_str.c_str(), nullptr, 16));
+ sta_caps.ht_bw = (ht_cap & NL80211_HT_CAP_SUPP_CHANNEL_WIDTH_SET) ? beerocks::BANDWIDTH_40
+ : beerocks::BANDWIDTH_20;
+ sta_caps.ht_sm_power_save = ((ht_cap & NL80211_HT_CAP_SMPS_MASK) >> 2) & 0x03;
+ sta_caps.ht_low_bw_short_gi = (ht_cap & NL80211_HT_CAP_SHORT_GI20MHZ) != 0;
+ sta_caps.ht_high_bw_short_gi = (ht_cap & NL80211_HT_CAP_SHORT_GI40MHZ) != 0;
+
+ uint32_t ht_mcs = 0;
+ for (uint8_t i = 0; i < 4; i++) {
+ ht_mcs |= uint32_t(HT_MCS[i]) << (8 * i);
+ }
+ uint32_t mask = 0x80000000;
+ for (uint8_t i = 4; i > 0; i--) { // 4ss
+ for (int8_t j = 7; j >= 0; j--) { // 8 bits
+ if ((ht_mcs & mask) > 0) {
+ sta_caps.ht_ss = i;
+ sta_caps.ant_num = i;
+ sta_caps.ht_mcs = j;
+ return;
+ }
+ mask /= 2;
+ }
+ }
+}
+
+static void sta_caps_from_vht(const int16_t *VHT_MCS, const std::string &vht_cap_str,
+ beerocks::message::sRadioCapabilities &sta_caps)
+{
+ sta_caps.vht_bw = beerocks::BANDWIDTH_UNKNOWN;
+
+ if (!vht_cap_str.empty()) {
+ uint32_t vht_cap = std::strtoul(vht_cap_str.c_str(), nullptr, 16);
+ uint8_t supported_bw_bits = (vht_cap >> 2) & 0x03;
+
+ sta_caps.vht_bw =
+ (supported_bw_bits == 0) ? beerocks::BANDWIDTH_80 : beerocks::BANDWIDTH_160;
+ sta_caps.vht_low_bw_short_gi = (vht_cap >> 5) & 0x01;
+ sta_caps.vht_high_bw_short_gi = (vht_cap >> 6) & 0x01;
+
+ uint16_t vht_mcs_rx = uint16_t(VHT_MCS[0]);
+ for (uint8_t i = 4; i > 0; i--) { // 4ss
+ uint16_t vht_mcs_temp = (vht_mcs_rx >> (2 * (i - 1))) & 0x03;
+ if (vht_mcs_temp != 0x3) { // 0x3 == not supported
+ sta_caps.vht_ss = i;
+ sta_caps.ant_num = i;
+ sta_caps.vht_mcs = vht_mcs_temp + 7;
+ break;
+ }
+ }
+ sta_caps.vht_su_beamformer = (vht_cap >> 11) & 0x01;
+ sta_caps.vht_mu_beamformer = (vht_cap >> 19) & 0x01;
+ } else if (sta_caps.ant_num == 0) {
+ sta_caps.ant_num = 1;
+ }
+
+ if (sta_caps.vht_ss) {
+ sta_caps.wifi_standard = STANDARD_AC;
+ } else if (sta_caps.ht_ss) {
+ sta_caps.wifi_standard = STANDARD_N;
+ } else {
+ sta_caps.wifi_standard = STANDARD_A;
+ }
+}
+
+static void sta_caps_from_rates(const int *supported_rates,
+ beerocks::message::sRadioCapabilities &sta_caps)
+{
+ uint16_t max_rate = 0;
+ for (int i = 0; i < 16; i++) {
+ uint16_t temp_rate = (supported_rates[i] & 0x7F) * 5; // rate/2 * 10
+ if (temp_rate > max_rate) {
+ max_rate = temp_rate;
+ }
+ }
+ son::wireless_utils::get_mcs_from_rate(max_rate, beerocks::ANT_MODE_1X1_SS1,
+ beerocks::BANDWIDTH_20, sta_caps.default_mcs,
+ sta_caps.default_short_gi);
+}
+
+// Parse one hostapd STA-FIRST/STA-NEXT reply block into an association
+// notification. Returns nullptr on malformed input. A station that has not
+// completed association carries no connected_time line; it is reported
+// through @a associated so the caller can skip it but keep iterating.
+static std::shared_ptr parse_sta_block(const std::string &reply, int vap_id, bool radio_5G,
+ bool &associated, sMacAddr &mac_out)
+{
+ std::istringstream stream(reply);
+ std::string line;
+
+ if (!std::getline(stream, line)) {
+ return nullptr;
+ }
+ if (!line.empty() && line.back() == '\r') {
+ line.pop_back();
+ }
+ if (!looks_like_mac(line)) {
+ return nullptr;
+ }
+ mac_out = tlvf::mac_from_string(line);
+
+ std::unordered_map options;
+ while (std::getline(stream, line)) {
+ if (!line.empty() && line.back() == '\r') {
+ line.pop_back();
+ }
+ auto separator = line.find('=');
+ if (separator == std::string::npos) {
+ continue;
+ }
+ options[line.substr(0, separator)] = line.substr(separator + 1);
+ }
+
+ associated = (options.find("connected_time") != options.end());
+
+ auto msg_buff = ALLOC_SMART_BUFFER(sizeof(sACTION_APMANAGER_CLIENT_ASSOCIATED_NOTIFICATION));
+ auto msg = reinterpret_cast(msg_buff.get());
+ if (!msg) {
+ LOG(FATAL) << "Memory allocation failed";
+ return nullptr;
+ }
+ memset(msg_buff.get(), 0, sizeof(sACTION_APMANAGER_CLIENT_ASSOCIATED_NOTIFICATION));
+
+ msg->params.vap_id = vap_id;
+ msg->params.mac = mac_out;
+ msg->params.capabilities = {};
+
+ int supported_rates[16] = {0};
+ std::istringstream rates(options["supported_rates"]);
+ std::string rate;
+ for (int i = 0; i < 16 && (rates >> rate); i++) {
+ supported_rates[i] = int(std::strtoul(rate.c_str(), nullptr, 16));
+ }
+
+ int HT_MCS[16] = {0};
+ const auto &ht_mcs_line = options["ht_mcs_bitmask"];
+ for (size_t i = 0; i < 16 && 2 * i + 1 < ht_mcs_line.length(); i++) {
+ HT_MCS[i] = int(std::strtoul(ht_mcs_line.substr(2 * i, 2).c_str(), nullptr, 16));
+ }
+
+ int16_t VHT_MCS[1] = {0};
+ const auto &vht_mcs_line = options["rx_vht_mcs_map"];
+ if (vht_mcs_line.length() >= 4) {
+ VHT_MCS[0] = int16_t(std::strtoul(vht_mcs_line.substr(0, 4).c_str(), nullptr, 16));
+ }
+
+ msg->params.capabilities.max_tx_power =
+ uint8_t(std::strtoul(options["max_txpower"].c_str(), nullptr, 10));
+
+ sta_caps_from_ht(HT_MCS, options["ht_caps_info"], msg->params.capabilities);
+ if (radio_5G) {
+ sta_caps_from_vht(VHT_MCS, options["vht_caps_info"], msg->params.capabilities);
+ }
+ sta_caps_from_rates(supported_rates, msg->params.capabilities);
+
+ return msg_buff;
+}
+
bool ap_wlan_hal_nl80211::generate_connected_clients_events(
bool &is_finished_all_clients, std::chrono::steady_clock::time_point max_iteration_timeout)
{
- LOG(TRACE) << __func__ << " - NOT IMPLEMENTED!";
+ auto next_unhandled_vap = [this]() {
+ for (const auto &vap : m_radio_info.available_vaps) {
+ if (m_completed_vaps.find(vap.first) == m_completed_vaps.end()) {
+ return vap.first;
+ }
+ }
+ return INVALID_VAP_ID;
+ };
+
+ is_finished_all_clients = false;
+
+ if (m_vap_id_in_progress == INVALID_VAP_ID) {
+ m_vap_id_in_progress = next_unhandled_vap();
+ m_vap_restart_count = 0;
+ }
+
+ while (m_vap_id_in_progress != INVALID_VAP_ID) {
+ auto vap_it = m_radio_info.available_vaps.find(m_vap_id_in_progress);
+ if (vap_it == m_radio_info.available_vaps.end() || vap_it->second.bss.empty()) {
+ m_completed_vaps.insert(m_vap_id_in_progress);
+ m_vap_id_in_progress = next_unhandled_vap();
+ continue;
+ }
+ // On stock OpenWrt the BSS name is the real netdev the control
+ // socket is registered under; wlanX.Y spellings do not exist here.
+ auto vap_iface = vap_it->second.bss;
+
+ bool vap_done = false;
+ bool vap_enumerated = false;
+ while (!vap_done) {
+ if (std::chrono::steady_clock::now() > max_iteration_timeout) {
+ // Out of time budget: resume from the same cursor next wakeup.
+ return true;
+ }
+
+ std::string cmd = m_queried_first ? "STA-NEXT " + tlvf::mac_to_string(m_prev_client_mac)
+ : "STA-FIRST";
+ char placeholder = 0;
+ char *reply = &placeholder;
+ if (!wpa_ctrl_send_msg(cmd, &reply, vap_iface) || !reply) {
+ LOG(WARNING) << __func__ << ": '" << cmd << "' failed on " << vap_iface;
+ if (m_queried_first && m_vap_restart_count++ < 2) {
+ // The cursor station may just have disconnected; restart
+ // this VAP - the handled set keeps events deduplicated.
+ m_queried_first = false;
+ m_prev_client_mac = beerocks::net::network_utils::ZERO_MAC;
+ return true;
+ }
+ // A dead control socket must not wedge the whole FSM.
+ vap_done = true;
+ break;
+ }
+
+ std::string text(reply);
+ if (text.empty() || text.rfind("FAIL", 0) == 0) {
+ if (!text.empty() && m_queried_first && m_vap_restart_count++ < 2) {
+ // FAIL for a known cursor: the station disappeared
+ // between STA-NEXT calls - restart this VAP once.
+ m_queried_first = false;
+ m_prev_client_mac = beerocks::net::network_utils::ZERO_MAC;
+ continue;
+ }
+ // Only an empty reply is a clean end-of-list; FAIL past the
+ // restart budget leaves the VAP only partially walked.
+ vap_enumerated = text.empty();
+ vap_done = true;
+ break;
+ }
+ if (text.rfind("UNKNOWN", 0) == 0) {
+ LOG(WARNING) << "Station enumeration is unsupported on " << vap_iface;
+ vap_done = true;
+ break;
+ }
+
+ bool associated = false;
+ sMacAddr sta_mac;
+ auto msg_buff = parse_sta_block(text, m_vap_id_in_progress, get_radio_info().is_5ghz,
+ associated, sta_mac);
+ if (!msg_buff) {
+ LOG(WARNING) << "Malformed station block on " << vap_iface
+ << ", skipping the rest of this VAP";
+ vap_done = true;
+ break;
+ }
+
+ m_queried_first = true;
+ m_prev_client_mac = sta_mac;
+
+ if (!m_handled_clients.insert(sta_mac).second) {
+ continue; // seen earlier in this sweep
+ }
+ if (!associated) {
+ continue; // authenticated but not associated
+ }
+
+ auto known = m_connected_clients.find(sta_mac);
+ if (known != m_connected_clients.end() && known->second == m_vap_id_in_progress) {
+ continue; // unchanged client: no duplicate event
+ }
+ m_connected_clients[sta_mac] = m_vap_id_in_progress;
+ LOG(DEBUG) << "Importing connected client " << sta_mac << " on " << vap_iface;
+ event_queue_push(Event::STA_Connected, msg_buff);
+ }
+
+ m_completed_vaps.insert(m_vap_id_in_progress);
+ if (vap_enumerated) {
+ m_enumerated_vaps.insert(m_vap_id_in_progress);
+ }
+ m_queried_first = false;
+ m_prev_client_mac = beerocks::net::network_utils::ZERO_MAC;
+ m_vap_id_in_progress = next_unhandled_vap();
+ m_vap_restart_count = 0;
+ }
+
+ // Reconcile only clients that existed before the sweep, on VAPs whose
+ // enumeration reached end-of-list. Runtime events remove their station
+ // from the snapshot, preventing a concurrent reconnect from being undone.
+ for (const auto &client : m_clients_before_sweep) {
+ if (m_enumerated_vaps.find(client.second) == m_enumerated_vaps.end() ||
+ m_handled_clients.find(client.first) != m_handled_clients.end()) {
+ continue;
+ }
+
+ auto current = m_connected_clients.find(client.first);
+ if (current == m_connected_clients.end() || current->second != client.second) {
+ continue;
+ }
+
+ auto msg_buff =
+ ALLOC_SMART_BUFFER(sizeof(sACTION_APMANAGER_CLIENT_DISCONNECTED_NOTIFICATION));
+ auto msg =
+ reinterpret_cast(msg_buff.get());
+ if (!msg) {
+ continue;
+ }
+ memset(msg_buff.get(), 0, sizeof(sACTION_APMANAGER_CLIENT_DISCONNECTED_NOTIFICATION));
+ msg->params.mac = client.first;
+ msg->params.vap_id = client.second;
+ LOG(DEBUG) << "Client " << client.first
+ << " no longer known to hostapd, reporting stale disconnect";
+ event_queue_push(Event::STA_Disconnected, msg_buff);
+ m_connected_clients.erase(current);
+ }
+
+ m_clients_before_sweep.clear();
+ m_enumerated_vaps.clear();
+ m_handled_clients.clear();
is_finished_all_clients = true;
return true;
}
bool ap_wlan_hal_nl80211::pre_generate_connected_clients_events()
{
-
- LOG(TRACE) << __func__ << " - NOT IMPLEMENTED!";
+ m_vap_id_in_progress = INVALID_VAP_ID;
+ m_prev_client_mac = beerocks::net::network_utils::ZERO_MAC;
+ m_queried_first = false;
+ m_vap_restart_count = 0;
+ m_completed_vaps.clear();
+ m_enumerated_vaps.clear();
+ m_handled_clients.clear();
+ m_clients_before_sweep = m_connected_clients;
return true;
}
@@ -1427,6 +1754,8 @@ bool ap_wlan_hal_nl80211::process_nl8021
}
// Add the message to the queue
+ m_connected_clients[msg->params.mac] = vap_id;
+ m_clients_before_sweep.erase(msg->params.mac);
event_queue_push(Event::STA_Connected, msg_buff);
} break;
@@ -1453,6 +1782,8 @@ bool ap_wlan_hal_nl80211::process_nl8021
msg->params.vap_id = vap_id;
msg->params.mac = tlvf::mac_from_string(parsed_obj[bwl::EVENT_KEYLESS_PARAM_MAC]);
+ m_connected_clients.erase(msg->params.mac);
+ m_clients_before_sweep.erase(msg->params.mac);
// Add the message to the queue
event_queue_push(Event::STA_Disconnected, msg_buff);
--- a/common/beerocks/bwl/nl80211/ap_wlan_hal_nl80211.h
+++ b/common/beerocks/bwl/nl80211/ap_wlan_hal_nl80211.h
@@ -12,6 +12,10 @@
#include "base_wlan_hal_nl80211.h"
#include
+#include
+#include
+#include
+
namespace bwl {
namespace nl80211 {
@@ -93,6 +97,7 @@ public:
* @see ap_wlan_hal::pre_generate_connected_clients_events
*/
virtual bool pre_generate_connected_clients_events() override;
+ virtual bool supports_connected_clients_reconciliation() const override { return true; }
virtual bool start_wps_pbc() override;
virtual bool set_mbo_assoc_disallow(const std::string &bssid, bool enable) override;
@@ -140,6 +145,18 @@ protected:
}
private:
+ // Connected-clients import and reconciliation (STA-FIRST/STA-NEXT sweep)
+ static constexpr int INVALID_VAP_ID = -1;
+ std::set m_completed_vaps;
+ std::set m_enumerated_vaps;
+ std::unordered_set m_handled_clients;
+ std::unordered_map m_connected_clients;
+ std::unordered_map m_clients_before_sweep;
+ sMacAddr m_prev_client_mac = {};
+ bool m_queried_first = false;
+ int m_vap_id_in_progress = INVALID_VAP_ID;
+ int m_vap_restart_count = 0;
+
// Unassociated measurement state variables
std::chrono::steady_clock::time_point m_unassoc_measure_start;
int m_unassoc_measure_window_size = 0;
--- a/common/beerocks/bwl/nl80211/base_wlan_hal_nl80211.cpp
+++ b/common/beerocks/bwl/nl80211/base_wlan_hal_nl80211.cpp
@@ -27,9 +27,28 @@
#include
#include
+#include
+
namespace bwl {
namespace nl80211 {
+bool looks_like_mac(const std::string &text)
+{
+ if (text.length() != 17) {
+ return false;
+ }
+ for (size_t i = 0; i < text.length(); i++) {
+ if (i % 3 == 2) {
+ if (text[i] != ':') {
+ return false;
+ }
+ } else if (!std::isxdigit(static_cast(text[i]))) {
+ return false;
+ }
+ }
+ return true;
+}
+
//////////////////////////////////////////////////////////////////////////////
///////////////////////// Local Module Definitions ///////////////////////////
//////////////////////////////////////////////////////////////////////////////
--- a/common/beerocks/bwl/nl80211/base_wlan_hal_nl80211.h
+++ b/common/beerocks/bwl/nl80211/base_wlan_hal_nl80211.h
@@ -34,6 +34,9 @@ enum class nl80211_fsm_event { Attach, D
constexpr char global_iface[] = "global";
constexpr char base_ctrl_path[] = "/var/run/";
+// Validate the canonical colon-separated MAC format returned by hostapd.
+bool looks_like_mac(const std::string &text);
+
/*!
* Base class for the wav abstraction layer.
* Read more about virtual inheritance: https://en.wikipedia.org/wiki/Virtual_inheritance
--- a/common/beerocks/bwl/nl80211/mon_wlan_hal_nl80211.cpp
+++ b/common/beerocks/bwl/nl80211/mon_wlan_hal_nl80211.cpp
@@ -26,6 +26,7 @@ extern "C" {
#include
#include
#include
+#include
namespace bwl {
namespace nl80211 {
@@ -457,24 +458,128 @@ bool mon_wlan_hal_nl80211::channel_scan_
bool mon_wlan_hal_nl80211::generate_connected_clients_events(
bool &is_finished_all_clients, std::chrono::steady_clock::time_point max_iteration_timeout)
{
- LOG(TRACE) << __func__ << " - NOT IMPLEMENTED";
- is_finished_all_clients = true;
+ auto next_unhandled_vap = [this]() {
+ for (const auto &vap : m_radio_info.available_vaps) {
+ if (m_completed_vaps.find(vap.first) == m_completed_vaps.end()) {
+ return vap.first;
+ }
+ }
+ return INVALID_VAP_ID;
+ };
+
+ is_finished_all_clients = false;
+
+ if (m_vap_id_in_progress == INVALID_VAP_ID) {
+ m_vap_id_in_progress = next_unhandled_vap();
+ m_vap_restart_count = 0;
+ }
+
+ while (m_vap_id_in_progress != INVALID_VAP_ID) {
+ auto vap_it = m_radio_info.available_vaps.find(m_vap_id_in_progress);
+ if (vap_it == m_radio_info.available_vaps.end() || vap_it->second.bss.empty()) {
+ m_completed_vaps.insert(m_vap_id_in_progress);
+ m_vap_id_in_progress = next_unhandled_vap();
+ continue;
+ }
+ // On stock OpenWrt the BSS name is the real netdev the control
+ // socket is registered under; wlanX.Y spellings do not exist here.
+ auto vap_iface = vap_it->second.bss;
+
+ bool vap_done = false;
+ while (!vap_done) {
+ if (std::chrono::steady_clock::now() > max_iteration_timeout) {
+ // Out of time budget: resume from the same cursor next wakeup.
+ return true;
+ }
+
+ std::string cmd = m_queried_first ? "STA-NEXT " + tlvf::mac_to_string(m_prev_client_mac)
+ : "STA-FIRST";
+ char placeholder = 0;
+ char *reply = &placeholder;
+ if (!wpa_ctrl_send_msg(cmd, &reply, vap_iface) || !reply) {
+ LOG(WARNING) << __func__ << ": '" << cmd << "' failed on " << vap_iface;
+ if (m_queried_first && m_vap_restart_count++ < 2) {
+ m_queried_first = false;
+ m_prev_client_mac = beerocks::net::network_utils::ZERO_MAC;
+ return true;
+ }
+ vap_done = true;
+ break;
+ }
+
+ std::string text(reply);
+ if (text.empty() || text.rfind("FAIL", 0) == 0) {
+ if (!text.empty() && m_queried_first && m_vap_restart_count++ < 2) {
+ m_queried_first = false;
+ m_prev_client_mac = beerocks::net::network_utils::ZERO_MAC;
+ continue;
+ }
+ vap_done = true;
+ break;
+ }
+ if (text.rfind("UNKNOWN", 0) == 0) {
+ LOG(WARNING) << "Station enumeration is unsupported on " << vap_iface;
+ vap_done = true;
+ break;
+ }
- // TODO: implement the API (PPM-1152)
- // currently returning true even though not implemented in order not to break
- // the flow if this HAL is used by any flow, since the API return value is checked by
- // a common flow in the monitor.
+ std::istringstream stream(text);
+ std::string first_line;
+ std::getline(stream, first_line);
+ if (!first_line.empty() && first_line.back() == '\r') {
+ first_line.pop_back();
+ }
+ if (!looks_like_mac(first_line)) {
+ LOG(WARNING) << "Malformed station block on " << vap_iface
+ << ", skipping the rest of this VAP";
+ vap_done = true;
+ break;
+ }
+
+ m_queried_first = true;
+ m_prev_client_mac = tlvf::mac_from_string(first_line);
+
+ if (!m_handled_clients.insert(m_prev_client_mac).second) {
+ continue; // seen earlier in this sweep
+ }
+ if (text.find("connected_time=") == std::string::npos) {
+ continue; // authenticated but not associated
+ }
+
+ auto msg_buff =
+ ALLOC_SMART_BUFFER(sizeof(sACTION_MONITOR_CLIENT_ASSOCIATED_NOTIFICATION));
+ auto msg =
+ reinterpret_cast(msg_buff.get());
+ if (!msg) {
+ LOG(FATAL) << "Memory allocation failed";
+ return false;
+ }
+ memset(msg_buff.get(), 0, sizeof(sACTION_MONITOR_CLIENT_ASSOCIATED_NOTIFICATION));
+ msg->vap_id = m_vap_id_in_progress;
+ msg->mac = m_prev_client_mac;
+ event_queue_push(Event::STA_Connected, msg_buff);
+ }
+
+ m_completed_vaps.insert(m_vap_id_in_progress);
+ m_queried_first = false;
+ m_prev_client_mac = beerocks::net::network_utils::ZERO_MAC;
+ m_vap_id_in_progress = next_unhandled_vap();
+ m_vap_restart_count = 0;
+ }
+
+ m_handled_clients.clear();
+ is_finished_all_clients = true;
return true;
}
bool mon_wlan_hal_nl80211::pre_generate_connected_clients_events()
{
- LOG(TRACE) << __func__ << " - NOT IMPLEMENTED";
-
- // TODO: implement the API (PPM-1152)
- // currently returning true even though not implemented in order not to break
- // the flow if this HAL is used by any flow, since the API return value is checked by
- // a common flow in the monitor.
+ m_vap_id_in_progress = INVALID_VAP_ID;
+ m_prev_client_mac = beerocks::net::network_utils::ZERO_MAC;
+ m_queried_first = false;
+ m_vap_restart_count = 0;
+ m_completed_vaps.clear();
+ m_handled_clients.clear();
return true;
}
--- a/common/beerocks/bwl/nl80211/mon_wlan_hal_nl80211.h
+++ b/common/beerocks/bwl/nl80211/mon_wlan_hal_nl80211.h
@@ -12,6 +12,9 @@
#include "base_wlan_hal_nl80211.h"
#include
+#include
+#include
+
namespace bwl {
namespace nl80211 {
@@ -88,6 +91,15 @@ protected:
// Private data-members:
private:
+ // Connected-clients import (STA-FIRST/STA-NEXT sweep)
+ static constexpr int INVALID_VAP_ID = -1;
+ std::set m_completed_vaps;
+ std::unordered_set m_handled_clients;
+ sMacAddr m_prev_client_mac = {};
+ bool m_queried_first = false;
+ int m_vap_id_in_progress = INVALID_VAP_ID;
+ int m_vap_restart_count = 0;
+
std::shared_ptr m_temp_wav_value;
};