Details of the Firmware

Overview

This section details the Nayad firmware. The code and all the material of the project are hosted in a Gitlab repository.

The code covers the parts of:

  • Obtaining information from senors
  • Defining climate models and using this data to control the operation of water heaters and, consequently, the temperature of the mesocosms.

Data is sent to an online platform (Hacking Ecology Sensing) for real-time visualization and stored in the integrated database. At the same time, every hour this data is saved in local memory (system microSD card).

  • Step 1 Add required libraries:
#include <ArduinoJson.h>
#include <DallasTemperature.h>
#include <EEPROM.h>
#include <NAYAD_RTC.h>
#include <nayad_do.h>
#include <pgmspace.h>
#include "FS.h"
#include "SD.h"
#include "SPI.h"
#include <ThingsBoard.h>
#include <Wire.h>
#include <WiFi.h>
#include <OneWire.h>
  • Step 2 Declare variables and data needed for the firmware:

// CLOUD SETUP
#define WIFI_AP "vodafoneB9E8"
#define WIFI_PASSWORD "2dWPgwtT@$"
#define TOKEN "lYsyZF3EHK2l32gyYq7o"
char NayadServer[] = "sensing.hackingecology.com";

//WiFi CONFIG
WiFiClient wifiClient;
ThingsBoard sensing(wifiClient);
int status = WL_IDLE_STATUS;
unsigned long lastSend;

//TOUCH CONFIG
int threshold = 44; // check the threshold value using the "touchread" code (at examples)
bool touch1detected = false;

// SD CONFIG
File myFile;

// RTC CONFIG
NAYAD_RTC RTC;
DateTime timeRTC = DateTime(__DATE__, __TIME__);

// TEMPERATURE CONFIG
const int oneWireBus1 = 0;
OneWire oneWire1 (oneWireBus1);
DallasTemperature sensorsTemp (&oneWire1);
float temperature_sensor;

// DO Sensor
Nayad_DO DO = Nayad_DO(25);
float DO_percent, DO_voltage, DO_concent;

uint8_t user_bytes_received = 0;
const uint8_t bufferlen = 32;
char user_data[bufferlen];
  • Step 3 Setup - Starts and configures all necessary information for Nayad to work:
void setup() {
  Serial.begin(115200);
  InitWiFi();
  lastSend = 0;
  sensorsTemp.begin();

  //SD card setup
  touchAttachInterrupt(T0, gotTouch1, threshold);

  if (!SD.begin()) {
    Serial.println("Card Mount Failed");
    return;
  }
  else {
    Serial.println("Card is Ready");
    return;
  }

// DO Setup
  Serial.println(F("Use command \"CAL\" to calibrate the circuit to 100% saturation in air\n\"CAL,CLEAR\" clears the calibration"));
  if (DO.begin()) {
    Serial.println("Loaded EEPROM");
  }

//RTC Setup
  DateTime compiled = DateTime(__DATE__, __TIME__);

  Serial.println("Checking I2C device...");
  if (RTC.searchDevice())
  {
    Serial.println("configuring RTC I2C");
    RTC.configure();

    if (!RTC.IsDateTimeValid())
    {
      if (RTC.LastError() != 0)
      {
        Serial.print("RTC communications error = ");
        Serial.println(RTC.LastError());
      }
      else
      {
        Serial.println("RTC lost confidence in the DateTime!");
        RTC.SetDateTime(compiled);
      }
    }
    else
    {
      Serial.printf("Found an RTC with valid time\n");
    }

    timeRTC = RTC.now();
    uint32_t nowTS = timeRTC.getTimeStamp();
    uint32_t compiledTS = compiled.getTimeStamp();
    if (nowTS < compiledTS)
    {
      Serial.printf("RTC is older than compile time!  (Updating DateTime)\n");
      RTC.SetDateTime(compiled);
    }
    else if (nowTS > compiledTS)
    {
      Serial.printf("RTC is newer than compile time. (this is expected)\n");
    }
    else if (nowTS == compiledTS)
    {
      Serial.printf("RTC is the same as compile time! (not expected but all is fine)\n");
    }

    if (!timeRTC.checkWeek())
    {
      Serial.printf("Update WEEK\n");
      RTC.setWeekDays(dow(timeRTC.year(), timeRTC.month(), timeRTC.day()));
    }
  }
  else
  {
    Serial.printf("device not found\n");
    while (1);
  }
}
  • Step 4 Configures all the functions and operations we want to work in loop:
void loop() {

  //Loop for WIFI
  if ( !sensing.connected() ) {
    reconnect();
  }

  sensing.loop();

// Loop calibration
  if (Serial.available() > 0) {
    user_bytes_received = Serial.readBytesUntil(13, user_data, sizeof(user_data));
  }
  if (user_bytes_received) {
    parse_cmd(user_data);
    user_bytes_received = 0;
    memset(user_data, 0, sizeof(user_data));
  }
  
// Loop sensor data
    SensorsDataDO();
}

2. Work/Complementary Functions

  • Step 5 Function with settings to get data from DO sensor:
void SensorsDataDO() {
  sensorsTemp.requestTemperatures();
  float temperature_sensor = sensorsTemp.getTempCByIndex(0);

  {
    static unsigned long timepoint = millis();
    if (millis() - timepoint > 1000U) //time interval: 1 second
    {
      DO_percent = DO.read_do_percentage();
      DO_voltage = DO.read_voltage();
      DO_concent = DO.read_do_concentration(DO_voltage, temperature_sensor);

      Serial.print("temperature: ");
      Serial.print(temperature_sensor, 2);
      Serial.println(" ^C");
      Serial.print("Dissolved Oxygen: ");
      Serial.print(DO_percent);
      Serial.print("%, ");
      Serial.print(DO_voltage);
      Serial.print(" mV, ");
      Serial.print(DO_concent);
      Serial.println(" mg/L");

// Loop for RTC
      timeRTC = RTC.now();
    
      if (timeRTC.IsValid())
      {
        Serial.printf("str Data: %s\n", timeRTC.getStrDate().c_str());
        Serial.printf("str Hora: %s\n", timeRTC.getStrTime().c_str());
        Serial.printf("TS: %u\n", timeRTC.getTimeStamp());
        delay(random(1000, 5000));
          }
      else
      {
        Serial.printf("Invalid DateTime\n");
      }
    }
    sensing.sendTelemetryFloat("DOperc", DO_percent);
    sensing.sendTelemetryFloat("DOv", DO_voltage); 
    sensing.sendTelemetryFloat("DOconc", DO_concent);   
    sensing.sendTelemetryFloat("temperature", temperature_sensor);
    delay(1000);

// Setup touch and SD card Storage
 
    if (touch1detected) {
    touch1detected = false;
    Serial.println("Touch 1 detected");

      if (SD.exists("/data.csv")) {
        Serial.println("Appending line...");
        File file = SD.open("/data.csv", FILE_APPEND);
        if (!file) {
          Serial.println("Failed to open file for appending");
          return;
        }
        if (file) {
          file.print(timeRTC.getStrDate().c_str());
          file.print(" ");
          file.print(timeRTC.getStrTime().c_str());
          file.print(",");
          file.print(timeRTC.getTimeStamp());
          file.print(",");    
          file.print(temperature_sensor);
          file.print(",");
          file.println(DO_concent);
          delay(1000);          
          Serial.println("Message appended");
        } else {
          Serial.println("Append failed");
        }
        file.close();
      }
      else if (!SD.exists("/data.csv")) {
        File file = SD.open("/data.csv", FILE_WRITE);
        if (!file) {
          Serial.println("Failed to open file for writing");
          return;
        }
        if (file) {
          file.println("Time, UNIX, Temperature (ÂșC), DO (mg/L)");
          file.print(timeRTC.getStrDate().c_str());
          file.print(" ");
          file.print(timeRTC.getStrTime().c_str());
          file.print(",");
          file.print(timeRTC.getTimeStamp());
          file.print(",");    
          file.print(temperature_sensor);
          file.print(",");
          file.println(DO_concent);
          Serial.println("File written");
        } else {
          Serial.println("Write failed");
        }
        file.close();
      }
    }
  }
}
  • Step 6 Function with command to obtain data from the sensors, print the output via USB serial and send the data to the online platform:
// DO calibration
void parse_cmd(char* string) {
  strupr(string);
  String cmd = String(string);
  if (cmd.startsWith("CAL")) {
    int index = cmd.indexOf(',');
    if (index != -1) {
      String param = cmd.substring(index + 1, cmd.length());
      if (param.equals("CAL CLEAR")) {
        DO.cal_clear();
        Serial.println("CALIBRATION CLEARED");
      }
    }
    else {
      DO.cal();
      Serial.println("DO PROBE CALIBRATED");
    }
  }
}
  • Step 7 Configure data storage using push bottom

void gotTouch1() {
  touch1detected = true;
}

//SD card
void writeFile(fs::FS &fs, const char * path, const char * message) {
  Serial.printf("Writing file: %s\n", path);

  File file = fs.open(path, FILE_WRITE);
  if (!file) {
    Serial.println("Failed to open file for writing");
    return;
  }
  if (file.print(message)) {
    Serial.println("File written");
  } else {
    Serial.println("Write failed");
  }
  file.close();
}

void appendFile(fs::FS &fs, const char * path, const char * message) {
  Serial.printf("Appending to file: %s\n", path);

  File file = fs.open(path, FILE_APPEND);
  if (!file) {
    Serial.println("Failed to open file for appending");
    return;
  }
  if (file.print(message)) {
    Serial.println("Message appended");
  } else {
    Serial.println("Append failed");
  }
  file.close();
}
  • Step 8 Function with network data for connection:
void InitWiFi() {
  Serial.println("Connecting to AP ...");
  // attempt to connect to WiFi network

  WiFi.begin(WIFI_AP, WIFI_PASSWORD);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("Connected to AP");
}
  • Step 9 Function that allows you to re-establish the connection if the system is offline:

void reconnect() {  
  status = WiFi.status();
  if ( status != WL_CONNECTED) {
    WiFi.begin(WIFI_AP, WIFI_PASSWORD);
//     while (WiFi.status() != WL_CONNECTED) {
//      delay(8000);
  //    Serial.print(".");
    //}
      Serial.println("Connected to AP");
    }
    Serial.print("Connecting to Sensing Platform ...");
    if ( tb.connect(NayadServer, TOKEN) ) {
      Serial.println( "[DONE]" );
    } else {
      Serial.print( "[FAILED]" );
      Serial.println( " : retrying in 5 seconds]" );
      // Wait 5 seconds before retrying
      delay( 5000 );
    }
}