Files
WII5Firmware/WII5ModeCapture.cpp
T
scottp 295abb37ee 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.
2026-05-07 16:27:18 +10:00

565 lines
18 KiB
C++

// 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 WII5ModeCapture.cpp
* @brief Capture mode: IMU/wave-motion data capture and SD logging.
*/
/*
MDOECapture - the main event
* Watch for our perfect time to start
* Prepare SD card, Sparton etc for running
* Trigger start / stop sparton
* Store data to disk
*/
#include <Arduino.h>
#include <TimeLib.h>
#include <WII5.h>
void WII5ModeCapture::reset() {
step = WII5CAPTURE_OFF; wait = 0; stepCount = 0;
stepLast = WII5CAPTURE_FINISH;
captureType = WII5CAPTURETYPE_UNKNOWN;
}
void WII5ModeCapture::begin() {
debug = true;
step = WII5CAPTURE_OFF; wait = 0; stepCount = 0;
captureType = WII5CAPTURETYPE_UNKNOWN;
enableAtFile = false;
enableGPS = true;
enableMaths = true;
enableComms = true;
enableLiveSwap = true; // Playing with this being safe for 2024
}
void WII5ModeCapture::start(bool force) {
// If OFF or WAITING for OFF - Skipt
if ( (step == WII5CAPTURE_OFF) || (step == WII5CAPTURE_WAIT5_OFF) || force ) {
console.log(LOG_INFO, F("Capture: MANUAL START - Now"));
captureType = WII5CAPTURETYPE_MANUAL;
captureStart = now();
step = WII5CAPTURE_START; wait = 0; retry = 0; stepCount = 0;
}
else {
// Switch to CANCEL Capture with restart now
console.log(LOG_INFO, F("Capture: MANUAL START - Cancelling existing and restarting"));
captureType = WII5CAPTURETYPE_MANUAL;
captureStart = now();
step = WII5CAPTURE_CANCEL_RESTART; wait = 0; retry = 0; stepCount = 0;
}
}
void WII5ModeCapture::stop(bool force) {
if ((step != WII5CAPTURE_OFF) || force) {
// Switch to CANCEL Capture with OFF (aka Wait)
step = WII5CAPTURE_CANCEL_OFF; wait = 0; retry = 0; stepCount = 0;
}
else {
// Manually just double check everything is off. (done above in the CANCEL_OFF stage)
if (enableGPS)
wii5Gps.off();
wii5Sparton.stop();
}
captureType = WII5CAPTURETYPE_UNKNOWN;
wait = 0; retry = 0; stepCount = 0;
}
elapsedMillis test1;
void WII5ModeCapture::loop() {
// Calculate seconds until next capture has to be started
bool first = (stepLast != step);
stepLast = step;
switch (step) {
case WII5CAPTURE_OFF:
// Total minutes since midnight
minutes = (hour() * 60) + minute();
// Automatically set to a max of 12 hours
period = wii5Config.getCapturePeriod();
remaining = period - (minutes % period);
if (first) {
console.log(LOG_INFO, F("Capture: OFF, waiting"));
sh3dNodeIO.led1Set( LED_SLOW );
}
// else if ((wii5Gps.sinceOnLast > (WII5TIME_1HOUR * 1000)) && (!wii5Gps.isTimeValid()))
// TODO 2024 Changed from 1 hour to 5 minutes - This should change to do a full auto every run
// Note: that & isTimeValid would often be true...
else if (wii5Gps.sinceOnLast > 300000) {
console.log(LOG_INFO, F("Capture: Internal clock / time is not valid, turn on GPS"));
step = WII5CAPTURE_TIME; wait = 0;
}
// NOTE: we have only 60 seconds
else if ( (minutes % period) == 0) {
console.log(LOG_INFO, F("Capture: Starting minutes match period min=%lu, period=%lu"), minutes, wii5Config.getCapturePeriod());
// TODO atData !!!
step = WII5CAPTURE_START; wait = 0; retry = 0; stepCount = 0;
captureType = WII5CAPTURETYPE_TIME;
captureStart = now();
}
// 20 or more mimutes
/*
else if (remaining > 30) {
// Hmmm... lets shut down everything, get the power lower
// TODO check Maths and Communications is off.
if (!wii5Communications.isRunning())
wii5Communications.stop();
if (wii5Controller.getSD() != 0)
wii5Controller.setSDOff();
}
*/
// 3 or more minutes
else if (remaining > 3) {
// Run temperature if more than 5 minutes old
if (wii5Weather_18B20.age > 300000) {
wii5Weather_18B20.temperatureRead();
wii5Battery.start();
}
}
// TODO
// - Iridium: loop can do its own start/stop processing data
// - Weather & Battery already do their own
// -
// TODO
// If WAIT is > X minutes, sleep, or just low power mode?
// GPS off, Iridium Off, Maths off, Sparton off, SD off
// Power Consumption = X
// AVR off as well
// Power Consumption = X
break;
case WII5CAPTURE_START:
// Output Manual vs Time start
switch(captureType) {
case WII5CAPTURETYPE_MANUAL:
wii5Display.statusSend(F("CaptureMode"), F("start,manual"));
wii5Display.atDataSend(
F("CaptureCommand"),
F("START,Manual")
);
break;
case WII5CAPTURETYPE_TIME:
wii5Display.statusSend(F("CaptureMode"), F("start,timeperiod"));
wii5Display.atDataSend(
F("CaptureCommand"),
F("START,Time,period=%lu,minutes=%lu"),
wii5Config.getCapturePeriod(),
minutes
);
break;
};
startTime = now();
step = WII5CAPTURE_PREPARE; wait = 0; retry = 0; stepCount = 0;
break;
case WII5CAPTURE_TIME:
if (first) {
// wii5Gps.autoTime();
wii5Gps.autoAccurate(); // TODO 2024 - Moved to full auto accurate instead
}
else if (wii5Gps.ready()) {
step = WII5CAPTURE_OFF; wait = 0;
}
else if (wait > 300000) {
step = WII5CAPTURE_OFF; wait = 0;
}
break;
case WII5CAPTURE_PREPARE:
// Experiment - start communications DURING a run
wii5Communications.setAutoMode();
// Some emergency off, just in case - also handy as it does a reboot
wii5Sparton.stop();
wii5Sparton.off();
step = WII5CAPTURE_PREPARE_ID; wait = 0; retry = 0; stepCount = 0;
break;
case WII5CAPTURE_PREPARE_ID:
wii5Config.updateRecordCount();
step = WII5CAPTURE_PREPARE_SD; wait = 0; retry = 0; stepCount = 0;
break;
case WII5CAPTURE_PREPARE_SD:
// TODO - consider power cylce and delays, and consequence on Maths
// TODO - Maybe only if card not open?
if (wii5Controller.getSD() == 1) {
wii5Controller.setSD1();
}
else {
wii5Controller.setSD2();
}
// TODO retry X times with power retry every second?
// TODO 2024 ! ZOMG ! safeDelay ? or not care? (was 1500)
delay(1000); // TODO Yes I know !
// TODO - If card not open?
if (sdBlock.cardOpen(wii5Controller.getSD())) {
wii5Display.statusSend(F("CaptureMode"), F("sdcard,current=%d"), wii5Controller.getSD());
wii5Display.atDataSend(
F("CaptureCommand"),
F("SDCard,current,%d"),
wii5Controller.getSD()
);
// open card - else - bugger !
if (sdBlock.dataOpen(wii5Config.getRecordCount())) {
wii5Display.atDataSend(
F("CaptureCommand"),
F("SDCard,current,%d,recordCount=%lu"),
wii5Controller.getSD(), wii5Config.getRecordCount()
);
#ifdef WII5_GPS
if (enableGPS) {
wii5Gps.autoAccurate();
}
#endif
step = WII5CAPTURE_PREPARE_COMMS; wait = 0; retry = 0; stepCount = 0;
}
else {
sh3dNodeIO.led1Set( LED_FAST );
console.log(LOG_FATAL, F("CAPTURE: Failed dataOpen cardNum=%d"), wii5Controller.getSD());
snprintf_P(
wii5Commands.lastCommandMessage, sizeof(wii5Commands.lastCommandMessage),
PSTR("@Err,sd,dataopen,%d"),
wii5Controller.getSD()
);
wii5Commands.lastCommandCmd = 0;
wii5Communications.sendError();
// Flip SD for next time
if (wii5Controller.getSD() == 1)
wii5Controller.setSD2();
else
wii5Controller.setSD1();
console.log(LOG_FATAL, F("CAPTURE: Cards swapped cardNum=%d"), wii5Controller.getSD());
step = WII5CAPTURE_WAIT5_OFF; wait = 0; retry = 0; stepCount = 0;
}
}
else {
sh3dNodeIO.led1Set( LED_FAST );
console.log(LOG_FATAL, F("CAPTURE: Failed cardOpen cardNum=%d"), wii5Controller.getSD());
snprintf_P(
wii5Commands.lastCommandMessage, sizeof(wii5Commands.lastCommandMessage),
PSTR("@Err,sd,cardopen,%d"),
wii5Controller.getSD()
);
wii5Commands.lastCommandCmd = 0;
wii5Communications.sendError();
// Flip SD for next time
if (wii5Controller.getSD() == 1)
wii5Controller.setSD2();
else
wii5Controller.setSD1();
console.log(LOG_FATAL, F("CAPTURE: Cards swapped cardNum=%d"), wii5Controller.getSD());
step = WII5CAPTURE_WAIT5_OFF; wait = 0; retry = 0; stepCount = 0;
}
break;
case WII5CAPTURE_PREPARE_COMMS:
// For now - skipping comms - leaving until the end
step = WII5CAPTURE_PREPARE_SPARTON; wait = 0; retry = 0; stepCount = 0;
break;
// Prepare the sparton to go
case WII5CAPTURE_PREPARE_SPARTON:
#ifdef WII5_IMU_SPARTON
wii5Display.statusSend(F("CaptureMode"), F("sparton,start"));
// TODO Configuration
wii5Sparton.setBinary(1);
wii5Sparton.setHz(8);
wii5Sparton.setRecords(wii5Config.getCaptureRecords());
wii5Sparton.setCapture(1);
// Start
// TODO 2024 - Do we need to wait? - or just start? When is commands sent to Sparton?
wii5Sparton.start();
#endif
step = WII5CAPTURE_INPROGRESS; wait = 0; retry = 0; stepCount = 0;
break;
// Capture is running - report progress? (might be done in IMU)
case WII5CAPTURE_INPROGRESS:
if (first) {
sh3dNodeIO.led1Set( LED_TRIPPLE );
}
else if (wii5Sparton.captureFinished()) {
step = WII5CAPTURE_SHUTDOWN; stepCount = 0; wait = 0;
}
// 30 minutes
else if (wait > 1800000) {
console.log(LOG_FATAL, F("Capture: Failed - timeout after 30 minutes"));
// TODO 2024 - sendError?
sh3dNodeIO.led1Set( LED_FAST );
step = WII5CAPTURE_CANCEL_OFF; wait = 0; retry = 0; stepCount = 0;
}
break;
// Shutdown the capture devices, IMU etc (should be internal IMIU)
case WII5CAPTURE_SHUTDOWN:
if (stepCount == 1) {
// Shutdown sparton?
}
step = WII5CAPTURE_SEND_PREPARE; stepCount = 0; wait = 0;
break;
case WII5CAPTURE_SEND_PREPARE:
step = WII5CAPTURE_PROCESS; stepCount = 0; wait = 0;
break;
case WII5CAPTURE_PROCESS:
// TODO This here should be switching SD Cards if possible
// - Stop SD
// - Switch SD
// - Start Maths
// - This allows code to be processed right away.
// TODO - if Maths is off
// TODO - Switch SD Cards
// TODO - store new one in wii5Controller.getSD() for next time - ie. shoulduse that even if hours away
step = WII5CAPTURE_FINISH; stepCount = 0; wait = 0;
break;
// Three ways to cancel
case WII5CAPTURE_FLIP:
case WII5CAPTURE_CANCEL_RESTART:
case WII5CAPTURE_CANCEL_OFF:
case WII5CAPTURE_MANUAL_OFF:
if (first) {
console.log(LOG_FATAL, F("Capture: Manual Cancel - Turning off the devices."));
#ifdef WII5_GPS
if (enableGPS)
wii5Gps.off();
#endif
wii5Sparton.stop();
if (step == WII5CAPTURE_FLIP) {
console.log(LOG_INFO, F("CaptureMode: FLIP manually requested on SD Cards"));
sdBlock.cardForceClose();
wii5Controller.setSDOff();
delay(500); // TODO Yes I know.... gah. TODO 2024 safeDelay - really 3 seconds TODO 2024 was 3000
if (wii5Controller.getSD() == 1) {
wii5Controller.setSD2();
wii5Display.statusSend(F("CaptureMode"), F("sdcard,swap,2"));
wii5Display.atDataSend(
F("MathsProcess"),
F("SD=2")
);
}
else {
wii5Controller.setSD1();
wii5Display.statusSend(F("CaptureMode"), F("sdcard,swap,1"));
wii5Display.atDataSend(
F("MathsProcess"),
F("SD=1")
);
}
}
}
// Maths ?
if (wait > 2000) {
if (step == WII5CAPTURE_CANCEL_RESTART) {
step = WII5CAPTURE_START; wait = 0; retry = 0; stepCount = 0;
}
else {
step = WII5CAPTURE_WAIT5_OFF; wait = 0; retry = 0; stepCount = 0;
}
}
break;
case WII5CAPTURE_WAIT5_OFF:
if (stepCount == 1)
console.log(LOG_DEBUG, F("ModeCapture: Waiting for 90 seconds after fatal error before retrying"));
if (wait > 90000) {
step = WII5CAPTURE_OFF; stepCount = 0; wait = 0;
}
break;
// Capture is finished - what do we do?
// - Need to Sleep. e.g. more than 1 hour to wait.
// - Twiddlge thumbs waiting for Maths CPU to process and send Iridium
// . Reduce power of all other devices
// . Monitor Maths - don't let it run forever
// . Monitor Iridium - don't let it run forever
case WII5CAPTURE_FINISH:
wii5Display.statusSend(F("CaptureMode"), F("sparton,end"));
// Just sit here
wii5Display.atDataSend(
F("CaptureCommand"),
F("END")
);
// Close DATA
sdBlock.dataClose(false);
// Create empty single entry for results.
sdBlock.dataOpen(wii5Config.getRecordCount());
sdBlock.dataPrepare();
sdBlock.dataWrite();
sdBlock.dataClose(true);
// Trigger temperatureRead - safe place at ene of a read
wii5Weather_18B20.temperatureRead();
#ifdef WII5_GPS
if (wii5Gps.isError()) {
// TODO report errors
console.log(LOG_DEBUG, F("GPS: failure I think"));
}
else if (wii5Gps.ready()) {
console.log(LOG_DEBUG, F("GPS: Success I think"));
}
else {
console.log(LOG_DEBUG, F("GPS: UNKNOWN I think"));
}
wii5Gps.off();
#endif
// TODO Check for errors
sdBlock.metadataPrepare();
if (sizeof(WII5MetaDataObject) <= sizeof(sdBlock.metadata->data)) {
console.log(LOG_DEBUG, F("Writing SDBlock Metadata - WII5 Size=%d, SDBlock Size=%d"),
int(sizeof(WII5MetaDataObject)),
int(sizeof(sdBlock.metadata->data))
);
wii5Display.updateMetadata(sdBlock.metadata->data);
}
else {
console.log(LOG_FATAL, F("SDBlock and MetaData are not compatible sizes - WII5 Size=%d, SDBlock Size=%d"),
int(sizeof(WII5MetaDataObject)),
int(sizeof(sdBlock.metadata->data))
);
}
sdBlock.metadataWrite();
console.log(LOG_DEBUG, F("SDBlock metadata write = %lu"), sdBlock.metadataLast);
// Always close card
sdBlock.cardClose();
if (debug) {
delay(1000);
sdBlock.cardOpen(wii5Controller.getSD());
sdBlock.metadataRead(0);
uint32_t dataBlockStart = sdBlock.metadata->dataBlockStart;
uint32_t dataBlocks = sdBlock.metadata->dataBlocks;
uint32_t resultsBlockStart = sdBlock.metadata->resultsBlockStart;
uint32_t resultsBlocks = sdBlock.metadata->resultsBlocks;
console.printf(F("@Block,metadata,address=%lu,deviceId=%lu,recordId=%lu\r\n"),
sdBlock.metadataLast, sdBlock.metadata->deviceId, sdBlock.metadata->recordId
);
console.printf(F("@Block,metadata,dataBlockStart=%lu,dataBlocks=%lu\r\n"),
dataBlockStart, dataBlocks
);
console.printf(F("@Block,metadata,resultsBlockStart=%lu,resultsBlocks=%lu\r\n"),
resultsBlockStart, resultsBlocks
);
}
// SAFETY
// - Check maths is off, others leave this one live
if (wii5Maths.isOff() || enableLiveSwap) {
// TODO need more delays...
// TODO - consider printing that we did Live Swap
if (wii5Controller.getSD() == 1) {
wii5Controller.setSD2();
wii5Display.statusSend(F("CaptureMode"), F("sdcard,swap,2"));
wii5Display.atDataSend(
F("MathsProcess"),
F("SD=2")
);
}
else {
wii5Controller.setSD1();
wii5Display.statusSend(F("CaptureMode"), F("sdcard,swap,1"));
wii5Display.atDataSend(
F("MathsProcess"),
F("SD=1")
);
}
if (!sdBlock.cardOpen(wii5Controller.getSD())) {
sh3dNodeIO.led1Set( LED_FAST );
console.log(LOG_ERROR, F("Error opening SD Card"));
}
// Signal we have swapped SD
wii5Communications.signalSD();
}
else {
wii5Display.statusSend(F("CaptureMode"), F("sdcard,noswap"));
wii5Display.atDataSend(
F("MathsProcess"),
F("noswap")
);
}
// TODO Calculate waiting time.
// If Maths finished, and Iridium finished.
// And we have to wait longer than X (30 minutes?)
// Deep Sleep for 90% of that time.
if(captureType == WII5CAPTURETYPE_MANUAL) {
console.log(LOG_INFO, F("Manual run executed, switching back to Default Mode"));
wii5Controller.setDefaultMode();
}
if (enableMaths)
wii5Maths.start(WII5MATHSMODE_AUTO);
wii5Communications.start(); // In case we didn't get SD, still tell it to do the job
step = WII5CAPTURE_WAIT_COMMS_MATHS; wait = 0; retry = 0; stepCount = 0;
break;
case WII5CAPTURE_WAIT_COMMS_MATHS:
if (first) {
console.log(LOG_DEBUG, F("Waiting for Maths and Communicatinos to Finish"));
}
// If maths and Comms is off, jumpt to OFF
else if ( !wii5Communications.isRunning() && wii5Maths.isOff()) {
console.log(LOG_INFO, F("CAPTURE: Maths and Comms finsihed"));
step = WII5CAPTURE_OFF; wait = 0; retry = 0; stepCount = 0;
}
// Wait 3 minuts here quietly
else if (wait > 180000) {
console.log(LOG_INFO, F("CAPTURE: 3 minutes and Maths and Communications not finished. Mvoing on but leaving them on."));
step = WII5CAPTURE_OFF; wait = 0; retry = 0; stepCount = 0;
}
break;
default:
console.log(LOG_FATAL, F("CAPTURE: Default step... step=%d"), step);
step = WII5CAPTURE_OFF; stepCount = 0; wait = 0;
break;
}
stepCount++;
stepTotal++;
}
void WII5ModeCapture::flip() {
step = WII5CAPTURE_FLIP; wait = 0; stepCount = 0;
}
WII5ModeCapture wii5ModeCapture;