diff --git a/.github/workflows/build-audiomoth-bin.yml b/.github/workflows/build-audiomoth-bin.yml new file mode 100644 index 0000000..90039d5 --- /dev/null +++ b/.github/workflows/build-audiomoth-bin.yml @@ -0,0 +1,172 @@ +name: Build AudioMoth firmware bin + +on: + workflow_dispatch: + inputs: + audiomoth_project_ref: + description: AudioMoth-Project branch or tag to use as the build framework + required: false + default: master + push: + branches: + - master + paths: + - 'src/**' + - 'inc/**' + - '.github/workflows/build-audiomoth-bin.yml' + pull_request: + paths: + - 'src/**' + - 'inc/**' + - '.github/workflows/build-audiomoth-bin.yml' + +permissions: + contents: read + +concurrency: + group: audiomoth-firmware-build-${{ github.ref }} + cancel-in-progress: true + +env: + AUDIOMOTH_PROJECT_REF: ${{ github.event.inputs.audiomoth_project_ref || 'master' }} + +jobs: + build-bin: + runs-on: ubuntu-latest + + steps: + - name: Check out firmware source at workflow ref + uses: actions/checkout@v4 + with: + path: firmware + fetch-depth: 1 + + - name: Show and validate source revision + shell: bash + run: | + set -euo pipefail + echo "Repository: ${GITHUB_REPOSITORY}" + echo "Workflow ref: ${GITHUB_REF}" + echo "Ref name: ${GITHUB_REF_NAME}" + echo "Commit: $(git -C firmware rev-parse HEAD)" + echo "AudioMoth-Project ref: ${AUDIOMOTH_PROJECT_REF}" + + required_files=( + firmware/src/main.c + firmware/src/espbridge.c + firmware/src/audioconfig.c + firmware/src/digitalfilter.c + firmware/src/gps.c + firmware/inc/espbridge.h + firmware/inc/audioconfig.h + firmware/inc/digitalfilter.h + firmware/inc/gps.h + ) + + for file in "${required_files[@]}"; do + test -f "$file" || { echo "Missing required firmware file: $file" >&2; exit 1; } + done + + grep -F 'static uint8_t firmwareVersion[AM_FIRMWARE_VERSION_LENGTH] = {1, 12, 0};' firmware/src/main.c + grep -F 'static uint8_t firmwareDescription[AM_FIRMWARE_DESCRIPTION_LENGTH] = "AudioMoth-Firmware-Basic";' firmware/src/main.c + grep -F '#define ESPBRIDGE_DEFAULT_BAUD 115200' firmware/inc/espbridge.h + grep -F 'sendLine("OK BRIDGE_READY")' firmware/src/espbridge.c + grep -F 'sendLine("OK PONG")' firmware/src/espbridge.c + + - name: Install ARM GCC and build tools + shell: bash + run: | + set -euo pipefail + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + git \ + make \ + gcc-arm-none-eabi \ + binutils-arm-none-eabi \ + libnewlib-arm-none-eabi + arm-none-eabi-gcc --version + arm-none-eabi-objcopy --version + + - name: Clone AudioMoth build framework + shell: bash + run: | + set -euo pipefail + git clone --depth 1 --branch "${AUDIOMOTH_PROJECT_REF}" https://github.com/OpenAcousticDevices/AudioMoth-Project.git project + echo "AudioMoth-Project commit: $(git -C project rev-parse HEAD)" + test -f project/build/Makefile + test -f project/build/audiomoth.ld + + - name: Overlay this repository into AudioMoth-Project + shell: bash + run: | + set -euo pipefail + find firmware/src -maxdepth 1 -type f -name '*.c' -print -exec cp -f {} project/src/ \; + find firmware/inc -maxdepth 1 -type f -name '*.h' -print -exec cp -f {} project/inc/ \; + + cmp -s firmware/src/main.c project/src/main.c + cmp -s firmware/src/espbridge.c project/src/espbridge.c + cmp -s firmware/inc/espbridge.h project/inc/espbridge.h + + echo "Overlay bridge snippets:" + grep -n 'ESPBRIDGE_DEFAULT_BAUD' project/inc/espbridge.h + grep -n 'OK BRIDGE_READY\|OK PONG\|OK DONE' project/src/espbridge.c + + - name: Configure AudioMoth Makefile for CI + shell: bash + run: | + set -euo pipefail + makefile=project/build/Makefile + sed -i 's|^TOOLCHAIN_PATH =.*|TOOLCHAIN_PATH = /usr|' "$makefile" + sed -i 's|^TOOLCHAIN_VERSION =.*|TOOLCHAIN_VERSION = |' "$makefile" + sed -i 's|^INC =.*|INC = ../cmsis ../device/inc ../emlib/inc ../emusb/inc ../drivers/inc ../fatfs/inc ../gps/inc ../inc|' "$makefile" + sed -i 's|^SRC =.*|SRC = ../device/src ../emlib/src ../emusb/src ../drivers/src ../fatfs/src ../gps/src ../src|' "$makefile" + grep -n '^TOOLCHAIN_PATH\|^TOOLCHAIN_VERSION\|^INC =\|^SRC =' "$makefile" + + - name: Build firmware binary + shell: bash + working-directory: project/build + run: | + set -euo pipefail + make clean + make -j"$(nproc)" audiomoth.bin + + - name: Verify binary and collect artifacts + shell: bash + run: | + set -euo pipefail + test -s project/build/audiomoth.bin + test -s project/build/audiomoth.hex + test -s project/build/audiomoth.map + test -s project/build/audiomoth.lst + + strings project/build/audiomoth.bin | grep -F 'AudioMoth-Firmware-Basic' + strings project/build/audiomoth.bin | grep -F 'OK BRIDGE_READY' + strings project/build/audiomoth.bin | grep -F 'OK PONG' + + mkdir -p firmware-artifacts + cp -f project/build/audiomoth.bin firmware-artifacts/audiomoth.bin + cp -f project/build/audiomoth.hex firmware-artifacts/audiomoth.hex + cp -f project/build/audiomoth.map firmware-artifacts/audiomoth.map + cp -f project/build/audiomoth.lst firmware-artifacts/audiomoth.lst + + { + echo "source_repository=${GITHUB_REPOSITORY}" + echo "source_ref=${GITHUB_REF}" + echo "source_ref_name=${GITHUB_REF_NAME}" + echo "source_sha=$(git -C firmware rev-parse HEAD)" + echo "audiomoth_project_ref=${AUDIOMOTH_PROJECT_REF}" + echo "audiomoth_project_sha=$(git -C project rev-parse HEAD)" + echo "firmware_description=AudioMoth-Firmware-Basic" + echo "firmware_version=1.12.0" + echo "espbridge_baud=115200" + date -u '+built_at_utc=%Y-%m-%dT%H:%M:%SZ' + } > firmware-artifacts/build-info.txt + + ls -lh firmware-artifacts/ + + - name: Upload firmware artifact + uses: actions/upload-artifact@v4 + with: + name: audiomoth-firmware-bin + path: firmware-artifacts/ + if-no-files-found: error diff --git a/README.md b/README.md index 2526668..c6bade5 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ Firmware for AudioMoth devices, used in conjunction with the AudioMoth-Project framework, to produce the standard AudioMoth releases. +This ESPBridge fork intentionally keeps the firmware name `AudioMoth-Firmware-Basic` so the AudioMoth Configuration App treats it like the standard Basic firmware. The ESP bridge changes live in the source files; do not rename the USB firmware description in `src/main.c`. + Compatible with the [AudioMoth Configuration App](https://github.com/OpenAcousticDevices/AudioMoth-Configuration-App). For usage instructions, visit [Open Acoustic Devices](https://www.openacousticdevices.info/getting-started). ### Usage #### diff --git a/inc/espbridge.h b/inc/espbridge.h new file mode 100644 index 0000000..bd4a021 --- /dev/null +++ b/inc/espbridge.h @@ -0,0 +1,36 @@ +/**************************************************************************** + * espbridge.h + * AudioMoth Dev <-> ESP32 bridge for SD-owned-by-AudioMoth uploads + * Drop-in bridge for AudioMoth-Firmware-Basic / AudioMoth-Project. + * + * AudioMoth owns the microSD at all times. The ESP32 requests file chunks + * over UART only while AudioMoth is outside its recording/preparation window. + *****************************************************************************/ + +#ifndef __ESPBRIDGE_H +#define __ESPBRIDGE_H + +#include +#include + +#define ESPBRIDGE_DEFAULT_BAUD 115200 +#define ESPBRIDGE_MAX_LINE 160 +#define ESPBRIDGE_MAX_PATH 96 +#define ESPBRIDGE_CHUNK_BYTES 512 +#define ESPBRIDGE_UPLOAD_GUARD_SECONDS 300 + +void ESPBridge_init(void); + +/* High means AudioMoth is recording/preparing/doing protected SD work. */ +void ESPBridge_setBusy(bool busy); + +/* True only when the main scheduler has enough time before the next recording. */ +void ESPBridge_setUploadAllowed(bool allowed); + +/* Reads the ESP request pin. */ +bool ESPBridge_isRequestActive(void); + +/* Services UART commands until deadlineUnixSeconds, request pin release, or idle timeout. */ +void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds); + +#endif /* __ESPBRIDGE_H */ diff --git a/src/espbridge.c b/src/espbridge.c new file mode 100644 index 0000000..749017b --- /dev/null +++ b/src/espbridge.c @@ -0,0 +1,454 @@ +/**************************************************************************** + * espbridge.c + * AudioMoth Dev <-> ESP32 upload bridge. + * + * Protocol is ASCII commands plus binary DATA payloads. AudioMoth owns the + * microSD at all times; the ESP32 requests files over UART only while the + * scheduler has opened a safe bridge window. + *****************************************************************************/ + +#include +#include +#include +#include +#include +#include + +#include "em_cmu.h" +#include "em_gpio.h" +#include "em_usart.h" +#include "em_wdog.h" + +#include "ff.h" +#include "audiomoth.h" +#include "espbridge.h" + +/* AudioMoth Dev left JST header. The b9/b10 U1 pins are UART1 route location 2. */ +#define BRIDGE_UART UART1 +#define BRIDGE_UART_CLOCK cmuClock_UART1 +#define BRIDGE_UART_LOCATION UART_ROUTE_LOCATION_LOC2 + +#define BRIDGE_TX_PORT gpioPortB /* b9, AudioMoth TX -> ESP RX */ +#define BRIDGE_TX_PIN 9 +#define BRIDGE_RX_PORT gpioPortB /* b10, AudioMoth RX <- ESP TX */ +#define BRIDGE_RX_PIN 10 + +#define BRIDGE_BUSY_PORT gpioPortA /* a8, AudioMoth output */ +#define BRIDGE_BUSY_PIN 8 +#define BRIDGE_REQ_PORT gpioPortA /* a7, ESP output to AudioMoth */ +#define BRIDGE_REQ_PIN 7 + +#define RX_LINE_TIMEOUT_MS 250 +#define SERVICE_IDLE_TIMEOUT_MS 3000 +#define SERVICE_MAX_WINDOW_MS 30000 +#define MILLISECONDS_PER_SECOND 1000 + +static volatile bool bridgeBusy = true; +static volatile bool uploadAllowed = false; +static bool filesystemEnabled = false; +static bool serviceActive = false; + +static char lineBuffer[ESPBRIDGE_MAX_LINE]; +static uint8_t chunkBuffer[ESPBRIDGE_CHUNK_BYTES]; + +/* ---------------- UART primitives ---------------- */ + +static inline void gpioWrite(GPIO_Port_TypeDef port, unsigned int pin, bool value) { + if (value) { + GPIO_PinOutSet(port, pin); + } else { + GPIO_PinOutClear(port, pin); + } +} + +static inline bool uartRxAvailable(void) { + return (USART_StatusGet(BRIDGE_UART) & UART_STATUS_RXDATAV) != 0; +} + +static uint8_t uartReadByte(void) { + return (uint8_t)USART_Rx(BRIDGE_UART); +} + +static void uartWriteByte(uint8_t byte) { + USART_Tx(BRIDGE_UART, byte); +} + +static void uartWrite(const void *data, uint32_t length) { + const uint8_t *p = (const uint8_t*)data; + for (uint32_t i = 0; i < length; i += 1) uartWriteByte(p[i]); +} + +static void sendLine(const char *fmt, ...) { + char out[192]; + va_list args; + va_start(args, fmt); + int n = vsnprintf(out, sizeof(out), fmt, args); + va_end(args); + if (n < 0) return; + if ((uint32_t)n >= sizeof(out)) n = sizeof(out) - 1; + uartWrite(out, (uint32_t)n); + uartWrite("\n", 1); +} + +/* Returns true when a complete line was read. CR is ignored. */ +static bool readLine(uint32_t timeoutMs) { + uint32_t index = 0; + uint32_t elapsed = 0; + + while (elapsed < timeoutMs && index < ESPBRIDGE_MAX_LINE - 1) { + WDOG_Feed(); + + if (uartRxAvailable()) { + char c = (char)uartReadByte(); + if (c == '\r') continue; + if (c == '\n') { + lineBuffer[index] = 0; + return index > 0; + } + lineBuffer[index++] = c; + } else { + AudioMoth_delay(1); + elapsed += 1; + } + } + + lineBuffer[index] = 0; + return false; +} + +/* ---------------- CRC and validation ---------------- */ + +static uint32_t crc32Update(uint32_t crc, const uint8_t *data, uint32_t length) { + crc = ~crc; + for (uint32_t i = 0; i < length; i += 1) { + crc ^= data[i]; + for (uint32_t j = 0; j < 8; j += 1) { + uint32_t mask = -(crc & 1U); + crc = (crc >> 1) ^ (0xEDB88320UL & mask); + } + } + return ~crc; +} + +static bool endsWithWav(const char *path) { + const char *dot = strrchr(path, '.'); + if (dot == NULL) return false; + return (dot[1] == 'W' || dot[1] == 'w') && + (dot[2] == 'A' || dot[2] == 'a') && + (dot[3] == 'V' || dot[3] == 'v') && + dot[4] == 0; +} + +static bool validPath(const char *path, bool requireWav) { + uint32_t len = strlen(path); + if (len == 0 || len >= ESPBRIDGE_MAX_PATH) return false; + if (path[0] == '/' || path[0] == '\\') return false; + if (strstr(path, "..") != NULL) return false; + + for (uint32_t i = 0; i < len; i += 1) { + char c = path[i]; + bool ok = isalnum((unsigned char)c) || c == '_' || c == '-' || c == '.' || c == '/'; + if (!ok) return false; + } + + if (requireWav && !endsWithWav(path)) return false; + return true; +} + +static bool ensureFilesystem(void) { + if (filesystemEnabled) return true; + filesystemEnabled = AudioMoth_enableFileSystem(AM_SD_CARD_NORMAL_SPEED); + return filesystemEnabled; +} + +static bool deadlineReached(uint32_t deadlineUnixSeconds) { + uint32_t now, ms; + AudioMoth_getTime(&now, &ms); + return now >= deadlineUnixSeconds; +} + +static uint32_t elapsedServiceMilliseconds(uint32_t startSeconds, uint32_t startMilliseconds) { + uint32_t now, milliseconds; + AudioMoth_getTime(&now, &milliseconds); + + if (now < startSeconds) return SERVICE_MAX_WINDOW_MS; + + uint32_t elapsedSeconds = now - startSeconds; + int32_t elapsedMilliseconds = (int32_t)milliseconds - (int32_t)startMilliseconds; + + if (elapsedMilliseconds < 0) { + if (elapsedSeconds == 0) return 0; + elapsedSeconds -= 1; + elapsedMilliseconds += MILLISECONDS_PER_SECOND; + } + + if (elapsedSeconds >= SERVICE_MAX_WINDOW_MS / MILLISECONDS_PER_SECOND + 1) return SERVICE_MAX_WINDOW_MS; + + uint32_t elapsed = elapsedSeconds * MILLISECONDS_PER_SECOND + (uint32_t)elapsedMilliseconds; + return elapsed > SERVICE_MAX_WINDOW_MS ? SERVICE_MAX_WINDOW_MS : elapsed; +} + +/* ---------------- File operations ---------------- */ + +static void listOneDirectory(const char *prefix) { + DIR dir; + FILINFO fno; + + FRESULT res = f_opendir(&dir, prefix[0] ? prefix : ""); + if (res != FR_OK) return; + + while (true) { + WDOG_Feed(); + res = f_readdir(&dir, &fno); + if (res != FR_OK || fno.fname[0] == 0) break; + + if (fno.fattrib & AM_DIR) { + /* Support one level of AudioMoth daily folders, e.g. 20260531/file.WAV. */ + char nested[ESPBRIDGE_MAX_PATH]; + if (prefix[0]) continue; + snprintf(nested, sizeof(nested), "%s", fno.fname); + + DIR subdir; + FILINFO subfno; + if (f_opendir(&subdir, nested) == FR_OK) { + while (true) { + FRESULT subres = f_readdir(&subdir, &subfno); + if (subres != FR_OK || subfno.fname[0] == 0) break; + if ((subfno.fattrib & AM_DIR) == 0 && endsWithWav(subfno.fname)) { + char full[ESPBRIDGE_MAX_PATH]; + snprintf(full, sizeof(full), "%s/%s", nested, subfno.fname); + sendLine("FILE %s %lu", full, (unsigned long)subfno.fsize); + } + } + f_closedir(&subdir); + } + } else if (endsWithWav(fno.fname)) { + sendLine("FILE %s %lu", fno.fname, (unsigned long)fno.fsize); + } + } + + f_closedir(&dir); +} + +static void commandList(void) { + if (bridgeBusy || !uploadAllowed) { + sendLine("ERR BUSY upload_not_allowed"); + return; + } + if (!ensureFilesystem()) { + sendLine("ERR SD filesystem_enable_failed"); + return; + } + + listOneDirectory(""); + sendLine("END"); +} + +static void commandGet(char *args) { + char path[ESPBRIDGE_MAX_PATH]; + unsigned long offset = 0; + unsigned long requested = ESPBRIDGE_CHUNK_BYTES; + + if (bridgeBusy || !uploadAllowed) { + sendLine("ERR BUSY upload_not_allowed"); + return; + } + if (sscanf(args, "%95s %lu %lu", path, &offset, &requested) < 2) { + sendLine("ERR ARG usage_GET_path_offset_maxbytes"); + return; + } + if (!validPath(path, true)) { + sendLine("ERR PATH invalid_path"); + return; + } + if (requested == 0 || requested > ESPBRIDGE_CHUNK_BYTES) requested = ESPBRIDGE_CHUNK_BYTES; + if (!ensureFilesystem()) { + sendLine("ERR SD filesystem_enable_failed"); + return; + } + + FIL file; + FRESULT res = f_open(&file, path, FA_READ); + if (res != FR_OK) { + sendLine("ERR OPEN %u", (unsigned int)res); + return; + } + + FSIZE_t size = f_size(&file); + if ((FSIZE_t)offset > size) { + f_close(&file); + sendLine("ERR RANGE offset_past_eof"); + return; + } + + res = f_lseek(&file, (FSIZE_t)offset); + if (res != FR_OK) { + f_close(&file); + sendLine("ERR SEEK %u", (unsigned int)res); + return; + } + + UINT bytesRead = 0; + res = f_read(&file, chunkBuffer, (UINT)requested, &bytesRead); + f_close(&file); + + if (res != FR_OK) { + sendLine("ERR READ %u", (unsigned int)res); + return; + } + + uint32_t crc = crc32Update(0, chunkBuffer, bytesRead); + sendLine("DATA %s %lu %u %08lX", path, offset, (unsigned int)bytesRead, (unsigned long)crc); + uartWrite(chunkBuffer, bytesRead); +} + +static void commandDelete(char *args) { + char path[ESPBRIDGE_MAX_PATH]; + + if (bridgeBusy || !uploadAllowed) { + sendLine("ERR BUSY upload_not_allowed"); + return; + } + + if (sscanf(args, "%95s", path) != 1) { + sendLine("ERR ARG usage_DELETE_path"); + return; + } + if (!validPath(path, true)) { + sendLine("ERR PATH invalid_path"); + return; + } + if (!ensureFilesystem()) { + sendLine("ERR SD filesystem_enable_failed"); + return; + } + + FRESULT res = f_unlink(path); + if (res == FR_OK) sendLine("OK DELETE %s", path); + else sendLine("ERR DELETE %u", (unsigned int)res); +} + +static void commandTime(char *args) { + unsigned long seconds = 0; + unsigned long milliseconds = 0; + if (sscanf(args, "%lu %lu", &seconds, &milliseconds) < 1) { + sendLine("ERR ARG usage_TIME_unix_seconds_milliseconds"); + return; + } + if (milliseconds > 999) milliseconds = 999; + AudioMoth_setTime((uint32_t)seconds, (uint32_t)milliseconds); + sendLine("OK TIME %lu %lu", seconds, milliseconds); +} + +static void commandStatus(uint32_t deadlineUnixSeconds) { + uint32_t now, ms; + AudioMoth_getTime(&now, &ms); + sendLine("OK STATUS busy=%u allowed=%u req=%u now=%lu ms=%lu deadline=%lu", + bridgeBusy ? 1 : 0, + uploadAllowed ? 1 : 0, + ESPBridge_isRequestActive() ? 1 : 0, + (unsigned long)now, + (unsigned long)ms, + (unsigned long)deadlineUnixSeconds); +} + +static bool handleCommand(uint32_t deadlineUnixSeconds) { + if (strcmp(lineBuffer, "PING") == 0) { + sendLine("OK PONG"); + } else if (strcmp(lineBuffer, "STATUS") == 0) { + commandStatus(deadlineUnixSeconds); + } else if (strncmp(lineBuffer, "TIME ", 5) == 0) { + commandTime(lineBuffer + 5); + } else if (strcmp(lineBuffer, "LIST") == 0) { + commandList(); + } else if (strncmp(lineBuffer, "GET ", 4) == 0) { + commandGet(lineBuffer + 4); + } else if (strncmp(lineBuffer, "DELETE ", 7) == 0) { + commandDelete(lineBuffer + 7); + } else if (strcmp(lineBuffer, "DONE") == 0) { + sendLine("OK DONE"); + return true; + } else { + sendLine("ERR CMD unknown_command"); + } + + return false; +} + +/* ---------------- Public API ---------------- */ + +void ESPBridge_init(void) { + CMU_ClockEnable(cmuClock_GPIO, true); + CMU_ClockEnable(BRIDGE_UART_CLOCK, true); + + GPIO_PinModeSet(BRIDGE_TX_PORT, BRIDGE_TX_PIN, gpioModePushPull, 1); + GPIO_PinModeSet(BRIDGE_RX_PORT, BRIDGE_RX_PIN, gpioModeInputPull, 1); + GPIO_PinModeSet(BRIDGE_BUSY_PORT, BRIDGE_BUSY_PIN, gpioModePushPull, 1); + GPIO_PinModeSet(BRIDGE_REQ_PORT, BRIDGE_REQ_PIN, gpioModeInputPull, 0); + + USART_InitAsync_TypeDef init = USART_INITASYNC_DEFAULT; + init.baudrate = ESPBRIDGE_DEFAULT_BAUD; + init.oversampling = usartOVS4; + USART_InitAsync(BRIDGE_UART, &init); + + BRIDGE_UART->ROUTE = UART_ROUTE_TXPEN | + UART_ROUTE_RXPEN | + BRIDGE_UART_LOCATION; + + bridgeBusy = true; + uploadAllowed = false; + filesystemEnabled = false; + serviceActive = false; +} + +void ESPBridge_setBusy(bool busy) { + bridgeBusy = busy; + gpioWrite(BRIDGE_BUSY_PORT, BRIDGE_BUSY_PIN, busy); +} + +void ESPBridge_setUploadAllowed(bool allowed) { + uploadAllowed = allowed; +} + +bool ESPBridge_isRequestActive(void) { + return GPIO_PinInGet(BRIDGE_REQ_PORT, BRIDGE_REQ_PIN) != 0; +} + +void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) { + if (bridgeBusy || serviceActive) return; + + serviceActive = true; + + uint32_t idleMs = 0; + uint32_t serviceStartSeconds, serviceStartMilliseconds; + AudioMoth_getTime(&serviceStartSeconds, &serviceStartMilliseconds); + + sendLine("OK BRIDGE_READY"); + + while (elapsedServiceMilliseconds(serviceStartSeconds, serviceStartMilliseconds) < SERVICE_MAX_WINDOW_MS) { + WDOG_Feed(); + + bool deadlineDone = deadlineReached(deadlineUnixSeconds); + bool requestActive = ESPBridge_isRequestActive(); + bool rxAvailable = uartRxAvailable(); + + if (deadlineDone && !requestActive && !rxAvailable) break; + + if (!requestActive && !rxAvailable) { + if (idleMs >= SERVICE_IDLE_TIMEOUT_MS) break; + AudioMoth_delay(10); + idleMs += 10; + continue; + } + + if (readLine(RX_LINE_TIMEOUT_MS)) { + idleMs = 0; + if (handleCommand(deadlineUnixSeconds)) break; + } else { + idleMs += RX_LINE_TIMEOUT_MS; + } + } + + sendLine("OK BRIDGE_SLEEP"); + serviceActive = false; +} diff --git a/src/main.c b/src/main.c index 0b810e3..5d54262 100644 --- a/src/main.c +++ b/src/main.c @@ -1,4343 +1,4443 @@ -/**************************************************************************** - * main.c - * openacousticdevices.info - * June 2017 - *****************************************************************************/ - -#include -#include -#include -#include -#include -#include - -#include "gps.h" -#include "sunrise.h" -#include "audiomoth.h" -#include "audioconfig.h" -#include "digitalfilter.h" - -/* Useful time constants */ - -#define MICROSECONDS_IN_SECOND 1000000 -#define MICROSECONDS_IN_MILLISECOND 1000 -#define MILLISECONDS_IN_SECOND 1000 - -#define SECONDS_IN_MINUTE 60 -#define SECONDS_IN_HOUR (60 * SECONDS_IN_MINUTE) -#define SECONDS_IN_DAY (24 * SECONDS_IN_HOUR) - -#define MINUTES_IN_HOUR 60 -#define MINUTES_IN_DAY 1440 -#define YEAR_OFFSET 1900 -#define MONTH_OFFSET 1 - -#define START_OF_CENTURY 946684800 -#define MIDPOINT_OF_CENTURY 2524608000 - -/* Useful coordinate constant */ - -#define MINUTES_IN_DEGREE 60 - -/* Useful type constants */ - -#define BITS_PER_BYTE 8 -#define UINT32_SIZE_IN_BITS 32 -#define UINT32_SIZE_IN_BYTES 4 -#define UINT16_SIZE_IN_BYTES 2 - -/* Sleep and LED constants */ - -#define LOW_BATTERY_LED_FLASHES 10 - -#define SHORT_LED_FLASH_DURATION 100 -#define LONG_LED_FLASH_DURATION 500 - -#define TRIGGERED_RECORDING_FLASH_DURATION 4 - -#define WAITING_LED_FLASH_DURATION 10 -#define WAITING_LED_FLASH_INTERVAL 2000 - -#define MINIMUM_LED_FLASH_INTERVAL 500 - -#define SHORT_WAIT_INTERVAL 100 -#define DEFAULT_WAIT_INTERVAL 1000 - -#define USB_CONFIGURATION_BLINK 400 - -/* SRAM buffer constants */ - -#define NUMBER_OF_BUFFERS 8 -#define NUMBER_OF_BYTES_IN_SAMPLE 2 -#define EXTERNAL_SRAM_SIZE_IN_SAMPLES (AM_EXTERNAL_SRAM_SIZE_IN_BYTES / NUMBER_OF_BYTES_IN_SAMPLE) -#define NUMBER_OF_SAMPLES_IN_BUFFER (EXTERNAL_SRAM_SIZE_IN_SAMPLES / NUMBER_OF_BUFFERS) - -/* DMA transfer constant */ - -#define MAXIMUM_SAMPLES_IN_DMA_TRANSFER 1024 - -/* Compression constant */ - -#define COMPRESSION_BUFFER_SIZE_IN_BYTES 512 - -/* File size constants */ - -#define MAXIMUM_FILE_NAME_LENGTH 64 - -#define MAXIMUM_WAV_DATA_SIZE (UINT32_MAX - 16 * 1024 * 1024) - -/* Configuration file constants */ - -#define CONFIG_BUFFER_LENGTH 512 -#define CONFIG_TIMEZONE_LENGTH 8 - -/* WAV header constants */ - -#define PCM_FORMAT 1 -#define RIFF_ID_LENGTH 4 -#define LENGTH_OF_ARTIST 32 -#define LENGTH_OF_COMMENT 384 - -/* USB configuration constant */ - -#define MAX_RECORDING_PERIODS 5 - -/* Digital filter constant */ - -#define FILTER_FREQ_MULTIPLIER 100 - -/* DC filter constants */ - -#define LOW_DC_BLOCKING_FREQ 8 -#define DEFAULT_DC_BLOCKING_FREQ 48 - -/* Supply voltage constant */ - -#define MINIMUM_SUPPLY_VOLTAGE 2800 - -/* Consecutive recording error constant */ - -#define MAX_CONSECUTIVE_RECORDING_ERRORS 5 - -/* Deployment ID constant */ - -#define DEPLOYMENT_ID_LENGTH 8 - -/* Acoustic location constant */ - -#define ACOUSTIC_LOCATION_SIZE_IN_BYTES 7 - -/* Audio configuration constants */ - -#define AUDIO_CONFIG_PULSE_INTERVAL 10 -#define AUDIO_CONFIG_TIME_CORRECTION 134 -#define AUDIO_CONFIG_TONE_TIMEOUT 250 -#define AUDIO_CONFIG_PACKETS_TIMEOUT 30000 - -/* GPS time setting constants */ - -#define GPS_MAXIMUM_DIFFERENCE SECONDS_IN_HOUR -#define GPS_INITIAL_TIME_SETTING_PERIOD 300 -#define GPS_DEFAULT_TIME_SETTING_PERIOD 300 -#define GPS_MIN_TIME_SETTING_PERIOD 30 -#define GPS_ADDITIONAL_POWER_INTERVAL 2 -#define GPS_TIME_SETTING_MARGIN 2 -#define GPS_FREQUENCY_PRECISION 1000 -#define GPS_MESSAGE_BUFFER_SIZE_IN_BYTES 128 -#define GPS_FILENAME "GPS.TXT" - -/* Magnetic switch constants */ - -#define MAGNETIC_SWITCH_WAIT_MULTIPLIER 2 -#define MAGNETIC_SWITCH_CHANGE_FLASHES 10 - -/* USB configuration constant */ - -#define USB_CONFIG_TIME_CORRECTION 26 - -/* Recording start time correction */ - -#define DELAY_CORRECTION_MICROSECONDS 80 - -/* Recording preparation constants */ - -#define MINIMUM_PREPARATION_PERIOD 500 -#define INITIAL_PREPARATION_PERIOD 1000 -#define MAXIMUM_PREPARATION_PERIOD 30000 - -#define DEFAULT_PREPARATION_PERIOD_BUFFER 2000 - -#define PREPARATION_PERIOD_SMOOTHING_FACTOR 4 -#define PREPARATION_PERIOD_LED_DIVIDER 4 - -/* Energy saver mode constant */ - -#define ENERGY_SAVER_SAMPLE_RATE_THRESHOLD 48000 - -/* Frequency trigger constants */ - -#define FREQUENCY_TRIGGER_WINDOW_MINIMUM 16 -#define FREQUENCY_TRIGGER_WINDOW_MAXIMUM 1024 - -/* Sunrise and sunset recording constants */ - -#define MINIMUM_SUN_RECORDING_GAP 60 -#define SUN_RECORDING_GAP_MULTIPLIER 4 - -/* Location constants */ - -#define ACOUSTIC_LONGITUDE_MULTIPLIER 2 - -#define CONFIG_LOCATION_PRECISION 100 -#define ACOUSTIC_LOCATION_PRECISION 1000000 -#define GPS_LOCATION_PRECISION 1000000 - -/* Useful macros */ - -#define FLASH_LED(led, duration) { \ - AudioMoth_set ## led ## LED(true); \ - AudioMoth_delay(duration); \ - AudioMoth_set ## led ## LED(false); \ -} - -#define FLASH_REPEAT_LED(led, repeats, duration) { \ - for (uint32_t i = 0; i < repeats; i += 1) { \ - AudioMoth_set ## led ## LED(true); \ - AudioMoth_delay(duration); \ - AudioMoth_set ## led ## LED(false); \ - AudioMoth_delay(duration); \ - } \ -} - -#define FLASH_LED_IF_ENABLED_AND_RETURN_ON_ERROR(fn) { \ - bool success = (fn); \ - if (success != true) { \ - if (enableLED) { \ - AudioMoth_setBothLED(false); \ - AudioMoth_delay(LONG_LED_FLASH_DURATION); \ - FLASH_LED(Both, LONG_LED_FLASH_DURATION) \ - } \ - return SDCARD_WRITE_ERROR; \ - } \ -} - -#define RETURN_BOOL_ON_ERROR(fn) { \ - bool success = (fn); \ - if (success != true) { \ - return success; \ - } \ -} - -#define SAVE_SWITCH_POSITION_AND_POWER_DOWN(milliseconds) { \ - *previousSwitchPosition = switchPosition; \ - AudioMoth_powerDownAndWakeMilliseconds(milliseconds); \ -} - -#define SERIAL_NUMBER "%08X%08X" - -#define FORMAT_SERIAL_NUMBER(src) (unsigned int)*((uint32_t*)src + 1), (unsigned int)*((uint32_t*)src) - -#define ABS(a) ((a) < (0) ? (-a) : (a)) - -#define MIN(a, b) ((a) < (b) ? (a) : (b)) - -#define MAX(a, b) ((a) > (b) ? (a) : (b)) - -#define ROUNDED_DIV(a, b) (((a) + ((b)/2)) / (b)) - -#define ROUNDED_UP_DIV(a, b) (((a) + (b) - 1) / (b)) - -#define ROUND_UP_TO_MULTIPLE(a, b) (((a) + (b) - 1) & ~((b)-1)) - -#define UNSIGNED_ROUND(n, d) ((d) * (((n) + (d)/2) / (d))) - -/* Recording state enumeration */ - -typedef enum {RECORDING_OKAY, FILE_SIZE_LIMITED, SUPPLY_VOLTAGE_LOW, SWITCH_CHANGED, MICROPHONE_CHANGED, MAGNETIC_SWITCH, SDCARD_WRITE_ERROR} AM_recordingState_t; - -/* Filter type enumeration */ - -typedef enum {NO_FILTER, LOW_PASS_FILTER, BAND_PASS_FILTER, HIGH_PASS_FILTER} AM_filterType_t; - -/* Battery level display type */ - -typedef enum {BATTERY_LEVEL, NIMH_LIPO_BATTERY_VOLTAGE} AM_batteryLevelDisplayType_t; - -/* Sun recording mode enumeration */ - -typedef enum {SUNRISE_RECORDING, SUNSET_RECORDING, SUNRISE_AND_SUNSET_RECORDING, SUNSET_TO_SUNRISE_RECORDING, SUNRISE_TO_SUNSET_RECORDING} AM_sunRecordingMode_t; - -/* Sun recording mode enumeration */ - -typedef enum {INITIAL_GPS_FIX, GPS_FIX_BEFORE_RECORDING_PERIOD, GPS_FIX_BETWEEN_RECORDING_PERIODS, GPS_FIX_AFTER_RECORDING_PERIOD, GPS_FIX_BEFORE_INDIVIDUAL_RECORDING, GPS_FIX_BETWEEN_INDIVIDUAL_RECORDINGS, GPS_FIX_AFTER_INDIVIDUAL_RECORDING} AM_gpsFixMode_t; - -/* WAV header */ - -#pragma pack(push, 1) - -typedef struct { - char id[RIFF_ID_LENGTH]; - uint32_t size; -} chunk_t; - -typedef struct { - chunk_t icmt; - char comment[LENGTH_OF_COMMENT]; -} icmt_t; - -typedef struct { - chunk_t iart; - char artist[LENGTH_OF_ARTIST]; -} iart_t; - -typedef struct { - uint16_t format; - uint16_t numberOfChannels; - uint32_t samplesPerSecond; - uint32_t bytesPerSecond; - uint16_t bytesPerCapture; - uint16_t bitsPerSample; -} wavFormat_t; - -typedef struct { - chunk_t riff; - char format[RIFF_ID_LENGTH]; - chunk_t fmt; - wavFormat_t wavFormat; - chunk_t list; - char info[RIFF_ID_LENGTH]; - icmt_t icmt; - iart_t iart; - chunk_t data; -} wavHeader_t; - -#pragma pack(pop) - -static wavHeader_t wavHeader = { - .riff = {.id = "RIFF", .size = 0}, - .format = "WAVE", - .fmt = {.id = "fmt ", .size = sizeof(wavFormat_t)}, - .wavFormat = {.format = PCM_FORMAT, .numberOfChannels = 1, .samplesPerSecond = 0, .bytesPerSecond = 0, .bytesPerCapture = 2, .bitsPerSample = 16}, - .list = {.id = "LIST", .size = RIFF_ID_LENGTH + sizeof(icmt_t) + sizeof(iart_t)}, - .info = "INFO", - .icmt = {.icmt.id = "ICMT", .icmt.size = LENGTH_OF_COMMENT, .comment = ""}, - .iart = {.iart.id = "IART", .iart.size = LENGTH_OF_ARTIST, .artist = ""}, - .data = {.id = "data", .size = 0} -}; - -/* USB configuration data structure */ - -#pragma pack(push, 1) - -typedef struct { - uint16_t startMinutes; - uint16_t endMinutes; -} recordingPeriod_t; - -typedef struct { - uint32_t time; - AM_gainSetting_t gain; - uint8_t clockDivider; - uint8_t acquisitionCycles; - uint8_t oversampleRate; - uint32_t sampleRate; - uint8_t sampleRateDivider; - uint16_t sleepDuration; - uint16_t recordDuration; - uint8_t enableLED; - union { - struct { - uint8_t activeRecordingPeriods; - recordingPeriod_t recordingPeriods[MAX_RECORDING_PERIODS]; - }; - struct { - uint8_t sunRecordingMode : 3; - uint8_t sunRecordingEvent : 2; - int16_t latitude; - int16_t longitude; - uint8_t sunRoundingMinutes; - uint16_t beforeSunriseMinutes : 10; - uint16_t afterSunriseMinutes : 10; - uint16_t beforeSunsetMinutes : 10; - uint16_t afterSunsetMinutes : 10; - }; - }; - int8_t timezoneHours; - uint8_t enableLowVoltageCutoff; - uint8_t disableBatteryLevelDisplay : 1; - uint8_t requireAcousticLocation : 1; - uint8_t useTimezoneFromAcousticChime: 1; - uint8_t adjustScheduleUsingTimezoneFromAcousticChime: 1; - uint8_t preparationPeriodBuffer: 4; - int8_t timezoneMinutes; - uint8_t disableSleepRecordCycle : 1; - uint8_t enableFilenameWithDeviceID : 1; - uint8_t enableTimeSettingBeforeAndAfterRecordings: 1; - uint8_t gpsTimeSettingPeriod: 4; - uint8_t ignoreExternalMicrophoneForAcousticChime: 1; - uint32_t earliestRecordingTime; - uint32_t latestRecordingTime; - uint16_t lowerFilterFreq; - uint16_t higherFilterFreq; - union { - uint16_t amplitudeThreshold; - uint16_t frequencyTriggerCentreFrequency; - }; - uint8_t requireAcousticConfiguration : 1; - AM_batteryLevelDisplayType_t batteryLevelDisplayType : 1; - uint8_t minimumTriggerDuration : 6; - union { - struct { - uint8_t frequencyTriggerWindowLengthShift : 4; - uint8_t frequencyTriggerThresholdPercentageMantissa : 4; - int8_t frequencyTriggerThresholdPercentageExponent : 3; - }; - struct { - uint8_t enableAmplitudeThresholdDecibelScale : 1; - uint8_t amplitudeThresholdDecibels : 7; - uint8_t enableAmplitudeThresholdPercentageScale : 1; - uint8_t amplitudeThresholdPercentageMantissa : 4; - int8_t amplitudeThresholdPercentageExponent : 3; - }; - }; - uint8_t enableEnergySaverMode : 1; - uint8_t disable48HzDCBlockingFilter : 1; - uint8_t enableTimeSettingFromGPS : 1; - uint8_t enableMagneticSwitch : 1; - uint8_t enableLowGainRange : 1; - uint8_t enableFrequencyTrigger : 1; - uint8_t enableDailyFolders : 1; - uint8_t enableSunRecording : 1; -} configSettings_t; - -#pragma pack(pop) - -static const configSettings_t defaultConfigSettings = { - .time = 0, - .gain = AM_GAIN_MEDIUM, - .clockDivider = 4, - .acquisitionCycles = 16, - .oversampleRate = 1, - .sampleRate = 384000, - .sampleRateDivider = 8, - .sleepDuration = 5, - .recordDuration = 55, - .enableLED = 1, - .activeRecordingPeriods = 1, - .recordingPeriods = { - {.startMinutes = 0, .endMinutes = 0}, - {.startMinutes = 0, .endMinutes = 0}, - {.startMinutes = 0, .endMinutes = 0}, - {.startMinutes = 0, .endMinutes = 0}, - {.startMinutes = 0, .endMinutes = 0} - }, - .timezoneHours = 0, - .enableLowVoltageCutoff = 1, - .disableBatteryLevelDisplay = 0, - .requireAcousticLocation = 0, - .useTimezoneFromAcousticChime = 0, - .adjustScheduleUsingTimezoneFromAcousticChime = 0, - .preparationPeriodBuffer = 2, - .timezoneMinutes = 0, - .disableSleepRecordCycle = 0, - .enableFilenameWithDeviceID = 0, - .enableTimeSettingBeforeAndAfterRecordings = 0, - .gpsTimeSettingPeriod = 0, - .ignoreExternalMicrophoneForAcousticChime = 0, - .earliestRecordingTime = 0, - .latestRecordingTime = 0, - .lowerFilterFreq = 0, - .higherFilterFreq = 0, - .amplitudeThreshold = 0, - .requireAcousticConfiguration = 0, - .batteryLevelDisplayType = BATTERY_LEVEL, - .minimumTriggerDuration = 0, - .enableAmplitudeThresholdDecibelScale = 0, - .amplitudeThresholdDecibels = 0, - .enableAmplitudeThresholdPercentageScale = 0, - .amplitudeThresholdPercentageMantissa = 0, - .amplitudeThresholdPercentageExponent = 0, - .enableEnergySaverMode = 0, - .disable48HzDCBlockingFilter = 0, - .enableTimeSettingFromGPS = 0, - .enableMagneticSwitch = 0, - .enableLowGainRange = 0, - .enableFrequencyTrigger = 0, - .enableDailyFolders = 0, - .enableSunRecording = 0 -}; - -/* Persistent configuration data structure */ - -#pragma pack(push, 1) - -typedef struct { - uint8_t firmwareVersion[AM_FIRMWARE_VERSION_LENGTH]; - uint8_t firmwareDescription[AM_FIRMWARE_DESCRIPTION_LENGTH]; - configSettings_t configSettings; -} persistentConfigSettings_t; - -#pragma pack(pop) - -/* Acoustic location data structure */ - -#pragma pack(push, 1) - -typedef struct { - int32_t latitude: 28; - int32_t longitude: 28; -} acousticLocation_t; - -#pragma pack(pop) - -/* Function to select energy saver mode */ - -static bool isEnergySaverMode(configSettings_t *configSettings) { - - return configSettings->enableEnergySaverMode && configSettings->sampleRate / configSettings->sampleRateDivider <= ENERGY_SAVER_SAMPLE_RATE_THRESHOLD; - -} - -/* Functions to format header and configuration components */ - -static uint32_t formatDecibels(char *dest, uint32_t value, bool space) { - - if (value > 0) return sprintf(dest, space ? "-%lu dB" : "-%ludB", value); - - return sprintf(dest, space ? "0 dB" : "0dB"); - -} - -static uint32_t formatPercentage(char *dest, uint32_t mantissa, int32_t exponent) { - - uint32_t length = exponent < 0 ? 1 - exponent : 0; - - memcpy(dest, "0.0000", length); - - length += sprintf(dest + length, "%lu", mantissa); - - while (exponent-- > 0) dest[length++] = '0'; - - dest[length++] = '%'; - - return length; - -} - -/* Functions to set WAV header details and comment */ - -static void setHeaderDetails(wavHeader_t *wavHeader, uint32_t sampleRate, uint32_t numberOfSamples, uint32_t guanoDataSize) { - - wavHeader->wavFormat.samplesPerSecond = sampleRate; - wavHeader->wavFormat.bytesPerSecond = NUMBER_OF_BYTES_IN_SAMPLE * sampleRate; - wavHeader->data.size = NUMBER_OF_BYTES_IN_SAMPLE * numberOfSamples; - wavHeader->riff.size = NUMBER_OF_BYTES_IN_SAMPLE * numberOfSamples + sizeof(wavHeader_t) + guanoDataSize - sizeof(chunk_t); - -} - -static void setHeaderComment(wavHeader_t *wavHeader, configSettings_t *configSettings, uint32_t currentTime, uint8_t *serialNumber, uint8_t *deploymentID, uint8_t *defaultDeploymentID, AM_extendedBatteryState_t extendedBatteryState, int32_t temperature, bool externalMicrophone, AM_recordingState_t recordingState, AM_filterType_t filterType) { - - struct tm time; - - int32_t timezoneHours, timezoneMinutes; - - AudioMoth_timezoneRequested(&timezoneHours, &timezoneMinutes); - - time_t rawTime = currentTime + timezoneHours * SECONDS_IN_HOUR + timezoneMinutes * SECONDS_IN_MINUTE; - - gmtime_r(&rawTime, &time); - - /* Format artist field */ - - char *artist = wavHeader->iart.artist; - - sprintf(artist, "AudioMoth " SERIAL_NUMBER, FORMAT_SERIAL_NUMBER(serialNumber)); - - /* Clear comment field */ - - char *comment = wavHeader->icmt.comment; - - memset(comment, 0, LENGTH_OF_COMMENT); - - /* Format comment field */ - - comment += sprintf(comment, "Recorded at %02d:%02d:%02d %02d/%02d/%04d (UTC", time.tm_hour, time.tm_min, time.tm_sec, time.tm_mday, MONTH_OFFSET + time.tm_mon, YEAR_OFFSET + time.tm_year); - - if (timezoneHours < 0) { - - comment += sprintf(comment, "%ld", timezoneHours); - - } else if (timezoneHours > 0) { - - comment += sprintf(comment, "+%ld", timezoneHours); - - } else { - - if (timezoneMinutes < 0) comment += sprintf(comment, "-%ld", timezoneHours); - - if (timezoneMinutes > 0) comment += sprintf(comment, "+%ld", timezoneHours); - - } - - if (timezoneMinutes < 0) comment += sprintf(comment, ":%02ld", -timezoneMinutes); - - if (timezoneMinutes > 0) comment += sprintf(comment, ":%02ld", timezoneMinutes); - - if (memcmp(deploymentID, defaultDeploymentID, DEPLOYMENT_ID_LENGTH)) { - - comment += sprintf(comment, ") during deployment " SERIAL_NUMBER " ", FORMAT_SERIAL_NUMBER(deploymentID)); - - } else { - - comment += sprintf(comment, ") by %s ", artist); - - } - - if (externalMicrophone) { - - comment += sprintf(comment, "using external microphone "); - - } - - static char *gainSettings[5] = {"low", "low-medium", "medium", "medium-high", "high"}; - - comment += sprintf(comment, "at %s gain while battery was ", gainSettings[configSettings->gain]); - - if (extendedBatteryState == AM_EXT_BAT_LOW) { - - comment += sprintf(comment, "less than 2.5V"); - - } else if (extendedBatteryState >= AM_EXT_BAT_FULL) { - - comment += sprintf(comment, "greater than 4.9V"); - - } else { - - uint32_t batteryVoltage = extendedBatteryState + AM_EXT_BAT_STATE_OFFSET / AM_BATTERY_STATE_INCREMENT; - - comment += sprintf(comment, "%01lu.%01luV", batteryVoltage / 10, batteryVoltage % 10); - - } - - char *temperatureSign = temperature < 0 ? "-" : ""; - - uint32_t temperatureInDecidegrees = ROUNDED_DIV(ABS(temperature), 100); - - comment += sprintf(comment, " and temperature was %s%lu.%luC.", temperatureSign, temperatureInDecidegrees / 10, temperatureInDecidegrees % 10); - - bool frequencyTriggerEnabled = configSettings->enableFrequencyTrigger; - - bool amplitudeThresholdEnabled = frequencyTriggerEnabled ? false : configSettings->amplitudeThreshold > 0 || configSettings->enableAmplitudeThresholdDecibelScale || configSettings->enableAmplitudeThresholdPercentageScale; - - if (frequencyTriggerEnabled) { - - comment += sprintf(comment, " Frequency trigger (%u.%ukHz and window length of %u samples) threshold was ", configSettings->frequencyTriggerCentreFrequency / 10, configSettings->frequencyTriggerCentreFrequency % 10, (0x01 << configSettings->frequencyTriggerWindowLengthShift)); - - comment += formatPercentage(comment, configSettings->frequencyTriggerThresholdPercentageMantissa, configSettings->frequencyTriggerThresholdPercentageExponent); - - comment += sprintf(comment, " with %us minimum trigger duration.", configSettings->minimumTriggerDuration); - - } - - uint16_t lowerFilterFreq = configSettings->lowerFilterFreq; - - uint16_t higherFilterFreq = configSettings->higherFilterFreq; - - if (filterType == LOW_PASS_FILTER) { - - comment += sprintf(comment, " Low-pass filter with frequency of %01u.%01ukHz applied.", higherFilterFreq / 10, higherFilterFreq % 10); - - } else if (filterType == BAND_PASS_FILTER) { - - comment += sprintf(comment, " Band-pass filter with frequencies of %01u.%01ukHz and %01u.%01ukHz applied.", lowerFilterFreq / 10, lowerFilterFreq % 10, higherFilterFreq / 10, higherFilterFreq % 10); - - } else if (filterType == HIGH_PASS_FILTER) { - - comment += sprintf(comment, " High-pass filter with frequency of %01u.%01ukHz applied.", lowerFilterFreq / 10, lowerFilterFreq % 10); - - } - - if (amplitudeThresholdEnabled) { - - comment += sprintf(comment, " Amplitude threshold was "); - - if (configSettings->enableAmplitudeThresholdDecibelScale && configSettings->enableAmplitudeThresholdPercentageScale == false) { - - comment += formatDecibels(comment, configSettings->amplitudeThresholdDecibels, true); - - } else if (configSettings->enableAmplitudeThresholdPercentageScale && configSettings->enableAmplitudeThresholdDecibelScale == false) { - - comment += formatPercentage(comment, configSettings->amplitudeThresholdPercentageMantissa, configSettings->amplitudeThresholdPercentageExponent); - - } else { - - comment += sprintf(comment, "%u", configSettings->amplitudeThreshold); - - } - - comment += sprintf(comment, " with %us minimum trigger duration.", configSettings->minimumTriggerDuration); - - } - - if (recordingState != RECORDING_OKAY) { - - comment += sprintf(comment, " Recording stopped"); - - if (recordingState == MICROPHONE_CHANGED) { - - comment += sprintf(comment, " due to microphone change."); - - } else if (recordingState == SWITCH_CHANGED) { - - comment += sprintf(comment, " due to switch position change."); - - } else if (recordingState == MAGNETIC_SWITCH) { - - comment += sprintf(comment, " by magnetic switch."); - - } else if (recordingState == SUPPLY_VOLTAGE_LOW) { - - comment += sprintf(comment, " due to low voltage."); - - } else if (recordingState == FILE_SIZE_LIMITED) { - - comment += sprintf(comment, " due to file size limit."); - - } else if (recordingState == SDCARD_WRITE_ERROR) { - - comment += sprintf(comment, " due to SD card write error."); - - } - - } - -} - -/* Function to write the GUANO data */ - -static uint32_t writeGuanoData(char *buffer, configSettings_t *configSettings, uint32_t currentTime, bool gpsLocationReceived, int32_t *gpsLastFixLatitude, int32_t *gpsLastFixLongitude, bool acousticLocationReceived, int32_t *acousticLatitude, int32_t *acousticLongitude, uint8_t *firmwareDescription, uint8_t *firmwareVersion, uint8_t *serialNumber, uint8_t *deploymentID, uint8_t *defaultDeploymentID, char *filename, AM_extendedBatteryState_t extendedBatteryState, int32_t temperature, AM_filterType_t filterType) { - - uint32_t length = sprintf(buffer, "guan") + UINT32_SIZE_IN_BYTES; - - /* General information */ - - length += sprintf(buffer + length, "GUANO|Version:1.0\nMake:Open Acoustic Devices\nModel:AudioMoth\nSerial:" SERIAL_NUMBER "\n", FORMAT_SERIAL_NUMBER(serialNumber)); - - if (memcmp(deploymentID, defaultDeploymentID, DEPLOYMENT_ID_LENGTH)) { - - length += sprintf(buffer + length, "OAD|Deployment ID:" SERIAL_NUMBER "\n", FORMAT_SERIAL_NUMBER(deploymentID)); - - } - - length += sprintf(buffer + length, "Firmware Version:%s (%u.%u.%u)\n", firmwareDescription, firmwareVersion[0], firmwareVersion[1], firmwareVersion[2]); - - /* Timestamp */ - - int32_t timezoneHours, timezoneMinutes; - - AudioMoth_timezoneRequested(&timezoneHours, &timezoneMinutes); - - int32_t timezoneSeconds = timezoneHours * SECONDS_IN_HOUR + timezoneMinutes * SECONDS_IN_MINUTE; - - time_t rawTime = currentTime + timezoneSeconds; - - struct tm time; - - gmtime_r(&rawTime, &time); - - length += sprintf(buffer + length, "Timestamp:%04d-%02d-%02dT%02d:%02d:%02d", YEAR_OFFSET + time.tm_year, MONTH_OFFSET + time.tm_mon, time.tm_mday, time.tm_hour, time.tm_min, time.tm_sec); - - if (timezoneSeconds == 0) { - - length += sprintf(buffer + length, "Z\n"); - - } else if (timezoneSeconds < 0) { - - length += sprintf(buffer + length, "-%02ld:%02ld\n", ABS(timezoneHours), ABS(timezoneMinutes)); - - } else { - - length += sprintf(buffer + length, "+%02ld:%02ld\n", timezoneHours, timezoneMinutes); - - } - - /* Location position and source */ - - if (gpsLocationReceived || acousticLocationReceived || configSettings->enableSunRecording) { - - int32_t latitude = gpsLocationReceived ? *gpsLastFixLatitude : acousticLocationReceived ? *acousticLatitude : configSettings->latitude; - - int32_t longitude = gpsLocationReceived ? *gpsLastFixLongitude : acousticLocationReceived ? *acousticLongitude : configSettings->longitude; - - char *latitudeSign = latitude < 0 ? "-" : ""; - - char *longitudeSign = longitude < 0 ? "-" : ""; - - if (gpsLocationReceived) { - - length += sprintf(buffer + length, "Loc Position:%s%ld.%06ld %s%ld.%06ld\nOAD|Loc Source:GPS\n", latitudeSign, ABS(latitude) / GPS_LOCATION_PRECISION, ABS(latitude) % GPS_LOCATION_PRECISION, longitudeSign, ABS(longitude) / GPS_LOCATION_PRECISION, ABS(longitude) % GPS_LOCATION_PRECISION); - - } else if (acousticLocationReceived) { - - length += sprintf(buffer + length, "Loc Position:%s%ld.%06ld %s%ld.%06ld\nOAD|Loc Source:Acoustic chime\n", latitudeSign, ABS(latitude) / ACOUSTIC_LOCATION_PRECISION, ABS(latitude) % ACOUSTIC_LOCATION_PRECISION, longitudeSign, ABS(longitude) / ACOUSTIC_LOCATION_PRECISION, ABS(longitude) % ACOUSTIC_LOCATION_PRECISION); - - } else { - - length += sprintf(buffer + length, "Loc Position:%s%ld.%02ld %s%ld.%02ld\nOAD|Loc Source:Configuration app\n", latitudeSign, ABS(latitude) / CONFIG_LOCATION_PRECISION, ABS(latitude) % CONFIG_LOCATION_PRECISION, longitudeSign, ABS(longitude) / CONFIG_LOCATION_PRECISION, ABS(longitude) % CONFIG_LOCATION_PRECISION); - - } - - } - - /* Filename */ - - char *start = strchr(filename, '/'); - - length += sprintf(buffer + length, "Original Filename:%s\n", start ? start + 1 : filename); - - /* Recording settings */ - - length += sprintf(buffer + length, "OAD|Recording Settings:%lu GAIN %u", configSettings->sampleRate / configSettings->sampleRateDivider, configSettings->gain); - - bool frequencyTriggerEnabled = configSettings->enableFrequencyTrigger; - - bool amplitudeThresholdEnabled = frequencyTriggerEnabled ? false : configSettings->amplitudeThreshold > 0 || configSettings->enableAmplitudeThresholdDecibelScale || configSettings->enableAmplitudeThresholdPercentageScale; - - if (frequencyTriggerEnabled) { - - length += sprintf(buffer + length, " FREQ %u %u ", FILTER_FREQ_MULTIPLIER * configSettings->frequencyTriggerCentreFrequency, 0x01 << configSettings->frequencyTriggerWindowLengthShift); - - length += formatPercentage(buffer + length, configSettings->frequencyTriggerThresholdPercentageMantissa, configSettings->frequencyTriggerThresholdPercentageExponent); - - length += sprintf(buffer + length, " %u", configSettings->minimumTriggerDuration); - - } - - uint32_t lowerFilterFreq = FILTER_FREQ_MULTIPLIER * configSettings->lowerFilterFreq; - - uint32_t higherFilterFreq = FILTER_FREQ_MULTIPLIER * configSettings->higherFilterFreq; - - if (filterType == LOW_PASS_FILTER) { - - length += sprintf(buffer + length, " LPF %lu", higherFilterFreq); - - } else if (filterType == BAND_PASS_FILTER) { - - length += sprintf(buffer + length, " BPF %lu %lu", lowerFilterFreq, higherFilterFreq); - - } else if (filterType == HIGH_PASS_FILTER) { - - length += sprintf(buffer + length, " HPF %lu", lowerFilterFreq); - - } - - if (amplitudeThresholdEnabled) { - - length += sprintf(buffer + length, " AMP "); - - if (configSettings->enableAmplitudeThresholdDecibelScale && configSettings->enableAmplitudeThresholdPercentageScale == false) { - - length += formatDecibels(buffer + length, configSettings->amplitudeThresholdDecibels, false); - - } else if (configSettings->enableAmplitudeThresholdPercentageScale && configSettings->enableAmplitudeThresholdDecibelScale == false) { - - length += formatPercentage(buffer + length, configSettings->amplitudeThresholdPercentageMantissa, configSettings->amplitudeThresholdPercentageExponent); - - } else { - - length += sprintf(buffer + length, "%u", configSettings->amplitudeThreshold); - - } - - length += sprintf(buffer + length, " %u", configSettings->minimumTriggerDuration); - - } - - if (configSettings->enableLowGainRange) length += sprintf(buffer + length, " LGR"); - - if (configSettings->disable48HzDCBlockingFilter) length += sprintf(buffer + length, " D48"); - - if (isEnergySaverMode(configSettings)) length += sprintf(buffer + length, " ESM"); - - /* Battery and temperature */ - - uint32_t batteryVoltage = extendedBatteryState == AM_EXT_BAT_LOW ? 24 : extendedBatteryState >= AM_EXT_BAT_FULL ? 50 : extendedBatteryState + AM_EXT_BAT_STATE_OFFSET / AM_BATTERY_STATE_INCREMENT; - - length += sprintf(buffer + length, "\nOAD|Battery Voltage:%01lu.%01lu\n", batteryVoltage / 10, batteryVoltage % 10); - - char *temperatureSign = temperature < 0 ? "-" : ""; - - uint32_t temperatureInDecidegrees = ROUNDED_DIV(ABS(temperature), 100); - - length += sprintf(buffer + length, "Temperature Int:%s%lu.%lu", temperatureSign, temperatureInDecidegrees / 10, temperatureInDecidegrees % 10); - - /* Set GUANO chunk size */ - - *(uint32_t*)(buffer + RIFF_ID_LENGTH) = length - sizeof(chunk_t);; - - return length; - -} - -/* Function to write configuration to file */ - -static bool writeConfigurationToFile(char *buffer, configSettings_t *configSettings, uint32_t currentTime, bool gpsLocationReceived, int32_t *gpsLatitude, int32_t *gpsLongitude, bool acousticLocationReceived, int32_t *acousticLatitude, int32_t *acousticLongitude, uint8_t *firmwareDescription, uint8_t *firmwareVersion, uint8_t *serialNumber, uint8_t *deploymentID, uint8_t *defaultDeploymentID) { - - static char timezoneBuffer[CONFIG_TIMEZONE_LENGTH]; - - int32_t timezoneHours, timezoneMinutes; - - AudioMoth_timezoneRequested(&timezoneHours, &timezoneMinutes); - - int32_t timezoneSeconds = timezoneHours * SECONDS_IN_HOUR + timezoneMinutes * SECONDS_IN_MINUTE; - - RETURN_BOOL_ON_ERROR(AudioMoth_openFile("CONFIG.TXT")); - - uint32_t length = sprintf(buffer, "Device ID : " SERIAL_NUMBER "\r\n", FORMAT_SERIAL_NUMBER(serialNumber)); - - length += sprintf(buffer + length, "Firmware : %s (%u.%u.%u)\r\n\r\n", firmwareDescription, firmwareVersion[0], firmwareVersion[1], firmwareVersion[2]); - - if (memcmp(deploymentID, defaultDeploymentID, DEPLOYMENT_ID_LENGTH)) { - - length += sprintf(buffer + length, "Deployment ID : " SERIAL_NUMBER "\r\n\r\n", FORMAT_SERIAL_NUMBER(deploymentID)); - - } - - uint32_t timezoneLength = sprintf(timezoneBuffer, "UTC"); - - if (timezoneHours < 0) { - - timezoneLength += sprintf(timezoneBuffer + timezoneLength, "%ld", timezoneHours); - - } else if (timezoneHours > 0) { - - timezoneLength += sprintf(timezoneBuffer + timezoneLength, "+%ld", timezoneHours); - - } else { - - if (timezoneMinutes < 0) timezoneLength += sprintf(timezoneBuffer + timezoneLength, "-%ld", timezoneHours); - - if (timezoneMinutes > 0) timezoneLength += sprintf(timezoneBuffer + timezoneLength, "+%ld", timezoneHours); - - } - - if (timezoneMinutes < 0) timezoneLength += sprintf(timezoneBuffer + timezoneLength, ":%02ld", -timezoneMinutes); - - if (timezoneMinutes > 0) timezoneLength += sprintf(timezoneBuffer + timezoneLength, ":%02ld", timezoneMinutes); - - time_t rawTime = currentTime + timezoneSeconds; - - struct tm time; - - gmtime_r(&rawTime, &time); - - length += sprintf(buffer + length, "Device time : %04d-%02d-%02d %02d:%02d:%02d (%s)", YEAR_OFFSET + time.tm_year, MONTH_OFFSET + time.tm_mon, time.tm_mday, time.tm_hour, time.tm_min, time.tm_sec, timezoneBuffer); - - RETURN_BOOL_ON_ERROR(AudioMoth_writeToFile(buffer, length)); - - length = sprintf(buffer, "\r\n\r\nSample rate (Hz) : %lu\r\n", configSettings->sampleRate / configSettings->sampleRateDivider); - - static char *gainSettings[5] = {"Low", "Low-Medium", "Medium", "Medium-High", "High"}; - - length += sprintf(buffer + length, "Gain : %s\r\n\r\n", gainSettings[configSettings->gain]); - - length += sprintf(buffer + length, "Sleep duration (s) : "); - - if (configSettings->disableSleepRecordCycle) { - - length += sprintf(buffer + length, "-"); - - } else { - - length += sprintf(buffer + length, "%u", configSettings->sleepDuration); - - } - - length += sprintf(buffer + length, "\r\nRecording duration (s) : "); - - if (configSettings->disableSleepRecordCycle) { - - length += sprintf(buffer + length, "-"); - - } else { - - length += sprintf(buffer + length, "%u", configSettings->recordDuration); - - } - - RETURN_BOOL_ON_ERROR(AudioMoth_writeToFile(buffer, length)); - - if (configSettings->enableSunRecording) { - - int32_t latitude = gpsLocationReceived ? *gpsLatitude : acousticLocationReceived ? *acousticLatitude : configSettings->latitude; - - int32_t longitude = gpsLocationReceived ? *gpsLongitude : acousticLocationReceived ? *acousticLongitude : configSettings->longitude; - - char latitudeDirection = latitude < 0 ? 'S' : 'N'; - - char longitudeDirection = longitude < 0 ? 'W' : 'E'; - - if (gpsLocationReceived) { - - length = sprintf(buffer, "\r\n\r\nLocation : %ld.%06ld°%c %ld.%06ld°%c (GPS)", ABS(latitude) / GPS_LOCATION_PRECISION, ABS(latitude) % GPS_LOCATION_PRECISION, latitudeDirection, ABS(longitude) / GPS_LOCATION_PRECISION, ABS(longitude) % GPS_LOCATION_PRECISION, longitudeDirection); - - } else if (acousticLocationReceived) { - - length = sprintf(buffer, "\r\n\r\nLocation : %ld.%06ld°%c %ld.%06ld°%c (Acoustic chime)", ABS(latitude) / ACOUSTIC_LOCATION_PRECISION, ABS(latitude) % ACOUSTIC_LOCATION_PRECISION, latitudeDirection, ABS(longitude) / ACOUSTIC_LOCATION_PRECISION, ABS(longitude) % ACOUSTIC_LOCATION_PRECISION, longitudeDirection); - - } else { - - length = sprintf(buffer, "\r\n\r\nLocation : %ld.%02ld°%c %ld.%02ld°%c (Configuration app)", ABS(latitude) / CONFIG_LOCATION_PRECISION, ABS(latitude) % CONFIG_LOCATION_PRECISION, latitudeDirection, ABS(longitude) / CONFIG_LOCATION_PRECISION, ABS(longitude) % CONFIG_LOCATION_PRECISION, longitudeDirection); - - } - - static char* twilightTypes[3] = {"Civil", "Nautical", "Astronomical"}; - - static char* dawnDuskModes[5] = {"dawn", "dusk", "dawn and dusk", "dusk to dawn", "dawn to dusk"}; - - static char* sunriseSunsetModes[5] = {"Sunrise", "Sunset", "Sunrise and sunset", "Sunset to sunrise", "Sunrise to sunset"}; - - length += sprintf(buffer + length, "\r\nSun recording mode : "); - - if (configSettings->sunRecordingEvent == SR_SUNRISE_AND_SUNSET) { - - length += sprintf(buffer + length, "%s", sunriseSunsetModes[configSettings->sunRecordingMode]); - - } else { - - length += sprintf(buffer + length, "%s %s", twilightTypes[configSettings->sunRecordingEvent - 1], dawnDuskModes[configSettings->sunRecordingMode]); - - } - - char *sunriseText = configSettings->sunRecordingEvent == SR_SUNRISE_AND_SUNSET ? "\r\nSunrise - before, after (mins) " : "\r\nDawn - before, after (mins) "; - - char *sunsetText = configSettings->sunRecordingEvent == SR_SUNRISE_AND_SUNSET ? "\r\nSunset - before, after (mins) " : "\r\nDusk - before, after (mins) "; - - if (configSettings->sunRecordingMode == SUNRISE_RECORDING) { - - length += sprintf(buffer + length, "%s: %u, %u", sunriseText, configSettings->beforeSunriseMinutes, configSettings->afterSunriseMinutes); - length += sprintf(buffer + length, "%s: -, -", sunsetText); - - } else if (configSettings->sunRecordingMode == SUNSET_RECORDING) { - - length += sprintf(buffer + length, "%s: -, -", sunriseText); - length += sprintf(buffer + length, "%s: %u, %u", sunsetText, configSettings->beforeSunsetMinutes, configSettings->afterSunsetMinutes); - - } else if (configSettings->sunRecordingMode == SUNRISE_AND_SUNSET_RECORDING) { - - length += sprintf(buffer + length, "%s: %u, %u", sunriseText, configSettings->beforeSunriseMinutes, configSettings->afterSunriseMinutes); - length += sprintf(buffer + length, "%s: %u, %u", sunsetText, configSettings->beforeSunsetMinutes, configSettings->afterSunsetMinutes); - - } else if (configSettings->sunRecordingMode == SUNSET_TO_SUNRISE_RECORDING) { - - length += sprintf(buffer + length, "%s: -, %u", sunriseText, configSettings->afterSunriseMinutes); - length += sprintf(buffer + length, "%s: %u, -", sunsetText, configSettings->beforeSunsetMinutes); - - } else if (configSettings->sunRecordingMode == SUNRISE_TO_SUNSET_RECORDING) { - - length += sprintf(buffer + length, "%s: %u, -", sunriseText, configSettings->beforeSunriseMinutes); - length += sprintf(buffer + length, "%s: -, %u", sunsetText, configSettings->afterSunsetMinutes); - - } - - char *roundingText = configSettings->sunRecordingEvent == SR_SUNRISE_AND_SUNSET ? "\r\nSunrise/sunset rounding (mins) : %u" : "\r\nDawn/dusk rounding (mins) : %u"; - - length += sprintf(buffer + length, roundingText, configSettings->sunRoundingMinutes); - - } else { - - length = sprintf(buffer, "\r\n\r\nActive recording periods : %u\r\n", configSettings->activeRecordingPeriods); - - /* Calculate the start time offset for the appropriate time zone */ - - int32_t startMinutesOffset = timezoneSeconds / SECONDS_IN_MINUTE; - - if (configSettings->useTimezoneFromAcousticChime && configSettings->adjustScheduleUsingTimezoneFromAcousticChime) { - - startMinutesOffset = configSettings->timezoneHours * MINUTES_IN_HOUR + configSettings->timezoneMinutes; - - } - - /* Find the first recording period */ - - uint32_t minimumIndex = 0; - - uint32_t minimumStartMinutes = UINT32_MAX; - - for (uint32_t i = 0; i < configSettings->activeRecordingPeriods; i += 1) { - - uint32_t startMinutes = (MINUTES_IN_DAY + configSettings->recordingPeriods[i].startMinutes + startMinutesOffset) % MINUTES_IN_DAY; - - if (startMinutes < minimumStartMinutes) { - - minimumStartMinutes = startMinutes; - - minimumIndex = i; - - } - - } - - /* Display the recording periods */ - - for (uint32_t i = 0; i < configSettings->activeRecordingPeriods; i += 1) { - - uint32_t index = (minimumIndex + i) % configSettings->activeRecordingPeriods; - - uint32_t startMinutes = (MINUTES_IN_DAY + configSettings->recordingPeriods[index].startMinutes + startMinutesOffset) % MINUTES_IN_DAY; - - uint32_t endMinutes = (MINUTES_IN_DAY + configSettings->recordingPeriods[index].endMinutes + startMinutesOffset) % MINUTES_IN_DAY; - - length += sprintf(buffer + length, "\r\nRecording period %lu : %02lu:%02lu - %02lu:%02lu (%s)", i + 1, startMinutes / MINUTES_IN_HOUR, startMinutes % MINUTES_IN_HOUR, endMinutes / MINUTES_IN_HOUR, endMinutes % MINUTES_IN_HOUR, timezoneBuffer); - - } - - } - - RETURN_BOOL_ON_ERROR(AudioMoth_writeToFile(buffer, length)); - - if (configSettings->earliestRecordingTime == 0) { - - length = sprintf(buffer, "\r\n\r\nFirst recording date : ----------"); - - } else { - - time_t rawTime = configSettings->earliestRecordingTime + timezoneSeconds; - - if (configSettings->useTimezoneFromAcousticChime && configSettings->adjustScheduleUsingTimezoneFromAcousticChime) { - - rawTime += configSettings->timezoneHours * SECONDS_IN_HOUR + configSettings->timezoneMinutes * SECONDS_IN_MINUTE - timezoneSeconds; - - } - - gmtime_r(&rawTime, &time); - - if (time.tm_hour == 0 && time.tm_min == 0 && time.tm_sec == 0) { - - length = sprintf(buffer, "\r\n\r\nFirst recording date : "); - - length += sprintf(buffer + length, "%04d-%02d-%02d (%s)", YEAR_OFFSET + time.tm_year, MONTH_OFFSET + time.tm_mon, time.tm_mday, timezoneBuffer); - - } else { - - length = sprintf(buffer, "\r\n\r\nFirst recording time : "); - - length += sprintf(buffer + length, "%04d-%02d-%02d %02d:%02d:%02d (%s)", YEAR_OFFSET + time.tm_year, MONTH_OFFSET + time.tm_mon, time.tm_mday, time.tm_hour, time.tm_min, time.tm_sec, timezoneBuffer); - - } - - } - - if (configSettings->latestRecordingTime == 0) { - - length += sprintf(buffer + length, "\r\nLast recording date : ----------"); - - } else { - - time_t rawTime = configSettings->latestRecordingTime + timezoneSeconds; - - if (configSettings->useTimezoneFromAcousticChime && configSettings->adjustScheduleUsingTimezoneFromAcousticChime) { - - rawTime += configSettings->timezoneHours * SECONDS_IN_HOUR + configSettings->timezoneMinutes * SECONDS_IN_MINUTE - timezoneSeconds; - - } - - gmtime_r(&rawTime, &time); - - if (time.tm_hour == 0 && time.tm_min == 0 && time.tm_sec == 0) { - - rawTime -= SECONDS_IN_DAY; - - gmtime_r(&rawTime, &time); - - length += sprintf(buffer + length, "\r\nLast recording date : "); - - length += sprintf(buffer + length, "%04d-%02d-%02d (%s)", YEAR_OFFSET + time.tm_year, MONTH_OFFSET + time.tm_mon, time.tm_mday, timezoneBuffer); - - } else { - - length += sprintf(buffer + length, "\r\nLast recording time : "); - - length += sprintf(buffer + length, "%04d-%02d-%02d %02d:%02d:%02d (%s)", YEAR_OFFSET + time.tm_year, MONTH_OFFSET + time.tm_mon, time.tm_mday, time.tm_hour, time.tm_min, time.tm_sec, timezoneBuffer); - - } - - } - - RETURN_BOOL_ON_ERROR(AudioMoth_writeToFile(buffer, length)); - - length = sprintf(buffer, "\r\n\r\nFilter : "); - - if (configSettings->lowerFilterFreq == 0 && configSettings->higherFilterFreq == 0) { - - length += sprintf(buffer + length, "-"); - - } else if (configSettings->lowerFilterFreq == UINT16_MAX) { - - length += sprintf(buffer + length, "Low-pass (%u.%ukHz)", configSettings->higherFilterFreq / 10, configSettings->higherFilterFreq % 10); - - } else if (configSettings->higherFilterFreq == UINT16_MAX) { - - length += sprintf(buffer + length, "High-pass (%u.%ukHz)", configSettings->lowerFilterFreq / 10, configSettings->lowerFilterFreq % 10); - - } else { - - length += sprintf(buffer + length, "Band-pass (%u.%ukHz - %u.%ukHz)", configSettings->lowerFilterFreq / 10, configSettings->lowerFilterFreq % 10, configSettings->higherFilterFreq / 10, configSettings->higherFilterFreq % 10); - - } - - bool frequencyTriggerEnabled = configSettings->enableFrequencyTrigger; - - bool amplitudeThresholdEnabled = frequencyTriggerEnabled ? false : configSettings->amplitudeThreshold > 0 || configSettings->enableAmplitudeThresholdDecibelScale || configSettings->enableAmplitudeThresholdPercentageScale; - - length += sprintf(buffer + length, "\r\n\r\nTrigger type : "); - - if (frequencyTriggerEnabled) { - - length += sprintf(buffer + length, "Frequency (%u.%ukHz and window length of %u samples)", configSettings->frequencyTriggerCentreFrequency / 10, configSettings->frequencyTriggerCentreFrequency % 10, (0x01 << configSettings->frequencyTriggerWindowLengthShift)); - - length += sprintf(buffer + length, "\r\nThreshold setting : "); - - length += formatPercentage(buffer + length, configSettings->frequencyTriggerThresholdPercentageMantissa, configSettings->frequencyTriggerThresholdPercentageExponent); - - } else if (amplitudeThresholdEnabled) { - - length += sprintf(buffer + length, "Amplitude"); - - length += sprintf(buffer + length, "\r\nThreshold setting : "); - - if (configSettings->enableAmplitudeThresholdDecibelScale && configSettings->enableAmplitudeThresholdPercentageScale == false) { - - length += formatDecibels(buffer + length, configSettings->amplitudeThresholdDecibels, true); - - } else if (configSettings->enableAmplitudeThresholdPercentageScale && configSettings->enableAmplitudeThresholdDecibelScale == false) { - - length += formatPercentage(buffer + length, configSettings->amplitudeThresholdPercentageMantissa, configSettings->amplitudeThresholdPercentageExponent); - - } else { - - length += sprintf(buffer + length, "%u", configSettings->amplitudeThreshold); - - } - - } else { - - length += sprintf(buffer + length, "-"); - - length += sprintf(buffer + length, "\r\nThreshold setting : -"); - - } - - length += sprintf(buffer + length, "\r\nMinimum trigger duration (s) : "); - - if (frequencyTriggerEnabled || amplitudeThresholdEnabled) { - - length += sprintf(buffer + length, "%u", configSettings->minimumTriggerDuration); - - } else { - - length += sprintf(buffer + length, "-"); - - } - - RETURN_BOOL_ON_ERROR(AudioMoth_writeToFile(buffer, length)); - - length = sprintf(buffer, "\r\n\r\nEnable LED : %s\r\n", configSettings->enableLED ? "Yes" : "No"); - - length += sprintf(buffer + length, "Enable low-voltage cut-off : %s\r\n", configSettings->enableLowVoltageCutoff ? "Yes" : "No"); - - length += sprintf(buffer + length, "Enable battery level indication : %s\r\n\r\n", configSettings->disableBatteryLevelDisplay ? "No" : configSettings->batteryLevelDisplayType == NIMH_LIPO_BATTERY_VOLTAGE ? "Yes (NiMH/LiPo voltage range)" : "Yes"); - - length += sprintf(buffer + length, "Always require acoustic chime : %s\r\n", configSettings->requireAcousticConfiguration ? "Yes" : "No"); - - length += sprintf(buffer + length, "Also require location in chime : %s\r\n", configSettings->requireAcousticConfiguration == false ? "-" : configSettings->requireAcousticLocation ? "Yes" : "No"); - - length += sprintf(buffer + length, "Use timezone from chime : %s\r\n", configSettings->useTimezoneFromAcousticChime ? "Yes" : "No"); - - length += sprintf(buffer + length, "Also adjust recording schedule : %s\r\n\r\n", configSettings->useTimezoneFromAcousticChime == false ? "-" : configSettings->adjustScheduleUsingTimezoneFromAcousticChime ? "Yes" : "No"); - - RETURN_BOOL_ON_ERROR(AudioMoth_writeToFile(buffer, length)); - - length = sprintf(buffer, "Recording preparation time (s) : %d\r\n", configSettings->preparationPeriodBuffer == 0 ? DEFAULT_PREPARATION_PERIOD_BUFFER / MILLISECONDS_IN_SECOND : configSettings->preparationPeriodBuffer); - - length += sprintf(buffer + length, "Use device ID in WAV file name : %s\r\n", configSettings->enableFilenameWithDeviceID ? "Yes" : "No"); - - length += sprintf(buffer + length, "Use daily folder for WAV files : %s\r\n\r\n", configSettings->enableDailyFolders ? "Yes" : "No"); - - length += sprintf(buffer + length, "Disable 48Hz DC blocking filter : %s\r\n", configSettings->disable48HzDCBlockingFilter ? "Yes" : "No"); - - length += sprintf(buffer + length, "Enable energy saver mode : %s\r\n", configSettings->enableEnergySaverMode ? "Yes" : "No"); - - length += sprintf(buffer + length, "Enable low gain range : %s\r\n\r\n", configSettings->enableLowGainRange ? "Yes" : "No"); - - RETURN_BOOL_ON_ERROR(AudioMoth_writeToFile(buffer, length)); - - length = sprintf(buffer, "Ignore external microphone : %s\r\n\r\n", configSettings->ignoreExternalMicrophoneForAcousticChime ? "Yes" : "No"); - - length += sprintf(buffer + length, "Enable magnetic switch : %s\r\n\r\n", configSettings->enableMagneticSwitch ? "Yes" : "No"); - - RETURN_BOOL_ON_ERROR(AudioMoth_writeToFile(buffer, length)); - - length = sprintf(buffer, "Enable GPS time setting : %s\r\n", configSettings->enableTimeSettingFromGPS ? "Yes" : "No"); - - length += sprintf(buffer + length, "GPS fix before and after : %s\r\n", configSettings->enableTimeSettingFromGPS == false ? "-" : configSettings->enableTimeSettingBeforeAndAfterRecordings ? "Individual recordings" : "Recording periods"); - - length += sprintf(buffer + length, "GPS fix time (mins) : "); - - if (configSettings->enableTimeSettingFromGPS) { - - uint32_t gpsTimeSettingPeriod = configSettings->gpsTimeSettingPeriod == 0 ? GPS_DEFAULT_TIME_SETTING_PERIOD / SECONDS_IN_MINUTE : configSettings->gpsTimeSettingPeriod; - - length += sprintf(buffer + length, "%ld\r\n", gpsTimeSettingPeriod); - - } else { - - length += sprintf(buffer + length, "-\r\n"); - - } - - RETURN_BOOL_ON_ERROR(AudioMoth_writeToFile(buffer, length)); - - RETURN_BOOL_ON_ERROR(AudioMoth_closeFile()); - - return true; - -} - -/* Backup domain variables */ - -static uint32_t *backupDomainFlags = (uint32_t*)AM_BACKUP_DOMAIN_START_ADDRESS; - -static uint32_t *previousSwitchPosition = (uint32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 4); - -static uint32_t *startOfRecordingPeriod = (uint32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 8); - -static uint32_t *timeOfNextRecording = (uint32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 12); - -static uint32_t *indexOfNextRecording = (uint32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 16); - -static uint32_t *durationOfNextRecording = (uint32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 20); - -static uint32_t *timeOfNextGPSTimeSetting = (uint32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 24); - -static uint8_t *deploymentID = (uint8_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 28); - -static uint32_t *numberOfRecordingErrors = (uint32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 36); - -static uint32_t *numberOfConsecutiveRecordingErrors = (uint32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 40); - -static uint32_t *recordingPreparationPeriod = (uint32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 44); - -static int32_t *gpsLatitude = (int32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 48); - -static int32_t *gpsLongitude = (int32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 52); - -static int32_t *gpsLastFixLatitude = (int32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 56); - -static int32_t *gpsLastFixLongitude = (int32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 60); - -static int32_t *acousticLatitude = (int32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 64); - -static int32_t *acousticLongitude = (int32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 68); - -static int32_t *acousticTimezoneMinutes = (int32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 72); - -static uint32_t *numberOfSunRecordingPeriods = (uint32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 76); - -static recordingPeriod_t *firstSunRecordingPeriod = (recordingPeriod_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 80); - -static recordingPeriod_t *secondSunRecordingPeriod = (recordingPeriod_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 84); - -static uint32_t *timeOfNextSunriseSunsetCalculation = (uint32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 88); - -static uint32_t *timeAtWhichToSwitchOffLED = (uint32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 92); - -/* Functions to query, set and clear backup domain flags */ - -typedef enum { - BACKUP_WRITTEN_CONFIGURATION_TO_FILE, - BACKUP_READY_TO_MAKE_RECORDING, - BACKUP_MUST_SET_TIME_FROM_GPS, - BACKUP_SHOULD_SET_TIME_FROM_GPS, - BACKUP_WAITING_FOR_MAGNETIC_SWITCH, - BACKUP_POWERED_DOWN_WITH_SHORT_WAIT_INTERVAL, - BACKUP_GPS_LOCATION_RECEIVED, - BACKUP_ACOUSTIC_LOCATION_RECEIVED, - BACKUP_ACOUSTIC_TIMEZONE_RECEIVED -} AM_backupDomainFlag_t; - -static inline bool getBackupFlag(AM_backupDomainFlag_t flag) { - uint32_t mask = (1 << flag); - return (*backupDomainFlags & mask) == mask; -} - -static inline void setBackupFlag(AM_backupDomainFlag_t flag, bool state) { - if (state) { - *backupDomainFlags |= (1 << flag); - } else { - *backupDomainFlags &= ~(1 << flag); - } -} - -/* Filter variables */ - -static AM_filterType_t requestedFilterType; - -/* DMA transfer variable */ - -static uint32_t numberOfRawSamplesInDMATransfer; - -/* SRAM buffer variables */ - -static volatile uint32_t writeBuffer; - -static volatile uint32_t writeBufferIndex; - -static int16_t* buffers[NUMBER_OF_BUFFERS]; - -/* Flag to start processing DMA transfers */ - -static volatile uint32_t numberOfDMATransfers; - -static volatile uint32_t numberOfDMATransfersToWait; - -/* Write indicator buffer */ - -static bool writeIndicator[NUMBER_OF_BUFFERS]; - -/* GPS message buffer */ - -static char gpsMessageBuffer[GPS_MESSAGE_BUFFER_SIZE_IN_BYTES]; - -/* Compression buffer */ - -static int16_t compressionBuffer[COMPRESSION_BUFFER_SIZE_IN_BYTES / NUMBER_OF_BYTES_IN_SAMPLE] __attribute__ ((aligned(UINT32_SIZE_IN_BYTES))); - -/* Configuration settings */ - -static configSettings_t *configSettings = &((persistentConfigSettings_t*)AM_FLASH_USER_DATA_ADDRESS)->configSettings; - -static persistentConfigSettings_t *persistentConfigSettings = (persistentConfigSettings_t*)AM_FLASH_USER_DATA_ADDRESS; - -static persistentConfigSettings_t *tempPersistentConfigSettings = (persistentConfigSettings_t*)compressionBuffer; - -/* LED variable */ - -static bool enableLED; - -/* GPS fix variables */ - -static bool gpsEnableLED; - -static bool gpsPPSEvent; - -static bool gpsFixEvent; - -static bool gpsMessageEvent; - -static uint32_t timeSetFromGPS; - -static bool gpsFirstMessageReceived; - -static uint32_t gpsTickEventCount = 1; - -static uint32_t gpsTickEventModulo = GPS_TICK_EVENTS_PER_SECOND; - -/* Audio configuration variables */ - -static bool audioConfigStateLED; - -static bool audioConfigToggleLED; - -static uint32_t audioConfigPulseCounter; - -static bool acousticConfigurationPerformed; - -static uint32_t secondsOfAcousticSignalStart; - -static uint32_t millisecondsOfAcousticSignalStart; - -/* Deployment ID variable */ - -static uint8_t defaultDeploymentID[DEPLOYMENT_ID_LENGTH]; - -/* Recording state */ - -static volatile bool magneticSwitch; - -static volatile bool microphoneChanged; - -static volatile bool switchPositionChanged; - -/* DMA buffers */ - -static int16_t primaryBuffer[MAXIMUM_SAMPLES_IN_DMA_TRANSFER]; - -static int16_t secondaryBuffer[MAXIMUM_SAMPLES_IN_DMA_TRANSFER]; - -/* Firmware version and description */ - -static uint8_t firmwareVersion[AM_FIRMWARE_VERSION_LENGTH] = {1, 12, 0}; - -static uint8_t firmwareDescription[AM_FIRMWARE_DESCRIPTION_LENGTH] = "AudioMoth-Firmware-Basic"; - -/* Function prototypes */ - -static AM_recordingState_t makeRecording(uint32_t timeOfNextRecording, uint32_t recordDuration, uint32_t timeAtWhichToSwitchOffLED, AM_extendedBatteryState_t extendedBatteryState, int32_t temperature, uint32_t *fileOpenTime, uint32_t *fileOpenMilliseconds); - -static void scheduleRecording(uint32_t currentTime, uint32_t *timeOfNextRecording, uint32_t *indexOfNextRecording, uint32_t *durationOfNextRecording, uint32_t *startOfRecordingPeriod, uint32_t *endOfRecordingPeriod); - -static void determineTimeOfNextSunriseSunsetCalculation(uint32_t currentTime, uint32_t *timeOfNextSunriseSunsetCalculation); - -static void determineSunriseAndSunsetTimesAndScheduleRecording(uint32_t currentTime); - -static void determineSunriseAndSunsetTimes(uint32_t currentTime); - -static void flashLedToIndicateBatteryLife(void); - -/* Functions of copy to and from the backup domain */ - -static void copyToBackupDomain(uint32_t *dst, uint8_t *src, uint32_t length) { - - uint32_t value = 0; - - for (uint32_t i = 0; i < length / UINT32_SIZE_IN_BYTES; i += 1) { - *(dst + i) = *((uint32_t*)src + i); - } - - for (uint32_t i = 0; i < length % UINT32_SIZE_IN_BYTES; i += 1) { - value = (value << BITS_PER_BYTE) + *(src + length - 1 - i); - } - - if (length % UINT32_SIZE_IN_BYTES) *(dst + length / UINT32_SIZE_IN_BYTES) = value; - -} - -/* GPS time setting functions */ - -static void writeGPSLogMessage(char *buffer, uint32_t currentTime, uint32_t currentMilliseconds, char *message) { - - struct tm time; - - time_t rawTime = currentTime; - - gmtime_r(&rawTime, &time); - - uint32_t length = sprintf(buffer, "%02d/%02d/%04d %02d:%02d:%02d.%03lu UTC: %s\r\n", time.tm_mday, MONTH_OFFSET + time.tm_mon, YEAR_OFFSET + time.tm_year, time.tm_hour, time.tm_min, time.tm_sec, currentMilliseconds, message); - - AudioMoth_writeToFile(buffer, length); - -} - -static GPS_fixResult_t setTimeFromGPS(bool showLED, AM_gpsFixMode_t fixMode, uint32_t timeout) { - - /* Enable GPS and get the current time */ - - GPS_powerUpGPS(); - - GPS_enableGPSInterface(); - - uint32_t currentTime, currentMilliseconds; - - /* Open the GPS log file */ - - bool success = AudioMoth_appendFile(GPS_FILENAME); - - /* Add power up message */ - - static char *messages[] = { - "GPS switched on for initial fix.", - "GPS switched on for fix before recording period.", - "GPS switched on for fix between recording periods.", - "GPS switched on for fix after recording period.", - "GPS switched on for fix before recording.", - "GPS switched on for fix between recordings.", - "GPS switched on for fix after recording." - }; - - char *message = messages[fixMode]; - - AudioMoth_getTime(¤tTime, ¤tMilliseconds); - - writeGPSLogMessage((char*)compressionBuffer, currentTime, currentMilliseconds, message); - - /* Set green LED and enter routine */ - - gpsEnableLED = showLED; - - if (gpsEnableLED) AudioMoth_setGreenLED(true); - - GPS_fixResult_t result = GPS_setTimeFromGPS(timeout); - - AudioMoth_setRedLED(false); - - GPS_disableGPSInterface(); - - /* Power down the GPS */ - - uint32_t powerOffTime, powerOffMilliseconds; - - AudioMoth_getTime(&powerOffTime, &powerOffMilliseconds); - - if (result == GPS_SUCCESS || result == GPS_TIMEOUT) powerOffMilliseconds = 0; - - if (result == GPS_SUCCESS) { - - powerOffTime = MIN(timeSetFromGPS + GPS_ADDITIONAL_POWER_INTERVAL, timeout); - - while (currentTime < powerOffTime) { - - AudioMoth_feedWatchdog(); - - AudioMoth_delay(SHORT_WAIT_INTERVAL); - - AudioMoth_getTime(¤tTime, ¤tMilliseconds); - - } - - } - - AudioMoth_setGreenLED(false); - - GPS_powerDownGPS(); - - /* Write result messages */ - - if (result == GPS_CANCELLED_BY_MAGNETIC_SWITCH) writeGPSLogMessage((char*)compressionBuffer, powerOffTime, powerOffMilliseconds, "Time was not updated. Cancelled by magnetic switch."); - - if (result == GPS_CANCELLED_BY_SWITCH) writeGPSLogMessage((char*)compressionBuffer, powerOffTime, powerOffMilliseconds, "Time was not updated. Cancelled by switch position change."); - - if (result == GPS_TIMEOUT) writeGPSLogMessage((char*)compressionBuffer, powerOffTime, powerOffMilliseconds, "Time was not updated. Timed out."); - - writeGPSLogMessage((char*)compressionBuffer, powerOffTime, powerOffMilliseconds, "GPS switched off.\r\n"); - - /* Close file and return */ - - if (success) AudioMoth_closeFile(); - - return result; - -} - -/* Magnetic switch wait functions */ - -static void startWaitingForMagneticSwitch() { - - /* Flash LED to indicate start of waiting for magnetic switch */ - - FLASH_REPEAT_LED(Red, MAGNETIC_SWITCH_CHANGE_FLASHES, SHORT_LED_FLASH_DURATION); - - /* Cancel any scheduled recording */ - - *indexOfNextRecording = 0; - - *timeOfNextRecording = UINT32_MAX; - - *startOfRecordingPeriod = UINT32_MAX; - - *durationOfNextRecording = UINT32_MAX; - - *timeOfNextGPSTimeSetting = UINT32_MAX; - - setBackupFlag(BACKUP_SHOULD_SET_TIME_FROM_GPS, false); - - setBackupFlag(BACKUP_WAITING_FOR_MAGNETIC_SWITCH, true); - -} - -static void stopWaitingForMagneticSwitch(uint32_t *currentTime, uint32_t *currentMilliseconds) { - - /* Flash LED to indicate end of waiting for magnetic switch */ - - FLASH_REPEAT_LED(Green, MAGNETIC_SWITCH_CHANGE_FLASHES, SHORT_LED_FLASH_DURATION); - - /* Schedule next recording */ - - AudioMoth_getTime(currentTime, currentMilliseconds); - - uint32_t scheduleTime = *currentTime + ROUNDED_UP_DIV(*currentMilliseconds + *recordingPreparationPeriod, MILLISECONDS_IN_SECOND); - - determineSunriseAndSunsetTimesAndScheduleRecording(scheduleTime); - - /* Update flag */ - - setBackupFlag(BACKUP_WAITING_FOR_MAGNETIC_SWITCH, false); - -} - -/* Function to calculate the time to the next event */ - -static void calculateTimeToNextEvent(uint32_t currentTime, uint32_t currentMilliseconds, int64_t *timeUntilPreparationStart, int64_t *timeUntilNextGPSTimeSetting) { - - int64_t preparationPeriodBuffer = (configSettings->preparationPeriodBuffer == 0 ? DEFAULT_PREPARATION_PERIOD_BUFFER : configSettings->preparationPeriodBuffer * MILLISECONDS_IN_SECOND); - - *timeUntilPreparationStart = (int64_t)*timeOfNextRecording * MILLISECONDS_IN_SECOND - (int64_t)*recordingPreparationPeriod - preparationPeriodBuffer - (int64_t)currentTime * MILLISECONDS_IN_SECOND - (int64_t)currentMilliseconds; - - *timeUntilNextGPSTimeSetting = (int64_t)*timeOfNextGPSTimeSetting * MILLISECONDS_IN_SECOND - (int64_t)currentTime * MILLISECONDS_IN_SECOND - (int64_t)currentMilliseconds; - -} - -/* Main function */ - -int main(void) { - - /* Initialise device */ - - AudioMoth_initialise(); - - AM_switchPosition_t switchPosition = AudioMoth_getSwitchPosition(); - - if (AudioMoth_isInitialPowerUp()) { - - /* Initialise recording schedule variables */ - - *indexOfNextRecording = 0; - - *timeOfNextRecording = UINT32_MAX; - - *startOfRecordingPeriod = UINT32_MAX; - - *durationOfNextRecording = UINT32_MAX; - - *timeOfNextGPSTimeSetting = UINT32_MAX; - - /* Initialise configuration writing variable */ - - setBackupFlag(BACKUP_WRITTEN_CONFIGURATION_TO_FILE, false); - - /* Initialise recording state variables */ - - *previousSwitchPosition = AM_SWITCH_NONE; - - setBackupFlag(BACKUP_MUST_SET_TIME_FROM_GPS, false); - - setBackupFlag(BACKUP_SHOULD_SET_TIME_FROM_GPS, false); - - setBackupFlag(BACKUP_READY_TO_MAKE_RECORDING, false); - - *numberOfRecordingErrors = 0; - - *numberOfConsecutiveRecordingErrors = 0; - - *recordingPreparationPeriod = INITIAL_PREPARATION_PERIOD; - - /* Initialise magnetic switch state variables */ - - setBackupFlag(BACKUP_WAITING_FOR_MAGNETIC_SWITCH, false); - - /* Initialise the power down interval flag */ - - setBackupFlag(BACKUP_POWERED_DOWN_WITH_SHORT_WAIT_INTERVAL, false); - - /* Initial GPS and sunrise and sunset variables */ - - *gpsLastFixLatitude = 0; - - *gpsLastFixLongitude = 0; - - setBackupFlag(BACKUP_GPS_LOCATION_RECEIVED, false); - - setBackupFlag(BACKUP_ACOUSTIC_LOCATION_RECEIVED, false); - - setBackupFlag(BACKUP_ACOUSTIC_TIMEZONE_RECEIVED, false); - - *timeOfNextSunriseSunsetCalculation = 0; - - /* Copy default deployment ID */ - - copyToBackupDomain((uint32_t*)deploymentID, (uint8_t*)defaultDeploymentID, DEPLOYMENT_ID_LENGTH); - - } - - /* Check the persistent configuration firmware version and description matches the default values */ - - if (memcmp(persistentConfigSettings->firmwareVersion, firmwareVersion, AM_FIRMWARE_VERSION_LENGTH) != 0 || memcmp(persistentConfigSettings->firmwareDescription, firmwareDescription, AM_FIRMWARE_DESCRIPTION_LENGTH) != 0) { - - memcpy(&tempPersistentConfigSettings->firmwareVersion, &firmwareVersion, AM_FIRMWARE_VERSION_LENGTH); - - memcpy(&tempPersistentConfigSettings->firmwareDescription, &firmwareDescription, AM_FIRMWARE_DESCRIPTION_LENGTH); - - memcpy(&tempPersistentConfigSettings->configSettings, (uint8_t*)&defaultConfigSettings, sizeof(configSettings_t)); - - uint32_t numberOfBytes = ROUND_UP_TO_MULTIPLE(sizeof(persistentConfigSettings_t), UINT32_SIZE_IN_BYTES); - - bool success = AudioMoth_writeToFlashUserDataPage((uint8_t*)tempPersistentConfigSettings, numberOfBytes); - - if (success == false) { - - /* Cannot recover from a failure at this point */ - - for (uint32_t i = 0; i < 5; i += 1) { - - AudioMoth_setBothLED(true); - - AudioMoth_delay(100); - - AudioMoth_setBothLED(false); - - AudioMoth_delay(100); - - } - - AudioMoth_powerDownAndWakeMilliseconds(DEFAULT_WAIT_INTERVAL); - - } - - } - - /* Handle the case that the switch is in USB position */ - - if (switchPosition == AM_SWITCH_USB) { - - if (configSettings->disableBatteryLevelDisplay == false && (*previousSwitchPosition == AM_SWITCH_DEFAULT || *previousSwitchPosition == AM_SWITCH_CUSTOM)) { - - flashLedToIndicateBatteryLife(); - - } - - AudioMoth_handleUSB(); - - SAVE_SWITCH_POSITION_AND_POWER_DOWN(DEFAULT_WAIT_INTERVAL); - - } - - /* Read the time */ - - uint32_t currentTime; - - uint32_t currentMilliseconds; - - AudioMoth_getTime(¤tTime, ¤tMilliseconds); - - /* Check if switch has just been moved to CUSTOM or DEFAULT */ - - bool fileSystemEnabled = false; - - if (switchPosition != *previousSwitchPosition) { - - /* Reset the LED and GPS flags */ - - *timeAtWhichToSwitchOffLED = UINT32_MAX; - - setBackupFlag(BACKUP_MUST_SET_TIME_FROM_GPS, false); - - setBackupFlag(BACKUP_SHOULD_SET_TIME_FROM_GPS, false); - - /* Reset the power down interval flag */ - - setBackupFlag(BACKUP_POWERED_DOWN_WITH_SHORT_WAIT_INTERVAL, false); - - /* Check there are active recording periods if the switch is in CUSTOM position */ - - setBackupFlag(BACKUP_READY_TO_MAKE_RECORDING, switchPosition == AM_SWITCH_DEFAULT || (switchPosition == AM_SWITCH_CUSTOM && (configSettings->activeRecordingPeriods > 0 || configSettings->enableSunRecording))); - - /* Check if acoustic configuration is required */ - - if (getBackupFlag(BACKUP_READY_TO_MAKE_RECORDING)) { - - /* Determine if acoustic configuration is required */ - - bool shouldPerformAcousticConfiguration = switchPosition == AM_SWITCH_CUSTOM && (AudioMoth_hasTimeBeenSet() == false || configSettings->requireAcousticConfiguration); - - /* Overrule this decision if setting of time from GPS is enabled and acoustic configuration not enforced. Also set GPS time setting flag */ - - if (switchPosition == AM_SWITCH_CUSTOM && configSettings->enableTimeSettingFromGPS) { - - if (configSettings->requireAcousticConfiguration == false) shouldPerformAcousticConfiguration = false; - - setBackupFlag(BACKUP_MUST_SET_TIME_FROM_GPS, true); - - } - - /* Determine whether to listen for the acoustic tone */ - - bool listenForAcousticTone = switchPosition == AM_SWITCH_CUSTOM && shouldPerformAcousticConfiguration == false; - - if (listenForAcousticTone) { - - AudioConfig_enableAudioConfiguration(configSettings->ignoreExternalMicrophoneForAcousticChime); - - shouldPerformAcousticConfiguration = AudioConfig_listenForAudioConfigurationTone(AUDIO_CONFIG_TONE_TIMEOUT); - - } - - if (shouldPerformAcousticConfiguration) { - - AudioMoth_setRedLED(true); - - AudioMoth_setGreenLED(false); - - audioConfigPulseCounter = 0; - - audioConfigStateLED = false; - - audioConfigToggleLED = false; - - acousticConfigurationPerformed = false; - - if (listenForAcousticTone == false) { - - AudioConfig_enableAudioConfiguration(configSettings->ignoreExternalMicrophoneForAcousticChime); - - } - - AudioConfig_listenForAudioConfigurationPackets(listenForAcousticTone, AUDIO_CONFIG_PACKETS_TIMEOUT); - - AudioConfig_disableAudioConfiguration(); - - if (acousticConfigurationPerformed) { - - /* Indicate success with LED flashes */ - - AudioMoth_setRedLED(false); - - AudioMoth_setGreenLED(true); - - AudioMoth_delay(1000); - - AudioMoth_delay(1000); - - AudioMoth_setGreenLED(false); - - AudioMoth_delay(500); - - } else { - - /* Turn off LED */ - - AudioMoth_setBothLED(false); - - /* Determine if it is possible to still make a recording */ - - if (configSettings->requireAcousticConfiguration) { - - setBackupFlag(BACKUP_READY_TO_MAKE_RECORDING, false); - - } else { - - setBackupFlag(BACKUP_READY_TO_MAKE_RECORDING, AudioMoth_hasTimeBeenSet() || getBackupFlag(BACKUP_MUST_SET_TIME_FROM_GPS)); - - } - - /* Power down */ - - SAVE_SWITCH_POSITION_AND_POWER_DOWN(DEFAULT_WAIT_INTERVAL); - - } - - } else if (listenForAcousticTone) { - - AudioConfig_disableAudioConfiguration(); - - } - - } - - /* Calculate time of next recording if ready to make a recording */ - - if (getBackupFlag(BACKUP_READY_TO_MAKE_RECORDING)) { - - /* Enable energy saver mode */ - - if (isEnergySaverMode(configSettings)) AudioMoth_setClockDivider(AM_HF_CLK_DIV2); - - /* Reset the recording error counter */ - - *numberOfRecordingErrors = 0; - - *numberOfConsecutiveRecordingErrors = 0; - - /* Reset the recording preparation period to default */ - - *recordingPreparationPeriod = INITIAL_PREPARATION_PERIOD; - - /* Reset persistent configuration write flag */ - - setBackupFlag(BACKUP_WRITTEN_CONFIGURATION_TO_FILE, false); - - /* Reset GPS and sunrise and sunset variables */ - - *gpsLastFixLatitude = 0; - - *gpsLastFixLongitude = 0; - - setBackupFlag(BACKUP_GPS_LOCATION_RECEIVED, false); - - *timeOfNextSunriseSunsetCalculation = 0; - - /* Try to write configuration now if it will not be written later when time is set */ - - if (configSettings->enableSunRecording == false || getBackupFlag(BACKUP_MUST_SET_TIME_FROM_GPS) == false) { - - fileSystemEnabled = AudioMoth_enableFileSystem(configSettings->sampleRateDivider == 1 ? AM_SD_CARD_HIGH_SPEED : AM_SD_CARD_NORMAL_SPEED); - - if (fileSystemEnabled) { - - AudioMoth_getTime(¤tTime, ¤tMilliseconds); - - bool gpsLocationReceived = getBackupFlag(BACKUP_GPS_LOCATION_RECEIVED); - - bool acousticLocationReceived = getBackupFlag(BACKUP_ACOUSTIC_LOCATION_RECEIVED); - - bool success = writeConfigurationToFile((char*)compressionBuffer, configSettings, currentTime, gpsLocationReceived, gpsLatitude, gpsLongitude, acousticLocationReceived, acousticLatitude, acousticLongitude, firmwareDescription, firmwareVersion, (uint8_t*)AM_UNIQUE_ID_START_ADDRESS, deploymentID, defaultDeploymentID); - - setBackupFlag(BACKUP_WRITTEN_CONFIGURATION_TO_FILE, success); - - } - - } - - /* Update the time and calculate earliest schedule start time */ - - AudioMoth_getTime(¤tTime, ¤tMilliseconds); - - uint32_t scheduleTime = currentTime + ROUNDED_UP_DIV(currentMilliseconds + *recordingPreparationPeriod, MILLISECONDS_IN_SECOND); - - /* Schedule the next recording */ - - if (switchPosition == AM_SWITCH_CUSTOM) { - - setBackupFlag(BACKUP_WAITING_FOR_MAGNETIC_SWITCH, configSettings->enableMagneticSwitch); - - if (configSettings->enableMagneticSwitch || getBackupFlag(BACKUP_MUST_SET_TIME_FROM_GPS)) { - - *indexOfNextRecording = 0; - - *timeOfNextRecording = UINT32_MAX; - - *startOfRecordingPeriod = UINT32_MAX; - - *durationOfNextRecording = UINT32_MAX; - - *timeOfNextGPSTimeSetting = UINT32_MAX; - - } else { - - *timeOfNextRecording = UINT32_MAX; - - *startOfRecordingPeriod = UINT32_MAX; - - determineSunriseAndSunsetTimesAndScheduleRecording(scheduleTime); - - if (configSettings->enableLED == false) *timeAtWhichToSwitchOffLED = currentTime + SECONDS_IN_MINUTE; - - } - - } - - /* Set parameters to start recording now */ - - if (switchPosition == AM_SWITCH_DEFAULT) { - - *indexOfNextRecording = 0; - - *timeOfNextRecording = scheduleTime; - - *startOfRecordingPeriod = scheduleTime; - - *durationOfNextRecording = UINT32_MAX; - - *timeOfNextGPSTimeSetting = UINT32_MAX; - - } - - } - - } - - /* If not ready to make a recording then flash LED and power down */ - - if (getBackupFlag(BACKUP_READY_TO_MAKE_RECORDING) == false) { - - FLASH_LED(Both, SHORT_LED_FLASH_DURATION) - - SAVE_SWITCH_POSITION_AND_POWER_DOWN(DEFAULT_WAIT_INTERVAL); - - } - - /* Enable the magnetic switch */ - - bool magneticSwitchEnabled = configSettings->enableMagneticSwitch && switchPosition == AM_SWITCH_CUSTOM; - - if (magneticSwitchEnabled) GPS_enableMagneticSwitch(); - - /* Reset LED flags */ - - enableLED = currentTime < *timeAtWhichToSwitchOffLED; - - bool shouldSuppressLED = getBackupFlag(BACKUP_POWERED_DOWN_WITH_SHORT_WAIT_INTERVAL); - - setBackupFlag(BACKUP_POWERED_DOWN_WITH_SHORT_WAIT_INTERVAL, false); - - /* Calculate time until next activity */ - - int64_t timeUntilPreparationStart; - - int64_t timeUntilNextGPSTimeSetting; - - calculateTimeToNextEvent(currentTime, currentMilliseconds, &timeUntilPreparationStart, &timeUntilNextGPSTimeSetting); - - /* If the GPS synchronisation window has passed then cancel it */ - - int64_t timeSinceScheduledGPSTimeSetting = -timeUntilNextGPSTimeSetting; - - uint32_t gpsTimeSettingPeriod = configSettings->gpsTimeSettingPeriod == 0 ? GPS_DEFAULT_TIME_SETTING_PERIOD : configSettings->gpsTimeSettingPeriod * SECONDS_IN_MINUTE; - - if (timeSinceScheduledGPSTimeSetting > gpsTimeSettingPeriod * MILLISECONDS_IN_SECOND) { - - *timeOfNextGPSTimeSetting = UINT32_MAX; - - calculateTimeToNextEvent(currentTime, currentMilliseconds, &timeUntilPreparationStart, &timeUntilNextGPSTimeSetting); - - } - - /* Set the time from the GPS */ - - if (getBackupFlag(BACKUP_MUST_SET_TIME_FROM_GPS) && getBackupFlag(BACKUP_WAITING_FOR_MAGNETIC_SWITCH) == false) { - - /* Enable the file system and set the time from the GPS */ - - if (!fileSystemEnabled) fileSystemEnabled = AudioMoth_enableFileSystem(AM_SD_CARD_NORMAL_SPEED); - - GPS_fixResult_t fixResult = setTimeFromGPS(enableLED, INITIAL_GPS_FIX, currentTime + GPS_INITIAL_TIME_SETTING_PERIOD); - - /* Update the schedule if successful */ - - if (fixResult == GPS_SUCCESS) { - - /* Reset the flag */ - - setBackupFlag(BACKUP_MUST_SET_TIME_FROM_GPS, false); - - /* Update the GPS location */ - - setBackupFlag(BACKUP_GPS_LOCATION_RECEIVED, true); - - *gpsLatitude = *gpsLastFixLatitude; - - *gpsLongitude = *gpsLastFixLongitude; - - /* Write the configuration file */ - - if (configSettings->enableSunRecording && fileSystemEnabled) { - - AudioMoth_getTime(¤tTime, ¤tMilliseconds); - - bool gpsLocationReceived = getBackupFlag(BACKUP_GPS_LOCATION_RECEIVED); - - bool acousticLocationReceived = getBackupFlag(BACKUP_ACOUSTIC_LOCATION_RECEIVED); - - bool success = writeConfigurationToFile((char*)compressionBuffer, configSettings, currentTime, gpsLocationReceived, gpsLatitude, gpsLongitude, acousticLocationReceived, acousticLatitude, acousticLongitude, firmwareDescription, firmwareVersion, (uint8_t*)AM_UNIQUE_ID_START_ADDRESS, deploymentID, defaultDeploymentID); - - setBackupFlag(BACKUP_WRITTEN_CONFIGURATION_TO_FILE, success); - - } - - /* Schedule the next recording */ - - AudioMoth_getTime(¤tTime, ¤tMilliseconds); - - uint32_t scheduleTime = currentTime + ROUNDED_UP_DIV(currentMilliseconds + *recordingPreparationPeriod, MILLISECONDS_IN_SECOND); - - determineSunriseAndSunsetTimesAndScheduleRecording(scheduleTime); - - if (configSettings->enableLED == false) *timeAtWhichToSwitchOffLED = currentTime + SECONDS_IN_MINUTE; - - } - - /* If time setting was cancelled with the magnet switch then start waiting for the magnetic switch */ - - if (fixResult == GPS_CANCELLED_BY_MAGNETIC_SWITCH) startWaitingForMagneticSwitch(); - - /* Power down */ - - SAVE_SWITCH_POSITION_AND_POWER_DOWN(DEFAULT_WAIT_INTERVAL); - - } - - /* Make a recording */ - - if (timeUntilPreparationStart <= 0) { - - /* Enable energy saver mode */ - - if (isEnergySaverMode(configSettings)) AudioMoth_setClockDivider(AM_HF_CLK_DIV2); - - /* Write configuration if not already done so */ - - if (getBackupFlag(BACKUP_WRITTEN_CONFIGURATION_TO_FILE) == false) { - - if (!fileSystemEnabled) fileSystemEnabled = AudioMoth_enableFileSystem(configSettings->sampleRateDivider == 1 ? AM_SD_CARD_HIGH_SPEED : AM_SD_CARD_NORMAL_SPEED); - - if (fileSystemEnabled) { - - bool gpsLocationReceived = getBackupFlag(BACKUP_GPS_LOCATION_RECEIVED); - - bool acousticLocationReceived = getBackupFlag(BACKUP_ACOUSTIC_LOCATION_RECEIVED); - - bool success = writeConfigurationToFile((char*)compressionBuffer, configSettings, currentTime, gpsLocationReceived, gpsLatitude, gpsLongitude, acousticLocationReceived, acousticLatitude, acousticLongitude, firmwareDescription, firmwareVersion, (uint8_t*)AM_UNIQUE_ID_START_ADDRESS, deploymentID, defaultDeploymentID); - - setBackupFlag(BACKUP_WRITTEN_CONFIGURATION_TO_FILE, success); - - } - - } - - /* Make the recording */ - - uint32_t fileOpenTime; - - uint32_t fileOpenMilliseconds; - - AM_recordingState_t recordingState = RECORDING_OKAY; - - /* Measure battery voltage */ - - uint32_t supplyVoltage = AudioMoth_getSupplyVoltage(); - - AM_extendedBatteryState_t extendedBatteryState = AudioMoth_getExtendedBatteryState(supplyVoltage); - - /* Check if low voltage check is enabled and that the voltage is okay */ - - bool okayToMakeRecording = true; - - if (configSettings->enableLowVoltageCutoff) { - - AudioMoth_enableSupplyMonitor(); - - AudioMoth_setSupplyMonitorThreshold(MINIMUM_SUPPLY_VOLTAGE); - - okayToMakeRecording = AudioMoth_isSupplyAboveThreshold(); - - } - - /* Make recording if okay */ - - if (okayToMakeRecording) { - - AudioMoth_enableTemperature(); - - int32_t temperature = AudioMoth_getTemperature(); - - AudioMoth_disableTemperature(); - - if (!fileSystemEnabled) fileSystemEnabled = AudioMoth_enableFileSystem(configSettings->sampleRateDivider == 1 ? AM_SD_CARD_HIGH_SPEED : AM_SD_CARD_NORMAL_SPEED); - - if (fileSystemEnabled) { - - recordingState = makeRecording(*timeOfNextRecording, *durationOfNextRecording, *timeAtWhichToSwitchOffLED, extendedBatteryState, temperature, &fileOpenTime, &fileOpenMilliseconds); - - } else { - - if (enableLED) FLASH_LED(Both, LONG_LED_FLASH_DURATION); - - recordingState = SDCARD_WRITE_ERROR; - - } - - } else { - - recordingState = SUPPLY_VOLTAGE_LOW; - - } - - /* Disable low voltage monitor if it was used */ - - if (configSettings->enableLowVoltageCutoff) AudioMoth_disableSupplyMonitor(); - - /* Enable the error warning flashes */ - - if (recordingState == SUPPLY_VOLTAGE_LOW) { - - AudioMoth_delay(LONG_LED_FLASH_DURATION); - - if (enableLED) FLASH_LED(Both, LONG_LED_FLASH_DURATION); - - *numberOfRecordingErrors += 1; - - *numberOfConsecutiveRecordingErrors += 1; - - } else if (recordingState == SDCARD_WRITE_ERROR) { - - *numberOfRecordingErrors += 1; - - *numberOfConsecutiveRecordingErrors += 1; - - } else { - - *numberOfConsecutiveRecordingErrors = 0; - - } - - /* Update the preparation period */ - - if (recordingState == RECORDING_OKAY) { - - int64_t measuredPreparationPeriod = (int64_t)fileOpenTime * MILLISECONDS_IN_SECOND + (int64_t)fileOpenMilliseconds - (int64_t)currentTime * MILLISECONDS_IN_SECOND - (int64_t)currentMilliseconds; - - measuredPreparationPeriod = MIN(MAXIMUM_PREPARATION_PERIOD, MAX(MINIMUM_PREPARATION_PERIOD, measuredPreparationPeriod)); - - if (*recordingPreparationPeriod == INITIAL_PREPARATION_PERIOD) { - - *recordingPreparationPeriod = measuredPreparationPeriod; - - } else { - - *recordingPreparationPeriod = ROUNDED_DIV(*recordingPreparationPeriod * (PREPARATION_PERIOD_SMOOTHING_FACTOR - 1) + measuredPreparationPeriod, PREPARATION_PERIOD_SMOOTHING_FACTOR); - - } - - } - - /* Update the time and calculate earliest schedule start time */ - - AudioMoth_getTime(¤tTime, ¤tMilliseconds); - - uint32_t scheduleTime = currentTime + ROUNDED_UP_DIV(currentMilliseconds + *recordingPreparationPeriod, MILLISECONDS_IN_SECOND); - - /* Schedule the next recording */ - - if (*numberOfConsecutiveRecordingErrors >= MAX_CONSECUTIVE_RECORDING_ERRORS) { - - /* Cancel the schedule */ - - *indexOfNextRecording = 0; - - *timeOfNextRecording = UINT32_MAX; - - *startOfRecordingPeriod = UINT32_MAX; - - *durationOfNextRecording = UINT32_MAX; - - *timeOfNextGPSTimeSetting = UINT32_MAX; - - } else if (switchPosition == AM_SWITCH_CUSTOM) { - - /* Update schedule time as if the recording has ended correctly */ - - if (recordingState == RECORDING_OKAY || recordingState == SUPPLY_VOLTAGE_LOW || recordingState == SDCARD_WRITE_ERROR) { - - scheduleTime = MAX(scheduleTime, *timeOfNextRecording + *durationOfNextRecording); - - } - - /* Calculate the next recording schedule */ - - determineSunriseAndSunsetTimesAndScheduleRecording(scheduleTime); - - } else { - - /* Set parameters to start recording now */ - - *indexOfNextRecording = 0; - - *timeOfNextRecording = scheduleTime; - - *startOfRecordingPeriod = scheduleTime; - - *durationOfNextRecording = UINT32_MAX; - - *timeOfNextGPSTimeSetting = UINT32_MAX; - - } - - /* If recording was cancelled with the magnetic switch then start waiting for the magnetic switch */ - - if (recordingState == MAGNETIC_SWITCH) { - - startWaitingForMagneticSwitch(); - - SAVE_SWITCH_POSITION_AND_POWER_DOWN(DEFAULT_WAIT_INTERVAL); - - } - - /* Power down with short interval if the next recording is due */ - - if (switchPosition == AM_SWITCH_CUSTOM) { - - calculateTimeToNextEvent(currentTime, currentMilliseconds, &timeUntilPreparationStart, &timeUntilNextGPSTimeSetting); - - if (timeUntilPreparationStart < DEFAULT_WAIT_INTERVAL) { - - setBackupFlag(BACKUP_POWERED_DOWN_WITH_SHORT_WAIT_INTERVAL, true); - - SAVE_SWITCH_POSITION_AND_POWER_DOWN(SHORT_WAIT_INTERVAL); - - } - - } - - /* Power down */ - - SAVE_SWITCH_POSITION_AND_POWER_DOWN(DEFAULT_WAIT_INTERVAL); - - } - - /* Update the time from the GPS */ - - if ((timeUntilNextGPSTimeSetting <= 0 || getBackupFlag(BACKUP_SHOULD_SET_TIME_FROM_GPS)) && timeUntilPreparationStart > GPS_MIN_TIME_SETTING_PERIOD * MILLISECONDS_IN_SECOND) { - - /* Set the time from the GPS */ - - AudioMoth_enableFileSystem(AM_SD_CARD_NORMAL_SPEED); - - AM_gpsFixMode_t fixMode = configSettings->enableTimeSettingBeforeAndAfterRecordings ? GPS_FIX_BEFORE_INDIVIDUAL_RECORDING : GPS_FIX_BEFORE_RECORDING_PERIOD; - - if (getBackupFlag(BACKUP_SHOULD_SET_TIME_FROM_GPS)) fixMode += 1; - - if (timeUntilNextGPSTimeSetting > 0) fixMode += 1; - - uint32_t gpsTimeSettingPeriod = configSettings->gpsTimeSettingPeriod == 0 ? GPS_DEFAULT_TIME_SETTING_PERIOD : configSettings->gpsTimeSettingPeriod * SECONDS_IN_MINUTE; - - uint32_t timeOut = MIN(*timeOfNextRecording - ROUNDED_UP_DIV(*recordingPreparationPeriod, MILLISECONDS_IN_SECOND) - GPS_TIME_SETTING_MARGIN, currentTime + gpsTimeSettingPeriod); - - GPS_fixResult_t fixResult = setTimeFromGPS(enableLED, fixMode, timeOut); - - /* Update flag and next scheduled GPS fix time */ - - if (timeUntilNextGPSTimeSetting <= 0) *timeOfNextGPSTimeSetting = UINT32_MAX; - - setBackupFlag(BACKUP_SHOULD_SET_TIME_FROM_GPS, false); - - /* If time setting was cancelled with the magnet switch then start waiting for the magnetic switch */ - - if (fixResult == GPS_CANCELLED_BY_MAGNETIC_SWITCH) startWaitingForMagneticSwitch(); - - /* Power down */ - - SAVE_SWITCH_POSITION_AND_POWER_DOWN(DEFAULT_WAIT_INTERVAL); - - } - - /* Update the post-recording GPS flag if the previous condition was not met */ - - setBackupFlag(BACKUP_SHOULD_SET_TIME_FROM_GPS, false); - - /* Power down if switch position has changed */ - - if (switchPosition != *previousSwitchPosition) SAVE_SWITCH_POSITION_AND_POWER_DOWN(DEFAULT_WAIT_INTERVAL); - - /* Calculate the wait intervals */ - - int64_t waitIntervalMilliseconds = WAITING_LED_FLASH_INTERVAL; - - uint32_t waitIntervalSeconds = WAITING_LED_FLASH_INTERVAL / MILLISECONDS_IN_SECOND; - - if (getBackupFlag(BACKUP_WAITING_FOR_MAGNETIC_SWITCH)) { - - waitIntervalMilliseconds *= MAGNETIC_SWITCH_WAIT_MULTIPLIER; - - waitIntervalSeconds *= MAGNETIC_SWITCH_WAIT_MULTIPLIER; - - } - - /* Wait for the next event whilst flashing the LED */ - - bool startedRealTimeClock = false; - - while (true) { - - /* Update the time */ - - AudioMoth_getTime(¤tTime, ¤tMilliseconds); - - /* Handle magnetic switch event */ - - bool magneticSwitchEvent = magneticSwitch || (magneticSwitchEnabled && GPS_isMagneticSwitchClosed()); - - if (magneticSwitchEvent) { - - if (getBackupFlag(BACKUP_WAITING_FOR_MAGNETIC_SWITCH)) { - - stopWaitingForMagneticSwitch(¤tTime, ¤tMilliseconds); - - if (switchPosition == AM_SWITCH_CUSTOM) { - - if (configSettings->enableTimeSettingFromGPS) { - - setBackupFlag(BACKUP_MUST_SET_TIME_FROM_GPS, true); - - } else if (configSettings->enableLED == false) { - - *timeAtWhichToSwitchOffLED = currentTime + SECONDS_IN_MINUTE; - - } - - } - - } else { - - startWaitingForMagneticSwitch(); - - } - - SAVE_SWITCH_POSITION_AND_POWER_DOWN(DEFAULT_WAIT_INTERVAL); - - } - - /* Handle switch position change */ - - bool switchEvent = switchPositionChanged || AudioMoth_getSwitchPosition() != *previousSwitchPosition; - - if (switchEvent) SAVE_SWITCH_POSITION_AND_POWER_DOWN(DEFAULT_WAIT_INTERVAL); - - /* Calculate the time to the next event */ - - calculateTimeToNextEvent(currentTime, currentMilliseconds, &timeUntilPreparationStart, &timeUntilNextGPSTimeSetting); - - int64_t timeToEarliestEvent = MIN(timeUntilPreparationStart, timeUntilNextGPSTimeSetting); - - /* Flash LED */ - - enableLED = currentTime < *timeAtWhichToSwitchOffLED; - - bool shouldFlashLED = getBackupFlag(BACKUP_WAITING_FOR_MAGNETIC_SWITCH) || (enableLED && shouldSuppressLED == false && timeToEarliestEvent > MINIMUM_LED_FLASH_INTERVAL); - - if (shouldFlashLED) { - - if (*numberOfRecordingErrors > 0) { - - FLASH_LED(Both, WAITING_LED_FLASH_DURATION); - - } else { - - FLASH_LED(Green, WAITING_LED_FLASH_DURATION); - - } - - } - - /* Check there is time to sleep */ - - if (timeToEarliestEvent < waitIntervalMilliseconds) { - - /* Calculate the remaining time to power down */ - - uint32_t timeToWait = timeToEarliestEvent < 0 ? 0 : timeToEarliestEvent; - - SAVE_SWITCH_POSITION_AND_POWER_DOWN(timeToWait); - - } - - /* Start the real time clock if it isn't running */ - - if (startedRealTimeClock == false) { - - AudioMoth_startRealTimeClock(waitIntervalSeconds); - - startedRealTimeClock = true; - - } - - /* Enter deep sleep */ - - AudioMoth_deepSleep(); - - /* Handle time overflow on awakening */ - - AudioMoth_checkAndHandleTimeOverflow(); - - } - -} - -/* Time zone handler */ - -inline void AudioMoth_timezoneRequested(int32_t *timezoneHours, int32_t *timezoneMinutes) { - - if (configSettings->useTimezoneFromAcousticChime && getBackupFlag(BACKUP_ACOUSTIC_TIMEZONE_RECEIVED)) { - - *timezoneHours = *acousticTimezoneMinutes / MINUTES_IN_HOUR; - - *timezoneMinutes = *acousticTimezoneMinutes % MINUTES_IN_HOUR; - - } else { - - *timezoneHours = configSettings->timezoneHours; - - *timezoneMinutes = configSettings->timezoneMinutes; - - } - -} - -/* GPS time handlers */ - -inline void GPS_handleSetTime(uint32_t time, uint32_t milliseconds, int64_t timeDifference, uint32_t measuredClockFrequency) { - - /* Update the time if appropriate */ - - timeSetFromGPS = time; - - if (!AudioMoth_hasTimeBeenSet()) { - - AudioMoth_setTime(time, milliseconds); - - writeGPSLogMessage((char*)compressionBuffer, time, milliseconds, "Time was set from GPS."); - - } else { - - int64_t maximumTimeDifference = (int64_t)GPS_MAXIMUM_DIFFERENCE * (int64_t)MICROSECONDS_IN_SECOND; - - if (timeDifference == 0) { - - writeGPSLogMessage((char*)compressionBuffer, time, milliseconds, "Time was not updated. The internal clock was correct."); - - } else if (timeDifference < -maximumTimeDifference || timeDifference > maximumTimeDifference) { - - writeGPSLogMessage((char*)compressionBuffer, time, milliseconds, "Time was not updated. The discrepancy between the internal clock and the GPS was too large."); - - } else { - - AudioMoth_setTime(time, milliseconds); - - int32_t scaledTimeDifference = (int32_t)ROUNDED_DIV(timeDifference, MICROSECONDS_IN_MILLISECOND / 10); - - bool isFast = scaledTimeDifference > 0; - - uint32_t units = ABS(scaledTimeDifference) / 10; - - uint32_t tenths = ABS(scaledTimeDifference) % 10; - - sprintf(gpsMessageBuffer, "Time was updated. The internal clock was %ld.%ldms %s.", units, tenths, isFast ? "fast" : "slow"); - - writeGPSLogMessage((char*)compressionBuffer, time, milliseconds, gpsMessageBuffer); - - } - - } - - /* Calculate the actual sampling rate */ - - uint32_t intendedClockFrequency = AudioMoth_getClockFrequency(); - - uint32_t intendedSamplingRate = configSettings->sampleRate / configSettings->sampleRateDivider; - - uint32_t clockTicksPerSample = intendedClockFrequency / intendedSamplingRate; - - uint64_t actualSamplingRate = ROUNDED_DIV(GPS_FREQUENCY_PRECISION * (uint64_t)measuredClockFrequency, (uint64_t)clockTicksPerSample); - - uint32_t integerPart = actualSamplingRate / GPS_FREQUENCY_PRECISION; - - uint32_t fractionalPart = actualSamplingRate % GPS_FREQUENCY_PRECISION; - - sprintf(gpsMessageBuffer, "Actual sample rate will be %lu.%03lu Hz.", integerPart, fractionalPart); - - writeGPSLogMessage((char*)compressionBuffer, time, milliseconds, gpsMessageBuffer); - -} - -inline void GPS_handleGetTime(uint32_t *time, uint32_t *milliseconds) { - - AudioMoth_getTime(time, milliseconds); - -} - -inline void GPS_handleGetTimeMicroseconds(uint32_t *time, uint32_t *microseconds) { - - AudioMoth_getTimeMicroseconds(time, microseconds); - -} - -/* GPS format conversion */ - -static int32_t convertToDecimalDegrees(uint32_t degrees, uint32_t minutes, uint32_t tenThousandths, char direction) { - - int32_t value = degrees * GPS_LOCATION_PRECISION; - - value += ROUNDED_DIV(minutes * GPS_LOCATION_PRECISION, MINUTES_IN_DEGREE); - - value += ROUNDED_DIV(tenThousandths * (GPS_LOCATION_PRECISION / 10000), MINUTES_IN_DEGREE); - - if (direction == 'S' || direction == 'W') value *= -1; - - return value; - -} - -/* GPS interrupt handlers */ - -inline void GPS_handleTickEvent() { - - if (gpsTickEventCount == 0) { - - gpsTickEventModulo = GPS_TICK_EVENTS_PER_SECOND; - - if (gpsPPSEvent || gpsFixEvent) { - - gpsTickEventModulo /= 3; - - } else if (gpsMessageEvent) { - - gpsTickEventModulo /= 2; - - } - - gpsMessageEvent = false; - - gpsFixEvent = false; - - gpsPPSEvent = false; - - } - - if (gpsEnableLED) AudioMoth_setRedLED(gpsTickEventCount % gpsTickEventModulo == 0); - - gpsTickEventCount = (gpsTickEventCount + 1) % GPS_TICK_EVENTS_PER_SECOND; - -} - -inline void GPS_handlePPSEvent(uint32_t time, uint32_t milliseconds) { - - writeGPSLogMessage((char*)compressionBuffer, time, milliseconds, "Received pulse per second signal."); - - gpsPPSEvent = true; - -} - -inline void GPS_handleFixEvent(uint32_t time, uint32_t milliseconds, GPS_fixTime_t *fixTime, GPS_fixPosition_t *fixPosition, char *message) { - - *gpsLastFixLatitude = convertToDecimalDegrees(fixPosition->latitudeDegrees, fixPosition->latitudeMinutes, fixPosition->latitudeTenThousandths, fixPosition->latitudeDirection); - - *gpsLastFixLongitude = convertToDecimalDegrees(fixPosition->longitudeDegrees, fixPosition->longitudeMinutes, fixPosition->longitudeTenThousandths, fixPosition->longitudeDirection); - - uint32_t length = sprintf(gpsMessageBuffer, "Received GPS fix - %ld.%06ld°%c %ld.%06ld°%c ", ABS(*gpsLastFixLatitude) / GPS_LOCATION_PRECISION, ABS(*gpsLastFixLatitude) % GPS_LOCATION_PRECISION, fixPosition->latitudeDirection, ABS(*gpsLastFixLongitude) / GPS_LOCATION_PRECISION, ABS(*gpsLastFixLongitude) % GPS_LOCATION_PRECISION, fixPosition->longitudeDirection); - - sprintf(gpsMessageBuffer + length, "at %02u/%02u/%04u %02u:%02u:%02u.%03u UTC.", fixTime->day, fixTime->month, fixTime->year, fixTime->hours, fixTime->minutes, fixTime->seconds, fixTime->milliseconds); - - writeGPSLogMessage((char*)compressionBuffer, time, milliseconds, gpsMessageBuffer); - - gpsFixEvent = true; - -} - -inline void GPS_handleMessageEvent(uint32_t time, uint32_t milliseconds, char *message) { - - if (!gpsFirstMessageReceived) { - - writeGPSLogMessage((char*)compressionBuffer, time, milliseconds, "Received first GPS message."); - - gpsFirstMessageReceived = true; - - } - - gpsMessageEvent = true; - -} - -inline void GPS_handleMagneticSwitchInterrupt() { - - magneticSwitch = true; - - GPS_cancelTimeSetting(GPS_CANCEL_BY_MAGNETIC_SWITCH); - -} - -/* AudioMoth interrupt handlers */ - -inline void AudioMoth_handleMicrophoneChangeInterrupt() { - - microphoneChanged = true; - - AudioConfig_handleMicrophoneChange(); - -} - -inline void AudioMoth_handleSwitchInterrupt() { - - switchPositionChanged = true; - - AudioConfig_cancelAudioConfiguration(); - - GPS_cancelTimeSetting(GPS_CANCEL_BY_SWITCH); - -} - -inline void AudioMoth_handleDirectMemoryAccessInterrupt(bool isPrimaryBuffer, int16_t **nextBuffer) { - - int16_t *source = secondaryBuffer; - - if (isPrimaryBuffer) source = primaryBuffer; - - /* Apply filter to samples */ - - bool thresholdExceeded = DigitalFilter_applyFilter(source, buffers[writeBuffer] + writeBufferIndex, configSettings->sampleRateDivider, numberOfRawSamplesInDMATransfer); - - numberOfDMATransfers += 1; - - /* Update the current buffer index and write buffer if wait period is over */ - - if (numberOfDMATransfers > numberOfDMATransfersToWait) { - - writeIndicator[writeBuffer] |= thresholdExceeded; - - writeBufferIndex += numberOfRawSamplesInDMATransfer / configSettings->sampleRateDivider; - - if (writeBufferIndex == NUMBER_OF_SAMPLES_IN_BUFFER) { - - writeBufferIndex = 0; - - writeBuffer = (writeBuffer + 1) & (NUMBER_OF_BUFFERS - 1); - - writeIndicator[writeBuffer] = false; - - } - - } else if (numberOfDMATransfers == numberOfDMATransfersToWait) { - - AudioMoth_setRedLED(false); - - } else { - - bool state = numberOfDMATransfers % PREPARATION_PERIOD_LED_DIVIDER == 0; - - if (enableLED) AudioMoth_setRedLED(state); - - } - -} - -/* AudioMoth USB message handlers */ - -inline void AudioMoth_usbFirmwareVersionRequested(uint8_t **firmwareVersionPtr) { - - *firmwareVersionPtr = firmwareVersion; - -} - -inline void AudioMoth_usbFirmwareDescriptionRequested(uint8_t **firmwareDescriptionPtr) { - - *firmwareDescriptionPtr = firmwareDescription; - -} - -inline void AudioMoth_usbApplicationPacketRequested(uint32_t messageType, uint8_t *transmitBuffer, uint32_t size) { - - /* Copy the current time to the USB packet */ - - uint32_t currentTime; - - AudioMoth_getTime(¤tTime, NULL); - - memcpy(transmitBuffer + 1, ¤tTime, UINT32_SIZE_IN_BYTES); - - /* Copy the unique ID to the USB packet */ - - memcpy(transmitBuffer + 5, (uint8_t*)AM_UNIQUE_ID_START_ADDRESS, AM_UNIQUE_ID_SIZE_IN_BYTES); - - /* Copy the battery state to the USB packet */ - - uint32_t supplyVoltage = AudioMoth_getSupplyVoltage(); - - AM_batteryState_t batteryState = AudioMoth_getBatteryState(supplyVoltage); - - memcpy(transmitBuffer + 5 + AM_UNIQUE_ID_SIZE_IN_BYTES, &batteryState, 1); - - /* Copy the firmware version to the USB packet */ - - memcpy(transmitBuffer + 6 + AM_UNIQUE_ID_SIZE_IN_BYTES, firmwareVersion, AM_FIRMWARE_VERSION_LENGTH); - - /* Copy the firmware description to the USB packet */ - - memcpy(transmitBuffer + 6 + AM_UNIQUE_ID_SIZE_IN_BYTES + AM_FIRMWARE_VERSION_LENGTH, firmwareDescription, AM_FIRMWARE_DESCRIPTION_LENGTH); - -} - -inline void AudioMoth_usbApplicationPacketReceived(uint32_t messageType, uint8_t* receiveBuffer, uint8_t *transmitBuffer, uint32_t size) { - - /* Make persistent configuration settings data structure */ - - memcpy(&tempPersistentConfigSettings->firmwareVersion, &firmwareVersion, AM_FIRMWARE_VERSION_LENGTH); - - memcpy(&tempPersistentConfigSettings->firmwareDescription, &firmwareDescription, AM_FIRMWARE_DESCRIPTION_LENGTH); - - memcpy(&tempPersistentConfigSettings->configSettings, receiveBuffer + 1, sizeof(configSettings_t)); - - /* Implement energy saver mode changes */ - - if (isEnergySaverMode(&tempPersistentConfigSettings->configSettings)) { - - tempPersistentConfigSettings->configSettings.sampleRate /= 2; - tempPersistentConfigSettings->configSettings.clockDivider /= 2; - tempPersistentConfigSettings->configSettings.sampleRateDivider /= 2; - - } - - /* Copy persistent configuration settings to flash */ - - uint32_t numberOfBytes = ROUND_UP_TO_MULTIPLE(sizeof(persistentConfigSettings_t), UINT32_SIZE_IN_BYTES); - - bool success = AudioMoth_writeToFlashUserDataPage((uint8_t*)tempPersistentConfigSettings, numberOfBytes); - - if (success) { - - /* Copy the persistent configuration to the USB packet */ - - memcpy(transmitBuffer + 1, configSettings, sizeof(configSettings_t)); - - /* Revert energy saver mode changes */ - - configSettings_t *tempConfigSettings = (configSettings_t*)(transmitBuffer + 1); - - if (isEnergySaverMode(tempConfigSettings)) { - - tempConfigSettings->sampleRate *= 2; - tempConfigSettings->clockDivider *= 2; - tempConfigSettings->sampleRateDivider *= 2; - - } - - /* Set the time */ - - AudioMoth_setTime(configSettings->time, USB_CONFIG_TIME_CORRECTION); - - /* Blink the green LED */ - - AudioMoth_blinkDuringUSB(USB_CONFIGURATION_BLINK); - - } else { - - /* Return blank configuration as error indicator */ - - memset(transmitBuffer + 1, 0, sizeof(configSettings_t)); - - } - -} - -/* Audio configuration handlers */ - -inline void AudioConfig_handleAudioConfigurationEvent(AC_audioConfigurationEvent_t event) { - - if (event == AC_EVENT_PULSE) { - - audioConfigPulseCounter = (audioConfigPulseCounter + 1) % AUDIO_CONFIG_PULSE_INTERVAL; - - } else if (event == AC_EVENT_START) { - - audioConfigStateLED = true; - - audioConfigToggleLED = true; - - AudioMoth_getTime(&secondsOfAcousticSignalStart, &millisecondsOfAcousticSignalStart); - - } else if (event == AC_EVENT_BYTE) { - - audioConfigToggleLED = !audioConfigToggleLED; - - } else if (event == AC_EVENT_BIT_ERROR || event == AC_EVENT_CRC_ERROR) { - - audioConfigStateLED = false; - - } - - AudioMoth_setGreenLED((audioConfigStateLED && audioConfigToggleLED) || (!audioConfigStateLED && !audioConfigPulseCounter)); - -} - -inline void AudioConfig_handleAudioConfigurationPacket(uint8_t *receiveBuffer, uint32_t size) { - - uint32_t standardPacketSize = UINT32_SIZE_IN_BYTES + UINT16_SIZE_IN_BYTES; - - bool standardPacket = size == standardPacketSize; - - bool hasLocation = size == (standardPacketSize + ACOUSTIC_LOCATION_SIZE_IN_BYTES) || size == (standardPacketSize + DEPLOYMENT_ID_LENGTH + ACOUSTIC_LOCATION_SIZE_IN_BYTES); - - bool hasDeploymentID = size == (standardPacketSize + DEPLOYMENT_ID_LENGTH) || size == (standardPacketSize + DEPLOYMENT_ID_LENGTH + ACOUSTIC_LOCATION_SIZE_IN_BYTES); - - if (configSettings->requireAcousticLocation && hasLocation == false) { - - audioConfigStateLED = false; - - return; - - } - - if (standardPacket || hasLocation || hasDeploymentID) { - - /* Copy time from the packet */ - - uint32_t time; - - memcpy(&time, receiveBuffer, UINT32_SIZE_IN_BYTES); - - /* Calculate the time correction */ - - uint32_t secondsOfAcousticSignalEnd; - - uint32_t millisecondsOfAcousticSignalEnd; - - AudioMoth_getTime(&secondsOfAcousticSignalEnd, &millisecondsOfAcousticSignalEnd); - - uint32_t millisecondTimeOffset = (secondsOfAcousticSignalEnd - secondsOfAcousticSignalStart) * MILLISECONDS_IN_SECOND + millisecondsOfAcousticSignalEnd - millisecondsOfAcousticSignalStart + AUDIO_CONFIG_TIME_CORRECTION; - - /* Set the time */ - - AudioMoth_setTime(time + millisecondTimeOffset / MILLISECONDS_IN_SECOND, millisecondTimeOffset % MILLISECONDS_IN_SECOND); - - /* Set the timezone */ - - *acousticTimezoneMinutes = *(int16_t*)(receiveBuffer + UINT32_SIZE_IN_BYTES); - - setBackupFlag(BACKUP_ACOUSTIC_TIMEZONE_RECEIVED, true); - - /* Set acoustic location */ - - if (hasLocation) { - - acousticLocation_t location; - - memcpy(&location, receiveBuffer + standardPacketSize, ACOUSTIC_LOCATION_SIZE_IN_BYTES); - - setBackupFlag(BACKUP_ACOUSTIC_LOCATION_RECEIVED, true); - - *acousticLatitude = location.latitude; - - *acousticLongitude = location.longitude * ACOUSTIC_LONGITUDE_MULTIPLIER; - - } - - /* Set deployment ID */ - - if (hasDeploymentID) { - - copyToBackupDomain((uint32_t*)deploymentID, receiveBuffer + standardPacketSize + (hasLocation ? ACOUSTIC_LOCATION_SIZE_IN_BYTES : 0), DEPLOYMENT_ID_LENGTH); - - } - - /* Indicate success */ - - AudioConfig_cancelAudioConfiguration(); - - acousticConfigurationPerformed = true; - - } - - /* Reset receive state */ - - audioConfigStateLED = false; - -} - -/* Clear and encode the compression buffer */ - -static void clearCompressionBuffer() { - - for (uint32_t i = 0; i < COMPRESSION_BUFFER_SIZE_IN_BYTES / NUMBER_OF_BYTES_IN_SAMPLE; i += 1) { - - compressionBuffer[i] = 0; - - } - -} - -static void encodeCompressionBuffer(uint32_t numberOfCompressedBuffers) { - - for (uint32_t i = 0; i < UINT32_SIZE_IN_BITS; i += 1) { - - compressionBuffer[i] = numberOfCompressedBuffers & 0x01 ? 1 : -1; - - numberOfCompressedBuffers >>= 1; - - } - - for (uint32_t i = UINT32_SIZE_IN_BITS; i < COMPRESSION_BUFFER_SIZE_IN_BYTES / NUMBER_OF_BYTES_IN_SAMPLE; i += 1) { - - compressionBuffer[i] = 0; - - } - -} - -/* Generate foldername and filename from time */ - -static void generateFolderAndFilename(char *foldername, char *filename, uint32_t timestamp, bool triggeredRecording) { - - struct tm time; - - int32_t timezoneHours, timezoneMinutes; - - AudioMoth_timezoneRequested(&timezoneHours, &timezoneMinutes); - - time_t rawTime = timestamp + timezoneHours * SECONDS_IN_HOUR + timezoneMinutes * SECONDS_IN_MINUTE; - - gmtime_r(&rawTime, &time); - - sprintf(foldername, "%04d%02d%02d", YEAR_OFFSET + time.tm_year, MONTH_OFFSET + time.tm_mon, time.tm_mday); - - uint32_t length = 0; - - if (configSettings->enableDailyFolders) { - - length += sprintf(filename, "%s/", foldername); - - } - - if (configSettings->enableFilenameWithDeviceID) { - - uint8_t *source = memcmp(deploymentID, defaultDeploymentID, DEPLOYMENT_ID_LENGTH) ? deploymentID : (uint8_t*)AM_UNIQUE_ID_START_ADDRESS; - - length += sprintf(filename + length, SERIAL_NUMBER "_", FORMAT_SERIAL_NUMBER(source)); - - } - - length += sprintf(filename + length, "%s_%02d%02d%02d", foldername, time.tm_hour, time.tm_min, time.tm_sec); - - char *extension = triggeredRecording ? "T.WAV" : ".WAV"; - - strcpy(filename + length, extension); - -} - -/* Save recording to SD card */ - -static AM_recordingState_t makeRecording(uint32_t timeOfNextRecording, uint32_t recordDuration, uint32_t timeAtWhichToSwitchOffLED, AM_extendedBatteryState_t extendedBatteryState, int32_t temperature, uint32_t *fileOpenTime, uint32_t *fileOpenMilliseconds) { - - /* Initialise buffers */ - - writeBuffer = 0; - - writeBufferIndex = 0; - - buffers[0] = (int16_t*)AM_EXTERNAL_SRAM_START_ADDRESS; - - for (uint32_t i = 1; i < NUMBER_OF_BUFFERS; i += 1) { - buffers[i] = buffers[i - 1] + NUMBER_OF_SAMPLES_IN_BUFFER; - } - - /* Calculate effective sample rate */ - - uint32_t effectiveSampleRate = configSettings->sampleRate / configSettings->sampleRateDivider; - - /* Set up the digital filter */ - - uint32_t blockingFilterFrequency = configSettings->disable48HzDCBlockingFilter ? LOW_DC_BLOCKING_FREQ : DEFAULT_DC_BLOCKING_FREQ; - - if (configSettings->lowerFilterFreq == 0 && configSettings->higherFilterFreq == 0) { - - requestedFilterType = NO_FILTER; - - DigitalFilter_designHighPassFilter(effectiveSampleRate, blockingFilterFrequency); - - } else if (configSettings->lowerFilterFreq == UINT16_MAX) { - - requestedFilterType = LOW_PASS_FILTER; - - DigitalFilter_designBandPassFilter(effectiveSampleRate, blockingFilterFrequency, FILTER_FREQ_MULTIPLIER * configSettings->higherFilterFreq); - - } else if (configSettings->higherFilterFreq == UINT16_MAX) { - - requestedFilterType = HIGH_PASS_FILTER; - - DigitalFilter_designHighPassFilter(effectiveSampleRate, MAX(blockingFilterFrequency, FILTER_FREQ_MULTIPLIER * configSettings->lowerFilterFreq)); - - } else { - - requestedFilterType = BAND_PASS_FILTER; - - DigitalFilter_designBandPassFilter(effectiveSampleRate, MAX(blockingFilterFrequency, FILTER_FREQ_MULTIPLIER * configSettings->lowerFilterFreq), FILTER_FREQ_MULTIPLIER * configSettings->higherFilterFreq); - - } - - /* Calculate the number of samples in each DMA transfer (while ensuring that number of samples written to the SRAM buffer on each DMA transfer is a power of two so each SRAM buffer is filled after an integer number of DMA transfers) */ - - numberOfRawSamplesInDMATransfer = MAXIMUM_SAMPLES_IN_DMA_TRANSFER / configSettings->sampleRateDivider; - - while (numberOfRawSamplesInDMATransfer & (numberOfRawSamplesInDMATransfer - 1)) { - - numberOfRawSamplesInDMATransfer = numberOfRawSamplesInDMATransfer & (numberOfRawSamplesInDMATransfer - 1); - - } - - numberOfRawSamplesInDMATransfer *= configSettings->sampleRateDivider; - - /* Calculate the minimum amplitude threshold duration */ - - uint32_t minimumNumberOfTriggeredBuffersToWrite = ROUNDED_UP_DIV(configSettings->minimumTriggerDuration * effectiveSampleRate, NUMBER_OF_SAMPLES_IN_BUFFER); - - /* Calculate LED flash timescale when no triggered buffers are detected */ - - uint32_t triggeredRecordingFlashInterval = MAX(1, ROUNDED_DIV(effectiveSampleRate, NUMBER_OF_SAMPLES_IN_BUFFER)); - - /* Initialise termination conditions */ - - microphoneChanged = false; - - bool supplyVoltageLow = false; - - /* Enable the external SRAM and microphone and initialise direct memory access */ - - AudioMoth_enableExternalSRAM(); - - AudioMoth_ignoreExternalMicrophone(false); - - AM_gainRange_t gainRange = configSettings->enableLowGainRange ? AM_LOW_GAIN_RANGE : AM_NORMAL_GAIN_RANGE; - - AM_externalMicrophone_t externalMicrophone = AudioMoth_enableMicrophone(gainRange, configSettings->gain, configSettings->clockDivider, configSettings->acquisitionCycles, configSettings->oversampleRate); - - AudioMoth_initialiseDirectMemoryAccess(primaryBuffer, secondaryBuffer, numberOfRawSamplesInDMATransfer); - - /* Calculate the sample multiplier */ - - float sampleMultiplier = 16.0f / (float)(configSettings->oversampleRate * configSettings->sampleRateDivider); - - if (AudioMoth_hasInvertedOutput()) sampleMultiplier = -sampleMultiplier; - - if (externalMicrophone == AM_EXTERNAL_PRESENT_AND_USED) sampleMultiplier = -sampleMultiplier; - - DigitalFilter_setAdditionalGain(sampleMultiplier); - - /* Determine if amplitude threshold is enabled */ - - bool frequencyTriggerEnabled = configSettings->enableFrequencyTrigger; - - bool amplitudeThresholdEnabled = frequencyTriggerEnabled ? false : configSettings->amplitudeThreshold > 0 || configSettings->enableAmplitudeThresholdDecibelScale || configSettings->enableAmplitudeThresholdPercentageScale; - - /* Configure the digital filter for the appropriate trigger */ - - if (frequencyTriggerEnabled) { - - uint32_t frequency = MIN(effectiveSampleRate / 2, FILTER_FREQ_MULTIPLIER * configSettings->frequencyTriggerCentreFrequency); - - uint32_t windowLength = MIN(FREQUENCY_TRIGGER_WINDOW_MAXIMUM, MAX(FREQUENCY_TRIGGER_WINDOW_MINIMUM, 1 << configSettings->frequencyTriggerWindowLengthShift)); - - float percentageThreshold = (float)configSettings->frequencyTriggerThresholdPercentageMantissa * powf(10.0f, (float)configSettings->frequencyTriggerThresholdPercentageExponent); - - DigitalFilter_setFrequencyTrigger(windowLength, effectiveSampleRate, frequency, percentageThreshold); - - } - - if (amplitudeThresholdEnabled) { - - DigitalFilter_setAmplitudeThreshold(configSettings->amplitudeThreshold); - - } - - /* Set the initial header comment details */ - - AM_recordingState_t recordingState = SDCARD_WRITE_ERROR; - - setHeaderDetails(&wavHeader, effectiveSampleRate, 0, 0); - - setHeaderComment(&wavHeader, configSettings, timeOfNextRecording, (uint8_t*)AM_UNIQUE_ID_START_ADDRESS, deploymentID, defaultDeploymentID, extendedBatteryState, temperature, externalMicrophone == AM_EXTERNAL_PRESENT_AND_USED, recordingState, requestedFilterType); - - /* Show LED for SD card activity */ - - if (enableLED) AudioMoth_setRedLED(true); - - /* Open a file with the current local time as the name */ - - static char filename[MAXIMUM_FILE_NAME_LENGTH]; - - static char foldername[MAXIMUM_FILE_NAME_LENGTH]; - - generateFolderAndFilename(foldername, filename, timeOfNextRecording, frequencyTriggerEnabled || amplitudeThresholdEnabled); - - if (configSettings->enableDailyFolders) { - - bool directoryExists = AudioMoth_doesDirectoryExist(foldername); - - if (directoryExists == false) FLASH_LED_IF_ENABLED_AND_RETURN_ON_ERROR(AudioMoth_makeDirectory(foldername)); - - } - - FLASH_LED_IF_ENABLED_AND_RETURN_ON_ERROR(AudioMoth_openFile(filename)); - - /* Write the header */ - - FLASH_LED_IF_ENABLED_AND_RETURN_ON_ERROR(AudioMoth_writeToFile(&wavHeader, sizeof(wavHeader_t))); - - FLASH_LED_IF_ENABLED_AND_RETURN_ON_ERROR(AudioMoth_syncFile()); - - FLASH_LED_IF_ENABLED_AND_RETURN_ON_ERROR(AudioMoth_seekInFile(0)); - - AudioMoth_setRedLED(false); - - /* Measure the time difference from the start time */ - - uint32_t fileOpenMicroseconds; - - AudioMoth_getTimeMicroseconds(fileOpenTime, &fileOpenMicroseconds); - - *fileOpenMilliseconds = ROUNDED_DIV(fileOpenMicroseconds, MICROSECONDS_IN_MILLISECOND); - - /* Calculate time correction for sample rate due to file header */ - - uint32_t numberOfSamplesInHeader = sizeof(wavHeader_t) / NUMBER_OF_BYTES_IN_SAMPLE; - - uint32_t sampleRateTimeOffset = ROUNDED_DIV(numberOfSamplesInHeader * MICROSECONDS_IN_SECOND, effectiveSampleRate); - - /* Calculate time until the recording should start */ - - int32_t microsecondsUntilRecordingShouldStart = (int64_t)timeOfNextRecording * MICROSECONDS_IN_SECOND - (int64_t)*fileOpenTime * MICROSECONDS_IN_SECOND - (int64_t)fileOpenMicroseconds - (int64_t)sampleRateTimeOffset; - - /* Calculate the actual recording start time if the intended start has been missed */ - - uint32_t timeOffset = microsecondsUntilRecordingShouldStart < 0 ? 1 - microsecondsUntilRecordingShouldStart / MICROSECONDS_IN_SECOND : 0; - - recordDuration = timeOffset >= recordDuration ? 0 : recordDuration - timeOffset; - - microsecondsUntilRecordingShouldStart += (int64_t)timeOffset * MICROSECONDS_IN_SECOND; - - uint32_t actualRecordingStartTime = timeOfNextRecording + timeOffset; - - /* Calculate the period to wait before starting the DMA transfers */ - - uint64_t numberOfRawSamplesToWait = ROUNDED_DIV((uint64_t)microsecondsUntilRecordingShouldStart * (uint64_t)configSettings->sampleRate, MICROSECONDS_IN_SECOND); - - numberOfDMATransfersToWait = (uint32_t)(numberOfRawSamplesToWait / (uint64_t)numberOfRawSamplesInDMATransfer); - - uint32_t remainingNumberOfRawSamples = (uint32_t)(numberOfRawSamplesToWait % (uint64_t)numberOfRawSamplesInDMATransfer); - - uint32_t remainingMicrosecondsToWait = ROUNDED_DIV(remainingNumberOfRawSamples * MICROSECONDS_IN_SECOND, configSettings->sampleRate); - - /* Calculate updated recording parameters */ - - uint32_t maximumNumberOfSeconds = MAXIMUM_WAV_DATA_SIZE / NUMBER_OF_BYTES_IN_SAMPLE / effectiveSampleRate; - - bool fileSizeLimited = (recordDuration > maximumNumberOfSeconds); - - uint32_t numberOfSamples = effectiveSampleRate * (fileSizeLimited ? maximumNumberOfSeconds : recordDuration); - - /* Calculate the number of samples at which LED should be turned off */ - - uint32_t numberOfSamplesToSwitchOffLED = UINT32_MAX; - - if (enableLED && timeAtWhichToSwitchOffLED < UINT32_MAX) { - - if (timeAtWhichToSwitchOffLED < actualRecordingStartTime) { - - enableLED = false; - - } else if (timeAtWhichToSwitchOffLED < actualRecordingStartTime + recordDuration) { - - numberOfSamplesToSwitchOffLED = numberOfSamplesInHeader + effectiveSampleRate * (timeAtWhichToSwitchOffLED - actualRecordingStartTime); - - } - - } - - /* Initialise main loop variables */ - - uint32_t readBuffer = 0; - - uint32_t samplesWritten = 0; - - uint32_t buffersProcessed = 0; - - uint32_t numberOfCompressedBuffers = 0; - - uint32_t totalNumberOfCompressedSamples = 0; - - uint32_t numberOfTriggeredBuffersWritten = 0; - - uint32_t numberOfBuffersProcessedSinceLastWrite = 0; - - bool triggerHasOccurred = false; - - /* Start processing DMA transfers */ - - numberOfDMATransfers = 0; - - if (remainingMicrosecondsToWait > DELAY_CORRECTION_MICROSECONDS) { - - remainingMicrosecondsToWait -= DELAY_CORRECTION_MICROSECONDS; - - AudioMoth_delayMicroseconds(remainingMicrosecondsToWait); - - } - - AudioMoth_startMicrophoneSamples(configSettings->sampleRate); - - /* Main recording loop */ - - while (samplesWritten < numberOfSamples + numberOfSamplesInHeader && !microphoneChanged && !switchPositionChanged && !magneticSwitch && !supplyVoltageLow) { - - while (readBuffer != writeBuffer && samplesWritten < numberOfSamples + numberOfSamplesInHeader && !microphoneChanged && !switchPositionChanged && !magneticSwitch && !supplyVoltageLow) { - - /* Determine the appropriate number of bytes to the SD card */ - - uint32_t numberOfSamplesToWrite = MIN(numberOfSamples + numberOfSamplesInHeader - samplesWritten, NUMBER_OF_SAMPLES_IN_BUFFER); - - /* Check if this buffer should actually be written to the SD card */ - - bool writeIndicated = (amplitudeThresholdEnabled == false && frequencyTriggerEnabled == false) || writeIndicator[readBuffer]; - - if (frequencyTriggerEnabled && configSettings->sampleRateDivider > 1) writeIndicated = DigitalFilter_applyFrequencyTrigger(buffers[readBuffer], NUMBER_OF_SAMPLES_IN_BUFFER); - - /* Ensure the minimum number of buffers will be written */ - - triggerHasOccurred |= writeIndicated; - - numberOfTriggeredBuffersWritten = writeIndicated ? 0 : numberOfTriggeredBuffersWritten + 1; - - bool shouldWriteThisSector = writeIndicated || (triggerHasOccurred && numberOfTriggeredBuffersWritten < minimumNumberOfTriggeredBuffersToWrite); - - /* Compress the buffer or write the buffer to SD card */ - - if (shouldWriteThisSector == false && buffersProcessed > 0 && numberOfSamplesToWrite == NUMBER_OF_SAMPLES_IN_BUFFER) { - - /* Increment the number of compressed buffers */ - - numberOfCompressedBuffers += NUMBER_OF_BYTES_IN_SAMPLE * NUMBER_OF_SAMPLES_IN_BUFFER / COMPRESSION_BUFFER_SIZE_IN_BYTES; - - /* Show LED at appropriate interval if no SD card writing has occurred */ - - numberOfBuffersProcessedSinceLastWrite += 1; - - if (numberOfBuffersProcessedSinceLastWrite % triggeredRecordingFlashInterval == 0) { - - if (enableLED) AudioMoth_setRedLED(true); - - AudioMoth_delay(TRIGGERED_RECORDING_FLASH_DURATION); - - AudioMoth_setRedLED(false); - - } - - } else { - - /* Light LED during SD card write if appropriate */ - - if (enableLED) AudioMoth_setRedLED(true); - - /* Encode and write compression buffer */ - - if (numberOfCompressedBuffers > 0) { - - encodeCompressionBuffer(numberOfCompressedBuffers); - - totalNumberOfCompressedSamples += (numberOfCompressedBuffers - 1) * COMPRESSION_BUFFER_SIZE_IN_BYTES / NUMBER_OF_BYTES_IN_SAMPLE; - - FLASH_LED_IF_ENABLED_AND_RETURN_ON_ERROR(AudioMoth_writeToFile(compressionBuffer, COMPRESSION_BUFFER_SIZE_IN_BYTES)); - - numberOfCompressedBuffers = 0; - - } - - /* Either write the buffer or write a blank buffer */ - - if (shouldWriteThisSector) { - - if (buffersProcessed == 0) memcpy(buffers[readBuffer], &wavHeader, sizeof(wavHeader_t)); - - FLASH_LED_IF_ENABLED_AND_RETURN_ON_ERROR(AudioMoth_writeToFile(buffers[readBuffer], NUMBER_OF_BYTES_IN_SAMPLE * numberOfSamplesToWrite)); - - } else { - - clearCompressionBuffer(); - - uint32_t blankBuffersWritten = 0; - - uint32_t numberOfBlankSamplesToWrite = numberOfSamplesToWrite; - - while (numberOfBlankSamplesToWrite > 0) { - - uint32_t numberOfSamples = MIN(numberOfBlankSamplesToWrite, COMPRESSION_BUFFER_SIZE_IN_BYTES / NUMBER_OF_BYTES_IN_SAMPLE); - - bool writeHeader = buffersProcessed == 0 && blankBuffersWritten == 0 && numberOfSamples >= sizeof(wavHeader_t) / NUMBER_OF_BYTES_IN_SAMPLE; - - if (writeHeader) memcpy(compressionBuffer, &wavHeader, sizeof(wavHeader_t)); - - FLASH_LED_IF_ENABLED_AND_RETURN_ON_ERROR(AudioMoth_writeToFile(compressionBuffer, NUMBER_OF_BYTES_IN_SAMPLE * numberOfSamples)); - - if (writeHeader) memset(compressionBuffer, 0, sizeof(wavHeader_t)); - - numberOfBlankSamplesToWrite -= numberOfSamples; - - blankBuffersWritten += 1; - - } - - } - - /* Clear LED */ - - AudioMoth_setRedLED(false); - - /* Reset the buffer counter */ - - numberOfBuffersProcessedSinceLastWrite = 0; - - } - - /* Increment buffer counters */ - - readBuffer = (readBuffer + 1) & (NUMBER_OF_BUFFERS - 1); - - samplesWritten += numberOfSamplesToWrite; - - buffersProcessed += 1; - - } - - /* Check the voltage level */ - - if (configSettings->enableLowVoltageCutoff && AudioMoth_isSupplyAboveThreshold() == false) { - - supplyVoltageLow = true; - - } - - /* Turn LED off if appropriate time has passed */ - - if (samplesWritten > numberOfSamplesToSwitchOffLED) enableLED = false; - - /* Sleep until next DMA transfer is complete */ - - AudioMoth_sleep(); - - } - - /* Write the compression buffer files at the end */ - - if (samplesWritten < numberOfSamples + numberOfSamplesInHeader && numberOfCompressedBuffers > 0) { - - /* Light LED during SD card write if appropriate */ - - if (enableLED) AudioMoth_setRedLED(true); - - /* Encode and write compression buffer */ - - encodeCompressionBuffer(numberOfCompressedBuffers); - - totalNumberOfCompressedSamples += (numberOfCompressedBuffers - 1) * COMPRESSION_BUFFER_SIZE_IN_BYTES / NUMBER_OF_BYTES_IN_SAMPLE; - - FLASH_LED_IF_ENABLED_AND_RETURN_ON_ERROR(AudioMoth_writeToFile(compressionBuffer, COMPRESSION_BUFFER_SIZE_IN_BYTES)); - - /* Clear LED */ - - AudioMoth_setRedLED(false); - - } - - /* Determine recording state */ - - recordingState = microphoneChanged ? MICROPHONE_CHANGED : - switchPositionChanged ? SWITCH_CHANGED : - magneticSwitch ? MAGNETIC_SWITCH : - supplyVoltageLow ? SUPPLY_VOLTAGE_LOW : - fileSizeLimited ? FILE_SIZE_LIMITED : - RECORDING_OKAY; - - /* Generate the new file name if necessary */ - - static char newFilename[MAXIMUM_FILE_NAME_LENGTH]; - - if (timeOffset > 0) { - - generateFolderAndFilename(foldername, newFilename, actualRecordingStartTime, frequencyTriggerEnabled || amplitudeThresholdEnabled); - - } - - /* Write the GUANO data */ - - bool gpsLocationReceived = getBackupFlag(BACKUP_GPS_LOCATION_RECEIVED); - - bool acousticLocationReceived = getBackupFlag(BACKUP_ACOUSTIC_LOCATION_RECEIVED); - - uint32_t guanoDataSize = writeGuanoData((char*)compressionBuffer, configSettings, actualRecordingStartTime, gpsLocationReceived, gpsLastFixLatitude, gpsLastFixLongitude, acousticLocationReceived, acousticLatitude, acousticLongitude, firmwareDescription, firmwareVersion, (uint8_t*)AM_UNIQUE_ID_START_ADDRESS, deploymentID, defaultDeploymentID, timeOffset > 0 ? newFilename : filename, extendedBatteryState, temperature, requestedFilterType); - - FLASH_LED_IF_ENABLED_AND_RETURN_ON_ERROR(AudioMoth_writeToFile(compressionBuffer, guanoDataSize)); - - /* Initialise the WAV header */ - - samplesWritten = MAX(numberOfSamplesInHeader, samplesWritten); - - setHeaderDetails(&wavHeader, effectiveSampleRate, samplesWritten - numberOfSamplesInHeader - totalNumberOfCompressedSamples, guanoDataSize); - - setHeaderComment(&wavHeader, configSettings, actualRecordingStartTime, (uint8_t*)AM_UNIQUE_ID_START_ADDRESS, deploymentID, defaultDeploymentID, extendedBatteryState, temperature, externalMicrophone == AM_EXTERNAL_PRESENT_AND_USED, recordingState, requestedFilterType); - - /* Write the header */ - - if (enableLED) AudioMoth_setRedLED(true); - - FLASH_LED_IF_ENABLED_AND_RETURN_ON_ERROR(AudioMoth_seekInFile(0)); - - FLASH_LED_IF_ENABLED_AND_RETURN_ON_ERROR(AudioMoth_writeToFile(&wavHeader, sizeof(wavHeader_t))); - - /* Close the file */ - - FLASH_LED_IF_ENABLED_AND_RETURN_ON_ERROR(AudioMoth_closeFile()); - - AudioMoth_setRedLED(false); - - /* Rename the file if necessary */ - - if (timeOffset > 0) { - - if (enableLED) AudioMoth_setRedLED(true); - - FLASH_LED_IF_ENABLED_AND_RETURN_ON_ERROR(AudioMoth_renameFile(filename, newFilename)); - - AudioMoth_setRedLED(false); - - } - - /* Return recording state */ - - return recordingState; - -} - -/* Determine sunrise and sunset and schedule recording */ - -static void determineSunriseAndSunsetTimesAndScheduleRecording(uint32_t currentTime) { - - uint32_t scheduleTime = currentTime; - - uint32_t currentTimeOfNextRecording = *timeOfNextRecording; - - uint32_t currentStartOfRecordingPeriod = *startOfRecordingPeriod; - - /* Calculate initial sunrise and sunset time if appropriate */ - - if (configSettings->enableSunRecording && *timeOfNextSunriseSunsetCalculation == 0) { - - determineSunriseAndSunsetTimes(scheduleTime); - - determineTimeOfNextSunriseSunsetCalculation(scheduleTime, timeOfNextSunriseSunsetCalculation); - - determineSunriseAndSunsetTimes(*timeOfNextSunriseSunsetCalculation - SECONDS_IN_DAY); - - determineTimeOfNextSunriseSunsetCalculation(scheduleTime, timeOfNextSunriseSunsetCalculation); - - scheduleRecording(scheduleTime, timeOfNextRecording, indexOfNextRecording, durationOfNextRecording, startOfRecordingPeriod, NULL); - - if (*timeOfNextSunriseSunsetCalculation < *timeOfNextRecording) *timeOfNextSunriseSunsetCalculation += SECONDS_IN_DAY; - - } else { - - scheduleRecording(scheduleTime, timeOfNextRecording, indexOfNextRecording, durationOfNextRecording, startOfRecordingPeriod, NULL); - - } - - /* Check if sunrise and sunset should be recalculated */ - - if (configSettings->enableSunRecording && *timeOfNextRecording >= *timeOfNextSunriseSunsetCalculation) { - - scheduleTime = MAX(scheduleTime, *timeOfNextSunriseSunsetCalculation); - - determineSunriseAndSunsetTimes(scheduleTime); - - determineTimeOfNextSunriseSunsetCalculation(scheduleTime, timeOfNextSunriseSunsetCalculation); - - scheduleRecording(scheduleTime, timeOfNextRecording, indexOfNextRecording, durationOfNextRecording, startOfRecordingPeriod, NULL); - - if (*timeOfNextSunriseSunsetCalculation < *timeOfNextRecording) *timeOfNextSunriseSunsetCalculation += SECONDS_IN_DAY; - - } - - /* Finished if not using GPS */ - - if (configSettings->enableTimeSettingFromGPS == false) return; - - /* Update GPS time setting time */ - - bool shouldSetTimeFromGPS = false; - - uint32_t gpsTimeSettingPeriod = configSettings->gpsTimeSettingPeriod == 0 ? GPS_DEFAULT_TIME_SETTING_PERIOD : configSettings->gpsTimeSettingPeriod * SECONDS_IN_MINUTE; - - if (configSettings->enableTimeSettingBeforeAndAfterRecordings) { - - *timeOfNextGPSTimeSetting = *timeOfNextRecording - gpsTimeSettingPeriod; - - shouldSetTimeFromGPS = currentTimeOfNextRecording != UINT32_MAX && currentTimeOfNextRecording != *timeOfNextRecording; - - } else { - - *timeOfNextGPSTimeSetting = *startOfRecordingPeriod - gpsTimeSettingPeriod; - - shouldSetTimeFromGPS = currentStartOfRecordingPeriod != UINT32_MAX && currentStartOfRecordingPeriod != *startOfRecordingPeriod; - - } - - setBackupFlag(BACKUP_SHOULD_SET_TIME_FROM_GPS, shouldSetTimeFromGPS); - -} - -/* Determine sunrise and sunset calculation time */ - -static void determineTimeOfNextSunriseSunsetCalculation(uint32_t currentTime, uint32_t *timeOfNextSunriseSunsetCalculation) { - - /* Check if limited by earliest recording time */ - - uint32_t earliestRecordingTime = configSettings->earliestRecordingTime; - - if (earliestRecordingTime > 0) { - - if (getBackupFlag(BACKUP_ACOUSTIC_TIMEZONE_RECEIVED) && configSettings->useTimezoneFromAcousticChime && configSettings->adjustScheduleUsingTimezoneFromAcousticChime) { - - earliestRecordingTime += configSettings->timezoneHours * SECONDS_IN_HOUR + configSettings->timezoneMinutes * SECONDS_IN_MINUTE - *acousticTimezoneMinutes * SECONDS_IN_MINUTE; - - } - - currentTime = MAX(currentTime, earliestRecordingTime); - - } - - /* Determine when the middle of largest gap between recording periods occurs */ - - uint32_t startOfGapMinutes, endOfGapMinutes; - - if (*numberOfSunRecordingPeriods == 1) { - - startOfGapMinutes = firstSunRecordingPeriod->endMinutes; - - endOfGapMinutes = firstSunRecordingPeriod->startMinutes; - - } else { - - uint32_t gapFromFirstPeriodToSecondPeriod = secondSunRecordingPeriod->startMinutes - firstSunRecordingPeriod->endMinutes; - - uint32_t gapFromSecondPeriodsToFirstPeriod = secondSunRecordingPeriod->endMinutes < secondSunRecordingPeriod->startMinutes ? firstSunRecordingPeriod->startMinutes - secondSunRecordingPeriod->endMinutes : MINUTES_IN_DAY - secondSunRecordingPeriod->endMinutes + firstSunRecordingPeriod->startMinutes; - - if (gapFromFirstPeriodToSecondPeriod > gapFromSecondPeriodsToFirstPeriod) { - - startOfGapMinutes = firstSunRecordingPeriod->endMinutes; - - endOfGapMinutes = secondSunRecordingPeriod->startMinutes; - - } else { - - startOfGapMinutes = secondSunRecordingPeriod->endMinutes; - - endOfGapMinutes = firstSunRecordingPeriod->startMinutes; - - } - - } - - uint32_t calculationMinutes = endOfGapMinutes + startOfGapMinutes; - - if (endOfGapMinutes < startOfGapMinutes) calculationMinutes += MINUTES_IN_DAY; - - calculationMinutes /= 2; - - calculationMinutes = calculationMinutes % MINUTES_IN_DAY; - - /* Determine the number of seconds of this day */ - - struct tm time; - - time_t rawTime = currentTime; - - gmtime_r(&rawTime, &time); - - uint32_t currentSeconds = SECONDS_IN_HOUR * time.tm_hour + SECONDS_IN_MINUTE * time.tm_min + time.tm_sec; - - /* Determine the time of the next sunrise and sunset calculation */ - - uint32_t calculationTime = currentTime - currentSeconds + SECONDS_IN_MINUTE * calculationMinutes; - - if (calculationTime <= currentTime) calculationTime += SECONDS_IN_DAY; - - *timeOfNextSunriseSunsetCalculation = calculationTime; - -} - -/* Determine sunrise and sunset times */ - -static void determineSunriseAndSunsetTimes(uint32_t currentTime) { - - /* Calculate future sunrise and sunset time if recording is limited by earliest recording time */ - - uint32_t earliestRecordingTime = configSettings->earliestRecordingTime; - - if (earliestRecordingTime > 0) { - - if (getBackupFlag(BACKUP_ACOUSTIC_TIMEZONE_RECEIVED) && configSettings->useTimezoneFromAcousticChime && configSettings->adjustScheduleUsingTimezoneFromAcousticChime) { - - earliestRecordingTime += configSettings->timezoneHours * SECONDS_IN_HOUR + configSettings->timezoneMinutes * SECONDS_IN_MINUTE - *acousticTimezoneMinutes * SECONDS_IN_MINUTE; - - } - - currentTime = MAX(currentTime, earliestRecordingTime); - - } - - /* Determine sunrise and sunset times */ - - SR_trend_t trend; - - SR_solution_t solution; - - uint32_t sunriseMinutes, sunsetMinutes; - - float latitude = getBackupFlag(BACKUP_GPS_LOCATION_RECEIVED) ? (float)*gpsLatitude / (float)GPS_LOCATION_PRECISION : getBackupFlag(BACKUP_ACOUSTIC_LOCATION_RECEIVED) ? (float)*acousticLatitude / (float)ACOUSTIC_LOCATION_PRECISION : (float)configSettings->latitude / (float)CONFIG_LOCATION_PRECISION; - - float longitude = getBackupFlag(BACKUP_GPS_LOCATION_RECEIVED) ? (float)*gpsLongitude / (float)GPS_LOCATION_PRECISION : getBackupFlag(BACKUP_ACOUSTIC_LOCATION_RECEIVED) ? (float)*acousticLongitude / (float)ACOUSTIC_LOCATION_PRECISION : (float)configSettings->longitude / (float)CONFIG_LOCATION_PRECISION; - - Sunrise_calculateFromUnix(configSettings->sunRecordingEvent, currentTime, latitude, longitude, &solution, &trend, &sunriseMinutes, &sunsetMinutes); - - /* Calculate maximum recording duration */ - - uint32_t minimumRecordingGap = MAX(MINIMUM_SUN_RECORDING_GAP, SUN_RECORDING_GAP_MULTIPLIER * configSettings->sunRoundingMinutes); - - uint32_t maximumRecordingDuration = MINUTES_IN_DAY - minimumRecordingGap; - - /* Round the sunrise and sunset times */ - - uint32_t roundedSunriseMinutes = configSettings->sunRoundingMinutes > 0 ? UNSIGNED_ROUND(sunriseMinutes, configSettings->sunRoundingMinutes) : sunriseMinutes; - - uint32_t roundedSunsetMinutes = configSettings->sunRoundingMinutes > 0 ? UNSIGNED_ROUND(sunsetMinutes, configSettings->sunRoundingMinutes) : sunsetMinutes; - - /* Calculate start and end of potential recording periods */ - - uint32_t beforeSunrise = (MINUTES_IN_DAY + roundedSunriseMinutes - configSettings->beforeSunriseMinutes) % MINUTES_IN_DAY; - - uint32_t afterSunrise = (MINUTES_IN_DAY + roundedSunriseMinutes + configSettings->afterSunriseMinutes) % MINUTES_IN_DAY; - - uint32_t beforeSunset = (MINUTES_IN_DAY + roundedSunsetMinutes - configSettings->beforeSunsetMinutes) % MINUTES_IN_DAY; - - uint32_t afterSunset = (MINUTES_IN_DAY + roundedSunsetMinutes + configSettings->afterSunsetMinutes) % MINUTES_IN_DAY; - - /* Determine schedule */ - - recordingPeriod_t tempRecordingPeriod; - - if (configSettings->sunRecordingMode == SUNRISE_RECORDING) { - - /* Recording from before sunrise to after sunrise */ - - *numberOfSunRecordingPeriods = 1; - - tempRecordingPeriod.startMinutes = beforeSunrise; - - tempRecordingPeriod.endMinutes = afterSunrise; - - copyToBackupDomain((uint32_t*)firstSunRecordingPeriod, (uint8_t*)&tempRecordingPeriod, sizeof(recordingPeriod_t)); - - } else if (configSettings->sunRecordingMode == SUNSET_RECORDING) { - - /* Recording from before sunset to after sunset */ - - *numberOfSunRecordingPeriods = 1; - - tempRecordingPeriod.startMinutes = beforeSunset; - - tempRecordingPeriod.endMinutes = afterSunset; - - copyToBackupDomain((uint32_t*)firstSunRecordingPeriod, (uint8_t*)&tempRecordingPeriod, sizeof(recordingPeriod_t)); - - } else if (configSettings->sunRecordingMode == SUNRISE_AND_SUNSET_RECORDING) { - - /* Order the recording periods */ - - uint32_t firstPeriodStartMinutes = beforeSunrise < beforeSunset ? beforeSunrise : beforeSunset; - - uint32_t firstPeriodEndMinutes = beforeSunrise < beforeSunset ? afterSunrise : afterSunset; - - uint32_t secondPeriodStartMinutes = beforeSunrise < beforeSunset ? beforeSunset : beforeSunrise; - - uint32_t secondPeriodEndMinutes = beforeSunrise < beforeSunset ? afterSunset : afterSunrise; - - /* Determine whether the recording periods wrap */ - - bool firstPeriodWraps = firstPeriodEndMinutes <= firstPeriodStartMinutes; - - bool secondPeriodWraps = secondPeriodEndMinutes <= secondPeriodStartMinutes; - - /* Combine recording periods together if they overlap */ - - if (firstPeriodWraps) { - - *numberOfSunRecordingPeriods = 1; - - tempRecordingPeriod.startMinutes = firstPeriodStartMinutes; - - tempRecordingPeriod.endMinutes = secondPeriodWraps ? MIN(firstPeriodStartMinutes, MAX(firstPeriodEndMinutes, secondPeriodEndMinutes)) : firstPeriodEndMinutes; - - copyToBackupDomain((uint32_t*)firstSunRecordingPeriod, (uint8_t*)&tempRecordingPeriod, sizeof(recordingPeriod_t)); - - } else if (secondPeriodStartMinutes <= firstPeriodEndMinutes) { - - *numberOfSunRecordingPeriods = 1; - - tempRecordingPeriod.startMinutes = firstPeriodStartMinutes; - - tempRecordingPeriod.endMinutes = secondPeriodWraps ? MIN(firstPeriodStartMinutes, secondPeriodEndMinutes) : MAX(firstPeriodEndMinutes, secondPeriodEndMinutes); - - copyToBackupDomain((uint32_t*)firstSunRecordingPeriod, (uint8_t*)&tempRecordingPeriod, sizeof(recordingPeriod_t)); - - } else if (secondPeriodWraps && secondPeriodEndMinutes >= firstPeriodStartMinutes) { - - *numberOfSunRecordingPeriods = 1; - - tempRecordingPeriod.startMinutes = secondPeriodStartMinutes; - - tempRecordingPeriod.endMinutes = MAX(firstPeriodEndMinutes, secondPeriodEndMinutes); - - copyToBackupDomain((uint32_t*)firstSunRecordingPeriod, (uint8_t*)&tempRecordingPeriod, sizeof(recordingPeriod_t)); - - } else { - - *numberOfSunRecordingPeriods = 2; - - tempRecordingPeriod.startMinutes = firstPeriodStartMinutes; - - tempRecordingPeriod.endMinutes = firstPeriodEndMinutes; - - copyToBackupDomain((uint32_t*)firstSunRecordingPeriod, (uint8_t*)&tempRecordingPeriod, sizeof(recordingPeriod_t)); - - tempRecordingPeriod.startMinutes = secondPeriodStartMinutes; - - tempRecordingPeriod.endMinutes = secondPeriodEndMinutes; - - copyToBackupDomain((uint32_t*)secondSunRecordingPeriod, (uint8_t*)&tempRecordingPeriod, sizeof(recordingPeriod_t)); - - } - - /* Adjust the size of the minimum gap between recording periods if it is less than the threshold */ - - if (*numberOfSunRecordingPeriods == 1) { - - uint32_t duration = firstSunRecordingPeriod->endMinutes <= firstSunRecordingPeriod->startMinutes ? MINUTES_IN_DAY + firstSunRecordingPeriod->endMinutes - firstSunRecordingPeriod->startMinutes : firstSunRecordingPeriod->endMinutes - firstSunRecordingPeriod->startMinutes; - - if (duration > maximumRecordingDuration) { - - tempRecordingPeriod.startMinutes = firstSunRecordingPeriod->startMinutes; - - tempRecordingPeriod.endMinutes = (firstSunRecordingPeriod->startMinutes + maximumRecordingDuration) % MINUTES_IN_DAY; - - copyToBackupDomain((uint32_t*)firstSunRecordingPeriod, (uint8_t*)&tempRecordingPeriod, sizeof(recordingPeriod_t)); - - } - - } - - if (*numberOfSunRecordingPeriods == 2) { - - uint32_t gapFromFirstPeriodToSecondPeriod = secondSunRecordingPeriod->startMinutes - firstSunRecordingPeriod->endMinutes; - - uint32_t gapFromSecondPeriodsToFirstPeriod = secondSunRecordingPeriod->endMinutes < secondSunRecordingPeriod->startMinutes ? firstSunRecordingPeriod->startMinutes - secondSunRecordingPeriod->endMinutes : MINUTES_IN_DAY + firstSunRecordingPeriod->startMinutes - secondSunRecordingPeriod->endMinutes; - - if (gapFromFirstPeriodToSecondPeriod >= gapFromSecondPeriodsToFirstPeriod && gapFromFirstPeriodToSecondPeriod < minimumRecordingGap) { - - tempRecordingPeriod.startMinutes = firstSunRecordingPeriod->startMinutes; - - tempRecordingPeriod.endMinutes = (MINUTES_IN_DAY + firstSunRecordingPeriod->endMinutes - minimumRecordingGap + gapFromFirstPeriodToSecondPeriod) % MINUTES_IN_DAY; - - copyToBackupDomain((uint32_t*)firstSunRecordingPeriod, (uint8_t*)&tempRecordingPeriod, sizeof(recordingPeriod_t)); - - } else if (gapFromSecondPeriodsToFirstPeriod >= gapFromFirstPeriodToSecondPeriod && gapFromSecondPeriodsToFirstPeriod < minimumRecordingGap) { - - tempRecordingPeriod.startMinutes = secondSunRecordingPeriod->startMinutes; - - tempRecordingPeriod.endMinutes = (MINUTES_IN_DAY + secondSunRecordingPeriod->endMinutes - minimumRecordingGap + gapFromSecondPeriodsToFirstPeriod) % MINUTES_IN_DAY; - - copyToBackupDomain((uint32_t*)secondSunRecordingPeriod, (uint8_t*)&tempRecordingPeriod, sizeof(recordingPeriod_t)); - - } - - } - - } else if (configSettings->sunRecordingMode == SUNSET_TO_SUNRISE_RECORDING) { - - /* Recording from before sunset to after sunrise */ - - *numberOfSunRecordingPeriods = 1; - - tempRecordingPeriod.startMinutes = beforeSunset; - - uint32_t timeFromSunsetToSunrise; - - if (roundedSunriseMinutes == roundedSunsetMinutes) { - - timeFromSunsetToSunrise = trend == SR_DAY_SHORTER_THAN_NIGHT ? MINUTES_IN_DAY : 0; - - } else { - - timeFromSunsetToSunrise = roundedSunriseMinutes < roundedSunsetMinutes ? MINUTES_IN_DAY + roundedSunriseMinutes - roundedSunsetMinutes : roundedSunriseMinutes - roundedSunsetMinutes; - - } - - uint32_t duration = timeFromSunsetToSunrise + configSettings->beforeSunsetMinutes + configSettings->afterSunriseMinutes; - - if (duration == 0) duration = 1; - - if (duration > maximumRecordingDuration) duration = maximumRecordingDuration; - - tempRecordingPeriod.endMinutes = (beforeSunset + duration) % MINUTES_IN_DAY; - - copyToBackupDomain((uint32_t*)firstSunRecordingPeriod, (uint8_t*)&tempRecordingPeriod, sizeof(recordingPeriod_t)); - - } else if (configSettings->sunRecordingMode == SUNRISE_TO_SUNSET_RECORDING) { - - /* Recording from before sunrise to after sunset */ - - *numberOfSunRecordingPeriods = 1; - - tempRecordingPeriod.startMinutes = beforeSunrise; - - uint32_t timeFromSunriseToSunset; - - if (roundedSunriseMinutes == roundedSunsetMinutes) { - - timeFromSunriseToSunset = trend == SR_DAY_LONGER_THAN_NIGHT ? MINUTES_IN_DAY : 0; - - } else { - - timeFromSunriseToSunset = roundedSunsetMinutes < roundedSunriseMinutes ? MINUTES_IN_DAY + roundedSunsetMinutes - roundedSunriseMinutes : roundedSunsetMinutes - roundedSunriseMinutes; - - } - - uint32_t duration = timeFromSunriseToSunset + configSettings->beforeSunriseMinutes + configSettings->afterSunsetMinutes; - - if (duration == 0) duration = 1; - - if (duration > maximumRecordingDuration) duration = maximumRecordingDuration; - - tempRecordingPeriod.endMinutes = (beforeSunrise + duration) % MINUTES_IN_DAY; - - copyToBackupDomain((uint32_t*)firstSunRecordingPeriod, (uint8_t*)&tempRecordingPeriod, sizeof(recordingPeriod_t)); - - } - -} - -/* Schedule recordings */ - -static void adjustRecordingDuration(uint32_t *duration, uint32_t recordDuration, uint32_t sleepDuration) { - - uint32_t durationOfCycle = recordDuration + sleepDuration; - - uint32_t numberOfCycles = *duration / durationOfCycle; - - uint32_t partialCycle = *duration % durationOfCycle; - - if (partialCycle == 0) { - - *duration = *duration > sleepDuration ? *duration - sleepDuration : 0; - - } else { - - *duration = MIN(*duration, numberOfCycles * durationOfCycle + recordDuration); - - } - -} - -static void calculateStartAndDuration(uint32_t currentTime, uint32_t currentSeconds, recordingPeriod_t *period, int32_t startMinutesOffset, uint32_t *startTime, uint32_t *duration) { - - uint32_t startMinutes = (2 * MINUTES_IN_DAY + period->startMinutes + startMinutesOffset) % MINUTES_IN_DAY; - - *startTime = currentTime - currentSeconds + SECONDS_IN_MINUTE * startMinutes; - - *duration = period->endMinutes <= period->startMinutes ? MINUTES_IN_DAY + period->endMinutes - period->startMinutes : period->endMinutes - period->startMinutes; - - *duration *= SECONDS_IN_MINUTE; - -} - -static void scheduleRecording(uint32_t currentTime, uint32_t *timeOfNextRecording, uint32_t *indexOfNextRecording, uint32_t *durationOfNextRecording, uint32_t *startOfRecordingPeriod, uint32_t *endOfRecordingPeriod) { - - /* Enforce minumum schedule date */ - - currentTime = MAX(currentTime, START_OF_CENTURY); - - /* Check if recording should be limited by earliest recording time */ - - uint32_t earliestRecordingTime = configSettings->earliestRecordingTime; - - if (earliestRecordingTime > 0) { - - if (getBackupFlag(BACKUP_ACOUSTIC_TIMEZONE_RECEIVED) && configSettings->useTimezoneFromAcousticChime && configSettings->adjustScheduleUsingTimezoneFromAcousticChime) { - - earliestRecordingTime += configSettings->timezoneHours * SECONDS_IN_HOUR + configSettings->timezoneMinutes * SECONDS_IN_MINUTE - *acousticTimezoneMinutes * SECONDS_IN_MINUTE; - - } - - currentTime = MAX(currentTime, earliestRecordingTime); - - } - - /* Select appropriate recording periods */ - - uint32_t activeRecordingPeriods = configSettings->enableSunRecording ? *numberOfSunRecordingPeriods : MIN(configSettings->activeRecordingPeriods, MAX_RECORDING_PERIODS); - - recordingPeriod_t *recordingPeriods = configSettings->enableSunRecording ? firstSunRecordingPeriod : configSettings->recordingPeriods; - - /* No suitable recording periods */ - - if (activeRecordingPeriods == 0) { - - *timeOfNextRecording = UINT32_MAX; - - *indexOfNextRecording = 0; - - if (startOfRecordingPeriod) *startOfRecordingPeriod = UINT32_MAX; - - if (endOfRecordingPeriod) *endOfRecordingPeriod = UINT32_MAX; - - *durationOfNextRecording = 0; - - return; - - } - - /* Calculate the number of seconds of this day */ - - struct tm time; - - time_t rawTime = currentTime; - - gmtime_r(&rawTime, &time); - - uint32_t currentSeconds = SECONDS_IN_HOUR * time.tm_hour + SECONDS_IN_MINUTE * time.tm_min + time.tm_sec; - - /* Calculate the start time offset for the appropriate time zone */ - - uint32_t minimumIndex = 0; - - int32_t startMinutesOffset = 0; - - if (configSettings->enableSunRecording == false) { - - if (getBackupFlag(BACKUP_ACOUSTIC_TIMEZONE_RECEIVED) && configSettings->useTimezoneFromAcousticChime && configSettings->adjustScheduleUsingTimezoneFromAcousticChime) { - - startMinutesOffset = configSettings->timezoneHours * MINUTES_IN_HOUR + configSettings->timezoneMinutes - *acousticTimezoneMinutes; - - } - - /* Find the first recording period */ - - uint32_t minimumStartMinutes = UINT32_MAX; - - for (uint32_t i = 0; i < activeRecordingPeriods; i += 1) { - - recordingPeriod_t *currentPeriod = recordingPeriods + i; - - uint32_t startMinutes = (2 * MINUTES_IN_DAY + currentPeriod->startMinutes + startMinutesOffset) % MINUTES_IN_DAY; - - if (startMinutes < minimumStartMinutes) { - - minimumStartMinutes = startMinutes; - - minimumIndex = i; - - } - - } - - } - - /* Check the last active period on the previous day */ - - uint32_t startTime, duration; - - uint32_t index = (minimumIndex + activeRecordingPeriods - 1) % activeRecordingPeriods; - - recordingPeriod_t *lastPeriod = recordingPeriods + index; - - calculateStartAndDuration(currentTime - SECONDS_IN_DAY, currentSeconds, lastPeriod, startMinutesOffset, &startTime, &duration); - - if (configSettings->disableSleepRecordCycle == false) { - - adjustRecordingDuration(&duration, configSettings->recordDuration, configSettings->sleepDuration); - - } - - if (currentTime < startTime + duration && duration > 0) goto done; - - /* Check each active recording period on the same day */ - - for (uint32_t i = 0; i < activeRecordingPeriods; i += 1) { - - index = (minimumIndex + i) % activeRecordingPeriods; - - recordingPeriod_t *currentPeriod = recordingPeriods + index; - - calculateStartAndDuration(currentTime, currentSeconds, currentPeriod, startMinutesOffset, &startTime, &duration); - - if (configSettings->disableSleepRecordCycle == false) { - - adjustRecordingDuration(&duration, configSettings->recordDuration, configSettings->sleepDuration); - - } - - if (currentTime < startTime + duration && duration > 0) goto done; - - } - - /* Calculate time until first period tomorrow */ - - index = minimumIndex; - - recordingPeriod_t *firstPeriod = recordingPeriods + index; - - calculateStartAndDuration(currentTime + SECONDS_IN_DAY, currentSeconds, firstPeriod, startMinutesOffset, &startTime, &duration); - - if (configSettings->disableSleepRecordCycle == false) { - - adjustRecordingDuration(&duration, configSettings->recordDuration, configSettings->sleepDuration); - - } - -done: - - /* Set the time for start and end of the recording period */ - - if (startOfRecordingPeriod) *startOfRecordingPeriod = startTime; - - if (endOfRecordingPeriod) *endOfRecordingPeriod = startTime + duration; - - /* Resolve sleep and record cycle */ - - if (configSettings->disableSleepRecordCycle) { - - *timeOfNextRecording = startTime; - - *durationOfNextRecording = duration; - - } else { - - if (currentTime <= startTime) { - - /* Recording should start at the start of the recording period */ - - *timeOfNextRecording = startTime; - - *durationOfNextRecording = MIN(duration, configSettings->recordDuration); - - } else { - - /* Recording should start immediately or at the start of the next recording cycle */ - - uint32_t secondsFromStartOfPeriod = currentTime - startTime; - - uint32_t durationOfCycle = configSettings->recordDuration + configSettings->sleepDuration; - - uint32_t partialCycle = secondsFromStartOfPeriod % durationOfCycle; - - *timeOfNextRecording = currentTime - partialCycle; - - if (partialCycle >= configSettings->recordDuration) { - - /* Wait for next cycle to begin */ - - *timeOfNextRecording += durationOfCycle; - - } - - uint32_t remainingDuration = startTime + duration - *timeOfNextRecording; - - *durationOfNextRecording = MIN(configSettings->recordDuration, remainingDuration); - - } - - } - - /* Update start time and duration is recording period has started */ - - if (currentTime > *timeOfNextRecording) { - - *durationOfNextRecording -= currentTime - *timeOfNextRecording; - - *timeOfNextRecording = currentTime; - - } - - /* Check if recording should be limited by last recording time */ - - uint32_t latestRecordingTime = MIDPOINT_OF_CENTURY; - - if (configSettings->latestRecordingTime > 0) { - - latestRecordingTime = configSettings->latestRecordingTime; - - if (getBackupFlag(BACKUP_ACOUSTIC_TIMEZONE_RECEIVED) && configSettings->useTimezoneFromAcousticChime && configSettings->adjustScheduleUsingTimezoneFromAcousticChime) { - - latestRecordingTime += configSettings->timezoneHours * SECONDS_IN_HOUR + configSettings->timezoneMinutes * SECONDS_IN_MINUTE - *acousticTimezoneMinutes * SECONDS_IN_MINUTE; - - } - - } - - if (*timeOfNextRecording >= latestRecordingTime) { - - *timeOfNextRecording = UINT32_MAX; - - if (startOfRecordingPeriod) *startOfRecordingPeriod = UINT32_MAX; - - if (endOfRecordingPeriod) *endOfRecordingPeriod = UINT32_MAX; - - *durationOfNextRecording = 0; - - } else { - - int64_t excessTime = (int64_t)*timeOfNextRecording + (int64_t)*durationOfNextRecording - (int64_t)latestRecordingTime; - - if (excessTime > 0) { - - *durationOfNextRecording -= excessTime; - - if (endOfRecordingPeriod) *endOfRecordingPeriod = *timeOfNextRecording + *durationOfNextRecording; - - } - - } - - /* Set the index of the next recording */ - - *indexOfNextRecording = index; - -} - -/* Flash LED according to battery life */ - -static void flashLedToIndicateBatteryLife(void) { - - uint32_t numberOfFlashes = LOW_BATTERY_LED_FLASHES; - - uint32_t supplyVoltage = AudioMoth_getSupplyVoltage(); - - if (configSettings->batteryLevelDisplayType == NIMH_LIPO_BATTERY_VOLTAGE) { - - /* Set number of flashes according to battery voltage */ - - AM_extendedBatteryState_t batteryState = AudioMoth_getExtendedBatteryState(supplyVoltage); - - if (batteryState > AM_EXT_BAT_4V3) { - - numberOfFlashes = 1; - - } else if (batteryState > AM_EXT_BAT_3V5) { - - numberOfFlashes = AM_EXT_BAT_4V4 - batteryState; - - } - - } else { - - /* Set number of flashes according to battery state */ - - AM_batteryState_t batteryState = AudioMoth_getBatteryState(supplyVoltage); - - if (batteryState > AM_BATTERY_LOW) { - - numberOfFlashes = (batteryState >= AM_BATTERY_4V6) ? 4 : (batteryState >= AM_BATTERY_4V4) ? 3 : (batteryState >= AM_BATTERY_4V0) ? 2 : 1; - - } - - } - - /* Flash LED */ - - for (uint32_t i = 0; i < numberOfFlashes; i += 1) { - - FLASH_LED(Red, SHORT_LED_FLASH_DURATION) - - if (numberOfFlashes == LOW_BATTERY_LED_FLASHES) { - - AudioMoth_delay(SHORT_LED_FLASH_DURATION); - - } else { - - AudioMoth_delay(LONG_LED_FLASH_DURATION); - - } - - } - -} +/**************************************************************************** + * main.c + * openacousticdevices.info + * June 2026 + *****************************************************************************/ + +#include +#include +#include +#include +#include +#include + +#include "gps.h" +#include "sunrise.h" +#include "audiomoth.h" +#include "audioconfig.h" +#include "digitalfilter.h" +#include "espbridge.h" + +/* Useful time constants */ + +#define MICROSECONDS_IN_SECOND 1000000 +#define MICROSECONDS_IN_MILLISECOND 1000 +#define MILLISECONDS_IN_SECOND 1000 + +#define SECONDS_IN_MINUTE 60 +#define SECONDS_IN_HOUR (60 * SECONDS_IN_MINUTE) +#define SECONDS_IN_DAY (24 * SECONDS_IN_HOUR) + +#define MINUTES_IN_HOUR 60 +#define MINUTES_IN_DAY 1440 +#define YEAR_OFFSET 1900 +#define MONTH_OFFSET 1 + +#define START_OF_CENTURY 946684800 +#define MIDPOINT_OF_CENTURY 2524608000 + +/* Useful coordinate constant */ + +#define MINUTES_IN_DEGREE 60 + +/* Useful type constants */ + +#define BITS_PER_BYTE 8 +#define UINT32_SIZE_IN_BITS 32 +#define UINT32_SIZE_IN_BYTES 4 +#define UINT16_SIZE_IN_BYTES 2 + +/* Sleep and LED constants */ + +#define LOW_BATTERY_LED_FLASHES 10 + +#define SHORT_LED_FLASH_DURATION 100 +#define LONG_LED_FLASH_DURATION 500 + +#define TRIGGERED_RECORDING_FLASH_DURATION 4 + +#define WAITING_LED_FLASH_DURATION 10 +#define WAITING_LED_FLASH_INTERVAL 2000 + +#define MINIMUM_LED_FLASH_INTERVAL 500 + +#define SHORT_WAIT_INTERVAL 100 +#define DEFAULT_WAIT_INTERVAL 1000 + +#define USB_CONFIGURATION_BLINK 400 + +#define ESP_TIME_WAIT_TIMEOUT_MS 60000 +#define ESP_TIME_WAIT_STEP_MS 50 +#define ESP_TIME_SERVICE_WINDOW_SECONDS 2 + +/* SRAM buffer constants */ + +#define NUMBER_OF_BUFFERS 8 +#define NUMBER_OF_BYTES_IN_SAMPLE 2 +#define EXTERNAL_SRAM_SIZE_IN_SAMPLES (AM_EXTERNAL_SRAM_SIZE_IN_BYTES / NUMBER_OF_BYTES_IN_SAMPLE) +#define NUMBER_OF_SAMPLES_IN_BUFFER (EXTERNAL_SRAM_SIZE_IN_SAMPLES / NUMBER_OF_BUFFERS) + +/* DMA transfer constant */ + +#define MAXIMUM_SAMPLES_IN_DMA_TRANSFER 1024 + +/* Compression constant */ + +#define COMPRESSION_BUFFER_SIZE_IN_BYTES 512 + +/* File size constants */ + +#define MAXIMUM_FILE_NAME_LENGTH 64 + +#define MAXIMUM_WAV_DATA_SIZE (UINT32_MAX - 16 * 1024 * 1024) + +/* Configuration file constants */ + +#define CONFIG_BUFFER_LENGTH 512 +#define CONFIG_TIMEZONE_LENGTH 8 + +/* WAV header constants */ + +#define PCM_FORMAT 1 +#define RIFF_ID_LENGTH 4 +#define LENGTH_OF_ARTIST 32 +#define LENGTH_OF_COMMENT 384 + +/* USB configuration constant */ + +#define MAX_RECORDING_PERIODS 5 + +/* Digital filter constant */ + +#define FILTER_FREQ_MULTIPLIER 100 + +/* DC filter constants */ + +#define LOW_DC_BLOCKING_FREQ 8 +#define DEFAULT_DC_BLOCKING_FREQ 48 + +/* Supply voltage constant */ + +#define MINIMUM_SUPPLY_VOLTAGE 2800 + +/* Consecutive recording error constant */ + +#define MAX_CONSECUTIVE_RECORDING_ERRORS 5 + +/* Deployment ID constant */ + +#define DEPLOYMENT_ID_LENGTH 8 + +/* Acoustic location constant */ + +#define ACOUSTIC_LOCATION_SIZE_IN_BYTES 7 + +/* Audio configuration constants */ + +#define AUDIO_CONFIG_PULSE_INTERVAL 10 +#define AUDIO_CONFIG_TIME_CORRECTION 134 +#define AUDIO_CONFIG_TONE_TIMEOUT 250 +#define AUDIO_CONFIG_PACKETS_TIMEOUT 30000 + +/* GPS time setting constants */ + +#define GPS_MAXIMUM_DIFFERENCE SECONDS_IN_HOUR +#define GPS_INITIAL_TIME_SETTING_PERIOD 300 +#define GPS_DEFAULT_TIME_SETTING_PERIOD 300 +#define GPS_MIN_TIME_SETTING_PERIOD 30 +#define GPS_ADDITIONAL_POWER_INTERVAL 2 +#define GPS_TIME_SETTING_MARGIN 2 +#define GPS_FREQUENCY_PRECISION 1000 +#define GPS_MESSAGE_BUFFER_SIZE_IN_BYTES 128 +#define GPS_FILENAME "GPS.TXT" + +/* Magnetic switch constants */ + +#define MAGNETIC_SWITCH_WAIT_MULTIPLIER 2 +#define MAGNETIC_SWITCH_CHANGE_FLASHES 10 + +/* USB configuration constant */ + +#define USB_CONFIG_TIME_CORRECTION 26 + +/* Recording start time correction */ + +#define DELAY_CORRECTION_MICROSECONDS 80 + +/* Recording preparation constants */ + +#define MINIMUM_PREPARATION_PERIOD 500 +#define INITIAL_PREPARATION_PERIOD 1000 +#define MAXIMUM_PREPARATION_PERIOD 30000 + +#define DEFAULT_PREPARATION_PERIOD_BUFFER 2000 + +#define PREPARATION_PERIOD_SMOOTHING_FACTOR 4 +#define PREPARATION_PERIOD_LED_DIVIDER 4 + +/* Energy saver mode constant */ + +#define ENERGY_SAVER_SAMPLE_RATE_THRESHOLD 48000 + +/* Frequency trigger constants */ + +#define FREQUENCY_TRIGGER_WINDOW_MINIMUM 16 +#define FREQUENCY_TRIGGER_WINDOW_MAXIMUM 1024 + +/* Sunrise and sunset recording constants */ + +#define MINIMUM_SUN_RECORDING_GAP 60 +#define SUN_RECORDING_GAP_MULTIPLIER 4 + +/* Location constants */ + +#define ACOUSTIC_LONGITUDE_MULTIPLIER 2 + +#define CONFIG_LOCATION_PRECISION 100 +#define ACOUSTIC_LOCATION_PRECISION 1000000 +#define GPS_LOCATION_PRECISION 1000000 + +/* Useful macros */ + +#define FLASH_LED(led, duration) { \ + AudioMoth_set ## led ## LED(true); \ + AudioMoth_delay(duration); \ + AudioMoth_set ## led ## LED(false); \ +} + +#define FLASH_REPEAT_LED(led, repeats, duration) { \ + for (uint32_t i = 0; i < repeats; i += 1) { \ + AudioMoth_set ## led ## LED(true); \ + AudioMoth_delay(duration); \ + AudioMoth_set ## led ## LED(false); \ + AudioMoth_delay(duration); \ + } \ +} + +#define FLASH_LED_IF_ENABLED_AND_RETURN_ON_ERROR(fn) { \ + bool success = (fn); \ + if (success != true) { \ + if (enableLED) { \ + AudioMoth_setBothLED(false); \ + AudioMoth_delay(LONG_LED_FLASH_DURATION); \ + FLASH_LED(Both, LONG_LED_FLASH_DURATION) \ + } \ + return SDCARD_WRITE_ERROR; \ + } \ +} + +#define RETURN_BOOL_ON_ERROR(fn) { \ + bool success = (fn); \ + if (success != true) { \ + return success; \ + } \ +} + +#define SAVE_SWITCH_POSITION_AND_POWER_DOWN(milliseconds) { \ + *previousSwitchPosition = switchPosition; \ + AudioMoth_powerDownAndWakeMilliseconds(milliseconds); \ +} + +#define SERIAL_NUMBER "%08X%08X" + +#define FORMAT_SERIAL_NUMBER(src) (unsigned int)*((uint32_t*)src + 1), (unsigned int)*((uint32_t*)src) + +#define ABS(a) ((a) < (0) ? (-a) : (a)) + +#define MIN(a, b) ((a) < (b) ? (a) : (b)) + +#define MAX(a, b) ((a) > (b) ? (a) : (b)) + +#define ROUNDED_DIV(a, b) (((a) + ((b)/2)) / (b)) + +#define ROUNDED_UP_DIV(a, b) (((a) + (b) - 1) / (b)) + +#define ROUND_UP_TO_MULTIPLE(a, b) (((a) + (b) - 1) & ~((b)-1)) + +#define UNSIGNED_ROUND(n, d) ((d) * (((n) + (d)/2) / (d))) + +/* Recording state enumeration */ + +typedef enum {RECORDING_OKAY, FILE_SIZE_LIMITED, SUPPLY_VOLTAGE_LOW, SWITCH_CHANGED, MICROPHONE_CHANGED, MAGNETIC_SWITCH, SDCARD_WRITE_ERROR} AM_recordingState_t; + +/* Filter type enumeration */ + +typedef enum {NO_FILTER, LOW_PASS_FILTER, BAND_PASS_FILTER, HIGH_PASS_FILTER} AM_filterType_t; + +/* Battery level display type */ + +typedef enum {BATTERY_LEVEL, NIMH_LIPO_BATTERY_VOLTAGE} AM_batteryLevelDisplayType_t; + +/* Sun recording mode enumeration */ + +typedef enum {SUNRISE_RECORDING, SUNSET_RECORDING, SUNRISE_AND_SUNSET_RECORDING, SUNSET_TO_SUNRISE_RECORDING, SUNRISE_TO_SUNSET_RECORDING} AM_sunRecordingMode_t; + +/* Sun recording mode enumeration */ + +typedef enum {INITIAL_GPS_FIX, GPS_FIX_BEFORE_RECORDING_PERIOD, GPS_FIX_BETWEEN_RECORDING_PERIODS, GPS_FIX_AFTER_RECORDING_PERIOD, GPS_FIX_BEFORE_INDIVIDUAL_RECORDING, GPS_FIX_BETWEEN_INDIVIDUAL_RECORDINGS, GPS_FIX_AFTER_INDIVIDUAL_RECORDING} AM_gpsFixMode_t; + +/* WAV header */ + +#pragma pack(push, 1) + +typedef struct { + char id[RIFF_ID_LENGTH]; + uint32_t size; +} chunk_t; + +typedef struct { + chunk_t icmt; + char comment[LENGTH_OF_COMMENT]; +} icmt_t; + +typedef struct { + chunk_t iart; + char artist[LENGTH_OF_ARTIST]; +} iart_t; + +typedef struct { + uint16_t format; + uint16_t numberOfChannels; + uint32_t samplesPerSecond; + uint32_t bytesPerSecond; + uint16_t bytesPerCapture; + uint16_t bitsPerSample; +} wavFormat_t; + +typedef struct { + chunk_t riff; + char format[RIFF_ID_LENGTH]; + chunk_t fmt; + wavFormat_t wavFormat; + chunk_t list; + char info[RIFF_ID_LENGTH]; + icmt_t icmt; + iart_t iart; + chunk_t data; +} wavHeader_t; + +#pragma pack(pop) + +static wavHeader_t wavHeader = { + .riff = {.id = "RIFF", .size = 0}, + .format = "WAVE", + .fmt = {.id = "fmt ", .size = sizeof(wavFormat_t)}, + .wavFormat = {.format = PCM_FORMAT, .numberOfChannels = 1, .samplesPerSecond = 0, .bytesPerSecond = 0, .bytesPerCapture = 2, .bitsPerSample = 16}, + .list = {.id = "LIST", .size = RIFF_ID_LENGTH + sizeof(icmt_t) + sizeof(iart_t)}, + .info = "INFO", + .icmt = {.icmt.id = "ICMT", .icmt.size = LENGTH_OF_COMMENT, .comment = ""}, + .iart = {.iart.id = "IART", .iart.size = LENGTH_OF_ARTIST, .artist = ""}, + .data = {.id = "data", .size = 0} +}; + +/* USB configuration data structure */ + +#pragma pack(push, 1) + +typedef struct { + uint16_t startMinutes; + uint16_t endMinutes; +} recordingPeriod_t; + +typedef struct { + uint32_t time; + AM_gainSetting_t gain; + uint8_t clockDivider; + uint8_t acquisitionCycles; + uint8_t oversampleRate; + uint32_t sampleRate; + uint8_t sampleRateDivider; + uint16_t sleepDuration; + uint16_t recordDuration; + uint8_t enableLED; + union { + struct { + uint8_t activeRecordingPeriods; + recordingPeriod_t recordingPeriods[MAX_RECORDING_PERIODS]; + }; + struct { + uint8_t sunRecordingMode : 3; + uint8_t sunRecordingEvent : 2; + int16_t latitude; + int16_t longitude; + uint8_t sunRoundingMinutes; + uint16_t beforeSunriseMinutes : 10; + uint16_t afterSunriseMinutes : 10; + uint16_t beforeSunsetMinutes : 10; + uint16_t afterSunsetMinutes : 10; + }; + }; + int8_t timezoneHours; + uint8_t enableLowVoltageCutoff; + uint8_t disableBatteryLevelDisplay : 1; + uint8_t requireAcousticLocation : 1; + uint8_t useTimezoneFromAcousticChime: 1; + uint8_t adjustScheduleUsingTimezoneFromAcousticChime: 1; + uint8_t preparationPeriodBuffer: 4; + int8_t timezoneMinutes; + uint8_t disableSleepRecordCycle : 1; + uint8_t enableFilenameWithDeviceID : 1; + uint8_t enableTimeSettingBeforeAndAfterRecordings: 1; + uint8_t gpsTimeSettingPeriod: 4; + uint8_t ignoreExternalMicrophoneForAcousticChime: 1; + uint32_t earliestRecordingTime; + uint32_t latestRecordingTime; + uint16_t lowerFilterFreq; + uint16_t higherFilterFreq; + union { + uint16_t amplitudeThreshold; + uint16_t frequencyTriggerCentreFrequency; + }; + uint8_t requireAcousticConfiguration : 1; + AM_batteryLevelDisplayType_t batteryLevelDisplayType : 1; + uint8_t minimumTriggerDuration : 6; + union { + struct { + uint8_t frequencyTriggerWindowLengthShift : 4; + uint8_t frequencyTriggerThresholdPercentageMantissa : 4; + int8_t frequencyTriggerThresholdPercentageExponent : 3; + }; + struct { + uint8_t enableAmplitudeThresholdDecibelScale : 1; + uint8_t amplitudeThresholdDecibels : 7; + uint8_t enableAmplitudeThresholdPercentageScale : 1; + uint8_t amplitudeThresholdPercentageMantissa : 4; + int8_t amplitudeThresholdPercentageExponent : 3; + }; + }; + uint8_t enableEnergySaverMode : 1; + uint8_t disable48HzDCBlockingFilter : 1; + uint8_t enableTimeSettingFromGPS : 1; + uint8_t enableMagneticSwitch : 1; + uint8_t enableLowGainRange : 1; + uint8_t enableFrequencyTrigger : 1; + uint8_t enableDailyFolders : 1; + uint8_t enableSunRecording : 1; +} configSettings_t; + +#pragma pack(pop) + +static const configSettings_t defaultConfigSettings = { + .time = 0, + .gain = AM_GAIN_MEDIUM, + .clockDivider = 4, + .acquisitionCycles = 16, + .oversampleRate = 1, + .sampleRate = 384000, + .sampleRateDivider = 8, + .sleepDuration = 5, + .recordDuration = 55, + .enableLED = 1, + .activeRecordingPeriods = 1, + .recordingPeriods = { + {.startMinutes = 0, .endMinutes = 0}, + {.startMinutes = 0, .endMinutes = 0}, + {.startMinutes = 0, .endMinutes = 0}, + {.startMinutes = 0, .endMinutes = 0}, + {.startMinutes = 0, .endMinutes = 0} + }, + .timezoneHours = 0, + .enableLowVoltageCutoff = 1, + .disableBatteryLevelDisplay = 0, + .requireAcousticLocation = 0, + .useTimezoneFromAcousticChime = 0, + .adjustScheduleUsingTimezoneFromAcousticChime = 0, + .preparationPeriodBuffer = 2, + .timezoneMinutes = 0, + .disableSleepRecordCycle = 0, + .enableFilenameWithDeviceID = 0, + .enableTimeSettingBeforeAndAfterRecordings = 0, + .gpsTimeSettingPeriod = 0, + .ignoreExternalMicrophoneForAcousticChime = 0, + .earliestRecordingTime = 0, + .latestRecordingTime = 0, + .lowerFilterFreq = 0, + .higherFilterFreq = 0, + .amplitudeThreshold = 0, + .requireAcousticConfiguration = 0, + .batteryLevelDisplayType = BATTERY_LEVEL, + .minimumTriggerDuration = 0, + .enableAmplitudeThresholdDecibelScale = 0, + .amplitudeThresholdDecibels = 0, + .enableAmplitudeThresholdPercentageScale = 0, + .amplitudeThresholdPercentageMantissa = 0, + .amplitudeThresholdPercentageExponent = 0, + .enableEnergySaverMode = 0, + .disable48HzDCBlockingFilter = 0, + .enableTimeSettingFromGPS = 0, + .enableMagneticSwitch = 0, + .enableLowGainRange = 0, + .enableFrequencyTrigger = 0, + .enableDailyFolders = 0, + .enableSunRecording = 0 +}; + +/* Persistent configuration data structure */ + +#pragma pack(push, 1) + +typedef struct { + uint8_t firmwareVersion[AM_FIRMWARE_VERSION_LENGTH]; + uint8_t firmwareDescription[AM_FIRMWARE_DESCRIPTION_LENGTH]; + configSettings_t configSettings; +} persistentConfigSettings_t; + +#pragma pack(pop) + +/* Acoustic location data structure */ + +#pragma pack(push, 1) + +typedef struct { + int32_t latitude: 28; + int32_t longitude: 28; +} acousticLocation_t; + +#pragma pack(pop) + +/* Function to select energy saver mode */ + +static bool isEnergySaverMode(configSettings_t *configSettings) { + + return configSettings->enableEnergySaverMode && configSettings->sampleRate / configSettings->sampleRateDivider <= ENERGY_SAVER_SAMPLE_RATE_THRESHOLD; + +} + +/* Functions to format header and configuration components */ + +static uint32_t formatDecibels(char *dest, uint32_t value, bool space) { + + if (value > 0) return sprintf(dest, space ? "-%lu dB" : "-%ludB", value); + + return sprintf(dest, space ? "0 dB" : "0dB"); + +} + +static uint32_t formatPercentage(char *dest, uint32_t mantissa, int32_t exponent) { + + uint32_t length = exponent < 0 ? 1 - exponent : 0; + + memcpy(dest, "0.0000", length); + + length += sprintf(dest + length, "%lu", mantissa); + + while (exponent-- > 0) dest[length++] = '0'; + + dest[length++] = '%'; + + return length; + +} + +/* Functions to set WAV header details and comment */ + +static void setHeaderDetails(wavHeader_t *wavHeader, uint32_t sampleRate, uint32_t numberOfSamples, uint32_t guanoDataSize) { + + wavHeader->wavFormat.samplesPerSecond = sampleRate; + wavHeader->wavFormat.bytesPerSecond = NUMBER_OF_BYTES_IN_SAMPLE * sampleRate; + wavHeader->data.size = NUMBER_OF_BYTES_IN_SAMPLE * numberOfSamples; + wavHeader->riff.size = NUMBER_OF_BYTES_IN_SAMPLE * numberOfSamples + sizeof(wavHeader_t) + guanoDataSize - sizeof(chunk_t); + +} + +static void setHeaderComment(wavHeader_t *wavHeader, configSettings_t *configSettings, uint32_t currentTime, uint8_t *serialNumber, uint8_t *deploymentID, uint8_t *defaultDeploymentID, AM_extendedBatteryState_t extendedBatteryState, int32_t temperature, bool externalMicrophone, AM_recordingState_t recordingState, AM_filterType_t filterType) { + + struct tm time; + + int32_t timezoneHours, timezoneMinutes; + + AudioMoth_timezoneRequested(&timezoneHours, &timezoneMinutes); + + time_t rawTime = currentTime + timezoneHours * SECONDS_IN_HOUR + timezoneMinutes * SECONDS_IN_MINUTE; + + gmtime_r(&rawTime, &time); + + /* Format artist field */ + + char *artist = wavHeader->iart.artist; + + sprintf(artist, "AudioMoth " SERIAL_NUMBER, FORMAT_SERIAL_NUMBER(serialNumber)); + + /* Clear comment field */ + + char *comment = wavHeader->icmt.comment; + + memset(comment, 0, LENGTH_OF_COMMENT); + + /* Format comment field */ + + comment += sprintf(comment, "Recorded at %02d:%02d:%02d %02d/%02d/%04d (UTC", time.tm_hour, time.tm_min, time.tm_sec, time.tm_mday, MONTH_OFFSET + time.tm_mon, YEAR_OFFSET + time.tm_year); + + if (timezoneHours < 0) { + + comment += sprintf(comment, "%ld", timezoneHours); + + } else if (timezoneHours > 0) { + + comment += sprintf(comment, "+%ld", timezoneHours); + + } else { + + if (timezoneMinutes < 0) comment += sprintf(comment, "-%ld", timezoneHours); + + if (timezoneMinutes > 0) comment += sprintf(comment, "+%ld", timezoneHours); + + } + + if (timezoneMinutes < 0) comment += sprintf(comment, ":%02ld", -timezoneMinutes); + + if (timezoneMinutes > 0) comment += sprintf(comment, ":%02ld", timezoneMinutes); + + if (memcmp(deploymentID, defaultDeploymentID, DEPLOYMENT_ID_LENGTH)) { + + comment += sprintf(comment, ") during deployment " SERIAL_NUMBER " ", FORMAT_SERIAL_NUMBER(deploymentID)); + + } else { + + comment += sprintf(comment, ") by %s ", artist); + + } + + if (externalMicrophone) { + + comment += sprintf(comment, "using external microphone "); + + } + + static char *gainSettings[5] = {"low", "low-medium", "medium", "medium-high", "high"}; + + comment += sprintf(comment, "at %s gain while battery was ", gainSettings[configSettings->gain]); + + if (extendedBatteryState == AM_EXT_BAT_LOW) { + + comment += sprintf(comment, "less than 2.5V"); + + } else if (extendedBatteryState >= AM_EXT_BAT_FULL) { + + comment += sprintf(comment, "greater than 4.9V"); + + } else { + + uint32_t batteryVoltage = extendedBatteryState + AM_EXT_BAT_STATE_OFFSET / AM_BATTERY_STATE_INCREMENT; + + comment += sprintf(comment, "%01lu.%01luV", batteryVoltage / 10, batteryVoltage % 10); + + } + + char *temperatureSign = temperature < 0 ? "-" : ""; + + uint32_t temperatureInDecidegrees = ROUNDED_DIV(ABS(temperature), 100); + + comment += sprintf(comment, " and temperature was %s%lu.%luC.", temperatureSign, temperatureInDecidegrees / 10, temperatureInDecidegrees % 10); + + bool frequencyTriggerEnabled = configSettings->enableFrequencyTrigger; + + bool amplitudeThresholdEnabled = frequencyTriggerEnabled ? false : configSettings->amplitudeThreshold > 0 || configSettings->enableAmplitudeThresholdDecibelScale || configSettings->enableAmplitudeThresholdPercentageScale; + + if (frequencyTriggerEnabled) { + + comment += sprintf(comment, " Frequency trigger (%u.%ukHz and window length of %u samples) threshold was ", configSettings->frequencyTriggerCentreFrequency / 10, configSettings->frequencyTriggerCentreFrequency % 10, (0x01 << configSettings->frequencyTriggerWindowLengthShift)); + + comment += formatPercentage(comment, configSettings->frequencyTriggerThresholdPercentageMantissa, configSettings->frequencyTriggerThresholdPercentageExponent); + + comment += sprintf(comment, " with %us minimum trigger duration.", configSettings->minimumTriggerDuration); + + } + + uint16_t lowerFilterFreq = configSettings->lowerFilterFreq; + + uint16_t higherFilterFreq = configSettings->higherFilterFreq; + + if (filterType == LOW_PASS_FILTER) { + + comment += sprintf(comment, " Low-pass filter with frequency of %01u.%01ukHz applied.", higherFilterFreq / 10, higherFilterFreq % 10); + + } else if (filterType == BAND_PASS_FILTER) { + + comment += sprintf(comment, " Band-pass filter with frequencies of %01u.%01ukHz and %01u.%01ukHz applied.", lowerFilterFreq / 10, lowerFilterFreq % 10, higherFilterFreq / 10, higherFilterFreq % 10); + + } else if (filterType == HIGH_PASS_FILTER) { + + comment += sprintf(comment, " High-pass filter with frequency of %01u.%01ukHz applied.", lowerFilterFreq / 10, lowerFilterFreq % 10); + + } + + if (amplitudeThresholdEnabled) { + + comment += sprintf(comment, " Amplitude threshold was "); + + if (configSettings->enableAmplitudeThresholdDecibelScale && configSettings->enableAmplitudeThresholdPercentageScale == false) { + + comment += formatDecibels(comment, configSettings->amplitudeThresholdDecibels, true); + + } else if (configSettings->enableAmplitudeThresholdPercentageScale && configSettings->enableAmplitudeThresholdDecibelScale == false) { + + comment += formatPercentage(comment, configSettings->amplitudeThresholdPercentageMantissa, configSettings->amplitudeThresholdPercentageExponent); + + } else { + + comment += sprintf(comment, "%u", configSettings->amplitudeThreshold); + + } + + comment += sprintf(comment, " with %us minimum trigger duration.", configSettings->minimumTriggerDuration); + + } + + if (recordingState != RECORDING_OKAY) { + + comment += sprintf(comment, " Recording stopped"); + + if (recordingState == MICROPHONE_CHANGED) { + + comment += sprintf(comment, " due to microphone change."); + + } else if (recordingState == SWITCH_CHANGED) { + + comment += sprintf(comment, " due to switch position change."); + + } else if (recordingState == MAGNETIC_SWITCH) { + + comment += sprintf(comment, " by magnetic switch."); + + } else if (recordingState == SUPPLY_VOLTAGE_LOW) { + + comment += sprintf(comment, " due to low voltage."); + + } else if (recordingState == FILE_SIZE_LIMITED) { + + comment += sprintf(comment, " due to file size limit."); + + } else if (recordingState == SDCARD_WRITE_ERROR) { + + comment += sprintf(comment, " due to SD card write error."); + + } + + } + +} + +/* Function to write the GUANO data */ + +static uint32_t writeGuanoData(char *buffer, configSettings_t *configSettings, uint32_t currentTime, bool gpsLocationReceived, int32_t *gpsLastFixLatitude, int32_t *gpsLastFixLongitude, bool acousticLocationReceived, int32_t *acousticLatitude, int32_t *acousticLongitude, uint8_t *firmwareDescription, uint8_t *firmwareVersion, uint8_t *serialNumber, uint8_t *deploymentID, uint8_t *defaultDeploymentID, char *filename, AM_extendedBatteryState_t extendedBatteryState, int32_t temperature, AM_filterType_t filterType) { + + uint32_t length = sprintf(buffer, "guan") + UINT32_SIZE_IN_BYTES; + + /* General information */ + + length += sprintf(buffer + length, "GUANO|Version:1.0\nMake:Open Acoustic Devices\nModel:AudioMoth\nSerial:" SERIAL_NUMBER "\n", FORMAT_SERIAL_NUMBER(serialNumber)); + + if (memcmp(deploymentID, defaultDeploymentID, DEPLOYMENT_ID_LENGTH)) { + + length += sprintf(buffer + length, "OAD|Deployment ID:" SERIAL_NUMBER "\n", FORMAT_SERIAL_NUMBER(deploymentID)); + + } + + length += sprintf(buffer + length, "Firmware Version:%s (%u.%u.%u)\n", firmwareDescription, firmwareVersion[0], firmwareVersion[1], firmwareVersion[2]); + + /* Timestamp */ + + int32_t timezoneHours, timezoneMinutes; + + AudioMoth_timezoneRequested(&timezoneHours, &timezoneMinutes); + + int32_t timezoneSeconds = timezoneHours * SECONDS_IN_HOUR + timezoneMinutes * SECONDS_IN_MINUTE; + + time_t rawTime = currentTime + timezoneSeconds; + + struct tm time; + + gmtime_r(&rawTime, &time); + + length += sprintf(buffer + length, "Timestamp:%04d-%02d-%02dT%02d:%02d:%02d", YEAR_OFFSET + time.tm_year, MONTH_OFFSET + time.tm_mon, time.tm_mday, time.tm_hour, time.tm_min, time.tm_sec); + + if (timezoneSeconds == 0) { + + length += sprintf(buffer + length, "Z\n"); + + } else if (timezoneSeconds < 0) { + + length += sprintf(buffer + length, "-%02ld:%02ld\n", ABS(timezoneHours), ABS(timezoneMinutes)); + + } else { + + length += sprintf(buffer + length, "+%02ld:%02ld\n", timezoneHours, timezoneMinutes); + + } + + /* Location position and source */ + + if (gpsLocationReceived || acousticLocationReceived || configSettings->enableSunRecording) { + + int32_t latitude = gpsLocationReceived ? *gpsLastFixLatitude : acousticLocationReceived ? *acousticLatitude : configSettings->latitude; + + int32_t longitude = gpsLocationReceived ? *gpsLastFixLongitude : acousticLocationReceived ? *acousticLongitude : configSettings->longitude; + + char *latitudeSign = latitude < 0 ? "-" : ""; + + char *longitudeSign = longitude < 0 ? "-" : ""; + + if (gpsLocationReceived) { + + length += sprintf(buffer + length, "Loc Position:%s%ld.%06ld %s%ld.%06ld\nOAD|Loc Source:GPS\n", latitudeSign, ABS(latitude) / GPS_LOCATION_PRECISION, ABS(latitude) % GPS_LOCATION_PRECISION, longitudeSign, ABS(longitude) / GPS_LOCATION_PRECISION, ABS(longitude) % GPS_LOCATION_PRECISION); + + } else if (acousticLocationReceived) { + + length += sprintf(buffer + length, "Loc Position:%s%ld.%06ld %s%ld.%06ld\nOAD|Loc Source:Acoustic chime\n", latitudeSign, ABS(latitude) / ACOUSTIC_LOCATION_PRECISION, ABS(latitude) % ACOUSTIC_LOCATION_PRECISION, longitudeSign, ABS(longitude) / ACOUSTIC_LOCATION_PRECISION, ABS(longitude) % ACOUSTIC_LOCATION_PRECISION); + + } else { + + length += sprintf(buffer + length, "Loc Position:%s%ld.%02ld %s%ld.%02ld\nOAD|Loc Source:Configuration app\n", latitudeSign, ABS(latitude) / CONFIG_LOCATION_PRECISION, ABS(latitude) % CONFIG_LOCATION_PRECISION, longitudeSign, ABS(longitude) / CONFIG_LOCATION_PRECISION, ABS(longitude) % CONFIG_LOCATION_PRECISION); + + } + + } + + /* Filename */ + + char *start = strchr(filename, '/'); + + length += sprintf(buffer + length, "Original Filename:%s\n", start ? start + 1 : filename); + + /* Recording settings */ + + length += sprintf(buffer + length, "OAD|Recording Settings:%lu GAIN %u", configSettings->sampleRate / configSettings->sampleRateDivider, configSettings->gain); + + bool frequencyTriggerEnabled = configSettings->enableFrequencyTrigger; + + bool amplitudeThresholdEnabled = frequencyTriggerEnabled ? false : configSettings->amplitudeThreshold > 0 || configSettings->enableAmplitudeThresholdDecibelScale || configSettings->enableAmplitudeThresholdPercentageScale; + + if (frequencyTriggerEnabled) { + + length += sprintf(buffer + length, " FREQ %u %u ", FILTER_FREQ_MULTIPLIER * configSettings->frequencyTriggerCentreFrequency, 0x01 << configSettings->frequencyTriggerWindowLengthShift); + + length += formatPercentage(buffer + length, configSettings->frequencyTriggerThresholdPercentageMantissa, configSettings->frequencyTriggerThresholdPercentageExponent); + + length += sprintf(buffer + length, " %u", configSettings->minimumTriggerDuration); + + } + + uint32_t lowerFilterFreq = FILTER_FREQ_MULTIPLIER * configSettings->lowerFilterFreq; + + uint32_t higherFilterFreq = FILTER_FREQ_MULTIPLIER * configSettings->higherFilterFreq; + + if (filterType == LOW_PASS_FILTER) { + + length += sprintf(buffer + length, " LPF %lu", higherFilterFreq); + + } else if (filterType == BAND_PASS_FILTER) { + + length += sprintf(buffer + length, " BPF %lu %lu", lowerFilterFreq, higherFilterFreq); + + } else if (filterType == HIGH_PASS_FILTER) { + + length += sprintf(buffer + length, " HPF %lu", lowerFilterFreq); + + } + + if (amplitudeThresholdEnabled) { + + length += sprintf(buffer + length, " AMP "); + + if (configSettings->enableAmplitudeThresholdDecibelScale && configSettings->enableAmplitudeThresholdPercentageScale == false) { + + length += formatDecibels(buffer + length, configSettings->amplitudeThresholdDecibels, false); + + } else if (configSettings->enableAmplitudeThresholdPercentageScale && configSettings->enableAmplitudeThresholdDecibelScale == false) { + + length += formatPercentage(buffer + length, configSettings->amplitudeThresholdPercentageMantissa, configSettings->amplitudeThresholdPercentageExponent); + + } else { + + length += sprintf(buffer + length, "%u", configSettings->amplitudeThreshold); + + } + + length += sprintf(buffer + length, " %u", configSettings->minimumTriggerDuration); + + } + + if (configSettings->enableLowGainRange) length += sprintf(buffer + length, " LGR"); + + if (configSettings->disable48HzDCBlockingFilter) length += sprintf(buffer + length, " D48"); + + if (isEnergySaverMode(configSettings)) length += sprintf(buffer + length, " ESM"); + + /* Battery and temperature */ + + uint32_t batteryVoltage = extendedBatteryState == AM_EXT_BAT_LOW ? 24 : extendedBatteryState >= AM_EXT_BAT_FULL ? 50 : extendedBatteryState + AM_EXT_BAT_STATE_OFFSET / AM_BATTERY_STATE_INCREMENT; + + length += sprintf(buffer + length, "\nOAD|Battery Voltage:%01lu.%01lu\n", batteryVoltage / 10, batteryVoltage % 10); + + char *temperatureSign = temperature < 0 ? "-" : ""; + + uint32_t temperatureInDecidegrees = ROUNDED_DIV(ABS(temperature), 100); + + length += sprintf(buffer + length, "Temperature Int:%s%lu.%lu", temperatureSign, temperatureInDecidegrees / 10, temperatureInDecidegrees % 10); + + /* Set GUANO chunk size */ + + *(uint32_t*)(buffer + RIFF_ID_LENGTH) = length - sizeof(chunk_t);; + + return length; + +} + +/* Function to write configuration to file */ + +static bool writeConfigurationToFile(char *buffer, configSettings_t *configSettings, uint32_t currentTime, bool gpsLocationReceived, int32_t *gpsLatitude, int32_t *gpsLongitude, bool acousticLocationReceived, int32_t *acousticLatitude, int32_t *acousticLongitude, uint8_t *firmwareDescription, uint8_t *firmwareVersion, uint8_t *serialNumber, uint8_t *deploymentID, uint8_t *defaultDeploymentID) { + + static char timezoneBuffer[CONFIG_TIMEZONE_LENGTH]; + + int32_t timezoneHours, timezoneMinutes; + + AudioMoth_timezoneRequested(&timezoneHours, &timezoneMinutes); + + int32_t timezoneSeconds = timezoneHours * SECONDS_IN_HOUR + timezoneMinutes * SECONDS_IN_MINUTE; + + RETURN_BOOL_ON_ERROR(AudioMoth_openFile("CONFIG.TXT")); + + uint32_t length = sprintf(buffer, "Device ID : " SERIAL_NUMBER "\r\n", FORMAT_SERIAL_NUMBER(serialNumber)); + + length += sprintf(buffer + length, "Firmware : %s (%u.%u.%u)\r\n\r\n", firmwareDescription, firmwareVersion[0], firmwareVersion[1], firmwareVersion[2]); + + if (memcmp(deploymentID, defaultDeploymentID, DEPLOYMENT_ID_LENGTH)) { + + length += sprintf(buffer + length, "Deployment ID : " SERIAL_NUMBER "\r\n\r\n", FORMAT_SERIAL_NUMBER(deploymentID)); + + } + + uint32_t timezoneLength = sprintf(timezoneBuffer, "UTC"); + + if (timezoneHours < 0) { + + timezoneLength += sprintf(timezoneBuffer + timezoneLength, "%ld", timezoneHours); + + } else if (timezoneHours > 0) { + + timezoneLength += sprintf(timezoneBuffer + timezoneLength, "+%ld", timezoneHours); + + } else { + + if (timezoneMinutes < 0) timezoneLength += sprintf(timezoneBuffer + timezoneLength, "-%ld", timezoneHours); + + if (timezoneMinutes > 0) timezoneLength += sprintf(timezoneBuffer + timezoneLength, "+%ld", timezoneHours); + + } + + if (timezoneMinutes < 0) timezoneLength += sprintf(timezoneBuffer + timezoneLength, ":%02ld", -timezoneMinutes); + + if (timezoneMinutes > 0) timezoneLength += sprintf(timezoneBuffer + timezoneLength, ":%02ld", timezoneMinutes); + + time_t rawTime = currentTime + timezoneSeconds; + + struct tm time; + + gmtime_r(&rawTime, &time); + + length += sprintf(buffer + length, "Device time : %04d-%02d-%02d %02d:%02d:%02d (%s)", YEAR_OFFSET + time.tm_year, MONTH_OFFSET + time.tm_mon, time.tm_mday, time.tm_hour, time.tm_min, time.tm_sec, timezoneBuffer); + + RETURN_BOOL_ON_ERROR(AudioMoth_writeToFile(buffer, length)); + + length = sprintf(buffer, "\r\n\r\nSample rate (Hz) : %lu\r\n", configSettings->sampleRate / configSettings->sampleRateDivider); + + static char *gainSettings[5] = {"Low", "Low-Medium", "Medium", "Medium-High", "High"}; + + length += sprintf(buffer + length, "Gain : %s\r\n\r\n", gainSettings[configSettings->gain]); + + length += sprintf(buffer + length, "Sleep duration (s) : "); + + if (configSettings->disableSleepRecordCycle) { + + length += sprintf(buffer + length, "-"); + + } else { + + length += sprintf(buffer + length, "%u", configSettings->sleepDuration); + + } + + length += sprintf(buffer + length, "\r\nRecording duration (s) : "); + + if (configSettings->disableSleepRecordCycle) { + + length += sprintf(buffer + length, "-"); + + } else { + + length += sprintf(buffer + length, "%u", configSettings->recordDuration); + + } + + RETURN_BOOL_ON_ERROR(AudioMoth_writeToFile(buffer, length)); + + if (configSettings->enableSunRecording) { + + int32_t latitude = gpsLocationReceived ? *gpsLatitude : acousticLocationReceived ? *acousticLatitude : configSettings->latitude; + + int32_t longitude = gpsLocationReceived ? *gpsLongitude : acousticLocationReceived ? *acousticLongitude : configSettings->longitude; + + char latitudeDirection = latitude < 0 ? 'S' : 'N'; + + char longitudeDirection = longitude < 0 ? 'W' : 'E'; + + if (gpsLocationReceived) { + + length = sprintf(buffer, "\r\n\r\nLocation : %ld.%06ld°%c %ld.%06ld°%c (GPS)", ABS(latitude) / GPS_LOCATION_PRECISION, ABS(latitude) % GPS_LOCATION_PRECISION, latitudeDirection, ABS(longitude) / GPS_LOCATION_PRECISION, ABS(longitude) % GPS_LOCATION_PRECISION, longitudeDirection); + + } else if (acousticLocationReceived) { + + length = sprintf(buffer, "\r\n\r\nLocation : %ld.%06ld°%c %ld.%06ld°%c (Acoustic chime)", ABS(latitude) / ACOUSTIC_LOCATION_PRECISION, ABS(latitude) % ACOUSTIC_LOCATION_PRECISION, latitudeDirection, ABS(longitude) / ACOUSTIC_LOCATION_PRECISION, ABS(longitude) % ACOUSTIC_LOCATION_PRECISION, longitudeDirection); + + } else { + + length = sprintf(buffer, "\r\n\r\nLocation : %ld.%02ld°%c %ld.%02ld°%c (Configuration app)", ABS(latitude) / CONFIG_LOCATION_PRECISION, ABS(latitude) % CONFIG_LOCATION_PRECISION, latitudeDirection, ABS(longitude) / CONFIG_LOCATION_PRECISION, ABS(longitude) % CONFIG_LOCATION_PRECISION, longitudeDirection); + + } + + static char* twilightTypes[3] = {"Civil", "Nautical", "Astronomical"}; + + static char* dawnDuskModes[5] = {"dawn", "dusk", "dawn and dusk", "dusk to dawn", "dawn to dusk"}; + + static char* sunriseSunsetModes[5] = {"Sunrise", "Sunset", "Sunrise and sunset", "Sunset to sunrise", "Sunrise to sunset"}; + + length += sprintf(buffer + length, "\r\nSun recording mode : "); + + if (configSettings->sunRecordingEvent == SR_SUNRISE_AND_SUNSET) { + + length += sprintf(buffer + length, "%s", sunriseSunsetModes[configSettings->sunRecordingMode]); + + } else { + + length += sprintf(buffer + length, "%s %s", twilightTypes[configSettings->sunRecordingEvent - 1], dawnDuskModes[configSettings->sunRecordingMode]); + + } + + char *sunriseText = configSettings->sunRecordingEvent == SR_SUNRISE_AND_SUNSET ? "\r\nSunrise - before, after (mins) " : "\r\nDawn - before, after (mins) "; + + char *sunsetText = configSettings->sunRecordingEvent == SR_SUNRISE_AND_SUNSET ? "\r\nSunset - before, after (mins) " : "\r\nDusk - before, after (mins) "; + + if (configSettings->sunRecordingMode == SUNRISE_RECORDING) { + + length += sprintf(buffer + length, "%s: %u, %u", sunriseText, configSettings->beforeSunriseMinutes, configSettings->afterSunriseMinutes); + length += sprintf(buffer + length, "%s: -, -", sunsetText); + + } else if (configSettings->sunRecordingMode == SUNSET_RECORDING) { + + length += sprintf(buffer + length, "%s: -, -", sunriseText); + length += sprintf(buffer + length, "%s: %u, %u", sunsetText, configSettings->beforeSunsetMinutes, configSettings->afterSunsetMinutes); + + } else if (configSettings->sunRecordingMode == SUNRISE_AND_SUNSET_RECORDING) { + + length += sprintf(buffer + length, "%s: %u, %u", sunriseText, configSettings->beforeSunriseMinutes, configSettings->afterSunriseMinutes); + length += sprintf(buffer + length, "%s: %u, %u", sunsetText, configSettings->beforeSunsetMinutes, configSettings->afterSunsetMinutes); + + } else if (configSettings->sunRecordingMode == SUNSET_TO_SUNRISE_RECORDING) { + + length += sprintf(buffer + length, "%s: -, %u", sunriseText, configSettings->afterSunriseMinutes); + length += sprintf(buffer + length, "%s: %u, -", sunsetText, configSettings->beforeSunsetMinutes); + + } else if (configSettings->sunRecordingMode == SUNRISE_TO_SUNSET_RECORDING) { + + length += sprintf(buffer + length, "%s: %u, -", sunriseText, configSettings->beforeSunriseMinutes); + length += sprintf(buffer + length, "%s: -, %u", sunsetText, configSettings->afterSunsetMinutes); + + } + + char *roundingText = configSettings->sunRecordingEvent == SR_SUNRISE_AND_SUNSET ? "\r\nSunrise/sunset rounding (mins) : %u" : "\r\nDawn/dusk rounding (mins) : %u"; + + length += sprintf(buffer + length, roundingText, configSettings->sunRoundingMinutes); + + } else { + + length = sprintf(buffer, "\r\n\r\nActive recording periods : %u\r\n", configSettings->activeRecordingPeriods); + + /* Calculate the start time offset for the appropriate time zone */ + + int32_t startMinutesOffset = timezoneSeconds / SECONDS_IN_MINUTE; + + if (configSettings->useTimezoneFromAcousticChime && configSettings->adjustScheduleUsingTimezoneFromAcousticChime) { + + startMinutesOffset = configSettings->timezoneHours * MINUTES_IN_HOUR + configSettings->timezoneMinutes; + + } + + /* Find the first recording period */ + + uint32_t minimumIndex = 0; + + uint32_t minimumStartMinutes = UINT32_MAX; + + for (uint32_t i = 0; i < configSettings->activeRecordingPeriods; i += 1) { + + uint32_t startMinutes = (MINUTES_IN_DAY + configSettings->recordingPeriods[i].startMinutes + startMinutesOffset) % MINUTES_IN_DAY; + + if (startMinutes < minimumStartMinutes) { + + minimumStartMinutes = startMinutes; + + minimumIndex = i; + + } + + } + + /* Display the recording periods */ + + for (uint32_t i = 0; i < configSettings->activeRecordingPeriods; i += 1) { + + uint32_t index = (minimumIndex + i) % configSettings->activeRecordingPeriods; + + uint32_t startMinutes = (MINUTES_IN_DAY + configSettings->recordingPeriods[index].startMinutes + startMinutesOffset) % MINUTES_IN_DAY; + + uint32_t endMinutes = (MINUTES_IN_DAY + configSettings->recordingPeriods[index].endMinutes + startMinutesOffset) % MINUTES_IN_DAY; + + length += sprintf(buffer + length, "\r\nRecording period %lu : %02lu:%02lu - %02lu:%02lu (%s)", i + 1, startMinutes / MINUTES_IN_HOUR, startMinutes % MINUTES_IN_HOUR, endMinutes / MINUTES_IN_HOUR, endMinutes % MINUTES_IN_HOUR, timezoneBuffer); + + } + + } + + RETURN_BOOL_ON_ERROR(AudioMoth_writeToFile(buffer, length)); + + if (configSettings->earliestRecordingTime == 0) { + + length = sprintf(buffer, "\r\n\r\nFirst recording date : ----------"); + + } else { + + time_t rawTime = configSettings->earliestRecordingTime + timezoneSeconds; + + if (configSettings->useTimezoneFromAcousticChime && configSettings->adjustScheduleUsingTimezoneFromAcousticChime) { + + rawTime += configSettings->timezoneHours * SECONDS_IN_HOUR + configSettings->timezoneMinutes * SECONDS_IN_MINUTE - timezoneSeconds; + + } + + gmtime_r(&rawTime, &time); + + if (time.tm_hour == 0 && time.tm_min == 0 && time.tm_sec == 0) { + + length = sprintf(buffer, "\r\n\r\nFirst recording date : "); + + length += sprintf(buffer + length, "%04d-%02d-%02d (%s)", YEAR_OFFSET + time.tm_year, MONTH_OFFSET + time.tm_mon, time.tm_mday, timezoneBuffer); + + } else { + + length = sprintf(buffer, "\r\n\r\nFirst recording time : "); + + length += sprintf(buffer + length, "%04d-%02d-%02d %02d:%02d:%02d (%s)", YEAR_OFFSET + time.tm_year, MONTH_OFFSET + time.tm_mon, time.tm_mday, time.tm_hour, time.tm_min, time.tm_sec, timezoneBuffer); + + } + + } + + if (configSettings->latestRecordingTime == 0) { + + length += sprintf(buffer + length, "\r\nLast recording date : ----------"); + + } else { + + time_t rawTime = configSettings->latestRecordingTime + timezoneSeconds; + + if (configSettings->useTimezoneFromAcousticChime && configSettings->adjustScheduleUsingTimezoneFromAcousticChime) { + + rawTime += configSettings->timezoneHours * SECONDS_IN_HOUR + configSettings->timezoneMinutes * SECONDS_IN_MINUTE - timezoneSeconds; + + } + + gmtime_r(&rawTime, &time); + + if (time.tm_hour == 0 && time.tm_min == 0 && time.tm_sec == 0) { + + rawTime -= SECONDS_IN_DAY; + + gmtime_r(&rawTime, &time); + + length += sprintf(buffer + length, "\r\nLast recording date : "); + + length += sprintf(buffer + length, "%04d-%02d-%02d (%s)", YEAR_OFFSET + time.tm_year, MONTH_OFFSET + time.tm_mon, time.tm_mday, timezoneBuffer); + + } else { + + length += sprintf(buffer + length, "\r\nLast recording time : "); + + length += sprintf(buffer + length, "%04d-%02d-%02d %02d:%02d:%02d (%s)", YEAR_OFFSET + time.tm_year, MONTH_OFFSET + time.tm_mon, time.tm_mday, time.tm_hour, time.tm_min, time.tm_sec, timezoneBuffer); + + } + + } + + RETURN_BOOL_ON_ERROR(AudioMoth_writeToFile(buffer, length)); + + length = sprintf(buffer, "\r\n\r\nFilter : "); + + if (configSettings->lowerFilterFreq == 0 && configSettings->higherFilterFreq == 0) { + + length += sprintf(buffer + length, "-"); + + } else if (configSettings->lowerFilterFreq == UINT16_MAX) { + + length += sprintf(buffer + length, "Low-pass (%u.%ukHz)", configSettings->higherFilterFreq / 10, configSettings->higherFilterFreq % 10); + + } else if (configSettings->higherFilterFreq == UINT16_MAX) { + + length += sprintf(buffer + length, "High-pass (%u.%ukHz)", configSettings->lowerFilterFreq / 10, configSettings->lowerFilterFreq % 10); + + } else { + + length += sprintf(buffer + length, "Band-pass (%u.%ukHz - %u.%ukHz)", configSettings->lowerFilterFreq / 10, configSettings->lowerFilterFreq % 10, configSettings->higherFilterFreq / 10, configSettings->higherFilterFreq % 10); + + } + + bool frequencyTriggerEnabled = configSettings->enableFrequencyTrigger; + + bool amplitudeThresholdEnabled = frequencyTriggerEnabled ? false : configSettings->amplitudeThreshold > 0 || configSettings->enableAmplitudeThresholdDecibelScale || configSettings->enableAmplitudeThresholdPercentageScale; + + length += sprintf(buffer + length, "\r\n\r\nTrigger type : "); + + if (frequencyTriggerEnabled) { + + length += sprintf(buffer + length, "Frequency (%u.%ukHz and window length of %u samples)", configSettings->frequencyTriggerCentreFrequency / 10, configSettings->frequencyTriggerCentreFrequency % 10, (0x01 << configSettings->frequencyTriggerWindowLengthShift)); + + length += sprintf(buffer + length, "\r\nThreshold setting : "); + + length += formatPercentage(buffer + length, configSettings->frequencyTriggerThresholdPercentageMantissa, configSettings->frequencyTriggerThresholdPercentageExponent); + + } else if (amplitudeThresholdEnabled) { + + length += sprintf(buffer + length, "Amplitude"); + + length += sprintf(buffer + length, "\r\nThreshold setting : "); + + if (configSettings->enableAmplitudeThresholdDecibelScale && configSettings->enableAmplitudeThresholdPercentageScale == false) { + + length += formatDecibels(buffer + length, configSettings->amplitudeThresholdDecibels, true); + + } else if (configSettings->enableAmplitudeThresholdPercentageScale && configSettings->enableAmplitudeThresholdDecibelScale == false) { + + length += formatPercentage(buffer + length, configSettings->amplitudeThresholdPercentageMantissa, configSettings->amplitudeThresholdPercentageExponent); + + } else { + + length += sprintf(buffer + length, "%u", configSettings->amplitudeThreshold); + + } + + } else { + + length += sprintf(buffer + length, "-"); + + length += sprintf(buffer + length, "\r\nThreshold setting : -"); + + } + + length += sprintf(buffer + length, "\r\nMinimum trigger duration (s) : "); + + if (frequencyTriggerEnabled || amplitudeThresholdEnabled) { + + length += sprintf(buffer + length, "%u", configSettings->minimumTriggerDuration); + + } else { + + length += sprintf(buffer + length, "-"); + + } + + RETURN_BOOL_ON_ERROR(AudioMoth_writeToFile(buffer, length)); + + length = sprintf(buffer, "\r\n\r\nEnable LED : %s\r\n", configSettings->enableLED ? "Yes" : "No"); + + length += sprintf(buffer + length, "Enable low-voltage cut-off : %s\r\n", configSettings->enableLowVoltageCutoff ? "Yes" : "No"); + + length += sprintf(buffer + length, "Enable battery level indication : %s\r\n\r\n", configSettings->disableBatteryLevelDisplay ? "No" : configSettings->batteryLevelDisplayType == NIMH_LIPO_BATTERY_VOLTAGE ? "Yes (NiMH/LiPo voltage range)" : "Yes"); + + length += sprintf(buffer + length, "Always require acoustic chime : %s\r\n", configSettings->requireAcousticConfiguration ? "Yes" : "No"); + + length += sprintf(buffer + length, "Also require location in chime : %s\r\n", configSettings->requireAcousticConfiguration == false ? "-" : configSettings->requireAcousticLocation ? "Yes" : "No"); + + length += sprintf(buffer + length, "Use timezone from chime : %s\r\n", configSettings->useTimezoneFromAcousticChime ? "Yes" : "No"); + + length += sprintf(buffer + length, "Also adjust recording schedule : %s\r\n\r\n", configSettings->useTimezoneFromAcousticChime == false ? "-" : configSettings->adjustScheduleUsingTimezoneFromAcousticChime ? "Yes" : "No"); + + RETURN_BOOL_ON_ERROR(AudioMoth_writeToFile(buffer, length)); + + length = sprintf(buffer, "Recording preparation time (s) : %d\r\n", configSettings->preparationPeriodBuffer == 0 ? DEFAULT_PREPARATION_PERIOD_BUFFER / MILLISECONDS_IN_SECOND : configSettings->preparationPeriodBuffer); + + length += sprintf(buffer + length, "Use device ID in WAV file name : %s\r\n", configSettings->enableFilenameWithDeviceID ? "Yes" : "No"); + + length += sprintf(buffer + length, "Use daily folder for WAV files : %s\r\n\r\n", configSettings->enableDailyFolders ? "Yes" : "No"); + + length += sprintf(buffer + length, "Disable 48Hz DC blocking filter : %s\r\n", configSettings->disable48HzDCBlockingFilter ? "Yes" : "No"); + + length += sprintf(buffer + length, "Enable energy saver mode : %s\r\n", configSettings->enableEnergySaverMode ? "Yes" : "No"); + + length += sprintf(buffer + length, "Enable low gain range : %s\r\n\r\n", configSettings->enableLowGainRange ? "Yes" : "No"); + + RETURN_BOOL_ON_ERROR(AudioMoth_writeToFile(buffer, length)); + + length = sprintf(buffer, "Ignore external microphone : %s\r\n\r\n", configSettings->ignoreExternalMicrophoneForAcousticChime ? "Yes" : "No"); + + length += sprintf(buffer + length, "Enable magnetic switch : %s\r\n\r\n", configSettings->enableMagneticSwitch ? "Yes" : "No"); + + RETURN_BOOL_ON_ERROR(AudioMoth_writeToFile(buffer, length)); + + length = sprintf(buffer, "Enable GPS time setting : %s\r\n", configSettings->enableTimeSettingFromGPS ? "Yes" : "No"); + + length += sprintf(buffer + length, "GPS fix before and after : %s\r\n", configSettings->enableTimeSettingFromGPS == false ? "-" : configSettings->enableTimeSettingBeforeAndAfterRecordings ? "Individual recordings" : "Recording periods"); + + length += sprintf(buffer + length, "GPS fix time (mins) : "); + + if (configSettings->enableTimeSettingFromGPS) { + + uint32_t gpsTimeSettingPeriod = configSettings->gpsTimeSettingPeriod == 0 ? GPS_DEFAULT_TIME_SETTING_PERIOD / SECONDS_IN_MINUTE : configSettings->gpsTimeSettingPeriod; + + length += sprintf(buffer + length, "%ld\r\n", gpsTimeSettingPeriod); + + } else { + + length += sprintf(buffer + length, "-\r\n"); + + } + + RETURN_BOOL_ON_ERROR(AudioMoth_writeToFile(buffer, length)); + + RETURN_BOOL_ON_ERROR(AudioMoth_closeFile()); + + return true; + +} + +/* Backup domain variables */ + +static uint32_t *backupDomainFlags = (uint32_t*)AM_BACKUP_DOMAIN_START_ADDRESS; + +static uint32_t *previousSwitchPosition = (uint32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 4); + +static uint32_t *startOfRecordingPeriod = (uint32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 8); + +static uint32_t *timeOfNextRecording = (uint32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 12); + +static uint32_t *indexOfNextRecording = (uint32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 16); + +static uint32_t *durationOfNextRecording = (uint32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 20); + +static uint32_t *timeOfNextGPSTimeSetting = (uint32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 24); + +static uint8_t *deploymentID = (uint8_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 28); + +static uint32_t *numberOfRecordingErrors = (uint32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 36); + +static uint32_t *numberOfConsecutiveRecordingErrors = (uint32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 40); + +static uint32_t *recordingPreparationPeriod = (uint32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 44); + +static int32_t *gpsLatitude = (int32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 48); + +static int32_t *gpsLongitude = (int32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 52); + +static int32_t *gpsLastFixLatitude = (int32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 56); + +static int32_t *gpsLastFixLongitude = (int32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 60); + +static int32_t *acousticLatitude = (int32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 64); + +static int32_t *acousticLongitude = (int32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 68); + +static int32_t *acousticTimezoneMinutes = (int32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 72); + +static uint32_t *numberOfSunRecordingPeriods = (uint32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 76); + +static recordingPeriod_t *firstSunRecordingPeriod = (recordingPeriod_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 80); + +static recordingPeriod_t *secondSunRecordingPeriod = (recordingPeriod_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 84); + +static uint32_t *timeOfNextSunriseSunsetCalculation = (uint32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 88); + +static uint32_t *timeAtWhichToSwitchOffLED = (uint32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 92); + +/* Functions to query, set and clear backup domain flags */ + +typedef enum { + BACKUP_WRITTEN_CONFIGURATION_TO_FILE, + BACKUP_READY_TO_MAKE_RECORDING, + BACKUP_MUST_SET_TIME_FROM_GPS, + BACKUP_SHOULD_SET_TIME_FROM_GPS, + BACKUP_WAITING_FOR_MAGNETIC_SWITCH, + BACKUP_POWERED_DOWN_WITH_SHORT_WAIT_INTERVAL, + BACKUP_GPS_LOCATION_RECEIVED, + BACKUP_ACOUSTIC_LOCATION_RECEIVED, + BACKUP_ACOUSTIC_TIMEZONE_RECEIVED +} AM_backupDomainFlag_t; + +static inline bool getBackupFlag(AM_backupDomainFlag_t flag) { + uint32_t mask = (1 << flag); + return (*backupDomainFlags & mask) == mask; +} + +static inline void setBackupFlag(AM_backupDomainFlag_t flag, bool state) { + if (state) { + *backupDomainFlags |= (1 << flag); + } else { + *backupDomainFlags &= ~(1 << flag); + } +} + +/* Filter variables */ + +static AM_filterType_t requestedFilterType; + +/* DMA transfer variable */ + +static uint32_t numberOfRawSamplesInDMATransfer; + +/* SRAM buffer variables */ + +static volatile uint32_t writeBuffer; + +static volatile uint32_t writeBufferIndex; + +static int16_t* buffers[NUMBER_OF_BUFFERS]; + +/* Flag to start processing DMA transfers */ + +static volatile uint32_t numberOfDMATransfers; + +static volatile uint32_t numberOfDMATransfersToWait; + +/* Write indicator buffer */ + +static bool writeIndicator[NUMBER_OF_BUFFERS]; + +/* GPS message buffer */ + +static char gpsMessageBuffer[GPS_MESSAGE_BUFFER_SIZE_IN_BYTES]; + +/* Compression buffer */ + +static int16_t compressionBuffer[COMPRESSION_BUFFER_SIZE_IN_BYTES / NUMBER_OF_BYTES_IN_SAMPLE] __attribute__ ((aligned(UINT32_SIZE_IN_BYTES))); + +/* Configuration settings */ + +static configSettings_t *configSettings = &((persistentConfigSettings_t*)AM_FLASH_USER_DATA_ADDRESS)->configSettings; + +static persistentConfigSettings_t *persistentConfigSettings = (persistentConfigSettings_t*)AM_FLASH_USER_DATA_ADDRESS; + +static persistentConfigSettings_t *tempPersistentConfigSettings = (persistentConfigSettings_t*)compressionBuffer; + +/* LED variable */ + +static bool enableLED; + +/* GPS fix variables */ + +static bool gpsEnableLED; + +static bool gpsPPSEvent; + +static bool gpsFixEvent; + +static bool gpsMessageEvent; + +static uint32_t timeSetFromGPS; + +static bool gpsFirstMessageReceived; + +static uint32_t gpsTickEventCount = 1; + +static uint32_t gpsTickEventModulo = GPS_TICK_EVENTS_PER_SECOND; + +/* Audio configuration variables */ + +static bool audioConfigStateLED; + +static bool audioConfigToggleLED; + +static uint32_t audioConfigPulseCounter; + +static bool acousticConfigurationPerformed; + +static uint32_t secondsOfAcousticSignalStart; + +static uint32_t millisecondsOfAcousticSignalStart; + +/* Deployment ID variable */ + +static uint8_t defaultDeploymentID[DEPLOYMENT_ID_LENGTH]; + +/* Recording state */ + +static volatile bool magneticSwitch; + +static volatile bool microphoneChanged; + +static volatile bool switchPositionChanged; + +/* DMA buffers */ + +static int16_t primaryBuffer[MAXIMUM_SAMPLES_IN_DMA_TRANSFER]; + +static int16_t secondaryBuffer[MAXIMUM_SAMPLES_IN_DMA_TRANSFER]; + +/* Firmware version and description */ + +static uint8_t firmwareVersion[AM_FIRMWARE_VERSION_LENGTH] = {1, 12, 0}; + +static uint8_t firmwareDescription[AM_FIRMWARE_DESCRIPTION_LENGTH] = "AudioMoth-Firmware-Basic"; + +/* Function prototypes */ + +static AM_recordingState_t makeRecording(uint32_t timeOfNextRecording, uint32_t recordDuration, uint32_t timeAtWhichToSwitchOffLED, AM_extendedBatteryState_t extendedBatteryState, int32_t temperature, uint32_t *fileOpenTime, uint32_t *fileOpenMilliseconds); + +static void scheduleRecording(uint32_t currentTime, uint32_t *timeOfNextRecording, uint32_t *indexOfNextRecording, uint32_t *durationOfNextRecording, uint32_t *startOfRecordingPeriod, uint32_t *endOfRecordingPeriod); + +static void determineTimeOfNextSunriseSunsetCalculation(uint32_t currentTime, uint32_t *timeOfNextSunriseSunsetCalculation); + +static void determineSunriseAndSunsetTimesAndScheduleRecording(uint32_t currentTime); + +static void determineSunriseAndSunsetTimes(uint32_t currentTime); + +static void flashLedToIndicateBatteryLife(void); + +/* Functions of copy to and from the backup domain */ + +static void copyToBackupDomain(uint32_t *dst, uint8_t *src, uint32_t length) { + + uint32_t value = 0; + + for (uint32_t i = 0; i < length / UINT32_SIZE_IN_BYTES; i += 1) { + *(dst + i) = *((uint32_t*)src + i); + } + + for (uint32_t i = 0; i < length % UINT32_SIZE_IN_BYTES; i += 1) { + value = (value << BITS_PER_BYTE) + *(src + length - 1 - i); + } + + if (length % UINT32_SIZE_IN_BYTES) *(dst + length / UINT32_SIZE_IN_BYTES) = value; + +} + +/* GPS time setting functions */ + +static void writeGPSLogMessage(char *buffer, uint32_t currentTime, uint32_t currentMilliseconds, char *message) { + + struct tm time; + + time_t rawTime = currentTime; + + gmtime_r(&rawTime, &time); + + uint32_t length = sprintf(buffer, "%02d/%02d/%04d %02d:%02d:%02d.%03lu UTC: %s\r\n", time.tm_mday, MONTH_OFFSET + time.tm_mon, YEAR_OFFSET + time.tm_year, time.tm_hour, time.tm_min, time.tm_sec, currentMilliseconds, message); + + AudioMoth_writeToFile(buffer, length); + +} + +static GPS_fixResult_t setTimeFromGPS(bool showLED, AM_gpsFixMode_t fixMode, uint32_t timeout) { + + /* Enable GPS and get the current time */ + + GPS_powerUpGPS(); + + GPS_enableGPSInterface(); + + uint32_t currentTime, currentMilliseconds; + + /* Open the GPS log file */ + + bool success = AudioMoth_appendFile(GPS_FILENAME); + + /* Add power up message */ + + static char *messages[] = { + "GPS switched on for initial fix.", + "GPS switched on for fix before recording period.", + "GPS switched on for fix between recording periods.", + "GPS switched on for fix after recording period.", + "GPS switched on for fix before recording.", + "GPS switched on for fix between recordings.", + "GPS switched on for fix after recording." + }; + + char *message = messages[fixMode]; + + AudioMoth_getTime(¤tTime, ¤tMilliseconds); + + writeGPSLogMessage((char*)compressionBuffer, currentTime, currentMilliseconds, message); + + /* Set green LED and enter routine */ + + gpsEnableLED = showLED; + + if (gpsEnableLED) AudioMoth_setGreenLED(true); + + GPS_fixResult_t result = GPS_setTimeFromGPS(timeout); + + AudioMoth_setRedLED(false); + + GPS_disableGPSInterface(); + + /* Power down the GPS */ + + uint32_t powerOffTime, powerOffMilliseconds; + + AudioMoth_getTime(&powerOffTime, &powerOffMilliseconds); + + if (result == GPS_SUCCESS || result == GPS_TIMEOUT) powerOffMilliseconds = 0; + + if (result == GPS_SUCCESS) { + + powerOffTime = MIN(timeSetFromGPS + GPS_ADDITIONAL_POWER_INTERVAL, timeout); + + while (currentTime < powerOffTime) { + + AudioMoth_feedWatchdog(); + + AudioMoth_delay(SHORT_WAIT_INTERVAL); + + AudioMoth_getTime(¤tTime, ¤tMilliseconds); + + } + + } + + AudioMoth_setGreenLED(false); + + GPS_powerDownGPS(); + + /* Write result messages */ + + if (result == GPS_CANCELLED_BY_MAGNETIC_SWITCH) writeGPSLogMessage((char*)compressionBuffer, powerOffTime, powerOffMilliseconds, "Time was not updated. Cancelled by magnetic switch."); + + if (result == GPS_CANCELLED_BY_SWITCH) writeGPSLogMessage((char*)compressionBuffer, powerOffTime, powerOffMilliseconds, "Time was not updated. Cancelled by switch position change."); + + if (result == GPS_TIMEOUT) writeGPSLogMessage((char*)compressionBuffer, powerOffTime, powerOffMilliseconds, "Time was not updated. Timed out."); + + writeGPSLogMessage((char*)compressionBuffer, powerOffTime, powerOffMilliseconds, "GPS switched off.\r\n"); + + /* Close file and return */ + + if (success) AudioMoth_closeFile(); + + return result; + +} + +/* Magnetic switch wait functions */ + +static void startWaitingForMagneticSwitch() { + + /* Flash LED to indicate start of waiting for magnetic switch */ + + FLASH_REPEAT_LED(Red, MAGNETIC_SWITCH_CHANGE_FLASHES, SHORT_LED_FLASH_DURATION); + + /* Cancel any scheduled recording */ + + *indexOfNextRecording = 0; + + *timeOfNextRecording = UINT32_MAX; + + *startOfRecordingPeriod = UINT32_MAX; + + *durationOfNextRecording = UINT32_MAX; + + *timeOfNextGPSTimeSetting = UINT32_MAX; + + setBackupFlag(BACKUP_SHOULD_SET_TIME_FROM_GPS, false); + + setBackupFlag(BACKUP_WAITING_FOR_MAGNETIC_SWITCH, true); + +} + +static void stopWaitingForMagneticSwitch(uint32_t *currentTime, uint32_t *currentMilliseconds) { + + /* Flash LED to indicate end of waiting for magnetic switch */ + + FLASH_REPEAT_LED(Green, MAGNETIC_SWITCH_CHANGE_FLASHES, SHORT_LED_FLASH_DURATION); + + /* Schedule next recording */ + + AudioMoth_getTime(currentTime, currentMilliseconds); + + uint32_t scheduleTime = *currentTime + ROUNDED_UP_DIV(*currentMilliseconds + *recordingPreparationPeriod, MILLISECONDS_IN_SECOND); + + determineSunriseAndSunsetTimesAndScheduleRecording(scheduleTime); + + /* Update flag */ + + setBackupFlag(BACKUP_WAITING_FOR_MAGNETIC_SWITCH, false); + +} + +/* Function to calculate the time to the next event */ + +static void calculateTimeToNextEvent(uint32_t currentTime, uint32_t currentMilliseconds, int64_t *timeUntilPreparationStart, int64_t *timeUntilNextGPSTimeSetting) { + + int64_t preparationPeriodBuffer = (configSettings->preparationPeriodBuffer == 0 ? DEFAULT_PREPARATION_PERIOD_BUFFER : configSettings->preparationPeriodBuffer * MILLISECONDS_IN_SECOND); + + *timeUntilPreparationStart = (int64_t)*timeOfNextRecording * MILLISECONDS_IN_SECOND - (int64_t)*recordingPreparationPeriod - preparationPeriodBuffer - (int64_t)currentTime * MILLISECONDS_IN_SECOND - (int64_t)currentMilliseconds; + + *timeUntilNextGPSTimeSetting = (int64_t)*timeOfNextGPSTimeSetting * MILLISECONDS_IN_SECOND - (int64_t)currentTime * MILLISECONDS_IN_SECOND - (int64_t)currentMilliseconds; + +} + +/* Main function */ + +int main(void) { + + /* Initialise device */ + + AudioMoth_initialise(); + + /* ESP32 upload bridge starts closed. It is opened only by the scheduler + * when no recording, preparation, GPS, USB, or protected SD activity is active. */ + ESPBridge_init(); + ESPBridge_setBusy(true); + ESPBridge_setUploadAllowed(false); + + AM_switchPosition_t switchPosition = AudioMoth_getSwitchPosition(); + + if (AudioMoth_isInitialPowerUp()) { + + /* Initialise recording schedule variables */ + + *indexOfNextRecording = 0; + + *timeOfNextRecording = UINT32_MAX; + + *startOfRecordingPeriod = UINT32_MAX; + + *durationOfNextRecording = UINT32_MAX; + + *timeOfNextGPSTimeSetting = UINT32_MAX; + + /* Initialise configuration writing variable */ + + setBackupFlag(BACKUP_WRITTEN_CONFIGURATION_TO_FILE, false); + + /* Initialise recording state variables */ + + *previousSwitchPosition = AM_SWITCH_NONE; + + setBackupFlag(BACKUP_MUST_SET_TIME_FROM_GPS, false); + + setBackupFlag(BACKUP_SHOULD_SET_TIME_FROM_GPS, false); + + setBackupFlag(BACKUP_READY_TO_MAKE_RECORDING, false); + + *numberOfRecordingErrors = 0; + + *numberOfConsecutiveRecordingErrors = 0; + + *recordingPreparationPeriod = INITIAL_PREPARATION_PERIOD; + + /* Initialise magnetic switch state variables */ + + setBackupFlag(BACKUP_WAITING_FOR_MAGNETIC_SWITCH, false); + + /* Initialise the power down interval flag */ + + setBackupFlag(BACKUP_POWERED_DOWN_WITH_SHORT_WAIT_INTERVAL, false); + + /* Initial GPS and sunrise and sunset variables */ + + *gpsLastFixLatitude = 0; + + *gpsLastFixLongitude = 0; + + setBackupFlag(BACKUP_GPS_LOCATION_RECEIVED, false); + + setBackupFlag(BACKUP_ACOUSTIC_LOCATION_RECEIVED, false); + + setBackupFlag(BACKUP_ACOUSTIC_TIMEZONE_RECEIVED, false); + + *timeOfNextSunriseSunsetCalculation = 0; + + /* Copy default deployment ID */ + + copyToBackupDomain((uint32_t*)deploymentID, (uint8_t*)defaultDeploymentID, DEPLOYMENT_ID_LENGTH); + + } + + /* Check the persistent configuration firmware version and description matches the default values */ + + if (memcmp(persistentConfigSettings->firmwareVersion, firmwareVersion, AM_FIRMWARE_VERSION_LENGTH) != 0 || memcmp(persistentConfigSettings->firmwareDescription, firmwareDescription, AM_FIRMWARE_DESCRIPTION_LENGTH) != 0) { + + memcpy(&tempPersistentConfigSettings->firmwareVersion, &firmwareVersion, AM_FIRMWARE_VERSION_LENGTH); + + memcpy(&tempPersistentConfigSettings->firmwareDescription, &firmwareDescription, AM_FIRMWARE_DESCRIPTION_LENGTH); + + memcpy(&tempPersistentConfigSettings->configSettings, (uint8_t*)&defaultConfigSettings, sizeof(configSettings_t)); + + uint32_t numberOfBytes = ROUND_UP_TO_MULTIPLE(sizeof(persistentConfigSettings_t), UINT32_SIZE_IN_BYTES); + + bool success = AudioMoth_writeToFlashUserDataPage((uint8_t*)tempPersistentConfigSettings, numberOfBytes); + + if (success == false) { + + /* Cannot recover from a failure at this point */ + + for (uint32_t i = 0; i < 5; i += 1) { + + AudioMoth_setBothLED(true); + + AudioMoth_delay(100); + + AudioMoth_setBothLED(false); + + AudioMoth_delay(100); + + } + + AudioMoth_powerDownAndWakeMilliseconds(DEFAULT_WAIT_INTERVAL); + + } + + } + + /* Handle the case that the switch is in USB position */ + + if (switchPosition == AM_SWITCH_USB) { + + if (configSettings->disableBatteryLevelDisplay == false && (*previousSwitchPosition == AM_SWITCH_DEFAULT || *previousSwitchPosition == AM_SWITCH_CUSTOM)) { + + flashLedToIndicateBatteryLife(); + + } + + AudioMoth_handleUSB(); + + SAVE_SWITCH_POSITION_AND_POWER_DOWN(DEFAULT_WAIT_INTERVAL); + + } + + /* Read the time */ + + uint32_t currentTime; + + uint32_t currentMilliseconds; + + AudioMoth_getTime(¤tTime, ¤tMilliseconds); + + /* ESP32 time-setting path. + * + * In ESP-node deployments, the ESP32 gets server time and sends TIME over + * UART. Do not fall into the stock acoustic chime configuration path just + * because the AudioMoth RTC is unset after flashing or power loss. + * + * File upload remains disabled here. Only non-file bridge commands should + * be useful here: PING, STATUS, TIME, DONE. LIST, GET, and DELETE are + * rejected because uploadAllowed is false. + */ + + if (switchPosition == AM_SWITCH_CUSTOM && (AudioMoth_hasTimeBeenSet() == false || ESPBridge_isRequestActive())) { + + uint32_t waitedMilliseconds = 0; + + ESPBridge_setBusy(false); + ESPBridge_setUploadAllowed(false); + + while ((AudioMoth_hasTimeBeenSet() == false || ESPBridge_isRequestActive()) && waitedMilliseconds < ESP_TIME_WAIT_TIMEOUT_MS) { + + AudioMoth_feedWatchdog(); + + if (ESPBridge_isRequestActive()) { + + uint32_t bridgeCurrentTime; + uint32_t bridgeCurrentMilliseconds; + + AudioMoth_getTime(&bridgeCurrentTime, &bridgeCurrentMilliseconds); + + ESPBridge_serviceUntil(bridgeCurrentTime + ESP_TIME_SERVICE_WINDOW_SECONDS); + + waitedMilliseconds += ESP_TIME_SERVICE_WINDOW_SECONDS * MILLISECONDS_IN_SECOND; + + } else { + + if (configSettings->enableLED && waitedMilliseconds % WAITING_LED_FLASH_INTERVAL == 0) { + FLASH_LED(Green, WAITING_LED_FLASH_DURATION) + } + + AudioMoth_delay(ESP_TIME_WAIT_STEP_MS); + + waitedMilliseconds += ESP_TIME_WAIT_STEP_MS; + + } + + } + + ESPBridge_setUploadAllowed(false); + ESPBridge_setBusy(true); + + if (AudioMoth_hasTimeBeenSet() == false) { + + SAVE_SWITCH_POSITION_AND_POWER_DOWN(DEFAULT_WAIT_INTERVAL); + + } + + AudioMoth_getTime(¤tTime, ¤tMilliseconds); + + } + + /* Check if switch has just been moved to CUSTOM or DEFAULT */ + + bool fileSystemEnabled = false; + + if (switchPosition != *previousSwitchPosition) { + + /* Reset the LED and GPS flags */ + + *timeAtWhichToSwitchOffLED = UINT32_MAX; + + setBackupFlag(BACKUP_MUST_SET_TIME_FROM_GPS, false); + + setBackupFlag(BACKUP_SHOULD_SET_TIME_FROM_GPS, false); + + /* Reset the power down interval flag */ + + setBackupFlag(BACKUP_POWERED_DOWN_WITH_SHORT_WAIT_INTERVAL, false); + + /* Check there are active recording periods if the switch is in CUSTOM position */ + + setBackupFlag(BACKUP_READY_TO_MAKE_RECORDING, switchPosition == AM_SWITCH_DEFAULT || (switchPosition == AM_SWITCH_CUSTOM && (configSettings->activeRecordingPeriods > 0 || configSettings->enableSunRecording))); + + /* Check if acoustic configuration is required */ + + if (getBackupFlag(BACKUP_READY_TO_MAKE_RECORDING)) { + + /* Determine if acoustic configuration is required */ + + bool shouldPerformAcousticConfiguration = switchPosition == AM_SWITCH_CUSTOM && configSettings->requireAcousticConfiguration; + + /* Overrule this decision if setting of time from GPS is enabled and acoustic configuration not enforced. Also set GPS time setting flag */ + + if (switchPosition == AM_SWITCH_CUSTOM && configSettings->enableTimeSettingFromGPS) { + + if (configSettings->requireAcousticConfiguration == false) shouldPerformAcousticConfiguration = false; + + setBackupFlag(BACKUP_MUST_SET_TIME_FROM_GPS, true); + + } + + /* Determine whether to listen for the acoustic tone */ + + bool listenForAcousticTone = false; + + if (listenForAcousticTone) { + + AudioConfig_enableAudioConfiguration(configSettings->ignoreExternalMicrophoneForAcousticChime); + + shouldPerformAcousticConfiguration = AudioConfig_listenForAudioConfigurationTone(AUDIO_CONFIG_TONE_TIMEOUT); + + } + + if (shouldPerformAcousticConfiguration) { + + AudioMoth_setRedLED(true); + + AudioMoth_setGreenLED(false); + + audioConfigPulseCounter = 0; + + audioConfigStateLED = false; + + audioConfigToggleLED = false; + + acousticConfigurationPerformed = false; + + if (listenForAcousticTone == false) { + + AudioConfig_enableAudioConfiguration(configSettings->ignoreExternalMicrophoneForAcousticChime); + + } + + AudioConfig_listenForAudioConfigurationPackets(listenForAcousticTone, AUDIO_CONFIG_PACKETS_TIMEOUT); + + AudioConfig_disableAudioConfiguration(); + + if (acousticConfigurationPerformed) { + + /* Indicate success with LED flashes */ + + AudioMoth_setRedLED(false); + + AudioMoth_setGreenLED(true); + + AudioMoth_delay(1000); + + AudioMoth_delay(1000); + + AudioMoth_setGreenLED(false); + + AudioMoth_delay(500); + + } else { + + /* Turn off LED */ + + AudioMoth_setBothLED(false); + + /* Determine if it is possible to still make a recording */ + + if (configSettings->requireAcousticConfiguration) { + + setBackupFlag(BACKUP_READY_TO_MAKE_RECORDING, false); + + } else { + + setBackupFlag(BACKUP_READY_TO_MAKE_RECORDING, AudioMoth_hasTimeBeenSet() || getBackupFlag(BACKUP_MUST_SET_TIME_FROM_GPS)); + + } + + /* Power down */ + + SAVE_SWITCH_POSITION_AND_POWER_DOWN(DEFAULT_WAIT_INTERVAL); + + } + + } else if (listenForAcousticTone) { + + AudioConfig_disableAudioConfiguration(); + + } + + } + + /* Calculate time of next recording if ready to make a recording */ + + if (getBackupFlag(BACKUP_READY_TO_MAKE_RECORDING)) { + + /* Enable energy saver mode */ + + if (isEnergySaverMode(configSettings)) AudioMoth_setClockDivider(AM_HF_CLK_DIV2); + + /* Reset the recording error counter */ + + *numberOfRecordingErrors = 0; + + *numberOfConsecutiveRecordingErrors = 0; + + /* Reset the recording preparation period to default */ + + *recordingPreparationPeriod = INITIAL_PREPARATION_PERIOD; + + /* Reset persistent configuration write flag */ + + setBackupFlag(BACKUP_WRITTEN_CONFIGURATION_TO_FILE, false); + + /* Reset GPS and sunrise and sunset variables */ + + *gpsLastFixLatitude = 0; + + *gpsLastFixLongitude = 0; + + setBackupFlag(BACKUP_GPS_LOCATION_RECEIVED, false); + + *timeOfNextSunriseSunsetCalculation = 0; + + /* Try to write configuration now if it will not be written later when time is set */ + + if (configSettings->enableSunRecording == false || getBackupFlag(BACKUP_MUST_SET_TIME_FROM_GPS) == false) { + + fileSystemEnabled = AudioMoth_enableFileSystem(configSettings->sampleRateDivider == 1 ? AM_SD_CARD_HIGH_SPEED : AM_SD_CARD_NORMAL_SPEED); + + if (fileSystemEnabled) { + + AudioMoth_getTime(¤tTime, ¤tMilliseconds); + + bool gpsLocationReceived = getBackupFlag(BACKUP_GPS_LOCATION_RECEIVED); + + bool acousticLocationReceived = getBackupFlag(BACKUP_ACOUSTIC_LOCATION_RECEIVED); + + bool success = writeConfigurationToFile((char*)compressionBuffer, configSettings, currentTime, gpsLocationReceived, gpsLatitude, gpsLongitude, acousticLocationReceived, acousticLatitude, acousticLongitude, firmwareDescription, firmwareVersion, (uint8_t*)AM_UNIQUE_ID_START_ADDRESS, deploymentID, defaultDeploymentID); + + setBackupFlag(BACKUP_WRITTEN_CONFIGURATION_TO_FILE, success); + + } + + } + + /* Update the time and calculate earliest schedule start time */ + + AudioMoth_getTime(¤tTime, ¤tMilliseconds); + + uint32_t scheduleTime = currentTime + ROUNDED_UP_DIV(currentMilliseconds + *recordingPreparationPeriod, MILLISECONDS_IN_SECOND); + + /* Schedule the next recording */ + + if (switchPosition == AM_SWITCH_CUSTOM) { + + setBackupFlag(BACKUP_WAITING_FOR_MAGNETIC_SWITCH, configSettings->enableMagneticSwitch); + + if (configSettings->enableMagneticSwitch || getBackupFlag(BACKUP_MUST_SET_TIME_FROM_GPS)) { + + *indexOfNextRecording = 0; + + *timeOfNextRecording = UINT32_MAX; + + *startOfRecordingPeriod = UINT32_MAX; + + *durationOfNextRecording = UINT32_MAX; + + *timeOfNextGPSTimeSetting = UINT32_MAX; + + } else { + + *timeOfNextRecording = UINT32_MAX; + + *startOfRecordingPeriod = UINT32_MAX; + + determineSunriseAndSunsetTimesAndScheduleRecording(scheduleTime); + + if (configSettings->enableLED == false) *timeAtWhichToSwitchOffLED = currentTime + SECONDS_IN_MINUTE; + + } + + } + + /* Set parameters to start recording now */ + + if (switchPosition == AM_SWITCH_DEFAULT) { + + *indexOfNextRecording = 0; + + *timeOfNextRecording = scheduleTime; + + *startOfRecordingPeriod = scheduleTime; + + *durationOfNextRecording = UINT32_MAX; + + *timeOfNextGPSTimeSetting = UINT32_MAX; + + } + + } + + } + + /* If not ready to make a recording then flash LED and power down */ + + if (getBackupFlag(BACKUP_READY_TO_MAKE_RECORDING) == false) { + + FLASH_LED(Both, SHORT_LED_FLASH_DURATION) + + SAVE_SWITCH_POSITION_AND_POWER_DOWN(DEFAULT_WAIT_INTERVAL); + + } + + /* Enable the magnetic switch */ + + bool magneticSwitchEnabled = configSettings->enableMagneticSwitch && switchPosition == AM_SWITCH_CUSTOM; + + if (magneticSwitchEnabled) GPS_enableMagneticSwitch(); + + /* Reset LED flags */ + + enableLED = currentTime < *timeAtWhichToSwitchOffLED; + + bool shouldSuppressLED = getBackupFlag(BACKUP_POWERED_DOWN_WITH_SHORT_WAIT_INTERVAL); + + setBackupFlag(BACKUP_POWERED_DOWN_WITH_SHORT_WAIT_INTERVAL, false); + + /* Calculate time until next activity */ + + int64_t timeUntilPreparationStart; + + int64_t timeUntilNextGPSTimeSetting; + + calculateTimeToNextEvent(currentTime, currentMilliseconds, &timeUntilPreparationStart, &timeUntilNextGPSTimeSetting); + + /* If the GPS synchronisation window has passed then cancel it */ + + int64_t timeSinceScheduledGPSTimeSetting = -timeUntilNextGPSTimeSetting; + + uint32_t gpsTimeSettingPeriod = configSettings->gpsTimeSettingPeriod == 0 ? GPS_DEFAULT_TIME_SETTING_PERIOD : configSettings->gpsTimeSettingPeriod * SECONDS_IN_MINUTE; + + if (timeSinceScheduledGPSTimeSetting > gpsTimeSettingPeriod * MILLISECONDS_IN_SECOND) { + + *timeOfNextGPSTimeSetting = UINT32_MAX; + + calculateTimeToNextEvent(currentTime, currentMilliseconds, &timeUntilPreparationStart, &timeUntilNextGPSTimeSetting); + + } + + /* Set the time from the GPS */ + + if (getBackupFlag(BACKUP_MUST_SET_TIME_FROM_GPS) && getBackupFlag(BACKUP_WAITING_FOR_MAGNETIC_SWITCH) == false) { + + /* Enable the file system and set the time from the GPS */ + + if (!fileSystemEnabled) fileSystemEnabled = AudioMoth_enableFileSystem(AM_SD_CARD_NORMAL_SPEED); + + GPS_fixResult_t fixResult = setTimeFromGPS(enableLED, INITIAL_GPS_FIX, currentTime + GPS_INITIAL_TIME_SETTING_PERIOD); + + /* Update the schedule if successful */ + + if (fixResult == GPS_SUCCESS) { + + /* Reset the flag */ + + setBackupFlag(BACKUP_MUST_SET_TIME_FROM_GPS, false); + + /* Update the GPS location */ + + setBackupFlag(BACKUP_GPS_LOCATION_RECEIVED, true); + + *gpsLatitude = *gpsLastFixLatitude; + + *gpsLongitude = *gpsLastFixLongitude; + + /* Write the configuration file */ + + if (configSettings->enableSunRecording && fileSystemEnabled) { + + AudioMoth_getTime(¤tTime, ¤tMilliseconds); + + bool gpsLocationReceived = getBackupFlag(BACKUP_GPS_LOCATION_RECEIVED); + + bool acousticLocationReceived = getBackupFlag(BACKUP_ACOUSTIC_LOCATION_RECEIVED); + + bool success = writeConfigurationToFile((char*)compressionBuffer, configSettings, currentTime, gpsLocationReceived, gpsLatitude, gpsLongitude, acousticLocationReceived, acousticLatitude, acousticLongitude, firmwareDescription, firmwareVersion, (uint8_t*)AM_UNIQUE_ID_START_ADDRESS, deploymentID, defaultDeploymentID); + + setBackupFlag(BACKUP_WRITTEN_CONFIGURATION_TO_FILE, success); + + } + + /* Schedule the next recording */ + + AudioMoth_getTime(¤tTime, ¤tMilliseconds); + + uint32_t scheduleTime = currentTime + ROUNDED_UP_DIV(currentMilliseconds + *recordingPreparationPeriod, MILLISECONDS_IN_SECOND); + + determineSunriseAndSunsetTimesAndScheduleRecording(scheduleTime); + + if (configSettings->enableLED == false) *timeAtWhichToSwitchOffLED = currentTime + SECONDS_IN_MINUTE; + + } + + /* If time setting was cancelled with the magnet switch then start waiting for the magnetic switch */ + + if (fixResult == GPS_CANCELLED_BY_MAGNETIC_SWITCH) startWaitingForMagneticSwitch(); + + /* Power down */ + + SAVE_SWITCH_POSITION_AND_POWER_DOWN(DEFAULT_WAIT_INTERVAL); + + } + + /* Make a recording */ + + if (timeUntilPreparationStart <= 0) { + + /* Enable energy saver mode */ + + if (isEnergySaverMode(configSettings)) AudioMoth_setClockDivider(AM_HF_CLK_DIV2); + + /* Write configuration if not already done so */ + + if (getBackupFlag(BACKUP_WRITTEN_CONFIGURATION_TO_FILE) == false) { + + if (!fileSystemEnabled) fileSystemEnabled = AudioMoth_enableFileSystem(configSettings->sampleRateDivider == 1 ? AM_SD_CARD_HIGH_SPEED : AM_SD_CARD_NORMAL_SPEED); + + if (fileSystemEnabled) { + + bool gpsLocationReceived = getBackupFlag(BACKUP_GPS_LOCATION_RECEIVED); + + bool acousticLocationReceived = getBackupFlag(BACKUP_ACOUSTIC_LOCATION_RECEIVED); + + bool success = writeConfigurationToFile((char*)compressionBuffer, configSettings, currentTime, gpsLocationReceived, gpsLatitude, gpsLongitude, acousticLocationReceived, acousticLatitude, acousticLongitude, firmwareDescription, firmwareVersion, (uint8_t*)AM_UNIQUE_ID_START_ADDRESS, deploymentID, defaultDeploymentID); + + setBackupFlag(BACKUP_WRITTEN_CONFIGURATION_TO_FILE, success); + + } + + } + + /* Make the recording */ + + uint32_t fileOpenTime; + + uint32_t fileOpenMilliseconds; + + AM_recordingState_t recordingState = RECORDING_OKAY; + + /* Measure battery voltage */ + + uint32_t supplyVoltage = AudioMoth_getSupplyVoltage(); + + AM_extendedBatteryState_t extendedBatteryState = AudioMoth_getExtendedBatteryState(supplyVoltage); + + /* Check if low voltage check is enabled and that the voltage is okay */ + + bool okayToMakeRecording = true; + + if (configSettings->enableLowVoltageCutoff) { + + AudioMoth_enableSupplyMonitor(); + + AudioMoth_setSupplyMonitorThreshold(MINIMUM_SUPPLY_VOLTAGE); + + okayToMakeRecording = AudioMoth_isSupplyAboveThreshold(); + + } + + /* Make recording if okay */ + + if (okayToMakeRecording) { + + AudioMoth_enableTemperature(); + + int32_t temperature = AudioMoth_getTemperature(); + + AudioMoth_disableTemperature(); + + if (!fileSystemEnabled) fileSystemEnabled = AudioMoth_enableFileSystem(configSettings->sampleRateDivider == 1 ? AM_SD_CARD_HIGH_SPEED : AM_SD_CARD_NORMAL_SPEED); + + if (fileSystemEnabled) { + + /* Recording owns the SD card. Do not allow ESP32 file service here. */ + ESPBridge_setUploadAllowed(false); + ESPBridge_setBusy(true); + + recordingState = makeRecording(*timeOfNextRecording, *durationOfNextRecording, *timeAtWhichToSwitchOffLED, extendedBatteryState, temperature, &fileOpenTime, &fileOpenMilliseconds); + + ESPBridge_setBusy(false); + + } else { + + if (enableLED) FLASH_LED(Both, LONG_LED_FLASH_DURATION); + + recordingState = SDCARD_WRITE_ERROR; + + } + + } else { + + recordingState = SUPPLY_VOLTAGE_LOW; + + } + + /* Disable low voltage monitor if it was used */ + + if (configSettings->enableLowVoltageCutoff) AudioMoth_disableSupplyMonitor(); + + /* Enable the error warning flashes */ + + if (recordingState == SUPPLY_VOLTAGE_LOW) { + + AudioMoth_delay(LONG_LED_FLASH_DURATION); + + if (enableLED) FLASH_LED(Both, LONG_LED_FLASH_DURATION); + + *numberOfRecordingErrors += 1; + + *numberOfConsecutiveRecordingErrors += 1; + + } else if (recordingState == SDCARD_WRITE_ERROR) { + + *numberOfRecordingErrors += 1; + + *numberOfConsecutiveRecordingErrors += 1; + + } else { + + *numberOfConsecutiveRecordingErrors = 0; + + } + + /* Update the preparation period */ + + if (recordingState == RECORDING_OKAY) { + + int64_t measuredPreparationPeriod = (int64_t)fileOpenTime * MILLISECONDS_IN_SECOND + (int64_t)fileOpenMilliseconds - (int64_t)currentTime * MILLISECONDS_IN_SECOND - (int64_t)currentMilliseconds; + + measuredPreparationPeriod = MIN(MAXIMUM_PREPARATION_PERIOD, MAX(MINIMUM_PREPARATION_PERIOD, measuredPreparationPeriod)); + + if (*recordingPreparationPeriod == INITIAL_PREPARATION_PERIOD) { + + *recordingPreparationPeriod = measuredPreparationPeriod; + + } else { + + *recordingPreparationPeriod = ROUNDED_DIV(*recordingPreparationPeriod * (PREPARATION_PERIOD_SMOOTHING_FACTOR - 1) + measuredPreparationPeriod, PREPARATION_PERIOD_SMOOTHING_FACTOR); + + } + + } + + /* Update the time and calculate earliest schedule start time */ + + AudioMoth_getTime(¤tTime, ¤tMilliseconds); + + uint32_t scheduleTime = currentTime + ROUNDED_UP_DIV(currentMilliseconds + *recordingPreparationPeriod, MILLISECONDS_IN_SECOND); + + /* Schedule the next recording */ + + if (*numberOfConsecutiveRecordingErrors >= MAX_CONSECUTIVE_RECORDING_ERRORS) { + + /* Cancel the schedule */ + + *indexOfNextRecording = 0; + + *timeOfNextRecording = UINT32_MAX; + + *startOfRecordingPeriod = UINT32_MAX; + + *durationOfNextRecording = UINT32_MAX; + + *timeOfNextGPSTimeSetting = UINT32_MAX; + + } else if (switchPosition == AM_SWITCH_CUSTOM) { + + /* Update schedule time as if the recording has ended correctly */ + + if (recordingState == RECORDING_OKAY || recordingState == SUPPLY_VOLTAGE_LOW || recordingState == SDCARD_WRITE_ERROR) { + + scheduleTime = MAX(scheduleTime, *timeOfNextRecording + *durationOfNextRecording); + + } + + /* Calculate the next recording schedule */ + + determineSunriseAndSunsetTimesAndScheduleRecording(scheduleTime); + + } else { + + /* Set parameters to start recording now */ + + *indexOfNextRecording = 0; + + *timeOfNextRecording = scheduleTime; + + *startOfRecordingPeriod = scheduleTime; + + *durationOfNextRecording = UINT32_MAX; + + *timeOfNextGPSTimeSetting = UINT32_MAX; + + } + + /* If recording was cancelled with the magnetic switch then start waiting for the magnetic switch */ + + if (recordingState == MAGNETIC_SWITCH) { + + startWaitingForMagneticSwitch(); + + SAVE_SWITCH_POSITION_AND_POWER_DOWN(DEFAULT_WAIT_INTERVAL); + + } + + /* Power down with short interval if the next recording is due */ + + if (switchPosition == AM_SWITCH_CUSTOM) { + + calculateTimeToNextEvent(currentTime, currentMilliseconds, &timeUntilPreparationStart, &timeUntilNextGPSTimeSetting); + + if (timeUntilPreparationStart < DEFAULT_WAIT_INTERVAL) { + + setBackupFlag(BACKUP_POWERED_DOWN_WITH_SHORT_WAIT_INTERVAL, true); + + SAVE_SWITCH_POSITION_AND_POWER_DOWN(SHORT_WAIT_INTERVAL); + + } + + } + + /* Power down */ + + SAVE_SWITCH_POSITION_AND_POWER_DOWN(DEFAULT_WAIT_INTERVAL); + + } + + /* Update the time from the GPS */ + + if ((timeUntilNextGPSTimeSetting <= 0 || getBackupFlag(BACKUP_SHOULD_SET_TIME_FROM_GPS)) && timeUntilPreparationStart > GPS_MIN_TIME_SETTING_PERIOD * MILLISECONDS_IN_SECOND) { + + /* Set the time from the GPS */ + + AudioMoth_enableFileSystem(AM_SD_CARD_NORMAL_SPEED); + + AM_gpsFixMode_t fixMode = configSettings->enableTimeSettingBeforeAndAfterRecordings ? GPS_FIX_BEFORE_INDIVIDUAL_RECORDING : GPS_FIX_BEFORE_RECORDING_PERIOD; + + if (getBackupFlag(BACKUP_SHOULD_SET_TIME_FROM_GPS)) fixMode += 1; + + if (timeUntilNextGPSTimeSetting > 0) fixMode += 1; + + uint32_t gpsTimeSettingPeriod = configSettings->gpsTimeSettingPeriod == 0 ? GPS_DEFAULT_TIME_SETTING_PERIOD : configSettings->gpsTimeSettingPeriod * SECONDS_IN_MINUTE; + + uint32_t timeOut = MIN(*timeOfNextRecording - ROUNDED_UP_DIV(*recordingPreparationPeriod, MILLISECONDS_IN_SECOND) - GPS_TIME_SETTING_MARGIN, currentTime + gpsTimeSettingPeriod); + + GPS_fixResult_t fixResult = setTimeFromGPS(enableLED, fixMode, timeOut); + + /* Update flag and next scheduled GPS fix time */ + + if (timeUntilNextGPSTimeSetting <= 0) *timeOfNextGPSTimeSetting = UINT32_MAX; + + setBackupFlag(BACKUP_SHOULD_SET_TIME_FROM_GPS, false); + + /* If time setting was cancelled with the magnet switch then start waiting for the magnetic switch */ + + if (fixResult == GPS_CANCELLED_BY_MAGNETIC_SWITCH) startWaitingForMagneticSwitch(); + + /* Power down */ + + SAVE_SWITCH_POSITION_AND_POWER_DOWN(DEFAULT_WAIT_INTERVAL); + + } + + /* Update the post-recording GPS flag if the previous condition was not met */ + + setBackupFlag(BACKUP_SHOULD_SET_TIME_FROM_GPS, false); + + /* Power down if switch position has changed */ + + if (switchPosition != *previousSwitchPosition) SAVE_SWITCH_POSITION_AND_POWER_DOWN(DEFAULT_WAIT_INTERVAL); + + /* Calculate the wait intervals */ + + int64_t waitIntervalMilliseconds = WAITING_LED_FLASH_INTERVAL; + + uint32_t waitIntervalSeconds = WAITING_LED_FLASH_INTERVAL / MILLISECONDS_IN_SECOND; + + if (getBackupFlag(BACKUP_WAITING_FOR_MAGNETIC_SWITCH)) { + + waitIntervalMilliseconds *= MAGNETIC_SWITCH_WAIT_MULTIPLIER; + + waitIntervalSeconds *= MAGNETIC_SWITCH_WAIT_MULTIPLIER; + + } + + /* Wait for the next event whilst flashing the LED */ + + bool startedRealTimeClock = false; + + while (true) { + + /* Update the time */ + + AudioMoth_getTime(¤tTime, ¤tMilliseconds); + + /* Handle magnetic switch event */ + + bool magneticSwitchEvent = magneticSwitch || (magneticSwitchEnabled && GPS_isMagneticSwitchClosed()); + + if (magneticSwitchEvent) { + + if (getBackupFlag(BACKUP_WAITING_FOR_MAGNETIC_SWITCH)) { + + stopWaitingForMagneticSwitch(¤tTime, ¤tMilliseconds); + + if (switchPosition == AM_SWITCH_CUSTOM) { + + if (configSettings->enableTimeSettingFromGPS) { + + setBackupFlag(BACKUP_MUST_SET_TIME_FROM_GPS, true); + + } else if (configSettings->enableLED == false) { + + *timeAtWhichToSwitchOffLED = currentTime + SECONDS_IN_MINUTE; + + } + + } + + } else { + + startWaitingForMagneticSwitch(); + + } + + SAVE_SWITCH_POSITION_AND_POWER_DOWN(DEFAULT_WAIT_INTERVAL); + + } + + /* Handle switch position change */ + + bool switchEvent = switchPositionChanged || AudioMoth_getSwitchPosition() != *previousSwitchPosition; + + if (switchEvent) SAVE_SWITCH_POSITION_AND_POWER_DOWN(DEFAULT_WAIT_INTERVAL); + + /* Calculate the time to the next event */ + + calculateTimeToNextEvent(currentTime, currentMilliseconds, &timeUntilPreparationStart, &timeUntilNextGPSTimeSetting); + + int64_t timeToEarliestEvent = MIN(timeUntilPreparationStart, timeUntilNextGPSTimeSetting); + + /* ESP32 upload service. + * + * No SD MUX is used: AudioMoth remains the only SD-card master. + * The ESP32 asserts ESP_REQ, waits for MOTH_BUSY to drop, then talks UART. + * Service is refused inside the guard window before preparation/recording. */ + if (switchPosition == AM_SWITCH_CUSTOM && + getBackupFlag(BACKUP_WAITING_FOR_MAGNETIC_SWITCH) == false && + timeUntilPreparationStart > (int64_t)ESPBRIDGE_UPLOAD_GUARD_SECONDS * MILLISECONDS_IN_SECOND) { + + uint32_t bridgeDeadline = *timeOfNextRecording - ESPBRIDGE_UPLOAD_GUARD_SECONDS; + + ESPBridge_setBusy(false); + ESPBridge_setUploadAllowed(true); + + if (ESPBridge_isRequestActive()) { + ESPBridge_serviceUntil(bridgeDeadline); + } + + ESPBridge_setUploadAllowed(false); + ESPBridge_setBusy(true); + + } + + /* Flash LED */ + + enableLED = currentTime < *timeAtWhichToSwitchOffLED; + + bool shouldFlashLED = getBackupFlag(BACKUP_WAITING_FOR_MAGNETIC_SWITCH) || (enableLED && shouldSuppressLED == false && timeToEarliestEvent > MINIMUM_LED_FLASH_INTERVAL); + + if (shouldFlashLED) { + + if (*numberOfRecordingErrors > 0) { + + FLASH_LED(Both, WAITING_LED_FLASH_DURATION); + + } else { + + FLASH_LED(Green, WAITING_LED_FLASH_DURATION); + + } + + } + + /* Check there is time to sleep */ + + if (timeToEarliestEvent < waitIntervalMilliseconds) { + + /* Calculate the remaining time to power down */ + + uint32_t timeToWait = timeToEarliestEvent < 0 ? 0 : timeToEarliestEvent; + + SAVE_SWITCH_POSITION_AND_POWER_DOWN(timeToWait); + + } + + /* Start the real time clock if it isn't running */ + + if (startedRealTimeClock == false) { + + AudioMoth_startRealTimeClock(waitIntervalSeconds); + + startedRealTimeClock = true; + + } + + /* Enter deep sleep */ + + AudioMoth_deepSleep(); + + /* Handle time overflow on awakening */ + + AudioMoth_checkAndHandleTimeOverflow(); + + } + +} + +/* Time zone handler */ + +inline void AudioMoth_timezoneRequested(int32_t *timezoneHours, int32_t *timezoneMinutes) { + + if (configSettings->useTimezoneFromAcousticChime && getBackupFlag(BACKUP_ACOUSTIC_TIMEZONE_RECEIVED)) { + + *timezoneHours = *acousticTimezoneMinutes / MINUTES_IN_HOUR; + + *timezoneMinutes = *acousticTimezoneMinutes % MINUTES_IN_HOUR; + + } else { + + *timezoneHours = configSettings->timezoneHours; + + *timezoneMinutes = configSettings->timezoneMinutes; + + } + +} + +/* GPS time handlers */ + +inline void GPS_handleSetTime(uint32_t time, uint32_t milliseconds, int64_t timeDifference, uint32_t measuredClockFrequency) { + + /* Update the time if appropriate */ + + timeSetFromGPS = time; + + if (!AudioMoth_hasTimeBeenSet()) { + + AudioMoth_setTime(time, milliseconds); + + writeGPSLogMessage((char*)compressionBuffer, time, milliseconds, "Time was set from GPS."); + + } else { + + int64_t maximumTimeDifference = (int64_t)GPS_MAXIMUM_DIFFERENCE * (int64_t)MICROSECONDS_IN_SECOND; + + if (timeDifference == 0) { + + writeGPSLogMessage((char*)compressionBuffer, time, milliseconds, "Time was not updated. The internal clock was correct."); + + } else if (timeDifference < -maximumTimeDifference || timeDifference > maximumTimeDifference) { + + writeGPSLogMessage((char*)compressionBuffer, time, milliseconds, "Time was not updated. The discrepancy between the internal clock and the GPS was too large."); + + } else { + + AudioMoth_setTime(time, milliseconds); + + int32_t scaledTimeDifference = (int32_t)ROUNDED_DIV(timeDifference, MICROSECONDS_IN_MILLISECOND / 10); + + bool isFast = scaledTimeDifference > 0; + + uint32_t units = ABS(scaledTimeDifference) / 10; + + uint32_t tenths = ABS(scaledTimeDifference) % 10; + + sprintf(gpsMessageBuffer, "Time was updated. The internal clock was %ld.%ldms %s.", units, tenths, isFast ? "fast" : "slow"); + + writeGPSLogMessage((char*)compressionBuffer, time, milliseconds, gpsMessageBuffer); + + } + + } + + /* Calculate the actual sampling rate */ + + uint32_t intendedClockFrequency = AudioMoth_getClockFrequency(); + + uint32_t intendedSamplingRate = configSettings->sampleRate / configSettings->sampleRateDivider; + + uint32_t clockTicksPerSample = intendedClockFrequency / intendedSamplingRate; + + uint64_t actualSamplingRate = ROUNDED_DIV(GPS_FREQUENCY_PRECISION * (uint64_t)measuredClockFrequency, (uint64_t)clockTicksPerSample); + + uint32_t integerPart = actualSamplingRate / GPS_FREQUENCY_PRECISION; + + uint32_t fractionalPart = actualSamplingRate % GPS_FREQUENCY_PRECISION; + + sprintf(gpsMessageBuffer, "Actual sample rate will be %lu.%03lu Hz.", integerPart, fractionalPart); + + writeGPSLogMessage((char*)compressionBuffer, time, milliseconds, gpsMessageBuffer); + +} + +inline void GPS_handleGetTime(uint32_t *time, uint32_t *milliseconds) { + + AudioMoth_getTime(time, milliseconds); + +} + +inline void GPS_handleGetTimeMicroseconds(uint32_t *time, uint32_t *microseconds) { + + AudioMoth_getTimeMicroseconds(time, microseconds); + +} + +/* GPS format conversion */ + +static int32_t convertToDecimalDegrees(uint32_t degrees, uint32_t minutes, uint32_t tenThousandths, char direction) { + + int32_t value = degrees * GPS_LOCATION_PRECISION; + + value += ROUNDED_DIV(minutes * GPS_LOCATION_PRECISION, MINUTES_IN_DEGREE); + + value += ROUNDED_DIV(tenThousandths * (GPS_LOCATION_PRECISION / 10000), MINUTES_IN_DEGREE); + + if (direction == 'S' || direction == 'W') value *= -1; + + return value; + +} + +/* GPS interrupt handlers */ + +inline void GPS_handleTickEvent() { + + if (gpsTickEventCount == 0) { + + gpsTickEventModulo = GPS_TICK_EVENTS_PER_SECOND; + + if (gpsPPSEvent || gpsFixEvent) { + + gpsTickEventModulo /= 3; + + } else if (gpsMessageEvent) { + + gpsTickEventModulo /= 2; + + } + + gpsMessageEvent = false; + + gpsFixEvent = false; + + gpsPPSEvent = false; + + } + + if (gpsEnableLED) AudioMoth_setRedLED(gpsTickEventCount % gpsTickEventModulo == 0); + + gpsTickEventCount = (gpsTickEventCount + 1) % GPS_TICK_EVENTS_PER_SECOND; + +} + +inline void GPS_handlePPSEvent(uint32_t time, uint32_t milliseconds) { + + writeGPSLogMessage((char*)compressionBuffer, time, milliseconds, "Received pulse per second signal."); + + gpsPPSEvent = true; + +} + +inline void GPS_handleFixEvent(uint32_t time, uint32_t milliseconds, GPS_fixTime_t *fixTime, GPS_fixPosition_t *fixPosition, char *message) { + + *gpsLastFixLatitude = convertToDecimalDegrees(fixPosition->latitudeDegrees, fixPosition->latitudeMinutes, fixPosition->latitudeTenThousandths, fixPosition->latitudeDirection); + + *gpsLastFixLongitude = convertToDecimalDegrees(fixPosition->longitudeDegrees, fixPosition->longitudeMinutes, fixPosition->longitudeTenThousandths, fixPosition->longitudeDirection); + + uint32_t length = sprintf(gpsMessageBuffer, "Received GPS fix - %ld.%06ld°%c %ld.%06ld°%c ", ABS(*gpsLastFixLatitude) / GPS_LOCATION_PRECISION, ABS(*gpsLastFixLatitude) % GPS_LOCATION_PRECISION, fixPosition->latitudeDirection, ABS(*gpsLastFixLongitude) / GPS_LOCATION_PRECISION, ABS(*gpsLastFixLongitude) % GPS_LOCATION_PRECISION, fixPosition->longitudeDirection); + + sprintf(gpsMessageBuffer + length, "at %02u/%02u/%04u %02u:%02u:%02u.%03u UTC.", fixTime->day, fixTime->month, fixTime->year, fixTime->hours, fixTime->minutes, fixTime->seconds, fixTime->milliseconds); + + writeGPSLogMessage((char*)compressionBuffer, time, milliseconds, gpsMessageBuffer); + + gpsFixEvent = true; + +} + +inline void GPS_handleMessageEvent(uint32_t time, uint32_t milliseconds, char *message) { + + if (!gpsFirstMessageReceived) { + + writeGPSLogMessage((char*)compressionBuffer, time, milliseconds, "Received first GPS message."); + + gpsFirstMessageReceived = true; + + } + + gpsMessageEvent = true; + +} + +inline void GPS_handleMagneticSwitchInterrupt() { + + magneticSwitch = true; + + GPS_cancelTimeSetting(GPS_CANCEL_BY_MAGNETIC_SWITCH); + +} + +/* AudioMoth interrupt handlers */ + +inline void AudioMoth_handleMicrophoneChangeInterrupt() { + + microphoneChanged = true; + + AudioConfig_handleMicrophoneChange(); + +} + +inline void AudioMoth_handleSwitchInterrupt() { + + switchPositionChanged = true; + + AudioConfig_cancelAudioConfiguration(); + + GPS_cancelTimeSetting(GPS_CANCEL_BY_SWITCH); + +} + +inline void AudioMoth_handleDirectMemoryAccessInterrupt(bool isPrimaryBuffer, int16_t **nextBuffer) { + + int16_t *source = secondaryBuffer; + + if (isPrimaryBuffer) source = primaryBuffer; + + /* Apply filter to samples */ + + bool thresholdExceeded = DigitalFilter_applyFilter(source, buffers[writeBuffer] + writeBufferIndex, configSettings->sampleRateDivider, numberOfRawSamplesInDMATransfer); + + numberOfDMATransfers += 1; + + /* Update the current buffer index and write buffer if wait period is over */ + + if (numberOfDMATransfers > numberOfDMATransfersToWait) { + + writeIndicator[writeBuffer] |= thresholdExceeded; + + writeBufferIndex += numberOfRawSamplesInDMATransfer / configSettings->sampleRateDivider; + + if (writeBufferIndex == NUMBER_OF_SAMPLES_IN_BUFFER) { + + writeBufferIndex = 0; + + writeBuffer = (writeBuffer + 1) & (NUMBER_OF_BUFFERS - 1); + + writeIndicator[writeBuffer] = false; + + } + + } else if (numberOfDMATransfers == numberOfDMATransfersToWait) { + + AudioMoth_setRedLED(false); + + } else { + + bool state = numberOfDMATransfers % PREPARATION_PERIOD_LED_DIVIDER == 0; + + if (enableLED) AudioMoth_setRedLED(state); + + } + +} + +/* AudioMoth USB message handlers */ + +inline void AudioMoth_usbFirmwareVersionRequested(uint8_t **firmwareVersionPtr) { + + *firmwareVersionPtr = firmwareVersion; + +} + +inline void AudioMoth_usbFirmwareDescriptionRequested(uint8_t **firmwareDescriptionPtr) { + + *firmwareDescriptionPtr = firmwareDescription; + +} + +inline void AudioMoth_usbApplicationPacketRequested(uint32_t messageType, uint8_t *transmitBuffer, uint32_t size) { + + /* Copy the current time to the USB packet */ + + uint32_t currentTime; + + AudioMoth_getTime(¤tTime, NULL); + + memcpy(transmitBuffer + 1, ¤tTime, UINT32_SIZE_IN_BYTES); + + /* Copy the unique ID to the USB packet */ + + memcpy(transmitBuffer + 5, (uint8_t*)AM_UNIQUE_ID_START_ADDRESS, AM_UNIQUE_ID_SIZE_IN_BYTES); + + /* Copy the battery state to the USB packet */ + + uint32_t supplyVoltage = AudioMoth_getSupplyVoltage(); + + AM_batteryState_t batteryState = AudioMoth_getBatteryState(supplyVoltage); + + memcpy(transmitBuffer + 5 + AM_UNIQUE_ID_SIZE_IN_BYTES, &batteryState, 1); + + /* Copy the firmware version to the USB packet */ + + memcpy(transmitBuffer + 6 + AM_UNIQUE_ID_SIZE_IN_BYTES, firmwareVersion, AM_FIRMWARE_VERSION_LENGTH); + + /* Copy the firmware description to the USB packet */ + + memcpy(transmitBuffer + 6 + AM_UNIQUE_ID_SIZE_IN_BYTES + AM_FIRMWARE_VERSION_LENGTH, firmwareDescription, AM_FIRMWARE_DESCRIPTION_LENGTH); + +} + +inline void AudioMoth_usbApplicationPacketReceived(uint32_t messageType, uint8_t* receiveBuffer, uint8_t *transmitBuffer, uint32_t size) { + + /* Make persistent configuration settings data structure */ + + memcpy(&tempPersistentConfigSettings->firmwareVersion, &firmwareVersion, AM_FIRMWARE_VERSION_LENGTH); + + memcpy(&tempPersistentConfigSettings->firmwareDescription, &firmwareDescription, AM_FIRMWARE_DESCRIPTION_LENGTH); + + memcpy(&tempPersistentConfigSettings->configSettings, receiveBuffer + 1, sizeof(configSettings_t)); + + /* Implement energy saver mode changes */ + + if (isEnergySaverMode(&tempPersistentConfigSettings->configSettings)) { + + tempPersistentConfigSettings->configSettings.sampleRate /= 2; + tempPersistentConfigSettings->configSettings.clockDivider /= 2; + tempPersistentConfigSettings->configSettings.sampleRateDivider /= 2; + + } + + /* Copy persistent configuration settings to flash */ + + uint32_t numberOfBytes = ROUND_UP_TO_MULTIPLE(sizeof(persistentConfigSettings_t), UINT32_SIZE_IN_BYTES); + + bool success = AudioMoth_writeToFlashUserDataPage((uint8_t*)tempPersistentConfigSettings, numberOfBytes); + + if (success) { + + /* Copy the persistent configuration to the USB packet */ + + memcpy(transmitBuffer + 1, configSettings, sizeof(configSettings_t)); + + /* Revert energy saver mode changes */ + + configSettings_t *tempConfigSettings = (configSettings_t*)(transmitBuffer + 1); + + if (isEnergySaverMode(tempConfigSettings)) { + + tempConfigSettings->sampleRate *= 2; + tempConfigSettings->clockDivider *= 2; + tempConfigSettings->sampleRateDivider *= 2; + + } + + /* Set the time */ + + AudioMoth_setTime(configSettings->time, USB_CONFIG_TIME_CORRECTION); + + /* Blink the green LED */ + + AudioMoth_blinkDuringUSB(USB_CONFIGURATION_BLINK); + + } else { + + /* Return blank configuration as error indicator */ + + memset(transmitBuffer + 1, 0, sizeof(configSettings_t)); + + } + +} + +/* Audio configuration handlers */ + +inline void AudioConfig_handleAudioConfigurationEvent(AC_audioConfigurationEvent_t event) { + + if (event == AC_EVENT_PULSE) { + + audioConfigPulseCounter = (audioConfigPulseCounter + 1) % AUDIO_CONFIG_PULSE_INTERVAL; + + } else if (event == AC_EVENT_START) { + + audioConfigStateLED = true; + + audioConfigToggleLED = true; + + AudioMoth_getTime(&secondsOfAcousticSignalStart, &millisecondsOfAcousticSignalStart); + + } else if (event == AC_EVENT_BYTE) { + + audioConfigToggleLED = !audioConfigToggleLED; + + } else if (event == AC_EVENT_BIT_ERROR || event == AC_EVENT_CRC_ERROR) { + + audioConfigStateLED = false; + + } + + AudioMoth_setGreenLED((audioConfigStateLED && audioConfigToggleLED) || (!audioConfigStateLED && !audioConfigPulseCounter)); + +} + +inline void AudioConfig_handleAudioConfigurationPacket(uint8_t *receiveBuffer, uint32_t size) { + + uint32_t standardPacketSize = UINT32_SIZE_IN_BYTES + UINT16_SIZE_IN_BYTES; + + bool standardPacket = size == standardPacketSize; + + bool hasLocation = size == (standardPacketSize + ACOUSTIC_LOCATION_SIZE_IN_BYTES) || size == (standardPacketSize + DEPLOYMENT_ID_LENGTH + ACOUSTIC_LOCATION_SIZE_IN_BYTES); + + bool hasDeploymentID = size == (standardPacketSize + DEPLOYMENT_ID_LENGTH) || size == (standardPacketSize + DEPLOYMENT_ID_LENGTH + ACOUSTIC_LOCATION_SIZE_IN_BYTES); + + if (configSettings->requireAcousticLocation && hasLocation == false) { + + audioConfigStateLED = false; + + return; + + } + + if (standardPacket || hasLocation || hasDeploymentID) { + + /* Copy time from the packet */ + + uint32_t time; + + memcpy(&time, receiveBuffer, UINT32_SIZE_IN_BYTES); + + /* Calculate the time correction */ + + uint32_t secondsOfAcousticSignalEnd; + + uint32_t millisecondsOfAcousticSignalEnd; + + AudioMoth_getTime(&secondsOfAcousticSignalEnd, &millisecondsOfAcousticSignalEnd); + + uint32_t millisecondTimeOffset = (secondsOfAcousticSignalEnd - secondsOfAcousticSignalStart) * MILLISECONDS_IN_SECOND + millisecondsOfAcousticSignalEnd - millisecondsOfAcousticSignalStart + AUDIO_CONFIG_TIME_CORRECTION; + + /* Set the time */ + + AudioMoth_setTime(time + millisecondTimeOffset / MILLISECONDS_IN_SECOND, millisecondTimeOffset % MILLISECONDS_IN_SECOND); + + /* Set the timezone */ + + *acousticTimezoneMinutes = *(int16_t*)(receiveBuffer + UINT32_SIZE_IN_BYTES); + + setBackupFlag(BACKUP_ACOUSTIC_TIMEZONE_RECEIVED, true); + + /* Set acoustic location */ + + if (hasLocation) { + + acousticLocation_t location; + + memcpy(&location, receiveBuffer + standardPacketSize, ACOUSTIC_LOCATION_SIZE_IN_BYTES); + + setBackupFlag(BACKUP_ACOUSTIC_LOCATION_RECEIVED, true); + + *acousticLatitude = location.latitude; + + *acousticLongitude = location.longitude * ACOUSTIC_LONGITUDE_MULTIPLIER; + + } + + /* Set deployment ID */ + + if (hasDeploymentID) { + + copyToBackupDomain((uint32_t*)deploymentID, receiveBuffer + standardPacketSize + (hasLocation ? ACOUSTIC_LOCATION_SIZE_IN_BYTES : 0), DEPLOYMENT_ID_LENGTH); + + } + + /* Indicate success */ + + AudioConfig_cancelAudioConfiguration(); + + acousticConfigurationPerformed = true; + + } + + /* Reset receive state */ + + audioConfigStateLED = false; + +} + +/* Clear and encode the compression buffer */ + +static void clearCompressionBuffer() { + + for (uint32_t i = 0; i < COMPRESSION_BUFFER_SIZE_IN_BYTES / NUMBER_OF_BYTES_IN_SAMPLE; i += 1) { + + compressionBuffer[i] = 0; + + } + +} + +static void encodeCompressionBuffer(uint32_t numberOfCompressedBuffers) { + + for (uint32_t i = 0; i < UINT32_SIZE_IN_BITS; i += 1) { + + compressionBuffer[i] = numberOfCompressedBuffers & 0x01 ? 1 : -1; + + numberOfCompressedBuffers >>= 1; + + } + + for (uint32_t i = UINT32_SIZE_IN_BITS; i < COMPRESSION_BUFFER_SIZE_IN_BYTES / NUMBER_OF_BYTES_IN_SAMPLE; i += 1) { + + compressionBuffer[i] = 0; + + } + +} + +/* Generate foldername and filename from time */ + +static void generateFolderAndFilename(char *foldername, char *filename, uint32_t timestamp, bool triggeredRecording) { + + struct tm time; + + int32_t timezoneHours, timezoneMinutes; + + AudioMoth_timezoneRequested(&timezoneHours, &timezoneMinutes); + + time_t rawTime = timestamp + timezoneHours * SECONDS_IN_HOUR + timezoneMinutes * SECONDS_IN_MINUTE; + + gmtime_r(&rawTime, &time); + + sprintf(foldername, "%04d%02d%02d", YEAR_OFFSET + time.tm_year, MONTH_OFFSET + time.tm_mon, time.tm_mday); + + uint32_t length = 0; + + if (configSettings->enableDailyFolders) { + + length += sprintf(filename, "%s/", foldername); + + } + + if (configSettings->enableFilenameWithDeviceID) { + + uint8_t *source = memcmp(deploymentID, defaultDeploymentID, DEPLOYMENT_ID_LENGTH) ? deploymentID : (uint8_t*)AM_UNIQUE_ID_START_ADDRESS; + + length += sprintf(filename + length, SERIAL_NUMBER "_", FORMAT_SERIAL_NUMBER(source)); + + } + + length += sprintf(filename + length, "%s_%02d%02d%02d", foldername, time.tm_hour, time.tm_min, time.tm_sec); + + char *extension = triggeredRecording ? "T.WAV" : ".WAV"; + + strcpy(filename + length, extension); + +} + +/* Save recording to SD card */ + +static AM_recordingState_t makeRecording(uint32_t timeOfNextRecording, uint32_t recordDuration, uint32_t timeAtWhichToSwitchOffLED, AM_extendedBatteryState_t extendedBatteryState, int32_t temperature, uint32_t *fileOpenTime, uint32_t *fileOpenMilliseconds) { + + /* Initialise buffers */ + + writeBuffer = 0; + + writeBufferIndex = 0; + + buffers[0] = (int16_t*)AM_EXTERNAL_SRAM_START_ADDRESS; + + for (uint32_t i = 1; i < NUMBER_OF_BUFFERS; i += 1) { + buffers[i] = buffers[i - 1] + NUMBER_OF_SAMPLES_IN_BUFFER; + } + + /* Calculate effective sample rate */ + + uint32_t effectiveSampleRate = configSettings->sampleRate / configSettings->sampleRateDivider; + + /* Set up the digital filter */ + + uint32_t blockingFilterFrequency = configSettings->disable48HzDCBlockingFilter ? LOW_DC_BLOCKING_FREQ : DEFAULT_DC_BLOCKING_FREQ; + + if (configSettings->lowerFilterFreq == 0 && configSettings->higherFilterFreq == 0) { + + requestedFilterType = NO_FILTER; + + DigitalFilter_designHighPassFilter(effectiveSampleRate, blockingFilterFrequency); + + } else if (configSettings->lowerFilterFreq == UINT16_MAX) { + + requestedFilterType = LOW_PASS_FILTER; + + DigitalFilter_designBandPassFilter(effectiveSampleRate, blockingFilterFrequency, FILTER_FREQ_MULTIPLIER * configSettings->higherFilterFreq); + + } else if (configSettings->higherFilterFreq == UINT16_MAX) { + + requestedFilterType = HIGH_PASS_FILTER; + + DigitalFilter_designHighPassFilter(effectiveSampleRate, MAX(blockingFilterFrequency, FILTER_FREQ_MULTIPLIER * configSettings->lowerFilterFreq)); + + } else { + + requestedFilterType = BAND_PASS_FILTER; + + DigitalFilter_designBandPassFilter(effectiveSampleRate, MAX(blockingFilterFrequency, FILTER_FREQ_MULTIPLIER * configSettings->lowerFilterFreq), FILTER_FREQ_MULTIPLIER * configSettings->higherFilterFreq); + + } + + /* Calculate the number of samples in each DMA transfer (while ensuring that number of samples written to the SRAM buffer on each DMA transfer is a power of two so each SRAM buffer is filled after an integer number of DMA transfers) */ + + numberOfRawSamplesInDMATransfer = MAXIMUM_SAMPLES_IN_DMA_TRANSFER / configSettings->sampleRateDivider; + + while (numberOfRawSamplesInDMATransfer & (numberOfRawSamplesInDMATransfer - 1)) { + + numberOfRawSamplesInDMATransfer = numberOfRawSamplesInDMATransfer & (numberOfRawSamplesInDMATransfer - 1); + + } + + numberOfRawSamplesInDMATransfer *= configSettings->sampleRateDivider; + + /* Calculate the minimum amplitude threshold duration */ + + uint32_t minimumNumberOfTriggeredBuffersToWrite = ROUNDED_UP_DIV(configSettings->minimumTriggerDuration * effectiveSampleRate, NUMBER_OF_SAMPLES_IN_BUFFER); + + /* Calculate LED flash timescale when no triggered buffers are detected */ + + uint32_t triggeredRecordingFlashInterval = MAX(1, ROUNDED_DIV(effectiveSampleRate, NUMBER_OF_SAMPLES_IN_BUFFER)); + + /* Initialise termination conditions */ + + microphoneChanged = false; + + bool supplyVoltageLow = false; + + /* Enable the external SRAM and microphone and initialise direct memory access */ + + AudioMoth_enableExternalSRAM(); + + AudioMoth_ignoreExternalMicrophone(false); + + AM_gainRange_t gainRange = configSettings->enableLowGainRange ? AM_LOW_GAIN_RANGE : AM_NORMAL_GAIN_RANGE; + + AM_externalMicrophone_t externalMicrophone = AudioMoth_enableMicrophone(gainRange, configSettings->gain, configSettings->clockDivider, configSettings->acquisitionCycles, configSettings->oversampleRate); + + AudioMoth_initialiseDirectMemoryAccess(primaryBuffer, secondaryBuffer, numberOfRawSamplesInDMATransfer); + + /* Calculate the sample multiplier */ + + float sampleMultiplier = 16.0f / (float)(configSettings->oversampleRate * configSettings->sampleRateDivider); + + if (AudioMoth_hasInvertedOutput()) sampleMultiplier = -sampleMultiplier; + + if (externalMicrophone == AM_EXTERNAL_PRESENT_AND_USED) sampleMultiplier = -sampleMultiplier; + + DigitalFilter_setAdditionalGain(sampleMultiplier); + + /* Determine if amplitude threshold is enabled */ + + bool frequencyTriggerEnabled = configSettings->enableFrequencyTrigger; + + bool amplitudeThresholdEnabled = frequencyTriggerEnabled ? false : configSettings->amplitudeThreshold > 0 || configSettings->enableAmplitudeThresholdDecibelScale || configSettings->enableAmplitudeThresholdPercentageScale; + + /* Configure the digital filter for the appropriate trigger */ + + if (frequencyTriggerEnabled) { + + uint32_t frequency = MIN(effectiveSampleRate / 2, FILTER_FREQ_MULTIPLIER * configSettings->frequencyTriggerCentreFrequency); + + uint32_t windowLength = MIN(FREQUENCY_TRIGGER_WINDOW_MAXIMUM, MAX(FREQUENCY_TRIGGER_WINDOW_MINIMUM, 1 << configSettings->frequencyTriggerWindowLengthShift)); + + float percentageThreshold = (float)configSettings->frequencyTriggerThresholdPercentageMantissa * powf(10.0f, (float)configSettings->frequencyTriggerThresholdPercentageExponent); + + DigitalFilter_setFrequencyTrigger(windowLength, effectiveSampleRate, frequency, percentageThreshold); + + } + + if (amplitudeThresholdEnabled) { + + DigitalFilter_setAmplitudeThreshold(configSettings->amplitudeThreshold); + + } + + /* Set the initial header comment details */ + + AM_recordingState_t recordingState = SDCARD_WRITE_ERROR; + + setHeaderDetails(&wavHeader, effectiveSampleRate, 0, 0); + + setHeaderComment(&wavHeader, configSettings, timeOfNextRecording, (uint8_t*)AM_UNIQUE_ID_START_ADDRESS, deploymentID, defaultDeploymentID, extendedBatteryState, temperature, externalMicrophone == AM_EXTERNAL_PRESENT_AND_USED, recordingState, requestedFilterType); + + /* Show LED for SD card activity */ + + if (enableLED) AudioMoth_setRedLED(true); + + /* Open a file with the current local time as the name */ + + static char filename[MAXIMUM_FILE_NAME_LENGTH]; + + static char foldername[MAXIMUM_FILE_NAME_LENGTH]; + + generateFolderAndFilename(foldername, filename, timeOfNextRecording, frequencyTriggerEnabled || amplitudeThresholdEnabled); + + if (configSettings->enableDailyFolders) { + + bool directoryExists = AudioMoth_doesDirectoryExist(foldername); + + if (directoryExists == false) FLASH_LED_IF_ENABLED_AND_RETURN_ON_ERROR(AudioMoth_makeDirectory(foldername)); + + } + + FLASH_LED_IF_ENABLED_AND_RETURN_ON_ERROR(AudioMoth_openFile(filename)); + + /* Write the header */ + + FLASH_LED_IF_ENABLED_AND_RETURN_ON_ERROR(AudioMoth_writeToFile(&wavHeader, sizeof(wavHeader_t))); + + FLASH_LED_IF_ENABLED_AND_RETURN_ON_ERROR(AudioMoth_syncFile()); + + FLASH_LED_IF_ENABLED_AND_RETURN_ON_ERROR(AudioMoth_seekInFile(0)); + + AudioMoth_setRedLED(false); + + /* Measure the time difference from the start time */ + + uint32_t fileOpenMicroseconds; + + AudioMoth_getTimeMicroseconds(fileOpenTime, &fileOpenMicroseconds); + + *fileOpenMilliseconds = ROUNDED_DIV(fileOpenMicroseconds, MICROSECONDS_IN_MILLISECOND); + + /* Calculate time correction for sample rate due to file header */ + + uint32_t numberOfSamplesInHeader = sizeof(wavHeader_t) / NUMBER_OF_BYTES_IN_SAMPLE; + + uint32_t sampleRateTimeOffset = ROUNDED_DIV(numberOfSamplesInHeader * MICROSECONDS_IN_SECOND, effectiveSampleRate); + + /* Calculate time until the recording should start */ + + int32_t microsecondsUntilRecordingShouldStart = (int64_t)timeOfNextRecording * MICROSECONDS_IN_SECOND - (int64_t)*fileOpenTime * MICROSECONDS_IN_SECOND - (int64_t)fileOpenMicroseconds - (int64_t)sampleRateTimeOffset; + + /* Calculate the actual recording start time if the intended start has been missed */ + + uint32_t timeOffset = microsecondsUntilRecordingShouldStart < 0 ? 1 - microsecondsUntilRecordingShouldStart / MICROSECONDS_IN_SECOND : 0; + + recordDuration = timeOffset >= recordDuration ? 0 : recordDuration - timeOffset; + + microsecondsUntilRecordingShouldStart += (int64_t)timeOffset * MICROSECONDS_IN_SECOND; + + uint32_t actualRecordingStartTime = timeOfNextRecording + timeOffset; + + /* Calculate the period to wait before starting the DMA transfers */ + + uint64_t numberOfRawSamplesToWait = ROUNDED_DIV((uint64_t)microsecondsUntilRecordingShouldStart * (uint64_t)configSettings->sampleRate, MICROSECONDS_IN_SECOND); + + numberOfDMATransfersToWait = (uint32_t)(numberOfRawSamplesToWait / (uint64_t)numberOfRawSamplesInDMATransfer); + + uint32_t remainingNumberOfRawSamples = (uint32_t)(numberOfRawSamplesToWait % (uint64_t)numberOfRawSamplesInDMATransfer); + + uint32_t remainingMicrosecondsToWait = ROUNDED_DIV(remainingNumberOfRawSamples * MICROSECONDS_IN_SECOND, configSettings->sampleRate); + + /* Calculate updated recording parameters */ + + uint32_t maximumNumberOfSeconds = MAXIMUM_WAV_DATA_SIZE / NUMBER_OF_BYTES_IN_SAMPLE / effectiveSampleRate; + + bool fileSizeLimited = (recordDuration > maximumNumberOfSeconds); + + uint32_t numberOfSamples = effectiveSampleRate * (fileSizeLimited ? maximumNumberOfSeconds : recordDuration); + + /* Calculate the number of samples at which LED should be turned off */ + + uint32_t numberOfSamplesToSwitchOffLED = UINT32_MAX; + + if (enableLED && timeAtWhichToSwitchOffLED < UINT32_MAX) { + + if (timeAtWhichToSwitchOffLED < actualRecordingStartTime) { + + enableLED = false; + + } else if (timeAtWhichToSwitchOffLED < actualRecordingStartTime + recordDuration) { + + numberOfSamplesToSwitchOffLED = numberOfSamplesInHeader + effectiveSampleRate * (timeAtWhichToSwitchOffLED - actualRecordingStartTime); + + } + + } + + /* Initialise main loop variables */ + + uint32_t readBuffer = 0; + + uint32_t samplesWritten = 0; + + uint32_t buffersProcessed = 0; + + uint32_t numberOfCompressedBuffers = 0; + + uint32_t totalNumberOfCompressedSamples = 0; + + uint32_t numberOfTriggeredBuffersWritten = 0; + + uint32_t numberOfBuffersProcessedSinceLastWrite = 0; + + bool triggerHasOccurred = false; + + /* Start processing DMA transfers */ + + numberOfDMATransfers = 0; + + if (remainingMicrosecondsToWait > DELAY_CORRECTION_MICROSECONDS) { + + remainingMicrosecondsToWait -= DELAY_CORRECTION_MICROSECONDS; + + AudioMoth_delayMicroseconds(remainingMicrosecondsToWait); + + } + + AudioMoth_startMicrophoneSamples(configSettings->sampleRate); + + /* Main recording loop */ + + while (samplesWritten < numberOfSamples + numberOfSamplesInHeader && !microphoneChanged && !switchPositionChanged && !magneticSwitch && !supplyVoltageLow) { + + while (readBuffer != writeBuffer && samplesWritten < numberOfSamples + numberOfSamplesInHeader && !microphoneChanged && !switchPositionChanged && !magneticSwitch && !supplyVoltageLow) { + + /* Determine the appropriate number of bytes to the SD card */ + + uint32_t numberOfSamplesToWrite = MIN(numberOfSamples + numberOfSamplesInHeader - samplesWritten, NUMBER_OF_SAMPLES_IN_BUFFER); + + /* Check if this buffer should actually be written to the SD card */ + + bool writeIndicated = (amplitudeThresholdEnabled == false && frequencyTriggerEnabled == false) || writeIndicator[readBuffer]; + + if (frequencyTriggerEnabled && configSettings->sampleRateDivider > 1) writeIndicated = DigitalFilter_applyFrequencyTrigger(buffers[readBuffer], NUMBER_OF_SAMPLES_IN_BUFFER); + + /* Ensure the minimum number of buffers will be written */ + + triggerHasOccurred |= writeIndicated; + + numberOfTriggeredBuffersWritten = writeIndicated ? 0 : numberOfTriggeredBuffersWritten + 1; + + bool shouldWriteThisSector = writeIndicated || (triggerHasOccurred && numberOfTriggeredBuffersWritten < minimumNumberOfTriggeredBuffersToWrite); + + /* Compress the buffer or write the buffer to SD card */ + + if (shouldWriteThisSector == false && buffersProcessed > 0 && numberOfSamplesToWrite == NUMBER_OF_SAMPLES_IN_BUFFER) { + + /* Increment the number of compressed buffers */ + + numberOfCompressedBuffers += NUMBER_OF_BYTES_IN_SAMPLE * NUMBER_OF_SAMPLES_IN_BUFFER / COMPRESSION_BUFFER_SIZE_IN_BYTES; + + /* Show LED at appropriate interval if no SD card writing has occurred */ + + numberOfBuffersProcessedSinceLastWrite += 1; + + if (numberOfBuffersProcessedSinceLastWrite % triggeredRecordingFlashInterval == 0) { + + if (enableLED) AudioMoth_setRedLED(true); + + AudioMoth_delay(TRIGGERED_RECORDING_FLASH_DURATION); + + AudioMoth_setRedLED(false); + + } + + } else { + + /* Light LED during SD card write if appropriate */ + + if (enableLED) AudioMoth_setRedLED(true); + + /* Encode and write compression buffer */ + + if (numberOfCompressedBuffers > 0) { + + encodeCompressionBuffer(numberOfCompressedBuffers); + + totalNumberOfCompressedSamples += (numberOfCompressedBuffers - 1) * COMPRESSION_BUFFER_SIZE_IN_BYTES / NUMBER_OF_BYTES_IN_SAMPLE; + + FLASH_LED_IF_ENABLED_AND_RETURN_ON_ERROR(AudioMoth_writeToFile(compressionBuffer, COMPRESSION_BUFFER_SIZE_IN_BYTES)); + + numberOfCompressedBuffers = 0; + + } + + /* Either write the buffer or write a blank buffer */ + + if (shouldWriteThisSector) { + + if (buffersProcessed == 0) memcpy(buffers[readBuffer], &wavHeader, sizeof(wavHeader_t)); + + FLASH_LED_IF_ENABLED_AND_RETURN_ON_ERROR(AudioMoth_writeToFile(buffers[readBuffer], NUMBER_OF_BYTES_IN_SAMPLE * numberOfSamplesToWrite)); + + } else { + + clearCompressionBuffer(); + + uint32_t blankBuffersWritten = 0; + + uint32_t numberOfBlankSamplesToWrite = numberOfSamplesToWrite; + + while (numberOfBlankSamplesToWrite > 0) { + + uint32_t numberOfSamples = MIN(numberOfBlankSamplesToWrite, COMPRESSION_BUFFER_SIZE_IN_BYTES / NUMBER_OF_BYTES_IN_SAMPLE); + + bool writeHeader = buffersProcessed == 0 && blankBuffersWritten == 0 && numberOfSamples >= sizeof(wavHeader_t) / NUMBER_OF_BYTES_IN_SAMPLE; + + if (writeHeader) memcpy(compressionBuffer, &wavHeader, sizeof(wavHeader_t)); + + FLASH_LED_IF_ENABLED_AND_RETURN_ON_ERROR(AudioMoth_writeToFile(compressionBuffer, NUMBER_OF_BYTES_IN_SAMPLE * numberOfSamples)); + + if (writeHeader) memset(compressionBuffer, 0, sizeof(wavHeader_t)); + + numberOfBlankSamplesToWrite -= numberOfSamples; + + blankBuffersWritten += 1; + + } + + } + + /* Clear LED */ + + AudioMoth_setRedLED(false); + + /* Reset the buffer counter */ + + numberOfBuffersProcessedSinceLastWrite = 0; + + } + + /* Increment buffer counters */ + + readBuffer = (readBuffer + 1) & (NUMBER_OF_BUFFERS - 1); + + samplesWritten += numberOfSamplesToWrite; + + buffersProcessed += 1; + + } + + /* Check the voltage level */ + + if (configSettings->enableLowVoltageCutoff && AudioMoth_isSupplyAboveThreshold() == false) { + + supplyVoltageLow = true; + + } + + /* Turn LED off if appropriate time has passed */ + + if (samplesWritten > numberOfSamplesToSwitchOffLED) enableLED = false; + + /* Sleep until next DMA transfer is complete */ + + AudioMoth_sleep(); + + } + + /* Write the compression buffer files at the end */ + + if (samplesWritten < numberOfSamples + numberOfSamplesInHeader && numberOfCompressedBuffers > 0) { + + /* Light LED during SD card write if appropriate */ + + if (enableLED) AudioMoth_setRedLED(true); + + /* Encode and write compression buffer */ + + encodeCompressionBuffer(numberOfCompressedBuffers); + + totalNumberOfCompressedSamples += (numberOfCompressedBuffers - 1) * COMPRESSION_BUFFER_SIZE_IN_BYTES / NUMBER_OF_BYTES_IN_SAMPLE; + + FLASH_LED_IF_ENABLED_AND_RETURN_ON_ERROR(AudioMoth_writeToFile(compressionBuffer, COMPRESSION_BUFFER_SIZE_IN_BYTES)); + + /* Clear LED */ + + AudioMoth_setRedLED(false); + + } + + /* Determine recording state */ + + recordingState = microphoneChanged ? MICROPHONE_CHANGED : + switchPositionChanged ? SWITCH_CHANGED : + magneticSwitch ? MAGNETIC_SWITCH : + supplyVoltageLow ? SUPPLY_VOLTAGE_LOW : + fileSizeLimited ? FILE_SIZE_LIMITED : + RECORDING_OKAY; + + /* Generate the new file name if necessary */ + + static char newFilename[MAXIMUM_FILE_NAME_LENGTH]; + + if (timeOffset > 0) { + + generateFolderAndFilename(foldername, newFilename, actualRecordingStartTime, frequencyTriggerEnabled || amplitudeThresholdEnabled); + + } + + /* Write the GUANO data */ + + bool gpsLocationReceived = getBackupFlag(BACKUP_GPS_LOCATION_RECEIVED); + + bool acousticLocationReceived = getBackupFlag(BACKUP_ACOUSTIC_LOCATION_RECEIVED); + + uint32_t guanoDataSize = writeGuanoData((char*)compressionBuffer, configSettings, actualRecordingStartTime, gpsLocationReceived, gpsLastFixLatitude, gpsLastFixLongitude, acousticLocationReceived, acousticLatitude, acousticLongitude, firmwareDescription, firmwareVersion, (uint8_t*)AM_UNIQUE_ID_START_ADDRESS, deploymentID, defaultDeploymentID, timeOffset > 0 ? newFilename : filename, extendedBatteryState, temperature, requestedFilterType); + + FLASH_LED_IF_ENABLED_AND_RETURN_ON_ERROR(AudioMoth_writeToFile(compressionBuffer, guanoDataSize)); + + /* Initialise the WAV header */ + + samplesWritten = MAX(numberOfSamplesInHeader, samplesWritten); + + setHeaderDetails(&wavHeader, effectiveSampleRate, samplesWritten - numberOfSamplesInHeader - totalNumberOfCompressedSamples, guanoDataSize); + + setHeaderComment(&wavHeader, configSettings, actualRecordingStartTime, (uint8_t*)AM_UNIQUE_ID_START_ADDRESS, deploymentID, defaultDeploymentID, extendedBatteryState, temperature, externalMicrophone == AM_EXTERNAL_PRESENT_AND_USED, recordingState, requestedFilterType); + + /* Write the header */ + + if (enableLED) AudioMoth_setRedLED(true); + + FLASH_LED_IF_ENABLED_AND_RETURN_ON_ERROR(AudioMoth_seekInFile(0)); + + FLASH_LED_IF_ENABLED_AND_RETURN_ON_ERROR(AudioMoth_writeToFile(&wavHeader, sizeof(wavHeader_t))); + + /* Close the file */ + + FLASH_LED_IF_ENABLED_AND_RETURN_ON_ERROR(AudioMoth_closeFile()); + + AudioMoth_setRedLED(false); + + /* Rename the file if necessary */ + + if (timeOffset > 0) { + + if (enableLED) AudioMoth_setRedLED(true); + + FLASH_LED_IF_ENABLED_AND_RETURN_ON_ERROR(AudioMoth_renameFile(filename, newFilename)); + + AudioMoth_setRedLED(false); + + } + + /* Return recording state */ + + return recordingState; + +} + +/* Determine sunrise and sunset and schedule recording */ + +static void determineSunriseAndSunsetTimesAndScheduleRecording(uint32_t currentTime) { + + uint32_t scheduleTime = currentTime; + + uint32_t currentTimeOfNextRecording = *timeOfNextRecording; + + uint32_t currentStartOfRecordingPeriod = *startOfRecordingPeriod; + + /* Calculate initial sunrise and sunset time if appropriate */ + + if (configSettings->enableSunRecording && *timeOfNextSunriseSunsetCalculation == 0) { + + determineSunriseAndSunsetTimes(scheduleTime); + + determineTimeOfNextSunriseSunsetCalculation(scheduleTime, timeOfNextSunriseSunsetCalculation); + + determineSunriseAndSunsetTimes(*timeOfNextSunriseSunsetCalculation - SECONDS_IN_DAY); + + determineTimeOfNextSunriseSunsetCalculation(scheduleTime, timeOfNextSunriseSunsetCalculation); + + scheduleRecording(scheduleTime, timeOfNextRecording, indexOfNextRecording, durationOfNextRecording, startOfRecordingPeriod, NULL); + + if (*timeOfNextSunriseSunsetCalculation < *timeOfNextRecording) *timeOfNextSunriseSunsetCalculation += SECONDS_IN_DAY; + + } else { + + scheduleRecording(scheduleTime, timeOfNextRecording, indexOfNextRecording, durationOfNextRecording, startOfRecordingPeriod, NULL); + + } + + /* Check if sunrise and sunset should be recalculated */ + + if (configSettings->enableSunRecording && *timeOfNextRecording >= *timeOfNextSunriseSunsetCalculation) { + + scheduleTime = MAX(scheduleTime, *timeOfNextSunriseSunsetCalculation); + + determineSunriseAndSunsetTimes(scheduleTime); + + determineTimeOfNextSunriseSunsetCalculation(scheduleTime, timeOfNextSunriseSunsetCalculation); + + scheduleRecording(scheduleTime, timeOfNextRecording, indexOfNextRecording, durationOfNextRecording, startOfRecordingPeriod, NULL); + + if (*timeOfNextSunriseSunsetCalculation < *timeOfNextRecording) *timeOfNextSunriseSunsetCalculation += SECONDS_IN_DAY; + + } + + /* Finished if not using GPS */ + + if (configSettings->enableTimeSettingFromGPS == false) return; + + /* Update GPS time setting time */ + + bool shouldSetTimeFromGPS = false; + + uint32_t gpsTimeSettingPeriod = configSettings->gpsTimeSettingPeriod == 0 ? GPS_DEFAULT_TIME_SETTING_PERIOD : configSettings->gpsTimeSettingPeriod * SECONDS_IN_MINUTE; + + if (configSettings->enableTimeSettingBeforeAndAfterRecordings) { + + *timeOfNextGPSTimeSetting = *timeOfNextRecording - gpsTimeSettingPeriod; + + shouldSetTimeFromGPS = currentTimeOfNextRecording != UINT32_MAX && currentTimeOfNextRecording != *timeOfNextRecording; + + } else { + + *timeOfNextGPSTimeSetting = *startOfRecordingPeriod - gpsTimeSettingPeriod; + + shouldSetTimeFromGPS = currentStartOfRecordingPeriod != UINT32_MAX && currentStartOfRecordingPeriod != *startOfRecordingPeriod; + + } + + setBackupFlag(BACKUP_SHOULD_SET_TIME_FROM_GPS, shouldSetTimeFromGPS); + +} + +/* Determine sunrise and sunset calculation time */ + +static void determineTimeOfNextSunriseSunsetCalculation(uint32_t currentTime, uint32_t *timeOfNextSunriseSunsetCalculation) { + + /* Check if limited by earliest recording time */ + + uint32_t earliestRecordingTime = configSettings->earliestRecordingTime; + + if (earliestRecordingTime > 0) { + + if (getBackupFlag(BACKUP_ACOUSTIC_TIMEZONE_RECEIVED) && configSettings->useTimezoneFromAcousticChime && configSettings->adjustScheduleUsingTimezoneFromAcousticChime) { + + earliestRecordingTime += configSettings->timezoneHours * SECONDS_IN_HOUR + configSettings->timezoneMinutes * SECONDS_IN_MINUTE - *acousticTimezoneMinutes * SECONDS_IN_MINUTE; + + } + + currentTime = MAX(currentTime, earliestRecordingTime); + + } + + /* Determine when the middle of largest gap between recording periods occurs */ + + uint32_t startOfGapMinutes, endOfGapMinutes; + + if (*numberOfSunRecordingPeriods == 1) { + + startOfGapMinutes = firstSunRecordingPeriod->endMinutes; + + endOfGapMinutes = firstSunRecordingPeriod->startMinutes; + + } else { + + uint32_t gapFromFirstPeriodToSecondPeriod = secondSunRecordingPeriod->startMinutes - firstSunRecordingPeriod->endMinutes; + + uint32_t gapFromSecondPeriodsToFirstPeriod = secondSunRecordingPeriod->endMinutes < secondSunRecordingPeriod->startMinutes ? firstSunRecordingPeriod->startMinutes - secondSunRecordingPeriod->endMinutes : MINUTES_IN_DAY - secondSunRecordingPeriod->endMinutes + firstSunRecordingPeriod->startMinutes; + + if (gapFromFirstPeriodToSecondPeriod > gapFromSecondPeriodsToFirstPeriod) { + + startOfGapMinutes = firstSunRecordingPeriod->endMinutes; + + endOfGapMinutes = secondSunRecordingPeriod->startMinutes; + + } else { + + startOfGapMinutes = secondSunRecordingPeriod->endMinutes; + + endOfGapMinutes = firstSunRecordingPeriod->startMinutes; + + } + + } + + uint32_t calculationMinutes = endOfGapMinutes + startOfGapMinutes; + + if (endOfGapMinutes < startOfGapMinutes) calculationMinutes += MINUTES_IN_DAY; + + calculationMinutes /= 2; + + calculationMinutes = calculationMinutes % MINUTES_IN_DAY; + + /* Determine the number of seconds of this day */ + + struct tm time; + + time_t rawTime = currentTime; + + gmtime_r(&rawTime, &time); + + uint32_t currentSeconds = SECONDS_IN_HOUR * time.tm_hour + SECONDS_IN_MINUTE * time.tm_min + time.tm_sec; + + /* Determine the time of the next sunrise and sunset calculation */ + + uint32_t calculationTime = currentTime - currentSeconds + SECONDS_IN_MINUTE * calculationMinutes; + + if (calculationTime <= currentTime) calculationTime += SECONDS_IN_DAY; + + *timeOfNextSunriseSunsetCalculation = calculationTime; + +} + +/* Determine sunrise and sunset times */ + +static void determineSunriseAndSunsetTimes(uint32_t currentTime) { + + /* Calculate future sunrise and sunset time if recording is limited by earliest recording time */ + + uint32_t earliestRecordingTime = configSettings->earliestRecordingTime; + + if (earliestRecordingTime > 0) { + + if (getBackupFlag(BACKUP_ACOUSTIC_TIMEZONE_RECEIVED) && configSettings->useTimezoneFromAcousticChime && configSettings->adjustScheduleUsingTimezoneFromAcousticChime) { + + earliestRecordingTime += configSettings->timezoneHours * SECONDS_IN_HOUR + configSettings->timezoneMinutes * SECONDS_IN_MINUTE - *acousticTimezoneMinutes * SECONDS_IN_MINUTE; + + } + + currentTime = MAX(currentTime, earliestRecordingTime); + + } + + /* Determine sunrise and sunset times */ + + SR_trend_t trend; + + SR_solution_t solution; + + uint32_t sunriseMinutes, sunsetMinutes; + + float latitude = getBackupFlag(BACKUP_GPS_LOCATION_RECEIVED) ? (float)*gpsLatitude / (float)GPS_LOCATION_PRECISION : getBackupFlag(BACKUP_ACOUSTIC_LOCATION_RECEIVED) ? (float)*acousticLatitude / (float)ACOUSTIC_LOCATION_PRECISION : (float)configSettings->latitude / (float)CONFIG_LOCATION_PRECISION; + + float longitude = getBackupFlag(BACKUP_GPS_LOCATION_RECEIVED) ? (float)*gpsLongitude / (float)GPS_LOCATION_PRECISION : getBackupFlag(BACKUP_ACOUSTIC_LOCATION_RECEIVED) ? (float)*acousticLongitude / (float)ACOUSTIC_LOCATION_PRECISION : (float)configSettings->longitude / (float)CONFIG_LOCATION_PRECISION; + + Sunrise_calculateFromUnix(configSettings->sunRecordingEvent, currentTime, latitude, longitude, &solution, &trend, &sunriseMinutes, &sunsetMinutes); + + /* Calculate maximum recording duration */ + + uint32_t minimumRecordingGap = MAX(MINIMUM_SUN_RECORDING_GAP, SUN_RECORDING_GAP_MULTIPLIER * configSettings->sunRoundingMinutes); + + uint32_t maximumRecordingDuration = MINUTES_IN_DAY - minimumRecordingGap; + + /* Round the sunrise and sunset times */ + + uint32_t roundedSunriseMinutes = configSettings->sunRoundingMinutes > 0 ? UNSIGNED_ROUND(sunriseMinutes, configSettings->sunRoundingMinutes) : sunriseMinutes; + + uint32_t roundedSunsetMinutes = configSettings->sunRoundingMinutes > 0 ? UNSIGNED_ROUND(sunsetMinutes, configSettings->sunRoundingMinutes) : sunsetMinutes; + + /* Calculate start and end of potential recording periods */ + + uint32_t beforeSunrise = (MINUTES_IN_DAY + roundedSunriseMinutes - configSettings->beforeSunriseMinutes) % MINUTES_IN_DAY; + + uint32_t afterSunrise = (MINUTES_IN_DAY + roundedSunriseMinutes + configSettings->afterSunriseMinutes) % MINUTES_IN_DAY; + + uint32_t beforeSunset = (MINUTES_IN_DAY + roundedSunsetMinutes - configSettings->beforeSunsetMinutes) % MINUTES_IN_DAY; + + uint32_t afterSunset = (MINUTES_IN_DAY + roundedSunsetMinutes + configSettings->afterSunsetMinutes) % MINUTES_IN_DAY; + + /* Determine schedule */ + + recordingPeriod_t tempRecordingPeriod; + + if (configSettings->sunRecordingMode == SUNRISE_RECORDING) { + + /* Recording from before sunrise to after sunrise */ + + *numberOfSunRecordingPeriods = 1; + + tempRecordingPeriod.startMinutes = beforeSunrise; + + tempRecordingPeriod.endMinutes = afterSunrise; + + copyToBackupDomain((uint32_t*)firstSunRecordingPeriod, (uint8_t*)&tempRecordingPeriod, sizeof(recordingPeriod_t)); + + } else if (configSettings->sunRecordingMode == SUNSET_RECORDING) { + + /* Recording from before sunset to after sunset */ + + *numberOfSunRecordingPeriods = 1; + + tempRecordingPeriod.startMinutes = beforeSunset; + + tempRecordingPeriod.endMinutes = afterSunset; + + copyToBackupDomain((uint32_t*)firstSunRecordingPeriod, (uint8_t*)&tempRecordingPeriod, sizeof(recordingPeriod_t)); + + } else if (configSettings->sunRecordingMode == SUNRISE_AND_SUNSET_RECORDING) { + + /* Order the recording periods */ + + uint32_t firstPeriodStartMinutes = beforeSunrise < beforeSunset ? beforeSunrise : beforeSunset; + + uint32_t firstPeriodEndMinutes = beforeSunrise < beforeSunset ? afterSunrise : afterSunset; + + uint32_t secondPeriodStartMinutes = beforeSunrise < beforeSunset ? beforeSunset : beforeSunrise; + + uint32_t secondPeriodEndMinutes = beforeSunrise < beforeSunset ? afterSunset : afterSunrise; + + /* Determine whether the recording periods wrap */ + + bool firstPeriodWraps = firstPeriodEndMinutes <= firstPeriodStartMinutes; + + bool secondPeriodWraps = secondPeriodEndMinutes <= secondPeriodStartMinutes; + + /* Combine recording periods together if they overlap */ + + if (firstPeriodWraps) { + + *numberOfSunRecordingPeriods = 1; + + tempRecordingPeriod.startMinutes = firstPeriodStartMinutes; + + tempRecordingPeriod.endMinutes = secondPeriodWraps ? MIN(firstPeriodStartMinutes, MAX(firstPeriodEndMinutes, secondPeriodEndMinutes)) : firstPeriodEndMinutes; + + copyToBackupDomain((uint32_t*)firstSunRecordingPeriod, (uint8_t*)&tempRecordingPeriod, sizeof(recordingPeriod_t)); + + } else if (secondPeriodStartMinutes <= firstPeriodEndMinutes) { + + *numberOfSunRecordingPeriods = 1; + + tempRecordingPeriod.startMinutes = firstPeriodStartMinutes; + + tempRecordingPeriod.endMinutes = secondPeriodWraps ? MIN(firstPeriodStartMinutes, secondPeriodEndMinutes) : MAX(firstPeriodEndMinutes, secondPeriodEndMinutes); + + copyToBackupDomain((uint32_t*)firstSunRecordingPeriod, (uint8_t*)&tempRecordingPeriod, sizeof(recordingPeriod_t)); + + } else if (secondPeriodWraps && secondPeriodEndMinutes >= firstPeriodStartMinutes) { + + *numberOfSunRecordingPeriods = 1; + + tempRecordingPeriod.startMinutes = secondPeriodStartMinutes; + + tempRecordingPeriod.endMinutes = MAX(firstPeriodEndMinutes, secondPeriodEndMinutes); + + copyToBackupDomain((uint32_t*)firstSunRecordingPeriod, (uint8_t*)&tempRecordingPeriod, sizeof(recordingPeriod_t)); + + } else { + + *numberOfSunRecordingPeriods = 2; + + tempRecordingPeriod.startMinutes = firstPeriodStartMinutes; + + tempRecordingPeriod.endMinutes = firstPeriodEndMinutes; + + copyToBackupDomain((uint32_t*)firstSunRecordingPeriod, (uint8_t*)&tempRecordingPeriod, sizeof(recordingPeriod_t)); + + tempRecordingPeriod.startMinutes = secondPeriodStartMinutes; + + tempRecordingPeriod.endMinutes = secondPeriodEndMinutes; + + copyToBackupDomain((uint32_t*)secondSunRecordingPeriod, (uint8_t*)&tempRecordingPeriod, sizeof(recordingPeriod_t)); + + } + + /* Adjust the size of the minimum gap between recording periods if it is less than the threshold */ + + if (*numberOfSunRecordingPeriods == 1) { + + uint32_t duration = firstSunRecordingPeriod->endMinutes <= firstSunRecordingPeriod->startMinutes ? MINUTES_IN_DAY + firstSunRecordingPeriod->endMinutes - firstSunRecordingPeriod->startMinutes : firstSunRecordingPeriod->endMinutes - firstSunRecordingPeriod->startMinutes; + + if (duration > maximumRecordingDuration) { + + tempRecordingPeriod.startMinutes = firstSunRecordingPeriod->startMinutes; + + tempRecordingPeriod.endMinutes = (firstSunRecordingPeriod->startMinutes + maximumRecordingDuration) % MINUTES_IN_DAY; + + copyToBackupDomain((uint32_t*)firstSunRecordingPeriod, (uint8_t*)&tempRecordingPeriod, sizeof(recordingPeriod_t)); + + } + + } + + if (*numberOfSunRecordingPeriods == 2) { + + uint32_t gapFromFirstPeriodToSecondPeriod = secondSunRecordingPeriod->startMinutes - firstSunRecordingPeriod->endMinutes; + + uint32_t gapFromSecondPeriodsToFirstPeriod = secondSunRecordingPeriod->endMinutes < secondSunRecordingPeriod->startMinutes ? firstSunRecordingPeriod->startMinutes - secondSunRecordingPeriod->endMinutes : MINUTES_IN_DAY + firstSunRecordingPeriod->startMinutes - secondSunRecordingPeriod->endMinutes; + + if (gapFromFirstPeriodToSecondPeriod >= gapFromSecondPeriodsToFirstPeriod && gapFromFirstPeriodToSecondPeriod < minimumRecordingGap) { + + tempRecordingPeriod.startMinutes = firstSunRecordingPeriod->startMinutes; + + tempRecordingPeriod.endMinutes = (MINUTES_IN_DAY + firstSunRecordingPeriod->endMinutes - minimumRecordingGap + gapFromFirstPeriodToSecondPeriod) % MINUTES_IN_DAY; + + copyToBackupDomain((uint32_t*)firstSunRecordingPeriod, (uint8_t*)&tempRecordingPeriod, sizeof(recordingPeriod_t)); + + } else if (gapFromSecondPeriodsToFirstPeriod >= gapFromFirstPeriodToSecondPeriod && gapFromSecondPeriodsToFirstPeriod < minimumRecordingGap) { + + tempRecordingPeriod.startMinutes = secondSunRecordingPeriod->startMinutes; + + tempRecordingPeriod.endMinutes = (MINUTES_IN_DAY + secondSunRecordingPeriod->endMinutes - minimumRecordingGap + gapFromSecondPeriodsToFirstPeriod) % MINUTES_IN_DAY; + + copyToBackupDomain((uint32_t*)secondSunRecordingPeriod, (uint8_t*)&tempRecordingPeriod, sizeof(recordingPeriod_t)); + + } + + } + + } else if (configSettings->sunRecordingMode == SUNSET_TO_SUNRISE_RECORDING) { + + /* Recording from before sunset to after sunrise */ + + *numberOfSunRecordingPeriods = 1; + + tempRecordingPeriod.startMinutes = beforeSunset; + + uint32_t timeFromSunsetToSunrise; + + if (roundedSunriseMinutes == roundedSunsetMinutes) { + + timeFromSunsetToSunrise = trend == SR_DAY_SHORTER_THAN_NIGHT ? MINUTES_IN_DAY : 0; + + } else { + + timeFromSunsetToSunrise = roundedSunriseMinutes < roundedSunsetMinutes ? MINUTES_IN_DAY + roundedSunriseMinutes - roundedSunsetMinutes : roundedSunriseMinutes - roundedSunsetMinutes; + + } + + uint32_t duration = timeFromSunsetToSunrise + configSettings->beforeSunsetMinutes + configSettings->afterSunriseMinutes; + + if (duration == 0) duration = 1; + + if (duration > maximumRecordingDuration) duration = maximumRecordingDuration; + + tempRecordingPeriod.endMinutes = (beforeSunset + duration) % MINUTES_IN_DAY; + + copyToBackupDomain((uint32_t*)firstSunRecordingPeriod, (uint8_t*)&tempRecordingPeriod, sizeof(recordingPeriod_t)); + + } else if (configSettings->sunRecordingMode == SUNRISE_TO_SUNSET_RECORDING) { + + /* Recording from before sunrise to after sunset */ + + *numberOfSunRecordingPeriods = 1; + + tempRecordingPeriod.startMinutes = beforeSunrise; + + uint32_t timeFromSunriseToSunset; + + if (roundedSunriseMinutes == roundedSunsetMinutes) { + + timeFromSunriseToSunset = trend == SR_DAY_LONGER_THAN_NIGHT ? MINUTES_IN_DAY : 0; + + } else { + + timeFromSunriseToSunset = roundedSunsetMinutes < roundedSunriseMinutes ? MINUTES_IN_DAY + roundedSunsetMinutes - roundedSunriseMinutes : roundedSunsetMinutes - roundedSunriseMinutes; + + } + + uint32_t duration = timeFromSunriseToSunset + configSettings->beforeSunriseMinutes + configSettings->afterSunsetMinutes; + + if (duration == 0) duration = 1; + + if (duration > maximumRecordingDuration) duration = maximumRecordingDuration; + + tempRecordingPeriod.endMinutes = (beforeSunrise + duration) % MINUTES_IN_DAY; + + copyToBackupDomain((uint32_t*)firstSunRecordingPeriod, (uint8_t*)&tempRecordingPeriod, sizeof(recordingPeriod_t)); + + } + +} + +/* Schedule recordings */ + +static void adjustRecordingDuration(uint32_t *duration, uint32_t recordDuration, uint32_t sleepDuration) { + + uint32_t durationOfCycle = recordDuration + sleepDuration; + + uint32_t numberOfCycles = *duration / durationOfCycle; + + uint32_t partialCycle = *duration % durationOfCycle; + + if (partialCycle == 0) { + + *duration = *duration > sleepDuration ? *duration - sleepDuration : 0; + + } else { + + *duration = MIN(*duration, numberOfCycles * durationOfCycle + recordDuration); + + } + +} + +static void calculateStartAndDuration(uint32_t currentTime, uint32_t currentSeconds, recordingPeriod_t *period, int32_t startMinutesOffset, uint32_t *startTime, uint32_t *duration) { + + uint32_t startMinutes = (2 * MINUTES_IN_DAY + period->startMinutes + startMinutesOffset) % MINUTES_IN_DAY; + + *startTime = currentTime - currentSeconds + SECONDS_IN_MINUTE * startMinutes; + + *duration = period->endMinutes <= period->startMinutes ? MINUTES_IN_DAY + period->endMinutes - period->startMinutes : period->endMinutes - period->startMinutes; + + *duration *= SECONDS_IN_MINUTE; + +} + +static void scheduleRecording(uint32_t currentTime, uint32_t *timeOfNextRecording, uint32_t *indexOfNextRecording, uint32_t *durationOfNextRecording, uint32_t *startOfRecordingPeriod, uint32_t *endOfRecordingPeriod) { + + /* Enforce minumum schedule date */ + + currentTime = MAX(currentTime, START_OF_CENTURY); + + /* Check if recording should be limited by earliest recording time */ + + uint32_t earliestRecordingTime = configSettings->earliestRecordingTime; + + if (earliestRecordingTime > 0) { + + if (getBackupFlag(BACKUP_ACOUSTIC_TIMEZONE_RECEIVED) && configSettings->useTimezoneFromAcousticChime && configSettings->adjustScheduleUsingTimezoneFromAcousticChime) { + + earliestRecordingTime += configSettings->timezoneHours * SECONDS_IN_HOUR + configSettings->timezoneMinutes * SECONDS_IN_MINUTE - *acousticTimezoneMinutes * SECONDS_IN_MINUTE; + + } + + currentTime = MAX(currentTime, earliestRecordingTime); + + } + + /* Select appropriate recording periods */ + + uint32_t activeRecordingPeriods = configSettings->enableSunRecording ? *numberOfSunRecordingPeriods : MIN(configSettings->activeRecordingPeriods, MAX_RECORDING_PERIODS); + + recordingPeriod_t *recordingPeriods = configSettings->enableSunRecording ? firstSunRecordingPeriod : configSettings->recordingPeriods; + + /* No suitable recording periods */ + + if (activeRecordingPeriods == 0) { + + *timeOfNextRecording = UINT32_MAX; + + *indexOfNextRecording = 0; + + if (startOfRecordingPeriod) *startOfRecordingPeriod = UINT32_MAX; + + if (endOfRecordingPeriod) *endOfRecordingPeriod = UINT32_MAX; + + *durationOfNextRecording = 0; + + return; + + } + + /* Calculate the number of seconds of this day */ + + struct tm time; + + time_t rawTime = currentTime; + + gmtime_r(&rawTime, &time); + + uint32_t currentSeconds = SECONDS_IN_HOUR * time.tm_hour + SECONDS_IN_MINUTE * time.tm_min + time.tm_sec; + + /* Calculate the start time offset for the appropriate time zone */ + + uint32_t minimumIndex = 0; + + int32_t startMinutesOffset = 0; + + if (configSettings->enableSunRecording == false) { + + if (getBackupFlag(BACKUP_ACOUSTIC_TIMEZONE_RECEIVED) && configSettings->useTimezoneFromAcousticChime && configSettings->adjustScheduleUsingTimezoneFromAcousticChime) { + + startMinutesOffset = configSettings->timezoneHours * MINUTES_IN_HOUR + configSettings->timezoneMinutes - *acousticTimezoneMinutes; + + } + + /* Find the first recording period */ + + uint32_t minimumStartMinutes = UINT32_MAX; + + for (uint32_t i = 0; i < activeRecordingPeriods; i += 1) { + + recordingPeriod_t *currentPeriod = recordingPeriods + i; + + uint32_t startMinutes = (2 * MINUTES_IN_DAY + currentPeriod->startMinutes + startMinutesOffset) % MINUTES_IN_DAY; + + if (startMinutes < minimumStartMinutes) { + + minimumStartMinutes = startMinutes; + + minimumIndex = i; + + } + + } + + } + + /* Check the last active period on the previous day */ + + uint32_t startTime, duration; + + uint32_t index = (minimumIndex + activeRecordingPeriods - 1) % activeRecordingPeriods; + + recordingPeriod_t *lastPeriod = recordingPeriods + index; + + calculateStartAndDuration(currentTime - SECONDS_IN_DAY, currentSeconds, lastPeriod, startMinutesOffset, &startTime, &duration); + + if (configSettings->disableSleepRecordCycle == false) { + + adjustRecordingDuration(&duration, configSettings->recordDuration, configSettings->sleepDuration); + + } + + if (currentTime < startTime + duration && duration > 0) goto done; + + /* Check each active recording period on the same day */ + + for (uint32_t i = 0; i < activeRecordingPeriods; i += 1) { + + index = (minimumIndex + i) % activeRecordingPeriods; + + recordingPeriod_t *currentPeriod = recordingPeriods + index; + + calculateStartAndDuration(currentTime, currentSeconds, currentPeriod, startMinutesOffset, &startTime, &duration); + + if (configSettings->disableSleepRecordCycle == false) { + + adjustRecordingDuration(&duration, configSettings->recordDuration, configSettings->sleepDuration); + + } + + if (currentTime < startTime + duration && duration > 0) goto done; + + } + + /* Calculate time until first period tomorrow */ + + index = minimumIndex; + + recordingPeriod_t *firstPeriod = recordingPeriods + index; + + calculateStartAndDuration(currentTime + SECONDS_IN_DAY, currentSeconds, firstPeriod, startMinutesOffset, &startTime, &duration); + + if (configSettings->disableSleepRecordCycle == false) { + + adjustRecordingDuration(&duration, configSettings->recordDuration, configSettings->sleepDuration); + + } + +done: + + /* Set the time for start and end of the recording period */ + + if (startOfRecordingPeriod) *startOfRecordingPeriod = startTime; + + if (endOfRecordingPeriod) *endOfRecordingPeriod = startTime + duration; + + /* Resolve sleep and record cycle */ + + if (configSettings->disableSleepRecordCycle) { + + *timeOfNextRecording = startTime; + + *durationOfNextRecording = duration; + + } else { + + if (currentTime <= startTime) { + + /* Recording should start at the start of the recording period */ + + *timeOfNextRecording = startTime; + + *durationOfNextRecording = MIN(duration, configSettings->recordDuration); + + } else { + + /* Recording should start immediately or at the start of the next recording cycle */ + + uint32_t secondsFromStartOfPeriod = currentTime - startTime; + + uint32_t durationOfCycle = configSettings->recordDuration + configSettings->sleepDuration; + + uint32_t partialCycle = secondsFromStartOfPeriod % durationOfCycle; + + *timeOfNextRecording = currentTime - partialCycle; + + if (partialCycle >= configSettings->recordDuration) { + + /* Wait for next cycle to begin */ + + *timeOfNextRecording += durationOfCycle; + + } + + uint32_t remainingDuration = startTime + duration - *timeOfNextRecording; + + *durationOfNextRecording = MIN(configSettings->recordDuration, remainingDuration); + + } + + } + + /* Update start time and duration is recording period has started */ + + if (currentTime > *timeOfNextRecording) { + + *durationOfNextRecording -= currentTime - *timeOfNextRecording; + + *timeOfNextRecording = currentTime; + + } + + /* Check if recording should be limited by last recording time */ + + uint32_t latestRecordingTime = MIDPOINT_OF_CENTURY; + + if (configSettings->latestRecordingTime > 0) { + + latestRecordingTime = configSettings->latestRecordingTime; + + if (getBackupFlag(BACKUP_ACOUSTIC_TIMEZONE_RECEIVED) && configSettings->useTimezoneFromAcousticChime && configSettings->adjustScheduleUsingTimezoneFromAcousticChime) { + + latestRecordingTime += configSettings->timezoneHours * SECONDS_IN_HOUR + configSettings->timezoneMinutes * SECONDS_IN_MINUTE - *acousticTimezoneMinutes * SECONDS_IN_MINUTE; + + } + + } + + if (*timeOfNextRecording >= latestRecordingTime) { + + *timeOfNextRecording = UINT32_MAX; + + if (startOfRecordingPeriod) *startOfRecordingPeriod = UINT32_MAX; + + if (endOfRecordingPeriod) *endOfRecordingPeriod = UINT32_MAX; + + *durationOfNextRecording = 0; + + } else { + + int64_t excessTime = (int64_t)*timeOfNextRecording + (int64_t)*durationOfNextRecording - (int64_t)latestRecordingTime; + + if (excessTime > 0) { + + *durationOfNextRecording -= excessTime; + + if (endOfRecordingPeriod) *endOfRecordingPeriod = *timeOfNextRecording + *durationOfNextRecording; + + } + + } + + /* Set the index of the next recording */ + + *indexOfNextRecording = index; + +} + +/* Flash LED according to battery life */ + +static void flashLedToIndicateBatteryLife(void) { + + uint32_t numberOfFlashes = LOW_BATTERY_LED_FLASHES; + + uint32_t supplyVoltage = AudioMoth_getSupplyVoltage(); + + if (configSettings->batteryLevelDisplayType == NIMH_LIPO_BATTERY_VOLTAGE) { + + /* Set number of flashes according to battery voltage */ + + AM_extendedBatteryState_t batteryState = AudioMoth_getExtendedBatteryState(supplyVoltage); + + if (batteryState > AM_EXT_BAT_4V3) { + + numberOfFlashes = 1; + + } else if (batteryState > AM_EXT_BAT_3V5) { + + numberOfFlashes = AM_EXT_BAT_4V4 - batteryState; + + } + + } else { + + /* Set number of flashes according to battery state */ + + AM_batteryState_t batteryState = AudioMoth_getBatteryState(supplyVoltage); + + if (batteryState > AM_BATTERY_LOW) { + + numberOfFlashes = (batteryState >= AM_BATTERY_4V6) ? 4 : (batteryState >= AM_BATTERY_4V4) ? 3 : (batteryState >= AM_BATTERY_4V0) ? 2 : 1; + + } + + } + + /* Flash LED */ + + for (uint32_t i = 0; i < numberOfFlashes; i += 1) { + + FLASH_LED(Red, SHORT_LED_FLASH_DURATION) + + if (numberOfFlashes == LOW_BATTERY_LED_FLASHES) { + + AudioMoth_delay(SHORT_LED_FLASH_DURATION); + + } else { + + AudioMoth_delay(LONG_LED_FLASH_DURATION); + + } + + } + +}