Дополнительная клавиатура своими руками

Старт идеи

Давно интересовался как можно объединить микроконтроллеры, Python и пк, и мне в голову приходила идея дополнительной клавиатуры для пользователя, которая будет заменять сочетания клавиш, всего лишь одной кнопкой. Сначала я пробовал объединить платы NodeMCU на базе ESP8266 с пк, с помощью Python. Знаний для написания скетча на ардуино у меня не было, и по гуглив, нашел язык MicroPython. Он сильно мне подошел, так как я владел базовыми знаниями Python, да и умение правильно задавать вопрос гуглу.

Попытка №1

Написал скетч на MicroPython, реализовав создание точки Wi-Fi на микроконтроллере, для подключения микроконтроллера к сети к которой подключен ноутбук. Реализовал 6 кнопок на плате, приложение на Python на основе библиотеки Socket и Keyboard.

Реализация была таковая на микроконтроллере:

  • Создать Wi-Fi точку

  • Подключиться к сети, к которой подключен ноутбук

  • Создать точку соединения по UDP

  • Передать текст при успешном подключении

  • Передавать номер кнопки на которую нажали

  • Закрыть точку соединения при отключение слушателя

Реализация на Python:

  • Подключения к точке соединения

  • Получить ответ по UDP об успешном подключении

  • Прослушивать точку соединения на получения номера нажатой кнопки

  • Воспроизвести сочетание кнопок, которая привязана к данной кнопке

Всё работало, но проблема была после того как приложение на Python могла потерять соединение, и уходила бесконечный цикл по получения пустого сообщения, хотя была реализовано обычное условие на проверку получения сообщений по длине полученного сообщения. Подумал что это не подходит, так как хотелось еще добавить подсветку кнопок с помощью адресных светодиодов на ws2812, и возможно не залезая в код микроконтроллера менять цвет.

Попытка №2

Как то на распродаже на Али, купил аналог Arduino Nano Digispark Attiny88 и решил попробовать это все проделать с ней, но не по Wi-Fi, а по usb. На плате распаян разъем MicroUSB, и подумал что можно реализовать симуляцию клавиатуры на плате. И как же мне повезло, это было уже кем то продуманно и реализовано на микроконтроллерах Attiny, но оказалось не все так гладко. Библиотека была от самих разработчиков плат на базе микроконтроллера Attiny, и работала на микроконтроллерах Attiny85, а на Attiny88 нет. И спустя пару минут, нахожу библиотеку, которую переделал один из коллег ютубера AlexGyver, я точно не знаю кто они друг другу, коллеги, партнеры, извините если что то сказал не так. Я понимаю что я нашел то что мне нужно и все понятно по описанию. Вот ссылка на репозиторий на GitHub. К этому времени появилось понимание как можно реализовать это все в среде ArduinoIDE и начал реализовывать желаемое. Припаял 8 свитчей, от механической клавиатуры к плате, по принципу клавиатуры для ардуино 4 на 3, только в моём случаи 4 на 2. И тут я уперся объем памяти Attiny88, которая не понятным мне причинам, заполнилась и ее не хватало для заливки скетча на плату. Подумал, тогда реализую так, у каждой кнопки свой пин, и общая земля. И все заработало. При нажатие на кнопке на пк отправлялось нажатие сочетание клавиш, которая была запрограммирована в скетче. Но снова я получил не то, мне пришлось бы заходить каждый раз в скетч и менять сочетание кнопок, хотя я этого не делал так много, но все равно, внутренний перфекционист говорил не то, но пока забудем о нем. И приступим к реализации подсвечивания кнопок с помощью ws2812. И тут я снова столкнулся с проблемой памяти, хотя я реализовал включения светодиода только одним цветом, но памяти уже не хватает. И понимаю, что надо бросать это делать и переходить на другой микроконтроллер.

Попытка №3

Покупал так же по распродаже Arduino Nano на Type-C, и решил все переделать на нем, но оказалось одно но, пришлось бы допаивать отдельный разъем, добавлять резисторы и тогда, а желание еще возиться с этим не было, я решил что можно все передавать через Com-порт, и получать команды от пк через него. Убрав часть кода с реализации симуляции клавиатуры, я начинаю писать отправку и нажатие кнопок по Com-порту. И ура, всё работает, микроконтроллер передает нажатие кнопки, светодиод светится, и можно еще реализовать другие функции, так как там по-любому есть память на всё остальное.

Приступаю написания приложения на Python. Ну тут аналогично как перейти с Attiny88 на Arduino Nano, был уже готовый код, который надо было чуток отредактировать. Консольное приложение готово, но на этом нельзя останавливаться, я же хочу реализовать возможность изменять сочетание клавиш, в легкой форме, и изменения подсветки кнопок не редактируя скетч.

И пару месяцев назад, я делал маленькую программу на Tkinter, для управления музыкой, с помощью маленького окна, и не держа развернутым плеер. Начал с того что я хочу реализовать.

  • Выбор Com-порта

  • Включать на недлительный период подсветки кнопки

  • Задать сочетание клавиш

  • Задать цвет подсветки кнопки

Выбор Com-порта легко реализовать в выпадающей строке, которая получает список портов. Теперь надо реализовать остальные функции. Начал по порядку, сначала в Tkinter добавил 8 кнопок, и при нажатие кнопки вызывалась функция, которая передает какую то команда по Com-порту на микроконтроллер, но как отправить атрибуты функции в Tkinter я не знал, и по гуглив какой то время нахожу, что можно реализовать через lambda: и сама функция и в скобках аргументы. Уже пол дела сделана по этому функционалу, и при нажатие на кнопку в приложении на Tkinter, вызывалась нужна функция с нужным мне аргументом

command=lambda: self.jobs.check_led('LED_1')


def check_led(self, led):
    if led == 'LED_1':
        ser.write(b'led_1')
    if led == 'LED_2':
        ser.write(b'led_2')
    if led == 'LED_3':
        ser.write(b'led_3')
    if led == 'LED_4':
        ser.write(b'led_4')
    if led == 'LED_5':
        ser.write(b'led_5')
    if led == 'LED_6':
        ser.write(b'led_6')
    if led == 'LED_7':
        ser.write(b'led_7')
    if led == 'LED_8':
        ser.write(b'led_8')

Теперь осталось научить Ардуину понимать что она получает, и реагировать так как мне нужно. через проверку длинны полученного сообщения запускалось проверка полученного сообщения по условиям, и реагирования включения нужного мне светодиода.

    if (Serial.available()) {
      data = Serial.readStringUntil('$');
      int len_data = data.length();
      if (len_data == 5) {
        if (data == "led_1") {
          led_on(1);
          myTimer3 = millis();
        }
        if (data == "led_2") {
          led_on(2);
          myTimer3 = millis();
        }


void led_on(int pins) {
  pins -= 1;
  pixels.setPixelColor(pins, pixels.Color(255, 198, 24));
  pixels.show();

Всё хорошо, работает, пора как то организовать понятный интерфейс в программирование сочетаний клавиш. Решил не делать много кнопок и реализовать сочетание максимум из 3 кнопок. У библиотеки Keyboard есть список кнопок которые он может воспроизвести, и теперь нужно создать словарь, который будет удобен для чтения мне, и понятный на Tkinter.

def key_libs():
    list = {
        '':'',
        'Backspace': 'backspace',
        'Tab': 'tab',
        'Enter': 'enter',
        'Shift': 'shift',
        'Ctrl': 'ctrl',
        'Alt': 'alt',
        'Caps_Lock': 'caps lock',
        'Esc': 'esc',
        'Spacebar': 'spacebar',
        'Page_Up': 'page up',
        'Page_Down': 'page down',
        'End': 'end',
        'Home': 'home',
        'Left': 'left',
        'Up': 'up',
        'Right': 'right',
        'Down': 'down',
        'Select': 'select',
        'Print_Screen': 'print screen',
        'Insert': 'insert',
        'Delete': 'delete',
        '0': '0',
        '1': '1',
        '2': '2',
        '3': '3',
        '4': '4',
        '5': '5',
        '6': '6',
        '7': '7',
        '8': '8',
        '9': '9',
        'a': 'a',
        'b': 'b',
        'c': 'c',
        'd': 'd',
        'e': 'e',
        'f': 'f',
        'g': 'g',
        'h': 'h',
        'i': 'i',
        'j': 'j',
        'k': 'k',
        'l': 'l',
        'm': 'm',
        'n': 'n',
        'o': 'o',
        'p': 'p',
        'q': 'q',
        'r': 'r',
        's': 's',
        't': 't',
        'u': 'u',
        'v': 'v',
        'w': 'w',
        'x': 'x',
        'y': 'y',
        'z': 'z',
        'Left_Windows': 'left windows',
        'Right_Windows': 'right windows',
        '*': '*',
        '+': '+',
        '-': '-',
        '/': '/',
        'F1': 'f1',
        'F2': 'f2',
        'F3': 'f3',
        'F4': 'f4',
        'F5': 'f5',
        'F6': 'f6',
        'F7': 'f7',
        'F8': 'f8',
        'F9': 'f9',
        'F10': 'f10',
        'F11': 'f11',
        'F12': 'f12',
        'Left_Shift': 'left shift',
        'Right_Shift': 'right shift',
        'Left_Ctrl': 'left ctrl',
        'Right_Ctrl': 'right ctrl',
        'Browser_Back': 'browser back',
        'Browser_Forward': 'browser forward',
        'Browser_Refresh': 'browser refresh',
        'Browser_Stop': 'browser stop',
        'Browser_Favorites': 'browser favorites',
        'Volume_Mute': 'volume mute',
        'Volume_Down': 'volume down',
        'Volume_Up': 'volume up',
        'Next_Track': 'next track',
        'Previous_Track': 'previous track',
        'Stop_Media': 'stop media',
        'Play/Pause_Media': 'play/pause media',
    }
    return list

Реализовать список кнопок решил снова через выпадающий список.

def generate_list(self):
    self.key_list = []
    libs = dict(lists())
    for key in libs:
        self.key_list.append(key)

self.key_box11 = ttk.Combobox(self, values=self.key_list, width=14, state="readonly")
self.key_box11.grid(row=0, column=1)

Так, но как реализовать запоминание какие кнопок нужно нажимать, а не заполнять их каждый раз. И я решил это сделать через БД. Это хороший опыт по работе с БД, не очень тяжелый и понятный, а еще и реализация по созданию БД, по поиску данных, и обновления данных. Перед созданием БД я начал изучать что и как можно реализовать, но в этот раз не в гугле, а у телеграмм бота с что-то типа ChatGPT и задавая вопросы получал нужный и понятный ответ. И создаю такую БД

7810e8aba14995dd26031c5df7369a3e.jpg

Теперь надо чтобы подключался мой код на Python и отправлял SQL запросы в БД и получал ответ. И я создаю отдельный файл, с классом для таких команд.

И теперь к выпадающему списку добавляется новая переменная и дополнительная строка.

self.value11 = StringVar(value=self.btn_set.check_key_db(1, 1))
self.key_box11 = ttk.Combobox(self, textvariable=self.value11, values=self.key_list, width=14, state="readonly")
self.key_box11.grid(row=0, column=1)

def check_key_db(self, num, ordinal):
    name = eval('"BTN_PIN_{}"'.format(num))
    var = eval('"key{}"'.format(ordinal))
    re = self.cursor.execute(f'SELECT {var} FROM key_settings WHERE btn_name = "{name}"').fetchone()[0]
    inv_d = {value: key for key, value in libs.items()}
    return inv_d[re]

Так как список выпадал, который мне нужен и я реализовал передачу в БД сразу данные которые понимает Keyboard, пришлось инвертировать словарь, чтобы можно было с помощью данных которые понимаем Keyboard, выводить понятный мне текст. Теперь нужно добавить кнопку которая будет сохранять кнопки в БД. Ну логично я использовал обычную кнопку в Tkinter, а которой снова использовал функцию через lambda.

Теперь нужно реализовать работу прослушивания Com-порта в реальном времени, параллельно работе программы. Для этого я выбрал Thread, реализовав запуск программы в одном потоке, а во втором потоке прослушивание Com-порта при подключении к нужному Com-порту.

Так еще одна функция реализована, пора переходить за изменения цвета подсветки кнопок. Поискав нахожу готовое решение от Tkinter как модуль colorchooser, который при вызове выводит палитру цветов, и при выборе передавал HEX код цвета и цвет в RGB, как мне нужно.

4c848d6be691038132dce35bf0064115.jpg

Так как теперь сделать так чтобы после выбора я видел какой цвет я выбрал, и добавляю поле Label, у которой при выборе цвета меняется фоновый цвет, на тот который я выбрал в палитре, и в это мне помогло, то что при выборе я получаю HEX код цвета, и передаю снова через lambda код цвета. Но снова нужно хранить цвет, и снова тут приходит на помощь БД, которую создал раннее. Так теперь надо передать цвет на Ардуино по Com-порту, тут решил не ломать голову и решил передать такой текст в формате Номер кнопки, и цвет в палитре RGB/

self.btn_set.save_colors_db(num, user_color_background)
label = eval('self.colorlabel{}'.format(int(num)))
label["background"] = user_color_background
sends_commands = f'clr{num}r{r_clr}g{g_clr}b{b_clr}$'
jobs.sends_color(sends_commands)

Но снова теперь надо обучить Ардуино понимать отправляемый текст. И тут я снова беру проверку длины сообщения и только потом разбивание текста на нужные мне переменные как номер кнопки, цвет в палитре RGB/

else if (len_data == 16) {
    int led_pins = data.substring(3).toInt() - 1;
    int r = data.substring(5, 8).toInt();
    int g = data.substring(9, 12).toInt();
    int b = data.substring(13, 16).toInt();
    pixels.setPixelColor(led_pins, pixels.Color(r, g, b));
    pixels.show();
}

И всё работает, так теперь надо как то помнить цвет который был забит до этого и я решил реализовать это через EEPROM и теперь код уже выглядит так.

// запись цвета в память
else if (len_data == 16) {
  int led_pins = data.substring(3).toInt() - 1;
  EEPROM.write(led_pins * 12, data.substring(5, 8).toInt());
  EEPROM.write(led_pins * 12 + 3, data.substring(9, 12).toInt());
  EEPROM.write(led_pins * 12 + 6, data.substring(13, 16).toInt());
  int r = EEPROM.read(led_pins * 12);
  int g = EEPROM.read(led_pins * 12 + 3);
  int b = EEPROM.read(led_pins * 12 + 6);
  pixels.setPixelColor(led_pins, pixels.Color(r, g, b));
  pixels.show();
}

// считывание из памяти при нажатие на кнопку
void led_on(int pins) {
  pins -= 1;
  int r = EEPROM.read(pins * 12);
  int g = EEPROM.read(pins * 12 + 3);
  int b = EEPROM.read(pins * 12 + 6);
  pixels.setPixelColor(pins, pixels.Color(r, g, b));
  pixels.show();
}

И вот теперь остался внешний вид, тут уже каждый сам выбирает что как. Но я решил, что мне не нравятся стоковый кнопки Tkinter, шрифт текста, и я всё это изменил. Скачав с какого то сайт который мне понравился, нарисовав кнопки в Photoshop Online, получилось вот такая программа

397ac1600037bb901c46828ca574d553.jpg5936eba96124550e6c710338bf074eec.jpg

Прикреплю ссылку на репозиторий на GitHub

Добавлена еще функция, что при закрытия программы, чтобы не мешала, появляется иконка в трее, в которой есть две функции разворота окна, и закрывания программы, но только она не закрывается полностью, так как не понял, как реализовать остановку второго потока.

© Habrahabr.ru