C/C++ coding tutorial - bitty data_logger

micro:bit code

Bitty Data Logger can capture and chart accelerometer, magnetometer and temperature data, streamed live from your micro:bit. The code we need to wrote for the micro:bit is actually very, very simple.

All we need the micro:bit to do is to provide a visual indication of whether or not it has a Bluetooth connection and to activate the Bluetooth accelerometer, magnetometer and temperature "services", which are software components responsible for making accelerometer, magnetometer and temperature data available over Bluetooth.

As with all our apps you can simply download and install a hex file which we've prepared for you or you can code it yourself, following the Bitty Software coding tutorial below. Naturally, we recommend you code it yourself!


before you start

To program your micro:bit using C/C++ you need the Yotta tools and the micro:bit 'DAL' source code on a PC or Mac. If you haven't yet set this environment up, read the information on our tools page and make sure you can build and install the Hello World sample code onto your micro:bit before going any further. If you've accomplished this then you have a working C/C++ environment for micro:bit development and can move on.

You should also install the bitty data logger application on your phone so you can use it for testing whilst you develop the micro:bit code. Check the bitty data_logger page for details of where to find the app for your type of phone or tablet.


step 1 - set up initial source code files

Download our project starter source code file to your microbit-samples\source folder. Delete any ".cpp" files you already had there. Rename the file to DataLogger.cpp so it has a name that reflects what it does.

During the course of the tutorial, you'll create a micro:bit hex file which does not require Bluetooth pairing. At the end of the tutorial, when everything is working, we'll do one final build that does need your phone or tablet to pair with your micro:bit so that nobody else can connect to your micro:bit.

Download this config.json file and save it in your microbit-samples folder. Rename it to config.json.

Your microbit-samples folder should now look like this:

And your microbit-samples source folder should look like this:

config.json includes various build properties that affects certain aspects of your micro:bit hex file once you have created it:

The property "open" which has a value of "1" means that your micro:bit will not need to be paired with your phone to be usable. This makes life easier during development but before you finish your project, remember to review this and if you decide you do want security then change the property to "0" and build again. You'll now need to pair your micro:bit with your phone.

Open DataLogger.cpp in a text editor.

The lines which start with "//" are comments which are there to help you understand the code. Review them now. Don't worry if the final comment block about fibers makes no sense to you at this stage. The key point here is that when programming a micro:bit, finishing with a call to the release_fiber() function is good practice.


step 2 - change the start-up message

To get things started let's just change the start-up message which is displayed on the micro:bit to something more fitting. Change it to "BDL" for Bitty Data Logger now and save your text file.

// Display a start-up message
uBit.display.scroll("BDL");

Your code should now look very similar to this:


step 3 - build and test

If you haven't already done so, find and launch the "Run Yotta" shortcut that should have been created for you when you installed Yotta. This will result in a window you can type commands into appearing. Use 'cd' to navigate to your microbit-samples folder.

Your micro:bit does not process C/C++ code directly. We have to translate it into a binary format which the micro:bit understands using a process called "building". Before we go any further, let's build and install your application now to make sure everything is working as expected. Follow the steps for building and installing your code and then return here and continue.

Tip: It's a good idea to progress by making small changes, building and testing at each step rather than typing huge amounts of code and then trying to build and test all in one go. It's much easier to solve problems when we're only making small changes each time.

Press the reset button on the back of your micro:bit. Your code will execute and you should see the text "DATA" scroll across the LED display.


step 4 - keeping track of the bluetooth connection state

bitty data logger uses Bluetooth for communication between the micro:bit and mobile app. Before any communication can take place, the mobile device must connect to the micro:bit. Our micro:bit code needs to keep track of whether or not something is connected by Bluetooth to it and and indicate this status to the user in a simple way using the LED.

Read all about keeping track of Bluetooth connections now and then come back here to resume the tutorial.

Change your code and include event handlers for responding to changes in the Bluetooth connection state as shown below.


void onConnected(MicroBitEvent)
{
    connected = 1;
    uBit.display.print("C");
}

void onDisconnected(MicroBitEvent)
{
    connected = 0;
    uBit.display.print("D");
}

In the main() function you should also have:

// Application code will go here and in functions outside of main()
uBit.messageBus.listen(MICROBIT_ID_BLE, MICROBIT_BLE_EVT_CONNECTED, onConnected);
uBit.messageBus.listen(MICROBIT_ID_BLE, MICROBIT_BLE_EVT_DISCONNECTED, onDisconnected);   

And at the top of your source code, add a variable declaration like this:

 uint8_t connected = 0;        

step 5 - activate the Bluetooth services

The BBC micro:bit is equipped with extensive Bluetooth capabilities. Think of any given feature of the micro:bit and there's a good chance you can use it over Bluetooth. This is made possible through the micro:bit having a custom Bluetooth "profile" designed for it. The profile includes a variety of "services", each of which is responsible for Bluetooth use of a given aspect of the micro:bit. If you'd like a better understanding of the terms and concepts relating to Bluetooth, read this article from the Bluetooth SIG's blog.

With one exception, the Bluetooth services we want to use need to be explicitly added to our code so that when the micro:bit starts up, those services are activated. We need the accelerometer, magnetometer, temperature and event services. Add the following lines of code to your main function:

    new MicroBitAccelerometerService(*uBit.ble, uBit.accelerometer);
    new MicroBitMagnetometerService(*uBit.ble, uBit.compass);
    new MicroBitTemperatureService(*uBit.ble, uBit.thermometer);

As you can see, one of our required services is not mentioned. What about the event service? Well, the event service has special status on the micro:bit, largely for historical reasons and so it can be included by setting a flag in a build configuration file called config.json. We'll deal with this later on.

Full details of the micro:bit's Bluetooth capabilities are documented at the Lancaster University web site.


step 6 - handle external sensors

We need to do some real coding now. The Bluetooth profile does include a service (the IO pin service) which will pretty much automatically, transfer analog values read from one or more pins, over Bluetooth and *concurrently* to a connected device. But. There's always a but. The way it works, you gain ease of use and concurrency but have to compromise on the quality of data, with those analog values encoded within an 8 bit field when for full quality and the highest supported resolution, 10 bits are needed.

So instead of using the IO pin service, Bitty Data Logger requires the event service to be used, with each value from each pin being transferred one at a time but at full resolution. Bitty Data Logger will send particular events to the micro:bit to set the pin sampling frequency in milliseconds and to enable and disable reading from pins 0, 1 and/or 2. Values will be read by our micro:bit code at the specified frequency and transmitted to Bitty Data Logger as the value part of another event type.

If you're not familiar with events as used in the micro:bit, see the FAQ


step 6a - declare variables

We'll need variables for the selected sampling frequency, to track the pins we should be reading from and to track whether or not we have a Bluetooth connection. Add the following near the top of your source code file.

    // only pins 0, 1 and 2 are supported
    uint8_t pin0_selected = 0;
    uint8_t pin1_selected = 0;
    uint8_t pin2_selected = 0;
    
    int sampling_frequency_ms = 1000;

step 6b - define event codes

Now let's define some constant values for use with the different types of event we'll be working with. Events have a 16 bit ID and an associated, 16 bit value. Note that when event codes and values are transmitted, each of the two fields is transmitted in little endian order which means that the last signficicant bytes get transmitted first. Check the example 4 byte values shown in these declarations to see what I mean by this.

//Event ID 9020, Event Value : bit mask bit n selects pin n where n <= 2    
// e.g. [0x3C, 0x23, 0x01, 0x00] selects pin 0
const int BDL_PIN_SELECTION_EVENT = 9020; 

//Event ID 9021, Event Value : millisecond sampling value
// e.g. [0x3D, 0x23, 0xF4, 0x01] (500ms)
const int BDL_SAMPLING_FREQUENCY_EVENT = 9021; 

//Event ID 9022, Event Value : anything
// e.g. [0x3E, 0x23, 0xF4, 0x01] (500ms)
const int BDL_START_SAMPLING_EVENT = 9022;     
                                             
// bits 0-9 contain value. Bits 15-14 contain a pin no.
const int MBI_SENSOR_VALUE_EVENT = 9030; //Event ID 9030, Event Value :     

The four different types of event we've just defined identifiers for, are to be used like this:

BDL_PIN_SELECTION_EVENT is sent by Bitty Data Logger over Bluetooth to the micro:bit when logging is started. The value part uses the least significant three bits (i.e. bits 0-2) to indicate which of the three pins, 0, 1 and 2 are to be read from. So a value of decimal 3, or binary 00000011 has bits 0 and 1 set, indicating that pins 0 and 1 must be sampled periodically and their values sent to Bitty Data Logger.

BDL_SAMPLING_FREQUENCY_EVENT is sent (written) by Bitty Data Logger over Bluetooth to the micro:bit when logging is started. The value part contains a binary value which is the frequency in milliseconds with which pins should be read (sampled). You'll see how we use this value in a loop later.

The BDL_START_SAMPLING_EVENT generated inside our micro:bit code as a signal which triggers the start of our pin sampling loop. All will become clear.

The MBI_SENSOR_VALUE_EVENT is sent over Bluetooth by the micro:bit to Bitty Data Logger and its value part contains both a pin number and the value which was just read from that pin. We use the most significant two bits, bits 15 and 14 to contain the pin number and the least significant 10 bits to contain the value. Bitty Data Logger will decode the event value to recover these two distinct pieces of information.


step 6c - register event handlers

We need to have our code listen for events from Bitty Data Logger, arriving over Bluetooth and pass control to a suitable function for processing each type of event. Add the following in the main function after the code which registers handlers for Bluetooth connect/disconnect events:

    uBit.messageBus.listen(BDL_SAMPLING_FREQUENCY_EVENT, 0, onSamplingFrequencyEvent);
    uBit.messageBus.listen(BDL_PIN_SELECTION_EVENT, 0, onPinSelectionEvent);
    uBit.messageBus.listen(BDL_START_SAMPLING_EVENT, 0, sampleLoop);


step 6d - update sampling frequency

Due to the code we just added, if an event arrives over Bluetooth with an event ID of BDL_SAMPLING_FREQUENCY_EVENT, a function called onSamplingFrequencyEvent will be called with an event object as a parameter. Let's create that function now.

    void onSamplingFrequencyEvent(MicroBitEvent e)
    {
        uBit.serial.printf("e.value: %d\r\n", e.value);
        // e.value = millisecond sampling value.
        sampling_frequency_ms = e.value;
        uBit.serial.printf("sampling_frequency_ms: %d\r\n", sampling_frequency_ms);
    }

MicroBitEvent is the micro:bit software type ("class") for events. See the micro:bit documentation for more information.

The code

uBit.serial.printf("e.value: %d\r\n", e.value);
is worth noting, because you'll see code like this several times in this tutorial. It writes the string "e.value: " followed by an integer value to the USB serial port of the micro:bit and that integer value is e.value (the value of the event) which is given as a second parameter to the printf function. All of which is a long way of saying it writes the event value to the USB serial port. Why? Because this allows you to watch what's happening inside the micro:bit by connecting a terminal emulation program like Putty to the serial port your micro:bit is connected to over USB on your computer.

The main point of this code is to take the event value and store it in the sampling_frequency variable. We'll use this variable later.


step 6e - update selected pins

The second type of event from Bitty Data Logger which we need to handle is the BDL_PIN_SELECTION_EVENT. The value of this event type will indicate which of pins 0, 1 or we should sample values from. If we have at least one pin selected then we should also initiate the sampling process in a loop.

Add this function:

    void onPinSelectionEvent(MicroBitEvent e)
    {
        // e.value = bit mask where bit n selects pin n and n <= 2
        uBit.serial.printf("e.value: %d\r\n", e.value);
        pin0_selected = e.value & 1;
        pin1_selected = e.value & 2;
        pin2_selected = e.value & 4;
        uBit.serial.printf("pin0_selected: %d\r\n", pin0_selected);
        uBit.serial.printf("pin1_selected: %d\r\n", pin1_selected);
        uBit.serial.printf("pin2_selected: %d\r\n", pin2_selected);
        if (pin0_selected != 0 || pin1_selected != 0 || pin2_selected != 0) {
             MicroBitEvent evt(BDL_START_SAMPLING_EVENT, 0);
        }
    }

Note the use of the "&" operator. What we're doing there is called a "bitwise AND". This is a logic operation which compares each bit in each operand. In our case our two operands are e.value and the constant value 1. e.value is a 16 bit number and so the constant 1 gets extended with enough leading zeroes so that it too has 16 bits. Then, each bit in the first operand gets compared with the corresponding bit in the same position in the other operand. If they are both 1 then the result is a binary 1. If either or both of them are zeroes, the result is a zero. This rule is often depicted by a truth table:

P Q P AND Q
0 0 0
0 1 0
1 0 0
1 1 1

Now think about what the result of this code would be if e.value contains a value of 3 which in binary is 00000000 00000011

        pin1_selected = e.value & 2;
        

Get it? The answer is of course 2 or binary 00000000 00000010.

So our pin selection variables will contain a zero if they are not selected and a non-zero value if they have been selected.

If any one or more of the three pins has been selected, then we now need to kick off some code which will periodically read values from those pins and send the values to Bitty Data Logger over Bluetooth. That's what the code MicroBitEvent evt(BDL_START_SAMPLING_EVENT, 0) will do for us. Simply creating a MicroBitEvent is all we need to do to have any code listening for that event type to receive and respond to it and earlier, we registered an event handler for this event type.


step 6f - sample pin values and send over Bluetooth

Add the following declarations somewhere near the top of your code:

            MicroBitPin P0(MICROBIT_ID_IO_P0, MICROBIT_PIN_P0, PIN_CAPABILITY_ANALOG);
            MicroBitPin P1(MICROBIT_ID_IO_P1, MICROBIT_PIN_P1, PIN_CAPABILITY_ANALOG);
            MicroBitPin P2(MICROBIT_ID_IO_P2, MICROBIT_PIN_P2, PIN_CAPABILITY_ANALOG);
        

We now have variable P0, P1 and P2 which represent the micro:bit pins. We'll use them in the next code we write.

When the BDL_START_SAMPLING_EVENT occurs, the sampleLoop function will be called. We haven't written this function yet so let's deal with it now:

                void sampleLoop(MicroBitEvent)
                {
                    uBit.serial.printf("entering sampleLoop\r\n");
                    int analog_value;
                    
                    while (connected == 1 && (pin0_selected != 0 || pin1_selected != 0 || pin2_selected != 0))
                    {
                        // analog_value is a value in the range of 0 - 1023
                        if (pin0_selected != 0)
                        {
                            analog_value = P0.getAnalogValue();
                            transmitSensorValue(0, analog_value);
                        }
                        if (pin1_selected != 0)
                        {
                            analog_value = P1.getAnalogValue();
                            transmitSensorValue(1, analog_value);
                        }
                        if (pin2_selected != 0)
                        {
                            analog_value = P2.getAnalogValue();
                            transmitSensorValue(2, analog_value);
                        }
                        uBit.sleep(sampling_frequency_ms);
                    }
                    uBit.serial.printf("exiting sampleLoop\r\n");
                }
        

This code is pretty simple. We start a while loop, which will execute as long as the micro:bit is connected to over Bluetooth and at least one of the three pins has been selected. We then read from each selected pin using the micro:bit getAnalogValue function and call a function we haven't yet written, transmitSensorValue to send the value over Bluetooth to Bitty Data Logger.

Add the transmitSensorValue function above sampleLoop plus another logging function which it will call:

            void logData(int pin,int analog_value) {
                int mv = round(analog_value * 3300.0 / 1024.0);
                // Centigrade temperature = [(analog voltage in mV) - 500] / 10
                int t = ((mv - 500) / 10);
                uBit.serial.printf("p%d: raw=%d mv=%d t=%d\r\n", pin,analog_value,mv,t);          
            }
            
            void transmitSensorValue(uint8_t pin, uint16_t analog_value)
            {
                uBit.serial.printf("pin=%d raw=%d\r\n",pin,analog_value);    
                // bits 0-9 contain value. Bits 15-14 contain a pin no.
                uint16_t pin_shifted = pin << 14;
                uBit.serial.printf("pin shifted=%d\r\n",pin_shifted);    
                
                uint16_t ev = pin_shifted | analog_value;
                
                uBit.serial.printf("ev=%d\r\n",ev);    
                logData(pin,analog_value);
                MicroBitEvent evt(MBI_SENSOR_VALUE_EVENT, ev);
            }
        

transmitSensorValue needs to place both the pin value and the sensor value just read from the pin, all in the same 16 bit event value variable. The pin number will be encoded using the two most significant bits of the event value, bits 15 and 14 (bit 0 is the least significant bit). The sensor value will occupy bits 0-9. " << " is another logic operation which in this case, shifts the pin value left by 14 bit positions, with zeroes coming in to fill the bits to the right. We then use a logical OR ("| ") to combine the sensor value and the shifted pin value into the same variable. Bitty Data Logger will reverse this process when it receives the event to extract the pin and sensor values seperately. There are lots of tutorials on bitwise logic on the internet if you'd like to know more about this technique.

With the code "MicroBitEvent evt(MBI_SENSOR_VALUE_EVENT, ev) " we create a micro:bit event which will be automatically sent over Bluetooth to Bitty Data Logger. Why does this happen? Because Bitty Data Logger has told the micro:bit it is interested in events of this type. Our code doesn't need to process this request though, it's taken care of automatically by the micro:bit firmware. All we need to do is create the event whenever we want to send a sensor value. And that's what we've done :-)


step 7 - include the Bluetooth event service and optionally, make Bluetooth pairing required

You have a choice as to whether or not you use Bluetooth pairing. If you pair your micro:bit with your phone then nobody else will be able to connect to your micro:bit. But not using pairing makes life a little simpler. The choice is yours. We'll assume you do want to use pairing.

We also still need to make sure that the Bluetooth event service is included in our code, since it is used when capturing data from sensors connected to pins, as explained above.

Edit the config.json file in your microbit-samples folder. Make sure the event_service property has a value of 1, the 'open' property is set to 0 and the 'pairing_mode' property is set to 1, as shown here:

            {
                "microbit-dal ": {
                    "bluetooth ": {
                        "enabled ": 1,
                        "pairing_mode ": 1,
                        "private_addressing ": 0,
                        "open ": 0,
                        "security_level ": "SECURITY_MODE_ENCRYPTION_NO_MITM ",
                        "whitelist ": 1,
                        "advertising_timeout ": 0,
                        "tx_power ": 7,
                        "dfu_service ": 1,
                        "event_service ": 1,
                        "device_info_service ": 1
                    },
                    "gatt_table_size ": "0x700 "
                }

step 8 - test

OK, that's all the code that is needed to make micro:bit data available to Bitty Data Logger over Bluetooth! If all has gone to plan, we should now have a completed micro:bit application for use with the bitty data logger application. Build and install your hex file onto your micro:bit in preparation for testing.

Immediately after installing your hex file, you will be prompted to "draw a circle " by a message which scrolls across the display. Hold your micro:bit so that the display is oriented in the vertical plane and then slowly rotate it through 360 degrees. You should see a single LED pixel lit and it should move to the bottom of the display with respect to gravity. This strange process involves collecting both data about magnetic fields in the local environment and motion and results in your micro:bit being properly calibrated for use in that environment.

The magnetometer may generate strange looking data if used in an environment other than that in which the calibration procedure was performed. Motion may also affect magnetometer data. For best results, always reinstall the hex file and recalibrate in any environment in which you want to capture magnetometer data.

Now test with the Bitty Data Logger application on your phone to make sure everything is working.

If Bitty Data Logger reports that you do not have one of the required Bluetooth services on your micro:bit, there are two possibilities. Either you really do not have this service activated, in which case there's an issue with your code or.... you do but your phone is looking at old, cached details associated with your micro:bit from some previous time the two have been used together. If you suspect the latter, try using the "Refresh Services " feature of the nRF Connect application which is recommended from the tools page. Make sure your micro:bit is not listed by your phone as a paired device as well. Right now it shouldn't be paired if you've been following the tutorial. If necessary, "forget " the device from the paired devices list. Switching Bluetooth off and on again may help. If after a reboot of your phone, Bitty Data Logger is still saying the accelerometer service is not on your phone then it probably isn't!

.

Run 'yt clean' and then 'yt build' to rebuild your application in full and install the hex file on your micro:bit.

you've finished!

That's it! You've finished and are all set to have fun with the bitty data logger application. You've also learned about Bluetooth connection trackingand how to activate the Bluetooth accelerometer service on your micro:bit. Congratulations!

if you get stuck

Check the steps above carefully and see if you can spot where things have gone wrong. If necessary, start again and proceed very slowly. building and testing at each step. Take a look at the solution and compare the code with yours.

If you did not install the correct config.json file in the microbit-samples directory or put it in the wrong place (e.g. in microbit-samples/source) then the app may not work because the pairing settings on the phone compared with the micro:bit may not match. Check the location and content of config.json. Try pairing your phone with your micro:bit. See our list of common issues for more information on this.

Make sure no other app on your phone is connected to your micro:bit. If you've been experimenting with other apps like nRF Connect, for example, this can easily happen. List the running apps on your phone and kill any that use Bluetooth, just to be sure. Kill and restart the bitty data logger app too.