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

659 lines
15 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 WII5Sh3dConsole.cpp
* @brief Console abstraction: routes printf/log to one or more serial streams.
*/
/*
Console code
TODO: 2024 Check this is the only copy of console and simplify
Map console to multiple outputs. Use Printf and a buffer. Automatically
support PROGMEM (Flash memory), String object and char pointers.
* TODO This should be callbacks
* NMEA Strings updated - do This
* Etc
* TODO Binary is almost certainly out by one start/end or both
* TODO Write test code that sends streams:
* NMEA - obvious one
* @CSV
* !Binary
Console console();
void setup() {
SerialConsole.begin(115200);
console.begin();
console.add(&SerialConsole);
console.printf(F("Hello world"));
* Rename to something better than console
* Release open source on github Sh3d
* Add print from SD card - ability to get and print data form a SD card out
* Add print from PROGMEM arrays
* Add print from SPIFlash
NOTE on NMEA devices
* NMEA is always processed from incoming data on the consoles
* If a second "TinyGPS++" is required, you can addNmea stream instead
* OR even better, just add a stream as add(&HWSerial, CONSOLE_DIRECTION_INPUT);
Restructure to simpler methods:
* BEFORE
= Sketch size 15378
* AFTER
= Sketch size 15046 (saved 340 bytes)
Restructure to error numbers:
* Sh3dNodeErrors
* console.error(n)
* console.safeError(n)
* uint16_t - 01..65 modules split, 1000 message per module
* Could we add an optional long?
*/
#include "Arduino.h"
#include <WII5Sh3dConsole.h>
#include <TimeLib.h>
#include <MemoryFree.h>
// printf support
// Just a string
void WII5Sh3dConsole::print(char *out) {
for(byte i = 0; i < CONSOLE_STREAMS; i++) {
if (
streams[i]
&& (
(streamsDirection[i] == CONSOLE_DIRECTION_BOTH)
|| (streamsDirection[i] == CONSOLE_DIRECTION_OUTPUT)
)
) {
streams[i]->print(out);
#ifdef CONSOLE_DEBUG_FLUSH
streams[i]->flush();
#endif
}
}
}
// sprintf - STREAM printf
void WII5Sh3dConsole::sprintf(Stream *s, const __FlashStringHelper *out, ... ) {
if (!check()) return;
va_list argptr;
va_start(argptr, out);
VSNPRINTF(printBuffer, bufferMax, (const char *)out, argptr);
va_end(argptr);
s->print(printBuffer);
}
// printf support
void WII5Sh3dConsole::printf(char* out, ...) {
if (!check()) return;
va_list argptr;
va_start(argptr, out);
vsnprintf(printBuffer, bufferMax, out, argptr);
va_end(argptr);
print(printBuffer);
}
// Convert FLASH String to String
void WII5Sh3dConsole::printf(const __FlashStringHelper *out, ... ){
if (!check()) return;
va_list argptr;
va_start(argptr, out);
VSNPRINTF(printBuffer, bufferMax, (const char *)out, argptr);
va_end(argptr);
print(printBuffer);
}
// TODO Move to Sh3dNodeErrors
void WII5Sh3dConsole::printPrefix(uint8_t level) {
switch(level) {
case LOG_INFO:
printf(F(CONSOLE_PRE_INFO));
break;
case LOG_WARN:
printf(F(CONSOLE_PRE_WARN));
break;
case LOG_ERROR:
printf(F(CONSOLE_PRE_ERROR));
break;
case LOG_DEBUG:
printf(F(CONSOLE_PRE_DEBUG));
break;
case LOG_FATAL:
printf(F(CONSOLE_PRE_FATAL));
break;
default:
// Do we need unknown?
break;
}
printDateTime();
printf(F(CONSOLE_POST_TIME));
}
// printf support
void WII5Sh3dConsole::log(uint8_t l, char* out, ...) {
if (level < l) return;
if (!check()) return;
printPrefix(l);
va_list argptr;
va_start(argptr, out);
vsnprintf(printBuffer, bufferMax, out, argptr);
va_end(argptr);
print(printBuffer);
printNewLine();
}
// Convert FLASH String to String
void WII5Sh3dConsole::log(uint8_t l, const __FlashStringHelper *out, ... ){
if (level < l) return;
if (!check()) return;
printPrefix(l);
va_list argptr;
va_start(argptr, out);
VSNPRINTF(printBuffer, bufferMax, (const char *)out, argptr);
va_end(argptr);
print(printBuffer);
printNewLine();
}
// TODO - Compile time option to remove unused entries - e.g We may not support String
void WII5Sh3dConsole::safeLog(uint8_t l, char* out, ...) {
if (!check()) return;
printPrefix(l);
va_list argptr;
va_start(argptr, out);
vsnprintf(printBuffer, bufferMax, out, argptr);
va_end(argptr);
print(printBuffer);
printNewLine();
flush();
}
void WII5Sh3dConsole::safeLog(uint8_t l, const __FlashStringHelper *out, ... ){
if (!check()) return;
printPrefix(l);
va_list argptr;
va_start(argptr, out);
VSNPRINTF(printBuffer, bufferMax, (const char *)out, argptr);
va_end(argptr);
print(printBuffer);
printNewLine();
flush();
}
void WII5Sh3dConsole::printDateTime(uint32_t t) {
if (t == 0) {
if (!displayDateTime) return; // Automatic mode
#ifdef _Time_h
t = now();
#endif
}
// uint16_t - date.year,
// uint8_t - date.month, date.day,
// uint8_t - time.hour, time.minute, time.second
sprintf_P(printBuffer, PSTR("%04d-%02d-%02dT%02d:%02d:%02d"),
year(t),
int(month(t)),
int(day(t)),
int(hour(t)),
int(minute(t)),
int(second(t))
);
print(printBuffer);
}
void WII5Sh3dConsole::loop() {
// Reset cmd
if (cmd) {
cmdCount = 0;
csv_count = 0;
csv_last = 0;
cmdType = 0;
cmd = '\0';
}
// Reset Binary
if (binAvailable) {
binAvailable = false;
binReceiveCount = 0;
}
for(byte i = 0; i < CONSOLE_STREAMS; i++) {
// NOTE - while important, but can be too much - limit?
if (
(streamsDirection[i] == CONSOLE_DIRECTION_BOTH)
|| (streamsDirection[i] == CONSOLE_DIRECTION_INPUT)
) {
while (streams[i] && streams[i]->available())
processCmd(streams[i]->read());
}
}
}
void WII5Sh3dConsole::processCsv() {
// TODO check for csv_count max and cmdCount max Etc
// Point to the start of the last strinb before the comma
csv_str[csv_count] = &cmdBuffer[csv_last];
// Null the ','
cmdBuffer[cmdCount] = '\0';
// Parse to ingeger
csv_uint32[csv_count] = atol(csv_str[csv_count]);
// Next character
csv_last = cmdCount + 1;
// console.log(LOG_DEBUG, F("CSV found comma last=%d, count=%d, str=%s, lu=%lu"), int(csv_last), int(csv_count), csv_str[csv_count], csv_uint32[csv_count]);
csv_count++;
}
// processCmd
void WII5Sh3dConsole::processCmd(char in) {
// DO Timeout
if ( (cmdCount > 0) && (waitInput > CONSOLE_TIMEOUT) ) {
printf(F("# CONSOLE: Timeout for new Serial Command. Max time = %d seconds"), int(CONSOLE_TIMEOUT / 1000));
printNewLine();
cmdCount = 0;
binAvailable = false;
binReceiveCount = 0;
}
// BINARY Data mode - keep here
if (binEnable && (cmdType == CONSOLE_TYPE_BINDATA_ID)) {
if (cmdCount == 0) {
printf(F("# FATAL ERROR - Must not be in BINDATA mode with no count, Invalid mode")); printNewLine();
cmdCount = 0;
cmdType = 0;
}
binBuffer[binReceiveCount] = in;
binReceiveCount++;
if (binReceiveCount >= binReceiveExpect) {
// COMPLETE !
cmdType = 0;
cmdCount = 0;
binAvailable = true;
return;
}
if (binReceiveCount >= binMax) {
// TODO This should NOT be allowed to happen - since we check below
safeLog(LOG_FATAL, F("CONSOLE: Binary incoming data too large"));
cmdCount = 0;
cmd = '\0';
val = 0;
csv_last = 0;
csv_count = 0;
binReceiveCount = 0;
}
// Rest of function is TEXT processing - leave now
return;
}
// TEXT Mode - NOT a new line, keep data
if ( (in != '\n') && (in != '\r') ) {
// First character - see if it is $
if ( (cmdCount == 0) ) {
// Reset timeout
waitInput = 0;
// $ = NMEA = 1
if (in == CONSOLE_TYPE_NMEA_C0) {
cmdType = CONSOLE_TYPE_NMEA_ID;
}
// CSV
else if (in == CONSOLE_TYPE_CSV_C0) {
csv_last = 1; // Skip the "@"
// Start all at Zero
for (csv_count = 0; csv_count < CONSOLE_TYPE_CSV_LEN; csv_count++) {
csv_uint32[csv_count] = 0;
}
csv_count = 0;
cmdType = CONSOLE_TYPE_CSV_ID;
// console.log(LOG_DEBUG, F("CSV START last=%d, count=%d"), int(csv_last), int(csv_count));
}
// BINARY
else if (binEnable && (in == CONSOLE_TYPE_BIN_C0)) {
cmdType = CONSOLE_TYPE_BIN_ID;
binReceiveCount = 0;
binReceiveExpect = 0;
binReceiveCRC = 0; // Optional?
}
// Else assume standard ending in ";"
else {
cmdType = CONSOLE_TYPE_OTHER_ID;
}
}
// Keep cmdBuffer if not NMEA
if (
// Keep Other
cmdType == CONSOLE_TYPE_OTHER_ID // Keep for external use
|| cmdType == CONSOLE_TYPE_CSV_ID // Keep and process on the fly
|| cmdType == CONSOLE_TYPE_NMEA_ID // NMEA
|| cmdType == CONSOLE_TYPE_BIN_ID
) {
// TODO test out by one
if (cmdCount < (consoleMax - 1)) {
cmdBuffer[cmdCount] = in;
// TODO ',' should be #define in case we use : or tab or something
if (cmdType == CONSOLE_TYPE_CSV_ID && in == ',') {
processCsv();
}
cmdCount++;
cmdBuffer[cmdCount] = '\0';
}
}
}
// TEXT MODE - New Line - process - NOTE: This may get called twice (\r then \n)
// - Once leaving this, the only way for a new command to start
// is to set cmdCount = 0
else {
// Do nothing with this
if (cmdType == CONSOLE_TYPE_IGNORE_ID) {
}
// Blank everything
else if (cmdType == CONSOLE_TYPE_DESTROY_ID) {
cmdCount = 0;
cmd = '\0';
val = 0;
csv_last = 0;
csv_count = 0;
}
// Binary
else if (binEnable && (cmdType == CONSOLE_TYPE_BIN_ID)) {
if (cmdCount < 2) {
printf(F("# CONSOLE: Error, no size requests for binary data")); printNewLine();
printf(F("BINARY ERROR: No size")); printNewLine();
cmdCount = 0;
cmdType = 0;
}
else {
val = atol(&cmdBuffer[1]);
if (val >= binMax) {
printf(F("# CONSOLE: Request Binary larger than buffer")); printNewLine();
printf(F("BINARY ERROR: Too large")); printNewLine();
cmd = '\0';
cmdType = 0;
cmdCount = 0;
}
else {
printf(F("# CONSOLE: Request Binary size=%d"), binReceiveExpect); printNewLine();
printf(F("BINARY OK: Ready to send")); printNewLine();
binReceiveExpect = val;
binReceiveCount = 0;
cmdType = CONSOLE_TYPE_BINDATA_ID;
}
}
}
// Add optional ending, or even CRC
else if (cmdType == CONSOLE_TYPE_OTHER_ID) {
if (echo)
log(LOG_DEBUG, F("CONSOLE Receved: %s"), cmdBuffer);
// Check we have a buffe?
if (cmdCount >= (consoleMax - 1)) {
// printf(F("# CONSOLE overflow: cmd size - c=%d of %d\r\n"), cmdCount, consoleMax);
printf(F("# CONSOLE cmd overflow"));
printNewLine();
cmdCount = 0;
cmdType = CONSOLE_TYPE_IGNORE_ID;
}
else if (cmdBuffer[cmdCount - 1] != ';') {
printf(F("# CONSOLE: standard input must end in ; count=%d, last=%d"), cmdCount, (uint8_t)cmdBuffer[cmdCount =- 1]);
printNewLine();
cmdCount = 0;
cmdType = CONSOLE_TYPE_IGNORE_ID;
}
else {
cmd = cmdBuffer[0];
val = atol(&cmdBuffer[1]);
cmdType = CONSOLE_TYPE_IGNORE_ID;
}
}
else if ( cmdType == CONSOLE_TYPE_NMEA_ID ) {
// Ignore the NMEA - already processed
// TODO: Maybe mark the buffer as now a copy of the NMEA for passthrough?
}
else if ( cmdType == CONSOLE_TYPE_CSV_ID ) {
cmdCount++; // Spin on, so this will be like the "," where we add the null
processCsv();
cmd = CONSOLE_TYPE_CSV_C0;
cmdType = CONSOLE_TYPE_IGNORE_ID;
if (echo) {
printPrefix(LOG_DEBUG);
printf(F("CONSOLE Received (@): "));
for (i = 0; i < console.getCsvCount(); i++) {
printf(console.getCsvBuffer(i));
printf(",");
}
printNewLine();
}
}
}
}
bool WII5Sh3dConsole::available(){
return cmd != '\0';
}
// begin - using External Buffers
void WII5Sh3dConsole::begin(char* buf, uint16_t bufLen, char* cmdBuf, uint16_t cmdLen) {
// TODO
echo = true;
printBuffer = buf;
bufferMax = bufLen;
cmdBuffer = cmdBuf;
consoleMax = cmdLen;
cmdType = 0;
setDateTime();
}
// begin - Allocate the buffers and get things going
void WII5Sh3dConsole::begin(uint16_t bufLen, uint16_t cmdLen) {
if ( (bufLen + cmdLen) >= freeMemory() )
internalErrorNotEnoughMemory();
else {
printBuffer = (char*)malloc(bufLen);
if (printBuffer == NULL)
internalErrorNotEnoughMemory();
else
bufferMax = bufLen;
cmdBuffer = (char*)malloc(cmdLen);
if (cmdBuffer == NULL)
internalErrorNotEnoughMemory();
else
consoleMax = cmdLen;
cmdType = 0;
setDateTime();
}
}
void WII5Sh3dConsole::internalErrorNotEnoughMemory() {
SerialConsole.println(F("CONSOLE IntErr: failbuf"));
}
void WII5Sh3dConsole::internalErrorNoBufferStream() {
SerialConsole.println(F("CONSOLE IntErr: buf, stream"));
}
bool WII5Sh3dConsole::check() {
if (streamCount == 0) {
internalErrorNoBufferStream();
return false;
}
if (bufferMax == 0) {
internalErrorNoBufferStream();
return false;
}
return true;
}
void WII5Sh3dConsole::clearCommand() {
cmdCount = 0;
csv_count = 0;
csv_last = 0;
cmdType = 0;
cmd = '\0';
}
void WII5Sh3dConsole::clearCallback() {
callback_default = false;
callback = NULL;
}
void WII5Sh3dConsole::setCallbackFunction(CallbackFunction f) {
callback_default = false;
callback = f;
}
void WII5Sh3dConsole::setCallbackLevel(uint8_t l) {
callback_level = l;
}
uint8_t WII5Sh3dConsole::getCallbackLevel() {
return callback_level;
}
// Add a stream - currently very limited (0 NEITHER, 1 INPUT, 2 OUTPUT, 3 BOTH)
bool WII5Sh3dConsole::add(Stream *s, uint8_t direction) {
for(byte i = 0; i < CONSOLE_STREAMS; i++) {
if (streams[i] == s) {
return true;
}
else if (! streams[i]) {
streams[i] = s;
streamsDirection[i] = direction;
streamCount++;
return true;
}
}
// Internal Error !
return false;
}
bool WII5Sh3dConsole::remove(Stream *s) {
for(byte i = 0; i < CONSOLE_STREAMS; i++) {
if (streams[i] == s) {
streams[i] = NULL;
streamsDirection[i] = 0;
streamCount--;
return true;
}
}
return false;
}
void WII5Sh3dConsole::setLevel(byte l) {
level = l;
}
// Flush sending....no longer doing inputs
void WII5Sh3dConsole::flush() {
for(byte i = 0; i < CONSOLE_STREAMS; i++) {
if (streams[i]) {
streams[i]->flush();
}
}
}
void WII5Sh3dConsole::printNewLine() {
printf(F("\r\n"));
}
bool WII5Sh3dConsole::enableBinary() {
binEnable = true;
}
bool WII5Sh3dConsole::disableBinary() {
binEnable = false;
}
void WII5Sh3dConsole::setBinaryBuffer(char *buf, uint16_t len) {
binBuffer = buf;
binMax = len;
binEnable = true;
}
// Pretty Print Binary
#define PERLINE 10
void WII5Sh3dConsole::printBinary(char* buf, uint16_t len) {
printf(F("LEN = %d\r\n"), len);
uint16_t i = 0;
while (i < len) {
for (uint16_t x = 0; x < PERLINE; x++) {
if ( (i+x) < len) {
printf(F("0x%.2X "), (uint8_t)buf[i+x]);
}
else {
printf(F(" "), (int16_t)buf[i+x]);
}
}
printf(F(" |"));
for (uint16_t x = 0; x < PERLINE; x++) {
if (
((i+x) < len)
&& ((uint8_t)buf[i+x] >= 32)
&& ((uint8_t)buf[i+x] < 127)
) {
printBuffer[x] = buf[i+x];
}
else {
printBuffer[x] = ' ';
}
}
printBuffer[PERLINE] = '\0';
print(printBuffer);
printf(F("|\r\n"));
i = i + PERLINE;
}
printNewLine();
}
void WII5Sh3dConsole::printBits(void* buf, uint16_t len) {
for (uint16_t x = 0; x < len; x++) {
void* n = buf + (x/8);
printf(bitRead(*(uint8_t*)n, x % 8) ? "1" : "0");
}
printNewLine();
SerialConsole.print(F("BIN: "));
SerialConsole.print(*(uint16_t*)buf, BIN);
SerialConsole.println();
}
void WII5Sh3dConsole::setEcho(bool in) {
echo = in;
}
bool WII5Sh3dConsole::getEcho() {
return echo;
}
WII5Sh3dConsole console;