Skip to main content

Custom Mechanical Keyboards

This is the 10th year I've been using mechanical keyboards - specifically with Cherry MX Blue switches and blank keycaps. Numerous keyboards and a few jobs later, the majestic clack is still euphoric.

top-down: travel, work, home

From the top down are my current travel, work, and home setups. The top keyboard is the first I built and was done so from a kit. The other two are built as this article describes. All keyboards are running the same firmware with the same layout. I value consistency a lot as it's one less think about or adjust to.

If you're curious, I love the Zowie FK1 mouse due its simple looks, lack of specialized drivers, and lack of hardware acceleration.

Build

After rotating through a number of preassembled keyboards and one DIY kit, I knew my preferences and decided to build the keyboard which embodied them all.

  • 60% Layout - Having owned both full-size and tenkeyless keyboards, I preferred the smaller footprint of a 60%. The seldom used keys (arrows, F*) are more accessible with combinations or remapped keys with minimal hand movement. The Pok3r was my first 60% keyboard. Loved it so much I bought a second one to keep at work.
  • Open Firmware - As much as I loved the size and layout of the Pok3r, remapping keys was limited. Things were either too cumbersome or impossible to remap to. The other problem was keeping the layouts consistent between home and work. I needed to move to a more stable configuration and QMK Firmware supported keyboard was a must.
  • Arrow Keys - The right shift key is useless to me. Touch typists may disagree, but years of gaming with WASD+Left Shift have made me a dedicated left shifter.
  • Plate Mount Stabilizers - These look, feel, and sound better than PCB mount stabilizers.
  • Detachable Cable - Cable management and cleaning becomes much easier when the cable is the right length and moving the keyboard does not require crawling under a desk.
  • Hotswap Switches - Sure, why not?
  • Looks - Blank, black, and no bullshit.

I eventually decided on the following parts for the keyboards. All are all off-the-shelf except for the plate which was custom ordered ordered using swillkb plate and case builder and painted with black Plasti Dip.

Layout

As mentioned earlier, the popular and very active QMK Firmware powers my keyboards with a layout customized to meet my desires.

The source code for my layout can be found here.

my layout

Simplified Python Parallelization

Using Python 3.6

I fell in love when I first discovered Python's multiprocessing.Pool. It provided a simple API for consuming an iterable over multiple child processes. It fit perfectly with my immediate needs — isolated, atomic operations.

However, a flaw, or rather—an oddity—soon revealed itself when using this module for tasks requiring state persistence. This frustration, among others, resulted in the birth of the consumers package.

Before getting into that, here is how multiprocessing.Pool is awesome.

Isolated, Atomic Operations

Using multiprocessing.Pool for processing which requires no persistence outside of the processing function is where it makes the most sense. An ETL process dealing solely with files, for example, would be ripe for usage with it.

def process(path):
    data = read_and_transform(path)
    with open(path, 'w') as f:
        f.write(data)

with multiprocessing.Pool() as pool:
    pool.map(process, paths)

Files paths are distributed to child processes which read, transform, and write the results. Nothing is shared between the processing of each file.

State Persistence: The Hack

Instead of writing to a file, let's say you want to update a database. It's not efficient to open a new connection for each file so you'll want to persist the connection throughout the life of each child process.

Using multiprocessing.Pool, you could end up with something like this:

connection = None

def init(config):
    global connection
    connection = db_connect(config)

def process(path):
    global connection
    data = read_and_transform(path)
    connection.insert(data)

with multiprocessing.Pool(initializer=init, (config,)) as pool:
    pool.map(process, paths)

Yuck! This not how I like to structure my code. Any use of globals outside of constants feels like a hack to a problem with a better solution.

Now some of you might be thinking that you could use callable classes or similar to avoid use of globals. True—there are other ways, but they are unnecessarily complex when you ultimately just want to iterate over data.

State Persistence: The Sane

This is where consumers comes in. Instead of the one-to-one relationship multiprocessing.Pool has between target function and individual datum, consumers.Pool has a one-to-one relationship between target function and child process.

def process(paths, config):
    connection = db_connect(config)
    for path in paths:
        data = read_and_transform(path)
        connection.insert(data)

with consumers.Pool(process, args=(config,)) as pool:
    for path in paths:
        pool.put(path)

No globals; no separate initializer function. Just a single function which receives an ordinary generator and optionally the initializer args and kwargs.

Process Completion

The consumers.Pool also supports functionality that is not possible with multiprocessing.Pool — logic upon child process completion.

Imagine if I instead wanted to insert all the results into the database with a single query run at the end of each process.

def process(paths, config):
    connection = db_connect(config)
    results = []
    for path in paths:
        data = read_and_transform(path)
        results.append(data)
    connection.insert(results)

with consumers.Pool(process, args=(config,)) as pool:
    for path in paths:
        pool.put(path)

That's it—nothing special required. It's simply a side-effect of having control of both how and when an item is consumed.

tl;dr

multiprocessing.Pool is part of the Python standard library, is great for basic tasks, but often feels unnatural for everything else.

consumers.Pool is part of the consumers package, has a minimal API, and provides fine control in an organic way. See the docs for additional examples.