All posts by Dave Hartburn

ThingsBoard: Graphing and Multiple Devices

Graphing with ThingsBoard can be a little odd at first, especially when selecting which devices to graph. However it is also a very powerful tool. If you have multiple devices of the same type, you can produce a graph showing all devices of that type, When you add another such device, it will automatically add it to the graph.

In this example we have a number of devices configured, of type ‘RoomTempSensor’, which are sending live telemetry.

Go to dashboards and click + to add a new dashboard. Give it a name and description, then on pressing ‘ok’ you are presented with a blank dashboard. Hit the edit icon in the bottom right to start making changes.

The first thing we notice is the time period is set to ‘Realtime – last minute’. This is useful for an instant status dashboard, but not a historical graph. Click on this heading and in the pulldown either select a preset time period or select advanced to put in a custom one. For temperature graphing, I often find 2 days, 6 hours a reasonable time period.

Graphing single devices

Next we need to select which device or devices we want to graph. Click the Entity Aliases entry, next to the settings cog on the graph control bar at the top. The Alias Name can be any text you want. To graph a single device, select a filter type ‘Entity Name’, select a type of ‘Device’ then an appropriate ‘Name starts with’. Often the full name is the best option to ensure you just have the one device. Also make sure ‘Resolve as multiple entities’ is not selected. A slight annoyance of ThingsBoard is you don’t seem to be able to get a pulldown of device names at this stage or are able to verify that a source has been found for your graph.

Now try adding a graph. Click ‘Add New Widget’ in the middle if no widgets have been added, or select the + icon in the lower right. Select Charts and Timeseries Float. You are presented with a window and requested ‘Please Add DataSource’, click Add. Leave the type as Entity then select your new alias from Entity Alias. Finally select the entity timeseries, to select which telemetry elements you want to graph. Only at this point will you learn if your data sources are correct.

Each data source will be given a default colour, you can change these. See below for more advanced customisation.

Graphing multiple devices

Graphing multiple devices is fairly similar to graphing a single device. Follow the same steps as above to add an Entity alias, only this time select ‘Device type’ as the filter. Under the Device Type menu which appears, you should have a pulldown of your configured device types. Make sure you select ‘Resolve as multiple entities’, and optionally you can filter by device names based on what they start with. Click save when done.

As before, add a new widget and pick a Chart of Timeseries Float. As a datasource, add the Entity Alias you have just created, and add two data series to graph. In my example, I selected temperature and humidity. If it has correctly filtered and located your devices with telemetry, it will list all telemetry keys for all devices, grouping them by key name, e.g. temperature. Leave the settings at default for now, and click ‘Add’.

You can drag the graph to a different location or adjust the size, then click on the tick to save it. In my example, it found 3 devices of type RoomTempSensor, then successfully combined Temperature and Humidity on the same axis, as below. But it has listed the key for each data source, meaning temperature and humidity is listed three times each, with no way of knowing which device is which. That is not terribly helpful.

Customising graphs

There are a number of things wrong with the above graph we need to fix:

  • There is no identification of the data source
  • No units are marked next to the data
  • The legend shows the average data, we want average and the latest
  • Other cosmetic settings

Identify data sources and setting units

Edit the widget and you will be presented with a screen to select your data source. Click the edit icon next to temperature. Label should be the default key name. Change this to be “${entityName}-Temperature”. While we are on this screen, change the number of digits after the floating point to 2 and the special symbol to show next to the value to °c. While you can change the colour here, this only applies to the first matched data source if multiple data sources are in use. If needed, under advanced, it is possible to set minimum and maximum values on the axis scale.

Do the same for humidity.

This should now label each data series. Other labels can be used. ${aliasName} will give the name of the alias used, but this will be the same for all devices. If you have configured a label for a device, you can use ${entityLabel} . If no label is configured, this will default to entityName.

Changing the legend and cosmetic settings

Edit the graph again and instead of the data tab, go to settings. Set a graph title, and you can edit the CSS for this if you wish. Deselect ‘Drop shadow’. If you don’t want the title, you can disable it completely here. You can set a background colour for the graph, or make this transparent (alpha) to show the background of the entire dashboard.

At the bottom of the main settings tab, there is a blue icon which can control which fields are shown in the legend. I left average then turned on min and max.

A nice feature in Advanced, is the ability to overlay data from another time period (default one month ago) to show a comparison.

The final graph is:

Bug!

Note, the above screenshot was taken from ThingsBoard v3.1.0, which has a bug in the graph legend where symbols on the legend are mixed up. Temperature is now percent where humidity is now in centigrade. When editing a dashboard, the ThingsBoard version can be found in the lower right corner.

Raspberry Pi: IR Remote and Python

Small infrared remotes are a cheap way to add a level of remote control to a Raspberry Pi project. The one pictured below was under £5 (from Everything Pi) for the remote and IR receiver module. As the other code I was working on was in python, I also wanted a python program which could detect and respond to the remote.

The following is based on Raspian Buster. From Raspian Stretch, support for the traditional IR handler, LIRC was dropped in favour of ir-keytable. I found documentation on how to use this with python hard to come by, hopefully this article will help others finding the same.

Wiring it up

The wiring connections are fairly simple. The receiver module had two pins marked + and -, connect these to the 3.3v output and GND. The other pin is for data, I connected this to GPIO 18, pin 12.

The remote requires a CR2025 battery, I didn’t have one but a CR2032 works fine.

Once connected, point the remote and the sensor and if it detects a signal, a small LED on the module flashes. Another way to test if the remote is working is to look at the end of the remote with a camera, it should detect the flash.

ir-keytable configuration

To start using ir-keytable, a device has to be configured. As root, edit /boot/config.txt and uncomment the line below, ensuring you have the correct pin set, and reboot.

dtoverlay=gpio-ir,gpio_pin=18

Install the ir-keytable package:

sudo apt install ir-keytable

There are a number of well known remote definition files in /lib/udev/rc_keymaps, however knowing what to look for in an unbranded cheap remote like mine is not easy. Creating a custom keymap is probably the easiest way forward.

To test input run:

sudo ir-keytable -v -t -p all

This should report it has found an input device (possibly /dev/input/event0) and list currently supported protocols. Begin pressing data on your remote and you should see events fill the screen, a key down and key up for each press:

Testing events. Please, press CTRL-C to abort.
54703.160065: lirc protocol(nec): scancode = 0x40
54703.160094: event type EV_MSC(0x04): scancode = 0x40
54703.160094: event type EV_KEY(0x01) key_down:(0x0006)
54703.160094: event type EV_SYN(0x00).
54703.220082: lirc protocol(nec): scancode = 0x40 repeat
54703.220110: event type EV_MSC(0x04): scancode = 0x40
54703.220110: event type EV_SYN(0x00).
54703.340030: event type EV_KEY(0x01) key_up: (0x0006)
54703.340030: event type EV_SYN(0x00).

In this case I pressed the number 5 key, which I can see has the code 0x40. I can also see it is using the nec protocol. Look at one of the examples in /lib/dev/rc_keymaps for an idea of the keymap file. Open a blank text document, give your remote a name then using the ir-keymap test above, run through and press each button, assigning it to an event code, to build a keymap file like below.

I called my remote ultraThin as this is how it was advertised when sold. For a list of valid event codes, see the kernel header file at https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h

[[protocols]]
name = "UltraThin"
protocol = "nec"
varient = "nec32"

[protocols.scancodes]
0x45 = "KEY_1"
0x46 = "KEY_2"
0x47 = "KEY_3"
0x44 = "KEY_4"
0x40 = "KEY_5"
0x43 = "KEY_6"
0x07 = "KEY_7"
0x15 = "KEY_8"
0x09 = "KEY_9"
0x16 = "KEY_NUMERIC_STAR"
0x19 = "KEY_0"
0x0d = "KEY_NUMERIC_POUND"
0x18 = "KEY_UP"
0x08 = "KEY_LEFT"
0x1c = "KEY_OK"
0x5a = "KEY_RIGHT"
0x52 = "KEY_DOWN"

We can test the file by flushing any configuration, ensuring our configuration is fully listed and running the test again. Below flushes the configuration, loads our file followed by me pressing 7, * and the right arrow (sudo -s is considered bad practice but makes life easier):

astroberry@astropi:/tmp $ sudo -s
root@astropi:/tmp# ir-keytable -c
Old keytable cleared
root@astropi:/tmp# ir-keytable -w ultraThin.toml 
Wrote 17 keycode(s) to driver
Protocols changed to nec 
root@astropi:/tmp# ir-keytable -r
scancode 0x0007 = KEY_7 (0x08)
scancode 0x0008 = KEY_LEFT (0x69)
scancode 0x0009 = KEY_9 (0x0a)
scancode 0x000d = KEY_NUMERIC_POUND (0x20b)
scancode 0x0015 = KEY_8 (0x09)
scancode 0x0016 = KEY_NUMERIC_STAR (0x20a)
scancode 0x0018 = KEY_UP (0x67)
scancode 0x0019 = KEY_0 (0x0b)
scancode 0x001c = KEY_OK (0x160)
scancode 0x0040 = KEY_5 (0x06)
scancode 0x0043 = KEY_6 (0x07)
scancode 0x0044 = KEY_4 (0x05)
scancode 0x0045 = KEY_1 (0x02)
scancode 0x0046 = KEY_2 (0x03)
scancode 0x0047 = KEY_3 (0x04)
scancode 0x0052 = KEY_DOWN (0x6c)
scancode 0x005a = KEY_RIGHT (0x6a)
Enabled kernel protocols: lirc nec 
root@astropi:/tmp# ir-keytable -t
Testing events. Please, press CTRL-C to abort.
55313.370128: lirc protocol(nec): scancode = 0x7
55313.370152: event type EV_MSC(0x04): scancode = 0x07
55313.370152: event type EV_KEY(0x01) key_down: KEY_7(0x0008)
55313.370152: event type EV_SYN(0x00).
55313.440052: lirc protocol(nec): scancode = 0x7 repeat
55313.440076: event type EV_MSC(0x04): scancode = 0x07
55313.440076: event type EV_SYN(0x00).
55313.560035: event type EV_KEY(0x01) key_up: KEY_7(0x0008)
55313.560035: event type EV_SYN(0x00).
55314.510051: lirc protocol(nec): scancode = 0x16
55314.510076: event type EV_MSC(0x04): scancode = 0x16
55314.510076: event type EV_KEY(0x01) key_down: KEY_NUMERIC_STAR(0x020a)
55314.510076: event type EV_SYN(0x00).
55314.560050: lirc protocol(nec): scancode = 0x16 repeat
55314.560071: event type EV_MSC(0x04): scancode = 0x16
55314.560071: event type EV_SYN(0x00).
55314.680031: event type EV_KEY(0x01) key_up: KEY_NUMERIC_STAR(0x020a)
55314.680031: event type EV_SYN(0x00).
55316.900134: lirc protocol(nec): scancode = 0x5a
55316.900162: event type EV_MSC(0x04): scancode = 0x5a
55316.900162: event type EV_KEY(0x01) key_down: KEY_RIGHT(0x006a)
55316.900162: event type EV_SYN(0x00).
55316.970052: lirc protocol(nec): scancode = 0x5a repeat
55316.970077: event type EV_MSC(0x04): scancode = 0x5a
55316.970077: event type EV_SYN(0x00).
55317.060081: lirc protocol(nec): scancode = 0x5a repeat
55317.060106: event type EV_MSC(0x04): scancode = 0x5a
55317.060106: event type EV_SYN(0x00).
55317.180032: event type EV_KEY(0x01) key_up: KEY_RIGHT(0x006a)
55317.180032: event type EV_SYN(0x00).
^C
root@astropi:/tmp# exit
exit
astroberry@astropi:/tmp $ 

Now we know this works, we can make a permanent change.

cp ultraThin.toml /etc/rc_keymaps
vi /etc/rc_maps.cfg
  #driver table                    file
  *       *                        ultraThin.toml
  *       rc-adstech-dvb-t-pci     adstech_dvb_t_pci.toml
vi /etc/rc.local
# add the following line
/usr/bin/ir-keytable -a /etc/rc_maps.cfg -s rc0

On reboot, try ir-keytable -t again and you should see your key codes listed on a button press. This will now feed events into the standard linux event system, which means you can use event handling libraries from any language you wish to use. Test this by installing and running evtest. Remember we noted the input device earlier, now is the time to use it.

$ sudo apt install evtest
$ evtest /dev/input/event0
Input driver version is 1.0.1
Input device ID: bus 0x19 vendor 0x1 product 0x1 version 0x100
Input device name: "gpio_ir_recv"
Supported events:
  Event type 0 (EV_SYN)
  Event type 1 (EV_KEY)
    Event code 2 (KEY_1)
    Event code 3 (KEY_2)
    Event code 4 (KEY_3)
... output cut ...
Testing ... (interrupt to exit)
Event: time 1612701161.123493, type 4 (EV_MSC), code 4 (MSC_SCAN), value 19
Event: time 1612701161.123493, type 1 (EV_KEY), code 11 (KEY_0), value 1
Event: time 1612701161.123493, -------------- SYN_REPORT ------------
Event: time 1612701161.173486, type 4 (EV_MSC), code 4 (MSC_SCAN), value 19
Event: time 1612701161.173486, -------------- SYN_REPORT ------------
Event: time 1612701161.293459, type 1 (EV_KEY), code 11 (KEY_0), value 0
Event: time 1612701161.293459, -------------- SYN_REPORT ------------
Event: time 1612701162.103503, type 4 (EV_MSC), code 4 (MSC_SCAN), value 52
Event: time 1612701162.103503, type 1 (EV_KEY), code 108 (KEY_DOWN), value 1
Event: time 1612701162.103503, -------------- SYN_REPORT ------------
Event: time 1612701162.153497, type 4 (EV_MSC), code 4 (MSC_SCAN), value 52
Event: time 1612701162.153497, -------------- SYN_REPORT ------------
Event: time 1612701162.273457, type 1 (EV_KEY), code 108 (KEY_DOWN), value 0
Event: time 1612701162.273457, -------------- SYN_REPORT ------------

A key press of 0 and down has been successfully detected.

Using events for IR remotes in python

To read events, python needs the evdev package:

apt install python3-evdev

There is a tutorial about using the event system at https://python-evdev.readthedocs.io/en/latest/tutorial.html. First we can use list_devices() to locate all input devices on the system we can use. We loop through to find one called gpio_ir_recv, which it is the IR receiver.

After that we can use read_loop to read all events. In the code below there are two ways demonstrated of reading the events. Another consideration is writing a function to return data.keycode as a string of a key pressed, or a blank string if nothing is pressed.

# IR remote tester
# Assumes device already configured with ir-keytable

from evdev import *

devices = [InputDevice(path) for path in list_devices()]
# Define IR input
irin = None
for device in devices:
    #print(device.path, device.name, device.phys)
    if(device.name=="gpio_ir_recv"):
        irin = device

if(irin == None):
    print("Unable to find IR input device, exiting")
    exit(1)

print("IR input device found at", irin.path)

# Read events and return string
def readInputEvent(device):
    for event in device.read_loop():
        # Event returns sec, usec (combined with .), type, code, value
        # Type 01 or ecodes.EV_KEY is a keypress event
        # a value of  0 is key up, 1 is key down
        # the code is the value of the keypress
        # Full details at https://python-evdev.readthedocs.io/en/latest/apidoc.html

        # However we can use the categorize structure to simplify things
        # .keycode - Text respresentation of the key
        # .keystate - State of the key, may match .key_down or .key_up
        # See https://python-evdev.readthedocs.io/en/latest/apidoc.html#evdev.events.InputEvent
        if event.type == ecodes.EV_KEY:
            print("Event received")
            # Alert about keydown on the 8 key
            if(event.code == ecodes.KEY_8 and event.value==1):
                print("Detected keydown event on the 8")

            # Or use categorize. This is more useful if we want to write a function to
            # return a text representation of the button press on a key down
            #print(categorize(event))
            data = categorize(event)
            if(data.keycode=="KEY_5" and data.keystate==data.key_down):
                print("Detected keydown event on the 5")
            else:
                print(data.keycode, "up/down")
            print()

while True:
    readInputEvent(irin)

Micro:bit Serial Communications

By using the serial connections on a Micro:bit, it is possible to interact with it by sending messages in text, rather than just reading the input from a sensor. You can even use serial connections to allow the Micro:bit to talk to other Micro:bits, or different types of microcontrollers, such as an Arduino.

Basic serial output

You can write from the serial line to a computer connected by USB, by using the ‘print’ statement.

from microbit import *

print("Serial test starting")

c = 0
while True:
    if (button_a.is_pressed()):
        print("Button A pressed "+str(c))
        c = c + 1
        sleep(50)

The above will print to the serial line when button A is pressed. If you are using MU editor, flash the code, click REPL, hit the reset button on the back of the Micro:bit then press A a few times.

Micro:bit to micro:bit serial communication

To allow two Micro:bits to communicate over a serial connection, we must make use of the ‘uart’ library. For a serial connection, two wires are needed, rx is used to receive and tx is used to transmit. When communicating between two devices, rx must attach to tx on the other device, and vice versa. It is also good practice to connect the gnd pins to ensure a common potential difference.

If we use pins 0 and 1 for our communication. we can connect two Micro:bits with a common ground, and wire each pin 0 to pin 1 on the other device.

Serial connection between two Micro:bits

Set one Micro:bit and the sender with the following code:

from microbit import *

# Micro:bit serial to serial - sender

# Initalise the UART
uart.init(baudrate=9600, tx=pin0, rx=pin1)

while True:
    if(button_a.is_pressed()):
        uart.write("A")
        sleep(1000)
    if(button_b.is_pressed()):
        uart.write("B")
        sleep(1000)

Use below on the receiver:

from microbit import *

# Micro:bit serial to serial - sender

# Initalise the UART
uart.init(baudrate=9600, tx=pin0, rx=pin1)

while True:
    msg = uart.readline()
    if(msg is not None):
        display.scroll(msg)

When a button is pressed on the sender, a message is sent over the serial line to the receiver which displays it on the LED matrix. While the message is short, e.g. “A”, this could be something larger. A more advanced system could parse messages to pull out commands or data.

Arduino to Micro:bit communications

While the Micro:bit is a very versitile device, the low amount of memory, available IO pins or readily available hardware libraries, can cause it to struggle. One option is to use an arduino to drive a single piece of hardware and pass messages back to the Micro:bit using the serial line. An example is an RFID reader.

There are two options for serial communications to an arduino, hardware serial or software serial. On most types of arduino, pin D0 doubles as serial rx and D1 as serial tx. In our receiver code above on the Micro:bit, we have our pin0 configured as tx and pin1 as rx. Connecting the ground of an arduino to the ground on the Micro:bit, then arduino D0 and D1 through to Micro:bit pin0 and pin1.

The following sketch on the aduino will say ‘Hi’ then increase numbers (a sketch is arduino for a program). Though, due to line ending differences it will print ‘??’ on the scrolling display of the receiver. We will deal with that in the more advanced example below.

void setup()
{
  // Open serial communications and wait for port to open:
  Serial.begin(9600);
  while (!Serial) {
    ; // wait for serial port to connect. Needed for Native USB only
  }
  Serial.println("Hi");
  delay(2000);
}

void loop() // run over and over
{
  int i=0;
  char msg[5];
  while (true) {
      sprintf(msg, "%d", i);
      Serial.println(msg);
      delay(5000);
      i++;
  }
}

There is another problem. As we now have hardware connected to the serial line, we will have problems programming our arduino. We have to remove the connections to D0 and D1 each time we want to flash a new sketch. Another option is to use the software serial library and use other pins.

Connect the aruino to the Micro:bit, mapping D2 to pin0 and D3 to pin1 as shown.

The following code now makes use of the arduino software serial library and writes over pins D2 and D3. The code on the Micro:bit does not need to change.

/* Based on example at https://www.arduino.cc/en/Tutorial/LibraryExamples/SoftwareSerialExample */
#include <SoftwareSerial.h>

SoftwareSerial mySerial(2, 3); // RX, TX

void setup()
{
  // Open serial communications and wait for port to open:
  Serial.begin(9600);
  while (!Serial) {
    ; // wait for serial port to connect. Needed for Native USB only
  }
  Serial.println("Hi");

  // set the data rate for the SoftwareSerial port
  mySerial.begin(9600);
  mySerial.println("Lo");
}

void loop() // run over and over
{
  int i=0;
  char msg[5];
  while (true) {
      sprintf(msg, "%d", i);
      mySerial.println(msg);
      Serial.println(msg);
      delay(5000);
      i++;
  }
}

You could create more ‘mySerial’ objects to communicate to multiple Micro:bits or connect one to the serial. Using the software serial is useful while you are still doing code development on the arduino. You could then switch to hardware serial later.

Arduino serial to Micro:bit – advanced

Now lets look at doing something more useful. The Micro:bit LED display is not the best thing to read longer messages with, so lets connect a small OLED display. We need the arduino doing something useful. This example adds an Adafruit PN532 RFID/NFC shield to the arduino and sends the serial number of cards and tags swiped over software serial to a Micro:bit.

Arduino code:

/* rfid_to_serial - Dave Hartburn December 2020
 *  Dumps the UID of a card to the serial console. Designed to be the RDIF workhorse for
 *  another microcontroller, such as a micro:bit.
 *  
 *  Using Arduino UNO and Adafruit PN532 RFID/NFC Shield
 *  Based on the readMifare Demo program
 */

#include <Wire.h>
#include <SPI.h>
#include <Adafruit_PN532.h>
#include <SoftwareSerial.h>   // Use the software serial library

SoftwareSerial mySerial(2, 3);  // RX, TX

#define PN532_IRQ   (2)
#define PN532_RESET (3)  // Not connected by default on the NFC Shield
// Define the board
Adafruit_PN532 nfc(PN532_IRQ, PN532_RESET);

void setup() {
  Serial.begin(9600);
  mySerial.begin(9600);
  
  // Init RFID board
  nfc.begin();

  uint32_t versiondata = nfc.getFirmwareVersion();
  if (! versiondata) {
    Serial.print("Didn't find PN53x board");
    while (1); // halt
  }
  // Got ok data, print it out!
  Serial.print("Found chip PN5"); Serial.println((versiondata>>24) & 0xFF, HEX); 
  Serial.print("Firmware ver. "); Serial.print((versiondata>>16) & 0xFF, DEC); 
  Serial.print('.'); Serial.println((versiondata>>8) & 0xFF, DEC);
  
  // configure board to read RFID tags
  nfc.SAMConfig();
  
  //Serial.println("Waiting for an ISO14443A Card ...");  
}

void loop() {
  uint8_t success;
  uint8_t uid[] = { 0, 0, 0, 0, 0, 0, 0 };  // Buffer to store the returned UID
  uint8_t uidLength;                        // Length of the UID (4 or 7 bytes depending on ISO14443A card type)
  int i,len;
  unsigned long uidl;
  char msg[50];
  
  // Wait for an ISO14443A type cards (Mifare, etc.).  When one is found
  // 'uid' will be populated with the UID, and uidLength will indicate
  // if the uid is 4 bytes (Mifare Classic) or 7 bytes (Mifare Ultralight)
  success = nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, &uidLength);
  
  if (success) {
    // Display some basic information about the card
    //Serial.println("Found an ISO14443A card");
    //Serial.print("  UID Value: ");
    //nfc.PrintHex(uid, uidLength);

    // Convert array into single UID integer
    uidl=uid[0];
    for(i=1;i<uidLength;i++) {
      uidl = (uidl << 8) | uid[i];
    }
    //mySerial.print("CARD");
    sprintf(msg,"c=%lX",uidl);
    mySerial.print(msg);
    Serial.println(msg);

    // Wait 3 seconds
    delay(3000);
  }
}

Micro:bit code:

# Micro:bit card reader with arduino acting as
# RFID/NFC slave

from microbit import *
from ssd1306 import initialize, clear_oled
from ssd1306_text import add_text

# Init a SDD1306 screen
initialize()
clear_oled()

# Initalise the UART
uart.init(baudrate=9600, tx=pin0, rx=pin1)

while True:
    msg = uart.readline()
    if(msg is not None):
        clear_oled()
        add_text(0, 0, str(msg))

While this only reads the card serial number and displays it, you could do something more advanced such as a tick for known good cards and a cross for unknown cards.

Micro:bit OLED (SSD1306) Screen

Small OLED screens for microcontrollers are a cheap and easy way to display more detailed output for your Micro:bit than the scrolling LED matrix allows. Often sold as 0.96″ screens, these are just under 25mm wide and come in two formats. The larger square screen is 128×64 pixels or the slimmer model (pictured above) is half the height at 128×32 pixels. Some SPI connection versions are available, this post covers connections for I2C connections or the 4-pin version.

Connect up the screen as shown below. The two power pins, Vcc and GND connect to 3v and GND on the Micro:bit. On the Micro:bit, pins 19 and 20 are the I2C pins, connect SCL on the screen to pin 19 and SDA to pin 20. It is possible to connect multiple devices to I2C, while leaving the other IO pins free.

To use the OLED screen, we need to add additional libraries to MU editor. Libraries are additional software packages that add functions and hardware support, so you do not have to write everything yourself. Helpfully, ‘fizban99’ has produced a Micro:bit python library for OLED screens. Go to their Github page and using the green ‘Code’ button, select ‘Download ZIP’.

Open the ZIP file and copy all the python (.py) files into your mu_code folder (not a subdirectory). Use the following code to display some sample text:

from microbit import *
from ssd1306 import initialize, clear_oled
from ssd1306_text import add_text

initialize()
clear_oled()
add_text(0, 0, "Hello")
add_text(0, 2, "World")
add_text(2, 3, "Micro:bit")
display.show(Image.HAPPY)

This will still fail. Any libraries you need must also be copied to your Micro:bit. Hit the files button in MU and drag to your Micro:bit ‘ssd1306.py’ and ‘ssd1306_text.py’. Now you should have three lines of text displayed.

The libraries are a little limited in that you can’t control the font or font size, however there are a number of graphical functions in the library available. See the Github page for examples. If you are using a 128×32 screen, the same code will work, however you have to be careful not to use additional lines.

Where these screens are useful, is for displaying debugging information or detailed sensor output while developing.

ThingsBoard: Pulling Data From Multiple Devices

ThingsBoard allows you to pull the latest telemetry from a single device via the API, however if you want to pull that data into another system (e.g. a small display screen or to analyse results from multiple sensors), you have to make a different API call for each device, and also store the device ID for each in the place you are pulling the data from.

Wouldn’t it be useful if there was a way to pull the latest telemetry from multiple devices with one API call? If you write your client code in a generic way, you could dynamically add and remove devices without changing the code at all.

In an ideal world you could pull a JSON object similar to the following:

{
 "deviceOne" : {
         "field1" : 15,
         "field2" : 19
  },
 "deviceTwo" : {
         "field1" : 42,
         "field2" : 394
  },

This is possible in ThingsBoard using Assets to group devices.

Step 1: Set up two tests devices

Create two test devices in ThingsBoard and start posting telemetry from a script, as described at the start of ThingsBoard: Setting up mail alerts. Having a regular set of incoming data allows us to see instantly if this is working. Make two copies of the script, one for each test device, but also edit the script to send a second field (Temp), which we will just fix to one value:

curl -v -X POST -d "{\"$NAME\": $v, \"Temp\": 25}" http:/..........

Step 2: Create an asset

When we look at telemetry from the API, the documentation states we can use the API key:

/api/plugins/telemetry/{entityType}/{entityId}/values/timeseries

On the Entities And Relations page, the different possible entity types are listed. Two of those listed are Device and Asset. A single device is no good if we want multiple devices, so we need to combine the two test (or more when live) devices together to form an asset. There is a guide for doing similar at Data Function based on telemetry from 2 devices. This is not quite what we want, but we can adapt the principals for our needs.

In the Asset section, create a test asset, with the name and type ‘testAsset’. Under relations for this asset, select Outbound relations by picking From, relation type Contains and then select Device. You can now add the two test devices we have created.

Once saved, if you look a the latest telemetry, nothing appears. We need to populate this asset with telemetry by creating a rule chain.

Step 3: Create a rule chain

In Rule Chains, create a new chain called copyMultipleToAsset, set a meaningful description and turn on debugging. Before we do anything else with this, we want to start sending telemetry data to the new chain.

Open the Root rule chain and follow the chain Message Type Switch->Post telemetry->Save Timeseries. Add the new rule chain as a node and link it to Save Timeseries with the label Success. It does not matter if there is already a link on Success, adding another will duplicate the messages.

At this stage we have all devices sending telemetry. While we will filter these later to just the asset type, copying the data from a device to an asset can create a problem if they are both the same type of device. Both have the same keys, in the case of my test scripts, this is ‘soilPC’ and ‘Temp’. What data relates to what device? Will one field overwrite another as the latest? It would be nice if when we pull the ThingsBoard data we could get nested JSON with each device on the first level then each telemetry key underneath.

Create a script node with the following code:

var newMsg = {};
newMsg[metadata.deviceName]= {};

for(var key in msg) {
    newMsg[metadata.deviceName][key]=msg[key];
}

return {msg: newMsg, metadata: metadata, msgType: msgType};

Name it “addDevToKeys” and join this to Input. If you save the rule chain, you can check data is coming in and being transformed using the debugging events.

Next we need to only filter events for devices connected to our testAsset. Create a Check Relation filter called testAssetFilter. Set the direction to ‘From’, Type ‘Asset’, then select testAsset. Again select debug, and do the same for all nodes in this chain. Link this to the addDevToKeys script, with the label “Success”. We can add other Check Relation filters for other Assets later, by creating identical notes and also joining these with “Success”.

Add a Change Originator block, and call this ChangeToTestAsset. Originator Source: Related. Set the Relation filters to Contains, Entity Types: Asset. Join this to the Check Relation filter with the label “True”.

Finally create a Save Timeseries node, call it Save Timeseries and join it to the Change Originator block with “Success”. Save the chain.

The completed rule chain. This also shows the check relation filter added for our next asset.

Step 4: Verify

Go to Assets, pick the testAsset and inspect the latest telemetry. You should see data appear from both devices.

Following the information on my post ThingsBoard: Latest Telemetry From The API, you can check this can be obtained from the API with a command similar to:

$ curl -v -X GET http://s<SERVER>:<PORT>/api/plugins/telemetry/ASSET<ASSET IT>/values/timeseries --header "Accept:application/json" --header "X-Authorization: Bearer xxxx"

{
 "TestDevice":[{"ts":1605462644325,
      "value":"{\"TestDevice-soilPC\":40,\"TestDevice-Temp\":25}"}],
 "TestDevice2":[{"ts":1605461450186,
      "value":"{\"TestDevice2-soilPC\":49,\"TestDevice2-Temp\":25}"}]
}

We can now parse this and either access individual values using the key Device:Key or grab everything as a JSON block. Note the device types do not have to be the same.

Note that one flaw with this is, the data dump from each device is encoded in the value field as a string, this is not true JSON. You do need to pull out the value and then decode that, which can be a pain.

Alternate data model

An alternate is to keep the flat data structure, but for each device, change the key to be deviceName-key. This allows values to be graphed etc. To do this, change the telemetry transformation script to the script below. You could even combine them to have the best of both.

var newMsg = {};
newMsg[metadata.deviceName]= {};

for(var key in msg) {
    var newKey=metadata.deviceName+"-"+key;
    newMsg[newKey]=msg[key];
}

return {msg: newMsg, metadata: metadata, msgType: msgType};

ThingsBoard: Latest Telemetry From The API

To use ThingsBoard data in other applications, you can pull the latest telemetry from the API.

Details for the API and background about obtaining the JWT token can be found at ThingsBoard Rest API. The JWT token is an authentication token to secure data. You can obtain this from the command line using curl:

curl -X POST --header 'Content-Type: application/json' --header 'Accept: application/json' -d '{"username":"<TB User>", "password":"------"}' 'http://<SERVER>:<PORT>/api/auth/login'

A JSON object will be returned (much longer than that shown below):

{"token":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","refreshToken":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}

The first part, just the token is the section you need.

Details of obtaining telemetry can be found at Working With Telemetry. The key we need to access is:

/api/plugins/telemetry/{entityType}/{entityId}/values/timeseries

This can be pulled using curl. The header defines JSON as the expected type and also use the JWT token as the X-Authorization. Note, you must prefix the JWT token with the word “Bearer”.

curl -v -X GET http://<SERVER>:<PORT>/api/plugins/telemetry/DEVICE/<DEVICE ID>/values/timeseries --header "Accept:application/json" --header "X-Authorization: Bearer xxxxxxx JWT Token xxxxxx"

---- output cut -----
{"temperature":[{"ts":1604646983559,"value":"2.67"}],
"humidity":[{"ts":1604646983559,"value":"100.0"}],
"battery":[{"ts":1604646983559,"value":"3.31"}],
"busVoltage":[{"ts":1604646983559,"value":"4.08"}]}

From Python

import requests 

      URL = 'http://'+tbServer+'/api/plugins/telemetry/DEVICE/'+devId+'/values/timeseries'
        APIheader = {
            "Accept": "application/json",
            "X-Authorization": "Bearer "+tbJWTtoken
        }
        try:
            r = requests.get(URL, headers=APIheader)
            #print "Response = "+str(r.status_code)
            if(r.status_code==200):
                js=r.json()
                print json.dumps(js, indent=2)
 
            else:
                print "ERROR - "+str(r.status_code)+": "+r.content
        except:
            print "Error contacting ThingsBoard server"

Ensure you set the variables tbServer, devId and tbJWTtoken appropriately.

JWT Tokens Expiring

By default, the token will expire after 2.5 hours. This will get annoying if you embed the token in your applications. While you could make this auto-refresh by automating the above method, it requires hard coding your password in your code, which is more of a security risk than sticking with the same token. You can use the refresh token, but if you want to use something like a Particle Webhook to pull telemetry, that is not going to work. The refresh token also breaks down if the device/system is offline for a while and misses the refresh Window.

You can extend the token expiration time by editing /etc/thingsboard/conf/thingsboard.yml:

    #tokenExpirationTime: "${JWT_TOKEN_EXPIRATION_TIME:9000}" # Number of seconds (2.5 hours)
    tokenExpirationTime: "${JWT_TOKEN_EXPIRATION_TIME:15768000}" # Number of seconds (6 months)

Note, this is not considered good security practice. Only do this if you are not dealing with personal data or you do not care if your telemetry can be leaked into the wild.

ThingsBoard: Setting up mail alerts

As well as graphing, wouldn’t it be useful if ThingsBoard could send a mail alert when a particular parameter for a device goes above/blow a threshold, or when telemetry has not been received for a period of time?

Such things are possible, the following walks through the required configuration steps.

Set up a test device and client

It is easier if we can control the values being sent to ensure this works, so set up a test client and a script to populate it with data.

  • Login to ThingsBoard and go to Devices
  • Add new device
  • Call it TestDevice and give it a device type of GenericTest
    • This is where the ThingsBoard GUI can be frustrating. If you try to type GenericTest, it will attempt to match an existing type and keep overtyping what you are writing. It can be easier to type your new type in another window then cut and paste the whole string in one go.
  • Copy the access token then open the Latest Telemetry tab
  • Paste your server details and access token into the script below and run. You should see the Telemetry update.
#!/bin/bash
# ThingsBoard Telemetry Test

ACTOK='blahblahblah'
# Insert your server and port number
SRV='thingsboardserver.x.y:port'

BASE=45		# Base value
VARI=5		# +- variance around the base value
NAME='soilPC'	# Name of the key we are simulating
SL=5		# Number of seconds sleep between data

RANGE=$((VARI*2))	# Range of numbers

if [ "x$1" != "x" ] ; then                                                                                                  BASE=$1                                                                                           fi                                                                                                         

echo $BASE $VARI $SL $RANGE

while true; do
	# Get random number
	r=$((RANDOM % RANGE))
	# Add to base value - variance
	v=$((BASE-VARI+r))
	
	echo Sending $v
	curl -v -X POST -d "{\"$NAME\": $v}" http://${SRV}/api/v1/${ACTOK}/telemetry --header  "Content-Type:application/json"
	sleep $SL
done

The above script will send telemetry every 5 seconds. It will be +/- 5 around the base value 45. It gives slightly more interesting test data. A single parameter will override the base value to give a quick way of triggering alerts.

Configure mail sending

You must have an account with a mail provider to be able to send mail from

  • Login to ThingsBoard with the sysadmin account. This is different to your ‘everyday’ account where you can set up devices.
  • Go to Settings->Outgoing Mail
  • Enter details of your outgoing mail account and mail provider
  • Click ‘Send Test Mail’
  • If a test mail is received, click ‘Save’

Alert when a value is above/below a threshold

Generating alerts in ThingsBoard is performed using Rule Chains, these are powerful ways of acting on data, but can be difficult to understand at first. Before following this section, it is worth at least a skim read of ThingsBoard Rule Engine. I followed the Create and Clear Alarms tutorial, making changes to suit.

The following example alerts when the soil moisture (soilPC) on a particular device type drops below 30 percent. The rule chain looks like:

  • In Rule Chains, add a new rule chain with the name test device thresholds.
  • Open that chain, it will already have an input node.
  • Ensure the data from the telemetry message is available in the meta data for our use
    • Add a transformation filter
    • Name: Add soilPC to metadata
    • In the script, before the return line, add metadata.soilPC = msg.soilPC;
    • Join this to input
  • Filter on the threshold
    • Add a filter script
    • Name: Soil PC low
    • Filter: return msg.soilPC < 30
    • Join it to the transformation script with type ‘Success’
  • Add a Create Alarm action:
    • Name: Raise Soil Low Alarm
    • Alarm type: SoilLow
    • Propagate selected
    • Alarm severity: Critical
    • The function counts the number of times the alarm is triggered and adds to the metadata
    • When done, join to the filter script with type True
var details = {};
details.soilPC = msg.soilPC;

if (metadata.prevAlarmDetails) {
    var prevDetails = JSON.parse(metadata.prevAlarmDetails);
    details.count = prevDetails.count + 1;
} else {
    details.count = 1;
}
return details;
  • Create a ‘to email’ transformation
    • Name: Mail Soil Warning
    • Set the from and to address
    • Subject: Device ${deviceName} soil moisture low
    • Body: Device ${deviceName} reporting soil moisture low, currently at ${soilPC}%
    • Join to Create Alarm with type ‘Created’
  • Add the external type ‘Send Mail’
    • Name: SendMail
    • Use system SMTP settings
    • Join to email transformation with type ‘Success’.
  • Add a ‘Clear Alarm’ action
    • Name: Clear Soil Low Alarm
    • In the function, before the return, add details.clearedSoil = msg.soilPC;
    • Alarm Type: SoilLow
    • Add to the ‘soil PC low’ filter script with type ‘False’
  • Create a ‘to email’ transformation
    • Name: Cleared Soil Warning
    • Set the from and to address
    • Subject: Device ${deviceName} soil moisture alert cleared
    • Body: Device ${deviceName} now reporting soil moisture at ${soilPC}%, alarm cleared
    • Join to Clear Alarm with type ‘Cleared’
    • Link to the same SendMail function we created for the alarm creation

The new rule chain will not be triggered unless it is linked from the root rule chain. There are a number of ways to do this. I prefer to filter on device type, then if a number of devices share the same data keys (e.g. temperature), the wrong devices to not trigger the wrong rule chain.

  • Open the Root Rule Chain
  • Create a filter switch
    • Name: DeviceTypeSwitch
    • Function: return metadata.deviceType
    • This produces the output of each type of DeviceType used when receiving telemetry.
    • Link this to the existing ‘Save Timeseries’ with type ‘Success’
    • This will save data as normal, then look at processing it.
  • Add the new rule chain we just created.
  • Join it to the new filter switch with a label that matches the device type being used, e.g. SoilSensor
  • Save and await mails, using the test script to trigger alerts.
  • If the process is not working, ‘Debug Mode’ can be turned on on any device to view events. Remember to remove debugging when finished.

Alert on no telemetry received

Has your device stopped working for some reason? You need to know ASAP, so configure ThingsBoard to send a mail. Based on the Thingsboard Inactivity Tutorial. The default time out is 10 minutes. I changed this to 1 hour by editing the thingsboard.yml file, changing defaultInactivityTimeoutInSec and restarting.

The following rule chain will be created:

  • Create a test device and make sure you can send telemetry to it, as above.
  • Under the device, go to Attributes, select server attributes and click + to add
    • Create a attribute inactivityTimeout of type integer and enter the timeout in ms.
    • For testing, make this short.
  • Create a new rule chain, called ‘Inactivity Alerts’
  • Add a filter switch:
    • Name: DeviceTypeSwitch
    • Function: return metadata.deviceType;
    • Join to the input.
    • This will return the device type for any inactive device. We can either pick different actions for a each device type or just feed all the ones we want into the next stage.
  • We need the inactivity time available to the mail sending later in the chain. This data must be copied from the message to the metadata.
    • Create a transformation script
      • Name: Add Inactivity Time To Metadata
      • Transform:
var tdiff = msg.lastInactivityAlarmTime-msg.lastActivityTime;
var tmin = Math.round(Math.floor(tdiff/1000)/60, 1);
metadata.inactiveMin = tmin;
metadata.moo = 12;
return {msg: msg, metadata: metadata, msgType: msgType};
  • Join the script to the device switch, creating a label for any device type to monitor inactivity for.
  • Add a ‘message type switch’
    • Join to transformation script with type ‘Success’
  • Add a create alarm node:
    • Name: Inactivity Alarm
    • Alarm Type: Inactivity Alarm
    • Select Propagate
    • Join to SwitchMessageType with ‘Inactivity Event’
  • Create a ‘to email’ transformation:
    • Name: Mail inactivity
    • Set to and from addresses
    • Subject: Device ${deviceName} inactivity
    • Body: Device ${deviceName} has been inactive for ${inactiveMin} minutes
    • Join to ‘create alarm’ with type ‘Created’
  • Add send mail external node and join it to the email transformation with Success
  • Create a clear alarm node:
    • Name: Clear Inactivity
    • Alarm Type: Inactivity Alarm
    • Join to SwitchMessageType with ‘Activity Event’
  • Add a ‘to email’ transformation:
    • Name: Mail device recovery
    • Set to and from addresses
    • Subject: Device ${deviceName} recovery
    • Body: Device ${deviceName} has returned to service and posted telemetry
    • Join to ‘clear alarm’ with type Cleared
    • Join output to the send email node with type Success
  • In the Root Rule Chain, add the new rule chain as a node
    • Join it to the Message Type Switch with Activity Events and Inactivity Events

ThingsBoard Data Hacking

Thingsboard is a great IoT logging platform, however some management of data can be impossible to do from the GUI and can get annoying. The following database hacks provide a workaround to some of the common issues.

Connecting To The Database

  • SSH to your ThingsBoard server
  • If you don’t remember the password:
    • cd /etc/thingsboard/conf
    • grep SPRING_DATASOURCE thingsboard.yml
  • Connect with psql -U <username> -d <database> -h 127.0.0.1 -W
    • The default database name is ‘thingsboard’
  • Leave the postgres=# prompt with \q

Database Tables

Postgres does not support ‘show tables’. Use ‘\dt‘ to view tables:

thingsboard=# \dt
                List of relations
 Schema |         Name         | Type  |  Owner
--------+----------------------+-------+----------
 public | admin_settings       | table | postgres
 public | alarm                | table | postgres
 public | asset                | table | postgres
 public | attribute_kv         | table | postgres
 public | audit_log            | table | postgres
 public | component_descriptor | table | postgres
 public | customer             | table | postgres
 public | dashboard            | table | postgres
 public | device               | table | postgres
 public | device_credentials   | table | postgres
 public | entity_view          | table | postgres
 public | event                | table | postgres
 public | relation             | table | postgres
 public | rule_chain           | table | postgres
 public | rule_node            | table | postgres
 public | tb_user              | table | postgres
 public | tenant               | table | postgres
 public | ts_kv                | table | postgres
 public | ts_kv_latest         | table | postgres
 public | user_credentials     | table | postgres
 public | widget_type          | table | postgres
 public | widgets_bundle       | table | postgres
(22 rows)

Describe a table with ‘\d‘, e.g.:

thingsboard=# \d device
                           Table "public.device"
     Column      |          Type          | Collation | Nullable | Default
-----------------+------------------------+-----------+----------+---------
 id              | character varying(31)  |           | not null |
 additional_info | character varying      |           |          |
 customer_id     | character varying(31)  |           |          |
 type            | character varying(255) |           |          |
 name            | character varying(255) |           |          |
 search_text     | character varying(255) |           |          |
 tenant_id       | character varying(31)  |           |          |
Indexes:
    "device_pkey" PRIMARY KEY, btree (id)
  • device – Contains the device to ID mappings. This is a different ID than what can be found from the ThingsBoard device control panel in the GUI
  • ts_kv – Telemetry data. entity_id can be the device ID, ‘key’ is the field name.

Removing unwanted telemetry fields

ThingsBoard never forgets. If you have sent data to a device ID, it will remember this field for ever, and always show these fields as being available for graphing & reporting on the dashboards. Latest Telemetry from the GUI will show these fields have not been received for a long time.

The following session determines a device ID for a known device (SoilSensor1), then deletes the test fields key1 to key4, along with ‘values’ which was created in error during an integration configuration. For the GUI, the ts_kv_latest table also needs cleaning.

thingsboard=# select id,name from device where name='SoilSensor1';
               id                |    name
---------------------------------+-------------
 1ea5d8e1f450e60959cbf5261aa9fac | SoilSensor1
(1 row)

thingsboard=# select distinct key from ts_kv where entity_id='1ea5d8e1f450e60959cbf5261aa9fac';
      key
----------------
 soilPC
 soilMoisture
 devCharge
 chargeStatus
 power
 devPower
 key1
 key4
 key2
 battery
 busVoltage
 current
 values
 key3
 pressure
 temperature
 humidity
 onBatteryPower
(18 rows)

thingsboard=# delete from ts_kv where entity_id='1ea5d8e1f450e60959cbf5261aa9fac' and key like 'key%';
DELETE 4
thingsboard=# delete from ts_kv where entity_id='1ea5d8e1f450e60959cbf5261aa9fac' and key='values';
DELETE 20
thingsboard=# select distinct key from ts_kv where entity_id='1ea5d8e1f450e60959cbf5261aa9fac';
      key
----------------
 power
 devPower
 battery
 busVoltage
 current
 pressure
 temperature
 humidity
 onBatteryPower
 soilPC
 soilMoisture
 devCharge
 chargeStatus
(13 rows)

thingsboard=# delete from ts_kv_latest  where entity_id='1ea5d8e1f450e60959cbf5261aa9fac' and key like 'key%';
DELETE 4
thingsboard=# delete from ts_kv_latest  where entity_id='1ea5d8e1f450e60959cbf5261aa9fac' and key='values';
DELETE 1

Note, if you are viewing the Devices tab in ThingsBoard, then some details cache. Navigate to another section then return to verify the unwanted data has gone.

Renaming a field

If you send data to ThingsBoard with a mistake in the field name, when you correct it, ThingsBoard can not tie the two sets of data together. While this is perfectly reasonable, it can be quite annoying. The following example had a rogue apostrophe when sending humidity data. As a result we ended up with two datasets, humidity and humidity’. The data was joined together and verified via a graph on the dashboard:

thingsboard=# select id,name from device where name='SoilSensor2';
               id                |    name
---------------------------------+-------------
 1eab940ff2287c0a4259322b72a0dfe | SoilSensor2
(1 row)

thingsboard=# select distinct key from ts_kv where entity_id='1eab940ff2287c0a4259322b72a0dfe';
      key
----------------
 power
 devPower
 battery
 current
 busVoltage
 pressure
 temperature
 humidity
 onBatteryPower
 humidity'
 soilMoisture
 soilPC
 devCharge
 chargeStatus
(14 rows)

thingsboard=# update ts_kv set key='humidity' where key='humidity''' and entity_id='1eab940ff2287c0a4259322b72a0dfe';
UPDATE 54827

thingsboard=# delete from ts_kv_latest where key='humidity''' and entity_id='1eab940ff2287c0a4259322b72a0dfe';
DELETE 1

Note the single quote needs to be doubled to escape it, and because we already have a field called humidity in the latest telemetry, we just delete rather than rename.

When changing a graph, if you edit it, click on the field, you can just rename in plain text rather than delete the old and set up the new. This is useful if you have added attributes such as colour or a custom label, e.g. ‘Humidity (%)’.

Micro:bit Keypads

Keypads make a convenient way to add lots of buttons to a Micro:bit and they come in a range of sizes. The type above are known as membrane keypads. They have a sticky back, are lightly waterproof and can be wiped clean.

4 x 1 Keypad

A 4 x 1 keypad is named as it is 4 buttons in 1 row. It comes with a ribbon connector cable, with 5 pins. There is one pin for each key and one for ground. The wiring for these can always differ, so it is worth checking. Often ground is on a strip of cable on it’s own. The connector usually had a small number on each end denoting the pin number. On the keypad pictured above, the pinout, going from left to right is:

PinFunctionMicro:bit
1GroundGnd
2Key ‘2’pin0
3Key ‘1’pin1
4Key ‘4’pin2
5Key ‘3’pin8

You can see from the above the wiring does not go 1, 2, 3, 4 as you may expect. Sometimes these keypads can have different symbols or be blank.

Have a quick read up on Micro:bit buttons to see how a basic push button works on a Micro:bit. If we use a variable for each button and assign it to a pin we can set an internal pull up resistor then use read_digital() to get the state. The following code will show on the display which button is being pressed:

# 4x1 keypad

from microbit import *

# Define our wiring. The 5th connection goes to ground
key1 = pin1
key2 = pin0
key3 = pin8
key4 = pin2

# Set internal pullup resistor
key1.set_pull(key1.PULL_UP)
key2.set_pull(key2.PULL_UP)
key3.set_pull(key3.PULL_UP)
key4.set_pull(key4.PULL_UP)

while True:
    if ( key1.read_digital() == 0 ):
        display.show("1")
        sleep(500)
        display.clear()
    if ( key2.read_digital() == 0 ):
        display.show("2")
        sleep(500)
        display.clear()
    if ( key3.read_digital() == 0 ):
        display.show("3")
        sleep(500)
        display.clear()
    if ( key4.read_digital() == 0 ):
        display.show("4")
        sleep(500)
        display.clear()        

There is a lot of repeated code there and the program does not scale well if we were to use a different sized keypad. We can take advantage of arrays in Python to avoid writing the same code over and over again. Read up on arrays at w3schools. Rather than use a straightforward array of single values, we use a two dimensional array. Each value in the array keypad is a small array itself, with two elements, the pin number and the value. Lists start at 0, so we can access the value of the second pin with keypad[1][1].

# 4x1 keypad

from microbit import *

# Create our pinout as a list. Use pairs of values
# of [ pin, value ]
keypad = [ [pin1, "1"],
           [pin0, "2"],
           [pin8, "3"],
           [pin2, "4"] ]


# Set internal pullup resistor, looping through the list
for k in keypad:
    k[0].set_pull(k[0].PULL_UP)

while True:
    # Loop through list checking for keypress
    for k in keypad:
        if ( k[0].read_digital() == 0 ):
            display.show(k[1])
            sleep(500)
            display.clear()

4×4 Keypads

We learned above that smaller keypads have one pin for each button, plus an additional pin for ground. A 2 button keypad will have 3 pins, a 4 button keypad has 5 pins. A 4×4 keypad has 16 keys, so it should have 17 pins, right? No, it only has 8 pins, so what is going on here?

The image below shows a circuit diagram of a 4×4 keypad:

The wiring forms a matrix. If you trace the circuit through, you can see if the ‘5’ key is pressed, a connection is formed between pin 7 and pin 3. If we can detect these connections then we can work out what button is being pressed.

Rather than use 8 IO pins on the Micro:bit, we can use a PCF8574 module to easily plug in the keypad and use some binary or hex to quickly work out what buttons are being pressed. Read up on the PCF8574, binary and hex here. Connect the keypad to the PCF8574 board making sure pin 1 on the keypad goes to p0 on the board. P0 is labelled on the underside.

The PCF8574 has four connections, connect Vcc to 3v and GND to GND. The remaining two connections are for the I2C bus. SDA goes to pin 20 and SCL goes to pin 19. It does not matter if you already have a device plugged into these two pins on the Micro:bit. As I2C is a bus technology, it can support multiple devices, each with it’s own address. The three small switches on the PCF8574 are used to change the address of the board, leave then all as ‘Off’.

On the PCF8574, if a pin set to be high is connected to a pin set to be low, then the high pin will be dragged low. If we set all the pins to high, except one row (for example pin 5), we can detect a button press on that row by a pin 1 to 4 also going low. We can write one value to set the pins then read it back, if there are no key presses on that row the value will be the same. If the value is different then something is being pressed. Cycle through pins 5 to 8 in turn setting each to be low then inspect to see if something is pressed and return what.

We can follow the circuit diagram and make a table of what the two low values should be for each key press. As this forms an 8 bit binary string, we can also shorten each of these to give a unique hex value for each pin.

KeyP8P7P6P5P4P3P2P1Hex
1011101110x77
2011110110x7B
3011111010x7D
4101101110xB7
5101110110xBB
6101111010xBD
7110101110xD7
8110110110xDB
9110111010xDD
A011111100x7E
B101111100xBE
C110111100xDE
D111011100xEE
*111001110xE7
0111010110xEB
#111011010xED

Similar to an array in Python, we can also use a data structure called a dictionary to store these hex values. An array uses an index number 0, 1, 2… etc for each entry, where as a dictionary can use anything else, even text. Read up here. If we make a dictionary with each of the above hex values as keys and the key symbol as the value, e.g. keys[0xBB] = "5", we can quickly look up what key is pressed from the value returned from the PCF8574 board.

This gives us a process we can follow to detect key presses:

  • Create a variable with all the bits high except pin 5.
  • Send variable to the PCF8574 to set the pins.
  • Read data back from the PCF8574 and store in a variable.
  • If the value sent is different to the value received, a key on that row is being pressed
    • Lookup that value in the dictionary and return the key press
  • Skip to the next row and repeat if no press detected

This is done in the readKeypad function in the following code:

# 4x4 keypad
from microbit import *

PCF_ADDR = 0x20     # Use the base address of the PCF8574

# I2C send, function to simplify writing
def sendI2C(addr, value):
    # Convert sent value to a byte array, which is
    # required for I2C
    buf = bytearray(1)
    buf[0] = value
    # Send value
    i2c.write(addr, buf)
    # Short delay to send
    sleep(5)

def readI2C(addr):
    # Read in as a single byte and return
    v = i2c.read(addr, 1)[0]
    return v
    
def readKeypad():
    # Define array of keypress values
    # Curly brackets are a dictionary as we are not
    # using consecutive index values
    keys = {}
    keys[0x77] = "1"
    keys[0x7B] = "2"
    keys[0x7D] = "3"
    keys[0xB7] = "4"
    keys[0xBB] = "5"
    keys[0xBD] = "6"
    keys[0xD7] = "7"
    keys[0xDB] = "8"
    keys[0xDD] = "9"
    keys[0x7E] = "A"
    keys[0xBE] = "B"
    keys[0xDE] = "C"
    keys[0xEE] = "D"
    keys[0xE7] = "*"
    keys[0xEB] = "0"
    keys[0xED] = "#"
    
    # Set the default keypress to be an empty string
    # to mean nothing pressed
    kp = ""
    
    # Start with a zero on bit 5, all the rest 1
    setState = 0xEF
    
    for x in range(4):
        sendI2C(PCF_ADDR, setState)
        # Read value back
        r = readI2C(PCF_ADDR)
        
        # If the return state is different to the set state, a
        # key press has pulled one of the pins, 1-4 low
        if(setState != r):
            # print("setState=",hex(setState),", r=",hex(r))
            kp = keys[r]
            # Return key press
            return kp
            
        # Move the zero up to the next pin with a left shift.
        # We want to keep the value at 8 bits, so need to AND
        # with FF. Also, a zero will be moved in from the right.
        # OR with 0x01 to set this lower bit.
        # e.g. 1110 1111 << 1
        #   = 11101 1110 & 0xFF
        #   =  1101 1110 | 0x01
        #   =  1101 1111
        setState = ( ( setState << 1) & 0xFF) | 0x01
    
    # Return keypress value
    return kp

while True:
        # Read from keypad
        kp = readKeypad()
        # Clear if an empty string comes back
        if( kp == "" ):
            display.clear()
        else:
            # Otherwise show
            display.show(kp)
        sleep(200)
   

Using a dictionary this way also allows you to change the meaning of keys. For example if you were making a calculator you could change the dictionary to return ‘+’ instead of ‘A’. Remember you can use the bin() and hex() functions with print if you want to see what is going on for any stage of the above.

Micro:bit LCD Display Screen

16×2 LCD module with I2C adapter and external power supply

A LCD screen is a great way to give more feedback to a user, either for a text message or values back from a sensor. Known as a 1602 LCD, this common display gives two rows of 16 characters to work with, and can scroll text. However it comes with two issues for the Micro:bit. First is that it needs 5v input and the second is it uses a lot of pins.

The first problem can be overcome by using an external power supply. The Micro:bit can only supply 3.3v. If you try powering a LCD screen from this, it will light up but if you can see anything at all, it will be very faint. There are a number of ways to supply 5v, one of the easiest is using a ‘3.3v 5v breadboard power supply module’. One of these comes with the Elegoo 37 sensor kit, but searching for the above description will find a number of other suppliers. These are usually quite cheap.

Supply the board with anywhere between 6.5v and 12v. A 5v and 3.3v will be supplied by the power output pins.

The second issue was the amount of pins used by the LCD module. The LCD requires 16 pins. While some of these are for power, plugging directly into the Micro:bit will not leave many free pins for other hardware. The easiest solution is to buy a I2C LCD module, pictured above the screen in the title image. To buy one, search for “I2C 1602 LCD”. You will often find screens with these already fitted.

Wiring it up

If you screen or your module has female headers, you can plug the module directly into the back of the screen. In the picture above, both the screen and the I2C module had male headers. Plug these into breadboard making sure the left most pin on the LCD (often marked ‘1’) lines up with the pin on the left of the module when it has it’s four pins on the side pointing out to the left.

From the power supply module, connect a 5v pin to Vcc on the I2C module and connect Gnd to a ground strip on the breadboard. You must connect this to Gnd on the Micro:bit. If you wish, you can connect a 3.3v pin from the power supply board to the 3v pin on the Micro:bit to power it, or you can power it via a serial cable.

The LCD interface module uses I2C, which is a common protocol that can be used to reduce connecting various modules to two wires each. So long as the devices have different addresses (don’t worry about this for now), you can connect multiple devices to I2C. The Micro:bit has two I2C pins to support this, 19 and 20. Connect SDA on the LCD module to pin 20 and SCL to pin 19.

Displaying text in your code

At the time of writing, there does not appear to be a common I2C LCD library, however ‘shaoziyang‘ has produced on at github, which works quite nicely (Thank you!).

The following code displays a hello message (to our dog) and counts up the seconds the Micro:bit has been running. If this does not work first time, check your wiring but also try changing LCD_I2C_ADDR from 63 to 39. Some modules use a different address:

from microbit import *
import time

LCD_I2C_ADDR=63

class LCD1620():
    def __init__(self):
        self.buf = bytearray(1)
        self.BK = 0x08
        self.RS = 0x00
        self.E = 0x04
        self.setcmd(0x33)
        sleep(5)
        self.send(0x30)
        sleep(5)
        self.send(0x20)
        sleep(5)
        self.setcmd(0x28)
        self.setcmd(0x0C)
        self.setcmd(0x06)
        self.setcmd(0x01)
        self.version='1.0'

    def setReg(self, dat):
        self.buf[0] = dat
        i2c.write(LCD_I2C_ADDR, self.buf)
        sleep(1)

    def send(self, dat):
        d=dat&0xF0
        d|=self.BK
        d|=self.RS
        self.setReg(d)
        self.setReg(d|0x04)
        self.setReg(d)

    def setcmd(self, cmd):
        self.RS=0
        self.send(cmd)
        self.send(cmd<<4)

    def setdat(self, dat):
        self.RS=1
        self.send(dat)
        self.send(dat<<4)

    def clear(self):
        self.setcmd(1)

    def backlight(self, on):
        if on:
            self.BK=0x08
        else:
            self.BK=0
        self.setdat(0)

    def on(self):
        self.setcmd(0x0C)

    def off(self):
        self.setcmd(0x08)

    def char(self, ch, x=-1, y=0):
        if x>=0:
            a=0x80
            if y>0:
                a=0xC0
            a+=x
            self.setcmd(a)
        self.setdat(ch)

    def puts(self, s, x=0, y=0):
        if len(s)>0:
            self.char(ord(s[0]),x,y)
            for i in range(1, len(s)):
                self.char(ord(s[i]))

lcd = LCD1620()
lcd.puts("Hello Benji!")
while True:
    lcd.puts("Running=" + str(running_time()/1000), 0, 1)
    sleep(500)

Functions in the library

FunctionUsage
clear()Clears the display
backlight(0 or 1)Setting the backlight to 0 turns the backlight off, 1 turns it back on again.
off()Turns the LCD off, thought the backlight stays on
on()Turns the LCD on
char(c,x,y)
char(65,x,y)
char(ord(‘@’),x,y)
Prints a single character at coordinates x,y
Prints the ASCII value 65 (capital A) at coordinates x,y
Using ord, prints @ at coordinates x,y
puts(s,x,y)
puts(“Hello World”, 0,1)
Writes a text string at coordinates x,y
Writes “Hello World” at the start of the second line.