21 Commits

Author SHA1 Message Date
zhiqiand
6fb30ce7e4 add lastAccess and created
Signed-off-by: zhiqiand <zhiqian@fb.com>
2022-10-05 20:30:25 -07:00
zhiqiand
350a45b616 fix refresh token logic and sychronizing
Signed-off-by: zhiqiand <zhiqian@fb.com>
2022-10-04 19:00:24 -07:00
zhiqiand
52dae760d8 fix some comments
Signed-off-by: zhiqiand <zhiqian@fb.com>
2022-10-04 16:08:03 -07:00
zhiqiand
343fc7b6ee fix comments on thread-safe and getting refreshAccessToken called
Signed-off-by: zhiqiand <zhiqian@fb.com>
2022-10-04 16:08:03 -07:00
zhiqiand
2a952f56a9 fix some comments
Signed-off-by: zhiqiand <zhiqian@fb.com>
2022-10-04 16:08:03 -07:00
zhiqiand
52a2258c2d initial commit
Signed-off-by: zhiqiand <zhiqian@fb.com>
2022-10-04 16:08:03 -07:00
RockyMandayam2
0b4fd49627 Handle invalid IEs (#94) 2022-10-03 12:41:22 -07:00
Jun Woo Shin
d81df03637 [WIFI-10943] Deal with "auto" value for channel and fix 80p80 representation (#92) 2022-09-30 17:53:32 -04:00
RockyMandayam2
594fd9fa91 Refactor IE parsing (#90) 2022-09-30 09:42:59 -07:00
RockyMandayam2
8c48a8901b Rename HTOperationElement, HTOperationElementTest, VHTOperationElement, and VHTOperationElementTest (#91) 2022-09-29 20:52:19 -07:00
Jun Woo Shin
0ac189f493 [WIFI-10819] parse cron into valid quartz cron (#89)
Signed-off-by: Jun Woo Shin <jwoos@fb.com>
2022-09-29 14:37:18 -04:00
RockyMandayam2
df21d07ec9 Discard unneeded IEs (#78) 2022-09-29 11:28:48 -07:00
RockyMandayam2
01a070c9b7 Only push updates for desired zones/venues (#80) 2022-09-28 12:37:19 -07:00
Jun Woo Shin
5211eae7c6 update comment around token validation to clarify behavior (#87)
Signed-off-by: Jun Woo Shin <jwoos@fb.com>
2022-09-26 17:01:58 -04:00
Jun Woo Shin
fafbda0bd8 update comments about validating tokens (#86)
Signed-off-by: Jun Woo Shin <jwoos@fb.com>
2022-09-26 15:51:10 -04:00
Jun Woo Shin
43c9aaafb2 Make inner classes static as necessary (#84) 2022-09-21 15:13:19 -04:00
RockyMandayam2
89e637cfeb Use short instead of byte to store unsigned byte values in VHTOperationElement (#81) 2022-09-21 08:19:54 -07:00
RockyMandayam2
0a64fb4963 Minor cleanup in TPC classes (#71) 2022-09-20 14:57:56 -07:00
RockyMandayam2
4191bc1a70 Separate createModel into two methods, one for single band and one for multi-band; sync the state and device status in multi-band test (#73) 2022-09-19 17:04:34 -07:00
Jeffrey Han
3b6e83d103 Bump default event loop timers (#77)
Signed-off-by: Jeffrey Han <39203126+elludraon@users.noreply.github.com>
2022-09-19 11:03:41 -07:00
Jun Woo Shin
27c36ff444 Make AP-AP TPC algorithm use tx power from statistics and fix TPC application to correct band (#76) 2022-09-16 17:45:40 -04:00
43 changed files with 1746 additions and 552 deletions

View File

@@ -478,8 +478,10 @@ components:
RRMSchedule:
type: object
properties:
cron:
type: string
crons:
type: array
items:
type: string
algorithms:
type: array
items:

View File

@@ -143,6 +143,9 @@ public class RRMAlgorithm {
* @param dryRun if set, do not apply changes
* @param allowDefaultMode if false, "mode" argument must be present and
* valid (returns error if invalid)
* @param updateImmediately true if the method should queue the zone for
* update and interrupt the config manager thread
* to trigger immediate update
*
* @return the algorithm result, with exactly one field set ("error" upon
* failure, any others upon success)
@@ -153,7 +156,8 @@ public class RRMAlgorithm {
Modeler modeler,
String zone,
boolean dryRun,
boolean allowDefaultMode
boolean allowDefaultMode,
boolean updateImmediately
) {
AlgorithmResult result = new AlgorithmResult();
if (name == null || args == null) {
@@ -212,11 +216,14 @@ public class RRMAlgorithm {
}
result.channelMap = optimizer.computeChannelMap();
if (!dryRun) {
optimizer.applyConfig(
optimizer.updateDeviceApConfig(
deviceDataManager,
configManager,
result.channelMap
);
if (updateImmediately) {
configManager.queueZoneAndWakeUp(zone);
}
}
} else if (
name.equals(RRMAlgorithm.AlgorithmType.OptimizeTxPower.name())
@@ -270,11 +277,14 @@ public class RRMAlgorithm {
}
result.txPowerMap = optimizer.computeTxPowerMap();
if (!dryRun) {
optimizer.applyConfig(
optimizer.updateDeviceApConfig(
deviceDataManager,
configManager,
result.txPowerMap
);
if (updateImmediately) {
configManager.queueZoneAndWakeUp(zone);
}
}
} else {
result.error = String.format("Unknown algorithm: '%s'", name);

View File

@@ -232,7 +232,7 @@ public class RRMConfig {
* The main logic loop interval (i.e. sleep time), in ms
* ({@code DATACOLLECTORPARAMS_UPDATEINTERVALMS})
*/
public int updateIntervalMs = 5000;
public int updateIntervalMs = 30000; // 30sec
/**
* The expected device statistics interval, in seconds (or -1 to
@@ -246,13 +246,13 @@ public class RRMConfig {
* automatic scans)
* ({@code DATACOLLECTORPARAMS_WIFISCANINTERVALSEC})
*/
public int wifiScanIntervalSec = 900;
public int wifiScanIntervalSec = 900; // 15min
/**
* The capabilities request interval (per device), in seconds
* ({@code DATACOLLECTORPARAMS_CAPABILITIESINTERVALSEC})
*/
public int capabilitiesIntervalSec = 3600;
public int capabilitiesIntervalSec = 3600; // 1hr
/**
* Number of executor threads for async tasks (ex. wifi scans)
@@ -273,7 +273,7 @@ public class RRMConfig {
* The main logic loop interval (i.e. sleep time), in ms
* ({@code CONFIGMANAGERPARAMS_UPDATEINTERVALMS})
*/
public int updateIntervalMs = 60000;
public int updateIntervalMs = 120000; // 2min
/**
* Enable pushing device config changes?
@@ -363,7 +363,7 @@ public class RRMConfig {
* Sync interval, in ms, for owprov venue information etc.
* ({@code PROVMONITORPARAMS_SYNCINTERVALMS})
*/
public int syncIntervalMs = 300000;
public int syncIntervalMs = 300000; // 5min
}
/** ProvMonitor parameters. */

View File

@@ -19,9 +19,9 @@ public class RRMSchedule {
*
* This field expects a cron-like format as defined by the Quartz Job
* Scheduler (CronTrigger):
* https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html
* https://www.quartz-scheduler.org/documentation/quartz-2.4.0/tutorials/crontrigger.html
*/
public String cron;
public List<String> crons;
/**
* The list of RRM algorithms to run.

View File

@@ -302,12 +302,13 @@ public class ApiServer implements Runnable {
}
/**
* Validate an OpenWiFi token (external), caching successful lookups.
* Validate an OpenWiFi token (external), caching successful lookups. This will
* validate a USER token - subscriber token won't work and will fail (plus only
* users should be dealing with RRM).
* @return true if token is valid
*/
private boolean validateOpenWifiToken(String token) {
// The below only checks /api/v1/validateToken and caches it as necessary.
// TODO - /api/v1/validateSubToken still has to be implemented.
Long expiry = tokenCache.get(token);
if (expiry == null) {
TokenValidationResult result = client.validateToken(token);
@@ -711,7 +712,8 @@ public class ApiServer implements Runnable {
modeler,
venue,
mock,
true /* allowDefaultMode */
true, /* allowDefaultMode */
true /* updateImmediately */
);
if (result.error != null) {
response.status(400);
@@ -917,7 +919,7 @@ public class ApiServer implements Runnable {
DeviceConfig networkConfig =
gson.fromJson(request.body(), DeviceConfig.class);
deviceDataManager.setDeviceNetworkConfig(networkConfig);
configManager.wakeUp();
configManager.queueAllZonesAndWakeUp();
// Revalidate data model
modeler.revalidate();
@@ -981,7 +983,7 @@ public class ApiServer implements Runnable {
DeviceConfig zoneConfig =
gson.fromJson(request.body(), DeviceConfig.class);
deviceDataManager.setDeviceZoneConfig(zone, zoneConfig);
configManager.wakeUp();
configManager.queueZoneAndWakeUp(zone);
// Revalidate data model
modeler.revalidate();
@@ -1044,7 +1046,10 @@ public class ApiServer implements Runnable {
DeviceConfig apConfig =
gson.fromJson(request.body(), DeviceConfig.class);
deviceDataManager.setDeviceApConfig(serialNumber, apConfig);
configManager.wakeUp();
// TODO enable updates to device(s), not just the entire zone
final String zone =
deviceDataManager.getDeviceZone(serialNumber);
configManager.queueZoneAndWakeUp(zone);
// Revalidate data model
modeler.revalidate();
@@ -1117,7 +1122,10 @@ public class ApiServer implements Runnable {
.computeIfAbsent(serialNumber, k -> new DeviceConfig())
.apply(apConfig);
});
configManager.wakeUp();
final String zone =
deviceDataManager.getDeviceZone(serialNumber);
// TODO enable updates to device(s), not just the entire zone
configManager.queueZoneAndWakeUp(zone);
// Revalidate data model
modeler.revalidate();
@@ -1260,7 +1268,8 @@ public class ApiServer implements Runnable {
modeler,
zone,
dryRun,
false /* allowDefaultMode */
false, /* allowDefaultMode */
true /* updateImmediately */
);
if (result.error != null) {
response.status(400);
@@ -1371,7 +1380,8 @@ public class ApiServer implements Runnable {
modeler,
zone,
dryRun,
false /* allowDefaultMode */
false, /* allowDefaultMode */
true /* updateImmediately */
);
if (result.error != null) {
response.status(400);

View File

@@ -10,9 +10,12 @@ package com.facebook.openwifirrm.modules;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
@@ -63,8 +66,11 @@ public class ConfigManager implements Runnable {
/** Is the main thread sleeping? */
private final AtomicBoolean sleepingFlag = new AtomicBoolean(false);
/** Was a manual config update requested? */
private final AtomicBoolean eventFlag = new AtomicBoolean(false);
/**
* Thread-safe set of zones for which manual config updates have been
* requested.
*/
private Set<String> zonesToUpdate = ConcurrentHashMap.newKeySet();
/** Config listener interface. */
public interface ConfigListener {
@@ -165,6 +171,7 @@ public class ConfigManager implements Runnable {
return;
}
}
client.refreshAccessToken();
// Fetch device list
List<DeviceWithStatus> devices = client.getDevices();
@@ -180,7 +187,10 @@ public class ConfigManager implements Runnable {
List<String> devicesNeedingUpdate = new ArrayList<>();
final long CONFIG_DEBOUNCE_INTERVAL_NS =
params.configDebounceIntervalSec * 1_000_000_000L;
final boolean isEvent = eventFlag.getAndSet(false);
Set<String> zonesToUpdateCopy = new HashSet<>(zonesToUpdate);
// use removeAll() instead of clear() in case items are added between
// the previous line and the following line
zonesToUpdate.removeAll(zonesToUpdateCopy);
for (DeviceWithStatus device : devices) {
// Update config structure
DeviceData data = deviceDataMap.computeIfAbsent(
@@ -201,11 +211,13 @@ public class ConfigManager implements Runnable {
for (ConfigListener listener : configListeners.values()) {
listener.receiveDeviceConfig(device.serialNumber, data.config);
}
// Check event flag
// Check if there are requested updates for this zone
String deviceZone =
deviceDataManager.getDeviceZone(device.serialNumber);
boolean isEvent = zonesToUpdateCopy.contains(deviceZone);
if (params.configOnEventOnly && !isEvent) {
logger.debug(
"Skipping config for {} (event flag not set)",
"Skipping config for {} (zone not marked for updates)",
device.serialNumber
);
continue;
@@ -251,15 +263,16 @@ public class ConfigManager implements Runnable {
}
}
final boolean shouldUpdate = !zonesToUpdateCopy.isEmpty();
// Send config changes to devices
if (!params.configEnabled) {
logger.trace("Config changes are disabled.");
} else if (devicesNeedingUpdate.isEmpty()) {
logger.debug("No device configs to send.");
} else if (params.configOnEventOnly && !isEvent) {
} else if (params.configOnEventOnly && !shouldUpdate) {
// shouldn't happen
logger.error(
"ERROR!! {} device(s) queued for config update, but event flag not set",
"ERROR!! {} device(s) queued for config update, but no zones queued for update.",
devicesNeedingUpdate.size()
);
} else {
@@ -364,9 +377,38 @@ public class ConfigManager implements Runnable {
return (configListeners.remove(id) != null);
}
/** Interrupt the main thread, possibly triggering an update immediately. */
public void wakeUp() {
eventFlag.set(true);
/**
* Mark the zone to be updated, then interrupt the main thread to possibly
* trigger an update immediately.
*
* @param zone non-null zone (i.e., venue)
*/
public void queueZoneAndWakeUp(String zone) {
if (zone == null) {
logger.debug("Zone to queue must be a non-null String.");
return;
}
zonesToUpdate.add(zone);
wakeUp();
}
/**
* Track all zones to be updated, then interrupt the main thread to possibly
* trigger an update immediately.
*/
public void queueAllZonesAndWakeUp() {
/*
* Note, addAll is not atomic, but that is ok. This just means that it
* is possible that some zones may get updated now by the main thread
* while others get updated either when the main thread is woken up or
* the next time the main thread does its periodic update.
*/
zonesToUpdate.addAll(deviceDataManager.getZones());
wakeUp();
}
/** Interrupt the main thread to possibly trigger an update immediately. */
private void wakeUp() {
if (mainThread != null && mainThread.isAlive() && sleepingFlag.get()) {
wakeupFlag.set(true);
mainThread.interrupt();

View File

@@ -218,6 +218,7 @@ public class DataCollector implements Runnable {
return;
}
}
client.refreshAccessToken();
// Fetch device list
List<DeviceWithStatus> devices = client.getDevices();

View File

@@ -97,7 +97,7 @@ public class Modeler implements Runnable {
public Map<String, State> latestState = new ConcurrentHashMap<>();
/** List of radio info per device. */
public Map<String, JsonArray> latestDeviceStatus =
public Map<String, JsonArray> latestDeviceStatusRadios =
new ConcurrentHashMap<>();
/** List of capabilities per device. */
@@ -238,6 +238,7 @@ public class Modeler implements Runnable {
return;
}
}
client.refreshAccessToken();
// TODO: backfill data from database?
@@ -379,7 +380,7 @@ public class Modeler implements Runnable {
// Get old vs new radios info and store the new radios info
JsonArray newRadioList = config.getRadioConfigList();
Set<String> newRadioBandsSet = config.getRadioBandsSet(newRadioList);
JsonArray oldRadioList = dataModel.latestDeviceStatus
JsonArray oldRadioList = dataModel.latestDeviceStatusRadios
.put(serialNumber, newRadioList);
Set<String> oldRadioBandsSet = config.getRadioBandsSet(oldRadioList);
@@ -429,7 +430,7 @@ public class Modeler implements Runnable {
logger.debug("Removed some state entries from data model");
}
if (
dataModel.latestDeviceStatus.entrySet()
dataModel.latestDeviceStatusRadios.entrySet()
.removeIf(e -> !isRRMEnabled(e.getKey()))
) {
logger.debug("Removed some status entries from data model");

View File

@@ -22,8 +22,8 @@ import com.facebook.openwifirrm.aggregators.Aggregator;
import com.facebook.openwifirrm.aggregators.MeanAggregator;
import com.facebook.openwifirrm.modules.Modeler.DataModel;
import com.facebook.openwifirrm.ucentral.WifiScanEntry;
import com.facebook.openwifirrm.ucentral.operationelement.HTOperationElement;
import com.facebook.openwifirrm.ucentral.operationelement.VHTOperationElement;
import com.facebook.openwifirrm.ucentral.informationelement.HTOperation;
import com.facebook.openwifirrm.ucentral.informationelement.VHTOperation;
/**
* Modeler utilities.
@@ -239,9 +239,9 @@ public class ModelerUtils {
return Objects.equals(entry1.bssid, entry2.bssid) &&
entry1.frequency == entry2.frequency &&
entry1.channel == entry2.channel &&
HTOperationElement
HTOperation
.matchesHtForAggregation(entry1.ht_oper, entry2.ht_oper) &&
VHTOperationElement
VHTOperation
.matchesVhtForAggregation(entry1.vht_oper, entry2.vht_oper);
}

View File

@@ -8,6 +8,7 @@
package com.facebook.openwifirrm.modules;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@@ -102,6 +103,7 @@ public class ProvMonitor implements Runnable {
return;
}
}
client.refreshAccessToken();
// Fetch data from owprov
// TODO: this may change later - for now, we only fetch inventory and
@@ -159,12 +161,21 @@ public class ProvMonitor implements Runnable {
return null;
}
RRMSchedule schedule = new RRMSchedule();
schedule.cron = RRMScheduler
String[] crons = RRMScheduler
.parseIntoQuartzCron(details.rrm.schedule);
if (schedule.cron == null || schedule.cron.isEmpty()) {
if (crons == null || crons.length == 0) {
return null;
}
// if ANY crons are invalid throw it out since it doesn't make sense to
// schedule partial jobs
for (String cron : crons) {
if (cron == null || cron.isEmpty()) {
return null;
}
}
RRMSchedule schedule = new RRMSchedule();
schedule.crons = Arrays.asList(crons);
if (details.rrm.algorithms != null) {
schedule.algorithms =
@@ -175,6 +186,7 @@ public class ProvMonitor implements Runnable {
)
.collect(Collectors.toList());
}
return schedule;
}

View File

@@ -8,15 +8,15 @@
package com.facebook.openwifirrm.modules;
import java.text.ParseException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.text.ParseException;
import org.quartz.CronScheduleBuilder;
import org.quartz.CronExpression;
import org.quartz.CronScheduleBuilder;
import org.quartz.Job;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
@@ -35,6 +35,7 @@ import org.slf4j.LoggerFactory;
import com.facebook.openwifirrm.DeviceConfig;
import com.facebook.openwifirrm.DeviceDataManager;
import com.facebook.openwifirrm.RRMAlgorithm;
import com.facebook.openwifirrm.RRMSchedule;
import com.facebook.openwifirrm.RRMConfig.ModuleConfig.RRMSchedulerParams;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
@@ -74,15 +75,21 @@ public class RRMScheduler {
/** The scheduler instance. */
private Scheduler scheduler;
/** The zones with active triggers scheduled. */
private Set<String> scheduledZones;
/**
* The job keys with active triggers scheduled. Job keys take the format of
* {@code <zone>:<index>}
*
* @see #parseIntoQuartzCron(String)
* */
private Set<String> scheduledJobKeys;
/** RRM job. */
public static class RRMJob implements Job {
@Override
public void execute(JobExecutionContext context)
throws JobExecutionException {
String zone = context.getTrigger().getKey().getName();
String jobKey = context.getTrigger().getKey().getName();
String zone = jobKey.split(":")[0];
logger.debug("Executing job for zone: {}", zone);
try {
SchedulerContext schedulerContext =
@@ -107,13 +114,14 @@ public class RRMScheduler {
* @param linuxCron Linux cron with seconds
* (seconds minutes hours day_of_month month day_of_week [year])
*
* @throws IllegalArgumentException when a linux cron cannot be parsed
* into a valid Quartz spec
* @return String a Quartz supported cron
* @throws IllegalArgumentException when a linux cron cannot be parsed into a
* valid Quartz spec
* @return String[] an array of length 1 or 2 of Quartz supported cron that's
* equivalent to the original linux cron
*/
public static String parseIntoQuartzCron(String linuxCron) {
public static String[] parseIntoQuartzCron(String linuxCron) {
if (CronExpression.isValidExpression(linuxCron)) {
return linuxCron;
return new String[] { linuxCron };
}
String[] split = linuxCron.split(" ");
@@ -144,15 +152,36 @@ public class RRMScheduler {
// if first case failed and only day of week is *, set to ?
split[DAY_OF_WEEK_INDEX] = "?";
} else {
// Quartz does not support both values being set, so return null
return null;
// Quartz does not support both values being set but the standard says that
// if both are specified then it becomes OR of the two fields. Which means
// that we can split it into two separate crons and have it work the same way
split[DAY_OF_MONTH_INDEX] = "?";
String dayOfWeekCron = String.join(" ", split);
split[DAY_OF_MONTH_INDEX] = dayOfMonth;
split[DAY_OF_WEEK_INDEX] = "?";
String dayOfMonthCron = String.join(" ", split);
if (
!CronExpression.isValidExpression(dayOfWeekCron) ||
!CronExpression.isValidExpression(dayOfMonthCron)
) {
logger.error(
"Unable to parse cron {} into valid crons",
linuxCron
);
return null;
}
return new String[] { dayOfWeekCron, dayOfMonthCron };
}
String quartzCron = String.join(" ", split);
if (!CronExpression.isValidExpression(quartzCron)) {
return null;
}
return quartzCron;
return new String[] { quartzCron };
}
/** Constructor. */
@@ -194,7 +223,7 @@ public class RRMScheduler {
// Schedule job and triggers
scheduler.addJob(job, false);
syncTriggers();
logger.info("Scheduled {} RRM trigger(s)", scheduledZones.size());
logger.info("Scheduled {} RRM trigger(s)", scheduledJobKeys.size());
// Start scheduler
scheduler.start();
@@ -218,85 +247,98 @@ public class RRMScheduler {
/**
* Synchronize triggers to the current topology, adding/updating/deleting
* them as necessary. This updates {@link #scheduledZones}.
* them as necessary. This updates {@link #scheduledJobKeys}.
*/
public void syncTriggers() {
Set<String> scheduled = ConcurrentHashMap.newKeySet();
Set<String> prevScheduled = new HashSet<>();
if (scheduledZones != null) {
prevScheduled.addAll(scheduledZones);
if (scheduledJobKeys != null) {
prevScheduled.addAll(scheduledJobKeys);
}
// Add new triggers
for (String zone : deviceDataManager.getZones()) {
DeviceConfig config = deviceDataManager.getZoneConfig(zone);
RRMSchedule schedule = config.schedule;
if (
config.schedule == null ||
config.schedule.cron == null ||
config.schedule.cron.isEmpty()
schedule == null || schedule.crons == null ||
schedule.crons.isEmpty()
) {
continue; // RRM not scheduled
}
try {
CronExpression.validateExpression(config.schedule.cron);
} catch (ParseException e) {
logger.error(
String.format(
"Invalid cron expression (%s) for zone %s",
config.schedule.cron,
zone
),
e
for (int i = 0; i < schedule.crons.size(); i++) {
String cron = schedule.crons.get(i);
// if even one schedule has invalid cron, the whole thing is probably wrong
if (cron == null || cron.isEmpty()) {
logger.error("There was an invalid cron in the schedule");
break;
}
try {
CronExpression.validateExpression(cron);
} catch (ParseException e) {
logger.error(
String.format(
"Invalid cron expression (%s) for zone %s",
cron,
zone
),
e
);
continue;
}
// Create trigger
String jobKey = String.format("%s:%d", zone, i);
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity(jobKey)
.forJob(job)
.withSchedule(
CronScheduleBuilder.cronSchedule(cron)
)
.build();
try {
if (!prevScheduled.contains(jobKey)) {
scheduler.scheduleJob(trigger);
} else {
scheduler.rescheduleJob(trigger.getKey(), trigger);
}
} catch (SchedulerException e) {
logger.error(
"Failed to schedule RRM trigger for job key: " + jobKey,
e
);
continue;
}
scheduled.add(jobKey);
logger.debug(
"Scheduled/updated RRM for job key '{}' at: < {} >",
jobKey,
cron
);
continue;
}
// Create trigger
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity(zone)
.forJob(job)
.withSchedule(
CronScheduleBuilder.cronSchedule(config.schedule.cron)
)
.build();
try {
if (!prevScheduled.contains(zone)) {
scheduler.scheduleJob(trigger);
} else {
scheduler.rescheduleJob(trigger.getKey(), trigger);
}
} catch (SchedulerException e) {
logger.error(
"Failed to schedule RRM trigger for zone: " + zone,
e
);
continue;
}
scheduled.add(zone);
logger.debug(
"Scheduled/updated RRM for zone '{}' at: < {} >",
zone,
config.schedule.cron
);
}
// Remove old triggers
prevScheduled.removeAll(scheduled);
for (String zone : prevScheduled) {
for (String jobKey : prevScheduled) {
try {
scheduler.unscheduleJob(TriggerKey.triggerKey(zone));
scheduler.unscheduleJob(TriggerKey.triggerKey(jobKey));
} catch (SchedulerException e) {
logger.error(
"Failed to remove RRM trigger for zone: " + zone,
"Failed to remove RRM trigger for jobKey: " + jobKey,
e
);
continue;
}
logger.debug("Removed RRM trigger for zone '{}'", zone);
logger.debug("Removed RRM trigger for jobKey '{}'", jobKey);
}
this.scheduledZones = scheduled;
this.scheduledJobKeys = scheduled;
}
/** Run RRM algorithms for the given zone. */
@@ -305,16 +347,19 @@ public class RRMScheduler {
// Get algorithms from zone config
DeviceConfig config = deviceDataManager.getZoneConfig(zone);
if (config.schedule == null) {
RRMSchedule schedule = config.schedule;
if (schedule == null) {
logger.error("RRM schedule missing for zone '{}', aborting!", zone);
return;
}
if (
config.schedule.algorithms == null ||
config.schedule.algorithms.isEmpty()
schedule.algorithms == null ||
schedule.algorithms.isEmpty()
) {
logger.debug("Using default RRM algorithms for zone '{}'", zone);
config.schedule.algorithms = Arrays.asList(
logger
.debug("Using default RRM algorithms for zone '{}'", zone);
schedule.algorithms = Arrays.asList(
new RRMAlgorithm(
RRMAlgorithm.AlgorithmType.OptimizeChannel.name()
),
@@ -325,14 +370,15 @@ public class RRMScheduler {
}
// Execute algorithms
for (RRMAlgorithm algo : config.schedule.algorithms) {
for (RRMAlgorithm algo : schedule.algorithms) {
RRMAlgorithm.AlgorithmResult result = algo.run(
deviceDataManager,
configManager,
modeler,
zone,
params.dryRun,
true /* allowDefaultMode */
true, /* allowDefaultMode */
false /* updateImmediately */
);
logger.info(
"'{}' result for zone '{}': {}",
@@ -341,5 +387,6 @@ public class RRMScheduler {
gson.toJson(result)
);
}
configManager.queueZoneAndWakeUp(zone);
}
}

View File

@@ -360,7 +360,7 @@ public class DatabaseManager {
/** Convert a list of state records to a State object. */
private State toState(List<StateRecord> records, long ts) {
State state = new State();
state.unit = state.new Unit();
state.unit = new State.Unit();
state.unit.localtime = ts;
// Parse each record
@@ -454,9 +454,10 @@ public class DatabaseManager {
.map(o -> gson.fromJson(o, State.Interface.class))
.collect(Collectors.toList())
.toArray(new State.Interface[0]);
state.radios = new JsonObject[radios.lastKey() + 1];
state.radios = new State.Radio[radios.lastKey() + 1];
for (Map.Entry<Integer, JsonObject> entry : radios.entrySet()) {
state.radios[entry.getKey()] = entry.getValue();
State.Radio radio = new State.Radio();
state.radios[entry.getKey()] = radio;
}
return state;
}

View File

@@ -25,9 +25,9 @@ import com.facebook.openwifirrm.modules.Modeler.DataModel;
import com.facebook.openwifirrm.ucentral.UCentralConstants;
import com.facebook.openwifirrm.ucentral.UCentralUtils;
import com.facebook.openwifirrm.ucentral.WifiScanEntry;
import com.facebook.openwifirrm.ucentral.informationelement.HTOperation;
import com.facebook.openwifirrm.ucentral.informationelement.VHTOperation;
import com.facebook.openwifirrm.ucentral.models.State;
import com.facebook.openwifirrm.ucentral.operationelement.HTOperationElement;
import com.facebook.openwifirrm.ucentral.operationelement.VHTOperationElement;
/**
* Channel optimizer base class.
@@ -156,7 +156,7 @@ public abstract class ChannelOptimizer {
.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber));
this.model.latestState.keySet()
.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber));
this.model.latestDeviceStatus.keySet()
this.model.latestDeviceStatusRadios.keySet()
.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber));
this.model.latestDeviceCapabilities.keySet()
.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber));
@@ -208,13 +208,13 @@ public abstract class ChannelOptimizer {
return MIN_CHANNEL_WIDTH;
}
HTOperationElement htOperObj = new HTOperationElement(htOper);
HTOperation htOperObj = new HTOperation(htOper);
if (vhtOper == null) {
// HT mode only supports 20/40 MHz
return htOperObj.staChannelWidth ? 40 : 20;
} else {
// VHT/HE mode supports 20/40/160/80+80 MHz
VHTOperationElement vhtOperObj = new VHTOperationElement(vhtOper);
VHTOperation vhtOperObj = new VHTOperation(vhtOper);
if (!htOperObj.staChannelWidth && vhtOperObj.channelWidth == 0) {
return 20;
} else if (
@@ -234,8 +234,9 @@ public abstract class ChannelOptimizer {
// the difference of 8 means it is consecutive
int channelDiff =
Math.abs(vhtOperObj.channel1 - vhtOperObj.channel2);
// the "8080" below does not mean 8080 MHz wide, it refers to 80+80 MHz channel
return channelDiff == 8 ? 160 : 8080;
// TODO it will currently return just 80 for 80p80 - it should be dealt
// with properly.
return channelDiff == 8 ? 160 : 80;
} else {
return MIN_CHANNEL_WIDTH;
}
@@ -378,15 +379,26 @@ public abstract class ChannelOptimizer {
radioIndex < state.radios.length;
radioIndex++
) {
int tempChannel = state.radios[radioIndex]
.get("channel")
.getAsInt();
int tempChannel = state.radios[radioIndex].channel;
if (UCentralUtils.isChannelInBand(tempChannel, band)) {
currentChannel = tempChannel;
currentChannelWidth = state.radios[radioIndex]
.get("channel_width")
.getAsInt();
break;
// treat as two separate 80MHz channel and only assign to one
// TODO: support 80p80 properly
Integer parsedChannelWidth = UCentralUtils
.parseChannelWidth(
state.radios[radioIndex].channel_width,
true
);
if (parsedChannelWidth != null) {
currentChannelWidth = parsedChannelWidth;
break;
}
logger.error(
"Invalid channel width {}",
state.radios[radioIndex].channel_width
);
continue;
}
}
return new int[] { currentChannel, currentChannelWidth };
@@ -627,14 +639,13 @@ public abstract class ChannelOptimizer {
public abstract Map<String, Map<String, Integer>> computeChannelMap();
/**
* Program the given channel map into the AP config and notify the config
* manager.
* Program the given channel map into the AP config.
*
* @param deviceDataManager the DeviceDataManager instance
* @param configManager the ConfigManager instance
* @param channelMap the map of devices (by serial number) to radio to channel
*/
public void applyConfig(
public void updateDeviceApConfig(
DeviceDataManager deviceDataManager,
ConfigManager configManager,
Map<String, Map<String, Integer>> channelMap
@@ -652,8 +663,5 @@ public abstract class ChannelOptimizer {
deviceConfig.autoChannels = entry.getValue();
}
});
// Trigger config update now
configManager.wakeUp();
}
}

View File

@@ -331,11 +331,11 @@ public class LeastUsedChannelOptimizer extends ChannelOptimizer {
public Map<String, Map<String, Integer>> computeChannelMap() {
Map<String, Map<String, Integer>> channelMap = new TreeMap<>();
Map<String, List<String>> bandsMap = UCentralUtils
.getBandsMap(model.latestDeviceStatus);
.getBandsMap(model.latestDeviceStatusRadios);
Map<String, Map<String, List<Integer>>> deviceAvailableChannels =
UCentralUtils.getDeviceAvailableChannels(
model.latestDeviceStatus,
model.latestDeviceStatusRadios,
model.latestDeviceCapabilities,
AVAILABLE_CHANNELS_BAND
);

View File

@@ -119,11 +119,11 @@ public class RandomChannelInitializer extends ChannelOptimizer {
public Map<String, Map<String, Integer>> computeChannelMap() {
Map<String, Map<String, Integer>> channelMap = new TreeMap<>();
Map<String, List<String>> bandsMap =
UCentralUtils.getBandsMap(model.latestDeviceStatus);
UCentralUtils.getBandsMap(model.latestDeviceStatusRadios);
Map<String, Map<String, List<Integer>>> deviceAvailableChannels =
UCentralUtils.getDeviceAvailableChannels(
model.latestDeviceStatus,
model.latestDeviceStatusRadios,
model.latestDeviceCapabilities,
AVAILABLE_CHANNELS_BAND
);

View File

@@ -14,7 +14,6 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
@@ -26,9 +25,6 @@ import com.facebook.openwifirrm.modules.Modeler.DataModel;
import com.facebook.openwifirrm.ucentral.UCentralUtils;
import com.facebook.openwifirrm.ucentral.WifiScanEntry;
import com.facebook.openwifirrm.ucentral.models.State;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
/**
* Measurement-based AP-AP TPC algorithm.
@@ -178,32 +174,6 @@ public class MeasurementBasedApApTPC extends TPC {
return managedBSSIDs;
}
/**
* Get the current band radio tx power (the first one found) for an AP using
* the latest device status.
*
* @param latestDeviceStatus JsonArray containing radio config for the AP
* @param band band (e.g., "2G")
* @return an Optional containing the tx power if one exists, or else an
* empty Optional
*/
protected static Optional<Integer> getCurrentTxPower(
JsonArray latestDeviceStatus,
String band
) {
for (JsonElement e : latestDeviceStatus) {
if (!e.isJsonObject()) {
continue;
}
JsonObject radioObject = e.getAsJsonObject();
String radioBand = radioObject.get("band").getAsString();
if (radioBand.equals(band) && radioObject.has("tx-power")) {
return Optional.of(radioObject.get("tx-power").getAsInt());
}
}
return Optional.empty();
}
/**
* Get a map from BSSID to the received signal strength at neighboring APs (RSSI).
* List of RSSIs are returned in sorted, ascending order.
@@ -340,7 +310,6 @@ public class MeasurementBasedApApTPC extends TPC {
Map<String, List<Integer>> bssidToRssiValues =
buildRssiMap(managedBSSIDs, model.latestWifiScans, band);
logger.debug("Starting TPC for the {} band", band);
Map<String, JsonArray> allStatuses = model.latestDeviceStatus;
for (String serialNumber : serialNumbers) {
State state = model.latestState.get(serialNumber);
if (
@@ -370,40 +339,68 @@ public class MeasurementBasedApApTPC extends TPC {
);
continue;
}
JsonArray radioStatuses =
allStatuses.get(serialNumber).getAsJsonArray();
Optional<Integer> possibleCurrentTxPower = getCurrentTxPower(
radioStatuses,
band
);
if (possibleCurrentTxPower.isEmpty()) {
// this AP is not on the band of interest
continue;
// An AP can have multiple interfaces, optimize for all of them
for (State.Interface iface : state.interfaces) {
if (iface.ssids == null) {
continue;
}
for (State.Interface.SSID ssid : iface.ssids) {
Integer idx = UCentralUtils.parseReferenceIndex(
ssid.radio.get("$ref").getAsString()
);
if (idx == null) {
logger.error(
"Unable to get radio for {}, invalid radio ref {}",
serialNumber,
ssid.radio.get("$ref").getAsString()
);
continue;
}
State.Radio radio = state.radios[idx];
// this specific SSID is not on the band of interest
if (
!UCentralUtils.isChannelInBand(radio.channel, band)
) {
continue;
}
int currentTxPower = radio.tx_power;
String bssid = ssid.bssid;
List<Integer> rssiValues = bssidToRssiValues.get(bssid);
logger
.debug(
"Device <{}> : Interface <{}> : Channel <{}> : BSSID <{}>",
serialNumber,
iface.name,
channel,
bssid
);
for (int rssi : rssiValues) {
logger.debug(" Neighbor received RSSI: {}", rssi);
}
List<Integer> txPowerChoices = updateTxPowerChoices(
band,
serialNumber,
DEFAULT_TX_POWER_CHOICES
);
int newTxPower = computeTxPower(
serialNumber,
currentTxPower,
rssiValues,
coverageThreshold,
nthSmallestRssi,
txPowerChoices
);
logger.debug(" Old tx_power: {}", currentTxPower);
logger.debug(" New tx_power: {}", newTxPower);
txPowerMap
.computeIfAbsent(serialNumber, k -> new TreeMap<>())
.put(band, newTxPower);
}
}
int currentTxPower = possibleCurrentTxPower.get();
String bssid = state.interfaces[0].ssids[0].bssid;
List<Integer> rssiValues = bssidToRssiValues.get(bssid);
logger.debug("Device <{}> : BSSID <{}>", serialNumber, bssid);
for (int rssi : rssiValues) {
logger.debug(" Neighbor received RSSI: {}", rssi);
}
List<Integer> txPowerChoices = updateTxPowerChoices(
band,
serialNumber,
DEFAULT_TX_POWER_CHOICES
);
int newTxPower = computeTxPower(
serialNumber,
currentTxPower,
rssiValues,
coverageThreshold,
nthSmallestRssi,
txPowerChoices
);
logger.debug(" Old tx_power: {}", currentTxPower);
logger.debug(" New tx_power: {}", newTxPower);
txPowerMap.computeIfAbsent(serialNumber, k -> new TreeMap<>())
.put(band, newTxPower);
}
}

View File

@@ -22,7 +22,6 @@ import org.slf4j.LoggerFactory;
import com.facebook.openwifirrm.DeviceDataManager;
import com.facebook.openwifirrm.modules.Modeler.DataModel;
import com.facebook.openwifirrm.ucentral.models.State;
import com.google.gson.JsonObject;
/**
* Measurement-based AP-client algorithm.
@@ -42,6 +41,9 @@ public class MeasurementBasedApClientTPC extends TPC {
/** Default tx power. */
public static final int DEFAULT_TX_POWER = 10;
/** Default channel width in HMz */
public static final int DEFAULT_CHANNEL_WIDTH = 20;
/** Mapping of MCS index to required SNR (dB) in 802.11ac. */
private static final List<Double> MCS_TO_SNR = Collections.unmodifiableList(
Arrays.asList(
@@ -154,19 +156,16 @@ public class MeasurementBasedApClientTPC extends TPC {
private int computeTxPowerForRadio(
String serialNumber,
State state,
JsonObject radio,
State.Radio radio,
List<Integer> txPowerChoices
) {
// Find current tx power and bandwidth
int currentTxPower =
radio.has("tx_power") && !radio.get("tx_power").isJsonNull()
? radio.get("tx_power").getAsInt()
: 0;
int channelWidth =
1_000_000 /* convert MHz to Hz */ * (radio.has("channel_width") &&
!radio.get("channel_width").isJsonNull()
? radio.get("channel_width").getAsInt()
: 20);
int currentTxPower = radio.tx_power;
// treat as one 160MHz channel vs two 80MHz channels
Integer channelWidthMHz =
UCentralUtils.parseChannelWidth(radio.channel_width, false);
int channelWidth = (channelWidthMHz != null
? channelWidthMHz : DEFAULT_CHANNEL_WIDTH) * 1_000_000; // convert MHz to HZ
Collections.sort(txPowerChoices);
int minTxPower = txPowerChoices.get(0);
int maxTxPower = txPowerChoices.get(txPowerChoices.size() - 1);
@@ -303,17 +302,10 @@ public class MeasurementBasedApClientTPC extends TPC {
);
continue;
}
Map<String, Integer> radioMap = new TreeMap<>();
for (JsonObject radio : state.radios) {
Integer currentChannel =
radio.has("channel") && !radio.get("channel").isJsonNull()
? radio.get("channel").getAsInt()
: null;
if (currentChannel == null) {
continue;
}
for (State.Radio radio : state.radios) {
int currentChannel = radio.channel;
String band = UCentralUtils.getBandFromChannel(currentChannel);
if (band == null) {
continue;

View File

@@ -13,20 +13,18 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.facebook.openwifirrm.DeviceConfig;
import com.facebook.openwifirrm.DeviceDataManager;
import com.facebook.openwifirrm.modules.ConfigManager;
import com.facebook.openwifirrm.modules.Modeler.DataModel;
import com.facebook.openwifirrm.ucentral.models.State;
import com.google.gson.JsonObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* TPC (Transmit Power Control) base class.
@@ -75,7 +73,7 @@ public abstract class TPC {
this.model.latestState.keySet()
.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber)
);
this.model.latestDeviceStatus.keySet()
this.model.latestDeviceStatusRadios.keySet()
.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber)
);
this.model.latestDeviceCapabilities.keySet()
@@ -84,19 +82,19 @@ public abstract class TPC {
}
/**
* Update the tx power choices based on user and allowed channels from deviceConfig
* Determine the new tx power choices based on user and allowed channels from deviceConfig.
*
* @param band the operational band
* @param serialNumber the device
* @param txPowerChoices the available tx powers of the device
* @return the updated tx powers of the device
* @param serialNumber the device's serial number
* @param txPowerChoices the device's available tx powers
* @return the device's updated tx powers
*/
protected List<Integer> updateTxPowerChoices(
String band,
String serialNumber,
List<Integer> txPowerChoices
) {
List<Integer> newTxPowerChoices =
new ArrayList<>(txPowerChoices);
List<Integer> newTxPowerChoices = new ArrayList<>(txPowerChoices);
// Update the available tx powers based on user tx powers or allowed tx powers
DeviceConfig deviceCfg = deviceConfigs.get(serialNumber);
@@ -128,8 +126,7 @@ public abstract class TPC {
newTxPowerChoices.retainAll(allowedTxPowers);
}
// If the intersection of the above steps gives an empty list,
// turn back to use the default available tx powers list
// If newTxPowerChoices is empty, use default available tx powers list
if (newTxPowerChoices.isEmpty()) {
logger.debug(
"Device {}: the updated availableTxPowersList is empty!!! " +
@@ -154,14 +151,13 @@ public abstract class TPC {
public abstract Map<String, Map<String, Integer>> computeTxPowerMap();
/**
* Program the given tx power map into the AP config and notify the config
* manager.
* Program the given tx power map into the AP config.
*
* @param deviceDataManager the DeviceDataManager instance
* @param configManager the ConfigManager instance
* @param txPowerMap the map of devices (by serial number) to radio to tx power
*/
public void applyConfig(
public void updateDeviceApConfig(
DeviceDataManager deviceDataManager,
ConfigManager configManager,
Map<String, Map<String, Integer>> txPowerMap
@@ -179,15 +175,12 @@ public abstract class TPC {
deviceConfig.autoTxPowers = entry.getValue();
}
});
// Trigger config update now
configManager.wakeUp();
}
/**
* Get AP serial numbers per channel.
*
* @return the map of channel to the list of serial numbers
* @return the map from channel to the list of AP serial numbers
*/
protected Map<Integer, List<String>> getApsPerChannel() {
Map<Integer, List<String>> apsPerChannel = new TreeMap<>();
@@ -203,12 +196,9 @@ public abstract class TPC {
continue;
}
for (JsonObject radio : state.radios) {
Integer currentChannel =
radio.has("channel") && !radio.get("channel").isJsonNull()
? radio.get("channel").getAsInt()
: null;
if (currentChannel == null) {
for (State.Radio radio : state.radios) {
Integer currentChannel = radio.channel;
if (currentChannel == 0) {
continue;
}
apsPerChannel

View File

@@ -0,0 +1,50 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral;
import java.util.Objects;
import com.facebook.openwifirrm.ucentral.informationelement.Country;
import com.facebook.openwifirrm.ucentral.informationelement.LocalPowerConstraint;
import com.facebook.openwifirrm.ucentral.informationelement.QbssLoad;
import com.facebook.openwifirrm.ucentral.informationelement.TxPwrInfo;
/** Wrapper class containing information elements */
public final class InformationElements {
public Country country;
public QbssLoad qbssLoad;
public LocalPowerConstraint localPowerConstraint;
public TxPwrInfo txPwrInfo;
@Override
public int hashCode() {
return Objects.hash(country, localPowerConstraint, qbssLoad, txPwrInfo);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
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) &&
Objects.equals(txPwrInfo, other.txPwrInfo);
}
}

View File

@@ -8,6 +8,7 @@
package com.facebook.openwifirrm.ucentral;
import java.time.Instant;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -30,6 +31,8 @@ import com.facebook.openwifirrm.ucentral.gw.models.ServiceEvent;
import com.facebook.openwifirrm.ucentral.gw.models.StatisticsRecords;
import com.facebook.openwifirrm.ucentral.gw.models.SystemInfoResults;
import com.facebook.openwifirrm.ucentral.gw.models.TokenValidationResult;
import com.facebook.openwifirrm.ucentral.gw.models.WebTokenRefreshRequest;
import com.facebook.openwifirrm.ucentral.gw.models.WebTokenResult;
import com.facebook.openwifirrm.ucentral.gw.models.WifiScanRequest;
import com.facebook.openwifirrm.ucentral.prov.models.EntityList;
import com.facebook.openwifirrm.ucentral.prov.models.InventoryTagList;
@@ -137,7 +140,18 @@ public class UCentralClient {
* The access token obtained from uCentralSec, needed only when using public
* endpoints.
*/
private String accessToken;
private WebTokenResult accessToken;
/**
* The unix timestamp (in seconds) keeps track of when the accessToken is created.
*/
private long created;
/**
* The unix timestamp (in seconds) keeps track of last time when the accessToken
* is accessed.
*/
private long lastAccess;
/**
* Constructor.
@@ -184,7 +198,8 @@ public class UCentralClient {
Map<String, Object> body = new HashMap<>();
body.put("userId", username);
body.put("password", password);
HttpResponse<String> response = httpPost("oauth2", OWSEC_SERVICE, body);
HttpResponse<String> response =
httpPost("oauth2", OWSEC_SERVICE, body, null);
if (!response.isSuccess()) {
logger.error(
"Login failed: Response code {}, body: {}",
@@ -195,27 +210,146 @@ public class UCentralClient {
}
// Parse access token from response
JSONObject respBody;
WebTokenResult token;
try {
respBody = new JSONObject(response.getBody());
} catch (JSONException e) {
token = gson.fromJson(response.getBody(), WebTokenResult.class);
} catch (JsonSyntaxException e) {
logger.error("Login failed: Unexpected response", e);
logger.debug("Response body: {}", response.getBody());
return false;
}
if (!respBody.has("access_token")) {
if (
token == null || token.access_token == null ||
token.access_token.isEmpty()
) {
logger.error("Login failed: Missing access token");
logger.debug("Response body: {}", respBody.toString());
logger.debug("Response body: {}", response.getBody());
return false;
}
this.accessToken = respBody.getString("access_token");
this.accessToken = token;
this.created = accessToken.created;
this.lastAccess = accessToken.created;
logger.info("Login successful as user: {}", username);
logger.debug("Access token: {}", accessToken);
logger.debug("Access token: {}", accessToken.access_token);
logger.debug("Refresh token: {}", accessToken.refresh_token);
// Load system endpoints
return loadSystemEndpoints();
}
/**
* when using public endpoints, refresh the access token if it's expired.
*/
public synchronized void refreshAccessToken() {
if (usePublicEndpoints) {
refreshAccessTokenImpl();
}
}
/**
* Check if the token is completely expired even if
* for a token refresh request
*
* @return true if the refresh token is expired
*/
private boolean isAccessTokenExpired() {
if (accessToken == null) {
return true;
}
return created + accessToken.expires_in <
Instant.now().getEpochSecond();
}
/**
* Check if an access token is expired.
*
* @return true if the access token is expired
*/
private boolean isAccessTokenTimedOut() {
if (accessToken == null) {
return true;
}
return lastAccess + accessToken.idle_timeout <
Instant.now().getEpochSecond();
}
/**
* Refresh the access toke when time out. If the refresh token is expired, login again.
* If the access token is expired, POST a WebTokenRefreshRequest to refresh token.
*/
private void refreshAccessTokenImpl() {
if (!usePublicEndpoints) {
return;
}
if (isAccessTokenExpired()) {
synchronized (this) {
if (isAccessTokenExpired()) {
logger.info("Token is expired, login again");
login();
}
}
} else if (isAccessTokenTimedOut()) {
synchronized (this) {
if (isAccessTokenTimedOut()) {
logger.debug("Access token timed out, refreshing the token");
accessToken = refreshToken();
created = Instant.now().getEpochSecond();
lastAccess = created;
if (accessToken != null) {
logger.debug("Successfully refresh token.");
}else{
logger.error(
"Fail to refresh token with access token: {}",
accessToken.access_token
);
}
}
}
}
}
/**
* POST a WebTokenRefreshRequest to refresh the access token.
*
* @return valid access token if success, otherwise return null.
*/
private WebTokenResult refreshToken() {
if (accessToken == null) {
return null;
}
WebTokenRefreshRequest refreshRequest = new WebTokenRefreshRequest();
refreshRequest.userId = username;
refreshRequest.refreshToken = accessToken.refresh_token;
logger.debug("refresh token: {}", accessToken.refresh_token);
Map<String, Object> parameters =
Collections.singletonMap("grant_type", "refresh_token");
HttpResponse<String> response =
httpPost(
"oauth2",
OWSEC_SERVICE,
refreshRequest,
parameters
);
if (!response.isSuccess()) {
logger.error(
"Failed to refresh token: Response code {}, body: {}",
response.getStatus(),
response.getBody()
);
return null;
}
try {
return gson.fromJson(response.getBody(), WebTokenResult.class);
} catch (JsonSyntaxException e) {
logger.error(
"Failed to serialize WebTokenResult: Unexpected response:",
e
);
logger.debug("Response body: {}", response.getBody());
return null;
}
}
/** Read system endpoint URLs from uCentralSec. */
private boolean loadSystemEndpoints() {
// Make request
@@ -324,8 +458,12 @@ public class UCentralClient {
.connectTimeout(connectTimeoutMs)
.socketTimeout(socketTimeoutMs);
if (usePublicEndpoints) {
if (accessToken != null) {
req.header("Authorization", "Bearer " + accessToken);
if (!isAccessTokenExpired()) {
req.header(
"Authorization",
"Bearer " + accessToken.access_token
);
lastAccess = Instant.now().getEpochSecond();
}
} else {
req
@@ -339,26 +477,29 @@ public class UCentralClient {
}
}
/** Send a POST request with a JSON body. */
/** Send a POST request with a JSON body and query params. */
private HttpResponse<String> httpPost(
String endpoint,
String service,
Object body
Object body,
Map<String, Object> parameters
) {
return httpPost(
endpoint,
service,
body,
parameters,
socketParams.connectTimeoutMs,
socketParams.socketTimeoutMs
);
}
/** Send a POST request with a JSON body using given timeout values. */
/** Send a POST request with a JSON body and query params using given timeout values. */
private HttpResponse<String> httpPost(
String endpoint,
String service,
Object body,
Map<String, Object> parameters,
int connectTimeoutMs,
int socketTimeoutMs
) {
@@ -367,9 +508,16 @@ public class UCentralClient {
.header("accept", "application/json")
.connectTimeout(connectTimeoutMs)
.socketTimeout(socketTimeoutMs);
if (parameters != null && !parameters.isEmpty()) {
req.queryString(parameters);
}
if (usePublicEndpoints) {
if (accessToken != null) {
req.header("Authorization", "Bearer " + accessToken);
if (!isAccessTokenExpired()) {
req.header(
"Authorization",
"Bearer " + accessToken.access_token
);
lastAccess = Instant.now().getEpochSecond();
}
} else {
req
@@ -454,6 +602,7 @@ public class UCentralClient {
String.format("device/%s/wifiscan", serialNumber),
OWGW_SERVICE,
req,
null,
socketParams.connectTimeoutMs,
socketParams.wifiScanTimeoutMs
);
@@ -482,7 +631,8 @@ public class UCentralClient {
HttpResponse<String> response = httpPost(
String.format("device/%s/configure", serialNumber),
OWGW_SERVICE,
req
req,
null
);
if (!response.isSuccess()) {
logger.error("Error: {}", response.getBody());

View File

@@ -24,6 +24,10 @@ import org.slf4j.LoggerFactory;
import com.facebook.openwifirrm.RRMConfig;
import com.facebook.openwifirrm.Utils;
import com.facebook.openwifirrm.optimizers.channel.ChannelOptimizer;
import com.facebook.openwifirrm.ucentral.informationelement.Country;
import com.facebook.openwifirrm.ucentral.informationelement.LocalPowerConstraint;
import com.facebook.openwifirrm.ucentral.informationelement.QbssLoad;
import com.facebook.openwifirrm.ucentral.informationelement.TxPwrInfo;
import com.facebook.openwifirrm.ucentral.models.State;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
@@ -37,6 +41,9 @@ public class UCentralUtils {
private static final Logger logger =
LoggerFactory.getLogger(UCentralUtils.class);
/** Information Element (IE) content field key */
private static final String IE_CONTENT_FIELD_KEY = "content";
/** The Gson instance. */
private static final Gson gson = new Gson();
@@ -79,15 +86,76 @@ public class UCentralUtils {
for (JsonElement e : scanInfo) {
WifiScanEntry entry = gson.fromJson(e, WifiScanEntry.class);
entry.unixTimeMs = timestampMs;
extractIEs(e, entry);
entries.add(entry);
}
} catch (Exception e) {
logger.debug("Exception when parsing wifiscan entries", e);
return null;
}
return entries;
}
/**
* Extract desired information elements (IEs) from the wifiscan entry.
* Modifies {@code entry} argument. Skips invalid IEs (IEs with missing
* fields).
*/
private static void extractIEs(
JsonElement entryJsonElement,
WifiScanEntry entry
) {
JsonElement iesJsonElement =
entryJsonElement.getAsJsonObject().get("ies");
if (iesJsonElement == null) {
logger.debug("Wifiscan entry does not contain 'ies' field.");
return;
}
JsonArray iesJsonArray = iesJsonElement.getAsJsonArray();
InformationElements ieContainer = new InformationElements();
for (JsonElement ieJsonElement : iesJsonArray) {
JsonElement typeElement =
ieJsonElement.getAsJsonObject().get("type");
if (typeElement == null) { // shouldn't happen
continue;
}
if (!ieJsonElement.isJsonObject()) {
// the IEs we are interested in are Json objects
continue;
}
JsonObject ie = ieJsonElement.getAsJsonObject();
JsonElement contentsJsonElement = ie.get(IE_CONTENT_FIELD_KEY);
if (
contentsJsonElement == null ||
!contentsJsonElement.isJsonObject()
) {
continue;
}
JsonObject contents = contentsJsonElement.getAsJsonObject();
try {
switch (typeElement.getAsInt()) {
case Country.TYPE:
ieContainer.country = Country.parse(contents);
break;
case QbssLoad.TYPE:
ieContainer.qbssLoad = QbssLoad.parse(contents);
break;
case LocalPowerConstraint.TYPE:
ieContainer.localPowerConstraint =
LocalPowerConstraint.parse(contents);
break;
case TxPwrInfo.TYPE:
ieContainer.txPwrInfo = TxPwrInfo.parse(contents);
break;
}
} catch (Exception e) {
logger.debug("Skipping invalid IE {}", ie);
continue;
}
}
entry.ieContainer = ieContainer;
}
/**
* Set all radios config of an AP to a given value.
*
@@ -130,9 +198,24 @@ public class UCentralUtils {
continue;
}
// Compare vs. existing value
int currentValue = radioConfig.get(fieldName).getAsInt();
if (currentValue == newValue) {
// Compare vs. existing value.
// not all values are int so override those values
Integer currentValue = null;
JsonElement fieldValue = radioConfig.get(fieldName);
if (
fieldValue.isJsonPrimitive() &&
fieldValue.getAsJsonPrimitive().isNumber()
) {
currentValue = fieldValue.getAsInt();
} else {
logger.debug(
"Unable to get field '{}' as int, value was {}",
fieldName,
fieldValue.toString()
);
}
if (currentValue != null && currentValue == newValue) {
logger.info(
"Device {}: {} {} is already {}",
serialNumber,
@@ -150,7 +233,7 @@ public class UCentralUtils {
operationalBand,
fieldName,
newValue,
currentValue
currentValue != null ? currentValue : fieldValue.toString()
);
wasModified = true;
}
@@ -384,4 +467,52 @@ public class UCentralUtils {
}
return null;
}
/**
* Tries to parse channel width, if it encounters an error it will return null.
* It can handle 80p80 in two ways. First it can just treat it as 160. Second,
* it can just apply to the first 80 channel and ignore the second. This is
* controlled by treatSeparate.
*
* @param channelWidthStr the channel width
* @param treatSeparate treats each band separately
* @return channel width in MHz
*/
public static Integer parseChannelWidth(
String channelWidthStr,
boolean treatSeparate
) {
// 80p80 is the only case where it can't be parsed into an integer
if (channelWidthStr.equals("80p80")) {
return treatSeparate ? 80 : 160;
}
try {
return Integer.parseInt(channelWidthStr);
} catch (NumberFormatException e) {
return null;
}
}
/**
* Tries to parse the index from the reference string in the JSON returned from
* other services. Note that this only returns the index, the caller is
* responsible for making sure that the correct field is passed in and the
* index is used in the correct fields. If there's an error parsing, it will
* return null.
*
* @param reference The reference string, keyed under "$ref"
* @return the index of the reference or null if an error occurred.
*/
public static Integer parseReferenceIndex(String reference) {
try {
return Integer.parseInt(
reference,
reference.lastIndexOf("/") + 1,
reference.length(),
10
);
} catch (NumberFormatException e) {
return null;
}
}
}

View File

@@ -22,6 +22,8 @@ public class WifiScanEntry extends WifiScanEntryResult {
* time reference.
*/
public long unixTimeMs;
/** Stores Information Elements (IEs) from the wifiscan entry. */
public InformationElements ieContainer;
/** Default Constructor. */
public WifiScanEntry() {}
@@ -30,13 +32,14 @@ public class WifiScanEntry extends WifiScanEntryResult {
public WifiScanEntry(WifiScanEntry o) {
super(o);
this.unixTimeMs = o.unixTimeMs;
this.ieContainer = o.ieContainer;
}
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + Objects.hash(unixTimeMs);
result = prime * result + Objects.hash(ieContainer, unixTimeMs);
return result;
}
@@ -52,7 +55,8 @@ public class WifiScanEntry extends WifiScanEntryResult {
return false;
}
WifiScanEntry other = (WifiScanEntry) obj;
return unixTimeMs == other.unixTimeMs;
return Objects.equals(ieContainer, other.ieContainer) &&
unixTimeMs == other.unixTimeMs;
}
@Override

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.
*/
package com.facebook.openwifirrm.ucentral.gw.models;
public class WebTokenRefreshRequest {
public String userId;
public String refreshToken;
}

View File

@@ -0,0 +1,160 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.informationelement;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonElement;
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.
*/
public class Country {
private static final Logger logger = LoggerFactory.getLogger(Country.class);
/** Defined in 802.11 */
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.
*/
public final int firstChannelNumber;
/**
* The maximum power, in dBm, allowed to be transmitted.
*/
public final int maximumTransmitPowerLevel;
/**
* 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;
/** Constructor. */
public CountryInfo(
int firstChannelNumber,
int maximumTransmitPowerLevel,
int numberOfChannels
) {
this.firstChannelNumber = firstChannelNumber;
this.maximumTransmitPowerLevel = maximumTransmitPowerLevel;
this.numberOfChannels = numberOfChannels;
}
/** 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
.get("Maximum Transmit Power Level (in dBm)")
.getAsInt();
final int numberOfChannels =
contents.get("Number of Channels").getAsInt();
return new CountryInfo(
firstChannelNumber,
maximumTransmitPowerLevel,
numberOfChannels
);
}
@Override
public int hashCode() {
return Objects.hash(
firstChannelNumber,
maximumTransmitPowerLevel,
numberOfChannels
);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
CountryInfo other = (CountryInfo) obj;
return firstChannelNumber == other.firstChannelNumber &&
maximumTransmitPowerLevel == other.maximumTransmitPowerLevel &&
numberOfChannels == other.numberOfChannels;
}
@Override
public String toString() {
return "CountryInfo [firstChannelNumber=" + firstChannelNumber +
", maximumTransmitPowerLevel=" + maximumTransmitPowerLevel +
", numberOfChannels=" + numberOfChannels + "]";
}
}
/**
* Each constraint is a CountryInfo describing tx power constraints on
* one or more channels, for the current country.
*/
public final List<CountryInfo> constraints;
/** Constructor */
public Country(List<CountryInfo> countryInfos) {
this.constraints = Collections.unmodifiableList(countryInfos);
}
/** Parse Country IE from the appropriate Json object. */
public static Country parse(JsonObject contents) {
List<CountryInfo> constraints = new ArrayList<>();
JsonElement constraintsObject = contents.get("constraints");
if (constraintsObject != null) {
for (JsonElement jsonElement : constraintsObject.getAsJsonArray()) {
CountryInfo countryInfo =
CountryInfo.parse(jsonElement.getAsJsonObject());
constraints.add(countryInfo);
}
}
return new Country(constraints);
}
@Override
public int hashCode() {
return Objects.hash(constraints);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Country other = (Country) obj;
return Objects.equals(constraints, other.constraints);
}
@Override
public String toString() {
return "Country [constraints=" + constraints + "]";
}
}

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.operationelement;
package com.facebook.openwifirrm.ucentral.informationelement;
import java.util.Arrays;
import java.util.Objects;
@@ -17,7 +17,7 @@ import org.apache.commons.codec.binary.Base64;
* High Throughput (HT) Operation Element, which is potentially present in
* wifiscan entries. Introduced in 802.11n (2009).
*/
public class HTOperationElement {
public class HTOperation {
/** Channel number of the primary channel. */
public final byte primaryChannel;
@@ -78,7 +78,7 @@ public class HTOperationElement {
* For details about the parameters, see the javadocs for the corresponding
* member variables.
*/
public HTOperationElement(
public HTOperation(
byte primaryChannel,
byte secondaryChannelOffset,
boolean staChannelWidth,
@@ -114,7 +114,7 @@ public class HTOperationElement {
}
/** Constructor with the most used parameters. */
public HTOperationElement(
public HTOperation(
byte primaryChannel,
byte secondaryChannelOffset,
boolean staChannelWidth,
@@ -141,7 +141,7 @@ public class HTOperationElement {
* @param htOper a base64 encoded properly formatted HT operation element (see
* 802.11)
*/
public HTOperationElement(String htOper) {
public HTOperation(String htOper) {
byte[] bytes = Base64.decodeBase64(htOper);
/*
* Note that the code here may seem to read "reversed" compared to 802.11. This
@@ -182,7 +182,7 @@ public class HTOperationElement {
* @return true if the the operation elements "match" for the purpose of
* aggregating statistics; false otherwise.
*/
public boolean matchesForAggregation(HTOperationElement other) {
public boolean matchesForAggregation(HTOperation other) {
return other != null && primaryChannel == other.primaryChannel &&
secondaryChannelOffset == other.secondaryChannelOffset &&
staChannelWidth == other.staChannelWidth &&
@@ -211,8 +211,8 @@ public class HTOperationElement {
if (htOper1 == null || htOper2 == null) {
return false; // false if exactly one is null
}
HTOperationElement htOperObj1 = new HTOperationElement(htOper1);
HTOperationElement htOperObj2 = new HTOperationElement(htOper2);
HTOperation htOperObj1 = new HTOperation(htOper1);
HTOperation htOperObj2 = new HTOperation(htOper2);
return htOperObj1.matchesForAggregation(htOperObj2);
}
@@ -248,7 +248,7 @@ public class HTOperationElement {
if (getClass() != obj.getClass()) {
return false;
}
HTOperationElement other = (HTOperationElement) obj;
HTOperation other = (HTOperation) obj;
return Arrays.equals(basicHtMcsSet, other.basicHtMcsSet) &&
channelCenterFrequencySegment2 ==
other.channelCenterFrequencySegment2 &&

View File

@@ -0,0 +1,72 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.informationelement;
import java.util.Objects;
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.
* Language in javadocs is taken from the specification.
*/
public class LocalPowerConstraint {
/** Defined in 802.11 */
public static final int TYPE = 32;
/**
* 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;
/** Constructor */
public LocalPowerConstraint(int 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();
return new LocalPowerConstraint(localPowerConstraint);
}
@Override
public int hashCode() {
return Objects.hash(localPowerConstraint);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
LocalPowerConstraint other = (LocalPowerConstraint) obj;
return localPowerConstraint == other.localPowerConstraint;
}
@Override
public String toString() {
return "LocalPowerConstraint [localPowerConstraint=" +
localPowerConstraint + "]";
}
}

View File

@@ -0,0 +1,117 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.informationelement;
import java.util.Objects;
import com.google.gson.JsonElement;
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
* javadocs is taken from the specification.
*/
public class QbssLoad {
/** Defined in 802.11 */
public static final int TYPE = 11;
/**
* The total number of STAs currently associated with the BSS.
*/
public final int stationCount;
/**
* 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
* channel. This percentage is computed using the following formula:
* <p>
* floor(255 * channelBusyTime /
* (dot11ChannelUtilizationBeaconIntervals * dot11BeaconPeriod * 1024)
* )
*/
public final int 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.
*/
public final int availableAdmissionCapacity;
/** Constructor */
public QbssLoad(
int stationCount,
int channelUtilization,
int availableAdmissionCapacity
) {
this.stationCount = stationCount;
this.channelUtilization = channelUtilization;
this.availableAdmissionCapacity = availableAdmissionCapacity;
}
/** Parse QbssLoad IE from appropriate Json object; return null if invalid. */
public static QbssLoad parse(JsonObject contents) {
// unclear why there is this additional nested layer
JsonElement ccaContentJsonElement = contents.get("802.11e CCA Version");
if (ccaContentJsonElement == null) {
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();
return new QbssLoad(
stationCount,
channelUtilization,
availableAdmissionCapacity
);
}
@Override
public int hashCode() {
return Objects.hash(
availableAdmissionCapacity,
channelUtilization,
stationCount
);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
QbssLoad other = (QbssLoad) obj;
return availableAdmissionCapacity == other.availableAdmissionCapacity &&
channelUtilization == other.channelUtilization &&
stationCount == other.stationCount;
}
@Override
public String toString() {
return "QbssLoad [stationCount=" + stationCount +
", channelUtilization=" + channelUtilization +
", availableAdmissionCapacity=" + availableAdmissionCapacity + "]";
}
}

View File

@@ -0,0 +1,119 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.informationelement;
import java.util.Objects;
import com.google.gson.JsonElement;
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
* javadocs is taken from the specification.
*/
public class TxPwrInfo {
/** Defined in 802.11 */
public static final int TYPE = 195;
/** Local maximum transmit power for 20 MHz. Required field. */
public final Integer 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;
/** Constructor */
public TxPwrInfo(
int localMaxTxPwrConstraint20MHz,
int localMaxTxPwrConstraint40MHz,
int localMaxTxPwrConstraint80MHz,
int localMaxTxPwrConstraint160MHz
) {
this.localMaxTxPwrConstraint20MHz = localMaxTxPwrConstraint20MHz;
this.localMaxTxPwrConstraint40MHz = localMaxTxPwrConstraint40MHz;
this.localMaxTxPwrConstraint80MHz = localMaxTxPwrConstraint80MHz;
this.localMaxTxPwrConstraint160MHz = localMaxTxPwrConstraint160MHz;
}
/** Parse TxPwrInfo IE from appropriate Json object. */
public static TxPwrInfo parse(JsonObject contents) {
// required field
int localMaxTxPwrConstraint20MHz =
contents.get("Local Max Tx Pwr Constraint 20MHz").getAsInt();
// optional field
Integer localMaxTxPwrConstraint40MHz =
parseOptionalField(contents, "Local Max Tx Pwr Constraint 40MHz");
Integer localMaxTxPwrConstraint80MHz =
parseOptionalField(contents, "Local Max Tx Pwr Constraint 40MHz");
Integer localMaxTxPwrConstraint160MHz =
parseOptionalField(contents, "Local Max Tx Pwr Constraint 40MHz");
return new TxPwrInfo(
localMaxTxPwrConstraint20MHz,
localMaxTxPwrConstraint40MHz,
localMaxTxPwrConstraint80MHz,
localMaxTxPwrConstraint160MHz
);
}
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(
localMaxTxPwrConstraint160MHz,
localMaxTxPwrConstraint20MHz,
localMaxTxPwrConstraint40MHz,
localMaxTxPwrConstraint80MHz
);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
TxPwrInfo other = (TxPwrInfo) obj;
return localMaxTxPwrConstraint160MHz ==
other.localMaxTxPwrConstraint160MHz &&
localMaxTxPwrConstraint20MHz ==
other.localMaxTxPwrConstraint20MHz &&
localMaxTxPwrConstraint40MHz ==
other.localMaxTxPwrConstraint40MHz &&
localMaxTxPwrConstraint80MHz == other.localMaxTxPwrConstraint80MHz;
}
@Override
public String toString() {
return "TxPwrInfo [localMaxTxPwrConstraint20MHz=" +
localMaxTxPwrConstraint20MHz + ", localMaxTxPwrConstraint40MHz=" +
localMaxTxPwrConstraint40MHz + ", localMaxTxPwrConstraint80MHz=" +
localMaxTxPwrConstraint80MHz + ", localMaxTxPwrConstraint160MHz=" +
localMaxTxPwrConstraint160MHz + "]";
}
}

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.operationelement;
package com.facebook.openwifirrm.ucentral.informationelement;
import java.util.Arrays;
import java.util.Objects;
@@ -17,7 +17,7 @@ import org.apache.commons.codec.binary.Base64;
* Very High Throughput (VHT) Operation Element, which is potentially present in
* wifiscan entries. Introduced in 802.11ac (2013).
*/
public class VHTOperationElement {
public class VHTOperation {
/**
* This field is 0 if the channel width is 20 MHz or 40 MHz, and 1 otherwise.
@@ -30,15 +30,23 @@ public class VHTOperationElement {
* 160 MHz wide channel, this parameter is the channel number of the 80MHz
* channel that contains the primary channel. For a 80+80 MHz wide channel, this
* parameter is the channel number of the primary channel.
* <p>
* This field is an unsigned byte in the specification (i.e., with values
* between 0 and 255). But because Java only supports signed bytes, a short
* data type is used to store the value.
*/
public final byte channel1;
public final short channel1;
/**
* This should be zero unless the channel is 160MHz or 80+80 MHz wide. If the
* channel is 160 MHz wide, this parameter is the channel number of the 160 MHz
* wide channel. If the channel is 80+80 MHz wide, this parameter is the channel
* index of the secondary 80 MHz wide channel.
* <p>
* This field is an unsigned byte in the specification (i.e., with values
* between 0 and 255). But because Java only supports signed bytes, a short
* data type is used to store the value.
*/
public final byte channel2;
public final short channel2;
/**
* An 8-element array where each element is between 0 and 4 inclusive. MCS means
* Modulation and Coding Scheme. NSS means Number of Spatial Streams. There can
@@ -57,11 +65,11 @@ public class VHTOperationElement {
* @param vhtOper a base64 encoded properly formatted VHT operation element (see
* 802.11 standard)
*/
public VHTOperationElement(String vhtOper) {
public VHTOperation(String vhtOper) {
byte[] bytes = Base64.decodeBase64(vhtOper);
this.channelWidth = bytes[0];
this.channel1 = bytes[1];
this.channel2 = bytes[2];
this.channel1 = (short) (bytes[1] & 0xff); // read as unsigned value
this.channel2 = (short) (bytes[2] & 0xff); // read as unsigned value
byte[] vhtMcsForNss = new byte[8];
vhtMcsForNss[0] = (byte) (bytes[3] >>> 6);
vhtMcsForNss[1] = (byte) ((bytes[3] & 0b00110000) >>> 4);
@@ -81,10 +89,10 @@ public class VHTOperationElement {
* For details about the parameters, see the javadocs for the corresponding
* member variables.
*/
public VHTOperationElement(
public VHTOperation(
byte channelWidth,
byte channel1,
byte channel2,
short channel1,
short channel2,
byte[] vhtMcsForNss
) {
/*
@@ -106,7 +114,7 @@ public class VHTOperationElement {
* @return true if the the operation elements "match" for the purpose of
* aggregating statistics; false otherwise.
*/
public boolean matchesForAggregation(VHTOperationElement other) {
public boolean matchesForAggregation(VHTOperation other) {
// check everything except vhtMcsForNss
return other != null && channel1 == other.channel1 &&
channel2 == other.channel2 && channelWidth == other.channelWidth;
@@ -134,8 +142,8 @@ public class VHTOperationElement {
if (vhtOper1 == null || vhtOper2 == null) {
return false; // false if exactly one is null
}
VHTOperationElement vhtOperObj1 = new VHTOperationElement(vhtOper1);
VHTOperationElement vhtOperObj2 = new VHTOperationElement(vhtOper2);
VHTOperation vhtOperObj1 = new VHTOperation(vhtOper1);
VHTOperation vhtOperObj2 = new VHTOperation(vhtOper2);
return vhtOperObj1.matchesForAggregation(vhtOperObj2);
}
@@ -160,7 +168,7 @@ public class VHTOperationElement {
if (getClass() != obj.getClass()) {
return false;
}
VHTOperationElement other = (VHTOperationElement) obj;
VHTOperation other = (VHTOperation) obj;
return channel1 == other.channel1 && channel2 == other.channel2 &&
channelWidth == other.channelWidth &&
Arrays.equals(vhtMcsForNss, other.vhtMcsForNss);

View File

@@ -12,8 +12,8 @@ import com.google.gson.JsonObject;
import com.google.gson.annotations.SerializedName;
public class State {
public class Interface {
public class Client {
public static class Interface {
public static class Client {
public String mac;
public String[] ipv4_addresses;
public String[] ipv6_addresses;
@@ -21,9 +21,9 @@ public class State {
// TODO last_seen
}
public class SSID {
public class Association {
public class Rate {
public static class SSID {
public static class Association {
public static class Rate {
public long bitrate;
public int chwidth;
public boolean sgi;
@@ -66,7 +66,7 @@ public class State {
public JsonObject radio;
}
public class Counters {
public static class Counters {
public long collisions;
public long multicast;
public long rx_bytes;
@@ -96,8 +96,8 @@ public class State {
public Interface[] interfaces;
public class Unit {
public class Memory {
public static class Unit {
public static class Memory {
public long buffered;
public long cached;
public long free;
@@ -112,8 +112,21 @@ public class State {
public Unit unit;
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 Radio[] radios;
// TODO
public JsonObject[] radios;
@SerializedName("link-state") public JsonObject linkState;
public JsonObject gps;
public JsonObject poe;

View File

@@ -10,9 +10,11 @@ package com.facebook.openwifirrm.ucentral.models;
import java.util.Objects;
import com.google.gson.JsonArray;
/** Represents a single entry in wifi scan results. */
/**
* Represents a single entry in wifi scan results.
* ies[] array is not stored directly, but parsed into WifiScanEntry fields
*
*/
public class WifiScanEntryResult {
public int channel;
public long last_seen;
@@ -50,8 +52,6 @@ public class WifiScanEntryResult {
public String vht_oper;
public int capability;
public int frequency;
/** IE = information element */
public JsonArray ies;
/** Default Constructor. */
public WifiScanEntryResult() {}
@@ -68,7 +68,6 @@ public class WifiScanEntryResult {
this.vht_oper = o.vht_oper;
this.capability = o.capability;
this.frequency = o.frequency;
this.ies = o.ies;
}
@Override
@@ -79,7 +78,6 @@ public class WifiScanEntryResult {
channel,
frequency,
ht_oper,
ies,
last_seen,
signal,
ssid,
@@ -104,7 +102,9 @@ public class WifiScanEntryResult {
capability == other.capability && channel == other.channel &&
frequency == other.frequency && Objects
.equals(ht_oper, other.ht_oper) &&
Objects.equals(ies, other.ies) && last_seen == other.last_seen && signal == other.signal && Objects.equals(ssid, other.ssid) && tsf == other.tsf && Objects.equals(vht_oper, other.vht_oper);
last_seen == other.last_seen &&
Objects.equals(ssid, other.ssid) && tsf == other.tsf &&
Objects.equals(vht_oper, other.vht_oper);
}
@Override

View File

@@ -13,7 +13,7 @@ import java.util.List;
import com.facebook.openwifirrm.ucentral.gw.models.NoteInfo;
public class DeviceConfiguration {
public class DeviceConfigurationElement {
public static class DeviceConfigurationElement {
public String name;
public String description;
public Integer weight;

View File

@@ -11,7 +11,7 @@ package com.facebook.openwifirrm.ucentral.prov.models;
import java.util.List;
public class RRMDetails {
public class RRMDetailsImpl {
public static class RRMDetailsImpl {
public String vendor;
public String schedule;
public List<RRMAlgorithmDetails> algorithms;

View File

@@ -9,7 +9,7 @@
package com.facebook.openwifirrm.modules;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import org.junit.jupiter.api.Test;
@@ -23,48 +23,48 @@ public class RRMSchedulerTest {
assertNull(RRMScheduler.parseIntoQuartzCron("* * * * * * * *"));
// correct (6 fields)
assertEquals(
"* * * ? * *",
assertArrayEquals(
new String[] { "* * * ? * *" },
RRMScheduler.parseIntoQuartzCron("* * * * * *")
);
// correct (7 fields)
assertEquals(
"* * * ? * * *",
assertArrayEquals(
new String[] { "* * * ? * * *" },
RRMScheduler.parseIntoQuartzCron("* * * * * * *")
);
// correct value other than * for day of month
assertEquals(
"* * * 1 * ?",
assertArrayEquals(
new String[] { "* * * 1 * ?" },
RRMScheduler.parseIntoQuartzCron("* * * 1 * *")
);
assertEquals(
"* * * 1 * ? *",
assertArrayEquals(
new String[] { "* * * 1 * ? *" },
RRMScheduler.parseIntoQuartzCron("* * * 1 * * *")
);
assertEquals(
"* * * 1/2 * ?",
assertArrayEquals(
new String[] { "* * * 1/2 * ?" },
RRMScheduler.parseIntoQuartzCron("* * * 1/2 * *")
);
assertEquals(
"* * * 1/2 * ? *",
assertArrayEquals(
new String[] { "* * * 1/2 * ? *" },
RRMScheduler.parseIntoQuartzCron("* * * 1/2 * * *")
);
assertEquals(
"* * * 1-2 * ?",
assertArrayEquals(
new String[] { "* * * 1-2 * ?" },
RRMScheduler.parseIntoQuartzCron("* * * 1-2 * *")
);
assertEquals(
"* * * 1-2 * ? *",
assertArrayEquals(
new String[] { "* * * 1-2 * ? *" },
RRMScheduler.parseIntoQuartzCron("* * * 1-2 * * *")
);
assertEquals(
"* * * 1,2 * ?",
assertArrayEquals(
new String[] { "* * * 1,2 * ?" },
RRMScheduler.parseIntoQuartzCron("* * * 1,2 * *")
);
assertEquals(
"* * * 1,2 * ? *",
assertArrayEquals(
new String[] { "* * * 1,2 * ? *" },
RRMScheduler.parseIntoQuartzCron("* * * 1,2 * * *")
);
@@ -79,70 +79,70 @@ public class RRMSchedulerTest {
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0,1 * * *"));
// correct value other than * for day of month
assertEquals(
"* * * ? * 1",
assertArrayEquals(
new String[] { "* * * ? * 1" },
RRMScheduler.parseIntoQuartzCron("* * * * * 1")
);
assertEquals(
"* * * ? * 1 *",
assertArrayEquals(
new String[] { "* * * ? * 1 *" },
RRMScheduler.parseIntoQuartzCron("* * * * * 1 *")
);
assertEquals(
"* * * ? * 1/3",
assertArrayEquals(
new String[] { "* * * ? * 1/3" },
RRMScheduler.parseIntoQuartzCron("* * * * * 1/3")
);
assertEquals(
"* * * ? * 1/3 *",
assertArrayEquals(
new String[] { "* * * ? * 1/3 *" },
RRMScheduler.parseIntoQuartzCron("* * * * * 1/3 *")
);
assertEquals(
"* * * ? * 1-3",
assertArrayEquals(
new String[] { "* * * ? * 1-3" },
RRMScheduler.parseIntoQuartzCron("* * * * * 1-3")
);
assertEquals(
"* * * ? * 1-3 *",
assertArrayEquals(
new String[] { "* * * ? * 1-3 *" },
RRMScheduler.parseIntoQuartzCron("* * * * * 1-3 *")
);
assertEquals(
"* * * ? * 1,3",
assertArrayEquals(
new String[] { "* * * ? * 1,3" },
RRMScheduler.parseIntoQuartzCron("* * * * * 1,3")
);
assertEquals(
"* * * ? * 1,3 *",
assertArrayEquals(
new String[] { "* * * ? * 1,3 *" },
RRMScheduler.parseIntoQuartzCron("* * * * * 1,3 *")
);
// correct value other than * for day of month, make sure 0 turns into 7
assertEquals(
"* * * ? * 7",
assertArrayEquals(
new String[] { "* * * ? * 7" },
RRMScheduler.parseIntoQuartzCron("* * * * * 0")
);
assertEquals(
"* * * ? * 7 *",
assertArrayEquals(
new String[] { "* * * ? * 7 *" },
RRMScheduler.parseIntoQuartzCron("* * * * * 0 *")
);
assertEquals(
"* * * ? * 1/7",
assertArrayEquals(
new String[] { "* * * ? * 1/7" },
RRMScheduler.parseIntoQuartzCron("* * * * * 1/0")
);
assertEquals(
"* * * ? * 1/7 *",
assertArrayEquals(
new String[] { "* * * ? * 1/7 *" },
RRMScheduler.parseIntoQuartzCron("* * * * * 1/0 *")
);
assertEquals(
"* * * ? * 1-7",
assertArrayEquals(
new String[] { "* * * ? * 1-7" },
RRMScheduler.parseIntoQuartzCron("* * * * * 1-0")
);
assertEquals(
"* * * ? * 1-7 *",
assertArrayEquals(
new String[] { "* * * ? * 1-7 *" },
RRMScheduler.parseIntoQuartzCron("* * * * * 1-0 *")
);
assertEquals(
"* * * ? * 1,7",
assertArrayEquals(
new String[] { "* * * ? * 1,7" },
RRMScheduler.parseIntoQuartzCron("* * * * * 1,0")
);
assertEquals(
"* * * ? * 1,7 *",
assertArrayEquals(
new String[] { "* * * ? * 1,7 *" },
RRMScheduler.parseIntoQuartzCron("* * * * * 1,0 *")
);
@@ -155,5 +155,119 @@ public class RRMSchedulerTest {
assertNull(RRMScheduler.parseIntoQuartzCron("* * * * * 7-8 *"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * * * 7,8"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * * * 7,8 *"));
// correct value for both day of week and day of month
assertArrayEquals(
new String[] { "* * * ? * 7", "* * * 1 * ?" },
RRMScheduler.parseIntoQuartzCron("* * * 1 * 0")
);
assertArrayEquals(
new String[] { "* * * ? * 7 *", "* * * 1 * ? *" },
RRMScheduler.parseIntoQuartzCron("* * * 1 * 0 *")
);
assertArrayEquals(
new String[] { "* * * ? * 1/7", "* * * 1 * ?" },
RRMScheduler.parseIntoQuartzCron("* * * 1 * 1/0")
);
assertArrayEquals(
new String[] { "* * * ? * 1", "* * * 1/2 * ?" },
RRMScheduler.parseIntoQuartzCron("* * * 1/2 * 1")
);
assertArrayEquals(
new String[] { "* * * ? * 1/7", "* * * 1/2 * ?" },
RRMScheduler.parseIntoQuartzCron("* * * 1/2 * 1/0")
);
assertArrayEquals(
new String[] { "* * * ? * 1/7 *", "* * * 1 * ? *" },
RRMScheduler.parseIntoQuartzCron("* * * 1 * 1/0 *")
);
assertArrayEquals(
new String[] { "* * * ? * 1 *", "* * * 1/2 * ? *" },
RRMScheduler.parseIntoQuartzCron("* * * 1/2 * 1 *")
);
assertArrayEquals(
new String[] { "* * * ? * 1/7 *", "* * * 1/2 * ? *" },
RRMScheduler.parseIntoQuartzCron("* * * 1/2 * 1/0 *")
);
assertArrayEquals(
new String[] { "* * * ? * 1-7", "* * * 1 * ?" },
RRMScheduler.parseIntoQuartzCron("* * * 1 * 1-0")
);
assertArrayEquals(
new String[] { "* * * ? * 1", "* * * 1-3 * ?" },
RRMScheduler.parseIntoQuartzCron("* * * 1-3 * 1")
);
assertArrayEquals(
new String[] { "* * * ? * 1-7", "* * * 1-3 * ?" },
RRMScheduler.parseIntoQuartzCron("* * * 1-3 * 1-0")
);
assertArrayEquals(
new String[] { "* * * ? * 1-7 *", "* * * 1 * ? *" },
RRMScheduler.parseIntoQuartzCron("* * * 1 * 1-0 *")
);
assertArrayEquals(
new String[] { "* * * ? * 1 *", "* * * 1-3 * ? *" },
RRMScheduler.parseIntoQuartzCron("* * * 1-3 * 1 *")
);
assertArrayEquals(
new String[] { "* * * ? * 1-7 *", "* * * 1-3 * ? *" },
RRMScheduler.parseIntoQuartzCron("* * * 1-3 * 1-0 *")
);
assertArrayEquals(
new String[] { "* * * ? * 1-7", "* * * 1/3 * ?" },
RRMScheduler.parseIntoQuartzCron("* * * 1/3 * 1-0")
);
assertArrayEquals(
new String[] { "* * * ? * 1/7", "* * * 1-3 * ?" },
RRMScheduler.parseIntoQuartzCron("* * * 1-3 * 1/0")
);
assertArrayEquals(
new String[] { "* * * ? * 1-7 *", "* * * 1/3 * ? *" },
RRMScheduler.parseIntoQuartzCron("* * * 1/3 * 1-0 *")
);
assertArrayEquals(
new String[] { "* * * ? * 1/7 *", "* * * 1-3 * ? *" },
RRMScheduler.parseIntoQuartzCron("* * * 1-3 * 1/0 *")
);
// wrong value for either day of week or day of month
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0 * 8"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0 * 8 *"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0 * 7/8"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0 * 7/8 *"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0 * 7-8"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0 * 7-8 *"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0 * 7,8"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0 * 7,8 *"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0/1 * 8"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0/1 * 8 *"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0/1 * 7/8"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0/1 * 7/8 *"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0/1 * 7-8"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0/1 * 7-8 *"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0/1 * 7,8"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0/1 * 7,8 *"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0-1 * 8"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0-1 * 8 *"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0-1 * 7/8"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0-1 * 7/8 *"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0-1 * 7-8"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0-1 * 7-8 *"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0-1 * 7,8"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0-1 * 7,8 *"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0,1 * 8"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0,1 * 8 *"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0,1 * 7/8"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0,1 * 7/8 *"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0,1 * 7-8"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0,1 * 7-8 *"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0,1 * 7,8"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0,1 * 7,8 *"));
}
}

View File

@@ -140,6 +140,26 @@ public class TestUtils {
return jsonList;
}
/**
* Create an array with one radio info entry with the given tx power and
* channel.
*/
public static JsonArray createDeviceStatusSingleBand(
int channel,
int txPower2G
) {
JsonArray jsonList = new JsonArray();
jsonList.add(
createDeviceStatusRadioObject(
UCentralUtils.getBandFromChannel(channel),
channel,
DEFAULT_CHANNEL_WIDTH,
txPower2G
)
);
return jsonList;
}
/**
* Create an array with two radio info entries (2G and 5G), with the given
* tx powers and channels.
@@ -397,23 +417,15 @@ public class TestUtils {
}
/** Create an element of {@link State#radios}. */
private static JsonObject createStateRadio() {
// @formatter:off
return gson.fromJson(
String.format(
" {\n" +
" \"active_ms\": 564328,\n" +
" \"busy_ms\": 36998,\n" +
" \"noise\": 4294967193,\n" +
" \"phy\": \"platform/soc/c000000.wifi\",\n" +
" \"receive_ms\": 28,\n" +
" \"temperature\": 45,\n" +
" \"transmit_ms\": 4893\n" +
" }\n"
),
JsonObject.class
);
// @formatter:on
private static State.Radio createStateRadio() {
State.Radio radio = new State.Radio();
radio.active_ms = 564328;
radio.busy_ms = 36998;
radio.noise = 4294967193L;
radio.phy = "platform/soc/c000000.wifi";
radio.receive_ms = 28;
radio.transmit_ms = 4893;
return radio;
}
/** Create a {@code State.Unit}. */
@@ -477,18 +489,18 @@ public class TestUtils {
state.interfaces[index] = createUpStateInterface(index);
}
state.interfaces[numRadios] = createDownStateInterface(numRadios);
state.radios = new JsonObject[numRadios];
state.radios = new State.Radio[numRadios];
for (int i = 0; i < numRadios; i++) {
state.radios[i] = createStateRadio();
state.radios[i].addProperty("channel", channels[i]);
state.radios[i].addProperty("channel_width", channelWidths[i]);
state.radios[i].addProperty("tx_power", txPowers[i]);
state.radios[i].channel = channels[i];
state.radios[i].channel_width = Integer.toString(channelWidths[i]);
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];
for (int j = 0; j < clientRssis[i].length; j++) {
state.interfaces[i].ssids[0].associations[j] =
state.interfaces[i].ssids[0].new Association();
new State.Interface.SSID.Association();
state.interfaces[i].ssids[0].associations[j].rssi =
clientRssis[i][j];
}

View File

@@ -51,7 +51,7 @@ public class LeastUsedChannelOptimizerTest {
// A -> No APs on current channel, so stay on it (48)
int aExpectedChannel = 48;
dataModel.latestDeviceStatus.put(
dataModel.latestDeviceStatusRadios.put(
deviceA,
TestUtils.createDeviceStatus(band, aExpectedChannel)
);
@@ -73,7 +73,7 @@ public class LeastUsedChannelOptimizerTest {
LinkedList<Integer> channelsB = new LinkedList<>();
channelsB.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band));
int bExpectedChannel = channelsB.removeLast();
dataModel.latestDeviceStatus.put(
dataModel.latestDeviceStatusRadios.put(
deviceB,
TestUtils.createDeviceStatus(band, 40)
);
@@ -94,7 +94,7 @@ public class LeastUsedChannelOptimizerTest {
channelsC.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band));
channelsC.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band));
int cExpectedChannel = channelsC.removeFirst();
dataModel.latestDeviceStatus.put(
dataModel.latestDeviceStatusRadios.put(
deviceC,
TestUtils.createDeviceStatus(band, 149)
);
@@ -138,7 +138,7 @@ public class LeastUsedChannelOptimizerTest {
// A -> No APs on current channel, so stay on it (1)
int aExpectedChannel = 1;
dataModel.latestDeviceStatus.put(
dataModel.latestDeviceStatusRadios.put(
deviceA,
TestUtils.createDeviceStatus(band, aExpectedChannel)
);
@@ -160,7 +160,7 @@ public class LeastUsedChannelOptimizerTest {
LinkedList<Integer> channelsB = new LinkedList<>();
channelsB.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band));
int bExpectedChannel = channelsB.removeLast();
dataModel.latestDeviceStatus.put(
dataModel.latestDeviceStatusRadios.put(
deviceB,
TestUtils.createDeviceStatus(band, 6)
);
@@ -178,7 +178,7 @@ public class LeastUsedChannelOptimizerTest {
// C -> Assigned to only free prioritized channel (1)
int cExpectedChannel = 1;
dataModel.latestDeviceStatus.put(
dataModel.latestDeviceStatusRadios.put(
deviceC,
TestUtils.createDeviceStatus(band, 6)
);
@@ -231,7 +231,7 @@ public class LeastUsedChannelOptimizerTest {
// A, B, C should just be assigned to the same userChannel
int aExpectedChannel = 48;
dataModel.latestDeviceStatus.put(
dataModel.latestDeviceStatusRadios.put(
deviceA,
TestUtils.createDeviceStatus(band, aExpectedChannel)
);
@@ -252,7 +252,7 @@ public class LeastUsedChannelOptimizerTest {
LinkedList<Integer> channelsB = new LinkedList<>();
channelsB.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band));
channelsB.removeLast();
dataModel.latestDeviceStatus.put(
dataModel.latestDeviceStatusRadios.put(
deviceB,
TestUtils.createDeviceStatus(band, 40)
);
@@ -272,7 +272,7 @@ public class LeastUsedChannelOptimizerTest {
channelsC.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band));
channelsC.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band));
channelsC.removeFirst();
dataModel.latestDeviceStatus.put(
dataModel.latestDeviceStatusRadios.put(
deviceC,
TestUtils.createDeviceStatus(band, 149)
);
@@ -324,7 +324,7 @@ public class LeastUsedChannelOptimizerTest {
// A -> No APs on current channel and the current channel is in allowedChannels,
// so stay on it (48)
int aExpectedChannel = 48;
dataModel.latestDeviceStatus.put(
dataModel.latestDeviceStatusRadios.put(
deviceA,
TestUtils.createDeviceStatus(band, aExpectedChannel)
);
@@ -347,7 +347,7 @@ public class LeastUsedChannelOptimizerTest {
LinkedList<Integer> channelsB = new LinkedList<>();
channelsB.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band));
channelsB.removeLast();
dataModel.latestDeviceStatus.put(
dataModel.latestDeviceStatusRadios.put(
deviceB,
TestUtils.createDeviceStatus(band, 40)
);
@@ -368,7 +368,7 @@ public class LeastUsedChannelOptimizerTest {
channelsC.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band));
channelsC.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band));
channelsC.removeFirst();
dataModel.latestDeviceStatus.put(
dataModel.latestDeviceStatusRadios.put(
deviceC,
TestUtils.createDeviceStatus(band, 149)
);
@@ -414,7 +414,7 @@ public class LeastUsedChannelOptimizerTest {
// A -> No APs on current channel, so stay on it (48)
int aExpectedChannel = 157;
dataModel.latestDeviceStatus.put(
dataModel.latestDeviceStatusRadios.put(
deviceA,
TestUtils.createDeviceStatus(band, aExpectedChannel)
);
@@ -438,7 +438,7 @@ public class LeastUsedChannelOptimizerTest {
channelsB.addAll(Arrays.asList(40, 48, 153, 161));
channelsB.addAll(Arrays.asList(40, 48, 153, 161));
int bExpectedChannel = channelsB.removeLast() - 4; // upper extension
dataModel.latestDeviceStatus.put(
dataModel.latestDeviceStatusRadios.put(
deviceB,
TestUtils.createDeviceStatus(band, 40)
);
@@ -459,7 +459,7 @@ public class LeastUsedChannelOptimizerTest {
channelsC.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band));
channelsC.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band));
int cExpectedChannel = channelsC.removeFirst();
dataModel.latestDeviceStatus.put(
dataModel.latestDeviceStatusRadios.put(
deviceC,
TestUtils.createDeviceStatus(band, 149)
);
@@ -479,7 +479,7 @@ public class LeastUsedChannelOptimizerTest {
LinkedList<Integer> channelsD = new LinkedList<>();
channelsD.addAll(Arrays.asList(36, 44, 149, 157));
int dExpectedChannel = channelsD.removeLast();
dataModel.latestDeviceStatus.put(
dataModel.latestDeviceStatusRadios.put(
deviceD,
TestUtils.createDeviceStatus(band, 40)
);
@@ -532,7 +532,7 @@ public class LeastUsedChannelOptimizerTest {
// A -> No APs on current channel, so stay on it (36)
int aExpectedChannel = 36;
dataModel.latestDeviceStatus.put(
dataModel.latestDeviceStatusRadios.put(
deviceA,
TestUtils.createDeviceStatus(band, aExpectedChannel)
);
@@ -554,7 +554,7 @@ public class LeastUsedChannelOptimizerTest {
LinkedList<Integer> channelsB = new LinkedList<>();
channelsB.addAll(Arrays.asList(40, 48, 149));
int bExpectedChannel = channelsB.removeLast();
dataModel.latestDeviceStatus.put(
dataModel.latestDeviceStatusRadios.put(
deviceB,
TestUtils.createDeviceStatus(band, 36)
);
@@ -575,7 +575,7 @@ public class LeastUsedChannelOptimizerTest {
channelsC.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band));
channelsC.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band));
int cExpectedChannel = channelsC.removeFirst();
dataModel.latestDeviceStatus.put(
dataModel.latestDeviceStatusRadios.put(
deviceC,
TestUtils.createDeviceStatus(band, 149)
);
@@ -597,7 +597,7 @@ public class LeastUsedChannelOptimizerTest {
channelsD.addAll(Arrays.asList(40, 48, 153, 161));
channelsD.addAll(Arrays.asList(40, 48, 153, 161));
int dExpectedChannel = channelsD.removeLast() - 12;
dataModel.latestDeviceStatus.put(
dataModel.latestDeviceStatusRadios.put(
deviceD,
TestUtils.createDeviceStatus(band, 36)
);
@@ -622,7 +622,7 @@ public class LeastUsedChannelOptimizerTest {
.put(UCentralConstants.BAND_5G, Arrays.asList(48, 165));
deviceDataManager.setDeviceApConfig(deviceE, apConfig);
int eExpectedChannel = 36;
dataModel.latestDeviceStatus.put(
dataModel.latestDeviceStatusRadios.put(
deviceE,
TestUtils.createDeviceStatus(band, eExpectedChannel)
);
@@ -668,7 +668,7 @@ public class LeastUsedChannelOptimizerTest {
// A -> No APs on current channel, so stay on it (48)
int aExpectedChannel = 48;
dataModel.latestDeviceStatus.put(
dataModel.latestDeviceStatusRadios.put(
deviceA,
TestUtils.createDeviceStatus(band, aExpectedChannel)
);
@@ -689,7 +689,7 @@ public class LeastUsedChannelOptimizerTest {
// B -> Same setting as A, but the scan results are bandwidth aware
// Assign to only free channel (165)
int bExpectedChannel = 165;
dataModel.latestDeviceStatus.put(
dataModel.latestDeviceStatusRadios.put(
deviceB,
TestUtils.createDeviceStatus(band, 48)
);
@@ -721,7 +721,7 @@ public class LeastUsedChannelOptimizerTest {
channelsC1.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band));
channelsC2.addAll(Arrays.asList(36, 157, 165));
int cExpectedChannel = channelsC1.removeFirst();
dataModel.latestDeviceStatus.put(
dataModel.latestDeviceStatusRadios.put(
deviceC,
TestUtils.createDeviceStatus(band, 149)
);

View File

@@ -54,11 +54,11 @@ public class RandomChannelInitializerTest {
deviceB,
TestUtils.createState(11, channelWidth, deviceBBssid)
);
dataModel.latestDeviceStatus.put(
dataModel.latestDeviceStatusRadios.put(
deviceA,
TestUtils.createDeviceStatus(band, 7)
);
dataModel.latestDeviceStatus.put(
dataModel.latestDeviceStatusRadios.put(
deviceB,
TestUtils.createDeviceStatus(band, 8)
);
@@ -99,11 +99,11 @@ public class RandomChannelInitializerTest {
deviceB,
TestUtils.createState(11, channelWidth, deviceBBssid)
);
dataModel.latestDeviceStatus.put(
dataModel.latestDeviceStatusRadios.put(
deviceA,
TestUtils.createDeviceStatus(band, 7)
);
dataModel.latestDeviceStatus.put(
dataModel.latestDeviceStatusRadios.put(
deviceB,
TestUtils.createDeviceStatus(band, 8)
);

View File

@@ -52,7 +52,7 @@ public class UnmanagedApAwareChannelOptimizerTest {
// A -> No APs on current channel, so stay on it (48)
int aExpectedChannel = 48;
dataModel.latestDeviceStatus.put(
dataModel.latestDeviceStatusRadios.put(
deviceA,
TestUtils.createDeviceStatus(band, aExpectedChannel)
);
@@ -76,7 +76,7 @@ public class UnmanagedApAwareChannelOptimizerTest {
LinkedList<Integer> channelsB = new LinkedList<>();
channelsB.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band));
int bExpectedChannel = channelsB.removeLast();
dataModel.latestDeviceStatus.put(
dataModel.latestDeviceStatusRadios.put(
deviceB,
TestUtils.createDeviceStatus(band, 40)
);
@@ -110,7 +110,7 @@ public class UnmanagedApAwareChannelOptimizerTest {
)
);
int cExpectedChannel = 48;
dataModel.latestDeviceStatus.put(
dataModel.latestDeviceStatusRadios.put(
deviceC,
TestUtils.createDeviceStatus(band, 149)
);
@@ -156,7 +156,7 @@ public class UnmanagedApAwareChannelOptimizerTest {
// A -> No APs on current channel, so stay on it (1)
int aExpectedChannel = 1;
dataModel.latestDeviceStatus.put(
dataModel.latestDeviceStatusRadios.put(
deviceA,
TestUtils.createDeviceStatus(band, aExpectedChannel)
);
@@ -178,7 +178,7 @@ public class UnmanagedApAwareChannelOptimizerTest {
LinkedList<Integer> channelsB = new LinkedList<>();
channelsB.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band));
int bExpectedChannel = channelsB.removeLast();
dataModel.latestDeviceStatus.put(
dataModel.latestDeviceStatusRadios.put(
deviceB,
TestUtils.createDeviceStatus(band, 6)
);
@@ -196,7 +196,7 @@ public class UnmanagedApAwareChannelOptimizerTest {
// C -> Assigned to only free prioritized channel (1)
int cExpectedChannel = 1;
dataModel.latestDeviceStatus.put(
dataModel.latestDeviceStatusRadios.put(
deviceC,
TestUtils.createDeviceStatus(band, 6)
);

View File

@@ -128,7 +128,7 @@ public class LocationBasedOptimalTPCTest {
DataModel dataModel = new DataModel();
for (String device : Arrays.asList(deviceA, deviceB, deviceC)) {
dataModel.latestDeviceStatus.put(
dataModel.latestDeviceStatusRadios.put(
device,
TestUtils.createDeviceStatus(UCentralConstants.BANDS)
);
@@ -195,7 +195,7 @@ public class LocationBasedOptimalTPCTest {
DataModel dataModel2 = new DataModel();
for (String device : Arrays.asList(deviceA, deviceB)) {
dataModel2.latestDeviceStatus.put(
dataModel2.latestDeviceStatusRadios.put(
device,
TestUtils.createDeviceStatus(UCentralConstants.BANDS)
);
@@ -213,7 +213,7 @@ public class LocationBasedOptimalTPCTest {
)
);
}
dataModel2.latestDeviceStatus
dataModel2.latestDeviceStatusRadios
.put(
deviceC,
TestUtils.createDeviceStatus(
@@ -304,7 +304,7 @@ public class LocationBasedOptimalTPCTest {
DataModel dataModel2 = new DataModel();
for (String device : Arrays.asList(deviceA, deviceB, deviceC)) {
dataModel2.latestDeviceStatus.put(
dataModel2.latestDeviceStatusRadios.put(
device,
TestUtils.createDeviceStatus(UCentralConstants.BANDS)
);
@@ -363,7 +363,7 @@ public class LocationBasedOptimalTPCTest {
DataModel dataModel3 = new DataModel();
for (String device : Arrays.asList(deviceA, deviceB, deviceC)) {
dataModel3.latestDeviceStatus.put(
dataModel3.latestDeviceStatusRadios.put(
device,
TestUtils.createDeviceStatus(UCentralConstants.BANDS)
);
@@ -412,7 +412,7 @@ public class LocationBasedOptimalTPCTest {
DataModel dataModel4 = new DataModel();
for (String device : Arrays.asList(deviceA, deviceB, deviceC)) {
dataModel4.latestDeviceStatus.put(
dataModel4.latestDeviceStatusRadios.put(
device,
TestUtils.createDeviceStatus(UCentralConstants.BANDS)
);

View File

@@ -33,8 +33,6 @@ import com.facebook.openwifirrm.optimizers.TestUtils;
import com.facebook.openwifirrm.ucentral.UCentralConstants;
import com.facebook.openwifirrm.ucentral.UCentralUtils;
import com.facebook.openwifirrm.ucentral.WifiScanEntry;
import com.facebook.openwifirrm.ucentral.models.State;
import com.google.gson.JsonArray;
@TestMethodOrder(OrderAnnotation.class)
public class MeasurementBasedApApTPCTest {
@@ -78,65 +76,84 @@ public class MeasurementBasedApApTPCTest {
return deviceDataManager;
}
/**
* Creates a data model with 3 devices in only the given band.
* All are at max_tx_power, which represents the first step in greedy TPC.
*
* @param band band (e.g., "2G")
*/
private static DataModel createModelSingleBand(String band) {
DataModel model = new DataModel();
final int channel = UCentralUtils.LOWER_CHANNEL_LIMIT.get(band);
List<String> bssids = Arrays.asList(BSSID_A, BSSID_B, BSSID_C);
List<String> devices = Arrays.asList(DEVICE_A, DEVICE_B, DEVICE_C);
for (int i = 0; i < devices.size(); i++) {
String device = devices.get(i);
String bssid = bssids.get(i);
model.latestState.put(
device,
TestUtils.createState(
channel,
DEFAULT_CHANNEL_WIDTH,
MAX_TX_POWER,
bssid
)
);
model.latestDeviceStatusRadios.put(
device,
TestUtils
.createDeviceStatusSingleBand(channel, MAX_TX_POWER)
);
}
return model;
}
/**
* Creates a data model with 3 devices. All are at max_tx_power, which
* represents the first step in greedy TPC.
*
* @return a data model
*/
private static DataModel createModel() {
private static DataModel createModelDualBand() {
DataModel model = new DataModel();
State stateA = TestUtils.createState(
1,
DEFAULT_CHANNEL_WIDTH,
MAX_TX_POWER,
BSSID_A,
36,
DEFAULT_CHANNEL_WIDTH,
MAX_TX_POWER,
BSSID_A
);
State stateB = TestUtils.createState(
1,
DEFAULT_CHANNEL_WIDTH,
MAX_TX_POWER,
BSSID_B,
36,
DEFAULT_CHANNEL_WIDTH,
MAX_TX_POWER,
BSSID_B
);
State stateC = TestUtils.createState(
1,
DEFAULT_CHANNEL_WIDTH,
MAX_TX_POWER,
BSSID_C,
36,
DEFAULT_CHANNEL_WIDTH,
MAX_TX_POWER,
BSSID_C
);
final int channel2G =
UCentralUtils.LOWER_CHANNEL_LIMIT.get(UCentralConstants.BAND_2G);
final int channel5G =
UCentralUtils.LOWER_CHANNEL_LIMIT.get(UCentralConstants.BAND_5G);
model.latestState.put(DEVICE_A, stateA);
model.latestState.put(DEVICE_B, stateB);
model.latestState.put(DEVICE_C, stateC);
model.latestDeviceStatus.put(
DEVICE_A,
TestUtils
.createDeviceStatusDualBand(1, MAX_TX_POWER, 36, MAX_TX_POWER)
);
model.latestDeviceStatus.put(
DEVICE_B,
TestUtils
.createDeviceStatusDualBand(1, MAX_TX_POWER, 36, MAX_TX_POWER)
);
model.latestDeviceStatus.put(
DEVICE_C,
TestUtils
.createDeviceStatusDualBand(1, MAX_TX_POWER, 36, MAX_TX_POWER)
);
List<String> bssids = Arrays.asList(BSSID_A, BSSID_B, BSSID_C);
List<String> devices = Arrays.asList(DEVICE_A, DEVICE_B, DEVICE_C);
for (int i = 0; i < devices.size(); i++) {
String device = devices.get(i);
String bssid = bssids.get(i);
model.latestState.put(
device,
TestUtils.createState(
channel2G,
DEFAULT_CHANNEL_WIDTH,
MAX_TX_POWER,
bssid,
channel5G,
DEFAULT_CHANNEL_WIDTH,
MAX_TX_POWER,
bssid
)
);
model.latestDeviceStatusRadios.put(
device,
TestUtils
.createDeviceStatusDualBand(
channel2G,
MAX_TX_POWER,
channel5G,
MAX_TX_POWER
)
);
}
return model;
}
@@ -287,7 +304,7 @@ public class MeasurementBasedApApTPCTest {
@Test
@Order(1)
void testGetManagedBSSIDs() throws Exception {
DataModel dataModel = createModel();
DataModel dataModel = createModelDualBand();
Set<String> managedBSSIDs =
MeasurementBasedApApTPC.getManagedBSSIDs(dataModel);
assertEquals(3, managedBSSIDs.size());
@@ -298,25 +315,6 @@ public class MeasurementBasedApApTPCTest {
@Test
@Order(2)
void testGetCurrentTxPower() throws Exception {
final int expectedTxPower = 29;
DataModel model = new DataModel();
model.latestDeviceStatus.put(
DEVICE_A,
TestUtils.createDeviceStatusDualBand(1, 5, 36, expectedTxPower)
);
JsonArray radioStatuses =
model.latestDeviceStatus.get(DEVICE_A).getAsJsonArray();
int txPower = MeasurementBasedApApTPC
.getCurrentTxPower(radioStatuses, UCentralConstants.BAND_5G)
.get();
assertEquals(expectedTxPower, txPower);
}
@Test
@Order(3)
void testBuildRssiMap() throws Exception {
// This example includes three APs, and one AP that is unmanaged
Set<String> bssidSet = Set.of(BSSID_A, BSSID_B, BSSID_C);
@@ -338,7 +336,7 @@ public class MeasurementBasedApApTPCTest {
}
@Test
@Order(4)
@Order(3)
void testComputeTxPower() throws Exception {
// Test examples here taken from algorithm design doc from @pohanhf
final String serialNumber = "testSerial";
@@ -413,7 +411,7 @@ public class MeasurementBasedApApTPCTest {
*/
private static void testComputeTxPowerMapSimpleInOneBand(String band) {
int channel = UCentralUtils.LOWER_CHANNEL_LIMIT.get(band);
DataModel dataModel = createModel();
DataModel dataModel = createModelSingleBand(band);
dataModel.latestWifiScans = createLatestWifiScansB(channel);
DeviceDataManager deviceDataManager = createDeviceDataManager();
@@ -438,7 +436,7 @@ public class MeasurementBasedApApTPCTest {
String band
) {
int channel = UCentralUtils.LOWER_CHANNEL_LIMIT.get(band);
DataModel dataModel = createModel();
DataModel dataModel = createModelSingleBand(band);
dataModel.latestWifiScans = createLatestWifiScansC(channel);
DeviceDataManager deviceDataManager = createDeviceDataManager();
@@ -465,7 +463,7 @@ public class MeasurementBasedApApTPCTest {
*/
private static void testComputeTxPowerMapMissingDataInOneBand(String band) {
int channel = UCentralUtils.LOWER_CHANNEL_LIMIT.get(band);
DataModel dataModel = createModel();
DataModel dataModel = createModelSingleBand(band);
dataModel.latestWifiScans =
createLatestWifiScansWithMissingEntries(channel);
DeviceDataManager deviceDataManager = createDeviceDataManager();
@@ -501,34 +499,17 @@ public class MeasurementBasedApApTPCTest {
@Order(6)
void testComputeTxPowerMapMultiBand() {
// test both bands simultaneously with different setups on each band
DataModel dataModel = createModel();
dataModel.latestState.remove(DEVICE_B);
dataModel.latestState.put(
DEVICE_B,
TestUtils.createState(
1,
DEFAULT_CHANNEL_WIDTH,
MAX_TX_POWER,
BSSID_B
)
);
// make device C not operate in the 5G band instead of dual band
dataModel.latestDeviceStatus.put(
DEVICE_C,
TestUtils.createDeviceStatus(
UCentralConstants.BAND_2G,
1,
MAX_TX_POWER
)
);
DataModel dataModel = createModelDualBand();
DeviceDataManager deviceDataManager = createDeviceDataManager();
// 2G setup
// 2G: use testComputeTxPowerMapSimpleInOneBand setup
final int channel2G =
UCentralUtils.LOWER_CHANNEL_LIMIT.get(UCentralConstants.BAND_2G);
dataModel.latestWifiScans = createLatestWifiScansB(channel2G);
// 5G setup
// 5G: use testComputeTxPowerMapMissingDataInOneBand setup
final int channel5G =
UCentralUtils.LOWER_CHANNEL_LIMIT.get(UCentralConstants.BAND_5G);
// add 5G wifiscan results to dataModel.latestWifiScans
Map<String, List<List<WifiScanEntry>>> toMerge =
createLatestWifiScansWithMissingEntries(channel5G);
for (
@@ -556,8 +537,10 @@ public class MeasurementBasedApApTPCTest {
Map<String, Map<String, Integer>> txPowerMap =
optimizer.computeTxPowerMap();
// test 2G band
// every AP operates in at least one band
assertEquals(3, txPowerMap.size());
// test 2G band
assertEquals(
2,
txPowerMap.get(DEVICE_A).get(UCentralConstants.BAND_2G)
@@ -576,11 +559,69 @@ public class MeasurementBasedApApTPCTest {
0,
txPowerMap.get(DEVICE_A).get(UCentralConstants.BAND_5G)
);
// deivce B does not have 5G radio
assertFalse(
txPowerMap.get(DEVICE_B).containsKey(UCentralConstants.BAND_5G)
assertEquals(
0,
txPowerMap.get(DEVICE_B).get(UCentralConstants.BAND_5G)
);
// device C is not in the 5G band
assertEquals(
30,
txPowerMap.get(DEVICE_C).get(UCentralConstants.BAND_5G)
);
// now test when device C does not have a 5G radio
dataModel.latestState.put(
DEVICE_C,
TestUtils.createState(
1,
DEFAULT_CHANNEL_WIDTH,
MAX_TX_POWER,
BSSID_C
)
);
dataModel.latestDeviceStatusRadios.put(
DEVICE_C,
TestUtils.createDeviceStatus(
UCentralConstants.BAND_2G,
1,
MAX_TX_POWER
)
);
optimizer = new MeasurementBasedApApTPC(
dataModel,
TEST_ZONE,
deviceDataManager,
-80,
0
);
txPowerMap = optimizer.computeTxPowerMap();
// every AP operates in at least one band
assertEquals(3, txPowerMap.size());
// test 2G band (all APs), same as testComputeTxPowerMapSimpleInOneBand
assertEquals(
2,
txPowerMap.get(DEVICE_A).get(UCentralConstants.BAND_2G)
);
assertEquals(
15,
txPowerMap.get(DEVICE_B).get(UCentralConstants.BAND_2G)
);
assertEquals(
10,
txPowerMap.get(DEVICE_C).get(UCentralConstants.BAND_2G)
);
// test 5G band (only device A and device B operate in 5G)
assertEquals(
0,
txPowerMap.get(DEVICE_A).get(UCentralConstants.BAND_5G)
);
assertEquals(
0,
txPowerMap.get(DEVICE_B).get(UCentralConstants.BAND_5G)
);
// this time device C has no 5G radio so it is not set to max power (30)
assertFalse(
txPowerMap.get(DEVICE_C).containsKey(UCentralConstants.BAND_5G)
);

View File

@@ -8,7 +8,12 @@
package com.facebook.openwifirrm.ucentral;
import java.util.Collections;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertFalse;
import org.junit.jupiter.api.Test;
@@ -17,4 +22,60 @@ public class UCentralUtilsTest {
void test_placeholder() throws Exception {
assertEquals(3, 1 + 2);
}
@Test
void test_setRadioConfigFieldChannel() throws Exception {
final String serialNumber = "aaaaaaaaaaaa";
final int expectedChannel = 1;
final Map<String, Integer> newValueList = Collections
.singletonMap(UCentralConstants.BAND_5G, expectedChannel);
// test case where channel value is a string and not an integer
UCentralApConfiguration config = new UCentralApConfiguration(
"{\"interfaces\": [], \"radios\": [{\"band\": \"5G\", \"channel\": \"auto\"}]}"
);
boolean modified = UCentralUtils
.setRadioConfigField(serialNumber, config, "channel", newValueList);
assertTrue(modified);
assertEquals(
config.getRadioConfig(0).get("channel").getAsInt(),
expectedChannel
);
// field doesn't exist
config = new UCentralApConfiguration(
"{\"interfaces\": [], \"radios\": [{\"band\": \"5G\"}]}"
);
modified = UCentralUtils
.setRadioConfigField(serialNumber, config, "channel", newValueList);
assertTrue(modified);
assertEquals(
config.getRadioConfig(0).get("channel").getAsInt(),
expectedChannel
);
// normal field, not modified
config = new UCentralApConfiguration(
"{\"interfaces\": [], \"radios\": [{\"band\": \"5G\", \"channel\": 1}]}"
);
modified = UCentralUtils
.setRadioConfigField(serialNumber, config, "channel", newValueList);
assertFalse(modified);
assertEquals(
config.getRadioConfig(0).get("channel").getAsInt(),
expectedChannel
);
// normal field, modified
config = new UCentralApConfiguration(
"{\"interfaces\": [], \"radios\": [{\"band\": \"5G\", \"channel\": 15}]}"
);
modified = UCentralUtils
.setRadioConfigField(serialNumber, config, "channel", newValueList);
assertTrue(modified);
assertEquals(
config.getRadioConfig(0).get("channel").getAsInt(),
expectedChannel
);
}
}

View File

@@ -6,17 +6,17 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.operationelement;
package com.facebook.openwifirrm.ucentral.informationelement;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
public class HTOperationElementTest {
public class HTOperationTest {
@Test
void testGetHtOper() {
String htOper = "AQAEAAAAAAAAAAAAAAAAAAAAAAAAAA==";
HTOperationElement htOperObj = new HTOperationElement(htOper);
HTOperation htOperObj = new HTOperation(htOper);
byte expectedPrimaryChannel = 1;
byte expectedSecondaryChannelOffset = 0;
boolean expectedStaChannelWidth = false;
@@ -28,7 +28,7 @@ public class HTOperationElementTest {
boolean expectedDualBeacon = false;
boolean expectedDualCtsProtection = false;
boolean expectedStbcBeacon = false;
HTOperationElement expectedHtOperObj = new HTOperationElement(
HTOperation expectedHtOperObj = new HTOperation(
expectedPrimaryChannel,
expectedSecondaryChannelOffset,
expectedStaChannelWidth,
@@ -44,11 +44,11 @@ public class HTOperationElementTest {
assertEquals(expectedHtOperObj, htOperObj);
htOper = "JAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==";
htOperObj = new HTOperationElement(htOper);
htOperObj = new HTOperation(htOper);
// all fields except the primary channel and nongreenfield field are the same
expectedPrimaryChannel = 36;
expectedNongreenfieldHtStasPresent = false;
expectedHtOperObj = new HTOperationElement(
expectedHtOperObj = new HTOperation(
expectedPrimaryChannel,
expectedSecondaryChannelOffset,
expectedStaChannelWidth,

View File

@@ -6,23 +6,23 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.operationelement;
package com.facebook.openwifirrm.ucentral.informationelement;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
public class VHTOperationElementTest {
public class VHTOperationTest {
@Test
void testGetVhtOper() {
String vhtOper = "ACQAAAA=";
VHTOperationElement vhtOperObj = new VHTOperationElement(vhtOper);
VHTOperation vhtOperObj = new VHTOperation(vhtOper);
byte expectedChannelWidthIndicator = 0; // 20 MHz channel width
byte expectedChannel1 = 36;
byte expectedChannel2 = 0;
short expectedChannel1 = 36;
short expectedChannel2 = 0;
byte[] expectedVhtMcsForNss = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 };
VHTOperationElement expectedVhtOperObj = new VHTOperationElement(
VHTOperation expectedVhtOperObj = new VHTOperation(
expectedChannelWidthIndicator,
expectedChannel1,
expectedChannel2,
@@ -31,12 +31,12 @@ public class VHTOperationElementTest {
assertEquals(expectedVhtOperObj, vhtOperObj);
vhtOper = "AToAUAE=";
vhtOperObj = new VHTOperationElement(vhtOper);
vhtOperObj = new VHTOperation(vhtOper);
expectedChannelWidthIndicator = 1; // 80 MHz channel width
expectedChannel1 = 58;
// same channel2
expectedVhtMcsForNss = new byte[] { 1, 1, 0, 0, 0, 0, 0, 1 };
expectedVhtOperObj = new VHTOperationElement(
expectedVhtOperObj = new VHTOperation(
expectedChannelWidthIndicator,
expectedChannel1,
expectedChannel2,
@@ -45,12 +45,27 @@ public class VHTOperationElementTest {
assertEquals(expectedVhtOperObj, vhtOperObj);
vhtOper = "ASoyUAE=";
vhtOperObj = new VHTOperationElement(vhtOper);
vhtOperObj = new VHTOperation(vhtOper);
// same channel width indicator (160 MHz channel width)
expectedChannel1 = 42;
expectedChannel2 = 50;
// same vhtMcsForNss
expectedVhtOperObj = new VHTOperationElement(
expectedVhtOperObj = new VHTOperation(
expectedChannelWidthIndicator,
expectedChannel1,
expectedChannel2,
expectedVhtMcsForNss
);
assertEquals(expectedVhtOperObj, vhtOperObj);
// test with channel number >= 128 (channel fields should be unsigned)
vhtOper = "AJUAAAA=";
vhtOperObj = new VHTOperation(vhtOper);
expectedChannelWidthIndicator = 0;
expectedChannel1 = 149;
expectedChannel2 = 0;
expectedVhtMcsForNss = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 };
expectedVhtOperObj = new VHTOperation(
expectedChannelWidthIndicator,
expectedChannel1,
expectedChannel2,