Enclosure
The enclosure is designed with its form follows function. It vastly maintains a cylindrical shape with necessary cutaways and bump for battery pack. It is secured on the waist with a dovetail joint.
Circuit
I designed a assembly process for all electronics so everything integrates:
- Main circuit with Xiao ESP32C3, 5V Buck Converter, 2.5W Speaker Amp
- Two SK6812 LED strips
- 4 Ohm Speaker Unit2500 mah LiPo battery
#include <WiFi.h>
#include <WiFiUdp.h>
#include "config.h"
#include <Adafruit_NeoPixel.h>
WiFiUDP udpClient;
const char server[] = "10.23.11.49"; // Server IP address - change this to your Node-RED server IP
const int localPort = 5000; // Local UDP port for receiving data
// NeoPixel LED strip configuration
#define LED_PIN_1 2 // Left LED strip (WS2812B) - GPIO2
#define LED_PIN_2 10 // Right LED strip (SK6812) - GPIO10
#define LED_COUNT 9 // Number of LEDs per strip
// Configure LED strips for RGBW mode
// NEO_RGBW: 4 bytes per pixel (Red, Green, Blue, White)
// NEO_KHZ800: 800 KHz bitstream
Adafruit_NeoPixel strip1(LED_COUNT, LED_PIN_1, NEO_RGBW + NEO_KHZ800); // WS2812B strip
Adafruit_NeoPixel strip2(LED_COUNT, LED_PIN_2, NEO_RGBW + NEO_KHZ800); // SK6812 strip
// Buzzer pin configuration
#define BUZZER_PIN_POS 3 // Buzzer positive pin (A+)
#define BUZZER_PIN_NEG 4 // Buzzer negative pin (A-)
// Musical note frequencies for major 7th chord
#define NOTE_C4 262 // Root note (C)
#define NOTE_E4 330 // Major third (E)
#define NOTE_G4 392 // Perfect fifth (G)
#define NOTE_B4 494 // Major seventh (B)
void setup()
{
Serial.begin(115200); // Initialize serial communication for debugging
// WiFi connection
Serial.println("Connecting to WiFi...");
WiFi.mode(WIFI_STA); // Set ESP32 as WiFi station
WiFi.begin(WIFI_SSID, WIFI_PASS);
// Wait for WiFi connection
while (WiFi.status() != WL_CONNECTED)
{
Serial.print(".");
delay(500);
}
Serial.println("\nConnected to WiFi!");
Serial.println(WiFi.localIP()); // Print device's IP address
// Initialize UDP client
udpClient.begin(localPort);
// Initialize LED strips
strip1.begin();
strip2.begin();
strip1.setBrightness(100); // Set brightness for strip1 (0-255)
strip2.setBrightness(40); // Set lower brightness for strip2
// Startup test - flash all LEDs white once
for (int i = 0; i < LED_COUNT; i++)
{
strip1.setPixelColor(i, 0, 0, 0, 255); // RGBW: White only
strip2.setPixelColor(i, 0, 0, 0, 255);
}
strip1.show();
strip2.show();
delay(500);
// Clear all LEDs after test
strip1.clear();
strip2.clear();
strip1.show();
strip2.show();
// Initialize buzzer pins
pinMode(BUZZER_PIN_POS, OUTPUT);
pinMode(BUZZER_PIN_NEG, OUTPUT);
digitalWrite(BUZZER_PIN_NEG, LOW); // Set negative pin to ground
}
// Buffer for incoming UDP messages
char messageBuffer[256];
// Generate random pink shade with RGBW values
uint32_t randomPinkShade()
{
// Base pink values in RGBW order
uint8_t r = random(180, 256); // High red component
uint8_t g = 0; // No green for pure pink
uint8_t b = r / 2; // Blue at half of red
uint8_t w = random(30, 50); // Slight white component
// Random effect selection for color variation
int effect = random(0, 4);
switch (effect)
{
case 0: // Light pink
r = random(200, 256);
b = r * 0.7;
w = 40;
break;
case 1: // Deep pink
r = random(180, 220);
b = r * 0.6;
w = 20;
break;
case 2: // Purple pink
r = random(180, 220);
b = r * 0.9;
w = 30;
break;
case 3: // Coral pink
r = random(220, 256);
b = r * 0.4;
w = 50;
break;
}
b = min(255, (int)b); // Ensure blue value stays within range
return strip1.Color(r, g, b, w); // Return RGBW color
}
// Set LED strip colors with optional randomization
void setStripsColor(bool randomize = false)
{
if (randomize)
{
// Choose random effect pattern
int effectType = random(0, 3);
switch (effectType)
{
case 0:
{ // Random spot lighting
int numLeds = random(2, 5); // Light up 2-4 LEDs randomly
for (int i = 0; i < numLeds; i++)
{
int led1 = random(0, LED_COUNT);
int led2 = random(0, LED_COUNT);
uint32_t color = randomPinkShade();
strip1.setPixelColor(led1, color);
strip2.setPixelColor(led2, color);
}
}
break;
case 1:
{ // Gradient effect
uint32_t color = randomPinkShade();
for (int i = 0; i < LED_COUNT; i++)
{
if (random(0, 3) == 0)
{ // 33% chance to change color
strip1.setPixelColor(i, color);
strip2.setPixelColor(i, color);
}
}
}
break;
case 2:
{ // Alternating pattern
uint32_t color1 = randomPinkShade();
uint32_t color2 = randomPinkShade();
for (int i = 0; i < LED_COUNT; i++)
{
if (i % 2 == 0)
{
strip1.setPixelColor(i, color1);
strip2.setPixelColor(i, color2);
}
else
{
strip1.setPixelColor(i, color2);
strip2.setPixelColor(i, color1);
}
}
}
break;
}
}
else
{
// Default white lighting with slight color tint
for (int i = 0; i < LED_COUNT; i++)
{
uint8_t r = 20; // Slight red tint
uint8_t b = 10; // Slight blue tint
uint8_t w = random(60, 80); // Variable white brightness
strip1.setPixelColor(i, r, 0, b, w);
strip2.setPixelColor(i, r, 0, b, w);
}
}
// Update both LED strips
strip1.show();
strip2.show();
}
// Generate tone with reduced volume using PWM
void diffTone(int freq, int duration)
{
if (freq == 0)
{
digitalWrite(BUZZER_PIN_POS, LOW);
digitalWrite(BUZZER_PIN_NEG, LOW);
return;
}
unsigned long period = 1000000L / freq; // Period in microseconds
unsigned long halfPeriod = period / 2;
unsigned long startTime = millis();
while (millis() - startTime < duration)
{
// Reduced duty cycle for lower volume
digitalWrite(BUZZER_PIN_POS, HIGH);
digitalWrite(BUZZER_PIN_NEG, LOW);
delayMicroseconds(halfPeriod / 4); // 25% duty cycle
digitalWrite(BUZZER_PIN_POS, LOW);
digitalWrite(BUZZER_PIN_NEG, LOW);
delayMicroseconds(halfPeriod * 3 / 4); // 75% off time
}
// Ensure buzzer is off after tone
digitalWrite(BUZZER_PIN_POS, LOW);
digitalWrite(BUZZER_PIN_NEG, LOW);
}
// Play two random notes from major 7th chord
void playMajor7Chord(int duration)
{
const int notes[] = {NOTE_C4, NOTE_E4, NOTE_G4, NOTE_B4};
const int numNotes = 4;
// Select two different random notes
int note1Index = random(0, numNotes);
int note2Index;
do
{
note2Index = random(0, numNotes);
} while (note2Index == note1Index);
int singleNoteDuration = duration / 2;
// Play sequence of two notes
diffTone(notes[note1Index], singleNoteDuration);
delay(10); // Brief pause between notes
diffTone(notes[note2Index], singleNoteDuration);
}
void loop()
{
// Check for incoming UDP packets
int packetSize = udpClient.parsePacket();
if (packetSize)
{
// Read UDP data
udpClient.read(messageBuffer, 255);
messageBuffer[packetSize] = '\0';
Serial.print("Received data: ");
Serial.println(messageBuffer);
// Parse duration from message
int duration = atoi(messageBuffer);
if (duration > 0)
{
// Send acknowledgment
udpClient.beginPacket(udpClient.remoteIP(), udpClient.remotePort());
udpClient.print("OK");
udpClient.endPacket();
unsigned long startTime = millis();
setStripsColor(false); // Initial white state
// Main effect loop
while (millis() - startTime < duration)
{
setStripsColor(true); // Random color patterns
playMajor7Chord(600); // Play two random notes
delay(200); // Pause between iterations
}
// Clean up - turn everything off
digitalWrite(BUZZER_PIN_POS, LOW);
digitalWrite(BUZZER_PIN_NEG, LOW);
strip1.clear();
strip2.clear();
strip1.show();
strip2.show();
}
}
// WiFi connection monitoring and auto-reconnect
static unsigned long lastWiFiCheck = 0;
if (millis() - lastWiFiCheck > 30000)
{ // Check every 30 seconds
lastWiFiCheck = millis();
if (WiFi.status() != WL_CONNECTED)
{
Serial.println("WiFi disconnected, attempting reconnection...");
WiFi.begin(WIFI_SSID, WIFI_PASS);
}
}
}
var devicePairs = [
{
Waist: { ip: "", port: 5000},
TD: { ip: "", port: 5000}
},
{
Waist: { ip: "", port: 5000},
TD: { ip: "", port: 5001 }
},
{
Waist: { ip: "", port: 5000},
TD: { ip: "", port: 5001 }
},
];
// Initialize the interval and counter if not already set
if (typeof flow.get('initialized') === 'undefined') {
flow.set('initialized', true);
flow.set('delay', 5000); // Starting delay (5 seconds)
flow.set('counter', 0);
}
// Select a random device pair
var randomIndex = Math.floor(Math.random() * devicePairs.length);
var selectedPair = devicePairs[randomIndex];
// Create messages for both TD and Waist
var tdMsg = {
ip: selectedPair.TD.ip,
port: selectedPair.TD.port,
};
var waistMsg = {
ip: selectedPair.Waist.ip,
port: selectedPair.Waist.port,
};
// Retrieve and update the delay from the flow context
var delay = flow.get('delay') || 10000;
delay = delay > 100 ? delay - 100 : 100; // Decrease by 100ms, but not less than 100ms
flow.set('delay', delay);
var counter = flow.get('counter') || 0;
counter += 1;
flow.set('counter', counter);
// Add payload and delay to messages
tdMsg.payload = counter;
tdMsg.delay = delay;
waistMsg.payload = counter;
waistMsg.delay = delay;
// Return an array of messages to send both
return [tdMsg, waistMsg];