- 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 🦊💜
228 lines
6.9 KiB
Python
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)
|