ModularSensors > Examples > menu_a_la_carte.ino

menu_a_la_carte.ino example

A la carte Example

This shows most of the functionality of the library at once. It has code in it for every possible sensor and modem and for both AVR and SAMD boards. This example should never be used directly; it is intended to document all possibilities and to verify compilating.

To create your own code, I recommend starting from a much simpler targeted example, like the Logging to MMW example, and then adding to it based on only the parts of this menu example that apply to you.


Walking Through the Code

NOTE: This walkthrough is intended to be viewed here: https://envirodiy.github.io/ModularSensors/menu_a_la_carte_8ino-example.html

WARNING: This example is long. This walk-through is really, really long. Make use of the table of contents to skip to the parts you need.


Defines and Includes

Defines for the Arduino IDE

The top few lines of the examples set defines of buffer sizes and yields needed for the Arduino IDE. That IDE read any defines within the top few lines and applies them as build flags for the processor. This is not standard behavior for C++ (which is what Arduino code really is) - this is a unique aspect of the Arduino IDE.

#ifndef TINY_GSM_RX_BUFFER
#define TINY_GSM_RX_BUFFER 64
#endif
#ifndef TINY_GSM_YIELD_MS
#define TINY_GSM_YIELD_MS 2
#endif
#ifndef MQTT_MAX_PACKET_SIZE
#define MQTT_MAX_PACKET_SIZE 240
#endif

If you are using PlatformIO, you should instead set these as global build flags in your platformio.ini. This is standard behaviour for C++.

build_flags =
    -DSDI12_EXTERNAL_PCINT
    -DNEOSWSERIAL_EXTERNAL_PCINT
    -DMQTT_MAX_PACKET_SIZE=240
    -DTINY_GSM_RX_BUFFER=64
    -DTINY_GSM_YIELD_MS=2

Library Includes

Next, include the libraries needed for every program using ModularSensors.

// 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>

Logger Settings

Creating Extra Serial Ports

This section of the example has all the code to create and link to serial ports for both AVR and SAMD based boards. The EnviroDIY Mayfly, the Arduino Mega, UNO, and Leonardo are all AVR boards. The Arduino Zero, the M0 and the Sodaq Autonomo are all SAMD boards.

Many different sensors communicate using some sort of serial or transistor-transistor-logic (TTL) protocol. Among these are any sensors using RS232, RS485, RS422. Generally each serial variant (or sometimes each sensor) needs a dedicated serial "port" - its own connection to the processor. Most processors have built in dedicated wires for serial communication - "Hardware" serial. See the page on Arduino streams for much more detail about serial connections with Arduino processors.


AVR Boards

Most Arduino AVR style boards have very few (ie, one, or none) dedicated serial ports available after counting out the programming serial port. So to connect anything else, we need to try to emulate the processor serial functionality with a software library. This example shows three possible libraries that can be used to emulate a serial port on an AVR board.

AltSoftSerial

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. See the processor compatibility page for more information on which pins are used on supported boards.

#include <AltSoftSerial.h>
AltSoftSerial altSoftSerial;
NeoSWSerial

NeoSWSerial is the best software serial that can be used on any pin supporting interrupts. You can use as many instances of NeoSWSerial as you want. Each instance requires two pins, one for data in and another for data out. If you only want to use the serial line for incoming or outgoing data, set the other pin to -1. Not all AVR boards are supported by NeoSWSerial.

#include <NeoSWSerial.h>          // for the stream communication
const int8_t neoSSerial1Rx = 11;  // data in pin
const int8_t neoSSerial1Tx = -1;  // data out pin
NeoSWSerial  neoSSerial1(neoSSerial1Rx, neoSSerial1Tx);
// To use NeoSWSerial in this library, we define a function to receive data
// This is just a short-cut for later
void neoSSerial1ISR() {
    NeoSWSerial::rxISR(*portInputRegister(digitalPinToPort(neoSSerial1Rx)));
}

When using NeoSWSerial we will also have to actually set the data receiving (Rx) pin modes for interrupt in the setup function.

SoftwareSerial with External Interrupts

The "standard" software serial library uses interrupts that conflict with several other libraries used within this program. I've created a version of software serial that has been stripped of interrupts but it is still far from ideal. This should be used only use if necessary. It is not a very accurate serial port!

Accepting its poor quality, you can use as many instances of SoftwareSerial as you want. Each instance requires two pins, one for data in and another for data out. If you only want to use the serial line for incoming or outgoing data, set the other pin to -1.

const int8_t softSerialRx = A3;  // data in pin
const int8_t softSerialTx = A4;  // data out pin

#include <SoftwareSerial_ExtInts.h>  // for the stream communication
SoftwareSerial_ExtInts softSerial1(softSerialRx, softSerialTx);

When using SoftwareSerial with External Interrupts we will also have to actually set the data receiving (Rx) pin modes for interrupt in the setup function.

Software I2C/Wire

This creates a software I2C (wire) instance that can be shared between multiple sensors. Only Testato's SoftwareWire library is supported.

// A software I2C (Wire) instance using Testato's SoftwareWire
// To use SoftwareWire, you must also set a define for the sensor you want to
// use Software I2C for, ie:
//   `#define MS_RAIN_SOFTWAREWIRE`
//   `#define MS_PALEOTERRA_SOFTWAREWIRE`
// or set the build flag(s):
//   `-D MS_RAIN_SOFTWAREWIRE`
//   `-D MS_PALEOTERRA_SOFTWAREWIRE`
#include <SoftwareWire.h>  // Testato's Software I2C
const int8_t softwareSDA = 5;
const int8_t softwareSCL = 4;
SoftwareWire softI2C(softwareSDA, softwareSCL);

SAMD Boards

The SAMD21 supports up to 6 hardware serial ports, which is awesome. But, the Arduino core doesn't make use of all of them, so we have to assign them ourselves.

This section of code assigns SERCOM's 1 and 2 to act as Serial2 and Serial3 on pins 10/11 and 5/2 respectively. These pin selections are based on the Adafruit Feather M0.

// The SAMD21 has 6 "SERCOM" ports, any of which can be used for UART
// communication.  The "core" code for most boards defines one or more UART
// (Serial) ports with the SERCOMs and uses others for I2C and SPI.  We can
// create new UART ports on any available SERCOM.  The table below shows
// definitions for select boards.

// Board =>   Arduino Zero       Adafruit Feather    Sodaq Boards
// -------    ---------------    ----------------    ----------------
// SERCOM0    Serial1 (D0/D1)    Serial1 (D0/D1)     Serial (D0/D1)
// SERCOM1    Available          Available           Serial3 (D12/D13)
// SERCOM2    Available          Available           I2C (A4/A5)
// SERCOM3    I2C (D20/D21)      I2C (D20/D21)       SPI (D11/12/13)
// SERCOM4    SPI (D21/22/23)    SPI (D21/22/23)     SPI1/Serial2
// SERCOM5    EDBG/Serial        Available           Serial1

// If using a Sodaq board, do not define the new sercoms, instead:
// #define ENABLE_SERIAL2
// #define ENABLE_SERIAL3

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

#ifndef ENABLE_SERIAL2
// 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() {
    Serial2.IrqHandler();
}
#endif

#ifndef ENABLE_SERIAL3
// Set up a 'new' UART using SERCOM2
// The Rx will be on digital pin 5, which is SERCOM2's Pad #3
// The Tx will be on digital pin 2, which is SERCOM2's Pad #2
// NOTE:  SERCOM2 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 Serial3(&sercom2, 5, 2, SERCOM_RX_PAD_3, UART_TX_PAD_2);
// Hand over the interrupts to the sercom port
void SERCOM2_Handler() {
    Serial3.IrqHandler();
}
#endif

In addition to creating the extra SERCOM ports here, the pins must be set up as the proper pin peripherals after the serial ports are begun. This is shown in the SAMD Pin Peripherals section of the setup function.

NOTE: The SAMD51 board has an amazing 8 available SERCOM's, but I do not have any exmple code for using them.


Assigning Serial Port Functionality

This section just assigns all the serial ports from the Creating Extra Serial Ports section above to specific functionality. For a board with the option of up to 4 hardware serial ports, like the SAMD21 or Arduino Mega, we use the Serial1 to talk to the modem, Serial2 for modbus, and Serial3 for the Maxbotix. For an AVR board where we're relying on a mix of hardware and software ports, we use hardware Serial 1 for the modem, AltSoftSerial for modbus, and NeoSWSerial for the Maxbotix. Depending on how you rank the importance of each component, you can adjust these to your liking.


Logging Options

Here we set options for the logging and dataLogger object. This includes setting the time zone (daylight savings time is NOT applied) and setting all of the input and output pins related to the logger.

// The name of this program file
const char* sketchName = "menu_a_la_carte.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

Wifi/Cellular Modem Options

This modem section is very lengthy because it contains the code with the constructor for every possible supported modem module. Do NOT try to use more than one modem at a time - it will NOT work.

To create any of the modems, we follow a similar pattern:

First, we'll create a pointer to the serial port (Arduino Stream object) that we'll use for communication between the modem and the MCU. We also assign the baud rate to a variable here. There is a table of Default Baud Rates of Supported Modems on the Notes about Modems page. The baud rate of any of the modules can be changed using AT commands or the modem.gsmModem.setBaud(uint32_t baud) function.

Next, we'll assign all the pin numbers for all the other pins connected between the modem and the MCU. Pins that do not apply should be set as -1. There is a table of general Sleep and Reset Pin Labels and Pin Numbers to Use when Connecting to the Mayfly on the Notes about Modems page.

All the modems also need some sort of network credentials for internet access. For WiFi modems, you need the network name and password (assuming WPA2). For cellular models, you will need the APN assigned to you by the carrier you bought your SIM card from.

Digi XBee Cellular

This is the code to use for any of Digi's cellular XBee or XBee3 modules. All of them can be implented as a DigiXBeeCellularTransparent object - a subclass of DigiXBee and loggerModem. To create a DigiXBeeCellularTransparent object we need to know

  • the serial object name,
  • the MCU pin controlling modem power,
  • the MCU pin connected to the status pin,
  • whether the status pin is the true status pin (ON/SLEEP_N/DIO9) or the CTS_N/DIO7 pin,
  • the MCU pin connected to the RESET_Npin,
  • the DTR_N/SLEEP_RQ/DIO8 pin,
  • and the SIM card's cellular access point name (APN).
// For any Digi Cellular XBee's
// NOTE:  The u-blox based Digi XBee's (3G global and LTE-M global) can be used
// in either bypass or transparent mode, each with pros and cons
// The Telit based Digi XBees (LTE Cat1) can only use this mode.
#include <modems/DigiXBeeCellularTransparent.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section
const int32_t modemBaud = 9600;  // All XBee's use 9600 by default

// Modem Pins - Describe the physical pin connection of your modem to your board
// NOTE:  Use -1 for pins that do not apply
// The pin numbers here are for a Digi XBee with a Mayfly and LTE adapter
// For options https://github.com/EnviroDIY/LTEbee-Adapter/edit/master/README.md
const int8_t modemVccPin = -1;     // MCU pin controlling modem power
                                   // Option: modemVccPin = A5, if Mayfly SJ7 is
                                   // connected to the ASSOC pin
const int8_t modemStatusPin = 19;  // MCU pin used to read modem status
// NOTE:  If possible, use the `STATUS/SLEEP_not` (XBee pin 13) for status, but
// the CTS pin can also be used if necessary
const bool   useCTSforStatus = false;  // Flag to use the CTS pin for status
const int8_t modemResetPin   = 20;     // MCU pin connected to modem reset pin
const int8_t modemSleepRqPin = 23;     // MCU pin for modem sleep/wake request
const int8_t modemLEDPin = redLED;     // MCU pin connected an LED to show modem
                                       // status

// Network connection information
const char* apn = "xxxxx";  // APN for GPRS connection

// Create the modem object
DigiXBeeCellularTransparent modemXBCT(&modemSerial, modemVccPin, modemStatusPin,
                                      useCTSforStatus, modemResetPin,
                                      modemSleepRqPin, apn);
// Create an extra reference to the modem by a generic name
DigiXBeeCellularTransparent modem = modemXBCT;

Depending on your cellular carrier, it is best to select the proper carrier profile and network. Setting these helps the modem to connect to network faster. This is shows in the XBee Cellular Carrier chunk of the setup function.

Digi XBee3 LTE-M Bypass

This code is for Digi's LTE-M XBee3 based on the u-blox SARA R410M - used in bypass mode. To create a DigiXBeeLTEBypass object we need to know

  • the serial object name,
  • the MCU pin controlling modem power,
  • the MCU pin connected to the status pin,
  • whether the status pin is the true status pin (ON/SLEEP_N/DIO9) or the CTS_N/DIO7 pin,
  • the MCU pin connected to the RESET_Npin,
  • the DTR_N/SLEEP_RQ/DIO8 pin,
  • and the SIM card's cellular access point name (APN).

Pins that do not apply should be set as -1.

// For the u-blox SARA R410M based Digi LTE-M XBee3
// NOTE:  According to the manual, this should be less stable than transparent
// mode, but my experience is the complete reverse.
#include <modems/DigiXBeeLTEBypass.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section
const int32_t modemBaud = 9600;  // All XBee's use 9600 by default

// Modem Pins - Describe the physical pin connection of your modem to your board
// NOTE:  Use -1 for pins that do not apply
// The pin numbers here are for a Digi XBee with a Mayfly and LTE adapter
const int8_t modemVccPin    = A5;  // MCU pin controlling modem power
const int8_t modemStatusPin = 19;  // MCU pin used to read modem status
// NOTE:  If possible, use the `STATUS/SLEEP_not` (XBee pin 13) for status, but
// the CTS pin can also be used if necessary
const bool   useCTSforStatus = false;  // Flag to use the CTS pin for status
const int8_t modemResetPin   = 20;     // MCU pin connected to modem reset pin
const int8_t modemSleepRqPin = 23;     // MCU pin for modem sleep/wake request
const int8_t modemLEDPin = redLED;     // MCU pin connected an LED to show modem
                                       // status

// Network connection information
const char* apn = "xxxxx";  // APN for GPRS connection

// Create the modem object
DigiXBeeLTEBypass modemXBLTEB(&modemSerial, modemVccPin, modemStatusPin,
                              useCTSforStatus, modemResetPin, modemSleepRqPin,
                              apn);
// Create an extra reference to the modem by a generic name
DigiXBeeLTEBypass modem = modemXBLTEB;

Depending on your cellular carrier, it is best to select the proper carrier profile and network. Setting these helps the modem to connect to network faster. This is shows in the SARA R4 Cellular Carrier chunk of the setup function.

Digi XBee 3G - Bypass Mode

This code is for Digi's 3G/2G XBee based on the u-blox SARA U201 - used in bypass mode. To create a DigiXBee3GBypass object we need to know

  • the serial object name,
  • the MCU pin controlling modem power,
  • the MCU pin connected to the status pin,
  • whether the status pin is the "true" status pin (ON/SLEEP_N/DIO9) or the CTS_N/DIO7 pin,
  • the MCU pin connected to the RESET_Npin,
  • the DTR_N/SLEEP_RQ/DIO8 pin,
  • and the SIM card's cellular access point name (APN).

Pins that do not apply should be set as -1.

// For the u-blox SARA U201 based Digi 3G XBee with 2G fallback
// NOTE:  According to the manual, this should be less stable than transparent
// mode, but my experience is the complete reverse.
#include <modems/DigiXBee3GBypass.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section
const int32_t modemBaud = 9600;  // All XBee's use 9600 by default

// Modem Pins - Describe the physical pin connection of your modem to your board
// NOTE:  Use -1 for pins that do not apply
// The pin numbers here are for a Digi XBee with a Mayfly and LTE adapter
const int8_t modemVccPin    = A5;  // MCU pin controlling modem power
const int8_t modemStatusPin = 19;  // MCU pin used to read modem status
// NOTE:  If possible, use the `STATUS/SLEEP_not` (XBee pin 13) for status, but
// the CTS pin can also be used if necessary
const bool   useCTSforStatus = false;  // Flag to use the CTS pin for status
const int8_t modemResetPin   = 20;     // MCU pin connected to modem reset pin
const int8_t modemSleepRqPin = 23;     // MCU pin for modem sleep/wake request
const int8_t modemLEDPin = redLED;     // MCU pin connected an LED to show modem
                                       // status

// Network connection information
const char* apn = "xxxxx";  // APN for GPRS connection

// Create the modem object
DigiXBee3GBypass modemXB3GB(&modemSerial, modemVccPin, modemStatusPin,
                            useCTSforStatus, modemResetPin, modemSleepRqPin,
                            apn);
// Create an extra reference to the modem by a generic name
DigiXBee3GBypass modem = modemXB3GB;

Digi XBee S6B Wifi

This code is for the Digi's S6B wifi module. To create a DigiXBeeWifi object we need to know

  • the serial object name,
  • the MCU pin controlling modem power,
  • the MCU pin connected to the status pin,
  • whether the status pin is the "true" status pin (ON/SLEEP_N/DIO9) or the CTS_N/DIO7 pin,
  • the MCU pin connected to the RESET_Npin,
  • the DTR_N/SLEEP_RQ/DIO8 pin,
  • the wifi access point name,
  • and the wifi WPA2 password.

Pins that do not apply should be set as -1.

// For the Digi Wifi XBee (S6B)
#include <modems/DigiXBeeWifi.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section
const int32_t modemBaud = 9600;  // All XBee's use 9600 by default

// Modem Pins - Describe the physical pin connection of your modem to your board
// NOTE:  Use -1 for pins that do not apply
// The pin numbers here are for a Digi XBee direcly connected to a Mayfly
const int8_t modemVccPin    = -1;  // MCU pin controlling modem power
const int8_t modemStatusPin = 19;  // MCU pin used to read modem status
// NOTE:  If possible, use the `STATUS/SLEEP_not` (XBee pin 13) for status, but
// the CTS pin can also be used if necessary
const bool   useCTSforStatus = true;  // Flag to use the CTS pin for status
const int8_t modemResetPin   = -1;    // MCU pin connected to modem reset pin
const int8_t modemSleepRqPin = 23;    // MCU pin for modem sleep/wake request
const int8_t modemLEDPin = redLED;    // MCU pin connected an LED to show modem
                                      // status

// Network connection information
const char* wifiId  = "xxxxx";  // WiFi access point name
const char* wifiPwd = "xxxxx";  // WiFi password (WPA2)

// Create the modem object
DigiXBeeWifi modemXBWF(&modemSerial, modemVccPin, modemStatusPin,
                       useCTSforStatus, modemResetPin, modemSleepRqPin, wifiId,
                       wifiPwd);
// Create an extra reference to the modem by a generic name
DigiXBeeWifi modem = modemXBWF;

Espressif ESP8266

This code is for the Espressif ESP8266 or ESP32 operating with "AT" firmware. To create a EspressifESP8266 object we need to know

  • the serial object name,
  • the MCU pin controlling modem power,
  • the light sleep status pin (on both ESP and MCU),
  • the reset pin (MCU pin connected to the ESP's RSTB/DIO16),
  • the light sleep wake pin (on both the ESP and the MCU),
  • the wifi access point name,
  • and the wifi WPA2 password.

Pins that do not apply should be set as -1.

// For almost anything based on the Espressif ESP8266 using the
// AT command firmware
#include <modems/EspressifESP8266.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section
const int32_t modemBaud = 115200;  // Communication speed of the modem
// NOTE:  This baud rate too fast for an 8MHz board, like the Mayfly!  The
// module should be programmed to a slower baud rate or set to auto-baud using
// the AT+UART_CUR or AT+UART_DEF command.

// Modem Pins - Describe the physical pin connection of your modem to your board
// NOTE:  Use -1 for pins that do not apply
// Example pins here are for a DFRobot ESP8266 Bee with Mayfly
const int8_t modemVccPin     = -2;  // MCU pin controlling modem power
const int8_t modemStatusPin  = -1;  // MCU pin used to read modem status
const int8_t modemResetPin   = -1;  // MCU pin connected to modem reset pin
const int8_t modemSleepRqPin = 19;  // MCU pin for wake from light sleep
const int8_t modemLEDPin = redLED;  // MCU pin connected an LED to show modem
                                    // status
// Pins for light sleep on the ESP8266. For power savings, I recommend
// NOT using these if it's possible to use deep sleep.
const int8_t espSleepRqPin = 13;  // GPIO# ON THE ESP8266 to assign for light
                                  // sleep request
const int8_t espStatusPin = -1;   // GPIO# ON THE ESP8266 to assign for light
                                  // sleep status

// Network connection information
const char* wifiId  = "xxxxx";  // WiFi access point name
const char* wifiPwd = "xxxxx";  // WiFi password (WPA2)

// Create the modem object
EspressifESP8266 modemESP(&modemSerial, modemVccPin, modemStatusPin,
                          modemResetPin, modemSleepRqPin, wifiId, wifiPwd,
                          espSleepRqPin,
                          espStatusPin  // Optional arguments
);
// Create an extra reference to the modem by a generic name
EspressifESP8266 modem = modemESP;

Because the ESP8266's default baud rate is too fast for an 8MHz board like the Mayfly, to use it you need to drop the baud rate down for sucessful communication. You can set the slower baud rate using some external method, or useing the code from the ESP8266 Baud Rate(https://envirodiy.github.io/ModularSensors/menu_a_la_carte_8ino-example.html#enu_setup_esp) part of the setup function below.

Quectel BG96

This code is for the Dragino, Nimbelink or other boards based on the Quectel BG96. To create a QuectelBG96 object we need to know

  • the serial object name,
  • the MCU pin controlling modem power,
  • the MCU pin connected to the STATUS pin,
  • the MCU pin connected to the RESET_N pin,
  • the MCU pin connected to the PWRKEY pin (for sleep request),
  • and the SIM card's cellular access point name (APN).

Pins that do not apply should be set as -1.

// For the Dragino, Nimbelink or other boards based on the Quectel BG96
#include <modems/QuectelBG96.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section
const int32_t modemBaud = 115200;  // Communication speed of the modem
// NOTE:  This baud rate too fast for an 8MHz board, like the Mayfly!  The
// module should be programmed to a slower baud rate or set to auto-baud using
// the AT+IPR=9600 command.

// Modem Pins - Describe the physical pin connection of your modem to your board
// NOTE:  Use -1 for pins that do not apply
// Example pins here are for a modified Mayfly and a Dragino IoT Bee
const int8_t modemVccPin     = -1;  // MCU pin controlling modem power
const int8_t modemStatusPin  = -1;  // MCU pin used to read modem status
const int8_t modemResetPin   = A4;  // MCU pin connected to modem reset pin
const int8_t modemSleepRqPin = A3;  // MCU pin for modem sleep/wake request
const int8_t modemLEDPin = redLED;  // MCU pin connected an LED to show modem
                                    // status

// Network connection information
const char* apn = "xxxxx";  // APN for GPRS connection

// Create the modem object
QuectelBG96 modemBG96(&modemSerial, modemVccPin, modemStatusPin, modemResetPin,
                      modemSleepRqPin, apn);
// Create an extra reference to the modem by a generic name
QuectelBG96 modem = modemBG96;

If you are interfacing with a Nimbelink Skywire board via the Skywire development board, you also need to handle the fact that the development board reverses the levels of the status, wake, and reset pins. Code to invert the pin levels is in the Skywire Pin Inversions part of the setup function below.

Sequans Monarch

This code is for the Nimbelink LTE-M Verizon/Sequans or other boards based on the Sequans Monarch series SoC. To create a SequansMonarch object we need to know

  • the serial object name,
  • the MCU pin controlling modem power,
  • the MCU pin connected to either the GPIO3/STATUS_LED or POWER_MON pin,
  • the MCU pin connected to the RESETN pin,
  • the MCU pin connected to the RTS or RTS0 pin (for sleep request),
  • and the SIM card's cellular access point name (APN).

Pins that do not apply should be set as -1.

// For the Nimbelink LTE-M Verizon/Sequans or other boards based on the Sequans
// Monarch series
#include <modems/SequansMonarch.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section
const int32_t modemBaud = 921600;  // Default baud rate of SVZM20 is 921600
// NOTE:  This baud rate is much too fast for many Arduinos!  The module should
// be programmed to a slower baud rate or set to auto-baud using the AT+IPR
// command.

// Modem Pins - Describe the physical pin connection of your modem to your board
// NOTE:  Use -1 for pins that do not apply
// Nimbelink Skywire (NOT directly connectable to a Mayfly!)
const int8_t modemVccPin     = -1;  // MCU pin controlling modem power
const int8_t modemStatusPin  = 19;  // MCU pin used to read modem status
const int8_t modemResetPin   = 20;  // MCU pin connected to modem reset pin
const int8_t modemSleepRqPin = 23;  // MCU pin for modem sleep/wake request
const int8_t modemLEDPin = redLED;  // MCU pin connected an LED to show modem
                                    // status

// Network connection information
const char* apn = "xxxxx";  // APN for GPRS connection

// Create the modem object
SequansMonarch modemSVZM(&modemSerial, modemVccPin, modemStatusPin,
                         modemResetPin, modemSleepRqPin, apn);
// Create an extra reference to the modem by a generic name
SequansMonarch modem = modemSVZM;

If you are interfacing with a Nimbelink Skywire board via the Skywire development board, you also need to handle the fact that the development board reverses the levels of the status, wake, and reset pins. Code to invert the pin levels is in the Skywire Pin Inversions part of the setup function below.

The default baud rate of the SVZM20 is much too fast for almost all Arduino boards. Before attampting to connect a SVZM20 to an Arduino you should connect it to your computer and use AT commands to decrease the baud rate. The proper command to decrease the baud rate to 9600 (8N1) is: AT+IPR=9600.

SIMCom SIM800

This code is for a SIMCom SIM800 or SIM900 or one of their many variants, including the Adafruit Fona and the Sodaq 2GBee R4. To create a SIMComSIM800 object we need to know

  • the serial object name,
  • the MCU pin controlling modem power,
  • the MCU pin connected to the STATUS pin,
  • the MCU pin connected to the RESET pin,
  • the MCU pin connected to the PWRKEY pin (for sleep request),
  • and the SIM card's cellular access point name (APN).

Pins that do not apply should be set as -1.

NOTE: This is NOT the correct form for a Sodaq 2GBee R6 or R7. See the section for a 2GBee R6.

// For almost anything based on the SIMCom SIM800 EXCEPT the Sodaq 2GBee R6 and
// higher
#include <modems/SIMComSIM800.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section
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
// Example pins are for a Sodaq GPRSBee R4 with a Mayfly
const int8_t modemVccPin     = -1;  // MCU pin controlling modem power
const int8_t modemStatusPin  = 19;  // MCU pin used to read modem status
const int8_t modemResetPin   = -1;  // MCU pin connected to modem reset pin
const int8_t modemSleepRqPin = 23;  // MCU pin for modem sleep/wake request
const int8_t modemLEDPin = redLED;  // MCU pin connected an LED to show modem
                                    // status

// Network connection information
const char* apn = "xxxxx";  // APN for GPRS connection

// Create the modem object
SIMComSIM800 modemS800(&modemSerial, modemVccPin, modemStatusPin, modemResetPin,
                       modemSleepRqPin, apn);
// Create an extra reference to the modem by a generic name
SIMComSIM800 modem = modemS800;

SIMCom SIM7000

This code is for a SIMCom SIM7000 or one of its variants. To create a SIMComSIM7000 object we need to know

  • the serial object name,
  • the MCU pin controlling modem power,
  • the MCU pin connected to the STATUS pin,
  • the MCU pin connected to the RESET pin,
  • the MCU pin connected to the PWRKEY pin (for sleep request),
  • and the SIM card's cellular access point name (APN).

Pins that do not apply should be set as -1.

// For almost anything based on the SIMCom SIM7000
#include <modems/SIMComSIM7000.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section
const int32_t modemBaud = 9600;  //  SIM7000 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     = -1;  // MCU pin controlling modem power
const int8_t modemStatusPin  = -1;  // MCU pin used to read modem status
const int8_t modemResetPin   = -1;  // MCU pin connected to modem reset pin
const int8_t modemSleepRqPin = 23;  // MCU pin for modem sleep/wake request
const int8_t modemLEDPin = redLED;  // MCU pin connected an LED to show modem
                                    // status

// Network connection information
const char* apn = "xxxxx";  // APN for GPRS connection

// Create the modem object
SIMComSIM7000 modem7000(&modemSerial, modemVccPin, modemStatusPin,
                        modemResetPin, modemSleepRqPin, apn);
// Create an extra reference to the modem by a generic name
SIMComSIM7000 modem = modem7000;

Sodaq GPRSBee

This code is for the Sodaq 2GBee R6 and R7 based on the SIMCom SIM800. To create a Sodaq2GBeeR6 object we need to know

  • the serial object name,
  • the MCU pin controlling modem power, (NOTE: On the GPRSBee R6 and R7 the pin labeled as ON/OFF in Sodaq's diagrams is tied to both the SIM800 power supply and the (inverted) SIM800 PWRKEY. You should enter this pin as the power pin.)
  • and the SIM card's cellular access point name (APN).

Pins that do not apply should be set as -1. The GPRSBee R6/R7 does not expose the RESET pin of the SIM800. The PWRKEY is held LOW as long as the SIM800 is powered (as mentioned above).

// 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>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section
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
// Example pins are for a Sodaq GPRSBee R6 or R7 with a Mayfly
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 modemResetPin   = -1;  // MCU pin connected to modem reset pin
const int8_t modemSleepRqPin = -1;  // MCU pin for modem sleep/wake request
const int8_t modemLEDPin = redLED;  // MCU pin connected an LED to show modem
                                    // status

// Network connection information
const char* apn = "xxxxx";  // APN for GPRS connection

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

u-blox SARA R410M

This code is for modules based on the 4G LTE-M u-blox SARA R410M including the Sodaq UBee. To create a SodaqUBeeR410M object we need to know

  • the serial object name,
  • the MCU pin controlling modem power,
  • the MCU pin connected to the V_INT pin (for status),
  • the MCU pin connected to the RESET_N pin,
  • the MCU pin connected to the PWR_ON pin (for sleep request),
  • and the SIM card's cellular access point name (APN).

Pins that do not apply should be set as -1.

// For the Sodaq UBee based on the 4G LTE-M u-blox SARA R410M
#include <modems/SodaqUBeeR410M.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section
const int32_t modemBaud =
    115200;  // Default baud rate of the SARA R410M is 115200
// NOTE:  The SARA R410N DOES NOT save baud rate to non-volatile memory.  After
// every power loss, the module will return to the default baud rate of 115200.
// NOTE:  115200 is TOO FAST for an 8MHz Arduino.  This library attempts to
// compensate by sending a baud rate change command in the wake function when
// compiled for a 8MHz board. Because of this, 8MHz boards, LIKE THE MAYFLY,
// *MUST* use a HardwareSerial instance as modemSerial.

// Modem Pins - Describe the physical pin connection of your modem to your board
// NOTE:  Use -1 for pins that do not apply
// Example pins are for a Sodaq uBee R410M with a Mayfly
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 modemResetPin   = -1;  // MCU pin connected to modem reset pin
const int8_t modemSleepRqPin = 20;  // MCU pin for modem sleep/wake request
const int8_t modemLEDPin = redLED;  // MCU pin connected an LED to show modem
                                    // status

// Network connection information
const char* apn = "xxxxx";  // APN for GPRS connection

// Create the modem object
SodaqUBeeR410M modemR410(&modemSerial, modemVccPin, modemStatusPin,
                         modemResetPin, modemSleepRqPin, apn);
// Create an extra reference to the modem by a generic name
SodaqUBeeR410M modem = modemR410;

Depending on your cellular carrier, it is best to select the proper carrier profile and network. Setting these helps the modem to connect to network faster. This is shows in the SARA R4 Cellular Carrier chunk of the setup function.

u-blox SARA U201

This code is for modules based on the 3G/2G u-blox SARA U201 including the Sodaq UBee or the Sodaq 3GBee. To create a SodaqUBeeU201 object we need to know

  • the serial object name,
  • the MCU pin controlling modem power,
  • the MCU pin connected to the V_INT pin (for status),
  • the MCU pin connected to the RESET_N pin,
  • the MCU pin connected to the PWR_ON pin (for sleep request),
  • and the SIM card's cellular access point name (APN).

Pins that do not apply should be set as -1.

// For the Sodaq UBee based on the 3G u-blox SARA U201
#include <modems/SodaqUBeeU201.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section
const int32_t modemBaud =
    9600;  //  SARA U2xx module 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
// Example pins are for a Sodaq uBee U201 with a Mayfly
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 modemResetPin   = -1;  // MCU pin connected to modem reset pin
const int8_t modemSleepRqPin = 20;  // MCU pin for modem sleep/wake request
const int8_t modemLEDPin = redLED;  // MCU pin connected an LED to show modem
                                    // status

// Network connection information
const char* apn = "xxxxx";  // APN for GPRS connection

// Create the modem object
SodaqUBeeU201 modemU201(&modemSerial, modemVccPin, modemStatusPin,
                        modemResetPin, modemSleepRqPin, apn);
// Create an extra reference to the modem by a generic name
SodaqUBeeU201 modem = modemU201;

Modem Measured Variables

After creating the modem object, we can create Variable objects for each of the variables the modem is capable of measuring (Modem_SignalPercent, Modem_BatteryState, Modem_BatteryPercent, Modem_BatteryVoltage, and Modem_Temp). When we create the modem-linked variable objects, the first argument of the constructor, the loggerModem to like the variables to is required. The second and third arguments (the UUID and the variable code) included here are optional. Note that here we create the variables for anything measured by any of the modems, but most modems are not capable of measuring all of the values. Some modem-measured values may be meaningless depending on the board configuration - often the battery parameters returned by a cellular component have little meaning because the module is downstream of a voltage regulator.

// Create RSSI and signal strength variable pointers for the modem
Variable* modemRSSI =
    new Modem_RSSI(&modem, "12345678-abcd-1234-ef00-1234567890ab", "RSSI");
Variable* modemSignalPct = new Modem_SignalPercent(
    &modem, "12345678-abcd-1234-ef00-1234567890ab", "signalPercent");
Variable* modemBatteryState = new Modem_BatteryState(
    &modem, "12345678-abcd-1234-ef00-1234567890ab", "modemBatteryCS");
Variable* modemBatteryPct = new Modem_BatteryPercent(
    &modem, "12345678-abcd-1234-ef00-1234567890ab", "modemBatteryPct");
Variable* modemBatteryVoltage = new Modem_BatteryVoltage(
    &modem, "12345678-abcd-1234-ef00-1234567890ab", "modemBatterymV");
Variable* modemTemperature =
    new Modem_Temp(&modem, "12345678-abcd-1234-ef00-1234567890ab", "modemTemp");

Sensors and Measured Variables

The processor as a sensor

Set options and create the objects for using the processor as a sensor to report battery level, processor free ram, and sample number.

The processor can return the number of "samples" it has taken, the amount of RAM it has available and, for some boards, the battery voltage (EnviroDIY Mayfly, Sodaq Mbili, Ndogo, Autonomo, and One, Adafruit Feathers). The version of the board is required as input (ie, for a EnviroDIY Mayfly: "v0.3" or "v0.4" or "v0.5"). Use a blank value (ie, "") for un-versioned boards. Please note that while you can opt to average more than one sample, it really makes no sense to do so for the processor. The number of "samples" taken will increase by one for each time another processor "measurement" is taken so averaging multiple measurements from the processor will result in the number of samples increasing by more than one with each loop.

#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");

Maxim DS3231 RTC as a sensor

In addition to the time, we can also use the required DS3231 real time clock to report the temperature of the circuit board. This temperature is not equivalent to an environmental temperature measurement and should only be used to as a diagnostic. As above, we create both the sensor and the variables measured by it.

#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");

AOSong AM2315

Here is the code for the AOSong AM2315 temperature and humidity sensor. This is an I2C sensor with only one possible address so the only argument required for the constructor is the pin on the MCU controlling power to the AM2315 (AM2315Power). The number of readings to average from the sensor is optional, but can be supplied as the second argument for the constructor if desired.

#include <sensors/AOSongAM2315.h>

const int8_t AM2315Power = sensorPowerPin;  // Power pin (-1 if unconnected)

// Create an AOSong AM2315 sensor object
AOSongAM2315 am2315(AM2315Power);

// Create humidity and temperature variable pointers for the AM2315
Variable* am2315Humid =
    new AOSongAM2315_Humidity(&am2315, "12345678-abcd-1234-ef00-1234567890ab");
Variable* am2315Temp =
    new AOSongAM2315_Temp(&am2315, "12345678-abcd-1234-ef00-1234567890ab");

AOSong DHT

Here is the code for the AOSong DHT temperature and humidity sensor. To create the DHT Sensor we need the power pin, the data pin, and the DHT type. The number of readings to average from the sensor is optional, but can be supplied as the fourth argument for the constructor if desired.

#include <sensors/AOSongDHT.h>

const int8_t DHTPower = sensorPowerPin;  // Power pin (-1 if unconnected)
const int8_t DHTPin   = 10;              // DHT data pin
DHTtype      dhtType  = DHT11;  // DHT type, either DHT11, DHT21, or DHT22

// Create an AOSong DHT sensor object
AOSongDHT dht(DHTPower, DHTPin, dhtType);

// Create humidity, temperature, and heat index variable pointers for the DHT
Variable* dhtHumid =
    new AOSongDHT_Humidity(&dht, "12345678-abcd-1234-ef00-1234567890ab");
Variable* dhtTemp = new AOSongDHT_Temp(&dht,
                                       "12345678-abcd-1234-ef00-1234567890ab");
Variable* dhtHI   = new AOSongDHT_HI(&dht,
                                   "12345678-abcd-1234-ef00-1234567890ab");

Apogee SQ-212 Quantum Light Sensor

Here is the code for the Apogee SQ-212 Quantum Light Sensor. The SQ-212 is not directly connected to the MCU, but rather to an TI ADS1115 that communicates with the MCU. The Arduino pin controlling power on/off and the analog data channel on the TI ADS1115 are required for the sensor constructor. If your ADD converter is not at the standard address of 0x48, you can enter its actual address as the third argument. The number of readings to average from the sensor is optional, but can be supplied as the fourth argument for the constructor if desired.

#include <sensors/AOSongDHT.h>

const int8_t DHTPower = sensorPowerPin;  // Power pin (-1 if unconnected)
const int8_t DHTPin   = 10;              // DHT data pin
DHTtype      dhtType  = DHT11;  // DHT type, either DHT11, DHT21, or DHT22

// Create an AOSong DHT sensor object
AOSongDHT dht(DHTPower, DHTPin, dhtType);

// Create humidity, temperature, and heat index variable pointers for the DHT
Variable* dhtHumid =
    new AOSongDHT_Humidity(&dht, "12345678-abcd-1234-ef00-1234567890ab");
Variable* dhtTemp = new AOSongDHT_Temp(&dht,
                                       "12345678-abcd-1234-ef00-1234567890ab");
Variable* dhtHI   = new AOSongDHT_HI(&dht,
                                   "12345678-abcd-1234-ef00-1234567890ab");

Atlas Scientific EZO Circuits

The next several sections are for Atlas Scientific EZO circuts and sensors. The sensor class constructors for each are nearly identical, except for the class name. In the most common setup, with hardware I2C, the only required argument for the constructor is the Arduino pin controlling power on/off; the i2cAddressHex is optional as is the number of readings to average.

The default I2C addresses for the circuits are:

  • CO2: 0x69 (105)
  • DO: 0x61 (97)
  • EC (conductivity): 0x64 (100)
  • ORP (redox): 0x62 (98)
  • pH: 0x63 (99)
  • RTD (temperature): 0x66 (102) All of the circuits can be re-addressed to any other 8 bit number if desired. To use multiple circuits of the same type, re-address them.

Atlas Scientific EZO-CO2 Embedded NDIR Carbon Dioxide Sensor

#include <sensors/AtlasScientificCO2.h>

const int8_t AtlasCO2Power = sensorPowerPin;  // Power pin (-1 if unconnected)
uint8_t      AtlasCO2i2c_addr = 0x69;  // Default for CO2-EZO is 0x69 (105)
// All Atlas sensors have different default I2C addresses, but any of them can
// be re-addressed to any 8 bit number.  If using the default address for any
// Atlas Scientific sensor, you may omit this argument.

// Create an Atlas Scientific CO2 sensor object
// AtlasScientificCO2 atlasCO2(AtlasCO2Power, AtlasCO2i2c_addr);
AtlasScientificCO2 atlasCO2(AtlasCO2Power);

// Create concentration and temperature variable pointers for the EZO-CO2
Variable* atlasCO2CO2 = new AtlasScientificCO2_CO2(
    &atlasCO2, "12345678-abcd-1234-ef00-1234567890ab");
Variable* atlasCO2Temp = new AtlasScientificCO2_Temp(
    &atlasCO2, "12345678-abcd-1234-ef00-1234567890ab");

Atlas Scientific EZO-DO Dissolved Oxygen Sensor

#include <sensors/AtlasScientificDO.h>

const int8_t AtlasDOPower    = sensorPowerPin;  // Power pin (-1 if unconnected)
uint8_t      AtlasDOi2c_addr = 0x61;            // Default for DO is 0x61 (97)
// All Atlas sensors have different default I2C addresses, but any of them can
// be re-addressed to any 8 bit number.  If using the default address for any
// Atlas Scientific sensor, you may omit this argument.

// Create an Atlas Scientific DO sensor object
// AtlasScientificDO atlasDO(AtlasDOPower, AtlasDOi2c_addr);
AtlasScientificDO atlasDO(AtlasDOPower);

// Create concentration and percent saturation variable pointers for the EZO-DO
Variable* atlasDOconc = new AtlasScientificDO_DOmgL(
    &atlasDO, "12345678-abcd-1234-ef00-1234567890ab");
Variable* atlasDOpct = new AtlasScientificDO_DOpct(
    &atlasDO, "12345678-abcd-1234-ef00-1234567890ab");

Atlas Scientific EZO-ORP Oxidation/Reduction Potential Sensor

#include <sensors/AtlasScientificORP.h>

const int8_t AtlasORPPower = sensorPowerPin;  // Power pin (-1 if unconnected)
uint8_t      AtlasORPi2c_addr = 0x62;         // Default for ORP is 0x62 (98)
// All Atlas sensors have different default I2C addresses, but any of them can
// be re-addressed to any 8 bit number.  If using the default address for any
// Atlas Scientific sensor, you may omit this argument.

// Create an Atlas Scientific ORP sensor object
// AtlasScientificORP atlasORP(AtlasORPPower, AtlasORPi2c_addr);
AtlasScientificORP atlasORP(AtlasORPPower);

// Create a potential variable pointer for the ORP
Variable* atlasORPot = new AtlasScientificORP_Potential(
    &atlasORP, "12345678-abcd-1234-ef00-1234567890ab");

Atlas Scientific EZO-pH Sensor

#include <sensors/AtlasScientificpH.h>

const int8_t AtlaspHPower    = sensorPowerPin;  // Power pin (-1 if unconnected)
uint8_t      AtlaspHi2c_addr = 0x63;            // Default for pH is 0x63 (99)
// All Atlas sensors have different default I2C addresses, but any of them can
// be re-addressed to any 8 bit number.  If using the default address for any
// Atlas Scientific sensor, you may omit this argument.

// Create an Atlas Scientific pH sensor object
// AtlasScientificpH atlaspH(AtlaspHPower, AtlaspHi2c_addr);
AtlasScientificpH atlaspH(AtlaspHPower);

// Create a pH variable pointer for the pH sensor
Variable* atlaspHpH =
    new AtlasScientificpH_pH(&atlaspH, "12345678-abcd-1234-ef00-1234567890ab");

Atlas Scientific EZO-RTD Temperature Sensor

#include <sensors/AtlasScientificRTD.h>

const int8_t AtlasRTDPower = sensorPowerPin;  // Power pin (-1 if unconnected)
uint8_t      AtlasRTDi2c_addr = 0x66;         // Default for RTD is 0x66 (102)
// All Atlas sensors have different default I2C addresses, but any of them can
// be re-addressed to any 8 bit number.  If using the default address for any
// Atlas Scientific sensor, you may omit this argument.

// Create an Atlas Scientific RTD sensor object
// AtlasScientificRTD atlasRTD(AtlasRTDPower, AtlasRTDi2c_addr);
AtlasScientificRTD atlasRTD(AtlasRTDPower);

// Create a temperature variable pointer for the RTD
Variable* atlasTemp = new AtlasScientificRTD_Temp(
    &atlasRTD, "12345678-abcd-1234-ef00-1234567890ab");

Atlas Scientific EZO-EC Conductivity Sensor

#include <sensors/AtlasScientificEC.h>

const int8_t AtlasECPower    = sensorPowerPin;  // Power pin (-1 if unconnected)
uint8_t      AtlasECi2c_addr = 0x64;            // Default for EC is 0x64 (100)
// All Atlas sensors have different default I2C addresses, but any of them can
// be re-addressed to any 8 bit number.  If using the default address for any
// Atlas Scientific sensor, you may omit this argument.

// Create an Atlas Scientific Conductivity sensor object
// AtlasScientificEC atlasEC(AtlasECPower, AtlasECi2c_addr);
AtlasScientificEC atlasEC(AtlasECPower);

// Create four variable pointers for the EZO-ES
Variable* atlasCond = new AtlasScientificEC_Cond(
    &atlasEC, "12345678-abcd-1234-ef00-1234567890ab");
Variable* atlasTDS =
    new AtlasScientificEC_TDS(&atlasEC, "12345678-abcd-1234-ef00-1234567890ab");
Variable* atlasSal = new AtlasScientificEC_Salinity(
    &atlasEC, "12345678-abcd-1234-ef00-1234567890ab");
Variable* atlasGrav = new AtlasScientificEC_SpecificGravity(
    &atlasEC, "12345678-abcd-1234-ef00-1234567890ab");

// Create a calculated variable for the temperature compensated conductivity
// (that is, the specific conductance).  For this example, we will use the
// temperature measured by the Atlas RTD above this.  You could use the
// temperature returned by any other water temperature sensor if desired.
// **DO NOT** use your logger board temperature (ie, from the DS3231) to
// calculate specific conductance!
float calculateAtlasSpCond(void) {
    float spCond    = -9999;  // Always safest to start with a bad value
    float waterTemp = atlasTemp->getValue();
    float rawCond   = atlasCond->getValue();
    // ^^ Linearized temperature correction coefficient per degrees Celsius.
    // The value of 0.019 comes from measurements reported here:
    // Hayashi M. Temperature-electrical conductivity relation of water for
    // environmental monitoring and geophysical data inversion. Environ Monit
    // Assess. 2004 Aug-Sep;96(1-3):119-28.
    // doi: 10.1023/b:emas.0000031719.83065.68. PMID: 15327152.
    if (waterTemp != -9999 && rawCond != -9999) {
        // make sure both inputs are good
        float temperatureCoef = 0.019;
        spCond = rawCond / (1 + temperatureCoef * (waterTemp - 25.0));
    }
    return spCond;
}

// Properties of the calculated variable
// The number of digits after the decimal place
const uint8_t atlasSpCondResolution = 0;
// This must be a value from http://vocabulary.odm2.org/variablename/
const char* atlasSpCondName = "specificConductance";
// This must be a value from http://vocabulary.odm2.org/units/
const char* atlasSpCondUnit = "microsiemenPerCentimeter";
// A short code for the variable
const char* atlasSpCondCode = "atlasSpCond";
// The (optional) universallly unique identifier
const char* atlasSpCondUUID = "12345678-abcd-1234-ef00-1234567890ab";

// Finally, create the specific conductance variable and return a pointer to it
Variable* atlasSpCond =
    new Variable(calculateAtlasSpCond, atlasSpCondResolution, atlasSpCondName,
                 atlasSpCondUnit, atlasSpCondCode, atlasSpCondUUID);

Bosch BME280 Environmental Sensor

Here is the code for the Bosch BME280 environmental sensor. The only input needed is the Arduino pin controlling power on/off; the i2cAddressHex is optional as is the number of readings to average. Keep in mind that the possible I2C addresses of the BME280 match those of the MS5803; when using those sensors together, make sure they are set to opposite addresses.

#include <sensors/BoschBME280.h>

const int8_t BME280Power = sensorPowerPin;  // Power pin (-1 if unconnected)
uint8_t      BMEi2c_addr = 0x76;
// The BME280 can be addressed either as 0x77 (Adafruit default) or 0x76 (Grove
// default) Either can be physically mofidied for the other address

// Create a Bosch BME280 sensor object
BoschBME280 bme280(BME280Power, BMEi2c_addr);

// Create four variable pointers for the BME280
Variable* bme280Humid =
    new BoschBME280_Humidity(&bme280, "12345678-abcd-1234-ef00-1234567890ab");
Variable* bme280Temp =
    new BoschBME280_Temp(&bme280, "12345678-abcd-1234-ef00-1234567890ab");
Variable* bme280Press =
    new BoschBME280_Pressure(&bme280, "12345678-abcd-1234-ef00-1234567890ab");
Variable* bme280Alt =
    new BoschBME280_Altitude(&bme280, "12345678-abcd-1234-ef00-1234567890ab");

Campbell OBS3+ Analog Turbidity Sensor

This is the code for the Campbell OBS3+. The Arduino pin controlling power on/off, analog data channel on the TI ADS1115, and calibration values in Volts for Ax^2 + Bx + C are required for the sensor constructor. A custom variable code can be entered as a second argument in the variable constructors, and it is very strongly recommended that you use this otherwise it will be very difficult to determine which return is high and which is low range on the sensor. If your ADD converter is not at the standard address of 0x48, you can enter its actual address as the third argument. Do NOT forget that if you want to give a number of measurements to average, that comes after the i2c address in the constructor!

Note that to access both the high and low range returns, two instances must be created, one at the low range return pin and one at the high pin.

#include <sensors/CampbellOBS3.h>

const int8_t  OBS3Power = sensorPowerPin;  // Power pin (-1 if unconnected)
const uint8_t OBS3NumberReadings = 10;
const uint8_t OBS3ADSi2c_addr    = 0x48;  // The I2C address of the ADS1115 ADC

const int8_t OBSLowADSChannel = 0;  // ADS channel for *low* range output

// Campbell OBS 3+ *Low* Range Calibration in Volts
const float OBSLow_A = 0.000E+00;  // "A" value (X^2) [*low* range]
const float OBSLow_B = 1.000E+00;  // "B" value (X) [*low* range]
const float OBSLow_C = 0.000E+00;  // "C" value [*low* range]

// Create a Campbell OBS3+ *low* range sensor object
CampbellOBS3 osb3low(OBS3Power, OBSLowADSChannel, OBSLow_A, OBSLow_B, OBSLow_C,
                     OBS3ADSi2c_addr, OBS3NumberReadings);

// Create turbidity and voltage variable pointers for the low range  of the OBS3
Variable* obs3TurbLow = new CampbellOBS3_Turbidity(
    &osb3low, "12345678-abcd-1234-ef00-1234567890ab", "TurbLow");
Variable* obs3VoltLow = new CampbellOBS3_Voltage(
    &osb3low, "12345678-abcd-1234-ef00-1234567890ab", "TurbLowV");


const int8_t OBSHighADSChannel = 1;  // ADS channel for *high* range output

// Campbell OBS 3+ *High* Range Calibration in Volts
const float OBSHigh_A = 0.000E+00;  // "A" value (X^2) [*high* range]
const float OBSHigh_B = 1.000E+00;  // "B" value (X) [*high* range]
const float OBSHigh_C = 0.000E+00;  // "C" value [*high* range]

// Create a Campbell OBS3+ *high* range sensor object
CampbellOBS3 osb3high(OBS3Power, OBSHighADSChannel, OBSHigh_A, OBSHigh_B,
                      OBSHigh_C, OBS3ADSi2c_addr, OBS3NumberReadings);

// Create turbidity and voltage variable pointers for the high range of the OBS3
Variable* obs3TurbHigh = new CampbellOBS3_Turbidity(
    &osb3high, "12345678-abcd-1234-ef00-1234567890ab", "TurbHigh");
Variable* obs3VoltHigh = new CampbellOBS3_Voltage(
    &osb3high, "12345678-abcd-1234-ef00-1234567890ab", "TurbHighV");

Decagon ES2 Conductivity and Temperature Sensor

The SDI-12 address of the sensor, the Arduino pin controlling power on/off, and the Arduino pin sending and receiving data are required for the sensor constructor. Optionally, you can include a number of distinct readings to average. The data pin must be a pin that supports pin-change interrupts.

#include <sensors/DecagonES2.h>

const char*   ES2SDI12address = "3";      // The SDI-12 Address of the ES2
const int8_t  ES2Power = sensorPowerPin;  // Power pin (-1 if unconnected)
const int8_t  ES2Data  = 7;               // The SDI12 data pin
const uint8_t ES2NumberReadings = 5;

// Create a Decagon ES2 sensor object
DecagonES2 es2(*ES2SDI12address, ES2Power, ES2Data, ES2NumberReadings);

// Create specific conductance and temperature variable pointers for the ES2
Variable* es2Cond = new DecagonES2_Cond(&es2,
                                        "12345678-abcd-1234-ef00-1234567890ab");
Variable* es2Temp = new DecagonES2_Temp(&es2,
                                        "12345678-abcd-1234-ef00-1234567890ab");

External Voltage via TI ADS1x15

The Arduino pin controlling power on/off and the analog data channel on the TI ADS1115 are required for the sensor constructor. If using a voltage divider to increase the measurable voltage range, enter the gain multiplier as the third argument. If your ADD converter is not at the standard address of 0x48, you can enter its actual address as the fourth argument. The number of measurements to average, if more than one is desired, goes as the fifth argument.

#include <sensors/ExternalVoltage.h>

const int8_t  ADSPower       = sensorPowerPin;  // Power pin (-1 if unconnected)
const int8_t  ADSChannel     = 2;               // The ADS channel of interest
const float   dividerGain    = 10;  //  Gain setting if using a voltage divider
const uint8_t evADSi2c_addr  = 0x48;  // The I2C address of the ADS1115 ADC
const uint8_t VoltReadsToAvg = 1;     // Only read one sample

// Create an External Voltage sensor object
ExternalVoltage extvolt(ADSPower, ADSChannel, dividerGain, evADSi2c_addr,
                        VoltReadsToAvg);

// Create a voltage variable pointer
Variable* extvoltV =
    new ExternalVoltage_Volt(&extvolt, "12345678-abcd-1234-ef00-1234567890ab");

Freescale Semiconductor MPL115A2 Miniature I2C Digital Barometer

The only input needed for the sensor constructor is the Arduino pin controlling power on/off and optionally the number of readings to average. Because this sensor can have only one I2C address (0x60), it is only possible to connect one of these sensors to your system.

#include <sensors/FreescaleMPL115A2.h>

const int8_t  MPLPower = sensorPowerPin;  // Power pin (-1 if unconnected)
const uint8_t MPL115A2ReadingsToAvg = 1;

// Create an MPL115A2 barometer sensor object
MPL115A2 mpl115a2(MPLPower, MPL115A2ReadingsToAvg);

// Create pressure and temperature variable pointers for the MPL
Variable* mplPress =
    new MPL115A2_Pressure(&mpl115a2, "12345678-abcd-1234-ef00-1234567890ab");
Variable* mplTemp = new MPL115A2_Temp(&mpl115a2,
                                      "12345678-abcd-1234-ef00-1234567890ab");

Keller RS485/Modbus Water Level Sensors

The next two sections are for Keller RS485/Modbus water level sensors. The sensor class constructors for each are nearly identical, except for the class name. The sensor constructors require as input: the sensor modbus address, a stream instance for data (ie, Serial), and one or two power pins. The Arduino pin controlling the receive and data enable on your RS485-to-TTL adapter and the number of readings to average are optional. (Use -1 for the second power pin and -1 for the enable pin if these don't apply and you want to average more than one reading.) Please see the section "[Notes on Arduino Streams and Software Serial](https://envirodiy.github.io/ModularSensors/page_arduino_streams.html)" for more information about what streams can be used along with this library. In tests on these sensors, SoftwareSerial_ExtInts did not work to communicate with these sensors, because it isn't stable enough. AltSoftSerial and HardwareSerial work fine.

The serial ports for this example are created in the Creating Extra Serial Ports section and then assigned to modbus functionality in the Assigning Serial Port Functionality section.

Up to two power pins are provided so that the RS485 adapter, the sensor and/or an external power relay can be controlled separately. If the power to everything is controlled by the same pin, use -1 for the second power pin or omit the argument. If they are controlled by different pins and no other sensors are dependent on power from either pin then the order of the pins doesn't matter. If the RS485 adapter, sensor, or relay are controlled by different pins and any other sensors are controlled by the same pins you should put the shared pin first and the un-shared pin second. Both pins cannot be shared pins.

Keller Nanolevel Level Transmitter

#include <sensors/KellerNanolevel.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section

byte nanolevelModbusAddress = 0x01;  // The modbus address of KellerNanolevel
const int8_t nlAdapterPower = sensorPowerPin;  // RS485 adapter power pin
                                               // (-1 if unconnected)
const int8_t  nanolevelPower = A3;             // Sensor power pin
const int8_t  nl485EnablePin = -1;  // Adapter RE/DE pin (-1 if not applicable)
const uint8_t nanolevelNumberReadings = 5;
// The manufacturer recommends taking and averaging a few readings

// Create a Keller Nanolevel sensor object
KellerNanolevel nanolevel(nanolevelModbusAddress, modbusSerial, nlAdapterPower,
                          nanolevelPower, nl485EnablePin,
                          nanolevelNumberReadings);

// Create pressure, temperature, and height variable pointers for the Nanolevel
Variable* nanolevPress = new KellerNanolevel_Pressure(
    &nanolevel, "12345678-abcd-1234-ef00-1234567890ab");
Variable* nanolevTemp = new KellerNanolevel_Temp(
    &nanolevel, "12345678-abcd-1234-ef00-1234567890ab");
Variable* nanolevHeight = new KellerNanolevel_Height(
    &nanolevel, "12345678-abcd-1234-ef00-1234567890ab");

Keller Acculevel High Accuracy Submersible Level Transmitter

#include <sensors/KellerAcculevel.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section

byte acculevelModbusAddress = 0x01;  // The modbus address of KellerAcculevel
const int8_t alAdapterPower = sensorPowerPin;  // RS485 adapter power pin
                                               // (-1 if unconnected)
const int8_t  acculevelPower = A3;             // Sensor power pin
const int8_t  al485EnablePin = -1;  // Adapter RE/DE pin (-1 if not applicable)
const uint8_t acculevelNumberReadings = 5;
// The manufacturer recommends taking and averaging a few readings

// Create a Keller Acculevel sensor object
KellerAcculevel acculevel(acculevelModbusAddress, modbusSerial, alAdapterPower,
                          acculevelPower, al485EnablePin,
                          acculevelNumberReadings);

// Create pressure, temperature, and height variable pointers for the Acculevel
Variable* acculevPress = new KellerAcculevel_Pressure(
    &acculevel, "12345678-abcd-1234-ef00-1234567890ab");
Variable* acculevTemp = new KellerAcculevel_Temp(
    &acculevel, "12345678-abcd-1234-ef00-1234567890ab");
Variable* acculevHeight = new KellerAcculevel_Height(
    &acculevel, "12345678-abcd-1234-ef00-1234567890ab");

Maxbotix HRXL Ultrasonic Range Finder

The Arduino pin controlling power on/off, a stream instance for received data (ie, Serial), and the Arduino pin controlling the trigger are required for the sensor constructor. (Use -1 for the trigger pin if you do not have it connected.) Please see the section "[Notes on Arduino Streams and Software Serial](https://envirodiy.github.io/ModularSensors/page_arduino_streams.html)" for more information about what streams can be used along with this library.

The serial ports for this example are created in the Creating Extra Serial Ports section and then assigned to the sonar functionality in the Assigning Serial Port Functionality section.

#include <sensors/MaxBotixSonar.h>

// A Maxbotix sonar with the trigger pin disconnect CANNOT share the serial port
// A Maxbotix sonar using the trigger may be able to share but YMMV

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section

const int8_t SonarPower =
    sensorPowerPin;  // Excite (power) pin (-1 if unconnected)
const int8_t Sonar1Trigger =
    -1;  // Trigger pin (a unique negative number if unconnected)
const uint8_t sonar1NumberReadings = 3;  // The number of readings to average

// Create a MaxBotix Sonar sensor object
MaxBotixSonar sonar1(sonarSerial, SonarPower, Sonar1Trigger,
                     sonar1NumberReadings);

// Create an ultrasonic range variable pointer
Variable* sonar1Range =
    new MaxBotixSonar_Range(&sonar1, "12345678-abcd-1234-ef00-1234567890ab");

Maxim DS18 One Wire Temperature Sensor

The OneWire hex address of the sensor, the Arduino pin controlling power on/off, and the Arduino pin sending and receiving data are required for the sensor constructor, though the address can be omitted if only one sensor is used. The OneWire address is an array of 8 hex values, for example: {0x28, 0x1D, 0x39, 0x31, 0x2, 0x0, 0x0, 0xF0}. To get the address of your sensor, plug a single sensor into your device and run the oneWireSearch example or the Single example provided within the Dallas Temperature library. The sensor address is programmed at the factory and cannot be changed.

#include <sensors/MaximDS18.h>

// OneWire Address [array of 8 hex characters]
// If only using a single sensor on the OneWire bus, you may omit the address
DeviceAddress OneWireAddress1 = {0x28, 0xFF, 0xBD, 0xBA,
                                 0x81, 0x16, 0x03, 0x0C};
const int8_t  OneWirePower = sensorPowerPin;  // Power pin (-1 if unconnected)
const int8_t  OneWireBus   = A0;  // OneWire Bus Pin (-1 if unconnected)
const int8_t  ds18NumberReadings = 3;

// Create a Maxim DS18 sensor objects (use this form for a known address)
MaximDS18 ds18(OneWireAddress1, OneWirePower, OneWireBus, ds18NumberReadings);

// Create a Maxim DS18 sensor object (use this form for a single sensor on bus
// with an unknown address)
// MaximDS18 ds18(OneWirePower, OneWireBus);

// Create a temperature variable pointer for the DS18
Variable* ds18Temp = new MaximDS18_Temp(&ds18,
                                        "12345678-abcd-1234-ef00-1234567890ab");

Measurement Specialties MS5803-14BA Pressure Sensor

The only input needed is the Arduino pin controlling power on/off; the i2cAddressHex and maximum pressure are optional as is the number of readings to average. Keep in mind that the possible I2C addresses of the MS5803 match those of the BME280.

#include <sensors/MeaSpecMS5803.h>

const int8_t  MS5803Power = sensorPowerPin;  // Power pin (-1 if unconnected)
const uint8_t MS5803i2c_addr =
    0x76;  // The MS5803 can be addressed either as 0x76 (default) or 0x77
const int16_t MS5803maxPressure =
    14;  // The maximum pressure measurable by the specific MS5803 model
const uint8_t MS5803ReadingsToAvg = 1;

// Create a MeaSpec MS5803 pressure and temperature sensor object
MeaSpecMS5803 ms5803(MS5803Power, MS5803i2c_addr, MS5803maxPressure,
                     MS5803ReadingsToAvg);

// Create pressure and temperature variable pointers for the MS5803
Variable* ms5803Press =
    new MeaSpecMS5803_Pressure(&ms5803, "12345678-abcd-1234-ef00-1234567890ab");
Variable* ms5803Temp =
    new MeaSpecMS5803_Temp(&ms5803, "12345678-abcd-1234-ef00-1234567890ab");

Meter SDI-12 Sensors

The next few sections are for Meter SDI-12 sensors. The SDI-12 address of the sensor, the Arduino pin controlling power on/off, and the Arduino pin sending and receiving data are required for the sensor constructor. Optionally, you can include a number of distinct readings to average. The data pin must be a pin that supports pin-change interrupts.

Meter ECH2O Soil Moisture Sensor

#include <sensors/Decagon5TM.h>

const char*  TMSDI12address = "2";             // The SDI-12 Address of the 5-TM
const int8_t TMPower        = sensorPowerPin;  // Power pin (-1 if unconnected)
const int8_t TMData         = 7;               // The SDI12 data pin

// Create a Decagon 5TM sensor object
Decagon5TM fivetm(*TMSDI12address, TMPower, TMData);

// Create the matric potential, volumetric water content, and temperature
// variable pointers for the 5TM
Variable* fivetmEa = new Decagon5TM_Ea(&fivetm,
                                       "12345678-abcd-1234-ef00-1234567890ab");
Variable* fivetmVWC =
    new Decagon5TM_VWC(&fivetm, "12345678-abcd-1234-ef00-1234567890ab");
Variable* fivetmTemp =
    new Decagon5TM_Temp(&fivetm, "12345678-abcd-1234-ef00-1234567890ab");

Meter Hydros 21 Conductivity, Temperature, and Depth Sensor

#include <sensors/MeterHydros21.h>

const char*   hydros21SDI12address = "1";  // The SDI-12 Address of the Hydros21
const uint8_t hydros21NumberReadings = 6;  // The number of readings to average
const int8_t  hydros21Power = sensorPowerPin;  // Power pin (-1 if unconnected)
const int8_t  hydros21Data  = 7;               // The SDI12 data pin

// Create a Decagon Hydros21 sensor object
MeterHydros21 hydros21(*hydros21SDI12address, hydros21Power, hydros21Data,
                       hydros21NumberReadings);

// Create conductivity, temperature, and depth variable pointers for the
// Hydros21
Variable* hydros21Cond =
    new MeterHydros21_Cond(&hydros21, "12345678-abcd-1234-ef00-1234567890ab");
Variable* hydros21Temp =
    new MeterHydros21_Temp(&hydros21, "12345678-abcd-1234-ef00-1234567890ab");
Variable* hydros21Depth =
    new MeterHydros21_Depth(&hydros21, "12345678-abcd-1234-ef00-1234567890ab");

Meter Teros 11 Soil Moisture Sensor

#include <sensors/MeterTeros11.h>

const char*   teros11SDI12address = "4";  // The SDI-12 Address of the Teros 11
const int8_t  terosPower = sensorPowerPin;  // Power pin (-1 if unconnected)
const int8_t  terosData  = 7;               // The SDI12 data pin
const uint8_t teros11NumberReadings = 3;    // The number of readings to average

// Create a METER TEROS 11 sensor object
MeterTeros11 teros11(*teros11SDI12address, terosPower, terosData,
                     teros11NumberReadings);

// Create the matric potential, volumetric water content, and temperature
// variable pointers for the Teros 11
Variable* teros11Ea =
    new MeterTeros11_Ea(&teros11, "12345678-abcd-1234-ef00-1234567890ab");
Variable* teros11Temp =
    new MeterTeros11_Temp(&teros11, "12345678-abcd-1234-ef00-1234567890ab");
Variable* teros11VWC =
    new MeterTeros11_VWC(&teros11, "12345678-abcd-1234-ef00-1234567890ab");

PaleoTerra Redox Sensors

Because older versions of these sensors all ship with the same I2C address, and more than one is frequently used at different soil depths in the same profile, this module has an optional dependence on Testato's SoftwareWire library for software I2C.

To use software I2C, compile with the build flag -D MS_PALEOTERRA_SOFTWAREWIRE. See the software wire section for an example of creating a software I2C instance to share between sensors.

The constructors for the software I2C implementation requires either the SCL and SDA pin numbers or a reference to the I2C object as arguments. All variants of the constructor require the Arduino power pin. The I2C address can be given if it the sensor is not set to the default of 0x68. A number of readings to average can also be given.

#include <sensors/PaleoTerraRedox.h>

int8_t paleoTerraPower = sensorPowerPin;  // Pin to switch RS485 adapter power
                                          // on and off (-1 if unconnected)
uint8_t paleoI2CAddress = 0x68;           // the I2C address of the redox sensor

// Create the PaleoTerra sensor object
#ifdef MS_PALEOTERRA_SOFTWAREWIRE
PaleoTerraRedox ptRedox(&softI2C, paleoTerraPower, paleoI2CAddress);
// PaleoTerraRedox ptRedox(paleoTerraPower, softwareSDA, softwareSCL,
// paleoI2CAddress);
#else
PaleoTerraRedox ptRedox(paleoTerraPower, paleoI2CAddress);
#endif

// Create the voltage variable for the redox sensor
Variable* ptVolt =
    new PaleoTerraRedox_Volt(&ptRedox, "12345678-abcd-1234-ef00-1234567890ab");

Trinket-Based Tipping Bucket Rain Gauge

This is for use with a simple external I2C tipping bucket counter based on the Adafriut Trinket. All constructor arguments are optional, but the first argument is for the I2C address of the tip counter (if not 0x08) and the second is for the depth of rain (in mm) per tip event (if not 0.2mm). Most metric tipping buckets are calibrated to have 1 tip per 0.2mm of rain. Most English tipping buckets are calibrated to have 1 tip per 0.01" of rain, which is 0.254mm. Note that you cannot input a number of measurements to average because averaging does not make sense with this kind of counted variable.

#include <sensors/RainCounterI2C.h>

const uint8_t RainCounterI2CAddress = 0x08;
// I2C Address for EnviroDIY external tip counter; 0x08 by default
const float depthPerTipEvent = 0.2;  // rain depth in mm per tip event

// Create a Rain Counter sensor object
#ifdef MS_RAIN_SOFTWAREWIRE
RainCounterI2C tbi2c(&softI2C, RainCounterI2CAddress, depthPerTipEvent);
// RainCounterI2C tbi2c(softwareSDA, softwareSCL, RainCounterI2CAddress,
//                      depthPerTipEvent);
#else
RainCounterI2C  tbi2c(RainCounterI2CAddress, depthPerTipEvent);
#endif

// Create number of tips and rain depth variable pointers for the tipping bucket
Variable* tbi2cTips =
    new RainCounterI2C_Tips(&tbi2c, "12345678-abcd-1234-ef00-1234567890ab");
Variable* tbi2cDepth =
    new RainCounterI2C_Depth(&tbi2c, "12345678-abcd-1234-ef00-1234567890ab");

Northern Widget Tally Event Counter

This is for use with Northern Widget's Tally event counter

The only option for the constructor is an optional setting for the I2C address, if the counter is not set at the default of 0x33. The counter should be continuously powered.

#include <sensors/TallyCounterI2C.h>

const int8_t TallyPower = -1;  // Power pin (-1 if unconnected)
// NorthernWidget Tally I2CPower is -1 by default because it is often deployed
// with power always on, but Tally also has a super capacitor that enables it
// to be self powered between readings/recharge as described at
// https://github.com/EnviroDIY/Project-Tally

const uint8_t TallyCounterI2CAddress = 0x33;
// NorthernWidget Tally I2C address is 0x33 by default

// Create a Tally Counter sensor object
TallyCounterI2C tallyi2c(TallyPower, TallyCounterI2CAddress);

// Create variable pointers for the Tally event counter
Variable* tallyEvents = new TallyCounterI2C_Events(
    &tallyi2c, "12345678-abcd-1234-ef00-1234567890ab");

// For  Wind Speed, create a Calculated Variable that converts, similar to:
// period = loggingInterval * 60.0;    // in seconds
// frequency = tallyEventCount/period; // average event frequency in Hz
// tallyWindSpeed = frequency * 2.5 * 1.60934;  // in km/h
// 2.5 mph/Hz & 1.60934 kmph/mph and 2.5 mph/Hz conversion factor from
// web: Inspeed-Version-II-Reed-Switch-Anemometer-Sensor-Only-WS2R

TI INA219 High Side Current Sensor

This is the code for the TI INA219 high side current and voltage sensor. The Arduino pin controlling power on/off is all that is required for the constructor. If your INA219 is not at the standard address of 0x40, you can enter its actual address as the fourth argument. The number of measurements to average, if more than one is desired, goes as the fifth argument.

#include <sensors/TIINA219.h>

const int8_t INA219Power    = sensorPowerPin;  // Power pin (-1 if unconnected)
uint8_t      INA219i2c_addr = 0x40;            // 1000000 (Board A0+A1=GND)
// The INA219 can have one of 16 addresses, depending on the connections of A0
// and A1
const uint8_t INA219ReadingsToAvg = 1;

// Create an INA219 sensor object
TIINA219 ina219(INA219Power, INA219i2c_addr, INA219ReadingsToAvg);

// Create current, voltage, and power variable pointers for the INA219
Variable* inaCurrent =
    new TIINA219_Current(&ina219, "12345678-abcd-1234-ef00-1234567890ab");
Variable* inaVolt  = new TIINA219_Volt(&ina219,
                                      "12345678-abcd-1234-ef00-1234567890ab");
Variable* inaPower = new TIINA219_Power(&ina219,
                                        "12345678-abcd-1234-ef00-1234567890ab");

Turner Cyclops-7F Submersible Fluorometer

This is the code for the Turner Cyclops-7F submersible fluorometer. The Arduino pin controlling power on/off and all calibration information is needed for the constructor. The address of the ADS1x15, if it is different than the default of 0x48, can be entered after the calibration information. The number of measurements to average, if more than one is desired, is the last argument.

The Cyclops sensors are NOT pre-calibrated and must be calibrated prior to deployment.

#include <sensors/TurnerCyclops.h>

const int8_t  cyclopsPower = sensorPowerPin;  // Power pin (-1 if unconnected)
const uint8_t cyclopsNumberReadings = 10;
const uint8_t cyclopsADSi2c_addr = 0x48;  // The I2C address of the ADS1115 ADC
const int8_t  cyclopsADSChannel  = 0;     // ADS channel

// Cyclops calibration information
const float cyclopsStdConc = 1.000;  // Concentration of the standard used
                                     // for a 1-point sensor calibration.
const float cyclopsStdVolt =
    1.000;  // The voltage (in volts) measured for the conc_std.
const float cyclopsBlankVolt =
    0.000;  // The voltage (in volts) measured for a blank.

// Create a Turner Cyclops sensor object
TurnerCyclops cyclops(cyclopsPower, cyclopsADSChannel, cyclopsStdConc,
                      cyclopsStdVolt, cyclopsBlankVolt, cyclopsADSi2c_addr,
                      cyclopsNumberReadings);

// Create the voltage variable pointer - used for any type of Cyclops
Variable* cyclopsVoltage =
    new TurnerCyclops_Voltage(&cyclops, "12345678-abcd-1234-ef00-1234567890ab");

// Create the variable pointer for the primary output parameter.  Only use
// **ONE** of these!  Which is possible depends on your specific sensor!
Variable* cyclopsChloro = new TurnerCyclops_Chlorophyll(
    &cyclops, "12345678-abcd-1234-ef00-1234567890ab");
Variable* cyclopsRWT = new TurnerCyclops_Rhodamine(
    &cyclops, "12345678-abcd-1234-ef00-1234567890ab");
Variable* cyclopsFluoroscein = new TurnerCyclops_Fluorescein(
    &cyclops, "12345678-abcd-1234-ef00-1234567890ab");
Variable* cyclopsPhycocyanin = new TurnerCyclops_Phycocyanin(
    &cyclops, "12345678-abcd-1234-ef00-1234567890ab");
Variable* cyclopsPhycoerythrin = new TurnerCyclops_Phycoerythrin(
    &cyclops, "12345678-abcd-1234-ef00-1234567890ab");
Variable* cyclopsCDOM =
    new TurnerCyclops_CDOM(&cyclops, "12345678-abcd-1234-ef00-1234567890ab");
Variable* cyclopsCrudeOil = new TurnerCyclops_CrudeOil(
    &cyclops, "12345678-abcd-1234-ef00-1234567890ab");
Variable* cyclopsBrighteners = new TurnerCyclops_Brighteners(
    &cyclops, "12345678-abcd-1234-ef00-1234567890ab");
Variable* cyclopsTurbidity = new TurnerCyclops_Turbidity(
    &cyclops, "12345678-abcd-1234-ef00-1234567890ab");
Variable* cyclopsPTSA =
    new TurnerCyclops_PTSA(&cyclops, "12345678-abcd-1234-ef00-1234567890ab");
Variable* cyclopsBTEX =
    new TurnerCyclops_BTEX(&cyclops, "12345678-abcd-1234-ef00-1234567890ab");
Variable* cyclopsTryptophan = new TurnerCyclops_Tryptophan(
    &cyclops, "12345678-abcd-1234-ef00-1234567890ab");
Variable* cyclopsRedChloro = new TurnerCyclops_RedChlorophyll(
    &cyclops, "12345678-abcd-1234-ef00-1234567890ab");

Analog Electrical Conductivity using the Processor's Analog Pins

This is the code for the measuring electrical conductivity using the processor's internal ADC and analog input pins. The Arduino pin controlling power on/off and the sensing pin are required for the constuctor. The power supply for the sensor absolutely must be switched on and off between readings! The resistance of your in-circuit resistor, the cell constant for your power cord, and the number of measurements to average are the optional third, fourth, and fifth arguments. If your processor has an ADS with resolution greater or less than 10-bit, compile with the build flag -D ANALOG_EC_ADC_RESOLUTION=##. For best results, you should also connect the AREF pin of your processors ADC to the power supply for the and compile with the build flag -D ANALOG_EC_ADC_REFERENCE_MODE=EXTERNAL.

#include <sensors/AnalogElecConductivity.h>

const int8_t ECpwrPin   = A4;  // Power pin (-1 if unconnected)
const int8_t ECdataPin1 = A0;  // Data pin (must be an analog pin, ie A#)

// Create an Analog Electrical Conductivity sensor object
AnalogElecConductivity analogEC_phy(ECpwrPin, ECdataPin1);

// Create a conductivity variable pointer for the analog sensor
Variable* analogEc_cond = new AnalogElecConductivity_EC(
    &analogEC_phy, "12345678-abcd-1234-ef00-1234567890ab");

// Create a calculated variable for the temperature compensated conductivity
// (that is, the specific conductance).  For this example, we will use the
// temperature measured by the Maxim DS18 saved as ds18Temp several sections
// above this.  You could use the temperature returned by any other water
// temperature sensor if desired.  **DO NOT** use your logger board temperature
// (ie, from the DS3231) to calculate specific conductance!
float calculateAnalogSpCond(void) {
    float spCond          = -9999;  // Always safest to start with a bad value
    float waterTemp       = ds18Temp->getValue();
    float rawCond         = analogEc_cond->getValue();
    float temperatureCoef = 0.019;
    // ^^ Linearized temperature correction coefficient per degrees Celsius.
    // The value of 0.019 comes from measurements reported here:
    // Hayashi M. Temperature-electrical conductivity relation of water for
    // environmental monitoring and geophysical data inversion. Environ Monit
    // Assess. 2004 Aug-Sep;96(1-3):119-28.
    // doi: 10.1023/b:emas.0000031719.83065.68. PMID: 15327152.
    if (waterTemp != -9999 && rawCond != -9999) {
        // make sure both inputs are good
        spCond = rawCond / (1 + temperatureCoef * (waterTemp - 25.0));
    }
    return spCond;
}

// Properties of the calculated variable
// The number of digits after the decimal place
const uint8_t analogSpCondResolution = 0;
// This must be a value from http://vocabulary.odm2.org/variablename/
const char* analogSpCondName = "specificConductance";
// This must be a value from http://vocabulary.odm2.org/units/
const char* analogSpCondUnit = "microsiemenPerCentimeter";
// A short code for the variable
const char* analogSpCondCode = "anlgSpCond";
// The (optional) universallly unique identifier
const char* analogSpCondUUID = "12345678-abcd-1234-ef00-1234567890ab";

// Finally, Create the specific conductance variable and return a pointer to it
Variable* analogEc_spcond = new Variable(
    calculateAnalogSpCond, analogSpCondResolution, analogSpCondName,
    analogSpCondUnit, analogSpCondCode, analogSpCondUUID);

Yosemitech RS485/Modbus Environmental Sensors

The next several sections are for Yosemitech brand sensors. The sensor class constructors for each are nearly identical, except for the class name. The sensor constructor requires as input: the sensor modbus address, a stream instance for data (ie, Serial), and one or two power pins. The Arduino pin controlling the receive and data enable on your RS485-to-TTL adapter and the number of readings to average are optional. (Use -1 for the second power pin and -1 for the enable pin if these don't apply and you want to average more than one reading.) For most of the sensors, Yosemitech strongly recommends averaging multiple (in most cases 10) readings for each measurement. Please see the section "[Notes on Arduino Streams and Software Serial](https://envirodiy.github.io/ModularSensors/page_arduino_streams.html)" for more information about what streams can be used along with this library. In tests on these sensors, SoftwareSerial_ExtInts did not work to communicate with these sensors, because it isn't stable enough. AltSoftSerial and HardwareSerial work fine. NeoSWSerial is a bit hit or miss, but can be used in a pinch.

The serial ports for this example are created in the Creating Extra Serial Ports section and then assigned to modbus functionality in the Assigning Serial Port Functionality section.

Yosemitech Y504 Dissolved Oxygen Sensor

#include <sensors/YosemitechY504.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section

byte         y504ModbusAddress = 0x04;  // The modbus address of the Y504
const int8_t y504AdapterPower  = sensorPowerPin;  // RS485 adapter power pin
                                                  // (-1 if unconnected)
const int8_t  y504SensorPower = A3;               // Sensor power pin
const int8_t  y504EnablePin   = -1;  // Adapter RE/DE pin (-1 if not applicable)
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, y504AdapterPower,
                    y504SensorPower, y504EnablePin, 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");

Yosemitech Y510 Turbidity Sensor

#include <sensors/YosemitechY510.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section

byte         y510ModbusAddress = 0x0B;  // The modbus address of the Y510
const int8_t y510AdapterPower  = sensorPowerPin;  // RS485 adapter power pin
                                                  // (-1 if unconnected)
const int8_t  y510SensorPower = A3;               // Sensor power pin
const int8_t  y510EnablePin   = -1;  // Adapter RE/DE pin (-1 if not applicable)
const uint8_t y510NumberReadings = 5;
// The manufacturer recommends averaging 10 readings, but we take 5 to minimize
// power consumption

// Create a Y510-B Turbidity sensor object
YosemitechY510 y510(y510ModbusAddress, modbusSerial, y510AdapterPower,
                    y510SensorPower, y510EnablePin, y510NumberReadings);

// Create turbidity and temperature variable pointers for the Y510
Variable* y510Turb =
    new YosemitechY510_Turbidity(&y510, "12345678-abcd-1234-ef00-1234567890ab");
Variable* y510Temp =
    new YosemitechY510_Temp(&y510, "12345678-abcd-1234-ef00-1234567890ab");

Yosemitech Y511 Turbidity Sensor with Wiper

#include <sensors/YosemitechY511.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section

byte         y511ModbusAddress = 0x1A;  // The modbus address of the Y511
const int8_t y511AdapterPower  = sensorPowerPin;  // RS485 adapter power pin
                                                  // (-1 if unconnected)
const int8_t  y511SensorPower = A3;               // Sensor power pin
const int8_t  y511EnablePin   = -1;  // Adapter RE/DE pin (-1 if not applicable)
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, y511AdapterPower,
                    y511SensorPower, y511EnablePin, 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");

Yosemitech Y514 Chlorophyll Sensor

#include <sensors/YosemitechY514.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section

byte         y514ModbusAddress = 0x14;  // The modbus address of the Y514
const int8_t y514AdapterPower =
    sensorPowerPin;  // RS485 adapter power pin (-1 if unconnected)
const int8_t  y514SensorPower = A3;  // Sensor power pin
const int8_t  y514EnablePin   = -1;  // Adapter RE/DE pin (-1 if not applicable)
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, y514AdapterPower,
                    y514SensorPower, y514EnablePin, 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");

Yosemitech Y520 Conductivity Sensor

#include <sensors/YosemitechY520.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section

byte         y520ModbusAddress = 0x20;  // The modbus address of the Y520
const int8_t y520AdapterPower  = sensorPowerPin;  // RS485 adapter power pin
                                                  // (-1 if unconnected)
const int8_t  y520SensorPower = A3;               // Sensor power pin
const int8_t  y520EnablePin   = -1;  // Adapter RE/DE pin (-1 if not applicable)
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, y520AdapterPower,
                    y520SensorPower, y520EnablePin, 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");

Yosemitech Y532 pH Sensor

#include <sensors/YosemitechY532.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section

byte         y532ModbusAddress = 0x32;  // The modbus address of the Y532
const int8_t y532AdapterPower  = sensorPowerPin;  // RS485 adapter power pin
                                                  // (-1 if unconnected)
const int8_t  y532SensorPower = A3;               // Sensor power pin
const int8_t  y532EnablePin   = 4;  // Adapter RE/DE pin (-1 if not applicable)
const uint8_t y532NumberReadings = 1;
// The manufacturer actually doesn't mention averaging for this one

// Create a Yosemitech Y532 pH sensor object
YosemitechY532 y532(y532ModbusAddress, modbusSerial, y532AdapterPower,
                    y532SensorPower, y532EnablePin, y532NumberReadings);

// Create pH, electrical potential, and temperature variable pointers for the
// Y532
Variable* y532Voltage =
    new YosemitechY532_Voltage(&y532, "12345678-abcd-1234-ef00-1234567890ab");
Variable* y532pH =
    new YosemitechY532_pH(&y532, "12345678-abcd-1234-ef00-1234567890ab");
Variable* y532Temp =
    new YosemitechY532_Temp(&y532, "12345678-abcd-1234-ef00-1234567890ab");

Yosemitech Y533 Oxidation Reduction Potential (ORP) Sensor

#include <sensors/YosemitechY533.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section

byte         y533ModbusAddress = 0x32;  // The modbus address of the Y533
const int8_t y533AdapterPower  = sensorPowerPin;  // RS485 adapter power pin
                                                  // (-1 if unconnected)
const int8_t  y533SensorPower = A3;               // Sensor power pin
const int8_t  y533EnablePin   = 4;  // Adapter RE/DE pin (-1 if not applicable)
const uint8_t y533NumberReadings = 1;
// The manufacturer actually doesn't mention averaging for this one

// Create a Yosemitech Y533 ORP sensor object
YosemitechY533 y533(y533ModbusAddress, modbusSerial, y533AdapterPower,
                    y533SensorPower, y533EnablePin, y533NumberReadings);

// Create ORP and temperature variable pointers for the Y533
Variable* y533ORP =
    new YosemitechY533_ORP(&y533, "12345678-abcd-1234-ef00-1234567890ab");
Variable* y533Temp =
    new YosemitechY533_Temp(&y533, "12345678-abcd-1234-ef00-1234567890ab");

Yosemitech Y550 Carbon Oxygen Demand (COD) Sensor with Wiper

#include <sensors/YosemitechY550.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section

byte         y550ModbusAddress = 0x50;  // The modbus address of the Y550
const int8_t y550AdapterPower  = sensorPowerPin;  // RS485 adapter power pin
                                                  // (-1 if unconnected)
const int8_t  y550SensorPower = A3;               // Sensor power pin
const int8_t  y550EnablePin   = -1;  // Adapter RE/DE pin (-1 if not applicable)
const uint8_t y550NumberReadings = 5;
// The manufacturer recommends averaging 10 readings, but we take 5 to minimize
// power consumption

// Create a Y550 chemical oxygen demand sensor object
YosemitechY550 y550(y550ModbusAddress, modbusSerial, y550AdapterPower,
                    y550SensorPower, y550EnablePin, y550NumberReadings);

// Create COD, turbidity, and temperature variable pointers for the Y550
Variable* y550COD =
    new YosemitechY550_COD(&y550, "12345678-abcd-1234-ef00-1234567890ab");
Variable* y550Turbid =
    new YosemitechY550_Turbidity(&y550, "12345678-abcd-1234-ef00-1234567890ab");
Variable* y550Temp =
    new YosemitechY550_Temp(&y550, "12345678-abcd-1234-ef00-1234567890ab");

Yosemitech Y4000 Multi-Parameter Sonde

#include <sensors/YosemitechY4000.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section

byte         y4000ModbusAddress = 0x05;  // The modbus address of the Y4000
const int8_t y4000AdapterPower  = sensorPowerPin;  // RS485 adapter power pin
                                                   // (-1 if unconnected)
const int8_t  y4000SensorPower = A3;               // Sensor power pin
const int8_t  y4000EnablePin = -1;  // Adapter RE/DE pin (-1 if not applicable)
const uint8_t y4000NumberReadings = 5;
// The manufacturer recommends averaging 10 readings, but we take 5 to minimize
// power consumption

// Create a Yosemitech Y4000 multi-parameter sensor object
YosemitechY4000 y4000(y4000ModbusAddress, modbusSerial, y4000AdapterPower,
                      y4000SensorPower, y4000EnablePin, y4000NumberReadings);

// Create all of the variable pointers for the Y4000
Variable* y4000DO =
    new YosemitechY4000_DOmgL(&y4000, "12345678-abcd-1234-ef00-1234567890ab");
Variable* y4000Turb = new YosemitechY4000_Turbidity(
    &y4000, "12345678-abcd-1234-ef00-1234567890ab");
Variable* y4000Cond =
    new YosemitechY4000_Cond(&y4000, "12345678-abcd-1234-ef00-1234567890ab");
Variable* y4000pH =
    new YosemitechY4000_pH(&y4000, "12345678-abcd-1234-ef00-1234567890ab");
Variable* y4000Temp =
    new YosemitechY4000_Temp(&y4000, "12345678-abcd-1234-ef00-1234567890ab");
Variable* y4000ORP =
    new YosemitechY4000_ORP(&y4000, "12345678-abcd-1234-ef00-1234567890ab");
Variable* y4000Chloro = new YosemitechY4000_Chlorophyll(
    &y4000, "12345678-abcd-1234-ef00-1234567890ab");
Variable* y4000BGA =
    new YosemitechY4000_BGA(&y4000, "12345678-abcd-1234-ef00-1234567890ab");

Zebra Tech D-Opto Dissolved Oxygen Sensor

The SDI-12 address of the sensor, the Arduino pin controlling power on/off, and the Arduino pin sending and receiving data are required for the sensor constructor. Optionally, you can include a number of distinct readings to average. The data pin must be a pin that supports pin-change interrupts.

#include <sensors/ZebraTechDOpto.h>

const char*  DOptoSDI12address = "5";   // The SDI-12 Address of the D-Opto
const int8_t ZTPower = sensorPowerPin;  // Power pin (-1 if unconnected)
const int8_t ZTData  = 7;               // The SDI12 data pin

// Create a Zebra Tech DOpto dissolved oxygen sensor object
ZebraTechDOpto dopto(*DOptoSDI12address, ZTPower, ZTData);

// Create dissolved oxygen percent, dissolved oxygen concentration, and
// temperature variable pointers for the Zebra Tech
Variable* dOptoDOpct =
    new ZebraTechDOpto_DOpct(&dopto, "12345678-abcd-1234-ef00-1234567890ab");
Variable* dOptoDOmgL =
    new ZebraTechDOpto_DOmgL(&dopto, "12345678-abcd-1234-ef00-1234567890ab");
Variable* dOptoTemp =
    new ZebraTechDOpto_Temp(&dopto, "12345678-abcd-1234-ef00-1234567890ab");

Calculated Variables

Create new Variable objects calculated from the measured variables. For these calculate variables, we must not only supply a function for the calculation, but also all of the metadata about the variable - like the name of the variable and its units.

// Create the function to give your calculated result.
// The function should take no input (void) and return a float.
// You can use any named variable pointers to access values by way of
// variable->getValue()

float calculateVariableValue(void) {
    float calculatedResult = -9999;  // Always safest to start with a bad value
    // float inputVar1 = variable1->getValue();
    // float inputVar2 = variable2->getValue();
    // make sure both inputs are good
    // if (inputVar1 != -9999 && inputVar2 != -9999) {
    //     calculatedResult = inputVar1 + inputVar2;
    // }
    return calculatedResult;
}

// Properties of the calculated variable
// The number of digits after the decimal place
const uint8_t calculatedVarResolution = 3;
// This must be a value from http://vocabulary.odm2.org/variablename/
const char* calculatedVarName = "varName";
// This must be a value from http://vocabulary.odm2.org/units/
const char* calculatedVarUnit = "varUnit";
// A short code for the variable
const char* calculatedVarCode = "calcVar";
// The (optional) universallly unique identifier
const char* calculatedVarUUID = "12345678-abcd-1234-ef00-1234567890ab";

// Finally, Create a calculated variable and return a variable pointer to it
Variable* calculatedVar = new Variable(
    calculateVariableValue, calculatedVarResolution, calculatedVarName,
    calculatedVarUnit, calculatedVarCode, calculatedVarUUID);

Creating the array, logger, publishers

The variable array

Create a VariableArray containing all of the Variable objects that we are logging the values of.

This shows three differnt ways of creating the same variable array and filling it with variables. You should only use ONE of these in your own code

Creating Variables within an Array

Here we use the new keyword to create multiple variables and get pointers to them all at the same time within the arry.

// Version 1: Create pointers for all of the variables from the sensors,
// at the same time putting them into an array
Variable* variableList[] = {
    new ProcessorStats_SampleNumber(&mcuBoard,
                                    "12345678-abcd-1234-ef00-1234567890ab"),
    new ProcessorStats_FreeRam(&mcuBoard,
                               "12345678-abcd-1234-ef00-1234567890ab"),
    new ProcessorStats_Battery(&mcuBoard,
                               "12345678-abcd-1234-ef00-1234567890ab"),
    new MaximDS3231_Temp(&ds3231, "12345678-abcd-1234-ef00-1234567890ab"),
    //  ... Add more variables as needed!
    new Modem_RSSI(&modem, "12345678-abcd-1234-ef00-1234567890ab"),
    new Modem_SignalPercent(&modem, "12345678-abcd-1234-ef00-1234567890ab"),
    new Modem_Temp(&modem, "12345678-abcd-1234-ef00-1234567890ab"),
    new Variable(calculateVariableValue, calculatedVarResolution,
                 calculatedVarName, calculatedVarUnit, calculatedVarCode,
                 calculatedVarUUID),
};
// Count up the number of pointers in the array
int variableCount = sizeof(variableList) / sizeof(variableList[0]);
// Create the VariableArray object
VariableArray varArray(variableCount, variableList);

Using Already-Created Variables

If you are sending data to monitor my watershed, it is much easier to create the variables in an array and then to paste the UUID's all together as copied from the "View Token UUID List" link for a site. If using this method, be very, very, very careful to make sure the order of your variables exactly matches the order of your UUID's.

// Version 2: Create two separate arrays, on for the variables and a separate
// one for the UUID's, then give both as input to the variable array
// constructor.  Be cautious when doing this though because order is CRUCIAL!
Variable* variableList[] = {
    new ProcessorStats_SampleNumber(&mcuBoard),
    new ProcessorStats_FreeRam(&mcuBoard),
    new ProcessorStats_Battery(&mcuBoard),
    new MaximDS3231_Temp(&ds3231),
    //  ... Add all of your variables!
    new Modem_RSSI(&modem),
    new Modem_SignalPercent(&modem),
    new Modem_Temp(&modem),
    new Variable(calculateVariableValue, calculatedVarResolution,
                 calculatedVarName, calculatedVarUnit, calculatedVarCode),
};
const char* UUIDs[] = {
    "12345678-abcd-1234-ef00-1234567890ab",
    //  ... The number of UUID's must match the number of variables!
    "12345678-abcd-1234-ef00-1234567890ab",
};
// Count up the number of pointers in the array
int variableCount = sizeof(variableList) / sizeof(variableList[0]);
// Create the VariableArray object and attach the UUID's
VariableArray varArray(variableCount, variableList, UUIDs);

Using Already-Created Variables

You can also create and name variable pointer objects outside of the array (as is demonstrated in all of the code chunks here) and then reference those pointers inside of the array like so:

// Version 3: Fill array with already created and named variable pointers
Variable* variableList[] = {
    mcuBoardSampNo,
    mcuBoardAvailableRAM,
    mcuBoardBatt,
    calculatedVar,
#if defined ARDUINO_ARCH_AVR || defined MS_SAMD_DS3231
    ds3231Temp,
#endif
#if defined MS_BUILD_SENSOR_AM2315
    am2315Humid,
    am2315Temp,
#endif
#if defined MS_BUILD_SENSOR_DHT
    dhtHumid,
    dhtTemp,
    dhtHI,
#endif
#if defined MS_BUILD_SENSOR_SQ212
    sq212PAR,
    sq212voltage,
#endif
#if defined MS_BUILD_SENSOR_ATLASCO2
    atlasCO2CO2,
    atlasCO2Temp,
#endif
#if defined MS_BUILD_SENSOR_ATLASDO
    atlasDOconc,
    atlasDOpct,
#endif
#if defined MS_BUILD_SENSOR_ATLASORP
    atlasORPot,
#endif
#if defined MS_BUILD_SENSOR_ATLASPH
    atlaspHpH,
#endif
#if defined MS_BUILD_SENSOR_ATLASRTD
    atlasTemp,
#endif
#if defined MS_BUILD_SENSOR_ATLASEC
    atlasCond,
    atlasTDS,
    atlasSal,
    atlasGrav,
    atlasSpCond,
#endif
#if defined MS_BUILD_SENSOR_BME280
    bme280Temp,
    bme280Humid,
    bme280Press,
    bme280Alt,
#endif
#if defined MS_BUILD_SENSOR_OBS3
    obs3TurbLow,
    obs3VoltLow,
    obs3TurbHigh,
    obs3VoltHigh,
#endif
#if defined MS_BUILD_SENSOR_CTD
    ctdCond,
    ctdTemp,
    ctdDepth,
#endif
#if defined MS_BUILD_SENSOR_ES2
    es2Cond,
    es2Temp,
#endif
#if defined MS_BUILD_SENSOR_VOLTAGE
    extvoltV,
#endif
#if defined MS_BUILD_SENSOR_MPL115A2
    mplTemp,
    mplPress,
#endif
#if defined MS_BUILD_SENSOR_INSITURDO
    rdoTemp,
    rdoDOpct,
    rdoDOmgL,
    rdoO2pp,
#endif
#if defined MS_BUILD_SENSOR_ACCULEVEL
    acculevPress,
    acculevTemp,
    acculevHeight,
#endif
#if defined MS_BUILD_SENSOR_NANOLEVEL
    nanolevPress,
    nanolevTemp,
    nanolevHeight,
#endif
#if defined MS_BUILD_SENSOR_MAXBOTIX
    sonar1Range,
#endif
#if defined MS_BUILD_SENSOR_DS18
    ds18Temp,
#endif
#if defined MS_BUILD_SENSOR_MS5803
    ms5803Temp,
    ms5803Press,
#endif
#if defined MS_BUILD_SENSOR_5TM
    fivetmEa,
    fivetmVWC,
    fivetmTemp,
#endif
#if defined MS_BUILD_SENSOR_HYDROS21
    hydros21Cond,
    hydros21Temp,
    hydros21Depth,
#endif
#if defined MS_BUILD_SENSOR_TEROS11
    teros11Ea,
    teros11Temp,
    teros11VWC,
#endif
#if defined MS_BUILD_SENSOR_PALEOTERRA
    ptVolt,
#endif
#if defined MS_BUILD_SENSOR_RAINI2C
    tbi2cTips,
    tbi2cDepth,
#endif
#if defined MS_BUILD_SENSOR_TALLY
    tallyEvents,
#endif
#if defined MS_BUILD_SENSOR_INA219
    inaVolt,
    inaCurrent,
    inaPower,
#endif
#if defined MS_BUILD_SENSOR_CYCLOPS
    cyclopsVoltage,
    cyclopsChloro,
    cyclopsRWT,
    cyclopsFluoroscein,
    cyclopsPhycocyanin,
    cyclopsPhycoerythrin,
    cyclopsCDOM,
    cyclopsCrudeOil,
    cyclopsBrighteners,
    cyclopsTurbidity,
    cyclopsPTSA,
    cyclopsBTEX,
    cyclopsTryptophan,
    cyclopsRedChloro,
#endif
#if defined MS_BUILD_SENSOR_ANALOGEC
    analogEc_cond,
    analogEc_spcond,
#endif
#if defined MS_BUILD_SENSOR_Y504
    y504DOpct,
    y504DOmgL,
    y504Temp,
#endif
#if defined MS_BUILD_SENSOR_Y510
    y510Turb,
    y510Temp,
#endif
#if defined MS_BUILD_SENSOR_Y511
    y511Turb,
    y511Temp,
#endif
#if defined MS_BUILD_SENSOR_Y514
    y514Chloro,
    y514Temp,
#endif
#if defined MS_BUILD_SENSOR_Y520
    y520Cond,
    y520Temp,
#endif
#if defined MS_BUILD_SENSOR_Y532
    y532Voltage,
    y532pH,
    y532Temp,
#endif
#if defined MS_BUILD_SENSOR_Y533
    y533ORP,
    y533Temp,
#endif
#if defined MS_BUILD_SENSOR_Y550
    y550COD,
    y550Turbid,
    y550Temp,
#endif
#if defined MS_BUILD_SENSOR_Y4000
    y4000DO,
    y4000Turb,
    y4000Cond,
    y4000pH,
    y4000Temp,
    y4000ORP,
    y4000Chloro,
    y4000BGA,
#endif
#if defined MS_BUILD_SENSOR_DOPTO
    dOptoDOpct,
    dOptoDOmgL,
    dOptoTemp,
#endif
    modemRSSI,
    modemSignalPct,
#ifdef TINY_GSM_MODEM_HAS_BATTERY
    modemBatteryState,
    modemBatteryPct,
    modemBatteryVoltage,
#endif
#ifdef TINY_GSM_MODEM_HAS_TEMPERATURE
    modemTemperature,
#endif
};
// Count up the number of pointers in the array
int variableCount = sizeof(variableList) / sizeof(variableList[0]);
// Create the VariableArray object
VariableArray varArray(variableCount, variableList);

The Logger Object

Now that we've created the array, we can actually create the Logger object.

// Create a new logger instance
Logger dataLogger(LoggerID, loggingInterval, &varArray);

Data Publisher

Here we set up all three possible data publisers and link all of them to the same Logger object.

Monitor My Watershed

To publish data to the Monitor My Watershed / EnviroDIY Data Sharing Portal first you must register yourself as a user at https://monitormywatershed.org or https://data.envirodiy.org. Then you must register your site. After registering your site, a sampling feature and registration token for that site should be visible on the site page.

// Device registration and sampling feature information can be obtained after
// registration at https://monitormywatershed.org or https://data.envirodiy.org
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
#include <publishers/EnviroDIYPublisher.h>
EnviroDIYPublisher EnviroDIYPOST(dataLogger, &modem.gsmClient,
                                 registrationToken, samplingFeature);

DreamHost

It is extrmemly unlikely you will use this. You should ignore this section.

// NOTE:  This is an outdated data collection tool used by the Stroud Center.
// It very, very unlikely that you will use this.

const char* DreamHostPortalRX = "xxxx";

// Create a data publisher to DreamHost
#include <publishers/DreamHostPublisher.h>
DreamHostPublisher DreamHostGET(dataLogger, &modem.gsmClient,
                                DreamHostPortalRX);

ThingSpeak

After you have set up channels on ThingSpeak, you can use this code to publish your data to it.

Keep in mind that the order of variables in the VariableArray is crucial when publishing to ThingSpeak.

// NOTE:  This is an outdated data collection tool used by the Stroud Center.
// It very, very unlikely that you will use this.

const char* DreamHostPortalRX = "xxxx";

// Create a data publisher to DreamHost
#include <publishers/DreamHostPublisher.h>
DreamHostPublisher DreamHostGET(dataLogger, &modem.gsmClient,
                                DreamHostPortalRX);

Extra Working Functions

Here we're creating a few extra functions on the global scope. The flash function is used at board start up just to give an indication that the board has restarted. The battery function calls the ProcessorStats sensor to check the battery level before attempting to log or publish data.

// 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);
        delay(rate);
        digitalWrite(greenLED, LOW);
        digitalWrite(redLED, HIGH);
        delay(rate);
    }
    digitalWrite(redLED, LOW);
}

// Uses the processor sensor object to read 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];
}

Arduino Setup Function

This is our setup function. In Arduino coding, the classic "main" function is replaced by two functions: setup() and loop(). The setup() function runs once when the board boots or restarts. It usually contains many functions that set the mode of input and output pins and prints out some debugging information to the serial port. These functions are frequently named "begin". Because we have a lot of parts to set up, there's a lot going on in this function!

Let's break it down.

Starting the Function

First we just open the function definitions:

void setup() {

Wait for USB

Next we wait for the USB debugging port to initialize. This only applies to SAMD and 32U4 boards that have built-in USB support. This code should not be used for deployed loggers; it's only for using a USB for debugging.

// 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
#if defined SERIAL_PORT_USBVIRTUAL
    while (!SERIAL_PORT_USBVIRTUAL && (millis() < 10000L)) {}
#endif

Printing a Hello

Next we print a message out to the debugging port. This is also just for debugging - it's very helpful when connected to the logger via USB to see a clear indication that the board is starting

    // Start the primary serial connection
    Serial.begin(serialBaud);

    // Print a start-up note to the first serial port
    Serial.print(F("\n\nNow running "));
    Serial.print(sketchName);
    Serial.print(F(" on Logger "));
    Serial.println(LoggerID);
    Serial.println();

    Serial.print(F("Using ModularSensors Library version "));
    Serial.println(MODULAR_SENSORS_VERSION);
    Serial.print(F("TinyGSM Library version "));
    Serial.println(TINYGSM_VERSION);
    Serial.println();

Serial Interrupts

If we're using either NeoSWSerial or SoftwareSerial_ExtInts we need to assign the data receiver pins to interrupt functionality here in the setup.

The NeoSWSerial and SoftwareSerial_ExtInts objects were created way up in the Extra Serial Ports section.

NOTE:** If you create more than one NeoSWSerial or Software serial object, you need to call the enableInterrupt function for each Rx pin!

For NeoSWSerial we use: enableInterrupt(neoSSerial1Rx, neoSSerial1ISR, CHANGE);

For SoftwareSerial with External interrupts we use:

enableInterrupt(softSerialRx, SoftwareSerial_ExtInts::handle_interrupt,
                CHANGE);

Serial Begin

Every serial port setup and used in the program must be "begun" in the setup function. This section calls the begin functions for all of the various ports defined in the Extra Serial Ports section

    // Start the serial connection with the modem
    modemSerial.begin(modemBaud);

    // Start the stream for the modbus sensors;
    // all currently supported modbus sensors use 9600 baud
    modbusSerial.begin(9600);

#if defined MS_BUILD_SENSOR_MAXBOTIX
    // Start the SoftwareSerial stream for the sonar; it will always be at 9600
    // baud
    sonarSerial.begin(9600);
#endif

SAMD Pin Peripherals

After beginning all of the serial ports, we need to set the pin peripheral settings for any SERCOM's we assigned to serial functionality on the SAMD boards. These were created in the Extra Serial Ports section above. This does not need to be done for an AVR board (like the Mayfly).

#if defined ARDUINO_ARCH_SAMD
#ifndef ENABLE_SERIAL2
    pinPeripheral(10, PIO_SERCOM);  // Serial2 Tx/Dout = SERCOM1 Pad #2
    pinPeripheral(11, PIO_SERCOM);  // Serial2 Rx/Din = SERCOM1 Pad #0
#endif
#ifndef ENABLE_SERIAL3
    pinPeripheral(2, PIO_SERCOM);  // Serial3 Tx/Dout = SERCOM2 Pad #2
    pinPeripheral(5, PIO_SERCOM);  // Serial3 Rx/Din = SERCOM2 Pad #3
#endif
#endif

Flash the LEDs

Like printing debugging information to the serial port, flashing the board LED's is a very helpful indication that the board just restarted. Here we set the pin modes for the LED pins and flash them back and forth using the greenredflash() function we created back in the working functions section.

    // 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
    greenredflash();

Begin the Logger

Next get ready and begin the logger. We set the logger time zone and the clock time zone. The clock time zone is what the RTC will report; the logger time zone is what will be written to the SD card and all data publishers. The values are set with the Logger:: prefix because they are static variables of the Logger class rather than member variables. Here we also tie the logger and modem together and set all the logger pins. Then we finally run the logger's begin function.

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

    // Attach the modem and information pins to the logger
    dataLogger.attachModem(modem);
    modem.setModemLED(modemLEDPin);
    dataLogger.setLoggerPins(wakePin, sdCardSSPin, sdCardPwrPin, buttonPin,
                             greenLED);

    // Begin the logger
    dataLogger.begin();

Setup the Sensors

After beginning the logger, we setup all the sensors. Unlike all the previous chuncks of the setup that are preparation steps only requiring the mcu processor, this might involve powering up the sensors. To prevent a low power restart loop, we put a battery voltage condition on the sensor setup. This prevents a solar powered board whose battery has died from continuously restarting as soon as it gains any power on sunrise. Without the condition the board would boot with power, try to power hungry sensors, brown out, and restart over and over.

    // Note:  Please change these battery voltages to match your battery
    // Set up the sensors, except at lowest battery level
    if (getBatteryVoltage() > 3.4) {
        Serial.println(F("Setting up sensors..."));
        varArray.setupSensors();
    }

Custom Modem Setup

Next we can opt to do some special setup needed for a few of the modems. You should only use the one chunk that applies to your specific modem configuration and delete the others.

ESP8266 Baud Rate

This chunk of code reduces the baud rate of the ESP8266 from its default of 115200 to 9600. This is only needed for 8MHz boards (like the Mayfly) that cannot communicate at 115200 baud.

    if (modemBaud > 57600) {
        modem.modemWake();  // NOTE:  This will also set up the modem
        modemSerial.begin(modemBaud);
        modem.gsmModem.sendAT(GF("+UART_DEF=9600,8,1,0,0"));
        modem.gsmModem.waitResponse();
        modemSerial.end();
        modemSerial.begin(9600);
    }

Skywire Pin Inversions

This chunk of code inverts the pin levels for status, wake, and reset of the modem. This is necessary for the Skywire development board and some other breakouts.

    modem.setModemStatusLevel(LOW);  // If using CTS, LOW
    modem.setModemWakeLevel(HIGH);   // Skywire dev board inverts the signal
    modem.setModemResetLevel(HIGH);  // Skywire dev board inverts the signal

XBee Cellular Carrier

This chunk of code sets the carrier profile and network technology for a Digi XBee or XBee3. You should change the lines with the CP and N# commands to the proper number to match your SIM card.

    // Extra modem set-up
    Serial.println(F("Waking modem and setting Cellular Carrier Options..."));
    modem.modemWake();  // NOTE:  This will also set up the modem
    // Go back to command mode to set carrier options
    modem.gsmModem.commandMode();
    // Carrier Profile - 0 = Automatic selection
    //                 - 1 = No profile/SIM ICCID selected
    //                 - 2 = AT&T
    //                 - 3 = Verizon
    // NOTE:  To select T-Mobile, you must enter bypass mode!
    modem.gsmModem.sendAT(GF("CP"), 2);
    modem.gsmModem.waitResponse();
    // Cellular network technology - 0 = LTE-M with NB-IoT fallback
    //                             - 1 = NB-IoT with LTE-M fallback
    //                             - 2 = LTE-M only
    //                             - 3 = NB-IoT only
    // NOTE:  As of 2020 in the USA, AT&T and Verizon only use LTE-M
    // T-Mobile uses NB-IOT
    modem.gsmModem.sendAT(GF("N#"), 2);
    modem.gsmModem.waitResponse();
    // Write changes to flash and apply them
    Serial.println(F("Wait while applying changes..."));
    // Write changes to flash
    modem.gsmModem.writeChanges();
    // Reset the cellular component to ensure network settings are changed
    modem.gsmModem.sendAT(GF("!R"));
    modem.gsmModem.waitResponse(30000L);
    // Force reset of the Digi component as well
    // This effectively exits command mode
    modem.gsmModem.sendAT(GF("FR"));
    modem.gsmModem.waitResponse(5000L);

SARA R4 Cellular Carrier

This chunk of code sets the carrier profile and network technology for a u-blox SARA R4 or N4 module, including a Sodaq R410 UBee or a Digi XBee3 LTE-M in bypass mode.. You should change the lines with the UMNOPROF and URAT commands to the proper number to match your SIM card.

    // Extra modem set-up
    Serial.println(F("Waking modem and setting Cellular Carrier Options..."));
    modem.modemWake();  // NOTE:  This will also set up the modem
    // Turn off the cellular radio while making network changes
    modem.gsmModem.sendAT(GF("+CFUN=0"));
    modem.gsmModem.waitResponse();
    // Mobile Network Operator Profile - 0 = SW default
    //                                 - 1 = SIM ICCID selected
    //                                 - 2: ATT
    //                                 - 6: China Telecom
    //                                 - 100: Standard Europe
    //                                 - 4: Telstra
    //                                 - 5: T-Mobile US
    //                                 - 19: Vodafone
    //                                 - 3: Verizon
    //                                 - 31: Deutsche Telekom
    modem.gsmModem.sendAT(GF("+UMNOPROF="), 2);
    modem.gsmModem.waitResponse();
    // Selected network technology - 7: LTE Cat.M1
    //                             - 8: LTE Cat.NB1
    // Fallback network technology - 7: LTE Cat.M1
    //                              - 8: LTE Cat.NB1
    // NOTE:  As of 2020 in the USA, AT&T and Verizon only use LTE-M
    // T-Mobile uses NB-IOT
    modem.gsmModem.sendAT(GF("+URAT="), 7, ',', 8);
    modem.gsmModem.waitResponse();
    // Restart the module to apply changes
    modem.gsmModem.sendAT(GF("+CFUN=1,1"));
    modem.gsmModem.waitResponse(10000L);

Sync the Real Time Clock

After any special modem options, we can opt to use the modem to synchronize the real time clock with the NIST time servers. This is very helpful in keeping the clock from drifting or resetting it if it lost time due to power loss. Like the sensor setup, we also apply a battery voltage voltage condition before attempting the clock sync. (All of the supported modems are large power eaters.) Unlike the sensor setup, we have an additional check for "sanity" of the clock time. To be considered "sane" the clock has to set somewhere between 2020 and 2025. It's a broad range, but it will automatically flag values like Jan 1, 2000 - which are the default start value of the clock on power up.

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

Setup a File on the SD card

We're getting close to the end of the setup function! This section verifies that the SD card is communicating with the MCU and sets up a file on it for saved data. Like with the sensors and the modem, we check for battery level before attempting to communicate with the SD card.

    // 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) {
        Serial.println(F("Setting up file on SD card"));
        dataLogger.turnOnSDcard(true);
        // true = wait for card to settle after power up
        dataLogger.createLogFile(true);  // true = write a new header
        dataLogger.turnOffSDcard(true);
        // true = wait for internal housekeeping after write
    }

Sleep until the First Data Collection Time

We're finally fished with setup! This chunk puts the system into low power deep sleep until the next logging interval.

    // Call the processor sleep
    Serial.println(F("Putting processor to sleep\n"));
    dataLogger.systemSleep();

Setup Complete

Set up is done! This setup function is really long. But don't forget you need to close it with a final curly brace.

}

Arduino Loop Function

This is the loop function which will run repeatedly as long as the board is turned on. NOTE:** This example has code for both a typical simple loop and a complex loop that calls lower level logger functions. You should only pick one loop function and delete the other.

A Typical Loop

After the incredibly long setup function, we can do the vast majority of all logger work in a very simple loop function. Every time the logger wakes we check the battery voltage and do 1 of three things:

  1. If the battery is very low, go immediately back to sleep and hope the sun comes back out
  2. If the battery is at a moderate level, attempt to collect data from sensors, but do not attempt to publish data. The modem the biggest power user of the whole system. 3. At full power, do everything.
void loop() {
    // Note:  Please change these battery voltages to match your battery
    // At very low battery, just go back to sleep
    if (getBatteryVoltage() < 3.4) {
        dataLogger.systemSleep();
    } else if (getBatteryVoltage() < 3.55) {
        // At moderate voltage, log data but don't send it over the modem
        dataLogger.logData();
    } else {
        // If the battery is good, send the data to the world
        dataLogger.logDataAndPublish();
    }
}

A Complex Loop

If you need finer control over the steps of the logging function, this code demonstrates how the loop should be constructed.

Here are some guidelines for writing a loop function:

  • If you want to log on an even interval, use if (checkInterval()) or if (checkMarkedInterval()) to verify that the current or marked time is an even interval of the logging interval..
  • Call the markTime() function if you want associate with a two iterations of sensor updates with the same timestamp. This allows you to use checkMarkedInterval() to check if an action should be preformed based on the exact time when the logger woke rather than upto several seconds later when iterating through sensors.
  • Either:
    • Power up all of your sensors with sensorsPowerUp().
    • Wake up all your sensors with sensorsWake().
    • Update the values all the sensors in your VariableArray together with updateAllSensors().
    • Immediately after running updateAllSensors(), put sensors to sleep to save power with sensorsSleep().
    • Power down all of your sensors with sensorsPowerDown().
  • Or:
    • Do a full update loop of all sensors, including powering them with completeUpdate(). (This combines the previous 5 functions.)
  • After updating the sensors, then call any functions you want to send/print/save data.
  • Finish by putting the logger back to sleep, if desired, with systemSleep().

All together, this gives:

// 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
    dataLogger.watchDogTimer.resetWatchDog();

    // 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 (dataLogger.checkInterval() && getBatteryVoltage() > 3.4) {
        // Flag to notify that we're in already awake and logging a point
        Logger::isLoggingNow = true;
        dataLogger.watchDogTimer.resetWatchDog();

        // Print a line to show new reading
        Serial.println(F("------------------------------------------"));
        // Turn on the LED to show we're taking a reading
        dataLogger.alertOn();
        // Power up the SD Card, but skip any waits after power up
        dataLogger.turnOnSDcard(false);
        dataLogger.watchDogTimer.resetWatchDog();

        // Turn on the modem to let it start searching for the network
        // Only turn the modem on if the battery at the last interval was high
        // enough
        // NOTE:  if the modemPowerUp function is not run before the
        // completeUpdate
        // function is run, the modem will not be powered and will not
        // return a signal strength reading.
        if (getBatteryVoltage() > 3.6) modem.modemPowerUp();

        // Start the stream for the modbus sensors, if your RS485 adapter bleeds
        // current from data pins when powered off & you stop modbus serial
        // connection with digitalWrite(5, LOW), below.
        // https://github.com/EnviroDIY/ModularSensors/issues/140#issuecomment-389380833
        altSoftSerial.begin(9600);

        // Do a complete update on the variable 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.
        varArray.completeUpdate();

        dataLogger.watchDogTimer.resetWatchDog();

        // Reset modbus serial pins to LOW, if your RS485 adapter bleeds power
        // on sleep, because Modbus Stop bit leaves these pins HIGH.
        // https://github.com/EnviroDIY/ModularSensors/issues/140#issuecomment-389380833
        digitalWrite(5, LOW);  // Reset AltSoftSerial Tx pin to LOW
        digitalWrite(6, LOW);  // Reset AltSoftSerial Rx pin to LOW

        // Create a csv data record and save it to the log file
        dataLogger.logToSD();
        dataLogger.watchDogTimer.resetWatchDog();

        // Connect to the network
        // Again, we're only doing this if the battery is doing well
        if (getBatteryVoltage() > 3.55) {
            dataLogger.watchDogTimer.resetWatchDog();
            if (modem.connectInternet()) {
                dataLogger.watchDogTimer.resetWatchDog();
                // Publish data to remotes
                Serial.println(F("Modem connected to internet."));
                dataLogger.publishDataToRemotes();

                // Sync the clock at midnight
                dataLogger.watchDogTimer.resetWatchDog();
                if (Logger::markedEpochTime != 0 &&
                    Logger::markedEpochTime % 86400 == 0) {
                    Serial.println(F("Running a daily clock sync..."));
                    dataLogger.setRTClock(modem.getNISTTime());
                    dataLogger.watchDogTimer.resetWatchDog();
                    modem.updateModemMetadata();
                    dataLogger.watchDogTimer.resetWatchDog();
                }

                // Disconnect from the network
                modem.disconnectInternet();
                dataLogger.watchDogTimer.resetWatchDog();
            }
            // Turn the modem off
            modem.modemSleepPowerDown();
            dataLogger.watchDogTimer.resetWatchDog();
        }

        // Cut power from the SD card - without additional housekeeping wait
        dataLogger.turnOffSDcard(false);
        dataLogger.watchDogTimer.resetWatchDog();
        // Turn off the LED
        dataLogger.alertOff();
        // Print a line to show reading ended
        Serial.println(F("------------------------------------------\n"));

        // Unset flag
        Logger::isLoggingNow = false;
    }

    // Check if it was instead the testing interrupt that woke us up
    if (Logger::startTesting) {
        // Start the stream for the modbus sensors, if your RS485 adapter bleeds
        // current from data pins when powered off & you stop modbus serial
        // connection with digitalWrite(5, LOW), below.
        // https://github.com/EnviroDIY/ModularSensors/issues/140#issuecomment-389380833
        altSoftSerial.begin(9600);

        dataLogger.testingMode();
    }

    // Reset modbus serial pins to LOW, if your RS485 adapter bleeds power
    // on sleep, because Modbus Stop bit leaves these pins HIGH.
    // https://github.com/EnviroDIY/ModularSensors/issues/140#issuecomment-389380833
    digitalWrite(5, LOW);  // Reset AltSoftSerial Tx pin to LOW
    digitalWrite(6, LOW);  // Reset AltSoftSerial Rx pin to LOW

    // Call the processor sleep
    dataLogger.systemSleep();
}
#endif

If you need more help in writing a complex loop, the double_logger example program demonstrates using a custom loop function in order to log two different groups of sensors at different logging intervals. The data_saving example program shows using a custom loop in order to save cellular data by saving data from many variables on the SD card, but only sending a portion of the data to the EnviroDIY data portal.

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
; http://docs.platformio.org/page/projectconf.html

[platformio]
description = ModularSensors example menu_a_la_carte

[env:mayfly]
monitor_speed = 115200
board = mayfly
platform = atmelavr
framework = arduino
lib_ldf_mode = deep+
lib_ignore =
    RTCZero
    Adafruit NeoPixel
    Adafruit GFX Library
    Adafruit SSD1306
    Adafruit ADXL343
    Adafruit STMPE610
    Adafruit TouchScreen
    Adafruit ILI9341
build_flags =
    -DSDI12_EXTERNAL_PCINT
    -DNEOSWSERIAL_EXTERNAL_PCINT
    -DMQTT_MAX_PACKET_SIZE=240
    -DTINY_GSM_RX_BUFFER=64
    -DTINY_GSM_YIELD_MS=2
    -DENABLE_SERIAL2
    -DENABLE_SERIAL3
    ; -D MS_BUILD_MODEM_XBEE_CELLULAR  ; Turn on first time w/ a Digi LTE-M module
    ; -D MS_LOGGERBASE_DEBUG
    ; -D MS_DATAPUBLISHERBASE_DEBUG
    ; -D MS_ENVIRODIYPUBLISHER_DEBUG
lib_deps =
    envirodiy/EnviroDIY_ModularSensors
;  ^^ Use this when working from a tagged release of the library
;     See tags at https://platformio.org/lib/show/1648/EnviroDIY_ModularSensors

;    https://github.com/EnviroDIY/ModularSensors.git#develop
;  ^^ Use this when if you want to pull from the develop branch

    https://github.com/PaulStoffregen/AltSoftSerial.git
    https://github.com/SRGDamia1/NeoSWSerial.git
    https://github.com/EnviroDIY/SoftwareSerial_ExternalInts.git
;  ^^ These are software serial port emulator libraries, you may not need them

The Complete Code

/** =========================================================================
 * @file menu_a_la_carte.ino
 * @brief Example with all possible functionality.
 *
 * @author Sara Geleskie Damiano <sdamiano@stroudcenter.org>
 * @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
 *
 * DISCLAIMER:
 * THIS CODE IS PROVIDED "AS IS" - NO WARRANTY IS GIVEN.
 * ======================================================================= */

// ==========================================================================
//  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] */
#ifndef TINY_GSM_RX_BUFFER
#define TINY_GSM_RX_BUFFER 64
#endif
#ifndef TINY_GSM_YIELD_MS
#define TINY_GSM_YIELD_MS 2
#endif
#ifndef MQTT_MAX_PACKET_SIZE
#define MQTT_MAX_PACKET_SIZE 240
#endif
/** 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] */


// ==========================================================================
//  Creating Additional 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 defined(ARDUINO_ARCH_AVR) || defined(__AVR__)  // 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

#ifdef MS_BUILD_TEST_ALTSOFTSERIAL
// AltSoftSerial by Paul Stoffregen
// (https://github.com/PaulStoffregen/AltSoftSerial) 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.
/** Start [altsoftserial] */
#include <AltSoftSerial.h>
AltSoftSerial altSoftSerial;
/** End [altsoftserial] */
#endif  // #ifdef MS_BUILD_TEST_ALTSOFTSERIAL

#ifdef MS_BUILD_TEST_NEOSWSERIAL
// NeoSWSerial (https://github.com/SRGDamia1/NeoSWSerial) is the best software
// serial that can be used on any pin supporting interrupts.
// You can use as many instances of NeoSWSerial as you need.
// Not all AVR boards are supported by NeoSWSerial.
/** Start [neoswserial] */
#include <NeoSWSerial.h>          // for the stream communication
const int8_t neoSSerial1Rx = 11;  // data in pin
const int8_t neoSSerial1Tx = -1;  // data out pin
NeoSWSerial  neoSSerial1(neoSSerial1Rx, neoSSerial1Tx);
// To use NeoSWSerial in this library, we define a function to receive data
// This is just a short-cut for later
void neoSSerial1ISR() {
    NeoSWSerial::rxISR(*portInputRegister(digitalPinToPort(neoSSerial1Rx)));
}
/** End [neoswserial] */
#endif  // #ifdef MS_BUILD_TEST_NEOSWSERIAL

#ifdef MS_BUILD_TEST_SOFTSERIAL
// The "standard" software serial library uses interrupts that conflict
// with several other libraries used within this program.  I've created a
// [version of software serial that has been stripped of
// interrupts](https://github.com/EnviroDIY/SoftwareSerial_ExtInts) but it is
// still far from ideal.
// NOTE:  Only use if necessary.  This is not a very accurate serial port!
// You can use as many instances of SoftwareSerial as you need.
/** Start [softwareserial] */
const int8_t softSerialRx = A3;  // data in pin
const int8_t softSerialTx = A4;  // data out pin

#include <SoftwareSerial_ExtInts.h>  // for the stream communication
SoftwareSerial_ExtInts softSerial1(softSerialRx, softSerialTx);
/** End [softwareserial] */
#endif  // #ifdef MS_BUILD_TEST_SOFTSERIAL


#if defined MS_PALEOTERRA_SOFTWAREWIRE || defined MS_RAIN_SOFTWAREWIRE
/** Start [softwarewire] */
// A software I2C (Wire) instance using Testato's SoftwareWire
// To use SoftwareWire, you must also set a define for the sensor you want to
// use Software I2C for, ie:
//   `#define MS_RAIN_SOFTWAREWIRE`
//   `#define MS_PALEOTERRA_SOFTWAREWIRE`
// or set the build flag(s):
//   `-D MS_RAIN_SOFTWAREWIRE`
//   `-D MS_PALEOTERRA_SOFTWAREWIRE`
#include <SoftwareWire.h>  // Testato's Software I2C
const int8_t softwareSDA = 5;
const int8_t softwareSCL = 4;
SoftwareWire softI2C(softwareSDA, softwareSCL);
/** End [softwarewire] */
#endif  //  #if defined MS_PALEOTERRA_SOFTWAREWIRE ...

#endif  // End software serial for avr boards

#if defined ARDUINO_ARCH_SAMD
/** Start [serial_ports_SAMD] */
// The SAMD21 has 6 "SERCOM" ports, any of which can be used for UART
// communication.  The "core" code for most boards defines one or more UART
// (Serial) ports with the SERCOMs and uses others for I2C and SPI.  We can
// create new UART ports on any available SERCOM.  The table below shows
// definitions for select boards.

// Board =>   Arduino Zero       Adafruit Feather    Sodaq Boards
// -------    ---------------    ----------------    ----------------
// SERCOM0    Serial1 (D0/D1)    Serial1 (D0/D1)     Serial (D0/D1)
// SERCOM1    Available          Available           Serial3 (D12/D13)
// SERCOM2    Available          Available           I2C (A4/A5)
// SERCOM3    I2C (D20/D21)      I2C (D20/D21)       SPI (D11/12/13)
// SERCOM4    SPI (D21/22/23)    SPI (D21/22/23)     SPI1/Serial2
// SERCOM5    EDBG/Serial        Available           Serial1

// If using a Sodaq board, do not define the new sercoms, instead:
// #define ENABLE_SERIAL2
// #define ENABLE_SERIAL3

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

#ifndef ENABLE_SERIAL2
// 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() {
    Serial2.IrqHandler();
}
#endif

#ifndef ENABLE_SERIAL3
// Set up a 'new' UART using SERCOM2
// The Rx will be on digital pin 5, which is SERCOM2's Pad #3
// The Tx will be on digital pin 2, which is SERCOM2's Pad #2
// NOTE:  SERCOM2 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 Serial3(&sercom2, 5, 2, SERCOM_RX_PAD_3, UART_TX_PAD_2);
// Hand over the interrupts to the sercom port
void SERCOM2_Handler() {
    Serial3.IrqHandler();
}
#endif

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


// ==========================================================================
//  Assigning Serial Port Functionality
// ==========================================================================
#if defined ARDUINO_ARCH_SAMD || defined ATMEGA2560 || \
    defined                              ARDUINO_AVR_MEGA2560
/** Start [assign_ports_hw] */
// If there are additional hardware Serial ports possible - use them!

// We give the modem first priority and assign it to hardware serial
// All of the supported processors have a hardware port available named Serial1
#define modemSerial Serial1

// Define the serial port for modbus
// Modbus (at 9600 8N1) is used by the Keller level loggers and Yosemitech
// sensors
#define modbusSerial Serial2

// The Maxbotix sonar is the only sensor that communicates over a serial port
// but does not use modbus
#define sonarSerial Serial3

/** End [assign_ports_hw] */
#else
/** Start [assign_ports_sw] */

// We give the modem first priority and assign it to hardware serial
// All of the supported processors have a hardware port available named Serial1
#define modemSerial Serial1

// Define the serial port for modbus
// Modbus (at 9600 8N1) is used by the Keller level loggers and Yosemitech
// sensors
// Since AltSoftSerial is the best software option, we use it for modbus
// If AltSoftSerial (or its pins) aren't avaiable, use NeoSWSerial
// SoftwareSerial **WILL NOT** work for modbus!
#define modbusSerial altSoftSerial  // For AltSoftSerial
// #define modbusSerial neoSSerial1  // For Neo software serial
// #define modbusSerial softSerial1  // For software serial

// The Maxbotix sonar is the only sensor that communicates over a serial port
// but does not use modbus
// Since the Maxbotix only needs one-way communication and sends a simple text
// string repeatedly, almost any software serial port will do for it.
// #define sonarSerial altSoftSerial  // For AltSoftSerial
#define sonarSerial neoSSerial1     // For Neo software serial
// #define sonarSerial softSerial1  // For software serial

/** End [assign_ports_sw] */
#endif


// ==========================================================================
//  Data Logging Options
// ==========================================================================
/** Start [logging_options] */
// The name of this program file
const char* sketchName = "menu_a_la_carte.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
//    NOTE:  DON'T USE MORE THAN ONE MODEM OBJECT!
//           Delete the sections you are not using!
// ==========================================================================

#if defined MS_BUILD_MODEM_XBEE_CELLULAR
/** Start [xbee_cell_transparent] */
// For any Digi Cellular XBee's
// NOTE:  The u-blox based Digi XBee's (3G global and LTE-M global) can be used
// in either bypass or transparent mode, each with pros and cons
// The Telit based Digi XBees (LTE Cat1) can only use this mode.
#include <modems/DigiXBeeCellularTransparent.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section
const int32_t modemBaud = 9600;  // All XBee's use 9600 by default

// Modem Pins - Describe the physical pin connection of your modem to your board
// NOTE:  Use -1 for pins that do not apply
// The pin numbers here are for a Digi XBee with a Mayfly and LTE adapter
// For options https://github.com/EnviroDIY/LTEbee-Adapter/edit/master/README.md
const int8_t modemVccPin = -1;     // MCU pin controlling modem power
                                   // Option: modemVccPin = A5, if Mayfly SJ7 is
                                   // connected to the ASSOC pin
const int8_t modemStatusPin = 19;  // MCU pin used to read modem status
// NOTE:  If possible, use the `STATUS/SLEEP_not` (XBee pin 13) for status, but
// the CTS pin can also be used if necessary
const bool   useCTSforStatus = false;  // Flag to use the CTS pin for status
const int8_t modemResetPin   = 20;     // MCU pin connected to modem reset pin
const int8_t modemSleepRqPin = 23;     // MCU pin for modem sleep/wake request
const int8_t modemLEDPin = redLED;     // MCU pin connected an LED to show modem
                                       // status

// Network connection information
const char* apn = "xxxxx";  // APN for GPRS connection

// Create the modem object
DigiXBeeCellularTransparent modemXBCT(&modemSerial, modemVccPin, modemStatusPin,
                                      useCTSforStatus, modemResetPin,
                                      modemSleepRqPin, apn);
// Create an extra reference to the modem by a generic name
DigiXBeeCellularTransparent modem = modemXBCT;
/** End [xbee_cell_transparent] */
// ==========================================================================


#elif defined MS_BUILD_MODEM_XBEE_LTE_B
/** Start [xbee3_ltem_bypass] */
// For the u-blox SARA R410M based Digi LTE-M XBee3
// NOTE:  According to the manual, this should be less stable than transparent
// mode, but my experience is the complete reverse.
#include <modems/DigiXBeeLTEBypass.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section
const int32_t modemBaud = 9600;  // All XBee's use 9600 by default

// Modem Pins - Describe the physical pin connection of your modem to your board
// NOTE:  Use -1 for pins that do not apply
// The pin numbers here are for a Digi XBee with a Mayfly and LTE adapter
const int8_t modemVccPin    = A5;  // MCU pin controlling modem power
const int8_t modemStatusPin = 19;  // MCU pin used to read modem status
// NOTE:  If possible, use the `STATUS/SLEEP_not` (XBee pin 13) for status, but
// the CTS pin can also be used if necessary
const bool   useCTSforStatus = false;  // Flag to use the CTS pin for status
const int8_t modemResetPin   = 20;     // MCU pin connected to modem reset pin
const int8_t modemSleepRqPin = 23;     // MCU pin for modem sleep/wake request
const int8_t modemLEDPin = redLED;     // MCU pin connected an LED to show modem
                                       // status

// Network connection information
const char* apn = "xxxxx";  // APN for GPRS connection

// Create the modem object
DigiXBeeLTEBypass modemXBLTEB(&modemSerial, modemVccPin, modemStatusPin,
                              useCTSforStatus, modemResetPin, modemSleepRqPin,
                              apn);
// Create an extra reference to the modem by a generic name
DigiXBeeLTEBypass modem = modemXBLTEB;
/** End [xbee3_ltem_bypass] */
// ==========================================================================


#elif defined MS_BUILD_MODEM_XBEE_3G_B
/** Start [xbee_3g_bypass] */
// For the u-blox SARA U201 based Digi 3G XBee with 2G fallback
// NOTE:  According to the manual, this should be less stable than transparent
// mode, but my experience is the complete reverse.
#include <modems/DigiXBee3GBypass.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section
const int32_t modemBaud = 9600;  // All XBee's use 9600 by default

// Modem Pins - Describe the physical pin connection of your modem to your board
// NOTE:  Use -1 for pins that do not apply
// The pin numbers here are for a Digi XBee with a Mayfly and LTE adapter
const int8_t modemVccPin    = A5;  // MCU pin controlling modem power
const int8_t modemStatusPin = 19;  // MCU pin used to read modem status
// NOTE:  If possible, use the `STATUS/SLEEP_not` (XBee pin 13) for status, but
// the CTS pin can also be used if necessary
const bool   useCTSforStatus = false;  // Flag to use the CTS pin for status
const int8_t modemResetPin   = 20;     // MCU pin connected to modem reset pin
const int8_t modemSleepRqPin = 23;     // MCU pin for modem sleep/wake request
const int8_t modemLEDPin = redLED;     // MCU pin connected an LED to show modem
                                       // status

// Network connection information
const char* apn = "xxxxx";  // APN for GPRS connection

// Create the modem object
DigiXBee3GBypass modemXB3GB(&modemSerial, modemVccPin, modemStatusPin,
                            useCTSforStatus, modemResetPin, modemSleepRqPin,
                            apn);
// Create an extra reference to the modem by a generic name
DigiXBee3GBypass modem = modemXB3GB;
/** End [xbee_3g_bypass] */
// ==========================================================================


#elif defined MS_BUILD_MODEM_XBEE_WIFI
/** Start [xbee_wifi] */
// For the Digi Wifi XBee (S6B)
#include <modems/DigiXBeeWifi.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section
const int32_t modemBaud = 9600;  // All XBee's use 9600 by default

// Modem Pins - Describe the physical pin connection of your modem to your board
// NOTE:  Use -1 for pins that do not apply
// The pin numbers here are for a Digi XBee direcly connected to a Mayfly
const int8_t modemVccPin    = -1;  // MCU pin controlling modem power
const int8_t modemStatusPin = 19;  // MCU pin used to read modem status
// NOTE:  If possible, use the `STATUS/SLEEP_not` (XBee pin 13) for status, but
// the CTS pin can also be used if necessary
const bool   useCTSforStatus = true;  // Flag to use the CTS pin for status
const int8_t modemResetPin   = -1;    // MCU pin connected to modem reset pin
const int8_t modemSleepRqPin = 23;    // MCU pin for modem sleep/wake request
const int8_t modemLEDPin = redLED;    // MCU pin connected an LED to show modem
                                      // status

// Network connection information
const char* wifiId  = "xxxxx";  // WiFi access point name
const char* wifiPwd = "xxxxx";  // WiFi password (WPA2)

// Create the modem object
DigiXBeeWifi modemXBWF(&modemSerial, modemVccPin, modemStatusPin,
                       useCTSforStatus, modemResetPin, modemSleepRqPin, wifiId,
                       wifiPwd);
// Create an extra reference to the modem by a generic name
DigiXBeeWifi modem = modemXBWF;
/** End [xbee_wifi] */
// ==========================================================================


#elif defined MS_BUILD_MODEM_ESP8266
/** Start [esp8266] */
// For almost anything based on the Espressif ESP8266 using the
// AT command firmware
#include <modems/EspressifESP8266.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section
const int32_t modemBaud = 115200;  // Communication speed of the modem
// NOTE:  This baud rate too fast for an 8MHz board, like the Mayfly!  The
// module should be programmed to a slower baud rate or set to auto-baud using
// the AT+UART_CUR or AT+UART_DEF command.

// Modem Pins - Describe the physical pin connection of your modem to your board
// NOTE:  Use -1 for pins that do not apply
// Example pins here are for a DFRobot ESP8266 Bee with Mayfly
const int8_t modemVccPin     = -2;  // MCU pin controlling modem power
const int8_t modemStatusPin  = -1;  // MCU pin used to read modem status
const int8_t modemResetPin   = -1;  // MCU pin connected to modem reset pin
const int8_t modemSleepRqPin = 19;  // MCU pin for wake from light sleep
const int8_t modemLEDPin = redLED;  // MCU pin connected an LED to show modem
                                    // status
// Pins for light sleep on the ESP8266. For power savings, I recommend
// NOT using these if it's possible to use deep sleep.
const int8_t espSleepRqPin = 13;  // GPIO# ON THE ESP8266 to assign for light
                                  // sleep request
const int8_t espStatusPin = -1;   // GPIO# ON THE ESP8266 to assign for light
                                  // sleep status

// Network connection information
const char* wifiId  = "xxxxx";  // WiFi access point name
const char* wifiPwd = "xxxxx";  // WiFi password (WPA2)

// Create the modem object
EspressifESP8266 modemESP(&modemSerial, modemVccPin, modemStatusPin,
                          modemResetPin, modemSleepRqPin, wifiId, wifiPwd,
                          espSleepRqPin,
                          espStatusPin  // Optional arguments
);
// Create an extra reference to the modem by a generic name
EspressifESP8266 modem = modemESP;
/** End [esp8266] */
// ==========================================================================


#elif defined MS_BUILD_MODEM_BG96
/** Start [bg96] */
// For the Dragino, Nimbelink or other boards based on the Quectel BG96
#include <modems/QuectelBG96.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section
const int32_t modemBaud = 115200;  // Communication speed of the modem
// NOTE:  This baud rate too fast for an 8MHz board, like the Mayfly!  The
// module should be programmed to a slower baud rate or set to auto-baud using
// the AT+IPR=9600 command.

// Modem Pins - Describe the physical pin connection of your modem to your board
// NOTE:  Use -1 for pins that do not apply
// Example pins here are for a modified Mayfly and a Dragino IoT Bee
const int8_t modemVccPin     = -1;  // MCU pin controlling modem power
const int8_t modemStatusPin  = -1;  // MCU pin used to read modem status
const int8_t modemResetPin   = A4;  // MCU pin connected to modem reset pin
const int8_t modemSleepRqPin = A3;  // MCU pin for modem sleep/wake request
const int8_t modemLEDPin = redLED;  // MCU pin connected an LED to show modem
                                    // status

// Network connection information
const char* apn = "xxxxx";  // APN for GPRS connection

// Create the modem object
QuectelBG96 modemBG96(&modemSerial, modemVccPin, modemStatusPin, modemResetPin,
                      modemSleepRqPin, apn);
// Create an extra reference to the modem by a generic name
QuectelBG96 modem = modemBG96;
/** End [bg96] */
// ==========================================================================


#elif defined MS_BUILD_MODEM_MONARCH
/** Start [monarch] */
// For the Nimbelink LTE-M Verizon/Sequans or other boards based on the Sequans
// Monarch series
#include <modems/SequansMonarch.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section
const int32_t modemBaud = 921600;  // Default baud rate of SVZM20 is 921600
// NOTE:  This baud rate is much too fast for many Arduinos!  The module should
// be programmed to a slower baud rate or set to auto-baud using the AT+IPR
// command.

// Modem Pins - Describe the physical pin connection of your modem to your board
// NOTE:  Use -1 for pins that do not apply
// Nimbelink Skywire (NOT directly connectable to a Mayfly!)
const int8_t modemVccPin     = -1;  // MCU pin controlling modem power
const int8_t modemStatusPin  = 19;  // MCU pin used to read modem status
const int8_t modemResetPin   = 20;  // MCU pin connected to modem reset pin
const int8_t modemSleepRqPin = 23;  // MCU pin for modem sleep/wake request
const int8_t modemLEDPin = redLED;  // MCU pin connected an LED to show modem
                                    // status

// Network connection information
const char* apn = "xxxxx";  // APN for GPRS connection

// Create the modem object
SequansMonarch modemSVZM(&modemSerial, modemVccPin, modemStatusPin,
                         modemResetPin, modemSleepRqPin, apn);
// Create an extra reference to the modem by a generic name
SequansMonarch modem = modemSVZM;
/** End [monarch] */
// ==========================================================================


#elif defined MS_BUILD_MODEM_SIM800
/** Start [sim800] */
// For almost anything based on the SIMCom SIM800 EXCEPT the Sodaq 2GBee R6 and
// higher
#include <modems/SIMComSIM800.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section
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
// Example pins are for a Sodaq GPRSBee R4 with a Mayfly
const int8_t modemVccPin     = -1;  // MCU pin controlling modem power
const int8_t modemStatusPin  = 19;  // MCU pin used to read modem status
const int8_t modemResetPin   = -1;  // MCU pin connected to modem reset pin
const int8_t modemSleepRqPin = 23;  // MCU pin for modem sleep/wake request
const int8_t modemLEDPin = redLED;  // MCU pin connected an LED to show modem
                                    // status

// Network connection information
const char* apn = "xxxxx";  // APN for GPRS connection

// Create the modem object
SIMComSIM800 modemS800(&modemSerial, modemVccPin, modemStatusPin, modemResetPin,
                       modemSleepRqPin, apn);
// Create an extra reference to the modem by a generic name
SIMComSIM800 modem = modemS800;
/** End [sim800] */
// ==========================================================================


#elif defined MS_BUILD_MODEM_SIM7000
/** Start [sim7000] */
// For almost anything based on the SIMCom SIM7000
#include <modems/SIMComSIM7000.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section
const int32_t modemBaud = 9600;  //  SIM7000 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     = -1;  // MCU pin controlling modem power
const int8_t modemStatusPin  = -1;  // MCU pin used to read modem status
const int8_t modemResetPin   = -1;  // MCU pin connected to modem reset pin
const int8_t modemSleepRqPin = 23;  // MCU pin for modem sleep/wake request
const int8_t modemLEDPin = redLED;  // MCU pin connected an LED to show modem
                                    // status

// Network connection information
const char* apn = "xxxxx";  // APN for GPRS connection

// Create the modem object
SIMComSIM7000 modem7000(&modemSerial, modemVccPin, modemStatusPin,
                        modemResetPin, modemSleepRqPin, apn);
// Create an extra reference to the modem by a generic name
SIMComSIM7000 modem = modem7000;
/** End [sim7000] */
// ==========================================================================


#elif defined MS_BUILD_MODEM_SIM7080
/** Start [sim7080] */
// For almost anything based on the SIMCom SIM7080G
#include <modems/SIMComSIM7080.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section
const int32_t modemBaud =
    9600;  //  SIM7080 does auto-bauding by default, but I set mine to 9600

// Modem Pins - Describe the physical pin connection of your modem to your board
// NOTE:  Use -1 for pins that do not apply
// and-global breakout bk-7080a
const int8_t modemVccPin     = -1;  // MCU pin controlling modem power
const int8_t modemStatusPin  = 19;  // MCU pin used to read modem status
const int8_t modemSleepRqPin = 23;  // MCU pin for modem sleep/wake request
const int8_t modemLEDPin = redLED;  // MCU pin connected an LED to show modem
                                    // status

// Network connection information
const char* apn = "xxxxx";  // APN for GPRS connection

// Create the modem object
SIMComSIM7080 modem7080(&modemSerial, modemVccPin, modemStatusPin,
                        modemSleepRqPin, apn);
// Create an extra reference to the modem by a generic name
SIMComSIM7080 modem = modem7080;
/** End [sim7080] */
// ==========================================================================


#elif defined MS_BUILD_MODEM_S2GB
/** 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>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section
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
// Example pins are for a Sodaq GPRSBee R6 or R7 with a Mayfly
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 modemResetPin   = -1;  // MCU pin connected to modem reset pin
const int8_t modemSleepRqPin = -1;  // MCU pin for modem sleep/wake request
const int8_t modemLEDPin = redLED;  // MCU pin connected an LED to show modem
                                    // status

// Network connection information
const char* apn = "xxxxx";  // APN for GPRS connection

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


#elif defined MS_BUILD_MODEM_UBEE_R410M
/** Start [sara_r410m] */
// For the Sodaq UBee based on the 4G LTE-M u-blox SARA R410M
#include <modems/SodaqUBeeR410M.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section
const int32_t modemBaud =
    115200;  // Default baud rate of the SARA R410M is 115200
// NOTE:  The SARA R410N DOES NOT save baud rate to non-volatile memory.  After
// every power loss, the module will return to the default baud rate of 115200.
// NOTE:  115200 is TOO FAST for an 8MHz Arduino.  This library attempts to
// compensate by sending a baud rate change command in the wake function when
// compiled for a 8MHz board. Because of this, 8MHz boards, LIKE THE MAYFLY,
// *MUST* use a HardwareSerial instance as modemSerial.

// Modem Pins - Describe the physical pin connection of your modem to your board
// NOTE:  Use -1 for pins that do not apply
// Example pins are for a Sodaq uBee R410M with a Mayfly
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 modemResetPin   = -1;  // MCU pin connected to modem reset pin
const int8_t modemSleepRqPin = 20;  // MCU pin for modem sleep/wake request
const int8_t modemLEDPin = redLED;  // MCU pin connected an LED to show modem
                                    // status

// Network connection information
const char* apn = "xxxxx";  // APN for GPRS connection

// Create the modem object
SodaqUBeeR410M modemR410(&modemSerial, modemVccPin, modemStatusPin,
                         modemResetPin, modemSleepRqPin, apn);
// Create an extra reference to the modem by a generic name
SodaqUBeeR410M modem = modemR410;
/** End [sara_r410m] */
// ==========================================================================


#elif defined MS_BUILD_MODEM_UBEE_U201
/** Start [sara_u201] */
// For the Sodaq UBee based on the 3G u-blox SARA U201
#include <modems/SodaqUBeeU201.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section
const int32_t modemBaud =
    9600;  //  SARA U2xx module 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
// Example pins are for a Sodaq uBee U201 with a Mayfly
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 modemResetPin   = -1;  // MCU pin connected to modem reset pin
const int8_t modemSleepRqPin = 20;  // MCU pin for modem sleep/wake request
const int8_t modemLEDPin = redLED;  // MCU pin connected an LED to show modem
                                    // status

// Network connection information
const char* apn = "xxxxx";  // APN for GPRS connection

// Create the modem object
SodaqUBeeU201 modemU201(&modemSerial, modemVccPin, modemStatusPin,
                        modemResetPin, modemSleepRqPin, apn);
// Create an extra reference to the modem by a generic name
SodaqUBeeU201 modem = modemU201;
/** End [sara_u201] */
// ==========================================================================
#endif


/** Start [modem_variables] */
// Create RSSI and signal strength variable pointers for the modem
Variable* modemRSSI =
    new Modem_RSSI(&modem, "12345678-abcd-1234-ef00-1234567890ab", "RSSI");
Variable* modemSignalPct = new Modem_SignalPercent(
    &modem, "12345678-abcd-1234-ef00-1234567890ab", "signalPercent");
Variable* modemBatteryState = new Modem_BatteryState(
    &modem, "12345678-abcd-1234-ef00-1234567890ab", "modemBatteryCS");
Variable* modemBatteryPct = new Modem_BatteryPercent(
    &modem, "12345678-abcd-1234-ef00-1234567890ab", "modemBatteryPct");
Variable* modemBatteryVoltage = new Modem_BatteryVoltage(
    &modem, "12345678-abcd-1234-ef00-1234567890ab", "modemBatterymV");
Variable* modemTemperature =
    new Modem_Temp(&modem, "12345678-abcd-1234-ef00-1234567890ab", "modemTemp");
/** End [modem_variables] */


// ==========================================================================
//  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] */


#if defined ARDUINO_ARCH_AVR || defined MS_SAMD_DS3231
// ==========================================================================
//  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] */
#endif


#if defined MS_BUILD_SENSOR_AM2315
// ==========================================================================
//  AOSong AM2315 Digital Humidity and Temperature Sensor
// ==========================================================================
/** Start [am2315] */
#include <sensors/AOSongAM2315.h>

const int8_t AM2315Power = sensorPowerPin;  // Power pin (-1 if unconnected)

// Create an AOSong AM2315 sensor object
AOSongAM2315 am2315(AM2315Power);

// Create humidity and temperature variable pointers for the AM2315
Variable* am2315Humid =
    new AOSongAM2315_Humidity(&am2315, "12345678-abcd-1234-ef00-1234567890ab");
Variable* am2315Temp =
    new AOSongAM2315_Temp(&am2315, "12345678-abcd-1234-ef00-1234567890ab");
/** End [am2315] */
#endif


#if defined MS_BUILD_SENSOR_DHT
// ==========================================================================
//  AOSong DHT 11/21 (AM2301)/22 (AM2302) Digital Humidity and Temperature
// ==========================================================================
/** Start [dht] */
#include <sensors/AOSongDHT.h>

const int8_t DHTPower = sensorPowerPin;  // Power pin (-1 if unconnected)
const int8_t DHTPin   = 10;              // DHT data pin
DHTtype      dhtType  = DHT11;  // DHT type, either DHT11, DHT21, or DHT22

// Create an AOSong DHT sensor object
AOSongDHT dht(DHTPower, DHTPin, dhtType);

// Create humidity, temperature, and heat index variable pointers for the DHT
Variable* dhtHumid =
    new AOSongDHT_Humidity(&dht, "12345678-abcd-1234-ef00-1234567890ab");
Variable* dhtTemp = new AOSongDHT_Temp(&dht,
                                       "12345678-abcd-1234-ef00-1234567890ab");
Variable* dhtHI   = new AOSongDHT_HI(&dht,
                                   "12345678-abcd-1234-ef00-1234567890ab");
/** End [dht] */
#endif


#if defined MS_BUILD_SENSOR_SQ212
// ==========================================================================
//  Apogee SQ-212 Photosynthetically Active Radiation (PAR) Sensor
// ==========================================================================
/** Start [sq212] */
#include <sensors/ApogeeSQ212.h>

const int8_t  SQ212Power = sensorPowerPin;  // Power pin (-1 if unconnected)
const int8_t  SQ212ADSChannel  = 3;         // The ADS channel for the SQ212
const uint8_t SQ212ADSi2c_addr = 0x48;  // The I2C address of the ADS1115 ADC

// Create an Apogee SQ212 sensor object
ApogeeSQ212 SQ212(SQ212Power, SQ212ADSChannel, SQ212ADSi2c_addr);

// Create PAR and raw voltage variable pointers for the SQ212
Variable* sq212PAR =
    new ApogeeSQ212_PAR(&SQ212, "12345678-abcd-1234-ef00-1234567890ab");
Variable* sq212voltage =
    new ApogeeSQ212_Voltage(&SQ212, "12345678-abcd-1234-ef00-1234567890ab");
/** End [sq212] */
#endif


#if defined MS_BUILD_SENSOR_ATLASCO2
// ==========================================================================
//  Atlas Scientific EZO-CO2 Embedded NDIR Carbon Dioxide Sensor
// ==========================================================================
/** Start [atlas_co2] */
#include <sensors/AtlasScientificCO2.h>

const int8_t AtlasCO2Power = sensorPowerPin;  // Power pin (-1 if unconnected)
uint8_t      AtlasCO2i2c_addr = 0x69;  // Default for CO2-EZO is 0x69 (105)
// All Atlas sensors have different default I2C addresses, but any of them can
// be re-addressed to any 8 bit number.  If using the default address for any
// Atlas Scientific sensor, you may omit this argument.

// Create an Atlas Scientific CO2 sensor object
// AtlasScientificCO2 atlasCO2(AtlasCO2Power, AtlasCO2i2c_addr);
AtlasScientificCO2 atlasCO2(AtlasCO2Power);

// Create concentration and temperature variable pointers for the EZO-CO2
Variable* atlasCO2CO2 = new AtlasScientificCO2_CO2(
    &atlasCO2, "12345678-abcd-1234-ef00-1234567890ab");
Variable* atlasCO2Temp = new AtlasScientificCO2_Temp(
    &atlasCO2, "12345678-abcd-1234-ef00-1234567890ab");
/** End [atlas_co2] */
#endif


#if defined MS_BUILD_SENSOR_ATLASDO
// ==========================================================================
//  Atlas Scientific EZO-DO Dissolved Oxygen Sensor
// ==========================================================================
/** Start [atlas_do] */
#include <sensors/AtlasScientificDO.h>

const int8_t AtlasDOPower    = sensorPowerPin;  // Power pin (-1 if unconnected)
uint8_t      AtlasDOi2c_addr = 0x61;            // Default for DO is 0x61 (97)
// All Atlas sensors have different default I2C addresses, but any of them can
// be re-addressed to any 8 bit number.  If using the default address for any
// Atlas Scientific sensor, you may omit this argument.

// Create an Atlas Scientific DO sensor object
// AtlasScientificDO atlasDO(AtlasDOPower, AtlasDOi2c_addr);
AtlasScientificDO atlasDO(AtlasDOPower);

// Create concentration and percent saturation variable pointers for the EZO-DO
Variable* atlasDOconc = new AtlasScientificDO_DOmgL(
    &atlasDO, "12345678-abcd-1234-ef00-1234567890ab");
Variable* atlasDOpct = new AtlasScientificDO_DOpct(
    &atlasDO, "12345678-abcd-1234-ef00-1234567890ab");
/** End [atlas_do] */
#endif


#if defined MS_BUILD_SENSOR_ATLASORP
// ==========================================================================
//  Atlas Scientific EZO-ORP Oxidation/Reduction Potential Sensor
// ==========================================================================
/** Start [atlas_orp] */
#include <sensors/AtlasScientificORP.h>

const int8_t AtlasORPPower = sensorPowerPin;  // Power pin (-1 if unconnected)
uint8_t      AtlasORPi2c_addr = 0x62;         // Default for ORP is 0x62 (98)
// All Atlas sensors have different default I2C addresses, but any of them can
// be re-addressed to any 8 bit number.  If using the default address for any
// Atlas Scientific sensor, you may omit this argument.

// Create an Atlas Scientific ORP sensor object
// AtlasScientificORP atlasORP(AtlasORPPower, AtlasORPi2c_addr);
AtlasScientificORP atlasORP(AtlasORPPower);

// Create a potential variable pointer for the ORP
Variable* atlasORPot = new AtlasScientificORP_Potential(
    &atlasORP, "12345678-abcd-1234-ef00-1234567890ab");
/** End [atlas_orp] */
#endif


#if defined MS_BUILD_SENSOR_ATLASPH
// ==========================================================================
//  Atlas Scientific EZO-pH Sensor
// ==========================================================================
/** Start [atlas_ph] */
#include <sensors/AtlasScientificpH.h>

const int8_t AtlaspHPower    = sensorPowerPin;  // Power pin (-1 if unconnected)
uint8_t      AtlaspHi2c_addr = 0x63;            // Default for pH is 0x63 (99)
// All Atlas sensors have different default I2C addresses, but any of them can
// be re-addressed to any 8 bit number.  If using the default address for any
// Atlas Scientific sensor, you may omit this argument.

// Create an Atlas Scientific pH sensor object
// AtlasScientificpH atlaspH(AtlaspHPower, AtlaspHi2c_addr);
AtlasScientificpH atlaspH(AtlaspHPower);

// Create a pH variable pointer for the pH sensor
Variable* atlaspHpH =
    new AtlasScientificpH_pH(&atlaspH, "12345678-abcd-1234-ef00-1234567890ab");
/** End [atlas_ph] */
#endif


#if defined MS_BUILD_SENSOR_ATLASRTD || defined MS_BUILD_SENSOR_ATLASEC
// ==========================================================================
//  Atlas Scientific EZO-RTD Temperature Sensor
// ==========================================================================
/** Start [atlas_rtd] */
#include <sensors/AtlasScientificRTD.h>

const int8_t AtlasRTDPower = sensorPowerPin;  // Power pin (-1 if unconnected)
uint8_t      AtlasRTDi2c_addr = 0x66;         // Default for RTD is 0x66 (102)
// All Atlas sensors have different default I2C addresses, but any of them can
// be re-addressed to any 8 bit number.  If using the default address for any
// Atlas Scientific sensor, you may omit this argument.

// Create an Atlas Scientific RTD sensor object
// AtlasScientificRTD atlasRTD(AtlasRTDPower, AtlasRTDi2c_addr);
AtlasScientificRTD atlasRTD(AtlasRTDPower);

// Create a temperature variable pointer for the RTD
Variable* atlasTemp = new AtlasScientificRTD_Temp(
    &atlasRTD, "12345678-abcd-1234-ef00-1234567890ab");
/** End [atlas_rtd] */
#endif


#if defined MS_BUILD_SENSOR_ATLASEC
// ==========================================================================
//  Atlas Scientific EZO-EC Conductivity Sensor
// ==========================================================================
/** Start [atlas_ec] */
#include <sensors/AtlasScientificEC.h>

const int8_t AtlasECPower    = sensorPowerPin;  // Power pin (-1 if unconnected)
uint8_t      AtlasECi2c_addr = 0x64;            // Default for EC is 0x64 (100)
// All Atlas sensors have different default I2C addresses, but any of them can
// be re-addressed to any 8 bit number.  If using the default address for any
// Atlas Scientific sensor, you may omit this argument.

// Create an Atlas Scientific Conductivity sensor object
// AtlasScientificEC atlasEC(AtlasECPower, AtlasECi2c_addr);
AtlasScientificEC atlasEC(AtlasECPower);

// Create four variable pointers for the EZO-ES
Variable* atlasCond = new AtlasScientificEC_Cond(
    &atlasEC, "12345678-abcd-1234-ef00-1234567890ab");
Variable* atlasTDS =
    new AtlasScientificEC_TDS(&atlasEC, "12345678-abcd-1234-ef00-1234567890ab");
Variable* atlasSal = new AtlasScientificEC_Salinity(
    &atlasEC, "12345678-abcd-1234-ef00-1234567890ab");
Variable* atlasGrav = new AtlasScientificEC_SpecificGravity(
    &atlasEC, "12345678-abcd-1234-ef00-1234567890ab");

// Create a calculated variable for the temperature compensated conductivity
// (that is, the specific conductance).  For this example, we will use the
// temperature measured by the Atlas RTD above this.  You could use the
// temperature returned by any other water temperature sensor if desired.
// **DO NOT** use your logger board temperature (ie, from the DS3231) to
// calculate specific conductance!
float calculateAtlasSpCond(void) {
    float spCond    = -9999;  // Always safest to start with a bad value
    float waterTemp = atlasTemp->getValue();
    float rawCond   = atlasCond->getValue();
    // ^^ Linearized temperature correction coefficient per degrees Celsius.
    // The value of 0.019 comes from measurements reported here:
    // Hayashi M. Temperature-electrical conductivity relation of water for
    // environmental monitoring and geophysical data inversion. Environ Monit
    // Assess. 2004 Aug-Sep;96(1-3):119-28.
    // doi: 10.1023/b:emas.0000031719.83065.68. PMID: 15327152.
    if (waterTemp != -9999 && rawCond != -9999) {
        // make sure both inputs are good
        float temperatureCoef = 0.019;
        spCond = rawCond / (1 + temperatureCoef * (waterTemp - 25.0));
    }
    return spCond;
}

// Properties of the calculated variable
// The number of digits after the decimal place
const uint8_t atlasSpCondResolution = 0;
// This must be a value from http://vocabulary.odm2.org/variablename/
const char* atlasSpCondName = "specificConductance";
// This must be a value from http://vocabulary.odm2.org/units/
const char* atlasSpCondUnit = "microsiemenPerCentimeter";
// A short code for the variable
const char* atlasSpCondCode = "atlasSpCond";
// The (optional) universallly unique identifier
const char* atlasSpCondUUID = "12345678-abcd-1234-ef00-1234567890ab";

// Finally, create the specific conductance variable and return a pointer to it
Variable* atlasSpCond =
    new Variable(calculateAtlasSpCond, atlasSpCondResolution, atlasSpCondName,
                 atlasSpCondUnit, atlasSpCondCode, atlasSpCondUUID);
/** End [atlas_ec] */
#endif


#if defined MS_BUILD_SENSOR_BME280
// ==========================================================================
//  Bosch BME280 Environmental Sensor
// ==========================================================================
/** Start [bme280] */
#include <sensors/BoschBME280.h>

const int8_t BME280Power = sensorPowerPin;  // Power pin (-1 if unconnected)
uint8_t      BMEi2c_addr = 0x76;
// The BME280 can be addressed either as 0x77 (Adafruit default) or 0x76 (Grove
// default) Either can be physically mofidied for the other address

// Create a Bosch BME280 sensor object
BoschBME280 bme280(BME280Power, BMEi2c_addr);

// Create four variable pointers for the BME280
Variable* bme280Humid =
    new BoschBME280_Humidity(&bme280, "12345678-abcd-1234-ef00-1234567890ab");
Variable* bme280Temp =
    new BoschBME280_Temp(&bme280, "12345678-abcd-1234-ef00-1234567890ab");
Variable* bme280Press =
    new BoschBME280_Pressure(&bme280, "12345678-abcd-1234-ef00-1234567890ab");
Variable* bme280Alt =
    new BoschBME280_Altitude(&bme280, "12345678-abcd-1234-ef00-1234567890ab");
/** End [bme280] */
#endif


#if defined MS_BUILD_SENSOR_OBS3
// ==========================================================================
//  Campbell OBS 3 / OBS 3+ Analog Turbidity Sensor
// ==========================================================================
/** Start [obs3] */
#include <sensors/CampbellOBS3.h>

const int8_t  OBS3Power = sensorPowerPin;  // Power pin (-1 if unconnected)
const uint8_t OBS3NumberReadings = 10;
const uint8_t OBS3ADSi2c_addr    = 0x48;  // The I2C address of the ADS1115 ADC

const int8_t OBSLowADSChannel = 0;  // ADS channel for *low* range output

// Campbell OBS 3+ *Low* Range Calibration in Volts
const float OBSLow_A = 0.000E+00;  // "A" value (X^2) [*low* range]
const float OBSLow_B = 1.000E+00;  // "B" value (X) [*low* range]
const float OBSLow_C = 0.000E+00;  // "C" value [*low* range]

// Create a Campbell OBS3+ *low* range sensor object
CampbellOBS3 osb3low(OBS3Power, OBSLowADSChannel, OBSLow_A, OBSLow_B, OBSLow_C,
                     OBS3ADSi2c_addr, OBS3NumberReadings);

// Create turbidity and voltage variable pointers for the low range  of the OBS3
Variable* obs3TurbLow = new CampbellOBS3_Turbidity(
    &osb3low, "12345678-abcd-1234-ef00-1234567890ab", "TurbLow");
Variable* obs3VoltLow = new CampbellOBS3_Voltage(
    &osb3low, "12345678-abcd-1234-ef00-1234567890ab", "TurbLowV");


const int8_t OBSHighADSChannel = 1;  // ADS channel for *high* range output

// Campbell OBS 3+ *High* Range Calibration in Volts
const float OBSHigh_A = 0.000E+00;  // "A" value (X^2) [*high* range]
const float OBSHigh_B = 1.000E+00;  // "B" value (X) [*high* range]
const float OBSHigh_C = 0.000E+00;  // "C" value [*high* range]

// Create a Campbell OBS3+ *high* range sensor object
CampbellOBS3 osb3high(OBS3Power, OBSHighADSChannel, OBSHigh_A, OBSHigh_B,
                      OBSHigh_C, OBS3ADSi2c_addr, OBS3NumberReadings);

// Create turbidity and voltage variable pointers for the high range of the OBS3
Variable* obs3TurbHigh = new CampbellOBS3_Turbidity(
    &osb3high, "12345678-abcd-1234-ef00-1234567890ab", "TurbHigh");
Variable* obs3VoltHigh = new CampbellOBS3_Voltage(
    &osb3high, "12345678-abcd-1234-ef00-1234567890ab", "TurbHighV");
/** End [obs3] */
#endif


#if defined MS_BUILD_SENSOR_CLARIVUE10
// ==========================================================================
//  Campbell ClariVUE Turbidity Sensor
// ==========================================================================
/** Start [clarivue] */
#include <sensors/CampbellClariVUE10.h>

const char* ClariVUESDI12address = "0";  // The SDI-12 Address of the ClariVUE10
const int8_t ClariVUEPower = sensorPowerPin;  // Power pin (-1 if unconnected)
const int8_t ClariVUEData  = 7;               // The SDI12 data pin
// NOTE:  you should NOT take more than one readings.  THe sensor already takes
// and averages 8 by default.

// Create a Campbell ClariVUE10 sensor object
CampbellClariVUE10 clarivue(*ClariVUESDI12address, ClariVUEPower, ClariVUEData);

// Create turbidity, temperature, and error variable pointers for the ClariVUE10
Variable* clarivueTurbidity = new CampbellClariVUE10_Turbidity(
    &clarivue, "12345678-abcd-1234-ef00-1234567890ab");
Variable* clarivueTemp = new CampbellClariVUE10_Temp(
    &clarivue, "12345678-abcd-1234-ef00-1234567890ab");
Variable* clarivueError = new CampbellClariVUE10_ErrorCode(
    &clarivue, "12345678-abcd-1234-ef00-1234567890ab");
/** End [clarivue] */
#endif


#if defined MS_BUILD_SENSOR_CTD
// ==========================================================================
//  Decagon CTD-10 Conductivity, Temperature, and Depth Sensor
// ==========================================================================
/** Start [decagonCTD] */
#include <sensors/DecagonCTD.h>

const char*   CTDSDI12address   = "1";    // The SDI-12 Address of the CTD
const uint8_t CTDNumberReadings = 6;      // The number of readings to average
const int8_t  CTDPower = sensorPowerPin;  // Power pin (-1 if unconnected)
const int8_t  CTDData  = 7;               // The SDI12 data pin

// Create a Decagon CTD sensor object
DecagonCTD ctd(*CTDSDI12address, CTDPower, CTDData, CTDNumberReadings);

// Create conductivity, temperature, and depth variable pointers for the CTD
Variable* ctdCond = new DecagonCTD_Cond(&ctd,
                                        "12345678-abcd-1234-ef00-1234567890ab");
Variable* ctdTemp = new DecagonCTD_Temp(&ctd,
                                        "12345678-abcd-1234-ef00-1234567890ab");
Variable* ctdDepth =
    new DecagonCTD_Depth(&ctd, "12345678-abcd-1234-ef00-1234567890ab");
/** End [decagonCTD] */
#endif


#if defined MS_BUILD_SENSOR_ES2
// ==========================================================================
//  Decagon ES2 Conductivity and Temperature Sensor
// ==========================================================================
/** Start [es2] */
#include <sensors/DecagonES2.h>

const char*   ES2SDI12address = "3";      // The SDI-12 Address of the ES2
const int8_t  ES2Power = sensorPowerPin;  // Power pin (-1 if unconnected)
const int8_t  ES2Data  = 7;               // The SDI12 data pin
const uint8_t ES2NumberReadings = 5;

// Create a Decagon ES2 sensor object
DecagonES2 es2(*ES2SDI12address, ES2Power, ES2Data, ES2NumberReadings);

// Create specific conductance and temperature variable pointers for the ES2
Variable* es2Cond = new DecagonES2_Cond(&es2,
                                        "12345678-abcd-1234-ef00-1234567890ab");
Variable* es2Temp = new DecagonES2_Temp(&es2,
                                        "12345678-abcd-1234-ef00-1234567890ab");
/** End [es2] */
#endif


#if defined MS_BUILD_SENSOR_VOLTAGE
// ==========================================================================
//  External Voltage via TI ADS1115
// ==========================================================================
/** Start [ext_volt] */
#include <sensors/ExternalVoltage.h>

const int8_t  ADSPower       = sensorPowerPin;  // Power pin (-1 if unconnected)
const int8_t  ADSChannel     = 2;               // The ADS channel of interest
const float   dividerGain    = 10;  //  Gain setting if using a voltage divider
const uint8_t evADSi2c_addr  = 0x48;  // The I2C address of the ADS1115 ADC
const uint8_t VoltReadsToAvg = 1;     // Only read one sample

// Create an External Voltage sensor object
ExternalVoltage extvolt(ADSPower, ADSChannel, dividerGain, evADSi2c_addr,
                        VoltReadsToAvg);

// Create a voltage variable pointer
Variable* extvoltV =
    new ExternalVoltage_Volt(&extvolt, "12345678-abcd-1234-ef00-1234567890ab");
/** End [ext_volt] */
#endif


#if defined MS_BUILD_SENSOR_MPL115A2
// ==========================================================================
//  Freescale Semiconductor MPL115A2 Barometer
// ==========================================================================
/** Start [mpl115a2] */
#include <sensors/FreescaleMPL115A2.h>

const int8_t  MPLPower = sensorPowerPin;  // Power pin (-1 if unconnected)
const uint8_t MPL115A2ReadingsToAvg = 1;

// Create an MPL115A2 barometer sensor object
MPL115A2 mpl115a2(MPLPower, MPL115A2ReadingsToAvg);

// Create pressure and temperature variable pointers for the MPL
Variable* mplPress =
    new MPL115A2_Pressure(&mpl115a2, "12345678-abcd-1234-ef00-1234567890ab");
Variable* mplTemp = new MPL115A2_Temp(&mpl115a2,
                                      "12345678-abcd-1234-ef00-1234567890ab");
/** End [mpl115a2] */
#endif


#if defined MS_BUILD_SENSOR_INSITURDO
// ==========================================================================
//  InSitu RDO PRO-X Rugged Dissolved Oxygen Probe
// ==========================================================================
/** Start [insitu_rdo] */
#include <sensors/InSituRDO.h>

const char*   RDOSDI12address = "5";      // The SDI-12 Address of the RDO PRO-X
const int8_t  RDOPower = sensorPowerPin;  // Power pin (-1 if unconnected)
const int8_t  RDOData  = 7;               // The SDI12 data pin
const uint8_t RDONumberReadings = 3;

// Create an In-Situ RDO PRO-X dissolved oxygen sensor object
InSituRDO insituRDO(*RDOSDI12address, RDOPower, RDOData, RDONumberReadings);

// Create dissolved oxygen percent, dissolved oxygen concentration, temperature,
// and oxygen partial pressure variable pointers for the RDO PRO-X
Variable* rdoDOpct =
    new InSituRDO_DOpct(&insituRDO, "12345678-abcd-1234-ef00-1234567890ab");
Variable* rdoDOmgL =
    new InSituRDO_DOmgL(&insituRDO, "12345678-abcd-1234-ef00-1234567890ab");
Variable* rdoTemp = new InSituRDO_Temp(&insituRDO,
                                       "12345678-abcd-1234-ef00-1234567890ab");
Variable* rdoO2pp =
    new InSituRDO_Pressure(&insituRDO, "12345678-abcd-1234-ef00-1234567890ab");
/** End [insitu_rdo] */
#endif


#if defined MS_BUILD_SENSOR_ACCULEVEL
// ==========================================================================
//  Keller Acculevel High Accuracy Submersible Level Transmitter
// ==========================================================================
/** Start [acculevel] */
#include <sensors/KellerAcculevel.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section

byte acculevelModbusAddress = 0x01;  // The modbus address of KellerAcculevel
const int8_t alAdapterPower = sensorPowerPin;  // RS485 adapter power pin
                                               // (-1 if unconnected)
const int8_t  acculevelPower = A3;             // Sensor power pin
const int8_t  al485EnablePin = -1;  // Adapter RE/DE pin (-1 if not applicable)
const uint8_t acculevelNumberReadings = 5;
// The manufacturer recommends taking and averaging a few readings

// Create a Keller Acculevel sensor object
KellerAcculevel acculevel(acculevelModbusAddress, modbusSerial, alAdapterPower,
                          acculevelPower, al485EnablePin,
                          acculevelNumberReadings);

// Create pressure, temperature, and height variable pointers for the Acculevel
Variable* acculevPress = new KellerAcculevel_Pressure(
    &acculevel, "12345678-abcd-1234-ef00-1234567890ab");
Variable* acculevTemp = new KellerAcculevel_Temp(
    &acculevel, "12345678-abcd-1234-ef00-1234567890ab");
Variable* acculevHeight = new KellerAcculevel_Height(
    &acculevel, "12345678-abcd-1234-ef00-1234567890ab");
/** End [acculevel] */
#endif


#if defined MS_BUILD_SENSOR_NANOLEVEL
// ==========================================================================
//  Keller Nanolevel High Accuracy Submersible Level Transmitter
// ==========================================================================
/** Start [nanolevel] */
#include <sensors/KellerNanolevel.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section

byte nanolevelModbusAddress = 0x01;  // The modbus address of KellerNanolevel
const int8_t nlAdapterPower = sensorPowerPin;  // RS485 adapter power pin
                                               // (-1 if unconnected)
const int8_t  nanolevelPower = A3;             // Sensor power pin
const int8_t  nl485EnablePin = -1;  // Adapter RE/DE pin (-1 if not applicable)
const uint8_t nanolevelNumberReadings = 5;
// The manufacturer recommends taking and averaging a few readings

// Create a Keller Nanolevel sensor object
KellerNanolevel nanolevel(nanolevelModbusAddress, modbusSerial, nlAdapterPower,
                          nanolevelPower, nl485EnablePin,
                          nanolevelNumberReadings);

// Create pressure, temperature, and height variable pointers for the Nanolevel
Variable* nanolevPress = new KellerNanolevel_Pressure(
    &nanolevel, "12345678-abcd-1234-ef00-1234567890ab");
Variable* nanolevTemp = new KellerNanolevel_Temp(
    &nanolevel, "12345678-abcd-1234-ef00-1234567890ab");
Variable* nanolevHeight = new KellerNanolevel_Height(
    &nanolevel, "12345678-abcd-1234-ef00-1234567890ab");
/** End [nanolevel] */
#endif


#if defined MS_BUILD_SENSOR_MAXBOTIX
// ==========================================================================
//  Maxbotix HRXL Ultrasonic Range Finder
// ==========================================================================
/** Start [maxbotics] */
#include <sensors/MaxBotixSonar.h>

// A Maxbotix sonar with the trigger pin disconnect CANNOT share the serial port
// A Maxbotix sonar using the trigger may be able to share but YMMV

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section

const int8_t SonarPower =
    sensorPowerPin;  // Excite (power) pin (-1 if unconnected)
const int8_t Sonar1Trigger =
    -1;  // Trigger pin (a unique negative number if unconnected)
const uint8_t sonar1NumberReadings = 3;  // The number of readings to average

// Create a MaxBotix Sonar sensor object
MaxBotixSonar sonar1(sonarSerial, SonarPower, Sonar1Trigger,
                     sonar1NumberReadings);

// Create an ultrasonic range variable pointer
Variable* sonar1Range =
    new MaxBotixSonar_Range(&sonar1, "12345678-abcd-1234-ef00-1234567890ab");
/** End [maxbotics] */
#endif


#if defined MS_BUILD_SENSOR_DS18 || defined MS_BUILD_SENSOR_ANALOGEC
// ==========================================================================
//  Maxim DS18 One Wire Temperature Sensor
// ==========================================================================
/** Start [ds18] */
#include <sensors/MaximDS18.h>

// OneWire Address [array of 8 hex characters]
// If only using a single sensor on the OneWire bus, you may omit the address
DeviceAddress OneWireAddress1 = {0x28, 0xFF, 0xBD, 0xBA,
                                 0x81, 0x16, 0x03, 0x0C};
const int8_t  OneWirePower = sensorPowerPin;  // Power pin (-1 if unconnected)
const int8_t  OneWireBus   = A0;  // OneWire Bus Pin (-1 if unconnected)
const int8_t  ds18NumberReadings = 3;

// Create a Maxim DS18 sensor objects (use this form for a known address)
MaximDS18 ds18(OneWireAddress1, OneWirePower, OneWireBus, ds18NumberReadings);

// Create a Maxim DS18 sensor object (use this form for a single sensor on bus
// with an unknown address)
// MaximDS18 ds18(OneWirePower, OneWireBus);

// Create a temperature variable pointer for the DS18
Variable* ds18Temp = new MaximDS18_Temp(&ds18,
                                        "12345678-abcd-1234-ef00-1234567890ab");
/** End [ds18] */
#endif


#if defined MS_BUILD_SENSOR_MS5803
// ==========================================================================
//  Measurement Specialties MS5803-14BA pressure sensor
// ==========================================================================
/** Start [ms5803] */
#include <sensors/MeaSpecMS5803.h>

const int8_t  MS5803Power = sensorPowerPin;  // Power pin (-1 if unconnected)
const uint8_t MS5803i2c_addr =
    0x76;  // The MS5803 can be addressed either as 0x76 (default) or 0x77
const int16_t MS5803maxPressure =
    14;  // The maximum pressure measurable by the specific MS5803 model
const uint8_t MS5803ReadingsToAvg = 1;

// Create a MeaSpec MS5803 pressure and temperature sensor object
MeaSpecMS5803 ms5803(MS5803Power, MS5803i2c_addr, MS5803maxPressure,
                     MS5803ReadingsToAvg);

// Create pressure and temperature variable pointers for the MS5803
Variable* ms5803Press =
    new MeaSpecMS5803_Pressure(&ms5803, "12345678-abcd-1234-ef00-1234567890ab");
Variable* ms5803Temp =
    new MeaSpecMS5803_Temp(&ms5803, "12345678-abcd-1234-ef00-1234567890ab");
/** End [ms5803] */
#endif


#if defined MS_BUILD_SENSOR_5TM
// ==========================================================================
//  Meter ECH2O Soil Moisture Sensor
// ==========================================================================
/** Start [fivetm] */
#include <sensors/Decagon5TM.h>

const char*  TMSDI12address = "2";             // The SDI-12 Address of the 5-TM
const int8_t TMPower        = sensorPowerPin;  // Power pin (-1 if unconnected)
const int8_t TMData         = 7;               // The SDI12 data pin

// Create a Decagon 5TM sensor object
Decagon5TM fivetm(*TMSDI12address, TMPower, TMData);

// Create the matric potential, volumetric water content, and temperature
// variable pointers for the 5TM
Variable* fivetmEa = new Decagon5TM_Ea(&fivetm,
                                       "12345678-abcd-1234-ef00-1234567890ab");
Variable* fivetmVWC =
    new Decagon5TM_VWC(&fivetm, "12345678-abcd-1234-ef00-1234567890ab");
Variable* fivetmTemp =
    new Decagon5TM_Temp(&fivetm, "12345678-abcd-1234-ef00-1234567890ab");
/** End [fivetm] */
#endif


#if defined MS_BUILD_SENSOR_HYDROS21
// ==========================================================================
//  Meter Hydros 21 Conductivity, Temperature, and Depth Sensor
// ==========================================================================
/** Start [hydros21] */
#include <sensors/MeterHydros21.h>

const char*   hydros21SDI12address = "1";  // The SDI-12 Address of the Hydros21
const uint8_t hydros21NumberReadings = 6;  // The number of readings to average
const int8_t  hydros21Power = sensorPowerPin;  // Power pin (-1 if unconnected)
const int8_t  hydros21Data  = 7;               // The SDI12 data pin

// Create a Decagon Hydros21 sensor object
MeterHydros21 hydros21(*hydros21SDI12address, hydros21Power, hydros21Data,
                       hydros21NumberReadings);

// Create conductivity, temperature, and depth variable pointers for the
// Hydros21
Variable* hydros21Cond =
    new MeterHydros21_Cond(&hydros21, "12345678-abcd-1234-ef00-1234567890ab");
Variable* hydros21Temp =
    new MeterHydros21_Temp(&hydros21, "12345678-abcd-1234-ef00-1234567890ab");
Variable* hydros21Depth =
    new MeterHydros21_Depth(&hydros21, "12345678-abcd-1234-ef00-1234567890ab");
/** End [hydros21] */
#endif


#if defined MS_BUILD_SENSOR_TEROS11
// ==========================================================================
//  Meter Teros 11 Soil Moisture Sensor
// ==========================================================================
/** Start [teros] */
#include <sensors/MeterTeros11.h>

const char*   teros11SDI12address = "4";  // The SDI-12 Address of the Teros 11
const int8_t  terosPower = sensorPowerPin;  // Power pin (-1 if unconnected)
const int8_t  terosData  = 7;               // The SDI12 data pin
const uint8_t teros11NumberReadings = 3;    // The number of readings to average

// Create a METER TEROS 11 sensor object
MeterTeros11 teros11(*teros11SDI12address, terosPower, terosData,
                     teros11NumberReadings);

// Create the matric potential, volumetric water content, and temperature
// variable pointers for the Teros 11
Variable* teros11Ea =
    new MeterTeros11_Ea(&teros11, "12345678-abcd-1234-ef00-1234567890ab");
Variable* teros11Temp =
    new MeterTeros11_Temp(&teros11, "12345678-abcd-1234-ef00-1234567890ab");
Variable* teros11VWC =
    new MeterTeros11_VWC(&teros11, "12345678-abcd-1234-ef00-1234567890ab");
/** End [teros] */
#endif


#if defined MS_BUILD_SENSOR_PALEOTERRA
// ==========================================================================
//  PaleoTerra Redox Sensors
// ==========================================================================
/** Start [pt_redox] */
#include <sensors/PaleoTerraRedox.h>

int8_t paleoTerraPower = sensorPowerPin;  // Pin to switch RS485 adapter power
                                          // on and off (-1 if unconnected)
uint8_t paleoI2CAddress = 0x68;           // the I2C address of the redox sensor

// Create the PaleoTerra sensor object
#ifdef MS_PALEOTERRA_SOFTWAREWIRE
PaleoTerraRedox ptRedox(&softI2C, paleoTerraPower, paleoI2CAddress);
// PaleoTerraRedox ptRedox(paleoTerraPower, softwareSDA, softwareSCL,
// paleoI2CAddress);
#else
PaleoTerraRedox ptRedox(paleoTerraPower, paleoI2CAddress);
#endif

// Create the voltage variable for the redox sensor
Variable* ptVolt =
    new PaleoTerraRedox_Volt(&ptRedox, "12345678-abcd-1234-ef00-1234567890ab");
/** End [pt_redox] */
#endif


#if defined MS_BUILD_SENSOR_RAINI2C
// ==========================================================================
//  External I2C Rain Tipping Bucket Counter
// ==========================================================================
/** Start [i2c_rain] */
#include <sensors/RainCounterI2C.h>

const uint8_t RainCounterI2CAddress = 0x08;
// I2C Address for EnviroDIY external tip counter; 0x08 by default
const float depthPerTipEvent = 0.2;  // rain depth in mm per tip event

// Create a Rain Counter sensor object
#ifdef MS_RAIN_SOFTWAREWIRE
RainCounterI2C tbi2c(&softI2C, RainCounterI2CAddress, depthPerTipEvent);
// RainCounterI2C tbi2c(softwareSDA, softwareSCL, RainCounterI2CAddress,
//                      depthPerTipEvent);
#else
RainCounterI2C  tbi2c(RainCounterI2CAddress, depthPerTipEvent);
#endif

// Create number of tips and rain depth variable pointers for the tipping bucket
Variable* tbi2cTips =
    new RainCounterI2C_Tips(&tbi2c, "12345678-abcd-1234-ef00-1234567890ab");
Variable* tbi2cDepth =
    new RainCounterI2C_Depth(&tbi2c, "12345678-abcd-1234-ef00-1234567890ab");
/** End [i2c_rain] */
#endif


#if defined MS_BUILD_SENSOR_TALLY
// ==========================================================================
//    Tally I2C Event Counter for rain or wind reed-switch sensors
// ==========================================================================
/** Start [i2c_wind_tally] */
#include <sensors/TallyCounterI2C.h>

const int8_t TallyPower = -1;  // Power pin (-1 if unconnected)
// NorthernWidget Tally I2CPower is -1 by default because it is often deployed
// with power always on, but Tally also has a super capacitor that enables it
// to be self powered between readings/recharge as described at
// https://github.com/EnviroDIY/Project-Tally

const uint8_t TallyCounterI2CAddress = 0x33;
// NorthernWidget Tally I2C address is 0x33 by default

// Create a Tally Counter sensor object
TallyCounterI2C tallyi2c(TallyPower, TallyCounterI2CAddress);

// Create variable pointers for the Tally event counter
Variable* tallyEvents = new TallyCounterI2C_Events(
    &tallyi2c, "12345678-abcd-1234-ef00-1234567890ab");

// For  Wind Speed, create a Calculated Variable that converts, similar to:
// period = loggingInterval * 60.0;    // in seconds
// frequency = tallyEventCount/period; // average event frequency in Hz
// tallyWindSpeed = frequency * 2.5 * 1.60934;  // in km/h
// 2.5 mph/Hz & 1.60934 kmph/mph and 2.5 mph/Hz conversion factor from
// web: Inspeed-Version-II-Reed-Switch-Anemometer-Sensor-Only-WS2R
/** End [i2c_wind_tally] */
#endif


#if defined MS_BUILD_SENSOR_INA219
// ==========================================================================
//  TI INA219 High Side Current/Voltage Sensor (Current mA, Voltage, Power)
// ==========================================================================
/** Start [ina219] */
#include <sensors/TIINA219.h>

const int8_t INA219Power    = sensorPowerPin;  // Power pin (-1 if unconnected)
uint8_t      INA219i2c_addr = 0x40;            // 1000000 (Board A0+A1=GND)
// The INA219 can have one of 16 addresses, depending on the connections of A0
// and A1
const uint8_t INA219ReadingsToAvg = 1;

// Create an INA219 sensor object
TIINA219 ina219(INA219Power, INA219i2c_addr, INA219ReadingsToAvg);

// Create current, voltage, and power variable pointers for the INA219
Variable* inaCurrent =
    new TIINA219_Current(&ina219, "12345678-abcd-1234-ef00-1234567890ab");
Variable* inaVolt  = new TIINA219_Volt(&ina219,
                                      "12345678-abcd-1234-ef00-1234567890ab");
Variable* inaPower = new TIINA219_Power(&ina219,
                                        "12345678-abcd-1234-ef00-1234567890ab");
/** End [ina219] */
#endif


#if defined MS_BUILD_SENSOR_CYCLOPS
// ==========================================================================
//  Turner Cyclops-7F Submersible Fluorometer
// ==========================================================================
/** Start [cyclops] */
#include <sensors/TurnerCyclops.h>

const int8_t  cyclopsPower = sensorPowerPin;  // Power pin (-1 if unconnected)
const uint8_t cyclopsNumberReadings = 10;
const uint8_t cyclopsADSi2c_addr = 0x48;  // The I2C address of the ADS1115 ADC
const int8_t  cyclopsADSChannel  = 0;     // ADS channel

// Cyclops calibration information
const float cyclopsStdConc = 1.000;  // Concentration of the standard used
                                     // for a 1-point sensor calibration.
const float cyclopsStdVolt =
    1.000;  // The voltage (in volts) measured for the conc_std.
const float cyclopsBlankVolt =
    0.000;  // The voltage (in volts) measured for a blank.

// Create a Turner Cyclops sensor object
TurnerCyclops cyclops(cyclopsPower, cyclopsADSChannel, cyclopsStdConc,
                      cyclopsStdVolt, cyclopsBlankVolt, cyclopsADSi2c_addr,
                      cyclopsNumberReadings);

// Create the voltage variable pointer - used for any type of Cyclops
Variable* cyclopsVoltage =
    new TurnerCyclops_Voltage(&cyclops, "12345678-abcd-1234-ef00-1234567890ab");

// Create the variable pointer for the primary output parameter.  Only use
// **ONE** of these!  Which is possible depends on your specific sensor!
Variable* cyclopsChloro = new TurnerCyclops_Chlorophyll(
    &cyclops, "12345678-abcd-1234-ef00-1234567890ab");
Variable* cyclopsRWT = new TurnerCyclops_Rhodamine(
    &cyclops, "12345678-abcd-1234-ef00-1234567890ab");
Variable* cyclopsFluoroscein = new TurnerCyclops_Fluorescein(
    &cyclops, "12345678-abcd-1234-ef00-1234567890ab");
Variable* cyclopsPhycocyanin = new TurnerCyclops_Phycocyanin(
    &cyclops, "12345678-abcd-1234-ef00-1234567890ab");
Variable* cyclopsPhycoerythrin = new TurnerCyclops_Phycoerythrin(
    &cyclops, "12345678-abcd-1234-ef00-1234567890ab");
Variable* cyclopsCDOM =
    new TurnerCyclops_CDOM(&cyclops, "12345678-abcd-1234-ef00-1234567890ab");
Variable* cyclopsCrudeOil = new TurnerCyclops_CrudeOil(
    &cyclops, "12345678-abcd-1234-ef00-1234567890ab");
Variable* cyclopsBrighteners = new TurnerCyclops_Brighteners(
    &cyclops, "12345678-abcd-1234-ef00-1234567890ab");
Variable* cyclopsTurbidity = new TurnerCyclops_Turbidity(
    &cyclops, "12345678-abcd-1234-ef00-1234567890ab");
Variable* cyclopsPTSA =
    new TurnerCyclops_PTSA(&cyclops, "12345678-abcd-1234-ef00-1234567890ab");
Variable* cyclopsBTEX =
    new TurnerCyclops_BTEX(&cyclops, "12345678-abcd-1234-ef00-1234567890ab");
Variable* cyclopsTryptophan = new TurnerCyclops_Tryptophan(
    &cyclops, "12345678-abcd-1234-ef00-1234567890ab");
Variable* cyclopsRedChloro = new TurnerCyclops_RedChlorophyll(
    &cyclops, "12345678-abcd-1234-ef00-1234567890ab");
/** End [cyclops] */
#endif


#if defined MS_BUILD_SENSOR_ANALOGEC
// ==========================================================================
//   Analog Electrical Conductivity using the Processor's Analog Pins
// ==========================================================================
/** Start [analog_cond] */
#include <sensors/AnalogElecConductivity.h>

const int8_t ECpwrPin   = A4;  // Power pin (-1 if unconnected)
const int8_t ECdataPin1 = A0;  // Data pin (must be an analog pin, ie A#)

// Create an Analog Electrical Conductivity sensor object
AnalogElecConductivity analogEC_phy(ECpwrPin, ECdataPin1);

// Create a conductivity variable pointer for the analog sensor
Variable* analogEc_cond = new AnalogElecConductivity_EC(
    &analogEC_phy, "12345678-abcd-1234-ef00-1234567890ab");

// Create a calculated variable for the temperature compensated conductivity
// (that is, the specific conductance).  For this example, we will use the
// temperature measured by the Maxim DS18 saved as ds18Temp several sections
// above this.  You could use the temperature returned by any other water
// temperature sensor if desired.  **DO NOT** use your logger board temperature
// (ie, from the DS3231) to calculate specific conductance!
float calculateAnalogSpCond(void) {
    float spCond          = -9999;  // Always safest to start with a bad value
    float waterTemp       = ds18Temp->getValue();
    float rawCond         = analogEc_cond->getValue();
    float temperatureCoef = 0.019;
    // ^^ Linearized temperature correction coefficient per degrees Celsius.
    // The value of 0.019 comes from measurements reported here:
    // Hayashi M. Temperature-electrical conductivity relation of water for
    // environmental monitoring and geophysical data inversion. Environ Monit
    // Assess. 2004 Aug-Sep;96(1-3):119-28.
    // doi: 10.1023/b:emas.0000031719.83065.68. PMID: 15327152.
    if (waterTemp != -9999 && rawCond != -9999) {
        // make sure both inputs are good
        spCond = rawCond / (1 + temperatureCoef * (waterTemp - 25.0));
    }
    return spCond;
}

// Properties of the calculated variable
// The number of digits after the decimal place
const uint8_t analogSpCondResolution = 0;
// This must be a value from http://vocabulary.odm2.org/variablename/
const char* analogSpCondName = "specificConductance";
// This must be a value from http://vocabulary.odm2.org/units/
const char* analogSpCondUnit = "microsiemenPerCentimeter";
// A short code for the variable
const char* analogSpCondCode = "anlgSpCond";
// The (optional) universallly unique identifier
const char* analogSpCondUUID = "12345678-abcd-1234-ef00-1234567890ab";

// Finally, Create the specific conductance variable and return a pointer to it
Variable* analogEc_spcond = new Variable(
    calculateAnalogSpCond, analogSpCondResolution, analogSpCondName,
    analogSpCondUnit, analogSpCondCode, analogSpCondUUID);
/** End [analog_cond] */
#endif


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

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section

byte         y504ModbusAddress = 0x04;  // The modbus address of the Y504
const int8_t y504AdapterPower  = sensorPowerPin;  // RS485 adapter power pin
                                                  // (-1 if unconnected)
const int8_t  y504SensorPower = A3;               // Sensor power pin
const int8_t  y504EnablePin   = -1;  // Adapter RE/DE pin (-1 if not applicable)
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, y504AdapterPower,
                    y504SensorPower, y504EnablePin, 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] */
#endif


#if defined MS_BUILD_SENSOR_Y510
// ==========================================================================
//  Yosemitech Y510 Turbidity Sensor
// ==========================================================================
/** Start [y510] */
#include <sensors/YosemitechY510.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section

byte         y510ModbusAddress = 0x0B;  // The modbus address of the Y510
const int8_t y510AdapterPower  = sensorPowerPin;  // RS485 adapter power pin
                                                  // (-1 if unconnected)
const int8_t  y510SensorPower = A3;               // Sensor power pin
const int8_t  y510EnablePin   = -1;  // Adapter RE/DE pin (-1 if not applicable)
const uint8_t y510NumberReadings = 5;
// The manufacturer recommends averaging 10 readings, but we take 5 to minimize
// power consumption

// Create a Y510-B Turbidity sensor object
YosemitechY510 y510(y510ModbusAddress, modbusSerial, y510AdapterPower,
                    y510SensorPower, y510EnablePin, y510NumberReadings);

// Create turbidity and temperature variable pointers for the Y510
Variable* y510Turb =
    new YosemitechY510_Turbidity(&y510, "12345678-abcd-1234-ef00-1234567890ab");
Variable* y510Temp =
    new YosemitechY510_Temp(&y510, "12345678-abcd-1234-ef00-1234567890ab");
/** End [y510] */
#endif


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

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section

byte         y511ModbusAddress = 0x1A;  // The modbus address of the Y511
const int8_t y511AdapterPower  = sensorPowerPin;  // RS485 adapter power pin
                                                  // (-1 if unconnected)
const int8_t  y511SensorPower = A3;               // Sensor power pin
const int8_t  y511EnablePin   = -1;  // Adapter RE/DE pin (-1 if not applicable)
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, y511AdapterPower,
                    y511SensorPower, y511EnablePin, 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] */
#endif


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

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section

byte         y514ModbusAddress = 0x14;  // The modbus address of the Y514
const int8_t y514AdapterPower =
    sensorPowerPin;  // RS485 adapter power pin (-1 if unconnected)
const int8_t  y514SensorPower = A3;  // Sensor power pin
const int8_t  y514EnablePin   = -1;  // Adapter RE/DE pin (-1 if not applicable)
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, y514AdapterPower,
                    y514SensorPower, y514EnablePin, 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] */
#endif


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

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section

byte         y520ModbusAddress = 0x20;  // The modbus address of the Y520
const int8_t y520AdapterPower  = sensorPowerPin;  // RS485 adapter power pin
                                                  // (-1 if unconnected)
const int8_t  y520SensorPower = A3;               // Sensor power pin
const int8_t  y520EnablePin   = -1;  // Adapter RE/DE pin (-1 if not applicable)
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, y520AdapterPower,
                    y520SensorPower, y520EnablePin, 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] */
#endif


#if defined MS_BUILD_SENSOR_Y532
// ==========================================================================
//  Yosemitech Y532 pH
// ==========================================================================
/** Start [y532] */
#include <sensors/YosemitechY532.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section

byte         y532ModbusAddress = 0x32;  // The modbus address of the Y532
const int8_t y532AdapterPower  = sensorPowerPin;  // RS485 adapter power pin
                                                  // (-1 if unconnected)
const int8_t  y532SensorPower = A3;               // Sensor power pin
const int8_t  y532EnablePin   = 4;  // Adapter RE/DE pin (-1 if not applicable)
const uint8_t y532NumberReadings = 1;
// The manufacturer actually doesn't mention averaging for this one

// Create a Yosemitech Y532 pH sensor object
YosemitechY532 y532(y532ModbusAddress, modbusSerial, y532AdapterPower,
                    y532SensorPower, y532EnablePin, y532NumberReadings);

// Create pH, electrical potential, and temperature variable pointers for the
// Y532
Variable* y532Voltage =
    new YosemitechY532_Voltage(&y532, "12345678-abcd-1234-ef00-1234567890ab");
Variable* y532pH =
    new YosemitechY532_pH(&y532, "12345678-abcd-1234-ef00-1234567890ab");
Variable* y532Temp =
    new YosemitechY532_Temp(&y532, "12345678-abcd-1234-ef00-1234567890ab");
/** End [y532] */
#endif


#if defined MS_BUILD_SENSOR_Y533
// ==========================================================================
//  Yosemitech Y533 Oxidation Reduction Potential (ORP)
// ==========================================================================
/** Start [y533] */
#include <sensors/YosemitechY533.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section

byte         y533ModbusAddress = 0x32;  // The modbus address of the Y533
const int8_t y533AdapterPower  = sensorPowerPin;  // RS485 adapter power pin
                                                  // (-1 if unconnected)
const int8_t  y533SensorPower = A3;               // Sensor power pin
const int8_t  y533EnablePin   = 4;  // Adapter RE/DE pin (-1 if not applicable)
const uint8_t y533NumberReadings = 1;
// The manufacturer actually doesn't mention averaging for this one

// Create a Yosemitech Y533 ORP sensor object
YosemitechY533 y533(y533ModbusAddress, modbusSerial, y533AdapterPower,
                    y533SensorPower, y533EnablePin, y533NumberReadings);

// Create ORP and temperature variable pointers for the Y533
Variable* y533ORP =
    new YosemitechY533_ORP(&y533, "12345678-abcd-1234-ef00-1234567890ab");
Variable* y533Temp =
    new YosemitechY533_Temp(&y533, "12345678-abcd-1234-ef00-1234567890ab");
/** End [y533] */
#endif


#if defined MS_BUILD_SENSOR_Y550
// ==========================================================================
//  Yosemitech Y550 COD Sensor with Wiper
// ==========================================================================
/** Start [y550] */
#include <sensors/YosemitechY550.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section

byte         y550ModbusAddress = 0x50;  // The modbus address of the Y550
const int8_t y550AdapterPower  = sensorPowerPin;  // RS485 adapter power pin
                                                  // (-1 if unconnected)
const int8_t  y550SensorPower = A3;               // Sensor power pin
const int8_t  y550EnablePin   = -1;  // Adapter RE/DE pin (-1 if not applicable)
const uint8_t y550NumberReadings = 5;
// The manufacturer recommends averaging 10 readings, but we take 5 to minimize
// power consumption

// Create a Y550 chemical oxygen demand sensor object
YosemitechY550 y550(y550ModbusAddress, modbusSerial, y550AdapterPower,
                    y550SensorPower, y550EnablePin, y550NumberReadings);

// Create COD, turbidity, and temperature variable pointers for the Y550
Variable* y550COD =
    new YosemitechY550_COD(&y550, "12345678-abcd-1234-ef00-1234567890ab");
Variable* y550Turbid =
    new YosemitechY550_Turbidity(&y550, "12345678-abcd-1234-ef00-1234567890ab");
Variable* y550Temp =
    new YosemitechY550_Temp(&y550, "12345678-abcd-1234-ef00-1234567890ab");
/** End [y550] */
#endif


#if defined MS_BUILD_SENSOR_Y4000
// ==========================================================================
//  Yosemitech Y4000 Multiparameter Sonde (DOmgL, Turbidity, Cond, pH, Temp,
//    ORP, Chlorophyll, BGA)
// ==========================================================================
/** Start [y4000] */
#include <sensors/YosemitechY4000.h>

// NOTE: Extra hardware and software serial ports are created in the "Settings
// for Additional Serial Ports" section

byte         y4000ModbusAddress = 0x05;  // The modbus address of the Y4000
const int8_t y4000AdapterPower  = sensorPowerPin;  // RS485 adapter power pin
                                                   // (-1 if unconnected)
const int8_t  y4000SensorPower = A3;               // Sensor power pin
const int8_t  y4000EnablePin = -1;  // Adapter RE/DE pin (-1 if not applicable)
const uint8_t y4000NumberReadings = 5;
// The manufacturer recommends averaging 10 readings, but we take 5 to minimize
// power consumption

// Create a Yosemitech Y4000 multi-parameter sensor object
YosemitechY4000 y4000(y4000ModbusAddress, modbusSerial, y4000AdapterPower,
                      y4000SensorPower, y4000EnablePin, y4000NumberReadings);

// Create all of the variable pointers for the Y4000
Variable* y4000DO =
    new YosemitechY4000_DOmgL(&y4000, "12345678-abcd-1234-ef00-1234567890ab");
Variable* y4000Turb = new YosemitechY4000_Turbidity(
    &y4000, "12345678-abcd-1234-ef00-1234567890ab");
Variable* y4000Cond =
    new YosemitechY4000_Cond(&y4000, "12345678-abcd-1234-ef00-1234567890ab");
Variable* y4000pH =
    new YosemitechY4000_pH(&y4000, "12345678-abcd-1234-ef00-1234567890ab");
Variable* y4000Temp =
    new YosemitechY4000_Temp(&y4000, "12345678-abcd-1234-ef00-1234567890ab");
Variable* y4000ORP =
    new YosemitechY4000_ORP(&y4000, "12345678-abcd-1234-ef00-1234567890ab");
Variable* y4000Chloro = new YosemitechY4000_Chlorophyll(
    &y4000, "12345678-abcd-1234-ef00-1234567890ab");
Variable* y4000BGA =
    new YosemitechY4000_BGA(&y4000, "12345678-abcd-1234-ef00-1234567890ab");
/** End [y4000] */
#endif


#if defined MS_BUILD_SENSOR_DOPTO
// ==========================================================================
//  Zebra Tech D-Opto Dissolved Oxygen Sensor
// ==========================================================================
/** Start [dopto] */
#include <sensors/ZebraTechDOpto.h>

const char*  DOptoSDI12address = "5";   // The SDI-12 Address of the D-Opto
const int8_t ZTPower = sensorPowerPin;  // Power pin (-1 if unconnected)
const int8_t ZTData  = 7;               // The SDI12 data pin

// Create a Zebra Tech DOpto dissolved oxygen sensor object
ZebraTechDOpto dopto(*DOptoSDI12address, ZTPower, ZTData);

// Create dissolved oxygen percent, dissolved oxygen concentration, and
// temperature variable pointers for the Zebra Tech
Variable* dOptoDOpct =
    new ZebraTechDOpto_DOpct(&dopto, "12345678-abcd-1234-ef00-1234567890ab");
Variable* dOptoDOmgL =
    new ZebraTechDOpto_DOmgL(&dopto, "12345678-abcd-1234-ef00-1234567890ab");
Variable* dOptoTemp =
    new ZebraTechDOpto_Temp(&dopto, "12345678-abcd-1234-ef00-1234567890ab");
/** End [dopto] */
#endif


// ==========================================================================
//  Calculated Variable[s]
// ==========================================================================
/** Start [calculated_variables] */
// Create the function to give your calculated result.
// The function should take no input (void) and return a float.
// You can use any named variable pointers to access values by way of
// variable->getValue()

float calculateVariableValue(void) {
    float calculatedResult = -9999;  // Always safest to start with a bad value
    // float inputVar1 = variable1->getValue();
    // float inputVar2 = variable2->getValue();
    // make sure both inputs are good
    // if (inputVar1 != -9999 && inputVar2 != -9999) {
    //     calculatedResult = inputVar1 + inputVar2;
    // }
    return calculatedResult;
}

// Properties of the calculated variable
// The number of digits after the decimal place
const uint8_t calculatedVarResolution = 3;
// This must be a value from http://vocabulary.odm2.org/variablename/
const char* calculatedVarName = "varName";
// This must be a value from http://vocabulary.odm2.org/units/
const char* calculatedVarUnit = "varUnit";
// A short code for the variable
const char* calculatedVarCode = "calcVar";
// The (optional) universallly unique identifier
const char* calculatedVarUUID = "12345678-abcd-1234-ef00-1234567890ab";

// Finally, Create a calculated variable and return a variable pointer to it
Variable* calculatedVar = new Variable(
    calculateVariableValue, calculatedVarResolution, calculatedVarName,
    calculatedVarUnit, calculatedVarCode, calculatedVarUUID);
/** End [calculated_variables] */


#if defined MS_BUILD_TEST_CREATE_IN_ARRAY
// ==========================================================================
//  Creating the Variable Array[s] and Filling with Variable Objects
//  NOTE:  This shows three differnt ways of creating the same variable array
//         and filling it with variables
// ==========================================================================
/** Start [variables_create_in_array] */
// Version 1: Create pointers for all of the variables from the sensors,
// at the same time putting them into an array
Variable* variableList[] = {
    new ProcessorStats_SampleNumber(&mcuBoard,
                                    "12345678-abcd-1234-ef00-1234567890ab"),
    new ProcessorStats_FreeRam(&mcuBoard,
                               "12345678-abcd-1234-ef00-1234567890ab"),
    new ProcessorStats_Battery(&mcuBoard,
                               "12345678-abcd-1234-ef00-1234567890ab"),
    new MaximDS3231_Temp(&ds3231, "12345678-abcd-1234-ef00-1234567890ab"),
    //  ... Add more variables as needed!
    new Modem_RSSI(&modem, "12345678-abcd-1234-ef00-1234567890ab"),
    new Modem_SignalPercent(&modem, "12345678-abcd-1234-ef00-1234567890ab"),
    new Modem_Temp(&modem, "12345678-abcd-1234-ef00-1234567890ab"),
    new Variable(calculateVariableValue, calculatedVarResolution,
                 calculatedVarName, calculatedVarUnit, calculatedVarCode,
                 calculatedVarUUID),
};
// Count up the number of pointers in the array
int variableCount = sizeof(variableList) / sizeof(variableList[0]);
// Create the VariableArray object
VariableArray varArray(variableCount, variableList);
/** End [variables_create_in_array] */
#endif
// ==========================================================================


#if defined MS_BUILD_TEST_SEPARATE_UUIDS
/** Start [variables_separate_uuids] */
// Version 2: Create two separate arrays, on for the variables and a separate
// one for the UUID's, then give both as input to the variable array
// constructor.  Be cautious when doing this though because order is CRUCIAL!
Variable* variableList[] = {
    new ProcessorStats_SampleNumber(&mcuBoard),
    new ProcessorStats_FreeRam(&mcuBoard),
    new ProcessorStats_Battery(&mcuBoard),
    new MaximDS3231_Temp(&ds3231),
    //  ... Add all of your variables!
    new Modem_RSSI(&modem),
    new Modem_SignalPercent(&modem),
    new Modem_Temp(&modem),
    new Variable(calculateVariableValue, calculatedVarResolution,
                 calculatedVarName, calculatedVarUnit, calculatedVarCode),
};
const char* UUIDs[] = {
    "12345678-abcd-1234-ef00-1234567890ab",
    //  ... The number of UUID's must match the number of variables!
    "12345678-abcd-1234-ef00-1234567890ab",
};
// Count up the number of pointers in the array
int variableCount = sizeof(variableList) / sizeof(variableList[0]);
// Create the VariableArray object and attach the UUID's
VariableArray varArray(variableCount, variableList, UUIDs);
/** End [variables_separate_uuids] */
#endif
// ==========================================================================


#if defined MS_BUILD_TEST_PRE_NAMED_VARS
/** Start [variables_pre_named] */
// Version 3: Fill array with already created and named variable pointers
Variable* variableList[] = {
    mcuBoardSampNo,
    mcuBoardAvailableRAM,
    mcuBoardBatt,
    calculatedVar,
#if defined ARDUINO_ARCH_AVR || defined MS_SAMD_DS3231
    ds3231Temp,
#endif
#if defined MS_BUILD_SENSOR_AM2315
    am2315Humid,
    am2315Temp,
#endif
#if defined MS_BUILD_SENSOR_DHT
    dhtHumid,
    dhtTemp,
    dhtHI,
#endif
#if defined MS_BUILD_SENSOR_SQ212
    sq212PAR,
    sq212voltage,
#endif
#if defined MS_BUILD_SENSOR_ATLASCO2
    atlasCO2CO2,
    atlasCO2Temp,
#endif
#if defined MS_BUILD_SENSOR_ATLASDO
    atlasDOconc,
    atlasDOpct,
#endif
#if defined MS_BUILD_SENSOR_ATLASORP
    atlasORPot,
#endif
#if defined MS_BUILD_SENSOR_ATLASPH
    atlaspHpH,
#endif
#if defined MS_BUILD_SENSOR_ATLASRTD
    atlasTemp,
#endif
#if defined MS_BUILD_SENSOR_ATLASEC
    atlasCond,
    atlasTDS,
    atlasSal,
    atlasGrav,
    atlasSpCond,
#endif
#if defined MS_BUILD_SENSOR_BME280
    bme280Temp,
    bme280Humid,
    bme280Press,
    bme280Alt,
#endif
#if defined MS_BUILD_SENSOR_OBS3
    obs3TurbLow,
    obs3VoltLow,
    obs3TurbHigh,
    obs3VoltHigh,
#endif
#if defined MS_BUILD_SENSOR_CTD
    ctdCond,
    ctdTemp,
    ctdDepth,
#endif
#if defined MS_BUILD_SENSOR_ES2
    es2Cond,
    es2Temp,
#endif
#if defined MS_BUILD_SENSOR_VOLTAGE
    extvoltV,
#endif
#if defined MS_BUILD_SENSOR_MPL115A2
    mplTemp,
    mplPress,
#endif
#if defined MS_BUILD_SENSOR_INSITURDO
    rdoTemp,
    rdoDOpct,
    rdoDOmgL,
    rdoO2pp,
#endif
#if defined MS_BUILD_SENSOR_ACCULEVEL
    acculevPress,
    acculevTemp,
    acculevHeight,
#endif
#if defined MS_BUILD_SENSOR_NANOLEVEL
    nanolevPress,
    nanolevTemp,
    nanolevHeight,
#endif
#if defined MS_BUILD_SENSOR_MAXBOTIX
    sonar1Range,
#endif
#if defined MS_BUILD_SENSOR_DS18
    ds18Temp,
#endif
#if defined MS_BUILD_SENSOR_MS5803
    ms5803Temp,
    ms5803Press,
#endif
#if defined MS_BUILD_SENSOR_5TM
    fivetmEa,
    fivetmVWC,
    fivetmTemp,
#endif
#if defined MS_BUILD_SENSOR_HYDROS21
    hydros21Cond,
    hydros21Temp,
    hydros21Depth,
#endif
#if defined MS_BUILD_SENSOR_TEROS11
    teros11Ea,
    teros11Temp,
    teros11VWC,
#endif
#if defined MS_BUILD_SENSOR_PALEOTERRA
    ptVolt,
#endif
#if defined MS_BUILD_SENSOR_RAINI2C
    tbi2cTips,
    tbi2cDepth,
#endif
#if defined MS_BUILD_SENSOR_TALLY
    tallyEvents,
#endif
#if defined MS_BUILD_SENSOR_INA219
    inaVolt,
    inaCurrent,
    inaPower,
#endif
#if defined MS_BUILD_SENSOR_CYCLOPS
    cyclopsVoltage,
    cyclopsChloro,
    cyclopsRWT,
    cyclopsFluoroscein,
    cyclopsPhycocyanin,
    cyclopsPhycoerythrin,
    cyclopsCDOM,
    cyclopsCrudeOil,
    cyclopsBrighteners,
    cyclopsTurbidity,
    cyclopsPTSA,
    cyclopsBTEX,
    cyclopsTryptophan,
    cyclopsRedChloro,
#endif
#if defined MS_BUILD_SENSOR_ANALOGEC
    analogEc_cond,
    analogEc_spcond,
#endif
#if defined MS_BUILD_SENSOR_Y504
    y504DOpct,
    y504DOmgL,
    y504Temp,
#endif
#if defined MS_BUILD_SENSOR_Y510
    y510Turb,
    y510Temp,
#endif
#if defined MS_BUILD_SENSOR_Y511
    y511Turb,
    y511Temp,
#endif
#if defined MS_BUILD_SENSOR_Y514
    y514Chloro,
    y514Temp,
#endif
#if defined MS_BUILD_SENSOR_Y520
    y520Cond,
    y520Temp,
#endif
#if defined MS_BUILD_SENSOR_Y532
    y532Voltage,
    y532pH,
    y532Temp,
#endif
#if defined MS_BUILD_SENSOR_Y533
    y533ORP,
    y533Temp,
#endif
#if defined MS_BUILD_SENSOR_Y550
    y550COD,
    y550Turbid,
    y550Temp,
#endif
#if defined MS_BUILD_SENSOR_Y4000
    y4000DO,
    y4000Turb,
    y4000Cond,
    y4000pH,
    y4000Temp,
    y4000ORP,
    y4000Chloro,
    y4000BGA,
#endif
#if defined MS_BUILD_SENSOR_DOPTO
    dOptoDOpct,
    dOptoDOmgL,
    dOptoTemp,
#endif
    modemRSSI,
    modemSignalPct,
#ifdef TINY_GSM_MODEM_HAS_BATTERY
    modemBatteryState,
    modemBatteryPct,
    modemBatteryVoltage,
#endif
#ifdef TINY_GSM_MODEM_HAS_TEMPERATURE
    modemTemperature,
#endif
};
// Count up the number of pointers in the array
int variableCount = sizeof(variableList) / sizeof(variableList[0]);
// Create the VariableArray object
VariableArray varArray(variableCount, variableList);
/** End [variables_pre_named] */
#endif


// ==========================================================================
//  The Logger Object[s]
// ==========================================================================
/** Start [loggers] */
// Create a new logger instance
Logger dataLogger(LoggerID, loggingInterval, &varArray);
/** End [loggers] */


#if defined MS_BUILD_PUB_MMW
// ==========================================================================
//  A Publisher to Monitor My Watershed / EnviroDIY Data Sharing Portal
// ==========================================================================
/** Start [monitormw] */
// Device registration and sampling feature information can be obtained after
// registration at https://monitormywatershed.org or https://data.envirodiy.org
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
#include <publishers/EnviroDIYPublisher.h>
EnviroDIYPublisher EnviroDIYPOST(dataLogger, &modem.gsmClient,
                                 registrationToken, samplingFeature);
/** End [monitormw] */
#endif


#if defined MS_BUILD_PUB_DREAMHOST
// ==========================================================================
//  A Publisher to DreamHost
// ==========================================================================
/** Start [dreamhost] */
// NOTE:  This is an outdated data collection tool used by the Stroud Center.
// It very, very unlikely that you will use this.

const char* DreamHostPortalRX = "xxxx";

// Create a data publisher to DreamHost
#include <publishers/DreamHostPublisher.h>
DreamHostPublisher DreamHostGET(dataLogger, &modem.gsmClient,
                                DreamHostPortalRX);
/** End [dreamhost] */
#endif


#if defined MS_BUILD_PUB_THINGSPEAK
// ==========================================================================
//  ThingSpeak Data Publisher
// ==========================================================================
/** Start [thingspeak] */
// Create a channel with fields on ThingSpeak in advance.
// The fields will be sent in exactly the order they are in the variable array.
// Any custom name or identifier given to the field on ThingSpeak is irrelevant.
// No more than 8 fields of data can go to any one channel.  Any fields beyond
// the eighth in the array will be ignored.
const char* thingSpeakMQTTKey =
    "XXXXXXXXXXXXXXXX";  // Your MQTT API Key from Account > MyProfile.
const char* thingSpeakChannelID =
    "######";  // The numeric channel id for your channel
const char* thingSpeakChannelKey =
    "XXXXXXXXXXXXXXXX";  // The Write API Key for your channel

// Create a data publisher for ThingSpeak
#include <publishers/ThingSpeakPublisher.h>
ThingSpeakPublisher TsMqtt(dataLogger, &modem.gsmClient, thingSpeakMQTTKey,
                           thingSpeakChannelID, thingSpeakChannelKey);
/** End [thingspeak] */
#endif


// ==========================================================================
//  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);
        delay(rate);
        digitalWrite(greenLED, LOW);
        digitalWrite(redLED, HIGH);
        delay(rate);
    }
    digitalWrite(redLED, LOW);
}

// Uses the processor sensor object to read 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
// ==========================================================================
void setup() {
/** Start [setup_wait] */
// 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
#if defined SERIAL_PORT_USBVIRTUAL
    while (!SERIAL_PORT_USBVIRTUAL && (millis() < 10000L)) {}
#endif
    /** End [setup_wait] */

    /** Start [setup_prints] */
    // Start the primary serial connection
    Serial.begin(serialBaud);

    // Print a start-up note to the first serial port
    Serial.print(F("\n\nNow running "));
    Serial.print(sketchName);
    Serial.print(F(" on Logger "));
    Serial.println(LoggerID);
    Serial.println();

    Serial.print(F("Using ModularSensors Library version "));
    Serial.println(MODULAR_SENSORS_VERSION);
    Serial.print(F("TinyGSM Library version "));
    Serial.println(TINYGSM_VERSION);
    Serial.println();
/** End [setup_prints] */

/** Start [setup_softserial] */
// Allow interrupts for software serial
#if defined SoftwareSerial_ExtInts_h
    enableInterrupt(softSerialRx, SoftwareSerial_ExtInts::handle_interrupt,
                    CHANGE);
#endif
#if defined NeoSWSerial_h
    enableInterrupt(neoSSerial1Rx, neoSSerial1ISR, CHANGE);
#endif
    /** End [setup_softserial] */

    /** Start [setup_serial_begins] */
    // Start the serial connection with the modem
    modemSerial.begin(modemBaud);

    // Start the stream for the modbus sensors;
    // all currently supported modbus sensors use 9600 baud
    modbusSerial.begin(9600);

#if defined MS_BUILD_SENSOR_MAXBOTIX
    // Start the SoftwareSerial stream for the sonar; it will always be at 9600
    // baud
    sonarSerial.begin(9600);
#endif
/** End [setup_serial_begins] */

// Assign pins SERCOM functionality for SAMD boards
// NOTE:  This must happen *after* the various serial.begin statements
/** Start [setup_samd_pins] */
#if defined ARDUINO_ARCH_SAMD
#ifndef ENABLE_SERIAL2
    pinPeripheral(10, PIO_SERCOM);  // Serial2 Tx/Dout = SERCOM1 Pad #2
    pinPeripheral(11, PIO_SERCOM);  // Serial2 Rx/Din = SERCOM1 Pad #0
#endif
#ifndef ENABLE_SERIAL3
    pinPeripheral(2, PIO_SERCOM);  // Serial3 Tx/Dout = SERCOM2 Pad #2
    pinPeripheral(5, PIO_SERCOM);  // Serial3 Rx/Din = SERCOM2 Pad #3
#endif
#endif
    /** End [setup_samd_pins] */

    /** Start [setup_flashing_led] */
    // 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
    greenredflash();
    /** End [setup_flashing_led] */

    /** Start [setup_logger] */
    // Set the timezones for the logger/data and the RTC
    // Logging in the given time zone
    Logger::setLoggerTimeZone(timeZone);
    // It is STRONGLY RECOMMENDED that you set the RTC to be in UTC (UTC+0)
    Logger::setRTCTimeZone(0);

    // Attach the modem and information pins to the logger
    dataLogger.attachModem(modem);
    modem.setModemLED(modemLEDPin);
    dataLogger.setLoggerPins(wakePin, sdCardSSPin, sdCardPwrPin, buttonPin,
                             greenLED);

    // Begin the logger
    dataLogger.begin();
    /** End [setup_logger] */

    /** Start [setup_sesors] */
    // Note:  Please change these battery voltages to match your battery
    // Set up the sensors, except at lowest battery level
    if (getBatteryVoltage() > 3.4) {
        Serial.println(F("Setting up sensors..."));
        varArray.setupSensors();
    }
    /** End [setup_sesors] */

#if defined MS_BUILD_MODEM_ESP8266 && F_CPU == 8000000L
    /** Start [setup_esp] */
    if (modemBaud > 57600) {
        modem.modemWake();  // NOTE:  This will also set up the modem
        modemSerial.begin(modemBaud);
        modem.gsmModem.sendAT(GF("+UART_DEF=9600,8,1,0,0"));
        modem.gsmModem.waitResponse();
        modemSerial.end();
        modemSerial.begin(9600);
    }
/** End [setup_esp] */
#endif

#if defined MS_BUILD_TEST_SKYWIRE
    /** Start [setup_skywire] */
    modem.setModemStatusLevel(LOW);  // If using CTS, LOW
    modem.setModemWakeLevel(HIGH);   // Skywire dev board inverts the signal
    modem.setModemResetLevel(HIGH);  // Skywire dev board inverts the signal
    /** End [setup_skywire] */
#endif

#if defined MS_BUILD_MODEM_SIM7080
    /** Start [setup_sim7080] */
    modem.setModemWakeLevel(HIGH);   // ModuleFun Bee inverts the signal
    modem.setModemResetLevel(HIGH);  // ModuleFun Bee inverts the signal
    Serial.println(F("Waking modem and setting Cellular Carrier Options..."));
    modem.modemWake();  // NOTE:  This will also set up the modem
    modem.gsmModem.setBaud(modemBaud);   // Make sure we're *NOT* auto-bauding!
    modem.gsmModem.setNetworkMode(38);   // set to LTE only
                                         // 2 Automatic
                                         // 13 GSM only
                                         // 38 LTE only
                                         // 51 GSM and LTE only
    modem.gsmModem.setPreferredMode(1);  // set to CAT-M
                                         // 1 CAT-M
                                         // 2 NB-IoT
                                         // 3 CAT-M and NB-IoT
    /** End [setup_sim7080] */
#endif

#if defined MS_BUILD_MODEM_XBEE_CELLULAR
    /** Start [setup_xbeec_carrier] */
    // Extra modem set-up
    Serial.println(F("Waking modem and setting Cellular Carrier Options..."));
    modem.modemWake();  // NOTE:  This will also set up the modem
    // Go back to command mode to set carrier options
    modem.gsmModem.commandMode();
    // Carrier Profile - 0 = Automatic selection
    //                 - 1 = No profile/SIM ICCID selected
    //                 - 2 = AT&T
    //                 - 3 = Verizon
    // NOTE:  To select T-Mobile, you must enter bypass mode!
    modem.gsmModem.sendAT(GF("CP"), 2);
    modem.gsmModem.waitResponse();
    // Cellular network technology - 0 = LTE-M with NB-IoT fallback
    //                             - 1 = NB-IoT with LTE-M fallback
    //                             - 2 = LTE-M only
    //                             - 3 = NB-IoT only
    // NOTE:  As of 2020 in the USA, AT&T and Verizon only use LTE-M
    // T-Mobile uses NB-IOT
    modem.gsmModem.sendAT(GF("N#"), 2);
    modem.gsmModem.waitResponse();
    // Write changes to flash and apply them
    Serial.println(F("Wait while applying changes..."));
    // Write changes to flash
    modem.gsmModem.writeChanges();
    // Reset the cellular component to ensure network settings are changed
    modem.gsmModem.sendAT(GF("!R"));
    modem.gsmModem.waitResponse(30000L);
    // Force reset of the Digi component as well
    // This effectively exits command mode
    modem.gsmModem.sendAT(GF("FR"));
    modem.gsmModem.waitResponse(5000L);
/** End [setup_xbeec_carrier] */
#endif


#if defined MS_BUILD_MODEM_XBEE_LTE_B
    /** Start [setup_r4_carrrier] */
    // Extra modem set-up
    Serial.println(F("Waking modem and setting Cellular Carrier Options..."));
    modem.modemWake();  // NOTE:  This will also set up the modem
    // Turn off the cellular radio while making network changes
    modem.gsmModem.sendAT(GF("+CFUN=0"));
    modem.gsmModem.waitResponse();
    // Mobile Network Operator Profile - 0 = SW default
    //                                 - 1 = SIM ICCID selected
    //                                 - 2: ATT
    //                                 - 6: China Telecom
    //                                 - 100: Standard Europe
    //                                 - 4: Telstra
    //                                 - 5: T-Mobile US
    //                                 - 19: Vodafone
    //                                 - 3: Verizon
    //                                 - 31: Deutsche Telekom
    modem.gsmModem.sendAT(GF("+UMNOPROF="), 2);
    modem.gsmModem.waitResponse();
    // Selected network technology - 7: LTE Cat.M1
    //                             - 8: LTE Cat.NB1
    // Fallback network technology - 7: LTE Cat.M1
    //                              - 8: LTE Cat.NB1
    // NOTE:  As of 2020 in the USA, AT&T and Verizon only use LTE-M
    // T-Mobile uses NB-IOT
    modem.gsmModem.sendAT(GF("+URAT="), 7, ',', 8);
    modem.gsmModem.waitResponse();
    // Restart the module to apply changes
    modem.gsmModem.sendAT(GF("+CFUN=1,1"));
    modem.gsmModem.waitResponse(10000L);
/** End [setup_r4_carrrier] */
#endif

    /** Start [setup_clock] */
    // Sync the clock if it isn't valid or we have battery to spare
    if (getBatteryVoltage() > 3.55 || !dataLogger.isRTCSane()) {
        // Synchronize the RTC with NIST
        // This will also set up the modem
        dataLogger.syncRTC();
    }
    /** End [setup_clock] */

    /** Start [setup_file] */
    // 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) {
        Serial.println(F("Setting up file on SD card"));
        dataLogger.turnOnSDcard(true);
        // true = wait for card to settle after power up
        dataLogger.createLogFile(true);  // true = write a new header
        dataLogger.turnOffSDcard(true);
        // true = wait for internal housekeeping after write
    }
    /** End [setup_file] */

    /** Start [setup_sleep] */
    // Call the processor sleep
    Serial.println(F("Putting processor to sleep\n"));
    dataLogger.systemSleep();
    /** End [setup_sleep] */
}


// ==========================================================================
//  Arduino Loop Function
// ==========================================================================
#ifndef MS_BUILD_TEST_COMPLEX_LOOP
// Use this short loop for simple data logging and sending
/** Start [simple_loop] */
void loop() {
    // Note:  Please change these battery voltages to match your battery
    // At very low battery, just go back to sleep
    if (getBatteryVoltage() < 3.4) {
        dataLogger.systemSleep();
    } else if (getBatteryVoltage() < 3.55) {
        // At moderate voltage, log data but don't send it over the modem
        dataLogger.logData();
    } else {
        // If the battery is good, send the data to the world
        dataLogger.logDataAndPublish();
    }
}
/** End [simple_loop] */

#else
/** Start [complex_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
    dataLogger.watchDogTimer.resetWatchDog();

    // 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 (dataLogger.checkInterval() && getBatteryVoltage() > 3.4) {
        // Flag to notify that we're in already awake and logging a point
        Logger::isLoggingNow = true;
        dataLogger.watchDogTimer.resetWatchDog();

        // Print a line to show new reading
        Serial.println(F("------------------------------------------"));
        // Turn on the LED to show we're taking a reading
        dataLogger.alertOn();
        // Power up the SD Card, but skip any waits after power up
        dataLogger.turnOnSDcard(false);
        dataLogger.watchDogTimer.resetWatchDog();

        // Turn on the modem to let it start searching for the network
        // Only turn the modem on if the battery at the last interval was high
        // enough
        // NOTE:  if the modemPowerUp function is not run before the
        // completeUpdate
        // function is run, the modem will not be powered and will not
        // return a signal strength reading.
        if (getBatteryVoltage() > 3.6) modem.modemPowerUp();

        // Start the stream for the modbus sensors, if your RS485 adapter bleeds
        // current from data pins when powered off & you stop modbus serial
        // connection with digitalWrite(5, LOW), below.
        // https://github.com/EnviroDIY/ModularSensors/issues/140#issuecomment-389380833
        altSoftSerial.begin(9600);

        // Do a complete update on the variable 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.
        varArray.completeUpdate();

        dataLogger.watchDogTimer.resetWatchDog();

        // Reset modbus serial pins to LOW, if your RS485 adapter bleeds power
        // on sleep, because Modbus Stop bit leaves these pins HIGH.
        // https://github.com/EnviroDIY/ModularSensors/issues/140#issuecomment-389380833
        digitalWrite(5, LOW);  // Reset AltSoftSerial Tx pin to LOW
        digitalWrite(6, LOW);  // Reset AltSoftSerial Rx pin to LOW

        // Create a csv data record and save it to the log file
        dataLogger.logToSD();
        dataLogger.watchDogTimer.resetWatchDog();

        // Connect to the network
        // Again, we're only doing this if the battery is doing well
        if (getBatteryVoltage() > 3.55) {
            dataLogger.watchDogTimer.resetWatchDog();
            if (modem.connectInternet()) {
                dataLogger.watchDogTimer.resetWatchDog();
                // Publish data to remotes
                Serial.println(F("Modem connected to internet."));
                dataLogger.publishDataToRemotes();

                // Sync the clock at midnight
                dataLogger.watchDogTimer.resetWatchDog();
                if (Logger::markedEpochTime != 0 &&
                    Logger::markedEpochTime % 86400 == 0) {
                    Serial.println(F("Running a daily clock sync..."));
                    dataLogger.setRTClock(modem.getNISTTime());
                    dataLogger.watchDogTimer.resetWatchDog();
                    modem.updateModemMetadata();
                    dataLogger.watchDogTimer.resetWatchDog();
                }

                // Disconnect from the network
                modem.disconnectInternet();
                dataLogger.watchDogTimer.resetWatchDog();
            }
            // Turn the modem off
            modem.modemSleepPowerDown();
            dataLogger.watchDogTimer.resetWatchDog();
        }

        // Cut power from the SD card - without additional housekeeping wait
        dataLogger.turnOffSDcard(false);
        dataLogger.watchDogTimer.resetWatchDog();
        // Turn off the LED
        dataLogger.alertOff();
        // Print a line to show reading ended
        Serial.println(F("------------------------------------------\n"));

        // Unset flag
        Logger::isLoggingNow = false;
    }

    // Check if it was instead the testing interrupt that woke us up
    if (Logger::startTesting) {
        // Start the stream for the modbus sensors, if your RS485 adapter bleeds
        // current from data pins when powered off & you stop modbus serial
        // connection with digitalWrite(5, LOW), below.
        // https://github.com/EnviroDIY/ModularSensors/issues/140#issuecomment-389380833
        altSoftSerial.begin(9600);

        dataLogger.testingMode();
    }

    // Reset modbus serial pins to LOW, if your RS485 adapter bleeds power
    // on sleep, because Modbus Stop bit leaves these pins HIGH.
    // https://github.com/EnviroDIY/ModularSensors/issues/140#issuecomment-389380833
    digitalWrite(5, LOW);  // Reset AltSoftSerial Tx pin to LOW
    digitalWrite(6, LOW);  // Reset AltSoftSerial Rx pin to LOW

    // Call the processor sleep
    dataLogger.systemSleep();
}
#endif
/** End [complex_loop] */