5 Commits

Author SHA1 Message Date
355d113e32 Fix rainbow pattern synchronization in LED bar
- Use controller's step for synchronization instead of internal step counter
- Rainbow pattern now syncs with controller timing like n_chase pattern
- Prevents rainbow from running independently and out of sync
- Uses beat_index % 256 for full color wheel cycling
2025-09-19 01:29:48 +12:00
d715af4344 Fix n_chase pattern to properly chase through all LED positions
- Replace oscillating behavior with proper chasing movement
- Use pattern_step for internal tracking instead of controller's step
- Calculate position relative to chase head: (i - pattern_step) % num_leds
- Chase head moves through all LED positions with n3 step multiplier
- n1 controls width of lit chase segment
2025-09-19 00:22:25 +12:00
67c4a1a6f6 Update LED bar to handle message type field
- Process 't' field to distinguish between beat ('b') and update ('u') messages
- Beat messages: execute pattern immediately using current parameters
- Update messages: only update parameters, don't execute pattern
- Maintains backward compatibility with default to beat if 't' not specified
- Enables proper synchronization between controller and bars
2025-09-18 22:10:23 +12:00
748ad4b507 Add n3 step rate functionality to patterns 2025-09-18 20:35:21 +12:00
1275d60aaa Make alternating pattern timing independent of n1
- Changed alternating pattern to return delay/2 instead of delay
- Each phase now lasts delay/2, making full cycle equal to delay
- n1 now only controls ON/OFF segment width, not timing
2025-09-18 19:11:35 +12:00
5 changed files with 210 additions and 26 deletions

71
8_BAR_SETUP.md Normal file
View File

@@ -0,0 +1,71 @@
# 8-LED Bar System Setup
This system supports 8 LED bars working together, each with unique names "100" through "107".
## Quick Setup
### 1. Configure Each LED Bar
Each LED bar needs a unique name. Run the configuration script on each bar:
```bash
python configure_bar.py
```
Then enter the bar name (100, 101, 102, etc.) when prompted.
### 2. Update Bar Names (Optional)
To change the bar names, edit `/home/jimmy/projects/lighting-controller/src/bar_config.py`:
```python
LED_BAR_NAMES = [
"100", # Bar 1
"101", # Bar 2
"102", # Bar 3
"103", # Bar 4
"104", # Bar 5
"105", # Bar 6
"106", # Bar 7
"107", # Bar 8
]
```
### 3. Default Settings
All bars use the same default settings defined in `bar_config.py`:
```python
DEFAULT_BAR_SETTINGS = {
"pattern": "pulse",
"delay": 100,
"colors": [(0, 255, 0)], # Default green
"brightness": 100,
"num_leds": 200,
"n1": 10,
"n2": 10,
"n3": 1,
"n": 0,
}
```
## How It Works
1. **Lighting Controller** sends ESP-NOW messages to all bars simultaneously
2. **Each LED Bar** listens for messages addressed to its unique name
3. **All bars** receive the same pattern/color/brightness settings
4. **Synchronized effects** across all 8 bars
## Current Features
- ✅ All bars show the same pattern simultaneously
- ✅ Individual bar addressing (100-107)
- ✅ Optimized JSON payloads with defaults deduplication
- ✅ Easy configuration via `bar_config.py`
- ✅ MIDI control for all bars
- ✅ n3 step rate functionality
## Future Enhancements
- Sequential patterns (bar 1 → bar 2 → bar 3...)
- Wave effects across bars
- Individual bar control
- Master/slave synchronization
- Physical arrangement awareness

58
configure_bar.py Normal file
View File

@@ -0,0 +1,58 @@
#!/usr/bin/env python3
"""
LED Bar Configuration Script
Updates the settings.json file for each LED bar with its unique name
"""
import json
import os
# LED Bar names/IDs
LED_BAR_NAMES = ["100", "101", "102", "103", "104", "105", "106", "107"]
def update_bar_settings(bar_name, settings_file="settings.json"):
"""Update the settings.json file with the bar name"""
if not os.path.exists(settings_file):
print(f"Error: {settings_file} not found")
return False
# Read current settings
with open(settings_file, 'r') as f:
settings = json.load(f)
# Update the name
settings["name"] = bar_name
# Write back to file
with open(settings_file, 'w') as f:
json.dump(settings, f, indent=4)
print(f"Updated {settings_file} with name: {bar_name}")
return True
def main():
print("LED Bar Configuration Script")
print("=" * 40)
print("Available bar names:", LED_BAR_NAMES)
print()
while True:
print("Enter bar name to configure (or 'quit' to exit):")
bar_name = input("> ").strip()
if bar_name.lower() == 'quit':
break
if bar_name not in LED_BAR_NAMES:
print(f"Invalid bar name. Must be one of: {LED_BAR_NAMES}")
continue
if update_bar_settings(bar_name):
print(f"Successfully configured LED bar as '{bar_name}'")
else:
print("Failed to update settings")
print()
if __name__ == "__main__":
main()

View File

@@ -1,5 +1,4 @@
import asyncio
import aioespnow
import patterns
from settings import Settings
from web import web
@@ -18,13 +17,19 @@ def main():
settings = Settings()
print(settings)
if settings.get("color_order", "rgb") == "rbg":
color_order = (1, 5, 3)
else:
color_order = (1, 3, 5)
patterns = Patterns(settings["led_pin"], settings["num_leds"], selected="off")
sta_if = network.WLAN(network.STA_IF)
sta_if.active(True)
e = espnow.ESPNow()
e.config(rxbuf=1024)
e.active(True)
# Increase buffer size for 8-bar payloads (default 526 bytes might be too small) # Set to 1KB to handle larger multi-bar payloads
wdt = machine.WDT(timeout=10000)
wdt.feed()
@@ -47,20 +52,31 @@ def main():
defaults = data.get("d", {})
bar = data.get(settings.get("name"), {})
patterns.brightness = bar.get("brightness", defaults.get("brightness", patterns.brightness))
patterns.delay = bar.get("delay", defaults.get("delay", patterns.delay))
colors = bar.get("colors", defaults.get("colors", patterns.colors))
patterns.colors = [tuple(int(color[i:i+2], 16) for i in settings.color_order) for color in colors]
# Check message type
message_type = defaults.get("t", "b") # Default to beat if not specified
# Always update parameters from message
patterns.brightness = bar.get("br", defaults.get("br", patterns.brightness))
patterns.delay = bar.get("dl", defaults.get("dl", patterns.delay))
patterns.colors = bar.get("cl", defaults.get("cl", patterns.colors))
patterns.n1 = bar.get("n1", defaults.get("n1", patterns.n1))
patterns.n2 = bar.get("n2", defaults.get("n2", patterns.n2))
patterns.step = bar.get("pattern_step", defaults.get("step", patterns.step))
patterns.n3 = bar.get("n3", defaults.get("n3", patterns.n3))
patterns.step = bar.get("s", defaults.get("s", patterns.step))
selected_pattern = bar.get("pattern", defaults.get("pattern", "off"))
if selected_pattern in patterns.patterns:
# Run the selected pattern ONCE in response to this message. Do not auto-tick elsewhere.
patterns.patterns[selected_pattern]()
# Only execute pattern if it's a beat message
if message_type == "b": # Beat message
selected_pattern = bar.get("pt", defaults.get("pt", "off"))
if selected_pattern in patterns.patterns:
# Run the selected pattern ONCE in response to this beat message
patterns.patterns[selected_pattern]()
else:
print(f"Pattern {selected_pattern} not found")
elif message_type == "u": # Update message
# Just update parameters, don't execute pattern
print(f"Parameters updated: brightness={patterns.brightness}, delay={patterns.delay}")
else:
print(f"Pattern {selected_pattern} not found")
print(f"Unknown message type: {message_type}")
except Exception as ex:
print(f"Failed to load espnow data {last_msg}: {ex}")
continue

View File

@@ -12,8 +12,11 @@ class Patterns(PatternBase): # Inherit from PatternBase
self.off_width = 2 # Default off width (so total segment is 3, matching original behavior)
self.n1 = 0 # Default start of fill range
self.n2 = self.num_leds - 1 # Default end of fill range
self.n3 = 1 # Default step factor
self.oneshot = False # New: One-shot flag for patterns like fill_range
self.patterns = {
"on": self.on,
"off": self.off,
"flicker": self.flicker,
"fill_range": self.fill_range,
"n_chase": self.n_chase,
@@ -22,21 +25,45 @@ class Patterns(PatternBase): # Inherit from PatternBase
"rainbow": self.rainbow,
"specto": self.specto,
"radiate": self.radiate,
# Shortened pattern names for optimized JSON payloads
"o": self.off,
"f": self.flicker,
"fr": self.fill_range,
"nc": self.n_chase,
"a": self.alternating,
"p": self.pulse,
"r": self.rainbow,
"s": self.specto,
"rd": self.radiate,
}
self.step = 0
def on(self):
"""Turn on all LEDs with current color"""
self.fill(self.apply_brightness(self.colors[0]))
self.n.write()
return self.delay
def off(self):
"""Turn off all LEDs"""
self.fill((0, 0, 0))
self.n.write()
return self.delay
def flicker(self):
current_time = utime.ticks_ms()
base_color = self.colors[0]
# Increase the range for flicker_brightness_offset
# Changed from self.brightness // 4 to self.brightness // 2 (or even self.brightness for max intensity)
# Use fixed minimum brightness of 10, flicker between 10 and full brightness
# Use n3 as step rate multiplier to control how fast patterns step
min_brightness = 10
step_rate = max(1, int(self.n3))
flicker_brightness_offset = random.randint(-int(self.brightness // 1.5), int(self.brightness // 1.5))
flicker_brightness = max(0, min(255, self.brightness + flicker_brightness_offset))
flicker_brightness = max(min_brightness, min(255, self.brightness + flicker_brightness_offset))
flicker_color = self.apply_brightness(base_color, brightness_override=flicker_brightness)
self.fill(flicker_color)
self.last_update = current_time
return max(1, int(self.delay // 5))
return max(1, int(self.delay // (5 * step_rate)))
def fill_range(self):
"""
@@ -61,6 +88,7 @@ class Patterns(PatternBase): # Inherit from PatternBase
A theater chase pattern using n1 for on-width and n2 for off-width.
"""
current_time = utime.ticks_ms()
step_rate = max(1, int(self.n3))
segment_length = self.n1 + self.n2
if segment_length == 0: # Avoid division by zero
self.fill((0,0,0))
@@ -68,13 +96,19 @@ class Patterns(PatternBase): # Inherit from PatternBase
self.last_update = current_time
return self.delay
# Use controller's step for synchronization, but scale it for chasing
chase_step = (self.step * step_rate) % self.num_leds
for i in range(self.num_leds):
if (i + self.pattern_step) % segment_length < self.n1:
# Calculate position relative to the chase head
pos_from_head = (i - chase_step) % self.num_leds
if pos_from_head < self.n1:
self.n[i] = self.apply_brightness(self.colors[0])
else:
self.n[i] = (0, 0, 0)
self.n.write()
self.pattern_step = (self.pattern_step + 1) % segment_length
# Don't update internal step - use controller's step for sync
self.last_update = current_time
return self.delay
@@ -108,8 +142,8 @@ class Patterns(PatternBase): # Inherit from PatternBase
self.n[i] = active_color
self.n.write()
self.step = (self.step + 1) % 2
return self.delay
# Don't update step - use the step value sent from controller for synchronization
return max(1, int(self.delay // 2))
def pulse(self):
@@ -166,11 +200,16 @@ class Patterns(PatternBase): # Inherit from PatternBase
pos -= 170
return (0, pos * 3, 255 - pos * 3)
step_rate = max(1, int(self.n3))
# Use controller's step for synchronization, scaled for rainbow cycling
rainbow_step = (self.step * step_rate) % 256
for i in range(self.num_leds):
rc_index = (i * 256 // max(1, self.num_leds)) + self.pattern_step
rc_index = (i * 256 // max(1, self.num_leds)) + rainbow_step
self.n[i] = self.apply_brightness(wheel(rc_index & 255))
self.n.write()
self.pattern_step = (self.pattern_step + 1) % 256
# Don't update internal step - use controller's step for sync
return max(1, int(self.delay // 5))
def specto(self):

View File

@@ -14,9 +14,9 @@ class Settings(dict):
def set_defaults(self):
self["led_pin"] = 4
self["num_leds"] = 100
self["num_leds"] = 59
self["color_order"] = "rgb"
self["name"] = f"3"
self["name"] = f"103"
def save(self):
try: