Files
moonlight-drive-in/docs/ESP32_RELAY_INTEGRATION.md

14 KiB

ESP32 Bluetooth Relay Integration Guide

Complete guide for integrating ESP32 8-channel Bluetooth relay controller with the Moonlight Drive-In Theatre system.

🎯 Overview

This integration allows the theatre software to automatically control:

  • House lights - Main theatre lighting
  • Screen lights - Illumination around the screen
  • Marquee lights - External entrance/sign lighting
  • Speaker power - Audio system power
  • Projector power - Video projector control
  • Effect lights - Special effect lighting
  • Concession lights - Snack bar area
  • Parking lights - Parking area illumination

📦 Prerequisites

Hardware Required

  1. ESP32 Development Board (with Bluetooth)
  2. 8-Channel Relay Module (5V)
  3. Wiring components (jumper wires, power supply)
  4. Computer with Bluetooth (for Python script communication)

Software Required

pip install pyserial

The ESP32 firmware is in the separate esp32-bluetooth-relay repository.

🔧 Hardware Setup

Step 1: Build ESP32 Relay Controller

Follow the complete guide in the esp32-bluetooth-relay repository:

  • Wire ESP32 to relay module
  • Upload firmware to ESP32
  • Test basic relay operation

Step 2: Connect Loads to Relays

Map your theatre equipment to relays (default configuration):

Relay Function Theatre Equipment
1 House Lights Main overhead lights
2 Screen Lights Screen border lighting
3 Marquee Lights Entrance sign
4 Speaker Power Audio amplifier
5 Projector Power Video projector
6 Effect Lights RGB/effect lighting
7 Concession Lights Snack bar
8 Parking Lights Outdoor parking

⚠️ SAFETY: Only qualified electricians should wire AC-powered equipment. Start with low-voltage DC loads for testing.

Step 3: Pair ESP32 with Computer

Windows:

  1. Open Bluetooth settings
  2. Search for "ESP32-Relay-8CH"
  3. Pair the device
  4. Note the COM port assigned (e.g., COM4)

Linux:

# Scan for ESP32
hcitool scan

# Pair with ESP32 (replace XX:XX... with actual MAC)
bluetoothctl
> pair XX:XX:XX:XX:XX:XX
> trust XX:XX:XX:XX:XX:XX
> connect XX:XX:XX:XX:XX:XX

# Bind to serial port
sudo rfcomm bind /dev/rfcomm0 XX:XX:XX:XX:XX:XX

🚀 Software Integration

Step 1: Install Python Dependencies

cd moonlight-drive-in
pip install pyserial

Step 2: Configure Theatre Automation

Edit config.py and add the relay configuration section:

# ESP32 Relay Controller Configuration
RELAY_CONFIG = {
    # Enable/disable features
    'enabled': True,
    'auto_theatre_lights': True,      # Auto dim lights during movies
    'auto_speaker_power': True,        # Auto power speakers
    'light_dim_delay': 5,              # Seconds before dimming
    
    # Serial port (None = auto-detect, or specify like 'COM4' or '/dev/rfcomm0')
    'relay_port': None,
    
    # Custom relay mapping (optional - override defaults)
    'relay_mapping': {
        'HOUSE_LIGHTS': 1,
        'SCREEN_LIGHTS': 2,
        'MARQUEE_LIGHTS': 3,
        'SPEAKER_POWER': 4,
        'PROJECTOR_POWER': 5,
        'EFFECT_LIGHTS': 6,
        'CONCESSION_LIGHTS': 7,
        'PARKING_LIGHTS': 8,
    }
}

Step 3: Modify MPV Player Integration

Edit mpv_seamless_player.py to add automation hooks:

# At top of file, add imports
from theatre_automation import TheatreAutomation, RelayFunction
import config

# In __init__ method, add:
self.theatre = None
if config.RELAY_CONFIG.get('enabled', False):
    try:
        self.theatre = TheatreAutomation(config.RELAY_CONFIG)
        if self.theatre.connect():
            logger.info("Theatre automation enabled")
        else:
            logger.warning("Failed to connect to theatre automation")
            self.theatre = None
    except Exception as e:
        logger.error(f"Theatre automation error: {e}")
        self.theatre = None

# Before playing trailer, add:
if self.theatre:
    self.theatre.movie_starting(is_trailer=True)

# After trailer ends, add:
if self.theatre:
    self.theatre.movie_ended(is_trailer=True)

# Before playing main movie, add:
if self.theatre:
    self.theatre.movie_starting(is_trailer=False)

# After movie ends, add:
if self.theatre:
    self.theatre.movie_ended(is_trailer=False)

# On shutdown/cleanup, add:
if self.theatre:
    self.theatre.disconnect()

Step 4: Add Web Interface Controls

Edit web_interface.py to add relay control routes:

from theatre_automation import TheatreAutomation, RelayFunction

# Add global theatre variable
theatre_automation = None

# Initialize in app startup
if config.RELAY_CONFIG.get('enabled'):
    theatre_automation = TheatreAutomation(config.RELAY_CONFIG)
    theatre_automation.connect()

# Add API endpoints
@app.route('/api/relay/status')
def relay_status():
    """Get status of all relays"""
    if theatre_automation and theatre_automation.connected:
        return jsonify(theatre_automation.get_status())
    return jsonify({'error': 'Theatre automation not connected'}), 503

@app.route('/api/relay/<function_name>/<state>')
def relay_control(function_name, state):
    """Control a specific relay"""
    if not theatre_automation or not theatre_automation.connected:
        return jsonify({'error': 'Not connected'}), 503
    
    try:
        function = RelayFunction[function_name.upper()]
        state_bool = state.lower() == 'on'
        
        if theatre_automation.set_relay(function, state_bool):
            return jsonify({'success': True})
        else:
            return jsonify({'error': 'Command failed'}), 500
    except KeyError:
        return jsonify({'error': 'Invalid function'}), 400

@app.route('/api/relay/effect/<effect_name>')
def relay_effect(effect_name):
    """Run a lighting effect"""
    if theatre_automation and theatre_automation.connected:
        theatre_automation.effect_sequence(effect_name)
        return jsonify({'success': True})
    return jsonify({'error': 'Not connected'}), 503

🧪 Testing

Test 1: Basic Connection

python esp32_relay_controller.py

This will:

  • List available serial ports
  • Connect to ESP32
  • Run interactive test mode

Test 2: Theatre Automation

python theatre_automation.py

This will:

  • Initialize theatre automation
  • Run a demo sequence (trailer → movie → closing)
  • Test all automation functions

Test 3: Integration Test

  1. Start the theatre software normally
  2. Watch for log messages: "Theatre automation enabled"
  3. Scan an NFC tag to start a movie
  4. Observe lights dimming after delay
  5. Movie ends and lights come back up

🎮 Manual Control

Command Line Control

from theatre_automation import TheatreAutomation, RelayFunction

theatre = TheatreAutomation()
theatre.connect()

# Manual relay control
theatre.set_relay(RelayFunction.HOUSE_LIGHTS, False)  # Lights off
theatre.set_relay(RelayFunction.SPEAKER_POWER, True)  # Speakers on

# Run effects
theatre.effect_sequence("flash")
theatre.effect_sequence("pulse")

# Get status
status = theatre.get_status()
print(status)

theatre.disconnect()

Web Dashboard Control

Add to dashboard.html:

<!-- Theatre Automation Controls -->
<div class="theatre-automation">
    <h2>Theatre Automation</h2>
    
    <div class="relay-grid">
        <div class="relay-control">
            <span>House Lights</span>
            <button onclick="controlRelay('HOUSE_LIGHTS', 'on')">ON</button>
            <button onclick="controlRelay('HOUSE_LIGHTS', 'off')">OFF</button>
        </div>
        
        <div class="relay-control">
            <span>Speaker Power</span>
            <button onclick="controlRelay('SPEAKER_POWER', 'on')">ON</button>
            <button onclick="controlRelay('SPEAKER_POWER', 'off')">OFF</button>
        </div>
        
        <!-- Add more controls for each relay... -->
    </div>
    
    <div class="effects">
        <button onclick="runEffect('flash')">Flash Effect</button>
        <button onclick="runEffect('pulse')">Pulse Effect</button>
        <button onclick="runEffect('marquee')">Marquee</button>
    </div>
</div>

<script>
function controlRelay(functionName, state) {
    fetch(`/api/relay/${functionName}/${state}`)
        .then(response => response.json())
        .then(data => {
            if (data.success) {
                console.log(`${functionName} turned ${state}`);
                updateStatus();
            }
        });
}

function runEffect(effectName) {
    fetch(`/api/relay/effect/${effectName}`)
        .then(response => response.json())
        .then(data => console.log('Effect complete'));
}

function updateStatus() {
    fetch('/api/relay/status')
        .then(response => response.json())
        .then(status => {
            // Update UI with relay states
            console.log('Theatre status:', status);
        });
}

// Update status every 5 seconds
setInterval(updateStatus, 5000);
</script>

📋 Automation Sequences

Movie Starting

  1. Wait for light_dim_delay (default 5 seconds)
  2. Turn off house lights
  3. Turn off screen lights (main movie only)
  4. Turn on speaker power

Movie Ending

  1. Turn on house lights
  2. Turn on screen lights
  3. Turn off speaker power (if no other content playing)

Intermission

  1. Turn on house lights
  2. Turn on concession lights
  3. Keep speakers powered

Closing

  1. Turn off speakers
  2. Turn off screen lights
  3. Turn off effect lights
  4. Keep parking and marquee lights on

Emergency

  1. Turn ALL lights ON immediately

⚙️ Customization

Change Relay Assignments

Edit config.py:

'relay_mapping': {
    'HOUSE_LIGHTS': 2,     # Changed from 1 to 2
    'SCREEN_LIGHTS': 1,     # Swapped with house lights
    # ... etc
}

Adjust Timing

'light_dim_delay': 10,      # Wait 10 seconds before dimming

Disable Auto-Features

'auto_theatre_lights': False,  # Manual light control only
'auto_speaker_power': False,   # Manual speaker control

Add Custom Functions

Edit theatre_automation.py:

class RelayFunction(Enum):
    # Add new function
    FOG_MACHINE = 5  # Use relay 5 for fog machine

# Add method to control it
def activate_fog(self, duration=5):
    self.set_relay(RelayFunction.FOG_MACHINE, True)
    time.sleep(duration)
    self.set_relay(RelayFunction.FOG_MACHINE, False)

🐛 Troubleshooting

ESP32 Not Found

Problem: "No serial ports found"

Solutions:

  1. Ensure ESP32 is powered on
  2. Verify Bluetooth pairing
  3. Check Bluetooth COM port in Device Manager (Windows)
  4. Run hcitool scan to verify device (Linux)

Connection Fails

Problem: "Failed to connect to relay controller"

Solutions:

  1. Check COM port in config matches actual port
  2. Verify no other program is using the port
  3. Try manual port specification: relay_port: 'COM4'
  4. Test with standalone script: python esp32_relay_controller.py

Relays Don't Respond

Problem: Commands sent but relays don't switch

Solutions:

  1. Test ESP32 with Serial Bluetooth Terminal app
  2. Check ESP32 is running and Bluetooth is active
  3. Verify relay module has power
  4. Check wiring between ESP32 and relay module

Lights Don't Dim

Problem: Automation enabled but lights stay on

Solutions:

  1. Check auto_theatre_lights: True in config
  2. Verify relay wiring to light switch/relay
  3. Check relay polarity (active HIGH vs LOW)
  4. Test manual control: theatre.set_relay(RelayFunction.HOUSE_LIGHTS, False)

📚 API Reference

ESP32RelayController

controller = ESP32RelayController()

# Connection
controller.connect(port='COM4')
controller.disconnect()

# Basic Control
controller.turn_on(1)        # Turn on relay 1
controller.turn_off(1)       # Turn off relay 1
controller.toggle(1)         # Toggle relay 1
controller.all_on()          # All relays on
controller.all_off()         # All relays off

# Status
controller.get_status()      # Request status update
controller.get_relay_state(1)  # Get cached state of relay 1
controller.get_all_states()  # Get all cached states

# Callbacks
controller.register_callback('connect', on_connect_func)
controller.register_callback('disconnect', on_disconnect_func)
controller.register_callback('status_update', on_status_func)

TheatreAutomation

theatre = TheatreAutomation(config)

# Connection
theatre.connect(port='COM4')
theatre.disconnect()

# Automation Sequences
theatre.movie_starting(is_trailer=True)
theatre.movie_ended(is_trailer=False)
theatre.intermission()
theatre.closing_sequence()
theatre.emergency_lights_on()

# Manual Control
theatre.set_relay(RelayFunction.HOUSE_LIGHTS, True)
theatre.toggle_relay(RelayFunction.EFFECT_LIGHTS)
theatre.effect_sequence("flash")

# Status
status = theatre.get_status()

🎬 Example Workflows

Unattended Mode Integration

# In unattend mode cycle handler
def cycle_content():
    if theatre:
        # Starting trailer
        theatre.movie_starting(is_trailer=True)
    
    play_trailer()
    
    if theatre:
        theatre.movie_ended(is_trailer=True)
        time.sleep(2)  # Brief pause
        theatre.movie_starting(is_trailer=False)
    
    play_movie()
    
    if theatre:
        theatre.movie_ended(is_trailer=False)

NFC Tag Triggered Effects

# Map NFC tags to lighting effects
NFC_EFFECTS = {
    'AAAAAAAA': 'flash',
    'BBBBBBBB': 'pulse',
    'CCCCCCCC': 'marquee',
}

def handle_nfc(tag_id):
    if tag_id in NFC_EFFECTS and theatre:
        theatre.effect_sequence(NFC_EFFECTS[tag_id])

🔐 Safety Notes

  1. AC Power: Only qualified electricians should work with mains voltage
  2. Fusing: Use appropriate fuses for all loads
  3. Ratings: Don't exceed relay current/voltage ratings
  4. Failsafe: Ensure system fails to a safe state (lights ON)
  5. Testing: Test thoroughly with low-voltage loads first

📖 Additional Resources


Version: 1.0
Last Updated: December 2025
Integration Status: Ready for testing