mirror of
				https://github.com/Telecominfraproject/wlan-cloud-rrm.git
				synced 2025-10-31 02:28:15 +00:00 
			
		
		
		
	Compare commits
	
		
			21 Commits
		
	
	
		
			v2.7.0-RC1
			...
			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 | 
| @@ -9,7 +9,7 @@ fullnameOverride: "" | |||||||
| images: | images: | ||||||
|   owrrm: |   owrrm: | ||||||
|     repository: tip-tip-wlan-cloud-ucentral.jfrog.io/owrrm |     repository: tip-tip-wlan-cloud-ucentral.jfrog.io/owrrm | ||||||
|     tag: v2.7.0-RC1 |     tag: main | ||||||
|     pullPolicy: Always |     pullPolicy: Always | ||||||
| #    regcred: | #    regcred: | ||||||
| #      registry: tip-tip-wlan-cloud-ucentral.jfrog.io | #      registry: tip-tip-wlan-cloud-ucentral.jfrog.io | ||||||
|   | |||||||
| @@ -478,8 +478,10 @@ components: | |||||||
|     RRMSchedule: |     RRMSchedule: | ||||||
|       type: object |       type: object | ||||||
|       properties: |       properties: | ||||||
|         cron: |         crons: | ||||||
|           type: string |           type: array | ||||||
|  |           items: | ||||||
|  |             type: string | ||||||
|         algorithms: |         algorithms: | ||||||
|           type: array |           type: array | ||||||
|           items: |           items: | ||||||
|   | |||||||
| @@ -143,6 +143,9 @@ public class RRMAlgorithm { | |||||||
| 	 * @param dryRun if set, do not apply changes | 	 * @param dryRun if set, do not apply changes | ||||||
| 	 * @param allowDefaultMode if false, "mode" argument must be present and | 	 * @param allowDefaultMode if false, "mode" argument must be present and | ||||||
| 	 *                         valid (returns error if invalid) | 	 *                         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 | 	 * @return the algorithm result, with exactly one field set ("error" upon | ||||||
| 	 *         failure, any others upon success) | 	 *         failure, any others upon success) | ||||||
| @@ -153,7 +156,8 @@ public class RRMAlgorithm { | |||||||
| 		Modeler modeler, | 		Modeler modeler, | ||||||
| 		String zone, | 		String zone, | ||||||
| 		boolean dryRun, | 		boolean dryRun, | ||||||
| 		boolean allowDefaultMode | 		boolean allowDefaultMode, | ||||||
|  | 		boolean updateImmediately | ||||||
| 	) { | 	) { | ||||||
| 		AlgorithmResult result = new AlgorithmResult(); | 		AlgorithmResult result = new AlgorithmResult(); | ||||||
| 		if (name == null || args == null) { | 		if (name == null || args == null) { | ||||||
| @@ -212,11 +216,14 @@ public class RRMAlgorithm { | |||||||
| 			} | 			} | ||||||
| 			result.channelMap = optimizer.computeChannelMap(); | 			result.channelMap = optimizer.computeChannelMap(); | ||||||
| 			if (!dryRun) { | 			if (!dryRun) { | ||||||
| 				optimizer.applyConfig( | 				optimizer.updateDeviceApConfig( | ||||||
| 					deviceDataManager, | 					deviceDataManager, | ||||||
| 					configManager, | 					configManager, | ||||||
| 					result.channelMap | 					result.channelMap | ||||||
| 				); | 				); | ||||||
|  | 				if (updateImmediately) { | ||||||
|  | 					configManager.queueZoneAndWakeUp(zone); | ||||||
|  | 				} | ||||||
| 			} | 			} | ||||||
| 		} else if ( | 		} else if ( | ||||||
| 			name.equals(RRMAlgorithm.AlgorithmType.OptimizeTxPower.name()) | 			name.equals(RRMAlgorithm.AlgorithmType.OptimizeTxPower.name()) | ||||||
| @@ -270,11 +277,14 @@ public class RRMAlgorithm { | |||||||
| 			} | 			} | ||||||
| 			result.txPowerMap = optimizer.computeTxPowerMap(); | 			result.txPowerMap = optimizer.computeTxPowerMap(); | ||||||
| 			if (!dryRun) { | 			if (!dryRun) { | ||||||
| 				optimizer.applyConfig( | 				optimizer.updateDeviceApConfig( | ||||||
| 					deviceDataManager, | 					deviceDataManager, | ||||||
| 					configManager, | 					configManager, | ||||||
| 					result.txPowerMap | 					result.txPowerMap | ||||||
| 				); | 				); | ||||||
|  | 				if (updateImmediately) { | ||||||
|  | 					configManager.queueZoneAndWakeUp(zone); | ||||||
|  | 				} | ||||||
| 			} | 			} | ||||||
| 		} else { | 		} else { | ||||||
| 			result.error = String.format("Unknown algorithm: '%s'", name); | 			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 | 			 * The main logic loop interval (i.e. sleep time), in ms | ||||||
| 			 * ({@code DATACOLLECTORPARAMS_UPDATEINTERVALMS}) | 			 * ({@code DATACOLLECTORPARAMS_UPDATEINTERVALMS}) | ||||||
| 			 */ | 			 */ | ||||||
| 			public int updateIntervalMs = 5000; | 			public int updateIntervalMs = 30000; // 30sec | ||||||
|  |  | ||||||
| 			/** | 			/** | ||||||
| 			 * The expected device statistics interval, in seconds (or -1 to | 			 * The expected device statistics interval, in seconds (or -1 to | ||||||
| @@ -246,13 +246,13 @@ public class RRMConfig { | |||||||
| 			 * automatic scans) | 			 * automatic scans) | ||||||
| 			 * ({@code DATACOLLECTORPARAMS_WIFISCANINTERVALSEC}) | 			 * ({@code DATACOLLECTORPARAMS_WIFISCANINTERVALSEC}) | ||||||
| 			 */ | 			 */ | ||||||
| 			public int wifiScanIntervalSec = 900; | 			public int wifiScanIntervalSec = 900; // 15min | ||||||
|  |  | ||||||
| 			/** | 			/** | ||||||
| 			 * The capabilities request interval (per device), in seconds | 			 * The capabilities request interval (per device), in seconds | ||||||
| 			 * ({@code DATACOLLECTORPARAMS_CAPABILITIESINTERVALSEC}) | 			 * ({@code DATACOLLECTORPARAMS_CAPABILITIESINTERVALSEC}) | ||||||
| 			 */ | 			 */ | ||||||
| 			public int capabilitiesIntervalSec = 3600; | 			public int capabilitiesIntervalSec = 3600; // 1hr | ||||||
|  |  | ||||||
| 			/** | 			/** | ||||||
| 			 * Number of executor threads for async tasks (ex. wifi scans) | 			 * 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 | 			 * The main logic loop interval (i.e. sleep time), in ms | ||||||
| 			 * ({@code CONFIGMANAGERPARAMS_UPDATEINTERVALMS}) | 			 * ({@code CONFIGMANAGERPARAMS_UPDATEINTERVALMS}) | ||||||
| 			 */ | 			 */ | ||||||
| 			public int updateIntervalMs = 60000; | 			public int updateIntervalMs = 120000; // 2min | ||||||
|  |  | ||||||
| 			/** | 			/** | ||||||
| 			 * Enable pushing device config changes? | 			 * Enable pushing device config changes? | ||||||
| @@ -363,7 +363,7 @@ public class RRMConfig { | |||||||
| 			 * Sync interval, in ms, for owprov venue information etc. | 			 * Sync interval, in ms, for owprov venue information etc. | ||||||
| 			 * ({@code PROVMONITORPARAMS_SYNCINTERVALMS}) | 			 * ({@code PROVMONITORPARAMS_SYNCINTERVALMS}) | ||||||
| 			 */ | 			 */ | ||||||
| 			public int syncIntervalMs = 300000; | 			public int syncIntervalMs = 300000; // 5min | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		/** ProvMonitor parameters. */ | 		/** ProvMonitor parameters. */ | ||||||
|   | |||||||
| @@ -19,9 +19,9 @@ public class RRMSchedule { | |||||||
| 	 * | 	 * | ||||||
| 	 * This field expects a cron-like format as defined by the Quartz Job | 	 * This field expects a cron-like format as defined by the Quartz Job | ||||||
| 	 * Scheduler (CronTrigger): | 	 * 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. | 	 * 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 | 	 * @return true if token is valid | ||||||
| 	 */ | 	 */ | ||||||
| 	private boolean validateOpenWifiToken(String token) { | 	private boolean validateOpenWifiToken(String token) { | ||||||
| 		// The below only checks /api/v1/validateToken and caches it as necessary. | 		// 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); | 		Long expiry = tokenCache.get(token); | ||||||
| 		if (expiry == null) { | 		if (expiry == null) { | ||||||
| 			TokenValidationResult result = client.validateToken(token); | 			TokenValidationResult result = client.validateToken(token); | ||||||
| @@ -711,7 +712,8 @@ public class ApiServer implements Runnable { | |||||||
| 				modeler, | 				modeler, | ||||||
| 				venue, | 				venue, | ||||||
| 				mock, | 				mock, | ||||||
| 				true /* allowDefaultMode */ | 				true, /* allowDefaultMode */ | ||||||
|  | 				true /* updateImmediately */ | ||||||
| 			); | 			); | ||||||
| 			if (result.error != null) { | 			if (result.error != null) { | ||||||
| 				response.status(400); | 				response.status(400); | ||||||
| @@ -917,7 +919,7 @@ public class ApiServer implements Runnable { | |||||||
| 				DeviceConfig networkConfig = | 				DeviceConfig networkConfig = | ||||||
| 					gson.fromJson(request.body(), DeviceConfig.class); | 					gson.fromJson(request.body(), DeviceConfig.class); | ||||||
| 				deviceDataManager.setDeviceNetworkConfig(networkConfig); | 				deviceDataManager.setDeviceNetworkConfig(networkConfig); | ||||||
| 				configManager.wakeUp(); | 				configManager.queueAllZonesAndWakeUp(); | ||||||
|  |  | ||||||
| 				// Revalidate data model | 				// Revalidate data model | ||||||
| 				modeler.revalidate(); | 				modeler.revalidate(); | ||||||
| @@ -981,7 +983,7 @@ public class ApiServer implements Runnable { | |||||||
| 				DeviceConfig zoneConfig = | 				DeviceConfig zoneConfig = | ||||||
| 					gson.fromJson(request.body(), DeviceConfig.class); | 					gson.fromJson(request.body(), DeviceConfig.class); | ||||||
| 				deviceDataManager.setDeviceZoneConfig(zone, zoneConfig); | 				deviceDataManager.setDeviceZoneConfig(zone, zoneConfig); | ||||||
| 				configManager.wakeUp(); | 				configManager.queueZoneAndWakeUp(zone); | ||||||
|  |  | ||||||
| 				// Revalidate data model | 				// Revalidate data model | ||||||
| 				modeler.revalidate(); | 				modeler.revalidate(); | ||||||
| @@ -1044,7 +1046,10 @@ public class ApiServer implements Runnable { | |||||||
| 				DeviceConfig apConfig = | 				DeviceConfig apConfig = | ||||||
| 					gson.fromJson(request.body(), DeviceConfig.class); | 					gson.fromJson(request.body(), DeviceConfig.class); | ||||||
| 				deviceDataManager.setDeviceApConfig(serialNumber, apConfig); | 				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 | 				// Revalidate data model | ||||||
| 				modeler.revalidate(); | 				modeler.revalidate(); | ||||||
| @@ -1117,7 +1122,10 @@ public class ApiServer implements Runnable { | |||||||
| 						.computeIfAbsent(serialNumber, k -> new DeviceConfig()) | 						.computeIfAbsent(serialNumber, k -> new DeviceConfig()) | ||||||
| 						.apply(apConfig); | 						.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 | 				// Revalidate data model | ||||||
| 				modeler.revalidate(); | 				modeler.revalidate(); | ||||||
| @@ -1260,7 +1268,8 @@ public class ApiServer implements Runnable { | |||||||
| 				modeler, | 				modeler, | ||||||
| 				zone, | 				zone, | ||||||
| 				dryRun, | 				dryRun, | ||||||
| 				false /* allowDefaultMode */ | 				false, /* allowDefaultMode */ | ||||||
|  | 				true /* updateImmediately */ | ||||||
| 			); | 			); | ||||||
| 			if (result.error != null) { | 			if (result.error != null) { | ||||||
| 				response.status(400); | 				response.status(400); | ||||||
| @@ -1371,7 +1380,8 @@ public class ApiServer implements Runnable { | |||||||
| 				modeler, | 				modeler, | ||||||
| 				zone, | 				zone, | ||||||
| 				dryRun, | 				dryRun, | ||||||
| 				false /* allowDefaultMode */ | 				false, /* allowDefaultMode */ | ||||||
|  | 				true /* updateImmediately */ | ||||||
| 			); | 			); | ||||||
| 			if (result.error != null) { | 			if (result.error != null) { | ||||||
| 				response.status(400); | 				response.status(400); | ||||||
|   | |||||||
| @@ -10,9 +10,12 @@ package com.facebook.openwifirrm.modules; | |||||||
|  |  | ||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
| import java.util.HashMap; | import java.util.HashMap; | ||||||
|  | import java.util.HashSet; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.Map; | import java.util.Map; | ||||||
|  | import java.util.Set; | ||||||
| import java.util.TreeMap; | import java.util.TreeMap; | ||||||
|  | import java.util.concurrent.ConcurrentHashMap; | ||||||
| import java.util.concurrent.atomic.AtomicBoolean; | import java.util.concurrent.atomic.AtomicBoolean; | ||||||
|  |  | ||||||
| import org.slf4j.Logger; | import org.slf4j.Logger; | ||||||
| @@ -63,8 +66,11 @@ public class ConfigManager implements Runnable { | |||||||
| 	/** Is the main thread sleeping? */ | 	/** Is the main thread sleeping? */ | ||||||
| 	private final AtomicBoolean sleepingFlag = new AtomicBoolean(false); | 	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. */ | 	/** Config listener interface. */ | ||||||
| 	public interface ConfigListener { | 	public interface ConfigListener { | ||||||
| @@ -165,6 +171,7 @@ public class ConfigManager implements Runnable { | |||||||
| 				return; | 				return; | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  | 		client.refreshAccessToken(); | ||||||
|  |  | ||||||
| 		// Fetch device list | 		// Fetch device list | ||||||
| 		List<DeviceWithStatus> devices = client.getDevices(); | 		List<DeviceWithStatus> devices = client.getDevices(); | ||||||
| @@ -180,7 +187,10 @@ public class ConfigManager implements Runnable { | |||||||
| 		List<String> devicesNeedingUpdate = new ArrayList<>(); | 		List<String> devicesNeedingUpdate = new ArrayList<>(); | ||||||
| 		final long CONFIG_DEBOUNCE_INTERVAL_NS = | 		final long CONFIG_DEBOUNCE_INTERVAL_NS = | ||||||
| 			params.configDebounceIntervalSec * 1_000_000_000L; | 			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) { | 		for (DeviceWithStatus device : devices) { | ||||||
| 			// Update config structure | 			// Update config structure | ||||||
| 			DeviceData data = deviceDataMap.computeIfAbsent( | 			DeviceData data = deviceDataMap.computeIfAbsent( | ||||||
| @@ -201,11 +211,13 @@ public class ConfigManager implements Runnable { | |||||||
| 			for (ConfigListener listener : configListeners.values()) { | 			for (ConfigListener listener : configListeners.values()) { | ||||||
| 				listener.receiveDeviceConfig(device.serialNumber, data.config); | 				listener.receiveDeviceConfig(device.serialNumber, data.config); | ||||||
| 			} | 			} | ||||||
|  | 			// Check if there are requested updates for this zone | ||||||
| 			// Check event flag | 			String deviceZone = | ||||||
|  | 				deviceDataManager.getDeviceZone(device.serialNumber); | ||||||
|  | 			boolean isEvent = zonesToUpdateCopy.contains(deviceZone); | ||||||
| 			if (params.configOnEventOnly && !isEvent) { | 			if (params.configOnEventOnly && !isEvent) { | ||||||
| 				logger.debug( | 				logger.debug( | ||||||
| 					"Skipping config for {} (event flag not set)", | 					"Skipping config for {} (zone not marked for updates)", | ||||||
| 					device.serialNumber | 					device.serialNumber | ||||||
| 				); | 				); | ||||||
| 				continue; | 				continue; | ||||||
| @@ -251,15 +263,16 @@ public class ConfigManager implements Runnable { | |||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		final boolean shouldUpdate = !zonesToUpdateCopy.isEmpty(); | ||||||
| 		// Send config changes to devices | 		// Send config changes to devices | ||||||
| 		if (!params.configEnabled) { | 		if (!params.configEnabled) { | ||||||
| 			logger.trace("Config changes are disabled."); | 			logger.trace("Config changes are disabled."); | ||||||
| 		} else if (devicesNeedingUpdate.isEmpty()) { | 		} else if (devicesNeedingUpdate.isEmpty()) { | ||||||
| 			logger.debug("No device configs to send."); | 			logger.debug("No device configs to send."); | ||||||
| 		} else if (params.configOnEventOnly && !isEvent) { | 		} else if (params.configOnEventOnly && !shouldUpdate) { | ||||||
| 			// shouldn't happen | 			// shouldn't happen | ||||||
| 			logger.error( | 			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() | 				devicesNeedingUpdate.size() | ||||||
| 			); | 			); | ||||||
| 		} else { | 		} else { | ||||||
| @@ -364,9 +377,38 @@ public class ConfigManager implements Runnable { | |||||||
| 		return (configListeners.remove(id) != null); | 		return (configListeners.remove(id) != null); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	/** Interrupt the main thread, possibly triggering an update immediately. */ | 	/** | ||||||
| 	public void wakeUp() { | 	 * Mark the zone to be updated, then interrupt the main thread to possibly | ||||||
| 		eventFlag.set(true); | 	 * 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()) { | 		if (mainThread != null && mainThread.isAlive() && sleepingFlag.get()) { | ||||||
| 			wakeupFlag.set(true); | 			wakeupFlag.set(true); | ||||||
| 			mainThread.interrupt(); | 			mainThread.interrupt(); | ||||||
|   | |||||||
| @@ -218,6 +218,7 @@ public class DataCollector implements Runnable { | |||||||
| 				return; | 				return; | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  | 		client.refreshAccessToken(); | ||||||
|  |  | ||||||
| 		// Fetch device list | 		// Fetch device list | ||||||
| 		List<DeviceWithStatus> devices = client.getDevices(); | 		List<DeviceWithStatus> devices = client.getDevices(); | ||||||
|   | |||||||
| @@ -97,7 +97,7 @@ public class Modeler implements Runnable { | |||||||
| 		public Map<String, State> latestState = new ConcurrentHashMap<>(); | 		public Map<String, State> latestState = new ConcurrentHashMap<>(); | ||||||
|  |  | ||||||
| 		/** List of radio info per device. */ | 		/** List of radio info per device. */ | ||||||
| 		public Map<String, JsonArray> latestDeviceStatus = | 		public Map<String, JsonArray> latestDeviceStatusRadios = | ||||||
| 			new ConcurrentHashMap<>(); | 			new ConcurrentHashMap<>(); | ||||||
|  |  | ||||||
| 		/** List of capabilities per device. */ | 		/** List of capabilities per device. */ | ||||||
| @@ -238,6 +238,7 @@ public class Modeler implements Runnable { | |||||||
| 				return; | 				return; | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  | 		client.refreshAccessToken(); | ||||||
|  |  | ||||||
| 		// TODO: backfill data from database? | 		// 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 | 		// Get old vs new radios info and store the new radios info | ||||||
| 		JsonArray newRadioList = config.getRadioConfigList(); | 		JsonArray newRadioList = config.getRadioConfigList(); | ||||||
| 		Set<String> newRadioBandsSet = config.getRadioBandsSet(newRadioList); | 		Set<String> newRadioBandsSet = config.getRadioBandsSet(newRadioList); | ||||||
| 		JsonArray oldRadioList = dataModel.latestDeviceStatus | 		JsonArray oldRadioList = dataModel.latestDeviceStatusRadios | ||||||
| 			.put(serialNumber, newRadioList); | 			.put(serialNumber, newRadioList); | ||||||
| 		Set<String> oldRadioBandsSet = config.getRadioBandsSet(oldRadioList); | 		Set<String> oldRadioBandsSet = config.getRadioBandsSet(oldRadioList); | ||||||
|  |  | ||||||
| @@ -429,7 +430,7 @@ public class Modeler implements Runnable { | |||||||
| 			logger.debug("Removed some state entries from data model"); | 			logger.debug("Removed some state entries from data model"); | ||||||
| 		} | 		} | ||||||
| 		if ( | 		if ( | ||||||
| 			dataModel.latestDeviceStatus.entrySet() | 			dataModel.latestDeviceStatusRadios.entrySet() | ||||||
| 				.removeIf(e -> !isRRMEnabled(e.getKey())) | 				.removeIf(e -> !isRRMEnabled(e.getKey())) | ||||||
| 		) { | 		) { | ||||||
| 			logger.debug("Removed some status entries from data model"); | 			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.aggregators.MeanAggregator; | ||||||
| import com.facebook.openwifirrm.modules.Modeler.DataModel; | import com.facebook.openwifirrm.modules.Modeler.DataModel; | ||||||
| import com.facebook.openwifirrm.ucentral.WifiScanEntry; | import com.facebook.openwifirrm.ucentral.WifiScanEntry; | ||||||
| import com.facebook.openwifirrm.ucentral.operationelement.HTOperationElement; | import com.facebook.openwifirrm.ucentral.informationelement.HTOperation; | ||||||
| import com.facebook.openwifirrm.ucentral.operationelement.VHTOperationElement; | import com.facebook.openwifirrm.ucentral.informationelement.VHTOperation; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Modeler utilities. |  * Modeler utilities. | ||||||
| @@ -239,9 +239,9 @@ public class ModelerUtils { | |||||||
| 		return Objects.equals(entry1.bssid, entry2.bssid) && | 		return Objects.equals(entry1.bssid, entry2.bssid) && | ||||||
| 			entry1.frequency == entry2.frequency && | 			entry1.frequency == entry2.frequency && | ||||||
| 			entry1.channel == entry2.channel && | 			entry1.channel == entry2.channel && | ||||||
| 			HTOperationElement | 			HTOperation | ||||||
| 				.matchesHtForAggregation(entry1.ht_oper, entry2.ht_oper) && | 				.matchesHtForAggregation(entry1.ht_oper, entry2.ht_oper) && | ||||||
| 			VHTOperationElement | 			VHTOperation | ||||||
| 				.matchesVhtForAggregation(entry1.vht_oper, entry2.vht_oper); | 				.matchesVhtForAggregation(entry1.vht_oper, entry2.vht_oper); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ | |||||||
|  |  | ||||||
| package com.facebook.openwifirrm.modules; | package com.facebook.openwifirrm.modules; | ||||||
|  |  | ||||||
|  | import java.util.Arrays; | ||||||
| import java.util.HashMap; | import java.util.HashMap; | ||||||
| import java.util.Map; | import java.util.Map; | ||||||
| import java.util.Set; | import java.util.Set; | ||||||
| @@ -102,6 +103,7 @@ public class ProvMonitor implements Runnable { | |||||||
| 				return; | 				return; | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  | 		client.refreshAccessToken(); | ||||||
|  |  | ||||||
| 		// Fetch data from owprov | 		// Fetch data from owprov | ||||||
| 		// TODO: this may change later - for now, we only fetch inventory and | 		// TODO: this may change later - for now, we only fetch inventory and | ||||||
| @@ -159,12 +161,21 @@ public class ProvMonitor implements Runnable { | |||||||
| 			return null; | 			return null; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		RRMSchedule schedule = new RRMSchedule(); | 		String[] crons = RRMScheduler | ||||||
| 		schedule.cron = RRMScheduler |  | ||||||
| 			.parseIntoQuartzCron(details.rrm.schedule); | 			.parseIntoQuartzCron(details.rrm.schedule); | ||||||
| 		if (schedule.cron == null || schedule.cron.isEmpty()) { | 		if (crons == null || crons.length == 0) { | ||||||
| 			return null; | 			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) { | 		if (details.rrm.algorithms != null) { | ||||||
| 			schedule.algorithms = | 			schedule.algorithms = | ||||||
| @@ -175,6 +186,7 @@ public class ProvMonitor implements Runnable { | |||||||
| 					) | 					) | ||||||
| 					.collect(Collectors.toList()); | 					.collect(Collectors.toList()); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		return schedule; | 		return schedule; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -8,15 +8,15 @@ | |||||||
|  |  | ||||||
| package com.facebook.openwifirrm.modules; | package com.facebook.openwifirrm.modules; | ||||||
|  |  | ||||||
|  | import java.text.ParseException; | ||||||
| import java.util.Arrays; | import java.util.Arrays; | ||||||
| import java.util.HashSet; | import java.util.HashSet; | ||||||
| import java.util.Properties; | import java.util.Properties; | ||||||
| import java.util.Set; | import java.util.Set; | ||||||
| import java.util.concurrent.ConcurrentHashMap; | import java.util.concurrent.ConcurrentHashMap; | ||||||
| import java.text.ParseException; |  | ||||||
|  |  | ||||||
| import org.quartz.CronScheduleBuilder; |  | ||||||
| import org.quartz.CronExpression; | import org.quartz.CronExpression; | ||||||
|  | import org.quartz.CronScheduleBuilder; | ||||||
| import org.quartz.Job; | import org.quartz.Job; | ||||||
| import org.quartz.JobBuilder; | import org.quartz.JobBuilder; | ||||||
| import org.quartz.JobDetail; | import org.quartz.JobDetail; | ||||||
| @@ -35,6 +35,7 @@ import org.slf4j.LoggerFactory; | |||||||
| import com.facebook.openwifirrm.DeviceConfig; | import com.facebook.openwifirrm.DeviceConfig; | ||||||
| import com.facebook.openwifirrm.DeviceDataManager; | import com.facebook.openwifirrm.DeviceDataManager; | ||||||
| import com.facebook.openwifirrm.RRMAlgorithm; | import com.facebook.openwifirrm.RRMAlgorithm; | ||||||
|  | import com.facebook.openwifirrm.RRMSchedule; | ||||||
| import com.facebook.openwifirrm.RRMConfig.ModuleConfig.RRMSchedulerParams; | import com.facebook.openwifirrm.RRMConfig.ModuleConfig.RRMSchedulerParams; | ||||||
| import com.google.gson.Gson; | import com.google.gson.Gson; | ||||||
| import com.google.gson.GsonBuilder; | import com.google.gson.GsonBuilder; | ||||||
| @@ -74,15 +75,21 @@ public class RRMScheduler { | |||||||
| 	/** The scheduler instance. */ | 	/** The scheduler instance. */ | ||||||
| 	private Scheduler scheduler; | 	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. */ | 	/** RRM job. */ | ||||||
| 	public static class RRMJob implements Job { | 	public static class RRMJob implements Job { | ||||||
| 		@Override | 		@Override | ||||||
| 		public void execute(JobExecutionContext context) | 		public void execute(JobExecutionContext context) | ||||||
| 			throws JobExecutionException { | 			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); | 			logger.debug("Executing job for zone: {}", zone); | ||||||
| 			try { | 			try { | ||||||
| 				SchedulerContext schedulerContext = | 				SchedulerContext schedulerContext = | ||||||
| @@ -107,13 +114,14 @@ public class RRMScheduler { | |||||||
| 	 * @param linuxCron Linux cron with seconds | 	 * @param linuxCron Linux cron with seconds | ||||||
| 	 *        (seconds minutes hours day_of_month month day_of_week [year]) | 	 *        (seconds minutes hours day_of_month month day_of_week [year]) | ||||||
| 	 * | 	 * | ||||||
| 	 * @throws IllegalArgumentException when a linux cron cannot be parsed | 	 * @throws IllegalArgumentException when a linux cron cannot be parsed into a | ||||||
| 	 *         into a valid Quartz spec | 	 *         valid Quartz spec | ||||||
| 	 * @return String a Quartz supported cron | 	 * @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)) { | 		if (CronExpression.isValidExpression(linuxCron)) { | ||||||
| 			return linuxCron; | 			return new String[] { linuxCron }; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		String[] split = linuxCron.split(" "); | 		String[] split = linuxCron.split(" "); | ||||||
| @@ -144,15 +152,36 @@ public class RRMScheduler { | |||||||
| 			// if first case failed and only day of week is *, set to ? | 			// if first case failed and only day of week is *, set to ? | ||||||
| 			split[DAY_OF_WEEK_INDEX] = "?"; | 			split[DAY_OF_WEEK_INDEX] = "?"; | ||||||
| 		} else { | 		} 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 | ||||||
| 			return null; | 			// 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); | 		String quartzCron = String.join(" ", split); | ||||||
| 		if (!CronExpression.isValidExpression(quartzCron)) { | 		if (!CronExpression.isValidExpression(quartzCron)) { | ||||||
| 			return null; | 			return null; | ||||||
| 		} | 		} | ||||||
| 		return quartzCron; |  | ||||||
|  | 		return new String[] { quartzCron }; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	/** Constructor. */ | 	/** Constructor. */ | ||||||
| @@ -194,7 +223,7 @@ public class RRMScheduler { | |||||||
| 			// Schedule job and triggers | 			// Schedule job and triggers | ||||||
| 			scheduler.addJob(job, false); | 			scheduler.addJob(job, false); | ||||||
| 			syncTriggers(); | 			syncTriggers(); | ||||||
| 			logger.info("Scheduled {} RRM trigger(s)", scheduledZones.size()); | 			logger.info("Scheduled {} RRM trigger(s)", scheduledJobKeys.size()); | ||||||
|  |  | ||||||
| 			// Start scheduler | 			// Start scheduler | ||||||
| 			scheduler.start(); | 			scheduler.start(); | ||||||
| @@ -218,85 +247,98 @@ public class RRMScheduler { | |||||||
|  |  | ||||||
| 	/** | 	/** | ||||||
| 	 * Synchronize triggers to the current topology, adding/updating/deleting | 	 * 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() { | 	public void syncTriggers() { | ||||||
| 		Set<String> scheduled = ConcurrentHashMap.newKeySet(); | 		Set<String> scheduled = ConcurrentHashMap.newKeySet(); | ||||||
| 		Set<String> prevScheduled = new HashSet<>(); | 		Set<String> prevScheduled = new HashSet<>(); | ||||||
| 		if (scheduledZones != null) { | 		if (scheduledJobKeys != null) { | ||||||
| 			prevScheduled.addAll(scheduledZones); | 			prevScheduled.addAll(scheduledJobKeys); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Add new triggers | 		// Add new triggers | ||||||
| 		for (String zone : deviceDataManager.getZones()) { | 		for (String zone : deviceDataManager.getZones()) { | ||||||
| 			DeviceConfig config = deviceDataManager.getZoneConfig(zone); | 			DeviceConfig config = deviceDataManager.getZoneConfig(zone); | ||||||
|  | 			RRMSchedule schedule = config.schedule; | ||||||
| 			if ( | 			if ( | ||||||
| 				config.schedule == null || | 				schedule == null || schedule.crons == null || | ||||||
| 					config.schedule.cron == null || | 					schedule.crons.isEmpty() | ||||||
| 					config.schedule.cron.isEmpty() |  | ||||||
| 			) { | 			) { | ||||||
| 				continue; // RRM not scheduled | 				continue; // RRM not scheduled | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			try { | 			for (int i = 0; i < schedule.crons.size(); i++) { | ||||||
| 				CronExpression.validateExpression(config.schedule.cron); | 				String cron = schedule.crons.get(i); | ||||||
| 			} catch (ParseException e) { | 				// if even one schedule has invalid cron, the whole thing is probably wrong | ||||||
| 				logger.error( | 				if (cron == null || cron.isEmpty()) { | ||||||
| 					String.format( | 					logger.error("There was an invalid cron in the schedule"); | ||||||
| 						"Invalid cron expression (%s) for zone %s", | 					break; | ||||||
| 						config.schedule.cron, | 				} | ||||||
| 						zone |  | ||||||
| 					), | 				try { | ||||||
| 					e | 					CronExpression.validateExpression(cron); | ||||||
|  | 				} catch (ParseException e) { | ||||||
|  | 					logger.error( | ||||||
|  | 						String.format( | ||||||
|  | 							"Invalid cron expression (%s) for zone %s", | ||||||
|  | 							cron, | ||||||
|  | 							zone | ||||||
|  | 						), | ||||||
|  | 						e | ||||||
|  | 					); | ||||||
|  | 					continue; | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				// Create trigger | ||||||
|  | 				String jobKey = String.format("%s:%d", zone, i); | ||||||
|  | 				Trigger trigger = TriggerBuilder.newTrigger() | ||||||
|  | 					.withIdentity(jobKey) | ||||||
|  | 					.forJob(job) | ||||||
|  | 					.withSchedule( | ||||||
|  | 						CronScheduleBuilder.cronSchedule(cron) | ||||||
|  | 					) | ||||||
|  | 					.build(); | ||||||
|  |  | ||||||
|  | 				try { | ||||||
|  | 					if (!prevScheduled.contains(jobKey)) { | ||||||
|  | 						scheduler.scheduleJob(trigger); | ||||||
|  | 					} else { | ||||||
|  | 						scheduler.rescheduleJob(trigger.getKey(), trigger); | ||||||
|  | 					} | ||||||
|  | 				} catch (SchedulerException e) { | ||||||
|  | 					logger.error( | ||||||
|  | 						"Failed to schedule RRM trigger for job key: " + jobKey, | ||||||
|  | 						e | ||||||
|  | 					); | ||||||
|  | 					continue; | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				scheduled.add(jobKey); | ||||||
|  | 				logger.debug( | ||||||
|  | 					"Scheduled/updated RRM for job key '{}' at: < {} >", | ||||||
|  | 					jobKey, | ||||||
|  | 					cron | ||||||
| 				); | 				); | ||||||
| 				continue; |  | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			// Create trigger |  | ||||||
| 			Trigger trigger = TriggerBuilder.newTrigger() |  | ||||||
| 				.withIdentity(zone) |  | ||||||
| 				.forJob(job) |  | ||||||
| 				.withSchedule( |  | ||||||
| 					CronScheduleBuilder.cronSchedule(config.schedule.cron) |  | ||||||
| 				) |  | ||||||
| 				.build(); |  | ||||||
| 			try { |  | ||||||
| 				if (!prevScheduled.contains(zone)) { |  | ||||||
| 					scheduler.scheduleJob(trigger); |  | ||||||
| 				} else { |  | ||||||
| 					scheduler.rescheduleJob(trigger.getKey(), trigger); |  | ||||||
| 				} |  | ||||||
| 			} catch (SchedulerException e) { |  | ||||||
| 				logger.error( |  | ||||||
| 					"Failed to schedule RRM trigger for zone: " + zone, |  | ||||||
| 					e |  | ||||||
| 				); |  | ||||||
| 				continue; |  | ||||||
| 			} |  | ||||||
| 			scheduled.add(zone); |  | ||||||
| 			logger.debug( |  | ||||||
| 				"Scheduled/updated RRM for zone '{}' at: < {} >", |  | ||||||
| 				zone, |  | ||||||
| 				config.schedule.cron |  | ||||||
| 			); |  | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Remove old triggers | 		// Remove old triggers | ||||||
| 		prevScheduled.removeAll(scheduled); | 		prevScheduled.removeAll(scheduled); | ||||||
| 		for (String zone : prevScheduled) { | 		for (String jobKey : prevScheduled) { | ||||||
| 			try { | 			try { | ||||||
| 				scheduler.unscheduleJob(TriggerKey.triggerKey(zone)); | 				scheduler.unscheduleJob(TriggerKey.triggerKey(jobKey)); | ||||||
| 			} catch (SchedulerException e) { | 			} catch (SchedulerException e) { | ||||||
| 				logger.error( | 				logger.error( | ||||||
| 					"Failed to remove RRM trigger for zone: " + zone, | 					"Failed to remove RRM trigger for jobKey: " + jobKey, | ||||||
| 					e | 					e | ||||||
| 				); | 				); | ||||||
| 				continue; | 				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. */ | 	/** Run RRM algorithms for the given zone. */ | ||||||
| @@ -305,16 +347,19 @@ public class RRMScheduler { | |||||||
|  |  | ||||||
| 		// Get algorithms from zone config | 		// Get algorithms from zone config | ||||||
| 		DeviceConfig config = deviceDataManager.getZoneConfig(zone); | 		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); | 			logger.error("RRM schedule missing for zone '{}', aborting!", zone); | ||||||
| 			return; | 			return; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if ( | 		if ( | ||||||
| 			config.schedule.algorithms == null || | 			schedule.algorithms == null || | ||||||
| 				config.schedule.algorithms.isEmpty() | 				schedule.algorithms.isEmpty() | ||||||
| 		) { | 		) { | ||||||
| 			logger.debug("Using default RRM algorithms for zone '{}'", zone); | 			logger | ||||||
| 			config.schedule.algorithms = Arrays.asList( | 				.debug("Using default RRM algorithms for zone '{}'", zone); | ||||||
|  | 			schedule.algorithms = Arrays.asList( | ||||||
| 				new RRMAlgorithm( | 				new RRMAlgorithm( | ||||||
| 					RRMAlgorithm.AlgorithmType.OptimizeChannel.name() | 					RRMAlgorithm.AlgorithmType.OptimizeChannel.name() | ||||||
| 				), | 				), | ||||||
| @@ -325,14 +370,15 @@ public class RRMScheduler { | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Execute algorithms | 		// Execute algorithms | ||||||
| 		for (RRMAlgorithm algo : config.schedule.algorithms) { | 		for (RRMAlgorithm algo : schedule.algorithms) { | ||||||
| 			RRMAlgorithm.AlgorithmResult result = algo.run( | 			RRMAlgorithm.AlgorithmResult result = algo.run( | ||||||
| 				deviceDataManager, | 				deviceDataManager, | ||||||
| 				configManager, | 				configManager, | ||||||
| 				modeler, | 				modeler, | ||||||
| 				zone, | 				zone, | ||||||
| 				params.dryRun, | 				params.dryRun, | ||||||
| 				true /* allowDefaultMode */ | 				true, /* allowDefaultMode */ | ||||||
|  | 				false /* updateImmediately */ | ||||||
| 			); | 			); | ||||||
| 			logger.info( | 			logger.info( | ||||||
| 				"'{}' result for zone '{}': {}", | 				"'{}' result for zone '{}': {}", | ||||||
| @@ -341,5 +387,6 @@ public class RRMScheduler { | |||||||
| 				gson.toJson(result) | 				gson.toJson(result) | ||||||
| 			); | 			); | ||||||
| 		} | 		} | ||||||
|  | 		configManager.queueZoneAndWakeUp(zone); | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -360,7 +360,7 @@ public class DatabaseManager { | |||||||
| 	/** Convert a list of state records to a State object. */ | 	/** Convert a list of state records to a State object. */ | ||||||
| 	private State toState(List<StateRecord> records, long ts) { | 	private State toState(List<StateRecord> records, long ts) { | ||||||
| 		State state = new State(); | 		State state = new State(); | ||||||
| 		state.unit = state.new Unit(); | 		state.unit = new State.Unit(); | ||||||
| 		state.unit.localtime = ts; | 		state.unit.localtime = ts; | ||||||
|  |  | ||||||
| 		// Parse each record | 		// Parse each record | ||||||
| @@ -454,9 +454,10 @@ public class DatabaseManager { | |||||||
| 			.map(o -> gson.fromJson(o, State.Interface.class)) | 			.map(o -> gson.fromJson(o, State.Interface.class)) | ||||||
| 			.collect(Collectors.toList()) | 			.collect(Collectors.toList()) | ||||||
| 			.toArray(new State.Interface[0]); | 			.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()) { | 		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; | 		return state; | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -25,9 +25,9 @@ import com.facebook.openwifirrm.modules.Modeler.DataModel; | |||||||
| import com.facebook.openwifirrm.ucentral.UCentralConstants; | import com.facebook.openwifirrm.ucentral.UCentralConstants; | ||||||
| import com.facebook.openwifirrm.ucentral.UCentralUtils; | import com.facebook.openwifirrm.ucentral.UCentralUtils; | ||||||
| import com.facebook.openwifirrm.ucentral.WifiScanEntry; | 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.models.State; | ||||||
| import com.facebook.openwifirrm.ucentral.operationelement.HTOperationElement; |  | ||||||
| import com.facebook.openwifirrm.ucentral.operationelement.VHTOperationElement; |  | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Channel optimizer base class. |  * Channel optimizer base class. | ||||||
| @@ -156,7 +156,7 @@ public abstract class ChannelOptimizer { | |||||||
| 			.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber)); | 			.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber)); | ||||||
| 		this.model.latestState.keySet() | 		this.model.latestState.keySet() | ||||||
| 			.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber)); | 			.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber)); | ||||||
| 		this.model.latestDeviceStatus.keySet() | 		this.model.latestDeviceStatusRadios.keySet() | ||||||
| 			.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber)); | 			.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber)); | ||||||
| 		this.model.latestDeviceCapabilities.keySet() | 		this.model.latestDeviceCapabilities.keySet() | ||||||
| 			.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber)); | 			.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber)); | ||||||
| @@ -208,13 +208,13 @@ public abstract class ChannelOptimizer { | |||||||
| 			return MIN_CHANNEL_WIDTH; | 			return MIN_CHANNEL_WIDTH; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		HTOperationElement htOperObj = new HTOperationElement(htOper); | 		HTOperation htOperObj = new HTOperation(htOper); | ||||||
| 		if (vhtOper == null) { | 		if (vhtOper == null) { | ||||||
| 			// HT mode only supports 20/40 MHz | 			// HT mode only supports 20/40 MHz | ||||||
| 			return htOperObj.staChannelWidth ? 40 : 20; | 			return htOperObj.staChannelWidth ? 40 : 20; | ||||||
| 		} else { | 		} else { | ||||||
| 			// VHT/HE mode supports 20/40/160/80+80 MHz | 			// 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) { | 			if (!htOperObj.staChannelWidth && vhtOperObj.channelWidth == 0) { | ||||||
| 				return 20; | 				return 20; | ||||||
| 			} else if ( | 			} else if ( | ||||||
| @@ -234,8 +234,9 @@ public abstract class ChannelOptimizer { | |||||||
| 				// the difference of 8 means it is consecutive | 				// the difference of 8 means it is consecutive | ||||||
| 				int channelDiff = | 				int channelDiff = | ||||||
| 					Math.abs(vhtOperObj.channel1 - vhtOperObj.channel2); | 					Math.abs(vhtOperObj.channel1 - vhtOperObj.channel2); | ||||||
| 				// the "8080" below does not mean 8080 MHz wide, it refers to 80+80 MHz channel | 				// TODO it will currently return just 80 for 80p80 - it should be dealt | ||||||
| 				return channelDiff == 8 ? 160 : 8080; | 				// with properly. | ||||||
|  | 				return channelDiff == 8 ? 160 : 80; | ||||||
| 			} else { | 			} else { | ||||||
| 				return MIN_CHANNEL_WIDTH; | 				return MIN_CHANNEL_WIDTH; | ||||||
| 			} | 			} | ||||||
| @@ -378,15 +379,26 @@ public abstract class ChannelOptimizer { | |||||||
| 			radioIndex < state.radios.length; | 			radioIndex < state.radios.length; | ||||||
| 			radioIndex++ | 			radioIndex++ | ||||||
| 		) { | 		) { | ||||||
| 			int tempChannel = state.radios[radioIndex] | 			int tempChannel = state.radios[radioIndex].channel; | ||||||
| 				.get("channel") |  | ||||||
| 				.getAsInt(); |  | ||||||
| 			if (UCentralUtils.isChannelInBand(tempChannel, band)) { | 			if (UCentralUtils.isChannelInBand(tempChannel, band)) { | ||||||
| 				currentChannel = tempChannel; | 				currentChannel = tempChannel; | ||||||
| 				currentChannelWidth = state.radios[radioIndex] | 				// treat as two separate 80MHz channel and only assign to one | ||||||
| 					.get("channel_width") | 				// TODO: support 80p80 properly | ||||||
| 					.getAsInt(); | 				Integer parsedChannelWidth = UCentralUtils | ||||||
| 				break; | 					.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 }; | 		return new int[] { currentChannel, currentChannelWidth }; | ||||||
| @@ -627,14 +639,13 @@ public abstract class ChannelOptimizer { | |||||||
| 	public abstract Map<String, Map<String, Integer>> computeChannelMap(); | 	public abstract Map<String, Map<String, Integer>> computeChannelMap(); | ||||||
|  |  | ||||||
| 	/** | 	/** | ||||||
| 	 * Program the given channel map into the AP config and notify the config | 	 * Program the given channel map into the AP config. | ||||||
| 	 * manager. |  | ||||||
| 	 * | 	 * | ||||||
| 	 * @param deviceDataManager the DeviceDataManager instance | 	 * @param deviceDataManager the DeviceDataManager instance | ||||||
| 	 * @param configManager the ConfigManager instance | 	 * @param configManager the ConfigManager instance | ||||||
| 	 * @param channelMap the map of devices (by serial number) to radio to channel | 	 * @param channelMap the map of devices (by serial number) to radio to channel | ||||||
| 	 */ | 	 */ | ||||||
| 	public void applyConfig( | 	public void updateDeviceApConfig( | ||||||
| 		DeviceDataManager deviceDataManager, | 		DeviceDataManager deviceDataManager, | ||||||
| 		ConfigManager configManager, | 		ConfigManager configManager, | ||||||
| 		Map<String, Map<String, Integer>> channelMap | 		Map<String, Map<String, Integer>> channelMap | ||||||
| @@ -652,8 +663,5 @@ public abstract class ChannelOptimizer { | |||||||
| 				deviceConfig.autoChannels = entry.getValue(); | 				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() { | 	public Map<String, Map<String, Integer>> computeChannelMap() { | ||||||
| 		Map<String, Map<String, Integer>> channelMap = new TreeMap<>(); | 		Map<String, Map<String, Integer>> channelMap = new TreeMap<>(); | ||||||
| 		Map<String, List<String>> bandsMap = UCentralUtils | 		Map<String, List<String>> bandsMap = UCentralUtils | ||||||
| 			.getBandsMap(model.latestDeviceStatus); | 			.getBandsMap(model.latestDeviceStatusRadios); | ||||||
|  |  | ||||||
| 		Map<String, Map<String, List<Integer>>> deviceAvailableChannels = | 		Map<String, Map<String, List<Integer>>> deviceAvailableChannels = | ||||||
| 			UCentralUtils.getDeviceAvailableChannels( | 			UCentralUtils.getDeviceAvailableChannels( | ||||||
| 				model.latestDeviceStatus, | 				model.latestDeviceStatusRadios, | ||||||
| 				model.latestDeviceCapabilities, | 				model.latestDeviceCapabilities, | ||||||
| 				AVAILABLE_CHANNELS_BAND | 				AVAILABLE_CHANNELS_BAND | ||||||
| 			); | 			); | ||||||
|   | |||||||
| @@ -119,11 +119,11 @@ public class RandomChannelInitializer extends ChannelOptimizer { | |||||||
| 	public Map<String, Map<String, Integer>> computeChannelMap() { | 	public Map<String, Map<String, Integer>> computeChannelMap() { | ||||||
| 		Map<String, Map<String, Integer>> channelMap = new TreeMap<>(); | 		Map<String, Map<String, Integer>> channelMap = new TreeMap<>(); | ||||||
| 		Map<String, List<String>> bandsMap = | 		Map<String, List<String>> bandsMap = | ||||||
| 			UCentralUtils.getBandsMap(model.latestDeviceStatus); | 			UCentralUtils.getBandsMap(model.latestDeviceStatusRadios); | ||||||
|  |  | ||||||
| 		Map<String, Map<String, List<Integer>>> deviceAvailableChannels = | 		Map<String, Map<String, List<Integer>>> deviceAvailableChannels = | ||||||
| 			UCentralUtils.getDeviceAvailableChannels( | 			UCentralUtils.getDeviceAvailableChannels( | ||||||
| 				model.latestDeviceStatus, | 				model.latestDeviceStatusRadios, | ||||||
| 				model.latestDeviceCapabilities, | 				model.latestDeviceCapabilities, | ||||||
| 				AVAILABLE_CHANNELS_BAND | 				AVAILABLE_CHANNELS_BAND | ||||||
| 			); | 			); | ||||||
|   | |||||||
| @@ -14,7 +14,6 @@ import java.util.HashMap; | |||||||
| import java.util.HashSet; | import java.util.HashSet; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.Map; | import java.util.Map; | ||||||
| import java.util.Optional; |  | ||||||
| import java.util.Set; | import java.util.Set; | ||||||
| import java.util.TreeMap; | 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.UCentralUtils; | ||||||
| import com.facebook.openwifirrm.ucentral.WifiScanEntry; | import com.facebook.openwifirrm.ucentral.WifiScanEntry; | ||||||
| import com.facebook.openwifirrm.ucentral.models.State; | 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. |  * Measurement-based AP-AP TPC algorithm. | ||||||
| @@ -178,32 +174,6 @@ public class MeasurementBasedApApTPC extends TPC { | |||||||
| 		return managedBSSIDs; | 		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). | 	 * Get a map from BSSID to the received signal strength at neighboring APs (RSSI). | ||||||
| 	 * List of RSSIs are returned in sorted, ascending order. | 	 * List of RSSIs are returned in sorted, ascending order. | ||||||
| @@ -340,7 +310,6 @@ public class MeasurementBasedApApTPC extends TPC { | |||||||
| 		Map<String, List<Integer>> bssidToRssiValues = | 		Map<String, List<Integer>> bssidToRssiValues = | ||||||
| 			buildRssiMap(managedBSSIDs, model.latestWifiScans, band); | 			buildRssiMap(managedBSSIDs, model.latestWifiScans, band); | ||||||
| 		logger.debug("Starting TPC for the {} band", band); | 		logger.debug("Starting TPC for the {} band", band); | ||||||
| 		Map<String, JsonArray> allStatuses = model.latestDeviceStatus; |  | ||||||
| 		for (String serialNumber : serialNumbers) { | 		for (String serialNumber : serialNumbers) { | ||||||
| 			State state = model.latestState.get(serialNumber); | 			State state = model.latestState.get(serialNumber); | ||||||
| 			if ( | 			if ( | ||||||
| @@ -370,40 +339,68 @@ public class MeasurementBasedApApTPC extends TPC { | |||||||
| 				); | 				); | ||||||
| 				continue; | 				continue; | ||||||
| 			} | 			} | ||||||
| 			JsonArray radioStatuses = |  | ||||||
| 				allStatuses.get(serialNumber).getAsJsonArray(); | 			// An AP can have multiple interfaces, optimize for all of them | ||||||
| 			Optional<Integer> possibleCurrentTxPower = getCurrentTxPower( | 			for (State.Interface iface : state.interfaces) { | ||||||
| 				radioStatuses, | 				if (iface.ssids == null) { | ||||||
| 				band | 					continue; | ||||||
| 			); | 				} | ||||||
| 			if (possibleCurrentTxPower.isEmpty()) { |  | ||||||
| 				// this AP is not on the band of interest | 				for (State.Interface.SSID ssid : iface.ssids) { | ||||||
| 				continue; | 					Integer idx = UCentralUtils.parseReferenceIndex( | ||||||
|  | 						ssid.radio.get("$ref").getAsString() | ||||||
|  | 					); | ||||||
|  | 					if (idx == null) { | ||||||
|  | 						logger.error( | ||||||
|  | 							"Unable to get radio for {}, invalid radio ref {}", | ||||||
|  | 							serialNumber, | ||||||
|  | 							ssid.radio.get("$ref").getAsString() | ||||||
|  | 						); | ||||||
|  | 						continue; | ||||||
|  | 					} | ||||||
|  | 					State.Radio radio = state.radios[idx]; | ||||||
|  |  | ||||||
|  | 					// this specific SSID is not on the band of interest | ||||||
|  | 					if ( | ||||||
|  | 						!UCentralUtils.isChannelInBand(radio.channel, band) | ||||||
|  | 					) { | ||||||
|  | 						continue; | ||||||
|  | 					} | ||||||
|  |  | ||||||
|  | 					int currentTxPower = radio.tx_power; | ||||||
|  | 					String bssid = ssid.bssid; | ||||||
|  | 					List<Integer> rssiValues = bssidToRssiValues.get(bssid); | ||||||
|  | 					logger | ||||||
|  | 						.debug( | ||||||
|  | 							"Device <{}> : Interface <{}> : Channel <{}> : BSSID <{}>", | ||||||
|  | 							serialNumber, | ||||||
|  | 							iface.name, | ||||||
|  | 							channel, | ||||||
|  | 							bssid | ||||||
|  | 						); | ||||||
|  | 					for (int rssi : rssiValues) { | ||||||
|  | 						logger.debug("  Neighbor received RSSI: {}", rssi); | ||||||
|  | 					} | ||||||
|  | 					List<Integer> txPowerChoices = updateTxPowerChoices( | ||||||
|  | 						band, | ||||||
|  | 						serialNumber, | ||||||
|  | 						DEFAULT_TX_POWER_CHOICES | ||||||
|  | 					); | ||||||
|  | 					int newTxPower = computeTxPower( | ||||||
|  | 						serialNumber, | ||||||
|  | 						currentTxPower, | ||||||
|  | 						rssiValues, | ||||||
|  | 						coverageThreshold, | ||||||
|  | 						nthSmallestRssi, | ||||||
|  | 						txPowerChoices | ||||||
|  | 					); | ||||||
|  | 					logger.debug("  Old tx_power: {}", currentTxPower); | ||||||
|  | 					logger.debug("  New tx_power: {}", newTxPower); | ||||||
|  | 					txPowerMap | ||||||
|  | 						.computeIfAbsent(serialNumber, k -> new TreeMap<>()) | ||||||
|  | 						.put(band, newTxPower); | ||||||
|  | 				} | ||||||
| 			} | 			} | ||||||
| 			int currentTxPower = possibleCurrentTxPower.get(); |  | ||||||
| 			String bssid = state.interfaces[0].ssids[0].bssid; |  | ||||||
| 			List<Integer> rssiValues = bssidToRssiValues.get(bssid); |  | ||||||
| 			logger.debug("Device <{}> : BSSID <{}>", serialNumber, bssid); |  | ||||||
| 			for (int rssi : rssiValues) { |  | ||||||
| 				logger.debug("  Neighbor received RSSI: {}", rssi); |  | ||||||
| 			} |  | ||||||
| 			List<Integer> txPowerChoices = updateTxPowerChoices( |  | ||||||
| 				band, |  | ||||||
| 				serialNumber, |  | ||||||
| 				DEFAULT_TX_POWER_CHOICES |  | ||||||
| 			); |  | ||||||
| 			int newTxPower = computeTxPower( |  | ||||||
| 				serialNumber, |  | ||||||
| 				currentTxPower, |  | ||||||
| 				rssiValues, |  | ||||||
| 				coverageThreshold, |  | ||||||
| 				nthSmallestRssi, |  | ||||||
| 				txPowerChoices |  | ||||||
| 			); |  | ||||||
| 			logger.debug("  Old tx_power: {}", currentTxPower); |  | ||||||
| 			logger.debug("  New tx_power: {}", newTxPower); |  | ||||||
| 			txPowerMap.computeIfAbsent(serialNumber, k -> new TreeMap<>()) |  | ||||||
| 				.put(band, newTxPower); |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -22,7 +22,6 @@ import org.slf4j.LoggerFactory; | |||||||
| import com.facebook.openwifirrm.DeviceDataManager; | import com.facebook.openwifirrm.DeviceDataManager; | ||||||
| import com.facebook.openwifirrm.modules.Modeler.DataModel; | import com.facebook.openwifirrm.modules.Modeler.DataModel; | ||||||
| import com.facebook.openwifirrm.ucentral.models.State; | import com.facebook.openwifirrm.ucentral.models.State; | ||||||
| import com.google.gson.JsonObject; |  | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Measurement-based AP-client algorithm. |  * Measurement-based AP-client algorithm. | ||||||
| @@ -42,6 +41,9 @@ public class MeasurementBasedApClientTPC extends TPC { | |||||||
| 	/** Default tx power. */ | 	/** Default tx power. */ | ||||||
| 	public static final int DEFAULT_TX_POWER = 10; | 	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. */ | 	/** Mapping of MCS index to required SNR (dB) in 802.11ac. */ | ||||||
| 	private static final List<Double> MCS_TO_SNR = Collections.unmodifiableList( | 	private static final List<Double> MCS_TO_SNR = Collections.unmodifiableList( | ||||||
| 		Arrays.asList( | 		Arrays.asList( | ||||||
| @@ -154,19 +156,16 @@ public class MeasurementBasedApClientTPC extends TPC { | |||||||
| 	private int computeTxPowerForRadio( | 	private int computeTxPowerForRadio( | ||||||
| 		String serialNumber, | 		String serialNumber, | ||||||
| 		State state, | 		State state, | ||||||
| 		JsonObject radio, | 		State.Radio radio, | ||||||
| 		List<Integer> txPowerChoices | 		List<Integer> txPowerChoices | ||||||
| 	) { | 	) { | ||||||
| 		// Find current tx power and bandwidth | 		// Find current tx power and bandwidth | ||||||
| 		int currentTxPower = | 		int currentTxPower = radio.tx_power; | ||||||
| 			radio.has("tx_power") && !radio.get("tx_power").isJsonNull() | 		// treat as one 160MHz channel vs two 80MHz channels | ||||||
| 				? radio.get("tx_power").getAsInt() | 		Integer channelWidthMHz = | ||||||
| 				: 0; | 			UCentralUtils.parseChannelWidth(radio.channel_width, false); | ||||||
| 		int channelWidth = | 		int channelWidth = (channelWidthMHz != null | ||||||
| 			1_000_000 /* convert MHz to Hz */ * (radio.has("channel_width") && | 			? channelWidthMHz : DEFAULT_CHANNEL_WIDTH) * 1_000_000; // convert MHz to HZ | ||||||
| 				!radio.get("channel_width").isJsonNull() |  | ||||||
| 					? radio.get("channel_width").getAsInt() |  | ||||||
| 					: 20); |  | ||||||
| 		Collections.sort(txPowerChoices); | 		Collections.sort(txPowerChoices); | ||||||
| 		int minTxPower = txPowerChoices.get(0); | 		int minTxPower = txPowerChoices.get(0); | ||||||
| 		int maxTxPower = txPowerChoices.get(txPowerChoices.size() - 1); | 		int maxTxPower = txPowerChoices.get(txPowerChoices.size() - 1); | ||||||
| @@ -303,17 +302,10 @@ public class MeasurementBasedApClientTPC extends TPC { | |||||||
| 				); | 				); | ||||||
| 				continue; | 				continue; | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			Map<String, Integer> radioMap = new TreeMap<>(); | 			Map<String, Integer> radioMap = new TreeMap<>(); | ||||||
|  | 			for (State.Radio radio : state.radios) { | ||||||
| 			for (JsonObject radio : state.radios) { | 				int currentChannel = radio.channel; | ||||||
| 				Integer currentChannel = |  | ||||||
| 					radio.has("channel") && !radio.get("channel").isJsonNull() |  | ||||||
| 						? radio.get("channel").getAsInt() |  | ||||||
| 						: null; |  | ||||||
| 				if (currentChannel == null) { |  | ||||||
| 					continue; |  | ||||||
| 				} |  | ||||||
|  |  | ||||||
| 				String band = UCentralUtils.getBandFromChannel(currentChannel); | 				String band = UCentralUtils.getBandFromChannel(currentChannel); | ||||||
| 				if (band == null) { | 				if (band == null) { | ||||||
| 					continue; | 					continue; | ||||||
|   | |||||||
| @@ -13,20 +13,18 @@ import java.util.Arrays; | |||||||
| import java.util.Collections; | import java.util.Collections; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.Map; | import java.util.Map; | ||||||
|  | import java.util.TreeMap; | ||||||
| import java.util.stream.Collectors; | import java.util.stream.Collectors; | ||||||
| import java.util.stream.IntStream; | import java.util.stream.IntStream; | ||||||
| import java.util.List; |  | ||||||
| import java.util.Map; | import org.slf4j.Logger; | ||||||
| import java.util.TreeMap; | import org.slf4j.LoggerFactory; | ||||||
|  |  | ||||||
| import com.facebook.openwifirrm.DeviceConfig; | import com.facebook.openwifirrm.DeviceConfig; | ||||||
| import com.facebook.openwifirrm.DeviceDataManager; | import com.facebook.openwifirrm.DeviceDataManager; | ||||||
| import com.facebook.openwifirrm.modules.ConfigManager; | import com.facebook.openwifirrm.modules.ConfigManager; | ||||||
| import com.facebook.openwifirrm.modules.Modeler.DataModel; | import com.facebook.openwifirrm.modules.Modeler.DataModel; | ||||||
| import com.facebook.openwifirrm.ucentral.models.State; | 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. |  * TPC (Transmit Power Control) base class. | ||||||
| @@ -75,7 +73,7 @@ public abstract class TPC { | |||||||
| 		this.model.latestState.keySet() | 		this.model.latestState.keySet() | ||||||
| 			.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber) | 			.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber) | ||||||
| 			); | 			); | ||||||
| 		this.model.latestDeviceStatus.keySet() | 		this.model.latestDeviceStatusRadios.keySet() | ||||||
| 			.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber) | 			.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber) | ||||||
| 			); | 			); | ||||||
| 		this.model.latestDeviceCapabilities.keySet() | 		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 band the operational band | ||||||
| 	 * @param serialNumber the device | 	 * @param serialNumber the device's serial number | ||||||
| 	 * @param txPowerChoices the available tx powers of the device | 	 * @param txPowerChoices the device's available tx powers | ||||||
| 	 * @return the updated tx powers of the device | 	 * @return the device's updated tx powers | ||||||
| 	 */ | 	 */ | ||||||
| 	protected List<Integer> updateTxPowerChoices( | 	protected List<Integer> updateTxPowerChoices( | ||||||
| 		String band, | 		String band, | ||||||
| 		String serialNumber, | 		String serialNumber, | ||||||
| 		List<Integer> txPowerChoices | 		List<Integer> txPowerChoices | ||||||
| 	) { | 	) { | ||||||
| 		List<Integer> newTxPowerChoices = | 		List<Integer> newTxPowerChoices = new ArrayList<>(txPowerChoices); | ||||||
| 			new ArrayList<>(txPowerChoices); |  | ||||||
|  |  | ||||||
| 		// Update the available tx powers based on user tx powers or allowed tx powers | 		// Update the available tx powers based on user tx powers or allowed tx powers | ||||||
| 		DeviceConfig deviceCfg = deviceConfigs.get(serialNumber); | 		DeviceConfig deviceCfg = deviceConfigs.get(serialNumber); | ||||||
| @@ -128,8 +126,7 @@ public abstract class TPC { | |||||||
| 			newTxPowerChoices.retainAll(allowedTxPowers); | 			newTxPowerChoices.retainAll(allowedTxPowers); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// If the intersection of the above steps gives an empty list, | 		// If newTxPowerChoices is empty, use default available tx powers list | ||||||
| 		// turn back to use the default available tx powers list |  | ||||||
| 		if (newTxPowerChoices.isEmpty()) { | 		if (newTxPowerChoices.isEmpty()) { | ||||||
| 			logger.debug( | 			logger.debug( | ||||||
| 				"Device {}: the updated availableTxPowersList is empty!!! " + | 				"Device {}: the updated availableTxPowersList is empty!!! " + | ||||||
| @@ -154,14 +151,13 @@ public abstract class TPC { | |||||||
| 	public abstract Map<String, Map<String, Integer>> computeTxPowerMap(); | 	public abstract Map<String, Map<String, Integer>> computeTxPowerMap(); | ||||||
|  |  | ||||||
| 	/** | 	/** | ||||||
| 	 * Program the given tx power map into the AP config and notify the config | 	 * Program the given tx power map into the AP config. | ||||||
| 	 * manager. |  | ||||||
| 	 * | 	 * | ||||||
| 	 * @param deviceDataManager the DeviceDataManager instance | 	 * @param deviceDataManager the DeviceDataManager instance | ||||||
| 	 * @param configManager the ConfigManager instance | 	 * @param configManager the ConfigManager instance | ||||||
| 	 * @param txPowerMap the map of devices (by serial number) to radio to tx power | 	 * @param txPowerMap the map of devices (by serial number) to radio to tx power | ||||||
| 	 */ | 	 */ | ||||||
| 	public void applyConfig( | 	public void updateDeviceApConfig( | ||||||
| 		DeviceDataManager deviceDataManager, | 		DeviceDataManager deviceDataManager, | ||||||
| 		ConfigManager configManager, | 		ConfigManager configManager, | ||||||
| 		Map<String, Map<String, Integer>> txPowerMap | 		Map<String, Map<String, Integer>> txPowerMap | ||||||
| @@ -179,15 +175,12 @@ public abstract class TPC { | |||||||
| 				deviceConfig.autoTxPowers = entry.getValue(); | 				deviceConfig.autoTxPowers = entry.getValue(); | ||||||
| 			} | 			} | ||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
| 		// Trigger config update now |  | ||||||
| 		configManager.wakeUp(); |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	/** | 	/** | ||||||
| 	 * Get AP serial numbers per channel. | 	 * 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() { | 	protected Map<Integer, List<String>> getApsPerChannel() { | ||||||
| 		Map<Integer, List<String>> apsPerChannel = new TreeMap<>(); | 		Map<Integer, List<String>> apsPerChannel = new TreeMap<>(); | ||||||
| @@ -203,12 +196,9 @@ public abstract class TPC { | |||||||
| 				continue; | 				continue; | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			for (JsonObject radio : state.radios) { | 			for (State.Radio radio : state.radios) { | ||||||
| 				Integer currentChannel = | 				Integer currentChannel = radio.channel; | ||||||
| 					radio.has("channel") && !radio.get("channel").isJsonNull() | 				if (currentChannel == 0) { | ||||||
| 						? radio.get("channel").getAsInt() |  | ||||||
| 						: null; |  | ||||||
| 				if (currentChannel == null) { |  | ||||||
| 					continue; | 					continue; | ||||||
| 				} | 				} | ||||||
| 				apsPerChannel | 				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; | package com.facebook.openwifirrm.ucentral; | ||||||
|  |  | ||||||
|  | import java.time.Instant; | ||||||
| import java.util.Collections; | import java.util.Collections; | ||||||
| import java.util.HashMap; | import java.util.HashMap; | ||||||
| import java.util.List; | 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.StatisticsRecords; | ||||||
| import com.facebook.openwifirrm.ucentral.gw.models.SystemInfoResults; | import com.facebook.openwifirrm.ucentral.gw.models.SystemInfoResults; | ||||||
| import com.facebook.openwifirrm.ucentral.gw.models.TokenValidationResult; | 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.gw.models.WifiScanRequest; | ||||||
| import com.facebook.openwifirrm.ucentral.prov.models.EntityList; | import com.facebook.openwifirrm.ucentral.prov.models.EntityList; | ||||||
| import com.facebook.openwifirrm.ucentral.prov.models.InventoryTagList; | 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 | 	 * The access token obtained from uCentralSec, needed only when using public | ||||||
| 	 * endpoints. | 	 * 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. | 	 * Constructor. | ||||||
| @@ -184,7 +198,8 @@ public class UCentralClient { | |||||||
| 		Map<String, Object> body = new HashMap<>(); | 		Map<String, Object> body = new HashMap<>(); | ||||||
| 		body.put("userId", username); | 		body.put("userId", username); | ||||||
| 		body.put("password", password); | 		body.put("password", password); | ||||||
| 		HttpResponse<String> response = httpPost("oauth2", OWSEC_SERVICE, body); | 		HttpResponse<String> response = | ||||||
|  | 			httpPost("oauth2", OWSEC_SERVICE, body, null); | ||||||
| 		if (!response.isSuccess()) { | 		if (!response.isSuccess()) { | ||||||
| 			logger.error( | 			logger.error( | ||||||
| 				"Login failed: Response code {}, body: {}", | 				"Login failed: Response code {}, body: {}", | ||||||
| @@ -195,27 +210,146 @@ public class UCentralClient { | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Parse access token from response | 		// Parse access token from response | ||||||
| 		JSONObject respBody; | 		WebTokenResult token; | ||||||
| 		try { | 		try { | ||||||
| 			respBody = new JSONObject(response.getBody()); | 			token = gson.fromJson(response.getBody(), WebTokenResult.class); | ||||||
| 		} catch (JSONException e) { | 		} catch (JsonSyntaxException e) { | ||||||
| 			logger.error("Login failed: Unexpected response", e); | 			logger.error("Login failed: Unexpected response", e); | ||||||
| 			logger.debug("Response body: {}", response.getBody()); | 			logger.debug("Response body: {}", response.getBody()); | ||||||
| 			return false; | 			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.error("Login failed: Missing access token"); | ||||||
| 			logger.debug("Response body: {}", respBody.toString()); | 			logger.debug("Response body: {}", response.getBody()); | ||||||
| 			return false; | 			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.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 | 		// Load system endpoints | ||||||
| 		return loadSystemEndpoints(); | 		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. */ | 	/** Read system endpoint URLs from uCentralSec. */ | ||||||
| 	private boolean loadSystemEndpoints() { | 	private boolean loadSystemEndpoints() { | ||||||
| 		// Make request | 		// Make request | ||||||
| @@ -324,8 +458,12 @@ public class UCentralClient { | |||||||
| 			.connectTimeout(connectTimeoutMs) | 			.connectTimeout(connectTimeoutMs) | ||||||
| 			.socketTimeout(socketTimeoutMs); | 			.socketTimeout(socketTimeoutMs); | ||||||
| 		if (usePublicEndpoints) { | 		if (usePublicEndpoints) { | ||||||
| 			if (accessToken != null) { | 			if (!isAccessTokenExpired()) { | ||||||
| 				req.header("Authorization", "Bearer " + accessToken); | 				req.header( | ||||||
|  | 					"Authorization", | ||||||
|  | 					"Bearer " + accessToken.access_token | ||||||
|  | 				); | ||||||
|  | 				lastAccess = Instant.now().getEpochSecond(); | ||||||
| 			} | 			} | ||||||
| 		} else { | 		} else { | ||||||
| 			req | 			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( | 	private HttpResponse<String> httpPost( | ||||||
| 		String endpoint, | 		String endpoint, | ||||||
| 		String service, | 		String service, | ||||||
| 		Object body | 		Object body, | ||||||
|  | 		Map<String, Object> parameters | ||||||
| 	) { | 	) { | ||||||
| 		return httpPost( | 		return httpPost( | ||||||
| 			endpoint, | 			endpoint, | ||||||
| 			service, | 			service, | ||||||
| 			body, | 			body, | ||||||
|  | 			parameters, | ||||||
| 			socketParams.connectTimeoutMs, | 			socketParams.connectTimeoutMs, | ||||||
| 			socketParams.socketTimeoutMs | 			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( | 	private HttpResponse<String> httpPost( | ||||||
| 		String endpoint, | 		String endpoint, | ||||||
| 		String service, | 		String service, | ||||||
| 		Object body, | 		Object body, | ||||||
|  | 		Map<String, Object> parameters, | ||||||
| 		int connectTimeoutMs, | 		int connectTimeoutMs, | ||||||
| 		int socketTimeoutMs | 		int socketTimeoutMs | ||||||
| 	) { | 	) { | ||||||
| @@ -367,9 +508,16 @@ public class UCentralClient { | |||||||
| 			.header("accept", "application/json") | 			.header("accept", "application/json") | ||||||
| 			.connectTimeout(connectTimeoutMs) | 			.connectTimeout(connectTimeoutMs) | ||||||
| 			.socketTimeout(socketTimeoutMs); | 			.socketTimeout(socketTimeoutMs); | ||||||
|  | 		if (parameters != null && !parameters.isEmpty()) { | ||||||
|  | 			req.queryString(parameters); | ||||||
|  | 		} | ||||||
| 		if (usePublicEndpoints) { | 		if (usePublicEndpoints) { | ||||||
| 			if (accessToken != null) { | 			if (!isAccessTokenExpired()) { | ||||||
| 				req.header("Authorization", "Bearer " + accessToken); | 				req.header( | ||||||
|  | 					"Authorization", | ||||||
|  | 					"Bearer " + accessToken.access_token | ||||||
|  | 				); | ||||||
|  | 				lastAccess = Instant.now().getEpochSecond(); | ||||||
| 			} | 			} | ||||||
| 		} else { | 		} else { | ||||||
| 			req | 			req | ||||||
| @@ -454,6 +602,7 @@ public class UCentralClient { | |||||||
| 			String.format("device/%s/wifiscan", serialNumber), | 			String.format("device/%s/wifiscan", serialNumber), | ||||||
| 			OWGW_SERVICE, | 			OWGW_SERVICE, | ||||||
| 			req, | 			req, | ||||||
|  | 			null, | ||||||
| 			socketParams.connectTimeoutMs, | 			socketParams.connectTimeoutMs, | ||||||
| 			socketParams.wifiScanTimeoutMs | 			socketParams.wifiScanTimeoutMs | ||||||
| 		); | 		); | ||||||
| @@ -482,7 +631,8 @@ public class UCentralClient { | |||||||
| 		HttpResponse<String> response = httpPost( | 		HttpResponse<String> response = httpPost( | ||||||
| 			String.format("device/%s/configure", serialNumber), | 			String.format("device/%s/configure", serialNumber), | ||||||
| 			OWGW_SERVICE, | 			OWGW_SERVICE, | ||||||
| 			req | 			req, | ||||||
|  | 			null | ||||||
| 		); | 		); | ||||||
| 		if (!response.isSuccess()) { | 		if (!response.isSuccess()) { | ||||||
| 			logger.error("Error: {}", response.getBody()); | 			logger.error("Error: {}", response.getBody()); | ||||||
|   | |||||||
| @@ -24,6 +24,10 @@ import org.slf4j.LoggerFactory; | |||||||
| import com.facebook.openwifirrm.RRMConfig; | import com.facebook.openwifirrm.RRMConfig; | ||||||
| import com.facebook.openwifirrm.Utils; | import com.facebook.openwifirrm.Utils; | ||||||
| import com.facebook.openwifirrm.optimizers.channel.ChannelOptimizer; | 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.facebook.openwifirrm.ucentral.models.State; | ||||||
| import com.google.gson.Gson; | import com.google.gson.Gson; | ||||||
| import com.google.gson.JsonArray; | import com.google.gson.JsonArray; | ||||||
| @@ -37,6 +41,9 @@ public class UCentralUtils { | |||||||
| 	private static final Logger logger = | 	private static final Logger logger = | ||||||
| 		LoggerFactory.getLogger(UCentralUtils.class); | 		LoggerFactory.getLogger(UCentralUtils.class); | ||||||
|  |  | ||||||
|  | 	/** Information Element (IE) content field key */ | ||||||
|  | 	private static final String IE_CONTENT_FIELD_KEY = "content"; | ||||||
|  |  | ||||||
| 	/** The Gson instance. */ | 	/** The Gson instance. */ | ||||||
| 	private static final Gson gson = new Gson(); | 	private static final Gson gson = new Gson(); | ||||||
|  |  | ||||||
| @@ -79,15 +86,76 @@ public class UCentralUtils { | |||||||
| 			for (JsonElement e : scanInfo) { | 			for (JsonElement e : scanInfo) { | ||||||
| 				WifiScanEntry entry = gson.fromJson(e, WifiScanEntry.class); | 				WifiScanEntry entry = gson.fromJson(e, WifiScanEntry.class); | ||||||
| 				entry.unixTimeMs = timestampMs; | 				entry.unixTimeMs = timestampMs; | ||||||
|  | 				extractIEs(e, entry); | ||||||
| 				entries.add(entry); | 				entries.add(entry); | ||||||
|  |  | ||||||
| 			} | 			} | ||||||
| 		} catch (Exception e) { | 		} catch (Exception e) { | ||||||
|  | 			logger.debug("Exception when parsing wifiscan entries", e); | ||||||
| 			return null; | 			return null; | ||||||
| 		} | 		} | ||||||
| 		return entries; | 		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. | 	 * Set all radios config of an AP to a given value. | ||||||
| 	 * | 	 * | ||||||
| @@ -130,9 +198,24 @@ public class UCentralUtils { | |||||||
| 				continue; | 				continue; | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			// Compare vs. existing value | 			// Compare vs. existing value. | ||||||
| 			int currentValue = radioConfig.get(fieldName).getAsInt(); | 			// not all values are int so override those values | ||||||
| 			if (currentValue == newValue) { | 			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( | 				logger.info( | ||||||
| 					"Device {}: {} {} is already {}", | 					"Device {}: {} {} is already {}", | ||||||
| 					serialNumber, | 					serialNumber, | ||||||
| @@ -150,7 +233,7 @@ public class UCentralUtils { | |||||||
| 					operationalBand, | 					operationalBand, | ||||||
| 					fieldName, | 					fieldName, | ||||||
| 					newValue, | 					newValue, | ||||||
| 					currentValue | 					currentValue != null ? currentValue : fieldValue.toString() | ||||||
| 				); | 				); | ||||||
| 				wasModified = true; | 				wasModified = true; | ||||||
| 			} | 			} | ||||||
| @@ -384,4 +467,52 @@ public class UCentralUtils { | |||||||
| 		} | 		} | ||||||
| 		return null; | 		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. | 	 * time reference. | ||||||
| 	 */ | 	 */ | ||||||
| 	public long unixTimeMs; | 	public long unixTimeMs; | ||||||
|  | 	/** Stores Information Elements (IEs) from the wifiscan entry. */ | ||||||
|  | 	public InformationElements ieContainer; | ||||||
|  |  | ||||||
| 	/** Default Constructor. */ | 	/** Default Constructor. */ | ||||||
| 	public WifiScanEntry() {} | 	public WifiScanEntry() {} | ||||||
| @@ -30,13 +32,14 @@ public class WifiScanEntry extends WifiScanEntryResult { | |||||||
| 	public WifiScanEntry(WifiScanEntry o) { | 	public WifiScanEntry(WifiScanEntry o) { | ||||||
| 		super(o); | 		super(o); | ||||||
| 		this.unixTimeMs = o.unixTimeMs; | 		this.unixTimeMs = o.unixTimeMs; | ||||||
|  | 		this.ieContainer = o.ieContainer; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	@Override | 	@Override | ||||||
| 	public int hashCode() { | 	public int hashCode() { | ||||||
| 		final int prime = 31; | 		final int prime = 31; | ||||||
| 		int result = super.hashCode(); | 		int result = super.hashCode(); | ||||||
| 		result = prime * result + Objects.hash(unixTimeMs); | 		result = prime * result + Objects.hash(ieContainer, unixTimeMs); | ||||||
| 		return result; | 		return result; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -52,7 +55,8 @@ public class WifiScanEntry extends WifiScanEntryResult { | |||||||
| 			return false; | 			return false; | ||||||
| 		} | 		} | ||||||
| 		WifiScanEntry other = (WifiScanEntry) obj; | 		WifiScanEntry other = (WifiScanEntry) obj; | ||||||
| 		return unixTimeMs == other.unixTimeMs; | 		return Objects.equals(ieContainer, other.ieContainer) && | ||||||
|  | 			unixTimeMs == other.unixTimeMs; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	@Override | 	@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. |  * 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.Arrays; | ||||||
| import java.util.Objects; | 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 |  * High Throughput (HT) Operation Element, which is potentially present in | ||||||
|  * wifiscan entries. Introduced in 802.11n (2009). |  * wifiscan entries. Introduced in 802.11n (2009). | ||||||
|  */ |  */ | ||||||
| public class HTOperationElement { | public class HTOperation { | ||||||
| 
 | 
 | ||||||
| 	/** Channel number of the primary channel. */ | 	/** Channel number of the primary channel. */ | ||||||
| 	public final byte primaryChannel; | 	public final byte primaryChannel; | ||||||
| @@ -78,7 +78,7 @@ public class HTOperationElement { | |||||||
| 	 * For details about the parameters, see the javadocs for the corresponding | 	 * For details about the parameters, see the javadocs for the corresponding | ||||||
| 	 * member variables. | 	 * member variables. | ||||||
| 	 */ | 	 */ | ||||||
| 	public HTOperationElement( | 	public HTOperation( | ||||||
| 		byte primaryChannel, | 		byte primaryChannel, | ||||||
| 		byte secondaryChannelOffset, | 		byte secondaryChannelOffset, | ||||||
| 		boolean staChannelWidth, | 		boolean staChannelWidth, | ||||||
| @@ -114,7 +114,7 @@ public class HTOperationElement { | |||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	/** Constructor with the most used parameters. */ | 	/** Constructor with the most used parameters. */ | ||||||
| 	public HTOperationElement( | 	public HTOperation( | ||||||
| 		byte primaryChannel, | 		byte primaryChannel, | ||||||
| 		byte secondaryChannelOffset, | 		byte secondaryChannelOffset, | ||||||
| 		boolean staChannelWidth, | 		boolean staChannelWidth, | ||||||
| @@ -141,7 +141,7 @@ public class HTOperationElement { | |||||||
| 	 * @param htOper a base64 encoded properly formatted HT operation element (see | 	 * @param htOper a base64 encoded properly formatted HT operation element (see | ||||||
| 	 *               802.11) | 	 *               802.11) | ||||||
| 	 */ | 	 */ | ||||||
| 	public HTOperationElement(String htOper) { | 	public HTOperation(String htOper) { | ||||||
| 		byte[] bytes = Base64.decodeBase64(htOper); | 		byte[] bytes = Base64.decodeBase64(htOper); | ||||||
| 		/* | 		/* | ||||||
| 		 * Note that the code here may seem to read "reversed" compared to 802.11. This | 		 * 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 | 	 * @return true if the the operation elements "match" for the purpose of | ||||||
| 	 *         aggregating statistics; false otherwise. | 	 *         aggregating statistics; false otherwise. | ||||||
| 	 */ | 	 */ | ||||||
| 	public boolean matchesForAggregation(HTOperationElement other) { | 	public boolean matchesForAggregation(HTOperation other) { | ||||||
| 		return other != null && primaryChannel == other.primaryChannel && | 		return other != null && primaryChannel == other.primaryChannel && | ||||||
| 			secondaryChannelOffset == other.secondaryChannelOffset && | 			secondaryChannelOffset == other.secondaryChannelOffset && | ||||||
| 			staChannelWidth == other.staChannelWidth && | 			staChannelWidth == other.staChannelWidth && | ||||||
| @@ -211,8 +211,8 @@ public class HTOperationElement { | |||||||
| 		if (htOper1 == null || htOper2 == null) { | 		if (htOper1 == null || htOper2 == null) { | ||||||
| 			return false; // false if exactly one is null | 			return false; // false if exactly one is null | ||||||
| 		} | 		} | ||||||
| 		HTOperationElement htOperObj1 = new HTOperationElement(htOper1); | 		HTOperation htOperObj1 = new HTOperation(htOper1); | ||||||
| 		HTOperationElement htOperObj2 = new HTOperationElement(htOper2); | 		HTOperation htOperObj2 = new HTOperation(htOper2); | ||||||
| 		return htOperObj1.matchesForAggregation(htOperObj2); | 		return htOperObj1.matchesForAggregation(htOperObj2); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| @@ -248,7 +248,7 @@ public class HTOperationElement { | |||||||
| 		if (getClass() != obj.getClass()) { | 		if (getClass() != obj.getClass()) { | ||||||
| 			return false; | 			return false; | ||||||
| 		} | 		} | ||||||
| 		HTOperationElement other = (HTOperationElement) obj; | 		HTOperation other = (HTOperation) obj; | ||||||
| 		return Arrays.equals(basicHtMcsSet, other.basicHtMcsSet) && | 		return Arrays.equals(basicHtMcsSet, other.basicHtMcsSet) && | ||||||
| 			channelCenterFrequencySegment2 == | 			channelCenterFrequencySegment2 == | ||||||
| 				other.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. |  * 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.Arrays; | ||||||
| import java.util.Objects; | 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 |  * Very High Throughput (VHT) Operation Element, which is potentially present in | ||||||
|  * wifiscan entries. Introduced in 802.11ac (2013). |  * wifiscan entries. Introduced in 802.11ac (2013). | ||||||
|  */ |  */ | ||||||
| public class VHTOperationElement { | public class VHTOperation { | ||||||
| 
 | 
 | ||||||
| 	/** | 	/** | ||||||
| 	 * This field is 0 if the channel width is 20 MHz or 40 MHz, and 1 otherwise. | 	 * 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 | 	 * 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 | 	 * channel that contains the primary channel. For a 80+80 MHz wide channel, this | ||||||
| 	 * parameter is the channel number of the primary channel. | 	 * 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 | 	 * 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 | 	 * 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 | 	 * wide channel. If the channel is 80+80 MHz wide, this parameter is the channel | ||||||
| 	 * index of the secondary 80 MHz wide 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 | 	 * 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 | 	 * 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 | 	 * @param vhtOper a base64 encoded properly formatted VHT operation element (see | ||||||
| 	 *                802.11 standard) | 	 *                802.11 standard) | ||||||
| 	 */ | 	 */ | ||||||
| 	public VHTOperationElement(String vhtOper) { | 	public VHTOperation(String vhtOper) { | ||||||
| 		byte[] bytes = Base64.decodeBase64(vhtOper); | 		byte[] bytes = Base64.decodeBase64(vhtOper); | ||||||
| 		this.channelWidth = bytes[0]; | 		this.channelWidth = bytes[0]; | ||||||
| 		this.channel1 = bytes[1]; | 		this.channel1 = (short) (bytes[1] & 0xff); // read as unsigned value | ||||||
| 		this.channel2 = bytes[2]; | 		this.channel2 = (short) (bytes[2] & 0xff); // read as unsigned value | ||||||
| 		byte[] vhtMcsForNss = new byte[8]; | 		byte[] vhtMcsForNss = new byte[8]; | ||||||
| 		vhtMcsForNss[0] = (byte) (bytes[3] >>> 6); | 		vhtMcsForNss[0] = (byte) (bytes[3] >>> 6); | ||||||
| 		vhtMcsForNss[1] = (byte) ((bytes[3] & 0b00110000) >>> 4); | 		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 | 	 * For details about the parameters, see the javadocs for the corresponding | ||||||
| 	 * member variables. | 	 * member variables. | ||||||
| 	 */ | 	 */ | ||||||
| 	public VHTOperationElement( | 	public VHTOperation( | ||||||
| 		byte channelWidth, | 		byte channelWidth, | ||||||
| 		byte channel1, | 		short channel1, | ||||||
| 		byte channel2, | 		short channel2, | ||||||
| 		byte[] vhtMcsForNss | 		byte[] vhtMcsForNss | ||||||
| 	) { | 	) { | ||||||
| 		/* | 		/* | ||||||
| @@ -106,7 +114,7 @@ public class VHTOperationElement { | |||||||
| 	 * @return true if the the operation elements "match" for the purpose of | 	 * @return true if the the operation elements "match" for the purpose of | ||||||
| 	 *         aggregating statistics; false otherwise. | 	 *         aggregating statistics; false otherwise. | ||||||
| 	 */ | 	 */ | ||||||
| 	public boolean matchesForAggregation(VHTOperationElement other) { | 	public boolean matchesForAggregation(VHTOperation other) { | ||||||
| 		// check everything except vhtMcsForNss | 		// check everything except vhtMcsForNss | ||||||
| 		return other != null && channel1 == other.channel1 && | 		return other != null && channel1 == other.channel1 && | ||||||
| 			channel2 == other.channel2 && channelWidth == other.channelWidth; | 			channel2 == other.channel2 && channelWidth == other.channelWidth; | ||||||
| @@ -134,8 +142,8 @@ public class VHTOperationElement { | |||||||
| 		if (vhtOper1 == null || vhtOper2 == null) { | 		if (vhtOper1 == null || vhtOper2 == null) { | ||||||
| 			return false; // false if exactly one is null | 			return false; // false if exactly one is null | ||||||
| 		} | 		} | ||||||
| 		VHTOperationElement vhtOperObj1 = new VHTOperationElement(vhtOper1); | 		VHTOperation vhtOperObj1 = new VHTOperation(vhtOper1); | ||||||
| 		VHTOperationElement vhtOperObj2 = new VHTOperationElement(vhtOper2); | 		VHTOperation vhtOperObj2 = new VHTOperation(vhtOper2); | ||||||
| 		return vhtOperObj1.matchesForAggregation(vhtOperObj2); | 		return vhtOperObj1.matchesForAggregation(vhtOperObj2); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| @@ -160,7 +168,7 @@ public class VHTOperationElement { | |||||||
| 		if (getClass() != obj.getClass()) { | 		if (getClass() != obj.getClass()) { | ||||||
| 			return false; | 			return false; | ||||||
| 		} | 		} | ||||||
| 		VHTOperationElement other = (VHTOperationElement) obj; | 		VHTOperation other = (VHTOperation) obj; | ||||||
| 		return channel1 == other.channel1 && channel2 == other.channel2 && | 		return channel1 == other.channel1 && channel2 == other.channel2 && | ||||||
| 			channelWidth == other.channelWidth && | 			channelWidth == other.channelWidth && | ||||||
| 			Arrays.equals(vhtMcsForNss, other.vhtMcsForNss); | 			Arrays.equals(vhtMcsForNss, other.vhtMcsForNss); | ||||||
| @@ -12,8 +12,8 @@ import com.google.gson.JsonObject; | |||||||
| import com.google.gson.annotations.SerializedName; | import com.google.gson.annotations.SerializedName; | ||||||
|  |  | ||||||
| public class State { | public class State { | ||||||
| 	public class Interface { | 	public static class Interface { | ||||||
| 		public class Client { | 		public static class Client { | ||||||
| 			public String mac; | 			public String mac; | ||||||
| 			public String[] ipv4_addresses; | 			public String[] ipv4_addresses; | ||||||
| 			public String[] ipv6_addresses; | 			public String[] ipv6_addresses; | ||||||
| @@ -21,9 +21,9 @@ public class State { | |||||||
| 			// TODO last_seen | 			// TODO last_seen | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		public class SSID { | 		public static class SSID { | ||||||
| 			public class Association { | 			public static class Association { | ||||||
| 				public class Rate { | 				public static class Rate { | ||||||
| 					public long bitrate; | 					public long bitrate; | ||||||
| 					public int chwidth; | 					public int chwidth; | ||||||
| 					public boolean sgi; | 					public boolean sgi; | ||||||
| @@ -66,7 +66,7 @@ public class State { | |||||||
| 			public JsonObject radio; | 			public JsonObject radio; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		public class Counters { | 		public static class Counters { | ||||||
| 			public long collisions; | 			public long collisions; | ||||||
| 			public long multicast; | 			public long multicast; | ||||||
| 			public long rx_bytes; | 			public long rx_bytes; | ||||||
| @@ -96,8 +96,8 @@ public class State { | |||||||
|  |  | ||||||
| 	public Interface[] interfaces; | 	public Interface[] interfaces; | ||||||
|  |  | ||||||
| 	public class Unit { | 	public static class Unit { | ||||||
| 		public class Memory { | 		public static class Memory { | ||||||
| 			public long buffered; | 			public long buffered; | ||||||
| 			public long cached; | 			public long cached; | ||||||
| 			public long free; | 			public long free; | ||||||
| @@ -112,8 +112,21 @@ public class State { | |||||||
|  |  | ||||||
| 	public Unit unit; | 	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 | 	// TODO | ||||||
| 	public JsonObject[] radios; |  | ||||||
| 	@SerializedName("link-state") public JsonObject linkState; | 	@SerializedName("link-state") public JsonObject linkState; | ||||||
| 	public JsonObject gps; | 	public JsonObject gps; | ||||||
| 	public JsonObject poe; | 	public JsonObject poe; | ||||||
|   | |||||||
| @@ -10,9 +10,11 @@ package com.facebook.openwifirrm.ucentral.models; | |||||||
|  |  | ||||||
| import java.util.Objects; | 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 class WifiScanEntryResult { | ||||||
| 	public int channel; | 	public int channel; | ||||||
| 	public long last_seen; | 	public long last_seen; | ||||||
| @@ -50,8 +52,6 @@ public class WifiScanEntryResult { | |||||||
| 	public String vht_oper; | 	public String vht_oper; | ||||||
| 	public int capability; | 	public int capability; | ||||||
| 	public int frequency; | 	public int frequency; | ||||||
| 	/** IE = information element */ |  | ||||||
| 	public JsonArray ies; |  | ||||||
|  |  | ||||||
| 	/** Default Constructor. */ | 	/** Default Constructor. */ | ||||||
| 	public WifiScanEntryResult() {} | 	public WifiScanEntryResult() {} | ||||||
| @@ -68,7 +68,6 @@ public class WifiScanEntryResult { | |||||||
| 		this.vht_oper = o.vht_oper; | 		this.vht_oper = o.vht_oper; | ||||||
| 		this.capability = o.capability; | 		this.capability = o.capability; | ||||||
| 		this.frequency = o.frequency; | 		this.frequency = o.frequency; | ||||||
| 		this.ies = o.ies; |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	@Override | 	@Override | ||||||
| @@ -79,7 +78,6 @@ public class WifiScanEntryResult { | |||||||
| 			channel, | 			channel, | ||||||
| 			frequency, | 			frequency, | ||||||
| 			ht_oper, | 			ht_oper, | ||||||
| 			ies, |  | ||||||
| 			last_seen, | 			last_seen, | ||||||
| 			signal, | 			signal, | ||||||
| 			ssid, | 			ssid, | ||||||
| @@ -104,7 +102,9 @@ public class WifiScanEntryResult { | |||||||
| 			capability == other.capability && channel == other.channel && | 			capability == other.capability && channel == other.channel && | ||||||
| 			frequency == other.frequency && Objects | 			frequency == other.frequency && Objects | ||||||
| 				.equals(ht_oper, other.ht_oper) && | 				.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 | 	@Override | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ import java.util.List; | |||||||
| import com.facebook.openwifirrm.ucentral.gw.models.NoteInfo; | import com.facebook.openwifirrm.ucentral.gw.models.NoteInfo; | ||||||
|  |  | ||||||
| public class DeviceConfiguration { | public class DeviceConfiguration { | ||||||
| 	public class DeviceConfigurationElement { | 	public static class DeviceConfigurationElement { | ||||||
| 		public String name; | 		public String name; | ||||||
| 		public String description; | 		public String description; | ||||||
| 		public Integer weight; | 		public Integer weight; | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ package com.facebook.openwifirrm.ucentral.prov.models; | |||||||
| import java.util.List; | import java.util.List; | ||||||
|  |  | ||||||
| public class RRMDetails { | public class RRMDetails { | ||||||
| 	public class RRMDetailsImpl { | 	public static class RRMDetailsImpl { | ||||||
| 		public String vendor; | 		public String vendor; | ||||||
| 		public String schedule; | 		public String schedule; | ||||||
| 		public List<RRMAlgorithmDetails> algorithms; | 		public List<RRMAlgorithmDetails> algorithms; | ||||||
|   | |||||||
| @@ -9,7 +9,7 @@ | |||||||
| package com.facebook.openwifirrm.modules; | package com.facebook.openwifirrm.modules; | ||||||
|  |  | ||||||
| import static org.junit.jupiter.api.Assertions.assertNull; | 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; | import org.junit.jupiter.api.Test; | ||||||
|  |  | ||||||
| @@ -23,48 +23,48 @@ public class RRMSchedulerTest { | |||||||
| 		assertNull(RRMScheduler.parseIntoQuartzCron("* * * * * * * *")); | 		assertNull(RRMScheduler.parseIntoQuartzCron("* * * * * * * *")); | ||||||
|  |  | ||||||
| 		// correct (6 fields) | 		// correct (6 fields) | ||||||
| 		assertEquals( | 		assertArrayEquals( | ||||||
| 			"* * * ? * *", | 			new String[] { "* * * ? * *" }, | ||||||
| 			RRMScheduler.parseIntoQuartzCron("* * * * * *") | 			RRMScheduler.parseIntoQuartzCron("* * * * * *") | ||||||
| 		); | 		); | ||||||
|  |  | ||||||
| 		// correct (7 fields) | 		// correct (7 fields) | ||||||
| 		assertEquals( | 		assertArrayEquals( | ||||||
| 			"* * * ? * * *", | 			new String[] { "* * * ? * * *" }, | ||||||
| 			RRMScheduler.parseIntoQuartzCron("* * * * * * *") | 			RRMScheduler.parseIntoQuartzCron("* * * * * * *") | ||||||
| 		); | 		); | ||||||
|  |  | ||||||
| 		// correct value other than * for day of month | 		// correct value other than * for day of month | ||||||
| 		assertEquals( | 		assertArrayEquals( | ||||||
| 			"* * * 1 * ?", | 			new String[] { "* * * 1 * ?" }, | ||||||
| 			RRMScheduler.parseIntoQuartzCron("* * * 1 * *") | 			RRMScheduler.parseIntoQuartzCron("* * * 1 * *") | ||||||
| 		); | 		); | ||||||
| 		assertEquals( | 		assertArrayEquals( | ||||||
| 			"* * * 1 * ? *", | 			new String[] { "* * * 1 * ? *" }, | ||||||
| 			RRMScheduler.parseIntoQuartzCron("* * * 1 * * *") | 			RRMScheduler.parseIntoQuartzCron("* * * 1 * * *") | ||||||
| 		); | 		); | ||||||
| 		assertEquals( | 		assertArrayEquals( | ||||||
| 			"* * * 1/2 * ?", | 			new String[] { "* * * 1/2 * ?" }, | ||||||
| 			RRMScheduler.parseIntoQuartzCron("* * * 1/2 * *") | 			RRMScheduler.parseIntoQuartzCron("* * * 1/2 * *") | ||||||
| 		); | 		); | ||||||
| 		assertEquals( | 		assertArrayEquals( | ||||||
| 			"* * * 1/2 * ? *", | 			new String[] { "* * * 1/2 * ? *" }, | ||||||
| 			RRMScheduler.parseIntoQuartzCron("* * * 1/2 * * *") | 			RRMScheduler.parseIntoQuartzCron("* * * 1/2 * * *") | ||||||
| 		); | 		); | ||||||
| 		assertEquals( | 		assertArrayEquals( | ||||||
| 			"* * * 1-2 * ?", | 			new String[] { "* * * 1-2 * ?" }, | ||||||
| 			RRMScheduler.parseIntoQuartzCron("* * * 1-2 * *") | 			RRMScheduler.parseIntoQuartzCron("* * * 1-2 * *") | ||||||
| 		); | 		); | ||||||
| 		assertEquals( | 		assertArrayEquals( | ||||||
| 			"* * * 1-2 * ? *", | 			new String[] { "* * * 1-2 * ? *" }, | ||||||
| 			RRMScheduler.parseIntoQuartzCron("* * * 1-2 * * *") | 			RRMScheduler.parseIntoQuartzCron("* * * 1-2 * * *") | ||||||
| 		); | 		); | ||||||
| 		assertEquals( | 		assertArrayEquals( | ||||||
| 			"* * * 1,2 * ?", | 			new String[] { "* * * 1,2 * ?" }, | ||||||
| 			RRMScheduler.parseIntoQuartzCron("* * * 1,2 * *") | 			RRMScheduler.parseIntoQuartzCron("* * * 1,2 * *") | ||||||
| 		); | 		); | ||||||
| 		assertEquals( | 		assertArrayEquals( | ||||||
| 			"* * * 1,2 * ? *", | 			new String[] { "* * * 1,2 * ? *" }, | ||||||
| 			RRMScheduler.parseIntoQuartzCron("* * * 1,2 * * *") | 			RRMScheduler.parseIntoQuartzCron("* * * 1,2 * * *") | ||||||
| 		); | 		); | ||||||
|  |  | ||||||
| @@ -79,70 +79,70 @@ public class RRMSchedulerTest { | |||||||
| 		assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0,1 * * *")); | 		assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0,1 * * *")); | ||||||
|  |  | ||||||
| 		// correct value other than * for day of month | 		// correct value other than * for day of month | ||||||
| 		assertEquals( | 		assertArrayEquals( | ||||||
| 			"* * * ? * 1", | 			new String[] { "* * * ? * 1" }, | ||||||
| 			RRMScheduler.parseIntoQuartzCron("* * * * * 1") | 			RRMScheduler.parseIntoQuartzCron("* * * * * 1") | ||||||
| 		); | 		); | ||||||
| 		assertEquals( | 		assertArrayEquals( | ||||||
| 			"* * * ? * 1 *", | 			new String[] { "* * * ? * 1 *" }, | ||||||
| 			RRMScheduler.parseIntoQuartzCron("* * * * * 1 *") | 			RRMScheduler.parseIntoQuartzCron("* * * * * 1 *") | ||||||
| 		); | 		); | ||||||
| 		assertEquals( | 		assertArrayEquals( | ||||||
| 			"* * * ? * 1/3", | 			new String[] { "* * * ? * 1/3" }, | ||||||
| 			RRMScheduler.parseIntoQuartzCron("* * * * * 1/3") | 			RRMScheduler.parseIntoQuartzCron("* * * * * 1/3") | ||||||
| 		); | 		); | ||||||
| 		assertEquals( | 		assertArrayEquals( | ||||||
| 			"* * * ? * 1/3 *", | 			new String[] { "* * * ? * 1/3 *" }, | ||||||
| 			RRMScheduler.parseIntoQuartzCron("* * * * * 1/3 *") | 			RRMScheduler.parseIntoQuartzCron("* * * * * 1/3 *") | ||||||
| 		); | 		); | ||||||
| 		assertEquals( | 		assertArrayEquals( | ||||||
| 			"* * * ? * 1-3", | 			new String[] { "* * * ? * 1-3" }, | ||||||
| 			RRMScheduler.parseIntoQuartzCron("* * * * * 1-3") | 			RRMScheduler.parseIntoQuartzCron("* * * * * 1-3") | ||||||
| 		); | 		); | ||||||
| 		assertEquals( | 		assertArrayEquals( | ||||||
| 			"* * * ? * 1-3 *", | 			new String[] { "* * * ? * 1-3 *" }, | ||||||
| 			RRMScheduler.parseIntoQuartzCron("* * * * * 1-3 *") | 			RRMScheduler.parseIntoQuartzCron("* * * * * 1-3 *") | ||||||
| 		); | 		); | ||||||
| 		assertEquals( | 		assertArrayEquals( | ||||||
| 			"* * * ? * 1,3", | 			new String[] { "* * * ? * 1,3" }, | ||||||
| 			RRMScheduler.parseIntoQuartzCron("* * * * * 1,3") | 			RRMScheduler.parseIntoQuartzCron("* * * * * 1,3") | ||||||
| 		); | 		); | ||||||
| 		assertEquals( | 		assertArrayEquals( | ||||||
| 			"* * * ? * 1,3 *", | 			new String[] { "* * * ? * 1,3 *" }, | ||||||
| 			RRMScheduler.parseIntoQuartzCron("* * * * * 1,3 *") | 			RRMScheduler.parseIntoQuartzCron("* * * * * 1,3 *") | ||||||
| 		); | 		); | ||||||
|  |  | ||||||
| 		// correct value other than * for day of month, make sure 0 turns into 7 | 		// correct value other than * for day of month, make sure 0 turns into 7 | ||||||
| 		assertEquals( | 		assertArrayEquals( | ||||||
| 			"* * * ? * 7", | 			new String[] { "* * * ? * 7" }, | ||||||
| 			RRMScheduler.parseIntoQuartzCron("* * * * * 0") | 			RRMScheduler.parseIntoQuartzCron("* * * * * 0") | ||||||
| 		); | 		); | ||||||
| 		assertEquals( | 		assertArrayEquals( | ||||||
| 			"* * * ? * 7 *", | 			new String[] { "* * * ? * 7 *" }, | ||||||
| 			RRMScheduler.parseIntoQuartzCron("* * * * * 0 *") | 			RRMScheduler.parseIntoQuartzCron("* * * * * 0 *") | ||||||
| 		); | 		); | ||||||
| 		assertEquals( | 		assertArrayEquals( | ||||||
| 			"* * * ? * 1/7", | 			new String[] { "* * * ? * 1/7" }, | ||||||
| 			RRMScheduler.parseIntoQuartzCron("* * * * * 1/0") | 			RRMScheduler.parseIntoQuartzCron("* * * * * 1/0") | ||||||
| 		); | 		); | ||||||
| 		assertEquals( | 		assertArrayEquals( | ||||||
| 			"* * * ? * 1/7 *", | 			new String[] { "* * * ? * 1/7 *" }, | ||||||
| 			RRMScheduler.parseIntoQuartzCron("* * * * * 1/0 *") | 			RRMScheduler.parseIntoQuartzCron("* * * * * 1/0 *") | ||||||
| 		); | 		); | ||||||
| 		assertEquals( | 		assertArrayEquals( | ||||||
| 			"* * * ? * 1-7", | 			new String[] { "* * * ? * 1-7" }, | ||||||
| 			RRMScheduler.parseIntoQuartzCron("* * * * * 1-0") | 			RRMScheduler.parseIntoQuartzCron("* * * * * 1-0") | ||||||
| 		); | 		); | ||||||
| 		assertEquals( | 		assertArrayEquals( | ||||||
| 			"* * * ? * 1-7 *", | 			new String[] { "* * * ? * 1-7 *" }, | ||||||
| 			RRMScheduler.parseIntoQuartzCron("* * * * * 1-0 *") | 			RRMScheduler.parseIntoQuartzCron("* * * * * 1-0 *") | ||||||
| 		); | 		); | ||||||
| 		assertEquals( | 		assertArrayEquals( | ||||||
| 			"* * * ? * 1,7", | 			new String[] { "* * * ? * 1,7" }, | ||||||
| 			RRMScheduler.parseIntoQuartzCron("* * * * * 1,0") | 			RRMScheduler.parseIntoQuartzCron("* * * * * 1,0") | ||||||
| 		); | 		); | ||||||
| 		assertEquals( | 		assertArrayEquals( | ||||||
| 			"* * * ? * 1,7 *", | 			new String[] { "* * * ? * 1,7 *" }, | ||||||
| 			RRMScheduler.parseIntoQuartzCron("* * * * * 1,0 *") | 			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")); | 		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; | 		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 | 	 * Create an array with two radio info entries (2G and 5G), with the given | ||||||
| 	 * tx powers and channels. | 	 * tx powers and channels. | ||||||
| @@ -397,23 +417,15 @@ public class TestUtils { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	/** Create an element of {@link State#radios}. */ | 	/** Create an element of {@link State#radios}. */ | ||||||
| 	private static JsonObject createStateRadio() { | 	private static State.Radio createStateRadio() { | ||||||
| 		// @formatter:off | 		State.Radio radio = new State.Radio(); | ||||||
| 		return gson.fromJson( | 		radio.active_ms = 564328; | ||||||
| 			String.format( | 		radio.busy_ms = 36998; | ||||||
| 				"    {\n" + | 		radio.noise = 4294967193L; | ||||||
| 				"      \"active_ms\": 564328,\n" + | 		radio.phy = "platform/soc/c000000.wifi"; | ||||||
| 				"      \"busy_ms\": 36998,\n" + | 		radio.receive_ms = 28; | ||||||
| 				"      \"noise\": 4294967193,\n" + | 		radio.transmit_ms = 4893; | ||||||
| 				"      \"phy\": \"platform/soc/c000000.wifi\",\n" + | 		return radio; | ||||||
| 				"      \"receive_ms\": 28,\n" + |  | ||||||
| 				"      \"temperature\": 45,\n" + |  | ||||||
| 				"      \"transmit_ms\": 4893\n" + |  | ||||||
| 				"    }\n" |  | ||||||
| 			), |  | ||||||
| 			JsonObject.class |  | ||||||
| 		); |  | ||||||
| 		// @formatter:on |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	/** Create a {@code State.Unit}. */ | 	/** Create a {@code State.Unit}. */ | ||||||
| @@ -477,18 +489,18 @@ public class TestUtils { | |||||||
| 			state.interfaces[index] = createUpStateInterface(index); | 			state.interfaces[index] = createUpStateInterface(index); | ||||||
| 		} | 		} | ||||||
| 		state.interfaces[numRadios] = createDownStateInterface(numRadios); | 		state.interfaces[numRadios] = createDownStateInterface(numRadios); | ||||||
| 		state.radios = new JsonObject[numRadios]; | 		state.radios = new State.Radio[numRadios]; | ||||||
| 		for (int i = 0; i < numRadios; i++) { | 		for (int i = 0; i < numRadios; i++) { | ||||||
| 			state.radios[i] = createStateRadio(); | 			state.radios[i] = createStateRadio(); | ||||||
| 			state.radios[i].addProperty("channel", channels[i]); | 			state.radios[i].channel = channels[i]; | ||||||
| 			state.radios[i].addProperty("channel_width", channelWidths[i]); | 			state.radios[i].channel_width = Integer.toString(channelWidths[i]); | ||||||
| 			state.radios[i].addProperty("tx_power", txPowers[i]); | 			state.radios[i].tx_power = txPowers[i]; | ||||||
| 			state.interfaces[i].ssids[0].bssid = bssids[i]; | 			state.interfaces[i].ssids[0].bssid = bssids[i]; | ||||||
| 			state.interfaces[i].ssids[0].associations = | 			state.interfaces[i].ssids[0].associations = | ||||||
| 				new State.Interface.SSID.Association[clientRssis[i].length]; | 				new State.Interface.SSID.Association[clientRssis[i].length]; | ||||||
| 			for (int j = 0; j < clientRssis[i].length; j++) { | 			for (int j = 0; j < clientRssis[i].length; j++) { | ||||||
| 				state.interfaces[i].ssids[0].associations[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 = | 				state.interfaces[i].ssids[0].associations[j].rssi = | ||||||
| 					clientRssis[i][j]; | 					clientRssis[i][j]; | ||||||
| 			} | 			} | ||||||
|   | |||||||
| @@ -51,7 +51,7 @@ public class LeastUsedChannelOptimizerTest { | |||||||
|  |  | ||||||
| 		// A -> No APs on current channel, so stay on it (48) | 		// A -> No APs on current channel, so stay on it (48) | ||||||
| 		int aExpectedChannel = 48; | 		int aExpectedChannel = 48; | ||||||
| 		dataModel.latestDeviceStatus.put( | 		dataModel.latestDeviceStatusRadios.put( | ||||||
| 			deviceA, | 			deviceA, | ||||||
| 			TestUtils.createDeviceStatus(band, aExpectedChannel) | 			TestUtils.createDeviceStatus(band, aExpectedChannel) | ||||||
| 		); | 		); | ||||||
| @@ -73,7 +73,7 @@ public class LeastUsedChannelOptimizerTest { | |||||||
| 		LinkedList<Integer> channelsB = new LinkedList<>(); | 		LinkedList<Integer> channelsB = new LinkedList<>(); | ||||||
| 		channelsB.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band)); | 		channelsB.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band)); | ||||||
| 		int bExpectedChannel = channelsB.removeLast(); | 		int bExpectedChannel = channelsB.removeLast(); | ||||||
| 		dataModel.latestDeviceStatus.put( | 		dataModel.latestDeviceStatusRadios.put( | ||||||
| 			deviceB, | 			deviceB, | ||||||
| 			TestUtils.createDeviceStatus(band, 40) | 			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)); | ||||||
| 		channelsC.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band)); | 		channelsC.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band)); | ||||||
| 		int cExpectedChannel = channelsC.removeFirst(); | 		int cExpectedChannel = channelsC.removeFirst(); | ||||||
| 		dataModel.latestDeviceStatus.put( | 		dataModel.latestDeviceStatusRadios.put( | ||||||
| 			deviceC, | 			deviceC, | ||||||
| 			TestUtils.createDeviceStatus(band, 149) | 			TestUtils.createDeviceStatus(band, 149) | ||||||
| 		); | 		); | ||||||
| @@ -138,7 +138,7 @@ public class LeastUsedChannelOptimizerTest { | |||||||
|  |  | ||||||
| 		// A -> No APs on current channel, so stay on it (1) | 		// A -> No APs on current channel, so stay on it (1) | ||||||
| 		int aExpectedChannel = 1; | 		int aExpectedChannel = 1; | ||||||
| 		dataModel.latestDeviceStatus.put( | 		dataModel.latestDeviceStatusRadios.put( | ||||||
| 			deviceA, | 			deviceA, | ||||||
| 			TestUtils.createDeviceStatus(band, aExpectedChannel) | 			TestUtils.createDeviceStatus(band, aExpectedChannel) | ||||||
| 		); | 		); | ||||||
| @@ -160,7 +160,7 @@ public class LeastUsedChannelOptimizerTest { | |||||||
| 		LinkedList<Integer> channelsB = new LinkedList<>(); | 		LinkedList<Integer> channelsB = new LinkedList<>(); | ||||||
| 		channelsB.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band)); | 		channelsB.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band)); | ||||||
| 		int bExpectedChannel = channelsB.removeLast(); | 		int bExpectedChannel = channelsB.removeLast(); | ||||||
| 		dataModel.latestDeviceStatus.put( | 		dataModel.latestDeviceStatusRadios.put( | ||||||
| 			deviceB, | 			deviceB, | ||||||
| 			TestUtils.createDeviceStatus(band, 6) | 			TestUtils.createDeviceStatus(band, 6) | ||||||
| 		); | 		); | ||||||
| @@ -178,7 +178,7 @@ public class LeastUsedChannelOptimizerTest { | |||||||
|  |  | ||||||
| 		// C -> Assigned to only free prioritized channel (1) | 		// C -> Assigned to only free prioritized channel (1) | ||||||
| 		int cExpectedChannel = 1; | 		int cExpectedChannel = 1; | ||||||
| 		dataModel.latestDeviceStatus.put( | 		dataModel.latestDeviceStatusRadios.put( | ||||||
| 			deviceC, | 			deviceC, | ||||||
| 			TestUtils.createDeviceStatus(band, 6) | 			TestUtils.createDeviceStatus(band, 6) | ||||||
| 		); | 		); | ||||||
| @@ -231,7 +231,7 @@ public class LeastUsedChannelOptimizerTest { | |||||||
|  |  | ||||||
| 		// A, B, C should just be assigned to the same userChannel | 		// A, B, C should just be assigned to the same userChannel | ||||||
| 		int aExpectedChannel = 48; | 		int aExpectedChannel = 48; | ||||||
| 		dataModel.latestDeviceStatus.put( | 		dataModel.latestDeviceStatusRadios.put( | ||||||
| 			deviceA, | 			deviceA, | ||||||
| 			TestUtils.createDeviceStatus(band, aExpectedChannel) | 			TestUtils.createDeviceStatus(band, aExpectedChannel) | ||||||
| 		); | 		); | ||||||
| @@ -252,7 +252,7 @@ public class LeastUsedChannelOptimizerTest { | |||||||
| 		LinkedList<Integer> channelsB = new LinkedList<>(); | 		LinkedList<Integer> channelsB = new LinkedList<>(); | ||||||
| 		channelsB.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band)); | 		channelsB.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band)); | ||||||
| 		channelsB.removeLast(); | 		channelsB.removeLast(); | ||||||
| 		dataModel.latestDeviceStatus.put( | 		dataModel.latestDeviceStatusRadios.put( | ||||||
| 			deviceB, | 			deviceB, | ||||||
| 			TestUtils.createDeviceStatus(band, 40) | 			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.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band)); | 		channelsC.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band)); | ||||||
| 		channelsC.removeFirst(); | 		channelsC.removeFirst(); | ||||||
| 		dataModel.latestDeviceStatus.put( | 		dataModel.latestDeviceStatusRadios.put( | ||||||
| 			deviceC, | 			deviceC, | ||||||
| 			TestUtils.createDeviceStatus(band, 149) | 			TestUtils.createDeviceStatus(band, 149) | ||||||
| 		); | 		); | ||||||
| @@ -324,7 +324,7 @@ public class LeastUsedChannelOptimizerTest { | |||||||
| 		// A -> No APs on current channel and the current channel is in allowedChannels, | 		// A -> No APs on current channel and the current channel is in allowedChannels, | ||||||
| 		// so stay on it (48) | 		// so stay on it (48) | ||||||
| 		int aExpectedChannel = 48; | 		int aExpectedChannel = 48; | ||||||
| 		dataModel.latestDeviceStatus.put( | 		dataModel.latestDeviceStatusRadios.put( | ||||||
| 			deviceA, | 			deviceA, | ||||||
| 			TestUtils.createDeviceStatus(band, aExpectedChannel) | 			TestUtils.createDeviceStatus(band, aExpectedChannel) | ||||||
| 		); | 		); | ||||||
| @@ -347,7 +347,7 @@ public class LeastUsedChannelOptimizerTest { | |||||||
| 		LinkedList<Integer> channelsB = new LinkedList<>(); | 		LinkedList<Integer> channelsB = new LinkedList<>(); | ||||||
| 		channelsB.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band)); | 		channelsB.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band)); | ||||||
| 		channelsB.removeLast(); | 		channelsB.removeLast(); | ||||||
| 		dataModel.latestDeviceStatus.put( | 		dataModel.latestDeviceStatusRadios.put( | ||||||
| 			deviceB, | 			deviceB, | ||||||
| 			TestUtils.createDeviceStatus(band, 40) | 			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.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band)); | 		channelsC.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band)); | ||||||
| 		channelsC.removeFirst(); | 		channelsC.removeFirst(); | ||||||
| 		dataModel.latestDeviceStatus.put( | 		dataModel.latestDeviceStatusRadios.put( | ||||||
| 			deviceC, | 			deviceC, | ||||||
| 			TestUtils.createDeviceStatus(band, 149) | 			TestUtils.createDeviceStatus(band, 149) | ||||||
| 		); | 		); | ||||||
| @@ -414,7 +414,7 @@ public class LeastUsedChannelOptimizerTest { | |||||||
|  |  | ||||||
| 		// A -> No APs on current channel, so stay on it (48) | 		// A -> No APs on current channel, so stay on it (48) | ||||||
| 		int aExpectedChannel = 157; | 		int aExpectedChannel = 157; | ||||||
| 		dataModel.latestDeviceStatus.put( | 		dataModel.latestDeviceStatusRadios.put( | ||||||
| 			deviceA, | 			deviceA, | ||||||
| 			TestUtils.createDeviceStatus(band, aExpectedChannel) | 			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)); | ||||||
| 		channelsB.addAll(Arrays.asList(40, 48, 153, 161)); | 		channelsB.addAll(Arrays.asList(40, 48, 153, 161)); | ||||||
| 		int bExpectedChannel = channelsB.removeLast() - 4; // upper extension | 		int bExpectedChannel = channelsB.removeLast() - 4; // upper extension | ||||||
| 		dataModel.latestDeviceStatus.put( | 		dataModel.latestDeviceStatusRadios.put( | ||||||
| 			deviceB, | 			deviceB, | ||||||
| 			TestUtils.createDeviceStatus(band, 40) | 			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)); | ||||||
| 		channelsC.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band)); | 		channelsC.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band)); | ||||||
| 		int cExpectedChannel = channelsC.removeFirst(); | 		int cExpectedChannel = channelsC.removeFirst(); | ||||||
| 		dataModel.latestDeviceStatus.put( | 		dataModel.latestDeviceStatusRadios.put( | ||||||
| 			deviceC, | 			deviceC, | ||||||
| 			TestUtils.createDeviceStatus(band, 149) | 			TestUtils.createDeviceStatus(band, 149) | ||||||
| 		); | 		); | ||||||
| @@ -479,7 +479,7 @@ public class LeastUsedChannelOptimizerTest { | |||||||
| 		LinkedList<Integer> channelsD = new LinkedList<>(); | 		LinkedList<Integer> channelsD = new LinkedList<>(); | ||||||
| 		channelsD.addAll(Arrays.asList(36, 44, 149, 157)); | 		channelsD.addAll(Arrays.asList(36, 44, 149, 157)); | ||||||
| 		int dExpectedChannel = channelsD.removeLast(); | 		int dExpectedChannel = channelsD.removeLast(); | ||||||
| 		dataModel.latestDeviceStatus.put( | 		dataModel.latestDeviceStatusRadios.put( | ||||||
| 			deviceD, | 			deviceD, | ||||||
| 			TestUtils.createDeviceStatus(band, 40) | 			TestUtils.createDeviceStatus(band, 40) | ||||||
| 		); | 		); | ||||||
| @@ -532,7 +532,7 @@ public class LeastUsedChannelOptimizerTest { | |||||||
|  |  | ||||||
| 		// A -> No APs on current channel, so stay on it (36) | 		// A -> No APs on current channel, so stay on it (36) | ||||||
| 		int aExpectedChannel = 36; | 		int aExpectedChannel = 36; | ||||||
| 		dataModel.latestDeviceStatus.put( | 		dataModel.latestDeviceStatusRadios.put( | ||||||
| 			deviceA, | 			deviceA, | ||||||
| 			TestUtils.createDeviceStatus(band, aExpectedChannel) | 			TestUtils.createDeviceStatus(band, aExpectedChannel) | ||||||
| 		); | 		); | ||||||
| @@ -554,7 +554,7 @@ public class LeastUsedChannelOptimizerTest { | |||||||
| 		LinkedList<Integer> channelsB = new LinkedList<>(); | 		LinkedList<Integer> channelsB = new LinkedList<>(); | ||||||
| 		channelsB.addAll(Arrays.asList(40, 48, 149)); | 		channelsB.addAll(Arrays.asList(40, 48, 149)); | ||||||
| 		int bExpectedChannel = channelsB.removeLast(); | 		int bExpectedChannel = channelsB.removeLast(); | ||||||
| 		dataModel.latestDeviceStatus.put( | 		dataModel.latestDeviceStatusRadios.put( | ||||||
| 			deviceB, | 			deviceB, | ||||||
| 			TestUtils.createDeviceStatus(band, 36) | 			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)); | ||||||
| 		channelsC.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band)); | 		channelsC.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band)); | ||||||
| 		int cExpectedChannel = channelsC.removeFirst(); | 		int cExpectedChannel = channelsC.removeFirst(); | ||||||
| 		dataModel.latestDeviceStatus.put( | 		dataModel.latestDeviceStatusRadios.put( | ||||||
| 			deviceC, | 			deviceC, | ||||||
| 			TestUtils.createDeviceStatus(band, 149) | 			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)); | ||||||
| 		channelsD.addAll(Arrays.asList(40, 48, 153, 161)); | 		channelsD.addAll(Arrays.asList(40, 48, 153, 161)); | ||||||
| 		int dExpectedChannel = channelsD.removeLast() - 12; | 		int dExpectedChannel = channelsD.removeLast() - 12; | ||||||
| 		dataModel.latestDeviceStatus.put( | 		dataModel.latestDeviceStatusRadios.put( | ||||||
| 			deviceD, | 			deviceD, | ||||||
| 			TestUtils.createDeviceStatus(band, 36) | 			TestUtils.createDeviceStatus(band, 36) | ||||||
| 		); | 		); | ||||||
| @@ -622,7 +622,7 @@ public class LeastUsedChannelOptimizerTest { | |||||||
| 			.put(UCentralConstants.BAND_5G, Arrays.asList(48, 165)); | 			.put(UCentralConstants.BAND_5G, Arrays.asList(48, 165)); | ||||||
| 		deviceDataManager.setDeviceApConfig(deviceE, apConfig); | 		deviceDataManager.setDeviceApConfig(deviceE, apConfig); | ||||||
| 		int eExpectedChannel = 36; | 		int eExpectedChannel = 36; | ||||||
| 		dataModel.latestDeviceStatus.put( | 		dataModel.latestDeviceStatusRadios.put( | ||||||
| 			deviceE, | 			deviceE, | ||||||
| 			TestUtils.createDeviceStatus(band, eExpectedChannel) | 			TestUtils.createDeviceStatus(band, eExpectedChannel) | ||||||
| 		); | 		); | ||||||
| @@ -668,7 +668,7 @@ public class LeastUsedChannelOptimizerTest { | |||||||
|  |  | ||||||
| 		// A -> No APs on current channel, so stay on it (48) | 		// A -> No APs on current channel, so stay on it (48) | ||||||
| 		int aExpectedChannel = 48; | 		int aExpectedChannel = 48; | ||||||
| 		dataModel.latestDeviceStatus.put( | 		dataModel.latestDeviceStatusRadios.put( | ||||||
| 			deviceA, | 			deviceA, | ||||||
| 			TestUtils.createDeviceStatus(band, aExpectedChannel) | 			TestUtils.createDeviceStatus(band, aExpectedChannel) | ||||||
| 		); | 		); | ||||||
| @@ -689,7 +689,7 @@ public class LeastUsedChannelOptimizerTest { | |||||||
| 		// B -> Same setting as A, but the scan results are bandwidth aware | 		// B -> Same setting as A, but the scan results are bandwidth aware | ||||||
| 		// Assign to only free channel (165) | 		// Assign to only free channel (165) | ||||||
| 		int bExpectedChannel = 165; | 		int bExpectedChannel = 165; | ||||||
| 		dataModel.latestDeviceStatus.put( | 		dataModel.latestDeviceStatusRadios.put( | ||||||
| 			deviceB, | 			deviceB, | ||||||
| 			TestUtils.createDeviceStatus(band, 48) | 			TestUtils.createDeviceStatus(band, 48) | ||||||
| 		); | 		); | ||||||
| @@ -721,7 +721,7 @@ public class LeastUsedChannelOptimizerTest { | |||||||
| 		channelsC1.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band)); | 		channelsC1.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band)); | ||||||
| 		channelsC2.addAll(Arrays.asList(36, 157, 165)); | 		channelsC2.addAll(Arrays.asList(36, 157, 165)); | ||||||
| 		int cExpectedChannel = channelsC1.removeFirst(); | 		int cExpectedChannel = channelsC1.removeFirst(); | ||||||
| 		dataModel.latestDeviceStatus.put( | 		dataModel.latestDeviceStatusRadios.put( | ||||||
| 			deviceC, | 			deviceC, | ||||||
| 			TestUtils.createDeviceStatus(band, 149) | 			TestUtils.createDeviceStatus(band, 149) | ||||||
| 		); | 		); | ||||||
|   | |||||||
| @@ -54,11 +54,11 @@ public class RandomChannelInitializerTest { | |||||||
| 			deviceB, | 			deviceB, | ||||||
| 			TestUtils.createState(11, channelWidth, deviceBBssid) | 			TestUtils.createState(11, channelWidth, deviceBBssid) | ||||||
| 		); | 		); | ||||||
| 		dataModel.latestDeviceStatus.put( | 		dataModel.latestDeviceStatusRadios.put( | ||||||
| 			deviceA, | 			deviceA, | ||||||
| 			TestUtils.createDeviceStatus(band, 7) | 			TestUtils.createDeviceStatus(band, 7) | ||||||
| 		); | 		); | ||||||
| 		dataModel.latestDeviceStatus.put( | 		dataModel.latestDeviceStatusRadios.put( | ||||||
| 			deviceB, | 			deviceB, | ||||||
| 			TestUtils.createDeviceStatus(band, 8) | 			TestUtils.createDeviceStatus(band, 8) | ||||||
| 		); | 		); | ||||||
| @@ -99,11 +99,11 @@ public class RandomChannelInitializerTest { | |||||||
| 			deviceB, | 			deviceB, | ||||||
| 			TestUtils.createState(11, channelWidth, deviceBBssid) | 			TestUtils.createState(11, channelWidth, deviceBBssid) | ||||||
| 		); | 		); | ||||||
| 		dataModel.latestDeviceStatus.put( | 		dataModel.latestDeviceStatusRadios.put( | ||||||
| 			deviceA, | 			deviceA, | ||||||
| 			TestUtils.createDeviceStatus(band, 7) | 			TestUtils.createDeviceStatus(band, 7) | ||||||
| 		); | 		); | ||||||
| 		dataModel.latestDeviceStatus.put( | 		dataModel.latestDeviceStatusRadios.put( | ||||||
| 			deviceB, | 			deviceB, | ||||||
| 			TestUtils.createDeviceStatus(band, 8) | 			TestUtils.createDeviceStatus(band, 8) | ||||||
| 		); | 		); | ||||||
|   | |||||||
| @@ -52,7 +52,7 @@ public class UnmanagedApAwareChannelOptimizerTest { | |||||||
|  |  | ||||||
| 		// A -> No APs on current channel, so stay on it (48) | 		// A -> No APs on current channel, so stay on it (48) | ||||||
| 		int aExpectedChannel = 48; | 		int aExpectedChannel = 48; | ||||||
| 		dataModel.latestDeviceStatus.put( | 		dataModel.latestDeviceStatusRadios.put( | ||||||
| 			deviceA, | 			deviceA, | ||||||
| 			TestUtils.createDeviceStatus(band, aExpectedChannel) | 			TestUtils.createDeviceStatus(band, aExpectedChannel) | ||||||
| 		); | 		); | ||||||
| @@ -76,7 +76,7 @@ public class UnmanagedApAwareChannelOptimizerTest { | |||||||
| 		LinkedList<Integer> channelsB = new LinkedList<>(); | 		LinkedList<Integer> channelsB = new LinkedList<>(); | ||||||
| 		channelsB.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band)); | 		channelsB.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band)); | ||||||
| 		int bExpectedChannel = channelsB.removeLast(); | 		int bExpectedChannel = channelsB.removeLast(); | ||||||
| 		dataModel.latestDeviceStatus.put( | 		dataModel.latestDeviceStatusRadios.put( | ||||||
| 			deviceB, | 			deviceB, | ||||||
| 			TestUtils.createDeviceStatus(band, 40) | 			TestUtils.createDeviceStatus(band, 40) | ||||||
| 		); | 		); | ||||||
| @@ -110,7 +110,7 @@ public class UnmanagedApAwareChannelOptimizerTest { | |||||||
| 			) | 			) | ||||||
| 		); | 		); | ||||||
| 		int cExpectedChannel = 48; | 		int cExpectedChannel = 48; | ||||||
| 		dataModel.latestDeviceStatus.put( | 		dataModel.latestDeviceStatusRadios.put( | ||||||
| 			deviceC, | 			deviceC, | ||||||
| 			TestUtils.createDeviceStatus(band, 149) | 			TestUtils.createDeviceStatus(band, 149) | ||||||
| 		); | 		); | ||||||
| @@ -156,7 +156,7 @@ public class UnmanagedApAwareChannelOptimizerTest { | |||||||
|  |  | ||||||
| 		// A -> No APs on current channel, so stay on it (1) | 		// A -> No APs on current channel, so stay on it (1) | ||||||
| 		int aExpectedChannel = 1; | 		int aExpectedChannel = 1; | ||||||
| 		dataModel.latestDeviceStatus.put( | 		dataModel.latestDeviceStatusRadios.put( | ||||||
| 			deviceA, | 			deviceA, | ||||||
| 			TestUtils.createDeviceStatus(band, aExpectedChannel) | 			TestUtils.createDeviceStatus(band, aExpectedChannel) | ||||||
| 		); | 		); | ||||||
| @@ -178,7 +178,7 @@ public class UnmanagedApAwareChannelOptimizerTest { | |||||||
| 		LinkedList<Integer> channelsB = new LinkedList<>(); | 		LinkedList<Integer> channelsB = new LinkedList<>(); | ||||||
| 		channelsB.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band)); | 		channelsB.addAll(ChannelOptimizer.AVAILABLE_CHANNELS_BAND.get(band)); | ||||||
| 		int bExpectedChannel = channelsB.removeLast(); | 		int bExpectedChannel = channelsB.removeLast(); | ||||||
| 		dataModel.latestDeviceStatus.put( | 		dataModel.latestDeviceStatusRadios.put( | ||||||
| 			deviceB, | 			deviceB, | ||||||
| 			TestUtils.createDeviceStatus(band, 6) | 			TestUtils.createDeviceStatus(band, 6) | ||||||
| 		); | 		); | ||||||
| @@ -196,7 +196,7 @@ public class UnmanagedApAwareChannelOptimizerTest { | |||||||
|  |  | ||||||
| 		// C -> Assigned to only free prioritized channel (1) | 		// C -> Assigned to only free prioritized channel (1) | ||||||
| 		int cExpectedChannel = 1; | 		int cExpectedChannel = 1; | ||||||
| 		dataModel.latestDeviceStatus.put( | 		dataModel.latestDeviceStatusRadios.put( | ||||||
| 			deviceC, | 			deviceC, | ||||||
| 			TestUtils.createDeviceStatus(band, 6) | 			TestUtils.createDeviceStatus(band, 6) | ||||||
| 		); | 		); | ||||||
|   | |||||||
| @@ -128,7 +128,7 @@ public class LocationBasedOptimalTPCTest { | |||||||
|  |  | ||||||
| 		DataModel dataModel = new DataModel(); | 		DataModel dataModel = new DataModel(); | ||||||
| 		for (String device : Arrays.asList(deviceA, deviceB, deviceC)) { | 		for (String device : Arrays.asList(deviceA, deviceB, deviceC)) { | ||||||
| 			dataModel.latestDeviceStatus.put( | 			dataModel.latestDeviceStatusRadios.put( | ||||||
| 				device, | 				device, | ||||||
| 				TestUtils.createDeviceStatus(UCentralConstants.BANDS) | 				TestUtils.createDeviceStatus(UCentralConstants.BANDS) | ||||||
| 			); | 			); | ||||||
| @@ -195,7 +195,7 @@ public class LocationBasedOptimalTPCTest { | |||||||
|  |  | ||||||
| 		DataModel dataModel2 = new DataModel(); | 		DataModel dataModel2 = new DataModel(); | ||||||
| 		for (String device : Arrays.asList(deviceA, deviceB)) { | 		for (String device : Arrays.asList(deviceA, deviceB)) { | ||||||
| 			dataModel2.latestDeviceStatus.put( | 			dataModel2.latestDeviceStatusRadios.put( | ||||||
| 				device, | 				device, | ||||||
| 				TestUtils.createDeviceStatus(UCentralConstants.BANDS) | 				TestUtils.createDeviceStatus(UCentralConstants.BANDS) | ||||||
| 			); | 			); | ||||||
| @@ -213,7 +213,7 @@ public class LocationBasedOptimalTPCTest { | |||||||
| 				) | 				) | ||||||
| 			); | 			); | ||||||
| 		} | 		} | ||||||
| 		dataModel2.latestDeviceStatus | 		dataModel2.latestDeviceStatusRadios | ||||||
| 			.put( | 			.put( | ||||||
| 				deviceC, | 				deviceC, | ||||||
| 				TestUtils.createDeviceStatus( | 				TestUtils.createDeviceStatus( | ||||||
| @@ -304,7 +304,7 @@ public class LocationBasedOptimalTPCTest { | |||||||
|  |  | ||||||
| 		DataModel dataModel2 = new DataModel(); | 		DataModel dataModel2 = new DataModel(); | ||||||
| 		for (String device : Arrays.asList(deviceA, deviceB, deviceC)) { | 		for (String device : Arrays.asList(deviceA, deviceB, deviceC)) { | ||||||
| 			dataModel2.latestDeviceStatus.put( | 			dataModel2.latestDeviceStatusRadios.put( | ||||||
| 				device, | 				device, | ||||||
| 				TestUtils.createDeviceStatus(UCentralConstants.BANDS) | 				TestUtils.createDeviceStatus(UCentralConstants.BANDS) | ||||||
| 			); | 			); | ||||||
| @@ -363,7 +363,7 @@ public class LocationBasedOptimalTPCTest { | |||||||
|  |  | ||||||
| 		DataModel dataModel3 = new DataModel(); | 		DataModel dataModel3 = new DataModel(); | ||||||
| 		for (String device : Arrays.asList(deviceA, deviceB, deviceC)) { | 		for (String device : Arrays.asList(deviceA, deviceB, deviceC)) { | ||||||
| 			dataModel3.latestDeviceStatus.put( | 			dataModel3.latestDeviceStatusRadios.put( | ||||||
| 				device, | 				device, | ||||||
| 				TestUtils.createDeviceStatus(UCentralConstants.BANDS) | 				TestUtils.createDeviceStatus(UCentralConstants.BANDS) | ||||||
| 			); | 			); | ||||||
| @@ -412,7 +412,7 @@ public class LocationBasedOptimalTPCTest { | |||||||
|  |  | ||||||
| 		DataModel dataModel4 = new DataModel(); | 		DataModel dataModel4 = new DataModel(); | ||||||
| 		for (String device : Arrays.asList(deviceA, deviceB, deviceC)) { | 		for (String device : Arrays.asList(deviceA, deviceB, deviceC)) { | ||||||
| 			dataModel4.latestDeviceStatus.put( | 			dataModel4.latestDeviceStatusRadios.put( | ||||||
| 				device, | 				device, | ||||||
| 				TestUtils.createDeviceStatus(UCentralConstants.BANDS) | 				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.UCentralConstants; | ||||||
| import com.facebook.openwifirrm.ucentral.UCentralUtils; | import com.facebook.openwifirrm.ucentral.UCentralUtils; | ||||||
| import com.facebook.openwifirrm.ucentral.WifiScanEntry; | import com.facebook.openwifirrm.ucentral.WifiScanEntry; | ||||||
| import com.facebook.openwifirrm.ucentral.models.State; |  | ||||||
| import com.google.gson.JsonArray; |  | ||||||
|  |  | ||||||
| @TestMethodOrder(OrderAnnotation.class) | @TestMethodOrder(OrderAnnotation.class) | ||||||
| public class MeasurementBasedApApTPCTest { | public class MeasurementBasedApApTPCTest { | ||||||
| @@ -78,65 +76,84 @@ public class MeasurementBasedApApTPCTest { | |||||||
| 		return deviceDataManager; | 		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 | 	 * Creates a data model with 3 devices. All are at max_tx_power, which | ||||||
| 	 * represents the first step in greedy TPC. | 	 * represents the first step in greedy TPC. | ||||||
| 	 * | 	 * | ||||||
| 	 * @return a data model | 	 * @return a data model | ||||||
| 	 */ | 	 */ | ||||||
| 	private static DataModel createModel() { | 	private static DataModel createModelDualBand() { | ||||||
| 		DataModel model = new DataModel(); | 		DataModel model = new DataModel(); | ||||||
|  |  | ||||||
| 		State stateA = TestUtils.createState( | 		final int channel2G = | ||||||
| 			1, | 			UCentralUtils.LOWER_CHANNEL_LIMIT.get(UCentralConstants.BAND_2G); | ||||||
| 			DEFAULT_CHANNEL_WIDTH, | 		final int channel5G = | ||||||
| 			MAX_TX_POWER, | 			UCentralUtils.LOWER_CHANNEL_LIMIT.get(UCentralConstants.BAND_5G); | ||||||
| 			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 |  | ||||||
| 		); |  | ||||||
|  |  | ||||||
| 		model.latestState.put(DEVICE_A, stateA); | 		List<String> bssids = Arrays.asList(BSSID_A, BSSID_B, BSSID_C); | ||||||
| 		model.latestState.put(DEVICE_B, stateB); | 		List<String> devices = Arrays.asList(DEVICE_A, DEVICE_B, DEVICE_C); | ||||||
| 		model.latestState.put(DEVICE_C, stateC); | 		for (int i = 0; i < devices.size(); i++) { | ||||||
|  | 			String device = devices.get(i); | ||||||
| 		model.latestDeviceStatus.put( | 			String bssid = bssids.get(i); | ||||||
| 			DEVICE_A, | 			model.latestState.put( | ||||||
| 			TestUtils | 				device, | ||||||
| 				.createDeviceStatusDualBand(1, MAX_TX_POWER, 36, MAX_TX_POWER) | 				TestUtils.createState( | ||||||
| 		); | 					channel2G, | ||||||
| 		model.latestDeviceStatus.put( | 					DEFAULT_CHANNEL_WIDTH, | ||||||
| 			DEVICE_B, | 					MAX_TX_POWER, | ||||||
| 			TestUtils | 					bssid, | ||||||
| 				.createDeviceStatusDualBand(1, MAX_TX_POWER, 36, MAX_TX_POWER) | 					channel5G, | ||||||
| 		); | 					DEFAULT_CHANNEL_WIDTH, | ||||||
| 		model.latestDeviceStatus.put( | 					MAX_TX_POWER, | ||||||
| 			DEVICE_C, | 					bssid | ||||||
| 			TestUtils | 				) | ||||||
| 				.createDeviceStatusDualBand(1, MAX_TX_POWER, 36, MAX_TX_POWER) | 			); | ||||||
| 		); | 			model.latestDeviceStatusRadios.put( | ||||||
|  | 				device, | ||||||
|  | 				TestUtils | ||||||
|  | 					.createDeviceStatusDualBand( | ||||||
|  | 						channel2G, | ||||||
|  | 						MAX_TX_POWER, | ||||||
|  | 						channel5G, | ||||||
|  | 						MAX_TX_POWER | ||||||
|  | 					) | ||||||
|  | 			); | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		return model; | 		return model; | ||||||
| 	} | 	} | ||||||
| @@ -287,7 +304,7 @@ public class MeasurementBasedApApTPCTest { | |||||||
| 	@Test | 	@Test | ||||||
| 	@Order(1) | 	@Order(1) | ||||||
| 	void testGetManagedBSSIDs() throws Exception { | 	void testGetManagedBSSIDs() throws Exception { | ||||||
| 		DataModel dataModel = createModel(); | 		DataModel dataModel = createModelDualBand(); | ||||||
| 		Set<String> managedBSSIDs = | 		Set<String> managedBSSIDs = | ||||||
| 			MeasurementBasedApApTPC.getManagedBSSIDs(dataModel); | 			MeasurementBasedApApTPC.getManagedBSSIDs(dataModel); | ||||||
| 		assertEquals(3, managedBSSIDs.size()); | 		assertEquals(3, managedBSSIDs.size()); | ||||||
| @@ -298,25 +315,6 @@ public class MeasurementBasedApApTPCTest { | |||||||
|  |  | ||||||
| 	@Test | 	@Test | ||||||
| 	@Order(2) | 	@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 { | 	void testBuildRssiMap() throws Exception { | ||||||
| 		// This example includes three APs, and one AP that is unmanaged | 		// This example includes three APs, and one AP that is unmanaged | ||||||
| 		Set<String> bssidSet = Set.of(BSSID_A, BSSID_B, BSSID_C); | 		Set<String> bssidSet = Set.of(BSSID_A, BSSID_B, BSSID_C); | ||||||
| @@ -338,7 +336,7 @@ public class MeasurementBasedApApTPCTest { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	@Test | 	@Test | ||||||
| 	@Order(4) | 	@Order(3) | ||||||
| 	void testComputeTxPower() throws Exception { | 	void testComputeTxPower() throws Exception { | ||||||
| 		// Test examples here taken from algorithm design doc from @pohanhf | 		// Test examples here taken from algorithm design doc from @pohanhf | ||||||
| 		final String serialNumber = "testSerial"; | 		final String serialNumber = "testSerial"; | ||||||
| @@ -413,7 +411,7 @@ public class MeasurementBasedApApTPCTest { | |||||||
| 	 */ | 	 */ | ||||||
| 	private static void testComputeTxPowerMapSimpleInOneBand(String band) { | 	private static void testComputeTxPowerMapSimpleInOneBand(String band) { | ||||||
| 		int channel = UCentralUtils.LOWER_CHANNEL_LIMIT.get(band); | 		int channel = UCentralUtils.LOWER_CHANNEL_LIMIT.get(band); | ||||||
| 		DataModel dataModel = createModel(); | 		DataModel dataModel = createModelSingleBand(band); | ||||||
| 		dataModel.latestWifiScans = createLatestWifiScansB(channel); | 		dataModel.latestWifiScans = createLatestWifiScansB(channel); | ||||||
| 		DeviceDataManager deviceDataManager = createDeviceDataManager(); | 		DeviceDataManager deviceDataManager = createDeviceDataManager(); | ||||||
|  |  | ||||||
| @@ -438,7 +436,7 @@ public class MeasurementBasedApApTPCTest { | |||||||
| 		String band | 		String band | ||||||
| 	) { | 	) { | ||||||
| 		int channel = UCentralUtils.LOWER_CHANNEL_LIMIT.get(band); | 		int channel = UCentralUtils.LOWER_CHANNEL_LIMIT.get(band); | ||||||
| 		DataModel dataModel = createModel(); | 		DataModel dataModel = createModelSingleBand(band); | ||||||
| 		dataModel.latestWifiScans = createLatestWifiScansC(channel); | 		dataModel.latestWifiScans = createLatestWifiScansC(channel); | ||||||
| 		DeviceDataManager deviceDataManager = createDeviceDataManager(); | 		DeviceDataManager deviceDataManager = createDeviceDataManager(); | ||||||
|  |  | ||||||
| @@ -465,7 +463,7 @@ public class MeasurementBasedApApTPCTest { | |||||||
| 	 */ | 	 */ | ||||||
| 	private static void testComputeTxPowerMapMissingDataInOneBand(String band) { | 	private static void testComputeTxPowerMapMissingDataInOneBand(String band) { | ||||||
| 		int channel = UCentralUtils.LOWER_CHANNEL_LIMIT.get(band); | 		int channel = UCentralUtils.LOWER_CHANNEL_LIMIT.get(band); | ||||||
| 		DataModel dataModel = createModel(); | 		DataModel dataModel = createModelSingleBand(band); | ||||||
| 		dataModel.latestWifiScans = | 		dataModel.latestWifiScans = | ||||||
| 			createLatestWifiScansWithMissingEntries(channel); | 			createLatestWifiScansWithMissingEntries(channel); | ||||||
| 		DeviceDataManager deviceDataManager = createDeviceDataManager(); | 		DeviceDataManager deviceDataManager = createDeviceDataManager(); | ||||||
| @@ -501,34 +499,17 @@ public class MeasurementBasedApApTPCTest { | |||||||
| 	@Order(6) | 	@Order(6) | ||||||
| 	void testComputeTxPowerMapMultiBand() { | 	void testComputeTxPowerMapMultiBand() { | ||||||
| 		// test both bands simultaneously with different setups on each band | 		// test both bands simultaneously with different setups on each band | ||||||
| 		DataModel dataModel = createModel(); | 		DataModel dataModel = createModelDualBand(); | ||||||
| 		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 |  | ||||||
| 			) |  | ||||||
| 		); |  | ||||||
| 		DeviceDataManager deviceDataManager = createDeviceDataManager(); | 		DeviceDataManager deviceDataManager = createDeviceDataManager(); | ||||||
| 		// 2G setup | 		// 2G: use testComputeTxPowerMapSimpleInOneBand setup | ||||||
| 		final int channel2G = | 		final int channel2G = | ||||||
| 			UCentralUtils.LOWER_CHANNEL_LIMIT.get(UCentralConstants.BAND_2G); | 			UCentralUtils.LOWER_CHANNEL_LIMIT.get(UCentralConstants.BAND_2G); | ||||||
| 		dataModel.latestWifiScans = createLatestWifiScansB(channel2G); | 		dataModel.latestWifiScans = createLatestWifiScansB(channel2G); | ||||||
| 		// 5G setup | 		// 5G: use testComputeTxPowerMapMissingDataInOneBand setup | ||||||
| 		final int channel5G = | 		final int channel5G = | ||||||
| 			UCentralUtils.LOWER_CHANNEL_LIMIT.get(UCentralConstants.BAND_5G); | 			UCentralUtils.LOWER_CHANNEL_LIMIT.get(UCentralConstants.BAND_5G); | ||||||
|  | 		// add 5G wifiscan results to dataModel.latestWifiScans | ||||||
| 		Map<String, List<List<WifiScanEntry>>> toMerge = | 		Map<String, List<List<WifiScanEntry>>> toMerge = | ||||||
| 			createLatestWifiScansWithMissingEntries(channel5G); | 			createLatestWifiScansWithMissingEntries(channel5G); | ||||||
| 		for ( | 		for ( | ||||||
| @@ -556,8 +537,10 @@ public class MeasurementBasedApApTPCTest { | |||||||
| 		Map<String, Map<String, Integer>> txPowerMap = | 		Map<String, Map<String, Integer>> txPowerMap = | ||||||
| 			optimizer.computeTxPowerMap(); | 			optimizer.computeTxPowerMap(); | ||||||
|  |  | ||||||
| 		// test 2G band | 		// every AP operates in at least one band | ||||||
| 		assertEquals(3, txPowerMap.size()); | 		assertEquals(3, txPowerMap.size()); | ||||||
|  |  | ||||||
|  | 		// test 2G band | ||||||
| 		assertEquals( | 		assertEquals( | ||||||
| 			2, | 			2, | ||||||
| 			txPowerMap.get(DEVICE_A).get(UCentralConstants.BAND_2G) | 			txPowerMap.get(DEVICE_A).get(UCentralConstants.BAND_2G) | ||||||
| @@ -576,11 +559,69 @@ public class MeasurementBasedApApTPCTest { | |||||||
| 			0, | 			0, | ||||||
| 			txPowerMap.get(DEVICE_A).get(UCentralConstants.BAND_5G) | 			txPowerMap.get(DEVICE_A).get(UCentralConstants.BAND_5G) | ||||||
| 		); | 		); | ||||||
| 		// deivce B does not have 5G radio | 		assertEquals( | ||||||
| 		assertFalse( | 			0, | ||||||
| 			txPowerMap.get(DEVICE_B).containsKey(UCentralConstants.BAND_5G) | 			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( | 		assertFalse( | ||||||
| 			txPowerMap.get(DEVICE_C).containsKey(UCentralConstants.BAND_5G) | 			txPowerMap.get(DEVICE_C).containsKey(UCentralConstants.BAND_5G) | ||||||
| 		); | 		); | ||||||
|   | |||||||
| @@ -8,7 +8,12 @@ | |||||||
|  |  | ||||||
| package com.facebook.openwifirrm.ucentral; | 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.assertEquals; | ||||||
|  | import static org.junit.jupiter.api.Assertions.assertTrue; | ||||||
|  | import static org.junit.jupiter.api.Assertions.assertFalse; | ||||||
|  |  | ||||||
| import org.junit.jupiter.api.Test; | import org.junit.jupiter.api.Test; | ||||||
|  |  | ||||||
| @@ -17,4 +22,60 @@ public class UCentralUtilsTest { | |||||||
| 	void test_placeholder() throws Exception { | 	void test_placeholder() throws Exception { | ||||||
| 		assertEquals(3, 1 + 2); | 		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. |  * 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 static org.junit.jupiter.api.Assertions.assertEquals; | ||||||
| 
 | 
 | ||||||
| import org.junit.jupiter.api.Test; | import org.junit.jupiter.api.Test; | ||||||
| 
 | 
 | ||||||
| public class HTOperationElementTest { | public class HTOperationTest { | ||||||
| 	@Test | 	@Test | ||||||
| 	void testGetHtOper() { | 	void testGetHtOper() { | ||||||
| 		String htOper = "AQAEAAAAAAAAAAAAAAAAAAAAAAAAAA=="; | 		String htOper = "AQAEAAAAAAAAAAAAAAAAAAAAAAAAAA=="; | ||||||
| 		HTOperationElement htOperObj = new HTOperationElement(htOper); | 		HTOperation htOperObj = new HTOperation(htOper); | ||||||
| 		byte expectedPrimaryChannel = 1; | 		byte expectedPrimaryChannel = 1; | ||||||
| 		byte expectedSecondaryChannelOffset = 0; | 		byte expectedSecondaryChannelOffset = 0; | ||||||
| 		boolean expectedStaChannelWidth = false; | 		boolean expectedStaChannelWidth = false; | ||||||
| @@ -28,7 +28,7 @@ public class HTOperationElementTest { | |||||||
| 		boolean expectedDualBeacon = false; | 		boolean expectedDualBeacon = false; | ||||||
| 		boolean expectedDualCtsProtection = false; | 		boolean expectedDualCtsProtection = false; | ||||||
| 		boolean expectedStbcBeacon = false; | 		boolean expectedStbcBeacon = false; | ||||||
| 		HTOperationElement expectedHtOperObj = new HTOperationElement( | 		HTOperation expectedHtOperObj = new HTOperation( | ||||||
| 			expectedPrimaryChannel, | 			expectedPrimaryChannel, | ||||||
| 			expectedSecondaryChannelOffset, | 			expectedSecondaryChannelOffset, | ||||||
| 			expectedStaChannelWidth, | 			expectedStaChannelWidth, | ||||||
| @@ -44,11 +44,11 @@ public class HTOperationElementTest { | |||||||
| 		assertEquals(expectedHtOperObj, htOperObj); | 		assertEquals(expectedHtOperObj, htOperObj); | ||||||
| 
 | 
 | ||||||
| 		htOper = "JAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="; | 		htOper = "JAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="; | ||||||
| 		htOperObj = new HTOperationElement(htOper); | 		htOperObj = new HTOperation(htOper); | ||||||
| 		// all fields except the primary channel and nongreenfield field are the same | 		// all fields except the primary channel and nongreenfield field are the same | ||||||
| 		expectedPrimaryChannel = 36; | 		expectedPrimaryChannel = 36; | ||||||
| 		expectedNongreenfieldHtStasPresent = false; | 		expectedNongreenfieldHtStasPresent = false; | ||||||
| 		expectedHtOperObj = new HTOperationElement( | 		expectedHtOperObj = new HTOperation( | ||||||
| 			expectedPrimaryChannel, | 			expectedPrimaryChannel, | ||||||
| 			expectedSecondaryChannelOffset, | 			expectedSecondaryChannelOffset, | ||||||
| 			expectedStaChannelWidth, | 			expectedStaChannelWidth, | ||||||
| @@ -6,23 +6,23 @@ | |||||||
|  * LICENSE file in the root directory of this source tree. |  * 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 static org.junit.jupiter.api.Assertions.assertEquals; | ||||||
| 
 | 
 | ||||||
| import org.junit.jupiter.api.Test; | import org.junit.jupiter.api.Test; | ||||||
| 
 | 
 | ||||||
| public class VHTOperationElementTest { | public class VHTOperationTest { | ||||||
| 
 | 
 | ||||||
| 	@Test | 	@Test | ||||||
| 	void testGetVhtOper() { | 	void testGetVhtOper() { | ||||||
| 		String vhtOper = "ACQAAAA="; | 		String vhtOper = "ACQAAAA="; | ||||||
| 		VHTOperationElement vhtOperObj = new VHTOperationElement(vhtOper); | 		VHTOperation vhtOperObj = new VHTOperation(vhtOper); | ||||||
| 		byte expectedChannelWidthIndicator = 0; // 20 MHz channel width | 		byte expectedChannelWidthIndicator = 0; // 20 MHz channel width | ||||||
| 		byte expectedChannel1 = 36; | 		short expectedChannel1 = 36; | ||||||
| 		byte expectedChannel2 = 0; | 		short expectedChannel2 = 0; | ||||||
| 		byte[] expectedVhtMcsForNss = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 }; | 		byte[] expectedVhtMcsForNss = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 }; | ||||||
| 		VHTOperationElement expectedVhtOperObj = new VHTOperationElement( | 		VHTOperation expectedVhtOperObj = new VHTOperation( | ||||||
| 			expectedChannelWidthIndicator, | 			expectedChannelWidthIndicator, | ||||||
| 			expectedChannel1, | 			expectedChannel1, | ||||||
| 			expectedChannel2, | 			expectedChannel2, | ||||||
| @@ -31,12 +31,12 @@ public class VHTOperationElementTest { | |||||||
| 		assertEquals(expectedVhtOperObj, vhtOperObj); | 		assertEquals(expectedVhtOperObj, vhtOperObj); | ||||||
| 
 | 
 | ||||||
| 		vhtOper = "AToAUAE="; | 		vhtOper = "AToAUAE="; | ||||||
| 		vhtOperObj = new VHTOperationElement(vhtOper); | 		vhtOperObj = new VHTOperation(vhtOper); | ||||||
| 		expectedChannelWidthIndicator = 1; // 80 MHz channel width | 		expectedChannelWidthIndicator = 1; // 80 MHz channel width | ||||||
| 		expectedChannel1 = 58; | 		expectedChannel1 = 58; | ||||||
| 		// same channel2 | 		// same channel2 | ||||||
| 		expectedVhtMcsForNss = new byte[] { 1, 1, 0, 0, 0, 0, 0, 1 }; | 		expectedVhtMcsForNss = new byte[] { 1, 1, 0, 0, 0, 0, 0, 1 }; | ||||||
| 		expectedVhtOperObj = new VHTOperationElement( | 		expectedVhtOperObj = new VHTOperation( | ||||||
| 			expectedChannelWidthIndicator, | 			expectedChannelWidthIndicator, | ||||||
| 			expectedChannel1, | 			expectedChannel1, | ||||||
| 			expectedChannel2, | 			expectedChannel2, | ||||||
| @@ -45,12 +45,27 @@ public class VHTOperationElementTest { | |||||||
| 		assertEquals(expectedVhtOperObj, vhtOperObj); | 		assertEquals(expectedVhtOperObj, vhtOperObj); | ||||||
| 
 | 
 | ||||||
| 		vhtOper = "ASoyUAE="; | 		vhtOper = "ASoyUAE="; | ||||||
| 		vhtOperObj = new VHTOperationElement(vhtOper); | 		vhtOperObj = new VHTOperation(vhtOper); | ||||||
| 		// same channel width indicator (160 MHz channel width) | 		// same channel width indicator (160 MHz channel width) | ||||||
| 		expectedChannel1 = 42; | 		expectedChannel1 = 42; | ||||||
| 		expectedChannel2 = 50; | 		expectedChannel2 = 50; | ||||||
| 		// same vhtMcsForNss | 		// 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, | 			expectedChannelWidthIndicator, | ||||||
| 			expectedChannel1, | 			expectedChannel1, | ||||||
| 			expectedChannel2, | 			expectedChannel2, | ||||||
		Reference in New Issue
	
	Block a user