Старт идеи

Давно интересовался как можно объединить микроконтроллеры, 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 и задавая вопросы получал нужный и понятный ответ. И создаю такую БД

Теперь надо чтобы подключался мой код на 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, как мне нужно.

Так как теперь сделать так чтобы после выбора я видел какой цвет я выбрал, и добавляю поле 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, получилось вот такая программа

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

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