- PVSM.RU - https://www.pvsm.ru -
Перевод статьи из блога самодельщика Billiam [1]
Через некоторое время после того, как мою Logitech G13 перестали выпускать, она у меня сломалась, и я решил разработать для неё замену, которую назвал Sherbet.
Сначала – что получилось:
Клавиатура с джойстиком
Файлы для печати и инструкция по сборке: www.prusaprinters.org/prints/5072-sherbet-gaming-keypad [2]
Мне хотелось сделать аналоговый джойстик под большой палец, как и у G13, а ещё я решил включить в проект несколько эргономических улучшений от других клавиатур — Dactyl keyboard [3], Dactyl Manuform [4], Kinesis Advantage [5] и Ergodox [6]. Конкретно – смещение клавиш от вертикали, смещения по высоте, кривизна столбцов и более удобный наклон.
Я выбрал переключатели клавиатуры с малым ходом (линейные Kailh Choc от NovelKeys), чтобы уменьшить высоту клавиатуры – в частности потому, что мой стол выше, чем нужно для комфортабельного клавиатурного набора, а под ним расположена большая полка. Между полкой и столешницей остаётся порядка 10 см, как для клавиатуры, так и для кисти. Если с местом у вас проблем нет, рекомендую более совместимый переключатель – тогда у вас не будет проблем с выбором кнопок для него. Также рекомендую начать с Dactyl или Dactyl-Manuform, поскольку этот проект отнял у меня гораздо больше времени и сил, чем я мог бы предположить.
Я начал с моделирования клавиш на основе спецификаций от Kailh для переключателей ещё до того, как они ко мне пришли, потом попытался найти удобную кривизну столбца, распечатал пару тестовых версий, а потом распечатал ещё несколько для проверки отступов по вертикали. Вот, что у меня получилось:
Подбор радиусов изгиба колонок
Подбор раскладки и высоты колонки
Проектирование клавиш, вид на ¾
Вид сверху, видно сдвиг колонок
Вид спереди, видно разброс колонок по высоте
Выбрав схему, я начал проектировать разъёмы для переключателей, из которых должна получится главная опорная пластина.
Модель пластины для клавиш
Первая печать после удаления и очистки опор
Пластина с переключателями
После добавления клавиш
Я подбирал различные углы наклона клавиатуры, и остановился на угле порядка 20 градусов. И это опять было сделано для того, чтобы над клавиатурой осталось место. Удобнее было бы сделать угол чуть побольше, однако это всё равно удобнее плоской G13 и моей текущей эргономической клавиатуры. В Fusion 360 я сделал угол наклона изменяемым, так, чтобы весь остальной проект подстраивался под него. После усложнения проекта этот параметр уже нельзя было настраивать, не поломав других.
Пластина с распечатанной опорой для выбора угла наклона
Затем я начал работу над подставкой для кисти. Мне нужна была удобная подставка, подходящая для моей руки, и я хотел сделать для неё литейную форму. Я слепил подставку из полимерной глины, потом использовал фотограмметрический [7] пакет Meshroom [8] (и кучу фотографий) для создания 3D-модели, а потом масштабировал её так, чтобы её размер совпал с оригиналом.
Грубое приближение к подставке при помощи полимерной глины
Много фоток
Подель подставки с текстурами из Meshroom
Модель, импортированная в Fusion 360
Затем я просто прошёлся по главным контурам модели, сгладил её и получил нужный оттиск:
Отсканированная модель с наложенной 3D-моделью
Модель из полимерной глины рядом со сглаженной и распечатанной версией
Этим было интересно заниматься, и результат получился удобным, однако потом я обнаружил, что во время печати все эти изгибы мешают двигаться руке, поэтому более поздние варианты все плоские. Ну, что ж…
Затем я начал работать над общим корпусом устройства, и это оказалось наиболее трудным этапом из всех. Я до сих пор неуверенно работаю в CAD, и ничего такого сложного ранее не делал. Попытки найти способы описать составные кривые и соединить две поверхности оказались очень сложными, и заняли большую часть времени в этом проекте.
Компьютерная модель корпуса вместе с джойстиком от Arduino и кнопками под большой палец.
Также я нашёл для себя более удобное окружение для рендеринга [9], в результате чего изображения стали лучше.
Повторно сделанная компьютерная модель корпуса
Компьютерная модель корпуса, ракурс в ¾
Я распечатал только область для большого пальца, чтобы проверить её удобство и расположение. Всё оказалось нормально, однако модуль джойстика оказался слишком объёмным, чтобы его можно было разместить именно там, где это было бы наилучшим с точки зрения эргономики.
Распечатанный джойстик с кнопками
Вместо него я приобрёл гораздо меньший по размеру контроллер Joy-Con для Nintendo Switch от стороннего производителя, который можно разместить гораздо ближе к клавишам, и ещё останется место для подсоединения. Он соединяется с (менее удобным) плоским кабелем на 5 проводов 0,5 мм. Интерфейсную плату на 6 контактов я взял на Amazon, но гораздо дешевле брать их с eBay или AliExpress.
Сравнение джойстиков по размеру
Джойстик от Joy-Con с переключателями
Изменённый корпус для маленького джойстика
Закончив с оболочкой, я добавил поддержку для электронных компонентов. Для джойстика нужно было сделать небольшую защитную панель, вкручивающуюся сбоку в корпус. Для микроконтроллера Teensy [10] я сделал держатель, в который он просто плотно входит, и винты для него. Добавил места для пластиковых хомутов и отверстия на 4 мм для крепления подставки для кисти.
Также я использую интерфейс micro USB для главного USB-контроллера, чтобы микроконтроллер не изнашивался и не повреждался.
Внутренности корпуса
Думаю, на всю эту фазу проектирования ушёл в общей сложности месяц. Не буду пытаться угадать количество ушедших на неё часов – скажем, что их было «много».
После того, как я потратил так много времени на проектирование и испытания, мне показалось, что окончательная печать проекта прошла скучно и без происшествий.
Печать с разрешением в 0,2 мм на Maker Select Plus заняла 15 часов
Постобработка: удаление подпорок и очистка. Повреждения оказались небольшими.
Нижняя часть корпуса с установленной электроникой
Крышку я тоже напечатал белым пластиком, и приклеил к ней пробковое покрытие. Для соединения крышки с корпусом использую винты M3 и ответную резьбовую муфту, вставляемую в пластик при помощи разогрева [11].
Крышка с нескользящим пробковым покрытием
Во время проектирования я перебрал несколько цветовых решений, и в итоге остановился на подобном. Вариантов цветов не очень много, поскольку клавиши бывают только чёрными и белыми.
Проект раскраски
Для финальной отделки я сначала отшлифовал деталь наждачкой на 220, чтобы сгладить полоски от слоёв и другие проблемы, а потом покрыл всё грунтовкой.
Первый слой грунта
После грунтовки я использовал шпаклёвку от Bondo, отшлифовал деталь (наждачка 220 и 600), потом опять покрыл грунтом, шпаклёвкой, и снова отшлифовал.
После двух проходов грунтом и шпаклёвкой, и упорной шлифовки
Ещё один слой, белым грунтом
Полоску я сделал при помощи тонкой виниловой плёнки, а потом покрыл это место розовым цветом из распылителя.
Корпус и остатки краски
После покраски я покрыл деталь 4-5 слоями глянцевого покрытия, а потом шлифанул наждачкой на 1200, чтобы убрать пыль, пух и жучков, а потом снова покрыл лаком.
Выглядит неплохо, однако видны шороховатости и пятна излишков лака. Самые плохие места я немного шлифанул наждачкой на 1200, а потом отполировал специальным составом.
После шлифовки и полировки
Пупырышки для ориентации вслепую я сделал, вдавив в клавиши керамические шарики от подшипников на 1,5 мм. На фото видно одно слишком крупное отверстие и одно слишком мелкое, в которое я вдавил шарик (без клея). Не знаю, было бы лучше вставить его в крупное отверстие с клеем, чем деформировать пластик, впихивая туда шарик.
Когда я исчерпал все задачи, которые помогли бы мне заниматься прокрастинацией и далее, я занялся подключением проводов, начав с рядов и столбцов клавиш.
Тоньше проводов в местных магазинах я не нашёл, и единственный провод с одним сердечником, который продавался, был на 22 awg [сечение 0,325 мм.кв. / прим. перев.], который было бы слишком тяжело загибать вокруг смещений столбцов и запихивать в небольшое пространство между корпусом и переключателями. Вместо него я использовал многожильный провод на 28 awg [сечение 0,089 мм.кв. / прим. перев.], который надёргал из плоского кабеля, зачистил при помощи инструмента для зачистки проводов, а потом сделал петельки на концах. С более тонким одножильным кабелем всё было бы проще.
Переключатели клавиш со спаянными рядами и столбцами
Ряды и столбцы соединены с плоским кабелем
Подсоединены ряды, столбцы, джойстик и интерфейсная плата USB
Подставку под кисть я сделал крепящейся на два винта М4, которые ввинчиваются в муфту, вставленную в пластик основного корпуса разогревом. После установки муфт получилось, что отверстия не очень совпадают, поэтому пока я их собрать не могу. Планирую перепечатать корпус с отверстием, в которое будет вставляться гайка М4, её будет проще выровнять.
На практике требуется как-то прочнее закрепить клавиатуру на столе. Даже с пробковым дном и дополнительным весом клавиатура сдвигается при использовании джойстика.
PS: Я переделал и перепечатал подставку так, чтобы она использовала две гайки М4, и всё работает нормально.
Готовый корпус и временная подставка под кисть
Корпус, вид снизу
Изначально я планировал использовать в качестве прошивки QMK, используя пока ещё не вошедший в основную ветку пул-реквест с поддержкой джойстика. Однако QMK не очень хорошо поддерживает новые контроллеры ARM Teensy (версии больше 3.2). Следующий патч пока не поддерживает контроллеры ARM.
Если этот патч будет реализован, как и поддержка ARM, я закончу и опубликую вариант с QMK. А пока я набросал скетч для Arduino, на базе чьей-то работы.
У него есть два режима, один – стандартная раскладка QWERTY и джойстик с одной кнопкой, а второй – где все клавиши назначены на кнопки джойстика. В итоге хватает кнопок для настройки конфигуратора контроллера Steam, и его можно использовать как устройство XInput с поддержкой более широкого спектра игр.
#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
};
// 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;
}
Автор: Вячеслав Голованов
Источник [13]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/diy-ili-sdelaj-sam/349213
Ссылки в тексте:
[1] Billiam: https://www.billiam.org
[2] www.prusaprinters.org/prints/5072-sherbet-gaming-keypad: https://www.prusaprinters.org/prints/5072-sherbet-gaming-keypad
[3] Dactyl keyboard: https://github.com/adereth/dactyl-keyboard
[4] Dactyl Manuform: https://github.com/abstracthat/dactyl-manuform
[5] Kinesis Advantage: https://kinesis-ergo.com/shop/advantage2/
[6] Ergodox: https://www.ergodox.io/
[7] фотограмметрический: https://ru.wikipedia.org/wiki/%D0%A4%D0%BE%D1%82%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B5%D1%82%D1%80%D0%B8%D1%8F
[8] Meshroom: https://alicevision.github.io/
[9] окружение для рендеринга: https://www.maximeroz.com/hdri
[10] микроконтроллера Teensy: https://www.pjrc.com/teensy/teensyLC.html
[11] вставляемую в пластик при помощи разогрева: https://markforged.com/blog/heat-set-inserts/
[12] forum.pjrc.com/threads/55395-Keyboard-simple-firmware: https://forum.pjrc.com/threads/55395-Keyboard-simple-firmware
[13] Источник: https://habr.com/ru/post/491742/?utm_source=habrahabr&utm_medium=rss&utm_campaign=491742
Нажмите здесь для печати.