/* * SPDX-License-Identifier: AGPL-3.0-only * Copyright (c) Camden Dixie O'Brien */ #include "alarms.h" #include "alarm_store.h" #include "console.h" #include "sound.h" #include "time_manager.h" #include "esp_log.h" #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" #include #include #include #define TAG "Alarms" typedef struct { const Alarm *alarm; Time end; } ActiveAlarm; static ActiveAlarm active[CONFIG_MAX_ALARMS]; static unsigned active_count; static ActiveAlarm snoozed[CONFIG_MAX_ALARMS]; static unsigned snoozed_count; static Time last_check; static SemaphoreHandle_t state_mutex; static Time in_minutes(unsigned minutes) { Time time = get_time(); time.minute += minutes; if (time.minute >= 60) { time.minute %= 60; ++time.hour; } return time; } static bool add_alarm(Time time, bool days[WEEK_DAY_COUNT]) { for (unsigned i = 0; i < CONFIG_MAX_ALARMS; ++i) { if (!alarms[i].set) { alarms[i].set = true; alarms[i].time = time; memcpy(alarms[i].days, days, WEEK_DAY_COUNT * sizeof(bool)); alarm_store_save(); return true; } } return false; } static void remove_alarm(unsigned index) { alarms[index].set = false; alarm_store_save(); } static void activate(const Alarm *alarm) { if (alarm < &alarms[0] || alarm >= &alarms[CONFIG_MAX_ALARMS] || !alarm->set) { ESP_LOGE(TAG, "Invalid alarm passed to %s()", __func__); return; } if (active_count == 0) sound_alert_on(); active[active_count].alarm = alarm; active[active_count].end = in_minutes(CONFIG_ALERT_MINUTES); ++active_count; } static void dismiss(unsigned index) { if (index >= active_count) { ESP_LOGE( TAG, "Invalid active index %u passed to %s()", index, __func__); return; } if (active_count == 1) sound_alert_off(); for (unsigned i = index + 1; i < active_count; ++i) active[i - 1] = active[i]; --active_count; } static void snooze(unsigned index) { if (index >= active_count) { ESP_LOGE( TAG, "Invalid active index %u passed to %s()", index, __func__); return; } const Alarm *alarm = active[index].alarm; dismiss(index); snoozed[snoozed_count].alarm = alarm; snoozed[snoozed_count].end = in_minutes(CONFIG_SNOOZE_MINUTES); ++snoozed_count; ESP_LOGI(TAG, "Alarm %u snoozed", alarm - &alarms[0]); } static void unsnooze(unsigned index) { if (index >= snoozed_count) { ESP_LOGE( TAG, "Invalid snooze index %u passed to %s()", index, __func__); return; } activate(snoozed[index].alarm); for (unsigned i = index + 1; i < snoozed_count; ++i) snoozed[i - 1] = snoozed[i]; --snoozed_count; } static bool passed_since_last_check(Time time, Time now) { return last_check.hour <= time.hour && time.hour <= now.hour && last_check.minute <= time.minute && time.minute <= now.minute && last_check.second <= time.second && time.second <= now.second; } static void check_alarms(Time now) { // Skip if time hasn't changed since last check if (last_check.hour == now.hour && last_check.minute == now.minute && last_check.second == now.second) return; if (xSemaphoreTake(state_mutex, (TickType_t)10) == pdFALSE) return; for (unsigned i = 0; i < CONFIG_MAX_ALARMS; ++i) { if (!alarms[i].set) continue; if (passed_since_last_check(alarms[i].time, now) && alarms[i].days[get_week_day()]) activate(&alarms[i]); } for (unsigned i = 0; i < active_count; ++i) { if (passed_since_last_check(active[i].end, now)) dismiss(i); } for (unsigned i = 0; i < snoozed_count; ++i) { if (passed_since_last_check(snoozed[i].end, now)) unsnooze(i); } last_check = now; xSemaphoreGive(state_mutex); } static bool read_dayspec(const char *dayspec, bool days_out[WEEK_DAY_COUNT]) { if (strlen(dayspec) != WEEK_DAY_COUNT) return false; for (unsigned i = 0; i < WEEK_DAY_COUNT; ++i) { if (dayspec[i] == 'x') days_out[i] = true; else if (dayspec[i] == '-') days_out[i] = false; else return false; } return true; } static void format_dayspec( bool days[WEEK_DAY_COUNT], char dayspec_out[WEEK_DAY_COUNT + 1]) { for (unsigned i = 0; i < WEEK_DAY_COUNT; ++i) dayspec_out[i] = days[i] ? 'x' : '-'; dayspec_out[WEEK_DAY_COUNT] = '\0'; } static int command_func(int argc, char **argv) { if (argc == 1) { char dayspec[WEEK_DAY_COUNT + 1]; for (unsigned i = 0; i < CONFIG_MAX_ALARMS; ++i) { if (!alarms[i].set) continue; format_dayspec(alarms[i].days, dayspec); printf( "[%2u] %02u:%02u %s\n", i, alarms[i].time.hour, alarms[i].time.minute, dayspec); } return 0; } else if (argc == 2 && strcmp(argv[1], "clear") == 0) { memset(alarms, 0, sizeof(alarms)); alarm_store_save(); return 0; } else if (argc >= 3) { if (strcmp(argv[1], "add") == 0) { Time time = { .second = 0 }; int n = sscanf(argv[2], "%02u:%02u", &time.hour, &time.minute); if (n != 2) { printf("Invalid time\n"); return 1; } bool days[WEEK_DAY_COUNT]; if (argc == 4) { if (!read_dayspec(argv[3], days)) { printf("Invalid dayspec\n"); return 1; } } else { for (unsigned i = 0; i < WEEK_DAY_COUNT; ++i) days[i] = true; } if (!add_alarm(time, days)) { printf("Max number of alarms already set.\n"); return 1; } return 0; } else if (strcmp(argv[1], "remove") == 0) { unsigned index; int n = sscanf(argv[2], "%u", &index); if (n != 1 || !alarms[index].set) { printf("Invalid index\n"); return 1; } remove_alarm(index); return 0; } else { printf("Unrecognised subcommand\n"); return 1; } } else { printf("Invalid number of arguments\n"); return 1; } } void alarms_init(void) { memset(active, 0, sizeof(active)); active_count = 0; memset(snoozed, 0, sizeof(snoozed)); snoozed_count = 0; last_check = get_time(); alarm_store_init(); add_time_callback(check_alarms); console_register( "alarms", "List, add and remove alarms\n\nIf a dayspec is specified, it " "should be a sequence of 'x's and '-'s (for enabled or disabled " "respectively). For example: \"*-*-*--\" means Mondays, Wednesdays " "and Fridays only.", "alarms OR alarms add [dayspec] OR alarms remove OR " "alarms ", command_func); state_mutex = xSemaphoreCreateMutex(); } void alarm_snooze(void) { if (xSemaphoreTake(state_mutex, (TickType_t)10) == pdFALSE) { ESP_LOGE(TAG, "Unable to aquire state semaphore"); return; } if (active_count > 0) snooze(0); xSemaphoreGive(state_mutex); } void alarm_dismiss(void) { if (xSemaphoreTake(state_mutex, (TickType_t)10) == pdFALSE) { ESP_LOGE(TAG, "Unable to aquire state semaphore"); return; } if (active_count > 0) dismiss(0); xSemaphoreGive(state_mutex); }