Data Capturing with micro:bit, XinaBox and IoT

Data Capturing with micro:bit, XinaBox and IoT

Capture accelerometer data remotely on a BBC micro:bit, then use the XinaBox Wi-Fi gateway to transfer the data to an IoT platform.

Requirements

Introduction

Acceleration is what makes an object change its velocity: when a force of sufficient strength is applied to an object and makes it slow down, speed up or change direction acceleration has occurred. It is a fundamental characteristic of our experience of the world and a core concept in the mental tool box we use to make sense of that world.

Not only is it exhilarating to experience, it is also fascinating and educational to analyse. Being able to visualise the forces associated with dropping or throwing a ball, or an accelerating rocket, or seeing what happens when a moving object hits a wall, provide unique insights into this building block of creation.

The project will show you how to use a micro:bit and IoT to analyse acceleration over a time period. You can use the code and techniques described here to measure the acceleration that applies to any moving object that you can fix a micro:bit to.

Below is an example of a visualistion of acceleration: the chart shows real data from an object moving in a plane. I won't say much more about it right now - I encourage you to look at the acceleration profile and try to infer what is going on (e.g. is it a ball bouncing, or a head rolling, or something else?). The final section of this blog reveals the real world event that is represented below.

Real world data showing 99 accelerometer readings taken over a 5 second period

One of the great features of the BBC micro:bit is it's onboard accelerometer. Many a young learner has spent loads of productive time reading data from the accelerometer and using it in all sorts of creative ways. I've seen countless RC cars that are controlled by tilting, gloves that act as controllers, some very clever balancing robots and even gesture based musical 'instruments'.

But the accelerometer also has great potential as a learning tool to support the perennial speed / distance / time triangle that is so important, ostensibly in physics but in so many spheres of knowledge. What better way to teach acceleration than by getting young learners to witness an event, then review the forces behind that event?

In this blog I am going to show you how to:

  • Use a micro:bit to collect a pile of accelerometer readings. This micro:bit will collect the data 'remotely', which means it will collect the data during the event and store it on the micro:bit, to share later (I'll explain why in a bit).
  • Once the event we are measuring is complete, we'll then use the Wi-fi based XinaBox IoT starter kit for micro:bit (which I refer to here as our micro:bit gateway) to take that data and to transfer it to an IoT platform.

For this example we will throw a ball between 2 people then fling the data onto the Ubidots IoT platform.

Why collect raw accelerometer data remotely?

The micro:bit includes radio functionality, which can be used to enable a Bluetooth connection between a micro:bit and another device (NOT another micro:bit though).

In some circumstances it is possible to transmit the data as you collect it, using either radio or BT. This approach did not suit the circumstances that led to this blog though. When I was asked to look at the real world problem that generated the chart at the beginning there were some specific challenges to consider:

  • It was critical that the data did not get lost. I needed to store the data during the run, then extract it later. I could not afford for radio packets to go missing or for a BT connection to drop out. Saving the data during the run means we can retrieve it at our leisure afterwards, and if that data retrieval fails we can keep trying.
  • For reasons I will briefly explain later, during the acceleration event (which I will call a run henceforth) I wanted to use ALL of the processor power to take accelerometer readings. If I transmit the data during the run it reduces the amount of data we can sample, which impacts on the quality of the data.
  • I wanted to store a LOT of data so I needed to ensure minimal memory was being used by non-essential processes. The production-level data collection program (not included here) really stretches the little micro:bit to its limits!
  • The object that is moving in the run moves over quite a distance. It is risky to have a receiver in a fixed location. Think of it this way - the code here could be used to measure acceleration of a body being dropped from a height. The code could be adapted to read data for 10 seconds or more, and in that time a body will have fallen a long way from where you dropped it.

So, I've gone with the following workflow:

The four main steps in collecting and analysing data from our run

Taking the measurements

  • Flash the attached microPython code Catch_Collector_Simple or Catch_Collector_Advanced onto a micro:bit.
  • Connect it to a battery pack
  • Cut a hole in your ball that is big enough to fit the micro:bit and battery pack into.
  • Pack the loose space in the ball with a filler (I used bubble-wrap, which I hoard!)

Any resemblance to Pacman vomiting a micro:bit is accidental, perhaps profoundly so!

Once the ball is built do the following to activate it:

  • Click the reset button to clear out any old data, then click the A-button
  • A countdown from 3 will commence - at the end of the countdown a full stop is shown on the LEDs indicating that the micro:bit is taking measurements. Throw the ball during this period.
  • Once the measurement phase is complete a small square is shown on the LEDs. The sample code will record data for about 3 seconds, but you can tweak the code to extend or reduce this.
  • When you click the B-button it will initiate the process of transferring data to the micro:bit gateway.

The difference between the Simple and the Advanced versions lies in the data collection technique. For both, 1 reading is saved every 25ms (which amounts to 40 readings per second). For the Simple version we read the accelerometer just once every 25ms, but the Advanced code will read the accelerometer up to 75 times in this 25ms period. We then save a single value that is the average of all 75 readings. This 'smooths' out the data: it reduces outliers and gives a decent approximation of the acceleration across the whole 25ms period. The difference in data quality is marked and I encourage you to check this yourself: try a few times with both versions of the code and look at the data charts. You can read a lot more detail about this method, when to use it and why it is effective here on my blog.

If you look at the code you will notice that I am recording acceleration in the y-plane. For the real world scenario that I developed this code for that was adequate as the micro:bit was moving in that plane. When it comes to throwing the ball, though, the forces acting in the y-plane will be in part determined by the orientation of the micro:bit when you throw it. Consider adapting the code to record x and z readings and also ways that you can throw the ball in such a way that the acceleration acts in plane you are recording.

You may also notice that the code provided takes 120 readings. With 1 reading every 25ms this amounts to 3 seconds of data collection. You can play around with this. I found that it was possible to record up to 400 data points in this way. My production code used a file to store the data, rather than a list. Both ways have their pros and cons, which I won't elaborate on here (but please ask if you are interested).

The micro:bit gateway + IoT Platform

You will need to do the following:

  • Build and setup a micro:bit gateway.
  • Connect to an IoT platform

I'd recommend having a look at this blog, which contains all the info you need to get connected.

Before completing the experiment ensure your gateway is powered up and connected to Ubidots.

Our micro:bit gateway - a micro:bit attached to the XinaBox IoT starter kit

Note that the microPython code in the gateway for this project is almost identical to the code used in the other blogs I've written. Adapting it for each circumstance is relatively easy. When MakeCode blocks are released (soon!) this process will be greatly simplified.

Putting it all together

  • Set up and throw the ball, as described.
  • When measuring is complete, ensure your ball micro:bit is within range of the micro:bit gateway (a couple of meters should do).
  • Ensure your gateway is ready to receive data.
  • Click the B-button on the ball
  • Sit back and watch the process unfold. Its fun to watch the data accumulate in Ubidots.

My code takes about a minute to transmit all the data to the IoT platform. This can be reduced significantly.

The reason it takes so long is that I have been very cautious - I spent some time adjusting the various sleep commands, and when I found a mix that worked consistently I left it at that. I would love to see it working much more quickly, so if you crack this please get in touch and let me know how.

Analysing the chart from the beginning

Lets take another look at the chart from the beginning of the blog. This time I've put some lines in to mark the interesting 'phases':

As the object moves through its run, so it is subject to various factors

  • Phase 0: The object is at rest.
  • Phase 1: A rapid (you might even say, explosive) acceleration is applied. It peaks at the point marked, but the object speeds up throughout this period. At the end of this period the object has reached its maximum speed
  • Phase 2: At the beginning of this period the forces slowing down the object have exceeded those accelerating it, and a rapid deceleration occurs.
  • Phase 3: The data here is a bit 'noisy' - the object is decelerating for most of the period, so the object slows down in this phase. Occasional changes to the rate of deceleration are interesting.
  • Phase 4: A rapid deceleration occurs, followed by an equally rapid decrease in that deceleration, until it reaches zero. The object has encountered a barrier that is not a solid wall, but which brought it very quickly to a stop.
  • Phase 5: the object is once again at rest.

Have you guessed what it is yet?

It is a rocket powered car. In Phase 1 the rocket engine fires, then stops firing and we enter Phase 2. This phase sees forces of resistance work strongly against the car. In Phase 3 the car bounces a bit, perhaps sometimes leaving the ground and going briefly into freefall (which might explain the minor acceleration noted in that period, idk?). In Phase 4 it then hits a specially designed breaking system which slows it to dead stop in about 1.5 meters.

Code

Catch_Collector_Simple MicroPython
This code collects 120 accelerometer readings (using the 'Simple' method) and transmits them to our gateway

from microbit import *
import radio
radio.on()
radio.config(length=16, queue=64, channel=11, power=6)

accReadingList = []             #   This is a list - we will add each accelerometer reading we take to this list.
totalNumberOfReadings = 120     #   The code is limited to 150 data points - a few more is possible but there is an upper limit
numberOfReadings = 0            #   I don't use a loop to do the 150 readings, so I count how many we've done and stop at 150

readingFrequencyInMS = 25

startingTime = 0                #   We time how long it takes (although this is a factor of totalNumberOfReadings and our sampling period)
duration = 0

recordData = False             #   This is a switch we set to True when we are reading accelerometer data


def countDownValue(currentCount):       #   Just a little countdown to prepare the user for when the micro:bit begins measuring data.
    display.show(str(currentCount))
    sleep(1000)


def getAccelerometerReading(readingFrequencyInMS):
    sleep(readingFrequencyInMS)
    return accelerometer.get_y()


while True:
    if(button_a.was_pressed()):         #   Initiate the process of reading data
        countDownValue(3)
        countDownValue(2)
        countDownValue(1)
        display.show(".")
        recordData  = True
        startingTime = running_time()

    if(recordData):                                         #   True until we collect totalNumberOfReadings data points
        accReadingList.append(str(getAccelerometerReading(readingFrequencyInMS)))
        numberOfReadings += 1

    if((numberOfReadings >= totalNumberOfReadings) and recordData):    #   for 1 cycle after totalNumberOfReadings readings have been taken this switch is True
        recordData = False
        duration = running_time() - startingTime
        display.show(Image.DIAMOND)

    if(button_b.was_pressed()):
        display.show(Image.YES)
        radio.send("0_a" + str(duration))

        for i in range(0, totalNumberOfReadings - 1):
            if( (i >0) and (i % 50) == 0):
                display.show(Image.SQUARE_SMALL)
                sleep(7500)

            radio.send("0_c" + str(accReadingList[i]))
            display.show(Image.YES)
            sleep(100)

Catch_Collector_Advanced MicroPython
This code collects 120 accelerometer readings (using the 'Advanced' method) and transmits them to our gateway

from microbit import *
import radio
radio.on()
radio.config(length=16, queue=64, channel=11, power=6)

accReadingList = []             #   This is a list - we will add each accelerometer reading we take to this list.
totalNumberOfReadings = 120     #   The code is limited to 150 data points - a few more is possible but there is an upper limit
numberOfReadings = 0            #   I don't use a loop to do the 150 readings, so I count how many we've done and stop at 150

readingFrequencyInMS = 25

startingTime = 0                #   We time how long it takes (although this is a factor of totalNumberOfReadings and our sampling period)
duration = 0

recordData = False             #   This is a switch we set to True when we are reading accelerometer data


def countDownValue(currentCount):       #   Just a little countdown to prepare the user for when the micro:bit begins measuring data.
    display.show(str(currentCount))
    sleep(1000)


def getAccelerometerReading(readingFrequencyInMS):
    loopStart = running_time()  #   record the time now, so we can stop the loop below after 25ms
    accSmoothReading = 0        #   we'll use this to find the sum of all readings during our 25ms
    accSmoothReadings = 0       #   and we count how many readings we are able to make.

    while(  (loopStart + readingFrequencyInMS -1) > running_time() ):   #   we stay in this loop for readingFrequencyInMS ms
        accSmoothReading += accelerometer.get_y()                       #   we add the accelerometer readings during this period
        accSmoothReadings += 1                                          #   and we record the number of readings we are able to make.

    if(accSmoothReadings > 0):                                      #   Unlikely we need this, but it means no risk of dividing by zero below.
        return (accSmoothReading / accSmoothReadings)

    return 0


while True:
    if(button_a.was_pressed()):         #   Initiate the process of reading data
        countDownValue(3)
        countDownValue(2)
        countDownValue(1)
        display.show(".")
        recordData  = True
        startingTime = running_time()

    if(recordData):                                         #   True until we collect totalNumberOfReadings data points
        accReadingList.append(str(getAccelerometerReading(readingFrequencyInMS)))
        numberOfReadings += 1

    if((numberOfReadings >= totalNumberOfReadings) and recordData):    #   for 1 cycle after totalNumberOfReadings readings have been taken this switch is True
        recordData = False
        duration = running_time() - startingTime
        display.show(Image.DIAMOND)

    if(button_b.was_pressed()):
        display.show(Image.YES)
        radio.send("0_a" + str(duration))

        for i in range(0, totalNumberOfReadings - 1):
            if( (i >0) and (i % 50) == 0):
                display.show(Image.SQUARE_SMALL)
                sleep(7500)

            radio.send("0_c" + str(accReadingList[i]))
            display.show(Image.YES)
            sleep(100)

Catch_Gateway MicroPython
Code for the micro:bit gateway

from microbit import *
import radio
radio.on()
radio.config(length=16, queue=64, channel=11, power=6)
#   NOTE: radio settings are identical to those in the classroomMicrobit.py code.  Long queue is necessary as we process each slowly

uart.init(baudrate=9600, bits=8, parity=None, stop=1, tx=pin20, rx=pin19)
# NOTE: gets the uart (serial / USB) port on the micro:bit ready to communicate with the CW01


def sendMessageToCW01(parm, pauseLength):
    display.clear()
    uart.write(parm)
    sleep(pauseLength)
    data = uart.readline()
    while(data is None):
        data = uart.readline()

    if(len(str(data)) >0):
        uartMessageID = getIntegerFromString(data[:1])
        if(uartMessageID == 1):     display.show(Image.YES)     # NOTE: a tick is displayed when data is being transmitted correctly to the CW01
        else:                       display.show(Image.NO)      # NOTE: this means that a cross is displayed when an attempt to send data fails

    sleep(pauseLength)


def processRadioSignal(radioSignal):
    global testMode

    if(len(str(radioSignal)) < 4):   return False                       #   NOTE: valid radio signals are at least 3 or more characters long

    locationOfUnderscore = getLocationOfUnderscore(radioSignal)
    if(locationOfUnderscore == -1): return False                        #   NOTE: valid radio signals contain an underscore

    currentMicrobitID = getIntegerFromString(radioSignal[0:locationOfUnderscore])
    if(currentMicrobitID < 0):    return False                          #   NOTE: valid radio messages begin with an integer starting at 0.
    if(currentMicrobitID > 9): return False                             #   NOTE: IDs should go from 0 to 9

    #   NOTE: If we've reached this point of the code the radioSignal has passed all our validation checks.  It is 'safe' to process it.
    return sendValidMessageToCW01(radioSignal, locationOfUnderscore)

def sendValidMessageToCW01(radioSignal, locationOfUnderscore):
    messageType = str(radioSignal[locationOfUnderscore +1 : locationOfUnderscore +2])

    if(messageType == "a"):
        sendMessageToCW01("+4@YOURMICROBIT@duration@" + getValueFromRadioSignal(radioSignal, locationOfUnderscore) + "$", 250)
        return True

    if(messageType == "b"):
        sendMessageToCW01("+4@YOURMICROBIT@speed@" + getValueFromRadioSignal(radioSignal, locationOfUnderscore) + "$", 250)
        return True

    if(messageType == "c"):
        sendMessageToCW01("+4@YOURMICROBIT@AccReading@" + getValueFromRadioSignal(radioSignal, locationOfUnderscore) + "$", 250)
        return True

    return False


def getLocationOfUnderscore(radioSignal):
    #   NOTE: The underscore can only be in 1 of 2 places in the string, so KISS:
    radioSignalStr = str(radioSignal)
    if(radioSignalStr[1:2] == "_"):    return 1
    if(radioSignalStr[2:3] == "_"):    return 2
    return -1


def getIntegerFromString(uncheckedString):
    try: return int(uncheckedString)
    except ValueError: return -1


def getValueFromRadioSignal(radioSignal, locationOfUnderscore):
    #display.show(str(radioSignal[locationOfUnderscore +2 : len(radioSignal)]))
    try: return str(radioSignal[locationOfUnderscore +2 : len(radioSignal)])
    except: return "0"

#   INIT:

display.show(Image.SQUARE)          # NOTE: the square is shown on your micro:bit while it is connecting to Ubidots
sleep(2000)
uart.write("$")                     # NOTE: Cleans out the serial buffer
sleep(100)
uart.write("+9@?$")                 # NOTE: Reboots the CW01
sleep(5000)                         # NOTE: long delay is necessary - we need to give Wi-Fi time to sort itself out.
uart.write("$")                     # NOTE: Clean out Serial buffer, again.
sleep(500)


#sendMessageToCW01("+1@WIFINAME@WIFIPASSWORD$")
# EDIT GUIDELINES: you MUST enter the name and password of the Wi-Fi network you are trying to connect to in the line above.

#sendMessageToCW01("+2@DEFAULTTOKEN@?$")
# EDIT GUIDELINES: you MUST enter the DEFAULT TOKEN from Ubidots in the line above.

sendMessageToCW01("+3@things.ubidots.com@1883$", 750)
# NOTE: above line tells the micro;bit where to send the data to - its a Ubidots URL.


while True:
    if(processRadioSignal(radio.receive())):
        display.show(Image.HAPPY)
    else:
        display.show(Image.SAD)

    sleep(50)
Previous article Panic Button using XinaBox, micro:bit and Ubidots
Next article Weather Station with XinaBox and Ubidots