From 8fbbcd6f3dea1b0d694db63b21b13230643267b4 Mon Sep 17 00:00:00 2001 From: Philip Gladstone Date: Fri, 2 Feb 2024 19:54:59 -0500 Subject: [PATCH 1/8] Ported rotary driver. Compiles but not tested. --- components/modules/CMakeLists.txt | 2 + components/modules/Kconfig | 6 + components/modules/rotary.c | 413 +++++++++++++++++++++++++++++ components/modules/rotary_driver.c | 284 ++++++++++++++++++++ components/modules/rotary_driver.h | 28 ++ docs/modules/rotary.md | 126 +++++++++ 6 files changed, 859 insertions(+) create mode 100644 components/modules/rotary.c create mode 100644 components/modules/rotary_driver.c create mode 100644 components/modules/rotary_driver.h create mode 100644 docs/modules/rotary.md diff --git a/components/modules/CMakeLists.txt b/components/modules/CMakeLists.txt index c5e77a2a0..81d68f3cb 100644 --- a/components/modules/CMakeLists.txt +++ b/components/modules/CMakeLists.txt @@ -23,6 +23,8 @@ set(module_srcs "otaupgrade.c" "ow.c" "pipe.c" + "rotary_driver.c" + "rotary.c" "rtcmem.c" "qrcodegen.c" "sigma_delta.c" diff --git a/components/modules/Kconfig b/components/modules/Kconfig index 7741e37ba..4b5a8f5ce 100644 --- a/components/modules/Kconfig +++ b/components/modules/Kconfig @@ -225,6 +225,12 @@ menu "NodeMCU modules" Includes the rmt module to use the ESP32's built-in remote control hardware. + config NODEMCU_CMODULE_ROTARY + bool "Rotary switch input device" + default "n" + help + Includes the rotary module which allows easy access to one or more rotary switches. + config NODEMCU_CMODULE_RTCMEM bool "Access to a limited amount of battery backed memory (rtcmem)" default "n" diff --git a/components/modules/rotary.c b/components/modules/rotary.c new file mode 100644 index 000000000..9ae23497b --- /dev/null +++ b/components/modules/rotary.c @@ -0,0 +1,413 @@ +/* + * Module for interfacing with cheap rotary switches that + * are much used in the automtive industry as the cntrols for + * CD players and the like. + * + * Philip Gladstone, N1DQ + */ + +#include "module.h" +#include "lauxlib.h" +#include "platform.h" +#include "task/task.h" +#include "esp_timer.h" +#include +#include +#include +#include "rotary_driver.h" + +#define MASK(x) (1 << ROTARY_ ## x ## _INDEX) + +#define ROTARY_PRESS_INDEX 0 +#define ROTARY_LONGPRESS_INDEX 1 +#define ROTARY_RELEASE_INDEX 2 +#define ROTARY_TURN_INDEX 3 +#define ROTARY_CLICK_INDEX 4 +#define ROTARY_DBLCLICK_INDEX 5 + +#define ROTARY_ALL 0x3f + +#define LONGPRESS_DELAY_US 500000 +#define CLICK_DELAY_US 500000 + +#define CALLBACK_COUNT 6 + +typedef struct { + int lastpos; + int last_recent_event_was_press : 1; + int last_recent_event_was_release : 1; + int timer_running : 1; + int possible_dbl_click : 1; + uint8_t id; + int click_delay_us; + int longpress_delay_us; + uint32_t last_event_time; + int callback[CALLBACK_COUNT]; + esp_timer_handle_t timer_handle; +} DATA; + +static DATA *data[ROTARY_CHANNEL_COUNT]; +static task_handle_t tasknumber; +static void lrotary_timer_done(void *param); +static void lrotary_check_timer(DATA *d, uint32_t time_us, bool dotimer); + +static void callback_free_one(lua_State *L, int *cb_ptr) +{ + if (*cb_ptr != LUA_NOREF) { + luaL_unref(L, LUA_REGISTRYINDEX, *cb_ptr); + *cb_ptr = LUA_NOREF; + } +} + +static void callback_free(lua_State* L, unsigned int id, int mask) +{ + DATA *d = data[id]; + + if (d) { + int i; + for (i = 0; i < CALLBACK_COUNT; i++) { + if (mask & (1 << i)) { + callback_free_one(L, &d->callback[i]); + } + } + } +} + +static int callback_setOne(lua_State* L, int *cb_ptr, int arg_number) +{ + if (lua_isfunction(L, arg_number)) { + lua_pushvalue(L, arg_number); // copy argument (func) to the top of stack + callback_free_one(L, cb_ptr); + *cb_ptr = luaL_ref(L, LUA_REGISTRYINDEX); + return 0; + } + + return -1; +} + +static int callback_set(lua_State* L, int id, int mask, int arg_number) +{ + DATA *d = data[id]; + int result = 0; + + int i; + for (i = 0; i < CALLBACK_COUNT; i++) { + if (mask & (1 << i)) { + result |= callback_setOne(L, &d->callback[i], arg_number); + } + } + + return result; +} + +static void callback_callOne(lua_State* L, int cb, int mask, int arg, uint32_t time) +{ + if (cb != LUA_NOREF) { + lua_rawgeti(L, LUA_REGISTRYINDEX, cb); + + lua_pushinteger(L, mask); + lua_pushinteger(L, arg); + lua_pushinteger(L, time); + + luaL_pcallx(L, 3, 0); + } +} + +static void callback_call(lua_State* L, DATA *d, int cbnum, int arg, uint32_t time) +{ + if (d) { + callback_callOne(L, d->callback[cbnum], 1 << cbnum, arg, time); + } +} + +int platform_rotary_exists( unsigned int id ) +{ + return (id < ROTARY_CHANNEL_COUNT); +} + +// Lua: setup(id, phase_a, phase_b [, press]) +static int lrotary_setup( lua_State* L ) +{ + unsigned int id; + + id = luaL_checkinteger( L, 1 ); + MOD_CHECK_ID( rotary, id ); + + if (rotary_close(id)) { + return luaL_error( L, "Unable to close switch." ); + } + callback_free(L, id, ROTARY_ALL); + + if (!data[id]) { + data[id] = (DATA *) calloc(1, sizeof(DATA)); + if (!data[id]) { + return -1; + } + } + + DATA *d = data[id]; + memset(d, 0, sizeof(*d)); + + d->id = id; + + esp_timer_create_args_t timer_args = { + .callback = lrotary_timer_done, + .dispatch_method = ESP_TIMER_TASK, + .name = "rotary_timer", + .arg = d + }; + + esp_timer_create(&timer_args, &d->timer_handle); + + int i; + for (i = 0; i < CALLBACK_COUNT; i++) { + d->callback[i] = LUA_NOREF; + } + + d->click_delay_us = CLICK_DELAY_US; + d->longpress_delay_us = LONGPRESS_DELAY_US; + + int phase_a = luaL_checkinteger(L, 2); + luaL_argcheck(L, platform_gpio_exists(phase_a) && phase_a > 0, 2, "Invalid pin"); + int phase_b = luaL_checkinteger(L, 3); + luaL_argcheck(L, platform_gpio_exists(phase_b) && phase_b > 0, 3, "Invalid pin"); + int press; + if (lua_gettop(L) >= 4) { + press = luaL_checkinteger(L, 4); + luaL_argcheck(L, platform_gpio_exists(press) && press > 0, 4, "Invalid pin"); + } else { + press = -1; + } + + if (lua_gettop(L) >= 5) { + d->longpress_delay_us = 1000 * luaL_checkinteger(L, 5); + luaL_argcheck(L, d->longpress_delay_us > 0, 5, "Invalid timeout"); + } + + if (lua_gettop(L) >= 6) { + d->click_delay_us = 1000 * luaL_checkinteger(L, 6); + luaL_argcheck(L, d->click_delay_us > 0, 6, "Invalid timeout"); + } + + if (rotary_setup(id, phase_a, phase_b, press, tasknumber)) { + return luaL_error(L, "Unable to setup rotary switch."); + } + return 0; +} + +// Lua: close( id ) +static int lrotary_close( lua_State* L ) +{ + unsigned int id; + + id = luaL_checkinteger( L, 1 ); + MOD_CHECK_ID( rotary, id ); + callback_free(L, id, ROTARY_ALL); + + DATA *d = data[id]; + if (d) { + data[id] = NULL; + free(d); + } + + if (rotary_close( id )) { + return luaL_error( L, "Unable to close switch." ); + } + return 0; +} + +// Lua: on( id, mask[, cb] ) +static int lrotary_on( lua_State* L ) +{ + unsigned int id; + id = luaL_checkinteger( L, 1 ); + MOD_CHECK_ID( rotary, id ); + + int mask = luaL_checkinteger(L, 2); + + if (lua_gettop(L) >= 3) { + if (callback_set(L, id, mask, 3)) { + return luaL_error( L, "Unable to set callback." ); + } + } else { + callback_free(L, id, mask); + } + + return 0; +} + +// Lua: getpos( id ) -> pos, PRESS/RELEASE +static int lrotary_getpos( lua_State* L ) +{ + unsigned int id; + id = luaL_checkinteger( L, 1 ); + MOD_CHECK_ID( rotary, id ); + + int pos = rotary_getpos(id); + + if (pos == -1) { + return 0; + } + + lua_pushinteger(L, (pos << 1) >> 1); + lua_pushinteger(L, (pos & 0x80000000) ? MASK(PRESS) : MASK(RELEASE)); + + return 2; +} + +// Returns TRUE if there maybe/is more stuff to do +static bool lrotary_dequeue_single(lua_State* L, DATA *d) +{ + bool something_pending = false; + + if (d) { + // This chnnel is open + rotary_event_t result; + + if (rotary_getevent(d->id, &result)) { + int pos = result.pos; + + lrotary_check_timer(d, result.time_us, 0); + + if (pos != d->lastpos) { + // We have something to enqueue + if ((pos ^ d->lastpos) & 0x7fffffff) { + // Some turning has happened + callback_call(L, d, ROTARY_TURN_INDEX, (pos << 1) >> 1, result.time_us); + } + if ((pos ^ d->lastpos) & 0x80000000) { + // pressing or releasing has happened + callback_call(L, d, (pos & 0x80000000) ? ROTARY_PRESS_INDEX : ROTARY_RELEASE_INDEX, (pos << 1) >> 1, result.time_us); + if (pos & 0x80000000) { + // Press + if (d->last_recent_event_was_release && result.time_us - d->last_event_time < d->click_delay_us) { + d->possible_dbl_click = 1; + } + d->last_recent_event_was_press = 1; + d->last_recent_event_was_release = 0; + } else { + // Release + d->last_recent_event_was_press = 0; + if (d->possible_dbl_click) { + callback_call(L, d, ROTARY_DBLCLICK_INDEX, (pos << 1) >> 1, result.time_us); + d->possible_dbl_click = 0; + // Do this to suppress the CLICK event + d->last_recent_event_was_release = 0; + } else { + d->last_recent_event_was_release = 1; + } + } + d->last_event_time = result.time_us; + } + + d->lastpos = pos; + } + + something_pending = rotary_has_queued_event(d->id); + } + + lrotary_check_timer(d, esp_timer_get_time(), 1); + } + + return something_pending; +} + +static void lrotary_timer_done(void *param) +{ + DATA *d = (DATA *) param; + + d->timer_running = 0; + + lrotary_check_timer(d, esp_timer_get_time(), 1); +} + +static void lrotary_check_timer(DATA *d, uint32_t time_us, bool dotimer) +{ + uint32_t delay = time_us - d->last_event_time; + if (d->timer_running) { + esp_timer_stop(d->timer_handle); + d->timer_running = 0; + } + + int timeout = -1; + + if (d->last_recent_event_was_press) { + if (delay > d->longpress_delay_us) { + callback_call(lua_getstate(), d, ROTARY_LONGPRESS_INDEX, (d->lastpos << 1) >> 1, d->last_event_time + d->longpress_delay_us); + d->last_recent_event_was_press = 0; + } else { + timeout = (d->longpress_delay_us - delay) / 1000; + } + } + if (d->last_recent_event_was_release) { + if (delay > d->click_delay_us) { + callback_call(lua_getstate(), d, ROTARY_CLICK_INDEX, (d->lastpos << 1) >> 1, d->last_event_time + d->click_delay_us); + d->last_recent_event_was_release = 0; + } else { + timeout = (d->click_delay_us - delay) / 1000; + } + } + + if (dotimer && timeout >= 0) { + d->timer_running = 1; + esp_timer_start_once(d->timer_handle, timeout + 1); + } +} + +static void lrotary_task(task_param_t param, task_prio_t prio) +{ + (void) param; + (void) prio; + + uint8_t *task_queue_ptr = (uint8_t*) param; + if (task_queue_ptr) { + // Signal that new events may need another task post + *task_queue_ptr = 0; + } + + int id; + bool need_to_post = false; + lua_State *L = lua_getstate(); + + for (id = 0; id < ROTARY_CHANNEL_COUNT; id++) { + DATA *d = data[id]; + if (d) { + if (lrotary_dequeue_single(L, d)) { + need_to_post = true; + } + } + } + + if (need_to_post) { + // If there is pending stuff, queue another task + task_post_medium(tasknumber, 0); + } +} + +static int rotary_open(lua_State *L) +{ + tasknumber = task_get_id(lrotary_task); + if (rotary_driver_init() != ESP_OK) { + return luaL_error(L, "Initialization fail"); + } + return 0; +} + +// Module function map +LROT_BEGIN(rotary, NULL, 0) + LROT_FUNCENTRY( setup, lrotary_setup ) + LROT_FUNCENTRY( close, lrotary_close ) + LROT_FUNCENTRY( on, lrotary_on ) + LROT_FUNCENTRY( getpos, lrotary_getpos ) + LROT_NUMENTRY( TURN, MASK(TURN) ) + LROT_NUMENTRY( PRESS, MASK(PRESS) ) + LROT_NUMENTRY( RELEASE, MASK(RELEASE) ) + LROT_NUMENTRY( LONGPRESS, MASK(LONGPRESS) ) + LROT_NUMENTRY( CLICK, MASK(CLICK) ) + LROT_NUMENTRY( DBLCLICK, MASK(DBLCLICK) ) + LROT_NUMENTRY( ALL, ROTARY_ALL ) + +LROT_END(rotary, NULL, 0) + + +NODEMCU_MODULE(ROTARY, "rotary", rotary, rotary_open); diff --git a/components/modules/rotary_driver.c b/components/modules/rotary_driver.c new file mode 100644 index 000000000..e73257ca2 --- /dev/null +++ b/components/modules/rotary_driver.c @@ -0,0 +1,284 @@ +/* + * Driver for interfacing to cheap rotary switches that + * have a quadrature output with an optional press button + * + * This sets up the relevant gpio as interrupt and then keeps track of + * the position of the switch in software. Changes are enqueued to task + * level and a task message posted when required. If the queue fills up + * then moves are ignored, but the last press/release will be included. + * + * Philip Gladstone, N1DQ + */ + +#include "platform.h" +#include +#include +#include +#include "task/task.h" +#include "rotary_driver.h" +#include "driver/gpio.h" +#include "esp_timer.h" + + +// +// Queue is empty if read == write. +// However, we always want to keep the previous value +// so writing is only allowed if write - read < QUEUE_SIZE - 1 + +#define QUEUE_SIZE 8 + +#define GET_LAST_STATUS(d) (d->queue[(d->write_offset-1) & (QUEUE_SIZE - 1)]) +#define GET_PREV_STATUS(d) (d->queue[(d->write_offset-2) & (QUEUE_SIZE - 1)]) +#define HAS_QUEUED_DATA(d) (d->read_offset < d->write_offset) +#define HAS_QUEUE_SPACE(d) (d->read_offset + QUEUE_SIZE - 1 > d->write_offset) + +#define REPLACE_STATUS(d, x) (d->queue[(d->write_offset-1) & (QUEUE_SIZE - 1)] = (rotary_event_t) { (x), esp_timer_get_time() }) +#define QUEUE_STATUS(d, x) (d->queue[(d->write_offset++) & (QUEUE_SIZE - 1)] = (rotary_event_t) { (x), esp_timer_get_time() }) + +#define GET_READ_STATUS(d) (d->queue[d->read_offset & (QUEUE_SIZE - 1)]) +#define ADVANCE_IF_POSSIBLE(d) if (d->read_offset < d->write_offset) { d->read_offset++; } + +#define STATUS_IS_PRESSED(x) (((x) & 0x80000000) != 0) + +typedef struct { + int8_t phase_a_pin; + int8_t phase_b_pin; + int8_t press_pin; + uint32_t read_offset; // Accessed by task + uint32_t write_offset; // Accessed by ISR + uint32_t last_press_change_time; + int tasknumber; + rotary_event_t queue[QUEUE_SIZE]; +} DATA; + +static DATA *data[ROTARY_CHANNEL_COUNT]; + +static uint8_t task_queued; + +static void set_gpio_mode(int pin, gpio_int_type_t intr) +{ + gpio_config_t config = { + .pin_bit_mask = 1LL << pin, + .mode = GPIO_MODE_INPUT, + .pull_up_en = GPIO_PULLUP_ENABLE, + .intr_type = intr + }; + + gpio_config(&config); +} + +static void rotary_clear_pin(int pin) +{ + if (pin >= 0) { + gpio_isr_handler_remove(pin); + set_gpio_mode(pin, GPIO_INTR_DISABLE); + } +} + +// Just takes the channel number. Cleans up the resources used. +int rotary_close(uint32_t channel) +{ + if (channel >= sizeof(data) / sizeof(data[0])) { + return -1; + } + + DATA *d = data[channel]; + + if (!d) { + return 0; + } + + data[channel] = NULL; + + rotary_clear_pin(d->phase_a_pin); + rotary_clear_pin(d->phase_b_pin); + rotary_clear_pin(d->press_pin); + + free(d); + + return 0; +} + +static void rotary_interrupt(void *arg) +{ + // This function runs with high priority + DATA *d = (DATA *) arg; + + uint32_t last_status = GET_LAST_STATUS(d).pos; + + uint32_t now = esp_timer_get_time(); + + uint32_t new_status; + + new_status = last_status & 0x80000000; + + // This is the debounce logic for the press switch. We ignore changes + // for 10ms after a change. + if (now - d->last_press_change_time > 10 * 1000) { + new_status = gpio_get_level(d->press_pin) ? 0 : 0x80000000; + if (STATUS_IS_PRESSED(new_status ^ last_status)) { + d->last_press_change_time = now; + } + } + + // A B + // 1 1 => 0 + // 1 0 => 1 + // 0 0 => 2 + // 0 1 => 3 + + int micropos = 2; + if (gpio_get_level(d->phase_b_pin)) { + micropos = 3; + } + if (gpio_get_level(d->phase_a_pin)) { + micropos ^= 3; + } + + int32_t rotary_pos = last_status; + + switch ((micropos - last_status) & 3) { + case 0: + // No change, nothing to do + break; + case 1: + // Incremented by 1 + rotary_pos++; + break; + case 3: + // Decremented by 1 + rotary_pos--; + break; + default: + // We missed an interrupt + // We will ignore... but mark it. + rotary_pos += 1000000; + break; + } + + new_status |= rotary_pos & 0x7fffffff; + + if (last_status != new_status) { + // Either we overwrite the status or we add a new one + if (!HAS_QUEUED_DATA(d) + || STATUS_IS_PRESSED(last_status ^ new_status) + || STATUS_IS_PRESSED(last_status ^ GET_PREV_STATUS(d).pos)) { + if (HAS_QUEUE_SPACE(d)) { + QUEUE_STATUS(d, new_status); + if (!task_queued) { + if (task_post_medium(d->tasknumber, (task_param_t) &task_queued)) { + task_queued = 1; + } + } + } else { + REPLACE_STATUS(d, new_status); + } + } else { + REPLACE_STATUS(d, new_status); + } + } +} + +// The pin numbers are actual platform GPIO numbers +int rotary_setup(uint32_t channel, int phase_a, int phase_b, int press, task_handle_t tasknumber ) +{ + if (channel >= sizeof(data) / sizeof(data[0])) { + return -1; + } + + if (data[channel]) { + if (rotary_close(channel)) { + return -1; + } + } + + DATA *d = (DATA *) calloc(1, sizeof(DATA)); + if (!d) { + return -1; + } + + data[channel] = d; + + d->tasknumber = tasknumber; + + set_gpio_mode(phase_a, GPIO_INTR_ANYEDGE); + gpio_isr_handler_add(phase_a, rotary_interrupt, d); + d->phase_a_pin = phase_a; + + set_gpio_mode(phase_b, GPIO_INTR_ANYEDGE); + gpio_isr_handler_add(phase_b, rotary_interrupt, d); + d->phase_b_pin = phase_b; + + if (press >= 0) { + set_gpio_mode(press, GPIO_INTR_ANYEDGE); + gpio_isr_handler_add(press, rotary_interrupt, d); + } + d->press_pin = press; + + return 0; +} + +bool rotary_has_queued_event(uint32_t channel) +{ + if (channel >= sizeof(data) / sizeof(data[0])) { + return false; + } + + DATA *d = data[channel]; + + if (!d) { + return false; + } + + return HAS_QUEUED_DATA(d); +} + +// Get the oldest event in the queue and remove it (if possible) +bool rotary_getevent(uint32_t channel, rotary_event_t *resultp) +{ + rotary_event_t result = { 0 }; + + if (channel >= sizeof(data) / sizeof(data[0])) { + return false; + } + + DATA *d = data[channel]; + + if (!d) { + return false; + } + + bool status = false; + + if (HAS_QUEUED_DATA(d)) { + result = GET_READ_STATUS(d); + d->read_offset++; + status = true; + } else { + result = GET_LAST_STATUS(d); + } + + *resultp = result; + + return status; +} + +int rotary_getpos(uint32_t channel) +{ + if (channel >= sizeof(data) / sizeof(data[0])) { + return -1; + } + + DATA *d = data[channel]; + + if (!d) { + return -1; + } + + return GET_LAST_STATUS(d).pos; +} + +esp_err_t rotary_driver_init() +{ + return gpio_install_isr_service(ESP_INTR_FLAG_LOWMED); +} diff --git a/components/modules/rotary_driver.h b/components/modules/rotary_driver.h new file mode 100644 index 000000000..5aeab051c --- /dev/null +++ b/components/modules/rotary_driver.h @@ -0,0 +1,28 @@ +/* + * Definitions to access the Rotary driver + */ +#ifndef __ROTARY_H__ +#define __ROTARY_H__ + +#include + +#define ROTARY_CHANNEL_COUNT 3 + +typedef struct { + uint32_t pos; + uint32_t time_us; +} rotary_event_t; + +int rotary_setup(uint32_t channel, int phaseA, int phaseB, int press, task_handle_t tasknumber); + +bool rotary_getevent(uint32_t channel, rotary_event_t *result); + +bool rotary_has_queued_event(uint32_t channel); + +int rotary_getpos(uint32_t channel); + +int rotary_close(uint32_t channel); + +int rotary_driver_init(); + +#endif diff --git a/docs/modules/rotary.md b/docs/modules/rotary.md new file mode 100644 index 000000000..f3c102f15 --- /dev/null +++ b/docs/modules/rotary.md @@ -0,0 +1,126 @@ +# rotary Module +| Since | Origin / Contributor | Maintainer | Source | +| :----- | :-------------------- | :---------- | :------ | +| 2016-03-01 | [Philip Gladstone](https://github.com/pjsg) | [Philip Gladstone](https://github.com/pjsg) | [rotary.c](../../components/modules/rotary.c)| + + +This module can read the state of cheap rotary encoder switches. These are available at all the standard places for a dollar or two. They are five pin devices where three are used for a gray code encoder for rotation, and two are used for the push switch. These switches are commonly used in car audio systems. + +These switches do not have absolute positioning, but only encode the number of positions rotated clockwise / anti-clockwise. To make use of this module, connect the common pin on the quadrature encoder to ground and the A and B phases to the NodeMCU. One pin of the push switch should also be grounded and the other pin connected to the NodeMCU. + +## Sources for parts + +- Amazon: This [search](http://www.amazon.com/s/ref=nb_sb_noss_1?url=search-alias%3Dindustrial&field-keywords=rotary+encoder+push+button&rh=n%3A16310091%2Ck%3Arotary+encoder+push+button) shows a variety. +- Ebay: Somewhat cheaper in this [search](http://www.ebay.com/sch/i.html?_from=R40&_trksid=p2050601.m570.l1313.TR0.TRC0.H0.Xrotary+encoder+push+button.TRS0&_nkw=rotary+encoder+push+button&_sacat=0) +- Adafruit: [rotary encoder](https://www.adafruit.com/products/377) +- Aliexpress: This [search](http://www.aliexpress.com/wholesale?catId=0&initiative_id=SB_20160217173657&SearchText=rotary+encoder+push+button) reveals all sorts of shapes and sizes. + +There is also a switch mounted on a board with standard 0.1" pins. +This is the KY-040, and can also be found at [lots of places](https://www.google.com/webhp?sourceid=chrome-instant&ion=1&espv=2&ie=UTF-8#q=ky-040%20rotary%20encoder). +Note that the pins are named somewhat eccentrically, and I suspect that it really does need the VCC connected. + +## Constants +- `rotary.PRESS = 1` The eventtype for the switch press. +- `rotary.LONGPRESS = 2` The eventtype for a long press. +- `rotary.RELEASE = 4` The eventtype for the switch release. +- `rotary.TURN = 8` The eventtype for the switch rotation. +- `rotary.CLICK = 16` The eventtype for a single click (after release) +- `rotary.DBLCLICK = 32` The eventtype for a double click (after second release) +- `rotary.ALL = 63` All event types. + +## rotary.setup() +Initialize the nodemcu to talk to a rotary encoder switch. + +#### Syntax +`rotary.setup(channel, pina, pinb[, pinpress[, longpress_time_ms[, dblclick_time_ms]]])` + +#### Parameters +- `channel` The rotary module supports three switches. The channel is either 0, 1 or 2. +- `pina` This is a GPIO number (excluding 0) and connects to pin phase A on the rotary switch. +- `pinb` This is a GPIO number (excluding 0) and connects to pin phase B on the rotary switch. +- `pinpress` (optional) This is a GPIO number (excluding 0) and connects to the press switch. +- `longpress_time_ms` (optional) The number of milliseconds (default 500) of press to be considered a long press. +- `dblclick_time_ms` (optional) The number of milliseconds (default 500) between a release and a press for the next release to be considered a double click. + +#### Returns +Nothing. If the arguments are in error, or the operation cannot be completed, then an error is thrown. + +For all API calls, if the channel number is out of range, then an error will be thrown. + +#### Example + + rotary.setup(0, 5,6, 7) + +## rotary.on() +Sets a callback on specific events. + +#### Syntax +`rotary.on(channel, eventtype[, callback])` + +#### Parameters +- `channel` The rotary module supports three switches. The channel is either 0, 1 or 2. +- `eventtype` This defines the type of event being registered. This is the logical or of one or more of `PRESS`, `LONGPRESS`, `RELEASE`, `TURN`, `CLICK` or `DBLCLICK`. +- `callback` This is a function that will be invoked when the specified event happens. + +If the callback is None or omitted, then the registration is cancelled. + +The callback will be invoked with three arguments when the event happens. The first argument is the eventtype, +the second is the current position of the rotary switch, and the third is the time when the event happened. + +The position is tracked +and is represented as a signed 32-bit integer. Increasing values indicate clockwise motion. The time is the number of microseconds represented +in a 32-bit integer. Note that this wraps every hour or so. + +#### Example + + rotary.on(0, rotary.ALL, function (type, pos, when) + print "Position=" .. pos .. " event type=" .. type .. " time=" .. when + end) + +#### Notes + +Events will be delivered in order, but there may be missing TURN events. If there is a long +queue of events, then PRESS and RELEASE events may also be missed. Multiple pending TURN events +are typically dispatched as one TURN callback with the final position as its parameter. + +Some switches have 4 steps per detent. This means that, in practice, the application +should divide the position by 4 and use that to determine the number of clicks. It is +unlikely that a switch will ever reach 30 bits of rotation in either direction -- some +are rated for under 50,000 revolutions. + +The `CLICK` and `LONGPRESS` events are delivered on a timeout. The `DBLCLICK` event is delivered after a `PRESS`, `RELEASE`, `PRESS`, `RELEASE` sequence +where this is a short time gap between the middle `RELEASE` and `PRESS`. + +#### Errors +If an invalid `eventtype` is supplied, then an error will be thrown. + +## rotary.getpos() +Gets the current position and press status of the switch + +#### Syntax +`pos, press = rotary.getpos(channel)` + +#### Parameters +- `channel` The rotary module supports three switches. The channel is either 0, 1 or 2. + +#### Returns +- `pos` The current position of the switch. +- `press` A boolean indicating if the switch is currently pressed. + +#### Example + + print rotary.getpos(0) + +## rotary.close() +Releases the resources associated with the rotary switch. + +#### Syntax +`rotary.close(channel)` + +#### Parameters +- `channel` The rotary module supports three switches. The channel is either 0, 1 or 2. + +#### Example + + rotary.close(0) + From 82b4c8148418b321e4197a34eb66eed6c0b3b8fb Mon Sep 17 00:00:00 2001 From: Philip Gladstone Date: Fri, 2 Feb 2024 21:47:10 -0500 Subject: [PATCH 2/8] Added the rotary switch driver to the esp32 version --- components/modules/rotary.c | 170 ++++++++++++----------------- components/modules/rotary_driver.c | 88 ++++----------- components/modules/rotary_driver.h | 16 +-- docs/modules/rotary.md | 33 ++---- 4 files changed, 111 insertions(+), 196 deletions(-) diff --git a/components/modules/rotary.c b/components/modules/rotary.c index 9ae23497b..409ef7847 100644 --- a/components/modules/rotary.c +++ b/components/modules/rotary.c @@ -38,7 +38,7 @@ typedef struct { int last_recent_event_was_release : 1; int timer_running : 1; int possible_dbl_click : 1; - uint8_t id; + struct rotary_driver_handle *handle; int click_delay_us; int longpress_delay_us; uint32_t last_event_time; @@ -46,7 +46,6 @@ typedef struct { esp_timer_handle_t timer_handle; } DATA; -static DATA *data[ROTARY_CHANNEL_COUNT]; static task_handle_t tasknumber; static void lrotary_timer_done(void *param); static void lrotary_check_timer(DATA *d, uint32_t time_us, bool dotimer); @@ -59,15 +58,13 @@ static void callback_free_one(lua_State *L, int *cb_ptr) } } -static void callback_free(lua_State* L, unsigned int id, int mask) +static void callback_free(lua_State* L, DATA *d, int mask) { - DATA *d = data[id]; - if (d) { int i; for (i = 0; i < CALLBACK_COUNT; i++) { if (mask & (1 << i)) { - callback_free_one(L, &d->callback[i]); + callback_free_one(L, &d->callback[i]); } } } @@ -85,9 +82,8 @@ static int callback_setOne(lua_State* L, int *cb_ptr, int arg_number) return -1; } -static int callback_set(lua_State* L, int id, int mask, int arg_number) +static int callback_set(lua_State* L, DATA *d, int mask, int arg_number) { - DATA *d = data[id]; int result = 0; int i; @@ -120,35 +116,16 @@ static void callback_call(lua_State* L, DATA *d, int cbnum, int arg, uint32_t ti } } -int platform_rotary_exists( unsigned int id ) -{ - return (id < ROTARY_CHANNEL_COUNT); -} - -// Lua: setup(id, phase_a, phase_b [, press]) +// Lua: setup(phase_a, phase_b [, press]) static int lrotary_setup( lua_State* L ) { - unsigned int id; - - id = luaL_checkinteger( L, 1 ); - MOD_CHECK_ID( rotary, id ); - - if (rotary_close(id)) { - return luaL_error( L, "Unable to close switch." ); - } - callback_free(L, id, ROTARY_ALL); - - if (!data[id]) { - data[id] = (DATA *) calloc(1, sizeof(DATA)); - if (!data[id]) { - return -1; - } - } + int nargs = lua_gettop(L); - DATA *d = data[id]; + DATA *d = (DATA *)lua_newuserdata(L, sizeof(DATA)); + if (!d) return luaL_error(L, "not enough memory"); memset(d, 0, sizeof(*d)); - - d->id = id; + luaL_getmetatable(L, "rotary.switch"); + lua_setmetatable(L, -2); esp_timer_create_args_t timer_args = { .callback = lrotary_timer_done, @@ -167,90 +144,83 @@ static int lrotary_setup( lua_State* L ) d->click_delay_us = CLICK_DELAY_US; d->longpress_delay_us = LONGPRESS_DELAY_US; - int phase_a = luaL_checkinteger(L, 2); - luaL_argcheck(L, platform_gpio_exists(phase_a) && phase_a > 0, 2, "Invalid pin"); - int phase_b = luaL_checkinteger(L, 3); - luaL_argcheck(L, platform_gpio_exists(phase_b) && phase_b > 0, 3, "Invalid pin"); + int phase_a = luaL_checkinteger(L, 1); + luaL_argcheck(L, platform_gpio_exists(phase_a) && phase_a > 0, 1, "Invalid pin"); + int phase_b = luaL_checkinteger(L, 2); + luaL_argcheck(L, platform_gpio_exists(phase_b) && phase_b > 0, 2, "Invalid pin"); int press; - if (lua_gettop(L) >= 4) { - press = luaL_checkinteger(L, 4); - luaL_argcheck(L, platform_gpio_exists(press) && press > 0, 4, "Invalid pin"); + if (nargs >= 3) { + press = luaL_checkinteger(L, 3); + luaL_argcheck(L, platform_gpio_exists(press) && press > 0, 3, "Invalid pin"); } else { press = -1; } - if (lua_gettop(L) >= 5) { - d->longpress_delay_us = 1000 * luaL_checkinteger(L, 5); - luaL_argcheck(L, d->longpress_delay_us > 0, 5, "Invalid timeout"); + if (nargs >= 4) { + d->longpress_delay_us = 1000 * luaL_checkinteger(L, 4); + luaL_argcheck(L, d->longpress_delay_us > 0, 4, "Invalid timeout"); } - if (lua_gettop(L) >= 6) { - d->click_delay_us = 1000 * luaL_checkinteger(L, 6); - luaL_argcheck(L, d->click_delay_us > 0, 6, "Invalid timeout"); + if (nargs >= 5) { + d->click_delay_us = 1000 * luaL_checkinteger(L, 5); + luaL_argcheck(L, d->click_delay_us > 0, 5, "Invalid timeout"); } - if (rotary_setup(id, phase_a, phase_b, press, tasknumber)) { + d->handle = rotary_setup(phase_a, phase_b, press, tasknumber, d); + if (!d->handle) { return luaL_error(L, "Unable to setup rotary switch."); } - return 0; + return 1; } -// Lua: close( id ) +// Lua: close( ) static int lrotary_close( lua_State* L ) { - unsigned int id; + DATA *d = (DATA *)luaL_checkudata(L, 1, "rotary.switch"); - id = luaL_checkinteger( L, 1 ); - MOD_CHECK_ID( rotary, id ); - callback_free(L, id, ROTARY_ALL); + if (d->handle) { + callback_free(L, d, ROTARY_ALL); - DATA *d = data[id]; - if (d) { - data[id] = NULL; - free(d); - } + if (rotary_close( d->handle )) { + return luaL_error( L, "Unable to close switch." ); + } - if (rotary_close( id )) { - return luaL_error( L, "Unable to close switch." ); + d->handle = NULL; } return 0; } -// Lua: on( id, mask[, cb] ) +// Lua: on( mask[, cb] ) static int lrotary_on( lua_State* L ) { - unsigned int id; - id = luaL_checkinteger( L, 1 ); - MOD_CHECK_ID( rotary, id ); + DATA *d = (DATA *)luaL_checkudata(L, 1, "rotary.switch"); int mask = luaL_checkinteger(L, 2); if (lua_gettop(L) >= 3) { - if (callback_set(L, id, mask, 3)) { + if (callback_set(L, d, mask, 3)) { return luaL_error( L, "Unable to set callback." ); } } else { - callback_free(L, id, mask); + callback_free(L, d, mask); } return 0; } -// Lua: getpos( id ) -> pos, PRESS/RELEASE +// Lua: getpos( ) -> pos, PRESS/RELEASE static int lrotary_getpos( lua_State* L ) { - unsigned int id; - id = luaL_checkinteger( L, 1 ); - MOD_CHECK_ID( rotary, id ); - - int pos = rotary_getpos(id); + DATA *d = (DATA *)luaL_checkudata(L, 1, "rotary.switch"); + + int pos = rotary_getpos(d->handle); if (pos == -1) { return 0; } lua_pushinteger(L, (pos << 1) >> 1); - lua_pushinteger(L, (pos & 0x80000000) ? MASK(PRESS) : MASK(RELEASE)); + lua_pushboolean(L, (pos & 0x80000000)); return 2; } @@ -264,7 +234,7 @@ static bool lrotary_dequeue_single(lua_State* L, DATA *d) // This chnnel is open rotary_event_t result; - if (rotary_getevent(d->id, &result)) { + if (rotary_getevent(d->handle, &result)) { int pos = result.pos; lrotary_check_timer(d, result.time_us, 0); @@ -300,10 +270,11 @@ static bool lrotary_dequeue_single(lua_State* L, DATA *d) d->last_event_time = result.time_us; } - d->lastpos = pos; + d->lastpos = pos; } - something_pending = rotary_has_queued_event(d->id); + rotary_event_handled(d->handle); + something_pending = rotary_has_queued_event(d->handle); } lrotary_check_timer(d, esp_timer_get_time(), 1); @@ -356,49 +327,28 @@ static void lrotary_check_timer(DATA *d, uint32_t time_us, bool dotimer) static void lrotary_task(task_param_t param, task_prio_t prio) { - (void) param; (void) prio; - uint8_t *task_queue_ptr = (uint8_t*) param; - if (task_queue_ptr) { - // Signal that new events may need another task post - *task_queue_ptr = 0; - } - - int id; bool need_to_post = false; lua_State *L = lua_getstate(); - for (id = 0; id < ROTARY_CHANNEL_COUNT; id++) { - DATA *d = data[id]; - if (d) { - if (lrotary_dequeue_single(L, d)) { - need_to_post = true; - } + DATA *d = (DATA *) param; + if (d) { + if (lrotary_dequeue_single(L, d)) { + need_to_post = true; } } if (need_to_post) { // If there is pending stuff, queue another task - task_post_medium(tasknumber, 0); + task_post_medium(tasknumber, param); } } -static int rotary_open(lua_State *L) -{ - tasknumber = task_get_id(lrotary_task); - if (rotary_driver_init() != ESP_OK) { - return luaL_error(L, "Initialization fail"); - } - return 0; -} // Module function map LROT_BEGIN(rotary, NULL, 0) LROT_FUNCENTRY( setup, lrotary_setup ) - LROT_FUNCENTRY( close, lrotary_close ) - LROT_FUNCENTRY( on, lrotary_on ) - LROT_FUNCENTRY( getpos, lrotary_getpos ) LROT_NUMENTRY( TURN, MASK(TURN) ) LROT_NUMENTRY( PRESS, MASK(PRESS) ) LROT_NUMENTRY( RELEASE, MASK(RELEASE) ) @@ -406,8 +356,22 @@ LROT_BEGIN(rotary, NULL, 0) LROT_NUMENTRY( CLICK, MASK(CLICK) ) LROT_NUMENTRY( DBLCLICK, MASK(DBLCLICK) ) LROT_NUMENTRY( ALL, ROTARY_ALL ) - LROT_END(rotary, NULL, 0) +// Module function map +LROT_BEGIN(rotary_switch, NULL, LROT_MASK_GC_INDEX) + LROT_FUNCENTRY(__gc, lrotary_close) + LROT_TABENTRY(__index, rotary_switch) + LROT_FUNCENTRY(on, lrotary_on) + LROT_FUNCENTRY(close, lrotary_close) + LROT_FUNCENTRY(getpos, lrotary_getpos) +LROT_END(rotary_switch, NULL, LROT_MASK_GC_INDEX) + +static int rotary_open(lua_State *L) { + luaL_rometatable(L, "rotary.switch", + LROT_TABLEREF(rotary_switch)); // create metatable + tasknumber = task_get_id(lrotary_task); + return 0; +} NODEMCU_MODULE(ROTARY, "rotary", rotary, rotary_open); diff --git a/components/modules/rotary_driver.c b/components/modules/rotary_driver.c index e73257ca2..934175cfb 100644 --- a/components/modules/rotary_driver.c +++ b/components/modules/rotary_driver.c @@ -40,20 +40,18 @@ #define STATUS_IS_PRESSED(x) (((x) & 0x80000000) != 0) -typedef struct { +typedef struct rotary_driver_handle { int8_t phase_a_pin; int8_t phase_b_pin; int8_t press_pin; + int8_t task_queued; uint32_t read_offset; // Accessed by task uint32_t write_offset; // Accessed by ISR uint32_t last_press_change_time; int tasknumber; rotary_event_t queue[QUEUE_SIZE]; -} DATA; - -static DATA *data[ROTARY_CHANNEL_COUNT]; - -static uint8_t task_queued; + void *callback_arg; +} *rotary_driver_handle_t; static void set_gpio_mode(int pin, gpio_int_type_t intr) { @@ -61,6 +59,7 @@ static void set_gpio_mode(int pin, gpio_int_type_t intr) .pin_bit_mask = 1LL << pin, .mode = GPIO_MODE_INPUT, .pull_up_en = GPIO_PULLUP_ENABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, .intr_type = intr }; @@ -76,20 +75,12 @@ static void rotary_clear_pin(int pin) } // Just takes the channel number. Cleans up the resources used. -int rotary_close(uint32_t channel) +int rotary_close(rotary_driver_handle_t d) { - if (channel >= sizeof(data) / sizeof(data[0])) { - return -1; - } - - DATA *d = data[channel]; - if (!d) { return 0; } - data[channel] = NULL; - rotary_clear_pin(d->phase_a_pin); rotary_clear_pin(d->phase_b_pin); rotary_clear_pin(d->press_pin); @@ -102,7 +93,7 @@ int rotary_close(uint32_t channel) static void rotary_interrupt(void *arg) { // This function runs with high priority - DATA *d = (DATA *) arg; + rotary_driver_handle_t d = (rotary_driver_handle_t)arg; uint32_t last_status = GET_LAST_STATUS(d).pos; @@ -165,41 +156,35 @@ static void rotary_interrupt(void *arg) || STATUS_IS_PRESSED(last_status ^ GET_PREV_STATUS(d).pos)) { if (HAS_QUEUE_SPACE(d)) { QUEUE_STATUS(d, new_status); - if (!task_queued) { - if (task_post_medium(d->tasknumber, (task_param_t) &task_queued)) { - task_queued = 1; + if (!d->task_queued) { + if (task_post_medium(d->tasknumber, (task_param_t) d->callback_arg)) { + d->task_queued = 1; } } } else { REPLACE_STATUS(d, new_status); } } else { - REPLACE_STATUS(d, new_status); + REPLACE_STATUS(d, new_status); } } } -// The pin numbers are actual platform GPIO numbers -int rotary_setup(uint32_t channel, int phase_a, int phase_b, int press, task_handle_t tasknumber ) +void rotary_event_handled(rotary_driver_handle_t d) { - if (channel >= sizeof(data) / sizeof(data[0])) { - return -1; - } - - if (data[channel]) { - if (rotary_close(channel)) { - return -1; - } - } + d->task_queued = 0; +} - DATA *d = (DATA *) calloc(1, sizeof(DATA)); +// The pin numbers are actual platform GPIO numbers +rotary_driver_handle_t rotary_setup(int phase_a, int phase_b, int press, + task_handle_t tasknumber, void *arg) { + rotary_driver_handle_t d = (rotary_driver_handle_t )calloc(1, sizeof(*d)); if (!d) { - return -1; + return NULL; } - data[channel] = d; - d->tasknumber = tasknumber; + d->callback_arg = arg; set_gpio_mode(phase_a, GPIO_INTR_ANYEDGE); gpio_isr_handler_add(phase_a, rotary_interrupt, d); @@ -215,17 +200,11 @@ int rotary_setup(uint32_t channel, int phase_a, int phase_b, int press, task_han } d->press_pin = press; - return 0; + return d; } -bool rotary_has_queued_event(uint32_t channel) +bool rotary_has_queued_event(rotary_driver_handle_t d) { - if (channel >= sizeof(data) / sizeof(data[0])) { - return false; - } - - DATA *d = data[channel]; - if (!d) { return false; } @@ -234,16 +213,9 @@ bool rotary_has_queued_event(uint32_t channel) } // Get the oldest event in the queue and remove it (if possible) -bool rotary_getevent(uint32_t channel, rotary_event_t *resultp) -{ +bool rotary_getevent(rotary_driver_handle_t d, rotary_event_t *resultp) { rotary_event_t result = { 0 }; - if (channel >= sizeof(data) / sizeof(data[0])) { - return false; - } - - DATA *d = data[channel]; - if (!d) { return false; } @@ -263,22 +235,10 @@ bool rotary_getevent(uint32_t channel, rotary_event_t *resultp) return status; } -int rotary_getpos(uint32_t channel) -{ - if (channel >= sizeof(data) / sizeof(data[0])) { - return -1; - } - - DATA *d = data[channel]; - +int rotary_getpos(rotary_driver_handle_t d) { if (!d) { return -1; } return GET_LAST_STATUS(d).pos; } - -esp_err_t rotary_driver_init() -{ - return gpio_install_isr_service(ESP_INTR_FLAG_LOWMED); -} diff --git a/components/modules/rotary_driver.h b/components/modules/rotary_driver.h index 5aeab051c..5d805009f 100644 --- a/components/modules/rotary_driver.h +++ b/components/modules/rotary_driver.h @@ -6,23 +6,23 @@ #include -#define ROTARY_CHANNEL_COUNT 3 - typedef struct { uint32_t pos; uint32_t time_us; } rotary_event_t; -int rotary_setup(uint32_t channel, int phaseA, int phaseB, int press, task_handle_t tasknumber); +struct rotary_driver_handle *rotary_setup(int phaseA, + int phaseB, int press, + task_handle_t tasknumber, void *arg); -bool rotary_getevent(uint32_t channel, rotary_event_t *result); +bool rotary_getevent(struct rotary_driver_handle *handle, rotary_event_t *result); -bool rotary_has_queued_event(uint32_t channel); +bool rotary_has_queued_event(struct rotary_driver_handle *handle); -int rotary_getpos(uint32_t channel); +int rotary_getpos(struct rotary_driver_handle *handle); -int rotary_close(uint32_t channel); +int rotary_close(struct rotary_driver_handle *handle); -int rotary_driver_init(); +void rotary_event_handled(struct rotary_driver_handle *handle); #endif diff --git a/docs/modules/rotary.md b/docs/modules/rotary.md index f3c102f15..4ebd23797 100644 --- a/docs/modules/rotary.md +++ b/docs/modules/rotary.md @@ -32,10 +32,9 @@ Note that the pins are named somewhat eccentrically, and I suspect that it reall Initialize the nodemcu to talk to a rotary encoder switch. #### Syntax -`rotary.setup(channel, pina, pinb[, pinpress[, longpress_time_ms[, dblclick_time_ms]]])` +`switch = rotary.setup(pina, pinb[, pinpress[, longpress_time_ms[, dblclick_time_ms]]])` #### Parameters -- `channel` The rotary module supports three switches. The channel is either 0, 1 or 2. - `pina` This is a GPIO number (excluding 0) and connects to pin phase A on the rotary switch. - `pinb` This is a GPIO number (excluding 0) and connects to pin phase B on the rotary switch. - `pinpress` (optional) This is a GPIO number (excluding 0) and connects to the press switch. @@ -45,20 +44,18 @@ Initialize the nodemcu to talk to a rotary encoder switch. #### Returns Nothing. If the arguments are in error, or the operation cannot be completed, then an error is thrown. -For all API calls, if the channel number is out of range, then an error will be thrown. #### Example - rotary.setup(0, 5,6, 7) + switch = rotary.setup(5, 6, 7) -## rotary.on() +## switch:on() Sets a callback on specific events. #### Syntax -`rotary.on(channel, eventtype[, callback])` +`switch:on(eventtype[, callback])` #### Parameters -- `channel` The rotary module supports three switches. The channel is either 0, 1 or 2. - `eventtype` This defines the type of event being registered. This is the logical or of one or more of `PRESS`, `LONGPRESS`, `RELEASE`, `TURN`, `CLICK` or `DBLCLICK`. - `callback` This is a function that will be invoked when the specified event happens. @@ -73,8 +70,8 @@ in a 32-bit integer. Note that this wraps every hour or so. #### Example - rotary.on(0, rotary.ALL, function (type, pos, when) - print "Position=" .. pos .. " event type=" .. type .. " time=" .. when + switch:on(rotary.ALL, function (type, pos, when) + print("Position=" .. pos .. " event type=" .. type .. " time=" .. when) end) #### Notes @@ -94,14 +91,11 @@ where this is a short time gap between the middle `RELEASE` and `PRESS`. #### Errors If an invalid `eventtype` is supplied, then an error will be thrown. -## rotary.getpos() +## switch:getpos() Gets the current position and press status of the switch #### Syntax -`pos, press = rotary.getpos(channel)` - -#### Parameters -- `channel` The rotary module supports three switches. The channel is either 0, 1 or 2. +`pos, press = switch:getpos()` #### Returns - `pos` The current position of the switch. @@ -109,18 +103,15 @@ Gets the current position and press status of the switch #### Example - print rotary.getpos(0) + print(switch:getpos()) -## rotary.close() +## switch:close() Releases the resources associated with the rotary switch. #### Syntax -`rotary.close(channel)` - -#### Parameters -- `channel` The rotary module supports three switches. The channel is either 0, 1 or 2. +`switch:close()` #### Example - rotary.close(0) + switch:close() From 39c95722e3d70ff39b29e2e11c2cf2b155d1703b Mon Sep 17 00:00:00 2001 From: Philip Gladstone Date: Sat, 3 Feb 2024 10:45:32 -0500 Subject: [PATCH 3/8] Review comments. Also ensure that we have GPIO if we have ROTARY. --- components/modules/Kconfig | 1 + components/modules/rotary.c | 1 - docs/modules/rotary.md | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/components/modules/Kconfig b/components/modules/Kconfig index 4b5a8f5ce..11f47bcdf 100644 --- a/components/modules/Kconfig +++ b/components/modules/Kconfig @@ -228,6 +228,7 @@ menu "NodeMCU modules" config NODEMCU_CMODULE_ROTARY bool "Rotary switch input device" default "n" + select NODEMCU_CMODULE_GPIO help Includes the rotary module which allows easy access to one or more rotary switches. diff --git a/components/modules/rotary.c b/components/modules/rotary.c index 409ef7847..0b69aa7b4 100644 --- a/components/modules/rotary.c +++ b/components/modules/rotary.c @@ -231,7 +231,6 @@ static bool lrotary_dequeue_single(lua_State* L, DATA *d) bool something_pending = false; if (d) { - // This chnnel is open rotary_event_t result; if (rotary_getevent(d->handle, &result)) { diff --git a/docs/modules/rotary.md b/docs/modules/rotary.md index 4ebd23797..1f969e18b 100644 --- a/docs/modules/rotary.md +++ b/docs/modules/rotary.md @@ -80,8 +80,8 @@ Events will be delivered in order, but there may be missing TURN events. If ther queue of events, then PRESS and RELEASE events may also be missed. Multiple pending TURN events are typically dispatched as one TURN callback with the final position as its parameter. -Some switches have 4 steps per detent. This means that, in practice, the application -should divide the position by 4 and use that to determine the number of clicks. It is +Some switches have 4 (or 2) steps per detent. This means that, in practice, the application +should divide the position by 4 (or 2) and use that to determine the number of clicks. It is unlikely that a switch will ever reach 30 bits of rotation in either direction -- some are rated for under 50,000 revolutions. From a94fc1000e7d0ab095e91f0034e2cc9cbc9b1593 Mon Sep 17 00:00:00 2001 From: Philip Gladstone Date: Sat, 3 Feb 2024 11:14:54 -0500 Subject: [PATCH 4/8] Allow use of GPIO 0 --- components/modules/rotary.c | 6 +++--- docs/modules/rotary.md | 10 +++++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/components/modules/rotary.c b/components/modules/rotary.c index 0b69aa7b4..94a9f72fc 100644 --- a/components/modules/rotary.c +++ b/components/modules/rotary.c @@ -145,13 +145,13 @@ static int lrotary_setup( lua_State* L ) d->longpress_delay_us = LONGPRESS_DELAY_US; int phase_a = luaL_checkinteger(L, 1); - luaL_argcheck(L, platform_gpio_exists(phase_a) && phase_a > 0, 1, "Invalid pin"); + luaL_argcheck(L, platform_gpio_exists(phase_a), 1, "Invalid pin"); int phase_b = luaL_checkinteger(L, 2); - luaL_argcheck(L, platform_gpio_exists(phase_b) && phase_b > 0, 2, "Invalid pin"); + luaL_argcheck(L, platform_gpio_exists(phase_b), 2, "Invalid pin"); int press; if (nargs >= 3) { press = luaL_checkinteger(L, 3); - luaL_argcheck(L, platform_gpio_exists(press) && press > 0, 3, "Invalid pin"); + luaL_argcheck(L, platform_gpio_exists(press), 3, "Invalid pin"); } else { press = -1; } diff --git a/docs/modules/rotary.md b/docs/modules/rotary.md index 1f969e18b..850bb5c41 100644 --- a/docs/modules/rotary.md +++ b/docs/modules/rotary.md @@ -35,9 +35,9 @@ Initialize the nodemcu to talk to a rotary encoder switch. `switch = rotary.setup(pina, pinb[, pinpress[, longpress_time_ms[, dblclick_time_ms]]])` #### Parameters -- `pina` This is a GPIO number (excluding 0) and connects to pin phase A on the rotary switch. -- `pinb` This is a GPIO number (excluding 0) and connects to pin phase B on the rotary switch. -- `pinpress` (optional) This is a GPIO number (excluding 0) and connects to the press switch. +- `pina` This is a GPIO number and connects to pin phase A on the rotary switch. +- `pinb` This is a GPIO number and connects to pin phase B on the rotary switch. +- `pinpress` (optional) This is a GPIO number and connects to the press switch. - `longpress_time_ms` (optional) The number of milliseconds (default 500) of press to be considered a long press. - `dblclick_time_ms` (optional) The number of milliseconds (default 500) between a release and a press for the next release to be considered a double click. @@ -49,6 +49,10 @@ Nothing. If the arguments are in error, or the operation cannot be completed, th switch = rotary.setup(5, 6, 7) +#### Notes + +This module uses pullups on the GPIO pins to detect the key presses. However, not all GPIO pins support pullups and so, if one of those pins is used, then there needs to be a real external resistor pullup. + ## switch:on() Sets a callback on specific events. From a6d3e4b8228e1f09f0c0a58a4ba2b4218047135b Mon Sep 17 00:00:00 2001 From: Philip Gladstone Date: Sat, 3 Feb 2024 12:20:26 -0500 Subject: [PATCH 5/8] Another bad piece of documentation --- docs/modules/rotary.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/rotary.md b/docs/modules/rotary.md index 850bb5c41..f591c53f7 100644 --- a/docs/modules/rotary.md +++ b/docs/modules/rotary.md @@ -42,7 +42,7 @@ Initialize the nodemcu to talk to a rotary encoder switch. - `dblclick_time_ms` (optional) The number of milliseconds (default 500) between a release and a press for the next release to be considered a double click. #### Returns -Nothing. If the arguments are in error, or the operation cannot be completed, then an error is thrown. +A switch object that can be used for the operations below. #### Example From cbcedbc6abc590d38b67b48750f76b7141b23397 Mon Sep 17 00:00:00 2001 From: Philip Gladstone Date: Sun, 4 Feb 2024 12:47:10 -0500 Subject: [PATCH 6/8] Fix docs and also free the timer --- components/modules/rotary.c | 6 ++++++ docs/modules/rotary.md | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/components/modules/rotary.c b/components/modules/rotary.c index 94a9f72fc..8292b549b 100644 --- a/components/modules/rotary.c +++ b/components/modules/rotary.c @@ -187,6 +187,12 @@ static int lrotary_close( lua_State* L ) d->handle = NULL; } + + if (d->timer_handle) { + esp_timer_stop(d->timer_handle); + esp_timer_delete(d->timer_handle); + d->timer_handle = NULL; + } return 0; } diff --git a/docs/modules/rotary.md b/docs/modules/rotary.md index f591c53f7..194c7fd24 100644 --- a/docs/modules/rotary.md +++ b/docs/modules/rotary.md @@ -60,7 +60,7 @@ Sets a callback on specific events. `switch:on(eventtype[, callback])` #### Parameters -- `eventtype` This defines the type of event being registered. This is the logical or of one or more of `PRESS`, `LONGPRESS`, `RELEASE`, `TURN`, `CLICK` or `DBLCLICK`. +- `eventtype` This defines the type of event being registered. This is the logical or of one or more of `PRESS`, `LONGPRESS`, `RELEASE`, `TURN`, `CLICK` or `DBLCLICK`. `ALL` can be used as shorthand for all event types. - `callback` This is a function that will be invoked when the specified event happens. If the callback is None or omitted, then the registration is cancelled. From f0170898b7fa7e031244c0e56595ce7f90990463 Mon Sep 17 00:00:00 2001 From: Philip Gladstone Date: Tue, 23 Apr 2024 22:04:15 -0400 Subject: [PATCH 7/8] Now adds a self reference to prevent GC until after close has been called and any queued messages have been flushed. --- components/modules/rotary.c | 33 ++++++++++++++++++++++++++++++ components/modules/rotary_driver.c | 9 ++++++++ components/modules/rotary_driver.h | 2 ++ 3 files changed, 44 insertions(+) diff --git a/components/modules/rotary.c b/components/modules/rotary.c index 8292b549b..7686557b0 100644 --- a/components/modules/rotary.c +++ b/components/modules/rotary.c @@ -44,6 +44,7 @@ typedef struct { uint32_t last_event_time; int callback[CALLBACK_COUNT]; esp_timer_handle_t timer_handle; + int self_ref; } DATA; static task_handle_t tasknumber; @@ -141,6 +142,8 @@ static int lrotary_setup( lua_State* L ) d->callback[i] = LUA_NOREF; } + d->self_ref = LUA_NOREF; + d->click_delay_us = CLICK_DELAY_US; d->longpress_delay_us = LONGPRESS_DELAY_US; @@ -173,6 +176,27 @@ static int lrotary_setup( lua_State* L ) return 1; } +static void update_self_ref(lua_State *L, DATA *d, int argnum) { + bool have_callback = false; + for (int i = 0; i < CALLBACK_COUNT; i++) { + if (d->callback[i] != LUA_NOREF) { + have_callback = true; + break; + } + } + if (have_callback) { + if (d->self_ref == LUA_NOREF && argnum > 0) { + lua_pushvalue(L, argnum); + d->self_ref = luaL_ref(L, LUA_REGISTRYINDEX); + } + } else { + if (d->self_ref != LUA_NOREF) { + luaL_unref(L, LUA_REGISTRYINDEX, d->self_ref); + d->self_ref = LUA_NOREF; + } + } +} + // Lua: close( ) static int lrotary_close( lua_State* L ) { @@ -181,6 +205,10 @@ static int lrotary_close( lua_State* L ) if (d->handle) { callback_free(L, d, ROTARY_ALL); + if (!rotary_has_queued_task(d->handle)) { + update_self_ref(L, d, 1); + } + if (rotary_close( d->handle )) { return luaL_error( L, "Unable to close switch." ); } @@ -193,6 +221,7 @@ static int lrotary_close( lua_State* L ) esp_timer_delete(d->timer_handle); d->timer_handle = NULL; } + return 0; } @@ -211,6 +240,8 @@ static int lrotary_on( lua_State* L ) callback_free(L, d, mask); } + update_self_ref(L, d, 1); + return 0; } @@ -347,6 +378,8 @@ static void lrotary_task(task_param_t param, task_prio_t prio) if (need_to_post) { // If there is pending stuff, queue another task task_post_medium(tasknumber, param); + } else if (d) { + update_self_ref(L, d, -1); } } diff --git a/components/modules/rotary_driver.c b/components/modules/rotary_driver.c index 934175cfb..702530876 100644 --- a/components/modules/rotary_driver.c +++ b/components/modules/rotary_driver.c @@ -175,6 +175,15 @@ void rotary_event_handled(rotary_driver_handle_t d) d->task_queued = 0; } +bool rotary_has_queued_task(rotary_driver_handle_t d) { + if (!d) { + return false; + } + + return d->task_queued; +} + + // The pin numbers are actual platform GPIO numbers rotary_driver_handle_t rotary_setup(int phase_a, int phase_b, int press, task_handle_t tasknumber, void *arg) { diff --git a/components/modules/rotary_driver.h b/components/modules/rotary_driver.h index 5d805009f..1b04f8b79 100644 --- a/components/modules/rotary_driver.h +++ b/components/modules/rotary_driver.h @@ -25,4 +25,6 @@ int rotary_close(struct rotary_driver_handle *handle); void rotary_event_handled(struct rotary_driver_handle *handle); +bool rotary_has_queued_task(struct rotary_driver_handle *handle); + #endif From 34850191a52d34ccc7a3167ba38f8ce52a55f755 Mon Sep 17 00:00:00 2001 From: Philip Gladstone Date: Thu, 25 Apr 2024 23:34:39 +0000 Subject: [PATCH 8/8] Simplified the code a bit --- components/modules/rotary.c | 4 ++-- components/modules/rotary_driver.c | 9 --------- components/modules/rotary_driver.h | 2 -- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/components/modules/rotary.c b/components/modules/rotary.c index 7686557b0..7ca2e4f9c 100644 --- a/components/modules/rotary.c +++ b/components/modules/rotary.c @@ -190,7 +190,7 @@ static void update_self_ref(lua_State *L, DATA *d, int argnum) { d->self_ref = luaL_ref(L, LUA_REGISTRYINDEX); } } else { - if (d->self_ref != LUA_NOREF) { + if (d->self_ref != LUA_NOREF && !rotary_has_queued_event(d->handle)) { luaL_unref(L, LUA_REGISTRYINDEX, d->self_ref); d->self_ref = LUA_NOREF; } @@ -205,7 +205,7 @@ static int lrotary_close( lua_State* L ) if (d->handle) { callback_free(L, d, ROTARY_ALL); - if (!rotary_has_queued_task(d->handle)) { + if (!rotary_has_queued_event(d->handle)) { update_self_ref(L, d, 1); } diff --git a/components/modules/rotary_driver.c b/components/modules/rotary_driver.c index 702530876..934175cfb 100644 --- a/components/modules/rotary_driver.c +++ b/components/modules/rotary_driver.c @@ -175,15 +175,6 @@ void rotary_event_handled(rotary_driver_handle_t d) d->task_queued = 0; } -bool rotary_has_queued_task(rotary_driver_handle_t d) { - if (!d) { - return false; - } - - return d->task_queued; -} - - // The pin numbers are actual platform GPIO numbers rotary_driver_handle_t rotary_setup(int phase_a, int phase_b, int press, task_handle_t tasknumber, void *arg) { diff --git a/components/modules/rotary_driver.h b/components/modules/rotary_driver.h index 1b04f8b79..5d805009f 100644 --- a/components/modules/rotary_driver.h +++ b/components/modules/rotary_driver.h @@ -25,6 +25,4 @@ int rotary_close(struct rotary_driver_handle *handle); void rotary_event_handled(struct rotary_driver_handle *handle); -bool rotary_has_queued_task(struct rotary_driver_handle *handle); - #endif