Should a newbie dare try OTA with download to modem

Hi

I have been reading this thread MKR GSM 1400 - WiFi101 OTA library - #30 by xchrisd with great interest to find out if I dare implement OTA on my board or not. I am not sure if the download to the modem filesystem was the right choice for newbies like me or not. I do not understand most of if, so my conclusion would be a big no no. But I do not understand most of the code at GSM MKR library and I managed to use it anyway to program my irrigation monitor with SMS reports.

I really have no clue on how to proceed to create a downloadable compiled sketch. Again a huge no no. But I guess it is a procedure that can be followed to the letter every single time, and I can do that.

I know Arduino Cloud includes OTA when a certain level of paid subscription is achieved. But I do not want to use it. I set up my Raspberry Pi with NodeRed so my "thing" reported to my cloud as often as I wanted and to keep my data for years to come.

Not that I really really need it. I need more likely a change of operating parameters and some remote storage of parameters to acquire at each boot. But since the "thing" is kind of a few hundred kilometres away from home, it would be nice to deploy the new functionality without paying a visit from time to time. Lately there have been some hardware improvements that required a visit anyway, but I hope that won't be the case for long.

So I am looking for a piece of advice. Would you think the OTA in the current development stage of the libraries is for newbies?

Thanks

Hi

I wasted a lot of time on the topic with OTA on MKR1400. After followed a lot of wrong path, the answer is simple:
Don't use watchdogs like WDTZero together with OTA functionality. The reason is that the modem sends a stream of bytes after requesting the download and the MKR CPU stops to listen while do the watchdog stuff and misses some bytes of the download.
Since I don't use watchdogs every approach works perfect, I assume JAnndrassy's library is the leader, I recommend to use this. If you have a large sketch you might use a SD card and his example. I use my own simple download routine with SDU.h, I can post if wanted

Conclusion: Yes, no problem for beginners, don't combine with watchdogs and start trying with the wonderful lib:

1 Like

Thank you very much for the quick response and the link. Any help would be great and appreciated of course.

That tip in wdtzero is invaluable. I actually do use it in software mode with a two minutes timeout. Did you experience the issues while in software mode, hardware mode or regardless?

I had a couple reboots throughout the year related to call to functions in the mkr gsm library. But I thought it was just chance since the AT commands sometimes take a while due to dependency on the gsm network behind.

I found convenient your approach of a simple download to the modem filesystem by means of its own http client, then flash from there "locally". Not sure that library does that, but I will look into it so I can understand the alternatives and make a decision.

I already found how to export the binary after compilation, although there are more files than I expected there would be.

Is this big? I do not know yet which file is the one to be uploaded over the air.

Meland:arduino.samd.mkrgsm1400 me$ ls -lahU
total 5920
drwxr-xr-x  8 me  staff   256B Jul 26 16:15 .
drwxr-xr-x  4 me  staff   128B Jul 26 16:15 ..
-rwxr-xr-x  1 me  staff    69K Jul 26 16:15 ControlRiego_0_2_0.ino.bin
-rwxr-xr-x  1 me  staff   1.2M Jul 26 16:15 ControlRiego_0_2_0.ino.elf
-rw-r--r--  1 me  staff   193K Jul 26 16:15 ControlRiego_0_2_0.ino.hex
-rw-r--r--  1 me  staff   1.1M Jul 26 16:15 ControlRiego_0_2_0.ino.map
-rw-r--r--  1 me  staff    77K Jul 26 16:15 ControlRiego_0_2_0.ino.with_bootloader.bin
-rw-r--r--  1 me  staff   210K Jul 26 16:15 ControlRiego_0_2_0.ino.with_bootloader.hex

Thank you.

Both hard and software with the watchdog. It's a internal SAMD21 function out of control by the independent modem. The main insight is that the mkr and the modem are independent computers communication just through the silly AT console.
As said, with a watchdog enabled, always some bytes were lost in my case.
That's why I made the approach with download the update.bin to the internal memory of the modem and then copy blockwise to the SD card and do a SDU reboot.
It's very slow (around 4 Minutes for 200 kB) , complicated and specific for MKR1400. That's why I changed my approach (Faster, same code for MKR1400,MKR1500,MKRWifi)
Size: JAndrassy lib works for small sketches without SD. For large sketches the MKR memory is too small.

Your ls screenshot: I don't understand what you mean, I just use export compiled binary from the Arduino IDE, so what i do now is:

  1. I compile the sketch to a local file, rename it UPDATE.bin, put it on the server.
  2. A MQTT command calls the OTA update function which downloads the file to the SD card.
  3. Check the file site and integrity and reboot with the SDU.h loaded. (The SDU.h has to be already in the "old" sketch.)

Here is my snippet, I use the HttpClient library. SSL is not working IMHO also not neccesary for this.


*/
// OTA Update
void handleSketchDownload() {

  const char *SERVER = "example.com";  // must be string for HttpClient
  const unsigned short SERVER_PORT = 80;
  const char *PATH = "/update-wifi.bin";
  const unsigned long CHECK_INTERVAL = 5000;

  static unsigned long previousMillis;

  unsigned long currentMillis = millis();

  if (currentMillis - previousMillis < CHECK_INTERVAL)
    return;
  previousMillis = currentMillis;

  WiFiClient transport;
  HttpClient http_client_update(transport, SERVER, SERVER_PORT);

  char buff[32];
  snprintf(buff, sizeof(buff), PATH, VERSION + 1);

  Serial.print("Check for update file ");
  Serial.println(buff);

  http_client_update.get(buff);

  int statusCode = http_client_update.responseStatusCode();
  Serial.print("HTTP status code: ");
  Serial.println(statusCode);
  if (statusCode != 200) {
    http_client_update.stop();
    return;
  }

  long length = http_client_update.contentLength();
  if (length == HttpClient::kNoContentLengthHeader) {
    http_client_update.stop();
   
    Serial.print("DOWNLOAD FAIL!");
    delay(1000);
    return;
  }
  Serial.print("Server returned update file of size ");
  Serial.print(length);
  Serial.println(" bytes");
 
  if (SD.exists(BIN_FILENAME)) {
    Serial.println("update file exists.");
    SD.remove(BIN_FILENAME);
    delay (1000);
  }
  else {
    Serial.println("update file doesn't exist.");
  }


  file = SD.open(BIN_FILENAME, FILE_WRITE);
  if (!file) {
    http_client_update.stop();
    tft.drawRect(0, 120, 320, 35, ST77XX_WHITE);
    tft.fillRect(1, 121, 318, 33, ST77XX_BLUE);
    tft.setFont(&FreeSans12pt7b);
    tft.setTextColor(ST77XX_WHITE);
    tft.setCursor(10, 145);
    tft.print("SD CARD ERROR");
    delay(500);
    Serial.println("Could not create bin file. Can't continue with update.");
    return;
  }
  byte b;
  while (length > 0) {
    if (!http_client_update.readBytes(&b, 1))  // reading a byte with timeout
      break;
    file.write(b);
    length--;
  }
  file.close();
  http_client_update.stop();
  if (length > 0) {
    SD.remove(BIN_FILENAME);
   Serial.print("DOWNLOAD FAIL!");
    delay(500);
    Serial.print("Timeout downloading update file at ");
    Serial.print(length);
    Serial.println(" bytes. Can't continue with update.");
    return;
  }

  Serial.println("Update file saved. Reset.");
  Serial.flush();

Serial.print("UPDATED! RESTART NOW!");
  delay(500);

  NVIC_SystemReset();
}

Should I disable entirely the WDTZero library or disabling the watchdog while moving the files is enough? I sometimes set the WDT_OFF command to my WDZero, but perhaps that is also insufficient.

I posted the file listing of my folder after exporting the compilation. I can only see one bin just 70KB in size, but it felt too small compared to the rest of the files so I do not know which is the one I am meant to upload. Perhaps my MacOS is showing uncompressed all the internal components of that bin you process? This is the full tree for my sketch. All created by the IDE software.

Meland:ControlRiego_0_2_0 me$ ls -laRh
total 120
drwxr-xr-x  6 me  staff   192B Jul 26 16:16 .
drwxr-xr-x@ 8 me  staff   256B Jul 26 11:25 ..
-rw-r--r--@ 1 me  staff   6.0K Jul 26 16:16 .DS_Store
-rw-r--r--  1 me  staff    45K Jul 26 11:25 ControlRiego_0_2_0.ino
-rw-r--r--  1 me  staff   1.0K Jul 26 11:25 arduino_secrets.h
drwxr-xr-x  4 me  staff   128B Jul 26 16:16 build

./build:
total 16
drwxr-xr-x  4 me  staff   128B Jul 26 16:16 .
drwxr-xr-x  6 me  staff   192B Jul 26 16:16 ..
-rw-r--r--@ 1 me  staff   6.0K Jul 27 11:29 .DS_Store
drwxr-xr-x  8 me  staff   256B Jul 26 16:15 arduino.samd.mkrgsm1400

./build/arduino.samd.mkrgsm1400:
total 5920
drwxr-xr-x  8 me  staff   256B Jul 26 16:15 .
drwxr-xr-x  4 me  staff   128B Jul 26 16:16 ..
-rwxr-xr-x  1 me  staff    69K Jul 26 16:15 ControlRiego_0_2_0.ino.bin
-rwxr-xr-x  1 me  staff   1.2M Jul 26 16:15 ControlRiego_0_2_0.ino.elf
-rw-r--r--  1 me  staff   193K Jul 26 16:15 ControlRiego_0_2_0.ino.hex
-rw-r--r--  1 me  staff   1.1M Jul 26 16:15 ControlRiego_0_2_0.ino.map
-rw-r--r--  1 me  staff    77K Jul 26 16:15 ControlRiego_0_2_0.ino.with_bootloader.bin
-rw-r--r--  1 me  staff   210K Jul 26 16:15 ControlRiego_0_2_0.ino.with_bootloader.hex

For the SD based process you need an SD card reader attached to the MKR GSM, right? My apologies if I am saying something stupid, but I feel I am missing something obvious here. I mean, if you can download to the modem FS, then load into RAM, do the size check and integrity check, then reboot into it, wouldn't it be safer to do and requires solely the standard board? Perhaps the external SD card has more read-write cycles available than the internal FS of the modem? Perhaps overusing the internal FS would in the end break the internal modem SO and render the MKR GSM board useless after that? I understand an specific method is not preferable, but there are peculiarities across the MKR family that required specifics for each of them anyway, right?

I am guessing NB1500 does not have a filesystem thus enabling this so specific method is no use for the new generation. Is it? Otherwise, why not take advantage of the filesystem and the OS inside the modem chip? Honest question. I am sure there are more consequences I cannot foresee from my short experience and lacking knowledge.

I am still reading through the library, so please, be patient with my comments and questions. Although I reached far enough to read that for dormant remote devices it is recommended the use of SDU.h as you suggested.

My current sketch relies on SMS messages coming and going, instead of MQTT. It had MQTT implemented on a previous version (I have been tinkering with this for a year and a half already). I am not going to need more than one consumer for the information, so really no benefit to MQTT over SMS. And MQTT required a data connection consuming more power from the battery, DDNS to be kept alive so my server at home is reachable, be careful to resolve the DDNS entry each time it required connection, the trouble of adapting to a forced IP renewal from the ISP, .... So I just installed a GSM dongle (with another SIM from an spare MKR GSM board on the selves) into my Raspberry with SMStools3 and nodered took care of the rest.

For the OTA I was planning on sending an SMS with the OTA command and current valid URL with an expiration timestamp and the MD5 hash, requiring the MKR to refresh the data if download fails or timestamp expires. Then use the modem http download, run the hash at the modem too, then copy to wherever it must be copied into and boot into it.

Still reading. So, please, let me go through it all before. I am sure I am lacking information that is already at the link you provided.

For your reference, this is the sketch in the latest version. Perhaps it could help someone else reading and it not too poorly written. My coding skills have been diminishing over the years, not improving. :smiley:

#include "arduino_secrets.h"
#include <MKRGSM.h>
#include <WDTZero.h>
#include <ArduinoLowPower.h>
#include <Arduino_PMIC.h>
#include <Modem.h>
#include <RTCZero.h>
#include <QuickMedianLib.h>

/****************************************************************************/
/*                         DEFINITIONS SECTION                              */
/****************************************************************************/

/////////
// LOG //
/////////

/* Log levels */
constexpr int LOG_LEVEL_SILENT = 0;
constexpr int LOG_LEVEL_ERROR = 1;
constexpr int LOG_LEVEL_WARN = 2;
constexpr int LOG_LEVEL_INFO = 3;
constexpr int LOG_LEVEL_TRACE = 4;

/* Log variable */
int LOG_LEVEL = LOG_LEVEL_TRACE;

#define CONSOLE_LOGGING  // Enables log messages to Serial console

#ifdef CONSOLE_LOGGING
#define consoleLogging(nivel, mensaje) \
  if (nivel <= LOG_LEVEL) { \
    Serial.print("LOGLVL "); \
    Serial.print(nivel); \
    Serial.print(": "); \
    Serial.println(mensaje); \
  }
#else
#define consoleLogging(nivel, mensaje)
#endif

//////////////////////
// Power management //
//////////////////////
// The IRQ is not called when in deep sleep mode, so this code is useless
volatile unsigned long timeLastBatteryInterrupt = 0;
volatile bool batteryInterruptFired = false;
bool batteryPowered = false;

////////////////////
// Self awareness //
////////////////////
#define watchdogBarkPeriod WDT_SOFTCYCLE2M
constexpr int sketchUpdateWindow = 30000;
constexpr int peripheralStabilizationTime = 10000;

////////////////
// Duty cycle //
////////////////
constexpr int HALF_SECOND = 500;
constexpr int ONE_SECOND = 1000;
constexpr int TWO_SECONDS = 2000;
constexpr int THREE_SECONDS = 3000;
constexpr int FOUR_SECONDS = 4000;
constexpr int FIVE_SECONDS = 5000;
constexpr int TEN_SECONDS = 5000;
constexpr int ONE_MINUTE = 60000;
constexpr int TWO_MINUTES = 120000;
constexpr int FIVE_MINUTES = 300000;
constexpr int TEN_MINUTES = 600000;
constexpr int HALF_HOUR = 1800000;
constexpr int ONE_HOUR = 3600000;
constexpr int onBatteryLoopPeriod = ONE_HOUR;
constexpr int onMainPowerloopPeriod = TEN_MINUTES;
int loopPeriod = onMainPowerloopPeriod;

//////////////////////
// GSM network data //
//////////////////////
char SIM_PIN[5];
const char* controlPhoneNumber[] = SECRET_CONTROL_PHONE_NUMBER;
const char* reportPhoneNumber[] = SECRET_REPORT_PHONE_NUMBER;
constexpr int GSM_CONNECT_WAITING_TIME = 10000;
constexpr int GSM_MAX_CONNECT_ATTEMPTS = 9;
constexpr int SMS_MAXIMUM_LENGTH = 140;

/////////////////////////////////////////////////////////////////
// Sensor attributes                                           //
/////////////////////////////////////////////////////////////////
int sensorSamplingPeriod = FIVE_SECONDS;
int sensorSamplesPerSampling = 5;

// Rain only shallow moisture sensor pin and attributes
#define SENSOR_ON_A1
// Rain only deep moisture sensor pin and attributes
#define SENSOR_ON_A2
// Rain and irrigation deep moisture sensor pin and attributes
#define SENSOR_ON_A3
// Water source pressure sensor pin and attributes
#define SENSOR_ON_A4
// Temperature sensor pin and attributes
#undef SENSOR_ON_A5

// Atributes for all sensors if defined the SENSOR on each PIN

// It meassures higher the drier, lower the wetter
// 100% dry = 0% wet = 575, 0% dry = 100% wet = 308 when powered at 5V
#ifdef SENSOR_ON_A1
#define SENSOR_A1 "M0"  //"shallow reference moisture"
constexpr int SENSOR_A1_PIN = A1;
constexpr int SENSOR_A1_MIN_VALUE = 0;
constexpr int SENSOR_A1_MAX_VALUE = 100;
constexpr int SENSOR_A1_MIN_READING = 308;
constexpr int SENSOR_A1_MAX_READING = 575;
#endif

// It meassures higher the drier, lower the wetter
// 100% dry = 0% wet = 575, 0% dry = 100% wet = 308 when powered at 5V
#ifdef SENSOR_ON_A2
#define SENSOR_A2 "M1"  //"deep reference moisture"
constexpr int SENSOR_A2_PIN = A2;
constexpr int SENSOR_A2_MIN_VALUE = 0;
constexpr int SENSOR_A2_MAX_VALUE = 100;
constexpr int SENSOR_A2_MIN_READING = 308;
constexpr int SENSOR_A2_MAX_READING = 575;
#endif

// It meassures higher the drier, lower the wetter
// 100% dry = 0% wet = 575, 0% dry = 100% wet = 308 when powered at 5V
#ifdef SENSOR_ON_A3
#define SENSOR_A3 "M2"  //"deep undertree moisture"
constexpr int SENSOR_A3_PIN = A3;
constexpr int SENSOR_A3_MIN_VALUE = 0;
constexpr int SENSOR_A3_MAX_VALUE = 100;
constexpr int SENSOR_A3_MIN_READING = 308;
constexpr int SENSOR_A3_MAX_READING = 575;
#endif

// Water source pressure sensor pin and attributes
// Reading 98 = 0 mBar, reading 900 = 5000mBar
// Expected error <100mBar => only dBar make sense, not mBar
#ifdef SENSOR_ON_A4
#define SENSOR_A4 "P0"  //"water source pressure"
constexpr int SENSOR_A4_PIN = A4;
constexpr int SENSOR_A4_MIN_VALUE = 0;
constexpr int SENSOR_A4_MAX_VALUE = 5000;
constexpr int SENSOR_A4_MIN_READING = 98;
constexpr int SENSOR_A4_MAX_READING = 900;
#endif

// PENDING CALIBRATION
#ifdef SENSOR_ON_A5
#define SENSOR_A5 "T0"  //"temperature"
constexpr int SENSOR_A5_PIN = A5;
constexpr int SENSOR_A5_MIN_VALUE = 0;
constexpr int SENSOR_A5_MAX_VALUE = 5000;
constexpr int SENSOR_A5_MIN_READING = -15;
constexpr int SENSOR_A5_MAX_READING = 65;
#endif

////////////////////
// Reporting data //
////////////////////
#define REPORT_HEADER "TS,M0,M1,M2,P0,T0\n"
#define MAX_REPORT_SAMPLES 4

/****************************************************************************/
/*                          INSTANCES SECTION                               */
/****************************************************************************/

////////////////////
// Self awareness //
////////////////////
WDTZero myWatchDog;

///////////////////////////////////
// Instances for Real Time Clock //
///////////////////////////////////
RTCZero rtc;

///////////////////////////////////
// Instances for GSM/GPRS module //
///////////////////////////////////
GSMPIN myPINmanager;
GSM myGsmAccess;
GSM_SMS mySms;
GSMModem myGSMModem;

///////////////////////////////////
// Instances for reporting       //
///////////////////////////////////
String manySamplesInCsvString = REPORT_HEADER;
int storedSamplesInCsvString = 0;

/****************************************************************************/
/*                             FUNCTION DECLARATIONS                        */
/****************************************************************************/

void initializeSerial();
void initializeWatchdog();
void initializePMIC();
void initializeGSM();
void synchronizeTime();
void initializeSensors();
void initializeReporting();
void myWatchDogShutdown();
void batteryInterrupt();
void restartBoard();
String doATcommandModemGSM(const char* s, unsigned long timeout);
void hardwarepPowerOffModemGSM();
void sendStringSMStoControl(String message, int retries, int period);
void sendStringSMStoReport(String message, int retries, int period);
int ledBlink(int durationMS, int periodMS);
int ledBlinkIdleCPU(int durationMS, int periodMS);

/****************************************************************************/
/*                        SETUP & LOOP SECTION                              */
/****************************************************************************/

void setup() {

  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, HIGH);  // turn the LED on (HIGH is the voltage level)

  initializeSerial();

  consoleLogging(LOG_LEVEL_INFO, "SETUP: Starting update window.");
  ledBlink(sketchUpdateWindow, TWO_SECONDS);
  consoleLogging(LOG_LEVEL_INFO, "SETUP: Finished update window.");

  initializeWatchdog();
  initializePMIC();
  initializeGSM();
  synchronizeTime();
  initializeSensors();
  initializeReporting();

  sendStringSMStoControl("Board finished setup after a reboot or cold restart.", 2, FIVE_SECONDS);

  // We know GSM works, so GSM module is throttled down to low power mode while not required
  myGsmAccess.lowPowerMode();
  digitalWrite(LED_BUILTIN, LOW);  // turn the LED off (LOW is the ground voltage)

  consoleLogging(LOG_LEVEL_INFO, "SETUP: Done.");
}

void loop() {

  consoleLogging(LOG_LEVEL_INFO, "LOOP: Starting a loop.");
  digitalWrite(LED_BUILTIN, HIGH);

  //////////////////////////////////////////
  // Control the duty cycle of activities //
  //////////////////////////////////////////

  consoleLogging(LOG_LEVEL_INFO, "LOOP: Going to sleep for " + String(loopPeriod) + " ms.");
  // Disable watchdog timer
  myWatchDog.setup(WDT_OFF);
  digitalWrite(LED_BUILTIN, LOW);  // turn the LED off (LOW is the ground level)
  // Put the board into sleep mode so IRQ can work but ADC and all peripherals are powered off
  LowPower.sleep(loopPeriod);
  digitalWrite(LED_BUILTIN, HIGH);  // turn the LED on (HIGH is the voltage level)
  // Enable watchdog timer before running the loop
  myWatchDog.setup(watchdogBarkPeriod);
  consoleLogging(LOG_LEVEL_TRACE, "LOOP: Waking from sleep.");

  consoleLogging(LOG_LEVEL_INFO, "LOOP: Starting peripherals stabilization after being powered off because of system sleep.");
  //Blink the led while we wait for peripherals
  ledBlinkIdleCPU(peripheralStabilizationTime, HALF_SECOND);
  consoleLogging(LOG_LEVEL_INFO, "LOOP: Finished peripherals stabilization.");

  //////////////////////////////////////
  // Power Management Integrated Chip //
  //////////////////////////////////////

  myWatchDog.clear();

  ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  // Power Management both for battery control Integrated Chip and power saving modes of board, GSM chip and may be ADC //
  ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  /*
      To-Do:
      - Poweroff the GSM modem for as long as it makes sense and power up to send many stored SMS
        · modem is powered off with the command AT+CPWROFF from SARA-201 docs, but GSM class does have a shutdown so...
        · maight be better to just keep using the lowpowermode since idle mode did not deplete the battery too fast
  */
  consoleLogging(LOG_LEVEL_TRACE, "PMIC: Backup Battery Voltage is " + String(analogRead(ADC_BATTERY) * (4208 / 1023)) + " mV.");
  // System Status Register
  consoleLogging(LOG_LEVEL_TRACE, "PMIC: USBmode (UNKNOWN_MODE 0x00, USB_HOST_MODE 0x40, ADAPTER_PORT_MODE 0x80, BOOST_MODE 0xC0) --> 0x" + String(PMIC.USBmode(), 16));
  consoleLogging(LOG_LEVEL_TRACE, "PMIC: chargeStatus (NOT_CHARGING 0x00, PRE_CHARGING 0x10, FAST_CHARGING 0x20, CHARGE_TERMINATION_DONE 0x30) --> 0x" + String(PMIC.chargeStatus(), 16));
  consoleLogging(LOG_LEVEL_TRACE, "PMIC: isBattConnected --> " + String(PMIC.isBattConnected()));
  consoleLogging(LOG_LEVEL_TRACE, "PMIC: isPowerGood --> " + String(PMIC.isPowerGood()));
  consoleLogging(LOG_LEVEL_TRACE, "PMIC: isHot --> " + String(PMIC.isHot()));
  consoleLogging(LOG_LEVEL_TRACE, "PMIC: canRunOnBattery --> " + String(PMIC.canRunOnBattery()));
  // Fault Register
  consoleLogging(LOG_LEVEL_TRACE, "PMIC: isWatchdogExpired --> " + String(PMIC.isWatchdogExpired()));
  consoleLogging(LOG_LEVEL_TRACE, "PMIC: getChargeFault (NO_CHARGE_FAULT 0x00, INPUT_OVER_VOLTAGE 0x10, THERMAL_SHUTDOWN 0x20, CHARGE_SAFETY_TIME_EXPIRED 0x30) --> 0x" + String(PMIC.getChargeFault(), 16));
  consoleLogging(LOG_LEVEL_TRACE, "PMIC: isBatteryInOverVoltage --> " + String(PMIC.isBatteryInOverVoltage()));
  consoleLogging(LOG_LEVEL_TRACE, "PMIC: hasBatteryTemperatureFault (NO_TEMPERATURE_FAULT 0x00, LOWER_THRESHOLD_TEMPERATURE_FAULT 0x05, HIGHER_THRESHOLD_TEMPERATURE_FAULT 0x06) --> 0x" + String(PMIC.hasBatteryTemperatureFault(), 16));

  /*****************************************************************************************************
  PENDING: Figure out if using the IRQ means the loop resumes right after the IRQ call,
           when fired while in idle, sleep or deepsleep
           or if the system goes back to idle, sleep or deepsleep
           --> If it continues inmediately with the loop, it would mess up the schedule of reports
  */
  consoleLogging(LOG_LEVEL_INFO, "PMIC: Battery IRQ flag is " + String(batteryInterruptFired));
  if (batteryInterruptFired) {
    batteryInterruptFired = false;  // Reset the flag
    consoleLogging(LOG_LEVEL_INFO, "PMIC: Battery IRQ flag changed to " + String(batteryInterruptFired));
  }

  bool mainSourcePowered = PMIC.isPowerGood();
  consoleLogging(LOG_LEVEL_INFO, batteryPowered ? "PMIC: Battery powered" : "Main source powered.");
  consoleLogging(LOG_LEVEL_TRACE, "PMIC: batteryPowered = " + String(batteryPowered));
  consoleLogging(LOG_LEVEL_TRACE, "PMIC: mainSourcePowered = " + String(mainSourcePowered));
  // Check if the board is running on battery but it was not the last loop
  if (!batteryPowered && !mainSourcePowered) {
    consoleLogging(LOG_LEVEL_ERROR, "PMIC: Lost main power. Running on battery! NO REPORTS WILL BE SENT.");
    batteryPowered = true;
    myGsmAccess.noLowPowerMode();
    ledBlink(TWO_SECONDS, HALF_SECOND);  // Wait for the modem to fully come back from lowpowermode
    sendStringSMStoControl("Lost main power. Running on battery! NO REPORTS WILL BE SENT.", 10, FIVE_SECONDS);
    myGsmAccess.secureShutdown();
    loopPeriod = onBatteryLoopPeriod;
  }

  // Check if the board was running on battery the last loop but not anymore
  if (batteryPowered && mainSourcePowered) {
    consoleLogging(LOG_LEVEL_WARN, "PMIC: Main power source restored. REPORTING WILL BE RESUMED.");
    batteryPowered = false;
    initializeGSM();
    synchronizeTime();
    sendStringSMStoControl("Main power source restored. REPORTING WILL BE RESUMED.", 2, FIVE_SECONDS);
    myGsmAccess.lowPowerMode();
    loopPeriod = onMainPowerloopPeriod;
  }

  /************************************************************************************************************
  When running on battery the reference voltage for sensors at the "connector carrier" board grove connectors
  drops below +5v rendering sensor scaling voltage vs reading useless. We cannot report these wrong data.
  The reference level drops because the reference is the +5V PIN at the connector carrier power buck. Since 
  the main power source is not feeding the "connector carrier" power buck though the VIN PIN, the output pins
  change voltage.
  PENDING:
  - We should probably power +5V PIN down to shutdown sensors and prevent damage to those that do not support it.
  - If we can use IRQ now in sleep mode, we could sleep forever when batterypowered because all we do is check if
    the main power source is recovered
  */
  if (batteryPowered) {
    consoleLogging(LOG_LEVEL_TRACE, "PMIC: Backup Battery Voltage is " + String(analogRead(ADC_BATTERY) * (4208 / 1023)) + " mV.");
    consoleLogging(LOG_LEVEL_ERROR, "PMIC: Running on battery. Ending this loop now.");
    digitalWrite(LED_BUILTIN, LOW);  // turn the LED off (LOW is the ground level)
    return;
  }

  myWatchDog.clear();

  ///////////////////////////////////////////
  // Checking status before doing anything //
  ///////////////////////////////////////////

  // Check if GSM is ready
  consoleLogging(LOG_LEVEL_TRACE, "GSM: We do not check GSM status at the loop anymore because we power it on only to report.");

  myWatchDog.clear();

  ///////////////////////////////////////////////
  // Finally, let's do what we are meant to do //
  ///////////////////////////////////////////////

  // Variables to store samples
#ifdef SENSOR_ON_A1
  int sensorA1Readings[sensorSamplesPerSampling];
#endif
#ifdef SENSOR_ON_A2
  int sensorA2Readings[sensorSamplesPerSampling];
#endif
#ifdef SENSOR_ON_A3
  int sensorA3Readings[sensorSamplesPerSampling];
#endif
#ifdef SENSOR_ON_A4
  int sensorA4Readings[sensorSamplesPerSampling];
#endif
#ifdef SENSOR_ON_A5
  int sensorA5Readings[sensorSamplesPerSampling];
#endif

  consoleLogging(LOG_LEVEL_INFO, "SAMPLING: Starting taking samples of sensor readings.");
  for (int i = 0; i < sensorSamplesPerSampling; i++) {
#ifdef SENSOR_ON_A1
    sensorA1Readings[i] = analogRead(SENSOR_A1_PIN);
#endif
#ifdef SENSOR_ON_A2
    sensorA2Readings[i] = analogRead(SENSOR_A2_PIN);
#endif
#ifdef SENSOR_ON_A3
    sensorA3Readings[i] = analogRead(SENSOR_A3_PIN);
#endif
#ifdef SENSOR_ON_A4
    sensorA4Readings[i] = analogRead(SENSOR_A4_PIN);
#endif
#ifdef SENSOR_ON_A5
    sensorA5Readings[i] = analogRead(SENSOR_A5_PIN);
#endif
    myWatchDog.clear();
    ledBlinkIdleCPU(sensorSamplingPeriod, HALF_SECOND);
    myWatchDog.clear();
  }
  consoleLogging(LOG_LEVEL_TRACE, "SAMPLING: Finished taking samples of sensor readings.");

  unsigned long timestamp = rtc.getEpoch();

  manySamplesInCsvString += String(timestamp);
  consoleLogging(LOG_LEVEL_TRACE, String("SAMPLING: Report string with report header follows.\n") + manySamplesInCsvString);

#ifdef SENSOR_ON_A1
  int sensorA1Median = QuickMedian< int >::GetMedian(sensorA1Readings, sensorSamplesPerSampling);
  int sensorA1toReport = 100 - map(sensorA1Median, SENSOR_A1_MIN_READING, SENSOR_A1_MAX_READING, SENSOR_A1_MIN_VALUE, SENSOR_A1_MAX_VALUE);
  manySamplesInCsvString += "," + String(sensorA1toReport);
  consoleLogging(LOG_LEVEL_TRACE, "SAMPLING: " + String(SENSOR_A1) + " reports: " + String(sensorA1Median));
  consoleLogging(LOG_LEVEL_TRACE, "SAMPLING: Report string:\n" + manySamplesInCsvString);
  myWatchDog.clear();
#else
  manySamplesInCsvString += ",";
  consoleLogging(LOG_LEVEL_TRACE, "SAMPLING: Report string:\n" + manySamplesInCsvString);
#endif

#ifdef SENSOR_ON_A2
  int sensorA2Median = QuickMedian< int >::GetMedian(sensorA2Readings, sensorSamplesPerSampling);
  int sensorA2toReport = 100 - map(sensorA2Median, SENSOR_A2_MIN_READING, SENSOR_A2_MAX_READING, SENSOR_A2_MIN_VALUE, SENSOR_A2_MAX_VALUE);
  manySamplesInCsvString += "," + String(sensorA2toReport);
  consoleLogging(LOG_LEVEL_TRACE, "SAMPLING: " + String(SENSOR_A2) + " reports: " + String(sensorA2Median));
  consoleLogging(LOG_LEVEL_TRACE, "SAMPLING: Report string:\n" + manySamplesInCsvString);
  myWatchDog.clear();
#else
  manySamplesInCsvString += ",";
  consoleLogging(LOG_LEVEL_TRACE, "SAMPLING: Report string:\n" + manySamplesInCsvString);
#endif

#ifdef SENSOR_ON_A3
  int sensorA3Median = QuickMedian< int >::GetMedian(sensorA3Readings, sensorSamplesPerSampling);
  int sensorA3toReport = 100 - map(sensorA3Median, SENSOR_A3_MIN_READING, SENSOR_A3_MAX_READING, SENSOR_A3_MIN_VALUE, SENSOR_A3_MAX_VALUE);
  manySamplesInCsvString += "," + String(sensorA3toReport);
  consoleLogging(LOG_LEVEL_TRACE, "SAMPLING: " + String(SENSOR_A3) + " reports: " + String(sensorA3Median));
  consoleLogging(LOG_LEVEL_TRACE, "SAMPLING: Report string:\n" + manySamplesInCsvString);
  myWatchDog.clear();
#else
  manySamplesInCsvString += ",";
  consoleLogging(LOG_LEVEL_TRACE, "SAMPLING: Report string:\n" + manySamplesInCsvString);
#endif

#ifdef SENSOR_ON_A4
  int sensorA4Median = QuickMedian< int >::GetMedian(sensorA4Readings, sensorSamplesPerSampling);
  int sensorA4toReport = map(sensorA4Median, SENSOR_A4_MIN_READING, SENSOR_A4_MAX_READING, SENSOR_A4_MIN_VALUE, SENSOR_A4_MAX_VALUE);
  manySamplesInCsvString += "," + String(sensorA4toReport);
  consoleLogging(LOG_LEVEL_TRACE, "SAMPLING: " + String(SENSOR_A4) + " reports: " + String(sensorA4Median));
  consoleLogging(LOG_LEVEL_TRACE, "SAMPLING: Report string:\n" + manySamplesInCsvString);
  myWatchDog.clear();
#else
  manySamplesInCsvString += ",";
  consoleLogging(LOG_LEVEL_TRACE, "SAMPLING: Report string:\n" + manySamplesInCsvString);
#endif

#ifdef SENSOR_ON_A5
  int sensorA5Median = QuickMedian< int >::GetMedian(sensorA5Readings, sensorSamplesPerSampling);
  int sensorA5toReport = map(sensorA5Median, SENSOR_A5_MIN_READING, SENSOR_A5_MAX_READING, SENSOR_A5_MIN_VALUE, SENSOR_A5_MAX_VALUE);
  manySamplesInCsvString += "," + String(sensorA5toReport);
  consoleLogging(LOG_LEVEL_TRACE, "SAMPLING: " + String(SENSOR_A5) + " reports: " + String(sensorA5Median));
  consoleLogging(LOG_LEVEL_TRACE, "SAMPLING: Report string:\n" + manySamplesInCsvString);
  myWatchDog.clear();
#else
  manySamplesInCsvString += ",";
  consoleLogging(LOG_LEVEL_TRACE, "SAMPLING: Report string:\n" + manySamplesInCsvString);
#endif

  manySamplesInCsvString += "\n";
  storedSamplesInCsvString++;

  consoleLogging(LOG_LEVEL_TRACE, "SAMPLING: All readings completed this loop.");
  consoleLogging(LOG_LEVEL_INFO, "SAMPLING: Report string:\n" + manySamplesInCsvString);

  consoleLogging(LOG_LEVEL_TRACE, "REPORTING: Reports in the string --> " + String(storedSamplesInCsvString));
  if (storedSamplesInCsvString >= MAX_REPORT_SAMPLES) {
    // If we are going to report, GSM must be enpowered first
    consoleLogging(LOG_LEVEL_INFO, "REPORTING: GSM should be in low power mode. Let's feed it properly.");
    myGsmAccess.noLowPowerMode();
    ledBlink(TWO_SECONDS, HALF_SECOND);  // Wait for the modem to fully come back from lowpowermode
    consoleLogging(LOG_LEVEL_TRACE, "REPORTING: About to send the report in an SMS.");
    // REPORT on SMS
    sendStringSMStoReport(manySamplesInCsvString, 3, FIVE_SECONDS);
    // We already reported, so GSM must be shutdown to save power
    consoleLogging(LOG_LEVEL_INFO, "REPORTING: GSM should be in full power mode. Let it starve to save battery.");
    myGsmAccess.lowPowerMode();
    // Then the sampling process resumes as if there was no power optimization
    manySamplesInCsvString = REPORT_HEADER;
    storedSamplesInCsvString = 0;
    consoleLogging(LOG_LEVEL_INFO, "REPORTING: SMS sent with combined readings.");
  }

  consoleLogging(LOG_LEVEL_TRACE, "SAMPLING: Report string at the end of the loop:\n" + manySamplesInCsvString);

  consoleLogging(LOG_LEVEL_INFO, "LOOP: All tasks completed this loop.");
  digitalWrite(LED_BUILTIN, LOW);
}

/****************************************************************************/
/*                          FUNCTION DEFINITIONS                            */
/****************************************************************************/

void initializeSerial() {
  Serial.begin(9600);
  while (!Serial) {
    delay(500);
  }
  consoleLogging(LOG_LEVEL_INFO, "CONSOLE: Serial USB initialized. Logging to console can now be used.");
}

/*
    PENDING: It seems to be OK and forum fellows tell me so,
             but still should check if this library is compatible with RTCZero used for PMIC library.
*/
void initializeWatchdog() {
  consoleLogging(LOG_LEVEL_INFO, "WATCHDOG: Starting...");
  myWatchDog.attachShutdown(myWatchDogShutdown);
  myWatchDog.setup(watchdogBarkPeriod);
  consoleLogging(LOG_LEVEL_INFO, "WATCHDOG: Initialized!");
}

void initializePMIC() {
  ///////////////////////////////////////////////////
  // Power Management Integrated Chip BQ24195LRGET //
  ///////////////////////////////////////////////////

  /* DO NOT NEED PMIC LIBRARY OR/AND DO NOT UNDERSTAND IT */

  // Clear watchdog counter
  consoleLogging(LOG_LEVEL_TRACE, "PMIC: Pet the watchdog before starting PMIC.");
  myWatchDog.clear();  // Let the dog know we are still in business

  consoleLogging(LOG_LEVEL_INFO, "PMIC: Starting...");

  // The IRQ is not called when in deep sleep mode, so this code is useless unless sleep mode does allow for IRQs to work
// Available only for MKRGSM1400 and MKRNB1500
#if defined(ARDUINO_SAMD_MKRGSM1400) || defined(ARDUINO_SAMD_MKRNB1500)
  // Attach the PMIC IRQ pin
  attachInterrupt(digitalPinToInterrupt(PMIC_IRQ_PIN), batteryInterrupt, FALLING);
  consoleLogging(LOG_LEVEL_INFO, "PMIC: Just attached IRQ with battery IRQ flag = " + String(batteryInterruptFired));
#endif

  // Initialize the power management integrated circuit that charges and uses the battery
  if (!PMIC.begin()) {
    consoleLogging(LOG_LEVEL_ERROR, "PMIC: Failed to initialize PMIC! Going for infinite sleep.");
    consoleLogging(LOG_LEVEL_TRACE, "PMIC: Disabling the watchdog before going to sleep.");
    myWatchDog.setup(WDT_OFF);
    do {
      // Shutdown the GSM module down and set the board into deepsleep mode
      myGsmAccess.secureShutdown();
      LowPower.deepSleep();
      // Somehow we were awaken from infinite sleep, so lets loop.
      consoleLogging(LOG_LEVEL_WARN, "PMIC: Awaken from infinite deepsleep due to PMIC initialization error. Lets deepsleep again.");
    } while (true);
  } else {
    // Reset the interrupt variable since the interrupt function is allways called upon setting it
    batteryInterruptFired = false;
  }
  consoleLogging(LOG_LEVEL_TRACE, "PMIC started properly: Battery IRQ flag = " + String(batteryInterruptFired));

  myWatchDog.clear();  // Let the dog know we are still in business

  // Set the input current limit to 2 A
  if (!PMIC.setInputCurrentLimit(2.0)) {
    consoleLogging(LOG_LEVEL_WARN, "PMIC: Error in set input current limit");
  } else {
    consoleLogging(LOG_LEVEL_TRACE, "PMIC: Successfully set input current limit");
  }

  // Set the overload input voltage to 3.88 V
  if (!PMIC.setInputVoltageLimit(3.88)) {
    consoleLogging(LOG_LEVEL_WARN, "PMIC: Error in set input voltage limit");
  } else {
    consoleLogging(LOG_LEVEL_TRACE, "PMIC: Successfully set input voltage limit");
  }

  // Set the minimum voltage used to feeding the module embed on Board
  if (!PMIC.setMinimumSystemVoltage(3.5)) {
    consoleLogging(LOG_LEVEL_WARN, "PMIC: Error in set minimum system voltage");
  } else {
    consoleLogging(LOG_LEVEL_TRACE, "PMIC: Successfully set minimum system voltage");
  }

  // Set the desired charge voltage to 4.11 V
  if (!PMIC.setChargeVoltage(4.2)) {
    consoleLogging(LOG_LEVEL_WARN, "PMIC: Error in set charge voltage");
  } else {
    consoleLogging(LOG_LEVEL_TRACE, "PMIC: Successfully set charge voltage");
  }

  // Set the charge current 500 mA
  if (!PMIC.setChargeCurrent(0.500)) {
    consoleLogging(LOG_LEVEL_WARN, "PMIC: Error in set charge current");
  } else {
    consoleLogging(LOG_LEVEL_TRACE, "PMIC: Successfully set charge current");
  }

  // Enable the Charger
  if (!PMIC.enableCharge()) {
    consoleLogging(LOG_LEVEL_WARN, "PMIC: Error enabling Charge mode");
  } else {
    consoleLogging(LOG_LEVEL_TRACE, "PMIC: Charge mode enabled");
  }

  myWatchDog.clear();  // Let the dog know we are still in business

  consoleLogging(LOG_LEVEL_INFO, "PMIC: initialization done!");
}

void initializeGSM() {

  consoleLogging(LOG_LEVEL_INFO, "GSM: Starting...");

  // Wait for the GSM module to start
  while (!myGSMModem.begin()) {
    consoleLogging(LOG_LEVEL_TRACE, "GSM: Trying to connect to the GSM modem...");
    delay(1000);
  }

  ledBlink(5000, 1000);

  consoleLogging(LOG_LEVEL_TRACE, "GSM: Pet the watchdog before checking SIM card status.");
  myWatchDog.clear();  // Let the dog know we are still in business

  // Check SIM status if enabled
  myPINmanager.begin();
  int myPINstatus = myPINmanager.isPIN();

  if (myPINstatus == 0) {
    // SIM does not require PIN
    handleSIMwithoutPIN();
  } else if (myPINstatus == 1) {
    // SIM requires PIN
    handleSIMRequiresPIN();
  } else if (myPINstatus == -1) {
    // PIN is locked
    handleSIMLocked("PIN");
  } else if (myPINstatus == -2) {
    // PUK is locked
    handleSIMLocked("PUK");
  } else {
    // Unknown status
    handleUnknownSIMStatus();
  }

  consoleLogging(LOG_LEVEL_TRACE, "GSM: Pet the watchdog before starting GSM connection.");
  myWatchDog.clear();  // Let the dog know we are still in business

  // Attempt to connect to GSM
  int attemptsConnectGSM = 0;
  do {
    GSM3_NetworkStatus_t lastCallGsmAccessReturned = myGsmAccess.begin(SIM_PIN);
    if (lastCallGsmAccessReturned == ERROR) {
      consoleLogging(LOG_LEVEL_ERROR, "GSM: myGsmAccess.begin(SIM_PIN) returned ERROR, but must be network unavailable because the PIN should be OK. If not, it will lock the SIM.");
    } else if (lastCallGsmAccessReturned == IDLE) {
      consoleLogging(LOG_LEVEL_INFO, "GSM: myGsmAccess.begin(SIM_PIN) returned IDLE");
    } else if (lastCallGsmAccessReturned == CONNECTING) {
      consoleLogging(LOG_LEVEL_INFO, "GSM: myGsmAccess.begin(SIM_PIN) returned CONNECTING");
    } else if (lastCallGsmAccessReturned == GSM_READY) {
      consoleLogging(LOG_LEVEL_INFO, "GSM: myGsmAccess.begin(SIM_PIN) returned GSM_READY");
      break;
    } else if (lastCallGsmAccessReturned == GPRS_READY) {
      consoleLogging(LOG_LEVEL_INFO, "GSM: myGsmAccess.begin(SIM_PIN) returned GPRS_READY");
    } else if (lastCallGsmAccessReturned == TRANSPARENT_CONNECTED) {
      consoleLogging(LOG_LEVEL_INFO, "GSM: myGsmAccess.begin(SIM_PIN) returned TRANSPARENT_CONNECTED");
    } else if (lastCallGsmAccessReturned == GSM_OFF) {
      consoleLogging(LOG_LEVEL_INFO, "GSM: myGsmAccess.begin(SIM_PIN) returned GSM_OFF");
    }

    attemptsConnectGSM++;
    consoleLogging(LOG_LEVEL_INFO, "GSM: [" + String(attemptsConnectGSM) + "/" + String(GSM_MAX_CONNECT_ATTEMPTS) + "] GSM not connected yet.");
    // Disable watchdog timer
    consoleLogging(LOG_LEVEL_TRACE, "GSM: Disabling the watchdog before going to idle waiting to GSM connections.");
    myWatchDog.setup(WDT_OFF);
    // Put the board into sleep mode
    ledBlinkIdleCPU(GSM_CONNECT_WAITING_TIME, HALF_SECOND);
    // Enable watchdog timer
    consoleLogging(LOG_LEVEL_TRACE, "GSM: Re-enabling the watchdog after going to idle waiting to GSM connections.");
    myWatchDog.setup(watchdogBarkPeriod);  // initialize WDT soft counter to 2 minutes interval since GSM is slow to connect

  } while (attemptsConnectGSM <= GSM_MAX_CONNECT_ATTEMPTS);

  consoleLogging(LOG_LEVEL_INFO, "GSM: Connection loop ended at [" + String(attemptsConnectGSM + 1) + "/" + String(GSM_MAX_CONNECT_ATTEMPTS) + "].");
  consoleLogging(LOG_LEVEL_TRACE, "GSM: Pet the watchdog after the GSM connection loop.");
  myWatchDog.clear();

  // Check if maximum connection attempts were reached
  if (attemptsConnectGSM >= GSM_MAX_CONNECT_ATTEMPTS) {
    handleMaxConnectionAttempts();
  } else {
    consoleLogging(LOG_LEVEL_TRACE, "GSM: Pet the watchdog after the GSM initialization.");
    myWatchDog.clear();  // Let the dog know we are still in business
    consoleLogging(LOG_LEVEL_INFO, "GSM: Initialization done!");
  }
}

void synchronizeTime() {

  consoleLogging(LOG_LEVEL_TRACE, "TIME: Disabling the watchdog before time synch with the GSM datetime.");
  myWatchDog.setup(WDT_OFF);

  rtc.begin();

  consoleLogging(LOG_LEVEL_INFO, "TIME: Pre synch epoch is " + String(rtc.getEpoch()));

  consoleLogging(LOG_LEVEL_INFO, "TIME: Synchronizing date and time with GSM network");
  /*********************************************************************************************
   THERE IS A GSM.GETTIME() FUNCTION AT MKRGSM LIBRARY, BUT IT RETURNED WRONG DATA IN THE PAST. 
  *********************************************************************************************/
  long gsmTime = myGsmAccess.getTime();
  if (gsmTime) {
    rtc.setEpoch(gsmTime);
    consoleLogging(LOG_LEVEL_INFO, "TIME: Time synchronized with GSM network as epoch " + String(rtc.getEpoch()));
  } else {
    consoleLogging(LOG_LEVEL_WARN, "TIME: Failed to get date and time from GSM network");
  }

  consoleLogging(LOG_LEVEL_TRACE, "TIME: Re-enabling the watchdog before time synch with the GSM datetime.");
  myWatchDog.setup(watchdogBarkPeriod);  // initialize WDT soft counter to 2 minutes interval since GSM is so slow to connect

  consoleLogging(LOG_LEVEL_INFO, "TIME: Clock synchronization done!");
}

void initializeReporting() {
  manySamplesInCsvString = REPORT_HEADER;
  storedSamplesInCsvString = 0;
}


/****************************************************************************/
/*                            PMIC FUNCTIONS                                */
/****************************************************************************/

// The IRQ is not called when in deep sleep mode, so this code is useless unless sleep mode allows this IRQ
void batteryInterrupt() {
  //Should be quick simple and straightforward, keep track of when the battery stopped charging
  consoleLogging(LOG_LEVEL_WARN, "PMIC: Battery interrupt!");
  batteryInterruptFired = true;  // Set the flag to true to indicate power source change
}

/****************************************************************************/
/*                             GSM FUNCTIONS                                */
/****************************************************************************/

String doATcommandModemGSM(const char* s, unsigned long timeout) {
  String doATresponse;
  doATresponse.reserve(15);
  MODEM.send(s);
#ifdef CONSOLE_LOGGING
  int doATreturn =
#endif
    MODEM.waitForResponse(timeout, &doATresponse);
  consoleLogging(LOG_LEVEL_TRACE, "Modem received \"" + String(s) + "\" command, returned \"" + String(doATreturn) + "\" with message \"" + String(doATresponse) + "\"");
  return doATresponse;
}

bool getPINbyICCID(const char* iccid, char* pin) {
  for (int i = 0; i < numSIMCards; i++) {
    if (strcmp(simCards[i].iccid, iccid) == 0) {
      strcpy(pin, simCards[i].pin);
      return true;
    }
  }
  return false;
}

void handleSIMwithoutPIN() {
  consoleLogging(LOG_LEVEL_INFO, "GSM: SIM card reports no PIN required. Lets check if we can enable it.");

  // Get ICCID of the SIM
  String iccid = myGSMModem.getICCID();
  consoleLogging(LOG_LEVEL_TRACE, "GSM: This SIM card ICCID is " + iccid);

  // Convert String to char array
  char iccidChar[21];
  iccid.toCharArray(iccidChar, 21);

  // Find the corresponding PIN
  if (getPINbyICCID(iccidChar, SIM_PIN)) {
    consoleLogging(LOG_LEVEL_INFO, "GSM: This SIM card has a stored PIN of [" + String(SIM_PIN) + "]");
    consoleLogging(LOG_LEVEL_TRACE, "GSM: PIN lock will be enabled on this SIM now, if PIN is accepted.");
    myPINmanager.switchPIN(SIM_PIN);
  } else {
    consoleLogging(LOG_LEVEL_ERROR, "GSM: This SIM card (ICCID) has no stored PIN.");
  }
}

void handleSIMRequiresPIN() {
  consoleLogging(LOG_LEVEL_INFO, "GSM: SIM card reports PIN required. PIN management needed.");

  // Get ICCID of the SIM
  String iccid = myGSMModem.getICCID();
  consoleLogging(LOG_LEVEL_TRACE, "GSM: This SIM card ICCID is " + iccid);

  // Convert String to char array
  char iccidChar[21];
  iccid.toCharArray(iccidChar, 21);

  // Find the corresponding PIN
  if (!getPINbyICCID(iccidChar, SIM_PIN)) {
    handleMissingPIN();
  }
  consoleLogging(LOG_LEVEL_INFO, "GSM: This SIM card (ICCID) has a stored PIN of " + String(SIM_PIN));
}

void handleSIMLocked(const char* lockType) {
  // Set watchdog timer off
  myWatchDog.setup(WDT_OFF);
  // Loop forever because we do not want to automatically use the PUK or handle locked status.
  consoleLogging(LOG_LEVEL_ERROR, "GSM: SIM card reports " + String(lockType) + " is locked.");
  // Shutdown the GSM module and set the board into deep sleep mode forever inside a loop
  myGsmAccess.secureShutdown();
  do {
    ledBlinkSOSidleCPU();
    LowPower.deepSleep();
    // Somehow we were awakened from infinite sleep, so let's loop.
    consoleLogging(LOG_LEVEL_WARN, "GSM: Awaken from infinite sleep due to SIM card being " + String(lockType) + " locked. Let's sleep again.");
  } while (true);
}

void handleUnknownSIMStatus() {
  // Set watchdog timer off
  myWatchDog.setup(WDT_OFF);
  // Loop forever because we cannot tell what happened.
  consoleLogging(LOG_LEVEL_ERROR, "GSM: SIM card reports unknown PIN status.");
  // Shutdown the GSM module and set the board into deep sleep mode forever inside a loop
  myGsmAccess.secureShutdown();
  do {
    ledBlinkSOSidleCPU();
    LowPower.deepSleep();
    // Somehow we were awakened from infinite sleep, so let's loop.
    consoleLogging(LOG_LEVEL_WARN, "GSM: Awaken from infinite sleep due to SIM unknown PIN status. Let's sleep again.");
  } while (true);
}

void handleMissingPIN() {
  consoleLogging(LOG_LEVEL_ERROR, "GSM: SIM card reports PIN required but none found.");
  // Shutdown the GSM module and set the board into deep sleep mode forever inside a loop
  myWatchDog.setup(WDT_OFF);
  myGsmAccess.secureShutdown();
  do {
    ledBlinkSOSidleCPU();
    LowPower.deepSleep();
    // Somehow we were awakened from infinite sleep, so let's loop.
    consoleLogging(LOG_LEVEL_WARN, "GSM: Awakened from infinite sleep due to SIM card required PIN but none found. Let's sleep again.");
  } while (true);
}

void handleIncorrectPIN() {
  consoleLogging(LOG_LEVEL_ERROR, "GSM: SIM card reports PIN required but the one found is not accepted.");
  // Shutdown the GSM module and set the board into deep sleep mode forever inside a loop
  myWatchDog.setup(WDT_OFF);
  myGsmAccess.secureShutdown();
  do {
    ledBlinkSOSidleCPU();
    LowPower.deepSleep();
    // Somehow we were awakened from infinite sleep, so let's loop.
    consoleLogging(LOG_LEVEL_WARN, "GSM: Awakened from infinite sleep due to SIM card required PIN but the chosen one was wrong. Let's sleep again.");
  } while (true);
}

void handleMaxConnectionAttempts() {
  consoleLogging(LOG_LEVEL_ERROR, "GSM: Maximum connection attempts reached. Going to sleep then restart.");
  // Disable watchdog timer
  myWatchDog.setup(WDT_OFF);
  // Shutdown the GSM module and set the board into deep sleep mode for a loop period in the hope something could change
  myGsmAccess.secureShutdown();
  ledBlinkSOSidleCPU();
  LowPower.deepSleep(loopPeriod);
  consoleLogging(LOG_LEVEL_WARN, "GSM: Awoke after a nap after maximum connection attempts were reached. Going to restart.");
  restartBoard();
}

/*
// Function to calculate the number of phone numbers
size_t getNumberOfPhoneNumbers(const char* phoneNumbers[]) {
  return sizeof(phoneNumbers) / sizeof(phoneNumbers[0]);
}
*/
// Template function to calculate the number of elements in an array at compile time
template<typename T, size_t N>
constexpr size_t getNumberOfPhoneNumbers(const T (&)[N]) {
  return N;
}

// Function to calculate the maximum length of phone numbers
size_t getMaxPhoneNumberLength(const char* phoneNumbers[], size_t numPhoneNumbers) {
  size_t maxLength = 0;
  for (size_t i = 0; i < numPhoneNumbers; ++i) {
    size_t length = strlen(phoneNumbers[i]);
    if (length > maxLength) {
      maxLength = length;
    }
  }
  return maxLength + 1;  // Adding 1 for the null terminator
}

void sendStringSMStoControl(String message, int retries, int period) {
  // Check if the length is lower than the SMS_MAXIMUM_LENGTH const defined
  if (message.length() > SMS_MAXIMUM_LENGTH) {
    message = message.substring(0, SMS_MAXIMUM_LENGTH - 1);
    consoleLogging(LOG_LEVEL_SILENT, "SMS truncated  because of SMS length limit: " + message);
  }
  message = "[" + myGSMModem.getIMEI() + "] " + message;
  // Declare and populate the char array that the sendSMS function needs
  char messageArray[SMS_MAXIMUM_LENGTH];
  message.toCharArray(messageArray, SMS_MAXIMUM_LENGTH);
  // Find the number of phones available
  size_t numPhoneNumbers = getNumberOfPhoneNumbers(controlPhoneNumber);
  // For each phone number we follow the same process
  for (size_t phoneIndex = 0; phoneIndex < numPhoneNumbers; phoneIndex++) {
    // Actually send the SMS in a loop until maximum retries have been reached
    for (int i = 0; i < retries; i++) {
      myWatchDog.clear();
      if (mySms.beginSMS(controlPhoneNumber[phoneIndex])) {
        mySms.print(messageArray);
        if (mySms.endSMS()) {
          consoleLogging(LOG_LEVEL_TRACE, "SMS sent: " + message);
          break;  // SMS sent successfully, let's try the next phone number
        } else {
          consoleLogging(LOG_LEVEL_WARN, "Error while sending SMS");
        }
      } else {
        consoleLogging(LOG_LEVEL_WARN, "Error starting to send SMS");
      }
      LowPower.idle(period);
    }
  }
  return;
}

void sendStringSMStoReport(String message, int retries, int period) {
  // Check if the length is lower than the SMS_MAXIMUM_LENGTH const defined
  if (message.length() > SMS_MAXIMUM_LENGTH) {
    message = message.substring(0, SMS_MAXIMUM_LENGTH - 1);
    consoleLogging(LOG_LEVEL_SILENT, "SMS truncated  because of SMS length limit: " + message);
  }
  //message = "[" + myGSMModem.getIMEI() + "] " + message;
  // Declare and populate the char array that the sendSMS function needs
  char messageArray[SMS_MAXIMUM_LENGTH];
  message.toCharArray(messageArray, SMS_MAXIMUM_LENGTH);
  // Find the number of phones available
  size_t numPhoneNumbers = getNumberOfPhoneNumbers(reportPhoneNumber);
  // For each phone number we follow the same process
  for (size_t phoneIndex = 0; phoneIndex < numPhoneNumbers; phoneIndex++) {
    // Actually send the SMS in a loop until maximum retries have been reached
    for (int i = 0; i < retries; i++) {
      myWatchDog.clear();
      if (mySms.beginSMS(reportPhoneNumber[phoneIndex])) {
        mySms.print(messageArray);
        if (mySms.endSMS()) {
          consoleLogging(LOG_LEVEL_TRACE, "SMS sent: " + message);
          break;  // SMS sent successfully, let's try the next phone number
        } else {
          consoleLogging(LOG_LEVEL_WARN, "Error while sending SMS");
        }
      } else {
        consoleLogging(LOG_LEVEL_WARN, "Error starting to send SMS");
      }
      LowPower.idle(period);
    }
  }
  return;
}

/****************************************************************************/
/*                            Sensor FUNCTIONS                              */
/****************************************************************************/

void initializeSensors() {
  /**************************************************************************
    PENDING: Should find a way to compare the watchdog timer with the sampling period.
             They cannot be compared straight away, since wdt counts in clycles while sampling period is in miliseconds
  // Check if sampling rate is compatible with watchdog timeout
  if ((2 * sensorSamplingPeriod) < watchdogBarkPeriod) {
    consoleLogging(LOG_LEVEL_ERROR, "Sensor sampling period is too high compared to watchdog timer.");
    myWatchDog.setup(WDT_OFF);
    sendStringSMS("Shutting down due to the sampling period being too high compared to watchdog timer.", 3, FIVE_SECONDS);
    do {
      // Put the board into deepsleep mode, sensors can be turned off because this state is inconsistent due to bad programming
      LowPower.deepSleep();
      // Somehow we were awaken from infinite sleep, so lets loop.
      consoleLogging(LOG_LEVEL_WARN, "Awaken from infinite sleep due to ensor sampling period is too high compared to watchdog timer. Lets sleep again.");
    } while (true);
  }
  ***************************************************************************/
}

/****************************************************************************/
/*                             TOOLKIT FUNCTIONS                            */
/****************************************************************************/

void myWatchDogShutdown() {
  // This must be quick since it is kind of an interrupt
  consoleLogging(LOG_LEVEL_ERROR, "The watch dog is shutting down the board! ...");
}

void restartBoard() {
  // Restart the Arduino board
  NVIC_SystemReset();
  /* Alternative low level method by Chat-GPT4o
  SCB_AIRCR = 0x5FA0004;  // Force reset
  */
}

int ledBlinkIdleCPU(int durationMS, int periodMS) {
  int initialState = digitalRead(LED_BUILTIN);
  for (int i = 0; i < (durationMS / periodMS); i++) {
    digitalWrite(LED_BUILTIN, initialState == HIGH ? LOW : HIGH);
    LowPower.idle(periodMS / 2);
    digitalWrite(LED_BUILTIN, initialState);
    LowPower.idle(periodMS / 2);
  }
  return initialState;
}

int ledBlink(int durationMS, int periodMS) {
  int initialState = digitalRead(LED_BUILTIN);
  for (int i = 0; i < (durationMS / periodMS); i++) {
    digitalWrite(LED_BUILTIN, initialState == HIGH ? LOW : HIGH);
    delay(periodMS / 2);
    digitalWrite(LED_BUILTIN, initialState);
    delay(periodMS / 2);
  }
  return initialState;
}

void ledBlinkSOSidleCPU() {
  int shortBlink = 500;    // 250 ms for a short blink
  int longBlink = 1500;    // 750 ms for a long blink
  int pause = 500;         // 250 ms between blinks
  int letterPause = 1500;  // 1000 ms between letters (dots and dashes)
  int wordPause = 3000;    // 5000 ms between SOS signals

  digitalWrite(LED_BUILTIN, LOW);
  LowPower.idle(wordPause);
  for (int t = 0; t < 5; t++) {
    // S: ...
    for (int i = 0; i < 3; i++) {
      digitalWrite(LED_BUILTIN, HIGH);
      LowPower.idle(shortBlink);
      digitalWrite(LED_BUILTIN, LOW);
      LowPower.idle(pause);
    }
    LowPower.idle(letterPause);

    // O: ---
    for (int i = 0; i < 3; i++) {
      digitalWrite(LED_BUILTIN, HIGH);
      LowPower.idle(longBlink);
      digitalWrite(LED_BUILTIN, LOW);
      LowPower.idle(pause);
    }
    LowPower.idle(letterPause);

    // S: ...
    for (int i = 0; i < 3; i++) {
      digitalWrite(LED_BUILTIN, HIGH);
      LowPower.idle(shortBlink);
      digitalWrite(LED_BUILTIN, LOW);
      LowPower.idle(pause);
    }
    LowPower.idle(wordPause);
  }
}

void ledBlinkSOS() {
  int shortBlink = 500;    // 250 ms for a short blink
  int longBlink = 1500;    // 750 ms for a long blink
  int pause = 500;         // 250 ms between blinks
  int letterPause = 1500;  // 1000 ms between letters (dots and dashes)
  int wordPause = 3000;    // 5000 ms between SOS signals

  digitalWrite(LED_BUILTIN, LOW);
  delay(wordPause);
  for (int t = 0; t < 5; t++) {
    // S: ...
    for (int i = 0; i < 3; i++) {
      digitalWrite(LED_BUILTIN, HIGH);
      delay(shortBlink);
      digitalWrite(LED_BUILTIN, LOW);
      delay(pause);
    }
    delay(letterPause);

    // O: ---
    for (int i = 0; i < 3; i++) {
      digitalWrite(LED_BUILTIN, HIGH);
      delay(longBlink);
      digitalWrite(LED_BUILTIN, LOW);
      delay(pause);
    }
    delay(letterPause);

    // S: ...
    for (int i = 0; i < 3; i++) {
      digitalWrite(LED_BUILTIN, HIGH);
      delay(shortBlink);
      digitalWrite(LED_BUILTIN, LOW);
      delay(pause);
    }
    delay(wordPause);
  }
}

And the redacted secrets:

#ifndef ARDUINO_SECRETS_H
#define ARDUINO_SECRETS_H

#define SECRET_SIM_APN "isp_apn"
#define SECRET_OPTIONAL_SIM_APN_USERNAME "ISP_username"
#define SECRET_OPTIONAL_SIM_APN_PASSWORD "ISP_password"
#define SECRET_CONTROL_PHONE_NUMBER { "666666666", "666666667" }
#define SECRET_REPORT_PHONE_NUMBER { "6666666668"}

// Structure to store ICCID, PIN and PUK details
typedef struct {
    char iccid[20];
    char pin[5];
    char puk[9];
} SIMCardInfo;

// Your SIM details here
// The SIM card label does not include the leading "1234" digits reported by a call to getICCID()
const SIMCardInfo simCards[] = {
    {"1234567890123456789", "1111", "12345678"}, // ICCID, PIN, PUK
    {"1234567890123456790", "2222", "12345679"}, // ICCID, PIN, PUK
    {"1234567890123456791", "3333", "12345680"}, // ICCID, PIN, PUK
    // Add more SIM card detaisl as needed
};

const int numSIMCards = sizeof(simCards) / sizeof(simCards[0]);

#endif

Hey, your code is very proper, all the comments etc. Like it should be!

No worries. The memory of the MKR is 256 kByte. My sketch for example is 220 kByte. So the previous firmware can not be installed in paralell. Thats why the SD card comes into play. The new firmware has to be stored on the SD card with the magic filename "UPDATE.bin" and then after a reboot, the SDU.h finds that file and does the update and deletes the file on the SD card. BTW thats why SDU.h has to be a part of the old firmware. The SD card reader has to be attached to the mkr, I'm using Adafruit TFT Displays with integrated SD Card reader.
My "Use the modem memory and copy to the SD card" was just a workaround while I was not aware of the root problem, the WDTZero blocks the AT console from time to time.
I see not way to update the firmware directly out of the modem memory (some cracks can maybe do that, i can't).
Attachinig a SD card reader is an easy task (SD CS (ChipSelect) has to be on Pin D4 as expected from SDU.h).
An advantage is that you can also do manual updates with the SD Card. Just export as binary in the IDE, rename the version without the bootloader as UPDATE.bin (Exactly like this with uppercases), copy it on the SD Card, insert that card and reset the board (Again, the loaded skatch has to have SDU.h).

However you trigger the OTA routine, the process of the update remains the same.

I'm not aware of a WDT_OFF command, the keywords.txt of the lib says:
WDTZero KEYWORD1
clear KEYWORD2
setup KEYWORD2
With "clear" ans "setup" later , if I remember right, was not working

Normally there is also version without bootloader? Anyway, this is the file to be renamed to UPDATE.bin and copied to the SD card.

Hope to not confusing even more ;=)

Just attach a SD card reader and try it out, I'm pretty sure you will laugh afterwards how easy everything is.

Thank you very much for the detailed explanation.

I use the call mywd.setup(WDT_OFF) to deactivate the watch dog when not needed. Like when going to sleep for ten minutes between tasks. Can you think of an easy way to test if the error you discovered in the communications between modem and cpu also happens in that state of the waychdog? From your explanation I assume you did not encounter that particular scenario.

I do not feel comfortable at all with magic methods, but feels simple enough not to expect any issues. Thank you. I was reading through the SDU library to try and take the rabbit out of the hat, but I got nothing after some serious staring.

I guess i could use the mem shield available at the store so my connector carrier does hold the mkr and the shield is attached on top.

I need to keep power consumption low, and the device is deployed outdoors and south oriented where the sun would prevent any screen from being readable.

Thanks a lot!!!

I don't knew that and will try this.

Wow, that device is also new to me...Thank you

I checked the pinout to see if the connector carrier and the mem shield are compatible or predate each other. It seems I loose D4 to the SD card but not much more.

The flash is supposed to last ten thousand cycles each sector, so for OTA should be enough forever. Maybe even store the config so it is persistent to reboots.

But i still haven't found out if the FlashStorage library would do the SNU.h integration for the magic OTA to happen. This is an example of the reason I dislike magic. I won't be able to predict if it should work unless someone did and shared or I purchase and try.

Anyway, i am in the middle of a full code reorg into libraries of my own, so I will wait for shipping to arrive with two of those and a new outdoor antena.

Please, let me know if you could benefit from my help testing the OFF option of the watchdog. I use it but, again, it's magic inside the WDTZero library that fierce stare did not translate into knowledge or understanding.

Btw, there is a second storage shield without flash, only the SD reader for the cards that you already own. And it is half the cost of the one I chose. I am very clumsy, loosing the sd cards is very likely for me, so having the flash as a bigger shield without soldering is aligned with my particular case. But the cheaper one feels more adequate for reuse of your sd cards.

I will probably use the library GitHub - arduino-libraries/Arduino_MKRMEM: Arduino library providing SPIFFS for the W25Q16DV flash on the MKR MEM shield. because it seems to be particularly oriented to the shield that carries on its name. Although being so specific, it arises the question of compatibility with the OTA process based on the SDU.h library.

The libraries GitHub - khoih-prog/FlashStorage_SAMD: The FlashStorage_SAMD library provides a convenient way to store and retrieve user's data using the non-volatile flash memory and GitHub - cmaglie/FlashStorage: A convenient way to store data into Flash memory on the ATSAMD21 and ATSAMD51 processor family seem somehow compatible and more widely used, but not oriented to the flash in the shield but the one internal, so who knows. I'd rather tamper with the flash at MKR MEM shield and revert to SD if I wear it off too soon with my tests, instead of plainly loose the MKR GSM 1400 board because of the internal flash end of life.

FYI: I just saw that I reset the timer every downloaded byte with the MKR1010:


// OTA Update
void handleSketchDownload() {

  const char *SERVER = "server.com";  // must be string for HttpClient
  const unsigned short SERVER_PORT = 80;
  const char *PATH = "/UPDATE.bin";
  const unsigned long CHECK_INTERVAL = 5000;

  static unsigned long previousMillis;

  unsigned long currentMillis = millis();

  if (currentMillis - previousMillis < CHECK_INTERVAL)
    return;
  previousMillis = currentMillis;
  MyWatchDoggy.clear();
  //GSMClient transport;
  HttpClient http_client_update(ota, SERVER, SERVER_PORT);
  MyWatchDoggy.clear();
  char buff[32];
  snprintf(buff, sizeof(buff), PATH, VERSION + 1);

  Serial.print("Check for update file ");
  Serial.println(buff);

  http_client_update.get(buff);

  int statusCode = http_client_update.responseStatusCode();
  Serial.print("HTTP status code: ");
  Serial.println(statusCode);
  if (statusCode != 200) {
    http_client_update.stop();
    return;
  }

  long length = http_client_update.contentLength();
  if (length == HttpClient::kNoContentLengthHeader) {
    http_client_update.stop();
    Serial.print("DOWNLOAD FAIL!");
    delay(1000);
    return;
  }
  //Serialprint("Server returned update file of size ");
  //Serialprint(length);
  //Serialprintln(" bytes");
  //dev löschen
  if (SD.exists("UPDATE.BIN")) {
    //Serialprintln("update file exists.");
    SD.remove("UPDATE.BIN");
    delay (1000);
  }
  else {
    //Serialprintln("update file on SD Card doesn't exist.");
  }

  file_ota = SD.open("UPDATE.BIN", FILE_WRITE);
  delay (500);
  if (!file_ota) {
    http_client_update.stop();
    Serial.print("SD CARD ERROR");
    delay(500);
    //Serialprintln("Could not create bin file. Can't continue with update.");
    return;
  }

  byte b;
  while (length > 0) {
    if (!http_client_update.readBytes(&b, 1))  // reading a byte with timeout
      break;
    file_ota.write(b);
    length--;
    MyWatchDoggy.clear();
  }


  file_ota.close();
  http_client_update.stop();
  if (length > 0) {
    SD.remove("UPDATE.BIN");
    
    Serial.print("Download fail");
    //Serialprint(length);
    //Serialprintln(" bytes. Can't continue with update.");
    return;
  }

  //Serial.println("Update file saved. Reset.");
  //Serial.flush();

 Serial.print("UPDATED! RESTART NOW!");
  delay(2000);

  NVIC_SystemReset();

}

That keeps the periodic check active on the wd, right? Wonder if WDT_OFF actually disables the check. I will try to find out reading the sources again.

Thanks for the update!

Yes, IMHO this makes sense to have the Watchdog watching during the update.

I checked WDT_OFF and it should work:


WDTZero::WDTZero()
{
}

void WDTZero::setup(unsigned int wdtzerosetup) {
  // One-time initialization of watchdog timer.

if (!wdtzerosetup){                   // code 0x00 stops the watchdog
    NVIC_DisableIRQ(WDT_IRQn);        // disable IRQ
    NVIC_ClearPendingIRQ(WDT_IRQn);
    WDT->CTRL.bit.ENABLE = 0;        // Stop watchdog now!
    while(WDT->STATUS.bit.SYNCBUSY);
    }
else {
//  Split out the bits wdtzerosetup =>  _ewtcounter=CNT[3] _w=EWen[1]  _x=DIV[4]  _y=PER[4]  _z=EW[4]
#ifdef DBGON
 Serial.print(wdtzerosetup,HEX); Serial.print("= w/");
#endif

Then the only issue would be that it was slow, right? Not very nice to have the dog dead while copying the file, though. Thanks for looking into it, I kind of got wrapped up on another issue and procrastinated a bit on this.

No, also to my surprise a 230 kByte sketch OTA update with this routine takes a second or two.

Perhaps that method would be ok if the watchdog is active but only cleared before and after the file transfer. Two seconds is not too much to wait without clearing the watchdog, is it?

Yes, probably not. It just works like that within two seconds and i see also no problem to reset the dog a million times..our electronic slaves have to work!!

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.