#!/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)