From ed4eb6a42ed8987d77b015446b3f065f9fdbc66a Mon Sep 17 00:00:00 2001 From: philippe44 Date: Wed, 5 Feb 2020 00:17:48 -0800 Subject: [PATCH] adding rotary encoder + better jack gpio handling --- components/driver_bt/bt_app_sink.c | 2 + components/raop/raop_sink.c | 2 + components/services/audio_controls.c | 48 +++- components/services/audio_controls.h | 5 +- components/services/buttons.c | 153 +++++++++--- components/services/buttons.h | 8 +- components/services/monitor.c | 53 ++-- components/services/rotary_encoder.c | 345 +++++++++++++++++++++++++++ components/services/rotary_encoder.h | 172 +++++++++++++ components/squeezelite/controls.c | 12 +- main/Kconfig.projbuild | 6 +- main/esp_app_main.c | 12 +- 12 files changed, 752 insertions(+), 66 deletions(-) create mode 100644 components/services/rotary_encoder.c create mode 100644 components/services/rotary_encoder.h diff --git a/components/driver_bt/bt_app_sink.c b/components/driver_bt/bt_app_sink.c index cff29501..7159e6ff 100644 --- a/components/driver_bt/bt_app_sink.c +++ b/components/driver_bt/bt_app_sink.c @@ -132,6 +132,8 @@ const static actrls_t controls = { bt_pause, bt_stop, // pause, stop NULL, NULL, // rew, fwd bt_prev, bt_next, // prev, next + NULL, NULL, NULL, NULL, // left, right, up, down + bt_volume_down, bt_volume_up, bt_toggle// knob left, knob_right, knob push }; /* disconnection */ diff --git a/components/raop/raop_sink.c b/components/raop/raop_sink.c index 2743b275..0627a794 100644 --- a/components/raop/raop_sink.c +++ b/components/raop/raop_sink.c @@ -77,6 +77,8 @@ const static actrls_t controls = { raop_pause, raop_stop, // pause, stop NULL, NULL, // rew, fwd raop_prev, raop_next, // prev, next + NULL, NULL, NULL, NULL, // left, right, up, down + raop_volume_down, raop_volume_up, raop_toggle// knob left, knob_right, knob push }; /**************************************************************************************** diff --git a/components/services/audio_controls.c b/components/services/audio_controls.c index f981d317..6deb8258 100644 --- a/components/services/audio_controls.c +++ b/components/services/audio_controls.c @@ -62,7 +62,9 @@ static const actrls_config_map_t actrls_config_map[] = #define EP(x) [x] = #x /* ENUM PRINT */ static const char * actrls_action_s[ ] = { 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_PUSH), EP(BCTRLS_UP),EP(BCTRLS_DOWN),EP(BCTRLS_LEFT),EP(BCTRLS_RIGHT), ""} ; + EP(BCTRLS_UP),EP(BCTRLS_DOWN),EP(BCTRLS_LEFT),EP(BCTRLS_RIGHT), + EP(KNOB_LEFT),EP(KNOB_RIGHT),EP(KNOB_PUSH), + ""} ; static const char * TAG = "audio controls"; static actrls_config_t *json_config; @@ -119,6 +121,28 @@ static void control_handler(void *client, button_event_e event, button_press_e p } } +static void control_rotary_handler(void *client, rotary_event_e event, bool long_press) { + actrls_action_e action; + + switch(event) { + case ROTARY_LEFT: + action = KNOB_LEFT; + break; + case ROTARY_RIGHT: + action = KNOB_RIGHT; + break; + case ROTARY_PRESSED: + // no handling of rotary long press + action = KNOB_PUSH; + break; + default: + action = ACTRLS_NONE; + break; + } + + if (action != ACTRLS_NONE) (*current_controls[action])(); +} + /* void up(void *id, button_event_e event, button_press_e press, bool longpress) { if (press == BUTTON_NORMAL) { @@ -364,8 +388,26 @@ esp_err_t actrls_init_json(const char *profile_name, bool create) { actrls_config_t *cur_config = NULL; actrls_config_t *config_root = NULL; const cJSON *button; - - char *config = config_alloc_get_default(NVS_TYPE_STR, profile_name, NULL, 0); + + char *config = config_alloc_get_default(NVS_TYPE_STR, "rotary_config", NULL, 0); + if (config && *config) { + char *p; + int A = -1, B = -1, SW = -1; + + // parse config + if ((p = strcasestr(config, "A")) != NULL) A = atoi(strchr(p, '=') + 1); + if ((p = strcasestr(config, "B")) != NULL) B = atoi(strchr(p, '=') + 1); + if ((p = strcasestr(config, "SW")) != NULL) SW = atoi(strchr(p, '=') + 1); + + // create rotary (no handling of long press) + err = create_rotary(NULL, A, B, SW, 0, control_rotary_handler) ? ESP_OK : ESP_FAIL; + } + + if (config) free(config); + + if (!profile_name || !*profile_name) return ESP_OK; + + config = config_alloc_get_default(NVS_TYPE_STR, profile_name, NULL, 0); if(!config) return ESP_FAIL; ESP_LOGD(TAG,"Parsing JSON structure %s", config); diff --git a/components/services/audio_controls.h b/components/services/audio_controls.h index 82da6620..58f0f6f8 100644 --- a/components/services/audio_controls.h +++ b/components/services/audio_controls.h @@ -23,8 +23,9 @@ // BEWARE: this is the index of the array of action below (change actrls_action_s as well!) typedef enum { ACTRLS_NONE = -1, ACTRLS_VOLUP, ACTRLS_VOLDOWN, ACTRLS_TOGGLE, ACTRLS_PLAY, ACTRLS_PAUSE, ACTRLS_STOP, ACTRLS_REW, ACTRLS_FWD, ACTRLS_PREV, ACTRLS_NEXT, - BCTRLS_PUSH, BCTRLS_UP, BCTRLS_DOWN, BCTRLS_LEFT, BCTRLS_RIGHT, ACTRLS_REMAP, - ACTRLS_MAX + BCTRLS_UP, BCTRLS_DOWN, BCTRLS_LEFT, BCTRLS_RIGHT, + KNOB_LEFT, KNOB_RIGHT, KNOB_PUSH, + ACTRLS_REMAP, ACTRLS_MAX } actrls_action_e; typedef void (*actrls_handler)(void); diff --git a/components/services/buttons.c b/components/services/buttons.c index 396ea868..0275efc4 100644 --- a/components/services/buttons.c +++ b/components/services/buttons.c @@ -31,6 +31,7 @@ #include "esp_task.h" #include "driver/gpio.h" #include "buttons.h" +#include "rotary_encoder.h" #include "globdefs.h" bool gpio36_39_used; @@ -42,6 +43,7 @@ static int n_buttons = 0; #define BUTTON_STACK_SIZE 4096 #define MAX_BUTTONS 16 #define DEBOUNCE 50 +#define BUTTON_QUEUE_LEN 10 static EXT_RAM_ATTR struct button_s { void *client; @@ -56,7 +58,16 @@ static EXT_RAM_ATTR struct button_s { TimerHandle_t timer; } buttons[MAX_BUTTONS]; -static xQueueHandle button_evt_queue = NULL; +static struct { + QueueHandle_t queue; + void *client; + rotary_encoder_info_t info; + int A, B, SW; + rotary_handler handler; +} rotary; + +static xQueueHandle button_evt_queue; +static QueueSetHandle_t button_queue_set; static void buttons_task(void* arg); @@ -103,44 +114,63 @@ static void buttons_task(void* arg) { ESP_LOGI(TAG, "starting button tasks"); while (1) { - struct button_s button; - button_event_e event; - button_press_e press; + QueueSetMemberHandle_t xActivatedMember; - if (!xQueueReceive(button_evt_queue, &button, portMAX_DELAY)) continue; + // wait on button and rotary queues + if ((xActivatedMember = xQueueSelectFromSet( button_queue_set, portMAX_DELAY )) == NULL) continue; + + if (xActivatedMember == button_evt_queue) { + struct button_s button; + button_event_e event; + button_press_e press; + + // received a button event + xQueueReceive(button_evt_queue, &button, 0); - event = (button.level == button.type) ? BUTTON_PRESSED : BUTTON_RELEASED; + event = (button.level == button.type) ? BUTTON_PRESSED : BUTTON_RELEASED; - ESP_LOGD(TAG, "received event:%u from gpio:%u level:%u (timer %u shifting %u)", event, button.gpio, button.level, button.long_timer, button.shifting); + ESP_LOGD(TAG, "received event:%u from gpio:%u level:%u (timer %u shifting %u)", event, button.gpio, button.level, button.long_timer, button.shifting); - // find if shifting is activated - if (button.shifter && button.shifter->type == button.shifter->level) press = BUTTON_SHIFTED; - else press = BUTTON_NORMAL; + // find if shifting is activated + if (button.shifter && button.shifter->type == button.shifter->level) press = BUTTON_SHIFTED; + else press = BUTTON_NORMAL; - /* - long_timer will be set either because we truly have a long press - or we have a release before the long press timer elapsed, so two - events shall be sent - */ - if (button.long_timer) { - if (event == BUTTON_RELEASED) { - // early release of a long-press button, send press/release - if (!button.shifting) { - (*button.handler)(button.client, BUTTON_PRESSED, press, false); - (*button.handler)(button.client, BUTTON_RELEASED, press, false); - } + /* + long_timer will be set either because we truly have a long press + or we have a release before the long press timer elapsed, so two + events shall be sent + */ + if (button.long_timer) { + if (event == BUTTON_RELEASED) { + // early release of a long-press button, send press/release + if (!button.shifting) { + (*button.handler)(button.client, BUTTON_PRESSED, press, false); + (*button.handler)(button.client, BUTTON_RELEASED, press, false); + } + // button is a copy, so need to go to real context + button.self->shifting = false; + } else if (!button.shifting) { + // normal long press and not shifting so don't discard + (*button.handler)(button.client, BUTTON_PRESSED, press, true); + } + } else { + // normal press/release of a button or release of a long-press button + if (!button.shifting) (*button.handler)(button.client, event, press, button.long_press); // button is a copy, so need to go to real context button.self->shifting = false; - } else if (!button.shifting) { - // normal long press and not shifting so don't discard - (*button.handler)(button.client, BUTTON_PRESSED, press, true); - } + } } else { - // normal press/release of a button or release of a long-press button - if (!button.shifting) (*button.handler)(button.client, event, press, button.long_press); - // button is a copy, so need to go to real context - button.self->shifting = false; - } + rotary_encoder_event_t event = { 0 }; + + // received a rotary event + xQueueReceive(rotary.queue, &event, 0); + + ESP_LOGI(TAG, "Event: position %d, direction %s", event.state.position, + event.state.direction ? (event.state.direction == ROTARY_ENCODER_DIRECTION_CLOCKWISE ? "CW" : "CCW") : "NOT_SET"); + + rotary.handler(rotary.client, event.state.direction == ROTARY_ENCODER_DIRECTION_CLOCKWISE ? + ROTARY_RIGHT : ROTARY_LEFT, false); + } } } @@ -163,7 +193,9 @@ void button_create(void *client, int gpio, int type, bool pull, int debounce, bu ESP_LOGI(TAG, "Creating button using GPIO %u, type %u, pull-up/down %u, long press %u shifter %u", gpio, type, pull, long_press, shifter_gpio); if (!n_buttons) { - button_evt_queue = xQueueCreate(10, sizeof(struct button_s)); + button_evt_queue = xQueueCreate(BUTTON_QUEUE_LEN, sizeof(struct button_s)); + if (!button_queue_set) button_queue_set = xQueueCreateSet(BUTTON_QUEUE_LEN + 1); + xQueueAddToSet( button_evt_queue, button_queue_set ); xTaskCreateStatic( (TaskFunction_t) buttons_task, "buttons_thread", BUTTON_STACK_SIZE, NULL, ESP_TASK_PRIO_MIN + 1, xStack, &xTaskBuffer); } @@ -229,7 +261,18 @@ void *button_get_client(int gpio) { if (buttons[i].gpio == gpio) return buttons[i].client; } return NULL; - } +} + +/**************************************************************************************** + * Get stored id + */ +bool button_is_pressed(int gpio, void *client) { + for (int i = 0; i < n_buttons; i++) { + if (gpio != -1 && buttons[i].gpio == gpio) return buttons[i].level == buttons[i].type; + else if (client && buttons[i].client == client) return buttons[i].level == buttons[i].type; + } + return false; +} /**************************************************************************************** * Update buttons @@ -270,3 +313,47 @@ void *button_remap(void *client, int gpio, button_handler handler, int long_pres return prev_client; } + +/**************************************************************************************** + * Create rotary encoder + */ +static void rotary_button_handler(void *id, button_event_e event, button_press_e mode, bool long_press) { + ESP_LOGI(TAG, "Rotary push-button %d", event); + rotary.handler(id, event == BUTTON_PRESSED ? ROTARY_PRESSED : ROTARY_RELEASED, long_press); +} + +/**************************************************************************************** + * Create rotary encoder + */ +bool create_rotary(void *id, int A, int B, int SW, int long_press, rotary_handler handler) { + if (A == -1 || B == -1) { + ESP_LOGI(TAG, "Cannot create rotary %d %d", A, B); + return false; + } + + rotary.A = A; + rotary.B = B; + rotary.SW = SW; + rotary.client = id; + rotary.handler = handler; + + // nasty ESP32 bug: fire-up constantly INT on GPIO 36/39 if ADC1, AMP, Hall used which WiFi does when PS is activated + if (A == 36 || A == 39 || B == 36 || B == 39 || SW == 36 || SW == 39) gpio36_39_used = true; + + // Initialise the rotary encoder device with the GPIOs for A and B signals + rotary_encoder_init(&rotary.info, A, B); + + // Create a queue for events from the rotary encoder driver. + rotary.queue = rotary_encoder_create_queue(); + rotary_encoder_set_queue(&rotary.info, rotary.queue); + + if (!button_queue_set) button_queue_set = xQueueCreateSet(BUTTON_QUEUE_LEN + 1); + xQueueAddToSet( rotary.queue, button_queue_set ); + + // create companion button if rotary has a switch + if (SW != -1) button_create(id, SW, BUTTON_LOW, true, 0, rotary_button_handler, long_press, -1); + + ESP_LOGI(TAG, "Creating rotary encoder A:%d B:%d, SW:%d", A, B, SW); + + return true; +} diff --git a/components/services/buttons.h b/components/services/buttons.h index 907b0e67..92b00b61 100644 --- a/components/services/buttons.h +++ b/components/services/buttons.h @@ -18,7 +18,7 @@ #pragma once -// button type (pressed = LOW or HIGH) +// button type (pressed = LOW or HIGH, matches GPIO level) #define BUTTON_LOW 0 #define BUTTON_HIGH 1 @@ -36,3 +36,9 @@ NOTE: shifter buttons *must* be created before shiftee void button_create(void *client, int gpio, int type, bool pull, int debounce, button_handler handler, int long_press, int shifter_gpio); void *button_remap(void *client, int gpio, button_handler handler, int long_press, int shifter_gpio); void *button_get_client(int gpio); +bool button_is_pressed(int gpio, void *client); + +typedef enum { ROTARY_LEFT, ROTARY_RIGHT, ROTARY_PRESSED, ROTARY_RELEASED } rotary_event_e; +typedef void (*rotary_handler)(void *id, rotary_event_e event, bool long_press); + +bool create_rotary(void *id, int A, int B, int SW, int long_press, rotary_handler handler); diff --git a/components/services/monitor.c b/components/services/monitor.c index 09a4b008..906df187 100644 --- a/components/services/monitor.c +++ b/components/services/monitor.c @@ -20,12 +20,17 @@ #include "led.h" #include "globdefs.h" #include "config.h" - +#include "accessors.h" #define MONITOR_TIMER (10*1000) static const char *TAG = "monitor"; static TimerHandle_t monitor_timer; +#ifdef JACK_GPIO +static int jack_gpio = JACK_GPIO; +#else +static int jack_gpio = -1; +#endif void (*jack_handler_svc)(bool inserted); bool jack_inserted_svc(void); @@ -56,11 +61,8 @@ static void jack_handler_default(void *id, button_event_e event, button_press_e * */ bool jack_inserted_svc (void) { -#ifdef JACK_GPIO - return !gpio_get_level(JACK_GPIO); -#else - return false; -#endif + if (jack_gpio != -1) return button_is_pressed(jack_gpio, NULL); + else return false; } /**************************************************************************************** @@ -88,15 +90,25 @@ bool spkfault_svc (void) { * */ void set_jack_gpio(int gpio, char *value) { - if (!strcasecmp(value, "jack")) { - ESP_LOGI(TAG,"Adding jack detection GPIO %d", gpio); + bool low = false; + + if (!strcasecmp(value, "jack_l")) { + jack_gpio = gpio; + low = true; + } else if (!strcasecmp(value, "jack_h")) { + jack_gpio = gpio; + } + + if (jack_gpio != -1) { + gpio_pad_select_gpio(jack_gpio); + gpio_set_direction(jack_gpio, GPIO_MODE_INPUT); + gpio_set_pull_mode(jack_gpio, low ? GPIO_PULLUP_ONLY : GPIO_PULLDOWN_ONLY); + + ESP_LOGI(TAG,"Adding jack (%s) detection GPIO %d", low ? "low" : "high", gpio); - gpio_pad_select_gpio(JACK_GPIO); - gpio_set_direction(JACK_GPIO, GPIO_MODE_INPUT); - // re-use button management for jack handler, it's a GPIO after all - button_create(NULL, JACK_GPIO, BUTTON_LOW, false, 250, jack_handler_default, 0, -1); - } + button_create(NULL, jack_gpio, low ? BUTTON_LOW : BUTTON_HIGH, false, 250, jack_handler_default, 0, -1); + } } /**************************************************************************************** @@ -104,12 +116,17 @@ void set_jack_gpio(int gpio, char *value) { */ void monitor_svc_init(void) { ESP_LOGI(TAG, "Initializing monitoring"); - -#if !defined(JACK_GPIO) || JACK_GPIO == -1 - parse_set_GPIO(set_jack_gpio); -#else - set_jack_gpio(JACK_GPIO, "jack"); + + // if JACK_GPIO is compiled-time defined set it there + if (jack_gpio != -1) { +#if JACK_GPIO_LEVEL == 1 + set_jack_gpio(JACK_GPIO, "jack_h"); +#else + set_jack_gpio(JACK_GPIO, "jack_l"); #endif + } else { + parse_set_GPIO(set_jack_gpio); + } #ifdef SPKFAULT_GPIO gpio_pad_select_gpio(SPKFAULT_GPIO); diff --git a/components/services/rotary_encoder.c b/components/services/rotary_encoder.c new file mode 100644 index 00000000..d9aa854d --- /dev/null +++ b/components/services/rotary_encoder.c @@ -0,0 +1,345 @@ +/* + * Copyright (c) 2019 David Antliff + * Copyright 2011 Ben Buxton + * + * This file is part of the esp32-rotary-encoder component. + * + * esp32-rotary-encoder is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * esp32-rotary-encoder is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with esp32-rotary-encoder. If not, see . + */ + +/** + * @file rotary_encoder.c + * @brief Driver implementation for the ESP32-compatible Incremental Rotary Encoder component. + * + * Based on https://github.com/buxtronix/arduino/tree/master/libraries/Rotary + * Original header follows: + * + * Rotary encoder handler for arduino. v1.1 + * + * Copyright 2011 Ben Buxton. Licenced under the GNU GPL Version 3. + * Contact: bb@cactii.net + * + * A typical mechanical rotary encoder emits a two bit gray code + * on 3 output pins. Every step in the output (often accompanied + * by a physical 'click') generates a specific sequence of output + * codes on the pins. + * + * There are 3 pins used for the rotary encoding - one common and + * two 'bit' pins. + * + * The following is the typical sequence of code on the output when + * moving from one step to the next: + * + * Position Bit1 Bit2 + * ---------------------- + * Step1 0 0 + * 1/4 1 0 + * 1/2 1 1 + * 3/4 0 1 + * Step2 0 0 + * + * From this table, we can see that when moving from one 'click' to + * the next, there are 4 changes in the output code. + * + * - From an initial 0 - 0, Bit1 goes high, Bit0 stays low. + * - Then both bits are high, halfway through the step. + * - Then Bit1 goes low, but Bit2 stays high. + * - Finally at the end of the step, both bits return to 0. + * + * Detecting the direction is easy - the table simply goes in the other + * direction (read up instead of down). + * + * To decode this, we use a simple state machine. Every time the output + * code changes, it follows state, until finally a full steps worth of + * code is received (in the correct order). At the final 0-0, it returns + * a value indicating a step in one direction or the other. + * + * It's also possible to use 'half-step' mode. This just emits an event + * at both the 0-0 and 1-1 positions. This might be useful for some + * encoders where you want to detect all positions. + * + * If an invalid state happens (for example we go from '0-1' straight + * to '1-0'), the state machine resets to the start until 0-0 and the + * next valid codes occur. + * + * The biggest advantage of using a state machine over other algorithms + * is that this has inherent debounce built in. Other algorithms emit spurious + * output with switch bounce, but this one will simply flip between + * sub-states until the bounce settles, then continue along the state + * machine. + * A side effect of debounce is that fast rotations can cause steps to + * be skipped. By not requiring debounce, fast rotations can be accurately + * measured. + * Another advantage is the ability to properly handle bad state, such + * as due to EMI, etc. + * It is also a lot simpler than others - a static state table and less + * than 10 lines of logic. + */ + +#include "rotary_encoder.h" + +#include "esp_log.h" +#include "driver/gpio.h" + +#define TAG "rotary_encoder" + +//#define ROTARY_ENCODER_DEBUG + +// Use a single-item queue so that the last value can be easily overwritten by the interrupt handler +#define EVENT_QUEUE_LENGTH 1 + +#define TABLE_ROWS 7 + +#define DIR_NONE 0x0 // No complete step yet. +#define DIR_CW 0x10 // Clockwise step. +#define DIR_CCW 0x20 // Anti-clockwise step. + +// Create the half-step state table (emits a code at 00 and 11) +#define R_START 0x0 +#define H_CCW_BEGIN 0x1 +#define H_CW_BEGIN 0x2 +#define H_START_M 0x3 +#define H_CW_BEGIN_M 0x4 +#define H_CCW_BEGIN_M 0x5 + +static const uint8_t _ttable_half[TABLE_ROWS][TABLE_COLS] = { + // 00 01 10 11 // BA + {H_START_M, H_CW_BEGIN, H_CCW_BEGIN, R_START}, // R_START (00) + {H_START_M | DIR_CCW, R_START, H_CCW_BEGIN, R_START}, // H_CCW_BEGIN + {H_START_M | DIR_CW, H_CW_BEGIN, R_START, R_START}, // H_CW_BEGIN + {H_START_M, H_CCW_BEGIN_M, H_CW_BEGIN_M, R_START}, // H_START_M (11) + {H_START_M, H_START_M, H_CW_BEGIN_M, R_START | DIR_CW}, // H_CW_BEGIN_M + {H_START_M, H_CCW_BEGIN_M, H_START_M, R_START | DIR_CCW}, // H_CCW_BEGIN_M +}; + +// Create the full-step state table (emits a code at 00 only) +# define F_CW_FINAL 0x1 +# define F_CW_BEGIN 0x2 +# define F_CW_NEXT 0x3 +# define F_CCW_BEGIN 0x4 +# define F_CCW_FINAL 0x5 +# define F_CCW_NEXT 0x6 + +static const uint8_t _ttable_full[TABLE_ROWS][TABLE_COLS] = { + // 00 01 10 11 // BA + {R_START, F_CW_BEGIN, F_CCW_BEGIN, R_START}, // R_START + {F_CW_NEXT, R_START, F_CW_FINAL, R_START | DIR_CW}, // F_CW_FINAL + {F_CW_NEXT, F_CW_BEGIN, R_START, R_START}, // F_CW_BEGIN + {F_CW_NEXT, F_CW_BEGIN, F_CW_FINAL, R_START}, // F_CW_NEXT + {F_CCW_NEXT, R_START, F_CCW_BEGIN, R_START}, // F_CCW_BEGIN + {F_CCW_NEXT, F_CCW_FINAL, R_START, R_START | DIR_CCW}, // F_CCW_FINAL + {F_CCW_NEXT, F_CCW_FINAL, F_CCW_BEGIN, R_START}, // F_CCW_NEXT +}; + +static uint8_t _process(rotary_encoder_info_t * info) +{ + uint8_t event = 0; + if (info != NULL) + { + // Get state of input pins. + uint8_t pin_state = (gpio_get_level(info->pin_b) << 1) | gpio_get_level(info->pin_a); + + // Determine new state from the pins and state table. +#ifdef ROTARY_ENCODER_DEBUG + uint8_t old_state = info->table_state; +#endif + info->table_state = info->table[info->table_state & 0xf][pin_state]; + + // Return emit bits, i.e. the generated event. + event = info->table_state & 0x30; +#ifdef ROTARY_ENCODER_DEBUG + ESP_EARLY_LOGD(TAG, "BA %d%d, state 0x%02x, new state 0x%02x, event 0x%02x", + pin_state >> 1, pin_state & 1, old_state, info->table_state, event); +#endif + } + return event; +} + +static void _isr_rotenc(void * args) +{ + rotary_encoder_info_t * info = (rotary_encoder_info_t *)args; + uint8_t event = _process(info); + bool send_event = false; + + switch (event) + { + case DIR_CW: + ++info->state.position; + info->state.direction = ROTARY_ENCODER_DIRECTION_CLOCKWISE; + send_event = true; + break; + case DIR_CCW: + --info->state.position; + info->state.direction = ROTARY_ENCODER_DIRECTION_COUNTER_CLOCKWISE; + send_event = true; + break; + default: + break; + } + + if (send_event && info->queue) + { + rotary_encoder_event_t queue_event = + { + .state = + { + .position = info->state.position, + .direction = info->state.direction, + }, + }; + BaseType_t task_woken = pdFALSE; + xQueueOverwriteFromISR(info->queue, &queue_event, &task_woken); + if (task_woken) + { + portYIELD_FROM_ISR(); + } + } +} + +esp_err_t rotary_encoder_init(rotary_encoder_info_t * info, gpio_num_t pin_a, gpio_num_t pin_b) +{ + esp_err_t err = ESP_OK; + if (info) + { + info->pin_a = pin_a; + info->pin_b = pin_b; + info->table = &_ttable_full[0]; //enable_half_step ? &_ttable_half[0] : &_ttable_full[0]; + info->table_state = R_START; + info->state.position = 0; + info->state.direction = ROTARY_ENCODER_DIRECTION_NOT_SET; + + // configure GPIOs + gpio_pad_select_gpio(info->pin_a); + gpio_set_pull_mode(info->pin_a, GPIO_PULLUP_ONLY); + gpio_set_direction(info->pin_a, GPIO_MODE_INPUT); + gpio_set_intr_type(info->pin_a, GPIO_INTR_ANYEDGE); + + gpio_pad_select_gpio(info->pin_b); + gpio_set_pull_mode(info->pin_b, GPIO_PULLUP_ONLY); + gpio_set_direction(info->pin_b, GPIO_MODE_INPUT); + gpio_set_intr_type(info->pin_b, GPIO_INTR_ANYEDGE); + + // install interrupt handlers + gpio_isr_handler_add(info->pin_a, _isr_rotenc, info); + gpio_isr_handler_add(info->pin_b, _isr_rotenc, info); + } + else + { + ESP_LOGE(TAG, "info is NULL"); + err = ESP_ERR_INVALID_ARG; + } + return err; +} + +esp_err_t rotary_encoder_enable_half_steps(rotary_encoder_info_t * info, bool enable) +{ + esp_err_t err = ESP_OK; + if (info) + { + info->table = enable ? &_ttable_half[0] : &_ttable_full[0]; + info->table_state = R_START; + } + else + { + ESP_LOGE(TAG, "info is NULL"); + err = ESP_ERR_INVALID_ARG; + } + return err; +} + +esp_err_t rotary_encoder_flip_direction(rotary_encoder_info_t * info) +{ + esp_err_t err = ESP_OK; + if (info) + { + gpio_num_t temp = info->pin_a; + info->pin_a = info->pin_b; + info->pin_b = temp; + } + else + { + ESP_LOGE(TAG, "info is NULL"); + err = ESP_ERR_INVALID_ARG; + } + return err; +} + +esp_err_t rotary_encoder_uninit(rotary_encoder_info_t * info) +{ + esp_err_t err = ESP_OK; + if (info) + { + gpio_isr_handler_remove(info->pin_a); + gpio_isr_handler_remove(info->pin_b); + } + else + { + ESP_LOGE(TAG, "info is NULL"); + err = ESP_ERR_INVALID_ARG; + } + return err; +} + +QueueHandle_t rotary_encoder_create_queue(void) +{ + return xQueueCreate(EVENT_QUEUE_LENGTH, sizeof(rotary_encoder_event_t)); +} + +esp_err_t rotary_encoder_set_queue(rotary_encoder_info_t * info, QueueHandle_t queue) +{ + esp_err_t err = ESP_OK; + if (info) + { + info->queue = queue; + } + else + { + ESP_LOGE(TAG, "info is NULL"); + err = ESP_ERR_INVALID_ARG; + } + return err; +} + +esp_err_t rotary_encoder_get_state(const rotary_encoder_info_t * info, rotary_encoder_state_t * state) +{ + esp_err_t err = ESP_OK; + if (info && state) + { + // make a snapshot of the state + state->position = info->state.position; + state->direction = info->state.direction; + } + else + { + ESP_LOGE(TAG, "info and/or state is NULL"); + err = ESP_ERR_INVALID_ARG; + } + return err; +} + +esp_err_t rotary_encoder_reset(rotary_encoder_info_t * info) +{ + esp_err_t err = ESP_OK; + if (info) + { + info->state.position = 0; + info->state.direction = ROTARY_ENCODER_DIRECTION_NOT_SET; + } + else + { + ESP_LOGE(TAG, "info is NULL"); + err = ESP_ERR_INVALID_ARG; + } + return err; +} diff --git a/components/services/rotary_encoder.h b/components/services/rotary_encoder.h new file mode 100644 index 00000000..4ff414bf --- /dev/null +++ b/components/services/rotary_encoder.h @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2019 David Antliff + * Copyright 2011 Ben Buxton + * + * This file is part of the esp32-rotary-encoder component. + * + * esp32-rotary-encoder is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * esp32-rotary-encoder is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with esp32-rotary-encoder. If not, see . + */ + +/** + * @file rotary_encoder.h + * @brief Interface definitions for the ESP32-compatible Incremental Rotary Encoder component. + * + * This component provides a means to interface with a typical rotary encoder such as the EC11 or LPD3806. + * These encoders produce a quadrature signal on two outputs, which can be used to track the position and + * direction as movement occurs. + * + * This component provides functions to initialise the GPIOs and install appropriate interrupt handlers to + * track a single device's position. An event queue is used to provide a way for a user task to obtain + * position information from the component as it is generated. + * + * Note that the queue is of length 1, and old values will be overwritten. Using a longer queue is + * possible with some minor modifications however newer values are lost if the queue overruns. A circular + * buffer where old values are lost would be better (maybe StreamBuffer in FreeRTOS 10.0.0?). + */ + +#ifndef ROTARY_ENCODER_H +#define ROTARY_ENCODER_H + +#include +#include + +#include "freertos/FreeRTOS.h" +#include "freertos/queue.h" +#include "esp_err.h" +#include "driver/gpio.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef int32_t rotary_encoder_position_t; + +/** + * @brief Enum representing the direction of rotation. + */ +typedef enum +{ + ROTARY_ENCODER_DIRECTION_NOT_SET = 0, ///< Direction not yet known (stationary since reset) + ROTARY_ENCODER_DIRECTION_CLOCKWISE, + ROTARY_ENCODER_DIRECTION_COUNTER_CLOCKWISE, +} rotary_encoder_direction_t; + +// Used internally +///@cond INTERNAL +#define TABLE_COLS 4 +typedef uint8_t table_row_t[TABLE_COLS]; +///@endcond + +/** + * @brief Struct represents the current state of the device in terms of incremental position and direction of last movement + */ +typedef struct +{ + rotary_encoder_position_t position; ///< Numerical position since reset. This value increments on clockwise rotation, and decrements on counter-clockewise rotation. Counts full or half steps depending on mode. Set to zero on reset. + rotary_encoder_direction_t direction; ///< Direction of last movement. Set to NOT_SET on reset. +} rotary_encoder_state_t; + +/** + * @brief Struct carries all the information needed by this driver to manage the rotary encoder device. + * The fields of this structure should not be accessed directly. + */ +typedef struct +{ + gpio_num_t pin_a; ///< GPIO for Signal A from the rotary encoder device + gpio_num_t pin_b; ///< GPIO for Signal B from the rotary encoder device + QueueHandle_t queue; ///< Handle for event queue, created by ::rotary_encoder_create_queue + const table_row_t * table; ///< Pointer to active state transition table + uint8_t table_state; ///< Internal state + volatile rotary_encoder_state_t state; ///< Device state +} rotary_encoder_info_t; + +/** + * @brief Struct represents a queued event, used to communicate current position to a waiting task + */ +typedef struct +{ + rotary_encoder_state_t state; ///< The device state corresponding to this event +} rotary_encoder_event_t; + +/** + * @brief Initialise the rotary encoder device with the specified GPIO pins and full step increments. + * This function will set up the GPIOs as needed, + * Note: this function assumes that gpio_install_isr_service(0) has already been called. + * @param[in, out] info Pointer to allocated rotary encoder info structure. + * @param[in] pin_a GPIO number for rotary encoder output A. + * @param[in] pin_b GPIO number for rotary encoder output B. + * @return ESP_OK if successful, ESP_FAIL or ESP_ERR_* if an error occurred. + */ +esp_err_t rotary_encoder_init(rotary_encoder_info_t * info, gpio_num_t pin_a, gpio_num_t pin_b); + +/** + * @brief Enable half-stepping mode. This generates twice as many counted steps per rotation. + * @param[in] info Pointer to initialised rotary encoder info structure. + * @param[in] enable If true, count half steps. If false, only count full steps. + * @return ESP_OK if successful, ESP_FAIL or ESP_ERR_* if an error occurred. + */ +esp_err_t rotary_encoder_enable_half_steps(rotary_encoder_info_t * info, bool enable); + +/** + * @brief Reverse (flip) the sense of the direction. + * Use this if clockwise/counterclockwise are not what you expect. + * @param[in] info Pointer to initialised rotary encoder info structure. + * @return ESP_OK if successful, ESP_FAIL or ESP_ERR_* if an error occurred. + */ +esp_err_t rotary_encoder_flip_direction(rotary_encoder_info_t * info); + +/** + * @brief Remove the interrupt handlers installed by ::rotary_encoder_init. + * Note: GPIOs will be left in the state they were configured by ::rotary_encoder_init. + * @param[in] info Pointer to initialised rotary encoder info structure. + * @return ESP_OK if successful, ESP_FAIL or ESP_ERR_* if an error occurred. + */ +esp_err_t rotary_encoder_uninit(rotary_encoder_info_t * info); + +/** + * @brief Create a queue handle suitable for use as an event queue. + * @return A handle to a new queue suitable for use as an event queue. + */ +QueueHandle_t rotary_encoder_create_queue(void); + +/** + * @brief Set the driver to use the specified queue as an event queue. + * It is recommended that a queue constructed by ::rotary_encoder_create_queue is used. + * @param[in] info Pointer to initialised rotary encoder info structure. + * @param[in] queue Handle to queue suitable for use as an event queue. See ::rotary_encoder_create_queue. + * @return ESP_OK if successful, ESP_FAIL or ESP_ERR_* if an error occurred. + */ +esp_err_t rotary_encoder_set_queue(rotary_encoder_info_t * info, QueueHandle_t queue); + +/** + * @brief Get the current position of the rotary encoder. + * @param[in] info Pointer to initialised rotary encoder info structure. + * @param[in, out] state Pointer to an allocated rotary_encoder_state_t struct that will + * @return ESP_OK if successful, ESP_FAIL or ESP_ERR_* if an error occurred. + */ +esp_err_t rotary_encoder_get_state(const rotary_encoder_info_t * info, rotary_encoder_state_t * state); + +/** + * @brief Reset the current position of the rotary encoder to zero. + * @param[in] info Pointer to initialised rotary encoder info structure. + * @return ESP_OK if successful, ESP_FAIL or ESP_ERR_* if an error occurred. + */ +esp_err_t rotary_encoder_reset(rotary_encoder_info_t * info); + + +#ifdef __cplusplus +} +#endif + +#endif // ROTARY_ENCODER_H diff --git a/components/squeezelite/controls.c b/components/squeezelite/controls.c index 27bf1978..a58ee2ab 100644 --- a/components/squeezelite/controls.c +++ b/components/squeezelite/controls.c @@ -85,7 +85,15 @@ static void lms_right(void) { cli_send_cmd("button arrow_right"); } -static void lms_push(void) { +static void lms_knob_left(void) { + cli_send_cmd("button knob_left"); +} + +static void lms_knob_right(void) { + cli_send_cmd("button knob_right"); +} + +static void lms_knob_push(void) { cli_send_cmd("button knob_push"); } @@ -95,9 +103,9 @@ const actrls_t LMS_controls = { lms_pause, lms_stop, // pause, stop lms_rew, lms_fwd, // rew, fwd lms_prev, lms_next, // prev, next - lms_push, lms_up, lms_down, lms_left, lms_right, + lms_knob_left, lms_knob_right, lms_knob_push, }; /**************************************************************************************** diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index acb02e2f..a1f99c21 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -207,7 +207,11 @@ menu "Squeezelite-ESP32" int "Jack insertion GPIO" default -1 help - GPIO to detect speaker jack insertion (0 = inserted). Set to -1 for no detection + GPIO to detect speaker jack insertion. Set to -1 for no detection + config JACK_GPIO_LEVEL + depends on JACK_GPIO != -1 + int "Level when inserted (0/1)" + default 0 endmenu endmenu \ No newline at end of file diff --git a/main/esp_app_main.c b/main/esp_app_main.c index 616f1776..1530ba37 100644 --- a/main/esp_app_main.c +++ b/main/esp_app_main.c @@ -281,6 +281,9 @@ void register_default_nvs(){ ESP_LOGD(TAG,"Registering default Audio control board type %s, value ","actrls_config"); config_set_default(NVS_TYPE_STR, "actrls_config", "", 0); + + ESP_LOGD(TAG,"Registering default Audio control board type %s, value ","rotary_config"); + config_set_default(NVS_TYPE_STR, "rotary_config", "", 0); char number_buffer[101] = {}; snprintf(number_buffer,sizeof(number_buffer)-1,"%u",OTA_FLASH_ERASE_BLOCK); @@ -375,15 +378,12 @@ void app_main() ESP_LOGD(TAG,"Getting audio control mapping "); char *actrls_config = config_alloc_get_default(NVS_TYPE_STR, "actrls_config", NULL, 0); - if (actrls_config) { - if(actrls_config[0] !='\0'){ - ESP_LOGD(TAG,"Initializing audio control buttons type %s", actrls_config); - actrls_init_json(actrls_config, true); - } - free(actrls_config); + if (actrls_init_json(actrls_config, true) == ESP_OK) { + ESP_LOGD(TAG,"Initializing audio control buttons type %s", actrls_config); } else { ESP_LOGD(TAG,"No audio control buttons"); } + if (actrls_config) free(actrls_config); /* start the wifi manager */ ESP_LOGD(TAG,"Blinking led");