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
5 changed files with 115 additions and 32 deletions

View File

@ -2,14 +2,22 @@
description = "Firmware for the Sinara 8451 Thermostat"; description = "Firmware for the Sinara 8451 Thermostat";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05"; inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
inputs.rust-overlay = { inputs.rust-overlay = {
url = "github:oxalica/rust-overlay"; url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
outputs = { self, nixpkgs, rust-overlay }: outputs =
{
self,
nixpkgs,
rust-overlay,
}:
let let
pkgs = import nixpkgs { system = "x86_64-linux"; overlays = [ (import rust-overlay) ]; }; pkgs = import nixpkgs {
system = "x86_64-linux";
overlays = [ (import rust-overlay) ];
};
rust = pkgs.rust-bin.stable."1.66.0".default.override { rust = pkgs.rust-bin.stable."1.66.0".default.override {
extensions = [ "rust-src" ]; extensions = [ "rust-src" ];
@ -25,7 +33,7 @@
version = "0.0.0"; version = "0.0.0";
src = self; src = self;
cargoLock = { cargoLock = {
lockFile = ./Cargo.lock; lockFile = ./Cargo.lock;
outputHashes = { outputHashes = {
"stm32-eth-0.2.0" = "sha256-48RpZgagUqgVeKm7GXdk3Oo0v19ScF9Uby0nTFlve2o="; "stm32-eth-0.2.0" = "sha256-48RpZgagUqgVeKm7GXdk3Oo0v19ScF9Uby0nTFlve2o=";
@ -49,9 +57,34 @@
dontFixup = true; dontFixup = true;
auditable = false; auditable = false;
}; };
in {
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
if [ -x $program ]; then
progname=`basename -s .py $program`
outname=$out/bin/$progname
echo "#!${pkgs.bash}/bin/bash" >> $outname
echo "exec python3 -m $progname \"\$@\"" >> $outname
chmod 755 $outname
fi
done
'';
in
{
packages.x86_64-linux = { packages.x86_64-linux = {
inherit thermostat; inherit thermostat pytec;
default = thermostat; default = thermostat;
}; };
@ -61,12 +94,25 @@
devShells.x86_64-linux.default = pkgs.mkShellNoCC { devShells.x86_64-linux.default = pkgs.mkShellNoCC {
name = "thermostat-dev-shell"; name = "thermostat-dev-shell";
packages = with pkgs; [ packages =
rust llvm with pkgs;
openocd dfu-util rlwrap [
] ++ (with python3Packages; [ rust
numpy matplotlib llvm
openocd
dfu-util
rlwrap
pytec-dev-wrappers
]
++ (with python3Packages; [
numpy
matplotlib
]); ]);
shellHook = ''
export PYTHONPATH=`git rev-parse --show-toplevel`/pytec:$PYTHONPATH
'';
}; };
formatter.x86_64-linux = nixpkgs.legacyPackages.x86_64-linux.nixfmt-rfc-style;
}; };
} }

0
pytec/autotune.py Normal file → Executable file
View File

View File

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

0
pytec/plot.py Normal file → Executable file
View File

View File

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