Предисловие:
В данной статье будет рассказано, как реализовать эмуляцию мыши, используя Arduino, но перед тем, как начать делать эмуляцию через Arduino, хочется рассказать о некоторых поправках в проекте, которые были сделаны вне статей. Правки небольшие, но всё же считаю нужным кратко рассказать о них для полного понимания того, как устроен проект от начала и до конца.Разделение проекта на разные файлы:
Первое, что было сделано, — это разделение логики каждой из версий нейронной сети на несколько файлов. Например, раньше в файле cs2.py были функции детекции, создания скриншотов, антиотдачи двух типов, аима двух типов, триггербота. Теперь же всё, что связано с эмуляцией, находится в отдельном файле. Аналогичная ситуация и для версии нейронной сети default.py.Также функция load_config использовалась практически в каждом файле проекта, и в каждом файле она была продублирована. Было принято решение сделать отдельный файл с функцией для загрузки конфига и во всех остальных файлах проекта просто её вызывать. Таким образом, суммарно стало меньше строк примерно на 100.
Если читали предыдущие статьи, то могли знать, что визуализацию объектов на экране я делал только для версии нейронной сети для игры CS2. Вне статьи был добавлен аналогичный функционал и к нейронной сети default.
Исправление ошибок в интерфейсе:
Также была проблема в веб-интерфейсе: при загрузке страницы не отображались данные из объектов true/false, например, включения и выключения триггербота для CS2. Проблема заключалась в JS-функции принятия данных из конфига и записи данных в объект. А если точнее, то из конфига брались логические данные, а объект на странице принимает строковые. Чтобы это исправить, нужно принятые из конфига данные конвертировать в строковые.
JavaScript:
document.getElementById('cs2_trigger_bot').value = config.cs2_trigger_bot ? 'true' : 'false';
Вот как выглядит структура проекта на данный момент:
Редизайн панели:
Также был полностью переделан дизайн веб-интерфейса. В самом дизайне ничего особенного и сложного нет, но пару моментов объяснить всё же стоит.
Как видно на скриншоте, была добавлена боковая панель. Боковая панель является отдельным HTML-файлом, который вызывается в HTML-файле с основными настройками нейронной сети. Кнопка смены игры расположена именно в файле с боковой панелью, а не в основном файле с настройками. Как могли некоторые подумать, функцию для принятия данных из конфига и отправки данных из объекта выбора игры также нужно будет разместить в файле с боковой панелью, где находится сам объект. Но это не так. По сути, оба эти файла как бы соединяются в один, и функция из файла с основными настройками спокойно будет обновлять данные в объекте из файла с боковой панелью. Но для этого нужно немного изменить код. А для начала рассмотрим файл боковой панели, а точнее, объект с выбором игры:
HTML:
<form id="config-form_game">
<select class="game_selector" id="game" name="game">
<option value="cs2">CS2</option>
<option value="default">DEFAULT</option>
</select><br>
</form>
Как видно, селектор, то есть объект с выбором игры, находится внутри формы с id config-form_game. Теперь рассмотрим JS-код из файла с основными настройками, где происходит загрузка данных из конфига при открытии и обновлении страницы:
JavaScript:
document.addEventListener("DOMContentLoaded", function() {
loadConfig();
const form = document.getElementById("config-form");
form.addEventListener('input', updateConfig);
});
Как видно, первым делом запускается функция загрузки данных из конфига. Затем создается константа (переменная), в которую записывается объект. В данном случае это объект форма, в котором находятся основные объекты с настройками.
form.addEventListener('input', updateConfig); — данная строка отвечает за вызов ивента, который будет срабатывать при взаимодействии с объектом из ранее записанной переменной (form), и после взаимодействия вызовется функция updateConfig (которая берет данные из объектов, и дальше уже данные попадают в конфиг). Но, как стало понятно, взаимодействие происходит с формой, где находятся основные настройки из основного файла, а не из файла боковой панели. Чтобы это исправить, нужно добавить еще одну константу, но записать в нее форму config-form_game, в которой находится объект выбора игры.
JavaScript:
const formGame = document.getElementById("config-form_game");
Затем нужно вызвать ивент, который будет срабатывать при взаимодействии с объектом, но на этот раз указать переменную не form, а formGame. Таким образом, ивент будет срабатывать при взаимодействии с объектом выбора игры и затем будет вызываться функция для обновления данных в конфиге.
JavaScript:
formGame.addEventListener('input', updateConfig);
Возможно, некоторые могут задаться вопросом, почему бы форму с объектом смены игры просто не сделать с таким же id, как и форма, в которой находятся основные настройки. Но так делать не желательно: указывать одинаковые id у нескольких объектов считается грубой ошибкой, и такой фокус попросту не сработает.
С тем, как обновлять данные из объекта другой страницы, закончено. Теперь можно рассмотреть, как собственно добавляются данные одной страницы на другую.
В данном случае у нас есть два HTML-файла: первый файл — это боковая панель, второй файл — это все настройки нейронной сети. В HTML-файле боковой панели нужно указать это:
HTML:
{% block content %}{% endblock %}
Затем, внутри HTML-файла с настройками нужно указать эту строку:
HTML:
{% extends "base.html" %}
Затем нужно указать эту строку:
HTML:
{% block content %}
В конце файла с настройками, а именно там, где заканчиваются объекты и JS-функции, нужно указать эту строку:
HTML:
{% endblock %}
В итоге, когда используется {% extends "base.html" %} в файле с настройками, это означает, что он будет наследовать файл с боковой панелью и вставлять свои объекты и другой код, находящийся внутри {% block content %}{% endblock %}, в место, указанное в base.html, то есть в {% block content %}{% endblock %}, который определён в файле с боковой панелью.
С использованием контента из разных файлов в одном разобрались. Теперь считаю нужным рассказать, как сделать в боковой панели кнопки, при нажатии на которые открываются страницы с настройками разных нейросетей. Как уже стало понятно, есть несколько страниц: страница боковой панели, страница с настройками нейросети для CS и страница с настройками нейросети default.
В Python-файле, в котором находится инициализация Flask, нужно указать маршруты до этих страниц.
Python:
@app.route('/cs2')
def cs2():
return render_template('cs2.html')
@app.route('/default')
def default():
return render_template('default.html')
Далее нужно перейти в HTML-файл боковой панели и создать объект <a></a>. Это объект для создания гиперссылок, то есть при нажатии на странице на этот объект будет происходить переход на ссылку, указанную внутри этого объекта.
HTML:
<a href="{{ url_for('default') }}" class="sidebar_item">
<h4 class="sidebar_text">Default</h4>
</a>
<a href="{{ url_for('cs2') }}" class="sidebar_item">
<h4 class="sidebar_text">CS2</h4>
</a>
На этом разбор всех изменений, сделанных вне статьи, закончен. Старался сделать его как можно компактнее, но при этом понятным, если вдруг кому-то интересен абсолютно каждый шаг при разработке нейронной сети.
Работа с Arduino и обновление Python кода:
Теперь можно приступить к работе с Arduino и к обновлению Python-логики. Для начала нужно выяснить, какая Arduino подойдет.Какой Arduino подойдет:
Подходящими Arduino являются те, которые поддерживают подключение к компьютеру как HID-устройство (Human Interface Device). То есть мышка, клавиатура, руль и т.д. А способны на такое платы с микроконтроллером ATmega32u4. В списке таких плат находятся Leonardo, Pro Micro, Micro. Также есть возможность некоторые другие версии Arduino перепрошить на работу как HID-устройство, но в данном случае будет использоваться Arduino Leonardo.
После того как было разобрано, какой Arduino потребуется для реализации эмуляции, можно приступать к написанию кода.
Инициализация Arduino в Python коде:
Первым делом нужно создать в проекте новый файл, который будет называться arduino_initialization.py. В данном файле будут находиться две функции: функция поиска подключенного Arduino и функция подключения к ArduinoФункция поиска Arduino:
Первой в списке на реализацию будет функция поиска подключенного к ПК Arduino. Функция будет называться find_arduino_port().Для того чтобы найти подключенный Arduino, нужно пройтись по всем портам компьютера и найти тот, к которому подключено устройство с названием Arduino. Чтобы реализовать данную механику проверки портов, нужно импортировать библиотеку pyserial. Эта библиотека как раз и предназначена для работы с портами.
Python:
import serial
import serial.tools.list_ports
Теперь можно приступить к функции, и первое, что в ней нужно сделать, так это добавить переменную, в которой будет вызываться функция из библиотеки pyserial.
Python:
ports = serial.tools.list_ports.comports()
Затем нужно создать цикл for, который будет перебирать каждый из портов и проверять его на то, чтобы подключенное к порту устройство было с названием Arduino.
Python:
for port in ports:
if "Arduino" in port.description or "Arduino" in port.manufacturer:
return port.device
port.manufacturer содержит информацию о производителе подключенного к порту устройства.
return port.device возвращает название подключенного к порту устройства.
Далее нужно вне цикла for указать возврат None, если подходящих портов не найдено.
Python:
print("Не удалось найти Arduino.")
return None
Затем нужно вызывать эту функцию. Сделать это нужно просто в файле вне какой-либо функции.
Python:
arduino_port = find_arduino_port()
Функция подключения к Arduino:
Теперь можно начать реализовывать функцию подключения к найденному Arduino. Функция будет называться connect_to_arduino, в аргументах которой будет указан baud_rate=115200. Данное значение является скоростью, с которой будет реализована передача данных, а именно количеством битов, которые могут быть переданы в секунду.
Python:
def connect_to_arduino(baud_rate=115200):
Затем, внутри функции нужно добавить проверку if, на то чтобы, если arduino_port равняется None, то возвращать None.
Python:
if not arduino_port:
return None
Затем нужно внутри функции указать try except. Внутри try будет код для подключения к Arduino, а в except — уведомление, если подключиться не получилось. В try нужно создать объект Serial с такими параметрами, как: название подключенного к порту устройства (Arduino), скорость передачи данных и время ожидания записи данных в Arduino.
Python:
ser = serial.Serial(arduino_port, baud_rate, timeout=1, write_timeout=5)
Далее можно добавить паузу, для того чтобы соединение успело произойти:
Python:
time.sleep(1)
Затем нужно добавить return, который будет возвращать переменную ser, в которой находится результат вызова объекта Serial.
Python:
print(f"Подключено к {arduino_port} со скоростью {baud_rate}")
return ser
Теперь в except нужно указать возврат None (except — это обработка ошибок, если код внутри try не был выполнен).
Python:
except serial.SerialException as e:
print(f"Не удалось открыть порт {arduino_port}: {e}")
return None
С функцией подключения к Arduino закончено, теперь эту функцию нужно вызвать:
Python:
ser = connect_to_arduino()
Изменение логики нейросети CS2:
Теперь можно изменить логику наводки и триггербота для нейросети CS2. Т.к. логика нейросети была разделена на несколько файлов, все функции, работающие с эмуляцией мыши, были вынесены в отдельный Python файл, если точнее, в cs2_mouse_emulated.py.Изменение функция наведения:
Первой будет изменена функция aim для наведения на врагов. Нужно найти внутри функции строку с эмуляцией мыши.
Python:
win32api.mouse_event(win32con.MOUSEEVENTF_MOVE, int(aim_step_x), int(aim_step_y), 0, 0)
Затем нужно заменить ее на эти строки:
Python:
command = f"{aim_step_x},{aim_step_y}\n"
ser.write(command.encode())
ser.write — это метод объекта ser (экземпляра класса serial.Serial), который был создан ранее в Python файле инициализации Arduino внутри функции подключения к Arduino.
command.encode() означает, что берется строка из переменной command и конвертируется в байты.
Таким образом, ser.write берет данные из переменной с данными для перемещения и отправляет на Arduino.
На этом можно было бы уже закончить работу с функцией наводки и начать писать код для самого Arduino, но есть одно НО. Дело в том, что данные на Arduino будут поступать очень быстро и очень много, в связи с чем буфер Arduino будет заполнен, если на него будут беспрерывно поступать команды в течение секунд 5-10, то есть если вы нажмете ЛКМ и не будете ее отпускать долгое время. Из-за этого Arduino не сможет считывать новые данные, и это вызовет краш программы. Чтобы этого избежать, нужно обрабатывать ошибку таймаута записи данных на Arduino и при ее возникновении просто очищать буфер. Чтобы это сделать, нужно добавить код, который был написан ранее для замены эмуляции на отправку команды на Arduino, внутрь try.
Python:
try:
command = f"{aim_step_x},{aim_step_y}\n"
ser.write(command.encode())
time.sleep(load_config("cs2_aim_time_sleep"))
А внутрь except нужно добавить это:
Python:
except serial.SerialTimeoutException:
print("Превышено время ожидания для записи данных на Arduino")
ser.reset_output_buffer()
ser.reset_input_buffer() сбрасывает входной буфер, чтобы освободить место для новых данных, которые могут поступать на Arduino.
Теперь с функцией наведения закончено, и можно переделать функцию триггербота. Но перед этим хочется уточнить, почему не была изменена функция антиотдачи. Если вы читали предыдущие статьи, то можете помнить, что функция антиотдачи не производит эмуляцию, а лишь берет координаты макроса и передает их в функцию наведения. То есть наведение происходит относительно координат антиотдачи прямо в функции aim, в связи с этим функция антиотдачи не использует эмуляцию, поэтому и редактировать ее не требуется.
После разъяснения можно приступать к изменению функции триггербота.
Изменение функции триггербота:
Внутри функции нужно найти эти строки эмуляции нажатия и отпускания левой кнопки мыши:
Python:
# Нажатие левой кнопки мыши
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0)
# Отпускание левой кнопки мыши
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)
И заменить их на одну маленькую строчку, отправляющую команду на Arduino.
Python:
ser.write(b'CLICK\n')
С Python-кодом закончено, и теперь можно начать писать на самом Arduino.
Подготовка к работе с Arduino:
Для того чтобы начать писать код, потребуется скачать IDE для Arduino по этой ссылке: https://www.arduino.cc/en/softwareПосле установки IDE подключите свой Arduino и дождитесь, пока установятся драйвера. После установки драйверов можно запускать IDE и выбирать в нем свое подключенное устройство.
Стандартные функции для Arduino:
И первое, что вы увидите в IDE, — это стандартный шаблон кода.
C++:
void setup() {
}
void loop() {
}
Я в C и C++ не силен, и поэтому пришлось изучать базу, смотреть сторонние сурсы и разбирать их для того, чтобы понять, как устроена его работа. Возможно, в моем коде, который будет предоставлен далее, будут грубые ошибки и плохая оптимизация, но это не отменяет того факта, что он работает. Если у вас будут замечания по коду Arduino, просьба написать об этом в комментариях.
После того как IDE подготовлена, Arduino подключен и обнаружен, можно приступать к написанию кода. Для начала нужно рассмотреть 2 пустых функции из шаблона.
В функции setup нужно указывать код, который будет срабатывать при подключении или перезапуске Arduino.
В функцию loop нужно писать код, который будет выполняться непрерывно во время работы Arduino.
Написание кода на Arduino:
Теперь нужно разобраться с тем, как будет работать эмуляция мыши. Для этого потребуется библиотека Mouse.h. Данная библиотека встроенная, и качать ее из сторонних источников не потребуется. Чтобы подключить данную библиотеку, нужно в начале кода указать данную строку:
C++:
#include <Mouse.h>
Далее нужно создать пустую переменную типа String, в которую будут записываться команды, поступающие от нейросети.
C++:
String command = "";
Далее нужно настроить соединение Arduino с нейронной сетью и инициализировать библиотеку для эмуляции мыши. Делать это всё нужно внутри функции setup, которая срабатывает при подключении Arduino к компьютеру.
C++:
void setup() {
Serial.begin(115200);
while (!Serial);
Mouse.begin();
}
Далее внутри функции loop нужно создать цикл while, в котором будет проверяться, есть ли в буфере данные, переданные на Arduino:
C++:
void loop() {
while (Serial.available()) {
}
}
Теперь внутри цикла нужно вызывать чтение символов из буфера и запись их в переменную.
C++:
char c = Serial.read();
Далее нужно создать проверку if на то, чтобы если в переменной “c” находится \n, значит получена полная команда от нейронной сети. То есть, если нейронная сеть отправляет команду 10,20\n (координаты для перемещения курсора), то Arduino будет считывать эту команду так: 1
0
,
2
0
\n
Таким образом и определяется конец команды благодаря \n.
C++:
if (c == '\n') {
}
Далее нужно указать else, который будет срабатывать, если прочитанный символ не является \n. В else будет добавление полученного символа в переменную string command для того, чтобы из полученных по одному символу собрать цельную команду в виде строки. Например, функция триггербота отправляет команду CLICK\n, Arduino ее получит в таком виде:
C
L
I
C
K
\n
При получении каждого из символов будет проверяться, является ли полученный символ \n; если не является, то символ будет добавляться в переменную command, то есть command = C+L+I+C+K (CLICK).
C++:
else {
command += c;
}
Далее, внутри if, т.к. получен символ \n и команда закончена, будет происходить логика обработки получившейся команды, которая хранится в переменной command. Первой на очереди будет обработка команды CLICK. Для этого нужно создать проверку if, в которой будет проверяться, равняется ли значение из переменной command слову CLICK.
C++:
if (command == "CLICK") {
}
Если равняется, то будет происходить нажатие левой кнопки мыши, затем небольшая пауза и отпускание левой кнопки мыши.
C++:
Mouse.press(MOUSE_LEFT);
delay(50);
Mouse.release(MOUSE_LEFT);
После if с проверкой на команду CLICK нужно создать указать else в котором будет обрабатываться команда с координатами, то есть если значение из переменной command не команда CLICK, то тогда это команда с координатами для эмуляции мыши. Внутри else первым делом нужно найти позицию запятой чтобы в будущем разделить значение из переменной command на две части, в первой части будут координаты x, во второй части будут координаты y
C++:
else {
int comma = command.indexOf(',');
}
Далее нужно создать также внутри этого else проверку if, которая будет проверять переменную comma, больше ли она нуля или нет. Если больше, то значит, запятая была найдена.
C++:
if (comma > 0) {
}
Далее, внутри if нужно извлекать значение до запятой и назначить это значение в переменную dx типа int.
C++:
int dx = command.substring(0, comma).toInt();
Теперь, таким же способом нужно получить значение после запятой и назначить его в переменную dy.
C++:
int dy = command.substring(comma + 1).toInt();
Далее следует строка, в которой будет происходить эмуляция перемещения курсора по координатам из переменных dx и dy.
C++:
Mouse.move(dx, dy, 0);
Напоминаю, весь этот код находится внутри проверки if (c == '\n'), так что следующая строка должна быть также внутри этой проверки, но в самом ее низу вне каких-либо других условий.
C++:
command = "";
Компиляция кода для Arduino:
Теперь нужно скомпилировать данный код и загрузить его на Arduino. Для компиляции нужно нажать на кнопку, показанную на скриншоте:
Далее, для загрузки кода на Arduino нужно нажать соседнюю кнопку в виде стрелочки:
P.S. Очень важное замечание. Загрузить код на arduino не получится, если оно используется каким-либо приложением, например, если запущена нейросеть, которая уже умеет подключаться к Arduino. Поэтому потребуется отключить нейросеть; в противном случае ничего не загрузится.
На этом с работой над кодом для Arduino закончено, и нейросеть успешно функционирует в связке с Arduino, но есть одно НО. Дело в том, что Arduino может перемещать курсор только на восьмибитное значение со знаком, то есть от -127 до 127. Если значение для перемещения больше, то в таком случае перемещение будет некорректным. Но это можно исправить, разбивая значения на несколько частей. Например, в функции наведения в нейронной сети используется переменная cs2_aim_step из конфиг-файла, данная переменная используется для того чтобы разделять значение координат до объекта на число, указанное в этой самой переменной. Раньше можно было установить значение 1, и тогда бы курсор переместился за один подход моментально и очень быстро. Также после выполнения эмуляции в функции наведения стоит пауза, которая срабатывает после каждого подхода, и т.к. сейчас нельзя установить всего 1 подход, приходится ставить 2 и больше, из-за чего скорость наведения падает. Но в таком случае можно просто убрать паузу после каждого подхода или установить значение паузы на 0, и в таком случае можно сделать перемещение в несколько этапов и разбить значения для перемещения на несколько частей и при этом практически не потерять скорость наведения.
Пауза в функции наведения:
Python:
time.sleep(load_config("cs2_aim_time_sleep"))
Значения в конфиге для того, чтобы все работало корректно (можно изменить через интерфейс, а не через файл):
JSON:
"cs2_aim_step": 2,
"cs2_aim_time_sleep": 0.0,
Теперь с написанием кода для Arduino закончено, и уже сейчас можно проверить нейросеть CS2 на работоспособность. Но в проекте также присутствует версия нейронной сети default с простой антиотдачей и упрощенной логикой наведения. Делается это аналогично тому, как это реализовано в нейросети CS2, но все же я считаю, что кратко объяснить это стоит. Поэтому сейчас будет показано, как обновить и эту версию для работы с Arduino.
Изменение логики нейросети default:
Для начала нужно перейти в Python файл, в котором находится логика эмуляции мыши для нейросети default, и в этом файле найти функцию aim. Внутри функции aim нужно найти эти строки кода:
Python:
win32api.mouse_event(win32con.MOUSEEVENTF_MOVE, int(aim_step_x), int(aim_step_y), 0, 0)
time.sleep(load_config("aim_time_sleep"))
Нужно заменить первую строку, которая эмулирует курсор мыши, на строку для создания переменной, в которую будут записаны координаты для эмуляции, и добавить еще одну строку для отправки этой команды на Arduino, аналогично реализации из нейросети для CS2.
Python:
command = f"{aim_step_x},{aim_step_y}\n"
ser.write(command.encode())
time.sleep(load_config("aim_time_sleep"))
Затем получившиеся три строки нужно обернуть в try, а под try указать except, который будет обрабатывать ошибки при записи данных в Arduino, опять же аналогично нейросети CS2.
Python:
except serial.SerialTimeoutException:
print("Превышено время ожидания для записи данных на Arduino.")
ser.reset_output_buffer()
С функцией aim закончено, и можно приступать к изменению функции антиотдачи. В версии нейросети для CS2 это не требовалось, так как там функция антиотдачи не перемещала, а лишь передавала координаты в функцию наведения. В этом же случае функция антиотдачи работает отдельно от функции наведения.
Для того чтобы обновить функцию антиотдачи, потребуется найти в этой функции эти строки:
Python:
if 0 > win32api.GetKeyState(win32con.VK_LBUTTON):
win32api.mouse_event(win32con.MOUSEEVENTF_MOVE, 0, int(load_config("anti_recoil_px")), 0, 0)
time.sleep(load_config("anti_recoil_time_sleep"))
Вторую строку потребуется заменить на аналогичные строки, как и в функции наведения, с одним лишь отличием в названии переменных, данные из которых будут записаны в команду для отправки.
Python:
if 0 > win32api.GetKeyState(win32con.VK_LBUTTON):
command = f"0,{int(load_config('anti_recoil_px'))}\n"
ser.write(command.encode())
time.sleep(load_config("anti_recoil_time_sleep"))
Далее нужно получившиеся 4 строки обернуть в try, а в except указать ту же самую обработку, что и в функции наведения.
Python:
except serial.SerialTimeoutException:
print("Превышено время ожидания для записи данных на Arduino.")
ser.reset_output_buffer()
На этом написание кода для Arduino и адаптация кода всех версий нейронной сети закончено, и посмотреть, как работает в данный момент софт, можно в видеопрезентации по ссылке ниже:
Видео презентация работы нейронной сети в связке с Arduino: https://rutube.ru/video/440ca9c3165ffc3daa5930387fb2c04a/
Статья в виде документа: https://docs.google.com/document/d/112I1dbRSvDqPAnnOmtcYvQu0xpsnUS2FUucb97pfGpU/edit?usp=sharing
Вывод:
Переписать нейросеть на эмуляцию курсора мыши, используя Arduino, оказалось не так сложно. Самым сложным в этом было только разобраться в библиотеке эмуляции и в синтаксисе языка, на котором пишется код для Arduino. В основном же данное обновление потребовало минимальное количество знаний, и надеюсь, статья получилась понятной и подробной.О будущих статьях:
Раз у меня появился Arduino, хочется написать еще несколько статей по работе с ним. У меня уже есть пару идей о том, какие программы на нем написать, но если у вас есть пожелания, то можете сообщить об этом в комментариях.Сделано OverlordGameDev специально для форума xss.pro