diff --git a/.gitignore b/.gitignore index 30f0cd0..09cef89 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ bin/ include/ lib/ lib64/ +__pycache__/ +share/ glob random re @@ -11,4 +13,8 @@ sys time lib lib64 -pyvenv.cfg \ No newline at end of file +pyvenv.cfg +fdr1gui.2.py +gzip +os +plt diff --git a/BlockStackApp.py b/BlockStackApp.py new file mode 100644 index 0000000..5915aba --- /dev/null +++ b/BlockStackApp.py @@ -0,0 +1,228 @@ +from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QPushButton +from PyQt5.QtCore import Qt +import glob +import requests +import serial +import re +import time +import math +from UpdateWorker import UpdateWorker + +class BlockStackApp(QWidget): + def __init__(self): + super().__init__() + + self.initUI() + self.check_usb_devices() + + self.update_count = 11 + + 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, QComboBox { + background-color: #5A5A5A; + border: 1px solid #6A6A6A; + padding: 2px; + margin: 2px; + height: 25px; + 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 = {} + + for port, bench_id, serial_number in bench_list: + block_id = f"{bench_id}" + self.worker = UpdateWorker(serial_number, bench_id, port, block_id, self) + 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 + + 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): + # Update the block with diameter_value and position_value + worker = self.sender() + diameter_avg = 2.0000 + est_wgh = 0 + worker.block.dia_label.setText(f"{diameter_value:.3f} ") + worker.block.len_label.setText(f"{position_value/1000:.3f}") + + if is_running: + if worker.reset_data: + worker.diameter_total = diameter_value + worker.diameter_count = 1 + worker.diameter_min = diameter_value + worker.diameter_max = diameter_value + worker.clear_graph() + worker.reset_data = False + else: + worker.diameter_total += diameter_value + worker.diameter_count += 1 + if diameter_value < worker.diameter_min: + worker.diameter_min = diameter_value + if diameter_value > worker.diameter_max: + worker.diameter_max = diameter_value + worker.block.min_label.setText(f"{worker.diameter_min:.3f} ") + worker.block.max_label.setText(f"{worker.diameter_max:.3f} ") + + self.update_graph(worker, diameter_value) + + # calculate and display average diameter + diameter_avg = worker.diameter_total / worker.diameter_count if worker.diameter_count > 0 else 0.0 + worker.block.avg_label.setText(f"{diameter_avg:.4f}") + + # calculate and display estimated weight + vol = math.pi * (diameter_avg / 2) ** 2 * position_value # all in mm → mm³ + est_wgh = worker.density * vol/1000 # density in g·cm³, vol in mm³ + worker.block.wht_label.setText(f"{est_wgh/1000:.3f} ") + + + 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() diff --git a/UpdateWorker.py b/UpdateWorker.py new file mode 100644 index 0000000..5fad03a --- /dev/null +++ b/UpdateWorker.py @@ -0,0 +1,521 @@ +from PyQt5.QtCore import QThread, pyqtSignal, QSize, QTimer +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QPushButton, QComboBox +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QFont + +import serial +import re +import time +import matplotlib.pyplot as plt +import os +import struct +import requests +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() + mode_signal = pyqtSignal(int) + offset_signal = pyqtSignal(int) + change_color_signal = pyqtSignal(str, str, str) + + def __init__(self, serial_number, bench_id, port, block_id, main_app): + super().__init__() + print(f"id{bench_id} :\tport {port}") + print(f"id{bench_id} :\tserial {serial_number}") + self.block = self.add_block(serial_number, bench_id, port, block_id, main_app) + self.main_app = main_app # Store the reference to the BlockStackApp instance + 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_total = 0.0 # for average calculation + self.diameter_count = 0 + self.diameter_value = 0.0 + self.diameter_min = 0.0 + self.diameter_max = 0.0 + self.density = 1.0 + self.prepare_flag = False + self.prepare_ok = False + self.reset_flag = False + self.ignore_stop = False + self.is_running = False + self.reset_data = False + self.homed = False + self.can_start = False + self.data_list = [] + self.profile_data = {} # Dictionary to store profile data + + # run at startup + self.update_profiles() + + self.timer = QTimer() + self.timer.timeout.connect(self.query_api) + self.timer.start(3000) + + def run(self): + try: + with serial.Serial(self.port, 1000000, timeout=1) as ser: + ser.write(b'a') + ser.write(b'F') + ser.write(b'i') + ser.write(b' ') + 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) and (self.diameter_value < 1.85)) or ((2.70 < self.diameter_value) and (self.diameter_value < 2.95)): + 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': + self.stop_running() + # if not self.ignore_stop: + # self.change_color_signal.emit(self.block_id, 'btn_start', "#A00000") + # self.is_running = False + # self.save_data_to_file() + # else: + # self.ignore_stop = False + elif data == '$NOTHOMED': + self.homed = False + self.change_color_signal.emit(self.block_id, 'btn_home', "#a0a000") + elif data == '$HOMED': + print(f"id{self.bench_id} :\tHoming ok") + self.homed = True + self.change_color_signal.emit(self.block_id, 'btn_home', "#5a5a5a") + except serial.SerialException as e: + print(f"Error with port {self.port}: {e}") + + def save_data_to_file(self): + #FIXME: set name + #FIXME: add file header + #FIXME: compress file + #FIXME: upload to API + file_path = os.path.join("data", f"{self.bench_id}_data.txt") + + # Ensure the directory exists + os.makedirs(os.path.dirname(file_path), exist_ok=True) + + # Write the data to the file + with open(file_path, "w") as file: + for data in self.data_list: + file.write(data + "\n") + + def parse_data(self, data): + self.data_list.append(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)) + self.update_signal.emit(self.is_running, self.diameter_value, self.position_value, self.diameter_min, self.diameter_max) + self.reset_flag = True + + offset_match = re.match(r"^#h:(\d+)", data) + if offset_match: + home_offset = int(offset_match.group(1)) + print(f"id{self.bench_id}:\tOffset: {home_offset:.2f}") + self.block.btn_offset.setText(f"Offset: {home_offset:.2f}") + + mode_match = re.match(r"^#o:(\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 int_to_two_bytes(self, value): + if not (0 <= value <= 0xFFFF): + raise ValueError("Value out of range for two bytes") + + two_bytes = struct.pack('>H', value) + return two_bytes + + def set_limits(self, left, right): + print(f"id{self.bench_id}:\tupdate limits to {left}+{right}") + try: + with serial.Serial(self.port, 1000000, timeout=1) as ser: + ser.write(b'\x04') + time.sleep(0.001) + ser.write(self.int_to_two_bytes(left)) + time.sleep(0.003) + ser.write(self.int_to_two_bytes(right)) + time.sleep(0.003) + ser.write(b'\x04') + time.sleep(0.001) + ser.write(b'\x00') + time.sleep(0.001) + 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 start_running(self): + if self.prepare_ok and self.can_start: + self.reset_data = True + self.is_running = True + + self.change_color_signal.emit(self.block_id, 'btn_start', "#00a000") + self.change_color_signal.emit(self.block_id, 'btn_prepare', "#5A5A5A") + + self.send_command('S') + + def stop_running(self): + if self.is_running: + self.send_command('R') + self.timer.start() + + self.block.data_label.setText( + f"Job done, processing..." + ) + + self.change_color_signal.emit(self.block_id, 'btn_start', "#a00000") + self.change_color_signal.emit(self.block_id, 'btn_prepare', "#5A5A5A") + + self.is_running = False + self.save_data_to_file() + self.prepare_ok = False + + + def clear_graph(self): + self.line.set_data([], []) + self.canvas.draw() + + def add_block(self, serial_number, bench_id, port, block_id, main_app): + 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) + + monospaced_bold_font = QFont("unexistent", 10) + monospaced_bold_font.setStyleHint(QFont.Monospace) + monospaced_bold_font.setBold(True) + + monospaced_italics_font = QFont("unexistent", 10) + monospaced_italics_font.setStyleHint(QFont.Monospace) + monospaced_italics_font.setItalic(True) + + left_layout = QVBoxLayout() + data_label = QLabel("No API data") + data_label.setTextFormat(Qt.RichText) + data_label.setAlignment(Qt.AlignLeft | Qt.AlignTop) + bench_label = QLabel(f"Bench ID: {bench_id}") + bench_label.setToolTip(f"Serial number: {serial_number}\nCPU: Microchip PIC18F4431 @ 32 MHz\nPort: {port}\nBandwidth: 1 MBps\nPosition maxspd: 638 mm/s\nDiameter freq: ≈10.5 Hz") + 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_bold_font) + avg_label = QLabel("2.0000") + avg_label.setFont(monospaced_font) + cur_label = QLabel("2.000 ") + cur_label.setFont(monospaced_bold_font) + len_label = QLabel("12345.00") + len_label.setFont(monospaced_font) + wht_label = QLabel("12.345 ") + wht_label.setFont(monospaced_italics_font) + min_label = QLabel("2.000 ") + min_label.setFont(monospaced_bold_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) + + avg_label.setToolTip("Average diameter in mm") + max_label.setToolTip("Maximum diameter in mm") + cur_label.setToolTip("Current diameter in mm") + len_label.setToolTip("Length in m") + wht_label.setToolTip("Estimated mass in kg") + min_label.setToolTip("Minimum diameter in mm") + + maxL_label.setToolTip("Average diameter in mm") + avgL_label.setToolTip("Maximum diameter in mm") + curL_label.setToolTip("Current diameter in mm") + lenL_label.setToolTip("Length in m") + whtL_label.setToolTip("Estimated mass in kg") + minL_label.setToolTip("Minimum diameter in mm") + + 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(self.start_running) + 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(self.stop_running) + right_layout_1.addWidget(btn_stop) + + btn_spare1 = QPushButton() + right_layout_1.addWidget(btn_spare1) + + + + btn_sleep = QPushButton("Sleep") + btn_sleep.clicked.connect(lambda: self.send_command('a')) + right_layout_2.addWidget(btn_sleep) + + btn_home = QPushButton("Home") + btn_home.clicked.connect(lambda: self.send_command(' ')) + 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_offset = QPushButton() + right_layout_2.addWidget(btn_offset) + + btn_down_left = QPushButton("Offset +") + btn_down_left.clicked.connect(lambda: self.send_command('b')) + right_layout_3.addWidget(btn_down_left) + + btn_up_left = QPushButton("Offset -") + btn_up_left.clicked.connect(lambda: self.send_command('n')) + right_layout_3.addWidget(btn_up_left) + + btn_save_offset = QPushButton("Save Offset") + btn_save_offset.clicked.connect(lambda: self.send_command('M')) + right_layout_3.addWidget(btn_save_offset) + + + + dropd_profile = QComboBox() + + dropd_profile.addItem("Select a profile...") + dropd_profile.model().item(0).setEnabled(False) + + + dropd_profile.currentIndexChanged.connect(self.on_profile_changed) + + dropd_profile.setFixedWidth(120) + + view = dropd_profile.view() + view.setMinimumWidth(350) # Set the minimum width of the dropdown list + + right_layout_3.addWidget(dropd_profile) + + btn_update_profiles = QPushButton("Reload profiles") + btn_update_profiles.clicked.connect(self.update_profiles) + right_layout_3.addWidget(btn_update_profiles) + + + 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.bench_label = bench_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 + block_widget.btn_offset = btn_offset + block_widget.dropd_profile = dropd_profile + + # Create a Matplotlib figure and canvas for the graph + plt.style.use('dark_background') + + fig, ax = plt.subplots() + plt.subplots_adjust(left=-0.01, right=1, top=1.01, bottom=0) + 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) + self.canvas = FigureCanvas(fig) + self.canvas.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored) + center_layout.addWidget(self.canvas) + + return block_widget + + def on_profile_changed(self, index): + # Get the selected ID + selected_id = self.sender().itemData(index) + + if selected_id in self.profile_data: + start_value = self.profile_data[selected_id]['start'] + width_value = self.profile_data[selected_id]['width'] + + self.set_limits(start_value, width_value) + + def update_profiles(self): + url = f"http://462filament/api.php?data=profiles" + try: + response = requests.get(url) + if response.status_code == 200: + profiles = response.json() + if profiles: + self.block.dropd_profile.clear() + self.block.dropd_profile.addItem("Select a profile...") + self.block.dropd_profile.model().item(0).setEnabled(False) + for profile in profiles: + profile_id = profile["id"] + profile_name = profile["show_name"] + start_value = int(profile["start"]) + width_value = int(profile["width"]) + self.block.dropd_profile.addItem(profile_name, profile_id) + self.profile_data[profile_id] = {'start': start_value, 'width': width_value} + except requests.RequestException as e: + print(f"Error making request to API: {e}") + return None + + def query_api(self): + try: + url = f"http://462filament/api.php?data=bench&benchid={self.bench_id}" + response = requests.get(url) + response.raise_for_status() # Raise an exception for HTTP errors + api_job = response.json() # Parse the JSON response + + if api_job is not None: + print(f"id{self.bench_id}:\tgot job {api_job['spool']} from API") + self.block.data_label.setText( + f"Job/Spool: {api_job['spool']}
" + f"Fabricant: {api_job['vendor']}
" + f"Matière: {api_job['material']}
" + f"Lot: {api_job['batch']}
" + f"Identifiant: {api_job['description']}
" + f"ρ = {api_job['density']}
" + ) + self.density = float(api_job['density']) + self.can_start = True + self.timer.stop() + else: + print(f"id{self.bench_id}:\tno job from API") + self.block.data_label.setText( + f"API responded with no job" + ) + self.can_start = False + except requests.RequestException as e: + print(f"Error querying API: {e}") \ No newline at end of file diff --git a/fdr1gui.py b/fdr1gui.py index 17984b6..d1b5d8c 100644 --- a/fdr1gui.py +++ b/fdr1gui.py @@ -1,512 +1,8 @@ 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() +from PyQt5.QtWidgets import QApplication +from BlockStackApp import BlockStackApp if __name__ == '__main__': app = QApplication(sys.argv) ex = BlockStackApp() - sys.exit(app.exec_()) + sys.exit(app.exec_()) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f514016 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,26 @@ +certifi==2025.1.31 +charset-normalizer==3.4.1 +contourpy==1.3.1 +cycler==0.12.1 +fonttools==4.57.0 +future==1.0.0 +idna==3.10 +iso8601==2.1.0 +kiwisolver==1.4.8 +matplotlib==3.10.1 +numpy==2.2.4 +packaging==24.2 +pillow==11.1.0 +pyparsing==3.2.3 +PyQt5==5.15.11 +PyQt5-Qt5==5.15.16 +PyQt5_sip==12.17.0 +PyQtChart==5.15.7 +PyQtChart-Qt5==5.15.16 +pyserial==3.5 +python-dateutil==2.9.0.post0 +PyYAML==6.0.2 +requests==2.32.3 +serial==0.0.97 +six==1.17.0 +urllib3==2.3.0