mirror of
				https://github.com/Telecominfraproject/wlan-cloud-rrm.git
				synced 2025-10-31 02:28:15 +00:00 
			
		
		
		
	Compare commits
	
		
			22 Commits
		
	
	
		
			scale-test
			...
			release/v2
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 787ef196d2 | ||
|   | 73693c07ef | ||
|   | 9e22796758 | ||
|   | f1c488eac8 | ||
|   | 066c523df5 | ||
|   | ba8c156e72 | ||
|   | d73eb23920 | ||
|   | ac5a1c8887 | ||
|   | ea3a13e98c | ||
|   | c1511e8e91 | ||
|   | 05c36a535f | ||
|   | c94c31cb63 | ||
|   | 404934eda9 | ||
|   | 80626388c8 | ||
|   | a79359c69d | ||
|   | a638d70fd6 | ||
|   | e3705699b4 | ||
|   | 35eddf73cf | ||
|   | f031027684 | ||
|   | a54f9a48be | ||
|   | c22ebeea31 | ||
|   | e1b9052ecc | 
							
								
								
									
										4
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -26,7 +26,7 @@ jobs: | ||||
|       DOCKER_REGISTRY_USERNAME: ucentral | ||||
|     steps: | ||||
|     - name: Checkout actions repo | ||||
|       uses: actions/checkout@v2 | ||||
|       uses: actions/checkout@v3 | ||||
|       with: | ||||
|         repository: Telecominfraproject/.github | ||||
|         path: github | ||||
| @@ -57,7 +57,7 @@ jobs: | ||||
|       - docker | ||||
|     steps: | ||||
|     - name: Checkout actions repo | ||||
|       uses: actions/checkout@v2 | ||||
|       uses: actions/checkout@v3 | ||||
|       with: | ||||
|         repository: Telecominfraproject/.github | ||||
|         path: github | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/enforce-jira-issue-key.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/enforce-jira-issue-key.yml
									
									
									
									
										vendored
									
									
								
							| @@ -11,7 +11,7 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout actions repo | ||||
|         uses: actions/checkout@v2 | ||||
|         uses: actions/checkout@v3 | ||||
|         with: | ||||
|           repository: Telecominfraproject/.github | ||||
|           path: github | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -17,7 +17,7 @@ jobs: | ||||
|       HELM_REPO_USERNAME: ucentral | ||||
|     steps: | ||||
|       - name: Checkout uCentral assembly chart repo | ||||
|         uses: actions/checkout@v2 | ||||
|         uses: actions/checkout@v3 | ||||
|         with: | ||||
|           path: wlan-cloud-rrm | ||||
|  | ||||
|   | ||||
							
								
								
									
										11
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										11
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,6 +1,12 @@ | ||||
| /target/ | ||||
| */target/ | ||||
|  | ||||
| # owrrm specific | ||||
| *.log* | ||||
| device_config.json | ||||
| settings.json | ||||
| topology.json | ||||
|  | ||||
| # Eclipse | ||||
| .settings/ | ||||
| bin/ | ||||
| @@ -14,3 +20,8 @@ bin/ | ||||
| *.iml | ||||
| *.iws | ||||
| *.ipr | ||||
|  | ||||
| # Miscellaneous files thzt should not be checked in | ||||
| temp/ | ||||
|  | ||||
| .DS_Store | ||||
|   | ||||
							
								
								
									
										16
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								Dockerfile
									
									
									
									
									
								
							| @@ -3,18 +3,20 @@ WORKDIR /usr/src/java | ||||
| COPY . . | ||||
| RUN mvn clean package -pl owrrm -am -DappendVersionString="$(./scripts/get_build_version.sh)" | ||||
|  | ||||
| FROM adoptopenjdk/openjdk11-openj9:latest | ||||
| RUN apt-get update && apt-get install -y gettext-base wget | ||||
| RUN wget https://raw.githubusercontent.com/Telecominfraproject/wlan-cloud-ucentral-deploy/main/docker-compose/certs/restapi-ca.pem \ | ||||
|     -O /usr/local/share/ca-certificates/restapi-ca-selfsigned.pem | ||||
| FROM ibm-semeru-runtimes:open-11-jre | ||||
| RUN apt-get update && apt-get install -y gettext-base | ||||
| ADD https://raw.githubusercontent.com/Telecominfraproject/wlan-cloud-ucentral-deploy/main/docker-compose/certs/restapi-ca.pem \ | ||||
|   /usr/local/share/ca-certificates/restapi-ca-selfsigned.pem | ||||
| RUN mkdir /owrrm-data | ||||
| WORKDIR /usr/src/java | ||||
| COPY docker-entrypoint.sh / | ||||
| COPY runner.sh / | ||||
| COPY --from=build /usr/src/java/owrrm/target/openwifi-rrm.jar /usr/local/bin/ | ||||
| EXPOSE 16789 16790 | ||||
| ENTRYPOINT ["/docker-entrypoint.sh"] | ||||
| CMD ["java", "-XX:+IdleTuningGcOnIdle", "-Xtune:virtualized", \ | ||||
|      "-jar", "/usr/local/bin/openwifi-rrm.jar", \ | ||||
|      "run", "--config-env", \ | ||||
| ENV JVM_IMPL=openj9 | ||||
| CMD ["/runner.sh", "java", "/usr/local/bin/openwifi-rrm.jar", \ | ||||
|     "run", \ | ||||
|     "--config-env", \ | ||||
|     "-t", "/owrrm-data/topology.json", \ | ||||
|     "-d", "/owrrm-data/device_config.json"] | ||||
|   | ||||
							
								
								
									
										28
									
								
								Dockerfile-hotspot
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								Dockerfile-hotspot
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| FROM maven:3-eclipse-temurin-11 as build | ||||
| WORKDIR /usr/src/java | ||||
| COPY . . | ||||
| RUN mvn clean package -pl owrrm -am -DappendVersionString="$(./scripts/get_build_version.sh)" | ||||
|  | ||||
| FROM eclipse-temurin:11-jre-jammy | ||||
| RUN apt-get update && apt-get install -y gettext-base | ||||
| ADD https://raw.githubusercontent.com/Telecominfraproject/wlan-cloud-ucentral-deploy/main/docker-compose/certs/restapi-ca.pem \ | ||||
|   /usr/local/share/ca-certificates/restapi-ca-selfsigned.pem | ||||
| RUN mkdir /owrrm-data | ||||
| WORKDIR /usr/src/java | ||||
| COPY docker-entrypoint.sh / | ||||
| COPY runner.sh / | ||||
| COPY --from=build /usr/src/java/owrrm/target/openwifi-rrm.jar /usr/local/bin/ | ||||
|  | ||||
| # generate Application CDS | ||||
| RUN java -Xshare:off -XX:DumpLoadedClassList=static-cds.lst -jar /usr/local/bin/openwifi-rrm.jar --help  && \ | ||||
|     java -Xshare:dump -XX:SharedClassListFile=static-cds.lst -XX:SharedArchiveFile=static-cds.jsa -jar /usr/local/bin/openwifi-rrm.jar | ||||
|  | ||||
| EXPOSE 16789 16790 | ||||
| ENTRYPOINT ["/docker-entrypoint.sh"] | ||||
| ENV JVM_IMPL=hotspot | ||||
| ENV EXTRA_JVM_FLAGS="-XX:SharedArchiveFile=static-cds.jsa -Xshare:auto" | ||||
| CMD ["/runner.sh", "java", "/usr/local/bin/openwifi-rrm.jar", \ | ||||
|     "run", \ | ||||
|     "--config-env", \ | ||||
|     "-t", "/owrrm-data/topology.json", \ | ||||
|     "-d", "/owrrm-data/device_config.json"] | ||||
| @@ -9,7 +9,7 @@ fullnameOverride: "" | ||||
| images: | ||||
|   owrrm: | ||||
|     repository: tip-tip-wlan-cloud-ucentral.jfrog.io/owrrm | ||||
|     tag: main | ||||
|     tag: v2.8.0 | ||||
|     pullPolicy: Always | ||||
| #    regcred: | ||||
| #      registry: tip-tip-wlan-cloud-ucentral.jfrog.io | ||||
|   | ||||
| @@ -10,11 +10,12 @@ package com.facebook.openwifi.cloudsdk; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| import java.util.Objects; | ||||
|  | ||||
| import com.facebook.openwifi.cloudsdk.models.ap.State; | ||||
| import com.facebook.openwifi.cloudsdk.models.ap.State.Interface.Counters; | ||||
| import com.facebook.openwifi.cloudsdk.models.ap.State.Interface.SSID.Association; | ||||
| import com.facebook.openwifi.cloudsdk.models.ap.State.Interface.SSID.Association.Rate; | ||||
| import com.google.gson.JsonObject; | ||||
|  | ||||
| /** | ||||
|  * Aggregation model for State aggregation. Only contains info useful for | ||||
| @@ -22,76 +23,32 @@ import com.facebook.openwifi.cloudsdk.models.ap.State.Interface.SSID.Association | ||||
|  */ | ||||
| public class AggregatedState { | ||||
|  | ||||
| 	/** Rate information with aggregated fields. */ | ||||
| 	public static class AggregatedRate { | ||||
| 		/** | ||||
| 		 * This is the common bitRate for all the aggregated fields. | ||||
| 		 */ | ||||
| 		public long bitRate; | ||||
|  | ||||
| 		/** | ||||
| 		 * This is the common channel width for all the aggregated fields. | ||||
| 		 */ | ||||
| 		public int chWidth; | ||||
|  | ||||
| 		/** | ||||
| 		 * Aggregated fields mcs | ||||
| 		 */ | ||||
| 		public List<Integer> mcs = new ArrayList<>(); | ||||
|  | ||||
| 		/** Constructor with no args */ | ||||
| 		private AggregatedRate() {} | ||||
|  | ||||
| 		/** Add a Rate to the AggregatedRate */ | ||||
| 		private void add(Rate rate) { | ||||
| 			if (rate == null) { | ||||
| 				return; | ||||
| 			} | ||||
| 			if (mcs.isEmpty()) { | ||||
| 				bitRate = rate.bitrate; | ||||
| 				chWidth = rate.chwidth; | ||||
| 			} | ||||
| 			mcs.add(rate.mcs); | ||||
| 		} | ||||
|  | ||||
| 		/** | ||||
| 		 * Add an AggregatedRate with the same channel_width to the | ||||
| 		 * AggregatedRate | ||||
| 		 */ | ||||
| 		private void add(AggregatedRate rate) { | ||||
| 			if (rate == null || rate.chWidth != chWidth) { | ||||
| 				return; | ||||
| 			} | ||||
| 			if (mcs.isEmpty()) { | ||||
| 				bitRate = rate.bitRate; | ||||
| 				chWidth = rate.chWidth; | ||||
| 			} | ||||
| 			mcs.addAll(rate.mcs); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Radio information with channel, channel_width and tx_power. | ||||
| 	 */ | ||||
| 	public static class Radio { | ||||
| 	public static class RadioConfig { | ||||
| 		public int channel; | ||||
| 		public int channelWidth; | ||||
| 		public int txPower; | ||||
| 		public String phy; | ||||
|  | ||||
| 		private Radio() {} | ||||
| 		/** Default constructor with no args */ | ||||
| 		private RadioConfig() {} | ||||
|  | ||||
| 		public Radio(int channel, int channelWidth, int txPower) { | ||||
| 		/** Constructor with args */ | ||||
| 		public RadioConfig(JsonObject radio) { | ||||
| 			this.channel = radio.get("channel").getAsInt(); | ||||
| 			this.channelWidth = radio.get("channel_width").getAsInt(); | ||||
| 			this.txPower = radio.get("tx_power").getAsInt(); | ||||
| 			this.phy = radio.get("phy").getAsString(); | ||||
| 		} | ||||
|  | ||||
| 		public RadioConfig(int channel, int channelWidth, int txPower) { | ||||
| 			this.channel = channel; | ||||
| 			this.channelWidth = channelWidth; | ||||
| 			this.txPower = txPower; | ||||
| 		} | ||||
|  | ||||
| 		private Radio(Map<String, Integer> radioInfo) { | ||||
| 			channel = radioInfo.getOrDefault("channel", -1); | ||||
| 			channelWidth = radioInfo.getOrDefault("channel_width", -1); | ||||
| 			txPower = radioInfo.getOrDefault("tx_power", -1); | ||||
| 		} | ||||
|  | ||||
| 		@Override | ||||
| 		public int hashCode() { | ||||
| 			return Objects.hash(channel, channelWidth, txPower); | ||||
| @@ -109,64 +66,161 @@ public class AggregatedState { | ||||
| 				return false; | ||||
| 			} | ||||
|  | ||||
| 			Radio other = (Radio) obj; | ||||
| 			RadioConfig other = (RadioConfig) obj; | ||||
| 			return channel == other.channel && | ||||
| 				channelWidth == other.channelWidth && txPower == other.txPower; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public String bssid; | ||||
| 	public String station; | ||||
| 	/** | ||||
| 	 * Data model to keep raw data from {@link State.Interface.SSID.Association}, | ||||
| 	 * {@link State.Radio} and {@link State.Interface.Counters}. | ||||
| 	 */ | ||||
| 	public static class AssociationInfo { | ||||
| 		/** Rate information with aggregated fields. */ | ||||
| 		public static class Rate { | ||||
| 			/** | ||||
| 			 * Aggregated fields bitRate | ||||
| 			 */ | ||||
| 			public long bitRate; | ||||
|  | ||||
| 			/** | ||||
| 			 * Aggregated fields chWidth | ||||
| 			 */ | ||||
| 			public int chWidth; | ||||
|  | ||||
| 			/** | ||||
| 			 * Aggregated fields mcs | ||||
| 			 */ | ||||
| 			public int mcs; | ||||
|  | ||||
| 			/** Constructor with no args */ | ||||
| 			private Rate() {} | ||||
|  | ||||
| 			/** Constructor with args */ | ||||
| 			private Rate(long bitRate, int chWidth, int mcs) { | ||||
| 				this.bitRate = bitRate; | ||||
| 				this.chWidth = chWidth; | ||||
| 				this.mcs = mcs; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		public long connected; | ||||
| 		public long inactive; | ||||
| 	public List<Integer> rssi; | ||||
| 		public int rssi; | ||||
| 		public long rxBytes; | ||||
| 		public long rxPackets; | ||||
| 	public AggregatedRate rxRate; | ||||
| 		public Rate rxRate; | ||||
| 		public long txBytes; | ||||
| 		public long txDuration; | ||||
| 		public long txFailed; | ||||
| 		public long txPackets; | ||||
| 	public AggregatedRate txRate; | ||||
| 		public Rate txRate; | ||||
| 		public long txRetries; | ||||
| 		public int ackSignal; | ||||
| 		public int ackSignalAvg; | ||||
| 	public Radio radio; | ||||
| 		public long txPacketsCounters; | ||||
| 		public long txErrorsCounters; | ||||
| 		public long txDroppedCounters; | ||||
| 		public long activeMsRadio; | ||||
| 		public long busyMsRadio; | ||||
| 		public long noiseRadio; | ||||
| 		public long receiveMsRadio; | ||||
| 		public long transmitMsRadio; | ||||
|  | ||||
| 	/** Constructor with no args */ | ||||
| 	public AggregatedState() { | ||||
| 		this.rxRate = new AggregatedRate(); | ||||
| 		this.txRate = new AggregatedRate(); | ||||
| 		this.rssi = new ArrayList<>(); | ||||
| 		this.radio = new Radio(); | ||||
| 		/** unix time in ms */ | ||||
| 		public long timestamp; | ||||
|  | ||||
| 		/** Default Constructor. */ | ||||
| 		public AssociationInfo() {} | ||||
|  | ||||
| 		/** Constructor with only rssi(for test purpose) */ | ||||
| 		public AssociationInfo(int rssi) { | ||||
| 			this.rssi = rssi; | ||||
| 		} | ||||
|  | ||||
| 	/** Construct from Association and radio */ | ||||
| 		/** Constructor with args */ | ||||
| 		public AssociationInfo( | ||||
| 			Association association, | ||||
| 			Counters counters, | ||||
| 			JsonObject radio, | ||||
| 			long timestamp | ||||
| 		) { | ||||
| 			// Association info | ||||
| 			connected = association.connected; | ||||
| 			inactive = association.inactive; | ||||
| 			rssi = association.rssi; | ||||
| 			rxBytes = association.rx_bytes; | ||||
| 			rxPackets = association.rx_packets; | ||||
| 			if (association.rx_rate != null) { | ||||
| 				rxRate = new Rate( | ||||
| 					association.rx_rate.bitrate, | ||||
| 					association.rx_rate.chwidth, | ||||
| 					association.rx_rate.mcs | ||||
| 				); | ||||
| 			} else { | ||||
| 				rxRate = new Rate(); | ||||
| 			} | ||||
|  | ||||
| 			txBytes = association.tx_bytes; | ||||
| 			txPackets = association.tx_packets; | ||||
|  | ||||
| 			if (association.tx_rate != null) { | ||||
| 				txRate = new Rate( | ||||
| 					association.tx_rate.bitrate, | ||||
| 					association.tx_rate.chwidth, | ||||
| 					association.tx_rate.mcs | ||||
| 				); | ||||
| 			} else { | ||||
| 				txRate = new Rate(); | ||||
| 			} | ||||
| 			txRetries = association.tx_retries; | ||||
| 			ackSignal = association.ack_signal; | ||||
| 			ackSignalAvg = association.ack_signal_avg; | ||||
|  | ||||
| 			//Counters info | ||||
| 			txPacketsCounters = counters.tx_packets; | ||||
| 			txErrorsCounters = counters.tx_errors; | ||||
| 			txDroppedCounters = counters.tx_dropped; | ||||
|  | ||||
| 			// Radio info | ||||
| 			activeMsRadio = radio.get("active_ms").getAsLong(); | ||||
| 			busyMsRadio = radio.get("busy_ms").getAsLong(); | ||||
| 			transmitMsRadio = radio.get("transmit_ms").getAsLong(); | ||||
| 			receiveMsRadio = radio.get("receive_ms").getAsLong(); | ||||
| 			noiseRadio = radio.get("noise").getAsLong(); | ||||
|  | ||||
| 			this.timestamp = timestamp; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Aggregate AssociationInfo over bssid, station and RadioConfig. | ||||
| 	public String bssid; | ||||
| 	public String station; | ||||
| 	public RadioConfig radioConfig; | ||||
|  | ||||
| 	// Store a list of AssociationInfo of the same link and radio configuration. */ | ||||
| 	public List<AssociationInfo> associationInfoList; | ||||
|  | ||||
| 	/** Constructor with no args. For test purpose. */ | ||||
| 	public AggregatedState() { | ||||
| 		this.associationInfoList = new ArrayList<>(); | ||||
| 		this.radioConfig = new RadioConfig(); | ||||
| 	} | ||||
|  | ||||
| 	/** Construct from Association, Counters, Radio and time stamp */ | ||||
| 	public AggregatedState( | ||||
| 		Association association, | ||||
| 		Map<String, Integer> radioInfo | ||||
| 		Counters counters, | ||||
| 		JsonObject radio, | ||||
| 		long timestamp | ||||
| 	) { | ||||
| 		this.rxRate = new AggregatedRate(); | ||||
| 		this.txRate = new AggregatedRate(); | ||||
| 		this.rssi = new ArrayList<>(); | ||||
|  | ||||
| 		this.bssid = association.bssid; | ||||
| 		this.station = association.station; | ||||
| 		this.connected = association.connected; | ||||
| 		this.inactive = association.inactive; | ||||
| 		this.rssi.add(association.rssi); | ||||
| 		this.rxBytes = association.rx_bytes; | ||||
| 		this.rxPackets = association.rx_packets; | ||||
| 		this.rxRate.add(association.rx_rate); | ||||
| 		this.txBytes = association.tx_bytes; | ||||
| 		this.txDuration = association.tx_duration; | ||||
| 		this.txFailed = association.tx_failed; | ||||
| 		this.txPackets = association.tx_packets; | ||||
| 		this.txRate.add(association.tx_rate); | ||||
| 		this.txRetries = association.tx_retries; | ||||
| 		this.ackSignal = association.ack_signal; | ||||
| 		this.ackSignalAvg = association.ack_signal_avg; | ||||
| 		this.radio = new Radio(radioInfo); | ||||
| 		this.associationInfoList = new ArrayList<>(); | ||||
| 		associationInfoList | ||||
| 			.add(new AssociationInfo(association, counters, radio, timestamp)); | ||||
| 		this.radioConfig = new RadioConfig(radio); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| @@ -178,7 +232,7 @@ public class AggregatedState { | ||||
| 	 */ | ||||
| 	public boolean matchesForAggregation(AggregatedState state) { | ||||
| 		return bssid == state.bssid && station == state.station && | ||||
| 			Objects.equals(radio, state.radio); | ||||
| 			Objects.equals(radioConfig, state.radioConfig); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| @@ -191,9 +245,7 @@ public class AggregatedState { | ||||
| 	 */ | ||||
| 	public boolean add(AggregatedState state) { | ||||
| 		if (matchesForAggregation(state)) { | ||||
| 			this.rssi.addAll(state.rssi); | ||||
| 			this.rxRate.add(state.rxRate); | ||||
| 			this.txRate.add(state.txRate); | ||||
| 			associationInfoList.addAll(state.associationInfoList); | ||||
| 			return true; | ||||
| 		} | ||||
| 		return false; | ||||
|   | ||||
| @@ -0,0 +1,125 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| package com.facebook.openwifi.cloudsdk; | ||||
|  | ||||
| import com.google.gson.JsonElement; | ||||
| import com.google.gson.JsonObject; | ||||
|  | ||||
| /** | ||||
|  * Utility functions for dealing with IEs | ||||
|  */ | ||||
| public abstract class IEUtils { | ||||
| 	/** | ||||
| 	 * Try to get a json object as a byte | ||||
| 	 * | ||||
| 	 * @param contents the JSON object to try to parse | ||||
| 	 * @param fieldName the field name | ||||
| 	 * @return the field as a byte or null | ||||
| 	 */ | ||||
| 	public static Byte parseOptionalByteField( | ||||
| 		JsonObject contents, | ||||
| 		String fieldName | ||||
| 	) { | ||||
| 		JsonElement element = contents.get(fieldName); | ||||
| 		if (element == null) { | ||||
| 			return null; | ||||
| 		} | ||||
| 		return element.getAsByte(); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Try to get a json object as a short | ||||
| 	 * | ||||
| 	 * @param contents the JSON object to try to parse | ||||
| 	 * @param fieldName the field name | ||||
| 	 * @return the field as a short or null | ||||
| 	 */ | ||||
| 	public static Short parseOptionalShortField( | ||||
| 		JsonObject contents, | ||||
| 		String fieldName | ||||
| 	) { | ||||
| 		JsonElement element = contents.get(fieldName); | ||||
| 		if (element == null) { | ||||
| 			return null; | ||||
| 		} | ||||
| 		return element.getAsShort(); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Try to get a json object as a int | ||||
| 	 * | ||||
| 	 * @param contents the JSON object to try to parse | ||||
| 	 * @param fieldName the field name | ||||
| 	 * @return the field as a int or null | ||||
| 	 */ | ||||
| 	public static Integer parseOptionalIntField( | ||||
| 		JsonObject contents, | ||||
| 		String fieldName | ||||
| 	) { | ||||
| 		JsonElement element = contents.get(fieldName); | ||||
| 		if (element == null) { | ||||
| 			return null; | ||||
| 		} | ||||
| 		return element.getAsInt(); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Try to get a json object as a int | ||||
| 	 * | ||||
| 	 * @param contents the JSON object to try to parse | ||||
| 	 * @param fieldName the field name | ||||
| 	 * @return the field as a int (0 if key not present) | ||||
| 	 */ | ||||
| 	public static Integer parseIntField( | ||||
| 		JsonObject contents, | ||||
| 		String fieldName | ||||
| 	) { | ||||
| 		JsonElement element = contents.get(fieldName); | ||||
| 		if (element == null) { | ||||
| 			return 0; | ||||
| 		} | ||||
| 		return element.getAsInt(); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Try to get a json object as a string | ||||
| 	 * | ||||
| 	 * @param contents the JSON object to try to parse | ||||
| 	 * @param fieldName the field name | ||||
| 	 * @return the field as a string or null | ||||
| 	 */ | ||||
| 	public static String parseOptionalStringField( | ||||
| 		JsonObject contents, | ||||
| 		String fieldName | ||||
| 	) { | ||||
| 		JsonElement element = contents.get(fieldName); | ||||
| 		if (element == null) { | ||||
| 			return null; | ||||
| 		} | ||||
| 		return element.getAsString(); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Try to get a json object as a boolean when represented as a number (0, 1) | ||||
| 	 * | ||||
| 	 * @param contents the JSON object to try to parse | ||||
| 	 * @param fieldName the field name | ||||
| 	 * @return the field as a boolean (false if key not present) | ||||
| 	 */ | ||||
| 	public static boolean parseBooleanNumberField( | ||||
| 		JsonObject contents, | ||||
| 		String fieldName | ||||
| 	) { | ||||
| 		JsonElement element = contents.get(fieldName); | ||||
| 		if (element == null) { | ||||
| 			return false; | ||||
| 		} | ||||
| 		return element.getAsInt() > 0; | ||||
| 	} | ||||
| } | ||||
| @@ -13,38 +13,42 @@ import java.util.Objects; | ||||
| import com.facebook.openwifi.cloudsdk.ies.Country; | ||||
| import com.facebook.openwifi.cloudsdk.ies.LocalPowerConstraint; | ||||
| import com.facebook.openwifi.cloudsdk.ies.QbssLoad; | ||||
| import com.facebook.openwifi.cloudsdk.ies.RMEnabledCapabilities; | ||||
| import com.facebook.openwifi.cloudsdk.ies.TxPwrInfo; | ||||
|  | ||||
| /** Wrapper class containing information elements */ | ||||
| public final class InformationElements { | ||||
|  | ||||
| 	public Country country; | ||||
| 	public QbssLoad qbssLoad; | ||||
| 	public LocalPowerConstraint localPowerConstraint; | ||||
| 	public TxPwrInfo txPwrInfo; | ||||
| 	public RMEnabledCapabilities rmEnabledCapabilities; | ||||
|  | ||||
| 	@Override | ||||
| 	public int hashCode() { | ||||
| 		return Objects.hash(country, localPowerConstraint, qbssLoad, txPwrInfo); | ||||
| 		return Objects.hash( | ||||
| 			country, | ||||
| 			localPowerConstraint, | ||||
| 			qbssLoad, | ||||
| 			rmEnabledCapabilities, | ||||
| 			txPwrInfo | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public boolean equals(Object obj) { | ||||
| 		if (this == obj) { | ||||
| 		if (this == obj) | ||||
| 			return true; | ||||
| 		} | ||||
| 		if (obj == null) { | ||||
| 		if (obj == null) | ||||
| 			return false; | ||||
| 		} | ||||
| 		if (getClass() != obj.getClass()) { | ||||
| 		if (getClass() != obj.getClass()) | ||||
| 			return false; | ||||
| 		} | ||||
| 		InformationElements other = (InformationElements) obj; | ||||
| 		return Objects.equals(country, other.country) && Objects.equals( | ||||
| 			localPowerConstraint, | ||||
| 			other.localPowerConstraint | ||||
| 		) && Objects.equals(qbssLoad, other.qbssLoad) && | ||||
| 		return Objects.equals(country, other.country) && | ||||
| 			Objects.equals(localPowerConstraint, other.localPowerConstraint) && | ||||
| 			Objects.equals(qbssLoad, other.qbssLoad) && | ||||
| 			Objects | ||||
| 				.equals(rmEnabledCapabilities, other.rmEnabledCapabilities) && | ||||
| 			Objects.equals(txPwrInfo, other.txPwrInfo); | ||||
| 	} | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,19 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| package com.facebook.openwifi.cloudsdk; | ||||
|  | ||||
| import com.facebook.openwifi.cloudsdk.models.ap.State; | ||||
|  | ||||
| public class StateInfo extends State { | ||||
| 	/** | ||||
| 	 * Unix time in milliseconds (ms). This is added it because State.unit.localtime is an unknown | ||||
| 	 * time reference. | ||||
| 	 */ | ||||
| 	public long timestamp; | ||||
| } | ||||
| @@ -8,13 +8,18 @@ | ||||
|  | ||||
| package com.facebook.openwifi.cloudsdk; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collections; | ||||
| import java.util.HashSet; | ||||
| import java.util.List; | ||||
| import java.util.Set; | ||||
|  | ||||
| import com.facebook.openwifi.cloudsdk.models.ap.UCentralSchema; | ||||
| import com.google.gson.Gson; | ||||
| import com.google.gson.JsonArray; | ||||
| import com.google.gson.JsonElement; | ||||
| import com.google.gson.JsonObject; | ||||
| import com.google.gson.reflect.TypeToken; | ||||
|  | ||||
| /** | ||||
|  * Wrapper around uCentral AP configuration. | ||||
| @@ -51,33 +56,33 @@ public class UCentralApConfiguration { | ||||
| 		return config.getAsJsonArray("radios").size(); | ||||
| 	} | ||||
|  | ||||
| 	/** Return all info in the radio config (or an empty array if none). */ | ||||
| 	public JsonArray getRadioConfigList() { | ||||
| 		if (!config.has("radios") || !config.get("radios").isJsonArray()) { | ||||
| 			return new JsonArray(); | ||||
| 	/** Return all info in the radio config (or an empty list if none). */ | ||||
| 	public List<UCentralSchema.Radio> getRadioConfigList() { | ||||
| 		if (config.has("radios") && config.get("radios").isJsonArray()) { | ||||
| 			List<UCentralSchema.Radio> radios = new Gson().fromJson( | ||||
| 				config.getAsJsonArray("radios"), | ||||
| 				new TypeToken<ArrayList<UCentralSchema.Radio>>() {}.getType() | ||||
| 			); | ||||
| 			if (radios != null) { | ||||
| 				return radios; | ||||
| 			} | ||||
| 		return config.getAsJsonArray("radios"); | ||||
| 		} | ||||
| 		return Collections.emptyList(); | ||||
| 	} | ||||
|  | ||||
| 	/** Return all the operational bands of an AP (from the radio config) */ | ||||
| 	public Set<String> getRadioBandsSet(JsonArray radioConfigList) { | ||||
| 	public Set<String> getRadioBandsSet( | ||||
| 		List<UCentralSchema.Radio> radioConfigList | ||||
| 	) { | ||||
| 		Set<String> radioBandsSet = new HashSet<>(); | ||||
| 		if (radioConfigList == null) { | ||||
| 			return radioBandsSet; | ||||
| 		} | ||||
| 		for ( | ||||
| 			int radioIndex = 0; radioIndex < radioConfigList.size(); | ||||
| 			radioIndex++ | ||||
| 		) { | ||||
| 			JsonElement e = radioConfigList.get(radioIndex); | ||||
| 			if (!e.isJsonObject()) { | ||||
| 		for (UCentralSchema.Radio radio : radioConfigList) { | ||||
| 			if (radio == null || radio.band == null) { | ||||
| 				continue; | ||||
| 			} | ||||
| 			JsonObject radioObject = e.getAsJsonObject(); | ||||
| 			if (!radioObject.has("band")) { | ||||
| 				continue; | ||||
| 			} | ||||
| 			radioBandsSet.add(radioObject.get("band").getAsString()); | ||||
| 			radioBandsSet.add(radio.band); | ||||
| 		} | ||||
| 		return radioBandsSet; | ||||
| 	} | ||||
|   | ||||
| @@ -25,6 +25,7 @@ import com.facebook.openwifi.cloudsdk.models.gw.DeviceCapabilities; | ||||
| import com.facebook.openwifi.cloudsdk.models.gw.DeviceConfigureRequest; | ||||
| import com.facebook.openwifi.cloudsdk.models.gw.DeviceListWithStatus; | ||||
| import com.facebook.openwifi.cloudsdk.models.gw.DeviceWithStatus; | ||||
| import com.facebook.openwifi.cloudsdk.models.gw.ScriptRequest; | ||||
| import com.facebook.openwifi.cloudsdk.models.gw.ServiceEvent; | ||||
| import com.facebook.openwifi.cloudsdk.models.gw.StatisticsRecords; | ||||
| import com.facebook.openwifi.cloudsdk.models.gw.SystemInfoResults; | ||||
| @@ -562,6 +563,71 @@ public class UCentralClient { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Run a shell script on a device and return the result, or null upon error. | ||||
| 	 * | ||||
| 	 * @see #runScript(String, String, int) | ||||
| 	 */ | ||||
| 	public CommandInfo runScript(String serialNumber, String script) { | ||||
| 		return runScript(serialNumber, script, 30); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Run a shell script on a device and return the result, or null upon error. | ||||
| 	 * | ||||
| 	 * @see #runScript(String, String, int, String) | ||||
| 	 */ | ||||
| 	public CommandInfo runScript( | ||||
| 		String serialNumber, | ||||
| 		String script, | ||||
| 		int timeoutSec | ||||
| 	) { | ||||
| 		return runScript(serialNumber, script, timeoutSec, "shell"); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Run a script on a device and return the result, or null upon error. | ||||
| 	 * | ||||
| 	 * @param serialNumber the device | ||||
| 	 * @param script the script contents | ||||
| 	 * @param timeoutSec the timeout in seconds | ||||
| 	 * @param type the script type (either "shell" or "ucode") | ||||
| 	 * | ||||
| 	 * @see UCentralUtils#getScriptOutput(CommandInfo) | ||||
| 	 */ | ||||
| 	public CommandInfo runScript( | ||||
| 		String serialNumber, | ||||
| 		String script, | ||||
| 		int timeoutSec, | ||||
| 		String type | ||||
| 	) { | ||||
| 		ScriptRequest req = new ScriptRequest(); | ||||
| 		req.serialNumber = serialNumber; | ||||
| 		req.timeout = timeoutSec; | ||||
| 		req.type = type; | ||||
| 		req.script = script; | ||||
| 		req.scriptId = "1"; // ?? | ||||
| 		HttpResponse<String> response = httpPost( | ||||
| 			String.format("device/%s/script", serialNumber), | ||||
| 			OWGW_SERVICE, | ||||
| 			req | ||||
| 		); | ||||
| 		if (!response.isSuccess()) { | ||||
| 			logger.error("Error: {}", response.getBody()); | ||||
| 			return null; | ||||
| 		} | ||||
| 		try { | ||||
| 			return gson.fromJson(response.getBody(), CommandInfo.class); | ||||
| 		} catch (JsonSyntaxException e) { | ||||
| 			String errMsg = String.format( | ||||
| 				"Failed to deserialize to CommandInfo: %s", | ||||
| 				response.getBody() | ||||
| 			); | ||||
| 			logger.error(errMsg, e); | ||||
| 			return null; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/** Retrieve a list of inventory from owprov. */ | ||||
| 	public InventoryTagList getProvInventory() { | ||||
| 		HttpResponse<String> response = httpGet("inventory", OWPROV_SERVICE); | ||||
| @@ -624,6 +690,19 @@ public class UCentralClient { | ||||
| 		try { | ||||
| 			return gson.fromJson(response.getBody(), RRMDetails.class); | ||||
| 		} catch (JsonSyntaxException e) { | ||||
| 			// catch strings like "no", "inherit", "invalid" (???) | ||||
| 			JSONObject respBody; | ||||
| 			try { | ||||
| 				respBody = new JSONObject(response.getBody()); | ||||
| 				respBody.getString("rrm"); | ||||
| 				logger.error( | ||||
| 					"RRMDetails returned unexpected string body: {}", | ||||
| 					respBody | ||||
| 				); | ||||
| 				return null; | ||||
| 			} catch (JSONException e2) { /* ignore and fall through */} | ||||
|  | ||||
| 			// otherwise, log as a deserialization error | ||||
| 			String errMsg = String.format( | ||||
| 				"Failed to deserialize to RRMDetails: %s", | ||||
| 				response.getBody() | ||||
|   | ||||
| @@ -8,8 +8,10 @@ | ||||
|  | ||||
| package com.facebook.openwifi.cloudsdk; | ||||
|  | ||||
| import java.nio.charset.StandardCharsets; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Arrays; | ||||
| import java.util.Base64; | ||||
| import java.util.Collections; | ||||
| import java.util.HashMap; | ||||
| import java.util.HashSet; | ||||
| @@ -17,6 +19,8 @@ import java.util.List; | ||||
| import java.util.Map; | ||||
| import java.util.Map.Entry; | ||||
| import java.util.Set; | ||||
| import java.util.zip.DataFormatException; | ||||
| import java.util.zip.Inflater; | ||||
|  | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
| @@ -24,8 +28,12 @@ import org.slf4j.LoggerFactory; | ||||
| import com.facebook.openwifi.cloudsdk.ies.Country; | ||||
| import com.facebook.openwifi.cloudsdk.ies.LocalPowerConstraint; | ||||
| import com.facebook.openwifi.cloudsdk.ies.QbssLoad; | ||||
| import com.facebook.openwifi.cloudsdk.ies.RMEnabledCapabilities; | ||||
| import com.facebook.openwifi.cloudsdk.ies.TxPwrInfo; | ||||
| import com.facebook.openwifi.cloudsdk.models.ap.Capabilities; | ||||
| import com.facebook.openwifi.cloudsdk.models.ap.State; | ||||
| import com.facebook.openwifi.cloudsdk.models.ap.UCentralSchema; | ||||
| import com.facebook.openwifi.cloudsdk.models.gw.CommandInfo; | ||||
| import com.google.gson.Gson; | ||||
| import com.google.gson.JsonArray; | ||||
| import com.google.gson.JsonElement; | ||||
| @@ -166,6 +174,10 @@ public class UCentralUtils { | ||||
| 				case TxPwrInfo.TYPE: | ||||
| 					ieContainer.txPwrInfo = TxPwrInfo.parse(contents); | ||||
| 					break; | ||||
| 				case RMEnabledCapabilities.TYPE: | ||||
| 					ieContainer.rmEnabledCapabilities = | ||||
| 						RMEnabledCapabilities.parse(contents); | ||||
| 					break; | ||||
| 				} | ||||
| 			} catch (Exception e) { | ||||
| 				logger.error(String.format("Skipping invalid IE %s", ie), e); | ||||
| @@ -268,28 +280,20 @@ public class UCentralUtils { | ||||
| 	 * Returns the results map | ||||
| 	 */ | ||||
| 	public static Map<String, List<String>> getBandsMap( | ||||
| 		Map<String, JsonArray> deviceStatus | ||||
| 		Map<String, List<UCentralSchema.Radio>> deviceStatus | ||||
| 	) { | ||||
| 		Map<String, List<String>> bandsMap = new HashMap<>(); | ||||
|  | ||||
| 		for (String serialNumber : deviceStatus.keySet()) { | ||||
| 			JsonArray radioList = | ||||
| 				deviceStatus.get(serialNumber).getAsJsonArray(); | ||||
| 		for ( | ||||
| 				int radioIndex = 0; radioIndex < radioList.size(); radioIndex++ | ||||
| 			Map.Entry<String, List<UCentralSchema.Radio>> entry : deviceStatus | ||||
| 				.entrySet() | ||||
| 		) { | ||||
| 				JsonElement e = radioList.get(radioIndex); | ||||
| 				if (!e.isJsonObject()) { | ||||
| 					return null; | ||||
| 				} | ||||
| 				JsonObject radioObject = e.getAsJsonObject(); | ||||
| 				String band = radioObject.get("band").getAsString(); | ||||
| 			String serialNumber = entry.getKey(); | ||||
| 			for (UCentralSchema.Radio radio : entry.getValue()) { | ||||
| 				bandsMap | ||||
| 					.computeIfAbsent(band, k -> new ArrayList<>()) | ||||
| 					.computeIfAbsent(radio.band, k -> new ArrayList<>()) | ||||
| 					.add(serialNumber); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		return bandsMap; | ||||
| 	} | ||||
|  | ||||
| @@ -303,75 +307,61 @@ public class UCentralUtils { | ||||
| 	 * @return the results map of {band, {device, list of available channels}} | ||||
| 	 */ | ||||
| 	public static Map<String, Map<String, List<Integer>>> getDeviceAvailableChannels( | ||||
| 		Map<String, JsonArray> deviceStatus, | ||||
| 		Map<String, JsonObject> deviceCapabilities, | ||||
| 		Map<String, List<UCentralSchema.Radio>> deviceStatus, | ||||
| 		Map<String, Map<String, Capabilities.Phy>> deviceCapabilities, | ||||
| 		Map<String, List<Integer>> defaultAvailableChannels | ||||
| 	) { | ||||
| 		Map<String, Map<String, List<Integer>>> deviceAvailableChannels = | ||||
| 			new HashMap<>(); | ||||
|  | ||||
| 		for (String serialNumber : deviceStatus.keySet()) { | ||||
| 			JsonArray radioList = | ||||
| 				deviceStatus.get(serialNumber).getAsJsonArray(); | ||||
| 		for ( | ||||
| 				int radioIndex = 0; radioIndex < radioList.size(); radioIndex++ | ||||
| 			Map.Entry<String, List<UCentralSchema.Radio>> entry : deviceStatus | ||||
| 				.entrySet() | ||||
| 		) { | ||||
| 				JsonElement e = radioList.get(radioIndex); | ||||
| 				if (!e.isJsonObject()) { | ||||
| 					return null; | ||||
| 				} | ||||
| 				JsonObject radioObject = e.getAsJsonObject(); | ||||
| 				String band = radioObject.get("band").getAsString(); | ||||
|  | ||||
| 				JsonObject capabilitesObject = | ||||
| 			String serialNumber = entry.getKey(); | ||||
| 			for (UCentralSchema.Radio radio : entry.getValue()) { | ||||
| 				Map<String, Capabilities.Phy> capabilitiesPhyMap = | ||||
| 					deviceCapabilities.get(serialNumber); | ||||
| 				List<Integer> availableChannels = new ArrayList<>(); | ||||
| 				if (capabilitesObject == null) { | ||||
| 				if (capabilitiesPhyMap == null) { | ||||
| 					availableChannels | ||||
| 						.addAll(defaultAvailableChannels.get(band)); | ||||
| 						.addAll(defaultAvailableChannels.get(radio.band)); | ||||
| 				} else { | ||||
| 					Set<Entry<String, JsonElement>> entrySet = capabilitesObject | ||||
| 					Set<Entry<String, Capabilities.Phy>> entrySet = | ||||
| 						capabilitiesPhyMap | ||||
| 							.entrySet(); | ||||
| 					for (Map.Entry<String, JsonElement> f : entrySet) { | ||||
| 						String bandInsideObject = f.getValue() | ||||
| 							.getAsJsonObject() | ||||
| 							.get("band") | ||||
| 							.getAsString(); | ||||
| 						if (bandInsideObject.equals(band)) { | ||||
| 					for (Map.Entry<String, Capabilities.Phy> f : entrySet) { | ||||
| 						Capabilities.Phy phy = f.getValue(); | ||||
| 						String bandInsideObject = phy.band.toString(); | ||||
| 						if (bandInsideObject.equals(radio.band)) { | ||||
| 							// (TODO) Remove the following dfsChannels code block | ||||
| 							// when the DFS channels are available | ||||
| 							Set<Integer> dfsChannels = new HashSet<>(); | ||||
| 							try { | ||||
| 								JsonArray channelInfo = f.getValue() | ||||
| 									.getAsJsonObject() | ||||
| 									.get("dfs_channels") | ||||
| 									.getAsJsonArray(); | ||||
|  | ||||
| 								for (JsonElement d : channelInfo) { | ||||
| 									dfsChannels.add(d.getAsInt()); | ||||
| 								int[] channelInfo = phy.dfs_channels; | ||||
| 								for (int d : channelInfo) { | ||||
| 									dfsChannels.add(d); | ||||
| 								} | ||||
| 							} catch (Exception d) {} | ||||
| 							try { | ||||
| 								JsonArray channelInfo = f.getValue() | ||||
| 									.getAsJsonObject() | ||||
| 									.get("channels") | ||||
| 									.getAsJsonArray(); | ||||
| 								for (JsonElement c : channelInfo) { | ||||
| 									int channel = c.getAsInt(); | ||||
| 								int[] channelInfo = phy.channels; | ||||
| 								for (int channel : channelInfo) { | ||||
| 									if (!dfsChannels.contains(channel)) { | ||||
| 										availableChannels.add(channel); | ||||
| 									} | ||||
| 								} | ||||
| 							} catch (Exception c) { | ||||
| 								availableChannels | ||||
| 									.addAll(defaultAvailableChannels.get(band)); | ||||
| 									.addAll( | ||||
| 										defaultAvailableChannels.get(radio.band) | ||||
| 									); | ||||
| 							} | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 				deviceAvailableChannels.computeIfAbsent( | ||||
| 					band, | ||||
| 					radio.band, | ||||
| 					k -> new HashMap<>() | ||||
| 				) | ||||
| 					.put( | ||||
| @@ -390,10 +380,10 @@ public class UCentralUtils { | ||||
| 	 * Returns the results map | ||||
| 	 */ | ||||
| 	public static Map<String, String> getBssidsMap( | ||||
| 		Map<String, State> latestState | ||||
| 		Map<String, ? extends State> latestState | ||||
| 	) { | ||||
| 		Map<String, String> bssidMap = new HashMap<>(); | ||||
| 		for (Map.Entry<String, State> e : latestState.entrySet()) { | ||||
| 		for (Entry<String, ? extends State> e : latestState.entrySet()) { | ||||
| 			State state = e.getValue(); | ||||
| 			for ( | ||||
| 				int interfaceIndex = 0; | ||||
| @@ -485,4 +475,116 @@ public class UCentralUtils { | ||||
| 			return null; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Return a map of Wi-Fi client (STA) MAC addresses to the Client structure | ||||
| 	 * found for that interface. This does NOT support clients connected on | ||||
| 	 * multiple interfaces simultaneously. | ||||
| 	 */ | ||||
| 	public static Map<String, State.Interface.Client> getWifiClientInfo( | ||||
| 		State state | ||||
| 	) { | ||||
| 		Map<String, State.Interface.Client> ret = new HashMap<>(); | ||||
|  | ||||
| 		// Aggregate over all interfaces | ||||
| 		for (State.Interface iface : state.interfaces) { | ||||
| 			if (iface.ssids == null || iface.clients == null) { | ||||
| 				continue; | ||||
| 			} | ||||
|  | ||||
| 			// Convert client array to map (for faster lookups) | ||||
| 			Map<String, State.Interface.Client> ifaceMap = new HashMap<>(); | ||||
| 			for (State.Interface.Client client : iface.clients) { | ||||
| 				ifaceMap.put(client.mac, client); | ||||
| 			} | ||||
|  | ||||
| 			// Loop over all SSIDs and connected clients | ||||
| 			for (State.Interface.SSID ssid : iface.ssids) { | ||||
| 				if (ssid.associations == null) { | ||||
| 					continue; | ||||
| 				} | ||||
| 				for ( | ||||
| 					State.Interface.SSID.Association association : ssid.associations | ||||
| 				) { | ||||
| 					State.Interface.Client client = | ||||
| 						ifaceMap.get(association.station); | ||||
| 					if (client != null) { | ||||
| 						ret.put(association.station, client); | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		return ret; | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Decompress (inflate) a UTF-8 string using ZLIB. | ||||
| 	 * | ||||
| 	 * @param compressed the compressed string | ||||
| 	 * @param uncompressedSize the uncompressed size (must be known) | ||||
| 	 */ | ||||
| 	private static String inflate(String compressed, int uncompressedSize) | ||||
| 		throws DataFormatException { | ||||
| 		if (compressed == null) { | ||||
| 			throw new NullPointerException("Null compressed string"); | ||||
| 		} | ||||
| 		if (uncompressedSize < 0) { | ||||
| 			throw new IllegalArgumentException("Invalid size"); | ||||
| 		} | ||||
|  | ||||
| 		byte[] input = compressed.getBytes(StandardCharsets.UTF_8); | ||||
| 		byte[] output = new byte[uncompressedSize]; | ||||
|  | ||||
| 		Inflater inflater = new Inflater(); | ||||
| 		inflater.setInput(input, 0, input.length); | ||||
| 		inflater.inflate(output); | ||||
| 		inflater.end(); | ||||
|  | ||||
| 		return new String(output, StandardCharsets.UTF_8); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Given the result of the "script" API, return the actual script output | ||||
| 	 * (decoded/decompressed if needed), or null if the script returned an | ||||
| 	 * error. | ||||
| 	 * | ||||
| 	 * @see UCentralClient#runScript(String, String, int, String) | ||||
| 	 */ | ||||
| 	public static String getScriptOutput(CommandInfo info) { | ||||
| 		if (info == null || info.results == null) { | ||||
| 			return null; | ||||
| 		} | ||||
| 		if (!info.results.has("status")) { | ||||
| 			return null; | ||||
| 		} | ||||
| 		JsonObject status = info.results.get("status").getAsJsonObject(); | ||||
| 		if (!status.has("error")) { | ||||
| 			return null; | ||||
| 		} | ||||
| 		int errorCode = status.get("error").getAsInt(); | ||||
| 		if (errorCode != 0) { | ||||
| 			logger.error("Script failed with code {}", errorCode); | ||||
| 			return null; | ||||
| 		} | ||||
| 		if (status.has("result")) { | ||||
| 			// Raw result | ||||
| 			return status.get("result").getAsString(); | ||||
| 		} else if (status.has("result_64") && status.has("result_sz")) { | ||||
| 			// Base64+compressed result | ||||
| 			// NOTE: untested, not actually implemented on ucentral-client? | ||||
| 			try { | ||||
| 				String encoded = status.get("result_64").getAsString(); | ||||
| 				int uncompressedSize = status.get("result_sz").getAsInt(); | ||||
| 				String decoded = new String( | ||||
| 					Base64.getDecoder().decode(encoded), | ||||
| 					StandardCharsets.UTF_8 | ||||
| 				); | ||||
| 				return inflate(decoded, uncompressedSize); | ||||
| 			} catch (Exception e) { | ||||
| 				logger.error("Failed to decode or inflate script result", e); | ||||
| 			} | ||||
| 		} | ||||
| 		return null; | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,155 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| package com.facebook.openwifi.cloudsdk.ies; | ||||
|  | ||||
| import java.util.Objects; | ||||
|  | ||||
| import com.google.gson.JsonObject; | ||||
|  | ||||
| // NOTE: Not validated (not seen on test devices) | ||||
| /** | ||||
|  * This information element (IE) appears in wifiscan entries. It's called "BSS AC Access Delay" in | ||||
|  * 802.11 specs (section 9.4.2.43). Refer to the specification for more details. | ||||
|  * Language in javadocs is taken from the specification. | ||||
|  */ | ||||
| public class BssAcAccessDelay { | ||||
| 	/** Defined in 802.11 table 9-92 */ | ||||
| 	public static final int TYPE = 68; | ||||
|  | ||||
| 	/** | ||||
| 	 * Subfield that goes into Access Category Access Delay field in BSS AC | ||||
| 	 * Access Delay. For information on what the values mean, check section | ||||
| 	 * 9.4.2.43 | ||||
| 	 */ | ||||
| 	public static class AccessCategoryAccessDelay { | ||||
| 		/** | ||||
| 		 * Unsigned 8 bits that represents a scaled representation of best effort AC | ||||
| 		 * access delay | ||||
| 		 */ | ||||
| 		public final short averageAccessDelayForBestEffort; | ||||
| 		/** | ||||
| 		 * Unsigned 8 bits that represents a scaled representation of background AC | ||||
| 		 * access delay | ||||
| 		 */ | ||||
| 		public final short averageAccessDelayForBackground; | ||||
| 		/** | ||||
| 		 * Unsigned 8 bits that represents a scaled representation of video AC access | ||||
| 		 * delay | ||||
| 		 */ | ||||
| 		public final short averageAccessDelayForVideo; | ||||
| 		/** | ||||
| 		 * Unsigned 8 bits that represents a scaled representation of voice AC access | ||||
| 		 * delay | ||||
| 		 */ | ||||
| 		public final short averageAccessDelayForVoice; | ||||
|  | ||||
| 		/** Constructor */ | ||||
| 		public AccessCategoryAccessDelay( | ||||
| 			short averageAccessDelayForBestEffort, | ||||
| 			short averageAccessDelayForBackground, | ||||
| 			short averageAccessDelayForVideo, | ||||
| 			short averageAccessDelayForVoice | ||||
| 		) { | ||||
| 			this.averageAccessDelayForBestEffort = | ||||
| 				averageAccessDelayForBestEffort; | ||||
| 			this.averageAccessDelayForBackground = | ||||
| 				averageAccessDelayForBackground; | ||||
| 			this.averageAccessDelayForVideo = averageAccessDelayForVideo; | ||||
| 			this.averageAccessDelayForVoice = averageAccessDelayForVoice; | ||||
| 		} | ||||
|  | ||||
| 		/** Parse AccessCategoryAccessDelay from JSON object */ | ||||
| 		// TODO rename fields as necessary - we don't know how the data format yet | ||||
| 		public static AccessCategoryAccessDelay parse(JsonObject contents) { | ||||
| 			return new AccessCategoryAccessDelay( | ||||
| 				contents.get("Average Access Delay For Best Effort").getAsShort(), | ||||
| 				contents.get("Average Access Delay For Background").getAsShort(), | ||||
| 				contents.get("Average Access Delay For Video").getAsShort(), | ||||
| 				contents.get("Average Access Delay For Voice").getAsShort() | ||||
| 			); | ||||
| 		} | ||||
|  | ||||
| 		@Override | ||||
| 		public int hashCode() { | ||||
| 			return Objects.hash( | ||||
| 				averageAccessDelayForBestEffort, | ||||
| 				averageAccessDelayForBestEffort, | ||||
| 				averageAccessDelayForVideo, | ||||
| 				averageAccessDelayForVoice | ||||
| 			); | ||||
| 		} | ||||
|  | ||||
| 		@Override | ||||
| 		public boolean equals(Object obj) { | ||||
| 			if (obj == null) { | ||||
| 				return false; | ||||
| 			} | ||||
|  | ||||
| 			if (this == obj) { | ||||
| 				return true; | ||||
| 			} | ||||
|  | ||||
| 			if (getClass() != obj.getClass()) { | ||||
| 				return false; | ||||
| 			} | ||||
|  | ||||
| 			AccessCategoryAccessDelay other = (AccessCategoryAccessDelay) obj; | ||||
| 			return averageAccessDelayForBestEffort == | ||||
| 				other.averageAccessDelayForBestEffort && | ||||
| 				averageAccessDelayForBackground == | ||||
| 					other.averageAccessDelayForBackground && | ||||
| 				averageAccessDelayForVideo == | ||||
| 					other.averageAccessDelayForVideo && | ||||
| 				averageAccessDelayForVoice == other.averageAccessDelayForVoice; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/** 32 bits - Holds AccessCategoryAccessDelay subfield */ | ||||
| 	public final AccessCategoryAccessDelay accessCategoryAccessDelay; | ||||
|  | ||||
| 	/** Constructor */ | ||||
| 	public BssAcAccessDelay( | ||||
| 		AccessCategoryAccessDelay accessCategoryAccessDelay | ||||
| 	) { | ||||
| 		this.accessCategoryAccessDelay = accessCategoryAccessDelay; | ||||
| 	} | ||||
|  | ||||
| 	/** Parse BssAcAccessDelay from JSON object */ | ||||
| 	// TODO rename fields as necessary - we don't know how the data format yet | ||||
| 	public static BssAcAccessDelay parse(JsonObject contents) { | ||||
| 		return new BssAcAccessDelay( | ||||
| 			AccessCategoryAccessDelay.parse( | ||||
| 				contents.get("AP Average Access Delay").getAsJsonObject() | ||||
| 			) | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public int hashCode() { | ||||
| 		return Objects.hash(accessCategoryAccessDelay); | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public boolean equals(Object obj) { | ||||
| 		if (obj == null) { | ||||
| 			return false; | ||||
| 		} | ||||
|  | ||||
| 		if (this == obj) { | ||||
| 			return true; | ||||
| 		} | ||||
|  | ||||
| 		if (getClass() != obj.getClass()) { | ||||
| 			return false; | ||||
| 		} | ||||
|  | ||||
| 		BssAcAccessDelay other = (BssAcAccessDelay) obj; | ||||
| 		return accessCategoryAccessDelay == other.accessCategoryAccessDelay; | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,68 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| package com.facebook.openwifi.cloudsdk.ies; | ||||
|  | ||||
| import java.util.Objects; | ||||
|  | ||||
| import com.google.gson.JsonObject; | ||||
|  | ||||
| // NOTE: Not validated (not seen on test devices) | ||||
| /** | ||||
|  * This information element (IE) appears in wifiscan entries. It's called "BSS Average Access Delay" in | ||||
|  * 802.11 specs (section 9.4.2.38). Refer to the specification for more details. | ||||
|  * Language in javadocs is taken from the specification. | ||||
|  */ | ||||
| public class BssAvgAccessDelay { | ||||
| 	/** Defined in 802.11 table 9-92 */ | ||||
| 	public static final int TYPE = 63; | ||||
|  | ||||
| 	/** | ||||
| 	 * Unsigned 8 bits representing a scaled average medium access delay for all DCF | ||||
| 	 * and EDCAF frames transmitted, measured from the time it's ready for | ||||
| 	 * transmission to actual transmission start time. | ||||
| 	 */ | ||||
| 	public final short apAvgAccessDelay; | ||||
|  | ||||
| 	/** Constructor */ | ||||
| 	public BssAvgAccessDelay(short apAvgAccessDelay) { | ||||
| 		this.apAvgAccessDelay = apAvgAccessDelay; | ||||
| 	} | ||||
|  | ||||
| 	/** Parse BssAvgAccessDelay from JSON object */ | ||||
| 	// TODO modify this method as necessary - since the IE doesn't seem to be | ||||
| 	// present, we have no idea what the format looks like | ||||
| 	public static BssAvgAccessDelay parse(JsonObject contents) { | ||||
| 		return new BssAvgAccessDelay( | ||||
| 			contents.get("AP Average Access Delay").getAsShort() | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public int hashCode() { | ||||
| 		return Objects.hash(apAvgAccessDelay); | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public boolean equals(Object obj) { | ||||
| 		if (obj == null) { | ||||
| 			return false; | ||||
| 		} | ||||
|  | ||||
| 		if (this == obj) { | ||||
| 			return true; | ||||
| 		} | ||||
|  | ||||
| 		if (getClass() != obj.getClass()) { | ||||
| 			return false; | ||||
| 		} | ||||
|  | ||||
| 		BssAvgAccessDelay other = (BssAvgAccessDelay) obj; | ||||
| 		return apAvgAccessDelay == other.apAvgAccessDelay; | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,87 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| package com.facebook.openwifi.cloudsdk.ies; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
| import java.util.Objects; | ||||
|  | ||||
| import com.google.gson.JsonElement; | ||||
| import com.google.gson.JsonObject; | ||||
|  | ||||
| // NOTE: Not validated (not seen on test devices) | ||||
| /** | ||||
|  * This information element (IE) appears in wifiscan entries. It's called "20/40 | ||||
|  * BSS Intolerant Channel Report" in 802.11 specs (section 9.4.2.57). Refer to | ||||
|  * the specification for more details. Language in javadocs is taken from the | ||||
|  * specification. | ||||
|  */ | ||||
| public class BssIntolerantChannelReport { | ||||
| 	/** Defined in 802.11 table 9-92 */ | ||||
| 	public static final int TYPE = 73; | ||||
|  | ||||
| 	/** | ||||
| 	 * Unsigned 8 bits representing the operating class in which the channel list | ||||
| 	 * is valid | ||||
| 	 */ | ||||
| 	public final short operatingClass; | ||||
| 	/** List of unsigned 8 bits, representing the channel numbers */ | ||||
| 	public final List<Short> channelList; | ||||
|  | ||||
| 	/** Constructor */ | ||||
| 	public BssIntolerantChannelReport( | ||||
| 		short operatingClass, | ||||
| 		List<Short> channelList | ||||
| 	) { | ||||
| 		this.operatingClass = operatingClass; | ||||
| 		this.channelList = Collections.unmodifiableList(channelList); | ||||
| 	} | ||||
|  | ||||
| 	/** Parse BssIntolerantChannelReport from JSON object */ | ||||
| 	// TODO rename fields as necessary - we don't know how the data format yet | ||||
| 	public static BssIntolerantChannelReport parse(JsonObject contents) { | ||||
| 		List<Short> channelList = new ArrayList<>(); | ||||
| 		JsonElement channelListJson = contents.get("Channel List"); | ||||
| 		if (channelListJson != null) { | ||||
| 			for (JsonElement elem : channelListJson.getAsJsonArray()) { | ||||
| 				channelList.add(elem.getAsShort()); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		return new BssIntolerantChannelReport( | ||||
| 			contents.get("Operating Class").getAsShort(), | ||||
| 			channelList | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public int hashCode() { | ||||
| 		return Objects.hash(operatingClass, channelList); | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public boolean equals(Object obj) { | ||||
| 		if (obj == null) { | ||||
| 			return false; | ||||
| 		} | ||||
|  | ||||
| 		if (this == obj) { | ||||
| 			return true; | ||||
| 		} | ||||
|  | ||||
| 		if (getClass() != obj.getClass()) { | ||||
| 			return false; | ||||
| 		} | ||||
|  | ||||
| 		BssIntolerantChannelReport other = (BssIntolerantChannelReport) obj; | ||||
| 		return operatingClass == other.operatingClass && | ||||
| 			channelList.equals(other.channelList); | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,201 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| package com.facebook.openwifi.cloudsdk.ies; | ||||
|  | ||||
| import java.util.Objects; | ||||
|  | ||||
| import com.google.gson.JsonObject; | ||||
|  | ||||
| // NOTE: Not validated (not seen on test devices) | ||||
| /** | ||||
|  * This information element (IE) appears in wifiscan entries. It's called | ||||
|  * "Collocated Interference Report" in 802.11 specs (section 9.4.2.84). Refer to | ||||
|  * the specification for more details. Language in javadocs is taken from the | ||||
|  * specification. | ||||
|  */ | ||||
| public class CollocatedInterferenceReport { | ||||
| 	/** Defined in 802.11 table 9-92 */ | ||||
| 	public static final int TYPE = 96; | ||||
|  | ||||
| 	public static class InterferenceAccuracyAndIndex { | ||||
| 		/** | ||||
| 		 * Unsigned int (4 bits) representing expected accuracy of the estimate of | ||||
| 		 * interference in dB with 95% confidence interval | ||||
| 		 */ | ||||
| 		public final byte expectedAccuracy; | ||||
| 		/** | ||||
| 		 * Unsigned int (4 bits) indicating the interference index that is unique for | ||||
| 		 * each type of interference source | ||||
| 		 */ | ||||
| 		public final byte interferenceIndex; | ||||
|  | ||||
| 		/** Constructor */ | ||||
| 		public InterferenceAccuracyAndIndex( | ||||
| 			byte expectedAccuracy, | ||||
| 			byte interferenceIndex | ||||
| 		) { | ||||
| 			this.expectedAccuracy = expectedAccuracy; | ||||
| 			this.interferenceIndex = interferenceIndex; | ||||
| 		} | ||||
|  | ||||
| 		/** Parse InterferenceAccuracyAndIndex from JSON object */ | ||||
| 		// TODO modify this method as necessary - since the IE doesn't seem to be | ||||
| 		// present, we have no idea what the format looks like | ||||
| 		public static InterferenceAccuracyAndIndex parse(JsonObject contents) { | ||||
| 			return new InterferenceAccuracyAndIndex( | ||||
| 				contents.get("Expected Accuracy").getAsByte(), | ||||
| 				contents.get("Interference Index").getAsByte() | ||||
| 			); | ||||
| 		} | ||||
|  | ||||
| 		@Override | ||||
| 		public int hashCode() { | ||||
| 			return Objects.hash(expectedAccuracy, interferenceIndex); | ||||
| 		} | ||||
|  | ||||
| 		@Override | ||||
| 		public boolean equals(Object obj) { | ||||
| 			if (obj == null) { | ||||
| 				return false; | ||||
| 			} | ||||
|  | ||||
| 			if (this == obj) { | ||||
| 				return true; | ||||
| 			} | ||||
|  | ||||
| 			if (getClass() != obj.getClass()) { | ||||
| 				return false; | ||||
| 			} | ||||
|  | ||||
| 			InterferenceAccuracyAndIndex other = | ||||
| 				(InterferenceAccuracyAndIndex) obj; | ||||
| 			return expectedAccuracy == other.expectedAccuracy && | ||||
| 				interferenceIndex == other.interferenceIndex; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/** Unsigned 8 bits representing when the report is generated */ | ||||
| 	public final short reportPeriod; | ||||
| 	/** | ||||
| 	 * signed 8 bits representing the maximum level of the collocated | ||||
| 	 * interference power in units of dBm over all receive chains averaged over a | ||||
| 	 * 4 microsecond period during an interference period and across interference | ||||
| 	 * bandwidth | ||||
| 	 */ | ||||
| 	public final byte interferenceLevel; | ||||
| 	/** Subfield for interference level accuracy and index - 8 bits */ | ||||
| 	public final InterferenceAccuracyAndIndex interferenceAccuracyAndIndex; | ||||
| 	/** | ||||
| 	 * Unsigned 32 bits representing the interval between two successibe periods | ||||
| 	 * of interference in microseconds | ||||
| 	 */ | ||||
| 	public final long interferenceInterval; | ||||
| 	/** | ||||
| 	 * Unsigned 32 bits representing the duration of each period of interference in | ||||
| 	 * microseconds | ||||
| 	 */ | ||||
| 	public final long interferenceBurstLength; | ||||
| 	/** | ||||
| 	 * Unsigned 32 bits contains the least significant 4 octets (i.e., B0–B31) of | ||||
| 	 * the TSF timer at the start of the interference burst. When either the | ||||
| 	 * Interference Interval or the Interference Burst Length fields are set to | ||||
| 	 * 2^32 – 1, this field indicates the average duty cycle | ||||
| 	 */ | ||||
| 	public final long interferenceStartTimeDutyCycle; | ||||
| 	/** | ||||
| 	 * Unsigned 32 bits representing indicates the center frequency of interference | ||||
| 	 * in units of 5 kHz | ||||
| 	 */ | ||||
| 	public final long interferenceCenterFrequency; | ||||
| 	/** | ||||
| 	 * Unsigned 16 bits representing the bandwidth in units of 5 kHz at the –3 dB | ||||
| 	 * roll-off point of the interference signal | ||||
| 	 */ | ||||
| 	public final short interferenceBandwidth; | ||||
|  | ||||
| 	/** Constructor */ | ||||
| 	public CollocatedInterferenceReport( | ||||
| 		short reportPeriod, | ||||
| 		byte interferenceLevel, | ||||
| 		InterferenceAccuracyAndIndex interferenceAccuracyAndIndex, | ||||
| 		long interferenceInterval, | ||||
| 		long interferenceBurstLength, | ||||
| 		long interferenceStartTimeDutyCycle, | ||||
| 		long interferenceCenterFrequency, | ||||
| 		short interferenceBandwidth | ||||
| 	) { | ||||
| 		this.reportPeriod = reportPeriod; | ||||
| 		this.interferenceLevel = interferenceLevel; | ||||
| 		this.interferenceAccuracyAndIndex = interferenceAccuracyAndIndex; | ||||
| 		this.interferenceInterval = interferenceInterval; | ||||
| 		this.interferenceBurstLength = interferenceBurstLength; | ||||
| 		this.interferenceStartTimeDutyCycle = interferenceStartTimeDutyCycle; | ||||
| 		this.interferenceCenterFrequency = interferenceCenterFrequency; | ||||
| 		this.interferenceBandwidth = interferenceBandwidth; | ||||
| 	} | ||||
|  | ||||
| 	/** Parse CollocatedInterferenceReport from JSON object */ | ||||
| 	// TODO rename fields as necessary - we don't know how the data format yet | ||||
| 	public static CollocatedInterferenceReport parse(JsonObject contents) { | ||||
| 		return new CollocatedInterferenceReport( | ||||
| 			contents.get("Report Period").getAsShort(), | ||||
| 			contents.get("Intereference Level").getAsByte(), | ||||
| 			InterferenceAccuracyAndIndex | ||||
| 				.parse( | ||||
| 					contents.get("Interference Level Accuracy/Inteference Index").getAsJsonObject() | ||||
| 				), | ||||
| 			contents.get("Interference Interval").getAsLong(), | ||||
| 			contents.get("Interference Burst Length").getAsLong(), | ||||
| 			contents.get("Interference Start Time/Duty Cycle").getAsLong(), | ||||
| 			contents.get("Interference Center Frequency").getAsLong(), | ||||
| 			contents.get("Interference Bandwidth").getAsShort() | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public int hashCode() { | ||||
| 		return Objects.hash( | ||||
| 			reportPeriod, | ||||
| 			interferenceLevel, | ||||
| 			interferenceAccuracyAndIndex, | ||||
| 			interferenceInterval, | ||||
| 			interferenceBurstLength, | ||||
| 			interferenceStartTimeDutyCycle, | ||||
| 			interferenceCenterFrequency, | ||||
| 			interferenceBandwidth | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public boolean equals(Object obj) { | ||||
| 		if (obj == null) { | ||||
| 			return false; | ||||
| 		} | ||||
|  | ||||
| 		if (this == obj) { | ||||
| 			return true; | ||||
| 		} | ||||
|  | ||||
| 		if (getClass() != obj.getClass()) { | ||||
| 			return false; | ||||
| 		} | ||||
|  | ||||
| 		CollocatedInterferenceReport other = (CollocatedInterferenceReport) obj; | ||||
| 		return reportPeriod == other.reportPeriod && | ||||
| 			interferenceLevel == other.interferenceLevel && | ||||
| 			interferenceAccuracyAndIndex | ||||
| 				.equals(other.interferenceAccuracyAndIndex) && | ||||
| 			interferenceInterval == other.interferenceInterval && | ||||
| 			interferenceBurstLength == other.interferenceBurstLength && | ||||
| 			interferenceStartTimeDutyCycle == | ||||
| 				other.interferenceStartTimeDutyCycle && | ||||
| 			interferenceCenterFrequency == other.interferenceCenterFrequency && | ||||
| 			interferenceBandwidth == other.interferenceBandwidth; | ||||
| 	} | ||||
| } | ||||
| @@ -18,35 +18,35 @@ import com.google.gson.JsonObject; | ||||
|  | ||||
| /** | ||||
|  * This information element (IE) appears in wifiscan entries. | ||||
|  * Refer to the 802.11 specification for more details. Language in | ||||
|  * javadocs is taken from the specification. | ||||
|  * Refer to the 802.11 specification (section 9.4.2.8) for more details. | ||||
|  * Language in javadocs is taken from the specification. | ||||
|  */ | ||||
| public class Country { | ||||
| 	/** Defined in 802.11 */ | ||||
| 	/** Defined in 802.11 table 9-92 */ | ||||
| 	public static final int TYPE = 7; | ||||
|  | ||||
| 	/** Constraints for a subset of channels in the AP's country */ | ||||
| 	public static class CountryInfo { | ||||
| 		/** | ||||
| 		 * The lowest channel number in the CountryInfo. | ||||
| 		 * 8 bits unsigned - the lowest channel number in the CountryInfo. | ||||
| 		 */ | ||||
| 		public final int firstChannelNumber; | ||||
| 		public final short firstChannelNumber; | ||||
| 		/** | ||||
| 		 * The maximum power, in dBm, allowed to be transmitted. | ||||
| 		 * 8 bits unsigned - The maximum power, in dBm, allowed to be transmitted. | ||||
| 		 */ | ||||
| 		public final int maximumTransmitPowerLevel; | ||||
| 		public final short maximumTransmitPowerLevel; | ||||
| 		/** | ||||
| 		 * Number of channels this CountryInfo applies to. E.g., if First | ||||
| 		 * Channel Number is 2 and Number of Channels is 4, this CountryInfo | ||||
| 		 * 8 bits unsigned - Number of channels this CountryInfo applies to. E.g., | ||||
| 		 * if First Channel Number is 2 and Number of Channels is 4, this CountryInfo | ||||
| 		 * describes channels 2, 3, 4, and 5. | ||||
| 		 */ | ||||
| 		public final int numberOfChannels; | ||||
| 		public final short numberOfChannels; | ||||
|  | ||||
| 		/** Constructor. */ | ||||
| 		public CountryInfo( | ||||
| 			int firstChannelNumber, | ||||
| 			int maximumTransmitPowerLevel, | ||||
| 			int numberOfChannels | ||||
| 			short firstChannelNumber, | ||||
| 			short maximumTransmitPowerLevel, | ||||
| 			short numberOfChannels | ||||
| 		) { | ||||
| 			this.firstChannelNumber = firstChannelNumber; | ||||
| 			this.maximumTransmitPowerLevel = maximumTransmitPowerLevel; | ||||
| @@ -55,13 +55,13 @@ public class Country { | ||||
|  | ||||
| 		/** Parse CountryInfo from the appropriate Json object. */ | ||||
| 		public static CountryInfo parse(JsonObject contents) { | ||||
| 			final int firstChannelNumber = | ||||
| 				contents.get("First Channel Number").getAsInt(); | ||||
| 			final int maximumTransmitPowerLevel = contents | ||||
| 			final short firstChannelNumber = | ||||
| 				contents.get("First Channel Number").getAsShort(); | ||||
| 			final short maximumTransmitPowerLevel = contents | ||||
| 				.get("Maximum Transmit Power Level (in dBm)") | ||||
| 				.getAsInt(); | ||||
| 			final int numberOfChannels = | ||||
| 				contents.get("Number of Channels").getAsInt(); | ||||
| 				.getAsShort(); | ||||
| 			final short numberOfChannels = | ||||
| 				contents.get("Number of Channels").getAsShort(); | ||||
| 			return new CountryInfo( | ||||
| 				firstChannelNumber, | ||||
| 				maximumTransmitPowerLevel, | ||||
| @@ -94,15 +94,10 @@ public class Country { | ||||
| 				maximumTransmitPowerLevel == other.maximumTransmitPowerLevel && | ||||
| 				numberOfChannels == other.numberOfChannels; | ||||
| 		} | ||||
|  | ||||
| 		@Override | ||||
| 		public String toString() { | ||||
| 			return "CountryInfo [firstChannelNumber=" + firstChannelNumber + | ||||
| 				", maximumTransmitPowerLevel=" + maximumTransmitPowerLevel + | ||||
| 				", numberOfChannels=" + numberOfChannels + "]"; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/** Country */ | ||||
| 	public final String country; | ||||
| 	/** | ||||
| 	 * Each constraint is a CountryInfo describing tx power constraints on | ||||
| 	 * one or more channels, for the current country. | ||||
| @@ -110,7 +105,11 @@ public class Country { | ||||
| 	public final List<CountryInfo> constraints; | ||||
|  | ||||
| 	/** Constructor */ | ||||
| 	public Country(List<CountryInfo> countryInfos) { | ||||
| 	public Country( | ||||
| 		String country, | ||||
| 		List<CountryInfo> countryInfos | ||||
| 	) { | ||||
| 		this.country = country; | ||||
| 		this.constraints = Collections.unmodifiableList(countryInfos); | ||||
| 	} | ||||
|  | ||||
| @@ -126,7 +125,10 @@ public class Country { | ||||
| 				constraints.add(countryInfo); | ||||
| 			} | ||||
| 		} | ||||
| 		return new Country(constraints); | ||||
| 		return new Country( | ||||
| 			contents.get("Code").getAsString(), | ||||
| 			constraints | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| @@ -148,9 +150,4 @@ public class Country { | ||||
| 		Country other = (Country) obj; | ||||
| 		return Objects.equals(constraints, other.constraints); | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public String toString() { | ||||
| 		return "Country [constraints=" + constraints + "]"; | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -15,9 +15,12 @@ import org.apache.commons.codec.binary.Base64; | ||||
|  | ||||
| /** | ||||
|  * High Throughput (HT) Operation Element, which is potentially present in | ||||
|  * wifiscan entries. Introduced in 802.11n (2009). | ||||
|  * wifiscan entries. Introduced in 802.11n (2009). Refer to the 802.11 | ||||
|  * specification (section 9.4.2.56)). | ||||
|  */ | ||||
| public class HTOperation { | ||||
| 	/** Defined in 802.11 table 9-92 */ | ||||
| 	public static final int TYPE = 61; | ||||
|  | ||||
| 	/** Channel number of the primary channel. */ | ||||
| 	public final byte primaryChannel; | ||||
|   | ||||
| @@ -15,32 +15,31 @@ import com.google.gson.JsonObject; | ||||
| /** | ||||
|  * This information element (IE) appears in wifiscan entries. It is called | ||||
|  * "Local Power Constraint" in these entries, and just "Power Constraint" in | ||||
|  * the 802.11 specification. Refer to the specification for more details. | ||||
|  * the 802.11 specification (section 9.4.2.13). Refer to the specification for more details. | ||||
|  * Language in javadocs is taken from the specification. | ||||
|  */ | ||||
| public class LocalPowerConstraint { | ||||
|  | ||||
| 	/** Defined in 802.11 */ | ||||
| 	/** Defined in 802.11 table 9-92 */ | ||||
| 	public static final int TYPE = 32; | ||||
|  | ||||
| 	/** | ||||
| 	 * Units are dB. | ||||
| 	 * Unsigned 8 bits - units are dB. | ||||
| 	 * <p> | ||||
| 	 * The local maximum transmit power for a channel is defined as the maximum | ||||
| 	 * transmit power level specified for the channel in the Country IE minus | ||||
| 	 * this variable for the given channel. | ||||
| 	 */ | ||||
| 	public final int localPowerConstraint; | ||||
| 	public final short localPowerConstraint; | ||||
|  | ||||
| 	/** Constructor */ | ||||
| 	public LocalPowerConstraint(int localPowerConstraint) { | ||||
| 	public LocalPowerConstraint(short localPowerConstraint) { | ||||
| 		this.localPowerConstraint = localPowerConstraint; | ||||
| 	} | ||||
|  | ||||
| 	/** Parse LocalPowerConstraint IE from appropriate Json object. */ | ||||
| 	public static LocalPowerConstraint parse(JsonObject contents) { | ||||
| 		final int localPowerConstraint = | ||||
| 			contents.get("Local Power Constraint").getAsInt(); | ||||
| 		final short localPowerConstraint = | ||||
| 			contents.get("Local Power Constraint").getAsShort(); | ||||
| 		return new LocalPowerConstraint(localPowerConstraint); | ||||
| 	} | ||||
|  | ||||
| @@ -63,10 +62,4 @@ public class LocalPowerConstraint { | ||||
| 		LocalPowerConstraint other = (LocalPowerConstraint) obj; | ||||
| 		return localPowerConstraint == other.localPowerConstraint; | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public String toString() { | ||||
| 		return "LocalPowerConstraint [localPowerConstraint=" + | ||||
| 			localPowerConstraint + "]"; | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,318 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| package com.facebook.openwifi.cloudsdk.ies; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
| import java.util.Objects; | ||||
|  | ||||
| import com.google.gson.JsonElement; | ||||
| import com.google.gson.JsonObject; | ||||
|  | ||||
| // NOTE: Not validated (not seen on test devices) | ||||
| /** | ||||
|  * This information element (IE) appears in wifiscan entries. It's called | ||||
|  * "Neighbor Report" in 802.11 specs (section 9.4.2.36). Refer to the | ||||
|  * specification for more details. Language in javadocs is taken from the | ||||
|  * specification. | ||||
|  */ | ||||
| public class NeighborReport { | ||||
| 	/** Defined in 802.11 table 9-92 */ | ||||
| 	public static final int TYPE = 52; | ||||
|  | ||||
| 	/** | ||||
| 	 * The BSSID Information field can be used to help determine neighbor service | ||||
| 	 * set transition candidates | ||||
| 	 */ | ||||
| 	public static class BssidInfo { | ||||
| 		/** | ||||
| 		 * The capability subelement containing selected capability information for | ||||
| 		 * the AP indicated by this BSSID. | ||||
| 		 */ | ||||
| 		public static class Capabilities { | ||||
| 			/** dot11SpectrumManagementRequired */ | ||||
| 			public final boolean spectrumManagement; | ||||
| 			/** dot11QosOptionImplemented */ | ||||
| 			public final boolean qos; | ||||
| 			/** dot11APSDOptionImplemented */ | ||||
| 			public final boolean apsd; | ||||
| 			/** dot11RadioMeasurementActivated */ | ||||
| 			public final boolean radioMeasurement; | ||||
|  | ||||
| 			/** Constructor */ | ||||
| 			public Capabilities( | ||||
| 				boolean spectrumManagement, | ||||
| 				boolean qos, | ||||
| 				boolean apsd, | ||||
| 				boolean radioMeasurement | ||||
| 			) { | ||||
| 				this.spectrumManagement = spectrumManagement; | ||||
| 				this.qos = qos; | ||||
| 				this.apsd = apsd; | ||||
| 				this.radioMeasurement = radioMeasurement; | ||||
| 			} | ||||
|  | ||||
| 			/** Parse Capabilities from JSON object */ | ||||
| 			// TODO modify this method as necessary - since the IE doesn't seem to be | ||||
| 			// present, we have no idea what the format looks like | ||||
| 			public static Capabilities parse(JsonObject contents) { | ||||
| 				return new Capabilities( | ||||
| 					contents.get("Spectrum Management").getAsBoolean(), | ||||
| 					contents.get("QoS").getAsBoolean(), | ||||
| 					contents.get("APSD").getAsBoolean(), | ||||
| 					contents.get("Radio Management").getAsBoolean() | ||||
| 				); | ||||
| 			} | ||||
|  | ||||
| 			@Override | ||||
| 			public int hashCode() { | ||||
| 				return Objects | ||||
| 					.hash(spectrumManagement, qos, apsd, radioMeasurement); | ||||
| 			} | ||||
|  | ||||
| 			@Override | ||||
| 			public boolean equals(Object obj) { | ||||
| 				if (obj == null) { | ||||
| 					return false; | ||||
| 				} | ||||
|  | ||||
| 				if (this == obj) { | ||||
| 					return true; | ||||
| 				} | ||||
|  | ||||
| 				if (getClass() != obj.getClass()) { | ||||
| 					return false; | ||||
| 				} | ||||
|  | ||||
| 				Capabilities other = (Capabilities) obj; | ||||
| 				return spectrumManagement == other.spectrumManagement && | ||||
| 					qos == other.qos && apsd == other.apsd && | ||||
| 					radioMeasurement == other.radioMeasurement; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		/** | ||||
| 		 * 2 unsigned bits - whether the AP identified by this BSSID is reachable by | ||||
| 		 * the STA that requested the neighbor report | ||||
| 		 */ | ||||
| 		public final byte apReachability; | ||||
| 		/** | ||||
| 		 * If true, indicates that the AP identified by this BSSID supports the same | ||||
| 		 * security provisioning as used by the STA in its current association. If the | ||||
| 		 * bit is false, it indicates either that the AP does not support the same | ||||
| 		 * security provisioning or that the security information is not available at | ||||
| 		 * this time. | ||||
| 		 */ | ||||
| 		public final boolean security; | ||||
| 		/** | ||||
| 		 * Indicates the AP indicated by this BSSID has the same authenticator as | ||||
| 		 * the AP sending the report. If this bit is false, it indicates a distinct | ||||
| 		 * authenticator or the information is not available. | ||||
| 		 */ | ||||
| 		public final boolean keyScope; | ||||
| 		/** | ||||
| 		 * @see Capabilities | ||||
| 		 */ | ||||
| 		public final Capabilities capabilities; | ||||
| 		/** | ||||
| 		 * Set to true to indicate that the AP represented by this BSSID is including | ||||
| 		 * an MDE in its Beacon frames and that the contents of that MDE are identical | ||||
| 		 * to the MDE advertised by the AP sending the report | ||||
| 		 */ | ||||
| 		public final boolean mobilityDomain; | ||||
| 		/** | ||||
| 		 * High throughput or not, if true the contents of the HT Capabilities in the | ||||
| 		 * Beacon frame should be identical to the HT Capabilities advertised by the | ||||
| 		 * AP sending the report | ||||
| 		 */ | ||||
| 		public final boolean highThroughput; | ||||
| 		/** | ||||
| 		 * Very High throughput or not, if true the contents of the VHT Capabilities | ||||
| 		 * in the Beacon frame should be identical to the VHT Capabilities advertised | ||||
| 		 * by the AP sending the report | ||||
| 		 */ | ||||
| 		public final boolean veryHighThroughput; | ||||
| 		/** | ||||
| 		 * Indicate that the AP represented by this BSSID is an AP that has set the Fine | ||||
| 		 * Timing Measurement Responder field of the Extended Capabilities element | ||||
| 		 */ | ||||
| 		public final boolean ftm; | ||||
|  | ||||
| 		/** Constructor */ | ||||
| 		public BssidInfo( | ||||
| 			byte apReachability, | ||||
| 			boolean security, | ||||
| 			boolean keyScope, | ||||
| 			Capabilities capabilities, | ||||
| 			boolean mobilityDomain, | ||||
| 			boolean highThroughput, | ||||
| 			boolean veryHighThroughput, | ||||
| 			boolean ftm | ||||
| 		) { | ||||
| 			this.apReachability = apReachability; | ||||
| 			this.security = security; | ||||
| 			this.keyScope = keyScope; | ||||
| 			this.capabilities = capabilities; | ||||
| 			this.mobilityDomain = mobilityDomain; | ||||
| 			this.highThroughput = highThroughput; | ||||
| 			this.veryHighThroughput = veryHighThroughput; | ||||
| 			this.ftm = ftm; | ||||
| 		} | ||||
|  | ||||
| 		/** Parse BssidInfo from JSON object */ | ||||
| 		// TODO rename fields as necessary - we don't know how the data format yet | ||||
| 		public static BssidInfo parse(JsonObject contents) { | ||||
| 			JsonElement capabilitiesJson = contents.get("capabilities"); | ||||
| 			if (capabilitiesJson == null) { | ||||
| 				return null; | ||||
| 			} | ||||
|  | ||||
| 			Capabilities capabilities = | ||||
| 				Capabilities.parse(capabilitiesJson.getAsJsonObject()); | ||||
| 			return new BssidInfo( | ||||
| 				contents.get("AP Reachability").getAsByte(), | ||||
| 				contents.get("Security").getAsBoolean(), | ||||
| 				contents.get("Key Scope").getAsBoolean(), | ||||
| 				capabilities, | ||||
| 				contents.get("Mobility Domain").getAsBoolean(), | ||||
| 				contents.get("High Throughput").getAsBoolean(), | ||||
| 				contents.get("Very High Throughput").getAsBoolean(), | ||||
| 				contents.get("FTM").getAsBoolean() | ||||
| 			); | ||||
| 		} | ||||
|  | ||||
| 		@Override | ||||
| 		public int hashCode() { | ||||
| 			return Objects.hash( | ||||
| 				apReachability, | ||||
| 				security, | ||||
| 				keyScope, | ||||
| 				mobilityDomain, | ||||
| 				highThroughput, | ||||
| 				veryHighThroughput, | ||||
| 				ftm | ||||
| 			); | ||||
| 		} | ||||
|  | ||||
| 		@Override | ||||
| 		public boolean equals(Object obj) { | ||||
| 			if (obj == null) { | ||||
| 				return false; | ||||
| 			} | ||||
|  | ||||
| 			if (this == obj) { | ||||
| 				return true; | ||||
| 			} | ||||
|  | ||||
| 			if (getClass() != obj.getClass()) { | ||||
| 				return false; | ||||
| 			} | ||||
|  | ||||
| 			BssidInfo other = (BssidInfo) obj; | ||||
| 			return apReachability == other.apReachability && | ||||
| 				security == other.security && keyScope == other.keyScope && | ||||
| 				capabilities == other.capabilities && | ||||
| 				mobilityDomain == other.mobilityDomain && | ||||
| 				highThroughput == other.highThroughput && | ||||
| 				veryHighThroughput == other.veryHighThroughput && | ||||
| 				ftm == other.ftm; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/** BSSID */ | ||||
| 	public final String bssid; | ||||
| 	/** | ||||
| 	 * @see BssidInfo | ||||
| 	 */ | ||||
| 	public final BssidInfo bssidInfo; | ||||
| 	/** | ||||
| 	 * Unsigned 8 bits - indicates the channel set of the AP indicated by this BSSID | ||||
| 	 */ | ||||
| 	public final short operatingClass; | ||||
| 	/** | ||||
| 	 * Unsigned 8 bits - channel number | ||||
| 	 */ | ||||
| 	public final short channelNumber; | ||||
| 	/** | ||||
| 	 * Unsigned 8 bits - PHY type | ||||
| 	 */ | ||||
| 	public final short phyType; | ||||
| 	// TODO do we want to support the subelements? | ||||
| 	/** | ||||
| 	 * Optional subelements | ||||
| 	 */ | ||||
| 	public final List<JsonObject> subelements; | ||||
|  | ||||
| 	/** Constructor */ | ||||
| 	public NeighborReport( | ||||
| 		String bssid, | ||||
| 		BssidInfo bssidInfo, | ||||
| 		short operatingClass, | ||||
| 		short channelNumber, | ||||
| 		short phyType, | ||||
| 		List<JsonObject> subelements | ||||
| 	) { | ||||
| 		this.bssid = bssid; | ||||
| 		this.bssidInfo = bssidInfo; | ||||
| 		this.operatingClass = operatingClass; | ||||
| 		this.channelNumber = channelNumber; | ||||
| 		this.phyType = phyType; | ||||
| 		this.subelements = Collections.unmodifiableList(subelements); | ||||
| 	} | ||||
|  | ||||
| 	/** Parse NeighborReport from JSON object */ | ||||
| 	// TODO modify this method as necessary - since the IE doesn't seem to be | ||||
| 	// present, we have no idea what the format looks like | ||||
| 	public static NeighborReport parse(JsonObject contents) { | ||||
| 		List<JsonObject> subelements = null; | ||||
| 		JsonElement subelementsObj = contents.get("Subelements"); | ||||
| 		if (subelementsObj != null) { | ||||
| 			subelements = new ArrayList<JsonObject>(); | ||||
| 			for (JsonElement elem : subelementsObj.getAsJsonArray()) { | ||||
| 				subelements.add(elem.getAsJsonObject()); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		return new NeighborReport( | ||||
| 			contents.get("BSSID").getAsString(), | ||||
| 			BssidInfo.parse(contents.get("BSSID Info").getAsJsonObject()), | ||||
| 			contents.get("Operating Class").getAsShort(), | ||||
| 			contents.get("Channel Number").getAsShort(), | ||||
| 			contents.get("Phy Type").getAsShort(), | ||||
| 			subelements | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public int hashCode() { | ||||
| 		return Objects | ||||
| 			.hash(bssid, bssidInfo, operatingClass, channelNumber, phyType); | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public boolean equals(Object obj) { | ||||
| 		if (obj == null) { | ||||
| 			return false; | ||||
| 		} | ||||
|  | ||||
| 		if (this == obj) { | ||||
| 			return true; | ||||
| 		} | ||||
|  | ||||
| 		if (getClass() != obj.getClass()) { | ||||
| 			return false; | ||||
| 		} | ||||
|  | ||||
| 		NeighborReport other = (NeighborReport) obj; | ||||
| 		return bssid == other.bssid && bssidInfo == other.bssidInfo && | ||||
| 			operatingClass == other.operatingClass && | ||||
| 			channelNumber == other.channelNumber && phyType == other.phyType; | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,80 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| package com.facebook.openwifi.cloudsdk.ies; | ||||
|  | ||||
| import java.util.Objects; | ||||
|  | ||||
| import com.google.gson.JsonObject; | ||||
|  | ||||
| // NOTE: Not validated (not seen on test devices) | ||||
| /** | ||||
|  * This information element (IE) appears in wifiscan entries. It's called "Power | ||||
|  * Capability" in 802.11 specs (section 9.4.2.14). Refer to the specification | ||||
|  * for more details. Language in javadocs is taken from the specification. | ||||
|  */ | ||||
| public class PowerCapability { | ||||
| 	/** Defined in 802.11 table 9-92 */ | ||||
| 	public static final int TYPE = 33; | ||||
|  | ||||
| 	/** | ||||
| 	 * Signed 8 bits units of dB relative to 1mW - nominal minimum transmit power | ||||
| 	 * with which the STA is capable of transmitting in the current channel, with a | ||||
| 	 * tolerance ± 5 dB. | ||||
| 	 */ | ||||
| 	public final byte minimumTxPowerCapability; | ||||
| 	/** | ||||
| 	 * Signed 8 bits units of dB relative to 1mW - nominal maximum transmit power | ||||
| 	 * with which the STA is capable of transmitting in the current channel, with a | ||||
| 	 * tolerance ± 5 dB. | ||||
| 	 */ | ||||
| 	public final byte maximumTxPowerCapability; | ||||
|  | ||||
| 	/** Constructor */ | ||||
| 	public PowerCapability( | ||||
| 		byte minimumTxPowerCapability, | ||||
| 		byte maximumTxPowerCapability | ||||
| 	) { | ||||
| 		this.minimumTxPowerCapability = minimumTxPowerCapability; | ||||
| 		this.maximumTxPowerCapability = maximumTxPowerCapability; | ||||
| 	} | ||||
|  | ||||
| 	/** Parse PowerCapability from JSON object */ | ||||
| 	// TODO modify this method as necessary - since the IE doesn't seem to be | ||||
| 	// present, we have no idea what the format looks like | ||||
| 	public static PowerCapability parse(JsonObject contents) { | ||||
| 		return new PowerCapability( | ||||
| 			contents.get("Minimum Tx Power Capability").getAsByte(), | ||||
| 			contents.get("Maximum Tx Power Capability").getAsByte() | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public int hashCode() { | ||||
| 		return Objects.hash(minimumTxPowerCapability, maximumTxPowerCapability); | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public boolean equals(Object obj) { | ||||
| 		if (obj == null) { | ||||
| 			return false; | ||||
| 		} | ||||
|  | ||||
| 		if (this == obj) { | ||||
| 			return true; | ||||
| 		} | ||||
|  | ||||
| 		if (getClass() != obj.getClass()) { | ||||
| 			return false; | ||||
| 		} | ||||
|  | ||||
| 		PowerCapability other = (PowerCapability) obj; | ||||
| 		return minimumTxPowerCapability == other.minimumTxPowerCapability && | ||||
| 			maximumTxPowerCapability == other.maximumTxPowerCapability; | ||||
| 	} | ||||
| } | ||||
| @@ -15,22 +15,21 @@ import com.google.gson.JsonObject; | ||||
|  | ||||
| /** | ||||
|  * This information element (IE) appears in wifiscan entries. It is called | ||||
|  * "QBSS Load" in these entries, and just "BSS Load" in the 802.11 | ||||
|  * specification. Refer to the specification for more details. Language in | ||||
|  * "QBSS Load" in these entries, and just "BSS Load" in the 802.11 specification | ||||
|  * (section 9.4.2.27). Refer to the specification for more details. Language in | ||||
|  * javadocs is taken from the specification. | ||||
|  */ | ||||
| public class QbssLoad { | ||||
|  | ||||
| 	/** Defined in 802.11 */ | ||||
| 	/** Defined in 802.11 table 9-92 */ | ||||
| 	public static final int TYPE = 11; | ||||
|  | ||||
| 	/** | ||||
| 	 * The total number of STAs currently associated with the BSS. | ||||
| 	 * Unsigned 16 bits - The total number of STAs currently associated with the BSS. | ||||
| 	 */ | ||||
| 	public final int stationCount; | ||||
| 	public final short stationCount; | ||||
| 	/** | ||||
| 	 * The Channel Utilization field is defined as the percentage of time, | ||||
| 	 * linearly scaled with 255 representing 100%, that the AP sensed the | ||||
| 	 * Unsigned 8 bits - The Channel Utilization field is defined as the percentage | ||||
| 	 * of time, linearly scaled with 255 representing 100%, that the AP sensed the | ||||
| 	 * medium was busy, as indicated by either the physical or virtual carrier | ||||
| 	 * sense (CS) mechanism. When more than one channel is in use for the BSS, | ||||
| 	 * the Channel Utilization field value is calculated only for the primary | ||||
| @@ -40,22 +39,22 @@ public class QbssLoad { | ||||
| 	 * 		(dot11ChannelUtilizationBeaconIntervals * dot11BeaconPeriod * 1024) | ||||
| 	 * ) | ||||
| 	 */ | ||||
| 	public final int channelUtilization; | ||||
| 	public final short channelUtilization; | ||||
| 	/** | ||||
| 	 * The Available Admission Capacity field contains an unsigned integer that | ||||
| 	 * specifies the remaining amount of medium time available via explicit | ||||
| 	 * admission control, in units of 32 miscrosecond/second. The field is | ||||
| 	 * helpful for roaming STAs to select an AP that is likely to accept future | ||||
| 	 * admission control requests, but it does not represent an assurance that | ||||
| 	 * the HC admits these requests. | ||||
| 	 * Unsigned 16 bits - The Available Admission Capacity field contains an | ||||
| 	 * unsigned integer that specifies the remaining amount of medium time | ||||
| 	 * available via explicit admission control, in units of 32 | ||||
| 	 * miscrosecond/second. The field is helpful for roaming STAs to select an AP | ||||
| 	 * that is likely to accept future admission control requests, but it does not | ||||
| 	 * represent an assurance that the HC admits these requests. | ||||
| 	 */ | ||||
| 	public final int availableAdmissionCapacity; | ||||
| 	public final short availableAdmissionCapacity; | ||||
|  | ||||
| 	/** Constructor */ | ||||
| 	public QbssLoad( | ||||
| 		int stationCount, | ||||
| 		int channelUtilization, | ||||
| 		int availableAdmissionCapacity | ||||
| 		short stationCount, | ||||
| 		short channelUtilization, | ||||
| 		short availableAdmissionCapacity | ||||
| 	) { | ||||
| 		this.stationCount = stationCount; | ||||
| 		this.channelUtilization = channelUtilization; | ||||
| @@ -70,11 +69,11 @@ public class QbssLoad { | ||||
| 			return null; | ||||
| 		} | ||||
| 		contents = ccaContentJsonElement.getAsJsonObject(); | ||||
| 		final int stationCount = contents.get("Station Count").getAsInt(); | ||||
| 		final int channelUtilization = | ||||
| 			contents.get("Channel Utilization").getAsInt(); | ||||
| 		final int availableAdmissionCapacity = | ||||
| 			contents.get("Available Admission Capabilities").getAsInt(); | ||||
| 		final short stationCount = contents.get("Station Count").getAsShort(); | ||||
| 		final short channelUtilization = | ||||
| 			contents.get("Channel Utilization").getAsShort(); | ||||
| 		final short availableAdmissionCapacity = | ||||
| 			contents.get("Available Admission Capabilities").getAsShort(); | ||||
| 		return new QbssLoad( | ||||
| 			stationCount, | ||||
| 			channelUtilization, | ||||
| @@ -107,11 +106,4 @@ public class QbssLoad { | ||||
| 			channelUtilization == other.channelUtilization && | ||||
| 			stationCount == other.stationCount; | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public String toString() { | ||||
| 		return "QbssLoad [stationCount=" + stationCount + | ||||
| 			", channelUtilization=" + channelUtilization + | ||||
| 			", availableAdmissionCapacity=" + availableAdmissionCapacity + "]"; | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,67 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| package com.facebook.openwifi.cloudsdk.ies; | ||||
|  | ||||
| import java.util.Objects; | ||||
|  | ||||
| import com.google.gson.JsonObject; | ||||
|  | ||||
| // NOTE: Not validated (not seen on test devices) | ||||
| /** | ||||
|  * This information element (IE) appears in wifiscan entries. It's called "RCPI" | ||||
|  * in 802.11 specs (section 9.4.2.37). Refer to the specification for more | ||||
|  * details. Language in javadocs is taken from the specification. | ||||
|  */ | ||||
| public class RCPI { | ||||
| 	/** Defined in 802.11 table 9-92 */ | ||||
| 	public static final int TYPE = 53; | ||||
|  | ||||
| 	/** | ||||
| 	 * Unsigned 8 bits - indication of the received RF power in the selected | ||||
| 	 * channel for a received frame | ||||
| 	 */ | ||||
| 	public final short rcpi; | ||||
|  | ||||
| 	/** Constructor */ | ||||
| 	public RCPI(short rcpi) { | ||||
| 		this.rcpi = rcpi; | ||||
| 	} | ||||
|  | ||||
| 	/** Parse RCPI from JSON object */ | ||||
| 	// TODO modify this method as necessary - since the IE doesn't seem to be | ||||
| 	// present, we have no idea what the format looks like | ||||
| 	public static RCPI parse(JsonObject contents) { | ||||
| 		return new RCPI( | ||||
| 			contents.get("RCPI").getAsShort() | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public int hashCode() { | ||||
| 		return Objects.hash(rcpi); | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public boolean equals(Object obj) { | ||||
| 		if (obj == null) { | ||||
| 			return false; | ||||
| 		} | ||||
|  | ||||
| 		if (this == obj) { | ||||
| 			return true; | ||||
| 		} | ||||
|  | ||||
| 		if (getClass() != obj.getClass()) { | ||||
| 			return false; | ||||
| 		} | ||||
|  | ||||
| 		RCPI other = (RCPI) obj; | ||||
| 		return rcpi == other.rcpi; | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,274 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| package com.facebook.openwifi.cloudsdk.ies; | ||||
|  | ||||
| import java.util.Objects; | ||||
|  | ||||
| import com.facebook.openwifi.cloudsdk.IEUtils; | ||||
| import com.google.gson.JsonObject; | ||||
|  | ||||
| /** | ||||
|  * This information element (IE) appears in wifiscan entries. It's called "RM | ||||
|  * Enabled Capabilities" in 802.11 specs (section 9.4.2.45). Refer to the | ||||
|  * specification for more details. Language in javadocs is taken from the | ||||
|  * specification. | ||||
|  */ | ||||
| public class RMEnabledCapabilities { | ||||
| 	/** Defined in 802.11 table 9-92 */ | ||||
| 	public static final int TYPE = 70; | ||||
|  | ||||
| 	// Bit fields | ||||
| 	// @formatter:off | ||||
| 	public final boolean linkMeasurementCapabilityEnabled; | ||||
| 	public final boolean neighborReportCapabilityEnabled; | ||||
| 	public final boolean parallelMeasurementsCapabilityEnabled; | ||||
| 	public final boolean repeatedMeasurementsCapabilityEnabled; | ||||
| 	public final boolean beaconPassiveMeasurementCapabilityEnabled; | ||||
| 	public final boolean beaconActiveMeasurementCapabilityEnabled; | ||||
| 	public final boolean beaconTableMeasurementCapabilityEnabled; | ||||
| 	public final boolean beaconMeasurementReportingConditionsCapabilityEnabled; | ||||
| 	public final boolean frameMeasurementCapabilityEnabled; | ||||
| 	public final boolean channelLoadMeasurementCapabilityEnabled; | ||||
| 	public final boolean noiseHistogramMeasurementCapabilityEnabled; | ||||
| 	public final boolean statisticsMeasurementCapabilityEnabled; | ||||
| 	public final boolean lciMeasurementCapabilityEnabled; | ||||
| 	public final boolean lciAzimuthCapabilityEnabled; | ||||
| 	public final boolean transmitStreamCategoryMeasurementCapabilityEnabled; | ||||
| 	public final boolean triggeredTransmitStreamCategoryMeasurementCapabilityEnabled; | ||||
| 	public final boolean apChannelReportCapabilityEnabled; | ||||
| 	public final boolean rmMibCapabilityEnabled; | ||||
| 	public final int operatingChannelMaxMeasurementDuration; | ||||
| 	public final int nonoperatingChannelMaxMeasurementDuration; | ||||
| 	public final int measurementPilotCapability; | ||||
| 	public final boolean measurementPilotTransmissionInformationCapabilityEnabled; | ||||
| 	public final boolean neighborReportTsfOffsetCapabilityEnabled; | ||||
| 	public final boolean rcpiMeasurementCapabilityEnabled; | ||||
| 	public final boolean rsniMeasurementCapabilityEnabled; | ||||
| 	public final boolean bssAverageAccessDelayCapabilityEnabled; | ||||
| 	public final boolean bssAvailableAdmissionCapacityCapabilityEnabled; | ||||
| 	public final boolean antennaCapabilityEnabled; | ||||
| 	public final boolean ftmRangeReportCapabilityEnabled; | ||||
| 	public final boolean civicLocationMeasurementCapabilityEnabled; | ||||
| 	// @formatter:on | ||||
|  | ||||
| 	/** Constructor */ | ||||
| 	public RMEnabledCapabilities( | ||||
| 		boolean linkMeasurementCapabilityEnabled, | ||||
| 		boolean neighborReportCapabilityEnabled, | ||||
| 		boolean parallelMeasurementsCapabilityEnabled, | ||||
| 		boolean repeatedMeasurementsCapabilityEnabled, | ||||
| 		boolean beaconPassiveMeasurementCapabilityEnabled, | ||||
| 		boolean beaconActiveMeasurementCapabilityEnabled, | ||||
| 		boolean beaconTableMeasurementCapabilityEnabled, | ||||
| 		boolean beaconMeasurementReportingConditionsCapabilityEnabled, | ||||
| 		boolean frameMeasurementCapabilityEnabled, | ||||
| 		boolean channelLoadMeasurementCapabilityEnabled, | ||||
| 		boolean noiseHistogramMeasurementCapabilityEnabled, | ||||
| 		boolean statisticsMeasurementCapabilityEnabled, | ||||
| 		boolean lciMeasurementCapabilityEnabled, | ||||
| 		boolean lciAzimuthCapabilityEnabled, | ||||
| 		boolean transmitStreamCategoryMeasurementCapabilityEnabled, | ||||
| 		boolean triggeredTransmitStreamCategoryMeasurementCapabilityEnabled, | ||||
| 		boolean apChannelReportCapabilityEnabled, | ||||
| 		boolean rmMibCapabilityEnabled, | ||||
| 		int operatingChannelMaxMeasurementDuration, | ||||
| 		int nonoperatingChannelMaxMeasurementDuration, | ||||
| 		int measurementPilotCapability, | ||||
| 		boolean measurementPilotTransmissionInformationCapabilityEnabled, | ||||
| 		boolean neighborReportTsfOffsetCapabilityEnabled, | ||||
| 		boolean rcpiMeasurementCapabilityEnabled, | ||||
| 		boolean rsniMeasurementCapabilityEnabled, | ||||
| 		boolean bssAverageAccessDelayCapabilityEnabled, | ||||
| 		boolean bssAvailableAdmissionCapacityCapabilityEnabled, | ||||
| 		boolean antennaCapabilityEnabled, | ||||
| 		boolean ftmRangeReportCapabilityEnabled, | ||||
| 		boolean civicLocationMeasurementCapabilityEnabled | ||||
| 	) { | ||||
| 		// @formatter:off | ||||
| 		this.linkMeasurementCapabilityEnabled = linkMeasurementCapabilityEnabled; | ||||
| 		this.neighborReportCapabilityEnabled = neighborReportCapabilityEnabled; | ||||
| 		this.parallelMeasurementsCapabilityEnabled = parallelMeasurementsCapabilityEnabled; | ||||
| 		this.repeatedMeasurementsCapabilityEnabled = repeatedMeasurementsCapabilityEnabled; | ||||
| 		this.beaconPassiveMeasurementCapabilityEnabled = beaconPassiveMeasurementCapabilityEnabled; | ||||
| 		this.beaconActiveMeasurementCapabilityEnabled = beaconActiveMeasurementCapabilityEnabled; | ||||
| 		this.beaconTableMeasurementCapabilityEnabled = beaconTableMeasurementCapabilityEnabled; | ||||
| 		this.beaconMeasurementReportingConditionsCapabilityEnabled = beaconMeasurementReportingConditionsCapabilityEnabled; | ||||
| 		this.frameMeasurementCapabilityEnabled = frameMeasurementCapabilityEnabled; | ||||
| 		this.channelLoadMeasurementCapabilityEnabled = channelLoadMeasurementCapabilityEnabled; | ||||
| 		this.noiseHistogramMeasurementCapabilityEnabled = noiseHistogramMeasurementCapabilityEnabled; | ||||
| 		this.statisticsMeasurementCapabilityEnabled = statisticsMeasurementCapabilityEnabled; | ||||
| 		this.lciMeasurementCapabilityEnabled = lciMeasurementCapabilityEnabled; | ||||
| 		this.lciAzimuthCapabilityEnabled = lciAzimuthCapabilityEnabled; | ||||
| 		this.transmitStreamCategoryMeasurementCapabilityEnabled = transmitStreamCategoryMeasurementCapabilityEnabled; | ||||
| 		this.triggeredTransmitStreamCategoryMeasurementCapabilityEnabled = triggeredTransmitStreamCategoryMeasurementCapabilityEnabled; | ||||
| 		this.apChannelReportCapabilityEnabled = apChannelReportCapabilityEnabled; | ||||
| 		this.rmMibCapabilityEnabled = rmMibCapabilityEnabled; | ||||
| 		this.operatingChannelMaxMeasurementDuration = operatingChannelMaxMeasurementDuration; | ||||
| 		this.nonoperatingChannelMaxMeasurementDuration = nonoperatingChannelMaxMeasurementDuration; | ||||
| 		this.measurementPilotCapability = measurementPilotCapability; | ||||
| 		this.measurementPilotTransmissionInformationCapabilityEnabled = measurementPilotTransmissionInformationCapabilityEnabled; | ||||
| 		this.neighborReportTsfOffsetCapabilityEnabled = neighborReportTsfOffsetCapabilityEnabled; | ||||
| 		this.rcpiMeasurementCapabilityEnabled = rcpiMeasurementCapabilityEnabled; | ||||
| 		this.rsniMeasurementCapabilityEnabled = rsniMeasurementCapabilityEnabled; | ||||
| 		this.bssAverageAccessDelayCapabilityEnabled = bssAverageAccessDelayCapabilityEnabled; | ||||
| 		this.bssAvailableAdmissionCapacityCapabilityEnabled = bssAvailableAdmissionCapacityCapabilityEnabled; | ||||
| 		this.antennaCapabilityEnabled = antennaCapabilityEnabled; | ||||
| 		this.ftmRangeReportCapabilityEnabled = ftmRangeReportCapabilityEnabled; | ||||
| 		this.civicLocationMeasurementCapabilityEnabled = civicLocationMeasurementCapabilityEnabled; | ||||
| 		// @formatter:on | ||||
| 	} | ||||
|  | ||||
| 	/** Parse RMEnabledCapabilities IE from appropriate Json object. */ | ||||
| 	public static RMEnabledCapabilities parse(JsonObject contents) { | ||||
| 		JsonObject o = contents.get("RM Capabilities").getAsJsonObject(); | ||||
| 		// @formatter:off | ||||
| 		return new RMEnabledCapabilities( | ||||
| 			/* bits 0-17 */ | ||||
| 			IEUtils.parseBooleanNumberField(o, "Link Measurement"), | ||||
| 			IEUtils.parseBooleanNumberField(o, "Neighbor Report"), | ||||
| 			IEUtils.parseBooleanNumberField(o, "Parallel Measurements"), | ||||
| 			IEUtils.parseBooleanNumberField(o, "Repeated Measurements"), | ||||
| 			IEUtils.parseBooleanNumberField(o, "Beacon Passive Measurement"), | ||||
| 			IEUtils.parseBooleanNumberField(o, "Beacon Active Measurement"), | ||||
| 			IEUtils.parseBooleanNumberField(o, "Beacon Table Measurement"), | ||||
| 			IEUtils.parseBooleanNumberField(o, "Beacon Measurement Reporting Conditions"), | ||||
| 			IEUtils.parseBooleanNumberField(o, "Frame Measurement"), | ||||
| 			IEUtils.parseBooleanNumberField(o, "Channel Load Measurement"), | ||||
| 			IEUtils.parseBooleanNumberField(o, "Noise Histogram Measurement"), | ||||
| 			IEUtils.parseBooleanNumberField(o, "Statistics Measurement"), | ||||
| 			IEUtils.parseBooleanNumberField(o, "LCI Measurement"), | ||||
| 			IEUtils.parseBooleanNumberField(o, "LCI Azimuth capability"), | ||||
| 			IEUtils.parseBooleanNumberField(o, "Transmit Stream/Category Measurement"), | ||||
| 			IEUtils.parseBooleanNumberField(o, "Triggered Transmit Stream/Category Measurement"), | ||||
| 			IEUtils.parseBooleanNumberField(o, "AP Channel Report capability"), | ||||
| 			IEUtils.parseBooleanNumberField(o, "RM MIB capability"), | ||||
| 			/* bits 18-20 */ | ||||
| 			IEUtils.parseIntField(o, "Operating Channel Max Measurement Duration"), | ||||
| 			/* bits 21-23 */ | ||||
| 			IEUtils.parseIntField(o, "Nonoperating Channel Max Measurement Duration"), | ||||
| 			/* bits 24-26 */ | ||||
| 			IEUtils.parseIntField(o, "Measurement Pilotcapability"), | ||||
| 			/* bits 27-35 */ | ||||
| 			false /* TODO "Measurement Pilot Transmission Information Capability" */, | ||||
| 			IEUtils.parseBooleanNumberField(o, "Neighbor Report TSF Offset"), | ||||
| 			IEUtils.parseBooleanNumberField(o, "RCPI Measurement capability"), | ||||
| 			IEUtils.parseBooleanNumberField(o, "RSNI Measurement capability"), | ||||
| 			IEUtils.parseBooleanNumberField(o, "BSS Average Access Delay capability"), | ||||
| 			IEUtils.parseBooleanNumberField(o, "BSS Available Admission Capacity capability"), | ||||
| 			IEUtils.parseBooleanNumberField(o, "Antenna capability"), | ||||
| 			false /* TODO "FTM Range Report Capability" */, | ||||
| 			false /* TODO "Civic Location Measurement Capability" */ | ||||
| 			/* bits 36-39 reserved */ | ||||
| 		); | ||||
| 		// @formatter:on | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public int hashCode() { | ||||
| 		return Objects.hash( | ||||
| 			antennaCapabilityEnabled, | ||||
| 			apChannelReportCapabilityEnabled, | ||||
| 			beaconActiveMeasurementCapabilityEnabled, | ||||
| 			beaconMeasurementReportingConditionsCapabilityEnabled, | ||||
| 			beaconPassiveMeasurementCapabilityEnabled, | ||||
| 			beaconTableMeasurementCapabilityEnabled, | ||||
| 			bssAvailableAdmissionCapacityCapabilityEnabled, | ||||
| 			bssAverageAccessDelayCapabilityEnabled, | ||||
| 			channelLoadMeasurementCapabilityEnabled, | ||||
| 			civicLocationMeasurementCapabilityEnabled, | ||||
| 			frameMeasurementCapabilityEnabled, | ||||
| 			ftmRangeReportCapabilityEnabled, | ||||
| 			lciAzimuthCapabilityEnabled, | ||||
| 			lciMeasurementCapabilityEnabled, | ||||
| 			linkMeasurementCapabilityEnabled, | ||||
| 			measurementPilotCapability, | ||||
| 			measurementPilotTransmissionInformationCapabilityEnabled, | ||||
| 			neighborReportCapabilityEnabled, | ||||
| 			neighborReportTsfOffsetCapabilityEnabled, | ||||
| 			noiseHistogramMeasurementCapabilityEnabled, | ||||
| 			nonoperatingChannelMaxMeasurementDuration, | ||||
| 			operatingChannelMaxMeasurementDuration, | ||||
| 			parallelMeasurementsCapabilityEnabled, | ||||
| 			rcpiMeasurementCapabilityEnabled, | ||||
| 			repeatedMeasurementsCapabilityEnabled, | ||||
| 			rmMibCapabilityEnabled, | ||||
| 			rsniMeasurementCapabilityEnabled, | ||||
| 			statisticsMeasurementCapabilityEnabled, | ||||
| 			transmitStreamCategoryMeasurementCapabilityEnabled, | ||||
| 			triggeredTransmitStreamCategoryMeasurementCapabilityEnabled | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public boolean equals(Object obj) { | ||||
| 		if (this == obj) | ||||
| 			return true; | ||||
| 		if (obj == null) | ||||
| 			return false; | ||||
| 		if (getClass() != obj.getClass()) | ||||
| 			return false; | ||||
| 		RMEnabledCapabilities other = (RMEnabledCapabilities) obj; | ||||
| 		return antennaCapabilityEnabled == other.antennaCapabilityEnabled && | ||||
| 			apChannelReportCapabilityEnabled == | ||||
| 				other.apChannelReportCapabilityEnabled && | ||||
| 			beaconActiveMeasurementCapabilityEnabled == | ||||
| 				other.beaconActiveMeasurementCapabilityEnabled && | ||||
| 			beaconMeasurementReportingConditionsCapabilityEnabled == | ||||
| 				other.beaconMeasurementReportingConditionsCapabilityEnabled && | ||||
| 			beaconPassiveMeasurementCapabilityEnabled == | ||||
| 				other.beaconPassiveMeasurementCapabilityEnabled && | ||||
| 			beaconTableMeasurementCapabilityEnabled == | ||||
| 				other.beaconTableMeasurementCapabilityEnabled && | ||||
| 			bssAvailableAdmissionCapacityCapabilityEnabled == | ||||
| 				other.bssAvailableAdmissionCapacityCapabilityEnabled && | ||||
| 			bssAverageAccessDelayCapabilityEnabled == | ||||
| 				other.bssAverageAccessDelayCapabilityEnabled && | ||||
| 			channelLoadMeasurementCapabilityEnabled == | ||||
| 				other.channelLoadMeasurementCapabilityEnabled && | ||||
| 			civicLocationMeasurementCapabilityEnabled == | ||||
| 				other.civicLocationMeasurementCapabilityEnabled && | ||||
| 			frameMeasurementCapabilityEnabled == | ||||
| 				other.frameMeasurementCapabilityEnabled && | ||||
| 			ftmRangeReportCapabilityEnabled == | ||||
| 				other.ftmRangeReportCapabilityEnabled && | ||||
| 			lciAzimuthCapabilityEnabled == other.lciAzimuthCapabilityEnabled && | ||||
| 			lciMeasurementCapabilityEnabled == | ||||
| 				other.lciMeasurementCapabilityEnabled && | ||||
| 			linkMeasurementCapabilityEnabled == | ||||
| 				other.linkMeasurementCapabilityEnabled && | ||||
| 			measurementPilotCapability == other.measurementPilotCapability && | ||||
| 			measurementPilotTransmissionInformationCapabilityEnabled == | ||||
| 				other.measurementPilotTransmissionInformationCapabilityEnabled && | ||||
| 			neighborReportCapabilityEnabled == | ||||
| 				other.neighborReportCapabilityEnabled && | ||||
| 			neighborReportTsfOffsetCapabilityEnabled == | ||||
| 				other.neighborReportTsfOffsetCapabilityEnabled && | ||||
| 			noiseHistogramMeasurementCapabilityEnabled == | ||||
| 				other.noiseHistogramMeasurementCapabilityEnabled && | ||||
| 			nonoperatingChannelMaxMeasurementDuration == | ||||
| 				other.nonoperatingChannelMaxMeasurementDuration && | ||||
| 			operatingChannelMaxMeasurementDuration == | ||||
| 				other.operatingChannelMaxMeasurementDuration && | ||||
| 			parallelMeasurementsCapabilityEnabled == | ||||
| 				other.parallelMeasurementsCapabilityEnabled && | ||||
| 			rcpiMeasurementCapabilityEnabled == | ||||
| 				other.rcpiMeasurementCapabilityEnabled && | ||||
| 			repeatedMeasurementsCapabilityEnabled == | ||||
| 				other.repeatedMeasurementsCapabilityEnabled && | ||||
| 			rmMibCapabilityEnabled == other.rmMibCapabilityEnabled && | ||||
| 			rsniMeasurementCapabilityEnabled == | ||||
| 				other.rsniMeasurementCapabilityEnabled && | ||||
| 			statisticsMeasurementCapabilityEnabled == | ||||
| 				other.statisticsMeasurementCapabilityEnabled && | ||||
| 			transmitStreamCategoryMeasurementCapabilityEnabled == | ||||
| 				other.transmitStreamCategoryMeasurementCapabilityEnabled && | ||||
| 			triggeredTransmitStreamCategoryMeasurementCapabilityEnabled == | ||||
| 				other.triggeredTransmitStreamCategoryMeasurementCapabilityEnabled; | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,313 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| package com.facebook.openwifi.cloudsdk.ies; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
| import java.util.Objects; | ||||
|  | ||||
| import com.facebook.openwifi.cloudsdk.IEUtils; | ||||
| import com.google.gson.JsonElement; | ||||
| import com.google.gson.JsonObject; | ||||
|  | ||||
| // NOTE: Not validated (not seen on test devices) | ||||
| /** | ||||
|  * This information element (IE) appears in wifiscan entries. It's called | ||||
|  * "Reduced Neighbor Report" in 802.11 specs (section 9.4.2.170). Refer to the | ||||
|  * specification for more details. Language in javadocs is taken from the | ||||
|  * specification. | ||||
|  */ | ||||
| public class ReducedNeighborReport { | ||||
| 	/** Defined in 802.11 table 9-92 */ | ||||
| 	public static final int TYPE = 201; | ||||
|  | ||||
| 	/** | ||||
| 	 * The Neighbor AP Information field specifies TBTT and other information | ||||
| 	 * related to a group of neighbor APs on one channel. | ||||
| 	 */ | ||||
| 	public static class NeighborApInformation { | ||||
| 		/** | ||||
| 		 * Subfield for TBTT Information header | ||||
| 		 */ | ||||
| 		public static class TbttInformationHeader { | ||||
| 			/** | ||||
| 			 * Unsigned 2 bits -  identifies, together with the TBTT Information Length | ||||
| 			 * subfield, the format of the TBTT Information field | ||||
| 			 */ | ||||
| 			public final byte tbttInformationType; | ||||
| 			/** | ||||
| 			 * 1 bit - reserved except when the Reduced Neighbor Report element is | ||||
| 			 * carried in a Probe Response frame transmitted by a TVHT AP | ||||
| 			 */ | ||||
| 			public final boolean filteredNeighborAp; | ||||
| 			/** | ||||
| 			 * Unsigned 4 bits - number of TBTT Information fields included in the TBTT | ||||
| 			 * Information Set field of the Neighbor AP Information field, minus one | ||||
| 			 */ | ||||
| 			public final byte tbttInformationCount; | ||||
| 			/** | ||||
| 			 * Unsigned 8 bits - the length of each TBTT Information field included in | ||||
| 			 * the TBTT Information Set field of the Neighbor AP Information field | ||||
| 			 */ | ||||
| 			public final short tbttInformationLength; | ||||
|  | ||||
| 			/** Constructor */ | ||||
| 			public TbttInformationHeader( | ||||
| 				byte tbttInformationType, | ||||
| 				boolean filteredNeighborAp, | ||||
| 				byte tbttInformationCount, | ||||
| 				short tbttInformationLength | ||||
| 			) { | ||||
| 				this.tbttInformationType = tbttInformationType; | ||||
| 				this.filteredNeighborAp = filteredNeighborAp; | ||||
| 				this.tbttInformationCount = tbttInformationCount; | ||||
| 				this.tbttInformationLength = tbttInformationLength; | ||||
| 			} | ||||
|  | ||||
| 			/** Parse TbttInformationHeader from JSON object */ | ||||
| 			// TODO modify this method as necessary - since the IE doesn't seem to be | ||||
| 			// present, we have no idea what the format looks like | ||||
| 			public static TbttInformationHeader parse(JsonObject contents) { | ||||
| 				return new TbttInformationHeader( | ||||
| 					contents.get("TBTT Information Type").getAsByte(), | ||||
| 					contents.get("Filtered Neighbor Map").getAsBoolean(), | ||||
| 					contents.get("TBTT Information Count").getAsByte(), | ||||
| 					contents.get("TBTT Information Length").getAsShort() | ||||
| 				); | ||||
| 			} | ||||
|  | ||||
| 			@Override | ||||
| 			public int hashCode() { | ||||
| 				return Objects.hash( | ||||
| 					tbttInformationType, | ||||
| 					filteredNeighborAp, | ||||
| 					tbttInformationCount, | ||||
| 					tbttInformationLength | ||||
| 				); | ||||
| 			} | ||||
|  | ||||
| 			@Override | ||||
| 			public boolean equals(Object obj) { | ||||
| 				if (obj == null) { | ||||
| 					return false; | ||||
| 				} | ||||
|  | ||||
| 				if (this == obj) { | ||||
| 					return true; | ||||
| 				} | ||||
|  | ||||
| 				if (getClass() != obj.getClass()) { | ||||
| 					return false; | ||||
| 				} | ||||
|  | ||||
| 				TbttInformationHeader other = (TbttInformationHeader) obj; | ||||
| 				return tbttInformationType == other.tbttInformationType && | ||||
| 					filteredNeighborAp == other.filteredNeighborAp && | ||||
| 					tbttInformationCount == other.tbttInformationCount && | ||||
| 					tbttInformationLength == other.tbttInformationLength; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		/** | ||||
| 		 * Subfield for TBTT Information | ||||
| 		 */ | ||||
| 		public static class TbttInformation { | ||||
| 			/** | ||||
| 			 * Unsigned 8 bits - offset in TUs, rounded down to nearest TU, to the next | ||||
| 			 * TBTT of an AP’s BSS from the immediately prior TBTT of the AP that | ||||
| 			 * transmits this element | ||||
| 			 */ | ||||
| 			public final short neighborApTbttOffset; | ||||
| 			/** BSSID of neighbor, optional */ | ||||
| 			public final String bssid; | ||||
| 			/** Short SSID of neighbor, optional */ | ||||
| 			public final String shortSsid; | ||||
|  | ||||
| 			/** Constructor */ | ||||
| 			public TbttInformation( | ||||
| 				short neighborApTbttOffset, | ||||
| 				String bssid, | ||||
| 				String shortSsid | ||||
| 			) { | ||||
| 				this.neighborApTbttOffset = neighborApTbttOffset; | ||||
| 				this.bssid = bssid; | ||||
| 				this.shortSsid = shortSsid; | ||||
| 			} | ||||
|  | ||||
| 			/** Parse TbttInformation from JSON object */ | ||||
| 			// TODO modify this method as necessary - since the IE doesn't seem to be | ||||
| 			// present, we have no idea what the format looks like | ||||
| 			public static TbttInformation parse(JsonObject contents) { | ||||
| 				return new TbttInformation( | ||||
| 					contents.get("Neighbor AP TBTT Offset").getAsShort(), | ||||
| 					IEUtils.parseOptionalStringField(contents, "BSSID"), | ||||
| 					IEUtils.parseOptionalStringField(contents, "Short SSID") | ||||
| 				); | ||||
| 			} | ||||
|  | ||||
| 			@Override | ||||
| 			public int hashCode() { | ||||
| 				return Objects.hash(neighborApTbttOffset, bssid, shortSsid); | ||||
| 			} | ||||
|  | ||||
| 			@Override | ||||
| 			public boolean equals(Object obj) { | ||||
| 				if (obj == null) { | ||||
| 					return false; | ||||
| 				} | ||||
|  | ||||
| 				if (this == obj) { | ||||
| 					return true; | ||||
| 				} | ||||
|  | ||||
| 				if (getClass() != obj.getClass()) { | ||||
| 					return false; | ||||
| 				} | ||||
|  | ||||
| 				TbttInformation other = (TbttInformation) obj; | ||||
| 				return neighborApTbttOffset == | ||||
| 					other.neighborApTbttOffset && bssid.equals(other.bssid) && | ||||
| 					Objects.equals(shortSsid, other.shortSsid); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		/** | ||||
| 		 * @see TbttInformationHeader | ||||
| 		 */ | ||||
| 		public final TbttInformationHeader tbttInformationHeader; | ||||
| 		/** | ||||
| 		 * Unsigned 8 bits - channel starting frequency that, together with the | ||||
| 		 * Channel Number field, indicates the primary channel of the BSSs of the APs | ||||
| 		 * in this Neighbor AP Information field | ||||
| 		 */ | ||||
| 		public final short operatingClass; | ||||
| 		/** | ||||
| 		 * Unsigned 8 bits - the last known primary channel of the APs in this | ||||
| 		 * Neighbor AP Information field. | ||||
| 		 */ | ||||
| 		public final short channelNumber; | ||||
| 		/** | ||||
| 		 * @see TbttInformation | ||||
| 		 */ | ||||
| 		public final TbttInformation tbttInformation; | ||||
|  | ||||
| 		/** Constructor */ | ||||
| 		public NeighborApInformation( | ||||
| 			TbttInformationHeader tbttInformationHeader, | ||||
| 			short operatingClass, | ||||
| 			short channelNumber, | ||||
| 			TbttInformation tbttInformation | ||||
| 		) { | ||||
| 			this.tbttInformationHeader = tbttInformationHeader; | ||||
| 			this.operatingClass = operatingClass; | ||||
| 			this.channelNumber = channelNumber; | ||||
| 			this.tbttInformation = tbttInformation; | ||||
| 		} | ||||
|  | ||||
| 		/** Parse NeighborApInformation from JSON object */ | ||||
| 		// TODO modify this method as necessary - since the IE doesn't seem to be | ||||
| 		// present, we have no idea what the format looks like | ||||
| 		public static NeighborApInformation parse(JsonObject contents) { | ||||
| 			return new NeighborApInformation( | ||||
| 				TbttInformationHeader.parse( | ||||
| 					contents.get("TBTT Information Header").getAsJsonObject() | ||||
| 				), | ||||
| 				contents.get("Operating Class").getAsShort(), | ||||
| 				contents.get("Channel Number").getAsShort(), | ||||
| 				TbttInformation.parse(contents.get("TBTT Information").getAsJsonObject()) | ||||
| 			); | ||||
| 		} | ||||
|  | ||||
| 		@Override | ||||
| 		public int hashCode() { | ||||
| 			return Objects.hash( | ||||
| 				tbttInformationHeader, | ||||
| 				operatingClass, | ||||
| 				channelNumber, | ||||
| 				tbttInformation | ||||
| 			); | ||||
| 		} | ||||
|  | ||||
| 		@Override | ||||
| 		public boolean equals(Object obj) { | ||||
| 			if (obj == null) { | ||||
| 				return false; | ||||
| 			} | ||||
|  | ||||
| 			if (this == obj) { | ||||
| 				return true; | ||||
| 			} | ||||
|  | ||||
| 			if (getClass() != obj.getClass()) { | ||||
| 				return false; | ||||
| 			} | ||||
|  | ||||
| 			NeighborApInformation other = (NeighborApInformation) obj; | ||||
| 			return tbttInformationHeader.equals(other.tbttInformationHeader) && | ||||
| 				operatingClass == other.operatingClass && | ||||
| 				channelNumber == other.channelNumber && | ||||
| 				Objects.equals(tbttInformation, other.tbttInformation); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/** number of channels in a subband of supported channels */ | ||||
| 	public final List<NeighborApInformation> neighborApInformations; | ||||
|  | ||||
| 	/** Constructor */ | ||||
| 	public ReducedNeighborReport( | ||||
| 		List<NeighborApInformation> neighborApInformations | ||||
| 	) { | ||||
| 		this.neighborApInformations = | ||||
| 			Collections.unmodifiableList(neighborApInformations); | ||||
| 	} | ||||
|  | ||||
| 	/** Parse ReducedNeighborReport from JSON object */ | ||||
| 	// TODO modify this method as necessary - since the IE doesn't seem to be | ||||
| 	// present, we have no idea what the format looks like | ||||
| 	public static ReducedNeighborReport parse(JsonObject contents) { | ||||
| 		List<NeighborApInformation> neighborApInformations = new ArrayList<>(); | ||||
|  | ||||
| 		JsonElement neighborApInformationsObject = | ||||
| 			contents.get("Neighbor AP Informations"); | ||||
| 		if (neighborApInformationsObject != null) { | ||||
| 			for ( | ||||
| 				JsonElement elem : neighborApInformationsObject.getAsJsonArray() | ||||
| 			) { | ||||
| 				neighborApInformations | ||||
| 					.add(NeighborApInformation.parse(elem.getAsJsonObject())); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		return new ReducedNeighborReport(neighborApInformations); | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public int hashCode() { | ||||
| 		return Objects.hash(neighborApInformations); | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public boolean equals(Object obj) { | ||||
| 		if (obj == null) { | ||||
| 			return false; | ||||
| 		} | ||||
|  | ||||
| 		if (this == obj) { | ||||
| 			return true; | ||||
| 		} | ||||
|  | ||||
| 		if (getClass() != obj.getClass()) { | ||||
| 			return false; | ||||
| 		} | ||||
|  | ||||
| 		ReducedNeighborReport other = (ReducedNeighborReport) obj; | ||||
| 		return neighborApInformations.equals(other.neighborApInformations); | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,70 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| package com.facebook.openwifi.cloudsdk.ies; | ||||
|  | ||||
| import java.util.Objects; | ||||
|  | ||||
| import com.google.gson.JsonObject; | ||||
|  | ||||
| // NOTE: Not validated (not seen on test devices) | ||||
| /** | ||||
|  * This information element (IE) appears in wifiscan entries. It's called | ||||
|  * "Supported Channels" in 802.11 specs (section 9.4.2.17). Refer to the | ||||
|  * specification for more details. Language in javadocs is taken from the | ||||
|  * specification. | ||||
|  */ | ||||
| public class SupportedChannels { | ||||
| 	/** Defined in 802.11 table 9-92 */ | ||||
| 	public static final int TYPE = 36; | ||||
|  | ||||
| 	/** Unsigned 8 bits - first channel in a subband of supported channels */ | ||||
| 	public final short firstChannelNumber; | ||||
| 	/** Unsigned 8 bits - number of channels in a subband of supported channels */ | ||||
| 	public final short numberOfChannels; | ||||
|  | ||||
| 	/** Constructor */ | ||||
| 	public SupportedChannels(short firstChannelNumber, short numberOfChannels) { | ||||
| 		this.firstChannelNumber = firstChannelNumber; | ||||
| 		this.numberOfChannels = numberOfChannels; | ||||
| 	} | ||||
|  | ||||
| 	/** Parse SupportedChannels from JSON object */ | ||||
| 	// TODO modify this method as necessary - since the IE doesn't seem to be | ||||
| 	// present, we have no idea what the format looks like | ||||
| 	public static SupportedChannels parse(JsonObject contents) { | ||||
| 		return new SupportedChannels( | ||||
| 			contents.get("First Channel Number").getAsShort(), | ||||
| 			contents.get("Number of Channels").getAsShort() | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public int hashCode() { | ||||
| 		return Objects.hash(firstChannelNumber, numberOfChannels); | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public boolean equals(Object obj) { | ||||
| 		if (obj == null) { | ||||
| 			return false; | ||||
| 		} | ||||
|  | ||||
| 		if (this == obj) { | ||||
| 			return true; | ||||
| 		} | ||||
|  | ||||
| 		if (getClass() != obj.getClass()) { | ||||
| 			return false; | ||||
| 		} | ||||
|  | ||||
| 		SupportedChannels other = (SupportedChannels) obj; | ||||
| 		return firstChannelNumber == other.firstChannelNumber && | ||||
| 			numberOfChannels == other.numberOfChannels; | ||||
| 	} | ||||
| } | ||||
| @@ -10,35 +10,42 @@ package com.facebook.openwifi.cloudsdk.ies; | ||||
|  | ||||
| import java.util.Objects; | ||||
|  | ||||
| import com.google.gson.JsonElement; | ||||
| import com.facebook.openwifi.cloudsdk.IEUtils; | ||||
| import com.google.gson.JsonObject; | ||||
|  | ||||
| /** | ||||
|  * This information element (IE) appears in wifiscan entries. It is called | ||||
|  * "Tx Pwr Info" in these entries, and "Transmit Power Envelope" in the 802.11 | ||||
|  * specification. Refer to the specification for more details. Language in | ||||
|  * specification (section 9.4.2.161). Refer to the specification for more details. Language in | ||||
|  * javadocs is taken from the specification. | ||||
|  */ | ||||
| public class TxPwrInfo { | ||||
|  | ||||
| 	/** Defined in 802.11 */ | ||||
| 	/** Defined in 802.11 table 9-92 */ | ||||
| 	public static final int TYPE = 195; | ||||
|  | ||||
| 	/** Local maximum transmit power for 20 MHz. Required field. */ | ||||
| 	public final int localMaxTxPwrConstraint20MHz; | ||||
| 	/** Local maximum transmit power for 40 MHz. Optional field. */ | ||||
| 	public final Integer localMaxTxPwrConstraint40MHz; | ||||
| 	/** Local maximum transmit power for 80 MHz. Optional field. */ | ||||
| 	public final Integer localMaxTxPwrConstraint80MHz; | ||||
| 	/** Local maximum transmit power for both 160 MHz and 80+80 MHz. Optional field. */ | ||||
| 	public final Integer localMaxTxPwrConstraint160MHz; | ||||
| 	/** | ||||
| 	 * Unsigned 8 bits - Local maximum transmit power for 20 MHz. Required field. | ||||
| 	 */ | ||||
| 	public final Short localMaxTxPwrConstraint20MHz; | ||||
| 	/** | ||||
| 	 * Unsigned 8 bits - Local maximum transmit power for 40 MHz. Optional field. | ||||
| 	 */ | ||||
| 	public final Short localMaxTxPwrConstraint40MHz; | ||||
| 	/** | ||||
| 	 * Unsigned 8 bits - Local maximum transmit power for 80 MHz. Optional field. | ||||
| 	 */ | ||||
| 	public final Short localMaxTxPwrConstraint80MHz; | ||||
| 	/** | ||||
| 	 * Unsigned 8 bits - Local maximum transmit power for both 160 MHz and 80+80 MHz. Optional field. | ||||
| 	 */ | ||||
| 	public final Short localMaxTxPwrConstraint160MHz; | ||||
|  | ||||
| 	/** Constructor */ | ||||
| 	public TxPwrInfo( | ||||
| 		int localMaxTxPwrConstraint20MHz, | ||||
| 		Integer localMaxTxPwrConstraint40MHz, | ||||
| 		Integer localMaxTxPwrConstraint80MHz, | ||||
| 		Integer localMaxTxPwrConstraint160MHz | ||||
| 		short localMaxTxPwrConstraint20MHz, | ||||
| 		Short localMaxTxPwrConstraint40MHz, | ||||
| 		Short localMaxTxPwrConstraint80MHz, | ||||
| 		Short localMaxTxPwrConstraint160MHz | ||||
| 	) { | ||||
| 		this.localMaxTxPwrConstraint20MHz = localMaxTxPwrConstraint20MHz; | ||||
| 		this.localMaxTxPwrConstraint40MHz = localMaxTxPwrConstraint40MHz; | ||||
| @@ -50,15 +57,24 @@ public class TxPwrInfo { | ||||
| 	public static TxPwrInfo parse(JsonObject contents) { | ||||
| 		JsonObject innerObj = contents.get("Tx Pwr Info").getAsJsonObject(); | ||||
| 		// required field | ||||
| 		int localMaxTxPwrConstraint20MHz = | ||||
| 			innerObj.get("Local Max Tx Pwr Constraint 20MHz").getAsInt(); | ||||
| 		short localMaxTxPwrConstraint20MHz = | ||||
| 			innerObj.get("Local Max Tx Pwr Constraint 20MHz").getAsShort(); | ||||
| 		// optional field | ||||
| 		Integer localMaxTxPwrConstraint40MHz = | ||||
| 			parseOptionalField(innerObj, "Local Max Tx Pwr Constraint 40MHz"); | ||||
| 		Integer localMaxTxPwrConstraint80MHz = | ||||
| 			parseOptionalField(innerObj, "Local Max Tx Pwr Constraint 80MHz"); | ||||
| 		Integer localMaxTxPwrConstraint160MHz = | ||||
| 			parseOptionalField(innerObj, "Local Max Tx Pwr Constraint 160MHz"); | ||||
| 		Short localMaxTxPwrConstraint40MHz = | ||||
| 			IEUtils.parseOptionalShortField( | ||||
| 				innerObj, | ||||
| 				"Local Max Tx Pwr Constraint 40MHz" | ||||
| 			); | ||||
| 		Short localMaxTxPwrConstraint80MHz = | ||||
| 			IEUtils.parseOptionalShortField( | ||||
| 				innerObj, | ||||
| 				"Local Max Tx Pwr Constraint 40MHz" | ||||
| 			); | ||||
| 		Short localMaxTxPwrConstraint160MHz = | ||||
| 			IEUtils.parseOptionalShortField( | ||||
| 				innerObj, | ||||
| 				"Local Max Tx Pwr Constraint 40MHz" | ||||
| 			); | ||||
| 		return new TxPwrInfo( | ||||
| 			localMaxTxPwrConstraint20MHz, | ||||
| 			localMaxTxPwrConstraint40MHz, | ||||
| @@ -67,17 +83,6 @@ public class TxPwrInfo { | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	private static Integer parseOptionalField( | ||||
| 		JsonObject contents, | ||||
| 		String fieldName | ||||
| 	) { | ||||
| 		JsonElement element = contents.get(fieldName); | ||||
| 		if (element == null) { | ||||
| 			return null; | ||||
| 		} | ||||
| 		return element.getAsInt(); | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public int hashCode() { | ||||
| 		return Objects.hash( | ||||
| @@ -108,13 +113,4 @@ public class TxPwrInfo { | ||||
| 				other.localMaxTxPwrConstraint40MHz && | ||||
| 			localMaxTxPwrConstraint80MHz == other.localMaxTxPwrConstraint80MHz; | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public String toString() { | ||||
| 		return "TxPwrInfo [localMaxTxPwrConstraint20MHz=" + | ||||
| 			localMaxTxPwrConstraint20MHz + ", localMaxTxPwrConstraint40MHz=" + | ||||
| 			localMaxTxPwrConstraint40MHz + ", localMaxTxPwrConstraint80MHz=" + | ||||
| 			localMaxTxPwrConstraint80MHz + ", localMaxTxPwrConstraint160MHz=" + | ||||
| 			localMaxTxPwrConstraint160MHz + "]"; | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -15,9 +15,12 @@ import org.apache.commons.codec.binary.Base64; | ||||
|  | ||||
| /** | ||||
|  * Very High Throughput (VHT) Operation Element, which is potentially present in | ||||
|  * wifiscan entries. Introduced in 802.11ac (2013). | ||||
|  * wifiscan entries. Introduced in 802.11ac (2013). Refer to the 802.11 | ||||
|  * specification (section 9.4.2.158) | ||||
|  */ | ||||
| public class VHTOperation { | ||||
| 	/** Defined in 802.11 table 9-92 */ | ||||
| 	public static final int TYPE = 192; | ||||
|  | ||||
| 	/** | ||||
| 	 * This field is 0 if the channel width is 20 MHz or 40 MHz, and 1 otherwise. | ||||
|   | ||||
| @@ -0,0 +1,12 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * Information elements (IEs) defined in the 802.11 specifications. | ||||
|  */ | ||||
| package com.facebook.openwifi.cloudsdk.ies; | ||||
| @@ -70,7 +70,12 @@ public class UCentralKafkaConsumer { | ||||
| 		/** The state payload JSON. */ | ||||
| 		public final JsonObject payload; | ||||
|  | ||||
| 		/** Unix time (ms). */ | ||||
| 		/** | ||||
| 		 * The record timestamp (Unix time, in ms). | ||||
| 		 * | ||||
| 		 * Depending on the broker configuration for "message.timestamp.type", | ||||
| 		 * this may either be the "CreateTime" or "LogAppendTime". | ||||
| 		 */ | ||||
| 		public final long timestampMs; | ||||
|  | ||||
| 		/** Constructor. */ | ||||
| @@ -85,7 +90,12 @@ public class UCentralKafkaConsumer { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/** Kafka record listener interface. */ | ||||
| 	/** | ||||
| 	 * Kafka record listener interface. | ||||
| 	 * | ||||
| 	 * The inputs must NOT be mutated, as they may be passed to multiple | ||||
| 	 * listeners and may result in ConcurrentModificationException. | ||||
| 	 */ | ||||
| 	public interface KafkaListener { | ||||
| 		/** Handle a list of state records. */ | ||||
| 		void handleStateRecords(List<KafkaRecord> records); | ||||
| @@ -271,7 +281,6 @@ public class UCentralKafkaConsumer { | ||||
| 					serialNumber, | ||||
| 					payload.toString() | ||||
| 				); | ||||
| 				// record.timestamp() is empirically confirmed to be Unix time (ms) | ||||
| 				KafkaRecord kafkaRecord = | ||||
| 					new KafkaRecord(serialNumber, payload, record.timestamp()); | ||||
| 				if (record.topic().equals(stateTopic)) { | ||||
|   | ||||
| @@ -0,0 +1,12 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * Kafka consumer and producer functionality required by the CloudSDK. | ||||
|  */ | ||||
| package com.facebook.openwifi.cloudsdk.kafka; | ||||
| @@ -0,0 +1,56 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| package com.facebook.openwifi.cloudsdk.models.ap; | ||||
|  | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
|  | ||||
| import com.google.gson.annotations.SerializedName; | ||||
|  | ||||
| /** | ||||
|  * AP capabilities schema. | ||||
|  * | ||||
|  * @see <a href="https://github.com/Telecominfraproject/wlan-ucentral-schema/blob/main/system/capabilities.uc">capabilities.uc</a> | ||||
|  */ | ||||
| public class Capabilities { | ||||
| 	public String compatible; | ||||
| 	public String model; | ||||
| 	public String platform; | ||||
| 	public Map<String, List<String>> network; | ||||
|  | ||||
| 	public static class Switch { | ||||
| 		public boolean enable; | ||||
| 		public boolean reset; | ||||
| 	} | ||||
|  | ||||
| 	@SerializedName("switch") public Map<String, Switch> switch_; | ||||
|  | ||||
| 	public static class Phy { | ||||
| 		public int tx_ant; | ||||
| 		public int rx_ant; | ||||
| 		public int[] frequencies; | ||||
| 		public int[] channels; | ||||
| 		public int[] dfs_channels; | ||||
| 		public String[] htmode; | ||||
| 		public String[] band; | ||||
| 		public int ht_capa; | ||||
| 		public int vht_capa; | ||||
| 		public int[] he_phy_capa; | ||||
| 		public int[] he_mac_capa; | ||||
| 		public String country; | ||||
| 		public String dfs_region; | ||||
| 		public int temperature; | ||||
| 	} | ||||
|  | ||||
| 	public Map<String, Phy> wifi; | ||||
| 	// TODO The fields below were omitted | ||||
| 	// macaddr; | ||||
| 	// country_code; | ||||
| 	// label_macaddr; | ||||
| } | ||||
| @@ -1,21 +0,0 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| package com.facebook.openwifi.cloudsdk.models.ap; | ||||
|  | ||||
| import com.google.gson.JsonObject; | ||||
| import com.google.gson.annotations.SerializedName; | ||||
|  | ||||
| public class DeviceCapabilities { | ||||
| 	public String compatible; | ||||
| 	public String model; | ||||
| 	public String platform; | ||||
| 	public JsonObject network; | ||||
| 	@SerializedName("switch") public JsonObject switch_; | ||||
| 	public JsonObject wifi; | ||||
| } | ||||
| @@ -11,6 +11,11 @@ package com.facebook.openwifi.cloudsdk.models.ap; | ||||
| import com.google.gson.JsonObject; | ||||
| import com.google.gson.annotations.SerializedName; | ||||
|  | ||||
| /** | ||||
|  * AP statistics/telemetry schema. | ||||
|  * | ||||
|  * @see <a href="https://github.com/Telecominfraproject/wlan-ucentral-schema/blob/main/state/state.yml">state.yml</a> | ||||
|  */ | ||||
| public class State { | ||||
| 	public static class Interface { | ||||
| 		public static class Client { | ||||
| @@ -54,6 +59,8 @@ public class State { | ||||
| 				public int ack_signal; | ||||
| 				public int ack_signal_avg; | ||||
| 				public JsonObject[] tid_stats; // TODO: see cfg80211_tid_stats | ||||
|  | ||||
| 				// TODO ipaddr_v4 - either string or object (ip4leases), but duplicated in "clients" | ||||
| 			} | ||||
|  | ||||
| 			public Association[] associations; | ||||
| @@ -115,7 +122,7 @@ public class State { | ||||
| 	public static class Radio { | ||||
| 		public long active_ms; | ||||
| 		public long busy_ms; | ||||
| 		public int channel; | ||||
| 		public int channel; // TODO might be int[] array?? | ||||
| 		public String channel_width; | ||||
| 		public long noise; | ||||
| 		public String phy; | ||||
|   | ||||
| @@ -0,0 +1,96 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| package com.facebook.openwifi.cloudsdk.models.ap; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
| import com.google.gson.JsonPrimitive; | ||||
| import com.google.gson.annotations.SerializedName; | ||||
|  | ||||
| /** | ||||
|  * AP configuration schema. | ||||
|  * | ||||
|  * @see <a href="https://github.com/Telecominfraproject/wlan-ucentral-schema/blob/main/schema/ucentral.yml">ucentral.yml</a> | ||||
|  */ | ||||
| public class UCentralSchema { | ||||
| 	public static class Radio { | ||||
| 		public String band; | ||||
| 		public int bandwidth; | ||||
| 		public JsonPrimitive channel; // either "auto" or int | ||||
| 		@SerializedName("valid-channels") public int[] validChannels; | ||||
| 		public String country; | ||||
| 		@SerializedName("allow-dfs") public boolean allowDfs; | ||||
| 		@SerializedName("channel-mode") public String channelMode; | ||||
| 		@SerializedName("channel-width") public int channelWidth; | ||||
| 		@SerializedName("require-mode") public String requireMode; | ||||
| 		public String mimo; | ||||
| 		@SerializedName("tx-power") public int txPower; | ||||
| 		@SerializedName("legacy-rates") public boolean legacyRates; | ||||
| 		@SerializedName("beacon-interval") public int beaconInterval; | ||||
| 		@SerializedName("dtim-period") public int dtimPeriod; | ||||
| 		@SerializedName("maximum-clients") public int maximumClients; | ||||
|  | ||||
| 		public static class Rates { | ||||
| 			public int beacon; | ||||
| 			public int multicast; | ||||
| 		} | ||||
|  | ||||
| 		public Rates rates; | ||||
|  | ||||
| 		public static class HESettings { | ||||
| 			@SerializedName("multiple-bssid") public boolean multipleBssid; | ||||
| 			public boolean ema; | ||||
| 			@SerializedName("bss-color") public int bssColor; | ||||
| 		} | ||||
|  | ||||
| 		@SerializedName("he-settings") public HESettings heSettings; | ||||
|  | ||||
| 		@SerializedName("hostapd-iface-raw") public String[] hostapdIfaceRaw; | ||||
| 	} | ||||
|  | ||||
| 	public List<Radio> radios; | ||||
|  | ||||
| 	public static class Metrics { | ||||
| 		public static class Statistics { | ||||
| 			public int interval; | ||||
| 			public List<String> types; | ||||
| 		} | ||||
|  | ||||
| 		public Statistics statistics; | ||||
|  | ||||
| 		public static class Health { | ||||
| 			public int interval; | ||||
| 		} | ||||
|  | ||||
| 		public Health health; | ||||
|  | ||||
| 		public static class WifiFrames { | ||||
| 			public List<String> filters; | ||||
| 		} | ||||
|  | ||||
| 		@SerializedName("wifi-frames") public WifiFrames wifiFrames; | ||||
|  | ||||
| 		public static class DhcpSnooping { | ||||
| 			public List<String> filters; | ||||
| 		} | ||||
|  | ||||
| 		@SerializedName("dhcp-snooping") public DhcpSnooping dhcpSnooping; | ||||
| 	} | ||||
|  | ||||
| 	public Metrics metrics; | ||||
|  | ||||
| 	// TODO also add fields below as needed | ||||
| 	// unit | ||||
| 	// globals | ||||
| 	// definitions | ||||
| 	// ethernet | ||||
| 	// switch | ||||
| 	// interfaces | ||||
| 	// services | ||||
| } | ||||
| @@ -10,10 +10,15 @@ package com.facebook.openwifi.cloudsdk.models.ap; | ||||
|  | ||||
| import java.util.Objects; | ||||
|  | ||||
| import com.facebook.openwifi.cloudsdk.WifiScanEntry; | ||||
|  | ||||
| /** | ||||
|  * Represents a single entry in wifi scan results. | ||||
|  * ies[] array is not stored directly, but parsed into WifiScanEntry fields | ||||
|  * Wi-Fi scan result schema. | ||||
|  * <p> | ||||
|  * Note that the {@code ies[]} array is not stored here, but parsed into | ||||
|  * {@link WifiScanEntry#ieContainer}. | ||||
|  * | ||||
|  * @see <a href="https://github.com/Telecominfraproject/wlan-ucentral-schema/blob/main/command/cmd_wifiscan.uc">cmd_wifiscan.uc</a> | ||||
|  */ | ||||
| public class WifiScanEntryResult { | ||||
| 	public int channel; | ||||
|   | ||||
| @@ -0,0 +1,14 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * Schemas originating from the AP-NOS (wlan-ap). | ||||
|  * | ||||
|  * @see <a href="https://github.com/Telecominfraproject/wlan-ucentral-schema">wlan-ucentral-schema</a> | ||||
|  */ | ||||
| package com.facebook.openwifi.cloudsdk.models.ap; | ||||
| @@ -8,10 +8,10 @@ | ||||
|  | ||||
| package com.facebook.openwifi.cloudsdk.models.gw; | ||||
|  | ||||
| import com.google.gson.JsonObject; | ||||
| import com.facebook.openwifi.cloudsdk.models.ap.Capabilities; | ||||
|  | ||||
| public class DeviceCapabilities { | ||||
| 	public JsonObject capabilities; | ||||
| 	public Capabilities capabilities; | ||||
| 	public long firstUpdate; | ||||
| 	public long lastUpdate; | ||||
| 	public String serialNumber; | ||||
|   | ||||
| @@ -0,0 +1,18 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| package com.facebook.openwifi.cloudsdk.models.gw; | ||||
|  | ||||
| public class ScriptRequest { | ||||
| 	public String serialNumber; | ||||
| 	public long timeout = 30; // in seconds | ||||
| 	public String type; // "shell", "ucode", "uci" | ||||
| 	public String script; | ||||
| 	public String scriptId; // required but unused? | ||||
| 	public long when = 0; | ||||
| } | ||||
| @@ -0,0 +1,14 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * Schemas defined in the uCentral Gateway. | ||||
|  * | ||||
|  * @see <a href="https://github.com/Telecominfraproject/wlan-cloud-ucentralgw">wlan-cloud-ucentralgw</a> | ||||
|  */ | ||||
| package com.facebook.openwifi.cloudsdk.models.gw; | ||||
| @@ -0,0 +1,14 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * Schemas defined in the Provisioning service. | ||||
|  * | ||||
|  * @see <a href="https://github.com/Telecominfraproject/wlan-cloud-owprov">wlan-cloud-owprov</a> | ||||
|  */ | ||||
| package com.facebook.openwifi.cloudsdk.models.prov; | ||||
| @@ -0,0 +1,14 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * Schemas defined in the Provisioning service specific to RRM. | ||||
|  * | ||||
|  * @see <a href="https://github.com/Telecominfraproject/wlan-cloud-owprov/blob/main/openapi/rrm_provider.yaml">rrm_provider.yaml</a> | ||||
|  */ | ||||
| package com.facebook.openwifi.cloudsdk.models.prov.rrm; | ||||
| @@ -0,0 +1,13 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * Library providing clients and models for the OpenWiFi uCentral-based | ||||
|  * CloudSDK. | ||||
|  */ | ||||
| package com.facebook.openwifi.cloudsdk; | ||||
							
								
								
									
										3
									
								
								lib-rca/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								lib-rca/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| # Root Cause Analysis (RCA) Java Library | ||||
| A Java library which analyzes statistics and provides root cause analysis (RCA) for clients. | ||||
| This is a work in progress. | ||||
							
								
								
									
										72
									
								
								lib-rca/pom.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								lib-rca/pom.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | ||||
| <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||||
|   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> | ||||
|   <modelVersion>4.0.0</modelVersion> | ||||
|   <artifactId>openwifi-librca</artifactId> | ||||
|   <packaging>jar</packaging> | ||||
|   <parent> | ||||
|     <groupId>com.facebook</groupId> | ||||
|     <artifactId>openwifi-base</artifactId> | ||||
|     <version>2.7.0</version> | ||||
|   </parent> | ||||
|   <properties> | ||||
|     <!-- Hack for static files located in root project --> | ||||
|     <myproject.root>${project.basedir}/..</myproject.root> | ||||
|   </properties> | ||||
|   <build> | ||||
|     <finalName>openwifi-librca</finalName> | ||||
|     <resources> | ||||
|       <resource> | ||||
|         <directory>src/main/resources</directory> | ||||
|         <includes> | ||||
|           <include>**/*</include> | ||||
|         </includes> | ||||
|       </resource> | ||||
|     </resources> | ||||
|     <plugins> | ||||
|       <plugin> | ||||
|         <groupId>org.apache.maven.plugins</groupId> | ||||
|         <artifactId>maven-compiler-plugin</artifactId> | ||||
|       </plugin> | ||||
|       <plugin> | ||||
|         <groupId>org.apache.maven.plugins</groupId> | ||||
|         <artifactId>maven-surefire-plugin</artifactId> | ||||
|       </plugin> | ||||
|       <plugin> | ||||
|         <groupId>org.apache.maven.plugins</groupId> | ||||
|         <artifactId>maven-javadoc-plugin</artifactId> | ||||
|       </plugin> | ||||
|       <plugin> | ||||
|         <groupId>com.diffplug.spotless</groupId> | ||||
|         <artifactId>spotless-maven-plugin</artifactId> | ||||
|       </plugin> | ||||
|     </plugins> | ||||
|   </build> | ||||
|   <dependencies> | ||||
|     <dependency> | ||||
|       <groupId>org.slf4j</groupId> | ||||
|       <artifactId>slf4j-api</artifactId> | ||||
|     </dependency> | ||||
|     <dependency> | ||||
|       <groupId>org.slf4j</groupId> | ||||
|       <artifactId>slf4j-log4j12</artifactId> | ||||
|     </dependency> | ||||
|     <dependency> | ||||
|       <groupId>org.junit.jupiter</groupId> | ||||
|       <artifactId>junit-jupiter-api</artifactId> | ||||
|       <scope>test</scope> | ||||
|     </dependency> | ||||
|     <dependency> | ||||
|       <groupId>org.junit.jupiter</groupId> | ||||
|       <artifactId>junit-jupiter-engine</artifactId> | ||||
|       <scope>test</scope> | ||||
|     </dependency> | ||||
|     <dependency> | ||||
|       <groupId>org.json</groupId> | ||||
|       <artifactId>json</artifactId> | ||||
|     </dependency> | ||||
|     <dependency> | ||||
|       <groupId>com.google.code.gson</groupId> | ||||
|       <artifactId>gson</artifactId> | ||||
|     </dependency> | ||||
|   </dependencies> | ||||
| </project> | ||||
| @@ -0,0 +1,13 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| package com.facebook.openwifi.librca; | ||||
|  | ||||
| public class RootCauseAnalyzer { | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,212 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| package com.facebook.openwifi.librca.inputs; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
|  | ||||
| /** Define root cause analysis configuration parameters */ | ||||
| public final class RCAParams { | ||||
| 	// Note: we expect to receive these parameters in json format, so for now | ||||
| 	// we do not include a constructor which takes in the member vars as inputs | ||||
|  | ||||
| 	/** Look-back window in ms */ | ||||
| 	public final int detectionWindowMs; | ||||
|  | ||||
| 	// KPI calculation parameters | ||||
| 	/** Minimum acceptable estimated throughput (Mbps) */ | ||||
| 	public final double minEstimatedThroughputMbps; | ||||
| 	/** Percentile (units are %) of estimated throughputs to use as the KPI */ | ||||
| 	public final double throughputAggregationPercentile; | ||||
| 	/** Maximum acceptable latency (ms) */ | ||||
| 	public final int maxLatencyThresholdMs; | ||||
| 	/** Maximum acceptable jitter (ms) */ | ||||
| 	public final int maxJitterThresholdMs; | ||||
| 	/** | ||||
| 	 * Maximum acceptable disconnection rate (disconnetions per hour). Note that | ||||
| 	 * this signifies a rate and the units happen to be per hour - this does not | ||||
| 	 * signify that every contiguous one-hour period be checked. | ||||
| 	 */ | ||||
| 	public final int maxDisconnectionRatePerHour; | ||||
|  | ||||
| 	// High Level metrics thresholds | ||||
| 	/** Minimum acceptable tx rate (Mbps) */ | ||||
| 	public final double minTxRateMbps; | ||||
| 	/** Maximum acceptable Packet Error Rate (PER) (units are %) */ | ||||
| 	public final double maxPERPercent; | ||||
| 	/** Minimum acceptable idle airtime (units are %) */ | ||||
| 	public final double minIdleAirtimePercent; | ||||
| 	/** Maximum acceptable number of clients for one radio */ | ||||
| 	public final int maxNumClients; | ||||
|  | ||||
| 	// Low Level metrics thresholds | ||||
| 	/** Minimum acceptable RSSI (dBm) */ | ||||
| 	public final int minRssidBm; | ||||
| 	/** Maximum acceptable noise (dBm) */ | ||||
| 	public final int maxNoisedBm; | ||||
| 	/** Maximum acceptable intf airtime (units are %) */ | ||||
| 	public final double maxIntfAirtimePercent; | ||||
| 	/** Maximum acceptable number of neighbors */ | ||||
| 	public final int maxNumNeighbors; | ||||
| 	/** Minimum acceptable client bandwidth (MHz) for non-2G bands / */ | ||||
| 	public final int minClientBandwidthMHz; | ||||
| 	/** Minimum acceptable Access Point (AP) bandwidth (MHz) for non-2G bands */ | ||||
| 	public final int minApBandwidthMHz; | ||||
| 	/** Minimum acceptable self airtime ratio (units are %) */ | ||||
| 	public final double minSelfAirtimeRatioPercent; | ||||
| 	/** Maximum acceptable tx dropped ratio (units are %) */ | ||||
| 	public final double maxTxDroppedRatioPercent; | ||||
|  | ||||
| 	/** Default constructor */ | ||||
| 	public RCAParams() { | ||||
| 		// 6 hours -> 21600000 ms | ||||
| 		this.detectionWindowMs = 21600000; | ||||
|  | ||||
| 		this.minEstimatedThroughputMbps = 10; | ||||
| 		this.throughputAggregationPercentile = 10.0; | ||||
| 		this.maxLatencyThresholdMs = 50; | ||||
| 		this.maxJitterThresholdMs = 20; | ||||
| 		this.maxDisconnectionRatePerHour = 20; | ||||
|  | ||||
| 		this.minTxRateMbps = 50; | ||||
| 		this.maxPERPercent = 10.0; | ||||
| 		this.minIdleAirtimePercent = 10.0; | ||||
| 		this.maxNumClients = 10; | ||||
|  | ||||
| 		this.minRssidBm = -70; | ||||
| 		this.maxNoisedBm = -95; | ||||
| 		this.maxIntfAirtimePercent = 75.0; | ||||
| 		this.maxNumNeighbors = 10; | ||||
| 		this.minClientBandwidthMHz = 80; | ||||
| 		this.minApBandwidthMHz = 80; | ||||
| 		this.minSelfAirtimeRatioPercent = 25.0; | ||||
| 		this.maxTxDroppedRatioPercent = 0.1; | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Confirm that the given value is positive. If it is not, add a String | ||||
| 	 * describing the problem to {@code errors}. | ||||
| 	 */ | ||||
| 	private static void validatePositive( | ||||
| 		String varName, | ||||
| 		int value, | ||||
| 		List<String> errors | ||||
| 	) { | ||||
| 		if (value <= 0) { | ||||
| 			errors.add(varName + " must be positive."); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Confirm that the given value is positive. If it is not, add a String | ||||
| 	 * describing the problem to {@code errors}. | ||||
| 	 */ | ||||
| 	private static void validatePositive( | ||||
| 		String varName, | ||||
| 		double value, | ||||
| 		List<String> errors | ||||
| 	) { | ||||
| 		if (value <= 0) { | ||||
| 			errors.add(varName + " must be positive."); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Confirm that the given value is a valid percentile (between 0 and 100 | ||||
| 	 * inclusive). If it is not, add a String describing the problem to | ||||
| 	 * {@code errors}. | ||||
| 	 */ | ||||
| 	private static void validatePercentile( | ||||
| 		String varName, | ||||
| 		double value, | ||||
| 		List<String> errors | ||||
| 	) { | ||||
| 		if (value < 0 || value > 100) { | ||||
| 			errors.add(varName + " must be between 0 and 100 inclusive."); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/** Return a list of errors (empty list of no errors) */ | ||||
| 	public List<String> validate() { | ||||
| 		List<String> errors = new ArrayList<>(); | ||||
| 		validatePositive("Detection window", detectionWindowMs, errors); | ||||
|  | ||||
| 		validatePositive( | ||||
| 			"Minimum estimated throughput", | ||||
| 			minEstimatedThroughputMbps, | ||||
| 			errors | ||||
| 		); | ||||
| 		validatePercentile( | ||||
| 			"Thoughput aggregation percentile", | ||||
| 			throughputAggregationPercentile, | ||||
| 			errors | ||||
| 		); | ||||
| 		validatePositive( | ||||
| 			"Maximum latency threshold", | ||||
| 			maxLatencyThresholdMs, | ||||
| 			errors | ||||
| 		); | ||||
| 		validatePositive( | ||||
| 			"Maximum jitter threshold", | ||||
| 			maxJitterThresholdMs, | ||||
| 			errors | ||||
| 		); | ||||
| 		validatePositive( | ||||
| 			"Maximum disconnection rate", | ||||
| 			maxDisconnectionRatePerHour, | ||||
| 			errors | ||||
| 		); | ||||
|  | ||||
| 		validatePositive("Minimum tx rate", minTxRateMbps, errors); | ||||
| 		validatePercentile( | ||||
| 			"Maximum Packet Error Rate (PER)", | ||||
| 			maxPERPercent, | ||||
| 			errors | ||||
| 		); | ||||
| 		validatePercentile( | ||||
| 			"Minimum idle airtime", | ||||
| 			minIdleAirtimePercent, | ||||
| 			errors | ||||
| 		); | ||||
| 		validatePositive("Maximum number of clients", maxNumClients, errors); | ||||
|  | ||||
| 		validatePercentile( | ||||
| 			"Maximum intf airtime", | ||||
| 			maxIntfAirtimePercent, | ||||
| 			errors | ||||
| 		); | ||||
| 		validatePositive( | ||||
| 			"Maximum number of neighbors", | ||||
| 			maxNumNeighbors, | ||||
| 			errors | ||||
| 		); | ||||
| 		validatePositive( | ||||
| 			"Minimum client bandwidth", | ||||
| 			minClientBandwidthMHz, | ||||
| 			errors | ||||
| 		); | ||||
| 		validatePositive( | ||||
| 			"Minimum Access Point (AP) bandwidth", | ||||
| 			minApBandwidthMHz, | ||||
| 			errors | ||||
| 		); | ||||
| 		validatePercentile( | ||||
| 			"Minimum self airtime ratio", | ||||
| 			minSelfAirtimeRatioPercent, | ||||
| 			errors | ||||
| 		); | ||||
| 		validatePercentile( | ||||
| 			"Maximum tx dropped ratio", | ||||
| 			maxTxDroppedRatioPercent, | ||||
| 			errors | ||||
| 		); | ||||
|  | ||||
| 		return errors; | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,12 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * Library providing Root Cause Analysis (RCA) functionality. | ||||
|  */ | ||||
| package com.facebook.openwifi.librca; | ||||
| @@ -0,0 +1,23 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| package com.facebook.openwifi.librca.stats; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
| /** | ||||
|  * Aggregated statistics for each client. | ||||
|  * Mainly handle KPI and metric calculations. | ||||
|  */ | ||||
| public class ClientStats { | ||||
| 	/** Client MAC */ | ||||
| 	public String station; | ||||
|  | ||||
| 	/** LinkStats that are of the same station(client) */ | ||||
| 	public List<LinkStats> connections; | ||||
| } | ||||
| @@ -0,0 +1,75 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| package com.facebook.openwifi.librca.stats; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
| /** | ||||
|  * Aggregation Statistics Model of InputStats. | ||||
|  * Aggregate by bssid, station and RadioConfig. | ||||
|  */ | ||||
| public class LinkStats { | ||||
| 	public static class RadioConfig { | ||||
| 		public int channel; | ||||
| 		public int channelWidth; | ||||
| 		public int txPower; | ||||
| 		public String phy; | ||||
| 	} | ||||
|  | ||||
| 	public static class AssociationInfo { | ||||
| 		/** Rate information for receive/transmit data rate. */ | ||||
| 		public static class Rate { | ||||
| 			public long bitRate; | ||||
| 			public int chWidth; | ||||
| 			public int mcs; | ||||
| 		} | ||||
|  | ||||
| 		public long connected; | ||||
| 		public long inactive; | ||||
| 		public int rssi; | ||||
| 		public long rxBytes; | ||||
| 		public long rxPackets; | ||||
| 		public Rate rxRate; | ||||
| 		public long txBytes; | ||||
| 		public long txDuration; | ||||
| 		public long txFailed; | ||||
| 		public long txPackets; | ||||
| 		public Rate txRate; | ||||
| 		public long txRetries; | ||||
| 		public int ackSignal; | ||||
| 		public int ackSignalAvg; | ||||
|  | ||||
| 		// The metrics below are from Interface the client was connected to. | ||||
| 		public long txPacketsCounters; | ||||
| 		public long txErrorsCounters; | ||||
| 		public long txDroppedCounters; | ||||
|  | ||||
| 		// The metrics below are from the radio the client was associated to. | ||||
| 		public long activeMsRadio; | ||||
| 		public long busyMsRadio; | ||||
| 		public long noiseRadio; | ||||
| 		public long receiveMsRadio; | ||||
| 		public long transmitMsRadio; | ||||
|  | ||||
| 		/** Unix time in milliseconds */ | ||||
| 		public long timestamp; | ||||
| 	} | ||||
|  | ||||
| 	/** BSSID of the AP radio */ | ||||
| 	public String bssid; | ||||
|  | ||||
| 	/** Client MAC */ | ||||
| 	public String station; | ||||
|  | ||||
| 	/** Radio configuration parameters */ | ||||
| 	public RadioConfig radioConfig; | ||||
|  | ||||
| 	/** Association list */ | ||||
| 	public List<AssociationInfo> associationInfoList; | ||||
| } | ||||
| @@ -0,0 +1,78 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| package com.facebook.openwifi.librca.stats.inputs; | ||||
|  | ||||
| /** | ||||
|  * Input data model. | ||||
|  * | ||||
|  * TODO: very incomplete | ||||
|   */ | ||||
| public class InputStats { | ||||
| 	/** Radio parameters */ | ||||
| 	public static class Radio { | ||||
| 		public long active_ms; | ||||
| 		public long busy_ms; | ||||
| 		public int channel; | ||||
| 		public String channel_width; | ||||
| 		public long noise; | ||||
| 		public String phy; | ||||
| 		public long receive_ms; | ||||
| 		public long transmit_ms; | ||||
| 		public int tx_power; | ||||
| 	} | ||||
|  | ||||
| 	public static class SSID { | ||||
| 		public static class Association { | ||||
| 			public static class Rate { | ||||
| 				public long bitrate; | ||||
| 				public int chwidth; | ||||
| 				public int mcs; | ||||
| 			} | ||||
|  | ||||
| 			public String bssid; // bssid of the AP radio | ||||
| 			public String station; // client MAC | ||||
| 			public long connected; | ||||
| 			public long inactive; | ||||
| 			public int rssi; | ||||
| 			public long rx_bytes; | ||||
| 			public long rx_packets; | ||||
| 			public Rate rx_rate; | ||||
| 			public long tx_bytes; | ||||
| 			public long tx_duration; | ||||
| 			public long tx_failed; | ||||
| 			public long tx_offset; | ||||
| 			public long tx_packets; | ||||
| 			public Rate tx_rate; | ||||
| 			public long tx_retries; | ||||
| 			public int ack_signal; | ||||
| 			public int ack_signal_avg; | ||||
| 		} | ||||
|  | ||||
| 		public Association[] associations; | ||||
| 		public Radio radio; | ||||
| 	} | ||||
|  | ||||
| 	/** Counters are for the wireless interface as a whole */ | ||||
| 	public static class Counters { | ||||
| 		public long rx_bytes; | ||||
| 		public long rx_packets; | ||||
| 		public long rx_errors; | ||||
| 		public long rx_dropped; | ||||
| 		public long tx_bytes; | ||||
| 		public long tx_packets; | ||||
| 		public long tx_errors; | ||||
| 		public long tx_dropped; | ||||
| 	} | ||||
|  | ||||
| 	public SSID[] ssids; | ||||
| 	public Counters counters; | ||||
|  | ||||
| 	/** Unix time in milliseconds */ | ||||
| 	public long timestamp; | ||||
| } | ||||
							
								
								
									
										7
									
								
								lib-rca/src/main/resources/log4j.properties
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								lib-rca/src/main/resources/log4j.properties
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| log4j.rootLogger=DEBUG, stdout | ||||
| #log4j.rootLogger=ERROR, stdout, file | ||||
|  | ||||
| log4j.appender.stdout=org.apache.log4j.ConsoleAppender | ||||
| log4j.appender.stdout.Target=System.out | ||||
| log4j.appender.stdout.layout=org.apache.log4j.PatternLayout | ||||
| log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd'T'HH:mm:ss.SSS} %-5p [%c{1}:%L] - %m%n | ||||
| @@ -0,0 +1,19 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| package com.facebook.openwifi.librca; | ||||
|  | ||||
| import org.junit.jupiter.api.Test; | ||||
|  | ||||
| public class RootCauseAnalyzerTest { | ||||
|  | ||||
| 	@Test | ||||
| 	void placeholderTest() { | ||||
| 		return; | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										4
									
								
								owrrm/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								owrrm/.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,4 +0,0 @@ | ||||
| /*.log* | ||||
| /device_config.json | ||||
| /settings.json | ||||
| /topology.json | ||||
| @@ -97,3 +97,27 @@ Parameters: | ||||
|     * values: int < 30 (default: -70) | ||||
| * `nthSmallestRssi`: the nth smallest RSSI that is used for tx power calculation | ||||
|     * values: int >= 0 (default: 0) | ||||
|  | ||||
| ## Client Steering | ||||
| `ClientSteeringOptimizer` and its subclasses implement client steering | ||||
| algorithms via 802.11k/v/r mechanisms, with the goal of moving clients to | ||||
| optimal APs and/or bands. | ||||
|  | ||||
| **Client steering is a work in progress and NOT currently functional.** | ||||
|  | ||||
| ### `SingleAPBandSteering` | ||||
| This algorithm performs same-AP RRSI-based steering only, using a simple | ||||
| decision and backoff procedure. | ||||
|  | ||||
| Parameters: | ||||
| * `mode`: "band" | ||||
| * `minRssi2G`: RSSI (dBm) below which a client on the 2G band should be kicked | ||||
|     * values: int < 30 (default: -87) | ||||
| * `maxRssi2G`: RSSI (dBm) above which a client on the 2G band should roam to | ||||
|    5G/6G | ||||
|     * values: int < 30 (default: -67) | ||||
| * `minRssiNon2G`: RSSI (dBm) below which a client on the 5G/6G band should roam | ||||
|   to 2G | ||||
|     * values: int < 30 (default: -82) | ||||
| * `backoffTimeSec`: Backoff time (seconds) for all APs and radios | ||||
|     * values: int >= 0 (default: 300) | ||||
|   | ||||
| @@ -551,6 +551,12 @@ components: | ||||
|             additionalProperties: | ||||
|               type: integer | ||||
|               format: int32 | ||||
|         apClientActionMap: | ||||
|           type: object | ||||
|           additionalProperties: | ||||
|             type: object | ||||
|             additionalProperties: | ||||
|               type: string | ||||
|     Certificate: | ||||
|       type: object | ||||
|       properties: | ||||
|   | ||||
| @@ -135,9 +135,5 @@ | ||||
|       <groupId>org.quartz-scheduler</groupId> | ||||
|       <artifactId>quartz</artifactId> | ||||
|     </dependency> | ||||
|     <dependency> | ||||
|       <groupId>org.openjdk.jol</groupId> | ||||
|       <artifactId>jol-core</artifactId> | ||||
|     </dependency> | ||||
|   </dependencies> | ||||
| </project> | ||||
|   | ||||
| @@ -47,17 +47,17 @@ public class DeviceDataManager { | ||||
| 	private final ReadWriteLock topologyLock = new ReentrantReadWriteLock(); | ||||
|  | ||||
| 	/** Lock on {@link #deviceLayeredConfig}. */ | ||||
| 	public final ReadWriteLock deviceLayeredConfigLock = | ||||
| 	private final ReadWriteLock deviceLayeredConfigLock = | ||||
| 		new ReentrantReadWriteLock(); | ||||
|  | ||||
| 	/** The current device topology. */ | ||||
| 	public DeviceTopology topology; | ||||
| 	private DeviceTopology topology; | ||||
|  | ||||
| 	/** The current layered device config. */ | ||||
| 	public DeviceLayeredConfig deviceLayeredConfig; | ||||
| 	private DeviceLayeredConfig deviceLayeredConfig; | ||||
|  | ||||
| 	/** The cached device configs (map of serial number to computed config). */ | ||||
| 	public Map<String, DeviceConfig> cachedDeviceConfigs = | ||||
| 	private Map<String, DeviceConfig> cachedDeviceConfigs = | ||||
| 		new ConcurrentHashMap<>(); | ||||
|  | ||||
| 	/** Empty constructor without backing files (ex. for unit tests). */ | ||||
|   | ||||
| @@ -30,6 +30,7 @@ import com.facebook.openwifi.rrm.modules.Modeler; | ||||
| import com.facebook.openwifi.rrm.modules.ProvMonitor; | ||||
| import com.facebook.openwifi.rrm.modules.RRMScheduler; | ||||
| import com.facebook.openwifi.rrm.mysql.DatabaseManager; | ||||
| import com.facebook.openwifi.rrm.rca.modules.StationPinger; | ||||
|  | ||||
| /** | ||||
|  * RRM service runner. | ||||
| @@ -134,10 +135,16 @@ public class RRM { | ||||
| 				) : null; | ||||
| 		KafkaRunner kafkaRunner = (consumer == null && producer == null) | ||||
| 			? null : new KafkaRunner(consumer, producer); | ||||
| 		StationPinger stationPinger = new StationPinger( | ||||
| 			config.rcaConfig.stationPingerParams, | ||||
| 			client, | ||||
| 			consumer | ||||
| 		); | ||||
|  | ||||
| 		// Add shutdown hook | ||||
| 		Runtime.getRuntime().addShutdownHook(new Thread(() -> { | ||||
| 			logger.debug("Running shutdown hook..."); | ||||
| 			stationPinger.shutdown(); | ||||
| 			if (kafkaRunner != null) { | ||||
| 				kafkaRunner.shutdown(); | ||||
| 			} | ||||
|   | ||||
| @@ -20,6 +20,9 @@ import com.facebook.openwifi.rrm.optimizers.channel.ChannelOptimizer; | ||||
| import com.facebook.openwifi.rrm.optimizers.channel.LeastUsedChannelOptimizer; | ||||
| import com.facebook.openwifi.rrm.optimizers.channel.RandomChannelInitializer; | ||||
| import com.facebook.openwifi.rrm.optimizers.channel.UnmanagedApAwareChannelOptimizer; | ||||
| import com.facebook.openwifi.rrm.optimizers.clientsteering.ClientSteeringOptimizer; | ||||
| import com.facebook.openwifi.rrm.optimizers.clientsteering.ClientSteeringState; | ||||
| import com.facebook.openwifi.rrm.optimizers.clientsteering.SingleAPBandSteering; | ||||
| import com.facebook.openwifi.rrm.optimizers.tpc.LocationBasedOptimalTPC; | ||||
| import com.facebook.openwifi.rrm.optimizers.tpc.MeasurementBasedApApTPC; | ||||
| import com.facebook.openwifi.rrm.optimizers.tpc.MeasurementBasedApClientTPC; | ||||
| @@ -33,6 +36,14 @@ public class RRMAlgorithm { | ||||
| 	private static final Logger logger = | ||||
| 		LoggerFactory.getLogger(RRMAlgorithm.class); | ||||
|  | ||||
| 	/** | ||||
| 	 * Client steering state. A single instance must be passed into all client | ||||
| 	 * steering algorithms, as the state must persist across runs of the | ||||
| 	 * optimizers. | ||||
| 	 */ | ||||
| 	private static final ClientSteeringState clientSteeringState = | ||||
| 		new ClientSteeringState(); | ||||
|  | ||||
| 	/** RRM algorithm type enum. */ | ||||
| 	public enum AlgorithmType { | ||||
| 		OptimizeChannel ( | ||||
| @@ -42,6 +53,10 @@ public class RRMAlgorithm { | ||||
| 		OptimizeTxPower ( | ||||
| 			"Optimize tx power configuration", | ||||
| 			"Run transmit power control algorithm" | ||||
| 		), | ||||
| 		ClientSteering ( | ||||
| 			"Steer clients onto the optimal AP and band", | ||||
| 			"Run client steering algorithm" | ||||
| 		); | ||||
|  | ||||
| 		/** The long name. */ | ||||
| @@ -72,6 +87,13 @@ public class RRMAlgorithm { | ||||
| 		 * @see TPC#computeTxPowerMap() | ||||
| 		 */ | ||||
| 		public Map<String, Map<String, Integer>> txPowerMap; | ||||
|  | ||||
| 		/** | ||||
| 		 * Computed actions for each AP-client pair. | ||||
| 		 * | ||||
| 		 * @see ClientSteeringOptimizer#computeApClientActionMap(boolean) | ||||
| 		 */ | ||||
| 		public Map<String, Map<String, String>> apClientActionMap; | ||||
| 	} | ||||
|  | ||||
| 	/** The algorithm name (should be AlgorithmType enum string). */ | ||||
| @@ -286,6 +308,41 @@ public class RRMAlgorithm { | ||||
| 					configManager.queueZoneAndWakeUp(zone); | ||||
| 				} | ||||
| 			} | ||||
| 		} else if ( | ||||
| 			name.equals(RRMAlgorithm.AlgorithmType.ClientSteering.name()) | ||||
| 		) { | ||||
| 			logger.info( | ||||
| 				"Zone '{}': Running client steering optimizer (mode='{}')", | ||||
| 				zone, | ||||
| 				mode | ||||
| 			); | ||||
| 			ClientSteeringOptimizer optimizer; | ||||
| 			switch (mode) { | ||||
| 			default: | ||||
| 				if (!allowDefaultMode || !mode.isEmpty()) { | ||||
| 					result.error = modeErrorStr; | ||||
| 					return result; | ||||
| 				} | ||||
| 				logger.info("Using default algorithm mode..."); | ||||
| 				// fall through | ||||
| 			case SingleAPBandSteering.ALGORITHM_ID: | ||||
| 				optimizer = SingleAPBandSteering.makeWithArgs( | ||||
| 					modeler.getDataModelCopy(), | ||||
| 					zone, | ||||
| 					deviceDataManager, | ||||
| 					clientSteeringState, | ||||
| 					args | ||||
| 				); | ||||
| 				break; | ||||
| 			} | ||||
| 			result.apClientActionMap = | ||||
| 				optimizer.computeApClientActionMap(dryRun); | ||||
| 			if (!dryRun) { | ||||
| 				optimizer.steer(result.apClientActionMap); | ||||
| 				if (updateImmediately) { | ||||
| 					configManager.queueZoneAndWakeUp(zone); | ||||
| 				} | ||||
| 			} | ||||
| 		} else { | ||||
| 			result.error = String.format("Unknown algorithm: '%s'", name); | ||||
| 		} | ||||
|   | ||||
| @@ -10,6 +10,8 @@ package com.facebook.openwifi.rrm; | ||||
|  | ||||
| import java.util.Map; | ||||
|  | ||||
| import com.facebook.openwifi.rrm.rca.RCAConfig; | ||||
|  | ||||
| /** | ||||
|  * RRM service configuration model. | ||||
|  */ | ||||
| @@ -405,6 +407,9 @@ public class RRMConfig { | ||||
| 	/** Module configuration. */ | ||||
| 	public ModuleConfig moduleConfig = new ModuleConfig(); | ||||
|  | ||||
| 	/** Root cause analysis configuration. */ | ||||
| 	public RCAConfig rcaConfig = new RCAConfig(); | ||||
|  | ||||
| 	/** Construct RRMConfig from environment variables. */ | ||||
| 	public static RRMConfig fromEnv(Map<String, String> env) { | ||||
| 		RRMConfig config = new RRMConfig(); | ||||
| @@ -583,6 +588,9 @@ public class RRMConfig { | ||||
|  | ||||
| 		// @formatter:on | ||||
|  | ||||
| 		/* RCAConfig */ | ||||
| 		config.rcaConfig = RCAConfig.fromEnv(env); | ||||
|  | ||||
| 		return config; | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -61,7 +61,6 @@ import com.facebook.openwifi.rrm.optimizers.tpc.RandomTxPowerInitializer; | ||||
| import com.google.gson.Gson; | ||||
| import com.google.gson.GsonBuilder; | ||||
|  | ||||
| import org.openjdk.jol.info.GraphLayout; | ||||
| import io.swagger.v3.core.util.Json; | ||||
| import io.swagger.v3.core.util.Yaml; | ||||
| import io.swagger.v3.jaxrs2.Reader; | ||||
| @@ -312,7 +311,6 @@ public class ApiServer implements Runnable { | ||||
| 		service.get("/api/v1/currentModel", new GetCurrentModelEndpoint()); | ||||
| 		service.get("/api/v1/optimizeChannel", new OptimizeChannelEndpoint()); | ||||
| 		service.get("/api/v1/optimizeTxPower", new OptimizeTxPowerEndpoint()); | ||||
| 		service.get("/api/v1/memory", new MemoryEndpoint(this)); | ||||
|  | ||||
| 		logger.info( | ||||
| 			"API server listening for HTTP internal on port {} and external on port {}", | ||||
| @@ -1362,91 +1360,6 @@ public class ApiServer implements Runnable { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	@Path("/api/v1/memory") | ||||
| 	public class MemoryEndpoint implements Route { | ||||
| 		private final ApiServer apiServer; | ||||
|  | ||||
| 		MemoryEndpoint(ApiServer server) { | ||||
| 			this.apiServer = server; | ||||
| 		} | ||||
|  | ||||
| 		@Override | ||||
| 		public String handle(Request request, Response response) { | ||||
| 			String type = request.queryParamOrDefault("type", ""); | ||||
| 			String view = request.queryParamOrDefault("view", "footprint"); | ||||
|  | ||||
| 			java.util.function.Function<GraphLayout, String> fn = (GraphLayout graph) -> { | ||||
| 				return view.equals("footprint") ? graph.toFootprint() : graph.toPrintable(); | ||||
| 			}; | ||||
|  | ||||
| 			String result; | ||||
| 			switch (type) { | ||||
| 				case "modeler.dataModel": | ||||
| 					result = fn.apply(GraphLayout.parseInstance(apiServer.modeler.dataModel)); | ||||
| 					break; | ||||
|  | ||||
| 				case "modeler.dataModel.latestWifiScans": | ||||
| 					result = fn.apply(GraphLayout.parseInstance(apiServer.modeler.dataModel.latestWifiScans)); | ||||
| 					break; | ||||
|  | ||||
| 				case "modeler.dataModel.latestStates": | ||||
| 					result = fn.apply(GraphLayout.parseInstance(apiServer.modeler.dataModel.latestStates)); | ||||
| 					break; | ||||
|  | ||||
| 				case "modeler.dataModel.latestDeviceStatusRadios": | ||||
| 					result = fn.apply(GraphLayout.parseInstance(apiServer.modeler.dataModel.latestDeviceStatusRadios)); | ||||
| 					break; | ||||
|  | ||||
| 				case "modeler.deviceDataManager": | ||||
| 					result = fn.apply(GraphLayout.parseInstance(apiServer.modeler.deviceDataManager)); | ||||
| 					break; | ||||
|  | ||||
| 				case "modeler.deviceDataManager.topology": | ||||
| 					result = fn.apply(GraphLayout.parseInstance(apiServer.modeler.deviceDataManager.topology)); | ||||
| 					break; | ||||
|  | ||||
| 				case "modeler.deviceDataManager.deviceLayeredConfig": | ||||
| 					result = fn.apply(GraphLayout.parseInstance(apiServer.modeler.deviceDataManager.deviceLayeredConfig)); | ||||
| 					break; | ||||
|  | ||||
| 				case "modeler.deviceDataManager.cachedDeviceConfigs": | ||||
| 					result = fn.apply(GraphLayout.parseInstance(apiServer.modeler.deviceDataManager.cachedDeviceConfigs)); | ||||
| 					break; | ||||
|  | ||||
| 				case "modeler.dataModel.latestDeviceCapabilities": | ||||
| 					result = fn.apply(GraphLayout.parseInstance(apiServer.modeler.dataModel.latestDeviceCapabilities)); | ||||
|  | ||||
| 				case "modeler": | ||||
| 					result = fn.apply(GraphLayout.parseInstance(apiServer.modeler)); | ||||
| 					break; | ||||
|  | ||||
| 				case "configManager.deviceDataMap": | ||||
| 					result = fn.apply(GraphLayout.parseInstance(apiServer.configManager.deviceDataMap)); | ||||
| 					break; | ||||
|  | ||||
| 				case "configManager": | ||||
| 					result = fn.apply(GraphLayout.parseInstance(apiServer.configManager)); | ||||
| 					break; | ||||
|  | ||||
| 				case "scheduler": | ||||
| 					result = fn.apply(GraphLayout.parseInstance(apiServer.scheduler)); | ||||
| 					break; | ||||
|  | ||||
| 				case "deviceDataManager": | ||||
| 					result = fn.apply(GraphLayout.parseInstance(apiServer.deviceDataManager)); | ||||
| 					break; | ||||
|  | ||||
| 				case "": | ||||
| 				default: | ||||
| 					result = GraphLayout.parseInstance(apiServer).toFootprint(); | ||||
| 					break; | ||||
| 			} | ||||
|  | ||||
| 			logger.info("MEMORY RESPONSE: \n{}", result); | ||||
| 			return result; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	@Path("/api/v1/optimizeTxPower") | ||||
| 	public class OptimizeTxPowerEndpoint implements Route { | ||||
| 		// Hack for use in @ApiResponse -> @Content -> @Schema | ||||
|   | ||||
| @@ -8,7 +8,6 @@ | ||||
|  | ||||
| package com.facebook.openwifi.rrm.modules; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.HashMap; | ||||
| import java.util.HashSet; | ||||
| import java.util.List; | ||||
| @@ -46,16 +45,13 @@ public class ConfigManager implements Runnable { | ||||
| 	private final UCentralClient client; | ||||
|  | ||||
| 	/** Runtime per-device data. */ | ||||
| 	public class DeviceData { | ||||
| 		/** Last received device config. */ | ||||
| 		public UCentralApConfiguration config; | ||||
|  | ||||
| 	private class DeviceData { | ||||
| 		/** Last config time (in monotonic ns). */ | ||||
| 		public Long lastConfigTimeNs; | ||||
| 	} | ||||
|  | ||||
| 	/** Map from device serial number to runtime data. */ | ||||
| 	public Map<String, DeviceData> deviceDataMap = new TreeMap<>(); | ||||
| 	private Map<String, DeviceData> deviceDataMap = new TreeMap<>(); | ||||
|  | ||||
| 	/** The main thread reference (i.e. where {@link #run()} is invoked). */ | ||||
| 	private Thread mainThread; | ||||
| @@ -183,7 +179,7 @@ public class ConfigManager implements Runnable { | ||||
| 		long now = System.nanoTime(); | ||||
|  | ||||
| 		// Apply any config updates locally | ||||
| 		List<String> devicesNeedingUpdate = new ArrayList<>(); | ||||
| 		Map<String, String> devicesNeedingUpdate = new HashMap<>(); | ||||
| 		final long CONFIG_DEBOUNCE_INTERVAL_NS = | ||||
| 			params.configDebounceIntervalSec * 1_000_000_000L; | ||||
| 		Set<String> zonesToUpdateCopy = new HashSet<>(zonesToUpdate); | ||||
| @@ -204,11 +200,12 @@ public class ConfigManager implements Runnable { | ||||
| 				); | ||||
| 				continue; | ||||
| 			} | ||||
| 			data.config = new UCentralApConfiguration(device.configuration); | ||||
| 			UCentralApConfiguration config = | ||||
| 				new UCentralApConfiguration(device.configuration); | ||||
|  | ||||
| 			// Call receive listeners | ||||
| 			for (ConfigListener listener : configListeners.values()) { | ||||
| 				listener.receiveDeviceConfig(device.serialNumber, data.config); | ||||
| 				listener.receiveDeviceConfig(device.serialNumber, config); | ||||
| 			} | ||||
| 			// Check if there are requested updates for this zone | ||||
| 			String deviceZone = | ||||
| @@ -236,7 +233,7 @@ public class ConfigManager implements Runnable { | ||||
| 			for (ConfigListener listener : configListeners.values()) { | ||||
| 				boolean wasModified = listener.processDeviceConfig( | ||||
| 					device.serialNumber, | ||||
| 					data.config | ||||
| 					config | ||||
| 				); | ||||
| 				if (wasModified) { | ||||
| 					modified = true; | ||||
| @@ -257,7 +254,8 @@ public class ConfigManager implements Runnable { | ||||
| 					); | ||||
| 					continue; | ||||
| 				} else { | ||||
| 					devicesNeedingUpdate.add(device.serialNumber); | ||||
| 					devicesNeedingUpdate | ||||
| 						.put(device.serialNumber, config.toString()); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| @@ -275,19 +273,26 @@ public class ConfigManager implements Runnable { | ||||
| 				devicesNeedingUpdate.size() | ||||
| 			); | ||||
| 		} else { | ||||
| 			// TODO: Replace with the newer owprov API to send only deltas, not | ||||
| 			// the full configuration blobs: | ||||
| 			//   PUT /configurationOverrides/{serialNumber}?source=owrrm | ||||
| 			logger.info( | ||||
| 				"Sending config to {} device(s): {}", | ||||
| 				devicesNeedingUpdate.size(), | ||||
| 				String.join(", ", devicesNeedingUpdate) | ||||
| 				String.join(", ", devicesNeedingUpdate.keySet()) | ||||
| 			); | ||||
| 			for (String serialNumber : devicesNeedingUpdate) { | ||||
| 			for ( | ||||
| 				Map.Entry<String, String> entry : devicesNeedingUpdate | ||||
| 					.entrySet() | ||||
| 			) { | ||||
| 				String serialNumber = entry.getKey(); | ||||
| 				DeviceData data = deviceDataMap.get(serialNumber); | ||||
| 				logger.info( | ||||
| 					"Device {}: sending new configuration...", | ||||
| 					serialNumber | ||||
| 				); | ||||
| 				data.lastConfigTimeNs = System.nanoTime(); | ||||
| 				client.configure(serialNumber, data.config.toString()); | ||||
| 				client.configure(serialNumber, entry.getValue()); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|   | ||||
| @@ -8,6 +8,7 @@ | ||||
|  | ||||
| package com.facebook.openwifi.rrm.modules; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.LinkedList; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| @@ -20,13 +21,15 @@ import java.util.concurrent.LinkedBlockingQueue; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
|  | ||||
| import com.facebook.openwifi.cloudsdk.StateInfo; | ||||
| import com.facebook.openwifi.cloudsdk.UCentralApConfiguration; | ||||
| import com.facebook.openwifi.cloudsdk.UCentralClient; | ||||
| import com.facebook.openwifi.cloudsdk.UCentralUtils; | ||||
| import com.facebook.openwifi.cloudsdk.WifiScanEntry; | ||||
| import com.facebook.openwifi.cloudsdk.kafka.UCentralKafkaConsumer; | ||||
| import com.facebook.openwifi.cloudsdk.kafka.UCentralKafkaConsumer.KafkaRecord; | ||||
| import com.facebook.openwifi.cloudsdk.models.ap.State; | ||||
| import com.facebook.openwifi.cloudsdk.models.ap.Capabilities; | ||||
| import com.facebook.openwifi.cloudsdk.models.ap.UCentralSchema; | ||||
| import com.facebook.openwifi.cloudsdk.models.gw.DeviceCapabilities; | ||||
| import com.facebook.openwifi.cloudsdk.models.gw.DeviceWithStatus; | ||||
| import com.facebook.openwifi.cloudsdk.models.gw.ServiceEvent; | ||||
| @@ -36,7 +39,6 @@ import com.facebook.openwifi.rrm.DeviceDataManager; | ||||
| import com.facebook.openwifi.rrm.RRMConfig.ModuleConfig.ModelerParams; | ||||
| import com.facebook.openwifi.rrm.Utils; | ||||
| import com.google.gson.Gson; | ||||
| import com.google.gson.JsonArray; | ||||
| import com.google.gson.JsonObject; | ||||
| import com.google.gson.JsonSyntaxException; | ||||
|  | ||||
| @@ -50,7 +52,7 @@ public class Modeler implements Runnable { | ||||
| 	private final ModelerParams params; | ||||
|  | ||||
| 	/** The device data manager. */ | ||||
| 	public final DeviceDataManager deviceDataManager; | ||||
| 	private final DeviceDataManager deviceDataManager; | ||||
|  | ||||
| 	/** The uCentral client instance. */ | ||||
| 	private final UCentralClient client; | ||||
| @@ -74,7 +76,7 @@ public class Modeler implements Runnable { | ||||
| 	} | ||||
|  | ||||
| 	/** The blocking data queue. */ | ||||
| 	public final BlockingQueue<InputData> dataQueue = | ||||
| 	private final BlockingQueue<InputData> dataQueue = | ||||
| 		new LinkedBlockingQueue<>(); | ||||
|  | ||||
| 	/** Data model representation. */ | ||||
| @@ -94,15 +96,15 @@ public class Modeler implements Runnable { | ||||
| 			new ConcurrentHashMap<>(); | ||||
|  | ||||
| 		/** List of latest states per device. */ | ||||
| 		public Map<String, List<State>> latestStates = | ||||
| 		public Map<String, List<StateInfo>> latestStates = | ||||
| 			new ConcurrentHashMap<>(); | ||||
|  | ||||
| 		/** List of radio info per device. */ | ||||
| 		public Map<String, JsonArray> latestDeviceStatusRadios = | ||||
| 		public Map<String, List<UCentralSchema.Radio>> latestDeviceStatusRadios = | ||||
| 			new ConcurrentHashMap<>(); | ||||
|  | ||||
| 		/** List of capabilities per device. */ | ||||
| 		public Map<String, JsonObject> latestDeviceCapabilities = | ||||
| 		public Map<String, Map<String, Capabilities.Phy>> latestDeviceCapabilitiesPhy = | ||||
| 			new ConcurrentHashMap<>(); | ||||
| 	} | ||||
|  | ||||
| @@ -168,10 +170,15 @@ public class Modeler implements Runnable { | ||||
| 			consumer.addKafkaListener( | ||||
| 				getClass().getSimpleName(), | ||||
| 				new UCentralKafkaConsumer.KafkaListener() { | ||||
| 					// NOTE: list copying due to potential modification in run() | ||||
|  | ||||
| 					@Override | ||||
| 					public void handleStateRecords(List<KafkaRecord> records) { | ||||
| 						dataQueue.offer( | ||||
| 							new InputData(InputDataType.STATE, records) | ||||
| 							new InputData( | ||||
| 								InputDataType.STATE, | ||||
| 								new ArrayList<>(records) | ||||
| 							) | ||||
| 						); | ||||
| 					} | ||||
|  | ||||
| @@ -180,7 +187,10 @@ public class Modeler implements Runnable { | ||||
| 						List<KafkaRecord> records | ||||
| 					) { | ||||
| 						dataQueue.offer( | ||||
| 							new InputData(InputDataType.WIFISCAN, records) | ||||
| 							new InputData( | ||||
| 								InputDataType.WIFISCAN, | ||||
| 								new ArrayList<>(records) | ||||
| 							) | ||||
| 						); | ||||
| 					} | ||||
|  | ||||
| @@ -265,9 +275,12 @@ public class Modeler implements Runnable { | ||||
| 				continue; | ||||
| 			} | ||||
| 			JsonObject state = records.data.get(0).data; | ||||
| 			long timestamp = records.data.get(0).recorded; | ||||
| 			if (state != null) { | ||||
| 				try { | ||||
| 					State stateModel = gson.fromJson(state, State.class); | ||||
| 					StateInfo stateModel = | ||||
| 						gson.fromJson(state, StateInfo.class); | ||||
| 					stateModel.timestamp = timestamp; | ||||
| 					dataModel.latestStates.computeIfAbsent( | ||||
| 						device.serialNumber, | ||||
| 						k -> new LinkedList<>() | ||||
| @@ -302,8 +315,11 @@ public class Modeler implements Runnable { | ||||
| 				JsonObject state = record.payload.getAsJsonObject("state"); | ||||
| 				if (state != null) { | ||||
| 					try { | ||||
| 						State stateModel = gson.fromJson(state, State.class); | ||||
| 						List<State> latestStatesList = dataModel.latestStates | ||||
| 						StateInfo stateModel = | ||||
| 							gson.fromJson(state, StateInfo.class); | ||||
| 						stateModel.timestamp = record.timestampMs; | ||||
| 						List<StateInfo> latestStatesList = | ||||
| 							dataModel.latestStates | ||||
| 								.computeIfAbsent( | ||||
| 									record.serialNumber, | ||||
| 									k -> new LinkedList<>() | ||||
| @@ -376,9 +392,9 @@ public class Modeler implements Runnable { | ||||
| 		String serialNumber, | ||||
| 		DeviceCapabilities capabilities | ||||
| 	) { | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			serialNumber, | ||||
| 			capabilities.capabilities.getAsJsonObject("wifi") | ||||
| 			capabilities.capabilities.wifi | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| @@ -390,9 +406,10 @@ public class Modeler implements Runnable { | ||||
| 		UCentralApConfiguration config | ||||
| 	) { | ||||
| 		// Get old vs new radios info and store the new radios info | ||||
| 		JsonArray newRadioList = config.getRadioConfigList(); | ||||
| 		List<UCentralSchema.Radio> newRadioList = config.getRadioConfigList(); | ||||
| 		Set<String> newRadioBandsSet = config.getRadioBandsSet(newRadioList); | ||||
| 		JsonArray oldRadioList = dataModel.latestDeviceStatusRadios | ||||
| 		List<UCentralSchema.Radio> oldRadioList = | ||||
| 			dataModel.latestDeviceStatusRadios | ||||
| 				.put(serialNumber, newRadioList); | ||||
| 		Set<String> oldRadioBandsSet = config.getRadioBandsSet(oldRadioList); | ||||
|  | ||||
| @@ -448,7 +465,7 @@ public class Modeler implements Runnable { | ||||
| 			logger.debug("Removed some status entries from data model"); | ||||
| 		} | ||||
| 		if ( | ||||
| 			dataModel.latestDeviceCapabilities.entrySet() | ||||
| 			dataModel.latestDeviceCapabilitiesPhy.entrySet() | ||||
| 				.removeIf(e -> !isRRMEnabled(e.getKey())) | ||||
| 		) { | ||||
| 			logger.debug("Removed some capabilities entries from data model"); | ||||
|   | ||||
| @@ -19,8 +19,10 @@ import java.util.stream.Collectors; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
|  | ||||
| import com.facebook.openwifi.cloudsdk.models.ap.Capabilities; | ||||
| import com.facebook.openwifi.cloudsdk.AggregatedState; | ||||
| import com.facebook.openwifi.cloudsdk.WifiScanEntry; | ||||
| import com.facebook.openwifi.cloudsdk.StateInfo; | ||||
| import com.facebook.openwifi.cloudsdk.ies.HTOperation; | ||||
| import com.facebook.openwifi.cloudsdk.ies.VHTOperation; | ||||
| import com.facebook.openwifi.cloudsdk.models.ap.State; | ||||
| @@ -30,9 +32,6 @@ import com.facebook.openwifi.cloudsdk.models.ap.State.Interface.SSID.Association | ||||
| import com.facebook.openwifi.rrm.aggregators.Aggregator; | ||||
| import com.facebook.openwifi.rrm.aggregators.MeanAggregator; | ||||
| import com.facebook.openwifi.rrm.modules.Modeler.DataModel; | ||||
| import com.google.gson.JsonArray; | ||||
| import com.google.gson.JsonElement; | ||||
| import com.google.gson.JsonObject; | ||||
|  | ||||
| /** | ||||
|  * Modeler utilities. | ||||
| @@ -406,22 +405,13 @@ public class ModelerUtils { | ||||
| 	 */ | ||||
| 	static void addStateToAggregation( | ||||
| 		Map<String, List<AggregatedState>> bssidToAggregatedStates, | ||||
| 		State state | ||||
| 		StateInfo state | ||||
| 	) { | ||||
| 		for (Interface stateInterface : state.interfaces) { | ||||
| 			if (stateInterface.ssids == null) { | ||||
| 				continue; | ||||
| 			} | ||||
| 			for (SSID ssid : stateInterface.ssids) { | ||||
| 				Map<String, Integer> radioInfo = new HashMap<>(); | ||||
| 				radioInfo.put("channel", ssid.radio.get("channel").getAsInt()); | ||||
| 				radioInfo.put( | ||||
| 					"channel_width", | ||||
| 					ssid.radio.get("channel_width").getAsInt() | ||||
| 				); | ||||
| 				radioInfo | ||||
| 					.put("tx_power", ssid.radio.get("tx_power").getAsInt()); | ||||
|  | ||||
| 				for (Association association : ssid.associations) { | ||||
| 					if (association == null) { | ||||
| 						continue; | ||||
| @@ -434,7 +424,12 @@ public class ModelerUtils { | ||||
| 						bssidToAggregatedStates | ||||
| 							.computeIfAbsent(key, k -> new ArrayList<>()); | ||||
| 					AggregatedState aggState = | ||||
| 						new AggregatedState(association, radioInfo); | ||||
| 						new AggregatedState( | ||||
| 							association, | ||||
| 							ssid.counters, | ||||
| 							ssid.radio, | ||||
| 							state.timestamp | ||||
| 						); | ||||
|  | ||||
| 					/** | ||||
| 					 * Indicate if the aggState can be merged into some old AggregatedState. | ||||
| @@ -491,11 +486,11 @@ public class ModelerUtils { | ||||
| 			new HashMap<>(); | ||||
|  | ||||
| 		for ( | ||||
| 			Map.Entry<String, List<State>> deviceToStateList : dataModel.latestStates | ||||
| 			Map.Entry<String, List<StateInfo>> deviceToStateList : dataModel.latestStates | ||||
| 				.entrySet() | ||||
| 		) { | ||||
| 			String serialNumber = deviceToStateList.getKey(); | ||||
| 			List<State> states = deviceToStateList.getValue(); | ||||
| 			List<StateInfo> states = deviceToStateList.getValue(); | ||||
|  | ||||
| 			if (states.isEmpty()) { | ||||
| 				continue; | ||||
| @@ -514,7 +509,7 @@ public class ModelerUtils { | ||||
| 				aggregatedStates | ||||
| 					.computeIfAbsent(serialNumber, k -> new HashMap<>()); | ||||
|  | ||||
| 			for (State state : states) { | ||||
| 			for (StateInfo state : states) { | ||||
| 				if (refTimeMs - state.unit.localtime > obsoletionPeriodMs) { | ||||
| 					// discard obsolete entries | ||||
| 					break; | ||||
| @@ -532,15 +527,16 @@ public class ModelerUtils { | ||||
| 	 * @param latestStates list of latest States per device | ||||
| 	 * @return map from device String to latest State | ||||
| 	 */ | ||||
| 	public static Map<String, State> getLatestState( | ||||
| 		Map<String, List<State>> latestStates | ||||
| 	public static Map<String, StateInfo> getLatestState( | ||||
| 		Map<String, List<StateInfo>> latestStates | ||||
| 	) { | ||||
| 		Map<String, State> latestState = new ConcurrentHashMap<>(); | ||||
| 		Map<String, StateInfo> latestState = new ConcurrentHashMap<>(); | ||||
| 		for ( | ||||
| 			Map.Entry<String, List<State>> stateEntry : latestStates.entrySet() | ||||
| 			Map.Entry<String, List<StateInfo>> stateEntry : latestStates | ||||
| 				.entrySet() | ||||
| 		) { | ||||
| 			String key = stateEntry.getKey(); | ||||
| 			List<State> value = stateEntry.getValue(); | ||||
| 			List<StateInfo> value = stateEntry.getValue(); | ||||
| 			if (value.isEmpty()) { | ||||
| 				latestState.put(key, null); | ||||
| 			} else { | ||||
| @@ -562,24 +558,19 @@ public class ModelerUtils { | ||||
| 	/** Return the radio's band, or null if band cannot be found */ | ||||
| 	public static String getBand( | ||||
| 		State.Radio radio, | ||||
| 		JsonObject deviceCapability | ||||
| 		Map<String, Capabilities.Phy> capabilityPhy | ||||
| 	) { | ||||
| 		if (radio.phy == null) { | ||||
| 			return null; | ||||
| 		} | ||||
| 		JsonElement radioCapabilityElement = deviceCapability.get(radio.phy); | ||||
| 		if (radioCapabilityElement == null) { | ||||
| 		Capabilities.Phy phy = capabilityPhy.get(radio.phy); | ||||
| 		if (phy == null) { | ||||
| 			return null; | ||||
| 		} | ||||
| 		JsonObject radioCapability = radioCapabilityElement.getAsJsonObject(); | ||||
| 		JsonElement bandsElement = radioCapability.get("band"); | ||||
| 		if (bandsElement == null) { | ||||
| 		String[] bands = phy.band; | ||||
| 		if (bands == null || bands.length == 0) { | ||||
| 			return null; | ||||
| 		} | ||||
| 		JsonArray bands = bandsElement.getAsJsonArray(); | ||||
| 		if (bands.isEmpty()) { | ||||
| 			return null; | ||||
| 		} | ||||
| 		return bands.get(0).getAsString(); | ||||
| 		return bands[0]; | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,12 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * RRM service modules. | ||||
|  */ | ||||
| package com.facebook.openwifi.rrm.modules; | ||||
| @@ -18,6 +18,7 @@ import java.util.Map; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
|  | ||||
| import com.facebook.openwifi.cloudsdk.models.ap.Capabilities; | ||||
| import com.facebook.openwifi.cloudsdk.UCentralConstants; | ||||
| import com.facebook.openwifi.cloudsdk.UCentralUtils; | ||||
| import com.facebook.openwifi.cloudsdk.WifiScanEntry; | ||||
| @@ -29,7 +30,6 @@ import com.facebook.openwifi.rrm.DeviceDataManager; | ||||
| import com.facebook.openwifi.rrm.modules.ConfigManager; | ||||
| import com.facebook.openwifi.rrm.modules.Modeler.DataModel; | ||||
| import com.facebook.openwifi.rrm.modules.ModelerUtils; | ||||
| import com.google.gson.JsonObject; | ||||
|  | ||||
| /** | ||||
|  * Channel optimizer base class. | ||||
| @@ -142,7 +142,7 @@ public abstract class ChannelOptimizer { | ||||
| 			.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber)); | ||||
| 		this.model.latestDeviceStatusRadios.keySet() | ||||
| 			.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber)); | ||||
| 		this.model.latestDeviceCapabilities.keySet() | ||||
| 		this.model.latestDeviceCapabilitiesPhy.keySet() | ||||
| 			.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber)); | ||||
| 	} | ||||
|  | ||||
| @@ -349,7 +349,7 @@ public abstract class ChannelOptimizer { | ||||
| 	 * @param band the operational band (e.g., "2G") | ||||
| 	 * @param serialNumber the device's serial number | ||||
| 	 * @param state the latest state of all the devices | ||||
| 	 * @param latestDeviceCapabilities latest device capabilities | ||||
| 	 * @param latestDeviceCapabilitiesPhy latest device phy from capabilities | ||||
| 	 * @return the current channel and channel width (MHz) of the device in the | ||||
| 	 * given band; returns a current channel of 0 if no channel in the given | ||||
| 	 * band is found. | ||||
| @@ -358,7 +358,7 @@ public abstract class ChannelOptimizer { | ||||
| 		String band, | ||||
| 		String serialNumber, | ||||
| 		State state, | ||||
| 		Map<String, JsonObject> latestDeviceCapabilities | ||||
| 		Map<String, Map<String, Capabilities.Phy>> latestDeviceCapabilitiesPhy | ||||
| 	) { | ||||
| 		int currentChannel = 0; | ||||
| 		int currentChannelWidth = MIN_CHANNEL_WIDTH; | ||||
| @@ -370,14 +370,14 @@ public abstract class ChannelOptimizer { | ||||
| 		) { | ||||
| 			State.Radio radio = state.radios[radioIndex]; | ||||
| 			// check if radio is in band of interest | ||||
| 			JsonObject deviceCapability = | ||||
| 				latestDeviceCapabilities.get(serialNumber); | ||||
| 			if (deviceCapability == null) { | ||||
| 			Map<String, Capabilities.Phy> capabilitiesPhy = | ||||
| 				latestDeviceCapabilitiesPhy.get(serialNumber); | ||||
| 			if (capabilitiesPhy == null) { | ||||
| 				continue; | ||||
| 			} | ||||
| 			final String radioBand = ModelerUtils.getBand( | ||||
| 				radio, | ||||
| 				deviceCapability | ||||
| 				capabilitiesPhy | ||||
| 			); | ||||
| 			if (radioBand == null || !radioBand.equals(band)) { | ||||
| 				continue; | ||||
|   | ||||
| @@ -337,11 +337,11 @@ public class LeastUsedChannelOptimizer extends ChannelOptimizer { | ||||
| 		Map<String, Map<String, List<Integer>>> deviceAvailableChannels = | ||||
| 			UCentralUtils.getDeviceAvailableChannels( | ||||
| 				model.latestDeviceStatusRadios, | ||||
| 				model.latestDeviceCapabilities, | ||||
| 				model.latestDeviceCapabilitiesPhy, | ||||
| 				UCentralUtils.AVAILABLE_CHANNELS_BAND | ||||
| 			); | ||||
|  | ||||
| 		Map<String, State> latestState = | ||||
| 		Map<String, ? extends State> latestState = | ||||
| 			ModelerUtils.getLatestState(model.latestStates); | ||||
| 		Map<String, String> bssidsMap = | ||||
| 			UCentralUtils.getBssidsMap(latestState); | ||||
| @@ -397,7 +397,7 @@ public class LeastUsedChannelOptimizer extends ChannelOptimizer { | ||||
| 						band, | ||||
| 						serialNumber, | ||||
| 						state, | ||||
| 						model.latestDeviceCapabilities | ||||
| 						model.latestDeviceCapabilitiesPhy | ||||
| 					); | ||||
| 				int currentChannel = currentChannelInfo[0]; | ||||
| 				// Filter out APs if the radios in the state do not contain a | ||||
|   | ||||
| @@ -125,11 +125,11 @@ public class RandomChannelInitializer extends ChannelOptimizer { | ||||
| 		Map<String, Map<String, List<Integer>>> deviceAvailableChannels = | ||||
| 			UCentralUtils.getDeviceAvailableChannels( | ||||
| 				model.latestDeviceStatusRadios, | ||||
| 				model.latestDeviceCapabilities, | ||||
| 				model.latestDeviceCapabilitiesPhy, | ||||
| 				UCentralUtils.AVAILABLE_CHANNELS_BAND | ||||
| 			); | ||||
|  | ||||
| 		Map<String, State> latestState = | ||||
| 		Map<String, ? extends State> latestState = | ||||
| 			ModelerUtils.getLatestState(model.latestStates); | ||||
| 		Map<String, String> bssidsMap = | ||||
| 			UCentralUtils.getBssidsMap(latestState); | ||||
| @@ -208,7 +208,7 @@ public class RandomChannelInitializer extends ChannelOptimizer { | ||||
| 						band, | ||||
| 						serialNumber, | ||||
| 						state, | ||||
| 						model.latestDeviceCapabilities | ||||
| 						model.latestDeviceCapabilitiesPhy | ||||
| 					); | ||||
| 				int currentChannel = currentChannelInfo[0]; | ||||
| 				int currentChannelWidth = currentChannelInfo[1]; | ||||
|   | ||||
| @@ -0,0 +1,12 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * Channel assignment algorithms. | ||||
|  */ | ||||
| package com.facebook.openwifi.rrm.optimizers.channel; | ||||
| @@ -0,0 +1,161 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| package com.facebook.openwifi.rrm.optimizers.clientsteering; | ||||
|  | ||||
| import java.util.Map; | ||||
|  | ||||
| import com.facebook.openwifi.rrm.DeviceConfig; | ||||
| import com.facebook.openwifi.rrm.DeviceDataManager; | ||||
| import com.facebook.openwifi.rrm.modules.Modeler.DataModel; | ||||
|  | ||||
| /** Client steering base class */ | ||||
| public abstract class ClientSteeringOptimizer { | ||||
| 	// TODO call upon triggers, not only via one-off or period runs | ||||
|  | ||||
| 	/** Represents client steering actions an AP can take */ | ||||
| 	public static enum CLIENT_STEERING_ACTIONS { | ||||
| 		/** Steer from 2G to 5G/6G */ | ||||
| 		STEER_UP, | ||||
| 		/** Steer from 5G/6G to 2G */ | ||||
| 		STEER_DOWN, | ||||
| 		/** Deauthenticate client */ | ||||
| 		DEAUTHENTICATE | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * 802.11 BTM reason codes (ex. for deauth). | ||||
| 	 * | ||||
| 	 * See IEEE Std 802.11-2016, 9.4.1.7, Table 9-45. | ||||
| 	 */ | ||||
| 	public static class BTMReasonCode { | ||||
| 		private BTMReasonCode() {} | ||||
|  | ||||
| 		public static final int UNSPECIFIED = 1; | ||||
| 		public static final int PREV_AUTH_NOT_VALID = 2; | ||||
| 		public static final int DEAUTH_LEAVING = 3; | ||||
| 		public static final int DISASSOC_DUE_TO_INACTIVITY = 4; | ||||
| 		public static final int DISASSOC_AP_BUSY = 5; | ||||
| 		public static final int CLASS2_FRAME_FROM_NONAUTH_STA = 6; | ||||
| 		public static final int CLASS3_FRAME_FROM_NONASSOC_STA = 7; | ||||
| 		public static final int DISASSOC_STA_HAS_LEFT = 8; | ||||
| 		public static final int STA_REQ_ASSOC_WITHOUT_AUTH = 9; | ||||
| 		public static final int PWR_CAPABILITY_NOT_VALID = 10; | ||||
| 		public static final int SUPPORTED_CHANNEL_NOT_VALID = 11; | ||||
| 		public static final int INVALID_IE = 13; | ||||
| 		public static final int MICHAEL_MIC_FAILURE = 14; | ||||
| 		public static final int FOURWAY_HANDSHAKE_TIMEOUT = 15; | ||||
| 		public static final int GROUP_KEY_UPDATE_TIMEOUT = 16; | ||||
| 		public static final int IE_IN_4WAY_DIFFERS = 17; | ||||
| 		public static final int GROUP_CIPHER_NOT_VALID = 18; | ||||
| 		public static final int PAIRWISE_CIPHER_NOT_VALID = 19; | ||||
| 		public static final int AKMP_NOT_VALID = 20; | ||||
| 		public static final int UNSUPPORTED_RSN_IE_VERSION = 21; | ||||
| 		public static final int INVALID_RSN_IE_CAPAB = 22; | ||||
| 		public static final int IEEE_802_1X_AUTH_FAILED = 23; | ||||
| 		public static final int CIPHER_SUITE_REJECTED = 24; | ||||
| 		public static final int TDLS_TEARDOWN_UNREACHABLE = 25; | ||||
| 		public static final int TDLS_TEARDOWN_UNSPECIFIED = 26; | ||||
| 		public static final int DISASSOC_LOW_ACK = 34; | ||||
| 		public static final int MESH_PEERING_CANCELLED = 52; | ||||
| 		public static final int MESH_MAX_PEERS = 53; | ||||
| 		public static final int MESH_CONFIG_POLICY_VIOLATION = 54; | ||||
| 		public static final int MESH_CLOSE_RCVD = 55; | ||||
| 		public static final int MESH_MAX_RETRIES = 56; | ||||
| 		public static final int MESH_CONFIRM_TIMEOUT = 57; | ||||
| 		public static final int MESH_INVALID_GTK = 58; | ||||
| 		public static final int MESH_INCONSISTENT_PARAMS = 59; | ||||
| 		public static final int MESH_INVALID_SECURITY_CAP = 60; | ||||
| 		public static final int MESH_PATH_ERROR_NO_PROXY_INFO = 61; | ||||
| 		public static final int MESH_PATH_ERROR_NO_FORWARDING_INFO = 62; | ||||
| 		public static final int MESH_PATH_ERROR_DEST_UNREACHABLE = 63; | ||||
| 		public static final int MAC_ADDRESS_ALREADY_EXISTS_IN_MBSS = 64; | ||||
| 		public static final int MESH_CHANNEL_SWITCH_REGULATORY_REQ = 65; | ||||
| 		public static final int MESH_CHANNEL_SWITCH_UNSPECIFIED = 66; | ||||
| 	} | ||||
|  | ||||
| 	/** The input data model. */ | ||||
| 	protected final DataModel model; | ||||
| 	/** The RF zone. */ | ||||
| 	protected final String zone; | ||||
| 	/** The device configs within {@link #zone}, keyed on serial number. */ | ||||
| 	protected final Map<String, DeviceConfig> deviceConfigs; | ||||
| 	/** Client steering state */ | ||||
| 	protected final ClientSteeringState clientSteeringState; | ||||
|  | ||||
| 	/** Constructor */ | ||||
| 	public ClientSteeringOptimizer( | ||||
| 		DataModel model, | ||||
| 		String zone, | ||||
| 		DeviceDataManager deviceDataManager, | ||||
| 		ClientSteeringState clientSteeringState | ||||
| 	) { | ||||
| 		this.model = model; | ||||
| 		this.zone = zone; | ||||
| 		this.deviceConfigs = deviceDataManager.getAllDeviceConfigs(zone); | ||||
|  | ||||
| 		this.clientSteeringState = clientSteeringState; | ||||
|  | ||||
| 		// Remove model entries not in the given zone | ||||
| 		this.model.latestWifiScans.keySet() | ||||
| 			.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber)); | ||||
| 		this.model.latestStates.keySet() | ||||
| 			.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber)); | ||||
| 		this.model.latestDeviceStatusRadios.keySet() | ||||
| 			.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber)); | ||||
| 		this.model.latestDeviceCapabilitiesPhy.keySet() | ||||
| 			.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber)); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Compute map from AP serial number to client MAC to client steering | ||||
| 	 * action. | ||||
| 	 */ | ||||
| 	public abstract Map<String, Map<String, String>> computeApClientActionMap( | ||||
| 		boolean dryRun | ||||
| 	); | ||||
|  | ||||
| 	/** | ||||
| 	 * Steer clients (steer up, steer down, and deauthenticate). | ||||
| 	 * | ||||
| 	 * @param apClientActionMap the map from AP serial number to client MAC to | ||||
| 	 *                          action to take | ||||
| 	 */ | ||||
| 	public void steer( | ||||
| 		Map<String, Map<String, String>> apClientActionMap | ||||
| 	) { | ||||
| 		// FIXME implement this | ||||
| 		// | ||||
| 		// TODO: input must also contain AP interface for each client (needed in hostapd commands below) | ||||
| 		// | ||||
| 		// NOTE: 802.11k/v features must first be enabled on APs: | ||||
| 		//         ubus call hostapd.<iface> bss_mgmt_enable \ | ||||
| 		//         '{"neighbor_report": true, "beacon_report": true, "link_measurements": true, "bss_transition": true}' | ||||
| 		// | ||||
| 		// Actions: | ||||
| 		// | ||||
| 		// - Kick/Deauth: | ||||
| 		//     ubus call hostapd.<iface> del_client \ | ||||
| 		//     '{"addr": "<client_mac>", "reason": 5, "deauth": true}' | ||||
| 		//   Where "reason" is a code in BTMReasonCode | ||||
| 		// | ||||
| 		// - Steer: | ||||
| 		//     ubus call hostapd.<iface> bss_transition_request \ | ||||
| 		//     '{"addr": "<client_mac>", "disassociation_imminent": false, "disassociation_timer": 0, "validity_period": 30, "neighbors": ["<hex>"], "abridged": 1}' | ||||
| 		//   Where "neighbors" list element = a hex identifier (array index 2 in command below) - MUST fetch per interface per AP | ||||
| 		//     ubus call hostapd.<iface> rrm_nr_get_own | ||||
| 		//   TODO: also send Multi Band Operation (MBO) code ("mbo_reason") for 802.11ax clients | ||||
| 	} | ||||
|  | ||||
| 	// TODO Issue 802.11k RRM Beacon Measurement Requests periodically | ||||
| 	// 1. Enable 802.11k/v features on the AP ("bss_mgmt_enable" hostapd command) | ||||
| 	// 2. Send request to client | ||||
| 	//      ubus call hostapd.wlan0-1 rrm_beacon_req '{"addr": "<client_mac>", "channel": <number>, "mode": 1, "op_class": 128, "duration": 100}' | ||||
| 	// 3. Must be subscribed to hostapd 'beacon-report' event on AP to receive reply ("BEACON-RESP-RX") | ||||
| 	//      ubus subscribe hostapd.<iface> | ||||
| } | ||||
| @@ -0,0 +1,80 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| package com.facebook.openwifi.rrm.optimizers.clientsteering; | ||||
|  | ||||
| import java.util.HashMap; | ||||
| import java.util.Map; | ||||
| import java.util.concurrent.ConcurrentHashMap; | ||||
| import java.util.concurrent.ConcurrentMap; | ||||
|  | ||||
| /** Class to manage global client steering state */ | ||||
| public class ClientSteeringState { | ||||
| 	/** | ||||
| 	 * Map from AP serial number to client MAC to time (JVM monotonic time in | ||||
| 	 * ns) of the latest attempted client steering action. The {@code Long} | ||||
| 	 * values are never null. | ||||
| 	 */ | ||||
| 	private ConcurrentMap<String, Map<String, Long>> apClientLastAttempt = | ||||
| 		new ConcurrentHashMap<>(); | ||||
|  | ||||
| 	/** | ||||
| 	 * Register a client steering attempt for the given AP and station at the | ||||
| 	 * given time if there is no previous registered attempt or more than the | ||||
| 	 * given backoff time has passed since the registration time of the last | ||||
| 	 * attempt and the current time. Note that only registration times are | ||||
| 	 * checked and/or entered, and nothing is executed here. Return true if the | ||||
| 	 * attempt was registered; false otherwise. The attempt is not registered if | ||||
| 	 * this run is specified as a dry run. | ||||
| 	 * <p> | ||||
| 	 * The backoff time must be non-negative. The backoff time window is | ||||
| 	 * "exclusive" - e.g., if the backoff time is X ns, and the current time is | ||||
| 	 * exactly X ns after the last attempt, the backoff is considered expired. | ||||
| 	 * <p> | ||||
| 	 * Note that if there was a previous attempt for the given AP and station, | ||||
| 	 * the current time must not be before the last attempt. | ||||
| 	 * | ||||
| 	 * @param apSerialNumber AP serial number | ||||
| 	 * @param station client MAC | ||||
| 	 * @param currentTimeNs JVM monotonic time in ns | ||||
| 	 * @param backoffTimeNs non-negative backoff time (ns) | ||||
| 	 * @param dryRun if set, do not apply changes | ||||
| 	 * @return true if client steering attempt was registered; false otherwise | ||||
| 	 */ | ||||
| 	public boolean registerIfBackoffExpired( | ||||
| 		String apSerialNumber, | ||||
| 		String station, | ||||
| 		long currentTimeNs, | ||||
| 		long backoffTimeNs, | ||||
| 		boolean dryRun | ||||
| 	) { | ||||
| 		if (backoffTimeNs < 0) { | ||||
| 			throw new IllegalArgumentException( | ||||
| 				"Backoff time must be non-negative." | ||||
| 			); | ||||
| 		} | ||||
| 		// get last attempt | ||||
| 		Map<String, Long> clientLastAttempt = apClientLastAttempt | ||||
| 			.computeIfAbsent(apSerialNumber, k -> new HashMap<>()); | ||||
| 		synchronized (clientLastAttempt) { | ||||
| 			Long lastAttempt = clientLastAttempt.get(station); | ||||
| 			// check if backoff expired | ||||
| 			if ( | ||||
| 				lastAttempt != null && | ||||
| 					currentTimeNs - lastAttempt < backoffTimeNs | ||||
| 			) { | ||||
| 				return false; | ||||
| 			} | ||||
| 			// register attempt | ||||
| 			if (!dryRun) { | ||||
| 				clientLastAttempt.put(station, currentTimeNs); | ||||
| 			} | ||||
| 		} | ||||
| 		return true; | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,314 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| package com.facebook.openwifi.rrm.optimizers.clientsteering; | ||||
|  | ||||
| import java.util.HashMap; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
|  | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
|  | ||||
| import com.facebook.openwifi.cloudsdk.StateInfo; | ||||
| import com.facebook.openwifi.cloudsdk.UCentralConstants; | ||||
| import com.facebook.openwifi.cloudsdk.models.ap.Capabilities; | ||||
| import com.facebook.openwifi.cloudsdk.models.ap.State; | ||||
| import com.facebook.openwifi.rrm.DeviceDataManager; | ||||
| import com.facebook.openwifi.rrm.modules.Modeler.DataModel; | ||||
| import com.facebook.openwifi.rrm.modules.ModelerUtils; | ||||
| import com.google.gson.Gson; | ||||
|  | ||||
| /** | ||||
|  * Implements simple band steering for each AP separately | ||||
|  * <p> | ||||
|  * 2G clients below a specified RSSI threshold are deauthenticated. 2G clients | ||||
|  * above a specified RSSI threshold are asked to move to either 5G or 6G. 5G and | ||||
|  * 6G clients below a configurable RSSI threshold are asked to move to 2G. | ||||
|  */ | ||||
| public class SingleAPBandSteering extends ClientSteeringOptimizer { | ||||
| 	private static final Logger logger = | ||||
| 		LoggerFactory.getLogger(SingleAPBandSteering.class); | ||||
|  | ||||
| 	/** The RRM algorithm ID. */ | ||||
| 	public static final String ALGORITHM_ID = "band"; | ||||
|  | ||||
| 	/** The Gson instance. */ | ||||
| 	private static final Gson gson = new Gson(); | ||||
|  | ||||
| 	/** | ||||
| 	 * RSSI (dBm) below which a client on 2G should be disconnected using | ||||
| 	 * deauthentication. | ||||
| 	 */ | ||||
| 	public static final short DEFAULT_MIN_RSSI_2G = -87; | ||||
| 	/** | ||||
| 	 * RSSI (dBm) above which a client on 2G should be requested to move to | ||||
| 	 * 5G/6G | ||||
| 	 */ | ||||
| 	public static final short DEFAULT_MAX_RSSI_2G = -67; | ||||
| 	/** | ||||
| 	 * RSSI (dBm) below which a client on 5G/6G should be requested to move to | ||||
| 	 * 2G | ||||
| 	 */ | ||||
| 	public static final short DEFAULT_MIN_RSSI_NON_2G = -82; | ||||
| 	/** Default backoff time (ns) for all APs and radios */ | ||||
| 	public static final long DEFAULT_BACKOFF_TIME_NS = 300_000_000_000L; // 5 min | ||||
|  | ||||
| 	/** RSSI below which 2G clients are deauthenticated */ | ||||
| 	private final short minRssi2G; | ||||
| 	/** RSSI above which 2G clients are asked to move to 5G or 6G */ | ||||
| 	private final short maxRssi2G; | ||||
| 	/** RSSI below which 5G and 6G clients are asked to move to 2G */ | ||||
| 	private final short minRssiNon2G; | ||||
| 	/** Backoff time (ns) for all APs and radios */ | ||||
| 	private final long backoffTimeNs; | ||||
|  | ||||
| 	/** Make a SingleAPBandSteering object with the given arguments */ | ||||
| 	public static SingleAPBandSteering makeWithArgs( | ||||
| 		DataModel model, | ||||
| 		String zone, | ||||
| 		DeviceDataManager deviceDataManager, | ||||
| 		ClientSteeringState clientSteeringState, | ||||
| 		Map<String, String> args | ||||
| 	) { | ||||
| 		short minRssi2G = DEFAULT_MIN_RSSI_2G; | ||||
| 		short maxRssi2G = DEFAULT_MAX_RSSI_2G; | ||||
| 		short minRssiNon2G = DEFAULT_MIN_RSSI_NON_2G; | ||||
| 		long backoffTimeNs = DEFAULT_BACKOFF_TIME_NS; | ||||
|  | ||||
| 		String arg; | ||||
| 		if ((arg = args.get("minRssi2G")) != null) { | ||||
| 			minRssi2G = Short.parseShort(arg); | ||||
| 		} | ||||
| 		if ((arg = args.get("maxRssi2G")) != null) { | ||||
| 			maxRssi2G = Short.parseShort(arg); | ||||
| 		} | ||||
| 		if ((arg = args.get("minRssiNon2G")) != null) { | ||||
| 			minRssiNon2G = Short.parseShort(arg); | ||||
| 		} | ||||
| 		if ((arg = args.get("backoffTimeSec")) != null) { | ||||
| 			backoffTimeNs = Long.parseLong(arg) * 1_000_000_000L; | ||||
| 		} | ||||
|  | ||||
| 		return new SingleAPBandSteering( | ||||
| 			model, | ||||
| 			zone, | ||||
| 			deviceDataManager, | ||||
| 			clientSteeringState, | ||||
| 			minRssi2G, | ||||
| 			maxRssi2G, | ||||
| 			minRssiNon2G, | ||||
| 			backoffTimeNs | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	/** Constructor */ | ||||
| 	public SingleAPBandSteering( | ||||
| 		DataModel model, | ||||
| 		String zone, | ||||
| 		DeviceDataManager deviceDataManager, | ||||
| 		ClientSteeringState clientSteeringState, | ||||
| 		short minRssi2G, | ||||
| 		short maxRssi2G, | ||||
| 		short minRssiNon2G, | ||||
| 		long backoffTimeNs | ||||
| 	) { | ||||
| 		super(model, zone, deviceDataManager, clientSteeringState); | ||||
| 		this.minRssi2G = minRssi2G; | ||||
| 		this.maxRssi2G = maxRssi2G; | ||||
| 		this.minRssiNon2G = minRssiNon2G; | ||||
| 		this.backoffTimeNs = backoffTimeNs; | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public Map<String, Map<String, String>> computeApClientActionMap( | ||||
| 		boolean dryRun | ||||
| 	) { | ||||
| 		Map<String, Map<String, String>> apClientActionMap = new HashMap<>(); | ||||
| 		// iterate through every AP | ||||
| 		for ( | ||||
| 			Map.Entry<String, List<StateInfo>> entry : model.latestStates | ||||
| 				.entrySet() | ||||
| 		) { | ||||
| 			// get the latest state | ||||
| 			// TODO window size (look at multiple states) | ||||
| 			// TODO window percent (% of samples that must violate thresholds) | ||||
| 			// TODO also check wifiscan IEs to see if 11k beacon requests are supported/enabled | ||||
| 			//      (RMEnabledCapabilities.beaconActiveMeasurementCapabilityEnabled) | ||||
| 			List<? extends State> states = entry.getValue(); | ||||
| 			if (states == null || states.isEmpty()) { | ||||
| 				continue; | ||||
| 			} | ||||
| 			final String serialNumber = entry.getKey(); | ||||
| 			final State state = states.get(states.size() - 1); | ||||
| 			// iterate through every radio and every connected client | ||||
| 			if (state.interfaces == null || state.interfaces.length == 0) { | ||||
| 				continue; | ||||
| 			} | ||||
| 			final long currentTimeNs = System.nanoTime(); | ||||
| 			for (State.Interface iface : state.interfaces) { | ||||
| 				if (iface.ssids == null || iface.ssids.length == 0) { | ||||
| 					continue; | ||||
| 				} | ||||
| 				for (State.Interface.SSID ssid : iface.ssids) { | ||||
| 					if ( | ||||
| 						ssid.associations == null || | ||||
| 							ssid.associations.length == 0 | ||||
| 					) { | ||||
| 						continue; | ||||
| 					} | ||||
| 					final State.Radio radio = gson.fromJson( | ||||
| 						ssid.radio, | ||||
| 						State.Radio.class | ||||
| 					); | ||||
| 					// get band for this radio/ssid | ||||
| 					Map<String, Capabilities.Phy> capabilitiesPhy = | ||||
| 						model.latestDeviceCapabilitiesPhy | ||||
| 							.get(serialNumber); | ||||
| 					if (capabilitiesPhy == null) { | ||||
| 						continue; | ||||
| 					} | ||||
| 					final String band = ModelerUtils.getBand( | ||||
| 						radio, | ||||
| 						capabilitiesPhy | ||||
| 					); | ||||
| 					if (band == null) { | ||||
| 						continue; | ||||
| 					} | ||||
| 					// decide steering action (if any) for each client | ||||
| 					for ( | ||||
| 						State.Interface.SSID.Association assoc : ssid.associations | ||||
| 					) { | ||||
| 						maybeAddApClientActionEntry( | ||||
| 							assoc, | ||||
| 							band, | ||||
| 							serialNumber, | ||||
| 							currentTimeNs, | ||||
| 							apClientActionMap, | ||||
| 							dryRun | ||||
| 						); | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		return apClientActionMap; | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * If a client steering action is desired, add an appropriate entry to the | ||||
| 	 * apClientActionMap, unless this run is marked as a dry run. | ||||
| 	 * | ||||
| 	 * @param assoc association between AP radio and client | ||||
| 	 * @param band band (e.g., "2G") | ||||
| 	 * @param serialNumber AP serial number | ||||
| 	 * @param currentTimeNs JVM monotonic time in ns | ||||
| 	 * @param dryRun if set, do not apply changes | ||||
| 	 * @param apClientActionMap map from AP serial number to client MAC to client | ||||
| 	 *                          steering action name ({@link ClientSteeringOptimizer.CLIENT_STEERING_ACTIONS}) | ||||
| 	 */ | ||||
| 	private void maybeAddApClientActionEntry( | ||||
| 		State.Interface.SSID.Association assoc, | ||||
| 		String band, | ||||
| 		String serialNumber, | ||||
| 		long currentTimeNs, | ||||
| 		Map<String, Map<String, String>> apClientActionMap, | ||||
| 		boolean dryRun | ||||
| 	) { | ||||
| 		// decide whether to do any band steering | ||||
| 		// TODO check which bands AP & client can use (see 11k) | ||||
| 		if (UCentralConstants.BAND_2G.equals(band)) { | ||||
| 			if (assoc.rssi < minRssi2G) { | ||||
| 				if ( | ||||
| 					clientSteeringState | ||||
| 						.registerIfBackoffExpired( | ||||
| 							serialNumber, | ||||
| 							assoc.station, | ||||
| 							currentTimeNs, | ||||
| 							backoffTimeNs, | ||||
| 							dryRun | ||||
| 						) | ||||
| 				) { | ||||
| 					logger.debug( | ||||
| 						"Planning to deauthenticate client {} on AP {}", | ||||
| 						assoc.station, | ||||
| 						serialNumber | ||||
| 					); | ||||
| 					apClientActionMap | ||||
| 						.computeIfAbsent( | ||||
| 							serialNumber, | ||||
| 							k -> new HashMap<>() | ||||
| 						) | ||||
| 						.put( | ||||
| 							assoc.station, | ||||
| 							CLIENT_STEERING_ACTIONS.DEAUTHENTICATE | ||||
| 								.name() | ||||
| 						); | ||||
| 				} | ||||
| 			} else if (assoc.rssi > maxRssi2G) { | ||||
| 				if ( | ||||
| 					clientSteeringState | ||||
| 						.registerIfBackoffExpired( | ||||
| 							serialNumber, | ||||
| 							assoc.station, | ||||
| 							currentTimeNs, | ||||
| 							backoffTimeNs, | ||||
| 							dryRun | ||||
| 						) | ||||
| 				) { | ||||
| 					logger.debug( | ||||
| 						"Planning to request client {} on AP {} to move to 5G or 6G", | ||||
| 						assoc.station, | ||||
| 						serialNumber | ||||
| 					); | ||||
| 					apClientActionMap | ||||
| 						.computeIfAbsent( | ||||
| 							serialNumber, | ||||
| 							k -> new HashMap<>() | ||||
| 						) | ||||
| 						.put( | ||||
| 							assoc.station, | ||||
| 							CLIENT_STEERING_ACTIONS.STEER_UP | ||||
| 								.name() | ||||
| 						); | ||||
| 				} | ||||
| 			} | ||||
| 			// otherwise, do nothing | ||||
| 		} else { | ||||
| 			// treat 5G and 6G clients the same way | ||||
| 			if (assoc.rssi < minRssiNon2G) { | ||||
| 				if ( | ||||
| 					clientSteeringState | ||||
| 						.registerIfBackoffExpired( | ||||
| 							serialNumber, | ||||
| 							assoc.station, | ||||
| 							currentTimeNs, | ||||
| 							backoffTimeNs, | ||||
| 							dryRun | ||||
| 						) | ||||
| 				) { | ||||
| 					logger.debug( | ||||
| 						"Planning to request client {} on AP {} to move to 2G", | ||||
| 						assoc.station, | ||||
| 						serialNumber | ||||
| 					); | ||||
| 					apClientActionMap | ||||
| 						.computeIfAbsent( | ||||
| 							serialNumber, | ||||
| 							k -> new HashMap<>() | ||||
| 						) | ||||
| 						.put( | ||||
| 							assoc.station, | ||||
| 							CLIENT_STEERING_ACTIONS.STEER_DOWN | ||||
| 								.name() | ||||
| 						); | ||||
| 				} | ||||
| 			} | ||||
| 			// otherwise, do nothing | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,12 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * Client steering algorithms. | ||||
|  */ | ||||
| package com.facebook.openwifi.rrm.optimizers.clientsteering; | ||||
| @@ -0,0 +1,14 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * RRM algorithm implementations. | ||||
|  * | ||||
|  * @see com.facebook.openwifi.rrm.RRMAlgorithm | ||||
|  */ | ||||
| package com.facebook.openwifi.rrm.optimizers; | ||||
| @@ -18,9 +18,9 @@ import java.util.stream.Collectors; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
|  | ||||
| import com.facebook.openwifi.cloudsdk.models.ap.State; | ||||
| import com.facebook.openwifi.rrm.DeviceConfig; | ||||
| import com.facebook.openwifi.rrm.DeviceDataManager; | ||||
| import com.facebook.openwifi.cloudsdk.models.ap.State; | ||||
| import com.facebook.openwifi.rrm.modules.Modeler.DataModel; | ||||
| import com.facebook.openwifi.rrm.modules.ModelerUtils; | ||||
|  | ||||
| @@ -175,7 +175,7 @@ public class LocationBasedOptimalTPC extends TPC { | ||||
| 		// Filter out the invalid APs (e.g., no radio, no location data) | ||||
| 		// Update txPowerChoices, boundary, apLocX, apLocY for the optimization | ||||
| 		for (String serialNumber : serialNumbers) { | ||||
| 			List<State> states = model.latestStates.get(serialNumber); | ||||
| 			List<? extends State> states = model.latestStates.get(serialNumber); | ||||
| 			State state = states.get(states.size() - 1); | ||||
|  | ||||
| 			// Ignore the device if its radio is not active | ||||
|   | ||||
| @@ -22,11 +22,12 @@ import org.slf4j.LoggerFactory; | ||||
|  | ||||
| import com.facebook.openwifi.cloudsdk.UCentralUtils; | ||||
| import com.facebook.openwifi.cloudsdk.WifiScanEntry; | ||||
| import com.facebook.openwifi.cloudsdk.models.ap.Capabilities; | ||||
| import com.facebook.openwifi.cloudsdk.models.ap.State; | ||||
| import com.facebook.openwifi.cloudsdk.StateInfo; | ||||
| import com.facebook.openwifi.rrm.DeviceDataManager; | ||||
| import com.facebook.openwifi.rrm.modules.Modeler.DataModel; | ||||
| import com.facebook.openwifi.rrm.modules.ModelerUtils; | ||||
| import com.google.gson.JsonObject; | ||||
|  | ||||
| /** | ||||
|  * Measurement-based AP-AP TPC algorithm. | ||||
| @@ -156,8 +157,10 @@ public class MeasurementBasedApApTPC extends TPC { | ||||
| 	 */ | ||||
| 	protected static Set<String> getManagedBSSIDs(DataModel model) { | ||||
| 		Set<String> managedBSSIDs = new HashSet<>(); | ||||
| 		for (Map.Entry<String, List<State>> e : model.latestStates.entrySet()) { | ||||
| 			List<State> states = e.getValue(); | ||||
| 		for ( | ||||
| 			Map.Entry<String, List<StateInfo>> e : model.latestStates.entrySet() | ||||
| 		) { | ||||
| 			List<StateInfo> states = e.getValue(); | ||||
| 			State state = states.get(states.size() - 1); | ||||
| 			if (state.interfaces == null) { | ||||
| 				continue; | ||||
| @@ -322,7 +325,7 @@ public class MeasurementBasedApApTPC extends TPC { | ||||
| 			buildRssiMap(managedBSSIDs, model.latestWifiScans, band); | ||||
| 		logger.debug("Starting TPC for the {} band", band); | ||||
| 		for (String serialNumber : serialNumbers) { | ||||
| 			List<State> states = model.latestStates.get(serialNumber); | ||||
| 			List<? extends State> states = model.latestStates.get(serialNumber); | ||||
| 			State state = states.get(states.size() - 1); | ||||
| 			if ( | ||||
| 				state == null || state.radios == null || | ||||
| @@ -373,14 +376,15 @@ public class MeasurementBasedApApTPC extends TPC { | ||||
| 					State.Radio radio = state.radios[idx]; | ||||
|  | ||||
| 					// this specific SSID is not on the band of interest | ||||
| 					JsonObject deviceCapability = model.latestDeviceCapabilities | ||||
| 					Map<String, Capabilities.Phy> capabilitiesPhy = | ||||
| 						model.latestDeviceCapabilitiesPhy | ||||
| 							.get(serialNumber); | ||||
| 					if (deviceCapability == null) { | ||||
| 					if (capabilitiesPhy == null) { | ||||
| 						continue; | ||||
| 					} | ||||
| 					final String radioBand = ModelerUtils.getBand( | ||||
| 						radio, | ||||
| 						deviceCapability | ||||
| 						capabilitiesPhy | ||||
| 					); | ||||
| 					if (radioBand == null || !radioBand.equals(band)) { | ||||
| 						continue; | ||||
|   | ||||
| @@ -18,12 +18,13 @@ import java.util.TreeMap; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
|  | ||||
| import com.facebook.openwifi.cloudsdk.models.ap.Capabilities; | ||||
| import com.facebook.openwifi.cloudsdk.UCentralUtils; | ||||
| import com.facebook.openwifi.cloudsdk.StateInfo; | ||||
| import com.facebook.openwifi.cloudsdk.models.ap.State; | ||||
| import com.facebook.openwifi.rrm.DeviceDataManager; | ||||
| import com.facebook.openwifi.rrm.modules.Modeler.DataModel; | ||||
| import com.facebook.openwifi.rrm.modules.ModelerUtils; | ||||
| import com.google.gson.JsonObject; | ||||
|  | ||||
| /** | ||||
|  * Measurement-based AP-client algorithm. | ||||
| @@ -293,9 +294,11 @@ public class MeasurementBasedApClientTPC extends TPC { | ||||
| 	public Map<String, Map<String, Integer>> computeTxPowerMap() { | ||||
| 		Map<String, Map<String, Integer>> txPowerMap = new TreeMap<>(); | ||||
|  | ||||
| 		for (Map.Entry<String, List<State>> e : model.latestStates.entrySet()) { | ||||
| 		for ( | ||||
| 			Map.Entry<String, List<StateInfo>> e : model.latestStates.entrySet() | ||||
| 		) { | ||||
| 			String serialNumber = e.getKey(); | ||||
| 			List<State> states = e.getValue(); | ||||
| 			List<StateInfo> states = e.getValue(); | ||||
| 			State state = states.get(states.size() - 1); | ||||
| 			if (state.radios == null || state.radios.length == 0) { | ||||
| 				logger.debug( | ||||
| @@ -307,14 +310,15 @@ public class MeasurementBasedApClientTPC extends TPC { | ||||
|  | ||||
| 			Map<String, Integer> radioMap = new TreeMap<>(); | ||||
| 			for (State.Radio radio : state.radios) { | ||||
| 				JsonObject deviceCapability = model.latestDeviceCapabilities | ||||
| 				Map<String, Capabilities.Phy> capabilityPhy = | ||||
| 					model.latestDeviceCapabilitiesPhy | ||||
| 						.get(serialNumber); | ||||
| 				if (deviceCapability == null) { | ||||
| 				if (capabilityPhy == null) { | ||||
| 					continue; | ||||
| 				} | ||||
| 				final String band = ModelerUtils.getBand( | ||||
| 					radio, | ||||
| 					deviceCapability | ||||
| 					capabilityPhy | ||||
| 				); | ||||
| 				if (band == null) { | ||||
| 					continue; | ||||
|   | ||||
| @@ -20,13 +20,14 @@ import java.util.stream.IntStream; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
|  | ||||
| import com.facebook.openwifi.cloudsdk.models.ap.Capabilities; | ||||
| import com.facebook.openwifi.cloudsdk.models.ap.State; | ||||
| import com.facebook.openwifi.cloudsdk.StateInfo; | ||||
| import com.facebook.openwifi.rrm.DeviceConfig; | ||||
| import com.facebook.openwifi.rrm.DeviceDataManager; | ||||
| import com.facebook.openwifi.rrm.modules.ConfigManager; | ||||
| import com.facebook.openwifi.rrm.modules.Modeler.DataModel; | ||||
| import com.facebook.openwifi.rrm.modules.ModelerUtils; | ||||
| import com.google.gson.JsonObject; | ||||
|  | ||||
| /** | ||||
|  * TPC (Transmit Power Control) base class. | ||||
| @@ -78,7 +79,7 @@ public abstract class TPC { | ||||
| 		this.model.latestDeviceStatusRadios.keySet() | ||||
| 			.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber) | ||||
| 			); | ||||
| 		this.model.latestDeviceCapabilities.keySet() | ||||
| 		this.model.latestDeviceCapabilitiesPhy.keySet() | ||||
| 			.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber) | ||||
| 			); | ||||
| 	} | ||||
| @@ -186,9 +187,11 @@ public abstract class TPC { | ||||
| 	 */ | ||||
| 	protected Map<String, Map<Integer, List<String>>> getApsPerChannel() { | ||||
| 		Map<String, Map<Integer, List<String>>> apsPerChannel = new TreeMap<>(); | ||||
| 		for (Map.Entry<String, List<State>> e : model.latestStates.entrySet()) { | ||||
| 		for ( | ||||
| 			Map.Entry<String, List<StateInfo>> e : model.latestStates.entrySet() | ||||
| 		) { | ||||
| 			String serialNumber = e.getKey(); | ||||
| 			List<State> states = e.getValue(); | ||||
| 			List<StateInfo> states = e.getValue(); | ||||
| 			State state = states.get(states.size() - 1); | ||||
|  | ||||
| 			if (state.radios == null || state.radios.length == 0) { | ||||
| @@ -204,14 +207,14 @@ public abstract class TPC { | ||||
| 				if (currentChannel == 0) { | ||||
| 					continue; | ||||
| 				} | ||||
| 				JsonObject deviceCapability = | ||||
| 					model.latestDeviceCapabilities.get(serialNumber); | ||||
| 				if (deviceCapability == null) { | ||||
| 				Map<String, Capabilities.Phy> capabilitiesPhy = | ||||
| 					model.latestDeviceCapabilitiesPhy.get(serialNumber); | ||||
| 				if (capabilitiesPhy == null) { | ||||
| 					continue; | ||||
| 				} | ||||
| 				final String band = ModelerUtils.getBand( | ||||
| 					radio, | ||||
| 					deviceCapability | ||||
| 					capabilitiesPhy | ||||
| 				); | ||||
| 				if (band == null) { | ||||
| 					continue; | ||||
|   | ||||
| @@ -0,0 +1,12 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * Transmit power control (TPC) algorithms. | ||||
|  */ | ||||
| package com.facebook.openwifi.rrm.optimizers.tpc; | ||||
| @@ -0,0 +1,12 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * Radio Resource Management (RRM) service. | ||||
|  */ | ||||
| package com.facebook.openwifi.rrm; | ||||
| @@ -0,0 +1,83 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| package com.facebook.openwifi.rrm.rca; | ||||
|  | ||||
| import java.util.Map; | ||||
|  | ||||
| /** | ||||
|  * Root cause analysis service configuration model. | ||||
|  */ | ||||
| public class RCAConfig { | ||||
| 	// | ||||
| 	// NOTE: | ||||
| 	// Currently assumes RCA is embedded in the RRM service and does NOT | ||||
| 	// duplicate SDK-related fields. | ||||
| 	// | ||||
|  | ||||
| 	/** | ||||
| 	 * StationPinger parameters. | ||||
| 	 */ | ||||
| 	public class StationPingerParams { | ||||
| 		/** | ||||
| 		 * How often to ping each station, in seconds (or 0 to disable) | ||||
| 		 * ({@code STATIONPINGERPARAMS_PINGINTERVALSEC}) | ||||
| 		 */ | ||||
| 		// NOTE: cannot be shorter than Kafka "state" publish interval | ||||
| 		public int pingIntervalSec = 0 /* TODO enable by default */; | ||||
|  | ||||
| 		/** | ||||
| 		 * The number of pings to send to each station | ||||
| 		 * ({@code STATIONPINGERPARAMS_PINGCOUNT}) | ||||
| 		 */ | ||||
| 		public int pingCount = 5; | ||||
|  | ||||
| 		/** | ||||
| 		 * Ignore state records older than this interval (in ms) | ||||
| 		 * ({@code STATIONPINGERPARAMS_STALESTATETHRESHOLDMS}) | ||||
| 		 */ | ||||
| 		// NOTE: should not be longer than Kafka "state" publish interval | ||||
| 		public int staleStateThresholdMs = 300000; // 5 min | ||||
|  | ||||
| 		/** | ||||
| 		 * Number of executor threads for ping tasks | ||||
| 		 * ({@code STATIONPINGERPARAMS_EXECUTORTHREADCOUNT}) | ||||
| 		 */ | ||||
| 		public int executorThreadCount = 3; | ||||
| 	} | ||||
|  | ||||
| 	/** StationPinger parameters. */ | ||||
| 	public StationPingerParams stationPingerParams = new StationPingerParams(); | ||||
|  | ||||
| 	/** Construct RCAConfig from environment variables. */ | ||||
| 	public static RCAConfig fromEnv(Map<String, String> env) { | ||||
| 		RCAConfig config = new RCAConfig(); | ||||
| 		String v; | ||||
|  | ||||
| 		// @formatter:off | ||||
|  | ||||
| 		/* StationPingerParams */ | ||||
| 		StationPingerParams stationPingerParams = config.stationPingerParams; | ||||
| 		if ((v = env.get("STATIONPINGERPARAMS_PINGINTERVALSEC")) != null) { | ||||
| 			stationPingerParams.pingIntervalSec = Integer.parseInt(v); | ||||
| 		} | ||||
| 		if ((v = env.get("STATIONPINGERPARAMS_PINGCOUNT")) != null) { | ||||
| 			stationPingerParams.pingCount = Integer.parseInt(v); | ||||
| 		} | ||||
| 		if ((v = env.get("STATIONPINGERPARAMS_STALESTATETHRESHOLDMS")) != null) { | ||||
| 			stationPingerParams.staleStateThresholdMs = Integer.parseInt(v); | ||||
| 		} | ||||
| 		if ((v = env.get("STATIONPINGERPARAMS_EXECUTORTHREADCOUNT")) != null) { | ||||
| 			stationPingerParams.executorThreadCount = Integer.parseInt(v); | ||||
| 		} | ||||
|  | ||||
| 		// @formatter:on | ||||
|  | ||||
| 		return config; | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										116
									
								
								owrrm/src/main/java/com/facebook/openwifi/rrm/rca/RCAUtils.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								owrrm/src/main/java/com/facebook/openwifi/rrm/rca/RCAUtils.java
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,116 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| package com.facebook.openwifi.rrm.rca; | ||||
|  | ||||
| import com.facebook.openwifi.cloudsdk.UCentralClient; | ||||
| import com.facebook.openwifi.cloudsdk.UCentralUtils; | ||||
| import com.facebook.openwifi.cloudsdk.models.gw.CommandInfo; | ||||
|  | ||||
| /** | ||||
|  * Utilities for root cause analysis. | ||||
|  */ | ||||
| public class RCAUtils { | ||||
| 	/** Ping result, only containing a data summary (not individual pings). */ | ||||
| 	public static class PingResult { | ||||
| 		// NOTE: fields are taken directly from ping output | ||||
| 		/** Minimum ping RTT (ms) */ | ||||
| 		public double min; | ||||
| 		/** Average ping RTT (ms) */ | ||||
| 		public double avg; | ||||
| 		/** Maximum ping RTT (ms) */ | ||||
| 		public double max; | ||||
| 		/** Standard deviation of ping RTT measurements (ms) */ | ||||
| 		public double mdev; | ||||
| 		// TODO other stats? (ex. tx/rx packets, % packet loss) | ||||
|  | ||||
| 		@Override | ||||
| 		public String toString() { | ||||
| 			return String.format("%.3f/%.3f/%.3f/%.3f ms", min, avg, max, mdev); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Parse raw ping output, returning null upon error. | ||||
| 	 * | ||||
| 	 * This only supports the busybox ping format. | ||||
| 	 */ | ||||
| 	private static PingResult parsePingOutput(String output) { | ||||
| 		// Only parse summary line (should be last line in output). | ||||
| 		// Code below is optimized for minimal string operations. | ||||
| 		// | ||||
| 		// Examples of supported formats: | ||||
| 		//   round-trip min/avg/max = 4.126/42.470/84.081 ms | ||||
| 		//   rtt min/avg/max/mdev = 16.853/20.114/23.375/3.261 ms | ||||
| 		final String SUMMARY_TEXT_3 = "min/avg/max"; | ||||
| 		int idx = output.lastIndexOf(SUMMARY_TEXT_3); | ||||
| 		if (idx != -1) { | ||||
| 			idx += SUMMARY_TEXT_3.length(); | ||||
| 		} else { | ||||
| 			final String SUMMARY_TEXT_4 = "min/avg/max/mdev"; | ||||
| 			idx = output.lastIndexOf(SUMMARY_TEXT_4); | ||||
| 			if (idx != -1) { | ||||
| 				idx += SUMMARY_TEXT_4.length(); | ||||
| 			} else { | ||||
| 				return null; | ||||
| 			} | ||||
| 		} | ||||
| 		PingResult result = null; | ||||
| 		for (; idx < output.length(); idx++) { | ||||
| 			if (Character.isDigit(output.charAt(idx))) { | ||||
| 				break; | ||||
| 			} | ||||
| 		} | ||||
| 		if (idx < output.length()) { | ||||
| 			int endIdx = output.indexOf(' ', idx); | ||||
| 			if (endIdx != -1) { | ||||
| 				String s = output.substring(idx, endIdx); | ||||
| 				String[] tokens = s.split("/"); | ||||
| 				if (tokens.length == 3) { | ||||
| 					result = new PingResult(); | ||||
| 					result.min = Double.parseDouble(tokens[0]); | ||||
| 					result.avg = Double.parseDouble(tokens[1]); | ||||
| 					result.max = Double.parseDouble(tokens[2]); | ||||
| 				} else if (tokens.length == 4) { | ||||
| 					result = new PingResult(); | ||||
| 					result.min = Double.parseDouble(tokens[0]); | ||||
| 					result.avg = Double.parseDouble(tokens[1]); | ||||
| 					result.max = Double.parseDouble(tokens[2]); | ||||
| 					result.mdev = Double.parseDouble(tokens[3]); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		return result; | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Instruct a device (AP) to ping a given destination (IP/hostname), | ||||
| 	 * returning the raw ping output or null upon error. | ||||
| 	 * | ||||
| 	 * @param client the UCentralClient instance | ||||
| 	 * @param serialNumber the device (AP) serial number | ||||
| 	 * @param host the ping destination | ||||
| 	 * @param pingCount the number of pings to send | ||||
| 	 * @return the ping output, or null upon error | ||||
| 	 */ | ||||
| 	public static PingResult pingFromDevice( | ||||
| 		UCentralClient client, | ||||
| 		String serialNumber, | ||||
| 		String host, | ||||
| 		int pingCount | ||||
| 	) { | ||||
| 		if (pingCount < 1) { | ||||
| 			throw new IllegalArgumentException("Invalid pingCount < 1"); | ||||
| 		} | ||||
| 		String script = String.format("ping -c %d %s", pingCount, host); | ||||
| 		int timeoutSec = pingCount /* time buffer as follows: */ * 2 + 10; | ||||
| 		CommandInfo info = client.runScript(serialNumber, script, timeoutSec); | ||||
| 		String output = UCentralUtils.getScriptOutput(info); | ||||
| 		return output != null ? parsePingOutput(output) : null; | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,269 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| package com.facebook.openwifi.rrm.rca.modules; | ||||
|  | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| import java.util.concurrent.ConcurrentHashMap; | ||||
| import java.util.concurrent.ExecutorService; | ||||
| import java.util.concurrent.Executors; | ||||
|  | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
|  | ||||
| import com.facebook.openwifi.cloudsdk.UCentralClient; | ||||
| import com.facebook.openwifi.cloudsdk.UCentralUtils; | ||||
| import com.facebook.openwifi.cloudsdk.kafka.UCentralKafkaConsumer; | ||||
| import com.facebook.openwifi.cloudsdk.kafka.UCentralKafkaConsumer.KafkaRecord; | ||||
| import com.facebook.openwifi.cloudsdk.models.ap.State; | ||||
| import com.facebook.openwifi.cloudsdk.models.gw.ServiceEvent; | ||||
| import com.facebook.openwifi.rrm.Utils; | ||||
| import com.facebook.openwifi.rrm.rca.RCAConfig.StationPingerParams; | ||||
| import com.facebook.openwifi.rrm.rca.RCAUtils; | ||||
| import com.facebook.openwifi.rrm.rca.RCAUtils.PingResult; | ||||
| import com.google.gson.Gson; | ||||
| import com.google.gson.JsonObject; | ||||
| import com.google.gson.JsonSyntaxException; | ||||
|  | ||||
| /** | ||||
|  * Ping service to measure latency/jitter between Wi-Fi APs and clients | ||||
|  * (stations). | ||||
|  * <p> | ||||
|  * This class subscribes to the Kafka "state" topic to retrieve the list of APs | ||||
|  * with connected clients, then issues ping commands for each (AP, STA) pair at | ||||
|  * a given frequency. All actions are submitted to an executor during Kafka | ||||
|  * callbacks. | ||||
|  */ | ||||
| public class StationPinger { | ||||
| 	private static final Logger logger = | ||||
| 		LoggerFactory.getLogger(StationPinger.class); | ||||
|  | ||||
| 	/** The module parameters. */ | ||||
| 	private final StationPingerParams params; | ||||
|  | ||||
| 	/** The uCentral client. */ | ||||
| 	private final UCentralClient uCentralClient; | ||||
|  | ||||
| 	/** The executor service instance. */ | ||||
| 	private final ExecutorService executor; | ||||
|  | ||||
| 	/** The Gson instance. */ | ||||
| 	private final Gson gson = new Gson(); | ||||
|  | ||||
| 	/** | ||||
| 	 * Map from device (serial number) to the latest map of STAs | ||||
| 	 * (i.e. client MAC address to Client structure). | ||||
| 	 */ | ||||
| 	private Map<String, Map<String, State.Interface.Client>> deviceToClients = | ||||
| 		new ConcurrentHashMap<>(); | ||||
|  | ||||
| 	/** | ||||
| 	 * Map of last ping timestamps, keyed on | ||||
| 	 * {@link #getDeviceKey(String, String)}. | ||||
| 	 */ | ||||
| 	private Map<String, Long> lastPingTsMap = new ConcurrentHashMap<>(); | ||||
|  | ||||
| 	/** Constructor. */ | ||||
| 	public StationPinger( | ||||
| 		StationPingerParams params, | ||||
| 		UCentralClient uCentralClient, | ||||
| 		UCentralKafkaConsumer consumer | ||||
| 	) { | ||||
| 		this.params = params; | ||||
| 		this.uCentralClient = uCentralClient; | ||||
| 		this.executor = | ||||
| 			Executors.newFixedThreadPool( | ||||
| 				params.executorThreadCount, | ||||
| 				new Utils.NamedThreadFactory( | ||||
| 					"RCA_" + this.getClass().getSimpleName() | ||||
| 				) | ||||
| 			); | ||||
|  | ||||
| 		if (params.pingIntervalSec < 1) { | ||||
| 			logger.info("StationPinger is disabled"); | ||||
| 			return; // quit before registering listeners | ||||
| 		} | ||||
|  | ||||
| 		// Register Kafka listener | ||||
| 		if (consumer != null) { | ||||
| 			consumer.addKafkaListener( | ||||
| 				getClass().getSimpleName(), | ||||
| 				new UCentralKafkaConsumer.KafkaListener() { | ||||
| 					@Override | ||||
| 					public void handleStateRecords(List<KafkaRecord> records) { | ||||
| 						handleKafkaStateRecords(records); | ||||
| 					} | ||||
|  | ||||
| 					@Override | ||||
| 					public void handleWifiScanRecords( | ||||
| 						List<KafkaRecord> records | ||||
| 					) { /* ignored */ } | ||||
|  | ||||
| 					@Override | ||||
| 					public void handleServiceEventRecords( | ||||
| 						List<ServiceEvent> serviceEventRecords | ||||
| 					) { /* ignored */ } | ||||
| 				} | ||||
| 			); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/** Process the list of received State records. */ | ||||
| 	private void handleKafkaStateRecords(List<KafkaRecord> records) { | ||||
| 		long now = System.currentTimeMillis(); | ||||
| 		for (KafkaRecord record : records) { | ||||
| 			// Drop old records | ||||
| 			if (now - record.timestampMs > params.staleStateThresholdMs) { | ||||
| 				logger.debug( | ||||
| 					"Dropping old state record for {} at time {}", | ||||
| 					record.serialNumber, | ||||
| 					record.timestampMs | ||||
| 				); | ||||
| 				continue; | ||||
| 			} | ||||
|  | ||||
| 			// Deserialize State | ||||
| 			JsonObject state = record.payload.getAsJsonObject("state"); | ||||
| 			if (state == null) { | ||||
| 				continue; | ||||
| 			} | ||||
| 			try { | ||||
| 				State stateModel = gson.fromJson(state, State.class); | ||||
| 				Map<String, State.Interface.Client> clientMap = | ||||
| 					UCentralUtils.getWifiClientInfo(stateModel); | ||||
| 				if ( | ||||
| 					deviceToClients.put(record.serialNumber, clientMap) == null | ||||
| 				) { | ||||
| 					// Enqueue this device | ||||
| 					final String serialNumber = record.serialNumber; | ||||
| 					executor.submit(() -> pingDevices(serialNumber)); | ||||
| 				} | ||||
| 			} catch (JsonSyntaxException e) { | ||||
| 				logger.error( | ||||
| 					String.format( | ||||
| 						"Device %s: failed to deserialize state: %s", | ||||
| 						record.serialNumber, | ||||
| 						state | ||||
| 					), | ||||
| 					e | ||||
| 				); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/** Shut down all resources. */ | ||||
| 	public void shutdown() { | ||||
| 		executor.shutdownNow(); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Issue ping commands to all clients of a given AP. | ||||
| 	 * | ||||
| 	 * Note that this is intentionally NOT parallelized to avoid collisions | ||||
| 	 * while transmitting to/from multiple clients of the same AP. | ||||
| 	 */ | ||||
| 	private void pingDevices(String serialNumber) { | ||||
| 		Map<String, State.Interface.Client> clientMap = | ||||
| 			deviceToClients.get(serialNumber); | ||||
| 		if (clientMap == null) { | ||||
| 			return; // shouldn't happen | ||||
| 		} | ||||
|  | ||||
| 		logger.trace( | ||||
| 			"{}: Pinging all clients ({} total)...", | ||||
| 			serialNumber, | ||||
| 			clientMap.size() | ||||
| 		); | ||||
| 		final long PING_INTERVAL_NS = | ||||
| 			Math.max(params.pingIntervalSec, 1) * 1_000_000_000L; | ||||
| 		for ( | ||||
| 			Map.Entry<String, State.Interface.Client> entry : clientMap | ||||
| 				.entrySet() | ||||
| 		) { | ||||
| 			String mac = entry.getKey(); | ||||
| 			String host = getClientAddress(entry.getValue()); | ||||
| 			if (host == null) { | ||||
| 				logger.debug( | ||||
| 					"{}: client {} has no pingable address", | ||||
| 					serialNumber, | ||||
| 					mac | ||||
| 				); | ||||
| 				continue; | ||||
| 			} | ||||
|  | ||||
| 			// Check backoff timer | ||||
| 			long now = System.nanoTime(); | ||||
| 			String deviceKey = getDeviceKey(serialNumber, mac); | ||||
| 			Long lastPingTs = lastPingTsMap.putIfAbsent(deviceKey, now); | ||||
| 			if (lastPingTs != null && now - lastPingTs < PING_INTERVAL_NS) { | ||||
| 				logger.trace( | ||||
| 					"{}: Skipping ping for {} (last pinged {}s ago)", | ||||
| 					serialNumber, | ||||
| 					mac, | ||||
| 					(now - lastPingTs) / 1_000_000_000L | ||||
| 				); | ||||
| 				continue; | ||||
| 			} | ||||
| 			lastPingTsMap.put(deviceKey, now); | ||||
|  | ||||
| 			// Issue ping command | ||||
| 			logger.debug( | ||||
| 				"{}: Pinging client {} ({})", | ||||
| 				serialNumber, | ||||
| 				mac, | ||||
| 				host | ||||
| 			); | ||||
| 			PingResult result = RCAUtils | ||||
| 				.pingFromDevice( | ||||
| 					uCentralClient, | ||||
| 					serialNumber, | ||||
| 					host, | ||||
| 					params.pingCount | ||||
| 				); | ||||
| 			if (result == null) { | ||||
| 				logger.debug( | ||||
| 					"Ping failed from {} to {} ({})", | ||||
| 					serialNumber, | ||||
| 					mac, | ||||
| 					host | ||||
| 				); | ||||
| 				continue; | ||||
| 			} | ||||
| 			// TODO handle results | ||||
| 			logger.info( | ||||
| 				"Ping result from {} to {} ({}): {}", | ||||
| 				serialNumber, | ||||
| 				mac, | ||||
| 				host, | ||||
| 				result.toString() | ||||
| 			); | ||||
| 		} | ||||
|  | ||||
| 		// Remove map entries after we process them | ||||
| 		deviceToClients.remove(serialNumber); | ||||
| 	} | ||||
|  | ||||
| 	/** Return an address to ping for the given client. */ | ||||
| 	private String getClientAddress(State.Interface.Client client) { | ||||
| 		if (client.ipv4_addresses.length > 0) { | ||||
| 			return client.ipv4_addresses[0]; | ||||
| 		} else if (client.ipv6_addresses.length > 0) { | ||||
| 			return client.ipv6_addresses[0]; | ||||
| 		} else { | ||||
| 			return null; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/** Return a key to use in {@link #lastPingTsMap}. */ | ||||
| 	private String getDeviceKey(String serialNumber, String sta) { | ||||
| 		// Use (AP, STA) pair as the key to handle STAs moving between APs | ||||
| 		// TODO - do we care about radio/band/channel changes too? | ||||
| 		return serialNumber + '\0' + sta; | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,12 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * Root Cause Analysis (RCA) service. | ||||
|  */ | ||||
| package com.facebook.openwifi.rrm.rca; | ||||
| @@ -23,8 +23,8 @@ import java.util.Map; | ||||
| import org.junit.jupiter.api.Test; | ||||
|  | ||||
| import com.facebook.openwifi.cloudsdk.AggregatedState; | ||||
| import com.facebook.openwifi.cloudsdk.StateInfo; | ||||
| import com.facebook.openwifi.cloudsdk.WifiScanEntry; | ||||
| import com.facebook.openwifi.cloudsdk.models.ap.State; | ||||
| import com.facebook.openwifi.rrm.aggregators.MeanAggregator; | ||||
| import com.facebook.openwifi.rrm.modules.Modeler.DataModel; | ||||
| import com.facebook.openwifi.rrm.optimizers.TestUtils; | ||||
| @@ -615,7 +615,7 @@ public class ModelerUtilsTest { | ||||
| 			new ArrayList<>(Arrays.asList(aggStateC)) | ||||
| 		); | ||||
|  | ||||
| 		State toBeAggregated1 = TestUtils.createState( | ||||
| 		StateInfo toBeAggregated1 = TestUtils.createState( | ||||
| 			6, | ||||
| 			20, | ||||
| 			10, | ||||
| @@ -628,27 +628,46 @@ public class ModelerUtilsTest { | ||||
| 		ModelerUtils | ||||
| 			.addStateToAggregation(bssidToAggregatedStates, toBeAggregated1); | ||||
|  | ||||
| 		assertEquals( | ||||
| 			bssidToAggregatedStates | ||||
| 		List<Integer> rssiList = new ArrayList<>(); | ||||
| 		for ( | ||||
| 			AggregatedState.AssociationInfo associationInfo : bssidToAggregatedStates | ||||
| 				.get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA1)) | ||||
| 				.get(0).rssi, | ||||
| 				.get(0).associationInfoList | ||||
| 		) { | ||||
| 			rssiList.add(associationInfo.rssi); | ||||
| 		} | ||||
| 		assertEquals( | ||||
| 			rssiList, | ||||
| 			Arrays.asList(10, 20, 30) | ||||
| 		); | ||||
|  | ||||
| 		assertEquals( | ||||
| 			bssidToAggregatedStates | ||||
| 		rssiList = new ArrayList<>(); | ||||
| 		for ( | ||||
| 			AggregatedState.AssociationInfo associationInfo : bssidToAggregatedStates | ||||
| 				.get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA1)) | ||||
| 				.get(1).rssi, | ||||
| 				.get(1).associationInfoList | ||||
| 		) { | ||||
| 			rssiList.add(associationInfo.rssi); | ||||
| 		} | ||||
| 		assertEquals( | ||||
| 			rssiList, | ||||
| 			Arrays.asList(40, 50) | ||||
| 		); | ||||
| 		assertEquals( | ||||
| 			bssidToAggregatedStates | ||||
|  | ||||
| 		rssiList = new ArrayList<>(); | ||||
| 		for ( | ||||
| 			AggregatedState.AssociationInfo associationInfo : bssidToAggregatedStates | ||||
| 				.get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA2)) | ||||
| 				.get(0).rssi, | ||||
| 				.get(0).associationInfoList | ||||
| 		) { | ||||
| 			rssiList.add(associationInfo.rssi); | ||||
| 		} | ||||
| 		assertEquals( | ||||
| 			rssiList, | ||||
| 			Arrays.asList(20, 30, 40, 60) | ||||
| 		); | ||||
|  | ||||
| 		State toBeAggregated2 = TestUtils.createState( | ||||
| 		StateInfo toBeAggregated2 = TestUtils.createState( | ||||
| 			11, | ||||
| 			20, | ||||
| 			20, | ||||
| @@ -659,10 +678,17 @@ public class ModelerUtilsTest { | ||||
| 		); | ||||
| 		ModelerUtils | ||||
| 			.addStateToAggregation(bssidToAggregatedStates, toBeAggregated2); | ||||
| 		assertEquals( | ||||
| 			bssidToAggregatedStates | ||||
|  | ||||
| 		rssiList = new ArrayList<>(); | ||||
| 		for ( | ||||
| 			AggregatedState.AssociationInfo associationInfo : bssidToAggregatedStates | ||||
| 				.get(ModelerUtils.getBssidStationKeyPair(bssidB, stationB)) | ||||
| 				.get(0).rssi, | ||||
| 				.get(0).associationInfoList | ||||
| 		) { | ||||
| 			rssiList.add(associationInfo.rssi); | ||||
| 		} | ||||
| 		assertEquals( | ||||
| 			rssiList, | ||||
| 			Arrays.asList(10, 20, 30, 40) | ||||
| 		); | ||||
| 	} | ||||
| @@ -688,7 +714,7 @@ public class ModelerUtilsTest { | ||||
| 		DataModel dataModel = new DataModel(); | ||||
|  | ||||
| 		// This serie of StateA is used to test a valid input states. | ||||
| 		State time1StateA = TestUtils.createState( | ||||
| 		StateInfo time1StateA = TestUtils.createState( | ||||
| 			1, | ||||
| 			80, | ||||
| 			10, | ||||
| @@ -704,7 +730,7 @@ public class ModelerUtilsTest { | ||||
| 			TestUtils.DEFAULT_LOCAL_TIME | ||||
| 		); | ||||
|  | ||||
| 		State time2StateA = TestUtils.createState( | ||||
| 		StateInfo time2StateA = TestUtils.createState( | ||||
| 			1, | ||||
| 			80, | ||||
| 			10, | ||||
| @@ -721,7 +747,7 @@ public class ModelerUtilsTest { | ||||
| 		); | ||||
|  | ||||
| 		//As State time3StateA is obsolete, it should not be aggregated. | ||||
| 		State time3StateA = TestUtils.createState( | ||||
| 		StateInfo time3StateA = TestUtils.createState( | ||||
| 			1, | ||||
| 			80, | ||||
| 			10, | ||||
| @@ -758,48 +784,98 @@ public class ModelerUtilsTest { | ||||
| 			2 | ||||
| 		); | ||||
| 		assertEquals( | ||||
| 			aggregatedMap.get(serialNumberA).get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA1)).get(0).radio, | ||||
| 			new AggregatedState.Radio(1, 80, 10) | ||||
| 			aggregatedMap.get(serialNumberA).get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA1)).get(0).radioConfig, | ||||
| 			new AggregatedState.RadioConfig(1, 80, 10) | ||||
| 		); | ||||
|  | ||||
| 		List<Integer> rssiList = new ArrayList<>(); | ||||
| 		for ( | ||||
| 			AggregatedState.AssociationInfo associationInfo : aggregatedMap | ||||
| 				.get(serialNumberA) | ||||
| 				.get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA1)) | ||||
| 				.get(0).associationInfoList | ||||
| 		) { | ||||
| 			rssiList.add(associationInfo.rssi); | ||||
| 		} | ||||
| 		assertEquals( | ||||
| 			aggregatedMap.get(serialNumberA).get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA1)).get(0).rssi, | ||||
| 			rssiList, | ||||
| 			Arrays.asList(-84, 27) | ||||
| 		); | ||||
| 		assertEquals( | ||||
| 			aggregatedMap.get(serialNumberA).get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA1)).get(1).radio, | ||||
| 			new AggregatedState.Radio(6, 40, 20) | ||||
| 			aggregatedMap.get(serialNumberA).get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA1)).get(1).radioConfig, | ||||
| 			new AggregatedState.RadioConfig(6, 40, 20) | ||||
| 		); | ||||
|  | ||||
| 		rssiList = new ArrayList<>(); | ||||
| 		for ( | ||||
| 			AggregatedState.AssociationInfo associationInfo : aggregatedMap | ||||
| 				.get(serialNumberA) | ||||
| 				.get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA1)) | ||||
| 				.get(1).associationInfoList | ||||
| 		) { | ||||
| 			rssiList.add(associationInfo.rssi); | ||||
| 		} | ||||
| 		assertEquals( | ||||
| 			aggregatedMap.get(serialNumberA).get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA1)).get(1).rssi, | ||||
| 			rssiList, | ||||
| 			Arrays.asList(-80) | ||||
| 		); | ||||
| 		assertEquals( | ||||
| 			aggregatedMap.get(serialNumberA).get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA2)).get(0).radio, | ||||
| 			new AggregatedState.Radio(1, 80, 10) | ||||
| 			aggregatedMap.get(serialNumberA).get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA2)).get(0).radioConfig, | ||||
| 			new AggregatedState.RadioConfig(1, 80, 10) | ||||
| 		); | ||||
|  | ||||
| 		rssiList = new ArrayList<>(); | ||||
| 		for ( | ||||
| 			AggregatedState.AssociationInfo associationInfo : aggregatedMap | ||||
| 				.get(serialNumberA) | ||||
| 				.get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA2)) | ||||
| 				.get(0).associationInfoList | ||||
| 		) { | ||||
| 			rssiList.add(associationInfo.rssi); | ||||
| 		} | ||||
| 		assertEquals( | ||||
| 			aggregatedMap.get(serialNumberA).get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA2)).get(0).rssi, | ||||
| 			rssiList, | ||||
| 			Arrays.asList(-67, -67) | ||||
| 		); | ||||
| 		assertEquals( | ||||
| 			aggregatedMap.get(serialNumberA).get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA2)).get(1).radio, | ||||
| 			new AggregatedState.Radio(6, 40, 20) | ||||
| 			aggregatedMap.get(serialNumberA).get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA2)).get(1).radioConfig, | ||||
| 			new AggregatedState.RadioConfig(6, 40, 20) | ||||
| 		); | ||||
|  | ||||
| 		rssiList = new ArrayList<>(); | ||||
| 		for ( | ||||
| 			AggregatedState.AssociationInfo associationInfo : aggregatedMap | ||||
| 				.get(serialNumberA) | ||||
| 				.get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA2)) | ||||
| 				.get(1).associationInfoList | ||||
| 		) { | ||||
| 			rssiList.add(associationInfo.rssi); | ||||
| 		} | ||||
| 		assertEquals( | ||||
| 			aggregatedMap.get(serialNumberA).get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA2)).get(1).rssi, | ||||
| 			rssiList, | ||||
| 			Arrays.asList(180, 67) | ||||
| 		); | ||||
| 		assertEquals( | ||||
| 			aggregatedMap.get(serialNumberA).get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA3)).get(0).radio, | ||||
| 			new AggregatedState.Radio(1, 80, 10) | ||||
| 			aggregatedMap.get(serialNumberA).get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA3)).get(0).radioConfig, | ||||
| 			new AggregatedState.RadioConfig(1, 80, 10) | ||||
| 		); | ||||
|  | ||||
| 		rssiList = new ArrayList<>(); | ||||
| 		for ( | ||||
| 			AggregatedState.AssociationInfo associationInfo : aggregatedMap | ||||
| 				.get(serialNumberA) | ||||
| 				.get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA3)) | ||||
| 				.get(0).associationInfoList | ||||
| 		) { | ||||
| 			rssiList.add(associationInfo.rssi); | ||||
| 		} | ||||
| 		assertEquals( | ||||
| 			aggregatedMap.get(serialNumberA).get(ModelerUtils.getBssidStationKeyPair(bssidA, stationA3)).get(0).rssi, | ||||
| 			rssiList, | ||||
| 			Arrays.asList(10, 100) | ||||
| 		); | ||||
|  | ||||
| 		// Test more clients operate on the same channel (stationB and stationA) | ||||
| 		State time1StateB = TestUtils.createState( | ||||
| 		StateInfo time1StateB = TestUtils.createState( | ||||
| 			1, | ||||
| 			80, | ||||
| 			10, | ||||
| @@ -812,7 +888,7 @@ public class ModelerUtilsTest { | ||||
| 			.computeIfAbsent(serialNumberB, k -> new ArrayList<>()) | ||||
| 			.add(time1StateB); | ||||
|  | ||||
| 		State time1StateC = TestUtils.createState( | ||||
| 		StateInfo time1StateC = TestUtils.createState( | ||||
| 			6, | ||||
| 			40, | ||||
| 			20, | ||||
| @@ -829,12 +905,32 @@ public class ModelerUtilsTest { | ||||
| 			ModelerUtils | ||||
| 				.getAggregatedStates(dataModel, obsoletionPeriodMs, refTimeMs); | ||||
|  | ||||
| 		rssiList = new ArrayList<>(); | ||||
| 		for ( | ||||
| 			AggregatedState.AssociationInfo associationInfo : aggregatedMap2 | ||||
| 				.get(serialNumberB) | ||||
| 				.get(ModelerUtils.getBssidStationKeyPair(bssidB, stationB)) | ||||
| 				.get(0).associationInfoList | ||||
| 		) { | ||||
| 			rssiList.add(associationInfo.rssi); | ||||
| 		} | ||||
| 		assertEquals( | ||||
| 			aggregatedMap2.get(serialNumberB).get(ModelerUtils.getBssidStationKeyPair(bssidB, stationB)).get(0).rssi, Arrays.asList(-30) | ||||
| 			rssiList, | ||||
| 			Arrays.asList(-30) | ||||
| 		); | ||||
|  | ||||
| 		rssiList = new ArrayList<>(); | ||||
| 		for ( | ||||
| 			AggregatedState.AssociationInfo associationInfo : aggregatedMap2 | ||||
| 				.get(serialNumberC) | ||||
| 				.get(ModelerUtils.getBssidStationKeyPair(bssidC, stationC)) | ||||
| 				.get(0).associationInfoList | ||||
| 		) { | ||||
| 			rssiList.add(associationInfo.rssi); | ||||
| 		} | ||||
| 		assertEquals( | ||||
| 			aggregatedMap2.get(serialNumberC).get(ModelerUtils.getBssidStationKeyPair(bssidC, stationC)).get(0).rssi, Arrays.asList(-100) | ||||
| 			rssiList, | ||||
| 			Arrays.asList(-100) | ||||
| 		); | ||||
|  | ||||
| 		assertEquals( | ||||
|   | ||||
| @@ -24,6 +24,7 @@ public class ProvMonitorTest { | ||||
| 	private DeviceDataManager deviceDataManager; | ||||
|  | ||||
| 	/** Test provisioning monitor. */ | ||||
| 	@SuppressWarnings("unused") | ||||
| 	private ProvMonitor provMonitor; | ||||
|  | ||||
| 	@BeforeEach | ||||
|   | ||||
| @@ -11,6 +11,7 @@ package com.facebook.openwifi.rrm.optimizers; | ||||
| import java.time.Instant; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Arrays; | ||||
| import java.util.HashMap; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| import java.util.Set; | ||||
| @@ -18,14 +19,17 @@ import java.util.TreeSet; | ||||
| import java.util.stream.Collectors; | ||||
|  | ||||
| import com.facebook.openwifi.cloudsdk.AggregatedState; | ||||
| import com.facebook.openwifi.cloudsdk.StateInfo; | ||||
| import com.facebook.openwifi.cloudsdk.UCentralConstants; | ||||
| import com.facebook.openwifi.cloudsdk.UCentralUtils; | ||||
| import com.facebook.openwifi.cloudsdk.WifiScanEntry; | ||||
| import com.facebook.openwifi.cloudsdk.models.ap.Capabilities; | ||||
| import com.facebook.openwifi.cloudsdk.models.ap.State; | ||||
| import com.facebook.openwifi.cloudsdk.models.ap.UCentralSchema; | ||||
| import com.facebook.openwifi.rrm.DeviceTopology; | ||||
| import com.google.gson.Gson; | ||||
| import com.google.gson.JsonArray; | ||||
| import com.google.gson.JsonObject; | ||||
| import com.google.gson.JsonPrimitive; | ||||
|  | ||||
| public class TestUtils { | ||||
| 	/** The Gson instance. */ | ||||
| @@ -102,34 +106,33 @@ public class TestUtils { | ||||
| 	 * @param band band (e.g., "2G") | ||||
| 	 * @param channel channel number | ||||
| 	 * @param channelWidth channel width in MHz | ||||
| 	 * @return a radio info object as a {@code JsonObject} | ||||
| 	 * @return a radio info object as a {@code UCentralSchema.Radio} | ||||
| 	 */ | ||||
| 	private static JsonObject createDeviceStatusRadioObject( | ||||
| 	private static UCentralSchema.Radio createDeviceStatusRadioObject( | ||||
| 		String band, | ||||
| 		int channel, | ||||
| 		int channelWidth, | ||||
| 		int txPower | ||||
| 	) { | ||||
| 		return gson.fromJson( | ||||
| 			String.format( | ||||
| 				"{\"band\": %s,\"channel\": %d,\"channel-mode\":\"HE\"," + | ||||
| 					"\"channel-width\":%d,\"country\":\"CA\",\"tx-power\":%d}", | ||||
| 				band, | ||||
| 				channel, | ||||
| 				channelWidth, | ||||
| 				txPower | ||||
| 			), | ||||
| 			JsonObject.class | ||||
| 		); | ||||
| 		UCentralSchema.Radio radio = new UCentralSchema.Radio(); | ||||
| 		radio.band = band; | ||||
| 		radio.channel = new JsonPrimitive(channel); | ||||
| 		radio.channelMode = "HE"; | ||||
| 		radio.channelWidth = channelWidth; | ||||
| 		radio.country = "CA"; | ||||
| 		radio.txPower = txPower; | ||||
| 		return radio; | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Create an array with one radio info entry with the given channel on a | ||||
| 	 * given band. | ||||
| 	 */ | ||||
| 	public static JsonArray createDeviceStatus(String band, int channel) { | ||||
| 		JsonArray jsonList = new JsonArray(); | ||||
| 		jsonList.add( | ||||
| 	public static List<UCentralSchema.Radio> createDeviceStatus( | ||||
| 		String band, | ||||
| 		int channel | ||||
| 	) { | ||||
| 		return Arrays.asList( | ||||
| 			createDeviceStatusRadioObject( | ||||
| 				band, | ||||
| 				channel, | ||||
| @@ -137,7 +140,6 @@ public class TestUtils { | ||||
| 				DEFAULT_TX_POWER | ||||
| 			) | ||||
| 		); | ||||
| 		return jsonList; | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| @@ -149,13 +151,12 @@ public class TestUtils { | ||||
| 	 * @return an array with one radio info entry with the given band, channel, | ||||
| 	 *         and tx power | ||||
| 	 */ | ||||
| 	public static JsonArray createDeviceStatus( | ||||
| 	public static List<UCentralSchema.Radio> createDeviceStatus( | ||||
| 		String band, | ||||
| 		int channel, | ||||
| 		int txPower | ||||
| 	) { | ||||
| 		JsonArray jsonList = new JsonArray(); | ||||
| 		jsonList.add( | ||||
| 		return Arrays.asList( | ||||
| 			createDeviceStatusRadioObject( | ||||
| 				band, | ||||
| 				channel, | ||||
| @@ -163,39 +164,36 @@ public class TestUtils { | ||||
| 				txPower | ||||
| 			) | ||||
| 		); | ||||
| 		return jsonList; | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Create an array with one radio info entry per given band (using the | ||||
| 	 * lowest channel). | ||||
| 	 */ | ||||
| 	public static JsonArray createDeviceStatus(List<String> bands) { | ||||
| 		JsonArray jsonList = new JsonArray(); | ||||
| 		for (String band : bands) { | ||||
| 			int channel = UCentralUtils.getLowerChannelLimit(band); | ||||
| 			jsonList.add( | ||||
| 				createDeviceStatusRadioObject( | ||||
| 	public static List<UCentralSchema.Radio> createDeviceStatus( | ||||
| 		List<String> bands | ||||
| 	) { | ||||
| 		return bands.stream() | ||||
| 			.map( | ||||
| 				band -> createDeviceStatusRadioObject( | ||||
| 					band, | ||||
| 					channel, | ||||
| 					UCentralUtils.getLowerChannelLimit(band), | ||||
| 					DEFAULT_CHANNEL_WIDTH, | ||||
| 					DEFAULT_TX_POWER | ||||
| 				) | ||||
| 			); | ||||
| 		} | ||||
| 		return jsonList; | ||||
| 			) | ||||
| 			.collect(Collectors.toList()); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Create an array with one radio info entry with the given tx power and | ||||
| 	 * channel. | ||||
| 	 */ | ||||
| 	public static JsonArray createDeviceStatusSingleBand( | ||||
| 	public static List<UCentralSchema.Radio> createDeviceStatusSingleBand( | ||||
| 		int channel, | ||||
| 		int txPower2G | ||||
| 	) { | ||||
| 		JsonArray jsonList = new JsonArray(); | ||||
| 		jsonList.add( | ||||
| 		return Arrays.asList( | ||||
| 			createDeviceStatusRadioObject( | ||||
| 				channelToLowestMatchingBand(channel), | ||||
| 				channel, | ||||
| @@ -203,29 +201,25 @@ public class TestUtils { | ||||
| 				txPower2G | ||||
| 			) | ||||
| 		); | ||||
| 		return jsonList; | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Create an array with two radio info entries (2G and 5G), with the given | ||||
| 	 * tx powers and channels. | ||||
| 	 */ | ||||
| 	public static JsonArray createDeviceStatusDualBand( | ||||
| 	public static List<UCentralSchema.Radio> createDeviceStatusDualBand( | ||||
| 		int channel2G, | ||||
| 		int txPower2G, | ||||
| 		int channel5G, | ||||
| 		int txPower5G | ||||
| 	) { | ||||
| 		JsonArray jsonList = new JsonArray(); | ||||
| 		jsonList.add( | ||||
| 		return Arrays.asList( | ||||
| 			createDeviceStatusRadioObject( | ||||
| 				UCentralConstants.BAND_2G, | ||||
| 				channel2G, | ||||
| 				DEFAULT_CHANNEL_WIDTH, | ||||
| 				txPower2G | ||||
| 			) | ||||
| 		); | ||||
| 		jsonList.add( | ||||
| 			), | ||||
| 			createDeviceStatusRadioObject( | ||||
| 				UCentralConstants.BAND_5G, | ||||
| 				channel5G, | ||||
| @@ -233,7 +227,6 @@ public class TestUtils { | ||||
| 				txPower5G | ||||
| 			) | ||||
| 		); | ||||
| 		return jsonList; | ||||
| 	} | ||||
|  | ||||
| 	/** Create a wifi scan entry with the given channel. */ | ||||
| @@ -533,7 +526,7 @@ public class TestUtils { | ||||
| 	 * @param localtime unix timestamp in seconds. | ||||
| 	 * @return the state of an AP with radios described by the given parameters | ||||
| 	 */ | ||||
| 	public static State createState( | ||||
| 	public static StateInfo createState( | ||||
| 		int[] channels, | ||||
| 		int[] channelWidths, | ||||
| 		int[] txPowers, | ||||
| @@ -554,13 +547,13 @@ public class TestUtils { | ||||
| 			); | ||||
| 		} | ||||
| 		final int numRadios = channels.length; | ||||
| 		State state = new State(); | ||||
| 		state.interfaces = new State.Interface[numRadios + 1]; | ||||
| 		StateInfo state = new StateInfo(); | ||||
| 		state.interfaces = new StateInfo.Interface[numRadios + 1]; | ||||
| 		for (int index = 0; index < numRadios; index++) { | ||||
| 			state.interfaces[index] = createUpStateInterface(index); | ||||
| 		} | ||||
| 		state.interfaces[numRadios] = createDownStateInterface(numRadios); | ||||
| 		state.radios = new State.Radio[numRadios]; | ||||
| 		state.radios = new StateInfo.Radio[numRadios]; | ||||
| 		for (int i = 0; i < numRadios; i++) { | ||||
| 			state.radios[i] = createStateRadio(i); | ||||
| 			state.radios[i].channel = channels[i]; | ||||
| @@ -568,10 +561,10 @@ public class TestUtils { | ||||
| 			state.radios[i].tx_power = txPowers[i]; | ||||
| 			state.interfaces[i].ssids[0].bssid = bssids[i]; | ||||
| 			state.interfaces[i].ssids[0].associations = | ||||
| 				new State.Interface.SSID.Association[clientRssis[i].length]; | ||||
| 				new StateInfo.Interface.SSID.Association[clientRssis[i].length]; | ||||
| 			for (int j = 0; j < clientRssis[i].length; j++) { | ||||
| 				state.interfaces[i].ssids[0].associations[j] = | ||||
| 					new State.Interface.SSID.Association(); | ||||
| 					new StateInfo.Interface.SSID.Association(); | ||||
| 				state.interfaces[i].ssids[0].associations[j].rssi = | ||||
| 					clientRssis[i][j]; | ||||
| 				state.interfaces[i].ssids[0].associations[j].station = | ||||
| @@ -597,7 +590,7 @@ public class TestUtils { | ||||
| 	 * @param localtime unix timestamp in seconds. | ||||
| 	 * @return the state of an AP with one radio | ||||
| 	 */ | ||||
| 	public static State createState( | ||||
| 	public static StateInfo createState( | ||||
| 		int channel, | ||||
| 		int channelWidth, | ||||
| 		int txPower, | ||||
| @@ -625,7 +618,7 @@ public class TestUtils { | ||||
| 	 * @param bssid bssid | ||||
| 	 * @return the state of an AP with one radio | ||||
| 	 */ | ||||
| 	public static State createState( | ||||
| 	public static StateInfo createState( | ||||
| 		int channel, | ||||
| 		int channelWidth, | ||||
| 		String bssid | ||||
| @@ -642,7 +635,7 @@ public class TestUtils { | ||||
| 	 * @param bssid bssid | ||||
| 	 * @return the state of an AP with one radio | ||||
| 	 */ | ||||
| 	public static State createState( | ||||
| 	public static StateInfo createState( | ||||
| 		int channel, | ||||
| 		int channelWidth, | ||||
| 		int txPower, | ||||
| @@ -667,7 +660,7 @@ public class TestUtils { | ||||
| 	 * @param clientRssis array of client RSSIs | ||||
| 	 * @return the state of an AP with one radio | ||||
| 	 */ | ||||
| 	public static State createState( | ||||
| 	public static StateInfo createState( | ||||
| 		int channel, | ||||
| 		int channelWidth, | ||||
| 		int txPower, | ||||
| @@ -698,7 +691,7 @@ public class TestUtils { | ||||
| 	 * @param bssidB bssid for radio on channelB | ||||
| 	 * @return the state of an AP with two radios | ||||
| 	 */ | ||||
| 	public static State createState( | ||||
| 	public static StateInfo createState( | ||||
| 		int channelA, | ||||
| 		int channelWidthA, | ||||
| 		int txPowerA, | ||||
| @@ -735,7 +728,7 @@ public class TestUtils { | ||||
| 	* @param localtime local time for the State | ||||
| 	* @return the state of an AP with two radios | ||||
| 	*/ | ||||
| 	public static State createState( | ||||
| 	public static StateInfo createState( | ||||
| 		int channelA, | ||||
| 		int channelWidthA, | ||||
| 		int txPowerA, | ||||
| @@ -764,11 +757,9 @@ public class TestUtils { | ||||
| 	/** | ||||
| 	 * Create a radio capability object which is part of the device capability. | ||||
| 	 */ | ||||
| 	public static JsonObject createRadioCapability(String band) { | ||||
| 		JsonObject radioCapability = new JsonObject(); | ||||
| 		JsonArray bandArray = new JsonArray(); | ||||
| 		bandArray.add(band); | ||||
| 		radioCapability.add("band", bandArray); | ||||
| 	public static Capabilities.Phy createCapabilitiesPhy(String band) { | ||||
| 		Capabilities.Phy phy = new Capabilities.Phy(); | ||||
| 		phy.band = new String[] { band }; | ||||
| 		// the following fields are present but unused so they are excluded here | ||||
| 		// channels | ||||
| 		// dfs_channels | ||||
| @@ -780,23 +771,27 @@ public class TestUtils { | ||||
| 		// rx_ant | ||||
| 		// tx_ant | ||||
| 		// vht_capa | ||||
| 		return radioCapability; | ||||
| 		return phy; | ||||
| 	} | ||||
|  | ||||
| 	/** Create a device capability object with radios in the given bands. */ | ||||
| 	public static JsonObject createDeviceCapability(String[] bands) { | ||||
| 		JsonObject deviceCapability = new JsonObject(); | ||||
| 	public static Map<String, Capabilities.Phy> createDeviceCapabilityPhy( | ||||
| 		String[] bands | ||||
| 	) { | ||||
| 		Map<String, Capabilities.Phy> capabilitiesPhy = new HashMap<>(); | ||||
| 		for (int i = 0; i < bands.length; i++) { | ||||
| 			String phyId = generatePhyString(i); | ||||
| 			JsonObject radioCapability = createRadioCapability(bands[i]); | ||||
| 			deviceCapability.add(phyId, radioCapability); | ||||
| 			Capabilities.Phy radioCapability = createCapabilitiesPhy(bands[i]); | ||||
| 			capabilitiesPhy.put(phyId, radioCapability); | ||||
| 		} | ||||
| 		return deviceCapability; | ||||
| 		return capabilitiesPhy; | ||||
| 	} | ||||
|  | ||||
| 	/** Create a device capability object with a radio in the given band. */ | ||||
| 	public static JsonObject createDeviceCapability(String band) { | ||||
| 		return createDeviceCapability(new String[] { band }); | ||||
| 	public static Map<String, Capabilities.Phy> createDeviceCapabilityPhy( | ||||
| 		String band | ||||
| 	) { | ||||
| 		return createDeviceCapabilityPhy(new String[] { band }); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| @@ -819,11 +814,15 @@ public class TestUtils { | ||||
| 		int[] clientRssi | ||||
| 	) { | ||||
| 		AggregatedState state = new AggregatedState(); | ||||
| 		state.radio = new AggregatedState.Radio(channel, channelWidth, txPower); | ||||
| 		state.radioConfig = | ||||
| 			new AggregatedState.RadioConfig(channel, channelWidth, txPower); | ||||
| 		state.bssid = bssid; | ||||
| 		state.station = station; | ||||
| 		state.associationInfoList = new ArrayList<>(); | ||||
| 		for (int rssi : clientRssi) { | ||||
| 			state.rssi.add(rssi); | ||||
| 			AggregatedState.AssociationInfo associationInfo = | ||||
| 				new AggregatedState.AssociationInfo(rssi); | ||||
| 			state.associationInfoList.add(associationInfo); | ||||
| 		} | ||||
| 		return state; | ||||
| 	} | ||||
|   | ||||
| @@ -63,9 +63,9 @@ public class LeastUsedChannelOptimizerTest { | ||||
| 					.createState(aExpectedChannel, channelWidth, dummyBssid) | ||||
| 			) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceA, | ||||
| 			TestUtils.createDeviceCapability(band) | ||||
| 			TestUtils.createDeviceCapabilityPhy(band) | ||||
| 		); | ||||
| 		dataModel.latestWifiScans.put( | ||||
| 			deviceA, | ||||
| @@ -89,9 +89,9 @@ public class LeastUsedChannelOptimizerTest { | ||||
| 			deviceB, | ||||
| 			Arrays.asList(TestUtils.createState(40, channelWidth, dummyBssid)) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceB, | ||||
| 			TestUtils.createDeviceCapability(band) | ||||
| 			TestUtils.createDeviceCapabilityPhy(band) | ||||
| 		); | ||||
| 		dataModel.latestWifiScans.put( | ||||
| 			deviceB, | ||||
| @@ -116,9 +116,9 @@ public class LeastUsedChannelOptimizerTest { | ||||
| 				TestUtils.createState(149, channelWidth, dummyBssid) | ||||
| 			) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceC, | ||||
| 			TestUtils.createDeviceCapability(band) | ||||
| 			TestUtils.createDeviceCapabilityPhy(band) | ||||
| 		); | ||||
| 		dataModel.latestWifiScans.put( | ||||
| 			deviceC, | ||||
| @@ -167,9 +167,9 @@ public class LeastUsedChannelOptimizerTest { | ||||
| 					.createState(aExpectedChannel, channelWidth, dummyBssid) | ||||
| 			) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceA, | ||||
| 			TestUtils.createDeviceCapability(band) | ||||
| 			TestUtils.createDeviceCapabilityPhy(band) | ||||
| 		); | ||||
| 		dataModel.latestWifiScans.put( | ||||
| 			deviceA, | ||||
| @@ -193,9 +193,9 @@ public class LeastUsedChannelOptimizerTest { | ||||
| 			deviceB, | ||||
| 			Arrays.asList(TestUtils.createState(6, channelWidth, dummyBssid)) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceB, | ||||
| 			TestUtils.createDeviceCapability(band) | ||||
| 			TestUtils.createDeviceCapabilityPhy(band) | ||||
| 		); | ||||
| 		dataModel.latestWifiScans.put( | ||||
| 			deviceB, | ||||
| @@ -215,9 +215,9 @@ public class LeastUsedChannelOptimizerTest { | ||||
| 			deviceC, | ||||
| 			Arrays.asList(TestUtils.createState(6, channelWidth, dummyBssid)) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceC, | ||||
| 			TestUtils.createDeviceCapability(band) | ||||
| 			TestUtils.createDeviceCapabilityPhy(band) | ||||
| 		); | ||||
| 		dataModel.latestWifiScans.put( | ||||
| 			deviceC, | ||||
| @@ -275,9 +275,9 @@ public class LeastUsedChannelOptimizerTest { | ||||
| 					.createState(aExpectedChannel, channelWidth, dummyBssid) | ||||
| 			) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceA, | ||||
| 			TestUtils.createDeviceCapability(band) | ||||
| 			TestUtils.createDeviceCapabilityPhy(band) | ||||
| 		); | ||||
| 		dataModel.latestWifiScans.put( | ||||
| 			deviceA, | ||||
| @@ -300,9 +300,9 @@ public class LeastUsedChannelOptimizerTest { | ||||
| 			deviceB, | ||||
| 			Arrays.asList(TestUtils.createState(40, channelWidth, dummyBssid)) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceB, | ||||
| 			TestUtils.createDeviceCapability(band) | ||||
| 			TestUtils.createDeviceCapabilityPhy(band) | ||||
| 		); | ||||
| 		dataModel.latestWifiScans.put( | ||||
| 			deviceB, | ||||
| @@ -326,9 +326,9 @@ public class LeastUsedChannelOptimizerTest { | ||||
| 				TestUtils.createState(149, channelWidth, dummyBssid) | ||||
| 			) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceC, | ||||
| 			TestUtils.createDeviceCapability(band) | ||||
| 			TestUtils.createDeviceCapabilityPhy(band) | ||||
| 		); | ||||
| 		dataModel.latestWifiScans.put( | ||||
| 			deviceC, | ||||
| @@ -385,9 +385,9 @@ public class LeastUsedChannelOptimizerTest { | ||||
| 					.createState(aExpectedChannel, channelWidth, dummyBssid) | ||||
| 			) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceA, | ||||
| 			TestUtils.createDeviceCapability(band) | ||||
| 			TestUtils.createDeviceCapabilityPhy(band) | ||||
| 		); | ||||
| 		dataModel.latestWifiScans.put( | ||||
| 			deviceA, | ||||
| @@ -412,9 +412,9 @@ public class LeastUsedChannelOptimizerTest { | ||||
| 			deviceB, | ||||
| 			Arrays.asList(TestUtils.createState(40, channelWidth, dummyBssid)) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceB, | ||||
| 			TestUtils.createDeviceCapability(band) | ||||
| 			TestUtils.createDeviceCapabilityPhy(band) | ||||
| 		); | ||||
| 		dataModel.latestWifiScans.put( | ||||
| 			deviceB, | ||||
| @@ -439,9 +439,9 @@ public class LeastUsedChannelOptimizerTest { | ||||
| 				TestUtils.createState(149, channelWidth, dummyBssid) | ||||
| 			) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceC, | ||||
| 			TestUtils.createDeviceCapability(band) | ||||
| 			TestUtils.createDeviceCapabilityPhy(band) | ||||
| 		); | ||||
| 		dataModel.latestWifiScans.put( | ||||
| 			deviceC, | ||||
| @@ -492,9 +492,9 @@ public class LeastUsedChannelOptimizerTest { | ||||
| 					.createState(aExpectedChannel, channelWidth, dummyBssid) | ||||
| 			) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceA, | ||||
| 			TestUtils.createDeviceCapability(band) | ||||
| 			TestUtils.createDeviceCapabilityPhy(band) | ||||
| 		); | ||||
| 		dataModel.latestWifiScans.put( | ||||
| 			deviceA, | ||||
| @@ -520,9 +520,9 @@ public class LeastUsedChannelOptimizerTest { | ||||
| 			deviceB, | ||||
| 			Arrays.asList(TestUtils.createState(40, channelWidth, dummyBssid)) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceB, | ||||
| 			TestUtils.createDeviceCapability(band) | ||||
| 			TestUtils.createDeviceCapabilityPhy(band) | ||||
| 		); | ||||
| 		dataModel.latestWifiScans.put( | ||||
| 			deviceB, | ||||
| @@ -547,9 +547,9 @@ public class LeastUsedChannelOptimizerTest { | ||||
| 				TestUtils.createState(149, channelWidth, dummyBssid) | ||||
| 			) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceC, | ||||
| 			TestUtils.createDeviceCapability(band) | ||||
| 			TestUtils.createDeviceCapabilityPhy(band) | ||||
| 		); | ||||
| 		dataModel.latestWifiScans.put( | ||||
| 			deviceC, | ||||
| @@ -571,9 +571,9 @@ public class LeastUsedChannelOptimizerTest { | ||||
| 			deviceD, | ||||
| 			Arrays.asList(TestUtils.createState(40, channelWidth, dummyBssid)) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceD, | ||||
| 			TestUtils.createDeviceCapability(band) | ||||
| 			TestUtils.createDeviceCapabilityPhy(band) | ||||
| 		); | ||||
| 		dataModel.latestWifiScans.put( | ||||
| 			deviceD, | ||||
| @@ -631,9 +631,9 @@ public class LeastUsedChannelOptimizerTest { | ||||
| 					.createState(aExpectedChannel, channelWidth, dummyBssid) | ||||
| 			) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceA, | ||||
| 			TestUtils.createDeviceCapability(band) | ||||
| 			TestUtils.createDeviceCapabilityPhy(band) | ||||
| 		); | ||||
| 		dataModel.latestWifiScans.put( | ||||
| 			deviceA, | ||||
| @@ -657,9 +657,9 @@ public class LeastUsedChannelOptimizerTest { | ||||
| 			deviceB, | ||||
| 			Arrays.asList(TestUtils.createState(36, channelWidth, dummyBssid)) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceB, | ||||
| 			TestUtils.createDeviceCapability(band) | ||||
| 			TestUtils.createDeviceCapabilityPhy(band) | ||||
| 		); | ||||
| 		dataModel.latestWifiScans.put( | ||||
| 			deviceB, | ||||
| @@ -684,9 +684,9 @@ public class LeastUsedChannelOptimizerTest { | ||||
| 				TestUtils.createState(149, channelWidth, dummyBssid) | ||||
| 			) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceC, | ||||
| 			TestUtils.createDeviceCapability(band) | ||||
| 			TestUtils.createDeviceCapabilityPhy(band) | ||||
| 		); | ||||
| 		dataModel.latestWifiScans.put( | ||||
| 			deviceC, | ||||
| @@ -710,9 +710,9 @@ public class LeastUsedChannelOptimizerTest { | ||||
| 			deviceD, | ||||
| 			Arrays.asList(TestUtils.createState(36, channelWidth, dummyBssid)) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceD, | ||||
| 			TestUtils.createDeviceCapability(band) | ||||
| 			TestUtils.createDeviceCapabilityPhy(band) | ||||
| 		); | ||||
| 		dataModel.latestWifiScans.put( | ||||
| 			deviceD, | ||||
| @@ -742,9 +742,9 @@ public class LeastUsedChannelOptimizerTest { | ||||
| 					.createState(aExpectedChannel, channelWidth, dummyBssid) | ||||
| 			) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceE, | ||||
| 			TestUtils.createDeviceCapability(band) | ||||
| 			TestUtils.createDeviceCapabilityPhy(band) | ||||
| 		); | ||||
| 		dataModel.latestWifiScans.put( | ||||
| 			deviceE, | ||||
| @@ -795,9 +795,9 @@ public class LeastUsedChannelOptimizerTest { | ||||
| 					.createState(aExpectedChannel, channelWidth, dummyBssid) | ||||
| 			) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceA, | ||||
| 			TestUtils.createDeviceCapability(band) | ||||
| 			TestUtils.createDeviceCapabilityPhy(band) | ||||
| 		); | ||||
| 		dataModel.latestWifiScans.put( | ||||
| 			deviceA, | ||||
| @@ -820,9 +820,9 @@ public class LeastUsedChannelOptimizerTest { | ||||
| 			deviceB, | ||||
| 			Arrays.asList(TestUtils.createState(48, channelWidth, dummyBssid)) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceB, | ||||
| 			TestUtils.createDeviceCapability(band) | ||||
| 			TestUtils.createDeviceCapabilityPhy(band) | ||||
| 		); | ||||
| 		dataModel.latestWifiScans.put( | ||||
| 			deviceB, | ||||
| @@ -858,9 +858,9 @@ public class LeastUsedChannelOptimizerTest { | ||||
| 				TestUtils.createState(149, channelWidth, dummyBssid) | ||||
| 			) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceC, | ||||
| 			TestUtils.createDeviceCapability(band) | ||||
| 			TestUtils.createDeviceCapabilityPhy(band) | ||||
| 		); | ||||
| 		dataModel.latestWifiScans.put( | ||||
| 			deviceC, | ||||
|   | ||||
| @@ -67,13 +67,13 @@ public class RandomChannelInitializerTest { | ||||
| 			deviceB, | ||||
| 			TestUtils.createDeviceStatus(band, 8) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceA, | ||||
| 			TestUtils.createDeviceCapability(band) | ||||
| 			TestUtils.createDeviceCapabilityPhy(band) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceB, | ||||
| 			TestUtils.createDeviceCapability(band) | ||||
| 			TestUtils.createDeviceCapabilityPhy(band) | ||||
| 		); | ||||
|  | ||||
| 		ChannelOptimizer optimizer = new RandomChannelInitializer( | ||||
| @@ -124,17 +124,17 @@ public class RandomChannelInitializerTest { | ||||
| 			deviceB, | ||||
| 			TestUtils.createDeviceStatus(band, 8) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceA, | ||||
| 			TestUtils.createDeviceCapability( | ||||
| 			TestUtils.createDeviceCapabilityPhy( | ||||
| 				new String[] { | ||||
| 					UCentralConstants.BAND_2G, | ||||
| 					UCentralConstants.BAND_2G } | ||||
| 			) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceB, | ||||
| 			TestUtils.createDeviceCapability( | ||||
| 			TestUtils.createDeviceCapabilityPhy( | ||||
| 				new String[] { | ||||
| 					UCentralConstants.BAND_2G, | ||||
| 					UCentralConstants.BAND_2G } | ||||
|   | ||||
| @@ -65,9 +65,9 @@ public class UnmanagedApAwareChannelOptimizerTest { | ||||
| 						.createState(aExpectedChannel, channelWidth, bssidA) | ||||
| 				) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceA, | ||||
| 			TestUtils.createDeviceCapability(band) | ||||
| 			TestUtils.createDeviceCapabilityPhy(band) | ||||
| 		); | ||||
| 		dataModel.latestWifiScans.put( | ||||
| 			deviceA, | ||||
| @@ -93,9 +93,9 @@ public class UnmanagedApAwareChannelOptimizerTest { | ||||
| 			deviceB, | ||||
| 			Arrays.asList(TestUtils.createState(40, channelWidth, bssidB)) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceB, | ||||
| 			TestUtils.createDeviceCapability(band) | ||||
| 			TestUtils.createDeviceCapabilityPhy(band) | ||||
| 		); | ||||
| 		dataModel.latestWifiScans.put( | ||||
| 			deviceB, | ||||
| @@ -131,9 +131,9 @@ public class UnmanagedApAwareChannelOptimizerTest { | ||||
| 			deviceC, | ||||
| 			Arrays.asList(TestUtils.createState(149, channelWidth, bssidC)) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceC, | ||||
| 			TestUtils.createDeviceCapability(band) | ||||
| 			TestUtils.createDeviceCapabilityPhy(band) | ||||
| 		); | ||||
| 		dataModel.latestWifiScans.put( | ||||
| 			deviceC, | ||||
| @@ -185,9 +185,9 @@ public class UnmanagedApAwareChannelOptimizerTest { | ||||
| 						.createState(aExpectedChannel, channelWidth, bssidA) | ||||
| 				) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceA, | ||||
| 			TestUtils.createDeviceCapability(band) | ||||
| 			TestUtils.createDeviceCapabilityPhy(band) | ||||
| 		); | ||||
| 		dataModel.latestWifiScans.put( | ||||
| 			deviceA, | ||||
| @@ -211,9 +211,9 @@ public class UnmanagedApAwareChannelOptimizerTest { | ||||
| 			deviceB, | ||||
| 			Arrays.asList(TestUtils.createState(6, channelWidth, bssidB)) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceB, | ||||
| 			TestUtils.createDeviceCapability(band) | ||||
| 			TestUtils.createDeviceCapabilityPhy(band) | ||||
| 		); | ||||
| 		dataModel.latestWifiScans.put( | ||||
| 			deviceB, | ||||
| @@ -233,9 +233,9 @@ public class UnmanagedApAwareChannelOptimizerTest { | ||||
| 			deviceC, | ||||
| 			Arrays.asList(TestUtils.createState(6, channelWidth, bssidC)) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceC, | ||||
| 			TestUtils.createDeviceCapability(band) | ||||
| 			TestUtils.createDeviceCapabilityPhy(band) | ||||
| 		); | ||||
| 		dataModel.latestWifiScans.put( | ||||
| 			deviceC, | ||||
|   | ||||
| @@ -0,0 +1,113 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| package com.facebook.openwifi.rrm.optimizers.clientsteering; | ||||
|  | ||||
| import static org.junit.jupiter.api.Assertions.assertFalse; | ||||
| import static org.junit.jupiter.api.Assertions.assertTrue; | ||||
|  | ||||
| import org.junit.jupiter.api.Test; | ||||
|  | ||||
| public class ClientSteeringStateTest { | ||||
|  | ||||
| 	@Test | ||||
| 	void testRegisterAndCheckBackoff() { | ||||
| 		final String apA = "aaaaaaaaaaaa"; | ||||
| 		final String clientA1 = "1a:aa:aa:aa:aa:aa"; | ||||
| 		final String clientA2 = "2a:aa:aa:aa:aa:aa"; | ||||
| 		final String apB = "bbbbbbbbbbbb"; | ||||
| 		final String clientB = "1b:bb:bb:bb:bb:bb"; | ||||
|  | ||||
| 		final long currentTimeNs = System.nanoTime(); | ||||
| 		final long bufferTimeNs = 60_000_000_000L; // 1 min | ||||
|  | ||||
| 		ClientSteeringState clientSteeringState = new ClientSteeringState(); | ||||
|  | ||||
| 		// first attempt should register | ||||
| 		assertTrue( | ||||
| 			clientSteeringState.registerIfBackoffExpired( | ||||
| 				apA, | ||||
| 				clientA1, | ||||
| 				currentTimeNs, | ||||
| 				bufferTimeNs, | ||||
| 				false // dryRun | ||||
| 			) | ||||
| 		); | ||||
| 		// should not register AP A & clientA1 again while backoff is in effect | ||||
| 		assertFalse( | ||||
| 			clientSteeringState.registerIfBackoffExpired( | ||||
| 				apA, | ||||
| 				clientA1, | ||||
| 				currentTimeNs + 1, | ||||
| 				bufferTimeNs, | ||||
| 				false // dryRun | ||||
| 			) | ||||
| 		); | ||||
| 		// one client's backoff should not affect another client | ||||
| 		assertTrue( | ||||
| 			clientSteeringState.registerIfBackoffExpired( | ||||
| 				apA, | ||||
| 				clientA2, | ||||
| 				currentTimeNs + 1, | ||||
| 				bufferTimeNs, | ||||
| 				false // dryRun | ||||
| 			) | ||||
| 		); | ||||
| 		// one AP should not affect another | ||||
| 		assertTrue( | ||||
| 			clientSteeringState.registerIfBackoffExpired( | ||||
| 				apB, | ||||
| 				clientB, | ||||
| 				currentTimeNs + 1, | ||||
| 				bufferTimeNs, | ||||
| 				false // dryRun | ||||
| 			) | ||||
| 		); | ||||
| 		// should re-register AP A & clientA1 after backoff has expired | ||||
| 		assertTrue( | ||||
| 			clientSteeringState.registerIfBackoffExpired( | ||||
| 				apA, | ||||
| 				clientA1, | ||||
| 				currentTimeNs + bufferTimeNs, | ||||
| 				bufferTimeNs, | ||||
| 				false // dryRun | ||||
| 			) | ||||
| 		); | ||||
| 		// an older timestamp should not register | ||||
| 		assertFalse( | ||||
| 			clientSteeringState.registerIfBackoffExpired( | ||||
| 				apB, | ||||
| 				clientB, | ||||
| 				currentTimeNs - 1, | ||||
| 				bufferTimeNs, | ||||
| 				false // dryRun | ||||
| 			) | ||||
| 		); | ||||
| 		// try a different backoffTimeNs | ||||
| 		assertTrue( | ||||
| 			clientSteeringState | ||||
| 				.registerIfBackoffExpired( | ||||
| 					apA, | ||||
| 					clientA2, | ||||
| 					currentTimeNs + 2, | ||||
| 					1, | ||||
| 					false /* dryRun */ | ||||
| 				) | ||||
| 		); | ||||
| 		// same client on a different AP should have separate timer | ||||
| 		assertTrue( | ||||
| 			clientSteeringState.registerIfBackoffExpired( | ||||
| 				apB, | ||||
| 				clientA2, | ||||
| 				currentTimeNs + 3, | ||||
| 				bufferTimeNs, | ||||
| 				false // dryRun | ||||
| 			) | ||||
| 		); | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,186 @@ | ||||
| /* | ||||
|  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * This source code is licensed under the BSD-style license found in the | ||||
|  * LICENSE file in the root directory of this source tree. | ||||
|  */ | ||||
|  | ||||
| package com.facebook.openwifi.rrm.optimizers.clientsteering; | ||||
|  | ||||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||||
|  | ||||
| import java.util.Arrays; | ||||
| import java.util.HashMap; | ||||
| import java.util.Map; | ||||
|  | ||||
| import org.junit.jupiter.api.Test; | ||||
|  | ||||
| import com.facebook.openwifi.cloudsdk.UCentralConstants; | ||||
| import com.facebook.openwifi.rrm.DeviceDataManager; | ||||
| import com.facebook.openwifi.rrm.modules.Modeler; | ||||
| import com.facebook.openwifi.rrm.optimizers.TestUtils; | ||||
| import com.facebook.openwifi.rrm.optimizers.clientsteering.ClientSteeringOptimizer.CLIENT_STEERING_ACTIONS; | ||||
|  | ||||
| public class SingleAPBandSteeringTest { | ||||
| 	// TODO test 6G also (should be treated same as 5G) | ||||
|  | ||||
| 	/** Test zone name. */ | ||||
| 	private static final String TEST_ZONE = "test-zone"; | ||||
|  | ||||
| 	// AP serial numbers | ||||
| 	private static final String apA = "aaaaaaaaaaaa"; | ||||
| 	private static final String apB = "bbbbbbbbbbbb"; | ||||
| 	private static final String apC = "cccccccccccc"; | ||||
|  | ||||
| 	// arrays are mutable, but these are private and this is just a test class | ||||
| 	// arrays are more convenient for constructing states | ||||
|  | ||||
| 	/** bssids for radios on AP A */ | ||||
| 	private static final String[] bssidsA = | ||||
| 		new String[] { "aa:aa:aa:aa:aa:a1", "aa:aa:aa:aa:aa:a2" }; | ||||
| 	/** bssids for radios on AP B */ | ||||
| 	private static final String[] bssidsB = | ||||
| 		new String[] { "bb:bb:bb:bb:bb:b1", "bb:bb:bb:bb:bb:b2" }; | ||||
| 	/** bssids for radios on AP C */ | ||||
| 	private static final String[] bssidsC = | ||||
| 		new String[] { "cc:cc:cc:cc:cc:c1", "cc:cc:cc:cc:cc:c2" }; | ||||
|  | ||||
| 	/** Array: each element is an array of client MACS for a radio on AP A */ | ||||
| 	private static final String[][] clientsA = | ||||
| 		new String[][] { new String[] { "1a:aa:aa:aa:aa:aa" }, | ||||
| 			new String[] { "2a:aa:aa:aa:aa:aa" } }; | ||||
| 	/** Array: each element is an array of client MACS for a radio on AP B */ | ||||
| 	private static final String[][] clientsB = | ||||
| 		new String[][] { new String[] { "1b:bb:bb:bb:bb:bb" }, | ||||
| 			new String[] { "2b:bb:bb:bb:bb:bb" } }; | ||||
| 	/** Array: each element is an array of client MACS for a radio on AP B */ | ||||
| 	private static final String[][] clientsC = | ||||
| 		new String[][] { new String[] { "1c:cc:cc:cc:cc:cc" }, | ||||
| 			new String[] { "2c:cc:cc:cc:cc:cc" } }; | ||||
|  | ||||
| 	/** Default channel width */ | ||||
| 	private static final int DEFAULT_CHANNEL_WIDTH = 20; | ||||
|  | ||||
| 	/** Default tx power */ | ||||
| 	private static final int DEFAULT_TX_POWER = 20; | ||||
|  | ||||
| 	/** Adds matching State and DeviceCapabilityPhy objects to the data model */ | ||||
| 	private void addStateAndCapability( | ||||
| 		Modeler.DataModel dataModel, | ||||
| 		String apSerialNumber, | ||||
| 		String[] bssids, | ||||
| 		String[][] clients, | ||||
| 		int[][] clientRssis | ||||
| 	) { | ||||
| 		dataModel.latestStates.put( | ||||
| 			apSerialNumber, | ||||
| 			Arrays.asList( | ||||
| 				TestUtils.createState( | ||||
| 					new int[] { 1, 36 }, | ||||
| 					new int[] { DEFAULT_CHANNEL_WIDTH, DEFAULT_CHANNEL_WIDTH }, | ||||
| 					new int[] { DEFAULT_TX_POWER, DEFAULT_TX_POWER }, | ||||
| 					bssids, | ||||
| 					clients, | ||||
| 					clientRssis, | ||||
| 					TestUtils.DEFAULT_LOCAL_TIME | ||||
| 				) | ||||
| 			) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			apSerialNumber, | ||||
| 			TestUtils.createDeviceCapabilityPhy( | ||||
| 				new String[] { | ||||
| 					UCentralConstants.BAND_2G, | ||||
| 					UCentralConstants.BAND_5G } | ||||
| 			) | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Creates a data model such that: | ||||
| 	 * Client on (AP A, radio 2G) -> should be deauthenticated | ||||
| 	 * Client on (AP A, radio 5G) -> should be steered down | ||||
| 	 * Client on (AP B, radio 2G) -> no action | ||||
| 	 * Client on (AP B, radio 5G) -> no action | ||||
| 	 * Client on (AP C, radio 2G) -> should be steered up | ||||
| 	 * Client on (AP C, radio 5G) -> no action | ||||
| 	 * | ||||
| 	 * @return the data model | ||||
| 	 */ | ||||
| 	private Modeler.DataModel createModel() { | ||||
| 		Modeler.DataModel dataModel = new Modeler.DataModel(); | ||||
| 		// AP A | ||||
| 		int[] clientRssis2G = | ||||
| 			new int[] { SingleAPBandSteering.DEFAULT_MIN_RSSI_2G - 1 }; // deauthenticate | ||||
| 		int[] clientRssis5G = | ||||
| 			new int[] { SingleAPBandSteering.DEFAULT_MIN_RSSI_NON_2G - 1 }; // steer down | ||||
| 		int[][] clientRssis = new int[][] { clientRssis2G, clientRssis5G }; | ||||
| 		addStateAndCapability( | ||||
| 			dataModel, | ||||
| 			apA, | ||||
| 			bssidsA, | ||||
| 			clientsA, | ||||
| 			clientRssis | ||||
| 		); | ||||
|  | ||||
| 		// AP B | ||||
| 		clientRssis2G = new int[] { SingleAPBandSteering.DEFAULT_MIN_RSSI_2G }; // do nothing | ||||
| 		clientRssis5G = | ||||
| 			new int[] { SingleAPBandSteering.DEFAULT_MIN_RSSI_NON_2G }; // do nothing | ||||
| 		clientRssis = new int[][] { clientRssis2G, clientRssis5G }; | ||||
| 		addStateAndCapability( | ||||
| 			dataModel, | ||||
| 			apB, | ||||
| 			bssidsB, | ||||
| 			clientsB, | ||||
| 			clientRssis | ||||
| 		); | ||||
|  | ||||
| 		// AP C | ||||
| 		clientRssis2G = | ||||
| 			new int[] { SingleAPBandSteering.DEFAULT_MAX_RSSI_2G + 1 }; // steer up | ||||
| 		clientRssis5G = | ||||
| 			new int[] { SingleAPBandSteering.DEFAULT_MIN_RSSI_NON_2G }; // do nothing | ||||
| 		clientRssis = new int[][] { clientRssis2G, clientRssis5G }; | ||||
| 		addStateAndCapability( | ||||
| 			dataModel, | ||||
| 			apC, | ||||
| 			bssidsC, | ||||
| 			clientsC, | ||||
| 			clientRssis | ||||
| 		); | ||||
|  | ||||
| 		return dataModel; | ||||
| 	} | ||||
|  | ||||
| 	@Test | ||||
| 	void testComputeApClientActionMap() { | ||||
| 		DeviceDataManager deviceDataManager = new DeviceDataManager(); | ||||
| 		deviceDataManager | ||||
| 			.setTopology(TestUtils.createTopology(TEST_ZONE, apA, apB, apC)); | ||||
| 		Modeler.DataModel dataModel = createModel(); | ||||
| 		// create expected results | ||||
| 		// see javadoc of createModel for more details | ||||
| 		Map<String, Map<String, String>> exp = new HashMap<>(); | ||||
| 		Map<String, String> apAMap = new HashMap<>(); | ||||
| 		apAMap | ||||
| 			.put(clientsA[0][0], CLIENT_STEERING_ACTIONS.DEAUTHENTICATE.name()); | ||||
| 		apAMap.put(clientsA[1][0], CLIENT_STEERING_ACTIONS.STEER_DOWN.name()); | ||||
| 		exp.put(apA, apAMap); | ||||
| 		// no action for AP B | ||||
| 		Map<String, String> apCMap = new HashMap<>(); | ||||
| 		apCMap.put(clientsC[0][0], CLIENT_STEERING_ACTIONS.STEER_UP.name()); | ||||
| 		exp.put(apC, apCMap); | ||||
| 		SingleAPBandSteering optimizer = SingleAPBandSteering.makeWithArgs( | ||||
| 			dataModel, | ||||
| 			TEST_ZONE, | ||||
| 			deviceDataManager, | ||||
| 			new ClientSteeringState(), | ||||
| 			new HashMap<>(0) // args (use default) | ||||
| 		); | ||||
| 		Map<String, Map<String, String>> apClientActionMap = | ||||
| 			optimizer.computeApClientActionMap(false /* dryRun */); | ||||
| 		assertEquals(exp, apClientActionMap); | ||||
| 	} | ||||
| } | ||||
| @@ -145,9 +145,9 @@ public class LocationBasedOptimalTPCTest { | ||||
| 					) | ||||
| 				) | ||||
| 			); | ||||
| 			dataModel.latestDeviceCapabilities.put( | ||||
| 			dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 				device, | ||||
| 				TestUtils.createDeviceCapability( | ||||
| 				TestUtils.createDeviceCapabilityPhy( | ||||
| 					new String[] { | ||||
| 						UCentralConstants.BAND_2G, | ||||
| 						UCentralConstants.BAND_5G } | ||||
| @@ -222,9 +222,9 @@ public class LocationBasedOptimalTPCTest { | ||||
| 					) | ||||
| 				) | ||||
| 			); | ||||
| 			dataModel2.latestDeviceCapabilities.put( | ||||
| 			dataModel2.latestDeviceCapabilitiesPhy.put( | ||||
| 				device, | ||||
| 				TestUtils.createDeviceCapability( | ||||
| 				TestUtils.createDeviceCapabilityPhy( | ||||
| 					new String[] { | ||||
| 						UCentralConstants.BAND_2G, | ||||
| 						UCentralConstants.BAND_5G } | ||||
| @@ -249,9 +249,9 @@ public class LocationBasedOptimalTPCTest { | ||||
| 				) | ||||
| 			) | ||||
| 		); | ||||
| 		dataModel2.latestDeviceCapabilities.put( | ||||
| 		dataModel2.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceC, | ||||
| 			TestUtils.createDeviceCapability(UCentralConstants.BAND_5G) | ||||
| 			TestUtils.createDeviceCapabilityPhy(UCentralConstants.BAND_5G) | ||||
| 		); | ||||
|  | ||||
| 		Map<String, Map<String, Integer>> expected2 = new HashMap<>(); | ||||
| @@ -347,9 +347,9 @@ public class LocationBasedOptimalTPCTest { | ||||
| 					) | ||||
| 				) | ||||
| 			); | ||||
| 			dataModel2.latestDeviceCapabilities.put( | ||||
| 			dataModel2.latestDeviceCapabilitiesPhy.put( | ||||
| 				device, | ||||
| 				TestUtils.createDeviceCapability( | ||||
| 				TestUtils.createDeviceCapabilityPhy( | ||||
| 					new String[] { | ||||
| 						UCentralConstants.BAND_2G, | ||||
| 						UCentralConstants.BAND_5G } | ||||
| @@ -416,9 +416,9 @@ public class LocationBasedOptimalTPCTest { | ||||
| 					) | ||||
| 				) | ||||
| 			); | ||||
| 			dataModel3.latestDeviceCapabilities.put( | ||||
| 			dataModel3.latestDeviceCapabilitiesPhy.put( | ||||
| 				device, | ||||
| 				TestUtils.createDeviceCapability( | ||||
| 				TestUtils.createDeviceCapabilityPhy( | ||||
| 					new String[] { | ||||
| 						UCentralConstants.BAND_2G, | ||||
| 						UCentralConstants.BAND_5G } | ||||
| @@ -475,9 +475,9 @@ public class LocationBasedOptimalTPCTest { | ||||
| 					) | ||||
| 				) | ||||
| 			); | ||||
| 			dataModel4.latestDeviceCapabilities.put( | ||||
| 			dataModel4.latestDeviceCapabilitiesPhy.put( | ||||
| 				device, | ||||
| 				TestUtils.createDeviceCapability( | ||||
| 				TestUtils.createDeviceCapabilityPhy( | ||||
| 					new String[] { | ||||
| 						UCentralConstants.BAND_2G, | ||||
| 						UCentralConstants.BAND_2G } | ||||
|   | ||||
| @@ -108,9 +108,9 @@ public class MeasurementBasedApApTPCTest { | ||||
| 				TestUtils | ||||
| 					.createDeviceStatusSingleBand(channel, MAX_TX_POWER) | ||||
| 			); | ||||
| 			model.latestDeviceCapabilities.put( | ||||
| 			model.latestDeviceCapabilitiesPhy.put( | ||||
| 				device, | ||||
| 				TestUtils.createDeviceCapability(band) | ||||
| 				TestUtils.createDeviceCapabilityPhy(band) | ||||
| 			); | ||||
| 		} | ||||
|  | ||||
| @@ -161,9 +161,9 @@ public class MeasurementBasedApApTPCTest { | ||||
| 						MAX_TX_POWER | ||||
| 					) | ||||
| 			); | ||||
| 			model.latestDeviceCapabilities.put( | ||||
| 			model.latestDeviceCapabilitiesPhy.put( | ||||
| 				device, | ||||
| 				TestUtils.createDeviceCapability( | ||||
| 				TestUtils.createDeviceCapabilityPhy( | ||||
| 					new String[] { | ||||
| 						UCentralConstants.BAND_2G, | ||||
| 						UCentralConstants.BAND_5G } | ||||
| @@ -604,9 +604,9 @@ public class MeasurementBasedApApTPCTest { | ||||
| 				MAX_TX_POWER | ||||
| 			) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			DEVICE_C, | ||||
| 			TestUtils.createDeviceCapability(UCentralConstants.BAND_2G) | ||||
| 			TestUtils.createDeviceCapabilityPhy(UCentralConstants.BAND_2G) | ||||
| 		); | ||||
| 		optimizer = new MeasurementBasedApApTPC( | ||||
| 			dataModel, | ||||
|   | ||||
| @@ -61,9 +61,9 @@ public class MeasurementBasedApClientTPCTest { | ||||
| 				TestUtils.createState(36, 20, 20, null, new int[] {}) | ||||
| 			) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceA, | ||||
| 			TestUtils.createDeviceCapability(UCentralConstants.BAND_5G) | ||||
| 			TestUtils.createDeviceCapabilityPhy(UCentralConstants.BAND_5G) | ||||
| 		); | ||||
| 		dataModel.latestStates.put( | ||||
| 			deviceB, | ||||
| @@ -71,9 +71,9 @@ public class MeasurementBasedApClientTPCTest { | ||||
| 				TestUtils.createState(36, 20, 20, "", new int[] { -65 }) | ||||
| 			) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceB, | ||||
| 			TestUtils.createDeviceCapability(UCentralConstants.BAND_5G) | ||||
| 			TestUtils.createDeviceCapabilityPhy(UCentralConstants.BAND_5G) | ||||
| 		); | ||||
| 		dataModel.latestStates.put( | ||||
| 			deviceC, | ||||
| @@ -87,9 +87,9 @@ public class MeasurementBasedApClientTPCTest { | ||||
| 				) | ||||
| 			) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceC, | ||||
| 			TestUtils.createDeviceCapability(UCentralConstants.BAND_5G) | ||||
| 			TestUtils.createDeviceCapabilityPhy(UCentralConstants.BAND_5G) | ||||
| 		); | ||||
| 		dataModel.latestStates.put( | ||||
| 			deviceD, | ||||
| @@ -97,9 +97,9 @@ public class MeasurementBasedApClientTPCTest { | ||||
| 				TestUtils.createState(36, 20, 22, null, new int[] { -80 }) | ||||
| 			) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceD, | ||||
| 			TestUtils.createDeviceCapability(UCentralConstants.BAND_5G) | ||||
| 			TestUtils.createDeviceCapabilityPhy(UCentralConstants.BAND_5G) | ||||
| 		); | ||||
| 		dataModel.latestStates.put( | ||||
| 			deviceE, | ||||
| @@ -107,9 +107,9 @@ public class MeasurementBasedApClientTPCTest { | ||||
| 				TestUtils.createState(36, 20, 23, null, new int[] { -45 }) | ||||
| 			) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceE, | ||||
| 			TestUtils.createDeviceCapability(UCentralConstants.BAND_5G) | ||||
| 			TestUtils.createDeviceCapabilityPhy(UCentralConstants.BAND_5G) | ||||
| 		); | ||||
|  | ||||
| 		TPC optimizer = new MeasurementBasedApClientTPC( | ||||
| @@ -178,9 +178,9 @@ public class MeasurementBasedApClientTPCTest { | ||||
| 				TestUtils.createState(1, 20, 20, null, new int[] {}) | ||||
| 			) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceA, | ||||
| 			TestUtils.createDeviceCapability(UCentralConstants.BAND_2G) | ||||
| 			TestUtils.createDeviceCapabilityPhy(UCentralConstants.BAND_2G) | ||||
| 		); | ||||
| 		// 5G only | ||||
| 		dataModel.latestStates.put( | ||||
| @@ -189,9 +189,9 @@ public class MeasurementBasedApClientTPCTest { | ||||
| 				TestUtils.createState(36, 20, 20, null, new int[] {}) | ||||
| 			) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceB, | ||||
| 			TestUtils.createDeviceCapability(UCentralConstants.BAND_5G) | ||||
| 			TestUtils.createDeviceCapabilityPhy(UCentralConstants.BAND_5G) | ||||
| 		); | ||||
| 		// 2G and 5G | ||||
| 		dataModel.latestStates.put( | ||||
| @@ -209,9 +209,9 @@ public class MeasurementBasedApClientTPCTest { | ||||
| 				) | ||||
| 			) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceC, | ||||
| 			TestUtils.createDeviceCapability( | ||||
| 			TestUtils.createDeviceCapabilityPhy( | ||||
| 				new String[] { | ||||
| 					UCentralConstants.BAND_2G, | ||||
| 					UCentralConstants.BAND_5G } | ||||
| @@ -284,9 +284,9 @@ public class MeasurementBasedApClientTPCTest { | ||||
| 				TestUtils.createState(36, 20, 20, null, new int[] {}) | ||||
| 			) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceA, | ||||
| 			TestUtils.createDeviceCapability(UCentralConstants.BAND_5G) | ||||
| 			TestUtils.createDeviceCapabilityPhy(UCentralConstants.BAND_5G) | ||||
| 		); | ||||
| 		dataModel.latestStates.put( | ||||
| 			deviceB, | ||||
| @@ -294,9 +294,9 @@ public class MeasurementBasedApClientTPCTest { | ||||
| 				TestUtils.createState(36, 20, 20, "", new int[] { -65 }) | ||||
| 			) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceB, | ||||
| 			TestUtils.createDeviceCapability(UCentralConstants.BAND_5G) | ||||
| 			TestUtils.createDeviceCapabilityPhy(UCentralConstants.BAND_5G) | ||||
| 		); | ||||
| 		dataModel.latestStates.put( | ||||
| 			deviceC, | ||||
| @@ -310,9 +310,9 @@ public class MeasurementBasedApClientTPCTest { | ||||
| 				) | ||||
| 			) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			deviceC, | ||||
| 			TestUtils.createDeviceCapability(UCentralConstants.BAND_5G) | ||||
| 			TestUtils.createDeviceCapabilityPhy(UCentralConstants.BAND_5G) | ||||
| 		); | ||||
|  | ||||
| 		TPC optimizer = new MeasurementBasedApClientTPC( | ||||
|   | ||||
| @@ -78,9 +78,9 @@ public class RandomTxPowerInitializerTest { | ||||
| 				) | ||||
| 			) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			DEVICE_A, | ||||
| 			TestUtils.createDeviceCapability( | ||||
| 			TestUtils.createDeviceCapabilityPhy( | ||||
| 				new String[] { | ||||
| 					UCentralConstants.BAND_5G, | ||||
| 					UCentralConstants.BAND_2G } | ||||
| @@ -97,9 +97,9 @@ public class RandomTxPowerInitializerTest { | ||||
| 				) | ||||
| 			) | ||||
| 		); | ||||
| 		dataModel.latestDeviceCapabilities.put( | ||||
| 		dataModel.latestDeviceCapabilitiesPhy.put( | ||||
| 			DEVICE_B, | ||||
| 			TestUtils.createDeviceCapability(UCentralConstants.BAND_2G) | ||||
| 			TestUtils.createDeviceCapabilityPhy(UCentralConstants.BAND_2G) | ||||
| 		); | ||||
| 		return dataModel; | ||||
| 	} | ||||
|   | ||||
							
								
								
									
										7
									
								
								pom.xml
									
									
									
									
									
								
							
							
						
						
									
										7
									
								
								pom.xml
									
									
									
									
									
								
							| @@ -7,6 +7,7 @@ | ||||
|   <packaging>pom</packaging> | ||||
|   <modules> | ||||
|     <module>lib-cloudsdk</module> | ||||
|     <module>lib-rca</module> | ||||
|     <module>owrrm</module> | ||||
|   </modules> | ||||
|   <properties> | ||||
| @@ -77,6 +78,7 @@ | ||||
|           <configuration> | ||||
|             <doclint>all,-missing</doclint> | ||||
|             <notimestamp>true</notimestamp> | ||||
|             <overview>${basedir}/owrrm/src/main/javadoc/overview.html</overview> | ||||
|             <bottom>Copyright © Meta Platforms, Inc. and affiliates.</bottom> | ||||
|           </configuration> | ||||
|         </plugin> | ||||
| @@ -211,11 +213,6 @@ | ||||
|         <artifactId>quartz</artifactId> | ||||
|         <version>2.3.2</version> | ||||
|       </dependency> | ||||
|     <dependency> | ||||
|       <groupId>org.openjdk.jol</groupId> | ||||
|       <artifactId>jol-core</artifactId> | ||||
|       <version>0.16</version> | ||||
|     </dependency> | ||||
|     </dependencies> | ||||
|   </dependencyManagement> | ||||
| </project> | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user