Files
wlan-cloud-rrm/src/main/java/com/facebook/openwifirrm/modules/ConfigManager.java
zhiqiand 52dae760d8 fix some comments
Signed-off-by: zhiqiand <zhiqian@fb.com>
2022-10-04 16:08:03 -07:00

418 lines
11 KiB
Java

/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.modules;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.facebook.openwifirrm.DeviceConfig;
import com.facebook.openwifirrm.DeviceDataManager;
import com.facebook.openwifirrm.RRMConfig.ModuleConfig.ConfigManagerParams;
import com.facebook.openwifirrm.ucentral.UCentralApConfiguration;
import com.facebook.openwifirrm.ucentral.UCentralClient;
import com.facebook.openwifirrm.ucentral.UCentralUtils;
import com.facebook.openwifirrm.ucentral.gw.models.DeviceWithStatus;
/**
* Device configuration manager module.
*/
public class ConfigManager implements Runnable {
private static final Logger logger =
LoggerFactory.getLogger(ConfigManager.class);
/** The module parameters. */
private final ConfigManagerParams params;
/** The device data manager. */
private final DeviceDataManager deviceDataManager;
/** The uCentral client. */
private final UCentralClient client;
/** Runtime per-device data. */
private class DeviceData {
/** Last received device config. */
public UCentralApConfiguration config;
/** Last config time (in monotonic ns). */
public Long lastConfigTimeNs;
}
/** Map from device serial number to runtime data. */
private Map<String, DeviceData> deviceDataMap = new TreeMap<>();
/** The main thread reference (i.e. where {@link #run()} is invoked). */
private Thread mainThread;
/** Was the main thread interrupt generated by {@link #wakeUp()}? */
private final AtomicBoolean wakeupFlag = new AtomicBoolean(false);
/** Is the main thread sleeping? */
private final AtomicBoolean sleepingFlag = new AtomicBoolean(false);
/**
* Thread-safe set of zones for which manual config updates have been
* requested.
*/
private Set<String> zonesToUpdate = ConcurrentHashMap.newKeySet();
/** Config listener interface. */
public interface ConfigListener {
/**
* Receive a new device config.
*
* The listener should NOT modify the "config" parameter.
*/
void receiveDeviceConfig(
String serialNumber,
UCentralApConfiguration config
);
/**
* Process a received device config.
*
* The listener should modify the "config" parameter as necessary and
* return true if any changes were made, otherwise return false.
*/
boolean processDeviceConfig(
String serialNumber,
UCentralApConfiguration config
);
}
/** State listeners. */
private Map<String, ConfigListener> configListeners = new TreeMap<>();
/** Constructor. */
public ConfigManager(
ConfigManagerParams params,
DeviceDataManager deviceDataManager,
UCentralClient client
) {
this.params = params;
this.deviceDataManager = deviceDataManager;
this.client = client;
// Apply RRM parameters
addConfigListener(
getClass().getSimpleName(),
new ConfigListener() {
@Override
public void receiveDeviceConfig(
String serialNumber,
UCentralApConfiguration config
) {
// do nothing
}
@Override
public boolean processDeviceConfig(
String serialNumber,
UCentralApConfiguration config
) {
return applyRRMConfig(serialNumber, config);
}
}
);
}
@Override
public void run() {
this.mainThread = Thread.currentThread();
// Run application logic in a periodic loop
while (!Thread.currentThread().isInterrupted()) {
try {
runImpl();
sleepingFlag.set(true);
Thread.sleep(params.updateIntervalMs);
wakeupFlag.set(false);
} catch (InterruptedException e) {
if (wakeupFlag.getAndSet(false)) {
// Intentional interrupt, clear flag
logger.debug("Interrupted by wakeup!");
Thread.interrupted();
continue;
} else {
logger.error("Interrupted!", e);
break;
}
} finally {
sleepingFlag.set(false);
}
}
logger.error("Thread terminated!");
}
/** Run single iteration of application logic. */
private void runImpl() {
while (!client.isInitialized()) {
logger.trace("Waiting for ucentral client");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
client.refreshAccessToken();
// Fetch device list
List<DeviceWithStatus> devices = client.getDevices();
if (devices == null) {
logger.error("Failed to fetch devices!");
return;
}
logger.debug("Received device list of size = {}", devices.size());
long now = System.nanoTime();
// Apply any config updates locally
List<String> devicesNeedingUpdate = new ArrayList<>();
final long CONFIG_DEBOUNCE_INTERVAL_NS =
params.configDebounceIntervalSec * 1_000_000_000L;
Set<String> zonesToUpdateCopy = new HashSet<>(zonesToUpdate);
// use removeAll() instead of clear() in case items are added between
// the previous line and the following line
zonesToUpdate.removeAll(zonesToUpdateCopy);
for (DeviceWithStatus device : devices) {
// Update config structure
DeviceData data = deviceDataMap.computeIfAbsent(
device.serialNumber,
k -> new DeviceData()
);
// Update the device only when it is still connected
if (!device.connected) {
logger.info(
"Device {} is disconnected, skipping...",
device.serialNumber
);
continue;
}
data.config = new UCentralApConfiguration(device.configuration);
// Call receive listeners
for (ConfigListener listener : configListeners.values()) {
listener.receiveDeviceConfig(device.serialNumber, data.config);
}
// Check if there are requested updates for this zone
String deviceZone =
deviceDataManager.getDeviceZone(device.serialNumber);
boolean isEvent = zonesToUpdateCopy.contains(deviceZone);
if (params.configOnEventOnly && !isEvent) {
logger.debug(
"Skipping config for {} (zone not marked for updates)",
device.serialNumber
);
continue;
}
// Check if pushing config is enabled in device config
if (!isDeviceConfigEnabled(device.serialNumber)) {
logger.debug(
"Skipping config for {} (disabled in device config)",
device.serialNumber
);
continue;
}
// Call processing listeners
boolean modified = false;
for (ConfigListener listener : configListeners.values()) {
boolean wasModified = listener.processDeviceConfig(
device.serialNumber,
data.config
);
if (wasModified) {
modified = true;
}
}
// Queue config update if past debounce interval
if (modified) {
if (
data.lastConfigTimeNs != null &&
now - data.lastConfigTimeNs <
CONFIG_DEBOUNCE_INTERVAL_NS
) {
logger.debug(
"Skipping config for {} (last configured {}s ago)",
device.serialNumber,
(now - data.lastConfigTimeNs) / 1_000_000_000L
);
continue;
} else {
devicesNeedingUpdate.add(device.serialNumber);
}
}
}
final boolean shouldUpdate = !zonesToUpdateCopy.isEmpty();
// Send config changes to devices
if (!params.configEnabled) {
logger.trace("Config changes are disabled.");
} else if (devicesNeedingUpdate.isEmpty()) {
logger.debug("No device configs to send.");
} else if (params.configOnEventOnly && !shouldUpdate) {
// shouldn't happen
logger.error(
"ERROR!! {} device(s) queued for config update, but no zones queued for update.",
devicesNeedingUpdate.size()
);
} else {
logger.info(
"Sending config to {} device(s): {}",
devicesNeedingUpdate.size(),
String.join(", ", devicesNeedingUpdate)
);
for (String serialNumber : devicesNeedingUpdate) {
DeviceData data = deviceDataMap.get(serialNumber);
logger.info(
"Device {}: sending new configuration...",
serialNumber
);
data.lastConfigTimeNs = System.nanoTime();
client.configure(serialNumber, data.config.toString());
}
}
}
/** Return whether the given device has pushing device config enabled. */
private boolean isDeviceConfigEnabled(String serialNumber) {
DeviceConfig deviceConfig =
deviceDataManager.getDeviceConfig(serialNumber);
if (deviceConfig == null) {
return false;
}
return deviceConfig.enableConfig;
}
/**
* Apply RRM parameters to the device.
*
* If any changes are needed, modify the config and return true.
* Otherwise, return false.
*/
private boolean applyRRMConfig(
String serialNumber,
UCentralApConfiguration config
) {
DeviceConfig deviceConfig =
deviceDataManager.getDeviceConfig(serialNumber);
if (deviceConfig == null || !deviceConfig.enableRRM) {
return false;
}
boolean modified = false;
// Apply channel config
Map<String, Integer> channelList = new HashMap<>();
if (deviceConfig.autoChannels != null) {
channelList.putAll(deviceConfig.autoChannels);
}
if (deviceConfig.userChannels != null) {
channelList.putAll(deviceConfig.userChannels);
}
if (!channelList.isEmpty()) {
modified |= UCentralUtils.setRadioConfigField(
serialNumber,
config,
"channel",
channelList
);
}
// Apply tx power config
Map<String, Integer> txPowerList = new HashMap<>();
if (deviceConfig.autoTxPowers != null) {
txPowerList.putAll(deviceConfig.autoTxPowers);
}
if (deviceConfig.userTxPowers != null) {
txPowerList.putAll(deviceConfig.userTxPowers);
}
if (!txPowerList.isEmpty()) {
modified |= UCentralUtils.setRadioConfigField(
serialNumber,
config,
"tx-power",
txPowerList
);
}
return modified;
}
/**
* Add/overwrite a config listener with an arbitrary identifier.
*
* The "id" string determines the order in which listeners are called.
*/
public void addConfigListener(String id, ConfigListener listener) {
logger.debug("Adding config listener: {}", id);
configListeners.put(id, listener);
}
/**
* Remove a config listener with the given identifier, returning true if
* anything was actually removed.
*/
public boolean removeConfigListener(String id) {
logger.debug("Removing config listener: {}", id);
return (configListeners.remove(id) != null);
}
/**
* Mark the zone to be updated, then interrupt the main thread to possibly
* trigger an update immediately.
*
* @param zone non-null zone (i.e., venue)
*/
public void queueZoneAndWakeUp(String zone) {
if (zone == null) {
logger.debug("Zone to queue must be a non-null String.");
return;
}
zonesToUpdate.add(zone);
wakeUp();
}
/**
* Track all zones to be updated, then interrupt the main thread to possibly
* trigger an update immediately.
*/
public void queueAllZonesAndWakeUp() {
/*
* Note, addAll is not atomic, but that is ok. This just means that it
* is possible that some zones may get updated now by the main thread
* while others get updated either when the main thread is woken up or
* the next time the main thread does its periodic update.
*/
zonesToUpdate.addAll(deviceDataManager.getZones());
wakeUp();
}
/** Interrupt the main thread to possibly trigger an update immediately. */
private void wakeUp() {
if (mainThread != null && mainThread.isAlive() && sleepingFlag.get()) {
wakeupFlag.set(true);
mainThread.interrupt();
}
}
}