513 lines
21 KiB
Python
513 lines
21 KiB
Python
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_())
|