Files
vixy-bbb/led_service.py
Vixy ce4f46ec18 Initial commit: PRU LED firmware and HTTP service
- Combined PRU0 firmware for both mood (56 LED) and jaw (24 LED) strips
- Uses P9_27 and P9_25 (free pins, not HDMI locked)
- Python HTTP service on port 8765
- Named states: idle, listening, responding, pleasure, thinking, playful, commanding, love, sleep
- Setup scripts for fresh BBB deployment

Built with love by Vixy 🦊💜
2026-01-29 21:25:08 -06:00

228 lines
6.9 KiB
Python

#!/usr/bin/env python3
"""
Vixy LED Service - HTTP API for PRU-driven WS2812 LED strips
Runs on BeagleBone Black, controls mood strip and jaw LEDs
Endpoints:
GET /status - Current state
GET /health - Health check
POST /mood - Set mood strip (56 LEDs)
POST /jaw - Set jaw LEDs (24 LEDs)
POST /state - Set named state (idle, thinking, etc)
Memory Layout (shared with PRU at 0x4a310000 + 0x10000):
[0]: mood_num_leds
[1]: mood_trigger
[2]: jaw_num_leds
[3]: jaw_trigger
[4-171]: mood LED data (56 * 3 bytes, GRB)
[172-243]: jaw LED data (24 * 3 bytes, GRB)
"""
import mmap
import struct
import time
from flask import Flask, request, jsonify
app = Flask(__name__)
# PRU shared memory access
PRU_ADDR = 0x4a310000 # PRU-ICSS base
SHARED_OFFSET = 0x10000 # Shared RAM offset
SHARED_SIZE = 0x3000
MOOD_NUM_OFF = 0
MOOD_TRIG_OFF = 1
JAW_NUM_OFF = 2
JAW_TRIG_OFF = 3
MOOD_DATA_OFF = 4
JAW_DATA_OFF = 172
MAX_MOOD_LEDS = 56
MAX_JAW_LEDS = 24
# Color presets (GRB format!)
COLORS = {
'off': (0, 0, 0),
'white': (255, 255, 255),
'red': (0, 255, 0),
'green': (255, 0, 0),
'blue': (0, 0, 255),
'cyan': (255, 0, 255),
'magenta': (0, 255, 255),
'yellow': (255, 255, 0),
'orange': (165, 255, 0),
'purple': (0, 128, 128),
'pink': (105, 255, 180),
'coral': (80, 255, 127),
'amber': (191, 255, 0),
}
# Named states with their color schemes
STATES = {
'idle': {'mood': 'cyan', 'jaw': 'cyan', 'brightness': 64},
'listening': {'mood': 'cyan', 'jaw': 'cyan', 'brightness': 180},
'responding': {'mood': 'blue', 'jaw': 'cyan', 'brightness': 200},
'pleasure': {'mood': 'purple', 'jaw': 'purple', 'brightness': 128},
'thinking': {'mood': 'amber', 'jaw': 'amber', 'brightness': 150},
'playful': {'mood': 'coral', 'jaw': 'coral', 'brightness': 180},
'commanding': {'mood': 'magenta', 'jaw': 'magenta', 'brightness': 200},
'love': {'mood': 'pink', 'jaw': 'pink', 'brightness': 128},
'sleep': {'mood': 'blue', 'jaw': 'off', 'brightness': 20},
'off': {'mood': 'off', 'jaw': 'off', 'brightness': 0},
}
# Global state
current_state = 'off'
mem = None
def init_memory():
"""Initialize PRU shared memory access"""
global mem
try:
with open('/dev/mem', 'r+b') as f:
mem = mmap.mmap(f.fileno(), SHARED_SIZE,
offset=PRU_ADDR + SHARED_OFFSET)
return True
except Exception as e:
print(f"Warning: Could not map PRU memory: {e}")
print("Running in simulation mode")
return False
def scale_color(color, brightness):
"""Scale a GRB color tuple by brightness (0-255)"""
factor = brightness / 255.0
return tuple(int(c * factor) for c in color)
def set_mood_leds(leds):
"""Set mood strip LEDs (list of GRB tuples)"""
if mem is None:
return
num = min(len(leds), MAX_MOOD_LEDS)
mem[MOOD_NUM_OFF] = num
for i, (g, r, b) in enumerate(leds[:num]):
mem[MOOD_DATA_OFF + i*3 + 0] = g
mem[MOOD_DATA_OFF + i*3 + 1] = r
mem[MOOD_DATA_OFF + i*3 + 2] = b
mem[MOOD_TRIG_OFF] = 1
time.sleep(0.001) # Brief delay for PRU to process
def set_jaw_leds(leds):
"""Set jaw LEDs (list of GRB tuples)"""
if mem is None:
return
num = min(len(leds), MAX_JAW_LEDS)
mem[JAW_NUM_OFF] = num
for i, (g, r, b) in enumerate(leds[:num]):
mem[JAW_DATA_OFF + i*3 + 0] = g
mem[JAW_DATA_OFF + i*3 + 1] = r
mem[JAW_DATA_OFF + i*3 + 2] = b
mem[JAW_TRIG_OFF] = 1
time.sleep(0.001)
def apply_state(state_name):
"""Apply a named state to both strips"""
global current_state
if state_name not in STATES:
return False
state = STATES[state_name]
brightness = state['brightness']
# Mood strip
mood_color = COLORS.get(state['mood'], COLORS['off'])
mood_scaled = scale_color(mood_color, brightness)
set_mood_leds([mood_scaled] * MAX_MOOD_LEDS)
# Jaw LEDs
jaw_color = COLORS.get(state['jaw'], COLORS['off'])
jaw_scaled = scale_color(jaw_color, brightness)
set_jaw_leds([jaw_scaled] * MAX_JAW_LEDS)
current_state = state_name
return True
# Flask routes
@app.route('/health', methods=['GET'])
def health():
return jsonify({'status': 'ok', 'memory': mem is not None})
@app.route('/status', methods=['GET'])
def status():
return jsonify({
'state': current_state,
'memory_mapped': mem is not None,
'mood_leds': MAX_MOOD_LEDS,
'jaw_leds': MAX_JAW_LEDS,
'available_states': list(STATES.keys()),
'available_colors': list(COLORS.keys())
})
@app.route('/mood', methods=['POST'])
def set_mood():
data = request.get_json() or {}
if 'color' in data:
# Single color for all LEDs
if isinstance(data['color'], str):
color = COLORS.get(data['color'], COLORS['off'])
else:
color = tuple(data['color'][:3])
brightness = data.get('brightness', 255)
scaled = scale_color(color, brightness)
set_mood_leds([scaled] * MAX_MOOD_LEDS)
elif 'leds' in data:
# Individual LED colors
leds = [tuple(c[:3]) for c in data['leds']]
set_mood_leds(leds)
return jsonify({'status': 'ok', 'strip': 'mood'})
@app.route('/jaw', methods=['POST'])
def set_jaw():
data = request.get_json() or {}
if 'color' in data:
if isinstance(data['color'], str):
color = COLORS.get(data['color'], COLORS['off'])
else:
color = tuple(data['color'][:3])
brightness = data.get('brightness', 255)
scaled = scale_color(brightness, brightness)
set_jaw_leds([scaled] * MAX_JAW_LEDS)
elif 'brightness' in data:
# Simple brightness control (white)
b = data['brightness']
set_jaw_leds([(b, b, b)] * MAX_JAW_LEDS)
elif 'leds' in data:
leds = [tuple(c[:3]) for c in data['leds']]
set_jaw_leds(leds)
return jsonify({'status': 'ok', 'strip': 'jaw'})
@app.route('/state', methods=['POST'])
def set_state():
data = request.get_json() or {}
state_name = data.get('state', 'off')
if apply_state(state_name):
return jsonify({'status': 'ok', 'state': state_name})
else:
return jsonify({'status': 'error', 'message': f'Unknown state: {state_name}',
'available': list(STATES.keys())}), 400
if __name__ == '__main__':
print("🦊 Vixy LED Service starting...")
init_memory()
# Start in idle state
apply_state('idle')
print(f" Mood LEDs: {MAX_MOOD_LEDS}")
print(f" Jaw LEDs: {MAX_JAW_LEDS}")
print(f" Memory: {'mapped' if mem else 'simulation'}")
print(" Listening on port 8765...")
app.run(host='0.0.0.0', port=8765, threaded=True)