Text Editor
Adafruit recommends using the Mu editor for editing your CircuitPython code. You can get more info in this guide.
Alternatively, you can use any text editor that saves simple text files.
Download the Project Bundle
Your project will use a specific set of CircuitPython libraries and the code.py file. To get everything you need, click on the Download Project Bundle link below, and uncompress the .zip file.
Hook your QT Py/board to your computer via a known good USB data+power cable. It should show up as a thumb drive named CIRCUITPY.
Using File Explorer/Finder (depending on your Operating System), drag the contents of the uncompressed bundle directory onto your board's CIRCUITPY drive, replacing any existing files or directories with the same names, and adding any new ones that are necessary.
Continue below the program listing for a breakdown of how the program functions, section by section.
# SPDX-FileCopyrightText: 2022 Jeff Epler for Adafruit Industries # SPDX-License-Identifier: MIT import array import time import board import rp2pio import usb_hid from keypad import Keys from adafruit_hid.consumer_control import ConsumerControl from adafruit_hid.keyboard import Keyboard from adafruit_hid.keyboard import Keycode from adafruit_hid.mouse import Mouse from adafruit_pioasm import Program from adafruit_ticks import ticks_add, ticks_less, ticks_ms from next_keycode import ( cc_value, is_cc, next_modifiers, next_scancodes, shifted_codes, shift_modifiers, ) # Compared to a modern mouse, the DPI of the NeXT mouse is low. Increasing this # number makes the pointer move further faster, but it also makes moves chunky. # Customize this number according to the trade-off you want, but also check # whether your operating system can assign a higher "sensitivity" or # "acceleration" for the mouse. MOUSE_SCALE = 8 # Customize the power key's keycode. You can change it to `Keycode.POWER` if # you really want to accidentally power off your computer! POWER_KEY_SENDS = Keycode.F1 # according to https://journal.spencerwnelson.com/entries/nextkb.html the # keyboard's timing source is a 455MHz crystal, and the serial data rate is # 1/24 the crystal frequency. This differs by a few percent from the "50us" bit # time reported in other sources. NEXT_SERIAL_BUS_FREQUENCY = round(455_000 / 24) pio_program = Program( """ top: set pins, 1 pull block ; wait for send request out x, 1 ; trigger receive? out y, 7 ; get count of bits to transmit (minus 1) bitloop: out pins, 1 [7] ; send next bit jmp y--, bitloop [7] ; loop if bits left to send set pins, 1 ; idle the bus after last bit jmp !x, top ; to top if no scancode expected set pins, 1 ; mark bus as idle so keyboard will send set y, 19 ; 20 bits to receive wait 0, pin 0 [7] ; wait for falling edge plus half bit time recvloop: in pins, 1 [7] ; sample in the middle of the bit jmp y--, recvloop [7] ; loop until all bits read push ; send report to CircuitPython """ ) def pack_message(bitcount, data, trigger_receive=False): if bitcount > 24: raise ValueError("too many bits in message") trigger_receive = bool(trigger_receive) message = ( (trigger_receive << 31) | ((bitcount - 1) << 24) | (data << (24 - bitcount)) ) return array.array("I", [message]) def pack_message_str(bitstring, trigger_receive=False): bitcount = len(bitstring) data = int(bitstring, 2) return pack_message(bitcount, data, trigger_receive=trigger_receive) def set_leds(i): return pack_message_str(f"0000000001110{i:02b}0000000") QUERY = pack_message_str("000001000", True) MOUSEQUERY = pack_message_str("010001000", True) RESET = pack_message_str("0111101111110000000000") BIT_BREAK = 1 << 11 BIT_MOD = 1 def is_make(report): return not bool(report & BIT_BREAK) def is_mod_report(report): return not bool(report & BIT_MOD) def extract_bits(report, *positions): result = 0 for p in positions: result = (result << 1) if report & (1 << p): result |= 1 #result = (result << 1) | bool(report & (1<<p)) return result # keycode bits are backwards compared to other information sources # (bit 0 is first) def keycode(report): return extract_bits(report, 12, 13, 14, 15, 16, 17, 18) def modifiers(report): return (report >> 1) & 0x7F sm = rp2pio.StateMachine( pio_program.assembled, first_in_pin=board.MISO, pull_in_pin_up=1, first_set_pin=board.MOSI, set_pin_count=1, first_out_pin=board.MOSI, out_pin_count=1, frequency=16 * NEXT_SERIAL_BUS_FREQUENCY, in_shift_right=False, wait_for_txstall=False, out_shift_right=False, **pio_program.pio_kwargs, ) def signfix(num, sign_pos): """Fix a signed number if the bit with weight `sign_pos` is actually the sign bit""" if num & sign_pos: return num - 2*sign_pos return num class KeyboardHandler: def __init__(self): self.old_modifiers = 0 self.cc = ConsumerControl(usb_hid.devices) self.kbd = Keyboard(usb_hid.devices) self.mouse = Mouse(usb_hid.devices) def set_key_state(self, key, state): if state: if isinstance(key, tuple): old_report_modifier = self.kbd.report_modifier[0] self.kbd.report_modifier[0] = 0 self.kbd.press(*key) self.kbd.release_all() self.kbd.report_modifier[0] = old_report_modifier else: self.kbd.press(key) else: if isinstance(key, tuple): pass else: self.kbd.release(key) def handle_mouse_report(self, report): if report == 1536: # the "nothing happened" report return dx = extract_bits(report, 11,12,13,14,15,16,17) dx = -signfix(dx, 64) dy = extract_bits(report, 0,1,2,3,4,5,6) dy = -signfix(dy, 64) b1 = not extract_bits(report, 18) b2 = not extract_bits(report, 7) self.mouse.report[0] = ( Mouse.MIDDLE_BUTTON if (b1 and b2) else Mouse.LEFT_BUTTON if b1 else Mouse.RIGHT_BUTTON if b2 else 0) if dx or dy: self.mouse.move(dx * MOUSE_SCALE, dy * MOUSE_SCALE) else: self.mouse._send_no_move() # pylint: disable=protected-access def handle_report(self, report_value): if report_value == 1536: # the "nothing happened" report return # Handle modifier changes mods = modifiers(report_value) changes = self.old_modifiers ^ mods self.old_modifiers = mods for i in range(7): bit = 1 << i if changes & bit: # Modifier key pressed or released self.set_key_state(next_modifiers[i], mods & bit) # Handle key press/release code = next_scancodes.get(keycode(report_value)) if mods & shift_modifiers: code = shifted_codes.get(keycode(report_value), code) make = is_make(report_value) if code: if is_cc(code): if make: self.cc.send(cc_value(code)) else: self.set_key_state(code, make) keys = Keys([board.SCK], value_when_pressed=False) handler = KeyboardHandler() recv_buf = array.array("I", [0]) time.sleep(0.1) sm.write(RESET) time.sleep(0.1) for _ in range(4): sm.write(set_leds(3)) time.sleep(0.1) sm.write(set_leds(0)) time.sleep(0.1) print("Keyboard ready!") try: while True: if (event := keys.events.get()): handler.set_key_state(POWER_KEY_SENDS, event.pressed) sm.write(QUERY) deadline = ticks_add(ticks_ms(), 100) while ticks_less(ticks_ms(), deadline): if sm.in_waiting: sm.readinto(recv_buf) value = recv_buf[0] handler.handle_report(value) break else: print("keyboard did not respond - resetting") sm.restart() sm.write(RESET) time.sleep(0.1) sm.write(MOUSEQUERY) deadline = ticks_add(ticks_ms(), 100) while ticks_less(ticks_ms(), deadline): if sm.in_waiting: sm.readinto(recv_buf) value = recv_buf[0] handler.handle_mouse_report(value) break else: print("keyboard did not respond - resetting") sm.restart() sm.write(RESET) time.sleep(0.1) finally: # Release all keys before e.g., code is reloaded handler.kbd.release_all()

How it works
Preliminaries
Note how one of the import
lines imports from next_keycode
. This file, included alongside code.py in the project bundle, has the long list of correspondences between NeXT keyboard codes and USB keyboard codes.
After the required import
lines, there is a line where you can customize the USB HID code sent by the power button. The guide defaults it to F1, because unexpectedly powering off a computer is not so great. Note that the button does not work to power on the computer, either, no matter how you configure it:
# SPDX-FileCopyrightText: 2022 Jeff Epler for Adafruit Industries # SPDX-License-Identifier: MIT import array import time import board import rp2pio import usb_hid from keypad import Keys from adafruit_hid.consumer_control import ConsumerControl from adafruit_hid.keyboard import Keyboard from adafruit_hid.keyboard import Keycode from adafruit_pioasm import Program from adafruit_ticks import ticks_add, ticks_less, ticks_ms from next_keycode import ( cc_value, is_cc, next_modifiers, next_scancodes, shifted_codes, shift_modifiers, ) # Customize the power key's keycode. You can change it to `Keycode.POWER` if # you really want to accidentally power off your computer! POWER_KEY_SENDS = Keycode.F1
Next, the code defines a program for the PIO peripheral to actually communicate with the keyboard.
A message can be a varying number of bits, and it can optionally call for a response from the keyboard. The PIO program expects this number first, accompanied by the bits to be sent.
Then, if the "response" bit is set, it will await a response from the keyboard. A response is always treated as consisting of 20 bits.
# according to https://journal.spencerwnelson.com/entries/nextkb.html the # keyboard's timing source is a 455MHz crystal, and the serial data rate is # 1/24 the crystal frequency. This differs by a few percent from the "50us" bit # time reported in other sources. NEXT_SERIAL_BUS_FREQUENCY = round(455_000 / 24) pio_program = Program( """ top: set pins, 1 pull block ; wait for send request out x, 1 ; trigger receive? out y, 7 ; get count of bits to transmit (minus 1) bitloop: out pins, 1 [7] ; send next bit jmp y--, bitloop [7] ; loop if bits left to send set pins, 1 ; idle the bus after last bit jmp !x, top ; to top if no scancode expected set pins, 1 ; mark bus as idle so keyboard will send set y, 19 ; 20 bits to receive wait 0, pin 0 [7] ; wait for falling edge plus half bit time recvloop: in pins, 1 [7] ; sample in the middle of the bit jmp y--, recvloop [7] ; loop until all bits read push ; send report to CircuitPython """ )
The next block of code is concerned with constructing messages to send to the keyboard. QUERY
and RESET
messages are defined for use later, as well as a function to set the keyboard LEDs:
def pack_message(bitcount, data, trigger_receive=False): if bitcount > 24: raise ValueError("too many bits in message") trigger_receive = bool(trigger_receive) message = ( (trigger_receive << 31) | ((bitcount - 1) << 24) | (data << (24 - bitcount)) ) return array.array("I", [message]) def pack_message_str(bitstring, trigger_receive=False): bitcount = len(bitstring) data = int(bitstring, 2) return pack_message(bitcount, data, trigger_receive=trigger_receive) def set_leds(i): return pack_message_str(f"0000000001110{i:02b}0000000") QUERY = pack_message_str("000001000", True) MOUSEQUERY = pack_message_str("010001000", True) RESET = pack_message_str("0111101111110000000000")
When a key event is sent back to CircuitPython, we need to decode it. These constants and functions help with that task, allowing the keycodes, modifiers (keys like CTRL and ALT which don't directly generate characters), and make/break state to be checked:
BIT_BREAK = 1 << 11 BIT_MOD = 1 def is_make(report): return not bool(report & BIT_BREAK) def is_mod_report(report): return not bool(report & BIT_MOD) def extract_bits(report, *positions): result = 0 for p in positions: result = (result << 1) if report & (1 << p): result |= 1 #result = (result << 1) | bool(report & (1<<p)) return result # keycode bits are backwards compared to other information sources # (bit 0 is first) def keycode(report): return extract_bits(report, 12, 13, 14, 15, 16, 17, 18) def modifiers(report): return (report >> 1) & 0x7F
The power button is connected directly to one of the wires, so read it like a 1-key keypad instead:
keys = Keys([board.SCK], value_when_pressed=False)
There is also code concerned with decoding mouse messages, extracting the correct bits and assembling them into signed "delta" (movement) values for X and Y. It treats "left+right" button as middle button, which was a common thing on Linux computers with two button mice back in the 90s. Another useful thing to do when both buttons are pressed would be to look at the Y movement and send wheel motion instead of regular motion, which is pretty straightforward with the HID library.
def handle_mouse_report(self, report): if report == 1536: # the "nothing happened" report return dx = extract_bits(report, 11,12,13,14,15,16,17) dx = -signfix(dx, 64) dy = extract_bits(report, 0,1,2,3,4,5,6) dy = -signfix(dy, 64) b1 = not extract_bits(report, 18) b2 = not extract_bits(report, 7) self.mouse.report[0] = ( Mouse.MIDDLE_BUTTON if (b1 and b2) else Mouse.LEFT_BUTTON if b1 else Mouse.RIGHT_BUTTON if b2 else 0) if dx or dy: self.mouse.move(dx * MOUSE_SCALE, dy * MOUSE_SCALE) else: self.mouse._send_no_move() # pylint: disable=protected-access
Reset the keyboard, then blink the LEDs to show that everything's working:
time.sleep(0.1) sm.write(RESET) time.sleep(0.1) for _ in range(4): sm.write(set_leds(3)) time.sleep(0.1) sm.write(set_leds(0)) time.sleep(0.1) print("Keyboard ready!")
In the forever loop, check for keyboard messages as well as for the independent power key. Act on each value as appropriate. When the program is stopped (for instance, because you updated code.py), use a 'finally
clause' to release all keys before actually exiting:
try: while True: if (event := keys.events.get()): handler.set_key_state(POWER_KEY_SENDS, event.pressed) sm.write(QUERY) deadline = ticks_add(ticks_ms(), 100) while ticks_less(ticks_ms(), deadline): if sm.in_waiting: sm.readinto(recv_buf) value = recv_buf[0] handler.handle_report(value) break else: print("keyboard did not respond - resetting") sm.restart() sm.write(RESET) time.sleep(0.1) sm.write(MOUSEQUERY) deadline = ticks_add(ticks_ms(), 100) while ticks_less(ticks_ms(), deadline): if sm.in_waiting: sm.readinto(recv_buf) value = recv_buf[0] handler.handle_mouse_report(value) break else: print("keyboard did not respond - resetting") sm.restart() sm.write(RESET) time.sleep(0.1) finally: # Release all keys before e.g., code is reloaded handler.kbd.release_all()
Until you've wired up the keyboard, CircuitPython won't do anything but show the message "keyboard did not respond - resetting" in the Serial REPL. You'll need to actually connect your keyboard to the QT Py as detailed on the following page.