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");