Klavgen handwired keyboard generator
Klavgen is a handwired keyboard generator written in Python, on top of CadQuery. It produces keyboards (like on the left) and makes handwiring easy and organized via switch holders (on the right):
Note: Klavgen is heavily work-in-progress and lots of stuff mey not be working. If you experience issues, please submit an Github issue.
Benefits of building a keyboard with Klavgen:
- Generates all the shells and support structures you need for a keyboard, in a way that’s optimized for home FDM printing with no supports
- Is low height: only 11 mm in the default configuration)
- Uses Kailh hotswap sockets
- Makes it easy to solder things, and requires no glue whatsoever
- Uses uninsulated wires, thus no tedious wire stripping
- Makes it easy to reposition the keys after creation
- Produces a sturdy build that doesn’t flex when typed on
The last 5 benefits are achieved by using switch holders which organize the wires, hold the Kailh sockets and the diodes, support the switches, enable soldering everything in place, and allow moving keys later (they even have wire slack so you can space keys further apart).
As of now:
- Code is still quite messy and not fully parameterized
- Configuring a keyboard requires code, it’s not as easy as YAML
- Docs are scarse (as you’ll see below)
Example generated keyboards
Klavyl
Klavyl is my personal keyboard, a modification to Redox. It uses most of Klavgen’s capabilities, including screw holes, manually defined case outlines, Pro Micro and TRRS jack cutouts and holders, and palm rests. Check out the repo for the full config.
Redox
The Redox keyboard, as generated by Klavgen based on its Keyboard Layout Editor config:
Ergodox
The Ergodox keyboard, as generated by Klavgen based on its Keyboard Layout Editor config:
Atreus
The Atreus keyboard, as generated by Klavgen based on its Keyboard Layout Editor config:
Installing
1. Install CadQuery
Install Miniconda and create an environment for CadQuery, following the official instructions.
2. Get comfortable using CadQuery
Get familiar with either of CadQuery’s UIs: CQ-editor or jupyter-cadquery (which I prefer).
Ensure that you know how to view objects returned from code, using show_object()
in CQ-editor or show()
from jupyter_cadquery.cadquery
.
3. Download Klavgen
Klavgen is not yet packaged, so you need to download and use it locally within a project (clone or download and extract the ZIP file; example assumes cloning).
git clone [email protected]:klavgen/klavgen.git
Start your project into the klavgen
repo directory. This is where you’ll have to write your scripts for now, since you need to import the klavgen
package (whic is in a sub-dir):
WARNING!!!
CadQuery is very finicky when it comes to filleting (the fillet()
command), and sometimes when it comes to shelling (the shell()
command). Klavgen tries to provide helpful messages when errors around these are provided, please follow them. In general, it is highly recommended to always use a Patch
object to define the outline of your case without small features.
If rendering fails in a shell, you should try render_case()
with debug=True
, which skips the shell step.
The 2 most important fillet settings, CaseConfig.side_fillet
and CaseConfig.palm_rests_top_fillet
are disabled by default, which makes default keyboards ugly.
These are the main gotchas:
- Shelling fails because your case is not continuous (e.g. the thumb cluster doesn’t connect to the main body), has very sharp corners, or has small features.
- Filleting fails because the object has features smaller than the fillet size.
- Fillering succeeds but produces an invalid shape that later on causes erros when operated on (e.g. via a
.cut()
).
Tutorial
Note that all the show()
commands below are meant to illustrate the use in Jupyter Lab. If you use CQ-editor, the commands may differ.
1. Generate a 1-key keyboard case
Run the following code to generate a single-key keyboard:
from klavgen import *
keys = [
Key(x=0, y=0, keycap_width=18, keycap_depth=18),
]
case_result = render_case(keys=keys)
The render_case()
method is the method you’ll use most often while designing the keybaord. It returns a fairly complex RenderCaseResult
object which tracks the full progress of the keyboard construction to help you in fixing issues.
Note: You can pass in a RenderCaseResult
object as the result
parameter. Then, render_case()
will save intermediate objects to the passed-in variable while rendering is taking place, allowing you to observe what happend if rendering raises an exception.
Here, we’ll only focus on these 3 keys part in the result
object:
case_result.top
is the top plate (show(case_result.top)
)
case_result.bottom
is the case bottom (show(case_result.bottom)
)
case_result.debug
shows key and holder outlines, helping you debug designs. This part should not be saved or used.
Rendering all 3 shows you the bottom, top and the debug outline in the air ((show(case_result.top, case_result.bottom, case_result.debug)
):
2. Save the 1-key keyboard case
You can now export the dummy “keyboard” you created, via the render_and_save_keyboard()
method, which takes the same parameters as render_case()
.
keyboard_result = render_and_save_keyboard(keys=keys)
Now you should see a series of .stl
files in your working directly. The files depend on the features your keyboard uses. For now you will only see the top plate, the case bottom, and the switch holder. For a complete keyboard, you may also see the controller holder, the TRRS jack holder, palm rest(s), and the palm rests connector.
The render_and_save_keyboard()
method renders the case (and takes the same parameters as render_case()
) and then saves all relevant components to .stl
files. It returns an object of type RenderKeyboardResult
. In the current example, that object contains only the .case_result
, .top
, .bottom
, and .switch_holder
items. See below for the full list of members.
3. Generate a more complex case
This is a more complex “keyboard”, with screw holes and a palm rest:
config = Config()
keys = [
Key(x=0, y=0, keycap_width=18, keycap_depth=18)
]
screw_holes = [
ScrewHole(x=13, y=13),
ScrewHole(x=13, y=-13),
ScrewHole(x=-13, y=-13),
]
palm_rests = [
PalmRest(
points=[
(13, -5),
(13, -30),
(-13, -30),
(-13, -5)
],
height=config.case_config.case_base_height + 2,
connector_locations_x = [0]
),
]
case_result = render_case(keys=keys, palm_rests=palm_rests, screw_holes=screw_holes, config=config)
This time, besides the case_result.top
and case_result.bottom
objects, you can also check out the case_result.palm_rests[0]
one to see the palm rest.
This is the result (show(result.top, result.bottom, result.palm_rests[0])
):
Note that the palm rest is detachable. You can make it part of the bottom by changing the config to:
config = Config(case_config=CaseConfig(detachable_palm_rests=False))
Now the case_result.palm_rests
object is None
so don’t try to view it. Instead just check out the case bottom (show(result.bottom)
):
4. Generate a keyboard with all of Klavgen’s features
Check out the example.py
file, which builds a dummy keyboard using all of Klavgen’s features.
This is the result (show(keyboard_result.top, keyboard_result.bottom, keyboard_result.palm_rests[0])
):
Extras
Use a Keyboard Layout Editor export
You can build a keyboard from a Keyboard Layout Editor layout. Do the followingf;
-
Ensure the laytout is contiguous. If it’s a split keyboard, manually remove all the keys from one of the splits before exporting.
-
Go to the “Raw Data” tab and click “Download JSON” on the bottom right.
-
Run code like the following to generate the keyboard:
case_result = generate_from_kle_json("<path to downloaded json file>")
The generate_from_kle_json()
method also takes in a config
, debug
, and result
parameters like render_case()
. It returns a standard RenderCaseResult
object.
To check the key positions this generated, look at case_result.keys
.
Important constructs
Important configs
CaseConfig.detachable_palm_rests
allows you to set whether palm rests should be detachable, or part of the case bottom.CaseConfig.side_fillet
sets a fillet on the case vertical edges. You should likely always use it in the final product, but it’s very prone to failure, see warning above.CaseConfig.palm_rests_top_fillet
sets a fillet on the palm rest top edges. You should likely always use it the final product if you have palm rests (so they don’t dig in your hands), but it’s very prone to failure, see warning above.ScrewHoleConfig.screw_insert_hole_width
sets the size of the hole for screws and defaults to a value suitable for melting inserts. If you don’t use inserts, you should lower the value.KeyConfig.case_tile_margin
,ControllerConfig.case_tile_margin
andTrrsJackConfig.case_tile_margin
is the size of the case generated around keys, controller holders, and TRRS jack holders. If you use aPatch
to define your outlines, you can freely lower these.
The render_standard_components
parameter
When render_standard_components
is set to True
in a call to render_case
, all holders (switch, controller, TRRS jack) and palm rest connectors are returned in the resulting .standard_components
object. These help you see what the final keyboard will look like.
This is what example.py
looks like with standard components added (show(keyboard_result.top, keybaord_result.bottom, keyboard_result.palm_rests[0], keyboard_result.case_result.standard_components)
):
The RenderCaseResult
object returned from render_case()
This object contains the individual steps when constructing the case. They key outputs are 3:
case_result.top
is the top platecase_result.bottom
is the case bottomcase_result.palm_rests
is a list of palm rest objectscase_result.standard_components
is a rendering of all holders (switch, controller, TRRS jack) and palm rest connectors (see above).
Additionally there are many intermediate objects are useful for troubleshooting and auditing when something is not going right.
The RenderKeyboardResult
object returned from render_and_save_keyboard()
This object contains the final keyboard components:
keyboard_result.case_result
is theRenderCaseResult
returned from the innerrender_case()
method. Always present.keyboard_result.top
is the top plate (same ascase_results.top
). Always present.keyboard_result.bottom
is the case bottom (same ascase_results.bottom
). Always present.keyboard_result.switch_holder
is the switch holder. Always present.keyboard_result.connector
is the palm rests to case bottom connector. Present only if palm rests are defined andCaseConfig.detachable_palm_rests
isTrue
.keyboard_result.controller_holder
is the controller holder. Present only if a controller is defined.keyboard_result.trrs_jack_holder
is the TRRS jack holder. Present only if a TRRS jack is defined.keyboard_result.palm_rests
is a list of palm rest objects (same ascase_results.palm_rests
). Present only if at least one palm rest is defined.