parent
9a6571bb24
commit
2312319401
17
README.md
17
README.md
|
@ -1,2 +1,19 @@
|
||||||
# PiXelTubes
|
# 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;
|
|
@ -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()
|
|
@ -1,3 +1,6 @@
|
||||||
Flask==2.3.2
|
Flask==2.3.2
|
||||||
Flask_MySQLdb==2.0.0
|
Flask_MySQLdb==2.0.0
|
||||||
|
neopixel==0.0.1
|
||||||
python_osc==1.8.3
|
python_osc==1.8.3
|
||||||
|
Requests==2.31.0
|
||||||
|
wifi==0.3.8
|
||||||
|
|
|
@ -65,10 +65,6 @@ def register_tube_route():
|
||||||
register_tube(mac_address)
|
register_tube(mac_address)
|
||||||
return jsonify({'success': True, 'message': 'Tube registered successfully.'})
|
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
|
# Function to retrieve registered tubes from the database
|
||||||
def get_tubes():
|
def get_tubes():
|
||||||
cur = mysql.connection.cursor()
|
cur = mysql.connection.cursor()
|
||||||
|
@ -77,16 +73,30 @@ def get_tubes():
|
||||||
cur.close()
|
cur.close()
|
||||||
return tubes
|
return tubes
|
||||||
|
|
||||||
|
@app.route('/get_assigned_params/<tube_id>', 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
|
# Index route for the web interface
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
tubes = get_tubes()
|
tubes = get_tubes()
|
||||||
return render_template('index.html', tubes=tubes)
|
return render_template('index.html', tubes=tubes)
|
||||||
|
|
||||||
# ... (your registration system code)
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
app.run(host='127.0.0.1', port=5000)
|
app.run(host='0.0.0.0', port=5000)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
|
@ -3,68 +3,128 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<!-- Bootstrap CSS from local folder -->
|
<title>PiXelTube Web Interface</title>
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='bootstrap/css/bootstrap.min.css') }}">
|
<!-- Include Bootstrap CSS (assuming you've downloaded it locally) -->
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
<link rel="stylesheet" href="/static/bootstrap.min.css">
|
||||||
<title>PiXelTube Master</title>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container mt-5">
|
|
||||||
<h1>PiXelTube Master</h1>
|
<div class="container mt-5">
|
||||||
<table class="table table-bordered mt-3">
|
<h1 class="mb-4">PiXelTube Web Interface</h1>
|
||||||
|
|
||||||
|
<!-- Tube List Table -->
|
||||||
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Tube ID</th>
|
<th scope="col">Tube ID</th>
|
||||||
<th>IP Address</th>
|
<th scope="col">Universe</th>
|
||||||
<th>Universe</th>
|
<th scope="col">DMX Address</th>
|
||||||
<th>Address</th>
|
<th scope="col">Actions</th>
|
||||||
<th>Toggle</th>
|
|
||||||
<th>Settings</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody id="tubeList">
|
||||||
{% for tube in tubes %}
|
<!-- Tube information will be dynamically added here -->
|
||||||
<tr>
|
|
||||||
<td>{{ tube[1] }}</td>
|
|
||||||
<td>{{ tube[2] }}</td>
|
|
||||||
<td>{{ tube[3] }}</td>
|
|
||||||
<td>{{ tube[4] }}</td>
|
|
||||||
<td>
|
|
||||||
<label class="switch">
|
|
||||||
<input type="checkbox" {% if tube[5] %}checked{% endif %} onchange="toggleTube('{{ tube[1] }}')">
|
|
||||||
<span class="slider"></span>
|
|
||||||
</label>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<button class="btn btn-primary" onclick="openSettings('{{ tube[1] }}')">Settings</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<button class="btn btn-success" onclick="toggleAll()">Toggle All</button>
|
|
||||||
|
<!-- Tube Settings Modal -->
|
||||||
|
<div class="modal fade" id="tubeSettingsModal" tabindex="-1" role="dialog" aria-labelledby="tubeSettingsModalLabel"
|
||||||
|
aria-hidden="true">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="tubeSettingsModalLabel">Tube Settings</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="tubeSettingsForm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="universeInput">Universe:</label>
|
||||||
|
<input type="number" class="form-control" id="universeInput" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="dmxAddressInput">DMX Address:</label>
|
||||||
|
<input type="number" class="form-control" id="dmxAddressInput" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bootstrap JS from local folder -->
|
</div>
|
||||||
<script src="{{ url_for('static', filename='bootstrap/js/bootstrap.bundle.min.js') }}"></script>
|
|
||||||
<!-- Your custom script -->
|
<!-- Include Bootstrap JS and jQuery (assuming you've downloaded them locally) -->
|
||||||
<script>
|
<script src="/static/jquery.min.js"></script>
|
||||||
function toggleTube(tubeId) {
|
<script src="/static/bootstrap.bundle.min.js"></script>
|
||||||
fetch(`/toggle_tube/${tubeId}`, { method: 'POST' })
|
<script>
|
||||||
.then(response => response.json())
|
$(document).ready(function () {
|
||||||
.then(data => console.log(data));
|
// Function to populate the tube list
|
||||||
|
function populateTubeList() {
|
||||||
|
$.ajax({
|
||||||
|
url: '/get_tube_list',
|
||||||
|
method: 'GET',
|
||||||
|
success: function (data) {
|
||||||
|
$('#tubeList').empty();
|
||||||
|
for (const tube of data.tubes) {
|
||||||
|
$('#tubeList').append(`
|
||||||
|
<tr>
|
||||||
|
<td>${tube.mac_address}</td>
|
||||||
|
<td>${tube.universe}</td>
|
||||||
|
<td>${tube.dmx_address}</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-primary btn-sm" onclick="openTubeSettingsModal('${tube.mac_address}')">Settings</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleAll() {
|
// Function to open the tube settings modal
|
||||||
fetch('/toggle_all', { method: 'POST' })
|
window.openTubeSettingsModal = function (tubeId) {
|
||||||
.then(response => response.json())
|
$('#tubeSettingsModal').modal('show');
|
||||||
.then(data => console.log(data));
|
// Set the modal title
|
||||||
}
|
$('#tubeSettingsModalLabel').text(`Tube Settings - ${tubeId}`);
|
||||||
|
|
||||||
function openSettings(tubeId) {
|
// Populate the form with current settings
|
||||||
// Implement your settings functionality here
|
$.ajax({
|
||||||
alert(`Open settings for Tube ${tubeId}`);
|
url: `/get_assigned_params/${tubeId}`,
|
||||||
|
method: 'GET',
|
||||||
|
success: function (data) {
|
||||||
|
$('#universeInput').val(data.universe);
|
||||||
|
$('#dmxAddressInput').val(data.dmx_address);
|
||||||
}
|
}
|
||||||
</script>
|
});
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
Loading…
Reference in New Issue