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

View File

@ -12,10 +12,6 @@ class Client:
self._lines = [""]
self._check_zero_limits()
def disconnect(self):
self._socket.shutdown(socket.SHUT_RDWR)
self._socket.close()
def _check_zero_limits(self):
output_report = self.get_output()
for output_channel in output_report:
@ -114,39 +110,6 @@ class Client:
"""
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):
"""Start reporting measurement values
@ -200,38 +163,10 @@ class Client:
self.set_param("pid", channel, "target", value=target)
self.set_param("output", channel, "pid")
def save_config(self, channel=""):
def save_config(self):
"""Save current configuration to EEPROM"""
self._command("save", channel)
if channel != "":
self._read_line() # read the extra {}
self._command("save")
def load_config(self, channel=""):
def load_config(self):
"""Load current configuration from EEPROM"""
self._command("load", channel)
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)
self._command("load")

View File

@ -40,7 +40,7 @@ class PIDAutoTuner(QObject):
self.autotuners[ch].setOff()
self.autotune_state_changed.emit(ch, self.autotuners[ch].state())
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)
async def tick(self, report):
@ -63,7 +63,7 @@ class PIDAutoTuner(QObject):
channel_report["temperature"], channel_report["time"]
)
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:
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, "ki", ki)
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(
"pid", ch, "target", self.target_temp[ch]
@ -81,4 +81,4 @@ class PIDAutoTuner(QObject):
case PIDAutotuneState.STATE_FAILED:
self.autotuners[ch].setOff()
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)
thermistor = Property(list)
pid = Property(list)
output = Property(list)
pwm = Property(list)
postfilter = Property(list)
report = Property(list)
@ -82,14 +82,8 @@ class Thermostat(QObject, metaclass=PropertyMeta):
await asyncio.sleep(self._update_s)
async def update_params(self):
(
self.fan,
self.output,
self.report,
self.pid,
self.thermistor,
self.postfilter,
) = await asyncio.gather(
self.fan, self.pwm, self.report, self.pid, self.thermistor, self.postfilter = (
await asyncio.gather(
self._client.get_fan(),
self._client.get_output(),
self._client.report(),
@ -97,6 +91,7 @@ class Thermostat(QObject, metaclass=PropertyMeta):
self._client.get_b_parameter(),
self._client.get_postfilter(),
)
)
def connected(self):
return self._client.connected()

View File

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

View File

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

View File

@ -6,18 +6,18 @@ class ZeroLimitsWarningView(QObject):
def __init__(self, thermostat, style, limit_warning):
super().__init__()
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._style = style
@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)]
for output_params in output_data:
channel = output_params["channel"]
for pwm_params in pwm_data:
channel = pwm_params["channel"]
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)
channel_disabled = [False, False]