Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions devices/CARDPUTER _ADV/definition.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
constants:
_MH_BATT_ADC: '10'
_MH_DISPLAY_BACKLIGHT: '38'
_MH_DISPLAY_BAUDRATE: '40_000_000'
_MH_DISPLAY_CS: '37'
_MH_DISPLAY_DC: '34'
_MH_DISPLAY_HEIGHT: '135'
_MH_DISPLAY_MISO: None
_MH_DISPLAY_MOSI: '35'
_MH_DISPLAY_RESET: '33'
_MH_DISPLAY_ROTATION: '1'
_MH_DISPLAY_SCK: '36'
_MH_DISPLAY_SPI_ID: '1'
_MH_DISPLAY_WIDTH: '240'
_MH_I2S_ID: '1'
_MH_I2S_SCK: '41'
_MH_I2S_SD: '42'
_MH_I2S_WS: '43'
_MH_SDCARD_CS: '12'
_MH_SDCARD_MISO: '39'
_MH_SDCARD_MOSI: '14'
_MH_SDCARD_SCK: '40'
_MH_SDCARD_SLOT: '2'

features:
- keyboard
- display
- i2s_speaker
- pdm_microphone
- ir_blaster
- wifi
- bluetooth

mpy_arch: xtensawin
source_board: MICROHYDRA_GENERIC_S3
132 changes: 132 additions & 0 deletions devices/CARDPUTER _ADV/lib/userinput/_keys.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
"""Read and return keyboard data for the M5Stack Cardputer ADV."""

from machine import Pin, I2C
from .tca8418 import TCA8418
import time

#lookup values for our keyboard
KC_SHIFT = const(7)
KC_FN = const(3)

KEYMAP = {
1:'`', 5:'1', 11:'2', 15:'3', 21:'4', 25:'5', 31:'6', 35:'7', 41:'8', 45:'9', 51:'0', 55:'-',61:'=', 65:'BSPC',

2:'TAB',6:'q', 12:'w', 16:'e', 22:'r', 26:'t', 32:'y', 36:'u', 42:'i', 46:'o', 52:'p', 56:'[', 62:']', 66:'\\',

3:"FN",7:"SHIFT",13:'a', 17:'s', 23:'d', 27:'f', 33:'g', 37:'h', 43:'j', 47:'k', 53:'l', 57:';', 63:"'", 67:'ENT',

4:'CTL',8:'OPT',14:'ALT',18:'z', 24:'x', 28:'c', 34:'v', 38:'b', 44:'n', 48:'m', 54:',', 58:'.', 64:'/', 68:'SPC',
}

KEYMAP_SHIFT = {
1:'~', 5:'!', 11:'@', 15:'#', 21:'$', 25:'%', 31:'^', 35:'&', 41:'*', 45:'(', 51:')', 55:'_', 61:'+', 65:'BSPC',

2:'TAB',6:'Q', 12:'W', 16:'E', 22:'R', 26:'T', 32:'Y', 36:'U', 42:'I', 46:'O', 52:'P', 56:'{',62:'}',66:'|',

3:"FN",7:"SHIFT",13:'A', 17:'S', 23:'D', 27:'F', 33:'G', 37:'H', 43:'J', 47:'K', 53:'L', 57:':', 63:'"', 67:'ENT',

4:'CTL',8:'OPT',14:'ALT',18:'Z', 24:'X', 28:'C', 34:'V', 38:'B', 44:'N', 48:'M', 54:'<', 58:'>', 64:'?', 68:'SPC',
}

KEYMAP_FN = {
1:'ESC',5:'F1', 11:'F2', 15:'F3',21:'F4',25:'F5',31:'F6',35:'F7',41:'F8',45:'F9',51:'F10',55:'_',61:'=', 65:'DEL',

2:'TAB',6:'q', 12:'w', 16:'e', 22:'r', 26:'t', 32:'y', 36:'u',42:'i', 46:'o', 52:'p', 56:'[', 62:']', 66:'\\',

3:"FN",7:"SHIFT",13:'a', 17:'s', 23:'d', 27:'f', 33:'g', 37:'h', 43:'j', 47:'k', 53:'l', 57:'UP',63:"'", 67:'ENT',

4:'CTL',8:'OPT',14:'ALT',18:'z', 24:'x', 28:'c', 34:'v', 38:'b', 44:'n',48:'m',54:'LEFT',58:'DOWN',64:'RIGHT',68:'SPC', # noqa: E501
}

MOD_KEYS = const(('ALT', 'CTL', 'FN', 'SHIFT', 'OPT'))
ALWAYS_NEW_KEYS = const(())

class Keys:
"""Keys class is responsible for reading and returning currently pressed keys.

It is intented to be used by the Input module.
"""

# optional values set preferred main/secondary action keys:
main_action = "ENT"
secondary_action = "SPC"
aux_action = "G0"

ext_dir_dict = {';':'UP', ',':'LEFT', '.':'DOWN', '/':'RIGHT', '`':'ESC'}

def __init__(self, **kwargs): # noqa: ARG002
self._key_list_buffer = []

# setup the "G0" button!
self.G0 = Pin(0, Pin.IN, Pin.PULL_UP)

# Initialize I2C bus
i2c = I2C()

try:
self.keypad = TCA8418(i2c)

except OSError as e:
print(f"I2C communication error: {e}")

self.key_state = []

@micropython.viper
def scan(self): # noqa: ANN202
"""Scan through the matrix to see what keys are pressed."""
key_code, is_press = self.keypad.read_key_event()
if is_press:
if key_code not in self._key_list_buffer:
self._key_list_buffer.append(key_code)
if not is_press:
if key_code in self._key_list_buffer:
self._key_list_buffer.remove(key_code)

return self._key_list_buffer

@staticmethod
def ext_dir_keys(keylist) -> list:
"""Convert typical (aphanumeric) keys into extended movement-specific keys."""
for idx, key in enumerate(keylist):
if key in Keys.ext_dir_dict:
keylist[idx] = Keys.ext_dir_dict[key]
return keylist

def get_pressed_keys(self, *, force_fn=False, force_shift=False) -> list:
"""Get a readable list of currently held keys.

Also, populate self.key_state with current vals.

Args:
force_fn: (bool)
If true, forces the use of 'FN' key layer
force_shift: (bool)
If True, forces the use of 'SHIFT' key layer
"""

#update our scan results
event_count = self.keypad.read_event_count()
while event_count:
self.scan()
event_count = self.keypad.read_event_count()
self.key_state = []

if self.G0.value() == 0:
self.key_state.append("G0")

if not self._key_list_buffer and not self.key_state: # if nothing is pressed, we can return an empty list
return self.key_state

if KC_FN in self._key_list_buffer or force_fn:
for keycode in self._key_list_buffer:
self.key_state.append(KEYMAP_FN[keycode])

elif KC_SHIFT in self._key_list_buffer or force_shift:
for keycode in self._key_list_buffer:
self.key_state.append(KEYMAP_SHIFT[keycode])

else:
for keycode in self._key_list_buffer:
self.key_state.append(KEYMAP[keycode])

return self.key_state
104 changes: 104 additions & 0 deletions devices/CARDPUTER _ADV/lib/userinput/tca8418.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
from machine import I2C, Pin
import time

# TCA8418 Register Addresses
REG_CFG = 0x01
REG_INT_STAT = 0x02
REG_KEY_STAT = 0x04
REG_KEY_LCK_EC = 0x03
REG_KP_ML_SM = 0x0E
REG_KP_SL_CFG = 0x0F
REG_KP_GPIO1 = 0x1D # KP_GPIO1 for COL0-COL7 / ROW0-ROW7
REG_KP_GPIO2 = 0x1E # KP_GPIO2 for COL8-COL10 / ROW8-ROW9
REG_KP_GPIO3 = 0x1F # KP_GPIO3 for ROW10-ROW11

# Default I2C address for the TCA8418
TCA8418_ADDR = 0x34

class TCA8418:
"""MicroPython driver for the TCA8418 I2C Keypad/GPIO expander."""

def __init__(self, i2c, address=TCA8418_ADDR):
self.i2c = i2c
self.address = address
if self.address not in self.i2c.scan():
raise OSError(f"TCA8418 not found at address {hex(self.address)}")
self.init_device()

def _write_register(self, register, value):
"""Writes a byte value to a specific register using writeto_mem."""
self.i2c.writeto_mem(self.address, register, bytes([value]))

def _read_register(self, register):
"""Reads a byte value from a specific register using readfrom_mem."""
data = self.i2c.readfrom_mem(self.address, register, 1)
return data[0] # Return the single byte value

def init_device(self):
"""Initializes the TCA8418 for keypad scanning mode and clears interrupts."""

# 1. Clear any pending interrupts by writing to the status register
self._write_register(REG_INT_STAT, 0xFF)

# 2. Configure the chip:
# Enable keypad interrupt (KE_IEN) and set interrupt config (INT_CFG)
# 0x11: INT_EN=1, INT_CFG=1 (cleared by writing 1), KE_IEN=1
self._write_register(REG_CFG, 0x11)

# 3. Define which pins are part of the keypad matrix (example for an 8x2 matrix)
# You MUST configure these registers based on your physical wiring.
# This example assumes R0-R7 and C0-C1 are used.

# To configure ALL rows (R0-R11) and columns (C0-C10) to be part of the keypad matrix:
self._write_register(REG_KP_GPIO1, 0xFF) # Enable all pins in this register for keypad use
self._write_register(REG_KP_GPIO2, 0xFF)
self._write_register(REG_KP_GPIO3, 0x0F) # Adjust this mask based on actual rows used

# 4. Enable Keypad Matrix Scan Mode
# Write 0x01 to KP_ML_SM to enable scanning
self._write_register(REG_KP_ML_SM, 0x01)

def read_event_count(self):
# Check the number of events in the FIFO - including press and release
event_count = self._read_register(REG_KEY_LCK_EC) & 0x0F
return event_count

def read_key_event(self):
"""Reads the next key event from the FIFO queue."""

# Read register to determin what asserted the interrupt
interrupt = self._read_register(REG_INT_STAT)

# Check if bit 0 is set
if (interrupt & 0x00) != 0:
return 0, 0

if (interrupt & 0x01) != 0:
# Check the number of events in the FIFO - including press and release
event_count = self._read_register(REG_KEY_LCK_EC) & 0x0F

if event_count > 0:
# Reading REG_KEY_STAT pops the oldest event from the FIFO
key_event = self._read_register(REG_KEY_STAT)

# Key event format:
# Bit 7: Key Event type (1=Press, 0=Release)
# Bits 6:0: Keypad representation code (Row/Column combination)
is_press = bool(key_event & 0x80)
key_code = key_event & 0x7F

# After processing all events, the interrupt line should go high
# You might need to clear the interrupt status bit manually if using interrupt pin
if event_count == 1:
self.clear_interrupt()

return key_code, is_press

return 0, 0

def clear_interrupt(self):
"""Clears the interrupt status by reading/writing to the INT_STAT register."""
# Reading the register clears it if INT_CFG bit is 0.
# If INT_CFG bit is 1 (as set in init_device), you must write 1 to clear the specific KE_INT bit.
# Writing 0xFF ensures all bits are cleared for safety.
self._write_register(REG_INT_STAT, 0xFF)