https://tamulaitis.lt/project/dialrhea/ < Giedrius Tamulaitis Contact meContact megiedrius@tamulaitis.lt Dialrhea - play Doom on rotary phone Dialrhea is a modified rotary phone that has been repurposed to control the classic first-person shooter game Doom via Bluetooth. We built it in two days during the "Internet Of Shit" hackathon organized by Technarium crew in Vilnius, Lithuania. The theme of this hackathon was to build devices that are completely useless, but fully functional. Here is Dialrhea in action: Watch Dialrhea in action: https://www.youtube.com/watch?v=YNjw6ZKlyNM During the same weekend we also made a "slick" promotional video demonstrating capabilities of this revolutionary shitty machine. Watch Dialrhea promotional video: https://www.youtube.com/watch?v= RVSE1t3wHlI What's next? I would like to host a workshop where we would build at least three more devices, and then play a Doom death match using these devices. Ideal setting would be some hacker camp or similar event. If you have ideas how to make it happen, drop me a line. The Story The device was built during "Internet Of Shit" hackathon. It's an amazing event that was hosted by Technarium crew in Vilnius, Lithuania in 2017 and 2018. Award categories included the likes of "Fire Means It's Working", "Least Private On-line Gadget", "This is Likely Illegal", and the teams were competing hard to turn these concepts into a working piece of technology. Our device won the "Least Shitty project" award, and we were also awarded the "Public Prize" for the most popular project. It is worth mentioning that the prize was a rubber drainage cleaning pump sprayed with gold paint. The ingenious idea was generated after a few beers with Donatas Valiulis and Dziugas Bartkus. The tree of us carried out the whole operation with me and Donatas taking care of technical aspects and Dziugas responsible for video production and promotional materials. Dziugas also made a nice trippy video about the making of the device and the whole hackathon in general. Watch Dialrhea making of video: https://www.youtube.com/watch?v= nAaBJcYIFyU There were lots of other absurd projects built during the event, if you are interested, here is the full one hour long presentation of all projects of "Internet of Shit 2017" including Dialrhea. Appearances In addition to the hackathon, the device was also a guest of honor in numerous events including Maker Faire 2017 at The Energy and Technology Museum, No Trolls Allowed hacker camp, Vilnius Tech Park birthday, Braille Satellite music festival and others. Vilnius Tech Park birthday 2018 I spent quite some time in a co-working space in a newly opened Sapiegos Tech Park, a start-up / scale-up infested complex of old hospital buildings in a middle of a beautiful park in the center of the city. An amazing place where I met a lot of interesting people, attended many events and heard a tons of interesting business ideas being pitched near the kitchen sink. When Sapiegos Tech Park celebrated their second birthday I offered to fire up Dialrhea as an interactive installation. Dialrhea at Sapiegos Tech Park 1 Dialrhea at Sapiegos Tech Park 2 There were many interesting people from business and tech world there. In a picture above you can see well known Lithuanian entrepreneur Valdas Lasas testing out Dialrhea and enjoying it. Unfortunately, he didn't end up investing in this revolutionary shitty machine, probably because being old-school he didn't understand how to use this high tech gadget properly and instinctively tried putting the phone to his ear, which obviously gained no results. Braille Satellite music festival 2018 On the other side of the spectrum was Dialrhea's appearance in the Braille Satellite music festival. Taking place in an amazing Mushroom Manor in a middle of Lithuanian countryside it's a small and cozy festival for the weirder side of electronic music. Dialrhea at Braille Satellite 1 Dialrhea at Braille Satellite 2 Instead of entrepreneurs this time Dialrhea was battle tested with a bunch of ravers, hipsters, musicians and people who haven't slept for three days. Technical details The device is built on Arduino, and uses Bluetooth LE to communicate to the computer wirelessly. It presents itself as bluetooth keyboard and can be paired with any device supporting Bluetooth LE. Below you can see the breakdown of components powering Dialrhea. Insides of Dialrhea Although the ultimate use case for the device ended up being playing Doom, device actually has full support for multiple modes of operation: * Doom - in this mode the device acts as a game controller, and is configured to control classic Doom game (using Doomsday Engine) * Emoji - this mode is best used with mobile phones and allows you to type emoji's and send them to your friends * Boring - in this mode Dialrhea just outputs the dialed numbers (not recommended) Reading data from rotary dial The most interesting learning during the process was how the rotary dial actually works from technical point of view. I am from the generation who still remember using rotary phones so it was really interesting to understand both how simple the mechanism actually is and why I was getting electric shock while touching phone wires and playing phone mechanic only sometimes and not all the time. If you are interested here is the video explaining the mechanism . Working on rotary dial Fighting bluetooth After understanding how the mechanism works implementing it with Arduino was pretty much straight forward and I don't remember having any major troubles with it. Fighting bluetooth Probably the biggest challenge was to make bluetooth communication work properly. We chose to use Adafruit Bluefruit LE UART Friend module for the job just because I had it lying around and also I already tried to work with it while working on another project. It is a very capable module, but most problems we had were around stability and reliability. Sometimes things worked well, sometimes we were getting some errors running exactly the same code. We read lots of documentation about how to follow proper handshake protocols, do pairing correctly, etc., but then ended up just adding retry loops and timeouts everywhere so the chip has time to "come to it's senses" after each risky operation. You can see full source code for Dialrhea below. Click to enable scrolling Tap to enable scrolling /*************************************************************************** These are the brains of Dialrhea - revolutionary shitty machine, written in few hours during Internet Of Shit Hackathon 2017 at Technariumas, Vilnius, Lithuania, so expect the code to be pretty shitty. Author: Giedrius Tamulaitis, giedrius@tamulaitis.lt Version: 1.0 This code is for Adafruit nRF51822 based Bluefruit LE modules 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" // 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 keyHoldDurations[i]) { keyPressStates[i] = false; keyPressStateChanged = true; } } } } There is actually a very interesting bug in this device that allows player to "leap" forward very quickly. I have seen it happening several times, usually after somebody was frantically bashing the device for a while. I literally have no idea why it happens or how to reproduce it, but I actually find it really cool, so I decided to just leave it in and call it a feature. Doom It is no coincidence that classic Doom was chosen as a subject to be controlled by Dialrhea. I absolutely love the game and was obsessed by it while growing up. My father brought it back from his work one day in like 10 floppy disks. I had to learn how to write custom autoexec.bat and config.sys files and boot the system up from special floppy disk containing minimal version of MS-DOS and highly optimized mouse driver so there is just enough memory to run Doom on my Intel 386 33Mhz machine that only had 4Mb of RAM if I remember correctly. "LH A:\MOUSE.COM" was the magic line that told DOS to load mouse driver into otherwise unreachable high memory therefore winning some additional kilobytes of RAM for Doom. Doom 1993 title screen Doom in floppy discs Apparently I was not the only one obsessed by Doom, many years later that I found out that Doom and it's creators "id Software" actually redefined the PC gaming market and were first to introduce shareware distribution model successfully. At one point more personal computers had Doom than Windows 95 operating system installed. There is a great book "Masters of Doom" by David Kushner that tells the story of "id Software" with lots of incredible details, highly recommended read. This YouTube video also paints quite a nice picture about the innovation and drama inside "id Software". I still think that Doom is the best game that was ever released. Doom running on everything One more reason to choose Doom to be controlled with rotary phone was that Doom is notorious for being able to run on pretty much any hardware. Well, actually crazy Doom fans are notorious for making Doom run on pretty much any hardware. Doom was made to run on calculators, microwaves, treadmills, traffic ticket validators, vapes and even pregnancy tests! If the device has a CPU and the screen, there will be some geek trying to run Doom on that machine. Subreddit r/itrunsdoom has the most impressive collection of devices running Doom that I've seen so far. Doom running on everything One of the more interesting projects was one guy running Doom on scientific calculator powered by ~800 potatoes ! Yes, potatoes! Compared to this our Dialrhea project doesn't seem so crazy. Competition Apparently we were not the only ones willing to waste our time building ridiculous machine like this. Good five years after our attempt somewhere in Japan the guy named Yoshino built similar device. I have to admit his marketing campaign was way more successful than ours and the video went viral, being covered by major gaming outlets like PC Gamer, IGN and others and even provoking a tweet from John Romero (one of the people who created original Doom) himself. John Romero's tween about rotary phone Compared to Dialrhea, Yoshino's device doesn't utilize handset at all forcing you to dial 1 to shoot (which is way less fun than frantically mashing the handset). Also you need to connect it to the computer via cable while Dialrhea is fully wireless and supports Bluetooth LE. Just another example how technologically superior solution doesn't always win the market, marketing does. Giedrius Tamulaitis Giedrius Tamulaitis My fascination with technology and arts started when my father bought me my first computer back in 1995. Since then I am on a constant journey of learning, building and experimenting. I love both technology and arts and am most fascinated to work on projects where the two go hand in hand. Currently mostly focused on a professional career as a technology leader and raising an amazing daughter, I still try to find the time to work on something creative and fun. I love to meet like minded individuals, so if you found some of my projects interesting, have something to suggest or want to collaborate, don't hesitate to drop me a line! LinkedInhttps://www.linkedin.com/in/giedriustamulaitisGitHubhttps:// github.com/GiedriusT Contact meContact megiedrius@tamulaitis.lt