Ported rotary driver. Compiles but not tested.

This commit is contained in:
Philip Gladstone 2024-02-02 19:54:59 -05:00
parent 3cc1d48b83
commit 8fbbcd6f3d
6 changed files with 859 additions and 0 deletions

View File

@ -23,6 +23,8 @@ set(module_srcs
"otaupgrade.c" "otaupgrade.c"
"ow.c" "ow.c"
"pipe.c" "pipe.c"
"rotary_driver.c"
"rotary.c"
"rtcmem.c" "rtcmem.c"
"qrcodegen.c" "qrcodegen.c"
"sigma_delta.c" "sigma_delta.c"

View File

@ -225,6 +225,12 @@ menu "NodeMCU modules"
Includes the rmt module to use the ESP32's built-in Includes the rmt module to use the ESP32's built-in
remote control hardware. 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 config NODEMCU_CMODULE_RTCMEM
bool "Access to a limited amount of battery backed memory (rtcmem)" bool "Access to a limited amount of battery backed memory (rtcmem)"
default "n" default "n"

413
components/modules/rotary.c Normal file
View File

@ -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 <stdint.h>
#include <string.h>
#include <stdlib.h>
#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);

View File

@ -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 <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#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);
}

View File

@ -0,0 +1,28 @@
/*
* Definitions to access the Rotary driver
*/
#ifndef __ROTARY_H__
#define __ROTARY_H__
#include <stdint.h>
#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

126
docs/modules/rotary.md Normal file
View File

@ -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)