Sorbete: teclado ergonómico para juegos

Traducción de un artículo del blog de bricolaje Billiam

Un tiempo después de que mi Logitech G13 ya no se produjera, se rompió conmigo y decidí desarrollar un reemplazo para él, que Sherbet llamó.

Primero, qué sucedió:


Teclado con joystick

Imprima archivos e instrucciones de ensamblaje: www.prusaprinters.org/prints/5072-sherbet-gaming-keypad

Diseño


Quería hacer un joystick analógico como el G13, y decidí incluir varias mejoras ergonómicas de otros teclados en el proyecto: teclado Dactyl , Dactyl Manuform , Kinesis Advantage y Ergodox . Específicamente: el cambio de las teclas desde la vertical, el cambio de altura, la curvatura de las columnas y una inclinación más conveniente.

Elegí interruptores de teclado de baja velocidad (NovelKeys Kailh Chocs lineales) para reducir la altura del teclado, en particular porque mi escritorio es más alto de lo que necesita para un conjunto de teclado cómodo y hay un estante grande debajo. Entre el estante y el tablero de la mesa quedan unos 10 cm, tanto para el teclado como para el pincel. Si no tiene problemas con el lugar, le recomiendo un interruptor más compatible, entonces no tendrá problemas con la elección de los botones. También recomiendo comenzar con Dactyl o Dactyl-Manuform, ya que este proyecto me llevó mucho más tiempo y esfuerzo de lo que podría haber esperado.

Comencé modelando claves basadas en las especificaciones de Kailh para los interruptores antes de que llegaran a mí, luego intenté encontrar una curvatura de columna conveniente, imprimí un par de versiones de prueba y luego imprimí algunas más para verificar la sangría vertical. Eso es lo que obtuve:


selección de radios de curvatura


Diseño de selección de altavoz y altura de columna


Teclas de diseño, una vista de la


vista ¾ desde arriba, se puede ver un altavoz de cambio


Vista frontal,

esquema de altura de columnas extendidas visto , comencé a diseñar conectores para interruptores que deben tener éxito Placa de respaldo principal.


Modelo de placa de llave


Primera impresión después de retirar y limpiar soportes.


Placa con interruptores.


Después de agregar llaves

Tomé diferentes ángulos del teclado y me detuve en un ángulo de aproximadamente 20 grados. Y esto se hizo nuevamente para que hubiera un lugar encima del teclado. Sería más conveniente hacer que el ángulo sea un poco más grande, pero aún es más conveniente que el G13 plano y mi teclado ergonómico actual. En Fusion 360, hice que el ángulo de inclinación fuera variable para que el resto del proyecto se ajustara a él. Después de complicar el proyecto, este parámetro ya no se pudo configurar sin romper otros.


Placa de soporte impresa para la selección de inclinación

Soporte de cepillo


Luego comencé a trabajar en el soporte del cepillo. Necesitaba un soporte cómodo adecuado para mi mano, y quería hacer un molde para él. Hice un soporte de arcilla, luego utilicé el paquete fotogramétrico Meshroom (y un montón de fotos) para crear un modelo 3D, y luego lo ajusté para que su tamaño coincida con el original.


Aproximación aproximada al soporte con arcilla polimérica.


Muchas fotos.


Separando el soporte con texturas de Meshroom.


El modelo importado a Fusion 360.

Luego caminé por los contornos principales del modelo, lo alisé y obtuve la impresión deseada:


un modelo escaneado con un modelo 3D superpuesto


de arcilla polimérica al lado con una versión lisa e impresa

Fue interesante hacer esto, y el resultado fue conveniente, pero luego descubrí que durante la impresión todas estas curvas evitan que la mano se mueva, por lo que las versiones posteriores son planas. Bien…

Alojamiento


Luego comencé a trabajar en el caso general del dispositivo, y resultó ser la etapa más difícil de todas. Todavía trabajo con incertidumbre en CAD, y no he hecho nada tan complicado antes. Los intentos de encontrar formas de describir las curvas compuestas y conectar las dos superficies resultaron ser muy difíciles, y tomaron la mayor parte del tiempo en este proyecto.


Un modelo de computadora de la caja junto con un joystick Arduino y botones de pulgar.

También encontré para mí un entorno más conveniente para renderizar , como resultado de lo cual las imágenes mejoraron.


Modelo de caja de computadora rehecho Modelo de


caja de computadora, ver en,

Imprimí solo el área del pulgar para verificar su conveniencia y ubicación. Todo resultó ser normal, pero el módulo de joystick era demasiado voluminoso para colocarlo exactamente donde sería lo mejor desde el punto de vista de la ergonomía.


Un joystick impreso con botones.

En cambio, compré un controlador Joy-Con mucho más pequeño para Nintendo Switch de un fabricante externo, que se puede colocar mucho más cerca de las teclas, y todavía hay espacio para la conexión. Se conecta a un cable plano (menos conveniente) de 5 hilos de 0,5 mm. Tomé la placa de interfaz para 6 contactos en Amazon, pero es mucho más barato tomarlos de eBay o AliExpress.


Comparación de


Joysticks de Joy-Con Joystick con interruptores


Carcasa modificada para un joystick pequeño

Después de terminar con una carcasa, agregué soporte para componentes electrónicos. Para el joystick, era necesario hacer un pequeño panel protector, atornillado a un lado de la carcasa. Para el microcontrolador Teensy, hice un soporte, en el que simplemente encaja bien, y lo atornilla. Espacio adicional para abrazaderas de plástico y agujeros de 4 mm para fijar el soporte para el cepillo.

También utilizo la interfaz micro USB para el controlador USB principal para que el microcontrolador no se desgaste y se dañe.


El interior del caso

creo que, durante toda esta fase de diseño, tomó un total de un mes. No trataré de adivinar la cantidad de horas que pasaron, digamos que hubo "muchos de ellos".

Impresión


Después de pasar tanto tiempo diseñando y probando, me pareció que la impresión final del proyecto era aburrida y sin incidentes.


La impresión con resolución de 0.2 mm en Maker Select Plus tomó 15 horas.


Postprocesamiento: eliminación de copias de seguridad y limpieza. El daño resultó ser pequeño.


La parte superior de la caja con la electrónica instalada.

También imprimí la tapa con plástico blanco, y le pegué una capa de corcho. Para conectar la cubierta al cuerpo, utilizo tornillos M3 y un manguito de acoplamiento roscado insertado en el plástico por calentamiento .


Tapa de corcho antideslizante.

Pintura


Durante el diseño, revisé varias soluciones de color y finalmente me decidí por una similar. No hay muchas opciones de color, ya que las teclas son solo en blanco y negro.


Proyecto de

pintura Para el acabado final, primero lijé la pieza con papel de lija de grano 220 para alisar las tiras de las capas y otros problemas, y luego cubrí todo con una imprimación.


La primera capa de tierra

Después de la imprimación, utilicé masilla de Bondo, lijé la pieza (papel de lija 220 y 600), luego otra vez cubrí con imprimación, masilla y lijé nuevamente.


Después de dos pasadas con imprimación y masilla, y lijado


duro.Otra capa, con imprimación blanca

, hice una tira usando una película delgada de vinilo, y luego cubrí este lugar con el color rosa de una pistola rociadora.


Caso y residuos de pintura

Después de pintar, recubrí la pieza con 4-5 capas de recubrimiento brillante, y luego la lijé con grano 1200 para eliminar el polvo, la pelusa y los insectos, y luego volví a barnizar.

Se ve bien, pero la aspereza y las manchas de exceso de barniz son visibles. Pulí los peores lugares un poco con papel de lija 1200, y luego lo pulí con un compuesto especial.


Después de lijar y pulir

Montaje


Hice granos para la orientación ciega presionando bolas de cerámica de 1,5 mm de los cojinetes en las teclas. La foto muestra un agujero demasiado grande y uno demasiado pequeño, en el que presioné la bola (sin pegamento). No lo sé, sería mejor insertarlo en un gran agujero con pegamento que deformar el plástico empujando la pelota allí.



Cuando había agotado todas las tareas que me ayudarían a lidiar más con la dilación, comencé a conectar cables, comenzando con filas y columnas de llaves.

No encontré cables más delgados en las tiendas locales, y el único cable de un solo núcleo que se vendió fue de 22 awg [ sección transversal de 0.325 mm cuadrados. / aprox. perev.] - y sería demasiado difícil doblar los desplazamientos de la columna y meterlo en un pequeño espacio entre la caja y los interruptores. En cambio, utilicé un cable trenzado de 28 awg [ sección transversal 0.089 mm.kv. / aprox. perev. ], que se extrajo de un cable plano, se despojó con un pelacables y luego se hicieron bucles en los extremos. Con un cable de un solo núcleo más delgado, todo sería más simple.


Interruptores de llave con filas y columnas soldadas


Filas y columnas conectadas a un cable plano Filas, columnas, un joystick y una placa de interfaz USB están conectados




Hice un soporte de cepillo sujeto a dos tornillos M4, que se atornillan en una funda insertada en el plástico del cuerpo principal por calentamiento. Después de instalar los acoplamientos, resultó que los agujeros no coinciden muy bien, por lo que aún no puedo ensamblarlos. Planeo volver a escribir la caja con el orificio en el que se insertará la tuerca M4, será más fácil alinearla.

En la práctica, debe asegurar de alguna manera el teclado en la mesa. Incluso con un fondo de corcho y un peso extra, el teclado cambia al usar el joystick.

PD: Rehice y volví a escribir el soporte para que usara dos tuercas M4, y todo funciona bien.

¡Hecho!



Carcasa preparada y portaescobillas temporal


Carcasa, vista inferior

Firmware


Inicialmente, planeé usar QMK como firmware, utilizando una solicitud de grupo con un soporte de joystick que aún no se ha incluido en la rama principal. Sin embargo, QMK no es muy compatible con los nuevos controladores ARM Teensy (versiones superiores a 3.2). El siguiente parche aún no es compatible con los controladores ARM.

Si se implementa este parche, así como el soporte de ARM, terminaré y publicaré la versión con QMK. Mientras tanto, dibujé un boceto para Arduino, basado en el trabajo de otra persona.

Tiene dos modos, uno es el diseño QWERTY estándar y un joystick con un botón, y el segundo es donde todas las teclas se asignan a los botones del joystick. Como resultado, hay suficientes botones para configurar el configurador del controlador Steam, y puede usarse como un dispositivo XInput con soporte para una gama más amplia de juegos.

Código opcional para dar un nombre al dispositivo
// , . , 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