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
local POLARH7_HRM_MEASUREMENT_CHARACTERISTIC_UUID = '2A37'
local heartBPM = nil
local nameCheckList = {'RHYTHM'}
local nameList = {'RHYTHM+'}-- Layout Functions (resize and paint)
function on.resize()
w = screen:width() or 841
h = screen:height() or 567pcall(function() ble.addStateListener(listenerCallback) end)
refreshMenu()screen:invalidate()
end
function on.paint(gc)
w = screen:width() or 841
h = screen:height() or 567local 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 567if 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
endscreen: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 doif 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)
endend
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 + 1end
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)
function on.resize()
w = screen:width() or 841
h = screen:height() or 567pcall(function() ble.addStateListener(listenerCallback) end)
refreshMenu()screen:invalidate()
end
function on.paint(gc)
w = screen:width() or 841
h = screen:height() or 567local 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 567if 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
endscreen: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 doif 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)
endend
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 + 1end
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