Русский
Русский
English
Статистика
Реклама

Mouse

Из песочницы Динамическое меню c поддержкой touch move и mouse move на RevolveR

18.10.2020 20:21:07 | Автор: admin
Наверняка многие из вас хотели бы научиться создавать красивые и подвижные меню в духе Android Java и Kotlin приложений. Скорее всего даже многие из вас ради этого уходили в области программирования отдельных приложений и были вынуждены осваивать инородный стек.

В этой статье я расскажу как просто и непринужденно можно создавать линейные динамические меню поддерживающие не только события мыши, но и поинтеры для мобильных версий.

И так: Simple Dynamic Menu by RevolveR Labs.

image

Начинается все с верстки. Она должна быть семантической, легкой и современной.



<nav class="dynamic-menu"><ul><li><a href="http://personeltest.ru/aways/revolvercmf.ru">RevolveR Labs</a></li><li><a href="#">Ultra newest solutions</a></li><li><a href="#">The way of incredible</a></li><li><a href="#">In search of the best</a></li><li><a href="#">Progressive RevolveR frontends</a></li><li><a href="#">Developing of new era</a></li></ul></nav>

Мы используем стандартный маркированный список и HTML 5 в качестве элемента враппера, а чтобы сделать меню плавающим сразу пропишем CSS стили вытягивающие меню на за пределы экрана на всю ширину списка элементов и скроем все лишнее до области видимости:



.dynamic-menu {display: inline-block;text-align: center;overflow: hidden;margin: 0 auto;height: 3vw;width: 80%;}.dynamic-menu ul {transition: all 2.5s ease-in-out;position: relative;list-style: none;width: 900vw;padding: 0;margin: 0;left: 0vw;}.dynamic-menu ul li {box-shadow: 0 0 0.1vw #333;border: .1vw dashed #fff;background: #a2a2a2;margin-bottom: 1vw;display: inline-block;border-radius: .2vw;margin-right: .5vw;padding: .2vw 1vw;background: #888;float: left;}.dynamic-menu ul li a {text-shadow: 0 0 0.2vw #fff;font: normal 2vw Helvetica;text-decoration: none;color: #006400;}.dynamic-menu ul li a:hover {text-decoration: underline;color: #674c2be0;}

Наш CSS готов. Теперь меню будет расположено по центру и все элементы списка не отобразяться. Нам осталось написать хэндлеры меню, которые будут отвечать за плавное перемещение списка при нажатой левой кнопке мыши или событии touch.



Handler для desktop версии


Для работы хэндлера нам понадобится инициализировать RevolveR инстанс и использовать некоторое встроенное API работы с событиями:



let launch = RR.browser;RR.menuMove = null;if( !RR.isM ) {RR.event('.dynamic-menu ul', 'mousedown', (e) => {e.preventDefault();if( !RR.menuMove ) {RR.menuLeft = RR.curxy[0];RR.MenuMoveObserver = RR.event('body', 'mousemove', (e) => {e.preventDefault();RR.styleApply('.dynamic-menu ul', ['transition: all 0s ease']);RR.menuMove = true;RR.menuPosition = ( RR.menuLeft - RR.curxy[0] ) *-1;RR.styleApply('.dynamic-menu ul', ['left:'+ RR.menuPosition +'px']);RR.event('body', 'mouseup', (e) => {e.preventDefault();if( e.target.tagName === 'A' && !RR.touchFreeze ) {//R.loadURI(target.href, target.title);console.log(e.target.href);RR.touchFreeze = true;RR.menuMove = null;}void setTimeout(() => { RR.menuMove = null;}, 50);void setTimeout(() => {if( !RR.menuMove ) {RR.styleApply('.dynamic-menu ul', ['left: 0px', 'transition: all 2.5s cubic-bezier(0.175, 0.885, 0.32, 1.275)']);}}, 2500);});});}});}

Большинство необходимых event уже работают после запуска гетера RR.browser(). Это например отслеживание событий изменения размера окна и постоянно обновление положения указателя мыши RR.curxy.



Здесь мы используем метод блокировки флагами для того, чтобы добиться синхронности выполнения событий и задать блокирующие флаги, например, для реализации запрета события перехода по ссылке, если меню находится в движении.



RR.MenuMoveObserver является собой event стеком, который хранит MD5 hash события для того, чтобы можно было выключить часть хэндлера отвечающего за смену положения по оси X. Мы выключаем обсерверы каждый раз когда событие клик завершилось в пользу mouseup.



Готово. При нажатии на левую клавишу мыши, если держать кнопку утопленной будет происходить отслеживание положения курсора мыши по оси X, а обсервер обеспечит своевременное обновление положения left контейнера списка меню внутри враппера области видимости и лента меню начнет двигаться открывая не поместившиеся элементы списка.



Мобильный handler меню


Основной принцип работы остается примерно таким же, но я не стал схлопывать разные хэндлерры в один обработчик чтобы оставить понятность и читабельность кода.



Проверив, что инициализирован мобильный инстанс браузера мы подключим аналоги слушателей событий уже не для мыши, а для touch экрана.



if( RR.isM ) {RR.event('.dynamic-menu ul', 'touchstart', (e) => {e.preventDefault();RR.menuMove = null;RR.event('body', 'touchend', (e) => {e.preventDefault();if( !RR.menuMove ) {RR.touchFreeze = null;let target = e.changedTouches[0].target;if( RR.isO(RR.MenuMoveObserver) ) {for( i of RR.MenuMoveObserver ) {RR.detachEvent( i[ 2 ] );}}if( target.tagName === 'A' && !RR.touchFreeze ) {//R.loadURI(target.href, target.title);console.log(e.target.href);RR.touchFreeze = true;RR.menuMove = null;}void setTimeout(() => {if( !RR.menuMove ) {RR.styleApply('.dynamic-menu ul', ['left: 0px', 'transition: all 2.5s cubic-bezier(0.175, 0.885, 0.32, 1.275)']);//RR.animate('.dynamic-menu ul', ['left:0px:1000:wobble']);}}, 2500);}});if( !RR.menuMove ) {RR.menuLeft = e.changedTouches[0].screenX;RR.MenuMoveObserver = RR.event('body', 'touchmove', (e) => {e.preventDefault();RR.styleApply('.dynamic-menu ul', ['transition: all 0s ease']);RR.menuMove = true;RR.menuPosition = ( RR.menuLeft - e.changedTouches[0].screenX ) *-1; RR.styleApply('.dynamic-menu ul', ['left:'+ RR.menuPosition +'px']);RR.event('body', 'touchend', (e) => {RR.menuMove = null;});});}});}

В коде вы увидите небольшую разницу. Во первых event.target теперь не работает и нужно следить за сериями touch. Я добавил анимацию возвращения меню с эффектом easing и теперь меню само плавно возвращается в начальное положение спустя некоторое время бездействия с меню:



void setTimeout(() => {if( !RR.menuMove ) {RR.styleApply('.dynamic-menu ul', ['left: 0px', 'transition: all 2.5s cubic-bezier(0.175, 0.885, 0.32, 1.275)']);}}, 2500);

Demo


Чтобы посмотреть как работает Dynamic Menu на базе библиотеки RevolveR вы можете пройти по ссылке.



Итог


Многие из вас используют готовые плагины или встраивают блоки верстки с меню гамбургера, однако динамические линейные меню ни чуть не хуже и позволяют экономить место в интерфейсах. Многим из вас наверняка показалось бы сложным реализовать такие меню из-за разницы в слушателях событий и неумения использовать флаги. Этот пример был направлен показать, что средствами обычного JavaScript можно не обладая какой-то запредельной квалификацией реализовывать такие же вещи, как и в удобных Android приложениях, а мастерская работа с анимациями позволяет создавать потрясные эффекты переходов и разные хуки событий на какие-либо срабатывания.

Подробнее..
Категории: Html , Javascript , Интерфейсы , Css , Html 5 , Touch , Mouse , Menu

Maple BUS в ореховой скорлупе или Периферия SEGA Dreamcast, как сделать

01.02.2021 10:05:55 | Автор: admin

И сразу к делу!

Протокол Maple BUS симметричный, то есть имея одну хорошую реализацию например HOST'а эту же реализацию можно использовать и как DEVICE. Проще, - можно читать джойстик, а можно им прикинуться.

Описание протокола (аппаратная часть).

Интерфейс Maple BUS двух-проводный. SDCKA/SDCKB, каждая из линий на определенных этапах выполняет роль как "передающая данные" и так и "защелкивающая данные".

Общение по шине Maple BUS осуществляется пакетами. Каждый пакет данных состоит из заголовочного паттерна, данных, контрольной суммы и завершающего паттерна. Максимальная длина пакета данных 1024 байта.

Всего паттернов 5 видов:

START - указывает на начало передачи данных (4-ре клока SDCKB в то время пока SDCKA в низком уровне).

Пакет всегда должен заканчиваться паттерном END (2-ва клока SDCKA пока SDCKB в низком уровне):

Occupancy паттерн - указывает на старт режима прослушивания шины (8-мь клоков SDCKB пока SDCKA в низком уровне). Переход линии HI->LO SDCKA после получения этого паттерна указывает на начало режима, LO->HI указывает на завершение. Этот режим используется для взаимодействия со световым пистолетом (Light GUN - Func. FT7):

RESET - аппаратный перезапуск устройства (14-ть клоков SDCKB пока SDCKA в низком уровне, только для DEVICE).

Теперь рассмотрим как по шине передаются данные.

Биты данных передаются фазами. В четной фазе линия данных - SDCKB, а клок - SDCKA, в нечетной наоборот (этот фрагмент тоже назовем паттерном :) ).

Величина таймаута на ответ от устройства после запроса хоста 1мс:

Помним, что например к джойстику можно подключать VMU, вибропак, микрофон...

Устройства подключаемые непосредственно к Maple BUS называются Device, а устройства подключаемые к Device называются Expansion Device, общение между Device и Expansion Device осуществляется средствами протокола LM-Bus. Expansion устройств можно подключить до 5-ти, хотя я не видел ни одного устройства в котором это было реализовано, а в чипах (например 315-6211-AB) "выведено наружу" только под 2-ва (хотя в программной части протокола под идентификацию EXP-DEV выделено пять бит, но тут честно говоря нужно уточнить, VMU например содержит память и LCD дисплей, это уже два Exp. устройства).

LM-BUS это что то типа суррогата Maple BUS, то есть шина на которую DEVICE напрямую переключает шину Maple BUS согласно тому какой Exp. DEVICE выбран HOST'ом.

LM-BUS тема отдельного разговора, отвлекаться не буду, перейдем к программной реализации протокола.

Программная часть протокола.

Как я уже писал выше данные передаются пакетами, рассмотрим пакет поближе:

  • COMMAND - команда, может принимать значения от 0x01 до 0xFE (см. возможные значения в коде ниже "maplebus.h").

maplebus commands
//HOST#defineDeviceRequest0x01#defineAllStatusRequest0x02#defineDeviceReset0x03#defineDeviceKill0x04#defineGetCondition0x09#defineGetMediaInfo0x0A#defineBlockRead0x0B#defineBlockWrite0x0C#defineGetLastError0x0D#defineSetCondition0x0E#defineFT4Control0x0F#defineARControl0x10#defineTransmitAgain0xFC//Device#defineDeviceStatus0x05#defineDeviceAllStatus0x06#defineDeviceReply0x07#defineDataTransfer0x08#defineARError0xF9#defineLCDError0xFA#defineFileError0xFB#defineTransmitAgain0xFC#defineCommandUnknown0xFD#defineFunctionTypeUnknown0xFE
  • DEST. AP - адрес назначения пакета (для какого устройства пакет).

  • ORIG. AP - от кого пакет.

Для AP справедлива следующая таблица:

PO[1:0] - Номер порта (A - 00, B - 01, C - 10, D - 11).

D/E - (1 - Device, 0 - Expansion Device или PORT).

LM[4:0] - (1 - Exp. DEVICE подключено, 0 - Слот Exp. пуст).

  • DATA SIZE - размер данных в пакете в 32-х битных чанках.

  • DATA - Состав пакета.

  • CRC - побайтный XOR всех данных включая COMMAND, AP, DATA SIZE, DATA.

"Общение" между HOST и DEVICE начинается с запроса DeviceRequest, в нем хост указывает какой порт он опрашивает, устройство, первый раз после включения или сброса "увидев" номер порта присваивает его себе (A/B/C/D).

Отвечать на данный запрос любое устройство обязано статусом (DeviceStatus answer):

Device ID - содержит функциональные возможности периферии (Device ID содержит блок FT, состав включенных битов в этом блоке определяет функции которые поддерживает устройство и FD - параметры поддерживаемых функций).

Device Functions
/*Device functions*/#define CONTROLLER    MAKE_DWORD(0x00000001)      //FT0 : Controller Function#define STORAGE            MAKE_DWORD(0x00000002)      //FT1 : Storage Function#define LCD                    MAKE_DWORD(0x00000004)      //FT2 : B/W LCD Function#define TIMER                MAKE_DWORD(0x00000008)      //FT3 : Timer Function#define AUDIO_INPUT    MAKE_DWORD(0x00000010)      //FT4 : Audio input device Function#define AR_GUN            MAKE_DWORD(0x00000020)      //FT5 : AR-Gun Function#define KEYBOARD        MAKE_DWORD(0x00000040)      //FT6 : Keyboard#define GUN                    MAKE_DWORD((unsigned int)0x00000080)        //FT7 : Light-Gun Function#define VIBRATION        MAKE_DWORD((unsigned int)0x00000100)        //FT8 : Vibration Function#define MOUSE                MAKE_DWORD((unsigned int)0x00000200)        //FT9 : Pointing Function#define EXMEDIA            MAKE_DWORD((unsigned int)0x00000400)        //FT10 : Exchange Media Function#define CAMERA            MAKE_DWORD((unsigned int)0x00000800)        //FT11 : Camera Device Functio

Destination code - указывает на целевой регион использования устройства.

Product name - название устройства (например {'D','r','e','a','m','c','a','s','t',' ','C','o','n','t','r','o','l','l','e','r', ' ',' ',' ',' ',' ',' ',' ',' ',' ',' '} - 30 байт).

License - кому принадлежит лицензия ( {'P','r','o','d','u','c','e','d',' ','B','y',' ','o','r',' ','U','n','d','e','r',' ','L','i','c','e','n','s','e',' ','F','r','o','m',' ','S','E','G','A',' ','E','N','T','E','R','P','R','I','S','E','S',',','L','T','D','.',' ',' ',' ',' ',' ',} -60 байт ).

Min./Max. current - соотв. минимальное и максимальное потребление устройства (1мА = 10 единиц, 43мА => 0x1AE).

Далее в пакете может идти "свободный статус устройства" (на изображении не указано, так как этот кусочек не обязателен), для джойстика он выглядит так: 40 байт "Version 1.000,1998/05/11,315-6125-AB Analog Module: The 4th Edition. 05/08".

То, какие команды применимы к устройству нам показывает блок Device ID.

К FT0, CONTROLLER, применима команда GetCondition - получить состояние кнопок/триггеров и аналоговых стиков геймпада. То в каких битах расположены какие значения указано всё в том же блоке Device ID. В частном случае, для геймпада Device ID будет выглядеть так:

В ответ на запрос GetCondition, джойстик обязан отправить отчет о состоянии кнопок, выглядит он следующим образом:

Ra/La/Da/Ua - Право/Лево/Вниз/Вверх (цифровой "крестик").

Start/A/B/X/Y - соотв кнопки.

A1, A2 - аналоговые курки

A3 и A4 - положение "стика".

Вот собственно и всё что нужно знать для реализации контроллера.

Реализация (аппаратная часть)

В общем то можно взять линии SDCKA и SDCKB и прикрутить на прерывания микроконтроллера и чисто программно реализовать, однако если МК медленный, то успевать не будет, и это не самое главное, во первых "плотность данных" для разных устройств разная и если например джойстик работать будет на одной программной реализации, то не факт что будет работать вибропак или карта памяти, во вторых в программной реализации определение ошибок внутри фрейма как и скажем команда "аппаратный сброс" не реализуемы, поэтому правильнее будет сделать аппаратный кусочек приемника, а отправлять и микроконтроллером можно.

Возьмем CPLD попроще (EPM3032) и реализуем xMAPLE:

SDCKA/SDCKB - вход линий Maple BUS.

GCLK - внешний CLK 16-48MHz.

INHTxD - сигнал блокировки работы приемника, 1 - игнорировать события на шине, 0 - нормальное функционирование.

RxD - идет прием пакета.

nSTRCV- начат прием пакета (Rising Edge).

nDLatch - Негативный импульс для "защелки данных" (сигнализирует о том что на линии данных Q[7..0] присутствует следующий полученный байт).

Q[7..0] - шина данных.

EOP - получен паттерн END (конец приема пакета).

FERR - обнаружена ошибка при приеме пакета.

nRST - подключается напрямую к микроконтроллеру - если получен RESET паттерн, - 0.

И общий вид:

Пишем это на верилог'е (3 файлика, надеюсь догадаетесь как это соединять):

SMAPLE.v
module SMAPLE(input GCLK,//MCU Generated 16MHz clock inputinput INHTxD,//Inhibit Input Data (User Can disable XMAPLE Detect Signals While MCU Transmit DATA)input SDCKAi,//Data/Clock A Lineinput SDCKBi,//Data/Clock B Lineoutput RxD,//Receive on progress (While receive is 1)output [7:0]Q// Output data bus (MCU can read valid data on this //port in time 200uS after data latch Negative Pulse received),output nSTRCV,//Receive start, negative pulse - Outputoutput OCPYi,//Occupancy packet received - Outputoutput nRST,//Reset packet received - Outputoutput FERR,//Frame error - Outputoutput EOPi,//End Of Packed received - Outputoutput nDLatch//New Data latched on BUS (Negative Pulse));/*Control Register*/reg rRxD = 0;assign RxD = rRxD;reg rFERR = 0;assign FERR = rFERR;wire nWE;assign nDLatch = (EOPi & nWE);wire iFERR;/* Align Data Packet */reg rENA = 1'b0;reg rENB = 1'b0;always @(posedge GCLK or negedge nRST) beginif(!nRST) beginrENA <= 1'b0;rENB <= 1'b0;end else beginrENA <= SDCKAi;rENB <= SDCKBi;endendalways @(posedge GCLK or negedge nRST) beginif(!nRST) beginrFERR <= 0;rRxD <= 0;end else beginif(!EOPi)// && !INHTxD) rRxD <=0 ;else beginif(!iFERR) rFERR <= 1;if(!nSTRCV) beginrFERR <= 0;rRxD <= ~INHTxD;endendendendline_monitor line_monitor(.GCLK(GCLK),//Global Clock - Input.SDCKA(SDCKAi|INHTxD),//CLOCK/DATA Line A disabled by data transmit - Input.SDCKB(SDCKBi|INHTxD),//CLOCK/DATA Line B disabled by data transmit - Input.RxDr(RxD),//Data Receive in progress - Input.RxD(nSTRCV),//Receive start, negative pulse - Output.OCPY(OCPYi),//Occupancy packet received - Output.RESET(nRST),//Reset packet received - Output.FERR(iFERR),//Frame error - Output.EOP(EOPi),//End Of Packed received - Output.ENA(rENA),//CLOCK For Line B.ENB(rENB)//CLOCK For Line A );/*Receive Maple Frame*/maple_receive maple_receive(.SDCKA(SDCKAi),//CLOCK/DATA Line A disabled by data transmit - Input.SDCKB(SDCKBi),//CLOCK/DATA Line B disabled by data transmit - Input.ENA(rENA),//CLOCK For Line B.ENB(rENB),//CLOCK For Line A .RCV(RxD),//Receive in progress, 1 - receive - Input.Dout(Q[7:0]), //Received data byte - Output.nWE(nWE),//Write Latch - Output.RxDi(nSTRCV),//Receive start, negative pulse - Input.INHTxD(INHTxD)//Inhibit Input Data (User Can disable XMAPLE Detect Signals While MCU Transmit DATA));endmodule
line_monitor.v
module line_monitor(input GCLK,input SDCKA,input SDCKB,input  RxDr,//Data Receive in progress - Inputoutput RxD,output OCPY,output RESET,output FERR,output EOP,input ENA,input ENB);reg [3:0] countA = 0;reg [2:0] countB = 0;reg [3:0] pcount = 0;reg rEOP = 1'b1;assign EOP = rEOP;assign RxD = (pcount == 4'h4) ? 1'b0 : 1'b1;assign OCPY = (pcount == 4'h8) ? 1'b0 : 1'b1;assign RESET = (pcount == 4'hE)? 1'b0 : 1'b1; //Output reset signal does not need to check for FERRassign FERR = (!((RxD & OCPY & RESET) && pcount[3:1])) | (!RxDr & !rEOP);//assign EOP = (eopcount == 3'h2) ? 1'b0 : 1'b1;always @(posedge SDCKA) pcount <= countA;always @(posedge SDCKB) rEOP <= !(countB == 3'h2);//Patterns//PATTERN Counter Managingalways @(posedge ENA or negedge ENB) beginif (ENA) begincountA <= 0;endelse begincountA <= countA + 4'h1;endend //EOP Counter Managingalways @(posedge ENB or negedge ENA) beginif (ENB) begin countB <= 0;endelse begin countB <= countB + 3'h1;endend //synopsys translate_off//synopsys translate_onendmodule
maple_receive.v
module maple_receive(input SDCKA,//CLOCK/DATA Line Ainput SDCKB,//CLOCK/DATA Line Binput ENA,//CLOCKinput ENB,//CLOCKinput RCV,//Receive in progress, 1 - validoutput [7:0]Dout, //received data outputoutput nWE,input RxDi,input INHTxD//Inhibit Input Data (User Can disable XMAPLE Detect Signals While MCU Transmit DATA));reg [3:0] dataA = 4'h0;reg [3:0] dataB = 4'h0;reg [1:0]countB = 2'b00;reg rLastBitCounted = 1'b1;//B LINE//Dout[1] = SDCKA Means Major version 1.//Dout[0] = SDCKB Means Minor version .0//And version result = 1.0assign Dout[1] = !INHTxD ? dataB[0] : SDCKA;assign Dout[3] = dataB[1];assign Dout[5] = dataB[2];assign Dout[7] = dataB[3];//A LINEassign Dout[0] = !INHTxD ? dataA[0] : SDCKB;assign Dout[2] = dataA[1];assign Dout[4] = dataA[2];assign Dout[6] = dataA[3];assign nWE = (dtaLock);always @(negedge ENA)begindataB[3:1] <= dataB[2:0];dataB[0] <= SDCKB;if(RCV) begincountB <= countB + 2'b1;end else begincountB <= 2'b11;endendalways @(negedge ENB)begindataA[3:1] <= dataA[2:0];dataA[0] <= SDCKA;rLastBitCounted <= !countB[0] | !countB[1];endwire dtaLock = rLastBitCounted;endmodule

Чтобы не "развлекаться с проводочками" накидал Eval Board.

Общий вид по блокам:

Полная схема модуля.

Gerber фалы.

Внешний вид:

И посадочное место под Eval...

Gerber файлы.

И соединяем всё это вместе:

Реализация устройства.

Железки есть, схемы есть, переходим к реализации.

Для начала заделаем небольшой код чтобы чтобы геймпад Dreamcast прикидывался геймпадом XBOX360 (поскольку я заботливо "выкусил хэндшейк" с XBOX360, данная реализация на приставке работать не будет только на ПК).

И, опять таки чтобы не на проводках, делаем плату коннектора для джойстика из двух половинок:

Верхняя часть (GERBER), нижняя часть (GERBER).

Разумеется чтобы получить хороший контакт с разъемом геймпада, нужно припаять "усы".

Для этой цели можно к примеру разобрать разъем SD вот как-то так:

Прикинем как должен работать алгоритм... не буду останавливаться на том как работает USB HID, опишу общую схему опроса устройств на шине MAPLE.

И собственно архив с исходниками.

Компилируем определив константы:

  • USE_STDPERIPH_DRIVER - использовать стандартную библиотеку периферии от ST.

  • STM32F10X_MD - чип Medium Density.

  • MAPLE_HOST - библиотека MAPLE работает в режиме HOST.

  • USB_HID - собрать целевое HID устройство.

Прошиваем, подключаемся:

и видим вот такую картину (не забываем что необходимо поставить Microsoft Xbox 360 Accessories, а ещё помним что геймпад у нас работает в режиме XInput... кому лень разбираться, можно скачать уже откомпилированную прошивку):

А теперь развернем xMAPLE в обратную сторону и...

Подключим мышь от ПК к DREAMCAST.

Мышь, - FT9 : Pointing Function.

Что нам нужно, DeviceID и состав команды GetCondition, чтоб собирать пакет с данными.

Mouse DeviceID:

Стандартная мышь Dreamcast содержит 3 кнопки: A,B,W, дельты смещения по осям X/Y: AC1,AC2 (ball) и смещение "колеса": AC3 (wheel).

AC1,AC2,AC3 - десяти-битные величины плюс флаг переполнения.

Вот так выглядит пакет данных:

AOV2, AOV1, AOV0 - флаги переполнения для AC3, AC2, AC1 соответственно.

Для удобства накидаем схемку адаптера PS/2 для нашей борды:

...разводим, получаем gerber'ы...

И с завода нам приезжает вот это:

Ну и чтобы совсем удобно, накидаем вот такую схему, если брать провод от оригинального пада, то просто подключаемся к разъему и УРА.

"Рисуем" gerber'ы и получаем вот такой переходник:

Собираем весь этот "огород" вместе:

Компилируем прошивку (ниже архив с исходниками) не забывая объявить константы препроцессора:

  • USE_STDPERIPH_DRIVER - использовать стандартную библиотеку периферии от ST.

  • STM32F10X_MD - чип Medium Density.

  • MAPLE_DEVICE - библиотека MAPLE работает в режиме DEVICE.

  • EN_MOUSE - собрать целевое HID устройство.

  • MOUSE_CALLBACK - обработать функцию чтения мыши в процессе ожидания запроса от HOST.

  • EXTI9_5_CALLBACK - передавать в код пользователя системные прерывания EXTI5-EXTI9 библиотеки MAPLE_BUS.

(исходники, скомпилированный HEX).

К слову, если вместо EN_MOUSE в данных исходниках определить константу EN_CONTROLLER, то мы получим довольно забавную штуку, переходник превращающий PS/2 мышь в контроллер DREAMCAST, собственно специально сделал, потому как мышью в меню DREAMCAST управлять нельзя. Поэтому чтобы наглядно увидеть работоспособность исходников и оборудования в целом не запуская скажем "HALF LIFE для проверки" можно прошить откомпилированный код с константой EN_CONTROLLER и управлять внутри меню мышкой PS/2.

Прошиваем, подключаемся к DREAMCAST и оно работает!!!

Вот собственно и всё что хотел поведать. Однако я не рассказал о (надеюсь ещё расскажу :) ):

  • Как работать с VibroPAK.

  • Как реализовать Memory Unit (хотя на борде расширения PS/2 SPI EEPROM память можно установить и работать с ней).

  • И у меня остались комплекты печатных плат и трём желающим "попробовать свои силы" могу отправить комплекты печатных плат за стоимость почты.

Удачного дня! Отличного настроения и взаимопонимания!!!

Подробнее..

Аппаратный эмулятор клавиатуры и мыши с интерфейсом USB

12.01.2021 20:07:52 | Автор: admin
Для чего нужен аппаратный эмулятор клавиатуры и мышки? Многим часто приходится выполнять рутинную работу и возникает мысль как-то автоматизировать этот процесс чтобы компьютер сам кликал в окнах и нажимал кнопки клавиатуры в то время как вы не спеша пьете кофе или занимаетесь другими делами. Не всегда для этой цели подходит программная эмуляция и в таких случаях необходим аппаратный эмулятор.

Что же из себя представляет аппаратный эмулятор клавиатуры и мыши? Обычно это небольшое устройство подключаемое к компьютеру через USB порт и которое он определяет как клавиатуру и мышь. Кроме того предусмотрен канал передачи управляющих команд (обычно через тот же USB порт) по которому устройству приходят команды нажать или отпустить кнопку клавиатуры или переместить мышь и покликать ее кнопками. Таким устройством могут быть различные микроконтроллеры имеющие в своем составе модуль связи по USB и прошитые специальной программой эмулятора клавиатуры и мыши.
Для этой цели хорошо подходит отладочная плата Blue Pill.



На ней есть все необходимое микроконтроллер STM32F103C8T6 с интерфейсом USB который выведен на разъем microUSB и вся необходимая обвязка для микроконтроллера. Нужно только прошить микроконтроллер программой эмулятора. Также для этой цели подойдет другие отладочные платы и устройства с микроконтроллером STM32F103C8T6 например отладчик ST-Link в форм-факторе флешки. Использование готовых компонентов позволяет сделать этот эмулятор почти каждому.

Возможности эмулятора зависят от прошивки и у предлагаемой они таковы:

  • Эмуляция расширенной клавиатуры содержащей 230 кнопок.
  • Одновременное нажатие до 14 кнопок клавиатуры не считая кнопок модификаторов.
  • Эмуляция дополнительной мультимедийной клавиатуры.
  • Эмуляция стандартной мыши с 8 кнопками, колесиком и возможностью позиционирования курсора в пределах координат 0 32000.
  • Скорость работы до 500 эмуляций в секунду (2 мс. на каждую эмуляцию).

Видео работы эмулятора. На нем вначале показан код передающий команды аппаратному эмулятору, а затем код выполнен и эмулятор напечатал 200 раз слово Emulator.


Инструкцию по прошивке эмулятора, примеры, документацию, прошивку и т. д. можно найти на сайте emulator.ucoz.org
Подробнее..

USB на регистрах interrupt endpoint на примере HID

10.04.2021 12:12:31 | Автор: admin

Еще более низкий уровень (avr-vusb)
USB на регистрах: STM32L1 / STM32F1
USB на регистрах: bulk endpoint на примере Mass Storage
USB на регистрах: isochronous endpoint на примере Audio device

Продолжаем разбираться с USB на контроллерах STM32L151. Как и в предыдущей части, ничего платформо-зависимого здесь не будет, зато будет USB-зависимое. Если точнее, будем рассматривать третий тип конечной точки interrupt. И делать мы это будем на примере составного устройства клавиатура + планшет (ссылка на исходники).
На всякий случай предупреждаю: данная статья (как и все остальные) скорее конспект того, что я понял, разбираясь в этой теме. Многие вещи так и остались магией и я буду благодарен если найдется специалист, способный объяснить их.

Первым делом напомню, что протокол HID (Human Interface Device) не предназначен для обмена большими массивами данных. Весь обмен строится на двух понятиях: событие и состояние. Событие это разовая посылка, возникающая в ответ на внешнее или внутреннее воздействие. Например, пользователь кнопочку нажал или мышь передвинул. Или на одной клавиатуре отключил NumLock, после чего хост вынужден и второй послать соответствующую команду, чтобы она это исправила, также послав сигнал нажатия NumLock и включила его обратно отобразила это на индикаторе. Для оповещения о событиях и используются interrupt точки. Состояние же это какая-то характеристика, которая не меняется просто так. Ну, скажем, температура. Или настройка уровня громкости. То есть что-то, посредством чего хост управляет поведением устройства. Необходимость в этом возникает редко, поэтому и взаимодействие самое примитивное через ep0.

Таким образом назначение у interrupt точки такое же как у прерывания в контроллере быстро сообщить о редком событии. Вот только USB штука хост-центричная, так что устройство не имеет права начинать передачу самостоятельно. Чтобы это обойти, разработчики USB придумали костыль: хост периодически посылает запросы на чтение всех interrupt точек. Периодичность запроса настраивается последним параметром в EndpointDescriptor'е (это часть ConfigurationDescriptor'а). В прошлых частях мы уже видели там поле bInterval, но его значение игнорировалось. Теперь ему наконец-то нашлось применение. Значение имеет размер 1 байт и задается в миллисекундах, так что опрашивать нас будут с интервалом от 1 мс до 2,55 секунд. Для низкоскоростных устройств минимальный интервал составляет 10 мс. Наличие костыля с опросом interrupt точек для нас означает, что даже в отсутствие обмена они будут впустую тратить полосу пропускания шины.

Логичный вывод: interrupt точки предназначены только для IN транзакций. В частности, они используются для передачи событий от клавиатуры или мыши, для оповещения об изменении служебных линий COM-порта, для синхронизации аудиопотока и тому подобных вещей. Но для всего этого придется добавлять другие типы точек. Поэтому, чтобы не усложнять пример, ограничимся реализацией HID-устройства. Вообще-то, такое устройство мы уже делали в первой части, но там дополнительные точки не использовались вовсе, да и структура HID-протокола рассмотрена не была.

ConfigurationDescriptor


static const uint8_t USB_ConfigDescriptor[] = {  ARRLEN34(  ARRLEN1(    bLENGTH, // bLength: Configuration Descriptor size    USB_DESCR_CONFIG,    //bDescriptorType: Configuration    wTOTALLENGTH, //wTotalLength    1, // bNumInterfaces    1, // bConfigurationValue: Configuration value    0, // iConfiguration: Index of string descriptor describing the configuration    0x80, // bmAttributes: bus powered    0x32, // MaxPower 100 mA  )  ARRLEN1(    bLENGTH, //bLength    USB_DESCR_INTERFACE, //bDescriptorType    0, //bInterfaceNumber    0, // bAlternateSetting    2, // bNumEndpoints    HIDCLASS_HID, // bInterfaceClass:     HIDSUBCLASS_BOOT, // bInterfaceSubClass:     HIDPROTOCOL_KEYBOARD, // bInterfaceProtocol:     0x00, // iInterface  )  ARRLEN1(    bLENGTH, //bLength    USB_DESCR_HID, //bDescriptorType    USB_U16(0x0110), //bcdHID    0, //bCountryCode    1, //bNumDescriptors    USB_DESCR_HID_REPORT, //bDescriptorType    USB_U16( sizeof(USB_HIDDescriptor) ), //wDescriptorLength  )  ARRLEN1(    bLENGTH, //bLength    USB_DESCR_ENDPOINT, //bDescriptorType    INTR_NUM, //bEdnpointAddress    USB_ENDP_INTR, //bmAttributes    USB_U16( INTR_SIZE ), //MaxPacketSize    10, //bInterval  )  ARRLEN1(    bLENGTH, //bLength    USB_DESCR_ENDPOINT, //bDescriptorType    INTR_NUM | 0x80, //bEdnpointAddress    USB_ENDP_INTR, //bmAttributes    USB_U16( INTR_SIZE ), //MaxPacketSize    10, //bInterval  )  )};


Внимательный читатель тут же может обратить внимание на описания конечных точек. Со второй все в порядке IN точка (раз произведено сложение с 0x80) типа interrupt, заданы размер и интервал. А вот первая вроде бы объявлена как OUT, но в то же время interrupt, что противоречит сказанному ранее. Да и здравому смыслу тоже: хост не нуждается в костылях чтобы передать в устройство что угодно и когда угодно. Но таким способом обходятся другие грабли: тип конечной точки в STM32 устанавливается не для одной точки, а только для пары IN/OUT, так что не получится задать 0x81-й точке тип interrupt, а 0x01-й control. Впрочем, для хоста это проблемой не является, он бы, наверное, и в bulk точку те же данные посылал что, впрочем, я проверять не стану.

HID descriptor


Структура HID descriptor'а больше всего похожа на конфигурационных файл имя=значение, но в отличие от него, имя представляет собой числовую константу из списка USB-специфичных, а значение либо тоже константу, либо переменную размером от 0 до 3 байт.
Важно: для некоторых имен длина значения задается в 2 младших битах поля имени. Например, возьмем LOGICAL_MINIMUM (минимальное значение, которое данная переменная может принимать в штатном режиме). Код этой константы равен 0x14. Соответственно, если значения нет (вроде бы такого не бывает, но утверждать не буду зачем-то же этот случай ввели), то в дескрипторе будет единственное число 0x14. Если значение равно 1 (один байт) то записано будет 0x15, 0x01. Для двухбайтного значения 0x1234 будет записано 0x16, 0x34, 0x12 значение записывается от младшего к старшему. Ну и до кучи число 0x123456 будет 0x17, 0x56, 0x34, 0x12.

Естественно, запоминать все эти числовые константы мне лень, поэтому воспользуемся макросами. К сожалению, я так и не нашел способа заставить их самостоятельно определять размер переданного значения и разворачиваться в 1, 2, 3 или 4 байта. Поэтому пришлось сделать костыль: макрос без суффикса отвечает за самые распространенные 8-битные значения, с суффиксом 16 за 16-битные, а с 24 за 24-битные. Также были написаны макросы для составных значений вроде диапазона LOGICAL_MINMAX24(min, max), которые разворачиваются в 4, 6 или 8 байтов.

Как и в конфигурационных файлах, здесь присутствуют секции, называемые страницами (usage_page), которые группируют устройства по назначению. Скажем, есть страница с базовой периферией вроде клавиатур, мышек и просто кнопок, есть джойстики и геймпады (искренне рекомендую посмотреть какие именно! Там и для танков, и для космических кораблей, и для подводных лодок и для всего чего угодно), даже дисплеи есть. Правда, где искать софт, умеющий со всем этим работать, я без понятия.

Внутри каждой страницы выбирается конкретное устройство. Например, для мышки это указатель и кнопки, а для планшета стилус или палец юзера (что?!). Ими же обозначаются составные части устройства. Так, частью указателя являются его координаты по X и Y. Некоторые характеристики можно сгруппировать в коллекцию, но для чего это делается я толком не понял. В документации к полям иногда ставится пометка из пары букв о назначении поля и способе работы с ним:

CA Collection(application) Служебная информация, никакой переменной не соответствующая
CL Collection(logical) -/-
CP Collection(phisical) -/-
DV Dynamic Value входное или выходное значение (переменная)
MC Momentary Control флаг состояния (1-флаг взведен, 0-сброшен)
OSC One Shot Control однократное событие. Обрабатывается только переход 0->1


Есть, разумеется, и другие, но в моем примере они не используются. Если, например, поле X помечено как DV, то оно считается переменной ненулевой длины и будет включено в структуру репорта. Поля MC или OSC также включаются в репорт, но имеют размер 1 бит.

Один репорт (пакет данных, посылаемый или принимаемый устройством) содержит значения всех описанных в нем переменных. Описание кнопки говорит о всего одном занимаемом бите, но для относительных координат (насколько передвинулась мышка, например) требуется как минимум байт, а для абсолютных (как для тачскрина) уже нужно минимум 2 байта. Плюс к этому, многие элементы управления имеют еще свои физические ограничения. Например, АЦП того же тачскрина может иметь разрешение всего 10 бит, то есть выдавать значения от 0 до 1023, которое хосту придется масштабировать к полному разрешению экрана. Поэтому в дескрипторе помимо предназначения каждого поля задается еще диапазон его допустимых значений (LOGICAL_MINMAX), плюс иногда диапазон физических значений (в миллиматрах там, или в градусах) и обязательно представление в репорте. Представление задается двумя числами: размер одной переменной (а битах) и их количество. Например, координаты касания тачскрина в создаваемом нами устройстве задаются так:
USAGE( USAGE_X ), // 0x09, 0x30,USAGE( USAGE_Y ), // 0x09, 0x31,LOGICAL_MINMAX16( 0, 10000 ), //0x16, 0x00, 0x00,   0x26, 0x10, 0x27,REPORT_FMT( 16, 2 ), // 0x75, 0x10, 0x95, 0x02,INPUT_HID( HID_VAR | HID_ABS | HID_DATA), // 0x91, 0x02,

Здесь видно, что объявлены две переменные, изменяющиеся в диапазоне от 0 до 10000 и занимающие в репорте два участка по 16 бит.

Последнее поле говорит, что вышеописанные переменные будут хостом читаться (IN) и поясняется как именно. Описывать его флаги подробно я не буду, остановлюсь только на нескольких. Флаг HID_ABS показывает, что значение абсолютное, то есть никакая предыстория на него не влияет. Альтернативное ему значение HID_REL показывает что значение является смещением относительно предыдущего. Флаг HID_VAR говорит, что каждое поле отвечает за свою переменную. Альтернативное значение HID_ARR говорит, что передаваться будут не состояния всех кнопок из списка, а только номера активных. Этот флаг применим только к однобитным полям. Вместо того, чтобы передавать 101/102 состояния всех кнопок клавиатуры можно ограничиться несколькими байтами со списком нажатых клавиш. Тогда первый параметр REPORT_FMT будет отвечать за размер номера, а второй за количество.

Поскольку размер всех переменных задается в битах, логично спросить: а что же с кнопками, ведь их количество может быть не кратно 8, а это приведет к трудностям выравнивания при чтении и записи. Можно было бы выделить каждой кнопке по байту, но тогда бы сильно вырос объем репорта, что для скоростных передач вроде interrupt, неприятно. Вместо этого кнопки стараются расположить поближе друг к другу, а оставшееся место заполняют битами с флагом HID_CONST.

Теперь мы можем если не написать дескриптор с нуля, то хотя бы попытаться его читать, то есть определить, каким битам соответствует то или иное поле. Достаточно посчитать INPUT_HID'ы и соответствующие им REPORT_FMT'ы. Только учтите, что именно такие макросы придумал я, больше их никто не использует. В чужих дескрипторах придется искать input, report_size, report_count, а то и вовсе числовые константы.

Вот теперь можно привести дескриптор целиком:
static const uint8_t USB_HIDDescriptor[] = {  //keyboard  USAGE_PAGE( USAGEPAGE_GENERIC ),//0x05, 0x01,  USAGE( USAGE_KEYBOARD ), // 0x09, 0x06,  COLLECTION( COLL_APPLICATION, // 0xA1, 0x01,    REPORT_ID( 1 ), // 0x85, 0x01,    USAGE_PAGE( USAGEPAGE_KEYBOARD ), // 0x05, 0x07,    USAGE_MINMAX(224, 231), //0x19, 0xE0, 0x29, 0xE7,        LOGICAL_MINMAX(0, 1), //0x15, 0x00, 0x25, 0x01,    REPORT_FMT(1, 8), //0x75, 0x01, 0x95, 0x08         INPUT_HID( HID_DATA | HID_VAR | HID_ABS ), // 0x81, 0x02,     //reserved    REPORT_FMT(8, 1), // 0x75, 0x08, 0x95, 0x01,    INPUT_HID(HID_CONST), // 0x81, 0x01,                  REPORT_FMT(1, 5),  // 0x75, 0x01, 0x95, 0x05,    USAGE_PAGE( USAGEPAGE_LEDS ), // 0x05, 0x08,    USAGE_MINMAX(1, 5), //0x19, 0x01, 0x29, 0x05,      OUTPUT_HID( HID_DATA | HID_VAR | HID_ABS ), // 0x91, 0x02,    //выравнивание до 1 байта    REPORT_FMT(3, 1), // 0x75, 0x03, 0x95, 0x01,    OUTPUT_HID( HID_CONST ), // 0x91, 0x01,    REPORT_FMT(8, 6),  // 0x75, 0x08, 0x95, 0x06,    LOGICAL_MINMAX(0, 101), // 0x15, 0x00, 0x25, 0x65,             USAGE_PAGE( USAGEPAGE_KEYBOARD ), // 0x05, 0x07,    USAGE_MINMAX(0, 101), // 0x19, 0x00, 0x29, 0x65,    INPUT_HID( HID_DATA | HID_ARR ), // 0x81, 0x00,             )  //touchscreen  USAGE_PAGE( USAGEPAGE_DIGITIZER ), // 0x05, 0x0D,  USAGE( USAGE_PEN ), // 0x09, 0x02,  COLLECTION( COLL_APPLICATION, // 0xA1, 0x0x01,    REPORT_ID( 2 ), //0x85, 0x02,    USAGE( USAGE_FINGER ), // 0x09, 0x22,    COLLECTION( COLL_PHISICAL, // 0xA1, 0x00,      USAGE( USAGE_TOUCH ), // 0x09, 0x42,      USAGE( USAGE_IN_RANGE ), // 0x09, 0x32,      LOGICAL_MINMAX( 0, 1), // 0x15, 0x00, 0x25, 0x01,      REPORT_FMT( 1, 2 ), // 0x75, 0x01, 0x95, 0x02,      INPUT_HID( HID_VAR | HID_DATA | HID_ABS ), // 0x91, 0x02,      REPORT_FMT( 1, 6 ), // 0x75, 0x01, 0x95, 0x06,      INPUT_HID( HID_CONST ), // 0x81, 0x01,                      USAGE_PAGE( USAGEPAGE_GENERIC ), //0x05, 0x01,      USAGE( USAGE_POINTER ), // 0x09, 0x01,      COLLECTION( COLL_PHISICAL, // 0xA1, 0x00,                 USAGE( USAGE_X ), // 0x09, 0x30,        USAGE( USAGE_Y ), // 0x09, 0x31,        LOGICAL_MINMAX16( 0, 10000 ), //0x16, 0x00, 0x00, 0x26, 0x10, 0x27,        REPORT_FMT( 16, 2 ), // 0x75, 0x10, 0x95, 0x02,        INPUT_HID( HID_VAR | HID_ABS | HID_DATA), // 0x91, 0x02,      )    )  )};

Помимо рассмотренных ранее полей тут есть и такое интересное поле, как REPORT_ID. Поскольку, как понятно из комментариев, устройство у нас составное, хосту надо как-то определить, чьи данные он принимает. Для этого данное поле и нужно.

И еще одно поле, на которое хотелось бы обратить внимание OUTPUT_HID. Как видно из названия, оно отвечает не за прием репорта (IN), а за передачу (OUT). Расположено оно в разделе клавиатуры и описывает индикаторы CapsLock, NumLock, ScrollLock а также два экзотических Compose (флаг ввода некоторых символов, для которых нет собственных кнопок вроде , или ) и Kana (ввод иероглифов). Собственно, ради этого поля мы и заводили OUT точку. В ее обработчике будем проверять не надо ли зажечь индикаторы CapsLock и NumLock: на плате как раз два диодика и разведено.

Существует и третье поле, связанное с обменом данными FEATURE_HID, мы его использовали в первом примере. Если INPUT и OUTPUT предназначены для передачи событий, то FEATURE состояния, которое можно как читать, так и писать. Правда, делается это не через выделенные endpoint'ы, а через обычную ep0 путем соответствующих запросов.

Если внимательно рассмотреть дескриптор, можно восстановить структуру репорта. Точнее, двух репортов:
struct{  uint8_t report_id; //1  union{    uint8_t modifiers;    struct{      uint8_t lctrl:1; //left control      uint8_t lshift:1;//left shift      uint8_t lalt:1;  //left alt      uint8_t lgui:1;  //left gui. Он же hyper, он же winkey      uint8_t rctrl:1; //right control      uint8_t rshift:1;//right shift      uint8_t ralt:1;  //right alt      uint8_t rgui:1;  //right gui    };  };  uint8_t reserved; //я не знаю зачем в официальной документации это поле  uint8_t keys[6]; //список номеров нажатых клавиш}__attribute__((packed)) report_kbd;struct{  uint8_t report_id; //2  union{    uint8_t buttons;    struct{      uint8_t touch:1;   //фактнажатия на тачскрин      uint8_t inrange:1; //нажатие в рабочей области      uint8_t reserved:6;//выравнивание до 1 байта    };  };  uint16_t x;  uint16_t y;}__attribute__((packed)) report_tablet;


Отправлять их будем по нажатию кнопок на плате, причем. поскольку пишем мы всего лишь пример реализации, а не законченное устройство, делать это будем по-варварски посылая два репорта, в первом из которых нажимая клавиши, а во втором отпуская. Причем с огромной тупой задержкой между посылками. Если не посылать репорт с отпущенными клавишами, система посчитает что клавиша осталась нажатой и будет ее повторять. Естественно, ни о какой эффективности тут не идет и речи, о безопасности тоже, но для теста сойдет. Ах да, куда ж без очередных граблей! Размер структуры должен совпадать с тем, что описано в дескрипторе, иначе винда сделает вид, что не понимает чего от нее хотят. Как обычно, линукс подобные ошибки игнорирует и работает как ни в чем не бывало.

В процессе тестирования наткнулся на забавный побочный эффект: в Windows7 при нажатии на тачскрин вылезает окошко рукописного ввода. Я об этой фиче не знал.

Если к вам попало готовое устройство


и хочется посмотреть на него изнутри. Первым делом, естественно, смотрим, можно даже от обычного пользователя, ConfigurationDescriptor:
lsusb -v -d <VID:PID>

Для HID-дескриптора же я не нашел (да и не искал) способа лучше, чем от рута:
cat /sys/kernel/debug/hid/<address>/rdes

Для полноты картины сюда стоило бы добавить как смотреть подобные вещи в других ОС. Но у меня соответствующих знаний нет, может в комментариях подскажут. Желательно, конечно, без установки стороннего софта.

Заключение


Вот, собственно, и все, что я нарыл по HID. План-минимум научиться читать готовые дескрипторы, эмулировать несколько устройств одновременно и реализовать планшетный ввод выполнен. Ну и философию interrupt точек рассмотрели заодно.

Как и в плошлый раз, немножко документации оставил в репозитории на случай если дизайнеры USB-IF снова решат испортить сайт.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru