Doom на дисковом телефоне Dialrhea

от автора

Dialrhea — это модифицированный дисковый телефон, переделанный для управления Doom»ом через Bluetooth. Мы собрали его за два дня во время хакатона «Internet Of Shit», организованного командой Technarium в Вильнюсе, Литва. Темой этого хакатона было создание устройств, которые совершенно бесполезны, но полностью функциональны.

Dialrhea в действии:

Мы даже сняли для него рекламный ролик: 

Хотелось бы ещё провести мастер‑класс, на котором мы собрали бы ещё как минимум три устройства, а затем сыграли бы на них deathmatch в Doom.

Предыстория

Устройство было создано во время хакатона «Internet Of Shit». Это мероприятие было организовано командой Technarium в Вильнюсе, Литва, в 2017 и 2018 годах. В число номинаций вошли такие, как «Если загорелось, значит работает», «Наименее приватный онлайн‑гаджет», «Это, скорее всего, незаконно», и команды упорно соревновались, чтобы превратить эти концепции в работающую технологию.

Наше устройствополучило награду «Наименее дерьмовый проект», а также нам вручили приз за самый популярный проект. Призом, кстати, был насос для прочистки канализации, покрытый золотой краской, хе‑хе.

Идея родилась во время пивных посиделок с Донатасом Валиулисом и Джюгасом Барткусомсом. Мы же втроём всё и реализовали. Я и Донатас позаботились о технических аспектах, а Джюгас отвечал за видео и рекламные материалы.

Джюгас также снял забавное психоделическое видео о создании устройства и обо всем хакатоне в целом.

Технические детали

Устройство построено на Arduino, а для беспроводной связи с компьютером использует Bluetooth LE. Оно представляет собой клавиатуру Bluetooth и может быть подключено к любому устройству, поддерживающему Bluetooth LE. Ниже вы можете увидеть разбивку компонентов, питающих Dialrhea.

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

  • Doom — в этом режиме устройство действует как игровой контроллер и настроено на управление классической игрой Doom (используя Doomsday Engine)

  • Эмодзи — этот режим лучше всего использовать на мобильных телефонах, он позволяет вводить эмодзи и отправлять их друзьям.

  • Скукота — в этом режиме Dialrhea просто выводит набранные номера (не рекомендуем к использованию)

Считывание данных с поворотного диска

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

После того, как я понял, как работает этот механизм, реализовать его с помощью Arduino оказалось довольно просто.

Борьба с Bluetooth

Вероятно,самой большой проблемой было заставить Bluetooth работать должным образом. Мы решили использовать модуль Adafruit Bluefruit LE UART Friend, потому что он у меня был, и я уже пробовал работать с ним в другом проекте. Это очень эффективный модуль, но большинство проблем, с которыми мы столкнулись, были связаны со стабильностью и надёжностью. Иногда все работало хорошо, иногда мы получали некоторые ошибки при запуске одного и того же кода.

Мы прочитали много документации о протоколе рукопожатий, о том, как правильно выполнять сопряжение и т. д., но в итоге просто везде добавили циклы повторов и тайм‑ауты, чтобы у чипа было время «прийти в себя» после каждой рискованной операции. Ниже вы можете увидеть полный исходный код Dialrhea.

#include <Arduino.h> #include <SPI.h> #if not defined (_VARIANT_ARDUINO_DUE_X_) && not defined(ARDUINO_ARCH_SAMD)   #include <SoftwareSerial.h> #endif #include "Adafruit_BLE.h" #include "Adafruit_BluefruitLE_UART.h" #include "BluefruitConfig.h"  #define DEVICE_NAME "Dialrhea"  // Rotary dial input PIN #define ROTARY_PIN 2 // Handset input PIN #define HANDSET_PIN 3 // Operation mode potentiometer PIN #define OPERATION_MODE_PIN A5  // How long to wait before sending keyup message for control keys in Gaming mode #define CONTROL_KEY_HOLD_DURATION 200 // How long to wait before sending keyup message for fire button in Gaming mode #define FIRE_KEY_HOLD_DURATION 200 // How long to wait before sending keyup message for keys that are supposed // to be just one clicks #define INSTANT_KEY_HOLD_DURATION 10  // Pins for status LED RGB legs #define STATUS_LED_RED_PIN 4 #define STATUS_LED_GREEN_PIN 6 #define STATUS_LED_BLUE_PIN 5  // Constants for colors #define COLOR_OFF 0 #define COLOR_RED 1 #define COLOR_GREEN 2 #define COLOR_BLUE 3  // Total number of keys that support timed presses #define KEY_COUNT 14  // Index of handset button data in data arrays (Gaming mode, we need two because //we are sending keys for both fire and open door) #define KEY_GAMING_MODE_HANDSET_1_INDEX 10 #define KEY_GAMING_MODE_HANDSET_2_INDEX 11 // Index of handset button data in data arrays (Emoji mode) #define KEY_EMOJI_MODE_HANDSET_INDEX 12 // Index of handset button data in data arrays (Boring mode) #define KEY_BORING_MODE_HANDSET_INDEX 13  // Map of values for each type of dialed number and handset click const int keyValues[KEY_COUNT] = {   0x42, // Number 0 in gaming mode (Currently quick load)   0x52, // Number 1 in gaming mode (Currently "up" arrow)   0x4F, // Number 2 in gaming mode (Currently "right" arrow)   0x50, // Number 3 in gaming mode (Currently "left" arrow)   0x51, // Number 4 in gaming mode (Currently "down" arrow)   0x2A, // Number 5 in gaming mode (Currently ?, next weapon)   0x00, // Number 6 in gaming mode   0x00, // Number 7 in gaming mode   0x00, // Number 8 in gaming mode   0x00, // Number 9 in gaming mode   0x10, // Handset click in gaming mode (KEY_GAMING_MODE_HANDSET_1_INDEX) (Currently space)   0x2C, // Handset click in gaming mode (KEY_GAMING_MODE_HANDSET_2_INDEX) (Currently 'm')   0x28, // Handset click in emoji mode (KEY_EMOJI_MODE_HANDSET_INDEX) (Currently Enter)   0x29  // Handset click in boring mode (KEY_BORING_MODE_HANDSET_INDEX) (Currently Esc) };  // Durations for each type of mey (mapping the same as for keyValues array) const int keyHoldDurations[KEY_COUNT] = {   INSTANT_KEY_HOLD_DURATION,    CONTROL_KEY_HOLD_DURATION,    CONTROL_KEY_HOLD_DURATION,   CONTROL_KEY_HOLD_DURATION,   CONTROL_KEY_HOLD_DURATION,   INSTANT_KEY_HOLD_DURATION,   INSTANT_KEY_HOLD_DURATION,   INSTANT_KEY_HOLD_DURATION,   INSTANT_KEY_HOLD_DURATION,   INSTANT_KEY_HOLD_DURATION,   FIRE_KEY_HOLD_DURATION,   FIRE_KEY_HOLD_DURATION,   INSTANT_KEY_HOLD_DURATION,   INSTANT_KEY_HOLD_DURATION };  // Array for storing times each key was pressed unsigned long keyPressTimes[KEY_COUNT]; // Array for storing states for each key bool keyPressStates[KEY_COUNT];  // Variables required for handling input from rotary dial int rotaryHasFinishedRotatingTimeout = 100; int rotaryDebounceTimeout = 10; int rotaryLastValue = LOW; int rotaryTrueValue = LOW; unsigned long rotaryLastValueChangeTime = 0; bool rotaryNeedToEmitEvent = 0; int rotaryPulseCount;  // Operation modes #define OPERATION_MODE_GAMING 0 // Gaming controls fine tuned for the best game of all times: "Doom" #define OPERATION_MODE_EMOJI 1 // Emojis + Enter #define OPERATION_MODE_BORING 2 // Numbers + Esc  // Current operation mode int operationMode;  // Emojis for each dialed number const char* emojis[] = {":-O", ":poop:",  ":-)", ":-(", ":-D", ":-\\", ";-)", ":-*", ":-P", ">:-("};  // Variables for handling handset clicker button bool isHandsetPressed = false; unsigned long handsetPressStartTime = 0; unsigned long handsetPressStartTimeout = 60;  // Variable that determines weather the state of keys changed during processing of the loop (so we // can send commands just once in the end of the loop if it is needed) bool keyPressStateChanged;  // Config settings for Bluetooth LE module #define FACTORYRESET_ENABLE         0 #define VERBOSE_MODE                false  // If set to 'true' enables debug output #define MINIMUM_FIRMWARE_VERSION    "0.6.6" #define BLUEFRUIT_HWSERIAL_NAME      Serial1  // Bluetooth LE module object Adafruit_BluefruitLE_UART ble(BLUEFRUIT_HWSERIAL_NAME, BLUEFRUIT_UART_MODE_PIN);  void setup(void) {   pinMode(ROTARY_PIN, INPUT);   pinMode(HANDSET_PIN, INPUT_PULLUP);   pinMode(STATUS_LED_RED_PIN, OUTPUT);   pinMode(STATUS_LED_GREEN_PIN, OUTPUT);   pinMode(STATUS_LED_BLUE_PIN, OUTPUT);    setStatusLEDColor(COLOR_GREEN);    // Wait while serial connection is established (required for Flora & Micro or when you want to   // halt initialization till you open serial monitor)   // while (!Serial);      // Give some time for chip to warm up or whatever   delay(1000);    initializeSerialConnection();   initializeBLEModule();    // Delay a bit because good devices always take some time to start   delay(100);    setStatusLEDColor(COLOR_BLUE); }  void loop(void) {   keyPressStateChanged = false;   refreshOperationMode();   handleHandset();   handleRotary();   processKeyUps();    // If state of pressed keys changed - send the new state   if (keyPressStateChanged)     sendCurrentlyPressedKeys(); }  // Sets the color of status LED void setStatusLEDColor(int colorID) {   digitalWrite(STATUS_LED_RED_PIN, colorID == COLOR_RED ? HIGH : LOW);   digitalWrite(STATUS_LED_GREEN_PIN, colorID == COLOR_GREEN ? HIGH : LOW);   digitalWrite(STATUS_LED_BLUE_PIN, colorID == COLOR_BLUE ? HIGH : LOW); }  // Outputs error message and bricks the revolutionary shitty machine void error(const __FlashStringHelper*err) {    setStatusLEDColor(COLOR_RED);      Serial.println(err);   while (1); }  // Blinks the status LED (only green supported for now) void blink() {   setStatusLEDColor(COLOR_OFF);   delay(100);   setStatusLEDColor(COLOR_GREEN); }  // Opens serial connection for debugging void initializeSerialConnection() {   Serial.begin(9600);   Serial.println(F("Hello, I am the Dialrhea! Ready for some dialing action?"));   Serial.println(F("8-------------------------------------D")); }  // Initializes Bluetooth LE module void initializeBLEModule() {   // Buffer for holding commands that have to be sent to BLE module   char commandString[64];    setStatusLEDColor(COLOR_GREEN);    Serial.print(F("Initialising the Bluefruit LE module: "));   if (!ble.begin(VERBOSE_MODE)) error(F("Couldn't find Bluefruit, make sure it's in CoMmanD mode & check wiring?"));   Serial.println( F("Easy!") );    blink();    if (FACTORYRESET_ENABLE)   {     Serial.println(F("Performing a factory reset: "));     if (!ble.factoryReset()) error(F("Couldn't factory reset. Have no idea why..."));     Serial.println(F("Done, feeling like a virgin again!"));   }    blink();    // Disable command echo from Bluefruit   ble.echo(false);    blink();    Serial.println("Requesting Bluefruit info:");   ble.info();    blink();    // Change the device name so the whole world knows it as Dialrhea   Serial.print(F("Setting device name to '"));   Serial.print(DEVICE_NAME);   Serial.print(F("': "));   sprintf(commandString, "AT+GAPDEVNAME=%s", DEVICE_NAME);   if (!ble.sendCommandCheckOK(commandString)) error(F("Could not set device name for some reason. Sad."));   Serial.println(F("It's beautiful!"));    blink();    Serial.print(F("Enable HID Service (including Keyboard): "));   strcpy(commandString, ble.isVersionAtLeast(MINIMUM_FIRMWARE_VERSION) ? "AT+BleHIDEn=On" : "AT+BleKeyboardEn=On");   if (!ble.sendCommandCheckOK(commandString)) error(F("Could not enable Keyboard, we're in deep shit..."));   Serial.println(F("I'm now officially a keyboard!"));    blink();    // Make software reset (add or remove service requires a reset)   Serial.print(F("Performing a SW reset (service changes require a reset): "));   if (!ble.reset()) error(F("Couldn't reset?? Lame."));   Serial.println(F("Baby I'm ready to go!"));   Serial.println(); }  // Reads the position of operation mode select potentiometer and determines current operation mode void refreshOperationMode() {   operationMode = floor((float)analogRead(OPERATION_MODE_PIN) / 342.0); }  // Handles tracking the handset state void handleHandset() {   // Ignore input until last action timeout passes (to filter out noise)   if (millis() - handsetPressStartTime > handsetPressStartTimeout) {     int ragelisCurrentValue = digitalRead(HANDSET_PIN);          if (!isHandsetPressed && ragelisCurrentValue == HIGH) {       isHandsetPressed = true;       handsetPressStartTime = millis();       onHandsetClicked();     }      else if (isHandsetPressed && ragelisCurrentValue == LOW) {       isHandsetPressed = false;       handsetPressStartTime = millis();      }   } }  // Handles tracking of the rotary dial state void handleRotary() {   int rotaryCurrentValue = digitalRead(ROTARY_PIN);    // If rotary isn't being dialed or it just finished being dialed   if ((millis() - rotaryLastValueChangeTime) > rotaryHasFinishedRotatingTimeout) {     // If rotary just finished being dialed - we need to emit the event     if (rotaryNeedToEmitEvent) {       // Emit the event (we mod the count by 10 because '0' will send 10 pulses)       onRotaryNumberDialed(rotaryPulseCount % 10);       rotaryNeedToEmitEvent = false;       rotaryPulseCount = 0;     }   }    // If rotary value has changed - register the time when it happened   if (rotaryCurrentValue != rotaryLastValue) {     rotaryLastValueChangeTime = millis();   }    // Start analyzing data only when signal stabilizes (debounce timeout passes)   if ((millis() - rotaryLastValueChangeTime) > rotaryDebounceTimeout) {     // This means that the switch has either just gone from closed to open or vice versa.     if (rotaryCurrentValue != rotaryTrueValue) {       // Register actual value change        rotaryTrueValue = rotaryCurrentValue;        // If it went to HIGH - increase pulse count       if (rotaryTrueValue == HIGH) {         rotaryPulseCount++;          rotaryNeedToEmitEvent = true;       }      }   }    // Store current value as last value   rotaryLastValue = rotaryCurrentValue; }  // Event handler triggered when click of the handset button is registered void onHandsetClicked() {   // Register state changes for handset button keys depending on the mode   if (operationMode == OPERATION_MODE_GAMING) {     if (keyPressStates[KEY_GAMING_MODE_HANDSET_1_INDEX] == false || keyPressStates[KEY_GAMING_MODE_HANDSET_2_INDEX] == false)       keyPressStateChanged = true;        keyPressStates[KEY_GAMING_MODE_HANDSET_1_INDEX] = true;     keyPressTimes[KEY_GAMING_MODE_HANDSET_1_INDEX] = millis();        keyPressStates[KEY_GAMING_MODE_HANDSET_2_INDEX] = true;     keyPressTimes[KEY_GAMING_MODE_HANDSET_2_INDEX] = millis();   } else if (operationMode == OPERATION_MODE_EMOJI) {     if (keyPressStates[KEY_EMOJI_MODE_HANDSET_INDEX] == false)       keyPressStateChanged = true;        keyPressStates[KEY_EMOJI_MODE_HANDSET_INDEX] = true;     keyPressTimes[KEY_EMOJI_MODE_HANDSET_INDEX] = millis();   } else if (operationMode == OPERATION_MODE_BORING) {     if (keyPressStates[KEY_BORING_MODE_HANDSET_INDEX] == false)       keyPressStateChanged = true;        keyPressStates[KEY_BORING_MODE_HANDSET_INDEX] = true;     keyPressTimes[KEY_BORING_MODE_HANDSET_INDEX] = millis();   } }  // Event handler triggered when number was dialed on rotary dial void onRotaryNumberDialed(int number) {   if (operationMode == OPERATION_MODE_GAMING) {     // Set key state for dialed key     if (keyPressStates[number] == false)       keyPressStateChanged = true;        keyPressStates[number] = true;     keyPressTimes[number] = millis();   } else if (operationMode == OPERATION_MODE_EMOJI) {     // Send emoji to happy device     sendCharArray(emojis[number]);   } else if (operationMode == OPERATION_MODE_BORING) {     // Form string from number and send it to device     char numberString[1];     sprintf(numberString, "%d", number);     sendCharArray(numberString);   } }  // Sends raw command to BLE module and prints debug output void sendBluetoothCommand(char *commandString) {   setStatusLEDColor(COLOR_OFF);    Serial.print(commandString);    ble.println(commandString);   if (ble.waitForOK()) {     Serial.println(F(" <- OK!"));      setStatusLEDColor(COLOR_BLUE);   }   else {     Serial.println(F(" <- FAILED!"));     setStatusLEDColor(COLOR_RED);   }; }  // Sends char array (string) to BLE module void sendCharArray(char* charArray) {   char commandString[64];   sprintf(commandString, "AT+BleKeyboard=%s", charArray);   sendBluetoothCommand(commandString); }  // Checks which keys are currently pressed and sends keycodes to BLE module void sendCurrentlyPressedKeys() {   char commandString[64] = "AT+BleKeyboardCode=00-00";    for (int i=0; i<KEY_COUNT; i++) {     if (keyPressStates[i] == true && keyValues[i] != 0x00) {       sprintf (commandString, "%s-%02X", commandString, keyValues[i]);     }   }    sendBluetoothCommand(commandString); }  // Process timers to detect when keyup messages have to be sent for each key void processKeyUps() {   for (int i=0; i<KEY_COUNT; i++) {     if (keyPressStates[i] == true) {       if (millis() - keyPressTimes[i] > keyHoldDurations[i]) {         keyPressStates[i] = false;         keyPressStateChanged = true;       }     }   } } 

На самом деле в этом устройстве есть очень интересный баг, который позволяет игроку очень быстро «прыгать» вперед. Я видел, как это происходило несколько раз, обычно после того, как кто‑то некоторое время отчаянно колотил по устройству. Я понятия не имею, почему это происходит или как это воспроизвести, но это довольно прикольно, так я решил просто оставить это и назвать фичей.

Doom 

Неслучайно в качестве объекта для управления с помощью Dialrhea был выбран классический Doom. Я безумно люблю эту игру и был одержим ею в детстве. Мой отец однажды принес ее с работы на 10 дискетах. Мне пришлось научиться писать пользовательские файлы  autoexec.bat config.sys и загружать систему со специальной дискеты, содержащей минимальную версию MS-DOS и высокооптимизированный драйвер мыши, чтобы памяти было достаточно для запуска Doom на моей машине Intel 386 33 МГц, на которой было всего 4 Мб ОЗУ.

«LH A:\MOUSE.COM» — волшебная строка, которая заставляла DOS загружать драйвер мыши в недоступную иным образом верхнюю память, тем самым выигрывая несколько дополнительных килобайт ОЗУ для Doom.

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

Спасибо за внимание!


ссылка на оригинал статьи https://habr.com/ru/articles/845974/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *