В этой статье мы создадим «клавиатуру» на Arduino и Python.
Я решил создать клавиатуру, которая поможет мне писать код быстрее. В интернете много таких статьей, но при этом большая часть опирается на программируемые клавиатуры, которые можно купить и запрограммировать с помощью специальных программ.
Я не буду использовать клавиатуру, только обычные кнопки.
Код на Arduino
int pins[] = {13, 12, 11, 10, 9} // Пины, к которым подключены кнопки
char coms[] = {'i', 'w', 'f', 'v', 's'} // Команды, которые мы отправляем
/*
* i - if(){}
* w - while(){}
* f - for(){}
* v - void func(){}
* s - Для Windows 10, выполняет Win+Shift+S, открывает возможность скриншота части экрана
*/
unsigned long long mls[] = {0, 0, 0, 0, 0} // Времена последних нажатий кнопок
void setup(){
// Код выполняется 1 раз, при запуске (или перезагрузке) платы
for(int i = 0; i < sizeof(pins)/sizeof(int); i++){ // sizeof(pins)/sizeof(int) - длина массива pins
pinMode(pins[i], INPUT_PULLUP); // Ставим выход №i в pins входом с подтяжкой (для избавления от помех на уровне платы)
}
Serial.begin(9600); // Открываем последовательный порт на скорости 9600
}
void loop(){
for(int i = 0; i < sizeof(pins)/sizeof(int); i++){
if(!digitalRead(pins[i]) && millis() - mls[i] > 500){ // Если кнопка на входе pins[i] нажата и с момента его последнего нажатия прошло больше 500 мс (0.5 с), то...
Serial.println(coms[i]); // выводим символ coms[i] в последовательный порт (а затем перевод строки)
mls[i] = millis(); // Время последнего нажатия = Время сейчас
}
}
}
Функция millis() возвращает количество миллисекунд с момента запуска (или перезагрузки) платы. На некоторых версиях Arduino IDE строчка №23 может выдать ошибку компиляции. Тогда можно реализовать функцию string, которая преобразует символ в строку:
String string(char c){ // Тип «строка» начинается с большой буквы: String
String res = " "; // Создаем строку длиной в 2 символа (на самом деле символ один, но откуда взялся второй объясню позже)
res[0] = c; // Ставим первый символ строки в переданный
return res; // Возвращаем результат
}
И вместо 23 строки написать следующее:
Serial.println(string(coms[i]))
Схема подключения
Python. Необходимые библиотеки.
Для начала через pip установим необходимые нам библиотеки. Это PyQt5 и pyautogui. Думаю, что объяснять установку библиотек бессмысленно.
Ещё понадобится программа Qt Designer, если вы захотите внести изменения в GUI программы. Но о ней, может быть, я расскажу в другой статье.
Пишем код на Python
Импортируем необходимые библиотеки:
from PyQt5 import QtWidgets, uic
from PyQt5.QtSerialPort import QSerialPort, QSerialPortInfo # Для работы с последовательным портом
from PyQt5.QtCore import QIODevice
import pyautogui # Для нажатия клавиш на клавиатуре и горячих клавиш
import time # Задержки
Создаем окно и подготавливаемся к открытию последовательного порта (далее просто порт)
app = QtWidgets.QApplication([])
ui = uic.loadUi("design.ui") # Загружаем дизайн из файла design.ui, созданного в QT Designer
serial = QSerialPort()
serial.setBaudRate(9600) # Частота должна совпадать с частотой из скетча для Arduino (строка 17)
Создаем функции для работы с портом:
def updatePins(close=True):
if close:
serial.close() #Если порт нужно закрыть, закрываем
ui.pinsAvailable.clear() #Очищаем поле для доступных портов
port_list = [] #Список портов
ports = QSerialPortInfo().availablePorts() #Получаем список портов
for port in ports:
port_list.append(port.portName()) #Добавляем название порта в список
ui.pinsAvailable.addItems(port_list) #Добавляем элементы в Combobox
ui.sost.setText("Closed and updated" if close else "Updated") # Ставим соответствующий текст в текстовое поле
def onOpen():
serial.setPortName(ui.pinsAvailable.currentText()) # Подготавливаемся к открытию порта, выбранного в Combobox
serial.open(QIODevice.ReadWrite) # Открываем порт
ui.sost.setText("Opened") # Ставим соответствующий текст в текстовое поле
def onClose():
serial.close() # Закрываем порт
ui.sost.setText("Closed") # Обновляем текст
updatePins(False) #Обновляем список портов, но не закрываем порт
Создаем функцию для чтения порта:
def onRead():
try:
if not serial.canReadLine(): # Если нечего читать, выходим
return
rx = serial.readLine() # Читаем строку
rxs = str(rx, 'utf-8')[:-2] # Обрезаем последние 2 символа. Последний - перенос строки, о предпоследнем - ниже (в разделе «Неизвестный символ»)
if rxs == "i":
pyautogui.write("if(){}")
elif rxs == "w":
pyautogui.write("while(){}")
elif rxs == "f":
pyautogui.write("for(){}")
elif rxs == "v":
pyautogui.write("void func(){}")
elif rxs == "s":
pyautogui.hotkey("Win", "Shift", "S")
ui.sost.setText("Pressed") # Ставим в текстовое поле соответствующий текст
except Exception as e:
print("Exception:", e) # Если ошибка, выводим в консоль
Устанавливаем события и запускаем окно:
serial.readyRead.connect(onRead) # Если в порте есть данные - читаем
ui.openB.clicked.connect(onOpen) # Если нажаты кнопки - обрабатываем
ui.closeB.clicked.connect(onClose)
ui.updateB.clicked.connect(updatePins)
# Запускаем интерфейс
ui.show()
app.exec()
Неизвестный символ
Как я сказал в строке 6 одного из кусков кода, нужно удалять последние 2 символа. И о предпоследнем символе я не знал. Однако когда я стал тестировать свою программу, я выяснил, что длина строки rxs равна 2, хотя должна быть равна 1. После его удаления программа стала работать корректно. Я предполагаю, что дело в самом хранении строк в языках C, C++ и Arduino. Любая строка (даже std::string в C++ и String в Arduino) представляют собой const char*[]. Так как массивы имеют постоянную длину, то в конце строки используется символ '\0', показывающий, что строка закончилась, даже если еще осталось место в массиве. К слову, поэтому если нам нужна строка на 20 символов, нужно делать массив на 21 символ. Я думаю, что этот символ и передается при конвертации символа в строку для передачи в порт. Если кто-то знает, какой там символ на самом деле, буду рад узнать. Пишите в комментарии.
Итоговый код
Python:
from PyQt5 import QtWidgets, uic
from PyQt5.QtSerialPort import QSerialPort, QSerialPortInfo # Для работы с последовательным портом
from PyQt5.QtCore import QIODevice
import pyautogui # Для нажатия клавиш на клавиатуре и горячих клавиш
import time # Задержки
app = QtWidgets.QApplication([])
ui = uic.loadUi("design.ui") # Загружаем дизайн из файла design.ui, созданного в QT Designer
serial = QSerialPort()
serial.setBaudRate(9600) # Частота должна совпадать с частотой из скетча для Arduino (строка 17)
def updatePins(close=True):
if close:
serial.close() #Если порт нужно закрыть, закрываем
ui.pinsAvailable.clear() #Очищаем поле для доступных портов
port_list = [] #Список портов
ports = QSerialPortInfo().availablePorts() #Получаем список портов
for port in ports:
port_list.append(port.portName()) #Добавляем название порта в список
ui.pinsAvailable.addItems(port_list) #Добавляем элементы в Combobox
ui.sost.setText("Closed and updated" if close else "Updated") # Ставим соответствующий текст в текстовое поле
def onOpen():
serial.setPortName(ui.pinsAvailable.currentText()) # Подготавливаемся к открытию порта, выбранного в Combobox
serial.open(QIODevice.ReadWrite) # Открываем порт
ui.sost.setText("Opened") # Ставим соответствующий текст в текстовое поле
def onClose():
serial.close() # Закрываем порт
ui.sost.setText("Closed") # Обновляем текст
updatePins(False) #Обновляем список портов, но не закрываем порт
def onRead():
try:
if not serial.canReadLine(): # Если нечего читать, выходим
return
rx = serial.readLine() # Читаем строку
rxs = str(rx, 'utf-8')[:-2] # Обрезаем последние 2 символа. Последний - перенос строки, о предпоследнем - ниже (в разделе «Неизвестный символ»)
if rxs == "i":
pyautogui.write("if(){}")
elif rxs == "w":
pyautogui.write("while(){}")
elif rxs == "f":
pyautogui.write("for(){}")
elif rxs == "v":
pyautogui.write("void func(){}")
elif rxs == "s":
pyautogui.hotkey("Win", "Shift", "S")
ui.sost.setText("Pressed") # Ставим в текстовое поле соответствующий текст
except Exception as e:
print("Exception:", e) # Если ошибка, выводим в консоль
serial.readyRead.connect(onRead) # Если в порте есть данные - читаем
ui.openB.clicked.connect(onOpen) # Если нажаты кнопки - обрабатываем
ui.closeB.clicked.connect(onClose)
ui.updateB.clicked.connect(updatePins)
# Запускаем интерфейс
ui.show()
app.exec()
Видео, по материалам которого создан данный проект: видео на YouTube.
В случае ошибок в коде, пишите в комментарии. Сам файл design.ui, в котором содержится дизайн окна PyQt5 можно скачать здесь.
P.S. К сожалению, я не профессионал в теме, я прочитал уже много комментариев про V-USB. Я сделал так, как мог. Я рад комментариям-предложениям, но не 5 комментариям на одну и ту же тему: Человек написал статью с кодом на костылях, надо было использовать библиотеку V-USB.
Комментарии (21)
COKPOWEHEU
22.11.2022 11:17+4Насколько я понял, вы сделали не клавиатуру, а набор кнопок, которые нестандартным способом соединяются с компьютером через COM-порт, после чего нестандартная программа имитирует нажатие клавиш.
А собственно зачем? Есть же vusb, благодаря которой AVR'ку можно напрямую подключить в USB в качестве клавиатуры, без всяких костылей.KindCat Автор
22.11.2022 18:00Этот способ нестандартный для тех, кто хорошо разбирается в Arduino и программировании микроконтроллеров. Но я не говорю, что я специалист, я просто реализовал свою идею так, как я это мог сделать. Так как мне показалось это интересным, я об этом рассказал.
COKPOWEHEU
23.11.2022 00:57+3Этот способ нестандартный для любого пользователя ПК. Когда человек подключает клавиатуру, он не ожидает, что придется еще и "драйвер" непонятно откуда скачивать. С весьма нехилым шансом, что он вообще не заведется на его старой / новой машине.
Что бы я посоветовал вам сделать в качестве развития этого проекта и что, полагаю, было бы более интересно читателям — собственно настраиваемая клавиатура. Разберитесь как запустить тот же vusb (а если захотите не просто запустить, но и разобраться в подробностях его работы, у меня есть статья по внутреннему устройству) и сделайте, чтобы последовательности символов, отправляемые по нажатию кнопки, можно было программировать через тот же COM-порт в терминальном режиме. Например, подключаетесь через родной ардуинский порт и в терминале вводите "1while(1){\n\n}\u ", а ардуинка запоминает, что при нажатии на первую клавишу надо послать коды кнопок 'w', 'h', 'i', 'l', 'e', '(', '1', ')', '{', два энтера, '}', кнопку вверх и два пробела. Такая реализация гораздо удобнее вашей, поскольку не требует дополнительного софта: при нормальной работе это обычная клавиатура, при настройке — обычный COM-порт. Ни то, ни другое не требуют от пользователя установки посторонних программ.
Вообще-то, еще удобнее была бы реализация составного устройства клавиатура + флешка, тогда настройки можно сделать обычными текстовыми файлами. Но средствами вашей Ардуинки это невозможно: vusb это low-speed устройство, а им стандартом запрещено работать в роли флешек, переходников на COM-порты, микрофонами и тому подобным. Да и знаний понадобится не в пример больше.
Вариант второй — реализовать что-то необычное. Скажем, не просто клавиатура, а запуск программ (хотя это тоже прекрасно решается через хоткеи), или, скажем, убийство зависшей программы, или сворачивание всех окон, кроме окна с рабочим проектом.
KindCat Автор
23.11.2022 08:06Спасибо за идеи, почитаю об этом. В версии программы, которую я писал для себя, запускалась Камера на Windows, правда, через костыль: нажималась кнопка Win, вводилось слово Camera, нажимался Enter.
sav13
22.11.2022 13:02+1Arduino Leonardo и пример "из коробки"
COKPOWEHEU
22.11.2022 13:56+3Arduino Leonardo и пример "из коробки"
Arduino Leonardo — контроллер на базе ATmega32u4У автора контроллер все же попроще и без аппаратного USB. Даже для обычного vusb придется допаивать обвязку — разъем, три резистора и два стабилитрона.
sav13
22.11.2022 14:23Lonardo может чуть и подороже (хотя с 32U4 есть очень недорогие платы), зато экономит кучу времени (дорогого?) на разработку кода под ОС, не требует никакой установки и гораздо надежнее благодаря этому.
Я не говорю уже о том что аппаратное решение изящнее с архитектурной точки зрения.
COKPOWEHEU
22.11.2022 15:44Ну а вот у автора оказалась только обычная, на m328 — ее выбрасывать что ли?
Цена времени в любительских проектах чаще всего оказывается отрицательной, ведь делается это не ради получения прибыли, а ради опыта.
"на разработку кода под ОС" — так vusb тоже просто подключается и работает, как обычная клавиатура. Плюс запускается в том числе на m328. Пять с половиной навесных компонентов это вообще не проблема, речь ведь не о какой-то экзотике.
KindCat Автор
22.11.2022 18:01+1К сожалению, у меня нет Arduino Leonardo. Для Arduino Uno я сделал это так, как у меня получилось.
kalapanga
22.11.2022 13:51+2Автор, Вы получили символы из порта, они уже лежат в Вашей переменной и Вы не можете сами на них посмотреть, а пишете "Если кто-то знает, какой там символ на самом деле, буду рад узнать. Пишите в комментарии." ???
KindCat Автор
22.11.2022 17:57Этот символ не отображался в консоли, а анализировать через ord() мне не хотелось. Так как мне было не важно какой это символ, мне было важно его просто удалить, то я не разобрался в этом. Но этот проект оказался мне интересным, и я захотел о нем рассказать, так как давно собирался сделать что-то подобное.
sdore
23.11.2022 18:24анализировать через ord()
Целую научно-исследовательскую работу проводить собрались?
halfworld
22.11.2022 15:54+2какой там символ на самом деле
В хелпе Ардуино:
Serial.println()
Description
Prints data to the serial port as human-readable ASCII text followed by a carriage return character (ASCII 13, or '\r') and a newline character (ASCII 10, or '\n'). This command takes the same forms as Serial.print().
KindCat Автор
22.11.2022 17:54Спасибо! Не догадался посмотреть там. Я не профессионал в Arduino и я знаю Arduino и его особенности хуже, чем некоторых других языков.
sdore
23.11.2022 18:26Arduino — это не язык. CRLF — это не особенность. Смотреть надо не там, а попросту знать, чем отличается print от println, не важно где.
serafims
22.11.2022 20:58Видел как-то программу специальную, которая символы из com порта в имитацию клавиатуры превращает, ну или вот, к примеру, полу коммерческая https://www.232key.com/
По запросу rs-232 to keyboard - десятки вариантов.
ibnteo
24.11.2022 03:41Есть недорогой контроллер Pro Micro, аналог Arduino Leonardo, в Arduino IDE есть библиотеки Keyboard и Keypad, с помощью которых можно сделать USB клавиатуру из большого количества кнопок, подключив их матрицей, а чтобы не было фантомных нажатий кнопок, можно к каждой кнопке последовательно подключить диод.
Urvin
НЯП, под тиньки была библиотека, позволяющая превратить микроконтроллер в USB-устройство. Можно "свободного назначения" - и обмениваться какими угодно данными с программой, а можно и прикинуться предопределенным устройством.
Как раз в Вашем случае предпочтительно прикинуться клавиатурой, и по нажатию хардверных кнопок слать в шину серию нажатий уже виртуальных кнопок, составляющих той или иной текст. Так устройство становится переносимым и не будет требовать софт для системы, соответственно, можно кодить уже хоть где.
Urvin
V-USB
SuperTEHb
Вроде, она не только под тини, а вообще под любые АВРы. То есть даже мега из ардуины справится. Но это не arduino-way подход.