I'm implementing a BLE protocol between a central (iPhone) and peripheral (custom device). The protocol works as follows:
central connects to peripheral and sets up notification
peripheral sends data on notification characteristic
central processes data and sends response on separate characteristic
peripheral sends addtnl data on notification characteristic
central process data and disconnects.
I'm attempting to implement this in a clean way using RxBluetoothKit. It currently works, but I'd like to solve the following challenges:
What is the best way to cleanly disconnect in step 5. I'm hoping to not have to dispose the overall observable, but rather just have it 'complete'. I'm currently using 'takeUntil', but not sure if that's the best way.
Allow for the notification to cleanup gracefully prior to disconnect. With my current code, I receive an 'API MISUSE can only accept commands while in the connected state' because I believe the notification is cleaning up while the disconnect is occurring.
Thanks.
enum TestPeripheralService: String, ServiceIdentifier {
case main = "CED916FA-6692-4A12-87D5-6F2764762B23"
var uuid: CBUUID { return CBUUID(string: self.rawValue) }
}
enum TestPeripheralCharacteristic: String, CharacteristicIdentifier {
case writer = "CED927B4-6692-4A12-87D5-6F2764762B2A"
case reader = "CED9D5D8-6692-4A12-87D5-6F2764762B2A"
var uuid: CBUUID { return CBUUID(string: self.rawValue) }
var service: ServiceIdentifier { return TestPeripheralService.main }
}
fileprivate lazy var centralManager: CentralManager = {
RxBluetoothKitLog.setLogLevel(.verbose)
return CentralManager(queue: .main)
}()
func executeConnectionAndHandshake() {
let disconnectSubject = PublishSubject<Bool>.init()
var peripheral: Peripheral?
var packetNum = 0
_ = centralManager
.observeState()
.startWith(centralManager.state)
.filter { $0 == .poweredOn }
.flatMap { _ in self.centralManager.scanForPeripherals(withServices: [TestPeripheralService.main.uuid]) }
.flatMap { $0.peripheral.establishConnection().takeUntil(disconnectSubject) }
.do(onNext: { peripheral = $0 })
.flatMap { $0.discoverServices([TestPeripheralService.main.uuid])}
.flatMap { $0[0].discoverCharacteristics(nil)}
.flatMap { _ in
Observable<Bool>.create { event in
let disposables = CompositeDisposable()
let readSubject = PublishSubject<Data>.init()
_ = disposables.insert(peripheral!.observeValueUpdateAndSetNotification(for: TestPeripheralCharacteristic.reader)
.subscribe(onNext: {
packetNum += 1
let packet = $0.value!
if (packetNum <= 1) {
readSubject.onNext(packet)
} else {
event.onNext(true)
event.onCompleted()
}
}, onError: { event.onError($0) })
)
_ = disposables.insert(readSubject
.flatMapLatest { data -> Single<Characteristic> in
var writeData = Data(capacity: 300)
for _ in 0..<300 {
writeData.append(0xFF)
}
return peripheral!.writeValue(writeData, for: TestPeripheralCharacteristic.writer, type: .withResponse)
}
.subscribe(onError: { event.onError($0) })
)
return Disposables.create {
disposables.dispose()
}
}
.do(onCompleted: { disconnectSubject.onNext(true) })
}
.subscribe(onError: { print($0) },
onCompleted: { print("Connection and handshake completed") })
}
Related
I'm wrapping some legacy completion-block code in an Observable. It will emit one event (next or error), and then complete. The problem is that calling onNext(), onCompleted() only sends the completed event to the observer. Why doesn't the next event get delivered?
UPDATE: The people stream actually works as expected. The issue turns out to be in the next stream, filteredPeople. The inner completed event is passed along to it, and I'm just returning it, which terminates the stream.
I need to filter out completed events from inner streams.
let people = Observable<Event<[Person]>>()
.flatMapLatest {
return fetchPeople().asObservable().materialize()
}
.share()
// this is bound to a search field
let filterText = PublishSubject<String>()
let filteredPeople = Observable.combineLatest(people, filterText) { peopleEvent, filter in
// this is the problem. the completed event from people is being returned, and it terminates the stream
guard let people = peopleEvent.element else { return peopleEvent }
if filterText.isEmpty { return .next(people) }
return .next(people.filter { ... })
}
func fetchPeople() -> Single<[Person]> {
return Single<[Person]>.create { observer in
PeopleService.fetch { result in
switch result {
case .success(let people):
observer(.success(people))
case .failure(let error):
observer(.error(error))
}
}
return Disposables.create()
}
}
filteredPeople.subscribe(
onNext: { event in
// ?! doesn't get called
},
onCompleted: {
// we get here, but why?
},
onError: {event in
...
}).disposed(by: disposeBag)
You haven't posted the code that is causing the problem. The code below works as expected:
struct Person { }
class PeopleService {
static func fetch(_ result: #escaping (Result<[Person], Error>) -> Void) {
result(.success([]))
}
}
let disposeBag = DisposeBag()
func fetchPeople() -> Single<[Person]> {
return Single<[Person]>.create { observer in
PeopleService.fetch { result in
switch result {
case .success(let people):
observer(.success(people))
case .failure(let error):
observer(.error(error))
}
}
return Disposables.create()
}
}
let people = Observable<Void>.just(())
.flatMapLatest { _ in
return fetchPeople().asObservable().materialize()
}
.share()
people.subscribe(
onNext: { event in
print("onNext does get called")
print("in fact, it will get called twice, once with a .next(.next([Person])) event")
print("and once with a .next(.completed) event.")
},
onCompleted: {
print("this prints after onNext gets called")
})
.disposed(by: disposeBag)
I fixed it by filtering out completed events from the inner stream. I am not sure this is the right way, but I can't think of a better solution.
let people = Observable<Event<[Person]>>()
.flatMapLatest {
return fetchPeople()
.asObservable()
.materialize()
// Our work is done, but don't end the parent stream
.filter { !$0.isCompleted }
}
.share()
I am trying out the yet-unstable async-await syntax in nightly Rust 1.38 with futures-preview = "0.3.0-alpha.16" and runtime = "0.3.0-alpha.6". It feels really cool, but the docs are (yet) scarce and I got stuck.
To go a bit beyond the basic examples I would like to create an app that:
Accepts TCP connections on a given port;
Broadcasts all the data received from any connection to all active connections.
Existing docs and examples got me this far:
#![feature(async_await)]
#![feature(async_closure)]
use futures::{
prelude::*,
select,
future::select_all,
io::{ReadHalf, WriteHalf, Read},
};
use runtime::net::{TcpListener, TcpStream};
use std::io;
async fn read_stream(mut reader: ReadHalf<TcpStream>) -> (ReadHalf<TcpStream>, io::Result<Box<[u8]>>) {
let mut buffer: Vec<u8> = vec![0; 1024];
match reader.read(&mut buffer).await {
Ok(len) => {
buffer.truncate(len);
(reader, Ok(buffer.into_boxed_slice()))
},
Err(err) => (reader, Err(err)),
}
}
#[runtime::main]
async fn main() -> std::io::Result<()> {
let mut listener = TcpListener::bind("127.0.0.1:8080")?;
println!("Listening on {}", listener.local_addr()?);
let mut incoming = listener.incoming().fuse();
let mut writers: Vec<WriteHalf<TcpStream>> = vec![];
let mut reads = vec![];
loop {
select! {
maybe_stream = incoming.select_next_some() => {
let (mut reader, writer) = maybe_stream?.split();
writers.push(writer);
reads.push(read_stream(reader).fuse());
},
maybe_read = select_all(reads.iter()) => {
match maybe_read {
(reader, Ok(data)) => {
for writer in writers {
writer.write_all(data).await.ok(); // Ignore errors here
}
reads.push(read_stream(reader).fuse());
},
(reader, Err(err)) => {
let reader_addr = reader.peer_addr().unwrap();
writers.retain(|writer| writer.peer_addr().unwrap() != reader_addr);
},
}
}
}
}
}
This fails with:
error: recursion limit reached while expanding the macro `$crate::dispatch`
--> src/main.rs:36:9
|
36 | / select! {
37 | | maybe_stream = incoming.select_next_some() => {
38 | | let (mut reader, writer) = maybe_stream?.split();
39 | | writers.push(writer);
... |
55 | | }
56 | | }
| |_________^
|
= help: consider adding a `#![recursion_limit="128"]` attribute to your crate
= note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info)
This is very confusing. Maybe I am using select_all() in a wrong way? Any help in making it work is appreciated!
For completeness, my Cargo.toml:
[package]
name = "async-test"
version = "0.1.0"
authors = ["xxx"]
edition = "2018"
[dependencies]
runtime = "0.3.0-alpha.6"
futures-preview = { version = "=0.3.0-alpha.16", features = ["async-await", "nightly"] }
In case someone is following, I hacked it together finally. This code works:
#![feature(async_await)]
#![feature(async_closure)]
#![recursion_limit="128"]
use futures::{
prelude::*,
select,
stream,
io::ReadHalf,
channel::{
oneshot,
mpsc::{unbounded, UnboundedSender},
}
};
use runtime::net::{TcpListener, TcpStream};
use std::{
io,
net::SocketAddr,
collections::HashMap,
};
async fn read_stream(
addr: SocketAddr,
drop: oneshot::Receiver<()>,
mut reader: ReadHalf<TcpStream>,
sender: UnboundedSender<(SocketAddr, io::Result<Box<[u8]>>)>
) {
let mut drop = drop.fuse();
loop {
let mut buffer: Vec<u8> = vec![0; 1024];
select! {
result = reader.read(&mut buffer).fuse() => {
match result {
Ok(len) => {
buffer.truncate(len);
sender.unbounded_send((addr, Ok(buffer.into_boxed_slice())))
.expect("Channel error");
if len == 0 {
return;
}
},
Err(err) => {
sender.unbounded_send((addr, Err(err))).expect("Channel error");
return;
}
}
},
_ = drop => {
return;
},
}
}
}
enum Event {
Connection(io::Result<TcpStream>),
Message(SocketAddr, io::Result<Box<[u8]>>),
}
#[runtime::main]
async fn main() -> std::io::Result<()> {
let mut listener = TcpListener::bind("127.0.0.1:8080")?;
eprintln!("Listening on {}", listener.local_addr()?);
let mut writers = HashMap::new();
let (sender, receiver) = unbounded();
let connections = listener.incoming().map(|maybe_stream| Event::Connection(maybe_stream));
let messages = receiver.map(|(addr, maybe_message)| Event::Message(addr, maybe_message));
let mut events = stream::select(connections, messages);
loop {
match events.next().await {
Some(Event::Connection(Ok(stream))) => {
let addr = stream.peer_addr().unwrap();
eprintln!("New connection from {}", addr);
let (reader, writer) = stream.split();
let (drop_sender, drop_receiver) = oneshot::channel();
writers.insert(addr, (writer, drop_sender));
runtime::spawn(read_stream(addr, drop_receiver, reader, sender.clone()));
},
Some(Event::Message(addr, Ok(message))) => {
if message.len() == 0 {
eprintln!("Connection closed by client: {}", addr);
writers.remove(&addr);
continue;
}
eprintln!("Received {} bytes from {}", message.len(), addr);
if &*message == b"quit\n" {
eprintln!("Dropping client {}", addr);
writers.remove(&addr);
continue;
}
for (&other_addr, (writer, _)) in &mut writers {
if addr != other_addr {
writer.write_all(&message).await.ok(); // Ignore errors
}
}
},
Some(Event::Message(addr, Err(err))) => {
eprintln!("Error reading from {}: {}", addr, err);
writers.remove(&addr);
},
_ => panic!("Event error"),
}
}
}
I use a channel and spawn a reading task for each client. Special care had to be taken to ensure that readers get dropped with writers: this is why oneshot future is used. When oneshot::Sender is dropped, the oneshot::Receiver future resolves to canceled state, which is a notification mechanism for a reading task to know it is time to halt. To demonstrate that it works, we drop a client as soon as we get "quit" message.
Sadly, there is a (seemingly useless) warning regarding an unused JoinHandle from the runtime::spawn call, and I don't really know how to eliminate it.
I know the client and server are connecting because my connect/disconnect events are firing. However, my custom events are not. I am using socket.io java client, and netty-socketio on the server. I usually use the socket.io javascript library which works seamlessly, so I am a bit lost as to why this is happening. I am writing this in Kotlin.
Client-Side
fun connectToServer(ipAddress : String)
{
socket = IO.socket("$ipAddress")
socket!!.on(Socket.EVENT_CONNECT) { obj ->
println("Connected To Server!!!")
}.on(EventNames.signOn) { obj ->
println(EventNames.signOn)
//cast value to string from server, hope for encrypted password
val encryptedPassword = obj[0] as String
when(encryptedPassword)
{
"no user" -> {
}
else -> {
val result = encryptedPassword!!.split("OR")
val isMatch = passwordTextField.text == dataProcessing.Encryption3().decryptValue("decrypt", result[0],result[1])
if(isMatch)
{
}
}
}
println("Encrypted Password: "+encryptedPassword)
}
// socket!!.on(Socket.EVENT_DISCONNECT, object : Emitter.Listener {
//
// override fun call(vararg args: Any) {}
//
// })
socket!!.connect()
// socket!!.open()
// socket!!.emit(Socket.EVENT_CONNECT, "Hello!")
socket!!.send("hey")
socket!!.emit(EventNames.requestClientSignOn, usernameTextField.text)
}
Server-Side
#Throws(InterruptedException::class, UnsupportedEncodingException::class)
fun server()
{
val config = Configuration()
config.setHostname("localhost")
config.setPort(PORT)
server = SocketIOServer(config)
server!!.addConnectListener {
println("Hello World!")
}
server!!.addEventListener(EventNames.requestClientSignOn, String::class.java) { client, data, ackRequest ->
println("Hello from requestClientSignOn..")
}
server!!.addDisconnectListener {
println("Client Disconnecting...")
}
server!!.addConnectListener {
println("client connected!! client: $it")
}
server!!.start()
You cannot use lambda expression in your event listeners, using netty-socketio on the sever.
Using the traditional EventListener solves this problem. I also converted the server to Kotlin, as it was easier to use the demo project as a reference.
server.addEventListener(EventNames.requestClientSignOn, String.class, new DataListener<String>() {
#Override
public void onData(SocketIOClient client, String username, AckRequest ackRequest) {
String isEncryptedPassword = new KOTS_EmployeeManager().getKOTS_User(KOTS_EmployeeManager.kotsUserType.CLIENT, username)
if(isEncryptedPassword != null)
{
//send back ack with encrypted password
ackRequest.sendAckData(isEncryptedPassword);
}else{
//send back ack with no user string
ackRequest.sendAckData("no user");
}
}
});
Basically I am working on a sleep monitoring application that monitors heart rate as well. So, I don't want to start any workout activity but I think that's the way apple works!
Here's the heart rate only code I am using:
#IBOutlet private weak var label: WKInterfaceLabel!
#IBOutlet private weak var deviceLabel : WKInterfaceLabel!
#IBOutlet private weak var heart: WKInterfaceImage!
#IBOutlet private weak var startStopButton : WKInterfaceButton!
let healthStore = HKHealthStore()
//State of the app - is the workout activated
var workoutActive = false
// define the activity type and location
var workoutSession : HKWorkoutSession?
let heartRateUnit = HKUnit(fromString: "count/min")
var anchor = HKQueryAnchor(fromValue: Int(HKAnchoredObjectQueryNoAnchor))
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
}
override func willActivate() {
super.willActivate()
guard HKHealthStore.isHealthDataAvailable() == true else {
label.setText("not available")
return
}
guard let quantityType = HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierHeartRate) else {
displayNotAllowed()
return
}
let dataTypes = Set(arrayLiteral: quantityType)
healthStore.requestAuthorizationToShareTypes(nil, readTypes: dataTypes) { (success, error) -> Void in
if success == false {
self.displayNotAllowed()
}
}
}
func displayNotAllowed() {
label.setText("not allowed")
}
func workoutSession(workoutSession: HKWorkoutSession, didChangeToState toState: HKWorkoutSessionState, fromState: HKWorkoutSessionState, date: NSDate) {
switch toState {
case .Running:
workoutDidStart(date)
case .Ended:
workoutDidEnd(date)
default:
print("Unexpected state \(toState)")
}
}
func workoutSession(workoutSession: HKWorkoutSession, didFailWithError error: NSError) {
// Do nothing for now
NSLog("Workout error: \(error.userInfo)")
}
func workoutDidStart(date : NSDate) {
if let query = createHeartRateStreamingQuery(date) {
healthStore.executeQuery(query)
} else {
label.setText("cannot start")
}
}
func workoutDidEnd(date : NSDate) {
if let query = createHeartRateStreamingQuery(date) {
healthStore.stopQuery(query)
label.setText("---")
} else {
label.setText("cannot stop")
}
}
// MARK: - Actions
#IBAction func startBtnTapped() {
if (self.workoutActive) {
//finish the current workout
self.workoutActive = false
self.startStopButton.setTitle("Start")
if let workout = self.workoutSession {
healthStore.endWorkoutSession(workout)
}
} else {
//start a new workout
self.workoutActive = true
self.startStopButton.setTitle("Stop")
startWorkout()
}
}
func startWorkout() {
self.workoutSession = HKWorkoutSession(activityType: HKWorkoutActivityType.CrossTraining, locationType: HKWorkoutSessionLocationType.Indoor)
self.workoutSession?.delegate = self
healthStore.startWorkoutSession(self.workoutSession!)
}
func createHeartRateStreamingQuery(workoutStartDate: NSDate) -> HKQuery? {
// adding predicate will not work
// let predicate = HKQuery.predicateForSamplesWithStartDate(workoutStartDate, endDate: nil, options: HKQueryOptions.None)
guard let quantityType = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierHeartRate) else { return nil }
let heartRateQuery = HKAnchoredObjectQuery(type: quantityType, predicate: nil, anchor: anchor, limit: Int(HKObjectQueryNoLimit)) { (query, sampleObjects, deletedObjects, newAnchor, error) -> Void in
guard let newAnchor = newAnchor else {return}
self.anchor = newAnchor
self.updateHeartRate(sampleObjects)
}
heartRateQuery.updateHandler = {(query, samples, deleteObjects, newAnchor, error) -> Void in
self.anchor = newAnchor!
self.updateHeartRate(samples)
}
return heartRateQuery
}
func updateHeartRate(samples: [HKSample]?) {
guard let heartRateSamples = samples as? [HKQuantitySample] else {return}
dispatch_async(dispatch_get_main_queue()) {
guard let sample = heartRateSamples.first else{return}
let value = sample.quantity.doubleValueForUnit(self.heartRateUnit)
self.label.setText(String(UInt16(value)))
// retrieve source from sample
let name = sample.sourceRevision.source.name
self.updateDeviceName(name)
self.animateHeart()
}
}
func updateDeviceName(deviceName: String) {
deviceLabel.setText(deviceName)
}
func animateHeart() {
self.animateWithDuration(0.5) {
self.heart.setWidth(60)
self.heart.setHeight(90)
}
let when = dispatch_time(DISPATCH_TIME_NOW, Int64(0.5 * double_t(NSEC_PER_SEC)))
let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
dispatch_after(when, queue) {
dispatch_async(dispatch_get_main_queue(), {
self.animateWithDuration(0.5, animations: {
self.heart.setWidth(50)
self.heart.setHeight(80)
})
})
}
} }
To summarize, the unexpected observations are:
1. The time I monitor the heart rate contributes to the green ring in the activity app.
2. Unexpected high amount of calories are being recorded i.e. when the person is on bed or asleep!
Can you please help with the correct code that helps me to monitor and display a person's heart beat at regular interval during his sleep without contributing to the green ring or contributing extra cals. ?
Thanks a lot in advance!
Starting a workout and running the heart rate monitor will drain the apple watch's battery after about 6 hours (if it has a full charge), so having it run continuously while sleeping is probably not realistic at this time.
From what I can tell, starting a workout using workoutSession does 2 things for your app. It keeps your app in the foreground, and it starts taking heart rate sample every few seconds. Have you considered not starting it? Your health kit queries will still work as is and the heart rate monitor still records the users heart rate every 15 minutes or so. The main thing you loose is keeping your app in the foreground, and I am wondering if you need to do that (since the user will be asleep).
To retrieve the last heart rate sample from healthkit:
func getLatestHeartRate() {
let quantityType = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierHeartRate)!
let sortDescriptor = NSSortDescriptor(key:HKSampleSortIdentifierStartDate, ascending: false)
let sampleQuery = HKSampleQuery(sampleType: quantityType, predicate: nil, limit: 1, sortDescriptors: [sortDescriptor])
{ (sampleQuery, results, error ) -> Void in
}
self.healthStore.executeQuery(sampleQuery)
}
I am setting up an iOS 8 app to request Heath Kit Store authorization to share types. The request Read/Write screen shows fine and on selecting Done, I see the completion callback immediately after. In this callback, I am pushing a new view controller. I set a breakpoint for the code that is programmatically pushing the next view controller and this is called immediately, but the transition doesn't occur until about 10 seconds later.
Some code:
#IBAction func enable(sender: AnyObject) {
let hkManager = HealthKitManager()
hkManager.setupHealthStoreIfPossible { (success, error) -> Void in
if let error = error {
println("error = \(error)")
} else {
println("enable HK success = \(success)")
self.nextStep()
}
}
}
func nextStep() {
self.nav!.pushViewController(nextController, animated: true)
}
class HealthKitManager: NSObject {
let healthStore: HKHealthStore!
override init() {
super.init()
healthStore = HKHealthStore()
}
class func isHealthKitAvailable() -> Bool {
return HKHealthStore.isHealthDataAvailable()
}
func setupHealthStoreIfPossible(completion: ((Bool, NSError!) -> Void)!) {
if HealthKitManager.isHealthKitAvailable()
{
healthStore.requestAuthorizationToShareTypes(dataTypesToWrite(), readTypes: dataTypesToRead(), completion: { (success, error) -> Void in
completion(success, error)
})
}
}
func dataTypesToWrite() -> NSSet {
let runningType = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierDistanceWalkingRunning)
let stepType = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierStepCount)
return NSSet(objects: runningType, stepType)
}
func dataTypesToRead() -> NSSet {
let runningType = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierDistanceWalkingRunning)
let stepType = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierStepCount)
let climbedType = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierFlightsClimbed)
return NSSet(objects: runningType, stepType, climbedType)
}
}
Any thoughts on what is causing the time delay for the transition?
The problem was that the completion block is returned in the background queue. I just put the transition call back onto the main queue as follows:
hkManager.setupHealthStoreIfPossible { (success, error) -> Void in
if let error = error {
println("error = \(error)")
} else {
dispatch_async(dispatch_get_main_queue(), {
println("enable HK success = \(success)")
self.nextStep()
});
}
}
}