Connect an iPad via BT MIDI as Peripheral (CABTMidiLocalPeripheralViewController) - ios

I am programming a MIDI tool on my iPad which connects to a synthesizer using Bluetooth.
First thing I did was to use the iPad as Central controller with the CABTMidiCentralViewController - if I connect to a Bluetooth device here I immediately get a notification that a new MIDI device has connected and everything's fine.
Now to support another Bluetooth dongle I need to config the iPad as peripheral, for that I use CABTMidiLocalPeripheralViewController. On the other side of BT (the central device which is NOT the iPad) I see the iPad advertising its service and I can connect to it, however I have no idea how to recognize this connection on the iPad side - I don't get any notification about new devices. Probably I'm missing something here?
I use CABTMidiLocalPeripheralViewController just like in the documentation example for CABTMidiCentralViewController, so that's the code (it's just displaying the vc anyway, isn't it?):
- (void)configCABTMIDIPeripheral:(id)sender
{
CABTMIDILocalPeripheralViewController *viewController =[CABTMIDILocalPeripheralViewController new];
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:viewController];
viewController.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem: UIBarButtonSystemItemDone target: self action: #selector(didConfigCABTMIDIPeripheral:)];
navController.modalPresentationStyle = UIModalPresentationPopover;
UIPopoverPresentationController *popC = navController.popoverPresentationController;
popC.permittedArrowDirections = UIPopoverArrowDirectionAny;
popC.sourceRect = [sender frame];
UIButton *button = (UIButton *)sender;
popC.sourceView = button.superview;
[self.delegateBaseViewController presentViewController:navController animated:YES completion:nil];
}
As for the notifications I register a static callback function when I create a MIDI client - my guess is that as a peripheral device this works different, maybe someone here can point me in the right direction:
OSStatus s = 0;
s = MIDIClientCreate((CFStringRef)#"MIDI Client", PGMIDINotifyProc, arc_cast<void>(self), &_client); // Create MIDI Client
s = MIDIOutputPortCreate(_client, (CFStringRef)#"Output Port", &_outputPort); // Create MIDI output port
s = MIDIInputPortCreate(_client, (CFStringRef)#"Input Port", PGMIDIReadProc, arc_cast<void>(self), &_inputPort); // Create MIDI input port
...with PGMIDINotifyProc looking like...
void PGMIDINotifyProc(const MIDINotification *message, void *refCon)
{
NSLog(#"PGMIDINotifyProc");
PGMidi *self = arc_cast<PGMidi>(refCon);
[self midiNotify:message];
}
(Credits to PGMidi at https://github.com/petegoodliffe/PGMidi)
So... how can I see if my peripheral iPad did connect to a MIDI device?
UPDATE
After a long break I decided to give it another try, this time I made some tests by implementing a CBPeripheralManager and a CBCentralManager on two devices (one peripheral and one central) to have a closer look on what's going on behind the scenes:
Without going too much into detail, here are some observations:
if the peripheral device doesn't power on the CBPeripheralManager, no Bluetooth services are offered. Makes sense.
if the peripheral device offers some random service, the central device will discover this service they connect and can exchange data. Great.
if the peripheral device still offers some random service while the central specifically looks for the MIDI service / characteristics, it will find the MIDI service and connect to it. I don't get any notifications about this on the peripheral side, since I'm just the delegate of the peripheral offering some random service.
if both the peripheral device and the central offer / scan for the MIDI service / characteristics, they don't find anything.
It's like the MIDI service appears on it's own if I offer some other service but is not there if I offer no service. Moreover the iPad seems to block or catch the MIDI service / characteristic if I try to offer it myself.
Any ideas here? Come on :) :)

Assuming you use CABTMIDILocalPeripheralViewController to connect, try replace arc_cast<void>(self) with (__bridge void*)self as the current PGMidi library does, but if you use this library you should not write this client initialization yourself, it is created inside the [[PGMidi alloc] init] call. anyway can you see the PGMIDINotifyProc is been called?
let me know

Related

BLE communication between a Garmin wearable and an iOS device

I would like to create a Garmin wearable app (Data Field), which would communicate with my iOS app using Bluetooth LE (BluetoothLowEnergy API on Garmin and CoreBluetooth on iOS). There's a limitation of Garmin's API - it can work only as a central device, so I configured my iPhone to act as a "virtual" peripheral (I tested it both with my own debug app and LightBlue).
I managed to establish a connection between my Garmin Vivoactive 3 Music and my iPhone, but I still have some issues to make it work.
From Garmin wearable I managed to search, find and pair a device (my iPhone virtual peripheral), so that both: self.pairedDevice = BluetoothLowEnergy.pairDevice(scanResult) and BluetoothLowEnergy.getPairedDevices().next() don't return nulls.
The problem I have is that this callback is never called on a Garmin device:
function onConnectedStateChanged(device, connectionState) {
if (connectionState==BluetoothLowEnergy.CONNECTION_STATE_CONNECTED) {
displayString = "Connected";
}
if (connectionState==BluetoothLowEnergy.CONNECTION_STATE_DISCONNECTED) {
displayString = "Disconnected";
}
}
Moreover, when discovering my virtual peripheral, I can see available services in advertisement data, but once the devices are paired, calling device.getServices() returns an empty iterator.
I already checked that BluetoothLowEnergy.getAvailableConnectionCount() is 3, so there should be no problem with regard to connections number limit. Is there any way to force the connection?
On iOS I do something like this:
let service = CBMutableService(type: serviceCbuuid, primary: true)
let writeableCharacteristic = CBMutableCharacteristic(type: characteristicCbuuid,
properties: [.write],
value: nil,
permissions: [.writeable])
service.characteristics = [writeableCharacteristic]
currentService = service
peripheralManager = CBPeripheralManager(delegate: self,
queue: DispatchQueue.global(qos: .userInteractive))
then I add currentService using peripheralManager?.add(currentService) and in didAdd service callback I start advertising by calling peripheral.startAdvertising(options).
Maybe I miss some setting on the iOS end to make this work?

Failing to obtain services from iOS peripheral using chrome.bluetoothLowEnergy

I have an iOS app which advertises itself successfully using a CBPeripheralManager. In a Chrome App, I have successfully discovered the device (hence obtaining its address).
I have added a service with a custom UUID to the peripheral manager. didAddService is called (after the peripheral manager is powered on), and so I assume that part is successful.
I call chrome.bluetoothLowEnergy.connect(device address) in the Chrome app. The callback function is called without issue, and so I believe the connection is successful. There is no function called within the app when the connection has occurred - should there be?
I then call chrome.bluetooth.getServices(device address) in the Chrome app. An array of services is returned, however the array is empty. I believe it should return an array of length 1, not 0.
Am I overlooking something? Why are no services being returned? Should any of the peripheralManager functions be called during these operations?
Connect code:
function connect(address) {
/* Connect persistently to the device. */
chrome.bluetoothLowEnergy.connect(address, {persistent: true}, function() {
if(chrome.runtime.lastError) {
/* If we click a green device, we get this. We should try and handle it
earlier. e.g. by removing the listener, or instead using the click
to disconnect the device. */
if(chrome.runtime.lastError.message == 'Already connected')
setDeviceConnected(address);
/* Print error and exit. */
console.log(chrome.runtime.lastError.message);
return;
}
console.log('Successfully connected to ' + address);
setDeviceConnected(address);
// getDeviceService();
getDeviceServices(address);
});
return true;
}
Get services code:
function getDeviceServices(address) {
chrome.bluetoothLowEnergy.getServices(address, function(services) {
console.log('services.length: ' + services.length);
});
}
Peripheral setup code:
- (void)setupPeripheral {
_serviceName = SERVICE_NAME;
_serviceUUID = [CBUUID UUIDWithString:SERVICE_UUID_STRING];
_characteristicUUID = [CBUUID UUIDWithString:CHARACTERISTIC_UUID_STRING];
_peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:nil];
_characteristic = [[CBMutableCharacteristic alloc] initWithType:_characteristicUUID
properties:CBCharacteristicPropertyRead | CBCharacteristicPropertyNotify
value:nil
permissions:CBAttributePermissionsReadable];
_service = [[CBMutableService alloc] initWithType:_serviceUUID primary:YES];
_service.characteristics = #[_characteristic];
NSLog(#"Peripheral set up");
}
Start advertising (add the service just before we start advertising):
/* Start advertising */
- (void)startAdvertising {
NSLog(#"Starting advertising...");
NSDictionary *advertisment = #{
CBAdvertisementDataServiceUUIDsKey : #[self.serviceUUID],
CBAdvertisementDataLocalNameKey: self.serviceName
};
[self addServices];
[self.peripheralManager startAdvertising:advertisment];
}
Call to start advertising within didUpdateState:
/* Did update state */
- (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral {
switch (peripheral.state) {
case CBPeripheralManagerStatePoweredOn:
NSLog(#"peripheralStateChange: Powered On");
// When the bluetooth has turned on, start advertising.
[self startAdvertising];
break;
Update
On connecting to the peripheral app with OSX app LightBlue, an alert shows up on the app to request a pairing. Then LightBlue can read characteristics from the app. When connecting with the Chrome app, the pairing alert does not show up.
Having chatted to web-bluetooth developers at W3C, I have now been able to discover services from the device, by updating chrome from v51 to v52 (apparantly also v53 works).
However currently (though I am yet to try it out), reading characteristics is currently unsupported for OSX, as seen here.
(I have not yet looked into it but this may provide some options on how to overcome the issues described here.)
I've ran into this problem myself. I've developed a simple app that scans for devices and their services, but all devices found always report back an empty array of services. I have this problem on both OsX and ChromeOS.
There's an exception to this: when the device has previously been paired, I was able to get the services array on ChromeOS. This is confusing to me, since nothing I've read anywhere says a device has to be manually paired first, and there's no programatic way to pair a device one discovered.
EDIT: Upgrading to Chrome 53 fixed the problem. The site linked in louisdeb's answer is related to web-bluetooth, not chrome.bluetooth, so characteristics may actually work.

centralManagerDidUpdateState not getting called

I am not new to iOS coding but very new to BLE in iOS.
I have successfully written code in my Main View Controller to access and write to a BLE device. So all the peripheral and manager stuff is tested.
My app will get a list of BLE devices, connect to them for the serial number, disconnect from them, and then put them in a tableView. When the user selects one, I need to go to a different view and reconnect. I want to write another class as a utility to connect to the peripheral, which I am successfully passing to the utility and read and write to it in the utility. The peripheral.state is connected.
Every time I try to write to the peripheral, my code crashes. However the peripheral reports that the data was written.
So to figure out what was wrong I started to just try and read the peripheral with the same results, Crash.
I'm starting a new manager in the utility and centralManagerDidUpdateState is not getting called there. I also tried it without the manager thinking that if I knew of the peripheral, it would just work.
So, how do I get centralManager to work in the new class? Do you have to somehow stop the manager in the main class?
- (void)getPeripheralStatus:(CBPeripheral *)peripheral{
device_Info_UUID = [CBUUID UUIDWithString:kDevice_Info_SID];
security_UUID = [CBUUID UUIDWithString:kSecurity_SID];
status_UUID = [CBUUID UUIDWithString:kStatus_SID];
device_ID_char_UUID =[CBUUID UUIDWithString:kSecurity_char_DeviceID];
SHA256in = [[NSMutableString alloc]initWithString:lockMasterKey];
mgr = [[CBCentralManager alloc] initWithDelegate:self queue:nil];
peripheral.delegate = self;
[self.mgr connectPeripheral:peripheral options:nil];
This is the line works in the main controller but not in my Utility.
[self.lock writeValue:myID forCharacteristic:charac type:CBCharacteristicWriteWithResponse];
Thanks!
I am not sure passing a peripheral to other viewcontroller is a "good idea" unless you don't need further update on the peripheral. If you want a "fresh" peripheral in multiple view controller i recommend to use singleton design thus you can handle all delegates at once and reduce redundant codes.
sample code I found.
https://github.com/kickingvegas/YmsCoreBluetooth

Network Interface Change Notification

What is the best way to get notification while network interface becomes available/unavailable in iOS, different from Reachability? (maybe there is some good way of doing in SystemConfiguration or CFNetworks framework, or somehow using the native unix sockets networking API?)
Instead of checking network reachability I'm just checking if I'm able to get network info with this function, the only problem is when to check. I don't want to fire up an NSTimer every 0.1 second to do this, though it is solution, I would rather like to somehow get notified when user switches WiFi on/off in settings. (Reachability takes several seconds to nofity me when Im disabling wwan interface in settings.)
- (void)initializeCurrentNetworkInfo
{
NSArray* interfacesSupported = (__bridge_transfer NSArray*) CNCopySupportedInterfaces();
NSLog(#"%s: Supported interfaces: %#", __func__, interfacesSupported);
NSDictionary* interfaceInfo = nil;
for (NSString *interfaceName in interfacesSupported) {
interfaceInfo = (__bridge_transfer NSDictionary*) CNCopyCurrentNetworkInfo((__bridge_retained CFStringRef)interfaceName);
if (interfaceInfo && [interfaceInfo count]) {
self.isInterfaceActive = YES;
self.currentSSID = [NSString stringWithString:interfaceInfo[#"SSID"]];
self.currentBSSID = [NSString stringWithString:interfaceInfo[#"BSSID"]];
break;
} else if (!interfaceInfo){
self.isInterfaceActive = NO;
self.currentBSSID = #"ff:ff:ff:ff:ff:ff";;
self.currentSSID = #"interface is unavailable";
}
}
}
Info: I'm developing the UPNP application, and each time interface becomes available/unavailable I subsequently initialise or nullify my UPNP service object.
The problem is that in some rare occasions my app crashes, while I'm changing Wi-Fi switch on/off consequently for the purpose of testing. (Reachability works fine in the background)
So I could have traced it and found it that it obviously crashes while trying to receive an httpu datagram on socket of no longer available interface, when switching wifi off (recvLen = cg_socket_recv(sock, dgmPkt);), if I understand it right, it means that the background thread listening on socket in my UPNP framework (CyberGarages' CyberLink) is unaware of interface state changes(bug probably?), that's why I really want to stop the upnpService as fast as possible once the interface state changes.
I'm using the Tony Millions' Reachability version, and once I receive networkReachabilityChanged: I check for available interfaces, but I guess it's not the best way of doing it.
Thanks.

"Connection reset by peer" errors with GCDAsyncUdpSocket on iOS6

I am having a problem with using GCDAsyncUdpSocket. I am using the iPad as a user interface app that interacts with another app - call it Host, the latter running on a separate Windows machine. Both machines are on their own private network, so they are on their own subnet. At certain points, the Host sends UDP packets to the iPad to instruct it which screen to show to the user, and the iPad sends user responses via UDP packets to the Host. Finally, the iPad periodically (at 2 Hz) sends simple "heartbeat" messages to the Host.
This all works fine - for a while. Then, apparently, the iPad abruptly stops accepting the UDP packets from the Host - the latter experiences "Connection reset by peer" errors, while it (the iPad) is still successfully sending, and the Host receiving, the heartbeat messages.
I'm thinking the problem comes from my confusion with respect to how Grand Central Dispatch (GCD) works. My iPad app is pretty simple; I based it off a tutorial on iOS programming (I'm a beginner here, but very experienced with Windows, Linux, embedded/real-time, and networking). It basically consists of a main screen, which creates a second screen from time to time. So the basic structure is this:
main.m
Delegate.m
MainViewController.m
PopupViewController.m
The main.m and Delegate.m were created automagically by the Xcode during the tutorial, and have nothing special in them. The MainViewController.m is my "main screen", and owns the GCDAsyncUdpSocket that is used by the iPad app. The final file, PopupViewController.m, is the second screen, that is used like this:
# MainViewController.m
- (IBAction)sendResponseOne:(id)sender {
// Send a message to Host
[self sendUdpMessage:1];
// Switch to other view
PopupViewController *vc = [[PopupViewController alloc] init];
[vc setMainScreen:self]; // Used to access UDP from 2nd screen
[self presentViewController:vc animated:NO completion:nil];
}
# PopupViewController.m
- (IBAction)confirmAnswers:(id)sender
{
// Send a message to Host - calls same function as above main screen
[self->mainScr sendUdpMessage:2];
[self dismissViewControllerAnimated:NO completion:nil];
}
Now for the code that sems to fail. First, here is the #interface section of MainViewController.m:
# From MainViewController.m
#interface MainViewController ()
{
GCDAsyncUdpSocket *udpSocket;
}
#end
Here is how/where I create the UDP object:
# From MainViewController.m
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
if ((self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]))
{
// Setup our socket, using the main dispatch queue
udpSocket = [[GCDAsyncUdpSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
}
return self;
}
Here is where I bind to the port:
# From MainViewController.m
- (void)viewDidLoad
{
[super viewDidLoad];
// Start UDP server
int port = 12349;
NSError *error = nil;
if (![udpSocket bindToPort:port error:&error])
{
NSLog(#"Error starting server (bind): %#", error);
return;
}
if (![udpSocket beginReceiving:&error])
{
[udpSocket close];
NSLog(#"Error starting server (recv): %#", error);
return;
}
[self startPingTimer];
isRunning = YES;
}
Here's the code that receives packets. Apparently, this function works fine for awhile, sometimes dozens of times, then unexpectedly fails.
# From MainViewController.m
- (void)udpSocket:(GCDAsyncUdpSocket *)sock didReceiveData:(NSData *)data
fromAddress:(NSData *)address
withFilterContext:(id)filterContext
{
if (data.length == sizeof(MyMessage)) {
MyMessage msg;
[data getBytes:&msg length:sizeof(MyMessage)];
msg.magic = ntohl(msg.magic);
msg.msgId = ntohl(msg.msgId);
for (int i = 0; i < 4; ++i) {
msg.values[i] = ntohl(msg.values[i]);
}
if (msg.magic == 0xdeadcafe) {
switch (msg.msgId) {
case imiStateControl:
self->iceState = (IceState)msg.values[0];
break;
default:
break;
}
}
}
}
I am at a loss as to why the didReceiveData function seems to work correctly for some random amount of time (and random number of messages sent/received). I wonder a couple of things:
Is it valid for me to send a UDP message from the second screen? I think so, and sending never fails - it continues to work even after receiving fails.
How does the didReceiveData get called anyway, and how could that get broken? If I was in Linux or an RTOS, I would probably have created an explicit thread that waits on packets; how does the GCD framework decide where a packet should go?
Why would my app suddenly stop listening on the port? How do I detect/debug that?
Does it matter that the GCDAsyncUdpSocket object is owned by the main screen, as opposed to the Delegate.m module?
Is it appropriate to use the main dispatch queue, as I think I am doing? Indeed, am I doing that, and correctly?
I'm totally at a loss, so of course, any advice would be greatly appreaciated! No need to answer all the questions - especially if your answer to one is the solution!
Thanks!
This post ended about eight hours of torture that I received at the hands of the POSIX/GCDAsyncSocket/NSStream/NSNetService gods. For anyone coming across this: the cause of my GCDAsyncSocket connection reset by peer/remote peer disconnected errors was simply that I was connecting by IP on the LAN, instead of using a host name. (i.e. using the connectToAddress family of methods instead of the connectToHost family of methods). I fired up WireShark, downloaded X11, all that jazz and confirmed I was facing the same issue Bob was seeing – ARP activity around the disconnect time. Trying to connect to a host instead of an address confirmed Seth's take on the situation – it was issues with the router reassigning IP addresses.
This couldn't have been a more innocuous question on SO – question and answer with 0 upvotes, but both of you combined to give more more than enough information to solve what I thought was an intractable problem. Thanks so much!
It sounds like the receiving UDP socket is either being closed or shifted to a different address/port pair.
If its being closed, the only way outgoing packets could still work is if there were actually two sockets. Perhaps the receive one bound to port 12349 and the send one bound to port 0. Then maybe GCD is closing the receive one after some period of time. Given your followup comments about ARP, this seems less likely but is worth keeping in mind.
The ARP activity suggests that the iPad's IP address may be changing. If it were to change, or if it were to switch from the WiFi interface to a cellular interface, then the packets it sends would still get through, but packets sent to it would fail exactly as described.
Whatever is receiving those heart-beat messages, have it check the recvfrom address and make sure that any messages it sends back go to that exact address. And, of course, make sure to pay attention to endian (host vs network byte order).
I am assuming that there are no firewalls or NAT in between the two devices. If there are such things, then a whole different world of possibilities opens up.

Resources