Partially created module.

This commit is contained in:
Philip Gladstone 2024-02-03 12:26:04 -05:00
parent 6798f027f3
commit 81f0763b80
4 changed files with 661 additions and 0 deletions

View File

@ -17,6 +17,7 @@ set(module_srcs
"i2c_hw_master.c"
"i2c_hw_slave.c"
"ledc.c"
"matrix.c"
"mqtt.c"
"net.c"
"node.c"

View File

@ -164,6 +164,13 @@ menu "NodeMCU modules"
help
Includes the LEDC module.
config NODEMCU_CMODULE_MATRIX
bool "MATRIX module"
default "n"
select NODEMCU_CMODULE_GPIO
help
The matrix module provides support for cheap matrixed keypads like a 3x4 telephone keypad.
config NODEMCU_CMODULE_MQTT
bool "MQTT module"
default "n"

574
components/modules/matrix.c Normal file
View File

@ -0,0 +1,574 @@
/*
* Module for interfacing with cheap matrix keyboards like telephone keypads
*
* The idea is to have pullups on all the rows, and drive the columns low.
* WHen a key is pressed, one of the rows will go low and trigger an interrupt. Disable
* all the row interrupts.
* Then we disable all the columns and then drive each column low in turn. Hopefully
* one of the rows will go low. This is a keypress. We only report the first keypress found.
* we start a timer to handle debounce.
* On timer expiry, see if any key is pressed, if so, just wait agin (maybe should use interrupts)
* If no key is pressed, run timer again. On timer expiry, re-enable interrupts.
*
* 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 <stdio.h>
#include "driver/gpio.h"
#define MATRIX_PRESS_INDEX 0
#define MATRIX_RELEASE_INDEX 1
#define MATRIX_ALL 0x3
#define CALLBACK_COUNT 2
#define QUEUE_SIZE 8
typedef struct {
uint8_t column_count;
uint8_t row_count;
uint8_t *columns;
uint8_t *rows;
int character_ref;
int callback[CALLBACK_COUNT];
esp_timer_handle_t timer_handle;
int8_t task_queued;
uint32_t read_offset; // Accessed by task
uint32_t write_offset; // Accessed by ISR
uint32_t last_press_change_time;
int tasknumber;
matrix_event_t queue[QUEUE_SIZE];
void *callback_arg;
} DATA;
static task_handle_t tasknumber;
static void lmatrix_timer_done(void *param);
static void lmatrix_check_timer(DATA *d, uint32_t time_us, bool dotimer);
//
// 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 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)] = \
(matrix_event_t){(x), esp_timer_get_time()})
#define QUEUE_STATUS(d, x) \
(d->queue[(d->write_offset++) & (QUEUE_SIZE - 1)] = \
(matrix_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++; \
}
typedef struct matrix_driver_handle {
int8_t phase_a_pin;
int8_t phase_b_pin;
int8_t press_pin;
int8_t task_queued;
uint32_t read_offset; // Accessed by task
uint32_t write_offset; // Accessed by ISR
uint32_t last_press_change_time;
int tasknumber;
matrix_event_t queue[QUEUE_SIZE];
void *callback_arg;
} *matrix_driver_handle_t;
static void set_gpio_mode_input(int pin, gpio_int_type_t intr) {
gpio_config_t config = {.pin_bit_mask = 1LL << pin,
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = intr};
gpio_config(&config);
}
static void set_gpio_mode_output(int pin) {
gpio_config_t config = {.pin_bit_mask = 1LL << pin,
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE
};
gpio_config(&config);
}
static void matrix_clear_pin(int pin) {
if (pin >= 0) {
gpio_isr_handler_remove(pin);
set_gpio_mode_input(pin, GPIO_INTR_DISABLE);
}
}
static void set_row_interrupts(DATA *d, bool enable)
{
for (int i = 0; i < d->row_count; i++) {
set_gpio_mode_input(d->row[i], enable ? GPIO_INTR_NEGEDGE : GPIO_INTR_DISABLE);
}
}
static int set_columns_as_input(DATA *d)
{
for (int i = 0; i < d->column_count; i++) {
set_gpio_mode_input(d->column[i], GPIO_INTR_DISABLE);
}
}
// Just takes the channel number. Cleans up the resources used.
int matrix_close(DATA *d) {
if (!d) {
return 0;
}
for (int i = 0; i < d->row_count; i++) {
matrix_clear_pin(d->row[i]);
}
set_columns_as_input(d);
return 0;
}
static void matrix_interrupt(void *arg) {
// This function runs with high priority
DATA *d = (DATA *)arg;
uint32_t now = esp_timer_get_time();
int i;
set_columns_as_input(d);
set_row_interrupts(d, false);
int character = -1;
for (int i = 0; i < d->column_count && character < 0; i++) {
set_gpio_mode_output(d->columns[i]);
gpio_set_level(d->columns[i], 0);
for (int j = 0; i < d->row_count && character < 0; j++) {
if (gpio_get_level(d->rows[j]) == 0) {
// We found a keypress
character = j * d->column_count + i;
}
}
set_gpio_mode_input(d->columns[i], GPIO_INTR_DISABLE);
}
// If character is >= 0 then we have found the character -- so send it.
if (last_status != new_status) {
// Either we overwrite the status or we add a new one
if (!HAS_QUEUED_DATA(d) || STATUS_IS_PRESSED(last_status ^ new_status) ||
STATUS_IS_PRESSED(last_status ^ GET_PREV_STATUS(d).pos)) {
if (HAS_QUEUE_SPACE(d)) {
QUEUE_STATUS(d, new_status);
if (!d->task_queued) {
if (task_post_medium(d->tasknumber, (task_param_t)d->callback_arg)) {
d->task_queued = 1;
}
}
} else {
REPLACE_STATUS(d, new_status);
}
} else {
REPLACE_STATUS(d, new_status);
}
}
}
void matrix_event_handled(matrix_driver_handle_t d) { d->task_queued = 0; }
// The pin numbers are actual platform GPIO numbers
matrix_driver_handle_t matrix_setup(int phase_a, int phase_b, int press,
task_handle_t tasknumber, void *arg) {
matrix_driver_handle_t d = (matrix_driver_handle_t)calloc(1, sizeof(*d));
if (!d) {
return NULL;
}
d->tasknumber = tasknumber;
d->callback_arg = arg;
set_gpio_mode(phase_a, GPIO_INTR_ANYEDGE);
gpio_isr_handler_add(phase_a, matrix_interrupt, d);
d->phase_a_pin = phase_a;
set_gpio_mode(phase_b, GPIO_INTR_ANYEDGE);
gpio_isr_handler_add(phase_b, matrix_interrupt, d);
d->phase_b_pin = phase_b;
if (press >= 0) {
set_gpio_mode(press, GPIO_INTR_ANYEDGE);
gpio_isr_handler_add(press, matrix_interrupt, d);
}
d->press_pin = press;
return d;
}
bool matrix_has_queued_event(matrix_driver_handle_t d) {
if (!d) {
return false;
}
return HAS_QUEUED_DATA(d);
}
// Get the oldest event in the queue and remove it (if possible)
bool matrix_getevent(matrix_driver_handle_t d, matrix_event_t *resultp) {
matrix_event_t result = {0};
if (!d) {
return false;
}
bool status = false;
if (HAS_QUEUED_DATA(d)) {
result = GET_READ_STATUS(d);
d->read_offset++;
status = true;
} else {
result = GET_LAST_STATUS(d);
}
*resultp = result;
return status;
}
int matrix_getpos(matrix_driver_handle_t d) {
if (!d) {
return -1;
}
return GET_LAST_STATUS(d).pos;
}
static void callback_free_one(lua_State *L, int *cb_ptr)
{
if (*cb_ptr != LUA_NOREF) {
luaL_unref(L, LUA_REGISTRYINDEX, *cb_ptr);
*cb_ptr = LUA_NOREF;
}
}
static void callback_free(lua_State* L, DATA *d, int mask)
{
if (d) {
int i;
for (i = 0; i < CALLBACK_COUNT; i++) {
if (mask & (1 << i)) {
callback_free_one(L, &d->callback[i]);
}
}
}
}
static int callback_setOne(lua_State* L, int *cb_ptr, int arg_number)
{
if (lua_isfunction(L, arg_number)) {
lua_pushvalue(L, arg_number); // copy argument (func) to the top of stack
callback_free_one(L, cb_ptr);
*cb_ptr = luaL_ref(L, LUA_REGISTRYINDEX);
return 0;
}
return -1;
}
static int callback_set(lua_State* L, DATA *d, int mask, int arg_number)
{
int result = 0;
int i;
for (i = 0; i < CALLBACK_COUNT; i++) {
if (mask & (1 << i)) {
result |= callback_setOne(L, &d->callback[i], arg_number);
}
}
return result;
}
static void callback_callOne(lua_State* L, int cb, int mask, int arg, uint32_t time)
{
if (cb != LUA_NOREF) {
lua_rawgeti(L, LUA_REGISTRYINDEX, cb);
lua_pushinteger(L, mask);
lua_pushvalue(L, arg - 2);
lua_pushinteger(L, time);
luaL_pcallx(L, 3, 0);
}
}
static void callback_call(lua_State* L, DATA *d, int cbnum, int key, uint32_t time)
{
if (d) {
lua_rawgeti(L, LUA_REGISTRYINDEX, d->character_ref);
lua_rawgeti(L, -1, key);
callback_callOne(L, d->callback[cbnum], 1 << cbnum, -1, time);
lua_pop(L, 2);
}
}
// Lua: setup({cols}, {rows}, {characters})
static int lmatrix_setup( lua_State* L )
{
int nargs = lua_gettop(L);
// Get the sizes of the first two tables
luaL_checktype(L, 1, LUA_TTABLE);
luaL_checktype(L, 2, LUA_TTABLE);
luaL_checktype(L, 3, LUA_TTABLE);
size_t columns = lua_rawlen(L, 1);
size_t rows = lua_rawlen(L, 2);
if (columns > 255 || rows > 255) {
return luaL_error(L, "Too many rows or columns");
}
DATA *d = (DATA *)lua_newuserdata(L, sizeof(DATA) + rows + columns);
if (!d) return luaL_error(L, "not enough memory");
memset(d, 0, sizeof(*d) + rows + columns);
luaL_getmetatable(L, "matrix.keyboard");
lua_setmetatable(L, -2);
d->columns = (uint8_t *) (d + 1);
d->rows = d->columns + columns;
d->column_count = columns;
d->row_count = rows;
esp_timer_create_args_t timer_args = {
.callback = lmatrix_timer_done,
.dispatch_method = ESP_TIMER_TASK,
.name = "matrix_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;
}
getpins(L, 1, columns, &d->columns);
getpins(L, 2, rows, &d->rows);
lua_pushvalue(L, 3);
d->character_ref = luaL_ref(L, LUA_REGISTRYINDEX);
d->handle = matrix_setup(phase_a, phase_b, press, tasknumber, d);
if (!d->handle) {
return luaL_error(L, "Unable to setup matrix switch.");
}
return 1;
}
// Lua: close( )
static int lmatrix_close( lua_State* L )
{
DATA *d = (DATA *)luaL_checkudata(L, 1, "matrix.keyboard");
if (d->handle) {
callback_free(L, d, MATRIX_ALL);
if (matrix_close( d->handle )) {
return luaL_error( L, "Unable to close switch." );
}
d->handle = NULL;
}
return 0;
}
// Lua: on( mask[, cb] )
static int lmatrix_on( lua_State* L )
{
DATA *d = (DATA *)luaL_checkudata(L, 1, "matrix.keyboard");
int mask = luaL_checkinteger(L, 2);
if (lua_gettop(L) >= 3) {
if (callback_set(L, d, mask, 3)) {
return luaL_error( L, "Unable to set callback." );
}
} else {
callback_free(L, d, mask);
}
return 0;
}
// Returns TRUE if there maybe/is more stuff to do
static bool lmatrix_dequeue_single(lua_State* L, DATA *d)
{
bool something_pending = false;
if (d) {
matrix_event_t result;
if (matrix_getevent(d->handle, &result)) {
int pos = result.pos;
lmatrix_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, matrix_TURN_INDEX, (pos << 1) >> 1, result.time_us);
}
if ((pos ^ d->lastpos) & 0x80000000) {
// pressing or releasing has happened
callback_call(L, d, (pos & 0x80000000) ? matrix_PRESS_INDEX : matrix_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, matrix_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;
}
matrix_event_handled(d->handle);
something_pending = matrix_has_queued_event(d->handle);
}
lmatrix_check_timer(d, esp_timer_get_time(), 1);
}
return something_pending;
}
static void lmatrix_timer_done(void *param)
{
DATA *d = (DATA *) param;
d->timer_running = 0;
lmatrix_check_timer(d, esp_timer_get_time(), 1);
}
static void lmatrix_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, matrix_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, matrix_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 lmatrix_task(task_param_t param, task_prio_t prio)
{
(void) prio;
bool need_to_post = false;
lua_State *L = lua_getstate();
DATA *d = (DATA *) param;
if (d) {
if (lmatrix_dequeue_single(L, d)) {
need_to_post = true;
}
}
if (need_to_post) {
// If there is pending stuff, queue another task
task_post_medium(tasknumber, param);
}
}
// Module function map
LROT_BEGIN(matrix, NULL, 0)
LROT_FUNCENTRY( setup, lmatrix_setup )
LROT_NUMENTRY( PRESS, MASK(PRESS) )
LROT_NUMENTRY( RELEASE, MASK(RELEASE) )
LROT_NUMENTRY( ALL, MATRIX_ALL )
LROT_END(matrix, NULL, 0)
// Module function map
LROT_BEGIN(matrix_keyboard, NULL, LROT_MASK_GC_INDEX)
LROT_FUNCENTRY(__gc, lmatrix_close)
LROT_TABENTRY(__index, matrix_keyboard)
LROT_FUNCENTRY(on, lmatrix_on)
LROT_FUNCENTRY(close, lmatrix_close)
LROT_END(matrix_keyboard, NULL, LROT_MASK_GC_INDEX)
static int matrix_open(lua_State *L) {
luaL_rometatable(L, "matrix.keyboard",
LROT_TABLEREF(matrix_keyboard)); // create metatable
tasknumber = task_get_id(lmatrix_task);
return 0;
}
NODEMCU_MODULE(matrix, "matrix", matrix, matrix_open);

79
docs/modules/matrix.md Normal file
View File

@ -0,0 +1,79 @@
# matrix Module
| Since | Origin / Contributor | Maintainer | Source |
| :----- | :-------------------- | :---------- | :------ |
| 2024-02-01 | [Philip Gladstone](https://github.com/pjsg) | [Philip Gladstone](https://github.com/pjsg) | [matrix.c](../../components/modules/matrix.c)|
This module processes key presses on matrixed keyboards such as cheap numeric keypads with the # and * keys. These are organized as a 3x4 matrix with 7 connections
in all.
## 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.
## Constants
- `matrix.PRESS = 1` The eventtype for a keyboard key press
- `matrix.RELEASE = 2` The eventtype for keyboard key release.
- `matrix.ALL = 3` Covers all event types
## matrix.setup()
Initialize the nodemcu to talk to a matrixed keyboard.
#### Syntax
`keyboard = matrix.setup({column pins}, {row pins}, {key characters})`
#### Parameters
- `column pins` These are the GPIO numbers of the pins connected to the columns of the keyboard
- `row pins` These are the GPIO numbers of the pins connected to the rows of the keyboard
- `key characters` These are the characters (or strings) to be returned when a key is pressed. The first character corresponds to the first row and first column. The next character is the second column and first row, etc.
#### Returns
The keyboard object.
#### Example
keyboard = matrix.setup({5,6,7}, {8,9,10,11}, { "1", "2", "3", "4", "5", "6", "7", "8", "9", "#", "0", "*"})
#### Notes
If an entry in the key characters table is nil, then that key press will not be reported.
## keyboard:on()
Sets a callback on specific events.
#### Syntax
`keyboard:on(eventtype[, callback])`
#### Parameters
- `eventtype` This defines the type of event being registered. This can be one or more of `matrix.PRESS` and `matrix.RELEASE`.
- `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 character, and the third is the time when the event happened.
The time is the number of microseconds represented in a 32-bit integer. Note that this wraps every hour or so.
#### Example
keyboard:on(matrix.ALL, function (type, char, when)
print("Character=" .. char .. " event type=" .. type .. " time=" .. when)
end)
#### Errors
If an invalid `eventtype` is supplied, then an error will be thrown.
## keyboard:close()
Releases the resources associated with the matrix keyboard.
#### Syntax
`keyboard:close()`
#### Example
keyboard:close()