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

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

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

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

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

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

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

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