diff --git a/.gitignore b/.gitignore index 8b50736..bf7300c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ venv __pycache__ -*.mkv \ No newline at end of file +*.mkv +*.json \ No newline at end of file diff --git a/bin/ffmpeg.exe b/bin/ffmpeg.exe new file mode 100644 index 0000000..b269e62 Binary files /dev/null and b/bin/ffmpeg.exe differ diff --git a/dmx.py b/dmx.py index 2556216..4786f44 100644 --- a/dmx.py +++ b/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', 14) # Protocol version (14 for Art-Net) - sequence = struct.pack('B', sequence) # Sequence - physical = struct.pack('B', physical) # Physical port - universe = 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', 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', 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() \ No newline at end of file + app = QApplication(sys.argv) + window = DMXApp() + window.show() + sys.exit(app.exec_()) \ No newline at end of file