Compare commits

..

10 Commits

Author SHA1 Message Date
cc90a88276 README: Introduce Thermostat GUI
Co-authored-by: topquark12 <aw@m-labs.hk>
2024-11-04 18:27:30 +08:00
4618543916 pytec GUI: Set up packaging
Co-authored-by: Egor Savkin <es@m-labs.hk>
2024-11-04 18:27:26 +08:00
20cd69fff7 pytec GUI: Implement Control Panel
Co-authored-by: linuswck <linuswck@m-labs.hk>
Co-authored-by: Egor Savkin <es@m-labs.hk>
2024-11-04 18:27:22 +08:00
e045b0288a pytec GUI: Implement PlotSettingsMenu
Co-authored-by: linuswck <linuswck@m-labs.hk>
2024-11-04 18:27:18 +08:00
ae244b66d1 pytec GUI: Implement plotting
Co-authored-by: linuswck <linuswck@m-labs.hk>
2024-11-04 18:27:14 +08:00
c77e6f73f1 pytec GUI: Incorporate autotuning
Co-authored-by: topquark12 <aw@m-labs.hk>
Co-authored-by: linuswck <linuswck@m-labs.hk>
Co-authored-by: Egor Savkin <es@m-labs.hk>
2024-11-04 18:27:10 +08:00
0de3c9452e pytec GUI: Implement ThermostatSettingsMenu
Co-authored-by: linuswck <linuswck@m-labs.hk>
Co-authored-by: Egor Savkin <es@m-labs.hk>
2024-11-04 18:27:06 +08:00
c95db11b62 pytec GUI: Implement status line
Co-authored-by: linuswck <linuswck@m-labs.hk>
Co-authored-by: Egor Savkin <es@m-labs.hk>
2024-11-04 18:26:54 +08:00
239b8d8791 pytec: Create GUI to Thermostat
- Add connection menu

- Add basic GUI layout skeleton

Co-authored-by: linuswck <linuswck@m-labs.hk>
Co-authored-by: Egor Savkin <es@m-labs.hk>
2024-11-04 18:26:01 +08:00
1d1500cc0f pytec: Create asyncio clients 2024-11-04 18:16:03 +08:00
7 changed files with 377 additions and 464 deletions

View File

@ -7,17 +7,9 @@
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
outputs = outputs = { self, nixpkgs, rust-overlay }:
{
self,
nixpkgs,
rust-overlay,
}:
let let
pkgs = import nixpkgs { pkgs = import nixpkgs { system = "x86_64-linux"; overlays = [ (import rust-overlay) ]; };
system = "x86_64-linux";
overlays = [ (import rust-overlay) ];
};
rust = pkgs.rust-bin.stable."1.66.0".default.override { rust = pkgs.rust-bin.stable."1.66.0".default.override {
extensions = [ "rust-src" ]; extensions = [ "rust-src" ];
@ -94,8 +86,7 @@
wrapQtApp "$out/bin/tec_qt" wrapQtApp "$out/bin/tec_qt"
''; '';
}; };
in in {
{
packages.x86_64-linux = { packages.x86_64-linux = {
inherit thermostat thermostat_gui; inherit thermostat thermostat_gui;
default = thermostat; default = thermostat;
@ -112,26 +103,17 @@
devShells.x86_64-linux.default = pkgs.mkShellNoCC { devShells.x86_64-linux.default = pkgs.mkShellNoCC {
name = "thermostat-dev-shell"; name = "thermostat-dev-shell";
packages = packages = with pkgs; [
with pkgs; rust llvm
[ openocd dfu-util rlwrap
rust
llvm
openocd
dfu-util
rlwrap
qtcreator qtcreator
] ] ++ (with python3Packages; [
++ (with python3Packages; [ numpy matplotlib
numpy
matplotlib
pyqtgraph pyqtgraph
pyqt6 pyqt6
qasync qasync
pglive pglive
]); ]);
}; };
formatter.x86_64-linux = nixpkgs.legacyPackages.x86_64-linux.nixfmt-rfc-style;
}; };
} }

View File

@ -12,10 +12,6 @@ class Client:
self._lines = [""] self._lines = [""]
self._check_zero_limits() self._check_zero_limits()
def disconnect(self):
self._socket.shutdown(socket.SHUT_RDWR)
self._socket.close()
def _check_zero_limits(self): def _check_zero_limits(self):
output_report = self.get_output() output_report = self.get_output()
for output_channel in output_report: for output_channel in output_report:
@ -114,39 +110,6 @@ class Client:
""" """
return self._get_conf("postfilter") return self._get_conf("postfilter")
def get_report(self):
"""Obtain one-time report on measurement values
Example of yielded data::
{'channel': 0,
'time': 2302524,
'interval': 0.12
'adc': 0.6199188965423515,
'sens': 6138.519310282602,
'temperature': 36.87032392655527,
'pid_engaged': True,
'i_set': 2.0635816680889123,
'dac_value': 2.527790834044456,
'dac_feedback': 2.523,
'i_tec': 2.331,
'tec_i': 2.0925,
'tec_u_meas': 2.5340000000000003,
'pid_output': 2.067581958092247}
"""
return self._get_conf("report")
def get_ipv4(self):
"""Get the IPv4 settings of the Thermostat"""
return self._command("ipv4")
def get_fan(self):
"""Get Thermostat current fan settings"""
return self._command("fan")
def get_hwrev(self):
"""Get Thermostat hardware revision"""
return self._command("hwrev")
def report_mode(self): def report_mode(self):
"""Start reporting measurement values """Start reporting measurement values
@ -200,38 +163,10 @@ class Client:
self.set_param("pid", channel, "target", value=target) self.set_param("pid", channel, "target", value=target)
self.set_param("output", channel, "pid") self.set_param("output", channel, "pid")
def save_config(self, channel=""): def save_config(self):
"""Save current configuration to EEPROM""" """Save current configuration to EEPROM"""
self._command("save", channel) self._command("save")
if channel != "":
self._read_line() # read the extra {}
def load_config(self, channel=""): def load_config(self):
"""Load current configuration from EEPROM""" """Load current configuration from EEPROM"""
self._command("load", channel) self._command("load")
if channel != "":
self._read_line() # read the extra {}
def reset(self):
"""Reset the device"""
self._socket.sendall("reset".encode("utf-8"))
self.disconnect() # resetting ends the TCP session, disconnect anyway
def enter_dfu_mode(self):
"""Reset device and enters USB device firmware update (DFU) mode"""
self._socket.sendall("dfu".encode("utf-8"))
self.disconnect() # resetting ends the TCP session, disconnect anyway
def set_ipv4(self, address, netmask, gateway=""):
"""Configure IPv4 address, netmask length, and optional default gateway"""
self._command("ipv4", f"{address}/{netmask}", gateway)
def set_fan(self, power=None):
"""Set fan power with values from 1 to 100. If omitted, set according to fcurve"""
if power is None:
power = "auto"
self._command("fan", power)
def set_fcurve(self, a=1.0, b=0.0, c=0.0):
"""Set fan controller curve coefficients"""
self._command("fcurve", a, b, c)

View File

@ -40,7 +40,7 @@ class PIDAutoTuner(QObject):
self.autotuners[ch].setOff() self.autotuners[ch].setOff()
self.autotune_state_changed.emit(ch, self.autotuners[ch].state()) self.autotune_state_changed.emit(ch, self.autotuners[ch].state())
if self._thermostat.connected(): if self._thermostat.connected():
await self._thermostat.set_param("output", ch, "i_set", 0) await self._thermostat.set_param("pwm", ch, "i_set", 0)
@asyncSlot(list) @asyncSlot(list)
async def tick(self, report): async def tick(self, report):
@ -63,7 +63,7 @@ class PIDAutoTuner(QObject):
channel_report["temperature"], channel_report["time"] channel_report["temperature"], channel_report["time"]
) )
await self._thermostat.set_param( await self._thermostat.set_param(
"output", ch, "i_set", self.autotuners[ch].output() "pwm", ch, "i_set", self.autotuners[ch].output()
) )
case PIDAutotuneState.STATE_SUCCEEDED: case PIDAutotuneState.STATE_SUCCEEDED:
kp, ki, kd = self.autotuners[ch].get_tec_pid() kp, ki, kd = self.autotuners[ch].get_tec_pid()
@ -73,7 +73,7 @@ class PIDAutoTuner(QObject):
await self._thermostat.set_param("pid", ch, "kp", kp) await self._thermostat.set_param("pid", ch, "kp", kp)
await self._thermostat.set_param("pid", ch, "ki", ki) await self._thermostat.set_param("pid", ch, "ki", ki)
await self._thermostat.set_param("pid", ch, "kd", kd) await self._thermostat.set_param("pid", ch, "kd", kd)
await self._thermostat.set_param("output", ch, "pid") await self._thermostat.set_param("pwm", ch, "pid")
await self._thermostat.set_param( await self._thermostat.set_param(
"pid", ch, "target", self.target_temp[ch] "pid", ch, "target", self.target_temp[ch]
@ -81,4 +81,4 @@ class PIDAutoTuner(QObject):
case PIDAutotuneState.STATE_FAILED: case PIDAutotuneState.STATE_FAILED:
self.autotuners[ch].setOff() self.autotuners[ch].setOff()
self.autotune_state_changed.emit(ch, self.autotuners[ch].state()) self.autotune_state_changed.emit(ch, self.autotuners[ch].state())
await self._thermostat.set_param("output", ch, "i_set", 0) await self._thermostat.set_param("pwm", ch, "i_set", 0)

View File

@ -19,7 +19,7 @@ class Thermostat(QObject, metaclass=PropertyMeta):
fan = Property(dict) fan = Property(dict)
thermistor = Property(list) thermistor = Property(list)
pid = Property(list) pid = Property(list)
output = Property(list) pwm = Property(list)
postfilter = Property(list) postfilter = Property(list)
report = Property(list) report = Property(list)
@ -82,14 +82,8 @@ class Thermostat(QObject, metaclass=PropertyMeta):
await asyncio.sleep(self._update_s) await asyncio.sleep(self._update_s)
async def update_params(self): async def update_params(self):
( self.fan, self.pwm, self.report, self.pid, self.thermistor, self.postfilter = (
self.fan, await asyncio.gather(
self.output,
self.report,
self.pid,
self.thermistor,
self.postfilter,
) = await asyncio.gather(
self._client.get_fan(), self._client.get_fan(),
self._client.get_output(), self._client.get_output(),
self._client.report(), self._client.report(),
@ -97,6 +91,7 @@ class Thermostat(QObject, metaclass=PropertyMeta):
self._client.get_b_parameter(), self._client.get_b_parameter(),
self._client.get_postfilter(), self._client.get_postfilter(),
) )
)
def connected(self): def connected(self):
return self._client.connected() return self._client.connected()

View File

@ -97,7 +97,7 @@ class CtrlPanel(QObject):
self.thermostat.pid_update.connect(self.update_pid) self.thermostat.pid_update.connect(self.update_pid)
self.thermostat.report_update.connect(self.update_report) self.thermostat.report_update.connect(self.update_report)
self.thermostat.thermistor_update.connect(self.update_thermistor) self.thermostat.thermistor_update.connect(self.update_thermistor)
self.thermostat.output_update.connect(self.update_output) self.thermostat.pwm_update.connect(self.update_pwm)
self.thermostat.postfilter_update.connect(self.update_postfilter) self.thermostat.postfilter_update.connect(self.update_postfilter)
self.autotuners.autotune_state_changed.connect(self.update_pid_autotune) self.autotuners.autotune_state_changed.connect(self.update_pid_autotune)
@ -225,19 +225,19 @@ class CtrlPanel(QObject):
) )
@pyqtSlot(list) @pyqtSlot(list)
def update_output(self, output_data): def update_pwm(self, pwm_data):
for output_params in output_data: for pwm_params in pwm_data:
channel = output_params["channel"] channel = pwm_params["channel"]
with QSignalBlocker(self.params[channel]): with QSignalBlocker(self.params[channel]):
self.params[channel].child( self.params[channel].child(
"Output Config", "Limits", "Max Voltage Difference" "Output Config", "Limits", "Max Voltage Difference"
).setValue(output_params["max_v"]) ).setValue(pwm_params["max_v"])
self.params[channel].child( self.params[channel].child(
"Output Config", "Limits", "Max Cooling Current" "Output Config", "Limits", "Max Cooling Current"
).setValue(output_params["max_i_pos"] * 1000) ).setValue(pwm_params["max_i_pos"] * 1000)
self.params[channel].child( self.params[channel].child(
"Output Config", "Limits", "Max Heating Current" "Output Config", "Limits", "Max Heating Current"
).setValue(output_params["max_i_neg"] * 1000) ).setValue(pwm_params["max_i_neg"] * 1000)
@pyqtSlot(list) @pyqtSlot(list)
def update_postfilter(self, postfilter_data): def update_postfilter(self, postfilter_data):
@ -304,3 +304,4 @@ class CtrlPanel(QObject):
| PIDAutotuneState.STATE_RELAY_STEP_DOWN | PIDAutotuneState.STATE_RELAY_STEP_DOWN
): ):
await self.autotuners.stop_pid_from_running(ch) await self.autotuners.stop_pid_from_running(ch)

View File

@ -27,7 +27,7 @@
], ],
"value": "Constant Current", "value": "Constant Current",
"thermostat:set_param":{ "thermostat:set_param":{
"topic": "output", "topic":"pwm",
"field":"pid" "field":"pid"
}, },
"children":[ "children":[
@ -44,7 +44,7 @@
"decimals":6, "decimals":6,
"suffix":"mA", "suffix":"mA",
"thermostat:set_param":{ "thermostat:set_param":{
"topic": "output", "topic":"pwm",
"field":"i_set" "field":"i_set"
}, },
"lock":false "lock":false
@ -84,7 +84,7 @@
], ],
"suffix":"mA", "suffix":"mA",
"thermostat:set_param":{ "thermostat:set_param":{
"topic": "output", "topic":"pwm",
"field":"max_i_pos" "field":"max_i_pos"
}, },
"lock":false "lock":false
@ -101,7 +101,7 @@
], ],
"suffix":"mA", "suffix":"mA",
"thermostat:set_param":{ "thermostat:set_param":{
"topic": "output", "topic":"pwm",
"field":"max_i_neg" "field":"max_i_neg"
}, },
"lock":false "lock":false
@ -118,7 +118,7 @@
"siPrefix":true, "siPrefix":true,
"suffix":"V", "suffix":"V",
"thermostat:set_param":{ "thermostat:set_param":{
"topic": "output", "topic":"pwm",
"field":"max_v" "field":"max_v"
}, },
"lock":false "lock":false

View File

@ -6,18 +6,18 @@ class ZeroLimitsWarningView(QObject):
def __init__(self, thermostat, style, limit_warning): def __init__(self, thermostat, style, limit_warning):
super().__init__() super().__init__()
self._thermostat = thermostat self._thermostat = thermostat
self._thermostat.output_update.connect(self.set_limits_warning) self._thermostat.pwm_update.connect(self.set_limits_warning)
self._lbl = limit_warning self._lbl = limit_warning
self._style = style self._style = style
@pyqtSlot(list) @pyqtSlot(list)
def set_limits_warning(self, output_data: list): def set_limits_warning(self, pwm_data: list):
channels_zeroed_limits = [set() for i in range(self._thermostat.NUM_CHANNELS)] channels_zeroed_limits = [set() for i in range(self._thermostat.NUM_CHANNELS)]
for output_params in output_data: for pwm_params in pwm_data:
channel = output_params["channel"] channel = pwm_params["channel"]
for limit in "max_i_pos", "max_i_neg", "max_v": for limit in "max_i_pos", "max_i_neg", "max_v":
if output_params[limit] == 0.0: if pwm_params[limit] == 0.0:
channels_zeroed_limits[channel].add(limit) channels_zeroed_limits[channel].add(limit)
channel_disabled = [False, False] channel_disabled = [False, False]