-
Shiny New Code
07/24/2021 at 23:07 • 0 commentsI fished this keyboard out of the proverbial drawer to try the new CircuitPython on it, with some of the new fancy features that have been added.
First of all, there is now a keypad module for scanning the key matrix in C — since it does it in the background using interrupts, it frees the Python code to display the LED animations much more smoothly. And it will never miss a key stroke.
Second, there is the new fancy way of enabling and disabling USB devices in boot.py, which means I can by default hide the CIRCUITPY drive and the serial console, and only show them if the upper left key was pressed while the keyboard was being connected. This makes it much easier to tweak the layouts and the code.
Finally, I decided to also test the new custom USB HID descriptors — in particular a descriptor for a "bitmap" keyboard, that lets you report any number of pressed keys, so called NKRO (n-key rollover). I will not lie, I'm not smart enough to come up with my own descriptor, so I copied it from the example generously provided by Jeff Epler. The boot.py file looks like this:
import board import digitalio import storage import usb_cdc import usb_hid bitmap_keyboard = usb_hid.Device( report_descriptor = ( b'\x05\x01\t\x06\xa1\x01\x85\xffu\x01\x95\x08\x05\x07\x19\xe0)\xe7\x15' b'\x00%\x01\x81\x02\x95\x05u\x01\x05\x08\x19\x01)\x05\x91\x02\x95\x01u' b'\x03\x91\x03\x95xu\x01\x15\x00%\x01\x05\x07\x19\x00)w\x81\x02\xc0' ), usage_page = 0x1, usage = 0x6, in_report_length = 16, out_report_length = 1, report_id_index = 7, ) row = digitalio.DigitalInOut(board.MOSI) col = digitalio.DigitalInOut(board.A6) col.switch_to_output(value=1) row.switch_to_input(pull=digitalio.Pull.DOWN) if not row.value: storage.disable_usb_drive() usb_cdc.disable() row.deinit() col.deinit() usb_hid.enable((bitmap_keyboard, usb_hid.Device.CONSUMER_CONTROL))
And the relevant piece of the keyboard handling code looks like this:
def send_nkro_report(self, pressed_keys): """Sends the USB HID NKRO keyboard report.""" report = bytearray(16) report_mod_keys = memoryview(report)[0:1] report_bitmap = memoryview(report)[1:] for code in pressed_keys: if code == 0: continue if code & 0xff00: report_mod_keys[0] |= (code & 0xff00) >> 8 if code & 0x00ff: report_bitmap[code >> 3] |= 1 << (code & 0x7) self.keyboard_device.send_report(report)
That's it. That's all it takes to make an NKRO keyboard in CircuitPython now.
Further research is required to automatically switch to the traditional HID keyboard when the host doesn't support the bitmap HID device — like, for example, when you go to your BIOS settings. But there are some promising prototypes for this already.
-
Macros
10/09/2020 at 11:56 • 0 commentsOver the last weekend I worked a little bit more on the code, cleaning it up and making it a library. I also added the ability of sending keys with modifiers, and media keys. And that means I have everything I need to turn this keyboard into a macro keyboard.
I programmed the bottom row to be numbers (for quickly switching things in games), then there are the function keys, the media keys (volume up/down, play/pause, next, prev, etc.), some of the rarely used keys (PrintScr, Pause and ScrollLock) and the top row switches my workspaces.
-
Simple Effect
10/03/2020 at 18:29 • 0 commentsFinally got some time to program the most basic keystroke effect:
The code for it is rather simple:
def update_lights(self): i = 0 y = 0 dt = time.monotonic() - self.key_time for x in range(10): self.leds[i] = self.light(x, y, dt) i += 1 y += 1 for x in range(9, -1, -1): self.leds[i] = self.light(x, y, dt) i += 1 y += 1 for x in range(10): self.leds[i] = self.light(x, y, dt) i += 1 y += 1 for x in range(9, -1, -1): self.leds[i] = self.light(x, y, dt) i += 1 self.leds.show() def light(self, x, y, dt): r2 = (x - self.key_x) ** 2 + (y - self.key_y) ** 2 c = 255 - min(255, r2 * 30 + (dt * 10) ** 2) return c, x * 25, min(255, y * 50)
The key_x, key_y and key_time are getting saved in the matrix scan routine when a key is detected to be pressed.
-
DotStars
10/01/2020 at 22:24 • 0 commentsFinally tonight I took a look at the lights on this keyboard. The Adafruit's DotStar library handles them very well, but for some reason the last 6 in a chain had different colors. After some experiments, I desoldered the switches, removed that LED, put a different one in its place and the problem magically disappeared.
I also wrote some very simple code to make a pretty gradient. Static for now. I'm going to call it a day, but tomorrow I will probably try to make some nice effects, like a water ripple on each keystroke, or something like that.
Oh, right, because both #Flounder Keyboard and #Turbot Keyboard are now dead and buried, I took the black double-shot key caps from them. I think they look quite pretty with the backlight.
-
Fork
09/21/2020 at 11:32 • 0 commentsI moved the 48k (actually 47 keys) version to #Dorsch 48k Keyboard because it has a different bill of materials and code.
I also just uploaded the design files of the 40k version with LEDs here.
-
How Thicc is your Keeb?
09/17/2020 at 12:12 • 0 commentsI also decided to shoot some thickness comparison photos:
From back to front:
- your standard mechanical keyboard
- low-profile mechanical keyboard from a shop
- Dorsch 48k
- Flounder
And a close-up of comparison with a standard mechanical keyboard:
So yeah, I think there is a definite improvement.
Why do I need a low-profile keyboard, though? It's simple. Ergonomics. You really don't want the heels of your hands to be pressing down on the table, leading to RSI, CTS or a number of other TLAs. With a low enough profile, your hands can just lie flat on the table without any special wrist rests or other contraptions, and be flat enough that it's the palms that bear the stress and not the heels.
-
Revisions
09/17/2020 at 11:41 • 0 commentsAfter using the keyboard for a few weeks (and getting much better at touch-typing in the process) I got very much used to it, so I was hesitating if I should scavenge it and rebuild it with the new PCBs that just arrived. And if I should, then which one?
In the end I decided to do it, and also to scavenge the #5plit Keyboard Clone, so that I can build both versions. The 48-key version went first:
Nothing surprising in here, I just had to edit the layout matrix to include the two extra columns, and it just works. I'm using it right now, and it works great for me. It's definitely a keeper. I didn't even need to add a stabilizer under the space. Oh, of course I reversed the D+ and D- labels for the USB cable, but that's easily fixed.
Then I came back to the smaller one, but with LEDs. Turns out that soldering by hand a string of 40 SMD APA102 LEDs is more work than I anticipated:
After several hours of looking for shorts, re-soldering, fixing bad joints and generally having a bad time, I finally got them all to light up:
And no, I have no idea why the last 6 of them are brighter. Probably a glitch in the matrix. You get it? Hahahaha. Anyways...
Adding the switches and some transparent key caps was just a formality:
Right now I left it with the keyboard code, but I'm thinking I could use it as a macro keyboard instead, with each key just sending a unique key combination when pressed. And of course with some fancy LED animations. But that's just all code.
I was considering putting those PCBs on Tindie as kits, with all the elements except switches and cable already soldered, but after debugging the LEDs, I think I will pass on that. I might do it with the bigger one without the LEDs, though. I wonder if there would be any interest in that?
-
Final Code
09/11/2020 at 15:17 • 0 commentsIn the process of moving the shift keys around, I have simplified the code somewhat, made it better at handling some corner cases, and fixed some bugs. Just in case someone wants to make a similar build, I'm putting it below. In the future it might grow into a proper CircuitPython library perhaps – then it will get its own repository.
---------- more ----------import board import digitalio import usb_hid import time COLS = (board.A6, board.A1, board.A4, board.A3, board.D6, board.SCL, board.SDA, board.D12, board.D10, board.D13) ROWS = (board.MOSI, board.AREF, board.D11, board.D5) LEDS = (board.A2, board.A5, board.TX, board.RX, board.D9) class Keyboard: def __init__(self, matrix, cols=COLS, rows=ROWS): self.matrix = matrix self.cols = [digitalio.DigitalInOut(pin) for pin in cols] self.rows = [digitalio.DigitalInOut(pin) for pin in rows] for col in self.cols: col.switch_to_output(value=0) for row in self.rows: row.switch_to_input(pull=digitalio.Pull.DOWN) for self.device in usb_hid.devices: if self.device.usage == 0x06 and self.device.usage_page == 0x01: break else: raise RuntimeError("no HID keyboard device") self.debounce = bytearray(len(cols)) self.last_state = bytearray(len(cols)) self.current_layer = 0 self.pressed_keys = set() self.last_held = 0 self.release_next = 0 def scan(self): if self.release_next: try: self.pressed_keys.remove(self.release_next) except KeyError: pass self.release_next = 0 for x, col in enumerate(self.cols): col.value = 1 debounce_bits = 0 for y, row in enumerate(self.rows): state = row.value debounce_bits |= state << y if state != bool(self.debounce[x] & (1 << y)): continue last_state = bool(self.last_state[x] & (1 << y)) if state: self.last_state[x] |= 1 << y else: self.last_state[x] &= ~(1 << y) if state == last_state: continue if state: self.press(x, y) else: self.release(x, y) col.value = 0 self.debounce[x] = debounce_bits def press(self, x, y): if self.last_held: self.last_held = 0 code = self.matrix[self.current_layer][y][x] if code & 0xff00: if (code & 0xff) != 0: self.last_held = code if code & 0xff00 == 0x0800: self.current_layer = 1 else: self.pressed_keys.add(code & 0xff00) return code = self.matrix[self.current_layer][y][x] self.pressed_keys.add(code) def release_all(self, x, y): for layer in 0, 1: for mask in 0xffff, 0xff00, 0x00ff: code = self.matrix[layer][y][x] & mask try: self.pressed_keys.remove(code) except KeyError: pass if code & 0xff00 == 0x0800: self.current_layer = 0 def release(self, x, y): for layer in 0, 1: code = self.matrix[self.current_layer][y][x] if self.last_held == code: self.release_all(x, y) self.pressed_keys.add(code & 0xff) self.release_next = code & 0xff self.last_held = 0 return self.release_all(x, y) def send_report(self, pressed_keys): report = bytearray(8) report_mod_keys = memoryview(report)[0:1] report_no_mod_keys = memoryview(report)[2:] keys = 0 for code in pressed_keys: if code == 0: continue elif code == 0x0800: continue elif code & 0xff00 and code & 0xff == 0: modifier = (code >> 8) - 1 report_mod_keys[0] |= 1 << modifier elif keys < 6: report_no_mod_keys[keys] = code keys += 1 self.device.send_report(report) def run(self): last_pressed_keys = set() while True: self.scan() if self.pressed_keys != last_pressed_keys: self.send_report(self.pressed_keys) last_pressed_keys = set(self.pressed_keys) time.sleep(0.01)
-
Better Shifts and More Variations
08/30/2020 at 22:23 • 0 commentsI'm using this as my main keyboard now, and there are still three issues that I struggle a bit with. The first one is with the shift key — it's too low and only one, which means it's awkward to press it with certain key combinations. Second problem is with the Ctrl key not being in the corner, and the last with the Backspace being in a completely wrong place, next to the Space. I managed to solve the first two today by remembering that I can do hold/tap with any key, not just the bottom row, and by moving the Shift to the Z and Quote keys, moving Ctrl to the freed place, and adding a Super key. The new layout now looks like this:
But before I came up with this, I tried to fix it by adding two more columns for the shifts and other control keys, arriving at something very similar to the Planck layout:
Note that this keyboard no longer needs the hold/tap mechanism, as it has enough keys for modifiers to have their own dedicated keys. Anyways, I designed and ordered a PCB for this new layout, so I might be trying it later this year:
Another alternative design for this keyboard (which I designed but didn't order yet) is a version with RGB LEDs under every key. Someone observed on Twitter that it would be nice to have a Python-programmable keyboard with lights that you could control with your own program.
It was a bit of work to fit that string of 40 APA102s on there, but I managed. I'm not sure if I want to actually build that version myself, though — I guess I don't enjoy keyboard lights so much, I'm a boring person. Maybe if it was programmed as a MIDI device, which is perfectly doable with CircuitPython…
-
Small Fixes
08/28/2020 at 20:38 • 0 commentsAfter using the keyboard for a little bit, I added a few little fixes. I already mentioned moving some of the symbols keys to make them easier to access — the apostrophe is more common in English than the question mark or slash, so it makes sense to make it its own key. I also played a bit with key caps, just to make it a little bit nicer:
The second fix is the ability to use modifier keys with your mouse. Previously, when you held down a top/hold key, it wouldn't do anything until you released it or pressed any other key with it — then it would decide whether to send the modifier key code or the regular key code. But that makes it impossible to use this keyboard for things like ctrl-clicking or shift-dragging with your mouse. I initially tried to add a timeout to the hold function, but that didn't work very well — too slow for fast mouse clicking. Finally I made it so that it sends the modifier key as soon as you press the hold/tap key down, but if it turns out that you meant to tap it, it will release the modifier key before sending anything else. That way you get some extra spurious modifier key presses, but they normally do nothing, so it should be fine.