The Firmware appears as 3 separate versions. AVR C++ code for origianal buoys and the latest, as we moved back to that for extra long battery life. A 32 bit embedded version for ARM, ESP32 versions, and a Linux Embedded version, originally written for Intel Edison. This page is talking about the first release of code based on AVR.
The AVR Control Code is a set of C++ code that is Arduino-compatible and works on multiple platforms including AVR and ESP32. Almost all the work — battery management, GPS, temperature, IMU, accelerometers, power control, Iridium / satellite control — is done here. This is the heart of the current release.
- C++ source
- Configuration files
- Build tools
Source repository
The firmware lives at
gitea.sh3d.com.au/Sh3d/WII5Firmware —
72 C++ source files targeting the ATmega2560 at 11.0592 MHz, built with
arduino-cli. Entry point: app/wii5_buoy/wii5_buoy.ino; the main control
loop lives in WII5Controller.cpp.
Highlights from the firmware
A flavour of what’s in there. Snippets below are trimmed (TODO comments and verbose logging stripped) for readability — each “Source” link jumps to the full original on Gitea.
GPS time and position lock
The GPS loop only updates the system clock once the fix is fresh, valid, and backed by at least five satellites — defensive against drift in marginal conditions.
void WII5GPS::loop() {
if (running) {
WII5SerialManager::loop();
if (
(waitTime > UPDATE_TIME)
&& (gps->date.isValid())
&& (gps->date.age() < 5000)
&& (gps->time.isValid())
&& (gps->time.age() < 5000)
&& (gps->satellites.isValid())
&& (gps->satellites.value() > 4)
) {
when = now();
setTime(
gps->time.hour(), gps->time.minute(), gps->time.second(),
gps->date.day(), gps->date.month(), gps->date.year()
);
Source: WII5GPS.cpp lines 123–149
Battery monitoring with low-battery mode switch
A running-average sampler reads BATTERY_1_VOLTS_ANALOG, then a separate
analyse step compares the mean against configured thresholds and flips the
controller into WII5MODE_LOWBATTERY when it drops.
case WII5BATTERY_MEASURE:
if (stepCount > 100) {
step = WII5BATTERY_STORE; stepWait = 0; stepCount = 0;
}
else {
#ifdef BATTERY_1_VOLTS_ANALOG
uint16_t in = analogRead(BATTERY_1_VOLTS_ANALOG);
avgBattery1.checkAndAddReading(scale((uint16_t) in, (uint16_t)BATTERY_1_VOLTS_MULT));
#endif
}
break;
case WII5BATTERY_ANALYSE:
if (
(value < wii5Config.getBatteryLow())
&& (wii5Controller.getMode() != WII5MODE_LOWBATTERY)
) {
console.log(LOG_INFO, F("Low Battery detected V=%d, Low=%d, Mid=%d"),
value, wii5Config.getBatteryLow(), wii5Config.getBatteryMid()
);
wii5Controller.setMode(WII5MODE_LOWBATTERY);
}
Source: WII5Battery.cpp lines 86–127
Iridium signal-quality polling with retry backoff
Before transmitting an SBD message, the buoy polls signal quality (AT+CSQ)
and retries up to IRIDIUM_RETRY_MAX times if it’s below the configured
minimum — saves power and avoids burning credits on doomed transmissions.
case WII5IRIDIUM_SIGNALQUALITY:
if (first) { sendCount = 0; }
else if (stepWait > 500) {
if (sendCount == 0) {
programLine(7);
setTimeout(30000);
}
switch (sendAndWait(WII5SERIALLAST_ATCSQ)) {
case WII5SERIALCMD_WAITING:
break;
case WII5SERIALCMD_OK:
default:
if (lastVal < sigQualMin) {
if (sigQualRetry < IRIDIUM_RETRY_MAX) {
sigQualRetry++;
step = WII5IRIDIUM_SIGNALQUALITY_WAIT; stepWait = 0;
}
else {
lastSignalQuality = int(lastVal);
step = WII5IRIDIUM_SEND_JUMP; stepWait = 0;
}
}
Source: WII5Iridium.cpp lines 329–365
Binary payload splitting across 340-byte SBD limits
Iridium short-burst-data messages are capped at 340 bytes. getSplit walks
the per-field bitmask in_t, packing as many fields as will fit into one
message and returning the rest as a continuation.
bool WII5BinData::getSplit(uint32_t in_t, uint16_t max_size,
uint8_t* start_bit, uint32_t* out_t) {
uint16_t ret_size = sizeof(WII5_BINDATA_HEADER);
uint16_t part_size = 0;
*out_t = 0;
for (uint8_t b = *start_bit; b < 16; b++) {
if (BitVal(in_t, b)) {
part_size = getSizeOne(b);
if ((ret_size + part_size) > max_size) {
*start_bit = b;
if (*out_t == 0) {
console.log(LOG_FATAL, F("BinData: Failed to split BinData to this size"));
return false;
}
return true;
}
SetBit(*out_t, b);
ret_size += part_size;
}
}
Source: WII5BinData.cpp lines 89–116
Deep-sleep cycle with peripheral shutdown
WII5SLEEP_WAIT powers everything down — GPS, Iridium, IMU, SD, the shared
5 V rail — waits 20 s for things to settle, then WII5SLEEP_SLEEPING calls
into the AVR sleep routine for the configured period.
case WII5SLEEP_WAIT:
if (first) {
#ifdef WII5_GPS
wii5Gps.off();
#endif
wii5Communications.stop();
wii5Iridium.stop();
wii5Sparton.stop();
wii5Controller.setSDOff();
wii5Controller.shared5Off();
}
else if (stepWait > 20000) {
step = WII5SLEEP_SLEEPING; stepWait = 0;
}
break;
case WII5SLEEP_SLEEPING:
if (sleepNextSeconds > 21600) {
sleepNextSeconds = 3600; // safety cap: max 6 h
}
console.log(LOG_INFO, F("Sleep: sleeping %lu seconds"), sleepNextSeconds);
wii5Setup.sleepBefore();
sh3dNodeUtil.sleep(sleepNextSeconds);
wii5Setup.sleepAfter();
Source: WII5ModeSleep.cpp lines 92–136
@WII5,cmd,sub,arg command dispatcher
The buoy speaks an AT-style CSV protocol over serial and over Iridium. A
@-prefixed line is parsed into command / sub-command / argument and
dispatched through runCommand — same code path for local console and
remote satellite command.
bool WII5Commands::processConsoleAT() {
if (!console.available() || (console.getCommand() != '@'))
return false;
if (strcmp_P(console.getCsvBuffer(0), (PGM_P) F("WII5")) == 0) {
return runCommand(
wii5Strings.parseCommand(
console.getCsvBuffer(1),
console.getCsvBuffer(2),
console.getCsvBuffer(3)
)
);
}
else if (strcmp_P(console.getCsvBuffer(0), (PGM_P) F("Help")) == 0) {
return wii5Help.processConsoleCsv();
}
return false;
}
Source: WII5Commands.cpp lines 1407–1425