Sherbet: keyboard gaming yang ergonomis

Terjemahan sebuah artikel dari blog Billiam do-it- yourself

Beberapa waktu setelah saya Logitech G13 tidak lagi diproduksi, itu rusak dengan saya, dan saya memutuskan untuk mengembangkan pengganti untuk itu, yang disebut Sherbet.

Pertama - apa yang terjadi:


Keyboard dengan joystick.

Mencetak file dan instruksi perakitan: www.prusaprinters.org/prints/5072-sherbet-gaming-keypad

Rancangan


Saya ingin membuat joystick jempol analog seperti G13, dan saya memutuskan untuk memasukkan beberapa peningkatan ergonomis dari keyboard lain dalam proyek ini - keyboard Dactyl , Dactyl Manuform , Kinesis Advantage dan Ergodox . Secara khusus - pergeseran tombol dari vertikal, pergeseran ketinggian, lengkungan kolom dan kemiringan yang lebih nyaman.

Saya memilih switch keyboard kecepatan rendah (NovelKeys Kailh Chocs linear) untuk mengurangi ketinggian keyboard - khususnya karena meja saya lebih tinggi daripada yang Anda butuhkan untuk set keyboard yang nyaman dan ada rak besar di bawahnya. Antara rak dan permukaan meja ada sekitar 10 cm tersisa, baik untuk keyboard dan untuk sikat. Jika Anda tidak memiliki masalah dengan tempat itu, saya sarankan saklar yang lebih kompatibel - maka Anda tidak akan memiliki masalah dengan pilihan tombol untuk itu. Saya juga merekomendasikan memulai dengan Dactyl atau Dactyl-Manuform, karena proyek ini membawa saya lebih banyak waktu dan usaha daripada yang saya harapkan.

Saya mulai dengan memodelkan kunci berdasarkan spesifikasi Kailh untuk sakelar sebelum bahkan datang kepada saya, kemudian saya mencoba menemukan kelengkungan kolom yang nyaman, mencetak beberapa versi uji, dan kemudian mencetak beberapa lagi untuk memeriksa lekukan vertikal. Inilah yang saya dapatkan:


Memilih jari-jari lengkung kolom


Memilih tata letak dan tinggi kolom


Merancang tombol, melihat ¾ Tampilan


atas, Anda dapat melihat pergeseran kolom


Tampak depan, Anda dapat melihat penyebaran kolom dengan tinggi.

Memilih skema, saya mulai merancang konektor untuk sakelar, yang harus dibuat piring dukungan utama.


Model pelat kunci


Pertama-tama mencetak setelah melepas dan membersihkan penopang.


Piring dengan sakelar


Setelah menambahkan kunci

Saya mengambil sudut keyboard yang berbeda, dan berhenti pada sudut sekitar 20 derajat. Dan ini dilakukan lagi sehingga ada tempat di atas keyboard. Akan lebih nyaman untuk membuat sudut sedikit lebih besar, tetapi masih lebih nyaman daripada flat G13 dan keyboard ergonomis saya saat ini. Di Fusion 360, saya membuat sudut kemiringan dapat diubah sehingga sisa proyek menyesuaikan dengannya. Setelah menyulitkan proyek, parameter ini tidak dapat lagi dikonfigurasi tanpa merusak yang lain.


Plat dukungan cetak untuk pemilihan kemiringan

Dudukan sikat


Kemudian saya mulai mengerjakan dudukan sikat. Saya membutuhkan dudukan yang nyaman yang cocok untuk tangan saya, dan saya ingin membuat cetakan untuk itu. Saya membentuk dudukan polimer tanah liat, kemudian menggunakan paket fotogrametri Meshroom (dan banyak foto) untuk membuat model 3D, dan kemudian menskala sehingga ukurannya sesuai dengan aslinya.


Pendekatan kasar ke dudukan menggunakan tanah liat polimer.


Banyak foto.


Membingkai dudukan dengan tekstur dari Meshroom.


Model diimpor ke Fusion 360.

Lalu saya hanya berjalan di sepanjang kontur utama model, melicinkannya dan mendapatkan cetakan yang diinginkan:


Model yang dipindai dengan model 3D


dari tanah liat polimer di sebelahnya dengan versi yang dihaluskan dan dicetak

Sangat menarik untuk melakukan ini, dan hasilnya nyaman, tetapi kemudian saya menemukan bahwa selama pencetakan semua lengkungan ini mencegah tangan bergerak, sehingga versi yang lebih baru semuanya rata. Baik…

Perumahan


Lalu saya mulai bekerja pada kasus umum perangkat, dan ini ternyata menjadi tahap yang paling sulit. Saya masih bekerja dengan tidak pasti di CAD, dan belum pernah melakukan hal yang begitu rumit sebelumnya. Upaya untuk menemukan cara untuk menggambarkan kurva komposit dan menghubungkan kedua permukaan terbukti sangat sulit, dan menghabiskan sebagian besar waktu dalam proyek ini.


Model komputer kasing bersama dengan joystick dan tombol jempol Arduino.

Saya juga menemukan lingkungan yang lebih nyaman untuk rendering , sebagai hasilnya gambar menjadi lebih baik.


Model case komputer yang dibuat ulang Model


case komputer, lihat di,

Saya hanya mencetak area ibu jari untuk memeriksa kenyamanan dan lokasinya. Semuanya ternyata normal, tetapi modul joystick terlalu tebal untuk ditempatkan tepat di tempat yang terbaik dari sudut pandang ergonomi.


Joystick yang dicetak dengan tombol

, sebagai gantinya, saya membeli controller Joy-Con yang jauh lebih kecil untuk Nintendo Switch dari produsen pihak ketiga, yang dapat ditempatkan lebih dekat ke tombol, dan masih ada ruang untuk koneksi. Ini terhubung ke kabel datar (kurang nyaman) dari 5 kabel 0,5 mm. Saya mengambil papan antarmuka untuk 6 kontak di Amazon, tetapi jauh lebih murah untuk mengambilnya dari eBay atau AliExpress.


Perbandingan


Joystick dengan Joy-Con Joystick dengan Switch


Housing yang dimodifikasi untuk joystick kecil

Setelah selesai dengan shell, saya menambahkan dukungan untuk komponen elektronik. Untuk joystick, perlu untuk membuat panel pelindung kecil, disekrup ke sisi perumahan. Untuk mikrokontroler Teensy, saya membuat dudukan, yang pas untuknya, dan sekrup untuknya. Menambahkan ruang untuk klem plastik dan lubang 4 mm untuk memasang dudukan untuk sikat.

Saya juga menggunakan antarmuka micro USB untuk pengontrol USB utama sehingga mikrokontroler tidak aus dan rusak.


Bagian dalam casing

saya pikir, untuk keseluruhan fase desain ini, butuh total satu bulan. Saya tidak akan mencoba menebak jumlah jam yang masuk ke dalamnya - katakanlah ada "banyak dari mereka".

Mencetak


Setelah saya menghabiskan begitu banyak waktu merancang dan menguji, saya merasa bahwa cetakan akhir proyek itu membosankan dan tanpa insiden.


Mencetak dengan resolusi 0,2 mm pada Maker Select Plus membutuhkan waktu 15 jam.


Pasca pemrosesan: penghapusan cadangan dan pembersihan. Kerusakan ternyata kecil.


Bagian atas kasing dengan elektronik terpasang.

Saya juga mencetak penutup dengan plastik putih, dan menempelkan lapisan gabus padanya. Untuk menghubungkan penutup ke bodi, saya menggunakan sekrup M3 dan selongsong kawin yang dimasukkan ke dalam plastik dengan memanaskan .


Penutup gabus non-slip

Lukisan


Selama desain, saya membahas beberapa solusi warna, dan akhirnya memilih yang serupa. Tidak ada banyak pilihan warna, karena tombolnya hanya hitam dan putih.


Proyek pewarnaan

Untuk hasil akhir, saya pertama-tama mengampelas bagian dengan amplas 220 grit untuk menghaluskan strip dari lapisan dan masalah lainnya, dan kemudian menutupi semuanya dengan primer.


Lapisan pertama tanah

Setelah primer, saya menggunakan dempul dari Bondo, mengampelas bagian (amplas 220 dan 600), kemudian sekali lagi ditutupi dengan primer, dempul, dan diampelas lagi.


Setelah dua lintasan dengan primer dan dempul, dan pengamplasan keras.


Lapisan lain, dengan primer putih

, saya membuat strip menggunakan film vinil tipis, dan kemudian menutupi tempat ini dengan warna pink dari pistol semprot.


Kasing dan sisa cat

Setelah melukis, saya melapisi bagian dengan 4-5 lapisan lapisan mengkilap, dan kemudian diampelas dengan 1200 grit untuk menghilangkan debu, bulu dan bug, dan kemudian dipernis lagi.

Itu terlihat bagus, tetapi kekasaran dan bintik-bintik pernis berlebih terlihat. Saya memoles tempat-tempat terburuk sedikit dengan amplas 1200, dan kemudian memolesnya dengan senyawa khusus.


Setelah digiling dan dipoles

Majelis


Saya membuat jerawat untuk orientasi buta dengan menekan bola keramik 1,5 mm dari bantalan ke tombol. Foto menunjukkan satu lubang terlalu besar dan satu terlalu kecil, di mana saya menekan bola (tanpa lem). Saya tidak tahu, akan lebih baik memasukkannya ke dalam lubang besar dengan lem daripada merusak plastik dengan mendorong bola di sana.



Ketika saya telah menghabiskan semua tugas yang akan membantu saya mengatasi penundaan lebih lanjut, saya mulai menghubungkan kabel, mulai dengan baris dan kolom kunci.

Saya tidak menemukan kabel yang lebih tipis di toko lokal, dan satu-satunya kawat inti yang dijual adalah 22 awg [ penampang 0,325 mm persegi. / kira-kira. perev.] - dan akan terlalu sulit untuk membengkokkan offset kolom dan memasukkannya ke dalam ruang kecil antara kasing dan sakelar. Sebagai gantinya, saya menggunakan kawat terpanjang 28 awg [ penampang 0,089 mm.kv. / kira-kira. perev. ], yang ditarik dari kabel datar, ditelanjangi dengan stripper kawat, dan kemudian membuat loop di ujungnya. Dengan kabel single-core yang lebih tipis, semuanya akan lebih sederhana.


Sakelar kunci dengan baris dan kolom yang disolder


Baris dan kolom terhubung ke kabel datar Baris, kolom, joystick dan papan antarmuka USB terhubung




Saya membuat dudukan sikat diikat ke dua sekrup M4, yang disekrup ke selongsong dimasukkan ke plastik dari tubuh utama dengan pemanasan. Setelah memasang kopling, ternyata lubangnya tidak cocok dengan baik, jadi saya belum bisa memasangnya. Saya berencana mengetik ulang kasing dengan lubang di mana mur M4 akan dimasukkan, akan lebih mudah untuk menyelaraskannya.

Dalam praktiknya, Anda perlu mengamankan keyboard di atas meja. Bahkan dengan dasar gabus dan bobot ekstra, keyboard bergeser saat menggunakan joystick.

PS: Saya redid dan mengetik ulang dudukan sehingga menggunakan dua mur M4, dan semuanya bekerja dengan baik.

Selesai!



Housing siap pakai dan pemegang brush sementara


Housing, tampilan bawah

Firmware


Awalnya, saya berencana untuk menggunakan QMK sebagai firmware, menggunakan permintaan kumpulan dengan dukungan joystick yang belum termasuk dalam cabang utama. Namun, QMK tidak terlalu mendukung pengontrol ARM Teensy baru (versi lebih besar dari 3,2). Patch berikut belum mendukung pengontrol ARM.

Jika tambalan ini diterapkan, serta dukungan ARM, saya akan menyelesaikan dan menerbitkan versi dengan QMK. Sementara itu, saya membuat sketsa untuk Arduino, berdasarkan karya orang lain.

Ini memiliki dua mode, satu adalah tata letak QWERTY standar dan joystick dengan satu tombol, dan yang kedua adalah di mana semua tombol ditugaskan ke tombol joystick. Hasilnya, ada cukup tombol untuk mengonfigurasi konfigurator pengontrol Uap, dan dapat digunakan sebagai perangkat XInput dengan dukungan untuk rentang gim yang lebih luas.

Kode opsional untuk memberi nama pada perangkat
// , . , 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