Meptlpad

Published: Oct 28, 2023

Meptlpad is a 4x4 keypad featuring a print-in-place magnetic key switch integrated into the top case.

The source code and CAD files are available here.

It derives heavily from riskeyboard70, another hall-effect sensor based 3d printed keyboard.

Whilst I was implementing riskeyboard, I measured that it took about 5 minutes (on a good run) to assemble a single switch. Given a 60% keyboard and a single 4x4 numpad, that was at least 6 hours of labor for a single keyboard set! And if I want to make one for a friend and a sibling that’s nearly a full day of just assembling key switches! “There’s got to be a better way.” So I spent the next 3 weeks, designing a print-in-place key switch.

Hardware Design Tour

I’m going to point out all the cool and cute things in my switch design! Except assembly. See the README in the source repository for that. Firstly, a switch in action:

Secondly, an overview:

In the assembled switch, there are 3 magnets which going forward I’ll label as “upper”, “case”, and “lower”

The case and lower magnets share polarity and the upper magnet has reversed polarity such that it repels the case magnet. The state in the above image is the “resting” state. In the “Assembled” view in the overview image, the upper and case magnets repel, and the case and lower magnets attract such that it wants to return to the resting state.

This setup is the same as the one used in the reference riskeyboard. I experimented with alternatives such as a case-lower only magnet setup with low travel distance (which actually works) and a central magnet setup, but ended up keeping the original design.

There are two things that a keyboard switch must minimally accomplish to function:

  • It needs to linearly actuate
  • It’s rotation must be constrained, otherwise the keycap will spin around

Here’s a simple design that can resolve this:

Here, the vertical walls carrying the top bridge are intended to be cut after printing. After this cut is done, the central column can freely move vertically within the cube body. The top-bridge is just there to prevent the column from falling out of the body via gravity. The second constraint of preventing rotation is accomplished by the flat cut on the central column.

Stem

It’s helpful to discuss some 3d printing principles before looking at how the two functions of a switch are achieved in the final switch design. Although a 3d printer can’t print on air, it can bridge across gaps, and can build up overhanging structures at an angle. The minimum angle above horizontal at which your 3d printer can print is a function of a variety of things i.e. your filament, your printer’s ability to cool, etc. However, a recurring theme of my prints is that my filaments are always wet so supposedly my ability to print anything is on the lower end of printer specs.

This cheatsheet has a decent number of 3d printing principles that I employed in the design of my switch.

Here’s an annotated image of the stem without the body.

The red-highlighted regions are intended to be cut and removed post-print. The blue lines here are all examples of a printer able to generate overhangs as long as they are not too shallow. The cyan lines are examples of the printer able to bridge gaps. Technically the Cherry MX cross, where a keycap attaches, isn’t a bridge because there’s a 90 degree turn but the feature is so small that there isn’t too much issue to print the overhang. Additionally, I added a lot of tolerance between this stem and a true Cherry MX stem profile, so even if the filament sags due to gravity, it isn’t too problematic.

The green lines are the stem profile. That entire area mates with the body such that it is a source of friction. Naturally, this profile prevents any sort of rotation. The tolerances are fairly large — about 0.3mm in all directions — and it’s easy to break any printer mishaps that might leak into the space between the stem and body.

As a quick aside, that’s the reasoning for this random pillar here.

For my particular slicer (SuperSlicer 2.4.58.3), the tool path navigates from the magnet holder to the pillar to the case body. This orients any stringing — fibrous plastic caused by filament unintentionally oozing from the 3d printer nozzle — to be away from the switch actuation direction. It doesn’t prevent the stringing that can occur from the body to the magnet holder, so honestly, not sure if it does anything. But whatever, can’t hurt!

The upper magnet holder and this area of the stem prevent the stem from falling down through the switch.

The highlighted area actually doubles as a lever by which you can release the stem from the aforementioned stringing. This design feature was serendipity. During development, I would often create a “pusher” 3d print to release the stem from the lower magnet area, but this is entirely unneeded in this particular design.

One other thing to note in the stem profile is the weird bottom curve. This alternation of fillets and chamfers was an idea suggested in the 3d printing cheatsheet I mentioned earlier. It’s probably overengineered. It’s probably also completely invisible at these scales (the “resolution” of this model would be dictated by the layer height of the 3d printer - 0.2mm in this case). I probably should’ve just used a 45 degree angle, but I’m here now so /shrug.

Body

The green outline here is the same green outline earlier for the stem. It highlights the mating profile between the stem and the body.

The red line here is a lip which can be used as a mechanical aid for aligning your cutters to cut the support structures from the stem. It’s a bit imperfect because it is an overhang, but I can’t think of a better solution. I tried some alternatives, but the scale here is super small, so any tapered identifiers are hard.

The magnet holder here is the same design as all the other magnet holders in the switch. The teardrop shape prevents misshapen holes caused by bridging the top of a circle. The rectangular cut generates wings which allows the socket to bend slightly to hold the magnet. It also doubles as a place you can stick tweezers in case you orient magnet polarity wrong. After adding the cutout, I’m able to slightly reduce the size of the holder.

The silly trapezoid cutout at the very bottom is a legacy feature from when the lower magnet holder used to be flush with the body. It’s hardly visible in this picture, and irrelevant, so I won’t point it out!

Initial Design

There’s an elephant in this room… “why not just print the switch in the simpler orientation and avoid all this bridging mumbo-jumbo”. In fact, I have a design that does exactly that!

The red highlighted area is the only region which needed to be removed. It still uses bridging to generate the upper magnet holder (Underneath that is a holder for the case switch. The problem is, no matter how much you tweak the tolerances, nothing will get over the fact that the direction of motion is perpendicular to the layer lines you’ve printed in.

As a result, there’s no way around the scratchiness that results from layer lines attempting to glide past one another. This causes occasional sticking of the switch.

Comparatively, it is a bit more tactile than my final design; There is a much more controlled and smaller space between the case and the lower magnet. But I couldn’t accept how often the switch would get stuck.

One other minor issue is that because there is more area along the Z-axis being printed, there’s more stringing to get in the way of actuation. This is solvable by lapping away the internal stringing, but it still requires looser tolerances which could allow for excessive play in the stem actuator.

Side note, this design for the magnet holder, which was used in riskeyboard never worked for me.

One, it doesn’t grip the magnet or requires too much print tolerance to do so. I repeat, my filament is ALWAYS wet. Two, if it does, you’ve stressed the plastic so if you ever remove the magnet, there’s no reseating the magnet without glue. Anyway, I kept the old design in the repo, in case a resin print could utilize it well.

I wish I had saved every stl I printed so I can generate a scrubbable timeline of my work. That would be sooo cool! I’ll do that if I ever do another hardware design project.

Plasticity

Plasticity is a modeling software with a workflow closer to sculpting, but which utilizes a CAD library for resolving the model geometry. For any NixOS users, I have a nix derivation. Look, I get it, “this ain’t FOSS!“. But I’ve tried FreeCAD - albeit years ago - and it constantly crashed and basic operations were completely opaque (if they didn’t explode). My initial prototype of this project was in openSCAD and it’s been my CAD tool of choice for a while now. But I was finding it incredibly tedious to iterate.

OpenSCAD Pains

Honestly, I seem to be having a lot of these “rants about all the papercuts” and I don’t want to project negativity into the world. But I assume it’s okay because I always put it into its own little section so really you’re the one choosing to indulge in negativity.

Let’s get started. Using what I assume is the most popular library for openSCAD instantly causes all renders to take 3 seconds, even if you’re rendering a basic cube or nothing! To me this is intractable. Yes, there are “Raw OpenSCAD Equivalent”s in the README, which I’ve copied, but this isn’t exhaustive and has the possibility of being outdated. The library functions can’t be extracted easily, naturally due to dependencies on shared utils.

Debugging is tedious; If I want to visualize something within a difference(), I have to either convert it to a union() where I’ll see ALL items that I’m difference-ing or copy the object of interest outside of the difference() which doesn’t copy any other operations you are doing on the difference() e.g. translates, rotates. As a result, either everything becomes a module/function or you have to mentally translate things the same way you’ve done in other objects. Sometimes I just want to cut a circle right there, and I don’t want to do the math of determining its position or convert this basic object into a module/function. So I just end up with a mess of hardcoded offsets and this makes me feel really icky. I know that it is some function of this width multiplied by the sin() of that angle, but I’m just testing things — I can’t be bothered.

An example of the carnage during development:

// Eyeballed.
rotate([0,0,-45]) translate([0,-(CHERRY_CYLINDER_DIAMETER/2+tail_len+MAGNET_DIAMETER+1),height+float_height/2+MAGNET_HEIGHT/2+0])
    difference() {
        cube([MAGNET_DIAMETER+1,1,float_height+MAGNET_HEIGHT+0.4], center=true);
    }

Why is there “+0” here? Why are we difference-ing a single object?

Fillets and tapers are a pain. This was the issue that “broke the camel’s back” so to speak. I wanted a tapered throughhole, and couldn’t think of a sane solution that didn’t involve reorganizing my entire codebase so far. Looking back I can see a simple solution using hull()…

There’s no “cut by plane” so converting from 3d to 2d is difficult. Although if this did exist fillets could be slightly easier because you can create your cut object like so:

minkowski() {
    linear_extrude(0.01) square(2);
    rotate([45,45,0]) cube(1);
}

Anywho, the lack of 3d to 2d conversion makes it difficult mixing workflows that use 2d extrusion vs 3d model manipulations.

While I’m here… we all know this issue and we all live with it: you have to add a smudge factor to everything otherwise perfectly overlapping differences causes visual issues in preview mode. Note that the cube’s origin here is the bottom-left and it grows to the upper right.

Plasticity Opinions

I’m a fan. There’s some odd quirks that need ironing out e.g. “Snap To Grid” takes precedence over the natural snapping that exists (the one that snaps to centers of circles and on existing lines). There are some situations where an operation doesn’t work until I reset the program. I can’t completely freely manipulate vertices as I would in Blender. There’s no “modifier” system, so repeatability of a series of operations is an issue. But overall, the program is really good and “just works”.

Maybe this is the realm of “Paid Software”.

PCB

There’s nothing interesting here. The pcb clones the designs from riskable’s reference pcb but configured for a 4x4 keyboard. I chose a 16 key device since that is the number of channels on a single multiplexer chip. I removed the debug JST connector, opting to connect directly into the SWDP pins for the Black Pill.

And then I stamped my logo onto the PCB because I’m cool, so is my logo, and so is my pcb.

Dumb Stupid Baby

Spot the idiocy!

As a hint, I do not trust the solder joints between the dev board and the pcb at all. Oh did I suggest a singular idiocy? That missing diode is also a hint. One of my usb cables looks like this now.

Not sure how people can attach dev boards in a surface mount like fashion to a PCB.

Software

Initially when writing the firmware, I upgraded everything to the latest version of packages. Things didn’t work out and I ended up mostly on the same versions as the reference keyboard. There’s a lot of pruning I did, so I think my firmware is a lot more approachable. I removed:

  • the entire config system instead opting for constants
  • the Multiple multiplexer abstraction opting for inline multiplexer polling. I only have 1 multiplexer so having the abstraction isn’t as useful.
  • Support for an encoder and IR remote. I don’t have this hardware.

Additionally I’ve added support for flashing via USB (using dfu-utils).

rtic 2.0

There is an updated version of rtic, a framework for embedded Rust programs, that I initially used. Unfortunately, at the time of writing, it didn’t have automatic monotonic timers - a timer to control the speed of the mcu. rtic_monotonics, a package with a set of shorthands for configuring such things, only had a few implementations. In short, I’d need to write my own variant of this. No thanks!

Additionally the API changed and I couldn’t find an equivalent to spawn_after(). So I don’t actually know a method to always run a function every second. The new API uses async/await syntax so equivalently you’d have something like

loop {
    // Do stuff
    await(1sec);
}

But such a loop would constantly drift by the time it takes to “Do stuff”. I’ve come to learn such a drift shouldn’t matter, but as someone not entirely familiar with the USB protocol, it’s hard to make decisions in the moment.

Next Steps

  • Explore a compliant mechanism for switch actuation. The KoolAid is here and I have drank it. It is time.

  • Use embassy, an even more high level framework for embedded Rust programs.

  • Create a full keyboard. Preferrably a split keyboard. I’m basically following the tracks of this repo.

  • Figure out keycap legends. I’m thinking it would be simple to print a hollow bit out of each cap and fill it with some epoxy, but the keycaps aren’t particularly my focus so far. Alternatively, I’ve really wanted to do some multi-material prints…

  • Improved case designs: Magnets are weak, but screws are ugly. Dilemma.

  • Find an option for rapid trigger