The IoT Garbage Collection device will gather data to determine the current status of the garbage container it is installed in and share it through wireless communications.
Knowing how much volume of garbage each container has from the management center will allow them determine when a container must be collected in order to optimize the routes, collection calendars, collection routes, whether more o less containers are needed in an specific area. Reducing the number of trucks and resources needed.
With the extra information of environmental data they can determine if there is an emergency or something happened to the container in order to take proper action.
1. Project descriptionHelped by its the Thingy:53 integrated sensors and an external distance sensor, this device will be able to collect many parameters of the garbage container, such as:
- The level of garbage in the container.
- 3D position of the container.
- Temperature and humidity in the container.
- Air quality monitoring.
- Sound monitoring.
This data collected will be used to determine the current state of the container and detect any hazard or dangerous condition:
- The container must be collected.
- The container has been moved or tipped over.
- The container could be on fire.
- The container could have dangerous items releasing dangerous gases.
- A person or an animal could be trapped in the container.
The data and any determined warning will be transmitted to the operation center for further actions.
2. Hardware setupThe hardware used is:
2.1 Nordic Thingy:53The Thingy:53 is used as the platform, this board already provides most of the sensors intended for use.
This board has a Humidity, Temperature and Pressure sensor (bme680) connected through an I2C bus from which Humidity and Temperature data will be collected. It also has accelerometers, color and light sensors, a microphone and air quality sensors.
Along these sensors, the board provides different communication possibilities like Bluetooth Low Energy ort Zigbee, in this project the bluetooth will be used.
2.2 Ultrasonic distance sensor (HC-SR04)For this prototype, I used a standard HC-SR04 sensor instead of a Grove Ultrasound distance sensor because I already have the standard sensor and I cannot find an affordable/cheap grove sensor due mainly to the high shipping costs. Using this sensor comes with a few extra challenges.
The differences between the sensors are:
- Connection pinout:
- Pinout for standard sensor: [ VCC - TRIGGER - ECHO - GND ]
- Pinout for Grove Ultrasound distance sensor: [ GND - VCC - Not Connected - SIGNAL ]
- Voltage:
- Voltage for standard sensor: 5V
- Voltage for Grove Ultrasound distance sensor: 3,3V
To solve these issues I used a breadboard and an Arduino Nano as the 5V external energy source for the sensor.
The result of the schematic is the following:
- The 5V pin of the Arduino Nano is connected to the VCC pin of the HC-SR04 sensor.
- The GND pins of the three elements are connected together.
- The sensor can be triggered with 3,3V therfore the TRIGGER pin is directly connected to the first pin (from left to right) of the Thingy:53 Grove port.
- The sensor outputs a 5V signal through the ECHO pin, a voltage divider is used to avoid damaging the Thingy:53.
Voltage Divider
The voltage divider circuit is explained in references [2a], [2b] and [3], its purpose is to scale down the input voltage to a lower value. It consists of 2 resistors connected in series as the following image.
The output voltage is measured between point 1 (between R1 and R2) and point 2 (between R2 and GND).
The equation of this circuit is the following:
Where:
- Vin is the input voltage we have (5V).
- Vout is the output voltage we need (3.3V).
- R1 and R2 are resistors of some determined values to obtained the Vout from the Vin. With help of a calculator ([2a] or [2b]) we can see that using R1 = 1K and R2 = 2K Ohms fits our needs [3].
The software will be divided in 4 steps main steps: Initialization, Data Collection, Data Processing and Data Publication:
- The Initialize phase prepares all the sensors used by the device.
- The Data Collection phase communicates with each sensor to obtain the needed information of the container.
- The Data Processing phase determines any hazard or warning situation based on the data collected.
- The Data Publication phase transmits all the information, either collected or processed.
3.2.1 Main app code
The main app code is as simple as an initialization phase and an infinit loop where data is collected, processed and published. The infinit loop is executed every X time saving energy and optimizing battery life.
/**
* Main function
*/
void main(void)
{
printk("Starting Garbage Collection IoT\n");
// Initialize phase
initialize ();
for (;;) {
// Data Collection
data_collection ();
// Data Processing
data_processing ();
// Data publication
data_publication ();
// Wait until next measure
k_sleep(K_MSEC(SLEEP_TIME_MS));
}
}
3.2.2 Initialization phase
In the initialization phase, the sensors and the communication peripherals are prepared one by one, in later sections these will be explained with more detail.
/**
* Initialize phase function
*/
void initialize (void)
{
// Configure sensor peripherals
hcsr04_init ();
bme680_init ();
// Configure communication peripherals
bt_init ();
}
3.2.3 Data collection phase
In the data collection phase, the sensors are called to realize the actual measurement and then the value is retrieved. The values are simplified based on thresholds. Measurements code will be detailed in later sections.
/**
* Data Collection phase function
*/
void data_collection (void)
{
// Temp variables
t_container container_status;
uint32_t distance = 0;
float temperature = -1.0;
float humidity = -1.0;
int err = 0;
// Measure distance and update fill status
// (The greater the distance the emptier the container)
distance = hcsr04_measure ();
if (distance < FILL_ERROR_THRESHOLD) container_status.fill = F_ERROR;
else if (distance >= FILL_EMPTY_THRESHOLD) container_status.fill = F_EMPTY;
else if (distance >= FILL_25P_THRESHOLD) container_status.fill = F_25P;
else if (distance >= FILL_50P_THRESHOLD) container_status.fill = F_50P;
else if (distance >= FILL_75P_THRESHOLD) container_status.fill = F_75P;
else container_status.fill = F_FULL;
// Measure and update Temperature
err = bme680_update_measurements ();
// Update temperature status
err = bme680_get_temperature (&temperature);
if (err == 0) {
if (temperature <= MIN_TEMP_THRESHOLD) container_status.temperature = T_LOW;
else if (temperature >= MAX_TEMP_THRESHOLD) container_status.temperature = T_HIGH;
else container_status.temperature = T_NORMAL;
} else {
container_status.temperature = T_ERROR;
}
// Update humidity status
err = bme680_get_humidity (&humidity);
if (err == 0) {
if (humidity <= HUM_LOW_THRESHOLD) container_status.humidity = H_LOW;
else if (humidity >= HUM_HIGH_THRESHOLD) container_status.humidity = H_HIGH;
else container_status.humidity = H_MEDIUM;
} else {
container_status.humidity = H_ERROR;
}
// End collection, copy to global memory
g_container = container_status;
}
3.2.4 Data processing phase
In the data processing phase, the data obtained in the previous phase is processed by the alert functions to determine if an alert must be raised. Alerts will be detailed in a later section.
/**
* Data Processing phase function
*/
void data_processing (void)
{
// Temp variables
t_alerts alerts_status;
// Process Filled alert
alerts_status.filled = alertFilled (g_container.fill);
// Process Temperature alert
alerts_status.temperature = alertTemperature (g_container.temperature);
// Process Humidity alert
alerts_status.humidity = alertHumidity (g_container.humidity);
// Process Position alert
alerts_status.position = alertPosition (g_container.position);
// End processing, copy to global memory
g_alerts = alerts_status;
}
Alert basic processing consists in checking whether the values are above a certain threshold:
/**
* Data alerts
*/
bool alertFilled (t_fill filled)
{
return (filled >= F_75P);
}
bool alertTemperature (t_temperature temperature_status)
{
return (temperature_status >= T_HIGH);
}
bool alertHumidity (t_humidity humidity)
{
return (humidity >= H_HIGH);
}
3.2.5 Data publication phase
In the data publication phase, the data obtained from the data collection phase and the alerts from the data processing phase are transmitted through the communication peripherals, bluetooth in this case.
/**
* Data Publication phase function
*/
void data_publication (void)
{
bt_update (g_container, g_alerts);
}
3.2.6 HC-SR04 sensor code
The code implemented for the distance sensor (HC-SR04) is based on another hackster project.
The HC-SR04 code is implemented in 2 functions: init & measure
#ifndef _INCLUDE_SENSORS_HCSR04_H_
#define _INCLUDE_SENSORS_HCSR04_H_
#include <stdint.h>
void hcsr04_init ();
uint32_t hcsr04_measure ();
#endif // _INCLUDE_SENSORS_HCSR04_H_
The HC-SR04 is controlled using the GPIO pins, therefore the device used is the GPIO port and the pins are controlled individually.
The initialization code is in the hcsr04_init()
function, it uses zephyr functions to obtain the HC-SR04 port (GPIO_0) and initializes the used pins for the Trigger (4) and the Echo (5) of the device.
static const struct device *hcsr04_dev;
void hcsr04_init ()
{
printk("Starting HC-SR04 Peripheral\n");
// Get Port device for HC-SR04
hcsr04_dev = device_get_binding(HCSR04_PORT);
if (hcsr04_dev == NULL)
{
printk ("Error binding device: HCSR04_PORT\n");
}
else
{
// Configure trigger and echo pins
gpio_pin_configure(hcsr04_dev, HCSR04_TRIG_PIN, GPIO_OUTPUT);
gpio_pin_configure(hcsr04_dev, HCSR04_ECHO_PIN, (GPIO_INPUT | GPIO_ACTIVE_HIGH));
printk("HCSR04 Configured!\n");
}
}
The measurement code is in the hcsr04_measure()
function this is a blocking funtion, it blocks the processor until the echo is received.
First of all it triggers a measurement by setting the Trigger pin for a predefined time and unsets it. Then, it waits until the HC-SR04 sets the Echo pin indicating the echo was received. The cycles of both trigger and echo events are recorded and the difference indicates the time lapsed between the activation and the reception. With this time and the known velocity of sound in air, the distance is calculated.
uint32_t hcsr04_measure ()
{
uint32_t cycle_start;
uint32_t cycle_stop;
uint32_t cycle_diff;
uint32_t measure_time;
uint32_t val;
uint32_t distance = -1.0f; // in cm
if (hcsr04_dev != NULL)
{
// Trigger measure
gpio_pin_set(hcsr04_dev, HCSR04_TRIG_PIN, 1);
k_sleep(K_MSEC(10));
gpio_pin_set(hcsr04_dev, HCSR04_TRIG_PIN, 0);
do {
val = gpio_pin_get(hcsr04_dev, HCSR04_ECHO_PIN);
} while (val == 0);
cycle_start = k_cycle_get_32();
// Wait for echo response
do {
val = gpio_pin_get(hcsr04_dev, HCSR04_ECHO_PIN);
cycle_stop = k_cycle_get_32();
cycle_diff = cycle_stop - cycle_start;
// 260cm for 84MHz (((MAX_RANGE * 58000) / 1000000000) * (CLOCK * 1000000))
if (cycle_diff > 1266720)
break;
} while (val == 1);
// Calculate distance based on cycles
measure_time = k_cyc_to_ns_floor64(cycle_diff);
distance = measure_time / 58000;
printk("Sensor D: %d [cm]\n", distance);
}
return distance;
}
3.2.7 BME680 sensor code
The code implemented for the temperature and humidity sensor (BME680) is taken from the "zigbee weather station" example for Thingy:53.
The configuration is done by enabling the BME680 sensor, this can be done in 2 ways:
1) Through the Kconfig menu enabling the options:
Device Drivers -> I2C Drivers
Device Drivers -> Sensor Drivers -> BME680 sensor
2) or manually modifying the prj.conf
file of the project and adding the following lines:
# Sensors
CONFIG_I2C=y
CONFIG_SENSOR=y
CONFIG_BME680=y
The device tree overlay (boards/thingy53_nrf5340_cpuapp.overlay
) must be modified to add the sensor:
&i2c1 {
status = "okay";
bme688@76 {
compatible = "bosch,bme680";
label = "BME688";
reg = <0x76>;
status = "okay";
};
};
The initialization is done by retrieving the i2c device we added previously in the device tree:
static const struct device *bme680;
int bme680_init(void)
{
int err = 0;
if (bme680) {
printk ("Sensor already initialized\n");
} else {
bme680 = DEVICE_DT_GET(DT_INST(0, bosch_bme680));
if (!bme680) {
printk ("Failed to get device\n");
err = ENODEV;
}
printk("BME680 Configured!\n");
}
return err;
}
The measurement is done in 2 steps, the first one is triggering the device to actually measure the environment:
int bme680_update_measurements(void)
{
int err = 0;
if (bme680) {
err = sensor_sample_fetch(bme680);
} else {
printk ("Sensor not initialized\n");
err = ENODEV;
}
return err;
}
The second step is done by getting the sensor value we want, temperature or humidity, separated in integer and decimal parts (only temperature code is shown, the humidity value is equivalent):
int bme680_get_temperature(float *temperature)
{
int err = 0;
/* Check for NULL pointers */
if (temperature) {
if (bme680) {
/* Obtain the sensor value */
struct sensor_value sensor_temperature;
err = sensor_channel_get(bme680,
SENSOR_CHAN_AMBIENT_TEMP,
&sensor_temperature);
if (err) {
printk ("Failed to get sensor channel: %d\n", err);
} else {
/* Convert the value to float */
printk ("Sensor T:%3d.%06d [*C]\n",
sensor_temperature.val1, sensor_temperature.val2);
*temperature = convert_sensor_value(sensor_temperature);
}
} else {
printk ("Sensor not initialized\n");
err = ENODEV;
}
} else {
printk ("NULL param\n");
err = EINVAL;
}
return err;
}
and converting them to a float value to easily work with them:
static float convert_sensor_value(struct sensor_value value)
{
float result = 0.0f;
/* Determine sign */
result = (value.val1 < 0 || value.val2 < 0) ? -1.0f : 1.0f;
/* Use absolute values */
value.val1 = value.val1 < 0 ? -value.val1 : value.val1;
value.val2 = value.val2 < 0 ? -value.val2 : value.val2;
/* Calculate value */
result *= (value.val1 + value.val2 / (float)SENSOR_VAL2_DIVISOR);
return result;
}
3.2.8 Bluetooth communications code
The bluetooth interface is used to publicate the sensor data and the alerts status throught GATT services. For this device two services are advertised, one for the sensor data and one for alerts status.
First of all, we need to define the UUID with which the services will be advertsed, this is set of 128-bit ids identifying each service and characteristic provided by the device.
/** @brief SVC Service DATA UUID. */
#define BT_UUID_SVC_DATA_VAL \
BT_UUID_128_ENCODE(0x00010000, 0xd7fa, 0x42d3, 0xb486, 0x962db285a927)
#define BT_UUID_SVC_DATA_FILLED_VAL \
BT_UUID_128_ENCODE(0x00010001, 0xd7fa, 0x42d3, 0xb486, 0x962db285a927)
#define BT_UUID_SVC_DATA_HUMIDITY_VAL
BT_UUID_128_ENCODE(0x00010002, 0xd7fa, 0x42d3, 0xb486, 0x962db285a927)
#define BT_UUID_SVC_DATA_TEMPERATURE_VAL
BT_UUID_128_ENCODE(0x00010003, 0xd7fa, 0x42d3, 0xb486, 0x962db285a927)
#define BT_UUID_SVC_DATA_POSITION_X_VAL
BT_UUID_128_ENCODE(0x00010004, 0xd7fa, 0x42d3, 0xb486, 0x962db285a927)
#define BT_UUID_SVC_DATA_POSITION_Y_VAL
BT_UUID_128_ENCODE(0x00010005, 0xd7fa, 0x42d3, 0xb486, 0x962db285a927)
#define BT_UUID_SVC_DATA_POSITION_Z_VAL
BT_UUID_128_ENCODE(0x00010006, 0xd7fa, 0x42d3, 0xb486, 0x962db285a927)
#define BT_UUID_SVC_DATA \
BT_UUID_DECLARE_128(BT_UUID_SVC_DATA_VAL)
#define BT_UUID_SVC_DATA_FILLED \
BT_UUID_DECLARE_128(BT_UUID_SVC_DATA_FILLED_VAL)
#define BT_UUID_SVC_DATA_HUMIDITY \
BT_UUID_DECLARE_128(BT_UUID_SVC_DATA_HUMIDITY_VAL)
#define BT_UUID_SVC_DATA_TEMPERATURE \
BT_UUID_DECLARE_128(BT_UUID_SVC_DATA_TEMPERATURE_VAL)
#define BT_UUID_SVC_DATA_POSITION_X \
BT_UUID_DECLARE_128(BT_UUID_SVC_DATA_POSITION_X_VAL)
#define BT_UUID_SVC_DATA_POSITION_Y \
BT_UUID_DECLARE_128(BT_UUID_SVC_DATA_POSITION_Y_VAL)
#define BT_UUID_SVC_DATA_POSITION_Z \
BT_UUID_DECLARE_128(BT_UUID_SVC_DATA_POSITION_Z_VAL)
There are tools to generate random UUIDs like this or this, but some random values would do, avoid using XXXXXXXX-0000-1000-8000-00805F9B34FB as this are reserved for GATT standard attributes.
In this case I randoimly choose 0001XXXX-d7fa-42d3-b486-962db285a927 for Sensor Data service related characteristics and 0x0002XXXX-0xd7fa-0x42d3-0xb486-0x962db285a927 for the Alerts service related characteristics.
The declaracion of each service is published as a primary service and contains a set of characteristics, one for each value shared by the device. Each characteristic is declared with an unique UUID as they are different with read permission.
A reading callback function is assigned to each characteristic to treat any read request received of that characteristic.
/* DATA Service Declaration */
BT_GATT_SERVICE_DEFINE(data_svc,
BT_GATT_PRIMARY_SERVICE(BT_UUID_SVC_DATA),
// Filled
BT_GATT_CHARACTERISTIC(BT_UUID_SVC_DATA_FILLED,
BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY,
BT_GATT_PERM_READ, read_data_filled, NULL, NULL),
// Humidity
BT_GATT_CHARACTERISTIC(BT_UUID_SVC_DATA_HUMIDITY,
BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY,
BT_GATT_PERM_READ, read_data_humidity, NULL, NULL),
// Temperature
BT_GATT_CHARACTERISTIC(BT_UUID_SVC_DATA_TEMPERATURE,
BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY,
BT_GATT_PERM_READ, read_data_temperature, NULL, NULL),
);
Each callback function is called when the bluetooth client requests the data of the characteristic and returns the value sent to the client. The value sent to the client could be data or a string text, in this application text is always sent, but the format depends on each characteristic, for the filled property the percentage is sent, for the humidity and temperature a LOW/MEDIUM/HIGH indication is sent along the current value of humidity (%) and temperature (ºC).
#define STR_LEN 15
const char* LEVEL_HUMIDITY [H_HIGH+1] = {
"ERROR",
"LOW",
"MEDIUM",
"HIGH"
};
char* get_humidity_str (char *str, uint32_t len)
{
snprintk(str, len, "%s (%d %%)",
LEVEL_HUMIDITY [container.humidity],
(uint32_t) container.humidity_val);
}
static ssize_t read_data_humidity(struct bt_conn *conn,
const struct bt_gatt_attr *attr,
void *buf,
uint16_t len,
uint16_t offset)
{
printk ("read_data_humidity called\n");
char str[STR_LEN];
get_humidity_str (str, STR_LEN);
return bt_gatt_attr_read(conn, attr, buf, len, offset, str, strlen(str));
}
4. Bluetooth clientTo test the bluetooth functionality, the nRF Connect mobile app is used. This app allows to scan for bluetooth devices and see their services and properties.
In the next image we can see the Thingy:53 device advertisement, the device name "Garbage Collector IoT", device type, and a list of provided services:
In the next image the device is connected and we can see the data obtained from the device: the container seems to be at 25%, with a humidity of 27% and a temperature of 32ºC, which translated to the application levels both are in medium level.
In the next image we can see the Alerts service, the device has 4 alerts, the Filled Alert is active and we can see the Filled Status is "Full". The other 3 alerts are inactive.
Note: The UUIDs of the services and charactheristics are unkown to the app until editing them and setting a name for them. Once assigned the app recognize these UUIDs and shows the assigned name.
5. Further developmentSome actions for further development and improvements would be:
- Adding more sensors and data collected.
- Adding more communication devices.
- Monitoring more risk situations.
- Using sensor interrupts for warning system instead of polling mechanism.
Some development issues I found and was able to solve during this project were:
- [Solved] HC-SR04 sensor hardware connection setup.
Other issues I found I was not able to solve during this project:
- BMI270 drivers: Default driver implementation is for I2C, an SPI driver would have to be implemented for using this sensor.
I want to thank the team of Nordic Semiconductor for giving me the opportunity to get to know this little board and learn a lot about Zephyr OS project and Nordic's hardware, specially the Thingy:53 prototyping board and its potential. I want to thank the people from Nordic's team that helped me through the devzone and forums that answered my questions on how to work, develop and deploy with the board.
References[2a] https://www.allaboutcircuits.com/tools/voltage-divider-calculator/
[2b] https://ohmslawcalculator.com/voltage-divider-calculator
Comments