This commit is contained in:
Jonathan Roth 2025-04-10 19:19:44 +02:00
parent 73cf7cbf03
commit c90ea4cf85
5 changed files with 785 additions and 508 deletions

8
.gitignore vendored
View File

@ -2,6 +2,8 @@ bin/
include/ include/
lib/ lib/
lib64/ lib64/
__pycache__/
share/
glob glob
random random
re re
@ -11,4 +13,8 @@ sys
time time
lib lib
lib64 lib64
pyvenv.cfg pyvenv.cfg
fdr1gui.2.py
gzip
os
plt

228
BlockStackApp.py Normal file
View File

@ -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()

521
UpdateWorker.py Normal file
View File

@ -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("<i>Average diameter</i> in <b>mm</b>")
max_label.setToolTip("<i>Maximum diameter</i> in <b>mm</b>")
cur_label.setToolTip("<i>Current diameter</i> in <b>mm</b>")
len_label.setToolTip("<i>Length</i> in <b>m</b>")
wht_label.setToolTip("<i>Estimated mass</i> in <b>kg</b>")
min_label.setToolTip("<i>Minimum diameter</i> in <b>mm</b>")
maxL_label.setToolTip("<i>Average diameter</i> in <b>mm</b>")
avgL_label.setToolTip("<i>Maximum diameter</i> in <b>mm</b>")
curL_label.setToolTip("<i>Current diameter</i> in <b>mm</b>")
lenL_label.setToolTip("<i>Length</i> in <b>m</b>")
whtL_label.setToolTip("<i>Estimated mass</i> in <b>kg</b>")
minL_label.setToolTip("<i>Minimum diameter</i> in <b>mm</b>")
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']}<hr>"
f"Fabricant: {api_job['vendor']}<br>"
f"Matière: {api_job['material']}<br>"
f"Lot: {api_job['batch']}<br>"
f"Identifiant: {api_job['description']}<hr>"
f"ρ = {api_job['density']}<br>"
)
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}")

View File

@ -1,512 +1,8 @@
import sys import sys
import glob from PyQt5.QtWidgets import QApplication
import serial from BlockStackApp import BlockStackApp
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__': if __name__ == '__main__':
app = QApplication(sys.argv) app = QApplication(sys.argv)
ex = BlockStackApp() ex = BlockStackApp()
sys.exit(app.exec_()) sys.exit(app.exec_())

26
requirements.txt Normal file
View File

@ -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