Havr anyone tried using Core Bluetooth with the ObjC api? Havent figured out how to do it yet. Any example would be appreciated
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
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
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.
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
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)
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.
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.