Aggregate wifiscans (#25)

This commit is contained in:
RockyMandayam2
2022-08-17 16:18:19 -07:00
committed by GitHub
parent b73017d32d
commit fb602c8b4b
19 changed files with 1203 additions and 90 deletions

View File

@@ -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();
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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;
} }

View File

@@ -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;
}
} }

View File

@@ -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) {

View File

@@ -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(

View File

@@ -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;

View File

@@ -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);
} }
} }
} }

View File

@@ -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.");
}
}
} }

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}

View File

@@ -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));
}
} }

View File

@@ -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==",

View File

@@ -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;
} }

View File

@@ -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);
}
}

View File

@@ -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);
}
}