import sys import glob import serial import re import requests from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QPushButton) from PyQt5.QtGui import QFont from PyQt5.QtCore import Qt, QThread, pyqtSignal, QSize import time import matplotlib.pyplot as plt from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas class UpdateWorker(QThread): update_signal = pyqtSignal(bool, float, float, float, float) data_received = pyqtSignal(str) stop_signal = pyqtSignal() not_homed_signal = pyqtSignal() mode_signal = pyqtSignal(int) change_color_signal = pyqtSignal(str, str, str) def __init__(self, serial_number, bench_id, port, block_id): super().__init__() self.block = self.add_block(serial_number, bench_id, port, block_id) self.serial_number = serial_number self.bench_id = bench_id self.port = port self.block_id = block_id self.running = True self.position_value = 0.0 self.diameter_value = 0.0 self.diameter_min = 0.0 self.diameter_max = 0.0 self.prepare_flag = False self.prepare_ok = False self.reset_flag = False self.ignore_stop = False self.is_running = False def run(self): try: with serial.Serial(self.port, 1000000, timeout=1) as ser: ser.write(b'a') ser.write(b'F') self.change_color_signal.emit(self.block_id, 'btn_home', "#A0A000") while self.running: if ser.in_waiting > 0: data = ser.readline().decode('utf-8').strip() if data: self.parse_data(data) if self.reset_flag and self.prepare_flag: if self.position_value > 100 and self.reset_flag: self.ignore_stop = True self.change_color_signal.emit(self.block_id, 'btn_prepare', "#5A5A5A") ser.write(b'R') self.prepare_flag = False if 1.65 < self.diameter_value < 2.99: self.prepare_ok = True else: self.prepare_ok = False if self.prepare_ok: self.change_color_signal.emit(self.block_id, 'btn_prepare', "#00A000") self.change_color_signal.emit(self.block_id, 'btn_start', "#A0A000") else: self.change_color_signal.emit(self.block_id, 'btn_prepare', "#A00000") else: self.change_color_signal.emit(self.block_id, 'btn_prepare', "#0000FF") else: if data == '%STOP': if not self.ignore_stop: self.change_color_signal.emit(self.block_id, 'btn_start', "#A00000") self.is_running = False else: self.ignore_stop = False elif data == '$NOTHOMED': self.not_homed_signal.emit() except serial.SerialException as e: print(f"Error with port {self.port}: {e}") def parse_data(self, data): match = re.match(r"[#d]:([\d.-]+),p:([\d.-]+)", data) if match: self.diameter_value = float(match.group(1)) self.position_value = float(match.group(2)) if self.diameter_value < self.diameter_min: self.diameter_min = self.diameter_value if self.diameter_value > self.diameter_max: self.diameter_max = self.diameter_value self.update_signal.emit(self.is_running, self.diameter_value, self.position_value, self.diameter_min, self.diameter_max) self.reset_flag = True mode_match = re.match(r"#o:(\d+),l:(\d+),r:(\d+),c:(\d+)", data) if mode_match: mode = int(mode_match.group(1)) self.mode_signal.emit(mode) match mode: case 1: self.change_color_signal.emit(self.block_id, 'btn_go_left', "#0000A0") self.change_color_signal.emit(self.block_id, 'btn_go_right', "#5a5a5a") self.change_color_signal.emit(self.block_id, 'btn_reset', "#5a5a5a") case 2: self.change_color_signal.emit(self.block_id, 'btn_go_left', "#5a5a5a") self.change_color_signal.emit(self.block_id, 'btn_go_right', "#0000A0") self.change_color_signal.emit(self.block_id, 'btn_reset', "#5a5a5a") case 3: self.change_color_signal.emit(self.block_id, 'btn_go_left', "#5a5a5a") self.change_color_signal.emit(self.block_id, 'btn_go_right', "#5a5a5a") self.change_color_signal.emit(self.block_id, 'btn_reset', "#00A0A0") else: self.mode_signal.emit(65535) self.change_color_signal.emit(self.block_id, 'btn_go_left', "#5a5a5a") self.change_color_signal.emit(self.block_id, 'btn_go_right', "#5a5a5a") self.change_color_signal.emit(self.block_id, 'btn_reset', "#5A5A5A") def send_command(self, command): try: with serial.Serial(self.port, 1000000, timeout=1) as ser: ser.write(command.encode()) except serial.SerialException as e: print(f"Could not send command to port {self.port}: {e}") def set_prepare(self, prepare): self.position_value = 0 self.prepare_flag = True self.reset_flag = False def reset_diameter_limits(self): self.diameter_min = self.diameter_value self.diameter_max = self.diameter_value def start_running(self): self.is_running = True self.clear_graph() def stop_running(self): self.is_running = False def clear_graph(self): self.line.set_data([], []) self.canvas.draw() def add_block(self, serial_number, bench_id, port, block_id): block_widget = QWidget() block_layout = QHBoxLayout() block_layout.setAlignment(Qt.AlignLeft | Qt.AlignTop) LABEL_SIZE = QSize(250, 20) BUTTON_SIZE = QSize(100, 25) VALUE_SIZE = QSize(80, 25) VALUEL_SIZE = QSize(25, 25) monospaced_font = QFont("unexistent", 10) monospaced_font.setStyleHint(QFont.Monospace) left_layout = QVBoxLayout() data_label = QLabel("No API data") data_label.setAlignment(Qt.AlignLeft | Qt.AlignTop) bench_label = QLabel(f"Bench ID: {bench_id}") bench_label.setAlignment(Qt.AlignCenter) bench_label.setFixedSize(LABEL_SIZE) left_layout.addWidget(bench_label) left_layout.addWidget(data_label) left2_layout = QHBoxLayout() left2L_layout = QVBoxLayout() left2V_layout = QVBoxLayout() max_label = QLabel("2.000 ") max_label.setFont(monospaced_font) avg_label = QLabel("2.0000") avg_label.setFont(monospaced_font) cur_label = QLabel("2.000 ") cur_label.setFont(monospaced_font) len_label = QLabel("12345.00") len_label.setFont(monospaced_font) wht_label = QLabel("12.345") wht_label.setFont(monospaced_font) min_label = QLabel("2.000 ") min_label.setFont(monospaced_font) maxL_label = QLabel("∨") avgL_label = QLabel("⌀̄") curL_label = QLabel("⌀") lenL_label = QLabel("L") whtL_label = QLabel("M") minL_label = QLabel("∧") avg_label.setAlignment(Qt.AlignRight) max_label.setAlignment(Qt.AlignRight) cur_label.setAlignment(Qt.AlignRight) len_label.setAlignment(Qt.AlignRight) wht_label.setAlignment(Qt.AlignRight) min_label.setAlignment(Qt.AlignRight) max_label.setFixedSize(VALUE_SIZE) avg_label.setFixedSize(VALUE_SIZE) cur_label.setFixedSize(VALUE_SIZE) len_label.setFixedSize(VALUE_SIZE) wht_label.setFixedSize(VALUE_SIZE) min_label.setFixedSize(VALUE_SIZE) maxL_label.setFixedSize(VALUEL_SIZE) avgL_label.setFixedSize(VALUEL_SIZE) curL_label.setFixedSize(VALUEL_SIZE) lenL_label.setFixedSize(VALUEL_SIZE) whtL_label.setFixedSize(VALUEL_SIZE) minL_label.setFixedSize(VALUEL_SIZE) left2V_layout.addWidget(max_label) left2V_layout.addWidget(avg_label) left2V_layout.addWidget(cur_label) left2V_layout.addWidget(len_label) left2V_layout.addWidget(wht_label) left2V_layout.addWidget(min_label) left2L_layout.addWidget(maxL_label) left2L_layout.addWidget(avgL_label) left2L_layout.addWidget(curL_label) left2L_layout.addWidget(lenL_label) left2L_layout.addWidget(whtL_label) left2L_layout.addWidget(minL_label) left2_layout.addLayout(left2L_layout) left2_layout.addLayout(left2V_layout) center_layout = QVBoxLayout() graph_placeholder = QLabel("Graph Placeholder") graph_placeholder.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored) center_layout.addWidget(graph_placeholder) right_layout = QHBoxLayout() right_layout_1 = QVBoxLayout() right_layout_2 = QVBoxLayout() right_layout_3 = QVBoxLayout() btn_prepare = QPushButton("Prepare") btn_prepare.clicked.connect(lambda: self.send_command('P')) btn_prepare.clicked.connect(lambda: self.set_prepare(True)) right_layout_1.addWidget(btn_prepare) btn_start = QPushButton("Start") btn_start.clicked.connect(lambda: self.change_color_signal.emit(self.block_id, 'btn_start', "#00a000")) btn_start.clicked.connect(lambda: self.change_color_signal.emit(self.block_id, 'btn_prepare', "#5A5A5A")) btn_start.clicked.connect(self.reset_diameter_limits) btn_start.clicked.connect(self.start_running) btn_start.clicked.connect(lambda: self.send_command('S')) right_layout_1.addWidget(btn_start) btn_reset = QPushButton("Reset/Zero") btn_reset.clicked.connect(lambda: self.send_command('r')) btn_reset.clicked.connect(lambda: self.change_color_signal.emit(self.block_id, 'btn_start', "#5A5A5A")) right_layout_1.addWidget(btn_reset) btn_stop = QPushButton("Stop") btn_stop.clicked.connect(lambda: self.send_command('R')) btn_stop.clicked.connect(lambda: self.change_color_signal.emit(self.block_id, 'btn_start', "#a00000")) btn_stop.clicked.connect(lambda: self.change_color_signal.emit(self.block_id, 'btn_prepare', "#5A5A5A")) btn_stop.clicked.connect(self.stop_running) right_layout_1.addWidget(btn_stop) btn_sleep = QPushButton("Sleep") btn_sleep.clicked.connect(lambda: self.send_command('a')) btn_sleep.clicked.connect(lambda: self.change_color_signal.emit(self.block_id, 'btn_home', "#a0a000")) right_layout_2.addWidget(btn_sleep) btn_home = QPushButton("Home") btn_home.clicked.connect(lambda: self.send_command(' ')) btn_home.clicked.connect(lambda: self.change_color_signal.emit(self.block_id, 'btn_home', "#5A5A5A")) btn_home.clicked.connect(lambda: self.change_color_signal.emit(self.block_id, 'btn_start', "#5A5A5A")) right_layout_2.addWidget(btn_home) btn_go_left = QPushButton("Go Left") btn_go_left.clicked.connect(lambda: self.send_command('1')) right_layout_2.addWidget(btn_go_left) btn_go_right = QPushButton("Go Right") btn_go_right.clicked.connect(lambda: self.send_command('2')) right_layout_2.addWidget(btn_go_right) btn_down_left = QPushButton("Down Left") btn_down_left.clicked.connect(lambda: self.send_command('g')) right_layout_3.addWidget(btn_down_left) btn_up_left = QPushButton("Up Left") btn_up_left.clicked.connect(lambda: self.send_command('h')) right_layout_3.addWidget(btn_up_left) btn_down_right = QPushButton("Down Right") btn_down_right.clicked.connect(lambda: self.send_command('t')) right_layout_3.addWidget(btn_down_right) btn_up_right = QPushButton("Up Right") btn_up_right.clicked.connect(lambda: self.send_command('y')) right_layout_3.addWidget(btn_up_right) right_layout.addLayout(right_layout_1) right_layout.addLayout(right_layout_2) right_layout.addLayout(right_layout_3) block_layout.addLayout(left_layout) block_layout.addLayout(left2_layout) block_layout.addLayout(center_layout) block_layout.addLayout(right_layout) block_widget.setLayout(block_layout) block_widget.btn_start = btn_start block_widget.btn_home = btn_home block_widget.btn_go_left = btn_go_left block_widget.btn_go_right = btn_go_right block_widget.btn_reset = btn_reset block_widget.btn_prepare = btn_prepare block_widget.data_label = data_label block_widget.max_label = max_label block_widget.avg_label = avg_label block_widget.dia_label = cur_label block_widget.len_label = len_label block_widget.wht_label = wht_label block_widget.min_label = min_label # Create a Matplotlib figure and canvas for the graph plt.style.use('dark_background') fig, ax = plt.subplots() plt.subplots_adjust(left=0.0, right=0.9995, top=1, bottom=0.005) ax.set_facecolor('#3a3a3a') ax.set_xticks([]) # Hide x-axis values ax.set_frame_on(True) # ax.set_ylabel('Diameter (mm)') # Show y-axis label self.line, = ax.plot([], [], '#ff5000', linewidth=0.5) # Set line width to 0.5 self.canvas = FigureCanvas(fig) center_layout.addWidget(self.canvas) return block_widget class BlockStackApp(QWidget): def __init__(self): super().__init__() self.initUI() self.check_usb_devices() def initUI(self): self.layout = QVBoxLayout() self.layout.setSpacing(5) self.layout.setContentsMargins(5, 5, 5, 5) self.layout.setAlignment(Qt.AlignLeft | Qt.AlignTop) self.setLayout(self.layout) self.setWindowTitle('462filament Bench GUI') self.setStyleSheet(""" QWidget { background-color: #2E2E2E; color: #FFFFFF; height: 100px; } QLabel { background-color: #4A4A4A; border: 1px solid #5A5A5A; padding: 2px; margin: 2px; height: 30px; width: 250px; } QPushButton { background-color: #5A5A5A; border: 1px solid #6A6A6A; padding: 2px; margin: 2px; height: 30px; width: 100px; } """) self.showMaximized() def check_usb_devices(self): bench_list = [] for port in glob.glob('/dev/ttyUSB*'): try: with serial.Serial(port, 1000000, timeout=1) as ser: response = ser.read_all().decode('utf-8').strip() ser.write(b'i') time.sleep(1) response = ser.read_all().decode('utf-8').strip() serial_number = self.extract_serial_number(response) if serial_number: print(f"Found bench on port {port}") bench_id = self.get_bench_id(serial_number) if bench_id: bench_list.append((port, bench_id, serial_number)) else: print(f"Second check port {port}") response = ser.read_all().decode('utf-8').strip() ser.write(b'i') time.sleep(1) response = ser.read_all().decode('utf-8').strip() serial_number = self.extract_serial_number(response) if serial_number: print(f"Found bench on port {port}") bench_id = self.get_bench_id(serial_number) if bench_id: bench_list.append((port, bench_id, serial_number)) else: print(f"Ignoring port {port}") except serial.SerialException as e: print(f"Could not open port {port}: {e}") bench_list.sort(key=lambda x: x[1]) self.blocks = {} # Dictionary to store blocks by block_id for port, bench_id, serial_number in bench_list: block_id = f"{bench_id}_{port}" # Unique identifier for the block self.worker = UpdateWorker(serial_number, bench_id, port, block_id) self.layout.addWidget(self.worker.block) self.worker.change_color_signal.connect(self.change_button_color) self.worker.update_signal.connect(self.update_block) self.worker.start() self.blocks[block_id] = self.worker.block # Store the block with its identifier def extract_serial_number(self, response): match = re.match(r"id:([0-9A-Fa-f]+)", response) if match: return match.group(1) return None def get_bench_id(self, serial_number): url = f"http://462filament/api.php?data=benchid&serial={serial_number}" try: response = requests.get(url) if response.status_code == 200: data = response.json() return data.get("id") except requests.RequestException as e: print(f"Error making request to API: {e}") return None def change_button_color(self, block_id, button_name, color): block = self.blocks.get(block_id) if block: button = getattr(block, button_name, None) if button: button.setStyleSheet(f"background-color: {color};") def update_mode(self, mode): # Update button colors based on mode if mode == 1: self.change_button_color(self.sender().block.btn_go_left, "#0000FF") self.change_button_color(self.sender().block.btn_go_right, "#5A5A5A") self.change_button_color(self.sender().block.btn_reset, "#5A5A5A") elif mode == 2: self.change_button_color(self.sender().block.btn_go_left, "#5A5A5A") self.change_button_color(self.sender().block.btn_go_right, "#0000FF") self.change_button_color(self.sender().block.btn_reset, "#5A5A5A") elif mode == 65535: self.change_button_color(self.sender().block.btn_go_left, "#5A5A5A") self.change_button_color(self.sender().block.btn_go_right, "#5A5A5A") self.change_button_color(self.sender().block.btn_reset, "#5A5A5A") else: self.change_button_color(self.sender().block.btn_go_left, "#5A5A5A") self.change_button_color(self.sender().block.btn_go_right, "#5A5A5A") self.change_button_color(self.sender().block.btn_reset, "#0000FF") def handle_data_received(self, data): # Handle the data received from the serial port print(f"Data received: {data}") def update_block(self, is_running, diameter_value, position_value, diameter_min, diameter_max): # Update the block with diameter_value and position_value worker = self.sender() diameter_avg = 2.0000 est_wgh = 1234 worker.block.min_label.setText(f"{diameter_min:.3f} ") worker.block.max_label.setText(f"{diameter_max:.3f} ") worker.block.dia_label.setText(f"{diameter_value:.3f} ") worker.block.avg_label.setText(f"{diameter_avg:.4f}") worker.block.len_label.setText(f"{20000/1000:.4f}") worker.block.wht_label.setText(f"{est_wgh/1000:.3f} ") worker.block.data_label.setText(f"No API data") # Update the graph with the new diameter value if is_running: self.update_graph(worker, diameter_value) def update_graph(self, worker, diameter_value): # Update the graph with the new diameter value x_data = worker.line.get_xdata() y_data = worker.line.get_ydata() x_data = list(x_data) y_data = list(y_data) # Append new data point x_data.append(len(x_data)) y_data.append(diameter_value) # Update the line with the new data worker.line.set_data(x_data, y_data) # Rescale the graph worker.line.axes.relim() worker.line.axes.autoscale_view() # Redraw the canvas worker.canvas.draw() if __name__ == '__main__': app = QApplication(sys.argv) ex = BlockStackApp() sys.exit(app.exec_())