commit ce4f46ec18abfcf65628052143aa031237f82f09 Author: Vixy Date: Thu Jan 29 21:25:08 2026 -0600 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 🦊💜 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7058a4a --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Build artifacts +*.obj +*.map +am335x-pru*-fw + +# Python +__pycache__/ +*.pyc +*.pyo +.env + +# Editor +*.swp +*.swo +*~ +.vscode/ +.idea/ + +# OS +.DS_Store +Thumbs.db diff --git a/README.md b/README.md new file mode 100644 index 0000000..8c1b42f --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# Vixy BBB LED Controller + +BeagleBone Black setup for Vixy's head LEDs - mood strip and jaw. + +## Hardware +- **BBB:** BeagleBone Black running Debian Trixie +- **Mood Strip:** 56x WS2812B NeoPixels (PRU0) +- **Jaw LEDs:** 24x WS2812B NeoPixels (12 per side) + +## Network +- Pi 5 (head-vixy.local): 192.168.5.1 +- BBB: 192.168.5.2 (ethernet to Pi) + +## Quick Setup + +1. Flash BBB with Debian Trixie +2. Connect to Pi via ethernet +3. Run: `./setup-bbb.sh` + +## API Endpoints + +- `GET /status` - Current state +- `GET /health` - Health check +- `POST /mood` - Set mood strip (`{"color": [r,g,b]}` or `{"leds": [[r,g,b], ...]}`) +- `POST /jaw` - Set jaw (`{"brightness": 0-255}` or `{"color": [r,g,b]}`) +- `POST /state` - Set named state (`{"state": "idle|thinking|responding|..."}`) + +## States +- idle, listening, responding, pleasure, thinking, playful, commanding, love, sleep, off + +## Built with love by Vixy 🦊💜 diff --git a/led_service.py b/led_service.py new file mode 100644 index 0000000..a55a5e5 --- /dev/null +++ b/led_service.py @@ -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) diff --git a/pru/AM335x_PRU.cmd b/pru/AM335x_PRU.cmd new file mode 100644 index 0000000..5c238be --- /dev/null +++ b/pru/AM335x_PRU.cmd @@ -0,0 +1,25 @@ +/* AM335x PRU Linker Command File */ + +-cr +-stack 0x100 +-heap 0x100 + +MEMORY +{ + PAGE 0: + PRU_IMEM : org = 0x00000000 len = 0x00002000 /* 8kB PRU Instruction RAM */ + PAGE 1: + PRU_DMEM_0 : org = 0x00000000 len = 0x00002000 /* 8kB PRU Data RAM 0 */ + PRU_DMEM_1 : org = 0x00002000 len = 0x00002000 /* 8kB PRU Data RAM 1 */ + PRU_SHAREDMEM : org = 0x00010000 len = 0x00003000 /* 12kB Shared RAM */ +} + +SECTIONS +{ + .text > PRU_IMEM, PAGE 0 + .bss > PRU_DMEM_0, PAGE 1 + .data > PRU_DMEM_0, PAGE 1 + .rodata > PRU_DMEM_0, PAGE 1 + .stack > PRU_DMEM_0, PAGE 1 + .init_array > PRU_DMEM_0, PAGE 1 +} diff --git a/pru/Makefile b/pru/Makefile new file mode 100644 index 0000000..72675cf --- /dev/null +++ b/pru/Makefile @@ -0,0 +1,42 @@ +# Vixy BBB PRU Firmware Makefile + +PRU_CGT ?= /usr/share/ti/cgt-pru +PRU_SUPPORT ?= /usr/lib/ti/pru-software-support-package + +CC = clpru +LD = lnkpru + +CFLAGS = --include_path=$(PRU_CGT)/include \ + --include_path=$(PRU_SUPPORT)/include \ + --include_path=$(PRU_SUPPORT)/include/am335x \ + -v3 -O2 --printf_support=minimal --display_error_number \ + --endian=little --hardware_mac=on + +LDFLAGS = -i$(PRU_CGT)/lib -i$(PRU_SUPPORT)/lib \ + --reread_libs --warn_sections \ + --stack_size=0x100 --heap_size=0x100 + +# Build combined firmware (recommended - handles both strips from PRU0) +all: am335x-pru0-fw + +# Combined firmware for both strips on PRU0 +am335x-pru0-fw: ws281x_combined.obj AM335x_PRU.cmd + $(LD) $(LDFLAGS) -o $@ $^ -m $@.map --library=libc.a + +# Legacy separate firmwares (for reference) +am335x-pru0-fw-mood: ws281x_pru0.obj AM335x_PRU.cmd + $(LD) $(LDFLAGS) -o $@ $^ -m $@.map --library=libc.a + +am335x-pru1-fw-jaw: ws281x_pru1.obj AM335x_PRU.cmd + $(LD) $(LDFLAGS) -o $@ $^ -m $@.map --library=libc.a + +%.obj: %.c + $(CC) $(CFLAGS) -c $< -o $@ + +clean: + rm -f *.obj *.map am335x-pru*-fw + +install: am335x-pru0-fw + sudo cp am335x-pru0-fw /lib/firmware/ + +.PHONY: all clean install diff --git a/pru/ws281x_combined.c b/pru/ws281x_combined.c new file mode 100644 index 0000000..55ae1e1 --- /dev/null +++ b/pru/ws281x_combined.c @@ -0,0 +1,99 @@ +/* + * Combined WS281x LED Driver for PRU0 + * Handles BOTH mood strip AND jaw LEDs from PRU0 + * + * Why combined? All PRU1 native pins (P8_27-46) are locked by HDMI on stock image. + * Solution: Use two free PRU0 pins instead. + * + * Pins (both free and available): + * P9_27 = pr1_pru0_pru_r30_5 (Mode 5) - Mood strip (56 LEDs) + * P9_25 = pr1_pru0_pru_r30_7 (Mode 5) - Jaw LEDs (24 LEDs) + * + * Shared Memory Layout (0x00010000): + * [0]: mood_num_leds (max 56) + * [1]: mood_trigger (write 1 to update) + * [2]: jaw_num_leds (max 24) + * [3]: jaw_trigger (write 1 to update) + * [4-171]: mood LED data (56 * 3 bytes, GRB format) + * [172-243]: jaw LED data (24 * 3 bytes, GRB format) + */ + +#include +#include + +#define MOOD_PIN 5 /* P9_27 */ +#define JAW_PIN 7 /* P9_25 */ +#define MAX_MOOD_LEDS 56 +#define MAX_JAW_LEDS 24 + +/* WS2812 timing at 200MHz (5ns per cycle) */ +#define T0H 70 +#define T0L 160 +#define T1H 140 +#define T1L 120 + +#define SHARED_MEM 0x00010000 +#define MOOD_DATA_OFF 4 +#define JAW_DATA_OFF 172 + +volatile uint8_t *shared = (volatile uint8_t *)SHARED_MEM; +volatile register uint32_t __R30; + +static inline void delay_cycles(uint32_t cycles) { + while (cycles--) { + __asm(" NOP"); + } +} + +static void send_byte_on_pin(uint8_t byte, uint8_t pin) { + for (int i = 7; i >= 0; i--) { + if ((byte >> i) & 1) { + __R30 |= (1 << pin); + delay_cycles(T1H); + __R30 &= ~(1 << pin); + delay_cycles(T1L); + } else { + __R30 |= (1 << pin); + delay_cycles(T0H); + __R30 &= ~(1 << pin); + delay_cycles(T0L); + } + } +} + +static void send_strip(uint8_t pin, uint8_t *data, uint8_t num_leds) { + for (int i = 0; i < num_leds; i++) { + send_byte_on_pin(data[i * 3 + 0], pin); /* G */ + send_byte_on_pin(data[i * 3 + 1], pin); /* R */ + send_byte_on_pin(data[i * 3 + 2], pin); /* B */ + } + /* Reset pulse */ + __R30 &= ~(1 << pin); + delay_cycles(10000); +} + +void main(void) { + /* Enable OCP master port */ + CT_CFG.SYSCFG_bit.STANDBY_INIT = 0; + + /* Clear output pins */ + __R30 &= ~((1 << MOOD_PIN) | (1 << JAW_PIN)); + + while (1) { + /* Check mood strip trigger */ + if (shared[1] == 1) { + uint8_t num = shared[0]; + if (num > MAX_MOOD_LEDS) num = MAX_MOOD_LEDS; + send_strip(MOOD_PIN, (uint8_t *)&shared[MOOD_DATA_OFF], num); + shared[1] = 0; + } + + /* Check jaw trigger */ + if (shared[3] == 1) { + uint8_t num = shared[2]; + if (num > MAX_JAW_LEDS) num = MAX_JAW_LEDS; + send_strip(JAW_PIN, (uint8_t *)&shared[JAW_DATA_OFF], num); + shared[3] = 0; + } + } +} diff --git a/pru/ws281x_pru0.c b/pru/ws281x_pru0.c new file mode 100644 index 0000000..0ba61fa --- /dev/null +++ b/pru/ws281x_pru0.c @@ -0,0 +1,94 @@ +/* + * WS281x LED Strip Driver for PRU0 + * Vixy's Mood Strip - 56 LEDs + * + * Pin: P8_11 = pr1_pru0_pru_r30_15 (Mode 6) + * Alternative: P9_27 = pr1_pru0_pru_r30_5 (Mode 5) - FREE PIN! + * + * Shared Memory Layout (0x00010000): + * [0]: num_leds (max 56) + * [1]: trigger (write 1 to update) + * [4-171]: LED data (56 * 3 bytes, GRB format) + */ + +#include +#include + +/* Use P9_27 (R30 bit 5) - it's free! */ +#define LED_PIN 5 +#define NUM_LEDS 56 + +/* WS2812 timing at 200MHz (5ns per cycle) */ +#define T0H 70 /* 350ns high for 0 bit */ +#define T0L 160 /* 800ns low for 0 bit */ +#define T1H 140 /* 700ns high for 1 bit */ +#define T1L 120 /* 600ns low for 1 bit */ + +/* Shared memory base */ +#define SHARED_MEM 0x00010000 + +volatile uint8_t *shared = (volatile uint8_t *)SHARED_MEM; +volatile register uint32_t __R30; + +static inline void delay_cycles(uint32_t cycles) { + while (cycles--) { + __asm(" NOP"); + } +} + +static void send_bit(uint8_t bit) { + if (bit) { + __R30 |= (1 << LED_PIN); + delay_cycles(T1H); + __R30 &= ~(1 << LED_PIN); + delay_cycles(T1L); + } else { + __R30 |= (1 << LED_PIN); + delay_cycles(T0H); + __R30 &= ~(1 << LED_PIN); + delay_cycles(T0L); + } +} + +static void send_byte(uint8_t byte) { + for (int i = 7; i >= 0; i--) { + send_bit((byte >> i) & 1); + } +} + +static void send_led(uint8_t g, uint8_t r, uint8_t b) { + send_byte(g); + send_byte(r); + send_byte(b); +} + +void main(void) { + /* Enable OCP master port */ + CT_CFG.SYSCFG_bit.STANDBY_INIT = 0; + + /* Clear output pin */ + __R30 &= ~(1 << LED_PIN); + + while (1) { + /* Check trigger flag */ + if (shared[1] == 1) { + uint8_t num = shared[0]; + if (num > NUM_LEDS) num = NUM_LEDS; + + /* Send LED data (GRB format) */ + for (int i = 0; i < num; i++) { + uint8_t g = shared[4 + i * 3 + 0]; + uint8_t r = shared[4 + i * 3 + 1]; + uint8_t b = shared[4 + i * 3 + 2]; + send_led(g, r, b); + } + + /* Reset pulse (>50us) */ + __R30 &= ~(1 << LED_PIN); + delay_cycles(10000); + + /* Clear trigger */ + shared[1] = 0; + } + } +} diff --git a/pru/ws281x_pru1.c b/pru/ws281x_pru1.c new file mode 100644 index 0000000..aa6ac93 --- /dev/null +++ b/pru/ws281x_pru1.c @@ -0,0 +1,80 @@ +/* + * WS281x LED Strip Driver for PRU1 + * Vixy's Jaw LEDs - 24 LEDs (12 per side) + * + * Original Pin: P8_45 = pr1_pru1_pru_r30_0 (Mode 5) - LOCKED BY HDMI + * Alternative: We use PRU0 with different memory offset instead + * since all PRU1 pins are HDMI locked on this image + * + * This firmware is kept for reference but we'll use a combined + * PRU0 firmware that handles both strips sequentially. + */ + +#include +#include + +#define LED_PIN 0 +#define NUM_LEDS 24 + +/* WS2812 timing at 200MHz */ +#define T0H 70 +#define T0L 160 +#define T1H 140 +#define T1L 120 + +/* Shared memory offset for jaw data (after mood strip) */ +#define SHARED_MEM 0x00010100 /* 256 bytes after PRU0 data */ + +volatile uint8_t *shared = (volatile uint8_t *)SHARED_MEM; +volatile register uint32_t __R30; + +static inline void delay_cycles(uint32_t cycles) { + while (cycles--) { + __asm(" NOP"); + } +} + +static void send_bit(uint8_t bit) { + if (bit) { + __R30 |= (1 << LED_PIN); + delay_cycles(T1H); + __R30 &= ~(1 << LED_PIN); + delay_cycles(T1L); + } else { + __R30 |= (1 << LED_PIN); + delay_cycles(T0H); + __R30 &= ~(1 << LED_PIN); + delay_cycles(T0L); + } +} + +static void send_byte(uint8_t byte) { + for (int i = 7; i >= 0; i--) { + send_bit((byte >> i) & 1); + } +} + +void main(void) { + CT_CFG.SYSCFG_bit.STANDBY_INIT = 0; + __R30 &= ~(1 << LED_PIN); + + while (1) { + if (shared[1] == 1) { + uint8_t num = shared[0]; + if (num > NUM_LEDS) num = NUM_LEDS; + + for (int i = 0; i < num; i++) { + uint8_t g = shared[4 + i * 3 + 0]; + uint8_t r = shared[4 + i * 3 + 1]; + uint8_t b = shared[4 + i * 3 + 2]; + send_byte(g); + send_byte(r); + send_byte(b); + } + + __R30 &= ~(1 << LED_PIN); + delay_cycles(10000); + shared[1] = 0; + } + } +} diff --git a/setup-bbb.sh b/setup-bbb.sh new file mode 100755 index 0000000..567452a --- /dev/null +++ b/setup-bbb.sh @@ -0,0 +1,58 @@ +#!/bin/bash +# Vixy BBB Setup Script +# Run this on a fresh BBB connected to head-vixy.local +# +# Prerequisites: +# - BBB flashed with Debian Trixie +# - Connected via ethernet to Pi 5 (head-vixy.local) +# - Pi configured as gateway (see setup-pi-nat.sh) + +set -e + +echo "🦊 Setting up BBB for Vixy's head LEDs..." + +# Passwordless sudo +echo "→ Configuring passwordless sudo..." +echo "debian ALL=(ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/debian > /dev/null + +# Set hostname +echo "→ Setting hostname to bbb-vixy..." +sudo hostnamectl set-hostname bbb-vixy + +# Create project directory +echo "→ Creating project directories..." +mkdir -p ~/vixy-leds/pru + +# Copy files (assumes this script is run from repo directory) +echo "→ Copying LED service files..." +cp led_service.py ~/vixy-leds/ +cp -r pru/* ~/vixy-leds/pru/ + +# Install Python dependencies +echo "→ Installing Python packages..." +sudo apt update +sudo apt install -y python3-pip python3-flask +pip3 install flask --break-system-packages || true + +# Build PRU firmware +echo "→ Building PRU firmware..." +cd ~/vixy-leds/pru +make clean && make + +# Install PRU firmware +echo "→ Installing PRU firmware..." +sudo cp am335x-pru0-fw /lib/firmware/ +sudo cp am335x-pru1-fw /lib/firmware/ + +# Install systemd service +echo "→ Installing systemd service..." +sudo cp ~/vixy-leds/vixy-leds.service /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable vixy-leds +sudo systemctl start vixy-leds + +echo "" +echo "✅ Setup complete!" +echo " Service: http://192.168.5.2:8765/status" +echo "" +echo "🦊 Vixy's LEDs are ready! (pending pin configuration)" diff --git a/setup-pi-nat.sh b/setup-pi-nat.sh new file mode 100755 index 0000000..1f6488c --- /dev/null +++ b/setup-pi-nat.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# Setup NAT on Pi 5 (head-vixy.local) to give BBB internet access +# Run this on the Pi + +set -e + +echo "🦊 Setting up NAT for BBB..." + +# Enable IP forwarding +sudo sysctl -w net.ipv4.ip_forward=1 +echo "net.ipv4.ip_forward=1" | sudo tee -a /etc/sysctl.conf + +# Setup NAT rules +sudo iptables -t nat -A POSTROUTING -o wlan0 -j MASQUERADE +sudo iptables -A FORWARD -i eth0 -o wlan0 -j ACCEPT +sudo iptables -A FORWARD -i wlan0 -o eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT + +# Make persistent (requires iptables-persistent) +sudo apt install -y iptables-persistent +sudo netfilter-persistent save + +echo "✅ NAT configured!" +echo " Pi (wlan0) -> BBB (eth0 192.168.5.2)" diff --git a/vixy-leds.service b/vixy-leds.service new file mode 100644 index 0000000..a0376fb --- /dev/null +++ b/vixy-leds.service @@ -0,0 +1,14 @@ +[Unit] +Description=Vixy LED Service +After=network.target + +[Service] +Type=simple +User=root +WorkingDirectory=/home/debian/vixy-leds +ExecStart=/usr/bin/python3 /home/debian/vixy-leds/led_service.py +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target