Forráskód Böngészése

Merge pull request #1067 from pjsg/rotarymod

Module to handle rotary decoders
Terry Ellison 8 éve
szülő
commit
00cc4b42dc

+ 320 - 0
app/driver/rotary.c

@@ -0,0 +1,320 @@
+/*
+ * 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 "c_types.h"
+#include "../libc/c_stdlib.h"
+#include "../libc/c_stdio.h"
+#include "driver/rotary.h"
+#include "user_interface.h"
+#include "task/task.h"
+#include "ets_sys.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), system_get_time() })
+#define QUEUE_STATUS(d, x)      (d->queue[(d->write_offset++) & (QUEUE_SIZE - 1)] = (rotary_event_t) { (x), system_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 pin_mask;
+  uint32_t phase_a;
+  uint32_t phase_b;
+  uint32_t press;
+  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_bits(void);
+
+static void rotary_clear_pin(int pin) 
+{
+  if (pin >= 0) {
+    gpio_pin_intr_state_set(GPIO_ID_PIN(pin_num[pin]), GPIO_PIN_INTR_DISABLE);
+    platform_gpio_mode(pin, PLATFORM_GPIO_INPUT, PLATFORM_GPIO_PULLUP);
+  }
+}
+
+// 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);
+
+  c_free(d);
+
+  set_gpio_bits();
+
+  return 0;
+}
+
+static uint32_t  ICACHE_RAM_ATTR rotary_interrupt(uint32_t ret_gpio_status) 
+{
+  // This function really is running at interrupt level with everything
+  // else masked off. It should take as little time as necessary.
+  //
+  //
+
+  // This gets the set of pins which have changed status
+  uint32 gpio_status = GPIO_REG_READ(GPIO_STATUS_ADDRESS);
+
+  int i;
+  for (i = 0; i < sizeof(data) / sizeof(data[0]); i++) {
+    DATA *d = data[i];
+    if (!d || (gpio_status & d->pin_mask) == 0) {
+      continue;
+    }
+
+    GPIO_REG_WRITE(GPIO_STATUS_W1TC_ADDRESS, gpio_status & d->pin_mask);
+
+    uint32_t bits = GPIO_REG_READ(GPIO_IN_ADDRESS);
+
+    uint32_t last_status = GET_LAST_STATUS(d).pos;
+
+    uint32_t now = system_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 = (bits & d->press) ? 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 (bits & d->phase_b) {
+      micropos = 3;
+    }
+    if (bits & d->phase_a) {
+      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, (os_param_t) &task_queued)) {
+	      task_queued = 1;
+	    }
+	  }
+	} else {
+	  REPLACE_STATUS(d, new_status);
+	}
+      } else {
+	REPLACE_STATUS(d, new_status);
+      }
+    }
+    ret_gpio_status &= ~(d->pin_mask);
+  }
+
+  return ret_gpio_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 *) c_zalloc(sizeof(DATA));
+  if (!d) {
+    return -1;
+  }
+
+  data[channel] = d;
+  int i;
+
+  d->tasknumber = tasknumber;
+
+  d->phase_a = 1 << pin_num[phase_a];
+  platform_gpio_mode(phase_a, PLATFORM_GPIO_INT, PLATFORM_GPIO_PULLUP);
+  gpio_pin_intr_state_set(GPIO_ID_PIN(pin_num[phase_a]), GPIO_PIN_INTR_ANYEDGE);
+  d->phase_a_pin = phase_a;
+
+  d->phase_b = 1 << pin_num[phase_b];
+  platform_gpio_mode(phase_b, PLATFORM_GPIO_INT, PLATFORM_GPIO_PULLUP);
+  gpio_pin_intr_state_set(GPIO_ID_PIN(pin_num[phase_b]), GPIO_PIN_INTR_ANYEDGE);
+  d->phase_b_pin = phase_b;
+
+  if (press >= 0) {
+    d->press = 1 << pin_num[press];
+    platform_gpio_mode(press, PLATFORM_GPIO_INT, PLATFORM_GPIO_PULLUP);
+    gpio_pin_intr_state_set(GPIO_ID_PIN(pin_num[press]), GPIO_PIN_INTR_ANYEDGE);
+  }
+  d->press_pin = press;
+
+  d->pin_mask = d->phase_a | d->phase_b | d->press;
+
+  set_gpio_bits();
+
+  return 0;
+}
+
+static void set_gpio_bits()
+{
+  uint32_t bits = 0;
+  for (int i = 0; i < ROTARY_CHANNEL_COUNT; i++) {
+    DATA *d = data[i];
+
+    if (d) {
+      bits = bits | d->pin_mask;
+    }
+  }
+
+  platform_gpio_register_intr_hook(bits, rotary_interrupt);
+}
+
+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;
+  }
+
+  ETS_GPIO_INTR_DISABLE();
+
+  bool status = FALSE;
+
+  if (HAS_QUEUED_DATA(d)) {
+    result = GET_READ_STATUS(d);
+    d->read_offset++;
+    status = TRUE;
+  } else {
+    result = GET_LAST_STATUS(d);
+  }
+
+  ETS_GPIO_INTR_ENABLE();
+
+  *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;
+}

+ 26 - 0
app/include/driver/rotary.h

@@ -0,0 +1,26 @@
+/*
+ * Definitions to access the Rotary driver
+ */
+#ifndef __ROTARY_H__
+#define __ROTARY_H__
+
+#include "c_types.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);
+
+#endif

+ 1 - 0
app/include/user_config.h

@@ -42,6 +42,7 @@
 #endif	/* NODE_ERROR */
 
 #define GPIO_INTERRUPT_ENABLE
+#define GPIO_INTERRUPT_HOOK_ENABLE
 // #define GPIO_SAFE_NO_INTR_ENABLE
 
 #define ICACHE_STORE_TYPEDEF_ATTR __attribute__((aligned(4),packed))

+ 1 - 0
app/include/user_modules.h

@@ -38,6 +38,7 @@
 //#define LUA_USE_MODULES_PERF
 #define LUA_USE_MODULES_PWM
 #define LUA_USE_MODULES_RC
+//#define LUA_USE_MODULES_ROTARY
 #define LUA_USE_MODULES_RTCFIFO
 #define LUA_USE_MODULES_RTCMEM
 #define LUA_USE_MODULES_RTCTIME

+ 405 - 0
app/modules/rotary.c

@@ -0,0 +1,405 @@
+/*
+ * 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 "c_types.h"
+#include "user_interface.h"
+#include "driver/rotary.h"
+#include "../libc/c_stdlib.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
+
+#ifdef LUA_USE_MODULES_ROTARY
+#if !defined(GPIO_INTERRUPT_ENABLE) || !defined(GPIO_INTERRUPT_HOOK_ENABLE)
+#error Must have GPIO_INTERRUPT and GPIO_INTERRUPT_HOOK if using ROTARY module
+#endif
+#endif
+
+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];
+  ETSTimer timer;
+} 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_type(L, arg_number) == LUA_TFUNCTION || lua_type(L, arg_number) == LUA_TLIGHTFUNCTION) {
+    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);
+
+    lua_call(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 *) c_zalloc(sizeof(DATA));
+    if (!data[id]) {
+      return -1;
+    } 
+  }
+
+  DATA *d = data[id];
+  memset(d, 0, sizeof(*d));
+
+  os_timer_setfn(&d->timer, lrotary_timer_done, (void *) d);
+  
+  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;
+    c_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_pushnumber(L, (pos << 1) >> 1);
+  lua_pushnumber(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, system_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, system_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) {
+    os_timer_disarm(&d->timer);
+    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;
+    os_timer_arm(&d->timer, timeout + 1, 0);
+  }
+}
+
+static void lrotary_task(os_param_t param, uint8_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);
+  return 0;
+}
+
+// Module function map
+static const LUA_REG_TYPE rotary_map[] = {
+  { LSTRKEY( "setup" ),    LFUNCVAL( lrotary_setup ) },
+  { LSTRKEY( "close" ),    LFUNCVAL( lrotary_close ) },
+  { LSTRKEY( "on" ),       LFUNCVAL( lrotary_on    ) },
+  { LSTRKEY( "getpos" ),   LFUNCVAL( lrotary_getpos) },
+  { LSTRKEY( "TURN" ),     LNUMVAL( MASK(TURN)    ) },
+  { LSTRKEY( "PRESS" ),    LNUMVAL( MASK(PRESS)   ) },
+  { LSTRKEY( "RELEASE" ),  LNUMVAL( MASK(RELEASE) ) },
+  { LSTRKEY( "LONGPRESS" ),LNUMVAL( MASK(LONGPRESS) ) },
+  { LSTRKEY( "CLICK" ),    LNUMVAL( MASK(CLICK)   ) },
+  { LSTRKEY( "DBLCLICK" ), LNUMVAL( MASK(DBLCLICK)) },
+  { LSTRKEY( "ALL" ),      LNUMVAL( ROTARY_ALL     ) },
+
+  { LNILKEY, LNILVAL }
+};
+
+NODEMCU_MODULE(ROTARY, "rotary", rotary_map, rotary_open);

+ 112 - 3
app/platform/platform.c

@@ -14,6 +14,24 @@
 #include "driver/uart.h"
 #include "driver/sigma_delta.h"
 
+#ifdef GPIO_INTERRUPT_ENABLE
+static task_handle_t gpio_task_handle;
+
+#ifdef GPIO_INTERRUPT_HOOK_ENABLE
+struct gpio_hook_entry {
+  platform_hook_function func;
+  uint32_t               bits;
+};
+struct gpio_hook {
+  struct gpio_hook_entry *entry;
+  uint32_t                all_bits;
+  uint32_t                count;
+};
+
+static struct gpio_hook platform_gpio_hook;
+#endif
+#endif
+
 static void pwms_init();
 
 int platform_init()
@@ -155,16 +173,23 @@ int platform_gpio_read( unsigned pin )
 }
 
 #ifdef GPIO_INTERRUPT_ENABLE
-static task_handle_t gpio_task_handle;
-
 static void ICACHE_RAM_ATTR platform_gpio_intr_dispatcher (void *dummy){
   uint32 j=0;
   uint32 gpio_status = GPIO_REG_READ(GPIO_STATUS_ADDRESS);
   UNUSED(dummy);
+
+#ifdef GPIO_INTERRUPT_HOOK_ENABLE
+  if (gpio_status & platform_gpio_hook.all_bits) {
+    for (j = 0; j < platform_gpio_hook.count; j++) {
+       if (gpio_status & platform_gpio_hook.entry[j].bits)
+         gpio_status = (platform_gpio_hook.entry[j].func)(gpio_status);
+    }
+  }
+#endif
   /*
    * gpio_status is a bit map where bit 0 is set if unmapped gpio pin 0 (pin3) has
    * triggered the ISR. bit 1 if unmapped gpio pin 1 (pin10=U0TXD), etc.  Since this
-   * in the ISR, it makes sense to optimize this by doing a fast scan of the status
+   * is the ISR, it makes sense to optimize this by doing a fast scan of the status
    * and reverse mapping any set bits.
    */
    for (j = 0; gpio_status>0; j++, gpio_status >>= 1) {
@@ -189,6 +214,90 @@ void platform_gpio_init( task_handle_t gpio_task )
 
   ETS_GPIO_INTR_ATTACH(platform_gpio_intr_dispatcher, NULL);
 }
+
+#ifdef GPIO_INTERRUPT_HOOK_ENABLE
+/*
+ * Register an ISR hook to be called from the GPIO ISR for a given GPIO bitmask.
+ * This routine is only called a few times so has been optimised for size and
+ * the unregister is a special case when the bits are 0.
+ *
+ * Each hook function can only be registered once. If it is re-registered
+ * then the hooked bits are just updated to the new value.
+ */
+int platform_gpio_register_intr_hook(uint32_t bits, platform_hook_function hook)
+{
+  struct gpio_hook nh, oh = platform_gpio_hook;
+  int i, j;
+
+  if (!hook) {
+    // Cannot register or unregister null hook
+    return 0;
+  }
+
+  int delete_slot = -1;
+
+  // If hook already registered, just update the bits
+  for (i=0; i<oh.count; i++) {
+    if (hook == oh.entry[i].func) {
+      if (!bits) {
+	// Unregister if move to zero bits
+	delete_slot = i;
+	break;
+      }
+      if (bits & (oh.all_bits & ~oh.entry[i].bits)) {
+	// Attempt to hook an already hooked bit
+	return 0;
+      }
+      // Update the hooked bits (in the right order)
+      uint32_t old_bits = oh.entry[i].bits;
+      *(volatile uint32_t *) &oh.entry[i].bits = bits;
+      *(volatile uint32_t *) &oh.all_bits = (oh.all_bits & ~old_bits) | bits;
+      return 1;
+    }
+  }
+
+  // This must be the register new hook / delete old hook
+  
+  if (delete_slot < 0) {
+    if (bits & oh.all_bits) {
+      return 0;   // Attempt to hook already hooked bits
+    }
+    nh.count = oh.count + 1;    // register a new hook
+  } else {
+    nh.count = oh.count - 1;    // unregister an old hook
+  }
+
+  // These return NULL if the count = 0 so only error check if > 0)
+  nh.entry = c_malloc( nh.count * sizeof(*(nh.entry)) );
+  if (nh.count && !(nh.entry)) {
+    return 0;  // Allocation failure
+  }
+
+  for (i=0, j=0; i<oh.count; i++) {
+    // Don't copy if this is the entry to delete
+    if (i != delete_slot) {
+      nh.entry[j++]   = oh.entry[i];
+    }
+  }
+
+  if (delete_slot < 0) { // for a register add the hook to the tail and set the all bits
+    nh.entry[j].bits  = bits;
+    nh.entry[j].func  = hook;
+    nh.all_bits = oh.all_bits | bits;
+  } else {    // for an unregister clear the matching all bits
+    nh.all_bits = oh.all_bits & (~oh.entry[delete_slot].bits);
+  }
+
+  ETS_GPIO_INTR_DISABLE();
+  // This is a structure copy, so interrupts need to be disabled
+  platform_gpio_hook = nh;
+  ETS_GPIO_INTR_ENABLE();
+
+  c_free(oh.entry);
+  return 1;
+}
+#endif // GPIO_INTERRUPT_HOOK_ENABLE
+
 /*
  * Initialise GPIO interrupt mode. Optionally in RAM because interrupts are dsabled
  */

+ 9 - 1
app/platform/platform.h

@@ -37,12 +37,20 @@ uint8_t platform_key_led( uint8_t level);
 #define PLATFORM_GPIO_HIGH 1
 #define PLATFORM_GPIO_LOW 0
 
+typedef uint32_t (* platform_hook_function)(uint32_t bitmask);
+
 static inline int platform_gpio_exists( unsigned pin ) { return pin < NUM_GPIO; }
 int platform_gpio_mode( unsigned pin, unsigned mode, unsigned pull );
 int platform_gpio_write( unsigned pin, unsigned level );
 int platform_gpio_read( unsigned pin );
-void platform_gpio_init( task_handle_t gpio_task );
+
+// Note that these functions will not be compiled in unless GPIO_INTERRUPT_ENABLE and
+// GPIO_INTERRUPT_HOOK_ENABLE are defined.
+int platform_gpio_register_intr_hook(uint32_t gpio_bits, platform_hook_function hook);
+#define platform_gpio_unregister_intr_hook(hook) \
+  platform_gpio_register_intr_hook(0, hook);
 void platform_gpio_intr_init( unsigned pin, GPIO_INT_TYPE type );
+void platform_gpio_init( task_handle_t gpio_task );
 // *****************************************************************************
 // Timer subsection
 

+ 129 - 0
docs/en/modules/rotary.md

@@ -0,0 +1,129 @@
+# rotary Module
+
+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 / anticlockwise. 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 
+current position of the rotary switch, the second is the eventtype, 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 (pos, type, 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, queue = 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.
+- `queue` The number of undelivered callbacks (normally 0).
+
+#### 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)
+

+ 1 - 0
mkdocs.yml

@@ -57,6 +57,7 @@ pages:
         - 'ow (1-Wire)': 'en/modules/ow.md'
         - 'perf': 'en/modules/perf.md'
         - 'pwm' : 'en/modules/pwm.md'
+        - 'rotary' : 'en/modules/rotary.md'
         - 'rtcmem': 'en/modules/rtcmem.md'
         - 'rtctime': 'en/modules/rtctime.md'
         - 'rtcfifo': 'en/modules/rtcfifo.md'