Cordova / Native APIs / Observables

144 views
Skip to first unread message

Rob Wormald

unread,
May 14, 2015, 6:51:28 PM5/14/15
to angular-...@googlegroups.com
In the interest of knowing-what-I'm-talking-about-before-talking-about-it, I figured I'd shut up about Promises and get really stuck into using Observables to give them a fair shake (you'll get the pun shortly) 

Wanted to share my experience on a side-project I'm working on, as a) i think its super-interesting and b) represents a use-case for angular2 data I hadn't really considered. 

I'm currently helping to build a concussion-sensing mouthguard (https://www.fitguard.me/) - I'm doing the firmware, companion app, and cloud backend for data aggregation. The device -> app link happens over Bluetooth Low Energy (BLE), and the app is (currently) built with Ionic in ng1, using ng2 DI to build the "SDK" (basically, I declare an injector and wrap it in an ng1 provider - see http://plnkr.co/edit/jwBMKG?p=info for that concept)

If you've not used BLE before, it's quite an interesting protocol - as opposed to regular Bluetooth, which uses serial-style communication, BLE uses a much simpler to reason about model : 

Two main roles: 
- A Central (usually this is the app) 
- A Peripheral (usually this is the widget/device/sensor) 

A peripheral exposes a GATT Server, which is more or less a "database" running on the device. For example: 

Our device exposes several Services - basically a logical grouping - BatteryStatus or Accelerometer would be examples of services exposed by our device.

A service exposes one-or-many Characteristics - these are effectively "properties", and a characteristic exposes one or many Descriptors - which describe (duh) the Characteristics.

Individual Characteristics can typically be:
- read / queried by a central (eg - the app reads the current battery level from the device)
- written by a central (eg, the app might set the device's name from "BLEDevice" to "Rob's BLE Device") 
- most interesting / relevant, a Central can subscribe to a characteristic, and be notified when it changes.

So - my Angular app uses a BLE Cordova plugin to handle all this. 

If you've not used cordova much, the basic idea is you have native code (Obj-C or Java) talking to native API's (like the BLE functionality), and then cordova exposes a single bridge method, available at window.cordova.exec() from Javascript. 

The standard Cordova interface is :
cordova.exec(onSuccess, onError, pluginName, pluginMethod, ...parameters);

Previously, I've typically wrapped that API in a Promise (this is what Ionic's ng-cordova project does), which works great for atomic things, like writing a property:

const SENSOR_ENABLE = 0x01; //yay ES6!
const SENOR_DISABLE = 0x00;

writeCharacteristicValue
(characteristicID, value){
 
return new Promise((resolve, reject) => {
   
cordova.exec(resolve, reject, 'BLE', 'writeCharacteristicValue', [characteristicID, value]);
 
});
}

BLEPlugin.writeCharacteristicValue('accelerometerEnabled', SENSOR_ENABLE).then(() => ....);


But doesn't work so well for things like scanning for devices, where the onSuccess callback might get called multiple times:

scanForDevices(timeout){
  let discoveredDevices = [];
  let onDeviceFound
= (device) => discoveredDevices.push(device);

  let scanForTime
= return new Promise((resolve, reject) => {
   
cordova.exec(onDeviceFound, reject, 'BLE', 'startScan');
   //gross
   setTimeout
(() => resolve(discoveredDevices),timeout);
 
});
 
 
return scanForTime.then((discoveredDevices) => {
   
cordova.exec(null, null, 'BLE', 'stopScan');
    return discoveredDevices;
 
});
}

BLEPlugin.scanForDevices(3000).then((devices) => { ... });

Clearly, this is better served by an observable: 

scanForDevices(){
  return new Rx.Observable.create((observer) => {
   
const onDeviceFound = device => observer.onNext(device);
    const onError = scanError => observer.onError(scanError);
   
    cordova
.exec(onDeviceFound, onError, 'BLE', 'startScan');
   
//this is SO much better(!)
   
return () => {
     
cordova.exec(null, null, 'BLE', 'stopScan');
   
}

 
});
}

Which lets you do some awesome stuff in a ridiculously simple fashion 
//scan for devices for 3 seconds and only give me distinct ones
let deviceScan
= BLEManager.scanForDevices().distinct((device) => device.address).takeUntil(Rx.Observable.timeout(3000));

deviceScan
.subscribe((device) => {
  console
.log('found device', device);
});
//or stop early and it cleans... itself... up!
deviceScan
.dispose();

And it gets *really* interesting when you start listening to sensors (this is just psuedo, excuse any foibles) 
//enable notification at 50hz (20ms)
let accStream
= remoteDevice.Accelerometer.enableNotification(20);

//turn them into Vectors (or whatever)
let readings
= accStream
 
.map((sensorReading) => new Vector(sensorReading));

//maybe run some impact detection magic?
let impactDetector
= MySuperSecretAlgorithm.impactThreshold(10) // in g's

let impactStream
= readings.subscribe(impactDetector);

//stream impacts to da cloud
let cloudLogger
= Rx.DOM.fromWebSocket('http://datacloud');

impactStream
.subscribe(cloudLogger);
 

And holy cow, how easy is it to mock out my remote sensor? This easy...
let fakeAccelerometerStream = generateFakeReading().interval(20);

Really super awesome stuff, consider me convinced on this one! That said, there was precious little info out there on doing this sort of thing. Given Angular2's mobile focus, this is one of the places I feel like it's clear to see the advantages of an Observable powered angular, and it would be worth documenting the hell out of these sorts of APIs. Presumably NativeScript et al exposes a similar type of interface (React Native Plugins work this way too, more or less).

Discuss? 



Benjamin Lesh

unread,
May 18, 2015, 8:08:50 PM5/18/15
to Rob Wormald, angular-...@googlegroups.com
So as one of the authors of `fromWebSocket` I feel I should point out a bug that I see in your code.

1. You're going to get errors if impactStream emits before the Socket is connected. You need to implement an openObserver
2. You're trying to subscribe to a Disposable in your previous example.  `someObservable.subscribe(observer)` returns a Disposable subscription.
3. *That* said, you're subscribing to impactStream twice, so it needs to have a `publish().refCount()` or a `singleInstance()` on it. Most likely the latter.

NOTE: I *seemed* like `impactDetector` is an Observer, so I treated it as such.

so (spit-balling) something like: https://gist.github.com/blesh/3a864d25031651dbdc50


1234567891011121314151617181920212223242526272829303132333435
// create an observer for when the socket actually connects
let socketOpen = new Rx.Subject();
 
//enable notification at 50hz (20ms)
let accStream = remoteDevice.Accelerometer.enableNotification(20);
 
//turn them into Vectors (or whatever)
let readings = accStream
.map((sensorReading) => new Vector(sensorReading))
.singleInstance(); // we only want one of these I presume??
 
// I presume this is an Observer it's returning? Since you were subscribing to it before.
let impactDetector = MySuperSecretAlgorithm.impactThreshold(10) // in g's
 
// merge the readings stream onto itself. (Concat could also be used here)
let impactStream = Observable.merge(
// first buffer until the socket opens, then flatten out the buffer
readings.buffer(socketOpen).take(1).flatMap(x => x),
// now merge in the rest of the readings
readings.skipUntil(socketOpen)
)
.singleInstance(); // since you're sharing this observable with two observers (impactDetector and impactStream)
 
let cloudLogger = Rx.DOM.fromWebSocket('ws://datacloud', undefined, socketOpen);
 
// run some impact detection magic?
let impactDetectorSubscription = impactStream.subscribe(impactDetector);
 
//stream impacts to da cloud
let loggerSubscription = impactStream.subscribe(cloudLogger);
 
 
//TODO: (later) dispose of subscriptions
impactDetectorSubscription.dispose();
loggerSubscription.dispose();

--
You received this message because you are subscribed to the Google Groups "angular-data-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to angular-data-d...@googlegroups.com.
To post to this group, send email to angular-...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/angular-data-dev/d60870d1-c9ec-4930-ab2e-34ca564b772c%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Reply all
Reply to author
Forward
0 new messages