Inhoud
Lees je fitnesstracker uit via bluetooth
Voorwoord
Dit deel van de workshop probeert een hartslag signaal vanuit een bluetooth apparaat te lezen door middel van een standaard bluetooth Heart Rate Service (UUID=0x180D) en bijbehorende Heart Rate Measurement characteristic (UUID=0x2A37). Meer informatie hierover kun je vinden in:Assigned Numbers.
Jammer, maar helaas. Mijn slimme horloge heeft wel een hartslag meter aan boord, communiceert deze hartslag ook met de telefoon app, maar niet op een standaard manier . Omdat ik nog niet heb kunnen achterhalen, hoe deze communicatie werkt, probeer ik de werking van het programma uit te leggen, maar dus zonder dit te kunnen demonstreren met behulp van het horloge.
Mocht ik terzijnertijd nog achter komen, hoe ik mijn horloge wel kan uitlezen, zal ik dit hier zeker nog aanvullen.
Documentatie om te bestuderen:
- List of Bluetooth protocols (Wikipedia)
Bluetooth Low Energy
Een Bluetooth Low Energy verbinding1) is een handig draadloos protocol op korte afstand. Veel bluetooth apparaten comuniceren hun dat met een standaard protocol, beschreven in het documentAssigned Numbers.
Wanneer je connectie maakt met het bluetooth apparaat, kun je je “abboneren” op een service/characteristic, die dan nieuwe data doorgeeft, zodra die beschikbaar is. Wanneer de nieuwe data binnenkomt, gaan wij die laten zien op een schermpje, via een I2C verbinding.
Het scherm aansturen
Als eerste gaan we een klein OLED schermpje aansluiten via I2C, gebaseerd op de SSD1306. Het door mij gebruikte scherm heeft een formaat van 0,96“, met een resolutie van 128×64 pixels. Het schermpje heeft vier aansluitingen:
- VCC (+3,3V - +5V)
- Massa (-)
- SCL (Klok signaal)
- SDA (Data signaal)
Let op: Bij het gebruikte Liligo schermpje zit de VCC aansluiting op de éérste pin, de massa op de tweede pin. Standaard zie je dat deze aansluitingen omgekeerd zitten (dus massa op pin 1 en VCC op pin 2). Bij het aansluiten van I2C componenten moet je hier wel rekening mee houden.
Verder, alhoewel de voedingspanning tot 5V mag bedragen, zijn de stuursignalen (SCL/SDA) maximaal 3,3V! Hier dien je rekening mee te houden, als je het display bijvoorbeeld aanstuurd vanuit een Arduino, met digitale output signalen van 5V.
Om het schermpje onder MicroPython te laten werken, hebben we de SSD1306 driver nodig:ssd1306.py 2)
Bewaar deze file op je computer als ssd1306.py en kopiëer de file naar jouw ESP32 (MicroPython device).
Een I2C driver is standaard aanwezig in de MicroPython boot code, dus hoeft niet als (extra) driver op je ESP32 geïnstalleerd te worden.
Wanneer je nu onderstaande code naar de ESP32 kopiëert en uitvoerd, zal na korte tijd de tekst “Hello World” op het schermpje komen.
- i2c-display_test.py
from machine import Pin, I2C from ssd1306 import SSD1306_I2C i2c = I2C(0,scl=Pin(22), sda=Pin(21), freq=100000) display = SSD1306_I2C(128, 64, i2c) display.text("Hello world", 20, 20, 1) display.show()
De I2C class: I2C(id,scl,sda,freq)
Met deze class definiëren we de I2C interface.
- id: I2 C hardware ID (MakePico: 0/1).
- scl: SCL pin (klok).
- sda: SDA pin (data).
- freq: Ingestelde maximale klok frequentie (Hertz).
De display class: SSD_I2C(hor, ver, interface)
Hiermee leggen we het scherm vast, waarbij de I2C interface doorgegeven wordt via de interface variabele.
- hor: Aantal pixels horizontaal (kolommen).
- ver: Aantal pizels vertikaal (lijnen).
- interface: De gedefiniëerde I2C interface.
De BlueTooth verbinding
Een BlueTooth verbinding kun je vrij simpel opzetten met de MicroPythonaioble bibliotheek. Deze bibliotheek fungeert als een object-geörienteerde, asyncrone schil over deMicroPython's bluetooth API.
Op jouw MakePico, maak de folder aioble, ga naar de github aioble source folder en kopieer de volgende bestanden:
Opmerking:
Ik heb zelf nog wat meer bestanden gekopiëerd, om een en ander uit te proberen. Echter, voor deze workshop volstaan de bestanden uit de lijst hierboven.
Het programma gaat uit, dat het BlueTooth apparaat (bv. een slimme horloge, of een borstband, of …), de hartslag uitleest met behulp van een tweetal standaard BlueTooth UUID's:
- Heart Rate Service.
- Heart Rate Measurement characteristic.
Indien dit niet met behulp van deze UUID's werkt, zal er waarschijnlijk gebruik gemaakt worden van het Logical Link Control and Adaptation Layer Protocol (L2CAP). Helaas ontbreekt mij op dit moment nog het begrip van dit protocol .
Het uitlezen van de hartslag via BlueTooth
Initialisaties
In het programma, zullen we eerst een aantal initialisaties doen. We importeren de juiste bibliothekem, initialiseren het display en definiëren de twee UUID's die we gaan gebruiken.
import bluetooth # Standaard low-level micropython bibliotheek from machine import Pin, I2C # Pin en I2C class import uasyncio as asyncio # Asynchrone funkties import aioble # Bluetooth high-lever schil from ssd1306 import SSD1306_I2C # Display class HRS_UUID = bluetooth.UUID(0x180D) # Heart Rate Service HRM_UUID = bluetooth.UUID(0x2A37) # Heart Rate Measurement characteristic # Initialiseer het display voor I2C i2c = I2C(0,scl=Pin(22), sda=Pin(21), freq=100000) display = SSD1306_I2C(128, 64, i2c)
Nu definiëren we enkele funkties, die een specifieke taak uitvoeren:
- show_text
- find_heart_rate_sensor
- decode_heart_rate
En natuurlijk uiteindelijk het hoofdprogramma:
- main
De funktie show_text
De funktie show_text(text) laat een tekst zien, bovenaan het scherm. De funktie wordt gebruikt om voortgang van het programma te laten zien. De funktie wist eerst het scherm, waarna de gewenste tekst linksboven op het scherm wordt gedisplayed.
def show_text(text): display.fill(0) # Maak het scherm in het geheugen leeg display.text(text, 0, 0, 1) # Schrijf de tekst op positie (0,0) in het geheugen display.show() # Verplaats de tekst in het geheugen naar het scherm
De funktie find_heart_rate_sensor
De (asynchrone) funktie find_heart_rate_sensor() zoekt tussen beschikbare BlueTooth apparaten een apparaat, die de Heart Rate Service adverteert. Als de service niet geadverteerd wordt, kun je zoeken naar de naam van jouw apparaat (in mijn geval O_QIN XC PRO), maar die moet je bewust opgeven.
Indien het apparaat gevonden is, geeft de funktie de apparaat devinitie terug. Zoniet, dan wordt None teruggegeven.
Opmerking:
In het linux magazine staat als teruggegeven waarde: return result, echter wanneer ik die gebruik, loopt het programma in een foutmelding ('ScanResult' object has no attribute 'connect'). Bij gebruik van return result.device, wordt een correct device teruggegeven.
async def find_heart_rate_sensor(): show_text("Searching sensor...") # Scan voor BlueTooth apparaten async with aioble.scan(5000, interval_us=30000, window_us=30000, active=True) as scanner: async for result in scanner: # Als een apparaat de HRS_UUID adverteerd, of als # de apparaat naam "O_QIN XC PRO" voorkomt, kunnen # we stoppen met zoeken. if HRS_UUID in result.services() or result.name() == "O_QIN XC PRO": return result.device # Geef het gevonden apparaat terug return None # Er is geen juist apparaat gevonden
De funktie decode_hart_rate
De funktie decode_heart_rate(date) ontsleutelt de ontvangen bluetooth data en berekent daaruit de hartslag.
Hoe deze data er exact uitziet, kan ik helaas nog niet vertellen, maar het lijkt erop dat er een array met drie getallen is, waarvan de hartslag berekend wordt vanuit het tweede en derde getal.
def decode_heart_rate(data): # Vertaal data naar de hartslag waarde heart_rate = data[1] # Bereken de hartslag if data[0] & 1: # Als de hartslag boven de 100 is ??, heart_rate = heart_rate + (data[2] << 8) # dan tel het tweede getal erbij op. return heart_rate
Het hoofdprogramma main
Het hoofdprogramma main(), ook een asynchrone funktie, zorgt ervoor dat het BlueTooth apparaat gevonden wordt en op de service/karakteristiek geabonneerd wordt. Door het programma heen wordt de voortgang op het schermpje gepresenteerd (via show_text(tekst)).
Daarna wordt er een oneindige loop ingegaan, die op het moment dat de hartslag veranderd er data terugkomt van het BlueTooth apparaat en de hartslag op het schermpje getoond wordt.
async def main(): device = await find_heart_rate_sensor() # Zoek het BlueTooth apparaat if not device: show_text("Sensor not found") # Geen apparaat gevonden return # Einde programma try: # Probeer met het gevonden apparaat show_text("Connecting...") # te connecten connection = await device.connect(timeout_ms=2000) except asyncio.TimeoutError: show_text("Connection timeout") # Time-out return # Einde programma async with connection: try: # Je bent verbonden met het apparaat, show_text("Find service...") # probeer de Heart Rate Service te vinden hrs_service = await connection.service(HRS_UUID) if hrs_service == None: show_text("HRS not found") # De Heart Rate Service bestaat niet return # Einde programma show_text("Characteristic...") # Probeer de Heart Rate Measurement # characteristic te vinden hrm_characteristic = await hrs_service.characteristic(HRM_UUID) if hrm_characteristic == None: show_text("HRM not found") # De Heart Rate Measurement characteristic return # bestaat niet. Einde programma except asyncio.TimeoutError: show_text("Timeout") # Time-out return # Einde programma await hrm_characteristic.subscribe(notify=True) # Schrijf in op de hartslag karakterestiek, # alleen nieuwe waardes worden teruggegeven while True: # Wacht tot dat er een nieuwe waarde komt # en vertaal de data naar een hartslag heart_rate = decode_heart_rate(await hrm_characteristic.notified()) display.fill(0) # Laat de hartslag op het midden van het display.text(str(heart_rate), 56, 32, 1) # scherm zien display.show()
Het complete programma
Hieronder zie je het complete programma, wat zou moeten werken als de “Heart Rate Service” bestaat. Tot op dit moment, werkt voor mij het programma tot de regel hrs_service = await connection.service(HRS_UUID). Deze funktie geeft als resultaat None, waardoor de daarop volgende regel de mist ingaat met een foutmelding 'NoneType' object has no attribute 'characteristic' 3).
- Graag van jou de resultaten, wanneer het programma bij jou wel verder gaat!
- hartslag.py
import bluetooth from machine import Pin, I2C import uasyncio as asyncio import aioble from ssd1306 import SSD1306_I2C HRS_UUID = bluetooth.UUID(0x180D) HRM_UUID = bluetooth.UUID(0x2A37) i2c = I2C(0,scl=Pin(22), sda=Pin(21), freq=100000) display = SSD1306_I2C(128, 64, i2c) def decode_heart_rate(data): heart_rate = data[1] if data[0] & 1: heart_rate = heart_rate + (data[2] << 8) return heart_rate def show_text(text): display.fill(0) display.text(text, 0, 0, 1) display.show() async def find_heart_rate_sensor(): show_text("Searching sensor...") async with aioble.scan(5000, interval_us=30000, window_us=30000, active=True) as scanner: async for result in scanner: if HRS_UUID in result.services() or result.name() == "O_QIN XC PRO": return result.device return None async def main(): device = await find_heart_rate_sensor() if not device: show_text("Sensor not found") return try: show_text("Connecting...") connection = await device.connect(timeout_ms=2000) except asyncio.TimeoutError: show_text("Connection timeout") return async with connection: try: show_text("Find service...") hrs_service = await connection.service(HRS_UUID) if hrs_service == None: show_text("HRS not found") return show_text("Characteristic...") hrm_characteristic = await hrs_service.characteristic(HRM_UUID) if hrm_characteristic == None: show_text("HRM not found") return except asyncio.TimeoutError: show_text("Timeout") return await hrm_characteristic.subscribe(notify=True) while True: heart_rate = decode_heart_rate(await hrm_characteristic.notified()) display.fill(0) display.text(str(heart_rate), 56, 32, 1) display.show() asyncio.run(main())
