diff --git a/devices/CARDPUTER _ADV/definition.yml b/devices/CARDPUTER _ADV/definition.yml new file mode 100644 index 0000000..1d70a3c --- /dev/null +++ b/devices/CARDPUTER _ADV/definition.yml @@ -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 diff --git a/devices/CARDPUTER _ADV/lib/userinput/_keys.py b/devices/CARDPUTER _ADV/lib/userinput/_keys.py new file mode 100644 index 0000000..47c9e76 --- /dev/null +++ b/devices/CARDPUTER _ADV/lib/userinput/_keys.py @@ -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 \ No newline at end of file diff --git a/devices/CARDPUTER _ADV/lib/userinput/tca8418.py b/devices/CARDPUTER _ADV/lib/userinput/tca8418.py new file mode 100644 index 0000000..6ebadf6 --- /dev/null +++ b/devices/CARDPUTER _ADV/lib/userinput/tca8418.py @@ -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)