冰冻果子露:人体工学游戏键盘

Billiam自己动手博客中文章的翻译

不再生产Logitech G13后不久,它就和我一起分解了,我决定开发一个替代品,Sherbet称之为。

首先-发生了什么:


带操纵杆的键盘。

打印文件和组装说明:www.prusaprinters.org/prints/5072-sherbet-gaming-keypad

设计


我想制作一个类似G13的模拟拇指操纵杆,因此我决定在项目中包括其他键盘的一些人体工程学改进-Dactyl 键盘Dactyl ManuformKinesis AdvantageErgodox具体来说-键从垂直方向的偏移,高度的偏移,列的曲率和更方便的倾斜。

我选择了低速键盘开关(线性NovelKeys Kailh Chocs)来减小键盘的高度-尤其是因为我的桌子比需要舒适键盘组的桌子高,并且下面有一个大架子。在架子和桌面之间,大约还有10 cm的空间,用于键盘和刷子。如果您在这个地方没有问题,我建议您使用一个更兼容的开关-这样您在选择按钮时就不会有问题。我还建议从Dactyl或Dactyl-Manuform开始,因为这个项目花费了我比预期更多的时间和精力。

我首先根据开关的规格对Kailh的规范进行建模,然后才找到它们,然后尝试找到方便的列曲率,打印了几个测试版本,然后再打印一些以检查垂直压痕。那就是我得到的:


选择弯曲半径的扬声器


选择布局和柱高


设计键,


从上方可以看到¾ 视图,可以看到中移扬声器的


正视图,可以看到扩音柱的高度

选择方案,我开始设计必须成功的开关连接器主垫板。


钥匙板型号


卸下和清洁支架后第一次打印


带开关的板


添加钥匙后

我拿起键盘的不同角度,并以大约20度的角度停下来。再次进行此操作,以便在键盘上方保留一个位置。将角度稍大些会更方便,但仍然比平板G13和我目前的人体工程学键盘更方便。在Fusion 360中,我使倾斜角度可变,以便项目的其余部分适应该角度。在使项目复杂化之后,就不能在不破坏其他参数的情况下配置该参数。


印刷支撑板,用于倾斜选择

刷架


然后我开始在刷架上工作。我需要一个适合我的手的舒适支架,我想为此制作模具。我制作了一个黏土支架,然后使用Meshroom 摄影测量软件包(和一堆照片)创建了3D模型,然后对其进行缩放以使其大小与原始模型匹配。使用聚合物黏土粗略地模拟支架,多张照片,从Meshroom中分离出带有纹理的支架,将模型导入Fusion 360, 然后我沿着模型的主要轮廓走动,使其平滑并得到所需的打印效果:扫描的模型旁边有聚合物黏土的叠加3D 模型带有平滑和打印的版本





















这样做很有趣,而且结果很方便,但是后来我发现在打印过程中所有这些弯曲都妨碍了手的移动,因此以后的版本都是平的。好…

住房


然后,我开始研究设备的一般情况,这是所有设备中最困难的阶段。我在CAD方面仍然工作不确定,并且以前没有做过任何复杂的事情。试图找到描述复合曲线并连接两个曲面的方法非常困难,并且在该项目中花费了大部分时间。


外壳的计算机模型以及Arduino游戏杆和拇指按钮。

我还发现了更方便的渲染环境,使图像更好。


翻新的电脑机箱模型


电脑机箱模型,在

我只打印了拇指区域以检查其便利性和位置。一切看起来都很正常,但是操纵杆模块的体积太大,无法从人机工程学的角度准确放置在最佳位置。


带有按钮的印刷操纵杆,

相反,我从第三方制造商那里购买了用于Nintendo Switch的小得多的Joy-Con控制器,可以将其放置在更靠近按键的位置,并且仍有连接的空间。它连接到5根0.5毫米扁平电缆(不太方便)。我在亚马逊上拿了6个联系人的接口板,但是从eBay或AliExpress拿走它们要便宜得多。Joy-Con操纵杆与开关


操纵杆比较





改进的小操纵杆

外壳在完成外壳后,我增加了对电子组件的支持。对于操纵杆,有必要制作一个小的保护面板,该保护面板拧在外壳的侧面。对于Teensy微控制器,我做了一个固定器,将其牢固地固定在其中,并用螺丝固定。增加了用于塑料夹的空间,并为连接刷子的支架增加了4毫米的孔。

我还将微型USB接口用于主USB控制器,以使微控制器不会磨损或损坏。 在整个设计阶段中,我认为


案件的内部

总共花了一个月的时间。我不会试图猜测花费了多少时间-假设有“很多时间”。

打印


在花了很多时间进行设计和测试之后,在我看来,该项目的最终印刷版很无聊,没有发生任何意外。


在Maker Select Plus上以0.2毫米分辨率打印需要15个小时,


后期处理:删除备份并进行清洁。损坏程度很小。


外壳的上部装有已安装的电子设备,

我还用白色塑料印刷了盖子,并在上面涂了软木涂层。要将盖子连接到主体,我使用M3螺钉和通过加热插入塑料中的螺纹配合套筒


防滑软木塞套

绘画


在设计过程中,我研究了几种颜色解决方案,并最终选择了类似的解决方案。由于键只有黑色和白色,因此没有多少颜色选项。


着色项目

为了获得最终效果,我首先用220粒度的砂纸打磨零件,以平滑各层和其他问题上的条纹,然后用底漆覆盖一切。


第一层土壤

涂完底漆后,我使用了邦多(Bondo)的腻子,将零件(砂纸220和600)打磨,然后再次用底漆覆盖,腻子再打磨。


经过两遍底漆和腻子,并用硬砂纸打磨,


另一层,用白色底漆

,我做了一条带有薄乙烯基膜的条,然后用喷枪用粉红色覆盖了这个地方。


外壳和油漆残留

涂漆后,我在零件上涂了4-5层光泽涂层,然后用1200砂纸打磨,以去除灰尘,绒毛和小虫,然后再次上光。

看起来不错,但是可以看到粗糙的表面和多余的清漆斑点。我用砂纸1200打磨了最坏的地方,然后用特殊的化合物打磨。


研磨抛光后

部件


我通过将1.5 mm的陶瓷球从轴承压入键中,制成了用于盲目定位的粉刺。照片显示一个球洞太大和一个球洞太小,我将球压入其中(没有胶水)。我不知道,将其插入带有胶水的大孔中比将球推入那里使塑料变形更好。



当我用尽了所有有助于进一步拖延的任务时,我就开始连接电线,从键的行和列开始。

我在当地商店找不到较细的电线,唯一售出的单芯电线是22 awg [ 横截面0.325平方毫米。 /大约佩雷夫。]-并且很难在列偏移量周围弯曲并将其塞入外壳和开关之间的狭小空间中。取而代之的是,我使用28 awg绞合线[ 横截面0.089mm.kv。 /大约佩雷夫。从扁平电缆中拉出],然后用剥线钳将其剥开,然后在其端部形成回路。使用较细的单芯电缆,一切将变得更加简单。


行和列已焊接的按键开关


行和列连接到扁平电缆行,列,操纵杆和USB接口板已连接




我做了一个刷架,固定在两个M4螺钉上,这些螺钉通过加热拧入插入主体塑料的套筒中。安装完联轴器后,结果发现孔的配合不是很好,所以我还不能组装它们。我计划用要插入M4螺母的孔来重新键入外壳,这将使对准更加容易。

实际上,您需要以某种方式将键盘固定在桌子上。即使软木塞底部和额外的重量,使用操纵杆时键盘也会移动。

PS:我重做并重新键入了支架,使其使用了两个M4螺母,并且一切正常。

做完了!



现成的外壳和临时刷架


外壳,底视图

固件


最初,我计划将QMK用作固件,并使用主分支中尚未包含的带有操纵杆支持的池请求。但是,QMK不能很好地支持新的ARM Teensy控制器(版本大于3.2)。以下修补程序尚不支持ARM控制器。

如果实现了此补丁以及ARM支持,我将完成并发布QMK版本。同时,我根据其他人的工作为Arduino绘制了草图。

它有两种模式,一种是标准QWERTY布局,一种是带有一个按钮的操纵杆,第二种是将所有键分配给操纵杆按钮的模式。因此,有足够的按钮来配置Steam控制器配置器,它可以用作XInput设备,并支持更广泛的游戏。

可选代码,为设备命名
// , . , 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
};



固件
/*
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