Клавіатура V2.0 – 3D друк, Arduino, AS5600

Утиліта для налаштування програмованої макро-клавіатури. Простий та інтуїтивний інтерфейс дозволяє швидко налаштувати до 4 програмованих кнопок для виконання різноманітних функцій.

Основні можливості:

  • Налаштування окремих клавіш, комбінацій клавіш та введення текстових рядків
  • Бібліотека готових пресетів для найпоширеніших команд
  • Можливість зміни напрямку прокрутки
  • Збереження налаштувань безпосередньо на пристрій
  • Простий та зрозумілий інтерфейс

Програма підключається до макро-клавіатури через COM-порт та дозволяє повністю налаштувати її функціональність без спеціальних навичок програмування.

Архів програми CHD:MAKER KEYBOARD CONFIGMacroKB Windows (.zip) MacroKB MacOS (.dmg)

Модель для друку доступна тут.

Компоненти до проєкту
Тонкостінні металічні закриті підшипники 6800-6810-2RS з гумовим ущільненням.
Підшипники 30X42X7mm
Новий Pro Micro для Arduino з 2-х рядним пін-хедером, ATmega32U4 5V/16MHz, в наявності
Arduino ATmega32U4
Кнопковий перемикач з металевим корпусом та LED, водонепроникний, 12/16/19/22мм, 1NO 6V 24V.
Кнопка металевий вимикач 6V/24V
Набір з 4-х магнітних енкодерів AS5600 12 біт для високоточної вимірювання кута
Магнітний енкодер AS5600
Високоякісний понижуючий перетворювач напруги 3A MINI DC-DC 5V-23V до 3.3V 6V 9V 12V
Перетворювач напруги 3A
Інструменти задіяні під час збірки
1PCS Американський плоский інструмент PLATO 170 для вирізання дроту, кліщі бічного різання
Плоскогубці PLATO 170
Суперклей Deli 502 1/2/3шт, швидкосохнучий, міцний клей для ремонту вдома та в офісі
Суперклей Deli 502 3шт
Cтрипер Pro’sKit AWG 30-20 з антистатичним ізольованим руків’ям 1PK-3001E
Pro'sKit стрипер дроту 30-20 AWG
Лабораторний блок живлення FNIRSI DPS-150 30В 5А 150Вт з цифровим дисплеєм, портативний регулятор
Лабораторний блок живлення FNIRSI
100 шт. пластмасові крапельниці з мікро-дрібним носиком для клею 7/12 см, точний аплікатор
Клейовий дозатор 100 шт.
Оригінальний розумний паяльник FNIRSI HS01
Паяльник FNIRSI HS-01
3PCS Антистатичні керамічні пінцети з нержавіючої сталі, ізоляційні, чорні-білі
3 шт. антистатичні пінцети
Вихідний код проєкту
#include <Wire.h>
#include <Mouse.h>
#include <Keyboard.h>
#include <EEPROM.h>

#define AS5600_ADDR 0x36
#define RAW_ANGLE_HIGH 0x0C
#define RAW_ANGLE_LOW 0x0D

#define NUM_BUTTONS 4

const uint8_t buttonPins[NUM_BUTTONS] = {7, 6, 5, 4};

#define NAME_LEN 16
#define COMBO_LEN 32
#define RECORD_SIZE (NAME_LEN + COMBO_LEN + 2)
#define CONFIG_SIZE (NUM_BUTTONS * RECORD_SIZE + 1)
#define MAX_ANGLE 4096
#define SCROLL_STEP 300


char buttonNames[NUM_BUTTONS][NAME_LEN + 1];
char buttonCombos[NUM_BUTTONS][COMBO_LEN + 1];
uint8_t buttonKeyCount[NUM_BUTTONS];
uint8_t buttonKeyCodes[NUM_BUTTONS][5];
bool isStringMode[NUM_BUTTONS];
bool buttonStates[NUM_BUTTONS] = {false};
int8_t scrollDirection = -1;

int32_t totalAngle = 0;
int32_t lastRawAngle = 0;

void setup() {
  Serial.begin(115200);
  Wire.begin();
  Mouse.begin();
  Keyboard.begin();
  for (uint8_t i = 0; i < NUM_BUTTONS; i++) pinMode(buttonPins[i], INPUT_PULLUP);
  loadConfig();
  delay(200);
}

void loop() {
  handleEncoderScroll();
  handleSerial();
  handleButtons();
  delay(10);
}

void handleEncoderScroll() {
  int32_t rawAngle = readRawAngle();
  int32_t delta = calculateAngleDelta(rawAngle, lastRawAngle);
  totalAngle += delta;
  if (abs(totalAngle) >= SCROLL_STEP) {
    int steps = totalAngle / SCROLL_STEP;
    Mouse.move(0, 0, steps * scrollDirection);
    totalAngle %= SCROLL_STEP;
  }
  lastRawAngle = rawAngle;
}

int32_t readRawAngle() {
  Wire.beginTransmission(AS5600_ADDR);
  Wire.write(RAW_ANGLE_HIGH);
  Wire.endTransmission();
  Wire.requestFrom(AS5600_ADDR, 2);
  uint16_t high = Wire.read();
  uint16_t low = Wire.read();
  return (high << 8) | low;
}

int32_t calculateAngleDelta(int32_t current, int32_t last) {
  int32_t delta = current - last;
  if (delta > MAX_ANGLE / 2) delta -= MAX_ANGLE;
  if (delta < -MAX_ANGLE / 2) delta += MAX_ANGLE;
  return delta;
}

void handleButtons() {
  for (uint8_t i = 0; i < NUM_BUTTONS; i++) {
    bool pressed = digitalRead(buttonPins[i]) == LOW;
    if (pressed && !buttonStates[i]) {
      if (isStringMode[i]) {
        Keyboard.print(buttonCombos[i] + 4); // skip "STR:"
      } else {
        for (uint8_t k = 0; k < buttonKeyCount[i]; k++) {
          Keyboard.press(buttonKeyCodes[i][k]);
        }
      }
      buttonStates[i] = true;
    } else if (!pressed && buttonStates[i]) {
      if (!isStringMode[i]) {
        for (uint8_t k = 0; k < buttonKeyCount[i]; k++) {
          Keyboard.release(buttonKeyCodes[i][k]);
        }
      }
      buttonStates[i] = false;
    }
  }
}



void handleSerial() {
  if (Serial.available()) {
    String cmd = Serial.readStringUntil('\n');
    cmd.trim();
    if (cmd == "GET_CONFIG") {
      for (uint8_t i = 0; i < NUM_BUTTONS; i++) {
        Serial.print(buttonNames[i]);
        Serial.print("|");
        Serial.print(buttonCombos[i]);
        Serial.print(";");
      }
      Serial.print("SCROLL_DIR=");
      Serial.println(scrollDirection);
    } else if (cmd.startsWith("SET_CONFIG ")) {
      String payload = cmd.substring(11);
      for (uint8_t i = 0; i < NUM_BUTTONS; i++) {
        int sep = payload.indexOf(';');
        String entry = (sep != -1) ? payload.substring(0, sep) : payload;
        int pipe = entry.indexOf('|');
        if (pipe != -1) {
          entry.substring(0, pipe).toCharArray(buttonNames[i], NAME_LEN);
          entry.substring(pipe + 1).toCharArray(buttonCombos[i], COMBO_LEN);
        }
        if (sep == -1) {
          payload = "";
          break;
        }
        payload = payload.substring(sep + 1);
      }

      // Handle SCROLL_DIR=±1
      if (payload.startsWith("SCROLL_DIR=")) {
        int val = payload.substring(11).toInt();
        if (val == 1 || val == -1) {
          scrollDirection = val;
        }
      }

      saveConfig();
      Serial.println("CONFIG SAVED");
    }
  }
}



void saveConfig() {
  for (uint8_t i = 0; i < NUM_BUTTONS; i++) {
    int base = i * RECORD_SIZE;
    for (uint8_t j = 0; j < NAME_LEN; j++) EEPROM.update(base + j, buttonNames[i][j]);
    EEPROM.update(base + NAME_LEN, '\0');
    for (uint8_t j = 0; j < COMBO_LEN; j++) EEPROM.update(base + NAME_LEN + 1 + j, buttonCombos[i][j]);
    EEPROM.update(base + NAME_LEN + 1 + COMBO_LEN, '\0');
    parseCombo(i);
  }
  EEPROM.update(NUM_BUTTONS * RECORD_SIZE, scrollDirection);
}

void loadConfig() {
  const char* defaults[] = {"ESC", "ENTER", "SPACE", "SHIFT"};
  for (uint8_t i = 0; i < NUM_BUTTONS; i++) {
    int base = i * RECORD_SIZE;
    for (uint8_t j = 0; j < NAME_LEN; j++) {
      char c = EEPROM.read(base + j);
      buttonNames[i][j] = (c == 0xFF || !isPrintable(c)) ? 0 : c;
    }
    buttonNames[i][NAME_LEN] = '\0';
    for (uint8_t j = 0; j < COMBO_LEN; j++) {
      char c = EEPROM.read(base + NAME_LEN + 1 + j);
      buttonCombos[i][j] = (c == 0xFF || !isPrintable(c)) ? 0 : c;
    }
    buttonCombos[i][COMBO_LEN] = '\0';
    if (buttonCombos[i][0] == '\0') {
      strcpy(buttonNames[i], (String("Button") + (i + 1)).c_str());
      strcpy(buttonCombos[i], defaults[i]);
    }
    parseCombo(i);
  }
  scrollDirection = EEPROM.read(NUM_BUTTONS * RECORD_SIZE);
  if (scrollDirection != 1 && scrollDirection != -1) scrollDirection = 1;
}

void parseCombo(uint8_t index) {
  isStringMode[index] = false;
  buttonKeyCount[index] = 0;

  if (strncmp(buttonCombos[index], "STR:", 4) == 0) {
    isStringMode[index] = true;
    return;
  }

  char combo[COMBO_LEN + 1];
  strncpy(combo, buttonCombos[index], COMBO_LEN);
  combo[COMBO_LEN] = '\0';

  char* token = strtok(combo, "+");
  while (token && buttonKeyCount[index] < 5) {
    if (strcasecmp(token, "CTRL") == 0) buttonKeyCodes[index][buttonKeyCount[index]++] = KEY_LEFT_CTRL;
    else if (strcasecmp(token, "SHIFT") == 0) buttonKeyCodes[index][buttonKeyCount[index]++] = KEY_LEFT_SHIFT;
    else if (strcasecmp(token, "ALT") == 0) buttonKeyCodes[index][buttonKeyCount[index]++] = KEY_LEFT_ALT;
    else if (strcasecmp(token, "GUI") == 0 || strcasecmp(token, "WIN") == 0) buttonKeyCodes[index][buttonKeyCount[index]++] = KEY_LEFT_GUI;
    else if (strcasecmp(token, "ESC") == 0) buttonKeyCodes[index][buttonKeyCount[index]++] = KEY_ESC;
    else if (strcasecmp(token, "TAB") == 0) buttonKeyCodes[index][buttonKeyCount[index]++] = KEY_TAB;
    else if (strcasecmp(token, "BACKSPACE") == 0) buttonKeyCodes[index][buttonKeyCount[index]++] = KEY_BACKSPACE;
    else if (strcasecmp(token, "PRIOR") == 0) buttonKeyCodes[index][buttonKeyCount[index]++] = KEY_PAGE_UP;
    else if (strcasecmp(token, "NEXT") == 0) buttonKeyCodes[index][buttonKeyCount[index]++] = KEY_PAGE_DOWN;
    else if (strcasecmp(token, "CAPS_LOCK") == 0) buttonKeyCodes[index][buttonKeyCount[index]++] = KEY_CAPS_LOCK;
    else if (strcasecmp(token, "DELETE") == 0) buttonKeyCodes[index][buttonKeyCount[index]++] = KEY_DELETE;
    else if (strcasecmp(token, "ENTER") == 0 || strcasecmp(token, "RETURN") == 0) buttonKeyCodes[index][buttonKeyCount[index]++] = KEY_RETURN;
    else if (strcasecmp(token, "SPACE") == 0) buttonKeyCodes[index][buttonKeyCount[index]++] = ' ';
    else if (strlen(token) == 1) buttonKeyCodes[index][buttonKeyCount[index]++] = token[0];
    token = strtok(NULL, "+");
  }
}

Інші проєкти

Обговорення

guest
Найстаріші
Найновіше Найбільше голосів
Зворотній зв'язок в режимі реального часу
Переглянути всі коментарі
keyboard_arrow_up
0
Буду радий вашим думкам, прокоментуйте.x