based-connect icon indicating copy to clipboard operation
based-connect copied to clipboard

Here's a version that works on OSX

Open krackers opened this issue 2 years ago • 2 comments

Thanks for all the work in reverse-engineering this! Here's a version that that works with osx/macos, basically using cocoa apis to replay the bytes you reverse-engineered. The code is pretty ugly and there's no error handling (I have no idea what the ack sequences you used are for, but it seems to work fine without it). Link to IOKit and Foundation when compiling.

#import <Cocoa/Cocoa.h>


const char* USAGE_DECL = "Usage:\n\t-h --help                   Help (print this message)\n\t-b --battery                Get battery life  \n\t-s --serial                 Print serial number\n\t-f --firmware               Get firmware version\n\t-z --getsleep               Get auto-off timeout\n\t-v --voice                  Set voice on/off\n\t-q --getnc                  Get current noise cancelling level\n\t-n --setnc [high/low/off]   Set current noise cancelling level";

int main(int argc, char *argv[])
{
    if (argc == 1 || !strcmp(argv[1], "-h"))
    {
        printf("%s\n", USAGE_DECL);
        return 0;
    }
    return NSApplicationMain(argc, (const char **)argv);
}


#import <Cocoa/Cocoa.h>
#import <IOBluetooth/objc/IOBluetoothRFCOMMChannel.h>
#import <IOBluetooth/objc/IOBluetoothDevice.h>

@interface AppDelegate : NSObject <NSApplicationDelegate>
{
    IOBluetoothRFCOMMChannel *mRFCOMMChannel;
}

@property(assign) IBOutlet NSWindow *window;
@end



#import <IOBluetooth/objc/IOBluetoothSDPUUID.h>
#import <IOBluetoothUI/objc/IOBluetoothDeviceSelectorController.h>

#include <getopt.h>

extern int *_NSGetArgc(void);
extern char ***_NSGetArgv(void);
extern const char* USAGE_DECL;

#define ANY 0x00

#define NC_HIGH 0x01
#define NC_LOW 0x03
#define NC_OFF 0x00

#define VP_MASK 0x20

enum PromptLanguage {
	PL_EN = 0x21,
	PL_FR = 0x22,
	PL_IT = 0x23,
	PL_DE = 0x24,
	PL_ES = 0x26,
	PL_PT = 0x27,
	PL_ZH = 0x28,
	PL_KO = 0x29,
	PL_NL = 0x2e,
	PL_JA = 0x2f,
	PL_SV = 0x32
};

int numBytesToss = 0;
int displayCode = 0; // 0 = don't display, 1 = display string, 2 = display uint8_t
bool isInit = false;

@implementation AppDelegate

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
    [self discover];
}

- (void)init_connection
{
    const unsigned char bytes[] = {0x00, 0x01, 0x01, 0x00};
    NSData *dt = [NSData dataWithBytes:bytes length:sizeof(bytes)];
    numBytesToss = 4;
    displayCode = 1;
    isInit = true;
    [self sendMessage:dt];
}

- (void)get_battery_level
{
    const unsigned char bytes[] = {0x02, 0x02, 0x01, 0x00};
    NSData *dt = [NSData dataWithBytes:bytes length:sizeof(bytes)];
    numBytesToss = 4;
    displayCode = 2;
    isInit = false;
    [self sendMessage:dt];
}

- (void)get_serial_number
{
    const unsigned char bytes[] = {0x00, 0x07, 0x01, 0x00};
    NSData *dt = [NSData dataWithBytes:bytes length:sizeof(bytes)];
    numBytesToss = 4;
    displayCode = 1;
    isInit = false;
    [self sendMessage:dt];
}

- (void)get_auto_off
{
    const unsigned char bytes[] = {0x01, 0x04, 0x03, 0x01, ANY};
    NSData *dt = [NSData dataWithBytes:bytes length:sizeof(bytes)];
    numBytesToss = 4;
    displayCode = 2;
    isInit = false;
    [self sendMessage:dt];
}

- (void)get_noise_cancelling
{
    const unsigned char bytes[] = {0x01, 0x06, 0x03, 0x02, ANY, 0x0b};
    NSData *dt = [NSData dataWithBytes:bytes length:sizeof(bytes)];
    numBytesToss = 3;
    displayCode = 4;
    isInit = false;
    [self sendMessage:dt];
}


- (void)set_noise_cancelling:(char)newLevel
{
    const unsigned char bytes[] = {0x01, 0x06, 0x02, 0x01, newLevel};
    NSData *dt = [NSData dataWithBytes:bytes length:sizeof(bytes)];
    numBytesToss = 4;
    displayCode = 2;
    isInit = false;
    [self sendMessage:dt];
}

- (void)set_voice:(BOOL) newVal
{
    const uint8_t lang = PL_JA;
    uint8_t language = newVal ? lang | VP_MASK : lang & ~VP_MASK;
    const unsigned char bytes[] = { 0x01, 0x03, 0x02, 0x01, language };
    NSData *dt = [NSData dataWithBytes:bytes length:sizeof(bytes)];
    numBytesToss = 4;
    displayCode = 2;
    isInit = false;
    [self sendMessage:dt];
}

- (void)get_firmware_version
{
    const unsigned char bytes[] = {0x00, 0x05, 0x01, 0x00};
    NSData *dt = [NSData dataWithBytes:bytes length:sizeof(bytes)];
    numBytesToss = 4;
    displayCode = 1;
    isInit = false;
    [self sendMessage:dt];
}

- (void)sendMessage:(NSData *)dataToSend
{
    //[self log:@"Sending Message\n"];
    [mRFCOMMChannel writeSync:(void *)dataToSend.bytes length:dataToSend.length];
}

- (void)log:(NSString *)text
{
    printf("%s", [text UTF8String]);
}


- (void)dispatchAction
{

    int argc = *_NSGetArgc();
    char **argv = *_NSGetArgv();

    if (argc == 1)
    {
        printf("%s\n", USAGE_DECL);
        [NSApp terminate:nil];
    }

    int c;
    const char *short_opt = "hfbszqnv:";
    struct option long_opt[] = {{"help", no_argument, NULL, 'h'},
                                {"battery", no_argument, NULL, 'b'},
                                {"voice", required_argument, NULL, 'v'},
                                {"serial", no_argument, NULL, 's'},
                                {"firmware", no_argument, NULL, 'f'},
                                {"getsleep", no_argument, NULL, 'z'},
                                {"getnc", no_argument, NULL, 'q'},
                                {"setnc", required_argument, NULL, 'n'},
                                {"voice", required_argument, NULL, 'v'},
                                {NULL, 0, NULL, 0}};

    while ((c = getopt_long(argc, argv, short_opt, long_opt, NULL)) != -1)
    {
        switch (c)
        {
            case -1: /* no more arguments */
            case 0:  /* long options toggles */
                break;

            case 'b':
                [self get_battery_level];
                break;

            case 's':
                [self get_serial_number];
                break;

            case 'f':
                [self get_firmware_version];
                break;

            case 'z':
                [self get_auto_off];
                break;

            case 'q':
                [self get_noise_cancelling];
                break;

            case 'h':
                printf("%s\n", USAGE_DECL);
                [NSApp terminate:nil];
                break;

            case 'n':
            {
                if (!strcmp(optarg, "high"))
                {
                    [self set_noise_cancelling:NC_HIGH];
                }
                else if (!strcmp(optarg, "low"))
                {
                    [self set_noise_cancelling:NC_LOW];
                }
                else
                {
                    [self set_noise_cancelling:NC_OFF];
                }
            }
            break;
                
                
            case 'v':
            {
                if (!strcmp(optarg, "on"))
                {
                    [self set_voice: true];
                }
                else if (!strcmp(optarg, "off"))
                {
                    [self set_voice: false];
                }
            }
                break;

            case ':':
            case '?':
                fprintf(stderr, "Try `--help\' for more information.\n");
                [NSApp terminate:nil];
                break;

            default:
                fprintf(stderr, "invalid option -- %c\n", c);
                fprintf(stderr, "Try `--help\' for more information.\n");
                [NSApp terminate:nil];
        };
    };
}

- (void)discover
{
    IOBluetoothDeviceSelectorController *deviceSelector;
    IOBluetoothSDPUUID *sppServiceUUID;

    IOBluetoothRFCOMMChannel *chan;

    [self log:@"Attempting to connect. If frozen, ensure Bose QC35 is paired and active\n"];

    // The device selector will provide UI to the end user to find a remote device
    deviceSelector = [IOBluetoothDeviceSelectorController deviceSelector];

    if (deviceSelector == nil)
    {
        [self log:@"Error - unable to allocate IOBluetoothDeviceSelectorController.\n"];
        return;
    }

    sppServiceUUID = [IOBluetoothSDPUUID uuid16:kBluetoothSDPUUID16ServiceClassSerialPort];

    IOBluetoothDevice *device = [IOBluetoothDevice deviceWithAddressString:@"00:00:00:00:00:00"];
    IOBluetoothSDPServiceRecord *sppServiceRecord = [device getServiceRecordForUUID:sppServiceUUID];
    if (sppServiceRecord == nil)
    {
        [self log:@"Error - no spp service in selected device.  ***This should never happen since the selector forces the user to select only devices with spp.***\n"];
        return;
    }
    // To connect we need a device to connect and an RFCOMM channel ID to open on the device:
    UInt8 rfcommChannelID;
    if ([sppServiceRecord getRFCOMMChannelID:&rfcommChannelID] != kIOReturnSuccess)
    {
        [self log:@"Error - no spp service in selected device.  ***This should never happen an spp service must have an rfcomm channel id.***\n"];
        return;
    }

    // Open asyncronously the rfcomm channel when all the open sequence is completed my implementation of "rfcommChannelOpenComplete:" will be called.
    if (([device openRFCOMMChannelAsync:&chan withChannelID:rfcommChannelID delegate:self] != kIOReturnSuccess) && (chan != nil))
    {
        // Something went bad (looking at the error codes I can also say what, but for the moment let's not dwell on
        // those details). If the device connection is left open close it and return an error:
        [self log:@"Error - open sequence failed.***\n"];
        return;
    }

    mRFCOMMChannel = chan;
}

- (void)rfcommChannelOpenComplete:(IOBluetoothRFCOMMChannel *)rfcommChannel status:(IOReturn)error
{

    if (error != kIOReturnSuccess)
    {
        [self log:@"Error - failed to open the RFCOMM channel with error %08lx.\n"];

        return;
    }
    else
    {
        [self log:@"Connected\n"];
        [self init_connection];
    }
}

- (void)rfcommChannelData:(IOBluetoothRFCOMMChannel *)rfcommChannel data:(void *)dataPointer length:(size_t)dataLength
{

    if (isInit)
    {
        [self dispatchAction];
        return;
    }

    NSData *message = [[NSData alloc] initWithBytes:((char *)dataPointer + numBytesToss)length:dataLength];
    NSLog(@"%@", message);
    if (displayCode == 1)
    {
        NSString *message = [[NSString alloc] initWithBytes:((char *)dataPointer + numBytesToss)length:dataLength encoding:NSUTF8StringEncoding];
        [self log:message];
    }
    else if (displayCode == 4)
    {
        char val = *(char *)([message bytes]);
        if (val == NC_HIGH)
        {
            printf("%s", "High");
        }
        else if (val == NC_LOW)
        {
            printf("%s", "Low");
        }
        else
        {
            printf("%s", "Off");
        }
    }
    else
    {
        char val = *(char *)([message bytes]);
        printf("%d", val);
    }
    [NSApp terminate:nil];
}

@end

krackers avatar May 09 '22 05:05 krackers

did you get this working on an apple silicon mac?

I used chatgpt to come up with the compile command, unsurprisingly it failed

$ gcc -o based-connect based-connect.m -ObjC -arch arm64 -framework IOKit -framework Foundation
Undefined symbols for architecture arm64:
  "_NSApp", referenced from:
      -[AppDelegate dispatchAction] in based-connect-70700c.o
      -[AppDelegate rfcommChannelData:data:length:] in based-connect-70700c.o
     (maybe you meant: __OBJC_PROTOCOL_$_NSApplicationDelegate, __OBJC_LABEL_PROTOCOL_$_NSApplicationDelegate )
  "_NSApplicationMain", referenced from:
      _main in based-connect-70700c.o
  "_OBJC_CLASS_$_IOBluetoothDevice", referenced from:
      objc-class-ref in based-connect-70700c.o
  "_OBJC_CLASS_$_IOBluetoothDeviceSelectorController", referenced from:
      objc-class-ref in based-connect-70700c.o
  "_OBJC_CLASS_$_IOBluetoothSDPUUID", referenced from:
      objc-class-ref in based-connect-70700c.o
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

bill-7 avatar Dec 07 '22 12:12 bill-7

Yes, it will work fine on M1 (although sometimes you have to try several times before it finds the device, not sure why. Seems like a monterey bug).

Due to a change in getopt implementation, you need to change the getopt short_opt to

const char *short_opt = "hfbszqn:v:";

(add colon after n) so it doesn't segfault when parsing input.

Also the compile command you gave is not quite enough, you also need linker flags -framework AppKit and -framework IOBluetooth. But also I think since it uses application delegate, it needs to be in a bundle structure with a default nib, otherwise no delegate will get loaded. So I recommend you create default xcode project and paste in that code (might need to put everything besides main into the AppDelegate.m so nib can launch it properly).

But it should also be possible to avoid the need for bundle by doing like https://stackoverflow.com/questions/8137538/cocoa-applications-from-the-command-line and manually or https://gist.github.com/karstenBriksoft/2bfe71e97e9b3ae1edd5bf4c37d55ecb and manually creating NSApplication object and setting its delegate

krackers avatar Dec 07 '22 17:12 krackers