1. Как появилась идея?
Всем привет, совсем недавно я начал изучать протокол USB на STM32F103C8, а именно HID-устройства. Я такой человек, который не сильно любит теорию, но обожает учиться всему на практике, поэтому я тут же начал думать над будущим проектом. И я вспомнил, что совсем недавно заказал себе wifi модуль - ESP8266.
И тут ко мне пришла идея: почему бы не объединить STM32f103c8 и ESP8266 в одно устройство - UsbDeviceHacker (так я его назвал). Моя идея заключалось в том, чтобы управлять мышкой и клавиатурой дистанционно по api, который будет работать на веб сервере, запущенном на ESP. Соответственно, ESP будет выступать в роли master устройства, а STM32 в роли slave.
Многие могут спросить: почему не сделать все сильно проще и вместо wifi использовать bluetooth, однако использование wifi будет намного интереснее, потому что наш веб сервер будет разворачиваться в локальной сети, и, соответственно, каждое устройство в ней сможет управлять девайсом просто отправив HTTP запрос на нужный ip.
2. Модель проекта
Перед началом реализации нужно описать модель работы устройства, начну с ESP8266. Wifi модуль подключается к внешней точке доступа, и создает веб сервер в ее локальной сети. В его api 8 endpoint (POST), каждый их которых отвечает за свой функционал.
Точки /mouse
и /keyboard
позволяют напрямую обращаться к мышке и клавиатуре, т.е. использовать согласно их официальным документациям. Точка /animation
уже использует готовый функционал, который облегчает работу с устройством.
После отправки корректного запроса ESP8266 формирует посылку и отправляет ее по UART прямо к STM32F103C8. Blue pill принимает данные и проверяет их на корректность, если все хорошо, то она отправляет report в USB.
Полную архитектуру проекта можно посмотреть в miro.
3. Реализация ESP8266
Прошивку для ESP8266 я писал, используя PlathormIO, в Visual Studio Code. Я не буду сильно останавливаться на объяснении кода, а просто расскажу все в общем виде, иначе статья может сильно растянуться. В любом случае вы сможете все сами просмотреть, репозиторий публичный, вот ссылка.
В целом структура проекта выглядит так :
Рабочую папку я проектировал следующим образом: она состоит из 4 папок: configs, controllers, modules, routes, и из файлов main.h
и main.cpp
. Функция InitRouter()
создает эндпоинты с помощью библиотеки ESP8266WebServer.h
, и перенаправляет их на контроллеры. Для контроллеров вместо классов я решил использовать namespace, просто потому что так понятнее и удобнее.
void InitRouter(){
server.on("/mouse/set", Mouse::set);
server.on("/mouse/remove", Mouse::remove);
server.on("/mouse/click", Mouse::click);
server.on("/keyboard/set", Keyboard::set);
server.on("/keyboard/remove", Keyboard::remove);
server.on("/keyboard/click", Keyboard::click);
server.on("/animation/set", Animation::set);
server.on("/animation/remove", Animation::remove);
}
И далее все основные действия происходят в контроллерах. Вот пример контроллера Animation.h
:
Для парсинга JSON я использую библиотеку ArduinoJson.h
. Она очень удобная и сильно облегчает работу, с ней можно: легко парсить, фильтровать данные и самому создавать JSON объекты.
Посылка для STM32 формируется в массиве data
и отправляется с помощью функции SendDataWithWait(uin8_t *data, uint8_t len)
класса serial
. Для проверки корректности данных в последний байт посылки добавляется контрольная сумма crc8
.
string SendDataWithWait(uint8_t *data, uint16_t len){
uint8_t data_out[len+2] = {HEADER,};
for(int i = 0; i<len; i++){
data_out[i+1] = data[i];
}
data_out[len+1] = crc8(data_out, len+1);
Serial.write(data_out, len+2);
if(AwaitResponse()){
if (status){
status = false;
return "ok";
}
return "error";
}
return "timeout";
}
ESP8266 ждет ответа от STM32 в функции AwaitResponse()
, после чего отправляет ответ клиенту. Вся работа с клиентской частью реализована в файле ClientProcessingModule.h
.
Одна из проблем при создании веб сервера - это его динамический ip. При подключении к роутеру или модему, устройству выдается ip, который меняется со временем. Из-за этого приходится постоянно подключать ESP8266 к сериал порту и смотреть его вручную. Решением проблемы может быть два варианта: 1-ый, если устройство подключается к роутеру можно настроить статический ip; 2-ой отправлять динамический ip на вебсервер со статическим ip.
Я же на данный момент не пользуюсь ни одним из решений, просто потому что пока мне это не нужно.
4. Подключение
Для начала работы с STM32 нужно разобраться с подключением ESP8266 к ней. Т.к blue pill будет подключаться по USB, то питаться ESP будет от нее. Пины VCC и СH_EN подключаются к пину 3.3V на STM32, GND к GND, RX к B10, TX к B11 и на этом все.
На STM32 пины B10 и B11 являются TX и RX USART3 соответственно.
5. Реализация STM32f103C8
Прошивку для STM я писал в CubeIDE. Код для нее весьма простой, он написан полностью на С. Из протоколов подключены UASRT3(HAL), по которому принимаются запросы от ESP, и, собственно, USB, который настроен как HID-device. Также добавлен светодиод на PC13.
Структура проекта состоит из 3 папок: inits, modules, controllers. Данные с USART3 принимаются в файле UsartController.c
, в HAL_UART_RxCpltCallback
, причем принимаются они по одному и собираются в массиве buffer
, после чего вызывается функция ParsingData
, которая проверяет на корректность, с помощью crc8
, и заполняет структуру Action
проверенными данными.
После приема посылки срабатывает нужный case
в main.c
, в котором запускается функция обработки данных, где и происходит основная работа. Далее данные в зависимости от девайса и команды формируют report, который отправляется по USB.
Для того чтобы отправлять репорты и мышки, и клавиатуры одновременно нужно в функцию HID_MOUSE_ReportDesc
, которая находиться в файле usbd_hid.c
, прописать следующие инструкции:
0x05, 0x01, /* Usage Page (Generic Desktop) */
0x09, 0x02, /* Usage (Mouse) */
0xA1, 0x01, /* Collection (Application) */
0x09, 0x01, /* Usage (Pointer) */
0xA1, 0x00, /* Collection (Physical) */
0x85, 0x01, /* Report ID */
0x05, 0x09, /* Usage Page (Buttons) */
0x19, 0x01, /* Usage Minimum (01) */
0x29, 0x03, /* Usage Maximum (03) */
0x15, 0x00, /* Logical Minimum (0) */
0x25, 0x01, /* Logical Maximum (0) */
0x95, 0x03, /* Report Count (3) */
0x75, 0x01, /* Report Size (1) */
0x81, 0x02, /* Input (Data, Variable, Absolute) */
0x95, 0x01, /* Report Count (1) */
0x75, 0x05, /* Report Size (5) */
0x81, 0x01, /* Input (Constant) ;5 bit padding */
0x05, 0x01, /* Usage Page (Generic Desktop) */
0x09, 0x30, /* Usage (X) */
0x09, 0x31, /* Usage (Y) */
0x15, 0x81, /* Logical Minimum (-127) */
0x25, 0x7F, /* Logical Maximum (127) */
0x75, 0x08, /* Report Size (8) */
0x95, 0x02, /* Report Count (2) */
0x81, 0x06, /* Input (Data, Variable, Relative) */
0xC0, 0xC0,/* End Collection,End Collection */
//
0x09, 0x06, /* Usage (Keyboard) */
0xA1, 0x01, /* Collection (Application) */
0x85, 0x02, /* Report ID */
0x05, 0x07, /* Usage (Key codes) */
0x19, 0xE0, /* Usage Minimum (224) */
0x29, 0xE7, /* Usage Maximum (231) */
0x15, 0x00, /* Logical Minimum (0) */
0x25, 0x01, /* Logical Maximum (1) */
0x75, 0x01, /* Report Size (1) */
0x95, 0x08, /* Report Count (8) */
0x81, 0x02, /* Input (Data, Variable, Absolute) */
0x95, 0x01, /* Report Count (1) */
0x75, 0x08, /* Report Size (8) */
0x81, 0x01, /* Input (Constant) ;5 bit padding */
0x95, 0x05, /* Report Count (5) */
0x75, 0x01, /* Report Size (1) */
0x05, 0x08, /* Usage Page (Page# for LEDs) */
0x19, 0x01, /* Usage Minimum (01) */
0x29, 0x05, /* Usage Maximum (05) */
0x91, 0x02, /* Output (Data, Variable, Absolute) */
0x95, 0x01, /* Report Count (1) */
0x75, 0x03, /* Report Size (3) */
0x91, 0x01, /* Output (Constant) */
0x95, 0x06, /* Report Count (1) */
0x75, 0x08, /* Report Size (3) */
0x15, 0x00, /* Logical Minimum (0) */
0x25, 0x65, /* Logical Maximum (101) */
0x05, 0x07, /* Usage (Key codes) */
0x19, 0x00, /* Usage Minimum (00) */
0x29, 0x65, /* Usage Maximum (101) */
0x81, 0x00, /* Input (Data, Array) */
0xC0 /* End Collection,End Collection */
Где report id мышки будет 0x01, клавиатуры - 0x02, т.е. данные например для мыши надо будет отправлять в таком формате:
uint8_t data_out[5] = {0x01, 0, 0, 0, 0}; // первый байт - report id
USBD_HID_SendReport(&hUsbDeviceFS, data_out, 5);
Отдельное внимание нужно уделить анимациям, потому что они добавляют готовый функционал в устройство. На данный момент доступно 3 анимации:
1) плавное движение мышки - мышка плавно двигается по оси x или y, можно задать направление и скорость движения от 1 до 10 включительно;
2)автоматический набор текста (работают только латинские символы, цифры и пробел) - поочередный набор текста, можно задать текст и повторы его написания, например набирать "Hello World" каждые 10 секунд.
3)движение мышки по кругу - просто мышка движется по кругу, можно задать скорость от 1 до 10 включительно и радиус движения.
Также 1 и 3 анимациям можно задавать статусы нажатия кнопок мыши, например пусть мышка движется по кругу с нажатой ПКМ или ЛКМ, или все сразу. Всего доступно три статуса: ЛКМ, ПКМ и кнопка у колесика мыши.
Документация по api тут.
6. Тесты
Теперь дошли до самого интересного, а именно к тестам. Подключим устройство к пк (ноутбуку) и дождемся пока ESP подключиться к модему. Я раздал точку доступа с телефона, и также еще подключился к ней с ноутбука, чтобы отправлять запросы с него. В итоге запускать я буду с ноутбука, а останавливать с телефона, отправляя запрос на endpoint /remove
.
Вот ссылка видео с тестами:
На этом и все, как я считаю, получился очень интересный проект, который точно заслуживал траты сил и времени на него. Оставляю ссылку на публичный git репозиторий, в котором вы сможете ознакомиться со всеми деталями. Подписывайтесь на мой тг канал, в котором я публикую информацию о своих проектах и о жизни в целом. Всем желаю удачи, надеюсь, еще увидимся.
Автор: ArseniyIgnatev