diff --git a/components/display/display.c b/components/display/display.c index 3a963af4..d91df159 100644 --- a/components/display/display.c +++ b/components/display/display.c @@ -15,6 +15,7 @@ #include "platform_config.h" #include "tools.h" #include "display.h" +#include "services.h" #include "gds.h" #include "gds_default_if.h" #include "gds_draw.h" @@ -73,7 +74,9 @@ static const char *known_drivers[] = {"SH1106", "ILI9341", NULL }; + static void displayer_task(void *args); +static void display_sleep(void); struct GDS_Device *display; extern GDS_DetectFunc SSD1306_Detect, SSD132x_Detect, SH1106_Detect, SSD1675_Detect, SSD1322_Detect, SSD1351_Detect, ST77xx_Detect, ILI9341_Detect; @@ -174,11 +177,21 @@ void display_init(char *welcome) { if (height <= 64 && width > height * 2) displayer.artwork.offset = width - height - ARTWORK_BORDER; PARSE_PARAM(displayer.metadata_config, "artwork", ':', displayer.artwork.fit); } + + // and finally register ourselves to power off upon deep sleep + services_sleep_sethook(display_sleep); } free(config); } +/**************************************************************************************** + * + */ +static void display_sleep(void) { + GDS_DisplayOff(display); +} + /**************************************************************************************** * This is not thread-safe as displayer_task might be in the middle of line drawing * but it won't crash (I think) and making it thread-safe would be complicated for a diff --git a/components/led_strip/led_vu.c b/components/led_strip/led_vu.c index a0dd2d38..643ecdaf 100644 --- a/components/led_strip/led_vu.c +++ b/components/led_strip/led_vu.c @@ -33,6 +33,8 @@ static const char *TAG = "led_vu"; #define LED_VU_PEAK_HOLD 6U #define LED_VU_DEFAULT_GPIO 22 +#define LED_VU_RMT_INTR_NUM 20 + #define LED_VU_DEFAULT_LENGTH 19 #define LED_VU_MAX_LENGTH 255 @@ -90,7 +92,7 @@ void led_vu_init() strip.vu_odd = strip.length - 1; // create driver configuration - struct led_strip_t led_strip_config = { .rgb_led_type = RGB_LED_TYPE_WS2812 }; + struct led_strip_t led_strip_config = { .rgb_led_type = RGB_LED_TYPE_WS2812, .rmt_interrupt_num = LED_VU_RMT_INTR_NUM }; led_strip_config.access_semaphore = xSemaphoreCreateBinary(); led_strip_config.led_strip_length = strip.length; led_strip_config.led_strip_working = heap_caps_malloc(strip.length * sizeof(struct led_color_t), MALLOC_CAP_8BIT); diff --git a/components/services/audio_controls.c b/components/services/audio_controls.c index bd3c56ed..60792ccf 100644 --- a/components/services/audio_controls.c +++ b/components/services/audio_controls.c @@ -19,6 +19,7 @@ #include "buttons.h" #include "platform_config.h" #include "accessors.h" +#include "services.h" #include "audio_controls.h" typedef esp_err_t (actrls_config_map_handler) (const cJSON * member, actrls_config_t *cur_config,uint32_t offset); @@ -57,7 +58,7 @@ static const actrls_config_map_t actrls_config_map[] = // BEWARE: the actions below need to stay aligned with the corresponding enum to properly support json parsing // along with the actrls_t controls in LMS_controls, bt_sink and raop_sink #define EP(x) [x] = #x /* ENUM PRINT */ -static const char * actrls_action_s[ ] = { EP(ACTRLS_POWER),EP(ACTRLS_VOLUP),EP(ACTRLS_VOLDOWN),EP(ACTRLS_TOGGLE),EP(ACTRLS_PLAY), +static const char * actrls_action_s[ ] = { EP(ACTRLS_SLEEP),EP(ACTRLS_POWER),EP(ACTRLS_VOLUP),EP(ACTRLS_VOLDOWN),EP(ACTRLS_TOGGLE),EP(ACTRLS_PLAY), EP(ACTRLS_PAUSE),EP(ACTRLS_STOP),EP(ACTRLS_REW),EP(ACTRLS_FWD),EP(ACTRLS_PREV),EP(ACTRLS_NEXT), EP(BCTRLS_UP),EP(BCTRLS_DOWN),EP(BCTRLS_LEFT),EP(BCTRLS_RIGHT), EP(BCTRLS_PS0),EP(BCTRLS_PS1),EP(BCTRLS_PS2),EP(BCTRLS_PS3),EP(BCTRLS_PS4),EP(BCTRLS_PS5),EP(BCTRLS_PS6),EP(BCTRLS_PS7),EP(BCTRLS_PS8),EP(BCTRLS_PS9), @@ -170,13 +171,6 @@ static void control_handler(void *client, button_event_e event, button_press_e p actrls_config_t *key = (actrls_config_t*) client; actrls_action_detail_t action_detail; - // in raw mode, we just do normal action press *and* release, there is no longpress nor shift - if (current_raw_controls) { - ESP_LOGD(TAG, "calling action %u in raw mode", key->normal[0].action); - if (current_controls[key->normal[0].action]) (*current_controls[key->normal[0].action])(event == BUTTON_PRESSED); - return; - } - switch(press) { case BUTTON_NORMAL: if (long_press) action_detail = key->longpress[event == BUTTON_PRESSED ? 0 : 1]; @@ -195,7 +189,14 @@ static void control_handler(void *client, button_event_e event, button_press_e p // stop here if control hook served the request if (current_hook && (*current_hook)(key->gpio, action_detail.action, event, press, long_press)) return; - + + // in raw mode, we just do normal action press *and* release, there is no longpress nor shift + if (current_raw_controls && action_detail.action != ACTRLS_SLEEP) { + ESP_LOGD(TAG, "calling action %u in raw mode", key->normal[0].action); + if (current_controls[key->normal[0].action]) (*current_controls[key->normal[0].action])(event == BUTTON_PRESSED); + return; + } + // otherwise process using configuration if (action_detail.action == ACTRLS_REMAP) { // remap requested @@ -216,7 +217,10 @@ static void control_handler(void *client, button_event_e event, button_press_e p } else { ESP_LOGE(TAG,"Invalid profile name %s. Cannot remap buttons",action_detail.name); } - } else if (action_detail.action != ACTRLS_NONE) { + } else if (action_detail.action == ACTRLS_SLEEP) { + ESP_LOGI(TAG, "Sleep button pressed"); + services_sleep_activate(SLEEP_ONKEY); + } else if (action_detail.action != ACTRLS_NONE) { ESP_LOGD(TAG, "calling action %u", action_detail.action); if (current_controls[action_detail.action]) (*current_controls[action_detail.action])(event == BUTTON_PRESSED); } diff --git a/components/services/audio_controls.h b/components/services/audio_controls.h index fd45784e..137bda7e 100644 --- a/components/services/audio_controls.h +++ b/components/services/audio_controls.h @@ -11,7 +11,7 @@ #include "buttons.h" // BEWARE: this is the index of the array of action below (change actrls_action_s as well!) -typedef enum { ACTRLS_NONE = -1, ACTRLS_POWER,ACTRLS_VOLUP, ACTRLS_VOLDOWN, ACTRLS_TOGGLE, ACTRLS_PLAY, +typedef enum { ACTRLS_NONE = -1, ACTRLS_SLEEP, ACTRLS_POWER, ACTRLS_VOLUP, ACTRLS_VOLDOWN, ACTRLS_TOGGLE, ACTRLS_PLAY, ACTRLS_PAUSE, ACTRLS_STOP, ACTRLS_REW, ACTRLS_FWD, ACTRLS_PREV, ACTRLS_NEXT, BCTRLS_UP, BCTRLS_DOWN, BCTRLS_LEFT, BCTRLS_RIGHT, BCTRLS_PS0,BCTRLS_PS1,BCTRLS_PS2,BCTRLS_PS3,BCTRLS_PS4,BCTRLS_PS5,BCTRLS_PS6,BCTRLS_PS7,BCTRLS_PS8,BCTRLS_PS9, diff --git a/components/services/services.c b/components/services/services.c index b1c1dbc4..6ace648b 100644 --- a/components/services/services.c +++ b/components/services/services.c @@ -7,7 +7,11 @@ */ #include +#include "freertos/FreeRTOS.h" +#include "freertos/timers.h" #include "esp_log.h" +#include "esp_sleep.h" +#include "driver/rtc_io.h" #include "driver/gpio.h" #include "driver/ledc.h" #include "driver/i2c.h" @@ -20,6 +24,8 @@ #include "globdefs.h" #include "accessors.h" #include "messaging.h" +#include "buttons.h" +#include "services.h" extern void battery_svc_init(void); extern void monitor_svc_init(void); @@ -30,11 +36,19 @@ int i2c_system_speed = 400000; int spi_system_host = SPI_SYSTEM_HOST; int spi_system_dc_gpio = -1; int rmt_system_base_channel = RMT_CHANNEL_0; + pwm_system_t pwm_system = { .timer = LEDC_TIMER_0, .base_channel = LEDC_CHANNEL_0, .max = (1 << LEDC_TIMER_13_BIT), - }; +}; + +static EXT_RAM_ATTR struct { + uint64_t wake_gpio, wake_level; + uint32_t delay; +} sleep_config; + +static EXT_RAM_ATTR void (*sleep_hooks[16])(void); static const char *TAG = "services"; @@ -60,6 +74,9 @@ void set_chip_power_gpio(int gpio, char *value) { if (parsed) ESP_LOGI(TAG, "set GPIO %u to %s", gpio, value); } +/**************************************************************************************** + * + */ void set_exp_power_gpio(int gpio, char *value) { bool parsed = true; @@ -75,8 +92,113 @@ void set_exp_power_gpio(int gpio, char *value) { } else parsed = false; if (parsed) ESP_LOGI(TAG, "set expanded GPIO %u to %s", gpio, value); - } - +} + +/**************************************************************************************** + * + */ +static void sleep_gpio_handler(void *id, button_event_e event, button_press_e mode, bool long_press) { + if (event == BUTTON_PRESSED) services_sleep_activate(SLEEP_ONGPIO); +} + +/**************************************************************************************** + * + */ +static void sleep_init(void) { + char *config = config_alloc_get(NVS_TYPE_STR, "sleep_config"); + char *p; + + // do we want delay sleep + PARSE_PARAM(config, "delay", '=', sleep_config.delay); + sleep_config.delay *= 60*1000; + if (sleep_config.delay) { + ESP_LOGI(TAG, "Sleep inactivity of %d minute(s)", sleep_config.delay / (60*1000)); + } + + // get the wake criteria + if ((p = strcasestr(config, "wake"))) { + char list[32] = "", item[8]; + sscanf(p, "%*[^=]=%31[^,]", list); + p = list - 1; + while (p++ && sscanf(p, "%7[^|]", item)) { + int level = 0, gpio = atoi(item); + if (!rtc_gpio_is_valid_gpio(gpio)) { + ESP_LOGE(TAG, "invalid wake GPIO %d (not in RTC domain)", gpio); + } else { + sleep_config.wake_gpio |= 1LL << gpio; + } + if (sscanf(item, "%*[^:]:%d", &level)) sleep_config.wake_level |= level << gpio; + p = strchr(p, '|'); + } + + // when moving to esp-idf more recent than 4.4.x, multiple gpio wake-up with level specific can be done + if (sleep_config.wake_gpio) { + ESP_LOGI(TAG, "Sleep wake-up gpio bitmap 0x%llx (active 0x%llx)", sleep_config.wake_gpio, sleep_config.wake_level); + } + } + + // then get the gpio that activate sleep (we could check that we have a valid wake) + if ((p = strcasestr(config, "sleep"))) { + int gpio, level = 0; + char sleep[8] = ""; + sscanf(p, "%*[^=]=%7[^,]", sleep); + gpio = atoi(sleep); + if ((p = strchr(sleep, ':')) != NULL) level = atoi(p + 1); + ESP_LOGI(TAG, "Sleep activation gpio %d (active %d)", gpio, level); + button_create(NULL, gpio, level ? BUTTON_HIGH : BUTTON_LOW, true, 0, sleep_gpio_handler, 0, -1); + } +} + +/**************************************************************************************** + * + */ +void services_sleep_callback(uint32_t elapsed) { + if (sleep_config.delay && elapsed >= sleep_config.delay) { + services_sleep_activate(SLEEP_ONTIMER); + } +} + +/**************************************************************************************** + * + */ +void services_sleep_activate(sleep_cause_e cause) { + // call all sleep hooks that might want to do something + for (void (**hook)(void) = sleep_hooks; *hook; hook++) (*hook)(); + + // isolate all possible GPIOs, except the wake-up ones + esp_sleep_config_gpio_isolate(); + for (int i = 0; i < GPIO_NUM_MAX; i++) { + if (!rtc_gpio_is_valid_gpio(i) || ((1LL << i) & sleep_config.wake_gpio)) continue; + rtc_gpio_isolate(i); + } + + // is there just one GPIO + if (sleep_config.wake_gpio & (sleep_config.wake_gpio - 1)) { + ESP_LOGI(TAG, "going to sleep cause %d, wake-up on multiple GPIO, any '1' wakes up 0x%llx", cause, sleep_config.wake_gpio); + esp_sleep_enable_ext1_wakeup(sleep_config.wake_gpio, ESP_EXT1_WAKEUP_ANY_HIGH); + } else { + int gpio = __builtin_ctz(sleep_config.wake_gpio); + int level = (sleep_config.wake_level >> gpio) & 0x01; + ESP_LOGI(TAG, "going to sleep cause %d, wake-up on GPIO %d level %d", cause, gpio, level); + esp_sleep_enable_ext0_wakeup(gpio, level); + } + + // we need to use a timer in case the same button is used for sleep and wake-up and it's "pressed" vs "released" selected + if (cause == SLEEP_ONKEY) xTimerStart(xTimerCreate("sleepTimer", pdMS_TO_TICKS(1000), pdFALSE, NULL, (void (*)(void*)) esp_deep_sleep_start), 0); + else esp_deep_sleep_start(); +} + +/**************************************************************************************** + * + */ +void services_sleep_sethook(void (*hook)(void)) { + for (int i = 0; i < sizeof(sleep_hooks)/sizeof(void(*)(void)); i++) { + if (!sleep_hooks[i]) { + sleep_hooks[i] = hook; + return; + } + } +} /**************************************************************************************** * @@ -147,5 +269,6 @@ void services_init(void) { led_svc_init(); battery_svc_init(); - monitor_svc_init(); + monitor_svc_init(); + sleep_init(); } diff --git a/components/services/services.h b/components/services/services.h new file mode 100644 index 00000000..62394143 --- /dev/null +++ b/components/services/services.h @@ -0,0 +1,16 @@ +/* + * Squeezelite for esp32 + * + * (c) Philippe G. 2019, philippe_44@outlook.com + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + * + */ + +#pragma once + +typedef enum { SLEEP_ONTIMER, SLEEP_ONKEY, SLEEP_ONGPIO, SLEEP_ONIR } sleep_cause_e; +void services_sleep_activate(sleep_cause_e cause); +void services_sleep_sethook(void (*hook)(void)); +void services_sleep_callback(uint32_t elapsed); diff --git a/components/squeezelite/output_i2s.c b/components/squeezelite/output_i2s.c index 022c9aaf..d9645ffd 100644 --- a/components/squeezelite/output_i2s.c +++ b/components/squeezelite/output_i2s.c @@ -41,6 +41,7 @@ sure that using rate_delay would fix that #include "adac.h" #include "time.h" #include "led.h" +#include "services.h" #include "monitor.h" #include "platform_config.h" #include "gpio_exp.h" @@ -102,9 +103,11 @@ const struct adac_s *adac = &dac_external; static log_level loglevel; +static uint32_t stopped_time; +static void (*pseudo_idle_chain)(uint32_t); static bool (*slimp_handler_chain)(u8_t *data, int len); static bool jack_mutes_amp; -static bool running, isI2SStarted, ended; +static bool running, isI2SStarted, ended, i2s_stats; static i2s_config_t i2s_config; static u8_t *obuf; static frames_t oframes; @@ -125,7 +128,8 @@ DECLARE_ALL_MIN_MAX; static int _i2s_write_frames(frames_t out_frames, bool silence, s32_t gainL, s32_t gainR, u8_t flags, s32_t cross_gain_in, s32_t cross_gain_out, ISAMPLE_T **cross_ptr); static void output_thread_i2s(void *arg); -static void i2s_stats(uint32_t now); +static void i2s_idle(uint32_t now); + static void spdif_convert(ISAMPLE_T *src, size_t frames, u32_t *dst, size_t *count); static void (*jack_handler_chain)(bool inserted); @@ -411,7 +415,15 @@ void output_init_i2s(log_level level, char *device, unsigned output_buf_size, ch else adac->speaker(true); adac->headset(jack_inserted_svc()); - + + // do we want stats + p = config_alloc_get_default(NVS_TYPE_STR, "stats", "n", 0); + i2s_stats = p && (*p == '1' || *p == 'Y' || *p == 'y'); + free(p); + + pseudo_idle_chain = pseudo_idle_svc; + pseudo_idle_svc = i2s_idle; + // create task as a FreeRTOS task but uses stack in internal RAM { static DRAM_ATTR StaticTask_t xTaskBuffer __attribute__ ((aligned (4))); @@ -419,14 +431,6 @@ void output_init_i2s(log_level level, char *device, unsigned output_buf_size, ch output_i2s_task = xTaskCreateStaticPinnedToCore( (TaskFunction_t) output_thread_i2s, "output_i2s", OUTPUT_THREAD_STACK_SIZE, NULL, CONFIG_ESP32_PTHREAD_TASK_PRIO_DEFAULT + 1, xStack, &xTaskBuffer, 0 ); } - - // do we want stats - p = config_alloc_get_default(NVS_TYPE_STR, "stats", "n", 0); - if (p && (*p == '1' || *p == 'Y' || *p == 'y')) { - pseudo_idle_chain = pseudo_idle_svc; - pseudo_idle_svc = i2s_stats; - } - free(p); } @@ -493,6 +497,7 @@ static void output_thread_i2s(void *arg) { uint32_t fullness = gettime_ms(); bool synced; output_state state = OUTPUT_OFF - 1; + stopped_time = pdMS_TO_TICKS(xTaskGetTickCount()); while (running) { @@ -508,6 +513,7 @@ static void output_thread_i2s(void *arg) { if (amp_control.gpio != -1) gpio_set_level_x(amp_control.gpio, !amp_control.active); LOG_INFO("switching off amp GPIO %d", amp_control.gpio); } else if (output.state == OUTPUT_STOPPED) { + stopped_time = pdMS_TO_TICKS(xTaskGetTickCount()); adac->speaker(false); led_blink(LED_GREEN, 200, 1000); } else if (output.state == OUTPUT_RUNNING) { @@ -632,16 +638,19 @@ static void output_thread_i2s(void *arg) { } /**************************************************************************************** - * Stats output thread + * stats output callback */ -static void i2s_stats(uint32_t now) { +static void i2s_idle(uint32_t now) { static uint32_t last; // first chain to next handler if (pseudo_idle_chain) pseudo_idle_chain(now); + // call the sleep mamanger + if (output.state <= OUTPUT_STOPPED) services_sleep_callback(now - stopped_time); + // then see if we need to act - if (output.state <= OUTPUT_STOPPED || now < last + STATS_PERIOD_MS) return; + if (!i2s_stats || output.state <= OUTPUT_STOPPED || now < last + STATS_PERIOD_MS) return; last = now; LOG_INFO( "Output State: %d, current sample rate: %d, bytes per frame: %d", output.state, output.current_sample_rate, BYTES_PER_FRAME); diff --git a/main/esp_app_main.c b/main/esp_app_main.c index bb817941..38754428 100644 --- a/main/esp_app_main.c +++ b/main/esp_app_main.c @@ -270,6 +270,7 @@ void register_default_nvs(){ register_default_string_val( "i2c_config", CONFIG_I2C_CONFIG); register_default_string_val( "spi_config", CONFIG_SPI_CONFIG); register_default_string_val( "set_GPIO", CONFIG_SET_GPIO); + register_default_string_val( "sleep_config", ""); register_default_string_val( "led_brightness", ""); register_default_string_val( "spdif_config", ""); register_default_string_val( "dac_config", "");