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 🦊💜
This commit is contained in:
227
led_service.py
Normal file
227
led_service.py
Normal file
@@ -0,0 +1,227 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user