

Custom wireless split ergonomic keyboard — PCB, case, CircuitPython firmware, companion tooling
- Size
- 393.6 × 142.2 mm
- Copper layers
- 2
- Thickness
- 1.6 mm
- Parts
- 156
- Nets
- 118
- Pads
- 678
- Tracks, vias
- 835, 76
- Routed
- Yes
- KiCad
- KiCad 9.0 (file 20241229)
- Created
- 5 Sep 2024
- Updated
- 20 May 2026
Overview
Roki is an open-source printed circuit board by rodrigokimura, from the rodrigokimura/roki repository on GitHub, published under the MIT licence. It is a 2-layer board of 393.6 × 142.2 mm with 156 parts on 118 nets, routed with 835 tracks and 76 vias.
Its parts include 62 switches, 60 diodes and 4 ICs. It belongs with the nRF52 and nRF5x boards of this library.
README
README.mdRoKi
An ergonomic, wireless, split mechanical keyboard — hardware, firmware and tooling in one repo.
RoKi (from the author's surname) is a custom, from-scratch split keyboard project. It covers the full stack: PCB design, 3D-printable case parts, CircuitPython firmware running on the nice!nano v2 (wireless Bluetooth Low Energy controller), and a companion Python CLI / web UI / TUI to flash, configure and debug the board.
⚠️ Status: Work in progress. Breaking changes are expected.
Features
- Split layout with BLE-based inter-half communication (no TRRS cable).
- 5 rows × 6 columns per half, diode matrix.
- Thumb cluster with analog thumbstick (for pointer / scroll / layer control).
- Rotary encoder on each half.
- Piezo buzzer for audio feedback.
- Layer system with custom
layer_handler(tap / hold / toggle commands). - HID support for keyboard, mouse and consumer-control (media) keys, via
adafruit_hid. - Per-side configuration (primary / secondary) selected via environment.
- Runtime calibration of the analog thumbstick (deadzone, center, range).
- Read-only filesystem toggle at boot, driven by a hardware switch, so you can hot-swap between "keyboard mode" and "developer mode" without rebuilding firmware.
Repository layout
roki/
├── roki/ # Python package
│ ├── firmware/ # CircuitPython firmware (runs on nice!nano v2)
│ │ ├── boot.py # Early boot: filesystem readonly switch
│ │ ├── code.py # Main entry point, pin mapping
│ │ ├── kb.py # Primary / Secondary keyboard classes
│ │ ├── keys.py # HID key definitions and handlers
│ │ ├── layer_handler.py # Layer / Command system
│ │ ├── calibration.py # Thumbstick calibration
│ │ ├── config.py # Runtime config model + JSON loader
│ │ ├── service.py # BLE service for half-to-half comms
│ │ ├── buzzer.py # Audio feedback
│ │ ├── logging.py # Color-coded adafruit_logging formatter
│ │ ├── params.py # Env-based parameter singleton
│ │ └── config.json # Default key map / layers
│ ├── cli/ # Host-side CLI (typer)
│ │ ├── app.py # `roki` command entry point
│ │ ├── utils.py # Mount / unmount / lib install helpers
│ │ ├── file_management.py
│ │ ├── html_generator.py
│ │ └── logging.py
│ ├── tui/ # Textual-based TUI key-map configurator
│ │ ├── app.py
│ │ ├── screens/Hardware
| Item | Choice |
|---|---|
| Microcontroller | nice!nano v2 (nRF52840, BLE) |
| Layout | Split, 5×6 + thumb cluster + encoder per half |
| Switches | MX-compatible, hotswap |
| Inter-half comms | Bluetooth Low Energy (custom GATT service) |
| Power | Li-Po, per-half power switch |
| Analog input | Thumbstick (X / Y + push) |
PCB sources live in kicad/roki/. Case, keycaps and mechanical parts live in freecad/ as .FCStd files.
Firmware (CircuitPython)
Runs on CircuitPython for nRF52840. The firmware is structured around a Primary / Secondary pair of keyboard halves:
- Primary holds the full config, scans its own matrix, talks HID over BLE to the host, and receives key events from the secondary over a custom BLE service.
- Secondary scans its matrix and forwards events to the primary.
The side is chosen at boot via the IS_LEFT_SIDE environment variable (or the boot.py switch pin), so the same code runs on both halves.
Pin mapping
Defined in roki/firmware/code.py:
Roki(
row_pins=("P0_24", "P1_00", "P0_11", "P1_04", "P1_06"),
column_pins=("P0_09", "P0_10", "P1_11", "P1_13", "P1_15", "P0_02"),
buzzer_pin="P0_06",
thumb_stick_pins=("P0_22", "AIN7", "AIN5"), # switch, X, Y
encoder_pins=("P0_17", "P0_20"),
)See the nice!nano pinout & schematic for reference.
Boot-time write protection
boot.py reads pin P0_22 on startup. When the switch is open the filesystem is mounted read-only (normal keyboard mode); when closed, read-write — useful for editing files over USB without your OS fighting CircuitPython.
Host-side tooling
A typer CLI is installed as roki:
roki upload # (alias: u) Flash firmware + libs to the connected board
roki run # Launch the TUI key-map configurator
roki serve # Start the FastAPI + Jinja web config UI
roki generate # (alias: g) Generate static HTML / artifacts
roki config # Show / edit config.jsonInstall
uv sync(Uses uv as the package manager; Python 3.12+.)
Run the tests
uv run pytestWith coverage:
uv run pytest --cov=roki --cov-report=termCurrent coverage: 88% across ~2.3k lines of source with 109 tests.
Run the web configurator locally (HTTPS)
# Generate self-signed certs once
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365
uv run roki serveEnvironment variables
Set in roki/firmware/settings.toml on the board, or via your shell for host-side runs:
| Variable | Default | Meaning |
|---|---|---|
IS_LEFT_SIDE | 1 | 1 for primary (left), 0 for secondary |
DEBUG | 0 | 1 enables the stream logger |
LOG_LEVEL | 0 | Standard logging levels (10 = DEBUG, etc.) |
Development workflow
This repo uses commitizen for Conventional Commits and ruff for linting / import sorting.
uv run cz commit # interactive commit
uv run ruff check --fix . # lint + autofix
uv run ruff format . # formatPre-commit hooks are configured in .pre-commit-config.yaml.
License
MIT © Rodrigo Eiti Kimura
Parts
156| Qty | Value | Footprint | References |
|---|---|---|---|
| 60 | Diode | ScottoKeebs_Components:Diode_SOD-123 | D1, D2, D10, D11, D12, D13, D14, D15, D16, D17, D18, D19, D20, D21, D22, D23 +44 |
| 58 | Keyswitch | ScottoKeebs_Hotswap:Hotswap_MX_1.00u | S1, S2, S10, S11, S12, S13, S14, S15, S16, S17, S18, S19, S20, S21, S22, S23 +42 |
| 2 | Battery_Cell | Battery:BatteryHolder_MPD_BH-18650-PC2 | BT1, BT2 |
| 2 | Buzzer | Buzzer_Beeper:Buzzer_TDK_PS1240P02BT_D12.2mm_H6.5mm | BZ1, BZ2 |
| 2 | Nice_Nano_V2 | ScottoKeebs_MCU:Nice_Nano_V2 | U1, U4 |
| 2 | RotaryEncoder_Switch | Rotary_Encoder:RotaryEncoder_Alps_EC11E-Switch_Vertical_H20mm | SW1, SW3 |
| 2 | SW_SPDT | Button_Switch_SMD:SW_SPDT_PCM12 | SW2, SW4 |
| 2 | ~ | Library:thumb_stick | U2, U3 |
The 130 most common of 156 parts.
Questions
How big is the Roki PCB?
It measures 393.6 × 142.2 mm and has 2 copper layers, 1.6 mm thick.
What parts does the Roki use?
156 parts. The full list of its parts is on this page, with links to each part's symbol and footprint where PCBKit's library has them.
Where can I download the Roki KiCad files?
From the rodrigokimura/roki repository on GitHub, at the commit this page was made from: the board is kicad/roki/roki.kicad_pcb and its schematic kicad/roki/roki.kicad_sch. PCBKit opens the board in the browser, with nothing to install.
Can I use the Roki design in my own project?
Its repository publishes it under the MIT licence: you may reuse it on that licence's terms (keep the author's notice, and for a copyleft licence share your changes alike).
Similar boards
- 2 layers65 × 100 mm

- 2 layers85 × 56.5 mm

- 2 layers109 × 71 mm

- 2 layers313.3 × 131.6 mm

KBIC65
b-karl/KBIC65
65% keyboard PCB with ProMicro footprint. Through-hole kit design with focus on wireless.
156 parts67MIT - 2 layers191.2 × 129.5 mm

- 2 layers361 × 120.7 mm

Trifecta
swiftrax/Trifecta
Open source Alice like mechanical keyboard with arrow keys and support of up to 3 rotary encoders
156 parts32GPL-2.0 - 2 layers90 × 38 mm

Kampela
Alzymologist/Kampela
Hardware signer and key storage for Substrate ecosystem
156 parts21GPL-3.0 - 2 layers100 × 100 mm
