Browse Source

adding rotary encoder + better jack gpio handling

philippe44 5 years ago
parent
commit
ed4eb6a42e

+ 2 - 0
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 */

+ 2 - 0
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
 };
 
 /****************************************************************************************

+ 45 - 3
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);

+ 3 - 2
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);

+ 124 - 37
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;
-
-        if (!xQueueReceive(button_evt_queue, &button, portMAX_DELAY)) continue;
-
-		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);
-
-		// find if shifting is activated
-		if (button.shifter && button.shifter->type == button.shifter->level) press = BUTTON_SHIFTED;
-		else press = BUTTON_NORMAL;
+		QueueSetMemberHandle_t xActivatedMember;
+
+		// 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;		
+
+			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;
 	
-		/* 
-		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;
+}	

+ 7 - 1
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);

+ 35 - 18
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);

+ 345 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @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;
+}

+ 172 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @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 <stdbool.h>
+#include <stdint.h>
+
+#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

+ 10 - 2
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,
 };
 
 /****************************************************************************************

+ 5 - 1
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

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