1
0
Fork 0

Compare commits

...

10 Commits

Author SHA1 Message Date
linuswck 26f12dbf77 add draft for thermostat usage guide 2024-10-18 12:58:20 +08:00
linuswck 388e6725ef thermostat: Clear alarm when settings are loaded 2024-10-17 17:36:17 +08:00
linuswck f4c761c5ca pid: Scale PID Parameters with Sampling Rate
- Breaking changes
2024-10-17 17:36:06 +08:00
linuswck e560d8f1eb thermostat, gui: Set max v range according to datasheet 2024-10-16 16:21:11 +08:00
linuswck 0437c6e76e thermostat, gui: Relax the max settable TEC Current Limit
- Improper settings can cause PoE power failure and over heating
- See hardware repo Issue #51
2024-10-16 16:14:58 +08:00
linuswck bad21806f8 gui: Add new form for temp adc filter cfg and settable polling rate
List of Changes:
1. Get report via polling instead of active report mode
2. Allow user to set a custom report polling rate while settings polling rate is fixed to 10Hz
- it is necessary for pid autotune to function correctly
3. Add a form for configuring temperature adc filter
4. Use two different timer for polling report and settings to optimize performance

Known Issue:
1. CPU utilization increases with the report polling rate as for each report recv-ed, gui renders and plots one frame of the 4 graphs
2024-10-10 17:25:33 +08:00
linuswck cb2bc505c9 driver: expose _odr_type for filter cfgs 2024-10-10 17:25:29 +08:00
linuswck 4f19d2c2be driver: Expose the upper and lower limit settings for Sinc3WithFineODR Filter Cfg 2024-10-10 15:23:57 +08:00
linuswck e8d3858fc9 driver: return whether task queue is full in task_dispatcher() 2024-10-10 15:20:58 +08:00
linuswck 9bec56ed6c driver: Add support for GetPollInterval cmd 2024-10-10 15:19:43 +08:00
10 changed files with 655 additions and 79 deletions

56
docs/thermostat.md Normal file
View File

@ -0,0 +1,56 @@
# Thermostat
Kirdy incorporates a single channel thermostat circuitry like Sinara-hw [Thermostat](https://github.com/sinara-hw/Thermostat) with improved noise. It can deliver more than 9W of cooling/heating power with better than +-1mK temperature stability.
## Power Input
If you use Kirdy with a PoE (IEEE 802.3af Class 3) PSE, Kirdy may experience power failure due to power limit. If you need more power on Thermostat, you can plug in additional Power supply on the 12V power Jack(Recommend: 12V 2A). Although the TEC Driver has a very high 165 degree thermal shutdown threshold, it is highly recommended to provide active cooling to the TEC Driving Circuitry.
## Control Mode
Like Sinara-hw [Thermostat](https://github.com/sinara-hw/Thermostat), they both share the same control mode and algorithm, namely constant Current Control Mode
### Constant Current Control Mode
You can drive a constant current to the load. The end user to connect an external control loop. If end user wants to use it with an external control loop, it is recommended to retrieve reports via the ActiveReportMode instead of polling status report for better consistency and higher maximum update rate.
### PID Control Mode
Thermostat uses the [PID algorithm](https://hackmd.io/IACbwcOTSt6Adj3_F9bKuw) derived by Quartiq. The included Pid Autotune Algorithm can already achieve the specified +-1mK stability.
During autotuning, Theremstat drives a user-specified step current into the load to calculate the pid parameters. Improper usage can damage the connected device.
#### Important Notes
1. Configure all the protection mechanisms correctly before running the pid autotune algorithm.
2. Supervise the PID autotune process.
3. PID Autotune algorithm sometimes can produce unstable PID parameters. Do evaluate the PID performance after autotune is completed
4. For best performance, you should do the autotune with Kirdy installed in the environment that it is gonna be used.
5. If you are using the GUI, you need to set a high enough polling rate for PID autotune to function correctly.
#### Usage Guide
You can run pid autotune via the standalone auto
1. Target Temperature
- Set it to the intended operating temperature for the connected device to operate in. You can still change temperature setpoint afterwards.
2. Test Current
- Set it to the maximum recommended operating current specified on the TEC/Resistive load.
3. Update Rate
- This can be set by configuring the filter settings in the Temperature ADC. High Update Rate can yield faster loop response with temperature regulation noise as the trade-off. Higher update rate can be useful if the load is actively cooled.
4. Temperature Swing
- Lower value can damp the control loop response.
5. Lookback Period
- Larger value can damp the control loop response. Do note that the larger lookback period also increases the autotune completion time.
You can give your system a try with the following parameters.
1. Target Temperature: (User-Specified)
2. Test Current: (User-Specified)
3. Update Rate: 16.67Hz (Sinc5Sinc1 Filter with 50Hz and 60Hz Rejection)
4. Temperature Swing: 0 Degree
5. Lookback Period: 5s
Once autotune is successful, you can improve its stability by increasing the lookback period.
#### Troubleshooting
If you obtain unstable PID parameters,
1. Check if the polling rate is close to the PID update rate. If not, increase the polling rate
2. Increase the lookback period and try again.
If the autotune does not finish running or faikls, you should
1. Check if the thermostat has a hard time heating or cooling the connected device to the targe temperature. If yes, you should consider adjust the target temperature accordingly.
2. Check if the polling rate is close to the PID update rate. If not, increase the polling rate.

View File

@ -85,21 +85,22 @@ async def ld_thermostat_cfg(kirdy: Kirdy):
await kirdy.thermostat.set_temp_mon_upper_limit(70) await kirdy.thermostat.set_temp_mon_upper_limit(70)
await kirdy.thermostat.set_temp_mon_lower_limit(0) await kirdy.thermostat.set_temp_mon_lower_limit(0)
# Configure the thermostat PID parameters.
# You can configure the PID parameter by the included autotune script.
await kirdy.thermostat.set_temperature_setpoint(25)
await kirdy.thermostat.set_pid_kp(0.15668282198105507)
await kirdy.thermostat.set_pid_ki(0.002135962407793784)
await kirdy.thermostat.set_pid_kd(0.829254515277143)
await kirdy.thermostat.set_pid_output_max(1.0)
await kirdy.thermostat.set_pid_output_min(-1.0)
# Configure the thermostat ADC Filter Setting / PID Update Rate / Report Rate. # Configure the thermostat ADC Filter Setting / PID Update Rate / Report Rate.
# The ADC sampling rate determines the report and pid update rate. # The ADC sampling rate determines the report and pid update rate.
# The chosen filter and sampling rate affects the noise of the readings. # The chosen filter and sampling rate affects the noise of the readings.
# For details, please refer to the AD7172 datasheet. # For details, please refer to the AD7172 datasheet.
await kirdy.thermostat.config_temp_adc_filter(FilterConfig.Sinc5Sinc1With50hz60HzRejection.f16sps) await kirdy.thermostat.config_temp_adc_filter(FilterConfig.Sinc5Sinc1With50hz60HzRejection.f16sps)
# Configure the thermostat PID parameters.
# You can configure the PID parameter by the included autotune script.
# You should configure the filter sampling rate first before applying pid parameters.
await kirdy.thermostat.set_temperature_setpoint(25)
await kirdy.thermostat.set_pid_kp(0.15668282198105507)
await kirdy.thermostat.set_pid_ki(0.0001281321)
await kirdy.thermostat.set_pid_kd(13.82367)
await kirdy.thermostat.set_pid_output_max(1.0)
await kirdy.thermostat.set_pid_output_min(-1.0)
# Configure thermostat to run in PID control mode # Configure thermostat to run in PID control mode
await kirdy.thermostat.set_pid_control_mode() await kirdy.thermostat.set_pid_control_mode()

View File

@ -66,6 +66,7 @@ class CmdList:
SetPidOutMin = _dt.f32, SetPidOutMin = _dt.f32,
SetPidOutMax = _dt.f32, SetPidOutMax = _dt.f32,
ConfigTempAdcFilter = _dt.temp_adc_filter, ConfigTempAdcFilter = _dt.temp_adc_filter,
GetPollInterval = _dt.none,
SetTempMonUpperLimit = _dt.f32, SetTempMonUpperLimit = _dt.f32,
SetTempMonLowerLimit = _dt.f32, SetTempMonLowerLimit = _dt.f32,
ClearAlarm = _dt.none, ClearAlarm = _dt.none,
@ -79,13 +80,19 @@ class FilterConfig:
f21sps = "F21SPS" f21sps = "F21SPS"
f20sps = "F20SPS" f20sps = "F20SPS"
f16sps = "F16SPS" f16sps = "F16SPS"
_odr_type = "sinc5sinc1postfilter"
def _odr_type(self):
return "sinc5sinc1postfilter"
def _filter_type(self): def _filter_type(self):
return "Sinc5Sinc1With50hz60HzRejection" return "Sinc5Sinc1With50hz60HzRejection"
@classmethod
def get_list_of_settings(cls):
ret = []
for e in cls:
if e not in [cls._odr_type]:
ret.append(e)
return ret
class Sinc5Sinc1(StrEnum): class Sinc5Sinc1(StrEnum):
f31250_0sps = "F31250_0SPS" f31250_0sps = "F31250_0SPS"
f15625_0sps = "F15625_0SPS" f15625_0sps = "F15625_0SPS"
@ -105,13 +112,19 @@ class FilterConfig:
f5_0sps = "F5_0SPS" f5_0sps = "F5_0SPS"
f2_5sps = "F2_5SPS" f2_5sps = "F2_5SPS"
f1_25sps = "F1_25SPS" f1_25sps = "F1_25SPS"
_odr_type = "sinc5sinc1odr"
def _odr_type(self):
return "sinc5sinc1odr"
def _filter_type(self): def _filter_type(self):
return "Sinc5Sinc1" return "Sinc5Sinc1"
@classmethod
def get_list_of_settings(cls):
ret = []
for e in cls:
if e not in [cls._odr_type]:
ret.append(e)
return ret
class Sinc3(StrEnum): class Sinc3(StrEnum):
f31250_0sps = "F31250_0SPS" f31250_0sps = "F31250_0SPS"
f15625_0sps = "F15625_0SPS" f15625_0sps = "F15625_0SPS"
@ -130,21 +143,28 @@ class FilterConfig:
f10_0sps = "F10_0SPS" f10_0sps = "F10_0SPS"
f5_0sps = "F5_0SPS" f5_0sps = "F5_0SPS"
f2_5sps = "F2_5SPS" f2_5sps = "F2_5SPS"
f1_25sps = "F1_25SPS" f1_25sps = "F1_25SPS"
_odr_type = "sinc3odr"
def _odr_type(self):
return "sinc3odr"
def _filter_type(self): def _filter_type(self):
return "Sinc3" return "Sinc3"
@classmethod
def get_list_of_settings(cls):
ret = []
for e in cls:
if e not in [cls._odr_type]:
ret.append(e)
return ret
class Sinc3WithFineODR(): class Sinc3WithFineODR():
def __init__(self, rate): upper_limit = 31250
assert rate >= 1.907465 and rate <= 31250 lower_limit = 1.907465
self.rate = float(rate) _odr_type = "sinc3fineodr"
def _odr_type(self): def __init__(self, rate):
return "sinc3fineodr" assert rate >= self.lower_limit and rate <= self.upper_limit
self.rate = float(rate)
def _filter_type(self): def _filter_type(self):
return "Sinc3WithFineODR" return "Sinc3WithFineODR"
@ -289,19 +309,19 @@ class Device:
'tec_settings': { 'tec_settings': {
'i_set': { # Current TEC Current Set by PID Controller/User (A) 'i_set': { # Current TEC Current Set by PID Controller/User (A)
'value': 0.04330516, # Value Set 'value': 0.04330516, # Value Set
'max': 1.0 # Max Value Settable 'max': 3.0 # Max Value Settable
}, },
'max_v': { # Max Voltage Across Tec Terminals (V) 'max_v': { # Max Voltage Across Tec Terminals (V)
'value': 4.990857, # Value Set 'value': 4.00000000, # Value Set
'max': 5.0 # Max Value Settable 'max': 4.3 # Max Value Settable
}, },
'max_i_pos': { # Max Cooling Current Across Tec Terminals (A) 'max_i_pos': { # Max Cooling Current Across Tec Terminals (A)
'value': 0.99628574, # Value Set 'value': 0.99628574, # Value Set
'max': 1.0 # Max Value Settable 'max': 3.0 # Max Value Settable
}, },
'max_i_neg': { # Max Heating Current Across Tec Terminals (A) 'max_i_neg': { # Max Heating Current Across Tec Terminals (A)
'value': 0.99628574, # Value Set 'value': 0.99628574, # Value Set
'max': 1.0 # Max Value Settable 'max': 3.0 # Max Value Settable
} }
}, },
'pid_params': { # PID Controller Parameters 'pid_params': { # PID Controller Parameters
@ -473,21 +493,21 @@ class Thermostat:
async def set_tec_max_cooling_i(self, max_i_pos): async def set_tec_max_cooling_i(self, max_i_pos):
""" """
Set Tec maximum cooling current (Settable Range: 0.0 - 1.0) Set Tec maximum cooling current (Settable Range: 0.0 - 3.0)
- max_i_pos: A - max_i_pos: A
""" """
return await self._send_cmd(self._cmd._target, self._cmd.SetTecMaxIPos, max_i_pos) return await self._send_cmd(self._cmd._target, self._cmd.SetTecMaxIPos, max_i_pos)
async def set_tec_max_heating_i(self, max_i_neg): async def set_tec_max_heating_i(self, max_i_neg):
""" """
Set Tec maximum heating current (Settable Range: 0.0 - 1.0) Set Tec maximum heating current (Settable Range: 0.0 - 3.0)
- max_i_neg: A - max_i_neg: A
""" """
return await self._send_cmd(self._cmd._target, self._cmd.SetTecMaxINeg, max_i_neg) return await self._send_cmd(self._cmd._target, self._cmd.SetTecMaxINeg, max_i_neg)
async def set_tec_i_out(self, i_out): async def set_tec_i_out(self, i_out):
""" """
Set Tec Output Current (Settable Range: 0.0 - 1.0) Set Tec Output Current (Settable Range: 0.0 - 3.0)
This cmd is only effective in constant current control mode This cmd is only effective in constant current control mode
or your newly set value will be overwritten by PID Controller Output or your newly set value will be overwritten by PID Controller Output
- i_out: A - i_out: A
@ -611,15 +631,19 @@ class Thermostat:
if hasattr(filter_config, 'rate'): if hasattr(filter_config, 'rate'):
cmd[self._cmd.ConfigTempAdcFilter] = { cmd[self._cmd.ConfigTempAdcFilter] = {
"filter_type": filter_config._filter_type(), "filter_type": filter_config._filter_type(),
filter_config._odr_type(): filter_config.rate, filter_config._odr_type: filter_config.rate,
} }
else: else:
cmd[self._cmd.ConfigTempAdcFilter] = { cmd[self._cmd.ConfigTempAdcFilter] = {
"filter_type": filter_config._filter_type(), "filter_type": filter_config._filter_type(),
filter_config._odr_type(): filter_config, filter_config._odr_type: filter_config,
} }
return await self._send_raw_cmd(cmd) return await self._send_raw_cmd(cmd)
async def get_poll_interval(self):
return await self._send_cmd(self._cmd._target, self._cmd.GetPollInterval, msg_type="Interval")
class Kirdy: class Kirdy:
def __init__(self): def __init__(self):
self.device = Device(self._send_cmd, self._send_raw_cmd) self.device = Device(self._send_cmd, self._send_raw_cmd)
@ -754,7 +778,11 @@ class Kirdy:
Enqueue a task to be handled by the handler. Enqueue a task to be handled by the handler.
""" """
if self.connected(): if self.connected():
self._task_queue.put_nowait(lambda: awaitable_fn) try:
self._task_queue.put_nowait(lambda: awaitable_fn)
return True
except asyncio.queues.QueueFull:
return False
else: else:
raise ConnectionError raise ConnectionError

View File

@ -18,7 +18,7 @@ import os
import argparse import argparse
import logging import logging
import asyncio import asyncio
from driver.kirdy import Kirdy as Kirdy_Driver from driver.kirdy import FilterConfig, Kirdy as Kirdy_Driver
import qasync import qasync
from qasync import asyncClose, asyncSlot from qasync import asyncClose, asyncSlot
from collections import deque from collections import deque
@ -28,6 +28,7 @@ from typing import Any, Optional, List
from ui.ui_conn_settings_form import Ui_Conn_Settings_Form from ui.ui_conn_settings_form import Ui_Conn_Settings_Form
from ui.ui_config_pd_mon_form import Ui_Cfg_Pd_Mon_Form from ui.ui_config_pd_mon_form import Ui_Cfg_Pd_Mon_Form
from ui.ui_update_network_settings_form import Ui_Update_Network_Settings_Form from ui.ui_update_network_settings_form import Ui_Update_Network_Settings_Form
from ui.ui_config_adc_filter_form import Ui_Cfg_Adc_Filter_Form
from dateutil import tz from dateutil import tz
import math import math
import socket import socket
@ -79,14 +80,21 @@ class Kirdy(QObject):
def __init__(self, parent, kirdy, _poll_interval): def __init__(self, parent, kirdy, _poll_interval):
super().__init__(parent) super().__init__(parent)
self._poll_interval = _poll_interval self._poll_interval = _poll_interval
self._default_poll_interval = _poll_interval
self._kirdy = kirdy self._kirdy = kirdy
self._kirdy.set_connected_sig(self.connected_sig) self._kirdy.set_connected_sig(self.connected_sig)
self.connected_sig.connect(self.start_polling) self.connected_sig.connect(self.start_polling)
self.connected_sig.connect(self.connected_setup) self._noti_info_box = QtWidgets.QMessageBox()
self._noti_info_box.setIcon(QtWidgets.QMessageBox.Icon.Information)
self._kirdy.set_report_sig(self.report_update_sig)
self._kirdy.set_err_msg_sig(self.cmd_fail_sig) self._kirdy.set_err_msg_sig(self.cmd_fail_sig)
self._timer = QtCore.QBasicTimer() self._poll_report_timer = QtCore.QTimer()
self._poll_report_timer.timeout.connect(self.polling_event)
self.poll_settings_timer = QtCore.QTimer()
self.poll_settings_timer.setInterval(100)
self.poll_settings_timer.timeout.connect(self.polling_settings_event)
def connected(self): def connected(self):
return self._kirdy.connected() return self._kirdy.connected()
@ -98,28 +106,37 @@ class Kirdy(QObject):
self._kirdy.start_session(host=host, port=port) self._kirdy.start_session(host=host, port=port)
def end_session(self): def end_session(self):
if self._timer.isActive(): if self._poll_report_timer.isActive():
self._timer.stop() self._poll_report_timer.stop()
asyncio.get_running_loop().create_task(self._kirdy.end_session()) asyncio.get_running_loop().create_task(self._kirdy.end_session())
@pyqtSlot(bool) @pyqtSlot()
def connected_setup(self, connected): def polling_settings_event(self):
if connected:
self._kirdy.task_dispatcher(self._kirdy.device.set_active_report_mode(True))
self._kirdy._report_mode_on = True
def timerEvent(self, event):
self._kirdy.task_dispatcher(self._kirdy.device.get_settings_summary(sig=self.setting_update_sig)) self._kirdy.task_dispatcher(self._kirdy.device.get_settings_summary(sig=self.setting_update_sig))
@pyqtSlot()
def polling_event(self):
success = True
success &= self._kirdy.task_dispatcher(self._kirdy.device.get_status_report(sig=self.report_update_sig))
if not(success):
self._noti_info_box.setWindowTitle("Polling rate is too high")
self._noti_info_box.setText(f"Kirdy cannot handle {1/(self._poll_interval)} Hz polling rate. Reset to default polling rate ({1/self._default_poll_interval} Hz)")
self._noti_info_box.show()
self.set_update_s(self._default_poll_interval)
@pyqtSlot(bool) @pyqtSlot(bool)
def start_polling(self, start): def start_polling(self, start):
if start: if start:
if not(self._timer.isActive()): if not(self._poll_report_timer.isActive()):
self._timer.start(int(self._poll_interval*1000), self) self._poll_report_timer.setInterval(int(self._poll_interval*1000))
self._poll_report_timer.start()
self.poll_settings_timer.start()
else: else:
logging.debug("Kirdy Polling Timer has been started already.") logging.debug("Kirdy Polling Timer has been started already.")
else: else:
self._timer.stop() self._poll_report_timer.stop()
self.poll_settings_timer.stop()
@pyqtSlot(float) @pyqtSlot(float)
def set_update_s(self, interval): def set_update_s(self, interval):
@ -127,9 +144,9 @@ class Kirdy(QObject):
self.update_polling_rate() self.update_polling_rate()
def update_polling_rate(self): def update_polling_rate(self):
if self._timer.isActive(): if self._poll_report_timer.isActive():
self._timer.stop() self._poll_report_timer.stop()
self.start_polling() self.start_polling(True)
else: else:
logging.debug("Attempt to update polling timer when it is stopped") logging.debug("Attempt to update polling timer when it is stopped")
@ -345,6 +362,38 @@ class ConnSettingsForm(QtWidgets.QDialog, Ui_Conn_Settings_Form):
except (OSError, ValueError): except (OSError, ValueError):
return None return None
class ConfigAdcFilterForm(QtWidgets.QDialog, Ui_Cfg_Adc_Filter_Form):
def __init__(self):
super().__init__()
self.setupUi(self)
self.filter_type_cbox.addItems(['Sinc5Sinc1With50hz60HzRejection', 'Sinc5Sinc1', 'Sinc3', 'Sinc3WithFineODR'])
self.fine_filter_sampling_rate_spinbox.setVisible(False)
self.fine_filter_sampling_rate_spinbox.setMinimum(FilterConfig.Sinc3WithFineODR.lower_limit)
self.fine_filter_sampling_rate_spinbox.setMaximum(FilterConfig.Sinc3WithFineODR.upper_limit)
self.filter_type_cbox.currentTextChanged.connect(self.sampling_rate_cbox_config)
@pyqtSlot(str)
def sampling_rate_cbox_config(self, filter_type):
if filter_type == "":
return
if filter_type == "Sinc3WithFineODR":
self.filter_sampling_rate_cbox.setVisible(False)
self.fine_filter_sampling_rate_spinbox.setVisible(True)
else:
self.fine_filter_sampling_rate_spinbox.setVisible(False)
self.filter_sampling_rate_cbox.setVisible(True)
self.filter_sampling_rate_cbox.clear()
self.filter_sampling_rate_cbox.addItems(getattr(FilterConfig, filter_type).get_list_of_settings())
def get_filter_settings(self):
filter_type = self.filter_type_cbox.currentText()
if filter_type == "Sinc3WithFineODR":
return getattr(FilterConfig, filter_type)(self.fine_filter_sampling_rate_spinbox.value())
else:
filter_type_val = getattr(FilterConfig, filter_type)
filter_cfg = getattr(filter_type_val, self.filter_sampling_rate_cbox.currentText().lower())
return filter_cfg
class MainWindow(QtWidgets.QMainWindow): class MainWindow(QtWidgets.QMainWindow):
"""The maximum number of sample points to store.""" """The maximum number of sample points to store."""
DEFAULT_MAX_SAMPLES = 1000 DEFAULT_MAX_SAMPLES = 1000
@ -395,28 +444,33 @@ class MainWindow(QtWidgets.QMainWindow):
{'name': 'Output Config', 'expanded': True, 'type': 'group', 'children': [ {'name': 'Output Config', 'expanded': True, 'type': 'group', 'children': [
{'name': 'Control Method', 'type': 'mutex', 'limits': ['Constant Current', 'Temperature PID'], {'name': 'Control Method', 'type': 'mutex', 'limits': ['Constant Current', 'Temperature PID'],
'target_action_pair': [['thermostat', 'set_constant_current_control_mode'], ['thermostat', 'set_pid_control_mode']], 'children': [ 'target_action_pair': [['thermostat', 'set_constant_current_control_mode'], ['thermostat', 'set_pid_control_mode']], 'children': [
{'name': 'Set Current', 'type': 'float', 'value': 0, 'step': 1, 'limits': (-1000, 1000), 'triggerOnShow': True, 'decimals': 6, {'name': 'Set Current', 'type': 'float', 'value': 0, 'step': 1, 'limits': (-3000, 3000), 'triggerOnShow': True, 'decimals': 6,
'unit': 'mA', 'lock': False, 'target': 'thermostat', 'action': 'set_tec_i_out', "compactHeight": False}, 'unit': 'mA', 'lock': False, 'target': 'thermostat', 'action': 'set_tec_i_out', "compactHeight": False},
{'name': 'Set Temperature', 'type': 'float', 'value': 25, 'step': 0.0001, 'limits': (-273, 300), 'format': '{value:.4f}', {'name': 'Set Temperature', 'type': 'float', 'value': 25, 'step': 0.0001, 'limits': (-273, 300), 'format': '{value:.4f}',
'unit': '', 'lock': False, 'target': 'thermostat', 'action': 'set_temperature_setpoint', "compactHeight": False}, 'unit': '', 'lock': False, 'target': 'thermostat', 'action': 'set_temperature_setpoint', "compactHeight": False},
]}, ]},
{'name': 'Limits', 'expanded': False, 'type': 'group', 'children': [ {'name': 'Limits', 'expanded': False, 'type': 'group', 'children': [
{'name': 'Max Cooling Current', 'type': 'float', 'value': 0, 'step': 1, 'decimals': 6, 'limits': (0, 1000), {'name': 'Max Cooling Current', 'type': 'float', 'value': 0, 'step': 1, 'decimals': 6, 'limits': (0, 3000),
'unit': 'mA', 'lock': False, 'target': 'thermostat', 'action': 'set_tec_max_cooling_i', "compactHeight": False}, 'unit': 'mA', 'lock': False, 'target': 'thermostat', 'action': 'set_tec_max_cooling_i', "compactHeight": False},
{'name': 'Max Heating Current', 'type': 'float', 'value': 0, 'step': 1, 'decimals': 6, 'limits': (0, 1000), {'name': 'Max Heating Current', 'type': 'float', 'value': 0, 'step': 1, 'decimals': 6, 'limits': (0, 3000),
'unit': 'mA', 'lock': False, 'target': 'thermostat', 'action': 'set_tec_max_heating_i', "compactHeight": False}, 'unit': 'mA', 'lock': False, 'target': 'thermostat', 'action': 'set_tec_max_heating_i', "compactHeight": False},
{'name': 'Max Voltage Difference', 'type': 'float', 'value': 0, 'step': 0.1, 'limits': (0, 4), {'name': 'Max Voltage Difference', 'type': 'float', 'value': 0, 'step': 0.1, 'limits': (0, 4.3),
'unit': 'V', 'lock': False, 'target': 'thermostat', 'action': 'set_tec_max_v', "compactHeight": False}, 'unit': 'V', 'lock': False, 'target': 'thermostat', 'action': 'set_tec_max_v', "compactHeight": False},
]}, ]},
{'name': 'Default Power On', 'type': 'bool', 'value': False, 'lock': False, 'target': 'thermostat', 'action': 'set_default_pwr_on'}, {'name': 'Default Power On', 'type': 'bool', 'value': False, 'lock': False, 'target': 'thermostat', 'action': 'set_default_pwr_on'},
]}, ]},
# TODO Temperature ADC Filter Settings
{'name': 'Temperature Monitor Config', 'expanded': False, 'type': 'group', 'children': [ {'name': 'Temperature Monitor Config', 'expanded': False, 'type': 'group', 'children': [
{'name': 'Upper Limit', 'type': 'float', 'value': 0, 'step': 1, 'decimals': 6, 'limits': (-273, 300), {'name': 'Upper Limit', 'type': 'float', 'value': 0, 'step': 1, 'decimals': 6, 'limits': (-273, 300),
'unit': '', 'lock': False, 'target': 'thermostat', 'action': 'set_temp_mon_upper_limit', "compactHeight": False}, 'unit': '', 'lock': False, 'target': 'thermostat', 'action': 'set_temp_mon_upper_limit', "compactHeight": False},
{'name': 'Lower Limit', 'type': 'float', 'value': 0, 'step': 1, 'decimals': 6, 'limits': (-273, 300), {'name': 'Lower Limit', 'type': 'float', 'value': 0, 'step': 1, 'decimals': 6, 'limits': (-273, 300),
'unit': '', 'lock': False, 'target': 'thermostat', 'action': 'set_temp_mon_lower_limit', "compactHeight": False}, 'unit': '', 'lock': False, 'target': 'thermostat', 'action': 'set_temp_mon_lower_limit', "compactHeight": False},
]}, ]},
{'name': 'Temperature ADC Filter Settings', 'expanded': False, 'type': 'group', 'children': [
{'name': 'Filter Type', 'type': 'list', 'limits': ['Sinc5Sinc1With50hz60HzRejection', 'Sinc5Sinc1', 'Sinc3', 'Sinc3WithFineODR'], 'readonly': True, "compactHeight": False},
{'name': 'Sampling Rate', 'type': 'float', 'value': 16.67, 'decimals': 4, 'unit': 'Hz', 'readonly': True, "compactHeight": False},
{'name': 'Recorded Sampling Rate', 'type': 'float', 'value': 16.67, 'decimals': 4, 'unit': 'Hz', 'readonly': True, "compactHeight": False},
{'name': 'Configure ADC Filter', 'type': 'action'},
]},
{'name': 'Thermistor Settings','expanded': False, 'type': 'group', 'children': [ {'name': 'Thermistor Settings','expanded': False, 'type': 'group', 'children': [
{'name': 'T₀', 'type': 'float', 'value': 0, 'step': 1, 'decimals': 6, 'limits': (-273, 300), {'name': 'T₀', 'type': 'float', 'value': 0, 'step': 1, 'decimals': 6, 'limits': (-273, 300),
'unit': '', 'lock': False, 'target': 'thermostat', 'action': 'set_sh_t0', "compactHeight": False}, 'unit': '', 'lock': False, 'target': 'thermostat', 'action': 'set_sh_t0', "compactHeight": False},
@ -470,6 +524,8 @@ class MainWindow(QtWidgets.QMainWindow):
self.update_net_settings_form = UpdateNetSettingsForm() self.update_net_settings_form = UpdateNetSettingsForm()
self.update_net_settings_form.accepted.connect(self.update_net_settings) self.update_net_settings_form.accepted.connect(self.update_net_settings)
self.cfg_adc_filter_form = ConfigAdcFilterForm()
self.max_samples = self.DEFAULT_MAX_SAMPLES self.max_samples = self.DEFAULT_MAX_SAMPLES
self.autotuner = PIDAutotune(25) self.autotuner = PIDAutotune(25)
@ -516,6 +572,7 @@ class MainWindow(QtWidgets.QMainWindow):
] ]
self._set_param_tree() self._set_param_tree()
self._set_up_pd_mon_form() self._set_up_pd_mon_form()
self._set_up_adc_filter_form()
self.tec_i_graph.setTitle("TEC Current") self.tec_i_graph.setTitle("TEC Current")
self.tec_temp_graph.setTitle("TEC Temperature") self.tec_temp_graph.setTitle("TEC Temperature")
@ -524,7 +581,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.connect_btn.clicked.connect(self.show_conn_settings_form) self.connect_btn.clicked.connect(self.show_conn_settings_form)
self.kirdy_handler = Kirdy(self, self.kirdy, 1.0) self.kirdy_handler = Kirdy(self, self.kirdy, 1/20.0)
self.kirdy_handler.setting_update_sig.connect(self.update_ld_ctrl_panel_settings) self.kirdy_handler.setting_update_sig.connect(self.update_ld_ctrl_panel_settings)
self.kirdy_handler.setting_update_sig.connect(self.update_thermostat_ctrl_panel_settings) self.kirdy_handler.setting_update_sig.connect(self.update_thermostat_ctrl_panel_settings)
@ -616,6 +673,10 @@ class MainWindow(QtWidgets.QMainWindow):
pwr_limit_unit = self.cfg_pd_mon_form.cfg_pwr_limit_spinbox.unit pwr_limit_unit = self.cfg_pd_mon_form.cfg_pwr_limit_spinbox.unit
self.cfg_pd_mon_form.cfg_pwr_limit_reading.setText(f"{siConvert(ld_settings['ld_pwr_limit']['value'], pwr_limit_unit):.4f}") self.cfg_pd_mon_form.cfg_pwr_limit_reading.setText(f"{siConvert(ld_settings['ld_pwr_limit']['value'], pwr_limit_unit):.4f}")
def update_adc_filter_form_readings(self, filter_type, filter_rate):
self.cfg_adc_filter_form.filter_type_reading_lbl.setText(filter_type)
self.cfg_adc_filter_form.filter_sampling_rate_reading_lbl.setText(str(filter_rate))
def show_conn_settings_form(self): def show_conn_settings_form(self):
ip_addr = self.ip_addr.split(".") ip_addr = self.ip_addr.split(".")
self.conn_settings_form.addr_in_0.setText(ip_addr[0]) self.conn_settings_form.addr_in_0.setText(ip_addr[0])
@ -656,6 +717,12 @@ class MainWindow(QtWidgets.QMainWindow):
self.kirdy.task_dispatcher(self.kirdy.thermostat.clear_alarm()) self.kirdy.task_dispatcher(self.kirdy.thermostat.clear_alarm())
self.tec_clear_alarm_btn.clicked.connect(tec_clear_alarm) self.tec_clear_alarm_btn.clicked.connect(tec_clear_alarm)
@pyqtSlot(bool)
def update_polling_rate(_):
self.kirdy_handler.set_update_s(1/self.polling_rate_spinbox.value())
self.kirdy_handler.update_polling_rate()
self.polling_rate_apply_btn.clicked.connect(update_polling_rate)
def _set_up_plot_menu(self): def _set_up_plot_menu(self):
self.plot_menu = QtWidgets.QMenu() self.plot_menu = QtWidgets.QMenu()
self.plot_menu.setTitle("Plot Settings") self.plot_menu.setTitle("Plot Settings")
@ -742,6 +809,13 @@ class MainWindow(QtWidgets.QMainWindow):
self.cfg_pd_mon_form.cfg_dark_current_lbl.setText(pd_dark_current_text.replace(":", f" ({pd_dark_current_unit}):")) self.cfg_pd_mon_form.cfg_dark_current_lbl.setText(pd_dark_current_text.replace(":", f" ({pd_dark_current_unit}):"))
self.cfg_pd_mon_form.cfg_dark_current_spinbox.unit = pd_dark_current_unit self.cfg_pd_mon_form.cfg_dark_current_spinbox.unit = pd_dark_current_unit
def _set_up_adc_filter_form(self):
@pyqtSlot(bool)
def apply_adc_filter_settings():
filter_cfg = self.cfg_adc_filter_form.get_filter_settings()
self.kirdy.task_dispatcher(self.kirdy.thermostat.config_temp_adc_filter(filter_cfg))
self.cfg_adc_filter_form.apply_btn.clicked.connect(apply_adc_filter_settings)
def _set_param_tree(self): def _set_param_tree(self):
status = self.ld_status status = self.ld_status
status.setHeaderHidden(True) status.setHeaderHidden(True)
@ -790,7 +864,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.params[3].child('PID Config', 'PID Auto Tune', 'Run').sigActivated.connect(autotune) self.params[3].child('PID Config', 'PID Auto Tune', 'Run').sigActivated.connect(autotune)
@pyqtSlot() @pyqtSlot()
def show_pd_mon_cfg_form(parm): def show_pd_mon_cfg_form(param):
ld_pwr_limit = self.params[1].child('Photodiode Monitor Config', 'LD Power Limit').value() ld_pwr_limit = self.params[1].child('Photodiode Monitor Config', 'LD Power Limit').value()
pd_responsitivity = self.params[1].child('Photodiode Monitor Config', 'Responsitivity').value() pd_responsitivity = self.params[1].child('Photodiode Monitor Config', 'Responsitivity').value()
pd_dark_current = self.params[1].child('Photodiode Monitor Config', 'Dark Current').value() pd_dark_current = self.params[1].child('Photodiode Monitor Config', 'Dark Current').value()
@ -802,6 +876,23 @@ class MainWindow(QtWidgets.QMainWindow):
self.cfg_pd_mon_form.show() self.cfg_pd_mon_form.show()
self.params[1].child('Photodiode Monitor Config', 'Configure Photodiode Monitor').sigActivated.connect(show_pd_mon_cfg_form) self.params[1].child('Photodiode Monitor Config', 'Configure Photodiode Monitor').sigActivated.connect(show_pd_mon_cfg_form)
@asyncSlot()
async def show_adc_filter_cfg_form(param):
settings = await self.kirdy.device.get_settings_summary()
filter_type = settings['thermostat']['temp_adc_settings']['filter_type']
filter_rate = settings['thermostat']['temp_adc_settings'][getattr(getattr(FilterConfig, filter_type), "_odr_type")]
self.cfg_adc_filter_form.filter_type_cbox.setCurrentIndex(self.cfg_adc_filter_form.filter_type_cbox.findText(filter_type))
self.cfg_adc_filter_form.sampling_rate_cbox_config(filter_type)
if filter_type == "Sinc3WithFineODR":
self.cfg_adc_filter_form.fine_filter_sampling_rate_spinbox.setValue(filter_rate)
else:
self.cfg_adc_filter_form.filter_sampling_rate_cbox.setCurrentIndex(self.cfg_adc_filter_form.filter_sampling_rate_cbox.findText(filter_rate))
self.cfg_adc_filter_form.show()
self.params[3].child('Temperature ADC Filter Settings', 'Configure ADC Filter').sigActivated.connect(show_adc_filter_cfg_form)
@pyqtSlot(str) @pyqtSlot(str)
def cmd_cannot_execute(self, kirdy_msg): def cmd_cannot_execute(self, kirdy_msg):
self.info_box.setText(kirdy_msg) self.info_box.setText(kirdy_msg)
@ -961,7 +1052,11 @@ class MainWindow(QtWidgets.QMainWindow):
self.params[3].child('Output Config', 'Limits', 'Max Heating Current').setValuewithLock(settings["tec_settings"]['max_i_neg']['value']) self.params[3].child('Output Config', 'Limits', 'Max Heating Current').setValuewithLock(settings["tec_settings"]['max_i_neg']['value'])
self.params[3].child('Output Config', 'Limits', 'Max Voltage Difference').setValuewithLock(settings["tec_settings"]['max_v']['value']) self.params[3].child('Output Config', 'Limits', 'Max Voltage Difference').setValuewithLock(settings["tec_settings"]['max_v']['value'])
self.params[3].child('Output Config', 'Default Power On').setValuewithLock(settings["default_pwr_on"]) self.params[3].child('Output Config', 'Default Power On').setValuewithLock(settings["default_pwr_on"])
# TODO: Update the Temperature ADC Settings here as well filter_type = settings['temp_adc_settings']['filter_type']
filter_rate = settings['temp_adc_settings'][getattr(getattr(FilterConfig, filter_type), "_odr_type")]
self.update_adc_filter_form_readings(filter_type, filter_rate)
self.params[3].child('Temperature ADC Filter Settings', 'Filter Type').setValue(filter_type)
self.params[3].child('Temperature ADC Filter Settings', 'Sampling Rate').setValue(settings['temp_adc_settings']['rate'])
self.params[3].child('Temperature Monitor Config', 'Upper Limit').setValuewithLock(settings["temp_mon_settings"]['upper_limit']) self.params[3].child('Temperature Monitor Config', 'Upper Limit').setValuewithLock(settings["temp_mon_settings"]['upper_limit'])
self.params[3].child('Temperature Monitor Config', 'Lower Limit').setValuewithLock(settings["temp_mon_settings"]['lower_limit']) self.params[3].child('Temperature Monitor Config', 'Lower Limit').setValuewithLock(settings["temp_mon_settings"]['lower_limit'])
self.params[3].child('PID Config', 'Kp').setValuewithLock(settings["pid_params"]['kp']) self.params[3].child('PID Config', 'Kp').setValuewithLock(settings["pid_params"]['kp'])
@ -996,6 +1091,9 @@ class MainWindow(QtWidgets.QMainWindow):
else: else:
self.params[3].child('Readings', 'Temperature').setValuewithLock(report["temperature"]) self.params[3].child('Readings', 'Temperature').setValuewithLock(report["temperature"])
self.params[3].child('Readings', 'Current through TEC').setValuewithLock(report["tec_i"]) self.params[3].child('Readings', 'Current through TEC').setValuewithLock(report["tec_i"])
rate = 1 / (report['interval']['ms'] / 1e3 + report['interval']['us'] / 1e6)
self.params[3].child('Temperature ADC Filter Settings', 'Recorded Sampling Rate').setValue(rate)
self.cfg_adc_filter_form.recorded_sampling_rate_reading_lbl.setText(f"{rate:.2f}")
except Exception as e: except Exception as e:
logging.error(f"Params tree cannot be updated. Data:{report}", exc_info=True) logging.error(f"Params tree cannot be updated. Data:{report}", exc_info=True)

View File

@ -45,6 +45,7 @@ class PIDAutotune:
raise ValueError('setpoint must be specified') raise ValueError('setpoint must be specified')
self._inputs = deque(maxlen=round(lookback / sampletime)) self._inputs = deque(maxlen=round(lookback / sampletime))
self._sampletime = sampletime
self._setpoint = setpoint self._setpoint = setpoint
self._outputstep = out_step self._outputstep = out_step
self._noiseband = noiseband self._noiseband = noiseband
@ -69,6 +70,7 @@ class PIDAutotune:
self._out_min = -step self._out_min = -step
self._noiseband = noiseband self._noiseband = noiseband
self._inputs = deque(maxlen=round(lookback / sampletime)) self._inputs = deque(maxlen=round(lookback / sampletime))
self._sampletime = sampletime
def setReady(self): def setReady(self):
self._state = PIDAutotuneState.STATE_READY self._state = PIDAutotuneState.STATE_READY
@ -96,8 +98,8 @@ class PIDAutotune:
def get_tec_pid (self): def get_tec_pid (self):
divisors = self._tuning_rules["tyreus-luyben"] divisors = self._tuning_rules["tyreus-luyben"]
kp = self._Ku * divisors[0] kp = self._Ku * divisors[0]
ki = divisors[1] * self._Ku / self._Pu ki = divisors[1] * self._Ku / self._Pu / (1 / self._sampletime)
kd = divisors[2] * self._Ku * self._Pu kd = divisors[2] * self._Ku * self._Pu * (1 / self._sampletime)
return kp, ki, kd return kp, ki, kd
def get_pid_parameters(self, tuning_rule='ziegler-nichols'): def get_pid_parameters(self, tuning_rule='ziegler-nichols'):
@ -109,8 +111,8 @@ class PIDAutotune:
""" """
divisors = self._tuning_rules[tuning_rule] divisors = self._tuning_rules[tuning_rule]
kp = self._Ku * divisors[0] kp = self._Ku * divisors[0]
ki = divisors[1] * self._Ku / self._Pu ki = divisors[1] * self._Ku / self._Pu / (1 / self._sampletime)
kd = divisors[2] * self._Ku * self._Pu kd = divisors[2] * self._Ku * self._Pu * (1 / self._sampletime)
return PIDAutotune.PIDParams(kp, ki, kd) return PIDAutotune.PIDParams(kp, ki, kd)
def run(self, input_val, time_input): def run(self, input_val, time_input):

View File

@ -0,0 +1,210 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Cfg_Adc_Filter_Form</class>
<widget class="QDialog" name="Cfg_Adc_Filter_Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>786</width>
<height>303</height>
</rect>
</property>
<property name="windowTitle">
<string>Config Temperature ADC Filter</string>
</property>
<widget class="QWidget" name="verticalLayoutWidget">
<property name="geometry">
<rect>
<x>20</x>
<y>20</y>
<width>731</width>
<height>251</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="3,4,4">
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>Value</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_5">
<property name="text">
<string>Readings</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="filter_type_layout" stretch="3,4,4">
<item>
<widget class="QLabel" name="filter_type_lbl">
<property name="text">
<string>Filter Type</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="filter_type_cbox">
<property name="editable">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="filter_type_reading_lbl">
<property name="text">
<string>Sinc5Sinc1With50hz60HzRejection</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="filter_sampling_rate_layout" stretch="3,4,4,4">
<item>
<widget class="QLabel" name="filter_sampling_rate_lbl">
<property name="text">
<string>Filter Sampling Rate</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="filter_sampling_rate_cbox"/>
</item>
<item>
<widget class="QDoubleSpinBox" name="fine_filter_sampling_rate_spinbox">
<property name="maximum">
<double>1000.000000000000000</double>
</property>
<property name="value">
<double>16.670000000000002</double>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="filter_sampling_rate_reading_lbl">
<property name="text">
<string>F16SPS</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="recorded_sampling_rate_layout" stretch="3,4,4">
<item>
<widget class="QLabel" name="recorded_sampling_rate_lbl">
<property name="text">
<string>Recorded Sampling Rate</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="recorded_sampling_rate_reading_lbl">
<property name="text">
<string>16.67</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="apply_btn_layout" stretch="3,2,3,2">
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="apply_btn">
<property name="text">
<string>Apply</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="close_btn">
<property name="text">
<string>Close</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
<resources/>
<connections>
<connection>
<sender>close_btn</sender>
<signal>clicked()</signal>
<receiver>Cfg_Adc_Filter_Form</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>677</x>
<y>246</y>
</hint>
<hint type="destinationlabel">
<x>392</x>
<y>151</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -415,6 +415,13 @@
</property> </property>
</spacer> </spacer>
</item> </item>
<item>
<widget class="QLabel" name="polling_rate_lbl">
<property name="text">
<string>Polling Rate (Hz): </string>
</property>
</widget>
</item>
<item> <item>
<widget class="QWidget" name="report_group" native="true"> <widget class="QWidget" name="report_group" native="true">
<property name="enabled"> <property name="enabled">
@ -448,9 +455,26 @@
<property name="bottomMargin"> <property name="bottomMargin">
<number>0</number> <number>0</number>
</property> </property>
<item>
<widget class="QDoubleSpinBox" name="polling_rate_spinbox">
<property name="maximum">
<double>1000.000000000000000</double>
</property>
<property name="value">
<double>20.000000000000000</double>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>
<item>
<widget class="QPushButton" name="polling_rate_apply_btn">
<property name="text">
<string>Apply</string>
</property>
</widget>
</item>
</layout> </layout>
</item> </item>
</layout> </layout>

View File

@ -0,0 +1,131 @@
# Form implementation generated from reading ui file 'config_adc_filter_form.ui'
#
# Created by: PyQt6 UI code generator 6.6.0
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt6 import QtCore, QtGui, QtWidgets
class Ui_Cfg_Adc_Filter_Form(object):
def setupUi(self, Cfg_Adc_Filter_Form):
Cfg_Adc_Filter_Form.setObjectName("Cfg_Adc_Filter_Form")
Cfg_Adc_Filter_Form.resize(786, 303)
self.verticalLayoutWidget = QtWidgets.QWidget(parent=Cfg_Adc_Filter_Form)
self.verticalLayoutWidget.setGeometry(QtCore.QRect(20, 20, 731, 251))
self.verticalLayoutWidget.setObjectName("verticalLayoutWidget")
self.verticalLayout = QtWidgets.QVBoxLayout(self.verticalLayoutWidget)
self.verticalLayout.setContentsMargins(0, 0, 0, 0)
self.verticalLayout.setObjectName("verticalLayout")
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.horizontalLayout.addItem(spacerItem)
self.label_4 = QtWidgets.QLabel(parent=self.verticalLayoutWidget)
self.label_4.setObjectName("label_4")
self.horizontalLayout.addWidget(self.label_4)
self.label_5 = QtWidgets.QLabel(parent=self.verticalLayoutWidget)
self.label_5.setObjectName("label_5")
self.horizontalLayout.addWidget(self.label_5)
self.horizontalLayout.setStretch(0, 3)
self.horizontalLayout.setStretch(1, 4)
self.horizontalLayout.setStretch(2, 4)
self.verticalLayout.addLayout(self.horizontalLayout)
self.filter_type_layout = QtWidgets.QHBoxLayout()
self.filter_type_layout.setObjectName("filter_type_layout")
self.filter_type_lbl = QtWidgets.QLabel(parent=self.verticalLayoutWidget)
self.filter_type_lbl.setObjectName("filter_type_lbl")
self.filter_type_layout.addWidget(self.filter_type_lbl)
self.filter_type_cbox = QtWidgets.QComboBox(parent=self.verticalLayoutWidget)
self.filter_type_cbox.setEditable(False)
self.filter_type_cbox.setObjectName("filter_type_cbox")
self.filter_type_layout.addWidget(self.filter_type_cbox)
self.filter_type_reading_lbl = QtWidgets.QLabel(parent=self.verticalLayoutWidget)
self.filter_type_reading_lbl.setObjectName("filter_type_reading_lbl")
self.filter_type_layout.addWidget(self.filter_type_reading_lbl)
self.filter_type_layout.setStretch(0, 3)
self.filter_type_layout.setStretch(1, 4)
self.filter_type_layout.setStretch(2, 4)
self.verticalLayout.addLayout(self.filter_type_layout)
self.filter_sampling_rate_layout = QtWidgets.QHBoxLayout()
self.filter_sampling_rate_layout.setObjectName("filter_sampling_rate_layout")
self.filter_sampling_rate_lbl = QtWidgets.QLabel(parent=self.verticalLayoutWidget)
self.filter_sampling_rate_lbl.setObjectName("filter_sampling_rate_lbl")
self.filter_sampling_rate_layout.addWidget(self.filter_sampling_rate_lbl)
self.filter_sampling_rate_cbox = QtWidgets.QComboBox(parent=self.verticalLayoutWidget)
self.filter_sampling_rate_cbox.setObjectName("filter_sampling_rate_cbox")
self.filter_sampling_rate_layout.addWidget(self.filter_sampling_rate_cbox)
self.fine_filter_sampling_rate_spinbox = QtWidgets.QDoubleSpinBox(parent=self.verticalLayoutWidget)
self.fine_filter_sampling_rate_spinbox.setMaximum(1000.0)
self.fine_filter_sampling_rate_spinbox.setProperty("value", 16.67)
self.fine_filter_sampling_rate_spinbox.setObjectName("fine_filter_sampling_rate_spinbox")
self.filter_sampling_rate_layout.addWidget(self.fine_filter_sampling_rate_spinbox)
self.filter_sampling_rate_reading_lbl = QtWidgets.QLabel(parent=self.verticalLayoutWidget)
self.filter_sampling_rate_reading_lbl.setObjectName("filter_sampling_rate_reading_lbl")
self.filter_sampling_rate_layout.addWidget(self.filter_sampling_rate_reading_lbl)
self.filter_sampling_rate_layout.setStretch(0, 3)
self.filter_sampling_rate_layout.setStretch(1, 4)
self.filter_sampling_rate_layout.setStretch(2, 4)
self.filter_sampling_rate_layout.setStretch(3, 4)
self.verticalLayout.addLayout(self.filter_sampling_rate_layout)
self.recorded_sampling_rate_layout = QtWidgets.QHBoxLayout()
self.recorded_sampling_rate_layout.setObjectName("recorded_sampling_rate_layout")
self.recorded_sampling_rate_lbl = QtWidgets.QLabel(parent=self.verticalLayoutWidget)
self.recorded_sampling_rate_lbl.setObjectName("recorded_sampling_rate_lbl")
self.recorded_sampling_rate_layout.addWidget(self.recorded_sampling_rate_lbl)
spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.recorded_sampling_rate_layout.addItem(spacerItem1)
self.recorded_sampling_rate_reading_lbl = QtWidgets.QLabel(parent=self.verticalLayoutWidget)
self.recorded_sampling_rate_reading_lbl.setObjectName("recorded_sampling_rate_reading_lbl")
self.recorded_sampling_rate_layout.addWidget(self.recorded_sampling_rate_reading_lbl)
self.recorded_sampling_rate_layout.setStretch(0, 3)
self.recorded_sampling_rate_layout.setStretch(1, 4)
self.recorded_sampling_rate_layout.setStretch(2, 4)
self.verticalLayout.addLayout(self.recorded_sampling_rate_layout)
self.apply_btn_layout = QtWidgets.QHBoxLayout()
self.apply_btn_layout.setObjectName("apply_btn_layout")
spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.apply_btn_layout.addItem(spacerItem2)
self.apply_btn = QtWidgets.QPushButton(parent=self.verticalLayoutWidget)
self.apply_btn.setObjectName("apply_btn")
self.apply_btn_layout.addWidget(self.apply_btn)
spacerItem3 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.apply_btn_layout.addItem(spacerItem3)
self.close_btn = QtWidgets.QPushButton(parent=self.verticalLayoutWidget)
self.close_btn.setObjectName("close_btn")
self.apply_btn_layout.addWidget(self.close_btn)
self.apply_btn_layout.setStretch(0, 3)
self.apply_btn_layout.setStretch(1, 2)
self.apply_btn_layout.setStretch(2, 3)
self.apply_btn_layout.setStretch(3, 2)
self.verticalLayout.addLayout(self.apply_btn_layout)
self.retranslateUi(Cfg_Adc_Filter_Form)
self.close_btn.clicked.connect(Cfg_Adc_Filter_Form.accept) # type: ignore
QtCore.QMetaObject.connectSlotsByName(Cfg_Adc_Filter_Form)
def retranslateUi(self, Cfg_Adc_Filter_Form):
_translate = QtCore.QCoreApplication.translate
Cfg_Adc_Filter_Form.setWindowTitle(_translate("Cfg_Adc_Filter_Form", "Config Temperature ADC Filter"))
self.label_4.setText(_translate("Cfg_Adc_Filter_Form", "Value"))
self.label_5.setText(_translate("Cfg_Adc_Filter_Form", "Readings"))
self.filter_type_lbl.setText(_translate("Cfg_Adc_Filter_Form", "Filter Type"))
self.filter_type_reading_lbl.setText(_translate("Cfg_Adc_Filter_Form", "Sinc5Sinc1With50hz60HzRejection"))
self.filter_sampling_rate_lbl.setText(_translate("Cfg_Adc_Filter_Form", "Filter Sampling Rate"))
self.filter_sampling_rate_reading_lbl.setText(_translate("Cfg_Adc_Filter_Form", "F16SPS"))
self.recorded_sampling_rate_lbl.setText(_translate("Cfg_Adc_Filter_Form", "Recorded Sampling Rate"))
self.recorded_sampling_rate_reading_lbl.setText(_translate("Cfg_Adc_Filter_Form", "16.67"))
self.apply_btn.setText(_translate("Cfg_Adc_Filter_Form", "Apply"))
self.close_btn.setText(_translate("Cfg_Adc_Filter_Form", "Close"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
Cfg_Adc_Filter_Form = QtWidgets.QDialog()
ui = Ui_Cfg_Adc_Filter_Form()
ui.setupUi(Cfg_Adc_Filter_Form)
Cfg_Adc_Filter_Form.show()
sys.exit(app.exec())

View File

@ -142,16 +142,16 @@ impl PidState {
self.controller.parameters = pid_params; self.controller.parameters = pid_params;
} }
pub fn set_pid_params(&mut self, param: PidSettings, val: f32) { pub fn set_pid_params(&mut self, param: PidSettings, val: f32, curr_rate: f32) {
match param { match param {
PidSettings::Kp => { PidSettings::Kp => {
self.controller.parameters.kp = val; self.controller.parameters.kp = val;
} }
PidSettings::Ki => { PidSettings::Ki => {
self.controller.parameters.ki = val; self.controller.parameters.ki = val * curr_rate;
} }
PidSettings::Kd => { PidSettings::Kd => {
self.controller.parameters.kd = val; self.controller.parameters.kd = val / curr_rate;
} }
PidSettings::Min => { PidSettings::Min => {
self.controller.parameters.output_min = val; self.controller.parameters.output_min = val;
@ -162,6 +162,18 @@ impl PidState {
} }
} }
pub fn update_pid_params_with_new_sampling_rate(&mut self, old_rate: f32, new_rate: f32) {
self.controller.parameters.ki = self.controller.parameters.ki * old_rate / new_rate;
self.controller.parameters.kd = self.controller.parameters.kd * new_rate / old_rate;
}
pub fn get_abs_pid_params(&mut self, curr_rate: f32) -> Parameters {
let mut pid_params = self.controller.parameters.clone();
pid_params.ki = self.controller.parameters.ki / curr_rate;
pid_params.kd = self.controller.parameters.kd * curr_rate;
pid_params
}
pub fn reset_pid_state(&mut self) { pub fn reset_pid_state(&mut self) {
self.controller.u1 = 0.0; self.controller.u1 = 0.0;
self.controller.x1 = 0.0; self.controller.x1 = 0.0;

View File

@ -57,14 +57,10 @@ impl TecSettings {
units: PhantomData, units: PhantomData,
value: 1.65, value: 1.65,
}; };
// Kirdy Design Specs:
// MaxV = 5.0V
// MAX Current = +- 1.0A
const MAX_I_SET: ElectricCurrent = ElectricCurrent { const MAX_I_SET: ElectricCurrent = ElectricCurrent {
dimension: PhantomData, dimension: PhantomData,
units: PhantomData, units: PhantomData,
value: 1.0, value: 3.0,
}; };
const MAX_V_DUTY_TO_VOLTAGE_RATE: ElectricPotential = ElectricPotential { const MAX_V_DUTY_TO_VOLTAGE_RATE: ElectricPotential = ElectricPotential {
dimension: PhantomData, dimension: PhantomData,
@ -74,7 +70,7 @@ impl TecSettings {
pub const MAX_V_MAX: ElectricPotential = ElectricPotential { pub const MAX_V_MAX: ElectricPotential = ElectricPotential {
dimension: PhantomData, dimension: PhantomData,
units: PhantomData, units: PhantomData,
value: 5.0, value: 4.3,
}; };
const MAX_V_DUTY_MAX: f64 = const MAX_V_DUTY_MAX: f64 =
TecSettings::MAX_V_MAX.value as f64 / TecSettings::MAX_V_DUTY_TO_VOLTAGE_RATE.value as f64; TecSettings::MAX_V_MAX.value as f64 / TecSettings::MAX_V_DUTY_TO_VOLTAGE_RATE.value as f64;
@ -86,12 +82,12 @@ impl TecSettings {
pub const MAX_I_POS_CURRENT: ElectricCurrent = ElectricCurrent { pub const MAX_I_POS_CURRENT: ElectricCurrent = ElectricCurrent {
dimension: PhantomData, dimension: PhantomData,
units: PhantomData, units: PhantomData,
value: 1.0, value: 3.0,
}; };
pub const MAX_I_NEG_CURRENT: ElectricCurrent = ElectricCurrent { pub const MAX_I_NEG_CURRENT: ElectricCurrent = ElectricCurrent {
dimension: PhantomData, dimension: PhantomData,
units: PhantomData, units: PhantomData,
value: 1.0, value: 3.0,
}; };
// .get::<ratio>() is not implemented for const // .get::<ratio>() is not implemented for const
const MAX_I_POS_DUTY_MAX: f64 = const MAX_I_POS_DUTY_MAX: f64 =
@ -457,7 +453,8 @@ impl Thermostat {
} }
pub fn set_pid(&mut self, param: PidSettings, val: f32) { pub fn set_pid(&mut self, param: PidSettings, val: f32) {
self.pid_ctrl_ch0.set_pid_params(param, val); let curr_rate = self.ad7172.get_filter_type_and_rate(0).unwrap().3;
self.pid_ctrl_ch0.set_pid_params(param, val, curr_rate);
} }
pub fn set_sh_beta(&mut self, beta: f32) { pub fn set_sh_beta(&mut self, beta: f32) {
@ -496,20 +493,35 @@ impl Thermostat {
} }
pub fn set_temp_adc_sinc5_sinc1_filter(&mut self, index: u8, odr: ad7172::SingleChODR) { pub fn set_temp_adc_sinc5_sinc1_filter(&mut self, index: u8, odr: ad7172::SingleChODR) {
let old_rate = self.ad7172.get_filter_type_and_rate(0).unwrap().3;
let new_rate = odr.output_rate().unwrap();
self.pid_ctrl_ch0
.update_pid_params_with_new_sampling_rate(old_rate, new_rate);
self.ad7172.set_sinc5_sinc1_filter(index, odr).unwrap(); self.ad7172.set_sinc5_sinc1_filter(index, odr).unwrap();
} }
pub fn set_temp_adc_sinc3_filter(&mut self, index: u8, odr: ad7172::SingleChODR) { pub fn set_temp_adc_sinc3_filter(&mut self, index: u8, odr: ad7172::SingleChODR) {
let old_rate = self.ad7172.get_filter_type_and_rate(0).unwrap().3;
let new_rate = odr.output_rate().unwrap();
self.pid_ctrl_ch0
.update_pid_params_with_new_sampling_rate(old_rate, new_rate);
self.ad7172.set_sinc3_filter(index, odr).unwrap(); self.ad7172.set_sinc3_filter(index, odr).unwrap();
} }
pub fn set_temp_adc_sinc5_sinc1_with_postfilter(&mut self, index: u8, odr: ad7172::PostFilter) { pub fn set_temp_adc_sinc5_sinc1_with_postfilter(&mut self, index: u8, odr: ad7172::PostFilter) {
let old_rate = self.ad7172.get_filter_type_and_rate(0).unwrap().3;
let new_rate = odr.output_rate().unwrap();
self.pid_ctrl_ch0
.update_pid_params_with_new_sampling_rate(old_rate, new_rate);
self.ad7172 self.ad7172
.set_sinc5_sinc1_with_50hz_60hz_rejection(index, odr) .set_sinc5_sinc1_with_50hz_60hz_rejection(index, odr)
.unwrap(); .unwrap();
} }
pub fn set_temp_adc_sinc3_fine_filter(&mut self, index: u8, rate: f32) { pub fn set_temp_adc_sinc3_fine_filter(&mut self, index: u8, rate: f32) {
let old_rate = self.ad7172.get_filter_type_and_rate(0).unwrap().3;
self.pid_ctrl_ch0
.update_pid_params_with_new_sampling_rate(old_rate, rate);
self.ad7172.set_sinc3_fine_filter(index, rate).unwrap(); self.ad7172.set_sinc3_fine_filter(index, rate).unwrap();
} }
@ -552,7 +564,7 @@ impl Thermostat {
pid_engaged: self.get_pid_engaged(), pid_engaged: self.get_pid_engaged(),
temperature_setpoint: self.pid_ctrl_ch0.get_pid_setpoint(), temperature_setpoint: self.pid_ctrl_ch0.get_pid_setpoint(),
tec_settings: self.get_tec_settings(), tec_settings: self.get_tec_settings(),
pid_params: self.get_pid_settings(), pid_params: self.pid_ctrl_ch0.get_abs_pid_params(temp_adc_settings.rate.unwrap()),
temp_adc_settings: temp_adc_settings, temp_adc_settings: temp_adc_settings,
temp_mon_settings: self.get_temp_mon_settings(), temp_mon_settings: self.get_temp_mon_settings(),
thermistor_params: self.get_steinhart_hart(), thermistor_params: self.get_steinhart_hart(),
@ -584,10 +596,12 @@ impl Thermostat {
self.set_pid_engaged(settings.pid_engaged); self.set_pid_engaged(settings.pid_engaged);
self.pid_ctrl_ch0.apply_pid_params(settings.pid_params); self.pid_ctrl_ch0.apply_pid_params(settings.pid_params);
self.pid_ctrl_ch0.update_pid_params_with_new_sampling_rate(settings.temp_adc_settings.rate.unwrap(), 1.0);
self.set_temperature_setpoint(settings.temperature_setpoint); self.set_temperature_setpoint(settings.temperature_setpoint);
if !settings.pid_engaged { if !settings.pid_engaged {
self.set_i(settings.tec_settings.i_set.value); self.set_i(settings.tec_settings.i_set.value);
} }
self.clear_temp_mon_alarm();
self.set_default_pwr_on(settings.default_pwr_on); self.set_default_pwr_on(settings.default_pwr_on);
if settings.default_pwr_on { if settings.default_pwr_on {
self.power_up(); self.power_up();