forked from M-Labs/thermostat
Compare commits
12 Commits
cc90a88276
...
953a48230c
Author | SHA1 | Date | |
---|---|---|---|
953a48230c | |||
0dbf154f82 | |||
287186d030 | |||
ef34d782e0 | |||
3d519826dd | |||
50a69fbfeb | |||
3a45fdaaa6 | |||
ebb240d7f3 | |||
8daa40f62b | |||
8d2b7e69bb | |||
52e35d2a98 | |||
f1da910c11 |
34
flake.nix
34
flake.nix
@ -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;
|
||||||
};
|
};
|
||||||
}
|
}
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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]
|
||||||
|
Loading…
Reference in New Issue
Block a user