In this session you'll be building five examples, introducing you to:
- Building IoT devices with Arm Mbed OS.
- Hooking up a thermistor to a development board.
- Connecting your device to The Things Network using LoRaWAN.
- Data visualization of temperature sensors.
In case you're stuck this document will help you get back on track. Please help your neighbours as well :-)
The Mbed OS documentation can be found here: Mbed OS docs.
If you use the search function on the website, make sure to tap the 'Documentation' tab:
- Create an Arm Mbed online account here.
- Then install the following software for your operating system below.
Windows
If you are on Windows, install:
- ST Link - serial driver for the board.
- Run
dpinst_amd64
on 64-bits Windows,dpinst_x86
on 32-bits Windows. - Afterwards, unplug your board and plug it back in.
- (Not sure if it configured correctly? Look in 'Device Manager > Ports (COM & LPT)', should list as STLink Virtual COM Port.
- Run
- Tera term - to see debug messages from the board.
- Node.js - to show visualizations.
Linux
If you're on Linux, install:
- screen - e.g. via
sudo apt install screen
- Node.js - to show visualizations.
MacOS
If you're on MacOS, install:
- Node.js - to show visualizations.
We're using the NUCLEO-L476RG development board, a Semtech SX1272 LoRa shield, a thermistor (to measure temperature) and a breadboard. Let's build a circuit.
Grab the following items:
- Development board.
- LoRa shield.
- Mini-USB cable.
- 3x jumper wires.
- Thermistor.
- Breadboard.
This is the LM35 pinout. Place it on a breadboard, and connect three wires to it. Make sure the orientation is correct!
This is the pinout of the LEFT side of the NUCLEO board (you should heva the USB connnection on the top). Connect:
- Thermistor VCC -> RED.
- Thermistor Analog Out -> YELLOW.
- Thermistor Ground -> BLACK.
If the thermistor gets hot, you did something wrong ;-)
There is an Mbed simulator which you can use to test things out quickly. Let's build some small examples:
- Go to the simulator.
- Load
Blinky
.
This blinks the LED every 500 ms. We can make it dependend on an input signal as well. For example, place the following code in the editor and click Compile.
#include "mbed.h"
DigitalOut led(LED1);
DigitalIn btn(BUTTON1);
int main() {
while (1) {
led = btn.read() ? 1 : 0;
printf("Blink! LED is now %d\r\n", led.read());
wait_ms(500);
}
}
This turns the LED on when you press the on-board button. However this is inefficient as the microcontroller needs to keep checking the state of the button. We can also an 'interrupt' to detect this instead.
#include "mbed.h"
DigitalOut led(LED1);
InterruptIn btn(BUTTON1);
void fall() {
led = !led;
}
int main() {
btn.fall(&fall);
}
Now the MCU can go to sleep automatically in between actions. A downside of this is that your fall
function now runs in an ISR, and some things are not safe in an ISR such as calling printf
(because it's guarded by Mutex'es). This is not an issue in the simulator, but it will be on the board. We can use an EventQueue to prevent this and automatically debounce the interrupt event:
#include "mbed.h"
#include "mbed_events.h"
EventQueue queue;
DigitalOut led(LED1);
InterruptIn btn(BUTTON1);
void fall() {
led = !led;
printf("LED is now %d\r\n", led.read());
}
int main() {
btn.fall(queue.event(&fall));
queue.dispatch_forever();
}
Let's use some components:
- Click Add component.
- Select 'Red LED', and pin
p5
- Click Add component. - Click Add component again.
- Select 'Analog Thermistor* and pin
p15
- Click Add component.
Now place the following code:
#include "mbed.h"
#include "mbed_events.h"
AnalogIn thermistor(p15);
DigitalOut led(p5);
EventQueue queue;
void check_temperature() {
uint16_t samples = 30;
float allReadings = 0.0f;
for (uint16_t ix = 0; ix < samples; ix++) {
allReadings += thermistor.read();
}
float tempC = allReadings / static_cast<float>(samples) / 5.0f * 1000.0f;
led = tempC > 20.0f ? 1 : 0;
printf("Temperature is %f\r\n", tempC);
}
int main() {
queue.call_every(1000, &check_temperature);
queue.dispatch_forever();
}
Click Run and observe what you see.
Now let's run it on an actual board.
- Go to https://os.mbed.com and sign up (or sign in).
- Go to the NUCLEO-L476RG platform page and click Add to your Mbed compiler.
- Import the example program into the Arm Mbed Compiler by clicking this link.
- Click Import.
- In the top right corner make sure you selected 'NUCLEO-L476RG'.
This has cloned blinky.
-
Open
main.cpp
and replace with:#include "mbed.h" DigitalOut led(LED1); int main() { while (1) { led = !led; printf("Blink! LED is now %d\r\n", led.read()); wait_ms(100); } }
-
Click Compile.
-
A binary (.bin) file downloads, use drag-and-drop to drag the file to the NODE_L476RG device (like a USB mass storage device).
Note: Here's a video.
-
When flashing is complete, hit the BLACK button (under the shield).
You should see the LED blink very fast.
Look at the examples you ran in the simulator, you can run them on the board too; just replace the code in main.cpp
. Also:
- Replace
p5
withLED1
(or hook up an LED to your board). - Replace
p15
withA2
(because we hooked this up to a different pin on the physical board).
If all is well, you should see something similar to:
Blink! LED is now 1
Blink! LED is now 0
Blink! LED is now 1
Blink! LED is now 0
Blink! LED is now 1
To see debug messages, install:
- Arm Mbed Windows serial driver - serial driver for the board.
- See above for more instructions.
- No need to install this if you're on Windows 10.
- Tera term - to see debug messages from the board.
When you open Tera Term, select Serial, and then select the Mbed COM port.
No need to install a driver. Open a terminal and run:
screen /dev/tty.usbm # now press TAB to autocomplete and then ENTER
To exit, press: CTRL+A
then CTRL+\
then press y
.
If it's not installed, install GNU screen (sudo apt-get install screen
). Then open a terminal and find out the handler for your device:
$ ls /dev/ttyACM*
/dev/ttyACM0
Then connect to the board using screen:
sudo screen /dev/ttyACM0 9600 # might not need sudo if set up lsusb rules properly
To exit, press CTRL+A
then type :quit
.
Mbed OS is an advanced Real-Time operating system that can spawn multiple threads and handle execution switching between them automatically. In addition it can automatically handle putting the MCU to sleep (or deep sleep). Let's look at how this works.
-
Create a new file in the project called
mbed_app.json
, and fill it with:{ "macros": [ "MBED_HEAP_STATS_ENABLED=1", "MBED_STACK_STATS_ENABLED=1", "MBED_CPU_STATS_ENABLED=1", "MBED_TICKLESS=1" ] }
This enables diagnostics information
-
In
main.cpp
, put;#include "mbed.h" #include "mbed_events.h" #include "mbed_stats.h" DigitalOut led(LED1); void print_stats() { // allocate enough room for every thread's stack statistics int cnt = osThreadGetCount(); mbed_stats_stack_t *stats = (mbed_stats_stack_t*) malloc(cnt * sizeof(mbed_stats_stack_t)); cnt = mbed_stats_stack_get_each(stats, cnt); for (int i = 0; i < cnt; i++) { printf("Thread: 0x%lX, Stack size: %lu / %lu\r\n", stats[i].thread_id, stats[i].max_size, stats[i].reserved_size); } free(stats); // Grab the heap statistics mbed_stats_heap_t heap_stats; mbed_stats_heap_get(&heap_stats); printf("Heap size: %lu / %lu bytes\r\n", heap_stats.current_size, heap_stats.reserved_size); mbed_stats_cpu_t cpu_stats; mbed_stats_cpu_get(&cpu_stats); printf("CPU: uptime=%lld, idle=%lld\r\n", cpu_stats.uptime, cpu_stats.idle_time); printf("Sleep: sleep=%lld, deepsleep=%lld\r\n", cpu_stats.sleep_time, cpu_stats.deep_sleep_time); printf("\r\n"); } int main() { while (1) { print_stats(); led = !led; Thread::wait(1000); } }
-
Flash this application on the board. What do you see?
You can run the stats tracking code in a separate thread. Replace the int main()
function with:
void new_thread_main() {
while (1) {
print_stats();
Thread::wait(2000);
}
}
int main() {
Thread new_thread;
new_thread.start(&new_thread_main);
while (1) {
led = !led;
Thread::wait(1000);
}
}
Flash this on the board and compare with the previous output. What do you see? Do you see the new thread?
Assignment: we use only a fraction of the stack of the new thread, but have allocated 4096 bytes for it. Make the stack size of the new thread smaller. Here's the documentation.
Look at the simulator code for interacting with the thermistor. Change the application so that it reads data from the real sensor.
Note that the thermistor is hooked up to A2
, not p15
.
Hook up an external LED. You'll need an LED, two jumper wires and a 100 Ohm resistor. Hook it up to a digital pin.
Now it's time to send this data to the internet over LoRaWAN.
- In the Online Compiler, click Import.
- Click Click here to import from URL.
- Enter
https://github.com/janjongboom/uni-vienna-firmware
. - Click Import.
We need to program some keys in the device. LoRaWAN uses an end-to-end encryption scheme that uses two session keys. The network server holds one key, and the application server holds the other. (In this tutorial, TTN fulfils both roles). These session keys are created when the device joins the network. For the initial authentication with the network, the application needs its device EUI, the EUI of the application it wants to join (referred to as the application EUI) and a preshared key (the application key).
Let's register this device in The Things Network and grab some keys!
-
Login with your account or click Create an account
The Console allows you to manage Applications and Gateways.
-
Click Applications
-
Click Add application
-
Enter a Application ID and Description, this can be anything
-
Be sure to select
ttn-handler-eu
in Handler registrationThe Things Network is a global and distributed network. Selecting the Handler that is closest to you and your gateways allows for higher response times.
-
Click Add application
LoRaWAN devices send binary data to minimize the payload size. This reduces channel utilization and power consumption. Applications, however, like to work with JSON objects or even XML. In order to decode binary payload to an object, The Things Network supports CayenneLPP and Payload Functions: JavaScript lambda functions to encode and decode binary payload and JSON objects. In this example, we use CayenneLPP.
-
Go to Payload Format and select CayenneLPP
-
In your application, go to Devices
-
Click register device
-
Enter a name.
-
Click the 'Generate' button next to 'Device EUI'.
You can leave the Application EUI to be generated automatically.
-
Click Register
Your device needs to be programmed with the Device EUI, Application EUI and App Key
-
Click the
< >
button of the Device EUI, Application EUI and App Key values to show the value as C-style array -
Click the Copy button on the right of the value to copy to clipboard
In the Online Compiler now open main.cpp
, and paste the Device EUI, Application EUI and Application Key in.
Note: Do not forget the ;
after pasting.
Simulator: The simulator can test the application for you. Copy the whole main.cpp
and paste into the simulator. Then click Run
.
Now click Compile and flash the application to your board again. The board should now connect to The Things Network. Inspect the Data tab in the TTN console to see the device connecting. You should first see a 'join request', then a 'join accept', and then data flowing in.
Right now we relay random numbers back to the device. Change the code so that it sends the temperature from the thermistor.
Note: Hook up an external LED to pin D7. The built-in LED is hooked up to a pin that the LoRa shield also uses!
We only send messages to the network. But you can also relay data back to the device. Note that LoRaWAN devices can only receive messages when a RX window is open. This RX window opens right after a transmission, so you can only relay data back to the device right after sending.
To send some data to the device:
- Open the device page in the TTN console.
- Under 'Downlink', enter some data under 'Payload', select port 15, and click Send.
- Inspect the logs on the device to see the device receive the message.
Change the code so that you can control the LED on the board over LoRaWAN.
To get some data out of The Things Network you can use their API. Today we'll use the node.js API, but there are many more.
First, you need the application ID, and the application key.
-
Open the TTN console and go to your application.
-
Your application ID is noted on the top, write it down.
-
Your application Key is at the bottom of the page. Click the 'show' icon to make it visible and note it down.
With these keys we can write a Node.js application that can retrieve data from TTN.
-
Open a terminal or command prompt.
-
Create a new folder:
$ mkdir viennna-ttn-api $ cd viennna-ttn-api
-
In this folder run:
$ npm install ttn blessed blessed-contrib
-
Create a new file
server.js
in this folder, and add the following content (replace YOUR_APP_ID and YOUR_ACCESS_KEY with the respective values from the TTN console):let TTN_APP_ID = 'YOUR_APP_ID'; let TTN_ACCESS_KEY = 'YOUR_ACCESS_KEY'; const ttn = require('ttn'); TTN_APP_ID = process.env['TTN_APP_ID'] || TTN_APP_ID; TTN_ACCESS_KEY = process.env['TTN_ACCESS_KEY'] || TTN_ACCESS_KEY; ttn.data(TTN_APP_ID, TTN_ACCESS_KEY).then(client => { client.on('uplink', (devId, payload) => { console.log('retrieved uplink message', devId, payload); }); console.log('Connected to The Things Network data channel'); });
-
Now run:
$ node server.js
The application authenticates with the The Things Network and receives any message from your device.
Showing simple graphs
We can also graph the values directly to the console. Replace server.js
with:
let TTN_APP_ID = 'YOUR_APP_ID';
let TTN_ACCESS_KEY = 'YOUR_ACCESS_KEY';
const ttn = require('ttn');
const blessed = require('blessed');
const contrib = require('blessed-contrib');
const screen = blessed.screen();
const line = contrib.line({ width: 80, height: 20, left: 0, bottom: 0, xPadding: 5, yPadding: 10, minY: 0, maxY: 100, numYLabels: 7 });
let data = [
{ title: 'Temperature',
x: [ ],
y: [ ],
style: {
line: 'red'
}
}
];
TTN_APP_ID = process.env['TTN_APP_ID'] || TTN_APP_ID;
TTN_ACCESS_KEY = process.env['TTN_ACCESS_KEY'] || TTN_ACCESS_KEY;
let series = [];
ttn.data(TTN_APP_ID, TTN_ACCESS_KEY).then(client => {
client.on('uplink', (devId, payload) => {
// console.log('retrieved uplink message', devId, payload.payload_fields.temperature_1);
data[0].x.push(new Date(payload.metadata.time).toLocaleTimeString().split(' ')[0]);
data[0].y.push(payload.payload_fields.temperature_1);
line.setData(data);
screen.render();
});
console.log('Connected to The Things Network data channel');
});
screen.append(line); //must append before setting data
screen.key(['escape', 'q', 'C-c'], function(ch, key) {
return process.exit(0);
});
There's some limitations in our current graph:
- It only shows a single device at the same time.
- The graph is drawn in the console.
Some extra credit excercises:
- Change the application so that it can show multiple devices at the same time.
- You can achieve this by inspecting
payload.dev_id
- this is the device that the message originated from. - Add a new series for every device that you see.
- Work with your neighbor to get multiple devices in your application (they need to change their keys to your app).
- You can achieve this by inspecting
- Turn this demo into a web application.
- Node.js + socket.io can be used to push new events down to the browser (tutorial here).
- Send the events from TTN -> your node app -> browser, and graph them in the browser.
- Storing data.
- At the moment nothing is stored.
- Store the data in a file, or in a database and read that back when you start the application.
- There's a ton of sensors available. Use them to build a useful device.
- A sensor which detects motion? Useful for meeting room or desk usage.
- An alarm that can be triggered remotely, there are buzzers.
- Soil moisture sensing, to know when to water your plants.
- Your call!
- Do some range tests. Take your device outside and see what the range is. What do you think effects the range most?
- Build a web application.
- You have an idea on how to get data from The Things Network but we don't do much with it.
- Add a web application that shows your devices and data.
- Store the data somewhere.
- Here's something that you might like: https://github.com/janjongboom/ttn-sensor-maps
Your pick!
-
Install Mbed CLI and the GNU ARM Compiler toolchain (v6).
-
Upgrade to JLink (ask Jan).
-
Install JLink GDB Server.
-
Run:
$ JLinkGDBServer -USB -device cortex-m4
-
In Visual Studio Code, hit
F5
.