Ported rotary driver. Compiles but not tested.
This commit is contained in:
parent
3cc1d48b83
commit
8fbbcd6f3d
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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);
|
|
@ -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);
|
||||||
|
}
|
|
@ -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
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue