This is the individual project for the IoT course 2022 of the master's degree "Engineering in computer science" at the "Sapienza" university. The project was born by observing that in most car-sharing services, people can drive the car even if they are drunk since there is no check on their conditions. Indeed to drive a car you need just to open it with the mobile application and take the keys inside. To solve the problem I created a cloud-based IoT breathalyzer connected to a box containing the keys of the car; if the test returns a negative value the box will open otherwise it will remain closed. The following are analyzed in more detail: the IoT device architecture, the cloud level, and the RIOT-OS code for the IoT device.
IoT deviceThe above image shows how the sensors and the actuators are connected to the SMT NUCLEO-f401re board.
The sensors used are the ultrasonic sensor, the MQ-3 alcohol sensor, and the push-button; the actuators used are the servo motor, three LEDs (mini traffic light), and a buzzer.
Ultrasonic sensor (HC SR04):
It is used to allow the alcohol sensor to compute a correct measure. Indeed it is located near the MQ 3 sensor and only if the distance from the sensors to the person is smaller than 5 cm, the MQ 3 module will start to take measurements of the person’s blood-alcohol level when he breathes out. The distance is estimated by sending a trigger signal and receiving an echo signal; the time (in us) computed divided by 58 is the distance in cm of the object in front of the ultrasonic sensor. It can measure distances in the range of 2-400 cm and the ranging accuracy can reach 3 mm. As soon as the car is opened through the mobile app (by giving power to the system), periodic sensing is done by the ultrasonic sensor (every 5 seconds a new measurement is performed). When the box containing the keys has been opened, the sensor stops to take measures. When the keys are put again in the box and the last one is locked by pressing the button on it, the sensor will start sensing again.
MQ 3 sensor:
It measures the concentration of alcohol in the air. Its detection range goes from 0.04 to 4 mg/l alcohol. It is a metal oxide semiconductor that detects the presence of alcohol vapors in the surroundings by changing resistance. Indeed when the concentration of alcohol becomes higher also the conductivity of the sensor rises. This change in conductivity is converted to an output value that indicates the level of the alcohol. In particular, when the value returned subtracted by 100 is greater than 450 the alcohol level is considered too high and the box key will remain closed. The sensor has both analog output and digital output, but for this project, the analog one is used. The MQ 3 sensor takes measurements only when the distance computed by the ultrasonic sensor is smaller than 5 cm, so a correct measurement can be computed.
Servo motor:
The servo motor is used to open or close the box containing the keys of the car. If the alcohol sensor returns a value smaller or equal to 450, the box will open so the keys can be taken. If the value measured is greater than 450 the box keys will remain closed.
Mini traffic light:
It has three LEDs: red, yellow, and green. They are used to provide feedback on the distance measured by the ultrasonic sensor. The red led is turned on when the distance is greater than 15 cm; the yellow one is turned on when the distance is between 5 cm and 15 cm; the green one is turned on when the distance is smaller than 5 cm. When the green led is on it means that the person is close enough to the sensors and can proceed to make the alcohol test, so the MQ 3 sensor is activated and can measure the alcohol level.
Button:
It is used to close the box keys. When it is pressed the servo motor is activated and the box keys will close. To connect the button to the board it was used a resistor of 10K Ohm.
Buzzer:
It is used to provide feedback when the breathalyzer returns a value over the limits. When the MQ 3 sensor measures a value greater than 450, the buzzer is turned on for 1 second. To connect the buzzer to the board it was used a resistor of 1 Ohm.
Cloud levelThe cloud level is developed entirely using the AWS ecosystem. In the below image there is a schema of how the AWS services used are connected in the overall system.
The IoT device level and the cloud one exchange messages through a communication protocol based on a publish/subscribe mechanism. The board sends the measures taken by the alcohol sensor to the Mosquitto broker using the MQTT-SN protocol. The messages are published under the topic “alcool_level”. Moreover, the board is subscribed to the topic “topic_in” to receive the messages sent from outside, that are used to close or open the box containing the keys. Mosquitto exchanges messages using MQTT with the AWS ecosystem through a transparent bridge, a python script that works as a bridge between Mosquitto and AWS IoT Core. Indeed it publishes the messages of “alcool_level” from the board to IoT Core and takes as input messages published from IoT Core under the topic “topic_in”, which are directed to the board. The messages coming from the board to IoT Core are then stored directly to the DynamoDB by setting a proper rule. They are then displayed on the web dashboard through a call of the REST API, which triggers the lambda function (“get_data_from_db.py”) that gets the data from the database. From the web dashboard, it is possible to close or open the box keys by publishing the message "close" or the message "open" under the topic “topic_in”. The messages are published to IoT Core by invoking the REST API which uses another lambda function (“publish_to_iotcore.py”) to perform this action. AWS Amplify is used to host all the static web content of the web dashboard.
On the web dashboard there are :
- two charts for displaying: the number of times the box keys has been opened in a day in the last seven days (values measured by the MQ-3 sensor smaller or equal to 450) and the number of times the alcohol test has returned a positive value in a day in the last seven days;
- a table displaying all measures taken by the MQ-3 sensor in the current day;
- the two buttons used to open or close the box keys;
- some statistics regarding the tests computed in the last seven days: the time slot in which the greatest number of tests resulted positive (a value between 8-12, 12-17, 17-20, 20-24, and 00-8); the number of times the box containing the keys has been opened; the number of time the breathalyzer detected a value over the limits; the percentage of positive tests over the total tests.
More details about the transparent bridge, the cloud level, and how to set up it correctly can be found on the GitHub repository of the project.
The logic of the RIOT codeThe main function is the following:
int main(void){
int result;
sensor_init();
mqtts_init();
while(true){
if(box_keys==0){
dist=distance_ultrasonic();
if(dist<5){
set_led("verde");
check_alcool();
}
else if(dist>=5 && dist<15){
set_led("giallo");
}
else{
set_led("rosso");
}
}
else{
while(box_keys==1){
result = gpio_read(box_pin);
if(result>0){
box_keys=0;
/*close box keys*/
servo_set(&servo, SERVO_MAX);
}
xtimer_sleep(0.5);
}
}
xtimer_sleep(5);
}
return 0;
}
If the global variable box_keys is equal to 0 it means that the box containing the keys is closed so we can proceed to take measurements. The function distance_ultrasonic returns the distance in cm computed from the ultrasonic sensor.
- If the distance is smaller than 5 cm: the green led of the mini traffic light is turned on thanks to the function set_led("verde") and the user can proceed to take the alcohol test. The function check_alcool menages all the parts related to the test (more details are explained below).
- If the distance is between 5 cm and 15 cm, the yellow led is turned on meaning that the distance to compute the test is almost good but the user must be more closed
- If the distance is greater than 15 cm, the red led is turned on meaning that the distance is too far and the user must be more close to the sensors to take the alcohol test.
If the global variable box_keys is not equal to 0 it means that the box containing the keys is opened so we enter in the "else" block. Until its value is equal to 1, every 0.5 seconds the pin connected to the button is read. If it returns a value greater than zero (when it is pressed it returns the value 256) the box is closed by locking it with the servo motor and the variable box_keys is set to 0 to allow to enter in the previous "if" block in the next round of the while loop.
If box_keys is equal to 0 the ultrasonic sensor will sense every 5 seconds due to the timer set outside the "if-else" block in the main while.
In the following there will be explained in more details all the functions mentioned before in the main function.
sensor_init function: it is used at the beginning of the main function to initialize all the GPIO pins of the sensors and actuators, plus the servo motor.
void sensor_init(void){
/*ultrasonic*/
gpio_init(trigger_pin, GPIO_OUT);
gpio_init_int(echo_pin, GPIO_IN, GPIO_BOTH, &call_back, NULL);
distance_ultrasonic(); /*first read returns always 0*/
/*mq3*/
adc_init(ADC_LINE(0));
/*traffic light*/
gpio_init(red_pin, GPIO_OUT);
gpio_init(yellow_pin, GPIO_OUT);
gpio_init(green_pin, GPIO_OUT);
/*button box keys*/
gpio_init(box_pin,GPIO_IN);
/*buzzer*/
gpio_init(buzzer_pin,GPIO_OUT);
/*servo init*/
servo_init(&servo, DEV, CHANNEL, SERVO_MIN, SERVO_MAX);
servo_set(&servo, SERVO_MAX);
}
All the variables used for the pins and the servo variable are global so they are defined outside the functions (you can find more information about them in the code inside the GitHub repository of the project). For the MQ 3 sensor, it is initialized the analog line from which the board receives the value. The constants DEV, CHANNEL, SERVO_MIN, SERVO_MAX used for initializing the servo motor are defined outside the function (you can find more details in the GitHub repository as well).
check_alcool function: it checks the level of the alcohol in the breath of the user and acts consequently.
void check_alcool(void){
int sample = 0;
char msg[4];
sample=read_mq3();
sprintf(msg, "%d", sample);
if (sample > 450) {
gpio_set(buzzer_pin);
xtimer_sleep(1);
gpio_clear(buzzer_pin);
} else {
/*open box keys*/
servo_set(&servo, SERVO_MIN);
box_keys=1;
}
pub(TOPIC_OUT1,msg);
}
The function read_mq3 returns the value computed by the MQ 3 sensor, if it is greater than 450 means that is over the legal limit so it is not possible to drive the car. The box containing the keys will remain closed and the buzzer is activated for 1 second (the buzzer is used to provide feedback on the positive result of the alcohol test to the user). If the value returned by the sensor is smaller or equal to 450, the box is opened (by unlocking the box through the servo motor) and the global variable box_keys is set to 1. In both cases, the value of the test computed by the breathalyzer is published with the function pub under the topic "alcool_level" (which is the value of the constant TOPIC_OUT1 ).
read_mq3 function: returns the value measured by the MQ 3 sensor.
int read_mq3(void){
int sample = 0;
int min = 100;
sample = adc_sample(ADC_LINE(0), RES);
sample = (sample > min) ? sample - min : 0;
return sample;
}
If the value measured by the sensor is greater than 100 it returns that value subtracted by 100, otherwise it returns 0.
distance_ultrasonic function: it returns the value measured by the ultrasonic sensor.
int distance_ultrasonic(void){
uint32_t dist;
dist=0;
echo_time = 0;
gpio_clear(trigger_pin);
xtimer_usleep(20);
gpio_set(trigger_pin);
xtimer_msleep(100);
if(echo_time > 0){
dist = echo_time/58;
}
return dist;
}
It sends an impulse to the sensor and waits 100 ms to read the value of the global variable echo_time. If the value is greater than 0 then it is divided by 58 to compute the distance in cm of the object in front of the sensor.
call_back function: it is used together with the distance_ultrasonic function to compute the value measured by the ultrasonic sensor.
void call_back(void* arg){
int val = gpio_read(echo_pin);
uint32_t echo_time_stop;
(void) arg;
if(val){
echo_time_start = xtimer_now_usec();
}
else{
echo_time_stop = xtimer_now_usec();
echo_time = echo_time_stop - echo_time_start;
}
}
This function is activated when it detects a change on the echo pin. It measures the difference in time from the sending of the ultrasonic pulse to when it is received back. It stores the value on the global variable echo_time which is used by the distance_ultrasonic function to compute the distance in cm of the object in front of the sensor. echo_time_stop is also a global variable.
set_led function: it is used to set the proper led of the mini traffic light depending on the parameter passed to the function.
void set_led(char *str){
if(strcmp(str,"verde")==0){
gpio_clear(red_pin);
gpio_clear(yellow_pin);
gpio_set(green_pin);
}
else if(strcmp(str,"rosso")==0){
gpio_clear(yellow_pin);
gpio_clear(green_pin);
gpio_set(red_pin);
}
else if(strcmp(str,"giallo")==0){
gpio_clear(red_pin);
gpio_clear(green_pin);
gpio_set(yellow_pin);
}
}
If str is "verde", the green led is turned on and the other ones are turned off. If str is "giallo", the yellow one is turned on and the other ones are turned off. If str is "rosso", then the red one is turned on and the other ones are turned off.
mqtts_init function: it initializes the connection with the MQTT-SN broker and it subscribes to the topic "topic_in" (value of the constant TOPIC_IN ) using the function sub.
static char stack[THREAD_STACKSIZE_DEFAULT];
static msg_t queue[8];
static emcute_sub_t subscriptions[NUMOFSUBS];
static char topics[NUMOFSUBS][TOPIC_MAXLEN];
void mqtts_init(void){
/* the main thread needs a msg queue to be able to run `ping`*/
msg_init_queue(queue, ARRAY_SIZE(queue));
/* initialize our subscription buffers */
memset(subscriptions, 0, (NUMOFSUBS * sizeof(emcute_sub_t)));
/* start the emcute thread */
thread_create(stack, sizeof(stack), EMCUTE_PRIO, 0, emcute_thread, NULL, "emcute");
char * addr1 = "fec0:affe::99";
add_address(addr1);
con();
sub(TOPIC_IN);
}
The following functions are used for the initialization part:
static void *emcute_thread(void *arg){
(void)arg;
emcute_run(BROKER_PORT, "board");
return NULL;
}
static int add_address(char* addr){
char * arg[] = {"ifconfig", "4", "add", addr};
return _gnrc_netif_config(4, arg);
}
static int con(void){
sock_udp_ep_t gw = { .family = AF_INET6, .port = BROKER_PORT };
char *topic = NULL;
char *message = NULL;
size_t len = 0;
ipv6_addr_from_str((ipv6_addr_t *)&gw.addr.ipv6, BROKER_ADDRESS);
if (emcute_con(&gw, true, topic, message, len, 0) != EMCUTE_OK) {
printf("error: unable to connect to [%s]:%i\n", BROKER_ADDRESS, (int)g w.port);
return 1;
}
printf("Successfully connected to gateway at [%s]:%i\n", BROKER_ADDRESS, (int)gw.port);
return 0;
}
The function sub is used to subscribe to the topic passed as parameter.
static int sub(char* topic){
unsigned flags = EMCUTE_QOS_0;
if (strlen(topic) > TOPIC_MAXLEN) {
puts("error: topic name exceeds maximum possible size");
return 1;
}
/* find empty subscription slot */
unsigned i = 0;
for (; (i < NUMOFSUBS) && (subscriptions[i].topic.id != 0); i++) {}
if (i == NUMOFSUBS) {
puts("error: no memory to store new subscriptions");
return 1;
}
subscriptions[i].cb = on_pub;
strcpy(topics[i], topic);
subscriptions[i].topic.name = topics[i];
if (emcute_sub(&subscriptions[i], flags) != EMCUTE_OK) {
printf("error: unable to subscribe to %s\n", topic);
return 1;
}
printf("Now subscribed to %s\n", topic);
return 0;
}
When a message is received under the topic subscribed (in this case' the topic "topic_in"), the function on_pub menages it:
static void on_pub(const emcute_topic_t *topic, void *data, size_t len){
(void)topic;
char *in = (char *)data;
printf("### got publication for topic '%s' [%i] ###\n", topic->name, (int)topic->id);
for (size_t i = 0; i < len; i++) {
printf("%c", in[i]);
}
puts("");
char msg[len+1];
strncpy(msg, in, len);
msg[len] = '\0';
if (strcmp(msg, "open") == 0){
if(box_keys==0){
/*open box keys*/
servo_set(&servo, SERVO_MIN);
box_keys=1;
}
}
else if (strcmp(msg, "close") == 0){
if(box_keys==1){
/*close box keys*/
servo_set(&servo, SERVO_MAX);
box_keys=0;
}
}
}
If the message received is "open" then the box containing the keys is open by unlocking it through the servo motor and the global variable box_keys is set to 1. If the message is "close" the box is locked using the servo motor and the global variable box_keys is set to 0. The first part of the function is used to get feedback by printing on the terminal the message received and its related topic.
The function pub is used to publish messages.
static int pub(char* topic,char* msg){
emcute_topic_t t;
unsigned flags = EMCUTE_QOS_0;
printf("pub with topic: %s and name %s and flags 0x%02x\n", topic, msg, (int)flags);
/* step 1: get topic id */
t.name = topic;
if (emcute_reg(&t) != EMCUTE_OK) {
puts("error: unable to obtain topic ID");
return 1;
}
/* step 2: publish data */
if (emcute_pub(&t, msg, strlen(msg), flags) != EMCUTE_OK) {
printf("error: unable to publish data to topic '%s [%i]'\n",t.name, (int)t.id);
return 1;
}
printf("Published %i bytes to topic '%s [%i]'\n", (int)strlen(msg), t.name, t.id);
return 0;
}
In particular, the second argument of the function is the message you want to publish and the first argument is the name of the related topic.
GalleryMore details about: the code of the web app, how to set up correctly the overall system, the transparent bridge used to connect the IoT core to the Mosquitto broker are available on the GitHub repository of the project.
Here there is a demonstration of how the prototype built works, it is a video uploaded on Youtube.
Here there is the link to the web dashboard.
This is my LinkedIn account.
Comments