ModularSensors > Examples > data_saving.ino

data_saving.ino example

Data Saving Example

This is another double logger example, but in this case, both loggers are going at the same interval and the only difference between the loggers is the list of variables. There are two sets of variables, all coming from Yosemitech sensors. Because each sensor outputs temperature and we don't want to waste cellular data sending out multiple nearly identical temperature values, we have one logger that logs every possible variable result to the SD card and another logger that sends only unique results to the EnviroDIY data portal.

The modem used in this example is a SIM800 based Sodaq GPRSBee r6.

The sensors used in this example are Yosemitech Y504 Dissolved Oxygen Sensor, Yosemitech Y511 Turbidity Sensor with Wiper, Yosemitech Y514 Chlorophyll Sensor, and Yosemitech Y520 Conductivity Sensor.

Unique Features of the Data Saving Example

  • Uses AltSoftSerial to create an additional serial port for RS485 communication.
  • All variables are created and named with their parent sensor (as opposed to being created within the variable array).
  • Two different variable arrays and loggers are created and used.
    • Many of the same variables are used in both arrays.
    • Only one of the loggers publishes data.
  • The loop function is expanded into its components rather than using the logData functions.
    • This demonstrates how to write the loop out, without using the logData functions.
    • It also shows how to forcibly set serial pins LOW at the start and end of the loop in order to prevent power loss through an RS485 adapter.

To Use this Example:

Prepare and set up PlatformIO

  • Register a site and sensors at the Monitor My Watershed/EnviroDIY data portal (
  • Create a new PlatformIO project
  • Replace the contents of the platformio.ini for your new project with the platformio.ini file in the examples/data_saving folder on GitHub.
    • It is important that your PlatformIO configuration has the lib_ldf_mode and build flags set as they are in the example.
    • Without this, the program won't compile.
  • Open data_saving.ino and save it to your computer.
    • After opening the link, you should be able to right click anywhere on the page and select "Save Page As".
    • Move it into the src directory of your project.
    • Delete main.cpp in that folder.

Set the logger ID

  • Change the "XXXX" in this section of code to the loggerID assigned by Stroud:
// Logger ID, also becomes the prefix for the name of the data file on SD card
const char *LoggerID = "XXXX";

Set the universally universal identifiers (UUID) for each variable

  • Go back to the web page for your site at the Monitor My Watershed/EnviroDIY data portal (
  • For each variable, find the dummy UUID ("12345678-abcd-1234-ef00-1234567890ab") and replace it with the real UUID for the variable.


  • Test everything at home before deploying out in the wild!

PlatformIO Configuration

; PlatformIO Project Configuration File
;   Build options: build flags, source filter
;   Upload options: custom upload port, speed and extra flags
;   Library options: dependencies, extra library storages
;   Advanced options: extra scripting
; Please visit documentation for the other options and examples

description = ModularSensors example using two "loggers" to save cellular data

monitor_speed = 115200
board = mayfly
platform = atmelavr
framework = arduino
lib_ldf_mode = deep+
lib_ignore =
    Adafruit NeoPixel
    Adafruit GFX Library
    Adafruit SSD1306
    Adafruit ADXL343
    Adafruit STMPE610
    Adafruit TouchScreen
    Adafruit ILI9341
build_flags =
lib_deps =
;  ^^ Use this when working from an official release of the library
;  ^^ Use this when if you want to pull from the develop branch

The Complete Code

/** =========================================================================
 * @file data_saving.ino
 * @brief Example publishing only a portion of the logged variables.
 * @author Sara Geleskie Damiano <>
 * @copyright (c) 2017-2020 Stroud Water Research Center (SWRC)
 *                          and the EnviroDIY Development Team
 *            This example is published under the BSD-3 license.
 * Build Environment: Visual Studios Code with PlatformIO
 * Hardware Platform: EnviroDIY Mayfly Arduino Datalogger
 * ======================================================================= */

// ==========================================================================
//  Defines for the Arduino IDE
//  NOTE:  These are ONLY needed to compile with the Arduino IDE.
//         If you use PlatformIO, you should set these build flags in your
//         platformio.ini
// ==========================================================================
/** Start [defines] */
/** End [defines] */

// ==========================================================================
//  Include the libraries required for any data logger
// ==========================================================================
/** Start [includes] */
// The Arduino library is needed for every Arduino program.
#include <Arduino.h>

// EnableInterrupt is used by ModularSensors for external and pin change
// interrupts and must be explicitly included in the main program.
#include <EnableInterrupt.h>

// Include the main header for ModularSensors
#include <ModularSensors.h>
/** End [includes] */

// ==========================================================================
//  Settings for Additional Serial Ports
// ==========================================================================
/** Start [serial_ports] */
// The modem and a number of sensors communicate over UART/TTL - often called
// "serial". "Hardware" serial ports (automatically controlled by the MCU) are
// generally the most accurate and should be configured and used for as many
// peripherals as possible.  In some cases (ie, modbus communication) many
// sensors can share the same serial port.

#if not defined ARDUINO_ARCH_SAMD && not defined ATMEGA2560  // For AVR boards
// Unfortunately, most AVR boards have only one or two hardware serial ports,
// so we'll set up three types of extra software serial ports to use

// AltSoftSerial by Paul Stoffregen
// ( is the most accurate
// software serial port for AVR boards. AltSoftSerial can only be used on one
// set of pins on each board so only one AltSoftSerial port can be used. Not all
// AVR boards are supported by AltSoftSerial.
#include <AltSoftSerial.h>
AltSoftSerial altSoftSerial;
#endif  // End software serial for avr boards

#include <wiring_private.h>  // Needed for SAMD pinPeripheral() function

// Set up a 'new' UART using SERCOM1
// The Rx will be on digital pin 11, which is SERCOM1's Pad #0
// The Tx will be on digital pin 10, which is SERCOM1's Pad #2
// NOTE:  SERCOM1 is undefinied on a "standard" Arduino Zero and many clones,
//        but not all!  Please check the variant.cpp file for you individual
//        board! Sodaq Autonomo's and Sodaq One's do NOT follow the 'standard'
//        SERCOM definitions!
Uart Serial2(&sercom1, 11, 10, SERCOM_RX_PAD_0, UART_TX_PAD_2);
// Hand over the interrupts to the sercom port
void SERCOM1_Handler() {

#endif  // End hardware serial on SAMD21 boards
/** End [serial_ports] */

// ==========================================================================
//  Data Logging Options
// ==========================================================================
/** Start [logging_options] */
// The name of this program file
const char* sketchName = "data_saving.ino";
// Logger ID, also becomes the prefix for the name of the data file on SD card
const char* LoggerID = "XXXXX";
// How frequently (in minutes) to log data
const uint8_t loggingInterval = 5;
// Your logger's timezone.
const int8_t timeZone = -5;  // Eastern Standard Time
// NOTE:  Daylight savings time will not be applied!  Please use standard time!

// Set the input and output pins for the logger
// NOTE:  Use -1 for pins that do not apply
const int32_t serialBaud = 115200;  // Baud rate for debugging
const int8_t  greenLED   = 8;       // Pin for the green LED
const int8_t  redLED     = 9;       // Pin for the red LED
const int8_t  buttonPin  = 21;      // Pin for debugging mode (ie, button pin)
const int8_t  wakePin    = 31;  // MCU interrupt/alarm pin to wake from sleep
// Mayfly 0.x D31 = A7
// Set the wake pin to -1 if you do not want the main processor to sleep.
// In a SAMD system where you are using the built-in rtc, set wakePin to 1
const int8_t sdCardPwrPin   = -1;  // MCU SD card power pin
const int8_t sdCardSSPin    = 12;  // SD card chip select/slave select pin
const int8_t sensorPowerPin = 22;  // MCU pin controlling main sensor power
/** End [logging_options] */

// ==========================================================================
//  Wifi/Cellular Modem Options
// ==========================================================================
/** Start [gprsbee] */
// For the Sodaq 2GBee R6 and R7 based on the SIMCom SIM800
// NOTE:  The Sodaq GPRSBee doesn't expose the SIM800's reset pin
#include <modems/Sodaq2GBeeR6.h>
// Create a reference to the serial port for the modem
HardwareSerial& modemSerial = Serial1;  // Use hardware serial if possible
const int32_t   modemBaud   = 9600;     //  SIM800 does auto-bauding by default

// Modem Pins - Describe the physical pin connection of your modem to your board
// NOTE:  Use -1 for pins that do not apply
const int8_t modemVccPin    = 23;      // MCU pin controlling modem power
const int8_t modemStatusPin = 19;      // MCU pin used to read modem status
const int8_t modemLEDPin    = redLED;  // MCU pin connected an LED to show modem
                                       // status (-1 if unconnected)

// Network connection information
const char* apn = "xxxxx";  // The APN for the gprs connection

Sodaq2GBeeR6 modem2GB(&modemSerial, modemVccPin, modemStatusPin, apn);
// Create an extra reference to the modem by a generic name
Sodaq2GBeeR6 modem = modem2GB;

// Create RSSI and signal strength variable pointers for the modem
Variable* modemRSSI = new Modem_RSSI(&modem,
Variable* modemSignalPct =
    new Modem_SignalPercent(&modem, "12345678-abcd-1234-ef00-1234567890ab");
/** End [gprsbee] */

// ==========================================================================
//  Using the Processor as a Sensor
// ==========================================================================
/** Start [processor_sensor] */
#include <sensors/ProcessorStats.h>

// Create the main processor chip "sensor" - for general metadata
const char*    mcuBoardVersion = "v0.5b";
ProcessorStats mcuBoard(mcuBoardVersion);

// Create sample number, battery voltage, and free RAM variable pointers for the
// processor
Variable* mcuBoardBatt = new ProcessorStats_Battery(
    &mcuBoard, "12345678-abcd-1234-ef00-1234567890ab");
Variable* mcuBoardAvailableRAM = new ProcessorStats_FreeRam(
    &mcuBoard, "12345678-abcd-1234-ef00-1234567890ab");
Variable* mcuBoardSampNo = new ProcessorStats_SampleNumber(
    &mcuBoard, "12345678-abcd-1234-ef00-1234567890ab");
/** End [processor_sensor] */

// ==========================================================================
//  Maxim DS3231 RTC (Real Time Clock)
// ==========================================================================
/** Start [ds3231] */
#include <sensors/MaximDS3231.h>

// Create a DS3231 sensor object
MaximDS3231 ds3231(1);

// Create a temperature variable pointer for the DS3231
Variable* ds3231Temp =
    new MaximDS3231_Temp(&ds3231, "12345678-abcd-1234-ef00-1234567890ab");
/** End [ds3231] */

// ==========================================================================
//  Settings shared between Modbus sensors
// ==========================================================================
/** Start [modbus_shared] */
// Create a reference to the serial port for modbus
#if defined ARDUINO_ARCH_SAMD || defined ATMEGA2560
HardwareSerial& modbusSerial = Serial2;  // Use hardware serial if possible
AltSoftSerial& modbusSerial = altSoftSerial;  // For software serial

// Define some pins that will be shared by all modbus sensors
const int8_t rs485AdapterPower =
    sensorPowerPin;  // RS485 adapter power pin (-1 if unconnected)
const int8_t modbusSensorPower = A3;  // Sensor power pin
const int8_t rs485EnablePin = -1;  // Adapter RE/DE pin (-1 if not applicable)
/** End [modbus_shared] */

// ==========================================================================
//  Yosemitech Y504 Dissolved Oxygen Sensor
// ==========================================================================
/** Start [Y504] */
#include <sensors/YosemitechY504.h>

byte          y504ModbusAddress  = 0x04;  // The modbus address of the Y504
const uint8_t y504NumberReadings = 5;
// The manufacturer recommends averaging 10 readings, but we take 5 to minimize
// power consumption

// Create a Yosemitech Y504 dissolved oxygen sensor object
YosemitechY504 y504(y504ModbusAddress, modbusSerial, rs485AdapterPower,
                    modbusSensorPower, rs485EnablePin, y504NumberReadings);

// Create the dissolved oxygen percent, dissolved oxygen concentration, and
// temperature variable pointers for the Y504
Variable* y504DOpct =
    new YosemitechY504_DOpct(&y504, "12345678-abcd-1234-ef00-1234567890ab");
Variable* y504DOmgL =
    new YosemitechY504_DOmgL(&y504, "12345678-abcd-1234-ef00-1234567890ab");
Variable* y504Temp =
    new YosemitechY504_Temp(&y504, "12345678-abcd-1234-ef00-1234567890ab");
/** End [Y504] */

// ==========================================================================
//  Yosemitech Y511 Turbidity Sensor with Wiper
// ==========================================================================
/** Start [Y511] */
#include <sensors/YosemitechY511.h>

byte          y511ModbusAddress  = 0x1A;  // The modbus address of the Y511
const uint8_t y511NumberReadings = 5;
// The manufacturer recommends averaging 10 readings, but we take 5 to minimize
// power consumption

// Create a Y511-A Turbidity sensor object
YosemitechY511 y511(y511ModbusAddress, modbusSerial, rs485AdapterPower,
                    modbusSensorPower, rs485EnablePin, y511NumberReadings);

// Create turbidity and temperature variable pointers for the Y511
Variable* y511Turb =
    new YosemitechY511_Turbidity(&y511, "12345678-abcd-1234-ef00-1234567890ab");
Variable* y511Temp =
    new YosemitechY511_Temp(&y511, "12345678-abcd-1234-ef00-1234567890ab");
/** End [Y511] */

// ==========================================================================
//  Yosemitech Y514 Chlorophyll Sensor
// ==========================================================================
/** Start [Y514] */
#include <sensors/YosemitechY514.h>

byte          y514ModbusAddress  = 0x14;  // The modbus address of the Y514
const uint8_t y514NumberReadings = 5;
// The manufacturer recommends averaging 10 readings, but we take 5 to
// minimize power consumption

// Create a Y514 chlorophyll sensor object
YosemitechY514 y514(y514ModbusAddress, modbusSerial, rs485AdapterPower,
                    modbusSensorPower, rs485EnablePin, y514NumberReadings);

// Create chlorophyll concentration and temperature variable pointers for the
// Y514
Variable* y514Chloro = new YosemitechY514_Chlorophyll(
    &y514, "12345678-abcd-1234-ef00-1234567890ab");
Variable* y514Temp =
    new YosemitechY514_Temp(&y514, "12345678-abcd-1234-ef00-1234567890ab");
/** End [Y514] */

// ==========================================================================
//  Yosemitech Y520 Conductivity Sensor
// ==========================================================================
/** Start [Y520] */
#include <sensors/YosemitechY520.h>

byte          y520ModbusAddress  = 0x20;  // The modbus address of the Y520
const uint8_t y520NumberReadings = 5;
// The manufacturer recommends averaging 10 readings, but we take 5 to minimize
// power consumption

// Create a Y520 conductivity sensor object
YosemitechY520 y520(y520ModbusAddress, modbusSerial, rs485AdapterPower,
                    modbusSensorPower, rs485EnablePin, y520NumberReadings);

// Create specific conductance and temperature variable pointers for the Y520
Variable* y520Cond =
    new YosemitechY520_Cond(&y520, "12345678-abcd-1234-ef00-1234567890ab");
Variable* y520Temp =
    new YosemitechY520_Temp(&y520, "12345678-abcd-1234-ef00-1234567890ab");
/** End [Y520] */

// ==========================================================================
//  Creating the Variable Array[s] and Filling with Variable Objects
// ==========================================================================
/** Start [variable_arrays] */
// FORM2: Fill array with already created and named variable pointers
// We put ALL of the variable pointers into the first array
Variable* variableList_complete[] = {
    mcuBoardSampNo, mcuBoardBatt, mcuBoardAvailableRAM,
    ds3231Temp,     y504DOpct,    y504DOmgL,
    y504Temp,       y511Turb,     y511Temp,
    y514Chloro,     y514Temp,     y520Cond,
    y520Temp,       modemRSSI,    modemSignalPct};
// Count up the number of pointers in the array
int variableCount_complete = sizeof(variableList_complete) /
// Create the VariableArray object
VariableArray arrayComplete(variableCount_complete, variableList_complete);

// Put only the particularly interesting variables into a second array
// NOTE:  We can the same variables into multiple arrays
Variable* variableList_toGo[] = {y504DOmgL,  y504Temp, y511Turb,
                                 y514Chloro, y520Cond, modemRSSI};
// Count up the number of pointers in the array
int variableCount_toGo = sizeof(variableList_toGo) /
// Create the VariableArray object
VariableArray arrayToGo(variableCount_toGo, variableList_toGo);
/** End [variable_arrays] */

// ==========================================================================
//  The Logger Object[s]
// ==========================================================================
/** Start [loggers] */
// Create one new logger instance for the complete array
Logger loggerAllVars(LoggerID, loggingInterval, &arrayComplete);

// Create "another" logger for the variables to go out over the internet
Logger loggerToGo(LoggerID, loggingInterval, &arrayToGo);
/** End [loggers] */

// ==========================================================================
//  Creating Data Publisher[s]
// ==========================================================================
/** Start [publishers] */
// Create a publisher to Monitor My Watershed / EnviroDIY Data Sharing Portal
// Device registration and sampling feature information can be obtained after
// registration at or
const char* registrationToken =
    "12345678-abcd-1234-ef00-1234567890ab";  // Device registration token
const char* samplingFeature =
    "12345678-abcd-1234-ef00-1234567890ab";  // Sampling feature UUID

// Create a data publisher for the Monitor My Watershed/EnviroDIY POST endpoint
// This is only attached to the logger with the shorter variable array
#include <publishers/EnviroDIYPublisher.h>
EnviroDIYPublisher EnviroDIYPOST(loggerToGo, &modem.gsmClient,
                                 registrationToken, samplingFeature);
/** End [publishers] */

// ==========================================================================
//  Working Functions
// ==========================================================================
/** Start [working_functions] */
// Flashes the LED's on the primary board
void greenredflash(uint8_t numFlash = 4, uint8_t rate = 75) {
    for (uint8_t i = 0; i < numFlash; i++) {
        digitalWrite(greenLED, HIGH);
        digitalWrite(redLED, LOW);
        digitalWrite(greenLED, LOW);
        digitalWrite(redLED, HIGH);
    digitalWrite(redLED, LOW);

// Reads the battery voltage
// NOTE: This will actually return the battery level from the previous update!
float getBatteryVoltage() {
    if (mcuBoard.sensorValues[0] == -9999) mcuBoard.update();
    return mcuBoard.sensorValues[0];
/** End [working_functions] */

// ==========================================================================
//  Arduino Setup Function
// ==========================================================================
/** Start [setup] */
void setup() {
// Wait for USB connection to be established by PC
// NOTE:  Only use this when debugging - if not connected to a PC, this
// could prevent the script from starting
    while (!SERIAL_PORT_USBVIRTUAL && (millis() < 10000)) {}

    // Start the primary serial connection

    // Print a start-up note to the first serial port
    Serial.print(F("Now running "));
    Serial.print(F(" on Logger "));

    Serial.print(F("Using ModularSensors Library version "));

    // Start the serial connection with the modem

    // Start the stream for the modbus sensors; all currently supported modbus
    // sensors use 9600 baud

// Assign pins SERCOM functionality for SAMD boards
// NOTE:  This must happen *after* the various serial.begin statements
    pinPeripheral(10, PIO_SERCOM);  // Serial2 Tx/Dout = SERCOM1 Pad #2
    pinPeripheral(11, PIO_SERCOM);  // Serial2 Rx/Din = SERCOM1 Pad #0
    // Set up pins for the LED's
    pinMode(greenLED, OUTPUT);
    digitalWrite(greenLED, LOW);
    pinMode(redLED, OUTPUT);
    digitalWrite(redLED, LOW);
    // Blink the LEDs to show the board is on and starting up

    // Set the timezones for the logger/data and the RTC
    // Logging in the given time zone
    // It is STRONGLY RECOMMENDED that you set the RTC to be in UTC (UTC+0)

    // Attach the same modem to both loggers
    // It is only needed for the logger that will be sending out data, but
    // attaching it to both allows either logger to control NIST synchronization
    loggerAllVars.setLoggerPins(wakePin, sdCardSSPin, sdCardPwrPin, buttonPin,

    // Set up the connection information with EnviroDIY for both loggers
    // Doing this for both loggers ensures that the header of the csv will have
    // the tokens in it

    // Note:  Please change these battery voltages to match your battery

    // Set up the sensors, except at lowest battery level
    // Like with the logger, because the variables are duplicated in the arrays,
    // we only need to do this for the complete array.
    if (getBatteryVoltage() > 3.4) {
        Serial.println(F("Setting up sensors..."));

    // Sync the clock if it isn't valid or we have battery to spare
    if (getBatteryVoltage() > 3.55 || !loggerAllVars.isRTCSane()) {
        // Synchronize the RTC with NIST
        // This will also set up the modem

    // Create the log file, adding the default header to it
    // Do this last so we have the best chance of getting the time correct and
    // all sensor names correct
    // Writing to the SD card can be power intensive, so if we're skipping
    // the sensor setup we'll skip this too.
    if (getBatteryVoltage() > 3.4) {
            true);  // true = wait for card to settle after power up
        loggerAllVars.createLogFile(true);  // true = write a new header
            true);  // true = wait for internal housekeeping after write

    // Call the processor sleep
    Serial.println(F("Putting processor to sleep"));
/** End [setup] */

// ==========================================================================
//  Arduino Loop Function
// ==========================================================================
/** Start [loop] */
// Use this long loop when you want to do something special
// Because of the way alarms work on the RTC, it will wake the processor and
// start the loop every minute exactly on the minute.
// The processor may also be woken up by another interrupt or level change on a
// pin - from a button or some other input.
// The "if" statements in the loop determine what will happen - whether the
// sensors update, testing mode starts, or it goes back to sleep.
void loop() {
    // Reset the watchdog

    // Assuming we were woken up by the clock, check if the current time is an
    // even interval of the logging interval
    // We're only doing anything at all if the battery is above 3.4V
    if (loggerAllVars.checkInterval() && getBatteryVoltage() > 3.4) {
        // Flag to notify that we're in already awake and logging a point
        Logger::isLoggingNow = true;

        // Print a line to show new reading
        // Turn on the LED to show we're taking a reading
        // Power up the SD Card, but skip any waits after power up

        // Start the stream for the modbus sensors
        // Because RS485 adapters tend to "steal" current from the data pins
        // we will explicitly start and end the serial connection in the loop.

        // Do a complete update on the "full" array.
        // This this includes powering all of the sensors, getting updated
        // values, and turing them back off.
        // NOTE:  The wake function for each sensor should force sensor setup
        // to run if the sensor was not previously set up.

        // End the stream for the modbus sensors
        // Because RS485 adapters tend to "steal" current from the data pins
        // we will explicitly start and end the serial connection in the loop.

#if defined AltSoftSerial_h
        // Explicitly set the pin modes for the AltSoftSerial pins to make sure
        // they're low
        pinMode(5, OUTPUT);  // On a Mayfly, pin D5 is the AltSoftSerial Tx pin
        pinMode(6, OUTPUT);  // On a Mayfly, pin D6 is the AltSoftSerial Rx pin
        digitalWrite(5, LOW);
        digitalWrite(6, LOW);

        digitalWrite(10, LOW);
        digitalWrite(11, LOW);

        // Create a csv data record and save it to the log file

        // Connect to the network
        // Again, we're only doing this if the battery is doing well
        if (getBatteryVoltage() > 3.55) {
            if (modem.modemWake()) {
                if (modem.connectInternet()) {
                    // Publish data to remotes

                    // Sync the clock at midnight
                    // NOTE:  All loggers have the same clock, pick one
                    if (Logger::markedEpochTime != 0 &&
                        Logger::markedEpochTime % 86400 == 0) {
                        Serial.println(F("Running a daily clock sync..."));

                    // Disconnect from the network
            // Turn the modem off

        // Cut power from the SD card - without additional housekeeping wait
        // Turn off the LED
        // Print a line to show reading ended

        // Unset flag
        Logger::isLoggingNow = false;

    // Check if it was instead the testing interrupt that woke us up
    // Want to enter the testing mode for the "complete" logger so we can see
    // the data from _ALL_ sensors
    // NOTE:  The testingISR attached to the button at the end of the "setup()"
    // function turns on the startTesting flag.  So we know if that flag is set
    // then we want to run the testing mode function.
    if (Logger::startTesting) loggerAllVars.testingMode();

    // Call the processor sleep
    // Only need to do this for one of the loggers
/** End [loop] */