Compare commits

...

22 Commits

Author SHA1 Message Date
8db4867ebf PyThermostat: Improve pyproject metadata 2024-11-25 13:38:04 +08:00
130bde480e PyThermostat: Replace setup.py with pyproject.toml 2024-11-25 13:14:54 +08:00
36d80ebdff PyThermostat: Add entry points for runnables
Forms a more convienient interface.
2024-11-25 10:07:30 +08:00
09300b5d44 PyThermostat: Add main function to plot.py 2024-11-25 10:07:30 +08:00
9743dca775 PyThermostat: Move scripts into subfolder
As Thermostat Python scripts are not single-file Python modules and
should be packaged inside PyThermostat.
2024-11-25 10:07:20 +08:00
11131deda2 README: Add PID Output Clamping section
Explains the need of having separate "max_i_pos/output_max" and
"max_i_neg/output_min" values; They serve different purposes.
2024-11-20 08:02:07 +08:00
764774fbce PyThermostat: Remove report mode in autotune.py 2024-11-18 17:47:33 +08:00
4beeec6021 PyThermostat: Remove all references to Pytec 2024-11-18 17:34:39 +08:00
6b8a5f5bb8 Rename the Pytec library to PyThermostat
Pytec is a misnomer, as the Thermostat is not limited to just
controlling TEC modules. The library also interfaces with and controls
the Thermostat itself, and not the TEC module directly.

See M-Labs/thermostat#149 (comment)
2024-11-18 16:22:57 +08:00
8dd58b364d README: Fix limits section 2024-11-18 14:01:51 +08:00
ae0d593139 pytec: Stop using client report mode in plot.py
Report mode has been removed from the client, stop using it.
2024-11-18 13:57:54 +08:00
adc25c9b2a pytec: Add hardware testing script
Eases the process of testing the hardware.

See #143.
2024-11-18 10:31:56 +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
9848c65de5 Fix command parser failing test due to changes 2024-10-21 15:51:21 +08:00
069d791802 Rename all Steinhart-Hart references to B-param
The Steinhart-Hart equation was changed in code long ago to the
B-parameter equation. Rename references to it and the interface
accordingly. The `s-h` command is now `b-p`.

The reason the name "B-Parameter" equation was chosen over
"Beta-Parameter" was due to its easier searchability.
2024-10-21 15:44:46 +08:00
bd9ae997ae PwmLimits: Use uom quantities for fields not f64s 2024-10-21 11:13:19 +08:00
45eb55d36d steinhart_hart: Beta Parameter uom dimensions
The Beta Parameter is not dimensionless, and has unit kelvin.
Incorporate that into the type system.
2024-10-21 11:12:47 +08:00
eddf05cae7 channels: get_i -> get_i_set 2024-10-21 11:12:16 +08:00
f68ae12c8d TCP command: Rename pwm to output
Current limit pins are driven by PWM inputs to the MAX1968 driver, but
this is an implementation detail, and should not be exposed in the form
of the command interface. Rename "pwm" to "output" in all instances.

See M-Labs/thermostat#62 (comment).
2024-10-21 10:09:24 +08:00
20 changed files with 578 additions and 398 deletions

View File

@ -93,15 +93,15 @@ Send commands as simple text string terminated by `\n`. Responses are
formatted as line-delimited JSON.
| Syntax | Function |
|------------------------------------------- |-------------------------------------------------------------------------------|
|-------------------------------------------|-------------------------------------------------------------------------------|
| `report` | Show latest report of channel parameters (see *Reports* section) |
| `pwm` | Show current PWM settings |
| `pwm <0/1> max_i_pos <amp>` | Set maximum positive output current, clamped to [0, 2] |
| `pwm <0/1> max_i_neg <amp>` | Set maximum negative output current, clamped to [0, 2] |
| `pwm <0/1> max_v <volt>` | Set maximum output voltage, clamped to [0, 4] |
| `pwm <0/1> i_set <amp>` | Disengage PID, set fixed output current, clamped to [-2, 2] |
| `pwm <0/1> polarity <normal/reversed>` | Set output current polarity, with 'normal' being the front panel polarity |
| `pwm <0/1> pid` | Let output current to be controlled by the PID |
| `output` | Show current output settings |
| `output <0/1> max_i_pos <amp>` | Set maximum positive output current, clamped to [0, 2] |
| `output <0/1> max_i_neg <amp>` | Set maximum negative output current, clamped to [0, 2] |
| `output <0/1> max_v <volt>` | Set maximum output voltage, clamped to [0, 4] |
| `output <0/1> i_set <amp>` | Disengage PID, set fixed output current, clamped to [-2, 2] |
| `output <0/1> polarity <normal/reversed>` | Set output current polarity, with 'normal' being the front panel polarity |
| `output <0/1> pid` | Let output current to be controlled by the PID |
| `center <0/1> <volt>` | Set the MAX1968 0A-centerpoint to the specified fixed voltage |
| `center <0/1> vref` | Set the MAX1968 0A-centerpoint to measure from VREF |
| `pid` | Show PID configuration |
@ -111,8 +111,8 @@ formatted as line-delimited JSON.
| `pid <0/1> kd <value>` | Set differential gain |
| `pid <0/1> output_min <amp>` | Set mininum output |
| `pid <0/1> output_max <amp>` | Set maximum output |
| `s-h` | Show Steinhart-Hart equation parameters |
| `s-h <0/1> <t0/b/r0> <value>` | Set Steinhart-Hart parameter for a channel |
| `b-p` | Show B-Parameter equation parameters |
| `b-p <0/1> <t0/b/r0> <value>` | Set B-Parameter for a channel |
| `postfilter` | Show postfilter settings |
| `postfilter <0/1> off` | Disable postfilter |
| `postfilter <0/1> rate <rate>` | Set postfilter output data rate |
@ -144,22 +144,22 @@ output will be truncated when USB buffers are full.
Connect the thermistor with the SENS pins of the
device. Temperature-depending resistance is measured by the AD7172
ADC. To prepare conversion to a temperature, set the Beta parameters
for the Steinhart-Hart equation.
ADC. To prepare conversion to a temperature, set the parameters
for the B-Parameter equation.
Set the base temperature in degrees celsius for the channel 0 thermistor:
```
s-h 0 t0 20
b-p 0 t0 20
```
Set the resistance in Ohms measured at the base temperature t0:
```
s-h 0 r0 10000
b-p 0 r0 10000
```
Set the Beta parameter:
```
s-h 0 b 3800
b-p 0 b 3800
```
### 50/60 Hz filtering
@ -183,48 +183,47 @@ postfilter rate can be tuned with the `postfilter` command.
When using a TEC module with the Thermostat, the Thermostat expects the thermal load (where the thermistor is located) to cool down with a positive software current set point, and heat up with a negative current set point.
If the Thermostat is used for temperature control with the Sinara 5432 DAC "Zotino", and is connected via an IDC cable, the TEC polarity may need to be reversed with the `pwm <ch> polarity reversed` TCP command.
If the Thermostat is used for temperature control with the Sinara 5432 DAC "Zotino", and is connected via an IDC cable, the TEC polarity may need to be reversed with the `output <ch> polarity reversed` TCP command.
Testing heat flow direction with a low set current is recommended before installation of the TEC module.
### Limits
Each MAX1968 TEC driver has analog/PWM inputs for setting
Each channel has maximum value settings, for setting
output limits.
Use the `pwm` command to see current settings and maximum values.
Use the `output` command to see them.
| Limit | Unit | Description |
| --- | :---: | --- |
| `max_v` | Volts | Maximum voltage |
| `max_i_pos` | Amperes | Maximum positive current |
| `max_i_neg` | Amperes | Maximum negative current |
| `i_set` | Amperes | (Not a limit; Open-loop mode) |
Example: set the maximum voltage of channel 0 to 1.5 V.
```
pwm 0 max_v 1.5
output 0 max_v 1.5
```
Example: set the maximum negative current of channel 0 to -3 A.
Example: set the maximum negative current of channel 0 to -2 A.
```
pwm 0 max_i_neg 3
output 0 max_i_neg 2
```
Example: set the maximum positive current of channel 1 to 3 A.
Example: set the maximum positive current of channel 1 to 2 A.
```
pwm 0 max_i_pos 3
output 1 max_i_pos 2
```
### Open-loop mode
To manually control TEC output current, set a fixed output current with
the `pwm` command. Doing so will disengage the PID control for that
the `output` command. Doing so will disengage the PID control for that
channel.
Example: set output current of channel 0 to 0 A.
```
pwm 0 i_set 0
output 0 i_set 0
```
## PID-stabilized temperature control
@ -237,7 +236,23 @@ pid 0 target 20
Enter closed-loop mode by switching control of the TEC output current
of channel 0 to the PID algorithm:
```
pwm 0 pid
output 0 pid
```
### PID output clamping
It is possible to clamp the PID algorithm output independently of channel output limits. This is desirable when e.g. there is a need to keep the current value above a certain threshold in closed-loop mode.
Note that the actual output will still ultimately be limited by the `max_i_pos` and `max_i_neg` values.
Set PID maximum output of channel 0 to 1.5 A.
```
pid 0 output_max 1.5
```
Set PID minimum output of channel 0 to 0.1 A.
```
pid 0 output_min 0.1
```
## LED indicators
@ -260,7 +275,7 @@ with the following keys.
| `interval` | Seconds | Time elapsed since last report update on channel |
| `adc` | Volts | AD7172 input |
| `sens` | Ohms | Thermistor resistance derived from `adc` |
| `temperature` | Degrees Celsius | Steinhart-Hart conversion result derived from `sens` |
| `temperature` | Degrees Celsius | B-Parameter conversion result derived from `sens` |
| `pid_engaged` | Boolean | `true` if in closed-loop mode |
| `i_set` | Amperes | TEC output current |
| `dac_value` | Volts | AD5680 output derived from `i_set` |

View File

@ -13,7 +13,7 @@ When tuning Thermostat PID parameters, it is helpful to view the temperature, PI
To use the Python real-time plotting utility, run
```shell
python pytec/plot.py
python pythermostat/pythermostat/plot.py
```
![default view](./assets/default%20view.png)
@ -44,12 +44,12 @@ Below are some general guidelines for manually tuning PID loops. Note that every
## Auto Tuning
A PID auto tuning utility is provided in the Pytec library. The auto tuning utility drives the the load to a controlled oscillation, observes the ultimate gain and oscillation period and calculates a set of PID parameters.
A PID auto tuning utility is provided in the PyThermostat library. The auto tuning utility drives the the load to a controlled oscillation, observes the ultimate gain and oscillation period and calculates a set of PID parameters.
To run the auto tuning utility, run
```shell
python pytec/autotune.py
python pythermostat/pythermostat/autotune.py
```
After some time, the auto tuning utility will output the auto tuning results, below is a sample output

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" ];
@ -49,9 +57,23 @@
dontFixup = true;
auditable = false;
};
in {
pythermostat = pkgs.python3Packages.buildPythonPackage {
pname = "pythermostat";
version = "0.0.0";
format = "pyproject";
src = "${self}/pythermostat";
propagatedBuildInputs =
with pkgs.python3Packages; [
numpy
matplotlib
];
};
in
{
packages.x86_64-linux = {
inherit thermostat;
inherit thermostat pythermostat;
default = thermostat;
};
@ -61,12 +83,21 @@
devShells.x86_64-linux.default = pkgs.mkShellNoCC {
name = "thermostat-dev-shell";
packages = with pkgs; [
rust llvm
openocd dfu-util rlwrap
] ++ (with python3Packages; [
numpy matplotlib
packages =
with pkgs;
[
rust
llvm
openocd
dfu-util
rlwrap
]
++ (with python3Packages; [
numpy
matplotlib
]);
};
formatter.x86_64-linux = nixpkgs.legacyPackages.x86_64-linux.nixfmt-rfc-style;
};
}

View File

@ -1,11 +0,0 @@
from pytec.client import Client
tec = Client() #(host="localhost", port=6667)
tec.set_param("s-h", 1, "t0", 20)
print(tec.get_pwm())
print(tec.get_pid())
print(tec.get_pwm())
print(tec.get_postfilter())
print(tec.get_steinhart_hart())
for data in tec.report_mode():
print(data)

View File

@ -1,128 +0,0 @@
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from threading import Thread, Lock
from pytec.client import Client
TIME_WINDOW = 300.0
tec = Client()
target_temperature = tec.get_pid()[0]['target']
print("Channel 0 target temperature: {:.3f}".format(target_temperature))
class Series:
def __init__(self, conv=lambda x: x):
self.conv = conv
self.x_data = []
self.y_data = []
def append(self, x, y):
self.x_data.append(x)
self.y_data.append(self.conv(y))
def clip(self, min_x):
drop = 0
while drop < len(self.x_data) and self.x_data[drop] < min_x:
drop += 1
self.x_data = self.x_data[drop:]
self.y_data = self.y_data[drop:]
series = {
# 'adc': Series(),
# 'sens': Series(lambda x: x * 0.0001),
'temperature': Series(),
# 'i_set': Series(),
'pid_output': Series(),
# 'vref': Series(),
# 'dac_value': Series(),
# 'dac_feedback': Series(),
# 'i_tec': Series(),
'tec_i': Series(),
'tec_u_meas': Series(),
# 'interval': Series(),
}
series_lock = Lock()
quit = False
def recv_data(tec):
global last_packet_time
for data in tec.report_mode():
ch0 = data[0]
series_lock.acquire()
try:
for k, s in series.items():
if k in ch0:
v = ch0[k]
if type(v) is float:
s.append(ch0['time'], v)
finally:
series_lock.release()
if quit:
break
thread = Thread(target=recv_data, args=(tec,))
thread.start()
fig, ax = plt.subplots()
for k, s in series.items():
s.plot, = ax.plot([], [], label=k)
legend = ax.legend()
def animate(i):
min_x, max_x, min_y, max_y = None, None, None, None
series_lock.acquire()
try:
for k, s in series.items():
s.plot.set_data(s.x_data, s.y_data)
if len(s.y_data) > 0:
s.plot.set_label("{}: {:.3f}".format(k, s.y_data[-1]))
if len(s.x_data) > 0:
min_x_ = min(s.x_data)
if min_x is None:
min_x = min_x_
else:
min_x = min(min_x, min_x_)
max_x_ = max(s.x_data)
if max_x is None:
max_x = max_x_
else:
max_x = max(max_x, max_x_)
if len(s.y_data) > 0:
min_y_ = min(s.y_data)
if min_y is None:
min_y = min_y_
else:
min_y = min(min_y, min_y_)
max_y_ = max(s.y_data)
if max_y is None:
max_y = max_y_
else:
max_y = max(max_y, max_y_)
if min_x and max_x - TIME_WINDOW > min_x:
for s in series.values():
s.clip(max_x - TIME_WINDOW)
finally:
series_lock.release()
if min_x != max_x:
ax.set_xlim(min_x, max_x)
if min_y != max_y:
margin_y = 0.01 * (max_y - min_y)
ax.set_ylim(min_y - margin_y, max_y + margin_y)
global legend
legend.remove()
legend = ax.legend()
ani = animation.FuncAnimation(
fig, animate, interval=1, blit=False, save_count=50)
plt.show()
quit = True
thread.join()

View File

@ -1,12 +0,0 @@
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(),
)

13
pythermostat/example.py Normal file
View File

@ -0,0 +1,13 @@
import time
from pythermostat.client import Client
tec = Client() #(host="localhost", port=6667)
tec.set_param("b-p", 1, "t0", 20)
print(tec.get_output())
print(tec.get_pid())
print(tec.get_output())
print(tec.get_postfilter())
print(tec.get_b_parameter())
while True:
print(tec.get_report())
time.sleep(0.05)

View File

@ -0,0 +1,18 @@
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[project]
name = "pythermostat"
version = "0.0"
authors = [{name = "M-Labs"}]
description = "Python utilities for the Sinara 8451 Thermostat"
urls.Repository = "https://git.m-labs.hk/M-Labs/thermostat"
license = {text = "GPLv3"}
[project.gui-scripts]
thermostat_plot = "pythermostat.plot:main"
[project.scripts]
thermostat_autotune = "pythermostat.autotune:main"
thermostat_test = "pythermostat.test:main"

View File

@ -1,9 +1,10 @@
import math
import logging
import time
from collections import deque, namedtuple
from enum import Enum
from pytec.client import Client
from pythermostat.client import Client
# Based on hirshmann pid-autotune libiary
# See https://github.com/hirschmann/pid-autotune
@ -236,13 +237,14 @@ def main():
tec = Client()
data = next(tec.report_mode())
data = tec.get_report()
ch = data[channel]
tuner = PIDAutotune(target_temperature, output_step,
lookback, noiseband, ch['interval'])
for data in tec.report_mode():
while True:
data = tec.get_report()
ch = data[channel]
@ -253,9 +255,11 @@ def main():
tuner_out = tuner.output()
tec.set_param("pwm", channel, "i_set", tuner_out)
tec.set_param("output", channel, "i_set", tuner_out)
tec.set_param("pwm", channel, "i_set", 0)
time.sleep(0.05)
tec.set_param("output", channel, "i_set", 0)
if __name__ == "__main__":

View File

@ -1,7 +1,7 @@
import socket
import json
import logging
import time
class CommandError(Exception):
pass
@ -12,12 +12,16 @@ 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):
pwm_report = self.get_pwm()
for pwm_channel in pwm_report:
output_report = self.get_output()
for output_channel in output_report:
for limit in ["max_i_neg", "max_i_pos", "max_v"]:
if pwm_channel[limit] == 0.0:
logging.warning("`{}` limit is set to zero on channel {}".format(limit, pwm_channel["channel"]))
if output_channel[limit] == 0.0:
logging.warning("`{}` limit is set to zero on channel {}".format(limit, output_channel["channel"]))
def _read_line(self):
# read more lines
@ -47,8 +51,8 @@ class Client:
result[int(item["channel"])] = item
return result
def get_pwm(self):
"""Retrieve PWM limits for the TEC
def get_output(self):
"""Retrieve output limits for the TEC
Example::
[{'channel': 0,
@ -67,7 +71,7 @@ class Client:
'polarity': 'normal',
]
"""
return self._get_conf("pwm")
return self._get_conf("output")
def get_pid(self):
"""Retrieve PID control state
@ -92,14 +96,14 @@ class Client:
"""
return self._get_conf("pid")
def get_steinhart_hart(self):
"""Retrieve Steinhart-Hart parameters for resistance to temperature conversion
def get_b_parameter(self):
"""Retrieve B-Parameter equation parameters for resistance to temperature conversion
Example::
[{'params': {'b': 3800.0, 'r0': 10000.0, 't0': 298.15}, 'channel': 0},
{'params': {'b': 3800.0, 'r0': 10000.0, 't0': 298.15}, 'channel': 1}]
"""
return self._get_conf("s-h")
return self._get_conf("b-p")
def get_postfilter(self):
"""Retrieve DAC postfilter configuration
@ -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,24 +133,27 @@ 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
Examples::
tec.set_param("pwm", 0, "max_v", 2.0)
tec.set_param("output", 0, "max_v", 2.0)
tec.set_param("pid", 1, "output_max", 2.5)
tec.set_param("s-h", 0, "t0", 20.0)
tec.set_param("b-p", 0, "t0", 20.0)
tec.set_param("center", 0, "vref")
tec.set_param("postfilter", 1, 21)
@ -161,12 +168,40 @@ class Client:
def power_up(self, channel, target):
"""Start closed-loop mode"""
self.set_param("pid", channel, "target", value=target)
self.set_param("pwm", channel, "pid")
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)

View File

@ -0,0 +1,137 @@
import time
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from threading import Thread, Lock
from pythermostat.client import Client
def main():
TIME_WINDOW = 300.0
tec = Client()
target_temperature = tec.get_pid()[0]['target']
print("Channel 0 target temperature: {:.3f}".format(target_temperature))
class Series:
def __init__(self, conv=lambda x: x):
self.conv = conv
self.x_data = []
self.y_data = []
def append(self, x, y):
self.x_data.append(x)
self.y_data.append(self.conv(y))
def clip(self, min_x):
drop = 0
while drop < len(self.x_data) and self.x_data[drop] < min_x:
drop += 1
self.x_data = self.x_data[drop:]
self.y_data = self.y_data[drop:]
series = {
# 'adc': Series(),
# 'sens': Series(lambda x: x * 0.0001),
'temperature': Series(),
# 'i_set': Series(),
'pid_output': Series(),
# 'vref': Series(),
# 'dac_value': Series(),
# 'dac_feedback': Series(),
# 'i_tec': Series(),
'tec_i': Series(),
'tec_u_meas': Series(),
# 'interval': Series(),
}
series_lock = Lock()
quit = False
def recv_data(tec):
global last_packet_time
while True:
data = tec.get_report()
ch0 = data[0]
series_lock.acquire()
try:
for k, s in series.items():
if k in ch0:
v = ch0[k]
if type(v) is float:
s.append(ch0['time'], v)
finally:
series_lock.release()
if quit:
break
time.sleep(0.05)
thread = Thread(target=recv_data, args=(tec,))
thread.start()
fig, ax = plt.subplots()
for k, s in series.items():
s.plot, = ax.plot([], [], label=k)
legend = ax.legend()
def animate(i):
min_x, max_x, min_y, max_y = None, None, None, None
series_lock.acquire()
try:
for k, s in series.items():
s.plot.set_data(s.x_data, s.y_data)
if len(s.y_data) > 0:
s.plot.set_label("{}: {:.3f}".format(k, s.y_data[-1]))
if len(s.x_data) > 0:
min_x_ = min(s.x_data)
if min_x is None:
min_x = min_x_
else:
min_x = min(min_x, min_x_)
max_x_ = max(s.x_data)
if max_x is None:
max_x = max_x_
else:
max_x = max(max_x, max_x_)
if len(s.y_data) > 0:
min_y_ = min(s.y_data)
if min_y is None:
min_y = min_y_
else:
min_y = min(min_y, min_y_)
max_y_ = max(s.y_data)
if max_y is None:
max_y = max_y_
else:
max_y = max(max_y, max_y_)
if min_x and max_x - TIME_WINDOW > min_x:
for s in series.values():
s.clip(max_x - TIME_WINDOW)
finally:
series_lock.release()
if min_x != max_x:
ax.set_xlim(min_x, max_x)
if min_y != max_y:
margin_y = 0.01 * (max_y - min_y)
ax.set_ylim(min_y - margin_y, max_y + margin_y)
nonlocal legend
legend.remove()
legend = ax.legend()
ani = animation.FuncAnimation(
fig, animate, interval=1, blit=False, save_count=50)
plt.show()
quit = True
thread.join()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,81 @@
import argparse
from contextlib import contextmanager
from pythermostat.client import Client
CHANNELS = 2
def get_argparser():
parser = argparse.ArgumentParser(description="Thermostat hardware testing script")
parser.add_argument("host", metavar="HOST", default="192.168.1.26", nargs="?")
parser.add_argument("port", metavar="PORT", default=23, nargs="?")
parser.add_argument(
"-r",
"--testing_resistance",
default=10_000,
help="Testing resistance value through SENS pin in Ohms",
)
parser.add_argument(
"-d",
"--deviation",
default=1,
help="Allowed deviation of resistance in percentage",
)
return parser
def main():
args = get_argparser().parse_args()
min_allowed_resistance = args.testing_resistance * (1 - args.deviation / 100)
max_allowed_resistance = args.testing_resistance * (1 + args.deviation / 100)
print(min_allowed_resistance, max_allowed_resistance)
thermostat = Client(args.host, args.port)
for channel in range(CHANNELS):
print(f"Channel {channel} is active")
print("Checking resistance through SENS input ....", end=" ")
sens_resistance = thermostat.get_report()[channel]["sens"]
if sens_resistance is not None:
print(sens_resistance, "Ω")
if min_allowed_resistance <= sens_resistance <= max_allowed_resistance:
print("PASSED")
else:
print("FAILED")
else:
print("Floating SENS input! Is the channel connected?")
with preserve_thermostat_output_settings(thermostat, channel):
test_output_settings = {
"max_i_pos": 2,
"max_i_neg": 2,
"max_v": 4,
"i_set": 0.1,
"polarity": "normal",
}
for field, value in test_output_settings.items():
thermostat.set_param("output", channel, field, value)
input(f"Check if channel {channel} current = 0.1 A, and press ENTER...")
input(f"Channel {channel} testing done, press ENTER to continue.")
print()
print("Testing complete.")
@contextmanager
def preserve_thermostat_output_settings(client, channel):
original_output_settings = client.get_output()[channel]
yield original_output_settings
for setting in "max_i_pos", "max_i_neg", "max_v", "i_set", "polarity":
client.set_param("output", channel, setting, original_output_settings[setting])
if __name__ == "__main__":
main()

View File

@ -2,27 +2,28 @@ use num_traits::float::Float;
use serde::{Deserialize, Serialize};
use uom::si::{
electrical_resistance::ohm,
f64::{ElectricalResistance, ThermodynamicTemperature},
f64::{ElectricalResistance, TemperatureInterval, ThermodynamicTemperature},
ratio::ratio,
temperature_interval::kelvin as kelvin_interval,
thermodynamic_temperature::{degree_celsius, kelvin},
};
/// Steinhart-Hart equation parameters
/// B-Parameter equation parameters
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Parameters {
/// Base temperature
pub t0: ThermodynamicTemperature,
/// Base resistance
/// Thermistor resistance at base temperature
pub r0: ElectricalResistance,
/// Beta
pub b: f64,
/// Beta (average slope of the function ln R vs. 1/T)
pub b: TemperatureInterval,
}
impl Parameters {
/// Perform the voltage to temperature conversion.
/// Perform the resistance to temperature conversion.
pub fn get_temperature(&self, r: ElectricalResistance) -> ThermodynamicTemperature {
let inv_temp = 1.0 / self.t0.get::<kelvin>() + (r / self.r0).get::<ratio>().ln() / self.b;
ThermodynamicTemperature::new::<kelvin>(1.0 / inv_temp)
let temp = (self.t0.recip() + (r / self.r0).get::<ratio>().ln() / self.b).recip();
ThermodynamicTemperature::new::<kelvin>(temp.get::<kelvin_interval>())
}
}
@ -31,7 +32,7 @@ impl Default for Parameters {
Parameters {
t0: ThermodynamicTemperature::new::<degree_celsius>(25.0),
r0: ElectricalResistance::new::<ohm>(10_000.0),
b: 3800.0,
b: TemperatureInterval::new::<kelvin_interval>(3800.0),
}
}
}

View File

@ -1,9 +1,10 @@
use crate::{
ad7172,
ad7172, b_parameter as bp,
command_parser::{CenterPoint, Polarity},
config::PwmLimits,
pid, steinhart_hart as sh,
pid,
};
use num_traits::Zero;
use smoltcp::time::{Duration, Instant};
use uom::si::{
electric_current::ampere,
@ -31,7 +32,7 @@ pub struct ChannelState {
pub pwm_limits: PwmLimits,
pub pid_engaged: bool,
pub pid: pid::Controller,
pub sh: sh::Parameters,
pub bp: bp::Parameters,
pub polarity: Polarity,
}
@ -47,13 +48,13 @@ impl ChannelState {
dac_value: ElectricPotential::new::<volt>(0.0),
i_set: ElectricCurrent::new::<ampere>(0.0),
pwm_limits: PwmLimits {
max_v: 0.0,
max_i_pos: 0.0,
max_i_neg: 0.0,
max_v: ElectricPotential::zero(),
max_i_pos: ElectricCurrent::zero(),
max_i_neg: ElectricCurrent::zero(),
},
pid_engaged: false,
pid: pid::Controller::new(pid::Parameters::default()),
sh: sh::Parameters::default(),
bp: bp::Parameters::default(),
polarity: Polarity::Normal,
}
}
@ -99,7 +100,7 @@ impl ChannelState {
pub fn get_temperature(&self) -> Option<ThermodynamicTemperature> {
let r = self.get_sens()?;
let temperature = self.sh.get_temperature(r);
let temperature = self.bp.get_temperature(r);
Some(temperature)
}
}

View File

@ -1,12 +1,11 @@
use crate::timer::sleep;
use crate::{
ad5680, ad7172,
ad5680, ad7172, b_parameter,
channel::{Channel, Channel0, Channel1},
channel_state::ChannelState,
command_handler::JsonBuffer,
command_parser::{CenterPoint, Polarity, PwmPin},
pins::{self, Channel0VRef, Channel1VRef},
steinhart_hart,
};
use core::marker::PhantomData;
use heapless::{consts::U2, Vec};
@ -144,7 +143,7 @@ impl Channels {
voltage
}
pub fn get_i(&mut self, channel: usize) -> ElectricCurrent {
pub fn get_i_set(&mut self, channel: usize) -> ElectricCurrent {
let i_set = self.channel_state(channel).i_set;
i_set
}
@ -364,15 +363,15 @@ impl Channels {
}
pub fn get_max_v(&mut self, channel: usize) -> ElectricPotential {
ElectricPotential::new::<volt>(self.channel_state(channel).pwm_limits.max_v)
self.channel_state(channel).pwm_limits.max_v
}
pub fn get_max_i_pos(&mut self, channel: usize) -> ElectricCurrent {
ElectricCurrent::new::<ampere>(self.channel_state(channel).pwm_limits.max_i_pos)
self.channel_state(channel).pwm_limits.max_i_pos
}
pub fn get_max_i_neg(&mut self, channel: usize) -> ElectricCurrent {
ElectricCurrent::new::<ampere>(self.channel_state(channel).pwm_limits.max_i_neg)
self.channel_state(channel).pwm_limits.max_i_neg
}
// Get current passing through TEC
@ -420,7 +419,7 @@ impl Channels {
let max_v = max_v.min(MAX_TEC_V).max(ElectricPotential::zero());
let duty = (max_v / max).get::<ratio>();
let duty = self.set_pwm(channel, PwmPin::MaxV, duty);
self.channel_state(channel).pwm_limits.max_v = max_v.get::<volt>();
self.channel_state(channel).pwm_limits.max_v = max_v;
(duty * max, max)
}
@ -436,7 +435,7 @@ impl Channels {
Polarity::Normal => self.set_pwm(channel, PwmPin::MaxIPos, duty),
Polarity::Reversed => self.set_pwm(channel, PwmPin::MaxINeg, duty),
};
self.channel_state(channel).pwm_limits.max_i_pos = max_i_pos.get::<ampere>();
self.channel_state(channel).pwm_limits.max_i_pos = max_i_pos;
(duty * MAX_TEC_I_DUTY_TO_CURRENT_RATE, max)
}
@ -452,7 +451,7 @@ impl Channels {
Polarity::Normal => self.set_pwm(channel, PwmPin::MaxINeg, duty),
Polarity::Reversed => self.set_pwm(channel, PwmPin::MaxIPos, duty),
};
self.channel_state(channel).pwm_limits.max_i_neg = max_i_neg.get::<ampere>();
self.channel_state(channel).pwm_limits.max_i_neg = max_i_neg;
(duty * MAX_TEC_I_DUTY_TO_CURRENT_RATE, max)
}
@ -470,7 +469,7 @@ impl Channels {
}
fn report(&mut self, channel: usize) -> Report {
let i_set = self.get_i(channel);
let i_set = self.get_i_set(channel);
let i_tec = self.adc_read(channel, PinsAdcReadTarget::ITec, 16);
let tec_i = self.get_tec_i(channel);
let dac_value = self.get_dac(channel);
@ -521,11 +520,11 @@ impl Channels {
false
}
fn pwm_summary(&mut self, channel: usize) -> PwmSummary {
PwmSummary {
fn output_summary(&mut self, channel: usize) -> OutputSummary {
OutputSummary {
channel,
center: CenterPointJson(self.channel_state(channel).center.clone()),
i_set: self.get_i(channel),
i_set: self.get_i_set(channel),
max_v: self.get_max_v(channel),
max_i_pos: self.get_max_i_pos(channel),
max_i_neg: self.get_max_i_neg(channel),
@ -533,10 +532,10 @@ impl Channels {
}
}
pub fn pwm_summaries_json(&mut self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
pub fn output_summaries_json(&mut self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
let mut summaries = Vec::<_, U2>::new();
for channel in 0..CHANNELS {
let _ = summaries.push(self.pwm_summary(channel));
let _ = summaries.push(self.output_summary(channel));
}
serde_json_core::to_vec(&summaries)
}
@ -558,17 +557,17 @@ impl Channels {
serde_json_core::to_vec(&summaries)
}
fn steinhart_hart_summary(&mut self, channel: usize) -> SteinhartHartSummary {
let params = self.channel_state(channel).sh.clone();
SteinhartHartSummary { channel, params }
fn b_parameter_summary(&mut self, channel: usize) -> BParameterSummary {
let params = self.channel_state(channel).bp.clone();
BParameterSummary { channel, params }
}
pub fn steinhart_hart_summaries_json(
pub fn b_parameter_summaries_json(
&mut self,
) -> Result<JsonBuffer, serde_json_core::ser::Error> {
let mut summaries = Vec::<_, U2>::new();
for channel in 0..CHANNELS {
let _ = summaries.push(self.steinhart_hart_summary(channel));
let _ = summaries.push(self.b_parameter_summary(channel));
}
serde_json_core::to_vec(&summaries)
}
@ -630,7 +629,7 @@ impl Serialize for PolarityJson {
}
#[derive(Serialize)]
pub struct PwmSummary {
pub struct OutputSummary {
channel: usize,
center: CenterPointJson,
i_set: ElectricCurrent,
@ -647,7 +646,7 @@ pub struct PostFilterSummary {
}
#[derive(Serialize)]
pub struct SteinhartHartSummary {
pub struct BParameterSummary {
channel: usize,
params: steinhart_hart::Parameters,
params: b_parameter::Parameters,
}

View File

@ -2,7 +2,7 @@ use super::{
ad7172,
channels::{Channels, CHANNELS},
command_parser::{
CenterPoint, Command, Ipv4Config, PidParameter, Polarity, PwmPin, ShParameter, ShowCommand,
BpParameter, CenterPoint, Command, Ipv4Config, PidParameter, Polarity, PwmPin, ShowCommand,
},
config::ChannelConfig,
dfu,
@ -19,7 +19,11 @@ use uom::si::{
electric_current::ampere,
electric_potential::volt,
electrical_resistance::ohm,
f64::{ElectricCurrent, ElectricPotential, ElectricalResistance, ThermodynamicTemperature},
f64::{
ElectricCurrent, ElectricPotential, ElectricalResistance, TemperatureInterval,
ThermodynamicTemperature,
},
temperature_interval::kelvin,
thermodynamic_temperature::degree_celsius,
};
@ -96,7 +100,7 @@ impl Handler {
}
fn show_pwm(socket: &mut TcpSocket, channels: &mut Channels) -> Result<Handler, Error> {
match channels.pwm_summaries_json() {
match channels.output_summaries_json() {
Ok(buf) => {
send_line(socket, &buf);
}
@ -109,16 +113,13 @@ impl Handler {
Ok(Handler::Handled)
}
fn show_steinhart_hart(
socket: &mut TcpSocket,
channels: &mut Channels,
) -> Result<Handler, Error> {
match channels.steinhart_hart_summaries_json() {
fn show_b_parameter(socket: &mut TcpSocket, channels: &mut Channels) -> Result<Handler, Error> {
match channels.b_parameter_summaries_json() {
Ok(buf) => {
send_line(socket, &buf);
}
Err(e) => {
error!("unable to serialize steinhart-hart summaries: {:?}", e);
error!("unable to serialize b parameter summaries: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
return Err(Error::Report);
}
@ -206,7 +207,7 @@ impl Handler {
channel: usize,
center: CenterPoint,
) -> Result<Handler, Error> {
let i_set = channels.get_i(channel);
let i_set = channels.get_i_set(channel);
let state = channels.channel_state(channel);
state.center = center;
if !state.pid_engaged {
@ -237,19 +238,19 @@ impl Handler {
Ok(Handler::Handled)
}
fn set_steinhart_hart(
fn set_b_parameter(
socket: &mut TcpSocket,
channels: &mut Channels,
channel: usize,
parameter: ShParameter,
parameter: BpParameter,
value: f64,
) -> Result<Handler, Error> {
let sh = &mut channels.channel_state(channel).sh;
use super::command_parser::ShParameter::*;
let bp = &mut channels.channel_state(channel).bp;
use super::command_parser::BpParameter::*;
match parameter {
T0 => sh.t0 = ThermodynamicTemperature::new::<degree_celsius>(value),
B => sh.b = value,
R0 => sh.r0 = ElectricalResistance::new::<ohm>(value),
T0 => bp.t0 = ThermodynamicTemperature::new::<degree_celsius>(value),
B => bp.b = TemperatureInterval::new::<kelvin>(value),
R0 => bp.r0 = ElectricalResistance::new::<ohm>(value),
}
send_line(socket, b"{}");
Ok(Handler::Handled)
@ -475,17 +476,15 @@ impl Handler {
Command::Quit => Ok(Handler::CloseSocket),
Command::Show(ShowCommand::Input) => Handler::show_report(socket, channels),
Command::Show(ShowCommand::Pid) => Handler::show_pid(socket, channels),
Command::Show(ShowCommand::Pwm) => Handler::show_pwm(socket, channels),
Command::Show(ShowCommand::SteinhartHart) => {
Handler::show_steinhart_hart(socket, channels)
}
Command::Show(ShowCommand::Output) => Handler::show_pwm(socket, channels),
Command::Show(ShowCommand::BParameter) => Handler::show_b_parameter(socket, channels),
Command::Show(ShowCommand::PostFilter) => Handler::show_post_filter(socket, channels),
Command::Show(ShowCommand::Ipv4) => Handler::show_ipv4(socket, ipv4_config),
Command::PwmPid { channel } => Handler::engage_pid(socket, channels, channel),
Command::PwmPolarity { channel, polarity } => {
Command::OutputPid { channel } => Handler::engage_pid(socket, channels, channel),
Command::OutputPolarity { channel, polarity } => {
Handler::set_polarity(socket, channels, channel, polarity)
}
Command::Pwm {
Command::Output {
channel,
pin,
value,
@ -498,11 +497,11 @@ impl Handler {
parameter,
value,
} => Handler::set_pid(socket, channels, channel, parameter, value),
Command::SteinhartHart {
Command::BParameter {
channel,
parameter,
value,
} => Handler::set_steinhart_hart(socket, channels, channel, parameter, value),
} => Handler::set_b_parameter(socket, channels, channel, parameter, value),
Command::PostFilter {
channel,
rate: None,

View File

@ -91,9 +91,9 @@ pub struct Ipv4Config {
#[derive(Debug, Clone, PartialEq)]
pub enum ShowCommand {
Input,
Pwm,
Output,
Pid,
SteinhartHart,
BParameter,
PostFilter,
Ipv4,
}
@ -108,9 +108,9 @@ pub enum PidParameter {
OutputMax,
}
/// Steinhart-Hart equation parameter
/// B-Parameter equation parameter
#[derive(Debug, Clone, PartialEq)]
pub enum ShParameter {
pub enum BpParameter {
T0,
B,
R0,
@ -149,16 +149,16 @@ pub enum Command {
Ipv4(Ipv4Config),
Show(ShowCommand),
/// PWM parameter setting
Pwm {
Output {
channel: usize,
pin: PwmPin,
value: f64,
},
/// Enable PID control for `i_set`
PwmPid {
OutputPid {
channel: usize,
},
PwmPolarity {
OutputPolarity {
channel: usize,
polarity: Polarity,
},
@ -172,9 +172,9 @@ pub enum Command {
parameter: PidParameter,
value: f64,
},
SteinhartHart {
BParameter {
channel: usize,
parameter: ShParameter,
parameter: BpParameter,
value: f64,
},
PostFilter {
@ -260,12 +260,12 @@ fn pwm_setup(input: &[u8]) -> IResult<&[u8], Result<(PwmPin, f64), Error>> {
))(input)
}
/// `pwm <0-1> pid` - Set PWM to be controlled by PID
fn pwm_pid(input: &[u8]) -> IResult<&[u8], ()> {
/// `output <0-1> pid` - Set output to be controlled by PID
fn output_pid(input: &[u8]) -> IResult<&[u8], ()> {
value((), tag("pid"))(input)
}
fn pwm_polarity(input: &[u8]) -> IResult<&[u8], Polarity> {
fn output_polarity(input: &[u8]) -> IResult<&[u8], Polarity> {
preceded(
tag("polarity"),
preceded(
@ -278,8 +278,8 @@ fn pwm_polarity(input: &[u8]) -> IResult<&[u8], Polarity> {
)(input)
}
fn pwm(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
let (input, _) = tag("pwm")(input)?;
fn output(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
let (input, _) = tag("output")(input)?;
alt((
|input| {
let (input, _) = whitespace(input)?;
@ -287,19 +287,19 @@ fn pwm(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
let (input, _) = whitespace(input)?;
let (input, result) = alt((
|input| {
let (input, ()) = pwm_pid(input)?;
Ok((input, Ok(Command::PwmPid { channel })))
let (input, ()) = output_pid(input)?;
Ok((input, Ok(Command::OutputPid { channel })))
},
|input| {
let (input, polarity) = pwm_polarity(input)?;
Ok((input, Ok(Command::PwmPolarity { channel, polarity })))
let (input, polarity) = output_polarity(input)?;
Ok((input, Ok(Command::OutputPolarity { channel, polarity })))
},
|input| {
let (input, config) = pwm_setup(input)?;
match config {
Ok((pin, value)) => Ok((
input,
Ok(Command::Pwm {
Ok(Command::Output {
channel,
pin,
value,
@ -312,7 +312,7 @@ fn pwm(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
end(input)?;
Ok((input, result))
},
value(Ok(Command::Show(ShowCommand::Pwm)), end),
value(Ok(Command::Show(ShowCommand::Output)), end),
))(input)
}
@ -366,18 +366,18 @@ fn pid(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
))(input)
}
/// `s-h <0-1> <parameter> <value>`
fn steinhart_hart_parameter(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
/// `b-p <0-1> <parameter> <value>`
fn b_parameter_parameter(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
let (input, channel) = channel(input)?;
let (input, _) = whitespace(input)?;
let (input, parameter) = alt((
value(ShParameter::T0, tag("t0")),
value(ShParameter::B, tag("b")),
value(ShParameter::R0, tag("r0")),
value(BpParameter::T0, tag("t0")),
value(BpParameter::B, tag("b")),
value(BpParameter::R0, tag("r0")),
))(input)?;
let (input, _) = whitespace(input)?;
let (input, value) = float(input)?;
let result = value.map(|value| Command::SteinhartHart {
let result = value.map(|value| Command::BParameter {
channel,
parameter,
value,
@ -385,12 +385,12 @@ fn steinhart_hart_parameter(input: &[u8]) -> IResult<&[u8], Result<Command, Erro
Ok((input, result))
}
/// `s-h` | `s-h <steinhart_hart_parameter>`
fn steinhart_hart(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
let (input, _) = tag("s-h")(input)?;
/// `b-p` | `b-p <b_parameter_parameter>`
fn b_parameter(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
let (input, _) = tag("b-p")(input)?;
alt((
preceded(whitespace, steinhart_hart_parameter),
value(Ok(Command::Show(ShowCommand::SteinhartHart)), end),
preceded(whitespace, b_parameter_parameter),
value(Ok(Command::Show(ShowCommand::BParameter)), end),
))(input)
}
@ -569,10 +569,10 @@ fn command(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
value(Ok(Command::Reset), tag("reset")),
ipv4,
map(report, Ok),
pwm,
output,
center_point,
pid,
steinhart_hart,
b_parameter,
postfilter,
value(Ok(Command::Dfu), tag("dfu")),
fan,
@ -664,11 +664,11 @@ mod test {
}
#[test]
fn parse_pwm_i_set() {
let command = Command::parse(b"pwm 1 i_set 16383");
fn parse_output_i_set() {
let command = Command::parse(b"output 1 i_set 16383");
assert_eq!(
command,
Ok(Command::Pwm {
Ok(Command::Output {
channel: 1,
pin: PwmPin::ISet,
value: 16383.0,
@ -677,11 +677,11 @@ mod test {
}
#[test]
fn parse_pwm_polarity() {
let command = Command::parse(b"pwm 0 polarity reversed");
fn parse_output_polarity() {
let command = Command::parse(b"output 0 polarity reversed");
assert_eq!(
command,
Ok(Command::PwmPolarity {
Ok(Command::OutputPolarity {
channel: 0,
polarity: Polarity::Reversed,
})
@ -689,17 +689,17 @@ mod test {
}
#[test]
fn parse_pwm_pid() {
let command = Command::parse(b"pwm 0 pid");
assert_eq!(command, Ok(Command::PwmPid { channel: 0 }));
fn parse_output_pid() {
let command = Command::parse(b"output 0 pid");
assert_eq!(command, Ok(Command::OutputPid { channel: 0 }));
}
#[test]
fn parse_pwm_max_i_pos() {
let command = Command::parse(b"pwm 0 max_i_pos 7");
fn parse_output_max_i_pos() {
let command = Command::parse(b"output 0 max_i_pos 7");
assert_eq!(
command,
Ok(Command::Pwm {
Ok(Command::Output {
channel: 0,
pin: PwmPin::MaxIPos,
value: 7.0,
@ -708,11 +708,11 @@ mod test {
}
#[test]
fn parse_pwm_max_i_neg() {
let command = Command::parse(b"pwm 0 max_i_neg 128");
fn parse_output_max_i_neg() {
let command = Command::parse(b"output 0 max_i_neg 128");
assert_eq!(
command,
Ok(Command::Pwm {
Ok(Command::Output {
channel: 0,
pin: PwmPin::MaxINeg,
value: 128.0,
@ -721,11 +721,11 @@ mod test {
}
#[test]
fn parse_pwm_max_v() {
let command = Command::parse(b"pwm 0 max_v 32768");
fn parse_output_max_v() {
let command = Command::parse(b"output 0 max_v 32768");
assert_eq!(
command,
Ok(Command::Pwm {
Ok(Command::Output {
channel: 0,
pin: PwmPin::MaxV,
value: 32768.0,
@ -753,19 +753,19 @@ mod test {
}
#[test]
fn parse_steinhart_hart() {
let command = Command::parse(b"s-h");
assert_eq!(command, Ok(Command::Show(ShowCommand::SteinhartHart)));
fn parse_b_parameter() {
let command = Command::parse(b"b-p");
assert_eq!(command, Ok(Command::Show(ShowCommand::BParameter)));
}
#[test]
fn parse_steinhart_hart_set() {
let command = Command::parse(b"s-h 1 t0 23.05");
fn parse_b_parameter_set() {
let command = Command::parse(b"b-p 1 t0 23.05");
assert_eq!(
command,
Ok(Command::SteinhartHart {
Ok(Command::BParameter {
channel: 1,
parameter: ShParameter::T0,
parameter: BpParameter::T0,
value: 23.05,
})
);

View File

@ -1,16 +1,13 @@
use crate::{
ad7172::PostFilter,
b_parameter,
channels::Channels,
command_parser::{CenterPoint, Polarity},
pid, steinhart_hart,
pid,
};
use num_traits::Zero;
use serde::{Deserialize, Serialize};
use uom::si::{
electric_current::ampere,
electric_potential::volt,
f64::{ElectricCurrent, ElectricPotential},
};
use uom::si::f64::{ElectricCurrent, ElectricPotential};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ChannelConfig {
@ -20,7 +17,7 @@ pub struct ChannelConfig {
pid_engaged: bool,
i_set: ElectricCurrent,
polarity: Polarity,
sh: steinhart_hart::Parameters,
bp: b_parameter::Parameters,
pwm: PwmLimits,
/// uses variant `PostFilter::Invalid` instead of `None` to save space
adc_postfilter: PostFilter,
@ -49,7 +46,7 @@ impl ChannelConfig {
pid_engaged: state.pid_engaged,
i_set,
polarity: state.polarity.clone(),
sh: state.sh.clone(),
bp: state.bp.clone(),
pwm,
adc_postfilter,
}
@ -61,7 +58,7 @@ impl ChannelConfig {
state.pid.parameters = self.pid.clone();
state.pid.target = self.pid_target.into();
state.pid_engaged = self.pid_engaged;
state.sh = self.sh.clone();
state.bp = self.bp.clone();
self.pwm.apply(channels, channel);
@ -77,9 +74,9 @@ impl ChannelConfig {
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct PwmLimits {
pub max_v: f64,
pub max_i_pos: f64,
pub max_i_neg: f64,
pub max_v: ElectricPotential,
pub max_i_pos: ElectricCurrent,
pub max_i_neg: ElectricCurrent,
}
impl PwmLimits {
@ -88,15 +85,15 @@ impl PwmLimits {
let max_i_pos = channels.get_max_i_pos(channel);
let max_i_neg = channels.get_max_i_neg(channel);
PwmLimits {
max_v: max_v.get::<volt>(),
max_i_pos: max_i_pos.get::<ampere>(),
max_i_neg: max_i_neg.get::<ampere>(),
max_v,
max_i_pos,
max_i_neg,
}
}
pub fn apply(&self, channels: &mut Channels, channel: usize) {
channels.set_max_v(channel, ElectricPotential::new::<volt>(self.max_v));
channels.set_max_i_pos(channel, ElectricCurrent::new::<ampere>(self.max_i_pos));
channels.set_max_i_neg(channel, ElectricCurrent::new::<ampere>(self.max_i_neg));
channels.set_max_v(channel, self.max_v);
channels.set_max_i_pos(channel, self.max_i_pos);
channels.set_max_i_neg(channel, self.max_i_neg);
}
}

View File

@ -35,9 +35,9 @@ mod session;
use session::{Session, SessionInput};
mod command_parser;
use command_parser::Ipv4Config;
mod b_parameter;
mod channels;
mod pid;
mod steinhart_hart;
mod timer;
use channels::{Channels, CHANNELS};
mod channel;