Skip to content

Commit

Permalink
Merge pull request #8 from PhilipsHue/small-bugfixes
Browse files Browse the repository at this point in the history
Small bugfixes
  • Loading branch information
remonh87 authored Jan 13, 2020
2 parents a326fa9 + 9f7472d commit 4d6998f
Show file tree
Hide file tree
Showing 31 changed files with 1,298 additions and 516 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.MD
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 1.0.1

* Fixes #5 Undeliverable exception.

* Small fixes for example app.

## 1.0.0+1

* Update homepage
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.signify.hue.flutterreactiveble

import com.polidea.rxandroidble2.exceptions.BleException
import com.signify.hue.flutterreactiveble.ble.RequestConnectionPriorityFailed
import com.signify.hue.flutterreactiveble.channelhandlers.BleStatusHandler
import com.signify.hue.flutterreactiveble.channelhandlers.CharNotificationHandler
Expand All @@ -16,6 +17,8 @@ import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.PluginRegistry
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.exceptions.UndeliverableException
import io.reactivex.plugins.RxJavaPlugins
import timber.log.Timber
import java.util.UUID
import com.signify.hue.flutterreactiveble.ProtobufModel as pb
Expand Down Expand Up @@ -68,6 +71,19 @@ class PluginController {
deviceConnectionChannel.setStreamHandler(deviceConnectionHandler)
charNotificationChannel.setStreamHandler(charNotificationHandler)
bleStatusChannel.setStreamHandler(bleStatusHandler)

/*Workaround for issue undeliverable https://github.com/Polidea/RxAndroidBle/wiki/FAQ:-UndeliverableException
note that this not override the onError for the observable only the RXJAVA error handler like described in:
https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling
*/
RxJavaPlugins.setErrorHandler { throwable ->
if (throwable is UndeliverableException && throwable.cause is BleException) {
return@setErrorHandler // ignore BleExceptions as they were surely delivered at least once
}
// add other custom handlers if needed
@Suppress("TooGenericExceptionThrown")
throw RuntimeException("Unexpected Throwable in RxJavaPlugins error handler", throwable)
}
}

internal fun execute(call: MethodCall, result: Result) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,16 @@ import java.util.concurrent.TimeUnit
internal class DeviceConnector(
private val device: RxBleDevice,
private val connectionTimeout: Duration,
private val updateListeners: (update: com.signify.hue.flutterreactiveble.ble.ConnectionUpdate) -> Unit,
private val connectionQueue: com.signify.hue.flutterreactiveble.ble.ConnectionQueue
private val updateListeners: (update: ConnectionUpdate) -> Unit,
private val connectionQueue: ConnectionQueue
) {

companion object {
private const val minTimeMsBeforeDisconnectingIsAllowed = 200L
private const val delayMsAfterClearingCache = 300L
}

private val connectDeviceSubject = BehaviorSubject.create<com.signify.hue.flutterreactiveble.ble.EstablishConnectionResult>()
private val connectDeviceSubject = BehaviorSubject.create<EstablishConnectionResult>()

private var timestampEstablishConnection: Long = 0

Expand All @@ -40,17 +40,17 @@ internal class DeviceConnector(
connectDeviceSubject
}

private val currentConnection: com.signify.hue.flutterreactiveble.ble.EstablishConnectionResult?
private val currentConnection: EstablishConnectionResult?
get() = if (lazyConnection.isInitialized()) connection.value else null

internal val connection by lazyConnection

private val connectionStatusUpdates by lazy {
device.observeConnectionStateChanges()
.startWith(device.connectionState)
.map<com.signify.hue.flutterreactiveble.ble.ConnectionUpdate> { com.signify.hue.flutterreactiveble.ble.ConnectionUpdateSuccess(device.macAddress, it.toConnectionState().code) }
.map<ConnectionUpdate> { ConnectionUpdateSuccess(device.macAddress, it.toConnectionState().code) }
.onErrorReturn {
com.signify.hue.flutterreactiveble.ble.ConnectionUpdateError(device.macAddress, it.message
ConnectionUpdateError(device.macAddress, it.message
?: "Unknown error")
}
.subscribe(
Expand All @@ -70,8 +70,8 @@ internal class DeviceConnector(
in order to prevent Android from ignoring disconnects we add a delay when we try to
disconnect to quickly after establishing connection. https://issuetracker.google.com/issues/37121223
*/
if (diff < com.signify.hue.flutterreactiveble.ble.DeviceConnector.Companion.minTimeMsBeforeDisconnectingIsAllowed) {
Single.timer(com.signify.hue.flutterreactiveble.ble.DeviceConnector.Companion.minTimeMsBeforeDisconnectingIsAllowed - diff, TimeUnit.MILLISECONDS)
if (diff < DeviceConnector.Companion.minTimeMsBeforeDisconnectingIsAllowed) {
Single.timer(DeviceConnector.Companion.minTimeMsBeforeDisconnectingIsAllowed - diff, TimeUnit.MILLISECONDS)
.doFinally {
disposeSubscriptions()
}.subscribe()
Expand All @@ -92,20 +92,20 @@ internal class DeviceConnector(

val shouldNotTimeout = connectionTimeout.value <= 0L
connectionQueue.addToQueue(deviceId)
updateListeners(com.signify.hue.flutterreactiveble.ble.ConnectionUpdateSuccess(deviceId, ConnectionState.CONNECTING.code))
updateListeners(ConnectionUpdateSuccess(deviceId, ConnectionState.CONNECTING.code))

return waitUntilFirstOfQueue(deviceId)
.switchMap { queue ->
if (!queue.contains(deviceId)) {
Observable.just(com.signify.hue.flutterreactiveble.ble.EstablishConnectionFailure(deviceId,
Observable.just(EstablishConnectionFailure(deviceId,
"Device is not in queue"))
} else {
connectDevice(rxBleDevice, shouldNotTimeout)
.map<com.signify.hue.flutterreactiveble.ble.EstablishConnectionResult> { com.signify.hue.flutterreactiveble.ble.EstablishedConnection(rxBleDevice.macAddress, it) }
.map<EstablishConnectionResult> { EstablishedConnection(rxBleDevice.macAddress, it) }
}
}
.onErrorReturn { error ->
com.signify.hue.flutterreactiveble.ble.EstablishConnectionFailure(rxBleDevice.macAddress,
EstablishConnectionFailure(rxBleDevice.macAddress,
error.message ?: "Unknown error")
}
.doOnNext {
Expand All @@ -115,13 +115,13 @@ internal class DeviceConnector(
connectionStatusUpdates
timestampEstablishConnection = System.currentTimeMillis()
connectionQueue.removeFromQueue(deviceId)
if (it is com.signify.hue.flutterreactiveble.ble.EstablishConnectionFailure) {
updateListeners.invoke(com.signify.hue.flutterreactiveble.ble.ConnectionUpdateError(deviceId, it.errorMessage))
if (it is EstablishConnectionFailure) {
updateListeners.invoke(ConnectionUpdateError(deviceId, it.errorMessage))
}
}
.doOnError {
connectionQueue.removeFromQueue(deviceId)
updateListeners.invoke(com.signify.hue.flutterreactiveble.ble.ConnectionUpdateError(deviceId, it.message
updateListeners.invoke(ConnectionUpdateError(deviceId, it.message
?: "Unknown error"))
}
.doOnDispose {
Expand Down Expand Up @@ -152,8 +152,8 @@ internal class DeviceConnector(

internal fun clearGattCache(): Completable = currentConnection?.let { connection ->
when (connection) {
is com.signify.hue.flutterreactiveble.ble.EstablishedConnection -> clearGattCache(connection.rxConnection)
is com.signify.hue.flutterreactiveble.ble.EstablishConnectionFailure -> Completable.error(Throwable(connection.errorMessage))
is EstablishedConnection -> clearGattCache(connection.rxConnection)
is EstablishConnectionFailure -> Completable.error(Throwable(connection.errorMessage))
}
} ?: Completable.error(IllegalStateException("Connection is not established"))

Expand All @@ -174,7 +174,7 @@ internal class DeviceConnector(
val success = refreshMethod.invoke(bluetoothGatt) as Boolean
if (success) {
Observable.empty<Unit>()
.delay(com.signify.hue.flutterreactiveble.ble.DeviceConnector.Companion.delayMsAfterClearingCache, TimeUnit.MILLISECONDS)
.delay(DeviceConnector.Companion.delayMsAfterClearingCache, TimeUnit.MILLISECONDS)
.doOnComplete { Timber.d("Clearing GATT cache completed") }
} else {
val reason = "BluetoothGatt.refresh() returned false"
Expand Down
35 changes: 25 additions & 10 deletions example/lib/ui/device_detail_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,11 @@ class _DetailState extends State<DeviceDetailScreen> {
)
.asBroadcastStream();

_charValueStream = _connectedDeviceStream.where((device) => device.connectionState == DeviceConnectionState.connected).take(1).asyncExpand((_) {
_charValueStream = _connectedDeviceStream
.where((device) =>
device.connectionState == DeviceConnectionState.connected)
.take(1)
.asyncExpand((_) {
log("Connected to $deviceName ($deviceId), getting characteristic");

return _ble.subscribeToCharacteristic(QualifiedCharacteristic(
Expand All @@ -69,10 +73,12 @@ class _DetailState extends State<DeviceDetailScreen> {
));
});

_currentValueUpdateSubscription = _charValueStream.listen((data) => _currentValue = data.first);
_currentValueUpdateSubscription =
_charValueStream.listen((data) => _currentValue = data.first);

_connectedDeviceStream
.where((device) => device.connectionState == DeviceConnectionState.connected)
.where((device) =>
device.connectionState == DeviceConnectionState.connected)
.first
.then((_) => Future<void>.delayed(const Duration(milliseconds: 100)))
.then((_) => _readCharacteristic());
Expand Down Expand Up @@ -111,7 +117,8 @@ class _DetailState extends State<DeviceDetailScreen> {
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: Text("${_isConnected ? 'Connected' : 'Connecting'} to: $deviceName"),
title: Text(
"${_isConnected ? 'Connected' : 'Connecting'} to: $deviceName"),
),
body: Padding(
padding: const EdgeInsets.all(16),
Expand All @@ -125,9 +132,12 @@ class _DetailState extends State<DeviceDetailScreen> {
stream: _connectedDeviceStream,
builder: (_, AsyncSnapshot<ConnectionStateUpdate> snapshot) {
if (snapshot.hasData) {
WidgetsBinding.instance.addPostFrameCallback((_) => setState(() => _connectionState = snapshot.data.connectionState));
WidgetsBinding.instance.addPostFrameCallback((_) =>
setState(() => _connectionState =
snapshot.data.connectionState));

return StatusMessage('Connection state is: ${snapshot.data.connectionState}');
return StatusMessage(
'Connection state is: ${snapshot.data.connectionState}');
} else {
return const StatusMessage('No data yet...');
}
Expand All @@ -137,11 +147,14 @@ class _DetailState extends State<DeviceDetailScreen> {
stream: _charValueStream,
builder: (_, AsyncSnapshot<List<int>> snapshot) {
if (snapshot.hasData) {
return StatusMessage('Value for notification is: ${snapshot.data}');
return StatusMessage(
'Value for notification is: ${snapshot.data}');
} else if (_currentValue == null) {
return const StatusMessage('No data for characteristic retrieved yet...');
return const StatusMessage(
'No data for characteristic retrieved yet...');
} else {
return StatusMessage('Value for notification is: $_currentValue');
return StatusMessage(
'Value for notification is: $_currentValue');
}
},
),
Expand All @@ -163,7 +176,9 @@ class _DetailState extends State<DeviceDetailScreen> {
const SizedBox(width: 16),
RaisedButton(
child: const Text('Write characteristic'),
onPressed: _isConnected && _textController.text.isNotEmpty ? _writeCharacteristic : null,
onPressed: _isConnected && _textController.text.isNotEmpty
? _writeCharacteristic
: null,
),
],
),
Expand Down
43 changes: 31 additions & 12 deletions example/lib/ui/device_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ class _DeviceListState extends State<DeviceList> {
void initState() {
super.initState();
_initBleState();
_uuidController = TextEditingController()..addListener(() => setState(() {}));
_uuidController = TextEditingController()
..addListener(() => setState(() {}));
}

@override
Expand All @@ -55,7 +56,7 @@ class _DeviceListState extends State<DeviceList> {

bool _isValidUuidInput() {
final uuidText = _uuidController.text;
if (uuidText.length > 3) {
if (uuidText.length > 3 && uuidText.length.isEven) {
try {
Uuid.parse(uuidText);
return true;
Expand All @@ -73,7 +74,8 @@ class _DeviceListState extends State<DeviceList> {
log("Starting to listen for devices...");

_clearDeviceList();
_scanSubscription = _ble.scanForDevices(withService: uuid).listen((device) {
_scanSubscription =
_ble.scanForDevices(withService: uuid).listen((device) {
if (!_devices.any((d) => d.id == device.id)) {
log("New device found: $device");

Expand Down Expand Up @@ -115,12 +117,16 @@ class _DeviceListState extends State<DeviceList> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 16),
const Text('UUID to use for scanning: (short or long version)'),
const Text(
'UUID to use for scanning: (short or long version)'),
TextField(
controller: _uuidController,
enabled: _scanSubscription == null,
decoration: InputDecoration(errorText: _uuidController.text.isEmpty || _isValidUuidInput() ? null : 'Invalid UUID format'),
autofocus: true,
decoration: InputDecoration(
errorText:
_uuidController.text.isEmpty || _isValidUuidInput()
? null
: 'Invalid UUID format'),
autocorrect: false,
),
const SizedBox(height: 16),
Expand All @@ -129,15 +135,21 @@ class _DeviceListState extends State<DeviceList> {
children: [
RaisedButton(
child: const Text('Start scanning'),
onPressed: _scanSubscription == null && _status == BleStatus.ready && _isValidUuidInput() ? _startScanning : null,
onPressed: _scanSubscription == null &&
_status == BleStatus.ready &&
_isValidUuidInput()
? _startScanning
: null,
),
RaisedButton(
child: const Text('Stop scanning'),
onPressed: _scanSubscription != null ? _stopScanning : null,
onPressed:
_scanSubscription != null ? _stopScanning : null,
),
RaisedButton(
child: const Text('Clear scanlist'),
onPressed: _devices.isNotEmpty ? _clearDeviceList : null,
onPressed:
_devices.isNotEmpty ? _clearDeviceList : null,
),
],
),
Expand All @@ -146,8 +158,11 @@ class _DeviceListState extends State<DeviceList> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (_status == BleStatus.ready) ...[
Text(_scanSubscription == null ? 'Enter a UUID above and tap start to begin scanning' : 'Tap a device to connect to it'),
if (_scanSubscription != null || _devices.isNotEmpty) Text('count: ${_devices.length}'),
Text(_scanSubscription == null
? 'Enter a UUID above and tap start to begin scanning'
: 'Tap a device to connect to it'),
if (_scanSubscription != null || _devices.isNotEmpty)
Text('count: ${_devices.length}'),
] else
const Text("BLE Status isn't valid for scanning"),
],
Expand All @@ -167,7 +182,11 @@ class _DeviceListState extends State<DeviceList> {
onTap: () async {
_stopScanning();
_clearDeviceList();
await Navigator.push<void>(context, MaterialPageRoute(builder: (_) => DeviceDetailScreen(device: device)));
await Navigator.push<void>(
context,
MaterialPageRoute(
builder: (_) =>
DeviceDetailScreen(device: device)));
},
),
)
Expand Down
2 changes: 1 addition & 1 deletion example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ packages:
path: ".."
relative: true
source: path
version: "0.1.0"
version: "1.0.0+1"
flutter_test:
dependency: "direct dev"
description: flutter
Expand Down
Loading

0 comments on commit 4d6998f

Please sign in to comment.