Три кнопки вместо клавиатуры: HID-кликер на ESP32 своими руками
- пятница, 11 сентября 2026 г. в 00:00:14

Мне надоело тянуться к клавиатуре и мыши, чтобы пролистнуть ролик, — и я собрал кликер для ПК на базе ESP32. Три кнопки, наподобие пульта для презентаций: две листают ленту стрелками вверх и вниз, третья отправляет F13. Её нажатие ловит собственное расширение браузера и ставит лайк.
F13 выбрана не случайно. На большинстве клавиатур ряд функциональных клавиш заканчивается на F12, поэтому F13 гарантированно не столкнётся с чужим сочетанием. Зато событие можно перехватить в расширении и обработать по-своему.
В моём случае получилась игрушка для Firefox, но тот же принцип закрывает и рабочие задачи. SMM-специалист может быстрее модерировать посты: переключаться между ними и помечать подходящие для публикации. Специалист по контролю качества при стационарной проверке — фиксировать брак в ERP одним касанием. Всё это выполняется мышью и клавиатурой, но любой, кто оптимизировал рутину, знает: чем меньше лишних движений, тем быстрее и проще идёт работа. Человек не отвлекается и не путается.
Дальше — весь путь: от печати корпуса и схемы на ёмкостных сенсорах до кода прошивки и расширения под Firefox и Chrome. Со сметой на 450 ₽ и граблями, на которые я наступил.
Я делал всё на недорогой китайской плате в форм-факторе Arduino Nano с микроконтроллером ESP32-S3 N8R2. Подойдут и другие микроконтроллеры, например STM32. Но ESP32-S3 из коробки поддерживает Wi-Fi и Bluetooth LE, а я планирую сделать кликер беспроводным и подключать его не только к ПК, но и к телефону.

Второй плюс платы — два порта USB-C. Один нужен для пользовательского подключения. Второй — для прошивки: отдельный программатор не нужен, интерфейс встроен в сам микроконтроллер. Достаточно подключить кабель и залить прошивку. Обратная сторона — габариты: 63×28×5 мм.
В качестве клавиш использовал емкостные сенсоры TTP223. Выбор неосознанный: датчики оказались под рукой, а тактовых кнопок не нашлось. На функциональность это никак не влияет, скорее дело вкуса. При желании сенсор можно превратить в полноценную кнопку: снизить чувствительность керамическим конденсатором и добавить мембрану — например, из старой клавиатуры.
Пластиковый корпус печатал на 3D-принтере. Если печатать негде, услугу легко найти на сайтах объявлений. Возьмите мою готовую модель, либо ищите подходящий по размеру корпус на Printables или MakerWorld. Подогнать габариты можно прямо в слайсере, используя функцию изменения размера без сохранения пропорций. Слайсер — программа, которая готовит 3D-модель к печати и превращает её в G-код для конкретного принтера. Все необходимые действия можно сделать в бесплатном OrcaSlicer.
Коробочку можно собрать даже в бесплатной версии SketchUp и экспортировать в STL. Если есть опыт моделирования в Blender, он тоже подойдет, хоть и заточен больше под художественные формы. Удобнее всего работать в САПР — например, в Fusion 360, который бесплатен для личного использования. У меня он просто не запустился. Благо под рукой был SolidWorks.

Я не стал тратить силы и время на проектирование сложного продуманного устройства с имитацией кнопок. Сделал обычную коробочку, на которой визуально выделил секции сенсоров. Внутри — четыре отсека: три под сенсоры TTP223 и один большой под плату. В месте, где располагается чип, добавил вентиляционные отверстия. Добавил две прорези под порты USB-C. Для удобства монтажа сделал правую стенку съёмной. Крепится она на саморезах — отверстия под них предусмотрел сразу.

Сам процесс выглядит просто: выбрал фронтальную плоскость, нарисовал в эскизе прямоугольник, задал размеры и либо вытянул бобышку, либо сделал вырез. Я не проектировщик и далеко не специалист по созданию моделей, поэтому процесс занял у меня практически целый день. Это с учетом всех переделок, которые обнаруживались не только на этапе проектирования, но и на напечатанном корпусе.
Для программирования микроконтроллера, использую Arduino IDE 2.3.10, это актуальная версия доступная на официальном сайте. Можно работать и в VS Code, установив расширение для ESP32, но в Arduino IDE процесс проще.
После установки IDE добавляем поддержку плат ESP32. Идём в меню Tools (Инструменты) → Board (Платы) → Board Manager (Менеджер плат). Если плата подключена к компьютеру, вместо Board будет указано полное название микросхемы. Например: Board Arduino Leonardo Board.

Слева откроется панель менеджера плат. В поиске набираем ESP32 — программа найдёт два варианта, нужен esp32 by Espressif Systems. Подключаем плату к компьютеру кабелем USB-C. Если разместить плату портами вниз, провод нужно подключить к правому. Это порт USB-OTG, он нужен для программирования микроконтроллера. Второй — для проверки: через него будем подключаться, когда зальём готовый скетч.

После установки в меню Board → esp32 нужно найти и выбрать «ESP32-S3-USB-OTG». Следующий шаг — выбор порта. В Tools находим Port, выбираем COM-порт, он должен быть один. Если к компьютеру подключены другие платы или USB-UART-переходники, вариантов будет несколько. Тогда отключаем плату и смотрим, какой порт исчез.
Область для меня новая: раньше я имел дело с микроконтроллерами лишь поверхностно, и то с простыми ATmega8. Поэтому начал с проводного подключения по USB. Порты на плате уже распаяны, устройство в Windows определяется.
Шортсы в браузерных версиях YouTube и TikTok переключаются нажатием клавиш со стрелками. Вниз — следующий ролик, вверх — предыдущий. К счастью, для ESP32 не нужно писать отдельный драйвер или программный модуль. Устройство по умолчанию определяется как HID (Human Interface Device). Если открыть диспетчер устройств и подключить плату, в списке появится новая клавиатура. Значит, устройство может отправлять нажатия клавиш, а операционная система их корректно обработает.
Работает это так: ESP32 отправляет специальный байт-код клавиши, операционная система интерпретирует его как нажатие. Конкретные значения кодов описаны в спецификации HID Usage Tables for USB. Посмотреть полный список можно в официальном PDF-документе на сайте usb.org.
В отличие от таблицы ASCII, коды привязаны не к символам, а к конкретным клавишам. Например, 0x04 — клавиша с английской буквой A. Сам код регистр не несёт: чтобы получить заглавную, нужно добавить модификатор — левый (0xE1) или правый (0xE5) Shift. При этом ESP32 ничего не знает про языки и раскладки — он просто отправляет код. Это задача операционной системы. Если в Windows выбрана английская раскладка, появится «a», если русская — «ф».
Сначала импортирую заголовочные файлы:
#include "USB.h" #include "USBHIDKeyboard.h"
С этими библиотеками устройство работает как полноценная USB HID-клавиатура, без лишнего кода. Кроме того, внутри есть все необходимые функции и константы для эмуляции нажатий. В том числе константы клавиш в человекочитаемом виде: вместо числового кода можно писать KEY_DOWN_ARROW.
Объявляю объект клавиатуры — через него буду эмулировать нажатия:
USBHIDKeyboard Keyboard;
Дальше объявляю константы с номерами пинов, которые будет слушать микроконтроллер. По даташиту платы остановился на пинах 4, 5 и 6 — к ним подключаются выводы I/O сенсоров TTP223.
const int pinUp = 4; // Стрелка вверх const int pinF13 = 5; // F13 const int pinDown = 6; // Стрелка вниз
При работе с микроконтроллерами, которые взаимодействуют с пользователем, всегда нужно помнить о двух проблемах: дребезге и залипании. При нажатии кнопка совершает микродвижения, из-за которых контакт нарушается: сигнал то появляется, то пропадает. ESP32-S3 работаетт на частоте до 240 МГц, поэтому опрашивает пины намного чаще, чем длится дребезг. Каждый разрыв контакта он воспримет как отдельное нажатие.
С емкостным сенсором TTP223 ситуация лучше, но если палец окажется на границе чувствительности, появится дребезг. Устройство пролистает десятки роликов или поставит и снимет лайк сотни раз — смотря в зоне действия какого датчика окажется палец. Устройство небольшое, поэтому палец может попасть в зону сразу нескольких сенсоров — и начнётся полный хаос.
Залипание — не менее серьезная проблема, а в случае с TTP223 гораздо более частая. Хотел пролистнуть одно видео, не успел убрать палец — и вот уже десятый ролик подряд.
Решаем проблему так: сохраняем состояние кнопки и делаем паузу после его смены. Объявляем константу с периодом задержки и структуру, в которой будем хранить всю информацию о сенсоре:
const unsigned long DEBOUNCE_MS = 30; struct Sensor { int pin; int keyCode; bool raw; bool stable; unsigned long lastChange; bool lastState; };
Все кнопки поместим в массив, проинициализировав начальные состояния:
Sensor sensors[3] = { {pinUp, KEY_UP_ARROW, false, false, 0, false}, {pinF13, KEY_F13, false, false, 0, false}, {pinDown, KEY_DOWN_ARROW, false, false, 0, false} };
Arduino IDE заботится о разработчике: предлагает простую структуру и прячет всё лишнее под капот. Пишем мы на том же C++, но структура скетча отличается от привычной программы. Для микроконтроллеров есть две обязательные функции:
setup — здесь задаём начальные настройки: режимы работы пинов, прерывания и прочую подготовку.
loop — функция главного цикла. Всё, что в ней написано, микроконтроллер выполняет снова и снова, пока на плату подаётся питание. Именно сюда попадает основная логика устройства.
Реализуем первичную настройку. Все кнопки лежат в массиве, поэтому инициализацию удобно выполнить циклом. Каждый пин переводим в режим INPUT, чтобы ESP32 принимал с него сигнал.
// Считываем начальное состояние пина. Это нужно на случай, если при включении какая-то из кнопок уже активна. void setup() { for (int i = 0; i < 3; i++) { pinMode(sensors[i].pin, INPUT); bool val = digitalRead(sensors[i].pin); sensors[i].raw = val; sensors[i].stable = val; sensors[i].lastChange = millis(); sensors[i].lastState = val; } // Запускается USB-стек и HID-клавиатура — плата начинает представляться компьютеру как HID-Keyboard USB.begin(); Keyboard.begin(); }
Функцию лучше объявить до места вызова. Поэтому создаём функцию, которая запрещает обработку сигнала, если после последней смены состояния прошло слишком мало времени:
bool debounceSensor(Sensor &s) { // Считываем текущее значение с пина bool reading = digitalRead(s.pin); // Если сырое значение изменилось по сравнению с прошлым разом — запоминаем новое значение и время изменения (это может быть как реальное срабатывание, так и дребезг). if (reading != s.raw) { s.raw = reading; s.lastChange = millis(); } // Если с момента последнего изменения прошло больше DEBOUNCE_MS (30 мс) — значит, сигнал стабилизировался (не дребезжит). if ((millis() - s.lastChange) > DEBOUNCE_MS) { // Проверяем, была ли смена состояний if (s.stable != s.raw) { // Обновляем состояние и разрешаем обработку s.stable = s.raw; return true; } } // Запрещаем обработку сигнала return false; }
Теперь главный цикл — здесь реализуем основную логику.
void loop() { // Как и в setup() обходим циклом все кнопки for (int i = 0; i < 3; i++) { // Проверяем, было ли подтвержденное (не дребезг) изменение состояния if (debounceSensor(sensors[i])) { // Проверяем, что сигнал нарастает (нажатие) или LOW в HIGH, а не наоборот (затухание сигнала = отпускание кнопки) if (sensors[i].stable == HIGH && sensors[i].lastState == LOW) { // Отправляем событие на компьютер. Конкретные значения KeyCode мы прописали при инициализации кнопок Keyboard.press(sensors[i].keyCode); // Небольшая задержка, чтобы компьютер успел обработать сигнал. При этом 15 мс — слишком мало, чтобы система восприняла нажатие как удержание и включила автоповтор delay(15); // Сразу отпускаем кнопку, чтобы избежать залипания на компьютере Keyboard.release(sensors[i].keyCode); } // Обновляем в массиве информацию о сенсоре sensors[i].lastState = sensors[i].stable; } } }
Это весь код для контроллера. Можно выполнить проверку программы и залить на устройство. Сначала кликнем на галочку, которая расположена вверху слева. Arduino IDE скомпилирует скетч и убедится, что все скобки закрыты, а строки завершены точкой с запятой.

Перед прошивкой убедитесь, что плата подключена к компьютеру по USB. Если расположить микросхему чипом к себе, а портами вниз, нужен правый порт. Именно через него реализован интерфейс UART, который позволяет перепрошивать микропрограмму в ESP32.
Если все подключено, нажимаем кнопку со стрелкой (расположена рядом с кнопкой проверки). Дожидаемся, когда Arduino IDE перезагрузит плату и сообщит об этом. Теперь открываем любой видеохостинг с вертикальной лентой и проверяем переключение. Напомню: оно работает через нажатия стрелок вверх и вниз — так устроено большинство таких лент.

Чтобы устройство могло нажимать кнопки на странице, проще всего написать своё расширение. Я использую Firefox и код буду писать под него. Разница с Chrome минимальная — буквально одно свойство в манифесте.
Манифест в расширениях для браузеров — это файл с именем manifest.json, который содержит всю базовую информацию о плагине. Он указывает название, версию, права доступа, иконки и главные скрипты расширения. По этому файлу браузер понимает, что нужно расширению для работы и как запустить код. С него и начнем:
{ "manifest_version": 3, "name": "ESP32 Clicker", "version": "1.0.0", "description": "Расширение ставит лайки по нажатию кнопки на кликере", "browser_specific_settings": { "gecko": { "id": "yt-tt-like-clicker@example.local", "strict_min_version": "109.0" } }, "content_scripts": [ { "matches": ["https://www.youtube.com/*"], "js": ["content_yt.js"], "run_at": "document_idle", "all_frames": false } ] }
Описание всегда начинается с manifest_version. Этот параметр говорит браузеру, какие свойства разбирать и с какой структурой проекта работать. Для Firefox и Chrome актуальная версия — третья.
Свойство browser_specific_settings нужно только для Firefox: здесь указываем идентификатор расширения и минимальную поддерживаемую версию браузера. В Chrome весь блок можно удалить.
Главный блок — контентный скрипт (content_scripts). В нем мы будем перехватывать нажатие клавиш и выполнять клик по кнопке с лайком. Контентным он называется потому, что файлы из свойства js подгружаются прямо в страницу — наш код становится частью сайта.
{ "matches": ["https://www.youtube.com/*"], "js": ["content_yt.js"], "run_at": "document_idle", "all_frames": false }
Свойство js — массив, скриптов может быть несколько. Это удобно в больших проектах со сложной логикой.
В свойстве matches указываем шаблоны URL, для которых браузер подгружает скрипт. Можно указать хоть все сайты, но нам нужен только youtube.com. Работаем с основным документом, а не со встроенными фреймами, поэтому ставим all_frames в false.
Через run_at определяем, на каком этапе подключить скрипт. Я указал document_idle и передал решение браузеру: обычно это момент события window.onload или сразу после него. Есть и другие варианты:
document_start — до начала рендеринга страницы
document_end — после построения DOM-страницы, при этом ресурсы (изображения, скрипты) могут еще загружаться.
Создадим рядом с manifest.json файл content_yt.js. Внутри объявим немедленно вызываемую функцию — весь дальнейший код будет в ней:
(function () { // Здесь будет весь код. Он выполнится сразу после загрузки файла })();
Чтобы кликнуть по какому-то элементу на странице, нам нужен однозначный селектор. Найти его можно, открыв код страницы в инспекторе. Кликаем правой кнопкой по сердечку и выбираем Исследовать.

Найти кнопку по идентификатору не получится: они генерируются динамически и меняются от загрузки к загрузке. К счастью, все управляющие элементы обернуты в кастомный тег reel-action-bar-view-model. Внутри него лежит искомый button. Добавлю тег в код как константу:
// тег для поиска элемента, в котором находится кнопка "Лайк" const TARGET_TAG = "reel-action-bar-view-model"; // Клавиша, которую будем слушать для триггера события "Лайк" const TRIGGER_KEY = "F13";
Сразу добавлю функцию, которая найдет нужный элемент:
function findTargetContainer() { // Ищем все элементы с указанным тегом const containers = document.querySelectorAll(TARGET_TAG); /* Если элементов нет, возвращаем null. Скорее всего, что-то пошло не так при загрузке страницы. В любом случае, продолжать нет смысла */ if (containers.length === 0) return null; // Если элемент только один, мы нашли контейнер с кнопкой. Возвращаем его. if (containers.length === 1) return containers[0]; /* Этот код на случай, если контейнеров найдено несколько. Скорее всего, только один из них показан на странице и работать надо с ним. */ for (const el of containers) { // Кастомная функция проверки видимости элемента if (isElementVisible(el)) return el; } return containers[0]; }
Если элементов несколько, важно кликать по видимому. Иначе в лучшем случае скрипт не сработает, а в худшем сайт сочтёт действие автоматическим: по невидимым элементам кликают только боты.
Функцию isElementVisible лучше разместить до findTargetContainer. Формально JavaScript поднимает объявления функций и порядок не важен, но читать код проще, когда вспомогательная функция объявлена раньше:
function isElementVisible(el) { // Получаем размеры и позицию элемента относительно окна браузера const rect = el.getBoundingClientRect(); // Если элемент имеет нулевую ширину и высоту, считаем его невидимым if (rect.width === 0 && rect.height === 0) return false; // Проверяем, находится ли элемент в пределах видимой области окна браузера return ( rect.top < window.innerHeight && rect.bottom > 0 && rect.left < window.innerWidth && rect.right > 0 ); }
Такая проверка работает не всегда: элемент может быть перекрыт другим или скрыт через opacity. Для более надёжного результата стоит проверять пересечение элемента с областью видимости через IntersectionObserver.
Случайные срабатывания — это самое неприятное, что может делать любое устройство. Например, пишешь комментарий, а кликер в этот момент ставит лайк. Учитывая, что у меня не кнопки, а сенсоры, вероятность проблем возрастает. Добавлю вспомогательную функцию, которая проверит, не стоит ли фокус на текстовом поле:
function shouldIgnoreEvent(e) { // В переменной e хранится описание события, нам нужно получить элемент, на котором оно произошло const target = e.target; // Если элемента нет, возвращаем false, чтобы обработать событие if (!target) return false; // Получаем тег элемента в верхнем регистре, чтобы сравнивать с запрещенными тегами const tag = target.tagName.toString().toUpperCase(); // Если событие произошло на поле для ввода, возвращаем true, чтобы игнорировать событие if (tag === "INPUT" || tag === "TEXTAREA") return true; // Редактируемые элементы тоже игнорируем, чтобы не мешать пользователю вводить текст if (target.isContentEditable) return true; // Все хорошо, событие нужно обработать return false; }
Обработчик нажатия F13 вынес в отдельную функцию handleKeyPress. Код ниже пишем тоже внутрь тела функции:
function handleKeyPress(e) { // Проверяем, что нажата нужная клавиша if (e.key !== TRIGGER_KEY) return; console.log('TRIGGERED'); // Проверяем, надо ли обрабатывать событие или это паразитное нажатие if (shouldIgnoreEvent(e)) return; // Ищем контейнер с кнопкой "Лайк" const container = findTargetContainer(); // Если контейнер не найден, выводим сообщение в консоль и прекращаем выполнение функции if (!container) { console.debug(`[F13-clicker] Элемент <${TARGET_TAG}> не найден на странице.`); return; } // Ищем первую кнопку внутри контейнера, она и есть "Лайк" const button = container.querySelector("button"); // Если кнопка не найдена, выводим сообщение в консоль и прекращаем выполнение функции if (!button) { console.debug(`[F13-clicker] Внутри <${TARGET_TAG}> не найдена кнопка <button>.`); return; } // Кликаем по кнопке "Лайк" button.click(); // Предотвращаем дальнейшую обработку события, чтобы не мешать другим скриптам на странице e.preventDefault(); e.stopPropagation(); }
Заключительная часть — добавить обработку события нашей функцией:
// Добавляем слушателя события document.addEventListener( // Слушаем нажатие клавиш "keydown", // Функция обработчик события handleKeyPress, // Включаем фазу перехвата, чтобы событие обрабатывалось до того, как оно достигнет других элементов true );
Расширение готово — добавляем его в браузер и проверяем. Кликаем на значок пазла в тулбаре и выбираем Управление расширениями.. Другой вариант — открыть новую вкладку и перейти на about:addons.

Наше расширение не упаковано и не прошло проверку в магазине расширений Mozilla Firefox, поэтому есть только один способ проверить его — «Отладка дополнений». Кликаем на шестеренку вверху страницы и переходим к соответствующему пункту меню. Браузер откроет страницу отладки about:debugging#/runtime/this-firefox. Нажимаем Загрузить временное дополнение и указываем путь к manifest.json. Расширение появится вверху списка. Чтобы увидеть вывод console.log, который мы добавили для отладки, нажимаем Исследовать. Появится окно с инструментами разработчика, но для расширения. На вкладке «Консоль» привычный вывод JavaScript, включая ошибки и console.log.
Осталось открыть шортсы и протестировать работоспособность устройства. Если все сделали, как описано, кликер будет переключать ролики и ставить/убирать лайки.
Сначала внесем правки в блок контентных скриптов. Блок content_scripts — массив, в который можно добавлять файлы для разных доменов. Используем эту возможность:
"content_scripts": [ { "matches": ["https://www.youtube.com/*"], "js": ["content_yt.js"], "run_at": "document_idle", "all_frames": false }, { "matches": ["https://www.tiktok.com/*"], "js": ["content_tiktok.js"], "run_at": "document_idle", "all_frames": false } ]
Дальше нужно найти подходящие селекторы, чтобы добраться до лайка. Здесь есть нюанс: в отличие от YouTube, у TikTok два режима отображения — классический и полноэкранный. Значит, нужно реализовывать два механизма поиска кнопки.
В обычном режиме сначала нужно найти первый доступный тег article:
let ttPage = document.querySelector('article')
Внутри него — второй section, и только там появляется кнопка лайка.
let ttButton = Array.from(ttPage.querySelectorAll('section'))[1]
Лайк — вторая кнопка:
let ttLike = Array.from(ttButton .querySelectorAll('button'))[1]

Когда приложение открыто на весь экран, ситуация немного сложнее. Разобрав HTML-код, пришёл к выводу: искать нужно этот div:
<div class="tiktok-1l9iosz-7937d88b--DivCinemaModeContent ezh7xdt1">
Скорее всего, статическая часть — DivCinemaModeContent, поэтому селектор будет таким:
let ttArea = document.querySelector('div[class*="DivCinemaModeContent"]') let ttLike = Array.from(ttArea.querySelectorAll('button'))[2]

Чтобы определить, в каком режиме работает TikTok, проверяем результат поиска DivCinemaModeContent. Если в ответ приходит null, значит, сайт работает в обычном режиме. В противном случае, полноэкранный просмотр ролика.
Можно создать файл content_tiktok.js рядом с манифестом и скриптом под YouTube.
(function () { const TARGET_DIV_CINEMA = "DivCinemaModeContent" // Клавиша, которую будем слушать для триггера события "Лайк" const TRIGGER_KEY = "F13"; function findCinemaContainer() { // Ищем контейнер полноэкранного режима (Cinema Mode) return document.querySelector(`div[class*="${TARGET_DIV_CINEMA}"]`); } function findRegularContainer() { // В обычном режиме кнопка лайка лежит внутри тега TARGET_TAG return document.querySelector('article'); } function getLikeButton() { // Сначала проверяем, не находимся ли мы в полноэкранном режиме const cinemaArea = findCinemaContainer(); if (cinemaArea) { // В полноэкранном режиме кнопка лайка - пятая по счету кнопка в контейнере const buttons = Array.from(cinemaArea.querySelectorAll("button")); if (buttons.length < 5) return null; return buttons[2]; } // Если полноэкранного контейнера нет, работаем в обычном режиме const ttPage = findRegularContainer(); if (!ttPage) { console.debug(`[F13-clicker] Элемент <${TARGET_TAG}> не найден на странице.`); return null; } const sections = Array.from(ttPage.querySelectorAll("section")); // Кнопки лайка/комментов/шеринга лежат во второй секции if (sections.length < 2) return null; const ttButton = sections[1]; const buttons = Array.from(ttButton.querySelectorAll("button")); // Внутри секции кнопка лайка - вторая по счету if (buttons.length < 2) return null; return buttons[1]; } function shouldIgnoreEvent(e) { // В переменной e хранится описание события, нам нужно получить элемент, на котором оно произошло const target = e.target; // Если элемента нет, возвращаем false, чтобы обработать событие if (!target) return false; // Получаем тег элемента в верхнем регистре, чтобы сравнивать с запрещенными тегами const tag = target.tagName.toString().toUpperCase(); // Если событие произошло на поле для ввода, возвращаем true, чтобы игнорировать событие if (tag === "INPUT" || tag === "TEXTAREA") return true; // Редактируемые элементы тоже игнорируем, чтобы не мешать пользователю вводить текст if (target.isContentEditable) return true; // Все хорошо, событие нужно обработать return false; } function handleKeyPress(e) { // Проверяем, что нажата нужная клавиша if (e.key !== TRIGGER_KEY) return; console.log('TRIGGERED'); // Проверяем, надо ли обрабатывать событие или это паразитное нажатие if (shouldIgnoreEvent(e)) return; // Ищем кнопку "Лайк" с учетом текущего режима (обычный/полноэкранный) const button = getLikeButton(); // Если кнопка не найдена, выводим сообщение в консоль и прекращаем выполнение функции if (!button) { console.debug("[F13-clicker] Кнопка лайка не найдена на странице TikTok."); return; } // Кликаем по кнопке "Лайк" button.click(); // Предотвращаем дальнейшую обработку события, чтобы не мешать другим скриптам на странице e.preventDefault(); e.stopPropagation(); } // Добавляем слушателя события document.addEventListener( // Слушаем нажатие клавиш "keydown", // Функция обработчик события handleKeyPress, // Включаем фазу перехвата, чтобы событие обрабатывалось до того, как оно достигнет других элементов true ); })();
Нет смысла комментировать каждый блок кода, так как это почти клон предыдущего скрипта. Ключевое отличие в функции getLikeButton. Сначала определяем, в каком режиме работает TikTok, а потом ищем кнопку лайка.
Паяльные принадлежности не считаю: они покупаются не под один проект. Только сами детали:
Плата разработки — 370 ₽ на маркетплейсах. Есть варианты интереснее и по цене, и по размеру, но там выше риск брака: в дешёвых платах часто стоят перемаркированные чипы, снятые с бывшего в употреблении оборудования.
Сенсор TTP223 — 210 ₽ за 10 шт. Для проекта нужны три штуки — 63 ₽. Если взять обычную тактовую кнопку, выйдет ещё дешевле. Например, KAN0611-0901B 6×6×9 мм выходит по 9 ₽ за штуку.
Самостоятельная печать корпуса — меньше 50 ₽. Я печатал дешёвым пластиком местного производства: 570 ₽ за килограмм, то есть 0,57 ₽ за грамм. На корпус ушло меньше 25 г. Даже на дорогом пластике корпус не выйдет дороже 50 ₽.
Электричество можно не учитывать: 3D-принтер потребляет копейки в прямом смысле слова. Пиковая мощность принтера — 350 Вт, печать заняла 55 минут: это примерно 0,3 кВт·ч.
Итоговая стоимость устройства — от 450 до 1000 ₽. Недорого, особенно с учётом того, что кликер можно перепрограммировать под другие задачи.
Кликер обошёлся в 450 ₽ и один день работы, причём большая часть времени ушла на моделирование корпуса — код занял куда меньше.
Неожиданным плюсом оказался почти полностью закрытый корпус: его легко протереть, поэтому листать ленту можно хоть во время еды. Запачкал — отключил от компьютера и вымыл. Никаких труднодоступных мест и никаких лишних движений.
Устройство собрано ради развлечения и опыта, но потенциал у него шире. Сенсоров можно добавить сколько угодно, каждый обойдётся в 20 ₽, а прошивка переписывается под любые горячие клавиши. По сути получается программируемый макропад за цену чашки кофе.
Следующий шаг — избавиться от провода. ESP32-S3 умеет в BLE HID, так что кликер должен заработать не только с ПК, но и с телефоном. Если получится, напишу вторую часть.
Исходники прошивки, оба скрипта расширения и STL-модель корпуса выложил на GitHub.
Автор текста — Евгений Хорошилов
НЛО прилетело и оставило здесь промокод для читателей нашего блога:
-15% на заказ нового VDS — HABRFIRSTVDS.