diff --git a/components/modules/CMakeLists.txt b/components/modules/CMakeLists.txt index cbb4186e3..dd755908e 100644 --- a/components/modules/CMakeLists.txt +++ b/components/modules/CMakeLists.txt @@ -30,6 +30,8 @@ set(module_srcs "otaupgrade.c" "ow.c" "pipe.c" + "rotary_driver.c" + "rotary.c" "rmt.c" "rtcmem.c" "qrcodegen.c" diff --git a/components/modules/Kconfig b/components/modules/Kconfig index 6554efd59..ce13f848d 100644 --- a/components/modules/Kconfig +++ b/components/modules/Kconfig @@ -232,6 +232,13 @@ 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" + select NODEMCU_CMODULE_GPIO + 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..7ca2e4f9c --- /dev/null +++ b/components/modules/rotary.c @@ -0,0 +1,415 @@ +/* + * 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; + struct rotary_driver_handle *handle; + int click_delay_us; + int longpress_delay_us; + uint32_t last_event_time; + int callback[CALLBACK_COUNT]; + esp_timer_handle_t timer_handle; + int self_ref; +} DATA; + +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, DATA *d, int mask) +{ + 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, DATA *d, int mask, int arg_number) +{ + 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); + } +} + +// Lua: setup(phase_a, phase_b [, press]) +static int lrotary_setup( lua_State* L ) +{ + int nargs = lua_gettop(L); + + DATA *d = (DATA *)lua_newuserdata(L, sizeof(DATA)); + if (!d) return luaL_error(L, "not enough memory"); + memset(d, 0, sizeof(*d)); + luaL_getmetatable(L, "rotary.switch"); + lua_setmetatable(L, -2); + + 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->self_ref = LUA_NOREF; + + d->click_delay_us = CLICK_DELAY_US; + d->longpress_delay_us = LONGPRESS_DELAY_US; + + int phase_a = luaL_checkinteger(L, 1); + 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), 2, "Invalid pin"); + int press; + if (nargs >= 3) { + press = luaL_checkinteger(L, 3); + luaL_argcheck(L, platform_gpio_exists(press), 3, "Invalid pin"); + } else { + press = -1; + } + + if (nargs >= 4) { + d->longpress_delay_us = 1000 * luaL_checkinteger(L, 4); + luaL_argcheck(L, d->longpress_delay_us > 0, 4, "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"); + } + + d->handle = rotary_setup(phase_a, phase_b, press, tasknumber, d); + if (!d->handle) { + return luaL_error(L, "Unable to setup rotary switch."); + } + 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 && !rotary_has_queued_event(d->handle)) { + luaL_unref(L, LUA_REGISTRYINDEX, d->self_ref); + d->self_ref = LUA_NOREF; + } + } +} + +// Lua: close( ) +static int lrotary_close( lua_State* L ) +{ + DATA *d = (DATA *)luaL_checkudata(L, 1, "rotary.switch"); + + if (d->handle) { + callback_free(L, d, ROTARY_ALL); + + if (!rotary_has_queued_event(d->handle)) { + update_self_ref(L, d, 1); + } + + if (rotary_close( d->handle )) { + return luaL_error( L, "Unable to close switch." ); + } + + 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; +} + +// Lua: on( mask[, cb] ) +static int lrotary_on( lua_State* L ) +{ + DATA *d = (DATA *)luaL_checkudata(L, 1, "rotary.switch"); + + int mask = luaL_checkinteger(L, 2); + + if (lua_gettop(L) >= 3) { + if (callback_set(L, d, mask, 3)) { + return luaL_error( L, "Unable to set callback." ); + } + } else { + callback_free(L, d, mask); + } + + update_self_ref(L, d, 1); + + return 0; +} + +// Lua: getpos( ) -> pos, PRESS/RELEASE +static int lrotary_getpos( lua_State* L ) +{ + 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_pushboolean(L, (pos & 0x80000000)); + + 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) { + rotary_event_t result; + + if (rotary_getevent(d->handle, &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; + } + + rotary_event_handled(d->handle); + something_pending = rotary_has_queued_event(d->handle); + } + + 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) prio; + + bool need_to_post = false; + lua_State *L = lua_getstate(); + + 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, param); + } else if (d) { + update_self_ref(L, d, -1); + } +} + + +// Module function map +LROT_BEGIN(rotary, NULL, 0) + LROT_FUNCENTRY( setup, lrotary_setup ) + 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) + +// 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 new file mode 100644 index 000000000..934175cfb --- /dev/null +++ b/components/modules/rotary_driver.c @@ -0,0 +1,244 @@ +/* + * 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 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]; + void *callback_arg; +} *rotary_driver_handle_t; + +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, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .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(rotary_driver_handle_t d) +{ + if (!d) { + return 0; + } + + 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 + rotary_driver_handle_t d = (rotary_driver_handle_t)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 (!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); + } + } +} + +void rotary_event_handled(rotary_driver_handle_t d) +{ + d->task_queued = 0; +} + +// 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 NULL; + } + + d->tasknumber = tasknumber; + d->callback_arg = arg; + + 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 d; +} + +bool rotary_has_queued_event(rotary_driver_handle_t d) +{ + if (!d) { + return false; + } + + return HAS_QUEUED_DATA(d); +} + +// Get the oldest event in the queue and remove it (if possible) +bool rotary_getevent(rotary_driver_handle_t d, rotary_event_t *resultp) { + rotary_event_t result = { 0 }; + + 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(rotary_driver_handle_t d) { + if (!d) { + return -1; + } + + return GET_LAST_STATUS(d).pos; +} diff --git a/components/modules/rotary_driver.h b/components/modules/rotary_driver.h new file mode 100644 index 000000000..5d805009f --- /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 + +typedef struct { + uint32_t pos; + uint32_t time_us; +} rotary_event_t; + +struct rotary_driver_handle *rotary_setup(int phaseA, + int phaseB, int press, + task_handle_t tasknumber, void *arg); + +bool rotary_getevent(struct rotary_driver_handle *handle, rotary_event_t *result); + +bool rotary_has_queued_event(struct rotary_driver_handle *handle); + +int rotary_getpos(struct rotary_driver_handle *handle); + +int rotary_close(struct rotary_driver_handle *handle); + +void rotary_event_handled(struct rotary_driver_handle *handle); + +#endif diff --git a/docs/modules/rotary.md b/docs/modules/rotary.md new file mode 100644 index 000000000..194c7fd24 --- /dev/null +++ b/docs/modules/rotary.md @@ -0,0 +1,121 @@ +# 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 +`switch = rotary.setup(pina, pinb[, pinpress[, longpress_time_ms[, dblclick_time_ms]]])` + +#### Parameters +- `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. + +#### Returns +A switch object that can be used for the operations below. + + +#### Example + + 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. + +#### Syntax +`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`. `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. + +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 + + switch:on(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 (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. + +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. + +## switch:getpos() +Gets the current position and press status of the switch + +#### Syntax +`pos, press = switch:getpos()` + +#### Returns +- `pos` The current position of the switch. +- `press` A boolean indicating if the switch is currently pressed. + +#### Example + + print(switch:getpos()) + +## switch:close() +Releases the resources associated with the rotary switch. + +#### Syntax +`switch:close()` + +#### Example + + switch:close() +