As part of the IoT course, I worked on an ventilation system as my individual project. The main feature of the system is its capability to trigger the ventilation system when the measured temperature level is above a threshold. Futhermore you can control the ventilation from the web application. In the following, we'll see how to interface with the board and sensor and the communication about the board and web application.
Usefull LinkOverviewThe system is composed by a RIOT-OS device, a Mosquitto broker (RSMB) and a transparent bridge that work as a bridge with the device and the AWS IoT Core. So it's a cloud-based IoT system that collects information from dht11 sensor using the MQTT protocol, with the AWS IoT services. I have also implemented a simple web application to display the data collected from the sensors. The most important parts needed to implement the application will be shown below.
Init Board & SensorsSince the STM32 Nucleo-64 F401RE does not provide a wireless network interface we need to provide network connectivity via the USB connection, this is achieved with ethos_uhcpd tool provide by RIOT-OS.
The ethos_uhcpd tool will provide network connectivity through the TAP interface. The UHCP will be used to provide a network address. Finally, The ethos_uhcpd tool will use the script setup_network.sh located in the $(RIOTTOOLS)/ethos directory using superuser privileges. This script sets up a tap device, configures a prefix and starts a uhcpd server serving that prefix towards the tap device. The execution of the script is specified within the Makefile.ethos.conf file.
CFLAGS += -DETHOS_BAUDRATE=$(ETHOS_BAUDRATE)
CFLAGS += -DCONFIG_GNRC_NETIF_IPV6_ADDRS_NUMOF=3
STATIC_ROUTES ?= 1
ifeq (1,$(USE_DHCPV6))
FLAGS_EXTRAS=--use-dhcpv6
endif
# Configure terminal parameters
TERMDEPS += host-tools
TERMPROG ?= sudo sh $(RIOTTOOLS)/ethos/start_network.sh
TERMFLAGS ?= $(FLAGS_EXTRAS) $(PORT) $(TAP) $(IPV6_PREFIX) $(ETHOS_BAUDRATE)
Then in the main code, we handle the initialization of the component and the definition of some usefull costants :
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "random.h"
#include "main.h"
#include "xtimer.h"
#include "fmt.h"
#include "dht.h"
#include "dht_params.h"
#include "periph/gpio.h"
//Networking
#include "net/emcute.h"
#include "net/ipv6/addr.h"
// Macro for networks processes.
#define _IPV6_DEFAULT_PREFIX_LEN (64U)
#define EMCUTE_PRIO (THREAD_PRIORITY_MAIN - 1)
#define NUMOFSUBS (16U)
#define TOPIC_MAXLEN (64U)
#define DEVICE_IP_ADDRESS ("fec0:affe::99")
#define DEFAULT_INTERFACE ("4")
#define MQTT_TOPIC_INT "topic_board"
#define MQTT_TOPIC_EXT "topic_data"
#define MQTT_QoS (EMCUTE_QOS_0)
/* GPIO pin for 7Segment Display */
gpio_t SEGMENT_A = GPIO_PIN(PORT_B,10);
gpio_t SEGMENT_B = GPIO_PIN(PORT_B,3);
gpio_t SEGMENT_C = GPIO_PIN(PORT_A,7);
gpio_t SEGMENT_D = GPIO_PIN(PORT_B,6);
gpio_t SEGMENT_E = GPIO_PIN(PORT_A,10);
gpio_t SEGMENT_F = GPIO_PIN(PORT_B,5);
gpio_t SEGMENT_G = GPIO_PIN(PORT_B,4);
// Initialize GPIO pin for motor
gpio_t motor_pin = GPIO_PIN(PORT_C,7);
/* Constant for internal algorithm */
typedef enum{
AUTO = 0,
ON = 1,
OFF = 2
} state_t;
state_t system_mod = AUTO;
dht_t dev;
//Function that handle the initialization
static int init_component(void){
// GPIO pin for 7-segment display
gpio_init(SEGMENT_A, GPIO_OUT);
gpio_init(SEGMENT_B, GPIO_OUT);
gpio_init(SEGMENT_C, GPIO_OUT);
gpio_init(SEGMENT_D, GPIO_OUT);
gpio_init(SEGMENT_E, GPIO_OUT);
gpio_init(SEGMENT_F, GPIO_OUT);
gpio_init(SEGMENT_G, GPIO_OUT);
// Initialization -------------------------------
static int init_component(void){
// GPIO pin for 7-segment display
gpio_init(SEGMENT_A, GPIO_OUT);
gpio_init(SEGMENT_B, GPIO_OUT);
gpio_init(SEGMENT_C, GPIO_OUT);
gpio_init(SEGMENT_D, GPIO_OUT);
gpio_init(SEGMENT_E, GPIO_OUT);
gpio_init(SEGMENT_F, GPIO_OUT);
gpio_init(SEGMENT_G, GPIO_OUT);
// Fix port parameter for digital sensor
dht_params_t my_params;
my_params.pin = GPIO_PIN(PORT_A, 8);
my_params.in_mode = DHT_PARAM_PULL;
// Initialize sensor
if (!(dht_init(&dev, &my_params) == DHT_OK)) {
printf("\ninit_component: Failed to connect to DHT sensor ");
return 1;
}
if (gpio_init(motor_pin, GPIO_OUT)) {
printf("\ninit_component: Error to initialize GPIO_PIN(%d %d) ", PORT_C, 9);
return 1;
}
printf("\ninit_component: Initialization components completed. ");
return 0;
}
Now in the makefile, we need to include not only the module necessary for providing network interface, also we need to include the module necessary for the sensor. So at least the makefile should be like this :
# name of your application
APPLICATION = WINDFORME
# If no BOARD is found in the environment, use this default:
BOARD ?= nucleo-f401re
# This has to be the absolute path to the RIOT base directory:
RIOTBASE ?= $(CURDIR)/../../RIOT
# Provide network connectivity via the USB
# Default to using ethos for providing the uplink when not on native
UPLINK ?= ethos
# Include packages that pull up and auto-init the link layer.
# NOTE: 6LoWPAN will be included if IEEE802.15.4 devices are present
USEMODULE += netdev_default
USEMODULE += auto_init_gnrc_netif
# Specify the mandatory networking modules for IPv6
USEMODULE += gnrc_ipv6_default
# Optimize the use of network interface
USEMODULE += gnrc_netif_single #non necessario
USEMODULE += stdio_ethos gnrc_uhcpc
# Include MQTT-SN client
USEMODULE += emcute
# Tap interface for ethos
EMCUTE_ID ?= 01
ETHOS_BAUDRATE ?= 115200
TAP ?= tap0
USE_DHCPV6 ?= 0
IPV6_PREFIX ?= fe80:2::/64
# IPv6 and port of the MQTT broker
SERVER_ADDR = fec0:affe::1 # Global Address tapbr0 interface
SERVER_PORT = 1885
CFLAGS += -DSERVER_ADDR='"$(SERVER_ADDR)"'
CFLAGS += -DSERVER_PORT='$(SERVER_PORT)'
# Comment this out to disable code in RIOT that does safety checking
# which is not needed in a production environment but helps in the
# development process:
DEVELHELP ?= 1
# Change this to 0 show compiler invocation lines by default:
QUIET ?= 1
# Allow for env-var-based override of the nodes name (EMCUTE_ID)
ifneq (,$(EMCUTE_ID))
CFLAGS += -DEMCUTE_ID=\"$(EMCUTE_ID)\"
endif
# Modules to include:
USEMODULE += dht
USEMODULE += fmt
USEMODULE += xtimer
USEMODULE += periph_gpio
USEMODULE += random
include $(CURDIR)/Makefile.ethos.conf
include $(RIOTBASE)/Makefile.include
.PHONY: host-tools
host-tools:
$(Q)env -u CC -u CFLAGS $(MAKE) -C $(RIOTTOOLS)
The most important thing is that the SERVER_ADDR IP must be the same of the global address of the tabr0 interface. We will see it on Mosquitto.RSMB section.
Sensor CamplingThis is one of the main features of our application. The process of data clamping by the dht11 sensor, is handled through a thread. To summarize, the logic of operation is as follows :The sensor reads the data every 10 seconds, then checking the success of the reading process. If it has given no errors, the data is converted into a format such that it can be compared with a number value i.e. temperature threshold. If the threshold is exceeded, the ventilation system is started, inserting a flag to prevent further retriggering. The ventilation system will only shut down once the temperature falls below the threshold temperature.
void *sampling_temperature(void* arg){
(void)arg;
bool dataFlag = false;
bool flag_on = true;
bool flag_off = true;
while(1){
// Retrieve sensor reading
int16_t temp, hum;
if (dht_read(&dev, &temp, &hum) != DHT_OK) {
printf("\nsampling_temperature: Error reading values ");
dataFlag = true;
}
if(!dataFlag){
// Extract + format temperature from sensor reading
char temp_s[10];
size_t n = fmt_s16_dfp(temp_s, temp, -1);
temp_s[n] = '\0';
// Extract + format humidity from sensor reading
char hum_s[10];
n = fmt_s16_dfp(hum_s, hum, -1);
hum_s[n] = '\0';
printf("\nDHT values - temp: %s°C - relative humidity: %s%% ",temp_s, hum_s);
//setting up message to send
char message[15];
sprintf(message,"t%dh%d", temp, hum);
publish(MQTT_TOPIC_EXT, message); //publishing message on broker
if(system_mod == AUTO ){
if (flag_on && ((unsigned int)temp >= 255)) {
printf("\nTemperature above threshold - activating motor ");
set_digit_value(1);
gpio_set(motor_pin);
flag_on = false;
flag_off = true;
}
if(flag_off && ((unsigned int)temp <= 254)) {
printf("\nTemperature under threshold - deactivating motor ");
set_digit_value(0);
gpio_clear(motor_pin);
flag_off = false;
flag_on = true;
}
}
}else{
//send a failure payload to mqtttopic
}
dataFlag=false;
// Sampling rate 10 for testing
xtimer_sleep(10);
}
}
Motor HandlingAnother important feature is the handling of the message we receive from the web application. Since we perform operations at the bridge level, which we will see later, to format our message, we know that the board will receive the strings : AUTO / ON / OFF, which represent the functionality of the system kernel. Consequently based on the type of message received the corresponding functionality will be triggered.
void motor_handling(void){
//GESTIONE DI TUTTA LA FASE DI AVVIO FORZATO E OFF FORZATO DEL SERVO.
switch(system_mod){
case AUTO:
printf("\nAUTO ");
break;
case ON:
printf("\nMotor activation by external control ");
set_digit_value(2);
gpio_set(motor_pin);
break;
case OFF:
printf("\nMotor deactivation by external control ");
set_digit_value(2);
gpio_clear(motor_pin);
break;
default:
printf("\nmotor_handling : command not found ");
}
}
static void on_pub(const emcute_topic_t *topic, void *data, size_t len){
...
if (strcmp(msg, "AUTO") == 0) {
system_mod = AUTO;
} else if (strcmp(msg, "ON") == 0) {
system_mod = ON;
} else if (strcmp(msg, "OFF") == 0) {
system_mod = OFF;
} else {
printf("Valore di msg non valido ");
}
printf("\n\nsystem_mod: %d\n\n", system_mod);
motor_handling();
...
}
7 Segment DisplayIn my case, the display has 10 pins, 2 of which function as common pins used for the power supply, and 1 is used to decide whether to turn on the LED corresponding to the decimal value. The other pins are connected to the respective light segments arranged to form the digits 0 to 9. The lighting logic of the display consist to assign 0 to the led segment we want to turn on and 1 to the led segment we want to keep off. To enable a segment, just call the following function from the riot os gpio.h library :
gpio_write(gpio_for_segment_a, 0); #1 if you want to keep off.Mosquitto RSMB
You can download mosquitto.rsmb from here.
SETUP
Since mosquitto.rsmb doesn't seem to be able to handle link-local addresses, we must configure some global addresses. So it is important that both the makefile and the interface configuration have the same ip addresses. To configure the interface, RIOT-OS provides us with a script called tapsetup, here are the steps to follow :
Setup the interfaces and the bridge using RIOT's tapsetup script:
sudo./RIOT/dist/tools/tapsetup/tapsetup -c
Assign a site-global prefix to the tapbr0 interface:
sudo ip a a fec0:affe::1/64 dev tapbr0
For testing purpose maybe you need to delete the interface created, then it is usefull to use the folling command to delete them :
sudo./RIOT/dist/tools/tapsetup/tapsetup -d
Then in the makefile remember to declare the costant with the same IP of the global ip assigned to the interface.
SERVER_ADDR = fec0:affe::1 # Global Address tapbr0 interface
LAUNCH
Once the mosquitto is downloaded, navigate in it's directory then :
cd mosquitto.rsmb/rsmb/src
make
After finishing the execution of make, create a file whatever_name.conf
. In this case we run the RSMB as MQTT and MQTT-SN capable broker, using port 1885 for MQTT-SN and 1886 for MQTT and enabling IPv6, so we need to write this inside the whatever_name.conf file.
# add some debug outputtrace_output protocol# listen for MQTT-SN traffic on UDP port 1885listener 1885 INADDR_ANY mqttsipv6 true# listen to MQTT connections on tcp port 1886listener 1886 INADDR_ANYipv6 true
To start the server :
./broker_mqtt whatever_name.conf
Pay attention to the server_port constant declared in the makefile, they need to be the same of the MQTT-SN port written in the previous file.
Publish & Subscribption MQTT-SNRIOT provides emCute to implement the MQTT-SN protocol, so first of all we need to configure them and add a network interface for the board.
static int setup_mqtt(void){
//Setting memory for the subscription list
memset(subscriptions, 0, (NUMOFSUBS * sizeof(emcute_sub_t)));
//Starting Emcute Thread
thread_create(stack, sizeof(stack), EMCUTE_PRIO, 0,emcute_thread, NULL, "emcute");
printf("\nEmcute Thread Started ");
if(add_netif(DEFAULT_INTERFACE,DEVICE_IP_ADDRESS)){
puts("\nsetup_mqtt: Faile to add network interface ");
return 1;
}
return 0;
}
static int add_netif(char *name_if, char *dev_ip_address){
netif_t *iface = netif_get_by_name(name_if); //getting the interface where to add the address
if(!iface){
puts("\nadd_netif: No valid Interface");
return 1;
}
ipv6_addr_t ip_addr;
uint16_t flag = GNRC_NETIF_IPV6_ADDRS_FLAGS_STATE_VALID;
uint8_t prefix_len;
prefix_len = get_prefix_len(dev_ip_address);
//Parsing IPv6 Address from String
if(ipv6_addr_from_str(&ip_addr, dev_ip_address) == NULL){
puts("\nadd_netif: Error in parsing ipv6 address");
return 1;
}
flag |= (prefix_len << 8U);
//Set Interface Options
if(netif_set_opt(iface, NETOPT_IPV6_ADDR, flag, &ip_addr, sizeof(ip_addr)) < 0){
puts("\nadd_netif: Error in Adding ipv6 address");
return 1;
}
printf("\nadd_netif : Added %s with prefix %d to interface %s ", dev_ip_address, prefix_len, name_if);
return 0;
}
After setting up everything the board needs, we start connecting the board with the local broker.
static int connect_broker(void){
printf("\nConnecting to MQTT-SN broker %s port %d. ",SERVER_ADDR, SERVER_PORT);
// Socket creation
sock_udp_ep_t gw = {
.family = AF_INET6,
.port = SERVER_PORT
};
char *topic = NULL;
char *message = NULL;
size_t len = 0;
//Parsing IPv6 Address from String
if (ipv6_addr_from_str((ipv6_addr_t *)&gw.addr.ipv6, SERVER_ADDR) == NULL) {
puts("\nconnect_broker: error parsing IPv6 address ");
return 1;
}
//Connecting to broker
if (emcute_con(&gw, true, topic, message, len, 0) != EMCUTE_OK) {
printf("\nconnect_broker: unable to connect to %s:%i ", SERVER_ADDR, (int)gw.port);
return 1;
}
printf("\nSuccessfully connected to gateway at [%s]:%i ",SERVER_ADDR, (int)gw.port);
return 0;
}
The following function subscribes to the topic used by the board to receive messages from outside. When it receives a message it is passed as parameter to the callback function on_pub which processes it.
// MQTT Initialization and subscription
static int sub(void){
//Setup subscription
subscriptions[0].cb = on_pub;
subscriptions[0].topic.name = MQTT_TOPIC_INT;
//Subscribing to topic
if (emcute_sub(&subscriptions[0], MQTT_QoS) != EMCUTE_OK) {
printf("\nconnect_broker: unable to subscribe to %s ", subscriptions[0].topic.name);
return 1;
}
printf("\nsub: Now subscribed to %s ", subscriptions[0].topic.name);
return 0;
}
//Function called when a message is published on the topic the board is subscribed
static void on_pub(const emcute_topic_t *topic, void *data, size_t len){
(void)topic;
(void)len;
char *in = (char *)data;
char msg[len+1];
strncpy(msg, in, len);
msg[len] = '\0';
printf("\nContenuto di msg : %s", msg);
if (strcmp(msg, "AUTO") == 0) {
system_mod = AUTO;
} else if (strcmp(msg, "ON") == 0) {
system_mod = ON;
} else if (strcmp(msg, "OFF") == 0) {
system_mod = OFF;
} else {
printf("Valore di msg non valido ");
}
printf("\n\nsystem_mod: %d\n\n", system_mod);
motor_handling();
//Setting memory for the message buffer
memset(in, 0, sizeof(char));
}
At least, the function that publish the message.
// Function for publish message to a topic
static int publish(char *t, char *message){
emcute_topic_t topic;
topic.name = t;
//getting ID from the topic
if(emcute_reg(&topic) != EMCUTE_OK){
printf("\npublish : cannot find topic:%s ", topic.name);
return 1;
}
//publishing on the broker
if(emcute_pub(&topic, message, strlen(message), MQTT_QoS) != EMCUTE_OK){
puts("\npublish : cannot publish data ");
return 1;
}
}
Transparent BridgeAlong with the mosquitto.rsmb server we must also have running a transparent bridge which serves as a link between mosquitto.rsmb and AWS IoTCore. The purpose is to receive messages only connecting to the desired topics ( "topic_data" ), and send them to AWS IoT via MQTT. It also reads messages from AWS IoTCore on “topic_board” and publishes them on the local broker with the same topic.
From the README of the related github repository, you can see how to download the dependencies. Then in order to configure your bridge, written in python, we must import some module and init some parameter.
import paho.mqtt.client as mqtt
from AWSIoTPythonSDK.MQTTLib import AWSIoTMQTTClient
from AWSIoTPythonSDK.exception.AWSIoTExceptions import connectError
import json
import paho.mqtt.publish as publish
#AWS Paramater Init
AWS_HOST = "a3b5fvirnosinx-ats.iot.eu-west-1.amazonaws.com" #can be found as parameter '--endpoint' in the last line of the file start.sh
# Change with your absolute path
AWS_ROOT_CAP_PATH = "/home/nardo/Desktop/IoTDeviceKey/root-CA.crt"
AWS_PRIVATE_KEY_PATH = "/home/nardo/Desktop/IoTDeviceKey/windforme.private.key"
AWS_CERTIFICATE_PATH = "/home/nardo/Desktop/IoTDeviceKey/windforme.cert.pem"
AWS_PORT = 8883
The certification must be downloaded from the AWS IoT Core services, by following the connection wizard of a device available on the AWSIoTCore dashboard.You have to create two client instances, one for the local broker and the other for AWS IoT Core, in order to connect them to the brokers.
print("MQTT Client connection")
# Create MQTT client instance
mqttClient = mqtt.Client()
# Set up callbacks
mqttClient.on_message = on_message
mqttClient.subscribe(TOPIC_EXT)
print("AWS Client connection")
myAWSIoTMQTTClient = None
myAWSIoTMQTTClient = AWSIoTMQTTClient("windforme")
myAWSIoTMQTTClient.configureEndpoint(AWS_HOST, AWS_PORT)
myAWSIoTMQTTClient.configureCredentials(AWS_ROOT_CAP_PATH, AWS_PRIVATE_KEY_PATH, AWS_CERTIFICATE_PATH)
try:
myAWSIoTMQTTClient.connect()
print("Connected to AWS IoT MQTT broker")
except connectError as e:
print("Connection to AWS IoT MQTT broker failed:", str(e))
print("AWSClient Subscription")
myAWSIoTMQTTClient.subscribe(TOPIC_INT,1,myCallback)
# Connect to MQTT broker
mqttClient.connect(BROKER_ADDRESS, BROKER_PORT , 60)
# Start the network loop to handle MQTT communication
mqttClient.loop_forever()
As you can see you have mqttClient subscribed to topic_data and the awsClient, representing our thing in the AWS IoT Core service, subscribed to topic_board. So now whenever the board writes to the topic_board the data read from the sensor, the callback function of the awsClient is invoked which will send the message to the AWS IoT Core. On the other way, when from the webapp we go to write to the topic listening from the mqttClient, the callback function will be triggered, which will send the message to the board.
def myCallback(client, userdata, message):
print("Received message on topic:", message.topic)
print("Message:", message.payload)
mqttClient.publish(TOPIC_INT,message.payload)
def on_connect(client, userdata, flags, rc):
print("Connected with result code "+str(rc))
if rc == 0:
print("Subscribe to topic : "+TOPIC_EXT)
client.subscribe(TOPIC_EXT)
else:
print("Connection failed. Return code =", rc)
def on_message(client, userdata, msg):
print("Received message:", msg.topic, msg.payload)
message = str(msg.payload)[2:len(str(msg.payload))-1]
message = message.split("h")
message_out = {}
message_out['temperature'] = message[0][1:len(message[0])-1]+"."+ message[0][len(message[0])-1:]
message_out['humidity'] = message[1][:len(message[1])-1]+"."+ message[1][len(message[1])-1:]
messageJson = json.dumps(message_out)
print("Json inviato : "+messageJson)
#Publishing message
myAWSIoTMQTTClient.publish(TOPIC_TO_AWS, messageJson, 1)
AWS ServicesIn the following you need to setup :
- DynamoDB : only by creating a single table which you want to save temperature and humidity.
- Lambda Function : write two lambda function ( see on github ) one which store the temperature and humidity value into dynamo DB, and the second one which publish a message to a topic. Then in order to invoke them without the use of AWS Api Gateway generate your lambda function URL, during this process check the box Configure cross-origin resource sharing(CORS) in this way you will avoid some restriction given from the CORS Policy.
- AWSIoTCore : create a device by following the connection wizard on the dashboard of the relative service. Then you need to modify the Policy associated to the device in order to accomplish all the function needed. (Example of Policy)
- IAM Management Console : futhermore, in order to avoid some conflict due to the functionality of the lambda function modify the role associated to them and add for the first one the permission to write on DynamoDB services, and the second one the permission to publish a message.
- AWS Amplify for hosting the site, but you will be able to test it on localhost. Any service that can host a web page will work.
The WebApplication provides the following functionality :
- Display the latest values received from all the sensors of a specified environmental station.
- Display some measurement values computed by considering all the value recieved during the last hour from all environmental station of a specified sensor.
- The possibility to change the kernel functionality of the board by invoking special command from the button.
- Start your rsmb server
- Start the transparent bridge
- Setup global prefix
- Flash the kernel of the nucleo board :
make flash term
That’s all hope you’re enjoying the reading. Thanks for your attention. Visit github repo for more information.
Comments