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
35 changed files with 1412 additions and 349 deletions

View File

@@ -9,7 +9,7 @@ fullnameOverride: ""
images:
owrrm:
repository: tip-tip-wlan-cloud-ucentral.jfrog.io/owrrm
tag: v2.7.0
tag: main
pullPolicy: Always
# regcred:
# registry: tip-tip-wlan-cloud-ucentral.jfrog.io

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

@@ -238,6 +238,7 @@ public class Modeler implements Runnable {
return;
}
}
client.refreshAccessToken();
// TODO: backfill data from database?

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

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.
@@ -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 (
@@ -639,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
@@ -664,8 +663,5 @@ public abstract class ChannelOptimizer {
deviceConfig.autoChannels = entry.getValue();
}
});
// Trigger config update now
configManager.wakeUp();
}
}

View File

@@ -13,19 +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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* TPC (Transmit Power Control) base class.
@@ -83,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);
@@ -127,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!!! " +
@@ -153,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
@@ -178,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<>();

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.
*

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.
@@ -65,7 +65,7 @@ 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 = (short) (bytes[1] & 0xff); // read as unsigned value
@@ -89,7 +89,7 @@ public class VHTOperationElement {
* For details about the parameters, see the javadocs for the corresponding
* member variables.
*/
public VHTOperationElement(
public VHTOperation(
byte channelWidth,
short channel1,
short channel2,
@@ -114,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;
@@ -142,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);
}
@@ -168,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;

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.
@@ -480,7 +500,7 @@ public class TestUtils {
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

@@ -33,7 +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;
@TestMethodOrder(OrderAnnotation.class)
public class MeasurementBasedApApTPCTest {
@@ -77,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.latestDeviceStatusRadios.put(
DEVICE_A,
TestUtils
.createDeviceStatusDualBand(1, MAX_TX_POWER, 36, MAX_TX_POWER)
);
model.latestDeviceStatusRadios.put(
DEVICE_B,
TestUtils
.createDeviceStatusDualBand(1, MAX_TX_POWER, 36, MAX_TX_POWER)
);
model.latestDeviceStatusRadios.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;
}
@@ -286,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());
@@ -393,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();
@@ -418,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();
@@ -445,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();
@@ -481,35 +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.latestState.put(
DEVICE_C,
TestUtils.createState(
1,
DEFAULT_CHANNEL_WIDTH,
MAX_TX_POWER,
BSSID_C
)
);
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 (
@@ -537,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)
@@ -557,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

@@ -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
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,12 @@ 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,
@@ -60,12 +60,12 @@ public class VHTOperationElementTest {
// test with channel number >= 128 (channel fields should be unsigned)
vhtOper = "AJUAAAA=";
vhtOperObj = new VHTOperationElement(vhtOper);
vhtOperObj = new VHTOperation(vhtOper);
expectedChannelWidthIndicator = 0;
expectedChannel1 = 149;
expectedChannel2 = 0;
expectedVhtMcsForNss = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 };
expectedVhtOperObj = new VHTOperationElement(
expectedVhtOperObj = new VHTOperation(
expectedChannelWidthIndicator,
expectedChannel1,
expectedChannel2,