mirror of
https://github.com/Telecominfraproject/wlan-cloud-rrm.git
synced 2025-11-01 19:17:53 +00:00
Aggregate wifiscans (#25)
This commit is contained in:
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.facebook.openwifirrm.aggregators;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aggregates added values into one "aggregate" measure.
|
||||||
|
*
|
||||||
|
* @param <T> the type of values being aggregated (e.g., Double).
|
||||||
|
*/
|
||||||
|
public interface Aggregator<T> {
|
||||||
|
/** Adds {@value} to the group of values being aggregated. */
|
||||||
|
void addValue(T value);
|
||||||
|
|
||||||
|
/** Returns the aggregate measure of all added values. */
|
||||||
|
T getAggregate();
|
||||||
|
|
||||||
|
/** Returns the number of values that are aggregated. */
|
||||||
|
long getCount();
|
||||||
|
|
||||||
|
/** Remove all added values from the group of values being aggregated. */
|
||||||
|
void reset();
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.facebook.openwifirrm.aggregators;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks the mean of all added values. If no values are added, the mean is 0.
|
||||||
|
*/
|
||||||
|
public class MeanAggregator implements Aggregator<Double> {
|
||||||
|
|
||||||
|
protected double mean = 0;
|
||||||
|
protected long count = 0;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addValue(Double value) {
|
||||||
|
mean = ((double) count / (count + 1)) * mean + (value / (count + 1));
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Double getAggregate() {
|
||||||
|
return mean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getCount() {
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset() {
|
||||||
|
mean = 0;
|
||||||
|
count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -397,8 +397,8 @@ public class DataCollector implements Runnable {
|
|||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
List<WifiScanEntry> scanEntries =
|
// wifiScanResult.executed is in seconds
|
||||||
UCentralUtils.parseWifiScanEntries(wifiScanResult.results);
|
List<WifiScanEntry> scanEntries = UCentralUtils.parseWifiScanEntries(wifiScanResult.results, 1000 * wifiScanResult.executed);
|
||||||
if (scanEntries == null) {
|
if (scanEntries == null) {
|
||||||
logger.error(
|
logger.error(
|
||||||
"Device {}: wifi scan returned unexpected result", serialNumber
|
"Device {}: wifi scan returned unexpected result", serialNumber
|
||||||
@@ -429,7 +429,7 @@ public class DataCollector implements Runnable {
|
|||||||
|
|
||||||
/** Insert wifi scan results into database. */
|
/** Insert wifi scan results into database. */
|
||||||
private void insertWifiScanResultsToDatabase(
|
private void insertWifiScanResultsToDatabase(
|
||||||
String serialNumber, long ts, List<WifiScanEntry> entries
|
String serialNumber, long timestampSeconds, List<WifiScanEntry> entries
|
||||||
) {
|
) {
|
||||||
if (dbManager == null) {
|
if (dbManager == null) {
|
||||||
return;
|
return;
|
||||||
@@ -437,7 +437,7 @@ public class DataCollector implements Runnable {
|
|||||||
|
|
||||||
// Insert into database
|
// Insert into database
|
||||||
try {
|
try {
|
||||||
dbManager.addWifiScan(serialNumber, ts, entries);
|
dbManager.addWifiScan(serialNumber, timestampSeconds, entries);
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
logger.error("Failed to insert wifi scan results into database", e);
|
logger.error("Failed to insert wifi scan results into database", e);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -83,7 +83,13 @@ public class Modeler implements Runnable {
|
|||||||
// At minimum, we may want to aggregate recent wifi scan responses and
|
// At minimum, we may want to aggregate recent wifi scan responses and
|
||||||
// keep a rolling average for stats.
|
// keep a rolling average for stats.
|
||||||
|
|
||||||
/** List of latest wifi scan results per device. */
|
/**
|
||||||
|
* The "result" of a wifiscan can include multiple responses. This maps from an
|
||||||
|
* AP (serial number) to a list of most recent wifiscan "results" where each
|
||||||
|
* "result" itself is a list of responses from other APs.
|
||||||
|
*
|
||||||
|
* @see UCentralClient#wifiScan(String, boolean)
|
||||||
|
*/
|
||||||
public Map<String, List<List<WifiScanEntry>>> latestWifiScans =
|
public Map<String, List<List<WifiScanEntry>>> latestWifiScans =
|
||||||
new ConcurrentHashMap<>();
|
new ConcurrentHashMap<>();
|
||||||
|
|
||||||
@@ -309,8 +315,7 @@ public class Modeler implements Runnable {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Parse and validate this record
|
// Parse and validate this record
|
||||||
List<WifiScanEntry> scanEntries =
|
List<WifiScanEntry> scanEntries = UCentralUtils.parseWifiScanEntries(record.payload, record.timestampMs);
|
||||||
UCentralUtils.parseWifiScanEntries(record.payload);
|
|
||||||
if (scanEntries == null) {
|
if (scanEntries == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,21 @@
|
|||||||
|
|
||||||
package com.facebook.openwifirrm.modules;
|
package com.facebook.openwifirrm.modules;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import com.facebook.openwifirrm.aggregators.Aggregator;
|
||||||
|
import com.facebook.openwifirrm.ucentral.UCentralUtils.WifiScanEntry;
|
||||||
|
import com.facebook.openwifirrm.ucentral.operationelement.HTOperationElement;
|
||||||
|
import com.facebook.openwifirrm.ucentral.operationelement.VHTOperationElement;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modeler utilities.
|
* Modeler utilities.
|
||||||
*/
|
*/
|
||||||
@@ -202,4 +212,109 @@ public class ModelerUtils {
|
|||||||
return Double.POSITIVE_INFINITY;
|
return Double.POSITIVE_INFINITY;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if two wifiscan entries should be aggregated without consideration
|
||||||
|
* for any kind of obsoletion period. This should handle APs that support
|
||||||
|
* pre-802.11n standards (no ht_oper and no vht_oper), 802.11n (supports ht_oper
|
||||||
|
* but no vht_oper), and 802.11ac (supports both ht_oper and vht_oper).
|
||||||
|
* Pre-802.11n, if two entries with the same bssid are in the same channel (and
|
||||||
|
* therefore frequency, since they share a one-to-one mapping), that is enough
|
||||||
|
* to aggregate them. However, 802.11n introduced channel bonding, so for
|
||||||
|
* 802.11n onwards, channel width must be checked.
|
||||||
|
*
|
||||||
|
* @return true if the entries should be aggregated
|
||||||
|
*/
|
||||||
|
private static boolean matchesForAggregation(WifiScanEntry entry1, WifiScanEntry entry2) {
|
||||||
|
// TODO test on real pre-802.11n APs (which do not have ht_oper and vht_oper)
|
||||||
|
// do not check SSID (other SSIDs can contribute to interference, and SSIDs can
|
||||||
|
// change any time)
|
||||||
|
return Objects.equals(entry1.bssid, entry2.bssid) && entry1.frequency == entry2.frequency
|
||||||
|
&& entry1.channel == entry2.channel
|
||||||
|
&& HTOperationElement.matchesHtForAggregation(entry1.ht_oper, entry2.ht_oper)
|
||||||
|
&& VHTOperationElement.matchesVhtForAggregation(entry1.vht_oper, entry2.vht_oper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For each AP, for each other AP that sent a wifiscan entry to that AP, this
|
||||||
|
* method calculates an aggregate wifiscan entry with an aggregated RSSI.
|
||||||
|
*
|
||||||
|
* @param dataModel the data model which includes the latest wifiscan
|
||||||
|
* entries
|
||||||
|
* @param obsoletionPeriodMs for each (scanning AP, responding AP) tuple, the
|
||||||
|
* maximum amount of time (in milliseconds) it is
|
||||||
|
* worth aggregating over, starting from the most
|
||||||
|
* recent scan entry for that tuple, and working
|
||||||
|
* backwards in time. An entry exactly
|
||||||
|
* {@code obsoletionPeriodMs} ms earlier than the most
|
||||||
|
* recent entry is considered non-obsolete (i.e., the
|
||||||
|
* "non-obsolete" window is inclusive). Must be
|
||||||
|
* non-negative.
|
||||||
|
* @param agg an aggregator to calculate the aggregated RSSI
|
||||||
|
* given recent wifiscan entries' RSSIs.
|
||||||
|
* @return a map from AP serial number to a map from BSSID to an "aggregated
|
||||||
|
* wifiscan entry". This aggregated entry is the most recent entry with
|
||||||
|
* its {@code signal} attribute modified to be the aggregated signal
|
||||||
|
* value instead of the value in just the most recent entry for that (AP
|
||||||
|
* serial number, BSSID) tuple. The returned map will only contain APs
|
||||||
|
* which received at least one non-obsolete wifiscan entry from a BSS.
|
||||||
|
*/
|
||||||
|
public static Map<String, Map<String, WifiScanEntry>> getAggregatedWifiScans(Modeler.DataModel dataModel,
|
||||||
|
long obsoletionPeriodMs,
|
||||||
|
Aggregator<Double> agg) {
|
||||||
|
if (obsoletionPeriodMs < 0) {
|
||||||
|
throw new IllegalArgumentException("obsoletionPeriodMs must be non-negative.");
|
||||||
|
}
|
||||||
|
Map<String, Map<String, WifiScanEntry>> aggregatedWifiScans = new HashMap<>();
|
||||||
|
for (Map.Entry<String, List<List<WifiScanEntry>>> apToScansMapEntry : dataModel.latestWifiScans.entrySet()) {
|
||||||
|
String serialNumber = apToScansMapEntry.getKey();
|
||||||
|
List<List<WifiScanEntry>> scans = apToScansMapEntry.getValue();
|
||||||
|
if (scans.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* Flatten the wifiscan entries and sort in reverse chronological order. Sorting
|
||||||
|
* is done just in case the entries in the original list are not chronological
|
||||||
|
* already - although they are inserted chronologically, perhaps latency,
|
||||||
|
* synchronization, etc. could cause the actual unixTimeMs to be out-of-order.
|
||||||
|
*/
|
||||||
|
List<WifiScanEntry> mostRecentToOldestEntries = scans.stream().flatMap(List::stream)
|
||||||
|
.sorted((entry1, entry2) -> {
|
||||||
|
return -Long.compare(entry1.unixTimeMs, entry2.unixTimeMs);
|
||||||
|
}).collect(Collectors.toUnmodifiableList());
|
||||||
|
// Split mostRecentToOldest into separate lists for each BSSID
|
||||||
|
// These lists will be in reverse chronological order also
|
||||||
|
Map<String, List<WifiScanEntry>> bssidToEntriesMap = new HashMap<>();
|
||||||
|
for (WifiScanEntry entry : mostRecentToOldestEntries) {
|
||||||
|
if (entry.bssid == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
bssidToEntriesMap.computeIfAbsent(entry.bssid, bssid -> new ArrayList<>()).add(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate the aggregate for each bssid for this AP
|
||||||
|
for (Map.Entry<String, List<WifiScanEntry>> bssidToWifiScanEntriesMapEntry : bssidToEntriesMap.entrySet()) {
|
||||||
|
String bssid = bssidToWifiScanEntriesMapEntry.getKey();
|
||||||
|
List<WifiScanEntry> entries = bssidToWifiScanEntriesMapEntry.getValue();
|
||||||
|
WifiScanEntry mostRecentEntry = entries.get(0);
|
||||||
|
agg.reset();
|
||||||
|
for (WifiScanEntry entry : entries) {
|
||||||
|
if (mostRecentEntry.unixTimeMs - entry.unixTimeMs > obsoletionPeriodMs) {
|
||||||
|
// discard obsolete entries
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (mostRecentEntry == entry || matchesForAggregation(mostRecentEntry, entry)) {
|
||||||
|
aggregatedWifiScans
|
||||||
|
.computeIfAbsent(serialNumber, k -> new HashMap<>())
|
||||||
|
.computeIfAbsent(bssid, k -> new WifiScanEntry(entry));
|
||||||
|
agg.addValue((double) entry.signal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (agg.getCount() > 0) {
|
||||||
|
aggregatedWifiScans.get(serialNumber).get(bssid).signal = (int) Math.round(agg.getAggregate());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return aggregatedWifiScans;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -429,9 +429,16 @@ public class DatabaseManager {
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Insert wifi scan results into the database. */
|
/**
|
||||||
|
* Insert wifi scan results into the database.
|
||||||
|
*
|
||||||
|
* @param serialNumber serial number
|
||||||
|
* @param timestampSeconds timestamp (Unix time in seconds).
|
||||||
|
* @param entries list of wifiscan entries
|
||||||
|
* @throws SQLException
|
||||||
|
*/
|
||||||
public void addWifiScan(
|
public void addWifiScan(
|
||||||
String serialNumber, long ts, List<WifiScanEntry> entries
|
String serialNumber, long timestampSeconds, List<WifiScanEntry> entries
|
||||||
) throws SQLException {
|
) throws SQLException {
|
||||||
if (ds == null) {
|
if (ds == null) {
|
||||||
return;
|
return;
|
||||||
@@ -444,7 +451,7 @@ public class DatabaseManager {
|
|||||||
"INSERT INTO `wifiscan` (`time`, `serial`) VALUES (?, ?)",
|
"INSERT INTO `wifiscan` (`time`, `serial`) VALUES (?, ?)",
|
||||||
Statement.RETURN_GENERATED_KEYS
|
Statement.RETURN_GENERATED_KEYS
|
||||||
);
|
);
|
||||||
stmt.setTimestamp(1, new Timestamp(ts * 1000));
|
stmt.setTimestamp(1, new Timestamp(timestampSeconds * 1000));
|
||||||
stmt.setString(2, serialNumber);
|
stmt.setString(2, serialNumber);
|
||||||
int rows = stmt.executeUpdate();
|
int rows = stmt.executeUpdate();
|
||||||
if (rows == 0) {
|
if (rows == 0) {
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ public abstract class ChannelOptimizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** List of available channels per band for use. */
|
/** List of available channels per band for use. */
|
||||||
protected static final Map<String, List<Integer>> AVAILABLE_CHANNELS_BAND =
|
public static final Map<String, List<Integer>> AVAILABLE_CHANNELS_BAND =
|
||||||
new HashMap<>();
|
new HashMap<>();
|
||||||
static {
|
static {
|
||||||
AVAILABLE_CHANNELS_BAND.put(
|
AVAILABLE_CHANNELS_BAND.put(
|
||||||
|
|||||||
@@ -419,7 +419,21 @@ public class UCentralClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Launch a wifi scan for a device (by serial number). */
|
/**
|
||||||
|
* Launch a wifi scan for a device (by serial number).
|
||||||
|
* <p>
|
||||||
|
* An AP can conduct a wifiscan, which can be either active or passive. In an
|
||||||
|
* active wifiscan, the AP sends out a wifiscan request and listens for
|
||||||
|
* responses from other APs. In a passive wifiscan, the AP does not send out a
|
||||||
|
* wifiscan request but instead just waits for periodic beacons from the other
|
||||||
|
* APs. (Note that neither the responses to requests (in active mode) or the
|
||||||
|
* periodic beacons are guaranteed to happen at any particular time (and it
|
||||||
|
* depends on network traffic)).
|
||||||
|
* <p>
|
||||||
|
* The AP conducting the wifiscan goes through every channel and listens for
|
||||||
|
* responses/beacons. However, the responding/beaconing APs only send responses
|
||||||
|
* on channels they are currently using.
|
||||||
|
*/
|
||||||
public CommandInfo wifiScan(String serialNumber, boolean verbose) {
|
public CommandInfo wifiScan(String serialNumber, boolean verbose) {
|
||||||
WifiScanRequest req = new WifiScanRequest();
|
WifiScanRequest req = new WifiScanRequest();
|
||||||
req.serialNumber = serialNumber;
|
req.serialNumber = serialNumber;
|
||||||
|
|||||||
@@ -68,10 +68,14 @@ public class UCentralKafkaConsumer {
|
|||||||
/** The state payload JSON. */
|
/** The state payload JSON. */
|
||||||
public final JsonObject payload;
|
public final JsonObject payload;
|
||||||
|
|
||||||
|
/** Unix time (ms). */
|
||||||
|
public final long timestampMs;
|
||||||
|
|
||||||
/** Constructor. */
|
/** Constructor. */
|
||||||
public KafkaRecord(String serialNumber, JsonObject payload) {
|
public KafkaRecord(String serialNumber, JsonObject payload, long timestampMs) {
|
||||||
this.serialNumber = serialNumber;
|
this.serialNumber = serialNumber;
|
||||||
this.payload = payload;
|
this.payload = payload;
|
||||||
|
this.timestampMs = timestampMs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,11 +249,12 @@ public class UCentralKafkaConsumer {
|
|||||||
"Offset {}: {} => {}",
|
"Offset {}: {} => {}",
|
||||||
record.offset(), serialNumber, payload.toString()
|
record.offset(), serialNumber, payload.toString()
|
||||||
);
|
);
|
||||||
|
// record.timestamp() is empirically confirmed to be Unix time (ms)
|
||||||
|
KafkaRecord kafkaRecord = new KafkaRecord(serialNumber, payload, record.timestamp());
|
||||||
if (record.topic().equals(stateTopic)) {
|
if (record.topic().equals(stateTopic)) {
|
||||||
stateRecords.add(new KafkaRecord(serialNumber, payload));
|
stateRecords.add(kafkaRecord);
|
||||||
} else if (record.topic().equals(wifiScanTopic)) {
|
} else if (record.topic().equals(wifiScanTopic)) {
|
||||||
wifiScanRecords.add(new KafkaRecord(serialNumber, payload));
|
wifiScanRecords.add(kafkaRecord);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import java.util.HashSet;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -23,7 +24,9 @@ import org.slf4j.LoggerFactory;
|
|||||||
|
|
||||||
import com.facebook.openwifirrm.RRMConfig;
|
import com.facebook.openwifirrm.RRMConfig;
|
||||||
import com.facebook.openwifirrm.Utils;
|
import com.facebook.openwifirrm.Utils;
|
||||||
|
import com.facebook.openwifirrm.optimizers.ChannelOptimizer;
|
||||||
import com.facebook.openwifirrm.ucentral.models.State;
|
import com.facebook.openwifirrm.ucentral.models.State;
|
||||||
|
import com.facebook.openwifirrm.ucentral.models.WifiScanEntryResult;
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.JsonArray;
|
import com.google.gson.JsonArray;
|
||||||
import com.google.gson.JsonElement;
|
import com.google.gson.JsonElement;
|
||||||
@@ -41,78 +44,74 @@ public class UCentralUtils {
|
|||||||
// This class should not be instantiated.
|
// This class should not be instantiated.
|
||||||
private UCentralUtils() {}
|
private UCentralUtils() {}
|
||||||
|
|
||||||
/** Represents a single entry in wifi scan results. */
|
/**
|
||||||
public static class WifiScanEntry {
|
* Extends {@link WifiScanEntryResult} to track the response time of the entry.
|
||||||
public int channel;
|
*/
|
||||||
public long last_seen;
|
public static class WifiScanEntry extends WifiScanEntryResult {
|
||||||
/** Signal strength measured in dBm */
|
|
||||||
public int signal;
|
|
||||||
/** BSSID is the MAC address of the device */
|
|
||||||
public String bssid;
|
|
||||||
public String ssid;
|
|
||||||
public long tsf;
|
|
||||||
/**
|
/**
|
||||||
* ht_oper is short for "high throughput operator". This field contains some
|
* Unix time in milliseconds (ms). This field is not defined in the uCentral
|
||||||
* information already present in other fields. This is because this field was
|
* API. This is added it because {@link WifiScanEntryResult#tsf} is an unknown
|
||||||
* added later in order to capture some new information but also includes some
|
* time reference.
|
||||||
* redundant information. 802.11 defines the HT operator and vendors may define
|
|
||||||
* additional fields. HT is supported on both the 2.4 GHz and 5 GHz bands.
|
|
||||||
*
|
|
||||||
* This field is specified as 24 bytes, but it is encoded in base64. It is
|
|
||||||
* likely the case that the first byte (the Element ID, which should be 61 for
|
|
||||||
* ht_oper) and the second byte (Length) are omitted in the wifi scan results,
|
|
||||||
* resulting in 22 bytes, which translates to a 32 byte base64 encoded String.
|
|
||||||
*/
|
*/
|
||||||
public String ht_oper;
|
public long unixTimeMs;
|
||||||
/**
|
|
||||||
* vht_oper is short for "very high throughput operator". This field contains
|
|
||||||
* some information already present in other fields. This is because this field
|
|
||||||
* was added later in order to capture some new information but also includes
|
|
||||||
* some redundant information. 802.11 defines the VHT operator and vendors may
|
|
||||||
* define additional fields. VHT is supported only on the 5 GHz band.
|
|
||||||
*
|
|
||||||
* For information about about the contents of this field, its encoding, etc.,
|
|
||||||
* please see the javadoc for {@link #ht_oper} first. The vht_oper likely
|
|
||||||
* operates similarly.
|
|
||||||
*/
|
|
||||||
public String vht_oper;
|
|
||||||
public int capability;
|
|
||||||
public int frequency;
|
|
||||||
/** IE = information element */
|
|
||||||
public JsonArray ies;
|
|
||||||
|
|
||||||
/** Default Constructor. */
|
/** Default Constructor. */
|
||||||
public WifiScanEntry() {}
|
public WifiScanEntry() {}
|
||||||
|
|
||||||
/** Copy Constructor. */
|
/** Copy Constructor. */
|
||||||
public WifiScanEntry(WifiScanEntry o) {
|
public WifiScanEntry(WifiScanEntry o) {
|
||||||
this.channel = o.channel;
|
super(o);
|
||||||
this.last_seen = o.last_seen;
|
this.unixTimeMs = o.unixTimeMs;
|
||||||
this.signal = o.signal;
|
}
|
||||||
this.bssid = o.bssid;
|
|
||||||
this.ssid = o.ssid;
|
@Override
|
||||||
this.tsf = o.tsf;
|
public int hashCode() {
|
||||||
this.ht_oper = o.ht_oper;
|
final int prime = 31;
|
||||||
this.vht_oper = o.vht_oper;
|
int result = super.hashCode();
|
||||||
this.capability = o.capability;
|
result = prime * result + Objects.hash(unixTimeMs);
|
||||||
this.frequency = o.frequency;
|
return result;
|
||||||
this.ies = o.ies;
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!super.equals(obj)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (getClass() != obj.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
WifiScanEntry other = (WifiScanEntry) obj;
|
||||||
|
return unixTimeMs == other.unixTimeMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format("WifiScanEntry[signal=%d, bssid=%s, unixTimeMs=%d]", signal, bssid, unixTimeMs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a JSON wifi scan result into a list of WifiScanEntry objects.
|
* Parse a JSON wifi scan result into a list of WifiScanEntry objects.
|
||||||
*
|
*
|
||||||
* Returns null if any parsing/deserialization error occurred.
|
* @param result result of the wifiscan
|
||||||
|
* @param timestampMs Unix time in ms
|
||||||
|
* @return list of wifiscan entries, or null if any parsing/deserialization
|
||||||
|
* error occurred.
|
||||||
*/
|
*/
|
||||||
public static List<WifiScanEntry> parseWifiScanEntries(JsonObject result) {
|
public static List<WifiScanEntry> parseWifiScanEntries(JsonObject result, long timestampMs) {
|
||||||
List<WifiScanEntry> entries = new ArrayList<>();
|
List<WifiScanEntry> entries = new ArrayList<>();
|
||||||
try {
|
try {
|
||||||
JsonArray scanInfo = result
|
JsonArray scanInfo = result
|
||||||
.getAsJsonObject("status")
|
.getAsJsonObject("status")
|
||||||
.getAsJsonArray("scan");
|
.getAsJsonArray("scan");
|
||||||
for (JsonElement e : scanInfo) {
|
for (JsonElement e : scanInfo) {
|
||||||
entries.add(gson.fromJson(e, WifiScanEntry.class));
|
WifiScanEntry entry = gson.fromJson(e, WifiScanEntry.class);
|
||||||
|
entry.unixTimeMs = timestampMs;
|
||||||
|
entries.add(entry);
|
||||||
|
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return null;
|
return null;
|
||||||
@@ -347,4 +346,22 @@ public class UCentralUtils {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts channel number to that channel's center frequency in MHz.
|
||||||
|
*
|
||||||
|
* @param channel channel number. See
|
||||||
|
* {@link ChannelOptimizer#AVAILABLE_CHANNELS_BAND} for channels
|
||||||
|
* in each band.
|
||||||
|
* @return the center frequency of the given channel in MHz
|
||||||
|
*/
|
||||||
|
public static int channelToFrequencyMHz(int channel) {
|
||||||
|
if (ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(UCentralConstants.BAND_2G).contains(channel)) {
|
||||||
|
return 2407 + 5 * channel;
|
||||||
|
} else if (ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(UCentralConstants.BAND_5G).contains(channel)) {
|
||||||
|
return 5000 + channel;
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Must provide a valid channel.");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.facebook.openwifirrm.ucentral.models;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
|
||||||
|
/** Represents a single entry in wifi scan results. */
|
||||||
|
public class WifiScanEntryResult {
|
||||||
|
public int channel;
|
||||||
|
public long last_seen;
|
||||||
|
/** Signal strength measured in dBm */
|
||||||
|
public int signal;
|
||||||
|
/** BSSID is the MAC address of the device */
|
||||||
|
public String bssid;
|
||||||
|
public String ssid;
|
||||||
|
public long tsf;
|
||||||
|
/**
|
||||||
|
* ht_oper is short for "high throughput operation element". Note that this
|
||||||
|
* field, when non-null, contains some information already present in other
|
||||||
|
* fields. This field may be null, however, since pre-802.11n BSSs do not
|
||||||
|
* support HT. For BSSs that do support HT, HT is supported on the 2G and 5G
|
||||||
|
* bands.
|
||||||
|
*
|
||||||
|
* This field is specified as 24 bytes, but it is encoded in base64. It is
|
||||||
|
* likely the case that the first byte (the Element ID, which should be 61 for
|
||||||
|
* ht_oper) and the second byte (Length) are omitted in the wifi scan results,
|
||||||
|
* resulting in 22 bytes, which translates to a 32 byte base64 encoded String.
|
||||||
|
*/
|
||||||
|
public String ht_oper;
|
||||||
|
/**
|
||||||
|
* vht_oper is short for "very high throughput operation element". Note that
|
||||||
|
* this field, when non-null, contains some information already present in other
|
||||||
|
* fields. This field may be null, however, since pre-802.11ac BSSs do not
|
||||||
|
* support HT. For BSSs that do support VHT, VHT is supported on the 5G band.
|
||||||
|
* VHT operation is controlled by both the HT operation element and the VHT
|
||||||
|
* operation element.
|
||||||
|
*
|
||||||
|
* For information about about the contents of this field, its encoding, etc.,
|
||||||
|
* please see the javadoc for {@link ht_oper} first. The vht_oper likely
|
||||||
|
* operates similarly.
|
||||||
|
*/
|
||||||
|
public String vht_oper;
|
||||||
|
public int capability;
|
||||||
|
public int frequency;
|
||||||
|
/** IE = information element */
|
||||||
|
public JsonArray ies;
|
||||||
|
|
||||||
|
/** Default Constructor. */
|
||||||
|
public WifiScanEntryResult() {}
|
||||||
|
|
||||||
|
/** Copy Constructor. */
|
||||||
|
public WifiScanEntryResult(WifiScanEntryResult o) {
|
||||||
|
this.channel = o.channel;
|
||||||
|
this.last_seen = o.last_seen;
|
||||||
|
this.signal = o.signal;
|
||||||
|
this.bssid = o.bssid;
|
||||||
|
this.ssid = o.ssid;
|
||||||
|
this.tsf = o.tsf;
|
||||||
|
this.ht_oper = o.ht_oper;
|
||||||
|
this.vht_oper = o.vht_oper;
|
||||||
|
this.capability = o.capability;
|
||||||
|
this.frequency = o.frequency;
|
||||||
|
this.ies = o.ies;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(bssid, capability, channel, frequency, ht_oper, ies, last_seen, signal, ssid, tsf,
|
||||||
|
vht_oper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (getClass() != obj.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
WifiScanEntryResult other = (WifiScanEntryResult) obj;
|
||||||
|
return Objects.equals(bssid, other.bssid) && capability == other.capability && channel == other.channel
|
||||||
|
&& frequency == other.frequency && Objects.equals(ht_oper, other.ht_oper)
|
||||||
|
&& Objects.equals(ies, other.ies) && last_seen == other.last_seen && signal == other.signal
|
||||||
|
&& Objects.equals(ssid, other.ssid) && tsf == other.tsf && Objects.equals(vht_oper, other.vht_oper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format("WifiScanEntryResult[signal=%d, bssid=%s]", signal, bssid);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.facebook.openwifirrm.ucentral.operationelement;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import org.apache.commons.codec.binary.Base64;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* High Throughput (HT) Operation Element, which is potentially present in
|
||||||
|
* wifiscan entries. Introduced in 802.11n (2009).
|
||||||
|
*/
|
||||||
|
public class HTOperationElement {
|
||||||
|
|
||||||
|
/** Channel number of the primary channel. */
|
||||||
|
private final byte primaryChannel;
|
||||||
|
/**
|
||||||
|
* Indicates the offset of the secondary channel relative to the primary
|
||||||
|
* channel. A 1 indicates that the secondary channel is above the primary
|
||||||
|
* channel. A 3 indicates that the secondary channel is below the primary
|
||||||
|
* channel. A 0 indicates that there is no secondary channel present. The value
|
||||||
|
* 2 is reserved.
|
||||||
|
*/
|
||||||
|
private final byte secondaryChannelOffset;
|
||||||
|
/**
|
||||||
|
* Defines the channel widths that can be used to transmit to the STA. With
|
||||||
|
* exceptions, false allows a 20 MHz channel width. True allows use of any
|
||||||
|
* channel width in the supported channel width set. See 802.11 for exceptions.
|
||||||
|
*/
|
||||||
|
private final boolean staChannelWidth;
|
||||||
|
/** True if RIFS is permitted; false otherwise. */
|
||||||
|
private final boolean rifsMode;
|
||||||
|
/**
|
||||||
|
* A 0 indicates no protection mode. A 1 indicates nonmember protection mode. A
|
||||||
|
* 2 indicates 20 MHz protection mode. A 3 indicates non-HT mixed mode.
|
||||||
|
*/
|
||||||
|
private final byte htProtection;
|
||||||
|
/**
|
||||||
|
* False if all HT STAs that are associated are HT-greenfield capable or all HT
|
||||||
|
* peer mesh STAs are HT-greenfield capable; true otherwise.
|
||||||
|
*/
|
||||||
|
private final boolean nongreenfieldHtStasPresent;
|
||||||
|
/**
|
||||||
|
* Indicates if the use of protection for non-HT STAs by overlapping BSSs is
|
||||||
|
* determined to be desirable. See 802.11 for details.
|
||||||
|
*/
|
||||||
|
private final boolean obssNonHtStasPresent;
|
||||||
|
/**
|
||||||
|
* Defines the channel center frequency for a 160 or 80+80 MHz BSS bandwidth
|
||||||
|
* with NSS support less than Max VHT NSS. This is 0 for non-VHT STAs. See
|
||||||
|
* 802.11 for details.
|
||||||
|
*/
|
||||||
|
private final byte channelCenterFrequencySegment2;
|
||||||
|
/** False if no STBC beacon is transmitted; true otherwise. */
|
||||||
|
private final boolean dualBeacon;
|
||||||
|
/** False if dual CTS protection is not required; true otherwise. */
|
||||||
|
private final boolean dualCtsProtection;
|
||||||
|
/** False in a primary beacon. True in an STBC beacon. */
|
||||||
|
private final boolean stbcBeacon;
|
||||||
|
/**
|
||||||
|
* Indicates the HT-MCS values that are supported by all HT STAs in the BSS. A
|
||||||
|
* bitmap where a bit is set to 1 to indicate support for that MCS and 0
|
||||||
|
* otherwise, where bit 0 corresponds to MCS 0.
|
||||||
|
*/
|
||||||
|
private final byte[] basicHtMcsSet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an {@code HTOperationElement} using the given field values. See
|
||||||
|
* 802.11 for more details.
|
||||||
|
* <p>
|
||||||
|
* For details about the parameters, see the javadocs for the corresponding
|
||||||
|
* member variables.
|
||||||
|
*/
|
||||||
|
public HTOperationElement(byte primaryChannel, byte secondaryChannelOffset, boolean staChannelWidth,
|
||||||
|
boolean rifsMode, byte htProtection, boolean nongreenfieldHtStasPresent, boolean obssNonHtStasPresent,
|
||||||
|
byte channelCenterFrequencySegment2, boolean dualBeacon, boolean dualCtsProtection, boolean stbcBeacon) {
|
||||||
|
/*
|
||||||
|
* XXX some combinations of these parameters may be invalid as defined by
|
||||||
|
* 802.11-2020, but this is not checked here. If fidelity to 802.11 is required,
|
||||||
|
* the caller of this method must make sure to pass in valid parameters. The
|
||||||
|
* 802.11-2020 specification has more details about the parameters.
|
||||||
|
*/
|
||||||
|
this.primaryChannel = primaryChannel;
|
||||||
|
this.secondaryChannelOffset = secondaryChannelOffset;
|
||||||
|
this.staChannelWidth = staChannelWidth;
|
||||||
|
this.rifsMode = rifsMode;
|
||||||
|
this.htProtection = htProtection;
|
||||||
|
this.nongreenfieldHtStasPresent = nongreenfieldHtStasPresent;
|
||||||
|
this.obssNonHtStasPresent = obssNonHtStasPresent;
|
||||||
|
this.channelCenterFrequencySegment2 = channelCenterFrequencySegment2;
|
||||||
|
this.dualBeacon = dualBeacon;
|
||||||
|
this.dualCtsProtection = dualCtsProtection;
|
||||||
|
this.stbcBeacon = stbcBeacon;
|
||||||
|
// the next 16 bytes are for the basic HT-MCS set
|
||||||
|
// a default is chosen; if needed, we can add a parameter to set these
|
||||||
|
this.basicHtMcsSet = new byte[16];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Constructor with the most used parameters. */
|
||||||
|
public HTOperationElement(byte primaryChannel, byte secondaryChannelOffset, boolean staChannelWidth,
|
||||||
|
byte channelCenterFrequencySegment2) {
|
||||||
|
this(primaryChannel, secondaryChannelOffset, staChannelWidth, false, (byte) 0, true, false,
|
||||||
|
channelCenterFrequencySegment2, false, false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an {@code HTOperationElement} by decoding {@code htOper}.
|
||||||
|
*
|
||||||
|
* @param htOper a base64 encoded properly formatted HT operation element (see
|
||||||
|
* 802.11)
|
||||||
|
*/
|
||||||
|
public HTOperationElement(String htOper) {
|
||||||
|
byte[] bytes = Base64.decodeBase64(htOper);
|
||||||
|
/*
|
||||||
|
* Note that the code here may seem to read "reversed" compared to 802.11. This
|
||||||
|
* is because the bits within a byte are delivered from MSB to LSB, whereas the
|
||||||
|
* 802.11 graphic shows the bits LSB-first. At least, this is our understanding
|
||||||
|
* from looking at 802.11 and at the actual HT operation elements from the
|
||||||
|
* edgecore APs.
|
||||||
|
*/
|
||||||
|
this.primaryChannel = bytes[0];
|
||||||
|
this.rifsMode = ((bytes[1] & 0b00001000) >>> 3) == 1;
|
||||||
|
this.staChannelWidth = ((bytes[1] & 0b00000100) >>> 2) == 1;
|
||||||
|
this.secondaryChannelOffset = (byte) (bytes[1] & 0b00000011);
|
||||||
|
byte channelCenterFrequencySegment2LastThreeBits = (byte) ((bytes[2] & 0b11100000) >>> 5);
|
||||||
|
this.obssNonHtStasPresent = ((bytes[2] & 0b00010000) >>> 4) == 1;
|
||||||
|
this.nongreenfieldHtStasPresent = ((bytes[2] & 0b00000100) >>> 2) == 1;
|
||||||
|
this.htProtection = (byte) (bytes[2] & 0b00000011);
|
||||||
|
byte channelCenterFrequencySegment2FirstFiveBits = (byte) (bytes[3] & 0b00011111);
|
||||||
|
this.channelCenterFrequencySegment2 = (byte) (((byte) (channelCenterFrequencySegment2FirstFiveBits << 3))
|
||||||
|
| channelCenterFrequencySegment2LastThreeBits);
|
||||||
|
this.dualCtsProtection = ((bytes[4] & 0b10000000) >>> 7) == 1;
|
||||||
|
this.dualBeacon = ((bytes[4] & 0b01000000) >>> 6) == 1;
|
||||||
|
this.stbcBeacon = (bytes[5] & 0b00000001) == 1;
|
||||||
|
byte[] basicHtMcsSet = new byte[16];
|
||||||
|
for (int i = 0; i < basicHtMcsSet.length; i++) {
|
||||||
|
basicHtMcsSet[i] = bytes[6 + i];
|
||||||
|
}
|
||||||
|
this.basicHtMcsSet = basicHtMcsSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether {@code this} and {@code other} "match" for the purpose of
|
||||||
|
* aggregating statistics.
|
||||||
|
*
|
||||||
|
* @param other another HT operation element
|
||||||
|
* @return true if the the operation elements "match" for the purpose of
|
||||||
|
* aggregating statistics; false otherwise.
|
||||||
|
*/
|
||||||
|
public boolean matchesForAggregation(HTOperationElement other) {
|
||||||
|
return other != null && primaryChannel == other.primaryChannel
|
||||||
|
&& secondaryChannelOffset == other.secondaryChannelOffset && staChannelWidth == other.staChannelWidth
|
||||||
|
&& channelCenterFrequencySegment2 == other.channelCenterFrequencySegment2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether two HT operation elements should have their statistics
|
||||||
|
* aggregated.
|
||||||
|
*
|
||||||
|
* @param htOper1 a base64 encoded properly formatted HT operation element (see
|
||||||
|
* 802.11)
|
||||||
|
* @param htOper2 a base64 encoded properly formatted HT operation element (see
|
||||||
|
* 802.11)
|
||||||
|
* @return true if the two inputs should have their statistics aggregated; false
|
||||||
|
* otherwise.
|
||||||
|
*/
|
||||||
|
public static boolean matchesHtForAggregation(String htOper1, String htOper2) {
|
||||||
|
if (Objects.equals(htOper1, htOper2)) {
|
||||||
|
return true; // true if both are null or they are equal
|
||||||
|
}
|
||||||
|
if (htOper1 == null || htOper2 == null) {
|
||||||
|
return false; // false if exactly one is null
|
||||||
|
}
|
||||||
|
HTOperationElement htOperObj1 = new HTOperationElement(htOper1);
|
||||||
|
HTOperationElement htOperObj2 = new HTOperationElement(htOper2);
|
||||||
|
return htOperObj1.matchesForAggregation(htOperObj2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
final int prime = 31;
|
||||||
|
int result = 1;
|
||||||
|
result = prime * result + Arrays.hashCode(basicHtMcsSet);
|
||||||
|
result = prime * result + Objects.hash(channelCenterFrequencySegment2, dualBeacon, dualCtsProtection,
|
||||||
|
htProtection, nongreenfieldHtStasPresent, obssNonHtStasPresent, primaryChannel, rifsMode,
|
||||||
|
secondaryChannelOffset, staChannelWidth, stbcBeacon);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (getClass() != obj.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
HTOperationElement other = (HTOperationElement) obj;
|
||||||
|
return Arrays.equals(basicHtMcsSet, other.basicHtMcsSet)
|
||||||
|
&& channelCenterFrequencySegment2 == other.channelCenterFrequencySegment2
|
||||||
|
&& dualBeacon == other.dualBeacon && dualCtsProtection == other.dualCtsProtection
|
||||||
|
&& htProtection == other.htProtection
|
||||||
|
&& nongreenfieldHtStasPresent == other.nongreenfieldHtStasPresent
|
||||||
|
&& obssNonHtStasPresent == other.obssNonHtStasPresent && primaryChannel == other.primaryChannel
|
||||||
|
&& rifsMode == other.rifsMode && secondaryChannelOffset == other.secondaryChannelOffset
|
||||||
|
&& staChannelWidth == other.staChannelWidth && stbcBeacon == other.stbcBeacon;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.facebook.openwifirrm.ucentral.operationelement;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import org.apache.commons.codec.binary.Base64;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Very High Throughput (VHT) Operation Element, which is potentially present in
|
||||||
|
* wifiscan entries. Introduced in 802.11ac (2013).
|
||||||
|
*/
|
||||||
|
public class VHTOperationElement {
|
||||||
|
|
||||||
|
/** False if the channel width is 20 MHz or 40 MHz; true otherwise. */
|
||||||
|
private final boolean channelWidthIndicator;
|
||||||
|
/**
|
||||||
|
* If the channel is 20 MHz, 40 MHz, or 80 MHz wide, this parameter is the
|
||||||
|
* channel number. E.g., the channel centered at 5180 MHz is channel 36. For a
|
||||||
|
* 160 MHz wide channel, this parameter is the channel number of the 80MHz
|
||||||
|
* channel that contains the primary channel. For a 80+80 MHz wide channel, this
|
||||||
|
* parameter is the channel number of the primary channel.
|
||||||
|
*/
|
||||||
|
private final byte channel1;
|
||||||
|
/**
|
||||||
|
* This should be zero unless the channel is 160MHz or 80+80 MHz wide. If the
|
||||||
|
* channel is 160 MHz wide, this parameter is the channel number of the 160 MHz
|
||||||
|
* wide channel. If the channel is 80+80 MHz wide, this parameter is the channel
|
||||||
|
* index of the secondary 80 MHz wide channel.
|
||||||
|
*/
|
||||||
|
private final byte channel2;
|
||||||
|
/**
|
||||||
|
* An 8-element array where each element is between 0 and 4 inclusive. MCS means
|
||||||
|
* Modulation and Coding Scheme. NSS means Number of Spatial Streams. There can
|
||||||
|
* be 1, 2, ..., or 8 spatial streams. For each NSS, the corresponding element
|
||||||
|
* in the array should specify which MCSs are supported for that NSS in the
|
||||||
|
* following manner: 0 indicates support for VHT-MCS 0-7, 1 indicates support
|
||||||
|
* for VHT-MCS 0-8, 2 indicates support for VHT-MCS 0-9, and 3 indicates that no
|
||||||
|
* VHT-MCS is supported for that NSS. For the specifics of what each VHT-MCS is,
|
||||||
|
* see IEEE 802.11-2020, Table "21-29" through Table "21-60".
|
||||||
|
*/
|
||||||
|
private final byte[] vhtMcsForNss;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a {@code VHTOperationElement} by decoding {@code vhtOper}.
|
||||||
|
*
|
||||||
|
* @param vhtOper a base64 encoded properly formatted VHT operation element (see
|
||||||
|
* 802.11 standard)
|
||||||
|
*/
|
||||||
|
public VHTOperationElement(String vhtOper) {
|
||||||
|
byte[] bytes = Base64.decodeBase64(vhtOper);
|
||||||
|
this.channelWidthIndicator = bytes[0] == 1;
|
||||||
|
this.channel1 = bytes[1];
|
||||||
|
this.channel2 = bytes[2];
|
||||||
|
byte[] vhtMcsForNss = new byte[8];
|
||||||
|
vhtMcsForNss[0] = (byte) (bytes[3] >>> 6);
|
||||||
|
vhtMcsForNss[1] = (byte) ((bytes[3] & 0b00110000) >>> 4);
|
||||||
|
vhtMcsForNss[2] = (byte) ((bytes[3] & 0b00001100) >>> 2);
|
||||||
|
vhtMcsForNss[3] = (byte) (bytes[3] & 0b00000011);
|
||||||
|
vhtMcsForNss[4] = (byte) (bytes[4] >>> 6);
|
||||||
|
vhtMcsForNss[5] = (byte) ((bytes[4] & 0b00110000) >>> 4);
|
||||||
|
vhtMcsForNss[6] = (byte) ((bytes[4] & 0b00001100) >>> 2);
|
||||||
|
vhtMcsForNss[7] = (byte) (bytes[4] & 0b00000011);
|
||||||
|
this.vhtMcsForNss = vhtMcsForNss;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an {@code HTOperationElement} using the given field values. See
|
||||||
|
* 802.11 for more details.
|
||||||
|
* <p>
|
||||||
|
* For details about the parameters, see the javadocs for the corresponding
|
||||||
|
* member variables.
|
||||||
|
*/
|
||||||
|
public VHTOperationElement(boolean channelWidthIndicator, byte channel1, byte channel2, byte[] vhtMcsForNss) {
|
||||||
|
/*
|
||||||
|
* XXX some combinations of channelWidth, channel, channel2, and vhtMcsAtNss are
|
||||||
|
* invalid, but this is not checked here. If fidelity to 802.11 is required, the
|
||||||
|
* caller of this method must make sure to pass in valid parameters.
|
||||||
|
*/
|
||||||
|
this.channelWidthIndicator = channelWidthIndicator;
|
||||||
|
this.channel1 = channel1;
|
||||||
|
this.channel2 = channel2;
|
||||||
|
this.vhtMcsForNss = vhtMcsForNss;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether {@code this} and {@code other} "match" for the purpose of
|
||||||
|
* aggregating statistics.
|
||||||
|
*
|
||||||
|
* @param other another VHT operation element
|
||||||
|
* @return true if the the operation elements "match" for the purpose of
|
||||||
|
* aggregating statistics; false otherwise.
|
||||||
|
*/
|
||||||
|
public boolean matchesForAggregation(VHTOperationElement other) {
|
||||||
|
// check everything except vhtMcsForNss
|
||||||
|
return other != null && channel1 == other.channel1 && channel2 == other.channel2
|
||||||
|
&& channelWidthIndicator == other.channelWidthIndicator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether two VHT operation elements should have their statistics
|
||||||
|
* aggregated.
|
||||||
|
*
|
||||||
|
* @param vhtOper1 a base64 encoded properly formatted VHT operation element
|
||||||
|
* (see 802.11 standard)
|
||||||
|
*
|
||||||
|
* @param vhtOper2 a base64 encoded properly formatted VHT operation element
|
||||||
|
* (see 802.11 standard)
|
||||||
|
* @return true if the two inputs should have their statistics aggregated; false
|
||||||
|
* otherwise.
|
||||||
|
*/
|
||||||
|
public static boolean matchesVhtForAggregation(String vhtOper1, String vhtOper2) {
|
||||||
|
if (Objects.equals(vhtOper1, vhtOper2)) {
|
||||||
|
return true; // true if both are null or they are equal
|
||||||
|
}
|
||||||
|
if (vhtOper1 == null || vhtOper2 == null) {
|
||||||
|
return false; // false if exactly one is null
|
||||||
|
}
|
||||||
|
VHTOperationElement vhtOperObj1 = new VHTOperationElement(vhtOper1);
|
||||||
|
VHTOperationElement vhtOperObj2 = new VHTOperationElement(vhtOper2);
|
||||||
|
return vhtOperObj1.matchesForAggregation(vhtOperObj2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
final int prime = 31;
|
||||||
|
int result = 1;
|
||||||
|
result = prime * result + Arrays.hashCode(vhtMcsForNss);
|
||||||
|
result = prime * result + Objects.hash(channel1, channel2, channelWidthIndicator);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (getClass() != obj.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
VHTOperationElement other = (VHTOperationElement) obj;
|
||||||
|
return channel1 == other.channel1 && channel2 == other.channel2
|
||||||
|
&& channelWidthIndicator == other.channelWidthIndicator
|
||||||
|
&& Arrays.equals(vhtMcsForNss, other.vhtMcsForNss);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.facebook.openwifirrm.aggregators;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
public class MeanAggregatorTest {
|
||||||
|
@Test
|
||||||
|
public void testEmptyAndNonEmptyAndReset() {
|
||||||
|
final double eps = 0.000001;
|
||||||
|
|
||||||
|
MeanAggregator agg = new MeanAggregator();
|
||||||
|
|
||||||
|
// default mean is 0
|
||||||
|
assertEquals(0, agg.getAggregate(), eps);
|
||||||
|
assertEquals(0, agg.getCount());
|
||||||
|
|
||||||
|
// adding 0 (the mean) does not change the mean
|
||||||
|
agg.addValue(0.0);
|
||||||
|
assertEquals(0, agg.getAggregate(), eps);
|
||||||
|
assertEquals(1, agg.getCount());
|
||||||
|
|
||||||
|
// add an "int"
|
||||||
|
agg.addValue(1.0);
|
||||||
|
assertEquals(0.5, agg.getAggregate(), eps);
|
||||||
|
assertEquals(2, agg.getCount());
|
||||||
|
|
||||||
|
// add a double
|
||||||
|
agg.addValue(3.5);
|
||||||
|
assertEquals(1.5, agg.getAggregate(), eps);
|
||||||
|
assertEquals(3, agg.getCount());
|
||||||
|
|
||||||
|
// add a negative number
|
||||||
|
agg.addValue(-0.5);
|
||||||
|
assertEquals(1.0, agg.getAggregate(), eps);
|
||||||
|
assertEquals(4, agg.getCount());
|
||||||
|
|
||||||
|
// adding the mean does not change the mean
|
||||||
|
agg.addValue(1.0);
|
||||||
|
assertEquals(1.0, agg.getAggregate(), eps);
|
||||||
|
assertEquals(5, agg.getCount());
|
||||||
|
|
||||||
|
// test reset
|
||||||
|
agg.reset();
|
||||||
|
assertEquals(0, agg.getAggregate(), eps);
|
||||||
|
assertEquals(0, agg.getCount());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,13 +9,22 @@
|
|||||||
package com.facebook.openwifirrm.modules;
|
package com.facebook.openwifirrm.modules;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import com.facebook.openwifirrm.aggregators.MeanAggregator;
|
||||||
|
import com.facebook.openwifirrm.modules.Modeler.DataModel;
|
||||||
|
import com.facebook.openwifirrm.optimizers.TestUtils;
|
||||||
|
import com.facebook.openwifirrm.ucentral.UCentralUtils.WifiScanEntry;
|
||||||
|
|
||||||
public class ModelerUtilsTest {
|
public class ModelerUtilsTest {
|
||||||
@Test
|
@Test
|
||||||
void testErrorCase() throws Exception {
|
void testErrorCase() throws Exception {
|
||||||
@@ -77,4 +86,239 @@ public class ModelerUtilsTest {
|
|||||||
);
|
);
|
||||||
assertEquals(0.861, metric, 0.001);
|
assertEquals(0.861, metric, 0.001);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPreDot11nAggregatedWifiScanEntry() {
|
||||||
|
final long obsoletionPeriodMs = 900000;
|
||||||
|
|
||||||
|
final String apA = "aaaaaaaaaaaa";
|
||||||
|
final String bssidA = "aa:aa:aa:aa:aa:aa";
|
||||||
|
final String apB = "bbbbbbbbbbbb";
|
||||||
|
final String bssidB = "bb:bb:bb:bb:bb:bb";
|
||||||
|
final String apC = "cccccccccccc";
|
||||||
|
final String bssidC = "cc:cc:cc:cc:cc:cc";
|
||||||
|
DataModel dataModel = new DataModel();
|
||||||
|
|
||||||
|
// if there are no scan entries, there should be no aggregates
|
||||||
|
dataModel.latestWifiScans.put(apB, new LinkedList<>());
|
||||||
|
dataModel.latestWifiScans.put(apC, new LinkedList<>());
|
||||||
|
dataModel.latestWifiScans.get(apC).add(new ArrayList<>());
|
||||||
|
assertTrue(ModelerUtils.getAggregatedWifiScans(dataModel, obsoletionPeriodMs, new MeanAggregator()).isEmpty());
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When only apB conducts a scan, and receives one response from apA, that
|
||||||
|
* response should be the "aggregate response" from apA to apB, and apA and apC
|
||||||
|
* should have no aggregates for any BSSID.
|
||||||
|
*/
|
||||||
|
WifiScanEntry entryAToB1 = TestUtils.createWifiScanEntryWithBssid(1, bssidA);
|
||||||
|
entryAToB1.signal = -60;
|
||||||
|
dataModel.latestWifiScans.get(apB).add(Arrays.asList(entryAToB1));
|
||||||
|
Map<String, Map<String, WifiScanEntry>> aggregateMap = ModelerUtils.getAggregatedWifiScans(dataModel,
|
||||||
|
obsoletionPeriodMs, new MeanAggregator());
|
||||||
|
assertFalse(aggregateMap.containsKey(apA));
|
||||||
|
assertFalse(aggregateMap.containsKey(apC));
|
||||||
|
assertEquals(entryAToB1, aggregateMap.get(apB).get(bssidA));
|
||||||
|
|
||||||
|
// add another scan with one entry from apA to apB and check the aggregation
|
||||||
|
WifiScanEntry entryAToB2 = TestUtils.createWifiScanEntryWithBssid(1, bssidA);
|
||||||
|
entryAToB2.signal = -62;
|
||||||
|
entryAToB2.unixTimeMs += 60000; // 1 min later
|
||||||
|
dataModel.latestWifiScans.get(apB).add(Arrays.asList(entryAToB2));
|
||||||
|
aggregateMap = ModelerUtils.getAggregatedWifiScans(dataModel, obsoletionPeriodMs, new MeanAggregator());
|
||||||
|
WifiScanEntry expectedAggregatedEntryAToB = new WifiScanEntry(entryAToB2);
|
||||||
|
expectedAggregatedEntryAToB.signal = -61; // average of -60 and -62
|
||||||
|
assertFalse(aggregateMap.containsKey(apA));
|
||||||
|
assertFalse(aggregateMap.containsKey(apC));
|
||||||
|
assertEquals(expectedAggregatedEntryAToB, aggregateMap.get(apB).get(bssidA));
|
||||||
|
|
||||||
|
// test the obsoletion period boundaries
|
||||||
|
// test the inclusive non-obsolete boundary
|
||||||
|
WifiScanEntry entryAToB3 = TestUtils.createWifiScanEntryWithBssid(1, bssidA);
|
||||||
|
entryAToB3.signal = -64;
|
||||||
|
entryAToB3.unixTimeMs += obsoletionPeriodMs;
|
||||||
|
dataModel.latestWifiScans.get(apB).add(Arrays.asList(entryAToB3));
|
||||||
|
aggregateMap = ModelerUtils.getAggregatedWifiScans(dataModel, obsoletionPeriodMs, new MeanAggregator());
|
||||||
|
expectedAggregatedEntryAToB = new WifiScanEntry(entryAToB3);
|
||||||
|
expectedAggregatedEntryAToB.signal = -62; // average of -60, -62, and -64 3;
|
||||||
|
assertEquals(expectedAggregatedEntryAToB, aggregateMap.get(apB).get(bssidA));
|
||||||
|
// test moving the boundary by 1 ms and excluding the earliest entry
|
||||||
|
aggregateMap = ModelerUtils.getAggregatedWifiScans(dataModel, obsoletionPeriodMs - 1, new MeanAggregator());
|
||||||
|
expectedAggregatedEntryAToB.signal = -63; // average of -62 and -64
|
||||||
|
assertEquals(expectedAggregatedEntryAToB, aggregateMap.get(apB).get(bssidA));
|
||||||
|
// test an obsoletion period of 0 ms
|
||||||
|
aggregateMap = ModelerUtils.getAggregatedWifiScans(dataModel, 0, new MeanAggregator());
|
||||||
|
expectedAggregatedEntryAToB.signal = -64; // latest rssid
|
||||||
|
assertEquals(expectedAggregatedEntryAToB, aggregateMap.get(apB).get(bssidA));
|
||||||
|
// Test that the obsoletion period starts counting backwards from the time of
|
||||||
|
// the most recent entry for each (ap, bssid) tuple.
|
||||||
|
WifiScanEntry entryCToB1 = TestUtils.createWifiScanEntryWithBssid(1, bssidC);
|
||||||
|
entryCToB1.signal = -70;
|
||||||
|
entryCToB1.unixTimeMs += 2 * obsoletionPeriodMs;
|
||||||
|
dataModel.latestWifiScans.get(apB).add(Arrays.asList(entryCToB1));
|
||||||
|
// an obsoletion period of 0 means to get the most recent entry for each bssid
|
||||||
|
// regardless of how far apart the entries are for different bssids
|
||||||
|
aggregateMap = ModelerUtils.getAggregatedWifiScans(dataModel, 0, new MeanAggregator());
|
||||||
|
WifiScanEntry expectedAggregatedEntryCToB = new WifiScanEntry(entryCToB1);
|
||||||
|
expectedAggregatedEntryAToB = new WifiScanEntry(entryAToB3);
|
||||||
|
assertEquals(expectedAggregatedEntryCToB, aggregateMap.get(apB).get(bssidC));
|
||||||
|
assertEquals(expectedAggregatedEntryAToB, aggregateMap.get(apB).get(bssidA));
|
||||||
|
|
||||||
|
// test multiple entries in one scan and scans from multiple APs
|
||||||
|
WifiScanEntry entryAToB4 = TestUtils.createWifiScanEntryWithBssid(1, bssidA);
|
||||||
|
entryAToB4.signal = -80;
|
||||||
|
entryAToB4.unixTimeMs += 3 * obsoletionPeriodMs;
|
||||||
|
WifiScanEntry entryCToB2 = TestUtils.createWifiScanEntryWithBssid(1, bssidC);
|
||||||
|
entryCToB2.signal = -80;
|
||||||
|
entryCToB2.unixTimeMs += 3 * obsoletionPeriodMs;
|
||||||
|
WifiScanEntry entryBToA1 = TestUtils.createWifiScanEntryWithBssid(1, bssidB);
|
||||||
|
entryBToA1.signal = -60;
|
||||||
|
entryBToA1.unixTimeMs += 3 * obsoletionPeriodMs;
|
||||||
|
WifiScanEntry entryCToA1 = TestUtils.createWifiScanEntryWithBssid(1, bssidC);
|
||||||
|
entryCToA1.signal = -60;
|
||||||
|
entryCToA1.unixTimeMs += 3 * obsoletionPeriodMs;
|
||||||
|
dataModel.latestWifiScans.get(apB).add(Arrays.asList(entryCToB2, entryAToB4));
|
||||||
|
dataModel.latestWifiScans.put(apA, new LinkedList<>());
|
||||||
|
dataModel.latestWifiScans.get(apA).add(Arrays.asList(entryBToA1, entryCToA1));
|
||||||
|
aggregateMap = ModelerUtils.getAggregatedWifiScans(dataModel, obsoletionPeriodMs, new MeanAggregator());
|
||||||
|
expectedAggregatedEntryCToB = new WifiScanEntry(entryCToB2);
|
||||||
|
expectedAggregatedEntryCToB.signal = -75; // average of -70 and-80
|
||||||
|
expectedAggregatedEntryAToB = new WifiScanEntry(entryAToB4);
|
||||||
|
WifiScanEntry expectedAggregatedEntryCToA = new WifiScanEntry(entryCToA1);
|
||||||
|
WifiScanEntry expectedAggregatedEntryBToA = new WifiScanEntry(entryBToA1);
|
||||||
|
assertEquals(expectedAggregatedEntryCToB, aggregateMap.get(apB).get(bssidC));
|
||||||
|
assertEquals(expectedAggregatedEntryAToB, aggregateMap.get(apB).get(bssidA));
|
||||||
|
assertEquals(expectedAggregatedEntryCToA, aggregateMap.get(apA).get(bssidC));
|
||||||
|
assertEquals(expectedAggregatedEntryBToA, aggregateMap.get(apA).get(bssidB));
|
||||||
|
|
||||||
|
// test that entries are not aggregated when channel information does not match
|
||||||
|
WifiScanEntry entryBToA2 = TestUtils.createWifiScanEntryWithBssid(2, bssidB); // different channel
|
||||||
|
entryBToA2.signal = -62;
|
||||||
|
entryBToA2.unixTimeMs += 3 * obsoletionPeriodMs + 1; // 1 sec after the most recent B->A response
|
||||||
|
dataModel.latestWifiScans.get(apA).add(Arrays.asList(entryBToA2));
|
||||||
|
expectedAggregatedEntryBToA = new WifiScanEntry(entryBToA2);
|
||||||
|
aggregateMap = ModelerUtils.getAggregatedWifiScans(dataModel, obsoletionPeriodMs, new MeanAggregator());
|
||||||
|
assertEquals(expectedAggregatedEntryBToA, aggregateMap.get(apA).get(bssidB));
|
||||||
|
|
||||||
|
// test out of order wifiscans
|
||||||
|
WifiScanEntry entryBToA3 = TestUtils.createWifiScanEntryWithBssid(2, bssidB); // different channel
|
||||||
|
entryBToA3.signal = -64;
|
||||||
|
entryBToA3.unixTimeMs += 3 * obsoletionPeriodMs - 1;
|
||||||
|
dataModel.latestWifiScans.get(apA).add(Arrays.asList(entryBToA3));
|
||||||
|
expectedAggregatedEntryBToA = new WifiScanEntry(entryBToA2); // use the most recent entry
|
||||||
|
expectedAggregatedEntryBToA.signal = -63; // average of -62 and -64 (skipping -60, different channel)
|
||||||
|
aggregateMap = ModelerUtils.getAggregatedWifiScans(dataModel, obsoletionPeriodMs, new MeanAggregator());
|
||||||
|
assertEquals(expectedAggregatedEntryBToA, aggregateMap.get(apA).get(bssidB));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPostDot11nAggregatedWifiScanEntry() {
|
||||||
|
final long obsoletionPeriodMs = 900000;
|
||||||
|
|
||||||
|
final String bssidA = "aa:aa:aa:aa:aa:aa";
|
||||||
|
final String apB = "bbbbbbbbbbbb";
|
||||||
|
DataModel dataModel = new DataModel();
|
||||||
|
|
||||||
|
// First, test that entries for different channels do not aggregate (this could
|
||||||
|
// have been tested in testPreDot11nAggregatedWifiScanEntry)
|
||||||
|
byte primaryChannel = 1; // first entry on channel 1
|
||||||
|
String htOper = "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAA==";
|
||||||
|
WifiScanEntry entryAToB1 = TestUtils.createWifiScanEntryWithWidth(bssidA, primaryChannel, htOper, null);
|
||||||
|
entryAToB1.signal = -60;
|
||||||
|
dataModel.latestWifiScans.put(apB, new LinkedList<>());
|
||||||
|
dataModel.latestWifiScans.get(apB).add(Arrays.asList(entryAToB1));
|
||||||
|
Map<String, Map<String, WifiScanEntry>> aggregateMap = ModelerUtils.getAggregatedWifiScans(dataModel,
|
||||||
|
obsoletionPeriodMs, new MeanAggregator());
|
||||||
|
WifiScanEntry expectedAggregatedEntryAToB = new WifiScanEntry(entryAToB1);
|
||||||
|
assertEquals(expectedAggregatedEntryAToB, aggregateMap.get(apB).get(bssidA));
|
||||||
|
|
||||||
|
primaryChannel = 6; // second entry on channel 6, should only aggregate this one
|
||||||
|
htOper = "BgAAAAAAAAAAAAAAAAAAAAAAAAAAAA==";
|
||||||
|
WifiScanEntry entryAToB2 = TestUtils.createWifiScanEntryWithWidth(bssidA, primaryChannel, htOper, null);
|
||||||
|
entryAToB2.signal = -62;
|
||||||
|
entryAToB2.unixTimeMs += 60000; // 1 min after previous entry
|
||||||
|
dataModel.latestWifiScans.get(apB).add(Arrays.asList(entryAToB2));
|
||||||
|
aggregateMap = ModelerUtils.getAggregatedWifiScans(dataModel, obsoletionPeriodMs, new MeanAggregator());
|
||||||
|
expectedAggregatedEntryAToB = new WifiScanEntry(entryAToB2);
|
||||||
|
assertEquals(expectedAggregatedEntryAToB, aggregateMap.get(apB).get(bssidA));
|
||||||
|
|
||||||
|
primaryChannel = 1; // third entry on channel 1 again, should aggregate first and third entry
|
||||||
|
htOper = "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAA==";
|
||||||
|
WifiScanEntry entryAToB3 = TestUtils.createWifiScanEntryWithWidth(bssidA, primaryChannel, htOper, null);
|
||||||
|
entryAToB3.signal = -70;
|
||||||
|
entryAToB3.unixTimeMs += 120000; // 1 min after previous entry
|
||||||
|
dataModel.latestWifiScans.get(apB).add(Arrays.asList(entryAToB3));
|
||||||
|
aggregateMap = ModelerUtils.getAggregatedWifiScans(dataModel, obsoletionPeriodMs, new MeanAggregator());
|
||||||
|
expectedAggregatedEntryAToB = new WifiScanEntry(entryAToB3);
|
||||||
|
expectedAggregatedEntryAToB.signal = -65; // average of -60 and -70 (would be -64 if the -62 entry was included)
|
||||||
|
assertEquals(expectedAggregatedEntryAToB, aggregateMap.get(apB).get(bssidA));
|
||||||
|
|
||||||
|
// Test that entries with HT operation elements which differ in the relevant
|
||||||
|
// fields (channel numbers and widths) are not aggregated together.
|
||||||
|
primaryChannel = 1;
|
||||||
|
htOper = "AQUAAAAAAAAAAAAAAAAAAAAAAAAAAA=="; // use secondary channel and wider channel
|
||||||
|
WifiScanEntry entryAToB4 = TestUtils.createWifiScanEntryWithWidth(bssidA, primaryChannel, htOper, null);
|
||||||
|
entryAToB4.signal = -72;
|
||||||
|
entryAToB4.unixTimeMs += 180000; // 1 min after previous entry
|
||||||
|
dataModel.latestWifiScans.get(apB).add(Arrays.asList(entryAToB4));
|
||||||
|
aggregateMap = ModelerUtils.getAggregatedWifiScans(dataModel, obsoletionPeriodMs, new MeanAggregator());
|
||||||
|
expectedAggregatedEntryAToB = new WifiScanEntry(entryAToB4);
|
||||||
|
expectedAggregatedEntryAToB.signal = -72;
|
||||||
|
assertEquals(expectedAggregatedEntryAToB, aggregateMap.get(apB).get(bssidA));
|
||||||
|
|
||||||
|
// Test that entries with HT operation elements with differ only in irrelevant
|
||||||
|
// fields are aggregated together
|
||||||
|
htOper = "AQUAAAAAgAAAAAAAAAAAAAAAAAAAAA=="; // use different Basic HT-MCS Set field
|
||||||
|
WifiScanEntry entryAToB5 = TestUtils.createWifiScanEntryWithWidth(bssidA, primaryChannel, htOper, null);
|
||||||
|
entryAToB5.signal = -74;
|
||||||
|
entryAToB5.unixTimeMs += 240000; // 1 min after previous entry
|
||||||
|
dataModel.latestWifiScans.get(apB).add(Arrays.asList(entryAToB5));
|
||||||
|
aggregateMap = ModelerUtils.getAggregatedWifiScans(dataModel, obsoletionPeriodMs, new MeanAggregator());
|
||||||
|
expectedAggregatedEntryAToB = new WifiScanEntry(entryAToB5);
|
||||||
|
expectedAggregatedEntryAToB.signal = -73; // average of -72 and -74
|
||||||
|
assertEquals(expectedAggregatedEntryAToB, aggregateMap.get(apB).get(bssidA));
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Test that entries with VHT operation elements which differ in the relevant
|
||||||
|
* fields (channel numbers and widths) are not aggregated together. Use channel
|
||||||
|
* 42 (80 MHz wide), with the primary channel being 36 (contained "within" the
|
||||||
|
* wider channel 42).
|
||||||
|
*/
|
||||||
|
primaryChannel = 36;
|
||||||
|
// use secondary channel offset field of 1 and sta channel width field of 1
|
||||||
|
htOper = "JAUAAAAAAAAAAAAAAAAAAAAAAAAAAA==";
|
||||||
|
String vhtOper = "ASoAAAA=";
|
||||||
|
WifiScanEntry entryAToB6 = TestUtils.createWifiScanEntryWithWidth(bssidA, primaryChannel, htOper, vhtOper);
|
||||||
|
entryAToB6.signal = -74;
|
||||||
|
entryAToB6.unixTimeMs += 300000; // 1 min after previous entry
|
||||||
|
dataModel.latestWifiScans.get(apB).add(Arrays.asList(entryAToB6));
|
||||||
|
aggregateMap = ModelerUtils.getAggregatedWifiScans(dataModel, obsoletionPeriodMs, new MeanAggregator());
|
||||||
|
expectedAggregatedEntryAToB = new WifiScanEntry(entryAToB6);
|
||||||
|
assertEquals(expectedAggregatedEntryAToB, aggregateMap.get(apB).get(bssidA));
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Switch to channel 50 (160 MHz wide) which still "contains" channel 36. All
|
||||||
|
* other fields stay the same. In reality, the entry's channel field may change,
|
||||||
|
* but here it remains the same, just to test vhtOper.
|
||||||
|
*/
|
||||||
|
vhtOper = "ASoyAAA=";
|
||||||
|
WifiScanEntry entryAToB7 = TestUtils.createWifiScanEntryWithWidth(bssidA, primaryChannel, htOper, vhtOper);
|
||||||
|
entryAToB7.signal = -76;
|
||||||
|
entryAToB7.unixTimeMs += 360000; // 1 min after previous entry
|
||||||
|
dataModel.latestWifiScans.get(apB).add(Arrays.asList(entryAToB7));
|
||||||
|
aggregateMap = ModelerUtils.getAggregatedWifiScans(dataModel, obsoletionPeriodMs, new MeanAggregator());
|
||||||
|
expectedAggregatedEntryAToB = new WifiScanEntry(entryAToB7);
|
||||||
|
assertEquals(expectedAggregatedEntryAToB, aggregateMap.get(apB).get(bssidA));
|
||||||
|
|
||||||
|
// Test that entries with VHT operation elements with differ only in irrelevant
|
||||||
|
// fields are aggregated together
|
||||||
|
vhtOper = "ASoygAA="; // use different Basic VHT-MCS Set and NSS Set field
|
||||||
|
WifiScanEntry entryAToB8 = TestUtils.createWifiScanEntryWithWidth(bssidA, primaryChannel, htOper, vhtOper);
|
||||||
|
entryAToB8.signal = -78;
|
||||||
|
entryAToB8.unixTimeMs += 420000; // 1 min after previous entry
|
||||||
|
dataModel.latestWifiScans.get(apB).add(Arrays.asList(entryAToB8));
|
||||||
|
aggregateMap = ModelerUtils.getAggregatedWifiScans(dataModel, obsoletionPeriodMs, new MeanAggregator());
|
||||||
|
expectedAggregatedEntryAToB = new WifiScanEntry(entryAToB8);
|
||||||
|
expectedAggregatedEntryAToB.signal = -77; // average of -78 and -76
|
||||||
|
assertEquals(expectedAggregatedEntryAToB, aggregateMap.get(apB).get(bssidA));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -622,6 +622,7 @@ public class LeastUsedChannelOptimizerTest {
|
|||||||
deviceB,
|
deviceB,
|
||||||
Arrays.asList(
|
Arrays.asList(
|
||||||
TestUtils.createWifiScanListWithWidth(
|
TestUtils.createWifiScanListWithWidth(
|
||||||
|
null,
|
||||||
Arrays.asList(36, 157),
|
Arrays.asList(36, 157),
|
||||||
Arrays.asList(
|
Arrays.asList(
|
||||||
"JAUWAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
"JAUWAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||||
@@ -654,6 +655,7 @@ public class LeastUsedChannelOptimizerTest {
|
|||||||
deviceC,
|
deviceC,
|
||||||
Arrays.asList(
|
Arrays.asList(
|
||||||
TestUtils.createWifiScanListWithWidth(
|
TestUtils.createWifiScanListWithWidth(
|
||||||
|
null,
|
||||||
channelsC2,
|
channelsC2,
|
||||||
Arrays.asList(
|
Arrays.asList(
|
||||||
"JAUWAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
"JAUWAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
package com.facebook.openwifirrm.optimizers;
|
package com.facebook.openwifirrm.optimizers;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -17,6 +18,7 @@ import java.util.TreeSet;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import com.facebook.openwifirrm.DeviceTopology;
|
import com.facebook.openwifirrm.DeviceTopology;
|
||||||
|
import com.facebook.openwifirrm.ucentral.UCentralUtils;
|
||||||
import com.facebook.openwifirrm.ucentral.UCentralUtils.WifiScanEntry;
|
import com.facebook.openwifirrm.ucentral.UCentralUtils.WifiScanEntry;
|
||||||
import com.facebook.openwifirrm.ucentral.models.State;
|
import com.facebook.openwifirrm.ucentral.models.State;
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
@@ -26,6 +28,9 @@ public class TestUtils {
|
|||||||
/** The Gson instance. */
|
/** The Gson instance. */
|
||||||
private static final Gson gson = new Gson();
|
private static final Gson gson = new Gson();
|
||||||
|
|
||||||
|
/** Default value for {@link WifiScanEntry#unixTimeMs} for testing. */
|
||||||
|
public static final Instant DEFAULT_WIFISCANENTRY_TIME = Instant.parse("2022-01-01T00:00:00Z");
|
||||||
|
|
||||||
/** Create a topology from the given devices in a single zone. */
|
/** Create a topology from the given devices in a single zone. */
|
||||||
public static DeviceTopology createTopology(String zone, String... devices) {
|
public static DeviceTopology createTopology(String zone, String... devices) {
|
||||||
DeviceTopology topology = new DeviceTopology();
|
DeviceTopology topology = new DeviceTopology();
|
||||||
@@ -69,7 +74,9 @@ public class TestUtils {
|
|||||||
public static WifiScanEntry createWifiScanEntry(int channel) {
|
public static WifiScanEntry createWifiScanEntry(int channel) {
|
||||||
WifiScanEntry entry = new WifiScanEntry();
|
WifiScanEntry entry = new WifiScanEntry();
|
||||||
entry.channel = channel;
|
entry.channel = channel;
|
||||||
|
entry.frequency = UCentralUtils.channelToFrequencyMHz(channel);
|
||||||
entry.signal = -60;
|
entry.signal = -60;
|
||||||
|
entry.unixTimeMs = TestUtils.DEFAULT_WIFISCANENTRY_TIME.toEpochMilli();
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,10 +90,10 @@ public class TestUtils {
|
|||||||
|
|
||||||
/** Create a wifi scan entry with the given BSSID and RSSI. */
|
/** Create a wifi scan entry with the given BSSID and RSSI. */
|
||||||
public static WifiScanEntry createWifiScanEntryWithBssid(String bssid, Integer rssi) {
|
public static WifiScanEntry createWifiScanEntryWithBssid(String bssid, Integer rssi) {
|
||||||
WifiScanEntry entry = new WifiScanEntry();
|
final int channel = 36;
|
||||||
entry.channel = 36;
|
WifiScanEntry entry = createWifiScanEntry(channel);
|
||||||
entry.bssid = bssid;
|
entry.bssid = bssid;
|
||||||
entry.signal = rssi;
|
entry.signal = rssi; // overwrite
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,37 +107,38 @@ public class TestUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a wifi scan entry with the given channel
|
* Create a wifi scan entry with the given channel and channel width info (in
|
||||||
* and channel width info (in the format of HT operation and VHT operation).
|
* the format of HT operation and VHT operation). It is the caller's
|
||||||
|
* responsibility to make sure {@code channel}, {@code htOper}, and
|
||||||
|
* {@code vhtOper} are consistent.
|
||||||
*/
|
*/
|
||||||
public static WifiScanEntry createWifiScanEntryWithWidth(
|
public static WifiScanEntry createWifiScanEntryWithWidth(String bssid, int channel, String htOper, String vhtOper) {
|
||||||
int channel,
|
|
||||||
String htOper,
|
|
||||||
String vhtOper
|
|
||||||
) {
|
|
||||||
WifiScanEntry entry = new WifiScanEntry();
|
WifiScanEntry entry = new WifiScanEntry();
|
||||||
|
entry.bssid = bssid;
|
||||||
entry.channel = channel;
|
entry.channel = channel;
|
||||||
|
entry.frequency = UCentralUtils.channelToFrequencyMHz(channel);
|
||||||
entry.signal = -60;
|
entry.signal = -60;
|
||||||
entry.ht_oper = htOper;
|
entry.ht_oper = htOper;
|
||||||
entry.vht_oper = vhtOper;
|
entry.vht_oper = vhtOper;
|
||||||
|
entry.unixTimeMs = TestUtils.DEFAULT_WIFISCANENTRY_TIME.toEpochMilli();
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a list of wifi scan entries with the given channels
|
* Create a list of wifi scan entries with the given channels and channel width
|
||||||
* and channel width info (in the format of HT operation and VHT operation).
|
* info (in the format of HT operation and VHT operation). It is the caller's
|
||||||
|
* responsibility to make sure {@code channels}, {@code htOper}, and
|
||||||
|
* {@code vhtOper} are consistent.
|
||||||
*/
|
*/
|
||||||
public static List<WifiScanEntry> createWifiScanListWithWidth(
|
public static List<WifiScanEntry> createWifiScanListWithWidth(String bssid, List<Integer> channels,
|
||||||
List<Integer> channels,
|
List<String> htOper, List<String> vhtOper) {
|
||||||
List<String> htOper,
|
|
||||||
List<String> vhtOper
|
|
||||||
) {
|
|
||||||
List<WifiScanEntry> wifiScanResults = new ArrayList<>();
|
List<WifiScanEntry> wifiScanResults = new ArrayList<>();
|
||||||
for (int i = 0; i < channels.size(); i++) {
|
for (int i = 0; i < channels.size(); i++) {
|
||||||
WifiScanEntry wifiScanResult = createWifiScanEntryWithWidth(
|
WifiScanEntry wifiScanResult = createWifiScanEntryWithWidth(
|
||||||
channels.get(i),
|
bssid,
|
||||||
((i >= htOper.size()) ? null : htOper.get(i)),
|
channels.get(i),
|
||||||
((i >= vhtOper.size()) ? null : vhtOper.get(i))
|
((i >= htOper.size()) ? null : htOper.get(i)),
|
||||||
|
((i >= vhtOper.size()) ? null : vhtOper.get(i))
|
||||||
);
|
);
|
||||||
wifiScanResults.add(wifiScanResult);
|
wifiScanResults.add(wifiScanResult);
|
||||||
}
|
}
|
||||||
@@ -141,10 +149,8 @@ public class TestUtils {
|
|||||||
public static WifiScanEntry createWifiScanEntryWithBssid(
|
public static WifiScanEntry createWifiScanEntryWithBssid(
|
||||||
int channel, String bssid
|
int channel, String bssid
|
||||||
) {
|
) {
|
||||||
WifiScanEntry entry = new WifiScanEntry();
|
WifiScanEntry entry = createWifiScanEntry(channel);
|
||||||
entry.channel = channel;
|
|
||||||
entry.bssid = bssid;
|
entry.bssid = bssid;
|
||||||
entry.signal = -60;
|
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.facebook.openwifirrm.ucentral.operationelement;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
public class HTOperationElementTest {
|
||||||
|
@Test
|
||||||
|
void testGetHtOper() {
|
||||||
|
String htOper = "AQAEAAAAAAAAAAAAAAAAAAAAAAAAAA==";
|
||||||
|
HTOperationElement htOperObj = new HTOperationElement(htOper);
|
||||||
|
byte expectedPrimaryChannel = 1;
|
||||||
|
byte expectedSecondaryChannelOffset = 0;
|
||||||
|
boolean expectedStaChannelWidth = false;
|
||||||
|
boolean expectedRifsMode = false;
|
||||||
|
byte expectedHtProtection = 0;
|
||||||
|
boolean expectedNongreenfieldHtStasPresent = true;
|
||||||
|
boolean expectedObssNonHtStasPresent = false;
|
||||||
|
byte expectedChannelCenterFrequencySegment2 = 0;
|
||||||
|
boolean expectedDualBeacon = false;
|
||||||
|
boolean expectedDualCtsProtection = false;
|
||||||
|
boolean expectedStbcBeacon = false;
|
||||||
|
HTOperationElement expectedHtOperObj = new HTOperationElement(expectedPrimaryChannel,
|
||||||
|
expectedSecondaryChannelOffset,
|
||||||
|
expectedStaChannelWidth, expectedRifsMode, expectedHtProtection, expectedNongreenfieldHtStasPresent,
|
||||||
|
expectedObssNonHtStasPresent, expectedChannelCenterFrequencySegment2, expectedDualBeacon,
|
||||||
|
expectedDualCtsProtection, expectedStbcBeacon);
|
||||||
|
assertEquals(expectedHtOperObj, htOperObj);
|
||||||
|
|
||||||
|
htOper = "JAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==";
|
||||||
|
htOperObj = new HTOperationElement(htOper);
|
||||||
|
// all fields except the primary channel and nongreenfield field are the same
|
||||||
|
expectedPrimaryChannel = 36;
|
||||||
|
expectedNongreenfieldHtStasPresent = false;
|
||||||
|
expectedHtOperObj = new HTOperationElement(expectedPrimaryChannel, expectedSecondaryChannelOffset,
|
||||||
|
expectedStaChannelWidth, expectedRifsMode, expectedHtProtection, expectedNongreenfieldHtStasPresent,
|
||||||
|
expectedObssNonHtStasPresent, expectedChannelCenterFrequencySegment2, expectedDualBeacon,
|
||||||
|
expectedDualCtsProtection, expectedStbcBeacon);
|
||||||
|
assertEquals(expectedHtOperObj, htOperObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.facebook.openwifirrm.ucentral.operationelement;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
public class VHTOperationElementTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetVhtOper() {
|
||||||
|
String vhtOper = "ACQAAAA=";
|
||||||
|
VHTOperationElement vhtOperObj = new VHTOperationElement(vhtOper);
|
||||||
|
boolean expectedChannelWidthIndicator = false; // 20 MHz channel width
|
||||||
|
byte expectedChannel1 = 36;
|
||||||
|
byte expectedChannel2 = 0;
|
||||||
|
byte[] expectedVhtMcsForNss = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 };
|
||||||
|
VHTOperationElement expectedVhtOperObj = new VHTOperationElement(expectedChannelWidthIndicator, expectedChannel1, expectedChannel2, expectedVhtMcsForNss);
|
||||||
|
assertEquals(expectedVhtOperObj, vhtOperObj);
|
||||||
|
|
||||||
|
vhtOper = "AToAUAE=";
|
||||||
|
vhtOperObj = new VHTOperationElement(vhtOper);
|
||||||
|
expectedChannelWidthIndicator = true; // 80 MHz channel width
|
||||||
|
expectedChannel1 = 58;
|
||||||
|
// same channel2
|
||||||
|
expectedVhtMcsForNss = new byte[] { 1, 1, 0, 0, 0, 0, 0, 1 };
|
||||||
|
expectedVhtOperObj = new VHTOperationElement(expectedChannelWidthIndicator, expectedChannel1, expectedChannel2,
|
||||||
|
expectedVhtMcsForNss);
|
||||||
|
assertEquals(expectedVhtOperObj, vhtOperObj);
|
||||||
|
|
||||||
|
vhtOper = "ASoyUAE=";
|
||||||
|
vhtOperObj = new VHTOperationElement(vhtOper);
|
||||||
|
// same channel width indicator (160 MHz channel width)
|
||||||
|
expectedChannel1 = 42;
|
||||||
|
expectedChannel2 = 50;
|
||||||
|
// same vhtMcsForNss
|
||||||
|
expectedVhtOperObj = new VHTOperationElement(expectedChannelWidthIndicator, expectedChannel1, expectedChannel2,
|
||||||
|
expectedVhtMcsForNss);
|
||||||
|
assertEquals(expectedVhtOperObj, vhtOperObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user