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óórradio.on()
.)
Extra sensoren#
Extra actuatoren#
ToDo#
toevoegen van print bij het ontvangen van een bericht.
bovenstaande teksten compleet maken.