I’ve seen these games around, and I wanted to make my own. The object is simple. The metal wand has a loop with a long windy wire going through it, and the object is to get from one end of the wire to the other, without touching the wand to the wire. It automatically detects when the game is starting and when it ends, and records the last time and record time. It also displays this information on the OLED display and it buzzes when the wire is touched.

NOTE: I tried to explain the construction of this as well as I could. If anything doesn’t make any sense, just let me know. I can post more pictures if needed.

Here’s a list of the materials that I used. Of course, you can definitely get similar results with different items. Overall, this cost me about $20, with $11 of it being the copper pipe.

The first thing that you need to do is create the windy wire and attach it to the base. I wanted a clean look with everything hidden away, so I used 2 blocks of wood (2″x4″x12″) and hollowed out the middle using a 3/4″ bit. I used the same bit in a few places to make a countersink for the screws so the screw heads aren’t sticking out.

I hollowed out 2 pieces of wood just like this to create a cavity for the electronics. This is a picture of the bottom block of wood.

Then I drilled 2x 1/4″ holes through the top piece of wood for the 2 ends of the copper pipe to go into. I also drilled 2 more holes right next to those for the wires for the washer to pass through. Next I drilled a small hole in the washers so that I can screw them into the wood. I also used the screw to hold the wires for the washers. I also drilled a larger hole in the middle to pass 6 wires (4 for the OLED and 2 for the speaker) and be able to fit the headers on the OLED. Be sure to put the pipe through the washers and the wand before fixing it to the wood.

Top block of wood. You can see the washers/screw on the left and right sides that hold the pipe. I also chiseled some channels for the washer wires.

Next, I bent the copper pipe in the shape I wanted. Be careful not to make really tight turn or else it might kink. Then I passed the copper pipe through the hole in the middle of the washers. I was able to dig through my bucket of screws to find a couple that fit perfectly into the pipe. If you don’t have a bucket of screws, you may want to go to a hardware store find screws that would go into the pipe. I used one of those screws to hold the wire that goes into D6 on the Wemos.

Next, I just screwed the speaker to the top, and used 2 screws to hold the OLED.

This shows how I used 2 screws to hold the OLED and the speaker. There is a hole between them to feed the wires down.

Now that we’re done being carpenters, we can wire everything up. Below is a schematic. Wire it up any way that is comfortable for you. I put headers on a protoboard along with the resistors and then jumpers to Wemos.

Schematic.

Once you have done that, you can load the code below on the Wemos/ESP8266. I have it connecting to WiFi using WifiManager so that I can upload firmware to it via OTA. You may want to disable that to speed up the boot.

A few notes about the code. I use an interrupt on the WIRE_PIN to catch any time the wand touches it. Before that, I just had the code in the loop to detect if it was touching. Since the loop took about 50ms to run(Mostly because of the OLED), that often caused issues where a ‘touch’ wouldn’t be detected if the player was lucky. However, the interrupt often caused the screen to get garbled if it happened in the middle of drawing.

I solved that problem by doing a complete redraw in the interrupt code. This causes a flashing of the screen when the wire is touched. If anyone knows of a better way to get around, please let me know.

The code just checks to see if the wand is on the ‘starting washer’ and not touching anything else. Then, when the player lifts up the wand, the timer starts. If the player completes the course, then the time is recorded to SPIFFS in JSON format. When the code is first run, SPIFFS is formatted and the high score file is created with really high values for the times.

I also wanted to play a a nice melody in the code when someone makes it to the end, but just didn’t get around to it. Also, the sound from the speaker isn’t that great, so I’m not sure if a ‘pleasant’ melody is possible. I’ll update here if I add that.

#include <ArduinoJson.h>
#include <FS.h>
#include <ESP8266WiFi.h>          //ESP8266 Core WiFi Library (you most likely already have this in your sketch)
#include <DNSServer.h>            //Local DNS Server used for redirecting all requests to the configuration portal
#include <ESP8266WebServer.h>     //Local WebServer used to serve the configuration portal
#include <WiFiManager.h>          //https://github.com/tzapu/WiFiManager WiFi Configuration Magic
#include <ESP8266mDNS.h>
#include <ESP8266HTTPUpdateServer.h>
#include <RemoteDebug.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

////**********START CUSTOM PARAMS******************//

//Define parameters for the http firmware update
const char* host = "WindyWire";
const char* update_path = "/WebFirmwareUpgrade";
const char* update_username = "admin";
const char* update_password = "espP@ssw0rd";

//Define the pins
#define START_PIN D5
#define WIRE_PIN D6
#define END_PIN D7
#define SPEAKER_PIN D8

bool RemoteSerial = false; //true = Remote and local serial, false = local serial only

//************END CUSTOM PARAMS********************//

#define OLED_RESET 0  // GPIO0
Adafruit_SSD1306 OLED(OLED_RESET);
RemoteDebug RSerial;

const char compile_date[] = __DATE__ " " __TIME__;

ESP8266WebServer httpServer(80);
ESP8266HTTPUpdateServer httpUpdater;

WiFiManager wifiManager;

double bestTime = 999.999;
double lastFinishTime = 999.999;
double currentTime = 0;
double startTime = 0;

//the four status of the 'game'
bool onStart = false;
bool playing = false;
bool onEnd = false;
bool failed = false;

//Bools to determine what wires are being touched by the wand. 
bool startPinStatus;
bool wirePinStatus;
bool endPinStatus;

void setup() {
  SPIFFS.begin();
  pinMode(START_PIN, INPUT);
  pinMode(WIRE_PIN, INPUT);
  pinMode(END_PIN, INPUT);

  //attachInterrupt to the wirePin so that a 'fail' is always recognized. Otherwise, the loop took over 50ms to run
  //and sometimes didn't catch the 'fail'. 
  attachInterrupt(WIRE_PIN, highInterrupt, RISING);

  //initialize the OLED
  OLED.begin();
  OLED.clearDisplay();
  OLED.setTextSize(1);
  OLED.setTextColor(WHITE);
  OLED.setCursor(0, 0);
  OLED.println("WindyWire");
  OLED.display(); //output 'display buffer' to screen

  Serial.begin(9600);

  RSerial.begin(host);
  RSerial.setSerialEnabled(true);

  //Set the wifi config portal to only show for 30 seconds, then continue.
  wifiManager.setConfigPortalTimeout(30);
  wifiManager.autoConnect(host);

  //setup http firmware update page.
  MDNS.begin(host);
  httpUpdater.setup(&httpServer, update_path, update_username, update_password);
  httpServer.begin();
  MDNS.addService("http", "tcp", 80);
  RSerial.printf("HTTPUpdateServer ready! Open http://%s.local%s in your browser and login with username '%s' and your password\n", host, update_path, update_username);
  delay(500);
  
  readTimesFile();

  //display the last time and the best time
  OLED.clearDisplay();
  OLED.setTextSize(1);
  displayBestTime();
  displayLastFinishTime();
  displayCurrentTime();
}

void readTimesFile() {
  //open the times file
  File timesFile = SPIFFS.open("/times.txt", "r");
  //if the times file doesn't exist, then format SPIFFS and create it. 
  if ( !timesFile ) {
    SPIFFS.format();
    timesFile.close();
    writeTimesFile( 989.99 , 989.99);
  }
  //If the file does exist, then read it and load the data into the global variables
  else {
    StaticJsonBuffer<200> jsonBuffer;
    size_t tFileSize = timesFile.size();
    std::unique_ptr<char[]> buff (new char[tFileSize]);
    timesFile.readBytes(buff.get(), tFileSize);
    JsonObject& root = jsonBuffer.parseObject(buff.get());
    bestTime = root["bestTime"];
    lastFinishTime = root["lastTime"];
  }
  timesFile.close();
}

void writeTimesFile( float aBestTime, float aLastTime ) {
  //This function writes the passed variables to the times file in Json format
  StaticJsonBuffer<200> jsonBuffer;
  JsonObject& root = jsonBuffer.createObject();
  // INFO: the data must be converted into a string; a problem occurs when using floats...
  root["bestTime"] = (String)aBestTime;
  root["lastTime"] = (String)aLastTime;
  File timesFile = SPIFFS.open("/times.txt", "w");
  root.printTo(timesFile);
  timesFile.close();
}

//display the best time on the top line
void displayBestTime() {
  OLED.setCursor(0, 0);
  OLED.print("Best: ");
  OLED.print( bestTime );
  OLED.print( " sec\n" );
  OLED.display();
}

//display the last success time on the middle line
void displayLastFinishTime() {
  OLED.setCursor(0, 10);
  OLED.print("Last: " );
  OLED.print( lastFinishTime );
  OLED.print( " sec\n" );
  OLED.display();
}

//display the current time on the last line, if playing. Otherwise, display the status
void displayCurrentTime() {
  OLED.setCursor(0, 20);
  if (onStart) {
    OLED.fillRect( 0, 20, 127, 20, BLACK);
    OLED.print("READY!");
    OLED.display();
    return;
  }
  else if (!onStart && !playing) {
    OLED.fillRect( 0, 20, 127, 20, BLACK);
    OLED.print("Go to Start.");
    OLED.display();
    return;
  }
  else if (playing) {
    OLED.fillRect( 0, 20, 127, 20, BLACK);
    OLED.print("Curr: ");
    OLED.print( currentTime );
    OLED.print( " sec\n" );
    OLED.display();
  }
}

//run this code any time the wand touches the pipe. 
//calls the 'failBuzz' function if it happens while playing. 
void highInterrupt() {
  OLED.clearDisplay();
  displayBestTime(); 
  displayLastFinishTime(); 
  displayCurrentTime();
  if (playing) {
    failBuzz();
  }
}

//If a fail happens while playing, then display a message and play a tone. 
void failBuzz() {
  playing = false;
  OLED.setCursor(0, 20);
  OLED.fillRect( 0, 20, 127, 20, BLACK);
  OLED.print("YOU HAVE FAILED!");
  OLED.display();
  tone(SPEAKER_PIN, 1000);
  delay(1000);
  tone(SPEAKER_PIN, 500);
  delay(1000);
  noTone(SPEAKER_PIN);
}

//Run this code when someone completes the game. Then record the time. 
void youWin() {
  playing = false;
  OLED.setCursor(0, 20);
  OLED.fillRect( 0, 20, 127, 20, BLACK);
  OLED.print("YOU WIN!!!!");
  OLED.display();
  lastFinishTime = currentTime;
  if (lastFinishTime < bestTime ) bestTime = lastFinishTime;
  writeTimesFile( bestTime, lastFinishTime);
  delay(5000);
}

void loop() {

  httpServer.handleClient(); //handles requests for the firmware update page

  if (RemoteSerial) RSerial.handle();

  //get the status of what the wand is touching
  startPinStatus = digitalRead(START_PIN);
  wirePinStatus = digitalRead(WIRE_PIN);
  endPinStatus = digitalRead(END_PIN);

  //if the status is playing, and the player touches the endPin, then they win
  if (playing && endPinStatus){ 
    currentTime = (millis() - startTime) / 1000;
    youWin();
  }

  //If nothing is being touched by the wand, and the status is onStart(Ready), then 
  //start playing. Record the startTime
  if (!startPinStatus && !wirePinStatus && !endPinStatus && onStart ) {
    playing = true;
    onStart = false;
    startTime = millis();
  }

  //If the wand is on only the startPin, then status is onStart/Ready
  else if (startPinStatus && !wirePinStatus) {
    onStart = true;
    playing = false;
  }

  //If the wirePin is touched while playing, then fail
  else if (playing && wirePinStatus) {
    failBuzz();
  }

  //if none of the above is true, then set the status to 'not ready'. 
  else {
    onStart = false;
  }

  //Calculate the time in seconds and display it to OLED
  //I am clearing the display() because the interrupt was messing up timings for the OLED,
  //and that caused some issues with drawing. 
  currentTime = (millis() - startTime) / 1000; 
  displayCurrentTime();
}

 

The ‘course’ that I made with the copper pipe has proven to be difficult. After trying dozens of times, I was only able to beat it once, and haven’t been able to do it again since then. No one else that has tried it was able to get more than half way through. You can see the high score in my pictures as less than a second. I had to cheat using a wire for testing. If you are testing it out, and you ever want to reset the scores, just add SPIFFS.format(); into the setup function and upload the code. Then remove that line and upload the code again.

If anyone has any questions or comments, feel free to post them here, on the Reddit post, or Twitter.

Leave a Reply