Gebruikershulpmiddelen

Site-hulpmiddelen


microcontroller:project:esp32_fitnesstracker

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 :-D. 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:

[ESP Controller] [sbczob.eu]

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.

[top] [ESP Controller] [sbczob.eu]

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:

  1. VCC (+3,3V - +5V)
  2. Massa (-)
  3. SCL (Klok signaal)
  4. 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.

[top] [ESP Controller] [sbczob.eu]

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 :-O.

[top] [ESP Controller] [sbczob.eu]

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

[top] [ESP Controller] [sbczob.eu]

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

[top] [ESP Controller] [sbczob.eu]

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

[top] [ESP Controller] [sbczob.eu]

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

[top] [ESP Controller] [sbczob.eu]

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()

[top] [ESP Controller] [sbczob.eu]

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())

[top] [ESP Controller] [sbczob.eu]

1)
BLE: Voor het gemak volstaan we verder met “Bluetooth”
3)
Na de await connection.service(HRS_UUID) en de await hrs_service.characteristic(HRM_UUID) aanroepen heb ik een tweetal “if” expressies toegevoegd die controleren op de None return waarde. Daardoor treden er geen foutmeldingen meer op als de HRS of HRM UUID's niet bestaan.
microcontroller/project/esp32_fitnesstracker.txt · Laatst gewijzigd: 2024/09/06 22:38 door sbczobbeheerder