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)); } }