Compare commits

..

146 Commits

Author SHA1 Message Date
ed9d067bee QVariantList -> list 2024-10-07 16:35:20 +08:00
1035978ce4 No need to create a new task for waiting 2024-10-07 13:27:54 +08:00
1481819233 super init's first 2024-10-07 13:27:54 +08:00
75f05aa252 Format 2024-10-07 13:27:54 +08:00
0833813909 ch_tuning -> autotuning_channels 2024-10-07 13:27:54 +08:00
6b30c33c1b Module docstring?? 2024-10-07 13:27:54 +08:00
56200902f3 Reorder imports 2024-10-07 13:27:54 +08:00
fae9122ad5 Sensible names for views 2024-10-07 13:27:54 +08:00
28a76d091d Unprivate conn_menu
Used for autoconnect
2024-10-07 13:27:54 +08:00
9c0c6ab323 Order? 2024-10-07 13:27:54 +08:00
242b516acc Reorder 2024-10-07 13:27:54 +08:00
524b6d9bce Not async 2024-10-07 13:27:54 +08:00
a84ebdd6ad Remove report mode from thermostat data model 2024-10-07 13:27:54 +08:00
7cf5bcb400 Remove report mode box 2024-10-07 13:27:54 +08:00
2b33d1f75a More sensible names 2024-10-07 13:27:54 +08:00
938d92ce02 tec_qt: Private everything possible 2024-10-07 13:27:54 +08:00
1099f6d9ec Remove error handling for connecting task
Just let the exception propagate, even when stopping the connection
2024-10-07 13:27:54 +08:00
97d26e3c65 Update thermostat state from controller code 2024-10-07 13:27:54 +08:00
60672d9590 extra func out 2024-10-07 13:27:54 +08:00
7f62c6ae4f No need extra function 2024-10-07 13:27:54 +08:00
f5cab6b825 State update explicitly 2024-10-07 13:27:54 +08:00
8f088e43cf Zero limits warning cleanup 2024-10-07 13:27:54 +08:00
25990d19a9 Add NUM_CHANNELS to Thermostat! 2024-10-07 13:27:54 +08:00
db7cdca24c Real concurrently 2024-10-07 13:27:54 +08:00
969a7be0cc Don't create report task if not polling for it 2024-10-07 13:27:54 +08:00
f45d2c5c9e PIDAutotuner: Don't use separate sig for interval 2024-10-07 13:27:54 +08:00
5c8270834a aioexample: Show polling while report mode on
WIP: handle KeyboardInterrupt
2024-10-07 13:27:54 +08:00
52550cf721 aioclient: More accurate DFU docstring 2024-10-07 13:27:54 +08:00
d1692ad0e6 No need for async as well 2024-10-07 13:27:54 +08:00
940603e7cf No need for async 2024-10-07 13:27:54 +08:00
b873b03aaf Remove extra imports 2024-10-07 13:27:54 +08:00
cd38a1c303 report group 2024-10-07 13:27:54 +08:00
9787dc1e8b end_session in thermostat itself afterall 2024-10-07 13:27:54 +08:00
02aab70e82 Reorder MainWindow 2024-10-07 13:27:54 +08:00
3c9b6a40d9 Rearrange MainWindow.__init__ 2024-10-07 13:27:54 +08:00
c549d0344e Thermostat: Add disconnect callback
For communicating with the autotuner before the client fully
disconnects

Also then there's no need for explicitly resetting autotune elements
2024-10-07 13:27:54 +08:00
15880290b0 Remove info_box_trigger 2024-10-07 13:27:54 +08:00
bb9a363b31 Remove connection errored 2024-10-07 13:27:54 +08:00
b3293cd431 Simply test for connectivity for turning PID off 2024-10-07 13:27:54 +08:00
d71c1f4e7e Fix pressing enter not working in conn menu 2024-10-07 13:27:54 +08:00
b5b8a374c0 Assign connection_errored as well 2024-10-07 13:27:54 +08:00
46831917ba Back out 2024-10-07 13:27:54 +08:00
6e357c14e2 Put UI changes into conn_menu 2024-10-07 13:27:54 +08:00
342f7c6655 ConnectionBtn + StatusLbl 2024-10-07 13:27:54 +08:00
f75348c69d Clear Graph UI changes 2024-10-07 13:27:54 +08:00
b98773784e Concentrate ThermostatCtrlMenu UI changes 2024-10-07 13:27:54 +08:00
b26747f527 Descriptive name 2024-10-07 13:27:54 +08:00
c81f09c9d8 Refactor repeated stuff 2024-10-07 13:27:54 +08:00
de6c16e380 Split PID handler UI up 2024-10-07 13:27:54 +08:00
d6f86c3435 Fix hwrev getting 2024-10-07 13:27:54 +08:00
6e9ded532b hwrev updates 2024-10-07 13:27:54 +08:00
d5e2abfac7 Fix info boxes for load/saving from flash 2024-10-07 13:27:54 +08:00
93d09e9467 Move reset request to thermostat control menu
We don't get auto reconnect anymore
2024-10-07 13:27:54 +08:00
1eae8029ad Move pid autotuning request to CtrlPanel
And update autotune UI only on state change instead of every single
report update
2024-10-07 13:27:54 +08:00
bb2ca2c7f8 Move channelGraph stuff inside LiveDataPlotter 2024-10-07 13:27:54 +08:00
0cf685a3a1 Move plot_options_menu stuff into menu 2024-10-07 13:27:54 +08:00
6ab41a1943 Move autotune ticking connect 2024-10-07 13:27:54 +08:00
b34c70742d Get thermostat_ctrl_menu to subscribe to hwrev updates 2024-10-07 13:27:54 +08:00
019fa31d44 Save/load info box content 2024-10-07 13:27:54 +08:00
c2fbc7029c Fix loading all channel settings would bring up 2 info boxes 2024-10-07 13:27:54 +08:00
99e1574886 Add back asyncSlot to save_cfg_request 2024-10-07 13:27:54 +08:00
1829d72536 hw_rev 2024-10-07 13:27:54 +08:00
b5a011aa0c fan_update: tec_qt to thermostat_ctrl_menu 2024-10-07 13:27:54 +08:00
c8b3bc9c0f Fix updating status label for hwrev = None 2024-10-07 13:27:54 +08:00
76a832c8ba Put some menu requests in menu itself 2024-10-07 13:27:54 +08:00
cb6c807b90 Broadcast ConnectionState changes from Thermostat 2024-10-07 13:27:54 +08:00
d3df467017 Move sigActivatedHandle to CtrlPanel 2024-10-07 13:27:54 +08:00
bd6adf9526 Get rid of timeout on readline 2024-10-07 13:27:54 +08:00
0786fa0158 Put send_command in CtrlPanel 2024-10-07 13:27:48 +08:00
547700ac51 Extra bail removed 2024-10-07 13:24:02 +08:00
a76268a81f return hwrev when start_session 2024-10-07 13:24:02 +08:00
7668bbf57e state str 2024-10-07 13:24:02 +08:00
f93e76eaa0 params update concurrently 2024-10-07 13:24:01 +08:00
067ab925dd Stuff non-UI changes in Thermostat model 2024-10-07 13:24:01 +08:00
5bef8883e0 Base Thermostat connectivity UI changes to state 2024-10-07 13:24:01 +08:00
a19c64ce98 Remove wait_for
OSError raised anyways
2024-10-07 13:24:01 +08:00
0107ed0acc Actually its OSError 2024-10-07 13:24:01 +08:00
3a1c7792c9 {start,end}_session -> [dis]connect 2024-10-07 13:24:01 +08:00
22de1b623f Simplify on_connect_btn_clicked
Raise if OSError
2024-10-07 13:24:01 +08:00
e8387acbc9 Connecting task moved? 2024-10-07 13:24:01 +08:00
7abcc63a90 AsyncIO version Client -> AsyncioClient 2024-10-07 13:24:01 +08:00
c4d31a78b1 Exclusively use the Thermostat object as a medium
All calls to the Thermostat should be forwarded by the medium.
2024-10-07 13:23:56 +08:00
047bde887e Integrate WrappedClient into Thermostat model 2024-10-07 13:22:55 +08:00
c83e6dc388 Should not stop cancelling read if timeout'd 2024-10-07 13:22:55 +08:00
d4f46b994b Fix Autotuner state for forceful disconnect 2024-10-07 13:22:55 +08:00
f61c09596e _ 2024-10-07 13:22:55 +08:00
b587a72345 Make connection loss handling more elegant
Show an info box on connection lost informing the user that the
Thermostat was forcefully disconnected.
2024-10-07 13:22:55 +08:00
ddd4ea9958 ================gui_dev-fix_asyncio=============== 2024-10-07 13:22:55 +08:00
88c3c6f815 Use new style super() 2024-10-07 13:22:55 +08:00
71d1c7390a Move command line host:port setting handling
To main
2024-10-07 13:22:55 +08:00
1256b5ff49 aioclient: Add missing readline for saving
Saving all channels returns multiple JSON objects, read the extra {}.
2024-10-07 13:22:55 +08:00
e59f8d05e0 import order rearrange 2024-10-07 13:22:55 +08:00
7f45437492 This is bail 2024-10-07 13:22:55 +08:00
dcf628b542 thermostat: Properly register task
Also Thermostat.task -> Thermostat._update_params_task
2024-10-07 13:22:55 +08:00
958fddf953 ip -> host 2024-10-07 13:22:55 +08:00
1db3a3ccb9 Lazy evaluating for debug string command 2024-10-07 13:22:55 +08:00
05d46030b4 Add pytec runnables 2024-10-07 13:22:55 +08:00
73c29338af PYTHON shell 2024-10-07 13:22:55 +08:00
d3e878e294 Exactlier wording 2024-10-07 13:22:55 +08:00
38eb1c886d unused 2024-10-07 13:22:55 +08:00
1d4bc5c53f encoding 2024-10-07 13:22:55 +08:00
1ec541d580 Just catch asyncio.TimeoutError
Will just change to TimeoutError once we switch to Python 3.11 in the
flake.
2024-10-07 13:22:55 +08:00
eb8944e5ac Remove exception too general 2024-10-07 13:22:55 +08:00
abe08e4be6 Use asserts to check for connectivity 2024-10-07 13:22:55 +08:00
0024ebae5f Add back the parent 2024-10-07 13:22:55 +08:00
3f5ae9e333 Fix method call 2024-10-07 13:22:55 +08:00
dd850d34c2 README: Proofread 2024-10-07 13:22:55 +08:00
f77f5399cf Swap order arounda bit more 2024-10-07 13:22:55 +08:00
f632349c62 Formatting 2024-10-07 13:22:55 +08:00
5bf33c01fe Use qtextras 2024-10-07 13:22:55 +08:00
5119c68c9a flake update 2024-10-07 13:22:55 +08:00
a0c8fb9285 flake.nix: nixfmt-rfc-style 2024-10-07 13:22:55 +08:00
19c3c7a8f2 Merge pull request 'GUI: Refactor send_command' (#4) from gui_dev-refactor-send_command into gui_dev
Reviewed-on: linuswck/thermostat#4
2024-10-07 13:03:38 +08:00
41abad7aa3 send_command: Remove "activater"
Interpret commands anomalies directly in send_command instead
2024-08-14 16:07:15 +08:00
5c8d9c7cce send_command: Simplify "pid_autotune" parameters 2024-08-14 16:07:15 +08:00
278898fad2 send_command: Switch to thermostat:set_param
Use a `dict` to map values to thermostat parameters, which correspond to
the `set_param` parameters in the pytec client. New tag
"thermostat:set_param" used in JSON.
2024-08-14 16:07:14 +08:00
dd83daa5d9 send_command: Remove indirect path to parameter
The child at inner_param's childpath to the root parameter... is just
inner_param itself.
2024-08-14 16:06:31 +08:00
d57cc9ef2a send_command: Alias data as new_value 2024-08-14 16:06:24 +08:00
be77a6f205 send_command: Use in syntax 2024-08-14 16:06:17 +08:00
b768d61e39 Merge pull request 'GUI: Text changes' (#3) from gui_dev-str_changes into gui_dev
Reviewed-on: linuswck/thermostat#3
2024-07-19 15:36:01 +08:00
d244ba392a Fix typos 2024-07-19 15:34:31 +08:00
93d6df5e92 Merge pull request 'GUI: Some repo organisation' (#1) from gui_dev-repo_org into gui_dev
Reviewed-on: linuswck/thermostat#1
2024-07-12 10:34:23 +08:00
44bea87f03 Thermostat.disconnect -> Thermostat.end_session
QObject already has a disconnect method, avoid overriding it.
2024-07-10 15:56:43 +08:00
e6f62e9e19 flake: sha256 -> hash 2024-07-10 15:56:43 +08:00
271fe449ba Remove duplicated show call
MainWindow.show() already called in coro_main
2024-07-10 15:56:43 +08:00
70db0a39eb Remove duplicated antialias config option
Already set in live_plot_view.py
2024-07-10 15:56:43 +08:00
26c7382b1e Move GUI components and examples into folder
For better organisation
2024-07-10 15:56:43 +08:00
c415d9de8a Use MANIFEST.in
Allows for more accurate control over included files in pytec package
2024-07-10 15:56:43 +08:00
7069111e21 Expose frontend scripts exclusively in pytec 2024-07-10 15:56:43 +08:00
1707728c3c thermostat_data_model.py -> thermostat.py 2024-07-10 15:56:34 +08:00
a16d2e9a9e Follow CapWords convention for class names
Re: PEP8
2024-07-10 15:45:03 +08:00
bc4ac43e0b Put comments in right place 2024-07-10 13:07:31 +08:00
9acff86547 Restructure GUI Code, Improve and Fix Bugs
- Bugs fix:
1. Params Tree user input will not get overwritten
    by incoming report thermostat_data_model.
2. PID Autotune Sampling Period is now set according to Thermostat sampling interval
3. PID Autotune won't get stuck in Fail State
4. Various types disconnection related Bugs
5. Number of Samples stored in the plot cannot be set
6. Limit the max settable output current to be 2000mA

- Improvement:
1. Params Tree settings can be changed with external json
2. Use a Tab system to show a single channel of config instead of two
3. Expose PID Autotune lookback params
4. Icon is changed to Artiq logo

- Restructure:
1. Restructure the code to follow Model-View-Delegate Design Pattern
2024-06-06 17:34:15 +08:00
8753f4a0fc Finish GUI 2024-05-08 14:49:03 +08:00
9a83d6850d Remove unused as clause 2024-05-08 14:49:03 +08:00
772863f4b2 Add paramtree view, without updates
Signed-off-by: Egor Savkin <es@m-labs.hk>

Fix signal blocker argument -atse
2024-05-08 14:49:03 +08:00
44ef2c04e3 Fix bugs, grammar, text, and refactor into class 2024-05-08 14:49:03 +08:00
623011fabb Change title 2024-05-08 14:49:03 +08:00
400f3a98e8 Stop polling drift
Just waiting for the update_s doesn't take into account the time to
execute update_params, and causes time drift.
2024-05-08 14:49:03 +08:00
7dd5d15047 Remove unused 'as' clause 2024-05-08 14:49:03 +08:00
297e589c30 Update docs 2024-05-08 14:49:03 +08:00
65e1f4a146 Finish moving over to qasync
Also:

* Add aioclient

The old client is synchronous and blocking, and the only way to achieve
true asynchronous IO is to create a new client that interfaces with
asyncio.

* Finish Nix Flake description and make the GUI available for `nix run`
2024-05-08 14:49:03 +08:00
e0ce14c616 Try move from Qthreads to qasync
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-05-08 14:49:03 +08:00
a79679a074 Create client watcher, that would poll Thermostat for config
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-05-08 14:49:03 +08:00
b9acba69d3 Create basic GUI, that would connect and control thermostat's fan
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-05-08 14:49:03 +08:00
b4b4ec987c add autotune 2024-05-08 14:49:03 +08:00
43 changed files with 1634 additions and 1683 deletions

4
.gitignore vendored
View File

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

117
README.md
View File

@ -23,7 +23,7 @@ cargo build --release
The resulting ELF file will be located under `target/thumbv7em-none-eabihf/release/thermostat`. The resulting ELF file will be located under `target/thumbv7em-none-eabihf/release/thermostat`.
Alternatively, you can install the Rust toolchain without Nix using rustup; see the `rust` variable in `flake.nix` to determine which Rust version to use. Alternatively, you can install the Rust toolchain without Nix using rustup; see the Rust manifest file pulled in `flake.nix` to determine which Rust version to use.
## Debugging ## Debugging
@ -45,7 +45,7 @@ There are several options for flashing Thermostat. DFU requires only a micro-USB
### dfu-util on Linux ### dfu-util on Linux
* Install the DFU USB tool (dfu-util). * Install the DFU USB tool (dfu-util).
* 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) * 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)
* Connect to the Micro USB connector to Thermostat below the RJ45. * 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. * Add jumper to Thermostat v2.0 across 2-pin jumper adjacent to JTAG connector.
* Cycle board power to put it in DFU update mode * Cycle board power to put it in DFU update mode
@ -95,7 +95,9 @@ invalidate the first line of input.
### Reading ADC input ### Reading ADC input
ADC input data is provided in reports. Query for the latest report with the command `report`. See the *Reports* section below. Set report mode to `on` for a continuous stream of input data.
The scope of this setting is per TCP session.
### TCP commands ### TCP commands
@ -103,41 +105,42 @@ ADC input data is provided in reports. Query for the latest report with the comm
Send commands as simple text string terminated by `\n`. Responses are Send commands as simple text string terminated by `\n`. Responses are
formatted as line-delimited JSON. formatted as line-delimited JSON.
| Syntax | Function | | Syntax | Function |
|-------------------------------------------|-------------------------------------------------------------------------------| |----------------------------------|-------------------------------------------------------------------------------|
| `report` | Show latest report of channel parameters (see *Reports* section) | | `report` | Show current input |
| `output` | Show current output settings | | `report mode` | Show current report mode |
| `output <0/1> max_i_pos <amp>` | Set maximum positive output current, clamped to [0, 2] | | `report mode <off/on>` | Set report mode |
| `output <0/1> max_i_neg <amp>` | Set maximum negative output current, clamped to [0, 2] | | `pwm` | Show current PWM settings |
| `output <0/1> max_v <volt>` | Set maximum output voltage, clamped to [0, 4] | | `pwm <0/1> max_i_pos <amp>` | Set maximum positive output current |
| `output <0/1> i_set <amp>` | Disengage PID, set fixed output current, clamped to [-2, 2] | | `pwm <0/1> max_i_neg <amp>` | Set maximum negative output current |
| `output <0/1> polarity <normal/reversed>` | Set output current polarity, with 'normal' being the front panel polarity | | `pwm <0/1> max_v <volt>` | Set maximum output voltage |
| `output <0/1> pid` | Let output current to be controlled by the PID | | `pwm <0/1> i_set <amp>` | Disengage PID, set fixed output current |
| `center <0/1> <volt>` | Set the MAX1968 0A-centerpoint to the specified fixed voltage | | `pwm <0/1> pid` | Let output current to be controlled by the PID |
| `center <0/1> vref` | Set the MAX1968 0A-centerpoint to measure from VREF | | `center <0/1> <volt>` | Set the MAX1968 0A-centerpoint to the specified fixed voltage |
| `pid` | Show PID configuration | | `center <0/1> vref` | Set the MAX1968 0A-centerpoint to measure from VREF |
| `pid <0/1> target <deg_celsius>` | Set the PID controller target temperature | | `pid` | Show PID configuration |
| `pid <0/1> kp <value>` | Set proportional gain | | `pid <0/1> target <deg_celsius>` | Set the PID controller target temperature |
| `pid <0/1> ki <value>` | Set integral gain | | `pid <0/1> kp <value>` | Set proportional gain |
| `pid <0/1> kd <value>` | Set differential gain | | `pid <0/1> ki <value>` | Set integral gain |
| `pid <0/1> output_min <amp>` | Set mininum output | | `pid <0/1> kd <value>` | Set differential gain |
| `pid <0/1> output_max <amp>` | Set maximum output | | `pid <0/1> output_min <amp>` | Set mininum output |
| `b-p` | Show B-Parameter equation parameters | | `pid <0/1> output_max <amp>` | Set maximum output |
| `b-p <0/1> <t0/b/r0> <value>` | Set B-Parameter for a channel | | `s-h` | Show Steinhart-Hart equation parameters |
| `postfilter` | Show postfilter settings | | `s-h <0/1> <t0/b/r0> <value>` | Set Steinhart-Hart parameter for a channel |
| `postfilter <0/1> off` | Disable postfilter | | `postfilter` | Show postfilter settings |
| `postfilter <0/1> rate <rate>` | Set postfilter output data rate | | `postfilter <0/1> off` | Disable postfilter |
| `load [0/1]` | Restore configuration for channel all/0/1 from flash | | `postfilter <0/1> rate <rate>` | Set postfilter output data rate |
| `save [0/1]` | Save configuration for channel all/0/1 to flash | | `load [0/1]` | Restore configuration for channel all/0/1 from flash |
| `reset` | Reset the device | | `save [0/1]` | Save configuration for channel all/0/1 to flash |
| `dfu` | Reset device and enters USB device firmware update (DFU) mode | | `reset` | Reset the device |
| `ipv4 <X.X.X.X/L> [Y.Y.Y.Y]` | Configure IPv4 address, netmask length, and optional default gateway | | `dfu` | Reset device and enters USB device firmware update (DFU) mode |
| `fan` | Show current fan settings and sensors' measurements | | `ipv4 <X.X.X.X/L> [Y.Y.Y.Y]` | Configure IPv4 address, netmask length, and optional default gateway |
| `fan <value>` | Set fan power with values from 1 to 100 | | `fan` | Show current fan settings and sensors' measurements |
| `fan auto` | Enable automatic fan speed control | | `fan <value>` | Set fan power with values from 1 to 100 |
| `fcurve <a> <b> <c>` | Set fan controller curve coefficients (see *Fan control* section) | | `fan auto` | Enable automatic fan speed control |
| `fcurve default` | Set fan controller curve coefficients to defaults (see *Fan control* section) | | `fcurve <a> <b> <c>` | Set fan controller curve coefficients (see *Fan control* section) |
| `hwrev` | Show hardware revision, and settings related to it | | `fcurve default` | Set fan controller curve coefficients to defaults (see *Fan control* section) |
| `hwrev` | Show hardware revision, and settings related to it |
## USB ## USB
@ -155,22 +158,22 @@ output will be truncated when USB buffers are full.
Connect the thermistor with the SENS pins of the Connect the thermistor with the SENS pins of the
device. Temperature-depending resistance is measured by the AD7172 device. Temperature-depending resistance is measured by the AD7172
ADC. To prepare conversion to a temperature, set the parameters ADC. To prepare conversion to a temperature, set the Beta parameters
for the B-Parameter equation. for the Steinhart-Hart equation.
Set the base temperature in degrees celsius for the channel 0 thermistor: Set the base temperature in degrees celsius for the channel 0 thermistor:
``` ```
b-p 0 t0 20 s-h 0 t0 20
``` ```
Set the resistance in Ohms measured at the base temperature t0: Set the resistance in Ohms measured at the base temperature t0:
``` ```
b-p 0 r0 10000 s-h 0 r0 10000
``` ```
Set the Beta parameter: Set the Beta parameter:
``` ```
b-p 0 b 3800 s-h 0 b 3800
``` ```
### 50/60 Hz filtering ### 50/60 Hz filtering
@ -194,16 +197,14 @@ postfilter rate can be tuned with the `postfilter` command.
When using a TEC module with the Thermostat, the Thermostat expects the thermal load (where the thermistor is located) to cool down with a positive software current set point, and heat up with a negative current set point. When using a TEC module with the Thermostat, the Thermostat expects the thermal load (where the thermistor is located) to cool down with a positive software current set point, and heat up with a negative current set point.
If the Thermostat is used for temperature control with the Sinara 5432 DAC "Zotino", and is connected via an IDC cable, the TEC polarity may need to be reversed with the `output <ch> polarity reversed` TCP command.
Testing heat flow direction with a low set current is recommended before installation of the TEC module. Testing heat flow direction with a low set current is recommended before installation of the TEC module.
### Limits ### Limits
Each MAX1968 TEC driver has analog/PWM inputs for setting Each of the MAX1968 TEC driver has analog/PWM inputs for setting
output limits. output limits.
Use the `output` command to see current settings and maximum values. Use the `pwm` command to see current settings and maximum values.
| Limit | Unit | Description | | Limit | Unit | Description |
| --- | :---: | --- | | --- | :---: | --- |
@ -214,28 +215,28 @@ Use the `output` command to see current settings and maximum values.
Example: set the maximum voltage of channel 0 to 1.5 V. Example: set the maximum voltage of channel 0 to 1.5 V.
``` ```
output 0 max_v 1.5 pwm 0 max_v 1.5
``` ```
Example: set the maximum negative current of channel 0 to -3 A. Example: set the maximum negative current of channel 0 to -3 A.
``` ```
output 0 max_i_neg 3 pwm 0 max_i_neg 3
``` ```
Example: set the maximum positive current of channel 1 to 3 A. Example: set the maximum positive current of channel 1 to 3 A.
``` ```
output 0 max_i_pos 3 pwm 0 max_i_pos 3
``` ```
### Open-loop mode ### Open-loop mode
To manually control TEC output current, set a fixed output current with To manually control TEC output current, omit the limit parameter of
the `output` command. Doing so will disengage the PID control for that the `pwm` command. Doing so will disengage the PID control for that
channel. channel.
Example: set output current of channel 0 to 0 A. Example: set output current of channel 0 to 0 A.
``` ```
output 0 i_set 0 pwm 0 i_set 0
``` ```
## PID-stabilized temperature control ## PID-stabilized temperature control
@ -248,7 +249,7 @@ pid 0 target 20
Enter closed-loop mode by switching control of the TEC output current Enter closed-loop mode by switching control of the TEC output current
of channel 0 to the PID algorithm: of channel 0 to the PID algorithm:
``` ```
output 0 pid pwm 0 pid
``` ```
## LED indicators ## LED indicators
@ -261,17 +262,17 @@ output 0 pid
## Reports ## Reports
Use the bare `report` command to obtain a single report. Reports are JSON objects Use the bare `report` command to obtain a single report. Enable
continuous reporting with `report mode on`. Reports are JSON objects
with the following keys. with the following keys.
| Key | Unit | Description | | Key | Unit | Description |
| --- | :---: | --- | | --- | :---: | --- |
| `channel` | Integer | Channel `0`, or `1` | | `channel` | Integer | Channel `0`, or `1` |
| `time` | Seconds | Temperature measurement time | | `time` | Seconds | Temperature measurement time |
| `interval` | Seconds | Time elapsed since last report update on channel |
| `adc` | Volts | AD7172 input | | `adc` | Volts | AD7172 input |
| `sens` | Ohms | Thermistor resistance derived from `adc` | | `sens` | Ohms | Thermistor resistance derived from `adc` |
| `temperature` | Degrees Celsius | B-Parameter conversion result derived from `sens` | | `temperature` | Degrees Celsius | Steinhart-Hart conversion result derived from `sens` |
| `pid_engaged` | Boolean | `true` if in closed-loop mode | | `pid_engaged` | Boolean | `true` if in closed-loop mode |
| `i_set` | Amperes | TEC output current | | `i_set` | Amperes | TEC output current |
| `dac_value` | Volts | AD5680 output derived from `i_set` | | `dac_value` | Volts | AD5680 output derived from `i_set` |
@ -281,7 +282,7 @@ with the following keys.
| `tec_u_meas` | Volts | Measurement of the voltage across the TEC | | `tec_u_meas` | Volts | Measurement of the voltage across the TEC |
| `pid_output` | Amperes | PID control output | | `pid_output` | Amperes | PID control output |
Note: Prior to Thermostat hardware revision v2.2.4, the voltage and current readouts `i_tec` and `tec_i` are noisy without the hardware fix shown in [this PR](https://git.m-labs.hk/M-Labs/thermostat/pulls/105). Note: With Thermostat v2 and below, the voltage and current readouts `i_tec` and `tec_i` are noisy without the hardware fix shown in [this PR][https://git.m-labs.hk/M-Labs/thermostat/pulls/105].
## PID Tuning ## PID Tuning

48
flake.lock generated
View File

@ -1,45 +1,41 @@
{ {
"nodes": { "nodes": {
"mozilla-overlay": {
"flake": false,
"locked": {
"lastModified": 1704373101,
"narHash": "sha256-+gi59LRWRQmwROrmE1E2b3mtocwueCQqZ60CwLG+gbg=",
"owner": "mozilla",
"repo": "nixpkgs-mozilla",
"rev": "9b11a87c0cc54e308fa83aac5b4ee1816d5418a2",
"type": "github"
},
"original": {
"owner": "mozilla",
"repo": "nixpkgs-mozilla",
"type": "github"
}
},
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1722791413, "lastModified": 1704290814,
"narHash": "sha256-rCTrlCWvHzMCNcKxPE3Z/mMK2gDZ+BvvpEVyRM4tKmU=", "narHash": "sha256-LWvKHp7kGxk/GEtlrGYV68qIvPHkU9iToomNFGagixU=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "8b5b6723aca5a51edf075936439d9cd3947b7b2c", "rev": "70bdadeb94ffc8806c0570eb5c2695ad29f0e421",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "NixOS",
"ref": "nixos-24.05", "ref": "nixos-23.05",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }
}, },
"root": { "root": {
"inputs": { "inputs": {
"nixpkgs": "nixpkgs", "mozilla-overlay": "mozilla-overlay",
"rust-overlay": "rust-overlay" "nixpkgs": "nixpkgs"
}
},
"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"
} }
} }
}, },

137
flake.nix
View File

@ -1,34 +1,47 @@
{ {
description = "Firmware for the Sinara 8451 Thermostat"; description = "Firmware for the Sinara 8451 Thermostat";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05"; inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.05";
inputs.rust-overlay = { inputs.mozilla-overlay = {
url = "github:oxalica/rust-overlay"; url = "github:mozilla/nixpkgs-mozilla";
inputs.nixpkgs.follows = "nixpkgs"; flake = false;
}; };
outputs = { self, nixpkgs, rust-overlay }: outputs = { self, nixpkgs, mozilla-overlay, }:
let let
pkgs = import nixpkgs { system = "x86_64-linux"; overlays = [ (import rust-overlay) ]; }; pkgs = import nixpkgs {
system = "x86_64-linux";
rust = pkgs.rust-bin.stable."1.66.0".default.override { overlays = [ (import mozilla-overlay) ];
extensions = [ "rust-src" ];
targets = [ "thumbv7em-none-eabihf" ];
}; };
rustPlatform = pkgs.makeRustPlatform { rustManifest = pkgs.fetchurl {
url =
"https://static.rust-lang.org/dist/2022-12-15/channel-rust-stable.toml";
hash = "sha256-S7epLlflwt0d1GZP44u5Xosgf6dRrmr8xxC+Ml2Pq7c=";
};
targets = [ "thumbv7em-none-eabihf" ];
rustChannelOfTargets = _channel: _date: targets:
(pkgs.lib.rustLib.fromManifestFile rustManifest {
inherit (pkgs) stdenv lib fetchurl patchelf;
}).rust.override {
inherit targets;
extensions = [ "rust-src" ];
};
rust = rustChannelOfTargets "stable" null targets;
rustPlatform = pkgs.recurseIntoAttrs (pkgs.makeRustPlatform {
rustc = rust; rustc = rust;
cargo = rust; cargo = rust;
}; });
thermostat = rustPlatform.buildRustPackage { thermostat = rustPlatform.buildRustPackage {
name = "thermostat"; name = "thermostat";
version = "0.0.0"; version = "0.0.0";
src = self; src = self;
cargoLock = { cargoLock = {
lockFile = ./Cargo.lock; lockFile = ./Cargo.lock;
outputHashes = { outputHashes = {
"stm32-eth-0.2.0" = "sha256-48RpZgagUqgVeKm7GXdk3Oo0v19ScF9Uby0nTFlve2o="; "stm32-eth-0.2.0" =
"sha256-48RpZgagUqgVeKm7GXdk3Oo0v19ScF9Uby0nTFlve2o=";
}; };
}; };
@ -47,7 +60,45 @@
''; '';
dontFixup = true; dontFixup = true;
auditable = false; };
qasync = pkgs.python3Packages.buildPythonPackage rec {
pname = "qasync";
version = "0.27.1";
format = "pyproject";
src = pkgs.fetchPypi {
inherit pname version;
hash = "sha256-jcdo/R7l3hBEx8MF7M8tOdJNh4A+pxGJ1AJPtHX0mF8=";
};
buildInputs = [ pkgs.python3Packages.poetry-core ];
propagatedBuildInputs = [ pkgs.python3Packages.pyqt6 ];
};
pyqtgraph = pkgs.python3Packages.buildPythonPackage rec {
pname = "pyqtgraph";
version = "0.13.3";
format = "pyproject";
src = pkgs.fetchPypi {
inherit pname version;
hash = "sha256-WBCNhBHHBU4IQdi3ke6F4QH8KWubNZwOAd3jipj/Ks4=";
};
propagatedBuildInputs = with pkgs.python3Packages; [ numpy pyqt6 ];
};
qtextras = pkgs.python3Packages.buildPythonPackage rec {
pname = "qtextras";
version = "0.6.8";
format = "pyproject";
src = pkgs.fetchPypi {
inherit pname version;
hash = "sha256-d1ZotSlOI4surUy0H0N4xHoq94IRQvMHunwRH1uubFg=";
};
buildInputs = [ pkgs.python3Packages.hatchling ];
propagatedBuildInputs = with pkgs.python3Packages; [
numpy
pyqtgraph
ruamel-yaml
];
}; };
pglive = pkgs.python3Packages.buildPythonPackage rec { pglive = pkgs.python3Packages.buildPythonPackage rec {
@ -59,12 +110,22 @@
hash = "sha256-jqj8X6H1N5mJQ4OrY5ANqRB0YJByqg/bNneEALWmH1A="; hash = "sha256-jqj8X6H1N5mJQ4OrY5ANqRB0YJByqg/bNneEALWmH1A=";
}; };
buildInputs = [ pkgs.python3Packages.poetry-core ]; buildInputs = [ pkgs.python3Packages.poetry-core ];
propagatedBuildInputs = with pkgs.python3Packages; [ propagatedBuildInputs = [ pyqtgraph pkgs.python3Packages.numpy ];
pyqtgraph
numpy
];
}; };
pytec-dev-wrappers = pkgs.runCommandNoCC "pytec-dev-wrappers" { } ''
mkdir -p $out/bin
for program in ${self}/pytec/*.py; do
if [ -x $program ]; then
progname=`basename -s .py $program`
outname=$out/bin/$progname
echo "#!${pkgs.bash}/bin/bash" >> $outname
echo "exec python3 -m pytec.$progname \"\$@\"" >> $outname
chmod 755 $outname
fi
done
'';
thermostat_gui = pkgs.python3Packages.buildPythonPackage { thermostat_gui = pkgs.python3Packages.buildPythonPackage {
pname = "thermostat_gui"; pname = "thermostat_gui";
version = "0.0.0"; version = "0.0.0";
@ -72,13 +133,13 @@
src = "${self}/pytec"; src = "${self}/pytec";
nativeBuildInputs = [ pkgs.qt6.wrapQtAppsHook ]; nativeBuildInputs = [ pkgs.qt6.wrapQtAppsHook ];
propagatedBuildInputs = propagatedBuildInputs = [ pkgs.qt6.qtbase ]
[ pkgs.qt6.qtbase ]
++ (with pkgs.python3Packages; [ ++ (with pkgs.python3Packages; [
pyqtgraph pyqtgraph
pyqt6 pyqt6
qasync qasync
pglive pglive
qtextras
]); ]);
dontWrapQtApps = true; dontWrapQtApps = true;
@ -87,33 +148,35 @@
''; '';
}; };
in { in {
packages.x86_64-linux = { packages.x86_64-linux = { inherit thermostat thermostat_gui; };
inherit thermostat thermostat_gui;
default = thermostat; formatter.x86_64-linux = nixpkgs.legacyPackages.x86_64-linux.nixfmt;
};
apps.x86_64-linux.thermostat_gui = { apps.x86_64-linux.thermostat_gui = {
type = "app"; type = "app";
program = "${self.packages.x86_64-linux.thermostat_gui}/bin/tec_qt"; program = "${self.packages.x86_64-linux.thermostat_gui}/bin/tec_qt";
}; };
hydraJobs = { hydraJobs = { inherit thermostat; };
inherit thermostat;
};
devShells.x86_64-linux.default = pkgs.mkShellNoCC { devShell.x86_64-linux = pkgs.mkShell {
name = "thermostat-dev-shell"; name = "thermostat-dev-shell";
packages = with pkgs; [ buildInputs = with pkgs;
rust llvm [ rust openocd dfu-util pytec-dev-wrappers ]
openocd dfu-util rlwrap ++ (with python3Packages; [
qtcreator numpy
] ++ (with python3Packages; [ matplotlib
numpy matplotlib
pyqtgraph pyqtgraph
setuptools
pyqt6 pyqt6
qasync qasync
pglive pglive
qtextras
]); ]);
shellHook = ''
export PYTHONPATH=`pwd`/pytec:$PYTHONPATH
'';
}; };
defaultPackage.x86_64-linux = thermostat;
}; };
} }

3
pytec/.flake8 Normal file
View File

@ -0,0 +1,3 @@
[flake8]
max-line-length = 88
extend-ignore = E203,E701

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

@ -12,32 +12,33 @@ from pytec.client import Client
class PIDAutotuneState(Enum): class PIDAutotuneState(Enum):
STATE_OFF = 'off' STATE_OFF = "off"
STATE_RELAY_STEP_UP = 'relay step up' STATE_RELAY_STEP_UP = "relay step up"
STATE_RELAY_STEP_DOWN = 'relay step down' STATE_RELAY_STEP_DOWN = "relay step down"
STATE_SUCCEEDED = 'succeeded' STATE_SUCCEEDED = "succeeded"
STATE_FAILED = 'failed' STATE_FAILED = "failed"
STATE_READY = "ready" STATE_READY = "ready"
class PIDAutotune: class PIDAutotune:
PIDParams = namedtuple('PIDParams', ['Kp', 'Ki', 'Kd']) PIDParams = namedtuple("PIDParams", ["Kp", "Ki", "Kd"])
PEAK_AMPLITUDE_TOLERANCE = 0.05 PEAK_AMPLITUDE_TOLERANCE = 0.05
_tuning_rules = { _tuning_rules = {
"ziegler-nichols": [0.6, 1.2, 0.075], "ziegler-nichols": [0.6, 1.2, 0.075],
"tyreus-luyben": [0.4545, 0.2066, 0.07214], "tyreus-luyben": [0.4545, 0.2066, 0.07214],
"ciancone-marlin": [0.303, 0.1364, 0.0481], "ciancone-marlin": [0.303, 0.1364, 0.0481],
"pessen-integral": [0.7, 1.75, 0.105], "pessen-integral": [0.7, 1.75, 0.105],
"some-overshoot": [0.333, 0.667, 0.111], "some-overshoot": [0.333, 0.667, 0.111],
"no-overshoot": [0.2, 0.4, 0.0667] "no-overshoot": [0.2, 0.4, 0.0667],
} }
def __init__(self, setpoint, out_step=10, lookback=60, def __init__(
noiseband=0.5, sampletime=1.2): self, setpoint, out_step=10, lookback=60, noiseband=0.5, sampletime=1.2
):
if setpoint is None: if setpoint is None:
raise ValueError('setpoint must be specified') raise ValueError("setpoint must be specified")
self._inputs = deque(maxlen=round(lookback / sampletime)) self._inputs = deque(maxlen=round(lookback / sampletime))
self._setpoint = setpoint self._setpoint = setpoint
@ -84,7 +85,7 @@ class PIDAutotune:
"""Get a list of all available tuning rules.""" """Get a list of all available tuning rules."""
return self._tuning_rules.keys() return self._tuning_rules.keys()
def get_pid_parameters(self, tuning_rule='ziegler-nichols'): def get_pid_parameters(self, tuning_rule="ziegler-nichols"):
"""Get PID parameters. """Get PID parameters.
Args: Args:
@ -116,27 +117,34 @@ class PIDAutotune:
""" """
now = time_input * 1000 now = time_input * 1000
if (self._state == PIDAutotuneState.STATE_OFF if (
or self._state == PIDAutotuneState.STATE_SUCCEEDED self._state == PIDAutotuneState.STATE_OFF
or self._state == PIDAutotuneState.STATE_FAILED): or self._state == PIDAutotuneState.STATE_SUCCEEDED
or self._state == PIDAutotuneState.STATE_FAILED
or self._state == PIDAutotuneState.STATE_READY
):
self._state = PIDAutotuneState.STATE_RELAY_STEP_UP self._state = PIDAutotuneState.STATE_RELAY_STEP_UP
self._last_run_timestamp = now self._last_run_timestamp = now
# check input and change relay state if necessary # check input and change relay state if necessary
if (self._state == PIDAutotuneState.STATE_RELAY_STEP_UP if (
and input_val > self._setpoint + self._noiseband): self._state == PIDAutotuneState.STATE_RELAY_STEP_UP
and input_val > self._setpoint + self._noiseband
):
self._state = PIDAutotuneState.STATE_RELAY_STEP_DOWN self._state = PIDAutotuneState.STATE_RELAY_STEP_DOWN
logging.debug('switched state: {0}'.format(self._state)) logging.debug("switched state: {0}".format(self._state))
logging.debug('input: {0}'.format(input_val)) logging.debug("input: {0}".format(input_val))
elif (self._state == PIDAutotuneState.STATE_RELAY_STEP_DOWN elif (
and input_val < self._setpoint - self._noiseband): self._state == PIDAutotuneState.STATE_RELAY_STEP_DOWN
and input_val < self._setpoint - self._noiseband
):
self._state = PIDAutotuneState.STATE_RELAY_STEP_UP self._state = PIDAutotuneState.STATE_RELAY_STEP_UP
logging.debug('switched state: {0}'.format(self._state)) logging.debug("switched state: {0}".format(self._state))
logging.debug('input: {0}'.format(input_val)) logging.debug("input: {0}".format(input_val))
# set output # set output
if (self._state == PIDAutotuneState.STATE_RELAY_STEP_UP): if self._state == PIDAutotuneState.STATE_RELAY_STEP_UP:
self._output = self._initial_output - self._outputstep self._output = self._initial_output - self._outputstep
elif self._state == PIDAutotuneState.STATE_RELAY_STEP_DOWN: elif self._state == PIDAutotuneState.STATE_RELAY_STEP_DOWN:
self._output = self._initial_output + self._outputstep self._output = self._initial_output + self._outputstep
@ -179,8 +187,8 @@ class PIDAutotune:
self._peak_count += 1 self._peak_count += 1
self._peaks.append(input_val) self._peaks.append(input_val)
self._peak_timestamps.append(now) self._peak_timestamps.append(now)
logging.debug('found peak: {0}'.format(input_val)) logging.debug("found peak: {0}".format(input_val))
logging.debug('peak count: {0}'.format(self._peak_count)) logging.debug("peak count: {0}".format(self._peak_count))
# check for convergence of induced oscillation # check for convergence of induced oscillation
# convergence of amplitude assessed on last 4 peaks (1.5 cycles) # convergence of amplitude assessed on last 4 peaks (1.5 cycles)
@ -190,20 +198,19 @@ class PIDAutotune:
abs_max = self._peaks[-2] abs_max = self._peaks[-2]
abs_min = self._peaks[-2] abs_min = self._peaks[-2]
for i in range(0, len(self._peaks) - 2): for i in range(0, len(self._peaks) - 2):
self._induced_amplitude += abs(self._peaks[i] self._induced_amplitude += abs(self._peaks[i] - self._peaks[i + 1])
- self._peaks[i+1])
abs_max = max(self._peaks[i], abs_max) abs_max = max(self._peaks[i], abs_max)
abs_min = min(self._peaks[i], abs_min) abs_min = min(self._peaks[i], abs_min)
self._induced_amplitude /= 6.0 self._induced_amplitude /= 6.0
# check convergence criterion for amplitude of induced oscillation # check convergence criterion for amplitude of induced oscillation
amplitude_dev = ((0.5 * (abs_max - abs_min) amplitude_dev = (
- self._induced_amplitude) 0.5 * (abs_max - abs_min) - self._induced_amplitude
/ self._induced_amplitude) ) / self._induced_amplitude
logging.debug('amplitude: {0}'.format(self._induced_amplitude)) logging.debug("amplitude: {0}".format(self._induced_amplitude))
logging.debug('amplitude deviation: {0}'.format(amplitude_dev)) logging.debug("amplitude deviation: {0}".format(amplitude_dev))
if amplitude_dev < PIDAutotune.PEAK_AMPLITUDE_TOLERANCE: if amplitude_dev < PIDAutotune.PEAK_AMPLITUDE_TOLERANCE:
self._state = PIDAutotuneState.STATE_SUCCEEDED self._state = PIDAutotuneState.STATE_SUCCEEDED
@ -217,25 +224,24 @@ class PIDAutotune:
if self._state == PIDAutotuneState.STATE_SUCCEEDED: if self._state == PIDAutotuneState.STATE_SUCCEEDED:
self._output = 0 self._output = 0
logging.debug('peak finding successful') logging.debug("peak finding successful")
# calculate ultimate gain # calculate ultimate gain
self._Ku = 4.0 * self._outputstep / \ self._Ku = 4.0 * self._outputstep / (self._induced_amplitude * math.pi)
(self._induced_amplitude * math.pi) logging.debug("Ku: {0}".format(self._Ku))
print('Ku: {0}'.format(self._Ku))
# calculate ultimate period in seconds # calculate ultimate period in seconds
period1 = self._peak_timestamps[3] - self._peak_timestamps[1] period1 = self._peak_timestamps[3] - self._peak_timestamps[1]
period2 = self._peak_timestamps[4] - self._peak_timestamps[2] period2 = self._peak_timestamps[4] - self._peak_timestamps[2]
self._Pu = 0.5 * (period1 + period2) / 1000.0 self._Pu = 0.5 * (period1 + period2) / 1000.0
print('Pu: {0}'.format(self._Pu)) logging.debug("Pu: {0}".format(self._Pu))
for rule in self._tuning_rules: for rule in self._tuning_rules:
params = self.get_pid_parameters(rule) params = self.get_pid_parameters(rule)
print('rule: {0}'.format(rule)) logging.debug("rule: {0}".format(rule))
print('Kp: {0}'.format(params.Kp)) logging.debug("Kp: {0}".format(params.Kp))
print('Ki: {0}'.format(params.Ki)) logging.debug("Ki: {0}".format(params.Ki))
print('Kd: {0}'.format(params.Kd)) logging.debug("Kd: {0}".format(params.Kd))
return True return True
return False return False
@ -262,23 +268,24 @@ def main():
data = next(tec.report_mode()) data = next(tec.report_mode())
ch = data[channel] ch = data[channel]
tuner = PIDAutotune(target_temperature, output_step, tuner = PIDAutotune(
lookback, noiseband, ch['interval']) target_temperature, output_step, lookback, noiseband, ch["interval"]
)
for data in tec.report_mode(): for data in tec.report_mode():
ch = data[channel] ch = data[channel]
temperature = ch['temperature'] temperature = ch["temperature"]
if (tuner.run(temperature, ch['time'])): if tuner.run(temperature, ch["time"]):
break break
tuner_out = tuner.output() tuner_out = tuner.output()
tec.set_param("output", channel, "i_set", tuner_out) tec.set_param("pwm", channel, "i_set", tuner_out)
tec.set_param("output", channel, "i_set", 0) tec.set_param("pwm", channel, "i_set", 0)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -5,8 +5,8 @@ from pytec.aioclient import AsyncioClient
async def poll_for_info(tec): async def poll_for_info(tec):
while True: while True:
print(tec.get_output()) print(tec.get_pwm())
print(tec.get_b_parameter()) print(tec.get_steinhart_hart())
print(tec.get_pid()) print(tec.get_pid())
print(tec.get_postfilter()) print(tec.get_postfilter())
print(tec.get_fan()) print(tec.get_fan())
@ -17,12 +17,12 @@ async def poll_for_info(tec):
async def main(): async def main():
tec = AsyncioClient() tec = AsyncioClient()
await tec.connect() # (host="192.168.1.26", port=23) await tec.connect() # (host="192.168.1.26", port=23)
await tec.set_param("b-p", 1, "t0", 20) await tec.set_param("s-h", 1, "t0", 20)
print(await tec.get_output()) print(await tec.get_pwm())
print(await tec.get_pid()) print(await tec.get_pid())
print(await tec.get_output()) print(await tec.get_pwm())
print(await tec.get_postfilter()) print(await tec.get_postfilter())
print(await tec.get_b_parameter()) print(await tec.get_steinhart_hart())
polling_task = asyncio.create_task(poll_for_info(tec)) polling_task = asyncio.create_task(poll_for_info(tec))

View File

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

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

@ -7,9 +7,10 @@ from pytec.client import Client
TIME_WINDOW = 300.0 TIME_WINDOW = 300.0
tec = Client() tec = Client()
target_temperature = tec.get_pid()[0]['target'] target_temperature = tec.get_pid()[0]["target"]
print("Channel 0 target temperature: {:.3f}".format(target_temperature)) print("Channel 0 target temperature: {:.3f}".format(target_temperature))
class Series: class Series:
def __init__(self, conv=lambda x: x): def __init__(self, conv=lambda x: x):
self.conv = conv self.conv = conv
@ -26,25 +27,27 @@ class Series:
drop += 1 drop += 1
self.x_data = self.x_data[drop:] self.x_data = self.x_data[drop:]
self.y_data = self.y_data[drop:] self.y_data = self.y_data[drop:]
series = { series = {
# 'adc': Series(), # 'adc': Series(),
# 'sens': Series(lambda x: x * 0.0001), # 'sens': Series(lambda x: x * 0.0001),
'temperature': Series(), "temperature": Series(),
# 'i_set': Series(), # 'i_set': Series(),
'pid_output': Series(), "pid_output": Series(),
# 'vref': Series(), # 'vref': Series(),
# 'dac_value': Series(), # 'dac_value': Series(),
# 'dac_feedback': Series(), # 'dac_feedback': Series(),
# 'i_tec': Series(), # 'i_tec': Series(),
'tec_i': Series(), "tec_i": Series(),
'tec_u_meas': Series(), "tec_u_meas": Series(),
# 'interval': Series(), # 'interval': Series(),
} }
series_lock = Lock() series_lock = Lock()
quit = False quit = False
def recv_data(tec): def recv_data(tec):
global last_packet_time global last_packet_time
for data in tec.report_mode(): for data in tec.report_mode():
@ -55,25 +58,27 @@ def recv_data(tec):
if k in ch0: if k in ch0:
v = ch0[k] v = ch0[k]
if type(v) is float: if type(v) is float:
s.append(ch0['time'], v) s.append(ch0["time"], v)
finally: finally:
series_lock.release() series_lock.release()
if quit: if quit:
break break
thread = Thread(target=recv_data, args=(tec,)) thread = Thread(target=recv_data, args=(tec,))
thread.start() thread.start()
fig, ax = plt.subplots() fig, ax = plt.subplots()
for k, s in series.items(): for k, s in series.items():
s.plot, = ax.plot([], [], label=k) (s.plot,) = ax.plot([], [], label=k)
legend = ax.legend() legend = ax.legend()
def animate(i): def animate(i):
min_x, max_x, min_y, max_y = None, None, None, None min_x, max_x, min_y, max_y = None, None, None, None
series_lock.acquire() series_lock.acquire()
try: try:
for k, s in series.items(): for k, s in series.items():
@ -120,8 +125,8 @@ def animate(i):
legend.remove() legend.remove()
legend = ax.legend() legend = ax.legend()
ani = animation.FuncAnimation(
fig, animate, interval=1, blit=False, save_count=50) ani = animation.FuncAnimation(fig, animate, interval=1, blit=False, save_count=50)
plt.show() plt.show()
quit = True quit = True

View File

@ -16,3 +16,6 @@ tec_qt = "tec_qt:main"
[tool.setuptools] [tool.setuptools]
packages.find = {} packages.find = {}
py-modules = ["autotune", "plot", "tec_qt"] py-modules = ["autotune", "plot", "tec_qt"]
[tool.pylint.format]
max-line-length = "88"

View File

@ -12,6 +12,7 @@ class AsyncioClient:
self._reader = None self._reader = None
self._writer = None self._writer = None
self._command_lock = asyncio.Lock() self._command_lock = asyncio.Lock()
self._report_mode_on = False
async def connect(self, host="192.168.1.26", port=23): async def connect(self, host="192.168.1.26", port=23):
"""Connect to Thermostat at specified host and port. """Connect to Thermostat at specified host and port.
@ -40,13 +41,13 @@ class AsyncioClient:
self._writer = None self._writer = None
async def _check_zero_limits(self): async def _check_zero_limits(self):
output_report = await self.get_output() pwm_report = await self.get_pwm()
for output_channel in output_report: for pwm_channel in pwm_report:
for limit in ["max_i_neg", "max_i_pos", "max_v"]: for limit in ["max_i_neg", "max_i_pos", "max_v"]:
if output_channel[limit] == 0.0: if pwm_channel[limit]["value"] == 0.0:
logging.warning( logging.warning(
"`{}` limit is set to zero on channel {}".format( "`{}` limit is set to zero on channel {}".format(
limit, output_channel["channel"] limit, pwm_channel["channel"]
) )
) )
@ -66,6 +67,7 @@ class AsyncioClient:
line = await self._read_write(command) line = await self._read_write(command)
response = json.loads(line) response = json.loads(line)
logging.debug("%s: %s", command, response)
if "error" in response: if "error" in response:
raise CommandError(response["error"]) raise CommandError(response["error"])
return response return response
@ -76,27 +78,25 @@ class AsyncioClient:
result[int(item["channel"])] = item result[int(item["channel"])] = item
return result return result
async def get_output(self): async def get_pwm(self):
"""Retrieve output limits for the TEC """Retrieve PWM limits for the TEC
Example:: Example::
[{'channel': 0, [{'channel': 0,
'center': 'vref', 'center': 'vref',
'i_set': -0.02002179650216762, 'i_set': {'max': 2.9802790335151985, 'value': -0.02002179650216762},
'max_i_neg': 2.0, 'max_i_neg': {'max': 3.0, 'value': 3.0},
'max_v': : 3.988, 'max_v': {'max': 5.988, 'value': 5.988},
'max_i_pos': 2.0, 'max_i_pos': {'max': 3.0, 'value': 3.0}},
'polarity': 'normal'},
{'channel': 1, {'channel': 1,
'center': 'vref', 'center': 'vref',
'i_set': -0.02002179650216762, 'i_set': {'max': 2.9802790335151985, 'value': -0.02002179650216762},
'max_i_neg': 2.0, 'max_i_neg': {'max': 3.0, 'value': 3.0},
'max_v': : 3.988, 'max_v': {'max': 5.988, 'value': 5.988},
'max_i_pos': 2.0, 'max_i_pos': {'max': 3.0, 'value': 3.0}}
'polarity': 'normal'},
] ]
""" """
return await self._get_conf("output") return await self._get_conf("pwm")
async def get_pid(self): async def get_pid(self):
"""Retrieve PID control state """Retrieve PID control state
@ -121,14 +121,14 @@ class AsyncioClient:
""" """
return await self._get_conf("pid") return await self._get_conf("pid")
async def get_b_parameter(self): async def get_steinhart_hart(self):
"""Retrieve B-Parameter equation parameters for resistance to temperature conversion """Retrieve Steinhart-Hart parameters for resistance to temperature conversion
Example:: Example::
[{'params': {'b': 3800.0, 'r0': 10000.0, 't0': 298.15}, 'channel': 0}, [{'params': {'b': 3800.0, 'r0': 10000.0, 't0': 298.15}, 'channel': 0},
{'params': {'b': 3800.0, 'r0': 10000.0, 't0': 298.15}, 'channel': 1}] {'params': {'b': 3800.0, 'r0': 10000.0, 't0': 298.15}, 'channel': 1}]
""" """
return await self._get_conf("b-p") return await self._get_conf("s-h")
async def get_postfilter(self): async def get_postfilter(self):
"""Retrieve DAC postfilter configuration """Retrieve DAC postfilter configuration
@ -144,17 +144,21 @@ class AsyncioClient:
return await self._command("fan") return await self._command("fan")
async def report(self): async def report(self):
"""Obtain one-time report on measurement values """Obtain one-time report on measurement values"""
return await self._command("report")
Example of yielded data:
async def report_mode(self):
"""Start reporting measurement values
Example of yielded data::
{'channel': 0, {'channel': 0,
'time': 2302524, 'time': 2302524,
'interval': 0.12
'adc': 0.6199188965423515, 'adc': 0.6199188965423515,
'sens': 6138.519310282602, 'sens': 6138.519310282602,
'temperature': 36.87032392655527, 'temperature': 36.87032392655527,
'pid_engaged': True, 'pid_engaged': True,
'i_set': 2.0635816680889123, 'i_set': 2.0635816680889123,
'vref': 1.494,
'dac_value': 2.527790834044456, 'dac_value': 2.527790834044456,
'dac_feedback': 2.523, 'dac_feedback': 2.523,
'i_tec': 2.331, 'i_tec': 2.331,
@ -162,13 +166,29 @@ class AsyncioClient:
'tec_u_meas': 2.5340000000000003, 'tec_u_meas': 2.5340000000000003,
'pid_output': 2.067581958092247} 'pid_output': 2.067581958092247}
""" """
return await self._command("report") await self._command("report mode", "on")
self._report_mode_on = True
while self._report_mode_on:
async with self._command_lock:
line = await self._read_line()
if not line:
break
try:
yield json.loads(line)
except json.decoder.JSONDecodeError:
pass
await self._command("report mode", "off")
def stop_report_mode(self):
self._report_mode_on = False
async def set_param(self, topic, channel, field="", value=""): async def set_param(self, topic, channel, field="", value=""):
"""Set configuration parameters """Set configuration parameters
Examples:: Examples::
await tec.set_param("output", 0, "max_v", 2.0) await tec.set_param("pwm", 0, "max_v", 2.0)
await tec.set_param("pid", 1, "output_max", 2.5) await tec.set_param("pid", 1, "output_max", 2.5)
await tec.set_param("s-h", 0, "t0", 20.0) await tec.set_param("s-h", 0, "t0", 20.0)
await tec.set_param("center", 0, "vref") await tec.set_param("center", 0, "vref")
@ -193,7 +213,7 @@ class AsyncioClient:
async def power_up(self, channel, target): async def power_up(self, channel, target):
"""Start closed-loop mode""" """Start closed-loop mode"""
await self.set_param("pid", channel, "target", value=target) await self.set_param("pid", channel, "target", value=target)
await self.set_param("output", channel, "pid") await self.set_param("pwm", channel, "pid")
async def save_config(self, channel=""): async def save_config(self, channel=""):
"""Save current configuration to EEPROM""" """Save current configuration to EEPROM"""

View File

@ -1,23 +1,32 @@
import socket import socket
import json import json
import logging import logging
import time
class CommandError(Exception): class CommandError(Exception):
pass pass
class Client: class Client:
def __init__(self, host="192.168.1.26", port=23, timeout=None): def __init__(self, host="192.168.1.26", port=23, timeout=None):
self._socket = socket.create_connection((host, port), timeout) self._socket = socket.create_connection((host, port), timeout)
self._lines = [""] self._lines = [""]
self._check_zero_limits() self._check_zero_limits()
def disconnect(self):
self._socket.shutdown(socket.SHUT_RDWR)
self._socket.close()
def _check_zero_limits(self): def _check_zero_limits(self):
output_report = self.get_output() pwm_report = self.get_pwm()
for output_channel in output_report: for pwm_channel in pwm_report:
for limit in ["max_i_neg", "max_i_pos", "max_v"]: for limit in ["max_i_neg", "max_i_pos", "max_v"]:
if output_channel[limit] == 0.0: if pwm_channel[limit]["value"] == 0.0:
logging.warning("`{}` limit is set to zero on channel {}".format(limit, output_channel["channel"])) logging.warning(
"`{}` limit is set to zero on channel {}".format(
limit, pwm_channel["channel"]
)
)
def _read_line(self): def _read_line(self):
# read more lines # read more lines
@ -25,7 +34,7 @@ class Client:
chunk = self._socket.recv(4096) chunk = self._socket.recv(4096)
if not chunk: if not chunk:
return None return None
buf = self._lines[-1] + chunk.decode('utf-8', errors='ignore') buf = self._lines[-1] + chunk.decode("utf-8", errors="ignore")
self._lines = buf.split("\n") self._lines = buf.split("\n")
line = self._lines[0] line = self._lines[0]
@ -33,10 +42,11 @@ class Client:
return line return line
def _command(self, *command): def _command(self, *command):
self._socket.sendall((" ".join(command) + "\n").encode('utf-8')) self._socket.sendall((" ".join(command) + "\n").encode("utf-8"))
line = self._read_line() line = self._read_line()
response = json.loads(line) response = json.loads(line)
logging.debug(f"{command}: {response}")
if "error" in response: if "error" in response:
raise CommandError(response["error"]) raise CommandError(response["error"])
return response return response
@ -47,27 +57,25 @@ class Client:
result[int(item["channel"])] = item result[int(item["channel"])] = item
return result return result
def get_output(self): def get_pwm(self):
"""Retrieve output limits for the TEC """Retrieve PWM limits for the TEC
Example:: Example::
[{'channel': 0, [{'channel': 0,
'center': 'vref', 'center': 'vref',
'i_set': -0.02002179650216762, 'i_set': {'max': 2.9802790335151985, 'value': -0.02002179650216762},
'max_i_neg': 2.0, 'max_i_neg': {'max': 3.0, 'value': 3.0},
'max_v': 3.988, 'max_v': {'max': 5.988, 'value': 5.988},
'max_i_pos': 2.0, 'max_i_pos': {'max': 3.0, 'value': 3.0}},
'polarity': 'normal',
{'channel': 1, {'channel': 1,
'center': 'vref', 'center': 'vref',
'i_set': -0.02002179650216762, 'i_set': {'max': 2.9802790335151985, 'value': -0.02002179650216762},
'max_i_neg': 2.0, 'max_i_neg': {'max': 3.0, 'value': 3.0},
'max_v': 3.988, 'max_v': {'max': 5.988, 'value': 5.988},
'max_i_pos': 2.0} 'max_i_pos': {'max': 3.0, 'value': 3.0}}
'polarity': 'normal',
] ]
""" """
return self._get_conf("output") return self._get_conf("pwm")
def get_pid(self): def get_pid(self):
"""Retrieve PID control state """Retrieve PID control state
@ -92,14 +100,14 @@ class Client:
""" """
return self._get_conf("pid") return self._get_conf("pid")
def get_b_parameter(self): def get_steinhart_hart(self):
"""Retrieve B-Parameter equation parameters for resistance to temperature conversion """Retrieve Steinhart-Hart parameters for resistance to temperature conversion
Example:: Example::
[{'params': {'b': 3800.0, 'r0': 10000.0, 't0': 298.15}, 'channel': 0}, [{'params': {'b': 3800.0, 'r0': 10000.0, 't0': 298.15}, 'channel': 0},
{'params': {'b': 3800.0, 'r0': 10000.0, 't0': 298.15}, 'channel': 1}] {'params': {'b': 3800.0, 'r0': 10000.0, 't0': 298.15}, 'channel': 1}]
""" """
return self._get_conf("b-p") return self._get_conf("s-h")
def get_postfilter(self): def get_postfilter(self):
"""Retrieve DAC postfilter configuration """Retrieve DAC postfilter configuration
@ -129,8 +137,9 @@ class Client:
'tec_u_meas': 2.5340000000000003, 'tec_u_meas': 2.5340000000000003,
'pid_output': 2.067581958092247} 'pid_output': 2.067581958092247}
""" """
self._command("report mode", "on")
while True: while True:
self._socket.sendall("report\n".encode('utf-8'))
line = self._read_line() line = self._read_line()
if not line: if not line:
break break
@ -138,15 +147,14 @@ class Client:
yield json.loads(line) yield json.loads(line)
except json.decoder.JSONDecodeError: except json.decoder.JSONDecodeError:
pass pass
time.sleep(0.05)
def set_param(self, topic, channel, field="", value=""): def set_param(self, topic, channel, field="", value=""):
"""Set configuration parameters """Set configuration parameters
Examples:: Examples::
tec.set_param("output", 0, "max_v", 2.0) tec.set_param("pwm", 0, "max_v", 2.0)
tec.set_param("pid", 1, "output_max", 2.5) tec.set_param("pid", 1, "output_max", 2.5)
tec.set_param("b-p", 0, "t0", 20.0) tec.set_param("s-h", 0, "t0", 20.0)
tec.set_param("center", 0, "vref") tec.set_param("center", 0, "vref")
tec.set_param("postfilter", 1, 21) tec.set_param("postfilter", 1, 21)
@ -161,7 +169,7 @@ class Client:
def power_up(self, channel, target): def power_up(self, channel, target):
"""Start closed-loop mode""" """Start closed-loop mode"""
self.set_param("pid", channel, "target", value=target) self.set_param("pid", channel, "target", value=target)
self.set_param("output", channel, "pid") self.set_param("pwm", channel, "pid")
def save_config(self): def save_config(self):
"""Save current configuration to EEPROM""" """Save current configuration to EEPROM"""
@ -170,3 +178,11 @@ class Client:
def load_config(self): def load_config(self):
"""Load current configuration from EEPROM""" """Load current configuration from EEPROM"""
self._command("load") self._command("load")
def hw_rev(self):
"""Get Thermostat hardware revision"""
return self._command("hwrev")
def fan(self):
"""Get Thermostat current fan settings"""
return self._command("fan")

View File

@ -85,10 +85,10 @@ class Thermostat(QObject, metaclass=PropertyMeta):
self.fan, self.pwm, self.report, self.pid, self.thermistor, self.postfilter = ( self.fan, self.pwm, self.report, self.pid, self.thermistor, self.postfilter = (
await asyncio.gather( await asyncio.gather(
self._client.get_fan(), self._client.get_fan(),
self._client.get_output(), self._client.get_pwm(),
self._client.report(), self._client.report(),
self._client.get_pid(), self._client.get_pid(),
self._client.get_b_parameter(), self._client.get_steinhart_hart(),
self._client.get_postfilter(), self._client.get_postfilter(),
) )
) )

View File

@ -231,13 +231,13 @@ class CtrlPanel(QObject):
with QSignalBlocker(self.params[channel]): with QSignalBlocker(self.params[channel]):
self.params[channel].child( self.params[channel].child(
"Output Config", "Limits", "Max Voltage Difference" "Output Config", "Limits", "Max Voltage Difference"
).setValue(pwm_params["max_v"]) ).setValue(pwm_params["max_v"]["value"])
self.params[channel].child( self.params[channel].child(
"Output Config", "Limits", "Max Cooling Current" "Output Config", "Limits", "Max Cooling Current"
).setValue(pwm_params["max_i_pos"] * 1000) ).setValue(pwm_params["max_i_pos"]["value"] * 1000)
self.params[channel].child( self.params[channel].child(
"Output Config", "Limits", "Max Heating Current" "Output Config", "Limits", "Max Heating Current"
).setValue(pwm_params["max_i_neg"] * 1000) ).setValue(pwm_params["max_i_neg"]["value"] * 1000)
@pyqtSlot(list) @pyqtSlot(list)
def update_postfilter(self, postfilter_data): def update_postfilter(self, postfilter_data):

View File

@ -25,7 +25,6 @@
"Constant Current", "Constant Current",
"Temperature PID" "Temperature PID"
], ],
"value": "Constant Current",
"thermostat:set_param":{ "thermostat:set_param":{
"topic":"pwm", "topic":"pwm",
"field":"pid" "field":"pid"

View File

@ -17,7 +17,7 @@ class ZeroLimitsWarningView(QObject):
for pwm_params in pwm_data: for pwm_params in pwm_data:
channel = pwm_params["channel"] channel = pwm_params["channel"]
for limit in "max_i_pos", "max_i_neg", "max_v": for limit in "max_i_pos", "max_i_neg", "max_v":
if pwm_params[limit] == 0.0: if pwm_params[limit]["value"] == 0.0:
channels_zeroed_limits[channel].add(limit) channels_zeroed_limits[channel].add(limit)
channel_disabled = [False, False] channel_disabled = [False, False]

View File

@ -1,15 +1,15 @@
"""GUI for the Sinara 8451 Thermostat""" """GUI for the Sinara 8451 Thermostat"""
import json
import asyncio import asyncio
import logging import logging
import argparse import argparse
import importlib.resources import importlib.resources
import json
from PyQt6 import QtWidgets, QtGui, uic
from PyQt6.QtCore import pyqtSlot
import qasync import qasync
from qasync import asyncSlot, asyncClose from qasync import asyncSlot, asyncClose
from autotune import PIDAutotuneState from autotune import PIDAutotuneState
from PyQt6 import QtWidgets, QtGui, uic
from PyQt6.QtCore import pyqtSlot
from pytec.gui.model.thermostat import Thermostat, ThermostatConnectionState from pytec.gui.model.thermostat import Thermostat, ThermostatConnectionState
from pytec.gui.model.pid_autotuner import PIDAutoTuner from pytec.gui.model.pid_autotuner import PIDAutoTuner
from pytec.gui.view.zero_limits_warning_view import ZeroLimitsWarningView from pytec.gui.view.zero_limits_warning_view import ZeroLimitsWarningView

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,14 @@
use crate::{
ad5680, ad7172,
channel_state::ChannelState,
pins::{ChannelPinSet, ChannelPins},
};
use stm32f4xx_hal::hal::digital::v2::OutputPin; use stm32f4xx_hal::hal::digital::v2::OutputPin;
use uom::si::{electric_potential::volt, f64::ElectricPotential}; use uom::si::{
f64::ElectricPotential,
electric_potential::volt,
};
use crate::{
ad5680,
ad7172,
channel_state::ChannelState,
pins::{ChannelPins, ChannelPinSet},
};
/// Marker type for the first channel /// Marker type for the first channel
pub struct Channel0; pub struct Channel0;
@ -20,7 +24,7 @@ pub struct Channel<C: ChannelPins> {
pub vref_meas: ElectricPotential, pub vref_meas: ElectricPotential,
pub shdn: C::Shdn, pub shdn: C::Shdn,
pub vref_pin: C::VRefPin, pub vref_pin: C::VRefPin,
pub itec_pin: C::ITecPin, pub itec_pin: C::ItecPin,
/// feedback from `dac` output /// feedback from `dac` output
pub dac_feedback_pin: C::DacFeedbackPin, pub dac_feedback_pin: C::DacFeedbackPin,
pub tec_u_meas_pin: C::TecUMeasPin, pub tec_u_meas_pin: C::TecUMeasPin,
@ -36,8 +40,7 @@ impl<C: ChannelPins> Channel<C> {
Channel { Channel {
state, state,
dac, dac, vref_meas,
vref_meas,
shdn: pins.shdn, shdn: pins.shdn,
vref_pin: pins.vref_pin, vref_pin: pins.vref_pin,
itec_pin: pins.itec_pin, itec_pin: pins.itec_pin,

View File

@ -1,21 +1,24 @@
use crate::{
ad7172, b_parameter as bp,
command_parser::{CenterPoint, Polarity},
config::PwmLimits,
pid,
};
use num_traits::Zero;
use smoltcp::time::{Duration, Instant}; use smoltcp::time::{Duration, Instant};
use uom::si::{ use uom::si::{
electric_current::ampere,
electric_potential::volt,
electrical_resistance::ohm,
f64::{ f64::{
ElectricCurrent, ElectricPotential, ElectricalResistance, ThermodynamicTemperature, Time, ElectricPotential,
ElectricCurrent,
ElectricalResistance,
ThermodynamicTemperature,
Time,
}, },
electric_potential::volt,
electric_current::ampere,
electrical_resistance::ohm,
thermodynamic_temperature::degree_celsius, thermodynamic_temperature::degree_celsius,
time::millisecond, time::millisecond,
}; };
use crate::{
ad7172,
pid,
steinhart_hart as sh,
command_parser::CenterPoint,
};
const R_INNER: f64 = 2.0 * 5100.0; const R_INNER: f64 = 2.0 * 5100.0;
const VREF_SENS: f64 = 3.3 / 2.0; const VREF_SENS: f64 = 3.3 / 2.0;
@ -29,11 +32,9 @@ pub struct ChannelState {
pub center: CenterPoint, pub center: CenterPoint,
pub dac_value: ElectricPotential, pub dac_value: ElectricPotential,
pub i_set: ElectricCurrent, pub i_set: ElectricCurrent,
pub pwm_limits: PwmLimits,
pub pid_engaged: bool, pub pid_engaged: bool,
pub pid: pid::Controller, pub pid: pid::Controller,
pub bp: bp::Parameters, pub sh: sh::Parameters,
pub polarity: Polarity,
} }
impl ChannelState { impl ChannelState {
@ -44,18 +45,12 @@ impl ChannelState {
adc_time: Instant::from_secs(0), adc_time: Instant::from_secs(0),
// default: 10 Hz // default: 10 Hz
adc_interval: Duration::from_millis(100), adc_interval: Duration::from_millis(100),
center: CenterPoint::VRef, center: CenterPoint::Vref,
dac_value: ElectricPotential::new::<volt>(0.0), dac_value: ElectricPotential::new::<volt>(0.0),
i_set: ElectricCurrent::new::<ampere>(0.0), i_set: ElectricCurrent::new::<ampere>(0.0),
pwm_limits: PwmLimits {
max_v: ElectricPotential::zero(),
max_i_pos: ElectricCurrent::zero(),
max_i_neg: ElectricCurrent::zero(),
},
pid_engaged: false, pid_engaged: false,
pid: pid::Controller::new(pid::Parameters::default()), pid: pid::Controller::new(pid::Parameters::default()),
bp: bp::Parameters::default(), sh: sh::Parameters::default(),
polarity: Polarity::Normal,
} }
} }
@ -72,7 +67,8 @@ impl ChannelState {
/// Update PID state on ADC input, calculate new DAC output /// Update PID state on ADC input, calculate new DAC output
pub fn update_pid(&mut self) -> Option<f64> { 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); let pid_output = self.pid.update(temperature);
Some(pid_output) Some(pid_output)
} }
@ -100,7 +96,7 @@ impl ChannelState {
pub fn get_temperature(&self) -> Option<ThermodynamicTemperature> { pub fn get_temperature(&self) -> Option<ThermodynamicTemperature> {
let r = self.get_sens()?; let r = self.get_sens()?;
let temperature = self.bp.get_temperature(r); let temperature = self.sh.get_temperature(r);
Some(temperature) Some(temperature)
} }
} }

View File

@ -1,29 +1,29 @@
use crate::timer::sleep; use core::cmp::max_by;
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 heapless::{consts::U2, Vec};
use num_traits::Zero;
use serde::{Serialize, Serializer}; use serde::{Serialize, Serializer};
use smoltcp::time::Instant; use smoltcp::time::Instant;
use stm32f4xx_hal::hal; use stm32f4xx_hal::hal;
use uom::si::{ use uom::si::{
electric_current::ampere,
electric_potential::{millivolt, volt},
electrical_resistance::ohm,
f64::{ElectricCurrent, ElectricPotential, ElectricalResistance, Time}, f64::{ElectricCurrent, ElectricPotential, ElectricalResistance, Time},
electric_potential::{millivolt, volt},
electric_current::ampere,
electrical_resistance::ohm,
ratio::ratio, ratio::ratio,
thermodynamic_temperature::degree_celsius, thermodynamic_temperature::degree_celsius,
}; };
use crate::{
ad5680,
ad7172,
channel::{Channel, Channel0, Channel1},
channel_state::ChannelState,
command_parser::{CenterPoint, PwmPin},
command_handler::JsonBuffer,
pins::{self, Channel0VRef, Channel1VRef},
steinhart_hart,
};
pub enum PinsAdcReadTarget { pub enum PinsAdcReadTarget {
VRef, VREF,
DacVfb, DacVfb,
ITec, ITec,
VTec, VTec,
@ -32,28 +32,12 @@ pub enum PinsAdcReadTarget {
pub const CHANNELS: usize = 2; pub const CHANNELS: usize = 2;
pub const R_SENSE: f64 = 0.05; pub const R_SENSE: f64 = 0.05;
// From design specs // as stated in the MAX1968 datasheet
pub const MAX_TEC_I: ElectricCurrent = ElectricCurrent { pub const MAX_TEC_I: f64 = 3.0;
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 // DAC chip outputs 0-5v, which is then passed through a resistor dividor to provide 0-3v range
const DAC_OUT_V_MAX: ElectricPotential = ElectricPotential { const DAC_OUT_V_MAX: f64 = 3.0;
dimension: PhantomData,
units: PhantomData,
value: 3.0,
};
// TODO: -pub // TODO: -pub
pub struct Channels { pub struct Channels {
channel0: Channel<Channel0>, channel0: Channel<Channel0>,
@ -71,25 +55,19 @@ impl Channels {
adc.set_sync_enable(false).unwrap(); adc.set_sync_enable(false).unwrap();
// Setup channels and start ADC // Setup channels and start ADC
adc.setup_channel(0, ad7172::Input::Ain2, ad7172::Input::Ain3) adc.setup_channel(0, ad7172::Input::Ain2, ad7172::Input::Ain3).unwrap();
.unwrap(); let adc_calibration0 = adc.get_calibration(0)
let adc_calibration0 = adc.get_calibration(0).expect("adc_calibration0"); .expect("adc_calibration0");
adc.setup_channel(1, ad7172::Input::Ain0, ad7172::Input::Ain1) adc.setup_channel(1, ad7172::Input::Ain0, ad7172::Input::Ain1).unwrap();
.unwrap(); let adc_calibration1 = adc.get_calibration(1)
let adc_calibration1 = adc.get_calibration(1).expect("adc_calibration1"); .expect("adc_calibration1");
adc.start_continuous_conversion().unwrap(); adc.start_continuous_conversion().unwrap();
let channel0 = Channel::new(pins.channel0, adc_calibration0); let channel0 = Channel::new(pins.channel0, adc_calibration0);
let channel1 = Channel::new(pins.channel1, adc_calibration1); let channel1 = Channel::new(pins.channel1, adc_calibration1);
let pins_adc = pins.pins_adc; let pins_adc = pins.pins_adc;
let pwm = pins.pwm; let pwm = pins.pwm;
let mut channels = Channels { let mut channels = Channels { channel0, channel1, adc, pins_adc, pwm };
channel0,
channel1,
adc,
pins_adc,
pwm,
};
for channel in 0..CHANNELS { for channel in 0..CHANNELS {
channels.calibrate_dac_value(channel); channels.calibrate_dac_value(channel);
channels.set_i(channel, ElectricCurrent::new::<ampere>(0.0)); channels.set_i(channel, ElectricCurrent::new::<ampere>(0.0));
@ -130,10 +108,10 @@ impl Channels {
/// calculate the TEC i_set centerpoint /// calculate the TEC i_set centerpoint
pub fn get_center(&mut self, channel: usize) -> ElectricPotential { pub fn get_center(&mut self, channel: usize) -> ElectricPotential {
match self.channel_state(channel).center { match self.channel_state(channel).center {
CenterPoint::VRef => self.adc_read(channel, PinsAdcReadTarget::VRef, 8), CenterPoint::Vref =>
CenterPoint::Override(center_point) => { self.adc_read(channel, PinsAdcReadTarget::VREF, 8),
ElectricPotential::new::<volt>(center_point.into()) CenterPoint::Override(center_point) =>
} ElectricPotential::new::<volt>(center_point.into()),
} }
} }
@ -143,14 +121,14 @@ impl Channels {
voltage voltage
} }
pub fn get_i_set(&mut self, channel: usize) -> ElectricCurrent { pub fn get_i(&mut self, channel: usize) -> ElectricCurrent {
let i_set = self.channel_state(channel).i_set; let i_set = self.channel_state(channel).i_set;
i_set i_set
} }
/// i_set DAC /// i_set DAC
fn set_dac(&mut self, channel: usize, voltage: ElectricPotential) -> ElectricPotential { fn set_dac(&mut self, channel: usize, voltage: ElectricPotential) -> ElectricPotential {
let value = ((voltage / DAC_OUT_V_MAX).get::<ratio>() * (ad5680::MAX_VALUE as f64)) as u32; let value = ((voltage / ElectricPotential::new::<volt>(DAC_OUT_V_MAX)).get::<ratio>() * (ad5680::MAX_VALUE as f64)) as u32 ;
match channel { match channel {
0 => self.channel0.dac.set(value).unwrap(), 0 => self.channel0.dac.set(value).unwrap(),
1 => self.channel1.dac.set(value).unwrap(), 1 => self.channel1.dac.set(value).unwrap(),
@ -161,72 +139,69 @@ impl Channels {
} }
pub fn set_i(&mut self, channel: usize, i_set: ElectricCurrent) -> ElectricCurrent { pub fn set_i(&mut self, channel: usize, i_set: ElectricCurrent) -> ElectricCurrent {
let i_set = i_set.min(MAX_TEC_I).max(-MAX_TEC_I); // Silently clamp i_set
self.channel_state(channel).i_set = i_set; let i_ceiling = ElectricCurrent::new::<ampere>(MAX_TEC_I);
let negate = match self.channel_state(channel).polarity { let i_floor = ElectricCurrent::new::<ampere>(-MAX_TEC_I);
Polarity::Normal => 1.0, let i_set = i_set.min(i_ceiling).max(i_floor);
Polarity::Reversed => -1.0,
}; let vref_meas = match channel.into() {
let vref_meas = match channel {
0 => self.channel0.vref_meas, 0 => self.channel0.vref_meas,
1 => self.channel1.vref_meas, 1 => self.channel1.vref_meas,
_ => unreachable!(), _ => unreachable!(),
}; };
let center_point = vref_meas; let center_point = vref_meas;
let r_sense = ElectricalResistance::new::<ohm>(R_SENSE); let r_sense = ElectricalResistance::new::<ohm>(R_SENSE);
let voltage = negate * i_set * 10.0 * r_sense + center_point; let voltage = i_set * 10.0 * r_sense + center_point;
let voltage = self.set_dac(channel, voltage); let voltage = self.set_dac(channel, voltage);
let i_set = (voltage - center_point) / (10.0 * r_sense);
negate * (voltage - center_point) / (10.0 * r_sense) self.channel_state(channel).i_set = i_set;
i_set
} }
/// AN4073: ADC Reading Dispersion can be reduced through Averaging /// AN4073: ADC Reading Dispersion can be reduced through Averaging
pub fn adc_read( pub fn adc_read(&mut self, channel: usize, adc_read_target: PinsAdcReadTarget, avg_pt: u16) -> ElectricPotential {
&mut self,
channel: usize,
adc_read_target: PinsAdcReadTarget,
avg_pt: u16,
) -> ElectricPotential {
let mut sample: u32 = 0; let mut sample: u32 = 0;
match channel { match channel {
0 => { 0 => {
sample = match adc_read_target { sample = match adc_read_target {
PinsAdcReadTarget::VRef => match &self.channel0.vref_pin { PinsAdcReadTarget::VREF => {
Channel0VRef::Analog(vref_pin) => { match &self.channel0.vref_pin {
for _ in (0..avg_pt).rev() { Channel0VRef::Analog(vref_pin) => {
sample += self.pins_adc.convert( for _ in (0..avg_pt).rev() {
vref_pin, sample += self
stm32f4xx_hal::adc::config::SampleTime::Cycles_480, .pins_adc
) as u32; .convert(vref_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
} as u32;
sample / avg_pt as u32 }
sample / avg_pt as u32
},
Channel0VRef::Disabled(_) => {2048 as u32}
} }
Channel0VRef::Disabled(_) => 2048_u32, }
},
PinsAdcReadTarget::DacVfb => { PinsAdcReadTarget::DacVfb => {
for _ in (0..avg_pt).rev() { for _ in (0..avg_pt).rev() {
sample += self.pins_adc.convert( sample += self
&self.channel0.dac_feedback_pin, .pins_adc
stm32f4xx_hal::adc::config::SampleTime::Cycles_480, .convert(&self.channel0.dac_feedback_pin,stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
) as u32; as u32;
} }
sample / avg_pt as u32 sample / avg_pt as u32
} }
PinsAdcReadTarget::ITec => { PinsAdcReadTarget::ITec => {
for _ in (0..avg_pt).rev() { for _ in (0..avg_pt).rev() {
sample += self.pins_adc.convert( sample += self
&self.channel0.itec_pin, .pins_adc
stm32f4xx_hal::adc::config::SampleTime::Cycles_480, .convert(&self.channel0.itec_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
) as u32; as u32;
} }
sample / avg_pt as u32 sample / avg_pt as u32
} }
PinsAdcReadTarget::VTec => { PinsAdcReadTarget::VTec => {
for _ in (0..avg_pt).rev() { for _ in (0..avg_pt).rev() {
sample += self.pins_adc.convert( sample += self
&self.channel0.tec_u_meas_pin, .pins_adc
stm32f4xx_hal::adc::config::SampleTime::Cycles_480, .convert(&self.channel0.tec_u_meas_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
) as u32; as u32;
} }
sample / avg_pt as u32 sample / avg_pt as u32
} }
@ -236,42 +211,44 @@ impl Channels {
} }
1 => { 1 => {
sample = match adc_read_target { sample = match adc_read_target {
PinsAdcReadTarget::VRef => match &self.channel1.vref_pin { PinsAdcReadTarget::VREF => {
Channel1VRef::Analog(vref_pin) => { match &self.channel1.vref_pin {
for _ in (0..avg_pt).rev() { Channel1VRef::Analog(vref_pin) => {
sample += self.pins_adc.convert( for _ in (0..avg_pt).rev() {
vref_pin, sample += self
stm32f4xx_hal::adc::config::SampleTime::Cycles_480, .pins_adc
) as u32; .convert(vref_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
} as u32;
sample / avg_pt as u32 }
sample / avg_pt as u32
},
Channel1VRef::Disabled(_) => {2048 as u32}
} }
Channel1VRef::Disabled(_) => 2048_u32, }
},
PinsAdcReadTarget::DacVfb => { PinsAdcReadTarget::DacVfb => {
for _ in (0..avg_pt).rev() { for _ in (0..avg_pt).rev() {
sample += self.pins_adc.convert( sample += self
&self.channel1.dac_feedback_pin, .pins_adc
stm32f4xx_hal::adc::config::SampleTime::Cycles_480, .convert(&self.channel1.dac_feedback_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
) as u32; as u32;
} }
sample / avg_pt as u32 sample / avg_pt as u32
} }
PinsAdcReadTarget::ITec => { PinsAdcReadTarget::ITec => {
for _ in (0..avg_pt).rev() { for _ in (0..avg_pt).rev() {
sample += self.pins_adc.convert( sample += self
&self.channel1.itec_pin, .pins_adc
stm32f4xx_hal::adc::config::SampleTime::Cycles_480, .convert(&self.channel1.itec_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
) as u32; as u32;
} }
sample / avg_pt as u32 sample / avg_pt as u32
} }
PinsAdcReadTarget::VTec => { PinsAdcReadTarget::VTec => {
for _ in (0..avg_pt).rev() { for _ in (0..avg_pt).rev() {
sample += self.pins_adc.convert( sample += self
&self.channel1.tec_u_meas_pin, .pins_adc
stm32f4xx_hal::adc::config::SampleTime::Cycles_480, .convert(&self.channel1.tec_u_meas_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
) as u32; as u32;
} }
sample / avg_pt as u32 sample / avg_pt as u32
} }
@ -279,7 +256,18 @@ impl Channels {
let mv = self.pins_adc.sample_to_millivolts(sample as u16); let mv = self.pins_adc.sample_to_millivolts(sample as u16);
ElectricPotential::new::<millivolt>(mv as f64) ElectricPotential::new::<millivolt>(mv as f64)
} }
_ => unreachable!(), _ => unreachable!()
}
}
pub fn read_dac_feedback_until_stable(&mut self, channel: usize, tolerance: ElectricPotential) -> ElectricPotential {
let mut prev = self.adc_read(channel, PinsAdcReadTarget::DacVfb, 1);
loop {
let current = self.adc_read(channel, PinsAdcReadTarget::DacVfb, 1);
if (current - prev).abs() < tolerance {
return current;
}
prev = current;
} }
} }
@ -287,29 +275,30 @@ impl Channels {
/// ///
/// The thermostat DAC applies a control voltage signal to the CTLI pin of MAX driver chip to control its output current. /// The thermostat DAC applies a control voltage signal to the CTLI pin of MAX driver chip to control its output current.
/// The CTLI input signal is centered around VREF of the MAX chip. Applying VREF to CTLI sets the output current to 0. /// The CTLI input signal is centered around VREF of the MAX chip. Applying VREF to CTLI sets the output current to 0.
/// ///
/// This calibration routine measures the VREF voltage and the DAC output with the STM32 ADC, and uses a breadth-first /// This calibration routine measures the VREF voltage and the DAC output with the STM32 ADC, and uses a breadth-first
/// search to find the DAC setting that will produce a DAC output voltage closest to VREF. This DAC output voltage will /// search to find the DAC setting that will produce a DAC output voltage closest to VREF. This DAC output voltage will
/// be stored and used in subsequent i_set routines to bias the current control signal to the measured VREF, reducing /// be stored and used in subsequent i_set routines to bias the current control signal to the measured VREF, reducing
/// the offset error of the current control signal. /// the offset error of the current control signal.
/// ///
/// The input offset of the STM32 ADC is eliminated by using the same ADC for the measurements, and by only using the /// The input offset of the STM32 ADC is eliminated by using the same ADC for the measurements, and by only using the
/// difference in VREF and DAC output for the calibration. /// difference in VREF and DAC output for the calibration.
/// ///
/// This routine should be called only once after boot, repeated reading of the vref signal and changing of the stored /// This routine should be called only once after boot, repeated reading of the vref signal and changing of the stored
/// VREF measurement can introduce significant noise at the current output, degrading the stabilily performance of the /// VREF measurement can introduce significant noise at the current output, degrading the stabilily performance of the
/// thermostat. /// thermostat.
pub fn calibrate_dac_value(&mut self, channel: usize) { pub fn calibrate_dac_value(&mut self, channel: usize) {
let samples = 50; let samples = 50;
let mut target_voltage = ElectricPotential::new::<volt>(0.0); let mut target_voltage = ElectricPotential::new::<volt>(0.0);
for _ in 0..samples { for _ in 0..samples {
target_voltage += self.get_center(channel); target_voltage = target_voltage + self.get_center(channel);
} }
target_voltage /= samples as f64; target_voltage = target_voltage / samples as f64;
let mut start_value = 1; let mut start_value = 1;
let mut best_error = ElectricPotential::new::<volt>(100.0); let mut best_error = ElectricPotential::new::<volt>(100.0);
for step in (5..18).rev() { for step in (0..18).rev() {
let mut prev_value = start_value;
for value in (start_value..=ad5680::MAX_VALUE).step_by(1 << step) { for value in (start_value..=ad5680::MAX_VALUE).step_by(1 << step) {
match channel { match channel {
0 => { 0 => {
@ -320,23 +309,24 @@ impl Channels {
} }
_ => unreachable!(), _ => unreachable!(),
} }
sleep(10);
let dac_feedback = self.adc_read(channel, PinsAdcReadTarget::DacVfb, 64); let dac_feedback = self.read_dac_feedback_until_stable(channel, ElectricPotential::new::<volt>(0.001));
let error = target_voltage - dac_feedback; let error = target_voltage - dac_feedback;
if error < ElectricPotential::new::<volt>(0.0) { if error < ElectricPotential::new::<volt>(0.0) {
break; break;
} else if error < best_error { } else if error < best_error {
best_error = error; best_error = error;
start_value = value; start_value = prev_value;
let vref = (value as f64 / ad5680::MAX_VALUE as f64) * DAC_OUT_V_MAX; let vref = (value as f64 / ad5680::MAX_VALUE as f64) * ElectricPotential::new::<volt>(DAC_OUT_V_MAX);
match channel { match channel {
0 => self.channel0.vref_meas = vref, 0 => self.channel0.vref_meas = vref,
1 => self.channel1.vref_meas = vref, 1 => self.channel1.vref_meas = vref,
_ => unreachable!(), _ => unreachable!(),
} }
} }
prev_value = value;
} }
} }
@ -362,114 +352,110 @@ 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 { pub fn get_max_v(&mut self, channel: usize) -> ElectricPotential {
self.channel_state(channel).pwm_limits.max_v let max = 4.0 * ElectricPotential::new::<volt>(3.3);
let duty = self.get_pwm(channel, PwmPin::MaxV);
duty * max
} }
pub fn get_max_i_pos(&mut self, channel: usize) -> ElectricCurrent { pub fn get_max_i_pos(&mut self, channel: usize) -> (ElectricCurrent, ElectricCurrent) {
self.channel_state(channel).pwm_limits.max_i_pos let max = ElectricCurrent::new::<ampere>(3.0);
let duty = self.get_pwm(channel, PwmPin::MaxIPos);
(duty * max, max)
} }
pub fn get_max_i_neg(&mut self, channel: usize) -> ElectricCurrent { pub fn get_max_i_neg(&mut self, channel: usize) -> (ElectricCurrent, ElectricCurrent) {
self.channel_state(channel).pwm_limits.max_i_neg let max = ElectricCurrent::new::<ampere>(3.0);
let duty = self.get_pwm(channel, PwmPin::MaxINeg);
(duty * max, max)
} }
// Get current passing through TEC // Get current passing through TEC
pub fn get_tec_i(&mut self, channel: usize) -> ElectricCurrent { pub fn get_tec_i(&mut self, channel: usize) -> ElectricCurrent {
let tec_i = (self.adc_read(channel, PinsAdcReadTarget::ITec, 16) (self.adc_read(channel, PinsAdcReadTarget::ITec, 16) - self.adc_read(channel, PinsAdcReadTarget::VREF, 16)) / ElectricalResistance::new::<ohm>(0.4)
- 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 // Get voltage across TEC
pub fn get_tec_v(&mut self, channel: usize) -> ElectricPotential { pub fn get_tec_v(&mut self, channel: usize) -> ElectricPotential {
(self.adc_read(channel, PinsAdcReadTarget::VTec, 16) - ElectricPotential::new::<volt>(1.5)) (self.adc_read(channel, PinsAdcReadTarget::VTec, 16) - ElectricPotential::new::<volt>(1.5)) * 4.0
* 4.0
} }
fn set_pwm(&mut self, channel: usize, pin: PwmPin, duty: f64) -> f64 { fn set_pwm(&mut self, channel: usize, pin: PwmPin, duty: f64) -> f64 {
fn set<P: hal::PwmPin<Duty = u16>>(pin: &mut P, duty: f64) -> f64 { fn set<P: hal::PwmPin<Duty=u16>>(pin: &mut P, duty: f64) -> f64 {
let max = pin.get_max_duty(); let max = pin.get_max_duty();
let value = ((duty * (max as f64)) as u16).min(max); let value = ((duty * (max as f64)) as u16).min(max);
pin.set_duty(value); pin.set_duty(value);
value as f64 / (max as f64) value as f64 / (max as f64)
} }
match (channel, pin) { match (channel, pin) {
(_, PwmPin::ISet) => panic!("i_set is no pwm pin"), (_, PwmPin::ISet) =>
(0, PwmPin::MaxIPos) => set(&mut self.pwm.max_i_pos0, duty), panic!("i_set is no pwm pin"),
(0, PwmPin::MaxINeg) => set(&mut self.pwm.max_i_neg0, duty), (0, PwmPin::MaxIPos) =>
(0, PwmPin::MaxV) => set(&mut self.pwm.max_v0, duty), set(&mut self.pwm.max_i_pos0, duty),
(1, PwmPin::MaxIPos) => set(&mut self.pwm.max_i_pos1, duty), (0, PwmPin::MaxINeg) =>
(1, PwmPin::MaxINeg) => set(&mut self.pwm.max_i_neg1, duty), set(&mut self.pwm.max_i_neg0, duty),
(1, PwmPin::MaxV) => set(&mut self.pwm.max_v1, duty), (0, PwmPin::MaxV) =>
_ => unreachable!(), 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( pub fn set_max_v(&mut self, channel: usize, max_v: ElectricPotential) -> (ElectricPotential, ElectricPotential) {
&mut self,
channel: usize,
max_v: ElectricPotential,
) -> (ElectricPotential, ElectricPotential) {
let max = 4.0 * ElectricPotential::new::<volt>(3.3); let max = 4.0 * ElectricPotential::new::<volt>(3.3);
let max_v = max_v.min(MAX_TEC_V).max(ElectricPotential::zero());
let duty = (max_v / max).get::<ratio>(); let duty = (max_v / max).get::<ratio>();
let duty = self.set_pwm(channel, PwmPin::MaxV, duty); let duty = self.set_pwm(channel, PwmPin::MaxV, duty);
self.channel_state(channel).pwm_limits.max_v = max_v;
(duty * max, max) (duty * max, max)
} }
pub fn set_max_i_pos( pub fn set_max_i_pos(&mut self, channel: usize, max_i_pos: ElectricCurrent) -> (ElectricCurrent, ElectricCurrent) {
&mut self,
channel: usize,
max_i_pos: ElectricCurrent,
) -> (ElectricCurrent, ElectricCurrent) {
let max = ElectricCurrent::new::<ampere>(3.0); let max = ElectricCurrent::new::<ampere>(3.0);
let max_i_pos = max_i_pos.min(MAX_TEC_I).max(ElectricCurrent::zero()); let duty = (max_i_pos / max).get::<ratio>();
let duty = (max_i_pos / MAX_TEC_I_DUTY_TO_CURRENT_RATE).get::<ratio>(); let duty = self.set_pwm(channel, PwmPin::MaxIPos, duty);
let duty = match self.channel_state(channel).polarity { (duty * max, max)
Polarity::Normal => self.set_pwm(channel, PwmPin::MaxIPos, duty),
Polarity::Reversed => self.set_pwm(channel, PwmPin::MaxINeg, duty),
};
self.channel_state(channel).pwm_limits.max_i_pos = max_i_pos;
(duty * MAX_TEC_I_DUTY_TO_CURRENT_RATE, max)
} }
pub fn set_max_i_neg( pub fn set_max_i_neg(&mut self, channel: usize, max_i_neg: ElectricCurrent) -> (ElectricCurrent, ElectricCurrent) {
&mut self,
channel: usize,
max_i_neg: ElectricCurrent,
) -> (ElectricCurrent, ElectricCurrent) {
let max = ElectricCurrent::new::<ampere>(3.0); let max = ElectricCurrent::new::<ampere>(3.0);
let max_i_neg = max_i_neg.min(MAX_TEC_I).max(ElectricCurrent::zero()); let duty = (max_i_neg / max).get::<ratio>();
let duty = (max_i_neg / MAX_TEC_I_DUTY_TO_CURRENT_RATE).get::<ratio>(); let duty = self.set_pwm(channel, PwmPin::MaxINeg, duty);
let duty = match self.channel_state(channel).polarity { (duty * max, max)
Polarity::Normal => self.set_pwm(channel, PwmPin::MaxINeg, duty),
Polarity::Reversed => self.set_pwm(channel, PwmPin::MaxIPos, duty),
};
self.channel_state(channel).pwm_limits.max_i_neg = max_i_neg;
(duty * MAX_TEC_I_DUTY_TO_CURRENT_RATE, max)
}
pub fn set_polarity(&mut self, channel: usize, polarity: Polarity) {
if self.channel_state(channel).polarity != polarity {
let i_set = self.channel_state(channel).i_set;
let max_i_pos = self.get_max_i_pos(channel);
let max_i_neg = self.get_max_i_neg(channel);
self.channel_state(channel).polarity = polarity;
self.set_i(channel, i_set);
self.set_max_i_pos(channel, max_i_pos);
self.set_max_i_neg(channel, max_i_neg);
}
} }
fn report(&mut self, channel: usize) -> Report { fn report(&mut self, channel: usize) -> Report {
let i_set = self.get_i_set(channel); let i_set = self.get_i(channel);
let i_tec = self.adc_read(channel, PinsAdcReadTarget::ITec, 16); let i_tec = self.adc_read(channel, PinsAdcReadTarget::ITec, 16);
let tec_i = self.get_tec_i(channel); let tec_i = self.get_tec_i(channel);
let dac_value = self.get_dac(channel); let dac_value = self.get_dac(channel);
@ -481,8 +467,7 @@ impl Channels {
interval: state.get_adc_interval(), interval: state.get_adc_interval(),
adc: state.get_adc(), adc: state.get_adc(),
sens: state.get_sens(), sens: state.get_sens(),
temperature: state temperature: state.get_temperature()
.get_temperature()
.map(|temperature| temperature.get::<degree_celsius>()), .map(|temperature| temperature.get::<degree_celsius>()),
pid_engaged: state.pid_engaged, pid_engaged: state.pid_engaged,
i_set, i_set,
@ -520,31 +505,27 @@ impl Channels {
false false
} }
fn output_summary(&mut self, channel: usize) -> OutputSummary { fn pwm_summary(&mut self, channel: usize) -> PwmSummary {
OutputSummary { PwmSummary {
channel, channel,
center: CenterPointJson(self.channel_state(channel).center.clone()), center: CenterPointJson(self.channel_state(channel).center.clone()),
i_set: self.get_i_set(channel), i_set: (self.get_i(channel), ElectricCurrent::new::<ampere>(3.0)).into(),
max_v: self.get_max_v(channel), max_v: (self.get_max_v(channel), ElectricPotential::new::<volt>(5.0)).into(),
max_i_pos: self.get_max_i_pos(channel), max_i_pos: self.get_max_i_pos(channel).into(),
max_i_neg: self.get_max_i_neg(channel), max_i_neg: self.get_max_i_neg(channel).into(),
polarity: PolarityJson(self.channel_state(channel).polarity.clone()),
} }
} }
pub fn output_summaries_json(&mut self) -> Result<JsonBuffer, serde_json_core::ser::Error> { pub fn pwm_summaries_json(&mut self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
let mut summaries = Vec::<_, U2>::new(); let mut summaries = Vec::<_, U2>::new();
for channel in 0..CHANNELS { for channel in 0..CHANNELS {
let _ = summaries.push(self.output_summary(channel)); let _ = summaries.push(self.pwm_summary(channel));
} }
serde_json_core::to_vec(&summaries) serde_json_core::to_vec(&summaries)
} }
fn postfilter_summary(&mut self, channel: usize) -> PostFilterSummary { fn postfilter_summary(&mut self, channel: usize) -> PostFilterSummary {
let rate = self let rate = self.adc.get_postfilter(channel as u8).unwrap()
.adc
.get_postfilter(channel as u8)
.unwrap()
.and_then(|filter| filter.output_rate()); .and_then(|filter| filter.output_rate());
PostFilterSummary { channel, rate } PostFilterSummary { channel, rate }
} }
@ -557,26 +538,23 @@ impl Channels {
serde_json_core::to_vec(&summaries) serde_json_core::to_vec(&summaries)
} }
fn b_parameter_summary(&mut self, channel: usize) -> BParameterSummary { fn steinhart_hart_summary(&mut self, channel: usize) -> SteinhartHartSummary {
let params = self.channel_state(channel).bp.clone(); let params = self.channel_state(channel).sh.clone();
BParameterSummary { channel, params } SteinhartHartSummary { channel, params }
} }
pub fn b_parameter_summaries_json( pub fn steinhart_hart_summaries_json(&mut self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
&mut self,
) -> Result<JsonBuffer, serde_json_core::ser::Error> {
let mut summaries = Vec::<_, U2>::new(); let mut summaries = Vec::<_, U2>::new();
for channel in 0..CHANNELS { for channel in 0..CHANNELS {
let _ = summaries.push(self.b_parameter_summary(channel)); let _ = summaries.push(self.steinhart_hart_summary(channel));
} }
serde_json_core::to_vec(&summaries) serde_json_core::to_vec(&summaries)
} }
pub fn current_abs_max_tec_i(&mut self) -> ElectricCurrent { pub fn current_abs_max_tec_i(&mut self) -> ElectricCurrent {
(0..CHANNELS) max_by(self.get_tec_i(0).abs(),
.map(|channel| self.get_tec_i(channel).abs()) self.get_tec_i(1).abs(),
.max_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal)) |a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal))
.unwrap()
} }
} }
@ -607,36 +585,34 @@ impl Serialize for CenterPointJson {
S: Serializer, S: Serializer,
{ {
match self.0 { match self.0 {
CenterPoint::VRef => serializer.serialize_str("vref"), CenterPoint::Vref =>
CenterPoint::Override(vref) => serializer.serialize_f32(vref), serializer.serialize_str("vref"),
CenterPoint::Override(vref) =>
serializer.serialize_f32(vref),
} }
} }
} }
pub struct PolarityJson(Polarity); #[derive(Serialize)]
pub struct PwmSummaryField<T: Serialize> {
value: T,
max: T,
}
// used in JSON encoding, not for config impl<T: Serialize> From<(T, T)> for PwmSummaryField<T> {
impl Serialize for PolarityJson { fn from((value, max): (T, T)) -> Self {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> PwmSummaryField { value, max }
where
S: Serializer,
{
serializer.serialize_str(match self.0 {
Polarity::Normal => "normal",
Polarity::Reversed => "reversed",
})
} }
} }
#[derive(Serialize)] #[derive(Serialize)]
pub struct OutputSummary { pub struct PwmSummary {
channel: usize, channel: usize,
center: CenterPointJson, center: CenterPointJson,
i_set: ElectricCurrent, i_set: PwmSummaryField<ElectricCurrent>,
max_v: ElectricPotential, max_v: PwmSummaryField<ElectricPotential>,
max_i_pos: ElectricCurrent, max_i_pos: PwmSummaryField<ElectricCurrent>,
max_i_neg: ElectricCurrent, max_i_neg: PwmSummaryField<ElectricCurrent>,
polarity: PolarityJson,
} }
#[derive(Serialize)] #[derive(Serialize)]
@ -646,7 +622,7 @@ pub struct PostFilterSummary {
} }
#[derive(Serialize)] #[derive(Serialize)]
pub struct BParameterSummary { pub struct SteinhartHartSummary {
channel: usize, channel: usize,
params: b_parameter::Parameters, params: steinhart_hart::Parameters,
} }

View File

@ -1,30 +1,45 @@
use smoltcp::socket::TcpSocket;
use log::{error, warn};
use core::fmt::Write;
use heapless::{consts::U1024, Vec};
use super::{ use super::{
ad7172, net,
channels::{Channels, CHANNELS},
command_parser::{ command_parser::{
BpParameter, CenterPoint, Command, Ipv4Config, PidParameter, Polarity, PwmPin, ShowCommand, Ipv4Config,
Command,
ShowCommand,
CenterPoint,
PidParameter,
PwmPin,
ShParameter
},
ad7172,
CHANNEL_CONFIG_KEY,
channels::{
Channels,
CHANNELS
}, },
config::ChannelConfig, config::ChannelConfig,
dfu, dfu,
flash_store::FlashStore, flash_store::FlashStore,
session::Session,
FanCtrl,
hw_rev::HWRev, 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::{ use uom::{
electric_current::ampere, si::{
electric_potential::volt, f64::{
electrical_resistance::ohm, ElectricCurrent,
f64::{ ElectricPotential,
ElectricCurrent, ElectricPotential, ElectricalResistance, TemperatureInterval, ElectricalResistance,
ThermodynamicTemperature, ThermodynamicTemperature,
},
electric_current::ampere,
electric_potential::volt,
electrical_resistance::ohm,
thermodynamic_temperature::degree_celsius,
}, },
temperature_interval::kelvin,
thermodynamic_temperature::degree_celsius,
}; };
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
@ -37,9 +52,9 @@ pub enum Handler {
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub enum Error { pub enum Error {
Report, ReportError,
PostFilterRate, PostFilterRateError,
Flash, FlashError
} }
pub type JsonBuffer = Vec<u8, U1024>; pub type JsonBuffer = Vec<u8, U1024>;
@ -51,19 +66,19 @@ fn send_line(socket: &mut TcpSocket, data: &[u8]) -> bool {
// instead of sending incomplete line // instead of sending incomplete line
warn!( warn!(
"TCP socket has only {}/{} needed {}", "TCP socket has only {}/{} needed {}",
send_free + 1, send_free + 1, socket.send_capacity(), data.len(),
socket.send_capacity(),
data.len(),
); );
} else { } else {
match socket.send_slice(data) { match socket.send_slice(&data) {
Ok(sent) if sent == data.len() => { Ok(sent) if sent == data.len() => {
let _ = socket.send_slice(b"\n"); let _ = socket.send_slice(b"\n");
// success // success
return true; return true
} }
Ok(sent) => warn!("sent only {}/{} bytes", sent, data.len()), Ok(sent) =>
Err(e) => error!("error sending line: {:?}", e), warn!("sent only {}/{} bytes", sent, data.len()),
Err(e) =>
error!("error sending line: {:?}", e),
} }
} }
// not success // not success
@ -71,6 +86,17 @@ fn send_line(socket: &mut TcpSocket, data: &[u8]) -> bool {
} }
impl Handler { impl Handler {
fn reporting(socket: &mut TcpSocket) -> Result<Handler, Error> {
send_line(socket, b"{}");
Ok(Handler::Handled)
}
fn show_report_mode(socket: &mut TcpSocket, session: &Session) -> Result<Handler, Error> {
let _ = writeln!(socket, "{{ \"report\": {:?} }}", session.reporting());
Ok(Handler::Handled)
}
fn show_report(socket: &mut TcpSocket, channels: &mut Channels) -> Result<Handler, Error> { fn show_report(socket: &mut TcpSocket, channels: &mut Channels) -> Result<Handler, Error> {
match channels.reports_json() { match channels.reports_json() {
Ok(buf) => { Ok(buf) => {
@ -79,7 +105,7 @@ impl Handler {
Err(e) => { Err(e) => {
error!("unable to serialize report: {:?}", e); error!("unable to serialize report: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e); let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
return Err(Error::Report); return Err(Error::ReportError);
} }
} }
Ok(Handler::Handled) Ok(Handler::Handled)
@ -93,41 +119,41 @@ impl Handler {
Err(e) => { Err(e) => {
error!("unable to serialize pid summary: {:?}", e); error!("unable to serialize pid summary: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e); let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
return Err(Error::Report); return Err(Error::ReportError);
} }
} }
Ok(Handler::Handled) Ok(Handler::Handled)
} }
fn show_pwm(socket: &mut TcpSocket, channels: &mut Channels) -> Result<Handler, Error> { fn show_pwm(socket: &mut TcpSocket, channels: &mut Channels) -> Result<Handler, Error> {
match channels.output_summaries_json() { match channels.pwm_summaries_json() {
Ok(buf) => { Ok(buf) => {
send_line(socket, &buf); send_line(socket, &buf);
} }
Err(e) => { Err(e) => {
error!("unable to serialize pwm summary: {:?}", e); error!("unable to serialize pwm summary: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e); let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
return Err(Error::Report); return Err(Error::ReportError);
} }
} }
Ok(Handler::Handled) Ok(Handler::Handled)
} }
fn show_b_parameter(socket: &mut TcpSocket, channels: &mut Channels) -> Result<Handler, Error> { fn show_steinhart_hart(socket: &mut TcpSocket, channels: &mut Channels) -> Result<Handler, Error> {
match channels.b_parameter_summaries_json() { match channels.steinhart_hart_summaries_json() {
Ok(buf) => { Ok(buf) => {
send_line(socket, &buf); send_line(socket, &buf);
} }
Err(e) => { Err(e) => {
error!("unable to serialize b parameter summaries: {:?}", e); error!("unable to serialize steinhart-hart summaries: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e); let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
return Err(Error::Report); return Err(Error::ReportError);
} }
} }
Ok(Handler::Handled) Ok(Handler::Handled)
} }
fn show_post_filter(socket: &mut TcpSocket, channels: &mut Channels) -> Result<Handler, Error> { fn show_post_filter (socket: &mut TcpSocket, channels: &mut Channels) -> Result<Handler, Error> {
match channels.postfilter_summaries_json() { match channels.postfilter_summaries_json() {
Ok(buf) => { Ok(buf) => {
send_line(socket, &buf); send_line(socket, &buf);
@ -135,13 +161,13 @@ impl Handler {
Err(e) => { Err(e) => {
error!("unable to serialize postfilter summary: {:?}", e); error!("unable to serialize postfilter summary: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e); let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
return Err(Error::Report); return Err(Error::ReportError);
} }
} }
Ok(Handler::Handled) Ok(Handler::Handled)
} }
fn show_ipv4(socket: &mut TcpSocket, ipv4_config: &mut Ipv4Config) -> Result<Handler, Error> { fn show_ipv4 (socket: &mut TcpSocket, ipv4_config: &mut Ipv4Config) -> Result<Handler, Error> {
let (cidr, gateway) = net::split_ipv4_config(ipv4_config.clone()); let (cidr, gateway) = net::split_ipv4_config(ipv4_config.clone());
let _ = write!(socket, "{{\"addr\":\"{}\"", cidr); let _ = write!(socket, "{{\"addr\":\"{}\"", cidr);
gateway.map(|gateway| write!(socket, ",\"gateway\":\"{}\"", gateway)); gateway.map(|gateway| write!(socket, ",\"gateway\":\"{}\"", gateway));
@ -149,34 +175,13 @@ impl Handler {
Ok(Handler::Handled) Ok(Handler::Handled)
} }
fn engage_pid( fn engage_pid (socket: &mut TcpSocket, channels: &mut Channels, channel: usize) -> Result<Handler, Error> {
socket: &mut TcpSocket,
channels: &mut Channels,
channel: usize,
) -> Result<Handler, Error> {
channels.channel_state(channel).pid_engaged = true; channels.channel_state(channel).pid_engaged = true;
send_line(socket, b"{}"); send_line(socket, b"{}");
Ok(Handler::Handled) Ok(Handler::Handled)
} }
fn set_polarity( fn set_pwm (socket: &mut TcpSocket, channels: &mut Channels, channel: usize, pin: PwmPin, value: f64) -> Result<Handler, Error> {
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 { match pin {
PwmPin::ISet => { PwmPin::ISet => {
channels.channel_state(channel).pid_engaged = false; channels.channel_state(channel).pid_engaged = false;
@ -201,13 +206,8 @@ impl Handler {
Ok(Handler::Handled) Ok(Handler::Handled)
} }
fn set_center_point( fn set_center_point(socket: &mut TcpSocket, channels: &mut Channels, channel: usize, center: CenterPoint) -> Result<Handler, Error> {
socket: &mut TcpSocket, let i_set = channels.get_i(channel);
channels: &mut Channels,
channel: usize,
center: CenterPoint,
) -> Result<Handler, Error> {
let i_set = channels.get_i_set(channel);
let state = channels.channel_state(channel); let state = channels.channel_state(channel);
state.center = center; state.center = center;
if !state.pid_engaged { if !state.pid_engaged {
@ -217,91 +217,65 @@ impl Handler {
Ok(Handler::Handled) Ok(Handler::Handled)
} }
fn set_pid( fn set_pid (socket: &mut TcpSocket, channels: &mut Channels, channel: usize, parameter: PidParameter, value: f64) -> Result<Handler, Error> {
socket: &mut TcpSocket,
channels: &mut Channels,
channel: usize,
parameter: PidParameter,
value: f64,
) -> Result<Handler, Error> {
let pid = &mut channels.channel_state(channel).pid; let pid = &mut channels.channel_state(channel).pid;
use super::command_parser::PidParameter::*; use super::command_parser::PidParameter::*;
match parameter { match parameter {
Target => pid.target = value, Target =>
KP => pid.parameters.kp = value as f32, pid.target = value,
KI => pid.update_ki(value as f32), KP =>
KD => pid.parameters.kd = value as f32, pid.parameters.kp = value as f32,
OutputMin => pid.parameters.output_min = value as f32, KI =>
OutputMax => pid.parameters.output_max = value as f32, 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"{}"); send_line(socket, b"{}");
Ok(Handler::Handled) Ok(Handler::Handled)
} }
fn set_b_parameter( fn set_steinhart_hart (socket: &mut TcpSocket, channels: &mut Channels, channel: usize, parameter: ShParameter, value: f64) -> Result<Handler, Error> {
socket: &mut TcpSocket, let sh = &mut channels.channel_state(channel).sh;
channels: &mut Channels, use super::command_parser::ShParameter::*;
channel: usize,
parameter: BpParameter,
value: f64,
) -> Result<Handler, Error> {
let bp = &mut channels.channel_state(channel).bp;
use super::command_parser::BpParameter::*;
match parameter { match parameter {
T0 => bp.t0 = ThermodynamicTemperature::new::<degree_celsius>(value), T0 => sh.t0 = ThermodynamicTemperature::new::<degree_celsius>(value),
B => bp.b = TemperatureInterval::new::<kelvin>(value), B => sh.b = value,
R0 => bp.r0 = ElectricalResistance::new::<ohm>(value), R0 => sh.r0 = ElectricalResistance::new::<ohm>(value),
} }
send_line(socket, b"{}"); send_line(socket, b"{}");
Ok(Handler::Handled) Ok(Handler::Handled)
} }
fn reset_post_filter( fn reset_post_filter (socket: &mut TcpSocket, channels: &mut Channels, channel: usize) -> Result<Handler, Error> {
socket: &mut TcpSocket,
channels: &mut Channels,
channel: usize,
) -> Result<Handler, Error> {
channels.adc.set_postfilter(channel as u8, None).unwrap(); channels.adc.set_postfilter(channel as u8, None).unwrap();
send_line(socket, b"{}"); send_line(socket, b"{}");
Ok(Handler::Handled) Ok(Handler::Handled)
} }
fn set_post_filter( fn set_post_filter (socket: &mut TcpSocket, channels: &mut Channels, channel: usize, rate: f32) -> Result<Handler, Error> {
socket: &mut TcpSocket,
channels: &mut Channels,
channel: usize,
rate: f32,
) -> Result<Handler, Error> {
let filter = ad7172::PostFilter::closest(rate); let filter = ad7172::PostFilter::closest(rate);
match filter { match filter {
Some(filter) => { Some(filter) => {
channels channels.adc.set_postfilter(channel as u8, Some(filter)).unwrap();
.adc
.set_postfilter(channel as u8, Some(filter))
.unwrap();
send_line(socket, b"{}"); send_line(socket, b"{}");
} }
None => { None => {
error!("unable to choose postfilter for rate {:.3}", rate); error!("unable to choose postfilter for rate {:.3}", rate);
send_line( send_line(socket, b"{{\"error\": \"unable to choose postfilter rate\"}}");
socket, return Err(Error::PostFilterRateError);
b"{{\"error\": \"unable to choose postfilter rate\"}}",
);
return Err(Error::PostFilterRate);
} }
} }
Ok(Handler::Handled) Ok(Handler::Handled)
} }
fn load_channel( fn load_channel (socket: &mut TcpSocket, channels: &mut Channels, store: &mut FlashStore, channel: Option<usize>) -> Result<Handler, Error> {
socket: &mut TcpSocket, for c in 0..CHANNELS {
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) { if channel.is_none() || channel == Some(c) {
match store.read_value::<ChannelConfig>(key) { match store.read_value::<ChannelConfig>(CHANNEL_CONFIG_KEY[c]) {
Ok(Some(config)) => { Ok(Some(config)) => {
config.apply(channels, c); config.apply(channels, c);
send_line(socket, b"{}"); send_line(socket, b"{}");
@ -313,7 +287,7 @@ impl Handler {
Err(e) => { Err(e) => {
error!("unable to load config from flash: {:?}", e); error!("unable to load config from flash: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e); let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
return Err(Error::Flash); return Err(Error::FlashError);
} }
} }
} }
@ -321,24 +295,19 @@ impl Handler {
Ok(Handler::Handled) Ok(Handler::Handled)
} }
fn save_channel( fn save_channel (socket: &mut TcpSocket, channels: &mut Channels, channel: Option<usize>, store: &mut FlashStore) -> Result<Handler, Error> {
socket: &mut TcpSocket, for c in 0..CHANNELS {
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]; let mut store_value_buf = [0u8; 256];
if channel.is_none() || channel == Some(c) { if channel.is_none() || channel == Some(c) {
let config = ChannelConfig::new(channels, c); let config = ChannelConfig::new(channels, c);
match store.write_value(key, &config, &mut store_value_buf) { match store.write_value(CHANNEL_CONFIG_KEY[c], &config, &mut store_value_buf) {
Ok(()) => { Ok(()) => {
send_line(socket, b"{}"); send_line(socket, b"{}");
} }
Err(e) => { Err(e) => {
error!("unable to save channel {} config to flash: {:?}", c, e); error!("unable to save channel {} config to flash: {:?}", c, e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e); let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
return Err(Error::Flash); return Err(Error::FlashError);
} }
} }
} }
@ -346,11 +315,7 @@ impl Handler {
Ok(Handler::Handled) Ok(Handler::Handled)
} }
fn set_ipv4( fn set_ipv4 (socket: &mut TcpSocket, store: &mut FlashStore, config: Ipv4Config) -> Result<Handler, Error> {
socket: &mut TcpSocket,
store: &mut FlashStore,
config: Ipv4Config,
) -> Result<Handler, Error> {
let _ = store let _ = store
.write_value("ipv4", &config, [0; 16]) .write_value("ipv4", &config, [0; 16])
.map_err(|e| error!("unable to save ipv4 config to flash: {:?}", e)); .map_err(|e| error!("unable to save ipv4 config to flash: {:?}", e));
@ -359,7 +324,7 @@ impl Handler {
Ok(Handler::NewIPV4(new_ipv4_config.unwrap())) Ok(Handler::NewIPV4(new_ipv4_config.unwrap()))
} }
fn reset(channels: &mut Channels) -> Result<Handler, Error> { fn reset (channels: &mut Channels) -> Result<Handler, Error> {
for i in 0..CHANNELS { for i in 0..CHANNELS {
channels.power_down(i); channels.power_down(i);
} }
@ -367,7 +332,7 @@ impl Handler {
Ok(Handler::Reset) Ok(Handler::Reset)
} }
fn dfu(channels: &mut Channels) -> Result<Handler, Error> { fn dfu (channels: &mut Channels) -> Result<Handler, Error> {
for i in 0..CHANNELS { for i in 0..CHANNELS {
channels.power_down(i); channels.power_down(i);
} }
@ -378,16 +343,9 @@ impl Handler {
Ok(Handler::Reset) Ok(Handler::Reset)
} }
fn set_fan( fn set_fan(socket: &mut TcpSocket, fan_pwm: u32, fan_ctrl: &mut FanCtrl) -> Result<Handler, Error> {
socket: &mut TcpSocket,
fan_pwm: u32,
fan_ctrl: &mut FanCtrl,
) -> Result<Handler, Error> {
if !fan_ctrl.fan_available() { if !fan_ctrl.fan_available() {
send_line( send_line(socket, b"{ \"warning\": \"this thermostat doesn't have fan!\" }");
socket,
b"{ \"warning\": \"this thermostat doesn't have a fan!\" }",
);
return Ok(Handler::Handled); return Ok(Handler::Handled);
} }
fan_ctrl.set_auto_mode(false); fan_ctrl.set_auto_mode(false);
@ -409,17 +367,14 @@ impl Handler {
Err(e) => { Err(e) => {
error!("unable to serialize fan summary: {:?}", e); error!("unable to serialize fan summary: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e); let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
Err(Error::Report) Err(Error::ReportError)
} }
} }
} }
fn fan_auto(socket: &mut TcpSocket, fan_ctrl: &mut FanCtrl) -> Result<Handler, Error> { fn fan_auto(socket: &mut TcpSocket, fan_ctrl: &mut FanCtrl) -> Result<Handler, Error> {
if !fan_ctrl.fan_available() { if !fan_ctrl.fan_available() {
send_line( send_line(socket, b"{ \"warning\": \"this thermostat doesn't have fan!\" }");
socket,
b"{ \"warning\": \"this thermostat doesn't have a fan!\" }",
);
return Ok(Handler::Handled); return Ok(Handler::Handled);
} }
fan_ctrl.set_auto_mode(true); fan_ctrl.set_auto_mode(true);
@ -431,13 +386,7 @@ impl Handler {
Ok(Handler::Handled) Ok(Handler::Handled)
} }
fn fan_curve( fn fan_curve(socket: &mut TcpSocket, fan_ctrl: &mut FanCtrl, k_a: f32, k_b: f32, k_c: f32) -> Result<Handler, Error> {
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); fan_ctrl.set_curve(k_a, k_b, k_c);
send_line(socket, b"{}"); send_line(socket, b"{}");
Ok(Handler::Handled) Ok(Handler::Handled)
@ -458,71 +407,40 @@ impl Handler {
Err(e) => { Err(e) => {
error!("unable to serialize HWRev summary: {:?}", e); error!("unable to serialize HWRev summary: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e); let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
Err(Error::Report) Err(Error::ReportError)
} }
} }
} }
pub fn handle_command( pub fn handle_command(command: Command, socket: &mut TcpSocket, channels: &mut Channels, session: &Session, store: &mut FlashStore, ipv4_config: &mut Ipv4Config, fan_ctrl: &mut FanCtrl, hwrev: HWRev) -> Result<Self, Error> {
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 { match command {
Command::Quit => Ok(Handler::CloseSocket), Command::Quit => Ok(Handler::CloseSocket),
Command::Reporting(_reporting) => Handler::reporting(socket),
Command::Show(ShowCommand::Reporting) => Handler::show_report_mode(socket, session),
Command::Show(ShowCommand::Input) => Handler::show_report(socket, channels), Command::Show(ShowCommand::Input) => Handler::show_report(socket, channels),
Command::Show(ShowCommand::Pid) => Handler::show_pid(socket, channels), Command::Show(ShowCommand::Pid) => Handler::show_pid(socket, channels),
Command::Show(ShowCommand::Output) => Handler::show_pwm(socket, channels), Command::Show(ShowCommand::Pwm) => Handler::show_pwm(socket, channels),
Command::Show(ShowCommand::BParameter) => Handler::show_b_parameter(socket, channels), Command::Show(ShowCommand::SteinhartHart) => Handler::show_steinhart_hart(socket, channels),
Command::Show(ShowCommand::PostFilter) => Handler::show_post_filter(socket, channels), Command::Show(ShowCommand::PostFilter) => Handler::show_post_filter(socket, channels),
Command::Show(ShowCommand::Ipv4) => Handler::show_ipv4(socket, ipv4_config), Command::Show(ShowCommand::Ipv4) => Handler::show_ipv4(socket, ipv4_config),
Command::OutputPid { channel } => Handler::engage_pid(socket, channels, channel), Command::PwmPid { channel } => Handler::engage_pid(socket, channels, channel),
Command::OutputPolarity { channel, polarity } => { Command::Pwm { channel, pin, value } => Handler::set_pwm(socket, channels, channel, pin, value),
Handler::set_polarity(socket, channels, channel, polarity) 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::Output { Command::SteinhartHart { channel, parameter, value } => Handler::set_steinhart_hart(socket, channels, channel, parameter, value),
channel, Command::PostFilter { channel, rate: None } => Handler::reset_post_filter(socket, channels, channel),
pin, Command::PostFilter { channel, rate: Some(rate) } => Handler::set_post_filter(socket, channels, channel, rate),
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::Load { channel } => Handler::load_channel(socket, channels, store, channel),
Command::Save { channel } => Handler::save_channel(socket, channels, channel, store), Command::Save { channel } => Handler::save_channel(socket, channels, channel, store),
Command::Ipv4(config) => Handler::set_ipv4(socket, store, config), Command::Ipv4(config) => Handler::set_ipv4(socket, store, config),
Command::Reset => Handler::reset(channels), Command::Reset => Handler::reset(channels),
Command::Dfu => Handler::dfu(channels), Command::Dfu => Handler::dfu(channels),
Command::FanSet { fan_pwm } => Handler::set_fan(socket, fan_pwm, fan_ctrl), Command::FanSet {fan_pwm} => Handler::set_fan(socket, fan_pwm, fan_ctrl),
Command::ShowFan => Handler::show_fan(socket, fan_ctrl), Command::ShowFan => Handler::show_fan(socket, fan_ctrl),
Command::FanAuto => Handler::fan_auto(socket, fan_ctrl), Command::FanAuto => Handler::fan_auto(socket, fan_ctrl),
Command::FanCurve { 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),
Handler::fan_curve(socket, fan_ctrl, k_a, k_b, k_c)
}
Command::FanCurveDefaults => Handler::fan_defaults(socket, fan_ctrl), Command::FanCurveDefaults => Handler::fan_defaults(socket, fan_ctrl),
Command::ShowHWRev => Handler::show_hwrev(socket, hwrev), Command::ShowHWRev => Handler::show_hwrev(socket, hwrev),
} }
} }
} }

View File

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

View File

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

View File

@ -14,7 +14,7 @@ pub unsafe fn set_dfu_trigger() {
} }
/// Called by reset handler in lib.rs immediately after reset. /// Called by reset handler in lib.rs immediately after reset.
/// This function should not be called outside of reset handler as /// This function should not be called outside of reset handler as
/// bootloader expects MCU to be in reset state when called. /// bootloader expects MCU to be in reset state when called.
#[cfg(target_arch = "arm")] #[cfg(target_arch = "arm")]
#[pre_init] #[pre_init]
@ -27,13 +27,13 @@ unsafe fn __pre_init() {
rcc.apb2enr.modify(|_, w| w.syscfgen().set_bit()); rcc.apb2enr.modify(|_, w| w.syscfgen().set_bit());
// Bypass BOOT pins and remap bootloader to 0x00000000 // Bypass BOOT pins and remap bootloader to 0x00000000
let syscfg = &*SYSCFG::ptr(); let syscfg = &*SYSCFG::ptr() ;
syscfg.memrm.write(|w| w.mem_mode().bits(0b01)); syscfg.memrm.write(|w| w.mem_mode().bits(0b01));
// Impose instruction and memory barriers // Impose instruction and memory barriers
cortex_m::asm::isb(); cortex_m::asm::isb();
cortex_m::asm::dsb(); cortex_m::asm::dsb();
asm!( asm!(
// Set stack pointer to bootloader location // Set stack pointer to bootloader location
"LDR R0, =0x1FFF0000", "LDR R0, =0x1FFF0000",

View File

@ -1,17 +1,25 @@
use crate::{channels::MAX_TEC_I, command_handler::JsonBuffer, hw_rev::HWSettings};
use num_traits::Float; use num_traits::Float;
use serde::Serialize; use serde::Serialize;
use stm32f4xx_hal::{ use stm32f4xx_hal::{
pac::TIM8,
pwm::{self, PwmChannels}, pwm::{self, PwmChannels},
pac::TIM8,
};
use uom::si::{
f64::ElectricCurrent,
electric_current::ampere,
};
use crate::{
hw_rev::HWSettings,
command_handler::JsonBuffer,
channels::MAX_TEC_I,
}; };
use uom::si::{electric_current::ampere, f64::ElectricCurrent};
pub type FanPin = PwmChannels<TIM8, pwm::C4>; pub type FanPin = PwmChannels<TIM8, pwm::C4>;
const MAX_USER_FAN_PWM: f32 = 100.0; const MAX_USER_FAN_PWM: f32 = 100.0;
const MIN_USER_FAN_PWM: f32 = 1.0; const MIN_USER_FAN_PWM: f32 = 1.0;
pub struct FanCtrl { pub struct FanCtrl {
fan: Option<FanPin>, fan: Option<FanPin>,
fan_auto: bool, fan_auto: bool,
@ -46,11 +54,9 @@ impl FanCtrl {
pub fn cycle(&mut self, abs_max_tec_i: ElectricCurrent) { pub fn cycle(&mut self, abs_max_tec_i: ElectricCurrent) {
self.abs_max_tec_i = abs_max_tec_i.get::<ampere>() as f32; self.abs_max_tec_i = abs_max_tec_i.get::<ampere>() as f32;
if self.fan_auto && self.hw_settings.fan_available { if self.fan_auto && self.hw_settings.fan_available {
let scaled_current = self.abs_max_tec_i / MAX_TEC_I.get::<ampere>() as f32; let scaled_current = self.abs_max_tec_i / MAX_TEC_I as f32;
// do not limit upper bound, as it will be limited in the set_pwm() // do not limit upper bound, as it will be limited in the set_pwm()
let pwm = (MAX_USER_FAN_PWM let pwm = (MAX_USER_FAN_PWM * (scaled_current * (scaled_current * self.k_a + self.k_b) + self.k_c)) as u32;
* (scaled_current * (scaled_current * self.k_a + self.k_b) + self.k_c))
as u32;
self.set_pwm(pwm); self.set_pwm(pwm);
} }
} }
@ -83,26 +89,18 @@ impl FanCtrl {
} }
pub fn restore_defaults(&mut self) { pub fn restore_defaults(&mut self) {
self.set_curve( self.set_curve(self.hw_settings.fan_k_a,
self.hw_settings.fan_k_a, self.hw_settings.fan_k_b,
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 { pub fn set_pwm(&mut self, fan_pwm: u32) -> f32 {
if self.fan.is_none() || (!self.pwm_enabled && !self.enable_pwm()) { if self.fan.is_none() || (!self.pwm_enabled && !self.enable_pwm()) {
return 0f32; return 0f32;
} }
let fan = self.fan.as_mut().unwrap(); let fan = self.fan.as_mut().unwrap();
let fan_pwm = fan_pwm.clamp(MIN_USER_FAN_PWM as u32, MAX_USER_FAN_PWM as u32); let fan_pwm = fan_pwm.min(MAX_USER_FAN_PWM as u32).max(MIN_USER_FAN_PWM as u32);
let duty = scale_number( let duty = scale_number(fan_pwm as f32, self.hw_settings.min_fan_pwm, self.hw_settings.max_fan_pwm, MIN_USER_FAN_PWM, MAX_USER_FAN_PWM);
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 max = fan.get_max_duty();
let value = ((duty * (max as f32)) as u16).min(max); let value = ((duty * (max as f32)) as u16).min(max);
fan.set_duty(value); fan.set_duty(value);
@ -121,17 +119,8 @@ impl FanCtrl {
if let Some(fan) = &self.fan { if let Some(fan) = &self.fan {
let duty = fan.get_duty(); let duty = fan.get_duty();
let max = fan.get_max_duty(); let max = fan.get_max_duty();
scale_number( 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
duty as f32 / (max as f32), } else { 0 }
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 { fn enable_pwm(&mut self) -> bool {
@ -147,6 +136,7 @@ impl FanCtrl {
} }
} }
fn scale_number(unscaled: f32, to_min: f32, to_max: f32, from_min: f32, from_max: f32) -> f32 { 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 (to_max - to_min) * (unscaled - from_min) / (from_max - from_min) + to_min
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -8,26 +8,30 @@ use panic_halt as _;
#[cfg(all(feature = "semihosting", not(test)))] #[cfg(all(feature = "semihosting", not(test)))]
use panic_semihosting as _; use panic_semihosting as _;
use log::{error, info, warn};
use cortex_m::asm::wfi; use cortex_m::asm::wfi;
use cortex_m_rt::entry; use cortex_m_rt::entry;
use log::{error, info, warn};
use smoltcp::{socket::TcpSocket, time::Instant, wire::EthernetAddress};
use stm32f4xx_hal::{ use stm32f4xx_hal::{
hal::watchdog::{Watchdog, WatchdogEnable}, hal::watchdog::{WatchdogEnable, Watchdog},
rcc::RccExt, rcc::RccExt,
stm32::{CorePeripherals, Peripherals, SCB}, stm32::{CorePeripherals, Peripherals, SCB},
time::{MegaHertz, U32Ext}, time::{U32Ext, MegaHertz},
watchdog::IndependentWatchdog, watchdog::IndependentWatchdog,
}; };
use smoltcp::{
time::Instant,
socket::TcpSocket,
wire::EthernetAddress,
};
mod init_log; mod init_log;
use init_log::init_log; use init_log::init_log;
mod usb;
mod leds; mod leds;
mod pins; mod pins;
mod usb;
use pins::Pins; use pins::Pins;
mod ad5680;
mod ad7172; mod ad7172;
mod ad5680;
mod net; mod net;
mod server; mod server;
use server::Server; use server::Server;
@ -35,18 +39,18 @@ mod session;
use session::{Session, SessionInput}; use session::{Session, SessionInput};
mod command_parser; mod command_parser;
use command_parser::Ipv4Config; use command_parser::Ipv4Config;
mod b_parameter;
mod channels;
mod pid;
mod timer; mod timer;
use channels::{Channels, CHANNELS}; mod pid;
mod steinhart_hart;
mod channels;
use channels::{CHANNELS, Channels};
mod channel; mod channel;
mod channel_state; mod channel_state;
mod config; mod config;
use config::ChannelConfig; use config::ChannelConfig;
mod command_handler;
mod dfu;
mod flash_store; mod flash_store;
mod dfu;
mod command_handler;
use command_handler::Handler; use command_handler::Handler;
mod fan_ctrl; mod fan_ctrl;
use fan_ctrl::FanCtrl; use fan_ctrl::FanCtrl;
@ -69,19 +73,19 @@ fn send_line(socket: &mut TcpSocket, data: &[u8]) -> bool {
// instead of sending incomplete line // instead of sending incomplete line
warn!( warn!(
"TCP socket has only {}/{} needed {}", "TCP socket has only {}/{} needed {}",
send_free + 1, send_free + 1, socket.send_capacity(), data.len(),
socket.send_capacity(),
data.len(),
); );
} else { } else {
match socket.send_slice(data) { match socket.send_slice(&data) {
Ok(sent) if sent == data.len() => { Ok(sent) if sent == data.len() => {
let _ = socket.send_slice(b"\n"); let _ = socket.send_slice(b"\n");
// success // success
return true; return true
} }
Ok(sent) => warn!("sent only {}/{} bytes", sent, data.len()), Ok(sent) =>
Err(e) => error!("error sending line: {:?}", e), warn!("sent only {}/{} bytes", sent, data.len()),
Err(e) =>
error!("error sending line: {:?}", e),
} }
} }
// not success // not success
@ -100,9 +104,7 @@ fn main() -> ! {
cp.SCB.enable_dcache(&mut cp.CPUID); cp.SCB.enable_dcache(&mut cp.CPUID);
let dp = Peripherals::take().unwrap(); let dp = Peripherals::take().unwrap();
let clocks = dp let clocks = dp.RCC.constrain()
.RCC
.constrain()
.cfgr .cfgr
.use_hse(HSE) .use_hse(HSE)
.sysclk(168.mhz()) .sysclk(168.mhz())
@ -118,15 +120,14 @@ fn main() -> ! {
timer::setup(cp.SYST, clocks); timer::setup(cp.SYST, clocks);
let (pins, mut leds, mut eeprom, eth_pins, usb, fan, hwrev, hw_settings) = Pins::setup( let (pins, mut leds, mut eeprom, eth_pins, usb, fan, hwrev, hw_settings) = Pins::setup(
clocks, clocks, dp.TIM1, dp.TIM3, dp.TIM8,
(dp.TIM1, dp.TIM3, dp.TIM8), dp.GPIOA, dp.GPIOB, dp.GPIOC, dp.GPIOD, dp.GPIOE, dp.GPIOF, dp.GPIOG,
(
dp.GPIOA, dp.GPIOB, dp.GPIOC, dp.GPIOD, dp.GPIOE, dp.GPIOF, dp.GPIOG,
),
dp.I2C1, dp.I2C1,
(dp.SPI2, dp.SPI4, dp.SPI5), dp.SPI2, dp.SPI4, dp.SPI5,
dp.ADC1, dp.ADC1,
(dp.OTG_FS_GLOBAL, dp.OTG_FS_DEVICE, dp.OTG_FS_PWRCLK), dp.OTG_FS_GLOBAL,
dp.OTG_FS_DEVICE,
dp.OTG_FS_PWRCLK,
); );
leds.r1.on(); leds.r1.on();
@ -138,11 +139,14 @@ fn main() -> ! {
let mut store = flash_store::store(dp.FLASH); let mut store = flash_store::store(dp.FLASH);
let mut channels = Channels::new(pins); let mut channels = Channels::new(pins);
for (c, key) in CHANNEL_CONFIG_KEY.iter().enumerate().take(CHANNELS) { for c in 0..CHANNELS {
match store.read_value::<ChannelConfig>(key) { match store.read_value::<ChannelConfig>(CHANNEL_CONFIG_KEY[c]) {
Ok(Some(config)) => config.apply(&mut channels, c), Ok(Some(config)) =>
Ok(None) => error!("flash config not found for channel {}", c), config.apply(&mut channels, c),
Err(e) => error!("unable to load config {} from flash: {:?}", c, e), Ok(None) =>
error!("flash config not found for channel {}", c),
Err(e) =>
error!("unable to load config {} from flash: {:?}", c, e),
} }
} }
@ -155,9 +159,11 @@ fn main() -> ! {
gateway: None, gateway: None,
}; };
match store.read_value("ipv4") { match store.read_value("ipv4") {
Ok(Some(config)) => ipv4_config = config, Ok(Some(config)) =>
ipv4_config = config,
Ok(None) => {} 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 // EEPROM ships with a read-only EUI-48 identifier
@ -166,115 +172,118 @@ fn main() -> ! {
let hwaddr = EthernetAddress(eui48); let hwaddr = EthernetAddress(eui48);
info!("EEPROM MAC address: {}", hwaddr); info!("EEPROM MAC address: {}", hwaddr);
net::run( net::run(clocks, dp.ETHERNET_MAC, dp.ETHERNET_DMA, eth_pins, hwaddr, ipv4_config.clone(), |iface| {
clocks, Server::<Session>::run(iface, |server| {
dp.ETHERNET_MAC, leds.r1.off();
dp.ETHERNET_DMA, let mut should_reset = false;
eth_pins,
hwaddr,
ipv4_config.clone(),
|iface| {
Server::<Session>::run(iface, |server| {
leds.r1.off();
let mut should_reset = false;
loop { loop {
let mut new_ipv4_config = None; let mut new_ipv4_config = None;
let instant = Instant::from_millis(i64::from(timer::now())); let instant = Instant::from_millis(i64::from(timer::now()));
channels.poll_adc(instant); let updated_channel = channels.poll_adc(instant);
if let Some(channel) = updated_channel {
server.for_each(|_, session| session.set_report_pending(channel.into()));
}
fan_ctrl.cycle(channels.current_abs_max_tec_i()); fan_ctrl.cycle(channels.current_abs_max_tec_i());
if channels.pid_engaged() { if channels.pid_engaged() {
leds.g3.on(); leds.g3.on();
} else { } else {
leds.g3.off(); leds.g3.off();
} }
let instant = Instant::from_millis(i64::from(timer::now())); let instant = Instant::from_millis(i64::from(timer::now()));
cortex_m::interrupt::free(net::clear_pending); cortex_m::interrupt::free(net::clear_pending);
server.poll(instant).unwrap_or_else(|e| { server.poll(instant)
.unwrap_or_else(|e| {
warn!("poll: {:?}", e); warn!("poll: {:?}", e);
}); });
if !should_reset { if ! should_reset {
// TCP protocol handling // TCP protocol handling
server.for_each(|mut socket, session| { server.for_each(|mut socket, session| {
if !socket.is_active() { if ! socket.is_active() {
let _ = socket.listen(TCP_PORT); let _ = socket.listen(TCP_PORT);
session.reset(); session.reset();
} else if socket.may_send() && !socket.may_recv() { } else if socket.may_send() && !socket.may_recv() {
socket.close() socket.close()
} else if socket.can_send() && socket.can_recv() { } else if socket.can_send() && socket.can_recv() {
match socket.recv(|buf| session.feed(buf)) { match socket.recv(|buf| session.feed(buf)) {
// SessionInput::Nothing happens when the line reader parses a string of characters that is not // SessionInput::Nothing happens when the line reader parses a string of characters that is not
// followed by a newline character. Could be due to partial commands not terminated with newline, // followed by a newline character. Could be due to partial commands not terminated with newline,
// socket RX ring buffer wraps around, or when the command is sent as seperate TCP packets etc. // socket RX ring buffer wraps around, or when the command is sent as seperate TCP packets etc.
// Do nothing and feed more data to the line reader in the next loop cycle. // Do nothing and feed more data to the line reader in the next loop cycle.
Ok(SessionInput::Nothing) => {} Ok(SessionInput::Nothing) => {}
Ok(SessionInput::Command(command)) => { Ok(SessionInput::Command(command)) => {
match Handler::handle_command( match Handler::handle_command(command, &mut socket, &mut channels, session, &mut store, &mut ipv4_config, &mut fan_ctrl, hwrev) {
command, Ok(Handler::NewIPV4(ip)) => new_ipv4_config = Some(ip),
&mut socket, Ok(Handler::Handled) => {},
&mut channels, Ok(Handler::CloseSocket) => socket.close(),
&mut store, Ok(Handler::Reset) => should_reset = true,
&mut ipv4_config, Err(_) => {},
&mut fan_ctrl,
hwrev,
) {
Ok(Handler::NewIPV4(ip)) => new_ipv4_config = Some(ip),
Ok(Handler::Handled) => {}
Ok(Handler::CloseSocket) => socket.close(),
Ok(Handler::Reset) => should_reset = true,
Err(_) => {}
}
} }
Ok(SessionInput::Error(e)) => { }
error!("session input: {:?}", e); Ok(SessionInput::Error(e)) => {
send_line(&mut socket, b"{ \"error\": \"invalid input\" }"); 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(),
} }
} }
});
} else {
// Should reset, close all TCP sockets.
let mut any_socket_alive = false;
server.for_each(|mut socket, _| {
if socket.is_active() {
socket.abort();
any_socket_alive = true;
}
});
// Must let loop run for one more cycle to poll server for RST to be sent,
// this makes sure system does not reset right after socket.abort() is called.
if !any_socket_alive {
SCB::sys_reset();
}
}
// Apply new IPv4 address/gateway
if let Some(config) = new_ipv4_config.take() {
server.set_ipv4_config(config.clone());
ipv4_config = config;
};
// Update watchdog
wd.feed();
leds.g4.off();
cortex_m::interrupt::free(|cs| {
if !net::is_pending(cs) {
// Wait for interrupts
// (Ethernet, SysTick, or USB)
wfi();
} }
}); });
leds.g4.on(); } else {
// Should reset, close all TCP sockets.
let mut any_socket_alive = false;
server.for_each(|mut socket, _| {
if socket.is_active() {
socket.abort();
any_socket_alive = true;
}
});
// Must let loop run for one more cycle to poll server for RST to be sent,
// this makes sure system does not reset right after socket.abort() is called.
if !any_socket_alive {
SCB::sys_reset();
}
} }
});
}, // Apply new IPv4 address/gateway
); new_ipv4_config.take()
.map(|config| {
server.set_ipv4_config(config.clone());
ipv4_config = config;
});
// Update watchdog
wd.feed();
leds.g4.off();
cortex_m::interrupt::free(|cs| {
if !net::is_pending(cs) {
// Wait for interrupts
// (Ethernet, SysTick, or USB)
wfi();
}
});
leds.g4.on();
}
});
});
unreachable!() unreachable!()
} }

View File

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

View File

@ -1,4 +1,4 @@
use serde::{Deserialize, Serialize}; use serde::{Serialize, Deserialize};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Parameters { pub struct Parameters {
@ -29,37 +29,40 @@ impl Default for Parameters {
#[derive(Clone)] #[derive(Clone)]
pub struct Controller { pub struct Controller {
pub parameters: Parameters, pub parameters: Parameters,
pub target: f64, pub target : f64,
u1: f64, u1 : f64,
x1: f64, x1 : f64,
x2: f64, x2 : f64,
pub y1: f64, pub y1 : f64,
} }
impl Controller { impl Controller {
pub const fn new(parameters: Parameters) -> Controller { pub const fn new(parameters: Parameters) -> Controller {
Controller { Controller {
parameters, parameters: parameters,
target: 0.0, target : 0.0,
u1: 0.0, u1 : 0.0,
x1: 0.0, x1 : 0.0,
x2: 0.0, x2 : 0.0,
y1: 0.0, y1 : 0.0,
} }
} }
// Based on https://hackmd.io/IACbwcOTSt6Adj3_F9bKuw PID implementation // Based on https://hackmd.io/IACbwcOTSt6Adj3_F9bKuw PID implementation
// Input x(t), target u(t), output y(t) // Input x(t), target u(t), output y(t)
// y0' = y1 - ki * u0 // y0' = y1 - ki * u0
// + x0 * (kp + ki + kd) // + x0 * (kp + ki + kd)
// - x1 * (kp + 2kd) // - x1 * (kp + 2kd)
// + x2 * kd // + x2 * kd
// + kp * (u0 - u1)
// y0 = clip(y0', ymin, ymax) // y0 = clip(y0', ymin, ymax)
pub fn update(&mut self, input: f64) -> f64 { pub fn update(&mut self, input: f64) -> f64 {
let mut output: f64 = self.y1 - self.target * f64::from(self.parameters.ki) 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) + 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.x1 * f64::from(self.parameters.kp + 2.0 * self.parameters.kd)
+ self.x2 * f64::from(self.parameters.kd); + self.x2 * f64::from(self.parameters.kd)
+ f64::from(self.parameters.kp) * (self.target - self.u1);
if output < self.parameters.output_min.into() { if output < self.parameters.output_min.into() {
output = self.parameters.output_min.into(); output = self.parameters.output_min.into();
} }
@ -69,7 +72,7 @@ impl Controller {
self.x2 = self.x1; self.x2 = self.x1;
self.x1 = input; self.x1 = input;
self.u1 = self.target; self.u1 = self.target;
self.y1 = output; self.y1 = output;
output output
} }
@ -108,17 +111,17 @@ mod test {
#[test] #[test]
fn test_controller() { fn test_controller() {
// Initial and ambient temperature // Initial and ambient temperature
const DEFAULT: f64 = 20.0; const DEFAULT: f64 = 20.0;
// Target temperature // Target temperature
const TARGET: f64 = 40.0; const TARGET: f64 = 40.0;
// Control tolerance // Control tolerance
const ERROR: f64 = 0.01; const ERROR: f64 = 0.01;
// System response delay // System response delay
const DELAY: usize = 10; const DELAY: usize = 10;
// Heat lost // Heat lost
const LOSS: f64 = 0.05; const LOSS: f64 = 0.05;
// Limit simulation cycle, reaching this limit before settling fails test // Limit simulation cycle, reaching this limit before settling fails test
const CYCLE_LIMIT: u32 = 1000; const CYCLE_LIMIT: u32 = 1000;
let mut pid = Controller::new(PARAMETERS.clone()); let mut pid = Controller::new(PARAMETERS.clone());
pid.target = TARGET; pid.target = TARGET;

View File

@ -1,41 +1,49 @@
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::{ use stm32f4xx_hal::{
adc::Adc, adc::Adc,
gpio::{ gpio::{
gpioa::*, gpiob::*, gpioc::*, gpioe::*, gpiof::*, gpiog::*, Alternate, AlternateOD, Analog, AF5, Alternate, AlternateOD, Analog, Floating, Input,
Floating, GpioExt, Input, Output, PushPull, AF5, gpioa::*,
gpiob::*,
gpioc::*,
gpioe::*,
gpiof::*,
gpiog::*,
GpioExt,
Output, PushPull,
}, },
hal::{self, blocking::spi::Transfer, digital::v2::OutputPin}, hal::{self, blocking::spi::Transfer, digital::v2::OutputPin},
i2c::I2c, i2c::I2c,
otg_fs::USB, otg_fs::USB,
pac::{
ADC1, GPIOA, GPIOB, GPIOC, GPIOD, GPIOE, GPIOF, GPIOG, I2C1, OTG_FS_DEVICE, OTG_FS_GLOBAL,
OTG_FS_PWRCLK, SPI2, SPI4, SPI5, TIM1, TIM3, TIM8,
},
pwm::{self, PwmChannels},
rcc::Clocks, rcc::Clocks,
spi::{NoMiso, Spi, TransferModeNormal}, pwm::{self, PwmChannels},
time::U32Ext, 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
},
timer::Timer, timer::Timer,
time::U32Ext,
};
use eeprom24x::{self, Eeprom24x};
use stm32_eth::EthPins;
use crate::{
channel::{Channel0, Channel1},
leds::Leds,
fan_ctrl::FanPin,
hw_rev::{HWRev, HWSettings},
}; };
pub type Eeprom = Eeprom24x< pub type Eeprom = Eeprom24x<
I2c< I2c<I2C1, (
I2C1, PB8<AlternateOD<{ stm32f4xx_hal::gpio::AF4 }>>,
( PB9<AlternateOD<{ stm32f4xx_hal::gpio::AF4 }>>
PB8<AlternateOD<{ stm32f4xx_hal::gpio::AF4 }>>, )>,
PB9<AlternateOD<{ stm32f4xx_hal::gpio::AF4 }>>,
),
>,
eeprom24x::page_size::B8, eeprom24x::page_size::B8,
eeprom24x::addr_size::OneByte, eeprom24x::addr_size::OneByte
>; >;
pub type EthernetPins = EthPins< pub type EthernetPins = EthPins<
@ -46,14 +54,14 @@ pub type EthernetPins = EthPins<
PB13<Input<Floating>>, PB13<Input<Floating>>,
PC4<Input<Floating>>, PC4<Input<Floating>>,
PC5<Input<Floating>>, PC5<Input<Floating>>,
>; >;
pub trait ChannelPins { pub trait ChannelPins {
type DacSpi: Transfer<u8>; type DacSpi: Transfer<u8>;
type DacSync: OutputPin; type DacSync: OutputPin;
type Shdn: OutputPin; type Shdn: OutputPin;
type VRefPin; type VRefPin;
type ITecPin; type ItecPin;
type DacFeedbackPin; type DacFeedbackPin;
type TecUMeasPin; type TecUMeasPin;
} }
@ -68,7 +76,7 @@ impl ChannelPins for Channel0 {
type DacSync = PE4<Output<PushPull>>; type DacSync = PE4<Output<PushPull>>;
type Shdn = PE10<Output<PushPull>>; type Shdn = PE10<Output<PushPull>>;
type VRefPin = Channel0VRef; type VRefPin = Channel0VRef;
type ITecPin = PA6<Analog>; type ItecPin = PA6<Analog>;
type DacFeedbackPin = PA4<Analog>; type DacFeedbackPin = PA4<Analog>;
type TecUMeasPin = PC2<Analog>; type TecUMeasPin = PC2<Analog>;
} }
@ -83,21 +91,13 @@ impl ChannelPins for Channel1 {
type DacSync = PF6<Output<PushPull>>; type DacSync = PF6<Output<PushPull>>;
type Shdn = PE15<Output<PushPull>>; type Shdn = PE15<Output<PushPull>>;
type VRefPin = Channel1VRef; type VRefPin = Channel1VRef;
type ITecPin = PB0<Analog>; type ItecPin = PB0<Analog>;
type DacFeedbackPin = PA5<Analog>; type DacFeedbackPin = PA5<Analog>;
type TecUMeasPin = PC3<Analog>; type TecUMeasPin = PC3<Analog>;
} }
/// SPI peripheral used for communication with the ADC /// SPI peripheral used for communication with the ADC
pub type AdcSpi = Spi< pub type AdcSpi = Spi<SPI2, (PB10<Alternate<AF5>>, PB14<Alternate<AF5>>, PB15<Alternate<AF5>>), TransferModeNormal>;
SPI2,
(
PB10<Alternate<AF5>>,
PB14<Alternate<AF5>>,
PB15<Alternate<AF5>>,
),
TransferModeNormal,
>;
pub type AdcNss = PB12<Output<PushPull>>; pub type AdcNss = PB12<Output<PushPull>>;
type Dac0Spi = Spi<SPI4, (PE2<Alternate<AF5>>, NoMiso, PE6<Alternate<AF5>>), TransferModeNormal>; type Dac0Spi = Spi<SPI4, (PE2<Alternate<AF5>>, NoMiso, PE6<Alternate<AF5>>), TransferModeNormal>;
type Dac1Spi = Spi<SPI5, (PF7<Alternate<AF5>>, NoMiso, PF9<Alternate<AF5>>), TransferModeNormal>; type Dac1Spi = Spi<SPI5, (PF7<Alternate<AF5>>, NoMiso, PF9<Alternate<AF5>>), TransferModeNormal>;
@ -108,7 +108,7 @@ pub struct ChannelPinSet<C: ChannelPins> {
pub dac_sync: C::DacSync, pub dac_sync: C::DacSync,
pub shdn: C::Shdn, pub shdn: C::Shdn,
pub vref_pin: C::VRefPin, pub vref_pin: C::VRefPin,
pub itec_pin: C::ITecPin, pub itec_pin: C::ItecPin,
pub dac_feedback_pin: C::DacFeedbackPin, pub dac_feedback_pin: C::DacFeedbackPin,
pub tec_u_meas_pin: C::TecUMeasPin, pub tec_u_meas_pin: C::TecUMeasPin,
} }
@ -133,34 +133,13 @@ impl Pins {
/// Setup GPIO pins and configure MCU peripherals /// Setup GPIO pins and configure MCU peripherals
pub fn setup( pub fn setup(
clocks: Clocks, clocks: Clocks,
(tim1, tim3, tim8): (TIM1, TIM3, TIM8), tim1: TIM1, tim3: TIM3, tim8: TIM8,
(gpioa, gpiob, gpioc, gpiod, gpioe, gpiof, gpiog): ( gpioa: GPIOA, gpiob: GPIOB, gpioc: GPIOC, gpiod: GPIOD, gpioe: GPIOE, gpiof: GPIOF, gpiog: GPIOG,
GPIOA,
GPIOB,
GPIOC,
GPIOD,
GPIOE,
GPIOF,
GPIOG,
),
i2c1: I2C1, i2c1: I2C1,
(spi2, spi4, spi5): (SPI2, SPI4, SPI5), spi2: SPI2, spi4: SPI4, spi5: SPI5,
adc1: ADC1, adc1: ADC1,
(otg_fs_global, otg_fs_device, otg_fs_pwrclk): ( otg_fs_global: OTG_FS_GLOBAL, otg_fs_device: OTG_FS_DEVICE, otg_fs_pwrclk: OTG_FS_PWRCLK,
OTG_FS_GLOBAL, ) -> (Self, Leds, Eeprom, EthernetPins, USB, Option<FanPin>, HWRev, HWSettings) {
OTG_FS_DEVICE,
OTG_FS_PWRCLK,
),
) -> (
Self,
Leds,
Eeprom,
EthernetPins,
USB,
Option<FanPin>,
HWRev,
HWSettings,
) {
let gpioa = gpioa.split(); let gpioa = gpioa.split();
let gpiob = gpiob.split(); let gpiob = gpiob.split();
let gpioc = gpioc.split(); let gpioc = gpioc.split();
@ -175,29 +154,23 @@ impl Pins {
let pins_adc = Adc::adc1(adc1, true, Default::default()); let pins_adc = Adc::adc1(adc1, true, Default::default());
let pwm = PwmPins::setup( let pwm = PwmPins::setup(
clocks, clocks, tim1, tim3,
(tim1, tim3), gpioc.pc6, gpioc.pc7,
(gpioc.pc6, gpioc.pc7), gpioe.pe9, gpioe.pe11,
(gpioe.pe9, gpioe.pe11), gpioe.pe13, gpioe.pe14
(gpioe.pe13, gpioe.pe14),
); );
let hwrev = HWRev::detect_hw_rev(&HWRevPins { let hwrev = HWRev::detect_hw_rev(&HWRevPins {hwrev0: gpiod.pd0, hwrev1: gpiod.pd1,
hwrev0: gpiod.pd0, hwrev2: gpiod.pd2, hwrev3: gpiod.pd3});
hwrev1: gpiod.pd1,
hwrev2: gpiod.pd2,
hwrev3: gpiod.pd3,
});
let hw_settings = hwrev.settings(); let hw_settings = hwrev.settings();
let (dac0_spi, dac0_sync) = Self::setup_dac0(clocks, spi4, gpioe.pe2, gpioe.pe4, gpioe.pe6); 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 mut shdn0 = gpioe.pe10.into_push_pull_output();
shdn0.set_low(); let _ = shdn0.set_low();
let vref0_pin = if hwrev.major > 2 { let vref0_pin = if hwrev.major > 2 {Channel0VRef::Analog(gpioa.pa0.into_analog())} else {Channel0VRef::Disabled(gpioa.pa0)};
Channel0VRef::Analog(gpioa.pa0.into_analog())
} else {
Channel0VRef::Disabled(gpioa.pa0)
};
let itec0_pin = gpioa.pa6.into_analog(); let itec0_pin = gpioa.pa6.into_analog();
let dac_feedback0_pin = gpioa.pa4.into_analog(); let dac_feedback0_pin = gpioa.pa4.into_analog();
let tec_u_meas0_pin = gpioc.pc2.into_analog(); let tec_u_meas0_pin = gpioc.pc2.into_analog();
@ -211,14 +184,13 @@ impl Pins {
tec_u_meas_pin: tec_u_meas0_pin, 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 mut shdn1 = gpioe.pe15.into_push_pull_output();
shdn1.set_low(); let _ = shdn1.set_low();
let vref1_pin = if hwrev.major > 2 { let vref1_pin = if hwrev.major > 2 {Channel1VRef::Analog(gpioa.pa3.into_analog())} else {Channel1VRef::Disabled(gpioa.pa3)};
Channel1VRef::Analog(gpioa.pa3.into_analog())
} else {
Channel1VRef::Disabled(gpioa.pa3)
};
let itec1_pin = gpiob.pb0.into_analog(); let itec1_pin = gpiob.pb0.into_analog();
let dac_feedback1_pin = gpioa.pa5.into_analog(); let dac_feedback1_pin = gpioa.pa5.into_analog();
let tec_u_meas1_pin = gpioc.pc3.into_analog(); let tec_u_meas1_pin = gpioc.pc3.into_analog();
@ -233,19 +205,14 @@ impl Pins {
}; };
let pins = Pins { let pins = Pins {
adc_spi, adc_spi, adc_nss,
adc_nss,
pins_adc, pins_adc,
pwm, pwm,
channel0, channel0,
channel1, channel1,
}; };
let leds = Leds::new( let leds = Leds::new(gpiod.pd9, gpiod.pd10.into_push_pull_output(), gpiod.pd11.into_push_pull_output());
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_scl = gpiob.pb8.into_alternate().set_open_drain();
let eeprom_sda = gpiob.pb9.into_alternate().set_open_drain(); let eeprom_sda = gpiob.pb9.into_alternate().set_open_drain();
@ -272,13 +239,8 @@ impl Pins {
}; };
let fan = if hw_settings.fan_available { let fan = if hw_settings.fan_available {
Some( Some(Timer::new(tim8, &clocks).pwm(gpioc.pc9.into_alternate(), hw_settings.fan_pwm_freq_hz.hz()))
Timer::new(tim8, &clocks) } else { None };
.pwm(gpioc.pc9.into_alternate(), hw_settings.fan_pwm_freq_hz.hz()),
)
} else {
None
};
(pins, leds, eeprom, eth_pins, usb, fan, hwrev, hw_settings) (pins, leds, eeprom, eth_pins, usb, fan, hwrev, hw_settings)
} }
@ -290,7 +252,8 @@ impl Pins {
sck: PB10<M1>, sck: PB10<M1>,
miso: PB14<M2>, miso: PB14<M2>,
mosi: PB15<M3>, mosi: PB15<M3>,
) -> AdcSpi { ) -> AdcSpi
{
let sck = sck.into_alternate(); let sck = sck.into_alternate();
let miso = miso.into_alternate(); let miso = miso.into_alternate();
let mosi = mosi.into_alternate(); let mosi = mosi.into_alternate();
@ -299,16 +262,13 @@ impl Pins {
(sck, miso, mosi), (sck, miso, mosi),
crate::ad7172::SPI_MODE, crate::ad7172::SPI_MODE,
crate::ad7172::SPI_CLOCK, crate::ad7172::SPI_CLOCK,
clocks, clocks
) )
} }
fn setup_dac0<M1, M2, M3>( fn setup_dac0<M1, M2, M3>(
clocks: Clocks, clocks: Clocks, spi4: SPI4,
spi4: SPI4, sclk: PE2<M1>, sync: PE4<M2>, sdin: PE6<M3>
sclk: PE2<M1>,
sync: PE4<M2>,
sdin: PE6<M3>,
) -> (Dac0Spi, <Channel0 as ChannelPins>::DacSync) { ) -> (Dac0Spi, <Channel0 as ChannelPins>::DacSync) {
let sclk = sclk.into_alternate(); let sclk = sclk.into_alternate();
let sdin = sdin.into_alternate(); let sdin = sdin.into_alternate();
@ -317,7 +277,7 @@ impl Pins {
(sclk, NoMiso {}, sdin), (sclk, NoMiso {}, sdin),
crate::ad5680::SPI_MODE, crate::ad5680::SPI_MODE,
crate::ad5680::SPI_CLOCK, crate::ad5680::SPI_CLOCK,
clocks, clocks
); );
let sync = sync.into_push_pull_output(); let sync = sync.into_push_pull_output();
@ -325,11 +285,8 @@ impl Pins {
} }
fn setup_dac1<M1, M2, M3>( fn setup_dac1<M1, M2, M3>(
clocks: Clocks, clocks: Clocks, spi5: SPI5,
spi5: SPI5, sclk: PF7<M1>, sync: PF6<M2>, sdin: PF9<M3>
sclk: PF7<M1>,
sync: PF6<M2>,
sdin: PF9<M3>,
) -> (Dac1Spi, <Channel1 as ChannelPins>::DacSync) { ) -> (Dac1Spi, <Channel1 as ChannelPins>::DacSync) {
let sclk = sclk.into_alternate(); let sclk = sclk.into_alternate();
let sdin = sdin.into_alternate(); let sdin = sdin.into_alternate();
@ -338,7 +295,7 @@ impl Pins {
(sclk, NoMiso {}, sdin), (sclk, NoMiso {}, sdin),
crate::ad5680::SPI_MODE, crate::ad5680::SPI_MODE,
crate::ad5680::SPI_CLOCK, crate::ad5680::SPI_CLOCK,
clocks, clocks
); );
let sync = sync.into_push_pull_output(); let sync = sync.into_push_pull_output();
@ -358,18 +315,25 @@ pub struct PwmPins {
impl PwmPins { impl PwmPins {
fn setup<M1, M2, M3, M4, M5, M6>( fn setup<M1, M2, M3, M4, M5, M6>(
clocks: Clocks, clocks: Clocks,
(tim1, tim3): (TIM1, TIM3), tim1: TIM1,
(max_v0, max_v1): (PC6<M1>, PC7<M2>), tim3: TIM3,
(max_i_pos0, max_i_pos1): (PE9<M3>, PE11<M4>), max_v0: PC6<M1>,
(max_i_neg0, max_i_neg1): (PE13<M5>, PE14<M6>), max_v1: PC7<M2>,
max_i_pos0: PE9<M3>,
max_i_pos1: PE11<M4>,
max_i_neg0: PE13<M5>,
max_i_neg1: PE14<M6>,
) -> PwmPins { ) -> PwmPins {
let freq = 20u32.khz(); let freq = 20u32.khz();
fn init_pwm_pin<P: hal::PwmPin<Duty = u16>>(pin: &mut P) { fn init_pwm_pin<P: hal::PwmPin<Duty=u16>>(pin: &mut P) {
pin.set_duty(0); pin.set_duty(0);
pin.enable(); 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) = pwm::tim3(tim3, channels, clocks, freq);
let (mut max_v0, mut max_v1) = Timer::new(tim3, &clocks).pwm(channels, freq); let (mut max_v0, mut max_v1) = Timer::new(tim3, &clocks).pwm(channels, freq);
init_pwm_pin(&mut max_v0); init_pwm_pin(&mut max_v0);
@ -389,12 +353,9 @@ impl PwmPins {
init_pwm_pin(&mut max_i_neg1); init_pwm_pin(&mut max_i_neg1);
PwmPins { PwmPins {
max_v0, max_v0, max_v1,
max_v1, max_i_pos0, max_i_pos1,
max_i_pos0, max_i_neg0, max_i_neg1,
max_i_pos1,
max_i_neg0,
max_i_neg1,
} }
} }
} }

View File

@ -1,29 +1,25 @@
use crate::command_parser::Ipv4Config;
use crate::net::split_ipv4_config;
use smoltcp::{ use smoltcp::{
iface::EthernetInterface, iface::EthernetInterface,
socket::{SocketHandle, SocketRef, SocketSet, TcpSocket, TcpSocketBuffer}, socket::{SocketSet, SocketHandle, TcpSocket, TcpSocketBuffer, SocketRef},
time::Instant, time::Instant,
wire::{IpAddress, IpCidr, Ipv4Address, Ipv4Cidr}, wire::{IpAddress, IpCidr, Ipv4Address, Ipv4Cidr},
}; };
use crate::command_parser::Ipv4Config;
use crate::net::split_ipv4_config;
pub struct SocketState<S> { pub struct SocketState<S> {
handle: SocketHandle, handle: SocketHandle,
state: S, state: S,
} }
impl<'a, S: Default> SocketState<S> { impl<'a, S: Default> SocketState<S>{
fn new( 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> {
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_rx_buffer = TcpSocketBuffer::new(&mut tcp_rx_storage[..]);
let tcp_tx_buffer = TcpSocketBuffer::new(&mut tcp_tx_storage[..]); let tcp_tx_buffer = TcpSocketBuffer::new(&mut tcp_tx_storage[..]);
let tcp_socket = TcpSocket::new(tcp_rx_buffer, tcp_tx_buffer); let tcp_socket = TcpSocket::new(tcp_rx_buffer, tcp_tx_buffer);
SocketState::<S> { SocketState::<S> {
handle: sockets.add(tcp_socket), handle: sockets.add(tcp_socket),
state: S::default(), state: S::default()
} }
} }
} }
@ -54,7 +50,7 @@ impl<'a, 'b, S: Default> Server<'a, 'b, S> {
($rx_storage:ident, $tx_storage:ident) => { ($rx_storage:ident, $tx_storage:ident) => {
let mut $rx_storage = [0; TCP_RX_BUFFER_SIZE]; let mut $rx_storage = [0; TCP_RX_BUFFER_SIZE];
let mut $tx_storage = [0; TCP_TX_BUFFER_SIZE]; let mut $tx_storage = [0; TCP_TX_BUFFER_SIZE];
}; }
} }
create_rtx_storage!(tcp_rx_storage0, tcp_tx_storage0); create_rtx_storage!(tcp_rx_storage0, tcp_tx_storage0);
@ -103,10 +99,15 @@ impl<'a, 'b, S: Default> Server<'a, 'b, S> {
fn set_ipv4_address(&mut self, ipv4_address: Ipv4Cidr) { fn set_ipv4_address(&mut self, ipv4_address: Ipv4Cidr) {
self.net.update_ip_addrs(|addrs| { self.net.update_ip_addrs(|addrs| {
for addr in addrs.iter_mut() { for addr in addrs.iter_mut() {
if let IpCidr::Ipv4(_) = addr { match addr {
*addr = IpCidr::Ipv4(ipv4_address); IpCidr::Ipv4(_) => {
// done *addr = IpCidr::Ipv4(ipv4_address);
break; // done
break
}
_ => {
// skip
}
} }
} }
}); });
@ -115,9 +116,10 @@ impl<'a, 'b, S: Default> Server<'a, 'b, S> {
fn set_gateway(&mut self, gateway: Option<Ipv4Address>) { fn set_gateway(&mut self, gateway: Option<Ipv4Address>) {
let routes = self.net.routes_mut(); let routes = self.net.routes_mut();
match gateway { match gateway {
None => routes.update(|routes_storage| { None =>
routes_storage.remove(&IpCidr::new(IpAddress::v4(0, 0, 0, 0), 0)); routes.update(|routes_storage| {
}), routes_storage.remove(&IpCidr::new(IpAddress::v4(0, 0, 0, 0), 0));
}),
Some(gateway) => { Some(gateway) => {
routes.add_default_ipv4_route(gateway).unwrap(); routes.add_default_ipv4_route(gateway).unwrap();
} }

View File

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

View File

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

View File

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

View File

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