From db1963390b67c64c27cbb8396dac5635c70b1afc Mon Sep 17 00:00:00 2001 From: Ash6414 Date: Mon, 8 Jun 2026 13:35:52 -0500 Subject: [PATCH 01/38] Delete src/main.c --- src/main.c | 4343 ---------------------------------------------------- 1 file changed, 4343 deletions(-) delete mode 100644 src/main.c diff --git a/src/main.c b/src/main.c deleted file mode 100644 index 0b810e3..0000000 --- a/src/main.c +++ /dev/null @@ -1,4343 +0,0 @@ -/**************************************************************************** - * 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); - - } - - } - -} From 3a7f56665a6113d08dd8d3120a25f8b0dd24fc2a Mon Sep 17 00:00:00 2001 From: Ash6414 Date: Mon, 8 Jun 2026 13:37:10 -0500 Subject: [PATCH 02/38] Add files via upload --- src/espbridge.c | 420 +++++ src/main.c | 4379 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 4799 insertions(+) create mode 100644 src/espbridge.c create mode 100644 src/main.c diff --git a/src/espbridge.c b/src/espbridge.c new file mode 100644 index 0000000..97d8789 --- /dev/null +++ b/src/espbridge.c @@ -0,0 +1,420 @@ +/**************************************************************************** + * espbridge.c + * AudioMoth Dev <-> ESP32 upload bridge. + * + * Protocol is ASCII commands plus binary DATA payloads. + * Commands from ESP32: + * STATUS\n + * TIME \n + * LIST\n + * GET \n + * DELETE \n // ESP32 should send only after server-confirmed upload + * DONE\n // ESP32 releases service window + * PING\n + * + * Responses from AudioMoth: + * OK ...\n + * ERR \n + * FILE \n + * END\n + * DATA \n + * + * Notes: + * - This file assumes the standard AudioMoth-Project build exposes EMLIB, + * FatFS, and audiomoth.h. + * - UART route uses USART1 location 2 because AudioMoth Dev labels b9/b10 + * as U1_TX#2 / U1_RX#2. + *****************************************************************************/ + +#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 */ +#define BRIDGE_UART USART1 +#define BRIDGE_UART_CLOCK cmuClock_USART1 +#define BRIDGE_UART_LOCATION USART_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 + +static volatile bool bridgeBusy = true; +static volatile bool uploadAllowed = false; +static bool filesystemEnabled = false; + +static char lineBuffer[ESPBRIDGE_MAX_LINE]; +static uint8_t chunkBuffer[ESPBRIDGE_CHUNK_BYTES]; + +/* ---------------- UART primitives ---------------- */ + +static inline bool uartRxAvailable(void) { + return (USART_StatusGet(BRIDGE_UART) & USART_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; +} + +/* ---------------- 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 (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 void 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"); + } else { + sendLine("ERR CMD unknown_command"); + } +} + +/* ---------------- 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, gpioModeInput, 0); + 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 = USART_ROUTE_TXPEN | + USART_ROUTE_RXPEN | + BRIDGE_UART_LOCATION; + + bridgeBusy = true; + uploadAllowed = false; + filesystemEnabled = false; +} + +void ESPBridge_setBusy(bool busy) { + bridgeBusy = busy; + GPIO_PinOutWrite(BRIDGE_BUSY_PORT, BRIDGE_BUSY_PIN, busy ? 1 : 0); +} + +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 || !uploadAllowed) return; + if (deadlineReached(deadlineUnixSeconds)) return; + + uint32_t idleMs = 0; + sendLine("OK BRIDGE_READY"); + + while (!deadlineReached(deadlineUnixSeconds)) { + WDOG_Feed(); + + if (!ESPBridge_isRequestActive() && !uartRxAvailable()) { + if (idleMs >= SERVICE_IDLE_TIMEOUT_MS) break; + AudioMoth_delay(10); + idleMs += 10; + continue; + } + + if (readLine(RX_LINE_TIMEOUT_MS)) { + idleMs = 0; + handleCommand(deadlineUnixSeconds); + } else { + idleMs += RX_LINE_TIMEOUT_MS; + } + } + + sendLine("OK BRIDGE_SLEEP"); +} diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..8c0b1b9 --- /dev/null +++ b/src/main.c @@ -0,0 +1,4379 @@ +/**************************************************************************** + * 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 + +/* 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, 1}; + +static uint8_t firmwareDescription[AM_FIRMWARE_DESCRIPTION_LENGTH] = "AudioMoth-Firmware-ESPnode"; + +/* 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); + + /* 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) { + + /* 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); + + } + + } + +} From ab5f80b7d128c98282e0ded5a552621b53a50255 Mon Sep 17 00:00:00 2001 From: Ash6414 Date: Mon, 8 Jun 2026 13:37:36 -0500 Subject: [PATCH 03/38] Add files via upload --- inc/espbridge.h | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 inc/espbridge.h diff --git a/inc/espbridge.h b/inc/espbridge.h new file mode 100644 index 0000000..f0dccfc --- /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 921600 +#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 */ From a5efacaabdaedea6a38cccbe5d94c845a2590986 Mon Sep 17 00:00:00 2001 From: Ash6414 Date: Mon, 8 Jun 2026 13:49:05 -0500 Subject: [PATCH 04/38] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2526668..860fd92 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# AudioMoth-Firmware-Basic +# AudioMoth-Firmware-EspNode Firmware for AudioMoth devices, used in conjunction with the AudioMoth-Project framework, to produce the standard AudioMoth releases. From d4689debd59aef33e0f477f094782c35aef5bea4 Mon Sep 17 00:00:00 2001 From: Ash6414 Date: Mon, 8 Jun 2026 13:50:19 -0500 Subject: [PATCH 05/38] Create build-audiomoth-bin.yml --- .github/workflows/build-audiomoth-bin.yml | 55 +++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 .github/workflows/build-audiomoth-bin.yml diff --git a/.github/workflows/build-audiomoth-bin.yml b/.github/workflows/build-audiomoth-bin.yml new file mode 100644 index 0000000..18dd3aa --- /dev/null +++ b/.github/workflows/build-audiomoth-bin.yml @@ -0,0 +1,55 @@ +name: Build AudioMoth ESPnode firmware + +on: + workflow_dispatch: + push: + branches: + - master + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Check out ESPnode firmware fork + uses: actions/checkout@v4 + with: + path: firmware + + - name: Install ARM toolchain and build tools + run: | + sudo apt-get update + sudo apt-get install -y git make gcc-arm-none-eabi + + - name: Clone AudioMoth-Project build framework + run: | + git clone https://github.com/OpenAcousticDevices/AudioMoth-Project.git project + + - name: Copy ESPnode firmware into AudioMoth-Project + run: | + cp firmware/src/main.c project/src/main.c + cp firmware/src/*.c project/src/ + cp firmware/inc/*.h project/inc/ + + - name: Patch Makefile for GitHub Actions toolchain and GPS sources + run: | + sed -i 's|^TOOLCHAIN_PATH =.*|TOOLCHAIN_PATH = /usr|' project/build/Makefile + sed -i 's|^TOOLCHAIN_VERSION =.*|TOOLCHAIN_VERSION = |' project/build/Makefile + sed -i 's|^INC =.*|INC = ../cmsis ../device/inc ../emlib/inc ../emusb/inc ../drivers/inc ../fatfs/inc ../gps/inc ../inc|' project/build/Makefile + sed -i 's|^SRC =.*|SRC = ../device/src ../emlib/src ../emusb/src ../drivers/src ../fatfs/src ../gps/src ../src|' project/build/Makefile + + - name: Build firmware binary + working-directory: project/build + run: | + make clean + make audiomoth.bin + + - name: Upload firmware artifact + uses: actions/upload-artifact@v4 + with: + name: audiomoth-espnode-firmware + path: | + project/build/audiomoth.bin + project/build/audiomoth.hex + project/build/audiomoth.map + project/build/audiomoth.lst From 39ca3ec77336d58974d8ee20d7976474cb743a5a Mon Sep 17 00:00:00 2001 From: Ash6414 Date: Mon, 8 Jun 2026 13:56:24 -0500 Subject: [PATCH 06/38] Update espbridge.c --- src/espbridge.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/espbridge.c b/src/espbridge.c index 97d8789..1658d74 100644 --- a/src/espbridge.c +++ b/src/espbridge.c @@ -69,6 +69,14 @@ 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) & USART_STATUS_RXDATAV) != 0; } @@ -380,7 +388,7 @@ void ESPBridge_init(void) { void ESPBridge_setBusy(bool busy) { bridgeBusy = busy; - GPIO_PinOutWrite(BRIDGE_BUSY_PORT, BRIDGE_BUSY_PIN, busy ? 1 : 0); + gpioWrite(BRIDGE_BUSY_PORT, BRIDGE_BUSY_PIN, busy); } void ESPBridge_setUploadAllowed(bool allowed) { From 85219b94bca0d6d03c53499908cd2dff30a52ffa Mon Sep 17 00:00:00 2001 From: Ash6414 Date: Mon, 8 Jun 2026 14:03:11 -0500 Subject: [PATCH 07/38] Use fork source directly in AudioMoth firmware build --- .github/workflows/build-audiomoth-bin.yml | 32 ++++++++++++++++++----- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-audiomoth-bin.yml b/.github/workflows/build-audiomoth-bin.yml index 18dd3aa..3ede6fe 100644 --- a/.github/workflows/build-audiomoth-bin.yml +++ b/.github/workflows/build-audiomoth-bin.yml @@ -15,6 +15,20 @@ jobs: uses: actions/checkout@v4 with: path: firmware + ref: master + fetch-depth: 1 + + - name: Show exact fork source being built + run: | + echo "Repository: Ash6414/AudioMoth-Firmware_ESPnode" + echo "Branch: master" + echo "Commit: $(git -C firmware rev-parse HEAD)" + echo "espbridge.c busy implementation:" + grep -n "ESPBridge_setBusy" -A8 firmware/src/espbridge.c + if grep -R "GPIO_PinOutWrite" -n firmware/src firmware/inc; then + echo "ERROR: fork source still contains GPIO_PinOutWrite" + exit 1 + fi - name: Install ARM toolchain and build tools run: | @@ -23,15 +37,21 @@ jobs: - name: Clone AudioMoth-Project build framework run: | - git clone https://github.com/OpenAcousticDevices/AudioMoth-Project.git project + git clone --depth 1 https://github.com/OpenAcousticDevices/AudioMoth-Project.git project - - name: Copy ESPnode firmware into AudioMoth-Project + - name: Copy fork source into AudioMoth-Project without editing it run: | - cp firmware/src/main.c project/src/main.c - cp firmware/src/*.c project/src/ - cp firmware/inc/*.h project/inc/ + cp -f firmware/src/main.c project/src/main.c + cp -f firmware/src/*.c project/src/ + cp -f firmware/inc/*.h project/inc/ + echo "Build-tree espbridge.c busy implementation:" + grep -n "ESPBridge_setBusy" -A8 project/src/espbridge.c + if grep -R "GPIO_PinOutWrite" -n project/src project/inc; then + echo "ERROR: build tree source still contains GPIO_PinOutWrite" + exit 1 + fi - - name: Patch Makefile for GitHub Actions toolchain and GPS sources + - name: Configure AudioMoth-Project Makefile for CI only run: | sed -i 's|^TOOLCHAIN_PATH =.*|TOOLCHAIN_PATH = /usr|' project/build/Makefile sed -i 's|^TOOLCHAIN_VERSION =.*|TOOLCHAIN_VERSION = |' project/build/Makefile From ffa0ad50a036d93da81a06ffc26d753f028b697f Mon Sep 17 00:00:00 2001 From: Ash6414 Date: Mon, 8 Jun 2026 17:27:41 -0500 Subject: [PATCH 08/38] Add one-time firmware identity patch workflow --- .../patch-firmware-identity-once.yml | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 .github/workflows/patch-firmware-identity-once.yml diff --git a/.github/workflows/patch-firmware-identity-once.yml b/.github/workflows/patch-firmware-identity-once.yml new file mode 100644 index 0000000..2fbaccb --- /dev/null +++ b/.github/workflows/patch-firmware-identity-once.yml @@ -0,0 +1,69 @@ +name: Patch firmware identity once + +on: + workflow_dispatch: + push: + branches: + - master + paths: + - .github/workflows/patch-firmware-identity-once.yml + +permissions: + contents: write + +jobs: + patch-main-c: + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + ref: master + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Patch src/main.c firmware identity + run: | + python3 - <<'PY' + from pathlib import Path + import re + + path = Path("src/main.c") + text = path.read_text() + + text2, n_version = re.subn( + r'static uint8_t firmwareVersion\[AM_FIRMWARE_VERSION_LENGTH\]\s*=\s*\{[^}]+\};', + 'static uint8_t firmwareVersion[AM_FIRMWARE_VERSION_LENGTH] = {1, 12, 0};', + text, + count=1, + ) + + text3, n_desc = re.subn( + r'static uint8_t firmwareDescription\[AM_FIRMWARE_DESCRIPTION_LENGTH\]\s*=\s*"[^"]+";', + 'static uint8_t firmwareDescription[AM_FIRMWARE_DESCRIPTION_LENGTH] = "AudioMoth-Firmware-Basic";', + text2, + count=1, + ) + + if n_version != 1 or n_desc != 1: + raise SystemExit(f"Expected to patch exactly one version and one description, patched version={n_version}, description={n_desc}") + + path.write_text(text3) + PY + + - name: Commit src/main.c if changed + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + echo "Patched identity lines:" + grep -n "static uint8_t firmwareVersion\|static uint8_t firmwareDescription" -A2 -B2 src/main.c + + if git diff --quiet -- src/main.c; then + echo "src/main.c already had the requested firmware identity. No commit needed." + exit 0 + fi + + git add src/main.c + git commit -m "Set firmware identity to AudioMoth Basic" + git push origin master From 1894abc735e39d482a28fe7e71434add9724c9a7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 22:27:48 +0000 Subject: [PATCH 09/38] Set firmware identity to AudioMoth Basic --- src/main.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main.c b/src/main.c index 8c0b1b9..8a3d7de 100644 --- a/src/main.c +++ b/src/main.c @@ -1478,9 +1478,9 @@ static int16_t secondaryBuffer[MAXIMUM_SAMPLES_IN_DMA_TRANSFER]; /* Firmware version and description */ -static uint8_t firmwareVersion[AM_FIRMWARE_VERSION_LENGTH] = {1, 12, 1}; +static uint8_t firmwareVersion[AM_FIRMWARE_VERSION_LENGTH] = {1, 12, 0}; -static uint8_t firmwareDescription[AM_FIRMWARE_DESCRIPTION_LENGTH] = "AudioMoth-Firmware-ESPnode"; +static uint8_t firmwareDescription[AM_FIRMWARE_DESCRIPTION_LENGTH] = "AudioMoth-Firmware-Basic"; /* Function prototypes */ From ec9a3d9072d670ae827d18697d3bed114b04ca7a Mon Sep 17 00:00:00 2001 From: Ash6414 Date: Mon, 8 Jun 2026 17:47:40 -0500 Subject: [PATCH 10/38] Update main.c --- src/main.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.c b/src/main.c index 8a3d7de..103fa48 100644 --- a/src/main.c +++ b/src/main.c @@ -1844,7 +1844,7 @@ int main(void) { /* Determine if acoustic configuration is required */ - bool shouldPerformAcousticConfiguration = switchPosition == AM_SWITCH_CUSTOM && (AudioMoth_hasTimeBeenSet() == false || configSettings->requireAcousticConfiguration); + 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 */ From 12c90e0e5f5fc03f9acdc04c794caf8d84941047 Mon Sep 17 00:00:00 2001 From: Ash6414 Date: Mon, 8 Jun 2026 17:57:15 -0500 Subject: [PATCH 11/38] Add one-time ESP time-wait patch workflow --- .../workflows/patch-esp-time-wait-once.yml | 234 ++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 .github/workflows/patch-esp-time-wait-once.yml diff --git a/.github/workflows/patch-esp-time-wait-once.yml b/.github/workflows/patch-esp-time-wait-once.yml new file mode 100644 index 0000000..682b8c3 --- /dev/null +++ b/.github/workflows/patch-esp-time-wait-once.yml @@ -0,0 +1,234 @@ +name: Patch ESP time wait once + +on: + workflow_dispatch: + push: + branches: + - master + paths: + - .github/workflows/patch-esp-time-wait-once.yml + +permissions: + contents: write + +jobs: + patch-esp-time-wait: + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + ref: master + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Patch AudioMoth ESP time-wait behavior + run: | + python3 - <<'PY' + from pathlib import Path + import re + + main_path = Path("src/main.c") + bridge_path = Path("src/espbridge.c") + + main = main_path.read_text() + bridge = bridge_path.read_text() + + # ------------------------------------------------------------------ + # 1) src/espbridge.c: allow PING/STATUS/TIME/DONE service even when + # uploadAllowed is false. LIST/GET already reject when uploadAllowed + # is false. Patch DELETE to do the same. + # ------------------------------------------------------------------ + old_delete_pattern = re.compile( + r'static void commandDelete\(char \*args\) \{.*?\n\}\n\nstatic void commandTime', + re.S, + ) + + new_delete = '''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''' + + bridge2, delete_count = old_delete_pattern.subn(new_delete, bridge, count=1) + if delete_count != 1: + raise SystemExit(f"Failed to patch commandDelete; patched {delete_count} instances") + + old_service_guard = """void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) { + if (bridgeBusy || !uploadAllowed) return; + if (deadlineReached(deadlineUnixSeconds)) return;""" + + new_service_guard = """void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) { + if (bridgeBusy) return; + if (deadlineReached(deadlineUnixSeconds)) return;""" + + if old_service_guard not in bridge2 and new_service_guard not in bridge2: + raise SystemExit("Could not find ESPBridge_serviceUntil guard") + bridge2 = bridge2.replace(old_service_guard, new_service_guard, 1) + + # ------------------------------------------------------------------ + # 2) src/main.c: define ESP time-wait constants. + # ------------------------------------------------------------------ + if "ESP_TIME_WAIT_TIMEOUT_MS" not in main: + anchor = "#define USB_CONFIGURATION_BLINK 400" + replacement = anchor + "\n\n#define ESP_TIME_WAIT_TIMEOUT_MS 60000\n#define ESP_TIME_WAIT_STEP_MS 50\n#define ESP_TIME_SERVICE_WINDOW_SECONDS 2" + if anchor not in main: + raise SystemExit("Could not find USB_CONFIGURATION_BLINK anchor") + main = main.replace(anchor, replacement, 1) + + # ------------------------------------------------------------------ + # 3) src/main.c: when time is unset in CUSTOM, wait for ESP32 TIME + # instead of falling into acoustic configuration. + # ------------------------------------------------------------------ + if "ESP32 time-setting path" not in main: + read_time_block = """ /* Read the time */ + + uint32_t currentTime; + + uint32_t currentMilliseconds; + + AudioMoth_getTime(¤tTime, ¤tMilliseconds); + + /* Check if switch has just been moved to CUSTOM or DEFAULT */""" + + esp_time_block = """ /* 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) { + + uint32_t waitedMilliseconds = 0; + + ESPBridge_setBusy(false); + ESPBridge_setUploadAllowed(false); + + while (AudioMoth_hasTimeBeenSet() == false && waitedMilliseconds < ESP_TIME_WAIT_TIMEOUT_MS) { + + 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 */""" + + if read_time_block not in main: + raise SystemExit("Could not find first read-time block in main.c") + main = main.replace(read_time_block, esp_time_block, 1) + + old_acoustic = "bool shouldPerformAcousticConfiguration = switchPosition == AM_SWITCH_CUSTOM && (AudioMoth_hasTimeBeenSet() == false || configSettings->requireAcousticConfiguration);" + new_acoustic = "bool shouldPerformAcousticConfiguration = switchPosition == AM_SWITCH_CUSTOM && configSettings->requireAcousticConfiguration;" + + if old_acoustic not in main and new_acoustic not in main: + raise SystemExit("Could not find acoustic configuration condition") + main = main.replace(old_acoustic, new_acoustic, 1) + + main_path.write_text(main) + bridge_path.write_text(bridge2) + PY + + - name: Show patched snippets + run: | + echo "main.c time wait constants:" + grep -n "ESP_TIME_" -A3 -B3 src/main.c + echo + echo "main.c ESP32 time path:" + grep -n "ESP32 time-setting path" -A55 -B8 src/main.c + echo + echo "main.c acoustic config condition:" + grep -n "shouldPerformAcousticConfiguration" -A2 -B2 src/main.c + echo + echo "espbridge.c service guard:" + grep -n "void ESPBridge_serviceUntil" -A5 src/espbridge.c + echo + echo "espbridge.c DELETE guard:" + grep -n "static void commandDelete" -A18 src/espbridge.c + + - name: Commit patched firmware code if changed + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + if git diff --quiet -- src/main.c src/espbridge.c; then + echo "No firmware source changes needed." + exit 0 + fi + + git add src/main.c src/espbridge.c + git commit -m "Wait for ESP32 time before acoustic configuration" + git push origin master From 74c79fece01371492fd44887a6eac62462772283 Mon Sep 17 00:00:00 2001 From: Ash6414 Date: Mon, 8 Jun 2026 17:59:56 -0500 Subject: [PATCH 12/38] Fix ESP time-wait patch workflow YAML --- .../workflows/patch-esp-time-wait-once.yml | 186 +++++++++--------- 1 file changed, 90 insertions(+), 96 deletions(-) diff --git a/.github/workflows/patch-esp-time-wait-once.yml b/.github/workflows/patch-esp-time-wait-once.yml index 682b8c3..355c171 100644 --- a/.github/workflows/patch-esp-time-wait-once.yml +++ b/.github/workflows/patch-esp-time-wait-once.yml @@ -2,11 +2,6 @@ name: Patch ESP time wait once on: workflow_dispatch: - push: - branches: - - master - paths: - - .github/workflows/patch-esp-time-wait-once.yml permissions: contents: write @@ -27,6 +22,7 @@ jobs: python3 - <<'PY' from pathlib import Path import re + import textwrap main_path = Path("src/main.c") bridge_path = Path("src/espbridge.c") @@ -34,158 +30,156 @@ jobs: main = main_path.read_text() bridge = bridge_path.read_text() - # ------------------------------------------------------------------ - # 1) src/espbridge.c: allow PING/STATUS/TIME/DONE service even when - # uploadAllowed is false. LIST/GET already reject when uploadAllowed - # is false. Patch DELETE to do the same. - # ------------------------------------------------------------------ old_delete_pattern = re.compile( r'static void commandDelete\(char \*args\) \{.*?\n\}\n\nstatic void commandTime', re.S, ) - new_delete = '''static void commandDelete(char *args) { - char path[ESPBRIDGE_MAX_PATH]; + new_delete = textwrap.dedent("""\ + static void commandDelete(char *args) { + char path[ESPBRIDGE_MAX_PATH]; - if (bridgeBusy || !uploadAllowed) { - sendLine("ERR BUSY upload_not_allowed"); - return; - } + if (bridgeBusy || !uploadAllowed) { + sendLine("ERR BUSY upload_not_allowed"); + return; + } - if (sscanf(args, "%95s", path) != 1) { - sendLine("ERR ARG usage_DELETE_path"); - 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 (!validPath(path, true)) { + sendLine("ERR PATH invalid_path"); + return; + } - if (!ensureFilesystem()) { - sendLine("ERR SD filesystem_enable_failed"); - return; - } + if (!ensureFilesystem()) { + sendLine("ERR SD filesystem_enable_failed"); + return; + } - FRESULT res = f_unlink(path); + FRESULT res = f_unlink(path); - if (res == FR_OK) { - sendLine("OK DELETE %s", path); - } else { - sendLine("ERR DELETE %u", (unsigned int)res); - } -} + if (res == FR_OK) { + sendLine("OK DELETE %s", path); + } else { + sendLine("ERR DELETE %u", (unsigned int)res); + } + } -static void commandTime''' + static void commandTime""") bridge2, delete_count = old_delete_pattern.subn(new_delete, bridge, count=1) if delete_count != 1: raise SystemExit(f"Failed to patch commandDelete; patched {delete_count} instances") - old_service_guard = """void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) { - if (bridgeBusy || !uploadAllowed) return; - if (deadlineReached(deadlineUnixSeconds)) return;""" + old_service_guard = textwrap.dedent("""\ + void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) { + if (bridgeBusy || !uploadAllowed) return; + if (deadlineReached(deadlineUnixSeconds)) return;""") - new_service_guard = """void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) { - if (bridgeBusy) return; - if (deadlineReached(deadlineUnixSeconds)) return;""" + new_service_guard = textwrap.dedent("""\ + void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) { + if (bridgeBusy) return; + if (deadlineReached(deadlineUnixSeconds)) return;""") if old_service_guard not in bridge2 and new_service_guard not in bridge2: raise SystemExit("Could not find ESPBridge_serviceUntil guard") bridge2 = bridge2.replace(old_service_guard, new_service_guard, 1) - # ------------------------------------------------------------------ - # 2) src/main.c: define ESP time-wait constants. - # ------------------------------------------------------------------ if "ESP_TIME_WAIT_TIMEOUT_MS" not in main: anchor = "#define USB_CONFIGURATION_BLINK 400" - replacement = anchor + "\n\n#define ESP_TIME_WAIT_TIMEOUT_MS 60000\n#define ESP_TIME_WAIT_STEP_MS 50\n#define ESP_TIME_SERVICE_WINDOW_SECONDS 2" + replacement = ( + anchor + + "\n\n#define ESP_TIME_WAIT_TIMEOUT_MS 60000" + + "\n#define ESP_TIME_WAIT_STEP_MS 50" + + "\n#define ESP_TIME_SERVICE_WINDOW_SECONDS 2" + ) if anchor not in main: raise SystemExit("Could not find USB_CONFIGURATION_BLINK anchor") main = main.replace(anchor, replacement, 1) - # ------------------------------------------------------------------ - # 3) src/main.c: when time is unset in CUSTOM, wait for ESP32 TIME - # instead of falling into acoustic configuration. - # ------------------------------------------------------------------ if "ESP32 time-setting path" not in main: - read_time_block = """ /* Read the time */ + read_time_block = textwrap.dedent("""\ + /* Read the time */ - uint32_t currentTime; + uint32_t currentTime; - uint32_t currentMilliseconds; + uint32_t currentMilliseconds; - AudioMoth_getTime(¤tTime, ¤tMilliseconds); + AudioMoth_getTime(¤tTime, ¤tMilliseconds); - /* Check if switch has just been moved to CUSTOM or DEFAULT */""" + /* Check if switch has just been moved to CUSTOM or DEFAULT */""") - esp_time_block = """ /* Read the time */ + esp_time_block = textwrap.dedent("""\ + /* Read the time */ - uint32_t currentTime; + uint32_t currentTime; - uint32_t currentMilliseconds; + uint32_t currentMilliseconds; - AudioMoth_getTime(¤tTime, ¤tMilliseconds); + 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. - */ + /* 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) { + if (switchPosition == AM_SWITCH_CUSTOM && AudioMoth_hasTimeBeenSet() == false) { - uint32_t waitedMilliseconds = 0; + uint32_t waitedMilliseconds = 0; - ESPBridge_setBusy(false); - ESPBridge_setUploadAllowed(false); + ESPBridge_setBusy(false); + ESPBridge_setUploadAllowed(false); - while (AudioMoth_hasTimeBeenSet() == false && waitedMilliseconds < ESP_TIME_WAIT_TIMEOUT_MS) { + while (AudioMoth_hasTimeBeenSet() == false && waitedMilliseconds < ESP_TIME_WAIT_TIMEOUT_MS) { - if (ESPBridge_isRequestActive()) { + if (ESPBridge_isRequestActive()) { - uint32_t bridgeCurrentTime; - uint32_t bridgeCurrentMilliseconds; + uint32_t bridgeCurrentTime; + uint32_t bridgeCurrentMilliseconds; - AudioMoth_getTime(&bridgeCurrentTime, &bridgeCurrentMilliseconds); + AudioMoth_getTime(&bridgeCurrentTime, &bridgeCurrentMilliseconds); - ESPBridge_serviceUntil(bridgeCurrentTime + ESP_TIME_SERVICE_WINDOW_SECONDS); + ESPBridge_serviceUntil(bridgeCurrentTime + ESP_TIME_SERVICE_WINDOW_SECONDS); - waitedMilliseconds += ESP_TIME_SERVICE_WINDOW_SECONDS * MILLISECONDS_IN_SECOND; + waitedMilliseconds += ESP_TIME_SERVICE_WINDOW_SECONDS * MILLISECONDS_IN_SECOND; - } else { + } else { - if (configSettings->enableLED && waitedMilliseconds % WAITING_LED_FLASH_INTERVAL == 0) { - FLASH_LED(Green, WAITING_LED_FLASH_DURATION) - } + if (configSettings->enableLED && waitedMilliseconds % WAITING_LED_FLASH_INTERVAL == 0) { + FLASH_LED(Green, WAITING_LED_FLASH_DURATION) + } - AudioMoth_delay(ESP_TIME_WAIT_STEP_MS); + AudioMoth_delay(ESP_TIME_WAIT_STEP_MS); - waitedMilliseconds += ESP_TIME_WAIT_STEP_MS; + waitedMilliseconds += ESP_TIME_WAIT_STEP_MS; - } + } - } + } - ESPBridge_setUploadAllowed(false); - ESPBridge_setBusy(true); + ESPBridge_setUploadAllowed(false); + ESPBridge_setBusy(true); - if (AudioMoth_hasTimeBeenSet() == false) { + if (AudioMoth_hasTimeBeenSet() == false) { - SAVE_SWITCH_POSITION_AND_POWER_DOWN(DEFAULT_WAIT_INTERVAL); + SAVE_SWITCH_POSITION_AND_POWER_DOWN(DEFAULT_WAIT_INTERVAL); - } + } - AudioMoth_getTime(¤tTime, ¤tMilliseconds); + AudioMoth_getTime(¤tTime, ¤tMilliseconds); - } + } - /* Check if switch has just been moved to CUSTOM or DEFAULT */""" + /* Check if switch has just been moved to CUSTOM or DEFAULT */""") if read_time_block not in main: raise SystemExit("Could not find first read-time block in main.c") From 4b8739171c27865f0d601848a5428d140f135913 Mon Sep 17 00:00:00 2001 From: Ash6414 Date: Mon, 8 Jun 2026 19:04:13 -0500 Subject: [PATCH 13/38] Fix ESP bridge patch workflow and trigger source patch --- .../workflows/patch-esp-time-wait-once.yml | 268 ++++++++---------- 1 file changed, 123 insertions(+), 145 deletions(-) diff --git a/.github/workflows/patch-esp-time-wait-once.yml b/.github/workflows/patch-esp-time-wait-once.yml index 355c171..e32f1ad 100644 --- a/.github/workflows/patch-esp-time-wait-once.yml +++ b/.github/workflows/patch-esp-time-wait-once.yml @@ -1,13 +1,18 @@ -name: Patch ESP time wait once +name: Patch ESP bridge source now on: workflow_dispatch: + push: + branches: + - master + paths: + - .github/workflows/patch-esp-time-wait-once.yml permissions: contents: write jobs: - patch-esp-time-wait: + patch-esp-bridge: runs-on: ubuntu-latest steps: @@ -17,12 +22,11 @@ jobs: ref: master token: ${{ secrets.GITHUB_TOKEN }} - - name: Patch AudioMoth ESP time-wait behavior + - name: Patch main.c and espbridge.c run: | python3 - <<'PY' from pathlib import Path import re - import textwrap main_path = Path("src/main.c") bridge_path = Path("src/espbridge.c") @@ -30,199 +34,173 @@ jobs: main = main_path.read_text() bridge = bridge_path.read_text() - old_delete_pattern = re.compile( - r'static void commandDelete\(char \*args\) \{.*?\n\}\n\nstatic void commandTime', - re.S, + main = re.sub( + r'static uint8_t firmwareVersion\[AM_FIRMWARE_VERSION_LENGTH\]\s*=\s*\{[^}]+\};', + 'static uint8_t firmwareVersion[AM_FIRMWARE_VERSION_LENGTH] = {1, 12, 0};', + main, + count=1, ) - new_delete = textwrap.dedent("""\ - 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""") - - bridge2, delete_count = old_delete_pattern.subn(new_delete, bridge, count=1) - if delete_count != 1: - raise SystemExit(f"Failed to patch commandDelete; patched {delete_count} instances") - - old_service_guard = textwrap.dedent("""\ - void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) { - if (bridgeBusy || !uploadAllowed) return; - if (deadlineReached(deadlineUnixSeconds)) return;""") - - new_service_guard = textwrap.dedent("""\ - void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) { - if (bridgeBusy) return; - if (deadlineReached(deadlineUnixSeconds)) return;""") - - if old_service_guard not in bridge2 and new_service_guard not in bridge2: - raise SystemExit("Could not find ESPBridge_serviceUntil guard") - bridge2 = bridge2.replace(old_service_guard, new_service_guard, 1) + main = re.sub( + r'static uint8_t firmwareDescription\[AM_FIRMWARE_DESCRIPTION_LENGTH\]\s*=\s*"[^"]+";', + 'static uint8_t firmwareDescription[AM_FIRMWARE_DESCRIPTION_LENGTH] = "AudioMoth-Firmware-Basic";', + main, + count=1, + ) if "ESP_TIME_WAIT_TIMEOUT_MS" not in main: anchor = "#define USB_CONFIGURATION_BLINK 400" - replacement = ( + if anchor not in main: + raise SystemExit("Missing USB_CONFIGURATION_BLINK anchor") + main = main.replace( + anchor, anchor + "\n\n#define ESP_TIME_WAIT_TIMEOUT_MS 60000" + "\n#define ESP_TIME_WAIT_STEP_MS 50" - + "\n#define ESP_TIME_SERVICE_WINDOW_SECONDS 2" + + "\n#define ESP_TIME_SERVICE_WINDOW_SECONDS 2", + 1, ) - if anchor not in main: - raise SystemExit("Could not find USB_CONFIGURATION_BLINK anchor") - main = main.replace(anchor, replacement, 1) - - if "ESP32 time-setting path" not in main: - read_time_block = textwrap.dedent("""\ - /* Read the time */ - - uint32_t currentTime; - - uint32_t currentMilliseconds; - - AudioMoth_getTime(¤tTime, ¤tMilliseconds); - /* Check if switch has just been moved to CUSTOM or DEFAULT */""") + if "ESP32 time-setting path." not in main: + pattern = re.compile( + r'( /\* Read the time \*/\n\n' + r' uint32_t currentTime;\n\n' + r' uint32_t currentMilliseconds;\n\n' + r' AudioMoth_getTime\(¤tTime, ¤tMilliseconds\);\n\n)' + r'( /\* Check if switch has just been moved to CUSTOM or DEFAULT \*/)', + re.M, + ) - esp_time_block = textwrap.dedent("""\ - /* Read the time */ + block = r''' + /* 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. + */ - uint32_t currentTime; + if (switchPosition == AM_SWITCH_CUSTOM && AudioMoth_hasTimeBeenSet() == false) { - uint32_t currentMilliseconds; + uint32_t waitedMilliseconds = 0; - AudioMoth_getTime(¤tTime, ¤tMilliseconds); + ESPBridge_setBusy(false); + ESPBridge_setUploadAllowed(false); - /* 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. - */ + while (AudioMoth_hasTimeBeenSet() == false && waitedMilliseconds < ESP_TIME_WAIT_TIMEOUT_MS) { - if (switchPosition == AM_SWITCH_CUSTOM && AudioMoth_hasTimeBeenSet() == false) { + AudioMoth_feedWatchdog(); - uint32_t waitedMilliseconds = 0; + if (ESPBridge_isRequestActive()) { - ESPBridge_setBusy(false); - ESPBridge_setUploadAllowed(false); + uint32_t bridgeCurrentTime; + uint32_t bridgeCurrentMilliseconds; - while (AudioMoth_hasTimeBeenSet() == false && waitedMilliseconds < ESP_TIME_WAIT_TIMEOUT_MS) { + AudioMoth_getTime(&bridgeCurrentTime, &bridgeCurrentMilliseconds); - if (ESPBridge_isRequestActive()) { + ESPBridge_serviceUntil(bridgeCurrentTime + ESP_TIME_SERVICE_WINDOW_SECONDS); - uint32_t bridgeCurrentTime; - uint32_t bridgeCurrentMilliseconds; + waitedMilliseconds += ESP_TIME_SERVICE_WINDOW_SECONDS * MILLISECONDS_IN_SECOND; - AudioMoth_getTime(&bridgeCurrentTime, &bridgeCurrentMilliseconds); + } else { - ESPBridge_serviceUntil(bridgeCurrentTime + ESP_TIME_SERVICE_WINDOW_SECONDS); + if (configSettings->enableLED && waitedMilliseconds % WAITING_LED_FLASH_INTERVAL == 0) { + FLASH_LED(Green, WAITING_LED_FLASH_DURATION) + } - waitedMilliseconds += ESP_TIME_SERVICE_WINDOW_SECONDS * MILLISECONDS_IN_SECOND; + AudioMoth_delay(ESP_TIME_WAIT_STEP_MS); - } else { + waitedMilliseconds += ESP_TIME_WAIT_STEP_MS; - 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); - ESPBridge_setUploadAllowed(false); - ESPBridge_setBusy(true); + } - if (AudioMoth_hasTimeBeenSet() == false) { + AudioMoth_getTime(¤tTime, ¤tMilliseconds); - SAVE_SWITCH_POSITION_AND_POWER_DOWN(DEFAULT_WAIT_INTERVAL); + } - } +''' - AudioMoth_getTime(¤tTime, ¤tMilliseconds); + main, n = pattern.subn(lambda m: m.group(1) + block + m.group(2), main, count=1) + if n != 1: + raise SystemExit(f"Failed to insert ESP32 time-setting path; replacements={n}") - } + main = main.replace( + 'bool shouldPerformAcousticConfiguration = switchPosition == AM_SWITCH_CUSTOM && (AudioMoth_hasTimeBeenSet() == false || configSettings->requireAcousticConfiguration);', + 'bool shouldPerformAcousticConfiguration = switchPosition == AM_SWITCH_CUSTOM && configSettings->requireAcousticConfiguration;', + 1, + ) - /* Check if switch has just been moved to CUSTOM or DEFAULT */""") + main = main.replace( + 'bool listenForAcousticTone = switchPosition == AM_SWITCH_CUSTOM && shouldPerformAcousticConfiguration == false;', + 'bool listenForAcousticTone = false;', + 1, + ) - if read_time_block not in main: - raise SystemExit("Could not find first read-time block in main.c") - main = main.replace(read_time_block, esp_time_block, 1) + bridge = bridge.replace( + 'void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) {\n if (bridgeBusy || !uploadAllowed) return;\n if (deadlineReached(deadlineUnixSeconds)) return;', + 'void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) {\n if (bridgeBusy) return;\n if (deadlineReached(deadlineUnixSeconds)) return;', + 1, + ) - old_acoustic = "bool shouldPerformAcousticConfiguration = switchPosition == AM_SWITCH_CUSTOM && (AudioMoth_hasTimeBeenSet() == false || configSettings->requireAcousticConfiguration);" - new_acoustic = "bool shouldPerformAcousticConfiguration = switchPosition == AM_SWITCH_CUSTOM && configSettings->requireAcousticConfiguration;" + if 'static void commandDelete(char *args) {\n char path[ESPBRIDGE_MAX_PATH];\n\n if (bridgeBusy || !uploadAllowed)' not in bridge: + bridge = bridge.replace( + 'static void commandDelete(char *args) {\n char path[ESPBRIDGE_MAX_PATH];\n', + 'static void commandDelete(char *args) {\n char path[ESPBRIDGE_MAX_PATH];\n\n if (bridgeBusy || !uploadAllowed) {\n sendLine("ERR BUSY upload_not_allowed");\n return;\n }\n\n', + 1, + ) - if old_acoustic not in main and new_acoustic not in main: - raise SystemExit("Could not find acoustic configuration condition") - main = main.replace(old_acoustic, new_acoustic, 1) + checks = { + "main identity version": 'static uint8_t firmwareVersion[AM_FIRMWARE_VERSION_LENGTH] = {1, 12, 0};' in main, + "main identity description": 'AudioMoth-Firmware-Basic' in main, + "main ESP constants": 'ESP_TIME_WAIT_TIMEOUT_MS' in main, + "main ESP wait block": 'ESP32 time-setting path.' in main, + "main no unset-time acoustic trigger": 'AudioMoth_hasTimeBeenSet() == false || configSettings->requireAcousticConfiguration' not in main, + "main acoustic tone disabled": 'bool listenForAcousticTone = false;' in main, + "bridge service allows non-upload commands": 'if (bridgeBusy || !uploadAllowed) return;' not in bridge, + "bridge delete still upload gated": 'static void commandDelete(char *args) {\n char path[ESPBRIDGE_MAX_PATH];\n\n if (bridgeBusy || !uploadAllowed)' in bridge, + } + + for name, ok in checks.items(): + print(f"{name}: {ok}") + if not ok: + raise SystemExit(f"Sanity check failed: {name}") main_path.write_text(main) - bridge_path.write_text(bridge2) + bridge_path.write_text(bridge) PY - - name: Show patched snippets + - name: Show final bridge-relevant source run: | - echo "main.c time wait constants:" - grep -n "ESP_TIME_" -A3 -B3 src/main.c - echo - echo "main.c ESP32 time path:" - grep -n "ESP32 time-setting path" -A55 -B8 src/main.c - echo - echo "main.c acoustic config condition:" - grep -n "shouldPerformAcousticConfiguration" -A2 -B2 src/main.c - echo - echo "espbridge.c service guard:" - grep -n "void ESPBridge_serviceUntil" -A5 src/espbridge.c - echo - echo "espbridge.c DELETE guard:" - grep -n "static void commandDelete" -A18 src/espbridge.c - - - name: Commit patched firmware code if changed + grep -n "ESP_TIME_" -A4 -B4 src/main.c + grep -n "ESP32 time-setting path" -A65 -B8 src/main.c + grep -n "shouldPerformAcousticConfiguration\|listenForAcousticTone" -A4 -B4 src/main.c + grep -n "static void commandDelete" -A25 src/espbridge.c + grep -n "void ESPBridge_serviceUntil" -A6 src/espbridge.c + + - name: Commit source patch run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" if git diff --quiet -- src/main.c src/espbridge.c; then - echo "No firmware source changes needed." + echo "Source already patched." exit 0 fi git add src/main.c src/espbridge.c - git commit -m "Wait for ESP32 time before acoustic configuration" + git commit -m "Fix ESP bridge startup time sync" git push origin master From 64f0807f275469fd6e4a93bf841a6c04fd692436 Mon Sep 17 00:00:00 2001 From: Ash6414 Date: Mon, 8 Jun 2026 19:20:39 -0500 Subject: [PATCH 14/38] Fix workflow YAML indentation for bridge patch --- .../workflows/patch-esp-time-wait-once.yml | 211 +++++++----------- 1 file changed, 83 insertions(+), 128 deletions(-) diff --git a/.github/workflows/patch-esp-time-wait-once.yml b/.github/workflows/patch-esp-time-wait-once.yml index e32f1ad..e6a3024 100644 --- a/.github/workflows/patch-esp-time-wait-once.yml +++ b/.github/workflows/patch-esp-time-wait-once.yml @@ -30,151 +30,106 @@ jobs: main_path = Path("src/main.c") bridge_path = Path("src/espbridge.c") - main = main_path.read_text() bridge = bridge_path.read_text() - main = re.sub( - r'static uint8_t firmwareVersion\[AM_FIRMWARE_VERSION_LENGTH\]\s*=\s*\{[^}]+\};', - 'static uint8_t firmwareVersion[AM_FIRMWARE_VERSION_LENGTH] = {1, 12, 0};', - main, - count=1, - ) - - main = re.sub( - r'static uint8_t firmwareDescription\[AM_FIRMWARE_DESCRIPTION_LENGTH\]\s*=\s*"[^"]+";', - 'static uint8_t firmwareDescription[AM_FIRMWARE_DESCRIPTION_LENGTH] = "AudioMoth-Firmware-Basic";', - main, - count=1, - ) + main = re.sub(r'static uint8_t firmwareVersion\[AM_FIRMWARE_VERSION_LENGTH\]\s*=\s*\{[^}]+\};', 'static uint8_t firmwareVersion[AM_FIRMWARE_VERSION_LENGTH] = {1, 12, 0};', main, count=1) + main = re.sub(r'static uint8_t firmwareDescription\[AM_FIRMWARE_DESCRIPTION_LENGTH\]\s*=\s*"[^"]+";', 'static uint8_t firmwareDescription[AM_FIRMWARE_DESCRIPTION_LENGTH] = "AudioMoth-Firmware-Basic";', main, count=1) if "ESP_TIME_WAIT_TIMEOUT_MS" not in main: anchor = "#define USB_CONFIGURATION_BLINK 400" if anchor not in main: raise SystemExit("Missing USB_CONFIGURATION_BLINK anchor") - main = main.replace( - anchor, - anchor - + "\n\n#define ESP_TIME_WAIT_TIMEOUT_MS 60000" - + "\n#define ESP_TIME_WAIT_STEP_MS 50" - + "\n#define ESP_TIME_SERVICE_WINDOW_SECONDS 2", - 1, - ) + main = main.replace(anchor, anchor + "\n\n#define ESP_TIME_WAIT_TIMEOUT_MS 60000\n#define ESP_TIME_WAIT_STEP_MS 50\n#define ESP_TIME_SERVICE_WINDOW_SECONDS 2", 1) if "ESP32 time-setting path." not in main: - pattern = re.compile( - r'( /\* Read the time \*/\n\n' - r' uint32_t currentTime;\n\n' - r' uint32_t currentMilliseconds;\n\n' - r' AudioMoth_getTime\(¤tTime, ¤tMilliseconds\);\n\n)' - r'( /\* Check if switch has just been moved to CUSTOM or DEFAULT \*/)', - re.M, - ) - - block = r''' - /* 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) { - - uint32_t waitedMilliseconds = 0; - - ESPBridge_setBusy(false); - ESPBridge_setUploadAllowed(false); - - while (AudioMoth_hasTimeBeenSet() == false && 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); - - } - -''' - + pattern = re.compile(r'( /\* Read the time \*/\n\n uint32_t currentTime;\n\n uint32_t currentMilliseconds;\n\n AudioMoth_getTime\(¤tTime, ¤tMilliseconds\);\n\n)( /\* Check if switch has just been moved to CUSTOM or DEFAULT \*/)', re.M) + block_lines = [ + ' /* 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) {', + '', + ' uint32_t waitedMilliseconds = 0;', + '', + ' ESPBridge_setBusy(false);', + ' ESPBridge_setUploadAllowed(false);', + '', + ' while (AudioMoth_hasTimeBeenSet() == false && 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);', + '', + ' }', + '' + ] + block = "\n".join(block_lines) + "\n" main, n = pattern.subn(lambda m: m.group(1) + block + m.group(2), main, count=1) if n != 1: raise SystemExit(f"Failed to insert ESP32 time-setting path; replacements={n}") - main = main.replace( - 'bool shouldPerformAcousticConfiguration = switchPosition == AM_SWITCH_CUSTOM && (AudioMoth_hasTimeBeenSet() == false || configSettings->requireAcousticConfiguration);', - 'bool shouldPerformAcousticConfiguration = switchPosition == AM_SWITCH_CUSTOM && configSettings->requireAcousticConfiguration;', - 1, - ) - - main = main.replace( - 'bool listenForAcousticTone = switchPosition == AM_SWITCH_CUSTOM && shouldPerformAcousticConfiguration == false;', - 'bool listenForAcousticTone = false;', - 1, - ) + main = main.replace('bool shouldPerformAcousticConfiguration = switchPosition == AM_SWITCH_CUSTOM && (AudioMoth_hasTimeBeenSet() == false || configSettings->requireAcousticConfiguration);', 'bool shouldPerformAcousticConfiguration = switchPosition == AM_SWITCH_CUSTOM && configSettings->requireAcousticConfiguration;', 1) + main = main.replace('bool listenForAcousticTone = switchPosition == AM_SWITCH_CUSTOM && shouldPerformAcousticConfiguration == false;', 'bool listenForAcousticTone = false;', 1) - bridge = bridge.replace( - 'void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) {\n if (bridgeBusy || !uploadAllowed) return;\n if (deadlineReached(deadlineUnixSeconds)) return;', - 'void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) {\n if (bridgeBusy) return;\n if (deadlineReached(deadlineUnixSeconds)) return;', - 1, - ) + bridge = bridge.replace('void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) {\n if (bridgeBusy || !uploadAllowed) return;\n if (deadlineReached(deadlineUnixSeconds)) return;', 'void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) {\n if (bridgeBusy) return;\n if (deadlineReached(deadlineUnixSeconds)) return;', 1) if 'static void commandDelete(char *args) {\n char path[ESPBRIDGE_MAX_PATH];\n\n if (bridgeBusy || !uploadAllowed)' not in bridge: - bridge = bridge.replace( - 'static void commandDelete(char *args) {\n char path[ESPBRIDGE_MAX_PATH];\n', - 'static void commandDelete(char *args) {\n char path[ESPBRIDGE_MAX_PATH];\n\n if (bridgeBusy || !uploadAllowed) {\n sendLine("ERR BUSY upload_not_allowed");\n return;\n }\n\n', - 1, - ) - - checks = { - "main identity version": 'static uint8_t firmwareVersion[AM_FIRMWARE_VERSION_LENGTH] = {1, 12, 0};' in main, - "main identity description": 'AudioMoth-Firmware-Basic' in main, - "main ESP constants": 'ESP_TIME_WAIT_TIMEOUT_MS' in main, - "main ESP wait block": 'ESP32 time-setting path.' in main, - "main no unset-time acoustic trigger": 'AudioMoth_hasTimeBeenSet() == false || configSettings->requireAcousticConfiguration' not in main, - "main acoustic tone disabled": 'bool listenForAcousticTone = false;' in main, - "bridge service allows non-upload commands": 'if (bridgeBusy || !uploadAllowed) return;' not in bridge, - "bridge delete still upload gated": 'static void commandDelete(char *args) {\n char path[ESPBRIDGE_MAX_PATH];\n\n if (bridgeBusy || !uploadAllowed)' in bridge, - } - - for name, ok in checks.items(): + bridge = bridge.replace('static void commandDelete(char *args) {\n char path[ESPBRIDGE_MAX_PATH];\n', 'static void commandDelete(char *args) {\n char path[ESPBRIDGE_MAX_PATH];\n\n if (bridgeBusy || !uploadAllowed) {\n sendLine("ERR BUSY upload_not_allowed");\n return;\n }\n\n', 1) + + checks = [ + ('main identity version', 'static uint8_t firmwareVersion[AM_FIRMWARE_VERSION_LENGTH] = {1, 12, 0};' in main), + ('main identity description', 'AudioMoth-Firmware-Basic' in main), + ('main ESP constants', 'ESP_TIME_WAIT_TIMEOUT_MS' in main), + ('main ESP wait block', 'ESP32 time-setting path.' in main), + ('main no unset-time acoustic trigger', 'AudioMoth_hasTimeBeenSet() == false || configSettings->requireAcousticConfiguration' not in main), + ('main acoustic tone disabled', 'bool listenForAcousticTone = false;' in main), + ('bridge service allows non-upload commands', 'if (bridgeBusy || !uploadAllowed) return;' not in bridge), + ('bridge delete still upload gated', 'static void commandDelete(char *args) {\n char path[ESPBRIDGE_MAX_PATH];\n\n if (bridgeBusy || !uploadAllowed)' in bridge), + ] + for name, ok in checks: print(f"{name}: {ok}") if not ok: raise SystemExit(f"Sanity check failed: {name}") From bd2de16a8eb835fb24954d3cdc7d91dc3139e4c6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 00:20:46 +0000 Subject: [PATCH 15/38] Fix ESP bridge startup time sync --- src/espbridge.c | 8 +++++- src/main.c | 66 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/src/espbridge.c b/src/espbridge.c index 1658d74..faf062a 100644 --- a/src/espbridge.c +++ b/src/espbridge.c @@ -299,6 +299,12 @@ static void commandGet(char *args) { 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; @@ -400,7 +406,7 @@ bool ESPBridge_isRequestActive(void) { } void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) { - if (bridgeBusy || !uploadAllowed) return; + if (bridgeBusy) return; if (deadlineReached(deadlineUnixSeconds)) return; uint32_t idleMs = 0; diff --git a/src/main.c b/src/main.c index 103fa48..faf0f60 100644 --- a/src/main.c +++ b/src/main.c @@ -66,6 +66,10 @@ #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 @@ -1816,6 +1820,66 @@ int main(void) { 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) { + + uint32_t waitedMilliseconds = 0; + + ESPBridge_setBusy(false); + ESPBridge_setUploadAllowed(false); + + while (AudioMoth_hasTimeBeenSet() == false && 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; @@ -1858,7 +1922,7 @@ int main(void) { /* Determine whether to listen for the acoustic tone */ - bool listenForAcousticTone = switchPosition == AM_SWITCH_CUSTOM && shouldPerformAcousticConfiguration == false; + bool listenForAcousticTone = false; if (listenForAcousticTone) { From 5a81f5a3d20dbc4336e264fcb13ca52ec01ac450 Mon Sep 17 00:00:00 2001 From: Ash6414 Date: Tue, 9 Jun 2026 08:32:24 -0500 Subject: [PATCH 16/38] Remove temporary ESP bridge patch workflow --- .../workflows/patch-esp-time-wait-once.yml | 161 ------------------ 1 file changed, 161 deletions(-) delete mode 100644 .github/workflows/patch-esp-time-wait-once.yml diff --git a/.github/workflows/patch-esp-time-wait-once.yml b/.github/workflows/patch-esp-time-wait-once.yml deleted file mode 100644 index e6a3024..0000000 --- a/.github/workflows/patch-esp-time-wait-once.yml +++ /dev/null @@ -1,161 +0,0 @@ -name: Patch ESP bridge source now - -on: - workflow_dispatch: - push: - branches: - - master - paths: - - .github/workflows/patch-esp-time-wait-once.yml - -permissions: - contents: write - -jobs: - patch-esp-bridge: - runs-on: ubuntu-latest - - steps: - - name: Check out repository - uses: actions/checkout@v4 - with: - ref: master - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Patch main.c and espbridge.c - run: | - python3 - <<'PY' - from pathlib import Path - import re - - main_path = Path("src/main.c") - bridge_path = Path("src/espbridge.c") - main = main_path.read_text() - bridge = bridge_path.read_text() - - main = re.sub(r'static uint8_t firmwareVersion\[AM_FIRMWARE_VERSION_LENGTH\]\s*=\s*\{[^}]+\};', 'static uint8_t firmwareVersion[AM_FIRMWARE_VERSION_LENGTH] = {1, 12, 0};', main, count=1) - main = re.sub(r'static uint8_t firmwareDescription\[AM_FIRMWARE_DESCRIPTION_LENGTH\]\s*=\s*"[^"]+";', 'static uint8_t firmwareDescription[AM_FIRMWARE_DESCRIPTION_LENGTH] = "AudioMoth-Firmware-Basic";', main, count=1) - - if "ESP_TIME_WAIT_TIMEOUT_MS" not in main: - anchor = "#define USB_CONFIGURATION_BLINK 400" - if anchor not in main: - raise SystemExit("Missing USB_CONFIGURATION_BLINK anchor") - main = main.replace(anchor, anchor + "\n\n#define ESP_TIME_WAIT_TIMEOUT_MS 60000\n#define ESP_TIME_WAIT_STEP_MS 50\n#define ESP_TIME_SERVICE_WINDOW_SECONDS 2", 1) - - if "ESP32 time-setting path." not in main: - pattern = re.compile(r'( /\* Read the time \*/\n\n uint32_t currentTime;\n\n uint32_t currentMilliseconds;\n\n AudioMoth_getTime\(¤tTime, ¤tMilliseconds\);\n\n)( /\* Check if switch has just been moved to CUSTOM or DEFAULT \*/)', re.M) - block_lines = [ - ' /* 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) {', - '', - ' uint32_t waitedMilliseconds = 0;', - '', - ' ESPBridge_setBusy(false);', - ' ESPBridge_setUploadAllowed(false);', - '', - ' while (AudioMoth_hasTimeBeenSet() == false && 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);', - '', - ' }', - '' - ] - block = "\n".join(block_lines) + "\n" - main, n = pattern.subn(lambda m: m.group(1) + block + m.group(2), main, count=1) - if n != 1: - raise SystemExit(f"Failed to insert ESP32 time-setting path; replacements={n}") - - main = main.replace('bool shouldPerformAcousticConfiguration = switchPosition == AM_SWITCH_CUSTOM && (AudioMoth_hasTimeBeenSet() == false || configSettings->requireAcousticConfiguration);', 'bool shouldPerformAcousticConfiguration = switchPosition == AM_SWITCH_CUSTOM && configSettings->requireAcousticConfiguration;', 1) - main = main.replace('bool listenForAcousticTone = switchPosition == AM_SWITCH_CUSTOM && shouldPerformAcousticConfiguration == false;', 'bool listenForAcousticTone = false;', 1) - - bridge = bridge.replace('void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) {\n if (bridgeBusy || !uploadAllowed) return;\n if (deadlineReached(deadlineUnixSeconds)) return;', 'void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) {\n if (bridgeBusy) return;\n if (deadlineReached(deadlineUnixSeconds)) return;', 1) - - if 'static void commandDelete(char *args) {\n char path[ESPBRIDGE_MAX_PATH];\n\n if (bridgeBusy || !uploadAllowed)' not in bridge: - bridge = bridge.replace('static void commandDelete(char *args) {\n char path[ESPBRIDGE_MAX_PATH];\n', 'static void commandDelete(char *args) {\n char path[ESPBRIDGE_MAX_PATH];\n\n if (bridgeBusy || !uploadAllowed) {\n sendLine("ERR BUSY upload_not_allowed");\n return;\n }\n\n', 1) - - checks = [ - ('main identity version', 'static uint8_t firmwareVersion[AM_FIRMWARE_VERSION_LENGTH] = {1, 12, 0};' in main), - ('main identity description', 'AudioMoth-Firmware-Basic' in main), - ('main ESP constants', 'ESP_TIME_WAIT_TIMEOUT_MS' in main), - ('main ESP wait block', 'ESP32 time-setting path.' in main), - ('main no unset-time acoustic trigger', 'AudioMoth_hasTimeBeenSet() == false || configSettings->requireAcousticConfiguration' not in main), - ('main acoustic tone disabled', 'bool listenForAcousticTone = false;' in main), - ('bridge service allows non-upload commands', 'if (bridgeBusy || !uploadAllowed) return;' not in bridge), - ('bridge delete still upload gated', 'static void commandDelete(char *args) {\n char path[ESPBRIDGE_MAX_PATH];\n\n if (bridgeBusy || !uploadAllowed)' in bridge), - ] - for name, ok in checks: - print(f"{name}: {ok}") - if not ok: - raise SystemExit(f"Sanity check failed: {name}") - - main_path.write_text(main) - bridge_path.write_text(bridge) - PY - - - name: Show final bridge-relevant source - run: | - grep -n "ESP_TIME_" -A4 -B4 src/main.c - grep -n "ESP32 time-setting path" -A65 -B8 src/main.c - grep -n "shouldPerformAcousticConfiguration\|listenForAcousticTone" -A4 -B4 src/main.c - grep -n "static void commandDelete" -A25 src/espbridge.c - grep -n "void ESPBridge_serviceUntil" -A6 src/espbridge.c - - - name: Commit source patch - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - - if git diff --quiet -- src/main.c src/espbridge.c; then - echo "Source already patched." - exit 0 - fi - - git add src/main.c src/espbridge.c - git commit -m "Fix ESP bridge startup time sync" - git push origin master From 98f9743bfaf2e0ad6f70e3cb70cea851767e03ed Mon Sep 17 00:00:00 2001 From: Ash6414 Date: Tue, 9 Jun 2026 08:33:14 -0500 Subject: [PATCH 17/38] Refine permanent AudioMoth bin build workflow --- .github/workflows/build-audiomoth-bin.yml | 72 ++++++++++++++--------- 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/.github/workflows/build-audiomoth-bin.yml b/.github/workflows/build-audiomoth-bin.yml index 3ede6fe..a0601d6 100644 --- a/.github/workflows/build-audiomoth-bin.yml +++ b/.github/workflows/build-audiomoth-bin.yml @@ -1,58 +1,65 @@ -name: Build AudioMoth ESPnode firmware +name: Build AudioMoth firmware bin on: workflow_dispatch: push: branches: - master + paths: + - 'src/**' + - 'inc/**' + - '.github/workflows/build-audiomoth-bin.yml' + +permissions: + contents: read jobs: - build: + build-bin: runs-on: ubuntu-latest steps: - - name: Check out ESPnode firmware fork + - name: Check out firmware source uses: actions/checkout@v4 with: path: firmware ref: master fetch-depth: 1 - - name: Show exact fork source being built + - name: Show source revision run: | + set -euo pipefail echo "Repository: Ash6414/AudioMoth-Firmware_ESPnode" echo "Branch: master" echo "Commit: $(git -C firmware rev-parse HEAD)" - echo "espbridge.c busy implementation:" - grep -n "ESPBridge_setBusy" -A8 firmware/src/espbridge.c - if grep -R "GPIO_PinOutWrite" -n firmware/src firmware/inc; then - echo "ERROR: fork source still contains GPIO_PinOutWrite" - exit 1 - fi + test -f firmware/src/main.c + test -f firmware/src/espbridge.c + grep -n "ESP_TIME_WAIT_TIMEOUT_MS" firmware/src/main.c + grep -n "ESPBridge_serviceUntil" -A3 firmware/src/espbridge.c - - name: Install ARM toolchain and build tools + - name: Install ARM GCC toolchain run: | sudo apt-get update - sudo apt-get install -y git make gcc-arm-none-eabi + sudo apt-get install -y --no-install-recommends \ + git \ + make \ + gcc-arm-none-eabi \ + binutils-arm-none-eabi - - name: Clone AudioMoth-Project build framework + - name: Clone AudioMoth build framework run: | git clone --depth 1 https://github.com/OpenAcousticDevices/AudioMoth-Project.git project - - name: Copy fork source into AudioMoth-Project without editing it + - name: Overlay repository files into build framework run: | - cp -f firmware/src/main.c project/src/main.c + set -euo pipefail cp -f firmware/src/*.c project/src/ - cp -f firmware/inc/*.h project/inc/ - echo "Build-tree espbridge.c busy implementation:" - grep -n "ESPBridge_setBusy" -A8 project/src/espbridge.c - if grep -R "GPIO_PinOutWrite" -n project/src project/inc; then - echo "ERROR: build tree source still contains GPIO_PinOutWrite" - exit 1 + if [ -d firmware/inc ]; then + cp -f firmware/inc/*.h project/inc/ fi - - name: Configure AudioMoth-Project Makefile for CI only + - name: Configure CI Makefile paths run: | + set -euo pipefail sed -i 's|^TOOLCHAIN_PATH =.*|TOOLCHAIN_PATH = /usr|' project/build/Makefile sed -i 's|^TOOLCHAIN_VERSION =.*|TOOLCHAIN_VERSION = |' project/build/Makefile sed -i 's|^INC =.*|INC = ../cmsis ../device/inc ../emlib/inc ../emusb/inc ../drivers/inc ../fatfs/inc ../gps/inc ../inc|' project/build/Makefile @@ -61,15 +68,24 @@ jobs: - name: Build firmware binary working-directory: project/build run: | + set -euo pipefail make clean make audiomoth.bin + - name: Verify binary output + run: | + set -euo pipefail + test -s project/build/audiomoth.bin + ls -lh project/build/audiomoth.bin + 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 + - name: Upload firmware artifact uses: actions/upload-artifact@v4 with: - name: audiomoth-espnode-firmware - path: | - project/build/audiomoth.bin - project/build/audiomoth.hex - project/build/audiomoth.map - project/build/audiomoth.lst + name: audiomoth-firmware-bin + path: firmware-artifacts/ + if-no-files-found: error From 21227c9bb7f5406dc2d0a4d3f1335e6888d866c1 Mon Sep 17 00:00:00 2001 From: Ash6414 Date: Tue, 9 Jun 2026 08:39:03 -0500 Subject: [PATCH 18/38] Install newlib headers in firmware bin workflow --- .github/workflows/build-audiomoth-bin.yml | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-audiomoth-bin.yml b/.github/workflows/build-audiomoth-bin.yml index a0601d6..f28d9c4 100644 --- a/.github/workflows/build-audiomoth-bin.yml +++ b/.github/workflows/build-audiomoth-bin.yml @@ -13,6 +13,10 @@ on: permissions: contents: read +concurrency: + group: audiomoth-firmware-build-${{ github.ref }} + cancel-in-progress: true + jobs: build-bin: runs-on: ubuntu-latest @@ -36,17 +40,23 @@ jobs: grep -n "ESP_TIME_WAIT_TIMEOUT_MS" firmware/src/main.c grep -n "ESPBridge_serviceUntil" -A3 firmware/src/espbridge.c - - name: Install ARM GCC toolchain + - name: Install ARM GCC and newlib 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 + binutils-arm-none-eabi \ + libnewlib-dev \ + libnewlib-arm-none-eabi + arm-none-eabi-gcc --version + printf '#include \nint main(void){return 0;}\n' | arm-none-eabi-gcc -x c -E - >/dev/null - name: Clone AudioMoth build framework run: | + set -euo pipefail git clone --depth 1 https://github.com/OpenAcousticDevices/AudioMoth-Project.git project - name: Overlay repository files into build framework @@ -56,14 +66,19 @@ jobs: if [ -d firmware/inc ]; then cp -f firmware/inc/*.h project/inc/ fi + echo "Build-tree bridge guard:" + grep -n "ESPBridge_serviceUntil" -A3 project/src/espbridge.c + echo "Build-tree time-wait constants:" + grep -n "ESP_TIME_WAIT_TIMEOUT_MS" project/src/main.c - - name: Configure CI Makefile paths + - name: Configure AudioMoth Makefile for CI run: | set -euo pipefail sed -i 's|^TOOLCHAIN_PATH =.*|TOOLCHAIN_PATH = /usr|' project/build/Makefile sed -i 's|^TOOLCHAIN_VERSION =.*|TOOLCHAIN_VERSION = |' project/build/Makefile sed -i 's|^INC =.*|INC = ../cmsis ../device/inc ../emlib/inc ../emusb/inc ../drivers/inc ../fatfs/inc ../gps/inc ../inc|' project/build/Makefile sed -i 's|^SRC =.*|SRC = ../device/src ../emlib/src ../emusb/src ../drivers/src ../fatfs/src ../gps/src ../src|' project/build/Makefile + grep -n "^TOOLCHAIN_PATH\|^TOOLCHAIN_VERSION\|^INC =\|^SRC =" project/build/Makefile - name: Build firmware binary working-directory: project/build @@ -76,12 +91,12 @@ jobs: run: | set -euo pipefail test -s project/build/audiomoth.bin - ls -lh project/build/audiomoth.bin 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 + ls -lh firmware-artifacts/ - name: Upload firmware artifact uses: actions/upload-artifact@v4 From 08a1678d5cb66d4b5eac712b3bd7fdcf9f6c21a0 Mon Sep 17 00:00:00 2001 From: Ash6414 Date: Tue, 9 Jun 2026 08:39:24 -0500 Subject: [PATCH 19/38] Add one-time ESP time-wait patch workflow --- .../workflows/patch-esp-time-wait-once.yml | 234 ++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 .github/workflows/patch-esp-time-wait-once.yml diff --git a/.github/workflows/patch-esp-time-wait-once.yml b/.github/workflows/patch-esp-time-wait-once.yml new file mode 100644 index 0000000..682b8c3 --- /dev/null +++ b/.github/workflows/patch-esp-time-wait-once.yml @@ -0,0 +1,234 @@ +name: Patch ESP time wait once + +on: + workflow_dispatch: + push: + branches: + - master + paths: + - .github/workflows/patch-esp-time-wait-once.yml + +permissions: + contents: write + +jobs: + patch-esp-time-wait: + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + ref: master + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Patch AudioMoth ESP time-wait behavior + run: | + python3 - <<'PY' + from pathlib import Path + import re + + main_path = Path("src/main.c") + bridge_path = Path("src/espbridge.c") + + main = main_path.read_text() + bridge = bridge_path.read_text() + + # ------------------------------------------------------------------ + # 1) src/espbridge.c: allow PING/STATUS/TIME/DONE service even when + # uploadAllowed is false. LIST/GET already reject when uploadAllowed + # is false. Patch DELETE to do the same. + # ------------------------------------------------------------------ + old_delete_pattern = re.compile( + r'static void commandDelete\(char \*args\) \{.*?\n\}\n\nstatic void commandTime', + re.S, + ) + + new_delete = '''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''' + + bridge2, delete_count = old_delete_pattern.subn(new_delete, bridge, count=1) + if delete_count != 1: + raise SystemExit(f"Failed to patch commandDelete; patched {delete_count} instances") + + old_service_guard = """void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) { + if (bridgeBusy || !uploadAllowed) return; + if (deadlineReached(deadlineUnixSeconds)) return;""" + + new_service_guard = """void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) { + if (bridgeBusy) return; + if (deadlineReached(deadlineUnixSeconds)) return;""" + + if old_service_guard not in bridge2 and new_service_guard not in bridge2: + raise SystemExit("Could not find ESPBridge_serviceUntil guard") + bridge2 = bridge2.replace(old_service_guard, new_service_guard, 1) + + # ------------------------------------------------------------------ + # 2) src/main.c: define ESP time-wait constants. + # ------------------------------------------------------------------ + if "ESP_TIME_WAIT_TIMEOUT_MS" not in main: + anchor = "#define USB_CONFIGURATION_BLINK 400" + replacement = anchor + "\n\n#define ESP_TIME_WAIT_TIMEOUT_MS 60000\n#define ESP_TIME_WAIT_STEP_MS 50\n#define ESP_TIME_SERVICE_WINDOW_SECONDS 2" + if anchor not in main: + raise SystemExit("Could not find USB_CONFIGURATION_BLINK anchor") + main = main.replace(anchor, replacement, 1) + + # ------------------------------------------------------------------ + # 3) src/main.c: when time is unset in CUSTOM, wait for ESP32 TIME + # instead of falling into acoustic configuration. + # ------------------------------------------------------------------ + if "ESP32 time-setting path" not in main: + read_time_block = """ /* Read the time */ + + uint32_t currentTime; + + uint32_t currentMilliseconds; + + AudioMoth_getTime(¤tTime, ¤tMilliseconds); + + /* Check if switch has just been moved to CUSTOM or DEFAULT */""" + + esp_time_block = """ /* 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) { + + uint32_t waitedMilliseconds = 0; + + ESPBridge_setBusy(false); + ESPBridge_setUploadAllowed(false); + + while (AudioMoth_hasTimeBeenSet() == false && waitedMilliseconds < ESP_TIME_WAIT_TIMEOUT_MS) { + + 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 */""" + + if read_time_block not in main: + raise SystemExit("Could not find first read-time block in main.c") + main = main.replace(read_time_block, esp_time_block, 1) + + old_acoustic = "bool shouldPerformAcousticConfiguration = switchPosition == AM_SWITCH_CUSTOM && (AudioMoth_hasTimeBeenSet() == false || configSettings->requireAcousticConfiguration);" + new_acoustic = "bool shouldPerformAcousticConfiguration = switchPosition == AM_SWITCH_CUSTOM && configSettings->requireAcousticConfiguration;" + + if old_acoustic not in main and new_acoustic not in main: + raise SystemExit("Could not find acoustic configuration condition") + main = main.replace(old_acoustic, new_acoustic, 1) + + main_path.write_text(main) + bridge_path.write_text(bridge2) + PY + + - name: Show patched snippets + run: | + echo "main.c time wait constants:" + grep -n "ESP_TIME_" -A3 -B3 src/main.c + echo + echo "main.c ESP32 time path:" + grep -n "ESP32 time-setting path" -A55 -B8 src/main.c + echo + echo "main.c acoustic config condition:" + grep -n "shouldPerformAcousticConfiguration" -A2 -B2 src/main.c + echo + echo "espbridge.c service guard:" + grep -n "void ESPBridge_serviceUntil" -A5 src/espbridge.c + echo + echo "espbridge.c DELETE guard:" + grep -n "static void commandDelete" -A18 src/espbridge.c + + - name: Commit patched firmware code if changed + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + if git diff --quiet -- src/main.c src/espbridge.c; then + echo "No firmware source changes needed." + exit 0 + fi + + git add src/main.c src/espbridge.c + git commit -m "Wait for ESP32 time before acoustic configuration" + git push origin master From 612e8958420d64be2166e9e313b3201d3f214101 Mon Sep 17 00:00:00 2001 From: Ash6414 Date: Tue, 9 Jun 2026 08:41:37 -0500 Subject: [PATCH 20/38] Remove obsolete patch workflow --- .../workflows/patch-esp-time-wait-once.yml | 234 ------------------ 1 file changed, 234 deletions(-) delete mode 100644 .github/workflows/patch-esp-time-wait-once.yml diff --git a/.github/workflows/patch-esp-time-wait-once.yml b/.github/workflows/patch-esp-time-wait-once.yml deleted file mode 100644 index 682b8c3..0000000 --- a/.github/workflows/patch-esp-time-wait-once.yml +++ /dev/null @@ -1,234 +0,0 @@ -name: Patch ESP time wait once - -on: - workflow_dispatch: - push: - branches: - - master - paths: - - .github/workflows/patch-esp-time-wait-once.yml - -permissions: - contents: write - -jobs: - patch-esp-time-wait: - runs-on: ubuntu-latest - - steps: - - name: Check out repository - uses: actions/checkout@v4 - with: - ref: master - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Patch AudioMoth ESP time-wait behavior - run: | - python3 - <<'PY' - from pathlib import Path - import re - - main_path = Path("src/main.c") - bridge_path = Path("src/espbridge.c") - - main = main_path.read_text() - bridge = bridge_path.read_text() - - # ------------------------------------------------------------------ - # 1) src/espbridge.c: allow PING/STATUS/TIME/DONE service even when - # uploadAllowed is false. LIST/GET already reject when uploadAllowed - # is false. Patch DELETE to do the same. - # ------------------------------------------------------------------ - old_delete_pattern = re.compile( - r'static void commandDelete\(char \*args\) \{.*?\n\}\n\nstatic void commandTime', - re.S, - ) - - new_delete = '''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''' - - bridge2, delete_count = old_delete_pattern.subn(new_delete, bridge, count=1) - if delete_count != 1: - raise SystemExit(f"Failed to patch commandDelete; patched {delete_count} instances") - - old_service_guard = """void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) { - if (bridgeBusy || !uploadAllowed) return; - if (deadlineReached(deadlineUnixSeconds)) return;""" - - new_service_guard = """void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) { - if (bridgeBusy) return; - if (deadlineReached(deadlineUnixSeconds)) return;""" - - if old_service_guard not in bridge2 and new_service_guard not in bridge2: - raise SystemExit("Could not find ESPBridge_serviceUntil guard") - bridge2 = bridge2.replace(old_service_guard, new_service_guard, 1) - - # ------------------------------------------------------------------ - # 2) src/main.c: define ESP time-wait constants. - # ------------------------------------------------------------------ - if "ESP_TIME_WAIT_TIMEOUT_MS" not in main: - anchor = "#define USB_CONFIGURATION_BLINK 400" - replacement = anchor + "\n\n#define ESP_TIME_WAIT_TIMEOUT_MS 60000\n#define ESP_TIME_WAIT_STEP_MS 50\n#define ESP_TIME_SERVICE_WINDOW_SECONDS 2" - if anchor not in main: - raise SystemExit("Could not find USB_CONFIGURATION_BLINK anchor") - main = main.replace(anchor, replacement, 1) - - # ------------------------------------------------------------------ - # 3) src/main.c: when time is unset in CUSTOM, wait for ESP32 TIME - # instead of falling into acoustic configuration. - # ------------------------------------------------------------------ - if "ESP32 time-setting path" not in main: - read_time_block = """ /* Read the time */ - - uint32_t currentTime; - - uint32_t currentMilliseconds; - - AudioMoth_getTime(¤tTime, ¤tMilliseconds); - - /* Check if switch has just been moved to CUSTOM or DEFAULT */""" - - esp_time_block = """ /* 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) { - - uint32_t waitedMilliseconds = 0; - - ESPBridge_setBusy(false); - ESPBridge_setUploadAllowed(false); - - while (AudioMoth_hasTimeBeenSet() == false && waitedMilliseconds < ESP_TIME_WAIT_TIMEOUT_MS) { - - 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 */""" - - if read_time_block not in main: - raise SystemExit("Could not find first read-time block in main.c") - main = main.replace(read_time_block, esp_time_block, 1) - - old_acoustic = "bool shouldPerformAcousticConfiguration = switchPosition == AM_SWITCH_CUSTOM && (AudioMoth_hasTimeBeenSet() == false || configSettings->requireAcousticConfiguration);" - new_acoustic = "bool shouldPerformAcousticConfiguration = switchPosition == AM_SWITCH_CUSTOM && configSettings->requireAcousticConfiguration;" - - if old_acoustic not in main and new_acoustic not in main: - raise SystemExit("Could not find acoustic configuration condition") - main = main.replace(old_acoustic, new_acoustic, 1) - - main_path.write_text(main) - bridge_path.write_text(bridge2) - PY - - - name: Show patched snippets - run: | - echo "main.c time wait constants:" - grep -n "ESP_TIME_" -A3 -B3 src/main.c - echo - echo "main.c ESP32 time path:" - grep -n "ESP32 time-setting path" -A55 -B8 src/main.c - echo - echo "main.c acoustic config condition:" - grep -n "shouldPerformAcousticConfiguration" -A2 -B2 src/main.c - echo - echo "espbridge.c service guard:" - grep -n "void ESPBridge_serviceUntil" -A5 src/espbridge.c - echo - echo "espbridge.c DELETE guard:" - grep -n "static void commandDelete" -A18 src/espbridge.c - - - name: Commit patched firmware code if changed - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - - if git diff --quiet -- src/main.c src/espbridge.c; then - echo "No firmware source changes needed." - exit 0 - fi - - git add src/main.c src/espbridge.c - git commit -m "Wait for ESP32 time before acoustic configuration" - git push origin master From e78d4fed4d7d3be76bff10329e96eb9d0b471428 Mon Sep 17 00:00:00 2001 From: Ash6414 Date: Tue, 9 Jun 2026 08:43:03 -0500 Subject: [PATCH 21/38] Fix ESP time-wait patch workflow YAML --- .../workflows/patch-esp-time-wait-once.yml | 228 ++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 .github/workflows/patch-esp-time-wait-once.yml diff --git a/.github/workflows/patch-esp-time-wait-once.yml b/.github/workflows/patch-esp-time-wait-once.yml new file mode 100644 index 0000000..355c171 --- /dev/null +++ b/.github/workflows/patch-esp-time-wait-once.yml @@ -0,0 +1,228 @@ +name: Patch ESP time wait once + +on: + workflow_dispatch: + +permissions: + contents: write + +jobs: + patch-esp-time-wait: + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + ref: master + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Patch AudioMoth ESP time-wait behavior + run: | + python3 - <<'PY' + from pathlib import Path + import re + import textwrap + + main_path = Path("src/main.c") + bridge_path = Path("src/espbridge.c") + + main = main_path.read_text() + bridge = bridge_path.read_text() + + old_delete_pattern = re.compile( + r'static void commandDelete\(char \*args\) \{.*?\n\}\n\nstatic void commandTime', + re.S, + ) + + new_delete = textwrap.dedent("""\ + 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""") + + bridge2, delete_count = old_delete_pattern.subn(new_delete, bridge, count=1) + if delete_count != 1: + raise SystemExit(f"Failed to patch commandDelete; patched {delete_count} instances") + + old_service_guard = textwrap.dedent("""\ + void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) { + if (bridgeBusy || !uploadAllowed) return; + if (deadlineReached(deadlineUnixSeconds)) return;""") + + new_service_guard = textwrap.dedent("""\ + void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) { + if (bridgeBusy) return; + if (deadlineReached(deadlineUnixSeconds)) return;""") + + if old_service_guard not in bridge2 and new_service_guard not in bridge2: + raise SystemExit("Could not find ESPBridge_serviceUntil guard") + bridge2 = bridge2.replace(old_service_guard, new_service_guard, 1) + + if "ESP_TIME_WAIT_TIMEOUT_MS" not in main: + anchor = "#define USB_CONFIGURATION_BLINK 400" + replacement = ( + anchor + + "\n\n#define ESP_TIME_WAIT_TIMEOUT_MS 60000" + + "\n#define ESP_TIME_WAIT_STEP_MS 50" + + "\n#define ESP_TIME_SERVICE_WINDOW_SECONDS 2" + ) + if anchor not in main: + raise SystemExit("Could not find USB_CONFIGURATION_BLINK anchor") + main = main.replace(anchor, replacement, 1) + + if "ESP32 time-setting path" not in main: + read_time_block = textwrap.dedent("""\ + /* Read the time */ + + uint32_t currentTime; + + uint32_t currentMilliseconds; + + AudioMoth_getTime(¤tTime, ¤tMilliseconds); + + /* Check if switch has just been moved to CUSTOM or DEFAULT */""") + + esp_time_block = textwrap.dedent("""\ + /* 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) { + + uint32_t waitedMilliseconds = 0; + + ESPBridge_setBusy(false); + ESPBridge_setUploadAllowed(false); + + while (AudioMoth_hasTimeBeenSet() == false && waitedMilliseconds < ESP_TIME_WAIT_TIMEOUT_MS) { + + 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 */""") + + if read_time_block not in main: + raise SystemExit("Could not find first read-time block in main.c") + main = main.replace(read_time_block, esp_time_block, 1) + + old_acoustic = "bool shouldPerformAcousticConfiguration = switchPosition == AM_SWITCH_CUSTOM && (AudioMoth_hasTimeBeenSet() == false || configSettings->requireAcousticConfiguration);" + new_acoustic = "bool shouldPerformAcousticConfiguration = switchPosition == AM_SWITCH_CUSTOM && configSettings->requireAcousticConfiguration;" + + if old_acoustic not in main and new_acoustic not in main: + raise SystemExit("Could not find acoustic configuration condition") + main = main.replace(old_acoustic, new_acoustic, 1) + + main_path.write_text(main) + bridge_path.write_text(bridge2) + PY + + - name: Show patched snippets + run: | + echo "main.c time wait constants:" + grep -n "ESP_TIME_" -A3 -B3 src/main.c + echo + echo "main.c ESP32 time path:" + grep -n "ESP32 time-setting path" -A55 -B8 src/main.c + echo + echo "main.c acoustic config condition:" + grep -n "shouldPerformAcousticConfiguration" -A2 -B2 src/main.c + echo + echo "espbridge.c service guard:" + grep -n "void ESPBridge_serviceUntil" -A5 src/espbridge.c + echo + echo "espbridge.c DELETE guard:" + grep -n "static void commandDelete" -A18 src/espbridge.c + + - name: Commit patched firmware code if changed + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + if git diff --quiet -- src/main.c src/espbridge.c; then + echo "No firmware source changes needed." + exit 0 + fi + + git add src/main.c src/espbridge.c + git commit -m "Wait for ESP32 time before acoustic configuration" + git push origin master From c43f288560be3fdf3d3b5ce1c11a936d9c74acb7 Mon Sep 17 00:00:00 2001 From: Ash6414 Date: Tue, 9 Jun 2026 08:49:51 -0500 Subject: [PATCH 22/38] Add temporary ESPBridge fix and build workflow --- .../temp-fix-and-build-espbridge.yml | 185 ++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 .github/workflows/temp-fix-and-build-espbridge.yml diff --git a/.github/workflows/temp-fix-and-build-espbridge.yml b/.github/workflows/temp-fix-and-build-espbridge.yml new file mode 100644 index 0000000..86510e6 --- /dev/null +++ b/.github/workflows/temp-fix-and-build-espbridge.yml @@ -0,0 +1,185 @@ +name: TEMP Fix and Build ESPBridge + +on: + workflow_dispatch: + push: + branches: + - master + paths: + - '.github/workflows/temp-fix-and-build-espbridge.yml' + +permissions: + contents: write + +env: + BUILD_FRAMEWORK_REPO: https://github.com/OpenAcousticDevices/AudioMoth-Project.git + +jobs: + fix-and-build: + runs-on: ubuntu-latest + + steps: + - name: Check out firmware repository + uses: actions/checkout@v4 + with: + ref: master + fetch-depth: 1 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Patch ESPBridge source if needed + run: | + set -euo pipefail + python3 - <<'PY' + from pathlib import Path + + bridge_path = Path('src/espbridge.c') + main_path = Path('src/main.c') + + bridge = bridge_path.read_text() + main = main_path.read_text() + + if 'static inline void gpioWrite(' not in bridge: + marker = 'static uint8_t chunkBuffer[ESPBRIDGE_CHUNK_BYTES];\n' + helper = """ + static inline void gpioWrite(GPIO_Port_TypeDef port, unsigned int pin, bool value) { + if (value) { + GPIO_PinOutSet(port, pin); + } else { + GPIO_PinOutClear(port, pin); + } + } + """ + if marker not in bridge: + raise SystemExit('Could not find chunkBuffer marker for gpioWrite helper') + bridge = bridge.replace(marker, marker + helper + '\n', 1) + + bridge = bridge.replace( + 'GPIO_PinOutWrite(BRIDGE_BUSY_PORT, BRIDGE_BUSY_PIN, busy ? 1 : 0);', + 'gpioWrite(BRIDGE_BUSY_PORT, BRIDGE_BUSY_PIN, busy);' + ) + bridge = bridge.replace( + 'GPIO_PinOutWrite(BRIDGE_BUSY_PORT, BRIDGE_BUSY_PIN, busy);', + 'gpioWrite(BRIDGE_BUSY_PORT, BRIDGE_BUSY_PIN, busy);' + ) + + bridge = bridge.replace( + 'void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) {\n if (bridgeBusy || !uploadAllowed) return;', + 'void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) {\n if (bridgeBusy) return;', + 1, + ) + + if 'static void commandDelete(char *args) {\n char path[ESPBRIDGE_MAX_PATH];\n\n if (bridgeBusy || !uploadAllowed)' not in bridge: + bridge = bridge.replace( + 'static void commandDelete(char *args) {\n char path[ESPBRIDGE_MAX_PATH];\n', + 'static void commandDelete(char *args) {\n char path[ESPBRIDGE_MAX_PATH];\n\n if (bridgeBusy || !uploadAllowed) {\n sendLine("ERR BUSY upload_not_allowed");\n return;\n }\n\n', + 1, + ) + + main = main.replace( + 'bool shouldPerformAcousticConfiguration = switchPosition == AM_SWITCH_CUSTOM && (AudioMoth_hasTimeBeenSet() == false || configSettings->requireAcousticConfiguration);', + 'bool shouldPerformAcousticConfiguration = switchPosition == AM_SWITCH_CUSTOM && configSettings->requireAcousticConfiguration;', + 1, + ) + main = main.replace( + 'bool listenForAcousticTone = switchPosition == AM_SWITCH_CUSTOM && shouldPerformAcousticConfiguration == false;', + 'bool listenForAcousticTone = false;', + 1, + ) + + checks = [ + ('no GPIO_PinOutWrite', 'GPIO_PinOutWrite' not in bridge), + ('gpioWrite helper present', 'static inline void gpioWrite(' in bridge), + ('service allows TIME before upload', 'if (bridgeBusy || !uploadAllowed) return;' not in bridge), + ('DELETE remains upload gated', 'if (bridgeBusy || !uploadAllowed)' in bridge), + ('main has ESP time wait constants', 'ESP_TIME_WAIT_TIMEOUT_MS' in main), + ('main has ESP time wait block', 'ESP32 time-setting path.' in main), + ('acoustic tone disabled', 'bool listenForAcousticTone = false;' in main), + ] + for name, ok in checks: + print(f'{name}: {ok}') + if not ok: + raise SystemExit(f'Failed source check: {name}') + + bridge_path.write_text(bridge) + main_path.write_text(main) + PY + + - name: Commit source patch if changed + run: | + set -euo pipefail + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + if git diff --quiet -- src/main.c src/espbridge.c; then + echo 'Source already contained required ESPBridge fixes.' + else + git diff -- src/main.c src/espbridge.c + git add src/main.c src/espbridge.c + git commit -m 'Fix AudioMoth ESPBridge source' + git push origin master + fi + + - name: Install build dependencies + 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-dev \ + libnewlib-arm-none-eabi + arm-none-eabi-gcc --version + printf '#include \nint main(void){return 0;}\n' | arm-none-eabi-gcc -x c -E - >/dev/null + + - name: Clone AudioMoth build framework + run: | + set -euo pipefail + git clone --depth 1 "$BUILD_FRAMEWORK_REPO" project + + - name: Overlay repository source into build framework + run: | + set -euo pipefail + cp -f src/*.c project/src/ + if [ -d inc ]; then + cp -f inc/*.h project/inc/ + fi + if grep -R "GPIO_PinOutWrite" -n project/src project/inc; then + echo 'ERROR: GPIO_PinOutWrite still exists in build tree.' + exit 1 + fi + grep -n "ESPBridge_setBusy" -A8 project/src/espbridge.c + grep -n "ESPBridge_serviceUntil" -A4 project/src/espbridge.c + + - name: Configure Makefile + run: | + set -euo pipefail + sed -i 's|^TOOLCHAIN_PATH =.*|TOOLCHAIN_PATH = /usr|' project/build/Makefile + sed -i 's|^TOOLCHAIN_VERSION =.*|TOOLCHAIN_VERSION = |' project/build/Makefile + sed -i 's|^INC =.*|INC = ../cmsis ../device/inc ../emlib/inc ../emusb/inc ../drivers/inc ../fatfs/inc ../gps/inc ../inc|' project/build/Makefile + sed -i 's|^SRC =.*|SRC = ../device/src ../emlib/src ../emusb/src ../drivers/src ../fatfs/src ../gps/src ../src|' project/build/Makefile + + - name: Build AudioMoth firmware + working-directory: project/build + run: | + set -euo pipefail + make clean + make audiomoth.bin + + - name: Package firmware outputs + run: | + set -euo pipefail + test -s project/build/audiomoth.bin + 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 + ls -lh firmware-artifacts + + - name: Upload firmware artifact + uses: actions/upload-artifact@v4 + with: + name: audiomoth-espbridge-fixed-firmware + path: firmware-artifacts/ + if-no-files-found: error From 78540d5f221f57158b8ad2a279164fed0c720774 Mon Sep 17 00:00:00 2001 From: Ash6414 Date: Tue, 9 Jun 2026 09:06:13 -0500 Subject: [PATCH 23/38] Add temporary request-aware bridge patch workflow --- .../temp-request-aware-bridge-patch.yml | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 .github/workflows/temp-request-aware-bridge-patch.yml diff --git a/.github/workflows/temp-request-aware-bridge-patch.yml b/.github/workflows/temp-request-aware-bridge-patch.yml new file mode 100644 index 0000000..1e80d44 --- /dev/null +++ b/.github/workflows/temp-request-aware-bridge-patch.yml @@ -0,0 +1,56 @@ +name: TEMP Request-Aware Bridge Patch + +on: + workflow_dispatch: + +permissions: + contents: write + +jobs: + patch: + runs-on: ubuntu-latest + steps: + - name: Check out firmware repository + uses: actions/checkout@v5 + with: + ref: master + fetch-depth: 1 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Make AudioMoth service ESP bridge when request is active + run: | + set -euo pipefail + python3 - <<'PY' + from pathlib import Path + p = Path('src/main.c') + s = p.read_text() + s = s.replace( + 'if (switchPosition == AM_SWITCH_CUSTOM && AudioMoth_hasTimeBeenSet() == false) {', + 'if (switchPosition == AM_SWITCH_CUSTOM && (AudioMoth_hasTimeBeenSet() == false || ESPBridge_isRequestActive())) {', + 1, + ) + s = s.replace( + 'while (AudioMoth_hasTimeBeenSet() == false && waitedMilliseconds < ESP_TIME_WAIT_TIMEOUT_MS) {', + 'while ((AudioMoth_hasTimeBeenSet() == false || ESPBridge_isRequestActive()) && waitedMilliseconds < ESP_TIME_WAIT_TIMEOUT_MS) {', + 1, + ) + if 'if (switchPosition == AM_SWITCH_CUSTOM && (AudioMoth_hasTimeBeenSet() == false || ESPBridge_isRequestActive())) {' not in s: + raise SystemExit('request-aware if condition missing') + if 'while ((AudioMoth_hasTimeBeenSet() == false || ESPBridge_isRequestActive()) && waitedMilliseconds < ESP_TIME_WAIT_TIMEOUT_MS) {' not in s: + raise SystemExit('request-aware while condition missing') + p.write_text(s) + PY + + - name: Commit patch + run: | + set -euo pipefail + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + if git diff --quiet -- src/main.c; then + echo 'No change needed.' + else + git diff -- src/main.c + git add src/main.c + git commit -m 'Service ESP bridge when request is active' + git push origin master + fi From 9c4e43315c3187ed46cb62c0755e7e22961d0871 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 14:11:00 +0000 Subject: [PATCH 24/38] Service ESP bridge when request is active --- src/main.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main.c b/src/main.c index faf0f60..5d54262 100644 --- a/src/main.c +++ b/src/main.c @@ -1831,14 +1831,14 @@ int main(void) { * rejected because uploadAllowed is false. */ - if (switchPosition == AM_SWITCH_CUSTOM && AudioMoth_hasTimeBeenSet() == 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 && waitedMilliseconds < ESP_TIME_WAIT_TIMEOUT_MS) { + while ((AudioMoth_hasTimeBeenSet() == false || ESPBridge_isRequestActive()) && waitedMilliseconds < ESP_TIME_WAIT_TIMEOUT_MS) { AudioMoth_feedWatchdog(); From 1ab0980d8f2deecd6696110e1f6b803e3c49b293 Mon Sep 17 00:00:00 2001 From: Ash6414 Date: Tue, 9 Jun 2026 09:37:32 -0500 Subject: [PATCH 25/38] Fix ESP bridge patch workflow and trigger source patch --- .../workflows/patch-esp-time-wait-once.yml | 268 ++++++++---------- 1 file changed, 123 insertions(+), 145 deletions(-) diff --git a/.github/workflows/patch-esp-time-wait-once.yml b/.github/workflows/patch-esp-time-wait-once.yml index 355c171..e32f1ad 100644 --- a/.github/workflows/patch-esp-time-wait-once.yml +++ b/.github/workflows/patch-esp-time-wait-once.yml @@ -1,13 +1,18 @@ -name: Patch ESP time wait once +name: Patch ESP bridge source now on: workflow_dispatch: + push: + branches: + - master + paths: + - .github/workflows/patch-esp-time-wait-once.yml permissions: contents: write jobs: - patch-esp-time-wait: + patch-esp-bridge: runs-on: ubuntu-latest steps: @@ -17,12 +22,11 @@ jobs: ref: master token: ${{ secrets.GITHUB_TOKEN }} - - name: Patch AudioMoth ESP time-wait behavior + - name: Patch main.c and espbridge.c run: | python3 - <<'PY' from pathlib import Path import re - import textwrap main_path = Path("src/main.c") bridge_path = Path("src/espbridge.c") @@ -30,199 +34,173 @@ jobs: main = main_path.read_text() bridge = bridge_path.read_text() - old_delete_pattern = re.compile( - r'static void commandDelete\(char \*args\) \{.*?\n\}\n\nstatic void commandTime', - re.S, + main = re.sub( + r'static uint8_t firmwareVersion\[AM_FIRMWARE_VERSION_LENGTH\]\s*=\s*\{[^}]+\};', + 'static uint8_t firmwareVersion[AM_FIRMWARE_VERSION_LENGTH] = {1, 12, 0};', + main, + count=1, ) - new_delete = textwrap.dedent("""\ - 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""") - - bridge2, delete_count = old_delete_pattern.subn(new_delete, bridge, count=1) - if delete_count != 1: - raise SystemExit(f"Failed to patch commandDelete; patched {delete_count} instances") - - old_service_guard = textwrap.dedent("""\ - void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) { - if (bridgeBusy || !uploadAllowed) return; - if (deadlineReached(deadlineUnixSeconds)) return;""") - - new_service_guard = textwrap.dedent("""\ - void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) { - if (bridgeBusy) return; - if (deadlineReached(deadlineUnixSeconds)) return;""") - - if old_service_guard not in bridge2 and new_service_guard not in bridge2: - raise SystemExit("Could not find ESPBridge_serviceUntil guard") - bridge2 = bridge2.replace(old_service_guard, new_service_guard, 1) + main = re.sub( + r'static uint8_t firmwareDescription\[AM_FIRMWARE_DESCRIPTION_LENGTH\]\s*=\s*"[^"]+";', + 'static uint8_t firmwareDescription[AM_FIRMWARE_DESCRIPTION_LENGTH] = "AudioMoth-Firmware-Basic";', + main, + count=1, + ) if "ESP_TIME_WAIT_TIMEOUT_MS" not in main: anchor = "#define USB_CONFIGURATION_BLINK 400" - replacement = ( + if anchor not in main: + raise SystemExit("Missing USB_CONFIGURATION_BLINK anchor") + main = main.replace( + anchor, anchor + "\n\n#define ESP_TIME_WAIT_TIMEOUT_MS 60000" + "\n#define ESP_TIME_WAIT_STEP_MS 50" - + "\n#define ESP_TIME_SERVICE_WINDOW_SECONDS 2" + + "\n#define ESP_TIME_SERVICE_WINDOW_SECONDS 2", + 1, ) - if anchor not in main: - raise SystemExit("Could not find USB_CONFIGURATION_BLINK anchor") - main = main.replace(anchor, replacement, 1) - - if "ESP32 time-setting path" not in main: - read_time_block = textwrap.dedent("""\ - /* Read the time */ - - uint32_t currentTime; - - uint32_t currentMilliseconds; - - AudioMoth_getTime(¤tTime, ¤tMilliseconds); - /* Check if switch has just been moved to CUSTOM or DEFAULT */""") + if "ESP32 time-setting path." not in main: + pattern = re.compile( + r'( /\* Read the time \*/\n\n' + r' uint32_t currentTime;\n\n' + r' uint32_t currentMilliseconds;\n\n' + r' AudioMoth_getTime\(¤tTime, ¤tMilliseconds\);\n\n)' + r'( /\* Check if switch has just been moved to CUSTOM or DEFAULT \*/)', + re.M, + ) - esp_time_block = textwrap.dedent("""\ - /* Read the time */ + block = r''' + /* 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. + */ - uint32_t currentTime; + if (switchPosition == AM_SWITCH_CUSTOM && AudioMoth_hasTimeBeenSet() == false) { - uint32_t currentMilliseconds; + uint32_t waitedMilliseconds = 0; - AudioMoth_getTime(¤tTime, ¤tMilliseconds); + ESPBridge_setBusy(false); + ESPBridge_setUploadAllowed(false); - /* 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. - */ + while (AudioMoth_hasTimeBeenSet() == false && waitedMilliseconds < ESP_TIME_WAIT_TIMEOUT_MS) { - if (switchPosition == AM_SWITCH_CUSTOM && AudioMoth_hasTimeBeenSet() == false) { + AudioMoth_feedWatchdog(); - uint32_t waitedMilliseconds = 0; + if (ESPBridge_isRequestActive()) { - ESPBridge_setBusy(false); - ESPBridge_setUploadAllowed(false); + uint32_t bridgeCurrentTime; + uint32_t bridgeCurrentMilliseconds; - while (AudioMoth_hasTimeBeenSet() == false && waitedMilliseconds < ESP_TIME_WAIT_TIMEOUT_MS) { + AudioMoth_getTime(&bridgeCurrentTime, &bridgeCurrentMilliseconds); - if (ESPBridge_isRequestActive()) { + ESPBridge_serviceUntil(bridgeCurrentTime + ESP_TIME_SERVICE_WINDOW_SECONDS); - uint32_t bridgeCurrentTime; - uint32_t bridgeCurrentMilliseconds; + waitedMilliseconds += ESP_TIME_SERVICE_WINDOW_SECONDS * MILLISECONDS_IN_SECOND; - AudioMoth_getTime(&bridgeCurrentTime, &bridgeCurrentMilliseconds); + } else { - ESPBridge_serviceUntil(bridgeCurrentTime + ESP_TIME_SERVICE_WINDOW_SECONDS); + if (configSettings->enableLED && waitedMilliseconds % WAITING_LED_FLASH_INTERVAL == 0) { + FLASH_LED(Green, WAITING_LED_FLASH_DURATION) + } - waitedMilliseconds += ESP_TIME_SERVICE_WINDOW_SECONDS * MILLISECONDS_IN_SECOND; + AudioMoth_delay(ESP_TIME_WAIT_STEP_MS); - } else { + waitedMilliseconds += ESP_TIME_WAIT_STEP_MS; - 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); - ESPBridge_setUploadAllowed(false); - ESPBridge_setBusy(true); + } - if (AudioMoth_hasTimeBeenSet() == false) { + AudioMoth_getTime(¤tTime, ¤tMilliseconds); - SAVE_SWITCH_POSITION_AND_POWER_DOWN(DEFAULT_WAIT_INTERVAL); + } - } +''' - AudioMoth_getTime(¤tTime, ¤tMilliseconds); + main, n = pattern.subn(lambda m: m.group(1) + block + m.group(2), main, count=1) + if n != 1: + raise SystemExit(f"Failed to insert ESP32 time-setting path; replacements={n}") - } + main = main.replace( + 'bool shouldPerformAcousticConfiguration = switchPosition == AM_SWITCH_CUSTOM && (AudioMoth_hasTimeBeenSet() == false || configSettings->requireAcousticConfiguration);', + 'bool shouldPerformAcousticConfiguration = switchPosition == AM_SWITCH_CUSTOM && configSettings->requireAcousticConfiguration;', + 1, + ) - /* Check if switch has just been moved to CUSTOM or DEFAULT */""") + main = main.replace( + 'bool listenForAcousticTone = switchPosition == AM_SWITCH_CUSTOM && shouldPerformAcousticConfiguration == false;', + 'bool listenForAcousticTone = false;', + 1, + ) - if read_time_block not in main: - raise SystemExit("Could not find first read-time block in main.c") - main = main.replace(read_time_block, esp_time_block, 1) + bridge = bridge.replace( + 'void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) {\n if (bridgeBusy || !uploadAllowed) return;\n if (deadlineReached(deadlineUnixSeconds)) return;', + 'void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) {\n if (bridgeBusy) return;\n if (deadlineReached(deadlineUnixSeconds)) return;', + 1, + ) - old_acoustic = "bool shouldPerformAcousticConfiguration = switchPosition == AM_SWITCH_CUSTOM && (AudioMoth_hasTimeBeenSet() == false || configSettings->requireAcousticConfiguration);" - new_acoustic = "bool shouldPerformAcousticConfiguration = switchPosition == AM_SWITCH_CUSTOM && configSettings->requireAcousticConfiguration;" + if 'static void commandDelete(char *args) {\n char path[ESPBRIDGE_MAX_PATH];\n\n if (bridgeBusy || !uploadAllowed)' not in bridge: + bridge = bridge.replace( + 'static void commandDelete(char *args) {\n char path[ESPBRIDGE_MAX_PATH];\n', + 'static void commandDelete(char *args) {\n char path[ESPBRIDGE_MAX_PATH];\n\n if (bridgeBusy || !uploadAllowed) {\n sendLine("ERR BUSY upload_not_allowed");\n return;\n }\n\n', + 1, + ) - if old_acoustic not in main and new_acoustic not in main: - raise SystemExit("Could not find acoustic configuration condition") - main = main.replace(old_acoustic, new_acoustic, 1) + checks = { + "main identity version": 'static uint8_t firmwareVersion[AM_FIRMWARE_VERSION_LENGTH] = {1, 12, 0};' in main, + "main identity description": 'AudioMoth-Firmware-Basic' in main, + "main ESP constants": 'ESP_TIME_WAIT_TIMEOUT_MS' in main, + "main ESP wait block": 'ESP32 time-setting path.' in main, + "main no unset-time acoustic trigger": 'AudioMoth_hasTimeBeenSet() == false || configSettings->requireAcousticConfiguration' not in main, + "main acoustic tone disabled": 'bool listenForAcousticTone = false;' in main, + "bridge service allows non-upload commands": 'if (bridgeBusy || !uploadAllowed) return;' not in bridge, + "bridge delete still upload gated": 'static void commandDelete(char *args) {\n char path[ESPBRIDGE_MAX_PATH];\n\n if (bridgeBusy || !uploadAllowed)' in bridge, + } + + for name, ok in checks.items(): + print(f"{name}: {ok}") + if not ok: + raise SystemExit(f"Sanity check failed: {name}") main_path.write_text(main) - bridge_path.write_text(bridge2) + bridge_path.write_text(bridge) PY - - name: Show patched snippets + - name: Show final bridge-relevant source run: | - echo "main.c time wait constants:" - grep -n "ESP_TIME_" -A3 -B3 src/main.c - echo - echo "main.c ESP32 time path:" - grep -n "ESP32 time-setting path" -A55 -B8 src/main.c - echo - echo "main.c acoustic config condition:" - grep -n "shouldPerformAcousticConfiguration" -A2 -B2 src/main.c - echo - echo "espbridge.c service guard:" - grep -n "void ESPBridge_serviceUntil" -A5 src/espbridge.c - echo - echo "espbridge.c DELETE guard:" - grep -n "static void commandDelete" -A18 src/espbridge.c - - - name: Commit patched firmware code if changed + grep -n "ESP_TIME_" -A4 -B4 src/main.c + grep -n "ESP32 time-setting path" -A65 -B8 src/main.c + grep -n "shouldPerformAcousticConfiguration\|listenForAcousticTone" -A4 -B4 src/main.c + grep -n "static void commandDelete" -A25 src/espbridge.c + grep -n "void ESPBridge_serviceUntil" -A6 src/espbridge.c + + - name: Commit source patch run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" if git diff --quiet -- src/main.c src/espbridge.c; then - echo "No firmware source changes needed." + echo "Source already patched." exit 0 fi git add src/main.c src/espbridge.c - git commit -m "Wait for ESP32 time before acoustic configuration" + git commit -m "Fix ESP bridge startup time sync" git push origin master From c9a587cb5d59ed7b205b3f1a1f8f46dc4cc0fb54 Mon Sep 17 00:00:00 2001 From: Ash6414 Date: Tue, 9 Jun 2026 09:49:15 -0500 Subject: [PATCH 26/38] Fix workflow YAML indentation for bridge patch --- .../workflows/patch-esp-time-wait-once.yml | 211 +++++++----------- 1 file changed, 83 insertions(+), 128 deletions(-) diff --git a/.github/workflows/patch-esp-time-wait-once.yml b/.github/workflows/patch-esp-time-wait-once.yml index e32f1ad..e6a3024 100644 --- a/.github/workflows/patch-esp-time-wait-once.yml +++ b/.github/workflows/patch-esp-time-wait-once.yml @@ -30,151 +30,106 @@ jobs: main_path = Path("src/main.c") bridge_path = Path("src/espbridge.c") - main = main_path.read_text() bridge = bridge_path.read_text() - main = re.sub( - r'static uint8_t firmwareVersion\[AM_FIRMWARE_VERSION_LENGTH\]\s*=\s*\{[^}]+\};', - 'static uint8_t firmwareVersion[AM_FIRMWARE_VERSION_LENGTH] = {1, 12, 0};', - main, - count=1, - ) - - main = re.sub( - r'static uint8_t firmwareDescription\[AM_FIRMWARE_DESCRIPTION_LENGTH\]\s*=\s*"[^"]+";', - 'static uint8_t firmwareDescription[AM_FIRMWARE_DESCRIPTION_LENGTH] = "AudioMoth-Firmware-Basic";', - main, - count=1, - ) + main = re.sub(r'static uint8_t firmwareVersion\[AM_FIRMWARE_VERSION_LENGTH\]\s*=\s*\{[^}]+\};', 'static uint8_t firmwareVersion[AM_FIRMWARE_VERSION_LENGTH] = {1, 12, 0};', main, count=1) + main = re.sub(r'static uint8_t firmwareDescription\[AM_FIRMWARE_DESCRIPTION_LENGTH\]\s*=\s*"[^"]+";', 'static uint8_t firmwareDescription[AM_FIRMWARE_DESCRIPTION_LENGTH] = "AudioMoth-Firmware-Basic";', main, count=1) if "ESP_TIME_WAIT_TIMEOUT_MS" not in main: anchor = "#define USB_CONFIGURATION_BLINK 400" if anchor not in main: raise SystemExit("Missing USB_CONFIGURATION_BLINK anchor") - main = main.replace( - anchor, - anchor - + "\n\n#define ESP_TIME_WAIT_TIMEOUT_MS 60000" - + "\n#define ESP_TIME_WAIT_STEP_MS 50" - + "\n#define ESP_TIME_SERVICE_WINDOW_SECONDS 2", - 1, - ) + main = main.replace(anchor, anchor + "\n\n#define ESP_TIME_WAIT_TIMEOUT_MS 60000\n#define ESP_TIME_WAIT_STEP_MS 50\n#define ESP_TIME_SERVICE_WINDOW_SECONDS 2", 1) if "ESP32 time-setting path." not in main: - pattern = re.compile( - r'( /\* Read the time \*/\n\n' - r' uint32_t currentTime;\n\n' - r' uint32_t currentMilliseconds;\n\n' - r' AudioMoth_getTime\(¤tTime, ¤tMilliseconds\);\n\n)' - r'( /\* Check if switch has just been moved to CUSTOM or DEFAULT \*/)', - re.M, - ) - - block = r''' - /* 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) { - - uint32_t waitedMilliseconds = 0; - - ESPBridge_setBusy(false); - ESPBridge_setUploadAllowed(false); - - while (AudioMoth_hasTimeBeenSet() == false && 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); - - } - -''' - + pattern = re.compile(r'( /\* Read the time \*/\n\n uint32_t currentTime;\n\n uint32_t currentMilliseconds;\n\n AudioMoth_getTime\(¤tTime, ¤tMilliseconds\);\n\n)( /\* Check if switch has just been moved to CUSTOM or DEFAULT \*/)', re.M) + block_lines = [ + ' /* 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) {', + '', + ' uint32_t waitedMilliseconds = 0;', + '', + ' ESPBridge_setBusy(false);', + ' ESPBridge_setUploadAllowed(false);', + '', + ' while (AudioMoth_hasTimeBeenSet() == false && 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);', + '', + ' }', + '' + ] + block = "\n".join(block_lines) + "\n" main, n = pattern.subn(lambda m: m.group(1) + block + m.group(2), main, count=1) if n != 1: raise SystemExit(f"Failed to insert ESP32 time-setting path; replacements={n}") - main = main.replace( - 'bool shouldPerformAcousticConfiguration = switchPosition == AM_SWITCH_CUSTOM && (AudioMoth_hasTimeBeenSet() == false || configSettings->requireAcousticConfiguration);', - 'bool shouldPerformAcousticConfiguration = switchPosition == AM_SWITCH_CUSTOM && configSettings->requireAcousticConfiguration;', - 1, - ) - - main = main.replace( - 'bool listenForAcousticTone = switchPosition == AM_SWITCH_CUSTOM && shouldPerformAcousticConfiguration == false;', - 'bool listenForAcousticTone = false;', - 1, - ) + main = main.replace('bool shouldPerformAcousticConfiguration = switchPosition == AM_SWITCH_CUSTOM && (AudioMoth_hasTimeBeenSet() == false || configSettings->requireAcousticConfiguration);', 'bool shouldPerformAcousticConfiguration = switchPosition == AM_SWITCH_CUSTOM && configSettings->requireAcousticConfiguration;', 1) + main = main.replace('bool listenForAcousticTone = switchPosition == AM_SWITCH_CUSTOM && shouldPerformAcousticConfiguration == false;', 'bool listenForAcousticTone = false;', 1) - bridge = bridge.replace( - 'void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) {\n if (bridgeBusy || !uploadAllowed) return;\n if (deadlineReached(deadlineUnixSeconds)) return;', - 'void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) {\n if (bridgeBusy) return;\n if (deadlineReached(deadlineUnixSeconds)) return;', - 1, - ) + bridge = bridge.replace('void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) {\n if (bridgeBusy || !uploadAllowed) return;\n if (deadlineReached(deadlineUnixSeconds)) return;', 'void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) {\n if (bridgeBusy) return;\n if (deadlineReached(deadlineUnixSeconds)) return;', 1) if 'static void commandDelete(char *args) {\n char path[ESPBRIDGE_MAX_PATH];\n\n if (bridgeBusy || !uploadAllowed)' not in bridge: - bridge = bridge.replace( - 'static void commandDelete(char *args) {\n char path[ESPBRIDGE_MAX_PATH];\n', - 'static void commandDelete(char *args) {\n char path[ESPBRIDGE_MAX_PATH];\n\n if (bridgeBusy || !uploadAllowed) {\n sendLine("ERR BUSY upload_not_allowed");\n return;\n }\n\n', - 1, - ) - - checks = { - "main identity version": 'static uint8_t firmwareVersion[AM_FIRMWARE_VERSION_LENGTH] = {1, 12, 0};' in main, - "main identity description": 'AudioMoth-Firmware-Basic' in main, - "main ESP constants": 'ESP_TIME_WAIT_TIMEOUT_MS' in main, - "main ESP wait block": 'ESP32 time-setting path.' in main, - "main no unset-time acoustic trigger": 'AudioMoth_hasTimeBeenSet() == false || configSettings->requireAcousticConfiguration' not in main, - "main acoustic tone disabled": 'bool listenForAcousticTone = false;' in main, - "bridge service allows non-upload commands": 'if (bridgeBusy || !uploadAllowed) return;' not in bridge, - "bridge delete still upload gated": 'static void commandDelete(char *args) {\n char path[ESPBRIDGE_MAX_PATH];\n\n if (bridgeBusy || !uploadAllowed)' in bridge, - } - - for name, ok in checks.items(): + bridge = bridge.replace('static void commandDelete(char *args) {\n char path[ESPBRIDGE_MAX_PATH];\n', 'static void commandDelete(char *args) {\n char path[ESPBRIDGE_MAX_PATH];\n\n if (bridgeBusy || !uploadAllowed) {\n sendLine("ERR BUSY upload_not_allowed");\n return;\n }\n\n', 1) + + checks = [ + ('main identity version', 'static uint8_t firmwareVersion[AM_FIRMWARE_VERSION_LENGTH] = {1, 12, 0};' in main), + ('main identity description', 'AudioMoth-Firmware-Basic' in main), + ('main ESP constants', 'ESP_TIME_WAIT_TIMEOUT_MS' in main), + ('main ESP wait block', 'ESP32 time-setting path.' in main), + ('main no unset-time acoustic trigger', 'AudioMoth_hasTimeBeenSet() == false || configSettings->requireAcousticConfiguration' not in main), + ('main acoustic tone disabled', 'bool listenForAcousticTone = false;' in main), + ('bridge service allows non-upload commands', 'if (bridgeBusy || !uploadAllowed) return;' not in bridge), + ('bridge delete still upload gated', 'static void commandDelete(char *args) {\n char path[ESPBRIDGE_MAX_PATH];\n\n if (bridgeBusy || !uploadAllowed)' in bridge), + ] + for name, ok in checks: print(f"{name}: {ok}") if not ok: raise SystemExit(f"Sanity check failed: {name}") From 089673c415f804bd4772156c8a35b4138dc57e1b Mon Sep 17 00:00:00 2001 From: Ash6414 Date: Tue, 9 Jun 2026 12:29:32 -0500 Subject: [PATCH 27/38] Update espbridge.h --- inc/espbridge.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/espbridge.h b/inc/espbridge.h index f0dccfc..bd4a021 100644 --- a/inc/espbridge.h +++ b/inc/espbridge.h @@ -13,7 +13,7 @@ #include #include -#define ESPBRIDGE_DEFAULT_BAUD 921600 +#define ESPBRIDGE_DEFAULT_BAUD 115200 #define ESPBRIDGE_MAX_LINE 160 #define ESPBRIDGE_MAX_PATH 96 #define ESPBRIDGE_CHUNK_BYTES 512 From d369e039611fc55aef5e414046e0a1899a2f43bf Mon Sep 17 00:00:00 2001 From: Ash6414 Date: Tue, 9 Jun 2026 13:23:33 -0500 Subject: [PATCH 28/38] Keep ESP bridge service alive while requested --- src/espbridge.c | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/espbridge.c b/src/espbridge.c index faf062a..d004fd2 100644 --- a/src/espbridge.c +++ b/src/espbridge.c @@ -59,6 +59,7 @@ #define RX_LINE_TIMEOUT_MS 250 #define SERVICE_IDLE_TIMEOUT_MS 3000 +#define SERVICE_MAX_WINDOW_MS 30000 static volatile bool bridgeBusy = true; static volatile bool uploadAllowed = false; @@ -407,18 +408,25 @@ bool ESPBridge_isRequestActive(void) { void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) { if (bridgeBusy) return; - if (deadlineReached(deadlineUnixSeconds)) return; uint32_t idleMs = 0; + uint32_t serviceMs = 0; sendLine("OK BRIDGE_READY"); - while (!deadlineReached(deadlineUnixSeconds)) { + while (serviceMs < SERVICE_MAX_WINDOW_MS) { WDOG_Feed(); - if (!ESPBridge_isRequestActive() && !uartRxAvailable()) { + 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; + serviceMs += 10; continue; } @@ -427,6 +435,7 @@ void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) { handleCommand(deadlineUnixSeconds); } else { idleMs += RX_LINE_TIMEOUT_MS; + serviceMs += RX_LINE_TIMEOUT_MS; } } From c2bd1d39f29e67ac2ab681f10fc78015d6163781 Mon Sep 17 00:00:00 2001 From: Ash6414 Date: Tue, 9 Jun 2026 13:27:40 -0500 Subject: [PATCH 29/38] Enter bridge service when uploads become allowed --- src/espbridge.c | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/espbridge.c b/src/espbridge.c index d004fd2..c656e39 100644 --- a/src/espbridge.c +++ b/src/espbridge.c @@ -64,6 +64,7 @@ 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]; @@ -391,6 +392,7 @@ void ESPBridge_init(void) { bridgeBusy = true; uploadAllowed = false; filesystemEnabled = false; + serviceActive = false; } void ESPBridge_setBusy(bool busy) { @@ -400,6 +402,12 @@ void ESPBridge_setBusy(bool busy) { void ESPBridge_setUploadAllowed(bool allowed) { uploadAllowed = allowed; + + if (allowed && !bridgeBusy && ESPBridge_isRequestActive() && !serviceActive) { + uint32_t now, ms; + AudioMoth_getTime(&now, &ms); + ESPBridge_serviceUntil(now + 1); + } } bool ESPBridge_isRequestActive(void) { @@ -407,7 +415,9 @@ bool ESPBridge_isRequestActive(void) { } void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) { - if (bridgeBusy) return; + if (bridgeBusy || serviceActive) return; + + serviceActive = true; uint32_t idleMs = 0; uint32_t serviceMs = 0; @@ -440,4 +450,5 @@ void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) { } sendLine("OK BRIDGE_SLEEP"); + serviceActive = false; } From 46d2f87daf07aa6b31bc63132d80f485f01b33d0 Mon Sep 17 00:00:00 2001 From: Ash6414 Date: Tue, 9 Jun 2026 13:31:03 -0500 Subject: [PATCH 30/38] Answer ESP bridge request when BUSY drops --- src/espbridge.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/espbridge.c b/src/espbridge.c index c656e39..0f1471c 100644 --- a/src/espbridge.c +++ b/src/espbridge.c @@ -398,6 +398,12 @@ void ESPBridge_init(void) { void ESPBridge_setBusy(bool busy) { bridgeBusy = busy; gpioWrite(BRIDGE_BUSY_PORT, BRIDGE_BUSY_PIN, busy); + + if (!busy && ESPBridge_isRequestActive() && !serviceActive) { + uint32_t now, ms; + AudioMoth_getTime(&now, &ms); + ESPBridge_serviceUntil(now + 1); + } } void ESPBridge_setUploadAllowed(bool allowed) { From f930b137a8c729f1063653035aa38d3b7f34f4a4 Mon Sep 17 00:00:00 2001 From: Ash6414 Date: Tue, 9 Jun 2026 13:37:12 -0500 Subject: [PATCH 31/38] Match Basic firmware name in docs --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 860fd92..c6bade5 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ -# AudioMoth-Firmware-EspNode +# AudioMoth-Firmware-Basic 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 #### From 86fc02255c805c19b87e473b2d1b7d6b5ffd8d45 Mon Sep 17 00:00:00 2001 From: Ash6414 Date: Tue, 9 Jun 2026 13:55:01 -0500 Subject: [PATCH 32/38] Delete .github/workflows/temp-request-aware-bridge-patch.yml --- .../temp-request-aware-bridge-patch.yml | 56 ------------------- 1 file changed, 56 deletions(-) delete mode 100644 .github/workflows/temp-request-aware-bridge-patch.yml diff --git a/.github/workflows/temp-request-aware-bridge-patch.yml b/.github/workflows/temp-request-aware-bridge-patch.yml deleted file mode 100644 index 1e80d44..0000000 --- a/.github/workflows/temp-request-aware-bridge-patch.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: TEMP Request-Aware Bridge Patch - -on: - workflow_dispatch: - -permissions: - contents: write - -jobs: - patch: - runs-on: ubuntu-latest - steps: - - name: Check out firmware repository - uses: actions/checkout@v5 - with: - ref: master - fetch-depth: 1 - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Make AudioMoth service ESP bridge when request is active - run: | - set -euo pipefail - python3 - <<'PY' - from pathlib import Path - p = Path('src/main.c') - s = p.read_text() - s = s.replace( - 'if (switchPosition == AM_SWITCH_CUSTOM && AudioMoth_hasTimeBeenSet() == false) {', - 'if (switchPosition == AM_SWITCH_CUSTOM && (AudioMoth_hasTimeBeenSet() == false || ESPBridge_isRequestActive())) {', - 1, - ) - s = s.replace( - 'while (AudioMoth_hasTimeBeenSet() == false && waitedMilliseconds < ESP_TIME_WAIT_TIMEOUT_MS) {', - 'while ((AudioMoth_hasTimeBeenSet() == false || ESPBridge_isRequestActive()) && waitedMilliseconds < ESP_TIME_WAIT_TIMEOUT_MS) {', - 1, - ) - if 'if (switchPosition == AM_SWITCH_CUSTOM && (AudioMoth_hasTimeBeenSet() == false || ESPBridge_isRequestActive())) {' not in s: - raise SystemExit('request-aware if condition missing') - if 'while ((AudioMoth_hasTimeBeenSet() == false || ESPBridge_isRequestActive()) && waitedMilliseconds < ESP_TIME_WAIT_TIMEOUT_MS) {' not in s: - raise SystemExit('request-aware while condition missing') - p.write_text(s) - PY - - - name: Commit patch - run: | - set -euo pipefail - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - if git diff --quiet -- src/main.c; then - echo 'No change needed.' - else - git diff -- src/main.c - git add src/main.c - git commit -m 'Service ESP bridge when request is active' - git push origin master - fi From 15c1c23f77bdebe2b4d77e63948184c17fff317c Mon Sep 17 00:00:00 2001 From: Ash6414 Date: Tue, 9 Jun 2026 13:55:12 -0500 Subject: [PATCH 33/38] Delete .github/workflows/temp-fix-and-build-espbridge.yml --- .../temp-fix-and-build-espbridge.yml | 185 ------------------ 1 file changed, 185 deletions(-) delete mode 100644 .github/workflows/temp-fix-and-build-espbridge.yml diff --git a/.github/workflows/temp-fix-and-build-espbridge.yml b/.github/workflows/temp-fix-and-build-espbridge.yml deleted file mode 100644 index 86510e6..0000000 --- a/.github/workflows/temp-fix-and-build-espbridge.yml +++ /dev/null @@ -1,185 +0,0 @@ -name: TEMP Fix and Build ESPBridge - -on: - workflow_dispatch: - push: - branches: - - master - paths: - - '.github/workflows/temp-fix-and-build-espbridge.yml' - -permissions: - contents: write - -env: - BUILD_FRAMEWORK_REPO: https://github.com/OpenAcousticDevices/AudioMoth-Project.git - -jobs: - fix-and-build: - runs-on: ubuntu-latest - - steps: - - name: Check out firmware repository - uses: actions/checkout@v4 - with: - ref: master - fetch-depth: 1 - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Patch ESPBridge source if needed - run: | - set -euo pipefail - python3 - <<'PY' - from pathlib import Path - - bridge_path = Path('src/espbridge.c') - main_path = Path('src/main.c') - - bridge = bridge_path.read_text() - main = main_path.read_text() - - if 'static inline void gpioWrite(' not in bridge: - marker = 'static uint8_t chunkBuffer[ESPBRIDGE_CHUNK_BYTES];\n' - helper = """ - static inline void gpioWrite(GPIO_Port_TypeDef port, unsigned int pin, bool value) { - if (value) { - GPIO_PinOutSet(port, pin); - } else { - GPIO_PinOutClear(port, pin); - } - } - """ - if marker not in bridge: - raise SystemExit('Could not find chunkBuffer marker for gpioWrite helper') - bridge = bridge.replace(marker, marker + helper + '\n', 1) - - bridge = bridge.replace( - 'GPIO_PinOutWrite(BRIDGE_BUSY_PORT, BRIDGE_BUSY_PIN, busy ? 1 : 0);', - 'gpioWrite(BRIDGE_BUSY_PORT, BRIDGE_BUSY_PIN, busy);' - ) - bridge = bridge.replace( - 'GPIO_PinOutWrite(BRIDGE_BUSY_PORT, BRIDGE_BUSY_PIN, busy);', - 'gpioWrite(BRIDGE_BUSY_PORT, BRIDGE_BUSY_PIN, busy);' - ) - - bridge = bridge.replace( - 'void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) {\n if (bridgeBusy || !uploadAllowed) return;', - 'void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) {\n if (bridgeBusy) return;', - 1, - ) - - if 'static void commandDelete(char *args) {\n char path[ESPBRIDGE_MAX_PATH];\n\n if (bridgeBusy || !uploadAllowed)' not in bridge: - bridge = bridge.replace( - 'static void commandDelete(char *args) {\n char path[ESPBRIDGE_MAX_PATH];\n', - 'static void commandDelete(char *args) {\n char path[ESPBRIDGE_MAX_PATH];\n\n if (bridgeBusy || !uploadAllowed) {\n sendLine("ERR BUSY upload_not_allowed");\n return;\n }\n\n', - 1, - ) - - main = main.replace( - 'bool shouldPerformAcousticConfiguration = switchPosition == AM_SWITCH_CUSTOM && (AudioMoth_hasTimeBeenSet() == false || configSettings->requireAcousticConfiguration);', - 'bool shouldPerformAcousticConfiguration = switchPosition == AM_SWITCH_CUSTOM && configSettings->requireAcousticConfiguration;', - 1, - ) - main = main.replace( - 'bool listenForAcousticTone = switchPosition == AM_SWITCH_CUSTOM && shouldPerformAcousticConfiguration == false;', - 'bool listenForAcousticTone = false;', - 1, - ) - - checks = [ - ('no GPIO_PinOutWrite', 'GPIO_PinOutWrite' not in bridge), - ('gpioWrite helper present', 'static inline void gpioWrite(' in bridge), - ('service allows TIME before upload', 'if (bridgeBusy || !uploadAllowed) return;' not in bridge), - ('DELETE remains upload gated', 'if (bridgeBusy || !uploadAllowed)' in bridge), - ('main has ESP time wait constants', 'ESP_TIME_WAIT_TIMEOUT_MS' in main), - ('main has ESP time wait block', 'ESP32 time-setting path.' in main), - ('acoustic tone disabled', 'bool listenForAcousticTone = false;' in main), - ] - for name, ok in checks: - print(f'{name}: {ok}') - if not ok: - raise SystemExit(f'Failed source check: {name}') - - bridge_path.write_text(bridge) - main_path.write_text(main) - PY - - - name: Commit source patch if changed - run: | - set -euo pipefail - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - if git diff --quiet -- src/main.c src/espbridge.c; then - echo 'Source already contained required ESPBridge fixes.' - else - git diff -- src/main.c src/espbridge.c - git add src/main.c src/espbridge.c - git commit -m 'Fix AudioMoth ESPBridge source' - git push origin master - fi - - - name: Install build dependencies - 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-dev \ - libnewlib-arm-none-eabi - arm-none-eabi-gcc --version - printf '#include \nint main(void){return 0;}\n' | arm-none-eabi-gcc -x c -E - >/dev/null - - - name: Clone AudioMoth build framework - run: | - set -euo pipefail - git clone --depth 1 "$BUILD_FRAMEWORK_REPO" project - - - name: Overlay repository source into build framework - run: | - set -euo pipefail - cp -f src/*.c project/src/ - if [ -d inc ]; then - cp -f inc/*.h project/inc/ - fi - if grep -R "GPIO_PinOutWrite" -n project/src project/inc; then - echo 'ERROR: GPIO_PinOutWrite still exists in build tree.' - exit 1 - fi - grep -n "ESPBridge_setBusy" -A8 project/src/espbridge.c - grep -n "ESPBridge_serviceUntil" -A4 project/src/espbridge.c - - - name: Configure Makefile - run: | - set -euo pipefail - sed -i 's|^TOOLCHAIN_PATH =.*|TOOLCHAIN_PATH = /usr|' project/build/Makefile - sed -i 's|^TOOLCHAIN_VERSION =.*|TOOLCHAIN_VERSION = |' project/build/Makefile - sed -i 's|^INC =.*|INC = ../cmsis ../device/inc ../emlib/inc ../emusb/inc ../drivers/inc ../fatfs/inc ../gps/inc ../inc|' project/build/Makefile - sed -i 's|^SRC =.*|SRC = ../device/src ../emlib/src ../emusb/src ../drivers/src ../fatfs/src ../gps/src ../src|' project/build/Makefile - - - name: Build AudioMoth firmware - working-directory: project/build - run: | - set -euo pipefail - make clean - make audiomoth.bin - - - name: Package firmware outputs - run: | - set -euo pipefail - test -s project/build/audiomoth.bin - 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 - ls -lh firmware-artifacts - - - name: Upload firmware artifact - uses: actions/upload-artifact@v4 - with: - name: audiomoth-espbridge-fixed-firmware - path: firmware-artifacts/ - if-no-files-found: error From 8c1d515f3ee2f4922f5a6449d64cdb42ed73677f Mon Sep 17 00:00:00 2001 From: Ash6414 Date: Tue, 9 Jun 2026 13:55:20 -0500 Subject: [PATCH 34/38] Delete .github/workflows/patch-firmware-identity-once.yml --- .../patch-firmware-identity-once.yml | 69 ------------------- 1 file changed, 69 deletions(-) delete mode 100644 .github/workflows/patch-firmware-identity-once.yml diff --git a/.github/workflows/patch-firmware-identity-once.yml b/.github/workflows/patch-firmware-identity-once.yml deleted file mode 100644 index 2fbaccb..0000000 --- a/.github/workflows/patch-firmware-identity-once.yml +++ /dev/null @@ -1,69 +0,0 @@ -name: Patch firmware identity once - -on: - workflow_dispatch: - push: - branches: - - master - paths: - - .github/workflows/patch-firmware-identity-once.yml - -permissions: - contents: write - -jobs: - patch-main-c: - runs-on: ubuntu-latest - - steps: - - name: Check out repository - uses: actions/checkout@v4 - with: - ref: master - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Patch src/main.c firmware identity - run: | - python3 - <<'PY' - from pathlib import Path - import re - - path = Path("src/main.c") - text = path.read_text() - - text2, n_version = re.subn( - r'static uint8_t firmwareVersion\[AM_FIRMWARE_VERSION_LENGTH\]\s*=\s*\{[^}]+\};', - 'static uint8_t firmwareVersion[AM_FIRMWARE_VERSION_LENGTH] = {1, 12, 0};', - text, - count=1, - ) - - text3, n_desc = re.subn( - r'static uint8_t firmwareDescription\[AM_FIRMWARE_DESCRIPTION_LENGTH\]\s*=\s*"[^"]+";', - 'static uint8_t firmwareDescription[AM_FIRMWARE_DESCRIPTION_LENGTH] = "AudioMoth-Firmware-Basic";', - text2, - count=1, - ) - - if n_version != 1 or n_desc != 1: - raise SystemExit(f"Expected to patch exactly one version and one description, patched version={n_version}, description={n_desc}") - - path.write_text(text3) - PY - - - name: Commit src/main.c if changed - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - - echo "Patched identity lines:" - grep -n "static uint8_t firmwareVersion\|static uint8_t firmwareDescription" -A2 -B2 src/main.c - - if git diff --quiet -- src/main.c; then - echo "src/main.c already had the requested firmware identity. No commit needed." - exit 0 - fi - - git add src/main.c - git commit -m "Set firmware identity to AudioMoth Basic" - git push origin master From de9e85f5d5e73888e457aa182e7bb4eaee84afb7 Mon Sep 17 00:00:00 2001 From: Ash6414 Date: Tue, 9 Jun 2026 13:55:28 -0500 Subject: [PATCH 35/38] Delete .github/workflows/patch-esp-time-wait-once.yml --- .../workflows/patch-esp-time-wait-once.yml | 161 ------------------ 1 file changed, 161 deletions(-) delete mode 100644 .github/workflows/patch-esp-time-wait-once.yml diff --git a/.github/workflows/patch-esp-time-wait-once.yml b/.github/workflows/patch-esp-time-wait-once.yml deleted file mode 100644 index e6a3024..0000000 --- a/.github/workflows/patch-esp-time-wait-once.yml +++ /dev/null @@ -1,161 +0,0 @@ -name: Patch ESP bridge source now - -on: - workflow_dispatch: - push: - branches: - - master - paths: - - .github/workflows/patch-esp-time-wait-once.yml - -permissions: - contents: write - -jobs: - patch-esp-bridge: - runs-on: ubuntu-latest - - steps: - - name: Check out repository - uses: actions/checkout@v4 - with: - ref: master - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Patch main.c and espbridge.c - run: | - python3 - <<'PY' - from pathlib import Path - import re - - main_path = Path("src/main.c") - bridge_path = Path("src/espbridge.c") - main = main_path.read_text() - bridge = bridge_path.read_text() - - main = re.sub(r'static uint8_t firmwareVersion\[AM_FIRMWARE_VERSION_LENGTH\]\s*=\s*\{[^}]+\};', 'static uint8_t firmwareVersion[AM_FIRMWARE_VERSION_LENGTH] = {1, 12, 0};', main, count=1) - main = re.sub(r'static uint8_t firmwareDescription\[AM_FIRMWARE_DESCRIPTION_LENGTH\]\s*=\s*"[^"]+";', 'static uint8_t firmwareDescription[AM_FIRMWARE_DESCRIPTION_LENGTH] = "AudioMoth-Firmware-Basic";', main, count=1) - - if "ESP_TIME_WAIT_TIMEOUT_MS" not in main: - anchor = "#define USB_CONFIGURATION_BLINK 400" - if anchor not in main: - raise SystemExit("Missing USB_CONFIGURATION_BLINK anchor") - main = main.replace(anchor, anchor + "\n\n#define ESP_TIME_WAIT_TIMEOUT_MS 60000\n#define ESP_TIME_WAIT_STEP_MS 50\n#define ESP_TIME_SERVICE_WINDOW_SECONDS 2", 1) - - if "ESP32 time-setting path." not in main: - pattern = re.compile(r'( /\* Read the time \*/\n\n uint32_t currentTime;\n\n uint32_t currentMilliseconds;\n\n AudioMoth_getTime\(¤tTime, ¤tMilliseconds\);\n\n)( /\* Check if switch has just been moved to CUSTOM or DEFAULT \*/)', re.M) - block_lines = [ - ' /* 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) {', - '', - ' uint32_t waitedMilliseconds = 0;', - '', - ' ESPBridge_setBusy(false);', - ' ESPBridge_setUploadAllowed(false);', - '', - ' while (AudioMoth_hasTimeBeenSet() == false && 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);', - '', - ' }', - '' - ] - block = "\n".join(block_lines) + "\n" - main, n = pattern.subn(lambda m: m.group(1) + block + m.group(2), main, count=1) - if n != 1: - raise SystemExit(f"Failed to insert ESP32 time-setting path; replacements={n}") - - main = main.replace('bool shouldPerformAcousticConfiguration = switchPosition == AM_SWITCH_CUSTOM && (AudioMoth_hasTimeBeenSet() == false || configSettings->requireAcousticConfiguration);', 'bool shouldPerformAcousticConfiguration = switchPosition == AM_SWITCH_CUSTOM && configSettings->requireAcousticConfiguration;', 1) - main = main.replace('bool listenForAcousticTone = switchPosition == AM_SWITCH_CUSTOM && shouldPerformAcousticConfiguration == false;', 'bool listenForAcousticTone = false;', 1) - - bridge = bridge.replace('void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) {\n if (bridgeBusy || !uploadAllowed) return;\n if (deadlineReached(deadlineUnixSeconds)) return;', 'void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) {\n if (bridgeBusy) return;\n if (deadlineReached(deadlineUnixSeconds)) return;', 1) - - if 'static void commandDelete(char *args) {\n char path[ESPBRIDGE_MAX_PATH];\n\n if (bridgeBusy || !uploadAllowed)' not in bridge: - bridge = bridge.replace('static void commandDelete(char *args) {\n char path[ESPBRIDGE_MAX_PATH];\n', 'static void commandDelete(char *args) {\n char path[ESPBRIDGE_MAX_PATH];\n\n if (bridgeBusy || !uploadAllowed) {\n sendLine("ERR BUSY upload_not_allowed");\n return;\n }\n\n', 1) - - checks = [ - ('main identity version', 'static uint8_t firmwareVersion[AM_FIRMWARE_VERSION_LENGTH] = {1, 12, 0};' in main), - ('main identity description', 'AudioMoth-Firmware-Basic' in main), - ('main ESP constants', 'ESP_TIME_WAIT_TIMEOUT_MS' in main), - ('main ESP wait block', 'ESP32 time-setting path.' in main), - ('main no unset-time acoustic trigger', 'AudioMoth_hasTimeBeenSet() == false || configSettings->requireAcousticConfiguration' not in main), - ('main acoustic tone disabled', 'bool listenForAcousticTone = false;' in main), - ('bridge service allows non-upload commands', 'if (bridgeBusy || !uploadAllowed) return;' not in bridge), - ('bridge delete still upload gated', 'static void commandDelete(char *args) {\n char path[ESPBRIDGE_MAX_PATH];\n\n if (bridgeBusy || !uploadAllowed)' in bridge), - ] - for name, ok in checks: - print(f"{name}: {ok}") - if not ok: - raise SystemExit(f"Sanity check failed: {name}") - - main_path.write_text(main) - bridge_path.write_text(bridge) - PY - - - name: Show final bridge-relevant source - run: | - grep -n "ESP_TIME_" -A4 -B4 src/main.c - grep -n "ESP32 time-setting path" -A65 -B8 src/main.c - grep -n "shouldPerformAcousticConfiguration\|listenForAcousticTone" -A4 -B4 src/main.c - grep -n "static void commandDelete" -A25 src/espbridge.c - grep -n "void ESPBridge_serviceUntil" -A6 src/espbridge.c - - - name: Commit source patch - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - - if git diff --quiet -- src/main.c src/espbridge.c; then - echo "Source already patched." - exit 0 - fi - - git add src/main.c src/espbridge.c - git commit -m "Fix ESP bridge startup time sync" - git push origin master From aff7e46207338571c3bc0029699c6b13c3271782 Mon Sep 17 00:00:00 2001 From: Ash6414 Date: Tue, 9 Jun 2026 13:56:34 -0500 Subject: [PATCH 36/38] Bound bridge service loop cleanly --- src/espbridge.c | 74 +++++++++++++++++++++++-------------------------- 1 file changed, 34 insertions(+), 40 deletions(-) diff --git a/src/espbridge.c b/src/espbridge.c index 0f1471c..60dc967 100644 --- a/src/espbridge.c +++ b/src/espbridge.c @@ -2,28 +2,9 @@ * espbridge.c * AudioMoth Dev <-> ESP32 upload bridge. * - * Protocol is ASCII commands plus binary DATA payloads. - * Commands from ESP32: - * STATUS\n - * TIME \n - * LIST\n - * GET \n - * DELETE \n // ESP32 should send only after server-confirmed upload - * DONE\n // ESP32 releases service window - * PING\n - * - * Responses from AudioMoth: - * OK ...\n - * ERR \n - * FILE \n - * END\n - * DATA \n - * - * Notes: - * - This file assumes the standard AudioMoth-Project build exposes EMLIB, - * FatFS, and audiomoth.h. - * - UART route uses USART1 location 2 because AudioMoth Dev labels b9/b10 - * as U1_TX#2 / U1_RX#2. + * 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 @@ -60,6 +41,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; @@ -185,6 +167,27 @@ static bool deadlineReached(uint32_t deadlineUnixSeconds) { 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) { @@ -349,7 +352,7 @@ static void commandStatus(uint32_t deadlineUnixSeconds) { (unsigned long)deadlineUnixSeconds); } -static void handleCommand(uint32_t deadlineUnixSeconds) { +static bool handleCommand(uint32_t deadlineUnixSeconds) { if (strcmp(lineBuffer, "PING") == 0) { sendLine("OK PONG"); } else if (strcmp(lineBuffer, "STATUS") == 0) { @@ -364,9 +367,12 @@ static void handleCommand(uint32_t deadlineUnixSeconds) { commandDelete(lineBuffer + 7); } else if (strcmp(lineBuffer, "DONE") == 0) { sendLine("OK DONE"); + return true; } else { sendLine("ERR CMD unknown_command"); } + + return false; } /* ---------------- Public API ---------------- */ @@ -398,22 +404,10 @@ void ESPBridge_init(void) { void ESPBridge_setBusy(bool busy) { bridgeBusy = busy; gpioWrite(BRIDGE_BUSY_PORT, BRIDGE_BUSY_PIN, busy); - - if (!busy && ESPBridge_isRequestActive() && !serviceActive) { - uint32_t now, ms; - AudioMoth_getTime(&now, &ms); - ESPBridge_serviceUntil(now + 1); - } } void ESPBridge_setUploadAllowed(bool allowed) { uploadAllowed = allowed; - - if (allowed && !bridgeBusy && ESPBridge_isRequestActive() && !serviceActive) { - uint32_t now, ms; - AudioMoth_getTime(&now, &ms); - ESPBridge_serviceUntil(now + 1); - } } bool ESPBridge_isRequestActive(void) { @@ -426,10 +420,12 @@ void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) { serviceActive = true; uint32_t idleMs = 0; - uint32_t serviceMs = 0; + uint32_t serviceStartSeconds, serviceStartMilliseconds; + AudioMoth_getTime(&serviceStartSeconds, &serviceStartMilliseconds); + sendLine("OK BRIDGE_READY"); - while (serviceMs < SERVICE_MAX_WINDOW_MS) { + while (elapsedServiceMilliseconds(serviceStartSeconds, serviceStartMilliseconds) < SERVICE_MAX_WINDOW_MS) { WDOG_Feed(); bool deadlineDone = deadlineReached(deadlineUnixSeconds); @@ -442,16 +438,14 @@ void ESPBridge_serviceUntil(uint32_t deadlineUnixSeconds) { if (idleMs >= SERVICE_IDLE_TIMEOUT_MS) break; AudioMoth_delay(10); idleMs += 10; - serviceMs += 10; continue; } if (readLine(RX_LINE_TIMEOUT_MS)) { idleMs = 0; - handleCommand(deadlineUnixSeconds); + if (handleCommand(deadlineUnixSeconds)) break; } else { idleMs += RX_LINE_TIMEOUT_MS; - serviceMs += RX_LINE_TIMEOUT_MS; } } From 7365ff0b640f06614b71866380c53c0a23149cef Mon Sep 17 00:00:00 2001 From: Ash6414 Date: Tue, 9 Jun 2026 14:13:25 -0500 Subject: [PATCH 37/38] Fix AudioMoth bin builder to use current repo files --- .github/workflows/build-audiomoth-bin.yml | 124 +++++++++++++++++----- 1 file changed, 95 insertions(+), 29 deletions(-) diff --git a/.github/workflows/build-audiomoth-bin.yml b/.github/workflows/build-audiomoth-bin.yml index f28d9c4..90039d5 100644 --- a/.github/workflows/build-audiomoth-bin.yml +++ b/.github/workflows/build-audiomoth-bin.yml @@ -2,6 +2,11 @@ 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 @@ -9,6 +14,11 @@ on: - 'src/**' - 'inc/**' - '.github/workflows/build-audiomoth-bin.yml' + pull_request: + paths: + - 'src/**' + - 'inc/**' + - '.github/workflows/build-audiomoth-bin.yml' permissions: contents: read @@ -17,30 +27,54 @@ 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 + - name: Check out firmware source at workflow ref uses: actions/checkout@v4 with: path: firmware - ref: master fetch-depth: 1 - - name: Show source revision + - name: Show and validate source revision + shell: bash run: | set -euo pipefail - echo "Repository: Ash6414/AudioMoth-Firmware_ESPnode" - echo "Branch: master" + echo "Repository: ${GITHUB_REPOSITORY}" + echo "Workflow ref: ${GITHUB_REF}" + echo "Ref name: ${GITHUB_REF_NAME}" echo "Commit: $(git -C firmware rev-parse HEAD)" - test -f firmware/src/main.c - test -f firmware/src/espbridge.c - grep -n "ESP_TIME_WAIT_TIMEOUT_MS" firmware/src/main.c - grep -n "ESPBridge_serviceUntil" -A3 firmware/src/espbridge.c + 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 - - name: Install ARM GCC and newlib + 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 @@ -49,53 +83,85 @@ jobs: make \ gcc-arm-none-eabi \ binutils-arm-none-eabi \ - libnewlib-dev \ libnewlib-arm-none-eabi arm-none-eabi-gcc --version - printf '#include \nint main(void){return 0;}\n' | arm-none-eabi-gcc -x c -E - >/dev/null + arm-none-eabi-objcopy --version - name: Clone AudioMoth build framework + shell: bash run: | set -euo pipefail - git clone --depth 1 https://github.com/OpenAcousticDevices/AudioMoth-Project.git project + 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 repository files into build framework + - name: Overlay this repository into AudioMoth-Project + shell: bash run: | set -euo pipefail - cp -f firmware/src/*.c project/src/ - if [ -d firmware/inc ]; then - cp -f firmware/inc/*.h project/inc/ - fi - echo "Build-tree bridge guard:" - grep -n "ESPBridge_serviceUntil" -A3 project/src/espbridge.c - echo "Build-tree time-wait constants:" - grep -n "ESP_TIME_WAIT_TIMEOUT_MS" project/src/main.c + 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 - sed -i 's|^TOOLCHAIN_PATH =.*|TOOLCHAIN_PATH = /usr|' project/build/Makefile - sed -i 's|^TOOLCHAIN_VERSION =.*|TOOLCHAIN_VERSION = |' project/build/Makefile - sed -i 's|^INC =.*|INC = ../cmsis ../device/inc ../emlib/inc ../emusb/inc ../drivers/inc ../fatfs/inc ../gps/inc ../inc|' project/build/Makefile - sed -i 's|^SRC =.*|SRC = ../device/src ../emlib/src ../emusb/src ../drivers/src ../fatfs/src ../gps/src ../src|' project/build/Makefile - grep -n "^TOOLCHAIN_PATH\|^TOOLCHAIN_VERSION\|^INC =\|^SRC =" project/build/Makefile + 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 audiomoth.bin + make -j"$(nproc)" audiomoth.bin - - name: Verify binary output + - 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 From d1cfd1617921dd74b70e9d6802c07185d4544337 Mon Sep 17 00:00:00 2001 From: Ash6414 Date: Tue, 9 Jun 2026 14:29:55 -0500 Subject: [PATCH 38/38] Route ESPBridge over UART1 header pins --- src/espbridge.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/espbridge.c b/src/espbridge.c index 60dc967..749017b 100644 --- a/src/espbridge.c +++ b/src/espbridge.c @@ -23,10 +23,10 @@ #include "audiomoth.h" #include "espbridge.h" -/* AudioMoth Dev left JST header */ -#define BRIDGE_UART USART1 -#define BRIDGE_UART_CLOCK cmuClock_USART1 -#define BRIDGE_UART_LOCATION USART_ROUTE_LOCATION_LOC2 +/* 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 @@ -62,7 +62,7 @@ static inline void gpioWrite(GPIO_Port_TypeDef port, unsigned int pin, bool valu } static inline bool uartRxAvailable(void) { - return (USART_StatusGet(BRIDGE_UART) & USART_STATUS_RXDATAV) != 0; + return (USART_StatusGet(BRIDGE_UART) & UART_STATUS_RXDATAV) != 0; } static uint8_t uartReadByte(void) { @@ -382,7 +382,7 @@ void ESPBridge_init(void) { CMU_ClockEnable(BRIDGE_UART_CLOCK, true); GPIO_PinModeSet(BRIDGE_TX_PORT, BRIDGE_TX_PIN, gpioModePushPull, 1); - GPIO_PinModeSet(BRIDGE_RX_PORT, BRIDGE_RX_PIN, gpioModeInput, 0); + 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); @@ -391,8 +391,8 @@ void ESPBridge_init(void) { init.oversampling = usartOVS4; USART_InitAsync(BRIDGE_UART, &init); - BRIDGE_UART->ROUTE = USART_ROUTE_TXPEN | - USART_ROUTE_RXPEN | + BRIDGE_UART->ROUTE = UART_ROUTE_TXPEN | + UART_ROUTE_RXPEN | BRIDGE_UART_LOCATION; bridgeBusy = true;