Compare commits

..

6 Commits

Author SHA1 Message Date
d38864740b flake: Add pytec runnables to devShell
Makes it possible to directly run `plot` and `autotune` in the devShell.
2024-11-18 11:12:33 +08:00
6ec05808ce flake: Introduce pytec to PYTHONPATH in devShell
For easier testing of pytec client code in the shell.
2024-11-18 10:40:54 +08:00
9af86be674 pytec: Remove artificial report mode in client
Encourage polling usage instead, as shown in example.
2024-11-16 13:11:59 +08:00
eabc7f6a12 flake: Register the pytec Python package 2024-11-11 17:11:37 +08:00
52e35d2a98 flake: Format with nixfmt-rfc-style
Also set up formatter so that `nix fmt` formats.
2024-11-04 18:38:08 +08:00
f1da910c11 pytec: Complete client
Expose all available Thermostat commands to the pytec client, and add
disconnect functionality.
2024-11-04 18:03:50 +08:00
3 changed files with 100 additions and 34 deletions

View File

@ -7,9 +7,17 @@
inputs.nixpkgs.follows = "nixpkgs";
};
outputs = { self, nixpkgs, rust-overlay }:
outputs =
{
self,
nixpkgs,
rust-overlay,
}:
let
pkgs = import nixpkgs { system = "x86_64-linux"; overlays = [ (import rust-overlay) ]; };
pkgs = import nixpkgs {
system = "x86_64-linux";
overlays = [ (import rust-overlay) ];
};
rust = pkgs.rust-bin.stable."1.66.0".default.override {
extensions = [ "rust-src" ];
@ -50,6 +58,17 @@
auditable = false;
};
pytec = pkgs.python3Packages.buildPythonPackage {
pname = "pytec";
version = "0.0.0";
src = "${self}/pytec";
propagatedBuildInputs = with pkgs.python3Packages; [
numpy
matplotlib
];
};
pytec-dev-wrappers = pkgs.runCommandNoCC "pytec-dev-wrappers" { } ''
mkdir -p $out/bin
for program in ${self}/pytec/*.py; do
@ -62,9 +81,10 @@
fi
done
'';
in {
in
{
packages.x86_64-linux = {
inherit thermostat;
inherit thermostat pytec;
default = thermostat;
};
@ -74,16 +94,25 @@
devShells.x86_64-linux.default = pkgs.mkShellNoCC {
name = "thermostat-dev-shell";
packages = with pkgs; [
rust llvm
openocd dfu-util rlwrap
packages =
with pkgs;
[
rust
llvm
openocd
dfu-util
rlwrap
pytec-dev-wrappers
] ++ (with python3Packages; [
numpy matplotlib
]
++ (with python3Packages; [
numpy
matplotlib
]);
shellHook = ''
export PYTHONPATH=`pwd`/pytec:$PYTHONPATH
export PYTHONPATH=`git rev-parse --show-toplevel`/pytec:$PYTHONPATH
'';
};
formatter.x86_64-linux = nixpkgs.legacyPackages.x86_64-linux.nixfmt-rfc-style;
};
}

View File

@ -1,3 +1,4 @@
import time
from pytec.client import Client
tec = Client() #(host="localhost", port=6667)
@ -7,5 +8,6 @@ print(tec.get_pid())
print(tec.get_output())
print(tec.get_postfilter())
print(tec.get_b_parameter())
for data in tec.report_mode():
print(data)
while True:
print(tec.get_report())
time.sleep(0.05)

View File

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