fdr1gui/UpdateWorker.py
Jonathan Roth 226f4469f8 add 2 graphs if data is available
update calculations and values
update diameter from float (mm) to int (um)
2025-04-27 10:50:07 +02:00

593 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
import subprocess
from datetime import datetime
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
class UpdateWorker(QThread):
update_signal = pyqtSignal(bool, int, float, float)
data_received = pyqtSignal(str)
stop_signal = pyqtSignal()
mode_signal = pyqtSignal(int)
offset_signal = pyqtSignal(int)
apitimer_signal = pyqtSignal()
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_value1 = 0.0
self.diameter_value2 = 0.0
self.diameter_value3 = 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.current_job = None
self.current_job = None
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):
# format:
#
# #bench: FDR1-proto0
# #date: 20250224_232750
# #spool: R587
# #material: Enky PP Ortho V1-2106 20251007-00 N°7
# d:1.730,dm:1.730,dM:1.730,p:0.000,s:0.0,m:1500.0000,q:3,ap:0/3598
# d:1.730,dm:1.730,dM:1.730,p:2.291,s:0.0,m:1504.5000,q:3,ap:0/3598
# d:1.750,dm:1.730,dM:1.750,p:6.021,s:0.0,m:1504.5000,q:3,ap:0/3598
# d:1.730,dm:1.730,dM:1.750,p:11.192,s:0.0,m:1504.5000,q:3,ap:0/3598
# d:1.730,dm:1.730,dM:1.750,p:17.999,s:0.0,m:1504.5000,q:3,ap:0/3598
# d:1.730,dm:1.730,dM:1.750,p:26.049,s:0.0,m:1504.5000,q:3,ap:0/3598
#
# [...]
#
# d:1.750,dm:1.690,dM:1.820,p:1614324.750,s:49.6,m:969.1023,q:2,ap:1180/3598
# d:1.820,dm:1.690,dM:1.820,p:1614324.250,s:1.1,m:972.0096,q:2,ap:1180/3598
# MOTOR DISABLED: FILAMENT OUT OF RANGE
# #end: 20250224_232750
print(f"id{self.bench_id}:\tsaving job: {self.current_job}")
spool, vendor, material, batch, description, startstamp = self.current_job
endstamp = datetime.now().strftime("%Y%m%d_%H%M%S")
outfilename = os.path.join("data", f"{spool}_{startstamp}.fslp")
with open(outfilename, "w") as outfile:
outfile.write(F"#bench: {self.serial_number} (#{self.bench_id})\n")
outfile.write(F"#date: {startstamp}\n")
outfile.write(F"#spool: {spool}\n")
outfile.write(F"#material: {vendor} {material} {batch} {description}\n")
for ts, position, diameter in self.data_list:
outfile.write(f"t:{ts},d:{diameter},p:{position}\n")
outfile.write(F"#end: {endstamp}\n")
subprocess.run(["gzip", "-9", outfilename])
files = {'fsl': open(outfilename+'.gz', 'rb')}
url = f"http://462filament/api.php?data=upload"
r = requests.post(url, files=files)
def parse_data(self, data):
match = re.match(r"^[#d](\d):(-?[\d]+),p:(-?[\d\.]+)$", data)
if match:
diameter_int = int(match.group(2))
if diameter_int<65000:
dial_number = int(match.group(1))
position_value = float(match.group(3))
diameter_value = float(diameter_int)/1000
if diameter_value < self.diameter_min: # process min/max
self.diameter_min = diameter_value
if diameter_value > self.diameter_max:
self.diameter_max = diameter_value
self.update_signal.emit(self.is_running, dial_number, diameter_value, position_value)
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.current_job = (
self.api_job['spool'],
self.api_job['vendor'],
self.api_job['material'],
self.api_job['batch'],
self.api_job['description'],
datetime.now().strftime("%Y%m%d_%H%M%S")
)
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.block.data_label.setText(
f"Job done, processing..."
)
# for position, diameter in self.data_list:
# print(f"{position} -> {diameter}")
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.line1.set_data([], [])
self.line2.set_data([], [])
self.line3.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'))
time.sleep(0.1)
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.line2, = ax.plot([], [], '#00ff50', linewidth=0.5)
self.line3, = ax.plot([], [], '#fffb00', linewidth=0.5)
self.line1, = ax.plot([], [], '#ff5000', linewidth=1.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
self.api_job = api_job
if self.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
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}")