«Удалённо» управляем компьютером с доступом в BIOS
Итак, о чём это? Сейчас для удалённого управления компьютером есть великое множество программ на любой цвет, вкус и запах. Но что, если мы хотим пойти немного дальше, и наши требования к удалённому управлению становятся немного жёстче:
- Мы хотим иметь возможность не только работать в операционной системе, но и заходить в биос, или вообще эту систему переустановить.
- По тем или иным причинам, компьютер не может быть подключён к сети, но управлять им мы от этого меньше не хотим, а рядом у нас есть компьютер, который в сети находиться может.
Но как это сделать? Вот этим мы тут и будем заниматься...
▍ Как я к этому пришёл
Иногда мне приносят разное железо с просьбами переустановить винду/почистить вирусы и т. д. А я что? Я ж программист простой: мне приносят и просят сделать — я делаю. Но порой не очень удобно подключать к этому всему отдельную мышь/клавиатуру и монитор, а бывает, что там идёт долгий процесс, не требующий особого вмешательства, но периодически надо сделать пару кликов, что я мог бы сделать удалённо с работы, будь у меня такая возможность, и сэкономить кучу времени вечером. Я знаю, что такое удалённое управление бывает на серверных материнских платах, но в последний раз мне приносили сервер, чтобы я переустановил на нём Windows никогда, или даже ещё раньше.
В какой-то момент у меня возникла в голове идея: есть же недорогие устройства видеозахвата USB-HDMI, а ещё есть ESP32 S2/S3, которые умеют эмулировать USB. А что нам ещё надо? Изображение с компьютера мы можем получить, клавиатуру/мышь можем проэмулировать. Может быть, такие проекты даже уже есть, но когда мне в голову приходит идея, которая кажется мне интересной, я:
Ну что ж, проекту быть, и для него нам потребуется следующий минимум:
- HDMI-плата захвата видео
- Плата ESP32 S3 — у неё сразу есть 2 TypeC разъёма, что упростит нам жизнь
- HDMI-кабель
- 2 кабеля USB TypeA — TypeC
- Компьютер с Windows, стоящий рядом
С железом условно всё, и если нам не нужно управлять аппаратной перезагрузкой/включением компьютера, то нам даже не придётся ничего паять. А если нужно, то всему своё время…
Итак, возможно, когда я написал пункт «Компьютер с Windows, стоящий рядом», я кого-то очень сильно огорчил. И я согласен, что решение не самое оптимальное, если бы всё работало под Linux, да ещё выводить всё в Web, то можно было бы взять Малинку/Апельсинку и… Но нет. Хотя, может и да, ведь проект открытый, и если у кого-нибудь будет время, желание и умение, то он может переделать под Линукс мой проект, сделанный на .Net, но пока я всё основное время работаю под осью одной из корпораций зла, проект только под Винду.
▍ Приступаем к работе
Ладно, начинаем. Схема подключения простая:
Теперь, когда у нас всё подключено, что дальше? Дальше пишем ПО.
ПО написано, что делаем дальше? Для начала надо залить прошивку на нашу ESP32 S3. Убеждаемся, что драйверы ком-порта у нас установлены (откуда их брать, обычно указывает продавец этой самой платы). Для заливки прошивки я использовал Arduino IDE, тем более что скетч написан именно в нём. В этой статье я не буду подробно описывать процесс настройки Arduino IDE на работу с платами EST32 и прошивки — его можно найти, например, везде, и он довольно прост. Дальше перейдём непосредственно к ПО для удалённого управления, а тут всё ещё проще:
Подключаем всё, включаем компьютер, которым мы хотим управлять, и в программе нажимаем кнопку подключения. И если все звёзды на небе сошлись, то мы видим изображение с монитора компьютера, у нас есть мышь и клавиатура и они работают.
▍ Немного о коде
Прежде чем продолжить проект, добавив к нему ещё пару крутых фич, я предпочту немного замедлиться и погрузиться в код, который был написан. И если со стороны всё выглядит как вжух и готово, то на деле было не совсем так. Начиная этот проект, я стал искать библиотеки для .Net для работы с устройствами видеозахвата. И первое, что мне попалось, было OpenCvSharp. Я проверил, что эта библиотека работает с веб-камерами и другими подобными устройствами и выдаёт изображение на WinForms. Отказался я от неё потому, что не нашёл у неё возможности нормально перечислить список всех камер с их именами для выбора в меню, а городить это отдельно не очень хотелось, и пока я ещё не сильно привык к этой библиотеке, я стал смотреть другие.
Следующей была AForge. В ней список камер перечислялся нормально, с именами и фамилиями, и, казалось бы, всё было хорошо, но нет. Когда я тестировал программу на реальном устройстве видеозахвата, всё было хорошо, но когда отлаживался на виртуальной веб-камере от OBS Studio, картинку я не получал. При этом ошибку тоже. Я не получал ничего, я был просто проигнорирован и видел, как происходит ничего. Мне не понравилось. Я бы подумал, что что-то не так с самой камерой у OBS, но в OpenCvSharp всё прекрасно работало. И я стал копать… И вот щетина на моём лице уже выросла, вся водка была выпита, медведь доиграл свою арию на балалайке, но ничего по-прежнему не работало. Но вскоре проблема вскрылась. AForge принудительно запрашивает у камеры формат RGB24, и если устройство его поддерживает, то всё ОК. А если нет, тады ой. Камера от OBS поддерживала формат NV12, но AForge не умела его декодировать, как и многие другие форматы. Можно было оставить эту библиотеку и пойти к следующей, но это уже было делом принципа, я потратил на поиск этой особенности слишком много времени, и раз уж исходный код AForge открыт, я решил его поправить. В очередной раз я убедился, насколько ChatGPT (и ему подобные) делают нашу программистскую жизнь легче.
Магия XXI века Запрос: write me c# function that converts YUY2 format pixels (IntPtr buffer, int bufferLen) to Bitmap
using System; using System.Drawing; using System.Drawing.Imaging; using System.Runtime.InteropServices; public class YUY2Converter { // Function to convert YUY2 buffer to a Bitmap public static Bitmap ConvertYUY2ToBitmap(IntPtr buffer, int bufferLen, int width, int height) { // Validate buffer length if (bufferLen != width * height * 2) throw new ArgumentException("Invalid buffer length for given width and height"); // Create a new Bitmap Bitmap bitmap = new Bitmap(width, height, PixelFormat.Format24bppRgb); // Define YUV constants for conversion float yFactor = 1.164F; float uFactor = 2.018F; float vFactorR = 1.596F; float uFactorG = -0.391F; float vFactorG = -0.813F; // Lock bitmap data for direct access BitmapData bmpData = bitmap.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, bitmap.PixelFormat); unsafe { byte* pYUY2 = (byte*)buffer; byte* pBmp = (byte*)bmpData.Scan0; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x += 2) { int y0 = pYUY2[0]; int u = pYUY2[1] - 128; int y1 = pYUY2[2]; int v = pYUY2[3] - 128; // Convert YUV to RGB for first pixel (x) byte r = ClampToByte(yFactor * y0 + vFactorR * v); byte g = ClampToByte(yFactor * y0 + uFactorG * u + vFactorG * v); byte b = ClampToByte(yFactor * y0 + uFactor * u); // Set first pixel (x) in bitmap int idx1 = ((y * width) + x) * 3; pBmp[idx1 + 2] = r; pBmp[idx1 + 1] = g; pBmp[idx1] = b; // Convert YUV to RGB for second pixel (x + 1) r = ClampToByte(yFactor * y1 + vFactorR * v); g = ClampToByte(yFactor * y1 + uFactorG * u + vFactorG * v); b = ClampToByte(yFactor * y1 + uFactor * u); // Set second pixel (x + 1) in bitmap int idx2 = ((y * width) + x + 1) * 3; pBmp[idx2 + 2] = r; pBmp[idx2 + 1] = g; pBmp[idx2] = b; // Move to the next set of YUYV values (4 bytes) pYUY2 += 4; } } } // Unlock the bitmap data bitmap.UnlockBits(bmpData); return bitmap; } // Helper function to clamp values to byte range private static byte ClampToByte(float value) { return (byte)Math.Max(0, Math.Min(255, (int)value)); } }
Вот так легко и просто ChatGPT сгенерировал мне функцию перекодировки YUY2 в RGB, также я сделал для NV12, YUYV и I420. Если бы я писал это сам, я бы потратил на это, может быть, весь день, пока разбирался в этих форматах пикселей и отлаживал баги. А тут код написан за минуту и после визуальной проверки на отсутствие явной лажи уже работал в проекте, сразу же после компиляции. Ладно, если нужна бочка дёгтя на вашу ложку мёда, я вам её таки дам: так хорошо бывает не всегда, иногда он генерит нерабочий код, иногда рабочий, но неэффективный. Но можно попросить его ещё раз? и часто у него получается лучше.
▍ Немного про экран
При удалённом управлении часто бывает, что размеры удалённого экрана превышают размеры окна, в котором мы работаем. И самый простой вариант — это обычное пропорциональное растягивание/сжатие картинки под размер рабочей области окна.
Но, бывают ситуации, когда хочется видеть всё в масштабе 1 к 1, а удалённый экран больше нашего. И я отметил для себя 4 разных варианта, один из которых и реализовал:
1. Просто скроллбары по краям, которые нужно скроллить мышкой вручную. Это не очень удобно.
2. Стиль RAdmin — когда мы подводим мышку к краю окна и ведём её дальше, экран начинает скроллиться, а движение мышки блокируется. Уже лучше, но мне не нравится, что в этом случае при проскролливании нужно блокировать движении мыши.
3. Стиль Aspia — когда мы подводим мышку к краю окна, окно начинает само скроллиться, перемещение мышки при этом не блокируется.
4. По мере того, как мы ведём мышью от одного края нашего окна к другому, экран сам проскролливается к этому краю. Поначалу не привычно, но потом вполне удобно. Этот вариант мне понравился больше, и именно его я и реализовал.
▍ Эмуляция устройства ввода
С ESP32 S3 программа взаимодействует через ком порт. Она просто отправляет ей команды (KeyDown, KeyUp, MouseDown, MouseUp, MouseMove). Для мышки была выбрана эмуляция устройства с абсолютным позиционированием курсора, там передаются координаты x и y в пределах от 0 до 32768. Таким образом мне не нужно думать, какое разрешение на удалённом компьютере, всё будет работать само. С клавиатурой оказалось немного сложнее — получаемые коды клавиш нельзя было просто передать один в один в класс USBHIDKeyboard, точнее можно, но со своими приколами, которые местами все портили. Но можно было передавать сырые USBHID-коды, в которые нажатые клавиши надо было сконвертировать. Этот путь я и выбрал. Дальше эти нажатия/отжатия уже отправляются на устройство и эмулируются на конечной системе. Я не стал заморачиваться с перехватом особых спец-клавиш типа CapsLock, но на сегодняшний день у меня нет сценариев, где это могло бы потребоваться.
Заходим в биос, загружаемся в ОС, проверяем, как работает мышь и клавиатура
▍ А теперь сделаем это ещё лучше!
В процессе создания всего этого безобразия я решил, что его можно сделать ещё безобразней! А именно: мне внезапно может потребоваться перезагрузить компьютер, если он завис наглухо. Или выключить, а то чего он тут работает? А потом включить потому, а то чего это он не работает? Для этого надо замкнуть соответствующие пины на материнской плате. Это разъём Fpanel и нам нужны вот эти ребята:
У меня дома валялось пара реле с управлением от 3 вольт, и я подключил его управление к пинам ESP32 S3, а замыкание к пинам материнской платы и… естественно, ничего не заработало, потому что нельзя подключать реле к пинам ESP32 напрямую, они не дают такой ток, чтобы сработала катушка реле, но:
Дяденька, я, на самом деле, не настоящий электронщик, я этот паяльник нашёл!
Ладно, у меня валялись ещё IFR3205. Не надо на меня так смотреть, я понимаю, что использовать их для включения реле — это дикая дичь, они были рождены летать, а не ползать. Но ничего не выйдет, потому что, я сказал «ползать» и они поползли! Я не буду выкладывать схему подключения этого безобразия, потому что мне стыдно. Проще взять готовые к подключению напрямую реле типа этих и не париться:
Я использовал пины 17 и 18, но если нужны другие, можно поменять это в скетче.
Итак, теперь у меня заработали кнопки перезагрузки и включения удалённого компьютера. Также я добавил возможность ввода текста из буфера обмена эмуляцией нажатия этих клавиш (как в HyperV). Программа даже умеет переключать раскладку, если видит русские буквы в тексте. Главное, чтобы изначально раскладка на удалённой системе всегда была выбрана английской, а то будет всё наоборот. И вроде бы всё было закончено. Но тут мне пришла в голову ещё одна безумная идея…
▍ А как насчёт реальной камеры?
Да, ведь мне могут принести ноутбук или моноблок, у которого может и не быть второго видеовыхода, а если и быть, то не факт, что он будет выводить туда стартовую загрузку. Поэтому я могу просто поставить камеру перед экраном и брать изображение с него. Но! Я же никогда не смогу поставить камеру настолько идеально ровно, чтобы экран был чётко в кадре, не вылезая из него и не оставляя лишнего по краям. А если экран не будет точно заполнять кадр, то собьются и координаты мыши. Поэтому мы будем натягивать сову на гло изображение с камеры на наш виртуальный экран. Здесь мне было откровенно лень. Я попросил ChatGPT сгенерить мне функции для Perspective Image Distortion. После N попыток он привёл меня к библиотеке Emgu.CV, которая делала это достаточно быстро, и я накидал редактор для этого растягивания.
Теперь мы можем натянуть картинку с камеры на наш экран, и мышка ездит от угла к углу достаточно точно. Качество изображения, конечно, прекрасно настолько, что от работы с такой картинкой возникает непременное желание куда-нибудь выйти, например, в какое-нибудь ближайшее окно. Но тут уже всё зависит от качества камеры. Главное, возможность есть.
Готовы поработать так на удалёнке весь день?)
▍ Бонус
Не люблю я, когда устройство не похоже на устройство. У меня давно сложилась простая схема для создания корпусов:
А ещё у меня есть 3D-принтер! Поэтому проектируем такую коробочку:
Я делал под свои реле, но это OpenSCAD и там легко можно задать размеры и количество для своих.
Я всегда делаю корпуса под болтики для компьютерных вентиляторов, они хорошо вкручиваются и крепко держатся в пластике и не нужно вплавлять гайки
Термоклеем делаем блямбы на проводах, чтобы не оторвать пайку, если дёрнуть за них
А вот и устройство. Во всяком случае похоже на устройство. Если бы я не знал, как выглядят устройства, я бы подумал, что это, возможно, оно.
▍ Дайте мне это немедленно!
Как обычно, всё, что я сюда выкладываю — MIT, поэтому делайте с этим, что хотите, кроме модифицированной AForge, там GPL/LGPL, но я все исходники выложил и чист перед ними)
Страница проекта: тут.
Но на всякий случай предупреждаю, что я особо не сижу на GitHub, редко туда захожу, не часто там отвечаю, просто выложил и забыл. А сюда пишите, отвечу)