Move all firmware files to subdirectory

This commit is contained in:
2023-05-21 15:06:44 +01:00
parent 3d8ec33cd3
commit 8c957d043a
46 changed files with 0 additions and 0 deletions

4
firmware/CMakeLists.txt Normal file
View File

@@ -0,0 +1,4 @@
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(bedside_clock)

View File

@@ -0,0 +1,5 @@
idf_component_register(
SRCS "alarm_store.c" "alarms.c"
INCLUDE_DIRS "."
REQUIRES console_wrapper esp_partition fatal sound system_utils time
)

View File

@@ -0,0 +1,77 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) Camden Dixie O'Brien
*/
#include "alarm_store.h"
#include "fatal.h"
#include "esp_log.h"
#include "esp_partition.h"
#include "system_utils.h"
#include <string.h>
#define TAG "Alarm store"
Alarm alarms[CONFIG_MAX_ALARMS];
static const esp_partition_t *partition;
static unsigned erase_size(void)
{
unsigned sector_count = sizeof(alarms) / partition->erase_size;
if (sizeof(alarms) % partition->erase_size != 0)
++sector_count;
return partition->erase_size * sector_count;
}
static void load()
{
const esp_err_t error
= esp_partition_read(partition, 0, alarms, sizeof(alarms));
if (error != ESP_OK) {
ESP_LOGE(TAG, "Error reading from alarms partition: %02x", error);
FATAL();
}
unsigned count = 0;
for (unsigned i = 0; i < CONFIG_MAX_ALARMS; ++i) {
if (alarms[i].set)
++count;
}
ESP_LOGI(TAG, "Loaded %u alarms from storage", count);
}
void alarm_store_init()
{
partition = esp_partition_find_first(
ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_UNDEFINED,
"alarms");
if (partition == NULL) {
ESP_LOGE(TAG, "Unable to find alarms partition");
FATAL();
}
if (is_first_boot()) {
ESP_LOGI(TAG, "Zeroing alarm store");
memset(alarms, 0, sizeof(alarms));
alarm_store_save();
} else {
load();
}
}
void alarm_store_save()
{
esp_err_t error;
error = esp_partition_erase_range(partition, 0, erase_size());
if (error != ESP_OK) {
ESP_LOGE(TAG, "Error erasing alarm storage: %02x", error);
return;
}
error = esp_partition_write(partition, 0, alarms, sizeof(alarms));
if (error != ESP_OK)
ESP_LOGE(TAG, "Error writing alarm storage: %02x", error);
}

View File

@@ -0,0 +1,26 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) Camden Dixie O'Brien
*/
#ifndef ALARM_STORE_H
#define ALARM_STORE_H
#include "alarm_types.h"
#include "sdkconfig.h"
#include <stdbool.h>
extern Alarm alarms[CONFIG_MAX_ALARMS];
/**
* Initialize alarm store and load alarms from storage.
*/
void alarm_store_init(void);
/**
* Save alarms to storage.
*/
void alarm_store_save(void);
#endif

View File

@@ -0,0 +1,19 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) Camden Dixie O'Brien
*/
#ifndef ALARM_TYPE_H
#define ALARM_TYPE_H
#include "time_types.h"
#include <stdbool.h>
typedef struct {
bool set;
Time time;
bool days[WEEK_DAY_COUNT];
} Alarm;
#endif

View File

@@ -0,0 +1,300 @@
/*
* 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);
}

View File

@@ -0,0 +1,25 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) Camden Dixie O'Brien
*/
#ifndef ALARMS_H
#define ALARMS_H
/**
* Intialize the alarms subsystem, loading any saved alarms from
* storage.
*/
void alarms_init(void);
/**
* Snooze the current alarm (if any).
*/
void alarm_snooze(void);
/**
* Dismiss the current alarm (if any).
*/
void alarm_dismiss(void);
#endif

View File

@@ -0,0 +1,5 @@
idf_component_register(
SRCS "buttons.c"
INCLUDE_DIRS "."
REQUIRES alarms driver esp_timer fatal
)

View File

@@ -0,0 +1,104 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) Camden Dixie O'Brien
*/
#include "buttons.h"
#include "alarms.h"
#include "fatal.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include "esp_timer.h"
#define TAG "Buttons"
#define PIN_BITMASK(n) ((uint64_t)1 << n)
#define SNOOZE_PIN GPIO_NUM_34
#define DISMISS_PIN GPIO_NUM_35
#define PRESS_HANDLE_PERIOD_US 300000UL
static bool snooze_pressed;
static bool dismiss_pressed;
static void handle_snooze_interrupt(void *arg)
{
(void)arg;
snooze_pressed = true;
}
static void handle_dismiss_interrupt(void *arg)
{
(void)arg;
dismiss_pressed = true;
}
static void handle_presses(void *arg)
{
(void)arg;
if (snooze_pressed) {
alarm_snooze();
snooze_pressed = false;
}
if (dismiss_pressed) {
alarm_dismiss();
dismiss_pressed = false;
}
}
void buttons_init()
{
esp_err_t error;
snooze_pressed = false;
dismiss_pressed = false;
const gpio_config_t pin_config = {
.pin_bit_mask = PIN_BITMASK(SNOOZE_PIN) | PIN_BITMASK(DISMISS_PIN),
.mode = GPIO_MODE_INPUT,
.pull_down_en = GPIO_PULLDOWN_ENABLE,
.intr_type = GPIO_INTR_POSEDGE,
};
error = gpio_config(&pin_config);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Error configuring GPIO: %04x", error);
FATAL();
}
error = gpio_install_isr_service(0);
if (error != ESP_OK) {
ESP_LOGE(
TAG, "Error installing GPIO interrupt service: %04x", error);
FATAL();
}
error = gpio_isr_handler_add(SNOOZE_PIN, &handle_snooze_interrupt, NULL);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Error adding snooze interrupt handler: %04x", error);
}
error
= gpio_isr_handler_add(DISMISS_PIN, &handle_dismiss_interrupt, NULL);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Error adding snooze interrupt handler: %04x", error);
FATAL();
}
const esp_timer_create_args_t timer_config = {
.callback = &handle_presses,
.name = "Buttons press handler",
};
esp_timer_handle_t timer;
error = esp_timer_create(&timer_config, &timer);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Error creating timer: %04x", error);
FATAL();
}
error = esp_timer_start_periodic(timer, PRESS_HANDLE_PERIOD_US);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Error starting timer: %04x", error);
FATAL();
}
}

View File

@@ -0,0 +1,16 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) Camden Dixie O'Brien
*/
#ifndef BUTTONS_H
#define BUTTONS_H
typedef void (*ButtonCallback)(void);
/**
* Initialize the buttons module.
*/
void buttons_init(void);
#endif

View File

@@ -0,0 +1,5 @@
idf_component_register(
SRCS "console.c"
INCLUDE_DIRS "."
REQUIRES console system_utils
)

View File

@@ -0,0 +1,64 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) Camden Dixie O'Brien
*/
#include "console.h"
#include "system_utils.h"
#include "esp_console.h"
#include "esp_log.h"
#define TAG "Console"
static esp_console_repl_t *repl;
static int reboot_command_func(int argc, char **argv)
{
(void)argc;
(void)argv;
reboot();
return 0;
}
void console_init(void)
{
esp_err_t error;
error = esp_console_register_help_command();
if (error != ESP_OK)
ESP_LOGE(TAG, "Error registering help command: %04x", error);
esp_console_repl_config_t repl_config
= ESP_CONSOLE_REPL_CONFIG_DEFAULT();
repl_config.prompt = ">";
esp_console_dev_uart_config_t uart_config
= ESP_CONSOLE_DEV_UART_CONFIG_DEFAULT();
error = esp_console_new_repl_uart(&uart_config, &repl_config, &repl);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Error initializing console REPL: %04x", error);
return;
}
error = esp_console_start_repl(repl);
if (error != ESP_OK)
ESP_LOGE(TAG, "Error starting console REPL: %04x", error);
console_register(
"reboot", "Reboot the system", "reboot", reboot_command_func);
}
void console_register(
const char *name, const char *help, const char *hint, CommandFunc func)
{
const esp_console_cmd_t command = {
.command = name,
.help = help,
.hint = hint,
.func = func,
};
const esp_err_t error = esp_console_cmd_register(&command);
if (error != ESP_OK)
ESP_LOGE(TAG, "Error registering command %s: %04x", name, error);
}

View File

@@ -0,0 +1,25 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) Camden Dixie O'Brien
*/
#ifndef CONSOLE_H
#define CONSOLE_H
typedef int (*CommandFunc)(int argc, char **argv);
/**
* Initialize and start the console.
*/
void console_init(void);
/**
* Register a console command.
*
* The name, help and hint should all be null-terminated strings. Hint
* should list possible arguments.
*/
void console_register(
const char *name, const char *help, const char *hint, CommandFunc func);
#endif

View File

@@ -0,0 +1,5 @@
idf_component_register(
SRCS "display.c" "display_driver.c"
INCLUDE_DIRS "."
REQUIRES driver fatal esp_timer time
)

View File

@@ -0,0 +1,84 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) Camden Dixie O'Brien
*/
#include "display.h"
#include "display_driver.h"
#include "fatal.h"
#include "time_manager.h"
#include "esp_log.h"
#include "esp_timer.h"
#include <string.h>
#define TAG "Display"
#define DRIVER_TASK_PERIOD_US 4000UL
#define CLAMP(x, lim) ((x) > (lim) ? (lim) : (x))
const DisplayDigitState digit_encodings[10] = {
[0] = { true, true, true, true, true, true, false, false },
[1] = { false, true, true, false, false, false, false, false },
[2] = { true, true, false, true, true, false, true, false },
[3] = { true, true, true, true, false, false, true, false },
[4] = { false, true, true, false, false, true, true, false },
[5] = { true, false, true, true, false, true, true, false },
[6] = { true, false, true, true, true, true, true, false },
[7] = { true, true, true, false, false, false, false, false },
[8] = { true, true, true, true, true, true, true, false },
[9] = { true, true, true, false, false, true, true, false },
};
static void show_digit(DisplayDigit digit, unsigned value)
{
memcpy(
&display_state[digit], digit_encodings[value],
sizeof(DisplayDigitState));
}
static void show_time(unsigned hour, unsigned minute)
{
if (hour > 99 || minute > 99)
ESP_LOGW(TAG, "Un-displayable time: %02u:%02u", hour, minute);
show_digit(DISPLAY_DIGIT_1, (hour / 10) % 10);
show_digit(DISPLAY_DIGIT_2, hour % 10);
show_digit(DISPLAY_DIGIT_3, (minute / 10) % 10);
show_digit(DISPLAY_DIGIT_4, minute % 10);
}
static void update_time(Time now)
{
show_time(now.hour, now.minute);
}
void display_init()
{
esp_err_t error;
display_driver_init();
esp_timer_handle_t driver_timer;
const esp_timer_create_args_t driver_timer_config = {
.callback = &display_driver_task,
.arg = NULL,
.name = "display driver task",
};
error = esp_timer_create(&driver_timer_config, &driver_timer);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Error creating timer for driver task: %04x", error);
FATAL();
}
error = esp_timer_start_periodic(driver_timer, DRIVER_TASK_PERIOD_US);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Error starting timer for driver task: %04x", error);
FATAL();
}
add_time_callback(&update_time);
}

View File

@@ -0,0 +1,17 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) Camden Dixie O'Brien
*/
#ifndef DISPLAY_H
#define DISPLAY_H
/**
* Initialize the display.
*
* This will configure the relevant GPIO pins and start a task that
* manages updates to the display.
*/
void display_init(void);
#endif

View File

@@ -0,0 +1,102 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) Camden Dixie O'Brien
*/
#include "display_driver.h"
#include "fatal.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include <string.h>
#define TAG "Display driver"
#define PIN_BITMASK(n) ((uint64_t)1 << n)
#define DIGIT_1_SELECT_PIN GPIO_NUM_15
#define DIGIT_2_SELECT_PIN GPIO_NUM_2
#define DIGIT_3_SELECT_PIN GPIO_NUM_0
#define DIGIT_4_SELECT_PIN GPIO_NUM_4
#define SEGMENT_A_PIN GPIO_NUM_16
#define SEGMENT_B_PIN GPIO_NUM_23
#define SEGMENT_C_PIN GPIO_NUM_19
#define SEGMENT_D_PIN GPIO_NUM_5
#define SEGMENT_E_PIN GPIO_NUM_17
#define SEGMENT_F_PIN GPIO_NUM_22
#define SEGMENT_G_PIN GPIO_NUM_21
#define SEGMENT_DP_PIN GPIO_NUM_18
static const gpio_num_t digit_select_pins[DISPLAY_DIGIT_COUNT] = {
[DISPLAY_DIGIT_1] = DIGIT_1_SELECT_PIN,
[DISPLAY_DIGIT_2] = DIGIT_2_SELECT_PIN,
[DISPLAY_DIGIT_3] = DIGIT_3_SELECT_PIN,
[DISPLAY_DIGIT_4] = DIGIT_4_SELECT_PIN,
};
static const gpio_num_t segment_pins[DISPLAY_SEGMENT_COUNT] = {
[DISPLAY_SEGMENT_A] = SEGMENT_A_PIN,
[DISPLAY_SEGMENT_B] = SEGMENT_B_PIN,
[DISPLAY_SEGMENT_C] = SEGMENT_C_PIN,
[DISPLAY_SEGMENT_D] = SEGMENT_D_PIN,
[DISPLAY_SEGMENT_E] = SEGMENT_E_PIN,
[DISPLAY_SEGMENT_F] = SEGMENT_F_PIN,
[DISPLAY_SEGMENT_G] = SEGMENT_G_PIN,
[DISPLAY_SEGMENT_DP] = SEGMENT_DP_PIN,
};
static DisplayDigit active_digit;
DisplayState display_state;
void display_driver_init()
{
esp_err_t error;
memset(&display_state, 0, sizeof(DisplayState));
active_digit = DISPLAY_DIGIT_1;
uint64_t digit_select_pin_bitmask = 0;
for (unsigned i = 0; i < DISPLAY_DIGIT_COUNT; ++i)
digit_select_pin_bitmask |= PIN_BITMASK(digit_select_pins[i]);
const gpio_config_t digit_select_gpio_config = {
.pin_bit_mask = digit_select_pin_bitmask,
.mode = GPIO_MODE_OUTPUT,
};
error = gpio_config(&digit_select_gpio_config);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Error configuring digit select pins: %04x", error);
FATAL();
}
uint64_t segment_pin_bitmask = 0;
for (unsigned i = 0; i < DISPLAY_SEGMENT_COUNT; ++i)
segment_pin_bitmask |= PIN_BITMASK(segment_pins[i]);
const gpio_config_t segment_gpio_config = {
.pin_bit_mask = segment_pin_bitmask,
.mode = GPIO_MODE_OUTPUT_OD,
.pull_down_en = GPIO_PULLDOWN_ENABLE,
};
error = gpio_config(&segment_gpio_config);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Error configuring segment pins: %04x", error);
FATAL();
}
(void)esp_log_level_set("gpio", ESP_LOG_WARN);
}
void display_driver_task(void *arg)
{
(void)gpio_set_level(digit_select_pins[active_digit], 0);
active_digit = (active_digit + 1) % DISPLAY_DIGIT_COUNT;
(void)gpio_set_level(digit_select_pins[active_digit], 1);
for (unsigned i = 0; i < DISPLAY_SEGMENT_COUNT; ++i) {
(void)gpio_set_level(
segment_pins[i], display_state[active_digit][i] ? 0 : 1);
}
}

View File

@@ -0,0 +1,49 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) Camden Dixie O'Brien
*/
#ifndef DISPLAY_DRIVER_H
#define DISPLAY_DRIVER_H
#include <stdbool.h>
typedef enum {
DISPLAY_SEGMENT_A,
DISPLAY_SEGMENT_B,
DISPLAY_SEGMENT_C,
DISPLAY_SEGMENT_D,
DISPLAY_SEGMENT_E,
DISPLAY_SEGMENT_F,
DISPLAY_SEGMENT_G,
DISPLAY_SEGMENT_DP,
DISPLAY_SEGMENT_COUNT,
} DisplaySegment;
typedef enum {
DISPLAY_DIGIT_1,
DISPLAY_DIGIT_2,
DISPLAY_DIGIT_3,
DISPLAY_DIGIT_4,
DISPLAY_DIGIT_COUNT,
} DisplayDigit;
typedef bool DisplayDigitState[DISPLAY_SEGMENT_COUNT];
typedef DisplayDigitState DisplayState[DISPLAY_DIGIT_COUNT];
/**
* The current state of the display.
*/
extern DisplayState display_state;
/**
* Initialize the display driver.
*/
void display_driver_init();
/**
* Driver update task; should be ran regularly with a short period.
*/
void display_driver_task(void *arg);
#endif

View File

@@ -0,0 +1,5 @@
idf_component_register(
SRCS "fatal.c"
INCLUDE_DIRS "."
REQUIRES log
)

View File

@@ -0,0 +1,16 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) Camden Dixie O'Brien
*/
#include "fatal.h"
#include "esp_log.h"
#define TAG "Fatal"
void _fatal(const char *func, const char *file, unsigned line)
{
ESP_LOGE(TAG, "%s() @ %s:%u", func, file, line);
while (1) { }
}

View File

@@ -0,0 +1,21 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) Camden Dixie O'Brien
*
* Fatal error module.
*
* This small module provides the FATAL() macro, intended to be used
* to signal a fatal error. This prompts a system restart.
*/
#ifndef FATAL_H
#define FATAL_H
/**
* Signals a fatal error.
*/
#define FATAL() _fatal(__func__, __FILE__, __LINE__)
void _fatal(const char *func, const char *file, unsigned line);
#endif

View File

@@ -0,0 +1,5 @@
idf_component_register(
SRCS "settings.c"
INCLUDE_DIRS "."
REQUIRES console_wrapper fatal nvs_flash
)

View File

@@ -0,0 +1,296 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) Camden Dixie O'Brien
*/
#include "settings.h"
#include "console.h"
#include "fatal.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include <stdbool.h>
#include <string.h>
#define TAG "Settings"
#define NAMESPACE "settings"
#define MAX_CALLBACKS 8
typedef enum {
HOSTNAME,
SSID,
PSK,
TIMEZONE,
SNTP_SERVER,
ITEM_COUNT,
} ItemIndex;
typedef struct {
const char *id;
const char *default_value;
char value[SETTINGS_MAX_VALUE_SIZE];
struct {
SettingsCallback funcs[MAX_CALLBACKS];
unsigned count;
} callbacks;
} Item;
static Item state[ITEM_COUNT] = {
[HOSTNAME] = {
.id = "hostname",
.default_value = CONFIG_DEFAULT_HOSTNAME,
},
[SSID] = {
.id = "ssid",
.default_value = CONFIG_DEFAULT_SSID,
},
[PSK] = {
.id = "psk",
.default_value = CONFIG_DEFAULT_PSK,
},
[TIMEZONE] = {
.id = "timezone",
.default_value = CONFIG_DEFAULT_TIMEZONE,
},
[SNTP_SERVER] = {
.id = "sntp-server",
.default_value = CONFIG_DEFAULT_SNTP_SERVER,
},
};
static bool load(ItemIndex item)
{
esp_err_t error;
nvs_handle_t handle;
error = nvs_open(NAMESPACE, NVS_READONLY, &handle);
if (error == ESP_ERR_NVS_NOT_FOUND)
return false;
if (error != ESP_OK) {
ESP_LOGE(TAG, "Error opening NVS: %04x", error);
return false;
}
size_t size = SETTINGS_MAX_VALUE_SIZE;
error = nvs_get_str(handle, state[item].id, state[item].value, &size);
if (error == ESP_ERR_NVS_NOT_FOUND) {
nvs_close(handle);
return false;
} else if (error != ESP_OK) {
ESP_LOGE(
TAG, "Error loading %s from storage: %04x", state[item].id,
error);
nvs_close(handle);
return false;
}
nvs_close(handle);
return true;
}
static void save(ItemIndex item)
{
esp_err_t error;
nvs_handle_t handle;
error = nvs_open(NAMESPACE, NVS_READWRITE, &handle);
if (error == ESP_ERR_NVS_NOT_FOUND)
return;
if (error != ESP_OK) {
ESP_LOGE(TAG, "Error opening NVS: %04x", error);
return;
}
error = nvs_set_str(handle, state[item].id, state[item].value);
if (error != ESP_OK) {
ESP_LOGE(
TAG, "Error loading %s from storage: %04x", state[item].id,
error);
nvs_close(handle);
return;
}
error = nvs_commit(handle);
if (error != ESP_OK)
ESP_LOGE(TAG, "Error commiting NVS update: %04x", error);
nvs_close(handle);
}
static void set(ItemIndex item, const char *value)
{
if (value == NULL) {
ESP_LOGW(
TAG, "Attempt to set %s to null pointer; ignored",
state[item].id);
return;
}
size_t len = strlen(value);
if (len >= SETTINGS_MAX_VALUE_SIZE) {
ESP_LOGW(
TAG, "%s value \"%s\" exceeds maximum size; truncated",
state[item].id, value);
len = SETTINGS_MAX_VALUE_SIZE - 1;
}
memcpy(state[item].value, value, len);
state[item].value[len] = '\0';
save(item);
for (unsigned i = 0; i < state[item].callbacks.count; ++i)
state[item].callbacks.funcs[i](state[item].value);
}
static size_t get(ItemIndex item, char *buffer, size_t buffer_size)
{
size_t len = strlen(state[item].value);
if (len < buffer_size) {
memcpy(buffer, state[item].value, len);
buffer[len] = '\0';
}
return len;
}
static void add_callback(ItemIndex item, SettingsCallback callback)
{
if (callback == NULL) {
ESP_LOGW(
TAG, "Attempt to add null callback for %s; ignored",
state[item].id);
} else if (state[item].callbacks.count >= MAX_CALLBACKS) {
ESP_LOGE(
TAG, "Max callbacks exceeded for %s; callback discarded",
state[item].id);
} else {
const unsigned pos = state[item].callbacks.count;
state[item].callbacks.funcs[pos] = callback;
++state[item].callbacks.count;
}
}
static int command_func(int argc, char **argv)
{
if (argc == 1) {
char buffer[SETTINGS_MAX_VALUE_SIZE];
for (unsigned i = 0; i < ITEM_COUNT; ++i) {
(void)get(i, buffer, SETTINGS_MAX_VALUE_SIZE);
printf("%-15s %s\n", state[i].id, buffer);
}
return 0;
} else if (argc == 2) {
for (unsigned i = 0; i < ITEM_COUNT; ++i) {
if (strcmp(state[i].id, argv[1]) != 0)
continue;
char buffer[SETTINGS_MAX_VALUE_SIZE];
(void)get(i, buffer, SETTINGS_MAX_VALUE_SIZE);
printf("%s\n", buffer);
return 0;
}
printf("Setting not found\n");
return 1;
} else if (argc == 3) {
for (unsigned i = 0; i < ITEM_COUNT; ++i) {
if (strcmp(state[i].id, argv[1]) != 0)
continue;
set(i, argv[2]);
return 0;
}
printf("Setting not found\n");
return 1;
} else {
printf("Invalid number of arguments\n");
return 1;
}
}
void settings_init()
{
for (ItemIndex item = (ItemIndex)0; item < ITEM_COUNT; ++item) {
if (!load(item)) {
set(item, state[item].default_value);
save(item);
}
}
console_register(
"settings", "Get or set a setting",
"settings [id] OR settings <id> <value>", command_func);
}
void settings_set_hostname(const char *hostname)
{
set(HOSTNAME, hostname);
}
size_t settings_get_hostname(char *buffer, size_t buffer_size)
{
return get(HOSTNAME, buffer, buffer_size);
}
void settings_add_hostname_callback(SettingsCallback callback)
{
add_callback(HOSTNAME, callback);
}
void settings_set_ssid(const char *ssid)
{
set(SSID, ssid);
}
size_t settings_get_ssid(char *buffer, size_t buffer_size)
{
return get(SSID, buffer, buffer_size);
}
void settings_add_ssid_callback(SettingsCallback callback)
{
add_callback(SSID, callback);
}
void settings_set_psk(const char *psk)
{
set(PSK, psk);
}
size_t settings_get_psk(char *buffer, size_t buffer_size)
{
return get(PSK, buffer, buffer_size);
}
void settings_add_psk_callback(SettingsCallback callback)
{
add_callback(PSK, callback);
}
void settings_set_timezone(const char *timezone)
{
set(TIMEZONE, timezone);
}
size_t settings_get_timezone(char *buffer, size_t buffer_size)
{
return get(TIMEZONE, buffer, buffer_size);
}
void settings_add_timezone_callback(SettingsCallback callback)
{
add_callback(TIMEZONE, callback);
}
void settings_set_sntp_server(const char *sntp_server)
{
set(SNTP_SERVER, sntp_server);
}
size_t settings_get_sntp_server(char *buffer, size_t buffer_size)
{
return get(SNTP_SERVER, buffer, buffer_size);
}
void settings_add_sntp_server_callback(SettingsCallback callback)
{
add_callback(SNTP_SERVER, callback);
}

View File

@@ -0,0 +1,167 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) Camden Dixie O'Brien
*/
#ifndef SETTINGS_H
#define SETTINGS_H
#include <stddef.h>
#define SETTINGS_MAX_VALUE_SIZE 32U
/**
* Callback type for settings updates
*/
typedef void (*SettingsCallback)(const char *value);
/**
* Initialize the settings module.
*
* If there are saved settings, they will be loaded. Otherwise,
* the default settings will be loaded and saved.
*/
void settings_init(void);
/**
* Set the device's hostname.
*
* The argument should be a null-terminated string. If the maximum
* length is exceeded, the value will still be used, but will be
* truncated.
*/
void settings_set_hostname(const char *hostname);
/**
* Write the device's hostname into the given buffer.
*
* The length of the hostname is returned. If the value's size exceeds
* the size of the buffer, nothing will be written to the buffer but
* the length is still returned.
*/
size_t settings_get_hostname(char *buffer, size_t buffer_size);
/**
* Add a callback for hostname updates.
*
* The function specified in the argument will be invoked whenever
* the hostname is updated, with the new value as its argument. The
* lifetime of the passed argument will be static, but the value may
* be modified once the callback returns.
*/
void settings_add_hostname_callback(SettingsCallback callback);
/**
* Set the SSID of the WiFi network.
*
* The argument should be a null-terminated string. If the maximum
* length is exceeded, the value will still be used, but will be
* truncated.
*/
void settings_set_ssid(const char *ssid);
/**
* Write the SSID of the WiFi network into the given buffer.
*
* The length of the SSID is returned. If the value's size exceeds
* the size of the buffer, nothing will be written to the buffer but
* the length is still returned.
*/
size_t settings_get_ssid(char *buffer, size_t buffer_size);
/**
* Add a callback for SSID updates.
*
* The function specified in the argument will be invoked whenever
* the SSID is updated, with the new value as its argument. The
* lifetime of the passed argument will be static, but the value may
* be modified once the callback returns.
*/
void settings_add_ssid_callback(SettingsCallback callback);
/**
* Set the PSK for the WiFi network.
*
* The argument should be a null-terminated string. If the maximum
* length is exceeded, the value will still be used, but will be
* truncated.
*/
void settings_set_psk(const char *psk);
/**
* Write the PSK for the WiFi network into the given buffer.
*
* The length of the psk is returned. If the value's size exceeds
* the size of the buffer, nothing will be written to the buffer but
* the length is still returned.
*/
size_t settings_get_psk(char *buffer, size_t buffer_size);
/**
* Add a callback for PSK updates.
*
* The function specified in the argument will be invoked whenever the
* PSK is updated, with the new value as its argument. The lifetime of
* the passed argument will be static, but the value may be modified
* once the callback returns.
*/
void settings_add_psk_callback(SettingsCallback callback);
/**
* Set the timezone.
*
* The argument should be a null-terminated string, containing a
* timezone spec in the format expected by tzset(). If the maximum
* length is exceeded, the value will still be used, but will be
* truncated.
*/
void settings_set_timezone(const char *psk);
/**
* Write the timezone into the given buffer.
*
* The length of the timezone is returned. If the value's size exceeds
* the size of the buffer, nothing will be written to the buffer but
* the length is still returned.
*/
size_t settings_get_timezone(char *buffer, size_t buffer_size);
/**
* Add a callback for timezone updates.
*
* The function specified in the argument will be invoked whenever the
* timezone is updated, with the new value as its argument. The
* lifetime of the passed argument will be static, but the value may
* be modified once the callback returns.
*/
void settings_add_timezone_callback(SettingsCallback callback);
/**
* Set the SNTP server URL.
*
* The argument should be a null-terminated string, containing a valid
* domain name for an SNTP server. If the maximum length is exceeded,
* the value will still be used, but will be truncated.
*/
void settings_set_sntp_server(const char *sntp_server);
/**
* Write the SNTP server URL into the given buffer.
*
* The length of the SNTP server domain is returned. If the value's
* size exceeds the size of the buffer, nothing will be written to the
* buffer but the length is still returned.
*/
size_t settings_get_sntp_server(char *buffer, size_t buffer_size);
/**
* Add a callback for SNTP server URL updates.
*
* The function specified in the argument will be invoked whenever the
* SNTP server is updated, with the new value as its argument. The
* lifetime of the passed argument will be static, but the value may
* be modified once the callback returns.
*/
void settings_add_sntp_server_callback(SettingsCallback callback);
#endif

View File

@@ -0,0 +1,5 @@
idf_component_register(
SRCS "sound.c"
INCLUDE_DIRS "."
REQUIRES console_wrapper driver esp_timer
)

View File

@@ -0,0 +1,129 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) Camden Dixie O'Brien
*/
#include "sound.h"
#include "console.h"
#include "driver/dac.h"
#include "esp_log.h"
#include "esp_timer.h"
#include <string.h>
#define TAG "Sound"
#define CHANNEL DAC_CHANNEL_1
#define SIGNAL_FREQ CONFIG_ALERT_FREQ
#define SIGNAL_AMPLITUDE CONFIG_ALERT_AMPLITUDE
#define TOGGLE_PERIOD_US (1000 * CONFIG_ALERT_TOGGLE_PERIOD_MS)
static esp_timer_handle_t timer;
static bool signal_active;
static void toggle_signal(void *arg)
{
(void)arg;
if (signal_active) {
const esp_err_t error = dac_cw_generator_enable();
if (error != ESP_OK)
ESP_LOGE(TAG, "Error enabling signal: %04x", error);
else
signal_active = false;
} else {
const esp_err_t error = dac_cw_generator_disable();
if (error != ESP_OK)
ESP_LOGE(TAG, "Error disabling signal: %04x", error);
else
signal_active = true;
}
}
static int alert_command_func(int argc, char **argv)
{
if (argc != 2) {
printf("Invalid number of arguments\n");
return 1;
} else if (strcmp(argv[1], "on") == 0) {
sound_alert_on();
return 0;
} else if (strcmp(argv[1], "off") == 0) {
sound_alert_off();
return 0;
} else {
printf("Invalid state %s\n", argv[1]);
return 1;
}
}
void sound_init()
{
esp_err_t error;
dac_cw_config_t wave_gen_config = {
.en_ch = CHANNEL,
.scale = SIGNAL_AMPLITUDE,
.freq = SIGNAL_FREQ,
};
error = dac_cw_generator_config(&wave_gen_config);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Error configuring wave generator: %04x", error);
return;
}
const esp_timer_create_args_t timer_config = {
.callback = &toggle_signal,
.arg = NULL,
.name = "signal toggle task",
};
error = esp_timer_create(&timer_config, &timer);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Error creating toggle timer: %04x", error);
return;
}
console_register(
"alert", "Set alert sound on and off", "alert <on|off>",
alert_command_func);
}
void sound_alert_on()
{
esp_err_t error;
error = dac_output_enable(CHANNEL);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Error enabling DAC output: %04x", error);
return;
}
signal_active = false;
toggle_signal(NULL);
error = esp_timer_start_periodic(timer, TOGGLE_PERIOD_US);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Error starting toggle timer: %04x", error);
return;
}
}
void sound_alert_off()
{
esp_err_t error;
error = esp_timer_stop(timer);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Error stopping toggle timer: %04x", error);
return;
}
if (signal_active)
toggle_signal(NULL);
error = dac_output_disable(CHANNEL);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Error disabling DAC output: %04x", error);
return;
}
}

View File

@@ -0,0 +1,24 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) Camden Dixie O'Brien
*/
#ifndef SOUND_H
#define SOUND_H
/**
* Initialize the sound subsystem.
*/
void sound_init(void);
/**
* Turn the alert sound on.
*/
void sound_alert_on(void);
/**
* Turn the alert sound off.
*/
void sound_alert_off(void);
#endif

View File

@@ -0,0 +1,5 @@
idf_component_register(
SRCS "system_utils.c"
INCLUDE_DIRS "."
REQUIRES esp_system fatal nvs_flash time
)

View File

@@ -0,0 +1,79 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) Camden Dixie O'Brien
*/
#include "system_utils.h"
#include "fatal.h"
#include "time_storage.h"
#include "esp_log.h"
#include "esp_system.h"
#include "nvs_flash.h"
#define TAG "System utils"
#define NVS_NAMESPACE "system-utils"
#define BOOTED_FLAG_KEY "booted"
static bool first_boot;
static void test_first_boot(void)
{
esp_err_t error;
nvs_handle_t handle;
error = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Error opening NVS store namespace: %02x", error);
first_boot = false;
return;
}
uint8_t tmp;
error = nvs_get_u8(handle, BOOTED_FLAG_KEY, &tmp);
if (error == ESP_OK) {
first_boot = false;
} else if (error == ESP_ERR_NVS_NOT_FOUND) {
ESP_LOGI(TAG, "First boot of system");
first_boot = true;
error = nvs_set_u8(handle, BOOTED_FLAG_KEY, 1);
if (error != ESP_OK)
ESP_LOGE(TAG, "Error setting booted flag: %02x", error);
} else {
ESP_LOGE(TAG, "Error getting booted flag: %02x", error);
first_boot = false;
}
nvs_close(handle);
}
void early_init()
{
esp_err_t error = nvs_flash_init();
if (error == ESP_ERR_NVS_NO_FREE_PAGES
|| error == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_LOGI(TAG, "NVS partition full or outdated; erasing");
(void)nvs_flash_erase();
error = nvs_flash_init();
}
if (error != ESP_OK) {
ESP_LOGE(TAG, "Error initializing NVS store: %04x", error);
FATAL();
}
test_first_boot();
}
bool is_first_boot()
{
return first_boot;
}
void reboot()
{
ESP_LOGI(TAG, "Rebooting system");
time_storage_save();
esp_restart();
}

View File

@@ -0,0 +1,27 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) Camden Dixie O'Brien
*/
#ifndef SYSTEM_UTILS_H
#define SYSTEM_UTILS_H
#include <stdbool.h>
/**
* Perform system initialization that must occur before any other
* components are initialized.
*/
void early_init(void);
/**
* Return whether or not this is the first boot of the system.
*/
bool is_first_boot(void);
/**
* Reboot the system, storing the current time beforehand.
*/
void reboot(void);
#endif

View File

@@ -0,0 +1,5 @@
idf_component_register(
SRCS "time_manager.c" "time_sntp.c" "time_storage.c"
INCLUDE_DIRS "."
REQUIRES console_wrapper esp_timer fatal lwip nvs_flash settings
)

View File

@@ -0,0 +1,244 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) Camden Dixie O'Brien
*/
#include "time_manager.h"
#include "console.h"
#include "settings.h"
#include "time_sntp.h"
#include "time_storage.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "fatal.h"
#include <string.h>
#include <sys/time.h>
#define TAG "Time"
#define TM_YEAR_OFFSET 1900
#define TM_MONTH_OFFSET 1
#define UPDATE_PERIOD_US 1000000UL
#define MAX_CALLBACKS 8
static TimeCallback callbacks[MAX_CALLBACKS];
static unsigned callback_count;
static void handle_timezone_update(const char *timezone)
{
setenv("TZ", timezone, 1);
tzset();
time_sntp_restart();
}
static void run_callbacks(void *arg)
{
const Time time = get_time();
for (unsigned i = 0; i < callback_count; ++i)
callbacks[i](time);
}
static int time_command_func(int argc, char **argv)
{
if (argc == 1) {
const Time time = get_time();
printf("%02u:%02u:%02u\n", time.hour, time.minute, time.second);
return 0;
} else if (argc == 2) {
if (strcmp(argv[1], "store") == 0) {
time_storage_save();
return 0;
} else {
Time time;
const int result = sscanf(
argv[1], "%02u:%02u:%02u", &time.hour, &time.minute,
&time.second);
if (result < 2 || time.hour > 23 || time.minute > 59
|| time.second > 59) {
printf("Invalid time\n");
return 1;
}
set_time(time);
return 0;
}
} else {
printf("Invalid number of arguments\n");
return 1;
}
}
static int date_command_func(int argc, char **argv)
{
if (argc == 1) {
const Date date = get_date();
printf("%04u-%02u-%02u\n", date.year, date.month, date.day);
return 0;
} else if (argc == 2) {
if (strcmp(argv[1], "week-day") == 0) {
const WeekDay day = get_week_day();
printf("%s\n", week_day_name(day));
return 0;
}
Date date;
const int result = sscanf(
argv[1], "%04u-%02u-%02u", &date.year, &date.month, &date.day);
if (result != 3 || date.month == 0 || date.month > 12
|| date.day == 0 || date.day > 31) {
printf("Invalid date\n");
return 1;
}
set_date(date);
return 0;
} else {
printf("Invalid number of arguments\n");
return 1;
}
}
void time_manager_init(void)
{
esp_err_t error;
callback_count = 0;
time_sntp_init();
time_storage_init();
char timezone[SETTINGS_MAX_VALUE_SIZE];
(void)settings_get_timezone(timezone, SETTINGS_MAX_VALUE_SIZE);
handle_timezone_update(timezone);
settings_add_timezone_callback(&handle_timezone_update);
esp_timer_handle_t update_timer;
const esp_timer_create_args_t update_timer_config = {
.callback = &run_callbacks,
.arg = NULL,
.name = "time updates",
};
error = esp_timer_create(&update_timer_config, &update_timer);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Error creating update timer: %04x", error);
FATAL();
}
error = esp_timer_start_periodic(update_timer, UPDATE_PERIOD_US);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Error starting update timer: %04x", error);
FATAL();
}
console_register(
"time", "Get, set or store the time",
"time [hh:mm:ss] OR time store", time_command_func);
console_register(
"date", "Get or set the date, or get the day of the week",
"date [yyyy-mm-dd] OR date week-day", date_command_func);
}
void add_time_callback(TimeCallback callback)
{
if (callback_count >= MAX_CALLBACKS) {
ESP_LOGE(TAG, "Max number of time callbacks exceeded");
return;
}
callbacks[callback_count] = callback;
++callback_count;
}
Time get_time(void)
{
struct timeval tv;
gettimeofday(&tv, NULL);
struct tm timeinfo;
(void)localtime_r(&tv.tv_sec, &timeinfo);
return (Time) {
.hour = timeinfo.tm_hour,
.minute = timeinfo.tm_min,
.second = timeinfo.tm_sec,
};
}
void set_time(Time time)
{
struct timeval tv;
gettimeofday(&tv, NULL);
struct tm timeinfo;
(void)localtime_r(&tv.tv_sec, &timeinfo);
timeinfo.tm_hour = time.hour;
timeinfo.tm_min = time.minute;
timeinfo.tm_sec = time.second;
tv.tv_sec = mktime(&timeinfo);
settimeofday(&tv, NULL);
}
Date get_date(void)
{
struct timeval tv;
gettimeofday(&tv, NULL);
struct tm timeinfo;
(void)localtime_r(&tv.tv_sec, &timeinfo);
return (Date) {
.year = timeinfo.tm_year + TM_YEAR_OFFSET,
.month = timeinfo.tm_mon + TM_MONTH_OFFSET,
.day = timeinfo.tm_mday,
};
}
void set_date(Date date)
{
struct timeval tv;
gettimeofday(&tv, NULL);
struct tm timeinfo;
(void)localtime_r(&tv.tv_sec, &timeinfo);
timeinfo.tm_year = date.year - TM_YEAR_OFFSET;
timeinfo.tm_mon = date.month - TM_MONTH_OFFSET;
timeinfo.tm_mday = date.day;
tv.tv_sec = mktime(&timeinfo);
settimeofday(&tv, NULL);
}
WeekDay get_week_day(void)
{
struct timeval tv;
gettimeofday(&tv, NULL);
struct tm timeinfo;
(void)localtime_r(&tv.tv_sec, &timeinfo);
if (timeinfo.tm_wday == 0)
return WEEK_DAY_SUNDAY;
else
return (WeekDay)(timeinfo.tm_wday - 1);
}
const char *week_day_name(WeekDay day)
{
switch (day) {
case WEEK_DAY_MONDAY:
return "Monday";
case WEEK_DAY_TUESDAY:
return "Tuesday";
case WEEK_DAY_WEDNESDAY:
return "Wednesday";
case WEEK_DAY_THURSDAY:
return "Thursday";
case WEEK_DAY_FRIDAY:
return "Friday";
case WEEK_DAY_SATURDAY:
return "Saturday";
case WEEK_DAY_SUNDAY:
return "Sunday";
default:
ESP_LOGE(
TAG, "Invalid day of the week passed to %s(): %u", __func__,
(unsigned)day);
return "INVALID";
}
}

View File

@@ -0,0 +1,53 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) Camden Dixie O'Brien
*/
#ifndef TIME_MANAGER_H
#define TIME_MANAGER_H
#include "time_types.h"
typedef void (*TimeCallback)(Time now);
/**
* Initialize the time module.
*/
void time_manager_init(void);
/**
* Add a callback to be regularly invoked with time updates.
*/
void add_time_callback(TimeCallback callback);
/**
* Get the current time.
*/
Time get_time(void);
/**
* Set the time.
*/
void set_time(Time time);
/**
* Get the current date.
*/
Date get_date(void);
/**
* Set the date.
*/
void set_date(Date date);
/**
* Get which day of the week it is.
*/
WeekDay get_week_day(void);
/**
* Get the name of a day of the week.
*/
const char *week_day_name(WeekDay day);
#endif

View File

@@ -0,0 +1,98 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) Camden Dixie O'Brien
*/
#include "time_sntp.h"
#include "console.h"
#include "settings.h"
#include "esp_log.h"
#include "esp_sntp.h"
#define TAG "Time SNTP"
static void handle_sntp_server_update(const char *sntp_server)
{
esp_sntp_setservername(0, sntp_server);
time_sntp_restart();
}
static void sntp_sync_callback(struct timeval *tv)
{
const time_t now = tv->tv_sec;
struct tm timeinfo;
(void)localtime_r(&now, &timeinfo);
ESP_LOGI(
TAG, "Received SNTP time notification: %02u:%02u", timeinfo.tm_hour,
timeinfo.tm_min);
}
static const char *sync_status_description(sntp_sync_status_t status)
{
switch (status) {
case SNTP_SYNC_STATUS_RESET:
return "Reset";
case SNTP_SYNC_STATUS_COMPLETED:
return "Completed";
case SNTP_SYNC_STATUS_IN_PROGRESS:
return "In progress";
default:
return "Invalid";
}
}
static int command_func(int argc, char **argv)
{
if (argc == 2) {
if (strcmp(argv[1], "status") == 0) {
if (esp_sntp_enabled()) {
const sntp_sync_status_t status = sntp_get_sync_status();
printf("%s\n", sync_status_description(status));
} else {
printf("Disabled\n");
}
return 0;
} else if (strcmp(argv[1], "restart") == 0) {
time_sntp_restart();
return 0;
} else if (strcmp(argv[1], "stop") == 0) {
sntp_stop();
return 0;
} else if (strcmp(argv[1], "ip") == 0) {
const ip_addr_t *ip = esp_sntp_getserver(0);
printf("%s\n", ipaddr_ntoa(ip));
return 0;
} else {
printf("Unrecognised subcommand\n");
return 1;
}
} else {
printf("Invalid number of arguments\n");
return 1;
}
}
void time_sntp_init(void)
{
char sntp_server[SETTINGS_MAX_VALUE_SIZE];
(void)settings_get_sntp_server(sntp_server, SETTINGS_MAX_VALUE_SIZE);
settings_add_sntp_server_callback(&handle_sntp_server_update);
esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL);
esp_sntp_setservername(0, sntp_server);
esp_sntp_init();
sntp_set_time_sync_notification_cb(sntp_sync_callback);
console_register(
"sntp", "Manage SNTP", "sntp <status|restart|stop|ip>",
command_func);
}
void time_sntp_restart(void)
{
if (!sntp_restart())
ESP_LOGW(TAG, "SNTP restart requested, but SNTP is not running");
}

View File

@@ -0,0 +1,19 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) Camden Dixie O'Brien
*/
#ifndef TIME_SNTP_H
#define TIME_SNTP_H
/**
* Initialize SNTP time synchronisation.
*/
void time_sntp_init(void);
/**
* Restart SNTP
*/
void time_sntp_restart(void);
#endif

View File

@@ -0,0 +1,73 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) Camden Dixie O'Brien
*/
#include "time_storage.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "nvs_flash.h"
#include <sys/time.h>
#define TAG "Time storage"
#define NVS_NAMESPACE "time"
#define TIMESTAMP_KEY "timestamp"
static void time_saver_func(void *arg)
{
(void)arg;
const TickType_t delay = CONFIG_TIME_SAVE_PERIOD_MS / portTICK_PERIOD_MS;
while (1) {
time_storage_save();
vTaskDelay(delay);
}
}
void time_storage_init()
{
esp_err_t error;
nvs_handle_t nvs;
error = nvs_open(NVS_NAMESPACE, NVS_READONLY, &nvs);
if (error == ESP_OK) {
uint64_t timestamp;
error = nvs_get_u64(nvs, TIMESTAMP_KEY, &timestamp);
if (error == ESP_OK) {
struct timeval tv = { .tv_sec = (time_t)timestamp };
settimeofday(&tv, NULL);
} else {
if (error != ESP_ERR_NVS_NOT_FOUND)
ESP_LOGE(TAG, "Error getting stored time: %04x", error);
}
nvs_close(nvs);
} else {
if (error != ESP_ERR_NVS_NOT_FOUND)
ESP_LOGE(TAG, "Error opening NVS: %04x", error);
}
(void)xTaskCreate(
&time_saver_func, "time saver", CONFIG_DEFAULT_TASK_STACK, NULL, 1,
NULL);
}
void time_storage_save()
{
esp_err_t error;
nvs_handle_t nvs;
error = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Error opening NVS: %04x", error);
return;
}
struct timeval tv;
gettimeofday(&tv, NULL);
error = nvs_set_u64(nvs, TIMESTAMP_KEY, tv.tv_sec);
if (error != ESP_OK)
ESP_LOGE(TAG, "Error storing time: %04x", error);
nvs_close(nvs);
}

View File

@@ -0,0 +1,20 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) Camden Dixie O'Brien
*/
#ifndef TIME_STORAGE_H
#define TIME_STORAGE_H
/**
* Load the system time from persistent storage and set up a regular
* task to save the system time
*/
void time_storage_init(void);
/**
* Save the system time in persistent storage.
*/
void time_storage_save(void);
#endif

View File

@@ -0,0 +1,32 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) Camden Dixie O'Brien
*/
#ifndef TIME_TYPES_H
#define TIME_TYPES_H
typedef struct {
unsigned hour;
unsigned minute;
unsigned second;
} Time;
typedef struct {
unsigned year;
unsigned month;
unsigned day;
} Date;
typedef enum {
WEEK_DAY_MONDAY,
WEEK_DAY_TUESDAY,
WEEK_DAY_WEDNESDAY,
WEEK_DAY_THURSDAY,
WEEK_DAY_FRIDAY,
WEEK_DAY_SATURDAY,
WEEK_DAY_SUNDAY,
WEEK_DAY_COUNT,
} WeekDay;
#endif

View File

@@ -0,0 +1,5 @@
idf_component_register(
SRCS "wifi.c"
INCLUDE_DIRS "."
REQUIRES esp_event esp_system esp_wifi settings
)

View File

@@ -0,0 +1,311 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) Camden Dixie O'Brien
*/
#include "wifi.h"
#include "console.h"
#include "settings.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "freertos/event_groups.h"
#include <string.h>
#define TAG "Wifi"
#define CONFIG_MAX_IPV6_ADDRS 5
static wifi_config_t config;
static WifiStatus status;
static unsigned retries;
static bool have_ipv4;
static esp_netif_ip_info_t ipv4;
static esp_ip6_addr_t ipv6_addrs[CONFIG_MAX_IPV6_ADDRS];
static unsigned ipv6_count;
static esp_netif_t *net_if;
static const char *ipv6_type_str(esp_ip6_addr_t *addr)
{
switch (esp_netif_ip6_get_addr_type(addr)) {
case ESP_IP6_ADDR_IS_GLOBAL:
return "Global";
case ESP_IP6_ADDR_IS_LINK_LOCAL:
return "Link local";
case ESP_IP6_ADDR_IS_SITE_LOCAL:
return "Site local";
case ESP_IP6_ADDR_IS_UNIQUE_LOCAL:
return "Unique local";
case ESP_IP6_ADDR_IS_IPV4_MAPPED_IPV6:
return "IPv4 mapped";
default:
return "Unknown:";
}
}
static void wifi_event_handler(
void *arg, esp_event_base_t event_base, int32_t event_id,
void *event_data)
{
(void)arg;
(void)event_base;
esp_err_t error;
switch (event_id) {
case WIFI_EVENT_STA_START:
error = esp_wifi_connect();
if (error != ESP_OK)
ESP_LOGE(TAG, "Error connecting to WiFi: %04x", error);
break;
case WIFI_EVENT_STA_CONNECTED:
error = esp_netif_create_ip6_linklocal(net_if);
if (error != ESP_OK)
ESP_LOGE(TAG, "Error creating IPv6 address");
break;
case WIFI_EVENT_STA_DISCONNECTED:
if (retries < CONFIG_WIFI_MAX_RETRIES) {
ESP_LOGI(TAG, "Retrying connection");
error = esp_wifi_connect();
if (error != ESP_OK)
ESP_LOGE(TAG, "Error connecting to WiFi: %04x", error);
++retries;
} else {
ESP_LOGW(
TAG, "Failed to connect to network %s", config.sta.ssid);
}
break;
default:
ESP_LOGI(TAG, "Unhandled WiFi event: %ld", event_id);
break;
}
}
static void got_ip_event_handler(
void *arg, esp_event_base_t event_base, int32_t event_id,
void *event_data)
{
(void)arg;
(void)event_base;
switch (event_id) {
case IP_EVENT_STA_GOT_IP: {
ip_event_got_ip_t *got_ip_data = (ip_event_got_ip_t *)event_data;
memcpy(&ipv4, &got_ip_data->ip_info, sizeof(ipv4));
have_ipv4 = true;
ESP_LOGI(TAG, "Got IPv4 address " IPSTR, IP2STR(&ipv4.ip));
status = WIFI_STATUS_CONNECTED;
break;
}
case IP_EVENT_GOT_IP6: {
ip_event_got_ip6_t *got_ip6_data = (ip_event_got_ip6_t *)event_data;
memcpy(
&ipv6_addrs[ipv6_count], &got_ip6_data->ip6_info.ip,
sizeof(esp_ip6_addr_t));
ESP_LOGI(
TAG, "Got IPv6 address " IPV6STR " (%s)",
IPV62STR(ipv6_addrs[ipv6_count]),
ipv6_type_str(&ipv6_addrs[ipv6_count]));
++ipv6_count;
status = WIFI_STATUS_CONNECTED;
break;
}
default:
ESP_LOGI(TAG, "Unhandled IP event received: %ld", event_id);
break;
}
}
static void handle_hostname_update(const char *hostname)
{
const esp_err_t error = esp_netif_set_hostname(net_if, hostname);
if (error != ESP_OK)
ESP_LOGE(TAG, "Failed to set hostname: %04x", error);
wifi_reconnect();
}
static void handle_ssid_update(const char *ssid)
{
ESP_LOGI(TAG, "SSID updated");
if (strlen(ssid) > sizeof(config.sta.ssid) - 1)
ESP_LOGW(TAG, "SSID too long (truncated)");
strncpy((char *)config.sta.ssid, ssid, sizeof(config.sta.ssid));
config.sta.ssid[sizeof(config.sta.ssid) - 1] = '\0';
const esp_err_t error = esp_wifi_set_config(WIFI_IF_STA, &config);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Error setting config: %04x", error);
return;
}
wifi_reconnect();
}
static void handle_psk_update(const char *psk)
{
ESP_LOGI(TAG, "PSK updated");
if (strlen(psk) > sizeof(config.sta.password) - 1)
ESP_LOGW(TAG, "PSK too long (truncated)");
strncpy((char *)config.sta.password, psk, sizeof(config.sta.password));
config.sta.password[sizeof(config.sta.password) - 1] = '\0';
const esp_err_t error = esp_wifi_set_config(WIFI_IF_STA, &config);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Error setting config: %04x", error);
return;
}
wifi_reconnect();
}
static int command_func(int argc, char **argv)
{
if (argc != 2) {
printf("Invalid number of arguments\n");
return 1;
} else if (strcmp(argv[1], "status") == 0) {
printf(
"%s\n",
status == WIFI_STATUS_CONNECTED ? "Connected" : "Disconnected");
return 0;
} else if (strcmp(argv[1], "reconnect") == 0) {
wifi_reconnect();
return 0;
} else if (strcmp(argv[1], "ipv4") == 0) {
if (!have_ipv4) {
printf("IPv4 not configured\n");
return 1;
}
printf(IPSTR "\n", IP2STR(&ipv4.ip));
printf("Netmask: " IPSTR "\n", IP2STR(&ipv4.netmask));
printf("Gateway: " IPSTR "\n", IP2STR(&ipv4.gw));
return 0;
} else if (strcmp(argv[1], "ipv6") == 0) {
if (ipv6_count == 0) {
printf("IPv6 not configured\n");
return 1;
}
for (unsigned i = 0; i < ipv6_count; ++i) {
printf(
"%-12s " IPV6STR "\n", ipv6_type_str(&ipv6_addrs[i]),
IPV62STR(ipv6_addrs[i]));
}
return 0;
} else {
printf("Subcommand not recognised\n");
return 1;
}
}
void wifi_init(void)
{
esp_err_t error;
status = WIFI_STATUS_DISCONNECTED;
have_ipv4 = false;
ipv6_count = 0;
error = esp_netif_init();
if (error != ESP_OK) {
ESP_LOGE(TAG, "Error initializing network stack: %04x", error);
return;
}
error = esp_event_loop_create_default();
if (error != ESP_OK) {
ESP_LOGE(TAG, "Error creating event loop: %04x", error);
return;
}
net_if = esp_netif_create_default_wifi_sta();
wifi_init_config_t init_config = WIFI_INIT_CONFIG_DEFAULT();
error = esp_wifi_init(&init_config);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Error initializing WiFi stack: %04x", error);
return;
}
esp_event_handler_instance_t instance;
error = esp_event_handler_instance_register(
WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL, &instance);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Error registering WiFi event handler: %04x", error);
return;
}
error = esp_event_handler_instance_register(
IP_EVENT, ESP_EVENT_ANY_ID, &got_ip_event_handler, NULL, &instance);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Error registering IP event handler: %04x", error);
return;
}
error = esp_wifi_set_mode(WIFI_MODE_STA);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Error setting mode: %04x", error);
return;
}
char hostname[SETTINGS_MAX_VALUE_SIZE];
(void)settings_get_hostname(hostname, SETTINGS_MAX_VALUE_SIZE);
error = esp_netif_set_hostname(net_if, hostname);
if (error != ESP_OK)
ESP_LOGE(TAG, "Failed to set hostname: %04x", error);
memset(&config, 0, sizeof(config));
if (settings_get_ssid((char *)config.sta.ssid, sizeof(config.sta.ssid))
> sizeof(config.sta.ssid))
ESP_LOGW(TAG, "SSID too long (truncated)");
if (settings_get_psk(
(char *)config.sta.password, sizeof(config.sta.password))
> sizeof(config.sta.password))
ESP_LOGW(TAG, "PSK too long (truncated)");
error = esp_wifi_set_config(WIFI_IF_STA, &config);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Error setting config: %04x", error);
return;
}
error = esp_wifi_start();
if (error != ESP_OK) {
ESP_LOGE(TAG, "Error starting WiFi: %04x", error);
return;
}
settings_add_hostname_callback(&handle_hostname_update);
settings_add_ssid_callback(&handle_ssid_update);
settings_add_psk_callback(&handle_psk_update);
console_register(
"wifi", "Manage WiFi connection",
"wifi <status|reconnect|ipv4|ipv6>", command_func);
}
void wifi_reconnect(void)
{
esp_err_t error;
error = esp_wifi_disconnect();
if (error != ESP_OK)
ESP_LOGE(TAG, "Error disconnecting from WiFi: %04x", error);
error = esp_wifi_connect();
if (error != ESP_OK)
ESP_LOGE(TAG, "Error connecting to WiFi: %04x", error);
retries = 0;
}
WifiStatus get_wifi_status(void)
{
return status;
}

View File

@@ -0,0 +1,29 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) Camden Dixie O'Brien
*/
#ifndef WIFI_H
#define WIFI_H
typedef enum {
WIFI_STATUS_DISCONNECTED,
WIFI_STATUS_CONNECTED,
} WifiStatus;
/**
* Initialize the WiFi subsystem, and try to connect to the network.
*/
void wifi_init(void);
/**
* Disconnect and reconnect to WiFi.
*/
void wifi_reconnect(void);
/**
* Get the status of the WiFi connection.
*/
WifiStatus get_wifi_status(void);
#endif

View File

@@ -0,0 +1,6 @@
idf_component_register(
SRCS "main.c"
INCLUDE_DIRS "."
REQUIRES
buttons console_wrapper display settings sound system_utils time wifi
)

View File

@@ -0,0 +1,48 @@
menu "Bedside clock settings"
menu "Default settings"
config DEFAULT_HOSTNAME
string "Default hostname"
default "bedside-clock"
config DEFAULT_SSID
string "SSID of default WiFi network"
default ""
config DEFAULT_PSK
string "PSK for default WiFi network"
default ""
config DEFAULT_TIMEZONE
string "Default timezone (TZ format)"
default "UTCUTC-1,M3.5.0/1,M10.5.0/2"
config DEFAULT_SNTP_SERVER
string "Default SNTP server domain"
default "pool.ntp.org"
endmenu
menu "Alert sound options"
config ALERT_FREQ
int "Frequency (in Hz) of the alert sound"
default 400
config ALERT_AMPLITUDE
int "Amplitude (0 to 255) of the alert sound"
default 180
config ALERT_TOGGLE_PERIOD_MS
int "Toggle period of alert sound in milliseconds"
default 300
endmenu
config ALERT_MINUTES
int "Number of minutes before alarm turned off automatically"
default 2
config SNOOZE_MINUTES
int "Number of minutes to snooze alarms for"
default 5
config WIFI_MAX_RETRIES
int "Maximum number of times to retry connecting to WiFi network"
default 10
config TIME_SAVE_PERIOD_MS
int "How often (in ms) to save the time to persistent storage"
default 60000
config DEFAULT_TASK_STACK
int "Default task stack size (in words)"
default 4096
config MAX_ALARMS
int "Maximum number of alarms"
default 16
endmenu

28
firmware/main/main.c Normal file
View File

@@ -0,0 +1,28 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) Camden Dixie O'Brien
*/
#include "alarms.h"
#include "buttons.h"
#include "console.h"
#include "display.h"
#include "settings.h"
#include "sound.h"
#include "system_utils.h"
#include "time_manager.h"
#include "wifi.h"
void app_main(void)
{
early_init();
console_init();
settings_init();
wifi_init();
time_manager_init();
display_init();
sound_init();
alarms_init();
buttons_init();
}

6
firmware/partitions.csv Normal file
View File

@@ -0,0 +1,6 @@
# ESP-IDF Partition Table
# Name, Type, SubType, Offset, Size, Flags
nvs,data,nvs,0x9000,24K,
phy_init,data,phy,0xf000,4K,
factory,app,factory,0x10000,1M,
alarms,data,undefined,,8K
1 # ESP-IDF Partition Table
2 # Name, Type, SubType, Offset, Size, Flags
3 nvs,data,nvs,0x9000,24K,
4 phy_init,data,phy,0xf000,4K,
5 factory,app,factory,0x10000,1M,
6 alarms,data,undefined,,8K

View File

@@ -0,0 +1,5 @@
# This file was generated using idf.py save-defconfig. It can be edited manually.
# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration
#
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_FREERTOS_UNICORE=y