CoreBluetooth: unable to write and retrieve a static characteristic - ios7

I am building an application where two iOS devices both transmit and scan (peripheral and central) for each other. Due to Apple's implementation, when the app is backgrounded, all identifiable information is removed from the advertising packet, meaning I need to connect to the discovered peripherals to find out who and what they are if they are transmitting in the background.
All I really need to do is identify the peripheral. (Connect and disconnect). Currently, the only way I can find to do this is to set a static characteristic attached to a common service that allows each device to uniquely identify itself, even when backgrounded. This value will not change or get updated. If I could simply look at peripheral.UUID after connecting, this would do the trick. But I can't anymore with iOS8. So, I create a characteristic to contain the unique identifier.
(Not sure if this is the best way, but its the only way I can think of.)
Everything is working great (discovering characteristic) but I am unable to retrieve anything other than nil for the characteristic, even though I have specifically set it when I started transmitting.
Here is my (Peripheral code):
-(void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral {
// Opt out from any other state
if (peripheral.state != CBPeripheralManagerStatePoweredOn) {
return;
}
NSLog(#"BT Transmitter Powered On");
NSString* uniqueString = #“foobar";
NSData* characteristicValue = [uniqueString dataUsingEncoding:NSUTF8StringEncoding];
self.transferCharacteristic = [[CBMutableCharacteristic alloc] initWithType:[CBUUID UUIDWithString:#"08590F7E-DB05-467E-8757-72F6FAEB13D4"]
properties:CBCharacteristicPropertyRead
value:characteristicValue
permissions:CBAttributePermissionsReadable];
CBMutableService *transferService = [[CBMutableService alloc] initWithType:[CBUUID UUIDWithString:#"E20A39F4-73F5-4BC4-A12F-17D1AD07A961"] primary:YES];
transferService.characteristics = #[self.transferCharacteristic];
[self.peripheralManager addService:transferService];
[self.peripheralManager startAdvertising:#{ CBAdvertisementDataServiceUUIDsKey: #[[CBUUID UUIDWithString:#"E20A39F4-73F5-4BC4-A12F-17D1AD07A961"]] }];
}
And here is my Central Code:
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error
{
if (error) {
NSLog(#"Error discovering characteristics: %#", [error localizedDescription]);
return;
}
for (CBCharacteristic *characteristic in service.characteristics) {
// print out value of discovered characteristic
NSLog (#"Characteristic discovered: %#", characteristic); // this outputs all all the properties of the characteristic, including a value of "null".
NSString *value = [[NSString alloc] initWithData:characteristic.value encoding:NSUTF8StringEncoding];
NSLog(#"Value: %#",value); // this prints out nothing
}
}
What am I doing wrong? I would expect to see the value of the characteristic as "foobar" when transformed back into an NSString. Instead it is null.

Having discovered the characteristic you need to perform a read request to actually get its value -
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error
{
if (error) {
NSLog(#"Error discovering characteristics: %#", [error localizedDescription]);
return;
}
for (CBCharacteristic *characteristic in service.characteristics) {
// print out value of discovered characteristic
NSLog (#"Characteristic discovered: %#", characteristic); // this outputs all all the properties of the characteristic, including a value of "null".
if ([characteristic.UUID.UUIDString isEqualToString:#"08590F7E-DB05-467E-8757-72F6FAEB13D4"]) {
[peripheral readValueForCharacteristic:characteristic];
NSString *value = [[NSString alloc] initWithData:characteristic.value encoding:NSUTF8StringEncoding];
NSLog(#"Value: %#",value); // this prints out nothing
}
}
You will subsequently get a call to didUpdateValueForCharacteristic: -
-(void) peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
if (error == nil) {
NSString *valueString=[[NSString alloc] initWithData:characteristic.value encoding:NSUTF8StringEncoding];
NSLog(#"The value=%#",valueString
}
}

Update: to find the RSSI of the peripheral once you have connected to it and read its service, use the readRSSI method.
Then, strangely enough, even though its not in the documentation, this is the only delegate callback method (with RSSI) that works for me running 8.1.1.
-(void) peripheral:(CBPeripheral *)peripheral didReadRSSI:(NSNumber *)RSSI error:(NSError *)error {
NSLog(#"Got RSSI update in didReadRSSI : %4.1f", [RSSI doubleValue]);
}
Now I just have to figure out how to link this RSSI signal with the specific peripheral I connected to and identified in the previous call.

Related

How to subclass NSDocumentController to only allow one doc at a time

I'm trying to create a Core Data, document based app but with the limitation that only one document can be viewed at a time (it's an audio app and wouldn't make sense for a lot of docs to be making noise at once).
My plan was to subclass NSDocumentController in a way that doesn't require linking it up to any of the menu's actions. This has been going reasonably but I've run into a problem that's making me question my approach a little.
The below code works for the most part except if a user does the following:
- Tries to open a doc with an existing 'dirty' doc open
- Clicks cancel on the save/dont save/cancel alert (this works ok)
- Then tries to open a doc again. For some reason now the openDocumentWithContentsOfURL method never gets called again, even though the open dialog appears.
Can anyone help me work out why? Or perhaps point me to an example of how to do this right? It feels like something that must have been implemented by a few people but I've not been able to find a 10.7+ example.
- (BOOL)presentError:(NSError *)error
{
if([error.domain isEqualToString:DOCS_ERROR_DOMAIN] && error.code == MULTIPLE_DOCS_ERROR_CODE)
return NO;
else
return [super presentError:error];
}
- (id)openUntitledDocumentAndDisplay:(BOOL)displayDocument error:(NSError **)outError
{
if(self.currentDocument) {
[self closeAllDocumentsWithDelegate:self
didCloseAllSelector:#selector(openUntitledDocumentAndDisplayIfClosedAll: didCloseAll: contextInfo:)
contextInfo:nil];
NSMutableDictionary* details = [NSMutableDictionary dictionary];
[details setValue:#"Suppressed multiple documents" forKey:NSLocalizedDescriptionKey];
*outError = [NSError errorWithDomain:DOCS_ERROR_DOMAIN code:MULTIPLE_DOCS_ERROR_CODE userInfo:details];
return nil;
}
return [super openUntitledDocumentAndDisplay:displayDocument error:outError];
}
- (void)openUntitledDocumentAndDisplayIfClosedAll:(NSDocumentController *)docController
didCloseAll: (BOOL)didCloseAll
contextInfo:(void *)contextInfo
{
if(self.currentDocument == nil)
[super openUntitledDocumentAndDisplay:YES error:nil];
}
- (void)openDocumentWithContentsOfURL:(NSURL *)url
display:(BOOL)displayDocument
completionHandler:(void (^)(NSDocument *document, BOOL documentWasAlreadyOpen, NSError *error))completionHandler NS_AVAILABLE_MAC(10_7)
{
NSLog(#"%s", __func__);
if(self.currentDocument) {
NSDictionary *info = [NSDictionary dictionaryWithObjectsAndKeys:[url copy], #"url",
[completionHandler copy], #"completionHandler",
nil];
[self closeAllDocumentsWithDelegate:self
didCloseAllSelector:#selector(openDocumentWithContentsOfURLIfClosedAll:didCloseAll:contextInfo:)
contextInfo:(__bridge_retained void *)(info)];
} else {
[super openDocumentWithContentsOfURL:url display:displayDocument completionHandler:completionHandler];
}
}
- (void)openDocumentWithContentsOfURLIfClosedAll:(NSDocumentController *)docController
didCloseAll: (BOOL)didCloseAll
contextInfo:(void *)contextInfo
{
NSDictionary *info = (__bridge NSDictionary *)contextInfo;
if(self.currentDocument == nil)
[super openDocumentWithContentsOfURL:[info objectForKey:#"url"] display:YES completionHandler:[info objectForKey:#"completionHandler"]];
}
There's a very informative exchange on Apple's cocoa-dev mailing list that describes what you have to do in order to subclass NSDocumentController for your purposes. The result is that an existing document is closed when a new one is opened.
Something else you might consider is to mute or stop playing a document when its window resigns main (i.e., sends NSWindowDidResignMainNotification to the window's delegate), if only to avoid forcing what might seem to be an artificial restriction on the user.
I know it's been a while, but in case it helps others....
I had what I think is a similar problem, and the solution was to call the completion handler when my custom DocumentController did not open the document, e.g.:
- (void)openDocumentWithContentsOfURL:(NSURL *)url display:(BOOL)displayDocument completionHandler:(void (^)(NSDocument * _Nullable, BOOL, NSError * _Nullable))completionHandler {
if (doOpenDocument) {
[super openDocumentWithContentsOfURL:url display:displayDocument completionHandler:completionHandler];
} else {
completionHandler(NULL, NO, NULL);
}
}
When I added the completionHandler(NULL, NO, NULL); it started working for more than a single shot.

Game Center turn-based match data is not saved and/or read

I am planning to develop a turn-based game and is trying to understand how to communicate with Game Center and send and receive mach data. I have read about it and tested this for days now and just cannot get it to work as planned.
The only thing i try to do with the code below is to be able to save and then read the mach data. I am using two sandbox Game Center accounts for the turns.
The turns are sending the same data by pressing "endTurn" button. Every time i run the actual user is authenticated and the app is set up correctly (i believe).
This is a test app without any other purpose than test what i stated. Below is the code i use for the match data processing.
I would really appreciate any ideas and tips on what i may do wrong. Before i started serious testing i did post a similar question but that did not solve this problem, https://stackoverflow.com/questions/14447392/start-gamecenter-turn-based-match-and-initiate-match-data-for-the-very-first-tim.
I also try to catch the participants but with no success, which may mean that it is the problem when processing the completionhandler.
-(IBAction)endTurn:(id)sender {
[_gameDictionary setObject:#"The Object" forKey:#"The Key"];
NSLog(#"_gameDictionary: %#", _gameDictionary);
NSData *data = [NSPropertyListSerialization dataFromPropertyList:_gameDictionary format:NSPropertyListXMLFormat_v1_0 errorDescription:nil];
GKTurnBasedParticipant *nextPlayer;
if (_match.currentParticipant == [_match.participants objectAtIndex:0]) {
nextPlayer = [[_match participants] lastObject];
} else {
nextPlayer = [[_match participants]objectAtIndex:0];
}
NSLog(#"_match.currentParticipant: %#", _match.currentParticipant);
[self.match endTurnWithNextParticipant:nextPlayer matchData:data completionHandler:^(NSError *error) {
if (error) {
NSLog(#"An error occured updating turn: %#", [error localizedDescription]);
}
[self.navigationController popViewControllerAnimated:YES];
}];
}
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
_gameDictionary = [[NSMutableDictionary alloc]init];
[self.match loadMatchDataWithCompletionHandler:^(NSData *matchData, NSError *error) {
NSDictionary *myDict = [NSPropertyListSerialization propertyListFromData:_match.matchData mutabilityOption:NSPropertyListImmutable format:nil errorDescription:nil];
[_gameDictionary addEntriesFromDictionary: myDict];
if (error) {
NSLog(#"loadMatchData - %#", [error localizedDescription]);
}
}];
NSLog(#"_gameDictionary: %#", _gameDictionary);
}
Output:
"gk-cdx" = "17.173.254.218:4398";
"gk-commnat-cohort" = "17.173.254.220:16386";
"gk-commnat-main0" = "17.173.254.219:16384";
"gk-commnat-main1" = "17.173.254.219:16385";
}
2013-02-11 22:44:11.707 GC_test1[8791:14f03] _gameDictionary: {
}
2013-02-11 22:44:13.894 GC_test1[8791:14f03] _gameDictionary: {
The Object = The Key;
}
2013-02-11 22:44:13.894 GC_test1[8791:14f03] _match.currentParticipant: (null)
The fact that _match.currentParticipant evaluates to nil is troubling. I suspect that _match was never initialized, or is nil, or that it was not obtained from a Game Center facility such as loadMatchesWithCompletionHandler:, the GKTurnBasedMatchmakerViewController, or using findMatchForRequest:withCompletionHandler:.
For a new match, if created through any of these facilities, currentParticipant would be guaranteed to represent the local player. You are not allowed to instantiate a GKTurnBasedMatch yourself.
To resolve this issue at least for testing, you could assign a new _match from within the completion handler of findMatchForRequest:withCompletionHandler:. Only then should you be allowed to press your test button.

Objective-C: Unable to send/receive data after two matchmaking two devices using Game Center

I was hoping someone would be able to help with an Objective-C problem I have relating to sending and receiving information between two devices using GameCenter once they have been through matchmaking.
I am using a textbook called Beginning iOS Game Center and Game Kit as my guide, and it is going through an example program, but I am stuck on the part where I would like to receive data from a device playing the game.
I can successfully matchmake two devices and the appropriate view to appear. I have two functions in my GameCenterManager.m file to send information - one being the following:
- (void)sendStringToAllPeers:(NSString *)dataString reliable:(BOOL)reliable
{
NSLog(#"Send String To All Peers");
NSLog(#"Data String: %#", dataString);
NSLog(#"match or session %#", self.matchOrSession);
if (self.matchOrSession == nil)
{
NSLog(#"GC Manager matchorsession ivar was not set - this needs to be set with the GKMatch or GKSession before sending or receiving data");
return;
}
NSData *dataToSend = [dataString dataUsingEncoding:NSUTF8StringEncoding];
GKSendDataMode mode;
if (reliable)
{
mode = GKSendDataReliable;
}
else{
mode = GKSendDataUnreliable;
}
NSError *error = nil;
if ([self.matchOrSession isKindOfClass:[GKSession class]])
{
NSLog(#"Match or session 1");
NSLog(#"Data to send: %#", dataToSend);
[self.matchOrSession sendDataToAllPeers:dataToSend withDataMode:mode error:&error];
}
else if ([self.matchOrSession isKindOfClass:[GKMatch class]])
{
NSLog(#"Match or session 2");
NSLog(#"Data to send: %#", dataToSend);
[self.matchOrSession sendDataToAllPlayers:dataToSend withDataMode:mode error:&error];
}
else
{
NSLog(#"GC Manager matchOrSession was not a GKMatch or a GK Session, we are unable to send data");
}
if (error != nil)
{
NSLog(#"An error occurred while sending data %#", [error localizedDescription]);
}
}
this function I call from a function in my racetohundredViewController.m file:
- (void)generateAndSendHostNumber;
{
NSLog(#"Generate and send host number");
randomHostNumber = arc4random();
NSString *randomNumberString = [NSString stringWithFormat: #"$Host:%f", randomHostNumber];
NSLog(#"the random number string is: %#", randomNumberString);
[self.gcManager sendStringToAllPeers:randomNumberString reliable: YES];
}
I successfully get the following NSLog output resulting from this:
2013-01-02 22:27:43.519 First to 50[1376:907] Send String To All Peers
2013-01-02 22:27:43.520 First to 50[1376:907] Data String: $Host:2087825492.000000
2013-01-02 22:27:43.521 First to 50[1376:907] match or session <GKMatch 0x200853d0 expected count: 0 seqnum: 2
G:1656671636:connected
reinvitedPlayers:(
)>
2013-01-02 22:27:43.522 First to 50[1376:907] Match or session 2
2013-01-02 22:27:43.523 First to 50[1376:907] Data to send: <24486f73 743a3230 38373832 35343932 2e303030 303030>
So I can see the 'Data to send' output which is great.
However I now have the command
[self.matchOrSession sendDataToAllPeers:dataToSend withDataMode:mode error:&error];
which doesn't seem to take me anywhere at all. I have the following function in GameCenterManager.m:
- (void)receiveData:(NSData *)data fromPeer:(NSString *)peer inSession: (GKSession *)session context:(void *)context
{
NSLog(#"*****Receive Data In Session");
NSString *dataString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSDictionary *dataDictionary = [NSDictionary dictionaryWithObjects:[NSArray arrayWithObjects:dataString, peer, session, nil] forKeys:[NSArray arrayWithObjects: #"data", #"peer", #"session", nil]];
[self callDelegateOnMainThread: #selector(receivedData:) withArg: dataDictionary error: nil];
}
But I am not seeing the NSLog output from this. Likewise, I have a function in my racetohundredViewController.m file
- (void)receivedData:(NSDictionary *)dataDictionary
{
NSLog(#"------Received Data");
}
which also doesn't get called; presumably because the previous function isn't able to call it.
I have been trying to work out why this doesn't work for a while now without any avail. Can anyone point out where I am going wrong? I hope I have put in all the relevant code but if you have any questions please ask.
Thanks to all, in advance.
I realised what I had done after looking online at other people's problems with the same thing. I had not set my delegate after finding a match, a very rookie mistake!

Return User's State as an NSString with reverseGeocodeLocation

I am trying to simply return a user's state. I understand that I need to use reverseGeocodeLocation. I would like to return the state as an NSString in the same way that I am returning the user latitude below:
- (NSString *)getUserLatitude
{
NSString *userLatitude = [NSString stringWithFormat:#"%f",
locationManager.location.coordinate.latitude];
return userLatitude;
}
I currently have this code, but I cannot get it to work. It may be because I am using (void). I am just a bit lost.
- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation
*)newLocation fromLocation:(CLLocation *)oldLocation {
CLGeocoder * geoCoder = [[CLGeocoder alloc] init];
[geoCoder reverseGeocodeLocation:newLocation completionHandler:^(NSArray *placemarks,
NSError *error) {
for (CLPlacemark * placemark in placemarks) {
NSString *userState = [placemark locality];
return userState;
}
}];
}
Anyone have any ideas? Thank you!
You have to do something with the retrieved locality in the completion block. This code is executed asynchronously long after the method (with void return) has returned itself.
Usually you would call some sort of method on your own view controller or model class that passes the retrieved information.
Replace the return userState, it does not match the return type of the block.
Instead put something like:
[myViewController didFinishGettingState:userState];
You should look into block and GCD basics so that you can appreciate how this asynchnonicity works.
You are probably not understanding the way your completionHandler is working. The reverseGeocodeLocation:completionHandler: takes an handler, which is a function that will be executed when the lookup is completed and invoked with the placemarks and error as parameters.
What you have to do is to perform something meaningful in that block.
I would start checking whether any error occurred, then I would call a method for failing or success as follows
[geoCoder reverseGeocodeLocation:newLocation completionHandler:^(NSArray *placemarks, NSError *error) {
if (error != nil) {
// Something bad happened...
[self didFailRetrievingUserState:error];
} else {
// Check whether the placemark retrieved is unique
if (placemarks.count > 1) {
NSMutableArray * states = [NSMutableArray array];
for (CLPlacemark * placemark in placemarks) {
NSString * userState = [placemark locality];
[states addObject:userState];
}
[self didFinishRetrievingUserStates:states];
} else {
[self didFinishRetrievingUserState:[placemarks[0] locality]];
}
}
}];
Then of course you need to implement the three methods we are calling in the block above
- (void)didFailRetrievingUserState:(NSError *)error {
// Show error
}
- (void)didFinishRetrievingUserStates:(NSArray *)userStates {
// Do something reasonable with the multiple possible values
}
- (void)didFinishRetrievingUserState:(NSString *)userState {
// Do something reasonable with the only result you have
}
Clearly the above code is meant as a suggestion. You can make different decisions, like for instance handling all the logic inside the handler block, or not discriminating between the unique/not unique cases.
In general it's just important that you understand that the handler block is not supposed to return anything since it's a void function. It's just supposed to do something, and this something may be invoking your "delegate" methods as defined in the example.

Using MKReverseGeocoder in a singleton class (ARC)

I'm in the process of trying to create a singleton class for weather functions (all encompassed) so that I can change/update/recall weather data throughout my entire app in a single allocated object.
I have it working except for one little weird bug. I'm using MKReverseGeocoder on devices running iOS < 5. CLGeocoder works fine on iOS 5+. Basically, this is what happens in the app:
In the app delegate, at launch, I create the shared instance of my weather singleton. This instance doesn't do anything unless there is a UIView that expects to report weather results. When a view that reports weather is loaded, if the location isn't already statically set in the preferences, the app attempts to pull your current location.
This works, and I can log the coordinates of the devices current location. After that is done, I immediately go into trying to reverse geolocate those coordinates. The method MKReverseGeocoder start does get executed and I can log that the instance's isQuerying property is true, so I know it's attempting to geolocate the coords. (Yes, I set the delegate as my shared instance and the instance is of type MKReverseGeocoderDelegate).
Now this is the weird part. If I am launching the app for the first time, and I add a weather UIView to the screen for the first time, MKReverseGeocoder starts but never calls the delegate method. If I then close the app and open it again (second time), the current location get's looked up, and MKReverseGeocoder does call the delegate methods and everything works. It just doesn't want to work on the first launch, no matter how many times I call it (I have a button that can initiate the lookup).
It's totally baffling. The iOS 5 CLGeocoder works fine on initial launch and every subsequent launch. the MKReverseGeocoder does not work (because it doesn't call the delegate methods) on initial launch, but does on subsequent launches.
Below is the relevant code:
-(void)reverseGeocoder:(MKReverseGeocoder *)geocoder didFailWithError:(NSError *)error{
NSLog(#"error getting location:%#", error);
self.reverseGeocoder = nil;
[[NSNotificationCenter defaultCenter] postNotificationName:#"errorGettingLocation" object:nil userInfo:[NSDictionary dictionaryWithObject:error forKey:#"error"]];
}
- (void)reverseGeocoder:(MKReverseGeocoder *)geocoder didFindPlacemark:(MKPlacemark *)pm
{
//update placemark and get weather from correct placemark
placemark = pm;
NSLog(#"singleton placemark: %#",placemark);
[self getLocationFromPlacemark];
self.reverseGeocoder = nil;
}
- (void) getReverseGeoCode:(CLLocation *)newLocation
{
NSLog(#"reversGeocode starting for location: %#", newLocation);
NSString *ver = [[UIDevice currentDevice] systemVersion];
float ver_float = [ver floatValue];
if (ver_float < 5.0) {
//reverseGeocoder = nil;
self.reverseGeocoder = [[MKReverseGeocoder alloc] initWithCoordinate:newLocation.coordinate];
self.reverseGeocoder.delegate = self;
[self.reverseGeocoder start];
if(self.reverseGeocoder.isQuerying){
NSLog(#"self.reverseGeocoder querying");
NSLog(#"self.reverseGeocoder delegate %#", self.reverseGeocoder.delegate);
NSLog(#"self %#", self);
}else {
NSLog(#"geocoder not querying");
}
}
else {
[reverseGeocoder5 reverseGeocodeLocation:newLocation completionHandler:^(NSArray *placemarks, NSError *error){
if([placemarks count]>0){
placemark = [placemarks objectAtIndex:0];
[self getLocationFromPlacemark];
}
else{
if (error) {
NSLog(#"error reverseGeocode: %#",[error localizedDescription]);
}
}
}];
}
}
Also, I am setting reverserGeocoder as a (nonatomic, strong) property and synthesizing it. Remember, this works fine after a clean launch the second time (when there is a weather UIView already loaded). The calls to log aren't even getting hit (which is why I'm assuming the delegate methods aren't getting hit).
Any input would be GREATLY appreciated!
Thanks!
I ended up figuring this out... well, kind of.
Instead of using MKReverseGeocoder, I'm just doing a static lookup to google maps ala How to deal with MKReverseGeocoder / PBHTTPStatusCode=503 errors in iOS 4.3?
Works perfectly.
MKReverseGeocoder doesn't exist anymore... in iOS 5
You have to import CoreLocation and use CLGeocoder. Below is just some example sample code :
CLGeocoder *geoCoder = [[CLGeocoder alloc] init];
CLLocation *location = [[CLLocation alloc] initWithLatitude:37.33188 longitude:-122.029497];
[geoCoder reverseGeocodeLocation:location completionHandler:^(NSArray *placemark, NSError *error) {
CLPlacemark *place = [placemark objectAtIndex:0];
if (error) {
NSLog(#"fail %#", error);
} else {
NSLog(#"return %#", place.addressDictionary);
}
}];