Compare commits

..

3 Commits

Author SHA1 Message Date
f53eb3eea5 Add pre-commit hooks 2024-08-16 15:26:23 +08:00
8d0aad31d4 clippy 2024-08-16 15:26:23 +08:00
41f558819b cargo fmt 2024-08-16 15:26:23 +08:00
27 changed files with 729 additions and 821 deletions

3
.gitignore vendored
View File

@ -1,5 +1,2 @@
target/ target/
result result
*.bin
__pycache__/

24
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,24 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
default_stages: [commit]
repos:
- repo: local
hooks:
- id: cargo-fmt
name: cargo format
entry: cargo
language: system
types: [file, rust]
pass_filenames: false
description: Runs cargo fmt on the codebase.
args: [fmt]
- id: cargo-clippy
name: cargo clippy
entry: cargo
language: system
types: [file, rust]
pass_filenames: false
description: Runs cargo clippy on the codebase.
args: [clippy]

View File

@ -23,7 +23,16 @@ cargo build --release
The resulting ELF file will be located under `target/thumbv7em-none-eabihf/release/thermostat`. The resulting ELF file will be located under `target/thumbv7em-none-eabihf/release/thermostat`.
Alternatively, you can install the Rust toolchain without Nix using rustup; see the `rust` variable in `flake.nix` to determine which Rust version to use. Alternatively, you can install the Rust toolchain without Nix using rustup; see the Rust manifest file pulled in `flake.nix` to determine which Rust version to use.
#### Pre-Commit Hooks
You are strongly recommended to use the provided pre-commit hooks to automatically reformat files and check for non-optimal Rust practices using Clippy. Run pre-commit install to install the hook and pre-commit will automatically run cargo fmt and cargo clippy for you.
Several things to note:
If cargo fmt or cargo clippy returns an error, the pre-commit hook will fail. You should fix all errors before trying to commit again.
If cargo fmt reformats some files, the pre-commit hook will also fail. You should review the changes and, if satisfied, try to commit again.
## Debugging ## Debugging
@ -84,7 +93,9 @@ invalidate the first line of input.
### Reading ADC input ### Reading ADC input
ADC input data is provided in reports. Query for the latest report with the command `report`. See the *Reports* section below. Set report mode to `on` for a continuous stream of input data.
The scope of this setting is per TCP session.
### TCP commands ### TCP commands
@ -93,15 +104,16 @@ Send commands as simple text string terminated by `\n`. Responses are
formatted as line-delimited JSON. formatted as line-delimited JSON.
| Syntax | Function | | Syntax | Function |
|-------------------------------------------|-------------------------------------------------------------------------------| |----------------------------------|-------------------------------------------------------------------------------|
| `report` | Show latest report of channel parameters (see *Reports* section) | | `report` | Show current input |
| `output` | Show current output settings | | `report mode` | Show current report mode |
| `output <0/1> max_i_pos <amp>` | Set maximum positive output current, clamped to [0, 2] | | `report mode <off/on>` | Set report mode |
| `output <0/1> max_i_neg <amp>` | Set maximum negative output current, clamped to [0, 2] | | `pwm` | Show current PWM settings |
| `output <0/1> max_v <volt>` | Set maximum output voltage, clamped to [0, 4] | | `pwm <0/1> max_i_pos <amp>` | Set maximum positive output current |
| `output <0/1> i_set <amp>` | Disengage PID, set fixed output current, clamped to [-2, 2] | | `pwm <0/1> max_i_neg <amp>` | Set maximum negative output current |
| `output <0/1> polarity <normal/reversed>` | Set output current polarity, with 'normal' being the front panel polarity | | `pwm <0/1> max_v <volt>` | Set maximum output voltage |
| `output <0/1> pid` | Let output current to be controlled by the PID | | `pwm <0/1> i_set <amp>` | Disengage PID, set fixed output current |
| `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> <volt>` | Set the MAX1968 0A-centerpoint to the specified fixed voltage |
| `center <0/1> vref` | Set the MAX1968 0A-centerpoint to measure from VREF | | `center <0/1> vref` | Set the MAX1968 0A-centerpoint to measure from VREF |
| `pid` | Show PID configuration | | `pid` | Show PID configuration |
@ -111,8 +123,8 @@ formatted as line-delimited JSON.
| `pid <0/1> kd <value>` | Set differential gain | | `pid <0/1> kd <value>` | Set differential gain |
| `pid <0/1> output_min <amp>` | Set mininum output | | `pid <0/1> output_min <amp>` | Set mininum output |
| `pid <0/1> output_max <amp>` | Set maximum output | | `pid <0/1> output_max <amp>` | Set maximum output |
| `b-p` | Show B-Parameter equation parameters | | `s-h` | Show Steinhart-Hart equation parameters |
| `b-p <0/1> <t0/b/r0> <value>` | Set B-Parameter for a channel | | `s-h <0/1> <t0/b/r0> <value>` | Set Steinhart-Hart parameter for a channel |
| `postfilter` | Show postfilter settings | | `postfilter` | Show postfilter settings |
| `postfilter <0/1> off` | Disable postfilter | | `postfilter <0/1> off` | Disable postfilter |
| `postfilter <0/1> rate <rate>` | Set postfilter output data rate | | `postfilter <0/1> rate <rate>` | Set postfilter output data rate |
@ -144,22 +156,22 @@ output will be truncated when USB buffers are full.
Connect the thermistor with the SENS pins of the Connect the thermistor with the SENS pins of the
device. Temperature-depending resistance is measured by the AD7172 device. Temperature-depending resistance is measured by the AD7172
ADC. To prepare conversion to a temperature, set the parameters ADC. To prepare conversion to a temperature, set the Beta parameters
for the B-Parameter equation. for the Steinhart-Hart equation.
Set the base temperature in degrees celsius for the channel 0 thermistor: Set the base temperature in degrees celsius for the channel 0 thermistor:
``` ```
b-p 0 t0 20 s-h 0 t0 20
``` ```
Set the resistance in Ohms measured at the base temperature t0: Set the resistance in Ohms measured at the base temperature t0:
``` ```
b-p 0 r0 10000 s-h 0 r0 10000
``` ```
Set the Beta parameter: Set the Beta parameter:
``` ```
b-p 0 b 3800 s-h 0 b 3800
``` ```
### 50/60 Hz filtering ### 50/60 Hz filtering
@ -183,47 +195,46 @@ 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. 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 `output <ch> polarity reversed` TCP command.
Testing heat flow direction with a low set current is recommended before installation of the TEC module. Testing heat flow direction with a low set current is recommended before installation of the TEC module.
### Limits ### Limits
Each channel has maximum value settings, for setting Each of the MAX1968 TEC driver has analog/PWM inputs for setting
output limits. output limits.
Use the `output` command to see them. Use the `pwm` command to see current settings and maximum values.
| Limit | Unit | Description | | Limit | Unit | Description |
| --- | :---: | --- | | --- | :---: | --- |
| `max_v` | Volts | Maximum voltage | | `max_v` | Volts | Maximum voltage |
| `max_i_pos` | Amperes | Maximum positive current | | `max_i_pos` | Amperes | Maximum positive current |
| `max_i_neg` | Amperes | Maximum negative 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. Example: set the maximum voltage of channel 0 to 1.5 V.
``` ```
output 0 max_v 1.5 pwm 0 max_v 1.5
``` ```
Example: set the maximum negative current of channel 0 to -2 A. Example: set the maximum negative current of channel 0 to -3 A.
``` ```
output 0 max_i_neg 2 pwm 0 max_i_neg 3
``` ```
Example: set the maximum positive current of channel 1 to 2 A. Example: set the maximum positive current of channel 1 to 3 A.
``` ```
output 1 max_i_pos 2 pwm 0 max_i_pos 3
``` ```
### Open-loop mode ### Open-loop mode
To manually control TEC output current, set a fixed output current with To manually control TEC output current, omit the limit parameter of
the `output` command. Doing so will disengage the PID control for that the `pwm` command. Doing so will disengage the PID control for that
channel. channel.
Example: set output current of channel 0 to 0 A. Example: set output current of channel 0 to 0 A.
``` ```
output 0 i_set 0 pwm 0 i_set 0
``` ```
## PID-stabilized temperature control ## PID-stabilized temperature control
@ -236,23 +247,7 @@ pid 0 target 20
Enter closed-loop mode by switching control of the TEC output current Enter closed-loop mode by switching control of the TEC output current
of channel 0 to the PID algorithm: of channel 0 to the PID algorithm:
``` ```
output 0 pid pwm 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 ## LED indicators
@ -265,17 +260,17 @@ pid 0 output_min 0.1
## Reports ## Reports
Use the bare `report` command to obtain a single report. Reports are JSON objects Use the bare `report` command to obtain a single report. Enable
continuous reporting with `report mode on`. Reports are JSON objects
with the following keys. with the following keys.
| Key | Unit | Description | | Key | Unit | Description |
| --- | :---: | --- | | --- | :---: | --- |
| `channel` | Integer | Channel `0`, or `1` | | `channel` | Integer | Channel `0`, or `1` |
| `time` | Seconds | Temperature measurement time | | `time` | Seconds | Temperature measurement time |
| `interval` | Seconds | Time elapsed since last report update on channel |
| `adc` | Volts | AD7172 input | | `adc` | Volts | AD7172 input |
| `sens` | Ohms | Thermistor resistance derived from `adc` | | `sens` | Ohms | Thermistor resistance derived from `adc` |
| `temperature` | Degrees Celsius | B-Parameter conversion result derived from `sens` | | `temperature` | Degrees Celsius | Steinhart-Hart conversion result derived from `sens` |
| `pid_engaged` | Boolean | `true` if in closed-loop mode | | `pid_engaged` | Boolean | `true` if in closed-loop mode |
| `i_set` | Amperes | TEC output current | | `i_set` | Amperes | TEC output current |
| `dac_value` | Volts | AD5680 output derived from `i_set` | | `dac_value` | Volts | AD5680 output derived from `i_set` |
@ -285,7 +280,7 @@ with the following keys.
| `tec_u_meas` | Volts | Measurement of the voltage across the TEC | | `tec_u_meas` | Volts | Measurement of the voltage across the TEC |
| `pid_output` | Amperes | PID control output | | `pid_output` | Amperes | PID control output |
Note: Prior to Thermostat hardware revision v2.2.4, 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). 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 ## PID Tuning

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 To use the Python real-time plotting utility, run
```shell ```shell
python pythermostat/pythermostat/plot.py python pytec/plot.py
``` ```
![default view](./assets/default%20view.png) ![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 ## Auto Tuning
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. 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.
To run the auto tuning utility, run To run the auto tuning utility, run
```shell ```shell
python pythermostat/pythermostat/autotune.py python pytec/autotune.py
``` ```
After some time, the auto tuning utility will output the auto tuning results, below is a sample output After some time, the auto tuning utility will output the auto tuning results, below is a sample output

View File

@ -7,17 +7,9 @@
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
outputs = outputs = { self, nixpkgs, rust-overlay }:
{
self,
nixpkgs,
rust-overlay,
}:
let let
pkgs = import nixpkgs { pkgs = import nixpkgs { system = "x86_64-linux"; overlays = [ (import rust-overlay) ]; };
system = "x86_64-linux";
overlays = [ (import rust-overlay) ];
};
rust = pkgs.rust-bin.stable."1.66.0".default.override { rust = pkgs.rust-bin.stable."1.66.0".default.override {
extensions = [ "rust-src" ]; extensions = [ "rust-src" ];
@ -57,23 +49,9 @@
dontFixup = true; dontFixup = true;
auditable = false; 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 = { packages.x86_64-linux = {
inherit thermostat pythermostat; inherit thermostat;
default = thermostat; default = thermostat;
}; };
@ -83,21 +61,13 @@
devShells.x86_64-linux.default = pkgs.mkShellNoCC { devShells.x86_64-linux.default = pkgs.mkShellNoCC {
name = "thermostat-dev-shell"; name = "thermostat-dev-shell";
packages = packages = with pkgs; [
with pkgs; rust llvm
[ openocd dfu-util
rust pre-commit
llvm ] ++ (with python3Packages; [
openocd numpy matplotlib
dfu-util
rlwrap
]
++ (with python3Packages; [
numpy
matplotlib
]); ]);
}; };
formatter.x86_64-linux = nixpkgs.legacyPackages.x86_64-linux.nixfmt-rfc-style;
}; };
} }

View File

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

11
pytec/example.py Normal file
View File

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

128
pytec/plot.py Normal file
View File

@ -0,0 +1,128 @@
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

@ -2,7 +2,6 @@ import socket
import json import json
import logging import logging
class CommandError(Exception): class CommandError(Exception):
pass pass
@ -12,16 +11,12 @@ class Client:
self._lines = [""] self._lines = [""]
self._check_zero_limits() self._check_zero_limits()
def disconnect(self):
self._socket.shutdown(socket.SHUT_RDWR)
self._socket.close()
def _check_zero_limits(self): def _check_zero_limits(self):
output_report = self.get_output() pwm_report = self.get_pwm()
for output_channel in output_report: for pwm_channel in pwm_report:
for limit in ["max_i_neg", "max_i_pos", "max_v"]: for limit in ["max_i_neg", "max_i_pos", "max_v"]:
if output_channel[limit] == 0.0: if pwm_channel[limit]["value"] == 0.0:
logging.warning("`{}` limit is set to zero on channel {}".format(limit, output_channel["channel"])) logging.warning("`{}` limit is set to zero on channel {}".format(limit, pwm_channel["channel"]))
def _read_line(self): def _read_line(self):
# read more lines # read more lines
@ -51,27 +46,25 @@ class Client:
result[int(item["channel"])] = item result[int(item["channel"])] = item
return result return result
def get_output(self): def get_pwm(self):
"""Retrieve output limits for the TEC """Retrieve PWM limits for the TEC
Example:: Example::
[{'channel': 0, [{'channel': 0,
'center': 'vref', 'center': 'vref',
'i_set': -0.02002179650216762, 'i_set': {'max': 2.9802790335151985, 'value': -0.02002179650216762},
'max_i_neg': 2.0, 'max_i_neg': {'max': 3.0, 'value': 3.0},
'max_v': 3.988, 'max_v': {'max': 5.988, 'value': 5.988},
'max_i_pos': 2.0, 'max_i_pos': {'max': 3.0, 'value': 3.0}},
'polarity': 'normal',
{'channel': 1, {'channel': 1,
'center': 'vref', 'center': 'vref',
'i_set': -0.02002179650216762, 'i_set': {'max': 2.9802790335151985, 'value': -0.02002179650216762},
'max_i_neg': 2.0, 'max_i_neg': {'max': 3.0, 'value': 3.0},
'max_v': 3.988, 'max_v': {'max': 5.988, 'value': 5.988},
'max_i_pos': 2.0} 'max_i_pos': {'max': 3.0, 'value': 3.0}}
'polarity': 'normal',
] ]
""" """
return self._get_conf("output") return self._get_conf("pwm")
def get_pid(self): def get_pid(self):
"""Retrieve PID control state """Retrieve PID control state
@ -96,14 +89,14 @@ class Client:
""" """
return self._get_conf("pid") return self._get_conf("pid")
def get_b_parameter(self): def get_steinhart_hart(self):
"""Retrieve B-Parameter equation parameters for resistance to temperature conversion """Retrieve Steinhart-Hart parameters for resistance to temperature conversion
Example:: Example::
[{'params': {'b': 3800.0, 'r0': 10000.0, 't0': 298.15}, 'channel': 0}, [{'params': {'b': 3800.0, 'r0': 10000.0, 't0': 298.15}, 'channel': 0},
{'params': {'b': 3800.0, 'r0': 10000.0, 't0': 298.15}, 'channel': 1}] {'params': {'b': 3800.0, 'r0': 10000.0, 't0': 298.15}, 'channel': 1}]
""" """
return self._get_conf("b-p") return self._get_conf("s-h")
def get_postfilter(self): def get_postfilter(self):
"""Retrieve DAC postfilter configuration """Retrieve DAC postfilter configuration
@ -114,18 +107,18 @@ class Client:
""" """
return self._get_conf("postfilter") return self._get_conf("postfilter")
def get_report(self): def report_mode(self):
"""Obtain one-time report on measurement values """Start reporting measurement values
Example of yielded data:: Example of yielded data::
{'channel': 0, {'channel': 0,
'time': 2302524, 'time': 2302524,
'interval': 0.12
'adc': 0.6199188965423515, 'adc': 0.6199188965423515,
'sens': 6138.519310282602, 'sens': 6138.519310282602,
'temperature': 36.87032392655527, 'temperature': 36.87032392655527,
'pid_engaged': True, 'pid_engaged': True,
'i_set': 2.0635816680889123, 'i_set': 2.0635816680889123,
'vref': 1.494,
'dac_value': 2.527790834044456, 'dac_value': 2.527790834044456,
'dac_feedback': 2.523, 'dac_feedback': 2.523,
'i_tec': 2.331, 'i_tec': 2.331,
@ -133,27 +126,24 @@ class Client:
'tec_u_meas': 2.5340000000000003, 'tec_u_meas': 2.5340000000000003,
'pid_output': 2.067581958092247} 'pid_output': 2.067581958092247}
""" """
return self._get_conf("report") self._command("report mode", "on")
def get_ipv4(self): while True:
"""Get the IPv4 settings of the Thermostat""" line = self._read_line()
return self._command("ipv4") if not line:
break
def get_fan(self): try:
"""Get Thermostat current fan settings""" yield json.loads(line)
return self._command("fan") except json.decoder.JSONDecodeError:
pass
def get_hwrev(self):
"""Get Thermostat hardware revision"""
return self._command("hwrev")
def set_param(self, topic, channel, field="", value=""): def set_param(self, topic, channel, field="", value=""):
"""Set configuration parameters """Set configuration parameters
Examples:: Examples::
tec.set_param("output", 0, "max_v", 2.0) tec.set_param("pwm", 0, "max_v", 2.0)
tec.set_param("pid", 1, "output_max", 2.5) tec.set_param("pid", 1, "output_max", 2.5)
tec.set_param("b-p", 0, "t0", 20.0) tec.set_param("s-h", 0, "t0", 20.0)
tec.set_param("center", 0, "vref") tec.set_param("center", 0, "vref")
tec.set_param("postfilter", 1, 21) tec.set_param("postfilter", 1, 21)
@ -168,40 +158,12 @@ class Client:
def power_up(self, channel, target): def power_up(self, channel, target):
"""Start closed-loop mode""" """Start closed-loop mode"""
self.set_param("pid", channel, "target", value=target) self.set_param("pid", channel, "target", value=target)
self.set_param("output", channel, "pid") self.set_param("pwm", channel, "pid")
def save_config(self, channel=""): def save_config(self):
"""Save current configuration to EEPROM""" """Save current configuration to EEPROM"""
self._command("save", channel) self._command("save")
if channel != "":
self._read_line() # read the extra {}
def load_config(self, channel=""): def load_config(self):
"""Load current configuration from EEPROM""" """Load current configuration from EEPROM"""
self._command("load", channel) self._command("load")
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)

12
pytec/setup.py Normal file
View File

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

View File

@ -1,13 +0,0 @@
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

@ -1,18 +0,0 @@
[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,137 +0,0 @@
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

@ -1,81 +0,0 @@
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

@ -237,10 +237,10 @@ impl<SPI: Transfer<u8, Error = E>, NSS: OutputPin, E: fmt::Debug> Adc<SPI, NSS>
Ok(()) Ok(())
} }
fn transfer( fn transfer<'w>(
&mut self, &mut self,
addr: u8, addr: u8,
reg_data: &mut [u8], reg_data: &'w mut [u8],
checksum: Option<u8>, checksum: Option<u8>,
) -> Result<Option<u8>, SPI::Error> { ) -> Result<Option<u8>, SPI::Error> {
let mut addr_buf = [addr]; let mut addr_buf = [addr];

View File

@ -1,10 +1,4 @@
use crate::{ use crate::{ad7172, command_parser::CenterPoint, pid, steinhart_hart as sh};
ad7172, b_parameter as bp,
command_parser::{CenterPoint, Polarity},
config::PwmLimits,
pid,
};
use num_traits::Zero;
use smoltcp::time::{Duration, Instant}; use smoltcp::time::{Duration, Instant};
use uom::si::{ use uom::si::{
electric_current::ampere, electric_current::ampere,
@ -29,11 +23,9 @@ pub struct ChannelState {
pub center: CenterPoint, pub center: CenterPoint,
pub dac_value: ElectricPotential, pub dac_value: ElectricPotential,
pub i_set: ElectricCurrent, pub i_set: ElectricCurrent,
pub pwm_limits: PwmLimits,
pub pid_engaged: bool, pub pid_engaged: bool,
pub pid: pid::Controller, pub pid: pid::Controller,
pub bp: bp::Parameters, pub sh: sh::Parameters,
pub polarity: Polarity,
} }
impl ChannelState { impl ChannelState {
@ -44,18 +36,12 @@ impl ChannelState {
adc_time: Instant::from_secs(0), adc_time: Instant::from_secs(0),
// default: 10 Hz // default: 10 Hz
adc_interval: Duration::from_millis(100), adc_interval: Duration::from_millis(100),
center: CenterPoint::VRef, center: CenterPoint::Vref,
dac_value: ElectricPotential::new::<volt>(0.0), dac_value: ElectricPotential::new::<volt>(0.0),
i_set: ElectricCurrent::new::<ampere>(0.0), i_set: ElectricCurrent::new::<ampere>(0.0),
pwm_limits: PwmLimits {
max_v: ElectricPotential::zero(),
max_i_pos: ElectricCurrent::zero(),
max_i_neg: ElectricCurrent::zero(),
},
pid_engaged: false, pid_engaged: false,
pid: pid::Controller::new(pid::Parameters::default()), pid: pid::Controller::new(pid::Parameters::default()),
bp: bp::Parameters::default(), sh: sh::Parameters::default(),
polarity: Polarity::Normal,
} }
} }
@ -100,7 +86,7 @@ impl ChannelState {
pub fn get_temperature(&self) -> Option<ThermodynamicTemperature> { pub fn get_temperature(&self) -> Option<ThermodynamicTemperature> {
let r = self.get_sens()?; let r = self.get_sens()?;
let temperature = self.bp.get_temperature(r); let temperature = self.sh.get_temperature(r);
Some(temperature) Some(temperature)
} }
} }

View File

@ -1,11 +1,11 @@
use crate::timer::sleep;
use crate::{ use crate::{
ad5680, ad7172, b_parameter, ad5680, ad7172,
channel::{Channel, Channel0, Channel1}, channel::{Channel, Channel0, Channel1},
channel_state::ChannelState, channel_state::ChannelState,
command_handler::JsonBuffer, command_handler::JsonBuffer,
command_parser::{CenterPoint, Polarity, PwmPin}, command_parser::{CenterPoint, PwmPin},
pins::{self, Channel0VRef, Channel1VRef}, pins::{self, Channel0VRef, Channel1VRef},
steinhart_hart,
}; };
use core::marker::PhantomData; use core::marker::PhantomData;
use heapless::{consts::U2, Vec}; use heapless::{consts::U2, Vec};
@ -23,14 +23,14 @@ use uom::si::{
}; };
pub enum PinsAdcReadTarget { pub enum PinsAdcReadTarget {
VRef, VREF,
DacVfb, DacVfb,
ITec, ITec,
VTec, VTec,
} }
pub const CHANNELS: usize = 2; pub const CHANNELS: usize = 2;
pub const R_SENSE: f64 = 0.05; const R_SENSE: f64 = 0.05;
// From design specs // From design specs
pub const MAX_TEC_I: ElectricCurrent = ElectricCurrent { pub const MAX_TEC_I: ElectricCurrent = ElectricCurrent {
@ -43,11 +43,7 @@ pub const MAX_TEC_V: ElectricPotential = ElectricPotential {
units: PhantomData, units: PhantomData,
value: 4.0, 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 // DAC chip outputs 0-5v, which is then passed through a resistor dividor to provide 0-3v range
const DAC_OUT_V_MAX: ElectricPotential = ElectricPotential { const DAC_OUT_V_MAX: ElectricPotential = ElectricPotential {
dimension: PhantomData, dimension: PhantomData,
@ -61,7 +57,7 @@ pub struct Channels {
pub adc: ad7172::Adc<pins::AdcSpi, pins::AdcNss>, pub adc: ad7172::Adc<pins::AdcSpi, pins::AdcNss>,
/// stm32f4 integrated adc /// stm32f4 integrated adc
pins_adc: pins::PinsAdc, pins_adc: pins::PinsAdc,
pub pwm: pins::PwmPins, pwm: pins::PwmPins,
} }
impl Channels { impl Channels {
@ -130,7 +126,7 @@ impl Channels {
/// calculate the TEC i_set centerpoint /// calculate the TEC i_set centerpoint
pub fn get_center(&mut self, channel: usize) -> ElectricPotential { pub fn get_center(&mut self, channel: usize) -> ElectricPotential {
match self.channel_state(channel).center { match self.channel_state(channel).center {
CenterPoint::VRef => self.adc_read(channel, PinsAdcReadTarget::VRef, 8), CenterPoint::Vref => self.adc_read(channel, PinsAdcReadTarget::VREF, 8),
CenterPoint::Override(center_point) => { CenterPoint::Override(center_point) => {
ElectricPotential::new::<volt>(center_point.into()) ElectricPotential::new::<volt>(center_point.into())
} }
@ -143,7 +139,7 @@ impl Channels {
voltage voltage
} }
pub fn get_i_set(&mut self, channel: usize) -> ElectricCurrent { pub fn get_i(&mut self, channel: usize) -> ElectricCurrent {
let i_set = self.channel_state(channel).i_set; let i_set = self.channel_state(channel).i_set;
i_set i_set
} }
@ -162,11 +158,6 @@ impl Channels {
pub fn set_i(&mut self, channel: usize, i_set: ElectricCurrent) -> ElectricCurrent { pub fn set_i(&mut self, channel: usize, i_set: ElectricCurrent) -> ElectricCurrent {
let i_set = i_set.min(MAX_TEC_I).max(-MAX_TEC_I); 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 { let vref_meas = match channel {
0 => self.channel0.vref_meas, 0 => self.channel0.vref_meas,
1 => self.channel1.vref_meas, 1 => self.channel1.vref_meas,
@ -174,10 +165,11 @@ impl Channels {
}; };
let center_point = vref_meas; let center_point = vref_meas;
let r_sense = ElectricalResistance::new::<ohm>(R_SENSE); let r_sense = ElectricalResistance::new::<ohm>(R_SENSE);
let voltage = negate * i_set * 10.0 * r_sense + center_point; let voltage = i_set * 10.0 * r_sense + center_point;
let voltage = self.set_dac(channel, voltage); let voltage = self.set_dac(channel, voltage);
let i_set = (voltage - center_point) / (10.0 * r_sense);
negate * (voltage - center_point) / (10.0 * r_sense) self.channel_state(channel).i_set = i_set;
i_set
} }
/// AN4073: ADC Reading Dispersion can be reduced through Averaging /// AN4073: ADC Reading Dispersion can be reduced through Averaging
@ -191,7 +183,7 @@ impl Channels {
match channel { match channel {
0 => { 0 => {
sample = match adc_read_target { sample = match adc_read_target {
PinsAdcReadTarget::VRef => match &self.channel0.vref_pin { PinsAdcReadTarget::VREF => match &self.channel0.vref_pin {
Channel0VRef::Analog(vref_pin) => { Channel0VRef::Analog(vref_pin) => {
for _ in (0..avg_pt).rev() { for _ in (0..avg_pt).rev() {
sample += self.pins_adc.convert( sample += self.pins_adc.convert(
@ -236,7 +228,7 @@ impl Channels {
} }
1 => { 1 => {
sample = match adc_read_target { sample = match adc_read_target {
PinsAdcReadTarget::VRef => match &self.channel1.vref_pin { PinsAdcReadTarget::VREF => match &self.channel1.vref_pin {
Channel1VRef::Analog(vref_pin) => { Channel1VRef::Analog(vref_pin) => {
for _ in (0..avg_pt).rev() { for _ in (0..avg_pt).rev() {
sample += self.pins_adc.convert( sample += self.pins_adc.convert(
@ -283,6 +275,21 @@ impl Channels {
} }
} }
pub fn read_dac_feedback_until_stable(
&mut self,
channel: usize,
tolerance: ElectricPotential,
) -> ElectricPotential {
let mut prev = self.adc_read(channel, PinsAdcReadTarget::DacVfb, 1);
loop {
let current = self.adc_read(channel, PinsAdcReadTarget::DacVfb, 1);
if (current - prev).abs() < tolerance {
return current;
}
prev = current;
}
}
/// Calibrates the DAC output to match vref of the MAX driver to reduce zero-current offset of the MAX driver output. /// Calibrates the DAC output to match vref of the MAX driver to reduce zero-current offset of the MAX driver output.
/// ///
/// The thermostat DAC applies a control voltage signal to the CTLI pin of MAX driver chip to control its output current. /// The thermostat DAC applies a control voltage signal to the CTLI pin of MAX driver chip to control its output current.
@ -309,7 +316,8 @@ impl Channels {
let mut start_value = 1; let mut start_value = 1;
let mut best_error = ElectricPotential::new::<volt>(100.0); let mut best_error = ElectricPotential::new::<volt>(100.0);
for step in (5..18).rev() { for step in (0..18).rev() {
let mut prev_value = start_value;
for value in (start_value..=ad5680::MAX_VALUE).step_by(1 << step) { for value in (start_value..=ad5680::MAX_VALUE).step_by(1 << step) {
match channel { match channel {
0 => { 0 => {
@ -320,15 +328,15 @@ impl Channels {
} }
_ => unreachable!(), _ => unreachable!(),
} }
sleep(10);
let dac_feedback = self.adc_read(channel, PinsAdcReadTarget::DacVfb, 64); let dac_feedback = self
.read_dac_feedback_until_stable(channel, ElectricPotential::new::<volt>(0.001));
let error = target_voltage - dac_feedback; let error = target_voltage - dac_feedback;
if error < ElectricPotential::new::<volt>(0.0) { if error < ElectricPotential::new::<volt>(0.0) {
break; break;
} else if error < best_error { } else if error < best_error {
best_error = error; best_error = error;
start_value = value; start_value = prev_value;
let vref = (value as f64 / ad5680::MAX_VALUE as f64) * DAC_OUT_V_MAX; let vref = (value as f64 / ad5680::MAX_VALUE as f64) * DAC_OUT_V_MAX;
match channel { match channel {
@ -337,6 +345,8 @@ impl Channels {
_ => unreachable!(), _ => unreachable!(),
} }
} }
prev_value = value;
} }
} }
@ -362,27 +372,47 @@ impl Channels {
} }
} }
pub fn get_max_v(&mut self, channel: usize) -> ElectricPotential { fn get_pwm(&self, channel: usize, pin: PwmPin) -> f64 {
self.channel_state(channel).pwm_limits.max_v 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_i_pos(&mut self, channel: usize) -> ElectricCurrent { pub fn get_max_v(&mut self, channel: usize) -> (ElectricPotential, ElectricPotential) {
self.channel_state(channel).pwm_limits.max_i_pos let max = 4.0 * ElectricPotential::new::<volt>(3.3);
let duty = self.get_pwm(channel, PwmPin::MaxV);
(duty * max, MAX_TEC_V)
} }
pub fn get_max_i_neg(&mut self, channel: usize) -> ElectricCurrent { pub fn get_max_i_pos(&mut self, channel: usize) -> (ElectricCurrent, ElectricCurrent) {
self.channel_state(channel).pwm_limits.max_i_neg let max = ElectricCurrent::new::<ampere>(3.0);
let duty = self.get_pwm(channel, PwmPin::MaxIPos);
(duty * max, MAX_TEC_I)
}
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_TEC_I)
} }
// Get current passing through TEC // Get current passing through TEC
pub fn get_tec_i(&mut self, channel: usize) -> ElectricCurrent { pub fn get_tec_i(&mut self, channel: usize) -> ElectricCurrent {
let tec_i = (self.adc_read(channel, PinsAdcReadTarget::ITec, 16) (self.adc_read(channel, PinsAdcReadTarget::ITec, 16)
- self.adc_read(channel, PinsAdcReadTarget::VRef, 16)) - self.adc_read(channel, PinsAdcReadTarget::VREF, 16))
/ ElectricalResistance::new::<ohm>(0.4); / ElectricalResistance::new::<ohm>(0.4)
match self.channel_state(channel).polarity {
Polarity::Normal => tec_i,
Polarity::Reversed => -tec_i,
}
} }
// Get voltage across TEC // Get voltage across TEC
@ -416,10 +446,8 @@ impl Channels {
max_v: ElectricPotential, max_v: ElectricPotential,
) -> (ElectricPotential, ElectricPotential) { ) -> (ElectricPotential, ElectricPotential) {
let max = 4.0 * ElectricPotential::new::<volt>(3.3); 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.min(MAX_TEC_V).max(ElectricPotential::zero()) / max).get::<ratio>();
let duty = (max_v / max).get::<ratio>();
let duty = self.set_pwm(channel, PwmPin::MaxV, duty); let duty = self.set_pwm(channel, PwmPin::MaxV, duty);
self.channel_state(channel).pwm_limits.max_v = max_v;
(duty * max, max) (duty * max, max)
} }
@ -429,14 +457,9 @@ impl Channels {
max_i_pos: ElectricCurrent, max_i_pos: ElectricCurrent,
) -> (ElectricCurrent, ElectricCurrent) { ) -> (ElectricCurrent, ElectricCurrent) {
let max = ElectricCurrent::new::<ampere>(3.0); let max = ElectricCurrent::new::<ampere>(3.0);
let max_i_pos = max_i_pos.min(MAX_TEC_I).max(ElectricCurrent::zero()); let duty = (max_i_pos.min(MAX_TEC_I).max(ElectricCurrent::zero()) / max).get::<ratio>();
let duty = (max_i_pos / MAX_TEC_I_DUTY_TO_CURRENT_RATE).get::<ratio>(); let duty = self.set_pwm(channel, PwmPin::MaxIPos, duty);
let duty = match self.channel_state(channel).polarity { (duty * max, max)
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;
(duty * MAX_TEC_I_DUTY_TO_CURRENT_RATE, max)
} }
pub fn set_max_i_neg( pub fn set_max_i_neg(
@ -445,31 +468,13 @@ impl Channels {
max_i_neg: ElectricCurrent, max_i_neg: ElectricCurrent,
) -> (ElectricCurrent, ElectricCurrent) { ) -> (ElectricCurrent, ElectricCurrent) {
let max = ElectricCurrent::new::<ampere>(3.0); let max = ElectricCurrent::new::<ampere>(3.0);
let max_i_neg = max_i_neg.min(MAX_TEC_I).max(ElectricCurrent::zero()); let duty = (max_i_neg.min(MAX_TEC_I).max(ElectricCurrent::zero()) / max).get::<ratio>();
let duty = (max_i_neg / MAX_TEC_I_DUTY_TO_CURRENT_RATE).get::<ratio>(); let duty = self.set_pwm(channel, PwmPin::MaxINeg, duty);
let duty = match self.channel_state(channel).polarity { (duty * max, max)
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;
(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 { fn report(&mut self, channel: usize) -> Report {
let i_set = self.get_i_set(channel); let i_set = self.get_i(channel);
let i_tec = self.adc_read(channel, PinsAdcReadTarget::ITec, 16); let i_tec = self.adc_read(channel, PinsAdcReadTarget::ITec, 16);
let tec_i = self.get_tec_i(channel); let tec_i = self.get_tec_i(channel);
let dac_value = self.get_dac(channel); let dac_value = self.get_dac(channel);
@ -520,22 +525,21 @@ impl Channels {
false false
} }
fn output_summary(&mut self, channel: usize) -> OutputSummary { fn pwm_summary(&mut self, channel: usize) -> PwmSummary {
OutputSummary { PwmSummary {
channel, channel,
center: CenterPointJson(self.channel_state(channel).center.clone()), center: CenterPointJson(self.channel_state(channel).center.clone()),
i_set: self.get_i_set(channel), i_set: (self.get_i(channel), MAX_TEC_I).into(),
max_v: self.get_max_v(channel), max_v: self.get_max_v(channel).into(),
max_i_pos: self.get_max_i_pos(channel), max_i_pos: self.get_max_i_pos(channel).into(),
max_i_neg: self.get_max_i_neg(channel), max_i_neg: self.get_max_i_neg(channel).into(),
polarity: PolarityJson(self.channel_state(channel).polarity.clone()),
} }
} }
pub fn output_summaries_json(&mut self) -> Result<JsonBuffer, serde_json_core::ser::Error> { pub fn pwm_summaries_json(&mut self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
let mut summaries = Vec::<_, U2>::new(); let mut summaries = Vec::<_, U2>::new();
for channel in 0..CHANNELS { for channel in 0..CHANNELS {
let _ = summaries.push(self.output_summary(channel)); let _ = summaries.push(self.pwm_summary(channel));
} }
serde_json_core::to_vec(&summaries) serde_json_core::to_vec(&summaries)
} }
@ -557,17 +561,17 @@ impl Channels {
serde_json_core::to_vec(&summaries) serde_json_core::to_vec(&summaries)
} }
fn b_parameter_summary(&mut self, channel: usize) -> BParameterSummary { fn steinhart_hart_summary(&mut self, channel: usize) -> SteinhartHartSummary {
let params = self.channel_state(channel).bp.clone(); let params = self.channel_state(channel).sh.clone();
BParameterSummary { channel, params } SteinhartHartSummary { channel, params }
} }
pub fn b_parameter_summaries_json( pub fn steinhart_hart_summaries_json(
&mut self, &mut self,
) -> Result<JsonBuffer, serde_json_core::ser::Error> { ) -> Result<JsonBuffer, serde_json_core::ser::Error> {
let mut summaries = Vec::<_, U2>::new(); let mut summaries = Vec::<_, U2>::new();
for channel in 0..CHANNELS { for channel in 0..CHANNELS {
let _ = summaries.push(self.b_parameter_summary(channel)); let _ = summaries.push(self.steinhart_hart_summary(channel));
} }
serde_json_core::to_vec(&summaries) serde_json_core::to_vec(&summaries)
} }
@ -607,36 +611,32 @@ impl Serialize for CenterPointJson {
S: Serializer, S: Serializer,
{ {
match self.0 { match self.0 {
CenterPoint::VRef => serializer.serialize_str("vref"), CenterPoint::Vref => serializer.serialize_str("vref"),
CenterPoint::Override(vref) => serializer.serialize_f32(vref), CenterPoint::Override(vref) => serializer.serialize_f32(vref),
} }
} }
} }
pub struct PolarityJson(Polarity); #[derive(Serialize)]
pub struct PwmSummaryField<T: Serialize> {
value: T,
max: T,
}
// used in JSON encoding, not for config impl<T: Serialize> From<(T, T)> for PwmSummaryField<T> {
impl Serialize for PolarityJson { fn from((value, max): (T, T)) -> Self {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> PwmSummaryField { value, max }
where
S: Serializer,
{
serializer.serialize_str(match self.0 {
Polarity::Normal => "normal",
Polarity::Reversed => "reversed",
})
} }
} }
#[derive(Serialize)] #[derive(Serialize)]
pub struct OutputSummary { pub struct PwmSummary {
channel: usize, channel: usize,
center: CenterPointJson, center: CenterPointJson,
i_set: ElectricCurrent, i_set: PwmSummaryField<ElectricCurrent>,
max_v: ElectricPotential, max_v: PwmSummaryField<ElectricPotential>,
max_i_pos: ElectricCurrent, max_i_pos: PwmSummaryField<ElectricCurrent>,
max_i_neg: ElectricCurrent, max_i_neg: PwmSummaryField<ElectricCurrent>,
polarity: PolarityJson,
} }
#[derive(Serialize)] #[derive(Serialize)]
@ -646,7 +646,7 @@ pub struct PostFilterSummary {
} }
#[derive(Serialize)] #[derive(Serialize)]
pub struct BParameterSummary { pub struct SteinhartHartSummary {
channel: usize, channel: usize,
params: b_parameter::Parameters, params: steinhart_hart::Parameters,
} }

View File

@ -2,13 +2,15 @@ use super::{
ad7172, ad7172,
channels::{Channels, CHANNELS}, channels::{Channels, CHANNELS},
command_parser::{ command_parser::{
BpParameter, CenterPoint, Command, Ipv4Config, PidParameter, Polarity, PwmPin, ShowCommand, CenterPoint, Command, Ipv4Config, PidParameter, PwmPin, ShParameter, ShowCommand,
}, },
config::ChannelConfig, config::ChannelConfig,
dfu, dfu,
flash_store::FlashStore, flash_store::FlashStore,
hw_rev::HWRev, hw_rev::HWRev,
net, FanCtrl, CHANNEL_CONFIG_KEY, net,
session::Session,
FanCtrl, CHANNEL_CONFIG_KEY,
}; };
use core::fmt::Write; use core::fmt::Write;
use heapless::{consts::U1024, Vec}; use heapless::{consts::U1024, Vec};
@ -19,11 +21,7 @@ use uom::si::{
electric_current::ampere, electric_current::ampere,
electric_potential::volt, electric_potential::volt,
electrical_resistance::ohm, electrical_resistance::ohm,
f64::{ f64::{ElectricCurrent, ElectricPotential, ElectricalResistance, ThermodynamicTemperature},
ElectricCurrent, ElectricPotential, ElectricalResistance, TemperatureInterval,
ThermodynamicTemperature,
},
temperature_interval::kelvin,
thermodynamic_temperature::degree_celsius, thermodynamic_temperature::degree_celsius,
}; };
@ -37,9 +35,9 @@ pub enum Handler {
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub enum Error { pub enum Error {
Report, ReportError,
PostFilterRate, PostFilterRateError,
Flash, FlashError,
} }
pub type JsonBuffer = Vec<u8, U1024>; pub type JsonBuffer = Vec<u8, U1024>;
@ -71,6 +69,16 @@ fn send_line(socket: &mut TcpSocket, data: &[u8]) -> bool {
} }
impl Handler { 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> { fn show_report(socket: &mut TcpSocket, channels: &mut Channels) -> Result<Handler, Error> {
match channels.reports_json() { match channels.reports_json() {
Ok(buf) => { Ok(buf) => {
@ -79,7 +87,7 @@ impl Handler {
Err(e) => { Err(e) => {
error!("unable to serialize report: {:?}", e); error!("unable to serialize report: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e); let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
return Err(Error::Report); return Err(Error::ReportError);
} }
} }
Ok(Handler::Handled) Ok(Handler::Handled)
@ -93,35 +101,38 @@ impl Handler {
Err(e) => { Err(e) => {
error!("unable to serialize pid summary: {:?}", e); error!("unable to serialize pid summary: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e); let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
return Err(Error::Report); return Err(Error::ReportError);
} }
} }
Ok(Handler::Handled) Ok(Handler::Handled)
} }
fn show_pwm(socket: &mut TcpSocket, channels: &mut Channels) -> Result<Handler, Error> { fn show_pwm(socket: &mut TcpSocket, channels: &mut Channels) -> Result<Handler, Error> {
match channels.output_summaries_json() { match channels.pwm_summaries_json() {
Ok(buf) => { Ok(buf) => {
send_line(socket, &buf); send_line(socket, &buf);
} }
Err(e) => { Err(e) => {
error!("unable to serialize pwm summary: {:?}", e); error!("unable to serialize pwm summary: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e); let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
return Err(Error::Report); return Err(Error::ReportError);
} }
} }
Ok(Handler::Handled) Ok(Handler::Handled)
} }
fn show_b_parameter(socket: &mut TcpSocket, channels: &mut Channels) -> Result<Handler, Error> { fn show_steinhart_hart(
match channels.b_parameter_summaries_json() { socket: &mut TcpSocket,
channels: &mut Channels,
) -> Result<Handler, Error> {
match channels.steinhart_hart_summaries_json() {
Ok(buf) => { Ok(buf) => {
send_line(socket, &buf); send_line(socket, &buf);
} }
Err(e) => { Err(e) => {
error!("unable to serialize b parameter summaries: {:?}", e); error!("unable to serialize steinhart-hart summaries: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e); let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
return Err(Error::Report); return Err(Error::ReportError);
} }
} }
Ok(Handler::Handled) Ok(Handler::Handled)
@ -135,7 +146,7 @@ impl Handler {
Err(e) => { Err(e) => {
error!("unable to serialize postfilter summary: {:?}", e); error!("unable to serialize postfilter summary: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e); let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
return Err(Error::Report); return Err(Error::ReportError);
} }
} }
Ok(Handler::Handled) Ok(Handler::Handled)
@ -159,17 +170,6 @@ impl Handler {
Ok(Handler::Handled) 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( fn set_pwm(
socket: &mut TcpSocket, socket: &mut TcpSocket,
channels: &mut Channels, channels: &mut Channels,
@ -207,7 +207,7 @@ impl Handler {
channel: usize, channel: usize,
center: CenterPoint, center: CenterPoint,
) -> Result<Handler, Error> { ) -> Result<Handler, Error> {
let i_set = channels.get_i_set(channel); let i_set = channels.get_i(channel);
let state = channels.channel_state(channel); let state = channels.channel_state(channel);
state.center = center; state.center = center;
if !state.pid_engaged { if !state.pid_engaged {
@ -238,19 +238,19 @@ impl Handler {
Ok(Handler::Handled) Ok(Handler::Handled)
} }
fn set_b_parameter( fn set_steinhart_hart(
socket: &mut TcpSocket, socket: &mut TcpSocket,
channels: &mut Channels, channels: &mut Channels,
channel: usize, channel: usize,
parameter: BpParameter, parameter: ShParameter,
value: f64, value: f64,
) -> Result<Handler, Error> { ) -> Result<Handler, Error> {
let bp = &mut channels.channel_state(channel).bp; let sh = &mut channels.channel_state(channel).sh;
use super::command_parser::BpParameter::*; use super::command_parser::ShParameter::*;
match parameter { match parameter {
T0 => bp.t0 = ThermodynamicTemperature::new::<degree_celsius>(value), T0 => sh.t0 = ThermodynamicTemperature::new::<degree_celsius>(value),
B => bp.b = TemperatureInterval::new::<kelvin>(value), B => sh.b = value,
R0 => bp.r0 = ElectricalResistance::new::<ohm>(value), R0 => sh.r0 = ElectricalResistance::new::<ohm>(value),
} }
send_line(socket, b"{}"); send_line(socket, b"{}");
Ok(Handler::Handled) Ok(Handler::Handled)
@ -287,7 +287,7 @@ impl Handler {
socket, socket,
b"{{\"error\": \"unable to choose postfilter rate\"}}", b"{{\"error\": \"unable to choose postfilter rate\"}}",
); );
return Err(Error::PostFilterRate); return Err(Error::PostFilterRateError);
} }
} }
Ok(Handler::Handled) Ok(Handler::Handled)
@ -299,9 +299,9 @@ impl Handler {
store: &mut FlashStore, store: &mut FlashStore,
channel: Option<usize>, channel: Option<usize>,
) -> Result<Handler, Error> { ) -> Result<Handler, Error> {
for (c, key) in CHANNEL_CONFIG_KEY.iter().enumerate().take(CHANNELS) { for c in 0..CHANNELS {
if channel.is_none() || channel == Some(c) { if channel.is_none() || channel == Some(c) {
match store.read_value::<ChannelConfig>(key) { match store.read_value::<ChannelConfig>(CHANNEL_CONFIG_KEY[c]) {
Ok(Some(config)) => { Ok(Some(config)) => {
config.apply(channels, c); config.apply(channels, c);
send_line(socket, b"{}"); send_line(socket, b"{}");
@ -313,7 +313,7 @@ impl Handler {
Err(e) => { Err(e) => {
error!("unable to load config from flash: {:?}", e); error!("unable to load config from flash: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e); let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
return Err(Error::Flash); return Err(Error::FlashError);
} }
} }
} }
@ -327,18 +327,18 @@ impl Handler {
channel: Option<usize>, channel: Option<usize>,
store: &mut FlashStore, store: &mut FlashStore,
) -> Result<Handler, Error> { ) -> Result<Handler, Error> {
for (c, key) in CHANNEL_CONFIG_KEY.iter().enumerate().take(CHANNELS) { for c in 0..CHANNELS {
let mut store_value_buf = [0u8; 256]; let mut store_value_buf = [0u8; 256];
if channel.is_none() || channel == Some(c) { if channel.is_none() || channel == Some(c) {
let config = ChannelConfig::new(channels, c); let config = ChannelConfig::new(channels, c);
match store.write_value(key, &config, &mut store_value_buf) { match store.write_value(CHANNEL_CONFIG_KEY[c], &config, &mut store_value_buf) {
Ok(()) => { Ok(()) => {
send_line(socket, b"{}"); send_line(socket, b"{}");
} }
Err(e) => { Err(e) => {
error!("unable to save channel {} config to flash: {:?}", c, e); error!("unable to save channel {} config to flash: {:?}", c, e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e); let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
return Err(Error::Flash); return Err(Error::FlashError);
} }
} }
} }
@ -409,7 +409,7 @@ impl Handler {
Err(e) => { Err(e) => {
error!("unable to serialize fan summary: {:?}", e); error!("unable to serialize fan summary: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e); let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
Err(Error::Report) Err(Error::ReportError)
} }
} }
} }
@ -458,7 +458,7 @@ impl Handler {
Err(e) => { Err(e) => {
error!("unable to serialize HWRev summary: {:?}", e); error!("unable to serialize HWRev summary: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e); let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
Err(Error::Report) Err(Error::ReportError)
} }
} }
} }
@ -467,6 +467,7 @@ impl Handler {
command: Command, command: Command,
socket: &mut TcpSocket, socket: &mut TcpSocket,
channels: &mut Channels, channels: &mut Channels,
session: &Session,
store: &mut FlashStore, store: &mut FlashStore,
ipv4_config: &mut Ipv4Config, ipv4_config: &mut Ipv4Config,
fan_ctrl: &mut FanCtrl, fan_ctrl: &mut FanCtrl,
@ -474,17 +475,18 @@ impl Handler {
) -> Result<Self, Error> { ) -> Result<Self, Error> {
match command { match command {
Command::Quit => Ok(Handler::CloseSocket), 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::Input) => Handler::show_report(socket, channels),
Command::Show(ShowCommand::Pid) => Handler::show_pid(socket, channels), Command::Show(ShowCommand::Pid) => Handler::show_pid(socket, channels),
Command::Show(ShowCommand::Output) => Handler::show_pwm(socket, channels), Command::Show(ShowCommand::Pwm) => Handler::show_pwm(socket, channels),
Command::Show(ShowCommand::BParameter) => Handler::show_b_parameter(socket, channels), Command::Show(ShowCommand::SteinhartHart) => {
Handler::show_steinhart_hart(socket, channels)
}
Command::Show(ShowCommand::PostFilter) => Handler::show_post_filter(socket, channels), Command::Show(ShowCommand::PostFilter) => Handler::show_post_filter(socket, channels),
Command::Show(ShowCommand::Ipv4) => Handler::show_ipv4(socket, ipv4_config), Command::Show(ShowCommand::Ipv4) => Handler::show_ipv4(socket, ipv4_config),
Command::OutputPid { channel } => Handler::engage_pid(socket, channels, channel), Command::PwmPid { channel } => Handler::engage_pid(socket, channels, channel),
Command::OutputPolarity { channel, polarity } => { Command::Pwm {
Handler::set_polarity(socket, channels, channel, polarity)
}
Command::Output {
channel, channel,
pin, pin,
value, value,
@ -497,11 +499,11 @@ impl Handler {
parameter, parameter,
value, value,
} => Handler::set_pid(socket, channels, channel, parameter, value), } => Handler::set_pid(socket, channels, channel, parameter, value),
Command::BParameter { Command::SteinhartHart {
channel, channel,
parameter, parameter,
value, value,
} => Handler::set_b_parameter(socket, channels, channel, parameter, value), } => Handler::set_steinhart_hart(socket, channels, channel, parameter, value),
Command::PostFilter { Command::PostFilter {
channel, channel,
rate: None, rate: None,

View File

@ -91,9 +91,10 @@ pub struct Ipv4Config {
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum ShowCommand { pub enum ShowCommand {
Input, Input,
Output, Reporting,
Pwm,
Pid, Pid,
BParameter, SteinhartHart,
PostFilter, PostFilter,
Ipv4, Ipv4,
} }
@ -108,9 +109,9 @@ pub enum PidParameter {
OutputMax, OutputMax,
} }
/// B-Parameter equation parameter /// Steinhart-Hart equation parameter
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum BpParameter { pub enum ShParameter {
T0, T0,
B, B,
R0, R0,
@ -126,16 +127,10 @@ pub enum PwmPin {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum CenterPoint { pub enum CenterPoint {
VRef, Vref,
Override(f32), Override(f32),
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Polarity {
Normal,
Reversed,
}
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum Command { pub enum Command {
Quit, Quit,
@ -148,20 +143,17 @@ pub enum Command {
Reset, Reset,
Ipv4(Ipv4Config), Ipv4(Ipv4Config),
Show(ShowCommand), Show(ShowCommand),
Reporting(bool),
/// PWM parameter setting /// PWM parameter setting
Output { Pwm {
channel: usize, channel: usize,
pin: PwmPin, pin: PwmPin,
value: f64, value: f64,
}, },
/// Enable PID control for `i_set` /// Enable PID control for `i_set`
OutputPid { PwmPid {
channel: usize, channel: usize,
}, },
OutputPolarity {
channel: usize,
polarity: Polarity,
},
CenterPoint { CenterPoint {
channel: usize, channel: usize,
center: CenterPoint, center: CenterPoint,
@ -172,9 +164,9 @@ pub enum Command {
parameter: PidParameter, parameter: PidParameter,
value: f64, value: f64,
}, },
BParameter { SteinhartHart {
channel: usize, channel: usize,
parameter: BpParameter, parameter: ShParameter,
value: f64, value: f64,
}, },
PostFilter { PostFilter {
@ -208,7 +200,7 @@ fn unsigned(input: &[u8]) -> IResult<&[u8], Result<u32, Error>> {
take_while1(is_digit)(input).map(|(input, digits)| { take_while1(is_digit)(input).map(|(input, digits)| {
let result = from_utf8(digits) let result = from_utf8(digits)
.map_err(|e| e.into()) .map_err(|e| e.into())
.and_then(|digits| digits.parse::<u32>().map_err(|e| e.into())); .and_then(|digits| u32::from_str_radix(digits, 10).map_err(|e| e.into()));
(input, result) (input, result)
}) })
} }
@ -224,6 +216,10 @@ fn float(input: &[u8]) -> IResult<&[u8], Result<f64, Error>> {
Ok((input, result)) 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> { fn channel(input: &[u8]) -> IResult<&[u8], usize> {
map(one_of("01"), |c| (c as usize) - ('0' as usize))(input) map(one_of("01"), |c| (c as usize) - ('0' as usize))(input)
} }
@ -231,8 +227,25 @@ fn channel(input: &[u8]) -> IResult<&[u8], usize> {
fn report(input: &[u8]) -> IResult<&[u8], Command> { fn report(input: &[u8]) -> IResult<&[u8], Command> {
preceded( preceded(
tag("report"), 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 // `report` - Report once
value(Command::Show(ShowCommand::Input), end), value(Command::Show(ShowCommand::Input), end),
)),
)(input) )(input)
} }
@ -260,26 +273,13 @@ fn pwm_setup(input: &[u8]) -> IResult<&[u8], Result<(PwmPin, f64), Error>> {
))(input) ))(input)
} }
/// `output <0-1> pid` - Set output to be controlled by PID /// `pwm <0-1> pid` - Set PWM to be controlled by PID
fn output_pid(input: &[u8]) -> IResult<&[u8], ()> { fn pwm_pid(input: &[u8]) -> IResult<&[u8], ()> {
value((), tag("pid"))(input) value((), tag("pid"))(input)
} }
fn output_polarity(input: &[u8]) -> IResult<&[u8], Polarity> { fn pwm(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
preceded( let (input, _) = tag("pwm")(input)?;
tag("polarity"),
preceded(
whitespace,
alt((
value(Polarity::Normal, tag("normal")),
value(Polarity::Reversed, tag("reversed")),
)),
),
)(input)
}
fn output(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
let (input, _) = tag("output")(input)?;
alt(( alt((
|input| { |input| {
let (input, _) = whitespace(input)?; let (input, _) = whitespace(input)?;
@ -287,19 +287,15 @@ fn output(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
let (input, _) = whitespace(input)?; let (input, _) = whitespace(input)?;
let (input, result) = alt(( let (input, result) = alt((
|input| { |input| {
let (input, ()) = output_pid(input)?; let (input, ()) = pwm_pid(input)?;
Ok((input, Ok(Command::OutputPid { channel }))) Ok((input, Ok(Command::PwmPid { channel })))
},
|input| {
let (input, polarity) = output_polarity(input)?;
Ok((input, Ok(Command::OutputPolarity { channel, polarity })))
}, },
|input| { |input| {
let (input, config) = pwm_setup(input)?; let (input, config) = pwm_setup(input)?;
match config { match config {
Ok((pin, value)) => Ok(( Ok((pin, value)) => Ok((
input, input,
Ok(Command::Output { Ok(Command::Pwm {
channel, channel,
pin, pin,
value, value,
@ -312,7 +308,7 @@ fn output(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
end(input)?; end(input)?;
Ok((input, result)) Ok((input, result))
}, },
value(Ok(Command::Show(ShowCommand::Output)), end), value(Ok(Command::Show(ShowCommand::Pwm)), end),
))(input) ))(input)
} }
@ -321,7 +317,7 @@ fn center_point(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
let (input, _) = whitespace(input)?; let (input, _) = whitespace(input)?;
let (input, channel) = channel(input)?; let (input, channel) = channel(input)?;
let (input, _) = whitespace(input)?; let (input, _) = whitespace(input)?;
let (input, center) = alt((value(Ok(CenterPoint::VRef), tag("vref")), |input| { let (input, center) = alt((value(Ok(CenterPoint::Vref), tag("vref")), |input| {
let (input, value) = float(input)?; let (input, value) = float(input)?;
Ok(( Ok((
input, input,
@ -366,18 +362,18 @@ fn pid(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
))(input) ))(input)
} }
/// `b-p <0-1> <parameter> <value>` /// `s-h <0-1> <parameter> <value>`
fn b_parameter_parameter(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> { fn steinhart_hart_parameter(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
let (input, channel) = channel(input)?; let (input, channel) = channel(input)?;
let (input, _) = whitespace(input)?; let (input, _) = whitespace(input)?;
let (input, parameter) = alt(( let (input, parameter) = alt((
value(BpParameter::T0, tag("t0")), value(ShParameter::T0, tag("t0")),
value(BpParameter::B, tag("b")), value(ShParameter::B, tag("b")),
value(BpParameter::R0, tag("r0")), value(ShParameter::R0, tag("r0")),
))(input)?; ))(input)?;
let (input, _) = whitespace(input)?; let (input, _) = whitespace(input)?;
let (input, value) = float(input)?; let (input, value) = float(input)?;
let result = value.map(|value| Command::BParameter { let result = value.map(|value| Command::SteinhartHart {
channel, channel,
parameter, parameter,
value, value,
@ -385,12 +381,12 @@ fn b_parameter_parameter(input: &[u8]) -> IResult<&[u8], Result<Command, Error>>
Ok((input, result)) Ok((input, result))
} }
/// `b-p` | `b-p <b_parameter_parameter>` /// `s-h` | `s-h <steinhart_hart_parameter>`
fn b_parameter(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> { fn steinhart_hart(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
let (input, _) = tag("b-p")(input)?; let (input, _) = tag("s-h")(input)?;
alt(( alt((
preceded(whitespace, b_parameter_parameter), preceded(whitespace, steinhart_hart_parameter),
value(Ok(Command::Show(ShowCommand::BParameter)), end), value(Ok(Command::Show(ShowCommand::SteinhartHart)), end),
))(input) ))(input)
} }
@ -541,13 +537,13 @@ fn fan_curve(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
let (input, k_b) = float(input)?; let (input, k_b) = float(input)?;
let (input, _) = whitespace(input)?; let (input, _) = whitespace(input)?;
let (input, k_c) = float(input)?; let (input, k_c) = float(input)?;
if let (Ok(k_a), Ok(k_b), Ok(k_c)) = (k_a, k_b, k_c) { if k_a.is_ok() && k_b.is_ok() && k_c.is_ok() {
Ok(( Ok((
input, input,
Ok(Command::FanCurve { Ok(Command::FanCurve {
k_a: k_a as f32, k_a: k_a.unwrap() as f32,
k_b: k_b as f32, k_b: k_b.unwrap() as f32,
k_c: k_c as f32, k_c: k_c.unwrap() as f32,
}), }),
)) ))
} else { } else {
@ -569,10 +565,10 @@ fn command(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
value(Ok(Command::Reset), tag("reset")), value(Ok(Command::Reset), tag("reset")),
ipv4, ipv4,
map(report, Ok), map(report, Ok),
output, pwm,
center_point, center_point,
pid, pid,
b_parameter, steinhart_hart,
postfilter, postfilter,
value(Ok(Command::Dfu), tag("dfu")), value(Ok(Command::Dfu), tag("dfu")),
fan, fan,
@ -664,11 +660,29 @@ mod test {
} }
#[test] #[test]
fn parse_output_i_set() { fn parse_report_mode() {
let command = Command::parse(b"output 1 i_set 16383"); 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");
assert_eq!( assert_eq!(
command, command,
Ok(Command::Output { Ok(Command::Pwm {
channel: 1, channel: 1,
pin: PwmPin::ISet, pin: PwmPin::ISet,
value: 16383.0, value: 16383.0,
@ -677,29 +691,17 @@ mod test {
} }
#[test] #[test]
fn parse_output_polarity() { fn parse_pwm_pid() {
let command = Command::parse(b"output 0 polarity reversed"); let command = Command::parse(b"pwm 0 pid");
assert_eq!( assert_eq!(command, Ok(Command::PwmPid { channel: 0 }));
command,
Ok(Command::OutputPolarity {
channel: 0,
polarity: Polarity::Reversed,
})
);
} }
#[test] #[test]
fn parse_output_pid() { fn parse_pwm_max_i_pos() {
let command = Command::parse(b"output 0 pid"); let command = Command::parse(b"pwm 0 max_i_pos 7");
assert_eq!(command, Ok(Command::OutputPid { channel: 0 }));
}
#[test]
fn parse_output_max_i_pos() {
let command = Command::parse(b"output 0 max_i_pos 7");
assert_eq!( assert_eq!(
command, command,
Ok(Command::Output { Ok(Command::Pwm {
channel: 0, channel: 0,
pin: PwmPin::MaxIPos, pin: PwmPin::MaxIPos,
value: 7.0, value: 7.0,
@ -708,11 +710,11 @@ mod test {
} }
#[test] #[test]
fn parse_output_max_i_neg() { fn parse_pwm_max_i_neg() {
let command = Command::parse(b"output 0 max_i_neg 128"); let command = Command::parse(b"pwm 0 max_i_neg 128");
assert_eq!( assert_eq!(
command, command,
Ok(Command::Output { Ok(Command::Pwm {
channel: 0, channel: 0,
pin: PwmPin::MaxINeg, pin: PwmPin::MaxINeg,
value: 128.0, value: 128.0,
@ -721,11 +723,11 @@ mod test {
} }
#[test] #[test]
fn parse_output_max_v() { fn parse_pwm_max_v() {
let command = Command::parse(b"output 0 max_v 32768"); let command = Command::parse(b"pwm 0 max_v 32768");
assert_eq!( assert_eq!(
command, command,
Ok(Command::Output { Ok(Command::Pwm {
channel: 0, channel: 0,
pin: PwmPin::MaxV, pin: PwmPin::MaxV,
value: 32768.0, value: 32768.0,
@ -753,19 +755,19 @@ mod test {
} }
#[test] #[test]
fn parse_b_parameter() { fn parse_steinhart_hart() {
let command = Command::parse(b"b-p"); let command = Command::parse(b"s-h");
assert_eq!(command, Ok(Command::Show(ShowCommand::BParameter))); assert_eq!(command, Ok(Command::Show(ShowCommand::SteinhartHart)));
} }
#[test] #[test]
fn parse_b_parameter_set() { fn parse_steinhart_hart_set() {
let command = Command::parse(b"b-p 1 t0 23.05"); let command = Command::parse(b"s-h 1 t0 23.05");
assert_eq!( assert_eq!(
command, command,
Ok(Command::BParameter { Ok(Command::SteinhartHart {
channel: 1, channel: 1,
parameter: BpParameter::T0, parameter: ShParameter::T0,
value: 23.05, value: 23.05,
}) })
); );
@ -820,7 +822,7 @@ mod test {
command, command,
Ok(Command::CenterPoint { Ok(Command::CenterPoint {
channel: 1, channel: 1,
center: CenterPoint::VRef, center: CenterPoint::Vref,
}) })
); );
} }

View File

@ -1,13 +1,13 @@
use crate::{ use crate::{
ad7172::PostFilter, ad7172::PostFilter, channels::Channels, command_parser::CenterPoint, pid, steinhart_hart,
b_parameter,
channels::Channels,
command_parser::{CenterPoint, Polarity},
pid,
}; };
use num_traits::Zero; use num_traits::Zero;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uom::si::f64::{ElectricCurrent, ElectricPotential}; use uom::si::{
electric_current::ampere,
electric_potential::volt,
f64::{ElectricCurrent, ElectricPotential},
};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ChannelConfig { pub struct ChannelConfig {
@ -16,8 +16,7 @@ pub struct ChannelConfig {
pid_target: f32, pid_target: f32,
pid_engaged: bool, pid_engaged: bool,
i_set: ElectricCurrent, i_set: ElectricCurrent,
polarity: Polarity, sh: steinhart_hart::Parameters,
bp: b_parameter::Parameters,
pwm: PwmLimits, pwm: PwmLimits,
/// uses variant `PostFilter::Invalid` instead of `None` to save space /// uses variant `PostFilter::Invalid` instead of `None` to save space
adc_postfilter: PostFilter, adc_postfilter: PostFilter,
@ -45,8 +44,7 @@ impl ChannelConfig {
pid_target: state.pid.target as f32, pid_target: state.pid.target as f32,
pid_engaged: state.pid_engaged, pid_engaged: state.pid_engaged,
i_set, i_set,
polarity: state.polarity.clone(), sh: state.sh.clone(),
bp: state.bp.clone(),
pwm, pwm,
adc_postfilter, adc_postfilter,
} }
@ -58,7 +56,7 @@ impl ChannelConfig {
state.pid.parameters = self.pid.clone(); state.pid.parameters = self.pid.clone();
state.pid.target = self.pid_target.into(); state.pid.target = self.pid_target.into();
state.pid_engaged = self.pid_engaged; state.pid_engaged = self.pid_engaged;
state.bp = self.bp.clone(); state.sh = self.sh.clone();
self.pwm.apply(channels, channel); self.pwm.apply(channels, channel);
@ -68,32 +66,31 @@ impl ChannelConfig {
}; };
let _ = channels.adc.set_postfilter(channel as u8, adc_postfilter); let _ = channels.adc.set_postfilter(channel as u8, adc_postfilter);
let _ = channels.set_i(channel, self.i_set); let _ = channels.set_i(channel, self.i_set);
channels.set_polarity(channel, self.polarity.clone());
} }
} }
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct PwmLimits { struct PwmLimits {
pub max_v: ElectricPotential, max_v: f64,
pub max_i_pos: ElectricCurrent, max_i_pos: f64,
pub max_i_neg: ElectricCurrent, max_i_neg: f64,
} }
impl PwmLimits { impl PwmLimits {
pub fn new(channels: &mut Channels, channel: usize) -> Self { pub fn new(channels: &mut Channels, channel: usize) -> Self {
let max_v = channels.get_max_v(channel); let (max_v, _) = channels.get_max_v(channel);
let max_i_pos = channels.get_max_i_pos(channel); let (max_i_pos, _) = channels.get_max_i_pos(channel);
let max_i_neg = channels.get_max_i_neg(channel); let (max_i_neg, _) = channels.get_max_i_neg(channel);
PwmLimits { PwmLimits {
max_v, max_v: max_v.get::<volt>(),
max_i_pos, max_i_pos: max_i_pos.get::<ampere>(),
max_i_neg, max_i_neg: max_i_neg.get::<ampere>(),
} }
} }
pub fn apply(&self, channels: &mut Channels, channel: usize) { pub fn apply(&self, channels: &mut Channels, channel: usize) {
channels.set_max_v(channel, self.max_v); channels.set_max_v(channel, ElectricPotential::new::<volt>(self.max_v));
channels.set_max_i_pos(channel, self.max_i_pos); channels.set_max_i_pos(channel, ElectricCurrent::new::<ampere>(self.max_i_pos));
channels.set_max_i_neg(channel, self.max_i_neg); channels.set_max_i_neg(channel, ElectricCurrent::new::<ampere>(self.max_i_neg));
} }
} }

View File

@ -95,7 +95,9 @@ impl FanCtrl {
return 0f32; return 0f32;
} }
let fan = self.fan.as_mut().unwrap(); let fan = self.fan.as_mut().unwrap();
let fan_pwm = fan_pwm.clamp(MIN_USER_FAN_PWM as u32, MAX_USER_FAN_PWM as u32); let fan_pwm = fan_pwm
.min(MAX_USER_FAN_PWM as u32)
.max(MIN_USER_FAN_PWM as u32);
let duty = scale_number( let duty = scale_number(
fan_pwm as f32, fan_pwm as f32,
self.hw_settings.min_fan_pwm, self.hw_settings.min_fan_pwm,

View File

@ -35,9 +35,9 @@ mod session;
use session::{Session, SessionInput}; use session::{Session, SessionInput};
mod command_parser; mod command_parser;
use command_parser::Ipv4Config; use command_parser::Ipv4Config;
mod b_parameter;
mod channels; mod channels;
mod pid; mod pid;
mod steinhart_hart;
mod timer; mod timer;
use channels::{Channels, CHANNELS}; use channels::{Channels, CHANNELS};
mod channel; mod channel;
@ -119,14 +119,24 @@ fn main() -> ! {
let (pins, mut leds, mut eeprom, eth_pins, usb, fan, hwrev, hw_settings) = Pins::setup( let (pins, mut leds, mut eeprom, eth_pins, usb, fan, hwrev, hw_settings) = Pins::setup(
clocks, clocks,
(dp.TIM1, dp.TIM3, dp.TIM8), dp.TIM1,
( dp.TIM3,
dp.GPIOA, dp.GPIOB, dp.GPIOC, dp.GPIOD, dp.GPIOE, dp.GPIOF, dp.GPIOG, dp.TIM8,
), dp.GPIOA,
dp.GPIOB,
dp.GPIOC,
dp.GPIOD,
dp.GPIOE,
dp.GPIOF,
dp.GPIOG,
dp.I2C1, dp.I2C1,
(dp.SPI2, dp.SPI4, dp.SPI5), dp.SPI2,
dp.SPI4,
dp.SPI5,
dp.ADC1, dp.ADC1,
(dp.OTG_FS_GLOBAL, dp.OTG_FS_DEVICE, dp.OTG_FS_PWRCLK), dp.OTG_FS_GLOBAL,
dp.OTG_FS_DEVICE,
dp.OTG_FS_PWRCLK,
); );
leds.r1.on(); leds.r1.on();
@ -138,8 +148,8 @@ fn main() -> ! {
let mut store = flash_store::store(dp.FLASH); let mut store = flash_store::store(dp.FLASH);
let mut channels = Channels::new(pins); let mut channels = Channels::new(pins);
for (c, key) in CHANNEL_CONFIG_KEY.iter().enumerate().take(CHANNELS) { for c in 0..CHANNELS {
match store.read_value::<ChannelConfig>(key) { match store.read_value::<ChannelConfig>(CHANNEL_CONFIG_KEY[c]) {
Ok(Some(config)) => config.apply(&mut channels, c), Ok(Some(config)) => config.apply(&mut channels, c),
Ok(None) => error!("flash config not found for channel {}", c), Ok(None) => error!("flash config not found for channel {}", c),
Err(e) => error!("unable to load config {} from flash: {:?}", c, e), Err(e) => error!("unable to load config {} from flash: {:?}", c, e),
@ -181,7 +191,10 @@ fn main() -> ! {
loop { loop {
let mut new_ipv4_config = None; let mut new_ipv4_config = None;
let instant = Instant::from_millis(i64::from(timer::now())); let instant = Instant::from_millis(i64::from(timer::now()));
channels.poll_adc(instant); let updated_channel = channels.poll_adc(instant);
if let Some(channel) = updated_channel {
server.for_each(|_, session| session.set_report_pending(channel.into()));
}
fan_ctrl.cycle(channels.current_abs_max_tec_i()); fan_ctrl.cycle(channels.current_abs_max_tec_i());
@ -217,6 +230,7 @@ fn main() -> ! {
command, command,
&mut socket, &mut socket,
&mut channels, &mut channels,
session,
&mut store, &mut store,
&mut ipv4_config, &mut ipv4_config,
&mut fan_ctrl, &mut fan_ctrl,
@ -235,6 +249,18 @@ fn main() -> ! {
} }
Err(_) => socket.close(), 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 { } else {
@ -254,10 +280,10 @@ fn main() -> ! {
} }
// Apply new IPv4 address/gateway // Apply new IPv4 address/gateway
if let Some(config) = new_ipv4_config.take() { new_ipv4_config.take().map(|config| {
server.set_ipv4_config(config.clone()); server.set_ipv4_config(config.clone());
ipv4_config = config; ipv4_config = config;
}; });
// Update watchdog // Update watchdog
wd.feed(); wd.feed();

View File

@ -133,24 +133,24 @@ impl Pins {
/// Setup GPIO pins and configure MCU peripherals /// Setup GPIO pins and configure MCU peripherals
pub fn setup( pub fn setup(
clocks: Clocks, clocks: Clocks,
(tim1, tim3, tim8): (TIM1, TIM3, TIM8), tim1: TIM1,
(gpioa, gpiob, gpioc, gpiod, gpioe, gpiof, gpiog): ( tim3: TIM3,
GPIOA, tim8: TIM8,
GPIOB, gpioa: GPIOA,
GPIOC, gpiob: GPIOB,
GPIOD, gpioc: GPIOC,
GPIOE, gpiod: GPIOD,
GPIOF, gpioe: GPIOE,
GPIOG, gpiof: GPIOF,
), gpiog: GPIOG,
i2c1: I2C1, i2c1: I2C1,
(spi2, spi4, spi5): (SPI2, SPI4, SPI5), spi2: SPI2,
spi4: SPI4,
spi5: SPI5,
adc1: ADC1, adc1: ADC1,
(otg_fs_global, otg_fs_device, otg_fs_pwrclk): ( otg_fs_global: OTG_FS_GLOBAL,
OTG_FS_GLOBAL, otg_fs_device: OTG_FS_DEVICE,
OTG_FS_DEVICE, otg_fs_pwrclk: OTG_FS_PWRCLK,
OTG_FS_PWRCLK,
),
) -> ( ) -> (
Self, Self,
Leds, Leds,
@ -175,11 +175,7 @@ impl Pins {
let pins_adc = Adc::adc1(adc1, true, Default::default()); let pins_adc = Adc::adc1(adc1, true, Default::default());
let pwm = PwmPins::setup( let pwm = PwmPins::setup(
clocks, clocks, tim1, tim3, gpioc.pc6, gpioc.pc7, gpioe.pe9, gpioe.pe11, gpioe.pe13, gpioe.pe14,
(tim1, tim3),
(gpioc.pc6, gpioc.pc7),
(gpioe.pe9, gpioe.pe11),
(gpioe.pe13, gpioe.pe14),
); );
let hwrev = HWRev::detect_hw_rev(&HWRevPins { let hwrev = HWRev::detect_hw_rev(&HWRevPins {
@ -358,10 +354,14 @@ pub struct PwmPins {
impl PwmPins { impl PwmPins {
fn setup<M1, M2, M3, M4, M5, M6>( fn setup<M1, M2, M3, M4, M5, M6>(
clocks: Clocks, clocks: Clocks,
(tim1, tim3): (TIM1, TIM3), tim1: TIM1,
(max_v0, max_v1): (PC6<M1>, PC7<M2>), tim3: TIM3,
(max_i_pos0, max_i_pos1): (PE9<M3>, PE11<M4>), max_v0: PC6<M1>,
(max_i_neg0, max_i_neg1): (PE13<M5>, PE14<M6>), max_v1: PC7<M2>,
max_i_pos0: PE9<M3>,
max_i_pos1: PE11<M4>,
max_i_neg0: PE13<M5>,
max_i_neg1: PE14<M6>,
) -> PwmPins { ) -> PwmPins {
let freq = 20u32.khz(); let freq = 20u32.khz();

View File

@ -103,11 +103,16 @@ impl<'a, 'b, S: Default> Server<'a, 'b, S> {
fn set_ipv4_address(&mut self, ipv4_address: Ipv4Cidr) { fn set_ipv4_address(&mut self, ipv4_address: Ipv4Cidr) {
self.net.update_ip_addrs(|addrs| { self.net.update_ip_addrs(|addrs| {
for addr in addrs.iter_mut() { for addr in addrs.iter_mut() {
if let IpCidr::Ipv4(_) = addr { match addr {
IpCidr::Ipv4(_) => {
*addr = IpCidr::Ipv4(ipv4_address); *addr = IpCidr::Ipv4(ipv4_address);
// done // done
break; break;
} }
_ => {
// skip
}
}
} }
}); });
} }

View File

@ -1,3 +1,4 @@
use super::channels::CHANNELS;
use super::command_parser::{Command, Error as ParserError}; use super::command_parser::{Command, Error as ParserError};
const MAX_LINE_LEN: usize = 64; const MAX_LINE_LEN: usize = 64;
@ -53,6 +54,8 @@ impl From<Result<Command, ParserError>> for SessionInput {
pub struct Session { pub struct Session {
reader: LineReader, reader: LineReader,
reporting: bool,
report_pending: [bool; CHANNELS],
} }
impl Default for Session { impl Default for Session {
@ -65,11 +68,42 @@ impl Session {
pub fn new() -> Self { pub fn new() -> Self {
Session { Session {
reader: LineReader::new(), reader: LineReader::new(),
reporting: false,
report_pending: [false; CHANNELS],
} }
} }
pub fn reset(&mut self) { pub fn reset(&mut self) {
self.reader = LineReader::new(); 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(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) { pub fn feed(&mut self, buf: &[u8]) -> (usize, SessionInput) {
@ -77,10 +111,19 @@ impl Session {
for (i, b) in buf.iter().enumerate() { for (i, b) in buf.iter().enumerate() {
buf_bytes = i + 1; buf_bytes = i + 1;
let line = self.reader.feed(*b); let line = self.reader.feed(*b);
if let Some(line) = line { match line {
Some(line) => {
let command = Command::parse(line); let command = Command::parse(line);
match command {
Ok(Command::Reporting(reporting)) => {
self.reporting = reporting;
}
_ => {}
}
return (buf_bytes, command.into()); return (buf_bytes, command.into());
} }
None => {}
}
} }
(buf_bytes, SessionInput::Nothing) (buf_bytes, SessionInput::Nothing)
} }

View File

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