Sherbet: teclado ergonômico para jogos

Tradução de um artigo do blog Billiam - it - yourself Você

algum tempo depois que o meu Logitech G13 não foi mais produzido, ele quebrou comigo e eu decidi desenvolver um substituto para ele, que Sherbet chamou.

Primeiro - o que aconteceu:


Teclado com joystick

Imprima arquivos e instruções de montagem: www.prusaprinters.org/prints/5072-sherbet-gaming-keypad

Projeto


Eu queria criar um joystick analógico como o G13 e decidi incluir várias melhorias ergonômicas de outros teclados no projeto - teclado Dactyl , Dactyl Manuform , Kinesis Advantage e Ergodox . Especificamente - a mudança das teclas da vertical, a mudança de altura, a curvatura das colunas e uma inclinação mais conveniente.

Escolhi interruptores de teclado de baixa velocidade (NovelKeys Kailh Chocs lineares) para reduzir a altura do teclado - principalmente porque minha mesa é mais alta do que você precisa para um conjunto de teclados confortável e há uma prateleira grande embaixo. Entre a prateleira e a mesa, restam cerca de 10 cm, tanto para o teclado quanto para o pincel. Se você não tiver problemas com o local, recomendo um switch mais compatível - então você não terá problemas com a escolha dos botões. Também recomendo começar com Dactyl ou Dactyl-Manuform, pois esse projeto me levou muito mais tempo e esforço do que eu esperava.

Comecei modelando chaves com base nas especificações de Kailh para os comutadores antes que eles chegassem até mim, tentei encontrar uma curvatura conveniente da coluna, imprimi algumas versões de teste e depois imprimi mais algumas para verificar o recuo vertical. Aqui está o que eu obtive:


Selecionando os raios de curvatura das colunas


Selecionando o layout e a altura da coluna


Projetando as teclas, a vista view Vista


superior, você pode ver o deslocamento das colunas


Vista frontal, você pode ver a dispersão das colunas em altura

Selecionando o esquema, comecei a projetar os conectores para os comutadores, o que deve fazer placa de apoio principal.


Modelo da placa das chaves


Primeira impressão após remoção e limpeza dos suportes


Placa com interruptores


Após adicionar as chaves

Peguei ângulos diferentes do teclado e parei em um ângulo de cerca de 20 graus. E isso foi feito novamente para que houvesse um lugar acima do teclado. Seria mais conveniente aumentar um pouco o ângulo, mas ainda é mais conveniente que o G13 plano e meu teclado ergonômico atual. No Fusion 360, alterei o ângulo de inclinação para que o resto do projeto se ajustasse a ele. Depois de complicar o projeto, esse parâmetro não pôde mais ser configurado sem interromper outros.


Placa de suporte impressa para seleção de inclinação

Suporte de escova


Então comecei a trabalhar no suporte de escova. Eu precisava de um suporte confortável adequado para a minha mão e queria fazer um molde para ela. Fiz um suporte de argila-argila e, em seguida, usei o pacote fotogramétrico do Meshroom (e várias fotos) para criar um modelo 3D e o dimensionei para que seu tamanho correspondesse ao original.


Abordagem grosseira do suporte usando argila do polímero


Muitas fotos.


Enquadrando o suporte com texturas do Meshroom.


O modelo importado para o Fusion 360.

Depois, apenas caminhei pelos contornos principais do modelo, suavizei-o e obtive a impressão desejada:


Um modelo digitalizado com um modelo 3D sobreposto


de argila do polímero ao lado com uma versão suavizada e impressa

Foi interessante fazer isso, e o resultado foi conveniente, mas descobri que durante a impressão todas essas dobras impedem o movimento da mão, de modo que as versões posteriores são todas planas. Bem…

Habitação


Então comecei a trabalhar no caso geral do dispositivo, e esse acabou sendo o estágio mais difícil de todos. Ainda trabalho com incerteza no CAD e não fiz nada tão complicado antes. As tentativas de encontrar maneiras de descrever as curvas compostas e conectar as duas superfícies se mostraram muito difíceis e levaram a maior parte do tempo neste projeto.


Um modelo de computador do gabinete, juntamente com um joystick do Arduino e botões de polegar.

Também achei para mim um ambiente mais conveniente para renderização , como resultado do qual as imagens se tornaram melhores.


Modelo de gabinete de computador reformulado Modelo de


gabinete de computador, visualizar,

Imprimi apenas a área do polegar para verificar sua conveniência e localização. Tudo acabou normal, mas o módulo do joystick era muito volumoso para ser colocado exatamente onde seria o melhor do ponto de vista da ergonomia.


Um joystick impresso com botões.Em

vez disso, comprei um controlador Joy-Con muito menor para o Nintendo Switch de um fabricante de terceiros, que pode ser colocado muito mais próximo das teclas, e ainda há espaço para conexão. Ele se conecta a um cabo plano (menos conveniente) de 5 fios de 0,5 mm. Levei a placa de interface para 6 contatos na Amazon, mas é muito mais barato tirá-los do eBay ou AliExpress.


Comparação de


Joysticks por Joy-Con Joystick com Switches


Caixa modificada para um pequeno joystick

Depois de terminar com uma concha, adicionei suporte para componentes eletrônicos. Para o joystick, era necessário fazer um pequeno painel de proteção, parafusado na lateral da caixa. Para o microcontrolador Teensy, fiz um suporte no qual ele simplesmente se encaixa firmemente e aparafusa. Adicionado espaço para grampos de plástico e orifícios de 4 mm para fixação do suporte da escova.

Também uso a interface micro USB para o controlador USB principal, para que o microcontrolador não se desgaste e seja danificado.


Por dentro

, acho que, durante toda essa fase de design, levou um total de um mês. Não tentarei adivinhar o número de horas que passaram - digamos que havia "muitas".

Impressão


Depois de tanto tempo projetando e testando, pareceu-me que a impressão final do projeto era entediante e sem incidentes.


A impressão com resolução de 0,2 mm no Maker Select Plus levou 15 horas ..


Pós-processamento: remoção de backups e limpeza. Os danos acabaram sendo pequenos.


Na parte superior do estojo, com os eletrônicos instalados,

imprimi a capa com plástico branco e colei um revestimento de cortiça. Para conectar a tampa ao corpo, eu uso parafusos M3 e uma luva roscada inserida no plástico por aquecimento .


Capa de cortiça antiderrapante

Pintura


Durante o design, examinei várias soluções de cores e, por fim, decidi por uma semelhante. Não há muitas opções de cores, pois as teclas são apenas preto e branco.


Projeto de coloração

Para o acabamento final, lixei a peça com uma lixa 220 para suavizar as tiras das camadas e de outros problemas, e depois cobri tudo com uma cartilha.


A primeira camada de solo

Após o primer, usei massa de vidraceiro de Bondo, lixei a peça (lixa 220 e 600), depois cobri novamente com primer, massa de vidraceiro e lixei novamente.


Depois de duas passagens com primer e massa de vidraceiro e lixamento duro,


outra camada, com primer branco

, fiz uma tira com uma fina película de vinil e depois cobri esse local com a cor rosa de uma pistola de pintura.


Estojo e resíduo de tinta

Após a pintura, cobri a peça com 4-5 camadas de revestimento brilhante e lixei-a com 1200 grit para remover poeira, cotão e insetos, e depois envernizei novamente.

Parece bom, mas a rugosidade e manchas do excesso de verniz são visíveis. Eu poli um pouco os piores lugares com uma lixa 1200 e depois poli-a com um composto especial.


Após moagem e polimento

Montagem


Fiz espinhas para orientação às cegas pressionando bolas de cerâmica de 1,5 mm dos rolamentos nas teclas. A foto mostra um buraco muito grande e um muito pequeno, no qual pressionei a bola (sem cola). Não sei, seria melhor inseri-lo em um grande buraco com cola do que deformar o plástico empurrando a bola para lá.



Depois de esgotar todas as tarefas que me ajudariam a continuar a procrastinação, comecei a conectar os fios, começando com linhas e colunas de chaves.

Não encontrei fios mais finos nas lojas locais, e o único fio de núcleo único vendido foi de 22 awg [ seção transversal de 0,325 mm². / Aproximadamente. perev.] - e seria muito difícil contornar as compensações da coluna e colocá-la em um espaço pequeno entre o gabinete e os comutadores. Em vez disso, usei um fio trançado de 28 awg [ seção transversal 0,089 mm.kv. / Aproximadamente. perev. ], que foi puxado de um cabo plano, descascado com um descascador de fios e feito laços nas extremidades. Com um cabo de núcleo único mais fino, tudo seria mais simples.


Interruptores de chave com linhas e colunas soldadas


Linhas e colunas conectadas a um cabo plano Linhas, colunas, um joystick e uma placa de interface USB estão conectados




Fiz um suporte de escova preso a dois parafusos M4, que são enroscados na manga inserida no plástico do corpo principal por aquecimento. Depois de instalar os acoplamentos, verificou-se que os orifícios não combinam muito bem, então ainda não consigo montá-los. Pretendo redigitar o gabinete com o orifício no qual a porca M4 será inserida, será mais fácil alinhá-lo.

Na prática, você precisa proteger o teclado sobre a mesa. Mesmo com um fundo de cortiça e um peso extra, o teclado muda ao usar o joystick.

PS: refiz e redigitei o suporte para que ele usasse duas porcas M4, e tudo funcione bem.

Feito!



Carcaça pronta e porta-escova temporário


Carcaça, vista inferior

Firmware


Inicialmente, planejei usar o QMK como firmware, usando uma solicitação de pool com um suporte de joystick que ainda não foi incluído na ramificação principal. No entanto, o QMK não suporta muito bem os novos controladores ARM Teensy (versões maiores que 3.2). O patch a seguir ainda não suporta controladores ARM.

Se esse patch for implementado, assim como o suporte ao ARM, terminarei e publicarei a versão com o QMK. Enquanto isso, desenhei um esboço para o Arduino, com base no trabalho de outra pessoa.

Ele possui dois modos, um é o layout QWERTY padrão e um joystick com um botão, e o segundo é o local em que todas as teclas são atribuídas aos botões do joystick. Como resultado, existem botões suficientes para configurar o configurador do controlador Steam, e ele pode ser usado como um dispositivo XInput com suporte para uma ampla gama de jogos.

Código opcional para dar um nome ao 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