Anyone tried with Core Bluetooth?

Havr anyone tried using Core Bluetooth with the ObjC api? Havent figured out how to do it yet. Any example would be appreciated :slight_smile:

Can you get a list of methods in a objc class? I tried this, but the scan method is missing…


CBD = objc.delegate("CBCentralManagerDelegate")

function CBD:centralManagerDidUpdateState(central)
    print(central.state)
end
    
function setup()
    d = CBD()
    cm = objc.cls.CBCentralManager(d, nil)
    cm:scanForPeripherals_withServices_(nil)
end

Ok, I made it so far. It wasn’t that obvious how to figure out the naming. Does it say in the documentation that the arguments must be named objClassName to work correctly. But fun to get it to discover BT-devices so far :slight_smile:

CBD = objc.delegate("CBCentralManagerDelegate")

function CBD:centralManagerDidUpdateState_(central)
    print("did", central, central.state)
    print(cm.state)
    if cm.state == 5 then -- poweredOn
        print("scan")
        cm:scanForPeripheralsWithServices_options_(nil)
    end
end

function CBD:centralManager_didDiscoverPeripheral_advertisementData_RSSI_(central,objCBPeripheral,c,d)
    print("discovered name", objCBPeripheral.name)
end

function setup()
    s = Scene()
    d = CBD()
    CM = objc.cls.CBCentralManager
    cm = CM()
    cm:initWithDelegate_queue_(d,nil)
end
2 Likes

I managed to get it working now. I had to convert the NSData to base64 to be able to read it.

Now it reads the heartrate, steps, accelerometer from my watch and displays it. It also sends a message to it.

2 Likes

Should I be able to link this to my own watch?

I guess you have to figure out the api for your watch. I think the pinetime watch uses a standard api for heartrate, but I havent tested it with any other device.

I’ve now rewritten it to read the step counter and motion sensor as well. And send a message to the watch.

May I have the code for that please? I just want to see if it actually works. (Im not trying to say I won’t work, I’m just saying that I like testing things out!)

Sure, here is the code. It only works for the pinetime watch, but if you modify with the device specs you might be able to use it. I havent done anything with it since my watch died when I took a bath with my child.


--# Main


function setup()
    viewer.mode = FULLSCREEN
    --getAuroraProb(function (p)
     -- al:send("Aurora probability", "is " .. p .. "%")
    --end)
    hr = HeartRate()
    steps = Steps()
    motion = RawMotion()
    al = Alert()
    complain = "Lazy like a plant, never moving, never doing anything. Just sitting there, wasting away."
    al:send("motivate!", complain)
    bt = false
    bt = Bluetooth("InfiniTime",
    PineTimeServices, {hr, steps, motion, al})
end

function draw()
    background(40, 40, 50)
   -- sprite(CAMERA, 100,100,200)
    translate(WIDTH/2,HEIGHT/2)    
    sprite(asset.builtin.SpaceCute.Health_Heart)
    fontSize(50)
    textAlign(CENTER)
    text(hr.value)
    text("Steps " .. steps.value, 0, -50)
    text(motion.x .. "," .. motion.y .. "," .. motion.z, 0, -100)

end

function touched(t)
    if t.state == BEGAN and bt == false then

    end
end

function willClose()
    bt:willClose()
end


--# Bluetooth
BTCharacteristic = class()
function BTCharacteristic:init(uuid, initial_value)
    self.uuid = uuid
    self.value = initial_value
end
function BTCharacteristic:eq(uuid)
    return string.upper(self.uuid) == uuid
end
function BTCharacteristic:onConnect()
    
end

Bluetooth = class()
    
    
function Bluetooth:init(name, services, characteristics)
    self.chars = characteristics
    self.connected = {}
    local connected = self.connected
    local bt = self
    CBD = objc.delegate("CBCentralManagerDelegate")
    
    function CBD:centralManagerDidUpdateState_(central)
        if cm.state == 5 then -- poweredOn
            print("Scan for devices")
            cm:scanForPeripheralsWithServices_options_(nil)
        end
    end
    
    function CBD:centralManager_didDiscoverPeripheral_advertisementData_RSSI_(central,objCBPeripheral,c,d)
        --  print("discovered name", objCBPeripheral.name)
        if objCBPeripheral.name == name then
            cm:stopScan()
            print(name, "found. Connecting.")
            cm:connectPeripheral_options_(objCBPeripheral, nil)
        end
    end
    
    function CBD:centralManager_didConnectPeripheral_(central, objCBPeripheral)
        print("connected! discover services.")
        table.insert(connected, objCBPeripheral)
        objCBPeripheral.delegate = pd
        objCBPeripheral:discoverServices_(nil)
    end
    
    
    CBPD = objc.delegate("CBPeripheralDelegate")
    
    function CBPD:peripheral_didDiscoverServices_(objCBPeripheral, error)
        print("services found!!")
        print(objCBPeripheral.services)
        for k,service in ipairs(objCBPeripheral.services) do
            uuid = service.UUID.UUIDString
            --print("service", uuid)
            for i,serviceid in ipairs(services) do
                if uuid == string.upper(serviceid) then
                    print("discover chars")
                    objCBPeripheral:discoverCharacteristics_forService_(nil, service)
                end
            end
        end
    end
    function CBPD:peripheral_didDiscoverCharacteristicsForService_error_(objCBPeripheral, objCBService, error)
        print("discovered characteristics!")
        for k,c in ipairs(objCBService.characteristics) do
            uuid = c.UUID.UUIDString
            print("characteristic", uuid)
            for i,char in ipairs(characteristics) do   
                if char:eq(uuid) then
                    char.char = c
                    char.peri = objCBPeripheral objCBPeripheral:readValueForCharacteristic_(c)
                    objCBPeripheral:setNotifyValue_forCharacteristic_(true, c)
                    char.connected = true
                    char:onConnect()
                    end
            end
        end
    end
    
    function CBPD:peripheral_didUpdateValueForCharacteristic_error_(objCBPeripheral, objCBCharacteristic,error)
        d = objCBCharacteristic.value:base64EncodedStringWithOptions_()      
        st = base64.decode(d)
        uuid = objCBCharacteristic.UUID.UUIDString
        for k,c in ipairs(characteristics) do
            if c:eq(uuid) then
                c:decode(st)
            end
        end
    end
    
    d = CBD()
    pd = CBPD()
    CM = objc.cls.CBCentralManager
    cm = CM()
    cm:initWithDelegate_queue_(d,nil)
    self.cm = cm
end

function Bluetooth:willClose()
    cm:stopScan()
    for k,peri in ipairs(self.connected) do
        print("disconnect")
        self.cm:cancelPeripheralConnection_(peri)
    end
    print("all disconnected")
end

--# Base64
base64 = {}

local extract = _G.bit32 and _G.bit32.extract -- Lua 5.2/Lua 5.3 in compatibility mode
if not extract then
    	if _G.bit then -- LuaJIT
        		local shl, shr, band = _G.bit.lshift, _G.bit.rshift, _G.bit.band
        		extract = function( v, from, width )
            			return band( shr( v, from ), shl( 1, width ) - 1 )
        		end
    	elseif _G._VERSION == "Lua 5.1" then
        		extract = function( v, from, width )
            			local w = 0
            			local flag = 2^from
            			for i = 0, width-1 do
                				local flag2 = flag + flag
                				if v % flag2 >= flag then
                    					w = w + 2^i
                				end
                				flag = flag2
            			end
            			return w
        		end
    	else -- Lua 5.3+
        		extract = load[[return function( v, from, width )
            			return ( v >> from ) & ((1 << width) - 1)
        		end]]()
	end
end


function base64.makeencoder( s62, s63, spad )
	local encoder = {}
	for b64code, char in pairs{[0]='A','B','C','D','E','F','G','H','I','J',
		'K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y',
		'Z','a','b','c','d','e','f','g','h','i','j','k','l','m','n',
		'o','p','q','r','s','t','u','v','w','x','y','z','0','1','2',
		'3','4','5','6','7','8','9',s62 or '+',s63 or'/',spad or'='} do
		encoder[b64code] = char:byte()
	end
	return encoder
end

function base64.makedecoder( s62, s63, spad )
	local decoder = {}
	for b64code, charcode in pairs( base64.makeencoder( s62, s63, spad )) do
		decoder[charcode] = b64code
	end
	return decoder
end

local DEFAULT_ENCODER = base64.makeencoder()
local DEFAULT_DECODER = base64.makedecoder()

local char, concat = string.char, table.concat

function base64.encode( str, encoder, usecaching )
	encoder = encoder or DEFAULT_ENCODER
	local t, k, n = {}, 1, #str
	local lastn = n % 3
	local cache = {}
	for i = 1, n-lastn, 3 do
		local a, b, c = str:byte( i, i+2 )
		local v = a*0x10000 + b*0x100 + c
		local s
		if usecaching then
    			s = cache[v]
    			if not s then
        				s = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[extract(v,0,6)])
        				cache[v] = s
    			end
		else
    			s = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[extract(v,0,6)])
		end
		t[k] = s
		k = k + 1
	end
	if lastn == 2 then
		local a, b = str:byte( n-1, n )
		local v = a*0x10000 + b*0x100
		t[k] = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[64])
	elseif lastn == 1 then
		local v = str:byte( n )*0x10000
		t[k] = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[64], encoder[64])
	end
	return concat( t )
end

function base64.decode( b64, decoder, usecaching )
	decoder = decoder or DEFAULT_DECODER
	local pattern = '[^%w%+%/%=]'
	if decoder then
		local s62, s63
		for charcode, b64code in pairs( decoder ) do
    			if b64code == 62 then s62 = charcode
    			elseif b64code == 63 then s63 = charcode
    			end
		end
		pattern = ('[^%%w%%%s%%%s%%=]'):format( char(s62), char(s63) )
	end
	b64 = b64:gsub( pattern, '' )
	local cache = usecaching and {}
	local t, k = {}, 1
	local n = #b64
	local padding = b64:sub(-2) == '==' and 2 or b64:sub(-1) == '=' and 1 or 0
	for i = 1, padding > 0 and n-4 or n, 4 do
		local a, b, c, d = b64:byte( i, i+3 )
		local s
		if usecaching then
    			local v0 = a*0x1000000 + b*0x10000 + c*0x100 + d
    			s = cache[v0]
    			if not s then
        				local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 + decoder[d]
        				s = char( extract(v,16,8), extract(v,8,8), extract(v,0,8))
        				cache[v0] = s
    			end
		else
    			local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 + decoder[d]
    			s = char( extract(v,16,8), extract(v,8,8), extract(v,0,8))
		end
		t[k] = s
		k = k + 1
	end
	if padding == 1 then
		local a, b, c = b64:byte( n-3, n-1 )
		local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40
		t[k] = char( extract(v,16,8), extract(v,8,8))
	elseif padding == 2 then
		local a, b = b64:byte( n-3, n-2 )
		local v = decoder[a]*0x40000 + decoder[b]*0x1000
		t[k] = char( extract(v,16,8))
	end
	return concat( t )
end

--# PineTime
MOTION_SERVICE = "00030000-78fc-48fe-8e23-433b3a1942d0"
HR_SERVICE = '180d'
ALERT_SERVICE = "1811"
PineTimeServices = {MOTION_SERVICE,HR_SERVICE,ALERT_SERVICE}

HeartRate = class(BTCharacteristic)
function HeartRate:init()
    BTCharacteristic.init(self, '2a37', 0)
end
function HeartRate:decode(value)
    local z,v=string.unpack("BB", value)
    self.value = v
end

Steps = class(BTCharacteristic)
function Steps:init()
    BTCharacteristic.init(self, '00030001-78fc-48fe-8e23-433b3a1942d0', 0)
end
function Steps:decode(value)
    self.value = string.unpack('I', value)
end

RawMotion = class(BTCharacteristic)
function RawMotion:init()
    self.uuid = "00030002-78fc-48fe-8e23-433b3a1942d0"
    self.x, self.y, self.z = 0, 0, 0
end
function RawMotion:decode(value)
    self.x, self.y, self.z = string.unpack('hhh', value)
end

NSData = objc.cls.NSData

Alert = class(BTCharacteristic)
function Alert:init()
    self.uuid = '2a46'
    self.msgs = {}
end
function Alert:sendToDevice(title, content)
    data = string.pack("BBB", 0,1,0) .. title .. string.pack("B",0) .. content
    ns_data = NSData():initWithBase64EncodedString_options_(base64.encode(data))
    print("send", ns_data.description)

-- r = objc.enum.CBCharacteristicWriteWithResponse
    self.peri:writeValue_forCharacteristic_type_(ns_data, self.char, 0)
end
function Alert:onConnect()
    print("onconnect")
    for i,msg in ipairs(self.msgs) do
        self:sendToDevice(msg[1], msg[2])
    end
end
function Alert:send(title, content)
    if self.connected then
        self:sendToDevice(title, content)
    else
       table.insert(self.msgs, {title,content}) 
    end
end


--# Aurora
function getAuroraProb(fn)
    
    local lat = 58.661923
    local lon = 16.0555882
    local url = 'https://aurora-forecast.herokuapp.com/aurora/' .. lat .. '/' .. lon
    http.request(url, function (data)
        data = json.decode(data)
        probability = data["value"][3]
        fn(probability)
    end)
end
3 Likes

Wow, nice work @tnlogy! I’m happy to see it’s possible to use Core Bluetooth through objc, I had not played around with this yet. Could be really fun to have a Codea project interact with an Arduino and Bluetooth module (e.g. HC-06) :slight_smile:

2 Likes

I’ve been thinking about connecting it to a lego boost hub, but have not had time for it yet. And the protocol is a bit messy with some bit flags, which is not so convenient to handle in lua.

But it would be fun to control a robot from Codea, maybe use the iPhone camera as camera for a lego robot.

3 Likes

Can you tell me how to get all of the service codes please because I want to try and connect it to an Apple Watch because I don’t have a PineTime watch, thanks!

I’m afraid you have to google it. Pinetime had some open source drivers, so I could just look it up. Don’t know whats available for apple watch. Don’t have one to test with.