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/
Добавить комментарий