Compare commits

...

2 Commits

Author SHA1 Message Date
Egor Savkin b4eb569957 Add paramtree view, without updates
Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-06-28 15:01:47 +08:00
Egor Savkin d5fb8b8317 Revert "Try move from Qthreads to qasync"
This reverts commit 5b1f2df261.
2023-06-28 14:59:54 +08:00
1 changed files with 131 additions and 63 deletions

View File

@ -8,8 +8,9 @@ import argparse
import logging import logging
import asyncio import asyncio
import atexit import atexit
from qasync import asyncSlot, QEventLoop
import qasync
from pytec.client import Client from pytec.client import Client
from qasync import QEventLoop
# pyuic6 -x tec_qt.ui -o ui_tec_qt.py # pyuic6 -x tec_qt.ui -o ui_tec_qt.py
from ui_tec_qt import Ui_MainWindow from ui_tec_qt import Ui_MainWindow
@ -19,12 +20,58 @@ tec_client: Client = None
# ui = None # ui = None
ui: Ui_MainWindow = None ui: Ui_MainWindow = None
queue = None thread_pool = QThreadPool.globalInstance()
connection_watcher = None connection_watcher = None
client_watcher = None client_watcher = None
app: QtWidgets.QApplication = None app: QtWidgets.QApplication = None
class CommandsParameter(Parameter):
def __init__(self, **opts):
super().__init__()
self.opts["commands"] = opts.get("commands", None)
self.opts["payload"] = opts.get("payload", None)
ThermostatParams = [[
{'name': 'Constant Current', 'type': 'float', 'value': 0, 'step': 0.1, 'limits': (-3, 3), 'siPrefix': True,
'suffix': 'A', 'commands': [f'pwm {ch} i_set {{value}}']},
{'name': 'Temperature PID', 'type': 'bool', 'value': False, 'commands': [f'pwm {ch} pid'], 'payload': ch,
'children': [
{'name': 'Set Temperature', 'type': 'float', 'value': 25, 'step': 0.1, 'limits': (-273, 300), 'siPrefix': True,
'suffix': '°C', 'commands': [f'pid {ch} target {{value}}']},
]},
{'name': 'Output Config', 'expanded': False, 'type': 'group', 'children': [
{'name': 'Max Current', 'type': 'float', 'value': 0, 'step': 0.1, 'limits': (0, 3), 'siPrefix': True,
'suffix': 'A', 'commands': [f'pwm {ch} max_i_pos {{value}}', f'pwm {ch} max_i_neg {{value}}',
f'pid {ch} output_min -{{value}}', f'pid {ch} output_max {{value}}']},
{'name': 'Max Voltage', 'type': 'float', 'value': 0, 'step': 0.1, 'limits': (0, 5), 'siPrefix': True,
'suffix': 'V', 'commands': [f'pwm {ch} max_v {{value}}']},
]},
{'name': 'Thermistor Config', 'expanded': False, 'type': 'group', 'children': [
{'name': 'T0', 'type': 'float', 'value': 25, 'step': 0.1, 'limits': (-100, 100), 'siPrefix': True,
'suffix': 'C', 'commands': [f's-h {ch} t0 {{value}}']},
{'name': 'R0', 'type': 'float', 'value': 10000, 'step': 1, 'siPrefix': True, 'suffix': 'Ohm',
'commands': [f's-h {ch} r0 {{value}}']},
{'name': 'Beta', 'type': 'float', 'value': 3950, 'step': 1, 'commands': [f's-h {ch} b {{value}}']},
]},
{'name': 'PID Config', 'expanded': False, 'type': 'group', 'children': [
{'name': 'kP', 'type': 'float', 'value': 0, 'step': 0.1, 'commands': [f'pid {ch} kp {{value}}']},
{'name': 'kI', 'type': 'float', 'value': 0, 'step': 0.1, 'commands': [f'pid {ch} ki {{value}}']},
{'name': 'kD', 'type': 'float', 'value': 0, 'step': 0.1, 'commands': [f'pid {ch} kd {{value}}']},
{'name': 'PID Auto Tune', 'expanded': False, 'type': 'group', 'children': [
{'name': 'Target Temperature', 'type': 'float', 'value': 20, 'step': 0.1, 'siPrefix': True, 'suffix': 'C'},
{'name': 'Test Current', 'type': 'float', 'value': 1, 'step': 0.1, 'siPrefix': True, 'suffix': 'A'},
{'name': 'Temperature Swing', 'type': 'float', 'value': 1.5, 'step': 0.1, 'siPrefix': True, 'suffix': 'C'},
{'name': 'Run', 'type': 'action', 'tip': 'Run'},
]},
]}
] for ch in range(2)]
params = [CommandsParameter.create(name='Thermostat Params 0', type='group', children=ThermostatParams[0]),
CommandsParameter.create(name='Thermostat Params 1', type='group', children=ThermostatParams[1])]
def get_argparser(): def get_argparser():
parser = argparse.ArgumentParser(description="ARTIQ master") parser = argparse.ArgumentParser(description="ARTIQ master")
@ -38,43 +85,7 @@ def get_argparser():
return parser return parser
def wrap_client_task(func, *args, **kwargs): class WatchConnectTask(QThread):
loop = asyncio.get_event_loop()
task = ClientTask(func, *args, **kwargs)
asyncio.ensure_future(queue.put(task), loop=loop)
async def process_client_tasks():
global queue
if queue is None:
queue = asyncio.Queue()
loop = asyncio.get_event_loop()
while True:
task = await queue.get()
await task.run()
queue.task_done()
class ClientTask:
def __init__(self, func, *args, **kwargs):
self.func = func
self.args = args
self.kwargs = kwargs
super().__init__()
async def run(self):
try:
lock = asyncio.Lock()
async with lock:
self.func(*self.args, **self.kwargs)
except (TimeoutError, OSError):
logging.warning("Client connection error, disconnecting", exc_info=True)
if connection_watcher:
#thread_pool.clear() # clearing all next requests
connection_watcher.client_disconnected()
class WatchConnectTask(QObject):
connected = pyqtSignal(bool) connected = pyqtSignal(bool)
hw_rev = pyqtSignal(dict) hw_rev = pyqtSignal(dict)
connecting = pyqtSignal() connecting = pyqtSignal()
@ -96,8 +107,8 @@ class WatchConnectTask(QObject):
self.connecting.emit() self.connecting.emit()
tec_client = Client(host=self.ip, port=self.port, timeout=30) tec_client = Client(host=self.ip, port=self.port, timeout=30)
self.connected.emit(True) self.connected.emit(True)
wrap_client_task(lambda: self.hw_rev.emit(tec_client.hw_rev())) thread_pool.start(ClientTask(lambda: self.hw_rev.emit(tec_client.hw_rev())))
# wrap_client_task(lambda: self.fan_update.emit(tec_client.fan())) thread_pool.start(ClientTask(lambda: self.fan_update.emit(tec_client.fan())))
except Exception as e: except Exception as e:
logging.error(f"Failed communicating to the {self.ip}:{self.port}: {e}") logging.error(f"Failed communicating to the {self.ip}:{self.port}: {e}")
self.connected.emit(False) self.connected.emit(False)
@ -111,8 +122,7 @@ class WatchConnectTask(QObject):
self.connected.emit(False) self.connected.emit(False)
class ClientWatcher(QThread):
class ClientWatcher(QObject):
fan_update = pyqtSignal(object) fan_update = pyqtSignal(object)
pwm_update = pyqtSignal(object) pwm_update = pyqtSignal(object)
report_update = pyqtSignal(object) report_update = pyqtSignal(object)
@ -123,28 +133,48 @@ class ClientWatcher(QObject):
self.running = True self.running = True
super().__init__(parent) super().__init__(parent)
async def run(self): def run(self):
while self.running: while self.running:
wrap_client_task(lambda: self.update_params()) thread_pool.start(ClientTask(lambda: self.update_params()))
await asyncio.sleep(int(self.update_s * 1000)) self.msleep(int(self.update_s * 1000))
def update_params(self): def update_params(self):
self.fan_update.emit(tec_client.fan()) self.fan_update.emit(tec_client.fan())
self.pwm_update.emit(tec_client.get_pwm())
self.report_update.emit(tec_client._command("report"))
self.pid_update.emit(tec_client.get_pid())
@pyqtSlot() @pyqtSlot()
def stop_watching(self): def stop_watching(self):
self.running = False self.running = False
#deadline = QDeadlineTimer() deadline = QDeadlineTimer()
#deadline.setDeadline(100) deadline.setDeadline(100)
#self.wait(deadline) self.wait(deadline)
#self.terminate() self.terminate()
@pyqtSlot() @pyqtSlot()
def set_update_s(self): def set_update_s(self):
self.update_s = ui.report_refresh_spin.value() self.update_s = ui.report_refresh_spin.value()
def on_connection_changed(result): class ClientTask(QRunnable):
def __init__(self, func, *args, **kwargs):
self.func = func
self.args = args
self.kwargs = kwargs
super().__init__()
def run(self):
try:
self.func(*self.args, **self.kwargs)
except (TimeoutError, OSError):
logging.warning("Client connection error, disconnecting", exc_info=True)
if connection_watcher:
thread_pool.clear() # clearing all next requests
connection_watcher.client_disconnected()
def connected(result):
global client_watcher, connection_watcher global client_watcher, connection_watcher
ui.graph_group.setEnabled(result) ui.graph_group.setEnabled(result)
ui.hw_rev_lbl.setEnabled(result) ui.hw_rev_lbl.setEnabled(result)
@ -166,7 +196,7 @@ def on_connection_changed(result):
client_watcher.fan_update.connect(fan_update) client_watcher.fan_update.connect(fan_update)
ui.report_apply_btn.clicked.connect(client_watcher.set_update_s) ui.report_apply_btn.clicked.connect(client_watcher.set_update_s)
app.aboutToQuit.connect(client_watcher.stop_watching) app.aboutToQuit.connect(client_watcher.stop_watching)
wrap_client_task(client_watcher.run) client_watcher.start()
def hw_rev(hw_rev_d: dict): def hw_rev(hw_rev_d: dict):
@ -196,7 +226,7 @@ def fan_set():
global tec_client global tec_client
if tec_client is None or ui.fan_auto_box.isChecked(): if tec_client is None or ui.fan_auto_box.isChecked():
return return
wrap_client_task(lambda: tec_client.set_param("fan", ui.fan_power_slider.value())) thread_pool.start(ClientTask(lambda: tec_client.set_param("fan", ui.fan_power_slider.value())))
def fan_auto_set(enabled): def fan_auto_set(enabled):
@ -205,24 +235,60 @@ def fan_auto_set(enabled):
return return
ui.fan_power_slider.setEnabled(not enabled) ui.fan_power_slider.setEnabled(not enabled)
if enabled: if enabled:
wrap_client_task(lambda: tec_client.set_param("fan", "auto")) thread_pool.start(ClientTask(lambda: tec_client.set_param("fan", "auto")))
else: else:
wrap_client_task(lambda: tec_client.set_param("fan", ui.fan_power_slider.value())) thread_pool.start(ClientTask(lambda: tec_client.set_param("fan", ui.fan_power_slider.value())))
def connect(): def connect():
global connection_watcher global connection_watcher
connection_watcher = WatchConnectTask(ui.main_widget, ui.ip_set_line.text(), ui.port_set_spin.value()) connection_watcher = WatchConnectTask(ui.main_widget, ui.ip_set_line.text(), ui.port_set_spin.value())
connection_watcher.connected.connect(on_connection_changed) connection_watcher.connected.connect(connected)
connection_watcher.connecting.connect(lambda: ui.status_lbl.setText("Connecting...")) connection_watcher.connecting.connect(lambda: ui.status_lbl.setText("Connecting..."))
connection_watcher.hw_rev.connect(hw_rev) connection_watcher.hw_rev.connect(hw_rev)
connection_watcher.fan_update.connect(fan_update) connection_watcher.fan_update.connect(fan_update)
wrap_client_task(connection_watcher.run) connection_watcher.start()
#app.aboutToQuit.connect(connection_watcher.terminate) app.aboutToQuit.connect(connection_watcher.terminate)
def update_pid(pid_settings):
for settings in pid_settings:
channel = settings["channel"]
with QSignalBlocker(params[channel].sigTreeStateChanged) as _:
params[channel].child("PID Config", "kP").setValue(settings["parameters"]["kp"])
params[channel].child("PID Config", "kI").setValue(settings["parameters"]["ki"])
params[channel].child("PID Config", "kD").setValue(settings["parameters"]["kd"])
if params[channel].child("Temperature PID").value():
params[channel].child("Temperature PID", "Set Temperature").setValue(settings["target"])
def update_report(report_data):
for settings in report_data:
channel = settings["channel"]
with QSignalBlocker(params[channel].sigTreeStateChanged) as _:
params[channel].child("Temperature PID").setValue(settings["pid_engaged"])
def send_command(param, changes):
for param, change, data in changes:
if param.name() == 'Temperature PID' and not data:
ch = param.opts["payload"]
thread_pool.start(ClientTask(
lambda: tec_client.set_param('pwm', ch, 'i_set', params[ch].child('Constant Current').value())))
elif param.opts.get("commands", None) is not None:
thread_pool.start(ClientTask(lambda: [tec_client._command(x.format(value=data)) for x in param.opts["commands"]]))
def set_param_tree():
ui.ch0_tree.setParameters(params[0], showTop=False)
ui.ch1_tree.setParameters(params[1], showTop=False)
params[0].sigTreeStateChanged.connect(send_command)
params[1].sigTreeStateChanged.connect(send_command)
def main(): def main():
global ui, app, queue global ui, thread_pool, app
args = get_argparser().parse_args() args = get_argparser().parse_args()
if args.logLevel: if args.logLevel:
logging.basicConfig(level=getattr(logging, args.logLevel)) logging.basicConfig(level=getattr(logging, args.logLevel))
@ -233,17 +299,20 @@ def main():
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
atexit.register(loop.close) atexit.register(loop.close)
loop.create_task(process_client_tasks())
main_window = QtWidgets.QMainWindow() main_window = QtWidgets.QMainWindow()
ui = Ui_MainWindow() ui = Ui_MainWindow()
ui.setupUi(main_window) ui.setupUi(main_window)
# ui = uic.loadUi('tec_qt.ui', main_window) # ui = uic.loadUi('tec_qt.ui', main_window)
thread_pool = QThreadPool(parent=ui.main_widget)
thread_pool.setMaxThreadCount(1) # avoid concurrent requests
ui.connect_btn.clicked.connect(lambda _checked: connect()) ui.connect_btn.clicked.connect(lambda _checked: connect())
ui.fan_power_slider.valueChanged.connect(fan_set) ui.fan_power_slider.valueChanged.connect(fan_set)
ui.fan_auto_box.stateChanged.connect(fan_auto_set) ui.fan_auto_box.stateChanged.connect(fan_auto_set)
set_param_tree()
if args.connect: if args.connect:
if args.IP: if args.IP:
ui.ip_set_line.setText(args.IP) ui.ip_set_line.setText(args.IP)
@ -252,8 +321,7 @@ def main():
ui.connect_btn.click() ui.connect_btn.click()
main_window.show() main_window.show()
sys.exit(app.exec())
loop.run_until_complete(app.exec())
if __name__ == '__main__': if __name__ == '__main__':