Initial public release of WII5 Buoy firmware

Firmware for an autonomous wave-measurement buoy (ATmega2560-based
WII5 v2 board). Reads wave motion from a Sparton AHRS-M1/M2 IMU,
samples GPS and battery state, and reports back over Iridium SBD
satellite telemetry. Originally developed 2012-2024.

This is the first public release. Code, documentation, and field-tested
operating modes (Capture, Sleep, Position, ManualTest, SelfTest,
LowBattery) are licensed under Apache 2.0 — see LICENSE and NOTICE.

See README.md for an overview and build instructions, CONTRIBUTING.md
for how to contribute, and DEPLOYMENTS.md for the field-deployment log.
This commit is contained in:
2026-05-07 16:00:21 +10:00
commit 295abb37ee
122 changed files with 38142 additions and 0 deletions
+521
View File
@@ -0,0 +1,521 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2012-2024 Scott Penrose <scottp@dd.com.au> and WII5 Buoy contributors
//
// This file is part of WII5 Buoy firmware.
// See LICENSE for full terms.
/**
* @file WII5Controller.cpp
* @brief Main controller: mode dispatch, watchdog, and the top-level loop.
*/
/*
Controller - Main controller, for loops and processing within WII5.
Basically the controller is in charge of what is called for setup and loop.
SPLIT:
* Setup - helpers and stuff to setup code - doesn't not need to be here
* Mode, setMode, calculateMode etc
* Console and Settings
HOWTO USE "yield" Here
NOT READY - Causes infinite loops
* create a "yield" in the sketch and call
* wii5Controller.yield()l
* And any other important entries, e.g. Network
* And in loop() call yield at the start of loop
TEMPLATE:
LOOPS:
loop:
- This is the actual loop and it just calls the others, depending on mode
- Calls loopWDT
- Based on mode, calls
.
- Call loopSafe - always?
loopWDT:
- Just update the WDT and spin on
loopSafe:
- All the commands that must be always run
- Calls
. sh3dNodeIO.loop();
. wii5Commands.processButtons();
. console.loop();
. wii5Commands.processConsoleAT();
. wii5Commands.processConsoleStandard();
. wii5Iridium.loop();
. wii5Communications.loop();
loopDefault:
- Old - everything, instead, this is now loopOther
loopOther:
- Everything else
- Calls
. wii5Sparton.loop();
. wii5RadioLoRa.loop();
. wii5Commands.processNetwork();
. wii5Gps.loop();
. wii5Battery.loop();
. wii5Weather_18B20.loop();
loopNotCapturing:
Other places loop* is called from
safeDelay (not complete)
*/
#include <Arduino.h>
#include <TimeLib.h>
#include <WII5.h>
#include <WII5Controller.h>
#include <WII5Strings.h>
#include <MemoryFree.h>
#include <avr/wdt.h>
// Debugging etc - move to header file
#define DEBUG_SLEEP
void WII5Controller::begin() {
uptime = 0;
minuteWait = 0;
wii5ModeSleep.begin();
wii5ModeCapture.begin();
wii5ModeLowBattery.begin();
wii5ModeManualTest.begin();
wii5ModePosition.begin();
wii5ModeSelfTest.begin();
mode = WII5MODE_BOOT; // Not using setMode here
currentSD = 0;
#ifdef WII5_DEBUG_SERIAL
SerialDebug.print(F("CONTROL:BOOTUP:freememory="));
SerialDebug.print(freeMemory());
SerialDebug.println();
#endif
// SD Card pins - should use Power object
pinMode(POWER_STORAGE_1_PIN, OUTPUT);
pinMode(STORAGE_SD1_MATHS_PIN, OUTPUT);
pinMode(STORAGE_SD2_MATHS_PIN, OUTPUT);
#ifdef WII5_WDT_INTERNAL
wdt_enable(WDTO_8S);
#endif
}
// Delay with SafeLoop
void WII5Controller::safeDelay(uint32_t waits) {
delayWait = 0;
while (delayWait < waits) {
loopWDT();
loopSafe();
}
}
void WII5Controller::setLastHello(WII5_PORTS p, WII5_DEVICES d) {
lastHelloReceived = now();
lastHelloElapsed = 0;
lastHelloPort = p;
lastHelloDevice = d;
}
bool WII5Controller::checkLastHello(WII5_DEVICES d, uint32_t msOld) {
// Expected device, and time is newer than we needed - TRUE!
if ((d == lastHelloDevice) && (lastHelloElapsed < msOld)) {
return true;
}
return false;
}
// Main Loop - broken up for easy reading
void WII5Controller::loop() {
loopWDT();
loopSafe();
// The *reasonably* accurate minute count
if (minuteWait > 60000) {
// TODO - use uptime += (minuteWait / 60000)
// minuteWait = (minuteWait % 60000);
// And log if it is over 5 minutes.
uptime++;
minuteWait = minuteWait - 60000;
}
// TODO timeout
switch (mode) {
case WII5MODE_BOOT:
// Safety: Automatically moves on
console.log(LOG_DEBUG, F("WII5: Boot up process complete"));
// Pretty safe to run
loopOther();
statusDump();
setMode(WII5MODE_START);
break;
case WII5MODE_START:
// Safety: Automatically moves on
loopOther();
setMode(calculateCurrentMode());
break;
case WII5MODE_DEMO:
// Safety: TODO This one never exits. Needs work.
// modeWait is screwed
loopOther();
if (modeWait > 60000) {
console.log(LOG_DEBUG, F("Demonstration mode, try T; for test"));
modeWait = 0;
}
if (modeWait > (WII5TIME_1DAY * 1000)) {
// Stuck here... now what. Move to sleep?
mode = WII5MODE_SLEEP; modeWait = 0;
}
break;
case WII5MODE_SLEEP:
// Safety: Sleep mode handles exit
wii5ModeSleep.loop();
// TODO Maybe?
loopOther();
break;
case WII5MODE_LOWBATTERY:
// Safety: Battery mode handles exit
wii5ModeLowBattery.loop();
loopOther();
break;
case WII5MODE_POSITION:
// Safety: Either temporary timed mode, or changed by sattelite comms
wii5ModePosition.loop();
loopOther();
break;
case WII5MODE_CAPTURE:
wii5ModeCapture.loop();
loopOther();
break;
case WII5MODE_MANUALTEST:
wii5ModeManualTest.loop();
if (modeWait > (WII5TIME_1DAY * 1000)) {
// Stuck here... now what. Move to sleep? Check saved mode is sleep or capture
console.log(LOG_FATAL, F("FATAL * FATAL * FATAL * reset is being called from manual test loop"));
console.flush();
sh3dNodeUtil.reset();
}
loopOther();
break;
case WII5MODE_SELFTEST:
wii5ModeSelfTest.loop();
// loopSafe plus some
loopOther();
if (modeWait > (WII5TIME_1DAY * 1000)) {
// Stuck here... now what. Move to sleep? Check saved mode is sleep or capture
console.log(LOG_FATAL, F("FATAL * FATAL * FATAL * reset is being called from self test loop"));
console.flush();
sh3dNodeUtil.reset();
}
break;
default:
console.log(LOG_FATAL, F("Invalide Mode, old=%d, new=%d"), mode, WII5MODE_CAPTURE);
mode = WII5MODE_CAPTURE;
}
}
// loopSafe - Minimal safe loop, to allow for callback from Iridium etc
void WII5Controller::loopSafe() {
// Buttons, LEDs etc
sh3dNodeIO.loop();
wii5Commands.processButtons();
// Serial port
console.loop();
wii5Commands.processConsoleAT();
wii5Commands.processConsoleStandard();
// Undesirable: Do not use these in loopSafe
// - Gps - lots of data
// - Sparton - lots of data
// - Iridium - lots of data and potential cost
// - Battery - Changes mode on us
wii5Iridium.loop();
wii5Communications.loop();
wii5Maths.loop();
}
void WII5Controller::loopWDT() {
#ifdef WDT_RESET
// TODO consider compile this out and have specific version
// TODO or don't call loopWDT inside TestMode and have own version
// TODO manual test mode timeout
// TODO Test the buoy - has it been more than 3 days since we sent a message - maybe reboot?
if (tempDisableWDT) return;
if (wdtWait > 5100) {
digitalWrite(WDT_RESET, LOW);
wdtWait = 0;
}
// TODO setting pin all the time for 100 ms
else if (wdtWait > 5000) {
digitalWrite(WDT_RESET, HIGH);
}
#endif
#ifdef WII5_WDT_INTERNAL
wdt_reset();
#endif
}
void WII5Controller::loopOther() {
#ifdef WII5_IMU_SPARTON
wii5Sparton.loop();
#endif
// LORA or Other Modems
#ifdef WII5_RADIO_LORA
wii5RadioLoRa.loop();
wii5Commands.processNetwork();
#endif
#ifdef WII5_GPS
wii5Gps.loop();
#endif
wii5Battery.loop();
wii5Weather_18B20.loop();
}
WII5_MODES WII5Controller::getMode() {
return mode;
}
bool WII5Controller::setMode(WII5_MODES newMode, bool nodelay) {
// Ignore same mode change - really this is an error
if (newMode == mode)
return false;
console.log(LOG_INFO, F("Controller: new mode set %d:%s"), newMode, wii5Strings.strMode(newMode));
#ifdef WII5_DEBUG_SERIAL
SerialDebug.print(F("CONTROL:MODE:mode="));
SerialDebug.print(newMode);
SerialDebug.print(F(":"));
SerialDebug.print(wii5Strings.strMode(newMode));
SerialDebug.println();
#endif
// TODO Delay ???
// - Put in a "when ready or timeout" to change mode
// Set the mode
mode = newMode;
modeWait = 0;
// Reset all STEPS to start
wii5ModeCapture.reset();
wii5ModeLowBattery.reset();
wii5ModePosition.reset();
wii5ModeManualTest.reset();
wii5ModeSelfTest.reset();
wii5ModeSleep.reset();
// Update status to tell users of mode change
statusDump();
return true;
}
// TODO move to SPI
void WII5Controller::disableChipSelect() {
#ifdef RADIO_CS
digitalWrite(RADIO_CS, HIGH);
#endif
#ifdef STORAGE_CS
digitalWrite(RADIO_CS, HIGH);
#endif
#ifdef SPIMEMORY_CS
digitalWrite(SPIMEMORY_CS, HIGH);
#endif
}
// TODO move to Status Dump
void WII5Controller::statusDump(bool human) {
// Basic status - mode, time, free memory
// TODO 2024 - Add run count - check this still works in node? (yes)
wii5Display.atCommandSend(F("status"), F("record=%lu,uptime=%lu,freeMemory=%d,run=%lu"),
wii5Config.getRecordCount(),
uptime, freeMemory(),
sh3dNodeConfig.getRunCount()
);
period = 0;
next = 0;
size = 0;
// CURRENT mode if we have one.
if (mode != wii5Config.getDefaultMode()) {
next = 0;
// Calculate next depending on noce
if (getMode() == WII5MODE_CAPTURE) {
period = wii5Config.getCapturePeriod();
size = wii5BinData.getSize(wii5Config.getCaptureBinaryType());
}
else if (getMode() == WII5MODE_POSITION) {
period = wii5Config.getPositionPeriod();
size = wii5BinData.getSize(wii5Config.getPositionBinaryType());
}
else if (getMode() == WII5MODE_SLEEP) {
period = wii5Config.getSleepPeriod();
size = wii5BinData.getSize(wii5Config.getSleepBinaryType());
}
next = wii5Display.minutesUntilNext(period);
// Only show current if not the same as default
wii5Display.atCommandSend(F("status"), F("modeType=current,mode=%s,modeTime=%lu,modePeriod=%lu,modeNext=%lu,binSize=%d,sd=%d"),
wii5Strings.strMode(mode),
(uint32_t)(modeWait / 60000), // Convert to Minutes
period, next, size, currentSD
);
}
// DEFAULT Mode
next = 0;
if (wii5Config.getDefaultMode() == WII5MODE_CAPTURE) {
period = wii5Config.getCapturePeriod();
size = wii5BinData.getSize(wii5Config.getCaptureBinaryType());
}
else if (wii5Config.getDefaultMode() == WII5MODE_POSITION) {
period = wii5Config.getPositionPeriod();
size = wii5BinData.getSize(wii5Config.getPositionBinaryType());
}
else if (wii5Config.getDefaultMode() == WII5MODE_SLEEP) {
period = wii5Config.getSleepPeriod();
size = wii5BinData.getSize(wii5Config.getSleepBinaryType());
}
next = wii5Display.minutesUntilNext(period);
wii5Display.atCommandSend(F("status"), F("modeType=default,mode=%s,modeTime=%lu,modePeriod=%lu,modeNext=%lu,binSize=%d,sd=%d"),
wii5Strings.strMode(wii5Config.getDefaultMode()),
(uint32_t)(modeWait / 60000), // Convert to Minutes
period, next, size, currentSD
);
wii5Display.atCommandSend(F("status"), F("version=%S,imei=%s"), F(WII5_SOFTWARE_VERSION), wii5Iridium.imei);
wii5Display.atCommandSend(F("status"), F("voltage=%ld,temperature=%ld,lat=%ld,lon=%ld,alt=%ld"),
wii5Battery.value, wii5Weather_18B20.value,
long(wii5Gps.gps->location.lat() * GPS_POS_MULT),
long(wii5Gps.gps->location.lng() * GPS_POS_MULT),
long(wii5Gps.gps->altitude.meters() * GPS_ALT_MULT)
);
// Calculate Maths mode.
wii5Display.atCommandSend(F("status"), F("maths_on=%d,maths_held_remaining=%lu,maths_power_remaining=%lu"),
wii5Maths.isRunning(), wii5Maths.remainingHold() / 60, wii5Maths.remainingUntil() / 60
);
// Tell the Maths CPU to advertise this info
wii5Display.atCommandSend(F("status"), F("update"));
if (human) {
console.log(LOG_INFO, F("# WII5: Waves In Ice 5 - 2019. Board:%S Version:%S ID=%lu"),
F(WII5_BOARD_NAME),
F(WII5_SOFTWARE_VERSION),
wii5Config.getDeviceId()
);
}
}
WII5_MODES WII5Controller::calculateDefaultMode() {
return wii5Config.getDefaultMode();
}
// TODO move to Status Dump
WII5_MODES WII5Controller::calculateCurrentMode() {
// TODO Urgent - temporary Modes
// TODO Urgent - find everywhere setDefaultMode is called, probably should be setCurrentMode
// TODO check we have valid date/time
// TODO check temporaryMode
// TODO check defaultMode
// return WII5MODE_CAPTURE;
// return WII5MODE_DEMO;
return wii5Config.getDefaultMode();
}
// What was the last default mode - for reboot and post sleep
// TODO - Proabably move to sh3dNodeConfig (EEPROM) data
void WII5Controller::setDefaultMode() {
// Get default mode from EEPROM and disable the Temporary Mode there
setMode(calculateDefaultMode());
}
void WII5Controller::setCurrentMode() {
// Get temporary mode from EEPROM, check it is valid, else fall back to Default
setMode(calculateCurrentMode());
}
void WII5Controller::shared5On() {
// Turn it on - and feel free to
digitalWrite(SHARED_5VOLT_PIN, HIGH);
}
void WII5Controller::shared5Off() {
// Turn 5 Volts off - ONLY if safe to do so
if (wii5Maths.isRunning() || wii5Iridium.isRunning() || wii5Sparton.isRunning() || (sh3dNodeIO.led2Get() != LED_OFF)) {
// Keep it on !
digitalWrite(SHARED_5VOLT_PIN, HIGH);
}
else {
// Yep, power off time
digitalWrite(SHARED_5VOLT_PIN, LOW);
}
}
uint8_t WII5Controller::getSD() {
return currentSD;
}
void WII5Controller::setSDOff() {
sdBlock.cardForceClose();
digitalWrite(POWER_STORAGE_1_PIN, LOW);
digitalWrite(STORAGE_SD1_MATHS_PIN, HIGH);
digitalWrite(STORAGE_SD2_MATHS_PIN, LOW);
currentSD = 0;
}
// TODO 2024 - delay - really ! Probably ok for now - consider safeDelay
void WII5Controller::setSD1() {
digitalWrite(POWER_STORAGE_1_PIN, LOW);
delay(500);
digitalWrite(STORAGE_SD1_MATHS_PIN, HIGH);
digitalWrite(STORAGE_SD2_MATHS_PIN, LOW);
digitalWrite(STORAGE_CS, HIGH);
delay(500);
digitalWrite(POWER_STORAGE_1_PIN, HIGH);
delay(500);
currentSD = 1;
}
// TODO 2024 - delay - really ! Probably ok for now - consider safeDelay
void WII5Controller::setSD2() {
digitalWrite(POWER_STORAGE_1_PIN, LOW);
delay(500);
digitalWrite(STORAGE_SD1_MATHS_PIN, LOW);
digitalWrite(STORAGE_SD2_MATHS_PIN, HIGH);
digitalWrite(STORAGE_CS, HIGH);
delay(500);
digitalWrite(POWER_STORAGE_1_PIN, HIGH);
delay(500);
currentSD = 2;
}
WII5Controller wii5Controller;