mirror of
https://github.com/sle118/squeezelite-esp32.git
synced 2025-12-07 12:07:09 +03:00
adding rotary encoder + better jack gpio handling
This commit is contained in:
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
/****************************************************************************************
|
||||
|
||||
@@ -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) {
|
||||
@@ -365,7 +389,25 @@ esp_err_t actrls_init_json(const char *profile_name, bool create) {
|
||||
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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,43 +114,62 @@ 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;
|
||||
|
||||
event = (button.level == button.type) ? BUTTON_PRESSED : BUTTON_RELEASED;
|
||||
if (xActivatedMember == button_evt_queue) {
|
||||
struct button_s button;
|
||||
button_event_e event;
|
||||
button_press_e press;
|
||||
|
||||
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);
|
||||
// received a button event
|
||||
xQueueReceive(button_evt_queue, &button, 0);
|
||||
|
||||
// find if shifting is activated
|
||||
if (button.shifter && button.shifter->type == button.shifter->level) press = BUTTON_SHIFTED;
|
||||
else press = BUTTON_NORMAL;
|
||||
event = (button.level == button.type) ? BUTTON_PRESSED : BUTTON_RELEASED;
|
||||
|
||||
/*
|
||||
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);
|
||||
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);
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
gpio_pad_select_gpio(JACK_GPIO);
|
||||
gpio_set_direction(JACK_GPIO, GPIO_MODE_INPUT);
|
||||
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);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
/****************************************************************************************
|
||||
@@ -105,11 +117,16 @@ 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);
|
||||
// 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");
|
||||
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
components/services/rotary_encoder.c
Normal file
345
components/services/rotary_encoder.c
Normal file
@@ -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
components/services/rotary_encoder.h
Normal file
172
components/services/rotary_encoder.h
Normal file
@@ -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
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
/****************************************************************************************
|
||||
|
||||
@@ -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
|
||||
@@ -282,6 +282,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);
|
||||
ESP_LOGD(TAG,"Registering default value for key %s, value %s", "ota_erase_blk", number_buffer);
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user