301 lines
6.6 KiB
C
301 lines
6.6 KiB
C
/*
|
|
* 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 <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#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 <hh:mm> [dayspec] OR alarms remove <index> OR "
|
|
"alarms <clear>",
|
|
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);
|
|
}
|