Home ← TI-Nspire Authoring ← TI-Nspire Scripting HQ ← Scripting Tutorial - Lesson 33
Scripting Tutorial - Lesson 33: BLE - Measuring Heart Rate
Back in an earlier lesson of this sequence, you were introduced to the fundamentals of BLE using the Vernier Go Wireless Temp probe. You might remember how simple this process was, in comparison to the TI Sensor Tag. Where the SensorTag requires two-way communication between BLE device and script, the Vernier probe was happy just to broadcast temperature data. If the user knew the correct UUID, this could be readily captured.
In this lesson, we build another simple script, this time for a heart rate monitor. We will be using the Scosche RHYTHM+ but what is important to note is that the manufacturers of BLE heart rate monitors have agreed upon common UUIDs, and this means that your script should be much more versatile.
For such a script, then, you should need to know only two things - the UUID value, and the calculation by which the raw data is converted into usable values.
Starting with the Vernier script from Lesson 31, why not try converting this one yourself?
Use '2A37' as the UUID, and for the heart rate BPM value, if value = characteristic:getValue() then use local data = ble.unpack("u16",value), and heartBPM = data/256 (NOT 128 as for the Vernier probe!).
platform.apilevel = '2.5'
screen = platform.window w = screen:width() h = screen:height()
pcall(function ()
require 'bleCentral' end)require "color" local bleState = '' local bleStatus = 'Stand by' local peripheralName = '' local myPeripheral = nil local characteristicsFound = 0
--RHYTHM+ Init Variables
-- Layout Functions (resize and paint)local POLARH7_HRM_MEASUREMENT_CHARACTERISTIC_UUID = '2A37' local heartBPM = nil local nameCheckList = {'RHYTHM'} local nameList = {'RHYTHM+'}
ble.addStateListener(listenerCallback) end) refreshMenu()function on.resize()
w = screen:width() or 841 h = screen:height() or 567
pcall(function()
screen:invalidate()
end
function on.paint(gc)
w = screen:width() or 841 h = screen:height() or 567
local fontSize = math.floor(h/30 + 0.5) fontSize = fontSize < 25 and fontSize or 24 fontSize = fontSize > 6 and fontSize or 7 if bleState:find("ON") then gc:setColorRGB(color.blue) else gc:setColorRGB(color.red) end gc:setFont("sansserif", "b", fontSize) gc:drawString(bleState, 0.1*w, 0.1*h)
gc:setColorRGB(color.green) gc:fillRect(0.9*w, 0.9*h, 0.1*w, 0.1*h) gc:setColorRGB(color.black) gc:drawRect(0.9*w, 0.9*h, 0.1*w, 0.1*h) local sw = gc:getStringWidth('Scan') gc:drawString('Scan', 0.95*w - sw/2, 0.95*h, 'middle')
gc:setColorRGB(color.red) gc:fillRect(0, 0.9*h, 0.1*w, 0.1*h) gc:setColorRGB(color.black) gc:drawRect(0, 0.9*h, 0.1*w, 0.1*h) local sw = gc:getStringWidth('Stop') gc:drawString('Stop', 0.05*w - sw/2, 0.95*h, 'middle')
for n = 1, #nameList do
local sw = gc:getStringWidth(nameList[n]) if peripheralName:find(nameCheckList[n]) then gc:setColorRGB(color.blue) else gc:setColorRGB(color.gray) end gc:drawString(nameList[n], w/(1+#nameList) + (n-1)*w/(1+#nameList) - sw/2, 0.95*h, 'middle')
end
gc:drawString(bleStatus, 0.1*w, 0.2*h)local fontSize = math.floor(h/20 + 0.5) fontSize = fontSize < 25 and fontSize or 24 fontSize = fontSize > 6 and fontSize or 7 gc:setFont("sansserif", "b", fontSize)
if heartBPM then
local msgH1 = string.format("%s %.1f bpm", "Heart Rate = ", heartBPM ) local sw = gc:getStringWidth(msgH1) gc:drawString(msgH1, 0.5*w - sw/2, 0.5*h)
end
end
--Menu, Keyboard and Mouse Functions--------------
function refreshMenu()
Menu={
}{"Controls",
},{"Scan and Connect", function() peripheralOn() end}, {"Disconnect", function() peripheralOff() end}, {"Reset", function() bleState = '' bleStatus = 'Stand by' on.resize() end},
toolpalette.register(Menu)
end
function on.enterKey()
peripheralOn()
end
function on.escapeKey()
resetall()
end
function resetall()
bleState = '' heartBPM = nil peripheralOff() on.resize()
end
function on.mouseUp(x, y)
w = screen:width() or 841 h = screen:height() or 567
if x > 0.9*w and y > 0.9*h then on.enterKey() end if x < 0.1*w and y > 0.9*h then on.escapeKey() end screen:invalidate()
end
-- BLE General Functions -----------
function listenerCallback(state, scriptError)
if state == ble.ON then bleState = 'BLE ON' elseif state == ble.OFF then bleState = 'BLE OFF' elseif state == ble.RESETTING then bleState = 'BLE RESET' elseif state == ble.UNSUPPORTED then bleState = 'UNSUPPORTED' if scriptError then print('Error message: BLE not supported') end end
screen:invalidate()
end
function peripheralOn()
bleCentral.startScanning(callbackScan) bleStatus = 'Scanning' screen:invalidate()
end
function peripheralOff()
bleCentral.stopScanning()
if myPeripheral then myPeripheral:disconnect() endbleStatus = 'Stand by' peripheralName = '' screen:invalidate()
end
function callbackScan(peripheral)
if peripheral ~= nil then peripheralName = peripheral:getName() or 'Unknown Device' for n =1, #nameCheckList do
if peripheralName and peripheralName:find(nameCheckList[n]) then
peripheral:connect(callbackConnect)
end
end
end
screen:invalidate()end
function callbackConnect(peripheral, event)
if event == bleCentral.CONNECTED then
bleCentral.stopScanning() bleStatus = 'Connected' myPeripheral = peripheral peripheralName = peripheralName:gsub('(unsupported)', '') peripheral:discoverServices(callbackServices)
elseif event == bleCentral.DISCONNECTED then bleStatus = 'Disconnected' peripheralName = ''
end screen:invalidate()
end
function callbackServices(peripheral)
if peripheral ~= nil and peripheral:getState() and peripheral:getState() == bleCentral.CONNECTED then
local services = peripheral:getServices() for _,service in ipairs(services) do service:discoverCharacteristics(callbackCharacteristics) end
end
screen:invalidate()
end
-- BLE Specific Functions ---------
function callbackCharacteristics(service)
local characteristicsList = service:getCharacteristics()
for _,characteristic in ipairs(characteristicsList) do
if characteristic:getUUID() == POLARH7_HRM_MEASUREMENT_CHARACTERISTIC_UUID then
characteristic:setValueUpdateListener(callbackCharacteristic) characteristic:setNotify(true) characteristicsFound = characteristicsFound + 1
end
end
end
function callbackCharacteristic(characteristic)
if characteristic:getUUID() == POLARH7_HRM_MEASUREMENT_CHARACTERISTIC_UUID then
-- RHYTHM+
local value = characteristic:getValue() local data = ble.unpack('u16', value) heartBPM = data/256
end
screen:invalidate()
end
platform.apilevel = '2.5'
screen = platform.window w = screen:width() h = screen:height()
pcall(function ()
require 'bleCentral' end)require "color" local bleState = '' local bleStatus = 'Stand by' local peripheralName = '' local myPeripheral = nil local characteristicsFound = 0
-- RHYTHM+ Init Variables
local POLARH7_HRM_MEASUREMENT_CHARACTERISTIC_UUID = '2A37' local heartBPM = nil local nameCheckList = {'RHYTHM'} local nameList = {'RHYTHM+'}
-- Layout Functions (resize and paint)ble.addStateListener(listenerCallback) end) refreshMenu()function on.resize()
w = screen:width() or 841 h = screen:height() or 567
pcall(function()
screen:invalidate()
end
function on.paint(gc)
w = screen:width() or 841 h = screen:height() or 567
local fontSize = math.floor(h/30 + 0.5) fontSize = fontSize < 25 and fontSize or 24 fontSize = fontSize > 6 and fontSize or 7 if bleState:find("ON") then gc:setColorRGB(color.blue) else gc:setColorRGB(color.red) end gc:setFont("sansserif", "b", fontSize) gc:drawString(bleState, 0.1*w, 0.1*h)
gc:setColorRGB(color.green) gc:fillRect(0.9*w, 0.9*h, 0.1*w, 0.1*h) gc:setColorRGB(color.black) gc:drawRect(0.9*w, 0.9*h, 0.1*w, 0.1*h) local sw = gc:getStringWidth('Scan') gc:drawString('Scan', 0.95*w - sw/2, 0.95*h, 'middle')
gc:setColorRGB(color.red) gc:fillRect(0, 0.9*h, 0.1*w, 0.1*h) gc:setColorRGB(color.black) gc:drawRect(0, 0.9*h, 0.1*w, 0.1*h) local sw = gc:getStringWidth('Stop') gc:drawString('Stop', 0.05*w - sw/2, 0.95*h, 'middle')
for n = 1, #nameList do
local sw = gc:getStringWidth(nameList[n]) if peripheralName:find(nameCheckList[n]) then gc:setColorRGB(color.blue) else gc:setColorRGB(color.gray) end gc:drawString(nameList[n], w/(1+#nameList) + (n-1)*w/(1+#nameList) - sw/2, 0.95*h, 'middle')
end
gc:drawString(bleStatus, 0.1*w, 0.2*h)local fontSize = math.floor(h/20 + 0.5) fontSize = fontSize < 25 and fontSize or 24 fontSize = fontSize > 6 and fontSize or 7 gc:setFont("sansserif", "b", fontSize)
if heartBPM then
local msgH1 = string.format("%s %.1f bpm", "Heart Rate = ", heartBPM ) local sw = gc:getStringWidth(msgH1) gc:drawString(msgH1, 0.5*w - sw/2, 0.5*h)
end
end
--Menu, Keyboard and Mouse Functions--------------
function refreshMenu()
Menu={
}{"Controls",
},{"Scan and Connect", function() peripheralOn() end}, {"Disconnect", function() peripheralOff() end}, {"Reset", function() bleState = '' bleStatus = 'Stand by' on.resize() end},
toolpalette.register(Menu)
end
function on.enterKey()
peripheralOn()
end
function on.escapeKey()
resetall()
end
function resetall()
bleState = '' heartBPM = nil peripheralOff() on.resize()
end
function on.mouseUp(x, y)
w = screen:width() or 841 h = screen:height() or 567
if x > 0.9*w and y > 0.9*h then on.enterKey() end if x < 0.1*w and y > 0.9*h then on.escapeKey() end screen:invalidate()
end
-- BLE General Functions -----------
function listenerCallback(state, scriptError)
if state == ble.ON then bleState = 'BLE ON' elseif state == ble.OFF then bleState = 'BLE OFF' elseif state == ble.RESETTING then bleState = 'BLE RESET' elseif state == ble.UNSUPPORTED then bleState = 'UNSUPPORTED' if scriptError then print('Error message: BLE not supported') end end
screen:invalidate()
end
function peripheralOn()
bleCentral.startScanning(callbackScan) bleStatus = 'Scanning' screen:invalidate()
end
function peripheralOff()
bleCentral.stopScanning()
if myPeripheral then myPeripheral:disconnect() endbleStatus = 'Stand by' peripheralName = '' screen:invalidate()
end
function callbackScan(peripheral)
if peripheral ~= nil then peripheralName = peripheral:getName() or 'Unknown Device' for n =1, #nameCheckList do
if peripheralName and peripheralName:find(nameCheckList[n]) then
peripheral:connect(callbackConnect)
else
peripheralName = peripheralName..' (unsupported)'
end
end
end
screen:invalidate()end
function callbackConnect(peripheral, event)
if event == bleCentral.CONNECTED then
bleCentral.stopScanning() bleStatus = 'Connected' myPeripheral = peripheral peripheralName = peripheralName:gsub('(unsupported)', '') peripheral:discoverServices(callbackServices)
elseif event == bleCentral.DISCONNECTED then bleStatus = 'Disconnected' peripheralName = ''
end screen:invalidate()
end
function callbackServices(peripheral)
if peripheral ~= nil and peripheral:getState() and peripheral:getState() == bleCentral.CONNECTED then
local services = peripheral:getServices() for _,service in ipairs(services) do service:discoverCharacteristics(callbackCharacteristics) end
end
screen:invalidate()
end
-- BLE Specific Functions ---------
characteristic:setValueUpdateListener(callbackCharacteristic) characteristic:setNotify(true) characteristicsFound = characteristicsFound + 1function callbackCharacteristics(service)
local characteristicsList = service:getCharacteristics()
for _,characteristic in ipairs(characteristicsList) do
if characteristic:getUUID() == POLARH7_HRM_MEASUREMENT_CHARACTERISTIC_UUID then
end
end
end
function callbackCharacteristic(characteristic)
if characteristic:getUUID() == POLARH7_HRM_MEASUREMENT_CHARACTERISTIC_UUID then
-- RHYTHM+
local value = characteristic:getValue() local data = ble.unpack('u16', value) heartBPM = data/256
end
screen:invalidate()
end
If you have a Heart Rate Monitor and Nspire iPad App, you may test this entire script by copying and pasting from this web page into the TI-Nspire Lua Script Editor.
Hopefully, by this stage, you are getting a feel for scripting for BLE using TI-Nspire and Lua. There are many opportunities for creative and interesting activities beginning with simple data collection based on physical personal data, suitable for mathematics classes from the middle years through to seniors. Of course, this is ideal STEM material, and will come to life in science and engineering classes, providing readily accessible real-world applications for content at all levels.
Home ← TI-Nspire Authoring ← TI-Nspire Scripting HQ ← Scripting Tutorial - Lesson 33