شربات: لوحة مفاتيح ألعاب مريحة

ترجمة مقال من مدونة Billiam do-it- بنفسك -

بعض الوقت بعد توقف إنتاج لوجيتك G13 ، انهار معي ، وقررت تطوير بديل له ، والذي اتصل به شربات.

أولاً - ما حدث:


لوحة المفاتيح بعصا التحكم.

طباعة الملفات وتعليمات التجميع: www.prusaprinters.org/prints/5072-sherbet-gaming-keypad

التصميم


كنت أرغب في صنع عصا تحكم الإبهام التناظرية مثل G13 ، وقررت تضمين العديد من التحسينات المريحة من لوحات المفاتيح الأخرى في المشروع - لوحة مفاتيح Dactyl و Dactyl Manuform و Kinesis Advantage و Ergodox . على وجه التحديد - تحول المفاتيح من العمودي ، والتحول في الارتفاع ، وانحناء الأعمدة وإمالة أكثر ملاءمة.

اخترت مفاتيح لوحة مفاتيح منخفضة السرعة (NovelKeys Kailh Chocs الخطي) لتقليل ارتفاع لوحة المفاتيح - خاصة لأن مكتبي أطول مما تحتاجه لمجموعة لوحة مفاتيح مريحة ويوجد رف كبير أسفلها. بين الرف والطاولة يتبقى حوالي 10 سم ، لكل من لوحة المفاتيح والفرشاة. إذا لم تكن لديك مشاكل مع المكان ، فإنني أوصي بمفتاح أكثر توافقًا - فلن تكون لديك مشاكل في اختيار الأزرار الخاصة به. أوصي أيضًا بالبدء بـ Dactyl أو Dactyl-Manuform ، حيث استغرقني هذا المشروع وقتًا وجهدًا أكثر مما كنت أتوقع.

لقد بدأت بنمذجة المفاتيح استنادًا إلى مواصفات Kailh للمفاتيح قبل أن تأتي إلي ، ثم حاولت العثور على انحناء عمود مناسب ، وطبعت نسختين تجريبيتين ، ثم طبعت عددًا أقل للتحقق من المسافة البادئة العمودية. هذا ما حصلت عليه:


اختيار


تصميم الشعاع الانحناء تخطيط السماعة وارتفاع العمود


مفاتيح التصميم ، طريقة عرض ¾


من الأعلى ، يمكن للمرء أن يرى السماعة


المتحركة منظر أمامي ، رأيت

مخطط الأعمدة المنتشرة المختارة ، بدأت في تصميم الموصلات للمفاتيح التي يجب أن تنجح لوحة الدعم الرئيسية.


طراز لوحة المفاتيح


الطباعة الأولى بعد إزالة الدعامات وتنظيفها.


اللوحة المزودة بمفاتيح.


بعد إضافة المفاتيح

التقطت زوايا مختلفة من لوحة المفاتيح ، وتوقفت عند زاوية حوالي 20 درجة. وتم ذلك مرة أخرى بحيث كان هناك مكان فوق لوحة المفاتيح. سيكون أكثر ملاءمة لجعل الزاوية أكبر قليلاً ، لكنها لا تزال أكثر ملاءمة من G13 المسطحة ولوحة المفاتيح المريحة الحالية. في Fusion 360 ، قمت بتغيير زاوية الإمالة بحيث يتكيف معها باقي المشروع. بعد تعقيد المشروع ، لم يعد من الممكن تكوين هذه المعلمة دون كسر الآخرين.


لوح دعم مطبوع لاختيار الإمالة

حامل الفرشاة


ثم بدأت العمل على حامل الفرشاة. كنت بحاجة إلى حامل مريح يناسب يدي ، وأردت صنع قالب له. لقد صنعت حاملًا من الصلصال ، ثم استخدمت حزمة التصوير الفوتوغرافي Meshroom (ومجموعة من الصور) لإنشاء نموذج ثلاثي الأبعاد ، ثم تحجيمه بحيث يتطابق حجمه مع الأصل.


تقريب تقريبي للحامل باستخدام طين البوليمر.


العديد من الصور.


تقسيم الحامل باستخدام مواد من غرفة النوم.


تم استيراد النموذج إلى Fusion 360.

ثم مشيت على طول الخطوط الرئيسية للنموذج ، وصقلته وحصلت على الطباعة المطلوبة:


نموذج ممسوح ضوئيًا مع نموذج ثلاثي الأبعاد متراكب


من طين البوليمر بجواره مع نسخة سلسة ومطبوعة

كان من المثير للاهتمام القيام بذلك ، وكانت النتيجة مريحة ، ولكن بعد ذلك اكتشفت أنه أثناء الطباعة كل هذه الانحناءات تمنع اليد من الحركة ، لذلك تكون الإصدارات الأحدث مسطحة. حسنا…

الإسكان


ثم بدأت العمل على الحالة العامة للجهاز ، وتبين أن هذه هي أصعب مرحلة على الإطلاق. ما زلت أعمل بشكل غير مؤكد في CAD ، ولم أفعل أي شيء معقد للغاية من قبل. أثبتت محاولات إيجاد طرق لوصف المنحنيات المركبة وربط السطحين أنها صعبة للغاية ، واستغرقت معظم الوقت في هذا المشروع.


نموذج كمبيوتر للحالة مع عصا التحكم وأزرار الإبهام من Arduino.

لقد وجدت أيضًا لنفسي بيئة أكثر ملاءمة للعرض ، ونتيجة لذلك أصبحت الصور أفضل.


نموذج حالة الكمبيوتر المعاد تصنيعه نموذج


حالة الكمبيوتر ، عرض في ،

قمت بطباعة منطقة الإبهام فقط للتحقق من ملاءمتها وموقعها. اتضح أن كل شيء طبيعي ، لكن وحدة عصا التحكم كانت ضخمة جدًا بحيث لا يمكن وضعها تمامًا حيث ستكون الأفضل من وجهة نظر بيئة العمل.


عصا تحكم مطبوعة بأزرار.

بدلاً من ذلك ، اشتريت وحدة تحكم Joy-Con أصغر بكثير لـ Nintendo Switch من جهة تصنيع تابعة لجهة خارجية ، والتي يمكن وضعها بالقرب من المفاتيح ، ولا يزال هناك مجال للاتصال. يتصل بكبل مسطح (أقل ملاءمة) من 5 أسلاك 0.5 مم. لقد أخذت لوحة الواجهة لـ 6 جهات اتصال على Amazon ، ولكن من الأرخص بكثير أخذها من eBay أو AliExpress.


مقارنة


المقود بواسطة Joy-Con المقود مع المفاتيح


مبيت معدّل لعصا تحكم صغيرة

بعد انتهائي من غلاف ، أضفت دعمًا للمكونات الإلكترونية. بالنسبة لعصا التحكم ، كان من الضروري عمل لوحة واقية صغيرة مثبتة على جانب السكن. بالنسبة إلى وحدة التحكم الدقيقة Teensy ، صنعت حاملًا ، والذي يناسبه ببساطة ، ومسامير. مساحة إضافية للمشابك البلاستيكية وفتحات 4 مم لربط الحامل للفرشاة.

أستخدم أيضًا واجهة USB الصغيرة لوحدة تحكم USB الرئيسية بحيث لا يتآكل المتحكم ويتلف. أعتقد أن


الدواخل الداخلية للحالة

استغرقت شهرًا كاملاً طوال مرحلة التصميم هذه. لن أحاول تخمين عدد الساعات التي استغرقتها - لنفترض أن هناك "الكثير منها".

طباعة


بعد أن أمضيت الكثير من الوقت في التصميم والاختبار ، بدا لي أن الطباعة النهائية للمشروع مملة وبدون حوادث.


تستغرق الطباعة بدقة 0.2 مم على Maker Select Plus 15 ساعة.


المعالجة اللاحقة: إزالة النسخ الاحتياطية والتنظيف. تبين أن الضرر كان صغيرا.


الجزء العلوي من العلبة بإلكترونيات مثبتة ،

كما قمت بطباعة الغلاف بالبلاستيك الأبيض ، ولصقه بطبقة من الفلين. لتوصيل الغطاء بالجسم ، أستخدم مسامير M3 وجلبة تزاوج مترابطة يتم إدخالها في البلاستيك عن طريق التسخين .


غطاء من الفلين مانع للانزلاق

لوحة


أثناء التصميم ، استعرضت العديد من حلول الألوان ، واستقرت في النهاية على حل مماثل. لا توجد العديد من خيارات الألوان ، لأن المفاتيح باللونين الأبيض والأسود فقط.


مشروع التلوين

للإنهاء النهائي ، قمت أولاً بصنفرة الجزء بورق صنفرة 220 حصى لتنعيم الشرائط من الطبقات والمشاكل الأخرى ، ثم غطيت كل شيء باستخدام برايمر.


الطبقة الأولى من التربة

بعد التمهيدي ، استخدمت المعجون من Bondo ، صنفرت الجزء (ورق الصنفرة 220 و 600) ، ثم غطيت مرة أخرى مع التمهيدي والمعجون والرمل مرة أخرى.


بعد مررين مع التمهيدي والمعجون والرمل الصلب.طبقة


أخرى ، مع التمهيدي الأبيض

، قمت بعمل شريط من فيلم الفينيل الرقيق ، ثم غطيت هذا المكان باللون الوردي من مسدس الرش.


بقايا الغلاف والطلاء

بعد الطلاء ، قمت بطلاء الجزء بطبقات 4-5 من الطلاء اللامع ، ثم صنفرته بحبيبات 1200 لإزالة الحبيبات ، الزغب والحشرات ، ثم تلميعها مرة أخرى.

تبدو جيدة ، ولكن خشونة وبقع الورنيش الزائد مرئية. لقد صقلت أسوأ الأماكن قليلاً باستخدام ورق الصنفرة 1200 ، ثم صقلها بمركب خاص.


بعد الطحن والتلميع

المجسم


لقد صنعت البثور للتوجيه الأعمى بالضغط على كرات سيراميك 1.5 مم من المحامل إلى المفاتيح. تظهر الصورة فتحة واحدة كبيرة جدًا وصغيرة جدًا ، حيث ضغطت على الكرة (بدون غراء). لا أدري ، سيكون من الأفضل إدخاله في حفرة كبيرة بغراء بدلاً من تشويه البلاستيك بدفع الكرة هناك.



عندما استنفدت جميع المهام التي ستساعدني على التعامل مع التسويف بشكل أكبر ، بدأت في توصيل الأسلاك ، بدءًا من صفوف وأعمدة المفاتيح.

لم أجد أسلاك أرق في المتاجر المحلية ، وكان السلك الوحيد أحادي النواة الذي تم بيعه 22 awg [ المقطع العرضي 0.325 مم مربع. / تقريبا. perev.] - وسيكون من الصعب جدًا الانحناء حول إزاحة العمود ووضعه في مساحة صغيرة بين العلبة والمفاتيح. بدلاً من ذلك ، استخدمت سلكًا مجدلاً 28 awg [ المقطع العرضي 0.089 مم. kv. / تقريبا. perev. ] ، الذي تم سحبه من كبل مسطح ، ومجرد تجريده من خلال أداة تعرية الأسلاك ، ثم عمل حلقات في الأطراف. مع كابل أنحف أحادي النواة ، سيكون كل شيء أبسط.


المفاتيح الرئيسية ذات الصفوف والأعمدة


الملحومة الصفوف والأعمدة المتصلة بكابل مسطح الصفوف والأعمدة وعصا التحكم ولوحة واجهة USB متصلة




لقد صنعت حامل فرشاة مثبتًا بمسمارين M4 ، مثبتين في جلبة مُدخلة في بلاستيك الجسم الرئيسي عن طريق التسخين. بعد تثبيت أدوات التوصيل ، اتضح أن الثقوب لا تتطابق جيدًا ، لذا لا يمكنني تجميعها بعد. أخطط لإعادة كتابة الحالة بالفتحة التي سيتم فيها إدخال صمولة M4 ، سيكون من الأسهل محاذاة ذلك.

في الممارسة العملية ، تحتاج إلى تأمين لوحة المفاتيح بطريقة ما على الطاولة. حتى مع وجود قاع من الفلين ووزن إضافي ، تتغير لوحة المفاتيح عند استخدام عصا التحكم.

ملاحظة: قمت بإعادة تصميم الحامل وإعادة كتابته بحيث يستخدم صواميل M4 ، وكل شيء يعمل بشكل جيد.

منجز!



مبيت جاهز وحامل فرشاة مؤقت


، منظر سفلي

البرامج الثابتة


في البداية ، خططت لاستخدام QMK كبرنامج ثابت ، باستخدام طلب تجمع مع دعم عصا التحكم لم يتم تضمينه بعد في الفرع الرئيسي. ومع ذلك ، لا تدعم QMK بشكل جيد وحدات تحكم ARM Teensy الجديدة (الإصدارات الأكبر من 3.2). التصحيح التالي لا يدعم حتى الآن وحدات تحكم ARM.

إذا تم تنفيذ هذا التصحيح ، بالإضافة إلى دعم ARM ، فسأنهي الإصدار ونشره باستخدام QMK. في هذه الأثناء ، رسمت رسمًا لأردوينو ، بناءً على عمل شخص آخر.

يحتوي على وضعين ، أحدهما هو تخطيط 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