![JAR search and dependency download from the Maven repository](/logo.png)
org.sputnikdev.bluetooth.manager.impl.CombinedDeviceGovernorImpl Maven / Gradle / Ivy
package org.sputnikdev.bluetooth.manager.impl;
/*-
* #%L
* org.sputnikdev:bluetooth-manager
* %%
* Copyright (C) 2017 Sputnik Dev
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sputnikdev.bluetooth.Filter;
import org.sputnikdev.bluetooth.RssiKalmanFilter;
import org.sputnikdev.bluetooth.URL;
import org.sputnikdev.bluetooth.manager.AdapterDiscoveryListener;
import org.sputnikdev.bluetooth.manager.BluetoothObjectType;
import org.sputnikdev.bluetooth.manager.BluetoothObjectVisitor;
import org.sputnikdev.bluetooth.manager.BluetoothSmartDeviceListener;
import org.sputnikdev.bluetooth.manager.CharacteristicGovernor;
import org.sputnikdev.bluetooth.manager.CombinedDeviceGovernor;
import org.sputnikdev.bluetooth.manager.ConnectionStrategy;
import org.sputnikdev.bluetooth.manager.DeviceGovernor;
import org.sputnikdev.bluetooth.manager.DiscoveredAdapter;
import org.sputnikdev.bluetooth.manager.GattCharacteristic;
import org.sputnikdev.bluetooth.manager.GattService;
import org.sputnikdev.bluetooth.manager.GenericBluetoothDeviceListener;
import org.sputnikdev.bluetooth.manager.GovernorListener;
import org.sputnikdev.bluetooth.manager.NotReadyException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
/**
*
* @author Vlad Kolotov
*/
class CombinedDeviceGovernorImpl implements DeviceGovernor, CombinedDeviceGovernor, BluetoothObjectGovernor {
// when RSSI reading is deemed to be stale for the nearest adapter calculation
private static final int STALE_TIMEOUT = 10000;
private Logger logger = LoggerFactory.getLogger(DeviceGovernorImpl.class);
private final BluetoothManagerImpl bluetoothManager;
private final URL url;
private final AtomicInteger governorsCount = new AtomicInteger();
private final Map governors = new ConcurrentHashMap<>();
private final DelegateRegistrar delegateRegistrar = new DelegateRegistrar();
private CompletableFuture> initTask;
// proxy listeners
private final List governorListeners = new CopyOnWriteArrayList<>();
private final List genericBluetoothDeviceListeners = new CopyOnWriteArrayList<>();
private final List bluetoothSmartDeviceListeners = new CopyOnWriteArrayList<>();
// state bitmap fields
private final ConcurrentBitMap ready = new ConcurrentBitMap();
private final ConcurrentBitMap online = new ConcurrentBitMap();
private final ConcurrentBitMap blocked = new ConcurrentBitMap();
private final ConcurrentBitMap connected = new ConcurrentBitMap();
private final ConcurrentBitMap servicesResolved = new ConcurrentBitMap();
// common state fields
private int bluetoothClass;
private boolean bleEnabled;
private String name;
private String alias;
private int onlineTimeout = DeviceGovernorImpl.DEFAULT_ONLINE_TIMEOUT;
private short rssi;
private KalmanFilterProxy rssiFilter = new KalmanFilterProxy();
private boolean rssiFilteringEnabled = true;
private long rssiReportingRate = DeviceGovernorImpl.DEFAULT_RSSI_REPORTING_RATE;
private short measuredTxPower;
private double signalPropagationExponent = DeviceGovernorImpl.DEFAULT_SIGNAL_PROPAGATION_EXPONENT;
private Date lastChanged;
// some specifics for the nearest adapter detection
private final SortedSet sortedByDistanceGovernors = new TreeSet<>(new DistanceComparator());
private DeviceGovernor nearest;
private final ReentrantLock rssiLock = new ReentrantLock();
// controlling fields
private boolean connectionControl;
private boolean blockedControl;
// combined governor specific fields
private ConnectionStrategy connectionStrategy = ConnectionStrategy.NEAREST_ADAPTER;
private URL preferredAdapter;
private DeviceGovernor connectionTarget;
CombinedDeviceGovernorImpl(BluetoothManagerImpl bluetoothManager, URL url) {
this.bluetoothManager = bluetoothManager;
this.url = url;
}
@Override
public int getBluetoothClass() throws NotReadyException {
return bluetoothClass;
}
@Override
public boolean isBleEnabled() throws NotReadyException {
return bleEnabled;
}
@Override
public String getName() throws NotReadyException {
return name != null ? name : url.getDeviceAddress();
}
@Override
public String getAlias() throws NotReadyException {
return alias;
}
@Override
public void setAlias(String alias) throws NotReadyException {
governors.values().forEach(deviceGovernorHandler -> {
if (deviceGovernorHandler.delegate.isReady()) {
deviceGovernorHandler.delegate.setAlias(alias);
}
});
}
@Override
public String getDisplayName() throws NotReadyException {
String alias = getAlias();
return alias != null ? alias : getName();
}
@Override
public boolean isConnected() throws NotReadyException {
return connected.get();
}
@Override
public boolean getConnectionControl() {
return connectionControl;
}
@Override
public void setConnectionControl(boolean connected) {
connectionControl = connected;
if (connected) {
updateConnectionTarget();
} else {
// make sure nothing sets connectionTarget and calls setConnectionControls
synchronized (this.connected) {
governors.values().forEach(deviceGovernorHandler -> deviceGovernorHandler.delegate
.setConnectionControl(false));
}
}
}
@Override
public boolean isServicesResolved() {
return servicesResolved.get();
}
@Override
public List getResolvedServices() throws NotReadyException {
DeviceGovernor deviceGovernor = getGovernor(servicesResolved.getUniqueIndex());
return deviceGovernor != null ? deviceGovernor.getResolvedServices() : null;
}
private void updateConnectionTarget() {
boolean connectionControl = this.connectionControl;
// make sure nothing sets connectionTarget and calls setConnectionControls
synchronized (connected) {
if (!isConnected()) {
DeviceGovernor newTarget = determineConnectionTarget();
if (connectionTarget != null && !connectionTarget.equals(newTarget)) {
connectionTarget.setConnectionControl(false);
}
connectionTarget = newTarget;
if (connectionTarget != null) {
connectionTarget.setConnectionControl(connectionControl);
}
}
}
}
private DeviceGovernor determineConnectionTarget() {
switch (connectionStrategy) {
case NEAREST_ADAPTER:
return nearest;
case PREFERRED_ADAPTER:
if (preferredAdapter != null) {
DeviceGovernorHandler preferredHandler = governors.get(
preferredAdapter.copyWithProtocol(null).copyWithDevice(url.getDeviceAddress()));
if (preferredHandler != null) {
return preferredHandler.delegate;
}
}
return null;
default: throw new IllegalStateException("Unknown connection strategy: " + connectionStrategy);
}
}
@Override
public boolean isBlocked() throws NotReadyException {
return blocked.get();
}
@Override
public boolean getBlockedControl() {
return blockedControl;
}
@Override
public void setBlockedControl(boolean blocked) {
blockedControl = blocked;
governors.values().forEach(
deviceGovernorHandler -> deviceGovernorHandler.delegate.setBlockedControl(blocked));
}
@Override
public boolean isOnline() {
return online.get();
}
@Override
public int getOnlineTimeout() {
return onlineTimeout;
}
@Override
public void setOnlineTimeout(int timeout) {
onlineTimeout = timeout;
governors.values().forEach(
deviceGovernorHandler -> deviceGovernorHandler.delegate.setOnlineTimeout(timeout));
}
@Override
public short getRSSI() throws NotReadyException {
return rssi;
}
@Override
public long getLastAdvertised() {
DeviceGovernor nearest = this.nearest;
return nearest.getLastAdvertised();
}
@Override
public short getTxPower() {
return 0;
}
@Override
public short getMeasuredTxPower() {
return measuredTxPower;
}
@Override
public void setMeasuredTxPower(short txPower) {
measuredTxPower = txPower;
governors.values().forEach(
deviceGovernorHandler -> deviceGovernorHandler.delegate.setMeasuredTxPower(txPower));
}
@Override
public double getSignalPropagationExponent() {
return signalPropagationExponent;
}
@Override
public void setSignalPropagationExponent(double exponent) {
signalPropagationExponent = exponent;
governors.values().forEach(
deviceGovernorHandler -> deviceGovernorHandler.delegate.setSignalPropagationExponent(exponent));
}
@Override
public double getEstimatedDistance() {
DeviceGovernor governor = nearest;
return governor != null ? governor.getEstimatedDistance() : 0.0;
}
@Override
public URL getLocation() {
DeviceGovernor governor = nearest;
return !isConnected() && governor != null ? governor.getURL().getAdapterURL() : null;
}
@Override
public void addBluetoothSmartDeviceListener(BluetoothSmartDeviceListener listener) {
bluetoothSmartDeviceListeners.add(listener);
}
@Override
public void removeBluetoothSmartDeviceListener(BluetoothSmartDeviceListener listener) {
bluetoothSmartDeviceListeners.remove(listener);
}
@Override
public void addGenericBluetoothDeviceListener(GenericBluetoothDeviceListener listener) {
genericBluetoothDeviceListeners.add(listener);
}
@Override
public void removeGenericBluetoothDeviceListener(GenericBluetoothDeviceListener listener) {
genericBluetoothDeviceListeners.remove(listener);
}
@Override
public void addGovernorListener(GovernorListener listener) {
governorListeners.add(listener);
}
@Override
public void removeGovernorListener(GovernorListener listener) {
governorListeners.remove(listener);
}
@Override
public Map> getServicesToCharacteristicsMap() throws NotReadyException {
return null;
}
@Override
public List getCharacteristics() throws NotReadyException {
return null;
}
@Override
public List getCharacteristicGovernors() throws NotReadyException {
return null;
}
@Override
public URL getURL() {
return url;
}
@Override
public boolean isReady() {
return ready.get();
}
@Override
public BluetoothObjectType getType() {
return BluetoothObjectType.DEVICE;
}
@Override
public Date getLastActivity() {
return lastChanged;
}
@Override
public void accept(BluetoothObjectVisitor visitor) throws Exception {
visitor.visit(this);
}
@Override
public void init() {
bluetoothManager.addAdapterDiscoveryListener(delegateRegistrar);
initTask = CompletableFuture.runAsync(() -> {
bluetoothManager.getDiscoveredAdapters().forEach(this::registerDelegate);
});
}
@Override
public void update() {
updateConnectionTarget();
}
@Override
public void reset() { /* do nothing */ }
@Override
public void dispose() {
if (initTask != null) {
initTask.cancel(true);
}
setConnectionControl(false);
bluetoothManager.removeAdapterDiscoveryListener(delegateRegistrar);
governors.values().forEach(DeviceGovernorHandler::dispose);
governors.clear();
governorListeners.clear();
genericBluetoothDeviceListeners.clear();
bluetoothSmartDeviceListeners.clear();
sortedByDistanceGovernors.clear();
}
@Override
public void setRssiFilter(Class extends Filter> filter) {
throw new IllegalStateException("Not supported by combined governor. ");
}
@Override
public Filter getRssiFilter() {
return rssiFilter;
}
@Override
public boolean isRssiFilteringEnabled() {
return rssiFilteringEnabled;
}
@Override
public void setRssiFilteringEnabled(boolean enabled) {
rssiFilteringEnabled = enabled;
governors.values().forEach(
deviceGovernorHandler -> deviceGovernorHandler.delegate.setRssiFilteringEnabled(enabled));
}
@Override
public void setRssiReportingRate(long rate) {
rssiReportingRate = rate;
governors.values().forEach(
deviceGovernorHandler -> deviceGovernorHandler.delegate.setRssiReportingRate(rate));
}
@Override
public long getRssiReportingRate() {
return rssiReportingRate;
}
@Override
public ConnectionStrategy getConnectionStrategy() {
return connectionStrategy;
}
@Override
public void setConnectionStrategy(ConnectionStrategy connectionStrategy) {
if (connectionStrategy == null) {
throw new IllegalArgumentException("Connection strategy cannot be null");
}
this.connectionStrategy = connectionStrategy;
}
@Override
public URL getPreferredAdapter() {
return preferredAdapter;
}
@Override
public void setPreferredAdapter(URL preferredAdapter) {
this.preferredAdapter = preferredAdapter;
}
@Override
public URL getConnectedAdapter() {
DeviceGovernor deviceGovernor = getGovernor(connected.getUniqueIndex());
return isConnected() && deviceGovernor != null ? deviceGovernor.getURL() : null;
}
private DeviceGovernor getGovernor(int index) {
return governors.values().stream().filter(handler -> handler.index == index)
.map(handler ->handler.delegate).findFirst().orElse(null);
}
private void registerDelegate(DiscoveredAdapter adapter) {
registerDelegate(url.copyWithAdapter(adapter.getURL().getAdapterAddress()));
}
private void registerDelegate(URL url) {
if (governorsCount.get() > 63) {
throw new IllegalStateException("Combined Device Governor can only span up to 63 device governors.");
}
if (url.isDevice() && this.url.getDeviceAddress().equals(url.getDeviceAddress())
&& !COMBINED_ADDRESS.equals(url.getAdapterAddress())) {
governors.computeIfAbsent(url.copyWithProtocol(null), newUrl -> {
DeviceGovernor deviceGovernor = bluetoothManager.getDeviceGovernor(url);
int index = governorsCount.getAndIncrement();
DeviceGovernorHandler handler = new DeviceGovernorHandler(deviceGovernor, index);
handler.init();
return handler;
});
}
}
private void updateLastUpdated(Date lastActivity) {
if (lastChanged == null || lastChanged.before(lastActivity)) {
lastChanged = lastActivity;
BluetoothManagerUtils.safeForEachError(governorListeners, listener -> {
listener.lastUpdatedChanged(lastActivity);
}, logger, "Execution error of a governor listener: last changed");
}
}
private void updateRssi(short newRssi) {
rssi = newRssi;
BluetoothManagerUtils.safeForEachError(genericBluetoothDeviceListeners, listener -> {
listener.rssiChanged(newRssi);
}, logger, "Execution error of a RSSI listener");
}
private final class DeviceGovernorHandler
implements GovernorListener, BluetoothSmartDeviceListener, GenericBluetoothDeviceListener {
private final DeviceGovernor delegate;
private final int index;
private double distance = Double.MAX_VALUE;
private long lastAdvertised;
private boolean inited;
private DeviceGovernorHandler(DeviceGovernor delegate, int index) {
this.delegate = delegate;
this.index = index;
}
private void init() {
initSafe();
initUnsafe();
}
private void initSafe() {
// safe operations
delegate.addBluetoothSmartDeviceListener(this);
delegate.addGenericBluetoothDeviceListener(this);
delegate.addGovernorListener(this);
notifyIfChangedOnline(delegate.isOnline());
if (!(delegate.getRssiFilter() instanceof RssiKalmanFilter)) {
delegate.setRssiFilter(RssiKalmanFilter.class);
}
delegate.setOnlineTimeout(onlineTimeout);
delegate.setBlockedControl(blockedControl);
delegate.setRssiFilteringEnabled(rssiFilteringEnabled);
delegate.setRssiReportingRate(rssiReportingRate);
delegate.setSignalPropagationExponent(signalPropagationExponent);
delegate.setMeasuredTxPower(measuredTxPower);
Date lastActivity = delegate.getLastActivity();
if (lastActivity != null) {
updateLastUpdated(lastActivity);
}
}
private void initUnsafe() {
if (!inited) {
// this method can be called by different threads (notifications) so the synchronization is needed
synchronized (delegate) {
// unsafe operations
if (delegate.isReady()) {
notifyIfChangedReady(true);
// any of the following operations can produce NotReadyException
try {
notifyIfChangedBlocked(delegate.isBlocked());
notifyIfChangedConnected(delegate.isConnected());
if (delegate.isServicesResolved()) {
servicesResolved(delegate.getResolvedServices());
}
int deviceBluetoothClass = delegate.getBluetoothClass();
if (deviceBluetoothClass != 0) {
bluetoothClass = deviceBluetoothClass;
}
bleEnabled |= delegate.isBleEnabled();
name = delegate.getName();
String deviceAlias = delegate.getAlias();
if (deviceAlias != null) {
alias = deviceAlias;
}
inited = true;
} catch (NotReadyException ex) {
// the device has become not ready, that's fine it will be initialized again later
// when it becomes ready, so just ignore it for now
logger.debug("Could not initialize device governor handler", ex);
}
}
}
}
}
@Override
public void connected() {
notifyIfChangedConnected(true);
}
@Override
public void disconnected() {
notifyIfChangedConnected(false);
}
@Override
public void servicesResolved(List gattServices) {
servicesResolved.exclusiveSet(index, true,
() -> {
notifyServicesResolved(gattServices);
}, () -> {
notifyServicesUnresolved();
notifyServicesResolved(gattServices);
}
);
}
@Override
public void servicesUnresolved() {
servicesResolved.exclusiveSet(index, false, () -> {
BluetoothManagerUtils.safeForEachError(bluetoothSmartDeviceListeners,
BluetoothSmartDeviceListener::servicesUnresolved,
logger, "Execution error of a service resolved listener");
});
}
@Override
public void online() {
notifyIfChangedOnline(true);
}
@Override
public void offline() {
sortedByDistanceGovernors.remove(this);
notifyIfChangedOnline(false);
}
@Override
public void blocked(boolean newState) {
notifyIfChangedBlocked(newState);
}
@Override
public void rssiChanged(short newRssi) {
try {
if (rssiLock.tryLock(50, TimeUnit.MILLISECONDS)) {
try {
sortedByDistanceGovernors.remove(this);
lastAdvertised = delegate.getLastAdvertised();
distance = delegate.getEstimatedDistance();
sortedByDistanceGovernors.add(this);
nearest = sortedByDistanceGovernors.first().delegate;
if (delegate == nearest) {
updateRssi(newRssi);
}
} finally {
rssiLock.unlock();
}
}
} catch (InterruptedException ignore) {
logger.warn("Could not acquire a lock to update RSSI");
}
}
@Override
public void ready(boolean isReady) {
if (isReady) {
initUnsafe();
} else {
sortedByDistanceGovernors.remove(this);
}
notifyIfChangedReady(isReady);
}
@Override
public void lastUpdatedChanged(Date lastActivity) {
updateLastUpdated(lastActivity);
}
private void dispose() {
delegate.removeBluetoothSmartDeviceListener(this);
delegate.removeGenericBluetoothDeviceListener(this);
delegate.removeGovernorListener(this);
}
private void notifyIfChangedOnline(boolean newState) {
online.cumulativeSet(index, newState, () -> {
BluetoothManagerUtils.safeForEachError(genericBluetoothDeviceListeners, listener -> {
if (newState) {
listener.online();
} else {
listener.offline();
}
}, logger, "Execution error of an online listener");
});
}
private void notifyIfChangedReady(boolean newState) {
ready.cumulativeSet(index, newState, () -> {
BluetoothManagerUtils.safeForEachError(governorListeners, listener -> {
listener.ready(newState);
}, logger, "Execution error of a governor listener: ready");
});
}
private void notifyIfChangedConnected(boolean newState) {
connected.exclusiveSet(index, newState, () -> {
BluetoothManagerUtils.safeForEachError(bluetoothSmartDeviceListeners, listener -> {
if (newState) {
listener.connected();
} else {
listener.disconnected();
}
}, logger, "Execution error of a connection listener");
});
}
private void notifyIfChangedBlocked(boolean newState) {
blocked.cumulativeSet(index, newState, () -> {
BluetoothManagerUtils.safeForEachError(genericBluetoothDeviceListeners, listener -> {
listener.blocked(newState);
}, logger, "Execution error of a Blocked listener");
});
}
private void notifyServicesResolved(List services) {
List combinedServices = new ArrayList<>(services.size());
services.forEach(service -> {
List combinedCharacteristics =
new ArrayList<>(service.getCharacteristics().size());
service.getCharacteristics().forEach(characteristic -> {
GattCharacteristic combinedCharacteristic = new GattCharacteristic(
characteristic.getURL().copyWithProtocol(null).copyWithAdapter(COMBINED_ADDRESS),
characteristic.getFlags());
combinedCharacteristics.add(combinedCharacteristic);
});
GattService combinedService = new GattService(
service.getURL().copyWithProtocol(null).copyWithAdapter(COMBINED_ADDRESS),
combinedCharacteristics);
combinedServices.add(combinedService);
});
BluetoothManagerUtils.safeForEachError(bluetoothSmartDeviceListeners, listener -> {
listener.servicesResolved(combinedServices);
}, logger, "Execution error of a service resolved listener");
}
private void notifyServicesUnresolved() {
BluetoothManagerUtils.safeForEachError(bluetoothSmartDeviceListeners,
BluetoothSmartDeviceListener::servicesUnresolved,
logger, "Execution error of a service resolved listener");
}
}
private class DelegateRegistrar implements AdapterDiscoveryListener {
@Override
public void discovered(DiscoveredAdapter adapter) {
registerDelegate(adapter);
}
@Override
public void adapterLost(URL address) { /* do nothing */}
}
private class KalmanFilterProxy extends RssiKalmanFilter {
@Override
public void setProcessNoise(double processNoise) {
super.setProcessNoise(processNoise);
forEachKalmanFilter(filter -> filter.setProcessNoise(processNoise));
}
@Override
public void setMeasurementNoise(double measurementNoise) {
super.setMeasurementNoise(measurementNoise);
forEachKalmanFilter(filter -> filter.setMeasurementNoise(measurementNoise));
}
private void forEachKalmanFilter(Consumer consumer) {
governors.values().stream()
.filter(governorHandler -> governorHandler.delegate
.getRssiFilter() instanceof RssiKalmanFilter)
.map(governorHandler -> (RssiKalmanFilter) governorHandler.delegate.getRssiFilter())
.forEach(consumer);
}
}
private class DistanceComparator implements Comparator {
@Override
public int compare(DeviceGovernorHandler first, DeviceGovernorHandler second) {
long current = System.currentTimeMillis();
boolean firstStale = current - first.lastAdvertised > STALE_TIMEOUT;
boolean secondStale = current - second.lastAdvertised > STALE_TIMEOUT;
double firstWeighedValue = first.distance * (firstStale ? 1 : 1000);
double secondWeighedValue = second.distance * (secondStale ? 1 : 1000);
return Double.compare(firstWeighedValue, secondWeighedValue);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy