kmk_firmware/kmk/firmware.py
Josh Klar d6a0538009
kmk.consts: rename UnicodeModes => UnicodeMode
This brings this naming into consistency with both fellow consts in the
same file (ex. LeaderMode is singular) as well as the variables in which
the consts are usually used (usually a `Firmware.unicode_mode` attribute
in a keymap).
2018-12-04 17:03:13 -08:00

217 lines
7.0 KiB
Python

# Welcome to RAM and stack size hacks central, I'm your host, klardotsh!
# We really get stuck between a rock and a hard place on CircuitPython
# sometimes: our import structure is deeply nested enough that stuff
# breaks in some truly bizarre ways, including:
# - explicit RuntimeError exceptions, complaining that our
# stack depth is too deep
#
# - silent hard locks of the device (basically unrecoverable without
# UF2 flash if done in main.py, fixable with a reboot if done
# in REPL)
#
# However, there's a hackaround that works for us! Because sys.modules
# caches everything it sees (and future imports will use that cached
# copy of the module), let's take this opportunity _way_ up the import
# chain to import _every single thing_ KMK eventually uses in a normal
# workflow, in order from fewest to least nested dependencies.
# First, stuff that has no dependencies, or only C/MPY deps
import collections # isort:skip
import kmk.consts # isort:skip
import kmk.kmktime # isort:skip
import kmk.types # isort:skip
# Now stuff that depends on the above (and so on)
import kmk.keycodes # isort:skip
import kmk.matrix # isort:skip
import kmk.hid # isort:skip
import kmk.internal_state # isort:skip
# GC runs automatically after CircuitPython imports. If we ever go back to
# supporting MicroPython, we'll need a GC here (and probably after each
# chunk of the above)
# Thanks for sticking around. Now let's do real work, starting below
import busio
import gc
import supervisor
from kmk.consts import LeaderMode, UnicodeMode
from kmk.hid import USB_HID
from kmk.internal_state import InternalState
from kmk.matrix import MatrixScanner
class Firmware:
debug_enabled = False
keymap = None
row_pins = None
col_pins = None
diode_orientation = None
unicode_mode = UnicodeMode.NOOP
tap_time = 300
leader_mode = LeaderMode.TIMEOUT
leader_dictionary = {}
leader_timeout = 1000
hid_helper = USB_HID
split_offsets = ()
split_flip = False
split_side = None
split_type = None
split_master_left = True
is_master = None
uart = None
uart_flip = True
def __init__(self):
self._state = InternalState(self)
def _send_hid(self):
self._hid_helper_inst.create_report(self._state.keys_pressed).send()
self._state.resolve_hid()
def _send_key(self, key):
if not getattr(key, 'no_press', None):
self._state.add_key(key)
self._send_hid()
if not getattr(key, 'no_release', None):
self._state.remove_key(key)
self._send_hid()
def _handle_matrix_report(self, update=None):
'''
Bulk processing of update code for each cycle
:param update:
'''
if update is not None:
self._state.matrix_changed(
update[0],
update[1],
update[2],
)
def _send_to_master(self, update):
if self.split_master_left:
update[1] += self.split_offsets[update[0]]
else:
update[1] -= self.split_offsets[update[0]]
if self.uart is not None:
self.uart.write(update)
def _receive_from_slave(self):
if self.uart is not None and self.uart.in_waiting > 0:
update = bytearray(self.uart.read(3))
# Built in debug mode switch
if update == b'DEB':
# TODO Pretty up output
print(self.uart.readline())
return None
return update
return None
def _send_debug(self, message):
'''
Prepends DEB and appends a newline to allow debug messages to
be detected and handled differently than typical keypresses.
:param message: Debug message
'''
if self.uart is not None:
self.uart.write('DEB')
self.uart.write(message, '\n')
def _master_half(self):
return supervisor.runtime.serial_connected
def init_uart(self, tx=None, rx=None, timeout=20):
if self._master_half():
# If running with one wire, only receive on master
if rx is None or self.uart_flip:
return busio.UART(tx=rx, rx=None, timeout=timeout)
else:
return busio.UART(tx=tx, rx=rx, timeout=timeout)
else:
return busio.UART(tx=tx, rx=rx, timeout=timeout)
def go(self):
assert self.keymap, 'must define a keymap with at least one row'
assert self.row_pins, 'no GPIO pins defined for matrix rows'
assert self.col_pins, 'no GPIO pins defined for matrix columns'
assert self.diode_orientation is not None, 'diode orientation must be defined'
self.is_master == self._master_half()
if self.split_flip and not self._master_half():
self.col_pins = list(reversed(self.col_pins))
if self.split_side == "Left":
self.split_master_left = self.is_master
elif self.split_side == "Right":
self.split_master_left = not self.is_master
self.matrix = MatrixScanner(
cols=self.col_pins,
rows=self.row_pins,
diode_orientation=self.diode_orientation,
rollover_cols_every_rows=getattr(self, 'rollover_cols_every_rows', None),
swap_indicies=getattr(self, 'swap_indicies', None),
)
self._hid_helper_inst = self.hid_helper()
if self.debug_enabled:
print("Firin' lazers. Keyboard is booted.")
while True:
state_changed = False
if self.split_type is not None and self._master_half:
update = self._receive_from_slave()
if update is not None:
self._handle_matrix_report(update)
state_changed = True
update = self.matrix.scan_for_changes()
if update is not None:
if self._master_half():
self._handle_matrix_report(update)
state_changed = True
else:
# This keyboard is a slave, and needs to send data to master
self._send_to_master(update)
if self._state.hid_pending:
self._send_hid()
old_timeouts_len = len(self._state.timeouts)
self._state.process_timeouts()
new_timeouts_len = len(self._state.timeouts)
if old_timeouts_len != new_timeouts_len:
state_changed = True
if self._state.macros_pending:
# Blindly assume macros are going to change state, which is almost
# always a safe assumption
state_changed = True
for macro in self._state.macros_pending:
for key in macro(self):
self._send_key(key)
self._state.resolve_macro()
if self.debug_enabled and state_changed:
print('New State: {}'.format(self._state._to_dict()))
gc.collect()