microbit IoT-knoop#

We beschrijven hier het gebruik van een microbit als IoT-knoop. In eerste instantie gebruiken we alleen de ingebouwde sensoren en actuatoren. Verderop geven we aan hoe je dit kunt uitbreiden met extra sensoren en actuatoren.

IoT netwerk: radio-communicatie#

De communicatie in het microbit IoT-netwerk is gebaseerd op de microbit radio, zoals die beschikbaar is in Python. (Zie: https://microbit-micropython.readthedocs.io/en/v2-docs/radio.html en https://lancaster-university.github.io/microbit-docs/ubit/radio/)
Belangrijke eigenschappen van deze radio zijn:

  • pakketcommunicatie; maximale pakket-grootte: 251 bytes

  • broadcast: de microbit radio biedt geen adressering (*)

  • tekst-berichten hebben een 3-bytes header: [1, 0, 1]

  • de communicatie is best effort: je hebt geen garantie dat een bericht ontvangen wordt.

Voor de IoT-communicatie gebruiken we berichten met een 5-bytes header gevolgd door de sensor- en actuator-payload in Cayenne LPP (Low Power Payload) formaat. We maken onderscheid tussen berichten van een IoT-knoop naar de gateway (uplink), en omgekeerd (downlink). De header bevat het adres van de IoT-knoop; de gateway is impliciet het andere adres. (IoT-knopen kunnen elkaar niet rechtstreeks berichten sturen.)

De header bestaat uit de volgende onderdelen:

  • protocol-tag (1 byte, =0x0A voor uplink, 0x0B voor downlink)

  • node-ID (2 bytes, MSB eerst)

  • counter (2 bytes, MSB eerst)

Zoals gezegd, gebruikt het microbit radio-protocol geen adressering: elk bericht is een broadcast in het netwerk. De node-ID in de header vormt het adres: een microbit IoT-knoop moet van elk binnenkomend bericht controleren of dit (i) een downlink bericht is; en zo ja, (ii) of dat bericht voor deze knoop bestemd is.

Met het protocol-tag onderscheiden we de IoT-berichten van de microbit tekst-berichten: deze beginnen met de header [1, 0, 1], gevolgd door een string-waarde.

Tip

Als je meerdere microbit-netwerken in dezelfde omgeving wilt kunnen gebruiken, dan kun je voor elk netwerk een aparte group gebruiken, zie https://microbit-micropython.readthedocs.io/en/v2-docs/radio.html. De broadcast-berichten blijven altijd beperkt tot dezelfde group.

node-ID#

De (2-bytes) node-ID wordt gebruikt als adres voor de communicatie in het IoT-netwerk, maar ook als “adres” in de MQTT-communicatie voor de toepassing.

Deze node-ID wordt afgeleid uit de onderstaande 32-bits identificatie van de microbit. (Zie: https://support.microbit.org/support/solutions/articles/19000070728-how-to-find-the-micro-bit-serial-number)

def get_serial_number() -> int:
    NRF_FICR_BASE = 0x10000000
    DEVICEID_INDEX = 25 # deviceid[1]
    return machine.mem32[NRF_FICR_BASE + (DEVICEID_INDEX*4)] & 0xFFFFFFFF

IoT-node code#

Het coderen en decoderen van de payload is hier uitgeschreven: de ulpp-library gebruikt teveel geheugen voor de microbit V1.

from microbit import *
import radio
import machine
from utime import *
import micropython

# version without uLPP library
# LPP tags
LPP_dIn = 0
LPP_dOut = 1
LPP_aIn = 2
LPP_aOut = 3
LPP_luminosity = 101
LPP_presence = 102
LPP_temperature = 103
LPP_humidity = 104
LPP_barometer = 115

def get_serial_number() -> int:
    NRF_FICR_BASE = 0x10000000
    DEVICEID_INDEX = 25 # deviceid[1]
    return machine.mem32[NRF_FICR_BASE + (DEVICEID_INDEX*4)] & 0xFFFFFFFF

nodeID = get_serial_number() & 0xFFFF
counter = 0

uplink_tag = 0x0A
downlink_tag = 0x0B

led0 = 0
led1 = 0

def handle_led(channel: int, value: int):
    global led0, led1
    if channel == 0:
        led1 = value
        pin0.write_digital(value)
        if value == 0:
            display.show(Image("00000:00000:00100:00000:00000"))
        else:
            display.show(Image("99999:99999:99999:99999:99999"))
    elif channel == 1:
        led1 = value
        pin1.write_digital(led1)
        
def handle_actuators(in_buffer):
    
    in_pos = 0
    
    def get_byte() -> int:
        nonlocal in_pos
        byte = in_buffer[in_pos]
        in_pos += 1
        return byte

    def get_int() -> int:
        hi = get_byte()
        lo = get_byte()
        return hi * 256 + lo    

    in_pos = 0
    while in_pos < len(in_buffer):
        channel = get_byte()
        tag = get_byte()
        if (channel == 0 or channel == 1) and tag == LPP_dOut:
            handle_led(channel, get_byte())
        elif tag == LPP_dIn or tag == LPP_presence or tag == LPP_humidity:
            skip = get_byte()
        else:   # LPP_aIn, LPP_aOut, LPP_barometer, LPP_temperature, LPP_luminosity
            skip = get_int()        

def send_sensors(buttonA, buttonB):
    global counter
    
    out_buffer = bytearray(30)
    out_pos = 0
    
    def put_byte(value: int):
        nonlocal out_pos
        out_buffer[out_pos] = value
        out_pos += 1

    def put_int(value: int):
        while value < 0:
            value = value + 65536
        put_byte(value // 256)
        put_byte(value % 256)

    def put_sensor_byte(channel: int, tag: int, value: int):
        put_byte(channel)
        put_byte(tag)
        put_byte(value)
    
    def put_sensor_int(channel: int, tag: int, value: int):
        put_byte(channel)
        put_byte(tag)
        put_int(value)    
    
    out_pos = 0
    put_byte(uplink_tag) # header
    put_int(nodeID)
    put_int(counter)
    counter += 1
  
    put_sensor_byte(0, LPP_dOut, led0)
    put_sensor_byte(1, LPP_dOut, led1)
    put_sensor_byte(2, LPP_dIn, buttonA)
    put_sensor_byte(3, LPP_dIn, buttonB)
    put_sensor_int(4, LPP_temperature, temperature() * 10)
    put_sensor_int(8, LPP_aIn, display.read_light_level())

    print("send: " + str(list(out_buffer[:out_pos])))
    radio.send_bytes(out_buffer[:out_pos]) 
    
timer_period = 60000
timer_deadline = ticks_add(ticks_ms(), timer_period)

display.scroll(hex(nodeID))
print(hex(nodeID))

radio.on()
send_sensors(button_a.is_pressed(), button_b.is_pressed())
while True:
    if button_a.was_pressed():
        send_sensors(1, button_b.is_pressed())
        
    if button_b.was_pressed():
        send_sensors(button_a.is_pressed(), 1)
        
    if ticks_diff(timer_deadline, ticks_ms()) <= 0:
        send_sensors(button_a.is_pressed(), button_b.is_pressed())
        timer_deadline = ticks_add(timer_deadline, timer_period)
        
    rec_bytes = radio.receive_bytes()
    if rec_bytes != None:
        if rec_bytes[0] == downlink_tag:
            rec_nodeID = rec_bytes[1] * 256 + rec_bytes[2]
            if nodeID == rec_nodeID:  # msg for this node
                display.show('R')
                rec_counter = rec_bytes[3] * 256 + rec_bytes[4]
                handle_actuators(rec_bytes[5:])
                send_sensors(button_a.is_pressed(), button_a.is_pressed())

    sleep_ms(10)

Uitleg bij de code#

  • het indrukken van een button wordt gezien als een event: er wordt dan direct een sensor-bericht verstuurd.

    • voor het gemak worden daarin alle sensoren opgenomen. (Dat gebeurt ook bij de andere IoT-platformen in het lesmateriaal.)

  • elke timer_period worden de sensoren bemonsterd, en de waarden verstuurd.

    • deze periode is ingesteld op 6000 ms (1 minuut), vooral voor test-doeleinden. In een echte toepassing is die periode vaak groter. (Je hoeft meestal de temperatuur niet van minuut tot minuut te weten.)

  • bij een binnenkomend bericht wordt gecontroleerd of dit (i) een downlink bericht is, en (ii) of het bericht voor deze knoop bestemd is. In het laatste geval worden de data verwerkt. Ook worden de sensor-waarden dan weer opgestuurd.

Ook de waarden van de actuatoren zijn onderdeel van de uplink-berichten. Je kunt daarmee controleren of deze actuatoren de waarden hebben die je verwacht.

Pakket-grootte. De grootte van de radio-berichten is: 5 (header) + 4 * 3 (LPP 1-byte value) + 2 * 4 (LPP 2-byte values) bytes, in totaal 25 bytes. (Het default-maximum is 32 bytes, dat past dus ruim.)

Uitbreiden met extra sensoren en actuatoren#

  • gebruik de LPP-codering

  • gebruik voor barometer en humidity de gereserveerde kanalen

  • gebruik voor andere sensoren en actuatoren kanalen 9 en hoger.

  • controleer de maximale grootte van de radio-berichten; als deze groter wordt dan 32 bytes moet je dit aanpassen. (Bijvoorbeeld: radio.config(length=40), vóór radio.on().)

Extra sensoren#

Extra actuatoren#

ToDo#

  • toevoegen van print bij het ontvangen van een bericht.

  • bovenstaande teksten compleet maken.