Compare commits

..

12 Commits

Author SHA1 Message Date
953a48230c README: Introduce Thermostat GUI
Co-authored-by: topquark12 <aw@m-labs.hk>
2024-11-04 18:43:51 +08:00
0dbf154f82 pytec GUI: Set up packaging
Co-authored-by: Egor Savkin <es@m-labs.hk>
2024-11-04 18:43:49 +08:00
287186d030 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:43:27 +08:00
ef34d782e0 pytec GUI: Implement PlotSettingsMenu
Co-authored-by: linuswck <linuswck@m-labs.hk>
2024-11-04 18:43:27 +08:00
3d519826dd pytec GUI: Implement plotting
Co-authored-by: linuswck <linuswck@m-labs.hk>
2024-11-04 18:43:27 +08:00
50a69fbfeb 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:43:27 +08:00
3a45fdaaa6 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:43:27 +08:00
ebb240d7f3 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:43:27 +08:00
8daa40f62b 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:43:23 +08:00
8d2b7e69bb pytec: Create asyncio clients 2024-11-04 18:42:59 +08:00
52e35d2a98 flake: Format with nixfmt-rfc-style
Also set up formatter so that `nix fmt` formats.
2024-11-04 18:38:08 +08:00
f1da910c11 pytec: Complete client
Expose all available Thermostat commands to the pytec client, and add
disconnect functionality.
2024-11-04 18:03:50 +08:00
7 changed files with 464 additions and 377 deletions

View File

@ -7,9 +7,17 @@
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
outputs = { self, nixpkgs, rust-overlay }: outputs =
{
self,
nixpkgs,
rust-overlay,
}:
let 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 { rust = pkgs.rust-bin.stable."1.66.0".default.override {
extensions = [ "rust-src" ]; extensions = [ "rust-src" ];
@ -86,7 +94,8 @@
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;
@ -103,17 +112,26 @@
devShells.x86_64-linux.default = pkgs.mkShellNoCC { devShells.x86_64-linux.default = pkgs.mkShellNoCC {
name = "thermostat-dev-shell"; name = "thermostat-dev-shell";
packages = with pkgs; [ packages =
rust llvm with pkgs;
openocd dfu-util rlwrap [
rust
llvm
openocd
dfu-util
rlwrap
qtcreator qtcreator
] ++ (with python3Packages; [ ]
numpy matplotlib ++ (with python3Packages; [
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,6 +12,10 @@ 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:
@ -110,6 +114,39 @@ 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
@ -163,10 +200,38 @@ 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): def save_config(self, channel=""):
"""Save current configuration to EEPROM""" """Save current configuration to EEPROM"""
self._command("save") self._command("save", channel)
if channel != "":
self._read_line() # read the extra {}
def load_config(self): def load_config(self, channel=""):
"""Load current configuration from EEPROM""" """Load current configuration from EEPROM"""
self._command("load") 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)

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("pwm", ch, "i_set", 0) await self._thermostat.set_param("output", 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(
"pwm", ch, "i_set", self.autotuners[ch].output() "output", 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("pwm", ch, "pid") await self._thermostat.set_param("output", 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("pwm", ch, "i_set", 0) await self._thermostat.set_param("output", 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)
pwm = Property(list) output = Property(list)
postfilter = Property(list) postfilter = Property(list)
report = Property(list) report = Property(list)
@ -82,8 +82,14 @@ 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 = ( (
await asyncio.gather( self.fan,
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(),
@ -91,7 +97,6 @@ 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.pwm_update.connect(self.update_pwm) self.thermostat.output_update.connect(self.update_output)
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_pwm(self, pwm_data): def update_output(self, output_data):
for pwm_params in pwm_data: for output_params in output_data:
channel = pwm_params["channel"] channel = output_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(pwm_params["max_v"]) ).setValue(output_params["max_v"])
self.params[channel].child( self.params[channel].child(
"Output Config", "Limits", "Max Cooling Current" "Output Config", "Limits", "Max Cooling Current"
).setValue(pwm_params["max_i_pos"] * 1000) ).setValue(output_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(pwm_params["max_i_neg"] * 1000) ).setValue(output_params["max_i_neg"] * 1000)
@pyqtSlot(list) @pyqtSlot(list)
def update_postfilter(self, postfilter_data): def update_postfilter(self, postfilter_data):
@ -304,4 +304,3 @@ 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":"pwm", "topic": "output",
"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":"pwm", "topic": "output",
"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":"pwm", "topic": "output",
"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":"pwm", "topic": "output",
"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":"pwm", "topic": "output",
"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.pwm_update.connect(self.set_limits_warning) self._thermostat.output_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, pwm_data: list): def set_limits_warning(self, output_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 pwm_params in pwm_data: for output_params in output_data:
channel = pwm_params["channel"] channel = output_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 pwm_params[limit] == 0.0: if output_params[limit] == 0.0:
channels_zeroed_limits[channel].add(limit) channels_zeroed_limits[channel].add(limit)
channel_disabled = [False, False] channel_disabled = [False, False]