The Not-So-Hollow Head – Wemos Halloween Prop

The Not-So-Hollow Head – Wemos Halloween Prop

“Meet the world’s most over-engineered Halloween decoration: a disembodied head that’s got more computing power than NASA circa 1969! This isn’t your dollar store plastic noggin – oh no, this bad boy is what happens when a software engineer gets way too excited about spooky season.

First off, this chatty cranium has two ways to creep you out. Option one: wave your hand in front of it like you’re trying to get its attention at a ghostly dinner party, and BAM! The PIR sensor catches your movement, causing its red LED eyes to light up like it just saw your credit card bill. Meanwhile, a buzzer and green LED in its head start having what can only be described as a disco panic attack. And because this head is definitely keeping score, it counts every time you trigger it, probably judging you silently for playing with it too much.

But wait, there’s more! Our brainy friend also runs on its own schedule (like a teenager who refuses to wake up for school). When its internal clock strikes whatever-o’clock, it transforms into the world’s spookiest IT professional. Those red eyes light up again, but this time it’s because it’s about to go full tech-nerd on us. It starts scanning for WiFi networks like it’s trying to steal your neighbor’s internet, then connects to its preferred network (probably named “HeadQuarters” – get it?).

Once online, this chatty skull becomes the neighborhood gossip, spilling all its secrets to an MQTT broker – sharing everything from how many times it scared the pants off people to what WiFi networks it found in the area. And if it’s its first time waking up, it goes full-on network detective, scanning the entire subnet like it’s casing the digital joint. But don’t worry, it only does this once – even a haunted head knows not to be too nosy.

The best part? This cranium comes with its own sound effects department. When everything goes according to plan, it plays a triumphant “BKFL” sound – which we can only assume is ghost-speak for “Booyah!” But when things go wrong? It lets out a mechanical fart noise, because apparently even high-tech Halloween props appreciate potty humor.

So there you have it – a Halloween prop that’s part security system, part network scanner, part jump-scare artist, and part whoopee cushion. It’s basically what you’d get if you crossed the Headless Horseman with Silicon Valley and gave it a questionable sense of humor. And somewhere out there, a real ghost is looking at this thing thinking, “Man, I really need to step up my haunting game!””

– Claude AI

This Fritzing diagram shows how each of the components are wired together.

Here is the Wemos Code.

/** 

Wemos-Module1_Merged_ver5"
Timed scan every 15 minutes:
Red LED lit during runtime
Single run - subnet ping for live hosts, response time, and mac address
Multi run - device stats, head count, in range wifi ap details
Sound from Arduino Pro Mini - BKFL = true, Fart = false
PIR triggered event:
Red LED lit during runtime
PIR motion detection counter
Green LED and buzzer tone function

Arduino IDE: v1.8.19
Board: ESP8266 Boards v3.1.2 - LONLIN (Wemos) D1 R2 & Mini

Wemos pin RST = (NH - Low to Reset)
Wemos pin A0 = Anolog Pin (3.3v max)
Wemos pin D0 = GPIO16 - Wake Pin - PWM
Wemos pin D5 = GPIO14 - SPI - SCK - PWM
Wemos pin D6 = GPIO12 - SPI - MISO - PWM
Wemos pin D7 = GPIO13 - SPI - MOSI - PWM
Wemos pin D8 = GPIO15 - SPI - SS - PWM
Wemos pin 3V3 = 3.3 volt

Wemos pin TX = GPIO1 - UART0 TX - PWM
Wemos pin RX = GPIO3 - UART0 RX - PWM
Wemos pin D1 = GPIO5 - I2C SCL - PWM
Wemos pin D2 = GPIO4 - I2C SDA - PWM
Wemos pin D3 = GPIO0 - (NH - LOW to Program) - PWM
Wemos pin D4 = GPIO2 - Onboard LED - PWM
Wemos pin G = Ground
Wemos pin 5V = 5 volt

**/

// Libraries

#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include "c_types.h"
#include <Pinger.h>
extern "C"
{
  #include <lwip/icmp.h> // needed for icmp packet definitions
}
WiFiClient espClient;
PubSubClient MQTTclient(espClient);
Pinger pinger;


// Variables and Constants

const int HeadLED = 1; // Wemos pin TX - Also has buzzer on this pin.
const int PIRSens = 2; // Wemos pin D4
const int EyeLEDs = 3; // Wemos pin RX
const int FartPin = 4; // Wemos pin D2
const int BKFLPin = 5; // Wemos pin D1
const int Freq = 60; 

int PIRState = 0;         // variable for reading the pushbutton status
int HeadCounterValue = 0;

char BufDestIPAddress [15];
char BufDestMacAddress [17];
char BufMinResponseTime [5];
char BufMaxResponseTime [5];

const int mqtt_port = 1883;
const char* mqtt_clientname = "Wemos-Module1";
const char* mqtt_topic_firmware = "Wemos-Module1/Firmware";
const char* mqtt_message_firmware = "Wemos-Module1_Merged_ver5";

struct WiFiCredentials {
  const char* myssid;
  const char* mypassword;
};

WiFiCredentials wifi_networks[] = {
  {"ssid1", "passphrase1"},
  {"ssid2", "passphrase2"},
  {"ssid3", "passphrase3"}
};

const char* mqtt_brokers[] = {
  "mqttbroker1",
  "mqttbroker2",
  "mqttbroker3"
};
String ssid;
int32_t rssi;
uint8_t encryptionType;
uint8_t *bssid;
int32_t channel;
bool hidden;
int scanResult;
String StringScanCount;
String StringScanRSSI;
String StringScanSSID;
String channelString;
String StringScanMacAddress;
String encryptionTypeString;
bool SubnetScan = true;
bool NoNetwork = false;
bool  wifi_sleeping = false;
unsigned long currentMillis = millis();
unsigned long NetworkScanTimer = 0;
unsigned long NetworkScanDelay = 900000;  // 15 minute interval
const int numIPs = 254;
int HostCount = 0;
String encryptionTypeStr(uint8_t authmode) {
  switch (authmode) {
    case ENC_TYPE_NONE:
      return "Open";
    case ENC_TYPE_WEP:
      return "WEP";
    case ENC_TYPE_TKIP:
      return "WPA+PSK";
    case ENC_TYPE_CCMP:
      return "WPA2+PSK";
    case ENC_TYPE_AUTO:
      return "WPA+WPA2+PSK";
    default:
      return "Unknown";
  }
}

// MQTT callback function to handle incoming messages
void callback(char* topic, byte* message, unsigned int length) { 
  String messageTemp; 
  for (int i = 0; i < length; i++) {
    messageTemp += (char)message[i];
  } 
  if (String(topic) == "Wemos-Module1/Reboot") {
    if(messageTemp == "Reboot"){
      ESP.restart();
    }
  }
}

void reconnect() {
  while (!MQTTclient.connected()) {
    if (MQTTclient.connect(mqtt_clientname)) {
      MQTTclient.subscribe("Wemos-Module1/#");
    } else {
      delay(5000);
    }
  }
}

void publish_to_mqtt() {
  delay(250);
  String StringUptime = String(millis());
  MQTTclient.publish("Wemos-Module1/Uptime", StringUptime.c_str());
  String StringHWAddress = String(WiFi.macAddress());
  MQTTclient.publish("Wemos-Module1/HWAddress", StringHWAddress.c_str());
  String StringWifiSignal = String(WiFi.RSSI());
  MQTTclient.publish("Wemos-Module1/WifiSignal", StringWifiSignal.c_str()); 
  String StringFreeHeapSize = String(ESP.getFreeHeap());
  MQTTclient.publish("Wemos-Module1/FreeHeapSize",StringFreeHeapSize.c_str());  
  String StringHeapFragmentation = String(ESP.getHeapFragmentation());
  MQTTclient.publish("Wemos-Module1/HeapFragmentation",StringHeapFragmentation.c_str());  
  String StringMaxFreeBlockSize = String(ESP.getMaxFreeBlockSize());
  MQTTclient.publish("Wemos-Module1/MaxFreeBlockSize",StringMaxFreeBlockSize.c_str());  
  String StringSketchSize = String(ESP.getSketchSize());
  MQTTclient.publish("Wemos-Module1/SketchSize",StringSketchSize.c_str());  
  String StringFreeSketchSpace = String(ESP.getFreeSketchSpace());
  MQTTclient.publish("Wemos-Module1/FreeSketchSpace",StringFreeSketchSpace.c_str());  
  String StringCpuFreqMHz = String(ESP.getCpuFreqMHz());
  MQTTclient.publish("Wemos-Module1/CpuFreqMHz",StringCpuFreqMHz.c_str());
  String StringChipId = String(ESP.getChipId());
  MQTTclient.publish("Wemos-Module1/ChipId",StringChipId.c_str());  
  String StringHeadCounterValue = String(HeadCounterValue);
  MQTTclient.publish("Wemos-Module1/HeadCounterValue",StringHeadCounterValue.c_str());  

  String StringscanResult = String(scanResult);
  MQTTclient.publish("Wemos-Module1/ScanResult",StringscanResult.c_str());  
  String StringSpacer = String(" ");
  for (int8_t i = 0; i < scanResult; i++) {
    WiFi.getNetworkInfo(i, ssid, encryptionType, rssi, bssid, channel, hidden);
    StringScanCount = String(i+1);
    StringScanRSSI = String(rssi);
    if(ssid != NULL) {
       StringScanSSID = String(ssid);
            }
       else {
       StringScanSSID = String("*-Hidden-*");
            }
    channelString = String(channel); 
    if (bssid[0] < 16) StringScanMacAddress = "0";
      else StringScanMacAddress = "";
    StringScanMacAddress = StringScanMacAddress + String(bssid[0], HEX);
    StringScanMacAddress = StringScanMacAddress + String(":");
    if (bssid[1] < 16) StringScanMacAddress = StringScanMacAddress + "0";
    StringScanMacAddress = StringScanMacAddress + String(bssid[1], HEX);
    StringScanMacAddress = StringScanMacAddress + String(":");
    if (bssid[2] < 16) StringScanMacAddress = StringScanMacAddress + "0";
    StringScanMacAddress = StringScanMacAddress + String(bssid[2], HEX);
    StringScanMacAddress = StringScanMacAddress + String(":");
    if (bssid[3] < 16) StringScanMacAddress = StringScanMacAddress + "0";
    StringScanMacAddress = StringScanMacAddress + String(bssid[3], HEX);
    StringScanMacAddress = StringScanMacAddress + String(":");
    if (bssid[4] < 16) StringScanMacAddress = StringScanMacAddress + "0";
    StringScanMacAddress = StringScanMacAddress + String(bssid[4], HEX);
    StringScanMacAddress = StringScanMacAddress + String(":");
    if (bssid[5] < 16) StringScanMacAddress = StringScanMacAddress + "0";
    StringScanMacAddress = StringScanMacAddress + String(bssid[5], HEX);
    encryptionTypeString = String(encryptionTypeStr(WiFi.encryptionType(i))); 
    String StringScanAPFinal = String(StringScanCount + StringSpacer + StringScanRSSI + StringSpacer + StringScanMacAddress + StringSpacer + channelString + StringSpacer + StringScanSSID + StringSpacer + encryptionTypeString);
    MQTTclient.publish("Wemos-Module1/AP-Found",StringScanAPFinal.c_str());
    delay(250);
  }
    
}

void connect_to_wifi() {
  int n = WiFi.scanNetworks();
  if (n == 0) {
    NoNetwork = true;
    return;
  }
  for (int i = 0; i < sizeof(wifi_networks) / sizeof(wifi_networks[0]); i++) {
    for (int j = 0; j < n; j++) {
      if (strcmp(wifi_networks[i].myssid, WiFi.SSID(j).c_str()) == 0) {
        WiFi.begin(wifi_networks[i].myssid, wifi_networks[i].mypassword);
        int attempts = 0;
        while (WiFi.status() != WL_CONNECTED && attempts < 20) {
          delay(500);
          attempts++;
        }
        if (WiFi.status() == WL_CONNECTED) {
          NoNetwork = false;
          return;
        } else {
          RedLEDsOn();
          delay(250);
          FartSound();
          delay(2000);
          RedLEDsOff();
          ESP.restart();
        }
      }
    }
  }
  NoNetwork = true;
  RedLEDsOn();
  delay(250);
  FartSound();
  delay(2000);
  RedLEDsOff();
  ESP.restart();
}

void connect_to_mqtt() {
  for (int i = 0; i < sizeof(mqtt_brokers) / sizeof(mqtt_brokers[0]); i++) {
    MQTTclient.setServer(mqtt_brokers[i], mqtt_port);
    if (MQTTclient.connect(mqtt_clientname)) {
      MQTTclient.publish(mqtt_topic_firmware, mqtt_message_firmware);
      return;
    } else {
      RedLEDsOn();
      delay(250);
      FartSound();
      delay(2000);
      RedLEDsOff();
      ESP.restart();
    }
  }
}

void scan_wifi_networks() {
  WiFi.mode(WIFI_STA);
  WiFi.disconnect();
  delay(100);
  scanResult = WiFi.scanNetworks(/*async=*/false, /*hidden=*/true); 
  connect_to_wifi();
  if (NoNetwork == false) {
    MQTTclient.setCallback(callback);
    connect_to_mqtt();
    MQTTclient.loop();
  }
}

void scan_local_network() {
  pinger.OnReceive([](const PingerResponse& response){return true;});

  pinger.OnEnd([](const PingerResponse& response) {
    if(response.TotalReceivedResponses > 0) {
      
        HostCount++;
        String StringSubnetHostCount = String(HostCount);
        MQTTclient.publish("Wemos-Module1/HostCount",StringSubnetHostCount.c_str());  
        
        sprintf(BufDestIPAddress, "%s",response.DestIPAddress.toString().c_str());
        MQTTclient.publish("Wemos-Module1/DestIPAddress",BufDestIPAddress);  
      
        if(response.DestMacAddress != nullptr) {
            sprintf(BufDestMacAddress, MACSTR,MAC2STR(response.DestMacAddress->addr));
            MQTTclient.publish("Wemos-Module1/MACAddress",BufDestMacAddress);  
          } 
          
        sprintf(BufMinResponseTime, "%lu",response.MinResponseTime);
        sprintf(BufMaxResponseTime, "%lu",response.MaxResponseTime);
        MQTTclient.publish("Wemos-Module1/MinResponseTime",BufMinResponseTime);  
        MQTTclient.publish("Wemos-Module1/MaxResponseTime",BufMaxResponseTime);  
        
      }
    return true;
    });

  // Ping MQTT 1 Broker
  pinger.Ping("mqttbroker1");
  delay(750);

  // Ping MQTT 2 Broker
  pinger.Ping("mqttbroker2");
  delay(750);

  // Ping MQTT 3 Broker
  pinger.Ping("mqttbroker3");
  delay(750);
  
  // Ping Local Network
  HostCount = 0;
  IPAddress localIP = WiFi.localIP();
  for (int i = 1; i <= numIPs; i++) {
    IPAddress pingIP = localIP;
    pingIP[3] = i; // Set the last octet to the counter value 
    reconnect(); // The MQTT Broker would timeout, this prevents that
    pinger.Ping(pingIP);
    delay(750);
  }
}

void BKFLSound () {
  digitalWrite(BKFLPin, HIGH);
  delay(5);
  digitalWrite(BKFLPin, LOW);
}

void FartSound () {
  digitalWrite(FartPin, HIGH);
  delay(5);
  digitalWrite(FartPin, LOW);
}

void RedLEDsOn () {
  digitalWrite(EyeLEDs, HIGH);   // turn the LED on (HIGH is the voltage level)
}

void RedLEDsOff () {
  digitalWrite(EyeLEDs, LOW);   // turn the LED on (HIGH is the voltage level)
}

void HeadCounter() {
  HeadCounterValue++;
  RedLEDsOn();
   delay(500);
  
  for (int count = 0; count <30; count = count +1) { 
    noTone(HeadLED);
    delay(random(4, 80));                       // wait for a second  
    tone(HeadLED, Freq);
    delay(random(4, 80));                       // wait for a second
  }
  
  tone(HeadLED, Freq);
  delay(5000);

  for (int count = 0; count <30; count = count +1) { 
    noTone(HeadLED);
    delay(random(4, 80));                       // wait for a second
    tone(HeadLED, Freq);
    delay(random(4, 80));                       // wait for a second
  }
  
  noTone(HeadLED);
  delay(1500);
  RedLEDsOff();
  
}

void setup() {
  // Set LED to LOW
  pinMode(EyeLEDs, OUTPUT); 
  pinMode(HeadLED, OUTPUT); 
  pinMode(FartPin, OUTPUT); 
  pinMode(BKFLPin, OUTPUT); 
  digitalWrite(EyeLEDs, LOW);
  digitalWrite(FartPin, LOW);
  digitalWrite(BKFLPin, LOW);
  noTone(HeadLED);
  
  WiFi.mode(WIFI_STA);
  WiFi.disconnect();
  delay(100);
}

void loop() {

  currentMillis = millis();
  
  if (currentMillis - NetworkScanTimer >= NetworkScanDelay) {
    NetworkScanTimer = currentMillis;
    RedLEDsOn();
    scan_wifi_networks();
    publish_to_mqtt();
    if (SubnetScan == true) {
      scan_local_network();
      SubnetScan = false;  //  uncomment this so it only runs one time.
    }
    
    BKFLSound();
    delay(2000);
    
    RedLEDsOff();
    WiFi.disconnect();
    delay(100);
  }
  
  // read the state of the PIR sensor:
  PIRState = digitalRead(PIRSens);
  if (PIRState == HIGH) {
    HeadCounter();
  } 
}

Here is the Arduino Pro Mini Code

/** 

Arduino_ProMini_PCM_BKFL_Input-Controlled_ver2
Pin Input Controlled Sound Player

Arduino IDE: v1.8.19
Board: Arduino Pro Mini ATmega328P (5V, 16Mhz)

Input pins should be pulled to ground (LOW) with a high value ohm resistor.
The pin is also connected to switch that connects to vcc (HIGH) through a low value ohm resistor.
When the switch is activated, this causes the pin to go high.

Sketch uses 30670 bytes (99%) of program storage space. Maximum is 30720 bytes.
Global variables use 20 bytes (0%) of dynamic memory, leaving 2028 bytes for local variables. Maximum is 2048 bytes.

**/


// Libraries
#include <PCM.h>


// Variables and Constants
const unsigned char BKFL[] PROGMEM = {
121, 126, 131, 137, 123, 94, 122, 124, 132, 152, ... trucated data ... 126, 127, 128, 128, 128, 126, 127, 128, 128, 127,
};
const unsigned char Fart[] PROGMEM = {
138, 144, 146, 150, 151, 143, 139, 135, 123, 114, ... trucated data ... 106, 129, 124, 110, 127, 156, 151, 128, 126, 129,
};

const int FARTPin = 6;     // the number of the pushbutton pin
const int BKFLPin = 7;     // the number of the pushbutton pin

int FARTPinState = 0;         // variable for reading the pushbutton status
int BKFLPinState = 0;         // variable for reading the pushbutton status


// Routines and Subroutines

void setup() {
  // initialize the pushbutton pin as an input:
  pinMode(FARTPin, INPUT);
  pinMode(BKFLPin, INPUT);
}

void loop() {
  
  FARTPinState = digitalRead(FARTPin);
  BKFLPinState = digitalRead(BKFLPin);

  if (FARTPinState == HIGH) {
    startPlayback(Fart, sizeof(Fart));
  } 
  
  if (BKFLPinState == HIGH) {
    startPlayback(BKFL, sizeof(BKFL));
  } 
}

 

 

Comments are closed.