Sherbet: ergonomic gaming keyboard

Translation of an article from the Billiam do-it- yourself blog

Some time after my Logitech G13 was no longer produced, it broke down with me, and I decided to develop a replacement for it, which Sherbet called.

First - what happened:


Keyboard with joystick.

Print files and assembly instructions: www.prusaprinters.org/prints/5072-sherbet-gaming-keypad

Design


I wanted to make an analog thumb joystick like the G13, and I decided to include several ergonomic improvements from other keyboards in the project - Dactyl keyboard , Dactyl Manuform , Kinesis Advantage and Ergodox . Specifically - the shift of the keys from the vertical, the shift in height, the curvature of the columns and a more convenient tilt.

I chose low-speed keyboard switches (linear NovelKeys Kailh Chocs) to reduce the height of the keyboard - in particular because my desk is taller than you need for a comfortable keyboard set and there is a large shelf underneath. Between the shelf and the tabletop there is about 10 cm left, both for the keyboard and for the brush. If you have no problems with the place, I recommend a more compatible switch - then you will have no problems with choosing buttons for it. I also recommend starting with Dactyl or Dactyl-Manuform, as this project took me much more time and effort than I might have expected.

I started by modeling keys based on Kailh's specifications for the switches before they even came to me, then I tried to find a convenient column curvature, printed a couple of test versions, and then printed a few more to check the vertical indentation. Here's what I got:


Selecting the bending radii of the columns


Selecting the layout and height of the column


Designing the keys, view ¾


Top view, you can see the shift of the columns


Front view, you can see the spread of the columns in height

Selecting the scheme, I started to design the connectors for the switches, which should make main backing plate.


Key plate model


First print after removal and cleaning of supports.


Plate with switches.


After adding keys

I picked up different angles of the keyboard, and stopped at an angle of about 20 degrees. And this was again done so that there was a place above the keyboard. It would be more convenient to make the angle a little larger, but it is still more convenient than the flat G13 and my current ergonomic keyboard. In Fusion 360, I made the tilt angle changeable so that the rest of the project adjusts to it. After complicating the project, this parameter could no longer be configured without breaking others.


Printed support plate for tilt selection

Brush stand


Then I started work on the brush stand. I needed a comfortable stand suitable for my hand, and I wanted to make a mold for it. I made a clay clay stand, then used the Meshroom photogrammetric package (and a bunch of photos) to create a 3D model, and then scaled it so that its size matches the original. Rough approximation to the stand using polymer clay. Many photos. Parting the stand with textures from Meshroom. The model imported into Fusion 360. Then I just walked along the main contours of the model, smoothed it and got the desired print: A scanned model with a superimposed 3D model of polymer clay next to it with a smoothed and printed version





















It was interesting to do this, and the result was convenient, but then I found that during printing all these bends prevent the hand from moving, so the later versions are all flat. Well…

Housing


Then I began to work on the general case of the device, and this turned out to be the most difficult stage of all. I still work uncertainly in CAD, and haven’t done anything so complicated before. Attempts to find ways to describe the composite curves and connect the two surfaces proved to be very difficult, and took most of the time in this project.


A computer model of the case along with an Arduino joystick and thumb buttons.

I also found for myself a more convenient environment for rendering , as a result of which the images became better.


Re-made computer case model


Computer case model, view in,

I printed only the thumb area to check its convenience and location. Everything turned out to be normal, but the joystick module was too voluminous to be placed exactly where it would be the best from the point of view of ergonomics.


A printed joystick with buttons.

Instead, I purchased a much smaller Joy-Con controller for the Nintendo Switch from a third-party manufacturer, which can be placed much closer to the keys, and there is still room for connection. It connects to a (less convenient) flat cable of 5 wires 0.5 mm. I took the interface board for 6 contacts on Amazon, but it is much cheaper to take them from eBay or AliExpress.


Comparison of


Joysticks by Joy-Con Joystick with Switches


Modified housing for a small joystick

Having finished with a shell, I added support for electronic components. For the joystick, it was necessary to make a small protective panel, screwed to the side of the housing. For the Teensy microcontroller, I made a holder, into which it simply fits tightly, and screws for it. Added space for plastic clamps and 4 mm holes for attaching the stand for the brush.

I also use the micro USB interface for the main USB controller so that the microcontroller does not wear out and get damaged.


The insides of the case

I think, for the whole of this design phase, it took a total of a month. I will not try to guess the number of hours that went into it - let's say that there were “a lot of them”.

Print


After I spent so much time designing and testing, it seemed to me that the final print of the project was boring and without incident.


Printing with 0.2 mm resolution on Maker Select Plus took 15 hours.


Post-processing: removal of backups and cleaning. Damage turned out to be small.


The upper part of the case with installed electronics.

I also printed the cover with white plastic, and glued a cork coating to it. To connect the cover to the body, I use M3 screws and a threaded mating sleeve inserted into the plastic by heating .


Non-slip cork cover

Painting


During the design, I went over several color solutions, and eventually settled on a similar one. There are not many color options, since the keys are only black and white. Painting


project

For the final finish, I first sanded the part with 220 grit sandpaper to smooth out the strips from the layers and other problems, and then covered everything with a primer.


The first layer of soil

After the primer, I used putty from Bondo, sanded the part (sandpaper 220 and 600), then again covered with primer, putty, and sanded again.


After two passes with primer and putty, and hard sanding.


Another layer, with white primer

, I made a strip with a thin vinyl film, and then covered this place with pink color from a spray gun.


Case and paint residue

After painting, I coated the part with 4-5 layers of glossy coating, and then sanded it with 1200 grit to remove dust, fluff and bugs, and then varnished again.

It looks good, but the roughness and spots of excess varnish are visible. I polished the worst places a little with sandpaper 1200, and then polished it with a special compound.


After grinding and polishing

Assembly


I made pimples for blind orientation by pressing 1.5 mm ceramic balls from the bearings into the keys. The photo shows one hole too large and one too small, into which I pressed the ball (without glue). I don’t know, it would be better to insert it into a large hole with glue than to deform the plastic by pushing the ball there.



When I had exhausted all the tasks that would help me deal with procrastination further, I started connecting wires, starting with rows and columns of keys.

I did not find thinner wires in local stores, and the only single-core wire that was sold was 22 awg [ cross-section 0.325 mm sq. / approx. perev.] - and it would be too hard to bend around the column offsets and stuff it into a small space between the case and the switches. Instead, I used a 28 awg stranded wire [ cross section 0.089 mm.kv. / approx. perev. ], which was pulled from a flat cable, stripped with a wire stripper, and then made loops at the ends. With a thinner single-core cable, everything would be simpler.


Key switches with soldered rows and columns


Rows and columns connected to a flat cable Rows, columns, a joystick and a USB interface board are connected




I made a brush stand fastened to two M4 screws, which are screwed into the sleeve inserted into the plastic of the main body by heating. After installing the couplings, it turned out that the holes do not match very well, so I can’t assemble them yet. I plan to retype the case with the hole into which the M4 nut will be inserted, it will be easier to align it.

In practice, you need to somehow secure the keyboard on the table. Even with a cork bottom and extra weight, the keyboard shifts when using the joystick.

PS: I redid and retyped the stand so that it used two M4 nuts, and everything works fine.

Done!



Ready-made housing and temporary brush holder


Housing, bottom view

Firmware


Initially, I planned to use QMK as firmware, using a pool request with a joystick support that has not yet been included in the main branch. However, QMK does not very well support the new ARM Teensy controllers (versions greater than 3.2). The following patch does not yet support ARM controllers.

If this patch is implemented, as well as ARM support, I will finish and publish the version with QMK. In the meantime, I sketched a sketch for Arduino, based on someone else's work.

It has two modes, one is the standard QWERTY layout and a joystick with one button, and the second is where all the keys are assigned to the joystick buttons. As a result, there are enough buttons to configure the Steam controller configurator, and it can be used as an XInput device with support for a wider range of games.

Optional code to give the device a name
// , . , sherbet.ino.
#include «usb_names.h»

#define PRODUCT_NAME {'s', 'h', 'e', 'r', 'b', 'e', 't'}
#define PRODUCT_NAME_LEN 7

struct usb_string_descriptor_struct usb_string_product_name = {
2 + PRODUCT_NAME_LEN * 2,
3,
PRODUCT_NAME
};



Firmware
/*
Original programming by Stefan Jakobsson, 2019
Released to public domain
forum.pjrc.com/threads/55395-Keyboard-simple-firmware
*/
/*
Copyright 2019 Colin Fein
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the «Software»), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED «AS IS», WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

// Use USB Type: Keybord+Mouse+Joystick

#include <Bounce2.h>

const int ROW_COUNT = 4; //Number of rows in the keyboard matrix
const int COL_COUNT = 6; //Number of columns in the keyboard matrix

const int DEBOUNCE = 5; //Adjust as needed: increase if bouncing problem, decrease if not all keypresses register; not less than 2
const int SCAN_DELAY = 5; //Delay between scan cycles in ms

const int JOYSTICK_X_PIN = 14; // Analog pin used for the X axis
const int JOYSTICK_Y_PIN = 15; // Analog pin used for the Y axis
const bool REVERSE_X = true; // Reverses X axis input
const bool REVERSE_Y = true; // Reverses Y axis input
const int MIN_X = 215; // Minimum range for the X axis
const int MAX_X = 780; // Maxixmum range for the X axis
const int MIN_Y = 280; // Minimum range for the Y axis
const int MAX_Y = 815; // Maximum range for the Y axis
const int BUTTON_COUNT = 1; // Number of joystick buttons

const int JOY_MIN = 0;
const int JOY_MAX = 1023;

Bounce buttons[BUTTON_COUNT];
Bounce switches[ROW_COUNT * COL_COUNT];

boolean buttonStatus[ROW_COUNT * COL_COUNT + BUTTON_COUNT]; //store button status so that inputs can be released
boolean keyStatus[ROW_COUNT * COL_COUNT]; //store keyboard status so that keys can be released

const int rowPins[] = {3, 2, 1, 0}; //Teensy pins attached to matrix rows
const int colPins[] = {11, 10, 9, 8, 7, 6}; //Teensy pins attached to matrix columns
const int buttonPins[] = {12}; //Teensy pins attached directly to switches

int axes[] = {512, 512};

int keyMode = true; // Whether to begin in standard qwerty mode or joystick button mode

// Keycodes for qwerty input
const int layer_rows[] = {
KEY_ESC, KEY_1, KEY_2, KEY_3, KEY_4, KEY_5,
KEY_TAB, KEY_Q, KEY_W, KEY_E, KEY_R, KEY_T,
KEY_CAPS_LOCK, KEY_A, KEY_S, KEY_D, KEY_F, KEY_G,
MODIFIERKEY_SHIFT, KEY_Z, KEY_X, KEY_C, KEY_V, KEY_B
};
// keystroke to use (counting from top left to top right of keypad) to switch between standard qwerty input and joystick buttons
// default uses B+5
const int mode_swap_keystroke[2] = {23, 5};
int pivoted_keystroke[2]; //rows to columns
boolean keystrokeModifier = false; //whether beginning of keystroke is active

// rows to columns
int layer[ROW_COUNT * COL_COUNT];

void setup() {
int i;
//pivot key array for row-to-column diodes
for (i = 0; i < ROW_COUNT * COL_COUNT; i++) {
layer[rotateIndex(i)] = layer_rows[i];

// create debouncers for row pins
Bounce debouncer = Bounce();
debouncer.attach(rowPins[i % ROW_COUNT]);
debouncer.interval(DEBOUNCE);
switches[i] = debouncer;
}

//convert keystroke to (pivoted) indexes
for (i = 0; i < 2; i++) {
pivoted_keystroke[i] = rotateIndex(mode_swap_keystroke[i]);
}

// create debouncers for non-matrix input pins
for (i = 0; i < BUTTON_COUNT; i++) {
Bounce debouncer = Bounce();

debouncer.attach(buttonPins[i], INPUT_PULLUP);
debouncer.interval(DEBOUNCE);
buttons[i] = debouncer;
}

// Ground first column pin
pinMode(colPins[0], OUTPUT);
digitalWrite(colPins[0], LOW);

for (i = 1; i < COL_COUNT; i++) {
pinMode(colPins[i], INPUT);
}

//Row pins
for (i = 0; i < ROW_COUNT; i++) {
pinMode(rowPins[i], INPUT_PULLUP);
}
}

void loop() {
scanMatrix();
scanJoy();
delay(SCAN_DELAY);
}

/*
Scan keyboard matrix, triggering press and release events
*/
void scanMatrix() {
int i;

for (i = 0; i < ROW_COUNT * COL_COUNT; i++) {
prepareMatrixRead(i);
switches[i].update();

if (switches[i].fell()) {
matrixPress(i);
} else if (switches[i].rose()) {
matrixRelease(i);
}
}
}

/*
Scan physical, non-matrix joystick buttons
*/
void scanJoy() {
int i;
boolean anyChange = false;

for (i=0; i < BUTTON_COUNT; i++) {
buttons[i].update();
if (buttons[i].fell()) {
buttonPress(i);
anyChange = true;
} else if (buttons[i].rose()) {
buttonRelease(i);
anyChange = true;
}
}

int x = getJoyDeflection(JOYSTICK_X_PIN, REVERSE_X, MIN_X, MAX_X);
int y = getJoyDeflection(JOYSTICK_Y_PIN, REVERSE_Y, MIN_Y, MAX_Y);
Joystick.X(x);
Joystick.Y(y);

if (x != axes[0] || y != axes[y]) {
anyChange = true;

axes[0] = x;
axes[1] = y;
}

if (anyChange) {
Joystick.send_now();
}
}

/*
Return a remapped and clamped analog value
*/
int getJoyDeflection(int pin, boolean reverse, int min, int max) {
int input = analogRead(pin);
if (reverse) {
input = JOY_MAX — input;
}

return map(constrain(input, min, max), min, max, JOY_MIN, JOY_MAX);
}
/*
Returns input pin to be read by keyScan method
Param key is the keyboard matrix scan code (col * ROW_COUNT + row)
*/
void prepareMatrixRead(int key) {
static int currentCol = 0;
int p = key / ROW_COUNT;

if (p != currentCol) {
pinMode(colPins[currentCol], INPUT);
pinMode(colPins[p], OUTPUT);
digitalWrite(colPins[p], LOW);
currentCol = p;
}
}

/*
Sends key press event
Param keyCode is the keyboard matrix scan code (col * ROW_COUNT + row)
*/
void matrixPress(int keyCode) {
if (keyMode) {
keyPress(keyCode);
} else {
buttonPress(BUTTON_COUNT + keyCode);
}

keystrokePress(keyCode);
}

/*
Sends key release event
Param keyCode is the keyboard matrix scan code (col * ROW_COUNT + row)
*/
void matrixRelease(int keyCode) {
//TODO: Possibly do not trigger keyboard.release if key not already pressed (due to changing modes)
if (keyMode) {
keyRelease(keyCode);
} else {
buttonRelease(BUTTON_COUNT + keyCode);
}

keystrokeRelease(keyCode);
}

/*
Send key press event
*/
void keyPress(int keyCode) {
Keyboard.press(layer[keyCode]);
keyStatus[keyCode]=true;
}

/*
Send key release event
*/
void keyRelease(int keyCode) {
Keyboard.release(layer[keyCode]);
keyStatus[keyCode]=false;
}

/*
Send joystick button press event
Param buttonId 0-indexed button ID
*/
void buttonPress(int buttonId) {
Joystick.button(buttonId + 1, 1);
buttonStatus[buttonId] = true;
}

/*
Send joystick button release event
Param buttonId 0-indexed button ID
*/
void buttonRelease(int buttonId) {
Joystick.button(buttonId + 1, 0);
buttonStatus[buttonId] = false;
}

/*
Listen for keystroke keys, and change keyboard mode when condition is met
*/
void keystrokePress(int keyCode) {
if (keyCode == pivoted_keystroke[0]) {
keystrokeModifier = true;
} else if (keystrokeModifier && keyCode == pivoted_keystroke[1]) {
releaseLayer();
keyMode = !keyMode;
}
}

/*
Listen for keystroke key release, unsetting keystroke flag
*/
void keystrokeRelease(int keyCode) {
if (keyCode == pivoted_keystroke[0]) {
keystrokeModifier = false;
}
}

/*
Releases all matrix and non-matrix keys; called upon change of key mode
*/
void releaseLayer() {
int i;
for (i = 0; i < ROW_COUNT * COL_COUNT; i++) {
matrixRelease(i);
}

for (i=0; i < BUTTON_COUNT; i++) {
if (buttonStatus[i]) {
buttonRelease(i);
}
}
}

/*
Converts an index in a row-first sequence to column-first
[1, 2, 3] [1, 4, 7]
[4, 5, 6] => [2, 5, 8]
[7, 8, 9] [3, 6, 9]
*/
int rotateIndex(int index) {
return index % COL_COUNT * ROW_COUNT + index / COL_COUNT;
}


All Articles