Skip to content

II. Smart Trash Bin: Software

pkemp00 edited this page Aug 30, 2024 · 2 revisions

This page provides information about the software of our smart trash bins.

Contents

Tools & Libraries

The entire software was written in C++ using the Arduino IDE (version 2.3.2). This IDE makes developing software for a variety of microcontrollers easier by providing so-called "cores" that contain all tools and basic libraries that are required for a microcontroller family. In our case, the esp32 core (version 3.0.2) by Espressif was used to compile and upload the software. It comes with a predefined profile for the Seeed Studio XIAO ESP32S3 microcontroller board and also provides some board-specific functionality from the ESP IDF.

In addition to the basic functionality of the core, there are two libraries that our software also depends on. The MCCI LoRaWAN LMIC library (version 4.1.1) is used to control the Adafruit RFM95W radio transceiver. It takes care of many details concerning the LoRaWAN protocol, including the timing of messages. To make sure that messages are properly received and sent, the library's programming model uses inversion of control. This means that it heavily relies on callbacks and event handlers. Additionally, a library-specific configuration file has to be copied into a dedicated directory. Further details are described below and in the library's documentation.

The STM32duino VL53L8CX library (version 1.0.4) provides access to the capabilities of the senseBox VL53L8CX time-of-flight sensor. A usage guide is also available for this library which does a good job at explaining its intended operation. At the time of writing, version 2.0.0 of this library was recently made available in the Arduino IDE. Do not attempt to compile the software with this version! It renames many existing functions that are referenced by their old names in the code. We tried to refactor our code to make use of the new version, but this introduced runtime issues while retrieving the measurements from the API.

File Structure

The software's source code is split up into multiple files based on their purpose.

Files Description
settings_template.h A template used to define important values or to alter the software's behavior.
lmic_project_config.h This file has to be copied to the MCCI LoRaWAN LMIC library's project_config directory.
trash_bin.ino This is the sketch's main file that defines a set of jobs.
scheduler.h scheduler.cpp The scheduler manages and executes jobs using the MCCI LoRaWAN LMIC library.
tof_sensor.h tof_sensor.cpp A wrapper for the functionality provided by the STM32duino VL53L8CX library.
battery.h battery.cpp A simple API to measure the LiPo battery's voltage using the voltage divider.
lora_transceiver.h lora_transceiver.cpp A wrapper for the functionality provided by the MCCI LoRaWAN LMIC library.
status.h An enumeration that represents the status of all components.
debug.h A macro that removes marked code if debug mode is disabled.
LICENSE.txt The license that applies to the code in this directory.

Settings

The source code refers to a dedicated header file called settings.h in order to alter its behavior. Due to the sensitive nature of some settings, only the template file settings_template.h was uploaded to the repository. It must be edited and renamed for the software to work properly.

Name Description
SETTINGS_DEVICE_EUI A comma-separated list of eight unsigned bytes. This is the device EUI of the transceiver board which is either provided by the manufacturer or generated by The Things Network.
SETTINGS_APP_KEY A comma-separated list of sixteen unsigned bytes. This is the encryption key which can be generated by and copied from The Things Network.
SETTINGS_SLEEP_TIME An unsigned integer. It specifies how many seconds are spent in deep sleep mode after each measurement cycle to save energy.
SETTINGS_DEBUG_MODE A signed integer. If this value is zero, no debugging output will be printed to the console. Any other value will print debugging messages to the serial monitor.

The MCCI LoRaWAN LMIC library requires a separate configuration file called lmic_project_config.h. The configuration provided in this repository has to be copied into the project_config directory within library's own installation directory.

The Measurement Cycle

The main purpose of our software is to regularly measure a trash bin's fill level and send it to the backend using LoRaWAN. Because this communication protocol defines precise timings, the MCCI LoRaWAN LMIC library requires the programmer to write application code as a set of jobs that are executed on the programmer's behalf by a scheduler. Three jobs are defined in the main file of our software that perform initialization, measurements and data transmission. When these jobs are completed, the microcontroller enters its deep sleep mode.

Each job either passes control to the next job, reschedules itself or aborts the measurement cycle. Which of these actions is taken depends on the status flag of each component that is used by a job. The following diagram gives a broad overview of the measurement cycle. It closely matches the logic described in the trash_bin.ino sketch.

flowchart LR
start([start])
initialize[initialize]
measure[measure]
measure_loop{status?}
transmit[transmit]
transmit_loop{status?}
sleep[sleep]
start --> initialize
initialize --> measure
measure --> measure_loop
measure_loop -- MEASURING --> measure
measure_loop -- SUCCEEDED --> transmit
measure_loop -- FAILED --> sleep
transmit --> transmit_loop
transmit_loop -- TRANSMITTING --> transmit
transmit_loop -- "SUCCEEDED\nor FAILED" --> sleep
sleep -- "SETTINGS_SLEEP_TIME\npassed" --> initialize
Loading

The Scheduler

The jobs in the measurement cycle are managed by a scheduler that is implemented by the MCCI LoRaWAN LMIC library. A simple wrapper API has been designed to hide some of its details in the main sketch. Its variables and functions are defined inside a C++ namespace called Scheduler.

Name Description
gStatus A read-only variable representing the scheduler's status. Its value is either SCHEDULING or SUCCEEDED.
begin() Prepares the scheduler for operation and starts it.
schedule() Runs any jobs that are scheduled for the current time and checks whether there are jobs remaining in the queue.
enqueue(cbjob_t) Schedules the next job. Job function signatures are described in the MCCI LoRaWAN LMIC library documentation.
end() Stops the scheduler, prepares it for deep sleep and finally causes the microcontroller to enter deep sleep.

Internally, this API has two main tasks. First, it maintains a ring buffer of data structures that are used to schedule sequentially executed jobs. Second, it adjusts the duty cycle timings used by the MCCI LoRaWAN LMIC library because they would otherwise be incorrect when the microcontroller wakes up from deep sleep.

The scheduler's state transitions can be described using the following diagram.

flowchart LR
start([start])
scheduling[SCHEDULING]
scheduling_loop{"any\njobs\nleft?"}
succeeded[SUCCEEDED]
sleep([sleep])
start -- begin() --> scheduling
scheduling -- schedule() --> scheduling_loop
scheduling_loop -- yes --> scheduling
scheduling_loop -- no --> succeeded
succeeded -- end() --> sleep
Loading

Component APIs

The hardware components of our sensors are represented in software as a wrapper API. These APIs abstract from the hardware-specific libraries and hide implementation details from the main sketch. Similar to other Arduino APIs, all wrappers define begin() and end() functions as well as other functions that are specific to their task. Furthermore, a global status variable provides information about the current state of each component.

The Battery API

The battery API is used to measure the battery's voltage. The C++ namespace Battery contains the API's variables and functions.

Name Description
gStatus A read-only variable representing the battery's status. Its value is either MEASURING or SUCCEEDED.
gVoltage A read-only variable containing the final voltage measurement in millivolts. Its value is only valid if gStatus is SUCCEEDED.
begin() Initializes the GPIO pin, its analog-to-digital converter and internal variables.
measure() Performs a single voltage measurement. Multiple measurements are required for stable values.
end() Does nothing. It was just added for the sake of API consistency.

Initially, the GPIO pin of the voltage divider is configured for analog readings with appropriate bit resolution and attenuation. When a voltage is measured, it is added to a grand total while a counter keeps track of the number of completed measurements. When 15 measurements were made, the total is divided by the counter to receive the final measurement as the average of all measurements. The number 15 was mostly an arbitrary choice. Any number that assures the final measurement's stability will suffice here. No more measurements will be taken once a final result has been computed.

The battery API's state transitions can be described using the following diagram.

flowchart LR
start([start])
measuring[MEASURING]
measuring_loop{"enough\nmeasure-\nments?"}
succeeded[SUCCEEDED]
finish([finish])
start -- begin() --> measuring
measuring -- measure() --> measuring_loop
measuring_loop -- no --> measuring
measuring_loop -- yes --> succeeded
succeeded -- end() --> finish
Loading

The ToF Sensor API

The ToF sensor API is used to measure the distance between the bottom of the box and any trash inside the bin. The C++ namespace TofSensor contains the API's variables and functions.

Name Description
gStatus A read-only variable representing the sensor's status. Its value is either MEASURING, SUCCEEDED or FAILED.
gDistance A read-only variable containing the final distance measurement in millimeters. Its value is only valid if gStatus is SUCCEEDED.
begin() Connects to the ToF sensor via I2C, configures it and initiates distance measurements.
measure() Checks if distances are available. If they are, a weighted sum is computed as the final measurement.
end() Stops any ongoing measurements and puts the sensor into sleep mode.

Before the ToF sensor can be used, an I2C connection has to be established. This allows the microcontroller to configure details like 8x8 and 4x4 resolution, measurement frequency and the pulse length used to acquire distances. The usage guide provides descriptions and recommendations for these settings.

When the sensor starts ranging, it takes some time for measurements to arrive. Each measurement is accompanied by a status flag that indicates how confident the sensor is about a measured distance. Based on these confidence levels, a weighted sum is computed as the final result. The status indicates whether the sum or its weights were invalid, for example if no measurements had an acceptable status flag. Finally, the sensor can be put into sleep mode to reduce its energy consumption.

The ToF sensor API's state transitions can be described using the following diagram.

flowchart LR
start([start])
measuring[MEASURING]
measuring_loop{"data\nready?"}
measuring_check{"valid\nresult?"}
succeeded[SUCCEEDED]
failed[FAILED]
finish([finish])
start -- begin() --> measuring
measuring -- measure() --> measuring_loop
measuring_loop -- no --> measuring
measuring_loop -- yes --> measuring_check
measuring_check -- yes --> succeeded
measuring_check -- no --> failed
succeeded -- end() --> finish
failed -- end() --> finish
Loading

The LoRa Transceiver API

The LoRa transceiver API is used to send the measured voltage and distance to our backend using LoRaWAN. It relies on the battery and ToF sensor API to provide measurements and pack them into a message. The C++ namespace LoraTransceiver contains the API's variables and functions.

Name Description
gStatus A read-only variable representing the transceiver's status. Its value is either TRANSMITTING, SUCCEEDED or FAILED.
begin() Signals the beginning of a transmission.
transmit() Checks if the transceiver is ready to transmit. If it is, a single attempt to send the message is made.
end() Does nothing. It was just added for the sake of API consistency.

Before anything can be transmitted or received by the LoRa transceiver, a connection to a LoRa gateway has to be established. The gateway will then forward messages to a service provider that manages incoming and outgoing messages on behalf of edge devices and application servers. This process is also known as joining the network. Once a device has joined, it can transmit and receive messages. The MCCI LoRaWAN LMIC library defines a set of events that occur at certain points in the LoRaWAN protocol. For our sensors, the most important ones are listed in the following table.

Event Description
EV_JOINED The device has successfully joined the network.
EV_JOIN_FAILED An error occurred while joining the network.
EV_JOIN_TXCOMPLETE The device will repeatedly try to join the network in cycles. This event indicates that one cycle was completed.
EV_TXCOMPLETE The device has successfully transmitted a message.

It is the API's job to react to these events appropriately. State transitions are based on events and function calls which are described in the following diagram.

flowchart LR
start([start])
transmitting[TRANSMITTING]
joining_loop{"joined?"}
transmitting_loop{"trans-\nmitted?"}
succeeded[SUCCEEDED]
failed[FAILED]
finish([finish])
start -- begin() --> transmitting
transmitting -- transmit() --> joining_loop
joining_loop -- EV_JOINED --> transmitting_loop
joining_loop -- no --> transmitting
joining_loop -- "EV_JOIN_FAILED or\nEV_JOIN_TXCOMPLETE" --> failed
transmitting_loop -- EV_TXCOMPLETE --> succeeded
transmitting_loop -- no --> transmitting
succeeded -- end() --> finish
failed -- end() --> finish
Loading

One design decision that may be surprising is that EV_JOIN_TXCOMPLETE leads to the FAILED status. This was done to prevent the sensor from entering a loop that keeps it powered on for an extended period of time. If it is not able to connect within one join cycle, it should enter deep sleep again to save the battery's power. Sending messages via LoRa is probably the most energy-intensive task of our sensors, so we want to keep this activity at a minimum.

If a connection was successfully established, measurements can be sent to a gateway. Since the payload size of LoRa messages is restricted, both voltage and distance are sent in a byte array with the following format.

Byte Offset Description
0 least significant byte of voltage in millivolts
1 most significant byte of voltage in millivolts
2 least significant byte of distance in millimeters
3 most significant byte of distance in millimeters

Measurements are stored as 16-bit unsigned integers, so they occupy more than one byte. The endianness of a processor's architecture determines in which order the bytes of a multi-byte integer are stored. To avoid relying on architecture-specific endianness, we manually insert each byte of both measurements in little endian order. Thus, a 16-bit integer is split into its least significant byte followed by its most significant byte.