20 Commits

Author SHA1 Message Date
Johann Hoffmann
9e22796758 Update checkout action version (#132)
Signed-off-by: Johann Hoffmann <johann.hoffmann@mailbox.org>
2022-11-28 17:32:48 -08:00
Jeffrey Han
f1c488eac8 Add package-info.java and other class-level javadocs (#131)
Signed-off-by: Jeffrey Han <39203126+elludraon@users.noreply.github.com>
2022-11-23 13:46:04 -08:00
Jeffrey Han
066c523df5 Add UCentralSchema and convert Modeler.DataModel.latestDeviceStatusRadios (#130) 2022-11-23 11:19:57 -08:00
zhiqiand
ba8c156e72 Data Models for RCA (#125)
Signed-off-by: zhiqiand <zhiqian@fb.com>
Signed-off-by: Jeffrey Han <39203126+elludraon@users.noreply.github.com>
Co-authored-by: Jeffrey Han <39203126+elludraon@users.noreply.github.com>
2022-11-21 18:01:44 -08:00
Jeffrey Han
d73eb23920 Remove device config schema copy in ConfigManager (#129)
Signed-off-by: Jeffrey Han <39203126+elludraon@users.noreply.github.com>
2022-11-21 16:09:40 -08:00
Jeffrey Han
ac5a1c8887 Client steering notes, docs, fixes (#128)
Signed-off-by: Jeffrey Han <39203126+elludraon@users.noreply.github.com>
2022-11-14 12:55:43 -08:00
Jeffrey Han
ea3a13e98c Fix owprov API handler /inventory/<serial>?rrmSettings=true (#127)
Signed-off-by: Jeffrey Han <39203126+elludraon@users.noreply.github.com>
2022-11-14 11:00:23 -08:00
Jun Woo Shin
c1511e8e91 More IE parsing (#95)
Signed-off-by: Jun Woo Shin <jwoos@fb.com>
Signed-off-by: Jeffrey Han <39203126+elludraon@users.noreply.github.com>
Co-authored-by: Jeffrey Han <39203126+elludraon@users.noreply.github.com>
2022-11-11 16:49:51 -08:00
Jeffrey Han
05c36a535f Fix several compile errors/warnings (#126) 2022-11-11 11:07:12 -08:00
zhiqiand
c94c31cb63 Refactor AggregatedState and add timestamp (#118)
* refactor AggregatedState and add timestamp

Signed-off-by: zhiqiand <zhiqian@fb.com>

* Update gitignore (#120)

Signed-off-by: Jeffrey Han <39203126+elludraon@users.noreply.github.com>

* Add CDS for hotspot and ability to add extra JVM flags in runner (#124)

* address some comments

Signed-off-by: zhiqiand <zhiqian@fb.com>

* refactor AggregatedState and add timestamp

Signed-off-by: zhiqiand <zhiqian@fb.com>

* address some comments

Signed-off-by: zhiqiand <zhiqian@fb.com>

* deep copy via gson in State Constructor

Signed-off-by: zhiqiand <zhiqian@fb.com>

* address some comments

Signed-off-by: zhiqiand <zhiqian@fb.com>

* address some comments

Signed-off-by: zhiqiand <zhiqian@fb.com>

Signed-off-by: zhiqiand <zhiqian@fb.com>
Signed-off-by: Jeffrey Han <39203126+elludraon@users.noreply.github.com>
Co-authored-by: Jeffrey Han <39203126+elludraon@users.noreply.github.com>
Co-authored-by: Jun Woo Shin <jwoos@users.noreply.github.com>
2022-11-08 15:30:17 -08:00
RockyMandayam2
404934eda9 Prepare for client steering (#122) 2022-11-07 14:29:13 -08:00
RockyMandayam2
80626388c8 Add rca params (#123) 2022-11-07 13:31:51 -08:00
Jun Woo Shin
a79359c69d Add CDS for hotspot and ability to add extra JVM flags in runner (#124) 2022-11-03 13:30:20 -04:00
Jeffrey Han
a638d70fd6 Update gitignore (#120)
Signed-off-by: Jeffrey Han <39203126+elludraon@users.noreply.github.com>
2022-11-02 13:31:02 -07:00
Jeffrey Han
e3705699b4 Add station pinger to RRM service (#116)
Signed-off-by: Jeffrey Han <39203126+elludraon@users.noreply.github.com>
2022-11-01 15:12:44 -07:00
RockyMandayam2
35eddf73cf Add lib-rca sub module (#113) 2022-10-31 15:44:33 -07:00
Jun Woo Shin
f031027684 JVM options for strings (#112) 2022-10-28 13:15:19 -04:00
Jun Woo Shin
a54f9a48be Move running image to ibm-semeru and introduce runner script (#111)
Signed-off-by: Jun Woo Shin <jwoos@meta.com>
2022-10-26 16:37:43 -04:00
Jeffrey Han
c22ebeea31 Add script API and some related utilities (#109) 2022-10-24 10:57:35 -07:00
Jun Woo Shin
e1b9052ecc Create separate Capabilities structure (#108) 2022-10-21 12:01:45 -04:00
97 changed files with 5184 additions and 694 deletions

View File

@@ -26,7 +26,7 @@ jobs:
DOCKER_REGISTRY_USERNAME: ucentral
steps:
- name: Checkout actions repo
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
repository: Telecominfraproject/.github
path: github
@@ -57,7 +57,7 @@ jobs:
- docker
steps:
- name: Checkout actions repo
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
repository: Telecominfraproject/.github
path: github

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout actions repo
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
repository: Telecominfraproject/.github
path: github

View File

@@ -17,7 +17,7 @@ jobs:
HELM_REPO_USERNAME: ucentral
steps:
- name: Checkout uCentral assembly chart repo
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
path: wlan-cloud-rrm

11
.gitignore vendored
View File

@@ -1,6 +1,12 @@
/target/
*/target/
# owrrm specific
*.log*
device_config.json
settings.json
topology.json
# Eclipse
.settings/
bin/
@@ -14,3 +20,8 @@ bin/
*.iml
*.iws
*.ipr
# Miscellaneous files thzt should not be checked in
temp/
.DS_Store

View File

@@ -3,18 +3,20 @@ WORKDIR /usr/src/java
COPY . .
RUN mvn clean package -pl owrrm -am -DappendVersionString="$(./scripts/get_build_version.sh)"
FROM adoptopenjdk/openjdk11-openj9:latest
RUN apt-get update && apt-get install -y gettext-base wget
RUN wget https://raw.githubusercontent.com/Telecominfraproject/wlan-cloud-ucentral-deploy/main/docker-compose/certs/restapi-ca.pem \
-O /usr/local/share/ca-certificates/restapi-ca-selfsigned.pem
FROM ibm-semeru-runtimes:open-11-jre
RUN apt-get update && apt-get install -y gettext-base
ADD https://raw.githubusercontent.com/Telecominfraproject/wlan-cloud-ucentral-deploy/main/docker-compose/certs/restapi-ca.pem \
/usr/local/share/ca-certificates/restapi-ca-selfsigned.pem
RUN mkdir /owrrm-data
WORKDIR /usr/src/java
COPY docker-entrypoint.sh /
COPY runner.sh /
COPY --from=build /usr/src/java/owrrm/target/openwifi-rrm.jar /usr/local/bin/
EXPOSE 16789 16790
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["java", "-XX:+IdleTuningGcOnIdle", "-Xtune:virtualized", \
"-jar", "/usr/local/bin/openwifi-rrm.jar", \
"run", "--config-env", \
"-t", "/owrrm-data/topology.json", \
"-d", "/owrrm-data/device_config.json"]
ENV JVM_IMPL=openj9
CMD ["/runner.sh", "java", "/usr/local/bin/openwifi-rrm.jar", \
"run", \
"--config-env", \
"-t", "/owrrm-data/topology.json", \
"-d", "/owrrm-data/device_config.json"]

28
Dockerfile-hotspot Normal file
View File

@@ -0,0 +1,28 @@
FROM maven:3-eclipse-temurin-11 as build
WORKDIR /usr/src/java
COPY . .
RUN mvn clean package -pl owrrm -am -DappendVersionString="$(./scripts/get_build_version.sh)"
FROM eclipse-temurin:11-jre-jammy
RUN apt-get update && apt-get install -y gettext-base
ADD https://raw.githubusercontent.com/Telecominfraproject/wlan-cloud-ucentral-deploy/main/docker-compose/certs/restapi-ca.pem \
/usr/local/share/ca-certificates/restapi-ca-selfsigned.pem
RUN mkdir /owrrm-data
WORKDIR /usr/src/java
COPY docker-entrypoint.sh /
COPY runner.sh /
COPY --from=build /usr/src/java/owrrm/target/openwifi-rrm.jar /usr/local/bin/
# generate Application CDS
RUN java -Xshare:off -XX:DumpLoadedClassList=static-cds.lst -jar /usr/local/bin/openwifi-rrm.jar --help && \
java -Xshare:dump -XX:SharedClassListFile=static-cds.lst -XX:SharedArchiveFile=static-cds.jsa -jar /usr/local/bin/openwifi-rrm.jar
EXPOSE 16789 16790
ENTRYPOINT ["/docker-entrypoint.sh"]
ENV JVM_IMPL=hotspot
ENV EXTRA_JVM_FLAGS="-XX:SharedArchiveFile=static-cds.jsa -Xshare:auto"
CMD ["/runner.sh", "java", "/usr/local/bin/openwifi-rrm.jar", \
"run", \
"--config-env", \
"-t", "/owrrm-data/topology.json", \
"-d", "/owrrm-data/device_config.json"]

View File

@@ -10,11 +10,12 @@ package com.facebook.openwifi.cloudsdk;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import com.facebook.openwifi.cloudsdk.models.ap.State;
import com.facebook.openwifi.cloudsdk.models.ap.State.Interface.Counters;
import com.facebook.openwifi.cloudsdk.models.ap.State.Interface.SSID.Association;
import com.facebook.openwifi.cloudsdk.models.ap.State.Interface.SSID.Association.Rate;
import com.google.gson.JsonObject;
/**
* Aggregation model for State aggregation. Only contains info useful for
@@ -22,76 +23,32 @@ import com.facebook.openwifi.cloudsdk.models.ap.State.Interface.SSID.Association
*/
public class AggregatedState {
/** Rate information with aggregated fields. */
public static class AggregatedRate {
/**
* This is the common bitRate for all the aggregated fields.
*/
public long bitRate;
/**
* This is the common channel width for all the aggregated fields.
*/
public int chWidth;
/**
* Aggregated fields mcs
*/
public List<Integer> mcs = new ArrayList<>();
/** Constructor with no args */
private AggregatedRate() {}
/** Add a Rate to the AggregatedRate */
private void add(Rate rate) {
if (rate == null) {
return;
}
if (mcs.isEmpty()) {
bitRate = rate.bitrate;
chWidth = rate.chwidth;
}
mcs.add(rate.mcs);
}
/**
* Add an AggregatedRate with the same channel_width to the
* AggregatedRate
*/
private void add(AggregatedRate rate) {
if (rate == null || rate.chWidth != chWidth) {
return;
}
if (mcs.isEmpty()) {
bitRate = rate.bitRate;
chWidth = rate.chWidth;
}
mcs.addAll(rate.mcs);
}
}
/**
* Radio information with channel, channel_width and tx_power.
*/
public static class Radio {
public static class RadioConfig {
public int channel;
public int channelWidth;
public int txPower;
public String phy;
private Radio() {}
/** Default constructor with no args */
private RadioConfig() {}
public Radio(int channel, int channelWidth, int txPower) {
/** Constructor with args */
public RadioConfig(JsonObject radio) {
this.channel = radio.get("channel").getAsInt();
this.channelWidth = radio.get("channel_width").getAsInt();
this.txPower = radio.get("tx_power").getAsInt();
this.phy = radio.get("phy").getAsString();
}
public RadioConfig(int channel, int channelWidth, int txPower) {
this.channel = channel;
this.channelWidth = channelWidth;
this.txPower = txPower;
}
private Radio(Map<String, Integer> radioInfo) {
channel = radioInfo.getOrDefault("channel", -1);
channelWidth = radioInfo.getOrDefault("channel_width", -1);
txPower = radioInfo.getOrDefault("tx_power", -1);
}
@Override
public int hashCode() {
return Objects.hash(channel, channelWidth, txPower);
@@ -109,64 +66,161 @@ public class AggregatedState {
return false;
}
Radio other = (Radio) obj;
RadioConfig other = (RadioConfig) obj;
return channel == other.channel &&
channelWidth == other.channelWidth && txPower == other.txPower;
}
}
public String bssid;
public String station;
public long connected;
public long inactive;
public List<Integer> rssi;
public long rxBytes;
public long rxPackets;
public AggregatedRate rxRate;
public long txBytes;
public long txDuration;
public long txFailed;
public long txPackets;
public AggregatedRate txRate;
public long txRetries;
public int ackSignal;
public int ackSignalAvg;
public Radio radio;
/**
* Data model to keep raw data from {@link State.Interface.SSID.Association},
* {@link State.Radio} and {@link State.Interface.Counters}.
*/
public static class AssociationInfo {
/** Rate information with aggregated fields. */
public static class Rate {
/**
* Aggregated fields bitRate
*/
public long bitRate;
/** Constructor with no args */
public AggregatedState() {
this.rxRate = new AggregatedRate();
this.txRate = new AggregatedRate();
this.rssi = new ArrayList<>();
this.radio = new Radio();
/**
* Aggregated fields chWidth
*/
public int chWidth;
/**
* Aggregated fields mcs
*/
public int mcs;
/** Constructor with no args */
private Rate() {}
/** Constructor with args */
private Rate(long bitRate, int chWidth, int mcs) {
this.bitRate = bitRate;
this.chWidth = chWidth;
this.mcs = mcs;
}
}
public long connected;
public long inactive;
public int rssi;
public long rxBytes;
public long rxPackets;
public Rate rxRate;
public long txBytes;
public long txDuration;
public long txFailed;
public long txPackets;
public Rate txRate;
public long txRetries;
public int ackSignal;
public int ackSignalAvg;
public long txPacketsCounters;
public long txErrorsCounters;
public long txDroppedCounters;
public long activeMsRadio;
public long busyMsRadio;
public long noiseRadio;
public long receiveMsRadio;
public long transmitMsRadio;
/** unix time in ms */
public long timestamp;
/** Default Constructor. */
public AssociationInfo() {}
/** Constructor with only rssi(for test purpose) */
public AssociationInfo(int rssi) {
this.rssi = rssi;
}
/** Constructor with args */
public AssociationInfo(
Association association,
Counters counters,
JsonObject radio,
long timestamp
) {
// Association info
connected = association.connected;
inactive = association.inactive;
rssi = association.rssi;
rxBytes = association.rx_bytes;
rxPackets = association.rx_packets;
if (association.rx_rate != null) {
rxRate = new Rate(
association.rx_rate.bitrate,
association.rx_rate.chwidth,
association.rx_rate.mcs
);
} else {
rxRate = new Rate();
}
txBytes = association.tx_bytes;
txPackets = association.tx_packets;
if (association.tx_rate != null) {
txRate = new Rate(
association.tx_rate.bitrate,
association.tx_rate.chwidth,
association.tx_rate.mcs
);
} else {
txRate = new Rate();
}
txRetries = association.tx_retries;
ackSignal = association.ack_signal;
ackSignalAvg = association.ack_signal_avg;
//Counters info
txPacketsCounters = counters.tx_packets;
txErrorsCounters = counters.tx_errors;
txDroppedCounters = counters.tx_dropped;
// Radio info
activeMsRadio = radio.get("active_ms").getAsLong();
busyMsRadio = radio.get("busy_ms").getAsLong();
transmitMsRadio = radio.get("transmit_ms").getAsLong();
receiveMsRadio = radio.get("receive_ms").getAsLong();
noiseRadio = radio.get("noise").getAsLong();
this.timestamp = timestamp;
}
}
/** Construct from Association and radio */
// Aggregate AssociationInfo over bssid, station and RadioConfig.
public String bssid;
public String station;
public RadioConfig radioConfig;
// Store a list of AssociationInfo of the same link and radio configuration. */
public List<AssociationInfo> associationInfoList;
/** Constructor with no args. For test purpose. */
public AggregatedState() {
this.associationInfoList = new ArrayList<>();
this.radioConfig = new RadioConfig();
}
/** Construct from Association, Counters, Radio and time stamp */
public AggregatedState(
Association association,
Map<String, Integer> radioInfo
Counters counters,
JsonObject radio,
long timestamp
) {
this.rxRate = new AggregatedRate();
this.txRate = new AggregatedRate();
this.rssi = new ArrayList<>();
this.bssid = association.bssid;
this.station = association.station;
this.connected = association.connected;
this.inactive = association.inactive;
this.rssi.add(association.rssi);
this.rxBytes = association.rx_bytes;
this.rxPackets = association.rx_packets;
this.rxRate.add(association.rx_rate);
this.txBytes = association.tx_bytes;
this.txDuration = association.tx_duration;
this.txFailed = association.tx_failed;
this.txPackets = association.tx_packets;
this.txRate.add(association.tx_rate);
this.txRetries = association.tx_retries;
this.ackSignal = association.ack_signal;
this.ackSignalAvg = association.ack_signal_avg;
this.radio = new Radio(radioInfo);
this.associationInfoList = new ArrayList<>();
associationInfoList
.add(new AssociationInfo(association, counters, radio, timestamp));
this.radioConfig = new RadioConfig(radio);
}
/**
@@ -178,7 +232,7 @@ public class AggregatedState {
*/
public boolean matchesForAggregation(AggregatedState state) {
return bssid == state.bssid && station == state.station &&
Objects.equals(radio, state.radio);
Objects.equals(radioConfig, state.radioConfig);
}
/**
@@ -191,9 +245,7 @@ public class AggregatedState {
*/
public boolean add(AggregatedState state) {
if (matchesForAggregation(state)) {
this.rssi.addAll(state.rssi);
this.rxRate.add(state.rxRate);
this.txRate.add(state.txRate);
associationInfoList.addAll(state.associationInfoList);
return true;
}
return false;

View File

@@ -0,0 +1,125 @@
/*
* 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.openwifi.cloudsdk;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
/**
* Utility functions for dealing with IEs
*/
public abstract class IEUtils {
/**
* Try to get a json object as a byte
*
* @param contents the JSON object to try to parse
* @param fieldName the field name
* @return the field as a byte or null
*/
public static Byte parseOptionalByteField(
JsonObject contents,
String fieldName
) {
JsonElement element = contents.get(fieldName);
if (element == null) {
return null;
}
return element.getAsByte();
}
/**
* Try to get a json object as a short
*
* @param contents the JSON object to try to parse
* @param fieldName the field name
* @return the field as a short or null
*/
public static Short parseOptionalShortField(
JsonObject contents,
String fieldName
) {
JsonElement element = contents.get(fieldName);
if (element == null) {
return null;
}
return element.getAsShort();
}
/**
* Try to get a json object as a int
*
* @param contents the JSON object to try to parse
* @param fieldName the field name
* @return the field as a int or null
*/
public static Integer parseOptionalIntField(
JsonObject contents,
String fieldName
) {
JsonElement element = contents.get(fieldName);
if (element == null) {
return null;
}
return element.getAsInt();
}
/**
* Try to get a json object as a int
*
* @param contents the JSON object to try to parse
* @param fieldName the field name
* @return the field as a int (0 if key not present)
*/
public static Integer parseIntField(
JsonObject contents,
String fieldName
) {
JsonElement element = contents.get(fieldName);
if (element == null) {
return 0;
}
return element.getAsInt();
}
/**
* Try to get a json object as a string
*
* @param contents the JSON object to try to parse
* @param fieldName the field name
* @return the field as a string or null
*/
public static String parseOptionalStringField(
JsonObject contents,
String fieldName
) {
JsonElement element = contents.get(fieldName);
if (element == null) {
return null;
}
return element.getAsString();
}
/**
* Try to get a json object as a boolean when represented as a number (0, 1)
*
* @param contents the JSON object to try to parse
* @param fieldName the field name
* @return the field as a boolean (false if key not present)
*/
public static boolean parseBooleanNumberField(
JsonObject contents,
String fieldName
) {
JsonElement element = contents.get(fieldName);
if (element == null) {
return false;
}
return element.getAsInt() > 0;
}
}

View File

@@ -13,38 +13,42 @@ import java.util.Objects;
import com.facebook.openwifi.cloudsdk.ies.Country;
import com.facebook.openwifi.cloudsdk.ies.LocalPowerConstraint;
import com.facebook.openwifi.cloudsdk.ies.QbssLoad;
import com.facebook.openwifi.cloudsdk.ies.RMEnabledCapabilities;
import com.facebook.openwifi.cloudsdk.ies.TxPwrInfo;
/** Wrapper class containing information elements */
public final class InformationElements {
public Country country;
public QbssLoad qbssLoad;
public LocalPowerConstraint localPowerConstraint;
public TxPwrInfo txPwrInfo;
public RMEnabledCapabilities rmEnabledCapabilities;
@Override
public int hashCode() {
return Objects.hash(country, localPowerConstraint, qbssLoad, txPwrInfo);
return Objects.hash(
country,
localPowerConstraint,
qbssLoad,
rmEnabledCapabilities,
txPwrInfo
);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
if (this == obj)
return true;
}
if (obj == null) {
if (obj == null)
return false;
}
if (getClass() != obj.getClass()) {
if (getClass() != obj.getClass())
return false;
}
InformationElements other = (InformationElements) obj;
return Objects.equals(country, other.country) && Objects.equals(
localPowerConstraint,
other.localPowerConstraint
) && Objects.equals(qbssLoad, other.qbssLoad) &&
return Objects.equals(country, other.country) &&
Objects.equals(localPowerConstraint, other.localPowerConstraint) &&
Objects.equals(qbssLoad, other.qbssLoad) &&
Objects
.equals(rmEnabledCapabilities, other.rmEnabledCapabilities) &&
Objects.equals(txPwrInfo, other.txPwrInfo);
}
}

View File

@@ -0,0 +1,19 @@
/*
* 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.openwifi.cloudsdk;
import com.facebook.openwifi.cloudsdk.models.ap.State;
public class StateInfo extends State {
/**
* Unix time in milliseconds (ms). This is added it because State.unit.localtime is an unknown
* time reference.
*/
public long timestamp;
}

View File

@@ -8,13 +8,18 @@
package com.facebook.openwifi.cloudsdk;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.facebook.openwifi.cloudsdk.models.ap.UCentralSchema;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;
/**
* Wrapper around uCentral AP configuration.
@@ -51,33 +56,33 @@ public class UCentralApConfiguration {
return config.getAsJsonArray("radios").size();
}
/** Return all info in the radio config (or an empty array if none). */
public JsonArray getRadioConfigList() {
if (!config.has("radios") || !config.get("radios").isJsonArray()) {
return new JsonArray();
/** Return all info in the radio config (or an empty list if none). */
public List<UCentralSchema.Radio> getRadioConfigList() {
if (config.has("radios") && config.get("radios").isJsonArray()) {
List<UCentralSchema.Radio> radios = new Gson().fromJson(
config.getAsJsonArray("radios"),
new TypeToken<ArrayList<UCentralSchema.Radio>>() {}.getType()
);
if (radios != null) {
return radios;
}
}
return config.getAsJsonArray("radios");
return Collections.emptyList();
}
/** Return all the operational bands of an AP (from the radio config) */
public Set<String> getRadioBandsSet(JsonArray radioConfigList) {
public Set<String> getRadioBandsSet(
List<UCentralSchema.Radio> radioConfigList
) {
Set<String> radioBandsSet = new HashSet<>();
if (radioConfigList == null) {
return radioBandsSet;
}
for (
int radioIndex = 0; radioIndex < radioConfigList.size();
radioIndex++
) {
JsonElement e = radioConfigList.get(radioIndex);
if (!e.isJsonObject()) {
for (UCentralSchema.Radio radio : radioConfigList) {
if (radio == null || radio.band == null) {
continue;
}
JsonObject radioObject = e.getAsJsonObject();
if (!radioObject.has("band")) {
continue;
}
radioBandsSet.add(radioObject.get("band").getAsString());
radioBandsSet.add(radio.band);
}
return radioBandsSet;
}

View File

@@ -25,6 +25,7 @@ import com.facebook.openwifi.cloudsdk.models.gw.DeviceCapabilities;
import com.facebook.openwifi.cloudsdk.models.gw.DeviceConfigureRequest;
import com.facebook.openwifi.cloudsdk.models.gw.DeviceListWithStatus;
import com.facebook.openwifi.cloudsdk.models.gw.DeviceWithStatus;
import com.facebook.openwifi.cloudsdk.models.gw.ScriptRequest;
import com.facebook.openwifi.cloudsdk.models.gw.ServiceEvent;
import com.facebook.openwifi.cloudsdk.models.gw.StatisticsRecords;
import com.facebook.openwifi.cloudsdk.models.gw.SystemInfoResults;
@@ -562,6 +563,71 @@ public class UCentralClient {
}
}
/**
* Run a shell script on a device and return the result, or null upon error.
*
* @see #runScript(String, String, int)
*/
public CommandInfo runScript(String serialNumber, String script) {
return runScript(serialNumber, script, 30);
}
/**
* Run a shell script on a device and return the result, or null upon error.
*
* @see #runScript(String, String, int, String)
*/
public CommandInfo runScript(
String serialNumber,
String script,
int timeoutSec
) {
return runScript(serialNumber, script, timeoutSec, "shell");
}
/**
* Run a script on a device and return the result, or null upon error.
*
* @param serialNumber the device
* @param script the script contents
* @param timeoutSec the timeout in seconds
* @param type the script type (either "shell" or "ucode")
*
* @see UCentralUtils#getScriptOutput(CommandInfo)
*/
public CommandInfo runScript(
String serialNumber,
String script,
int timeoutSec,
String type
) {
ScriptRequest req = new ScriptRequest();
req.serialNumber = serialNumber;
req.timeout = timeoutSec;
req.type = type;
req.script = script;
req.scriptId = "1"; // ??
HttpResponse<String> response = httpPost(
String.format("device/%s/script", serialNumber),
OWGW_SERVICE,
req
);
if (!response.isSuccess()) {
logger.error("Error: {}", response.getBody());
return null;
}
try {
return gson.fromJson(response.getBody(), CommandInfo.class);
} catch (JsonSyntaxException e) {
String errMsg = String.format(
"Failed to deserialize to CommandInfo: %s",
response.getBody()
);
logger.error(errMsg, e);
return null;
}
}
/** Retrieve a list of inventory from owprov. */
public InventoryTagList getProvInventory() {
HttpResponse<String> response = httpGet("inventory", OWPROV_SERVICE);
@@ -624,6 +690,19 @@ public class UCentralClient {
try {
return gson.fromJson(response.getBody(), RRMDetails.class);
} catch (JsonSyntaxException e) {
// catch strings like "no", "inherit", "invalid" (???)
JSONObject respBody;
try {
respBody = new JSONObject(response.getBody());
respBody.getString("rrm");
logger.error(
"RRMDetails returned unexpected string body: {}",
respBody
);
return null;
} catch (JSONException e2) { /* ignore and fall through */}
// otherwise, log as a deserialization error
String errMsg = String.format(
"Failed to deserialize to RRMDetails: %s",
response.getBody()

View File

@@ -8,8 +8,10 @@
package com.facebook.openwifi.cloudsdk;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@@ -17,6 +19,8 @@ import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -24,8 +28,12 @@ import org.slf4j.LoggerFactory;
import com.facebook.openwifi.cloudsdk.ies.Country;
import com.facebook.openwifi.cloudsdk.ies.LocalPowerConstraint;
import com.facebook.openwifi.cloudsdk.ies.QbssLoad;
import com.facebook.openwifi.cloudsdk.ies.RMEnabledCapabilities;
import com.facebook.openwifi.cloudsdk.ies.TxPwrInfo;
import com.facebook.openwifi.cloudsdk.models.ap.Capabilities;
import com.facebook.openwifi.cloudsdk.models.ap.State;
import com.facebook.openwifi.cloudsdk.models.ap.UCentralSchema;
import com.facebook.openwifi.cloudsdk.models.gw.CommandInfo;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
@@ -166,6 +174,10 @@ public class UCentralUtils {
case TxPwrInfo.TYPE:
ieContainer.txPwrInfo = TxPwrInfo.parse(contents);
break;
case RMEnabledCapabilities.TYPE:
ieContainer.rmEnabledCapabilities =
RMEnabledCapabilities.parse(contents);
break;
}
} catch (Exception e) {
logger.error(String.format("Skipping invalid IE %s", ie), e);
@@ -268,28 +280,20 @@ public class UCentralUtils {
* Returns the results map
*/
public static Map<String, List<String>> getBandsMap(
Map<String, JsonArray> deviceStatus
Map<String, List<UCentralSchema.Radio>> deviceStatus
) {
Map<String, List<String>> bandsMap = new HashMap<>();
for (String serialNumber : deviceStatus.keySet()) {
JsonArray radioList =
deviceStatus.get(serialNumber).getAsJsonArray();
for (
int radioIndex = 0; radioIndex < radioList.size(); radioIndex++
) {
JsonElement e = radioList.get(radioIndex);
if (!e.isJsonObject()) {
return null;
}
JsonObject radioObject = e.getAsJsonObject();
String band = radioObject.get("band").getAsString();
for (
Map.Entry<String, List<UCentralSchema.Radio>> entry : deviceStatus
.entrySet()
) {
String serialNumber = entry.getKey();
for (UCentralSchema.Radio radio : entry.getValue()) {
bandsMap
.computeIfAbsent(band, k -> new ArrayList<>())
.computeIfAbsent(radio.band, k -> new ArrayList<>())
.add(serialNumber);
}
}
return bandsMap;
}
@@ -303,75 +307,61 @@ public class UCentralUtils {
* @return the results map of {band, {device, list of available channels}}
*/
public static Map<String, Map<String, List<Integer>>> getDeviceAvailableChannels(
Map<String, JsonArray> deviceStatus,
Map<String, JsonObject> deviceCapabilities,
Map<String, List<UCentralSchema.Radio>> deviceStatus,
Map<String, Map<String, Capabilities.Phy>> deviceCapabilities,
Map<String, List<Integer>> defaultAvailableChannels
) {
Map<String, Map<String, List<Integer>>> deviceAvailableChannels =
new HashMap<>();
for (String serialNumber : deviceStatus.keySet()) {
JsonArray radioList =
deviceStatus.get(serialNumber).getAsJsonArray();
for (
int radioIndex = 0; radioIndex < radioList.size(); radioIndex++
) {
JsonElement e = radioList.get(radioIndex);
if (!e.isJsonObject()) {
return null;
}
JsonObject radioObject = e.getAsJsonObject();
String band = radioObject.get("band").getAsString();
JsonObject capabilitesObject =
for (
Map.Entry<String, List<UCentralSchema.Radio>> entry : deviceStatus
.entrySet()
) {
String serialNumber = entry.getKey();
for (UCentralSchema.Radio radio : entry.getValue()) {
Map<String, Capabilities.Phy> capabilitiesPhyMap =
deviceCapabilities.get(serialNumber);
List<Integer> availableChannels = new ArrayList<>();
if (capabilitesObject == null) {
if (capabilitiesPhyMap == null) {
availableChannels
.addAll(defaultAvailableChannels.get(band));
.addAll(defaultAvailableChannels.get(radio.band));
} else {
Set<Entry<String, JsonElement>> entrySet = capabilitesObject
.entrySet();
for (Map.Entry<String, JsonElement> f : entrySet) {
String bandInsideObject = f.getValue()
.getAsJsonObject()
.get("band")
.getAsString();
if (bandInsideObject.equals(band)) {
Set<Entry<String, Capabilities.Phy>> entrySet =
capabilitiesPhyMap
.entrySet();
for (Map.Entry<String, Capabilities.Phy> f : entrySet) {
Capabilities.Phy phy = f.getValue();
String bandInsideObject = phy.band.toString();
if (bandInsideObject.equals(radio.band)) {
// (TODO) Remove the following dfsChannels code block
// when the DFS channels are available
Set<Integer> dfsChannels = new HashSet<>();
try {
JsonArray channelInfo = f.getValue()
.getAsJsonObject()
.get("dfs_channels")
.getAsJsonArray();
for (JsonElement d : channelInfo) {
dfsChannels.add(d.getAsInt());
int[] channelInfo = phy.dfs_channels;
for (int d : channelInfo) {
dfsChannels.add(d);
}
} catch (Exception d) {}
try {
JsonArray channelInfo = f.getValue()
.getAsJsonObject()
.get("channels")
.getAsJsonArray();
for (JsonElement c : channelInfo) {
int channel = c.getAsInt();
int[] channelInfo = phy.channels;
for (int channel : channelInfo) {
if (!dfsChannels.contains(channel)) {
availableChannels.add(channel);
}
}
} catch (Exception c) {
availableChannels
.addAll(defaultAvailableChannels.get(band));
.addAll(
defaultAvailableChannels.get(radio.band)
);
}
}
}
}
deviceAvailableChannels.computeIfAbsent(
band,
radio.band,
k -> new HashMap<>()
)
.put(
@@ -390,10 +380,10 @@ public class UCentralUtils {
* Returns the results map
*/
public static Map<String, String> getBssidsMap(
Map<String, State> latestState
Map<String, ? extends State> latestState
) {
Map<String, String> bssidMap = new HashMap<>();
for (Map.Entry<String, State> e : latestState.entrySet()) {
for (Entry<String, ? extends State> e : latestState.entrySet()) {
State state = e.getValue();
for (
int interfaceIndex = 0;
@@ -485,4 +475,116 @@ public class UCentralUtils {
return null;
}
}
/**
* Return a map of Wi-Fi client (STA) MAC addresses to the Client structure
* found for that interface. This does NOT support clients connected on
* multiple interfaces simultaneously.
*/
public static Map<String, State.Interface.Client> getWifiClientInfo(
State state
) {
Map<String, State.Interface.Client> ret = new HashMap<>();
// Aggregate over all interfaces
for (State.Interface iface : state.interfaces) {
if (iface.ssids == null || iface.clients == null) {
continue;
}
// Convert client array to map (for faster lookups)
Map<String, State.Interface.Client> ifaceMap = new HashMap<>();
for (State.Interface.Client client : iface.clients) {
ifaceMap.put(client.mac, client);
}
// Loop over all SSIDs and connected clients
for (State.Interface.SSID ssid : iface.ssids) {
if (ssid.associations == null) {
continue;
}
for (
State.Interface.SSID.Association association : ssid.associations
) {
State.Interface.Client client =
ifaceMap.get(association.station);
if (client != null) {
ret.put(association.station, client);
}
}
}
}
return ret;
}
/**
* Decompress (inflate) a UTF-8 string using ZLIB.
*
* @param compressed the compressed string
* @param uncompressedSize the uncompressed size (must be known)
*/
private static String inflate(String compressed, int uncompressedSize)
throws DataFormatException {
if (compressed == null) {
throw new NullPointerException("Null compressed string");
}
if (uncompressedSize < 0) {
throw new IllegalArgumentException("Invalid size");
}
byte[] input = compressed.getBytes(StandardCharsets.UTF_8);
byte[] output = new byte[uncompressedSize];
Inflater inflater = new Inflater();
inflater.setInput(input, 0, input.length);
inflater.inflate(output);
inflater.end();
return new String(output, StandardCharsets.UTF_8);
}
/**
* Given the result of the "script" API, return the actual script output
* (decoded/decompressed if needed), or null if the script returned an
* error.
*
* @see UCentralClient#runScript(String, String, int, String)
*/
public static String getScriptOutput(CommandInfo info) {
if (info == null || info.results == null) {
return null;
}
if (!info.results.has("status")) {
return null;
}
JsonObject status = info.results.get("status").getAsJsonObject();
if (!status.has("error")) {
return null;
}
int errorCode = status.get("error").getAsInt();
if (errorCode != 0) {
logger.error("Script failed with code {}", errorCode);
return null;
}
if (status.has("result")) {
// Raw result
return status.get("result").getAsString();
} else if (status.has("result_64") && status.has("result_sz")) {
// Base64+compressed result
// NOTE: untested, not actually implemented on ucentral-client?
try {
String encoded = status.get("result_64").getAsString();
int uncompressedSize = status.get("result_sz").getAsInt();
String decoded = new String(
Base64.getDecoder().decode(encoded),
StandardCharsets.UTF_8
);
return inflate(decoded, uncompressedSize);
} catch (Exception e) {
logger.error("Failed to decode or inflate script result", e);
}
}
return null;
}
}

View File

@@ -0,0 +1,155 @@
/*
* 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.openwifi.cloudsdk.ies;
import java.util.Objects;
import com.google.gson.JsonObject;
// NOTE: Not validated (not seen on test devices)
/**
* This information element (IE) appears in wifiscan entries. It's called "BSS AC Access Delay" in
* 802.11 specs (section 9.4.2.43). Refer to the specification for more details.
* Language in javadocs is taken from the specification.
*/
public class BssAcAccessDelay {
/** Defined in 802.11 table 9-92 */
public static final int TYPE = 68;
/**
* Subfield that goes into Access Category Access Delay field in BSS AC
* Access Delay. For information on what the values mean, check section
* 9.4.2.43
*/
public static class AccessCategoryAccessDelay {
/**
* Unsigned 8 bits that represents a scaled representation of best effort AC
* access delay
*/
public final short averageAccessDelayForBestEffort;
/**
* Unsigned 8 bits that represents a scaled representation of background AC
* access delay
*/
public final short averageAccessDelayForBackground;
/**
* Unsigned 8 bits that represents a scaled representation of video AC access
* delay
*/
public final short averageAccessDelayForVideo;
/**
* Unsigned 8 bits that represents a scaled representation of voice AC access
* delay
*/
public final short averageAccessDelayForVoice;
/** Constructor */
public AccessCategoryAccessDelay(
short averageAccessDelayForBestEffort,
short averageAccessDelayForBackground,
short averageAccessDelayForVideo,
short averageAccessDelayForVoice
) {
this.averageAccessDelayForBestEffort =
averageAccessDelayForBestEffort;
this.averageAccessDelayForBackground =
averageAccessDelayForBackground;
this.averageAccessDelayForVideo = averageAccessDelayForVideo;
this.averageAccessDelayForVoice = averageAccessDelayForVoice;
}
/** Parse AccessCategoryAccessDelay from JSON object */
// TODO rename fields as necessary - we don't know how the data format yet
public static AccessCategoryAccessDelay parse(JsonObject contents) {
return new AccessCategoryAccessDelay(
contents.get("Average Access Delay For Best Effort").getAsShort(),
contents.get("Average Access Delay For Background").getAsShort(),
contents.get("Average Access Delay For Video").getAsShort(),
contents.get("Average Access Delay For Voice").getAsShort()
);
}
@Override
public int hashCode() {
return Objects.hash(
averageAccessDelayForBestEffort,
averageAccessDelayForBestEffort,
averageAccessDelayForVideo,
averageAccessDelayForVoice
);
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (this == obj) {
return true;
}
if (getClass() != obj.getClass()) {
return false;
}
AccessCategoryAccessDelay other = (AccessCategoryAccessDelay) obj;
return averageAccessDelayForBestEffort ==
other.averageAccessDelayForBestEffort &&
averageAccessDelayForBackground ==
other.averageAccessDelayForBackground &&
averageAccessDelayForVideo ==
other.averageAccessDelayForVideo &&
averageAccessDelayForVoice == other.averageAccessDelayForVoice;
}
}
/** 32 bits - Holds AccessCategoryAccessDelay subfield */
public final AccessCategoryAccessDelay accessCategoryAccessDelay;
/** Constructor */
public BssAcAccessDelay(
AccessCategoryAccessDelay accessCategoryAccessDelay
) {
this.accessCategoryAccessDelay = accessCategoryAccessDelay;
}
/** Parse BssAcAccessDelay from JSON object */
// TODO rename fields as necessary - we don't know how the data format yet
public static BssAcAccessDelay parse(JsonObject contents) {
return new BssAcAccessDelay(
AccessCategoryAccessDelay.parse(
contents.get("AP Average Access Delay").getAsJsonObject()
)
);
}
@Override
public int hashCode() {
return Objects.hash(accessCategoryAccessDelay);
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (this == obj) {
return true;
}
if (getClass() != obj.getClass()) {
return false;
}
BssAcAccessDelay other = (BssAcAccessDelay) obj;
return accessCategoryAccessDelay == other.accessCategoryAccessDelay;
}
}

View File

@@ -0,0 +1,68 @@
/*
* 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.openwifi.cloudsdk.ies;
import java.util.Objects;
import com.google.gson.JsonObject;
// NOTE: Not validated (not seen on test devices)
/**
* This information element (IE) appears in wifiscan entries. It's called "BSS Average Access Delay" in
* 802.11 specs (section 9.4.2.38). Refer to the specification for more details.
* Language in javadocs is taken from the specification.
*/
public class BssAvgAccessDelay {
/** Defined in 802.11 table 9-92 */
public static final int TYPE = 63;
/**
* Unsigned 8 bits representing a scaled average medium access delay for all DCF
* and EDCAF frames transmitted, measured from the time it's ready for
* transmission to actual transmission start time.
*/
public final short apAvgAccessDelay;
/** Constructor */
public BssAvgAccessDelay(short apAvgAccessDelay) {
this.apAvgAccessDelay = apAvgAccessDelay;
}
/** Parse BssAvgAccessDelay from JSON object */
// TODO modify this method as necessary - since the IE doesn't seem to be
// present, we have no idea what the format looks like
public static BssAvgAccessDelay parse(JsonObject contents) {
return new BssAvgAccessDelay(
contents.get("AP Average Access Delay").getAsShort()
);
}
@Override
public int hashCode() {
return Objects.hash(apAvgAccessDelay);
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (this == obj) {
return true;
}
if (getClass() != obj.getClass()) {
return false;
}
BssAvgAccessDelay other = (BssAvgAccessDelay) obj;
return apAvgAccessDelay == other.apAvgAccessDelay;
}
}

View File

@@ -0,0 +1,87 @@
/*
* 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.openwifi.cloudsdk.ies;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
// NOTE: Not validated (not seen on test devices)
/**
* This information element (IE) appears in wifiscan entries. It's called "20/40
* BSS Intolerant Channel Report" in 802.11 specs (section 9.4.2.57). Refer to
* the specification for more details. Language in javadocs is taken from the
* specification.
*/
public class BssIntolerantChannelReport {
/** Defined in 802.11 table 9-92 */
public static final int TYPE = 73;
/**
* Unsigned 8 bits representing the operating class in which the channel list
* is valid
*/
public final short operatingClass;
/** List of unsigned 8 bits, representing the channel numbers */
public final List<Short> channelList;
/** Constructor */
public BssIntolerantChannelReport(
short operatingClass,
List<Short> channelList
) {
this.operatingClass = operatingClass;
this.channelList = Collections.unmodifiableList(channelList);
}
/** Parse BssIntolerantChannelReport from JSON object */
// TODO rename fields as necessary - we don't know how the data format yet
public static BssIntolerantChannelReport parse(JsonObject contents) {
List<Short> channelList = new ArrayList<>();
JsonElement channelListJson = contents.get("Channel List");
if (channelListJson != null) {
for (JsonElement elem : channelListJson.getAsJsonArray()) {
channelList.add(elem.getAsShort());
}
}
return new BssIntolerantChannelReport(
contents.get("Operating Class").getAsShort(),
channelList
);
}
@Override
public int hashCode() {
return Objects.hash(operatingClass, channelList);
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (this == obj) {
return true;
}
if (getClass() != obj.getClass()) {
return false;
}
BssIntolerantChannelReport other = (BssIntolerantChannelReport) obj;
return operatingClass == other.operatingClass &&
channelList.equals(other.channelList);
}
}

View File

@@ -0,0 +1,201 @@
/*
* 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.openwifi.cloudsdk.ies;
import java.util.Objects;
import com.google.gson.JsonObject;
// NOTE: Not validated (not seen on test devices)
/**
* This information element (IE) appears in wifiscan entries. It's called
* "Collocated Interference Report" in 802.11 specs (section 9.4.2.84). Refer to
* the specification for more details. Language in javadocs is taken from the
* specification.
*/
public class CollocatedInterferenceReport {
/** Defined in 802.11 table 9-92 */
public static final int TYPE = 96;
public static class InterferenceAccuracyAndIndex {
/**
* Unsigned int (4 bits) representing expected accuracy of the estimate of
* interference in dB with 95% confidence interval
*/
public final byte expectedAccuracy;
/**
* Unsigned int (4 bits) indicating the interference index that is unique for
* each type of interference source
*/
public final byte interferenceIndex;
/** Constructor */
public InterferenceAccuracyAndIndex(
byte expectedAccuracy,
byte interferenceIndex
) {
this.expectedAccuracy = expectedAccuracy;
this.interferenceIndex = interferenceIndex;
}
/** Parse InterferenceAccuracyAndIndex from JSON object */
// TODO modify this method as necessary - since the IE doesn't seem to be
// present, we have no idea what the format looks like
public static InterferenceAccuracyAndIndex parse(JsonObject contents) {
return new InterferenceAccuracyAndIndex(
contents.get("Expected Accuracy").getAsByte(),
contents.get("Interference Index").getAsByte()
);
}
@Override
public int hashCode() {
return Objects.hash(expectedAccuracy, interferenceIndex);
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (this == obj) {
return true;
}
if (getClass() != obj.getClass()) {
return false;
}
InterferenceAccuracyAndIndex other =
(InterferenceAccuracyAndIndex) obj;
return expectedAccuracy == other.expectedAccuracy &&
interferenceIndex == other.interferenceIndex;
}
}
/** Unsigned 8 bits representing when the report is generated */
public final short reportPeriod;
/**
* signed 8 bits representing the maximum level of the collocated
* interference power in units of dBm over all receive chains averaged over a
* 4 microsecond period during an interference period and across interference
* bandwidth
*/
public final byte interferenceLevel;
/** Subfield for interference level accuracy and index - 8 bits */
public final InterferenceAccuracyAndIndex interferenceAccuracyAndIndex;
/**
* Unsigned 32 bits representing the interval between two successibe periods
* of interference in microseconds
*/
public final long interferenceInterval;
/**
* Unsigned 32 bits representing the duration of each period of interference in
* microseconds
*/
public final long interferenceBurstLength;
/**
* Unsigned 32 bits contains the least significant 4 octets (i.e., B0B31) of
* the TSF timer at the start of the interference burst. When either the
* Interference Interval or the Interference Burst Length fields are set to
* 2^32 1, this field indicates the average duty cycle
*/
public final long interferenceStartTimeDutyCycle;
/**
* Unsigned 32 bits representing indicates the center frequency of interference
* in units of 5 kHz
*/
public final long interferenceCenterFrequency;
/**
* Unsigned 16 bits representing the bandwidth in units of 5 kHz at the 3 dB
* roll-off point of the interference signal
*/
public final short interferenceBandwidth;
/** Constructor */
public CollocatedInterferenceReport(
short reportPeriod,
byte interferenceLevel,
InterferenceAccuracyAndIndex interferenceAccuracyAndIndex,
long interferenceInterval,
long interferenceBurstLength,
long interferenceStartTimeDutyCycle,
long interferenceCenterFrequency,
short interferenceBandwidth
) {
this.reportPeriod = reportPeriod;
this.interferenceLevel = interferenceLevel;
this.interferenceAccuracyAndIndex = interferenceAccuracyAndIndex;
this.interferenceInterval = interferenceInterval;
this.interferenceBurstLength = interferenceBurstLength;
this.interferenceStartTimeDutyCycle = interferenceStartTimeDutyCycle;
this.interferenceCenterFrequency = interferenceCenterFrequency;
this.interferenceBandwidth = interferenceBandwidth;
}
/** Parse CollocatedInterferenceReport from JSON object */
// TODO rename fields as necessary - we don't know how the data format yet
public static CollocatedInterferenceReport parse(JsonObject contents) {
return new CollocatedInterferenceReport(
contents.get("Report Period").getAsShort(),
contents.get("Intereference Level").getAsByte(),
InterferenceAccuracyAndIndex
.parse(
contents.get("Interference Level Accuracy/Inteference Index").getAsJsonObject()
),
contents.get("Interference Interval").getAsLong(),
contents.get("Interference Burst Length").getAsLong(),
contents.get("Interference Start Time/Duty Cycle").getAsLong(),
contents.get("Interference Center Frequency").getAsLong(),
contents.get("Interference Bandwidth").getAsShort()
);
}
@Override
public int hashCode() {
return Objects.hash(
reportPeriod,
interferenceLevel,
interferenceAccuracyAndIndex,
interferenceInterval,
interferenceBurstLength,
interferenceStartTimeDutyCycle,
interferenceCenterFrequency,
interferenceBandwidth
);
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (this == obj) {
return true;
}
if (getClass() != obj.getClass()) {
return false;
}
CollocatedInterferenceReport other = (CollocatedInterferenceReport) obj;
return reportPeriod == other.reportPeriod &&
interferenceLevel == other.interferenceLevel &&
interferenceAccuracyAndIndex
.equals(other.interferenceAccuracyAndIndex) &&
interferenceInterval == other.interferenceInterval &&
interferenceBurstLength == other.interferenceBurstLength &&
interferenceStartTimeDutyCycle ==
other.interferenceStartTimeDutyCycle &&
interferenceCenterFrequency == other.interferenceCenterFrequency &&
interferenceBandwidth == other.interferenceBandwidth;
}
}

View File

@@ -18,35 +18,35 @@ import com.google.gson.JsonObject;
/**
* This information element (IE) appears in wifiscan entries.
* Refer to the 802.11 specification for more details. Language in
* javadocs is taken from the specification.
* Refer to the 802.11 specification (section 9.4.2.8) for more details.
* Language in javadocs is taken from the specification.
*/
public class Country {
/** Defined in 802.11 */
/** Defined in 802.11 table 9-92 */
public static final int TYPE = 7;
/** Constraints for a subset of channels in the AP's country */
public static class CountryInfo {
/**
* The lowest channel number in the CountryInfo.
* 8 bits unsigned - the lowest channel number in the CountryInfo.
*/
public final int firstChannelNumber;
public final short firstChannelNumber;
/**
* The maximum power, in dBm, allowed to be transmitted.
* 8 bits unsigned - The maximum power, in dBm, allowed to be transmitted.
*/
public final int maximumTransmitPowerLevel;
public final short maximumTransmitPowerLevel;
/**
* Number of channels this CountryInfo applies to. E.g., if First
* Channel Number is 2 and Number of Channels is 4, this CountryInfo
* 8 bits unsigned - Number of channels this CountryInfo applies to. E.g.,
* if First Channel Number is 2 and Number of Channels is 4, this CountryInfo
* describes channels 2, 3, 4, and 5.
*/
public final int numberOfChannels;
public final short numberOfChannels;
/** Constructor. */
public CountryInfo(
int firstChannelNumber,
int maximumTransmitPowerLevel,
int numberOfChannels
short firstChannelNumber,
short maximumTransmitPowerLevel,
short numberOfChannels
) {
this.firstChannelNumber = firstChannelNumber;
this.maximumTransmitPowerLevel = maximumTransmitPowerLevel;
@@ -55,13 +55,13 @@ public class Country {
/** Parse CountryInfo from the appropriate Json object. */
public static CountryInfo parse(JsonObject contents) {
final int firstChannelNumber =
contents.get("First Channel Number").getAsInt();
final int maximumTransmitPowerLevel = contents
final short firstChannelNumber =
contents.get("First Channel Number").getAsShort();
final short maximumTransmitPowerLevel = contents
.get("Maximum Transmit Power Level (in dBm)")
.getAsInt();
final int numberOfChannels =
contents.get("Number of Channels").getAsInt();
.getAsShort();
final short numberOfChannels =
contents.get("Number of Channels").getAsShort();
return new CountryInfo(
firstChannelNumber,
maximumTransmitPowerLevel,
@@ -94,15 +94,10 @@ public class Country {
maximumTransmitPowerLevel == other.maximumTransmitPowerLevel &&
numberOfChannels == other.numberOfChannels;
}
@Override
public String toString() {
return "CountryInfo [firstChannelNumber=" + firstChannelNumber +
", maximumTransmitPowerLevel=" + maximumTransmitPowerLevel +
", numberOfChannels=" + numberOfChannels + "]";
}
}
/** Country */
public final String country;
/**
* Each constraint is a CountryInfo describing tx power constraints on
* one or more channels, for the current country.
@@ -110,7 +105,11 @@ public class Country {
public final List<CountryInfo> constraints;
/** Constructor */
public Country(List<CountryInfo> countryInfos) {
public Country(
String country,
List<CountryInfo> countryInfos
) {
this.country = country;
this.constraints = Collections.unmodifiableList(countryInfos);
}
@@ -126,7 +125,10 @@ public class Country {
constraints.add(countryInfo);
}
}
return new Country(constraints);
return new Country(
contents.get("Code").getAsString(),
constraints
);
}
@Override
@@ -148,9 +150,4 @@ public class Country {
Country other = (Country) obj;
return Objects.equals(constraints, other.constraints);
}
@Override
public String toString() {
return "Country [constraints=" + constraints + "]";
}
}

View File

@@ -15,9 +15,12 @@ import org.apache.commons.codec.binary.Base64;
/**
* High Throughput (HT) Operation Element, which is potentially present in
* wifiscan entries. Introduced in 802.11n (2009).
* wifiscan entries. Introduced in 802.11n (2009). Refer to the 802.11
* specification (section 9.4.2.56)).
*/
public class HTOperation {
/** Defined in 802.11 table 9-92 */
public static final int TYPE = 61;
/** Channel number of the primary channel. */
public final byte primaryChannel;
@@ -263,4 +266,4 @@ public class HTOperation {
staChannelWidth == other.staChannelWidth &&
stbcBeacon == other.stbcBeacon;
}
}
}

View File

@@ -15,32 +15,31 @@ import com.google.gson.JsonObject;
/**
* This information element (IE) appears in wifiscan entries. It is called
* "Local Power Constraint" in these entries, and just "Power Constraint" in
* the 802.11 specification. Refer to the specification for more details.
* the 802.11 specification (section 9.4.2.13). Refer to the specification for more details.
* Language in javadocs is taken from the specification.
*/
public class LocalPowerConstraint {
/** Defined in 802.11 */
/** Defined in 802.11 table 9-92 */
public static final int TYPE = 32;
/**
* Units are dB.
* Unsigned 8 bits - units are dB.
* <p>
* The local maximum transmit power for a channel is defined as the maximum
* transmit power level specified for the channel in the Country IE minus
* this variable for the given channel.
*/
public final int localPowerConstraint;
public final short localPowerConstraint;
/** Constructor */
public LocalPowerConstraint(int localPowerConstraint) {
public LocalPowerConstraint(short localPowerConstraint) {
this.localPowerConstraint = localPowerConstraint;
}
/** Parse LocalPowerConstraint IE from appropriate Json object. */
public static LocalPowerConstraint parse(JsonObject contents) {
final int localPowerConstraint =
contents.get("Local Power Constraint").getAsInt();
final short localPowerConstraint =
contents.get("Local Power Constraint").getAsShort();
return new LocalPowerConstraint(localPowerConstraint);
}
@@ -63,10 +62,4 @@ public class LocalPowerConstraint {
LocalPowerConstraint other = (LocalPowerConstraint) obj;
return localPowerConstraint == other.localPowerConstraint;
}
@Override
public String toString() {
return "LocalPowerConstraint [localPowerConstraint=" +
localPowerConstraint + "]";
}
}

View File

@@ -0,0 +1,318 @@
/*
* 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.openwifi.cloudsdk.ies;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
// NOTE: Not validated (not seen on test devices)
/**
* This information element (IE) appears in wifiscan entries. It's called
* "Neighbor Report" in 802.11 specs (section 9.4.2.36). Refer to the
* specification for more details. Language in javadocs is taken from the
* specification.
*/
public class NeighborReport {
/** Defined in 802.11 table 9-92 */
public static final int TYPE = 52;
/**
* The BSSID Information field can be used to help determine neighbor service
* set transition candidates
*/
public static class BssidInfo {
/**
* The capability subelement containing selected capability information for
* the AP indicated by this BSSID.
*/
public static class Capabilities {
/** dot11SpectrumManagementRequired */
public final boolean spectrumManagement;
/** dot11QosOptionImplemented */
public final boolean qos;
/** dot11APSDOptionImplemented */
public final boolean apsd;
/** dot11RadioMeasurementActivated */
public final boolean radioMeasurement;
/** Constructor */
public Capabilities(
boolean spectrumManagement,
boolean qos,
boolean apsd,
boolean radioMeasurement
) {
this.spectrumManagement = spectrumManagement;
this.qos = qos;
this.apsd = apsd;
this.radioMeasurement = radioMeasurement;
}
/** Parse Capabilities from JSON object */
// TODO modify this method as necessary - since the IE doesn't seem to be
// present, we have no idea what the format looks like
public static Capabilities parse(JsonObject contents) {
return new Capabilities(
contents.get("Spectrum Management").getAsBoolean(),
contents.get("QoS").getAsBoolean(),
contents.get("APSD").getAsBoolean(),
contents.get("Radio Management").getAsBoolean()
);
}
@Override
public int hashCode() {
return Objects
.hash(spectrumManagement, qos, apsd, radioMeasurement);
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (this == obj) {
return true;
}
if (getClass() != obj.getClass()) {
return false;
}
Capabilities other = (Capabilities) obj;
return spectrumManagement == other.spectrumManagement &&
qos == other.qos && apsd == other.apsd &&
radioMeasurement == other.radioMeasurement;
}
}
/**
* 2 unsigned bits - whether the AP identified by this BSSID is reachable by
* the STA that requested the neighbor report
*/
public final byte apReachability;
/**
* If true, indicates that the AP identified by this BSSID supports the same
* security provisioning as used by the STA in its current association. If the
* bit is false, it indicates either that the AP does not support the same
* security provisioning or that the security information is not available at
* this time.
*/
public final boolean security;
/**
* Indicates the AP indicated by this BSSID has the same authenticator as
* the AP sending the report. If this bit is false, it indicates a distinct
* authenticator or the information is not available.
*/
public final boolean keyScope;
/**
* @see Capabilities
*/
public final Capabilities capabilities;
/**
* Set to true to indicate that the AP represented by this BSSID is including
* an MDE in its Beacon frames and that the contents of that MDE are identical
* to the MDE advertised by the AP sending the report
*/
public final boolean mobilityDomain;
/**
* High throughput or not, if true the contents of the HT Capabilities in the
* Beacon frame should be identical to the HT Capabilities advertised by the
* AP sending the report
*/
public final boolean highThroughput;
/**
* Very High throughput or not, if true the contents of the VHT Capabilities
* in the Beacon frame should be identical to the VHT Capabilities advertised
* by the AP sending the report
*/
public final boolean veryHighThroughput;
/**
* Indicate that the AP represented by this BSSID is an AP that has set the Fine
* Timing Measurement Responder field of the Extended Capabilities element
*/
public final boolean ftm;
/** Constructor */
public BssidInfo(
byte apReachability,
boolean security,
boolean keyScope,
Capabilities capabilities,
boolean mobilityDomain,
boolean highThroughput,
boolean veryHighThroughput,
boolean ftm
) {
this.apReachability = apReachability;
this.security = security;
this.keyScope = keyScope;
this.capabilities = capabilities;
this.mobilityDomain = mobilityDomain;
this.highThroughput = highThroughput;
this.veryHighThroughput = veryHighThroughput;
this.ftm = ftm;
}
/** Parse BssidInfo from JSON object */
// TODO rename fields as necessary - we don't know how the data format yet
public static BssidInfo parse(JsonObject contents) {
JsonElement capabilitiesJson = contents.get("capabilities");
if (capabilitiesJson == null) {
return null;
}
Capabilities capabilities =
Capabilities.parse(capabilitiesJson.getAsJsonObject());
return new BssidInfo(
contents.get("AP Reachability").getAsByte(),
contents.get("Security").getAsBoolean(),
contents.get("Key Scope").getAsBoolean(),
capabilities,
contents.get("Mobility Domain").getAsBoolean(),
contents.get("High Throughput").getAsBoolean(),
contents.get("Very High Throughput").getAsBoolean(),
contents.get("FTM").getAsBoolean()
);
}
@Override
public int hashCode() {
return Objects.hash(
apReachability,
security,
keyScope,
mobilityDomain,
highThroughput,
veryHighThroughput,
ftm
);
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (this == obj) {
return true;
}
if (getClass() != obj.getClass()) {
return false;
}
BssidInfo other = (BssidInfo) obj;
return apReachability == other.apReachability &&
security == other.security && keyScope == other.keyScope &&
capabilities == other.capabilities &&
mobilityDomain == other.mobilityDomain &&
highThroughput == other.highThroughput &&
veryHighThroughput == other.veryHighThroughput &&
ftm == other.ftm;
}
}
/** BSSID */
public final String bssid;
/**
* @see BssidInfo
*/
public final BssidInfo bssidInfo;
/**
* Unsigned 8 bits - indicates the channel set of the AP indicated by this BSSID
*/
public final short operatingClass;
/**
* Unsigned 8 bits - channel number
*/
public final short channelNumber;
/**
* Unsigned 8 bits - PHY type
*/
public final short phyType;
// TODO do we want to support the subelements?
/**
* Optional subelements
*/
public final List<JsonObject> subelements;
/** Constructor */
public NeighborReport(
String bssid,
BssidInfo bssidInfo,
short operatingClass,
short channelNumber,
short phyType,
List<JsonObject> subelements
) {
this.bssid = bssid;
this.bssidInfo = bssidInfo;
this.operatingClass = operatingClass;
this.channelNumber = channelNumber;
this.phyType = phyType;
this.subelements = Collections.unmodifiableList(subelements);
}
/** Parse NeighborReport from JSON object */
// TODO modify this method as necessary - since the IE doesn't seem to be
// present, we have no idea what the format looks like
public static NeighborReport parse(JsonObject contents) {
List<JsonObject> subelements = null;
JsonElement subelementsObj = contents.get("Subelements");
if (subelementsObj != null) {
subelements = new ArrayList<JsonObject>();
for (JsonElement elem : subelementsObj.getAsJsonArray()) {
subelements.add(elem.getAsJsonObject());
}
}
return new NeighborReport(
contents.get("BSSID").getAsString(),
BssidInfo.parse(contents.get("BSSID Info").getAsJsonObject()),
contents.get("Operating Class").getAsShort(),
contents.get("Channel Number").getAsShort(),
contents.get("Phy Type").getAsShort(),
subelements
);
}
@Override
public int hashCode() {
return Objects
.hash(bssid, bssidInfo, operatingClass, channelNumber, phyType);
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (this == obj) {
return true;
}
if (getClass() != obj.getClass()) {
return false;
}
NeighborReport other = (NeighborReport) obj;
return bssid == other.bssid && bssidInfo == other.bssidInfo &&
operatingClass == other.operatingClass &&
channelNumber == other.channelNumber && phyType == other.phyType;
}
}

View File

@@ -0,0 +1,80 @@
/*
* 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.openwifi.cloudsdk.ies;
import java.util.Objects;
import com.google.gson.JsonObject;
// NOTE: Not validated (not seen on test devices)
/**
* This information element (IE) appears in wifiscan entries. It's called "Power
* Capability" in 802.11 specs (section 9.4.2.14). Refer to the specification
* for more details. Language in javadocs is taken from the specification.
*/
public class PowerCapability {
/** Defined in 802.11 table 9-92 */
public static final int TYPE = 33;
/**
* Signed 8 bits units of dB relative to 1mW - nominal minimum transmit power
* with which the STA is capable of transmitting in the current channel, with a
* tolerance ± 5 dB.
*/
public final byte minimumTxPowerCapability;
/**
* Signed 8 bits units of dB relative to 1mW - nominal maximum transmit power
* with which the STA is capable of transmitting in the current channel, with a
* tolerance ± 5 dB.
*/
public final byte maximumTxPowerCapability;
/** Constructor */
public PowerCapability(
byte minimumTxPowerCapability,
byte maximumTxPowerCapability
) {
this.minimumTxPowerCapability = minimumTxPowerCapability;
this.maximumTxPowerCapability = maximumTxPowerCapability;
}
/** Parse PowerCapability from JSON object */
// TODO modify this method as necessary - since the IE doesn't seem to be
// present, we have no idea what the format looks like
public static PowerCapability parse(JsonObject contents) {
return new PowerCapability(
contents.get("Minimum Tx Power Capability").getAsByte(),
contents.get("Maximum Tx Power Capability").getAsByte()
);
}
@Override
public int hashCode() {
return Objects.hash(minimumTxPowerCapability, maximumTxPowerCapability);
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (this == obj) {
return true;
}
if (getClass() != obj.getClass()) {
return false;
}
PowerCapability other = (PowerCapability) obj;
return minimumTxPowerCapability == other.minimumTxPowerCapability &&
maximumTxPowerCapability == other.maximumTxPowerCapability;
}
}

View File

@@ -15,22 +15,21 @@ import com.google.gson.JsonObject;
/**
* This information element (IE) appears in wifiscan entries. It is called
* "QBSS Load" in these entries, and just "BSS Load" in the 802.11
* specification. Refer to the specification for more details. Language in
* "QBSS Load" in these entries, and just "BSS Load" in the 802.11 specification
* (section 9.4.2.27). Refer to the specification for more details. Language in
* javadocs is taken from the specification.
*/
public class QbssLoad {
/** Defined in 802.11 */
/** Defined in 802.11 table 9-92 */
public static final int TYPE = 11;
/**
* The total number of STAs currently associated with the BSS.
* Unsigned 16 bits - The total number of STAs currently associated with the BSS.
*/
public final int stationCount;
public final short stationCount;
/**
* The Channel Utilization field is defined as the percentage of time,
* linearly scaled with 255 representing 100%, that the AP sensed the
* Unsigned 8 bits - The Channel Utilization field is defined as the percentage
* of time, linearly scaled with 255 representing 100%, that the AP sensed the
* medium was busy, as indicated by either the physical or virtual carrier
* sense (CS) mechanism. When more than one channel is in use for the BSS,
* the Channel Utilization field value is calculated only for the primary
@@ -40,22 +39,22 @@ public class QbssLoad {
* (dot11ChannelUtilizationBeaconIntervals * dot11BeaconPeriod * 1024)
* )
*/
public final int channelUtilization;
public final short channelUtilization;
/**
* The Available Admission Capacity field contains an unsigned integer that
* specifies the remaining amount of medium time available via explicit
* admission control, in units of 32 miscrosecond/second. The field is
* helpful for roaming STAs to select an AP that is likely to accept future
* admission control requests, but it does not represent an assurance that
* the HC admits these requests.
* Unsigned 16 bits - The Available Admission Capacity field contains an
* unsigned integer that specifies the remaining amount of medium time
* available via explicit admission control, in units of 32
* miscrosecond/second. The field is helpful for roaming STAs to select an AP
* that is likely to accept future admission control requests, but it does not
* represent an assurance that the HC admits these requests.
*/
public final int availableAdmissionCapacity;
public final short availableAdmissionCapacity;
/** Constructor */
public QbssLoad(
int stationCount,
int channelUtilization,
int availableAdmissionCapacity
short stationCount,
short channelUtilization,
short availableAdmissionCapacity
) {
this.stationCount = stationCount;
this.channelUtilization = channelUtilization;
@@ -70,11 +69,11 @@ public class QbssLoad {
return null;
}
contents = ccaContentJsonElement.getAsJsonObject();
final int stationCount = contents.get("Station Count").getAsInt();
final int channelUtilization =
contents.get("Channel Utilization").getAsInt();
final int availableAdmissionCapacity =
contents.get("Available Admission Capabilities").getAsInt();
final short stationCount = contents.get("Station Count").getAsShort();
final short channelUtilization =
contents.get("Channel Utilization").getAsShort();
final short availableAdmissionCapacity =
contents.get("Available Admission Capabilities").getAsShort();
return new QbssLoad(
stationCount,
channelUtilization,
@@ -107,11 +106,4 @@ public class QbssLoad {
channelUtilization == other.channelUtilization &&
stationCount == other.stationCount;
}
@Override
public String toString() {
return "QbssLoad [stationCount=" + stationCount +
", channelUtilization=" + channelUtilization +
", availableAdmissionCapacity=" + availableAdmissionCapacity + "]";
}
}

View File

@@ -0,0 +1,67 @@
/*
* 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.openwifi.cloudsdk.ies;
import java.util.Objects;
import com.google.gson.JsonObject;
// NOTE: Not validated (not seen on test devices)
/**
* This information element (IE) appears in wifiscan entries. It's called "RCPI"
* in 802.11 specs (section 9.4.2.37). Refer to the specification for more
* details. Language in javadocs is taken from the specification.
*/
public class RCPI {
/** Defined in 802.11 table 9-92 */
public static final int TYPE = 53;
/**
* Unsigned 8 bits - indication of the received RF power in the selected
* channel for a received frame
*/
public final short rcpi;
/** Constructor */
public RCPI(short rcpi) {
this.rcpi = rcpi;
}
/** Parse RCPI from JSON object */
// TODO modify this method as necessary - since the IE doesn't seem to be
// present, we have no idea what the format looks like
public static RCPI parse(JsonObject contents) {
return new RCPI(
contents.get("RCPI").getAsShort()
);
}
@Override
public int hashCode() {
return Objects.hash(rcpi);
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (this == obj) {
return true;
}
if (getClass() != obj.getClass()) {
return false;
}
RCPI other = (RCPI) obj;
return rcpi == other.rcpi;
}
}

View File

@@ -0,0 +1,274 @@
/*
* 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.openwifi.cloudsdk.ies;
import java.util.Objects;
import com.facebook.openwifi.cloudsdk.IEUtils;
import com.google.gson.JsonObject;
/**
* This information element (IE) appears in wifiscan entries. It's called "RM
* Enabled Capabilities" in 802.11 specs (section 9.4.2.45). Refer to the
* specification for more details. Language in javadocs is taken from the
* specification.
*/
public class RMEnabledCapabilities {
/** Defined in 802.11 table 9-92 */
public static final int TYPE = 70;
// Bit fields
// @formatter:off
public final boolean linkMeasurementCapabilityEnabled;
public final boolean neighborReportCapabilityEnabled;
public final boolean parallelMeasurementsCapabilityEnabled;
public final boolean repeatedMeasurementsCapabilityEnabled;
public final boolean beaconPassiveMeasurementCapabilityEnabled;
public final boolean beaconActiveMeasurementCapabilityEnabled;
public final boolean beaconTableMeasurementCapabilityEnabled;
public final boolean beaconMeasurementReportingConditionsCapabilityEnabled;
public final boolean frameMeasurementCapabilityEnabled;
public final boolean channelLoadMeasurementCapabilityEnabled;
public final boolean noiseHistogramMeasurementCapabilityEnabled;
public final boolean statisticsMeasurementCapabilityEnabled;
public final boolean lciMeasurementCapabilityEnabled;
public final boolean lciAzimuthCapabilityEnabled;
public final boolean transmitStreamCategoryMeasurementCapabilityEnabled;
public final boolean triggeredTransmitStreamCategoryMeasurementCapabilityEnabled;
public final boolean apChannelReportCapabilityEnabled;
public final boolean rmMibCapabilityEnabled;
public final int operatingChannelMaxMeasurementDuration;
public final int nonoperatingChannelMaxMeasurementDuration;
public final int measurementPilotCapability;
public final boolean measurementPilotTransmissionInformationCapabilityEnabled;
public final boolean neighborReportTsfOffsetCapabilityEnabled;
public final boolean rcpiMeasurementCapabilityEnabled;
public final boolean rsniMeasurementCapabilityEnabled;
public final boolean bssAverageAccessDelayCapabilityEnabled;
public final boolean bssAvailableAdmissionCapacityCapabilityEnabled;
public final boolean antennaCapabilityEnabled;
public final boolean ftmRangeReportCapabilityEnabled;
public final boolean civicLocationMeasurementCapabilityEnabled;
// @formatter:on
/** Constructor */
public RMEnabledCapabilities(
boolean linkMeasurementCapabilityEnabled,
boolean neighborReportCapabilityEnabled,
boolean parallelMeasurementsCapabilityEnabled,
boolean repeatedMeasurementsCapabilityEnabled,
boolean beaconPassiveMeasurementCapabilityEnabled,
boolean beaconActiveMeasurementCapabilityEnabled,
boolean beaconTableMeasurementCapabilityEnabled,
boolean beaconMeasurementReportingConditionsCapabilityEnabled,
boolean frameMeasurementCapabilityEnabled,
boolean channelLoadMeasurementCapabilityEnabled,
boolean noiseHistogramMeasurementCapabilityEnabled,
boolean statisticsMeasurementCapabilityEnabled,
boolean lciMeasurementCapabilityEnabled,
boolean lciAzimuthCapabilityEnabled,
boolean transmitStreamCategoryMeasurementCapabilityEnabled,
boolean triggeredTransmitStreamCategoryMeasurementCapabilityEnabled,
boolean apChannelReportCapabilityEnabled,
boolean rmMibCapabilityEnabled,
int operatingChannelMaxMeasurementDuration,
int nonoperatingChannelMaxMeasurementDuration,
int measurementPilotCapability,
boolean measurementPilotTransmissionInformationCapabilityEnabled,
boolean neighborReportTsfOffsetCapabilityEnabled,
boolean rcpiMeasurementCapabilityEnabled,
boolean rsniMeasurementCapabilityEnabled,
boolean bssAverageAccessDelayCapabilityEnabled,
boolean bssAvailableAdmissionCapacityCapabilityEnabled,
boolean antennaCapabilityEnabled,
boolean ftmRangeReportCapabilityEnabled,
boolean civicLocationMeasurementCapabilityEnabled
) {
// @formatter:off
this.linkMeasurementCapabilityEnabled = linkMeasurementCapabilityEnabled;
this.neighborReportCapabilityEnabled = neighborReportCapabilityEnabled;
this.parallelMeasurementsCapabilityEnabled = parallelMeasurementsCapabilityEnabled;
this.repeatedMeasurementsCapabilityEnabled = repeatedMeasurementsCapabilityEnabled;
this.beaconPassiveMeasurementCapabilityEnabled = beaconPassiveMeasurementCapabilityEnabled;
this.beaconActiveMeasurementCapabilityEnabled = beaconActiveMeasurementCapabilityEnabled;
this.beaconTableMeasurementCapabilityEnabled = beaconTableMeasurementCapabilityEnabled;
this.beaconMeasurementReportingConditionsCapabilityEnabled = beaconMeasurementReportingConditionsCapabilityEnabled;
this.frameMeasurementCapabilityEnabled = frameMeasurementCapabilityEnabled;
this.channelLoadMeasurementCapabilityEnabled = channelLoadMeasurementCapabilityEnabled;
this.noiseHistogramMeasurementCapabilityEnabled = noiseHistogramMeasurementCapabilityEnabled;
this.statisticsMeasurementCapabilityEnabled = statisticsMeasurementCapabilityEnabled;
this.lciMeasurementCapabilityEnabled = lciMeasurementCapabilityEnabled;
this.lciAzimuthCapabilityEnabled = lciAzimuthCapabilityEnabled;
this.transmitStreamCategoryMeasurementCapabilityEnabled = transmitStreamCategoryMeasurementCapabilityEnabled;
this.triggeredTransmitStreamCategoryMeasurementCapabilityEnabled = triggeredTransmitStreamCategoryMeasurementCapabilityEnabled;
this.apChannelReportCapabilityEnabled = apChannelReportCapabilityEnabled;
this.rmMibCapabilityEnabled = rmMibCapabilityEnabled;
this.operatingChannelMaxMeasurementDuration = operatingChannelMaxMeasurementDuration;
this.nonoperatingChannelMaxMeasurementDuration = nonoperatingChannelMaxMeasurementDuration;
this.measurementPilotCapability = measurementPilotCapability;
this.measurementPilotTransmissionInformationCapabilityEnabled = measurementPilotTransmissionInformationCapabilityEnabled;
this.neighborReportTsfOffsetCapabilityEnabled = neighborReportTsfOffsetCapabilityEnabled;
this.rcpiMeasurementCapabilityEnabled = rcpiMeasurementCapabilityEnabled;
this.rsniMeasurementCapabilityEnabled = rsniMeasurementCapabilityEnabled;
this.bssAverageAccessDelayCapabilityEnabled = bssAverageAccessDelayCapabilityEnabled;
this.bssAvailableAdmissionCapacityCapabilityEnabled = bssAvailableAdmissionCapacityCapabilityEnabled;
this.antennaCapabilityEnabled = antennaCapabilityEnabled;
this.ftmRangeReportCapabilityEnabled = ftmRangeReportCapabilityEnabled;
this.civicLocationMeasurementCapabilityEnabled = civicLocationMeasurementCapabilityEnabled;
// @formatter:on
}
/** Parse RMEnabledCapabilities IE from appropriate Json object. */
public static RMEnabledCapabilities parse(JsonObject contents) {
JsonObject o = contents.get("RM Capabilities").getAsJsonObject();
// @formatter:off
return new RMEnabledCapabilities(
/* bits 0-17 */
IEUtils.parseBooleanNumberField(o, "Link Measurement"),
IEUtils.parseBooleanNumberField(o, "Neighbor Report"),
IEUtils.parseBooleanNumberField(o, "Parallel Measurements"),
IEUtils.parseBooleanNumberField(o, "Repeated Measurements"),
IEUtils.parseBooleanNumberField(o, "Beacon Passive Measurement"),
IEUtils.parseBooleanNumberField(o, "Beacon Active Measurement"),
IEUtils.parseBooleanNumberField(o, "Beacon Table Measurement"),
IEUtils.parseBooleanNumberField(o, "Beacon Measurement Reporting Conditions"),
IEUtils.parseBooleanNumberField(o, "Frame Measurement"),
IEUtils.parseBooleanNumberField(o, "Channel Load Measurement"),
IEUtils.parseBooleanNumberField(o, "Noise Histogram Measurement"),
IEUtils.parseBooleanNumberField(o, "Statistics Measurement"),
IEUtils.parseBooleanNumberField(o, "LCI Measurement"),
IEUtils.parseBooleanNumberField(o, "LCI Azimuth capability"),
IEUtils.parseBooleanNumberField(o, "Transmit Stream/Category Measurement"),
IEUtils.parseBooleanNumberField(o, "Triggered Transmit Stream/Category Measurement"),
IEUtils.parseBooleanNumberField(o, "AP Channel Report capability"),
IEUtils.parseBooleanNumberField(o, "RM MIB capability"),
/* bits 18-20 */
IEUtils.parseIntField(o, "Operating Channel Max Measurement Duration"),
/* bits 21-23 */
IEUtils.parseIntField(o, "Nonoperating Channel Max Measurement Duration"),
/* bits 24-26 */
IEUtils.parseIntField(o, "Measurement Pilotcapability"),
/* bits 27-35 */
false /* TODO "Measurement Pilot Transmission Information Capability" */,
IEUtils.parseBooleanNumberField(o, "Neighbor Report TSF Offset"),
IEUtils.parseBooleanNumberField(o, "RCPI Measurement capability"),
IEUtils.parseBooleanNumberField(o, "RSNI Measurement capability"),
IEUtils.parseBooleanNumberField(o, "BSS Average Access Delay capability"),
IEUtils.parseBooleanNumberField(o, "BSS Available Admission Capacity capability"),
IEUtils.parseBooleanNumberField(o, "Antenna capability"),
false /* TODO "FTM Range Report Capability" */,
false /* TODO "Civic Location Measurement Capability" */
/* bits 36-39 reserved */
);
// @formatter:on
}
@Override
public int hashCode() {
return Objects.hash(
antennaCapabilityEnabled,
apChannelReportCapabilityEnabled,
beaconActiveMeasurementCapabilityEnabled,
beaconMeasurementReportingConditionsCapabilityEnabled,
beaconPassiveMeasurementCapabilityEnabled,
beaconTableMeasurementCapabilityEnabled,
bssAvailableAdmissionCapacityCapabilityEnabled,
bssAverageAccessDelayCapabilityEnabled,
channelLoadMeasurementCapabilityEnabled,
civicLocationMeasurementCapabilityEnabled,
frameMeasurementCapabilityEnabled,
ftmRangeReportCapabilityEnabled,
lciAzimuthCapabilityEnabled,
lciMeasurementCapabilityEnabled,
linkMeasurementCapabilityEnabled,
measurementPilotCapability,
measurementPilotTransmissionInformationCapabilityEnabled,
neighborReportCapabilityEnabled,
neighborReportTsfOffsetCapabilityEnabled,
noiseHistogramMeasurementCapabilityEnabled,
nonoperatingChannelMaxMeasurementDuration,
operatingChannelMaxMeasurementDuration,
parallelMeasurementsCapabilityEnabled,
rcpiMeasurementCapabilityEnabled,
repeatedMeasurementsCapabilityEnabled,
rmMibCapabilityEnabled,
rsniMeasurementCapabilityEnabled,
statisticsMeasurementCapabilityEnabled,
transmitStreamCategoryMeasurementCapabilityEnabled,
triggeredTransmitStreamCategoryMeasurementCapabilityEnabled
);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
RMEnabledCapabilities other = (RMEnabledCapabilities) obj;
return antennaCapabilityEnabled == other.antennaCapabilityEnabled &&
apChannelReportCapabilityEnabled ==
other.apChannelReportCapabilityEnabled &&
beaconActiveMeasurementCapabilityEnabled ==
other.beaconActiveMeasurementCapabilityEnabled &&
beaconMeasurementReportingConditionsCapabilityEnabled ==
other.beaconMeasurementReportingConditionsCapabilityEnabled &&
beaconPassiveMeasurementCapabilityEnabled ==
other.beaconPassiveMeasurementCapabilityEnabled &&
beaconTableMeasurementCapabilityEnabled ==
other.beaconTableMeasurementCapabilityEnabled &&
bssAvailableAdmissionCapacityCapabilityEnabled ==
other.bssAvailableAdmissionCapacityCapabilityEnabled &&
bssAverageAccessDelayCapabilityEnabled ==
other.bssAverageAccessDelayCapabilityEnabled &&
channelLoadMeasurementCapabilityEnabled ==
other.channelLoadMeasurementCapabilityEnabled &&
civicLocationMeasurementCapabilityEnabled ==
other.civicLocationMeasurementCapabilityEnabled &&
frameMeasurementCapabilityEnabled ==
other.frameMeasurementCapabilityEnabled &&
ftmRangeReportCapabilityEnabled ==
other.ftmRangeReportCapabilityEnabled &&
lciAzimuthCapabilityEnabled == other.lciAzimuthCapabilityEnabled &&
lciMeasurementCapabilityEnabled ==
other.lciMeasurementCapabilityEnabled &&
linkMeasurementCapabilityEnabled ==
other.linkMeasurementCapabilityEnabled &&
measurementPilotCapability == other.measurementPilotCapability &&
measurementPilotTransmissionInformationCapabilityEnabled ==
other.measurementPilotTransmissionInformationCapabilityEnabled &&
neighborReportCapabilityEnabled ==
other.neighborReportCapabilityEnabled &&
neighborReportTsfOffsetCapabilityEnabled ==
other.neighborReportTsfOffsetCapabilityEnabled &&
noiseHistogramMeasurementCapabilityEnabled ==
other.noiseHistogramMeasurementCapabilityEnabled &&
nonoperatingChannelMaxMeasurementDuration ==
other.nonoperatingChannelMaxMeasurementDuration &&
operatingChannelMaxMeasurementDuration ==
other.operatingChannelMaxMeasurementDuration &&
parallelMeasurementsCapabilityEnabled ==
other.parallelMeasurementsCapabilityEnabled &&
rcpiMeasurementCapabilityEnabled ==
other.rcpiMeasurementCapabilityEnabled &&
repeatedMeasurementsCapabilityEnabled ==
other.repeatedMeasurementsCapabilityEnabled &&
rmMibCapabilityEnabled == other.rmMibCapabilityEnabled &&
rsniMeasurementCapabilityEnabled ==
other.rsniMeasurementCapabilityEnabled &&
statisticsMeasurementCapabilityEnabled ==
other.statisticsMeasurementCapabilityEnabled &&
transmitStreamCategoryMeasurementCapabilityEnabled ==
other.transmitStreamCategoryMeasurementCapabilityEnabled &&
triggeredTransmitStreamCategoryMeasurementCapabilityEnabled ==
other.triggeredTransmitStreamCategoryMeasurementCapabilityEnabled;
}
}

View File

@@ -0,0 +1,313 @@
/*
* 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.openwifi.cloudsdk.ies;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import com.facebook.openwifi.cloudsdk.IEUtils;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
// NOTE: Not validated (not seen on test devices)
/**
* This information element (IE) appears in wifiscan entries. It's called
* "Reduced Neighbor Report" in 802.11 specs (section 9.4.2.170). Refer to the
* specification for more details. Language in javadocs is taken from the
* specification.
*/
public class ReducedNeighborReport {
/** Defined in 802.11 table 9-92 */
public static final int TYPE = 201;
/**
* The Neighbor AP Information field specifies TBTT and other information
* related to a group of neighbor APs on one channel.
*/
public static class NeighborApInformation {
/**
* Subfield for TBTT Information header
*/
public static class TbttInformationHeader {
/**
* Unsigned 2 bits - identifies, together with the TBTT Information Length
* subfield, the format of the TBTT Information field
*/
public final byte tbttInformationType;
/**
* 1 bit - reserved except when the Reduced Neighbor Report element is
* carried in a Probe Response frame transmitted by a TVHT AP
*/
public final boolean filteredNeighborAp;
/**
* Unsigned 4 bits - number of TBTT Information fields included in the TBTT
* Information Set field of the Neighbor AP Information field, minus one
*/
public final byte tbttInformationCount;
/**
* Unsigned 8 bits - the length of each TBTT Information field included in
* the TBTT Information Set field of the Neighbor AP Information field
*/
public final short tbttInformationLength;
/** Constructor */
public TbttInformationHeader(
byte tbttInformationType,
boolean filteredNeighborAp,
byte tbttInformationCount,
short tbttInformationLength
) {
this.tbttInformationType = tbttInformationType;
this.filteredNeighborAp = filteredNeighborAp;
this.tbttInformationCount = tbttInformationCount;
this.tbttInformationLength = tbttInformationLength;
}
/** Parse TbttInformationHeader from JSON object */
// TODO modify this method as necessary - since the IE doesn't seem to be
// present, we have no idea what the format looks like
public static TbttInformationHeader parse(JsonObject contents) {
return new TbttInformationHeader(
contents.get("TBTT Information Type").getAsByte(),
contents.get("Filtered Neighbor Map").getAsBoolean(),
contents.get("TBTT Information Count").getAsByte(),
contents.get("TBTT Information Length").getAsShort()
);
}
@Override
public int hashCode() {
return Objects.hash(
tbttInformationType,
filteredNeighborAp,
tbttInformationCount,
tbttInformationLength
);
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (this == obj) {
return true;
}
if (getClass() != obj.getClass()) {
return false;
}
TbttInformationHeader other = (TbttInformationHeader) obj;
return tbttInformationType == other.tbttInformationType &&
filteredNeighborAp == other.filteredNeighborAp &&
tbttInformationCount == other.tbttInformationCount &&
tbttInformationLength == other.tbttInformationLength;
}
}
/**
* Subfield for TBTT Information
*/
public static class TbttInformation {
/**
* Unsigned 8 bits - offset in TUs, rounded down to nearest TU, to the next
* TBTT of an APs BSS from the immediately prior TBTT of the AP that
* transmits this element
*/
public final short neighborApTbttOffset;
/** BSSID of neighbor, optional */
public final String bssid;
/** Short SSID of neighbor, optional */
public final String shortSsid;
/** Constructor */
public TbttInformation(
short neighborApTbttOffset,
String bssid,
String shortSsid
) {
this.neighborApTbttOffset = neighborApTbttOffset;
this.bssid = bssid;
this.shortSsid = shortSsid;
}
/** Parse TbttInformation from JSON object */
// TODO modify this method as necessary - since the IE doesn't seem to be
// present, we have no idea what the format looks like
public static TbttInformation parse(JsonObject contents) {
return new TbttInformation(
contents.get("Neighbor AP TBTT Offset").getAsShort(),
IEUtils.parseOptionalStringField(contents, "BSSID"),
IEUtils.parseOptionalStringField(contents, "Short SSID")
);
}
@Override
public int hashCode() {
return Objects.hash(neighborApTbttOffset, bssid, shortSsid);
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (this == obj) {
return true;
}
if (getClass() != obj.getClass()) {
return false;
}
TbttInformation other = (TbttInformation) obj;
return neighborApTbttOffset ==
other.neighborApTbttOffset && bssid.equals(other.bssid) &&
Objects.equals(shortSsid, other.shortSsid);
}
}
/**
* @see TbttInformationHeader
*/
public final TbttInformationHeader tbttInformationHeader;
/**
* Unsigned 8 bits - channel starting frequency that, together with the
* Channel Number field, indicates the primary channel of the BSSs of the APs
* in this Neighbor AP Information field
*/
public final short operatingClass;
/**
* Unsigned 8 bits - the last known primary channel of the APs in this
* Neighbor AP Information field.
*/
public final short channelNumber;
/**
* @see TbttInformation
*/
public final TbttInformation tbttInformation;
/** Constructor */
public NeighborApInformation(
TbttInformationHeader tbttInformationHeader,
short operatingClass,
short channelNumber,
TbttInformation tbttInformation
) {
this.tbttInformationHeader = tbttInformationHeader;
this.operatingClass = operatingClass;
this.channelNumber = channelNumber;
this.tbttInformation = tbttInformation;
}
/** Parse NeighborApInformation from JSON object */
// TODO modify this method as necessary - since the IE doesn't seem to be
// present, we have no idea what the format looks like
public static NeighborApInformation parse(JsonObject contents) {
return new NeighborApInformation(
TbttInformationHeader.parse(
contents.get("TBTT Information Header").getAsJsonObject()
),
contents.get("Operating Class").getAsShort(),
contents.get("Channel Number").getAsShort(),
TbttInformation.parse(contents.get("TBTT Information").getAsJsonObject())
);
}
@Override
public int hashCode() {
return Objects.hash(
tbttInformationHeader,
operatingClass,
channelNumber,
tbttInformation
);
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (this == obj) {
return true;
}
if (getClass() != obj.getClass()) {
return false;
}
NeighborApInformation other = (NeighborApInformation) obj;
return tbttInformationHeader.equals(other.tbttInformationHeader) &&
operatingClass == other.operatingClass &&
channelNumber == other.channelNumber &&
Objects.equals(tbttInformation, other.tbttInformation);
}
}
/** number of channels in a subband of supported channels */
public final List<NeighborApInformation> neighborApInformations;
/** Constructor */
public ReducedNeighborReport(
List<NeighborApInformation> neighborApInformations
) {
this.neighborApInformations =
Collections.unmodifiableList(neighborApInformations);
}
/** Parse ReducedNeighborReport from JSON object */
// TODO modify this method as necessary - since the IE doesn't seem to be
// present, we have no idea what the format looks like
public static ReducedNeighborReport parse(JsonObject contents) {
List<NeighborApInformation> neighborApInformations = new ArrayList<>();
JsonElement neighborApInformationsObject =
contents.get("Neighbor AP Informations");
if (neighborApInformationsObject != null) {
for (
JsonElement elem : neighborApInformationsObject.getAsJsonArray()
) {
neighborApInformations
.add(NeighborApInformation.parse(elem.getAsJsonObject()));
}
}
return new ReducedNeighborReport(neighborApInformations);
}
@Override
public int hashCode() {
return Objects.hash(neighborApInformations);
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (this == obj) {
return true;
}
if (getClass() != obj.getClass()) {
return false;
}
ReducedNeighborReport other = (ReducedNeighborReport) obj;
return neighborApInformations.equals(other.neighborApInformations);
}
}

View File

@@ -0,0 +1,70 @@
/*
* 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.openwifi.cloudsdk.ies;
import java.util.Objects;
import com.google.gson.JsonObject;
// NOTE: Not validated (not seen on test devices)
/**
* This information element (IE) appears in wifiscan entries. It's called
* "Supported Channels" in 802.11 specs (section 9.4.2.17). Refer to the
* specification for more details. Language in javadocs is taken from the
* specification.
*/
public class SupportedChannels {
/** Defined in 802.11 table 9-92 */
public static final int TYPE = 36;
/** Unsigned 8 bits - first channel in a subband of supported channels */
public final short firstChannelNumber;
/** Unsigned 8 bits - number of channels in a subband of supported channels */
public final short numberOfChannels;
/** Constructor */
public SupportedChannels(short firstChannelNumber, short numberOfChannels) {
this.firstChannelNumber = firstChannelNumber;
this.numberOfChannels = numberOfChannels;
}
/** Parse SupportedChannels from JSON object */
// TODO modify this method as necessary - since the IE doesn't seem to be
// present, we have no idea what the format looks like
public static SupportedChannels parse(JsonObject contents) {
return new SupportedChannels(
contents.get("First Channel Number").getAsShort(),
contents.get("Number of Channels").getAsShort()
);
}
@Override
public int hashCode() {
return Objects.hash(firstChannelNumber, numberOfChannels);
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (this == obj) {
return true;
}
if (getClass() != obj.getClass()) {
return false;
}
SupportedChannels other = (SupportedChannels) obj;
return firstChannelNumber == other.firstChannelNumber &&
numberOfChannels == other.numberOfChannels;
}
}

View File

@@ -10,35 +10,42 @@ package com.facebook.openwifi.cloudsdk.ies;
import java.util.Objects;
import com.google.gson.JsonElement;
import com.facebook.openwifi.cloudsdk.IEUtils;
import com.google.gson.JsonObject;
/**
* This information element (IE) appears in wifiscan entries. It is called
* "Tx Pwr Info" in these entries, and "Transmit Power Envelope" in the 802.11
* specification. Refer to the specification for more details. Language in
* specification (section 9.4.2.161). Refer to the specification for more details. Language in
* javadocs is taken from the specification.
*/
public class TxPwrInfo {
/** Defined in 802.11 */
/** Defined in 802.11 table 9-92 */
public static final int TYPE = 195;
/** Local maximum transmit power for 20 MHz. Required field. */
public final int localMaxTxPwrConstraint20MHz;
/** Local maximum transmit power for 40 MHz. Optional field. */
public final Integer localMaxTxPwrConstraint40MHz;
/** Local maximum transmit power for 80 MHz. Optional field. */
public final Integer localMaxTxPwrConstraint80MHz;
/** Local maximum transmit power for both 160 MHz and 80+80 MHz. Optional field. */
public final Integer localMaxTxPwrConstraint160MHz;
/**
* Unsigned 8 bits - Local maximum transmit power for 20 MHz. Required field.
*/
public final Short localMaxTxPwrConstraint20MHz;
/**
* Unsigned 8 bits - Local maximum transmit power for 40 MHz. Optional field.
*/
public final Short localMaxTxPwrConstraint40MHz;
/**
* Unsigned 8 bits - Local maximum transmit power for 80 MHz. Optional field.
*/
public final Short localMaxTxPwrConstraint80MHz;
/**
* Unsigned 8 bits - Local maximum transmit power for both 160 MHz and 80+80 MHz. Optional field.
*/
public final Short localMaxTxPwrConstraint160MHz;
/** Constructor */
public TxPwrInfo(
int localMaxTxPwrConstraint20MHz,
Integer localMaxTxPwrConstraint40MHz,
Integer localMaxTxPwrConstraint80MHz,
Integer localMaxTxPwrConstraint160MHz
short localMaxTxPwrConstraint20MHz,
Short localMaxTxPwrConstraint40MHz,
Short localMaxTxPwrConstraint80MHz,
Short localMaxTxPwrConstraint160MHz
) {
this.localMaxTxPwrConstraint20MHz = localMaxTxPwrConstraint20MHz;
this.localMaxTxPwrConstraint40MHz = localMaxTxPwrConstraint40MHz;
@@ -50,15 +57,24 @@ public class TxPwrInfo {
public static TxPwrInfo parse(JsonObject contents) {
JsonObject innerObj = contents.get("Tx Pwr Info").getAsJsonObject();
// required field
int localMaxTxPwrConstraint20MHz =
innerObj.get("Local Max Tx Pwr Constraint 20MHz").getAsInt();
short localMaxTxPwrConstraint20MHz =
innerObj.get("Local Max Tx Pwr Constraint 20MHz").getAsShort();
// optional field
Integer localMaxTxPwrConstraint40MHz =
parseOptionalField(innerObj, "Local Max Tx Pwr Constraint 40MHz");
Integer localMaxTxPwrConstraint80MHz =
parseOptionalField(innerObj, "Local Max Tx Pwr Constraint 80MHz");
Integer localMaxTxPwrConstraint160MHz =
parseOptionalField(innerObj, "Local Max Tx Pwr Constraint 160MHz");
Short localMaxTxPwrConstraint40MHz =
IEUtils.parseOptionalShortField(
innerObj,
"Local Max Tx Pwr Constraint 40MHz"
);
Short localMaxTxPwrConstraint80MHz =
IEUtils.parseOptionalShortField(
innerObj,
"Local Max Tx Pwr Constraint 40MHz"
);
Short localMaxTxPwrConstraint160MHz =
IEUtils.parseOptionalShortField(
innerObj,
"Local Max Tx Pwr Constraint 40MHz"
);
return new TxPwrInfo(
localMaxTxPwrConstraint20MHz,
localMaxTxPwrConstraint40MHz,
@@ -67,17 +83,6 @@ public class TxPwrInfo {
);
}
private static Integer parseOptionalField(
JsonObject contents,
String fieldName
) {
JsonElement element = contents.get(fieldName);
if (element == null) {
return null;
}
return element.getAsInt();
}
@Override
public int hashCode() {
return Objects.hash(
@@ -108,13 +113,4 @@ public class TxPwrInfo {
other.localMaxTxPwrConstraint40MHz &&
localMaxTxPwrConstraint80MHz == other.localMaxTxPwrConstraint80MHz;
}
@Override
public String toString() {
return "TxPwrInfo [localMaxTxPwrConstraint20MHz=" +
localMaxTxPwrConstraint20MHz + ", localMaxTxPwrConstraint40MHz=" +
localMaxTxPwrConstraint40MHz + ", localMaxTxPwrConstraint80MHz=" +
localMaxTxPwrConstraint80MHz + ", localMaxTxPwrConstraint160MHz=" +
localMaxTxPwrConstraint160MHz + "]";
}
}

View File

@@ -15,9 +15,12 @@ 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).
* wifiscan entries. Introduced in 802.11ac (2013). Refer to the 802.11
* specification (section 9.4.2.158)
*/
public class VHTOperation {
/** Defined in 802.11 table 9-92 */
public static final int TYPE = 192;
/**
* This field is 0 if the channel width is 20 MHz or 40 MHz, and 1 otherwise.
@@ -173,4 +176,4 @@ public class VHTOperation {
channelWidth == other.channelWidth &&
Arrays.equals(vhtMcsForNss, other.vhtMcsForNss);
}
}
}

View File

@@ -0,0 +1,12 @@
/*
* 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.
*/
/**
* Information elements (IEs) defined in the 802.11 specifications.
*/
package com.facebook.openwifi.cloudsdk.ies;

View File

@@ -70,7 +70,12 @@ public class UCentralKafkaConsumer {
/** The state payload JSON. */
public final JsonObject payload;
/** Unix time (ms). */
/**
* The record timestamp (Unix time, in ms).
*
* Depending on the broker configuration for "message.timestamp.type",
* this may either be the "CreateTime" or "LogAppendTime".
*/
public final long timestampMs;
/** Constructor. */
@@ -85,7 +90,12 @@ public class UCentralKafkaConsumer {
}
}
/** Kafka record listener interface. */
/**
* Kafka record listener interface.
*
* The inputs must NOT be mutated, as they may be passed to multiple
* listeners and may result in ConcurrentModificationException.
*/
public interface KafkaListener {
/** Handle a list of state records. */
void handleStateRecords(List<KafkaRecord> records);
@@ -271,7 +281,6 @@ public class UCentralKafkaConsumer {
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)) {

View File

@@ -0,0 +1,12 @@
/*
* 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.
*/
/**
* Kafka consumer and producer functionality required by the CloudSDK.
*/
package com.facebook.openwifi.cloudsdk.kafka;

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.openwifi.cloudsdk.models.ap;
import java.util.List;
import java.util.Map;
import com.google.gson.annotations.SerializedName;
/**
* AP capabilities schema.
*
* @see <a href="https://github.com/Telecominfraproject/wlan-ucentral-schema/blob/main/system/capabilities.uc">capabilities.uc</a>
*/
public class Capabilities {
public String compatible;
public String model;
public String platform;
public Map<String, List<String>> network;
public static class Switch {
public boolean enable;
public boolean reset;
}
@SerializedName("switch") public Map<String, Switch> switch_;
public static class Phy {
public int tx_ant;
public int rx_ant;
public int[] frequencies;
public int[] channels;
public int[] dfs_channels;
public String[] htmode;
public String[] band;
public int ht_capa;
public int vht_capa;
public int[] he_phy_capa;
public int[] he_mac_capa;
public String country;
public String dfs_region;
public int temperature;
}
public Map<String, Phy> wifi;
// TODO The fields below were omitted
// macaddr;
// country_code;
// label_macaddr;
}

View File

@@ -1,21 +0,0 @@
/*
* 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.openwifi.cloudsdk.models.ap;
import com.google.gson.JsonObject;
import com.google.gson.annotations.SerializedName;
public class DeviceCapabilities {
public String compatible;
public String model;
public String platform;
public JsonObject network;
@SerializedName("switch") public JsonObject switch_;
public JsonObject wifi;
}

View File

@@ -11,6 +11,11 @@ package com.facebook.openwifi.cloudsdk.models.ap;
import com.google.gson.JsonObject;
import com.google.gson.annotations.SerializedName;
/**
* AP statistics/telemetry schema.
*
* @see <a href="https://github.com/Telecominfraproject/wlan-ucentral-schema/blob/main/state/state.yml">state.yml</a>
*/
public class State {
public static class Interface {
public static class Client {
@@ -54,6 +59,8 @@ public class State {
public int ack_signal;
public int ack_signal_avg;
public JsonObject[] tid_stats; // TODO: see cfg80211_tid_stats
// TODO ipaddr_v4 - either string or object (ip4leases), but duplicated in "clients"
}
public Association[] associations;
@@ -115,7 +122,7 @@ public class State {
public static class Radio {
public long active_ms;
public long busy_ms;
public int channel;
public int channel; // TODO might be int[] array??
public String channel_width;
public long noise;
public String phy;

View File

@@ -0,0 +1,96 @@
/*
* 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.openwifi.cloudsdk.models.ap;
import java.util.List;
import com.google.gson.JsonPrimitive;
import com.google.gson.annotations.SerializedName;
/**
* AP configuration schema.
*
* @see <a href="https://github.com/Telecominfraproject/wlan-ucentral-schema/blob/main/schema/ucentral.yml">ucentral.yml</a>
*/
public class UCentralSchema {
public static class Radio {
public String band;
public int bandwidth;
public JsonPrimitive channel; // either "auto" or int
@SerializedName("valid-channels") public int[] validChannels;
public String country;
@SerializedName("allow-dfs") public boolean allowDfs;
@SerializedName("channel-mode") public String channelMode;
@SerializedName("channel-width") public int channelWidth;
@SerializedName("require-mode") public String requireMode;
public String mimo;
@SerializedName("tx-power") public int txPower;
@SerializedName("legacy-rates") public boolean legacyRates;
@SerializedName("beacon-interval") public int beaconInterval;
@SerializedName("dtim-period") public int dtimPeriod;
@SerializedName("maximum-clients") public int maximumClients;
public static class Rates {
public int beacon;
public int multicast;
}
public Rates rates;
public static class HESettings {
@SerializedName("multiple-bssid") public boolean multipleBssid;
public boolean ema;
@SerializedName("bss-color") public int bssColor;
}
@SerializedName("he-settings") public HESettings heSettings;
@SerializedName("hostapd-iface-raw") public String[] hostapdIfaceRaw;
}
public List<Radio> radios;
public static class Metrics {
public static class Statistics {
public int interval;
public List<String> types;
}
public Statistics statistics;
public static class Health {
public int interval;
}
public Health health;
public static class WifiFrames {
public List<String> filters;
}
@SerializedName("wifi-frames") public WifiFrames wifiFrames;
public static class DhcpSnooping {
public List<String> filters;
}
@SerializedName("dhcp-snooping") public DhcpSnooping dhcpSnooping;
}
public Metrics metrics;
// TODO also add fields below as needed
// unit
// globals
// definitions
// ethernet
// switch
// interfaces
// services
}

View File

@@ -10,10 +10,15 @@ package com.facebook.openwifi.cloudsdk.models.ap;
import java.util.Objects;
import com.facebook.openwifi.cloudsdk.WifiScanEntry;
/**
* Represents a single entry in wifi scan results.
* ies[] array is not stored directly, but parsed into WifiScanEntry fields
* Wi-Fi scan result schema.
* <p>
* Note that the {@code ies[]} array is not stored here, but parsed into
* {@link WifiScanEntry#ieContainer}.
*
* @see <a href="https://github.com/Telecominfraproject/wlan-ucentral-schema/blob/main/command/cmd_wifiscan.uc">cmd_wifiscan.uc</a>
*/
public class WifiScanEntryResult {
public int channel;

View File

@@ -0,0 +1,14 @@
/*
* 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.
*/
/**
* Schemas originating from the AP-NOS (wlan-ap).
*
* @see <a href="https://github.com/Telecominfraproject/wlan-ucentral-schema">wlan-ucentral-schema</a>
*/
package com.facebook.openwifi.cloudsdk.models.ap;

View File

@@ -8,10 +8,10 @@
package com.facebook.openwifi.cloudsdk.models.gw;
import com.google.gson.JsonObject;
import com.facebook.openwifi.cloudsdk.models.ap.Capabilities;
public class DeviceCapabilities {
public JsonObject capabilities;
public Capabilities capabilities;
public long firstUpdate;
public long lastUpdate;
public String serialNumber;

View File

@@ -0,0 +1,18 @@
/*
* 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.openwifi.cloudsdk.models.gw;
public class ScriptRequest {
public String serialNumber;
public long timeout = 30; // in seconds
public String type; // "shell", "ucode", "uci"
public String script;
public String scriptId; // required but unused?
public long when = 0;
}

View File

@@ -0,0 +1,14 @@
/*
* 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.
*/
/**
* Schemas defined in the uCentral Gateway.
*
* @see <a href="https://github.com/Telecominfraproject/wlan-cloud-ucentralgw">wlan-cloud-ucentralgw</a>
*/
package com.facebook.openwifi.cloudsdk.models.gw;

View File

@@ -0,0 +1,14 @@
/*
* 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.
*/
/**
* Schemas defined in the Provisioning service.
*
* @see <a href="https://github.com/Telecominfraproject/wlan-cloud-owprov">wlan-cloud-owprov</a>
*/
package com.facebook.openwifi.cloudsdk.models.prov;

View File

@@ -0,0 +1,14 @@
/*
* 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.
*/
/**
* Schemas defined in the Provisioning service specific to RRM.
*
* @see <a href="https://github.com/Telecominfraproject/wlan-cloud-owprov/blob/main/openapi/rrm_provider.yaml">rrm_provider.yaml</a>
*/
package com.facebook.openwifi.cloudsdk.models.prov.rrm;

View File

@@ -0,0 +1,13 @@
/*
* 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.
*/
/**
* Library providing clients and models for the OpenWiFi uCentral-based
* CloudSDK.
*/
package com.facebook.openwifi.cloudsdk;

3
lib-rca/README.md Normal file
View File

@@ -0,0 +1,3 @@
# Root Cause Analysis (RCA) Java Library
A Java library which analyzes statistics and provides root cause analysis (RCA) for clients.
This is a work in progress.

72
lib-rca/pom.xml Normal file
View File

@@ -0,0 +1,72 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>openwifi-librca</artifactId>
<packaging>jar</packaging>
<parent>
<groupId>com.facebook</groupId>
<artifactId>openwifi-base</artifactId>
<version>2.7.0</version>
</parent>
<properties>
<!-- Hack for static files located in root project -->
<myproject.root>${project.basedir}/..</myproject.root>
</properties>
<build>
<finalName>openwifi-librca</finalName>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*</include>
</includes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
</plugin>
<plugin>
<groupId>com.diffplug.spotless</groupId>
<artifactId>spotless-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,13 @@
/*
* 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.openwifi.librca;
public class RootCauseAnalyzer {
}

View File

@@ -0,0 +1,212 @@
/*
* 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.openwifi.librca.inputs;
import java.util.ArrayList;
import java.util.List;
/** Define root cause analysis configuration parameters */
public final class RCAParams {
// Note: we expect to receive these parameters in json format, so for now
// we do not include a constructor which takes in the member vars as inputs
/** Look-back window in ms */
public final int detectionWindowMs;
// KPI calculation parameters
/** Minimum acceptable estimated throughput (Mbps) */
public final double minEstimatedThroughputMbps;
/** Percentile (units are %) of estimated throughputs to use as the KPI */
public final double throughputAggregationPercentile;
/** Maximum acceptable latency (ms) */
public final int maxLatencyThresholdMs;
/** Maximum acceptable jitter (ms) */
public final int maxJitterThresholdMs;
/**
* Maximum acceptable disconnection rate (disconnetions per hour). Note that
* this signifies a rate and the units happen to be per hour - this does not
* signify that every contiguous one-hour period be checked.
*/
public final int maxDisconnectionRatePerHour;
// High Level metrics thresholds
/** Minimum acceptable tx rate (Mbps) */
public final double minTxRateMbps;
/** Maximum acceptable Packet Error Rate (PER) (units are %) */
public final double maxPERPercent;
/** Minimum acceptable idle airtime (units are %) */
public final double minIdleAirtimePercent;
/** Maximum acceptable number of clients for one radio */
public final int maxNumClients;
// Low Level metrics thresholds
/** Minimum acceptable RSSI (dBm) */
public final int minRssidBm;
/** Maximum acceptable noise (dBm) */
public final int maxNoisedBm;
/** Maximum acceptable intf airtime (units are %) */
public final double maxIntfAirtimePercent;
/** Maximum acceptable number of neighbors */
public final int maxNumNeighbors;
/** Minimum acceptable client bandwidth (MHz) for non-2G bands / */
public final int minClientBandwidthMHz;
/** Minimum acceptable Access Point (AP) bandwidth (MHz) for non-2G bands */
public final int minApBandwidthMHz;
/** Minimum acceptable self airtime ratio (units are %) */
public final double minSelfAirtimeRatioPercent;
/** Maximum acceptable tx dropped ratio (units are %) */
public final double maxTxDroppedRatioPercent;
/** Default constructor */
public RCAParams() {
// 6 hours -> 21600000 ms
this.detectionWindowMs = 21600000;
this.minEstimatedThroughputMbps = 10;
this.throughputAggregationPercentile = 10.0;
this.maxLatencyThresholdMs = 50;
this.maxJitterThresholdMs = 20;
this.maxDisconnectionRatePerHour = 20;
this.minTxRateMbps = 50;
this.maxPERPercent = 10.0;
this.minIdleAirtimePercent = 10.0;
this.maxNumClients = 10;
this.minRssidBm = -70;
this.maxNoisedBm = -95;
this.maxIntfAirtimePercent = 75.0;
this.maxNumNeighbors = 10;
this.minClientBandwidthMHz = 80;
this.minApBandwidthMHz = 80;
this.minSelfAirtimeRatioPercent = 25.0;
this.maxTxDroppedRatioPercent = 0.1;
}
/**
* Confirm that the given value is positive. If it is not, add a String
* describing the problem to {@code errors}.
*/
private static void validatePositive(
String varName,
int value,
List<String> errors
) {
if (value <= 0) {
errors.add(varName + " must be positive.");
}
}
/**
* Confirm that the given value is positive. If it is not, add a String
* describing the problem to {@code errors}.
*/
private static void validatePositive(
String varName,
double value,
List<String> errors
) {
if (value <= 0) {
errors.add(varName + " must be positive.");
}
}
/**
* Confirm that the given value is a valid percentile (between 0 and 100
* inclusive). If it is not, add a String describing the problem to
* {@code errors}.
*/
private static void validatePercentile(
String varName,
double value,
List<String> errors
) {
if (value < 0 || value > 100) {
errors.add(varName + " must be between 0 and 100 inclusive.");
}
}
/** Return a list of errors (empty list of no errors) */
public List<String> validate() {
List<String> errors = new ArrayList<>();
validatePositive("Detection window", detectionWindowMs, errors);
validatePositive(
"Minimum estimated throughput",
minEstimatedThroughputMbps,
errors
);
validatePercentile(
"Thoughput aggregation percentile",
throughputAggregationPercentile,
errors
);
validatePositive(
"Maximum latency threshold",
maxLatencyThresholdMs,
errors
);
validatePositive(
"Maximum jitter threshold",
maxJitterThresholdMs,
errors
);
validatePositive(
"Maximum disconnection rate",
maxDisconnectionRatePerHour,
errors
);
validatePositive("Minimum tx rate", minTxRateMbps, errors);
validatePercentile(
"Maximum Packet Error Rate (PER)",
maxPERPercent,
errors
);
validatePercentile(
"Minimum idle airtime",
minIdleAirtimePercent,
errors
);
validatePositive("Maximum number of clients", maxNumClients, errors);
validatePercentile(
"Maximum intf airtime",
maxIntfAirtimePercent,
errors
);
validatePositive(
"Maximum number of neighbors",
maxNumNeighbors,
errors
);
validatePositive(
"Minimum client bandwidth",
minClientBandwidthMHz,
errors
);
validatePositive(
"Minimum Access Point (AP) bandwidth",
minApBandwidthMHz,
errors
);
validatePercentile(
"Minimum self airtime ratio",
minSelfAirtimeRatioPercent,
errors
);
validatePercentile(
"Maximum tx dropped ratio",
maxTxDroppedRatioPercent,
errors
);
return errors;
}
}

View File

@@ -0,0 +1,12 @@
/*
* 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.
*/
/**
* Library providing Root Cause Analysis (RCA) functionality.
*/
package com.facebook.openwifi.librca;

View File

@@ -0,0 +1,23 @@
/*
* 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.openwifi.librca.stats;
import java.util.List;
/**
* Aggregated statistics for each client.
* Mainly handle KPI and metric calculations.
*/
public class ClientStats {
/** Client MAC */
public String station;
/** LinkStats that are of the same station(client) */
public List<LinkStats> connections;
}

View File

@@ -0,0 +1,75 @@
/*
* 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.openwifi.librca.stats;
import java.util.List;
/**
* Aggregation Statistics Model of InputStats.
* Aggregate by bssid, station and RadioConfig.
*/
public class LinkStats {
public static class RadioConfig {
public int channel;
public int channelWidth;
public int txPower;
public String phy;
}
public static class AssociationInfo {
/** Rate information for receive/transmit data rate. */
public static class Rate {
public long bitRate;
public int chWidth;
public int mcs;
}
public long connected;
public long inactive;
public int rssi;
public long rxBytes;
public long rxPackets;
public Rate rxRate;
public long txBytes;
public long txDuration;
public long txFailed;
public long txPackets;
public Rate txRate;
public long txRetries;
public int ackSignal;
public int ackSignalAvg;
// The metrics below are from Interface the client was connected to.
public long txPacketsCounters;
public long txErrorsCounters;
public long txDroppedCounters;
// The metrics below are from the radio the client was associated to.
public long activeMsRadio;
public long busyMsRadio;
public long noiseRadio;
public long receiveMsRadio;
public long transmitMsRadio;
/** Unix time in milliseconds */
public long timestamp;
}
/** BSSID of the AP radio */
public String bssid;
/** Client MAC */
public String station;
/** Radio configuration parameters */
public RadioConfig radioConfig;
/** Association list */
public List<AssociationInfo> associationInfoList;
}

View File

@@ -0,0 +1,78 @@
/*
* 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.openwifi.librca.stats.inputs;
/**
* Input data model.
*
* TODO: very incomplete
*/
public class InputStats {
/** Radio parameters */
public static class Radio {
public long active_ms;
public long busy_ms;
public int channel;
public String channel_width;
public long noise;
public String phy;
public long receive_ms;
public long transmit_ms;
public int tx_power;
}
public static class SSID {
public static class Association {
public static class Rate {
public long bitrate;
public int chwidth;
public int mcs;
}
public String bssid; // bssid of the AP radio
public String station; // client MAC
public long connected;
public long inactive;
public int rssi;
public long rx_bytes;
public long rx_packets;
public Rate rx_rate;
public long tx_bytes;
public long tx_duration;
public long tx_failed;
public long tx_offset;
public long tx_packets;
public Rate tx_rate;
public long tx_retries;
public int ack_signal;
public int ack_signal_avg;
}
public Association[] associations;
public Radio radio;
}
/** Counters are for the wireless interface as a whole */
public static class Counters {
public long rx_bytes;
public long rx_packets;
public long rx_errors;
public long rx_dropped;
public long tx_bytes;
public long tx_packets;
public long tx_errors;
public long tx_dropped;
}
public SSID[] ssids;
public Counters counters;
/** Unix time in milliseconds */
public long timestamp;
}

View File

@@ -0,0 +1,7 @@
log4j.rootLogger=DEBUG, stdout
#log4j.rootLogger=ERROR, stdout, file
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd'T'HH:mm:ss.SSS} %-5p [%c{1}:%L] - %m%n

View File

@@ -0,0 +1,19 @@
/*
* 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.openwifi.librca;
import org.junit.jupiter.api.Test;
public class RootCauseAnalyzerTest {
@Test
void placeholderTest() {
return;
}
}

4
owrrm/.gitignore vendored
View File

@@ -1,4 +0,0 @@
/*.log*
/device_config.json
/settings.json
/topology.json

View File

@@ -94,6 +94,30 @@ levels of these APs will be determined by the following steps:
Parameters:
* `mode`: "measure_ap_ap"
* `coverageThreshold`: Coverage threshold between APs in dBm
* values: int < 30 (default: -70)
* values: int < 30 (default: -70)
* `nthSmallestRssi`: the nth smallest RSSI that is used for tx power calculation
* values: int >= 0 (default: 0)
## Client Steering
`ClientSteeringOptimizer` and its subclasses implement client steering
algorithms via 802.11k/v/r mechanisms, with the goal of moving clients to
optimal APs and/or bands.
**Client steering is a work in progress and NOT currently functional.**
### `SingleAPBandSteering`
This algorithm performs same-AP RRSI-based steering only, using a simple
decision and backoff procedure.
Parameters:
* `mode`: "band"
* `minRssi2G`: RSSI (dBm) below which a client on the 2G band should be kicked
* values: int < 30 (default: -87)
* `maxRssi2G`: RSSI (dBm) above which a client on the 2G band should roam to
5G/6G
* values: int < 30 (default: -67)
* `minRssiNon2G`: RSSI (dBm) below which a client on the 5G/6G band should roam
to 2G
* values: int < 30 (default: -82)
* `backoffTimeSec`: Backoff time (seconds) for all APs and radios
* values: int >= 0 (default: 300)

View File

@@ -551,6 +551,12 @@ components:
additionalProperties:
type: integer
format: int32
apClientActionMap:
type: object
additionalProperties:
type: object
additionalProperties:
type: string
Certificate:
type: object
properties:

View File

@@ -30,6 +30,7 @@ import com.facebook.openwifi.rrm.modules.Modeler;
import com.facebook.openwifi.rrm.modules.ProvMonitor;
import com.facebook.openwifi.rrm.modules.RRMScheduler;
import com.facebook.openwifi.rrm.mysql.DatabaseManager;
import com.facebook.openwifi.rrm.rca.modules.StationPinger;
/**
* RRM service runner.
@@ -134,10 +135,16 @@ public class RRM {
) : null;
KafkaRunner kafkaRunner = (consumer == null && producer == null)
? null : new KafkaRunner(consumer, producer);
StationPinger stationPinger = new StationPinger(
config.rcaConfig.stationPingerParams,
client,
consumer
);
// Add shutdown hook
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
logger.debug("Running shutdown hook...");
stationPinger.shutdown();
if (kafkaRunner != null) {
kafkaRunner.shutdown();
}

View File

@@ -20,6 +20,9 @@ import com.facebook.openwifi.rrm.optimizers.channel.ChannelOptimizer;
import com.facebook.openwifi.rrm.optimizers.channel.LeastUsedChannelOptimizer;
import com.facebook.openwifi.rrm.optimizers.channel.RandomChannelInitializer;
import com.facebook.openwifi.rrm.optimizers.channel.UnmanagedApAwareChannelOptimizer;
import com.facebook.openwifi.rrm.optimizers.clientsteering.ClientSteeringOptimizer;
import com.facebook.openwifi.rrm.optimizers.clientsteering.ClientSteeringState;
import com.facebook.openwifi.rrm.optimizers.clientsteering.SingleAPBandSteering;
import com.facebook.openwifi.rrm.optimizers.tpc.LocationBasedOptimalTPC;
import com.facebook.openwifi.rrm.optimizers.tpc.MeasurementBasedApApTPC;
import com.facebook.openwifi.rrm.optimizers.tpc.MeasurementBasedApClientTPC;
@@ -33,6 +36,14 @@ public class RRMAlgorithm {
private static final Logger logger =
LoggerFactory.getLogger(RRMAlgorithm.class);
/**
* Client steering state. A single instance must be passed into all client
* steering algorithms, as the state must persist across runs of the
* optimizers.
*/
private static final ClientSteeringState clientSteeringState =
new ClientSteeringState();
/** RRM algorithm type enum. */
public enum AlgorithmType {
OptimizeChannel (
@@ -42,6 +53,10 @@ public class RRMAlgorithm {
OptimizeTxPower (
"Optimize tx power configuration",
"Run transmit power control algorithm"
),
ClientSteering (
"Steer clients onto the optimal AP and band",
"Run client steering algorithm"
);
/** The long name. */
@@ -72,6 +87,13 @@ public class RRMAlgorithm {
* @see TPC#computeTxPowerMap()
*/
public Map<String, Map<String, Integer>> txPowerMap;
/**
* Computed actions for each AP-client pair.
*
* @see ClientSteeringOptimizer#computeApClientActionMap(boolean)
*/
public Map<String, Map<String, String>> apClientActionMap;
}
/** The algorithm name (should be AlgorithmType enum string). */
@@ -286,6 +308,41 @@ public class RRMAlgorithm {
configManager.queueZoneAndWakeUp(zone);
}
}
} else if (
name.equals(RRMAlgorithm.AlgorithmType.ClientSteering.name())
) {
logger.info(
"Zone '{}': Running client steering optimizer (mode='{}')",
zone,
mode
);
ClientSteeringOptimizer optimizer;
switch (mode) {
default:
if (!allowDefaultMode || !mode.isEmpty()) {
result.error = modeErrorStr;
return result;
}
logger.info("Using default algorithm mode...");
// fall through
case SingleAPBandSteering.ALGORITHM_ID:
optimizer = SingleAPBandSteering.makeWithArgs(
modeler.getDataModelCopy(),
zone,
deviceDataManager,
clientSteeringState,
args
);
break;
}
result.apClientActionMap =
optimizer.computeApClientActionMap(dryRun);
if (!dryRun) {
optimizer.steer(result.apClientActionMap);
if (updateImmediately) {
configManager.queueZoneAndWakeUp(zone);
}
}
} else {
result.error = String.format("Unknown algorithm: '%s'", name);
}

View File

@@ -10,6 +10,8 @@ package com.facebook.openwifi.rrm;
import java.util.Map;
import com.facebook.openwifi.rrm.rca.RCAConfig;
/**
* RRM service configuration model.
*/
@@ -405,6 +407,9 @@ public class RRMConfig {
/** Module configuration. */
public ModuleConfig moduleConfig = new ModuleConfig();
/** Root cause analysis configuration. */
public RCAConfig rcaConfig = new RCAConfig();
/** Construct RRMConfig from environment variables. */
public static RRMConfig fromEnv(Map<String, String> env) {
RRMConfig config = new RRMConfig();
@@ -583,6 +588,9 @@ public class RRMConfig {
// @formatter:on
/* RCAConfig */
config.rcaConfig = RCAConfig.fromEnv(env);
return config;
}
}

View File

@@ -8,7 +8,6 @@
package com.facebook.openwifi.rrm.modules;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -47,9 +46,6 @@ public class ConfigManager implements Runnable {
/** Runtime per-device data. */
private class DeviceData {
/** Last received device config. */
public UCentralApConfiguration config;
/** Last config time (in monotonic ns). */
public Long lastConfigTimeNs;
}
@@ -183,7 +179,7 @@ public class ConfigManager implements Runnable {
long now = System.nanoTime();
// Apply any config updates locally
List<String> devicesNeedingUpdate = new ArrayList<>();
Map<String, String> devicesNeedingUpdate = new HashMap<>();
final long CONFIG_DEBOUNCE_INTERVAL_NS =
params.configDebounceIntervalSec * 1_000_000_000L;
Set<String> zonesToUpdateCopy = new HashSet<>(zonesToUpdate);
@@ -204,11 +200,12 @@ public class ConfigManager implements Runnable {
);
continue;
}
data.config = new UCentralApConfiguration(device.configuration);
UCentralApConfiguration config =
new UCentralApConfiguration(device.configuration);
// Call receive listeners
for (ConfigListener listener : configListeners.values()) {
listener.receiveDeviceConfig(device.serialNumber, data.config);
listener.receiveDeviceConfig(device.serialNumber, config);
}
// Check if there are requested updates for this zone
String deviceZone =
@@ -236,7 +233,7 @@ public class ConfigManager implements Runnable {
for (ConfigListener listener : configListeners.values()) {
boolean wasModified = listener.processDeviceConfig(
device.serialNumber,
data.config
config
);
if (wasModified) {
modified = true;
@@ -257,7 +254,8 @@ public class ConfigManager implements Runnable {
);
continue;
} else {
devicesNeedingUpdate.add(device.serialNumber);
devicesNeedingUpdate
.put(device.serialNumber, config.toString());
}
}
}
@@ -275,19 +273,26 @@ public class ConfigManager implements Runnable {
devicesNeedingUpdate.size()
);
} else {
// TODO: Replace with the newer owprov API to send only deltas, not
// the full configuration blobs:
// PUT /configurationOverrides/{serialNumber}?source=owrrm
logger.info(
"Sending config to {} device(s): {}",
devicesNeedingUpdate.size(),
String.join(", ", devicesNeedingUpdate)
String.join(", ", devicesNeedingUpdate.keySet())
);
for (String serialNumber : devicesNeedingUpdate) {
for (
Map.Entry<String, String> entry : devicesNeedingUpdate
.entrySet()
) {
String serialNumber = entry.getKey();
DeviceData data = deviceDataMap.get(serialNumber);
logger.info(
"Device {}: sending new configuration...",
serialNumber
);
data.lastConfigTimeNs = System.nanoTime();
client.configure(serialNumber, data.config.toString());
client.configure(serialNumber, entry.getValue());
}
}
}

View File

@@ -8,6 +8,7 @@
package com.facebook.openwifi.rrm.modules;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@@ -20,13 +21,15 @@ import java.util.concurrent.LinkedBlockingQueue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.facebook.openwifi.cloudsdk.StateInfo;
import com.facebook.openwifi.cloudsdk.UCentralApConfiguration;
import com.facebook.openwifi.cloudsdk.UCentralClient;
import com.facebook.openwifi.cloudsdk.UCentralUtils;
import com.facebook.openwifi.cloudsdk.WifiScanEntry;
import com.facebook.openwifi.cloudsdk.kafka.UCentralKafkaConsumer;
import com.facebook.openwifi.cloudsdk.kafka.UCentralKafkaConsumer.KafkaRecord;
import com.facebook.openwifi.cloudsdk.models.ap.State;
import com.facebook.openwifi.cloudsdk.models.ap.Capabilities;
import com.facebook.openwifi.cloudsdk.models.ap.UCentralSchema;
import com.facebook.openwifi.cloudsdk.models.gw.DeviceCapabilities;
import com.facebook.openwifi.cloudsdk.models.gw.DeviceWithStatus;
import com.facebook.openwifi.cloudsdk.models.gw.ServiceEvent;
@@ -36,7 +39,6 @@ import com.facebook.openwifi.rrm.DeviceDataManager;
import com.facebook.openwifi.rrm.RRMConfig.ModuleConfig.ModelerParams;
import com.facebook.openwifi.rrm.Utils;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
@@ -94,15 +96,15 @@ public class Modeler implements Runnable {
new ConcurrentHashMap<>();
/** List of latest states per device. */
public Map<String, List<State>> latestStates =
public Map<String, List<StateInfo>> latestStates =
new ConcurrentHashMap<>();
/** List of radio info per device. */
public Map<String, JsonArray> latestDeviceStatusRadios =
public Map<String, List<UCentralSchema.Radio>> latestDeviceStatusRadios =
new ConcurrentHashMap<>();
/** List of capabilities per device. */
public Map<String, JsonObject> latestDeviceCapabilities =
public Map<String, Map<String, Capabilities.Phy>> latestDeviceCapabilitiesPhy =
new ConcurrentHashMap<>();
}
@@ -168,10 +170,15 @@ public class Modeler implements Runnable {
consumer.addKafkaListener(
getClass().getSimpleName(),
new UCentralKafkaConsumer.KafkaListener() {
// NOTE: list copying due to potential modification in run()
@Override
public void handleStateRecords(List<KafkaRecord> records) {
dataQueue.offer(
new InputData(InputDataType.STATE, records)
new InputData(
InputDataType.STATE,
new ArrayList<>(records)
)
);
}
@@ -180,7 +187,10 @@ public class Modeler implements Runnable {
List<KafkaRecord> records
) {
dataQueue.offer(
new InputData(InputDataType.WIFISCAN, records)
new InputData(
InputDataType.WIFISCAN,
new ArrayList<>(records)
)
);
}
@@ -265,9 +275,12 @@ public class Modeler implements Runnable {
continue;
}
JsonObject state = records.data.get(0).data;
long timestamp = records.data.get(0).recorded;
if (state != null) {
try {
State stateModel = gson.fromJson(state, State.class);
StateInfo stateModel =
gson.fromJson(state, StateInfo.class);
stateModel.timestamp = timestamp;
dataModel.latestStates.computeIfAbsent(
device.serialNumber,
k -> new LinkedList<>()
@@ -302,12 +315,15 @@ public class Modeler implements Runnable {
JsonObject state = record.payload.getAsJsonObject("state");
if (state != null) {
try {
State stateModel = gson.fromJson(state, State.class);
List<State> latestStatesList = dataModel.latestStates
.computeIfAbsent(
record.serialNumber,
k -> new LinkedList<>()
);
StateInfo stateModel =
gson.fromJson(state, StateInfo.class);
stateModel.timestamp = record.timestampMs;
List<StateInfo> latestStatesList =
dataModel.latestStates
.computeIfAbsent(
record.serialNumber,
k -> new LinkedList<>()
);
while (
latestStatesList.size() >= params.stateBufferSize
) {
@@ -376,9 +392,9 @@ public class Modeler implements Runnable {
String serialNumber,
DeviceCapabilities capabilities
) {
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
serialNumber,
capabilities.capabilities.getAsJsonObject("wifi")
capabilities.capabilities.wifi
);
}
@@ -390,10 +406,11 @@ public class Modeler implements Runnable {
UCentralApConfiguration config
) {
// Get old vs new radios info and store the new radios info
JsonArray newRadioList = config.getRadioConfigList();
List<UCentralSchema.Radio> newRadioList = config.getRadioConfigList();
Set<String> newRadioBandsSet = config.getRadioBandsSet(newRadioList);
JsonArray oldRadioList = dataModel.latestDeviceStatusRadios
.put(serialNumber, newRadioList);
List<UCentralSchema.Radio> oldRadioList =
dataModel.latestDeviceStatusRadios
.put(serialNumber, newRadioList);
Set<String> oldRadioBandsSet = config.getRadioBandsSet(oldRadioList);
// Print info only when there are any updates
@@ -448,7 +465,7 @@ public class Modeler implements Runnable {
logger.debug("Removed some status entries from data model");
}
if (
dataModel.latestDeviceCapabilities.entrySet()
dataModel.latestDeviceCapabilitiesPhy.entrySet()
.removeIf(e -> !isRRMEnabled(e.getKey()))
) {
logger.debug("Removed some capabilities entries from data model");

View File

@@ -19,8 +19,10 @@ import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.facebook.openwifi.cloudsdk.models.ap.Capabilities;
import com.facebook.openwifi.cloudsdk.AggregatedState;
import com.facebook.openwifi.cloudsdk.WifiScanEntry;
import com.facebook.openwifi.cloudsdk.StateInfo;
import com.facebook.openwifi.cloudsdk.ies.HTOperation;
import com.facebook.openwifi.cloudsdk.ies.VHTOperation;
import com.facebook.openwifi.cloudsdk.models.ap.State;
@@ -30,9 +32,6 @@ import com.facebook.openwifi.cloudsdk.models.ap.State.Interface.SSID.Association
import com.facebook.openwifi.rrm.aggregators.Aggregator;
import com.facebook.openwifi.rrm.aggregators.MeanAggregator;
import com.facebook.openwifi.rrm.modules.Modeler.DataModel;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
/**
* Modeler utilities.
@@ -406,22 +405,13 @@ public class ModelerUtils {
*/
static void addStateToAggregation(
Map<String, List<AggregatedState>> bssidToAggregatedStates,
State state
StateInfo state
) {
for (Interface stateInterface : state.interfaces) {
if (stateInterface.ssids == null) {
continue;
}
for (SSID ssid : stateInterface.ssids) {
Map<String, Integer> radioInfo = new HashMap<>();
radioInfo.put("channel", ssid.radio.get("channel").getAsInt());
radioInfo.put(
"channel_width",
ssid.radio.get("channel_width").getAsInt()
);
radioInfo
.put("tx_power", ssid.radio.get("tx_power").getAsInt());
for (Association association : ssid.associations) {
if (association == null) {
continue;
@@ -434,7 +424,12 @@ public class ModelerUtils {
bssidToAggregatedStates
.computeIfAbsent(key, k -> new ArrayList<>());
AggregatedState aggState =
new AggregatedState(association, radioInfo);
new AggregatedState(
association,
ssid.counters,
ssid.radio,
state.timestamp
);
/**
* Indicate if the aggState can be merged into some old AggregatedState.
@@ -491,11 +486,11 @@ public class ModelerUtils {
new HashMap<>();
for (
Map.Entry<String, List<State>> deviceToStateList : dataModel.latestStates
Map.Entry<String, List<StateInfo>> deviceToStateList : dataModel.latestStates
.entrySet()
) {
String serialNumber = deviceToStateList.getKey();
List<State> states = deviceToStateList.getValue();
List<StateInfo> states = deviceToStateList.getValue();
if (states.isEmpty()) {
continue;
@@ -514,7 +509,7 @@ public class ModelerUtils {
aggregatedStates
.computeIfAbsent(serialNumber, k -> new HashMap<>());
for (State state : states) {
for (StateInfo state : states) {
if (refTimeMs - state.unit.localtime > obsoletionPeriodMs) {
// discard obsolete entries
break;
@@ -532,15 +527,16 @@ public class ModelerUtils {
* @param latestStates list of latest States per device
* @return map from device String to latest State
*/
public static Map<String, State> getLatestState(
Map<String, List<State>> latestStates
public static Map<String, StateInfo> getLatestState(
Map<String, List<StateInfo>> latestStates
) {
Map<String, State> latestState = new ConcurrentHashMap<>();
Map<String, StateInfo> latestState = new ConcurrentHashMap<>();
for (
Map.Entry<String, List<State>> stateEntry : latestStates.entrySet()
Map.Entry<String, List<StateInfo>> stateEntry : latestStates
.entrySet()
) {
String key = stateEntry.getKey();
List<State> value = stateEntry.getValue();
List<StateInfo> value = stateEntry.getValue();
if (value.isEmpty()) {
latestState.put(key, null);
} else {
@@ -562,24 +558,19 @@ public class ModelerUtils {
/** Return the radio's band, or null if band cannot be found */
public static String getBand(
State.Radio radio,
JsonObject deviceCapability
Map<String, Capabilities.Phy> capabilityPhy
) {
if (radio.phy == null) {
return null;
}
JsonElement radioCapabilityElement = deviceCapability.get(radio.phy);
if (radioCapabilityElement == null) {
Capabilities.Phy phy = capabilityPhy.get(radio.phy);
if (phy == null) {
return null;
}
JsonObject radioCapability = radioCapabilityElement.getAsJsonObject();
JsonElement bandsElement = radioCapability.get("band");
if (bandsElement == null) {
String[] bands = phy.band;
if (bands == null || bands.length == 0) {
return null;
}
JsonArray bands = bandsElement.getAsJsonArray();
if (bands.isEmpty()) {
return null;
}
return bands.get(0).getAsString();
return bands[0];
}
}

View File

@@ -0,0 +1,12 @@
/*
* 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.
*/
/**
* RRM service modules.
*/
package com.facebook.openwifi.rrm.modules;

View File

@@ -18,6 +18,7 @@ import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.facebook.openwifi.cloudsdk.models.ap.Capabilities;
import com.facebook.openwifi.cloudsdk.UCentralConstants;
import com.facebook.openwifi.cloudsdk.UCentralUtils;
import com.facebook.openwifi.cloudsdk.WifiScanEntry;
@@ -29,7 +30,6 @@ import com.facebook.openwifi.rrm.DeviceDataManager;
import com.facebook.openwifi.rrm.modules.ConfigManager;
import com.facebook.openwifi.rrm.modules.Modeler.DataModel;
import com.facebook.openwifi.rrm.modules.ModelerUtils;
import com.google.gson.JsonObject;
/**
* Channel optimizer base class.
@@ -142,7 +142,7 @@ public abstract class ChannelOptimizer {
.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber));
this.model.latestDeviceStatusRadios.keySet()
.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber));
this.model.latestDeviceCapabilities.keySet()
this.model.latestDeviceCapabilitiesPhy.keySet()
.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber));
}
@@ -349,7 +349,7 @@ public abstract class ChannelOptimizer {
* @param band the operational band (e.g., "2G")
* @param serialNumber the device's serial number
* @param state the latest state of all the devices
* @param latestDeviceCapabilities latest device capabilities
* @param latestDeviceCapabilitiesPhy latest device phy from capabilities
* @return the current channel and channel width (MHz) of the device in the
* given band; returns a current channel of 0 if no channel in the given
* band is found.
@@ -358,7 +358,7 @@ public abstract class ChannelOptimizer {
String band,
String serialNumber,
State state,
Map<String, JsonObject> latestDeviceCapabilities
Map<String, Map<String, Capabilities.Phy>> latestDeviceCapabilitiesPhy
) {
int currentChannel = 0;
int currentChannelWidth = MIN_CHANNEL_WIDTH;
@@ -370,14 +370,14 @@ public abstract class ChannelOptimizer {
) {
State.Radio radio = state.radios[radioIndex];
// check if radio is in band of interest
JsonObject deviceCapability =
latestDeviceCapabilities.get(serialNumber);
if (deviceCapability == null) {
Map<String, Capabilities.Phy> capabilitiesPhy =
latestDeviceCapabilitiesPhy.get(serialNumber);
if (capabilitiesPhy == null) {
continue;
}
final String radioBand = ModelerUtils.getBand(
radio,
deviceCapability
capabilitiesPhy
);
if (radioBand == null || !radioBand.equals(band)) {
continue;

View File

@@ -337,11 +337,11 @@ public class LeastUsedChannelOptimizer extends ChannelOptimizer {
Map<String, Map<String, List<Integer>>> deviceAvailableChannels =
UCentralUtils.getDeviceAvailableChannels(
model.latestDeviceStatusRadios,
model.latestDeviceCapabilities,
model.latestDeviceCapabilitiesPhy,
UCentralUtils.AVAILABLE_CHANNELS_BAND
);
Map<String, State> latestState =
Map<String, ? extends State> latestState =
ModelerUtils.getLatestState(model.latestStates);
Map<String, String> bssidsMap =
UCentralUtils.getBssidsMap(latestState);
@@ -397,7 +397,7 @@ public class LeastUsedChannelOptimizer extends ChannelOptimizer {
band,
serialNumber,
state,
model.latestDeviceCapabilities
model.latestDeviceCapabilitiesPhy
);
int currentChannel = currentChannelInfo[0];
// Filter out APs if the radios in the state do not contain a

View File

@@ -125,11 +125,11 @@ public class RandomChannelInitializer extends ChannelOptimizer {
Map<String, Map<String, List<Integer>>> deviceAvailableChannels =
UCentralUtils.getDeviceAvailableChannels(
model.latestDeviceStatusRadios,
model.latestDeviceCapabilities,
model.latestDeviceCapabilitiesPhy,
UCentralUtils.AVAILABLE_CHANNELS_BAND
);
Map<String, State> latestState =
Map<String, ? extends State> latestState =
ModelerUtils.getLatestState(model.latestStates);
Map<String, String> bssidsMap =
UCentralUtils.getBssidsMap(latestState);
@@ -208,7 +208,7 @@ public class RandomChannelInitializer extends ChannelOptimizer {
band,
serialNumber,
state,
model.latestDeviceCapabilities
model.latestDeviceCapabilitiesPhy
);
int currentChannel = currentChannelInfo[0];
int currentChannelWidth = currentChannelInfo[1];

View File

@@ -0,0 +1,12 @@
/*
* 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.
*/
/**
* Channel assignment algorithms.
*/
package com.facebook.openwifi.rrm.optimizers.channel;

View File

@@ -0,0 +1,161 @@
/*
* 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.openwifi.rrm.optimizers.clientsteering;
import java.util.Map;
import com.facebook.openwifi.rrm.DeviceConfig;
import com.facebook.openwifi.rrm.DeviceDataManager;
import com.facebook.openwifi.rrm.modules.Modeler.DataModel;
/** Client steering base class */
public abstract class ClientSteeringOptimizer {
// TODO call upon triggers, not only via one-off or period runs
/** Represents client steering actions an AP can take */
public static enum CLIENT_STEERING_ACTIONS {
/** Steer from 2G to 5G/6G */
STEER_UP,
/** Steer from 5G/6G to 2G */
STEER_DOWN,
/** Deauthenticate client */
DEAUTHENTICATE
}
/**
* 802.11 BTM reason codes (ex. for deauth).
*
* See IEEE Std 802.11-2016, 9.4.1.7, Table 9-45.
*/
public static class BTMReasonCode {
private BTMReasonCode() {}
public static final int UNSPECIFIED = 1;
public static final int PREV_AUTH_NOT_VALID = 2;
public static final int DEAUTH_LEAVING = 3;
public static final int DISASSOC_DUE_TO_INACTIVITY = 4;
public static final int DISASSOC_AP_BUSY = 5;
public static final int CLASS2_FRAME_FROM_NONAUTH_STA = 6;
public static final int CLASS3_FRAME_FROM_NONASSOC_STA = 7;
public static final int DISASSOC_STA_HAS_LEFT = 8;
public static final int STA_REQ_ASSOC_WITHOUT_AUTH = 9;
public static final int PWR_CAPABILITY_NOT_VALID = 10;
public static final int SUPPORTED_CHANNEL_NOT_VALID = 11;
public static final int INVALID_IE = 13;
public static final int MICHAEL_MIC_FAILURE = 14;
public static final int FOURWAY_HANDSHAKE_TIMEOUT = 15;
public static final int GROUP_KEY_UPDATE_TIMEOUT = 16;
public static final int IE_IN_4WAY_DIFFERS = 17;
public static final int GROUP_CIPHER_NOT_VALID = 18;
public static final int PAIRWISE_CIPHER_NOT_VALID = 19;
public static final int AKMP_NOT_VALID = 20;
public static final int UNSUPPORTED_RSN_IE_VERSION = 21;
public static final int INVALID_RSN_IE_CAPAB = 22;
public static final int IEEE_802_1X_AUTH_FAILED = 23;
public static final int CIPHER_SUITE_REJECTED = 24;
public static final int TDLS_TEARDOWN_UNREACHABLE = 25;
public static final int TDLS_TEARDOWN_UNSPECIFIED = 26;
public static final int DISASSOC_LOW_ACK = 34;
public static final int MESH_PEERING_CANCELLED = 52;
public static final int MESH_MAX_PEERS = 53;
public static final int MESH_CONFIG_POLICY_VIOLATION = 54;
public static final int MESH_CLOSE_RCVD = 55;
public static final int MESH_MAX_RETRIES = 56;
public static final int MESH_CONFIRM_TIMEOUT = 57;
public static final int MESH_INVALID_GTK = 58;
public static final int MESH_INCONSISTENT_PARAMS = 59;
public static final int MESH_INVALID_SECURITY_CAP = 60;
public static final int MESH_PATH_ERROR_NO_PROXY_INFO = 61;
public static final int MESH_PATH_ERROR_NO_FORWARDING_INFO = 62;
public static final int MESH_PATH_ERROR_DEST_UNREACHABLE = 63;
public static final int MAC_ADDRESS_ALREADY_EXISTS_IN_MBSS = 64;
public static final int MESH_CHANNEL_SWITCH_REGULATORY_REQ = 65;
public static final int MESH_CHANNEL_SWITCH_UNSPECIFIED = 66;
}
/** The input data model. */
protected final DataModel model;
/** The RF zone. */
protected final String zone;
/** The device configs within {@link #zone}, keyed on serial number. */
protected final Map<String, DeviceConfig> deviceConfigs;
/** Client steering state */
protected final ClientSteeringState clientSteeringState;
/** Constructor */
public ClientSteeringOptimizer(
DataModel model,
String zone,
DeviceDataManager deviceDataManager,
ClientSteeringState clientSteeringState
) {
this.model = model;
this.zone = zone;
this.deviceConfigs = deviceDataManager.getAllDeviceConfigs(zone);
this.clientSteeringState = clientSteeringState;
// Remove model entries not in the given zone
this.model.latestWifiScans.keySet()
.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber));
this.model.latestStates.keySet()
.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber));
this.model.latestDeviceStatusRadios.keySet()
.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber));
this.model.latestDeviceCapabilitiesPhy.keySet()
.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber));
}
/**
* Compute map from AP serial number to client MAC to client steering
* action.
*/
public abstract Map<String, Map<String, String>> computeApClientActionMap(
boolean dryRun
);
/**
* Steer clients (steer up, steer down, and deauthenticate).
*
* @param apClientActionMap the map from AP serial number to client MAC to
* action to take
*/
public void steer(
Map<String, Map<String, String>> apClientActionMap
) {
// FIXME implement this
//
// TODO: input must also contain AP interface for each client (needed in hostapd commands below)
//
// NOTE: 802.11k/v features must first be enabled on APs:
// ubus call hostapd.<iface> bss_mgmt_enable \
// '{"neighbor_report": true, "beacon_report": true, "link_measurements": true, "bss_transition": true}'
//
// Actions:
//
// - Kick/Deauth:
// ubus call hostapd.<iface> del_client \
// '{"addr": "<client_mac>", "reason": 5, "deauth": true}'
// Where "reason" is a code in BTMReasonCode
//
// - Steer:
// ubus call hostapd.<iface> bss_transition_request \
// '{"addr": "<client_mac>", "disassociation_imminent": false, "disassociation_timer": 0, "validity_period": 30, "neighbors": ["<hex>"], "abridged": 1}'
// Where "neighbors" list element = a hex identifier (array index 2 in command below) - MUST fetch per interface per AP
// ubus call hostapd.<iface> rrm_nr_get_own
// TODO: also send Multi Band Operation (MBO) code ("mbo_reason") for 802.11ax clients
}
// TODO Issue 802.11k RRM Beacon Measurement Requests periodically
// 1. Enable 802.11k/v features on the AP ("bss_mgmt_enable" hostapd command)
// 2. Send request to client
// ubus call hostapd.wlan0-1 rrm_beacon_req '{"addr": "<client_mac>", "channel": <number>, "mode": 1, "op_class": 128, "duration": 100}'
// 3. Must be subscribed to hostapd 'beacon-report' event on AP to receive reply ("BEACON-RESP-RX")
// ubus subscribe hostapd.<iface>
}

View File

@@ -0,0 +1,80 @@
/*
* 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.openwifi.rrm.optimizers.clientsteering;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/** Class to manage global client steering state */
public class ClientSteeringState {
/**
* Map from AP serial number to client MAC to time (JVM monotonic time in
* ns) of the latest attempted client steering action. The {@code Long}
* values are never null.
*/
private ConcurrentMap<String, Map<String, Long>> apClientLastAttempt =
new ConcurrentHashMap<>();
/**
* Register a client steering attempt for the given AP and station at the
* given time if there is no previous registered attempt or more than the
* given backoff time has passed since the registration time of the last
* attempt and the current time. Note that only registration times are
* checked and/or entered, and nothing is executed here. Return true if the
* attempt was registered; false otherwise. The attempt is not registered if
* this run is specified as a dry run.
* <p>
* The backoff time must be non-negative. The backoff time window is
* "exclusive" - e.g., if the backoff time is X ns, and the current time is
* exactly X ns after the last attempt, the backoff is considered expired.
* <p>
* Note that if there was a previous attempt for the given AP and station,
* the current time must not be before the last attempt.
*
* @param apSerialNumber AP serial number
* @param station client MAC
* @param currentTimeNs JVM monotonic time in ns
* @param backoffTimeNs non-negative backoff time (ns)
* @param dryRun if set, do not apply changes
* @return true if client steering attempt was registered; false otherwise
*/
public boolean registerIfBackoffExpired(
String apSerialNumber,
String station,
long currentTimeNs,
long backoffTimeNs,
boolean dryRun
) {
if (backoffTimeNs < 0) {
throw new IllegalArgumentException(
"Backoff time must be non-negative."
);
}
// get last attempt
Map<String, Long> clientLastAttempt = apClientLastAttempt
.computeIfAbsent(apSerialNumber, k -> new HashMap<>());
synchronized (clientLastAttempt) {
Long lastAttempt = clientLastAttempt.get(station);
// check if backoff expired
if (
lastAttempt != null &&
currentTimeNs - lastAttempt < backoffTimeNs
) {
return false;
}
// register attempt
if (!dryRun) {
clientLastAttempt.put(station, currentTimeNs);
}
}
return true;
}
}

View File

@@ -0,0 +1,314 @@
/*
* 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.openwifi.rrm.optimizers.clientsteering;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.facebook.openwifi.cloudsdk.StateInfo;
import com.facebook.openwifi.cloudsdk.UCentralConstants;
import com.facebook.openwifi.cloudsdk.models.ap.Capabilities;
import com.facebook.openwifi.cloudsdk.models.ap.State;
import com.facebook.openwifi.rrm.DeviceDataManager;
import com.facebook.openwifi.rrm.modules.Modeler.DataModel;
import com.facebook.openwifi.rrm.modules.ModelerUtils;
import com.google.gson.Gson;
/**
* Implements simple band steering for each AP separately
* <p>
* 2G clients below a specified RSSI threshold are deauthenticated. 2G clients
* above a specified RSSI threshold are asked to move to either 5G or 6G. 5G and
* 6G clients below a configurable RSSI threshold are asked to move to 2G.
*/
public class SingleAPBandSteering extends ClientSteeringOptimizer {
private static final Logger logger =
LoggerFactory.getLogger(SingleAPBandSteering.class);
/** The RRM algorithm ID. */
public static final String ALGORITHM_ID = "band";
/** The Gson instance. */
private static final Gson gson = new Gson();
/**
* RSSI (dBm) below which a client on 2G should be disconnected using
* deauthentication.
*/
public static final short DEFAULT_MIN_RSSI_2G = -87;
/**
* RSSI (dBm) above which a client on 2G should be requested to move to
* 5G/6G
*/
public static final short DEFAULT_MAX_RSSI_2G = -67;
/**
* RSSI (dBm) below which a client on 5G/6G should be requested to move to
* 2G
*/
public static final short DEFAULT_MIN_RSSI_NON_2G = -82;
/** Default backoff time (ns) for all APs and radios */
public static final long DEFAULT_BACKOFF_TIME_NS = 300_000_000_000L; // 5 min
/** RSSI below which 2G clients are deauthenticated */
private final short minRssi2G;
/** RSSI above which 2G clients are asked to move to 5G or 6G */
private final short maxRssi2G;
/** RSSI below which 5G and 6G clients are asked to move to 2G */
private final short minRssiNon2G;
/** Backoff time (ns) for all APs and radios */
private final long backoffTimeNs;
/** Make a SingleAPBandSteering object with the given arguments */
public static SingleAPBandSteering makeWithArgs(
DataModel model,
String zone,
DeviceDataManager deviceDataManager,
ClientSteeringState clientSteeringState,
Map<String, String> args
) {
short minRssi2G = DEFAULT_MIN_RSSI_2G;
short maxRssi2G = DEFAULT_MAX_RSSI_2G;
short minRssiNon2G = DEFAULT_MIN_RSSI_NON_2G;
long backoffTimeNs = DEFAULT_BACKOFF_TIME_NS;
String arg;
if ((arg = args.get("minRssi2G")) != null) {
minRssi2G = Short.parseShort(arg);
}
if ((arg = args.get("maxRssi2G")) != null) {
maxRssi2G = Short.parseShort(arg);
}
if ((arg = args.get("minRssiNon2G")) != null) {
minRssiNon2G = Short.parseShort(arg);
}
if ((arg = args.get("backoffTimeSec")) != null) {
backoffTimeNs = Long.parseLong(arg) * 1_000_000_000L;
}
return new SingleAPBandSteering(
model,
zone,
deviceDataManager,
clientSteeringState,
minRssi2G,
maxRssi2G,
minRssiNon2G,
backoffTimeNs
);
}
/** Constructor */
public SingleAPBandSteering(
DataModel model,
String zone,
DeviceDataManager deviceDataManager,
ClientSteeringState clientSteeringState,
short minRssi2G,
short maxRssi2G,
short minRssiNon2G,
long backoffTimeNs
) {
super(model, zone, deviceDataManager, clientSteeringState);
this.minRssi2G = minRssi2G;
this.maxRssi2G = maxRssi2G;
this.minRssiNon2G = minRssiNon2G;
this.backoffTimeNs = backoffTimeNs;
}
@Override
public Map<String, Map<String, String>> computeApClientActionMap(
boolean dryRun
) {
Map<String, Map<String, String>> apClientActionMap = new HashMap<>();
// iterate through every AP
for (
Map.Entry<String, List<StateInfo>> entry : model.latestStates
.entrySet()
) {
// get the latest state
// TODO window size (look at multiple states)
// TODO window percent (% of samples that must violate thresholds)
// TODO also check wifiscan IEs to see if 11k beacon requests are supported/enabled
// (RMEnabledCapabilities.beaconActiveMeasurementCapabilityEnabled)
List<? extends State> states = entry.getValue();
if (states == null || states.isEmpty()) {
continue;
}
final String serialNumber = entry.getKey();
final State state = states.get(states.size() - 1);
// iterate through every radio and every connected client
if (state.interfaces == null || state.interfaces.length == 0) {
continue;
}
final long currentTimeNs = System.nanoTime();
for (State.Interface iface : state.interfaces) {
if (iface.ssids == null || iface.ssids.length == 0) {
continue;
}
for (State.Interface.SSID ssid : iface.ssids) {
if (
ssid.associations == null ||
ssid.associations.length == 0
) {
continue;
}
final State.Radio radio = gson.fromJson(
ssid.radio,
State.Radio.class
);
// get band for this radio/ssid
Map<String, Capabilities.Phy> capabilitiesPhy =
model.latestDeviceCapabilitiesPhy
.get(serialNumber);
if (capabilitiesPhy == null) {
continue;
}
final String band = ModelerUtils.getBand(
radio,
capabilitiesPhy
);
if (band == null) {
continue;
}
// decide steering action (if any) for each client
for (
State.Interface.SSID.Association assoc : ssid.associations
) {
maybeAddApClientActionEntry(
assoc,
band,
serialNumber,
currentTimeNs,
apClientActionMap,
dryRun
);
}
}
}
}
return apClientActionMap;
}
/**
* If a client steering action is desired, add an appropriate entry to the
* apClientActionMap, unless this run is marked as a dry run.
*
* @param assoc association between AP radio and client
* @param band band (e.g., "2G")
* @param serialNumber AP serial number
* @param currentTimeNs JVM monotonic time in ns
* @param dryRun if set, do not apply changes
* @param apClientActionMap map from AP serial number to client MAC to client
* steering action name ({@link ClientSteeringOptimizer.CLIENT_STEERING_ACTIONS})
*/
private void maybeAddApClientActionEntry(
State.Interface.SSID.Association assoc,
String band,
String serialNumber,
long currentTimeNs,
Map<String, Map<String, String>> apClientActionMap,
boolean dryRun
) {
// decide whether to do any band steering
// TODO check which bands AP & client can use (see 11k)
if (UCentralConstants.BAND_2G.equals(band)) {
if (assoc.rssi < minRssi2G) {
if (
clientSteeringState
.registerIfBackoffExpired(
serialNumber,
assoc.station,
currentTimeNs,
backoffTimeNs,
dryRun
)
) {
logger.debug(
"Planning to deauthenticate client {} on AP {}",
assoc.station,
serialNumber
);
apClientActionMap
.computeIfAbsent(
serialNumber,
k -> new HashMap<>()
)
.put(
assoc.station,
CLIENT_STEERING_ACTIONS.DEAUTHENTICATE
.name()
);
}
} else if (assoc.rssi > maxRssi2G) {
if (
clientSteeringState
.registerIfBackoffExpired(
serialNumber,
assoc.station,
currentTimeNs,
backoffTimeNs,
dryRun
)
) {
logger.debug(
"Planning to request client {} on AP {} to move to 5G or 6G",
assoc.station,
serialNumber
);
apClientActionMap
.computeIfAbsent(
serialNumber,
k -> new HashMap<>()
)
.put(
assoc.station,
CLIENT_STEERING_ACTIONS.STEER_UP
.name()
);
}
}
// otherwise, do nothing
} else {
// treat 5G and 6G clients the same way
if (assoc.rssi < minRssiNon2G) {
if (
clientSteeringState
.registerIfBackoffExpired(
serialNumber,
assoc.station,
currentTimeNs,
backoffTimeNs,
dryRun
)
) {
logger.debug(
"Planning to request client {} on AP {} to move to 2G",
assoc.station,
serialNumber
);
apClientActionMap
.computeIfAbsent(
serialNumber,
k -> new HashMap<>()
)
.put(
assoc.station,
CLIENT_STEERING_ACTIONS.STEER_DOWN
.name()
);
}
}
// otherwise, do nothing
}
}
}

View File

@@ -0,0 +1,12 @@
/*
* 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.
*/
/**
* Client steering algorithms.
*/
package com.facebook.openwifi.rrm.optimizers.clientsteering;

View File

@@ -0,0 +1,14 @@
/*
* 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.
*/
/**
* RRM algorithm implementations.
*
* @see com.facebook.openwifi.rrm.RRMAlgorithm
*/
package com.facebook.openwifi.rrm.optimizers;

View File

@@ -18,9 +18,9 @@ import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.facebook.openwifi.cloudsdk.models.ap.State;
import com.facebook.openwifi.rrm.DeviceConfig;
import com.facebook.openwifi.rrm.DeviceDataManager;
import com.facebook.openwifi.cloudsdk.models.ap.State;
import com.facebook.openwifi.rrm.modules.Modeler.DataModel;
import com.facebook.openwifi.rrm.modules.ModelerUtils;
@@ -175,7 +175,7 @@ public class LocationBasedOptimalTPC extends TPC {
// Filter out the invalid APs (e.g., no radio, no location data)
// Update txPowerChoices, boundary, apLocX, apLocY for the optimization
for (String serialNumber : serialNumbers) {
List<State> states = model.latestStates.get(serialNumber);
List<? extends State> states = model.latestStates.get(serialNumber);
State state = states.get(states.size() - 1);
// Ignore the device if its radio is not active

View File

@@ -22,11 +22,12 @@ import org.slf4j.LoggerFactory;
import com.facebook.openwifi.cloudsdk.UCentralUtils;
import com.facebook.openwifi.cloudsdk.WifiScanEntry;
import com.facebook.openwifi.cloudsdk.models.ap.Capabilities;
import com.facebook.openwifi.cloudsdk.models.ap.State;
import com.facebook.openwifi.cloudsdk.StateInfo;
import com.facebook.openwifi.rrm.DeviceDataManager;
import com.facebook.openwifi.rrm.modules.Modeler.DataModel;
import com.facebook.openwifi.rrm.modules.ModelerUtils;
import com.google.gson.JsonObject;
/**
* Measurement-based AP-AP TPC algorithm.
@@ -156,8 +157,10 @@ public class MeasurementBasedApApTPC extends TPC {
*/
protected static Set<String> getManagedBSSIDs(DataModel model) {
Set<String> managedBSSIDs = new HashSet<>();
for (Map.Entry<String, List<State>> e : model.latestStates.entrySet()) {
List<State> states = e.getValue();
for (
Map.Entry<String, List<StateInfo>> e : model.latestStates.entrySet()
) {
List<StateInfo> states = e.getValue();
State state = states.get(states.size() - 1);
if (state.interfaces == null) {
continue;
@@ -322,7 +325,7 @@ public class MeasurementBasedApApTPC extends TPC {
buildRssiMap(managedBSSIDs, model.latestWifiScans, band);
logger.debug("Starting TPC for the {} band", band);
for (String serialNumber : serialNumbers) {
List<State> states = model.latestStates.get(serialNumber);
List<? extends State> states = model.latestStates.get(serialNumber);
State state = states.get(states.size() - 1);
if (
state == null || state.radios == null ||
@@ -373,14 +376,15 @@ public class MeasurementBasedApApTPC extends TPC {
State.Radio radio = state.radios[idx];
// this specific SSID is not on the band of interest
JsonObject deviceCapability = model.latestDeviceCapabilities
.get(serialNumber);
if (deviceCapability == null) {
Map<String, Capabilities.Phy> capabilitiesPhy =
model.latestDeviceCapabilitiesPhy
.get(serialNumber);
if (capabilitiesPhy == null) {
continue;
}
final String radioBand = ModelerUtils.getBand(
radio,
deviceCapability
capabilitiesPhy
);
if (radioBand == null || !radioBand.equals(band)) {
continue;

View File

@@ -18,12 +18,13 @@ import java.util.TreeMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.facebook.openwifi.cloudsdk.models.ap.Capabilities;
import com.facebook.openwifi.cloudsdk.UCentralUtils;
import com.facebook.openwifi.cloudsdk.StateInfo;
import com.facebook.openwifi.cloudsdk.models.ap.State;
import com.facebook.openwifi.rrm.DeviceDataManager;
import com.facebook.openwifi.rrm.modules.Modeler.DataModel;
import com.facebook.openwifi.rrm.modules.ModelerUtils;
import com.google.gson.JsonObject;
/**
* Measurement-based AP-client algorithm.
@@ -293,9 +294,11 @@ public class MeasurementBasedApClientTPC extends TPC {
public Map<String, Map<String, Integer>> computeTxPowerMap() {
Map<String, Map<String, Integer>> txPowerMap = new TreeMap<>();
for (Map.Entry<String, List<State>> e : model.latestStates.entrySet()) {
for (
Map.Entry<String, List<StateInfo>> e : model.latestStates.entrySet()
) {
String serialNumber = e.getKey();
List<State> states = e.getValue();
List<StateInfo> states = e.getValue();
State state = states.get(states.size() - 1);
if (state.radios == null || state.radios.length == 0) {
logger.debug(
@@ -307,14 +310,15 @@ public class MeasurementBasedApClientTPC extends TPC {
Map<String, Integer> radioMap = new TreeMap<>();
for (State.Radio radio : state.radios) {
JsonObject deviceCapability = model.latestDeviceCapabilities
.get(serialNumber);
if (deviceCapability == null) {
Map<String, Capabilities.Phy> capabilityPhy =
model.latestDeviceCapabilitiesPhy
.get(serialNumber);
if (capabilityPhy == null) {
continue;
}
final String band = ModelerUtils.getBand(
radio,
deviceCapability
capabilityPhy
);
if (band == null) {
continue;

View File

@@ -20,13 +20,14 @@ import java.util.stream.IntStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.facebook.openwifi.cloudsdk.models.ap.Capabilities;
import com.facebook.openwifi.cloudsdk.models.ap.State;
import com.facebook.openwifi.cloudsdk.StateInfo;
import com.facebook.openwifi.rrm.DeviceConfig;
import com.facebook.openwifi.rrm.DeviceDataManager;
import com.facebook.openwifi.rrm.modules.ConfigManager;
import com.facebook.openwifi.rrm.modules.Modeler.DataModel;
import com.facebook.openwifi.rrm.modules.ModelerUtils;
import com.google.gson.JsonObject;
/**
* TPC (Transmit Power Control) base class.
@@ -78,7 +79,7 @@ public abstract class TPC {
this.model.latestDeviceStatusRadios.keySet()
.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber)
);
this.model.latestDeviceCapabilities.keySet()
this.model.latestDeviceCapabilitiesPhy.keySet()
.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber)
);
}
@@ -186,9 +187,11 @@ public abstract class TPC {
*/
protected Map<String, Map<Integer, List<String>>> getApsPerChannel() {
Map<String, Map<Integer, List<String>>> apsPerChannel = new TreeMap<>();
for (Map.Entry<String, List<State>> e : model.latestStates.entrySet()) {
for (
Map.Entry<String, List<StateInfo>> e : model.latestStates.entrySet()
) {
String serialNumber = e.getKey();
List<State> states = e.getValue();
List<StateInfo> states = e.getValue();
State state = states.get(states.size() - 1);
if (state.radios == null || state.radios.length == 0) {
@@ -204,14 +207,14 @@ public abstract class TPC {
if (currentChannel == 0) {
continue;
}
JsonObject deviceCapability =
model.latestDeviceCapabilities.get(serialNumber);
if (deviceCapability == null) {
Map<String, Capabilities.Phy> capabilitiesPhy =
model.latestDeviceCapabilitiesPhy.get(serialNumber);
if (capabilitiesPhy == null) {
continue;
}
final String band = ModelerUtils.getBand(
radio,
deviceCapability
capabilitiesPhy
);
if (band == null) {
continue;

View File

@@ -0,0 +1,12 @@
/*
* 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.
*/
/**
* Transmit power control (TPC) algorithms.
*/
package com.facebook.openwifi.rrm.optimizers.tpc;

View File

@@ -0,0 +1,12 @@
/*
* 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.
*/
/**
* Radio Resource Management (RRM) service.
*/
package com.facebook.openwifi.rrm;

View File

@@ -0,0 +1,83 @@
/*
* 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.openwifi.rrm.rca;
import java.util.Map;
/**
* Root cause analysis service configuration model.
*/
public class RCAConfig {
//
// NOTE:
// Currently assumes RCA is embedded in the RRM service and does NOT
// duplicate SDK-related fields.
//
/**
* StationPinger parameters.
*/
public class StationPingerParams {
/**
* How often to ping each station, in seconds (or 0 to disable)
* ({@code STATIONPINGERPARAMS_PINGINTERVALSEC})
*/
// NOTE: cannot be shorter than Kafka "state" publish interval
public int pingIntervalSec = 0 /* TODO enable by default */;
/**
* The number of pings to send to each station
* ({@code STATIONPINGERPARAMS_PINGCOUNT})
*/
public int pingCount = 5;
/**
* Ignore state records older than this interval (in ms)
* ({@code STATIONPINGERPARAMS_STALESTATETHRESHOLDMS})
*/
// NOTE: should not be longer than Kafka "state" publish interval
public int staleStateThresholdMs = 300000; // 5 min
/**
* Number of executor threads for ping tasks
* ({@code STATIONPINGERPARAMS_EXECUTORTHREADCOUNT})
*/
public int executorThreadCount = 3;
}
/** StationPinger parameters. */
public StationPingerParams stationPingerParams = new StationPingerParams();
/** Construct RCAConfig from environment variables. */
public static RCAConfig fromEnv(Map<String, String> env) {
RCAConfig config = new RCAConfig();
String v;
// @formatter:off
/* StationPingerParams */
StationPingerParams stationPingerParams = config.stationPingerParams;
if ((v = env.get("STATIONPINGERPARAMS_PINGINTERVALSEC")) != null) {
stationPingerParams.pingIntervalSec = Integer.parseInt(v);
}
if ((v = env.get("STATIONPINGERPARAMS_PINGCOUNT")) != null) {
stationPingerParams.pingCount = Integer.parseInt(v);
}
if ((v = env.get("STATIONPINGERPARAMS_STALESTATETHRESHOLDMS")) != null) {
stationPingerParams.staleStateThresholdMs = Integer.parseInt(v);
}
if ((v = env.get("STATIONPINGERPARAMS_EXECUTORTHREADCOUNT")) != null) {
stationPingerParams.executorThreadCount = Integer.parseInt(v);
}
// @formatter:on
return config;
}
}

View File

@@ -0,0 +1,116 @@
/*
* 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.openwifi.rrm.rca;
import com.facebook.openwifi.cloudsdk.UCentralClient;
import com.facebook.openwifi.cloudsdk.UCentralUtils;
import com.facebook.openwifi.cloudsdk.models.gw.CommandInfo;
/**
* Utilities for root cause analysis.
*/
public class RCAUtils {
/** Ping result, only containing a data summary (not individual pings). */
public static class PingResult {
// NOTE: fields are taken directly from ping output
/** Minimum ping RTT (ms) */
public double min;
/** Average ping RTT (ms) */
public double avg;
/** Maximum ping RTT (ms) */
public double max;
/** Standard deviation of ping RTT measurements (ms) */
public double mdev;
// TODO other stats? (ex. tx/rx packets, % packet loss)
@Override
public String toString() {
return String.format("%.3f/%.3f/%.3f/%.3f ms", min, avg, max, mdev);
}
}
/**
* Parse raw ping output, returning null upon error.
*
* This only supports the busybox ping format.
*/
private static PingResult parsePingOutput(String output) {
// Only parse summary line (should be last line in output).
// Code below is optimized for minimal string operations.
//
// Examples of supported formats:
// round-trip min/avg/max = 4.126/42.470/84.081 ms
// rtt min/avg/max/mdev = 16.853/20.114/23.375/3.261 ms
final String SUMMARY_TEXT_3 = "min/avg/max";
int idx = output.lastIndexOf(SUMMARY_TEXT_3);
if (idx != -1) {
idx += SUMMARY_TEXT_3.length();
} else {
final String SUMMARY_TEXT_4 = "min/avg/max/mdev";
idx = output.lastIndexOf(SUMMARY_TEXT_4);
if (idx != -1) {
idx += SUMMARY_TEXT_4.length();
} else {
return null;
}
}
PingResult result = null;
for (; idx < output.length(); idx++) {
if (Character.isDigit(output.charAt(idx))) {
break;
}
}
if (idx < output.length()) {
int endIdx = output.indexOf(' ', idx);
if (endIdx != -1) {
String s = output.substring(idx, endIdx);
String[] tokens = s.split("/");
if (tokens.length == 3) {
result = new PingResult();
result.min = Double.parseDouble(tokens[0]);
result.avg = Double.parseDouble(tokens[1]);
result.max = Double.parseDouble(tokens[2]);
} else if (tokens.length == 4) {
result = new PingResult();
result.min = Double.parseDouble(tokens[0]);
result.avg = Double.parseDouble(tokens[1]);
result.max = Double.parseDouble(tokens[2]);
result.mdev = Double.parseDouble(tokens[3]);
}
}
}
return result;
}
/**
* Instruct a device (AP) to ping a given destination (IP/hostname),
* returning the raw ping output or null upon error.
*
* @param client the UCentralClient instance
* @param serialNumber the device (AP) serial number
* @param host the ping destination
* @param pingCount the number of pings to send
* @return the ping output, or null upon error
*/
public static PingResult pingFromDevice(
UCentralClient client,
String serialNumber,
String host,
int pingCount
) {
if (pingCount < 1) {
throw new IllegalArgumentException("Invalid pingCount < 1");
}
String script = String.format("ping -c %d %s", pingCount, host);
int timeoutSec = pingCount /* time buffer as follows: */ * 2 + 10;
CommandInfo info = client.runScript(serialNumber, script, timeoutSec);
String output = UCentralUtils.getScriptOutput(info);
return output != null ? parsePingOutput(output) : null;
}
}

View File

@@ -0,0 +1,269 @@
/*
* 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.openwifi.rrm.rca.modules;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.facebook.openwifi.cloudsdk.UCentralClient;
import com.facebook.openwifi.cloudsdk.UCentralUtils;
import com.facebook.openwifi.cloudsdk.kafka.UCentralKafkaConsumer;
import com.facebook.openwifi.cloudsdk.kafka.UCentralKafkaConsumer.KafkaRecord;
import com.facebook.openwifi.cloudsdk.models.ap.State;
import com.facebook.openwifi.cloudsdk.models.gw.ServiceEvent;
import com.facebook.openwifi.rrm.Utils;
import com.facebook.openwifi.rrm.rca.RCAConfig.StationPingerParams;
import com.facebook.openwifi.rrm.rca.RCAUtils;
import com.facebook.openwifi.rrm.rca.RCAUtils.PingResult;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
/**
* Ping service to measure latency/jitter between Wi-Fi APs and clients
* (stations).
* <p>
* This class subscribes to the Kafka "state" topic to retrieve the list of APs
* with connected clients, then issues ping commands for each (AP, STA) pair at
* a given frequency. All actions are submitted to an executor during Kafka
* callbacks.
*/
public class StationPinger {
private static final Logger logger =
LoggerFactory.getLogger(StationPinger.class);
/** The module parameters. */
private final StationPingerParams params;
/** The uCentral client. */
private final UCentralClient uCentralClient;
/** The executor service instance. */
private final ExecutorService executor;
/** The Gson instance. */
private final Gson gson = new Gson();
/**
* Map from device (serial number) to the latest map of STAs
* (i.e. client MAC address to Client structure).
*/
private Map<String, Map<String, State.Interface.Client>> deviceToClients =
new ConcurrentHashMap<>();
/**
* Map of last ping timestamps, keyed on
* {@link #getDeviceKey(String, String)}.
*/
private Map<String, Long> lastPingTsMap = new ConcurrentHashMap<>();
/** Constructor. */
public StationPinger(
StationPingerParams params,
UCentralClient uCentralClient,
UCentralKafkaConsumer consumer
) {
this.params = params;
this.uCentralClient = uCentralClient;
this.executor =
Executors.newFixedThreadPool(
params.executorThreadCount,
new Utils.NamedThreadFactory(
"RCA_" + this.getClass().getSimpleName()
)
);
if (params.pingIntervalSec < 1) {
logger.info("StationPinger is disabled");
return; // quit before registering listeners
}
// Register Kafka listener
if (consumer != null) {
consumer.addKafkaListener(
getClass().getSimpleName(),
new UCentralKafkaConsumer.KafkaListener() {
@Override
public void handleStateRecords(List<KafkaRecord> records) {
handleKafkaStateRecords(records);
}
@Override
public void handleWifiScanRecords(
List<KafkaRecord> records
) { /* ignored */ }
@Override
public void handleServiceEventRecords(
List<ServiceEvent> serviceEventRecords
) { /* ignored */ }
}
);
}
}
/** Process the list of received State records. */
private void handleKafkaStateRecords(List<KafkaRecord> records) {
long now = System.currentTimeMillis();
for (KafkaRecord record : records) {
// Drop old records
if (now - record.timestampMs > params.staleStateThresholdMs) {
logger.debug(
"Dropping old state record for {} at time {}",
record.serialNumber,
record.timestampMs
);
continue;
}
// Deserialize State
JsonObject state = record.payload.getAsJsonObject("state");
if (state == null) {
continue;
}
try {
State stateModel = gson.fromJson(state, State.class);
Map<String, State.Interface.Client> clientMap =
UCentralUtils.getWifiClientInfo(stateModel);
if (
deviceToClients.put(record.serialNumber, clientMap) == null
) {
// Enqueue this device
final String serialNumber = record.serialNumber;
executor.submit(() -> pingDevices(serialNumber));
}
} catch (JsonSyntaxException e) {
logger.error(
String.format(
"Device %s: failed to deserialize state: %s",
record.serialNumber,
state
),
e
);
}
}
}
/** Shut down all resources. */
public void shutdown() {
executor.shutdownNow();
}
/**
* Issue ping commands to all clients of a given AP.
*
* Note that this is intentionally NOT parallelized to avoid collisions
* while transmitting to/from multiple clients of the same AP.
*/
private void pingDevices(String serialNumber) {
Map<String, State.Interface.Client> clientMap =
deviceToClients.get(serialNumber);
if (clientMap == null) {
return; // shouldn't happen
}
logger.trace(
"{}: Pinging all clients ({} total)...",
serialNumber,
clientMap.size()
);
final long PING_INTERVAL_NS =
Math.max(params.pingIntervalSec, 1) * 1_000_000_000L;
for (
Map.Entry<String, State.Interface.Client> entry : clientMap
.entrySet()
) {
String mac = entry.getKey();
String host = getClientAddress(entry.getValue());
if (host == null) {
logger.debug(
"{}: client {} has no pingable address",
serialNumber,
mac
);
continue;
}
// Check backoff timer
long now = System.nanoTime();
String deviceKey = getDeviceKey(serialNumber, mac);
Long lastPingTs = lastPingTsMap.putIfAbsent(deviceKey, now);
if (lastPingTs != null && now - lastPingTs < PING_INTERVAL_NS) {
logger.trace(
"{}: Skipping ping for {} (last pinged {}s ago)",
serialNumber,
mac,
(now - lastPingTs) / 1_000_000_000L
);
continue;
}
lastPingTsMap.put(deviceKey, now);
// Issue ping command
logger.debug(
"{}: Pinging client {} ({})",
serialNumber,
mac,
host
);
PingResult result = RCAUtils
.pingFromDevice(
uCentralClient,
serialNumber,
host,
params.pingCount
);
if (result == null) {
logger.debug(
"Ping failed from {} to {} ({})",
serialNumber,
mac,
host
);
continue;
}
// TODO handle results
logger.info(
"Ping result from {} to {} ({}): {}",
serialNumber,
mac,
host,
result.toString()
);
}
// Remove map entries after we process them
deviceToClients.remove(serialNumber);
}
/** Return an address to ping for the given client. */
private String getClientAddress(State.Interface.Client client) {
if (client.ipv4_addresses.length > 0) {
return client.ipv4_addresses[0];
} else if (client.ipv6_addresses.length > 0) {
return client.ipv6_addresses[0];
} else {
return null;
}
}
/** Return a key to use in {@link #lastPingTsMap}. */
private String getDeviceKey(String serialNumber, String sta) {
// Use (AP, STA) pair as the key to handle STAs moving between APs
// TODO - do we care about radio/band/channel changes too?
return serialNumber + '\0' + sta;
}
}

View File

@@ -0,0 +1,12 @@
/*
* 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.
*/
/**
* Root Cause Analysis (RCA) service.
*/
package com.facebook.openwifi.rrm.rca;

View File

@@ -23,8 +23,8 @@ import java.util.Map;
import org.junit.jupiter.api.Test;
import com.facebook.openwifi.cloudsdk.AggregatedState;
import com.facebook.openwifi.cloudsdk.StateInfo;
import com.facebook.openwifi.cloudsdk.WifiScanEntry;
import com.facebook.openwifi.cloudsdk.models.ap.State;
import com.facebook.openwifi.rrm.aggregators.MeanAggregator;
import com.facebook.openwifi.rrm.modules.Modeler.DataModel;
import com.facebook.openwifi.rrm.optimizers.TestUtils;
@@ -615,7 +615,7 @@ public class ModelerUtilsTest {
new ArrayList<>(Arrays.asList(aggStateC))
);
State toBeAggregated1 = TestUtils.createState(
StateInfo toBeAggregated1 = TestUtils.createState(
6,
20,
10,
@@ -628,27 +628,46 @@ public class ModelerUtilsTest {
ModelerUtils
.addStateToAggregation(bssidToAggregatedStates, toBeAggregated1);
assertEquals(
bssidToAggregatedStates
List<Integer> rssiList = new ArrayList<>();
for (
AggregatedState.AssociationInfo associationInfo : bssidToAggregatedStates
.get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA1))
.get(0).rssi,
.get(0).associationInfoList
) {
rssiList.add(associationInfo.rssi);
}
assertEquals(
rssiList,
Arrays.asList(10, 20, 30)
);
assertEquals(
bssidToAggregatedStates
rssiList = new ArrayList<>();
for (
AggregatedState.AssociationInfo associationInfo : bssidToAggregatedStates
.get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA1))
.get(1).rssi,
.get(1).associationInfoList
) {
rssiList.add(associationInfo.rssi);
}
assertEquals(
rssiList,
Arrays.asList(40, 50)
);
assertEquals(
bssidToAggregatedStates
rssiList = new ArrayList<>();
for (
AggregatedState.AssociationInfo associationInfo : bssidToAggregatedStates
.get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA2))
.get(0).rssi,
.get(0).associationInfoList
) {
rssiList.add(associationInfo.rssi);
}
assertEquals(
rssiList,
Arrays.asList(20, 30, 40, 60)
);
State toBeAggregated2 = TestUtils.createState(
StateInfo toBeAggregated2 = TestUtils.createState(
11,
20,
20,
@@ -659,10 +678,17 @@ public class ModelerUtilsTest {
);
ModelerUtils
.addStateToAggregation(bssidToAggregatedStates, toBeAggregated2);
assertEquals(
bssidToAggregatedStates
rssiList = new ArrayList<>();
for (
AggregatedState.AssociationInfo associationInfo : bssidToAggregatedStates
.get(ModelerUtils.getBssidStationKeyPair(bssidB, stationB))
.get(0).rssi,
.get(0).associationInfoList
) {
rssiList.add(associationInfo.rssi);
}
assertEquals(
rssiList,
Arrays.asList(10, 20, 30, 40)
);
}
@@ -688,7 +714,7 @@ public class ModelerUtilsTest {
DataModel dataModel = new DataModel();
// This serie of StateA is used to test a valid input states.
State time1StateA = TestUtils.createState(
StateInfo time1StateA = TestUtils.createState(
1,
80,
10,
@@ -704,7 +730,7 @@ public class ModelerUtilsTest {
TestUtils.DEFAULT_LOCAL_TIME
);
State time2StateA = TestUtils.createState(
StateInfo time2StateA = TestUtils.createState(
1,
80,
10,
@@ -721,7 +747,7 @@ public class ModelerUtilsTest {
);
//As State time3StateA is obsolete, it should not be aggregated.
State time3StateA = TestUtils.createState(
StateInfo time3StateA = TestUtils.createState(
1,
80,
10,
@@ -758,48 +784,98 @@ public class ModelerUtilsTest {
2
);
assertEquals(
aggregatedMap.get(serialNumberA).get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA1)).get(0).radio,
new AggregatedState.Radio(1, 80, 10)
aggregatedMap.get(serialNumberA).get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA1)).get(0).radioConfig,
new AggregatedState.RadioConfig(1, 80, 10)
);
List<Integer> rssiList = new ArrayList<>();
for (
AggregatedState.AssociationInfo associationInfo : aggregatedMap
.get(serialNumberA)
.get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA1))
.get(0).associationInfoList
) {
rssiList.add(associationInfo.rssi);
}
assertEquals(
aggregatedMap.get(serialNumberA).get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA1)).get(0).rssi,
rssiList,
Arrays.asList(-84, 27)
);
assertEquals(
aggregatedMap.get(serialNumberA).get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA1)).get(1).radio,
new AggregatedState.Radio(6, 40, 20)
aggregatedMap.get(serialNumberA).get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA1)).get(1).radioConfig,
new AggregatedState.RadioConfig(6, 40, 20)
);
rssiList = new ArrayList<>();
for (
AggregatedState.AssociationInfo associationInfo : aggregatedMap
.get(serialNumberA)
.get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA1))
.get(1).associationInfoList
) {
rssiList.add(associationInfo.rssi);
}
assertEquals(
aggregatedMap.get(serialNumberA).get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA1)).get(1).rssi,
rssiList,
Arrays.asList(-80)
);
assertEquals(
aggregatedMap.get(serialNumberA).get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA2)).get(0).radio,
new AggregatedState.Radio(1, 80, 10)
aggregatedMap.get(serialNumberA).get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA2)).get(0).radioConfig,
new AggregatedState.RadioConfig(1, 80, 10)
);
rssiList = new ArrayList<>();
for (
AggregatedState.AssociationInfo associationInfo : aggregatedMap
.get(serialNumberA)
.get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA2))
.get(0).associationInfoList
) {
rssiList.add(associationInfo.rssi);
}
assertEquals(
aggregatedMap.get(serialNumberA).get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA2)).get(0).rssi,
rssiList,
Arrays.asList(-67, -67)
);
assertEquals(
aggregatedMap.get(serialNumberA).get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA2)).get(1).radio,
new AggregatedState.Radio(6, 40, 20)
aggregatedMap.get(serialNumberA).get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA2)).get(1).radioConfig,
new AggregatedState.RadioConfig(6, 40, 20)
);
rssiList = new ArrayList<>();
for (
AggregatedState.AssociationInfo associationInfo : aggregatedMap
.get(serialNumberA)
.get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA2))
.get(1).associationInfoList
) {
rssiList.add(associationInfo.rssi);
}
assertEquals(
aggregatedMap.get(serialNumberA).get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA2)).get(1).rssi,
rssiList,
Arrays.asList(180, 67)
);
assertEquals(
aggregatedMap.get(serialNumberA).get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA3)).get(0).radio,
new AggregatedState.Radio(1, 80, 10)
aggregatedMap.get(serialNumberA).get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA3)).get(0).radioConfig,
new AggregatedState.RadioConfig(1, 80, 10)
);
rssiList = new ArrayList<>();
for (
AggregatedState.AssociationInfo associationInfo : aggregatedMap
.get(serialNumberA)
.get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA3))
.get(0).associationInfoList
) {
rssiList.add(associationInfo.rssi);
}
assertEquals(
aggregatedMap.get(serialNumberA).get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA3)).get(0).rssi,
rssiList,
Arrays.asList(10, 100)
);
// Test more clients operate on the same channel (stationB and stationA)
State time1StateB = TestUtils.createState(
StateInfo time1StateB = TestUtils.createState(
1,
80,
10,
@@ -812,7 +888,7 @@ public class ModelerUtilsTest {
.computeIfAbsent(serialNumberB, k -> new ArrayList<>())
.add(time1StateB);
State time1StateC = TestUtils.createState(
StateInfo time1StateC = TestUtils.createState(
6,
40,
20,
@@ -829,12 +905,32 @@ public class ModelerUtilsTest {
ModelerUtils
.getAggregatedStates(dataModel, obsoletionPeriodMs, refTimeMs);
rssiList = new ArrayList<>();
for (
AggregatedState.AssociationInfo associationInfo : aggregatedMap2
.get(serialNumberB)
.get(ModelerUtils.getBssidStationKeyPair(bssidB, stationB))
.get(0).associationInfoList
) {
rssiList.add(associationInfo.rssi);
}
assertEquals(
aggregatedMap2.get(serialNumberB).get(ModelerUtils.getBssidStationKeyPair(bssidB, stationB)).get(0).rssi, Arrays.asList(-30)
rssiList,
Arrays.asList(-30)
);
rssiList = new ArrayList<>();
for (
AggregatedState.AssociationInfo associationInfo : aggregatedMap2
.get(serialNumberC)
.get(ModelerUtils.getBssidStationKeyPair(bssidC, stationC))
.get(0).associationInfoList
) {
rssiList.add(associationInfo.rssi);
}
assertEquals(
aggregatedMap2.get(serialNumberC).get(ModelerUtils.getBssidStationKeyPair(bssidC, stationC)).get(0).rssi, Arrays.asList(-100)
rssiList,
Arrays.asList(-100)
);
assertEquals(

View File

@@ -24,6 +24,7 @@ public class ProvMonitorTest {
private DeviceDataManager deviceDataManager;
/** Test provisioning monitor. */
@SuppressWarnings("unused")
private ProvMonitor provMonitor;
@BeforeEach

View File

@@ -11,6 +11,7 @@ package com.facebook.openwifi.rrm.optimizers;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -18,14 +19,17 @@ import java.util.TreeSet;
import java.util.stream.Collectors;
import com.facebook.openwifi.cloudsdk.AggregatedState;
import com.facebook.openwifi.cloudsdk.StateInfo;
import com.facebook.openwifi.cloudsdk.UCentralConstants;
import com.facebook.openwifi.cloudsdk.UCentralUtils;
import com.facebook.openwifi.cloudsdk.WifiScanEntry;
import com.facebook.openwifi.cloudsdk.models.ap.Capabilities;
import com.facebook.openwifi.cloudsdk.models.ap.State;
import com.facebook.openwifi.cloudsdk.models.ap.UCentralSchema;
import com.facebook.openwifi.rrm.DeviceTopology;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
public class TestUtils {
/** The Gson instance. */
@@ -102,34 +106,33 @@ public class TestUtils {
* @param band band (e.g., "2G")
* @param channel channel number
* @param channelWidth channel width in MHz
* @return a radio info object as a {@code JsonObject}
* @return a radio info object as a {@code UCentralSchema.Radio}
*/
private static JsonObject createDeviceStatusRadioObject(
private static UCentralSchema.Radio createDeviceStatusRadioObject(
String band,
int channel,
int channelWidth,
int txPower
) {
return gson.fromJson(
String.format(
"{\"band\": %s,\"channel\": %d,\"channel-mode\":\"HE\"," +
"\"channel-width\":%d,\"country\":\"CA\",\"tx-power\":%d}",
band,
channel,
channelWidth,
txPower
),
JsonObject.class
);
UCentralSchema.Radio radio = new UCentralSchema.Radio();
radio.band = band;
radio.channel = new JsonPrimitive(channel);
radio.channelMode = "HE";
radio.channelWidth = channelWidth;
radio.country = "CA";
radio.txPower = txPower;
return radio;
}
/**
* Create an array with one radio info entry with the given channel on a
* given band.
*/
public static JsonArray createDeviceStatus(String band, int channel) {
JsonArray jsonList = new JsonArray();
jsonList.add(
public static List<UCentralSchema.Radio> createDeviceStatus(
String band,
int channel
) {
return Arrays.asList(
createDeviceStatusRadioObject(
band,
channel,
@@ -137,7 +140,6 @@ public class TestUtils {
DEFAULT_TX_POWER
)
);
return jsonList;
}
/**
@@ -149,13 +151,12 @@ public class TestUtils {
* @return an array with one radio info entry with the given band, channel,
* and tx power
*/
public static JsonArray createDeviceStatus(
public static List<UCentralSchema.Radio> createDeviceStatus(
String band,
int channel,
int txPower
) {
JsonArray jsonList = new JsonArray();
jsonList.add(
return Arrays.asList(
createDeviceStatusRadioObject(
band,
channel,
@@ -163,39 +164,36 @@ public class TestUtils {
txPower
)
);
return jsonList;
}
/**
* Create an array with one radio info entry per given band (using the
* lowest channel).
*/
public static JsonArray createDeviceStatus(List<String> bands) {
JsonArray jsonList = new JsonArray();
for (String band : bands) {
int channel = UCentralUtils.getLowerChannelLimit(band);
jsonList.add(
createDeviceStatusRadioObject(
public static List<UCentralSchema.Radio> createDeviceStatus(
List<String> bands
) {
return bands.stream()
.map(
band -> createDeviceStatusRadioObject(
band,
channel,
UCentralUtils.getLowerChannelLimit(band),
DEFAULT_CHANNEL_WIDTH,
DEFAULT_TX_POWER
)
);
}
return jsonList;
)
.collect(Collectors.toList());
}
/**
* Create an array with one radio info entry with the given tx power and
* channel.
*/
public static JsonArray createDeviceStatusSingleBand(
public static List<UCentralSchema.Radio> createDeviceStatusSingleBand(
int channel,
int txPower2G
) {
JsonArray jsonList = new JsonArray();
jsonList.add(
return Arrays.asList(
createDeviceStatusRadioObject(
channelToLowestMatchingBand(channel),
channel,
@@ -203,29 +201,25 @@ public class TestUtils {
txPower2G
)
);
return jsonList;
}
/**
* Create an array with two radio info entries (2G and 5G), with the given
* tx powers and channels.
*/
public static JsonArray createDeviceStatusDualBand(
public static List<UCentralSchema.Radio> createDeviceStatusDualBand(
int channel2G,
int txPower2G,
int channel5G,
int txPower5G
) {
JsonArray jsonList = new JsonArray();
jsonList.add(
return Arrays.asList(
createDeviceStatusRadioObject(
UCentralConstants.BAND_2G,
channel2G,
DEFAULT_CHANNEL_WIDTH,
txPower2G
)
);
jsonList.add(
),
createDeviceStatusRadioObject(
UCentralConstants.BAND_5G,
channel5G,
@@ -233,7 +227,6 @@ public class TestUtils {
txPower5G
)
);
return jsonList;
}
/** Create a wifi scan entry with the given channel. */
@@ -533,7 +526,7 @@ public class TestUtils {
* @param localtime unix timestamp in seconds.
* @return the state of an AP with radios described by the given parameters
*/
public static State createState(
public static StateInfo createState(
int[] channels,
int[] channelWidths,
int[] txPowers,
@@ -554,13 +547,13 @@ public class TestUtils {
);
}
final int numRadios = channels.length;
State state = new State();
state.interfaces = new State.Interface[numRadios + 1];
StateInfo state = new StateInfo();
state.interfaces = new StateInfo.Interface[numRadios + 1];
for (int index = 0; index < numRadios; index++) {
state.interfaces[index] = createUpStateInterface(index);
}
state.interfaces[numRadios] = createDownStateInterface(numRadios);
state.radios = new State.Radio[numRadios];
state.radios = new StateInfo.Radio[numRadios];
for (int i = 0; i < numRadios; i++) {
state.radios[i] = createStateRadio(i);
state.radios[i].channel = channels[i];
@@ -568,10 +561,10 @@ public class TestUtils {
state.radios[i].tx_power = txPowers[i];
state.interfaces[i].ssids[0].bssid = bssids[i];
state.interfaces[i].ssids[0].associations =
new State.Interface.SSID.Association[clientRssis[i].length];
new StateInfo.Interface.SSID.Association[clientRssis[i].length];
for (int j = 0; j < clientRssis[i].length; j++) {
state.interfaces[i].ssids[0].associations[j] =
new State.Interface.SSID.Association();
new StateInfo.Interface.SSID.Association();
state.interfaces[i].ssids[0].associations[j].rssi =
clientRssis[i][j];
state.interfaces[i].ssids[0].associations[j].station =
@@ -597,7 +590,7 @@ public class TestUtils {
* @param localtime unix timestamp in seconds.
* @return the state of an AP with one radio
*/
public static State createState(
public static StateInfo createState(
int channel,
int channelWidth,
int txPower,
@@ -625,7 +618,7 @@ public class TestUtils {
* @param bssid bssid
* @return the state of an AP with one radio
*/
public static State createState(
public static StateInfo createState(
int channel,
int channelWidth,
String bssid
@@ -642,7 +635,7 @@ public class TestUtils {
* @param bssid bssid
* @return the state of an AP with one radio
*/
public static State createState(
public static StateInfo createState(
int channel,
int channelWidth,
int txPower,
@@ -667,7 +660,7 @@ public class TestUtils {
* @param clientRssis array of client RSSIs
* @return the state of an AP with one radio
*/
public static State createState(
public static StateInfo createState(
int channel,
int channelWidth,
int txPower,
@@ -698,7 +691,7 @@ public class TestUtils {
* @param bssidB bssid for radio on channelB
* @return the state of an AP with two radios
*/
public static State createState(
public static StateInfo createState(
int channelA,
int channelWidthA,
int txPowerA,
@@ -735,7 +728,7 @@ public class TestUtils {
* @param localtime local time for the State
* @return the state of an AP with two radios
*/
public static State createState(
public static StateInfo createState(
int channelA,
int channelWidthA,
int txPowerA,
@@ -764,11 +757,9 @@ public class TestUtils {
/**
* Create a radio capability object which is part of the device capability.
*/
public static JsonObject createRadioCapability(String band) {
JsonObject radioCapability = new JsonObject();
JsonArray bandArray = new JsonArray();
bandArray.add(band);
radioCapability.add("band", bandArray);
public static Capabilities.Phy createCapabilitiesPhy(String band) {
Capabilities.Phy phy = new Capabilities.Phy();
phy.band = new String[] { band };
// the following fields are present but unused so they are excluded here
// channels
// dfs_channels
@@ -780,23 +771,27 @@ public class TestUtils {
// rx_ant
// tx_ant
// vht_capa
return radioCapability;
return phy;
}
/** Create a device capability object with radios in the given bands. */
public static JsonObject createDeviceCapability(String[] bands) {
JsonObject deviceCapability = new JsonObject();
public static Map<String, Capabilities.Phy> createDeviceCapabilityPhy(
String[] bands
) {
Map<String, Capabilities.Phy> capabilitiesPhy = new HashMap<>();
for (int i = 0; i < bands.length; i++) {
String phyId = generatePhyString(i);
JsonObject radioCapability = createRadioCapability(bands[i]);
deviceCapability.add(phyId, radioCapability);
Capabilities.Phy radioCapability = createCapabilitiesPhy(bands[i]);
capabilitiesPhy.put(phyId, radioCapability);
}
return deviceCapability;
return capabilitiesPhy;
}
/** Create a device capability object with a radio in the given band. */
public static JsonObject createDeviceCapability(String band) {
return createDeviceCapability(new String[] { band });
public static Map<String, Capabilities.Phy> createDeviceCapabilityPhy(
String band
) {
return createDeviceCapabilityPhy(new String[] { band });
}
/**
@@ -819,11 +814,15 @@ public class TestUtils {
int[] clientRssi
) {
AggregatedState state = new AggregatedState();
state.radio = new AggregatedState.Radio(channel, channelWidth, txPower);
state.radioConfig =
new AggregatedState.RadioConfig(channel, channelWidth, txPower);
state.bssid = bssid;
state.station = station;
state.associationInfoList = new ArrayList<>();
for (int rssi : clientRssi) {
state.rssi.add(rssi);
AggregatedState.AssociationInfo associationInfo =
new AggregatedState.AssociationInfo(rssi);
state.associationInfoList.add(associationInfo);
}
return state;
}

View File

@@ -63,9 +63,9 @@ public class LeastUsedChannelOptimizerTest {
.createState(aExpectedChannel, channelWidth, dummyBssid)
)
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceA,
TestUtils.createDeviceCapability(band)
TestUtils.createDeviceCapabilityPhy(band)
);
dataModel.latestWifiScans.put(
deviceA,
@@ -89,9 +89,9 @@ public class LeastUsedChannelOptimizerTest {
deviceB,
Arrays.asList(TestUtils.createState(40, channelWidth, dummyBssid))
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceB,
TestUtils.createDeviceCapability(band)
TestUtils.createDeviceCapabilityPhy(band)
);
dataModel.latestWifiScans.put(
deviceB,
@@ -116,9 +116,9 @@ public class LeastUsedChannelOptimizerTest {
TestUtils.createState(149, channelWidth, dummyBssid)
)
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceC,
TestUtils.createDeviceCapability(band)
TestUtils.createDeviceCapabilityPhy(band)
);
dataModel.latestWifiScans.put(
deviceC,
@@ -167,9 +167,9 @@ public class LeastUsedChannelOptimizerTest {
.createState(aExpectedChannel, channelWidth, dummyBssid)
)
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceA,
TestUtils.createDeviceCapability(band)
TestUtils.createDeviceCapabilityPhy(band)
);
dataModel.latestWifiScans.put(
deviceA,
@@ -193,9 +193,9 @@ public class LeastUsedChannelOptimizerTest {
deviceB,
Arrays.asList(TestUtils.createState(6, channelWidth, dummyBssid))
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceB,
TestUtils.createDeviceCapability(band)
TestUtils.createDeviceCapabilityPhy(band)
);
dataModel.latestWifiScans.put(
deviceB,
@@ -215,9 +215,9 @@ public class LeastUsedChannelOptimizerTest {
deviceC,
Arrays.asList(TestUtils.createState(6, channelWidth, dummyBssid))
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceC,
TestUtils.createDeviceCapability(band)
TestUtils.createDeviceCapabilityPhy(band)
);
dataModel.latestWifiScans.put(
deviceC,
@@ -275,9 +275,9 @@ public class LeastUsedChannelOptimizerTest {
.createState(aExpectedChannel, channelWidth, dummyBssid)
)
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceA,
TestUtils.createDeviceCapability(band)
TestUtils.createDeviceCapabilityPhy(band)
);
dataModel.latestWifiScans.put(
deviceA,
@@ -300,9 +300,9 @@ public class LeastUsedChannelOptimizerTest {
deviceB,
Arrays.asList(TestUtils.createState(40, channelWidth, dummyBssid))
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceB,
TestUtils.createDeviceCapability(band)
TestUtils.createDeviceCapabilityPhy(band)
);
dataModel.latestWifiScans.put(
deviceB,
@@ -326,9 +326,9 @@ public class LeastUsedChannelOptimizerTest {
TestUtils.createState(149, channelWidth, dummyBssid)
)
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceC,
TestUtils.createDeviceCapability(band)
TestUtils.createDeviceCapabilityPhy(band)
);
dataModel.latestWifiScans.put(
deviceC,
@@ -385,9 +385,9 @@ public class LeastUsedChannelOptimizerTest {
.createState(aExpectedChannel, channelWidth, dummyBssid)
)
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceA,
TestUtils.createDeviceCapability(band)
TestUtils.createDeviceCapabilityPhy(band)
);
dataModel.latestWifiScans.put(
deviceA,
@@ -412,9 +412,9 @@ public class LeastUsedChannelOptimizerTest {
deviceB,
Arrays.asList(TestUtils.createState(40, channelWidth, dummyBssid))
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceB,
TestUtils.createDeviceCapability(band)
TestUtils.createDeviceCapabilityPhy(band)
);
dataModel.latestWifiScans.put(
deviceB,
@@ -439,9 +439,9 @@ public class LeastUsedChannelOptimizerTest {
TestUtils.createState(149, channelWidth, dummyBssid)
)
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceC,
TestUtils.createDeviceCapability(band)
TestUtils.createDeviceCapabilityPhy(band)
);
dataModel.latestWifiScans.put(
deviceC,
@@ -492,9 +492,9 @@ public class LeastUsedChannelOptimizerTest {
.createState(aExpectedChannel, channelWidth, dummyBssid)
)
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceA,
TestUtils.createDeviceCapability(band)
TestUtils.createDeviceCapabilityPhy(band)
);
dataModel.latestWifiScans.put(
deviceA,
@@ -520,9 +520,9 @@ public class LeastUsedChannelOptimizerTest {
deviceB,
Arrays.asList(TestUtils.createState(40, channelWidth, dummyBssid))
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceB,
TestUtils.createDeviceCapability(band)
TestUtils.createDeviceCapabilityPhy(band)
);
dataModel.latestWifiScans.put(
deviceB,
@@ -547,9 +547,9 @@ public class LeastUsedChannelOptimizerTest {
TestUtils.createState(149, channelWidth, dummyBssid)
)
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceC,
TestUtils.createDeviceCapability(band)
TestUtils.createDeviceCapabilityPhy(band)
);
dataModel.latestWifiScans.put(
deviceC,
@@ -571,9 +571,9 @@ public class LeastUsedChannelOptimizerTest {
deviceD,
Arrays.asList(TestUtils.createState(40, channelWidth, dummyBssid))
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceD,
TestUtils.createDeviceCapability(band)
TestUtils.createDeviceCapabilityPhy(band)
);
dataModel.latestWifiScans.put(
deviceD,
@@ -631,9 +631,9 @@ public class LeastUsedChannelOptimizerTest {
.createState(aExpectedChannel, channelWidth, dummyBssid)
)
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceA,
TestUtils.createDeviceCapability(band)
TestUtils.createDeviceCapabilityPhy(band)
);
dataModel.latestWifiScans.put(
deviceA,
@@ -657,9 +657,9 @@ public class LeastUsedChannelOptimizerTest {
deviceB,
Arrays.asList(TestUtils.createState(36, channelWidth, dummyBssid))
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceB,
TestUtils.createDeviceCapability(band)
TestUtils.createDeviceCapabilityPhy(band)
);
dataModel.latestWifiScans.put(
deviceB,
@@ -684,9 +684,9 @@ public class LeastUsedChannelOptimizerTest {
TestUtils.createState(149, channelWidth, dummyBssid)
)
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceC,
TestUtils.createDeviceCapability(band)
TestUtils.createDeviceCapabilityPhy(band)
);
dataModel.latestWifiScans.put(
deviceC,
@@ -710,9 +710,9 @@ public class LeastUsedChannelOptimizerTest {
deviceD,
Arrays.asList(TestUtils.createState(36, channelWidth, dummyBssid))
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceD,
TestUtils.createDeviceCapability(band)
TestUtils.createDeviceCapabilityPhy(band)
);
dataModel.latestWifiScans.put(
deviceD,
@@ -742,9 +742,9 @@ public class LeastUsedChannelOptimizerTest {
.createState(aExpectedChannel, channelWidth, dummyBssid)
)
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceE,
TestUtils.createDeviceCapability(band)
TestUtils.createDeviceCapabilityPhy(band)
);
dataModel.latestWifiScans.put(
deviceE,
@@ -795,9 +795,9 @@ public class LeastUsedChannelOptimizerTest {
.createState(aExpectedChannel, channelWidth, dummyBssid)
)
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceA,
TestUtils.createDeviceCapability(band)
TestUtils.createDeviceCapabilityPhy(band)
);
dataModel.latestWifiScans.put(
deviceA,
@@ -820,9 +820,9 @@ public class LeastUsedChannelOptimizerTest {
deviceB,
Arrays.asList(TestUtils.createState(48, channelWidth, dummyBssid))
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceB,
TestUtils.createDeviceCapability(band)
TestUtils.createDeviceCapabilityPhy(band)
);
dataModel.latestWifiScans.put(
deviceB,
@@ -858,9 +858,9 @@ public class LeastUsedChannelOptimizerTest {
TestUtils.createState(149, channelWidth, dummyBssid)
)
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceC,
TestUtils.createDeviceCapability(band)
TestUtils.createDeviceCapabilityPhy(band)
);
dataModel.latestWifiScans.put(
deviceC,

View File

@@ -67,13 +67,13 @@ public class RandomChannelInitializerTest {
deviceB,
TestUtils.createDeviceStatus(band, 8)
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceA,
TestUtils.createDeviceCapability(band)
TestUtils.createDeviceCapabilityPhy(band)
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceB,
TestUtils.createDeviceCapability(band)
TestUtils.createDeviceCapabilityPhy(band)
);
ChannelOptimizer optimizer = new RandomChannelInitializer(
@@ -124,17 +124,17 @@ public class RandomChannelInitializerTest {
deviceB,
TestUtils.createDeviceStatus(band, 8)
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceA,
TestUtils.createDeviceCapability(
TestUtils.createDeviceCapabilityPhy(
new String[] {
UCentralConstants.BAND_2G,
UCentralConstants.BAND_2G }
)
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceB,
TestUtils.createDeviceCapability(
TestUtils.createDeviceCapabilityPhy(
new String[] {
UCentralConstants.BAND_2G,
UCentralConstants.BAND_2G }

View File

@@ -65,9 +65,9 @@ public class UnmanagedApAwareChannelOptimizerTest {
.createState(aExpectedChannel, channelWidth, bssidA)
)
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceA,
TestUtils.createDeviceCapability(band)
TestUtils.createDeviceCapabilityPhy(band)
);
dataModel.latestWifiScans.put(
deviceA,
@@ -93,9 +93,9 @@ public class UnmanagedApAwareChannelOptimizerTest {
deviceB,
Arrays.asList(TestUtils.createState(40, channelWidth, bssidB))
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceB,
TestUtils.createDeviceCapability(band)
TestUtils.createDeviceCapabilityPhy(band)
);
dataModel.latestWifiScans.put(
deviceB,
@@ -131,9 +131,9 @@ public class UnmanagedApAwareChannelOptimizerTest {
deviceC,
Arrays.asList(TestUtils.createState(149, channelWidth, bssidC))
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceC,
TestUtils.createDeviceCapability(band)
TestUtils.createDeviceCapabilityPhy(band)
);
dataModel.latestWifiScans.put(
deviceC,
@@ -185,9 +185,9 @@ public class UnmanagedApAwareChannelOptimizerTest {
.createState(aExpectedChannel, channelWidth, bssidA)
)
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceA,
TestUtils.createDeviceCapability(band)
TestUtils.createDeviceCapabilityPhy(band)
);
dataModel.latestWifiScans.put(
deviceA,
@@ -211,9 +211,9 @@ public class UnmanagedApAwareChannelOptimizerTest {
deviceB,
Arrays.asList(TestUtils.createState(6, channelWidth, bssidB))
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceB,
TestUtils.createDeviceCapability(band)
TestUtils.createDeviceCapabilityPhy(band)
);
dataModel.latestWifiScans.put(
deviceB,
@@ -233,9 +233,9 @@ public class UnmanagedApAwareChannelOptimizerTest {
deviceC,
Arrays.asList(TestUtils.createState(6, channelWidth, bssidC))
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceC,
TestUtils.createDeviceCapability(band)
TestUtils.createDeviceCapabilityPhy(band)
);
dataModel.latestWifiScans.put(
deviceC,

View File

@@ -0,0 +1,113 @@
/*
* 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.openwifi.rrm.optimizers.clientsteering;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
public class ClientSteeringStateTest {
@Test
void testRegisterAndCheckBackoff() {
final String apA = "aaaaaaaaaaaa";
final String clientA1 = "1a:aa:aa:aa:aa:aa";
final String clientA2 = "2a:aa:aa:aa:aa:aa";
final String apB = "bbbbbbbbbbbb";
final String clientB = "1b:bb:bb:bb:bb:bb";
final long currentTimeNs = System.nanoTime();
final long bufferTimeNs = 60_000_000_000L; // 1 min
ClientSteeringState clientSteeringState = new ClientSteeringState();
// first attempt should register
assertTrue(
clientSteeringState.registerIfBackoffExpired(
apA,
clientA1,
currentTimeNs,
bufferTimeNs,
false // dryRun
)
);
// should not register AP A & clientA1 again while backoff is in effect
assertFalse(
clientSteeringState.registerIfBackoffExpired(
apA,
clientA1,
currentTimeNs + 1,
bufferTimeNs,
false // dryRun
)
);
// one client's backoff should not affect another client
assertTrue(
clientSteeringState.registerIfBackoffExpired(
apA,
clientA2,
currentTimeNs + 1,
bufferTimeNs,
false // dryRun
)
);
// one AP should not affect another
assertTrue(
clientSteeringState.registerIfBackoffExpired(
apB,
clientB,
currentTimeNs + 1,
bufferTimeNs,
false // dryRun
)
);
// should re-register AP A & clientA1 after backoff has expired
assertTrue(
clientSteeringState.registerIfBackoffExpired(
apA,
clientA1,
currentTimeNs + bufferTimeNs,
bufferTimeNs,
false // dryRun
)
);
// an older timestamp should not register
assertFalse(
clientSteeringState.registerIfBackoffExpired(
apB,
clientB,
currentTimeNs - 1,
bufferTimeNs,
false // dryRun
)
);
// try a different backoffTimeNs
assertTrue(
clientSteeringState
.registerIfBackoffExpired(
apA,
clientA2,
currentTimeNs + 2,
1,
false /* dryRun */
)
);
// same client on a different AP should have separate timer
assertTrue(
clientSteeringState.registerIfBackoffExpired(
apB,
clientA2,
currentTimeNs + 3,
bufferTimeNs,
false // dryRun
)
);
}
}

View File

@@ -0,0 +1,186 @@
/*
* 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.openwifi.rrm.optimizers.clientsteering;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.Test;
import com.facebook.openwifi.cloudsdk.UCentralConstants;
import com.facebook.openwifi.rrm.DeviceDataManager;
import com.facebook.openwifi.rrm.modules.Modeler;
import com.facebook.openwifi.rrm.optimizers.TestUtils;
import com.facebook.openwifi.rrm.optimizers.clientsteering.ClientSteeringOptimizer.CLIENT_STEERING_ACTIONS;
public class SingleAPBandSteeringTest {
// TODO test 6G also (should be treated same as 5G)
/** Test zone name. */
private static final String TEST_ZONE = "test-zone";
// AP serial numbers
private static final String apA = "aaaaaaaaaaaa";
private static final String apB = "bbbbbbbbbbbb";
private static final String apC = "cccccccccccc";
// arrays are mutable, but these are private and this is just a test class
// arrays are more convenient for constructing states
/** bssids for radios on AP A */
private static final String[] bssidsA =
new String[] { "aa:aa:aa:aa:aa:a1", "aa:aa:aa:aa:aa:a2" };
/** bssids for radios on AP B */
private static final String[] bssidsB =
new String[] { "bb:bb:bb:bb:bb:b1", "bb:bb:bb:bb:bb:b2" };
/** bssids for radios on AP C */
private static final String[] bssidsC =
new String[] { "cc:cc:cc:cc:cc:c1", "cc:cc:cc:cc:cc:c2" };
/** Array: each element is an array of client MACS for a radio on AP A */
private static final String[][] clientsA =
new String[][] { new String[] { "1a:aa:aa:aa:aa:aa" },
new String[] { "2a:aa:aa:aa:aa:aa" } };
/** Array: each element is an array of client MACS for a radio on AP B */
private static final String[][] clientsB =
new String[][] { new String[] { "1b:bb:bb:bb:bb:bb" },
new String[] { "2b:bb:bb:bb:bb:bb" } };
/** Array: each element is an array of client MACS for a radio on AP B */
private static final String[][] clientsC =
new String[][] { new String[] { "1c:cc:cc:cc:cc:cc" },
new String[] { "2c:cc:cc:cc:cc:cc" } };
/** Default channel width */
private static final int DEFAULT_CHANNEL_WIDTH = 20;
/** Default tx power */
private static final int DEFAULT_TX_POWER = 20;
/** Adds matching State and DeviceCapabilityPhy objects to the data model */
private void addStateAndCapability(
Modeler.DataModel dataModel,
String apSerialNumber,
String[] bssids,
String[][] clients,
int[][] clientRssis
) {
dataModel.latestStates.put(
apSerialNumber,
Arrays.asList(
TestUtils.createState(
new int[] { 1, 36 },
new int[] { DEFAULT_CHANNEL_WIDTH, DEFAULT_CHANNEL_WIDTH },
new int[] { DEFAULT_TX_POWER, DEFAULT_TX_POWER },
bssids,
clients,
clientRssis,
TestUtils.DEFAULT_LOCAL_TIME
)
)
);
dataModel.latestDeviceCapabilitiesPhy.put(
apSerialNumber,
TestUtils.createDeviceCapabilityPhy(
new String[] {
UCentralConstants.BAND_2G,
UCentralConstants.BAND_5G }
)
);
}
/**
* Creates a data model such that:
* Client on (AP A, radio 2G) -> should be deauthenticated
* Client on (AP A, radio 5G) -> should be steered down
* Client on (AP B, radio 2G) -> no action
* Client on (AP B, radio 5G) -> no action
* Client on (AP C, radio 2G) -> should be steered up
* Client on (AP C, radio 5G) -> no action
*
* @return the data model
*/
private Modeler.DataModel createModel() {
Modeler.DataModel dataModel = new Modeler.DataModel();
// AP A
int[] clientRssis2G =
new int[] { SingleAPBandSteering.DEFAULT_MIN_RSSI_2G - 1 }; // deauthenticate
int[] clientRssis5G =
new int[] { SingleAPBandSteering.DEFAULT_MIN_RSSI_NON_2G - 1 }; // steer down
int[][] clientRssis = new int[][] { clientRssis2G, clientRssis5G };
addStateAndCapability(
dataModel,
apA,
bssidsA,
clientsA,
clientRssis
);
// AP B
clientRssis2G = new int[] { SingleAPBandSteering.DEFAULT_MIN_RSSI_2G }; // do nothing
clientRssis5G =
new int[] { SingleAPBandSteering.DEFAULT_MIN_RSSI_NON_2G }; // do nothing
clientRssis = new int[][] { clientRssis2G, clientRssis5G };
addStateAndCapability(
dataModel,
apB,
bssidsB,
clientsB,
clientRssis
);
// AP C
clientRssis2G =
new int[] { SingleAPBandSteering.DEFAULT_MAX_RSSI_2G + 1 }; // steer up
clientRssis5G =
new int[] { SingleAPBandSteering.DEFAULT_MIN_RSSI_NON_2G }; // do nothing
clientRssis = new int[][] { clientRssis2G, clientRssis5G };
addStateAndCapability(
dataModel,
apC,
bssidsC,
clientsC,
clientRssis
);
return dataModel;
}
@Test
void testComputeApClientActionMap() {
DeviceDataManager deviceDataManager = new DeviceDataManager();
deviceDataManager
.setTopology(TestUtils.createTopology(TEST_ZONE, apA, apB, apC));
Modeler.DataModel dataModel = createModel();
// create expected results
// see javadoc of createModel for more details
Map<String, Map<String, String>> exp = new HashMap<>();
Map<String, String> apAMap = new HashMap<>();
apAMap
.put(clientsA[0][0], CLIENT_STEERING_ACTIONS.DEAUTHENTICATE.name());
apAMap.put(clientsA[1][0], CLIENT_STEERING_ACTIONS.STEER_DOWN.name());
exp.put(apA, apAMap);
// no action for AP B
Map<String, String> apCMap = new HashMap<>();
apCMap.put(clientsC[0][0], CLIENT_STEERING_ACTIONS.STEER_UP.name());
exp.put(apC, apCMap);
SingleAPBandSteering optimizer = SingleAPBandSteering.makeWithArgs(
dataModel,
TEST_ZONE,
deviceDataManager,
new ClientSteeringState(),
new HashMap<>(0) // args (use default)
);
Map<String, Map<String, String>> apClientActionMap =
optimizer.computeApClientActionMap(false /* dryRun */);
assertEquals(exp, apClientActionMap);
}
}

View File

@@ -145,9 +145,9 @@ public class LocationBasedOptimalTPCTest {
)
)
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
device,
TestUtils.createDeviceCapability(
TestUtils.createDeviceCapabilityPhy(
new String[] {
UCentralConstants.BAND_2G,
UCentralConstants.BAND_5G }
@@ -222,9 +222,9 @@ public class LocationBasedOptimalTPCTest {
)
)
);
dataModel2.latestDeviceCapabilities.put(
dataModel2.latestDeviceCapabilitiesPhy.put(
device,
TestUtils.createDeviceCapability(
TestUtils.createDeviceCapabilityPhy(
new String[] {
UCentralConstants.BAND_2G,
UCentralConstants.BAND_5G }
@@ -249,9 +249,9 @@ public class LocationBasedOptimalTPCTest {
)
)
);
dataModel2.latestDeviceCapabilities.put(
dataModel2.latestDeviceCapabilitiesPhy.put(
deviceC,
TestUtils.createDeviceCapability(UCentralConstants.BAND_5G)
TestUtils.createDeviceCapabilityPhy(UCentralConstants.BAND_5G)
);
Map<String, Map<String, Integer>> expected2 = new HashMap<>();
@@ -347,9 +347,9 @@ public class LocationBasedOptimalTPCTest {
)
)
);
dataModel2.latestDeviceCapabilities.put(
dataModel2.latestDeviceCapabilitiesPhy.put(
device,
TestUtils.createDeviceCapability(
TestUtils.createDeviceCapabilityPhy(
new String[] {
UCentralConstants.BAND_2G,
UCentralConstants.BAND_5G }
@@ -416,9 +416,9 @@ public class LocationBasedOptimalTPCTest {
)
)
);
dataModel3.latestDeviceCapabilities.put(
dataModel3.latestDeviceCapabilitiesPhy.put(
device,
TestUtils.createDeviceCapability(
TestUtils.createDeviceCapabilityPhy(
new String[] {
UCentralConstants.BAND_2G,
UCentralConstants.BAND_5G }
@@ -475,9 +475,9 @@ public class LocationBasedOptimalTPCTest {
)
)
);
dataModel4.latestDeviceCapabilities.put(
dataModel4.latestDeviceCapabilitiesPhy.put(
device,
TestUtils.createDeviceCapability(
TestUtils.createDeviceCapabilityPhy(
new String[] {
UCentralConstants.BAND_2G,
UCentralConstants.BAND_2G }

View File

@@ -108,9 +108,9 @@ public class MeasurementBasedApApTPCTest {
TestUtils
.createDeviceStatusSingleBand(channel, MAX_TX_POWER)
);
model.latestDeviceCapabilities.put(
model.latestDeviceCapabilitiesPhy.put(
device,
TestUtils.createDeviceCapability(band)
TestUtils.createDeviceCapabilityPhy(band)
);
}
@@ -161,9 +161,9 @@ public class MeasurementBasedApApTPCTest {
MAX_TX_POWER
)
);
model.latestDeviceCapabilities.put(
model.latestDeviceCapabilitiesPhy.put(
device,
TestUtils.createDeviceCapability(
TestUtils.createDeviceCapabilityPhy(
new String[] {
UCentralConstants.BAND_2G,
UCentralConstants.BAND_5G }
@@ -604,9 +604,9 @@ public class MeasurementBasedApApTPCTest {
MAX_TX_POWER
)
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
DEVICE_C,
TestUtils.createDeviceCapability(UCentralConstants.BAND_2G)
TestUtils.createDeviceCapabilityPhy(UCentralConstants.BAND_2G)
);
optimizer = new MeasurementBasedApApTPC(
dataModel,

View File

@@ -61,9 +61,9 @@ public class MeasurementBasedApClientTPCTest {
TestUtils.createState(36, 20, 20, null, new int[] {})
)
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceA,
TestUtils.createDeviceCapability(UCentralConstants.BAND_5G)
TestUtils.createDeviceCapabilityPhy(UCentralConstants.BAND_5G)
);
dataModel.latestStates.put(
deviceB,
@@ -71,9 +71,9 @@ public class MeasurementBasedApClientTPCTest {
TestUtils.createState(36, 20, 20, "", new int[] { -65 })
)
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceB,
TestUtils.createDeviceCapability(UCentralConstants.BAND_5G)
TestUtils.createDeviceCapabilityPhy(UCentralConstants.BAND_5G)
);
dataModel.latestStates.put(
deviceC,
@@ -87,9 +87,9 @@ public class MeasurementBasedApClientTPCTest {
)
)
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceC,
TestUtils.createDeviceCapability(UCentralConstants.BAND_5G)
TestUtils.createDeviceCapabilityPhy(UCentralConstants.BAND_5G)
);
dataModel.latestStates.put(
deviceD,
@@ -97,9 +97,9 @@ public class MeasurementBasedApClientTPCTest {
TestUtils.createState(36, 20, 22, null, new int[] { -80 })
)
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceD,
TestUtils.createDeviceCapability(UCentralConstants.BAND_5G)
TestUtils.createDeviceCapabilityPhy(UCentralConstants.BAND_5G)
);
dataModel.latestStates.put(
deviceE,
@@ -107,9 +107,9 @@ public class MeasurementBasedApClientTPCTest {
TestUtils.createState(36, 20, 23, null, new int[] { -45 })
)
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceE,
TestUtils.createDeviceCapability(UCentralConstants.BAND_5G)
TestUtils.createDeviceCapabilityPhy(UCentralConstants.BAND_5G)
);
TPC optimizer = new MeasurementBasedApClientTPC(
@@ -178,9 +178,9 @@ public class MeasurementBasedApClientTPCTest {
TestUtils.createState(1, 20, 20, null, new int[] {})
)
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceA,
TestUtils.createDeviceCapability(UCentralConstants.BAND_2G)
TestUtils.createDeviceCapabilityPhy(UCentralConstants.BAND_2G)
);
// 5G only
dataModel.latestStates.put(
@@ -189,9 +189,9 @@ public class MeasurementBasedApClientTPCTest {
TestUtils.createState(36, 20, 20, null, new int[] {})
)
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceB,
TestUtils.createDeviceCapability(UCentralConstants.BAND_5G)
TestUtils.createDeviceCapabilityPhy(UCentralConstants.BAND_5G)
);
// 2G and 5G
dataModel.latestStates.put(
@@ -209,9 +209,9 @@ public class MeasurementBasedApClientTPCTest {
)
)
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceC,
TestUtils.createDeviceCapability(
TestUtils.createDeviceCapabilityPhy(
new String[] {
UCentralConstants.BAND_2G,
UCentralConstants.BAND_5G }
@@ -284,9 +284,9 @@ public class MeasurementBasedApClientTPCTest {
TestUtils.createState(36, 20, 20, null, new int[] {})
)
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceA,
TestUtils.createDeviceCapability(UCentralConstants.BAND_5G)
TestUtils.createDeviceCapabilityPhy(UCentralConstants.BAND_5G)
);
dataModel.latestStates.put(
deviceB,
@@ -294,9 +294,9 @@ public class MeasurementBasedApClientTPCTest {
TestUtils.createState(36, 20, 20, "", new int[] { -65 })
)
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceB,
TestUtils.createDeviceCapability(UCentralConstants.BAND_5G)
TestUtils.createDeviceCapabilityPhy(UCentralConstants.BAND_5G)
);
dataModel.latestStates.put(
deviceC,
@@ -310,9 +310,9 @@ public class MeasurementBasedApClientTPCTest {
)
)
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
deviceC,
TestUtils.createDeviceCapability(UCentralConstants.BAND_5G)
TestUtils.createDeviceCapabilityPhy(UCentralConstants.BAND_5G)
);
TPC optimizer = new MeasurementBasedApClientTPC(

View File

@@ -78,9 +78,9 @@ public class RandomTxPowerInitializerTest {
)
)
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
DEVICE_A,
TestUtils.createDeviceCapability(
TestUtils.createDeviceCapabilityPhy(
new String[] {
UCentralConstants.BAND_5G,
UCentralConstants.BAND_2G }
@@ -97,9 +97,9 @@ public class RandomTxPowerInitializerTest {
)
)
);
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
DEVICE_B,
TestUtils.createDeviceCapability(UCentralConstants.BAND_2G)
TestUtils.createDeviceCapabilityPhy(UCentralConstants.BAND_2G)
);
return dataModel;
}

View File

@@ -7,6 +7,7 @@
<packaging>pom</packaging>
<modules>
<module>lib-cloudsdk</module>
<module>lib-rca</module>
<module>owrrm</module>
</modules>
<properties>
@@ -77,6 +78,7 @@
<configuration>
<doclint>all,-missing</doclint>
<notimestamp>true</notimestamp>
<overview>${basedir}/owrrm/src/main/javadoc/overview.html</overview>
<bottom>Copyright &#169; Meta Platforms, Inc. and affiliates.</bottom>
</configuration>
</plugin>
@@ -129,7 +131,7 @@
</plugins>
</pluginManagement>
</build>
<dependencyManagement>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>

41
runner.sh Executable file
View File

@@ -0,0 +1,41 @@
#!/usr/bin/env sh
# script to help run the binary with specific jvm and jvm options
# Defaults to openj9. Switch by setting JVM_IMPL environment variable
JAVA_BIN=$1
JAR=$2
shift 2
COMMON_PARAMETERS=" \
-XX:+CompactStrings \
"
JVM_IMPL="${JVM_IMPL:-openj9}"
EXTRA_JVM_FLAGS="${EXTRA_JVM_FLAGS:-}"
if [ "$JVM_IMPL" = "hotspot" ]; then
# for hotspot
PARAMETERS="\
$COMMON_PARAMETERS \
-XX:+UseG1GC \
-XX:+UseStringDeduplication \
$EXTRA_JVM_FLAGS \
"
elif [ "$JVM_IMPL" = "openj9" ]; then
# for openj9
PARAMETERS=" \
$COMMON_PARAMETERS \
-XX:+IdleTuningGcOnIdle \
-Xtune:virtualized
$EXTRA_JVM_FLAGS \
"
else
echo "Invalid JVM_IMPL option"
exit 1
fi
"$JAVA_BIN" \
$PARAMETERS \
-jar "$JAR" \
$@