Move all firmware files to subdirectory
This commit is contained in:
4
firmware/CMakeLists.txt
Normal file
4
firmware/CMakeLists.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(bedside_clock)
|
||||
5
firmware/components/alarms/CMakeLists.txt
Normal file
5
firmware/components/alarms/CMakeLists.txt
Normal 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
|
||||
)
|
||||
77
firmware/components/alarms/alarm_store.c
Normal file
77
firmware/components/alarms/alarm_store.c
Normal 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);
|
||||
}
|
||||
26
firmware/components/alarms/alarm_store.h
Normal file
26
firmware/components/alarms/alarm_store.h
Normal 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
|
||||
19
firmware/components/alarms/alarm_types.h
Normal file
19
firmware/components/alarms/alarm_types.h
Normal 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
|
||||
300
firmware/components/alarms/alarms.c
Normal file
300
firmware/components/alarms/alarms.c
Normal 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);
|
||||
}
|
||||
25
firmware/components/alarms/alarms.h
Normal file
25
firmware/components/alarms/alarms.h
Normal 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
|
||||
5
firmware/components/buttons/CMakeLists.txt
Normal file
5
firmware/components/buttons/CMakeLists.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
idf_component_register(
|
||||
SRCS "buttons.c"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES alarms driver esp_timer fatal
|
||||
)
|
||||
104
firmware/components/buttons/buttons.c
Normal file
104
firmware/components/buttons/buttons.c
Normal 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();
|
||||
}
|
||||
}
|
||||
16
firmware/components/buttons/buttons.h
Normal file
16
firmware/components/buttons/buttons.h
Normal 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
|
||||
5
firmware/components/console_wrapper/CMakeLists.txt
Normal file
5
firmware/components/console_wrapper/CMakeLists.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
idf_component_register(
|
||||
SRCS "console.c"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES console system_utils
|
||||
)
|
||||
64
firmware/components/console_wrapper/console.c
Normal file
64
firmware/components/console_wrapper/console.c
Normal 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);
|
||||
}
|
||||
25
firmware/components/console_wrapper/console.h
Normal file
25
firmware/components/console_wrapper/console.h
Normal 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
|
||||
5
firmware/components/display/CMakeLists.txt
Normal file
5
firmware/components/display/CMakeLists.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
idf_component_register(
|
||||
SRCS "display.c" "display_driver.c"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES driver fatal esp_timer time
|
||||
)
|
||||
84
firmware/components/display/display.c
Normal file
84
firmware/components/display/display.c
Normal 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);
|
||||
}
|
||||
17
firmware/components/display/display.h
Normal file
17
firmware/components/display/display.h
Normal 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
|
||||
102
firmware/components/display/display_driver.c
Normal file
102
firmware/components/display/display_driver.c
Normal 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);
|
||||
}
|
||||
}
|
||||
49
firmware/components/display/display_driver.h
Normal file
49
firmware/components/display/display_driver.h
Normal 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
|
||||
5
firmware/components/fatal/CMakeLists.txt
Normal file
5
firmware/components/fatal/CMakeLists.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
idf_component_register(
|
||||
SRCS "fatal.c"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES log
|
||||
)
|
||||
16
firmware/components/fatal/fatal.c
Normal file
16
firmware/components/fatal/fatal.c
Normal 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) { }
|
||||
}
|
||||
21
firmware/components/fatal/fatal.h
Normal file
21
firmware/components/fatal/fatal.h
Normal 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
|
||||
5
firmware/components/settings/CMakeLists.txt
Normal file
5
firmware/components/settings/CMakeLists.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
idf_component_register(
|
||||
SRCS "settings.c"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES console_wrapper fatal nvs_flash
|
||||
)
|
||||
296
firmware/components/settings/settings.c
Normal file
296
firmware/components/settings/settings.c
Normal 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);
|
||||
}
|
||||
167
firmware/components/settings/settings.h
Normal file
167
firmware/components/settings/settings.h
Normal 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
|
||||
5
firmware/components/sound/CMakeLists.txt
Normal file
5
firmware/components/sound/CMakeLists.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
idf_component_register(
|
||||
SRCS "sound.c"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES console_wrapper driver esp_timer
|
||||
)
|
||||
129
firmware/components/sound/sound.c
Normal file
129
firmware/components/sound/sound.c
Normal 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;
|
||||
}
|
||||
}
|
||||
24
firmware/components/sound/sound.h
Normal file
24
firmware/components/sound/sound.h
Normal 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
|
||||
5
firmware/components/system_utils/CMakeLists.txt
Normal file
5
firmware/components/system_utils/CMakeLists.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
idf_component_register(
|
||||
SRCS "system_utils.c"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES esp_system fatal nvs_flash time
|
||||
)
|
||||
79
firmware/components/system_utils/system_utils.c
Normal file
79
firmware/components/system_utils/system_utils.c
Normal 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();
|
||||
}
|
||||
27
firmware/components/system_utils/system_utils.h
Normal file
27
firmware/components/system_utils/system_utils.h
Normal 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
|
||||
5
firmware/components/time/CMakeLists.txt
Normal file
5
firmware/components/time/CMakeLists.txt
Normal 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
|
||||
)
|
||||
244
firmware/components/time/time_manager.c
Normal file
244
firmware/components/time/time_manager.c
Normal 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";
|
||||
}
|
||||
}
|
||||
53
firmware/components/time/time_manager.h
Normal file
53
firmware/components/time/time_manager.h
Normal 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
|
||||
98
firmware/components/time/time_sntp.c
Normal file
98
firmware/components/time/time_sntp.c
Normal 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");
|
||||
}
|
||||
19
firmware/components/time/time_sntp.h
Normal file
19
firmware/components/time/time_sntp.h
Normal 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
|
||||
73
firmware/components/time/time_storage.c
Normal file
73
firmware/components/time/time_storage.c
Normal 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, ×tamp);
|
||||
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);
|
||||
}
|
||||
20
firmware/components/time/time_storage.h
Normal file
20
firmware/components/time/time_storage.h
Normal 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
|
||||
32
firmware/components/time/time_types.h
Normal file
32
firmware/components/time/time_types.h
Normal 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
|
||||
5
firmware/components/wifi/CMakeLists.txt
Normal file
5
firmware/components/wifi/CMakeLists.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
idf_component_register(
|
||||
SRCS "wifi.c"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES esp_event esp_system esp_wifi settings
|
||||
)
|
||||
311
firmware/components/wifi/wifi.c
Normal file
311
firmware/components/wifi/wifi.c
Normal 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;
|
||||
}
|
||||
29
firmware/components/wifi/wifi.h
Normal file
29
firmware/components/wifi/wifi.h
Normal 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
|
||||
6
firmware/main/CMakeLists.txt
Normal file
6
firmware/main/CMakeLists.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
idf_component_register(
|
||||
SRCS "main.c"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES
|
||||
buttons console_wrapper display settings sound system_utils time wifi
|
||||
)
|
||||
48
firmware/main/Kconfig.projbuild
Normal file
48
firmware/main/Kconfig.projbuild
Normal 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
28
firmware/main/main.c
Normal 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
6
firmware/partitions.csv
Normal 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
|
||||
|
5
firmware/sdkconfig.defaults
Normal file
5
firmware/sdkconfig.defaults
Normal 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
|
||||
Reference in New Issue
Block a user