Skip to main content

Implementation

Requirements

The following libraries are needed for the plant monitoring system:

  • AVR-IoT-Cellular
  • AVR-IoT MCP9808
  • AVR-IoT VEML3328
  • Adafruit Seesaw Library
  • Adafruit AHTX0
  • Adafruit VEML7700
  • ArduinoJson

Code

info

You can find the whole Arduino Sketch in the examples for the AVR-IoT Cellular Library. In the Arduino IDE, navigate to File -> Examples -> AVR-IoT Cellular -> plant_monitoring to follow along with this outline.

Setup

The plant monitoring system requires a unique identifier for the device, such that it can be differentiated from other devices within your AWS.

#define DEVICE_ID "<name your plant monitoring system here>"  

The plant monitoring sketch includes options for low power with preprocessor directives for power save mode and power down mode. Power save mode is preferred if your operator supports it (the board will notify when attempting to configure power save if this is the case) as it does not disconnect the board completely from the network. The board thus stays connected to the operator and does not have to use a lot of power to re-establish the connection. The power save mode will also save data as the MQTT connection will not terminate.

// Define whether you want to use power save (not available on 
// all operators), power down (shuts down the modem completely)
// or no low power mode at all (both commented out).
//
// #define USE_POWER_SAVE
// #define USE_POWER_DOWN

To set up the sensors in the plant monitoring system, a convenience function is declared in the sketch. It sets up the Seesaw, which retrieves the moisture of the soil, the AHTX0, which retrieves humidity and temperature and the VEML7700, which retrieves the illumination. Note that the onboard sensor for temperature and illumination (MCP9808 and VEML3328) are not used, they are explicitly shut down to save power in the whole system.


/**
* @brief Initializes the sensors used by the plant monitoring example. Will
* also shutdown the onboard sensors to save power.
*
* @return true If all sensors were successfully initialized.
*/
bool setupSensors() {
if (!seesaw.begin(0x36)) {
Log.error(F("Adafruit seesaw not found."));
return false;
}

if (!aht.begin(&Wire1)) {
Log.error(F("Adafruit AHT not found."));
return false;
}

if (!veml.begin(&Wire1)) {
Log.error(F("Adafruit VEML7700 not found."));
return false;
}

// We want to shutdown the onboard sensors to save power as they are not
// used
if (Mcp9808.begin()) {
Log.error(F("Could not start MCP9808."));
return false;
}

Mcp9808.shutdown();

if (Veml3328.begin()) {
Log.error(F("Could not start VEML3328."));
return false;
}

Veml3328.shutdown();

return true;
}

The setup function sets up the LED controller and the logging module before setting up the sensors. Based on the preprocessor directives for low power, the respective methods are configured.

void setup() {
LedCtrl.begin();
LedCtrl.startupCycle();

Log.begin(115200);
Log.info(F("Starting up plant monitoring example\r\n"));

if (!setupSensors()) {
while (1) {}
}

#if defined(USE_POWER_SAVE) && !defined(USE_POWER_DOWN)
// Configure power save. Here we set to sleep for 30 minutes
LowPower.configurePeriodicPowerSave(PowerSaveModePeriodMultiplier::ONE_MINUTE, 1);
#elif !defined(USE_POWER_SAVE) && defined(USE_POWER_DOWN)
// Configure power down. Note that here we don't need to preconfigure the
// time to power down
LowPower.configurePowerDown();
#elif defined(USE_POWER_SAVE) && defined(USE_POWER_DOWN)
#error "Cannot use both power save and power down at the same time"
#endif
}

Main loop

The main loop of the plant monitoring system consists of four stages:

  1. Connect to the operator and AWS if the board is not already connected
  2. Collecting data.
  3. Publishing data to AWS.
  4. Optionally entering low power.

Connect to the operator and AWS

The first step in the main loop is to connect to the network and to AWS, but only if we are not already connected. In this way, if a network disconnection occurs, we can re-connect again.


void loop() {

// Check first if we are connected to the operator. We might get
// disconnected, so for our application to continue to run, we double check
// here
if (!Lte.isConnected()) {

// Attempt to connect to the operator
if (!Lte.begin()) {
return;
}
}

if (Lte.isConnected()) {
// Check if we are connected to the cloud. If not, attempt to connect
if (!MqttClient.isConnected()) {
if (!MqttClient.beginAWS()) {
Log.error(F("Failed to connect to AWS."));
return;
}
}
}

// ...

}

Collecting data

The convenience function retrieveData collects air and soil data and JSON encodes it. It also retrieves the board's voltage which can be used to monitor when one should recharge the battery of the system.

Note that the function uses an out parameter in the form of data. This is where the encoded JSON data is placed. It also returns the number of bytes written to data such that we can guard ourselves against a buffer overflow error.

/**
* @brief Collects data, JSON encodes it and places it in the
* @p data buffer.
*
* @param data [out] Buffer to place the JSON encoded data in.
* @param data_capacity [in] The capacity of the data buffer.
*
* @return The number of bytes written to the @p data buffer.
*/
size_t retrieveData(char* data, const size_t data_capacity) {
// Get air metrics
JsonDocument air_data;
sensors_event_t humidity, temperature;
aht.getEvent(&humidity, &temperature);

air_data["Temperature"] = temperature.temperature;
air_data["Humidity"] = humidity.relative_humidity;
air_data["Illumination"] = veml.readLux();

// Get soil metrics
JsonDocument soil_data;
const float moisture_level = 100 * ((float)seesaw.touchRead(0) / 1023);
soil_data["Moisture"] = moisture_level;

// Get device metrics
JsonDocument device_data;
device_data["SupplyVoltage"] = LowPower.getSupplyVoltage();

// Build data payload
JsonDocument payload;
payload["Device_ID"] = DEVICE_ID;
payload["Air"] = air_data;
payload["Soil"] = soil_data;
payload["Board"] = device_data;

return serializeJson(payload, data, data_capacity);
}

Publishing data

Publishing the data is done with MqttClient.publish. In the main loop, the data is first retrieved using the retrieveData function and then published.


void loop() {

// ...

// Data is declared static here so that it appears in RAM
// usage during compilation. In that way, we have a better
// estimate of how much RAM is used.
static char data[512] = "";

if (retrieveData(data, sizeof(data)) > sizeof(data)) {
Log.error(F("Data buffer too small."));
while (1) {}
}

Log.infof(F("Publishing data: %s\r\n"), data);
if (!MqttClient.publish(AWS_PUB_TOPIC, data)) {
Log.warn(F("Failed to publish data"));
}

// ...
}

Entering low power

If the preprocessor directives for low power is defined, we can enter low power.

void loop() {

// ...

#if defined(USE_POWER_SAVE) && !defined(USE_POWER_DOWN)
Log.info(F("Power saving..."));
LowPower.powerSave();
#elif !defined(USE_POWER_SAVE) && defined(USE_POWER_DOWN)
Log.info(F("Powering down..."));
// Power down for 1 minute
LowPower.powerDown(60);
#elif defined(USE_POWER_SAVE) && defined(USE_POWER_DOWN)
#error "Cannot use both power save and power down at the same time"
#endif
}

Final remarks

To establish the connection between the AVR-IoT Cellular Board and AWS, it is necessary to follow the AWS Cloud Provisioning guide.

Arduino Upload Bug

If you are using Arduino IDE 2, there is a bug that doesn't allow you to use the Upload button to program your device.

Instead, you will need to go to Sketch -> Upload using Programmer and let the magic happen.