Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Привет, Хабр.
Хочу рассказать как заставил работать старый монитор от медицинского оборудования с не менее старым неттопом.
Давеча моему другу досталась куча старого оборудования, среди которого был монитор, являющийся частью медицинского комплекса. Монитор имел сенсорный экран и прекрасно работал будучи подключенным к родному системному блоку по COM порту. В той же куче был и неттоп Acer Veriton n280g, о возрасте которого можно судить по наличию у него COM порта. Так как оба устройства имели COM порты было бы глупо не попробовать их поженить.
Поиск готового решения
Первым делом я проверил не — поддерживает ли установленная на неттопе Windows 7 работу с такими дисплеями. Подключив монитор к COM порту поддержки тачскринов в ОС я не нашел, хотя некоторые OEM версии такой функционал имеют.
Погуглил драйвера и утилиты по номеру модели, но ничего не находилось.
Затем принялся к изучению системного блока с которым монитор работал. На нем была установлена Windows XP Embedded, после загрузки которой открывалась оболочка специализированного ПО. Получить доступ к самой ОС простыми методами наподобие Ctrl+Alt+Del не получилось.
Последним вариантом было изучить содержимое жесткого диска. Мне подсказали что все не стандартные для Windows драйвера хранят свои inf файлы в директории C:\Windows\INF и имеют вид oem0.inf, oem1.inf, oem2.inf и т.д. В данном случае такие файлы были только для других устройств, значит сенсорный ввод работает с помощью какой то утилиты. И тут уже удалось кое что найти, поиск в FAR по слову “touchscreen” выдал файлы с многообещающими названиями - TouchScreenSetup.exe, TouchScreenCalibration.exe, TouchScreen.dll, TouchScreenSV.exe причем последний из них находился в C:\Windows\System32 что мне показалось интересным. Запустить на своей системе удалось только TouchScreenSetup.exe который предлагал выбрать количество мониторов и сразу же падал, остальные exe не запускались никак и различные варианты режимов совместимости не помогали, оставалось только изучить эти файлы. Прежде всего я проверил не написан ли они на .NET, в таком случае их можно было бы просто отрефлекторить, дотнетовским оказался только упоминавшийся выше TouchScreenSetup.exe и ничего интересного в нем не было, все остальные файлы были нативными. Я не имеют опыта в дизассемблировании и анализе нативного кода и потыкавшись полчаса в IDA понял, что с наскоку разобраться не получится.
Так как все остальные варианты были исчерпаны, остается только разобрать в протоколе и написать утилиту самому. Откровенно говоря мне изначально хотелось этим заняться.
Анализ протокола
Для чтения данных я использовал COM Port Toolkit с конфигурацией порта по умолчанию. При касании к дисплею в порт пошли данные.
В них сразу был заметен паттерн - 2 повторяющиеся последовательности из 2-х байт первая 55 54 и вторая FF 00. Между 55 54 и FF 00 5 байт, а между FF 00 и 55 54 1 байт. Я предположил что 5 байт являются полезной нагрузкой, а 1 байт контрольной суммой.
В таком случае пакет выглядит например так:
55 54 01 C7 01 E7 01 FF 00 03
А его структура имеет следующий вид:
Байт | Значение |
0-1 | Признак начала пакета |
2-6 | Полезная нагрузка |
7-8 | Признак конца пакета |
9 | Контрольная сумма |
Оставалось разобрать содержимое полезной нагрузки и понять как вычисляется контрольная сумма.
Разбор полезной нагрузки
Логично было предположить, что полезная нагрузка должна содержать в себе координаты нажатия, я сохранил данные о нажатиях в четырех углах монитора и проанализировать их.
Сокращенный лог короткого касания в нижнем левом углу:
Признак начала пакета | Полезная нагрузка | Признак конца пакета | Контрольная сумма |
55 54 | 01 C7 01 E7 01 | FF 00 | 03 |
55 54 | 02 C8 01 EA 01 | FF 00 | 08 |
55 54 | 02 CA 01 E8 01 | FF 00 | 08 |
55 54 | 02 C7 01 E8 01 | FF 00 | 08 |
55 54 | 04 C7 01 EA 01 | FF 00 | 09 |
В глаза бросалось что третий байт (первый в полезной нагрузке) принимает всего 3 значения: 01, 02 и 04. Причем, значение 01 стоит только в первом пакете, 04 только в последнем, а 02 во всех пакетах между первым и последним. Вероятно, в третий байт записывается состояние нажатия: 01 - начало касания, 02 - удержание и 04 - конец касания. Было бы понятнее, если бы конец нажатия обозначался значением 03, а так оставался вопрос: может ли байт принимать другие значение? Поэкспериментировав некоторое время и не разу не встретив значение отличного от уже известных 3-х, решил пока принять это как данность. Структура пакета стала понятнее.
Байт | Значение |
0-1 | Признак начала пакета |
2 | Состояние нажатия |
3-6 | Полезная нагрузка |
7-8 | Признак конца пакета |
9 | Контрольная сумма |
В полезной нагрузке оставалось 4 не разобранных байта. Очевидно они должны содержать координаты в декартовой системе координат x и y, получается по 2 байта на координату.
Для анализа я взял полученные ранее данные о касаниях в каждом из четырех углов дисплея и преобразовал значения в десятичную систему как little-endian и big-endian.
Получились следующие значения.
Верхний левый угол | Верхний правый угол | ||||
Hex | CF 01 | F2 0D | Hex | 4D 0E | 01 0E |
Big-endian | 52993 | 61965 | Big-endian | 19726 | 270 |
Little-endian | 463 | 3570 | Little-endian | 3661 | 3585 |
Нижний левый угол | Нижний правый угол | ||||
Hex | C7 01 | E7 01 | Hex | 44 0E | E9 01 |
Big-endian | 50945 | 59137 | Big-endian | 17422 | 59649 |
Little-endian | 455 | 487 | Little-endian | 3652 | 489 |
Если посмотреть на координаты little-endian то видно что мы имеем координатную плоскость с началом координат в нижнем левом углу и концом в верхнем правом.
Правда началом координат является не точка (0, 0), а точка (455, 487). Все содержимое пакета стало понятным.
Байт | Значение |
0-1 | Признак начала пакета |
2 | Состояние нажатия |
3-4 | Координата X |
5-6 | Координата Y |
7-8 | Признак конца пакета |
9 | Контрольная сумма |
Оставалось выяснить как рассчитывается контрольная сумма.
Вычисление контрольной суммы.
Так как значение контрольной суммы составляет всего один байт, я предположил что это скорее всего CRC-8 или какая то его вариация, так как результатом вычисления CRC-8 является 1 байт или 8 бит что и отражено в его названии. Я использовал crccalc чтобы перебрать все варианты расчета контрольной суммы, брал только полезную нагрузку, включал признаки конца и начала пакета как вместе так и по отдельности, но ничего не подходило.
Внимательнее изучив уже записанные пакеты я заметил закономерность, что при изменении полезной нагрузки на единицу контрольная сумма также отличалась на единицу. На сколько мне известно — алгоритм CRC не обладает свойствами линейной функции, то есть в данном случае используется некий собственный алгоритм, в котором вероятно используется только сложение и вычитание. Если это так, то вычислить его должно быть не сложно.
Для последующего анализа я взял несколько пакетов чьи контрольные суммы последовательно отличались друг от друга на единицу и расположил их в порядке возрастания, перевел в десятичную систему и откинул признаки начала и конца пакета так как они являются константой.
Полезная нагрузка | Контрольная сумма | ||||
Состояние нажатия | Координата X | Координата Y | |||
Байт 1 | Байт 2 | Байт 1 | Байт 2 | ||
2 | 78 | 14 | 2 | 14 | 192 |
2 | 80 | 14 | 1 | 14 | 193 |
2 | 80 | 14 | 2 | 14 | 194 |
2 | 81 | 14 | 2 | 14 | 195 |
2 | 82 | 14 | 2 | 14 | 196 |
Для примера взял первую строку, первое что приходит в голову — это просто просуммировать все данные, получается значение 110, контрольная сумма равна 192 то есть разница 82.
Предположим, что контрольная сумма равна сумме данных плюс константа 82.
На данной небольшой выборке этот простейший алгоритм работает, результат вычислений всегда совпадает с известной контрольной суммой. Однако, чтобы быть уверенным, я решил проверить все полученные ранее данные и набросал формулы в excel для проверки. На большем количестве данных сразу стало видно ошибку.
Для примера вот этот пакет:
Полезная нагрузка | Контрольная сумма | ||||
Состояние нажатия | Координата X | Координата Y | |||
Байт 1 | Байт 2 | Байт 1 | Байт 2 | ||
2 | 212 | 1 | 246 | 13 | 46 |
Сумма данных и константы 558
Очевидно, что во многих случаях общая сумма больше 255, то есть не помещается в один байт. Простейшим решением будет взять остаток от деления суммы на 255.
558 % 255 = 48
Результат отличается от контрольной суммы на 2, легко заметить что 2 в данном случае — это результат целой части деления 558 на 255.
Если из предыдущего результата вычесть 2, то получается верная контрольная сумма.
Проверил этот вариант на всей выборке и он сработал.
То есть, контрольная сумма равна разнице неполного частного суммы всех байт полезной нагрузки и константы 82 и остатка деления суммы всех байт полезной нагрузки и константы 82.
Не уверен как правильно записать формулу, в excel выглядит так:
=IF((SUM(A1:E1)+82)<=255;SUM(A1:E1)+82;(SUM(A1:E1)+82) - (255 * INT((SUM(A1:E1)+82)/255)) - INT((SUM(A1:E1)+82)/255))
Алгоритм очень простой и вероятно является известным, просто я не смог его найти.
Неясно только почему константа равна 82, сумма отброшенных байт не подходит, я предположил, что это может быть битовая маска так как в двоичном виде число имеет вид “1010010”. Но какая от неё здесь польза мне не понятно, отпишитесь в комментариях если знаете.
Теперь, когда все данные известны пришло время реализовать работу тачскрина в коде.
Реализация утилиты
Я написал консольное приложение на C# с тем, чтобы потом запустить его как службу.
Логика работы очень простая. Читать данные с COM порта ожидая признак начала пакета, разобрать пакет и проверить контрольную сумму. Если все ок — то установить позицию курсора и имитировать нажатие клавиши через вызов user32.dll.
Но координаты с порта еще необходимо преобразовать, так как система координат экрана в Windows отличается от системы координат сенсорного экрана.
Разрешение монитора составляет 1024 × 768 и начинается в верхнем левом углу с точки (0, 0).
Разрешение сенсорного экрана составляет 3661 x 3585, по крайней мере это максимальные значения которые мне удалось получить экспериментальным путем, с началом в нижнем левом углу в точке (455, 487).
Для преобразования координат прежде всего необходимо вычислить соотношение соответствующих осей обеих систем друг к другу, обозначим данные соотношения как Rx и Ry для оси X и Y соответственно.
Для этого для каждой оси сенсора (Xs, Ys) из максимального значения (Xs(max), Ys(max)) вычитаем ее минимальное значение то есть начальную точку (Xs(min), Ys(min)) так как оно не равно нулю. Затем, делим на разрешение экрана в Windows (Xw(max), Yw(max)), то есть на максимальное значение в системе координат Windows, которое для данного монитора будет всегда меньше, чем максимальное значение в системе координат сенсора.
Получаем следующие формулы:
Зная соотношение для каждой оси можно преобразовывать координаты.
Чтобы получить из координаты X сенсора (Xs) координату X в Windows (Xw) вычитаем из Xs значение начала координат сенсора (Xs(min), Ys(min)) и делим результат на ранее вычисленное соотношение осей Rx. Для оси Y процедура та же самая, но после результат нужно вычесть из Yw(max) чтобы инвертировать ось.
Правый клик реализовал через длительное касание, как на современных ноутбуках с сенсорным экраном. Если через секунду курсор остается в том же месте с допустимой точностью — то вызвать клик правой клавиши.
Однако возникла неожиданная проблема.
Приложение я отлаживал на MacBook Air 2013 с Windows 10 подключив монитор через переходник USB2COM. При выполнении на ноутбуке приложение работало нормально, а при запуске на неттопе в каждом пакете последний байт несущий в себе контрольную сумму всегда имел значение 0. Я проверил какие данные идут в COM Port Toolkit: и на ноутбуке и на неттопе в обоих случаях данные были валдиные, контрольная сумма имела значения отличные от 0, конфигурация порта в приложении и COM Port Toolkit совпадала. Выходило что проблем не в железе и баг кроется в софте, но где именно если на ноутбуке приложение отрабатывало правильно. Ноутбук и неттоп различались как версиями Windows, 10 и 7, так и версиями .NET Framework, 4.7.2 и 4.0, следовательно, проблема должна была быть в чем то из них. Обновление Windows на неттопе до 10 версии было чревато потенциальными проблемами с производительностью, отсутствием драйверов и совместимости с специализированным софтом, который планировалось потом на нем использовать. Обновление же .NET Framework не проходило без установки обновлений Windows, которые по непонятной причине не устанавливались, оставалось только пытаться решить проблему на стороне приложения. Игры с параметрами COM порта результатов не дали, а вот посмотрев внимательно на код я смог найти куда вставить костыль.
Проблемный участок кода:
_serialPort.ReadTo("\x55\x54");
_serialPort.Read(_buffer, 0, 8);
Здесь происходит ожидание признак начала пакета и затем читаются оставшиеся 8 байт пакета и как уже упоминалось при исполнении кода на неттопе последний байт в буфере всегда имел значение 0.
Но если читать байты по одному — то код выполняется одинаково на обоих компьютерах.
_serialPort.ReadTo("\x55\x54");
_buffer[0] = (byte) _serialPort.ReadByte();
_buffer[1] = (byte) _serialPort.ReadByte();
_buffer[2] = (byte) _serialPort.ReadByte();
_buffer[3] = (byte) _serialPort.ReadByte();
_buffer[4] = (byte) _serialPort.ReadByte();
_buffer[5] = (byte) _serialPort.ReadByte();
_buffer[6] = (byte) _serialPort.ReadByte();
_buffer[7] = (byte) _serialPort.ReadByte();
Мне не известно, чем именно вызвано такое поведение. Если кто-то знает — отпишитесь в комментариях.
Проверив все в консольном приложении и убедившись, что все работает как ожидалось, я создал из него службу Windows и добавил её в автозапуск, но тут крылся еще один сюрприз, думаю читатель, имеющий опыт в разработке под Windows уже понял в чем он заключается. Приложение просто не подавало признаков жизни будучи запущенным как служба, запуск от имени пользователя тоже не помог. Как выяснилось — службы не могут взаимодействовать с графическом интерфейсом, так как запущены в отдельной сессии. Есть возможность создать интерактивную службу, но данный способ не рекомендуется самой microsoft по соображениям безопасности и если я правильно понял поддерживается только в версиях Windows до Vista. В связи с этим я переписал приложение на WinForms, оставив из интерфейса только иконку в трее. На этот раз все заработало как надо.
Я получил настоящие удовольствие разбираясь с этим протоколом и реализацией его парсинга. Конечно, если бы он был зашифрован даже простейшим способом его разбор потребовал бы уже криптоанализа, что по-моему требует квалификации как минимум на несколько порядков выше и сделало бы порог входа очень высоким. Скорее всего в современном проприетарном оборудовании шифрование используется повсеместно и так просто с ним поиграть не получится, но старое железо все еще может порадовать своей простотой.
Код приложения на GitHub