forked from M-Labs/thermostat
Compare commits
37 Commits
d7462e6791
...
298746c338
Author | SHA1 | Date | |
---|---|---|---|
298746c338 | |||
fcb5cf1d4e | |||
d517087e10 | |||
798b400aa5 | |||
93dc39e943 | |||
5c3b759d0c | |||
6224486662 | |||
32bd49b258 | |||
ad54842c43 | |||
b336c4f993 | |||
680193b34b | |||
ae4bea0c8a | |||
1f2de942e4 | |||
1041d3ecbb | |||
c6040899dd | |||
9d89104f50 | |||
136c7a0b52 | |||
5000cae1b1 | |||
78ec77509f | |||
52aa3890c1 | |||
1ae6a6fdd4 | |||
7333d2cea5 | |||
44e9130010 | |||
5b0c6f7018 | |||
1007982b48 | |||
925601f4f5 | |||
8c1cb3117c | |||
1fcfe41a63 | |||
9fce19a418 | |||
00d5feaa8d | |||
09be55e12a | |||
76547be90a | |||
8b975e656e | |||
ae3d8b51d4 | |||
17edae44fb | |||
03b4561142 | |||
631a10938d |
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,2 +1,5 @@
|
||||
target/
|
||||
result
|
||||
*.bin
|
||||
|
||||
__pycache__/
|
||||
|
56
README.md
56
README.md
@ -45,7 +45,7 @@ There are several options for flashing Thermostat. DFU requires only a micro-USB
|
||||
|
||||
### dfu-util on Linux
|
||||
* Install the DFU USB tool (dfu-util).
|
||||
* Convert firmware from ELF to BIN: `arm-none-eabi-objcopy -O binary thermostat thermostat.bin` (you can skip this step if using the BIN from Hydra)
|
||||
* Convert firmware from ELF to BIN: `llvm-objcopy -O binary target/thumbv7em-none-eabihf/release/thermostat thermostat.bin` (you can skip this step if using the BIN from Hydra)
|
||||
* Connect to the Micro USB connector to Thermostat below the RJ45.
|
||||
* Add jumper to Thermostat v2.0 across 2-pin jumper adjacent to JTAG connector.
|
||||
* Cycle board power to put it in DFU update mode
|
||||
@ -84,9 +84,7 @@ invalidate the first line of input.
|
||||
|
||||
### Reading ADC input
|
||||
|
||||
Set report mode to `on` for a continuous stream of input data.
|
||||
|
||||
The scope of this setting is per TCP session.
|
||||
ADC input data is provided in reports. Query for the latest report with the command `report`. See the *Reports* section below.
|
||||
|
||||
|
||||
### TCP commands
|
||||
@ -95,15 +93,14 @@ Send commands as simple text string terminated by `\n`. Responses are
|
||||
formatted as line-delimited JSON.
|
||||
|
||||
| Syntax | Function |
|
||||
|----------------------------------|-------------------------------------------------------------------------------|
|
||||
|------------------------------------------- |-------------------------------------------------------------------------------|
|
||||
| `report` | Show current input |
|
||||
| `report mode` | Show current report mode |
|
||||
| `report mode <off/on>` | Set report mode |
|
||||
| `pwm` | Show current PWM settings |
|
||||
| `pwm <0/1> max_i_pos <amp>` | Set maximum positive output current |
|
||||
| `pwm <0/1> max_i_neg <amp>` | Set maximum negative output current |
|
||||
| `pwm <0/1> max_v <volt>` | Set maximum output voltage |
|
||||
| `pwm <0/1> i_set <amp>` | Disengage PID, set fixed output current |
|
||||
| `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 |
|
||||
| `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 |
|
||||
@ -114,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 |
|
||||
@ -147,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
|
||||
@ -186,6 +183,8 @@ 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.
|
||||
|
||||
Testing heat flow direction with a low set current is recommended before installation of the TEC module.
|
||||
|
||||
### Limits
|
||||
@ -251,8 +250,7 @@ pwm 0 pid
|
||||
|
||||
## Reports
|
||||
|
||||
Use the bare `report` command to obtain a single report. Enable
|
||||
continuous reporting with `report mode on`. Reports are JSON objects
|
||||
Use the bare `report` command to obtain a single report. Reports are JSON objects
|
||||
with the following keys.
|
||||
|
||||
| Key | Unit | Description |
|
||||
@ -261,10 +259,9 @@ with the following keys.
|
||||
| `time` | Seconds | Temperature measurement time |
|
||||
| `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 |
|
||||
| `vref` | Volts | MAX1968 VREF (1.5 V) |
|
||||
| `dac_value` | Volts | AD5680 output derived from `i_set` |
|
||||
| `dac_feedback` | Volts | ADC measurement of the AD5680 output |
|
||||
| `i_tec` | Volts | MAX1968 TEC current monitor |
|
||||
@ -272,18 +269,19 @@ with the following keys.
|
||||
| `tec_u_meas` | Volts | Measurement of the voltage across the TEC |
|
||||
| `pid_output` | Amperes | PID control output |
|
||||
|
||||
Note: With Thermostat v2 and below, the voltage and current readouts `i_tec` and `tec_i` are noisy without the hardware fix shown in [this PR][https://git.m-labs.hk/M-Labs/thermostat/pulls/105].
|
||||
|
||||
## PID Tuning
|
||||
|
||||
The thermostat implements a PID control loop for each of the TEC channels, more details on setting up the PID control loop can be found [here](./doc/PID%20tuning.md).
|
||||
|
||||
## Fan control
|
||||
|
||||
Fan control is available for the thermostat revisions with integrated fan system. For this purpose four commands are available:
|
||||
Fan control commands are available for thermostat revisions with an integrated fan system:
|
||||
1. `fan` - show fan stats: `fan_pwm`, `abs_max_tec_i`, `auto_mode`, `k_a`, `k_b`, `k_c`.
|
||||
2. `fan auto` - enable auto speed controller mode, which correlates with fan curve `fcurve`.
|
||||
3. `fan <value>` - set the fan power with the value from `1` to `100` and disable auto mode. There is no way to disable the fan.
|
||||
2. `fan auto` - enable auto speed controller mode, where fan speed is controlled by the fan curve `fcurve`.
|
||||
3. `fan <value>` - set the fan power with the value from `1` to `100` and disable auto mode. There is no way to completely disable the fan.
|
||||
Please note that power doesn't correlate with the actual speed linearly.
|
||||
4. `fcurve <a> <b> <c>` - set coefficients of the controlling curve `a*x^2 + b*x + c`, where `x` is `abs_max_tec_i/MAX_TEC_I`,
|
||||
i.e. receives values from 0 to 1 linearly tied to the maximum current. The controlling curve should produce values from 0 to 1,
|
||||
as below and beyond values would be substituted by 0 and 1 respectively.
|
||||
5. `fcurve default` - restore fan curve settings to defaults: `a = 1.0, b = 0.0, c = 0.0`.
|
||||
4. `fcurve <a> <b> <c>` - set coefficients of the controlling curve `a*x^2 + b*x + c`, where `x` is `abs_max_tec_i/MAX_TEC_I`, a normalized value in range [0,1],
|
||||
i.e. the (linear) proportion of current output capacity used, on the channel with the largest current flow. The controlling curve is also clamped to [0,1].
|
||||
5. `fcurve default` - restore fan curve coefficients to defaults: `a = 1.0, b = 0.0, c = 0.0`.
|
||||
|
48
flake.lock
generated
48
flake.lock
generated
@ -1,41 +1,45 @@
|
||||
{
|
||||
"nodes": {
|
||||
"mozilla-overlay": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1690536331,
|
||||
"narHash": "sha256-aRIf2FB2GTdfF7gl13WyETmiV/J7EhBGkSWXfZvlxcA=",
|
||||
"owner": "mozilla",
|
||||
"repo": "nixpkgs-mozilla",
|
||||
"rev": "db89c8707edcffefcd8e738459d511543a339ff5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "mozilla",
|
||||
"repo": "nixpkgs-mozilla",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1691421349,
|
||||
"narHash": "sha256-RRJyX0CUrs4uW4gMhd/X4rcDG8PTgaaCQM5rXEJOx6g=",
|
||||
"lastModified": 1722791413,
|
||||
"narHash": "sha256-rCTrlCWvHzMCNcKxPE3Z/mMK2gDZ+BvvpEVyRM4tKmU=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "011567f35433879aae5024fc6ec53f2a0568a6c4",
|
||||
"rev": "8b5b6723aca5a51edf075936439d9cd3947b7b2c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-23.05",
|
||||
"ref": "nixos-24.05",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"mozilla-overlay": "mozilla-overlay",
|
||||
"nixpkgs": "nixpkgs"
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
43
flake.nix
43
flake.nix
@ -1,32 +1,25 @@
|
||||
{
|
||||
description = "Firmware for the Sinara 8451 Thermostat";
|
||||
|
||||
inputs.nixpkgs.url = github:NixOS/nixpkgs/nixos-23.05;
|
||||
inputs.mozilla-overlay = { url = github:mozilla/nixpkgs-mozilla; flake = false; };
|
||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
|
||||
inputs.rust-overlay = {
|
||||
url = "github:oxalica/rust-overlay";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, mozilla-overlay }:
|
||||
outputs = { self, nixpkgs, rust-overlay }:
|
||||
let
|
||||
pkgs = import nixpkgs { system = "x86_64-linux"; 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=";
|
||||
};
|
||||
pkgs = import nixpkgs { system = "x86_64-linux"; overlays = [ (import rust-overlay) ]; };
|
||||
|
||||
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"];
|
||||
rust = pkgs.rust-bin.stable."1.66.0".default.override {
|
||||
extensions = [ "rust-src" ];
|
||||
targets = [ "thumbv7em-none-eabihf" ];
|
||||
};
|
||||
rust = rustChannelOfTargets "stable" null targets;
|
||||
rustPlatform = pkgs.recurseIntoAttrs (pkgs.makeRustPlatform {
|
||||
rustPlatform = pkgs.makeRustPlatform {
|
||||
rustc = rust;
|
||||
cargo = rust;
|
||||
});
|
||||
};
|
||||
|
||||
thermostat = rustPlatform.buildRustPackage {
|
||||
name = "thermostat";
|
||||
version = "0.0.0";
|
||||
@ -54,24 +47,26 @@
|
||||
'';
|
||||
|
||||
dontFixup = true;
|
||||
auditable = false;
|
||||
};
|
||||
in {
|
||||
packages.x86_64-linux = {
|
||||
inherit thermostat;
|
||||
default = thermostat;
|
||||
};
|
||||
|
||||
hydraJobs = {
|
||||
inherit thermostat;
|
||||
};
|
||||
|
||||
devShell.x86_64-linux = pkgs.mkShell {
|
||||
devShells.x86_64-linux.default = pkgs.mkShellNoCC {
|
||||
name = "thermostat-dev-shell";
|
||||
buildInputs = with pkgs; [
|
||||
rust openocd dfu-util
|
||||
packages = with pkgs; [
|
||||
rust llvm
|
||||
openocd dfu-util rlwrap
|
||||
] ++ (with python3Packages; [
|
||||
numpy matplotlib
|
||||
]);
|
||||
};
|
||||
defaultPackage.x86_64-linux = thermostat;
|
||||
};
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
from pytec.client import Client
|
||||
|
||||
tec = Client() #(host="localhost", port=6667)
|
||||
tec.set_param("s-h", 1, "t0", 20)
|
||||
tec.set_param("b-p", 1, "t0", 20)
|
||||
print(tec.get_pwm())
|
||||
print(tec.get_pid())
|
||||
print(tec.get_pwm())
|
||||
print(tec.get_postfilter())
|
||||
print(tec.get_steinhart_hart())
|
||||
print(tec.get_b_parameter())
|
||||
for data in tec.report_mode():
|
||||
print(data)
|
||||
|
@ -1,6 +1,7 @@
|
||||
import socket
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
|
||||
class CommandError(Exception):
|
||||
pass
|
||||
@ -15,7 +16,7 @@ class Client:
|
||||
pwm_report = self.get_pwm()
|
||||
for pwm_channel in pwm_report:
|
||||
for limit in ["max_i_neg", "max_i_pos", "max_v"]:
|
||||
if pwm_channel[limit]["value"] == 0.0:
|
||||
if pwm_channel[limit] == 0.0:
|
||||
logging.warning("`{}` limit is set to zero on channel {}".format(limit, pwm_channel["channel"]))
|
||||
|
||||
def _read_line(self):
|
||||
@ -52,16 +53,18 @@ class Client:
|
||||
Example::
|
||||
[{'channel': 0,
|
||||
'center': 'vref',
|
||||
'i_set': {'max': 2.9802790335151985, 'value': -0.02002179650216762},
|
||||
'max_i_neg': {'max': 3.0, 'value': 3.0},
|
||||
'max_v': {'max': 5.988, 'value': 5.988},
|
||||
'max_i_pos': {'max': 3.0, 'value': 3.0}},
|
||||
'i_set': -0.02002179650216762,
|
||||
'max_i_neg': 2.0,
|
||||
'max_v': 3.988,
|
||||
'max_i_pos': 2.0,
|
||||
'polarity': 'normal',
|
||||
{'channel': 1,
|
||||
'center': 'vref',
|
||||
'i_set': {'max': 2.9802790335151985, 'value': -0.02002179650216762},
|
||||
'max_i_neg': {'max': 3.0, 'value': 3.0},
|
||||
'max_v': {'max': 5.988, 'value': 5.988},
|
||||
'max_i_pos': {'max': 3.0, 'value': 3.0}}
|
||||
'i_set': -0.02002179650216762,
|
||||
'max_i_neg': 2.0,
|
||||
'max_v': 3.988,
|
||||
'max_i_pos': 2.0}
|
||||
'polarity': 'normal',
|
||||
]
|
||||
"""
|
||||
return self._get_conf("pwm")
|
||||
@ -89,14 +92,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
|
||||
@ -126,9 +129,8 @@ class Client:
|
||||
'tec_u_meas': 2.5340000000000003,
|
||||
'pid_output': 2.067581958092247}
|
||||
"""
|
||||
self._command("report mode", "on")
|
||||
|
||||
while True:
|
||||
self._socket.sendall("report\n".encode('utf-8'))
|
||||
line = self._read_line()
|
||||
if not line:
|
||||
break
|
||||
@ -136,6 +138,7 @@ class Client:
|
||||
yield json.loads(line)
|
||||
except json.decoder.JSONDecodeError:
|
||||
pass
|
||||
time.sleep(0.05)
|
||||
|
||||
def set_param(self, topic, channel, field="", value=""):
|
||||
"""Set configuration parameters
|
||||
@ -143,7 +146,7 @@ class Client:
|
||||
Examples::
|
||||
tec.set_param("pwm", 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)
|
||||
|
||||
|
@ -10,15 +10,15 @@ use uom::si::{
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 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,
|
||||
/// B, the average slope of the function ln R vs. 1/T
|
||||
pub b: ThermodynamicTemperature,
|
||||
}
|
||||
|
||||
impl Parameters {
|
@ -24,7 +24,7 @@ pub struct Channel<C: ChannelPins> {
|
||||
pub vref_meas: ElectricPotential,
|
||||
pub shdn: C::Shdn,
|
||||
pub vref_pin: C::VRefPin,
|
||||
pub itec_pin: C::ItecPin,
|
||||
pub itec_pin: C::ITecPin,
|
||||
/// feedback from `dac` output
|
||||
pub dac_feedback_pin: C::DacFeedbackPin,
|
||||
pub tec_u_meas_pin: C::TecUMeasPin,
|
||||
|
@ -2,11 +2,13 @@ use smoltcp::time::{Duration, Instant};
|
||||
use uom::si::{
|
||||
f64::{
|
||||
ElectricPotential,
|
||||
ElectricCurrent,
|
||||
ElectricalResistance,
|
||||
ThermodynamicTemperature,
|
||||
Time,
|
||||
},
|
||||
electric_potential::volt,
|
||||
electric_current::ampere,
|
||||
electrical_resistance::ohm,
|
||||
thermodynamic_temperature::degree_celsius,
|
||||
time::millisecond,
|
||||
@ -14,8 +16,9 @@ use uom::si::{
|
||||
use crate::{
|
||||
ad7172,
|
||||
pid,
|
||||
steinhart_hart as sh,
|
||||
command_parser::CenterPoint,
|
||||
config::PwmLimits,
|
||||
b_parameter as bp,
|
||||
command_parser::{CenterPoint, Polarity},
|
||||
};
|
||||
|
||||
const R_INNER: f64 = 2.0 * 5100.0;
|
||||
@ -29,9 +32,12 @@ pub struct ChannelState {
|
||||
/// i_set 0A center point
|
||||
pub center: CenterPoint,
|
||||
pub dac_value: ElectricPotential,
|
||||
pub i_set: ElectricCurrent,
|
||||
pub pwm_limits: PwmLimits,
|
||||
pub pid_engaged: bool,
|
||||
pub pid: pid::Controller,
|
||||
pub sh: sh::Parameters,
|
||||
pub bp: bp::Parameters,
|
||||
pub polarity: Polarity,
|
||||
}
|
||||
|
||||
impl ChannelState {
|
||||
@ -44,9 +50,16 @@ impl ChannelState {
|
||||
adc_interval: Duration::from_millis(100),
|
||||
center: CenterPoint::Vref,
|
||||
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,
|
||||
},
|
||||
pid_engaged: false,
|
||||
pid: pid::Controller::new(pid::Parameters::default()),
|
||||
sh: sh::Parameters::default(),
|
||||
bp: bp::Parameters::default(),
|
||||
polarity: Polarity::Normal,
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,7 +105,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)
|
||||
}
|
||||
}
|
||||
|
411
src/channels.rs
411
src/channels.rs
@ -1,5 +1,6 @@
|
||||
use core::cmp::max_by;
|
||||
use core::marker::PhantomData;
|
||||
use heapless::{consts::U2, Vec};
|
||||
use num_traits::Zero;
|
||||
use serde::{Serialize, Serializer};
|
||||
use smoltcp::time::Instant;
|
||||
use stm32f4xx_hal::hal;
|
||||
@ -16,17 +17,45 @@ use crate::{
|
||||
ad7172,
|
||||
channel::{Channel, Channel0, Channel1},
|
||||
channel_state::ChannelState,
|
||||
command_parser::{CenterPoint, PwmPin},
|
||||
command_parser::{CenterPoint, PwmPin, Polarity},
|
||||
command_handler::JsonBuffer,
|
||||
pins,
|
||||
steinhart_hart,
|
||||
pins::{self, Channel0VRef, Channel1VRef},
|
||||
b_parameter,
|
||||
};
|
||||
use crate::timer::sleep;
|
||||
|
||||
pub enum PinsAdcReadTarget {
|
||||
VREF,
|
||||
DacVfb,
|
||||
ITec,
|
||||
VTec,
|
||||
}
|
||||
|
||||
pub const CHANNELS: usize = 2;
|
||||
pub const R_SENSE: f64 = 0.05;
|
||||
// DAC chip outputs 0-5v, which is then passed through a resistor dividor to provide 0-3v range
|
||||
const DAC_OUT_V_MAX: f64 = 3.0;
|
||||
|
||||
// From design specs
|
||||
pub const MAX_TEC_I: ElectricCurrent = ElectricCurrent {
|
||||
dimension: PhantomData,
|
||||
units: PhantomData,
|
||||
value: 2.0,
|
||||
};
|
||||
pub const MAX_TEC_V: ElectricPotential = ElectricPotential {
|
||||
dimension: PhantomData,
|
||||
units: PhantomData,
|
||||
value: 4.0,
|
||||
};
|
||||
const MAX_TEC_I_DUTY_TO_CURRENT_RATE: ElectricCurrent = ElectricCurrent {
|
||||
dimension: PhantomData,
|
||||
units: PhantomData,
|
||||
value: 1.0 / (10.0 * R_SENSE / 3.3),
|
||||
};
|
||||
// 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 {
|
||||
dimension: PhantomData,
|
||||
units: PhantomData,
|
||||
value: 3.0,
|
||||
};
|
||||
// TODO: -pub
|
||||
pub struct Channels {
|
||||
channel0: Channel<Channel0>,
|
||||
@ -98,7 +127,7 @@ impl Channels {
|
||||
pub fn get_center(&mut self, channel: usize) -> ElectricPotential {
|
||||
match self.channel_state(channel).center {
|
||||
CenterPoint::Vref =>
|
||||
self.read_vref(channel),
|
||||
self.adc_read(channel, PinsAdcReadTarget::VREF, 8),
|
||||
CenterPoint::Override(center_point) =>
|
||||
ElectricPotential::new::<volt>(center_point.into()),
|
||||
}
|
||||
@ -111,16 +140,13 @@ impl Channels {
|
||||
}
|
||||
|
||||
pub fn get_i(&mut self, channel: usize) -> ElectricCurrent {
|
||||
let center_point = self.get_center(channel);
|
||||
let r_sense = ElectricalResistance::new::<ohm>(R_SENSE);
|
||||
let voltage = self.get_dac(channel);
|
||||
let i_tec = (voltage - center_point) / (10.0 * r_sense);
|
||||
i_tec
|
||||
let i_set = self.channel_state(channel).i_set;
|
||||
i_set
|
||||
}
|
||||
|
||||
/// i_set DAC
|
||||
fn set_dac(&mut self, channel: usize, voltage: ElectricPotential) -> ElectricPotential {
|
||||
let value = ((voltage / ElectricPotential::new::<volt>(DAC_OUT_V_MAX)).get::<ratio>() * (ad5680::MAX_VALUE as f64)) as u32 ;
|
||||
let value = ((voltage / DAC_OUT_V_MAX).get::<ratio>() * (ad5680::MAX_VALUE as f64)) as u32 ;
|
||||
match channel {
|
||||
0 => self.channel0.dac.set(value).unwrap(),
|
||||
1 => self.channel1.dac.set(value).unwrap(),
|
||||
@ -130,7 +156,13 @@ impl Channels {
|
||||
voltage
|
||||
}
|
||||
|
||||
pub fn set_i(&mut self, channel: usize, i_tec: 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);
|
||||
self.channel_state(channel).i_set = i_set;
|
||||
let negate = match self.channel_state(channel).polarity {
|
||||
Polarity::Normal => 1.0,
|
||||
Polarity::Reversed => -1.0,
|
||||
};
|
||||
let vref_meas = match channel.into() {
|
||||
0 => self.channel0.vref_meas,
|
||||
1 => self.channel1.vref_meas,
|
||||
@ -138,109 +170,111 @@ impl Channels {
|
||||
};
|
||||
let center_point = vref_meas;
|
||||
let r_sense = ElectricalResistance::new::<ohm>(R_SENSE);
|
||||
let voltage = i_tec * 10.0 * r_sense + center_point;
|
||||
let voltage = negate * i_set * 10.0 * r_sense + center_point;
|
||||
let voltage = self.set_dac(channel, voltage);
|
||||
let i_tec = (voltage - center_point) / (10.0 * r_sense);
|
||||
i_tec
|
||||
let i_set = negate * (voltage - center_point) / (10.0 * r_sense);
|
||||
i_set
|
||||
}
|
||||
|
||||
pub fn read_dac_feedback(&mut self, channel: usize) -> ElectricPotential {
|
||||
/// AN4073: ADC Reading Dispersion can be reduced through Averaging
|
||||
pub fn adc_read(&mut self, channel: usize, adc_read_target: PinsAdcReadTarget, avg_pt: u16) -> ElectricPotential {
|
||||
let mut sample: u32 = 0;
|
||||
match channel {
|
||||
0 => {
|
||||
let sample = self.pins_adc.convert(
|
||||
&self.channel0.dac_feedback_pin,
|
||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480
|
||||
);
|
||||
let mv = self.pins_adc.sample_to_millivolts(sample);
|
||||
sample = match adc_read_target {
|
||||
PinsAdcReadTarget::VREF => {
|
||||
match &self.channel0.vref_pin {
|
||||
Channel0VRef::Analog(vref_pin) => {
|
||||
for _ in (0..avg_pt).rev() {
|
||||
sample += self
|
||||
.pins_adc
|
||||
.convert(vref_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
|
||||
as u32;
|
||||
}
|
||||
sample / avg_pt as u32
|
||||
},
|
||||
Channel0VRef::Disabled(_) => {2048 as u32}
|
||||
}
|
||||
}
|
||||
PinsAdcReadTarget::DacVfb => {
|
||||
for _ in (0..avg_pt).rev() {
|
||||
sample += self
|
||||
.pins_adc
|
||||
.convert(&self.channel0.dac_feedback_pin,stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
|
||||
as u32;
|
||||
}
|
||||
sample / avg_pt as u32
|
||||
}
|
||||
PinsAdcReadTarget::ITec => {
|
||||
for _ in (0..avg_pt).rev() {
|
||||
sample += self
|
||||
.pins_adc
|
||||
.convert(&self.channel0.itec_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
|
||||
as u32;
|
||||
}
|
||||
sample / avg_pt as u32
|
||||
}
|
||||
PinsAdcReadTarget::VTec => {
|
||||
for _ in (0..avg_pt).rev() {
|
||||
sample += self
|
||||
.pins_adc
|
||||
.convert(&self.channel0.tec_u_meas_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
|
||||
as u32;
|
||||
}
|
||||
sample / avg_pt as u32
|
||||
}
|
||||
};
|
||||
let mv = self.pins_adc.sample_to_millivolts(sample as u16);
|
||||
ElectricPotential::new::<millivolt>(mv as f64)
|
||||
}
|
||||
1 => {
|
||||
let sample = self.pins_adc.convert(
|
||||
&self.channel1.dac_feedback_pin,
|
||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480
|
||||
);
|
||||
let mv = self.pins_adc.sample_to_millivolts(sample);
|
||||
sample = match adc_read_target {
|
||||
PinsAdcReadTarget::VREF => {
|
||||
match &self.channel1.vref_pin {
|
||||
Channel1VRef::Analog(vref_pin) => {
|
||||
for _ in (0..avg_pt).rev() {
|
||||
sample += self
|
||||
.pins_adc
|
||||
.convert(vref_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
|
||||
as u32;
|
||||
}
|
||||
sample / avg_pt as u32
|
||||
},
|
||||
Channel1VRef::Disabled(_) => {2048 as u32}
|
||||
}
|
||||
}
|
||||
PinsAdcReadTarget::DacVfb => {
|
||||
for _ in (0..avg_pt).rev() {
|
||||
sample += self
|
||||
.pins_adc
|
||||
.convert(&self.channel1.dac_feedback_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
|
||||
as u32;
|
||||
}
|
||||
sample / avg_pt as u32
|
||||
}
|
||||
PinsAdcReadTarget::ITec => {
|
||||
for _ in (0..avg_pt).rev() {
|
||||
sample += self
|
||||
.pins_adc
|
||||
.convert(&self.channel1.itec_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
|
||||
as u32;
|
||||
}
|
||||
sample / avg_pt as u32
|
||||
}
|
||||
PinsAdcReadTarget::VTec => {
|
||||
for _ in (0..avg_pt).rev() {
|
||||
sample += self
|
||||
.pins_adc
|
||||
.convert(&self.channel1.tec_u_meas_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
|
||||
as u32;
|
||||
}
|
||||
sample / avg_pt as u32
|
||||
}
|
||||
};
|
||||
let mv = self.pins_adc.sample_to_millivolts(sample as u16);
|
||||
ElectricPotential::new::<millivolt>(mv as f64)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_dac_feedback_until_stable(&mut self, channel: usize, tolerance: ElectricPotential) -> ElectricPotential {
|
||||
let mut prev = self.read_dac_feedback(channel);
|
||||
loop {
|
||||
let current = self.read_dac_feedback(channel);
|
||||
if (current - prev).abs() < tolerance {
|
||||
return current;
|
||||
}
|
||||
prev = current;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_itec(&mut self, channel: usize) -> ElectricPotential {
|
||||
match channel {
|
||||
0 => {
|
||||
let sample = self.pins_adc.convert(
|
||||
&self.channel0.itec_pin,
|
||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480
|
||||
);
|
||||
let mv = self.pins_adc.sample_to_millivolts(sample);
|
||||
ElectricPotential::new::<millivolt>(mv as f64)
|
||||
}
|
||||
1 => {
|
||||
let sample = self.pins_adc.convert(
|
||||
&self.channel1.itec_pin,
|
||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480
|
||||
);
|
||||
let mv = self.pins_adc.sample_to_millivolts(sample);
|
||||
ElectricPotential::new::<millivolt>(mv as f64)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// should be 1.5V
|
||||
pub fn read_vref(&mut self, channel: usize) -> ElectricPotential {
|
||||
match channel {
|
||||
0 => {
|
||||
let sample = self.pins_adc.convert(
|
||||
&self.channel0.vref_pin,
|
||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480
|
||||
);
|
||||
let mv = self.pins_adc.sample_to_millivolts(sample);
|
||||
ElectricPotential::new::<millivolt>(mv as f64)
|
||||
}
|
||||
1 => {
|
||||
let sample = self.pins_adc.convert(
|
||||
&self.channel1.vref_pin,
|
||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480
|
||||
);
|
||||
let mv = self.pins_adc.sample_to_millivolts(sample);
|
||||
ElectricPotential::new::<millivolt>(mv as f64)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_tec_u_meas(&mut self, channel: usize) -> ElectricPotential {
|
||||
match channel {
|
||||
0 => {
|
||||
let sample = self.pins_adc.convert(
|
||||
&self.channel0.tec_u_meas_pin,
|
||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480
|
||||
);
|
||||
let mv = self.pins_adc.sample_to_millivolts(sample);
|
||||
ElectricPotential::new::<millivolt>(mv as f64)
|
||||
}
|
||||
1 => {
|
||||
let sample = self.pins_adc.convert(
|
||||
&self.channel1.tec_u_meas_pin,
|
||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480
|
||||
);
|
||||
let mv = self.pins_adc.sample_to_millivolts(sample);
|
||||
ElectricPotential::new::<millivolt>(mv as f64)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
_ => unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
@ -270,8 +304,7 @@ impl Channels {
|
||||
let mut start_value = 1;
|
||||
let mut best_error = ElectricPotential::new::<volt>(100.0);
|
||||
|
||||
for step in (0..18).rev() {
|
||||
let mut prev_value = start_value;
|
||||
for step in (5..18).rev() {
|
||||
for value in (start_value..=ad5680::MAX_VALUE).step_by(1 << step) {
|
||||
match channel {
|
||||
0 => {
|
||||
@ -282,24 +315,23 @@ impl Channels {
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
sleep(10);
|
||||
|
||||
let dac_feedback = self.read_dac_feedback_until_stable(channel, ElectricPotential::new::<volt>(0.001));
|
||||
let dac_feedback = self.adc_read(channel, PinsAdcReadTarget::DacVfb, 64);
|
||||
let error = target_voltage - dac_feedback;
|
||||
if error < ElectricPotential::new::<volt>(0.0) {
|
||||
break;
|
||||
} else if error < best_error {
|
||||
best_error = error;
|
||||
start_value = prev_value;
|
||||
start_value = value;
|
||||
|
||||
let vref = (value as f64 / ad5680::MAX_VALUE as f64) * ElectricPotential::new::<volt>(DAC_OUT_V_MAX);
|
||||
let vref = (value as f64 / ad5680::MAX_VALUE as f64) * DAC_OUT_V_MAX;
|
||||
match channel {
|
||||
0 => self.channel0.vref_meas = vref,
|
||||
1 => self.channel1.vref_meas = vref,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
prev_value = value;
|
||||
}
|
||||
}
|
||||
|
||||
@ -325,58 +357,30 @@ impl Channels {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_pwm(&self, channel: usize, pin: PwmPin) -> f64 {
|
||||
fn get<P: hal::PwmPin<Duty=u16>>(pin: &P) -> f64 {
|
||||
let duty = pin.get_duty();
|
||||
let max = pin.get_max_duty();
|
||||
duty as f64 / (max as f64)
|
||||
}
|
||||
match (channel, pin) {
|
||||
(_, PwmPin::ISet) =>
|
||||
panic!("i_set is no pwm pin"),
|
||||
(0, PwmPin::MaxIPos) =>
|
||||
get(&self.pwm.max_i_pos0),
|
||||
(0, PwmPin::MaxINeg) =>
|
||||
get(&self.pwm.max_i_neg0),
|
||||
(0, PwmPin::MaxV) =>
|
||||
get(&self.pwm.max_v0),
|
||||
(1, PwmPin::MaxIPos) =>
|
||||
get(&self.pwm.max_i_pos1),
|
||||
(1, PwmPin::MaxINeg) =>
|
||||
get(&self.pwm.max_i_neg1),
|
||||
(1, PwmPin::MaxV) =>
|
||||
get(&self.pwm.max_v1),
|
||||
_ =>
|
||||
unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_max_v(&mut self, channel: usize) -> ElectricPotential {
|
||||
let max = 4.0 * ElectricPotential::new::<volt>(3.3);
|
||||
let duty = self.get_pwm(channel, PwmPin::MaxV);
|
||||
duty * max
|
||||
ElectricPotential::new::<volt>(self.channel_state(channel).pwm_limits.max_v)
|
||||
}
|
||||
|
||||
pub fn get_max_i_pos(&mut self, channel: usize) -> (ElectricCurrent, ElectricCurrent) {
|
||||
let max = ElectricCurrent::new::<ampere>(3.0);
|
||||
let duty = self.get_pwm(channel, PwmPin::MaxIPos);
|
||||
(duty * max, max)
|
||||
pub fn get_max_i_pos(&mut self, channel: usize) -> ElectricCurrent {
|
||||
ElectricCurrent::new::<ampere>(self.channel_state(channel).pwm_limits.max_i_pos)
|
||||
}
|
||||
|
||||
pub fn get_max_i_neg(&mut self, channel: usize) -> (ElectricCurrent, ElectricCurrent) {
|
||||
let max = ElectricCurrent::new::<ampere>(3.0);
|
||||
let duty = self.get_pwm(channel, PwmPin::MaxINeg);
|
||||
(duty * max, max)
|
||||
pub fn get_max_i_neg(&mut self, channel: usize) -> ElectricCurrent {
|
||||
ElectricCurrent::new::<ampere>(self.channel_state(channel).pwm_limits.max_i_neg)
|
||||
}
|
||||
|
||||
// Get current passing through TEC
|
||||
pub fn get_tec_i(&mut self, channel: usize) -> ElectricCurrent {
|
||||
(self.read_itec(channel) - self.read_vref(channel)) / ElectricalResistance::new::<ohm>(0.4)
|
||||
let tec_i = (self.adc_read(channel, PinsAdcReadTarget::ITec, 16) - self.adc_read(channel, PinsAdcReadTarget::VREF, 16)) / ElectricalResistance::new::<ohm>(0.4);
|
||||
match self.channel_state(channel).polarity {
|
||||
Polarity::Normal => tec_i,
|
||||
Polarity::Reversed => -tec_i,
|
||||
}
|
||||
}
|
||||
|
||||
// Get voltage across TEC
|
||||
pub fn get_tec_v(&mut self, channel: usize) -> ElectricPotential {
|
||||
(self.read_tec_u_meas(channel) - ElectricPotential::new::<volt>(1.5)) * 4.0
|
||||
(self.adc_read(channel, PinsAdcReadTarget::VTec, 16) - ElectricPotential::new::<volt>(1.5)) * 4.0
|
||||
}
|
||||
|
||||
fn set_pwm(&mut self, channel: usize, pin: PwmPin, duty: f64) -> f64 {
|
||||
@ -408,28 +412,53 @@ impl Channels {
|
||||
|
||||
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_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>();
|
||||
(duty * max, max)
|
||||
}
|
||||
|
||||
pub fn set_max_i_pos(&mut self, channel: usize, max_i_pos: ElectricCurrent) -> (ElectricCurrent, ElectricCurrent) {
|
||||
let max = ElectricCurrent::new::<ampere>(3.0);
|
||||
let duty = (max_i_pos / max).get::<ratio>();
|
||||
let duty = self.set_pwm(channel, PwmPin::MaxIPos, duty);
|
||||
(duty * max, max)
|
||||
let max_i_pos = max_i_pos.min(MAX_TEC_I).max(ElectricCurrent::zero());
|
||||
let duty = (max_i_pos / MAX_TEC_I_DUTY_TO_CURRENT_RATE).get::<ratio>();
|
||||
let duty = match self.channel_state(channel).polarity {
|
||||
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>();
|
||||
(duty * MAX_TEC_I_DUTY_TO_CURRENT_RATE, max)
|
||||
}
|
||||
|
||||
pub fn set_max_i_neg(&mut self, channel: usize, max_i_neg: ElectricCurrent) -> (ElectricCurrent, ElectricCurrent) {
|
||||
let max = ElectricCurrent::new::<ampere>(3.0);
|
||||
let duty = (max_i_neg / max).get::<ratio>();
|
||||
let duty = self.set_pwm(channel, PwmPin::MaxINeg, duty);
|
||||
(duty * max, max)
|
||||
let max_i_neg = max_i_neg.min(MAX_TEC_I).max(ElectricCurrent::zero());
|
||||
let duty = (max_i_neg / MAX_TEC_I_DUTY_TO_CURRENT_RATE).get::<ratio>();
|
||||
let duty = match self.channel_state(channel).polarity {
|
||||
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>();
|
||||
(duty * MAX_TEC_I_DUTY_TO_CURRENT_RATE, max)
|
||||
}
|
||||
|
||||
pub fn set_polarity(&mut self, channel: usize, polarity: Polarity) {
|
||||
if self.channel_state(channel).polarity != polarity {
|
||||
let i_set = self.channel_state(channel).i_set;
|
||||
let max_i_pos = self.get_max_i_pos(channel);
|
||||
let max_i_neg = self.get_max_i_neg(channel);
|
||||
self.channel_state(channel).polarity = polarity;
|
||||
|
||||
self.set_i(channel, i_set);
|
||||
self.set_max_i_pos(channel, max_i_pos);
|
||||
self.set_max_i_neg(channel, max_i_neg);
|
||||
}
|
||||
}
|
||||
|
||||
fn report(&mut self, channel: usize) -> Report {
|
||||
let i_set = self.get_i(channel);
|
||||
let i_tec = self.read_itec(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);
|
||||
let state = self.channel_state(channel);
|
||||
@ -445,7 +474,7 @@ impl Channels {
|
||||
pid_engaged: state.pid_engaged,
|
||||
i_set,
|
||||
dac_value,
|
||||
dac_feedback: self.read_dac_feedback(channel),
|
||||
dac_feedback: self.adc_read(channel, PinsAdcReadTarget::DacVfb, 1),
|
||||
i_tec,
|
||||
tec_i,
|
||||
tec_u_meas: self.get_tec_v(channel),
|
||||
@ -482,10 +511,11 @@ impl Channels {
|
||||
PwmSummary {
|
||||
channel,
|
||||
center: CenterPointJson(self.channel_state(channel).center.clone()),
|
||||
i_set: (self.get_i(channel), ElectricCurrent::new::<ampere>(3.0)).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_neg: self.get_max_i_neg(channel).into(),
|
||||
i_set: self.get_i(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),
|
||||
polarity: PolarityJson(self.channel_state(channel).polarity.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
@ -511,23 +541,24 @@ 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(&mut self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
|
||||
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)
|
||||
}
|
||||
|
||||
pub fn current_abs_max_tec_i(&mut self) -> f64 {
|
||||
max_by(self.get_tec_i(0).abs().get::<ampere>(),
|
||||
self.get_tec_i(1).abs().get::<ampere>(),
|
||||
|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal))
|
||||
pub fn current_abs_max_tec_i(&mut self) -> ElectricCurrent {
|
||||
(0..CHANNELS)
|
||||
.map(|channel| self.get_tec_i(channel).abs())
|
||||
.max_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal))
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@ -566,15 +597,18 @@ impl Serialize for CenterPointJson {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct PwmSummaryField<T: Serialize> {
|
||||
value: T,
|
||||
max: T,
|
||||
}
|
||||
pub struct PolarityJson(Polarity);
|
||||
|
||||
impl<T: Serialize> From<(T, T)> for PwmSummaryField<T> {
|
||||
fn from((value, max): (T, T)) -> Self {
|
||||
PwmSummaryField { value, max }
|
||||
// used in JSON encoding, not for config
|
||||
impl Serialize for PolarityJson {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(match self.0 {
|
||||
Polarity::Normal => "normal",
|
||||
Polarity::Reversed => "reversed",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -582,10 +616,11 @@ impl<T: Serialize> From<(T, T)> for PwmSummaryField<T> {
|
||||
pub struct PwmSummary {
|
||||
channel: usize,
|
||||
center: CenterPointJson,
|
||||
i_set: PwmSummaryField<ElectricCurrent>,
|
||||
max_v: PwmSummaryField<ElectricPotential>,
|
||||
max_i_pos: PwmSummaryField<ElectricCurrent>,
|
||||
max_i_neg: PwmSummaryField<ElectricCurrent>,
|
||||
i_set: ElectricCurrent,
|
||||
max_v: ElectricPotential,
|
||||
max_i_pos: ElectricCurrent,
|
||||
max_i_neg: ElectricCurrent,
|
||||
polarity: PolarityJson,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@ -595,7 +630,7 @@ pub struct PostFilterSummary {
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct SteinhartHartSummary {
|
||||
pub struct BParameterSummary {
|
||||
channel: usize,
|
||||
params: steinhart_hart::Parameters,
|
||||
params: b_parameter::Parameters,
|
||||
}
|
||||
|
@ -11,7 +11,8 @@ use super::{
|
||||
CenterPoint,
|
||||
PidParameter,
|
||||
PwmPin,
|
||||
ShParameter
|
||||
BpParameter,
|
||||
Polarity
|
||||
},
|
||||
ad7172,
|
||||
CHANNEL_CONFIG_KEY,
|
||||
@ -22,7 +23,6 @@ use super::{
|
||||
config::ChannelConfig,
|
||||
dfu,
|
||||
flash_store::FlashStore,
|
||||
session::Session,
|
||||
FanCtrl,
|
||||
hw_rev::HWRev,
|
||||
};
|
||||
@ -87,16 +87,6 @@ fn send_line(socket: &mut TcpSocket, data: &[u8]) -> bool {
|
||||
|
||||
impl Handler {
|
||||
|
||||
fn reporting(socket: &mut TcpSocket) -> Result<Handler, Error> {
|
||||
send_line(socket, b"{}");
|
||||
Ok(Handler::Handled)
|
||||
}
|
||||
|
||||
fn show_report_mode(socket: &mut TcpSocket, session: &Session) -> Result<Handler, Error> {
|
||||
let _ = writeln!(socket, "{{ \"report\": {:?} }}", session.reporting());
|
||||
Ok(Handler::Handled)
|
||||
}
|
||||
|
||||
fn show_report(socket: &mut TcpSocket, channels: &mut Channels) -> Result<Handler, Error> {
|
||||
match channels.reports_json() {
|
||||
Ok(buf) => {
|
||||
@ -139,13 +129,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::ReportError);
|
||||
}
|
||||
@ -181,6 +171,12 @@ impl Handler {
|
||||
Ok(Handler::Handled)
|
||||
}
|
||||
|
||||
fn set_polarity(socket: &mut TcpSocket, channels: &mut Channels, channel: usize, polarity: Polarity) -> Result<Handler, Error> {
|
||||
channels.set_polarity(channel, polarity);
|
||||
send_line(socket, b"{}");
|
||||
Ok(Handler::Handled)
|
||||
}
|
||||
|
||||
fn set_pwm (socket: &mut TcpSocket, channels: &mut Channels, channel: usize, pin: PwmPin, value: f64) -> Result<Handler, Error> {
|
||||
match pin {
|
||||
PwmPin::ISet => {
|
||||
@ -207,11 +203,11 @@ impl Handler {
|
||||
}
|
||||
|
||||
fn set_center_point(socket: &mut TcpSocket, channels: &mut Channels, channel: usize, center: CenterPoint) -> Result<Handler, Error> {
|
||||
let i_tec = channels.get_i(channel);
|
||||
let i_set = channels.get_i(channel);
|
||||
let state = channels.channel_state(channel);
|
||||
state.center = center;
|
||||
if !state.pid_engaged {
|
||||
channels.set_i(channel, i_tec);
|
||||
channels.set_i(channel, i_set);
|
||||
}
|
||||
send_line(socket, b"{}");
|
||||
Ok(Handler::Handled)
|
||||
@ -238,13 +234,13 @@ impl Handler {
|
||||
Ok(Handler::Handled)
|
||||
}
|
||||
|
||||
fn set_steinhart_hart (socket: &mut TcpSocket, channels: &mut Channels, channel: usize, parameter: ShParameter, value: f64) -> Result<Handler, Error> {
|
||||
let sh = &mut channels.channel_state(channel).sh;
|
||||
use super::command_parser::ShParameter::*;
|
||||
fn set_b_parameter (socket: &mut TcpSocket, channels: &mut Channels, channel: usize, parameter: BpParameter, value: f64) -> Result<Handler, Error> {
|
||||
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 = value,
|
||||
R0 => bp.r0 = ElectricalResistance::new::<ohm>(value),
|
||||
}
|
||||
send_line(socket, b"{}");
|
||||
Ok(Handler::Handled)
|
||||
@ -345,7 +341,7 @@ impl Handler {
|
||||
|
||||
fn set_fan(socket: &mut TcpSocket, fan_pwm: u32, fan_ctrl: &mut FanCtrl) -> Result<Handler, Error> {
|
||||
if !fan_ctrl.fan_available() {
|
||||
send_line(socket, b"{ \"warning\": \"this thermostat doesn't have fan!\" }");
|
||||
send_line(socket, b"{ \"warning\": \"this thermostat doesn't have a fan!\" }");
|
||||
return Ok(Handler::Handled);
|
||||
}
|
||||
fan_ctrl.set_auto_mode(false);
|
||||
@ -374,7 +370,7 @@ impl Handler {
|
||||
|
||||
fn fan_auto(socket: &mut TcpSocket, fan_ctrl: &mut FanCtrl) -> Result<Handler, Error> {
|
||||
if !fan_ctrl.fan_available() {
|
||||
send_line(socket, b"{ \"warning\": \"this thermostat doesn't have fan!\" }");
|
||||
send_line(socket, b"{ \"warning\": \"this thermostat doesn't have a fan!\" }");
|
||||
return Ok(Handler::Handled);
|
||||
}
|
||||
fan_ctrl.set_auto_mode(true);
|
||||
@ -412,22 +408,21 @@ impl Handler {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_command(command: Command, socket: &mut TcpSocket, channels: &mut Channels, session: &Session, store: &mut FlashStore, ipv4_config: &mut Ipv4Config, fan_ctrl: &mut FanCtrl, hwrev: HWRev) -> Result<Self, Error> {
|
||||
pub fn handle_command(command: Command, socket: &mut TcpSocket, channels: &mut Channels, store: &mut FlashStore, ipv4_config: &mut Ipv4Config, fan_ctrl: &mut FanCtrl, hwrev: HWRev) -> Result<Self, Error> {
|
||||
match command {
|
||||
Command::Quit => Ok(Handler::CloseSocket),
|
||||
Command::Reporting(_reporting) => Handler::reporting(socket),
|
||||
Command::Show(ShowCommand::Reporting) => Handler::show_report_mode(socket, session),
|
||||
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::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 } => Handler::set_polarity(socket, channels, channel, polarity),
|
||||
Command::Pwm { channel, pin, value } => Handler::set_pwm(socket, channels, channel, pin, value),
|
||||
Command::CenterPoint { channel, center } => Handler::set_center_point(socket, channels, channel, center),
|
||||
Command::Pid { channel, parameter, value } => Handler::set_pid(socket, channels, channel, parameter, value),
|
||||
Command::SteinhartHart { channel, parameter, value } => Handler::set_steinhart_hart(socket, channels, channel, parameter, value),
|
||||
Command::BParameter { channel, parameter, value } => Handler::set_b_parameter(socket, channels, channel, parameter, value),
|
||||
Command::PostFilter { channel, rate: None } => Handler::reset_post_filter(socket, channels, channel),
|
||||
Command::PostFilter { channel, rate: Some(rate) } => Handler::set_post_filter(socket, channels, channel, rate),
|
||||
Command::Load { channel } => Handler::load_channel(socket, channels, store, channel),
|
||||
|
@ -96,10 +96,9 @@ pub struct Ipv4Config {
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum ShowCommand {
|
||||
Input,
|
||||
Reporting,
|
||||
Pwm,
|
||||
Pid,
|
||||
SteinhartHart,
|
||||
BParameter,
|
||||
PostFilter,
|
||||
Ipv4,
|
||||
}
|
||||
@ -114,9 +113,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,
|
||||
@ -136,6 +135,12 @@ pub enum CenterPoint {
|
||||
Override(f32),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum Polarity {
|
||||
Normal,
|
||||
Reversed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Command {
|
||||
Quit,
|
||||
@ -148,7 +153,6 @@ pub enum Command {
|
||||
Reset,
|
||||
Ipv4(Ipv4Config),
|
||||
Show(ShowCommand),
|
||||
Reporting(bool),
|
||||
/// PWM parameter setting
|
||||
Pwm {
|
||||
channel: usize,
|
||||
@ -159,6 +163,10 @@ pub enum Command {
|
||||
PwmPid {
|
||||
channel: usize,
|
||||
},
|
||||
PwmPolarity {
|
||||
channel: usize,
|
||||
polarity: Polarity,
|
||||
},
|
||||
CenterPoint {
|
||||
channel: usize,
|
||||
center: CenterPoint,
|
||||
@ -169,9 +177,9 @@ pub enum Command {
|
||||
parameter: PidParameter,
|
||||
value: f64,
|
||||
},
|
||||
SteinhartHart {
|
||||
BParameter {
|
||||
channel: usize,
|
||||
parameter: ShParameter,
|
||||
parameter: BpParameter,
|
||||
value: f64,
|
||||
},
|
||||
PostFilter {
|
||||
@ -233,12 +241,6 @@ fn float(input: &[u8]) -> IResult<&[u8], Result<f64, Error>> {
|
||||
Ok((input, result))
|
||||
}
|
||||
|
||||
fn off_on(input: &[u8]) -> IResult<&[u8], bool> {
|
||||
alt((value(false, tag("off")),
|
||||
value(true, tag("on"))
|
||||
))(input)
|
||||
}
|
||||
|
||||
fn channel(input: &[u8]) -> IResult<&[u8], usize> {
|
||||
map(one_of("01"), |c| (c as usize) - ('0' as usize))(input)
|
||||
}
|
||||
@ -246,24 +248,8 @@ fn channel(input: &[u8]) -> IResult<&[u8], usize> {
|
||||
fn report(input: &[u8]) -> IResult<&[u8], Command> {
|
||||
preceded(
|
||||
tag("report"),
|
||||
alt((
|
||||
preceded(
|
||||
whitespace,
|
||||
preceded(
|
||||
tag("mode"),
|
||||
alt((
|
||||
preceded(
|
||||
whitespace,
|
||||
// `report mode <on | off>` - Switch repoting mode
|
||||
map(off_on, Command::Reporting)
|
||||
),
|
||||
// `report mode` - Show current reporting state
|
||||
value(Command::Show(ShowCommand::Reporting), end)
|
||||
))
|
||||
)),
|
||||
// `report` - Report once
|
||||
value(Command::Show(ShowCommand::Input), end)
|
||||
))
|
||||
)(input)
|
||||
}
|
||||
|
||||
@ -321,6 +307,18 @@ fn pwm_pid(input: &[u8]) -> IResult<&[u8], ()> {
|
||||
value((), tag("pid"))(input)
|
||||
}
|
||||
|
||||
fn pwm_polarity(input: &[u8]) -> IResult<&[u8], Polarity> {
|
||||
preceded(
|
||||
tag("polarity"),
|
||||
preceded(
|
||||
whitespace,
|
||||
alt((value(Polarity::Normal, tag("normal")),
|
||||
value(Polarity::Reversed, tag("reversed"))
|
||||
))
|
||||
)
|
||||
)(input)
|
||||
}
|
||||
|
||||
fn pwm(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
|
||||
let (input, _) = tag("pwm")(input)?;
|
||||
alt((
|
||||
@ -333,6 +331,10 @@ fn pwm(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
|
||||
let (input, ()) = pwm_pid(input)?;
|
||||
Ok((input, Ok(Command::PwmPid { channel })))
|
||||
},
|
||||
|input| {
|
||||
let (input, polarity) = pwm_polarity(input)?;
|
||||
Ok((input, Ok(Command::PwmPolarity { channel, polarity })))
|
||||
},
|
||||
|input| {
|
||||
let (input, config) = pwm_setup(input)?;
|
||||
match config {
|
||||
@ -400,31 +402,31 @@ 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"))
|
||||
alt((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 { channel, parameter, value });
|
||||
.map(|value| Command::BParameter { channel, parameter, value });
|
||||
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
|
||||
b_parameter_parameter
|
||||
),
|
||||
value(Ok(Command::Show(ShowCommand::SteinhartHart)), end)
|
||||
value(Ok(Command::Show(ShowCommand::BParameter)), end)
|
||||
))(input)
|
||||
}
|
||||
|
||||
@ -594,7 +596,7 @@ fn command(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
|
||||
pwm,
|
||||
center_point,
|
||||
pid,
|
||||
steinhart_hart,
|
||||
b_parameter,
|
||||
postfilter,
|
||||
value(Ok(Command::Dfu), tag("dfu")),
|
||||
fan,
|
||||
@ -682,24 +684,6 @@ mod test {
|
||||
assert_eq!(command, Ok(Command::Show(ShowCommand::Input)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_report_mode() {
|
||||
let command = Command::parse(b"report mode");
|
||||
assert_eq!(command, Ok(Command::Show(ShowCommand::Reporting)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_report_mode_on() {
|
||||
let command = Command::parse(b"report mode on");
|
||||
assert_eq!(command, Ok(Command::Reporting(true)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_report_mode_off() {
|
||||
let command = Command::parse(b"report mode off");
|
||||
assert_eq!(command, Ok(Command::Reporting(false)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_pwm_i_set() {
|
||||
let command = Command::parse(b"pwm 1 i_set 16383");
|
||||
@ -710,6 +694,15 @@ mod test {
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_pwm_polarity() {
|
||||
let command = Command::parse(b"pwm 0 polarity reversed");
|
||||
assert_eq!(command, Ok(Command::PwmPolarity {
|
||||
channel: 0,
|
||||
polarity: Polarity::Reversed,
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_pwm_pid() {
|
||||
let command = Command::parse(b"pwm 0 pid");
|
||||
@ -765,17 +758,17 @@ 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");
|
||||
assert_eq!(command, Ok(Command::SteinhartHart {
|
||||
fn parse_b_parameter_set() {
|
||||
let command = Command::parse(b"b-p 1 t0 23.05");
|
||||
assert_eq!(command, Ok(Command::BParameter {
|
||||
channel: 1,
|
||||
parameter: ShParameter::T0,
|
||||
parameter: BpParameter::T0,
|
||||
value: 23.05,
|
||||
}));
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
use num_traits::Zero;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use uom::si::{
|
||||
electric_potential::volt,
|
||||
@ -7,9 +8,9 @@ use uom::si::{
|
||||
use crate::{
|
||||
ad7172::PostFilter,
|
||||
channels::Channels,
|
||||
command_parser::CenterPoint,
|
||||
command_parser::{CenterPoint, Polarity},
|
||||
pid,
|
||||
steinhart_hart,
|
||||
b_parameter,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
@ -18,7 +19,9 @@ pub struct ChannelConfig {
|
||||
pid: pid::Parameters,
|
||||
pid_target: f32,
|
||||
pid_engaged: bool,
|
||||
sh: steinhart_hart::Parameters,
|
||||
i_set: ElectricCurrent,
|
||||
polarity: Polarity,
|
||||
bp: b_parameter::Parameters,
|
||||
pwm: PwmLimits,
|
||||
/// uses variant `PostFilter::Invalid` instead of `None` to save space
|
||||
adc_postfilter: PostFilter,
|
||||
@ -33,12 +36,19 @@ impl ChannelConfig {
|
||||
.unwrap_or(PostFilter::Invalid);
|
||||
|
||||
let state = channels.channel_state(channel);
|
||||
let i_set = if state.pid_engaged {
|
||||
ElectricCurrent::zero()
|
||||
} else {
|
||||
state.i_set
|
||||
};
|
||||
ChannelConfig {
|
||||
center: state.center.clone(),
|
||||
pid: state.pid.parameters.clone(),
|
||||
pid_target: state.pid.target as f32,
|
||||
pid_engaged: state.pid_engaged,
|
||||
sh: state.sh.clone(),
|
||||
i_set,
|
||||
polarity: state.polarity.clone(),
|
||||
bp: state.bp.clone(),
|
||||
pwm,
|
||||
adc_postfilter,
|
||||
}
|
||||
@ -50,7 +60,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);
|
||||
|
||||
@ -59,21 +69,23 @@ impl ChannelConfig {
|
||||
adc_postfilter => Some(adc_postfilter),
|
||||
};
|
||||
let _ = channels.adc.set_postfilter(channel as u8, adc_postfilter);
|
||||
let _ = channels.set_i(channel, self.i_set);
|
||||
channels.set_polarity(channel, self.polarity.clone());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
struct PwmLimits {
|
||||
max_v: f64,
|
||||
max_i_pos: f64,
|
||||
max_i_neg: f64,
|
||||
pub struct PwmLimits {
|
||||
pub max_v: f64,
|
||||
pub max_i_pos: f64,
|
||||
pub max_i_neg: f64,
|
||||
}
|
||||
|
||||
impl PwmLimits {
|
||||
pub fn new(channels: &mut Channels, channel: usize) -> Self {
|
||||
let max_v = channels.get_max_v(channel);
|
||||
let (max_i_pos, _) = channels.get_max_i_pos(channel);
|
||||
let (max_i_neg, _) = channels.get_max_i_neg(channel);
|
||||
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>(),
|
||||
|
@ -4,17 +4,18 @@ use stm32f4xx_hal::{
|
||||
pwm::{self, PwmChannels},
|
||||
pac::TIM8,
|
||||
};
|
||||
|
||||
use uom::si::{
|
||||
f64::ElectricCurrent,
|
||||
electric_current::ampere,
|
||||
};
|
||||
use crate::{
|
||||
hw_rev::HWSettings,
|
||||
command_handler::JsonBuffer,
|
||||
channels::MAX_TEC_I,
|
||||
};
|
||||
|
||||
pub type FanPin = PwmChannels<TIM8, pwm::C4>;
|
||||
|
||||
// as stated in the schematics
|
||||
const MAX_TEC_I: f32 = 3.0;
|
||||
|
||||
const MAX_USER_FAN_PWM: f32 = 100.0;
|
||||
const MIN_USER_FAN_PWM: f32 = 1.0;
|
||||
|
||||
@ -50,10 +51,10 @@ impl FanCtrl {
|
||||
fan_ctrl
|
||||
}
|
||||
|
||||
pub fn cycle(&mut self, abs_max_tec_i: f32) {
|
||||
self.abs_max_tec_i = abs_max_tec_i;
|
||||
pub fn cycle(&mut self, abs_max_tec_i: ElectricCurrent) {
|
||||
self.abs_max_tec_i = abs_max_tec_i.get::<ampere>() as f32;
|
||||
if self.fan_auto && self.hw_settings.fan_available {
|
||||
let scaled_current = self.abs_max_tec_i / MAX_TEC_I;
|
||||
let scaled_current = self.abs_max_tec_i / MAX_TEC_I.get::<ampere>() as f32;
|
||||
// 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;
|
||||
self.set_pwm(pwm);
|
||||
|
24
src/main.rs
24
src/main.rs
@ -41,7 +41,7 @@ mod command_parser;
|
||||
use command_parser::Ipv4Config;
|
||||
mod timer;
|
||||
mod pid;
|
||||
mod steinhart_hart;
|
||||
mod b_parameter;
|
||||
mod channels;
|
||||
use channels::{CHANNELS, Channels};
|
||||
mod channel;
|
||||
@ -180,12 +180,9 @@ fn main() -> ! {
|
||||
loop {
|
||||
let mut new_ipv4_config = None;
|
||||
let instant = Instant::from_millis(i64::from(timer::now()));
|
||||
let updated_channel = channels.poll_adc(instant);
|
||||
if let Some(channel) = updated_channel {
|
||||
server.for_each(|_, session| session.set_report_pending(channel.into()));
|
||||
}
|
||||
channels.poll_adc(instant);
|
||||
|
||||
fan_ctrl.cycle(channels.current_abs_max_tec_i() as f32);
|
||||
fan_ctrl.cycle(channels.current_abs_max_tec_i());
|
||||
|
||||
if channels.pid_engaged() {
|
||||
leds.g3.on();
|
||||
@ -216,7 +213,7 @@ fn main() -> ! {
|
||||
// Do nothing and feed more data to the line reader in the next loop cycle.
|
||||
Ok(SessionInput::Nothing) => {}
|
||||
Ok(SessionInput::Command(command)) => {
|
||||
match Handler::handle_command(command, &mut socket, &mut channels, session, &mut store, &mut ipv4_config, &mut fan_ctrl, hwrev) {
|
||||
match Handler::handle_command(command, &mut socket, &mut channels, &mut store, &mut ipv4_config, &mut fan_ctrl, hwrev) {
|
||||
Ok(Handler::NewIPV4(ip)) => new_ipv4_config = Some(ip),
|
||||
Ok(Handler::Handled) => {},
|
||||
Ok(Handler::CloseSocket) => socket.close(),
|
||||
@ -231,19 +228,6 @@ fn main() -> ! {
|
||||
Err(_) =>
|
||||
socket.close(),
|
||||
}
|
||||
} else if socket.can_send() {
|
||||
if let Some(channel) = session.is_report_pending() {
|
||||
match channels.reports_json() {
|
||||
Ok(buf) => {
|
||||
send_line(&mut socket, &buf[..]);
|
||||
session.mark_report_sent(channel);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("unable to serialize report: {:?}", e);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
@ -54,15 +54,13 @@ impl Controller {
|
||||
// + x0 * (kp + ki + kd)
|
||||
// - x1 * (kp + 2kd)
|
||||
// + x2 * kd
|
||||
// + kp * (u0 - u1)
|
||||
// y0 = clip(y0', ymin, ymax)
|
||||
pub fn update(&mut self, input: f64) -> f64 {
|
||||
|
||||
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)
|
||||
- self.x1 * f64::from(self.parameters.kp + 2.0 * self.parameters.kd)
|
||||
+ self.x2 * f64::from(self.parameters.kd)
|
||||
+ f64::from(self.parameters.kp) * (self.target - self.u1);
|
||||
+ self.x2 * f64::from(self.parameters.kd);
|
||||
if output < self.parameters.output_min.into() {
|
||||
output = self.parameters.output_min.into();
|
||||
}
|
||||
|
34
src/pins.rs
34
src/pins.rs
@ -61,27 +61,37 @@ pub trait ChannelPins {
|
||||
type DacSync: OutputPin;
|
||||
type Shdn: OutputPin;
|
||||
type VRefPin;
|
||||
type ItecPin;
|
||||
type ITecPin;
|
||||
type DacFeedbackPin;
|
||||
type TecUMeasPin;
|
||||
}
|
||||
|
||||
pub enum Channel0VRef {
|
||||
Analog(PA0<Analog>),
|
||||
Disabled(PA0<Input<Floating>>),
|
||||
}
|
||||
|
||||
impl ChannelPins for Channel0 {
|
||||
type DacSpi = Dac0Spi;
|
||||
type DacSync = PE4<Output<PushPull>>;
|
||||
type Shdn = PE10<Output<PushPull>>;
|
||||
type VRefPin = PA0<Analog>;
|
||||
type ItecPin = PA6<Analog>;
|
||||
type VRefPin = Channel0VRef;
|
||||
type ITecPin = PA6<Analog>;
|
||||
type DacFeedbackPin = PA4<Analog>;
|
||||
type TecUMeasPin = PC2<Analog>;
|
||||
}
|
||||
|
||||
pub enum Channel1VRef {
|
||||
Analog(PA3<Analog>),
|
||||
Disabled(PA3<Input<Floating>>),
|
||||
}
|
||||
|
||||
impl ChannelPins for Channel1 {
|
||||
type DacSpi = Dac1Spi;
|
||||
type DacSync = PF6<Output<PushPull>>;
|
||||
type Shdn = PE15<Output<PushPull>>;
|
||||
type VRefPin = PA3<Analog>;
|
||||
type ItecPin = PB0<Analog>;
|
||||
type VRefPin = Channel1VRef;
|
||||
type ITecPin = PB0<Analog>;
|
||||
type DacFeedbackPin = PA5<Analog>;
|
||||
type TecUMeasPin = PC3<Analog>;
|
||||
}
|
||||
@ -98,7 +108,7 @@ pub struct ChannelPinSet<C: ChannelPins> {
|
||||
pub dac_sync: C::DacSync,
|
||||
pub shdn: C::Shdn,
|
||||
pub vref_pin: C::VRefPin,
|
||||
pub itec_pin: C::ItecPin,
|
||||
pub itec_pin: C::ITecPin,
|
||||
pub dac_feedback_pin: C::DacFeedbackPin,
|
||||
pub tec_u_meas_pin: C::TecUMeasPin,
|
||||
}
|
||||
@ -150,13 +160,17 @@ impl Pins {
|
||||
gpioe.pe13, gpioe.pe14
|
||||
);
|
||||
|
||||
let hwrev = HWRev::detect_hw_rev(&HWRevPins {hwrev0: gpiod.pd0, hwrev1: gpiod.pd1,
|
||||
hwrev2: gpiod.pd2, hwrev3: gpiod.pd3});
|
||||
let hw_settings = hwrev.settings();
|
||||
|
||||
let (dac0_spi, dac0_sync) = Self::setup_dac0(
|
||||
clocks, spi4,
|
||||
gpioe.pe2, gpioe.pe4, gpioe.pe6
|
||||
);
|
||||
let mut shdn0 = gpioe.pe10.into_push_pull_output();
|
||||
let _ = shdn0.set_low();
|
||||
let vref0_pin = gpioa.pa0.into_analog();
|
||||
let vref0_pin = if hwrev.major > 2 {Channel0VRef::Analog(gpioa.pa0.into_analog())} else {Channel0VRef::Disabled(gpioa.pa0)};
|
||||
let itec0_pin = gpioa.pa6.into_analog();
|
||||
let dac_feedback0_pin = gpioa.pa4.into_analog();
|
||||
let tec_u_meas0_pin = gpioc.pc2.into_analog();
|
||||
@ -176,7 +190,7 @@ impl Pins {
|
||||
);
|
||||
let mut shdn1 = gpioe.pe15.into_push_pull_output();
|
||||
let _ = shdn1.set_low();
|
||||
let vref1_pin = gpioa.pa3.into_analog();
|
||||
let vref1_pin = if hwrev.major > 2 {Channel1VRef::Analog(gpioa.pa3.into_analog())} else {Channel1VRef::Disabled(gpioa.pa3)};
|
||||
let itec1_pin = gpiob.pb0.into_analog();
|
||||
let dac_feedback1_pin = gpioa.pa5.into_analog();
|
||||
let tec_u_meas1_pin = gpioc.pc3.into_analog();
|
||||
@ -198,10 +212,6 @@ impl Pins {
|
||||
channel1,
|
||||
};
|
||||
|
||||
let hwrev = HWRev::detect_hw_rev(&HWRevPins {hwrev0: gpiod.pd0, hwrev1: gpiod.pd1,
|
||||
hwrev2: gpiod.pd2, hwrev3: gpiod.pd3});
|
||||
let hw_settings = hwrev.settings();
|
||||
|
||||
let leds = Leds::new(gpiod.pd9, gpiod.pd10.into_push_pull_output(), gpiod.pd11.into_push_pull_output());
|
||||
|
||||
let eeprom_scl = gpiob.pb8.into_alternate().set_open_drain();
|
||||
|
@ -1,5 +1,4 @@
|
||||
use super::command_parser::{Command, Error as ParserError};
|
||||
use super::channels::CHANNELS;
|
||||
|
||||
const MAX_LINE_LEN: usize = 64;
|
||||
|
||||
@ -53,8 +52,6 @@ impl From<Result<Command, ParserError>> for SessionInput {
|
||||
|
||||
pub struct Session {
|
||||
reader: LineReader,
|
||||
reporting: bool,
|
||||
report_pending: [bool; CHANNELS],
|
||||
}
|
||||
|
||||
impl Default for Session {
|
||||
@ -67,43 +64,11 @@ impl Session {
|
||||
pub fn new() -> Self {
|
||||
Session {
|
||||
reader: LineReader::new(),
|
||||
reporting: false,
|
||||
report_pending: [false; CHANNELS],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.reader = LineReader::new();
|
||||
self.reporting = false;
|
||||
self.report_pending = [false; CHANNELS];
|
||||
}
|
||||
|
||||
pub fn reporting(&self) -> bool {
|
||||
self.reporting
|
||||
}
|
||||
|
||||
pub fn set_report_pending(&mut self, channel: usize) {
|
||||
if self.reporting {
|
||||
self.report_pending[channel] = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_report_pending(&self) -> Option<usize> {
|
||||
if ! self.reporting {
|
||||
None
|
||||
} else {
|
||||
self.report_pending.iter()
|
||||
.enumerate()
|
||||
.fold(None, |result, (channel, report_pending)| {
|
||||
result.or_else(|| {
|
||||
if *report_pending { Some(channel) } else { None }
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mark_report_sent(&mut self, channel: usize) {
|
||||
self.report_pending[channel] = false;
|
||||
}
|
||||
|
||||
pub fn feed(&mut self, buf: &[u8]) -> (usize, SessionInput) {
|
||||
@ -114,12 +79,6 @@ impl Session {
|
||||
match line {
|
||||
Some(line) => {
|
||||
let command = Command::parse(&line);
|
||||
match command {
|
||||
Ok(Command::Reporting(reporting)) => {
|
||||
self.reporting = reporting;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
return (buf_bytes, command.into());
|
||||
}
|
||||
None => {}
|
||||
|
Loading…
Reference in New Issue
Block a user