diff --git a/components/modules/CMakeLists.txt b/components/modules/CMakeLists.txt index c5e77a2a..81d68f3c 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 7741e37b..4b5a8f5c 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 00000000..9ae23497 --- /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 00000000..e73257ca --- /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 00000000..5aeab051 --- /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 00000000..f3c102f1 --- /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) +