diff --git a/README.md b/README.md index ef44a62..9745ca5 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,19 @@ # PiXelTubes +CREATE DATABASE IF NOT EXISTS pixeltube_db; + +USE pixeltube_db; + +CREATE TABLE IF NOT EXISTS tubes ( + id INT AUTO_INCREMENT PRIMARY KEY, + mac_address VARCHAR(17) NOT NULL UNIQUE, + universe INT NOT NULL, + dmx_address INT NOT NULL, + CONSTRAINT mac_address_format CHECK (LENGTH(mac_address) = 17 AND mac_address REGEXP '([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})') +); + +create user 'pxm'@'localhost' IDENTIFIED by 'pixel'; + +grant all privileges on pixeltube_db . * to 'pxm'@'localhost'; + +flush privileges; \ No newline at end of file diff --git a/client/main.py b/client/main.py new file mode 100644 index 0000000..9919a21 --- /dev/null +++ b/client/main.py @@ -0,0 +1,107 @@ +import socket +import os +import wifi +from pythonosc import udp_client +from neopixel import * +import threading +import requests +import json + +# Replace with your server's IP address and port +SERVER_IP = '192.168.0.1' # Change to the actual IP of the PiXelTube Master +SERVER_PORT = 5000 # Change to the port your Flask app is running on + +# Dynamically obtain the MAC address of the WLAN interface +wlan_mac_address = ':'.join(['{:02x}'.format((int(os.popen(f'cat /sys/class/net/wlan0/address').read().split(':'))[i]),) for i in range(6)]) + +# Replace with the GPIO pin connected to the data input of the WS2812B LED strip +LED_STRIP_PIN = 18 +LED_COUNT = 60 + +# Global variables for LED strip control +strip = Adafruit_NeoPixel(LED_COUNT, LED_STRIP_PIN, 800000, 10, False) +strip.begin() + +def register_tube(): + # Register or reauthenticate the tube with the server + try: + response = requests.post(f'http://{SERVER_IP}:{SERVER_PORT}/register_tube', data={'mac_address': wlan_mac_address}) + data = response.json() + if data.get('success'): + print('Tube registered successfully.') + else: + print(f'Registration failed: {data.get("message")}') + except requests.RequestException as e: + print(f'Registration failed: {e}') + +def is_connected_to_wifi(): + try: + ssid = wifi.current() + return ssid is not None + except wifi.exceptions.InterfaceError: + return False + +def listen_to_artnet(universe, dmx_address): + # Set up Art-Net client + client = udp_client.SimpleUDPClient(SERVER_IP, SERVER_PORT) + + # Listen to Art-Net messages + while True: + try: + # Receive Art-Net message + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: + sock.bind(('0.0.0.0', 6454)) # Listen on all interfaces + data, addr = sock.recvfrom(1024) + + # Process Art-Net message + universe_id = int.from_bytes(data[14:15], byteorder='big') + dmx_start_address = int.from_bytes(data[15:17], byteorder='big') + + if universe_id == universe and dmx_start_address <= dmx_address <= dmx_start_address + 2 * LED_COUNT: + # Extract RGB values from Art-Net packet + r = data[17] + g = data[18] + b = data[19] + + # Map DMX address to LED index + led_index = (dmx_address - dmx_start_address) // 3 + + # Update LED strip + strip.setPixelColor(led_index, Color(r, g, b)) + strip.show() + + # Send confirmation to the server + client.send_message('/acknowledge', {'tube_id': wlan_mac_address, 'led_index': led_index}) + + except Exception as e: + print(f"Error: {e}") + +def get_assigned_params(): + try: + response = requests.get(f'http://{SERVER_IP}:{SERVER_PORT}/get_assigned_params/{wlan_mac_address}') + data = response.json() + if data.get('success'): + return data.get('universe'), data.get('dmx_address') + else: + print(f'Failed to fetch assigned parameters: {data.get("message")}') + return None, None + except requests.RequestException as e: + print(f'Failed to fetch assigned parameters: {e}') + return None, None + +if __name__ == "__main__": + # Connect to Wi-Fi + if is_connected_to_wifi(): + # Register/reauthenticate the tube + register_tube() + + # Fetch assigned universe and DMX address + assigned_universe, assigned_dmx_address = get_assigned_params() + + if assigned_universe is not None and assigned_dmx_address is not None: + # Start a thread for listening to Art-Net messages + art_net_thread = threading.Thread(target=listen_to_artnet, args=(assigned_universe, assigned_dmx_address)) + art_net_thread.start() + + # Wait for the thread to finish (you can add more logic here as needed) + art_net_thread.join() diff --git a/requirements.txt b/requirements.txt index 64c849b..149b53a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,6 @@ Flask==2.3.2 Flask_MySQLdb==2.0.0 +neopixel==0.0.1 python_osc==1.8.3 +Requests==2.31.0 +wifi==0.3.8 diff --git a/server/app.py b/server/app.py index 200cae0..4bd796a 100644 --- a/server/app.py +++ b/server/app.py @@ -65,10 +65,6 @@ def register_tube_route(): register_tube(mac_address) return jsonify({'success': True, 'message': 'Tube registered successfully.'}) -# Your other routes and functions for toggling and retrieving tube information - -# ... (your registration system code) - # Function to retrieve registered tubes from the database def get_tubes(): cur = mysql.connection.cursor() @@ -77,16 +73,30 @@ def get_tubes(): cur.close() return tubes +@app.route('/get_assigned_params/', methods=['GET']) +def get_assigned_params(tube_id): + try: + cur = mysql.connection.cursor() + cur.execute("SELECT universe, dmx_address FROM tubes WHERE mac_address = %s", (tube_id,)) + result = cur.fetchone() + cur.close() + + if result: + universe, dmx_address = result + return jsonify({'success': True, 'universe': universe, 'dmx_address': dmx_address}) + else: + return jsonify({'success': False, 'message': 'Tube not found in the database'}) + except Exception as e: + return jsonify({'success': False, 'message': f'Error: {e}'}) + # Index route for the web interface @app.route('/') def index(): tubes = get_tubes() return render_template('index.html', tubes=tubes) -# ... (your registration system code) - def main(): - app.run(host='127.0.0.1', port=5000) + app.run(host='0.0.0.0', port=5000) if __name__ == "__main__": main() diff --git a/server/templates/index.html b/server/templates/index.html index 3e76f5d..8f545ec 100644 --- a/server/templates/index.html +++ b/server/templates/index.html @@ -3,68 +3,128 @@ - - - - PiXelTube Master + PiXelTube Web Interface + + -
-

PiXelTube Master

- - - - - - - - - - - - - {% for tube in tubes %} - - - - - - - - - {% endfor %} - -
Tube IDIP AddressUniverseAddressToggleSettings
{{ tube[1] }}{{ tube[2] }}{{ tube[3] }}{{ tube[4] }} - - - -
- + +
+

PiXelTube Web Interface

+ + + + + + + + + + + + + + +
Tube IDUniverseDMX AddressActions
+ + + - - - - + + + // Populate the form with current settings + $.ajax({ + url: `/get_assigned_params/${tubeId}`, + method: 'GET', + success: function (data) { + $('#universeInput').val(data.universe); + $('#dmxAddressInput').val(data.dmx_address); + } + }); + + // Submit form on save button click + $('#tubeSettingsForm').off('submit').on('submit', function (event) { + event.preventDefault(); + const newUniverse = $('#universeInput').val(); + const newDmxAddress = $('#dmxAddressInput').val(); + + // Update tube settings + $.ajax({ + url: `/update_tube_settings/${tubeId}`, + method: 'POST', + contentType: 'application/json', + data: JSON.stringify({universe: newUniverse, dmx_address: newDmxAddress}), + success: function () { + // Close modal and refresh tube list + $('#tubeSettingsModal').modal('hide'); + populateTubeList(); + } + }); + }); + }; + + // Initial population of the tube list + populateTubeList(); + }); +