commit
0b43e70f4a
8 changed files with 318 additions and 0 deletions
-
3.gitignore
-
141dmx.py
-
85main.py
-
2requirements.txt
-
9run.bat
-
9send.bat
-
55sender.py
-
14win-install.bat
@ -0,0 +1,3 @@ |
|||
venv |
|||
__pycache__ |
|||
*.mkv |
|||
@ -0,0 +1,141 @@ |
|||
# dmx.py |
|||
|
|||
from PyQt5.QtCore import QObject, QThread, pyqtSignal |
|||
import socket |
|||
import struct |
|||
import numpy as np |
|||
import subprocess |
|||
import time |
|||
|
|||
class DMXRecorder(QObject): |
|||
def __init__(self): |
|||
super().__init__() |
|||
self.thread = None |
|||
self.recording_thread = None |
|||
|
|||
def start_recording(self): |
|||
self.thread = QThread() |
|||
self.recording_thread = RecordingThread() |
|||
self.recording_thread.moveToThread(self.thread) |
|||
|
|||
self.thread.started.connect(self.recording_thread.run) |
|||
self.recording_thread.finished.connect(self.thread.quit) |
|||
self.recording_thread.finished.connect(self.recording_thread.deleteLater) |
|||
self.thread.finished.connect(self.thread.deleteLater) |
|||
|
|||
self.thread.start() |
|||
|
|||
def stop_recording(self): |
|||
if self.recording_thread: |
|||
self.recording_thread.stop() |
|||
if self.thread: |
|||
self.thread.quit() |
|||
self.thread.wait() |
|||
|
|||
class RecordingThread(QObject): |
|||
finished = pyqtSignal() |
|||
|
|||
def __init__(self): |
|||
super().__init__() |
|||
self.running = False |
|||
self.socket = None |
|||
self.ffmpeg_process = None |
|||
|
|||
def run(self): |
|||
self.running = True |
|||
|
|||
# Set up UDP socket to listen for Art-Net DMX data |
|||
UDP_IP = "0.0.0.0" |
|||
UDP_PORT = 6454 |
|||
|
|||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |
|||
self.socket.bind((UDP_IP, UDP_PORT)) |
|||
|
|||
# Set socket to non-blocking |
|||
self.socket.setblocking(False) |
|||
|
|||
# Prepare for recording |
|||
expected_universes = set(range(4)) # Universes 0 to 3 (adjust as needed) |
|||
num_universes = len(expected_universes) |
|||
universe_size = 512 |
|||
|
|||
# Define frame dimensions |
|||
frame_width = 64 # Adjust as needed (must divide evenly into total data size) |
|||
total_pixels = num_universes * universe_size |
|||
frame_height = total_pixels // frame_width |
|||
|
|||
# Frame rate and timing |
|||
frame_rate = 30 # frames per second |
|||
frame_interval = 1.0 / frame_rate |
|||
last_frame_time = time.time() |
|||
|
|||
# Set up ffmpeg process |
|||
ffmpeg_cmd = [ |
|||
'ffmpeg', |
|||
'-y', |
|||
'-f', 'rawvideo', |
|||
'-pix_fmt', 'gray', |
|||
'-s', f'{frame_width}x{frame_height}', |
|||
'-r', str(frame_rate), |
|||
'-i', '-', # Input from stdin |
|||
'-c:v', 'ffv1', |
|||
'dmx_video.mkv' |
|||
] |
|||
|
|||
self.ffmpeg_process = subprocess.Popen(ffmpeg_cmd, stdin=subprocess.PIPE) |
|||
|
|||
universes_data = {} |
|||
last_received = {} |
|||
|
|||
try: |
|||
while self.running: |
|||
current_time = time.time() |
|||
|
|||
# Receive data |
|||
try: |
|||
data, addr = self.socket.recvfrom(1024) |
|||
if data.startswith(b'Art-Net'): |
|||
op_code = struct.unpack('<H', data[8:10])[0] |
|||
# OpCode for ArtDMX is 0x5000 |
|||
if op_code == 0x5000: |
|||
# Parse universe |
|||
universe = struct.unpack('<H', data[14:16])[0] |
|||
# Get length |
|||
length = struct.unpack('>H', data[16:18])[0] |
|||
dmx_data = data[18:18+length] |
|||
# Store the data for this universe |
|||
universes_data[universe] = dmx_data |
|||
last_received[universe] = current_time |
|||
except BlockingIOError: |
|||
# No data received |
|||
pass |
|||
except Exception as e: |
|||
print(f"Error receiving data: {e}") |
|||
|
|||
# Check if it's time to write a frame |
|||
if current_time - last_frame_time >= frame_interval: |
|||
# 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)) |
|||
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()) |
|||
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() |
|||
self.socket.close() |
|||
self.finished.emit() |
|||
|
|||
def stop(self): |
|||
self.running = False |
|||
@ -0,0 +1,85 @@ |
|||
# main.py |
|||
|
|||
import sys |
|||
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QWidget, QVBoxLayout |
|||
from PyQt5.QtCore import Qt |
|||
from dmx import DMXRecorder |
|||
|
|||
class MainWindow(QMainWindow): |
|||
def __init__(self): |
|||
super().__init__() |
|||
self.initUI() |
|||
self.isRecording = False |
|||
self.recorder = None |
|||
|
|||
def initUI(self): |
|||
self.setWindowTitle("DMX Recorder") |
|||
self.resize(300, 200) |
|||
|
|||
# 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; |
|||
} |
|||
''') |
|||
|
|||
self.central_widget = QWidget() |
|||
self.setCentralWidget(self.central_widget) |
|||
|
|||
self.layout = QVBoxLayout() |
|||
self.central_widget.setLayout(self.layout) |
|||
|
|||
self.record_button = QPushButton("Record") |
|||
self.record_button.clicked.connect(self.toggle_recording) |
|||
|
|||
self.layout.addWidget(self.record_button) |
|||
|
|||
def toggle_recording(self): |
|||
if not self.isRecording: |
|||
# Start recording |
|||
self.recorder = DMXRecorder() |
|||
self.recorder.start_recording() |
|||
self.record_button.setText("Stop") |
|||
self.isRecording = True |
|||
else: |
|||
# Stop recording |
|||
if self.recorder: |
|||
self.recorder.stop_recording() |
|||
self.recorder = None |
|||
self.record_button.setText("Record") |
|||
self.isRecording = False |
|||
|
|||
if __name__ == '__main__': |
|||
app = QApplication(sys.argv) |
|||
window = MainWindow() |
|||
window.show() |
|||
sys.exit(app.exec_()) |
|||
@ -0,0 +1,2 @@ |
|||
PyQt5>=5.15.0 |
|||
numpy>=1.19.0 |
|||
@ -0,0 +1,9 @@ |
|||
@echo off |
|||
echo Activating the virtual environment... |
|||
call venv\Scripts\activate |
|||
|
|||
echo Running main.py... |
|||
python main.py |
|||
|
|||
echo Deactivating the virtual environment... |
|||
call venv\Scripts\deactivate |
|||
@ -0,0 +1,9 @@ |
|||
@echo off |
|||
echo Activating the virtual environment... |
|||
call venv\Scripts\activate |
|||
|
|||
echo Running sender.py... |
|||
python sender.py |
|||
|
|||
echo Deactivating the virtual environment... |
|||
call venv\Scripts\deactivate |
|||
@ -0,0 +1,55 @@ |
|||
import socket |
|||
import struct |
|||
import time |
|||
|
|||
def send_dmx(universe=0, num_channels=512, fade_time=5): |
|||
UDP_IP = '255.255.255.255' # Broadcast address |
|||
UDP_PORT = 6454 |
|||
|
|||
# Create UDP socket |
|||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |
|||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) |
|||
|
|||
sequence = 0 |
|||
physical = 0 |
|||
|
|||
# Fading parameters |
|||
fade_steps = 100 |
|||
fade_interval = fade_time / fade_steps |
|||
|
|||
try: |
|||
while True: |
|||
# Fade up |
|||
for value in range(0, 256): |
|||
dmx_data = [value] * num_channels |
|||
dmx_packet = build_artdmx_packet(sequence, physical, universe, dmx_data) |
|||
sock.sendto(dmx_packet, (UDP_IP, UDP_PORT)) |
|||
time.sleep(fade_interval / 256) |
|||
|
|||
# Fade down |
|||
for value in range(255, -1, -1): |
|||
dmx_data = [value] * num_channels |
|||
dmx_packet = build_artdmx_packet(sequence, physical, 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 |
|||
|
|||
if __name__ == '__main__': |
|||
send_dmx() |
|||
@ -0,0 +1,14 @@ |
|||
@echo off |
|||
echo Creating virtual environment named venv... |
|||
python -m venv venv |
|||
|
|||
echo Activating virtual environment... |
|||
call venv\Scripts\activate |
|||
|
|||
echo Installing dependencies from requirements.txt... |
|||
pip install -r requirements.txt |
|||
|
|||
echo Installation complete. Virtual environment is ready to use. |
|||
|
|||
echo Deactivating the virtual environment... |
|||
call venv\Scripts\deactivate |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue