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
+553
View File
@@ -0,0 +1,553 @@
// 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 WII5SerialManager.cpp
* @brief Serial-port helper: AT-response parsing, command queue.
*/
/*
WII5SerialManager - Base class for managed serial ports
Sketch uses 106588 bytes (41%) of program storage space. Maximum is 258048 bytes.
Global variables use 5621 bytes (68%) of dynamic memory, leaving 2571 bytes for local variables. Maximum is 8192 bytes.
Global variables use 5621 bytes (68%) of dynamic memory, leaving 2571 bytes for local variables. Maximum is 8192 bytes.
*/
#include <Arduino.h>
#include <WII5SerialManager.h>
#include <WII5.h>
void WII5SerialManager::sendNewLine() {
stream->print(F("\r\n"));
}
void WII5SerialManager::beginSerialManager() {
// console.log(LOG_DEBUG, F("SerialManager: Buffer size=%d"), bufMax);
processTimeout = 15000; // TODO This should be configurable
}
void WII5SerialManager::endSerialManager() {
// closeFile();
}
// No disable ATM
void WII5SerialManager::enableNmea() {
gps = new TinyGPSPlus();
}
void WII5SerialManager::setPassthrough(bool in) {
passthrough = in;
}
bool WII5SerialManager::getPassthrough() {
return passthrough;
}
void WII5SerialManager::setDebug(bool in) {
debug = in;
}
bool WII5SerialManager::getDebug() {
return debug;
}
void WII5SerialManager::setCapture(bool in) {
capture = false;
}
bool WII5SerialManager::getCapture() {
return capture;
}
void WII5SerialManager::setRecords(uint32_t in) {
recordTotal = in;
}
uint32_t WII5SerialManager::getRecords() {
return recordTotal;
}
void WII5SerialManager::setTimeout(uint32_t in) {
processTimeout = in;
}
uint32_t WII5SerialManager::getTimeout() {
return processTimeout;
}
// TODO - Make this reusable - e.g. plug in different list of lines (callback?)
// Before calling:
// - probramCount (where to start), -programTotal (where to end) (not length)
// - sendCount = 0
WII5_SERIALCMDS WII5SerialManager::sendAll(WII5SERIAL_LAST lastUntil) {
// Sent everyting - spin on
if (programCount > programTotal) {
return WII5SERIALCMD_OK;
}
// First count - read line to buffer
if (sendCount == 0) {
programLine(programCount);
}
switch (sendAndWait()) {
// Still waiting.
case WII5SERIALCMD_WAITING:
if (debug && ((sendCount % 10000) == 0))
console.log(LOG_DEBUG, F("SERIAL: waiting, sendCount=%lu"), sendCount);
break;
// Allow these to fall through - we will spin on lines even if error
case WII5SERIALCMD_TIMEOUT:
console.log(LOG_ERROR, F("SERIAL: TIMEOUT sending: %s"), buffer);
programCount++; // NOTE - Just spin on for now
sendCount = 0;
// TODO - consier return WII5SERIALCMD_TIMEOUT;
break;
case WII5SERIALCMD_ERROR:
console.log(LOG_ERROR, F("SERIAL: ERROR sending: %s"), buffer);
programCount++; // NOTE - Just spin on for now
sendCount = 0;
// TODO - consier return WII5SERIALCMD_ERROR;
break;
case WII5SERIALCMD_OK:
// TODO why?
if (debug) {
programLine(programCount);
}
programCount++;
sendCount = 0;
break;
default:
console.log(LOG_ERROR, F("SERIAL: Unknown sendAndWait return"));
programCount++; // NOTE - Just spin on for now
sendCount = 0;
break;
}
return WII5SERIALCMD_WAITING;
}
// TODO return true when done - either timeout or error or success
// - Instead could return:
// CMD_WAITING - Stil waiting
// CMD_OK - OK - success
// CMD_ERROR - Error returned to us (see buffer)
// CMD_TIMEOUT - Timeout
WII5_SERIALCMDS WII5SerialManager::sendAndWait(WII5SERIAL_LAST lastUntil) {
// First time in - send the string
if (sendCount == 0) {
// Send to IMU (this should allow for write, or print and with none, \r, \n or \r\n
// stream->write(buffer, strlen(buffer));
stream->print(buffer);
sendNewLine();
if (passthrough)
consoleBuffer();
// if (debug)
// console.log(LOG_DEBUG, F("SerialManager: send line: %s"), buffer);
// Clear timeout
processWait = 0;
// Clear any incoming data
last = WII5SERIALLAST_NONE;
bufCount = 0; fieldLoc = 0;
buffer[bufCount] = '\0';
processMode = WII5SERIALPARSER_OKHUH;
}
// Future times
else {
// Did we get OK or ERROR? (could this get into other modes - e.g. LINE or RECORD)
if (last == lastUntil) {
sendCount = 0;
return WII5SERIALCMD_OK;
}
else if (last == WII5SERIALLAST_ERROR) {
sendCount = 0;
return WII5SERIALCMD_ERROR;
}
}
if (processWait > processTimeout) {
sendCount = 0;
return WII5SERIALCMD_TIMEOUT;
}
// Increment counter and inform - WAITING
sendCount++;
return WII5SERIALCMD_WAITING;
}
// SerialManager Things:
// - Turn Off
// - Start now
// - Start at X
void WII5SerialManager::loop() {
// Process the Serial - only if it is running - to consider this more...
if (stream) {
while (stream->available()) {
// One line at a time - to allow proessing return data
if (processSerial(stream->read())) {
break;
}
}
}
}
void WII5SerialManager::start(bool force) {}
void WII5SerialManager::stop(bool force) {}
void WII5SerialManager::setBuffer(char* buf, uint8_t sz) {
buffer = buf;
bufMax = sz;
}
void WII5SerialManager::setBaudrate(uint32_t baud) {
console.log(LOG_WARN, F("Baudrate: Not supported by this module"));
}
void WII5SerialManager::repl(uint32_t baud) {
uint8_t lastDot = 0;
start();
if (baud > 0)
setBaudrate(baud);
if (!stream) {
console.log(LOG_FATAL, F("SerialManager: Can not do REPL - no stream after start"));
return;
}
while (1) {
if (SerialConsole.available()) {
char c = SerialConsole.read();
if (c == '.')
lastDot++;
else
lastDot = 0;
if (lastDot >= 3) {
SerialConsole.flush();
stream->flush();
stop();
console.log(LOG_DEBUG, F("SerialManager: Completed REPL. User exit."));
return;
}
stream->write(c);
}
if (stream->available()) {
char c = stream->read() ;
SerialConsole.write(c);
}
}
}
void WII5SerialManager::processBufferLine() {
if (debug)
console.log(LOG_DEBUG, F("SerialManager: received line: %s"), buffer);
last = WII5SERIALLAST_LINE;
}
void WII5SerialManager::processBufferOK() {
if (bufCount < 1)
return;
// If last 2 chars = "ok"
if (
(strncmp_P(buffer,(PGM_P) F("OK"), 2) == 0)
|| (strncmp_P(buffer,(PGM_P) F("ok"), 2) == 0)
) {
if (debug)
console.log(LOG_DEBUG, F("SerialManager: received ok: len=%d, str=%s"), bufCount, buffer);
last = WII5SERIALLAST_OK;
}
// READY
if (
(strncmp_P(buffer,(PGM_P) F("READY"), 5) == 0)
|| (strncmp_P(buffer,(PGM_P) F("ready"), 5) == 0)
) {
if (debug)
console.log(LOG_DEBUG, F("SerialManager: received ready: len=%d, str=%s"), bufCount, buffer);
last = WII5SERIALLAST_READY;
}
if (strncmp_P(buffer,(PGM_P) F("+CSQ:"), 5) == 0) {
lastVal = atol(&buffer[5]);
if (debug)
console.log(LOG_DEBUG, F("SerialManager: received CSQ: val=%lu, buffer=%s"), lastVal, buffer);
last = WII5SERIALLAST_ATCSQ;
}
if (strncmp_P(buffer,(PGM_P) F("+SBDI:"), 6) == 0) {
// +SBDI:<MO status>,<MOMSN>,<MT status>,<MTMSN>,<MT length>,<MT queued>
// MO status
// 0 = No SBD message to send
// 1 = SBD message successfully sent from the 9602 to the GSS
// 2 = An error occurred while attempting to send SBD message from 9602 to GSS
// MOMSN
// Unique number of this message sent, Keep
// MT status
// 0 = No SBD message to receive from the GSS
// 1 = SBD message successfully received from the GSS (how to get?)
// 2 = An error occurred while attempting to perform a mailbox check or receive a message from the GSS.
// MT length
// The MT length is the length in bytes of the mobile terminated SBD message received from the GSS. If no message was received, this eld will be zero
// MT queued
// MT queued is a count of mobile terminated SBD messages waiting at the GSS to be transferred to the 9602
//
// TODO Note - these need to be split on "," and stored in an array
// ?recordVals[]
// lastVal = atol(&buffer[4]);
// Serial:< +SBDI: 1, 8, 0, 0, 0, 0
lastVal = atol(&buffer[5]);
/*
uint8_t lastMOStatus;
uint16_t lastMOMSN;
uint8_t lastMTStatus;
uint16_t lastMTMSN;
uint16_t lastMTLength;
uint16_t lastMTQueued;
*/
if (debug)
console.log(LOG_DEBUG, F("SerialManager: received SBDI: buffer=%s"), buffer);
// +SBDI:<MO status>,<MOMSN>,<MT status>,<MTMSN>,<MT length>,<MT queued>
// Can't seem to ignore the spaces?
// sscanf_P(&buffer[6], PSTR("%d,%d,%d,%d,%d,%d"), lastMSOStatus, lastMOMSN, lastMTStatus, lastMTMSN, lastMTLength, lastMTQueued);
for(fieldCount = 0; fieldCount < WII5_FIELD_MAX; fieldCount++) {
recordVals[fieldCount] = 0;
}
fieldCount = 0;
p = strtok(&buffer[6], ",");
while (p) {
if (fieldCount < WII5_FIELD_MAX) {
recordVals[fieldCount] = atol(p);
p = strtok(NULL, ",");
fieldCount++;
}
else {
p = NULL;
}
}
last = WII5SERIALLAST_ATSBDI;
}
if (strncmp_P(buffer,(PGM_P) F("BOOT Version:"), 13) == 0) {
// TODO Note - these need to be split on "," and stored in an array
// ?recordVals[]
// lastVal = atol(&buffer[4]);
if (debug)
console.log(LOG_DEBUG, F("SerialManager: received BOOT Version: buffer=%s"), buffer);
last = WII5SERIALLAST_BOOTVERSION;
}
// A number, but do we check the whole thing?
if (buffer[0] == '3') {
// lastVal = atol(&buffer[0]);
// TODO Keeping this? Howto
if (debug)
console.log(LOG_DEBUG, F("SerialManager: received a number=%s"), buffer);
last = WII5SERIALLAST_NUMBER;
}
/*
if (debug) {
if (bufCount == 0)
console.log(LOG_DEBUG, F("SerialManager: failed ok: len=%d, str={NA}"), bufCount);
else if (bufCount >= 2)
console.log(LOG_DEBUG, F("SerialManager: failed ok: len-2=%c len-1=%c"), buffer[bufCount - 2], buffer[bufCount - 1]);
else
console.log(LOG_DEBUG, F("SerialManager: failed ok: len=%d, str=%s"), bufCount, buffer);
}
*/
// ??? last = WII5SERIALLAST_OK;
}
// Sample Buffer Field - just assume longs
void WII5SerialManager::processBufferField(char *buf, uint8_t len) {
if (fieldCount < WII5_FIELD_MAX) {
// Check size is valid
if (len >= WII5SERIALMAANGER_FIELD_SIZE) {
console.log(LOG_FATAL, F("SerialManager: processBufferField too large len=%d, available=%d"), int(len), int(WII5SERIALMAANGER_FIELD_SIZE));
return;
}
// Copy the data
// TODO find all strncpy and do this !!!
memset(fieldBuffer, 0, sizeof(fieldBuffer));
strncpy(fieldBuffer, buf, len);
// Convert to Long
recordVals[fieldCount] = atol(buf);
fieldCount++;
}
}
void WII5SerialManager::processRecord() {
if (bufCount == 0) return;
recordCount++;
last = WII5SERIALLAST_RECORD;
}
// Basic line parsers
bool WII5SerialManager::processSerial(char in) {
// processMode
// NONE, LINE, RECORD, OKHUH, AT
if (processMode == WII5SERIALPARSER_NONE) return false;
// TODO Untested binary1 mode - from Iridium....
if (processMode == WII5SERIALPARSER_BINARY1) {
// TODO - maximum buffer size
console.printf(F("# BINARY1 received = %d - %d\r\n"), bufCount, int(in));
binBuffer[bufCount] = in;
bufCount++;
if (binBufferExpect == 0) {
// Look for length
if ((bufCount > 1) && (binBuffer[bufCount - 2] == '\0')) {
binBufferExpect = (uint8_t)binBuffer[bufCount - 1];
console.printf(F("# BINARY1 Expect = %d\r\n"), binBufferExpect);
bufCount = 0;
}
// Hmm... too much guff...
else if (bufCount > 25) {
// Something went very wrong.
processMode = WII5SERIALPARSER_NONE;
console.printf(F("# BINARY1 No length provided\r\n"));
return false;
}
}
else if (bufCount >= (binBufferExpect + 2)) {
processMode = WII5SERIALPARSER_NONE;
return true;
}
return false;
}
// TODO return gps->encode(in);
if (gps)
gps->encode(in);
// NOT a new line
if ( (in != '\n') && (in != '\r') ) {
// Process 1 field at a time (when comma)
if ( (processMode == WII5SERIALPARSER_RECORD) && (in == ',') ) {
processBufferField(&buffer[fieldLoc], bufCount - fieldLoc);
buffer[bufCount] = ',';
bufCount++;
fieldLoc = bufCount; // Next field will start here (could be null)
}
// Keep whole record
else {
if ((bufCount + 1) < bufMax) {
buffer[bufCount] = in;
bufCount++;
buffer[bufCount] = '\0';
}
else {
// TODO Log - just once, FATAL
// last = WII5SPARTONRETURN_ERROR;
}
}
}
// New line - end of record
else {
writePassthrough();
writeCapture();
// CSV Data
if (processMode == WII5SERIALPARSER_RECORD) {
processBufferField(&buffer[fieldLoc], bufCount - fieldLoc);
processRecord();
}
else if (processMode == WII5SERIALPARSER_OKHUH) {
processBufferOK();
}
// An unknown line
else {
processBufferLine();
}
bufCount = 0;
// buffer[0] = '\0';
fieldCount = 0;
fieldLoc = 0;
return true;
}
return false;
}
void WII5SerialManager::writePassthrough() {
if (passthrough && bufCount > 0)
consoleBuffer(true); // Input
}
// Allow override of output of lines to log file (or even multiple)
void WII5SerialManager::writeCapture() {
}
void WII5SerialManager::sendLine(uint16_t l) {
programLine(l);
stream->print(buffer);
sendNewLine();
if (passthrough)
consoleBuffer();
}
// TODO: Move from buffer to wii5StringBuffer
void WII5SerialManager::programLine(uint16_t l) {
if (debug)
console.log(LOG_DEBUG, F("SerialManager: set command=%d buffer=%s"), l, buffer);
}
void WII5SerialManager::consolePrefix(bool input) {
if (input)
console.printf(F("# Serial:< "));
else
console.printf(F("# Serial:> "));
}
void WII5SerialManager::consoleBuffer(bool input, bool useLog) {
// TODO: This will fail if split comma and buffer gets added nulls
if (useLog) {
// TODO prefix?
// TODO Log level?
console.log(LOG_INFO, F("Serial:%s %s"), input ? "<" : ">", buffer);
}
else {
consolePrefix(input);
console.printf(buffer);
console.printNewLine();
}
}
WII5_DRIVERS WII5SerialManager::driverId() {
return WII5DRIVER_UNKNOWN;
}
/*
void WII5SerialManager::displaySTATS() {
// TODO wii5Display.atStatsSend
console.printf(F("STATS:%s:SerialManager:TODO\r\n"), wii5Strings.strDriver(driverId()));
}
*/
// Where? and What? - assumes buffer now has the comma separated data correct
void WII5SerialManager::sendAtDataLine() {
// In other words - assume this serial port, instead of say.... Console
// if (!s) s = stream; - currently assumes console
// NOTE: Steal the console buffer - so don't use it for debugging half way through here.
// You can still use console.print, just not printf, or log
// TODO How to calculate CNC
wii5Display.atDataSend(
F("wii5direct"), // 8 or 64?
// NOTE: Must not add the "@" - it isn't used in CRC calculation
F("%s"), buffer
);
}