Dialrhea – playing Doom on a disk phone

Dialrhea – playing Doom on a disk phone

Dialrhea is a disc phone repurposed to play the classic Doom Bluetooth game. We put it together in two days during the Internet Of Shit hackathon organized by the Technarium team in Vilnius. The theme of the hackathon was the creation of completely useless, but fully functional devices. Below you can see our Dialrhea in action.

That same weekend we edited an original promo video demonstrating the capabilities of this revolutionary shit machine.

What’s next?

I want to conduct a master class in which we will collect at least three more such devices, after which we will play Doom on them in deathmatch mode. The ideal setting would be some kind of Hacker Camp or similar event. If you have any ideas on how to organize this, please email me.

History

We assembled the device at a hackathon

Internet Of Shit

“. This is a great event, organized in 2017 and 2018, as I said, by the Technarium team in Vilnius. The winners were chosen in the following categories: Fire Means It’s Working, Least Private On-line Gadget, This is Likely Illegal, and the teams competed hard to embody these principles in technological devices.

It was our device that won the “Least Shitty project” award, plus we won the “Public Prize” for the most popular project. The prize turned out to be a rubber drainage pump painted with a can of gold paint.

This genius idea itself was born after several bottles of beer drunk in the company of Donatas Valioulis and Dzyugas Bartkus. Our trio checked the entire process, in which Donatas and I took care of the technical aspects, while Dzyugas was responsible for filming videos and promotional materials.

Dzyugas also shot a cool trippy video about assembling the device and the whole hackathon as a whole.

Many other absurd projects were implemented as part of the event. who cares

here is the one hour presentation

of all Internet of Shit 2017 projects, including Dialrhea.

Showings

In addition to the hackathon, our device also became an honored guest at many events, including “Maker Faire 2017” at The Energy and Technology Museum, Hacker Camp “No Trolls Allowed”, the birthday of “Vilnius Tech Park”, the music festival “Braille Satellite” and others.

▍ Birthday “Vilnius Tech Park 2018”

I spent a lot of time in a co-working space at the recently opened Sapiegos Tech Park, a startup-filled complex of old hospital buildings in the middle of a picturesque park in the center of the city. An amazing place where I met a lot of interesting people, attended a lot of events and heard a lot of interesting business ideas discussed around the kitchen sink. When “Sapiegos Tech Park” celebrated its second birthday, I proposed to launch our Dialrhea as an interactive installation.

The event was attended by many interesting people from the world of business and technology. In the photo above, you can see famous Latvian entrepreneur Valdas Lasas having fun testing our Dialrhea. Unfortunately, he did not invest in this revolutionary shit machine. Perhaps, due to his conservative nature, he simply did not understand how to properly use this high-tech gadget, and instinctively tried to put the receiver to his ear, which obviously did not produce any results.

▍ Braille Satellite Music Festival 2018

As a rather contrasting previous event, where our Dialrhea was presented, was the music festival “Braille Satellite”. Held in the amazing Mushroom Manor Park in the Latvian hinterland, this festival was dedicated to underground trends in electronic music.

This time, instead of entrepreneurs, Dialrhea was tested by all kinds of ravers, hipsters, musicians and people who didn’t sleep for three days.

Technical details

The device itself is assembled on Arduino and uses Bluetooth LE for wireless communication with a PC. It is defined as a Bluetooth keyboard and can be paired with any device that supports Bluetooth LE. Below are the ingredients that underlie Dialrhea.

And although the final use of the device was playing Doom, the fact is that it supports several modes of operation:

  • Doom – In this mode, the device acts as a game controller and is configured to control the classic Doom game (using the Doomsday Engine).
  • Emoji – this mode is best used with smartphones. It allows you to type and send emojis to your friends.
  • Boring – in this mode, Dialrhea simply displays dialed numbers (not recommended).

▍ Data reading when dialing a number

The most interesting thing in the whole process was to find out the actual principle of operation of the disk set from a technical point of view. I belong to the generation that still remembers disk phones, so I was really interested to understand the simplicity of this mechanism and why I was not always electrocuted by wires when I played the mechanic-telephone operator. If you are interested, then

here is the video

where this mechanism is described.

Having figured out the principle of operation, it was not difficult to implement it on the basis of Arduino, and I do not remember that there were any serious difficulties.

▍ Bluetooth problems

Perhaps the hardest part was getting Bluetooth to work properly. For this task, we have chosen a module

Adafruit Bluefruit LE UART Friend

simply because I had it and I was already trying to use it in another project. It is a very functional module, but our main problems were related to stability and reliability. Sometimes it worked fine, but sometimes when we ran the exact same code we got errors. We read many pages of documentation about the correct implementation of protocols for exchanging handshakes, performing pairing, and so on, but as a result, we simply added retry cycles and timeouts everywhere, so that the chip had time to “come to its senses” after each risky operation. Below is the complete source code for Dialrhea.

Code

/***************************************************************************
Это мозг Dialrhea — революционной дерьмовой машины — написанный за два часа на хакатоне «Internet Of Shit 2017», проводившемся организацией Technarium в Вильнюсе, Латвия. Так что код вполне ожидаемо получился тоже дерьмовым.

Автор: Giedrius Tamulaitis, [email protected]
Version: 1.0

Код для Adafruit nRF51822 на базе модулей Bluefruit LE https://learn.adafruit.com/introducing-the-adafruit-bluefruit-le-uart-friend
***************************************************************************/

#include 
#include 
#if not defined (_VARIANT_ARDUINO_DUE_X_) && not defined(ARDUINO_ARCH_SAMD)
  #include 
#endif
#include "Adafruit_BLE.h"
#include "Adafruit_BluefruitLE_UART.h"
#include "BluefruitConfig.h"

#define DEVICE_NAME "Dialrhea"

// Входной контакт дискового набора
#define ROTARY_PIN 2
// Входной контакт телефонной трубки
#define HANDSET_PIN 3
// Контакт потенциометра переключения рабочего режима
#define OPERATION_MODE_PIN A5

// Сколько ждать до отправки сообщения «keyup» для клавиш движения в режиме Gaming
#define CONTROL_KEY_HOLD_DURATION 200
// Сколько ждать до отправки сообщения «keyup» для кнопки выстрела в режиме Gaming
#define FIRE_KEY_HOLD_DURATION 200
// Сколько ждать до отправки сообщения «keyup» для клавиш, которые должны быть просто одиночными нажатиями
#define INSTANT_KEY_HOLD_DURATION 10

// Контакты для отражения состояния RGB-светодиодов
#define STATUS_LED_RED_PIN 4
#define STATUS_LED_GREEN_PIN 6
#define STATUS_LED_BLUE_PIN 5

// Константы для цветов
#define COLOR_OFF 0
#define COLOR_RED 1
#define COLOR_GREEN 2
#define COLOR_BLUE 3

// Общее число клавиш, поддерживающих хронометрированные нажатия
#define KEY_COUNT 14

// Индекс данных рычага телефонной трубки в массивах (режим Gaming, нужно два индекса, так как мы отправляем сигналы нажатия клавиш для выстрела и открытия двери)
#define KEY_GAMING_MODE_HANDSET_1_INDEX 10
#define KEY_GAMING_MODE_HANDSET_2_INDEX 11
// Индекс данных с рычага телефонной трубки в массивах (режим Emoji)
#define KEY_EMOJI_MODE_HANDSET_INDEX 12
// Индекс данных с рычага телефонной трубки в массивах (режим Boring)
#define KEY_BORING_MODE_HANDSET_INDEX 13

// Отображение значений для каждого типа набранного номера и нажатия рычага телефонной трубки
const int keyValues[KEY_COUNT] = {
  0x42, // Номер 0 в режиме gaming (сейчас быстрая загрузка)
  0x52, // Номер 1 в режиме gaming (сейчас стрелка «вверх»)
  0x4F, // Номер 2 в режиме gaming (сейчас стрелка «вправо»)
  0x50, // Номер 3 в режиме gaming (сейчас стрелка «влево»)
  0x51, // Номер 4 в режиме gaming (сейчас стрелка «вниз»)
  0x2A, // Номер 5 в режиме gaming (сейчас ?, следующее оружие)
  0x00, // Номер 6 в режиме gaming
  0x00, // Номер 7 в режиме gaming
  0x00, // Номер 8 в режиме gaming
  0x00, // Номер 9 в режиме gaming
  0x10, // Нажатие рычага трубки в режиме Gaming (KEY_GAMING_MODE_HANDSET_1_INDEX) (на данный момент пробел)
  0x2C, // Нажатие рычага трубки в режиме Gaming (KEY_GAMING_MODE_HANDSET_2_INDEX) (на данный момент 'm')
  0x28, // Нажатие рычага трубки в режиме Emoji (KEY_EMOJI_MODE_HANDSET_INDEX) (на данный момент Enter)
  0x29  // Нажатие рычага трубки в режиме Boring (KEY_BORING_MODE_HANDSET_INDEX) (на данный момент Esc)
};

// Продолжительность нажатия каждого вида клавиши (отображение того же содержимого, что и в массиве keyValues)
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
};

// Массив для хранения времени нажатия каждой клавиши
unsigned long keyPressTimes[KEY_COUNT];
// Массив для хранения состояния каждой клавиши
bool keyPressStates[KEY_COUNT];

// Переменные, необходимые для обработки ввода с номеронабирателя
int rotaryHasFinishedRotatingTimeout = 100;
int rotaryDebounceTimeout = 10;
int rotaryLastValue = LOW;
int rotaryTrueValue = LOW;
unsigned long rotaryLastValueChangeTime = 0;
bool rotaryNeedToEmitEvent = 0;
int rotaryPulseCount;

// Режимы работы
#define OPERATION_MODE_GAMING 0 // Управление в режиме Gaming, настроенное для лучшей игры всех времён: "Doom"
#define OPERATION_MODE_EMOJI 1 // Эмодзи + Enter
#define OPERATION_MODE_BORING 2 // Номера + Esc

// Текущий рабочий режим
int operationMode;

// Эмодзи для каждого набранного номера
const char* emojis[] = {":-O", ":poop:",  ":-)", ":-(", ":-D", ":-\\", ";-)", ":-*", ":-P", ">:-("};

// Переменные для обработки рычага трубки
bool isHandsetPressed = false;
unsigned long handsetPressStartTime = 0;
unsigned long handsetPressStartTimeout = 60;

// Переменная, определяющая изменение состояний клавиш во время обработки цикла (чтобы можно было отправлять команды по необходимости один раз в конце цикла)

bool keyPressStateChanged;

// Настройки конфигурации модуля Bluetooth LE
#define FACTORYRESET_ENABLE         0
#define VERBOSE_MODE                false  // Если установлено «true», активируется отладочный вывод
#define MINIMUM_FIRMWARE_VERSION    "0.6.6"
#define BLUEFRUIT_HWSERIAL_NAME      Serial1

// Объект модуля Bluetooth LE
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);

  // Ожидание установки последовательного соединения (необходимо для Flora & Micro, или когда нужно 
  // придержать инициализацию, пока не откроется
  // монитор последовательного интерфейса 
 // while (!Serial);
  
  // Даём микросхеме немного времени на разогрев
  delay(1000);

  initializeSerialConnection();
  initializeBLEModule();

  // Небольшая задержка, поскольку хорошим устройствам всегда нужно какое-то время для запуска
  delay(100);

  setStatusLEDColor(COLOR_BLUE);
}

void loop(void) {
  keyPressStateChanged = false;
  refreshOperationMode();
  handleHandset();
  handleRotary();
  processKeyUps();

  // Если состояние нажатых клавиш изменилось — отправляем новое состояние
  if (keyPressStateChanged)
    sendCurrentlyPressedKeys();
}

// Устанавливает цвет светодиода, отражающего состояние
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);
}

// Выводит сообщение об ошибке и кирпичит революционную дерьмовую машину
void error(const __FlashStringHelper*err) {

  setStatusLEDColor(COLOR_RED);
  
  Serial.println(err);
  while (1);
}

// Мигает светодиодом состояния (пока поддерживается только зелёный)
void blink() {
  setStatusLEDColor(COLOR_OFF);
  delay(100);
  setStatusLEDColor(COLOR_GREEN);
}

// Открывает последовательное соединение для отладки
void initializeSerialConnection() {
  Serial.begin(9600);
  Serial.println(F("Hello, I am the Dialrhea! Ready for some dialing action?"));
  Serial.println(F("8-------------------------------------D"));
}

// Инициализирует модуль Bluetooth LE
void initializeBLEModule() {
  // Буфер для хранения команд, отправленных модулю BLE 
  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();

  // Отключение отражения команды от Bluefruit
  ble.echo(false);

  blink();

  Serial.println("Requesting Bluefruit info:");
  ble.info();

  blink();

  // Изменение имени устройства — пусть весь мир знает, что это 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();

  // Сброс ПО (добавление и удаление сервисов требует сброса)
  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();
}

// Считывает положение потенциометра выбора режима работы и определяет текущий режим
void refreshOperationMode() {
  operationMode = floor((float)analogRead(OPERATION_MODE_PIN) / 342.0);
}

// Отслеживает состояние рычага трубки
void handleHandset() {
  // Игнорирует ввод, пока не пройдёт таймаут последнего действия (для исключения шума)
  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(); 
    }
  }
}

// Отслеживает состояние номеронабирателя
void handleRotary() {
  int rotaryCurrentValue = digitalRead(ROTARY_PIN);

  // Если номер не набирается, или его набор только что закончился
  if ((millis() - rotaryLastValueChangeTime) > rotaryHasFinishedRotatingTimeout) {
    // Если вращение номеронабирателя только остановилось, нужно отправить событие
    if (rotaryNeedToEmitEvent) {
      // Отправка события (мы берём количество по модулю 10, так как «0» отправляет 10 импульсов).
      onRotaryNumberDialed(rotaryPulseCount % 10);
      rotaryNeedToEmitEvent = false;
      rotaryPulseCount = 0;
    }
  }

  // Если значение номеронабирателя изменилось, регистрируем время, когда это произошло
  if (rotaryCurrentValue != rotaryLastValue) {
    rotaryLastValueChangeTime = millis();
  }

  // Начинаем анализировать данные, только когда сигнал стабилизируется (таймаут антидребезга проходит)
  if ((millis() - rotaryLastValueChangeTime) > rotaryDebounceTimeout) {
    // Это означает, что переключатель перешёл либо из состояния закрыт в открыт, либо наоборот
    if (rotaryCurrentValue != rotaryTrueValue) {
      // Регистрация фактического изменения значения 
      rotaryTrueValue = rotaryCurrentValue;

      // При переходе в состояние HIGH повышаем количество импульсов
      if (rotaryTrueValue == HIGH) {
        rotaryPulseCount++; 
        rotaryNeedToEmitEvent = true;
      } 
    }
  }

  // Сохраняем текущее значение в качестве последнего
  rotaryLastValue = rotaryCurrentValue;
}

// Обработчик событий сработал, когда был зарегистрирован щелчок рычага трубки
void onHandsetClicked() {
  // Регистрация изменения состояния рычага трубки при нажатии клавиш в зависимости от режима
  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();
  }
}

// Обработчик событий срабатывает при наборе номера
void onRotaryNumberDialed(int number) {
  if (operationMode == OPERATION_MODE_GAMING) {
    // Установка состояния клавиши для набранной клавиши
    if (keyPressStates[number] == false)
      keyPressStateChanged = true;
  
    keyPressStates[number] = true;
    keyPressTimes[number] = millis();
  } else if (operationMode == OPERATION_MODE_EMOJI) {
    // Отправка эмодзи на радостное устройство
    sendCharArray(emojis[number]);
  } else if (operationMode == OPERATION_MODE_BORING) {
    // Создание строки из номера и её отправка на устройство
    char numberString[1];
    sprintf(numberString, "%d", number);
    sendCharArray(numberString);
  }
}

// Отправка сырой команды модулю BLE и вывод отладочной информации
void sendBluetoothCommand(char *commandString) {
  setStatusLEDColor(COLOR_OFF);

  Serial.print(commandString);

  ble.println(commandString);
  if (ble.waitForOK()) {
    Serial.println(F("  keyHoldDurations[i]) {
        keyPressStates[i] = false;
        keyPressStateChanged = true;
      }
    }
  }
}

It is worth noting that our device has a very interesting bug that allows the player to make a sharp jump forward. I’ve seen this happen a few times, usually after someone has been frantically on the phone for a while. I have no idea why this is happening or how to reproduce it. Although in general I like this behavior, so I decided to leave it as it is and just consider it a kind of feature.

Doom

Not surprisingly, the classic Doom was chosen as the subject to control with Dialhrea. I really love this game and was obsessed with it for a long time as a kid. My father brought it once from work on about 10 diskettes. I had to learn how to write files myself

autoexec.bat

and

config.sys

and boot the system from a special diskette containing a minimal version of MS-DOS and an optimized mouse driver. All this just to leave enough memory to run Doom on my Intel 386 33MHz machine, which I remember only having 4MB of RAM.

LH A:\MOUSE.COM

was the magic line that asked DOS to load the mouse driver into an otherwise inaccessible upper area of ​​memory, thereby winning extra kilobytes for Doom.

Obviously, I wasn’t the only one obsessed with this game. It wasn’t until many years later that I discovered that Doom and its creators, id Software, had a profound impact on the PC gaming market and were the first to successfully implement the Shareware distribution model. At the same time, Doom was installed on more computers than Windows 95. David Kushner has an excellent book, Masters of Doom, in which he tells the story of id Software in a lot of incredible detail. I highly recommend it. In addition, there is a video on YouTube that describes a rather interesting picture of innovative solutions and dramatic events inside “id Sodtware”. I still think Doom is the best game ever released.

Doom runs on everything

Another reason for choosing Doom for our project was its well-known ability to run on almost any hardware. Such projects are usually implemented by rabid Doom fans. This game has been ported to run on calculators, microwaves, treadmills, toll devices, vapes, and even pregnancy tests! If the device has a processor and a screen, there will probably be some geek who will try to run Doom on it. The largest collection of such devices known to me is collected on the Subreddit channel

r/itrunsdoom

.

One of the more interesting projects was done by a guy who ran Doom on a scientific calculator requested from ~800 potatoes. Yes, potatoes! Compared to it, our Dialrhea project doesn’t seem so crazy anymore.

Rivalry

As it turned out, we were not the only ones willing to spend our time building such a ridiculous machine. Five years after our attempt, somewhere in Japan, a guy named Yoshino assembled a similar device. I have to admit that his marketing campaign was much more successful than ours, and the video went viral with coverage from top gaming media like PC Gamer, IGN, and more. Moreover, it was even noted in a tweet by John Romero himself (one of the creators of the original Doom).

Compared to Dialrhea, Yoshino’s device does not engage the receiver, forcing you to dial for a shot 1 (which is much less fun than frantically hitting the lever with a pipe). Also, it needs to be connected to a computer with a cable, while the Dialrhea is completely wireless and supports Bluetooth LE. Another example of the fact that the technologically more perfect solution does not always win the market battle. All the power is in marketing.

Telegram channel with discounts, prize draws and IT news 💻

Related posts