forked from M-Labs/thermostat
Compare commits
93 Commits
d7bacdecd7
...
dc5460f591
Author | SHA1 | Date | |
---|---|---|---|
dc5460f591 | |||
df79d4d977 | |||
9910f935a9 | |||
e3ac7debc4 | |||
a8d7986c82 | |||
5a09c026fa | |||
8ab04ac3cd | |||
5ecbb262be | |||
f141705b0e | |||
19470b3d02 | |||
5862a2f7d5 | |||
ac34472d31 | |||
f45061a652 | |||
d9a3fcdc4b | |||
22fc7cbf22 | |||
0133d2e41b | |||
43758e12a3 | |||
5dcf9e8f31 | |||
40c0519237 | |||
260a466078 | |||
8a13ce2b47 | |||
ba369c880e | |||
375e159c39 | |||
4240312bf4 | |||
20f168e04c | |||
efe0c086d8 | |||
9f3591770a | |||
17157fd890 | |||
42268e2186 | |||
db15c0052e | |||
bfddfd8e20 | |||
77b66e15cc | |||
ae51fc739e | |||
7279c4d64a | |||
71f40027f5 | |||
388c914c18 | |||
659ed384ae | |||
b252dc6a44 | |||
0dbed18ba1 | |||
2b9a4c168e | |||
83405103f2 | |||
71f4ad6e34 | |||
4d21770542 | |||
52ee422a70 | |||
5475bf7951 | |||
da70430c35 | |||
5e8f61be9e | |||
b6f936a65f | |||
79cc11dd14 | |||
7a5bb8d308 | |||
7245e514e8 | |||
fbaeb870c6 | |||
f922ea906f | |||
aef3a9870b | |||
752d6f8eab | |||
7a5ec14b95 | |||
b2f188b556 | |||
a2afd81dcd | |||
89319c0cd9 | |||
f75de51447 | |||
6f0956b35c | |||
6067c41ca4 | |||
8a01249d60 | |||
0ec18dfbff | |||
6f7b46bc2f | |||
2fc0d265e4 | |||
b768d61e39 | |||
d244ba392a | |||
93d6df5e92 | |||
44bea87f03 | |||
e6f62e9e19 | |||
271fe449ba | |||
70db0a39eb | |||
26c7382b1e | |||
c415d9de8a | |||
7069111e21 | |||
1707728c3c | |||
a16d2e9a9e | |||
bc4ac43e0b | |||
9acff86547 | |||
8753f4a0fc | |||
9a83d6850d | |||
772863f4b2 | |||
44ef2c04e3 | |||
623011fabb | |||
400f3a98e8 | |||
7dd5d15047 | |||
297e589c30 | |||
65e1f4a146 | |||
e0ce14c616 | |||
a79679a074 | |||
b9acba69d3 | |||
b4b4ec987c |
46
flake.lock
generated
46
flake.lock
generated
@ -1,12 +1,28 @@
|
|||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
|
"mozilla-overlay": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1704373101,
|
||||||
|
"narHash": "sha256-+gi59LRWRQmwROrmE1E2b3mtocwueCQqZ60CwLG+gbg=",
|
||||||
|
"owner": "mozilla",
|
||||||
|
"repo": "nixpkgs-mozilla",
|
||||||
|
"rev": "9b11a87c0cc54e308fa83aac5b4ee1816d5418a2",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "mozilla",
|
||||||
|
"repo": "nixpkgs-mozilla",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1691421349,
|
"lastModified": 1704290814,
|
||||||
"narHash": "sha256-RRJyX0CUrs4uW4gMhd/X4rcDG8PTgaaCQM5rXEJOx6g=",
|
"narHash": "sha256-LWvKHp7kGxk/GEtlrGYV68qIvPHkU9iToomNFGagixU=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "011567f35433879aae5024fc6ec53f2a0568a6c4",
|
"rev": "70bdadeb94ffc8806c0570eb5c2695ad29f0e421",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@ -18,28 +34,8 @@
|
|||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": "nixpkgs",
|
"mozilla-overlay": "mozilla-overlay",
|
||||||
"rust-overlay": "rust-overlay"
|
"nixpkgs": "nixpkgs"
|
||||||
}
|
|
||||||
},
|
|
||||||
"rust-overlay": {
|
|
||||||
"inputs": {
|
|
||||||
"nixpkgs": [
|
|
||||||
"nixpkgs"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1719281921,
|
|
||||||
"narHash": "sha256-LIBMfhM9pMOlEvBI757GOK5l0R58SRi6YpwfYMbf4yc=",
|
|
||||||
"owner": "oxalica",
|
|
||||||
"repo": "rust-overlay",
|
|
||||||
"rev": "b6032d3a404d8a52ecfc8571ff0c26dfbe221d07",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "oxalica",
|
|
||||||
"repo": "rust-overlay",
|
|
||||||
"type": "github"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
92
flake.nix
92
flake.nix
@ -2,32 +2,36 @@
|
|||||||
description = "Firmware for the Sinara 8451 Thermostat";
|
description = "Firmware for the Sinara 8451 Thermostat";
|
||||||
|
|
||||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.05";
|
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.05";
|
||||||
inputs.rust-overlay = {
|
inputs.mozilla-overlay = {
|
||||||
url = "github:oxalica/rust-overlay";
|
url = "github:mozilla/nixpkgs-mozilla";
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
flake = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs =
|
outputs = { self, nixpkgs, mozilla-overlay, }:
|
||||||
{
|
|
||||||
self,
|
|
||||||
nixpkgs,
|
|
||||||
rust-overlay,
|
|
||||||
}:
|
|
||||||
let
|
let
|
||||||
pkgs = import nixpkgs {
|
pkgs = import nixpkgs {
|
||||||
system = "x86_64-linux";
|
system = "x86_64-linux";
|
||||||
overlays = [ (import rust-overlay) ];
|
overlays = [ (import mozilla-overlay) ];
|
||||||
|
};
|
||||||
|
rustManifest = pkgs.fetchurl {
|
||||||
|
url =
|
||||||
|
"https://static.rust-lang.org/dist/2022-12-15/channel-rust-stable.toml";
|
||||||
|
hash = "sha256-S7epLlflwt0d1GZP44u5Xosgf6dRrmr8xxC+Ml2Pq7c=";
|
||||||
};
|
};
|
||||||
|
|
||||||
rust = pkgs.rust-bin.stable."1.66.0".default.override {
|
|
||||||
extensions = [ "rust-src" ];
|
|
||||||
targets = [ "thumbv7em-none-eabihf" ];
|
targets = [ "thumbv7em-none-eabihf" ];
|
||||||
|
rustChannelOfTargets = _channel: _date: targets:
|
||||||
|
(pkgs.lib.rustLib.fromManifestFile rustManifest {
|
||||||
|
inherit (pkgs) stdenv lib fetchurl patchelf;
|
||||||
|
}).rust.override {
|
||||||
|
inherit targets;
|
||||||
|
extensions = [ "rust-src" ];
|
||||||
};
|
};
|
||||||
rustPlatform = pkgs.makeRustPlatform {
|
rust = rustChannelOfTargets "stable" null targets;
|
||||||
|
rustPlatform = pkgs.recurseIntoAttrs (pkgs.makeRustPlatform {
|
||||||
rustc = rust;
|
rustc = rust;
|
||||||
cargo = rust;
|
cargo = rust;
|
||||||
};
|
});
|
||||||
|
|
||||||
thermostat = rustPlatform.buildRustPackage {
|
thermostat = rustPlatform.buildRustPackage {
|
||||||
name = "thermostat";
|
name = "thermostat";
|
||||||
version = "0.0.0";
|
version = "0.0.0";
|
||||||
@ -36,7 +40,8 @@
|
|||||||
cargoLock = {
|
cargoLock = {
|
||||||
lockFile = ./Cargo.lock;
|
lockFile = ./Cargo.lock;
|
||||||
outputHashes = {
|
outputHashes = {
|
||||||
"stm32-eth-0.2.0" = "sha256-48RpZgagUqgVeKm7GXdk3Oo0v19ScF9Uby0nTFlve2o=";
|
"stm32-eth-0.2.0" =
|
||||||
|
"sha256-48RpZgagUqgVeKm7GXdk3Oo0v19ScF9Uby0nTFlve2o=";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -77,10 +82,7 @@
|
|||||||
inherit pname version;
|
inherit pname version;
|
||||||
hash = "sha256-WBCNhBHHBU4IQdi3ke6F4QH8KWubNZwOAd3jipj/Ks4=";
|
hash = "sha256-WBCNhBHHBU4IQdi3ke6F4QH8KWubNZwOAd3jipj/Ks4=";
|
||||||
};
|
};
|
||||||
propagatedBuildInputs = with pkgs.python3Packages; [
|
propagatedBuildInputs = with pkgs.python3Packages; [ numpy pyqt6 ];
|
||||||
numpy
|
|
||||||
pyqt6
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
qtextras = pkgs.python3Packages.buildPythonPackage rec {
|
qtextras = pkgs.python3Packages.buildPythonPackage rec {
|
||||||
@ -108,12 +110,22 @@
|
|||||||
hash = "sha256-jqj8X6H1N5mJQ4OrY5ANqRB0YJByqg/bNneEALWmH1A=";
|
hash = "sha256-jqj8X6H1N5mJQ4OrY5ANqRB0YJByqg/bNneEALWmH1A=";
|
||||||
};
|
};
|
||||||
buildInputs = [ pkgs.python3Packages.poetry-core ];
|
buildInputs = [ pkgs.python3Packages.poetry-core ];
|
||||||
propagatedBuildInputs = [
|
propagatedBuildInputs = [ pyqtgraph pkgs.python3Packages.numpy ];
|
||||||
pyqtgraph
|
|
||||||
pkgs.python3Packages.numpy
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pytec-dev-wrappers = pkgs.runCommandNoCC "pytec-dev-wrappers" { } ''
|
||||||
|
mkdir -p $out/bin
|
||||||
|
for program in ${self}/pytec/*.py; do
|
||||||
|
if [ -x $program ]; then
|
||||||
|
progname=`basename -s .py $program`
|
||||||
|
outname=$out/bin/$progname
|
||||||
|
echo "#!${pkgs.bash}/bin/bash" >> $outname
|
||||||
|
echo "exec python3 -m pytec.$progname \"\$@\"" >> $outname
|
||||||
|
chmod 755 $outname
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
'';
|
||||||
|
|
||||||
thermostat_gui = pkgs.python3Packages.buildPythonPackage {
|
thermostat_gui = pkgs.python3Packages.buildPythonPackage {
|
||||||
pname = "thermostat_gui";
|
pname = "thermostat_gui";
|
||||||
version = "0.0.0";
|
version = "0.0.0";
|
||||||
@ -121,8 +133,7 @@
|
|||||||
src = "${self}/pytec";
|
src = "${self}/pytec";
|
||||||
|
|
||||||
nativeBuildInputs = [ pkgs.qt6.wrapQtAppsHook ];
|
nativeBuildInputs = [ pkgs.qt6.wrapQtAppsHook ];
|
||||||
propagatedBuildInputs =
|
propagatedBuildInputs = [ pkgs.qt6.qtbase ]
|
||||||
[ pkgs.qt6.qtbase ]
|
|
||||||
++ (with pkgs.python3Packages; [
|
++ (with pkgs.python3Packages; [
|
||||||
pyqtgraph
|
pyqtgraph
|
||||||
pyqt6
|
pyqt6
|
||||||
@ -136,31 +147,22 @@
|
|||||||
wrapQtApp "$out/bin/tec_qt"
|
wrapQtApp "$out/bin/tec_qt"
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
in
|
in {
|
||||||
{
|
packages.x86_64-linux = { inherit thermostat thermostat_gui; };
|
||||||
packages.x86_64-linux = {
|
|
||||||
inherit thermostat thermostat_gui;
|
formatter.x86_64-linux = nixpkgs.legacyPackages.x86_64-linux.nixfmt;
|
||||||
default = thermostat;
|
|
||||||
};
|
|
||||||
|
|
||||||
apps.x86_64-linux.thermostat_gui = {
|
apps.x86_64-linux.thermostat_gui = {
|
||||||
type = "app";
|
type = "app";
|
||||||
program = "${self.packages.x86_64-linux.thermostat_gui}/bin/tec_qt";
|
program = "${self.packages.x86_64-linux.thermostat_gui}/bin/tec_qt";
|
||||||
};
|
};
|
||||||
|
|
||||||
hydraJobs = {
|
hydraJobs = { inherit thermostat; };
|
||||||
inherit thermostat;
|
|
||||||
};
|
|
||||||
|
|
||||||
devShells.x86_64-linux.default = pkgs.mkShellNoCC {
|
devShell.x86_64-linux = pkgs.mkShell {
|
||||||
name = "thermostat-dev-shell";
|
name = "thermostat-dev-shell";
|
||||||
packages =
|
buildInputs = with pkgs;
|
||||||
with pkgs;
|
[ rust openocd dfu-util pytec-dev-wrappers ]
|
||||||
[
|
|
||||||
rust
|
|
||||||
openocd
|
|
||||||
dfu-util
|
|
||||||
]
|
|
||||||
++ (with python3Packages; [
|
++ (with python3Packages; [
|
||||||
numpy
|
numpy
|
||||||
matplotlib
|
matplotlib
|
||||||
@ -171,6 +173,10 @@
|
|||||||
pglive
|
pglive
|
||||||
qtextras
|
qtextras
|
||||||
]);
|
]);
|
||||||
|
shellHook = ''
|
||||||
|
export PYTHONPATH=`pwd`/pytec:$PYTHONPATH
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
|
defaultPackage.x86_64-linux = thermostat;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
0
pytec/autotune.py
Normal file → Executable file
0
pytec/autotune.py
Normal file → Executable file
@ -4,7 +4,7 @@ from pytec.aioclient import AsyncioClient
|
|||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
tec = AsyncioClient()
|
tec = AsyncioClient()
|
||||||
await tec.start_session() # (host="192.168.1.26", port=23)
|
await tec.connect() # (host="192.168.1.26", port=23)
|
||||||
await tec.set_param("s-h", 1, "t0", 20)
|
await tec.set_param("s-h", 1, "t0", 20)
|
||||||
print(await tec.get_pwm())
|
print(await tec.get_pwm())
|
||||||
print(await tec.get_pid())
|
print(await tec.get_pid())
|
||||||
|
0
pytec/plot.py
Normal file → Executable file
0
pytec/plot.py
Normal file → Executable file
@ -14,7 +14,7 @@ license = {text = "GPLv3"}
|
|||||||
tec_qt = "tec_qt:main"
|
tec_qt = "tec_qt:main"
|
||||||
|
|
||||||
[tool.setuptools]
|
[tool.setuptools]
|
||||||
packages.find = {} # Use setuptools custom discovery, package directory structure isn't standard
|
packages.find = {}
|
||||||
py-modules = ["autotune", "plot", "tec_qt"]
|
py-modules = ["autotune", "plot", "tec_qt"]
|
||||||
|
|
||||||
[tool.pylint.format]
|
[tool.pylint.format]
|
||||||
|
@ -7,56 +7,29 @@ class CommandError(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class StoppedConnecting(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class AsyncioClient:
|
class AsyncioClient:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._reader = None
|
self._reader = None
|
||||||
self._writer = None
|
self._writer = None
|
||||||
self._connecting_task = None
|
|
||||||
self._command_lock = asyncio.Lock()
|
self._command_lock = asyncio.Lock()
|
||||||
self._report_mode_on = False
|
self._report_mode_on = False
|
||||||
self.timeout = None
|
|
||||||
|
|
||||||
async def start_session(self, host="192.168.1.26", port=23, timeout=None):
|
async def connect(self, host="192.168.1.26", port=23):
|
||||||
"""Start session to Thermostat at specified host and port.
|
"""Connect to Thermostat at specified host and port.
|
||||||
Throws StoppedConnecting if disconnect was called while connecting.
|
|
||||||
Throws asyncio.TimeoutError if timeout was exceeded.
|
|
||||||
|
|
||||||
Example::
|
Example::
|
||||||
client = AsyncioClient()
|
client = AsyncioClient()
|
||||||
try:
|
await client.connect()
|
||||||
await client.start_session()
|
|
||||||
except StoppedConnecting:
|
|
||||||
print("Stopped connecting")
|
|
||||||
"""
|
"""
|
||||||
self._connecting_task = asyncio.create_task(
|
self._reader, self._writer = await asyncio.open_connection(host, port)
|
||||||
asyncio.wait_for(asyncio.open_connection(host, port), timeout)
|
|
||||||
)
|
|
||||||
self.timeout = timeout
|
|
||||||
try:
|
|
||||||
self._reader, self._writer = await self._connecting_task
|
|
||||||
except asyncio.CancelledError as exc:
|
|
||||||
raise StoppedConnecting from exc
|
|
||||||
finally:
|
|
||||||
self._connecting_task = None
|
|
||||||
|
|
||||||
await self._check_zero_limits()
|
await self._check_zero_limits()
|
||||||
|
|
||||||
def connecting(self):
|
|
||||||
"""Returns True if client is connecting"""
|
|
||||||
return self._connecting_task is not None
|
|
||||||
|
|
||||||
def connected(self):
|
def connected(self):
|
||||||
"""Returns True if client is connected"""
|
"""Returns True if client is connected"""
|
||||||
return self._writer is not None
|
return self._writer is not None
|
||||||
|
|
||||||
async def end_session(self):
|
async def disconnect(self):
|
||||||
"""End session to Thermostat if connected, cancel connection if connecting"""
|
"""Disconnect from the Thermostat"""
|
||||||
if self._connecting_task is not None:
|
|
||||||
self._connecting_task.cancel()
|
|
||||||
|
|
||||||
if self._writer is None:
|
if self._writer is None:
|
||||||
return
|
return
|
||||||
@ -80,9 +53,7 @@ class AsyncioClient:
|
|||||||
|
|
||||||
async def _read_line(self):
|
async def _read_line(self):
|
||||||
# read 1 line
|
# read 1 line
|
||||||
chunk = await asyncio.wait_for(
|
chunk = await self._reader.readline()
|
||||||
self._reader.readline(), self.timeout
|
|
||||||
) # Only wait for response until timeout
|
|
||||||
return chunk.decode("utf-8", errors="ignore")
|
return chunk.decode("utf-8", errors="ignore")
|
||||||
|
|
||||||
async def _read_write(self, command):
|
async def _read_write(self, command):
|
||||||
@ -96,7 +67,7 @@ class AsyncioClient:
|
|||||||
line = await self._read_write(command)
|
line = await self._read_write(command)
|
||||||
|
|
||||||
response = json.loads(line)
|
response = json.loads(line)
|
||||||
logging.debug(f"{command}: {response}")
|
logging.debug("%s: %s", command, response)
|
||||||
if "error" in response:
|
if "error" in response:
|
||||||
raise CommandError(response["error"])
|
raise CommandError(response["error"])
|
||||||
return response
|
return response
|
||||||
@ -247,6 +218,8 @@ class AsyncioClient:
|
|||||||
async def save_config(self, channel=""):
|
async def save_config(self, channel=""):
|
||||||
"""Save current configuration to EEPROM"""
|
"""Save current configuration to EEPROM"""
|
||||||
await self._command("save", str(channel))
|
await self._command("save", str(channel))
|
||||||
|
if channel == "":
|
||||||
|
await self._read_line() # Read the extra {}
|
||||||
|
|
||||||
async def load_config(self, channel=""):
|
async def load_config(self, channel=""):
|
||||||
"""Load current configuration from EEPROM"""
|
"""Load current configuration from EEPROM"""
|
||||||
@ -267,7 +240,7 @@ class AsyncioClient:
|
|||||||
self._writer.write("reset\n".encode("utf-8"))
|
self._writer.write("reset\n".encode("utf-8"))
|
||||||
await self._writer.drain()
|
await self._writer.drain()
|
||||||
|
|
||||||
await self.end_session()
|
await self.disconnect()
|
||||||
|
|
||||||
async def dfu(self):
|
async def dfu(self):
|
||||||
"""Put the Thermostat in DFU update mode
|
"""Put the Thermostat in DFU update mode
|
||||||
@ -280,7 +253,7 @@ class AsyncioClient:
|
|||||||
self._writer.write("dfu\n".encode("utf-8"))
|
self._writer.write("dfu\n".encode("utf-8"))
|
||||||
await self._writer.drain()
|
await self._writer.drain()
|
||||||
|
|
||||||
await self.end_session()
|
await self.disconnect()
|
||||||
|
|
||||||
async def ipv4(self):
|
async def ipv4(self):
|
||||||
"""Get the IPv4 settings of the Thermostat"""
|
"""Get the IPv4 settings of the Thermostat"""
|
||||||
|
@ -1,13 +1,18 @@
|
|||||||
from PyQt6.QtCore import QObject, pyqtSlot
|
from PyQt6.QtCore import QObject, pyqtSlot, pyqtSignal
|
||||||
from qasync import asyncSlot
|
from qasync import asyncSlot
|
||||||
from autotune import PIDAutotuneState, PIDAutotune
|
from autotune import PIDAutotuneState, PIDAutotune
|
||||||
|
|
||||||
|
|
||||||
class PIDAutoTuner(QObject):
|
class PIDAutoTuner(QObject):
|
||||||
|
autotune_state_changed = pyqtSignal(int, PIDAutotuneState)
|
||||||
|
|
||||||
def __init__(self, parent, thermostat, num_of_channel):
|
def __init__(self, parent, thermostat, num_of_channel):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
self._thermostat = thermostat
|
self._thermostat = thermostat
|
||||||
|
self._thermostat.report_update.connect(self.tick)
|
||||||
|
self._thermostat.interval_update.connect(self.update_sampling_interval)
|
||||||
|
|
||||||
self.autotuners = [PIDAutotune(25) for _ in range(num_of_channel)]
|
self.autotuners = [PIDAutotune(25) for _ in range(num_of_channel)]
|
||||||
self.target_temp = [20.0 for _ in range(num_of_channel)]
|
self.target_temp = [20.0 for _ in range(num_of_channel)]
|
||||||
self.test_current = [1.0 for _ in range(num_of_channel)]
|
self.test_current = [1.0 for _ in range(num_of_channel)]
|
||||||
@ -34,9 +39,11 @@ class PIDAutoTuner(QObject):
|
|||||||
self.lookback[ch],
|
self.lookback[ch],
|
||||||
)
|
)
|
||||||
self.autotuners[ch].setReady()
|
self.autotuners[ch].setReady()
|
||||||
|
self.autotune_state_changed.emit(ch, self.autotuners[ch].state())
|
||||||
|
|
||||||
async def stop_pid_from_running(self, ch):
|
async def stop_pid_from_running(self, ch):
|
||||||
self.autotuners[ch].setOff()
|
self.autotuners[ch].setOff()
|
||||||
|
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("pwm", ch, "i_set", 0)
|
||||||
|
|
||||||
@asyncSlot(list)
|
@asyncSlot(list)
|
||||||
@ -62,6 +69,7 @@ class PIDAutoTuner(QObject):
|
|||||||
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()
|
||||||
self.autotuners[ch].setOff()
|
self.autotuners[ch].setOff()
|
||||||
|
self.autotune_state_changed.emit(ch, self.autotuners[ch].state())
|
||||||
|
|
||||||
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)
|
||||||
@ -73,4 +81,5 @@ 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())
|
||||||
await self._thermostat.set_param("pwm", ch, "i_set", 0)
|
await self._thermostat.set_param("pwm", ch, "i_set", 0)
|
||||||
|
@ -3,9 +3,16 @@ from qasync import asyncSlot
|
|||||||
from pytec.gui.model.property import Property, PropertyMeta
|
from pytec.gui.model.property import Property, PropertyMeta
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
from enum import Enum
|
||||||
from pytec.aioclient import AsyncioClient
|
from pytec.aioclient import AsyncioClient
|
||||||
|
|
||||||
|
|
||||||
|
class ThermostatConnectionState(Enum):
|
||||||
|
DISCONNECTED = "disconnected"
|
||||||
|
CONNECTING = "connecting"
|
||||||
|
CONNECTED = "connected"
|
||||||
|
|
||||||
|
|
||||||
class Thermostat(QObject, metaclass=PropertyMeta):
|
class Thermostat(QObject, metaclass=PropertyMeta):
|
||||||
hw_rev = Property(dict)
|
hw_rev = Property(dict)
|
||||||
fan = Property(dict)
|
fan = Property(dict)
|
||||||
@ -17,6 +24,7 @@ class Thermostat(QObject, metaclass=PropertyMeta):
|
|||||||
report = Property(list)
|
report = Property(list)
|
||||||
info_box_trigger = pyqtSignal(str, str)
|
info_box_trigger = pyqtSignal(str, str)
|
||||||
connection_error = pyqtSignal()
|
connection_error = pyqtSignal()
|
||||||
|
connection_state_changed = pyqtSignal(ThermostatConnectionState)
|
||||||
|
|
||||||
def __init__(self, parent, update_s):
|
def __init__(self, parent, update_s):
|
||||||
self._update_s = update_s
|
self._update_s = update_s
|
||||||
@ -24,51 +32,63 @@ class Thermostat(QObject, metaclass=PropertyMeta):
|
|||||||
self._watch_task = None
|
self._watch_task = None
|
||||||
self._report_mode_task = None
|
self._report_mode_task = None
|
||||||
self._poll_for_report = True
|
self._poll_for_report = True
|
||||||
|
self._update_params_task = None
|
||||||
|
self.connection_errored = False
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
async def start_session(self, host, port):
|
async def start_session(self, host, port):
|
||||||
await self._client.start_session(host, port, timeout=5)
|
self.connection_state_changed.emit(ThermostatConnectionState.CONNECTING)
|
||||||
|
await self._client.connect(host, port)
|
||||||
|
await self.get_hw_rev()
|
||||||
|
self.connection_state_changed.emit(ThermostatConnectionState.CONNECTED)
|
||||||
|
self.start_watching()
|
||||||
|
|
||||||
async def run(self):
|
async def run(self):
|
||||||
self.task = asyncio.create_task(self.update_params())
|
self._update_params_task = asyncio.create_task(self.update_params())
|
||||||
while True:
|
while True:
|
||||||
if self.task.done():
|
if self._update_params_task.done():
|
||||||
if self.task.exception() is not None:
|
|
||||||
try:
|
try:
|
||||||
raise self.task.exception()
|
self._update_params_task.result()
|
||||||
except asyncio.TimeoutError:
|
except OSError:
|
||||||
logging.error(
|
logging.error(
|
||||||
"Encountered an error while updating parameter tree.",
|
"Encountered an error while polling for information from Thermostat.",
|
||||||
exc_info=True,
|
exc_info=True,
|
||||||
)
|
)
|
||||||
self.connection_error.emit()
|
self.connection_error.emit()
|
||||||
return
|
return
|
||||||
_ = self.task.result()
|
self._update_params_task = asyncio.create_task(self.update_params())
|
||||||
self.task = asyncio.create_task(self.update_params())
|
|
||||||
await asyncio.sleep(self._update_s)
|
await asyncio.sleep(self._update_s)
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def timed_out(self):
|
||||||
|
self.connection_errored = True
|
||||||
|
|
||||||
async def get_hw_rev(self):
|
async def get_hw_rev(self):
|
||||||
self.hw_rev = await self._client.hw_rev()
|
self.hw_rev = await self._client.hw_rev()
|
||||||
return self.hw_rev
|
return self.hw_rev
|
||||||
|
|
||||||
async def update_params(self):
|
async def update_params(self):
|
||||||
self.fan = await self._client.get_fan()
|
fan_task = asyncio.create_task(self._client.get_fan())
|
||||||
self.pwm = await self._client.get_pwm()
|
pwm_task = asyncio.create_task(self._client.get_pwm())
|
||||||
|
pid_task = asyncio.create_task(self._client.get_pid())
|
||||||
|
report_task = asyncio.create_task(self._client.report())
|
||||||
|
thermistor_task = asyncio.create_task(self._client.get_steinhart_hart())
|
||||||
|
postfilter_task = asyncio.create_task(self._client.get_postfilter())
|
||||||
|
|
||||||
|
self.fan = await fan_task
|
||||||
|
self.pwm = await pwm_task
|
||||||
if self._poll_for_report:
|
if self._poll_for_report:
|
||||||
self.report = await self._client.report()
|
self.report = await report_task
|
||||||
self.interval = [
|
self.interval = [
|
||||||
self.report[i]["interval"] for i in range(len(self.report))
|
self.report[i]["interval"] for i in range(len(self.report))
|
||||||
]
|
]
|
||||||
self.pid = await self._client.get_pid()
|
self.pid = await pid_task
|
||||||
self.thermistor = await self._client.get_steinhart_hart()
|
self.thermistor = await thermistor_task
|
||||||
self.postfilter = await self._client.get_postfilter()
|
self.postfilter = await postfilter_task
|
||||||
|
|
||||||
def connected(self):
|
def connected(self):
|
||||||
return self._client.connected()
|
return self._client.connected()
|
||||||
|
|
||||||
def connecting(self):
|
|
||||||
return self._client.connecting()
|
|
||||||
|
|
||||||
def start_watching(self):
|
def start_watching(self):
|
||||||
self._watch_task = asyncio.create_task(self.run())
|
self._watch_task = asyncio.create_task(self.run())
|
||||||
|
|
||||||
@ -78,8 +98,8 @@ class Thermostat(QObject, metaclass=PropertyMeta):
|
|||||||
await self.set_report_mode(False)
|
await self.set_report_mode(False)
|
||||||
self._watch_task.cancel()
|
self._watch_task.cancel()
|
||||||
self._watch_task = None
|
self._watch_task = None
|
||||||
self.task.cancel()
|
self._update_params_task.cancel()
|
||||||
self.task = None
|
self._update_params_task = None
|
||||||
|
|
||||||
async def set_report_mode(self, enabled: bool):
|
async def set_report_mode(self, enabled: bool):
|
||||||
self._poll_for_report = not enabled
|
self._poll_for_report = not enabled
|
||||||
@ -95,8 +115,13 @@ class Thermostat(QObject, metaclass=PropertyMeta):
|
|||||||
self.report[i]["interval"] for i in range(len(self.report))
|
self.report[i]["interval"] for i in range(len(self.report))
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@asyncSlot()
|
||||||
async def end_session(self):
|
async def end_session(self):
|
||||||
await self._client.end_session()
|
await self.set_report_mode(False)
|
||||||
|
self.stop_watching()
|
||||||
|
await self._client.disconnect()
|
||||||
|
self.connection_state_changed.emit(ThermostatConnectionState.DISCONNECTED)
|
||||||
|
self.connection_errored = False
|
||||||
|
|
||||||
async def set_ipv4(self, ipv4):
|
async def set_ipv4(self, ipv4):
|
||||||
await self._client.set_param("ipv4", ipv4)
|
await self._client.set_param("ipv4", ipv4)
|
||||||
@ -105,18 +130,12 @@ class Thermostat(QObject, metaclass=PropertyMeta):
|
|||||||
return await self._client.ipv4()
|
return await self._client.ipv4()
|
||||||
|
|
||||||
@asyncSlot()
|
@asyncSlot()
|
||||||
async def save_cfg(self, ch):
|
async def save_cfg(self, ch=""):
|
||||||
await self._client.save_config(ch)
|
await self._client.save_config(ch)
|
||||||
self.info_box_trigger.emit(
|
|
||||||
"Settings loaded", f"Channel {ch} Settings has been saved to flash."
|
|
||||||
)
|
|
||||||
|
|
||||||
@asyncSlot()
|
@asyncSlot()
|
||||||
async def load_cfg(self, ch):
|
async def load_cfg(self, ch=""):
|
||||||
await self._client.load_config(ch)
|
await self._client.load_config(ch)
|
||||||
self.info_box_trigger.emit(
|
|
||||||
"Settings loaded", f"Channel {ch} Settings has been loaded from flash."
|
|
||||||
)
|
|
||||||
|
|
||||||
async def dfu(self):
|
async def dfu(self):
|
||||||
await self._client.dfu()
|
await self._client.dfu()
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
|
from functools import partial
|
||||||
from PyQt6.QtCore import pyqtSignal, QObject, QSignalBlocker, pyqtSlot
|
from PyQt6.QtCore import pyqtSignal, QObject, QSignalBlocker, pyqtSlot
|
||||||
import pyqtgraph.parametertree.parameterTypes as pTypes
|
import pyqtgraph.parametertree.parameterTypes as pTypes
|
||||||
from pyqtgraph.parametertree import (
|
from pyqtgraph.parametertree import (
|
||||||
Parameter,
|
Parameter,
|
||||||
registerParameterType,
|
registerParameterType,
|
||||||
)
|
)
|
||||||
|
from qasync import asyncSlot
|
||||||
|
from autotune import PIDAutotuneState
|
||||||
|
|
||||||
|
|
||||||
class MutexParameter(pTypes.ListParameter):
|
class MutexParameter(pTypes.ListParameter):
|
||||||
@ -42,26 +45,23 @@ class MutexParameter(pTypes.ListParameter):
|
|||||||
registerParameterType("mutex", MutexParameter)
|
registerParameterType("mutex", MutexParameter)
|
||||||
|
|
||||||
|
|
||||||
def set_tree_label_tips(tree):
|
|
||||||
for item in tree.listAllItems():
|
|
||||||
p = item.param
|
|
||||||
if "tip" in p.opts:
|
|
||||||
item.setToolTip(0, p.opts["tip"])
|
|
||||||
|
|
||||||
|
|
||||||
class CtrlPanel(QObject):
|
class CtrlPanel(QObject):
|
||||||
set_zero_limits_warning_sig = pyqtSignal(list)
|
set_zero_limits_warning_sig = pyqtSignal(list)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
thermostat,
|
||||||
|
autotuners,
|
||||||
|
info_box,
|
||||||
trees_ui,
|
trees_ui,
|
||||||
param_tree,
|
param_tree,
|
||||||
sigTreeStateChanged_handle,
|
|
||||||
sigActivated_handles,
|
|
||||||
parent=None,
|
parent=None,
|
||||||
):
|
):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self.thermostat = thermostat
|
||||||
|
self.autotuners = autotuners
|
||||||
|
self.info_box = info_box
|
||||||
self.trees_ui = trees_ui
|
self.trees_ui = trees_ui
|
||||||
self.NUM_CHANNELS = len(trees_ui)
|
self.NUM_CHANNELS = len(trees_ui)
|
||||||
|
|
||||||
@ -84,12 +84,23 @@ class CtrlPanel(QObject):
|
|||||||
tree.setHeaderHidden(True)
|
tree.setHeaderHidden(True)
|
||||||
tree.setParameters(self.params[i], showTop=False)
|
tree.setParameters(self.params[i], showTop=False)
|
||||||
self.params[i].setValue = self._setValue
|
self.params[i].setValue = self._setValue
|
||||||
self.params[i].sigTreeStateChanged.connect(sigTreeStateChanged_handle)
|
self.params[i].sigTreeStateChanged.connect(self.send_command)
|
||||||
|
|
||||||
set_tree_label_tips(tree)
|
self.params[i].child("Save to flash").sigActivated.connect(
|
||||||
|
partial(self.save_settings, i)
|
||||||
|
)
|
||||||
|
self.params[i].child("Load from flash").sigActivated.connect(
|
||||||
|
partial(self.load_settings, i)
|
||||||
|
)
|
||||||
|
self.params[i].child(
|
||||||
|
"PID Config", "PID Auto Tune", "Run"
|
||||||
|
).sigActivated.connect(partial(self.pid_auto_tune_request, i))
|
||||||
|
|
||||||
for handle in sigActivated_handles[i]:
|
self.thermostat.pid_update.connect(self.update_pid)
|
||||||
self.params[i].child(*handle[0]).sigActivated.connect(handle[1])
|
self.thermostat.report_update.connect(self.update_report)
|
||||||
|
self.thermostat.thermistor_update.connect(self.update_thermistor)
|
||||||
|
self.thermostat.pwm_update.connect(self.update_pwm)
|
||||||
|
self.thermostat.postfilter_update.connect(self.update_postfilter)
|
||||||
|
|
||||||
def _setValue(self, value, blockSignal=None):
|
def _setValue(self, value, blockSignal=None):
|
||||||
"""
|
"""
|
||||||
@ -120,28 +131,66 @@ class CtrlPanel(QObject):
|
|||||||
def change_params_title(self, channel, path, title):
|
def change_params_title(self, channel, path, title):
|
||||||
self.params[channel].child(*path).setOpts(title=title)
|
self.params[channel].child(*path).setOpts(title=title)
|
||||||
|
|
||||||
|
@asyncSlot(object, object)
|
||||||
|
async def send_command(self, param, changes):
|
||||||
|
"""Translates parameter tree changes into thermostat set_param calls"""
|
||||||
|
ch = param.channel
|
||||||
|
|
||||||
|
for inner_param, change, data in changes:
|
||||||
|
if change == "value":
|
||||||
|
if inner_param.opts.get("param", None) is not None:
|
||||||
|
if inner_param.opts.get("suffix", None) == "mA":
|
||||||
|
data /= 1000 # Given in mA
|
||||||
|
|
||||||
|
thermostat_param = inner_param.opts["param"]
|
||||||
|
if thermostat_param[1] == "ch":
|
||||||
|
thermostat_param[1] = ch
|
||||||
|
|
||||||
|
if inner_param.name() == "Postfilter Rate" and data is None:
|
||||||
|
set_param_args = (*thermostat_param[:2], "off")
|
||||||
|
else:
|
||||||
|
set_param_args = (*thermostat_param, data)
|
||||||
|
param.child(*param.childPath(inner_param)).setOpts(lock=True)
|
||||||
|
await self.thermostat.set_param(*set_param_args)
|
||||||
|
param.child(*param.childPath(inner_param)).setOpts(lock=False)
|
||||||
|
|
||||||
|
if inner_param.opts.get("pid_autotune", None) is not None:
|
||||||
|
auto_tuner_param = inner_param.opts["pid_autotune"][0]
|
||||||
|
if inner_param.opts["pid_autotune"][1] != "ch":
|
||||||
|
ch = inner_param.opts["pid_autotune"][1]
|
||||||
|
self.autotuners.set_params(auto_tuner_param, ch, data)
|
||||||
|
|
||||||
|
if inner_param.opts.get("activaters", None) is not None:
|
||||||
|
activater = inner_param.opts["activaters"][
|
||||||
|
inner_param.opts["limits"].index(data)
|
||||||
|
]
|
||||||
|
if activater is not None:
|
||||||
|
if activater[1] == "ch":
|
||||||
|
activater[1] = ch
|
||||||
|
await self.thermostat.set_param(*activater)
|
||||||
|
|
||||||
@pyqtSlot("QVariantList")
|
@pyqtSlot("QVariantList")
|
||||||
def update_pid(self, pid_settings):
|
def update_pid(self, pid_settings):
|
||||||
for settings in pid_settings:
|
for settings in pid_settings:
|
||||||
channel = settings["channel"]
|
channel = settings["channel"]
|
||||||
with QSignalBlocker(self.params[channel]):
|
with QSignalBlocker(self.params[channel]):
|
||||||
self.params[channel].child("pid", "kp").setValue(
|
self.params[channel].child("PID Config", "Kp").setValue(
|
||||||
settings["parameters"]["kp"]
|
settings["parameters"]["kp"]
|
||||||
)
|
)
|
||||||
self.params[channel].child("pid", "ki").setValue(
|
self.params[channel].child("PID Config", "Ki").setValue(
|
||||||
settings["parameters"]["ki"]
|
settings["parameters"]["ki"]
|
||||||
)
|
)
|
||||||
self.params[channel].child("pid", "kd").setValue(
|
self.params[channel].child("PID Config", "Kd").setValue(
|
||||||
settings["parameters"]["kd"]
|
settings["parameters"]["kd"]
|
||||||
)
|
)
|
||||||
self.params[channel].child(
|
self.params[channel].child(
|
||||||
"pid", "pid_output_clamping", "output_min"
|
"PID Config", "PID Output Clamping", "Minimum"
|
||||||
).setValue(settings["parameters"]["output_min"] * 1000)
|
).setValue(settings["parameters"]["output_min"] * 1000)
|
||||||
self.params[channel].child(
|
self.params[channel].child(
|
||||||
"pid", "pid_output_clamping", "output_max"
|
"PID Config", "PID Output Clamping", "Maximum"
|
||||||
).setValue(settings["parameters"]["output_max"] * 1000)
|
).setValue(settings["parameters"]["output_max"] * 1000)
|
||||||
self.params[channel].child(
|
self.params[channel].child(
|
||||||
"output", "control_method", "set_temperature"
|
"Output Config", "Control Method", "Set Temperature"
|
||||||
).setValue(settings["target"])
|
).setValue(settings["target"])
|
||||||
|
|
||||||
@pyqtSlot("QVariantList")
|
@pyqtSlot("QVariantList")
|
||||||
@ -149,19 +198,19 @@ class CtrlPanel(QObject):
|
|||||||
for settings in report_data:
|
for settings in report_data:
|
||||||
channel = settings["channel"]
|
channel = settings["channel"]
|
||||||
with QSignalBlocker(self.params[channel]):
|
with QSignalBlocker(self.params[channel]):
|
||||||
self.params[channel].child("output", "control_method").setValue(
|
self.params[channel].child("Output Config", "Control Method").setValue(
|
||||||
"temperature_pid" if settings["pid_engaged"] else "constant_current"
|
"Temperature PID" if settings["pid_engaged"] else "Constant Current"
|
||||||
)
|
)
|
||||||
self.params[channel].child(
|
self.params[channel].child(
|
||||||
"output", "control_method", "set_current"
|
"Output Config", "Control Method", "Set Current"
|
||||||
).setValue(settings["i_set"] * 1000)
|
).setValue(settings["i_set"] * 1000)
|
||||||
if settings["temperature"] is not None:
|
if settings["temperature"] is not None:
|
||||||
self.params[channel].child("temperature").setValue(
|
self.params[channel].child("Temperature").setValue(
|
||||||
settings["temperature"]
|
settings["temperature"]
|
||||||
)
|
)
|
||||||
if settings["tec_i"] is not None:
|
if settings["tec_i"] is not None:
|
||||||
self.params[channel].child("current").setValue(
|
self.params[channel].child("Current through TEC").setValue(
|
||||||
settings["tec_i"]
|
settings["tec_i"] * 1000
|
||||||
)
|
)
|
||||||
|
|
||||||
@pyqtSlot("QVariantList")
|
@pyqtSlot("QVariantList")
|
||||||
@ -169,13 +218,13 @@ class CtrlPanel(QObject):
|
|||||||
for sh_param in sh_data:
|
for sh_param in sh_data:
|
||||||
channel = sh_param["channel"]
|
channel = sh_param["channel"]
|
||||||
with QSignalBlocker(self.params[channel]):
|
with QSignalBlocker(self.params[channel]):
|
||||||
self.params[channel].child("thermistor", "t0").setValue(
|
self.params[channel].child("Thermistor Config", "T₀").setValue(
|
||||||
sh_param["params"]["t0"] - 273.15
|
sh_param["params"]["t0"] - 273.15
|
||||||
)
|
)
|
||||||
self.params[channel].child("thermistor", "r0").setValue(
|
self.params[channel].child("Thermistor Config", "R₀").setValue(
|
||||||
sh_param["params"]["r0"]
|
sh_param["params"]["r0"]
|
||||||
)
|
)
|
||||||
self.params[channel].child("thermistor", "b").setValue(
|
self.params[channel].child("Thermistor Config", "B").setValue(
|
||||||
sh_param["params"]["b"]
|
sh_param["params"]["b"]
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -186,15 +235,15 @@ class CtrlPanel(QObject):
|
|||||||
for pwm_params in pwm_data:
|
for pwm_params in pwm_data:
|
||||||
channel = pwm_params["channel"]
|
channel = pwm_params["channel"]
|
||||||
with QSignalBlocker(self.params[channel]):
|
with QSignalBlocker(self.params[channel]):
|
||||||
self.params[channel].child("output", "limits", "max_v").setValue(
|
self.params[channel].child(
|
||||||
pwm_params["max_v"]["value"]
|
"Output Config", "Limits", "Max Voltage Difference"
|
||||||
)
|
).setValue(pwm_params["max_v"]["value"])
|
||||||
self.params[channel].child("output", "limits", "max_i_pos").setValue(
|
self.params[channel].child(
|
||||||
pwm_params["max_i_pos"]["value"] * 1000
|
"Output Config", "Limits", "Max Cooling Current"
|
||||||
)
|
).setValue(pwm_params["max_i_pos"]["value"] * 1000)
|
||||||
self.params[channel].child("output", "limits", "max_i_neg").setValue(
|
self.params[channel].child(
|
||||||
pwm_params["max_i_neg"]["value"] * 1000
|
"Output Config", "Limits", "Max Heating Current"
|
||||||
)
|
).setValue(pwm_params["max_i_neg"]["value"] * 1000)
|
||||||
|
|
||||||
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]["value"] == 0.0:
|
if pwm_params[limit]["value"] == 0.0:
|
||||||
@ -206,6 +255,39 @@ class CtrlPanel(QObject):
|
|||||||
for postfilter_params in postfilter_data:
|
for postfilter_params in postfilter_data:
|
||||||
channel = postfilter_params["channel"]
|
channel = postfilter_params["channel"]
|
||||||
with QSignalBlocker(self.params[channel]):
|
with QSignalBlocker(self.params[channel]):
|
||||||
self.params[channel].child("thermistor", "postfilter_rate").setValue(
|
self.params[channel].child(
|
||||||
postfilter_params["rate"]
|
"Thermistor Config", "Postfilter Rate"
|
||||||
|
).setValue(postfilter_params["rate"])
|
||||||
|
|
||||||
|
@asyncSlot(int)
|
||||||
|
async def load_settings(self, ch):
|
||||||
|
await self.thermostat.load_cfg(ch)
|
||||||
|
|
||||||
|
self.info_box.display_info_box(
|
||||||
|
f"Channel {ch} settings loaded",
|
||||||
|
f"Channel {ch} settings has been loaded from flash.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@asyncSlot(int)
|
||||||
|
async def save_settings(self, ch):
|
||||||
|
await self.thermostat.save_cfg(ch)
|
||||||
|
|
||||||
|
self.info_box.display_info_box(
|
||||||
|
f"Channel {ch} settings saved",
|
||||||
|
f"Channel {ch} settings has been saved to flash.\n"
|
||||||
|
"It will be loaded on Thermostat reset, or when settings are explicitly loaded.",
|
||||||
|
)
|
||||||
|
|
||||||
|
@asyncSlot()
|
||||||
|
async def pid_auto_tune_request(self, ch=0):
|
||||||
|
match self.autotuners.get_state(ch):
|
||||||
|
case PIDAutotuneState.STATE_OFF | PIDAutotuneState.STATE_FAILED:
|
||||||
|
self.autotuners.load_params_and_set_ready(ch)
|
||||||
|
|
||||||
|
case (
|
||||||
|
PIDAutotuneState.STATE_READY
|
||||||
|
| PIDAutotuneState.STATE_RELAY_STEP_UP
|
||||||
|
| PIDAutotuneState.STATE_RELAY_STEP_DOWN
|
||||||
|
):
|
||||||
|
await self.autotuners.stop_pid_from_running(ch)
|
||||||
|
|
||||||
|
@ -10,8 +10,12 @@ pg.setConfigOptions(antialias=True)
|
|||||||
|
|
||||||
|
|
||||||
class LiveDataPlotter(QObject):
|
class LiveDataPlotter(QObject):
|
||||||
def __init__(self, live_plots):
|
def __init__(self, thermostat, live_plots):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
self._thermostat = thermostat
|
||||||
|
|
||||||
|
self._thermostat.report_update.connect(self.update_report)
|
||||||
|
self._thermostat.pid_update.connect(self.update_pid)
|
||||||
|
|
||||||
self.NUM_CHANNELS = len(live_plots)
|
self.NUM_CHANNELS = len(live_plots)
|
||||||
self.graphs = []
|
self.graphs = []
|
||||||
|
@ -1,36 +1,31 @@
|
|||||||
{
|
{
|
||||||
"ctrl_panel": [
|
"ctrl_panel":[
|
||||||
{
|
{
|
||||||
"name": "temperature",
|
"name":"Temperature",
|
||||||
"title": "Temperature",
|
"type":"float",
|
||||||
"type": "float",
|
"format":"{value:.4f} °C",
|
||||||
"format": "{value:.4f} °C",
|
"readonly":true
|
||||||
"readonly": true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "current",
|
"name":"Current through TEC",
|
||||||
"title": "Current through TEC",
|
"type":"float",
|
||||||
"type": "float",
|
"suffix":"mA",
|
||||||
"siPrefix": true,
|
"decimals":6,
|
||||||
"suffix": "A",
|
"readonly":true
|
||||||
"decimals": 6,
|
|
||||||
"readonly": true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "output",
|
"name":"Output Config",
|
||||||
"title": "Output Settings",
|
"expanded":true,
|
||||||
"expanded": true,
|
"type":"group",
|
||||||
"type": "group",
|
"children":[
|
||||||
"children": [
|
|
||||||
{
|
{
|
||||||
"name": "control_method",
|
"name":"Control Method",
|
||||||
"title": "Control Method",
|
"type":"mutex",
|
||||||
"type": "mutex",
|
"limits":[
|
||||||
"limits": {
|
"Constant Current",
|
||||||
"Constant Current": "constant_current",
|
"Temperature PID"
|
||||||
"Temperature PID": "temperature_pid"
|
],
|
||||||
},
|
"activaters":[
|
||||||
"activaters": [
|
|
||||||
null,
|
null,
|
||||||
[
|
[
|
||||||
"pwm",
|
"pwm",
|
||||||
@ -38,370 +33,333 @@
|
|||||||
"pid"
|
"pid"
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"children": [
|
"children":[
|
||||||
{
|
{
|
||||||
"name": "set_current",
|
"name":"Set Current",
|
||||||
"title": "Set Current (mA)",
|
"type":"float",
|
||||||
"type": "float",
|
"value":0,
|
||||||
"value": 0,
|
"step":100,
|
||||||
"step": 100,
|
"limits":[
|
||||||
"limits": [
|
|
||||||
-2000,
|
-2000,
|
||||||
2000
|
2000
|
||||||
],
|
],
|
||||||
"triggerOnShow": true,
|
"triggerOnShow":true,
|
||||||
"decimals": 6,
|
"decimals":6,
|
||||||
"compactHeight": false,
|
"suffix":"mA",
|
||||||
"param": [
|
"param":[
|
||||||
"pwm",
|
"pwm",
|
||||||
"ch",
|
"ch",
|
||||||
"i_set"
|
"i_set"
|
||||||
],
|
],
|
||||||
"lock": false
|
"lock":false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "set_temperature",
|
"name":"Set Temperature",
|
||||||
"title": "Set Temperature (°C)",
|
"type":"float",
|
||||||
"type": "float",
|
"value":25,
|
||||||
"value": 25,
|
"step":0.1,
|
||||||
"step": 0.1,
|
"limits":[
|
||||||
"limits": [
|
|
||||||
-273,
|
-273,
|
||||||
300
|
300
|
||||||
],
|
],
|
||||||
"format": "{value:.4f}",
|
"format":"{value:.4f} °C",
|
||||||
"compactHeight": false,
|
"param":[
|
||||||
"param": [
|
|
||||||
"pid",
|
"pid",
|
||||||
"ch",
|
"ch",
|
||||||
"target"
|
"target"
|
||||||
],
|
],
|
||||||
"lock": false
|
"lock":false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "limits",
|
"name":"Limits",
|
||||||
"title": "Limits",
|
"expanded":true,
|
||||||
"expanded": true,
|
"type":"group",
|
||||||
"type": "group",
|
"children":[
|
||||||
"children": [
|
|
||||||
{
|
{
|
||||||
"name": "max_i_pos",
|
"name":"Max Cooling Current",
|
||||||
"title": "Max Cooling Current (mA)",
|
"type":"float",
|
||||||
"type": "float",
|
"value":0,
|
||||||
"value": 0,
|
"step":100,
|
||||||
"step": 100,
|
"decimals":6,
|
||||||
"decimals": 6,
|
"limits":[
|
||||||
"compactHeight": false,
|
|
||||||
"limits": [
|
|
||||||
0,
|
0,
|
||||||
2000
|
2000
|
||||||
],
|
],
|
||||||
"param": [
|
"suffix":"mA",
|
||||||
|
"param":[
|
||||||
"pwm",
|
"pwm",
|
||||||
"ch",
|
"ch",
|
||||||
"max_i_pos"
|
"max_i_pos"
|
||||||
],
|
],
|
||||||
"lock": false
|
"lock":false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "max_i_neg",
|
"name":"Max Heating Current",
|
||||||
"title": "Max Heating Current (mA)",
|
"type":"float",
|
||||||
"type": "float",
|
"value":0,
|
||||||
"value": 0,
|
"step":100,
|
||||||
"step": 100,
|
"decimals":6,
|
||||||
"decimals": 6,
|
"limits":[
|
||||||
"compactHeight": false,
|
|
||||||
"limits": [
|
|
||||||
0,
|
0,
|
||||||
2000
|
2000
|
||||||
],
|
],
|
||||||
"param": [
|
"suffix":"mA",
|
||||||
|
"param":[
|
||||||
"pwm",
|
"pwm",
|
||||||
"ch",
|
"ch",
|
||||||
"max_i_neg"
|
"max_i_neg"
|
||||||
],
|
],
|
||||||
"lock": false
|
"lock":false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "max_v",
|
"name":"Max Voltage Difference",
|
||||||
"title": "Max Voltage Difference (V)",
|
"type":"float",
|
||||||
"type": "float",
|
"value":0,
|
||||||
"value": 0,
|
"step":0.1,
|
||||||
"step": 0.1,
|
"limits":[
|
||||||
"limits": [
|
|
||||||
0,
|
0,
|
||||||
5
|
5
|
||||||
],
|
],
|
||||||
"siPrefix": true,
|
"siPrefix":true,
|
||||||
"compactHeight": false,
|
"suffix":"V",
|
||||||
"param": [
|
"param":[
|
||||||
"pwm",
|
"pwm",
|
||||||
"ch",
|
"ch",
|
||||||
"max_v"
|
"max_v"
|
||||||
],
|
],
|
||||||
"lock": false
|
"lock":false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "thermistor",
|
"name":"Thermistor Config",
|
||||||
"title": "Thermistor Settings",
|
"expanded":true,
|
||||||
"expanded": true,
|
"type":"group",
|
||||||
"type": "group",
|
"children":[
|
||||||
"tip": "Settings of the connected Thermistor",
|
|
||||||
"children": [
|
|
||||||
{
|
{
|
||||||
"name": "t0",
|
"name":"T₀",
|
||||||
"title": "T₀ (°C)",
|
"type":"float",
|
||||||
"type": "float",
|
"value":25,
|
||||||
"value": 25,
|
"step":0.1,
|
||||||
"step": 0.1,
|
"limits":[
|
||||||
"limits": [
|
|
||||||
-100,
|
-100,
|
||||||
100
|
100
|
||||||
],
|
],
|
||||||
"format": "{value:.4f}",
|
"format":"{value:.4f} °C",
|
||||||
"compactHeight": false,
|
"param":[
|
||||||
"param": [
|
|
||||||
"s-h",
|
"s-h",
|
||||||
"ch",
|
"ch",
|
||||||
"t0"
|
"t0"
|
||||||
],
|
],
|
||||||
"tip": "The origin temperature for the B-Parameter Formula",
|
"lock":false
|
||||||
"lock": false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "r0",
|
"name":"R₀",
|
||||||
"title": "R₀ (Ω)",
|
"type":"float",
|
||||||
"type": "float",
|
"value":10000,
|
||||||
"value": 10000,
|
"step":1,
|
||||||
"step": 1,
|
"siPrefix":true,
|
||||||
"siPrefix": true,
|
"suffix":"Ω",
|
||||||
"compactHeight": false,
|
"param":[
|
||||||
"param": [
|
|
||||||
"s-h",
|
"s-h",
|
||||||
"ch",
|
"ch",
|
||||||
"r0"
|
"r0"
|
||||||
],
|
],
|
||||||
"tip": "The origin resistance for the B-Parameter Formula",
|
"lock":false
|
||||||
"lock": false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "b",
|
"name":"B",
|
||||||
"title": "B (K)",
|
"type":"float",
|
||||||
"type": "float",
|
"value":3950,
|
||||||
"value": 3950,
|
"step":1,
|
||||||
"step": 1,
|
"suffix":"K",
|
||||||
"decimals": 4,
|
"decimals":4,
|
||||||
"compactHeight": false,
|
"param":[
|
||||||
"param": [
|
|
||||||
"s-h",
|
"s-h",
|
||||||
"ch",
|
"ch",
|
||||||
"b"
|
"b"
|
||||||
],
|
],
|
||||||
"lock": false
|
"lock":false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "postfilter_rate",
|
"name":"Postfilter Rate",
|
||||||
"title": "Postfilter Rate",
|
"type":"list",
|
||||||
"type": "list",
|
"value":16.67,
|
||||||
"value": 16.67,
|
"param":[
|
||||||
"param": [
|
|
||||||
"postfilter",
|
"postfilter",
|
||||||
"ch",
|
"ch",
|
||||||
"rate"
|
"rate"
|
||||||
],
|
],
|
||||||
"limits": {
|
"limits":{
|
||||||
"Off": null,
|
"Off":null,
|
||||||
"16.67 Hz": 16.67,
|
"16.67 Hz":16.67,
|
||||||
"20 Hz": 20.0,
|
"20 Hz":20.0,
|
||||||
"21.25 Hz": 21.25,
|
"21.25 Hz":21.25,
|
||||||
"27 Hz": 27.0
|
"27 Hz":27.0
|
||||||
},
|
},
|
||||||
"lock": false
|
"lock":false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "pid",
|
"name":"PID Config",
|
||||||
"title": "PID Settings",
|
"expanded":true,
|
||||||
"expanded": true,
|
"type":"group",
|
||||||
"type": "group",
|
"children":[
|
||||||
"children": [
|
|
||||||
{
|
{
|
||||||
"name": "kp",
|
"name":"Kp",
|
||||||
"title": "Kp",
|
"type":"float",
|
||||||
"type": "float",
|
"step":0.1,
|
||||||
"step": 0.1,
|
"suffix":"",
|
||||||
"suffix": "",
|
"param":[
|
||||||
"compactHeight": false,
|
|
||||||
"param": [
|
|
||||||
"pid",
|
"pid",
|
||||||
"ch",
|
"ch",
|
||||||
"kp"
|
"kp"
|
||||||
],
|
],
|
||||||
"lock": false
|
"lock":false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "ki",
|
"name":"Ki",
|
||||||
"title": "Ki",
|
"type":"float",
|
||||||
"type": "float",
|
"step":0.1,
|
||||||
"step": 0.1,
|
"suffix":"Hz",
|
||||||
"suffix": "Hz",
|
"param":[
|
||||||
"compactHeight": false,
|
|
||||||
"param": [
|
|
||||||
"pid",
|
"pid",
|
||||||
"ch",
|
"ch",
|
||||||
"ki"
|
"ki"
|
||||||
],
|
],
|
||||||
"lock": false
|
"lock":false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "kd",
|
"name":"Kd",
|
||||||
"title": "Kd",
|
"type":"float",
|
||||||
"type": "float",
|
"step":0.1,
|
||||||
"step": 0.1,
|
"suffix":"s",
|
||||||
"suffix": "s",
|
"param":[
|
||||||
"compactHeight": false,
|
|
||||||
"param": [
|
|
||||||
"pid",
|
"pid",
|
||||||
"ch",
|
"ch",
|
||||||
"kd"
|
"kd"
|
||||||
],
|
],
|
||||||
"lock": false
|
"lock":false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "pid_output_clamping",
|
"name":"PID Output Clamping",
|
||||||
"title": "PID Output Clamping",
|
"expanded":true,
|
||||||
"expanded": true,
|
"type":"group",
|
||||||
"type": "group",
|
"children":[
|
||||||
"children": [
|
|
||||||
{
|
{
|
||||||
"name": "output_min",
|
"name":"Minimum",
|
||||||
"title": "Minimum (mA)",
|
"type":"float",
|
||||||
"type": "float",
|
"step":100,
|
||||||
"step": 100,
|
"limits":[
|
||||||
"limits": [
|
|
||||||
-2000,
|
-2000,
|
||||||
2000
|
2000
|
||||||
],
|
],
|
||||||
"decimals": 6,
|
"decimals":6,
|
||||||
"compactHeight": false,
|
"suffix":"mA",
|
||||||
"param": [
|
"param":[
|
||||||
"pid",
|
"pid",
|
||||||
"ch",
|
"ch",
|
||||||
"output_min"
|
"output_min"
|
||||||
],
|
],
|
||||||
"lock": false
|
"lock":false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "output_max",
|
"name":"Maximum",
|
||||||
"title": "Maximum (mA)",
|
"type":"float",
|
||||||
"type": "float",
|
"step":100,
|
||||||
"step": 100,
|
"limits":[
|
||||||
"limits": [
|
|
||||||
-2000,
|
-2000,
|
||||||
2000
|
2000
|
||||||
],
|
],
|
||||||
"decimals": 6,
|
"decimals":6,
|
||||||
"compactHeight": false,
|
"suffix":"mA",
|
||||||
"param": [
|
"param":[
|
||||||
"pid",
|
"pid",
|
||||||
"ch",
|
"ch",
|
||||||
"output_max"
|
"output_max"
|
||||||
],
|
],
|
||||||
"lock": false
|
"lock":false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "pid_autotune",
|
"name":"PID Auto Tune",
|
||||||
"title": "PID Autotune",
|
"expanded":false,
|
||||||
"expanded": false,
|
"type":"group",
|
||||||
"type": "group",
|
"children":[
|
||||||
"children": [
|
|
||||||
{
|
{
|
||||||
"name": "target_temp",
|
"name":"Target Temperature",
|
||||||
"title": "Target Temperature (°C)",
|
"type":"float",
|
||||||
"type": "float",
|
"value":20,
|
||||||
"value": 20,
|
"step":0.1,
|
||||||
"step": 0.1,
|
"format":"{value:.4f} °C",
|
||||||
"format": "{value:.4f}",
|
"pid_autotune":[
|
||||||
"compactHeight": false,
|
|
||||||
"pid_autotune": [
|
|
||||||
"target_temp",
|
"target_temp",
|
||||||
"ch"
|
"ch"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "test_current",
|
"name":"Test Current",
|
||||||
"title": "Test Current (mA)",
|
"type":"float",
|
||||||
"type": "float",
|
"value":0,
|
||||||
"value": 0,
|
"decimals":6,
|
||||||
"decimals": 6,
|
"step":100,
|
||||||
"compactHeight": false,
|
"limits":[
|
||||||
"step": 100,
|
|
||||||
"limits": [
|
|
||||||
-2000,
|
-2000,
|
||||||
2000
|
2000
|
||||||
],
|
],
|
||||||
"pid_autotune": [
|
"suffix":"mA",
|
||||||
|
"pid_autotune":[
|
||||||
"test_current",
|
"test_current",
|
||||||
"ch"
|
"ch"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "temp_swing",
|
"name":"Temperature Swing",
|
||||||
"title": "Temperature Swing (°C)",
|
"type":"float",
|
||||||
"type": "float",
|
"value":1.5,
|
||||||
"value": 1.5,
|
"step":0.1,
|
||||||
"step": 0.1,
|
"prefix":"±",
|
||||||
"prefix": "±",
|
"format":"{value:.4f} °C",
|
||||||
"format": "{value:.4f}",
|
"pid_autotune":[
|
||||||
"compactHeight": false,
|
|
||||||
"pid_autotune": [
|
|
||||||
"temp_swing",
|
"temp_swing",
|
||||||
"ch"
|
"ch"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "lookback",
|
"name":"Lookback",
|
||||||
"title": "Lookback (s)",
|
"type":"float",
|
||||||
"type": "float",
|
"value":3.0,
|
||||||
"value": 3.0,
|
"step":0.1,
|
||||||
"step": 0.1,
|
"format":"{value:.4f} s",
|
||||||
"format": "{value:.4f}",
|
"pid_autotune":[
|
||||||
"compactHeight": false,
|
|
||||||
"pid_autotune": [
|
|
||||||
"lookback",
|
"lookback",
|
||||||
"ch"
|
"ch"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "run_pid",
|
"name":"Run",
|
||||||
"title": "Run",
|
"type":"action",
|
||||||
"type": "action",
|
"tip":"Run"
|
||||||
"tip": "Run PID Autotune with above settings"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "save",
|
"name":"Save to flash",
|
||||||
"title": "Save to flash",
|
"type":"action",
|
||||||
"type": "action",
|
"tip":"Save config to thermostat, applies on reset"
|
||||||
"tip": "Save settings to thermostat, applies on reset"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "load",
|
"name":"Load from flash",
|
||||||
"title": "Load from flash",
|
"type":"action",
|
||||||
"type": "action",
|
"tip":"Load config from flash"
|
||||||
"tip": "Load settings from flash"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -2,18 +2,22 @@ from PyQt6 import QtWidgets, QtGui
|
|||||||
|
|
||||||
|
|
||||||
class PlotOptionsMenu(QtWidgets.QMenu):
|
class PlotOptionsMenu(QtWidgets.QMenu):
|
||||||
def __init__(self, max_samples=1000):
|
def __init__(self, channel_graphs, max_samples=1000):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
self.channel_graphs = channel_graphs
|
||||||
|
|
||||||
self.setTitle("Plot Settings")
|
self.setTitle("Plot Settings")
|
||||||
|
|
||||||
clear = QtGui.QAction("Clear graphs", self)
|
clear = QtGui.QAction("Clear graphs", self)
|
||||||
self.addAction(clear)
|
self.addAction(clear)
|
||||||
self.clear = clear
|
self.clear = clear
|
||||||
|
self.clear.triggered.connect(self.channel_graphs.clear_graphs)
|
||||||
|
|
||||||
self.samples_spinbox = QtWidgets.QSpinBox()
|
self.samples_spinbox = QtWidgets.QSpinBox()
|
||||||
self.samples_spinbox.setRange(2, 100000)
|
self.samples_spinbox.setRange(2, 100000)
|
||||||
self.samples_spinbox.setSuffix(" samples")
|
self.samples_spinbox.setSuffix(" samples")
|
||||||
self.samples_spinbox.setValue(max_samples)
|
self.samples_spinbox.setValue(max_samples)
|
||||||
|
self.samples_spinbox.valueChanged.connect(self.channel_graphs.set_max_samples)
|
||||||
|
|
||||||
limit_samples = QtWidgets.QWidgetAction(self)
|
limit_samples = QtWidgets.QWidgetAction(self)
|
||||||
limit_samples.setDefaultWidget(self.samples_spinbox)
|
limit_samples.setDefaultWidget(self.samples_spinbox)
|
||||||
|
@ -1,24 +1,20 @@
|
|||||||
|
import logging
|
||||||
from PyQt6 import QtWidgets, QtGui, QtCore
|
from PyQt6 import QtWidgets, QtGui, QtCore
|
||||||
from PyQt6.QtCore import pyqtSignal, pyqtSlot
|
from PyQt6.QtCore import pyqtSignal, pyqtSlot, QSignalBlocker
|
||||||
|
from qasync import asyncSlot
|
||||||
|
from pytec.gui.view.net_settings_input_diag import NetSettingsInputDiag
|
||||||
|
|
||||||
|
|
||||||
class ThermostatCtrlMenu(QtWidgets.QMenu):
|
class ThermostatCtrlMenu(QtWidgets.QMenu):
|
||||||
fan_set_act = pyqtSignal(int)
|
def __init__(self, thermostat, info_box, style):
|
||||||
fan_auto_set_act = pyqtSignal(int)
|
|
||||||
|
|
||||||
connect_act = pyqtSignal()
|
|
||||||
reset_act = pyqtSignal(bool)
|
|
||||||
dfu_act = pyqtSignal(bool)
|
|
||||||
load_cfg_act = pyqtSignal(int)
|
|
||||||
save_cfg_act = pyqtSignal(int)
|
|
||||||
net_cfg_act = pyqtSignal(bool)
|
|
||||||
|
|
||||||
def __init__(self, style):
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
self._thermostat = thermostat
|
||||||
|
self._info_box = info_box
|
||||||
self._style = style
|
self._style = style
|
||||||
self.setTitle("Thermostat settings")
|
self.setTitle("Thermostat settings")
|
||||||
|
|
||||||
self.hw_rev_data = dict()
|
self.hw_rev_data = dict()
|
||||||
|
self._thermostat.hw_rev_update.connect(self.hw_rev)
|
||||||
|
|
||||||
self.fan_group = QtWidgets.QWidget()
|
self.fan_group = QtWidgets.QWidget()
|
||||||
self.fan_group.setEnabled(False)
|
self.fan_group.setEnabled(False)
|
||||||
@ -45,8 +41,9 @@ class ThermostatCtrlMenu(QtWidgets.QMenu):
|
|||||||
self.fan_pwm_warning.setMinimumSize(QtCore.QSize(16, 0))
|
self.fan_pwm_warning.setMinimumSize(QtCore.QSize(16, 0))
|
||||||
self.fan_layout.addWidget(self.fan_pwm_warning)
|
self.fan_layout.addWidget(self.fan_pwm_warning)
|
||||||
|
|
||||||
self.fan_power_slider.valueChanged.connect(self.fan_set_act)
|
self.fan_power_slider.valueChanged.connect(self.fan_set_request)
|
||||||
self.fan_auto_box.stateChanged.connect(self.fan_auto_set_act)
|
self.fan_auto_box.stateChanged.connect(self.fan_auto_set_request)
|
||||||
|
self._thermostat.fan_update.connect(self.fan_update)
|
||||||
|
|
||||||
self.fan_lbl.setToolTip("Adjust the fan")
|
self.fan_lbl.setToolTip("Adjust the fan")
|
||||||
self.fan_lbl.setText("Fan:")
|
self.fan_lbl.setText("Fan:")
|
||||||
@ -58,40 +55,36 @@ class ThermostatCtrlMenu(QtWidgets.QMenu):
|
|||||||
self.fan = fan
|
self.fan = fan
|
||||||
|
|
||||||
self.actionReset = QtGui.QAction("Reset Thermostat", self)
|
self.actionReset = QtGui.QAction("Reset Thermostat", self)
|
||||||
self.actionReset.triggered.connect(self.reset_act)
|
self.actionReset.triggered.connect(self.reset_request)
|
||||||
self.addAction(self.actionReset)
|
self.addAction(self.actionReset)
|
||||||
|
|
||||||
self.actionEnter_DFU_Mode = QtGui.QAction("Enter DFU Mode", self)
|
self.actionEnter_DFU_Mode = QtGui.QAction("Enter DFU Mode", self)
|
||||||
self.actionEnter_DFU_Mode.triggered.connect(self.dfu_act)
|
self.actionEnter_DFU_Mode.triggered.connect(self.dfu_request)
|
||||||
self.addAction(self.actionEnter_DFU_Mode)
|
self.addAction(self.actionEnter_DFU_Mode)
|
||||||
|
|
||||||
self.actionnet_settings_input_diag = QtGui.QAction("Set IPV4 Settings", self)
|
self.actionnet_settings_input_diag = QtGui.QAction("Set IPV4 Settings", self)
|
||||||
self.actionnet_settings_input_diag.triggered.connect(self.net_cfg_act)
|
self.actionnet_settings_input_diag.triggered.connect(self.net_settings_request)
|
||||||
self.addAction(self.actionnet_settings_input_diag)
|
self.addAction(self.actionnet_settings_input_diag)
|
||||||
|
|
||||||
@pyqtSlot(bool)
|
@asyncSlot(bool)
|
||||||
def load(_):
|
async def load(_):
|
||||||
self.load_cfg_act.emit(0)
|
await self._thermostat.load_cfg()
|
||||||
self.load_cfg_act.emit(1)
|
|
||||||
loaded = QtWidgets.QMessageBox(self)
|
self._info_box.display_info_box(
|
||||||
loaded.setWindowTitle("Config loaded")
|
"Config loaded", "All channel configs have been loaded from flash."
|
||||||
loaded.setText("All channel configs have been loaded from flash.")
|
)
|
||||||
loaded.setIcon(QtWidgets.QMessageBox.Icon.Information)
|
|
||||||
loaded.show()
|
|
||||||
|
|
||||||
self.actionLoad_all_configs = QtGui.QAction("Load Config", self)
|
self.actionLoad_all_configs = QtGui.QAction("Load Config", self)
|
||||||
self.actionLoad_all_configs.triggered.connect(load)
|
self.actionLoad_all_configs.triggered.connect(load)
|
||||||
self.addAction(self.actionLoad_all_configs)
|
self.addAction(self.actionLoad_all_configs)
|
||||||
|
|
||||||
@pyqtSlot(bool)
|
@asyncSlot(bool)
|
||||||
def save(_):
|
async def save(_):
|
||||||
self.save_cfg_act.emit(0)
|
await self._thermostat.save_cfg()
|
||||||
self.save_cfg_act.emit(1)
|
|
||||||
saved = QtWidgets.QMessageBox(self)
|
self._info_box.display_info_box(
|
||||||
saved.setWindowTitle("Config saved")
|
"Config saved", "All channel configs have been saved to flash."
|
||||||
saved.setText("All channel configs have been saved to flash.")
|
)
|
||||||
saved.setIcon(QtWidgets.QMessageBox.Icon.Information)
|
|
||||||
saved.show()
|
|
||||||
|
|
||||||
self.actionSave_all_configs = QtGui.QAction("Save Config", self)
|
self.actionSave_all_configs = QtGui.QAction("Save Config", self)
|
||||||
self.actionSave_all_configs.triggered.connect(save)
|
self.actionSave_all_configs.triggered.connect(save)
|
||||||
@ -127,6 +120,18 @@ class ThermostatCtrlMenu(QtWidgets.QMenu):
|
|||||||
self.actionAbout_Thermostat.triggered.connect(about_thermostat)
|
self.actionAbout_Thermostat.triggered.connect(about_thermostat)
|
||||||
self.addAction(self.actionAbout_Thermostat)
|
self.addAction(self.actionAbout_Thermostat)
|
||||||
|
|
||||||
|
@pyqtSlot("QVariantMap")
|
||||||
|
def fan_update(self, fan_settings):
|
||||||
|
logging.debug(fan_settings)
|
||||||
|
if fan_settings is None:
|
||||||
|
return
|
||||||
|
with QSignalBlocker(self.fan_power_slider):
|
||||||
|
self.fan_power_slider.setValue(
|
||||||
|
fan_settings["fan_pwm"] or 100 # 0 = PWM off = full strength
|
||||||
|
)
|
||||||
|
with QSignalBlocker(self.fan_auto_box):
|
||||||
|
self.fan_auto_box.setChecked(fan_settings["auto_mode"])
|
||||||
|
|
||||||
def set_fan_pwm_warning(self):
|
def set_fan_pwm_warning(self):
|
||||||
if self.fan_power_slider.value() != 100:
|
if self.fan_power_slider.value() != 100:
|
||||||
pixmapi = getattr(QtWidgets.QStyle.StandardPixmap, "SP_MessageBoxWarning")
|
pixmapi = getattr(QtWidgets.QStyle.StandardPixmap, "SP_MessageBoxWarning")
|
||||||
@ -143,3 +148,55 @@ class ThermostatCtrlMenu(QtWidgets.QMenu):
|
|||||||
def hw_rev(self, hw_rev):
|
def hw_rev(self, hw_rev):
|
||||||
self.hw_rev_data = hw_rev
|
self.hw_rev_data = hw_rev
|
||||||
self.fan_group.setEnabled(self.hw_rev_data["settings"]["fan_available"])
|
self.fan_group.setEnabled(self.hw_rev_data["settings"]["fan_available"])
|
||||||
|
|
||||||
|
@asyncSlot(int)
|
||||||
|
async def fan_set_request(self, value):
|
||||||
|
assert self._thermostat.connected()
|
||||||
|
|
||||||
|
if self.fan_auto_box.isChecked():
|
||||||
|
with QSignalBlocker(self.fan_auto_box):
|
||||||
|
self.fan_auto_box.setChecked(False)
|
||||||
|
await self._thermostat.set_fan(value)
|
||||||
|
if not self.hw_rev_data["settings"]["fan_pwm_recommended"]:
|
||||||
|
self.set_fan_pwm_warning()
|
||||||
|
|
||||||
|
@asyncSlot(int)
|
||||||
|
async def fan_auto_set_request(self, enabled):
|
||||||
|
assert self._thermostat.connected()
|
||||||
|
|
||||||
|
if enabled:
|
||||||
|
await self._thermostat.set_fan("auto")
|
||||||
|
self.fan_update(await self._thermostat.get_fan())
|
||||||
|
else:
|
||||||
|
await self.thermostat.set_fan(
|
||||||
|
self.fan_power_slider.value()
|
||||||
|
)
|
||||||
|
|
||||||
|
@asyncSlot(bool)
|
||||||
|
async def reset_request(self, _):
|
||||||
|
assert self._thermostat.connected()
|
||||||
|
|
||||||
|
await self._thermostat.reset()
|
||||||
|
await self._thermostat.end_session()
|
||||||
|
|
||||||
|
@asyncSlot(bool)
|
||||||
|
async def dfu_request(self, _):
|
||||||
|
assert self._thermostat.connected()
|
||||||
|
|
||||||
|
await self._thermostat.dfu()
|
||||||
|
await self._thermostat.end_session()
|
||||||
|
|
||||||
|
@asyncSlot(bool)
|
||||||
|
async def net_settings_request(self, _):
|
||||||
|
assert self._thermostat.connected()
|
||||||
|
|
||||||
|
ipv4 = await self._thermostat.get_ipv4()
|
||||||
|
self.net_settings_input_diag = NetSettingsInputDiag(ipv4["addr"])
|
||||||
|
self.net_settings_input_diag.set_ipv4_act.connect(self.set_net_settings_request)
|
||||||
|
|
||||||
|
@asyncSlot(str)
|
||||||
|
async def set_net_settings_request(self, ipv4_settings):
|
||||||
|
assert self._thermostat.connected()
|
||||||
|
|
||||||
|
await self._thermostat.set_ipv4(ipv4_settings)
|
||||||
|
await self._thermostat.end_session()
|
18
pytec/setup.py
Normal file
18
pytec/setup.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name="pytec",
|
||||||
|
version="0.0",
|
||||||
|
author="M-Labs",
|
||||||
|
url="https://git.m-labs.hk/M-Labs/thermostat",
|
||||||
|
description="Control TEC",
|
||||||
|
license="GPLv3",
|
||||||
|
install_requires=["setuptools"],
|
||||||
|
packages=find_packages(),
|
||||||
|
entry_points={
|
||||||
|
"gui_scripts": [
|
||||||
|
"tec_qt = tec_qt:main",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
py_modules=['autotune', 'plot', 'tec_qt'],
|
||||||
|
)
|
321
pytec/tec_qt.py
Normal file → Executable file
321
pytec/tec_qt.py
Normal file → Executable file
@ -1,5 +1,4 @@
|
|||||||
from pytec.gui.view.zero_limits_warning import ZeroLimitsWarningView
|
from pytec.gui.view.zero_limits_warning import ZeroLimitsWarningView
|
||||||
from pytec.gui.view.net_settings_input_diag import NetSettingsInputDiag
|
|
||||||
from pytec.gui.view.thermostat_ctrl_menu import ThermostatCtrlMenu
|
from pytec.gui.view.thermostat_ctrl_menu import ThermostatCtrlMenu
|
||||||
from pytec.gui.view.conn_menu import ConnMenu
|
from pytec.gui.view.conn_menu import ConnMenu
|
||||||
from pytec.gui.view.plot_options_menu import PlotOptionsMenu
|
from pytec.gui.view.plot_options_menu import PlotOptionsMenu
|
||||||
@ -7,12 +6,11 @@ from pytec.gui.view.live_plot_view import LiveDataPlotter
|
|||||||
from pytec.gui.view.ctrl_panel import CtrlPanel
|
from pytec.gui.view.ctrl_panel import CtrlPanel
|
||||||
from pytec.gui.view.info_box import InfoBox
|
from pytec.gui.view.info_box import InfoBox
|
||||||
from pytec.gui.model.pid_autotuner import PIDAutoTuner
|
from pytec.gui.model.pid_autotuner import PIDAutoTuner
|
||||||
from pytec.gui.model.thermostat import Thermostat
|
from pytec.gui.model.thermostat import Thermostat, ThermostatConnectionState
|
||||||
import json
|
import json
|
||||||
from autotune import PIDAutotuneState
|
from autotune import PIDAutotuneState
|
||||||
from qasync import asyncSlot, asyncClose
|
from qasync import asyncSlot, asyncClose
|
||||||
import qasync
|
import qasync
|
||||||
from pytec.aioclient import StoppedConnecting
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import argparse
|
import argparse
|
||||||
@ -30,9 +28,9 @@ def get_argparser():
|
|||||||
"--connect",
|
"--connect",
|
||||||
default=None,
|
default=None,
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Automatically connect to the specified Thermostat in IP:port format",
|
help="Automatically connect to the specified Thermostat in host:port format",
|
||||||
)
|
)
|
||||||
parser.add_argument("IP", metavar="ip", default=None, nargs="?")
|
parser.add_argument("HOST", metavar="host", default=None, nargs="?")
|
||||||
parser.add_argument("PORT", metavar="port", default=None, nargs="?")
|
parser.add_argument("PORT", metavar="port", default=None, nargs="?")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-l",
|
"-l",
|
||||||
@ -60,47 +58,38 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
ui_file_path = importlib.resources.files("pytec.gui.view").joinpath("tec_qt.ui")
|
ui_file_path = importlib.resources.files("pytec.gui.view").joinpath("tec_qt.ui")
|
||||||
uic.loadUi(ui_file_path, self)
|
uic.loadUi(ui_file_path, self)
|
||||||
|
|
||||||
self.hw_rev_data = None
|
|
||||||
self.info_box = InfoBox()
|
self.info_box = InfoBox()
|
||||||
|
|
||||||
self.thermostat = Thermostat(
|
self.thermostat = Thermostat(
|
||||||
self, self.report_refresh_spin.value()
|
self, self.report_refresh_spin.value()
|
||||||
)
|
)
|
||||||
|
self._connecting_task = None
|
||||||
|
|
||||||
def handle_connection_error():
|
def handle_connection_error():
|
||||||
logging.error("Client connection error, disconnecting")
|
|
||||||
self.info_box.display_info_box(
|
self.info_box.display_info_box(
|
||||||
"Connection Error", "Thermostat connection lost. Is it unplugged?"
|
"Connection Error", "Thermostat connection lost. Is it unplugged?"
|
||||||
)
|
)
|
||||||
|
|
||||||
self.bail()
|
|
||||||
|
|
||||||
self.thermostat.connection_error.connect(handle_connection_error)
|
self.thermostat.connection_error.connect(handle_connection_error)
|
||||||
|
|
||||||
|
self.thermostat.connection_error.connect(self.thermostat.timed_out)
|
||||||
|
self.thermostat.connection_error.connect(self.thermostat.end_session)
|
||||||
|
|
||||||
|
self.thermostat.connection_state_changed.connect(self._on_connection_changed)
|
||||||
|
|
||||||
self.autotuners = PIDAutoTuner(self, self.thermostat, 2)
|
self.autotuners = PIDAutoTuner(self, self.thermostat, 2)
|
||||||
|
self.autotuners.autotune_state_changed.connect(self.pid_autotune_handler)
|
||||||
|
|
||||||
def get_ctrl_panel_config(args):
|
def get_ctrl_panel_config(args):
|
||||||
with open(args.param_tree, "r") as f:
|
with open(args.param_tree, "r", encoding="utf-8") as f:
|
||||||
return json.load(f)["ctrl_panel"]
|
return json.load(f)["ctrl_panel"]
|
||||||
|
|
||||||
param_tree_sigActivated_handles = [
|
|
||||||
[
|
|
||||||
[["save"], partial(self.thermostat.save_cfg, ch)],
|
|
||||||
[["load"], partial(self.thermostat.load_cfg, ch)],
|
|
||||||
[
|
|
||||||
["pid", "pid_autotune", "run_pid"],
|
|
||||||
partial(self.pid_auto_tune_request, ch),
|
|
||||||
],
|
|
||||||
]
|
|
||||||
for ch in range(self.NUM_CHANNELS)
|
|
||||||
]
|
|
||||||
self.thermostat.info_box_trigger.connect(self.info_box.display_info_box)
|
|
||||||
|
|
||||||
self.ctrl_panel_view = CtrlPanel(
|
self.ctrl_panel_view = CtrlPanel(
|
||||||
|
self.thermostat,
|
||||||
|
self.autotuners,
|
||||||
|
self.info_box,
|
||||||
[self.ch0_tree, self.ch1_tree],
|
[self.ch0_tree, self.ch1_tree],
|
||||||
get_ctrl_panel_config(args),
|
get_ctrl_panel_config(args),
|
||||||
self.send_command,
|
|
||||||
param_tree_sigActivated_handles,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.zero_limits_warning = ZeroLimitsWarningView(
|
self.zero_limits_warning = ZeroLimitsWarningView(
|
||||||
@ -110,55 +99,28 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
self.zero_limits_warning.set_limits_warning
|
self.zero_limits_warning.set_limits_warning
|
||||||
)
|
)
|
||||||
|
|
||||||
self.thermostat.fan_update.connect(self.fan_update)
|
self.thermostat.hw_rev_update.connect(self._status)
|
||||||
self.thermostat.report_update.connect(self.ctrl_panel_view.update_report)
|
|
||||||
self.thermostat.report_update.connect(self.autotuners.tick)
|
|
||||||
self.thermostat.report_update.connect(self.pid_autotune_handler)
|
|
||||||
self.thermostat.pid_update.connect(self.ctrl_panel_view.update_pid)
|
|
||||||
self.thermostat.pwm_update.connect(self.ctrl_panel_view.update_pwm)
|
|
||||||
self.thermostat.thermistor_update.connect(
|
|
||||||
self.ctrl_panel_view.update_thermistor
|
|
||||||
)
|
|
||||||
self.thermostat.postfilter_update.connect(
|
|
||||||
self.ctrl_panel_view.update_postfilter
|
|
||||||
)
|
|
||||||
self.thermostat.interval_update.connect(
|
|
||||||
self.autotuners.update_sampling_interval
|
|
||||||
)
|
|
||||||
self.report_apply_btn.clicked.connect(
|
self.report_apply_btn.clicked.connect(
|
||||||
lambda: self.thermostat.set_update_s(self.report_refresh_spin.value())
|
lambda: self.thermostat.set_update_s(self.report_refresh_spin.value())
|
||||||
)
|
)
|
||||||
|
|
||||||
self.channel_graphs = LiveDataPlotter(
|
self.channel_graphs = LiveDataPlotter(
|
||||||
|
self.thermostat,
|
||||||
[
|
[
|
||||||
[getattr(self, f"ch{ch}_t_graph"), getattr(self, f"ch{ch}_i_graph")]
|
[getattr(self, f"ch{ch}_t_graph"), getattr(self, f"ch{ch}_i_graph")]
|
||||||
for ch in range(self.NUM_CHANNELS)
|
for ch in range(self.NUM_CHANNELS)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
self.thermostat.report_update.connect(self.channel_graphs.update_report)
|
self.plot_options_menu = PlotOptionsMenu(self.channel_graphs)
|
||||||
self.thermostat.pid_update.connect(self.channel_graphs.update_pid)
|
|
||||||
|
|
||||||
self.plot_options_menu = PlotOptionsMenu()
|
|
||||||
self.plot_options_menu.clear.triggered.connect(self.clear_graphs)
|
|
||||||
self.plot_options_menu.samples_spinbox.valueChanged.connect(
|
|
||||||
self.channel_graphs.set_max_samples
|
|
||||||
)
|
|
||||||
self.plot_settings.setMenu(self.plot_options_menu)
|
self.plot_settings.setMenu(self.plot_options_menu)
|
||||||
|
|
||||||
self.conn_menu = ConnMenu()
|
self.conn_menu = ConnMenu()
|
||||||
self.connect_btn.setMenu(self.conn_menu)
|
self.connect_btn.setMenu(self.conn_menu)
|
||||||
|
|
||||||
self.thermostat_ctrl_menu = ThermostatCtrlMenu(self.style())
|
self.thermostat_ctrl_menu = ThermostatCtrlMenu(
|
||||||
self.thermostat_ctrl_menu.fan_set_act.connect(self.fan_set_request)
|
self.thermostat, self.info_box, self.style()
|
||||||
self.thermostat_ctrl_menu.fan_auto_set_act.connect(self.fan_auto_set_request)
|
)
|
||||||
self.thermostat_ctrl_menu.reset_act.connect(self.reset_request)
|
|
||||||
self.thermostat_ctrl_menu.dfu_act.connect(self.dfu_request)
|
|
||||||
self.thermostat_ctrl_menu.save_cfg_act.connect(self.save_cfg_request)
|
|
||||||
self.thermostat_ctrl_menu.load_cfg_act.connect(self.load_cfg_request)
|
|
||||||
self.thermostat_ctrl_menu.net_cfg_act.connect(self.net_settings_request)
|
|
||||||
|
|
||||||
self.thermostat.hw_rev_update.connect(self.thermostat_ctrl_menu.hw_rev)
|
|
||||||
self.thermostat_settings.setMenu(self.thermostat_ctrl_menu)
|
self.thermostat_settings.setMenu(self.thermostat_ctrl_menu)
|
||||||
|
|
||||||
self.loading_spinner.hide()
|
self.loading_spinner.hide()
|
||||||
@ -170,166 +132,97 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
self.port_set_spin.setValue(int(args.PORT))
|
self.port_set_spin.setValue(int(args.PORT))
|
||||||
self.connect_btn.click()
|
self.connect_btn.click()
|
||||||
|
|
||||||
def clear_graphs(self):
|
@asyncSlot(ThermostatConnectionState)
|
||||||
self.channel_graphs.clear_graphs()
|
|
||||||
|
|
||||||
async def _on_connection_changed(self, result):
|
async def _on_connection_changed(self, result):
|
||||||
self.graph_group.setEnabled(result)
|
match result:
|
||||||
self.report_group.setEnabled(result)
|
case ThermostatConnectionState.CONNECTED:
|
||||||
self.thermostat_settings.setEnabled(result)
|
self.graph_group.setEnabled(True)
|
||||||
|
self.report_group.setEnabled(True)
|
||||||
|
self.thermostat_settings.setEnabled(True)
|
||||||
|
|
||||||
self.conn_menu.host_set_line.setEnabled(not result)
|
self.conn_menu.host_set_line.setEnabled(False)
|
||||||
self.conn_menu.port_set_spin.setEnabled(not result)
|
self.conn_menu.port_set_spin.setEnabled(False)
|
||||||
self.connect_btn.setText("Disconnect" if result else "Connect")
|
self.connect_btn.setText("Disconnect")
|
||||||
if result:
|
|
||||||
self.hw_rev_data = await self.thermostat.get_hw_rev()
|
case ThermostatConnectionState.CONNECTING:
|
||||||
logging.debug(self.hw_rev_data)
|
self.status_lbl.setText("Connecting...")
|
||||||
|
self.connect_btn.setText("Stop")
|
||||||
|
self.conn_menu.host_set_line.setEnabled(False)
|
||||||
|
self.conn_menu.port_set_spin.setEnabled(False)
|
||||||
|
|
||||||
|
case ThermostatConnectionState.DISCONNECTED:
|
||||||
|
self.graph_group.setEnabled(False)
|
||||||
|
self.report_group.setEnabled(False)
|
||||||
|
self.thermostat_settings.setEnabled(False)
|
||||||
|
|
||||||
|
self.conn_menu.host_set_line.setEnabled(True)
|
||||||
|
self.conn_menu.port_set_spin.setEnabled(True)
|
||||||
|
self.connect_btn.setText("Connect")
|
||||||
|
|
||||||
self._status(self.hw_rev_data)
|
|
||||||
self.thermostat.start_watching()
|
|
||||||
else:
|
|
||||||
self.status_lbl.setText("Disconnected")
|
self.status_lbl.setText("Disconnected")
|
||||||
self.background_task_lbl.setText("Ready.")
|
self.background_task_lbl.setText("Ready.")
|
||||||
self.loading_spinner.hide()
|
self.loading_spinner.hide()
|
||||||
self.loading_spinner.stop()
|
self.loading_spinner.stop()
|
||||||
self.thermostat_ctrl_menu.fan_pwm_warning.setPixmap(QtGui.QPixmap())
|
self.thermostat_ctrl_menu.fan_pwm_warning.setPixmap(QtGui.QPixmap())
|
||||||
self.thermostat_ctrl_menu.fan_pwm_warning.setToolTip("")
|
self.thermostat_ctrl_menu.fan_pwm_warning.setToolTip("")
|
||||||
self.clear_graphs()
|
self.channel_graphs.clear_graphs()
|
||||||
self.report_box.setChecked(False)
|
self.report_box.setChecked(False)
|
||||||
if not Thermostat.connecting or Thermostat.connected:
|
|
||||||
for ch in range(self.NUM_CHANNELS):
|
for ch in range(self.NUM_CHANNELS):
|
||||||
if self.autotuners.get_state(ch) != PIDAutotuneState.STATE_OFF:
|
if self.autotuners.get_state(ch) != PIDAutotuneState.STATE_OFF:
|
||||||
|
if self.thermostat.connection_errored:
|
||||||
|
# Don't send any commands, just reset local state
|
||||||
|
self.autotuners.autotuners[ch].setOff()
|
||||||
|
else:
|
||||||
await self.autotuners.stop_pid_from_running(ch)
|
await self.autotuners.stop_pid_from_running(ch)
|
||||||
await self.thermostat.set_report_mode(False)
|
|
||||||
self.thermostat.stop_watching()
|
|
||||||
|
|
||||||
def _status(self, hw_rev_d: dict):
|
def _status(self, hw_rev_d: dict):
|
||||||
logging.debug(hw_rev_d)
|
|
||||||
self.status_lbl.setText(
|
self.status_lbl.setText(
|
||||||
f"Connected to Thermostat v{hw_rev_d['rev']['major']}.{hw_rev_d['rev']['minor']}"
|
f"Connected to Thermostat v{hw_rev_d['rev']['major']}.{hw_rev_d['rev']['minor']}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@pyqtSlot("QVariantMap")
|
|
||||||
def fan_update(self, fan_settings):
|
|
||||||
logging.debug(fan_settings)
|
|
||||||
if fan_settings is None:
|
|
||||||
return
|
|
||||||
with QSignalBlocker(self.thermostat_ctrl_menu.fan_power_slider):
|
|
||||||
self.thermostat_ctrl_menu.fan_power_slider.setValue(
|
|
||||||
fan_settings["fan_pwm"] or 100 # 0 = PWM off = full strength
|
|
||||||
)
|
|
||||||
with QSignalBlocker(self.thermostat_ctrl_menu.fan_auto_box):
|
|
||||||
self.thermostat_ctrl_menu.fan_auto_box.setChecked(fan_settings["auto_mode"])
|
|
||||||
if not self.hw_rev_data["settings"]["fan_pwm_recommended"]:
|
|
||||||
self.thermostat_ctrl_menu.set_fan_pwm_warning()
|
|
||||||
|
|
||||||
@asyncSlot(int)
|
@asyncSlot(int)
|
||||||
async def on_report_box_stateChanged(self, enabled):
|
async def on_report_box_stateChanged(self, enabled):
|
||||||
await self.thermostat.set_report_mode(enabled)
|
await self.thermostat.set_report_mode(enabled)
|
||||||
|
|
||||||
@asyncClose
|
@asyncClose
|
||||||
async def closeEvent(self, event):
|
async def closeEvent(self, _event):
|
||||||
try:
|
try:
|
||||||
await self.bail()
|
await self.thermostat.end_session()
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@asyncSlot()
|
@asyncSlot()
|
||||||
async def on_connect_btn_clicked(self):
|
async def on_connect_btn_clicked(self):
|
||||||
host, port = (
|
if (self._connecting_task is None) and (not self.thermostat.connected()):
|
||||||
self.conn_menu.host_set_line.text(),
|
self._connecting_task = asyncio.create_task(
|
||||||
self.conn_menu.port_set_spin.value(),
|
self.thermostat.start_session(
|
||||||
|
host=self.conn_menu.host_set_line.text(),
|
||||||
|
port=self.conn_menu.port_set_spin.value(),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
if not (self.thermostat.connecting() or self.thermostat.connected()):
|
await self._connecting_task
|
||||||
self.status_lbl.setText("Connecting...")
|
except (OSError, asyncio.CancelledError) as exc:
|
||||||
self.connect_btn.setText("Stop")
|
await self.thermostat.end_session()
|
||||||
self.conn_menu.host_set_line.setEnabled(False)
|
if isinstance(exc, asyncio.CancelledError):
|
||||||
self.conn_menu.port_set_spin.setEnabled(False)
|
|
||||||
|
|
||||||
try:
|
|
||||||
await self.thermostat.start_session(host=host, port=port)
|
|
||||||
except StoppedConnecting:
|
|
||||||
return
|
return
|
||||||
await self._on_connection_changed(True)
|
raise
|
||||||
|
finally:
|
||||||
|
self._connecting_task = None
|
||||||
|
|
||||||
|
elif self._connecting_task is not None:
|
||||||
|
self._connecting_task.cancel()
|
||||||
else:
|
else:
|
||||||
await self.bail()
|
|
||||||
|
|
||||||
# TODO: Remove asyncio.TimeoutError in Python 3.11
|
|
||||||
except (OSError, asyncio.TimeoutError):
|
|
||||||
try:
|
|
||||||
await self.bail()
|
|
||||||
except ConnectionResetError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@asyncSlot()
|
|
||||||
async def bail(self):
|
|
||||||
await self._on_connection_changed(False)
|
|
||||||
await self.thermostat.end_session()
|
await self.thermostat.end_session()
|
||||||
|
|
||||||
@asyncSlot(object, object)
|
@asyncSlot(int, PIDAutotuneState)
|
||||||
async def send_command(self, param, changes):
|
async def pid_autotune_handler(self, _ch, _state):
|
||||||
"""Translates parameter tree changes into thermostat set_param calls"""
|
|
||||||
ch = param.channel
|
|
||||||
|
|
||||||
for inner_param, change, data in changes:
|
|
||||||
if change == "value":
|
|
||||||
if inner_param.opts.get("param", None) is not None:
|
|
||||||
if inner_param.opts.get("suffix", None) == "mA":
|
|
||||||
data /= 1000 # Given in mA
|
|
||||||
|
|
||||||
thermostat_param = inner_param.opts["param"]
|
|
||||||
if thermostat_param[1] == "ch":
|
|
||||||
thermostat_param[1] = ch
|
|
||||||
|
|
||||||
if inner_param.name() == "Postfilter Rate" and data is None:
|
|
||||||
set_param_args = (*thermostat_param[:2], "off")
|
|
||||||
else:
|
|
||||||
set_param_args = (*thermostat_param, data)
|
|
||||||
param.child(*param.childPath(inner_param)).setOpts(lock=True)
|
|
||||||
await self.thermostat.set_param(*set_param_args)
|
|
||||||
param.child(*param.childPath(inner_param)).setOpts(lock=False)
|
|
||||||
|
|
||||||
if inner_param.opts.get("pid_autotune", None) is not None:
|
|
||||||
auto_tuner_param = inner_param.opts["pid_autotune"][0]
|
|
||||||
if inner_param.opts["pid_autotune"][1] != "ch":
|
|
||||||
ch = inner_param.opts["pid_autotune"][1]
|
|
||||||
self.autotuners.set_params(auto_tuner_param, ch, data)
|
|
||||||
|
|
||||||
if inner_param.opts.get("activaters", None) is not None:
|
|
||||||
activater = inner_param.opts["activaters"][
|
|
||||||
inner_param.reverse[0].index(
|
|
||||||
data
|
|
||||||
) # ListParameter.reverse = list of codename values
|
|
||||||
]
|
|
||||||
if activater is not None:
|
|
||||||
if activater[1] == "ch":
|
|
||||||
activater[1] = ch
|
|
||||||
await self.thermostat.set_param(*activater)
|
|
||||||
|
|
||||||
@asyncSlot()
|
|
||||||
async def pid_auto_tune_request(self, ch=0):
|
|
||||||
match self.autotuners.get_state(ch):
|
|
||||||
case PIDAutotuneState.STATE_OFF | PIDAutotuneState.STATE_FAILED:
|
|
||||||
self.autotuners.load_params_and_set_ready(ch)
|
|
||||||
|
|
||||||
case (
|
|
||||||
PIDAutotuneState.STATE_READY
|
|
||||||
| PIDAutotuneState.STATE_RELAY_STEP_UP
|
|
||||||
| PIDAutotuneState.STATE_RELAY_STEP_DOWN
|
|
||||||
):
|
|
||||||
await self.autotuners.stop_pid_from_running(ch)
|
|
||||||
# To Update the UI elements
|
|
||||||
self.pid_autotune_handler([])
|
|
||||||
|
|
||||||
@asyncSlot(list)
|
|
||||||
async def pid_autotune_handler(self, _):
|
|
||||||
ch_tuning = []
|
ch_tuning = []
|
||||||
for ch in range(self.NUM_CHANNELS):
|
for ch in range(self.NUM_CHANNELS):
|
||||||
match self.autotuners.get_state(ch):
|
match self.autotuners.get_state(ch):
|
||||||
case PIDAutotuneState.STATE_OFF:
|
case PIDAutotuneState.STATE_OFF:
|
||||||
self.ctrl_panel_view.change_params_title(
|
self.ctrl_panel_view.change_params_title(
|
||||||
ch, ("pid", "pid_autotune", "run_pid"), "Run"
|
ch, ("PID Config", "PID Auto Tune", "Run"), "Run"
|
||||||
)
|
)
|
||||||
case (
|
case (
|
||||||
PIDAutotuneState.STATE_READY
|
PIDAutotuneState.STATE_READY
|
||||||
@ -337,21 +230,21 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
| PIDAutotuneState.STATE_RELAY_STEP_DOWN
|
| PIDAutotuneState.STATE_RELAY_STEP_DOWN
|
||||||
):
|
):
|
||||||
self.ctrl_panel_view.change_params_title(
|
self.ctrl_panel_view.change_params_title(
|
||||||
ch, ("pid", "pid_autotune", "run_pid"), "Stop"
|
ch, ("PID Config", "PID Auto Tune", "Run"), "Stop"
|
||||||
)
|
)
|
||||||
ch_tuning.append(ch)
|
ch_tuning.append(ch)
|
||||||
|
|
||||||
case PIDAutotuneState.STATE_SUCCEEDED:
|
case PIDAutotuneState.STATE_SUCCEEDED:
|
||||||
self.info_box.display_info_box(
|
self.info_box.display_info_box(
|
||||||
"PID Autotune Success",
|
"PID Autotune Success",
|
||||||
f"Channel {ch} PID Settings has been loaded to Thermostat. Regulating temperature.",
|
f"Channel {ch} PID Config has been loaded to Thermostat. Regulating temperature.",
|
||||||
)
|
)
|
||||||
self.info_box.show()
|
self.info_box.show()
|
||||||
|
|
||||||
case PIDAutotuneState.STATE_FAILED:
|
case PIDAutotuneState.STATE_FAILED:
|
||||||
self.info_box.display_info_box(
|
self.info_box.display_info_box(
|
||||||
"PID Autotune Failed",
|
"PID Autotune Failed",
|
||||||
f"Channel {ch} PID Autotune on channel has failed.",
|
f"Channel {ch} PID Autotune has failed.",
|
||||||
)
|
)
|
||||||
self.info_box.show()
|
self.info_box.show()
|
||||||
|
|
||||||
@ -366,74 +259,6 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
self.loading_spinner.start()
|
self.loading_spinner.start()
|
||||||
self.loading_spinner.show()
|
self.loading_spinner.show()
|
||||||
|
|
||||||
@asyncSlot(int)
|
|
||||||
async def fan_set_request(self, value):
|
|
||||||
assert self.thermostat.connected()
|
|
||||||
|
|
||||||
if self.thermostat_ctrl_menu.fan_auto_box.isChecked():
|
|
||||||
with QSignalBlocker(self.thermostat_ctrl_menu.fan_auto_box):
|
|
||||||
self.thermostat_ctrl_menu.fan_auto_box.setChecked(False)
|
|
||||||
await self.thermostat.set_fan(value)
|
|
||||||
if not self.hw_rev_data["settings"]["fan_pwm_recommended"]:
|
|
||||||
self.thermostat_ctrl_menu.set_fan_pwm_warning()
|
|
||||||
|
|
||||||
@asyncSlot(int)
|
|
||||||
async def fan_auto_set_request(self, enabled):
|
|
||||||
assert self.thermostat.connected()
|
|
||||||
|
|
||||||
if enabled:
|
|
||||||
await self.thermostat.set_fan("auto")
|
|
||||||
self.fan_update(await self.thermostat.get_fan())
|
|
||||||
else:
|
|
||||||
await self.thermostat.set_fan(
|
|
||||||
self.thermostat_ctrl_menu.fan_power_slider.value()
|
|
||||||
)
|
|
||||||
|
|
||||||
@asyncSlot(int)
|
|
||||||
async def save_cfg_request(self, ch):
|
|
||||||
assert self.thermostat.connected()
|
|
||||||
|
|
||||||
await self.thermostat.save_cfg(str(ch))
|
|
||||||
|
|
||||||
@asyncSlot(int)
|
|
||||||
async def load_cfg_request(self, ch):
|
|
||||||
assert self.thermostat.connected()
|
|
||||||
|
|
||||||
await self.thermostat.load_cfg(str(ch))
|
|
||||||
|
|
||||||
@asyncSlot(bool)
|
|
||||||
async def dfu_request(self, _):
|
|
||||||
assert self.thermostat.connected()
|
|
||||||
|
|
||||||
await self._on_connection_changed(False)
|
|
||||||
await self.thermostat.dfu()
|
|
||||||
|
|
||||||
@asyncSlot(bool)
|
|
||||||
async def reset_request(self, _):
|
|
||||||
assert self.thermostat.connected()
|
|
||||||
|
|
||||||
await self._on_connection_changed(False)
|
|
||||||
await self.thermostat.reset()
|
|
||||||
await asyncio.sleep(0.1) # Wait for the reset to start
|
|
||||||
|
|
||||||
self.connect_btn.click() # Reconnect
|
|
||||||
|
|
||||||
@asyncSlot(bool)
|
|
||||||
async def net_settings_request(self, _):
|
|
||||||
assert self.thermostat.connected()
|
|
||||||
|
|
||||||
ipv4 = await self.thermostat.get_ipv4()
|
|
||||||
self.net_settings_input_diag = NetSettingsInputDiag(ipv4["addr"])
|
|
||||||
self.net_settings_input_diag.set_ipv4_act.connect(self.set_net_settings_request)
|
|
||||||
|
|
||||||
@asyncSlot(str)
|
|
||||||
async def set_net_settings_request(self, ipv4_settings):
|
|
||||||
assert self.thermostat.connected()
|
|
||||||
|
|
||||||
await self.thermostat.set_ipv4(ipv4_settings)
|
|
||||||
await self.thermostat.end_session()
|
|
||||||
await self._on_connection_changed(False)
|
|
||||||
|
|
||||||
|
|
||||||
async def coro_main():
|
async def coro_main():
|
||||||
args = get_argparser().parse_args()
|
args = get_argparser().parse_args()
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
use core::{cmp::max_by, marker::PhantomData};
|
use core::cmp::max_by;
|
||||||
use heapless::{consts::U2, Vec};
|
use heapless::{consts::U2, Vec};
|
||||||
use num_traits::Zero;
|
|
||||||
use serde::{Serialize, Serializer};
|
use serde::{Serialize, Serializer};
|
||||||
use smoltcp::time::Instant;
|
use smoltcp::time::Instant;
|
||||||
use stm32f4xx_hal::hal;
|
use stm32f4xx_hal::hal;
|
||||||
@ -33,24 +32,12 @@ pub enum PinsAdcReadTarget {
|
|||||||
pub const CHANNELS: usize = 2;
|
pub const CHANNELS: usize = 2;
|
||||||
pub const R_SENSE: f64 = 0.05;
|
pub const R_SENSE: f64 = 0.05;
|
||||||
|
|
||||||
// From design specs
|
// as stated in the MAX1968 datasheet
|
||||||
pub const MAX_TEC_I: ElectricCurrent = ElectricCurrent {
|
pub const MAX_TEC_I: f64 = 3.0;
|
||||||
dimension: PhantomData,
|
|
||||||
units: PhantomData,
|
|
||||||
value: 2.0,
|
|
||||||
};
|
|
||||||
pub const MAX_TEC_V: ElectricPotential = ElectricPotential {
|
|
||||||
dimension: PhantomData,
|
|
||||||
units: PhantomData,
|
|
||||||
value: 4.0,
|
|
||||||
};
|
|
||||||
|
|
||||||
// DAC chip outputs 0-5v, which is then passed through a resistor dividor to provide 0-3v range
|
// DAC chip outputs 0-5v, which is then passed through a resistor dividor to provide 0-3v range
|
||||||
const DAC_OUT_V_MAX: ElectricPotential = ElectricPotential {
|
const DAC_OUT_V_MAX: f64 = 3.0;
|
||||||
dimension: PhantomData,
|
|
||||||
units: PhantomData,
|
|
||||||
value: 3.0,
|
|
||||||
};
|
|
||||||
// TODO: -pub
|
// TODO: -pub
|
||||||
pub struct Channels {
|
pub struct Channels {
|
||||||
channel0: Channel<Channel0>,
|
channel0: Channel<Channel0>,
|
||||||
@ -141,7 +128,7 @@ impl Channels {
|
|||||||
|
|
||||||
/// i_set DAC
|
/// i_set DAC
|
||||||
fn set_dac(&mut self, channel: usize, voltage: ElectricPotential) -> ElectricPotential {
|
fn set_dac(&mut self, channel: usize, voltage: ElectricPotential) -> ElectricPotential {
|
||||||
let value = ((voltage / DAC_OUT_V_MAX).get::<ratio>() * (ad5680::MAX_VALUE as f64)) as u32 ;
|
let value = ((voltage / ElectricPotential::new::<volt>(DAC_OUT_V_MAX)).get::<ratio>() * (ad5680::MAX_VALUE as f64)) as u32 ;
|
||||||
match channel {
|
match channel {
|
||||||
0 => self.channel0.dac.set(value).unwrap(),
|
0 => self.channel0.dac.set(value).unwrap(),
|
||||||
1 => self.channel1.dac.set(value).unwrap(),
|
1 => self.channel1.dac.set(value).unwrap(),
|
||||||
@ -152,7 +139,11 @@ impl Channels {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_i(&mut self, channel: usize, i_set: ElectricCurrent) -> ElectricCurrent {
|
pub fn set_i(&mut self, channel: usize, i_set: ElectricCurrent) -> ElectricCurrent {
|
||||||
let i_set = i_set.min(MAX_TEC_I).max(-MAX_TEC_I);
|
// Silently clamp i_set
|
||||||
|
let i_ceiling = ElectricCurrent::new::<ampere>(MAX_TEC_I);
|
||||||
|
let i_floor = ElectricCurrent::new::<ampere>(-MAX_TEC_I);
|
||||||
|
let i_set = i_set.min(i_ceiling).max(i_floor);
|
||||||
|
|
||||||
let vref_meas = match channel.into() {
|
let vref_meas = match channel.into() {
|
||||||
0 => self.channel0.vref_meas,
|
0 => self.channel0.vref_meas,
|
||||||
1 => self.channel1.vref_meas,
|
1 => self.channel1.vref_meas,
|
||||||
@ -327,7 +318,7 @@ impl Channels {
|
|||||||
best_error = error;
|
best_error = error;
|
||||||
start_value = prev_value;
|
start_value = prev_value;
|
||||||
|
|
||||||
let vref = (value as f64 / ad5680::MAX_VALUE as f64) * DAC_OUT_V_MAX;
|
let vref = (value as f64 / ad5680::MAX_VALUE as f64) * ElectricPotential::new::<volt>(DAC_OUT_V_MAX);
|
||||||
match channel {
|
match channel {
|
||||||
0 => self.channel0.vref_meas = vref,
|
0 => self.channel0.vref_meas = vref,
|
||||||
1 => self.channel1.vref_meas = vref,
|
1 => self.channel1.vref_meas = vref,
|
||||||
@ -387,22 +378,22 @@ impl Channels {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_max_v(&mut self, channel: usize) -> (ElectricPotential, ElectricPotential) {
|
pub fn get_max_v(&mut self, channel: usize) -> ElectricPotential {
|
||||||
let max = 4.0 * ElectricPotential::new::<volt>(3.3);
|
let max = 4.0 * ElectricPotential::new::<volt>(3.3);
|
||||||
let duty = self.get_pwm(channel, PwmPin::MaxV);
|
let duty = self.get_pwm(channel, PwmPin::MaxV);
|
||||||
(duty * max, MAX_TEC_V)
|
duty * max
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_max_i_pos(&mut self, channel: usize) -> (ElectricCurrent, ElectricCurrent) {
|
pub fn get_max_i_pos(&mut self, channel: usize) -> (ElectricCurrent, ElectricCurrent) {
|
||||||
let max = ElectricCurrent::new::<ampere>(3.0);
|
let max = ElectricCurrent::new::<ampere>(3.0);
|
||||||
let duty = self.get_pwm(channel, PwmPin::MaxIPos);
|
let duty = self.get_pwm(channel, PwmPin::MaxIPos);
|
||||||
(duty * max, MAX_TEC_I)
|
(duty * max, max)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_max_i_neg(&mut self, channel: usize) -> (ElectricCurrent, ElectricCurrent) {
|
pub fn get_max_i_neg(&mut self, channel: usize) -> (ElectricCurrent, ElectricCurrent) {
|
||||||
let max = ElectricCurrent::new::<ampere>(3.0);
|
let max = ElectricCurrent::new::<ampere>(3.0);
|
||||||
let duty = self.get_pwm(channel, PwmPin::MaxINeg);
|
let duty = self.get_pwm(channel, PwmPin::MaxINeg);
|
||||||
(duty * max, MAX_TEC_I)
|
(duty * max, max)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current passing through TEC
|
// Get current passing through TEC
|
||||||
@ -444,21 +435,21 @@ impl Channels {
|
|||||||
|
|
||||||
pub fn set_max_v(&mut self, channel: usize, max_v: ElectricPotential) -> (ElectricPotential, ElectricPotential) {
|
pub fn set_max_v(&mut self, channel: usize, max_v: ElectricPotential) -> (ElectricPotential, ElectricPotential) {
|
||||||
let max = 4.0 * ElectricPotential::new::<volt>(3.3);
|
let max = 4.0 * ElectricPotential::new::<volt>(3.3);
|
||||||
let duty = (max_v.min(MAX_TEC_V).max(ElectricPotential::zero()) / max).get::<ratio>();
|
let duty = (max_v / max).get::<ratio>();
|
||||||
let duty = self.set_pwm(channel, PwmPin::MaxV, duty);
|
let duty = self.set_pwm(channel, PwmPin::MaxV, duty);
|
||||||
(duty * max, max)
|
(duty * max, max)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_max_i_pos(&mut self, channel: usize, max_i_pos: ElectricCurrent) -> (ElectricCurrent, ElectricCurrent) {
|
pub fn set_max_i_pos(&mut self, channel: usize, max_i_pos: ElectricCurrent) -> (ElectricCurrent, ElectricCurrent) {
|
||||||
let max = ElectricCurrent::new::<ampere>(3.0);
|
let max = ElectricCurrent::new::<ampere>(3.0);
|
||||||
let duty = (max_i_pos.min(MAX_TEC_I).max(ElectricCurrent::zero()) / max).get::<ratio>();
|
let duty = (max_i_pos / max).get::<ratio>();
|
||||||
let duty = self.set_pwm(channel, PwmPin::MaxIPos, duty);
|
let duty = self.set_pwm(channel, PwmPin::MaxIPos, duty);
|
||||||
(duty * max, max)
|
(duty * max, max)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_max_i_neg(&mut self, channel: usize, max_i_neg: ElectricCurrent) -> (ElectricCurrent, ElectricCurrent) {
|
pub fn set_max_i_neg(&mut self, channel: usize, max_i_neg: ElectricCurrent) -> (ElectricCurrent, ElectricCurrent) {
|
||||||
let max = ElectricCurrent::new::<ampere>(3.0);
|
let max = ElectricCurrent::new::<ampere>(3.0);
|
||||||
let duty = (max_i_neg.min(MAX_TEC_I).max(ElectricCurrent::zero()) / max).get::<ratio>();
|
let duty = (max_i_neg / max).get::<ratio>();
|
||||||
let duty = self.set_pwm(channel, PwmPin::MaxINeg, duty);
|
let duty = self.set_pwm(channel, PwmPin::MaxINeg, duty);
|
||||||
(duty * max, max)
|
(duty * max, max)
|
||||||
}
|
}
|
||||||
@ -518,8 +509,8 @@ impl Channels {
|
|||||||
PwmSummary {
|
PwmSummary {
|
||||||
channel,
|
channel,
|
||||||
center: CenterPointJson(self.channel_state(channel).center.clone()),
|
center: CenterPointJson(self.channel_state(channel).center.clone()),
|
||||||
i_set: (self.get_i(channel), MAX_TEC_I).into(),
|
i_set: (self.get_i(channel), ElectricCurrent::new::<ampere>(3.0)).into(),
|
||||||
max_v: self.get_max_v(channel).into(),
|
max_v: (self.get_max_v(channel), ElectricPotential::new::<volt>(5.0)).into(),
|
||||||
max_i_pos: self.get_max_i_pos(channel).into(),
|
max_i_pos: self.get_max_i_pos(channel).into(),
|
||||||
max_i_neg: self.get_max_i_neg(channel).into(),
|
max_i_neg: self.get_max_i_neg(channel).into(),
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
use num_traits::Zero;
|
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
use uom::si::{
|
use uom::si::{
|
||||||
electric_potential::volt,
|
electric_potential::volt,
|
||||||
@ -19,7 +18,6 @@ pub struct ChannelConfig {
|
|||||||
pid: pid::Parameters,
|
pid: pid::Parameters,
|
||||||
pid_target: f32,
|
pid_target: f32,
|
||||||
pid_engaged: bool,
|
pid_engaged: bool,
|
||||||
i_set: ElectricCurrent,
|
|
||||||
sh: steinhart_hart::Parameters,
|
sh: steinhart_hart::Parameters,
|
||||||
pwm: PwmLimits,
|
pwm: PwmLimits,
|
||||||
/// uses variant `PostFilter::Invalid` instead of `None` to save space
|
/// uses variant `PostFilter::Invalid` instead of `None` to save space
|
||||||
@ -35,17 +33,11 @@ impl ChannelConfig {
|
|||||||
.unwrap_or(PostFilter::Invalid);
|
.unwrap_or(PostFilter::Invalid);
|
||||||
|
|
||||||
let state = channels.channel_state(channel);
|
let state = channels.channel_state(channel);
|
||||||
let i_set = if state.pid_engaged {
|
|
||||||
ElectricCurrent::zero()
|
|
||||||
} else {
|
|
||||||
state.i_set
|
|
||||||
};
|
|
||||||
ChannelConfig {
|
ChannelConfig {
|
||||||
center: state.center.clone(),
|
center: state.center.clone(),
|
||||||
pid: state.pid.parameters.clone(),
|
pid: state.pid.parameters.clone(),
|
||||||
pid_target: state.pid.target as f32,
|
pid_target: state.pid.target as f32,
|
||||||
pid_engaged: state.pid_engaged,
|
pid_engaged: state.pid_engaged,
|
||||||
i_set: i_set,
|
|
||||||
sh: state.sh.clone(),
|
sh: state.sh.clone(),
|
||||||
pwm,
|
pwm,
|
||||||
adc_postfilter,
|
adc_postfilter,
|
||||||
@ -67,7 +59,6 @@ impl ChannelConfig {
|
|||||||
adc_postfilter => Some(adc_postfilter),
|
adc_postfilter => Some(adc_postfilter),
|
||||||
};
|
};
|
||||||
let _ = channels.adc.set_postfilter(channel as u8, adc_postfilter);
|
let _ = channels.adc.set_postfilter(channel as u8, adc_postfilter);
|
||||||
let _ = channels.set_i(channel, self.i_set);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,7 +71,7 @@ struct PwmLimits {
|
|||||||
|
|
||||||
impl PwmLimits {
|
impl PwmLimits {
|
||||||
pub fn new(channels: &mut Channels, channel: usize) -> Self {
|
pub fn new(channels: &mut Channels, channel: usize) -> Self {
|
||||||
let (max_v, _) = channels.get_max_v(channel);
|
let max_v = channels.get_max_v(channel);
|
||||||
let (max_i_pos, _) = channels.get_max_i_pos(channel);
|
let (max_i_pos, _) = channels.get_max_i_pos(channel);
|
||||||
let (max_i_neg, _) = channels.get_max_i_neg(channel);
|
let (max_i_neg, _) = channels.get_max_i_neg(channel);
|
||||||
PwmLimits {
|
PwmLimits {
|
||||||
|
@ -54,7 +54,7 @@ impl FanCtrl {
|
|||||||
pub fn cycle(&mut self, abs_max_tec_i: ElectricCurrent) {
|
pub fn cycle(&mut self, abs_max_tec_i: ElectricCurrent) {
|
||||||
self.abs_max_tec_i = abs_max_tec_i.get::<ampere>() as f32;
|
self.abs_max_tec_i = abs_max_tec_i.get::<ampere>() as f32;
|
||||||
if self.fan_auto && self.hw_settings.fan_available {
|
if self.fan_auto && self.hw_settings.fan_available {
|
||||||
let scaled_current = self.abs_max_tec_i / MAX_TEC_I.get::<ampere>() as f32;
|
let scaled_current = self.abs_max_tec_i / MAX_TEC_I as f32;
|
||||||
// do not limit upper bound, as it will be limited in the set_pwm()
|
// do not limit upper bound, as it will be limited in the set_pwm()
|
||||||
let pwm = (MAX_USER_FAN_PWM * (scaled_current * (scaled_current * self.k_a + self.k_b) + self.k_c)) as u32;
|
let pwm = (MAX_USER_FAN_PWM * (scaled_current * (scaled_current * self.k_a + self.k_b) + self.k_c)) as u32;
|
||||||
self.set_pwm(pwm);
|
self.set_pwm(pwm);
|
||||||
|
@ -54,13 +54,15 @@ impl Controller {
|
|||||||
// + x0 * (kp + ki + kd)
|
// + x0 * (kp + ki + kd)
|
||||||
// - x1 * (kp + 2kd)
|
// - x1 * (kp + 2kd)
|
||||||
// + x2 * kd
|
// + x2 * kd
|
||||||
|
// + kp * (u0 - u1)
|
||||||
// y0 = clip(y0', ymin, ymax)
|
// y0 = clip(y0', ymin, ymax)
|
||||||
pub fn update(&mut self, input: f64) -> f64 {
|
pub fn update(&mut self, input: f64) -> f64 {
|
||||||
|
|
||||||
let mut output: f64 = self.y1 - self.target * f64::from(self.parameters.ki)
|
let mut output: f64 = self.y1 - self.target * f64::from(self.parameters.ki)
|
||||||
+ input * f64::from(self.parameters.kp + self.parameters.ki + self.parameters.kd)
|
+ input * f64::from(self.parameters.kp + self.parameters.ki + self.parameters.kd)
|
||||||
- self.x1 * f64::from(self.parameters.kp + 2.0 * self.parameters.kd)
|
- self.x1 * f64::from(self.parameters.kp + 2.0 * self.parameters.kd)
|
||||||
+ self.x2 * f64::from(self.parameters.kd);
|
+ self.x2 * f64::from(self.parameters.kd)
|
||||||
|
+ f64::from(self.parameters.kp) * (self.target - self.u1);
|
||||||
if output < self.parameters.output_min.into() {
|
if output < self.parameters.output_min.into() {
|
||||||
output = self.parameters.output_min.into();
|
output = self.parameters.output_min.into();
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user