Skip to content

Commit

Permalink
Merge branch 'i2c_gpio_expander' into squeezetoo
Browse files Browse the repository at this point in the history
* i2c_gpio_expander:
  don't call null button handlers
  update readme.md
  expander works minimally
  works with two gpio banks, second bank needs testing
  try to bring up second bank
  basic gpio expander code
  rotary control recognised
  basic setup of MCP23017
  switch to mcp chip
  gpio expander with PI4IO chip... failed
  first crack at gpio expander framework
  battery-hacking
  • Loading branch information
rochuck committed Sep 16, 2020
2 parents bac2967 + af5da7f commit acb427f
Show file tree
Hide file tree
Showing 16 changed files with 682 additions and 151 deletions.
33 changes: 20 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ The NVS parameter "metadata_config" sets how metadata is displayed for AirPlay a

- 'pause' is the pause time between scrolls in ms (default is 3600ms)

- 'format' can contain free text and any of the 3 keywords %artist%, %album%, %title%. Using that format string, the keywords are replaced by their value to build the string to be displayed. Note that the plain text following a keyword that happens to be empty during playback of a track will be removed. For example, if you have set format=%artist% - %title% and there is no artist in the metadata then only <title> will be displayed not " - <title>".
- 'format' can contain free text and any of the 3 keywords %artist%, %album%, %title%. Using that format string, the keywords are replaced by their value to build the string to be displayed. Note that the plain text following a keyword that happens to be empty during playback of a track will be removed. For example, if you have set format=%artist% - %title% and there is no artist in the metadata then only \<title> will be displayed not - \<title>.

You can install the excellent plugin "Music Information Screen" which is super useful to tweak the layout for these small displays.

Expand All @@ -98,6 +98,7 @@ The NVS parameter "led_vu_config" sets the parameters for an RGB VU meter displa
```
WS2812,length=<leds>,data<gpio>[hold=<cycles>,enable=<gpio>,refresh=<delay>,bright=<bright>]
```
- WS2812 is the string type, more will be added in the future
- 'length' is the number of leds in the string. This should be an odd number.

- 'data' is the gpio used for the data line for the leds
Expand All @@ -114,15 +115,19 @@ a 3.3 volt supply. An enable will help conserve power when the the player is in
Currently the only supported LEDs are the WS2812B strips. The number of LEDs specified should be odd due to an LED in the center between the two channels,
The display is layed out as follows:
```
RRROOOGGG...GRG...GGGOOORRR
RRROOOGGG...GXG...GGGOOORRR
```
Where R is an always on RED led in the center of the string, The VU meters are green at lower sound levels starting on either side of the RED LED.
At higher sound levels the leds are ORANGE then RED. The peak hold LED is BLUE.
Where the colour of Center LED X is dependant on the battery voltage.
The colours are as follow:
- Blue - full charge (>3.9 V per cell)
- Green- good charge (>3.75V per cell)
- Orange - ok charge (>3.7 V per cell)
- Red - needs charge (>3.4V per cell)
- Unlit - bad charge (3.4V or less per cell)

Configuring the RGB leds will bring in the display driver and code that consumes the sound data that is also used by the OLED VU Meters and spectrum analyzer.
Thus the OLED VUs may not be as responsive since they will be competing for audio data with the RGB leds that usually have faster update times.
This short coming will be addressed in a future release.
On either side of the center led are the VU meters. They are green for the lower levels. At higher sound levels the LEDs are ORANGE then RED. The peak hold LED is BLUE.

Configuring the RGB leds will bring in the display driver and code that consumes the sound data that is also used by the OLED VU Meters and spectrum analyzer.

### Infrared
You can use any IR receiver compatible with NEC protocol (38KHz). Vcc, GND and output are the only pins that need to be connected, no pullup, no filtering capacitor, it's a straight connection.
Expand All @@ -146,21 +151,25 @@ You can set the Green and Red status led as well with their respective active st

The \<ir\> parameter set the GPIO associated to an IR receiver. No need to add pullup or capacitor

The \<expander\> parameter sets the gpio associated with the gpio expander interrupt. This allows additional buttons to be defined using the i2c gpio expander.

Syntax is:

```
<gpio>=Vcc|GND|amp|ir|jack[:0|1]|green[:0|1]|red[:0|1]|spkfault[:0|1][,<repeated sequence for next GPIO>]
<gpio>=Vcc|GND|amp|ir|jack[:0|1]|green[:0|1]|red[:0|1]|spkfault[:0|1]|expander[,<repeated sequence for next GPIO>]
```
You can define the defaults for jack, spkfault leds at compile time but nvs parameter takes precedence except for SqueezeAMP where these are forced at runtime.
### Rotary Encoder
One rotary encoder is supported, quadrature shift with press. Such encoders usually have 2 pins for encoders (A and B), and common C that must be set to ground and an optional SW pin for press. A, B and SW must be pulled up, so automatic pull-up is provided by ESP32, but you can add your own resistors. A bit of filtering on A and B (~470nF) helps for debouncing which is not made by software.

Encoder is normally hard-coded to respectively knob left, right and push on LMS and to volume down/up/play toggle on BT and AirPlay. Using the option 'volume' makes it hard-coded to volume down/up/play toggle all the time (even in LMS). The option 'longpress' allows an alternate mode when SW is long-pressed. In that mode, left is previous, right is next and press is toggle. Every long press on SW alternates between modes.

An option external allows the use of an MCP23017 I2C gpio expander with gpios from 0 to 15. When external is specified, long press is not available.

Use parameter rotary_config with the following syntax:

```
A=<gpio>,B=<gpio>[,SW=gpio>[,volume][,longpress]]
A=<gpio>,B=<gpio>[,SW=gpio>[,volume][,longpress]],external
```

HW note: all gpio used for rotary have internal pull-up so normally there is no need to provide Vcc to the encoder. Nevertheless if the encoder board you're using also has its own pull-up that are stronger than ESP32's ones (which is likely the case), then there will be crosstalk between gpio, so you must bring Vcc. Look at your board schematic and you'll understand that these board pull-up create a "winning" pull-down when any other pin is grounded.
Expand All @@ -173,6 +182,7 @@ Buttons are described using a JSON string with the following syntax
[
{"gpio":<num>,
"type":"BUTTON_LOW | BUTTON_HIGH",
"external":[true|false],
"pull":[true|false],
"long_press":<ms>,
"debounce":<ms>,
Expand All @@ -190,6 +200,7 @@ Buttons are described using a JSON string with the following syntax
Where (all parameters are optionals except gpio)
- "type": (BUTTON_LOW) logic level when the button is pressed
- "pull": (false) activate internal pull up/down
- "external": allows the use of an MCP23017 I2C gpio expander with gpios from 0 to 15 instead of the ESP32 GPIOs. When external is specified, long press, and shift and debounce options not available. "pull" is available only for BUTTON_LOW
- "long_press": (0) duration (in ms) of keypress to detect long press, 0 to disable it
- "debounce": (0) debouncing duration in ms (0 = internal default of 50 ms)
- "shifter_gpio": (-1) gpio number of another button that can be pressed together to create a "shift". Set to -1 to disable shifter
Expand Down Expand Up @@ -281,15 +292,11 @@ The above command will mount this repo into the docker container and start a bas
for you to then follow the below build steps

### Manual Install of ESP-IDF
<<<<<<< HEAD
Follow the instructions from https://docs.espressif.com/projects/esp-idf/en/v4.0/get-started/index.html to install the esp-idf v4.0. This is the currently supported release of the espressif software development system.
=======
<strong>Currently the master branch of this project requires this [IDF](https://github.com/espressif/esp-idf/tree/28f1cdf5ed7149d146ad5019c265c8bc3bfa2ac9) with gcc 5.2 (toolschain dated 20181001)
If you want to use a more recent version of gcc and IDF (4.0 stable), move to cmake-master branch</strong>

You can install IDF manually on Linux or Windows (using the Subsystem for Linux) following the instructions at: https://www.instructables.com/id/ESP32-Development-on-Windows-Subsystem-for-Linux/
You also need to use esp-dsp recent version or at least make sure you have this patch https://github.com/espressif/esp-dsp/pull/12/commits/8b082c1071497d49346ee6ed55351470c1cb4264
>>>>>>> refs/remotes/origin/master

## Building Squeezelite-esp32
MOST IMPORTANT: create the right default config file
Expand Down
4 changes: 4 additions & 0 deletions components/display/display.c
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,11 @@ void display_init(char *welcome) {
GDS_SetLayout( display, strcasestr(config, "HFlip"), strcasestr(config, "VFlip"), strcasestr(config, "rotate"));
GDS_SetFont(display, &Font_droid_sans_fallback_15x17 );
GDS_TextPos(display, GDS_FONT_MEDIUM, GDS_TEXT_CENTERED, GDS_TEXT_CLEAR | GDS_TEXT_UPDATE, welcome);
<<<<<<< HEAD
GDS_TextPos(display, GDS_FONT_MEDIUM, GDS_TEXT_BOTTOM_LEFT, GDS_TEXT_UPDATE, "squeezetoo branch"); /** @todo cgr fix this */
=======
GDS_TextPos(display, GDS_FONT_MEDIUM, GDS_TEXT_BOTTOM_LEFT, GDS_TEXT_UPDATE, "GPIO Expander Dev");
>>>>>>> i2c_gpio_expander

// start the task that will handle scrolling & counting
displayer.mutex = xSemaphoreCreateMutex();
Expand Down
67 changes: 43 additions & 24 deletions components/services/audio_controls.c
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ static const actrls_config_map_t actrls_config_map[] =
{"debounce", offsetof(actrls_config_t,debounce), actrls_process_int},
{"type", offsetof(actrls_config_t,type), actrls_process_type},
{"pull", offsetof(actrls_config_t,pull), actrls_process_bool},
{"external", offsetof(actrls_config_t,external), actrls_process_bool},
{"long_press", offsetof(actrls_config_t,long_press),actrls_process_int},
{"shifter_gpio", offsetof(actrls_config_t,shifter_gpio), actrls_process_int},
{"normal", offsetof(actrls_config_t,normal), actrls_process_action},
Expand Down Expand Up @@ -106,11 +107,12 @@ bool actrls_ir_action(uint16_t addr, uint16_t cmd) {
}
}


/****************************************************************************************
*
*/
static void ir_handler(uint16_t addr, uint16_t cmd) {
ESP_LOGD(TAG, "recaived IR %04hx:%04hx", addr, cmd);
ESP_LOGD(TAG, "received IR %04hx:%04hx", addr, cmd);
if (current_ir_handler) current_ir_handler(addr, cmd);
}

Expand All @@ -129,28 +131,43 @@ static void set_ir_gpio(int gpio, char *value) {
esp_err_t actrls_init(const char *profile_name) {
esp_err_t err = ESP_OK;
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, longpress = 0;

// 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);
if ((p = strcasestr(config, "knobonly")) != NULL) {
p = strchr(p, '=');
int double_press = p ? atoi(p + 1) : 350;
rotary.timer = xTimerCreate("knobTimer", double_press / portTICK_RATE_MS, pdFALSE, NULL, rotary_timer);
longpress = 500;
ESP_LOGI(TAG, "single knob navigation %d", double_press);
} else {
if ((p = strcasestr(config, "volume")) != NULL) rotary.volume_lock = true;
if ((p = strcasestr(config, "longpress")) != NULL) longpress = 1000;
}

// create rotary (no handling of long press)
err = create_rotary(NULL, A, B, SW, longpress, control_rotary_handler) ? ESP_OK : ESP_FAIL;
char* p;
int A = -1;
int B = -1;
int SW = -1;
int longpress = 0;
int external = 0;

// parse config
if ((p = strcasestr(config, "external")) != NULL)
external = 1;
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);
if ((p = strcasestr(config, "knobonly")) != NULL) {
p = strchr(p, '=');
int double_press = p ? atoi(p + 1) : 350;
rotary.timer = xTimerCreate("knobTimer",
double_press / portTICK_RATE_MS,
pdFALSE,
NULL,
rotary_timer);
longpress = 500;
ESP_LOGI(TAG, "single knob navigation %d", double_press);
} else {
if ((p = strcasestr(config, "volume")) != NULL)
rotary.volume_lock = true;
if ((p = strcasestr(config, "longpress")) != NULL)
longpress = 1000;
}

// create rotary (no handling of long press)
err = create_rotary(NULL, A, B, SW, longpress, external, control_rotary_handler) ? ESP_OK : ESP_FAIL;
}

// set infrared GPIO if any
Expand Down Expand Up @@ -214,7 +231,7 @@ static void control_handler(void *client, button_event_e event, button_press_e p
ESP_LOGE(TAG,"Invalid profile name %s. Cannot remap buttons",action_detail.name);
}
} else if (action_detail.action != ACTRLS_NONE) {
ESP_LOGD(TAG, "calling action %u", action_detail.action);
ESP_LOGI(TAG, "calling action %u", action_detail.action);
if (current_controls[action_detail.action]) (*current_controls[action_detail.action])(event == BUTTON_PRESSED);
}
}
Expand Down Expand Up @@ -486,6 +503,7 @@ static actrls_config_t * actrls_init_alloc_structure(const cJSON *buttons, const
static void actrls_defaults(actrls_config_t *config) {
config->type = BUTTON_LOW;
config->pull = false;
config->external = false;
config->debounce = 0;
config->long_press = 0;
config->shifter_gpio = -1;
Expand Down Expand Up @@ -538,7 +556,8 @@ static esp_err_t actrls_init_json(const char *profile_name, bool create) {
err = (err == ESP_OK) ? loc_err : err;
if (loc_err == ESP_OK) {
if (create) button_create((void*) cur_config, cur_config->gpio,cur_config->type,
cur_config->pull,cur_config->debounce, control_handler,
cur_config->pull,cur_config->debounce,
cur_config->external, control_handler,
cur_config->long_press, cur_config->shifter_gpio);
} else {
ESP_LOGE(TAG,"Error parsing button structure. Button will not be registered.");
Expand Down
16 changes: 9 additions & 7 deletions components/services/audio_controls.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ typedef struct {
const char * name;
} actrls_action_detail_t;
typedef struct actrl_config_s {
int gpio;
int type;
bool pull;
int debounce;
int long_press;
int shifter_gpio;
actrls_action_detail_t normal[2], longpress[2], shifted[2], longshifted[2]; // [0] keypressed, [1] keyreleased
int gpio;
int type;
bool pull;
bool external;
int debounce;
int long_press;
int shifter_gpio;
actrls_action_detail_t normal[2], longpress[2], shifted[2],
longshifted[2]; // [0] keypressed, [1] keyreleased
} actrls_config_t;

esp_err_t actrls_init(const char *profile_name);
Expand Down
10 changes: 10 additions & 0 deletions components/services/battery.c
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,17 @@ static struct {
/****************************************************************************************
*
*/
<<<<<<< HEAD
float battery_value_svc(void) {
=======
float battery_voltage(void) {
return battery.avg;
}
/****************************************************************************************
*
*/
int battery_value_svc(void) {
>>>>>>> i2c_gpio_expander
return battery.avg;
}

Expand Down
Loading

0 comments on commit acb427f

Please sign in to comment.