Browse Source

10/30/2024

master
Joe DiPrima 1 year ago
parent
commit
3b96813bbc
  1. 3
      .gitignore
  2. BIN
      bin/ffmpeg.exe
  3. 64
      dmx.py
  4. 26
      main.py
  5. 363
      sender.py

3
.gitignore

@ -1,3 +1,4 @@
venv
__pycache__
*.mkv
*.mkv
*.json

BIN
bin/ffmpeg.exe

64
dmx.py

@ -1,13 +1,14 @@
# dmx.py
from PyQt5.QtCore import QObject, QThread, pyqtSignal
import socket
import struct
import numpy as np
import subprocess
import time
import os
class DMXRecorder(QObject):
frame_ready = pyqtSignal(np.ndarray)
def __init__(self):
super().__init__()
self.thread = None
@ -23,6 +24,9 @@ class DMXRecorder(QObject):
self.recording_thread.finished.connect(self.recording_thread.deleteLater)
self.thread.finished.connect(self.thread.deleteLater)
# Connect frame_ready signal
self.recording_thread.frame_ready.connect(self.frame_ready)
self.thread.start()
def stop_recording(self):
@ -34,6 +38,7 @@ class DMXRecorder(QObject):
class RecordingThread(QObject):
finished = pyqtSignal()
frame_ready = pyqtSignal(np.ndarray)
def __init__(self):
super().__init__()
@ -69,9 +74,28 @@ class RecordingThread(QObject):
frame_interval = 1.0 / frame_rate
last_frame_time = time.time()
# Set up ffmpeg process
# Adjust the path to the ffmpeg executable
ffmpeg_executable = os.path.join(os.path.dirname(__file__), 'bin', 'ffmpeg')
# For Windows, add .exe extension
if os.name == 'nt':
ffmpeg_executable += '.exe'
# Ensure the ffmpeg executable is executable
# For Unix-like systems, set the execute permission if needed
if not os.access(ffmpeg_executable, os.X_OK):
if os.name != 'nt':
os.chmod(ffmpeg_executable, 0o755)
else:
# On Windows, os.access may not check execute permissions accurately
if not os.path.isfile(ffmpeg_executable):
print(f"ffmpeg executable not found at: {ffmpeg_executable}")
self.finished.emit()
return
# Set up ffmpeg process using the local executable path
ffmpeg_cmd = [
'ffmpeg',
ffmpeg_executable,
'-y',
'-f', 'rawvideo',
'-pix_fmt', 'gray',
@ -82,7 +106,12 @@ class RecordingThread(QObject):
'dmx_video.mkv'
]
self.ffmpeg_process = subprocess.Popen(ffmpeg_cmd, stdin=subprocess.PIPE)
try:
self.ffmpeg_process = subprocess.Popen(ffmpeg_cmd, stdin=subprocess.PIPE)
except Exception as e:
print(f"Error starting ffmpeg: {e}")
self.finished.emit()
return
universes_data = {}
last_received = {}
@ -93,7 +122,7 @@ class RecordingThread(QObject):
# Receive data
try:
data, addr = self.socket.recvfrom(1024)
data, addr = self.socket.recvfrom(2048)
if data.startswith(b'Art-Net'):
op_code = struct.unpack('<H', data[8:10])[0]
# OpCode for ArtDMX is 0x5000
@ -117,23 +146,34 @@ class RecordingThread(QObject):
# Assemble frame data
frame_data = b''
for universe in sorted(expected_universes):
dmx_data = universes_data.get(universe, b'\x00'*512)
if len(dmx_data) < 512:
dmx_data += b'\x00' * (512 - len(dmx_data))
dmx_data = universes_data.get(universe, b'\x00' * universe_size)
if len(dmx_data) < universe_size:
dmx_data += b'\x00' * (universe_size - len(dmx_data))
frame_data += dmx_data
# Convert frame_data to numpy array
frame_array = np.frombuffer(frame_data, dtype=np.uint8)
# Reshape to image dimensions
frame_array = frame_array.reshape((frame_height, frame_width))
# Write frame to ffmpeg stdin
self.ffmpeg_process.stdin.write(frame_array.tobytes())
if self.ffmpeg_process.stdin:
try:
self.ffmpeg_process.stdin.write(frame_array.tobytes())
except BrokenPipeError:
print("FFmpeg process has terminated unexpectedly.")
self.running = False
# Emit frame for visualization
self.frame_ready.emit(frame_array)
last_frame_time = current_time
time.sleep(0.001) # Sleep briefly to avoid maxing out CPU
finally:
# Clean up
self.ffmpeg_process.stdin.close()
self.ffmpeg_process.wait()
if self.ffmpeg_process and self.ffmpeg_process.stdin:
try:
self.ffmpeg_process.stdin.close()
self.ffmpeg_process.wait()
except Exception as e:
print(f"Error closing ffmpeg process: {e}")
self.socket.close()
self.finished.emit()

26
main.py

@ -1,8 +1,7 @@
# main.py
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QWidget, QVBoxLayout
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QWidget, QVBoxLayout, QLabel
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QImage, QPixmap, qRgb
from dmx import DMXRecorder
class MainWindow(QMainWindow):
@ -14,7 +13,7 @@ class MainWindow(QMainWindow):
def initUI(self):
self.setWindowTitle("DMX Recorder")
self.resize(300, 200)
self.resize(300, 400)
# Use dark theme
self.setStyleSheet('''
@ -61,23 +60,42 @@ class MainWindow(QMainWindow):
self.record_button = QPushButton("Record")
self.record_button.clicked.connect(self.toggle_recording)
self.video_label = QLabel()
self.video_label.setAlignment(Qt.AlignCenter)
self.layout.addWidget(self.video_label)
self.layout.addWidget(self.record_button)
def toggle_recording(self):
if not self.isRecording:
# Start recording
self.recorder = DMXRecorder()
self.recorder.frame_ready.connect(self.update_frame)
self.recorder.start_recording()
self.record_button.setText("Stop")
self.isRecording = True
else:
# Stop recording
if self.recorder:
self.recorder.frame_ready.disconnect(self.update_frame)
self.recorder.stop_recording()
self.recorder = None
self.record_button.setText("Record")
self.isRecording = False
def update_frame(self, frame_array):
# Convert the numpy array to QImage and display
height, width = frame_array.shape
# Convert the numpy array to QImage
q_img = QImage(frame_array.data, width, height, width, QImage.Format_Indexed8)
# Create a grayscale color table
color_table = [qRgb(i, i, i) for i in range(256)]
q_img.setColorTable(color_table)
# Scale image to fit the label
pixmap = QPixmap.fromImage(q_img).scaled(self.video_label.width(), self.video_label.height(), Qt.KeepAspectRatio)
# Set the pixmap to the label
self.video_label.setPixmap(pixmap)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()

363
sender.py

@ -1,55 +1,340 @@
import sys
import socket
import struct
import time
import threading
import json
import math
from PyQt5.QtWidgets import (
QApplication, QWidget, QPushButton, QVBoxLayout, QHBoxLayout,
QLabel, QLineEdit, QComboBox, QMessageBox
)
from PyQt5.QtCore import pyqtSignal, QObject
def send_dmx(universe=0, num_channels=512, fade_time=5):
UDP_IP = '255.255.255.255' # Broadcast address
UDP_PORT = 6454
class DMXController(QObject):
# Signal to notify parameter changes
params_changed = pyqtSignal()
# Create UDP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
def __init__(self):
super().__init__()
self.load_params()
self.running = False
self.lock = threading.Lock()
self.thread = None
sequence = 0
physical = 0
def load_params(self):
try:
with open('dmx_params.json', 'r') as f:
params = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
params = {
'ip_address': '255.255.255.255',
'port': 6454,
'universe': 0,
'num_channels': 512,
'fade_time': 5,
'pattern': 'Fade Up and Down',
'speed': 1.0 # Default speed for Moving Sine Wave
}
self.ip_address = params.get('ip_address', '255.255.255.255')
self.port = params.get('port', 6454)
self.universe = params.get('universe', 0)
self.num_channels = params.get('num_channels', 512)
self.fade_time = params.get('fade_time', 5)
self.pattern = params.get('pattern', 'Fade Up and Down')
self.speed = params.get('speed', 1.0)
# Fading parameters
fade_steps = 100
fade_interval = fade_time / fade_steps
def save_params(self):
params = {
'ip_address': self.ip_address,
'port': self.port,
'universe': self.universe,
'num_channels': self.num_channels,
'fade_time': self.fade_time,
'pattern': self.pattern,
'speed': self.speed
}
with open('dmx_params.json', 'w') as f:
json.dump(params, f, indent=4)
try:
while True:
def start(self):
if not self.running:
self.running = True
self.thread = threading.Thread(target=self.run)
self.thread.start()
def stop(self):
if self.running:
self.running = False
if self.thread is not None:
self.thread.join()
def run(self):
try:
# Create UDP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
UDP_IP = self.ip_address
UDP_PORT = self.port
universe = self.universe
sequence = 0
physical = 0
if self.pattern == 'Fade Up and Down':
self.fade_up_down(sock, UDP_IP, UDP_PORT, universe)
elif self.pattern == 'Moving Sine Wave':
self.moving_sine_wave(sock, UDP_IP, UDP_PORT, universe)
else:
print("Unknown pattern selected.")
except Exception as e:
print(f"Error in DMXController: {e}")
finally:
sock.close()
def fade_up_down(self, sock, UDP_IP, UDP_PORT, universe):
number_of_steps = 256
sleep_time = self.fade_time / number_of_steps
while self.running:
# Fade up
for value in range(0, 256):
dmx_data = [value] * num_channels
dmx_packet = build_artdmx_packet(sequence, physical, universe, dmx_data)
if not self.running:
break
dmx_data = [value] * self.num_channels
dmx_packet = self.build_artdmx_packet(universe, dmx_data)
sock.sendto(dmx_packet, (UDP_IP, UDP_PORT))
time.sleep(fade_interval / 256)
time.sleep(sleep_time)
# Fade down
for value in range(255, -1, -1):
dmx_data = [value] * num_channels
dmx_packet = build_artdmx_packet(sequence, physical, universe, dmx_data)
if not self.running:
break
dmx_data = [value] * self.num_channels
dmx_packet = self.build_artdmx_packet(universe, dmx_data)
sock.sendto(dmx_packet, (UDP_IP, UDP_PORT))
time.sleep(fade_interval / 256)
except KeyboardInterrupt:
print("DMX transmission stopped.")
finally:
sock.close()
def build_artdmx_packet(sequence, physical, universe, dmx_data):
header = b'Art-Net\x00' # Protocol identifier
opcode = struct.pack('<H', 0x5000) # OpCode: ArtDMX (0x5000), little endian
protocol_version = struct.pack('>H', 14) # Protocol version (14 for Art-Net)
sequence = struct.pack('B', sequence) # Sequence
physical = struct.pack('B', physical) # Physical port
universe = struct.pack('<H', universe) # Universe, little endian
length = struct.pack('>H', len(dmx_data)) # Data length, big endian
data = bytes(dmx_data) # DMX data
packet = header + opcode + protocol_version + sequence + physical + universe + length + data
return packet
time.sleep(sleep_time)
def moving_sine_wave(self, sock, UDP_IP, UDP_PORT, universe):
while self.running:
for t in range(0, 360):
if not self.running:
break
dmx_data = []
for channel in range(self.num_channels):
angle = (channel * 10 + t) % 360
value = int((math.sin(math.radians(angle)) + 1) * 127)
dmx_data.append(value)
dmx_packet = self.build_artdmx_packet(universe, dmx_data)
sock.sendto(dmx_packet, (UDP_IP, UDP_PORT))
time.sleep(0.02 / self.speed) # Adjust sleep time based on speed
def build_artdmx_packet(self, universe, dmx_data):
header = b'Art-Net\x00' # Protocol identifier
opcode = struct.pack('<H', 0x5000) # OpCode: ArtDMX (0x5000), little endian
protocol_version = struct.pack('>H', 14) # Protocol version (14 for Art-Net)
sequence = struct.pack('B', 0) # Sequence (ignored)
physical = struct.pack('B', 0) # Physical port (ignored)
universe = struct.pack('<H', universe) # Universe, little endian
length = struct.pack('>H', len(dmx_data)) # Data length, big endian
data = bytes(dmx_data) # DMX data
packet = header + opcode + protocol_version + sequence + physical + universe + length + data
return packet
def update_params(self, ip_address, port, universe, num_channels, fade_time, pattern, speed):
with self.lock:
self.ip_address = ip_address
self.port = port
self.universe = universe
self.num_channels = num_channels
self.fade_time = fade_time
self.pattern = pattern
self.speed = speed
self.save_params()
self.params_changed.emit()
class DMXApp(QWidget):
def __init__(self):
super().__init__()
self.controller = DMXController()
self.init_ui()
self.bind_signals()
# Use dark theme
self.setStyleSheet('''
QWidget {
background-color: #222222;
color: #FFFFFF;
}
QSlider::groove:horizontal {
border: 1px solid #444444;
height: 8px;
background: #555555;
}
QSlider::handle:horizontal {
background: #888888;
border: 1px solid #444444;
width: 18px;
margin: -8px 0;
}
QLineEdit {
background-color: #333333;
border: 1px solid #555555;
color: #FFFFFF;
padding: 4px;
}
QPushButton {
background-color: #444444;
border: 1px solid #666666;
padding: 6px;
}
QPushButton:hover {
background-color: #555555;
}
QLabel {
color: #FFFFFF;
}
''')
def init_ui(self):
# Labels and input fields
self.ip_label = QLabel('IP Address:')
self.ip_input = QLineEdit(self.controller.ip_address)
self.port_label = QLabel('Port:')
self.port_input = QLineEdit(str(self.controller.port))
self.universe_label = QLabel('Universe:')
self.universe_input = QLineEdit(str(self.controller.universe))
self.channels_label = QLabel('Number of Channels:')
self.channels_input = QLineEdit(str(self.controller.num_channels))
self.fade_time_label = QLabel('Fade Time (s):')
self.fade_time_input = QLineEdit(str(self.controller.fade_time))
self.speed_label = QLabel('Speed (1-10):')
self.speed_input = QLineEdit(str(self.controller.speed))
self.pattern_label = QLabel('Pattern:')
self.pattern_combo = QComboBox()
self.pattern_combo.addItems(['Fade Up and Down', 'Moving Sine Wave'])
self.pattern_combo.setCurrentText(self.controller.pattern)
# Buttons
self.start_button = QPushButton('Start')
self.stop_button = QPushButton('Stop')
self.stop_button.setEnabled(False)
# Layouts
layout = QVBoxLayout()
layout.addWidget(self.ip_label)
layout.addWidget(self.ip_input)
layout.addWidget(self.port_label)
layout.addWidget(self.port_input)
layout.addWidget(self.universe_label)
layout.addWidget(self.universe_input)
layout.addWidget(self.channels_label)
layout.addWidget(self.channels_input)
layout.addWidget(self.pattern_label)
layout.addWidget(self.pattern_combo)
layout.addWidget(self.fade_time_label)
layout.addWidget(self.fade_time_input)
layout.addWidget(self.speed_label)
layout.addWidget(self.speed_input)
button_layout = QHBoxLayout()
button_layout.addWidget(self.start_button)
button_layout.addWidget(self.stop_button)
layout.addLayout(button_layout)
self.setLayout(layout)
self.setWindowTitle('DMX Controller')
# Initially hide or show inputs based on the selected pattern
if self.controller.pattern == 'Moving Sine Wave':
self.fade_time_label.hide()
self.fade_time_input.hide()
else:
self.speed_label.hide()
self.speed_input.hide()
def bind_signals(self):
# Start and stop buttons
self.start_button.clicked.connect(self.start_dmx)
self.stop_button.clicked.connect(self.stop_dmx)
# Input fields
self.ip_input.editingFinished.connect(self.update_params)
self.port_input.editingFinished.connect(self.update_params)
self.universe_input.editingFinished.connect(self.update_params)
self.channels_input.editingFinished.connect(self.update_params)
self.fade_time_input.editingFinished.connect(self.update_params)
self.speed_input.editingFinished.connect(self.update_params)
self.pattern_combo.currentIndexChanged.connect(self.pattern_changed)
# Parameter change signal
self.controller.params_changed.connect(self.on_params_changed)
def start_dmx(self):
self.controller.start()
self.start_button.setEnabled(False)
self.stop_button.setEnabled(True)
def stop_dmx(self):
self.controller.stop()
self.start_button.setEnabled(True)
self.stop_button.setEnabled(False)
def update_params(self):
# Validate and update parameters
try:
ip_address = self.ip_input.text()
socket.inet_aton(ip_address) # Validate IP
port = int(self.port_input.text())
universe = int(self.universe_input.text())
num_channels = int(self.channels_input.text())
pattern = self.pattern_combo.currentText()
if pattern == 'Fade Up and Down':
fade_time = float(self.fade_time_input.text())
speed = self.controller.speed # Keep existing speed
elif pattern == 'Moving Sine Wave':
speed = float(self.speed_input.text())
fade_time = self.controller.fade_time # Keep existing fade_time
else:
fade_time = self.controller.fade_time
speed = self.controller.speed
self.controller.update_params(
ip_address, port, universe, num_channels, fade_time, pattern, speed
)
except Exception as e:
QMessageBox.warning(self, 'Invalid Input', str(e))
def pattern_changed(self):
pattern = self.pattern_combo.currentText()
if pattern == 'Moving Sine Wave':
self.fade_time_label.hide()
self.fade_time_input.hide()
self.speed_label.show()
self.speed_input.show()
else:
self.fade_time_label.show()
self.fade_time_input.show()
self.speed_label.hide()
self.speed_input.hide()
self.update_params()
def on_params_changed(self):
# Update the UI if needed
pass
if __name__ == '__main__':
send_dmx()
app = QApplication(sys.argv)
window = DMXApp()
window.show()
sys.exit(app.exec_())
Loading…
Cancel
Save