-
Notifications
You must be signed in to change notification settings - Fork 487
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adds nullbitsco/nibble mapping with multiplex keymatrix support #991
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
Keyboard mapping for the [nullbits nibble](https://nullbits.co/nibble/). | ||
|
||
Copy `kb.py` and `main.py` to your top level CircuitPython folder beside the kmk folder. | ||
Edit the key mapping in `main.py` to match your keyboard layout. | ||
|
||
The Keyboard constructor supports an optional `encoder` argument (see `kb.py`). | ||
See the sample `main.py` for an example of how to configure the encoder. | ||
|
||
The RGB extension in the example requires a copy of `neopixel.py` in your top level | ||
CircuitPython folder: see https://github.com/KMKfw/kmk_firmware/blob/main/docs/en/rgb.md. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import board | ||
import digitalio | ||
|
||
from kmk.kmk_keyboard import KMKKeyboard as _KMKKeyboard | ||
from kmk.modules.encoder import EncoderHandler | ||
from kmk.quickpin.pro_micro.bitc_promicro import pinout as pins | ||
from kmk.scanners import DiodeOrientation | ||
from kmk.scanners.digitalio import MatrixScanner | ||
|
||
# bitc pro pinout: https://nullbits.co/static/img/bitc_pro_pinout.png | ||
# nibble/tidbit pinout: https://github.com/nullbitsco/docs/blob/main/nibble/build_guide_img/mcu_pinouts.png | ||
# key - diode mapping https://nullbits.co/static/file/NIBBLE_diode_key.pdf | ||
# row connects to anode end, col connects to cathode end | ||
|
||
# also defined: board.LED_RED, board.LED_GREEN, and board.LED_BLUE == board.LED | ||
row_pins = (pins[15], pins[14], pins[13], pins[12], pins[6]) # GPIO 22,20,23,21,4 | ||
col_mux_pins = (pins[19], pins[18], pins[17], pins[16]) # GPIO 29..26 | ||
encoder_pins = (pins[10], pins[11], None) # GPIO 8,9, button in key matrix | ||
pixel_pin = pins[9] # GPIO 7 | ||
# LED R, G, B pins: GPIO 6, 5, 3 | ||
# extension pins GPIO 11, 12, 13, 14 | ||
|
||
|
||
class KMKKeyboard(_KMKKeyboard): | ||
''' | ||
Create a nullbits nibble keyboard. | ||
optional constructor arguments: | ||
|
||
encoder=True if encoder installed | ||
then declare keyboard.encoders.map = [(KC.<left> , KC.<right>, None), (...)] | ||
''' | ||
|
||
pixel_pin = pixel_pin | ||
i2c = board.I2C # TODO ?? | ||
|
||
def __init__(self, encoder=False): | ||
super().__init__() | ||
|
||
self.matrix = MatrixScanner( | ||
col_mux_pins, | ||
row_pins, | ||
diode_orientation=DiodeOrientation.ROW2COL, # row is anode, col is cathode | ||
pull=digitalio.Pull.UP, | ||
multiplexed=True, | ||
) | ||
|
||
if encoder: | ||
self.encoders = EncoderHandler() | ||
self.encoders.pins = (encoder_pins,) | ||
self.modules.append(self.encoders) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
from kb import KMKKeyboard | ||
|
||
from kmk.extensions.media_keys import MediaKeys | ||
from kmk.extensions.rgb import RGB, AnimationModes | ||
from kmk.keys import KC | ||
|
||
keyboard = KMKKeyboard(encoder=True) # assume encoder installed | ||
keyboard.extensions.append(MediaKeys()) | ||
|
||
XXXXX = KC.NO | ||
|
||
# fmt: off | ||
keyboard.keymap = [ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would you mind formatting this in the usual "looks like keymatrix" way? Prefix the keymap definition with |
||
[ | ||
XXXXX, KC.ESC, KC.N1, KC.N2, KC.N3, KC.N4, KC.N5, KC.N6, KC.N7, KC.N8, KC.N9, KC.N0, KC.MINS,KC.EQL, KC.BKSP,KC.DEL, # noqa: E231 | ||
KC.MUTE,KC.TAB, KC.Q, KC.W, KC.E, KC.R, KC.T, KC.Y, KC.U, KC.I, KC.O, KC.P, KC.LBRC,KC.RBRC,KC.BSLS,KC.GRV, # noqa: E231 | ||
KC.F1, KC.CAPS,KC.A, KC.S, KC.D, KC.F, KC.G, KC.H, KC.J, KC.K, KC.L, KC.SCLN,KC.QUOT,KC.ENT, KC.ENT, KC.PGUP, # noqa: E231 | ||
KC.F2, KC.LSFT,KC.Z, KC.X, KC.C, KC.V, KC.B, KC.N, KC.M, KC.COMM,KC.DOT, KC.SLSH,KC.RSFT,XXXXX, KC.UP, KC.PGDN, # noqa: E231 | ||
KC.F3, KC.LCTL,KC.LCMD,KC.LALT,KC.SPC, KC.SPC, KC.SPC, KC.SPC, KC.SPC, KC.RCMD,KC.RALT,KC.RCTL,KC.LEFT,XXXXX, KC.DOWN,KC.RGHT, # noqa: E231 | ||
] | ||
] | ||
# fmt: on | ||
|
||
# note that encoder button is configured in the keymap (KC.MUTE above) so set to XXXXX here | ||
keyboard.encoders.map = [ | ||
((KC.VOLD, KC.VOLU, XXXXX),), # Layer 1, encoder 1 | ||
] | ||
|
||
rgb = RGB( | ||
pixel_pin=keyboard.pixel_pin, | ||
num_pixels=10, | ||
hue_default=180, | ||
sat_default=255, | ||
val_default=50, | ||
animation_mode=AnimationModes.BREATHING, | ||
animation_speed=3, | ||
breathe_center=2, | ||
) | ||
|
||
keyboard.extensions.append(rgb) | ||
|
||
if __name__ == '__main__': | ||
keyboard.go() |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,14 +6,14 @@ for example the number and location of encoders and double-size keys. | |
|
||
The Keyboard constructor supports a couple of optional arguments (see `kb.py`). | ||
|
||
If you're setting your tidbit up in landscape mode, | ||
with the USB connector at top right instead of top left, pass | ||
If you're setting your tidbit up in landscape mode, | ||
with the USB connector at top right instead of top left, pass | ||
`landscape_layout=True`. | ||
|
||
You can specify the active encoder positions by passing a list like | ||
`active_encoders=[0, 2]` which corresponds to the 1st and 3rd positions shown | ||
in [step 6](https://github.com/nullbitsco/docs/blob/main/tidbit/build_guide_en.md#6-optional-solder-rotary-encoder-led-matrix-andor-oled-display) of the build guide. | ||
The default is for a single encoder in either of the top two locations labeled 1 | ||
The default is for a single encoder in either of the top two locations labeled 1 | ||
in the build diagram, i.e. `active_encoders=[0]`. Pass an empty list if you skipped | ||
adding any encoders. | ||
|
||
|
@@ -27,7 +27,7 @@ from kmk.extensions.rgb import RGB, AnimationModes | |
keyboard = KMKKeyboard(active_encoders=[0], landscape_layout=True) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not correct. There is no There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is for the tidbit, but not for the nibble. See here: https://github.com/KMKfw/kmk_firmware/blob/main/boards/nullbitsco/tidbit/kb.py#L48 |
||
|
||
rgb = RGB( | ||
pixel_pin=keyboard.pixel_pin, | ||
pixel_pin=keyboard.pixel_pin, | ||
num_pixels=8, | ||
animation_mode=AnimationModes.BREATHING, | ||
animation_speed=3, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,8 +22,9 @@ def __init__( | |
rows, | ||
diode_orientation=DiodeOrientation.COL2ROW, | ||
pull=digitalio.Pull.UP, | ||
rollover_cols_every_rows=None, | ||
rollover_cols_every_rows=None, # with value k, treat k*r x c matrix as r x c*k | ||
offset=0, | ||
multiplexed=False, # 2^k outputs are multiplexed on k output pins | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not a big fan of packing even more conditional functionality into the already slow digitalio scanner. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I looked at Honestly I was a bit confused why If you prefer I could clone or subclass MatrixScanner to DemuxMatrixScanner instead? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Getting The Can you subclass without duplicating all of the code? I'm a bit short on time atm. and can't estimate that right now. |
||
): | ||
self.len_cols = len(cols) | ||
self.len_rows = len(rows) | ||
|
@@ -41,15 +42,16 @@ def __init__( | |
del unique_pins | ||
|
||
self.diode_orientation = diode_orientation | ||
self.multiplexed = multiplexed | ||
|
||
if self.diode_orientation == DiodeOrientation.COL2ROW: | ||
self.anodes = [ensure_DIO(x) for x in cols] | ||
self.cathodes = [ensure_DIO(x) for x in rows] | ||
self.translate_coords = True | ||
self.rows_are_inputs = True | ||
elif self.diode_orientation == DiodeOrientation.ROW2COL: | ||
self.anodes = [ensure_DIO(x) for x in rows] | ||
self.cathodes = [ensure_DIO(x) for x in cols] | ||
self.translate_coords = False | ||
self.rows_are_inputs = False | ||
else: | ||
raise ValueError(f'Invalid DiodeOrientation: {self.diode_orienttaion}') | ||
|
||
|
@@ -59,7 +61,7 @@ def __init__( | |
elif self.pull == digitalio.Pull.UP: | ||
self.outputs = self.cathodes | ||
self.inputs = self.anodes | ||
self.translate_coords = not self.translate_coords | ||
self.rows_are_inputs = not self.rows_are_inputs | ||
else: | ||
raise ValueError(f'Invalid pull: {self.pull}') | ||
|
||
|
@@ -69,6 +71,12 @@ def __init__( | |
for pin in self.inputs: | ||
pin.switch_to_input(pull=self.pull) | ||
|
||
if self.multiplexed: | ||
if self.rows_are_inputs: | ||
self.len_cols = 1 << self.len_cols | ||
else: | ||
self.len_rows = 1 << self.len_rows | ||
|
||
self.rollover_cols_every_rows = rollover_cols_every_rows | ||
if self.rollover_cols_every_rows is None: | ||
self.rollover_cols_every_rows = self.len_rows | ||
|
@@ -90,9 +98,17 @@ def scan_for_changes(self): | |
''' | ||
ba_idx = 0 | ||
any_changed = False | ||
|
||
for oidx, opin in enumerate(self.outputs): | ||
opin.value = self.pull is not digitalio.Pull.UP | ||
n = len(self.outputs) | ||
|
||
for oidx in range(n if not self.multiplexed else (1 << n)): | ||
if not self.multiplexed: | ||
opin = self.outputs[oidx] | ||
opin.value = self.pull is not digitalio.Pull.UP | ||
else: | ||
for bit, opin in enumerate(self.outputs): | ||
opin.value = (oidx & (1 << bit) != 0) != ( | ||
self.pull is not digitalio.Pull.UP | ||
) | ||
|
||
for iidx, ipin in enumerate(self.inputs): | ||
# cast to int to avoid | ||
|
@@ -110,7 +126,7 @@ def scan_for_changes(self): | |
old_val = self.state[ba_idx] | ||
|
||
if old_val != new_val: | ||
if self.translate_coords: | ||
if self.rows_are_inputs: | ||
new_oidx = oidx + self.len_cols * ( | ||
iidx // self.rollover_cols_every_rows | ||
) | ||
|
@@ -135,7 +151,8 @@ def scan_for_changes(self): | |
|
||
ba_idx += 1 | ||
|
||
opin.value = self.pull is digitalio.Pull.UP | ||
if not self.multiplexed: | ||
opin.value = self.pull is digitalio.Pull.UP | ||
if any_changed: | ||
break | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The encoder import belongs here, otherwise the encoder code is always loaded and wastes memory. Not an issue for this board, but a bad example to set.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, this entire code block should go in
main.py
, including the conditional import.