Compare commits

..

79 Commits

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

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

See #143.
2024-11-18 10:31:56 +08:00
atse 9af86be674 pytec: Remove artificial report mode in client
Encourage polling usage instead, as shown in example.
2024-11-16 13:11:59 +08:00
atse eabc7f6a12 flake: Register the pytec Python package 2024-11-11 17:11:37 +08:00
atse 52e35d2a98 flake: Format with nixfmt-rfc-style
Also set up formatter so that `nix fmt` formats.
2024-11-04 18:38:08 +08:00
atse f1da910c11 pytec: Complete client
Expose all available Thermostat commands to the pytec client, and add
disconnect functionality.
2024-11-04 18:03:50 +08:00
atse 9848c65de5 Fix command parser failing test due to changes 2024-10-21 15:51:21 +08:00
atse 069d791802 Rename all Steinhart-Hart references to B-param
The Steinhart-Hart equation was changed in code long ago to the
B-parameter equation. Rename references to it and the interface
accordingly. The `s-h` command is now `b-p`.

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

See M-Labs/thermostat#62 (comment).
2024-10-21 10:09:24 +08:00
atse 101a68fcfc README: Update instruction on finding Rust version 2024-10-16 11:16:23 +08:00
atse 9d1adbc7f7 README: Fixes to grammar and instructions 2024-10-16 11:15:57 +08:00
atse 47fa9f757e README: Document interval key in reports 2024-10-16 11:15:28 +08:00
atse d1df6b8e3a README: Add updated info on new hardware revision 2024-10-16 11:15:10 +08:00
atse 0ffd784e91 README: Improve description of report command 2024-10-16 11:14:40 +08:00
atse d8ec083dbc Apply clippy suggestions 2024-10-14 13:25:49 +08:00
atse 1962135e79 Apply cargo fmt 2024-10-14 13:25:49 +08:00
atse fcb5cf1d4e Add command parser test for polarity command 2024-10-14 12:46:13 +08:00
atse d517087e10 README: Purge all traces of report mode 2024-10-14 10:08:08 +08:00
atse 798b400aa5 Show and save set value for PWM limits
Show in PwmSummary the set value, before all PWM duty calculations
instead of the machine value after the calculations. Also save the set
value into the flash store instead of the machine value.
2024-10-07 10:41:31 +08:00
atse 93dc39e943 README: Document PWM value clamping 2024-10-07 10:41:31 +08:00
atse 5c3b759d0c PwmSummary: Don't show max PWM values
Putting the maximum in PwmSummary has little use - the maximum never
changes throughout the runtime of the firmware and can be replaced by
documentation elsewhere.
2024-10-07 10:41:31 +08:00
atse 6224486662 Show polarity in pwm command 2024-09-30 17:36:50 +08:00
atse 32bd49b258 Add a command to reverse the output polarity
Effectively flips all values related to the directionality of current
through the TEC, including current maximums. The reversed status is
stored in the flash store and will be loaded on reset once saved.

The command is "pwm <ch> polarity <normal/reversed>", where the "normal"
polarity is indicated by the front panel markings.

This is needed for IDC cable connections to the Sinara 5432 DAC
"Zotino", since the TEC pins of the 10-pin connector on the Thermostat
and Zotino have opposite polarities.
2024-09-30 17:36:47 +08:00
linuswck ad54842c43 Fix wrong current limit duty cycle calculation
- prev commit assumed setting 3.3V -> 3A current limit which is wrong
- Please refer to the MAX1968 datasheet for the duty cycle
calculation equation
2024-09-25 17:20:29 +08:00
atse b336c4f993 Remove report mode
As the report mode command breaks the send-receive architecture model of
the command interface, and is deemed an anti-feature, making clients
difficult to write, remove it from the firmware entirely.
2024-09-25 17:19:34 +08:00
linuswck 680193b34b Fix incorrect dac calibration algo
- Fix abnormally long calibration time
2024-09-20 21:21:14 +08:00
atse ae4bea0c8a gitignore: Ignore .bin files and __pycache__ 2024-09-19 10:08:58 +08:00
atse 1f2de942e4 flake: Add rlwrap to devShell 2024-09-19 10:06:45 +08:00
atse 1041d3ecbb Improve the VREF calibration routine
* Fix wrong calibration of VREF on startup. Caused new v2.2.2 boards to
wrongly calibrate the zero-point to ~2.2 V instead of 1.5 V.

* Fix bootloop on some boards.

* Adjust watchdog interval accordingly.
2024-09-16 18:14:47 +08:00
atse c6040899dd ItecPin -> ITecPin 2024-08-12 13:02:22 +08:00
atse 9d89104f50 README: Fix command to make firmware BIN (#115)
Co-authored-by: atse <atse@m-labs.hk>
Co-committed-by: atse <atse@m-labs.hk>
2024-08-07 18:22:48 +08:00
atse 136c7a0b52 Calculate current_abs_max_tec_i from all channels
Instead of picking channels 0 and 1. Helps to generalise for more than 2
channels.
2024-08-07 16:14:28 +08:00
atse 5000cae1b1 Grammar fixes 2024-08-07 16:09:30 +08:00
atse 78ec77509f flake: Install LLVM in devShell too
So that developers can use `llvm-objcopy` in their devShells as well.
2024-08-07 16:06:19 +08:00
atse 52aa3890c1 Update nix repos 2024-08-06 13:38:58 +08:00
atse 1ae6a6fdd4 flake: More concise devShell
No need for C compiler in development shell + use "packages" to
explicitly refer to devShell packages
2024-08-06 11:14:25 +08:00
atse 7333d2cea5 flake: Don't use deprecated flake output schemas 2024-08-06 11:09:51 +08:00
atse 44e9130010 Use oxalica's rust-overlay
Follow ARTIQ, and in this project lets us include the version number
directly in flake.nix instead of linking to the toml file of a specific
release date, as we use stable Rust.

Also, from nixpkgs manual:
    both oxalica's overlay and fenix better integrate with nix and cache
    optimizations. Because of this and ergonomics, either of those
    community projects should be preferred to the Mozilla's Rust overlay
    (nixpkgs-mozilla).
2024-06-27 12:42:00 +08:00
linuswck 5b0c6f7018 Save i_set into ChannelConfig 2024-05-18 10:50:54 +08:00
linuswck 1007982b48 clamp TEC settings to a valid & design specs range
- Not respecting the design specs can cause hardware to get stuck in unrecoverable state
2024-05-10 15:17:46 +08:00
linuswck 925601f4f5 rm pid setpoint change kick 2024-05-10 10:29:08 +08:00
linuswck 8c1cb3117c README: Add notes on `i_tec` & `tec_i`readouts 2024-05-02 17:48:47 +08:00
linuswck 1fcfe41a63 Add averaging filter on the pin_adc readings
- Adapted from Kirdy Firmware
- Can reduce the i_tec readings noise dispersion
2024-05-02 16:49:55 +08:00
linuswck 9fce19a418 Revert "Disable feedback current readout on flawed HW Revs"
This reverts commit ae3d8b51d4.
2024-05-02 14:38:40 +08:00
atse 00d5feaa8d Limit i_set within range of MAX1968 chip 2024-04-24 18:05:20 +08:00
atse 09be55e12a Don't load REF pin of MAX1968 chip on HWRevs < 3.0
The REF pin of the MAX1968 on hardware revisions v2.x is missing a
buffer, loading the pin on every CPU ADC read. Avoid reading from it and
leave the pin floating on affected hardware revisions, and return the
nominal 1.5V instead.
2024-04-03 16:32:57 +08:00
atse 76547be90a i_tec -> i_set
i_tec is reserved for the voltage signal coming out of the MAX1968 chip
for now.
2024-02-14 17:27:12 +08:00
atse 8b975e656e Stop i_set from fluctuating in every report
i_set is a user-provided value that shouldn't fluctuate with every VREF
measurement. Storing i_set as channel state is the simplest way to avoid
that.
2024-02-14 17:21:39 +08:00
atse ae3d8b51d4 Disable feedback current readout on flawed HW Revs
Thermostats v2.2 and below have a noisy and offset feedback current
`tec_i` caused by missing hardware on 2 MAX1968 TEC driver pins:

1. A missing RC filter on the ITEC pin that would have isolated CPU
sampling pulses from the signal; and
2. Some missing buffering on the VREF pin that would have avoided
loading the VREF signal, preventing voltage drops from the nominal 1.5V.

Since the resulting signal `tec_i` derived from these two signals can
have an error of around +/- 100mA, and readback may affect the stability
performance of the Thermostat, disable current readback entirely on
affected hardware revisions for now.

See https://github.com/sinara-hw/Thermostat/issues/117 and
https://github.com/sinara-hw/Thermostat/issues/120.

On hardware revisions v3.x and above, this would be fixed.
2024-01-31 12:12:22 +08:00
atse 17edae44fb README: Proofread fan control documentation 2024-01-30 12:43:19 +08:00
atse 03b4561142 Refactor current_abs_max_tec_i to use uom 2024-01-30 11:41:52 +08:00
atse 631a10938d README: Remove VREF 2024-01-26 17:00:27 +08:00
atse 6cd6a6a2c2 Fix warning '...not permit being left uninit..d'
Put SocketState initialisation logic in new. This avoids using an unsafe
and unnerving MaybeUninit::uninit().assume_init() to initialise an
array, which the compiler yells at since it causes undefined behavior.
2024-01-17 15:29:56 +08:00
atse b93e2fbb7b Update rust edition 2024-01-17 15:29:56 +08:00
atse 76b95f66e0 Use latest working stable rust 2024-01-17 15:29:41 +08:00
atse 8008870bc1 Switch panic_handler to panic_halt
Move away from panic_abort as it uses intrinsics, which is nightly only.
2024-01-17 15:29:15 +08:00
atse 7646ff9037 README: Avoid deprecated OpenOCD ST-Link config
The config file interface/stlink-v2-1.cfg is deprecated, and the warning
message encourages the switch to interface/stlink.cfg. Do accordingly.
2024-01-04 12:44:52 +08:00
atse 6f81a63d12 Remove unused LED parameters 2023-09-20 15:51:37 +08:00
atse 78012f6fdd flake: Use rust from manifest, not from pkgs
Fix the rustPlatform deprecation warnings properly.
2023-09-20 11:29:38 +08:00
atse bb4f43fe1c Remove stale reference to channel_state vref 2023-08-22 17:16:25 +08:00
atse 9df0fe406f Remove VREF in reports
Since VREF is an implementation detail, there shouldn't be a need to
include it in reports.

The ChannelState vref is removed along with it as its only use was to
save VREF measurements for later reporting.
2023-08-22 11:40:42 +08:00
topquark12 5ba74c6d9b README: Correct expected TEC polarity
Adhere to the general convention of TECs cooling down with positive
voltages.
2023-08-15 16:37:51 +08:00
atse 6f0acc73b8 Update LED L3 for PID status on every cycle
Check if PID is engaged on any channel every cycle, and match the status
with LED L3.
2023-08-10 16:43:19 +08:00
atse f29e86310d Update nix repos 2023-08-09 11:23:32 +08:00
atse b04a61c414 Turn off LED L3 only when all channels have no PID
Change the behaviour of LED L3 to turn off only when all channels have
PID disengaged, as opposed to when any channel disengages PID.

Otherwise, when disengaging PID on a Thermostat that has had both
channels engaged in PID, the LED would turn off, even when PID is still
engaged on the other channel.

This lets the LED better reflect the status of the Thermostat as a
whole, as it would stay on as long as PID is engaged on at least one
channel.
2023-08-07 16:09:54 +08:00
atse cd680dd6cd README: Correct unit of time in reports 2023-07-20 17:45:16 +08:00
47 changed files with 2035 additions and 3390 deletions

3
.gitignore vendored
View File

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

8
Cargo.lock generated
View File

@ -327,10 +327,10 @@ dependencies = [
]
[[package]]
name = "panic-abort"
version = "0.3.2"
name = "panic-halt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e20e6499bbbc412f280b04a42346b356c6fa0753d5fd22b7bd752ff34c778ee"
checksum = "de96540e0ebde571dc55c73d60ef407c653844e6f9a1e2fdbd40c07b9252d812"
[[package]]
name = "panic-semihosting"
@ -563,7 +563,7 @@ dependencies = [
"nb 1.0.0",
"nom",
"num-traits",
"panic-abort",
"panic-halt",
"panic-semihosting",
"serde",
"serde-json-core",

View File

@ -7,14 +7,14 @@ authors = ["Astro <astro@spaceboyz.net>"]
version = "0.0.0"
keywords = ["thermostat", "laser", "physics"]
repository = "https://git.m-labs.hk/M-Labs/thermostat"
edition = "2018"
edition = "2021"
[package.metadata.docs.rs]
features = []
default-target = "thumbv7em-none-eabihf"
[dependencies]
panic-abort = "0.3"
panic-halt = "0.2"
panic-semihosting = { version = "0.5", optional = true }
log = "0.4"
bare-metal = "1"

126
README.md
View File

@ -23,13 +23,13 @@ cargo build --release
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 manifest file pulled in `flake.nix` to determine which Rust version to use.
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.
## Debugging
Connect SWDIO/SWCLK/RST/GND to a programmer such as ST-Link v2.1. Run OpenOCD:
```shell
openocd -f interface/stlink-v2-1.cfg -f target/stm32f4x.cfg
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg
```
You may need to power up the programmer before powering the device.
@ -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
@ -64,24 +64,10 @@ On a Windows machine install [st.com](https://st.com) DfuSe USB device firmware
### OpenOCD
```shell
openocd -f interface/stlink-v2-1.cfg -f target/stm32f4x.cfg -c "program target/thumbv7em-none-eabihf/release/thermostat verify reset;exit"
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg -c "program target/thumbv7em-none-eabihf/release/thermostat verify reset;exit"
```
## GUI Usage
A GUI has been developed for easy configuration and plotting of key parameters.
The Python GUI program is located at pytec/tecQT.py
The GUI is developed based on the Python library pyqtgraph. The environment needed to run the GUI is configured automatically by running:
```shell
nix develop
```
The GUI program assumes the default IP and port of 192.168.1.26 23 is used. If a different IP or port is used, the IP and port setting should be changed in the GUI code.
## Command Line Usage
## Network
### Connecting
@ -98,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
@ -109,16 +93,15 @@ 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> pid` | Let output current to be controlled by the PID |
|-------------------------------------------|-------------------------------------------------------------------------------|
| `report` | Show latest report of channel parameters (see *Reports* section) |
| `output` | Show current output settings |
| `output <0/1> max_i_pos <amp>` | Set maximum positive output current, clamped to [0, 2] |
| `output <0/1> max_i_neg <amp>` | Set maximum negative output current, clamped to [0, 2] |
| `output <0/1> max_v <volt>` | Set maximum output voltage, clamped to [0, 4] |
| `output <0/1> i_set <amp>` | Disengage PID, set fixed output current, clamped to [-2, 2] |
| `output <0/1> polarity <normal/reversed>` | Set output current polarity, with 'normal' being the front panel polarity |
| `output <0/1> pid` | Let output current to be controlled by the PID |
| `center <0/1> <volt>` | Set the MAX1968 0A-centerpoint to the specified fixed voltage |
| `center <0/1> vref` | Set the MAX1968 0A-centerpoint to measure from VREF |
| `pid` | Show PID configuration |
@ -128,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 |
@ -161,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
@ -200,46 +183,47 @@ postfilter rate can be tuned with the `postfilter` command.
When using a TEC module with the Thermostat, the Thermostat expects the thermal load (where the thermistor is located) to cool down with a positive software current set point, and heat up with a negative current set point.
If the Thermostat is used for temperature control with the Sinara 5432 DAC "Zotino", and is connected via an IDC cable, the TEC polarity may need to be reversed with the `output <ch> polarity reversed` TCP command.
Testing heat flow direction with a low set current is recommended before installation of the TEC module.
### Limits
Each of the MAX1968 TEC driver has analog/PWM inputs for setting
Each channel has maximum value settings, for setting
output limits.
Use the `pwm` command to see current settings and maximum values.
Use the `output` command to see them.
| Limit | Unit | Description |
| --- | :---: | --- |
| `max_v` | Volts | Maximum voltage |
| `max_i_pos` | Amperes | Maximum positive current |
| `max_i_neg` | Amperes | Maximum negative current |
| `i_set` | Amperes | (Not a limit; Open-loop mode) |
Example: set the maximum voltage of channel 0 to 1.5 V.
```
pwm 0 max_v 1.5
output 0 max_v 1.5
```
Example: set the maximum negative current of channel 0 to -3 A.
Example: set the maximum negative current of channel 0 to -2 A.
```
pwm 0 max_i_neg 3
output 0 max_i_neg 2
```
Example: set the maximum positive current of channel 1 to 3 A.
Example: set the maximum positive current of channel 1 to 2 A.
```
pwm 0 max_i_pos 3
output 1 max_i_pos 2
```
### Open-loop mode
To manually control TEC output current, omit the limit parameter of
the `pwm` command. Doing so will disengage the PID control for that
To manually control TEC output current, set a fixed output current with
the `output` command. Doing so will disengage the PID control for that
channel.
Example: set output current of channel 0 to 0 A.
```
pwm 0 i_set 0
output 0 i_set 0
```
## PID-stabilized temperature control
@ -252,7 +236,23 @@ pid 0 target 20
Enter closed-loop mode by switching control of the TEC output current
of channel 0 to the PID algorithm:
```
pwm 0 pid
output 0 pid
```
### PID output clamping
It is possible to clamp the PID algorithm output independently of channel output limits. This is desirable when e.g. there is a need to keep the current value above a certain threshold in closed-loop mode.
Note that the actual output will still ultimately be limited by the `max_i_pos` and `max_i_neg` values.
Set PID maximum output of channel 0 to 1.5 A.
```
pid 0 output_max 1.5
```
Set PID minimum output of channel 0 to 0.1 A.
```
pid 0 output_min 0.1
```
## LED indicators
@ -265,20 +265,19 @@ 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 |
| --- | :---: | --- |
| `channel` | Integer | Channel `0`, or `1` |
| `time` | Milliseconds | Temperature measurement time |
| `time` | Seconds | Temperature measurement time |
| `interval` | Seconds | Time elapsed since last report update on channel |
| `adc` | Volts | AD7172 input |
| `sens` | Ohms | Thermistor resistance derived from `adc` |
| `temperature` | Degrees Celsius | Steinhart-Hart conversion result derived from `sens` |
| `temperature` | Degrees Celsius | B-Parameter conversion result derived from `sens` |
| `pid_engaged` | Boolean | `true` if in closed-loop mode |
| `i_set` | Amperes | TEC output current |
| `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 |
@ -286,18 +285,19 @@ with the following keys.
| `tec_u_meas` | Volts | Measurement of the voltage across the TEC |
| `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).
## 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`.

View File

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

View File

@ -1,41 +1,45 @@
{
"nodes": {
"mozilla-overlay": {
"flake": false,
"locked": {
"lastModified": 1638887313,
"narHash": "sha256-FMYV6rVtvSIfthgC1sK1xugh3y7muoQcvduMdriz4ag=",
"owner": "mozilla",
"repo": "nixpkgs-mozilla",
"rev": "7c1e8b1dd6ed0043fb4ee0b12b815256b0b9de6f",
"type": "github"
},
"original": {
"owner": "mozilla",
"repo": "nixpkgs-mozilla",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1684171562,
"narHash": "sha256-BMUWjVWAUdyMWKk0ATMC9H0Bv4qAV/TXwwPUvTiC5IQ=",
"lastModified": 1722791413,
"narHash": "sha256-rCTrlCWvHzMCNcKxPE3Z/mMK2gDZ+BvvpEVyRM4tKmU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "55af203d468a6f5032a519cba4f41acf5a74b638",
"rev": "8b5b6723aca5a51edf075936439d9cd3947b7b2c",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-22.11",
"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"
}
}
},

115
flake.nix
View File

@ -1,33 +1,34 @@
{
description = "Firmware for the Sinara 8451 Thermostat";
inputs.nixpkgs.url = github:NixOS/nixpkgs/nixos-22.11;
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/2021-10-26/channel-rust-nightly.toml";
sha256 = "sha256-1hLbypXA+nuH7o3AHCokzSBZAvQxvef4x9+XxO3aBao=";
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;
rust = pkgs.rust-bin.stable."1.66.0".default.override {
extensions = [ "rust-src" ];
targets = [ "thumbv7em-none-eabihf" ];
};
rust = rustChannelOfTargets "nightly" null targets;
rustPlatform = pkgs.recurseIntoAttrs (pkgs.makeRustPlatform {
rustPlatform = pkgs.makeRustPlatform {
rustc = rust;
cargo = rust;
});
thermostat = rustPlatform.buildRustPackage rec {
};
thermostat = rustPlatform.buildRustPackage {
name = "thermostat";
version = "0.0.0";
@ -54,67 +55,49 @@
'';
dontFixup = true;
auditable = false;
};
qasync = pkgs.python3Packages.buildPythonPackage rec {
pname = "qasync";
version = "0.24.0";
src = pkgs.fetchFromGitHub {
owner = "CabbageDevelopment";
repo = "qasync";
rev = "v${version}";
sha256 = "sha256-ls5F+VntXXa3n+dULaYWK9sAmwly1nk/5+RGWLrcf2Y=";
};
propagatedBuildInputs = [ pkgs.python3Packages.pyqt6 ];
checkInputs = [ pkgs.python3Packages.pytest ];
checkPhase = ''
pytest -k 'test_qthreadexec.py' # the others cause the test execution to be aborted, I think because of asyncio
'';
};
thermostat_gui = pkgs.python3Packages.buildPythonPackage rec {
pname = "thermostat_gui";
pythermostat = pkgs.python3Packages.buildPythonPackage {
pname = "pythermostat";
version = "0.0.0";
src = self;
format = "pyproject";
src = "${self}/pythermostat";
preBuild =
''
export VERSIONEER_OVERRIDE=${version}
export VERSIONEER_REV=v0.0.0
'';
nativeBuildInputs = [ pkgs.qt6.wrapQtAppsHook ];
propagatedBuildInputs = (with pkgs.python3Packages; [ pyqtgraph pyqt6 qasync]);
dontWrapQtApps = true;
postFixup = ''
ls -al $out/
wrapQtApp "$out/pytec/tec_qt"
'';
propagatedBuildInputs =
with pkgs.python3Packages; [
numpy
matplotlib
];
};
in {
in
{
packages.x86_64-linux = {
inherit thermostat qasync thermostat_gui;
inherit thermostat pythermostat;
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; [
rustPlatform.rust.rustc
rustPlatform.rust.cargo
openocd dfu-util
] ++ (with python3Packages; [
numpy matplotlib pyqtgraph setuptools pyqt6 qasync
packages =
with pkgs;
[
rust
llvm
openocd
dfu-util
rlwrap
]
++ (with python3Packages; [
numpy
matplotlib
]);
shellHook=
''
export QT_PLUGIN_PATH=${pkgs.qt6.qtbase}/${pkgs.qt6.qtbase.dev.qtPluginPrefix}
export QML2_IMPORT_PATH=${pkgs.qt6.qtbase}/${pkgs.qt6.qtbase.dev.qtQmlPrefix}
'';
};
defaultPackage.x86_64-linux = thermostat;
formatter.x86_64-linux = nixpkgs.legacyPackages.x86_64-linux.nixfmt-rfc-style;
};
}

View File

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

View File

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

View File

@ -1,12 +0,0 @@
from setuptools import setup, find_packages
setup(
name="pytec",
version="0.0",
author="M-Labs",
url="https://git.m-labs.hk/M-Labs/thermostat",
description="Control TEC",
license="GPLv3",
install_requires=["setuptools"],
packages=find_packages(),
)

View File

@ -1,335 +0,0 @@
from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph.parametertree.parameterTypes as pTypes
from pyqtgraph.parametertree import Parameter, ParameterTree, ParameterItem, registerParameterType
import numpy as np
import pyqtgraph as pg
from pytec.client import Client
from enum import Enum
from autotune import PIDAutotune, PIDAutotuneState
rec_len = 1000
refresh_period = 20
TECparams = [[
{'tag': 'report', 'type': 'parent', 'children': [
{'tag': 'pid_engaged', 'type': 'bool', 'value': False},
]},
{'tag': 'pwm', 'type': 'parent', 'children': [
{'tag': 'max_i_pos', 'type': 'float', 'value': 0},
{'tag': 'max_i_neg', 'type': 'float', 'value': 0},
{'tag': 'max_v', 'type': 'float', 'value': 0},
{'tag': 'i_set', 'type': 'float', 'value': 0},
]},
{'tag': 'pid', 'type': 'parent', 'children': [
{'tag': 'kp', 'type': 'float', 'value': 0},
{'tag': 'ki', 'type': 'float', 'value': 0},
{'tag': 'kd', 'type': 'float', 'value': 0},
{'tag': 'output_min', 'type': 'float', 'value': 0},
{'tag': 'output_max', 'type': 'float', 'value': 0},
]},
{'tag': 's-h', 'type': 'parent', 'children': [
{'tag': 't0', 'type': 'float', 'value': 0},
{'tag': 'r0', 'type': 'float', 'value': 0},
{'tag': 'b', 'type': 'float', 'value': 0},
]},
{'tag': 'PIDtarget', 'type': 'parent', 'children': [
{'tag': 'target', 'type': 'float', 'value': 0},
]},
] for _ in range(2)]
GUIparams = [[
{'name': 'Disable Output', 'type': 'action', 'tip': 'Disable Output'},
{'name': 'Constant Current', 'type': 'group', 'children': [
{'name': 'Set Current', 'type': 'float', 'value': 0, 'step': 0.1, 'limits': (-3, 3), 'siPrefix': True,
'suffix': 'A'},
]},
{'name': 'Temperature PID', 'type': 'bool', 'value': False, 'children': [
{'name': 'Set Temperature', 'type': 'float', 'value': 25, 'step': 0.1, 'limits': (-273, 300), 'siPrefix': True,
'suffix': 'C'},
]},
{'name': 'Output Config', 'expanded': False, 'type': 'group', 'children': [
{'name': 'Max Current', 'type': 'float', 'value': 0, 'step': 0.1, 'limits': (0, 3), 'siPrefix': True,
'suffix': 'A'},
{'name': 'Max Voltage', 'type': 'float', 'value': 0, 'step': 0.1, 'limits': (0, 5), 'siPrefix': True,
'suffix': 'V'},
]},
{'name': 'Thermistor Config', 'expanded': False, 'type': 'group', 'children': [
{'name': 'T0', 'type': 'float', 'value': 25, 'step': 0.1, 'limits': (-100, 100), 'siPrefix': True,
'suffix': 'C'},
{'name': 'R0', 'type': 'float', 'value': 10000, 'step': 1, 'siPrefix': True, 'suffix': 'Ohm'},
{'name': 'Beta', 'type': 'float', 'value': 3950, 'step': 1},
]},
{'name': 'PID Config', 'expanded': False, 'type': 'group', 'children': [
{'name': 'kP', 'type': 'float', 'value': 0, 'step': 0.1},
{'name': 'kI', 'type': 'float', 'value': 0, 'step': 0.1},
{'name': 'kD', 'type': 'float', 'value': 0, 'step': 0.1},
{'name': 'PID Auto Tune', 'expanded': False, 'type': 'group', 'children': [
{'name': 'Target Temperature', 'type': 'float', 'value': 20, 'step': 0.1, 'siPrefix': True, 'suffix': 'C'},
{'name': 'Test Current', 'type': 'float', 'value': 1, 'step': 0.1, 'siPrefix': True, 'suffix': 'A'},
{'name': 'Temperature Swing', 'type': 'float', 'value': 1.5, 'step': 0.1, 'siPrefix': True, 'suffix': 'C'},
{'name': 'Run', 'type': 'action', 'tip': 'Run'},
]},
]},
{'name': 'Save to flash', 'type': 'action', 'tip': 'Save to flash'},
] for _ in range(2)]
autoTuner = [PIDAutotune(20, 1, 1, 1.5, refresh_period / 1000),
PIDAutotune(20, 1, 1, 1.5, refresh_period / 1000)]
# If anything changes in the tree, print a message
def change(param, changes, ch):
print("tree changes:")
for param, change, data in changes:
path = paramList[ch].childPath(param)
if path is not None:
childName = '.'.join(path)
else:
childName = param.name()
print(' parameter: %s' % childName)
print(' change: %s' % change)
print(' data: %s' % str(data))
print(' ----------')
if childName == 'Disable Output':
tec.set_param('pwm', ch, 'i_set', 0)
paramList[ch].child('Constant Current').child('Set Current').setValue(0)
paramList[ch].child('Temperature PID').setValue(False)
autoTuner[ch].setOff()
if childName == 'Temperature PID':
if (data):
tec.set_param("pwm", ch, "pid")
else:
tec.set_param('pwm', ch, 'i_set', paramList[ch].child('Constant Current').child('Set Current').value())
if childName == 'Constant Current.Set Current':
tec.set_param('pwm', ch, 'i_set', data)
paramList[ch].child('Temperature PID').setValue(False)
if childName == 'Temperature PID.Set Temperature':
tec.set_param('pid', ch, 'target', data)
if childName == 'Output Config.Max Current':
tec.set_param('pwm', ch, 'max_i_pos', data)
tec.set_param('pwm', ch, 'max_i_neg', data)
tec.set_param('pid', ch, 'output_min', -data)
tec.set_param('pid', ch, 'output_max', data)
if childName == 'Output Config.Max Voltage':
tec.set_param('pwm', ch, 'max_v', data)
if childName == 'Thermistor Config.T0':
tec.set_param('s-h', ch, 't0', data)
if childName == 'Thermistor Config.R0':
tec.set_param('s-h', ch, 'r0', data)
if childName == 'Thermistor Config.Beta':
tec.set_param('s-h', ch, 'b', data)
if childName == 'PID Config.kP':
tec.set_param('pid', ch, 'kp', data)
if childName == 'PID Config.kI':
tec.set_param('pid', ch, 'ki', data)
if childName == 'PID Config.kD':
tec.set_param('pid', ch, 'kd', data)
if childName == 'PID Config.PID Auto Tune.Run':
autoTuner[ch].setParam(
paramList[ch].child('PID Config').child('PID Auto Tune').child('Target Temperature').value(),
paramList[ch].child('PID Config').child('PID Auto Tune').child('Test Current').value(),
paramList[ch].child('PID Config').child('PID Auto Tune').child('Temperature Swing').value(),
refresh_period / 1000,
1)
autoTuner[ch].setReady()
paramList[ch].child('Temperature PID').setValue(False)
if childName == 'Save to flash':
tec.save_config()
def change0(param, changes):
change(param, changes, 0)
def change1(param, changes):
change(param, changes, 1)
class Curves:
def __init__(self, legend: str, key: str, channel: int, color: str, buffer_len: int, period: int):
self.curveItem = pg.PlotCurveItem(pen=({'color': color, 'width': 1}))
self.legendStr = legend
self.keyStr = key
self.channel = channel
self.data_buf = np.zeros(buffer_len)
self.time_stamp = np.zeros(buffer_len)
self.buffLen = buffer_len
self.period = period
def update(self, tec_data, cnt):
if cnt == 0:
np.copyto(self.data_buf, np.full(self.buffLen, tec_data[self.channel][self.keyStr]))
else:
self.data_buf[:-1] = self.data_buf[1:]
self.data_buf[-1] = tec_data[self.channel][self.keyStr]
self.time_stamp[:-1] = self.time_stamp[1:]
self.time_stamp[-1] = cnt * self.period / 1000
self.curveItem.setData(x=self.time_stamp, y=self.data_buf)
class Graph:
def __init__(self, parent: pg.LayoutWidget, title: str, row: int, col: int, curves: list[Curves]):
self.plotItem = pg.PlotWidget(title=title)
self.legendItem = pg.LegendItem(offset=(75, 30), brush=(50, 50, 200, 150))
self.legendItem.setParentItem(self.plotItem.getPlotItem())
parent.addWidget(self.plotItem, row, col)
self.curves = curves
for curve in self.curves:
self.plotItem.addItem(curve.curveItem)
self.legendItem.addItem(curve.curveItem, curve.legendStr)
def update(self, tec_data, cnt):
for curve in self.curves:
curve.update(tec_data, cnt)
self.plotItem.setRange(
xRange=[(cnt - self.curves[0].buffLen) * self.curves[0].period / 1000, cnt * self.curves[0].period / 1000])
def TECsync():
global TECparams
for channel in range(2):
for parents in TECparams[channel]:
if parents['tag'] == 'report':
for data in tec.report_mode():
for children in parents['children']:
print(data)
children['value'] = data[channel][children['tag']]
if quit:
break
if parents['tag'] == 'pwm':
for children in parents['children']:
children['value'] = tec.get_pwm()[channel][children['tag']]['value']
if parents['tag'] == 'pid':
for children in parents['children']:
children['value'] = tec.get_pid()[channel]['parameters'][children['tag']]
if parents['tag'] == 's-h':
for children in parents['children']:
children['value'] = tec.get_steinhart_hart()[channel]['params'][children['tag']]
if parents['tag'] == 'PIDtarget':
for children in parents['children']:
children['value'] = tec.get_pid()[channel]['target']
def refreshTreeParam(tempTree: dict, channel: int) -> dict:
tempTree['children']['Constant Current']['children']['Set Current']['value'] = TECparams[channel][1]['children'][3][
'value']
tempTree['children']['Temperature PID']['value'] = TECparams[channel][0]['children'][0]['value']
tempTree['children']['Temperature PID']['children']['Set Temperature']['value'] = \
TECparams[channel][4]['children'][0]['value']
tempTree['children']['Output Config']['children']['Max Current']['value'] = TECparams[channel][1]['children'][0][
'value']
tempTree['children']['Output Config']['children']['Max Voltage']['value'] = TECparams[channel][1]['children'][2][
'value']
tempTree['children']['Thermistor Config']['children']['T0']['value'] = TECparams[channel][3]['children'][0][
'value'] - 273.15
tempTree['children']['Thermistor Config']['children']['R0']['value'] = TECparams[channel][3]['children'][1]['value']
tempTree['children']['Thermistor Config']['children']['Beta']['value'] = TECparams[channel][3]['children'][2][
'value']
tempTree['children']['PID Config']['children']['kP']['value'] = TECparams[channel][2]['children'][0]['value']
tempTree['children']['PID Config']['children']['kI']['value'] = TECparams[channel][2]['children'][1]['value']
tempTree['children']['PID Config']['children']['kD']['value'] = TECparams[channel][2]['children'][2]['value']
return tempTree
cnt = 0
def updateData():
global cnt
for data in tec.report_mode():
ch0tempGraph.update(data, cnt)
ch1tempGraph.update(data, cnt)
ch0currentGraph.update(data, cnt)
ch1currentGraph.update(data, cnt)
for channel in range(2):
if (autoTuner[channel].state() == PIDAutotuneState.STATE_READY or
autoTuner[channel].state() == PIDAutotuneState.STATE_RELAY_STEP_UP or
autoTuner[channel].state() == PIDAutotuneState.STATE_RELAY_STEP_DOWN):
autoTuner[channel].run(data[channel]['temperature'], data[channel]['time'])
tec.set_param('pwm', channel, 'i_set', autoTuner[channel].output())
elif autoTuner[channel].state() == PIDAutotuneState.STATE_SUCCEEDED:
kp, ki, kd = autoTuner[channel].get_tec_pid()
autoTuner[channel].setOff()
paramList[channel].child('PID Config').child('kP').setValue(kp)
paramList[channel].child('PID Config').child('kI').setValue(ki)
paramList[channel].child('PID Config').child('kD').setValue(kd)
tec.set_param('pid', channel, 'kp', kp)
tec.set_param('pid', channel, 'ki', ki)
tec.set_param('pid', channel, 'kd', kd)
elif autoTuner[channel].state() == PIDAutotuneState.STATE_FAILED:
tec.set_param('pwm', channel, 'i_set', 0)
autoTuner[channel].setOff()
if quit:
break
cnt += 1
if __name__ == '__main__':
tec = Client(host="192.168.1.26", port=23, timeout=None)
app = pg.mkQApp()
pg.setConfigOptions(antialias=True)
mw = QtGui.QMainWindow()
mw.setWindowTitle('Thermostat Control Panel')
mw.resize(1920, 1200)
cw = QtGui.QWidget()
mw.setCentralWidget(cw)
l = QtGui.QVBoxLayout()
layout = pg.LayoutWidget()
l.addWidget(layout)
cw.setLayout(l)
## Create tree of Parameter objects
paramList = [Parameter.create(name='GUIparams', type='group', children=GUIparams[0]),
Parameter.create(name='GUIparams', type='group', children=GUIparams[1])]
ch0Tree = ParameterTree()
ch0Tree.setParameters(paramList[0], showTop=False)
ch1Tree = ParameterTree()
ch1Tree.setParameters(paramList[1], showTop=False)
TECsync()
paramList[0].restoreState(refreshTreeParam(paramList[0].saveState(), 0))
paramList[1].restoreState(refreshTreeParam(paramList[1].saveState(), 1))
paramList[0].sigTreeStateChanged.connect(change0)
paramList[1].sigTreeStateChanged.connect(change1)
layout.addWidget(ch0Tree, 1, 1, 1, 1)
layout.addWidget(ch1Tree, 2, 1, 1, 1)
ch0tempGraph = Graph(layout, 'Channel 0 Temperature', 1, 2,
[Curves('Feedback', 'temperature', 0, 'r', rec_len, refresh_period)])
ch1tempGraph = Graph(layout, 'Channel 1 Temperature', 2, 2,
[Curves('Feedback', 'temperature', 1, 'r', rec_len, refresh_period)])
ch0currentGraph = Graph(layout, 'Channel 0 Current', 1, 3,
[Curves('Feedback', 'tec_i', 0, 'r', rec_len, refresh_period),
Curves('Setpoint', 'i_set', 0, 'g', rec_len, refresh_period)])
ch1currentGraph = Graph(layout, 'Channel 1 Current', 2, 3,
[Curves('Feedback', 'tec_i', 1, 'r', rec_len, refresh_period),
Curves('Setpoint', 'i_set', 1, 'g', rec_len, refresh_period)])
t = QtCore.QTimer()
t.timeout.connect(updateData)
t.start(refresh_period)
mw.show()
pg.exec()

View File

@ -1,328 +0,0 @@
from PyQt6 import QtWidgets, uic
from PyQt6.QtCore import QThread, QThreadPool, pyqtSignal, QRunnable, QObject, QSignalBlocker, pyqtSlot, QDeadlineTimer
from pyqtgraph import PlotWidget
from pyqtgraph.parametertree import Parameter, ParameterTree, ParameterItem, registerParameterType
import pyqtgraph as pg
import sys
import argparse
import logging
import asyncio
import atexit
from qasync import asyncSlot, QEventLoop
import qasync
from pytec.client import Client
# pyuic6 -x tec_qt.ui -o ui_tec_qt.py
from ui_tec_qt import Ui_MainWindow
tec_client: Client = None
# ui = None
ui: Ui_MainWindow = None
thread_pool = QThreadPool.globalInstance()
connection_watcher = None
client_watcher = None
app: QtWidgets.QApplication = None
class CommandsParameter(Parameter):
def __init__(self, **opts):
super().__init__()
self.opts["commands"] = opts.get("commands", None)
self.opts["payload"] = opts.get("payload", None)
ThermostatParams = [[
{'name': 'Constant Current', 'type': 'float', 'value': 0, 'step': 0.1, 'limits': (-3, 3), 'siPrefix': True,
'suffix': 'A', 'commands': [f'pwm {ch} i_set {{value}}']},
{'name': 'Temperature PID', 'type': 'bool', 'value': False, 'commands': [f'pwm {ch} pid'], 'payload': ch,
'children': [
{'name': 'Set Temperature', 'type': 'float', 'value': 25, 'step': 0.1, 'limits': (-273, 300), 'siPrefix': True,
'suffix': '°C', 'commands': [f'pid {ch} target {{value}}']},
]},
{'name': 'Output Config', 'expanded': False, 'type': 'group', 'children': [
{'name': 'Max Current', 'type': 'float', 'value': 0, 'step': 0.1, 'limits': (0, 3), 'siPrefix': True,
'suffix': 'A', 'commands': [f'pwm {ch} max_i_pos {{value}}', f'pwm {ch} max_i_neg {{value}}',
f'pid {ch} output_min -{{value}}', f'pid {ch} output_max {{value}}']},
{'name': 'Max Voltage', 'type': 'float', 'value': 0, 'step': 0.1, 'limits': (0, 5), 'siPrefix': True,
'suffix': 'V', 'commands': [f'pwm {ch} max_v {{value}}']},
]},
{'name': 'Thermistor Config', 'expanded': False, 'type': 'group', 'children': [
{'name': 'T0', 'type': 'float', 'value': 25, 'step': 0.1, 'limits': (-100, 100), 'siPrefix': True,
'suffix': 'C', 'commands': [f's-h {ch} t0 {{value}}']},
{'name': 'R0', 'type': 'float', 'value': 10000, 'step': 1, 'siPrefix': True, 'suffix': 'Ohm',
'commands': [f's-h {ch} r0 {{value}}']},
{'name': 'Beta', 'type': 'float', 'value': 3950, 'step': 1, 'commands': [f's-h {ch} b {{value}}']},
]},
{'name': 'PID Config', 'expanded': False, 'type': 'group', 'children': [
{'name': 'kP', 'type': 'float', 'value': 0, 'step': 0.1, 'commands': [f'pid {ch} kp {{value}}']},
{'name': 'kI', 'type': 'float', 'value': 0, 'step': 0.1, 'commands': [f'pid {ch} ki {{value}}']},
{'name': 'kD', 'type': 'float', 'value': 0, 'step': 0.1, 'commands': [f'pid {ch} kd {{value}}']},
{'name': 'PID Auto Tune', 'expanded': False, 'type': 'group', 'children': [
{'name': 'Target Temperature', 'type': 'float', 'value': 20, 'step': 0.1, 'siPrefix': True, 'suffix': 'C'},
{'name': 'Test Current', 'type': 'float', 'value': 1, 'step': 0.1, 'siPrefix': True, 'suffix': 'A'},
{'name': 'Temperature Swing', 'type': 'float', 'value': 1.5, 'step': 0.1, 'siPrefix': True, 'suffix': 'C'},
{'name': 'Run', 'type': 'action', 'tip': 'Run'},
]},
]}
] for ch in range(2)]
params = [CommandsParameter.create(name='Thermostat Params 0', type='group', children=ThermostatParams[0]),
CommandsParameter.create(name='Thermostat Params 1', type='group', children=ThermostatParams[1])]
def get_argparser():
parser = argparse.ArgumentParser(description="ARTIQ master")
parser.add_argument("--connect", default=None, action="store_true",
help="Automatically connect to the specified Thermostat in IP:port format")
parser.add_argument('IP', metavar="ip", default=None, nargs='?')
parser.add_argument('PORT', metavar="port", default=None, nargs='?')
parser.add_argument("-l", "--log", dest="logLevel", choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
help="Set the logging level")
return parser
class WatchConnectTask(QThread):
connected = pyqtSignal(bool)
hw_rev = pyqtSignal(dict)
connecting = pyqtSignal()
fan_update = pyqtSignal(object)
def __init__(self, parent, ip, port):
self.ip = ip
self.port = port
super().__init__(parent)
def run(self):
global tec_client
try:
if tec_client:
tec_client.disconnect()
tec_client = None
self.connected.emit(False)
else:
self.connecting.emit()
tec_client = Client(host=self.ip, port=self.port, timeout=30)
self.connected.emit(True)
thread_pool.start(ClientTask(lambda: self.hw_rev.emit(tec_client.hw_rev())))
thread_pool.start(ClientTask(lambda: self.fan_update.emit(tec_client.fan())))
except Exception as e:
logging.error(f"Failed communicating to the {self.ip}:{self.port}: {e}")
self.connected.emit(False)
@pyqtSlot()
def client_disconnected(self):
global tec_client
if tec_client:
tec_client.disconnect()
tec_client = None
self.connected.emit(False)
class ClientWatcher(QThread):
fan_update = pyqtSignal(object)
pwm_update = pyqtSignal(object)
report_update = pyqtSignal(object)
pid_update = pyqtSignal(object)
def __init__(self, parent, update_s):
self.update_s = update_s
self.running = True
super().__init__(parent)
def run(self):
while self.running:
thread_pool.start(ClientTask(lambda: self.update_params()))
self.msleep(int(self.update_s * 1000))
def update_params(self):
self.fan_update.emit(tec_client.fan())
self.pwm_update.emit(tec_client.get_pwm())
self.report_update.emit(tec_client._command("report"))
self.pid_update.emit(tec_client.get_pid())
@pyqtSlot()
def stop_watching(self):
self.running = False
deadline = QDeadlineTimer()
deadline.setDeadline(100)
self.wait(deadline)
self.terminate()
@pyqtSlot()
def set_update_s(self):
self.update_s = ui.report_refresh_spin.value()
class ClientTask(QRunnable):
def __init__(self, func, *args, **kwargs):
self.func = func
self.args = args
self.kwargs = kwargs
super().__init__()
def run(self):
try:
self.func(*self.args, **self.kwargs)
except (TimeoutError, OSError):
logging.warning("Client connection error, disconnecting", exc_info=True)
if connection_watcher:
thread_pool.clear() # clearing all next requests
connection_watcher.client_disconnected()
def connected(result):
global client_watcher, connection_watcher
ui.graph_group.setEnabled(result)
ui.hw_rev_lbl.setEnabled(result)
ui.fan_group.setEnabled(result)
ui.report_group.setEnabled(result)
ui.ip_set_line.setEnabled(not result)
ui.port_set_spin.setEnabled(not result)
ui.status_lbl.setText("Connected" if result else "Disconnected")
ui.connect_btn.setText("Disconnect" if result else "Connect")
if not result:
ui.hw_rev_lbl.setText("Thermostat vX.Y")
ui.fan_group.setStyleSheet("")
if client_watcher:
client_watcher.stop_watching()
client_watcher = None
elif client_watcher is None:
client_watcher = ClientWatcher(ui.main_widget, ui.report_refresh_spin.value())
client_watcher.fan_update.connect(fan_update)
ui.report_apply_btn.clicked.connect(client_watcher.set_update_s)
app.aboutToQuit.connect(client_watcher.stop_watching)
client_watcher.start()
def hw_rev(hw_rev_d: dict):
logging.debug(hw_rev_d)
ui.hw_rev_lbl.setText(f"Thermostat v{hw_rev_d['rev']['major']}.{hw_rev_d['rev']['major']}")
ui.fan_group.setEnabled(hw_rev_d["settings"]["fan_available"])
if hw_rev_d["settings"]["fan_pwm_recommended"]:
ui.fan_group.setStyleSheet("")
ui.fan_group.setToolTip("")
else:
ui.fan_group.setStyleSheet("background-color: yellow")
ui.fan_group.setToolTip("Changing the fan settings of not recommended")
def fan_update(fan_settings):
logging.debug(fan_settings)
if fan_settings is None:
return
with QSignalBlocker(ui.fan_power_slider) as _:
ui.fan_power_slider.setValue(fan_settings["fan_pwm"])
ui.fan_power_slider.setEnabled(not fan_settings["auto_mode"])
with QSignalBlocker(ui.fan_auto_box) as _:
ui.fan_auto_box.setChecked(fan_settings["auto_mode"])
def fan_set():
global tec_client
if tec_client is None or ui.fan_auto_box.isChecked():
return
thread_pool.start(ClientTask(lambda: tec_client.set_param("fan", ui.fan_power_slider.value())))
def fan_auto_set(enabled):
global tec_client
if tec_client is None:
return
ui.fan_power_slider.setEnabled(not enabled)
if enabled:
thread_pool.start(ClientTask(lambda: tec_client.set_param("fan", "auto")))
else:
thread_pool.start(ClientTask(lambda: tec_client.set_param("fan", ui.fan_power_slider.value())))
def connect():
global connection_watcher
connection_watcher = WatchConnectTask(ui.main_widget, ui.ip_set_line.text(), ui.port_set_spin.value())
connection_watcher.connected.connect(connected)
connection_watcher.connecting.connect(lambda: ui.status_lbl.setText("Connecting..."))
connection_watcher.hw_rev.connect(hw_rev)
connection_watcher.fan_update.connect(fan_update)
connection_watcher.start()
app.aboutToQuit.connect(connection_watcher.terminate)
def update_pid(pid_settings):
for settings in pid_settings:
channel = settings["channel"]
with QSignalBlocker(params[channel].sigTreeStateChanged) as _:
params[channel].child("PID Config", "kP").setValue(settings["parameters"]["kp"])
params[channel].child("PID Config", "kI").setValue(settings["parameters"]["ki"])
params[channel].child("PID Config", "kD").setValue(settings["parameters"]["kd"])
if params[channel].child("Temperature PID").value():
params[channel].child("Temperature PID", "Set Temperature").setValue(settings["target"])
def update_report(report_data):
for settings in report_data:
channel = settings["channel"]
with QSignalBlocker(params[channel].sigTreeStateChanged) as _:
params[channel].child("Temperature PID").setValue(settings["pid_engaged"])
def send_command(param, changes):
for param, change, data in changes:
if param.name() == 'Temperature PID' and not data:
ch = param.opts["payload"]
thread_pool.start(ClientTask(
lambda: tec_client.set_param('pwm', ch, 'i_set', params[ch].child('Constant Current').value())))
elif param.opts.get("commands", None) is not None:
thread_pool.start(ClientTask(lambda: [tec_client._command(x.format(value=data)) for x in param.opts["commands"]]))
def set_param_tree():
ui.ch0_tree.setParameters(params[0], showTop=False)
ui.ch1_tree.setParameters(params[1], showTop=False)
params[0].sigTreeStateChanged.connect(send_command)
params[1].sigTreeStateChanged.connect(send_command)
def main():
global ui, thread_pool, app
args = get_argparser().parse_args()
if args.logLevel:
logging.basicConfig(level=getattr(logging, args.logLevel))
app = QtWidgets.QApplication(sys.argv)
loop = QEventLoop(app)
asyncio.set_event_loop(loop)
atexit.register(loop.close)
main_window = QtWidgets.QMainWindow()
ui = Ui_MainWindow()
ui.setupUi(main_window)
# ui = uic.loadUi('tec_qt.ui', main_window)
thread_pool = QThreadPool(parent=ui.main_widget)
thread_pool.setMaxThreadCount(1) # avoid concurrent requests
ui.connect_btn.clicked.connect(lambda _checked: connect())
ui.fan_power_slider.valueChanged.connect(fan_set)
ui.fan_auto_box.stateChanged.connect(fan_auto_set)
set_param_tree()
if args.connect:
if args.IP:
ui.ip_set_line.setText(args.IP)
if args.PORT:
ui.port_set_spin.setValue(int(args.PORT))
ui.connect_btn.click()
main_window.show()
sys.exit(app.exec())
if __name__ == '__main__':
main()

View File

@ -1,671 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1280</width>
<height>720</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>1280</width>
<height>720</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>3840</width>
<height>2160</height>
</size>
</property>
<property name="windowTitle">
<string>Control TEC</string>
</property>
<widget class="QWidget" name="main_widget">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>1</horstretch>
<verstretch>1</verstretch>
</sizepolicy>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<property name="spacing">
<number>3</number>
</property>
<item row="0" column="1">
<layout class="QVBoxLayout" name="main_layout">
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="QFrame" name="graph_group">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>1</horstretch>
<verstretch>1</verstretch>
</sizepolicy>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QGridLayout" name="graphs_layout" rowstretch="1,1" columnstretch="1,1,1" rowminimumheight="100,100" columnminimumwidth="100,100,100">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<property name="spacing">
<number>2</number>
</property>
<item row="1" column="0">
<widget class="ParameterTree" name="ch1_tree" native="true"/>
</item>
<item row="0" column="0">
<widget class="ParameterTree" name="ch0_tree" native="true"/>
</item>
<item row="1" column="1">
<widget class="PlotWidget" name="ch1_t_graph" native="true">
<property name="title">
<string>Channel 1 Temperature</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="PlotWidget" name="ch0_t_graph" native="true">
<property name="title">
<string>Channel 0 Temperature</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="PlotWidget" name="ch0_i_graph" native="true">
<property name="title">
<string>Channel 0 Current</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="PlotWidget" name="ch1_i_graph" native="true">
<property name="title">
<string>Channel 1 Current</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="bottom_settings_group">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>40</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>40</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="spacing">
<number>3</number>
</property>
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<item>
<layout class="QHBoxLayout" name="settings_layout">
<item>
<widget class="QLineEdit" name="ip_set_line">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>160</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>160</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>192.168.1.26</string>
</property>
<property name="maxLength">
<number>15</number>
</property>
<property name="placeholderText">
<string>IP:port for the Thermostat</string>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="port_set_spin">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>70</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>70</width>
<height>16777215</height>
</size>
</property>
<property name="maximum">
<number>65535</number>
</property>
<property name="value">
<number>23</number>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="connect_btn">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="baseSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Connect</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="status_lbl">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>120</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>120</width>
<height>16777215</height>
</size>
</property>
<property name="baseSize">
<size>
<width>120</width>
<height>50</height>
</size>
</property>
<property name="text">
<string>Disconnected</string>
</property>
</widget>
</item>
<item>
<widget class="Line" name="line_0">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<widget class="QWidget" name="report_group" native="true">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>40</width>
<height>0</height>
</size>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="report_layout" stretch="1,1,1">
<property name="spacing">
<number>6</number>
</property>
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<item>
<widget class="QDoubleSpinBox" name="report_refresh_spin">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>70</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>70</width>
<height>16777215</height>
</size>
</property>
<property name="baseSize">
<size>
<width>70</width>
<height>0</height>
</size>
</property>
<property name="decimals">
<number>1</number>
</property>
<property name="minimum">
<double>0.100000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
<property name="stepType">
<enum>QAbstractSpinBox::AdaptiveDecimalStepType</enum>
</property>
<property name="value">
<double>1.000000000000000</double>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="report_box">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>16777215</height>
</size>
</property>
<property name="baseSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Report</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="report_apply_btn">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>16777215</height>
</size>
</property>
<property name="baseSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Apply</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="Line" name="line_1">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<widget class="QWidget" name="fan_group" native="true">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimumSize">
<size>
<width>40</width>
<height>0</height>
</size>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_6">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="gan_layout">
<property name="spacing">
<number>9</number>
</property>
<item>
<widget class="QLabel" name="fan_lbl">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>40</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>40</width>
<height>16777215</height>
</size>
</property>
<property name="baseSize">
<size>
<width>40</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Fan:</string>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="fan_power_slider">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>16777215</height>
</size>
</property>
<property name="baseSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="fan_auto_box">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>70</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>70</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>Auto</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="Line" name="line_3">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="hw_rev_lbl">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>150</width>
<height>16777215</height>
</size>
</property>
<property name="baseSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Thermostat vX.Y</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
<customwidgets>
<customwidget>
<class>PlotWidget</class>
<extends>QWidget</extends>
<header>pyqtgraph</header>
<container>1</container>
</customwidget>
<customwidget>
<class>ParameterTree</class>
<extends>QWidget</extends>
<header>pyqtgraph.parametertree</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@ -1,323 +0,0 @@
# Form implementation generated from reading ui file 'tec_qt.ui'
#
# Created by: PyQt6 UI code generator 6.4.2
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt6 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(1280, 720)
MainWindow.setMinimumSize(QtCore.QSize(1280, 720))
MainWindow.setMaximumSize(QtCore.QSize(3840, 2160))
self.main_widget = QtWidgets.QWidget(parent=MainWindow)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(1)
sizePolicy.setVerticalStretch(1)
sizePolicy.setHeightForWidth(self.main_widget.sizePolicy().hasHeightForWidth())
self.main_widget.setSizePolicy(sizePolicy)
self.main_widget.setObjectName("main_widget")
self.gridLayout_2 = QtWidgets.QGridLayout(self.main_widget)
self.gridLayout_2.setContentsMargins(3, 3, 3, 3)
self.gridLayout_2.setSpacing(3)
self.gridLayout_2.setObjectName("gridLayout_2")
self.main_layout = QtWidgets.QVBoxLayout()
self.main_layout.setSpacing(0)
self.main_layout.setObjectName("main_layout")
self.graph_group = QtWidgets.QFrame(parent=self.main_widget)
self.graph_group.setEnabled(False)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(1)
sizePolicy.setVerticalStretch(1)
sizePolicy.setHeightForWidth(self.graph_group.sizePolicy().hasHeightForWidth())
self.graph_group.setSizePolicy(sizePolicy)
self.graph_group.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.graph_group.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.graph_group.setObjectName("graph_group")
self.graphs_layout = QtWidgets.QGridLayout(self.graph_group)
self.graphs_layout.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetDefaultConstraint)
self.graphs_layout.setContentsMargins(3, 3, 3, 3)
self.graphs_layout.setSpacing(2)
self.graphs_layout.setObjectName("graphs_layout")
self.ch1_tree = ParameterTree(parent=self.graph_group)
self.ch1_tree.setObjectName("ch1_tree")
self.graphs_layout.addWidget(self.ch1_tree, 1, 0, 1, 1)
self.ch0_tree = ParameterTree(parent=self.graph_group)
self.ch0_tree.setObjectName("ch0_tree")
self.graphs_layout.addWidget(self.ch0_tree, 0, 0, 1, 1)
self.ch1_t_graph = PlotWidget(parent=self.graph_group)
self.ch1_t_graph.setObjectName("ch1_t_graph")
self.graphs_layout.addWidget(self.ch1_t_graph, 1, 1, 1, 1)
self.ch0_t_graph = PlotWidget(parent=self.graph_group)
self.ch0_t_graph.setObjectName("ch0_t_graph")
self.graphs_layout.addWidget(self.ch0_t_graph, 0, 1, 1, 1)
self.ch0_i_graph = PlotWidget(parent=self.graph_group)
self.ch0_i_graph.setObjectName("ch0_i_graph")
self.graphs_layout.addWidget(self.ch0_i_graph, 0, 2, 1, 1)
self.ch1_i_graph = PlotWidget(parent=self.graph_group)
self.ch1_i_graph.setObjectName("ch1_i_graph")
self.graphs_layout.addWidget(self.ch1_i_graph, 1, 2, 1, 1)
self.graphs_layout.setColumnMinimumWidth(0, 100)
self.graphs_layout.setColumnMinimumWidth(1, 100)
self.graphs_layout.setColumnMinimumWidth(2, 100)
self.graphs_layout.setRowMinimumHeight(0, 100)
self.graphs_layout.setRowMinimumHeight(1, 100)
self.graphs_layout.setColumnStretch(0, 1)
self.graphs_layout.setColumnStretch(1, 1)
self.graphs_layout.setColumnStretch(2, 1)
self.graphs_layout.setRowStretch(0, 1)
self.graphs_layout.setRowStretch(1, 1)
self.main_layout.addWidget(self.graph_group)
self.bottom_settings_group = QtWidgets.QFrame(parent=self.main_widget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.bottom_settings_group.sizePolicy().hasHeightForWidth())
self.bottom_settings_group.setSizePolicy(sizePolicy)
self.bottom_settings_group.setMinimumSize(QtCore.QSize(0, 40))
self.bottom_settings_group.setMaximumSize(QtCore.QSize(16777215, 40))
self.bottom_settings_group.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.bottom_settings_group.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.bottom_settings_group.setObjectName("bottom_settings_group")
self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.bottom_settings_group)
self.horizontalLayout_2.setContentsMargins(3, 3, 3, 3)
self.horizontalLayout_2.setSpacing(3)
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
self.settings_layout = QtWidgets.QHBoxLayout()
self.settings_layout.setObjectName("settings_layout")
self.ip_set_line = QtWidgets.QLineEdit(parent=self.bottom_settings_group)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.ip_set_line.sizePolicy().hasHeightForWidth())
self.ip_set_line.setSizePolicy(sizePolicy)
self.ip_set_line.setMinimumSize(QtCore.QSize(160, 0))
self.ip_set_line.setMaximumSize(QtCore.QSize(160, 16777215))
self.ip_set_line.setMaxLength(15)
self.ip_set_line.setClearButtonEnabled(True)
self.ip_set_line.setObjectName("ip_set_line")
self.settings_layout.addWidget(self.ip_set_line)
self.port_set_spin = QtWidgets.QSpinBox(parent=self.bottom_settings_group)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.port_set_spin.sizePolicy().hasHeightForWidth())
self.port_set_spin.setSizePolicy(sizePolicy)
self.port_set_spin.setMinimumSize(QtCore.QSize(70, 0))
self.port_set_spin.setMaximumSize(QtCore.QSize(70, 16777215))
self.port_set_spin.setMaximum(65535)
self.port_set_spin.setProperty("value", 23)
self.port_set_spin.setObjectName("port_set_spin")
self.settings_layout.addWidget(self.port_set_spin)
self.connect_btn = QtWidgets.QPushButton(parent=self.bottom_settings_group)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.connect_btn.sizePolicy().hasHeightForWidth())
self.connect_btn.setSizePolicy(sizePolicy)
self.connect_btn.setMinimumSize(QtCore.QSize(100, 0))
self.connect_btn.setMaximumSize(QtCore.QSize(100, 16777215))
self.connect_btn.setBaseSize(QtCore.QSize(100, 0))
self.connect_btn.setObjectName("connect_btn")
self.settings_layout.addWidget(self.connect_btn)
self.status_lbl = QtWidgets.QLabel(parent=self.bottom_settings_group)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.status_lbl.sizePolicy().hasHeightForWidth())
self.status_lbl.setSizePolicy(sizePolicy)
self.status_lbl.setMinimumSize(QtCore.QSize(120, 0))
self.status_lbl.setMaximumSize(QtCore.QSize(120, 16777215))
self.status_lbl.setBaseSize(QtCore.QSize(120, 50))
self.status_lbl.setObjectName("status_lbl")
self.settings_layout.addWidget(self.status_lbl)
self.line_0 = QtWidgets.QFrame(parent=self.bottom_settings_group)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.line_0.sizePolicy().hasHeightForWidth())
self.line_0.setSizePolicy(sizePolicy)
self.line_0.setFrameShape(QtWidgets.QFrame.Shape.VLine)
self.line_0.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken)
self.line_0.setObjectName("line_0")
self.settings_layout.addWidget(self.line_0)
self.report_group = QtWidgets.QWidget(parent=self.bottom_settings_group)
self.report_group.setEnabled(False)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.report_group.sizePolicy().hasHeightForWidth())
self.report_group.setSizePolicy(sizePolicy)
self.report_group.setMinimumSize(QtCore.QSize(40, 0))
self.report_group.setObjectName("report_group")
self.horizontalLayout_4 = QtWidgets.QHBoxLayout(self.report_group)
self.horizontalLayout_4.setContentsMargins(0, 0, 0, 0)
self.horizontalLayout_4.setSpacing(0)
self.horizontalLayout_4.setObjectName("horizontalLayout_4")
self.report_layout = QtWidgets.QHBoxLayout()
self.report_layout.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetDefaultConstraint)
self.report_layout.setContentsMargins(0, -1, -1, -1)
self.report_layout.setSpacing(6)
self.report_layout.setObjectName("report_layout")
self.report_refresh_spin = QtWidgets.QDoubleSpinBox(parent=self.report_group)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.report_refresh_spin.sizePolicy().hasHeightForWidth())
self.report_refresh_spin.setSizePolicy(sizePolicy)
self.report_refresh_spin.setMinimumSize(QtCore.QSize(70, 0))
self.report_refresh_spin.setMaximumSize(QtCore.QSize(70, 16777215))
self.report_refresh_spin.setBaseSize(QtCore.QSize(70, 0))
self.report_refresh_spin.setDecimals(1)
self.report_refresh_spin.setMinimum(0.1)
self.report_refresh_spin.setSingleStep(0.1)
self.report_refresh_spin.setStepType(QtWidgets.QAbstractSpinBox.StepType.AdaptiveDecimalStepType)
self.report_refresh_spin.setProperty("value", 1.0)
self.report_refresh_spin.setObjectName("report_refresh_spin")
self.report_layout.addWidget(self.report_refresh_spin)
self.report_box = QtWidgets.QCheckBox(parent=self.report_group)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.report_box.sizePolicy().hasHeightForWidth())
self.report_box.setSizePolicy(sizePolicy)
self.report_box.setMaximumSize(QtCore.QSize(80, 16777215))
self.report_box.setBaseSize(QtCore.QSize(80, 0))
self.report_box.setObjectName("report_box")
self.report_layout.addWidget(self.report_box)
self.report_apply_btn = QtWidgets.QPushButton(parent=self.report_group)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.report_apply_btn.sizePolicy().hasHeightForWidth())
self.report_apply_btn.setSizePolicy(sizePolicy)
self.report_apply_btn.setMinimumSize(QtCore.QSize(80, 0))
self.report_apply_btn.setMaximumSize(QtCore.QSize(80, 16777215))
self.report_apply_btn.setBaseSize(QtCore.QSize(80, 0))
self.report_apply_btn.setObjectName("report_apply_btn")
self.report_layout.addWidget(self.report_apply_btn)
self.report_layout.setStretch(0, 1)
self.report_layout.setStretch(1, 1)
self.report_layout.setStretch(2, 1)
self.horizontalLayout_4.addLayout(self.report_layout)
self.settings_layout.addWidget(self.report_group)
self.line_1 = QtWidgets.QFrame(parent=self.bottom_settings_group)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.line_1.sizePolicy().hasHeightForWidth())
self.line_1.setSizePolicy(sizePolicy)
self.line_1.setFrameShape(QtWidgets.QFrame.Shape.VLine)
self.line_1.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken)
self.line_1.setObjectName("line_1")
self.settings_layout.addWidget(self.line_1)
self.fan_group = QtWidgets.QWidget(parent=self.bottom_settings_group)
self.fan_group.setEnabled(False)
self.fan_group.setMinimumSize(QtCore.QSize(40, 0))
self.fan_group.setObjectName("fan_group")
self.horizontalLayout_6 = QtWidgets.QHBoxLayout(self.fan_group)
self.horizontalLayout_6.setContentsMargins(0, 0, 0, 0)
self.horizontalLayout_6.setSpacing(0)
self.horizontalLayout_6.setObjectName("horizontalLayout_6")
self.gan_layout = QtWidgets.QHBoxLayout()
self.gan_layout.setSpacing(9)
self.gan_layout.setObjectName("gan_layout")
self.fan_lbl = QtWidgets.QLabel(parent=self.fan_group)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.fan_lbl.sizePolicy().hasHeightForWidth())
self.fan_lbl.setSizePolicy(sizePolicy)
self.fan_lbl.setMinimumSize(QtCore.QSize(40, 0))
self.fan_lbl.setMaximumSize(QtCore.QSize(40, 16777215))
self.fan_lbl.setBaseSize(QtCore.QSize(40, 0))
self.fan_lbl.setObjectName("fan_lbl")
self.gan_layout.addWidget(self.fan_lbl)
self.fan_power_slider = QtWidgets.QSlider(parent=self.fan_group)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.fan_power_slider.sizePolicy().hasHeightForWidth())
self.fan_power_slider.setSizePolicy(sizePolicy)
self.fan_power_slider.setMinimumSize(QtCore.QSize(200, 0))
self.fan_power_slider.setMaximumSize(QtCore.QSize(200, 16777215))
self.fan_power_slider.setBaseSize(QtCore.QSize(200, 0))
self.fan_power_slider.setMaximum(100)
self.fan_power_slider.setOrientation(QtCore.Qt.Orientation.Horizontal)
self.fan_power_slider.setObjectName("fan_power_slider")
self.gan_layout.addWidget(self.fan_power_slider)
self.fan_auto_box = QtWidgets.QCheckBox(parent=self.fan_group)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.fan_auto_box.sizePolicy().hasHeightForWidth())
self.fan_auto_box.setSizePolicy(sizePolicy)
self.fan_auto_box.setMinimumSize(QtCore.QSize(70, 0))
self.fan_auto_box.setMaximumSize(QtCore.QSize(70, 16777215))
self.fan_auto_box.setObjectName("fan_auto_box")
self.gan_layout.addWidget(self.fan_auto_box)
self.horizontalLayout_6.addLayout(self.gan_layout)
self.settings_layout.addWidget(self.fan_group)
self.line_3 = QtWidgets.QFrame(parent=self.bottom_settings_group)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.line_3.sizePolicy().hasHeightForWidth())
self.line_3.setSizePolicy(sizePolicy)
self.line_3.setFrameShape(QtWidgets.QFrame.Shape.VLine)
self.line_3.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken)
self.line_3.setObjectName("line_3")
self.settings_layout.addWidget(self.line_3)
self.hw_rev_lbl = QtWidgets.QLabel(parent=self.bottom_settings_group)
self.hw_rev_lbl.setEnabled(False)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.hw_rev_lbl.sizePolicy().hasHeightForWidth())
self.hw_rev_lbl.setSizePolicy(sizePolicy)
self.hw_rev_lbl.setMinimumSize(QtCore.QSize(150, 0))
self.hw_rev_lbl.setMaximumSize(QtCore.QSize(150, 16777215))
self.hw_rev_lbl.setBaseSize(QtCore.QSize(150, 0))
self.hw_rev_lbl.setObjectName("hw_rev_lbl")
self.settings_layout.addWidget(self.hw_rev_lbl)
self.horizontalLayout_2.addLayout(self.settings_layout)
self.main_layout.addWidget(self.bottom_settings_group)
self.gridLayout_2.addLayout(self.main_layout, 0, 1, 1, 1)
MainWindow.setCentralWidget(self.main_widget)
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "Control TEC"))
self.ch1_t_graph.setTitle(_translate("MainWindow", "Channel 1 Temperature"))
self.ch0_t_graph.setTitle(_translate("MainWindow", "Channel 0 Temperature"))
self.ch0_i_graph.setTitle(_translate("MainWindow", "Channel 0 Current"))
self.ch1_i_graph.setTitle(_translate("MainWindow", "Channel 1 Current"))
self.ip_set_line.setText(_translate("MainWindow", "192.168.1.26"))
self.ip_set_line.setPlaceholderText(_translate("MainWindow", "IP:port for the Thermostat"))
self.connect_btn.setText(_translate("MainWindow", "Connect"))
self.status_lbl.setText(_translate("MainWindow", "Disconnected"))
self.report_box.setText(_translate("MainWindow", "Report"))
self.report_apply_btn.setText(_translate("MainWindow", "Apply"))
self.fan_lbl.setText(_translate("MainWindow", "Fan:"))
self.fan_auto_box.setText(_translate("MainWindow", "Auto"))
self.hw_rev_lbl.setText(_translate("MainWindow", "Thermostat vX.Y"))
from pyqtgraph import PlotWidget
from pyqtgraph.parametertree import ParameterTree
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
MainWindow = QtWidgets.QMainWindow()
ui = Ui_MainWindow()
ui.setupUi(MainWindow)
MainWindow.show()
sys.exit(app.exec())

13
pythermostat/example.py Normal file
View File

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

View File

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

View File

@ -1,9 +1,10 @@
import math
import logging
import time
from collections import deque, namedtuple
from enum import Enum
from pytec.client import Client
from pythermostat.client import Client
# Based on hirshmann pid-autotune libiary
# See https://github.com/hirschmann/pid-autotune
@ -17,7 +18,6 @@ class PIDAutotuneState(Enum):
STATE_RELAY_STEP_DOWN = 'relay step down'
STATE_SUCCEEDED = 'succeeded'
STATE_FAILED = 'failed'
STATE_READY = 'ready'
class PIDAutotune:
@ -57,20 +57,6 @@ class PIDAutotune:
self._Ku = 0
self._Pu = 0
def setParam(self, target, step, noiseband, sampletime, lookback):
self._setpoint = target
self._outputstep = step
self._out_max = step
self._out_min = -step
self._noiseband = noiseband
self._inputs = deque(maxlen=round(lookback / sampletime))
def setReady(self):
self._state = PIDAutotuneState.STATE_READY
def setOff(self):
self._state = PIDAutotuneState.STATE_OFF
def state(self):
"""Get the current state."""
return self._state
@ -96,13 +82,6 @@ class PIDAutotune:
kd = divisors[2] * self._Ku * self._Pu
return PIDAutotune.PIDParams(kp, ki, kd)
def get_tec_pid (self):
divisors = self._tuning_rules["tyreus-luyben"]
kp = self._Ku * divisors[0]
ki = divisors[1] * self._Ku / self._Pu
kd = divisors[2] * self._Ku * self._Pu
return kp, ki, kd
def run(self, input_val, time_input):
"""To autotune a system, this method must be called periodically.
@ -117,8 +96,7 @@ class PIDAutotune:
if (self._state == PIDAutotuneState.STATE_OFF
or self._state == PIDAutotuneState.STATE_SUCCEEDED
or self._state == PIDAutotuneState.STATE_FAILED
or self._state == PIDAutotuneState.STATE_READY):
or self._state == PIDAutotuneState.STATE_FAILED):
self._state = PIDAutotuneState.STATE_RELAY_STEP_UP
self._last_run_timestamp = now
@ -259,13 +237,14 @@ def main():
tec = Client()
data = next(tec.report_mode())
data = tec.get_report()
ch = data[channel]
tuner = PIDAutotune(target_temperature, output_step,
lookback, noiseband, ch['interval'])
for data in tec.report_mode():
while True:
data = tec.get_report()
ch = data[channel]
@ -276,9 +255,11 @@ def main():
tuner_out = tuner.output()
tec.set_param("pwm", channel, "i_set", tuner_out)
tec.set_param("output", channel, "i_set", tuner_out)
tec.set_param("pwm", channel, "i_set", 0)
time.sleep(0.05)
tec.set_param("output", channel, "i_set", 0)
if __name__ == "__main__":

View File

@ -2,6 +2,7 @@ import socket
import json
import logging
class CommandError(Exception):
pass
@ -16,11 +17,11 @@ class Client:
self._socket.close()
def _check_zero_limits(self):
pwm_report = self.get_pwm()
for pwm_channel in pwm_report:
output_report = self.get_output()
for output_channel in output_report:
for limit in ["max_i_neg", "max_i_pos", "max_v"]:
if pwm_channel[limit]["value"] == 0.0:
logging.warning("`{}` limit is set to zero on channel {}".format(limit, pwm_channel["channel"]))
if output_channel[limit] == 0.0:
logging.warning("`{}` limit is set to zero on channel {}".format(limit, output_channel["channel"]))
def _read_line(self):
# read more lines
@ -36,11 +37,10 @@ class Client:
return line
def _command(self, *command):
self._socket.sendall(((" ".join(command)).strip() + "\n").encode('utf-8'))
self._socket.sendall((" ".join(command) + "\n").encode('utf-8'))
line = self._read_line()
response = json.loads(line)
logging.debug(f"{command}: {response}")
if "error" in response:
raise CommandError(response["error"])
return response
@ -51,25 +51,27 @@ class Client:
result[int(item["channel"])] = item
return result
def get_pwm(self):
"""Retrieve PWM limits for the TEC
def get_output(self):
"""Retrieve output limits for the TEC
Example::
[{'channel': 0,
'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")
return self._get_conf("output")
def get_pid(self):
"""Retrieve PID control state
@ -94,14 +96,14 @@ class Client:
"""
return self._get_conf("pid")
def get_steinhart_hart(self):
"""Retrieve Steinhart-Hart parameters for resistance to temperature conversion
def get_b_parameter(self):
"""Retrieve B-Parameter equation parameters for resistance to temperature conversion
Example::
[{'params': {'b': 3800.0, 'r0': 10000.0, 't0': 298.15}, 'channel': 0},
{'params': {'b': 3800.0, 'r0': 10000.0, 't0': 298.15}, 'channel': 1}]
"""
return self._get_conf("s-h")
return self._get_conf("b-p")
def get_postfilter(self):
"""Retrieve DAC postfilter configuration
@ -112,18 +114,18 @@ class Client:
"""
return self._get_conf("postfilter")
def report_mode(self):
"""Start reporting measurement values
def get_report(self):
"""Obtain one-time report on measurement values
Example of yielded data::
{'channel': 0,
'time': 2302524,
'interval': 0.12
'adc': 0.6199188965423515,
'sens': 6138.519310282602,
'temperature': 36.87032392655527,
'pid_engaged': True,
'i_set': 2.0635816680889123,
'vref': 1.494,
'dac_value': 2.527790834044456,
'dac_feedback': 2.523,
'i_tec': 2.331,
@ -131,24 +133,27 @@ class Client:
'tec_u_meas': 2.5340000000000003,
'pid_output': 2.067581958092247}
"""
self._command("report mode", "on")
return self._get_conf("report")
while True:
line = self._read_line()
if not line:
break
try:
yield json.loads(line)
except json.decoder.JSONDecodeError:
pass
def get_ipv4(self):
"""Get the IPv4 settings of the Thermostat"""
return self._command("ipv4")
def get_fan(self):
"""Get Thermostat current fan settings"""
return self._command("fan")
def get_hwrev(self):
"""Get Thermostat hardware revision"""
return self._command("hwrev")
def set_param(self, topic, channel, field="", value=""):
"""Set configuration parameters
Examples::
tec.set_param("pwm", 0, "max_v", 2.0)
tec.set_param("output", 0, "max_v", 2.0)
tec.set_param("pid", 1, "output_max", 2.5)
tec.set_param("s-h", 0, "t0", 20.0)
tec.set_param("b-p", 0, "t0", 20.0)
tec.set_param("center", 0, "vref")
tec.set_param("postfilter", 1, 21)
@ -163,20 +168,40 @@ class Client:
def power_up(self, channel, target):
"""Start closed-loop mode"""
self.set_param("pid", channel, "target", value=target)
self.set_param("pwm", channel, "pid")
self.set_param("output", channel, "pid")
def save_config(self):
def save_config(self, channel=""):
"""Save current configuration to EEPROM"""
self._command("save")
self._command("save", channel)
if channel != "":
self._read_line() # read the extra {}
def load_config(self):
def load_config(self, channel=""):
"""Load current configuration from EEPROM"""
self._command("load")
self._command("load", channel)
if channel != "":
self._read_line() # read the extra {}
def hw_rev(self):
"""Get Thermostat hardware revision"""
return self._command("hwrev")
def reset(self):
"""Reset the device"""
self._socket.sendall("reset".encode("utf-8"))
self.disconnect() # resetting ends the TCP session, disconnect anyway
def fan(self):
"""Get Thermostat current fan settings"""
return self._command("fan")
def enter_dfu_mode(self):
"""Reset device and enters USB device firmware update (DFU) mode"""
self._socket.sendall("dfu".encode("utf-8"))
self.disconnect() # resetting ends the TCP session, disconnect anyway
def set_ipv4(self, address, netmask, gateway=""):
"""Configure IPv4 address, netmask length, and optional default gateway"""
self._command("ipv4", f"{address}/{netmask}", gateway)
def set_fan(self, power=None):
"""Set fan power with values from 1 to 100. If omitted, set according to fcurve"""
if power is None:
power = "auto"
self._command("fan", power)
def set_fcurve(self, a=1.0, b=0.0, c=0.0):
"""Set fan controller curve coefficients"""
self._command("fcurve", a, b, c)

View File

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

View File

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

View File

@ -1,12 +1,9 @@
use stm32f4xx_hal::{
hal::{
blocking::spi::Transfer,
digital::v2::OutputPin,
},
time::MegaHertz,
spi,
};
use crate::timer::sleep;
use stm32f4xx_hal::{
hal::{blocking::spi::Transfer, digital::v2::OutputPin},
spi,
time::MegaHertz,
};
/// SPI Mode 1
pub const SPI_MODE: spi::Mode = spi::Mode {
@ -28,10 +25,7 @@ impl<SPI: Transfer<u8>, S: OutputPin> Dac<SPI, S> {
pub fn new(spi: SPI, mut sync: S) -> Self {
let _ = sync.set_low();
Dac {
spi,
sync,
}
Dac { spi, sync }
}
fn write(&mut self, buf: &mut [u8]) -> Result<(), SPI::Error> {
@ -47,11 +41,7 @@ impl<SPI: Transfer<u8>, S: OutputPin> Dac<SPI, S> {
pub fn set(&mut self, value: u32) -> Result<u32, SPI::Error> {
let value = value.min(MAX_VALUE);
let mut buf = [
(value >> 14) as u8,
(value >> 6) as u8,
(value << 2) as u8,
];
let mut buf = [(value >> 14) as u8, (value >> 6) as u8, (value << 2) as u8];
self.write(&mut buf)?;
Ok(value)
}

View File

@ -1,18 +1,12 @@
use super::{
checksum::{Checksum, ChecksumMode},
regs::{self, Register, RegisterData},
DigitalFilterOrder, Input, Mode, PostFilter, RefSource,
};
use core::fmt;
use log::{info, warn};
use stm32f4xx_hal::hal::{
blocking::spi::Transfer,
digital::v2::OutputPin,
};
use uom::si::{
f64::ElectricPotential,
electric_potential::volt,
};
use super::{
regs::{self, Register, RegisterData},
checksum::{ChecksumMode, Checksum},
Mode, Input, RefSource, PostFilter, DigitalFilterOrder,
};
use stm32f4xx_hal::hal::{blocking::spi::Transfer, digital::v2::OutputPin};
use uom::si::{electric_potential::volt, f64::ElectricPotential};
/// AD7172-2 implementation
///
@ -27,7 +21,8 @@ impl<SPI: Transfer<u8, Error = E>, NSS: OutputPin, E: fmt::Debug> Adc<SPI, NSS>
pub fn new(spi: SPI, mut nss: NSS) -> Result<Self, SPI::Error> {
let _ = nss.set_high();
let mut adc = Adc {
spi, nss,
spi,
nss,
checksum_mode: ChecksumMode::Off,
};
adc.reset()?;
@ -55,8 +50,7 @@ impl<SPI: Transfer<u8, Error = E>, NSS: OutputPin, E: fmt::Debug> Adc<SPI, NSS>
/// `0x00DX` for AD7172-2
pub fn identify(&mut self) -> Result<u16, SPI::Error> {
self.read_reg(&regs::Id)
.map(|id| id.id())
self.read_reg(&regs::Id).map(|id| id.id())
}
pub fn set_checksum_mode(&mut self, mode: ChecksumMode) -> Result<(), SPI::Error> {
@ -76,7 +70,10 @@ impl<SPI: Transfer<u8, Error = E>, NSS: OutputPin, E: fmt::Debug> Adc<SPI, NSS>
}
pub fn setup_channel(
&mut self, index: u8, in_pos: Input, in_neg: Input
&mut self,
index: u8,
in_pos: Input,
in_neg: Input,
) -> Result<(), SPI::Error> {
self.update_reg(&regs::SetupCon { index }, |data| {
data.set_bipolar(false);
@ -106,7 +103,11 @@ impl<SPI: Transfer<u8, Error = E>, NSS: OutputPin, E: fmt::Debug> Adc<SPI, NSS>
let offset = self.read_reg(&regs::Offset { index })?.offset();
let gain = self.read_reg(&regs::Gain { index })?.gain();
let bipolar = self.read_reg(&regs::SetupCon { index })?.bipolar();
Ok(ChannelCalibration { offset, gain, bipolar })
Ok(ChannelCalibration {
offset,
gain,
bipolar,
})
}
pub fn start_continuous_conversion(&mut self) -> Result<(), SPI::Error> {
@ -119,8 +120,7 @@ impl<SPI: Transfer<u8, Error = E>, NSS: OutputPin, E: fmt::Debug> Adc<SPI, NSS>
}
pub fn get_postfilter(&mut self, index: u8) -> Result<Option<PostFilter>, SPI::Error> {
self.read_reg(&regs::FiltCon { index })
.map(|data| {
self.read_reg(&regs::FiltCon { index }).map(|data| {
if data.enh_filt_en() {
Some(data.enh_filt())
} else {
@ -129,22 +129,23 @@ impl<SPI: Transfer<u8, Error = E>, NSS: OutputPin, E: fmt::Debug> Adc<SPI, NSS>
})
}
pub fn set_postfilter(&mut self, index: u8, filter: Option<PostFilter>) -> Result<(), SPI::Error> {
self.update_reg(&regs::FiltCon { index }, |data| {
match filter {
pub fn set_postfilter(
&mut self,
index: u8,
filter: Option<PostFilter>,
) -> Result<(), SPI::Error> {
self.update_reg(&regs::FiltCon { index }, |data| match filter {
None => data.set_enh_filt_en(false),
Some(filter) => {
data.set_enh_filt_en(true);
data.set_enh_filt(filter);
}
}
})
}
/// Returns the channel the data is from
pub fn data_ready(&mut self) -> Result<Option<u8>, SPI::Error> {
self.read_reg(&regs::Status)
.map(|status| {
self.read_reg(&regs::Status).map(|status| {
if status.ready() {
Some(status.channel())
} else {
@ -155,8 +156,7 @@ impl<SPI: Transfer<u8, Error = E>, NSS: OutputPin, E: fmt::Debug> Adc<SPI, NSS>
/// Get data
pub fn read_data(&mut self) -> Result<u32, SPI::Error> {
self.read_reg(&regs::Data)
.map(|data| data.data())
self.read_reg(&regs::Data).map(|data| data.data())
}
fn read_reg<R: regs::Register>(&mut self, reg: &R) -> Result<R::Data, SPI::Error> {
@ -175,12 +175,21 @@ impl<SPI: Transfer<u8, Error = E>, NSS: OutputPin, E: fmt::Debug> Adc<SPI, NSS>
break;
}
// Retry
warn!("read_reg {:02X}: checksum error: {:?}!={:?}, retrying", reg.address(), checksum_expected, checksum_in);
warn!(
"read_reg {:02X}: checksum error: {:?}!={:?}, retrying",
reg.address(),
checksum_expected,
checksum_in
);
}
Ok(reg_data)
}
fn write_reg<R: regs::Register>(&mut self, reg: &R, reg_data: &mut R::Data) -> Result<(), SPI::Error> {
fn write_reg<R: regs::Register>(
&mut self,
reg: &R,
reg_data: &mut R::Data,
) -> Result<(), SPI::Error> {
loop {
let address = reg.address();
let mut checksum = Checksum::new(match self.checksum_mode {
@ -190,7 +199,7 @@ impl<SPI: Transfer<u8, Error = E>, NSS: OutputPin, E: fmt::Debug> Adc<SPI, NSS>
ChecksumMode::Crc => ChecksumMode::Crc,
});
checksum.feed(&[address]);
checksum.feed(&reg_data);
checksum.feed(reg_data);
let checksum_out = checksum.result();
let mut data = reg_data.clone();
@ -201,7 +210,10 @@ impl<SPI: Transfer<u8, Error = E>, NSS: OutputPin, E: fmt::Debug> Adc<SPI, NSS>
if *readback_data == **reg_data {
return Ok(());
}
warn!("write_reg {:02X}: readback error, {:?}!={:?}, retrying", address, &*readback_data, &**reg_data);
warn!(
"write_reg {:02X}: readback error, {:?}!={:?}, retrying",
address, &*readback_data, &**reg_data
);
}
}
@ -225,7 +237,12 @@ impl<SPI: Transfer<u8, Error = E>, NSS: OutputPin, E: fmt::Debug> Adc<SPI, NSS>
Ok(())
}
fn transfer<'w>(&mut self, addr: u8, reg_data: &'w mut [u8], checksum: Option<u8>) -> Result<Option<u8>, SPI::Error> {
fn transfer(
&mut self,
addr: u8,
reg_data: &mut [u8],
checksum: Option<u8>,
) -> Result<Option<u8>, SPI::Error> {
let mut addr_buf = [addr];
let _ = self.nss.set_low();
@ -234,8 +251,7 @@ impl<SPI: Transfer<u8, Error = E>, NSS: OutputPin, E: fmt::Debug> Adc<SPI, NSS>
Err(e) => Err(e),
};
let result = match (result, checksum) {
(Ok(_), None) =>
Ok(None),
(Ok(_), None) => Ok(None),
(Ok(_), Some(checksum_out)) => {
let mut checksum_buf = [checksum_out; 1];
match self.spi.transfer(&mut checksum_buf) {
@ -243,8 +259,7 @@ impl<SPI: Transfer<u8, Error = E>, NSS: OutputPin, E: fmt::Debug> Adc<SPI, NSS>
Err(e) => Err(e),
}
}
(Err(e), _) =>
Err(e),
(Err(e), _) => Err(e),
};
let _ = self.nss.set_high();

View File

@ -29,13 +29,13 @@ impl Checksum {
fn feed_byte(&mut self, input: u8) {
match self.mode {
ChecksumMode::Off => {},
ChecksumMode::Off => {}
ChecksumMode::Xor => self.state ^= input,
ChecksumMode::Crc => {
for i in 0..8 {
let input_mask = 0x80 >> i;
self.state = (self.state << 1) ^
if ((self.state & 0x80) != 0) != ((input & input_mask) != 0) {
self.state = (self.state << 1)
^ if ((self.state & 0x80) != 0) != ((input & input_mask) != 0) {
0x07 /* x8 + x2 + x + 1 */
} else {
0
@ -54,7 +54,7 @@ impl Checksum {
pub fn result(&self) -> Option<u8> {
match self.mode {
ChecksumMode::Off => None,
_ => Some(self.state)
_ => Some(self.state),
}
}
}

View File

@ -1,13 +1,10 @@
use core::fmt;
use num_traits::float::Float;
use serde::{Serialize, Deserialize};
use stm32f4xx_hal::{
time::MegaHertz,
spi,
};
use serde::{Deserialize, Serialize};
use stm32f4xx_hal::{spi, time::MegaHertz};
pub mod regs;
mod checksum;
pub mod regs;
pub use checksum::ChecksumMode;
mod adc;
pub use adc::*;
@ -22,7 +19,6 @@ pub const SPI_CLOCK: MegaHertz = MegaHertz(2);
pub const MAX_VALUE: u32 = 0xFF_FFFF;
#[derive(Clone, Copy, Debug)]
#[repr(u8)]
pub enum Mode {
@ -105,7 +101,8 @@ impl fmt::Display for Input {
RefPos => "ref+",
RefNeg => "ref-",
_ => "<INVALID>",
}.fmt(fmt)
}
.fmt(fmt)
}
}
@ -141,7 +138,8 @@ impl fmt::Display for RefSource {
Internal => "internal",
Avdd1MinusAvss => "avdd1-avss",
_ => "<INVALID>",
}.fmt(fmt)
}
.fmt(fmt)
}
}

View File

@ -1,6 +1,6 @@
use core::ops::{Deref, DerefMut};
use byteorder::{BigEndian, ByteOrder};
use bit_field::BitField;
use byteorder::{BigEndian, ByteOrder};
use core::ops::{Deref, DerefMut};
use super::*;
@ -49,7 +49,9 @@ macro_rules! def_reg {
}
};
($Reg: ident, u8, $reg: ident, $addr: expr, $size: expr) => {
pub struct $Reg { pub index: u8, }
pub struct $Reg {
pub index: u8,
}
impl Register for $Reg {
type Data = $reg::Data;
fn address(&self) -> u8 {
@ -76,7 +78,7 @@ macro_rules! def_reg {
}
}
}
}
};
}
macro_rules! reg_bit {
@ -159,9 +161,21 @@ impl status::Data {
def_reg!(AdcMode, adc_mode, 0x01, 2);
impl adc_mode::Data {
reg_bits!(delay, set_delay, 0, 0..=2, "Delay after channel switch");
reg_bit!(sing_cyc, set_sing_cyc, 0, 5, "Can only used with single channel");
reg_bit!(
sing_cyc,
set_sing_cyc,
0,
5,
"Can only used with single channel"
);
reg_bit!(hide_delay, set_hide_delay, 0, 6, "Hide delay");
reg_bit!(ref_en, set_ref_en, 0, 7, "Enable internal reference, output buffered 2.5 V to REFOUT");
reg_bit!(
ref_en,
set_ref_en,
0,
7,
"Enable internal reference, output buffered 2.5 V to REFOUT"
);
reg_bits!(clockset, set_clocksel, 1, 2..=3, "Clock source");
reg_bits!(mode, set_mode, 1, 4..=6, Mode, "Operating mode");
}
@ -174,15 +188,19 @@ impl if_mode::Data {
def_reg!(Data, data, 0x04, 3);
impl data::Data {
pub fn data(&self) -> u32 {
(u32::from(self.0[0]) << 16) |
(u32::from(self.0[1]) << 8) |
u32::from(self.0[2])
(u32::from(self.0[0]) << 16) | (u32::from(self.0[1]) << 8) | u32::from(self.0[2])
}
}
def_reg!(GpioCon, gpio_con, 0x06, 2);
impl gpio_con::Data {
reg_bit!(sync_en, set_sync_en, 0, 3, "Enables the SYNC/ERROR pin as a sync input");
reg_bit!(
sync_en,
set_sync_en,
0,
3,
"Enables the SYNC/ERROR pin as a sync input"
);
}
def_reg!(Id, id, 0x07, 2);
@ -200,8 +218,7 @@ impl channel::Data {
/// Which input is connected to positive input of this channel
#[allow(unused)]
pub fn a_in_pos(&self) -> Input {
((self.0[0].get_bits(0..=1) << 3) |
self.0[1].get_bits(5..=7)).into()
((self.0[0].get_bits(0..=1) << 3) | self.0[1].get_bits(5..=7)).into()
}
/// Set which input is connected to positive input of this channel
#[allow(unused)]
@ -210,27 +227,66 @@ impl channel::Data {
self.0[0].set_bits(0..=1, value >> 3);
self.0[1].set_bits(5..=7, value & 0x7);
}
reg_bits!(a_in_neg, set_a_in_neg, 1, 0..=4, Input,
"Which input is connected to negative input of this channel");
reg_bits!(
a_in_neg,
set_a_in_neg,
1,
0..=4,
Input,
"Which input is connected to negative input of this channel"
);
}
def_reg!(SetupCon, u8, setup_con, 0x20, 2);
impl setup_con::Data {
reg_bit!(bipolar, set_bipolar, 0, 4, "Unipolar (`false`) or bipolar (`true`) coded output");
reg_bit!(
bipolar,
set_bipolar,
0,
4,
"Unipolar (`false`) or bipolar (`true`) coded output"
);
reg_bit!(refbuf_pos, set_refbuf_pos, 0, 3, "Enable REF+ input buffer");
reg_bit!(refbuf_neg, set_refbuf_neg, 0, 2, "Enable REF- input buffer");
reg_bit!(ainbuf_pos, set_ainbuf_pos, 0, 1, "Enable AIN+ input buffer");
reg_bit!(ainbuf_neg, set_ainbuf_neg, 0, 0, "Enable AIN- input buffer");
reg_bit!(burnout_en, 1, 7, "enables a 10 µA current source on the positive analog input selected and a 10 µA current sink on the negative analog input selected");
reg_bits!(ref_sel, set_ref_sel, 1, 4..=5, RefSource, "Select reference source for conversion");
reg_bits!(
ref_sel,
set_ref_sel,
1,
4..=5,
RefSource,
"Select reference source for conversion"
);
}
def_reg!(FiltCon, u8, filt_con, 0x28, 2);
impl filt_con::Data {
reg_bit!(sinc3_map, 0, 7, "If set, mapping of filter register changes to directly program the decimation rate of the sinc3 filter");
reg_bit!(enh_filt_en, set_enh_filt_en, 0, 3, "Enable postfilters for enhanced 50Hz and 60Hz rejection");
reg_bits!(enh_filt, set_enh_filt, 0, 0..=2, PostFilter, "Select postfilters for enhanced 50Hz and 60Hz rejection");
reg_bits!(order, set_order, 1, 5..=6, DigitalFilterOrder, "order of the digital filter that processes the modulator data");
reg_bit!(
enh_filt_en,
set_enh_filt_en,
0,
3,
"Enable postfilters for enhanced 50Hz and 60Hz rejection"
);
reg_bits!(
enh_filt,
set_enh_filt,
0,
0..=2,
PostFilter,
"Select postfilters for enhanced 50Hz and 60Hz rejection"
);
reg_bits!(
order,
set_order,
1,
5..=6,
DigitalFilterOrder,
"order of the digital filter that processes the modulator data"
);
reg_bits!(odr, set_odr, 1, 0..=4, "Output data rate");
}
@ -238,9 +294,7 @@ def_reg!(Offset, u8, offset, 0x30, 3);
impl offset::Data {
#[allow(unused)]
pub fn offset(&self) -> u32 {
(u32::from(self.0[0]) << 16) |
(u32::from(self.0[1]) << 8) |
u32::from(self.0[2])
(u32::from(self.0[0]) << 16) | (u32::from(self.0[1]) << 8) | u32::from(self.0[2])
}
#[allow(unused)]
pub fn set_offset(&mut self, value: u32) {
@ -254,9 +308,7 @@ def_reg!(Gain, u8, gain, 0x38, 3);
impl gain::Data {
#[allow(unused)]
pub fn gain(&self) -> u32 {
(u32::from(self.0[0]) << 16) |
(u32::from(self.0[1]) << 8) |
u32::from(self.0[2])
(u32::from(self.0[0]) << 16) | (u32::from(self.0[1]) << 8) | u32::from(self.0[2])
}
#[allow(unused)]
pub fn set_gain(&mut self, value: u32) {

View File

@ -1,31 +1,29 @@
use num_traits::float::Float;
use serde::{Deserialize, Serialize};
use uom::si::{
f64::{
ElectricalResistance,
ThermodynamicTemperature,
},
electrical_resistance::ohm,
f64::{ElectricalResistance, TemperatureInterval, ThermodynamicTemperature},
ratio::ratio,
temperature_interval::kelvin as kelvin_interval,
thermodynamic_temperature::{degree_celsius, kelvin},
};
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,
/// Beta (average slope of the function ln R vs. 1/T)
pub b: TemperatureInterval,
}
impl Parameters {
/// Perform the voltage to temperature conversion.
/// Perform the resistance to temperature conversion.
pub fn get_temperature(&self, r: ElectricalResistance) -> ThermodynamicTemperature {
let inv_temp = 1.0 / self.t0.get::<kelvin>() + (r / self.r0).get::<ratio>().ln() / self.b;
ThermodynamicTemperature::new::<kelvin>(1.0 / inv_temp)
let temp = (self.t0.recip() + (r / self.r0).get::<ratio>().ln() / self.b).recip();
ThermodynamicTemperature::new::<kelvin>(temp.get::<kelvin_interval>())
}
}
@ -34,7 +32,7 @@ impl Default for Parameters {
Parameters {
t0: ThermodynamicTemperature::new::<degree_celsius>(25.0),
r0: ElectricalResistance::new::<ohm>(10_000.0),
b: 3800.0,
b: TemperatureInterval::new::<kelvin_interval>(3800.0),
}
}
}

View File

@ -1,14 +1,10 @@
use stm32f4xx_hal::hal::digital::v2::OutputPin;
use uom::si::{
f64::ElectricPotential,
electric_potential::volt,
};
use crate::{
ad5680,
ad7172,
ad5680, ad7172,
channel_state::ChannelState,
pins::{ChannelPins, ChannelPinSet},
pins::{ChannelPinSet, ChannelPins},
};
use stm32f4xx_hal::hal::digital::v2::OutputPin;
use uom::si::{electric_potential::volt, f64::ElectricPotential};
/// Marker type for the first channel
pub struct Channel0;
@ -24,7 +20,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,
@ -40,7 +36,8 @@ impl<C: ChannelPins> Channel<C> {
Channel {
state,
dac, vref_meas,
dac,
vref_meas,
shdn: pins.shdn,
vref_pin: pins.vref_pin,
itec_pin: pins.itec_pin,

View File

@ -1,22 +1,21 @@
use crate::{
ad7172, b_parameter as bp,
command_parser::{CenterPoint, Polarity},
config::PwmLimits,
pid,
};
use num_traits::Zero;
use smoltcp::time::{Duration, Instant};
use uom::si::{
f64::{
ElectricPotential,
ElectricalResistance,
ThermodynamicTemperature,
Time,
},
electric_current::ampere,
electric_potential::volt,
electrical_resistance::ohm,
f64::{
ElectricCurrent, ElectricPotential, ElectricalResistance, ThermodynamicTemperature, Time,
},
thermodynamic_temperature::degree_celsius,
time::millisecond,
};
use crate::{
ad7172,
pid,
steinhart_hart as sh,
command_parser::CenterPoint,
};
const R_INNER: f64 = 2.0 * 5100.0;
const VREF_SENS: f64 = 3.3 / 2.0;
@ -26,14 +25,15 @@ pub struct ChannelState {
pub adc_calibration: ad7172::ChannelCalibration,
pub adc_time: Instant,
pub adc_interval: Duration,
/// VREF for the TEC (1.5V)
pub vref: ElectricPotential,
/// 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,13 +44,18 @@ impl ChannelState {
adc_time: Instant::from_secs(0),
// default: 10 Hz
adc_interval: Duration::from_millis(100),
// updated later with Channels.read_vref()
vref: ElectricPotential::new::<volt>(1.5),
center: CenterPoint::Vref,
center: CenterPoint::VRef,
dac_value: ElectricPotential::new::<volt>(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: pid::Controller::new(pid::Parameters::default()),
sh: sh::Parameters::default(),
bp: bp::Parameters::default(),
polarity: Polarity::Normal,
}
}
@ -67,8 +72,7 @@ impl ChannelState {
/// Update PID state on ADC input, calculate new DAC output
pub fn update_pid(&mut self) -> Option<f64> {
let temperature = self.get_temperature()?
.get::<degree_celsius>();
let temperature = self.get_temperature()?.get::<degree_celsius>();
let pid_output = self.pid.update(temperature);
Some(pid_output)
}
@ -96,7 +100,7 @@ impl ChannelState {
pub fn get_temperature(&self) -> Option<ThermodynamicTemperature> {
let r = self.get_sens()?;
let temperature = self.sh.get_temperature(r);
let temperature = self.bp.get_temperature(r);
Some(temperature)
}
}

View File

@ -1,32 +1,59 @@
use core::cmp::max_by;
use crate::timer::sleep;
use crate::{
ad5680, ad7172, b_parameter,
channel::{Channel, Channel0, Channel1},
channel_state::ChannelState,
command_handler::JsonBuffer,
command_parser::{CenterPoint, Polarity, PwmPin},
pins::{self, Channel0VRef, Channel1VRef},
};
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;
use uom::si::{
f64::{ElectricCurrent, ElectricPotential, ElectricalResistance, Time},
electric_potential::{millivolt, volt},
electric_current::ampere,
electric_potential::{millivolt, volt},
electrical_resistance::ohm,
f64::{ElectricCurrent, ElectricPotential, ElectricalResistance, Time},
ratio::ratio,
thermodynamic_temperature::degree_celsius,
};
use crate::{
ad5680,
ad7172,
channel::{Channel, Channel0, Channel1},
channel_state::ChannelState,
command_parser::{CenterPoint, PwmPin},
command_handler::JsonBuffer,
pins,
steinhart_hart,
};
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>,
@ -44,21 +71,26 @@ impl Channels {
adc.set_sync_enable(false).unwrap();
// Setup channels and start ADC
adc.setup_channel(0, ad7172::Input::Ain2, ad7172::Input::Ain3).unwrap();
let adc_calibration0 = adc.get_calibration(0)
.expect("adc_calibration0");
adc.setup_channel(1, ad7172::Input::Ain0, ad7172::Input::Ain1).unwrap();
let adc_calibration1 = adc.get_calibration(1)
.expect("adc_calibration1");
adc.setup_channel(0, ad7172::Input::Ain2, ad7172::Input::Ain3)
.unwrap();
let adc_calibration0 = adc.get_calibration(0).expect("adc_calibration0");
adc.setup_channel(1, ad7172::Input::Ain0, ad7172::Input::Ain1)
.unwrap();
let adc_calibration1 = adc.get_calibration(1).expect("adc_calibration1");
adc.start_continuous_conversion().unwrap();
let channel0 = Channel::new(pins.channel0, adc_calibration0);
let channel1 = Channel::new(pins.channel1, adc_calibration1);
let pins_adc = pins.pins_adc;
let pwm = pins.pwm;
let mut channels = Channels { channel0, channel1, adc, pins_adc, pwm };
let mut channels = Channels {
channel0,
channel1,
adc,
pins_adc,
pwm,
};
for channel in 0..CHANNELS {
channels.channel_state(channel).vref = channels.read_vref(channel);
channels.calibrate_dac_value(channel);
channels.set_i(channel, ElectricCurrent::new::<ampere>(0.0));
}
@ -98,13 +130,10 @@ impl Channels {
/// calculate the TEC i_set centerpoint
pub fn get_center(&mut self, channel: usize) -> ElectricPotential {
match self.channel_state(channel).center {
CenterPoint::Vref => {
let vref = self.read_vref(channel);
self.channel_state(channel).vref = vref;
vref
},
CenterPoint::Override(center_point) =>
ElectricPotential::new::<volt>(center_point.into()),
CenterPoint::VRef => self.adc_read(channel, PinsAdcReadTarget::VRef, 8),
CenterPoint::Override(center_point) => {
ElectricPotential::new::<volt>(center_point.into())
}
}
}
@ -114,22 +143,14 @@ impl Channels {
voltage
}
pub fn get_i(&mut self, channel: usize) -> ElectricCurrent {
let center_point = match channel.into() {
0 => self.channel0.vref_meas,
1 => self.channel1.vref_meas,
_ => unreachable!(),
};
// 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
pub fn get_i_set(&mut self, channel: usize) -> ElectricCurrent {
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(),
@ -139,114 +160,123 @@ impl Channels {
voltage
}
pub fn set_i(&mut self, channel: usize, i_tec: ElectricCurrent) -> ElectricCurrent {
let vref_meas = match channel.into() {
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 {
0 => self.channel0.vref_meas,
1 => self.channel1.vref_meas,
_ => unreachable!(),
};
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
negate * (voltage - center_point) / (10.0 * r_sense)
}
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(
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_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
);
let mv = self.pins_adc.sample_to_millivolts(sample);
ElectricPotential::new::<millivolt>(mv as f64)
stm32f4xx_hal::adc::config::SampleTime::Cycles_480,
) as u32;
}
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);
ElectricPotential::new::<millivolt>(mv as f64)
sample / avg_pt as u32
}
_ => 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(
PinsAdcReadTarget::ITec => {
for _ in (0..avg_pt).rev() {
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)
stm32f4xx_hal::adc::config::SampleTime::Cycles_480,
) as u32;
}
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)
sample / avg_pt as u32
}
_ => 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(
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
);
let mv = self.pins_adc.sample_to_millivolts(sample);
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(
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_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
);
let mv = self.pins_adc.sample_to_millivolts(sample);
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!(),
@ -273,14 +303,13 @@ impl Channels {
let samples = 50;
let mut target_voltage = ElectricPotential::new::<volt>(0.0);
for _ in 0..samples {
target_voltage = target_voltage + self.get_center(channel);
target_voltage += self.get_center(channel);
}
target_voltage = target_voltage / samples as f64;
target_voltage /= samples as f64;
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 => {
@ -291,24 +320,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;
}
}
@ -334,58 +362,33 @@ 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
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 {
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 {
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 {
@ -396,50 +399,78 @@ impl Channels {
value as f64 / (max as f64)
}
match (channel, pin) {
(_, PwmPin::ISet) =>
panic!("i_set is no pwm pin"),
(0, PwmPin::MaxIPos) =>
set(&mut self.pwm.max_i_pos0, duty),
(0, PwmPin::MaxINeg) =>
set(&mut self.pwm.max_i_neg0, duty),
(0, PwmPin::MaxV) =>
set(&mut self.pwm.max_v0, duty),
(1, PwmPin::MaxIPos) =>
set(&mut self.pwm.max_i_pos1, duty),
(1, PwmPin::MaxINeg) =>
set(&mut self.pwm.max_i_neg1, duty),
(1, PwmPin::MaxV) =>
set(&mut self.pwm.max_v1, duty),
_ =>
unreachable!(),
(_, PwmPin::ISet) => panic!("i_set is no pwm pin"),
(0, PwmPin::MaxIPos) => set(&mut self.pwm.max_i_pos0, duty),
(0, PwmPin::MaxINeg) => set(&mut self.pwm.max_i_neg0, duty),
(0, PwmPin::MaxV) => set(&mut self.pwm.max_v0, duty),
(1, PwmPin::MaxIPos) => set(&mut self.pwm.max_i_pos1, duty),
(1, PwmPin::MaxINeg) => set(&mut self.pwm.max_i_neg1, duty),
(1, PwmPin::MaxV) => set(&mut self.pwm.max_v1, duty),
_ => unreachable!(),
}
}
pub fn set_max_v(&mut self, channel: usize, max_v: ElectricPotential) -> (ElectricPotential, ElectricPotential) {
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;
(duty * max, max)
}
pub fn set_max_i_pos(&mut self, channel: usize, max_i_pos: ElectricCurrent) -> (ElectricCurrent, ElectricCurrent) {
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;
(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) {
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;
(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 vref = self.channel_state(channel).vref;
let i_set = self.get_i(channel);
let i_tec = self.read_itec(channel);
let i_set = self.get_i_set(channel);
let i_tec = self.adc_read(channel, PinsAdcReadTarget::ITec, 16);
let tec_i = self.get_tec_i(channel);
let dac_value = self.get_dac(channel);
let state = self.channel_state(channel);
@ -450,13 +481,13 @@ impl Channels {
interval: state.get_adc_interval(),
adc: state.get_adc(),
sens: state.get_sens(),
temperature: state.get_temperature()
temperature: state
.get_temperature()
.map(|temperature| temperature.get::<degree_celsius>()),
pid_engaged: state.pid_engaged,
i_set,
vref,
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),
@ -480,27 +511,40 @@ impl Channels {
serde_json_core::to_vec(&summaries)
}
fn pwm_summary(&mut self, channel: usize) -> PwmSummary {
PwmSummary {
pub fn pid_engaged(&mut self) -> bool {
for channel in 0..CHANNELS {
if self.channel_state(channel).pid_engaged {
return true;
}
}
false
}
fn output_summary(&mut self, channel: usize) -> OutputSummary {
OutputSummary {
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_set(channel),
max_v: self.get_max_v(channel),
max_i_pos: self.get_max_i_pos(channel),
max_i_neg: self.get_max_i_neg(channel),
polarity: PolarityJson(self.channel_state(channel).polarity.clone()),
}
}
pub fn pwm_summaries_json(&mut self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
pub fn output_summaries_json(&mut self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
let mut summaries = Vec::<_, U2>::new();
for channel in 0..CHANNELS {
let _ = summaries.push(self.pwm_summary(channel));
let _ = summaries.push(self.output_summary(channel));
}
serde_json_core::to_vec(&summaries)
}
fn postfilter_summary(&mut self, channel: usize) -> PostFilterSummary {
let rate = self.adc.get_postfilter(channel as u8).unwrap()
let rate = self
.adc
.get_postfilter(channel as u8)
.unwrap()
.and_then(|filter| filter.output_rate());
PostFilterSummary { channel, rate }
}
@ -513,23 +557,26 @@ 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()
}
}
@ -543,7 +590,6 @@ pub struct Report {
temperature: Option<f64>,
pid_engaged: bool,
i_set: ElectricCurrent,
vref: ElectricPotential,
dac_value: ElectricPotential,
dac_feedback: ElectricPotential,
i_tec: ElectricPotential,
@ -561,34 +607,36 @@ impl Serialize for CenterPointJson {
S: Serializer,
{
match self.0 {
CenterPoint::Vref =>
serializer.serialize_str("vref"),
CenterPoint::Override(vref) =>
serializer.serialize_f32(vref),
CenterPoint::VRef => serializer.serialize_str("vref"),
CenterPoint::Override(vref) => serializer.serialize_f32(vref),
}
}
}
pub struct PolarityJson(Polarity);
// 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",
})
}
}
#[derive(Serialize)]
pub struct PwmSummaryField<T: Serialize> {
value: T,
max: T,
}
impl<T: Serialize> From<(T, T)> for PwmSummaryField<T> {
fn from((value, max): (T, T)) -> Self {
PwmSummaryField { value, max }
}
}
#[derive(Serialize)]
pub struct PwmSummary {
pub struct OutputSummary {
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)]
@ -598,7 +646,7 @@ pub struct PostFilterSummary {
}
#[derive(Serialize)]
pub struct SteinhartHartSummary {
pub struct BParameterSummary {
channel: usize,
params: steinhart_hart::Parameters,
params: b_parameter::Parameters,
}

View File

@ -1,46 +1,30 @@
use smoltcp::socket::TcpSocket;
use log::{error, warn};
use core::fmt::Write;
use heapless::{consts::U1024, Vec};
use super::{
net,
command_parser::{
Ipv4Config,
Command,
ShowCommand,
CenterPoint,
PidParameter,
PwmPin,
ShParameter
},
leds::Leds,
ad7172,
CHANNEL_CONFIG_KEY,
channels::{
Channels,
CHANNELS
channels::{Channels, CHANNELS},
command_parser::{
BpParameter, CenterPoint, Command, Ipv4Config, PidParameter, Polarity, PwmPin, ShowCommand,
},
config::ChannelConfig,
dfu,
flash_store::FlashStore,
session::Session,
FanCtrl,
hw_rev::HWRev,
net, FanCtrl, CHANNEL_CONFIG_KEY,
};
use core::fmt::Write;
use heapless::{consts::U1024, Vec};
use log::{error, warn};
use smoltcp::socket::TcpSocket;
use uom::{
si::{
f64::{
ElectricCurrent,
ElectricPotential,
ElectricalResistance,
ThermodynamicTemperature,
},
use uom::si::{
electric_current::ampere,
electric_potential::volt,
electrical_resistance::ohm,
thermodynamic_temperature::degree_celsius,
f64::{
ElectricCurrent, ElectricPotential, ElectricalResistance, TemperatureInterval,
ThermodynamicTemperature,
},
temperature_interval::kelvin,
thermodynamic_temperature::degree_celsius,
};
#[derive(Debug, Clone, PartialEq)]
@ -53,9 +37,9 @@ pub enum Handler {
#[derive(Clone, Debug, PartialEq)]
pub enum Error {
ReportError,
PostFilterRateError,
FlashError
Report,
PostFilterRate,
Flash,
}
pub type JsonBuffer = Vec<u8, U1024>;
@ -67,19 +51,19 @@ fn send_line(socket: &mut TcpSocket, data: &[u8]) -> bool {
// instead of sending incomplete line
warn!(
"TCP socket has only {}/{} needed {}",
send_free + 1, socket.send_capacity(), data.len(),
send_free + 1,
socket.send_capacity(),
data.len(),
);
} else {
match socket.send_slice(&data) {
match socket.send_slice(data) {
Ok(sent) if sent == data.len() => {
let _ = socket.send_slice(b"\n");
// success
return true
return true;
}
Ok(sent) =>
warn!("sent only {}/{} bytes", sent, data.len()),
Err(e) =>
error!("error sending line: {:?}", e),
Ok(sent) => warn!("sent only {}/{} bytes", sent, data.len()),
Err(e) => error!("error sending line: {:?}", e),
}
}
// not success
@ -87,17 +71,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) => {
@ -106,7 +79,7 @@ impl Handler {
Err(e) => {
error!("unable to serialize report: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
return Err(Error::ReportError);
return Err(Error::Report);
}
}
Ok(Handler::Handled)
@ -120,35 +93,35 @@ impl Handler {
Err(e) => {
error!("unable to serialize pid summary: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
return Err(Error::ReportError);
return Err(Error::Report);
}
}
Ok(Handler::Handled)
}
fn show_pwm(socket: &mut TcpSocket, channels: &mut Channels) -> Result<Handler, Error> {
match channels.pwm_summaries_json() {
match channels.output_summaries_json() {
Ok(buf) => {
send_line(socket, &buf);
}
Err(e) => {
error!("unable to serialize pwm summary: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
return Err(Error::ReportError);
return Err(Error::Report);
}
}
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);
return Err(Error::Report);
}
}
Ok(Handler::Handled)
@ -162,7 +135,7 @@ impl Handler {
Err(e) => {
error!("unable to serialize postfilter summary: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
return Err(Error::ReportError);
return Err(Error::Report);
}
}
Ok(Handler::Handled)
@ -176,18 +149,37 @@ impl Handler {
Ok(Handler::Handled)
}
fn engage_pid (socket: &mut TcpSocket, channels: &mut Channels, leds: &mut Leds, channel: usize) -> Result<Handler, Error> {
fn engage_pid(
socket: &mut TcpSocket,
channels: &mut Channels,
channel: usize,
) -> Result<Handler, Error> {
channels.channel_state(channel).pid_engaged = true;
leds.g3.on();
send_line(socket, b"{}");
Ok(Handler::Handled)
}
fn set_pwm (socket: &mut TcpSocket, channels: &mut Channels, leds: &mut Leds, channel: usize, pin: PwmPin, value: f64) -> Result<Handler, Error> {
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 => {
channels.channel_state(channel).pid_engaged = false;
leds.g3.off();
let current = ElectricCurrent::new::<ampere>(value);
channels.set_i(channel, current);
channels.power_up(channel);
@ -209,76 +201,107 @@ impl Handler {
Ok(Handler::Handled)
}
fn set_center_point(socket: &mut TcpSocket, channels: &mut Channels, channel: usize, center: CenterPoint) -> Result<Handler, Error> {
let i_tec = channels.get_i(channel);
fn set_center_point(
socket: &mut TcpSocket,
channels: &mut Channels,
channel: usize,
center: CenterPoint,
) -> Result<Handler, Error> {
let i_set = channels.get_i_set(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)
}
fn set_pid (socket: &mut TcpSocket, channels: &mut Channels, channel: usize, parameter: PidParameter, value: f64) -> Result<Handler, Error> {
fn set_pid(
socket: &mut TcpSocket,
channels: &mut Channels,
channel: usize,
parameter: PidParameter,
value: f64,
) -> Result<Handler, Error> {
let pid = &mut channels.channel_state(channel).pid;
use super::command_parser::PidParameter::*;
match parameter {
Target =>
pid.target = value,
KP =>
pid.parameters.kp = value as f32,
KI =>
pid.update_ki(value as f32),
KD =>
pid.parameters.kd = value as f32,
OutputMin =>
pid.parameters.output_min = value as f32,
OutputMax =>
pid.parameters.output_max = value as f32,
Target => pid.target = value,
KP => pid.parameters.kp = value as f32,
KI => pid.update_ki(value as f32),
KD => pid.parameters.kd = value as f32,
OutputMin => pid.parameters.output_min = value as f32,
OutputMax => pid.parameters.output_max = value as f32,
}
send_line(socket, b"{}");
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 = TemperatureInterval::new::<kelvin>(value),
R0 => bp.r0 = ElectricalResistance::new::<ohm>(value),
}
send_line(socket, b"{}");
Ok(Handler::Handled)
}
fn reset_post_filter (socket: &mut TcpSocket, channels: &mut Channels, channel: usize) -> Result<Handler, Error> {
fn reset_post_filter(
socket: &mut TcpSocket,
channels: &mut Channels,
channel: usize,
) -> Result<Handler, Error> {
channels.adc.set_postfilter(channel as u8, None).unwrap();
send_line(socket, b"{}");
Ok(Handler::Handled)
}
fn set_post_filter (socket: &mut TcpSocket, channels: &mut Channels, channel: usize, rate: f32) -> Result<Handler, Error> {
fn set_post_filter(
socket: &mut TcpSocket,
channels: &mut Channels,
channel: usize,
rate: f32,
) -> Result<Handler, Error> {
let filter = ad7172::PostFilter::closest(rate);
match filter {
Some(filter) => {
channels.adc.set_postfilter(channel as u8, Some(filter)).unwrap();
channels
.adc
.set_postfilter(channel as u8, Some(filter))
.unwrap();
send_line(socket, b"{}");
}
None => {
error!("unable to choose postfilter for rate {:.3}", rate);
send_line(socket, b"{{\"error\": \"unable to choose postfilter rate\"}}");
return Err(Error::PostFilterRateError);
send_line(
socket,
b"{{\"error\": \"unable to choose postfilter rate\"}}",
);
return Err(Error::PostFilterRate);
}
}
Ok(Handler::Handled)
}
fn load_channel (socket: &mut TcpSocket, channels: &mut Channels, store: &mut FlashStore, channel: Option<usize>) -> Result<Handler, Error> {
for c in 0..CHANNELS {
fn load_channel(
socket: &mut TcpSocket,
channels: &mut Channels,
store: &mut FlashStore,
channel: Option<usize>,
) -> Result<Handler, Error> {
for (c, key) in CHANNEL_CONFIG_KEY.iter().enumerate().take(CHANNELS) {
if channel.is_none() || channel == Some(c) {
match store.read_value::<ChannelConfig>(CHANNEL_CONFIG_KEY[c]) {
match store.read_value::<ChannelConfig>(key) {
Ok(Some(config)) => {
config.apply(channels, c);
send_line(socket, b"{}");
@ -290,7 +313,7 @@ impl Handler {
Err(e) => {
error!("unable to load config from flash: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
return Err(Error::FlashError);
return Err(Error::Flash);
}
}
}
@ -298,19 +321,24 @@ impl Handler {
Ok(Handler::Handled)
}
fn save_channel (socket: &mut TcpSocket, channels: &mut Channels, channel: Option<usize>, store: &mut FlashStore) -> Result<Handler, Error> {
for c in 0..CHANNELS {
fn save_channel(
socket: &mut TcpSocket,
channels: &mut Channels,
channel: Option<usize>,
store: &mut FlashStore,
) -> Result<Handler, Error> {
for (c, key) in CHANNEL_CONFIG_KEY.iter().enumerate().take(CHANNELS) {
let mut store_value_buf = [0u8; 256];
if channel.is_none() || channel == Some(c) {
let config = ChannelConfig::new(channels, c);
match store.write_value(CHANNEL_CONFIG_KEY[c], &config, &mut store_value_buf) {
match store.write_value(key, &config, &mut store_value_buf) {
Ok(()) => {
send_line(socket, b"{}");
}
Err(e) => {
error!("unable to save channel {} config to flash: {:?}", c, e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
return Err(Error::FlashError);
return Err(Error::Flash);
}
}
}
@ -318,7 +346,11 @@ impl Handler {
Ok(Handler::Handled)
}
fn set_ipv4 (socket: &mut TcpSocket, store: &mut FlashStore, config: Ipv4Config) -> Result<Handler, Error> {
fn set_ipv4(
socket: &mut TcpSocket,
store: &mut FlashStore,
config: Ipv4Config,
) -> Result<Handler, Error> {
let _ = store
.write_value("ipv4", &config, [0; 16])
.map_err(|e| error!("unable to save ipv4 config to flash: {:?}", e));
@ -346,9 +378,16 @@ impl Handler {
Ok(Handler::Reset)
}
fn set_fan(socket: &mut TcpSocket, fan_pwm: u32, fan_ctrl: &mut FanCtrl) -> Result<Handler, Error> {
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);
@ -370,14 +409,17 @@ impl Handler {
Err(e) => {
error!("unable to serialize fan summary: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
Err(Error::ReportError)
Err(Error::Report)
}
}
}
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);
@ -389,7 +431,13 @@ impl Handler {
Ok(Handler::Handled)
}
fn fan_curve(socket: &mut TcpSocket, fan_ctrl: &mut FanCtrl, k_a: f32, k_b: f32, k_c: f32) -> Result<Handler, Error> {
fn fan_curve(
socket: &mut TcpSocket,
fan_ctrl: &mut FanCtrl,
k_a: f32,
k_b: f32,
k_c: f32,
) -> Result<Handler, Error> {
fan_ctrl.set_curve(k_a, k_b, k_c);
send_line(socket, b"{}");
Ok(Handler::Handled)
@ -410,29 +458,58 @@ impl Handler {
Err(e) => {
error!("unable to serialize HWRev summary: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
Err(Error::ReportError)
Err(Error::Report)
}
}
}
pub fn handle_command(command: Command, socket: &mut TcpSocket, channels: &mut Channels, session: &Session, leds: &mut Leds, 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::Output) => Handler::show_pwm(socket, channels),
Command::Show(ShowCommand::BParameter) => Handler::show_b_parameter(socket, channels),
Command::Show(ShowCommand::PostFilter) => Handler::show_post_filter(socket, channels),
Command::Show(ShowCommand::Ipv4) => Handler::show_ipv4(socket, ipv4_config),
Command::PwmPid { channel } => Handler::engage_pid(socket, channels, leds, channel),
Command::Pwm { channel, pin, value } => Handler::set_pwm(socket, channels, leds, 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::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::OutputPid { channel } => Handler::engage_pid(socket, channels, channel),
Command::OutputPolarity { channel, polarity } => {
Handler::set_polarity(socket, channels, channel, polarity)
}
Command::Output {
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::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),
Command::Save { channel } => Handler::save_channel(socket, channels, channel, store),
Command::Ipv4(config) => Handler::set_ipv4(socket, store, config),
@ -441,7 +518,9 @@ impl Handler {
Command::FanSet { fan_pwm } => Handler::set_fan(socket, fan_pwm, fan_ctrl),
Command::ShowFan => Handler::show_fan(socket, fan_ctrl),
Command::FanAuto => Handler::fan_auto(socket, fan_ctrl),
Command::FanCurve { k_a, k_b, k_c } => Handler::fan_curve(socket, fan_ctrl, k_a, k_b, k_c),
Command::FanCurve { k_a, k_b, k_c } => {
Handler::fan_curve(socket, fan_ctrl, k_a, k_b, k_c)
}
Command::FanCurveDefaults => Handler::fan_defaults(socket, fan_ctrl),
Command::ShowHWRev => Handler::show_hwrev(socket, hwrev),
}

View File

@ -2,19 +2,20 @@ use core::fmt;
use core::num::ParseIntError;
use core::str::{from_utf8, Utf8Error};
use nom::{
IResult,
branch::alt,
bytes::complete::{is_a, tag, take_while1},
character::{is_digit, complete::{char, one_of}},
character::{
complete::{char, one_of},
is_digit,
},
combinator::{complete, map, opt, value},
sequence::preceded,
multi::{fold_many0, fold_many1},
error::ErrorKind,
Needed,
multi::{fold_many0, fold_many1},
sequence::preceded,
IResult, Needed,
};
use num_traits::{Num, ParseFloatError};
use serde::{Serialize, Deserialize};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq)]
pub enum Error {
@ -30,12 +31,9 @@ pub enum Error {
impl<'t> From<nom::Err<(&'t [u8], ErrorKind)>> for Error {
fn from(e: nom::Err<(&'t [u8], ErrorKind)>) -> Self {
match e {
nom::Err::Incomplete(_) =>
Error::Incomplete,
nom::Err::Error((_, e)) =>
Error::Parser(e),
nom::Err::Failure((_, e)) =>
Error::Parser(e),
nom::Err::Incomplete(_) => Error::Incomplete,
nom::Err::Error((_, e)) => Error::Parser(e),
nom::Err::Failure((_, e)) => Error::Parser(e),
}
}
}
@ -61,8 +59,7 @@ impl From<ParseFloatError> for Error {
impl fmt::Display for Error {
fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match self {
Error::Incomplete =>
"incomplete input".fmt(fmt),
Error::Incomplete => "incomplete input".fmt(fmt),
Error::UnexpectedInput(c) => {
"unexpected input: ".fmt(fmt)?;
c.fmt(fmt)
@ -79,9 +76,7 @@ impl fmt::Display for Error {
"parsing int: ".fmt(fmt)?;
(e as &dyn core::fmt::Debug).fmt(fmt)
}
Error::ParseFloat => {
"parsing float".fmt(fmt)
}
Error::ParseFloat => "parsing float".fmt(fmt),
}
}
}
@ -96,10 +91,9 @@ pub struct Ipv4Config {
#[derive(Debug, Clone, PartialEq)]
pub enum ShowCommand {
Input,
Reporting,
Pwm,
Output,
Pid,
SteinhartHart,
BParameter,
PostFilter,
Ipv4,
}
@ -114,9 +108,9 @@ pub enum PidParameter {
OutputMax,
}
/// Steinhart-Hart equation parameter
/// B-Parameter equation parameter
#[derive(Debug, Clone, PartialEq)]
pub enum ShParameter {
pub enum BpParameter {
T0,
B,
R0,
@ -132,10 +126,16 @@ pub enum PwmPin {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum CenterPoint {
Vref,
VRef,
Override(f32),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Polarity {
Normal,
Reversed,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Command {
Quit,
@ -148,17 +148,20 @@ pub enum Command {
Reset,
Ipv4(Ipv4Config),
Show(ShowCommand),
Reporting(bool),
/// PWM parameter setting
Pwm {
Output {
channel: usize,
pin: PwmPin,
value: f64,
},
/// Enable PID control for `i_set`
PwmPid {
OutputPid {
channel: usize,
},
OutputPolarity {
channel: usize,
polarity: Polarity,
},
CenterPoint {
channel: usize,
center: CenterPoint,
@ -169,9 +172,9 @@ pub enum Command {
parameter: PidParameter,
value: f64,
},
SteinhartHart {
BParameter {
channel: usize,
parameter: ShParameter,
parameter: BpParameter,
value: f64,
},
PostFilter {
@ -180,7 +183,7 @@ pub enum Command {
},
Dfu,
FanSet {
fan_pwm: u32
fan_pwm: u32,
},
FanAuto,
ShowFan,
@ -194,12 +197,7 @@ pub enum Command {
}
fn end(input: &[u8]) -> IResult<&[u8], ()> {
complete(
fold_many0(
one_of("\r\n\t "),
(), |(), _| ()
)
)(input)
complete(fold_many0(one_of("\r\n\t "), (), |(), _| ()))(input)
}
fn whitespace(input: &[u8]) -> IResult<&[u8], ()> {
@ -207,14 +205,10 @@ fn whitespace(input: &[u8]) -> IResult<&[u8], ()> {
}
fn unsigned(input: &[u8]) -> IResult<&[u8], Result<u32, Error>> {
take_while1(is_digit)(input)
.map(|(input, digits)| {
let result =
from_utf8(digits)
take_while1(is_digit)(input).map(|(input, digits)| {
let result = from_utf8(digits)
.map_err(|e| e.into())
.and_then(|digits| u32::from_str_radix(digits, 10)
.map_err(|e| e.into())
);
.and_then(|digits| digits.parse::<u32>().map_err(|e| e.into()));
(input, result)
})
}
@ -222,23 +216,14 @@ fn unsigned(input: &[u8]) -> IResult<&[u8], Result<u32, Error>> {
fn float(input: &[u8]) -> IResult<&[u8], Result<f64, Error>> {
let (input, sign) = opt(is_a("-"))(input)?;
let negative = sign.is_some();
let (input, digits) = take_while1(|c| is_digit(c) || c == '.' as u8)(input)?;
let result =
from_utf8(digits)
let (input, digits) = take_while1(|c| is_digit(c) || c == b'.')(input)?;
let result = from_utf8(digits)
.map_err(|e| e.into())
.and_then(|digits| f64::from_str_radix(digits, 10)
.map_err(|e| e.into())
)
.and_then(|digits| f64::from_str_radix(digits, 10).map_err(|e| e.into()))
.map(|result: f64| if negative { -result } else { 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> {
map(one_of("01"), |c| (c as usize) - ('0' as usize))(input)
}
@ -246,83 +231,55 @@ 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)
))
value(Command::Show(ShowCommand::Input), end),
)(input)
}
fn pwm_setup(input: &[u8]) -> IResult<&[u8], Result<(PwmPin, f64), Error>> {
let result_with_pin = |pin: PwmPin|
move |result: Result<f64, Error>|
result.map(|value| (pin, value));
let result_with_pin =
|pin: PwmPin| move |result: Result<f64, Error>| result.map(|value| (pin, value));
alt((
map(
preceded(
tag("i_set"),
preceded(
whitespace,
float
)
),
result_with_pin(PwmPin::ISet)
preceded(tag("i_set"), preceded(whitespace, float)),
result_with_pin(PwmPin::ISet),
),
map(
preceded(
tag("max_i_pos"),
preceded(
whitespace,
float
)
),
result_with_pin(PwmPin::MaxIPos)
preceded(tag("max_i_pos"), preceded(whitespace, float)),
result_with_pin(PwmPin::MaxIPos),
),
map(
preceded(
tag("max_i_neg"),
preceded(
whitespace,
float
)
),
result_with_pin(PwmPin::MaxINeg)
preceded(tag("max_i_neg"), preceded(whitespace, float)),
result_with_pin(PwmPin::MaxINeg),
),
map(
preceded(
tag("max_v"),
preceded(
whitespace,
float
)
preceded(tag("max_v"), preceded(whitespace, float)),
result_with_pin(PwmPin::MaxV),
),
result_with_pin(PwmPin::MaxV)
))
)(input)
))(input)
}
/// `pwm <0-1> pid` - Set PWM to be controlled by PID
fn pwm_pid(input: &[u8]) -> IResult<&[u8], ()> {
/// `output <0-1> pid` - Set output to be controlled by PID
fn output_pid(input: &[u8]) -> IResult<&[u8], ()> {
value((), tag("pid"))(input)
}
fn pwm(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
let (input, _) = tag("pwm")(input)?;
fn output_polarity(input: &[u8]) -> IResult<&[u8], Polarity> {
preceded(
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((
|input| {
let (input, _) = whitespace(input)?;
@ -330,23 +287,32 @@ fn pwm(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
let (input, _) = whitespace(input)?;
let (input, result) = alt((
|input| {
let (input, ()) = pwm_pid(input)?;
Ok((input, Ok(Command::PwmPid { channel })))
let (input, ()) = output_pid(input)?;
Ok((input, Ok(Command::OutputPid { channel })))
},
|input| {
let (input, polarity) = output_polarity(input)?;
Ok((input, Ok(Command::OutputPolarity { channel, polarity })))
},
|input| {
let (input, config) = pwm_setup(input)?;
match config {
Ok((pin, value)) =>
Ok((input, Ok(Command::Pwm { channel, pin, value }))),
Err(e) =>
Ok((input, Err(e))),
Ok((pin, value)) => Ok((
input,
Ok(Command::Output {
channel,
pin,
value,
}),
)),
Err(e) => Ok((input, Err(e))),
}
},
))(input)?;
end(input)?;
Ok((input, result))
},
value(Ok(Command::Show(ShowCommand::Pwm)), end)
value(Ok(Command::Show(ShowCommand::Output)), end),
))(input)
}
@ -355,26 +321,26 @@ fn center_point(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
let (input, _) = whitespace(input)?;
let (input, channel) = channel(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)?;
Ok((input, value.map(|value| CenterPoint::Override(value as f32))))
}
))(input)?;
Ok((
input,
value.map(|value| CenterPoint::Override(value as f32)),
))
}))(input)?;
end(input)?;
Ok((input, center.map(|center| Command::CenterPoint {
channel,
center,
})))
Ok((
input,
center.map(|center| Command::CenterPoint { channel, center }),
))
}
/// `pid <0-1> <parameter> <value>`
fn pid_parameter(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
let (input, channel) = channel(input)?;
let (input, _) = whitespace(input)?;
let (input, parameter) =
alt((value(PidParameter::Target, tag("target")),
let (input, parameter) = alt((
value(PidParameter::Target, tag("target")),
value(PidParameter::KP, tag("kp")),
value(PidParameter::KI, tag("ki")),
value(PidParameter::KD, tag("kd")),
@ -383,8 +349,11 @@ fn pid_parameter(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
))(input)?;
let (input, _) = whitespace(input)?;
let (input, value) = float(input)?;
let result = value
.map(|value| Command::Pid { channel, parameter, value });
let result = value.map(|value| Command::Pid {
channel,
parameter,
value,
});
Ok((input, result))
}
@ -392,70 +361,66 @@ fn pid_parameter(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
fn pid(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
let (input, _) = tag("pid")(input)?;
alt((
preceded(
whitespace,
pid_parameter
),
value(Ok(Command::Show(ShowCommand::Pid)), end)
preceded(whitespace, pid_parameter),
value(Ok(Command::Show(ShowCommand::Pid)), end),
))(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"))
let (input, parameter) = 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 });
let result = 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
),
value(Ok(Command::Show(ShowCommand::SteinhartHart)), end)
preceded(whitespace, b_parameter_parameter),
value(Ok(Command::Show(ShowCommand::BParameter)), end),
))(input)
}
fn postfilter(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
let (input, _) = tag("postfilter")(input)?;
alt((
preceded(
whitespace,
|input| {
preceded(whitespace, |input| {
let (input, channel) = channel(input)?;
let (input, _) = whitespace(input)?;
alt((
value(Ok(Command::PostFilter {
value(
Ok(Command::PostFilter {
channel,
rate: None,
}), tag("off")),
}),
tag("off"),
),
move |input| {
let (input, _) = tag("rate")(input)?;
let (input, _) = whitespace(input)?;
let (input, rate) = float(input)?;
let result = rate
.map(|rate| Command::PostFilter {
let result = rate.map(|rate| Command::PostFilter {
channel,
rate: Some(rate as f32),
});
Ok((input, result))
}
},
))(input)
}
),
value(Ok(Command::Show(ShowCommand::PostFilter)), end)
}),
value(Ok(Command::Show(ShowCommand::PostFilter)), end),
))(input)
}
@ -468,7 +433,7 @@ fn load(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
let (input, _) = end(input)?;
Ok((input, Some(channel)))
},
value(None, end)
value(None, end),
))(input)?;
let result = Ok(Command::Load { channel });
@ -484,7 +449,7 @@ fn save(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
let (input, _) = end(input)?;
Ok((input, Some(channel)))
},
value(None, end)
value(None, end),
))(input)?;
let result = Ok(Command::Save { channel });
@ -546,12 +511,17 @@ fn fan(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
},
|input| {
let (input, value) = unsigned(input)?;
Ok((input, Ok(Command::FanSet { fan_pwm: value.unwrap_or(0)})))
Ok((
input,
Ok(Command::FanSet {
fan_pwm: value.unwrap_or(0),
}),
))
},
))(input)?;
Ok((input, result))
},
value(Ok(Command::ShowFan), end)
value(Ok(Command::ShowFan), end),
))(input)
}
@ -571,8 +541,15 @@ fn fan_curve(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
let (input, k_b) = float(input)?;
let (input, _) = whitespace(input)?;
let (input, k_c) = float(input)?;
if k_a.is_ok() && k_b.is_ok() && k_c.is_ok() {
Ok((input, Ok(Command::FanCurve { k_a: k_a.unwrap() as f32, k_b: k_b.unwrap() as f32, k_c: k_c.unwrap() as f32 })))
if let (Ok(k_a), Ok(k_b), Ok(k_c)) = (k_a, k_b, k_c) {
Ok((
input,
Ok(Command::FanCurve {
k_a: k_a as f32,
k_b: k_b as f32,
k_c: k_c as f32,
}),
))
} else {
Err(nom::Err::Incomplete(Needed::Size(3)))
}
@ -580,21 +557,22 @@ fn fan_curve(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
))(input)?;
Ok((input, result))
},
value(Err(Error::Incomplete), end)
value(Err(Error::Incomplete), end),
))(input)
}
fn command(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
alt((value(Ok(Command::Quit), tag("quit")),
alt((
value(Ok(Command::Quit), tag("quit")),
load,
save,
value(Ok(Command::Reset), tag("reset")),
ipv4,
map(report, Ok),
pwm,
output,
center_point,
pid,
steinhart_hart,
b_parameter,
postfilter,
value(Ok(Command::Dfu), tag("dfu")),
fan,
@ -606,12 +584,9 @@ fn command(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
impl Command {
pub fn parse(input: &[u8]) -> Result<Self, Error> {
match command(input) {
Ok((input_remain, result)) if input_remain.len() == 0 =>
result,
Ok((input_remain, _)) =>
Err(Error::UnexpectedInput(input_remain[0])),
Err(e) =>
Err(e.into()),
Ok((input_remain, result)) if input_remain.is_empty() => result,
Ok((input_remain, _)) => Err(Error::UnexpectedInput(input_remain[0])),
Err(e) => Err(e.into()),
}
}
}
@ -659,21 +634,27 @@ mod test {
#[test]
fn parse_ipv4() {
let command = Command::parse(b"ipv4 192.168.1.26/24");
assert_eq!(command, Ok(Command::Ipv4(Ipv4Config {
assert_eq!(
command,
Ok(Command::Ipv4(Ipv4Config {
address: [192, 168, 1, 26],
mask_len: 24,
gateway: None,
})));
}))
);
}
#[test]
fn parse_ipv4_and_gateway() {
let command = Command::parse(b"ipv4 10.42.0.126/8 10.1.0.1");
assert_eq!(command, Ok(Command::Ipv4(Ipv4Config {
assert_eq!(
command,
Ok(Command::Ipv4(Ipv4Config {
address: [10, 42, 0, 126],
mask_len: 8,
gateway: Some([10, 1, 0, 1]),
})));
}))
);
}
#[test]
@ -683,69 +664,73 @@ mod test {
}
#[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");
assert_eq!(command, Ok(Command::Pwm {
fn parse_output_i_set() {
let command = Command::parse(b"output 1 i_set 16383");
assert_eq!(
command,
Ok(Command::Output {
channel: 1,
pin: PwmPin::ISet,
value: 16383.0,
}));
})
);
}
#[test]
fn parse_pwm_pid() {
let command = Command::parse(b"pwm 0 pid");
assert_eq!(command, Ok(Command::PwmPid {
fn parse_output_polarity() {
let command = Command::parse(b"output 0 polarity reversed");
assert_eq!(
command,
Ok(Command::OutputPolarity {
channel: 0,
}));
polarity: Polarity::Reversed,
})
);
}
#[test]
fn parse_pwm_max_i_pos() {
let command = Command::parse(b"pwm 0 max_i_pos 7");
assert_eq!(command, Ok(Command::Pwm {
fn parse_output_pid() {
let command = Command::parse(b"output 0 pid");
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!(
command,
Ok(Command::Output {
channel: 0,
pin: PwmPin::MaxIPos,
value: 7.0,
}));
})
);
}
#[test]
fn parse_pwm_max_i_neg() {
let command = Command::parse(b"pwm 0 max_i_neg 128");
assert_eq!(command, Ok(Command::Pwm {
fn parse_output_max_i_neg() {
let command = Command::parse(b"output 0 max_i_neg 128");
assert_eq!(
command,
Ok(Command::Output {
channel: 0,
pin: PwmPin::MaxINeg,
value: 128.0,
}));
})
);
}
#[test]
fn parse_pwm_max_v() {
let command = Command::parse(b"pwm 0 max_v 32768");
assert_eq!(command, Ok(Command::Pwm {
fn parse_output_max_v() {
let command = Command::parse(b"output 0 max_v 32768");
assert_eq!(
command,
Ok(Command::Output {
channel: 0,
pin: PwmPin::MaxV,
value: 32768.0,
}));
})
);
}
#[test]
@ -757,27 +742,33 @@ mod test {
#[test]
fn parse_pid_target() {
let command = Command::parse(b"pid 0 target 36.5");
assert_eq!(command, Ok(Command::Pid {
assert_eq!(
command,
Ok(Command::Pid {
channel: 0,
parameter: PidParameter::Target,
value: 36.5,
}));
})
);
}
#[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,
}));
})
);
}
#[test]
@ -789,37 +780,49 @@ mod test {
#[test]
fn parse_postfilter_off() {
let command = Command::parse(b"postfilter 1 off");
assert_eq!(command, Ok(Command::PostFilter {
assert_eq!(
command,
Ok(Command::PostFilter {
channel: 1,
rate: None,
}));
})
);
}
#[test]
fn parse_postfilter_rate() {
let command = Command::parse(b"postfilter 0 rate 21");
assert_eq!(command, Ok(Command::PostFilter {
assert_eq!(
command,
Ok(Command::PostFilter {
channel: 0,
rate: Some(21.0),
}));
})
);
}
#[test]
fn parse_center_point() {
let command = Command::parse(b"center 0 1.5");
assert_eq!(command, Ok(Command::CenterPoint {
assert_eq!(
command,
Ok(Command::CenterPoint {
channel: 0,
center: CenterPoint::Override(1.5),
}));
})
);
}
#[test]
fn parse_center_point_vref() {
let command = Command::parse(b"center 1 vref");
assert_eq!(command, Ok(Command::CenterPoint {
assert_eq!(
command,
Ok(Command::CenterPoint {
channel: 1,
center: CenterPoint::Vref,
}));
center: CenterPoint::VRef,
})
);
}
#[test]
@ -843,11 +846,14 @@ mod test {
#[test]
fn parse_fcurve_set() {
let command = Command::parse(b"fcurve 1.2 3.4 5.6");
assert_eq!(command, Ok(Command::FanCurve {
assert_eq!(
command,
Ok(Command::FanCurve {
k_a: 1.2,
k_b: 3.4,
k_c: 5.6
}));
})
);
}
#[test]

View File

@ -1,16 +1,13 @@
use serde::{Serialize, Deserialize};
use uom::si::{
electric_potential::volt,
electric_current::ampere,
f64::{ElectricCurrent, ElectricPotential},
};
use crate::{
ad7172::PostFilter,
b_parameter,
channels::Channels,
command_parser::CenterPoint,
command_parser::{CenterPoint, Polarity},
pid,
steinhart_hart,
};
use num_traits::Zero;
use serde::{Deserialize, Serialize};
use uom::si::f64::{ElectricCurrent, ElectricPotential};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ChannelConfig {
@ -18,7 +15,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,
@ -28,17 +27,26 @@ impl ChannelConfig {
pub fn new(channels: &mut Channels, channel: usize) -> Self {
let pwm = PwmLimits::new(channels, channel);
let adc_postfilter = channels.adc.get_postfilter(channel as u8)
let adc_postfilter = channels
.adc
.get_postfilter(channel as u8)
.unwrap()
.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 +58,7 @@ impl ChannelConfig {
state.pid.parameters = self.pid.clone();
state.pid.target = self.pid_target.into();
state.pid_engaged = self.pid_engaged;
state.sh = self.sh.clone();
state.bp = self.bp.clone();
self.pwm.apply(channels, channel);
@ -59,31 +67,33 @@ 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: ElectricPotential,
pub max_i_pos: ElectricCurrent,
pub max_i_neg: ElectricCurrent,
}
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>(),
max_i_neg: max_i_neg.get::<ampere>(),
max_v,
max_i_pos,
max_i_neg,
}
}
pub fn apply(&self, channels: &mut Channels, channel: usize) {
channels.set_max_v(channel, ElectricPotential::new::<volt>(self.max_v));
channels.set_max_i_pos(channel, ElectricCurrent::new::<ampere>(self.max_i_pos));
channels.set_max_i_neg(channel, ElectricCurrent::new::<ampere>(self.max_i_neg));
channels.set_max_v(channel, self.max_v);
channels.set_max_i_pos(channel, self.max_i_pos);
channels.set_max_i_neg(channel, self.max_i_neg);
}
}

View File

@ -1,3 +1,4 @@
use core::arch::asm;
use cortex_m_rt::pre_init;
use stm32f4xx_hal::stm32::{RCC, SYSCFG};

View File

@ -1,24 +1,17 @@
use crate::{channels::MAX_TEC_I, command_handler::JsonBuffer, hw_rev::HWSettings};
use num_traits::Float;
use serde::Serialize;
use stm32f4xx_hal::{
pwm::{self, PwmChannels},
pac::TIM8,
pwm::{self, PwmChannels},
};
use crate::{
hw_rev::HWSettings,
command_handler::JsonBuffer,
};
use uom::si::{electric_current::ampere, f64::ElectricCurrent};
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;
pub struct FanCtrl {
fan: Option<FanPin>,
fan_auto: bool,
@ -50,12 +43,14 @@ 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;
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);
}
}
@ -88,9 +83,11 @@ impl FanCtrl {
}
pub fn restore_defaults(&mut self) {
self.set_curve(self.hw_settings.fan_k_a,
self.set_curve(
self.hw_settings.fan_k_a,
self.hw_settings.fan_k_b,
self.hw_settings.fan_k_c);
self.hw_settings.fan_k_c,
);
}
pub fn set_pwm(&mut self, fan_pwm: u32) -> f32 {
@ -98,8 +95,14 @@ impl FanCtrl {
return 0f32;
}
let fan = self.fan.as_mut().unwrap();
let fan_pwm = fan_pwm.min(MAX_USER_FAN_PWM as u32).max(MIN_USER_FAN_PWM as u32);
let duty = scale_number(fan_pwm as f32, self.hw_settings.min_fan_pwm, self.hw_settings.max_fan_pwm, MIN_USER_FAN_PWM, MAX_USER_FAN_PWM);
let fan_pwm = fan_pwm.clamp(MIN_USER_FAN_PWM as u32, MAX_USER_FAN_PWM as u32);
let duty = scale_number(
fan_pwm as f32,
self.hw_settings.min_fan_pwm,
self.hw_settings.max_fan_pwm,
MIN_USER_FAN_PWM,
MAX_USER_FAN_PWM,
);
let max = fan.get_max_duty();
let value = ((duty * (max as f32)) as u16).min(max);
fan.set_duty(value);
@ -118,8 +121,17 @@ impl FanCtrl {
if let Some(fan) = &self.fan {
let duty = fan.get_duty();
let max = fan.get_max_duty();
scale_number(duty as f32 / (max as f32), MIN_USER_FAN_PWM, MAX_USER_FAN_PWM, self.hw_settings.min_fan_pwm, self.hw_settings.max_fan_pwm).round() as u32
} else { 0 }
scale_number(
duty as f32 / (max as f32),
MIN_USER_FAN_PWM,
MAX_USER_FAN_PWM,
self.hw_settings.min_fan_pwm,
self.hw_settings.max_fan_pwm,
)
.round() as u32
} else {
0
}
}
fn enable_pwm(&mut self) -> bool {
@ -135,7 +147,6 @@ impl FanCtrl {
}
}
fn scale_number(unscaled: f32, to_min: f32, to_max: f32, from_min: f32, from_max: f32) -> f32 {
(to_max - to_min) * (unscaled - from_min) / (from_max - from_min) + to_min
}

View File

@ -1,9 +1,9 @@
use log::{info, error};
use log::{error, info};
use sfkv::{Store, StoreBackend};
use stm32f4xx_hal::{
flash::{Error, FlashExt},
stm32::FLASH,
};
use sfkv::{Store, StoreBackend};
/// 16 KiB
pub const FLASH_SECTOR_SIZE: usize = 0x4000;
@ -21,9 +21,7 @@ pub struct FlashBackend {
}
fn get_offset() -> usize {
unsafe {
(&_config_start as *const usize as usize) - (&_flash_start as *const usize as usize)
}
unsafe { (&_config_start as *const usize as usize) - (&_flash_start as *const usize as usize) }
}
impl StoreBackend for FlashBackend {
@ -40,7 +38,8 @@ impl StoreBackend for FlashBackend {
}
fn program(&mut self, offset: usize, payload: &[u8]) -> Result<(), Self::Error> {
self.flash.unlocked()
self.flash
.unlocked()
.program(get_offset() + offset, payload.iter())
}
@ -60,7 +59,8 @@ pub fn store(flash: FLASH) -> FlashStore {
Ok(_) => {}
Err(e) => {
error!("corrupt store, erasing. error: {:?}", e);
let _ = store.erase()
let _ = store
.erase()
.map_err(|e| error!("flash erase failed: {:?}", e));
}
}

View File

@ -1,9 +1,6 @@
use serde::Serialize;
use crate::{
pins::HWRevPins,
command_handler::JsonBuffer,
};
use crate::{command_handler::JsonBuffer, pins::HWRevPins};
#[derive(Serialize, Copy, Clone)]
pub struct HWRev {
@ -31,13 +28,17 @@ struct HWSummary<'a> {
impl HWRev {
pub fn detect_hw_rev(hwrev_pins: &HWRevPins) -> Self {
let (h0, h1, h2, h3) = (hwrev_pins.hwrev0.is_high(), hwrev_pins.hwrev1.is_high(),
hwrev_pins.hwrev2.is_high(), hwrev_pins.hwrev3.is_high());
let (h0, h1, h2, h3) = (
hwrev_pins.hwrev0.is_high(),
hwrev_pins.hwrev1.is_high(),
hwrev_pins.hwrev2.is_high(),
hwrev_pins.hwrev3.is_high(),
);
match (h0, h1, h2, h3) {
(true, true, true, false) => HWRev { major: 1, minor: 0 },
(true, false, false, false) => HWRev { major: 2, minor: 0 },
(false, true, false, false) => HWRev { major: 2, minor: 2 },
(_, _, _, _) => HWRev { major: 0, minor: 0 }
(_, _, _, _) => HWRev { major: 0, minor: 0 },
}
}
@ -70,13 +71,16 @@ impl HWRev {
fan_pwm_freq_hz: 0,
fan_available: false,
fan_pwm_recommended: false,
}
},
}
}
pub fn summary(&self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
let settings = self.settings();
let summary = HWSummary { rev: self, settings: &settings };
let summary = HWSummary {
rev: self,
settings: &settings,
};
serde_json_core::to_vec(&summary)
}
}

View File

@ -10,17 +10,15 @@ pub fn init_log() {
#[cfg(feature = "semihosting")]
pub fn init_log() {
use cortex_m_log::log::{init, Logger};
use cortex_m_log::printer::semihosting::{hio::HStdout, InterruptOk};
use log::LevelFilter;
use cortex_m_log::log::{Logger, init};
use cortex_m_log::printer::semihosting::{InterruptOk, hio::HStdout};
static mut LOGGER: Option<Logger<InterruptOk<HStdout>>> = None;
let logger = Logger {
inner: InterruptOk::<_>::stdout().expect("semihosting stdout"),
level: LevelFilter::Info,
};
let logger = unsafe {
LOGGER.get_or_insert(logger)
};
let logger = unsafe { LOGGER.get_or_insert(logger) };
init(logger).expect("set logger");
}

View File

@ -1,6 +1,6 @@
use stm32f4xx_hal::{
gpio::{
gpiod::{PD9, PD10, PD11},
gpiod::{PD10, PD11, PD9},
Output, PushPull,
},
hal::digital::v2::OutputPin,

View File

@ -1,38 +1,33 @@
#![cfg_attr(not(test), no_std)]
#![cfg_attr(not(test), no_main)]
#![feature(maybe_uninit_extra, asm)]
#![cfg_attr(test, allow(unused))]
// TODO: #![deny(warnings, unused)]
#[cfg(not(any(feature = "semihosting", test)))]
use panic_abort as _;
use panic_halt as _;
#[cfg(all(feature = "semihosting", not(test)))]
use panic_semihosting as _;
use log::{error, info, warn};
use cortex_m::asm::wfi;
use cortex_m_rt::entry;
use log::{error, info, warn};
use smoltcp::{socket::TcpSocket, time::Instant, wire::EthernetAddress};
use stm32f4xx_hal::{
hal::watchdog::{WatchdogEnable, Watchdog},
hal::watchdog::{Watchdog, WatchdogEnable},
rcc::RccExt,
stm32::{CorePeripherals, Peripherals, SCB},
time::{U32Ext, MegaHertz},
time::{MegaHertz, U32Ext},
watchdog::IndependentWatchdog,
};
use smoltcp::{
time::Instant,
socket::TcpSocket,
wire::EthernetAddress,
};
mod init_log;
use init_log::init_log;
mod usb;
mod leds;
mod pins;
mod usb;
use pins::Pins;
mod ad7172;
mod ad5680;
mod ad7172;
mod net;
mod server;
use server::Server;
@ -40,18 +35,18 @@ mod session;
use session::{Session, SessionInput};
mod command_parser;
use command_parser::Ipv4Config;
mod timer;
mod pid;
mod steinhart_hart;
mod b_parameter;
mod channels;
use channels::{CHANNELS, Channels};
mod pid;
mod timer;
use channels::{Channels, CHANNELS};
mod channel;
mod channel_state;
mod config;
use config::ChannelConfig;
mod flash_store;
mod dfu;
mod command_handler;
mod dfu;
mod flash_store;
use command_handler::Handler;
mod fan_ctrl;
use fan_ctrl::FanCtrl;
@ -74,19 +69,19 @@ fn send_line(socket: &mut TcpSocket, data: &[u8]) -> bool {
// instead of sending incomplete line
warn!(
"TCP socket has only {}/{} needed {}",
send_free + 1, socket.send_capacity(), data.len(),
send_free + 1,
socket.send_capacity(),
data.len(),
);
} else {
match socket.send_slice(&data) {
match socket.send_slice(data) {
Ok(sent) if sent == data.len() => {
let _ = socket.send_slice(b"\n");
// success
return true
return true;
}
Ok(sent) =>
warn!("sent only {}/{} bytes", sent, data.len()),
Err(e) =>
error!("error sending line: {:?}", e),
Ok(sent) => warn!("sent only {}/{} bytes", sent, data.len()),
Err(e) => error!("error sending line: {:?}", e),
}
}
// not success
@ -105,7 +100,9 @@ fn main() -> ! {
cp.SCB.enable_dcache(&mut cp.CPUID);
let dp = Peripherals::take().unwrap();
let clocks = dp.RCC.constrain()
let clocks = dp
.RCC
.constrain()
.cfgr
.use_hse(HSE)
.sysclk(168.mhz())
@ -121,14 +118,15 @@ fn main() -> ! {
timer::setup(cp.SYST, clocks);
let (pins, mut leds, mut eeprom, eth_pins, usb, fan, hwrev, hw_settings) = Pins::setup(
clocks, dp.TIM1, dp.TIM3, dp.TIM8,
clocks,
(dp.TIM1, dp.TIM3, dp.TIM8),
(
dp.GPIOA, dp.GPIOB, dp.GPIOC, dp.GPIOD, dp.GPIOE, dp.GPIOF, dp.GPIOG,
),
dp.I2C1,
dp.SPI2, dp.SPI4, dp.SPI5,
(dp.SPI2, dp.SPI4, dp.SPI5),
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();
@ -140,14 +138,11 @@ fn main() -> ! {
let mut store = flash_store::store(dp.FLASH);
let mut channels = Channels::new(pins);
for c in 0..CHANNELS {
match store.read_value::<ChannelConfig>(CHANNEL_CONFIG_KEY[c]) {
Ok(Some(config)) =>
config.apply(&mut channels, c),
Ok(None) =>
error!("flash config not found for channel {}", c),
Err(e) =>
error!("unable to load config {} from flash: {:?}", c, e),
for (c, key) in CHANNEL_CONFIG_KEY.iter().enumerate().take(CHANNELS) {
match store.read_value::<ChannelConfig>(key) {
Ok(Some(config)) => config.apply(&mut channels, c),
Ok(None) => error!("flash config not found for channel {}", c),
Err(e) => error!("unable to load config {} from flash: {:?}", c, e),
}
}
@ -160,11 +155,9 @@ fn main() -> ! {
gateway: None,
};
match store.read_value("ipv4") {
Ok(Some(config)) =>
ipv4_config = config,
Ok(Some(config)) => ipv4_config = config,
Ok(None) => {}
Err(e) =>
error!("cannot read ipv4 config: {:?}", e),
Err(e) => error!("cannot read ipv4 config: {:?}", e),
}
// EEPROM ships with a read-only EUI-48 identifier
@ -173,7 +166,14 @@ fn main() -> ! {
let hwaddr = EthernetAddress(eui48);
info!("EEPROM MAC address: {}", hwaddr);
net::run(clocks, dp.ETHERNET_MAC, dp.ETHERNET_DMA, eth_pins, hwaddr, ipv4_config.clone(), |iface| {
net::run(
clocks,
dp.ETHERNET_MAC,
dp.ETHERNET_DMA,
eth_pins,
hwaddr,
ipv4_config.clone(),
|iface| {
Server::<Session>::run(iface, |server| {
leds.r1.off();
let mut should_reset = false;
@ -181,17 +181,19 @@ 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();
} else {
leds.g3.off();
}
let instant = Instant::from_millis(i64::from(timer::now()));
cortex_m::interrupt::free(net::clear_pending);
server.poll(instant)
.unwrap_or_else(|e| {
server.poll(instant).unwrap_or_else(|e| {
warn!("poll: {:?}", e);
});
@ -211,33 +213,27 @@ 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 leds, &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::Handled) => {}
Ok(Handler::CloseSocket) => socket.close(),
Ok(Handler::Reset) => should_reset = true,
Err(_) => {},
Err(_) => {}
}
}
Ok(SessionInput::Error(e)) => {
error!("session input: {:?}", e);
send_line(&mut socket, b"{ \"error\": \"invalid input\" }");
}
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);
}
}
Err(_) => socket.close(),
}
}
});
@ -258,11 +254,10 @@ fn main() -> ! {
}
// Apply new IPv4 address/gateway
new_ipv4_config.take()
.map(|config| {
if let Some(config) = new_ipv4_config.take() {
server.set_ipv4_config(config.clone());
ipv4_config = config;
});
};
// Update watchdog
wd.feed();
@ -278,7 +273,8 @@ fn main() -> ! {
leds.g4.on();
}
});
});
},
);
unreachable!()
}

View File

@ -1,20 +1,17 @@
//! As there is only one peripheral, supporting data structures are
//! declared once and globally.
use core::cell::RefCell;
use cortex_m::interrupt::{CriticalSection, Mutex};
use stm32f4xx_hal::{
rcc::Clocks,
pac::{interrupt, Peripherals, ETHERNET_MAC, ETHERNET_DMA},
};
use smoltcp::wire::{EthernetAddress, Ipv4Address, Ipv4Cidr};
use smoltcp::iface::{
EthernetInterfaceBuilder, EthernetInterface,
NeighborCache, Routes,
};
use stm32_eth::{Eth, RingEntry, RxDescriptor, TxDescriptor};
use crate::command_parser::Ipv4Config;
use crate::pins::EthernetPins;
use core::cell::RefCell;
use cortex_m::interrupt::{CriticalSection, Mutex};
use smoltcp::iface::{EthernetInterface, EthernetInterfaceBuilder, NeighborCache, Routes};
use smoltcp::wire::{EthernetAddress, Ipv4Address, Ipv4Cidr};
use stm32_eth::{Eth, RingEntry, RxDescriptor, TxDescriptor};
use stm32f4xx_hal::{
pac::{interrupt, Peripherals, ETHERNET_DMA, ETHERNET_MAC},
rcc::Clocks,
};
/// Not on the stack so that stack can be placed in CCMRAM (which the
/// ethernet peripheral cannot access)
@ -30,27 +27,27 @@ static NET_PENDING: Mutex<RefCell<bool>> = Mutex::new(RefCell::new(false));
/// Run callback `f` with ethernet driver and TCP/IP stack
pub fn run<F>(
clocks: Clocks,
ethernet_mac: ETHERNET_MAC, ethernet_dma: ETHERNET_DMA,
ethernet_mac: ETHERNET_MAC,
ethernet_dma: ETHERNET_DMA,
eth_pins: EthernetPins,
ethernet_addr: EthernetAddress,
ipv4_config: Ipv4Config,
f: F
f: F,
) where
F: FnOnce(EthernetInterface<&mut stm32_eth::Eth<'static, 'static>>),
{
let rx_ring = unsafe {
RX_RING.get_or_insert(Default::default())
};
let tx_ring = unsafe {
TX_RING.get_or_insert(Default::default())
};
let rx_ring = unsafe { RX_RING.get_or_insert(Default::default()) };
let tx_ring = unsafe { TX_RING.get_or_insert(Default::default()) };
// Ethernet driver
let mut eth_dev = Eth::new(
ethernet_mac, ethernet_dma,
&mut rx_ring[..], &mut tx_ring[..],
ethernet_mac,
ethernet_dma,
&mut rx_ring[..],
&mut tx_ring[..],
clocks,
eth_pins,
).unwrap();
)
.unwrap();
eth_dev.enable_interrupt();
// IP stack
@ -76,8 +73,7 @@ pub fn run<F>(
#[interrupt]
fn ETH() {
cortex_m::interrupt::free(|cs| {
*NET_PENDING.borrow(cs)
.borrow_mut() = true;
*NET_PENDING.borrow(cs).borrow_mut() = true;
});
let p = unsafe { Peripherals::steal() };
@ -86,15 +82,13 @@ fn ETH() {
/// Has an interrupt occurred since last call to `clear_pending()`?
pub fn is_pending(cs: &CriticalSection) -> bool {
*NET_PENDING.borrow(cs)
.borrow()
*NET_PENDING.borrow(cs).borrow()
}
/// Clear the interrupt pending flag before polling the interface for
/// data.
pub fn clear_pending(cs: &CriticalSection) {
*NET_PENDING.borrow(cs)
.borrow_mut() = false;
*NET_PENDING.borrow(cs).borrow_mut() = false;
}
/// utility for destructuring into smoltcp types

View File

@ -1,4 +1,4 @@
use serde::{Serialize, Deserialize};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Parameters {
@ -39,7 +39,7 @@ pub struct Controller {
impl Controller {
pub const fn new(parameters: Parameters) -> Controller {
Controller {
parameters: parameters,
parameters,
target: 0.0,
u1: 0.0,
x1: 0.0,
@ -54,15 +54,12 @@ 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();
}

View File

@ -1,49 +1,41 @@
use crate::{
channel::{Channel0, Channel1},
fan_ctrl::FanPin,
hw_rev::{HWRev, HWSettings},
leds::Leds,
};
use eeprom24x::{self, Eeprom24x};
use stm32_eth::EthPins;
use stm32f4xx_hal::{
adc::Adc,
gpio::{
AF5, Alternate, AlternateOD, Analog, Floating, Input,
gpioa::*,
gpiob::*,
gpioc::*,
gpioe::*,
gpiof::*,
gpiog::*,
GpioExt,
Output, PushPull,
gpioa::*, gpiob::*, gpioc::*, gpioe::*, gpiof::*, gpiog::*, Alternate, AlternateOD, Analog,
Floating, GpioExt, Input, Output, PushPull, AF5,
},
hal::{self, blocking::spi::Transfer, digital::v2::OutputPin},
i2c::I2c,
otg_fs::USB,
rcc::Clocks,
pwm::{self, PwmChannels},
spi::{Spi, NoMiso, TransferModeNormal},
pac::{
ADC1,
GPIOA, GPIOB, GPIOC, GPIOD, GPIOE, GPIOF, GPIOG,
I2C1,
OTG_FS_GLOBAL, OTG_FS_DEVICE, OTG_FS_PWRCLK,
SPI2, SPI4, SPI5,
TIM1, TIM3, TIM8
ADC1, GPIOA, GPIOB, GPIOC, GPIOD, GPIOE, GPIOF, GPIOG, I2C1, OTG_FS_DEVICE, OTG_FS_GLOBAL,
OTG_FS_PWRCLK, SPI2, SPI4, SPI5, TIM1, TIM3, TIM8,
},
timer::Timer,
pwm::{self, PwmChannels},
rcc::Clocks,
spi::{NoMiso, Spi, TransferModeNormal},
time::U32Ext,
};
use eeprom24x::{self, Eeprom24x};
use stm32_eth::EthPins;
use crate::{
channel::{Channel0, Channel1},
leds::Leds,
fan_ctrl::FanPin,
hw_rev::{HWRev, HWSettings},
timer::Timer,
};
pub type Eeprom = Eeprom24x<
I2c<I2C1, (
I2c<
I2C1,
(
PB8<AlternateOD<{ stm32f4xx_hal::gpio::AF4 }>>,
PB9<AlternateOD<{ stm32f4xx_hal::gpio::AF4 }>>
)>,
PB9<AlternateOD<{ stm32f4xx_hal::gpio::AF4 }>>,
),
>,
eeprom24x::page_size::B8,
eeprom24x::addr_size::OneByte
eeprom24x::addr_size::OneByte,
>;
pub type EthernetPins = EthPins<
@ -61,33 +53,51 @@ 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>;
}
/// SPI peripheral used for communication with the ADC
pub type AdcSpi = Spi<SPI2, (PB10<Alternate<AF5>>, PB14<Alternate<AF5>>, PB15<Alternate<AF5>>), TransferModeNormal>;
pub type AdcSpi = Spi<
SPI2,
(
PB10<Alternate<AF5>>,
PB14<Alternate<AF5>>,
PB15<Alternate<AF5>>,
),
TransferModeNormal,
>;
pub type AdcNss = PB12<Output<PushPull>>;
type Dac0Spi = Spi<SPI4, (PE2<Alternate<AF5>>, NoMiso, PE6<Alternate<AF5>>), TransferModeNormal>;
type Dac1Spi = Spi<SPI5, (PF7<Alternate<AF5>>, NoMiso, PF9<Alternate<AF5>>), TransferModeNormal>;
@ -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,
}
@ -123,13 +133,34 @@ impl Pins {
/// Setup GPIO pins and configure MCU peripherals
pub fn setup(
clocks: Clocks,
tim1: TIM1, tim3: TIM3, tim8: TIM8,
gpioa: GPIOA, gpiob: GPIOB, gpioc: GPIOC, gpiod: GPIOD, gpioe: GPIOE, gpiof: GPIOF, gpiog: GPIOG,
(tim1, tim3, tim8): (TIM1, TIM3, TIM8),
(gpioa, gpiob, gpioc, gpiod, gpioe, gpiof, gpiog): (
GPIOA,
GPIOB,
GPIOC,
GPIOD,
GPIOE,
GPIOF,
GPIOG,
),
i2c1: I2C1,
spi2: SPI2, spi4: SPI4, spi5: SPI5,
(spi2, spi4, spi5): (SPI2, SPI4, SPI5),
adc1: ADC1,
otg_fs_global: OTG_FS_GLOBAL, otg_fs_device: OTG_FS_DEVICE, otg_fs_pwrclk: OTG_FS_PWRCLK,
) -> (Self, Leds, Eeprom, EthernetPins, USB, Option<FanPin>, HWRev, HWSettings) {
(otg_fs_global, otg_fs_device, otg_fs_pwrclk): (
OTG_FS_GLOBAL,
OTG_FS_DEVICE,
OTG_FS_PWRCLK,
),
) -> (
Self,
Leds,
Eeprom,
EthernetPins,
USB,
Option<FanPin>,
HWRev,
HWSettings,
) {
let gpioa = gpioa.split();
let gpiob = gpiob.split();
let gpioc = gpioc.split();
@ -144,19 +175,29 @@ impl Pins {
let pins_adc = Adc::adc1(adc1, true, Default::default());
let pwm = PwmPins::setup(
clocks, tim1, tim3,
gpioc.pc6, gpioc.pc7,
gpioe.pe9, gpioe.pe11,
gpioe.pe13, gpioe.pe14
clocks,
(tim1, tim3),
(gpioc.pc6, gpioc.pc7),
(gpioe.pe9, gpioe.pe11),
(gpioe.pe13, gpioe.pe14),
);
let (dac0_spi, dac0_sync) = Self::setup_dac0(
clocks, spi4,
gpioe.pe2, gpioe.pe4, gpioe.pe6
);
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();
shdn0.set_low();
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();
@ -170,13 +211,14 @@ impl Pins {
tec_u_meas_pin: tec_u_meas0_pin,
};
let (dac1_spi, dac1_sync) = Self::setup_dac1(
clocks, spi5,
gpiof.pf7, gpiof.pf6, gpiof.pf9
);
let (dac1_spi, dac1_sync) = Self::setup_dac1(clocks, spi5, gpiof.pf7, gpiof.pf6, gpiof.pf9);
let mut shdn1 = gpioe.pe15.into_push_pull_output();
let _ = shdn1.set_low();
let vref1_pin = gpioa.pa3.into_analog();
shdn1.set_low();
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();
@ -191,18 +233,19 @@ impl Pins {
};
let pins = Pins {
adc_spi, adc_nss,
adc_spi,
adc_nss,
pins_adc,
pwm,
channel0,
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 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();
let eeprom_sda = gpiob.pb9.into_alternate().set_open_drain();
@ -229,8 +272,13 @@ impl Pins {
};
let fan = if hw_settings.fan_available {
Some(Timer::new(tim8, &clocks).pwm(gpioc.pc9.into_alternate(), hw_settings.fan_pwm_freq_hz.hz()))
} else { None };
Some(
Timer::new(tim8, &clocks)
.pwm(gpioc.pc9.into_alternate(), hw_settings.fan_pwm_freq_hz.hz()),
)
} else {
None
};
(pins, leds, eeprom, eth_pins, usb, fan, hwrev, hw_settings)
}
@ -242,8 +290,7 @@ impl Pins {
sck: PB10<M1>,
miso: PB14<M2>,
mosi: PB15<M3>,
) -> AdcSpi
{
) -> AdcSpi {
let sck = sck.into_alternate();
let miso = miso.into_alternate();
let mosi = mosi.into_alternate();
@ -252,13 +299,16 @@ impl Pins {
(sck, miso, mosi),
crate::ad7172::SPI_MODE,
crate::ad7172::SPI_CLOCK,
clocks
clocks,
)
}
fn setup_dac0<M1, M2, M3>(
clocks: Clocks, spi4: SPI4,
sclk: PE2<M1>, sync: PE4<M2>, sdin: PE6<M3>
clocks: Clocks,
spi4: SPI4,
sclk: PE2<M1>,
sync: PE4<M2>,
sdin: PE6<M3>,
) -> (Dac0Spi, <Channel0 as ChannelPins>::DacSync) {
let sclk = sclk.into_alternate();
let sdin = sdin.into_alternate();
@ -267,7 +317,7 @@ impl Pins {
(sclk, NoMiso {}, sdin),
crate::ad5680::SPI_MODE,
crate::ad5680::SPI_CLOCK,
clocks
clocks,
);
let sync = sync.into_push_pull_output();
@ -275,8 +325,11 @@ impl Pins {
}
fn setup_dac1<M1, M2, M3>(
clocks: Clocks, spi5: SPI5,
sclk: PF7<M1>, sync: PF6<M2>, sdin: PF9<M3>
clocks: Clocks,
spi5: SPI5,
sclk: PF7<M1>,
sync: PF6<M2>,
sdin: PF9<M3>,
) -> (Dac1Spi, <Channel1 as ChannelPins>::DacSync) {
let sclk = sclk.into_alternate();
let sdin = sdin.into_alternate();
@ -285,7 +338,7 @@ impl Pins {
(sclk, NoMiso {}, sdin),
crate::ad5680::SPI_MODE,
crate::ad5680::SPI_CLOCK,
clocks
clocks,
);
let sync = sync.into_push_pull_output();
@ -305,14 +358,10 @@ pub struct PwmPins {
impl PwmPins {
fn setup<M1, M2, M3, M4, M5, M6>(
clocks: Clocks,
tim1: TIM1,
tim3: TIM3,
max_v0: PC6<M1>,
max_v1: PC7<M2>,
max_i_pos0: PE9<M3>,
max_i_pos1: PE11<M4>,
max_i_neg0: PE13<M5>,
max_i_neg1: PE14<M6>,
(tim1, tim3): (TIM1, TIM3),
(max_v0, max_v1): (PC6<M1>, PC7<M2>),
(max_i_pos0, max_i_pos1): (PE9<M3>, PE11<M4>),
(max_i_neg0, max_i_neg1): (PE13<M5>, PE14<M6>),
) -> PwmPins {
let freq = 20u32.khz();
@ -320,10 +369,7 @@ impl PwmPins {
pin.set_duty(0);
pin.enable();
}
let channels = (
max_v0.into_alternate(),
max_v1.into_alternate(),
);
let channels = (max_v0.into_alternate(), max_v1.into_alternate());
//let (mut max_v0, mut max_v1) = pwm::tim3(tim3, channels, clocks, freq);
let (mut max_v0, mut max_v1) = Timer::new(tim3, &clocks).pwm(channels, freq);
init_pwm_pin(&mut max_v0);
@ -343,9 +389,12 @@ impl PwmPins {
init_pwm_pin(&mut max_i_neg1);
PwmPins {
max_v0, max_v1,
max_i_pos0, max_i_pos1,
max_i_neg0, max_i_neg1,
max_v0,
max_v1,
max_i_pos0,
max_i_pos1,
max_i_neg0,
max_i_neg1,
}
}
}

View File

@ -1,18 +1,33 @@
use core::mem::MaybeUninit;
use crate::command_parser::Ipv4Config;
use crate::net::split_ipv4_config;
use smoltcp::{
iface::EthernetInterface,
socket::{SocketSet, SocketHandle, TcpSocket, TcpSocketBuffer, SocketRef},
socket::{SocketHandle, SocketRef, SocketSet, TcpSocket, TcpSocketBuffer},
time::Instant,
wire::{IpAddress, IpCidr, Ipv4Address, Ipv4Cidr},
};
use crate::command_parser::Ipv4Config;
use crate::net::split_ipv4_config;
pub struct SocketState<S> {
handle: SocketHandle,
state: S,
}
impl<'a, S: Default> SocketState<S> {
fn new(
sockets: &mut SocketSet<'a>,
tcp_rx_storage: &'a mut [u8; TCP_RX_BUFFER_SIZE],
tcp_tx_storage: &'a mut [u8; TCP_TX_BUFFER_SIZE],
) -> SocketState<S> {
let tcp_rx_buffer = TcpSocketBuffer::new(&mut tcp_rx_storage[..]);
let tcp_tx_buffer = TcpSocketBuffer::new(&mut tcp_tx_storage[..]);
let tcp_socket = TcpSocket::new(tcp_rx_buffer, tcp_tx_buffer);
SocketState::<S> {
handle: sockets.add(tcp_socket),
state: S::default(),
}
}
}
/// Number of server sockets and therefore concurrent client
/// sessions. Many data structures in `Server::run()` correspond to
/// this const.
@ -35,28 +50,27 @@ impl<'a, 'b, S: Default> Server<'a, 'b, S> {
where
F: FnOnce(&mut Server<'a, '_, S>),
{
let mut sockets_storage: [_; SOCKET_COUNT] = Default::default();
let mut sockets = SocketSet::new(&mut sockets_storage[..]);
let mut states: [SocketState<S>; SOCKET_COUNT] = unsafe { MaybeUninit::uninit().assume_init() };
macro_rules! create_socket {
($set:ident, $rx_storage:ident, $tx_storage:ident, $target:expr) => {
macro_rules! create_rtx_storage {
($rx_storage:ident, $tx_storage:ident) => {
let mut $rx_storage = [0; TCP_RX_BUFFER_SIZE];
let mut $tx_storage = [0; TCP_TX_BUFFER_SIZE];
let tcp_rx_buffer = TcpSocketBuffer::new(&mut $rx_storage[..]);
let tcp_tx_buffer = TcpSocketBuffer::new(&mut $tx_storage[..]);
let tcp_socket = TcpSocket::new(tcp_rx_buffer, tcp_tx_buffer);
$target = $set.add(tcp_socket);
};
}
}
create_socket!(sockets, tcp_rx_storage0, tcp_tx_storage0, states[0].handle);
create_socket!(sockets, tcp_rx_storage1, tcp_tx_storage1, states[1].handle);
create_socket!(sockets, tcp_rx_storage2, tcp_tx_storage2, states[2].handle);
create_socket!(sockets, tcp_rx_storage3, tcp_tx_storage3, states[3].handle);
for state in &mut states {
state.state = S::default();
}
create_rtx_storage!(tcp_rx_storage0, tcp_tx_storage0);
create_rtx_storage!(tcp_rx_storage1, tcp_tx_storage1);
create_rtx_storage!(tcp_rx_storage2, tcp_tx_storage2);
create_rtx_storage!(tcp_rx_storage3, tcp_tx_storage3);
let mut sockets_storage: [_; SOCKET_COUNT] = Default::default();
let mut sockets = SocketSet::new(&mut sockets_storage[..]);
let states: [SocketState<S>; SOCKET_COUNT] = [
SocketState::<S>::new(&mut sockets, &mut tcp_rx_storage0, &mut tcp_tx_storage0),
SocketState::<S>::new(&mut sockets, &mut tcp_rx_storage1, &mut tcp_tx_storage1),
SocketState::<S>::new(&mut sockets, &mut tcp_rx_storage2, &mut tcp_tx_storage2),
SocketState::<S>::new(&mut sockets, &mut tcp_rx_storage3, &mut tcp_tx_storage3),
];
let mut server = Server {
states,
@ -89,15 +103,10 @@ impl<'a, 'b, S: Default> Server<'a, 'b, S> {
fn set_ipv4_address(&mut self, ipv4_address: Ipv4Cidr) {
self.net.update_ip_addrs(|addrs| {
for addr in addrs.iter_mut() {
match addr {
IpCidr::Ipv4(_) => {
if let IpCidr::Ipv4(_) = addr {
*addr = IpCidr::Ipv4(ipv4_address);
// done
break
}
_ => {
// skip
}
break;
}
}
});
@ -106,8 +115,7 @@ impl<'a, 'b, S: Default> Server<'a, 'b, S> {
fn set_gateway(&mut self, gateway: Option<Ipv4Address>) {
let routes = self.net.routes_mut();
match gateway {
None =>
routes.update(|routes_storage| {
None => routes.update(|routes_storage| {
routes_storage.remove(&IpCidr::new(IpAddress::v4(0, 0, 0, 0), 0));
}),
Some(gateway) => {

View File

@ -1,5 +1,4 @@
use super::command_parser::{Command, Error as ParserError};
use super::channels::CHANNELS;
const MAX_LINE_LEN: usize = 64;
@ -46,15 +45,14 @@ pub enum SessionInput {
impl From<Result<Command, ParserError>> for SessionInput {
fn from(input: Result<Command, ParserError>) -> Self {
input.map(SessionInput::Command)
input
.map(SessionInput::Command)
.unwrap_or_else(SessionInput::Error)
}
}
pub struct Session {
reader: LineReader,
reporting: bool,
report_pending: [bool; CHANNELS],
}
impl Default for Session {
@ -67,43 +65,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) {
@ -111,19 +77,10 @@ impl Session {
for (i, b) in buf.iter().enumerate() {
buf_bytes = i + 1;
let line = self.reader.feed(*b);
match line {
Some(line) => {
let command = Command::parse(&line);
match command {
Ok(Command::Reporting(reporting)) => {
self.reporting = reporting;
}
_ => {}
}
if let Some(line) = line {
let command = Command::parse(line);
return (buf_bytes, command.into());
}
None => {}
}
}
(buf_bytes, SessionInput::Nothing)
}

View File

@ -4,9 +4,9 @@ use cortex_m::interrupt::Mutex;
use cortex_m_rt::exception;
use stm32f4xx_hal::{
rcc::Clocks,
time::U32Ext,
timer::{Timer, Event as TimerEvent},
stm32::SYST,
time::U32Ext,
timer::{Event as TimerEvent, Timer},
};
/// Rate in Hz
@ -18,7 +18,6 @@ static TIMER_MS: Mutex<RefCell<u32>> = Mutex::new(RefCell::new(0));
/// Setup SysTick exception
pub fn setup(syst: SYST, clocks: Clocks) {
let timer = Timer::syst(syst, &clocks);
let mut countdown = timer.start_count_down(TIMER_RATE.hz());
countdown.listen(TimerEvent::TimeOut);
@ -28,18 +27,13 @@ pub fn setup(syst: SYST, clocks: Clocks) {
#[exception]
fn SysTick() {
cortex_m::interrupt::free(|cs| {
*TIMER_MS.borrow(cs)
.borrow_mut() += TIMER_DELTA;
*TIMER_MS.borrow(cs).borrow_mut() += TIMER_DELTA;
});
}
/// Obtain current time in milliseconds
pub fn now() -> u32 {
cortex_m::interrupt::free(|cs| {
*TIMER_MS.borrow(cs)
.borrow()
.deref()
})
cortex_m::interrupt::free(|cs| *TIMER_MS.borrow(cs).borrow().deref())
}
/// block for at least `amount` milliseconds

View File

@ -1,15 +1,18 @@
use core::{fmt::{self, Write}, mem::MaybeUninit};
use core::{
fmt::{self, Write},
mem::MaybeUninit,
};
use cortex_m::interrupt::free;
use log::{Log, Metadata, Record};
use stm32f4xx_hal::{
otg_fs::{USB, UsbBus as Bus},
otg_fs::{UsbBus as Bus, USB},
stm32::{interrupt, Interrupt, NVIC},
};
use usb_device::{
class_prelude::{UsbBusAllocator},
class_prelude::UsbBusAllocator,
prelude::{UsbDevice, UsbDeviceBuilder, UsbVidPid},
};
use usbd_serial::SerialPort;
use log::{Record, Log, Metadata};
static mut EP_MEMORY: [u32; 1024] = [0; 1024];
@ -36,8 +39,8 @@ impl State {
.device_class(usbd_serial::USB_CLASS_CDC)
.build();
free(|_| {
unsafe { STATE = Some(State { serial, dev }); }
free(|_| unsafe {
STATE = Some(State { serial, dev });
});
unsafe {
@ -94,8 +97,7 @@ impl Write for SerialOutput {
fn write_str(&mut self, s: &str) -> core::result::Result<(), core::fmt::Error> {
if let Some(ref mut state) = State::get() {
for chunk in s.as_bytes().chunks(16) {
free(|_| state.serial.write(chunk))
.map_err(|_| fmt::Error)?;
free(|_| state.serial.write(chunk)).map_err(|_| fmt::Error)?;
}
}
Ok(())