Files
WII5Firmware/WII5Communications.cpp
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

658 lines
24 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 WII5Communications.cpp
* @brief High-level communications dispatcher: orchestrates Iridium send/receive flows.
*/
/*
TODO 2024 - This seems a little over complicated, any simplification possible
* List of results to send with current SD card???
* Status - what to send what not to?
TODO: Full testing, especially:
. Receiving multiple messages when only sending one
. New sdBlock code for loading and finding records
Communications - Really in control of Iridium
However, it may become more, startup of Weather and Voltage.
Possibly even read/write SD cared for data
When is a message sent:
* By request - e.g. sendBinModeData
* Automatically - e.g. looking for unset results
* By time - if start is called and we get to JUMP, and it has been more than 12 hours.
(this last one covers the case where something has gotten stuck - e.g. Sparton Failure and no results to post)
Template in another Mode / State machine
Simple Mode - Just send messages requested, no looking at SD
Start sending a message (don't call again until stopped)
wii5Communications.setSimpleMode();
wii5Communications.sendBinModeType(1, 0);
wii5Communications.start();
while (!wii5Communications.isRunning()) {
Wait...
Complex Mode:
Start the background processing
wii5Communications.setAutoMode();
Be very careful of SD changes, power on/off/swap
Ideally, once you start - don't turn it off.
However, this may not always be practical.
Send signalSD when an SD Card is changed.
*/
#include <Arduino.h>
#include <TimeLib.h>
#include <WII5.h>
void WII5Communications::begin() {
step = WII5COMMUNICATIONS_IDLE;
lastStep = WII5COMMUNICATIONS_END;
wait = 0;
autoMode = false;
metadataScan = false;
testMode = false;
mainBinType = 0;
metadataBlock = 0;
resultsBlock = 0;
mainRecordCount = 0;
// Clear the request
requestBinType = 0;
requestMetadataBlock = 0;
requestResultsBlock = 0;
requestRecordCount = 0;
updateStatus = false;
countSucces = 0;
countFailed = 0;
// Find Request - these should happen before other requests or auto searches
findBinType = 0;
findRecordCount = 0; // When it finds this record it will select this address and the correct Results record
// TODO 2024 - Disabled - this is unsafe. Can we make it so there is a maimum - look at other loops
disabled = false;
}
// NOTE: Call start to clear
void WII5Communications::setTESTING() { testMode = true; }
// TODO 2024 - New code to disable - must timeout !!!
bool WII5Communications::setDisabled(bool in) {
disabled = in;
if (disabled) {
step = WII5COMMUNICATIONS_DISABLED;
console.log(LOG_INFO, F("Communications: disabled on (do nothing)"));
}
else {
step = WII5COMMUNICATIONS_IDLE;
console.log(LOG_INFO, F("Communications: disabled off (move to idle)"));
}
wait = 0;
return disabled;
}
void WII5Communications::setAutoMode() { autoMode = true; }
void WII5Communications::setSimpleMode() { autoMode = false; }
bool WII5Communications::getAutoMode() { return autoMode; }
bool WII5Communications::getSimpleMode() { return !autoMode; }
void WII5Communications::signalSD() {
// Reset that we definitely want a metadata scan
metadataScan = true;
// disable existing scans - set them to 0 - as this is a new SD
sdCardId = 0;
sdStartBlock = 0;
sdCurrentBlock = 0;
// Check the mode, and put it in appropriate place.
start();
}
void WII5Communications::findRecord(uint32_t t, uint32_t rc) {
// Set a type and Record Count to find, it will override the previous
findRecordCount = rc;
findBinType = t;
}
void WII5Communications::sendText(void *buf = NULL, uint16_t bufSize = 0) {
if (buf && bufSize > 0) {
strncpy(wii5Commands.lastCommandMessage, buf, bufSize); // < 235 ? bufSize, 235);
}
uint32_t replyType = wii5BinData.setBit(0, 4);
sendBinModeType(replyType, 0, 0, 0);
setSimpleMode();
console.log(LOG_INFO, F("Communications: sendText() message=%s"), wii5Commands.lastCommandMessage);
start();
}
void WII5Communications::sendError() {
uint32_t replyType = wii5BinData.setBit(0, 3);
wii5Commands.lastCommandResult = WII5RESULTS_ERROR;
sendBinModeType(replyType, 0, 0, 0);
setSimpleMode();
console.log(LOG_INFO, F("Communications: sendError() message=%s"), wii5Commands.lastCommandMessage);
start();
}
void WII5Communications::sendBinModeType(uint32_t t, uint32_t recordCount, uint32_t mdBlock, uint32_t resultsBlock) {
updateStatus = false;
requestWait = 0;
requestBinType = t;
requestRecordCount = recordCount; // Note - this does not look up the count - uses it to check Block
// Leave this at 0 to load currecnt record count
requestMetadataBlock = mdBlock; // Leave this at zero to not load a Block
requestResultsBlock = resultsBlock; // Leave this at zero to not load a Block
}
/*
How to use this library.
setType - The BinData type to be sent
setResultsRecord - Pick and SD Card and record to read.
- Or could have a setResultsBuf
- Not sure how to choose SD card - as it may be locked by maths etc
- Not sure how to keep copy of data - 512 bytes too much
. readRecord, just before BinData.set ?
- How to deal with queues and wrong cards.
Advanced use
Instead of just a type with record etc, this library should handle
things like sending raw data.
Simple Mode
- Like used in Position. Just send message requested.
Queue Mode
- Find missing entries, not yet sent
*/
void WII5Communications::start() {
// Note: Test mode is turned to false at each start. So to enable test mode, start then setTESTING
testMode = false;
sdDone = 0;
sdTried = 0;
if (step == WII5COMMUNICATIONS_IDLE) {
step = WII5COMMUNICATIONS_PREPARE; wait = 0;
}
}
void WII5Communications::stop() {
// TODO I think we should consider a shutdown, or just use first?
if (step != WII5COMMUNICATIONS_IDLE) {
step = WII5COMMUNICATIONS_END; wait = 0;
}
}
bool WII5Communications::isRunning() {
return (step != WII5COMMUNICATIONS_IDLE);
}
void WII5Communications::loop() {
bool first = (lastStep != step);
lastStep = step;
switch (step) {
case WII5COMMUNICATIONS_DISABLED:
if (first) {
console.log(LOG_DEBUG, F("Communications: step disabled"));
}
break;
case WII5COMMUNICATIONS_IDLE:
if (first) {
if (disabled) {
step = WII5COMMUNICATIONS_DISABLED; wait = 0;
}
console.log(LOG_DEBUG, F("Communications: step idle - manual waiting for start"));
wii5Iridium.stop();
countSend = 0; // We use this to count how many in this run
}
// More than 14 hours since last messsage - send out a ping
else if (lastSuccess > ((uint32_t)14*3600000)) {
console.log(LOG_DEBUG, F("Communications: Not sent a successful message in 14 hours - send pinkg"));
sendBinModeType(1,1,0,0);
// TODO Custom text for the error/reason
lastSuccess = 0; // To stop repeating every loop
}
break;
case WII5COMMUNICATIONS_PREPARE:
step = WII5COMMUNICATIONS_IRIDIUM_START; wait = 0;
break;
case WII5COMMUNICATIONS_IRIDIUM_START:
if (first) {
console.log(LOG_DEBUG, F("Communications: step IRIDIUM_START (firmware, imei, signalQuality)"));
if (testMode) {
console.log(LOG_FATAL, F("FAKE: skipping iridium request Firmware"));
step = WII5COMMUNICATIONS_JUMP; wait = 0;
}
else {
wii5Iridium.requestFirmware();
}
binNextBit = 0; // Important to start split
binCount = 0;
}
// Waiting - we are ready to send the text
else if (wii5Iridium.waiting()) {
step = WII5COMMUNICATIONS_JUMP; wait = 0;
}
// TODO Hard coded
else if (wait > 600000) {
console.log(LOG_ERROR, F("Communications: Iridium Timed out after 600 seconds"));
step = WII5COMMUNICATIONS_JUMP; wait = 0;
}
break;
// Decide what to do
case WII5COMMUNICATIONS_JUMP:
if (first) {
}
// Already got one moving on
else if (mainBinType > 0) {
console.log(LOG_DEBUG, F("Communications: step JUMP - existing message in mainBinType"));
step = WII5COMMUNICATIONS_IRIDIUM_SEND; wait = 0;
}
// New request - set it and move on
else if (requestBinType > 0) {
console.log(LOG_DEBUG, F("Communications: step JUMP - new message in requestBiType"));
// One in the background - must clearit, so not used again
mainBinType = requestBinType;
metadataBlock = requestMetadataBlock;
resultsBlock = requestResultsBlock;
mainRecordCount = requestRecordCount;
countSucces = 0;
countFailed = 0;
// Clear the request
requestBinType = 0;
requestMetadataBlock = 0;
requestResultsBlock = 0;
requestRecordCount = 0;
step = WII5COMMUNICATIONS_IRIDIUM_SEND; wait = 0;
}
else if (autoMode) {
// TODO - autoMode - decide what to do
// Only when we have nothing else to do
step = WII5COMMUNICATIONS_SD_NEXT; wait = 0;
}
// else if (Old!) {
// TODO Send a minimum packet
// }
else {
step = WII5COMMUNICATIONS_END; wait = 0;
}
// TODO Decide on loading data from SD
// 1. requestBlock or similar manuarl request (Which SD Card?)
// 2. Auto Mode - find a record not sent and send it
break;
// SD Next - find a record
case WII5COMMUNICATIONS_SD_NEXT:
// Find out the SD Card and Block we started at - if either change, start again
// TODO - consider openging the SD Card ourselves
// No card is open.... quit
if (!sdBlock.cardIsOpen()) {
// TODO - consider opening one? - Nah - Capture mode only
console.log(LOG_DEBUG, F("Communications: card is not open - finishing"));
sdCardId = 0;
sdStartBlock = 0;
sdCurrentBlock = 0;
step = WII5COMMUNICATIONS_END; wait = 0;
}
// Nothing processed - lets get that data from the SD card
else if ( (sdCardId == 0) || (sdStartBlock == 0) ) {
console.log(LOG_DEBUG, F("Communications: new card... getting locations"));
sdCardId = sdBlock.getCardId();
sdStartBlock = sdBlock.getMetadataNext(); // 1 record past where to read
sdCurrentBlock = sdBlock.getMetadataNext(); // Current record read, starts past because never reads. Sub one first
}
// Card has changed, blank these and move on
else if ( (sdBlock.getCardId() != sdCardId) || (sdBlock.getMetadataNext() != sdStartBlock)) {
console.log(LOG_DEBUG, F("Communications: card changed ! - trying to read new card"));
console.log(LOG_DEBUG, F("Communications: OldSD=%d, NewCard=%d"), int(sdCardId), int(sdBlock.getCardId()));
console.log(LOG_DEBUG, F("Communications: OldBlock=%lu, NewBlock=%lu"), sdCurrentBlock, sdBlock.getMetadataNext());
sdCardId = 0;
sdStartBlock = 0;
sdCurrentBlock = 0;
}
// Is the next block we have to read before the start?
else if ((sdCurrentBlock - 1) < sdBlock.getMetadataFirst()) {
console.log(LOG_DEBUG, F("Communications: Read all metadata records to start"));
// Finsihed readin as far as we can.
sdCardId = 0;
sdStartBlock = 0;
sdCurrentBlock = 0;
step = WII5COMMUNICATIONS_END; wait = 0;
}
// TODO - check if we have gone back far enough - e.g. 100 records.
// TODO - FIND ! This is where we can do the find.
// Do 100 max tests
else if (sdTried > 100) {
console.log(LOG_DEBUG, F("Communications: Tried 100 records, Finishing"));
// Finsihed readin as far as we can.
sdCardId = 0;
sdStartBlock = 0;
sdCurrentBlock = 0;
step = WII5COMMUNICATIONS_END; wait = 0;
}
// Do 10 transmitts
else if (sdDone > 10) {
console.log(LOG_DEBUG, F("Communications: Done 10 transmitts. Finsihing"));
// Finsihed readin as far as we can.
sdCardId = 0;
sdStartBlock = 0;
sdCurrentBlock = 0;
step = WII5COMMUNICATIONS_END; wait = 0;
}
// Read record and use now - cos might loose it
else {
sdTried++;
sdCurrentBlock--;
console.log(LOG_DEBUG, F("Communications: reading block: %lu"), sdCurrentBlock);
if (!sdBlock.read(sdCurrentBlock)) {
console.log(LOG_FATAL, F("Communications: Error reading this block. Skipping to next"));
// TODO what next afer error?
}
else {
WII5MetaDataObject* local_metadata = (WII5MetaDataObject*)(sdBlock.metadata->data);
// TODO Print this out to test ?
if (
// Status is blank, not sent or tried
(sdBlock.metadata->status == 0)
&& (sdBlock.metadata->resultsBlockStart != 0)
&& (sdBlock.metadata->recordId != 0)
&& (sdBlock.metadata->recordId == local_metadata->recordCount)
) {
console.log(LOG_DEBUG, F("Communications: Metadata readyy, but is Result?"));
// Copy the values across, but zero if next record fails - because we loose the data
mainBinType = wii5Config.getCaptureBinaryType();
metadataBlock = sdCurrentBlock;
resultsBlock = sdBlock.metadata->resultsBlockStart;
mainRecordCount = sdBlock.metadata->recordId;
updateStatus = true;
bool badResults = false;
// Read the block
if (!sdBlock.read(resultsBlock)) {
badResults = true;
console.log(LOG_ERROR, F("Communications: Failed to read results for testing"));
}
// All records should match
else if (sdBlock.block->recordId != mainRecordCount) {
badResults = true;
console.log(LOG_ERROR, F("Communications: Results bock record ID does not match, skipping"));
}
// This is a way I know the RESULTS block has been written to
else if (sdBlock.block->status != mainRecordCount) {
badResults = true;
console.log(LOG_ERROR, F("Communications: Results status is not the record count - it isn't ready to send"));
}
if (badResults) {
// Abandon ship
console.log(LOG_FATAL, F("Communications: Error reading this block. Skipping to next"));
mainBinType = 0;
metadataBlock = 0;
resultsBlock = 0;
mainRecordCount = 0;
updateStatus = false;
}
else {
console.log(LOG_FATAL, F("Communications: Block ready to send = %lu"), resultsBlock);
step = WII5COMMUNICATIONS_IRIDIUM_SEND; wait = 0;
}
}
else {
console.log(LOG_DEBUG, F("Communications: Skipping - Status = %lu"), sdBlock.metadata->status);
}
}
}
break;
// SD Load Block
case WII5COMMUNICATIONS_SD_BLOCK:
step = WII5COMMUNICATIONS_IRIDIUM_SEND; wait = 0;
break;
// SEND a message
case WII5COMMUNICATIONS_IRIDIUM_SEND:
if (first) {
console.log(LOG_DEBUG, F("Communications: step IRIDIUM_SEND"));
}
// Safety
else if (binCount > 25) {
console.log(LOG_FATAL, F("Communications: More than 25 data splits - cancelling Iridium"));
step = WII5COMMUNICATIONS_END; wait = 0;
}
// if (wii5BinData.getSplit(wii5Config.getCommunicationsBinaryType(), 300, &binNextBit, &binType)) {
else {
// NOTE: Stil hard coded iridium number
if (wii5BinData.getSplit(mainBinType, 340, &binNextBit, &binType)) {
console.log(LOG_INFO, F("Communications: Sending BinData Splitting #=%d, original type/size=%lu/%d, new type/size=%lu/%d, nextBit=%d"),
binCount,
mainBinType, wii5BinData.getSize(mainBinType),
binType, wii5BinData.getSize(binType),
int(binNextBit)
);
binCount++;
// NOTE: setData will not set data from Blocks, call the necessary after steps.
// see BinData for more information.
wii5BinData.createData(binType, wii5BinaryIridium, 340, mainRecordCount);
wii5BinData.setData(binType, wii5BinaryIridium, 340); // TODO Hard coded
// NOTE: Performance. This should be split up into steps in this state machine, card open, read metadata, etc... This will do for now
if (metadataBlock) {
if (sdBlock.read(metadataBlock)) {
console.log(LOG_INFO, F("Communications: Loaded metadata=%lu and setting Iridium data"), metadataBlock);
// TODO Consider checking metadta block wth mainRecordCount
wii5BinData.setBlockMetadata(binType, wii5BinaryIridium, 340, (WII5MetaDataObject*)sdBlock.metadata->data);
}
else {
console.log(LOG_FATAL, F("Communications: FAILED TO LOAD Load metadata=%lu"), metadataBlock);
}
}
if (resultsBlock) {
if (sdBlock.read(resultsBlock)) {
console.log(LOG_INFO, F("Communications: Loaded results=%lu"), resultsBlock);
wii5BinData.setBlockResults(binType, wii5BinaryIridium, 340, (WII5Processed*)sdBlock.block->data);
}
else {
console.log(LOG_FATAL, F("Communications: FAILED TO LOAD Load results=%lu"), resultsBlock);
}
}
wii5Iridium.setBinSize(
wii5BinData.getSize(binType)
);
// Send it !
if (testMode)
console.log(LOG_FATAL, F("FAKE: Not requestSendBinary"));
else {
wii5Iridium.requestSendBinary();
countSend++;
}
waitStatus = true; // We are waiting for the status, unlike command replies
step = WII5COMMUNICATIONS_IRIDIUM_SEND_WAIT; wait = 0;
}
// Clear them and move on
else {
console.log(LOG_DEBUG, F("Communications: Iridium send complete, moving to end"));
if (
updateStatus
&& metadataBlock
&& (countSucces > 0)
// && (countFailed < 1) - TODO add this test
) {
// TODO how about some better meaning to the number?
sdDone++;
console.log(LOG_DEBUG, F("Communications: Updating status block=%lu, record=%lu, status=1"), metadataBlock, mainRecordCount);
sdBlock.metadataUpdateStatus(metadataBlock, mainRecordCount, 1);
sdBlock.read(metadataBlock);
console.log(LOG_INFO, F("Communications: TODO Updated status and read for block = %lu, record = %lu and status = %lu"),
metadataBlock, mainRecordCount, sdBlock.metadata->status
);
}
else {
console.log(LOG_INFO, F("Communications: Message sent but status not updated - metadataBlock=%lu,countSucces=%d,update=%d"), metadataBlock, int(countSucces), int(updateStatus));
}
updateStatus = false;
binNextBit = 0; // Important to start split
binCount = 0;
mainBinType = 0;
metadataBlock = 0;
resultsBlock = 0;
step = WII5COMMUNICATIONS_JUMP; wait = 0;
}
}
break;
// Wait for send and check reply
case WII5COMMUNICATIONS_IRIDIUM_SEND_WAIT:
if (first) {
console.log(LOG_DEBUG, F("Communications: step IRIDIUM_SEND_WAIT"));
if (testMode) {
console.log(LOG_FATAL, F("FAKE: Not waiting for Iridium"));
updateStatus = true;
countSucces++;
step = WII5COMMUNICATIONS_JUMP; wait = 0; first = true;
}
}
else if (wii5Iridium.waiting()) {
// Wait for status success/fail for counts. This allos us to ignore command messages
if (waitStatus) {
if (wii5Iridium.lastSend == WII5IRIDIUMSENDRESULT_OK) {
countSucces++;
lastSuccess = 0; // To stop repeating every loop
}
else
countFailed++;
waitStatus = false;
}
// TODO - check this isn't a race condition.
// Check incoming messages
if (wii5Iridium.lastReceive == WII5IRIDIUMRECEIVERESULT_OK) {
if (wii5Iridium.lastSend == WII5IRIDIUMSENDRESULT_OK)
console.log(LOG_INFO, F("Communications: (Send success) Received Iridium Data length=%d"), wii5Iridium.lastReceiveLen);
else
console.log(LOG_ERROR, F("Communications: (Send failed) Received Iridium Data length=%d"), wii5Iridium.lastReceiveLen);
wii5Commands.processBinData();
if ( (wii5Commands.lastCommandCmd != WII5COMMAND_NONE) && (wii5Commands.lastCommandResult != WII5RESULTS_UNKNOWN) ) {
console.log(LOG_ERROR, F("Communications: Sending reply command=%d, result=%d"), int(wii5Commands.lastCommandCmd), int(wii5Commands.lastCommandResult));
// Always send a result, allowing for next message too - 3 or 4 depending on lenght reply
uint32_t replyType = wii5BinData.setBit(0, 3);
wii5BinData.createData(replyType, wii5BinaryIridium, 340, 1);
wii5BinData.setData(replyType, wii5BinaryIridium, 340); // TODO Hard coded
wii5Iridium.setBinSize(wii5BinData.getSize(replyType));
wii5Iridium.requestSendBinary();
// Blank last command
wii5Commands.lastCommandCmd = WII5COMMAND_NONE;
wii5Commands.lastCommandResult = WII5RESULTS_UNKNOWN;
memset(wii5Commands.lastCommandMessage, 0, sizeof(wii5Commands.lastCommandMessage));
}
else {
// TODO Send error !
console.log(LOG_ERROR, F("Communications: Unable to reply command=%d, result=%d"), int(wii5Commands.lastCommandCmd), int(wii5Commands.lastCommandResult));
step = WII5COMMUNICATIONS_JUMP; wait = 0; first = true;
}
// No change of step - as we are sending again
}
else {
if (wii5Iridium.lastSend == WII5IRIDIUMSENDRESULT_OK)
console.log(LOG_INFO, F("Communications: (Send success) nothing received"));
else
console.log(LOG_ERROR, F("Communications: (Send failed) nothing received"));
// Check any further waiting messages - send same message again.
step = WII5COMMUNICATIONS_JUMP; wait = 0; first = true;
}
}
// TODO Configurables
else if (wait > 300000) {
console.log(LOG_ERROR, F("Communications: Iridium Timed out after 5 minutes"));
step = WII5COMMUNICATIONS_END; wait = 0;
}
break;
case WII5COMMUNICATIONS_END:
if (first) {
console.log(LOG_INFO, F("Communications: step END - checking"));
}
else if (metadataScan && (countSend < 1)) {
// Turn it off, so we don't get in here twice on no send.
metadataScan = false;
console.log(LOG_INFO, F("Communications: NO TRANSMISSIONS - Sending a default"));
uint32_t replyType = wii5BinData.setBit(1, 5);
sendBinModeType(replyType, 0, 0, 0);
setSimpleMode();
step = WII5COMMUNICATIONS_JUMP; wait = 0;
}
else {
metadataScan = false;
console.log(LOG_INFO, F("Communications: Sent %d messages"), countSend);
// Noithing left to do - lets shut it down.
wii5Iridium.stop();
step = WII5COMMUNICATIONS_IDLE; wait = 0;
}
break;
default:
console.log(LOG_FATAL, F("COMMUNICATIONS: Default step... step=%d"), step);
step = WII5COMMUNICATIONS_IDLE; wait = 0;
}
}
WII5Communications wii5Communications;