Doom on the dial phone Dialrhea

Dialrhea is a modified dial phone, redesigned to control Doom via Bluetooth.

We assembled it in two days during the "Internet Of Shit" hackathon organized by the Technarium team in Vilnius, Lithuania. The theme of this hackathon was to create devices that are completely useless but fully functional.

Dialrhea in action:

We even made a commercial for it:

We would like to hold a workshop where we would assemble at least three more devices and then play a deathmatch in Doom on them.

Background

The device was created during the "Internet Of Shit" hackathon. This amazing event was organized by the Technarium team in Vilnius, Lithuania, in 2017 and 2018. The nominations included categories such as "If it’s on fire, it works," "The least private online gadget," "This is probably illegal," and teams worked hard to turn these concepts into working technology.

Our device won the "Least Shitty Project" award, and we also received a prize for the most popular project. The prize, by the way, was a drain pump painted gold, haha.

The idea was born during beer gatherings with Donatas Valiulis and Džiugas Bartkus. The three of us implemented it. Donatas and I took care of the technical aspects, and Džiugas was responsible for the video and promotional materials.

Džiugas also made a funny psychedelic video about the creation of the device and the hackathon as a whole.

Technical Details

The device is built on Arduino and uses Bluetooth LE for wireless communication with a computer. It is a Bluetooth keyboard and can be connected to any device that supports Bluetooth LE. Below you can see a breakdown of the components powering Dialrhea.

Doom on the screen of the old dial phone Dialrhea

Although the final use case of the device became playing Doom, the device actually fully supports several operating modes:

  • Doom — in this mode, the device acts as a game controller and is configured to control the classic game Doom (using the Doomsday Engine)

  • Emoji — this mode is best used on mobile phones, it allows you to enter emojis and send them to friends.

  • Boredom — in this mode, Dialrhea simply outputs the dialed numbers (not recommended for use)

Reading data from the rotary dial

The most interesting part of the process was how the rotary dial actually works from a technical point of view. I am from the generation that caught rotary phones, so it was really interesting to understand how simple the mechanism actually is, and why I was shocked when I touched the phone wires while playing phone mechanic, only sometimes, and not constantly. If you are interested, here is a video explaining the mechanism.

Doom gameplay on a retro dial phone
Classic Doom game on the vintage Dialrhea phone

Once I understood how this mechanism works, implementing it with Arduino was quite simple.

Struggling with Bluetooth

Probably the biggest problem was getting Bluetooth to work properly. We decided to use the Adafruit Bluefruit LE UART Friend module because I had it, and I had already tried working with it in another project. It is a very efficient module, but most of the problems we encountered were related to stability and reliability. Sometimes everything worked fine, sometimes we got some errors when running the same code. We read a lot of documentation about the handshake protocol, how to pair correctly, etc., but in the end, we just added repeat loops and timeouts everywhere so that the chip had time to "recover" after each risky operation. Below you can see the full source code of Dialrhea.

#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"

// 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))

Doom

It is no coincidence that the classic Doom was chosen as the object to be controlled using Dialrhea. I absolutely love this game and was obsessed with it in my childhood. My father once brought it home from work on 10 floppy disks. I had to learn how to write custom autoexec.bat and config.sys files and boot the system from a special floppy disk containing a minimal version of MS-DOS and a highly optimized mouse driver so that there was enough memory to run Doom on my Intel 386 33 MHz machine, which had only 4 MB of RAM. "LH A:\MOUSE.COM" was the magic line that made DOS load the mouse driver into otherwise inaccessible upper memory, thereby gaining a few extra kilobytes of RAM for Doom.

Doom on the display of an antique dial phone
Retro Dialrhea phone with Doom on the screen

Another reason to choose Doom was that it can run on almost anything: calculators, microwaves, treadmills, vapes, and even pregnancy tests! If a device has a processor and a screen, there will be some geek who will try to run Doom on that machine. Compared to this, Doom on a rotary phone doesn't seem so crazy :)

Thank you for your attention!

Comments