mirror of
				https://github.com/Telecominfraproject/wlan-cloud-rrm.git
				synced 2025-10-31 10:38:02 +00:00 
			
		
		
		
	Compare commits
	
		
			21 Commits
		
	
	
		
			v2.7.0-RC2
			...
			token_refr
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 6fb30ce7e4 | ||
|   | 350a45b616 | ||
|   | 52dae760d8 | ||
|   | 343fc7b6ee | ||
|   | 2a952f56a9 | ||
|   | 52a2258c2d | ||
|   | 0b4fd49627 | ||
|   | d81df03637 | ||
|   | 594fd9fa91 | ||
|   | 8c48a8901b | ||
|   | 0ac189f493 | ||
|   | df21d07ec9 | ||
|   | 01a070c9b7 | ||
|   | 5211eae7c6 | ||
|   | fafbda0bd8 | ||
|   | 43c9aaafb2 | ||
|   | 89e637cfeb | ||
|   | 0a64fb4963 | ||
|   | 4191bc1a70 | ||
|   | 3b6e83d103 | ||
|   | 27c36ff444 | 
| @@ -478,7 +478,9 @@ components: | ||||
|     RRMSchedule: | ||||
|       type: object | ||||
|       properties: | ||||
|         cron: | ||||
|         crons: | ||||
|           type: array | ||||
|           items: | ||||
|             type: string | ||||
|         algorithms: | ||||
|           type: array | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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. */ | ||||
|   | ||||
| @@ -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. | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -218,6 +218,7 @@ public class DataCollector implements Runnable { | ||||
| 				return; | ||||
| 			} | ||||
| 		} | ||||
| 		client.refreshAccessToken(); | ||||
|  | ||||
| 		// Fetch device list | ||||
| 		List<DeviceWithStatus> devices = client.getDevices(); | ||||
|   | ||||
| @@ -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"); | ||||
|   | ||||
| @@ -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); | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -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; | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
| 			// 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,33 +247,41 @@ 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 | ||||
| 			} | ||||
|  | ||||
| 			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(config.schedule.cron); | ||||
| 					CronExpression.validateExpression(cron); | ||||
| 				} catch (ParseException e) { | ||||
| 					logger.error( | ||||
| 						String.format( | ||||
| 							"Invalid cron expression (%s) for zone %s", | ||||
| 						config.schedule.cron, | ||||
| 							cron, | ||||
| 							zone | ||||
| 						), | ||||
| 						e | ||||
| @@ -253,50 +290,55 @@ public class RRMScheduler { | ||||
| 				} | ||||
|  | ||||
| 				// Create trigger | ||||
| 				String jobKey = String.format("%s:%d", zone, i); | ||||
| 				Trigger trigger = TriggerBuilder.newTrigger() | ||||
| 				.withIdentity(zone) | ||||
| 					.withIdentity(jobKey) | ||||
| 					.forJob(job) | ||||
| 					.withSchedule( | ||||
| 					CronScheduleBuilder.cronSchedule(config.schedule.cron) | ||||
| 						CronScheduleBuilder.cronSchedule(cron) | ||||
| 					) | ||||
| 					.build(); | ||||
|  | ||||
| 				try { | ||||
| 				if (!prevScheduled.contains(zone)) { | ||||
| 					if (!prevScheduled.contains(jobKey)) { | ||||
| 						scheduler.scheduleJob(trigger); | ||||
| 					} else { | ||||
| 						scheduler.rescheduleJob(trigger.getKey(), trigger); | ||||
| 					} | ||||
| 				} catch (SchedulerException e) { | ||||
| 					logger.error( | ||||
| 					"Failed to schedule RRM trigger for zone: " + zone, | ||||
| 						"Failed to schedule RRM trigger for job key: " + jobKey, | ||||
| 						e | ||||
| 					); | ||||
| 					continue; | ||||
| 				} | ||||
| 			scheduled.add(zone); | ||||
|  | ||||
| 				scheduled.add(jobKey); | ||||
| 				logger.debug( | ||||
| 				"Scheduled/updated RRM for zone '{}' at: < {} >", | ||||
| 				zone, | ||||
| 				config.schedule.cron | ||||
| 					"Scheduled/updated RRM for job key '{}' at: < {} >", | ||||
| 					jobKey, | ||||
| 					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); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -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; | ||||
| 	} | ||||
|   | ||||
| @@ -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,16 +379,27 @@ 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(); | ||||
| 				// 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(); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
| 			); | ||||
|   | ||||
| @@ -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 | ||||
| 			); | ||||
|   | ||||
| @@ -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,20 +339,45 @@ 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 | ||||
|  | ||||
| 			// An AP can have multiple interfaces, optimize for all of them | ||||
| 			for (State.Interface iface : state.interfaces) { | ||||
| 				if (iface.ssids == null) { | ||||
| 					continue; | ||||
| 				} | ||||
| 			int currentTxPower = possibleCurrentTxPower.get(); | ||||
| 			String bssid = state.interfaces[0].ssids[0].bssid; | ||||
|  | ||||
| 				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 <{}> : BSSID <{}>", serialNumber, bssid); | ||||
| 					logger | ||||
| 						.debug( | ||||
| 							"Device <{}> : Interface <{}> : Channel <{}> : BSSID <{}>", | ||||
| 							serialNumber, | ||||
| 							iface.name, | ||||
| 							channel, | ||||
| 							bssid | ||||
| 						); | ||||
| 					for (int rssi : rssiValues) { | ||||
| 						logger.debug("  Neighbor received RSSI: {}", rssi); | ||||
| 					} | ||||
| @@ -402,10 +396,13 @@ public class MeasurementBasedApApTPC extends TPC { | ||||
| 					); | ||||
| 					logger.debug("  Old tx_power: {}", currentTxPower); | ||||
| 					logger.debug("  New tx_power: {}", newTxPower); | ||||
| 			txPowerMap.computeIfAbsent(serialNumber, k -> new TreeMap<>()) | ||||
| 					txPowerMap | ||||
| 						.computeIfAbsent(serialNumber, k -> new TreeMap<>()) | ||||
| 						.put(band, newTxPower); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public Map<String, Map<String, Integer>> computeTxPowerMap() { | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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); | ||||
| 	} | ||||
|  | ||||
| } | ||||
| @@ -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()); | ||||
|   | ||||
| @@ -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; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
| @@ -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 + "]"; | ||||
| 	} | ||||
| } | ||||
| @@ -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 && | ||||
| @@ -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 + "]"; | ||||
| 	} | ||||
| } | ||||
| @@ -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 + "]"; | ||||
| 	} | ||||
| } | ||||
| @@ -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 + "]"; | ||||
| 	} | ||||
| } | ||||
| @@ -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); | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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 *")); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -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]; | ||||
| 			} | ||||
|   | ||||
| @@ -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) | ||||
| 		); | ||||
|   | ||||
| @@ -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) | ||||
| 		); | ||||
|   | ||||
| @@ -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) | ||||
| 		); | ||||
|   | ||||
| @@ -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) | ||||
| 			); | ||||
|   | ||||
| @@ -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) | ||||
| 		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.latestDeviceStatus.put( | ||||
| 			DEVICE_B, | ||||
| 			model.latestDeviceStatusRadios.put( | ||||
| 				device, | ||||
| 				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) | ||||
| 					.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) | ||||
| 		); | ||||
|   | ||||
| @@ -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 | ||||
| 		); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -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, | ||||
| @@ -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, | ||||
		Reference in New Issue
	
	Block a user