Skip to content

Commit

Permalink
Added support for Cardputer keyboard input.
Browse files Browse the repository at this point in the history
  • Loading branch information
joshua-beck-0908 committed Apr 5, 2024
1 parent 5836f11 commit 090f330
Show file tree
Hide file tree
Showing 8 changed files with 438 additions and 3 deletions.
159 changes: 159 additions & 0 deletions ports/espressif/boards/m5stack_cardputer/board.c
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,51 @@
* THE SOFTWARE.
*/

#include "keymap.h"
#include "supervisor/board.h"
#include "supervisor/serial.h"
#include "mpconfigboard.h"
#include "shared-bindings/busio/SPI.h"
#include "shared-bindings/fourwire/FourWire.h"
#include "shared-bindings/microcontroller/Pin.h"
#include "shared-module/displayio/__init__.h"
#include "shared-module/displayio/mipi_constants.h"
#include "shared-bindings/board/__init__.h"
#include "shared-bindings/keypad_demux/DemuxKeyMatrix.h"
#include "shared-bindings/keypad/EventQueue.h"
#include "shared-bindings/keypad/Event.h"
#include "supervisor/shared/reload.h"
#include "py/runtime.h"

fourwire_fourwire_obj_t board_display_obj;
keypad_demux_demuxkeymatrix_obj_t board_keyboard;

void update_keyboard(void);
void queue_key(char c);
void queue_seq(const char *seq);

const mcu_pin_obj_t *row_addr_pins[] = {
&pin_GPIO8,
&pin_GPIO9,
&pin_GPIO11,
};

const mcu_pin_obj_t *column_pins[] = {
&pin_GPIO13,
&pin_GPIO15,
&pin_GPIO3,
&pin_GPIO4,
&pin_GPIO5,
&pin_GPIO6,
&pin_GPIO7
};

keypad_event_obj_t event;

char keystate[56];
char keyqueue[16];
int keyqueue_head = 0;
int keyqueue_tail = 0;

#define DELAY 0x80

Expand Down Expand Up @@ -106,8 +141,132 @@ void board_init(void) {
false, // SH1107_addressing
350 // backlight pwm frequency
);
//void common_hal_keypad_demux_demuxkeymatrix_construct(
// keypad_demux_demuxkeymatrix_obj_t *self,
// mp_uint_t num_row_addr_pins,
// const mcu_pin_obj_t *row_addr_pins[],
// mp_uint_t num_column_pins,
// const mcu_pin_obj_t *column_pins[],
// mp_float_t interval,
// size_t max_events,
// uint8_t debounce_threshold) {
}

void board_serial_init() {
common_hal_keypad_demux_demuxkeymatrix_construct(
&board_keyboard, // self
3, // num_row_addr_pins
row_addr_pins, // row_addr_pins
7, // num_column_pins
column_pins, // column_pins
0.01f, // interval
20, // max_events
2 // debounce_threshold
);
demuxkeymatrix_never_reset(&board_keyboard);
}

bool board_serial_connected() {
return true;
}

bool board_serial_bytes_available() {
update_keyboard();
if (keyqueue_head != keyqueue_tail) {
return true;
} else {
return false;
}
}

void queue_key(char c) {
if ((keyqueue_head + 1) % 16 != keyqueue_tail) {
keyqueue[keyqueue_head] = c;
keyqueue_head = (keyqueue_head + 1) % 16;
}
}

void queue_seq(const char *seq) {
while (*seq) {
queue_key(*seq++);
}
}

void update_keyboard() {
char ascii = 0;

if (common_hal_keypad_eventqueue_get_length(board_keyboard.events) == 0) {
return;
}

//mp_printf(&mp_plat_print, "NextScan:%d\n", board_keyboard.next_scan_ticks);
while (common_hal_keypad_eventqueue_get_into(board_keyboard.events, &event)) {
//mp_printf(&mp_plat_print, "Got Event: %d %d\n", event.key_number, event.pressed);
if (event.pressed) {
keystate[event.key_number] = 1;

if (keystate[KEY_CTRL]) {
if (keystate[KEY_ALT] && keystate[KEY_BACKSPACE]) {
reload_initiate(RUN_REASON_REPL_RELOAD);
}
ascii = keymap[event.key_number];
if (ascii >= 'a' && ascii <= 'z') {
ascii -= 'a' - 1;
}
//if (ascii == 3) {
// // Ctrl-C
// mp_sched_keyboard_interrupt();
//}
} else if (keystate[KEY_SHIFT]) {
ascii = keymap_shifted[event.key_number];
} else if (keystate[KEY_FN] && event.key_number != KEY_FN) {
switch (event.key_number | FN_MOD) {
case KEY_DOWN:
queue_seq("\e[B");
break;
case KEY_UP:
queue_seq("\e[A");
break;
case KEY_DELETE:
queue_seq("\e[3~");
break;
case KEY_LEFT:
queue_seq("\e[D");
break;
case KEY_RIGHT:
queue_seq("\e[C");
break;
case KEY_ESC:
queue_key('\e');
break;
}
} else {
ascii = keymap[event.key_number];
}

if (ascii > 0) {
if (keystate[KEY_ALT]) {
queue_key('\e');
}
queue_key(ascii);
}

} else {
keystate[event.key_number] = 0;
}
}
}

char board_serial_read() {
update_keyboard();
if (keyqueue_head != keyqueue_tail) {
char c = keyqueue[keyqueue_tail];
keyqueue_tail = (keyqueue_tail + 1) % 16;
return c;
} else {
return -1;
}
}

// Use the MP_WEAK supervisor/shared/board.c versions of routines not defined here.

Expand Down
206 changes: 206 additions & 0 deletions ports/espressif/boards/m5stack_cardputer/keymap.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
#define SHIFT_MOD 0x40
#define FN_MOD 0x80

#define KEY_OPT 0
#define KEY_Z 1
#define KEY_C 2
#define KEY_B 3
#define KEY_M 4
#define KEY_DOT 5
#define KEY_SPACE 6
#define KEY_SHIFT 7
#define KEY_S 8
#define KEY_F 9
#define KEY_H 10
#define KEY_K 11
#define KEY_SEMICOLON 12
#define KEY_ENTER 13
#define KEY_Q 14
#define KEY_E 15
#define KEY_T 16
#define KEY_U 17
#define KEY_O 18
#define KEY_LEFT_BRACKET 19
#define KEY_BACKSLASH 20
#define KEY_1 21
#define KEY_3 22
#define KEY_5 23
#define KEY_7 24
#define KEY_9 25
#define KEY_UNDERSCORE 26
#define KEY_BACKSPACE 27
#define KEY_CTRL 28
#define KEY_ALT 29
#define KEY_X 30
#define KEY_V 31
#define KEY_N 32
#define KEY_COMMA 33
#define KEY_SLASH 34
#define KEY_FN 35
#define KEY_A 36
#define KEY_D 37
#define KEY_G 38
#define KEY_J 39
#define KEY_L 40
#define KEY_APOSTROPHE 41
#define KEY_TAB 42
#define KEY_W 43
#define KEY_R 44
#define KEY_Y 45
#define KEY_I 46
#define KEY_P 47
#define KEY_RIGHT_BRACKET 48
#define KEY_GRAVE 49
#define KEY_2 50
#define KEY_4 51
#define KEY_6 52
#define KEY_8 53
#define KEY_0 54
#define KEY_EQUALS 55

#define KEY_GREATER (5 | SHIFT_MOD)
#define KEY_COLON (12 | SHIFT_MOD)
#define KEY_LEFT_CURLY_BRACKET (19 | SHIFT_MOD)
#define KEY_PIPE (20 | SHIFT_MOD)
#define KEY_EXCLAMATION (21 | SHIFT_MOD)
#define KEY_HASH (22 | SHIFT_MOD)
#define KEY_PERCENT (23 | SHIFT_MOD)
#define KEY_AMPERSAND (24 | SHIFT_MOD)
#define KEY_OPEN_PARENTHESIS (25 | SHIFT_MOD)
#define KEY_MINUS (26 | SHIFT_MOD)
#define KEY_LESS (33 | SHIFT_MOD)
#define KEY_QUESTION (34 | SHIFT_MOD)
#define KEY_DOUBLE_QUOTE (41 | SHIFT_MOD)
#define KEY_RIGHT_CURLY_BRACKET (48 | SHIFT_MOD)
#define KEY_TILDE (49 | SHIFT_MOD)
#define KEY_AT (50 | SHIFT_MOD)
#define KEY_DOLLAR (51 | SHIFT_MOD)
#define KEY_CARET (52 | SHIFT_MOD)
#define KEY_ASTERISK (53 | SHIFT_MOD)
#define KEY_CLOSE_PARENTHESIS (54 | SHIFT_MOD)
#define KEY_PLUS (55 | SHIFT_MOD)

#define KEY_DOWN (5 | FN_MOD)
#define KEY_UP (12 | FN_MOD)
#define KEY_DELETE (27 | FN_MOD)
#define KEY_LEFT (33 | FN_MOD)
#define KEY_RIGHT (34 | FN_MOD)
#define KEY_ESC (49 | FN_MOD)

int keymap[56] = {
0, // KEY_OPT
'z', // KEY_Z
'c', // KEY_C
'b', // KEY_B
'm', // KEY_M
'.', // KEY_DOT
' ', // KEY_SPACE
0, // KEY_SHIFT
's', // KEY_S
'f', // KEY_F
'h', // KEY_H
'k', // KEY_K
';', // KEY_SEMICOLON
'\r',// KEY_ENTER
'q', // KEY_Q
'e', // KEY_E
't', // KEY_T
'u', // KEY_U
'o', // KEY_O
'[', // KEY_LEFT_BRACKET
'\\',// KEY_BACKSLASH
'1', // KEY_1
'3', // KEY_3
'5', // KEY_5
'7', // KEY_7
'9', // KEY_9
'_', // KEY_UNDERSCORE
'\b',// KEY_BACKSPACE
0, // KEY_CTRL
0, // KEY_ALT
'x', // KEY_X
'v', // KEY_V
'n', // KEY_N
',', // KEY_COMMA
'/', // KEY_SLASH
0, // KEY_FN
'a', // KEY_A
'd', // KEY_D
'g', // KEY_G
'j', // KEY_J
'l', // KEY_L
'\'',// KEY_APOSTROPHE
'\t',// KEY_TAB
'w', // KEY_W
'r', // KEY_R
'y', // KEY_Y
'i', // KEY_I
'p', // KEY_P
']', // KEY_RIGHT_BRACKET
'`', // KEY_GRAVE
'2', // KEY_2
'4', // KEY_4
'6', // KEY_6
'8', // KEY_8
'0', // KEY_0
'=' // KEY_EQUALS
};

int keymap_shifted[56] = {
0, // KEY_OPT
'Z', // KEY_Z
'C', // KEY_C
'B', // KEY_B
'M', // KEY_M
'>', // KEY_DOT -> '>'
' ', // KEY_SPACE
0, // KEY_SHIFT
'S', // KEY_S
'F', // KEY_F
'H', // KEY_H
'K', // KEY_K
':', // KEY_SEMICOLON -> ':'
'\r',// KEY_ENTER
'Q', // KEY_Q
'E', // KEY_E
'T', // KEY_T
'U', // KEY_U
'O', // KEY_O
'{', // KEY_LEFT_BRACKET -> '{'
'|', // KEY_BACKSLASH -> '|'
'!', // KEY_1 -> '!'
'#', // KEY_3 -> '#'
'%', // KEY_5 -> '%'
'&', // KEY_7 -> '&'
'(', // KEY_9 -> '('
'_', // KEY_UNDERSCORE -> '-'
'\x7F',// KEY_BACKSPACE
0, // KEY_CTRL
0, // KEY_ALT
'X', // KEY_X
'V', // KEY_V
'N', // KEY_N
'<', // KEY_COMMA -> '<'
'?', // KEY_SLASH -> '?'
0, // KEY_FN
'A', // KEY_A
'D', // KEY_D
'G', // KEY_G
'J', // KEY_J
'L', // KEY_L
'"', // KEY_APOSTROPHE -> '"'
'\t',// KEY_TAB
'W', // KEY_W
'R', // KEY_R
'Y', // KEY_Y
'I', // KEY_I
'P', // KEY_P
'}', // KEY_RIGHT_BRACKET -> '}'
'~', // KEY_GRAVE -> '~'
'@', // KEY_2 -> '@'
'$', // KEY_4 -> '$'
'^', // KEY_6 -> '^'
'*', // KEY_8 -> '*'
')', // KEY_0 -> ')'
'+' // KEY_EQUALS -> '+'
};
Loading

8 comments on commit 090f330

@konstantint
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can one use the keyboard from the CircuitPython program? E.g. if I just try to manually create a DemuxKeyMatrix object, CircuitPython justifiably complains that "KB_A_0 is in use". Indeed, the keyboard has been configured for scanning at board initialization time in this commit.

Is there a way to get a hold of the event queue the keyboard events are being pushed by the pre-initialized object? Alternatively, is there a way to deinitialize the keyboard scanner (and free the pins) so that I could create an instance usable directly in my code?

@joshua-beck-0908
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can one use the keyboard from the CircuitPython program? E.g. if I just try to manually create a DemuxKeyMatrix object, CircuitPython justifiably complains that "KB_A_0 is in use". Indeed, the keyboard has been configured for scanning at board initialization time in this commit.

Is there a way to get a hold of the event queue the keyboard events are being pushed by the pre-initialized object? Alternatively, is there a way to deinitialize the keyboard scanner (and free the pins) so that I could create an instance usable directly in my code?

Is there a reason you don't want to use input() or sys.stdin.read() ?
What are you trying to do?

@konstantint
Copy link

@konstantint konstantint commented on 090f330 Aug 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't realize the keys are accessible via stdin (didn't guess by just looking at the code that board_serial_xxx does that).

I do not think this would be enough, though. I'm trying to use the keyboard as a sound controller of sorts and would like to get access to individual key up / key down events. I also want to use non-character keys. Would that be possible?
If not now, could the board_keyboard object be somehow exposed as board.KEYBOARD to the Python code here?

@joshua-beck-0908
Copy link
Author

@joshua-beck-0908 joshua-beck-0908 commented on 090f330 Aug 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't realize the keys are accessible via stdin (didn't guess by just looking at the code that board_serial_xxx does that).

I do not think this would be enough, though. I'm trying to use the keyboard as a sound controller of sorts and would like to get access to individual key up / key down events. Would that be possible? If not now, could the board_keyboard object be somehow exposed as board.KEYBOARD to the Python code here?

How is it set up

The keyboard is set up like a serial input—a board-specific one. There's currently no alternative to this.
How does this work? It produces the same key strokes as you'd get from a serial terminal.

What are my key codes?

You can find out what they are! Look them up on the VT102 spec or even easier, just run this program and press the keys:

while True:
    print(hex(ord(sys.stdin.read(1))))

So you can get arrows, backspace, escape, ctrl-f, alt-r, opt-l, etc. Whatever's on the keyboard there's an output sequence for it!

For example, the up key will generate something like: \e[B (in hex that's: 0x1B, 0x5B, 0x41).
But that's only if you're holding down the fn modifier. Otherwise, you'd get what's printed in black, the semicolon.

Why is it set up this way?

Because it:

  • Works with serial terminals.
  • Requires no special code to read input.
  • Lets you Ctrl-C to stop a program.
  • Lets you enter code without a computer connection.
  • Is easy (relatively) to implement.

Can I check which keys are down?

No, keyboard_demux doesn't provide this, and it's the only viable built-in API.

Can I access the key queue?

No, the system steals all the keystrokes before your program can see them.

Can you expose the keyboard object so I can deinit it and create my own?

I've tried... either the driver stops working or the whole system just crashes.
So I decided not to expose the keyboard object.
I'll get some advice and have another look later.

@konstantint
Copy link

@konstantint konstantint commented on 090f330 Aug 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, thanks for the detailed write-up.

Having access to individual key-up/key-down events is pretty important to be able to implement any sort of game-like interaction, however (which, for my personal purposes, is way more important than the built-in terminal. Neither do I want to be able to ctrl+C out of the uploaded program).

It would be nice then, if we could come up with a slighly looser integration here. Either of the following would work:

  • Ability to disable the built-in keyboard/serial integration in settings.toml (and then simply instantiate the DemuxKeyMatrix manually).
  • No built-in keyboard integration, but a convenient library instead that could enable it using a one-liner (so anyone who needs it would just add the respective line in code.py).
  • Pre-initialized board.KEYBOARD object that can however be released if necessary (in the same spirit displayio.release_display() works).

I'm happy to try some of these options (although it is literally the first time I am seeeing the micro/circuitpython codebase so I'll still need to figure out how to build it, expose C++ objects to Python, etc).

@konstantint
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... something like this, for example. WDYT?

@joshua-beck-0908
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's great that you've already written some code to flesh out how this will work.
I do like the idea of controlling the keyboard mode with a config variable. Have you tested this code?
However, I wonder if there's a method to toggle whether the serial driver takes events from the queue.
One that can be controlled by the program.
I'll look over this tomorrow when I have some time.

Are you on the Adafruit Discord?
You'd be able to get support there, and you'd be able to send me a DM if you'd like.
Or use any of the socials linked to on my profile.
It's got to be a bit easier than having this discussion through the comments on an old GitHub commit message.

@konstantint
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. Joined the discord, pinged you there.

I did of course check that the thing I wrote behaves as intended.

Please sign in to comment.