Quantum Flux Calibrator

Quantum Flux Calibrator

This tutorial builds an advanced logic puzzle using four potentiometer-based levers and a NeoPixel matrix display. Players must position four levers to specific values that satisfy a complex set of mathematical conditions. The puzzle features real-time WebSocket communication, auto-calibration capabilities, and stunning visual feedback through an 8-LED matrix that displays both reference colors and current lever states.

Gameplay Mechanics

The Challenge

Players must discover lever positions that satisfy four interconnected mathematical conditions. The puzzle requires understanding relationships between levers A, B, C, and D through logical deduction.

The puzzle's difficulty comes from the constrained solution space: while there are 256 possible combinations (4 positions × 4 levers = 4⁴), the mathematical conditions eliminate most invalid combinations, leaving only a few valid solutions.

Hidden complexity: The conditions create dependencies - changing one lever often requires adjusting others to maintain validity.

How Players Interact

  • Observe the matrix: Bottom row shows reference colors (Blue, Yellow, Purple, Cyan) representing positions 1-4
  • Adjust levers: Turn each potentiometer to change its position value (1-4)
  • Watch feedback: Top row displays current lever positions using matching colors
  • Listen for tones: Each position change plays a unique musical note (C4, E4, G4, C5)
  • Solve the logic: Find the combination that satisfies all four mathematical conditions
  • Visual confirmation: Matrix performs rotating color animation when solved

Technical drawing of the Quantum Flux Calibrator


The Four Conditions Explained

Direct Relationships

  • Condition A: Lever A + Lever B must equal 5
  • Condition D: Lever D must equal 5 - Lever A

These create a mirror relationship: as A increases, B and D must decrease proportionally.

Sequential & Global Constraints

  • Condition B: Lever B must be exactly one less than Lever C
  • Condition C: The sum of all four levers must be divisible by 3

These add complexity by linking adjacent levers and requiring a specific total sum.

Solution Strategy Tips

  • Start with Condition A: Only four valid combinations exist (1+4, 2+3, 3+2, 4+1)
  • Use Condition D to immediately determine Lever D once A is set
  • Apply Condition B to find valid C values based on your chosen B
  • Check if the total sum satisfies Condition C (divisible by 3)
  • Listen for the ascending tone sequence when moving through positions 1→2→3→4

Example Valid Solution:

A=2, B=3, C=4, D=3

  • ✓ A + B = 2 + 3 = 5 (Condition A satisfied)
  • ✓ B = C - 1 → 3 = 4 - 1 (Condition B satisfied)
  • ✓ Sum = 2 + 3 + 4 + 3 = 12, which is divisible by 3 (Condition C satisfied)
  • ✓ D = 5 - A → 3 = 5 - 2 (Condition D satisfied)

Feedback Systems

Visual Feedback

  • Real-time color display of lever positions
  • Success animation: rotating colors
  • Special display mode when levers are in sequence

Audio Feedback

  • Position tones provide immediate confirmation
  • Success melody plays when solved
  • Musical progression helps players learn positions

Network Feedback

  • Real-time updates to game master dashboard
  • Progress tracking for hint system
  • Integration with room-wide effects

Required Components

Hardware

  • 1x Arduino Uno R4 WiFi
  • 4x 10kΩ Potentiometers (lever controls)
  • 8x WS2812B NeoPixel LEDs (4x2 matrix)
  • 2x Push buttons (calibration start/confirm)
  • 1x Piezo buzzer
  • Jumper wires
  • Breadboard
  • 470Ω resistor (Between LED strip and Arduino pin)

Software Requirements

  • Arduino IDE (2.0 or later)
  • Arduino UNO R4 Board Package
  • Adafruit NeoPixel Library
  • WiFiS3 Library (built-in)
  • ArduinoJson Library (6.x)
  • WebSocketsClient Library

Wiring Diagram

Quantum Flux Calibrator wiring diagram

Potentiometers

  • Lever A → A0
  • Lever B → A1
  • Lever C → A2
  • Lever D → A3
  • All VCC → 5V
  • All GND → GND

Control Buttons

  • Calibration Start → Pin 2
  • Calibration Confirm → Pin 3
  • Internal pull-ups enabled
  • Other pin → GND

Output Devices

  • NeoPixel Data → Pin 6
  • NeoPixel VCC → 5V
  • NeoPixel GND → GND
  • Buzzer → Pin 8
  • Buzzer GND → GND
Important: The Arduino R4 WiFi has different WiFi capabilities than ESP32. Ensure you're using the WiFiS3 library specific to the R4. The NeoPixels should be powered from a separate 5V supply if using more than a few LEDs to avoid overloading the Arduino's regulator.

The Code

1Include Required Libraries

Import all necessary libraries for LED control, WiFi connectivity, and data storage:

// LED control and data storage
#include <Adafruit_NeoPixel.h>
#include <EEPROM.h>

// Network and communication libraries
#include <WiFiS3.h>
#include <ArduinoJson.h>
#include <WebSocketsClient.h>

What each library does:

Adafruit_NeoPixel.h: Controls WS2812B individually addressable LEDs. Provides color manipulation and efficient data transmission for the visual feedback matrix.

EEPROM.h: Allows persistent storage of calibration data. The calibration values survive power cycles, eliminating the need to recalibrate after each restart.

WiFiS3.h: Arduino R4 WiFi's specific networking library. Handles connection to wireless networks with support for static IP configuration.

ArduinoJson.h: Efficient JSON parsing and generation for structured communication with the escape room control server.

WebSocketsClient.h: Enables real-time bidirectional communication, perfect for instant status updates and remote monitoring.


2Network Configuration and Pin Definitions

Configure your network settings and hardware connections:

// WiFi and WebSocket settings
const char* ssid = "Network SSID";
const char* password = "Network Password";
const char* wsHost = "172.16.12.2"; 
const int wsPort = 1880;
const char* wsPath = "/events";

// Static IP configuration
IPAddress staticIP(172, 16, 12, 12);
IPAddress gateway(172, 16, 12, 1);
IPAddress subnet(255, 255, 255, 0);

// Puzzle identification
const char* PUZZLE_ID = "QFC";

// Pin definitions
#define POT_A A0
#define POT_B A1
#define POT_C A2
#define POT_D A3
#define CALIBRATION_START_BUTTON 2
#define CALIBRATION_CONFIRM_BUTTON 3
#define NEOPIXEL_PIN 6
#define BUZZER_PIN 8

Configuration explained:

Static IP: Ensures consistent network address for reliable server communication and easier debugging.

PUZZLE_ID: "QFC" stands for "Quantum Flux Calibrator" - unique identifier in the escape room ecosystem.

Analog pins: Four potentiometers provide 1024 discrete positions each (10-bit ADC), mapped to 4 positions per lever.


3LED Matrix and Color Configuration

Set up the visual feedback system with color-coded levers:

// Matrix definitions
#define MATRIX_WIDTH 4
#define MATRIX_HEIGHT 2
#define NUM_PIXELS (MATRIX_WIDTH * MATRIX_HEIGHT)
#define BRIGHTNESS 50

// Audio feedback frequencies (in Hertz)
const int TONE_POS_1 = 262;  // C4
const int TONE_POS_2 = 330;  // E4
const int TONE_POS_3 = 392;  // G4
const int TONE_POS_4 = 523;  // C5

// Color-blind friendly palette (stored in PROGMEM)
const uint32_t COLOR_BLUE PROGMEM = 0x0000FF;
const uint32_t COLOR_YELLOW PROGMEM = 0xFFFF00;
const uint32_t COLOR_PURPLE PROGMEM = 0x800080;
const uint32_t COLOR_CYAN PROGMEM = 0x00FFFF;

Adafruit_NeoPixel pixels(NUM_PIXELS, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);

Matrix organization:

Layout: 4x2 matrix with bottom row showing reference colors and top row showing current lever positions.

Color mapping: Position 1=Blue, 2=Yellow, 3=Purple, 4=Cyan. Colors chosen for maximum distinguishability including for colorblind players.

Audio feedback: Each position plays a unique tone in a C major triad progression, providing multi-sensory confirmation.

PROGMEM storage: Color constants stored in flash memory to conserve RAM on the Arduino.


4Calibration System

Implement a sophisticated calibration routine for accurate lever positioning:

// Calibration structure
struct CalibrationData {
  int positions[4][4];  // [lever][position_1, position_2, position_3, position_4]
  int tolerances[4];    // Tolerance for each lever
} calibration;

void calibratePotentiometers() {
    Serial.println("Starting calibration...");
    
    // Iterate through positions first, then levers
    for (int position = 1; position <= 4; position++) {
        for (int lever = 0; lever < 4; lever++) {
            // Visual feedback - highlight active lever
            for (int i = 0; i < 4; i++) {
                setMatrixPixel(i, 1, i == lever ? 
                    getLeverColor(position) : COLOR_DEFAULT);
            }
            pixels.show();
            
            // Wait for button press
            while (digitalRead(CALIBRATION_CONFIRM_BUTTON) == HIGH) {
                // Breathing effect while waiting
                int brightness = (sin(millis() / 500.0) + 1) * 127;
                pixels.setBrightness(brightness);
                pixels.show();
            }
            
            // Read and store the potentiometer value
            int value = analogRead(A0 + lever);
            calibration.positions[lever][position - 1] = value;
            
            // Play confirmation tone
            playPositionTone(position);
        }
    }
    
    // Save to EEPROM
    EEPROM.put(0, calibration);
}

Calibration process:

Position-first approach: Calibrates all levers for position 1, then 2, etc., ensuring consistent positioning across levers.

Visual guidance: Active lever highlighted with breathing effect, reference colors shown on bottom row.

Persistent storage: Calibration data saved to EEPROM, surviving power cycles and reducing setup time.

Audio confirmation: Each successful calibration point plays its corresponding tone for immediate feedback.


5Complex Logic Solution

The puzzle requires satisfying four mathematical conditions simultaneously:

Solution Checking Logic

// Function to check complex logic
bool checkComplexLogic() {
    return checkConditionA() && 
           checkConditionB() && 
           checkConditionC() && 
           checkConditionD();
}

// Condition A: Sum of A and B equals 5
bool checkConditionA() {
    return (leverA + leverB) == 5;
}

// Condition B: B is one less than C
bool checkConditionB() {
    return leverB == (leverC - 1);
}

// Condition C: Sum of all levers is divisible by 3
bool checkConditionC() {
    return (leverA + leverB + leverC + leverD) % 3 == 0;
}

// Condition D: D equals 5 minus A
bool checkConditionD() {
    return leverD == (5 - leverA);
}

Mathematical constraints:

Condition A: Creates a dependency between levers A and B (possible combinations: 1+4, 2+3, 3+2, 4+1)

Condition B: Links lever B to C with a sequential relationship

Condition C: Requires the total sum to be divisible by 3, adding a global constraint

Condition D: Creates a mirror relationship between A and D

Solution: The constraints have multiple valid solutions, but one example is: A=1, B=4, C=5 (invalid), requiring players to find A=2, B=3, C=4, D=3 (sum=12, divisible by 3)

Visual Feedback System

// Update lever feedback with special mode for ordered sequence
void updateLeverFeedback(bool leversInOrder) {
    if (!puzzleSolved) {
        if (leversInOrder) {
            // Reverse display order when levers are 1-2-3-4
            setMatrixPixel(0, 1, getLeverColor(leverD));
            setMatrixPixel(1, 1, getLeverColor(leverC));
            setMatrixPixel(2, 1, getLeverColor(leverB));
            setMatrixPixel(3, 1, getLeverColor(leverA));
        } else {
            // Normal display order
            setMatrixPixel(0, 1, getLeverColor(leverA));
            setMatrixPixel(1, 1, getLeverColor(leverB));
            setMatrixPixel(2, 1, getLeverColor(leverC));
            setMatrixPixel(3, 1, getLeverColor(leverD));
        }
    }
}

6WebSocket Communication

Real-time status updates to the escape room control system:

// Send initialization message upon connection
void sendInitializationMessage() {
    if (!connected) return;
    
    StaticJsonDocument<200> doc;
    doc["shortName"] = PUZZLE_ID;
    doc["type"] = "INITIATION";
    
    JsonObject encoders = doc.createNestedObject("encoders");
    encoders["1"] = leverA;
    encoders["2"] = leverB;
    encoders["3"] = leverC;
    encoders["4"] = leverD;
    
    doc["solved"] = puzzleSolved;
    
    char jsonBuffer[200];
    serializeJson(doc, jsonBuffer);
    webSocket.sendTXT(jsonBuffer);
}

// Send lever update when position changes
void sendLeverUpdateMessage(int encoderId, int position) {
    if (!connected) return;
    
    StaticJsonDocument<200> doc;
    doc["shortName"] = PUZZLE_ID;
    doc["type"] = "UPDATE";
    doc["encoder"] = encoderId;
    doc["position"] = position;
    
    char jsonBuffer[200];
    serializeJson(doc, jsonBuffer);
    webSocket.sendTXT(jsonBuffer);
}

Message types:

INITIATION: Sent on connection to register puzzle and report all lever positions

UPDATE: Sent whenever any lever changes position for real-time monitoring

SOLVE: Sent when puzzle is solved or becomes unsolved after being solved

All messages use efficient StaticJsonDocument with pre-allocated buffers to minimize memory fragmentation

Testing Your Quantum Flux Calibrator

1Initial Hardware Verification

Upload this test sketch to verify all components:

void setup() {
    Serial.begin(9600);
    
    // Test potentiometers
    Serial.println("Testing Potentiometers...");
    
    // Test buttons with pull-ups
    pinMode(CALIBRATION_START_BUTTON, INPUT_PULLUP);
    pinMode(CALIBRATION_CONFIRM_BUTTON, INPUT_PULLUP);
    
    // Test NeoPixel matrix
    pixels.begin();
    pixels.setBrightness(50);
    
    // Cycle through all colors on all pixels
    uint32_t colors[] = {COLOR_BLUE, COLOR_YELLOW, COLOR_PURPLE, COLOR_CYAN};
    for(int color = 0; color < 4; color++) {
        for(int i = 0; i < NUM_PIXELS; i++) {
            pixels.setPixelColor(i, pgm_read_dword(&colors[color]));
            pixels.show();
            delay(100);
        }
    }
    
    // Test buzzer with scale
    tone(BUZZER_PIN, TONE_POS_1, 100);
    delay(150);
    tone(BUZZER_PIN, TONE_POS_2, 100);
    delay(150);
    tone(BUZZER_PIN, TONE_POS_3, 100);
    delay(150);
    tone(BUZZER_PIN, TONE_POS_4, 100);
}

void loop() {
    // Print potentiometer values
    Serial.print("A:");
    Serial.print(analogRead(POT_A));
    Serial.print(" B:");
    Serial.print(analogRead(POT_B));
    Serial.print(" C:");
    Serial.print(analogRead(POT_C));
    Serial.print(" D:");
    Serial.println(analogRead(POT_D));
    
    // Print button states
    if(digitalRead(CALIBRATION_START_BUTTON) == LOW) {
        Serial.println("Start button pressed!");
    }
    if(digitalRead(CALIBRATION_CONFIRM_BUTTON) == LOW) {
        Serial.println("Confirm button pressed!");
    }
    
    delay(500);
}

What this test verifies:

  • NeoPixel matrix: Each LED lights up sequentially in all four colors, confirming data connection and color accuracy
  • Buzzer: Plays the four position tones in sequence (C4, E4, G4, C5) to verify audio feedback
  • Potentiometers: Continuously displays raw analog values (0-1023) for each lever in Serial Monitor
  • Buttons: Reports when either calibration button is pressed

Expected Serial Monitor output:

Testing Potentiometers...
A:512 B:256 C:768 D:1023
A:512 B:256 C:768 D:1023
Start button pressed!
A:512 B:256 C:768 D:1023
Confirm button pressed!
A:512 B:256 C:768 D:1023

Interpreting the results:

  • Potentiometer values: Should change smoothly from 0-1023 as you rotate. Jumpy values indicate poor connections or damaged pots
  • LED sequence: All 8 LEDs should light in order. Missing LEDs indicate wiring issues or damaged pixels
  • Button response: Messages should appear immediately when pressed. No response indicates wiring or pull-up issues
  • Audio tones: Should hear four distinct ascending notes. No sound indicates buzzer wiring issues
Troubleshooting tip: If potentiometer values are stuck at 0 or 1023, check the power connections. If values jump erratically, try adding 0.1µF capacitors between each analog pin and ground to filter noise.

2Troubleshooting Common Issues

Potentiometer Issues

  • Check wiper (middle pin) connections
  • Verify 5V and GND on outer pins
  • Test raw values with Serial.print
  • Clean potentiometer contacts if erratic
  • Ensure stable mounting to prevent drift

NeoPixel Problems

  • Verify data pin connection (Pin 6)
  • Check 5V power supply adequacy
  • Test with simple single-LED sketch
  • Ensure correct pixel count (8)
  • Add 470Ω resistor on data line

WiFi Connection

  • Verify WiFiS3 library (not WiFi.h)
  • Check SSID and password spelling
  • Confirm static IP isn't in use
  • Monitor Serial for connection status
  • Test with DHCP before static IP

Calibration Problems

  • Clear EEPROM before first calibration
  • Ensure buttons have pull-ups enabled
  • Check button wiring (normally open)
  • Verify potentiometer full range motion
  • Allow time for value stabilization

3Serial Monitor Debug Output

The system provides detailed debug information during operation:

WiFi connected
IP address: 172.16.12.12
Gateway: 172.16.12.1
WebSocket connected to: /events

Current Values:
---------------
Lever A: Position 2
Lever B: Position 3
Lever C: Position 4
Lever D: Position 3
---------------
Checking conditions...
Condition A (A+B=5): PASS ✓
Condition B (B=C-1): PASS ✓
Condition C (Sum%3=0): PASS ✓
Condition D (D=5-A): PASS ✓
Success! Puzzle solved!

Sent: {"shortName":"QFC","type":"SOLVE","solved":true}

4Calibration Process

To calibrate the levers for accurate position detection:

  • Press and hold the calibration start button (Pin 2)
  • For each position (1-4), move all levers to that position when prompted
  • Press the confirm button (Pin 3) after positioning each lever
  • The breathing LED effect indicates waiting for confirmation
  • A tone plays after each successful calibration point
  • Calibration saves automatically to EEPROM when complete

The calibration data persists through power cycles, so you only need to calibrate once unless the hardware changes.

5Understanding the Zig-Zag Matrix Layout

The NeoPixel matrix uses a zig-zag wiring pattern:

// Matrix pixel arrangement (physical layout)
// Row 0 (bottom): [3] [2] [1] [0]  ← Right to left
// Row 1 (top):    [4] [5] [6] [7]  → Left to right

void setMatrixPixel(int x, int y, uint32_t color) {
    int pixelIndex;
    if (y % 2 == 0) {
        // Even rows run right to left
        pixelIndex = y * MATRIX_WIDTH + (MATRIX_WIDTH - 1 - x);
    } else {
        // Odd rows run left to right
        pixelIndex = y * MATRIX_WIDTH + x;
    }
    pixels.setPixelColor(pixelIndex, color);
}

This wiring pattern is common in LED matrices as it allows for continuous data flow without long return wires.

6Testing WebSocket Communication

To verify WebSocket connectivity independently:

// Simple WebSocket test function
void testWebSocket() {
    if (connected) {
        StaticJsonDocument<128> doc;
        doc["shortName"] = PUZZLE_ID;
        doc["type"] = "TEST";
        doc["timestamp"] = millis();
        
        char buffer[128];
        serializeJson(doc, buffer);
        webSocket.sendTXT(buffer);
        
        Serial.println("Test message sent");
    } else {
        Serial.println("WebSocket not connected");
    }
}

Get in touch!

We are currently looking for educators and students to collaborate with on future projects.

Get in Touch!

hello@escapehumber.ca