Compare commits

..

3 Commits
master ... rtic

Author SHA1 Message Date
topquark12 7e314410a0 rustfmt, squash warnings, add bmp support 2022-11-02 17:18:33 +08:00
topquark12 dab73c3fec working ethernet 2022-11-01 11:16:55 +08:00
topquark12 ca0453df54 rtic implementation 2022-10-23 20:07:44 +08:00
62 changed files with 917 additions and 11097 deletions

12
.cargo/config Normal file
View File

@ -0,0 +1,12 @@
[target.thumbv7em-none-eabihf]
runner = "gdb -batch -q -x bmp.gdb"
rustflags = [
"-C", "link-arg=-Tlink.x",
]
[build]
# Pick ONE of these compilation targets
# target = "thumbv6m-none-eabi" # Cortex-M0 and Cortex-M0+
# target = "thumbv7m-none-eabi" # Cortex-M3
# target = "thumbv7em-none-eabi" # Cortex-M4 and Cortex-M7 (no FPU)
target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU)

View File

@ -1,8 +0,0 @@
[target.thumbv7em-none-eabihf]
runner = "gdb -q -x openocd.gdb"
rustflags = [
"-C", "link-arg=-Tlink.x",
]
[build]
target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU)

2
.gitignore vendored
View File

@ -1,4 +1,4 @@
target/
result
*.pyc
.vscode*
*.jdebug*

60
BSD.md
View File

@ -1,60 +0,0 @@
# Building Rust embedded firmware on BSD
Using the Rust stable compiler from the BSD ports is mostly a matter of removing the arbitrary limitations that have been built into the Rust toolchain.
Get rid of the "nightly toolchain" check in cargo-xbuild:
```
diff --git a/src/lib.rs b/src/lib.rs
index 204ad8f..9c61e39 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -157,28 +157,11 @@ pub fn build(args: Args, command_name: &str, crate_config: Option<Config>) -> Re
})
})?;
- // We can't build sysroot with stable or beta due to unstable features
let sysroot = rustc::sysroot(verbose)?;
- let src = match meta.channel {
- Channel::Dev => rustc::Src::from_env().ok_or(anyhow!(
+ let src = rustc::Src::from_env().ok_or(anyhow!(
"The XARGO_RUST_SRC env variable must be set and point to the \
- Rust source directory when working with the 'dev' channel",
- ))?,
- Channel::Nightly => {
- if let Some(src) = rustc::Src::from_env() {
- src
- } else {
- sysroot.src()?
- }
- }
- Channel::Stable | Channel::Beta => {
- bail!(
- "The sysroot can't be built for the {:?} channel. \
- Switch to nightly.",
- meta.channel
- );
- }
- };
+ Rust source directory",
+ ))?;
let cmode = if let Some(triple) = args.target() {
if triple == meta.host {
```
Run ``cargo install --path .`` to install the modified cargo-xbuild
Get a copy of the Rust sources that corresponds to the installed compiler binary, and set up the environment accordingly:
```
> rustc --version
rustc 1.72.1 (d5c2e9c34 2023-09-13) (built from a source tarball)
> git clone --depth=1 --recurse-submodules --shallow-submodules https://github.com/rust-lang/rust --branch 1.72.1
> export XARGO_RUST_SRC=`pwd`/rust/library
```
Disable other arbitrary checks in the Rust compiler:
```
> export RUSTC_BOOTSTRAP=1
```
And you can now simply run ``cargo xbuild`` to build the firmware.

796
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -15,30 +15,27 @@ default-target = "thumbv7em-none-eabihf"
[dependencies]
panic-halt = "0.2.0"
cortex-m = { version = "0.7.7", features = ["critical-section-single-core"] }
cortex-m-rt = { version = "0.7.3", features = ["device"] }
cortex-m = "0.7.6"
cortex-m-rt = { version = "0.7.1", features = ["device"] }
cortex-m-semihosting = "0.5.0"
log = "0.4.17"
bare-metal = "1"
nb = "1"
cortex-m-log = { version = "0.7.0", features = ["log-integration", "semihosting"] }
stm32f4xx-hal = { version = "0.20.0", features = ["stm32f407", "otg-fs", "usb_fs"] }
stm32-eth = { version = "0.6.0", features = ["stm32f407", "smoltcp-phy", "smoltcp"] }
ieee802_3_miim = "0.8.0"
smoltcp = { version = "0.11.0", default-features = false, features = ["proto-ipv4", "socket-tcp", "log", "medium-ethernet", "iface-neighbor-cache-count-16"] }
uom = { version = "0.30", default-features = false, features = ["autoconvert", "si", "f32", "use_serde"] }
stm32f4xx-hal = { version = "0.13.2", features = ["rt", "stm32f407", "usb_fs", "rtic-monotonic"] }
stm32-eth = { git = "https://github.com/stm32-rs/stm32-eth", features = ["stm32f407", "smoltcp-phy", "smoltcp"] }
smoltcp = { version = "0.8.1", default-features = false, features = ["proto-ipv4", "socket-tcp", "medium-ethernet"] }
ieee802_3_miim = "0.7.2"
num-traits = { version = "0.2.15", default-features = false, features = ["libm"] }
usb-device = "0.3.2"
usbd-serial = "0.2.1"
usb-device = "0.2.9"
usbd-serial = "0.1.1"
fugit = "0.3.6"
rtt-target = { version = "0.3.1", features = ["cortex-m"] }
miniconf = "0.9.0"
serde = { version = "1.0.158", features = ["derive"], default-features = false }
sfkv = "0.1"
bit_field = "0.10"
crc = "3.0.1"
byteorder = { version = "1", default-features = false }
cortex-m-rtic = "1.0.0"
systick-monotonic = "1.0.0"
[features]
default = ["RTT"]
semihosting = ["cortex-m-log/semihosting"]
RTT = []

View File

@ -1,85 +0,0 @@
# Testing Firmware for the Sinara 1550 Kirdy
- This repo is for testing only. Not intended for production use.
## Building
### Reproducible build with Nix
kirdy firmware is packaged using the [Nix](https://nixos.org) Flakes system. Install Nix 2.4+ and enable flakes by adding ``experimental-features = nix-command flakes`` to ``nix.conf`` (e.g. ``~/.config/nix/nix.conf``).
Once you have Flakes enabled, you can use ``nix build`` to build the firmware.
### Development environment
Clone this repository and with Nix Flakes enabled, use the following commands:
```shell
nix develop
cargo build
```
The resulting ELF file will be located under `target/thumbv7em-none-eabihf/release/kirdy`.
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.
For building on FreeBSD or OpenBSD, see BSD.md.
## Debugging
Connect SWDIO/SWCLK/RST/GND to a programmer such as ST-Link v2.1. Run OpenOCD:
```shell
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg
```
You may need to power up the programmer before powering the device.
Leave OpenOCD running. Run the GNU debugger:
```shell
gdb target/thumbv7em-none-eabihf/release/kirdy
(gdb) source openocd.gdb
```
## Flashing
If the firmware to be flashed involves an update on the flash settings, it is required to erase the flash settings before flashing the new firmware to avoid unexpected hardware behavior. There are several options for flashing kirdy. DFU requires only a USB-C cable or RJ45 cable, whereas OpenOCD needs a JTAG/SWD adapter.
### dfu-util on Linux
* Install the DFU USB tool (dfu-util).
* Convert firmware from ELF to BIN: `arm-none-eabi-objcopy -O binary kirdy kirdy.bin` (you can skip this step if using the BIN from Hydra)
* Put STM32 into DFU Mode. You can either
* Connect to the USB Type C cable to kirdy next to the RJ45 Jack. After that, add BOOT0 jumper to kirdy near programming headers and then cycle board power to put it in DFU mode. OR
* Plug in RJ45 cable, which connect to a network that is accessible by your computer and send the corresponding dfu json command via TCP Socket to kirdy. Please see the python test script for the command.
* Push firmware to flash: `dfu-util -a 0 -s 0x08000000:leave -D kirdy.bin`
* If you plugged in the BOOT0 jumper, you will need to
1. Remove BOOT0 jumper
2. Cycle power to leave DFU update mode
* If you plugged in the RJ45 cable, the MCU would start its application code automatically. No power cycle is needed.
### st.com DfuSe tool on Windows
On a Windows machine install [st.com](https://st.com) DfuSe USB device firmware upgrade (DFU) software. [link](https://www.st.com/en/development-tools/stsw-stm32080.html).
- add jumper to kirdy across 2-pin jumper adjacent to JTAG connector
- cycle board power to put it in DFU update mode
- connect USB Type C to PC
- use st.com software to upload firmware
- remove jumper
- cycle power to leave DFU update mode
### OpenOCD
```shell
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg -c "program target/thumbv7em-none-eabihf/debug/kirdy verify reset; exit"
```
## Erasing Flash Settings
The flash settings are stored in the last flash sector(ID: 11) of bank 0 of stm32f405. You can erase it with JTAG/SWD adapter or by putting the device in Dfu mode. You may find it useful if you have set network settings incorrectly.
With JTAG/SWD adapter connected, issue the following command:
```shell
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg -c "flash init; init; halt; flash erase_sector 0 11 last; reset; exit"
```
With STM32 in DFU Mode, connect the USB Type C cable and then issue the following command:
```
dfu-util -a 0 -s 0x080E0000:leave -D erase_flash_settings.bin
```

6
bmp.gdb Normal file
View File

@ -0,0 +1,6 @@
target extended-remote /dev/ttyBmpGdb
monitor swdp_scan
attach 1
load
compare-sections
kill

View File

@ -1,4 +1,7 @@
use std::{env, fs::File, io::Write, path::PathBuf};
use std::env;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
fn main() {
// Put the linker script somewhere the linker can find it

Binary file not shown.

View File

@ -3,11 +3,11 @@
"mozilla-overlay": {
"flake": false,
"locked": {
"lastModified": 1704373101,
"narHash": "sha256-+gi59LRWRQmwROrmE1E2b3mtocwueCQqZ60CwLG+gbg=",
"lastModified": 1664789696,
"narHash": "sha256-UGWJHQShiwLCr4/DysMVFrYdYYHcOqAOVsWNUu+l6YU=",
"owner": "mozilla",
"repo": "nixpkgs-mozilla",
"rev": "9b11a87c0cc54e308fa83aac5b4ee1816d5418a2",
"rev": "80627b282705101e7b38e19ca6e8df105031b072",
"type": "github"
},
"original": {
@ -18,16 +18,16 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1713725259,
"narHash": "sha256-9ZR/Rbx5/Z/JZf5ehVNMoz/s5xjpP0a22tL6qNvLt5E=",
"lastModified": 1666164185,
"narHash": "sha256-5v+YB4ijeUfg5LCz9ck4gIpCPhIS+qn02OyPJO48bCE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "a5e4bbcb4780c63c79c87d29ea409abf097de3f7",
"rev": "c5203abb1329f7ea084c04acda330ca75d5b9fb5",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-23.11",
"ref": "nixos-22.05",
"repo": "nixpkgs",
"type": "github"
}

View File

@ -1,15 +1,15 @@
{
description = "Firmware for kirdy";
inputs.nixpkgs.url = github:NixOS/nixpkgs/nixos-23.11;
inputs.nixpkgs.url = github:NixOS/nixpkgs/nixos-22.05;
inputs.mozilla-overlay = { url = github:mozilla/nixpkgs-mozilla; flake = false; };
outputs = { self, nixpkgs, mozilla-overlay }:
let
pkgs = import nixpkgs { system = "x86_64-linux"; overlays = [ (import mozilla-overlay) ]; };
rustManifest = pkgs.fetchurl {
url = "https://static.rust-lang.org/dist/2024-03-21/channel-rust-nightly.toml";
sha256 = "1c7db6ab80d20682b5cc5bda7360c63311d7188c0c082902d3790820527cd4e0";
url = "https://static.rust-lang.org/dist/2022-09-22/channel-rust-stable.toml";
sha256 = "f257a7de2f284f025238964ca2c1865c704be4156e1909d6393d92b796005055";
};
targets = [
@ -22,7 +22,7 @@
inherit targets;
extensions = ["rust-src"];
};
rust = rustChannelOfTargets "nightly" null targets;
rust = rustChannelOfTargets "stable" null targets;
rustPlatform = pkgs.recurseIntoAttrs (pkgs.makeRustPlatform {
rustc = rust;
cargo = rust;
@ -51,34 +51,6 @@
'';
dontFixup = true;
auditable = false;
};
pglive = pkgs.python3Packages.buildPythonPackage rec {
pname = "pglive";
version = "0.7.2";
format = "pyproject";
src = pkgs.fetchPypi {
inherit pname version;
hash = "sha256-jqj8X6H1N5mJQ4OrY5ANqRB0YJByqg/bNneEALWmH1A=";
};
buildInputs = [ pkgs.python3Packages.poetry-core ];
propagatedBuildInputs = with pkgs.python3Packages; [ pyqtgraph numpy ];
};
kirdy_gui = pkgs.python3Packages.buildPythonPackage {
pname = "kirdy_gui";
version = "0.0.0";
format = "pyproject";
src = "${self}/pykirdy";
nativeBuildInputs = [ pkgs.qt6.wrapQtAppsHook ];
propagatedBuildInputs = [ pkgs.qt6.qtbase ] ++ (with pkgs.python3Packages; [ pyqtgraph pyqt6 qasync pglive ]);
dontWrapQtApps = true;
postFixup = ''
wrapQtApp "$out/bin/tec_qt"
'';
};
in {
packages.x86_64-linux = {
@ -92,14 +64,16 @@
devShell.x86_64-linux = pkgs.mkShell {
name = "kirdy-dev-shell";
buildInputs = with pkgs; [
rust openocd dfu-util glibc
rustPlatform.rust.rustc
rustPlatform.rust.cargo
openocd dfu-util glibc picocom gdb
] ++ (with python3Packages; [
numpy matplotlib pyqtgraph setuptools pyqt6 qasync pglive
numpy matplotlib pyqtgraph
]);
shellHook=
''
export QT_PLUGIN_PATH=${pkgs.qt6.qtbase}/${pkgs.qt6.qtbase.dev.qtPluginPrefix}
export QML2_IMPORT_PATH=${pkgs.qt6.qtbase}/${pkgs.qt6.qtbase.dev.qtQmlPrefix}
export QT_PLUGIN_PATH=${pkgs.qt5.qtbase}/${pkgs.qt5.qtbase.dev.qtPluginPrefix}
export QML2_IMPORT_PATH=${pkgs.qt5.qtbase}/${pkgs.qt5.qtbase.dev.qtQmlPrefix}
'';
};
defaultPackage.x86_64-linux = kirdy;

View File

@ -2,8 +2,9 @@
MEMORY
{
/* The last flash sector is reserved for config */
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 896K
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 1024K
/* reserved for config data */
CONFIG (rx) : ORIGIN = 0x8100000, LENGTH = 16K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 112K - 4
/* reserved for DFU trigger message */
DFU_MSG (wrx) : ORIGIN = 0x2001BFFC, LENGTH = 4
@ -13,5 +14,6 @@ MEMORY
}
_flash_start = ORIGIN(FLASH);
_config_start = ORIGIN(CONFIG);
_dfu_msg = ORIGIN(DFU_MSG);
_stack_start = ORIGIN(CCMRAM) + LENGTH(CCMRAM);

View File

@ -1,47 +0,0 @@
from driver.kirdy_async import Kirdy
import asyncio
async def main():
kirdy = Kirdy()
await kirdy.start_session(host='192.168.1.128', port=1337, timeout=0.25)
await kirdy.device.set_active_report_mode(False)
await kirdy.laser.set_power_on(False)
await kirdy.laser.clear_alarm()
await kirdy.laser.set_i(0)
await kirdy.laser.set_i_soft_limit(0.25)
await kirdy.laser.set_power_on(True)
await kirdy.thermostat.set_power_on(False)
await kirdy.thermostat.clear_alarm()
await kirdy.thermostat.set_sh_r0(10.0*1000)
await kirdy.thermostat.set_sh_t0(25)
await kirdy.thermostat.set_sh_beta(3900)
await kirdy.thermostat.set_temperature_setpoint(25)
await kirdy.thermostat.set_temp_mon_upper_limit(40)
await kirdy.thermostat.set_temp_mon_lower_limit(10)
await kirdy.thermostat.set_pid_kp(0.15668282198105507)
await kirdy.thermostat.set_pid_ki(0.002135962407793784)
await kirdy.thermostat.set_pid_kd(0.829254515277143)
await kirdy.thermostat.set_pid_output_max(1.0)
await kirdy.thermostat.set_pid_output_min(-1.0)
await kirdy.thermostat.config_temp_adc_filter("Sinc5Sinc1With50hz60HzRejection", "F16SPS")
await kirdy.thermostat.set_power_on(True)
await kirdy.thermostat.set_pid_control_mode()
await kirdy.laser.set_default_pwr_on(False)
await kirdy.thermostat.set_default_pwr_on(True)
await kirdy.device.save_current_settings_to_flash()
async for data in kirdy.report_mode():
print(data)
await kirdy.end_session()
if __name__ == "__main__":
asyncio.run(main())

View File

@ -1,743 +0,0 @@
import socket
import asyncio
import json
import logging
# Data Type Enums
IP_SETTINGS = "ip_settings"
TEMP_ADC_FILTER = "temp_adc_filter"
DATA_F32 = "data_f32"
DATA_BOOL = "data_bool"
TARGET_DEVICE = "device_cmd"
TARGET_LD = "laser_diode_cmd"
TARGET_THERMOSTAT = "thermostat_cmd"
class CmdDoesNotExist(Exception):
pass
class InvalidDataType(Exception):
pass
class InvalidCmd(Exception):
pass
class NoAckRecv(Exception):
pass
class StoppedConnecting(Exception):
pass
Filter_Config = {
"Sinc5Sinc1With50hz60HzRejection": [
"sinc5sinc1postfilter",
[
"F27SPS",
"F21SPS",
"F20SPS",
"F16SPS",
]
],
"Sinc5Sinc1": [
"sinc5sinc1odr",
[
"F31250_0SPS",
"F15625_0SPS",
"F10417_0SPS",
"F5208_0SPS" ,
"F2597_0SPS" ,
"F1007_0SPS" ,
"F503_8SPS" ,
"F381_0SPS" ,
"F200_3SPS" ,
"F100_2SPS" ,
"F59_52SPS" ,
"F49_68SPS" ,
"F20_01SPS" ,
"F16_63SPS" ,
"F10_0SPS" ,
"F5_0SPS" ,
"F2_5SPS" ,
"F1_25SPS" ,
]
],
"Sinc3": [
"sinc3odr",
[
"F31250_0SPS",
"F15625_0SPS",
"F10417_0SPS",
"F5208_0SPS" ,
"F2597_0SPS" ,
"F1007_0SPS" ,
"F503_8SPS" ,
"F381_0SPS" ,
"F200_3SPS" ,
"F100_2SPS" ,
"F59_52SPS" ,
"F49_68SPS" ,
"F20_01SPS" ,
"F16_63SPS" ,
"F10_0SPS" ,
"F5_0SPS" ,
"F2_5SPS" ,
"F1_25SPS" ,
]
],
"Sinc3WithFineODR": [
"sinc3fineodr",
DATA_F32
],
}
class Device:
def __init__(self, send_cmd_handler, send_raw_cmd_handler, read_response, cmd_lock):
self._send_cmd = send_cmd_handler
self._send_raw_cmd = send_raw_cmd_handler
self._read_response = read_response
self._cmd_lock = cmd_lock
async def set_ip_settings(self, addr="192.168.1.128", port=1337, prefix_len=24, gateway="192.168.1.1"):
"""
After calling this fn, the ip settings are immediately saved into flash and will be effective on next reboot.
"""
try:
socket.inet_aton(addr)
socket.inet_aton(gateway)
except OSError:
raise InvalidDataType
addr = addr.split(".")
gateway = gateway.split(".")
if not(isinstance(port, int) and isinstance(prefix_len, int)):
raise InvalidDataType
return await self._send_raw_cmd(
{
"device_cmd": "SetIPSettings",
"ip_settings": {
"addr": addr,
"port": port,
"prefix_len": prefix_len,
"gateway": gateway,
}
}
)
async def set_active_report_mode(self, on):
"""
Set active report to be on. If it is on, Kirdy will send status report
to ALL client socket connections according to the temperature polling rate set.
"""
return await self._send_cmd(TARGET_DEVICE, "SetActiveReportMode", on)
async def get_status_report(self):
"""
Get status of all peripherals in a json object
Example of yielded data::
{
'ts': 227657, # Relative Timestamp (ms)
'msg_type': 'Report' # Indicate it is a 'Report' json object
'laser': {
'pwr_on': False, # Laser Power is On (True/False)
'pwr_excursion': False, # Was Laser experienced an Overpowered Condition? (True/False)
'ld_i_set': 0.0, # Laser Diode Output Current (A)
'pd_i': 2.0000002e-06, # Internal Photodiode Monitor current (A)
'pd_pwr': None, # Power Readings from Internal Photodiode (W). Return None if pd_mon parameter(s) are not defined.
'term_status': 'Is50Ohm' # Is the Low Frequency Modulation Input's Impedance 50 Ohm? (Is50Ohm/Not50Ohm)
},
'thermostat': {
'pwr_on': False, # Tec Power is On (True/False)
'pid_engaged': False, # Is Pid_Engaged. If False, it is in Constant Current Mode (True/False)
'temp_mon_status': { # Temperature Monitor:
'status': 'Off', # (To be revised)
'over_temp_alarm': False # Was Laser Diode experienced an Overtemperature condition (True/False)
},
'temperature': 25.03344, # Temperature Readings (Degree Celsius)
'i_set': 0.0, # Tec Current Set by User/PID Controller(A)
'tec_i': 0.0024998188, # Tec Current Readings (A)
'tec_v': -0.00399971 # Tec Voltage Readings (V)
}
}
"""
return await self._send_cmd(TARGET_DEVICE, "GetStatusReport", msg_type="Report")
async def get_settings_summary(self):
"""
Get the current settings of laser and thermostat in a json object
{
'msg_type': 'Settings', # Indicate it is a 'Settings' json object
'laser': {
'default_pwr_on': False, # Power On Laser Diode at Startup
'ld_drive_current': { # Laser Diode Output Current(A)
'value': 0.0, # Value Set
'max': 0.3 # Max Value Settable
,
'ld_drive_current_limit': { # Laser Diode Software Current Limit(A)
'value': 0.3, # Value Set
'max': 0.3 # Max Value Settable
,
'pd_mon_params': { # Laser Diode Software Current Limit(A)
'responsitivity': None, # Value Set
'i_dark': 0.0 # Max Value Settable
,
'ld_pwr_limit': 0.0 # Laser Diode Power Limit(W)
'ld_terms_short: False # Is Laser Diode Terminals short? (True/False)
},
'thermostat': {
'default_pwr_on': True, # Power on Thermostat at Startup
'pid_engaged': True, # True: PID Control Mode | False Constant Current Mode
'temperature_setpoint': 25.0, # Temperature Setpoint (Degree Celsius)
'tec_settings': {
'i_set': { # Current TEC Current Set by PID Controller/User
'value': 0.04330516, # Value Set
'max': 1.0 # Max Value Settable
},
'max_v': { # Max Voltage Across Tec Terminals
'value': 4.990857, # Value Set
'max': 5.0 # Max Value Settable
},
'max_i_pos': { # Max Cooling Current Across Tec Terminals
'value': 0.99628574, # Value Set
'max': 1.0 # Max Value Settable
},
'max_i_neg': { # Max Heating Current Across Tec Terminals
'value': 0.99628574, # Value Set
'max': 1.0 # Max Value Settable
}
},
'pid_params': { # PID Controller Parameters
'kp': 0.15668282, # Proportional Gain
'ki': 0.0021359625, # Integral Gain
'kd': 0.8292545, # Derivative Gain
'output_min': -1.0, # Minimum Current Output (A)
'output_max': 1.0 # Maximum Current Output (A)
},
'temp_adc_settings': { # Temperature ADC Settings (Please read AD7172-2 Documentation)
'filter_type': 'Sinc5Sinc1With50hz60HzRejection', # Filter Types
'sinc5sinc1odr': None, # (Unused)
'sinc3odr': None, # (Unused)
'sinc5sinc1postfilter': None, # (Unused)
'sinc3fineodr': None, # (Unused)
'rate': 16.67 # ADC Sampling Rate (Hz)
},
'temp_mon_settings': { # Temperature Monitor Settings
'upper_limit': 40.0, # Temperature Upper Limit (Degree Celsius)
'lower_limit': 10.0 # Temperature Lower Limit (Degree Celsius)
},
'thermistor_params': { # Thermistor Steinhart-Hart equation parameters
't0': 25.0, # t0: Degree Celsius
'r0': 10000.0, # r0: Ohm
'b': 3900.0 # b: (unitless)
}
}
}
"""
return await self._send_cmd(TARGET_DEVICE, "GetSettingsSummary", msg_type="Settings")
async def dfu(self):
"""
Issuing this cmd will HARD RESET the device and
put Kirdy into Dfu mode for flashing firmware.
"""
return await self._send_cmd(TARGET_DEVICE, "Dfu")
async def save_current_settings_to_flash(self):
"""
Save the current laser diode and thermostat configurations into flash.
"""
return await self._send_cmd(TARGET_DEVICE, "SaveFlashSettings")
async def load_current_settings_from_flash(self):
"""
Restore the laser diode and thermostat settings from flash
"""
return await self._send_cmd(TARGET_DEVICE, "LoadFlashSettings")
async def hard_reset(self):
"""
Hard Reset Kirdy. The socket connection will be closed by Kirdy.
Laser diode power and Tec power will be turned off.
Kirdy will send out a json({'msg_type': 'HardReset'}) to all sockets indicating. The device is being reset.
"""
response = await self._send_cmd(TARGET_DEVICE, "HardReset")
if response is not None:
if response["msg_type"] == "Acknowledge":
# Delay for a second to wait for the hard reset message being sent out on Kirdy
await asyncio.sleep(1.0)
return response
class Laser:
def __init__(self, send_cmd_handler, cmd_lock):
self._send_cmd = send_cmd_handler
self._cmd_lock = cmd_lock
async def set_power_on(self, on):
"""
Power Up or Power Down laser diode. Powering up the Laser Diode resets the pwr_excursion status
- on (True/False)
"""
if on:
return await self._send_cmd(TARGET_LD, "PowerUp", None)
else:
return await self._send_cmd(TARGET_LD, "PowerDown", None)
async def set_default_pwr_on(self, on):
"""
Set whether laser diode is powered up at Startup
- on (True/False)
"""
return await self._send_cmd(TARGET_LD, "SetDefaultPowerOn", on)
async def set_ld_terms_short(self, short):
"""
Open/Short laser diode terminals.
- on (True/False)
"""
if short:
return await self._send_cmd(TARGET_LD, "LdTermsShort", None)
else:
return await self._send_cmd(TARGET_LD, "LdTermsOpen", None)
async def set_i(self, i):
"""
Set laser diode output current: Max(0, Min(i_set, i_soft_limit))
- i: A
"""
return await self._send_cmd(TARGET_LD, "SetI", i)
async def set_i_soft_limit(self, i_limit):
"""
Set laser diode software output current limit
- i_limit: A
"""
return await self._send_cmd(TARGET_LD, "SetISoftLimit", i_limit)
async def set_pd_mon_responsitivity(self, responsitivity):
"""
Configure the photodiode monitor responsitivity parameter
- responsitivity: A/W
"""
return await self._send_cmd(TARGET_LD, "SetPdResponsitivity", responsitivity)
async def set_pd_mon_dark_current(self, dark_current):
"""
Configure the photodiode monitor responsitivity parameter
- dark_current: A/W
"""
return await self._send_cmd(TARGET_LD, "SetPdDarkCurrent", dark_current)
async def set_ld_pwr_limit(self, pwr_limit):
"""
Set power limit for the power excursion monitor
If the calculated power with the params of pd_mon > pwr_limit,
overpower protection is triggered.
- pwr_limit: W
"""
return await self._send_cmd(TARGET_LD, "SetLdPwrLimit", pwr_limit)
async def clear_alarm(self):
"""
Clear the power excursion monitor alarm
"""
return await self._send_cmd(TARGET_LD, "ClearAlarm")
class Thermostat:
def __init__(self, send_cmd_handler, send_raw_cmd_handler, cmd_lock):
self._send_cmd = send_cmd_handler
self._send_raw_cmd = send_raw_cmd_handler
self._cmd_lock = cmd_lock
async def set_power_on(self, on):
"""
Power up or power down thermostat
- Powering up the thermostat resets the pwr_excursion status
"""
if on:
return await self._send_cmd(TARGET_THERMOSTAT, "PowerUp", None)
else:
return await self._send_cmd(TARGET_THERMOSTAT, "PowerDown", None)
async def set_default_pwr_on(self, on):
"""
Set whether thermostat is powered up at Startup
"""
return await self._send_cmd(TARGET_THERMOSTAT, "SetDefaultPowerOn", on)
async def set_tec_max_v(self, max_v):
"""
Set Tec Maximum Voltage Across the TEC Terminals
- max_v: V
"""
return await self._send_cmd(TARGET_THERMOSTAT, "SetTecMaxV", max_v)
async def set_tec_max_cooling_i(self, max_i_pos):
"""
Set Tec maximum cooling current (Settable Range: 0.0 - 1.0)
- max_i_pos: A
"""
return await self._send_cmd(TARGET_THERMOSTAT, "SetTecMaxIPos", max_i_pos)
async def set_tec_max_heating_i(self, max_i_neg):
"""
Set Tec maximum heating current (Settable Range: 0.0 - 1.0)
- max_i_neg: A
"""
return await self._send_cmd(TARGET_THERMOSTAT, "SetTecMaxINeg", max_i_neg)
async def set_tec_i_out(self, i_out):
"""
Set Tec Output Current
This cmd is only effective in constant current control mode
or your newly set value will be overwritten by PID Controller Output
- i_out: A
"""
if isinstance(i_out, float):
return await self._send_raw_cmd({"tec_set_i": i_out})
elif isinstance(i_out, int):
return await self._send_raw_cmd({"tec_set_i": float(i_out)})
else:
raise InvalidDataType
async def set_constant_current_control_mode(self):
"""
Disable PID Controller and output current can be controlled with set_tec_i_out() cmd.
"""
return await self._send_cmd(TARGET_THERMOSTAT, "SetPidDisEngage", None)
async def set_temperature_setpoint(self, temperature):
"""
Set Temperature Setpoint for PID Controller. This parameter is not active in constant current control mode
- temperature: Degree Celsius
"""
return await self._send_cmd(TARGET_THERMOSTAT, "SetTemperatureSetpoint", temperature)
async def set_pid_control_mode(self):
"""
Enable PID Controller. Its PID Update Interval is controlled by the Temperature ADC polling rate.
Please refer to config_temp_adc_filter for the possible polling rate options
"""
return await self._send_cmd(TARGET_THERMOSTAT, "SetPidEngage", None)
async def set_pid_kp(self, kp):
"""
Set Kp parameter for PID Controller
kp: (unitless)
"""
return await self._send_cmd(TARGET_THERMOSTAT, "SetPidKp", kp)
async def set_pid_ki(self, ki):
"""
Set Ki parameter for PID Controller
ki: (unitless)
"""
await self._send_cmd(TARGET_THERMOSTAT, "SetPidKi", ki)
async def set_pid_kd(self, kd):
"""
Set Kd parameter for PID Controller
kd: (unitless)
"""
return await self._send_cmd(TARGET_THERMOSTAT, "SetPidKd", kd)
async def set_pid_output_max(self, out_max):
"""
Set max output limit at the PID Output
- out_max: A
"""
return await self._send_cmd(TARGET_THERMOSTAT, "SetPidOutMax", out_max)
async def set_pid_output_min(self, out_min):
"""
Set min output limit at the PID Output
- out_min: A
"""
return await self._send_cmd(TARGET_THERMOSTAT, "SetPidOutMin", out_min)
async def set_temp_mon_upper_limit(self, upper_limit):
"""
Set Temperature Monitor Upper Limit Threshold. Exceeding the limit for too long
will force the TEC Controller, PID Controller and Laser Diode Power to Shutdown
- upper_limit: Degree Celsius
"""
return await self._send_cmd(TARGET_THERMOSTAT, "SetTempMonUpperLimit", upper_limit)
async def set_temp_mon_lower_limit(self, lower_limit):
"""
Set Temperature Monitor Lower Limit Threshold. Exceeding the limit for too long
will force the TEC Controller, PID Controller and Laser Diode Power to Shutdown
- lower_limit: Degree Celsius
"""
return await self._send_cmd(TARGET_THERMOSTAT, "SetTempMonLowerLimit", lower_limit)
async def clear_alarm(self):
"""
Clear the temperature monitor alarm
"""
return await self._send_cmd(TARGET_THERMOSTAT, "ClearAlarm")
async def set_sh_t0(self, t0):
"""
Set t0 Steinhart-Hart parameter for the laser diode NTC
- t0: Degree Celsius
"""
return await self._send_cmd(TARGET_THERMOSTAT, "SetShT0", t0)
async def set_sh_r0(self, r0):
"""
Set r0 Steinhart-Hart parameter for the laser diode NTC
- r0: Ohm
"""
return await self._send_cmd(TARGET_THERMOSTAT, "SetShR0", r0)
async def set_sh_beta(self, beta):
"""
Set beta Steinhart-Hart parameter for the laser diode NTC
- beta: (unitless)
"""
return await self._send_cmd(TARGET_THERMOSTAT, "SetShBeta", beta)
async def config_temp_adc_filter(self, filter_type, sampling_rate):
"""
Configure the temperature adc filter type and sampling rate.
Please refer to AD7172 datasheet for the usage of various types of filter.
The actual temperature polling rate is bottlenecked by the processing speed of the MCU and
performs differently under different kinds of workload. Please verify the polling rate with
the timestamp.
"""
if not(filter_type in Filter_Config.keys()):
raise InvalidDataType
if Filter_Config[filter_type][1] != DATA_F32:
if not(sampling_rate in Filter_Config[filter_type][1]):
raise InvalidDataType
else:
if not(isinstance(sampling_rate, float)):
raise InvalidDataType
cmd = {}
cmd["thermostat_cmd"] = "ConfigTempAdcFilter"
cmd["temp_adc_filter"] = {
"filter_type": filter_type,
Filter_Config[filter_type][0]: sampling_rate,
}
return await self._send_raw_cmd(cmd)
class Kirdy:
def __init__(self):
self._reader = None
self._writer = None
self._connecting_task = None
self._cmd_lock = asyncio.Lock()
self._report_mode_on = False
self.timeout = None
self.device = Device(self._send_cmd_handler, self._send_raw_cmd_handler, self._read_response, self._cmd_lock)
self.laser = Laser(self._send_cmd_handler, self._cmd_lock)
self.thermostat = Thermostat(self._send_cmd_handler, self._send_raw_cmd_handler, self._cmd_lock)
self._cmd_list = {
TARGET_DEVICE: {
"SetIPSettings": IP_SETTINGS,
"SetActiveReportMode": DATA_BOOL,
"GetStatusReport": None,
"GetSettingsSummary": None,
"Dfu": None,
"SaveFlashSettings": None,
"LoadFlashSettings": None,
"HardReset": None,
},
TARGET_LD: {
# LD Drive Related
"SetDefaultPowerOn": DATA_BOOL,
"PowerUp": None,
"PowerDown": None,
"LdTermsShort": None,
"LdTermsOpen": None,
"SetI": DATA_F32,
"SetISoftLimit": DATA_F32,
# PD Mon Related
"SetPdResponsitivity": DATA_F32,
"SetPdDarkCurrent": DATA_F32,
"SetLdPwrLimit": DATA_F32,
"ClearAlarm": None,
},
TARGET_THERMOSTAT: {
"SetDefaultPowerOn": DATA_BOOL,
"PowerUp": DATA_F32,
"PowerDown": DATA_F32,
# TEC Controller Settings
"SetTecMaxV": DATA_F32,
"SetTecMaxIPos": DATA_F32,
"SetTecMaxINeg": DATA_F32,
"SetTecIOut": DATA_F32,
"SetTemperatureSetpoint": DATA_F32,
# PID Controller Settings
"SetPidEngage": None,
"SetPidDisEngage": None,
"SetPidKp": DATA_F32,
"SetPidKi": DATA_F32,
"SetPidKd": DATA_F32,
"SetPidOutMin": DATA_F32,
"SetPidOutMax": DATA_F32,
# Temperature Adc Internal Filters Settings
"ConfigTempAdcFilter": TEMP_ADC_FILTER,
# Temperature Monitor Settings
"SetTempMonUpperLimit": DATA_F32,
"SetTempMonLowerLimit": DATA_F32,
"ClearAlarm": None,
# Thermistor Parameter Settings
"SetShT0": DATA_F32,
"SetShR0": DATA_F32,
"SetShBeta": DATA_F32,
}
}
async def start_session(self, host='192.168.1.128', port=1337, timeout=None):
self._connecting_task = asyncio.create_task(
asyncio.wait_for(asyncio.open_connection(host, port), timeout)
)
self.timeout = timeout
try:
self._reader, self._writer = await self._connecting_task
writer_sock = self._writer.get_extra_info("socket")
writer_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
except asyncio.CancelledError:
raise StoppedConnecting
finally:
self._connecting_task = None
def connecting(self):
"""Returns True if client is connecting"""
return self._connecting_task is not None
def connected(self):
"""Returns True if client is connected"""
return self._writer is not None
async def end_session(self):
"""End session to Kirdy if connected, cancel connection if connecting"""
if self._connecting_task is not None:
self._connecting_task.cancel()
if self._writer is None:
return
# Reader needn't be closed
self._writer.close()
await self._writer.wait_closed()
self._reader = None
self._writer = None
async def _read_response(self, buffer_size=16384):
"""
Decode newline delimited Json objects and return the latest json received inside the buffer.
- buffer_size: Integer
"""
try:
raw_response = await asyncio.wait_for(self._reader.read(buffer_size), self.timeout)
response = raw_response.decode('utf-8', errors='ignore').split("\n")
for item in reversed(response):
try:
return json.loads(item)
except json.decoder.JSONDecodeError as e:
pass
return { "msg_type": "Exception"}
except TimeoutError:
return { "msg_type": "Exception"}
async def _send_raw_cmd_handler(self, cmd, lock=True, msg_type="Acknowledge"):
if lock:
async with self._cmd_lock:
return await asyncio.shield(self._send_raw_cmd(cmd, msg_type))
else:
return await asyncio.shield(self._send_raw_cmd(cmd, msg_type))
# If the cmd involves a cmd specific data type,
# checking is done separately within the functions being called
async def _send_raw_cmd(self, cmd, msg_type):
retry = 0
while retry < 10:
try:
self._writer.write(bytes(json.dumps(cmd), "UTF-8"))
await self._writer.drain()
response = await self._read_response()
if response["msg_type"] == msg_type:
return response
if response["msg_type"] == "InvalidCmd":
return InvalidCmd
except asyncio.exceptions.CancelledError:
return None
except Exception as e:
retry += 1
logging.error(f"_send_raw_cmd Exception: {e}")
await asyncio.sleep(0.1)
raise NoAckRecv
async def _send_cmd_handler(self, target, cmd, data=None, msg_type="Acknowledge", lock=True):
if lock:
async with self._cmd_lock:
return await asyncio.shield(self._send_cmd(target, cmd, data, msg_type))
else:
return await asyncio.shield(self._send_cmd(target, cmd, data, msg_type))
async def _send_cmd(self, target, cmd, data, msg_type):
cmd_dict = {}
if not(target in self._cmd_list.keys()) or not(cmd in self._cmd_list[target].keys()):
raise CmdDoesNotExist
cmd_dict[target] = cmd
if self._cmd_list[target][cmd] == DATA_F32:
if isinstance(data, float):
cmd_dict[DATA_F32] = data
elif isinstance(data, int):
cmd_dict[DATA_F32] = float(data)
elif self._cmd_list[target][cmd] == DATA_BOOL:
if isinstance(data, bool):
cmd_dict[DATA_BOOL] = data
else:
raise InvalidDataType
elif self._cmd_list[target][cmd] == None:
pass
else:
# Undefined Data Type
raise CmdDoesNotExist
retry = 0
while retry < 10:
try:
self._writer.write(bytes(json.dumps(cmd_dict), "UTF-8"))
await self._writer.drain()
response = await self._read_response()
if response["msg_type"] == msg_type:
return response
retry += 1
await asyncio.sleep(0.1)
except asyncio.exceptions.CancelledError:
return None
raise NoAckRecv
async def report_mode(self, report_interval = 0.0, buffer_size=16384):
"""
Start reporting device status in json object. Optional report_interval can be added to discard unwanted samples.
Only the latest status report received within the buffer is returned.
- polling interval: float/int (unit: seconds)
- buffer_size: int
"""
await self.device.set_active_report_mode(True)
self._report_mode_on = True
while self._report_mode_on:
await asyncio.sleep(report_interval)
async with self._cmd_lock:
yield await self._read_response(buffer_size)
await self.device.set_active_report_mode(False)
def stop_report_mode(self):
self._report_mode_on = False

View File

@ -1,840 +0,0 @@
from PyQt6 import QtWidgets, QtGui, QtCore
from PyQt6.QtCore import pyqtSignal, QObject, QSignalBlocker, pyqtSlot
import pyqtgraph.parametertree.parameterTypes as pTypes
from pyqtgraph.parametertree import Parameter, ParameterTree, ParameterItem, registerParameterType
import pyqtgraph as pg
pg.setConfigOptions(antialias=True)
from pyqtgraph import mkPen
from pglive.sources.live_axis_range import LiveAxisRange
from pglive.sources.data_connector import DataConnector
from pglive.kwargs import Axis, LeadingLine
from pglive.sources.live_plot import LiveLinePlot
from pglive.sources.live_plot_widget import LivePlotWidget
from pglive.sources.live_axis import LiveAxis
import sys
import os
import argparse
import logging
import asyncio
from driver.kirdy_async import Kirdy, StoppedConnecting
import qasync
from qasync import asyncSlot, asyncClose
from collections import deque
from datetime import datetime, timezone, timedelta
from time import time
from typing import Any, Optional, List
from ui.ui_conn_settings_form import Ui_Conn_Settings_Form
from ui.ui_update_network_settings_form import Ui_Update_Network_Settings_Form
from ui.ui_kirdy_qt import Ui_MainWindow
from dateutil import tz
import math
import socket
COMMON_ERROR_MSG = "Connection Timeout. Disconnecting."
def get_argparser():
parser = argparse.ArgumentParser(description="ARTIQ master")
parser.add_argument("--connect", default=None, action="store_true",
help="Automatically connect to the specified Thermostat in IP:port format")
parser.add_argument('IP', metavar="ip", default=None, nargs='?')
parser.add_argument('PORT', metavar="port", default=None, nargs='?')
parser.add_argument("-l", "--log", dest="logLevel", choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
help="Set the logging level")
return parser
class KirdyDataWatcher(QObject):
"""
This class provides various signals for Mainwindow to update various kinds of GUI objects
"""
connection_error_sig = pyqtSignal()
setting_update_sig = pyqtSignal(dict)
report_update_sig = pyqtSignal(dict)
def __init__(self, parent, kirdy, update_s):
self._update_s = update_s
self._kirdy = kirdy
self._watch_task = None
self._report_mode_task = None
self._poll_for_report = True
super().__init__(parent)
async def signal_emitter(self):
settings_summary = await self._kirdy.device.get_settings_summary()
self.setting_update_sig.emit(settings_summary)
if self._poll_for_report:
status_report = await self._kirdy.device.get_status_report()
self.report_update_sig.emit(status_report)
async def run(self):
try:
task = asyncio.create_task(self.signal_emitter())
while True:
if task.done():
_ = task.result()
task = asyncio.create_task(self.signal_emitter())
await asyncio.sleep(self._update_s)
except asyncio.CancelledError:
task.cancel()
except Exception as e:
logging.error(COMMON_ERROR_MSG)
self._kirdy.stop_report_mode()
self.connection_error_sig.emit()
def start_watching(self):
self._watch_task = asyncio.create_task(self.run())
async def stop_watching(self):
if self._watch_task is not None:
self._watch_task.cancel()
await self._watch_task
self._watch_task = None
await self.set_report_mode(False)
async def set_report_mode(self, enabled: bool):
self._poll_for_report = not enabled
if enabled:
self._report_mode_task = asyncio.create_task(self.report_mode())
else:
if self._report_mode_task is not None:
self._kirdy.stop_report_mode()
await self._report_mode_task
self._report_mode_task = None
async def report_mode(self):
try:
async for status_report in self._kirdy.report_mode():
if status_report["msg_type"] == "Exception":
raise TimeoutError("Connection Timeout")
self.report_update_sig.emit(status_report)
except Exception as e:
logging.error(f"{COMMON_ERROR_MSG}")
self.connection_error_sig.emit()
@pyqtSlot(float)
def set_update_s(self, update_s):
self._update_s = update_s
class Graphs:
def __init__(self, ld_i_set_graph, pd_mon_pwr_graph, tec_i_graph, tec_temp_graph, max_samples=1000):
self.graphs = [ld_i_set_graph, pd_mon_pwr_graph, tec_i_graph, tec_temp_graph]
self.connectors = []
self._pd_mon_pwr_plot = LiveLinePlot(pen=pg.mkPen('r'))
self._ld_i_set_plot = LiveLinePlot(name="Set", pen=pg.mkPen('r'))
self._tec_temp_plot = LiveLinePlot(pen=pg.mkPen('r'))
self._tec_setpoint_plot = LiveLinePlot(pen=pg.mkPen('r'))
self._tec_i_target_plot = LiveLinePlot(name="Target", pen=pg.mkPen('r'))
self._tec_i_measure_plot = LiveLinePlot(name="Measure", pen=pg.mkPen('g'))
self._temp_setpoint_line = tec_temp_graph.getPlotItem().addLine(label='{value} °C', pen=pg.mkPen('g'))
# Render the temperature setpoint line on top of the temperature being plotted
self._temp_setpoint_line.setZValue(10)
self._temp_setpoint_line.setVisible(False)
def tickStrings(values: List, scale: float, spacing: float) -> List:
return [datetime.fromtimestamp(value/1000, tz=timezone.utc).strftime("%H:%M:%S") for value in values]
for graph in ld_i_set_graph, pd_mon_pwr_graph, tec_i_graph, tec_temp_graph:
time_axis = LiveAxis('bottom', text="Time since Kirdy Reset (Hr:Min:Sec)", tick_angle=-45, units="")
# Display the relative ts in custom %H:%M:%S format without local timezone
time_axis.tickStrings = tickStrings
# Prevent scaling prefix being added to the back fo axis label
time_axis.autoSIPrefix = False
time_axis.showLabel()
graph.setAxisItems({'bottom': time_axis})
graph.add_crosshair(pg.mkPen(color='red', width=1), {'color': 'green'})
#TODO: x_range should not be updated on every tick
graph.x_range_controller = LiveAxisRange(roll_on_tick=17, offset_left=4900)
graph.x_range_controller.crop_left_offset_to_data = True
# Enable linking of axes in the graph widget's context menu
graph.register(graph.getPlotItem().titleLabel.text) # Slight hack getting the title
self.max_samples = max_samples
ld_i_set_axis = LiveAxis('left', text="Current", units="A")
ld_i_set_axis.showLabel()
ld_i_set_graph.setAxisItems({'left': ld_i_set_axis})
ld_i_set_graph.addItem(self._ld_i_set_plot)
self.ld_i_set_connector = DataConnector(self._ld_i_set_plot, max_points=self.max_samples)
self.connectors += [self.ld_i_set_connector]
pd_mon_pwr_axis = LiveAxis('left', text="Power", units="W")
pd_mon_pwr_axis.showLabel()
pd_mon_pwr_graph.setAxisItems({'left': pd_mon_pwr_axis})
pd_mon_pwr_graph.addItem(self._pd_mon_pwr_plot)
self.pd_mon_pwr_connector = DataConnector(self._pd_mon_pwr_plot, max_points=self.max_samples)
self.connectors += [self.pd_mon_pwr_connector]
tec_temp_axis = LiveAxis('left', text="Temperature", units="°C")
tec_temp_axis.showLabel()
tec_temp_graph.setAxisItems({'left': tec_temp_axis})
tec_temp_graph.addItem(self._tec_setpoint_plot)
tec_temp_graph.addItem(self._tec_temp_plot)
self.tec_setpoint_connector = DataConnector(self._tec_setpoint_plot, max_points=1)
self.tec_temp_connector = DataConnector(self._tec_temp_plot, max_points=self.max_samples)
self.connectors += [self.tec_temp_connector, self.tec_setpoint_connector]
tec_i_axis = LiveAxis('left', text="Current", units="A")
tec_i_axis.showLabel()
tec_i_graph.setAxisItems({'left': tec_i_axis})
tec_i_graph.addLegend(brush=(50, 50, 200, 150))
tec_i_graph.y_range_controller = LiveAxisRange(fixed_range=[-1.0, 1.0])
tec_i_graph.addItem(self._tec_i_target_plot)
tec_i_graph.addItem(self._tec_i_measure_plot)
self.tec_i_target_connector = DataConnector(self._tec_i_target_plot, max_points=self.max_samples)
self.tec_i_measure_connector = DataConnector(self._tec_i_measure_plot, max_points=self.max_samples)
self.connectors += [self.tec_i_target_connector, self.tec_i_measure_connector]
def set_max_samples(self, max_samples):
self.max_samples = max_samples
for connector in self.connectors:
with connector.data_lock:
connector.max_points = self.max_samples
connector.x = deque(maxlen=int(connector.max_points))
connector.y = deque(maxlen=int(connector.max_points))
def plot_append(self, report):
try:
ld_i_set = report['laser']['ld_i_set']
pd_pwr = report['laser']['pd_pwr']
tec_i_set = report['thermostat']['i_set']
tec_i_measure = report['thermostat']['tec_i']
tec_temp = report['thermostat']['temperature']
ts = report['ts']
self.ld_i_set_connector.cb_append_data_point(ld_i_set, ts)
self.pd_mon_pwr_connector.cb_append_data_point(pd_pwr, ts)
if tec_temp is not None:
self.tec_temp_connector.cb_append_data_point(tec_temp, ts)
if self._temp_setpoint_line.isVisible():
self.tec_setpoint_connector.cb_append_data_point(self._temp_setpoint_line.value(), ts)
else:
self.tec_setpoint_connector.cb_append_data_point(tec_temp, ts)
if tec_i_measure is not None:
self.tec_i_measure_connector.cb_append_data_point(tec_i_measure, ts)
self.tec_i_target_connector.cb_append_data_point(tec_i_set, ts)
except Exception as e:
logging.error(f"Graph Value cannot be updated. Data:{report}", exc_info=True)
def clear_data_pts(self):
for connector in self.connectors:
connector.clear()
connector.resume()
def set_temp_setpoint_line(self, temp=None, visible=None):
if visible is not None:
self._temp_setpoint_line.setVisible(visible)
if temp is not None:
self._temp_setpoint_line.setValue(temp)
# PyQtGraph normally does not update this text when the line
# is not visible, so make sure that the temperature label
# gets updated always, and doesn't stay at an old value.
self._temp_setpoint_line.label.setText(f"{temp} °C", color='g')
class MutexParameter(pTypes.ListParameter):
"""
Mutually exclusive parameter where only one of its children is visible at a time, list selectable.
The ordering of the list items determines which children will be visible.
"""
def __init__(self, **opts):
super().__init__(**opts)
self.sigValueChanged.connect(self.show_chosen_child)
self.sigValueChanged.emit(self, self.opts['value'])
def _get_param_from_value(self, value):
if isinstance(self.opts['limits'], dict):
values_list = list(self.opts['limits'].values())
else:
values_list = self.opts['limits']
return self.children()[values_list.index(value)]
@pyqtSlot(object, object)
def show_chosen_child(self, value):
for param in self.children():
param.hide()
child_to_show = self._get_param_from_value(value.value())
child_to_show.show()
if child_to_show.opts.get('triggerOnShow', None):
child_to_show.sigValueChanged.emit(child_to_show, child_to_show.value())
registerParameterType('mutex', MutexParameter)
class UpdateNetSettingsForm(QtWidgets.QDialog, Ui_Update_Network_Settings_Form):
def __init__(self):
super().__init__()
self.setupUi(self)
def get_net_settings(self):
try:
ip_addr = f"{self.addr_in_0.text()}.{self.addr_in_1.text()}.{self.addr_in_2.text()}.{self.addr_in_3.text()}"
gateway_addr = f"{self.gateway_in_0.text()}.{self.gateway_in_1.text()}.{self.gateway_in_2.text()}.{self.gateway_in_3.text()}"
socket.inet_aton(ip_addr)
socket.inet_aton(gateway_addr)
return {
"ip_addr": ip_addr,
"gateway_addr": gateway_addr,
"prefix_len": int(self.prefix_len_in.text()),
"port": int(self.port_in.text()),
}
except (OSError, ValueError):
return None
class ConnSettingsForm(QtWidgets.QDialog, Ui_Conn_Settings_Form):
def __init__(self):
super().__init__()
self.setupUi(self)
def get_net_settings(self):
try:
ip_addr = f"{self.addr_in_0.text()}.{self.addr_in_1.text()}.{self.addr_in_2.text()}.{self.addr_in_3.text()}"
socket.inet_aton(ip_addr)
return {
"ip_addr": ip_addr,
"port": int(self.port_in.text())
}
except (OSError, ValueError):
return None
class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
"""The maximum number of sample points to store."""
DEFAULT_MAX_SAMPLES = 1000
DEFAULT_IP_ADDR = '192.168.1.128'
DEFAULT_PORT = 1337
LASER_DIODE_STATUS = [
{'name': 'Power', 'type': 'color', 'value': 'w', 'readonly': True},
{'name': 'Alarm', 'type': 'color', 'value': 'w', 'readonly': True},
]
LASER_DIODE_PARAMETERS = [
{'name': 'Readings', 'expanded': True, 'type': 'group', 'children': [
{'name': 'LD Current Set', 'type': 'float', 'suffix': 'A', 'siPrefix': True, 'readonly': True},
{'name': 'PD Current', 'type': 'float', 'suffix': 'A', 'siPrefix': True, 'readonly': True},
{'name': 'PD Power', 'type': 'float', 'suffix': 'W', 'siPrefix': True, 'readonly': True},
{'name': 'LF Mod Impedance', 'type': 'list', 'limits': ['Is50Ohm', 'Not50Ohm'], 'readonly': True}
]},
{'name': 'Output Config', 'expanded': True, 'type': 'group', 'children': [
{'name': 'LD Current Set', 'type': 'float', 'value': 0, 'step': 1, 'decimals': 6, 'limits': (0, 1),
'suffix': 'A', 'siPrefix': True, 'target': 'laser', 'action': 'set_i'},
{'name': 'LD Current Set Soft Limit', 'type': 'float', 'value': 0, 'step': 1, 'decimals': 6, 'limits': (0, 1),
'suffix': 'A', 'siPrefix': True, 'target': 'laser', 'action': 'set_i_soft_limit'},
{'name': 'LD Power Limit', 'type': 'float', 'value': 0, 'step': 1, 'decimals': 6, 'limits': (0, 0.3),
'suffix': 'W', 'siPrefix': True, 'target': 'laser', 'action': 'set_ld_pwr_limit'},
{'name': 'LD Terminals Short', 'type': 'bool', 'value': False, 'target': 'laser', 'action': 'set_ld_terms_short'},
{'name': 'Default Power On', 'type': 'bool', 'value': False, 'target': 'laser', 'action': 'set_default_pwr_on'},
]},
{'name': 'Photodiode Monitor Config', 'expanded': False, 'type': 'group', 'children': [
{'name': 'Responsitivity', 'type': 'float', 'value': 0, 'step': 1, 'decimals': 6, 'limits': (0, 3000),
'suffix': 'A/W', 'siPrefix': True, 'target': 'laser', 'action': 'set_pd_mon_responsitivity'},
{'name': 'Dark Current', 'type': 'float', 'value': 0, 'step': 1, 'decimals': 6, 'limits': (0, 3000),
'suffix': 'A', 'siPrefix': True, 'target': 'laser', 'action': 'set_pd_mon_dark_current'},
]},
]
THERMOSTAT_STATUS = [
{'name': 'Power', 'type': 'color', 'value': 'w', 'readonly': True},
{'name': 'Alarm', 'type': 'color', 'value': 'w', 'readonly': True},
]
THERMOSTAT_PARAMETERS = [
{'name': 'Readings', 'expanded': True, 'type': 'group', 'children': [
{'name': 'Temperature', 'type': 'float', 'format': '{value:.4f} °C', 'readonly': True},
{'name': 'Current through TEC', 'type': 'float', 'suffix': 'A', 'siPrefix': True, 'decimals': 6, 'readonly': True},
]},
{'name': 'Output Config', 'expanded': True, 'type': 'group', 'children': [
{'name': 'Control Method', 'type': 'mutex', 'limits': ['Constant Current', 'Temperature PID'],
'target_action_pair': [['thermostat', 'set_constant_current_control_mode'], ['thermostat', 'set_pid_control_mode']], 'children': [
{'name': 'Set Current', 'type': 'float', 'value': 0, 'step': 0.001, 'limits': (-1, 1), 'triggerOnShow': True,
'decimals': 6, 'suffix': 'A', 'siPrefix': True, 'target': 'thermostat', 'action': 'set_tec_i_out'},
{'name': 'Set Temperature', 'type': 'float', 'value': 25, 'step': 0.1, 'limits': (-273, 300),
'format': '{value:.4f} °C', 'target': 'thermostat', 'action': 'set_temperature_setpoint'},
]},
{'name': 'Limits', 'expanded': False, 'type': 'group', 'children': [
{'name': 'Max Cooling Current', 'type': 'float', 'value': 0, 'step': 0.001, 'decimals': 6, 'limits': (0, 1),
'suffix': 'A', 'siPrefix': True, 'target': 'thermostat', 'action': 'set_tec_max_cooling_i'},
{'name': 'Max Heating Current', 'type': 'float', 'value': 0, 'step': 0.001, 'decimals': 6, 'limits': (0, 1),
'suffix': 'A', 'siPrefix': True, 'target': 'thermostat', 'action': 'set_tec_max_heating_i'},
{'name': 'Max Voltage Difference', 'type': 'float', 'value': 0, 'step': 0.1, 'limits': (0, 5),
'suffix': 'V', 'siPrefix': True, 'target': 'thermostat', 'action': 'set_tec_max_v'},
]},
{'name': 'Default Power On', 'type': 'bool', 'value': False, 'target': 'thermostat', 'action': 'set_default_pwr_on'},
]},
# TODO Temperature ADC Filter Settings
{'name': 'Temperature Monitor Config', 'expanded': False, 'type': 'group', 'children': [
{'name': 'Upper Limit', 'type': 'float', 'value': 0, 'step': 1, 'decimals': 6, 'limits': (-273, 300),
'suffix': '°C', 'target': 'thermostat', 'action': 'set_temp_mon_upper_limit'},
{'name': 'Lower Limit', 'type': 'float', 'value': 0, 'step': 1, 'decimals': 6, 'limits': (-273, 300),
'suffix': '°C', 'target': 'thermostat', 'action': 'set_temp_mon_upper_limit'},
]},
{'name': 'Thermistor Settings','expanded': False, 'type': 'group', 'children': [
{'name': 'T₀', 'type': 'float', 'value': 0, 'step': 1, 'decimals': 6, 'limits': (-273, 300),
'suffix': '°C', 'target': 'thermostat', 'action': 'set_sh_t0'},
{'name': 'R₀', 'type': 'float', 'value': 0, 'step': 1, 'decimals': 6,
'suffix': 'Ω', 'siPrefix': True, 'target': 'thermostat', 'action': 'set_sh_r0'},
{'name': 'B', 'type': 'float', 'value': 3950, 'step': 1, 'suffix': 'K', 'decimals': 4, 'target': 'thermostat', 'action': 'set_sh_beta'},
]},
{'name': 'PID Config', 'expanded': False, 'type': 'group', 'children': [
{'name': 'Kp', 'type': 'float', 'step': 0.1, 'suffix': '', 'target': 'thermostat', 'action': 'set_pid_kp'},
{'name': 'Ki', 'type': 'float', 'step': 0.1, 'suffix': 'Hz', 'target': 'thermostat', 'action': 'set_pid_ki'},
{'name': 'Kd', 'type': 'float', 'step': 0.1, 'suffix': 's', 'target': 'thermostat', 'action': 'set_pid_kd'},
{'name': "PID Output Clamping", 'expanded': True, 'type': 'group', 'children': [
{'name': 'Minimum', 'type': 'float', 'step': 100, 'limits': (-1, 1), 'decimals': 6, 'suffix': 'A', 'target': 'thermostat', 'action': 'set_pid_output_min'},
{'name': 'Maximum', 'type': 'float', 'step': 100, 'limits': (-1, 1), 'decimals': 6, 'suffix': 'A', 'target': 'thermostat', 'action': 'set_pid_output_max'},
]},
# TODO PID AutoTune
]},
]
def __init__(self, args):
super().__init__()
self.kirdy = Kirdy()
self.setupUi(self)
# Load Global QT Style Sheet Settings
qss=os.path.join(os.path.dirname(__file__), "ui/mainwindow.qss")
with open(qss,"r") as fh:
self.setStyleSheet(fh.read())
self.ip_addr = self.DEFAULT_IP_ADDR
self.port = self.DEFAULT_PORT
self.conn_settings_form = ConnSettingsForm()
self.conn_settings_form.accepted.connect(self.start_connecting)
self.update_net_settings_form = UpdateNetSettingsForm()
self.update_net_settings_form.accepted.connect(self.update_net_settings)
self.max_samples = self.DEFAULT_MAX_SAMPLES
self.setup_menu_bar()
self._set_up_ctrl_btns()
self._set_up_plot_menu()
self.params = [
Parameter.create(name=f"Laser Diode Status", type='group', value=0, children=self.LASER_DIODE_STATUS),
Parameter.create(name=f"Laser Diode Parameters", type='group', value=1, children=self.LASER_DIODE_PARAMETERS),
Parameter.create(name=f"Thermostat Status", type='group', value=2, children=self.THERMOSTAT_STATUS),
Parameter.create(name=f"Thermostat Parameters", type='group', value=3, children=self.THERMOSTAT_PARAMETERS),
]
self._set_param_tree()
self.tec_i_graph.setTitle("TEC Current")
self.tec_temp_graph.setTitle("TEC Temperature")
self.ld_i_set_graph.setTitle("LD Current Set")
self.pd_mon_pwr_graph.setTitle("PD Mon Power")
self.connect_btn.clicked.connect(self.show_conn_settings_form)
self.report_box.stateChanged.connect(self.on_report_box_stateChanged)
self.kirdy_data_watcher = KirdyDataWatcher(self, self.kirdy, self.report_refresh_spin.value())
self.kirdy_data_watcher.connection_error_sig.connect(self.bail)
# TODO: Identify the usable range of set_update_s
self.report_apply_btn.clicked.connect(
lambda: self.kirdy_data_watcher.set_update_s(self.report_refresh_spin.value())
)
self.kirdy_data_watcher.setting_update_sig.connect(self.update_ld_ctrl_panel_settings)
self.kirdy_data_watcher.setting_update_sig.connect(self.update_thermostat_ctrl_panel_settings)
self.kirdy_data_watcher.report_update_sig.connect(self.update_ld_ctrl_panel_readings)
self.kirdy_data_watcher.report_update_sig.connect(self.update_thermostat_ctrl_panel_readings)
self.graphs = Graphs(self.ld_i_set_graph, self.pd_mon_pwr_graph, self.tec_i_graph, self.tec_temp_graph, max_samples=self.max_samples)
self.kirdy_data_watcher.report_update_sig.connect(self.graphs.plot_append)
self.loading_spinner.hide()
def setup_menu_bar(self):
@pyqtSlot(bool)
def about_kirdy(_):
# TODO: Replace the hardware revision placeholder
QtWidgets.QMessageBox.about(
self,
"About Kirdy",
f"""
<h1>Sinara 1550 Kirdy v"major rev"."minor rev"</h1>
"""
)
self.menu_action_about_kirdy.triggered.connect(about_kirdy)
@pyqtSlot(bool)
def about_gui(_):
# TODO: Replace the hardware revision placeholder
QtWidgets.QMessageBox.about(
self,
"About Kirdy Control Panel",
f"""
<h1>Version: "Version"</h1>
"""
)
self.menu_action_about_gui.triggered.connect(about_gui)
@asyncSlot(bool)
async def dfu_mode(_):
await self.kirdy.device.dfu()
await self._on_connection_changed(False)
self.menu_action_dfu_mode.triggered.connect(dfu_mode)
@asyncSlot(bool)
async def reset_kirdy(_):
await self._on_connection_changed(False, hard_reset=True)
self.menu_action_hard_reset.triggered.connect(reset_kirdy)
@asyncSlot(bool)
async def save_settings(_):
await self.kirdy.device.save_current_settings_to_flash()
saved = QtWidgets.QMessageBox(self)
saved.setWindowTitle("Config saved")
saved.setText(f"Laser diode and thermostat configs have been saved into flash.")
saved.setIcon(QtWidgets.QMessageBox.Icon.Information)
saved.show()
self.menu_action_save.triggered.connect(save_settings)
@asyncSlot(bool)
async def load_settings(_):
await self.kirdy.device.load_current_settings_from_flash()
loaded = QtWidgets.QMessageBox(self)
loaded.setWindowTitle("Config loaded")
loaded.setText(f"Laser Diode and Thermostat configs have been loaded from flash.")
loaded.setIcon(QtWidgets.QMessageBox.Icon.Information)
loaded.show()
self.menu_action_load.triggered.connect(load_settings)
@asyncSlot(bool)
async def show_update_net_settings_form(_):
self.update_net_settings_form.retranslateUi(self.update_net_settings_form)
self.update_net_settings_form.show()
self.menu_action_update_net_settings.triggered.connect(show_update_net_settings_form)
def show_conn_settings_form(self):
ip_addr = self.ip_addr.split(".")
self.conn_settings_form.addr_in_0.setText(ip_addr[0])
self.conn_settings_form.addr_in_1.setText(ip_addr[1])
self.conn_settings_form.addr_in_2.setText(ip_addr[2])
self.conn_settings_form.addr_in_3.setText(ip_addr[3])
self.conn_settings_form.port_in.setText(str(self.port))
self.conn_settings_form.show()
def _set_up_ctrl_btns(self):
@asyncSlot(bool)
async def ld_pwr_on(_):
await self.kirdy.laser.set_power_on(True)
self.ld_pwr_on_btn.clicked.connect(ld_pwr_on)
@asyncSlot(bool)
async def ld_pwr_off(_):
await self.kirdy.laser.set_power_on(False)
self.ld_pwr_off_btn.clicked.connect(ld_pwr_off)
@asyncSlot(bool)
async def ld_clear_alarm(_):
await self.kirdy.laser.clear_alarm()
self.ld_clear_alarm_btn.clicked.connect(ld_clear_alarm)
@asyncSlot(bool)
async def tec_pwr_on(_):
await self.kirdy.thermostat.set_power_on(True)
self.tec_pwr_on_btn.clicked.connect(tec_pwr_on)
@asyncSlot(bool)
async def tec_pwr_off(_):
await self.kirdy.thermostat.set_power_on(False)
self.tec_pwr_off_btn.clicked.connect(tec_pwr_off)
@asyncSlot(bool)
async def tec_clear_alarm(_):
await self.kirdy.thermostat.clear_alarm()
self.tec_clear_alarm_btn.clicked.connect(tec_clear_alarm)
def _set_up_plot_menu(self):
self.plot_menu = QtWidgets.QMenu()
self.plot_menu.setTitle("Plot Settings")
clear = QtGui.QAction("Clear graphs", self.plot_menu)
clear.triggered.connect(self.clear_graphs)
self.plot_menu.addAction(clear)
self.plot_menu.clear = clear
self.samples_spinbox = QtWidgets.QSpinBox()
self.samples_spinbox.setRange(2, 100000)
self.samples_spinbox.setSuffix(' samples')
self.samples_spinbox.setValue(self.max_samples)
self.samples_spinbox.valueChanged.connect(self.set_max_samples)
limit_samples = QtWidgets.QWidgetAction(self.plot_menu)
limit_samples.setDefaultWidget(self.samples_spinbox)
self.plot_menu.addAction(limit_samples)
self.plot_menu.limit_samples = limit_samples
self.plot_settings.setMenu(self.plot_menu)
def _set_param_tree(self):
status = self.ld_status
status.setHeaderHidden(True)
status.setParameters(self.params[0], showTop=False)
tree = self.ld_tree
tree.setHeaderHidden(True)
tree.setParameters(self.params[1], showTop=False)
self.params[1].sigTreeStateChanged.connect(self.send_command)
status = self.tec_status
status.setHeaderHidden(True)
status.setParameters(self.params[2], showTop=False)
tree = self.tec_tree
tree.setHeaderHidden(True)
tree.setParameters(self.params[3], showTop=False)
self.params[3].sigTreeStateChanged.connect(self.send_command)
async def _on_connection_changed(self, result, hard_reset=False):
def ctrl_panel_setEnable(result):
self.ld_status.setEnabled(result)
self.ld_tree.setEnabled(result)
self.ld_pwr_on_btn.setEnabled(result)
self.ld_pwr_off_btn.setEnabled(result)
self.ld_clear_alarm_btn.setEnabled(result)
self.tec_status.setEnabled(result)
self.tec_tree.setEnabled(result)
self.tec_pwr_on_btn.setEnabled(result)
self.tec_pwr_off_btn.setEnabled(result)
self.tec_clear_alarm_btn.setEnabled(result)
ctrl_panel_setEnable(result)
def menu_bar_setEnable(result):
self.menu_action_about_kirdy.setEnabled(result)
self.menu_action_connect.setEnabled(result)
self.menu_action_dfu_mode.setEnabled(result)
self.menu_action_disconnect.setEnabled(result)
self.menu_action_hard_reset.setEnabled(result)
self.menu_action_save.setEnabled(result)
self.menu_action_load.setEnabled(result)
self.menu_action_update_net_settings.setEnabled(result)
menu_bar_setEnable(result)
def graph_group_setEnable(result):
self.ld_i_set_graph.setEnabled(result)
self.pd_mon_pwr_graph.setEnabled(result)
self.tec_i_graph.setEnabled(result)
self.tec_temp_graph.setEnabled(result)
graph_group_setEnable(result)
self.report_refresh_spin.setEnabled(result)
self.report_group.setEnabled(result)
self.report_refresh_spin.setEnabled(result)
self.report_box.setEnabled(result)
self.report_apply_btn.setEnabled(result)
# TODO: Use QStateMachine to manage connections
self.connect_btn.clicked.disconnect()
if result:
self.connect_btn.setText("Disconnect")
self.connect_btn.clicked.connect(self.bail)
else:
self.connect_btn.setText("Connect")
self.connect_btn.clicked.connect(self.show_conn_settings_form)
if result:
# TODO: self.hw_rev_data = await self.kirdy.hw_rev()
self._status()
self.kirdy_data_watcher.start_watching()
else:
self.clear_graphs()
self.report_box.setChecked(False)
await self.kirdy_data_watcher.stop_watching()
if hard_reset:
await self.kirdy.device.hard_reset()
await self.kirdy.end_session()
self.status_lbl.setText("Disconnected")
def _status(self):
# TODO: Get rev no from Kirdy and then add revision into the text
host = self.ip_addr
port = self.port
self.status_lbl.setText(f"Connected to Kirdy @ {host}:{port}")
def clear_graphs(self):
self.graphs.clear_data_pts()
@asyncSlot(dict)
async def graphs_update(self, report):
self.graphs.plot_append(report)
@asyncSlot(dict)
async def update_ld_ctrl_panel_settings(self, settings):
try:
settings = settings['laser']
with QSignalBlocker(self.params[1]):
self.params[1].child('Output Config', 'LD Current Set').setValue(settings["ld_drive_current"]['value'])
self.params[1].child('Output Config', 'LD Current Set Soft Limit').setValue(settings["ld_drive_current_limit"]['value'])
self.params[1].child('Output Config', 'LD Power Limit').setValue(settings["ld_pwr_limit"])
self.params[1].child('Output Config', 'LD Terminals Short').setValue(settings["ld_terms_short"])
self.params[1].child('Output Config', 'Default Power On').setValue(settings["default_pwr_on"])
if settings["pd_mon_params"]["responsitivity"] is not None:
self.params[1].child('Photodiode Monitor Config', 'Responsitivity').setValue(settings["pd_mon_params"]["responsitivity"])
else:
self.params[1].child('Photodiode Monitor Config', 'Responsitivity').setValue(0)
self.params[1].child('Photodiode Monitor Config', 'Dark Current').setValue(settings["pd_mon_params"]["i_dark"])
except Exception as e:
logging.error(f"Params tree cannot be updated. Data:{settings}", exc_info=True)
@asyncSlot(dict)
async def update_ld_ctrl_panel_readings(self, report):
try:
report = report['laser']
with QSignalBlocker(self.params[0]):
self.params[0].child('Power').setValue('g' if report['pwr_on'] else 'w')
self.params[0].child('Alarm').setValue('r' if report['pwr_excursion'] else 'w')
with QSignalBlocker(self.params[1]):
self.params[1].child('Readings', 'LD Current Set').setValue(report["ld_i_set"])
self.params[1].child('Readings', 'PD Current').setValue(report["pd_i"])
if report["pd_pwr"] is not None:
self.params[1].child('Readings', 'PD Power').setValue(report["pd_pwr"])
else:
self.params[1].child('Readings', 'PD Power').setValue(0)
self.params[1].child('Readings', 'LF Mod Impedance').setValue(report["term_status"])
except Exception as e:
logging.error(f"Params tree cannot be updated. Data:{report}", exc_info=True)
@asyncSlot(dict)
async def update_thermostat_ctrl_panel_settings(self, settings):
try:
settings = settings['thermostat']
with QSignalBlocker(self.params[3]):
self.params[3].child('Output Config', 'Control Method').setValue("Temperature PID" if settings["pid_engaged"] else "Constant Current")
self.params[3].child('Output Config', 'Control Method', 'Set Current').setValue(settings["tec_settings"]['i_set']['value'])
self.params[3].child('Output Config', 'Control Method', 'Set Temperature').setValue(float(settings["temperature_setpoint"]))
self.params[3].child('Output Config', 'Limits', 'Max Cooling Current').setValue(settings["tec_settings"]['max_i_pos']['value'])
self.params[3].child('Output Config', 'Limits', 'Max Heating Current').setValue(settings["tec_settings"]['max_i_neg']['value'])
self.params[3].child('Output Config', 'Limits', 'Max Voltage Difference').setValue(settings["tec_settings"]['max_v']['value'])
self.params[3].child('Output Config', 'Default Power On').setValue(settings["default_pwr_on"])
# TODO: Update the Temperature ADC Settings here as well
self.params[3].child('Temperature Monitor Config', 'Upper Limit').setValue(settings["temp_mon_settings"]['upper_limit'])
self.params[3].child('Temperature Monitor Config', 'Lower Limit').setValue(settings["temp_mon_settings"]['lower_limit'])
self.params[3].child('PID Config', 'Kp').setValue(settings["pid_params"]['kp'])
self.params[3].child('PID Config', 'Ki').setValue(settings["pid_params"]['ki'])
self.params[3].child('PID Config', 'Kd').setValue(settings["pid_params"]['kd'])
self.params[3].child('PID Config', 'PID Output Clamping', 'Minimum').setValue(settings["pid_params"]['output_min'])
self.params[3].child('PID Config', 'PID Output Clamping', 'Maximum').setValue(settings["pid_params"]['output_max'])
self.params[3].child('Thermistor Settings', 'T₀').setValue(settings["thermistor_params"]['t0'])
self.params[3].child('Thermistor Settings', 'R₀').setValue(settings["thermistor_params"]['r0'])
self.params[3].child('Thermistor Settings', 'B').setValue(settings["thermistor_params"]['b'])
self.graphs.set_temp_setpoint_line(temp=round(settings["temperature_setpoint"], 6))
self.graphs.set_temp_setpoint_line(visible=settings['pid_engaged'])
except Exception as e:
logging.error(f"Params tree cannot be updated. Data:{settings}", exc_info=True)
@asyncSlot(dict)
async def update_thermostat_ctrl_panel_readings(self, report):
try:
report = report['thermostat']
with QSignalBlocker(self.params[2]):
self.params[2].child('Power').setValue('g' if report['pwr_on'] else 'w')
self.params[2].child('Alarm').setValue('r' if report['temp_mon_status']['over_temp_alarm'] else 'w')
with QSignalBlocker(self.params[3]):
self.params[3].child('Readings', 'Temperature').setValue(report["temperature"])
self.params[3].child('Readings', 'Current through TEC').setValue(report["tec_i"])
except Exception as e:
logging.error(f"Params tree cannot be updated. Data:{report}", exc_info=True)
@pyqtSlot(int)
def set_max_samples(self, samples: int):
self.graphs.set_max_samples(samples)
@asyncSlot(int)
async def on_report_box_stateChanged(self, enabled):
await self.kirdy_data_watcher.set_report_mode(enabled)
@asyncSlot()
async def update_net_settings(self):
net_settings = self.update_net_settings_form.get_net_settings()
if net_settings is None:
self.status_lbl.setText("Invalid IP Settings Input")
return
addr = net_settings["ip_addr"]
port = net_settings["port"]
prefix_len = net_settings["prefix_len"]
gateway = net_settings["gateway_addr"]
await self.kirdy.device.set_ip_settings(addr, port, prefix_len, gateway)
self.status_lbl.setText("IP Settings is Updated")
@asyncSlot()
async def start_connecting(self):
net_settings = self.conn_settings_form.get_net_settings()
if net_settings is None:
self.status_lbl.setText("Invalid IP Settings Input")
return
self.ip_addr = net_settings["ip_addr"]
self.port = net_settings["port"]
host = self.ip_addr
port = self.port
try:
if not (self.kirdy.connecting() or self.kirdy.connected()):
self.status_lbl.setText("Connecting...")
await self.kirdy.start_session(host=host, port=port, timeout=0.1)
await self._on_connection_changed(True)
else:
await self.bail()
except (OSError, TimeoutError, ConnectionResetError) as e:
logging.error(f"Failed communicating to {host}:{port}: {e}")
await self.bail()
self.status_lbl.setText(f"Cannot connect to Kirdy@ {host}:{port}")
@asyncSlot()
async def bail(self):
await self._on_connection_changed(False)
await self.kirdy.end_session()
@asyncSlot(object, object)
async def send_command(self, param, changes):
for inner_param, change, data in changes:
if change == 'value':
""" cmd translation from mutex type parameter """
if inner_param.opts.get('target_action_pair', None) is not None:
target, action = inner_param.opts['target_action_pair'][inner_param.opts['limits'].index(data)]
cmd = getattr(getattr(self.kirdy, target), action)
await cmd()
continue
""" cmd translation from non-mutex type parameter"""
if inner_param.opts.get("target", None) is not None:
if inner_param.opts.get("action", None) is not None:
cmd = getattr(getattr(self.kirdy, inner_param.opts["target"]), inner_param.opts["action"])
await cmd(data)
continue
async def coro_main():
args = get_argparser().parse_args()
if args.logLevel:
logging.basicConfig(level=getattr(logging, args.logLevel))
app_quit_event = asyncio.Event()
app = QtWidgets.QApplication.instance()
app.aboutToQuit.connect(app_quit_event.set)
main_window = MainWindow(args)
main_window.show()
await app_quit_event.wait()
def main():
qasync.run(coro_main())
if __name__ == '__main__':
main()

View File

@ -1,316 +0,0 @@
import math
import logging
from collections import deque, namedtuple
from enum import Enum
import socket
import json
import time
import signal
from driver.kirdy_async import Kirdy
import asyncio
# Based on hirshmann pid-autotune libiary
# See https://github.com/hirschmann/pid-autotune
# Which is in turn based on a fork of Arduino PID AutoTune Library
# See https://github.com/t0mpr1c3/Arduino-PID-AutoTune-Library
class PIDAutotuneState(Enum):
STATE_OFF = 'off'
STATE_RELAY_STEP_UP = 'relay step up'
STATE_RELAY_STEP_DOWN = 'relay step down'
STATE_SUCCEEDED = 'succeeded'
STATE_FAILED = 'failed'
class PIDAutotune:
PIDParams = namedtuple('PIDParams', ['Kp', 'Ki', 'Kd'])
PEAK_AMPLITUDE_TOLERANCE = 0.05
_tuning_rules = {
"ziegler-nichols": [0.6, 1.2, 0.075],
"tyreus-luyben": [0.4545, 0.2066, 0.07214],
"ciancone-marlin": [0.303, 0.1364, 0.0481],
"pessen-integral": [0.7, 1.75, 0.105],
"some-overshoot": [0.333, 0.667, 0.111],
"no-overshoot": [0.2, 0.4, 0.0667]
}
def __init__(self, setpoint, out_step=10, lookback=60,
noiseband=0.5, sampletime=1.2):
if setpoint is None:
raise ValueError('setpoint must be specified')
self._inputs = deque(maxlen=round(lookback / sampletime))
self._setpoint = setpoint
self._outputstep = out_step
self._noiseband = noiseband
self._out_min = -out_step
self._out_max = out_step
self._state = PIDAutotuneState.STATE_OFF
self._peak_timestamps = deque(maxlen=5)
self._peaks = deque(maxlen=5)
self._output = 0
self._last_run_timestamp = 0
self._peak_type = 0
self._peak_count = 0
self._initial_output = 0
self._induced_amplitude = 0
self._Ku = 0
self._Pu = 0
def state(self):
"""Get the current state."""
return self._state
def output(self):
"""Get the last output value."""
return self._output
def tuning_rules(self):
"""Get a list of all available tuning rules."""
return self._tuning_rules.keys()
def get_pid_parameters(self, tuning_rule='ziegler-nichols'):
"""Get PID parameters.
Args:
tuning_rule (str): Sets the rule which should be used to calculate
the parameters.
"""
divisors = self._tuning_rules[tuning_rule]
kp = self._Ku * divisors[0]
ki = divisors[1] * self._Ku / self._Pu
kd = divisors[2] * self._Ku * self._Pu
return PIDAutotune.PIDParams(kp, ki, kd)
def run(self, input_val, time_input):
"""To autotune a system, this method must be called periodically.
Args:
input_val (float): The temperature input value.
time_input (float): Current time in seconds.
Returns:
`true` if tuning is finished, otherwise `false`.
"""
now = time_input * 1000
if (self._state == PIDAutotuneState.STATE_OFF
or self._state == PIDAutotuneState.STATE_SUCCEEDED
or self._state == PIDAutotuneState.STATE_FAILED):
self._state = PIDAutotuneState.STATE_RELAY_STEP_UP
self._last_run_timestamp = now
# check input and change relay state if necessary
if (self._state == PIDAutotuneState.STATE_RELAY_STEP_UP
and input_val > self._setpoint + self._noiseband):
self._state = PIDAutotuneState.STATE_RELAY_STEP_DOWN
logging.debug('switched state: {0}'.format(self._state))
logging.debug('input: {0}'.format(input_val))
elif (self._state == PIDAutotuneState.STATE_RELAY_STEP_DOWN
and input_val < self._setpoint - self._noiseband):
self._state = PIDAutotuneState.STATE_RELAY_STEP_UP
logging.debug('switched state: {0}'.format(self._state))
logging.debug('input: {0}'.format(input_val))
# set output
if (self._state == PIDAutotuneState.STATE_RELAY_STEP_UP):
self._output = self._initial_output - self._outputstep
elif self._state == PIDAutotuneState.STATE_RELAY_STEP_DOWN:
self._output = self._initial_output + self._outputstep
# respect output limits
self._output = min(self._output, self._out_max)
self._output = max(self._output, self._out_min)
# identify peaks
is_max = True
is_min = True
for val in self._inputs:
is_max = is_max and (input_val >= val)
is_min = is_min and (input_val <= val)
self._inputs.append(input_val)
# we don't trust the maxes or mins until the input array is full
if len(self._inputs) < self._inputs.maxlen:
return False
# increment peak count and record peak time for maxima and minima
inflection = False
# peak types:
# -1: minimum
# +1: maximum
if is_max:
if self._peak_type == -1:
inflection = True
self._peak_type = 1
elif is_min:
if self._peak_type == 1:
inflection = True
self._peak_type = -1
# update peak times and values
if inflection:
self._peak_count += 1
self._peaks.append(input_val)
self._peak_timestamps.append(now)
logging.debug('found peak: {0}'.format(input_val))
logging.debug('peak count: {0}'.format(self._peak_count))
# check for convergence of induced oscillation
# convergence of amplitude assessed on last 4 peaks (1.5 cycles)
self._induced_amplitude = 0
if inflection and (self._peak_count > 4):
abs_max = self._peaks[-2]
abs_min = self._peaks[-2]
for i in range(0, len(self._peaks) - 2):
self._induced_amplitude += abs(self._peaks[i]
- self._peaks[i+1])
abs_max = max(self._peaks[i], abs_max)
abs_min = min(self._peaks[i], abs_min)
self._induced_amplitude /= 6.0
# check convergence criterion for amplitude of induced oscillation
amplitude_dev = ((0.5 * (abs_max - abs_min)
- self._induced_amplitude)
/ self._induced_amplitude)
logging.debug('amplitude: {0}'.format(self._induced_amplitude))
logging.debug('amplitude deviation: {0}'.format(amplitude_dev))
if amplitude_dev < PIDAutotune.PEAK_AMPLITUDE_TOLERANCE:
self._state = PIDAutotuneState.STATE_SUCCEEDED
# if the autotune has not already converged
# terminate after 10 cycles
if self._peak_count >= 20:
self._output = 0
self._state = PIDAutotuneState.STATE_FAILED
return True
if self._state == PIDAutotuneState.STATE_SUCCEEDED:
self._output = 0
logging.debug('peak finding successful')
# calculate ultimate gain
self._Ku = 4.0 * self._outputstep / \
(self._induced_amplitude * math.pi)
print('Ku: {0}'.format(self._Ku))
# calculate ultimate period in seconds
period1 = self._peak_timestamps[3] - self._peak_timestamps[1]
period2 = self._peak_timestamps[4] - self._peak_timestamps[2]
self._Pu = 0.5 * (period1 + period2) / 1000.0
print('Pu: {0}'.format(self._Pu))
for rule in self._tuning_rules:
params = self.get_pid_parameters(rule)
print('rule: {0}'.format(rule))
print('Kp: {0}'.format(params.Kp))
print('Ki: {0}'.format(params.Ki))
print('Kd: {0}'.format(params.Kd))
return True
return False
async def main():
"""
PID AutoTune Tools for Kirdy
The obtained temperature works best at the target temperature specified.
Before running PID AutoTune, please
1. Secure the laser diode onto the LD adapter and copper heat sink
2. Make sure Kirdy has warmed up and reached thermal equilibrium state
In case of PID Autotune Failure, you can
1. Run the PID Autotune again
2. Or increase the lookback period
3. Or increase the sampling rate
"""
# Target temperature of the autotune routine, celsius
target_temperature = 20
# Value by which output will be increased/decreased from zero, amps
output_step = 1
# Reference period for local minima/maxima, seconds
lookback = 2.0
# Determines by how much the input value must
# overshoot/undershoot the setpoint, celsius
noiseband = 1.5
kirdy = Kirdy()
await kirdy.start_session(host='192.168.1.128', port=1337, timeout=0.25)
await kirdy.laser.set_power_on(False)
await kirdy.laser.set_i(0)
await kirdy.thermostat.set_power_on(False)
await kirdy.thermostat.set_constant_current_control_mode()
await kirdy.thermostat.set_tec_i_out(0)
await kirdy.thermostat.clear_alarm()
class SignalHandler:
KEEP_PROCESSING = True
def __init__(self):
signal.signal(signal.SIGINT, self.exit_gracefully)
signal.signal(signal.SIGTERM, self.exit_gracefully)
def exit_gracefully(self, signum, frame):
self.KEEP_PROCESSING = False
signal_handler = SignalHandler()
await kirdy.device.set_active_report_mode(False)
# Configure the Thermistor Parameters
await kirdy.thermostat.set_sh_beta(3900)
await kirdy.thermostat.set_sh_r0(10.0 * 1000)
await kirdy.thermostat.set_sh_t0(25)
# Set a large enough temperature range so that it won't trigger overtemperature protection
await kirdy.thermostat.set_temp_mon_upper_limit(target_temperature + 20)
await kirdy.thermostat.set_temp_mon_lower_limit(target_temperature - 20)
await kirdy.thermostat.set_tec_max_cooling_i(output_step)
await kirdy.thermostat.set_tec_max_heating_i(output_step)
# The Polling Rate of Temperature Adc is equal to the PID Update Interval
await kirdy.thermostat.config_temp_adc_filter("Sinc5Sinc1With50hz60HzRejection", "F16SPS")
settings = await kirdy.device.get_settings_summary()
sampling_rate = settings["thermostat"]["temp_adc_settings"]["rate"]
print("Settings: {0}".format(settings))
tuner = PIDAutotune(target_temperature, output_step,
lookback, noiseband, 1/sampling_rate)
await kirdy.thermostat.set_power_on(True)
while True and signal_handler.KEEP_PROCESSING:
status_report = await kirdy.device.get_status_report()
temperature = status_report["thermostat"]["temperature"]
ts = status_report['ts']
print("Ts: {0} Current Temperature: {1} degree".format(ts, temperature))
if (tuner.run(temperature, ts / 1000.0)):
print(tuner._state)
break
tuner_out = tuner.output()
await kirdy.thermostat.set_tec_i_out(float(tuner_out))
await kirdy.thermostat.set_tec_i_out(0)
await kirdy.thermostat.set_power_on(False)
await kirdy.end_session()
if __name__ == "__main__":
asyncio.run(main())

View File

@ -1,376 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Conn_Settings_Form</class>
<widget class="QDialog" name="Conn_Settings_Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>415</width>
<height>145</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>415</width>
<height>145</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>415</width>
<height>145</height>
</size>
</property>
<property name="windowTitle">
<string>Connection Settings</string>
</property>
<widget class="QWidget" name="horizontalLayoutWidget">
<property name="geometry">
<rect>
<x>20</x>
<y>10</y>
<width>371</width>
<height>41</height>
</rect>
</property>
<layout class="QHBoxLayout" name="ip_addr_layout">
<item>
<widget class="QLabel" name="ip_addr_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>120</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="layoutDirection">
<enum>Qt::LeftToRight</enum>
</property>
<property name="text">
<string>IP Address:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="addr_in_0">
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>192</string>
</property>
<property name="maxLength">
<number>3</number>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="dot_0_label">
<property name="maximumSize">
<size>
<width>10</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
<bold>false</bold>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string>.</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="addr_in_1">
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>168</string>
</property>
<property name="maxLength">
<number>3</number>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="dot_1_label">
<property name="maximumSize">
<size>
<width>10</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
<bold>false</bold>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string>.</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="addr_in_2">
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>1</string>
</property>
<property name="maxLength">
<number>3</number>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="dot_2_label">
<property name="maximumSize">
<size>
<width>10</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
<bold>false</bold>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string>.</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="addr_in_3">
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>128</string>
</property>
<property name="maxLength">
<number>3</number>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="horizontalLayoutWidget_2">
<property name="geometry">
<rect>
<x>20</x>
<y>50</y>
<width>371</width>
<height>41</height>
</rect>
</property>
<layout class="QHBoxLayout" name="port_layout" stretch="3,0,6">
<item>
<widget class="QLabel" name="port_no_label">
<property name="maximumSize">
<size>
<width>97</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="text">
<string>Port:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="port_in">
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>1337</string>
</property>
<property name="maxLength">
<number>5</number>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<spacer name="port_layout_spacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Preferred</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>50</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="horizontalLayoutWidget_4">
<property name="geometry">
<rect>
<x>20</x>
<y>90</y>
<width>371</width>
<height>47</height>
</rect>
</property>
<layout class="QHBoxLayout" name="buttons_layout">
<property name="topMargin">
<number>0</number>
</property>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="connect_btn">
<property name="text">
<string>Connect</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cancel_btn">
<property name="text">
<string>Cancel</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<tabstops>
<tabstop>addr_in_0</tabstop>
<tabstop>addr_in_1</tabstop>
<tabstop>addr_in_2</tabstop>
<tabstop>addr_in_3</tabstop>
<tabstop>port_in</tabstop>
<tabstop>connect_btn</tabstop>
<tabstop>cancel_btn</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>cancel_btn</sender>
<signal>clicked()</signal>
<receiver>Conn_Settings_Form</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>340</x>
<y>140</y>
</hint>
<hint type="destinationlabel">
<x>364</x>
<y>96</y>
</hint>
</hints>
</connection>
<connection>
<sender>connect_btn</sender>
<signal>clicked()</signal>
<receiver>Conn_Settings_Form</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>258</x>
<y>147</y>
</hint>
<hint type="destinationlabel">
<x>237</x>
<y>99</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -1,806 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1280</width>
<height>720</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>1280</width>
<height>720</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>3840</width>
<height>2160</height>
</size>
</property>
<property name="windowTitle">
<string>Kirdy Control Panel</string>
</property>
<property name="windowIcon">
<iconset theme="application-x-executable"/>
</property>
<widget class="QWidget" name="main_widget">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>1</horstretch>
<verstretch>1</verstretch>
</sizepolicy>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<property name="spacing">
<number>3</number>
</property>
<item row="0" column="0">
<layout class="QVBoxLayout" name="main_layout">
<property name="spacing">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="1,2">
<property name="spacing">
<number>0</number>
</property>
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<item>
<layout class="QVBoxLayout" name="ctrl_vertical_layout" stretch="1,1,10,1,1,1,10,1">
<property name="spacing">
<number>0</number>
</property>
<property name="sizeConstraint">
<enum>QLayout::SetMaximumSize</enum>
</property>
<item>
<widget class="QLabel" name="ld_section_label">
<property name="font">
<font>
<pointsize>14</pointsize>
<bold>true</bold>
</font>
</property>
<property name="text">
<string> Laser Diode</string>
</property>
<property name="wordWrap">
<bool>false</bool>
</property>
<property name="margin">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="ParameterTree" name="ld_status" native="true">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>55</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="ParameterTree" name="ld_tree" native="true">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="ld_btns_layout">
<property name="spacing">
<number>30</number>
</property>
<property name="leftMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<item>
<widget class="QPushButton" name="ld_pwr_on_btn">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>POWER ON</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="ld_pwr_off_btn">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>POWER OFF</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="ld_clear_alarm_btn">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>CLEAR ALARM</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="tec_section_label">
<property name="font">
<font>
<pointsize>14</pointsize>
<bold>true</bold>
</font>
</property>
<property name="text">
<string> Thermostat</string>
</property>
<property name="wordWrap">
<bool>false</bool>
</property>
<property name="margin">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="ParameterTree" name="tec_status" native="true">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>55</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="ParameterTree" name="tec_tree" native="true">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="tec_btns_layout">
<property name="spacing">
<number>30</number>
</property>
<property name="leftMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<item>
<widget class="QPushButton" name="tec_pwr_on_btn">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>POWER ON</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="tec_pwr_off_btn">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>POWER OFF</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="tec_clear_alarm_btn">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>CLEAR ALAM</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QGridLayout" name="graphgroup">
<property name="sizeConstraint">
<enum>QLayout::SetNoConstraint</enum>
</property>
<property name="spacing">
<number>0</number>
</property>
<item row="1" column="0">
<widget class="LivePlotWidget" name="tec_temp_graph" native="true">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="LivePlotWidget" name="tec_i_graph" native="true">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="LivePlotWidget" name="pd_mon_pwr_graph" native="true">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="LivePlotWidget" name="ld_i_set_graph" native="true">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<widget class="QFrame" name="bottom_settings_group">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>40</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>40</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="spacing">
<number>3</number>
</property>
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<item>
<layout class="QHBoxLayout" name="settings_layout">
<item>
<widget class="QPushButton" name="connect_btn">
<property name="text">
<string>Connect</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="status_lbl">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>360</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>360</width>
<height>16777215</height>
</size>
</property>
<property name="baseSize">
<size>
<width>120</width>
<height>50</height>
</size>
</property>
<property name="text">
<string>Disconnected</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="plot_settings">
<property name="toolTip">
<string>Plot Settings</string>
</property>
<property name="text">
<string>📉</string>
</property>
<property name="popupMode">
<enum>QToolButton::InstantPopup</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="limits_warning">
<property name="toolTipDuration">
<number>1000000000</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="background_task_lbl">
<property name="text">
<string>Ready.</string>
</property>
</widget>
</item>
<item>
<widget class="QtWaitingSpinner" name="loading_spinner" native="true"/>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QWidget" name="report_group" native="true">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>40</width>
<height>0</height>
</size>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="report_layout" stretch="0,1,1,1">
<property name="spacing">
<number>6</number>
</property>
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="report_lbl">
<property name="text">
<string>Poll every: </string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="report_refresh_spin">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>70</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>70</width>
<height>16777215</height>
</size>
</property>
<property name="baseSize">
<size>
<width>70</width>
<height>0</height>
</size>
</property>
<property name="suffix">
<string> s</string>
</property>
<property name="decimals">
<number>1</number>
</property>
<property name="minimum">
<double>0.100000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
<property name="stepType">
<enum>QAbstractSpinBox::AdaptiveDecimalStepType</enum>
</property>
<property name="value">
<double>1.000000000000000</double>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="report_box">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>16777215</height>
</size>
</property>
<property name="baseSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Report</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="report_apply_btn">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>16777215</height>
</size>
</property>
<property name="baseSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Apply</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menuBar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1280</width>
<height>23</height>
</rect>
</property>
<property name="styleSheet">
<string notr="true"/>
</property>
<widget class="QMenu" name="menuKirdy">
<property name="enabled">
<bool>true</bool>
</property>
<property name="autoFillBackground">
<bool>false</bool>
</property>
<property name="styleSheet">
<string notr="true"/>
</property>
<property name="title">
<string>Kirdy</string>
</property>
<addaction name="menu_action_about_kirdy"/>
<addaction name="menu_action_update_net_settings"/>
<addaction name="menu_action_save"/>
<addaction name="menu_action_load"/>
<addaction name="menu_action_dfu_mode"/>
<addaction name="menu_action_hard_reset"/>
</widget>
<widget class="QMenu" name="menuAbout">
<property name="styleSheet">
<string notr="true"/>
</property>
<property name="title">
<string>About</string>
</property>
<addaction name="menu_action_about_gui"/>
</widget>
<addaction name="menuKirdy"/>
<addaction name="menuAbout"/>
</widget>
<action name="actionReset">
<property name="text">
<string>Reset</string>
</property>
<property name="toolTip">
<string>Reset the Kirdy</string>
</property>
<property name="menuRole">
<enum>QAction::NoRole</enum>
</property>
</action>
<action name="actionEnter_DFU_Mode">
<property name="text">
<string>Enter DFU Mode</string>
</property>
<property name="toolTip">
<string>Reset kirdy and enter USB device firmware update (DFU) mode</string>
</property>
<property name="menuRole">
<enum>QAction::NoRole</enum>
</property>
</action>
<action name="actionNetwork_Settings">
<property name="text">
<string>Network Settings</string>
</property>
<property name="toolTip">
<string>Configure IPv4 address, netmask length, and optional default gateway</string>
</property>
<property name="menuRole">
<enum>QAction::NoRole</enum>
</property>
</action>
<action name="actionAbout_Kirdy">
<property name="text">
<string>About Kirdy</string>
</property>
<property name="toolTip">
<string>Show Kirdy hardware revision, and settings related to i</string>
</property>
<property name="menuRole">
<enum>QAction::NoRole</enum>
</property>
</action>
<action name="actionLoad_all_configs">
<property name="text">
<string>Load all channel configs from flash</string>
</property>
<property name="toolTip">
<string>Restore configuration for all channels from flash</string>
</property>
<property name="menuRole">
<enum>QAction::NoRole</enum>
</property>
</action>
<action name="actionSave_all_configs">
<property name="text">
<string>Save all channel configs to flash</string>
</property>
<property name="toolTip">
<string>Save configuration for all channels to flash</string>
</property>
<property name="menuRole">
<enum>QAction::NoRole</enum>
</property>
</action>
<action name="actionConnection_Settings">
<property name="text">
<string>Connect</string>
</property>
</action>
<action name="menu_action_update_net_settings">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Update Network Settings</string>
</property>
</action>
<action name="menu_action_dfu_mode">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Enter DFU Mode</string>
</property>
</action>
<action name="menu_action_save">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Save Settings to Flash</string>
</property>
</action>
<action name="menu_action_hard_reset">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Hard Reset</string>
</property>
</action>
<action name="menu_action_connect">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Connect</string>
</property>
</action>
<action name="menu_action_disconnect">
<property name="text">
<string>Disconnect</string>
</property>
</action>
<action name="menu_action_about_gui">
<property name="text">
<string>About GUI</string>
</property>
</action>
<action name="action">
<property name="text">
<string>test</string>
</property>
</action>
<action name="menu_action_about_kirdy">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>About Kirdy</string>
</property>
</action>
<action name="menu_action_load">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Load Settings from Flash</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
<class>ParameterTree</class>
<extends>QWidget</extends>
<header>pyqtgraph.parametertree</header>
<container>1</container>
</customwidget>
<customwidget>
<class>LivePlotWidget</class>
<extends>QWidget</extends>
<header>pglive.sources.live_plot_widget</header>
<container>1</container>
</customwidget>
<customwidget>
<class>QtWaitingSpinner</class>
<extends>QWidget</extends>
<header>waitingspinnerwidget</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@ -1,6 +0,0 @@
QPushButton:disabled { color: gray }
ParameterTree:disabled { color: gray }
QToolButton:disabled { color: gray }
QDoubleSpinBox:disabled { color: gray }
QCheckBox:disabled { color: gray }
QMenu:disabled { color: gray }

View File

@ -1,161 +0,0 @@
# Form implementation generated from reading ui file 'conn_settings_form.ui'
#
# Created by: PyQt6 UI code generator 6.6.0
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt6 import QtCore, QtGui, QtWidgets
class Ui_Conn_Settings_Form(object):
def setupUi(self, Conn_Settings_Form):
Conn_Settings_Form.setObjectName("Conn_Settings_Form")
Conn_Settings_Form.resize(415, 145)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(Conn_Settings_Form.sizePolicy().hasHeightForWidth())
Conn_Settings_Form.setSizePolicy(sizePolicy)
Conn_Settings_Form.setMinimumSize(QtCore.QSize(415, 145))
Conn_Settings_Form.setMaximumSize(QtCore.QSize(415, 145))
self.horizontalLayoutWidget = QtWidgets.QWidget(parent=Conn_Settings_Form)
self.horizontalLayoutWidget.setGeometry(QtCore.QRect(20, 10, 371, 41))
self.horizontalLayoutWidget.setObjectName("horizontalLayoutWidget")
self.ip_addr_layout = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget)
self.ip_addr_layout.setContentsMargins(0, 0, 0, 0)
self.ip_addr_layout.setObjectName("ip_addr_layout")
self.ip_addr_label = QtWidgets.QLabel(parent=self.horizontalLayoutWidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.ip_addr_label.sizePolicy().hasHeightForWidth())
self.ip_addr_label.setSizePolicy(sizePolicy)
self.ip_addr_label.setMaximumSize(QtCore.QSize(120, 16777215))
font = QtGui.QFont()
font.setPointSize(12)
self.ip_addr_label.setFont(font)
self.ip_addr_label.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight)
self.ip_addr_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter)
self.ip_addr_label.setObjectName("ip_addr_label")
self.ip_addr_layout.addWidget(self.ip_addr_label)
self.addr_in_0 = QtWidgets.QLineEdit(parent=self.horizontalLayoutWidget)
self.addr_in_0.setMaximumSize(QtCore.QSize(50, 16777215))
self.addr_in_0.setMaxLength(3)
self.addr_in_0.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.addr_in_0.setObjectName("addr_in_0")
self.ip_addr_layout.addWidget(self.addr_in_0)
self.dot_0_label = QtWidgets.QLabel(parent=self.horizontalLayoutWidget)
self.dot_0_label.setMaximumSize(QtCore.QSize(10, 16777215))
font = QtGui.QFont()
font.setPointSize(12)
font.setBold(False)
font.setKerning(True)
self.dot_0_label.setFont(font)
self.dot_0_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.dot_0_label.setObjectName("dot_0_label")
self.ip_addr_layout.addWidget(self.dot_0_label)
self.addr_in_1 = QtWidgets.QLineEdit(parent=self.horizontalLayoutWidget)
self.addr_in_1.setMaximumSize(QtCore.QSize(50, 16777215))
self.addr_in_1.setMaxLength(3)
self.addr_in_1.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.addr_in_1.setObjectName("addr_in_1")
self.ip_addr_layout.addWidget(self.addr_in_1)
self.dot_1_label = QtWidgets.QLabel(parent=self.horizontalLayoutWidget)
self.dot_1_label.setMaximumSize(QtCore.QSize(10, 16777215))
font = QtGui.QFont()
font.setPointSize(12)
font.setBold(False)
font.setKerning(True)
self.dot_1_label.setFont(font)
self.dot_1_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.dot_1_label.setObjectName("dot_1_label")
self.ip_addr_layout.addWidget(self.dot_1_label)
self.addr_in_2 = QtWidgets.QLineEdit(parent=self.horizontalLayoutWidget)
self.addr_in_2.setMaximumSize(QtCore.QSize(50, 16777215))
self.addr_in_2.setMaxLength(3)
self.addr_in_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.addr_in_2.setObjectName("addr_in_2")
self.ip_addr_layout.addWidget(self.addr_in_2)
self.dot_2_label = QtWidgets.QLabel(parent=self.horizontalLayoutWidget)
self.dot_2_label.setMaximumSize(QtCore.QSize(10, 16777215))
font = QtGui.QFont()
font.setPointSize(12)
font.setBold(False)
font.setKerning(True)
self.dot_2_label.setFont(font)
self.dot_2_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.dot_2_label.setObjectName("dot_2_label")
self.ip_addr_layout.addWidget(self.dot_2_label)
self.addr_in_3 = QtWidgets.QLineEdit(parent=self.horizontalLayoutWidget)
self.addr_in_3.setMaximumSize(QtCore.QSize(50, 16777215))
self.addr_in_3.setMaxLength(3)
self.addr_in_3.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.addr_in_3.setObjectName("addr_in_3")
self.ip_addr_layout.addWidget(self.addr_in_3)
self.horizontalLayoutWidget_2 = QtWidgets.QWidget(parent=Conn_Settings_Form)
self.horizontalLayoutWidget_2.setGeometry(QtCore.QRect(20, 50, 371, 41))
self.horizontalLayoutWidget_2.setObjectName("horizontalLayoutWidget_2")
self.port_layout = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget_2)
self.port_layout.setContentsMargins(0, 0, 0, 0)
self.port_layout.setObjectName("port_layout")
self.port_no_label = QtWidgets.QLabel(parent=self.horizontalLayoutWidget_2)
self.port_no_label.setMaximumSize(QtCore.QSize(97, 16777215))
font = QtGui.QFont()
font.setPointSize(12)
self.port_no_label.setFont(font)
self.port_no_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter)
self.port_no_label.setObjectName("port_no_label")
self.port_layout.addWidget(self.port_no_label)
self.port_in = QtWidgets.QLineEdit(parent=self.horizontalLayoutWidget_2)
self.port_in.setMaximumSize(QtCore.QSize(50, 16777215))
self.port_in.setMaxLength(5)
self.port_in.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.port_in.setObjectName("port_in")
self.port_layout.addWidget(self.port_in)
spacerItem = QtWidgets.QSpacerItem(50, 20, QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Minimum)
self.port_layout.addItem(spacerItem)
self.port_layout.setStretch(0, 3)
self.port_layout.setStretch(2, 6)
self.horizontalLayoutWidget_4 = QtWidgets.QWidget(parent=Conn_Settings_Form)
self.horizontalLayoutWidget_4.setGeometry(QtCore.QRect(20, 90, 371, 47))
self.horizontalLayoutWidget_4.setObjectName("horizontalLayoutWidget_4")
self.buttons_layout = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget_4)
self.buttons_layout.setContentsMargins(0, 0, 0, 0)
self.buttons_layout.setObjectName("buttons_layout")
spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.buttons_layout.addItem(spacerItem1)
self.connect_btn = QtWidgets.QPushButton(parent=self.horizontalLayoutWidget_4)
self.connect_btn.setObjectName("connect_btn")
self.buttons_layout.addWidget(self.connect_btn)
self.cancel_btn = QtWidgets.QPushButton(parent=self.horizontalLayoutWidget_4)
self.cancel_btn.setObjectName("cancel_btn")
self.buttons_layout.addWidget(self.cancel_btn)
self.retranslateUi(Conn_Settings_Form)
self.cancel_btn.clicked.connect(Conn_Settings_Form.reject) # type: ignore
self.connect_btn.clicked.connect(Conn_Settings_Form.accept) # type: ignore
QtCore.QMetaObject.connectSlotsByName(Conn_Settings_Form)
Conn_Settings_Form.setTabOrder(self.addr_in_0, self.addr_in_1)
Conn_Settings_Form.setTabOrder(self.addr_in_1, self.addr_in_2)
Conn_Settings_Form.setTabOrder(self.addr_in_2, self.addr_in_3)
Conn_Settings_Form.setTabOrder(self.addr_in_3, self.port_in)
Conn_Settings_Form.setTabOrder(self.port_in, self.connect_btn)
Conn_Settings_Form.setTabOrder(self.connect_btn, self.cancel_btn)
def retranslateUi(self, Conn_Settings_Form):
_translate = QtCore.QCoreApplication.translate
Conn_Settings_Form.setWindowTitle(_translate("Conn_Settings_Form", "Connection Settings"))
self.ip_addr_label.setText(_translate("Conn_Settings_Form", "IP Address:"))
self.addr_in_0.setText(_translate("Conn_Settings_Form", "192"))
self.dot_0_label.setText(_translate("Conn_Settings_Form", "."))
self.addr_in_1.setText(_translate("Conn_Settings_Form", "168"))
self.dot_1_label.setText(_translate("Conn_Settings_Form", "."))
self.addr_in_2.setText(_translate("Conn_Settings_Form", "1"))
self.dot_2_label.setText(_translate("Conn_Settings_Form", "."))
self.addr_in_3.setText(_translate("Conn_Settings_Form", "128"))
self.port_no_label.setText(_translate("Conn_Settings_Form", "Port:"))
self.port_in.setText(_translate("Conn_Settings_Form", "1337"))
self.connect_btn.setText(_translate("Conn_Settings_Form", "Connect"))
self.cancel_btn.setText(_translate("Conn_Settings_Form", "Cancel"))

View File

@ -1,391 +0,0 @@
# Form implementation generated from reading ui file 'kirdy_qt.ui'
#
# Created by: PyQt6 UI code generator 6.6.0
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt6 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(1280, 720)
MainWindow.setMinimumSize(QtCore.QSize(1280, 720))
MainWindow.setMaximumSize(QtCore.QSize(3840, 2160))
icon = QtGui.QIcon.fromTheme("application-x-executable")
MainWindow.setWindowIcon(icon)
self.main_widget = QtWidgets.QWidget(parent=MainWindow)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(1)
sizePolicy.setVerticalStretch(1)
sizePolicy.setHeightForWidth(self.main_widget.sizePolicy().hasHeightForWidth())
self.main_widget.setSizePolicy(sizePolicy)
self.main_widget.setObjectName("main_widget")
self.gridLayout_2 = QtWidgets.QGridLayout(self.main_widget)
self.gridLayout_2.setContentsMargins(3, 3, 3, 3)
self.gridLayout_2.setSpacing(3)
self.gridLayout_2.setObjectName("gridLayout_2")
self.main_layout = QtWidgets.QVBoxLayout()
self.main_layout.setSpacing(0)
self.main_layout.setObjectName("main_layout")
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetDefaultConstraint)
self.horizontalLayout.setSpacing(0)
self.horizontalLayout.setObjectName("horizontalLayout")
self.ctrl_vertical_layout = QtWidgets.QVBoxLayout()
self.ctrl_vertical_layout.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetMaximumSize)
self.ctrl_vertical_layout.setSpacing(0)
self.ctrl_vertical_layout.setObjectName("ctrl_vertical_layout")
self.ld_section_label = QtWidgets.QLabel(parent=self.main_widget)
font = QtGui.QFont()
font.setPointSize(14)
font.setBold(True)
self.ld_section_label.setFont(font)
self.ld_section_label.setWordWrap(False)
self.ld_section_label.setObjectName("ld_section_label")
self.ctrl_vertical_layout.addWidget(self.ld_section_label)
self.ld_status = ParameterTree(parent=self.main_widget)
self.ld_status.setEnabled(False)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.ld_status.sizePolicy().hasHeightForWidth())
self.ld_status.setSizePolicy(sizePolicy)
self.ld_status.setMaximumSize(QtCore.QSize(16777215, 55))
self.ld_status.setObjectName("ld_status")
self.ctrl_vertical_layout.addWidget(self.ld_status)
self.ld_tree = ParameterTree(parent=self.main_widget)
self.ld_tree.setEnabled(False)
self.ld_tree.setObjectName("ld_tree")
self.ctrl_vertical_layout.addWidget(self.ld_tree)
self.ld_btns_layout = QtWidgets.QHBoxLayout()
self.ld_btns_layout.setContentsMargins(10, -1, 10, -1)
self.ld_btns_layout.setSpacing(30)
self.ld_btns_layout.setObjectName("ld_btns_layout")
self.ld_pwr_on_btn = QtWidgets.QPushButton(parent=self.main_widget)
self.ld_pwr_on_btn.setEnabled(False)
self.ld_pwr_on_btn.setObjectName("ld_pwr_on_btn")
self.ld_btns_layout.addWidget(self.ld_pwr_on_btn)
self.ld_pwr_off_btn = QtWidgets.QPushButton(parent=self.main_widget)
self.ld_pwr_off_btn.setEnabled(False)
self.ld_pwr_off_btn.setObjectName("ld_pwr_off_btn")
self.ld_btns_layout.addWidget(self.ld_pwr_off_btn)
self.ld_clear_alarm_btn = QtWidgets.QPushButton(parent=self.main_widget)
self.ld_clear_alarm_btn.setEnabled(False)
self.ld_clear_alarm_btn.setObjectName("ld_clear_alarm_btn")
self.ld_btns_layout.addWidget(self.ld_clear_alarm_btn)
self.ctrl_vertical_layout.addLayout(self.ld_btns_layout)
self.tec_section_label = QtWidgets.QLabel(parent=self.main_widget)
font = QtGui.QFont()
font.setPointSize(14)
font.setBold(True)
self.tec_section_label.setFont(font)
self.tec_section_label.setWordWrap(False)
self.tec_section_label.setObjectName("tec_section_label")
self.ctrl_vertical_layout.addWidget(self.tec_section_label)
self.tec_status = ParameterTree(parent=self.main_widget)
self.tec_status.setEnabled(False)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.tec_status.sizePolicy().hasHeightForWidth())
self.tec_status.setSizePolicy(sizePolicy)
self.tec_status.setMaximumSize(QtCore.QSize(16777215, 55))
self.tec_status.setObjectName("tec_status")
self.ctrl_vertical_layout.addWidget(self.tec_status)
self.tec_tree = ParameterTree(parent=self.main_widget)
self.tec_tree.setEnabled(False)
self.tec_tree.setObjectName("tec_tree")
self.ctrl_vertical_layout.addWidget(self.tec_tree)
self.tec_btns_layout = QtWidgets.QHBoxLayout()
self.tec_btns_layout.setContentsMargins(10, -1, 10, -1)
self.tec_btns_layout.setSpacing(30)
self.tec_btns_layout.setObjectName("tec_btns_layout")
self.tec_pwr_on_btn = QtWidgets.QPushButton(parent=self.main_widget)
self.tec_pwr_on_btn.setEnabled(False)
self.tec_pwr_on_btn.setObjectName("tec_pwr_on_btn")
self.tec_btns_layout.addWidget(self.tec_pwr_on_btn)
self.tec_pwr_off_btn = QtWidgets.QPushButton(parent=self.main_widget)
self.tec_pwr_off_btn.setEnabled(False)
self.tec_pwr_off_btn.setObjectName("tec_pwr_off_btn")
self.tec_btns_layout.addWidget(self.tec_pwr_off_btn)
self.tec_clear_alarm_btn = QtWidgets.QPushButton(parent=self.main_widget)
self.tec_clear_alarm_btn.setEnabled(False)
self.tec_clear_alarm_btn.setObjectName("tec_clear_alarm_btn")
self.tec_btns_layout.addWidget(self.tec_clear_alarm_btn)
self.ctrl_vertical_layout.addLayout(self.tec_btns_layout)
self.ctrl_vertical_layout.setStretch(0, 1)
self.ctrl_vertical_layout.setStretch(1, 1)
self.ctrl_vertical_layout.setStretch(2, 10)
self.ctrl_vertical_layout.setStretch(3, 1)
self.ctrl_vertical_layout.setStretch(4, 1)
self.ctrl_vertical_layout.setStretch(5, 1)
self.ctrl_vertical_layout.setStretch(6, 10)
self.ctrl_vertical_layout.setStretch(7, 1)
self.horizontalLayout.addLayout(self.ctrl_vertical_layout)
self.graphgroup = QtWidgets.QGridLayout()
self.graphgroup.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetNoConstraint)
self.graphgroup.setSpacing(0)
self.graphgroup.setObjectName("graphgroup")
self.tec_temp_graph = LivePlotWidget(parent=self.main_widget)
self.tec_temp_graph.setEnabled(False)
self.tec_temp_graph.setObjectName("tec_temp_graph")
self.graphgroup.addWidget(self.tec_temp_graph, 1, 0, 1, 1)
self.tec_i_graph = LivePlotWidget(parent=self.main_widget)
self.tec_i_graph.setEnabled(False)
self.tec_i_graph.setObjectName("tec_i_graph")
self.graphgroup.addWidget(self.tec_i_graph, 1, 2, 1, 1)
self.pd_mon_pwr_graph = LivePlotWidget(parent=self.main_widget)
self.pd_mon_pwr_graph.setEnabled(False)
self.pd_mon_pwr_graph.setObjectName("pd_mon_pwr_graph")
self.graphgroup.addWidget(self.pd_mon_pwr_graph, 0, 0, 1, 1)
self.ld_i_set_graph = LivePlotWidget(parent=self.main_widget)
self.ld_i_set_graph.setEnabled(False)
self.ld_i_set_graph.setObjectName("ld_i_set_graph")
self.graphgroup.addWidget(self.ld_i_set_graph, 0, 2, 1, 1)
self.horizontalLayout.addLayout(self.graphgroup)
self.horizontalLayout.setStretch(0, 1)
self.horizontalLayout.setStretch(1, 2)
self.main_layout.addLayout(self.horizontalLayout)
self.bottom_settings_group = QtWidgets.QFrame(parent=self.main_widget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.bottom_settings_group.sizePolicy().hasHeightForWidth())
self.bottom_settings_group.setSizePolicy(sizePolicy)
self.bottom_settings_group.setMinimumSize(QtCore.QSize(0, 40))
self.bottom_settings_group.setMaximumSize(QtCore.QSize(16777215, 40))
self.bottom_settings_group.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.bottom_settings_group.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.bottom_settings_group.setObjectName("bottom_settings_group")
self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.bottom_settings_group)
self.horizontalLayout_2.setContentsMargins(3, 3, 3, 3)
self.horizontalLayout_2.setSpacing(3)
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
self.settings_layout = QtWidgets.QHBoxLayout()
self.settings_layout.setObjectName("settings_layout")
self.connect_btn = QtWidgets.QPushButton(parent=self.bottom_settings_group)
self.connect_btn.setObjectName("connect_btn")
self.settings_layout.addWidget(self.connect_btn)
self.status_lbl = QtWidgets.QLabel(parent=self.bottom_settings_group)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.status_lbl.sizePolicy().hasHeightForWidth())
self.status_lbl.setSizePolicy(sizePolicy)
self.status_lbl.setMinimumSize(QtCore.QSize(360, 0))
self.status_lbl.setMaximumSize(QtCore.QSize(360, 16777215))
self.status_lbl.setBaseSize(QtCore.QSize(120, 50))
self.status_lbl.setObjectName("status_lbl")
self.settings_layout.addWidget(self.status_lbl)
self.plot_settings = QtWidgets.QToolButton(parent=self.bottom_settings_group)
self.plot_settings.setPopupMode(QtWidgets.QToolButton.ToolButtonPopupMode.InstantPopup)
self.plot_settings.setObjectName("plot_settings")
self.settings_layout.addWidget(self.plot_settings)
self.limits_warning = QtWidgets.QLabel(parent=self.bottom_settings_group)
self.limits_warning.setToolTipDuration(1000000000)
self.limits_warning.setObjectName("limits_warning")
self.settings_layout.addWidget(self.limits_warning)
self.background_task_lbl = QtWidgets.QLabel(parent=self.bottom_settings_group)
self.background_task_lbl.setObjectName("background_task_lbl")
self.settings_layout.addWidget(self.background_task_lbl)
self.loading_spinner = QtWaitingSpinner(parent=self.bottom_settings_group)
self.loading_spinner.setObjectName("loading_spinner")
self.settings_layout.addWidget(self.loading_spinner)
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.settings_layout.addItem(spacerItem)
self.report_group = QtWidgets.QWidget(parent=self.bottom_settings_group)
self.report_group.setEnabled(False)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.report_group.sizePolicy().hasHeightForWidth())
self.report_group.setSizePolicy(sizePolicy)
self.report_group.setMinimumSize(QtCore.QSize(40, 0))
self.report_group.setObjectName("report_group")
self.horizontalLayout_4 = QtWidgets.QHBoxLayout(self.report_group)
self.horizontalLayout_4.setContentsMargins(0, 0, 0, 0)
self.horizontalLayout_4.setSpacing(0)
self.horizontalLayout_4.setObjectName("horizontalLayout_4")
self.report_layout = QtWidgets.QHBoxLayout()
self.report_layout.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetDefaultConstraint)
self.report_layout.setContentsMargins(0, -1, -1, -1)
self.report_layout.setSpacing(6)
self.report_layout.setObjectName("report_layout")
self.report_lbl = QtWidgets.QLabel(parent=self.report_group)
self.report_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter)
self.report_lbl.setObjectName("report_lbl")
self.report_layout.addWidget(self.report_lbl)
self.report_refresh_spin = QtWidgets.QDoubleSpinBox(parent=self.report_group)
self.report_refresh_spin.setEnabled(False)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.report_refresh_spin.sizePolicy().hasHeightForWidth())
self.report_refresh_spin.setSizePolicy(sizePolicy)
self.report_refresh_spin.setMinimumSize(QtCore.QSize(70, 0))
self.report_refresh_spin.setMaximumSize(QtCore.QSize(70, 16777215))
self.report_refresh_spin.setBaseSize(QtCore.QSize(70, 0))
self.report_refresh_spin.setDecimals(1)
self.report_refresh_spin.setMinimum(0.1)
self.report_refresh_spin.setSingleStep(0.1)
self.report_refresh_spin.setStepType(QtWidgets.QAbstractSpinBox.StepType.AdaptiveDecimalStepType)
self.report_refresh_spin.setProperty("value", 1.0)
self.report_refresh_spin.setObjectName("report_refresh_spin")
self.report_layout.addWidget(self.report_refresh_spin)
self.report_box = QtWidgets.QCheckBox(parent=self.report_group)
self.report_box.setEnabled(False)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.report_box.sizePolicy().hasHeightForWidth())
self.report_box.setSizePolicy(sizePolicy)
self.report_box.setMaximumSize(QtCore.QSize(80, 16777215))
self.report_box.setBaseSize(QtCore.QSize(80, 0))
self.report_box.setObjectName("report_box")
self.report_layout.addWidget(self.report_box)
self.report_apply_btn = QtWidgets.QPushButton(parent=self.report_group)
self.report_apply_btn.setEnabled(False)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.report_apply_btn.sizePolicy().hasHeightForWidth())
self.report_apply_btn.setSizePolicy(sizePolicy)
self.report_apply_btn.setMinimumSize(QtCore.QSize(80, 0))
self.report_apply_btn.setMaximumSize(QtCore.QSize(80, 16777215))
self.report_apply_btn.setBaseSize(QtCore.QSize(80, 0))
self.report_apply_btn.setObjectName("report_apply_btn")
self.report_layout.addWidget(self.report_apply_btn)
self.report_layout.setStretch(1, 1)
self.report_layout.setStretch(2, 1)
self.report_layout.setStretch(3, 1)
self.horizontalLayout_4.addLayout(self.report_layout)
self.settings_layout.addWidget(self.report_group)
self.horizontalLayout_2.addLayout(self.settings_layout)
self.main_layout.addWidget(self.bottom_settings_group)
self.gridLayout_2.addLayout(self.main_layout, 0, 0, 1, 1)
MainWindow.setCentralWidget(self.main_widget)
self.menuBar = QtWidgets.QMenuBar(parent=MainWindow)
self.menuBar.setGeometry(QtCore.QRect(0, 0, 1280, 23))
self.menuBar.setStyleSheet("")
self.menuBar.setObjectName("menuBar")
self.menuKirdy = QtWidgets.QMenu(parent=self.menuBar)
self.menuKirdy.setEnabled(True)
self.menuKirdy.setAutoFillBackground(False)
self.menuKirdy.setStyleSheet("")
self.menuKirdy.setObjectName("menuKirdy")
self.menuAbout = QtWidgets.QMenu(parent=self.menuBar)
self.menuAbout.setStyleSheet("")
self.menuAbout.setObjectName("menuAbout")
MainWindow.setMenuBar(self.menuBar)
self.actionReset = QtGui.QAction(parent=MainWindow)
self.actionReset.setMenuRole(QtGui.QAction.MenuRole.NoRole)
self.actionReset.setObjectName("actionReset")
self.actionEnter_DFU_Mode = QtGui.QAction(parent=MainWindow)
self.actionEnter_DFU_Mode.setMenuRole(QtGui.QAction.MenuRole.NoRole)
self.actionEnter_DFU_Mode.setObjectName("actionEnter_DFU_Mode")
self.actionNetwork_Settings = QtGui.QAction(parent=MainWindow)
self.actionNetwork_Settings.setMenuRole(QtGui.QAction.MenuRole.NoRole)
self.actionNetwork_Settings.setObjectName("actionNetwork_Settings")
self.actionAbout_Kirdy = QtGui.QAction(parent=MainWindow)
self.actionAbout_Kirdy.setMenuRole(QtGui.QAction.MenuRole.NoRole)
self.actionAbout_Kirdy.setObjectName("actionAbout_Kirdy")
self.actionLoad_all_configs = QtGui.QAction(parent=MainWindow)
self.actionLoad_all_configs.setMenuRole(QtGui.QAction.MenuRole.NoRole)
self.actionLoad_all_configs.setObjectName("actionLoad_all_configs")
self.actionSave_all_configs = QtGui.QAction(parent=MainWindow)
self.actionSave_all_configs.setMenuRole(QtGui.QAction.MenuRole.NoRole)
self.actionSave_all_configs.setObjectName("actionSave_all_configs")
self.actionConnection_Settings = QtGui.QAction(parent=MainWindow)
self.actionConnection_Settings.setObjectName("actionConnection_Settings")
self.menu_action_update_net_settings = QtGui.QAction(parent=MainWindow)
self.menu_action_update_net_settings.setEnabled(False)
self.menu_action_update_net_settings.setObjectName("menu_action_update_net_settings")
self.menu_action_dfu_mode = QtGui.QAction(parent=MainWindow)
self.menu_action_dfu_mode.setEnabled(False)
self.menu_action_dfu_mode.setObjectName("menu_action_dfu_mode")
self.menu_action_save = QtGui.QAction(parent=MainWindow)
self.menu_action_save.setEnabled(False)
self.menu_action_save.setObjectName("menu_action_save")
self.menu_action_hard_reset = QtGui.QAction(parent=MainWindow)
self.menu_action_hard_reset.setEnabled(False)
self.menu_action_hard_reset.setObjectName("menu_action_hard_reset")
self.menu_action_connect = QtGui.QAction(parent=MainWindow)
self.menu_action_connect.setEnabled(False)
self.menu_action_connect.setObjectName("menu_action_connect")
self.menu_action_disconnect = QtGui.QAction(parent=MainWindow)
self.menu_action_disconnect.setObjectName("menu_action_disconnect")
self.menu_action_about_gui = QtGui.QAction(parent=MainWindow)
self.menu_action_about_gui.setObjectName("menu_action_about_gui")
self.action = QtGui.QAction(parent=MainWindow)
self.action.setObjectName("action")
self.menu_action_about_kirdy = QtGui.QAction(parent=MainWindow)
self.menu_action_about_kirdy.setEnabled(False)
self.menu_action_about_kirdy.setObjectName("menu_action_about_kirdy")
self.menu_action_load = QtGui.QAction(parent=MainWindow)
self.menu_action_load.setEnabled(False)
self.menu_action_load.setObjectName("menu_action_load")
self.menuKirdy.addAction(self.menu_action_about_kirdy)
self.menuKirdy.addAction(self.menu_action_update_net_settings)
self.menuKirdy.addAction(self.menu_action_save)
self.menuKirdy.addAction(self.menu_action_load)
self.menuKirdy.addAction(self.menu_action_dfu_mode)
self.menuKirdy.addAction(self.menu_action_hard_reset)
self.menuAbout.addAction(self.menu_action_about_gui)
self.menuBar.addAction(self.menuKirdy.menuAction())
self.menuBar.addAction(self.menuAbout.menuAction())
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "Kirdy Control Panel"))
self.ld_section_label.setText(_translate("MainWindow", " Laser Diode"))
self.ld_pwr_on_btn.setText(_translate("MainWindow", "POWER ON"))
self.ld_pwr_off_btn.setText(_translate("MainWindow", "POWER OFF"))
self.ld_clear_alarm_btn.setText(_translate("MainWindow", "CLEAR ALARM"))
self.tec_section_label.setText(_translate("MainWindow", " Thermostat"))
self.tec_pwr_on_btn.setText(_translate("MainWindow", "POWER ON"))
self.tec_pwr_off_btn.setText(_translate("MainWindow", "POWER OFF"))
self.tec_clear_alarm_btn.setText(_translate("MainWindow", "CLEAR ALAM"))
self.connect_btn.setText(_translate("MainWindow", "Connect"))
self.status_lbl.setText(_translate("MainWindow", "Disconnected"))
self.plot_settings.setToolTip(_translate("MainWindow", "Plot Settings"))
self.plot_settings.setText(_translate("MainWindow", "📉"))
self.background_task_lbl.setText(_translate("MainWindow", "Ready."))
self.report_lbl.setText(_translate("MainWindow", "Poll every: "))
self.report_refresh_spin.setSuffix(_translate("MainWindow", " s"))
self.report_box.setText(_translate("MainWindow", "Report"))
self.report_apply_btn.setText(_translate("MainWindow", "Apply"))
self.menuKirdy.setTitle(_translate("MainWindow", "Kirdy"))
self.menuAbout.setTitle(_translate("MainWindow", "About"))
self.actionReset.setText(_translate("MainWindow", "Reset"))
self.actionReset.setToolTip(_translate("MainWindow", "Reset the Kirdy"))
self.actionEnter_DFU_Mode.setText(_translate("MainWindow", "Enter DFU Mode"))
self.actionEnter_DFU_Mode.setToolTip(_translate("MainWindow", "Reset kirdy and enter USB device firmware update (DFU) mode"))
self.actionNetwork_Settings.setText(_translate("MainWindow", "Network Settings"))
self.actionNetwork_Settings.setToolTip(_translate("MainWindow", "Configure IPv4 address, netmask length, and optional default gateway"))
self.actionAbout_Kirdy.setText(_translate("MainWindow", "About Kirdy"))
self.actionAbout_Kirdy.setToolTip(_translate("MainWindow", "Show Kirdy hardware revision, and settings related to i"))
self.actionLoad_all_configs.setText(_translate("MainWindow", "Load all channel configs from flash"))
self.actionLoad_all_configs.setToolTip(_translate("MainWindow", "Restore configuration for all channels from flash"))
self.actionSave_all_configs.setText(_translate("MainWindow", "Save all channel configs to flash"))
self.actionSave_all_configs.setToolTip(_translate("MainWindow", "Save configuration for all channels to flash"))
self.actionConnection_Settings.setText(_translate("MainWindow", "Connect"))
self.menu_action_update_net_settings.setText(_translate("MainWindow", "Update Network Settings"))
self.menu_action_dfu_mode.setText(_translate("MainWindow", "Enter DFU Mode"))
self.menu_action_save.setText(_translate("MainWindow", "Save Settings to Flash"))
self.menu_action_hard_reset.setText(_translate("MainWindow", "Hard Reset"))
self.menu_action_connect.setText(_translate("MainWindow", "Connect"))
self.menu_action_disconnect.setText(_translate("MainWindow", "Disconnect"))
self.menu_action_about_gui.setText(_translate("MainWindow", "About GUI"))
self.action.setText(_translate("MainWindow", "test"))
self.menu_action_about_kirdy.setText(_translate("MainWindow", "About Kirdy"))
self.menu_action_load.setText(_translate("MainWindow", "Load Settings from Flash"))
from pglive.sources.live_plot_widget import LivePlotWidget
from pyqtgraph.parametertree import ParameterTree
from waitingspinnerwidget import QtWaitingSpinner

View File

@ -1,264 +0,0 @@
# Form implementation generated from reading ui file 'update_network_settings_form.ui'
#
# Created by: PyQt6 UI code generator 6.6.0
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt6 import QtCore, QtGui, QtWidgets
class Ui_Update_Network_Settings_Form(object):
def setupUi(self, Update_Network_Settings_Form):
Update_Network_Settings_Form.setObjectName("Update_Network_Settings_Form")
Update_Network_Settings_Form.resize(415, 180)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(Update_Network_Settings_Form.sizePolicy().hasHeightForWidth())
Update_Network_Settings_Form.setSizePolicy(sizePolicy)
Update_Network_Settings_Form.setMinimumSize(QtCore.QSize(415, 180))
Update_Network_Settings_Form.setMaximumSize(QtCore.QSize(415, 180))
self.horizontalLayoutWidget = QtWidgets.QWidget(parent=Update_Network_Settings_Form)
self.horizontalLayoutWidget.setGeometry(QtCore.QRect(20, 10, 371, 41))
self.horizontalLayoutWidget.setObjectName("horizontalLayoutWidget")
self.ip_addr_layout = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget)
self.ip_addr_layout.setContentsMargins(0, 0, 0, 0)
self.ip_addr_layout.setObjectName("ip_addr_layout")
self.ip_addr_label = QtWidgets.QLabel(parent=self.horizontalLayoutWidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.ip_addr_label.sizePolicy().hasHeightForWidth())
self.ip_addr_label.setSizePolicy(sizePolicy)
self.ip_addr_label.setMaximumSize(QtCore.QSize(120, 16777215))
font = QtGui.QFont()
font.setPointSize(12)
self.ip_addr_label.setFont(font)
self.ip_addr_label.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight)
self.ip_addr_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter)
self.ip_addr_label.setObjectName("ip_addr_label")
self.ip_addr_layout.addWidget(self.ip_addr_label)
self.addr_in_0 = QtWidgets.QLineEdit(parent=self.horizontalLayoutWidget)
self.addr_in_0.setMaximumSize(QtCore.QSize(50, 16777215))
self.addr_in_0.setMaxLength(3)
self.addr_in_0.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.addr_in_0.setObjectName("addr_in_0")
self.ip_addr_layout.addWidget(self.addr_in_0)
self.dot_0_lbl = QtWidgets.QLabel(parent=self.horizontalLayoutWidget)
self.dot_0_lbl.setMaximumSize(QtCore.QSize(10, 16777215))
font = QtGui.QFont()
font.setPointSize(12)
font.setBold(False)
font.setKerning(True)
self.dot_0_lbl.setFont(font)
self.dot_0_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.dot_0_lbl.setObjectName("dot_0_lbl")
self.ip_addr_layout.addWidget(self.dot_0_lbl)
self.addr_in_1 = QtWidgets.QLineEdit(parent=self.horizontalLayoutWidget)
self.addr_in_1.setMaximumSize(QtCore.QSize(50, 16777215))
self.addr_in_1.setMaxLength(3)
self.addr_in_1.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.addr_in_1.setObjectName("addr_in_1")
self.ip_addr_layout.addWidget(self.addr_in_1)
self.dot_1_lbl = QtWidgets.QLabel(parent=self.horizontalLayoutWidget)
self.dot_1_lbl.setMaximumSize(QtCore.QSize(10, 16777215))
font = QtGui.QFont()
font.setPointSize(12)
font.setBold(False)
font.setKerning(True)
self.dot_1_lbl.setFont(font)
self.dot_1_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.dot_1_lbl.setObjectName("dot_1_lbl")
self.ip_addr_layout.addWidget(self.dot_1_lbl)
self.addr_in_2 = QtWidgets.QLineEdit(parent=self.horizontalLayoutWidget)
self.addr_in_2.setMaximumSize(QtCore.QSize(50, 16777215))
self.addr_in_2.setMaxLength(3)
self.addr_in_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.addr_in_2.setObjectName("addr_in_2")
self.ip_addr_layout.addWidget(self.addr_in_2)
self.dot_2_lbl = QtWidgets.QLabel(parent=self.horizontalLayoutWidget)
self.dot_2_lbl.setMaximumSize(QtCore.QSize(10, 16777215))
font = QtGui.QFont()
font.setPointSize(12)
font.setBold(False)
font.setKerning(True)
self.dot_2_lbl.setFont(font)
self.dot_2_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.dot_2_lbl.setObjectName("dot_2_lbl")
self.ip_addr_layout.addWidget(self.dot_2_lbl)
self.addr_in_3 = QtWidgets.QLineEdit(parent=self.horizontalLayoutWidget)
self.addr_in_3.setMaximumSize(QtCore.QSize(50, 16777215))
self.addr_in_3.setMaxLength(3)
self.addr_in_3.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.addr_in_3.setObjectName("addr_in_3")
self.ip_addr_layout.addWidget(self.addr_in_3)
self.horizontalLayoutWidget_4 = QtWidgets.QWidget(parent=Update_Network_Settings_Form)
self.horizontalLayoutWidget_4.setGeometry(QtCore.QRect(20, 130, 371, 47))
self.horizontalLayoutWidget_4.setObjectName("horizontalLayoutWidget_4")
self.buttons_layout = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget_4)
self.buttons_layout.setContentsMargins(0, 0, 0, 0)
self.buttons_layout.setObjectName("buttons_layout")
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.buttons_layout.addItem(spacerItem)
self.update_btn = QtWidgets.QPushButton(parent=self.horizontalLayoutWidget_4)
self.update_btn.setObjectName("update_btn")
self.buttons_layout.addWidget(self.update_btn)
self.cancel_btn = QtWidgets.QPushButton(parent=self.horizontalLayoutWidget_4)
self.cancel_btn.setObjectName("cancel_btn")
self.buttons_layout.addWidget(self.cancel_btn)
self.horizontalLayoutWidget_3 = QtWidgets.QWidget(parent=Update_Network_Settings_Form)
self.horizontalLayoutWidget_3.setGeometry(QtCore.QRect(20, 90, 371, 41))
self.horizontalLayoutWidget_3.setObjectName("horizontalLayoutWidget_3")
self.port_layout_2 = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget_3)
self.port_layout_2.setContentsMargins(0, 0, 0, 0)
self.port_layout_2.setSpacing(6)
self.port_layout_2.setObjectName("port_layout_2")
self.prefix_len_lbl = QtWidgets.QLabel(parent=self.horizontalLayoutWidget_3)
self.prefix_len_lbl.setMaximumSize(QtCore.QSize(98, 16777215))
font = QtGui.QFont()
font.setPointSize(12)
self.prefix_len_lbl.setFont(font)
self.prefix_len_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter)
self.prefix_len_lbl.setObjectName("prefix_len_lbl")
self.port_layout_2.addWidget(self.prefix_len_lbl)
self.prefix_len_in = QtWidgets.QLineEdit(parent=self.horizontalLayoutWidget_3)
self.prefix_len_in.setMaximumSize(QtCore.QSize(50, 16777215))
self.prefix_len_in.setMaxLength(2)
self.prefix_len_in.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.prefix_len_in.setObjectName("prefix_len_in")
self.port_layout_2.addWidget(self.prefix_len_in)
self.port_no_lbl = QtWidgets.QLabel(parent=self.horizontalLayoutWidget_3)
self.port_no_lbl.setMinimumSize(QtCore.QSize(82, 0))
self.port_no_lbl.setMaximumSize(QtCore.QSize(50, 16777215))
font = QtGui.QFont()
font.setPointSize(12)
self.port_no_lbl.setFont(font)
self.port_no_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter)
self.port_no_lbl.setObjectName("port_no_lbl")
self.port_layout_2.addWidget(self.port_no_lbl)
self.port_in = QtWidgets.QLineEdit(parent=self.horizontalLayoutWidget_3)
self.port_in.setMaximumSize(QtCore.QSize(50, 16777215))
self.port_in.setMaxLength(5)
self.port_in.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.port_in.setObjectName("port_in")
self.port_layout_2.addWidget(self.port_in)
spacerItem1 = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.port_layout_2.addItem(spacerItem1)
self.horizontalLayoutWidget_5 = QtWidgets.QWidget(parent=Update_Network_Settings_Form)
self.horizontalLayoutWidget_5.setGeometry(QtCore.QRect(20, 50, 371, 41))
self.horizontalLayoutWidget_5.setObjectName("horizontalLayoutWidget_5")
self.gateway_layout = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget_5)
self.gateway_layout.setContentsMargins(0, 0, 0, 0)
self.gateway_layout.setObjectName("gateway_layout")
self.gateway_lbl = QtWidgets.QLabel(parent=self.horizontalLayoutWidget_5)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.gateway_lbl.sizePolicy().hasHeightForWidth())
self.gateway_lbl.setSizePolicy(sizePolicy)
self.gateway_lbl.setMaximumSize(QtCore.QSize(120, 16777215))
font = QtGui.QFont()
font.setPointSize(12)
self.gateway_lbl.setFont(font)
self.gateway_lbl.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight)
self.gateway_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter)
self.gateway_lbl.setObjectName("gateway_lbl")
self.gateway_layout.addWidget(self.gateway_lbl)
self.gateway_in_0 = QtWidgets.QLineEdit(parent=self.horizontalLayoutWidget_5)
self.gateway_in_0.setMaximumSize(QtCore.QSize(50, 16777215))
self.gateway_in_0.setMaxLength(3)
self.gateway_in_0.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.gateway_in_0.setObjectName("gateway_in_0")
self.gateway_layout.addWidget(self.gateway_in_0)
self.dot_0_lbl_2 = QtWidgets.QLabel(parent=self.horizontalLayoutWidget_5)
self.dot_0_lbl_2.setMaximumSize(QtCore.QSize(10, 16777215))
font = QtGui.QFont()
font.setPointSize(12)
font.setBold(False)
font.setKerning(True)
self.dot_0_lbl_2.setFont(font)
self.dot_0_lbl_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.dot_0_lbl_2.setObjectName("dot_0_lbl_2")
self.gateway_layout.addWidget(self.dot_0_lbl_2)
self.gateway_in_1 = QtWidgets.QLineEdit(parent=self.horizontalLayoutWidget_5)
self.gateway_in_1.setMaximumSize(QtCore.QSize(50, 16777215))
self.gateway_in_1.setMaxLength(3)
self.gateway_in_1.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.gateway_in_1.setObjectName("gateway_in_1")
self.gateway_layout.addWidget(self.gateway_in_1)
self.dot_1_lbl_2 = QtWidgets.QLabel(parent=self.horizontalLayoutWidget_5)
self.dot_1_lbl_2.setMaximumSize(QtCore.QSize(10, 16777215))
font = QtGui.QFont()
font.setPointSize(12)
font.setBold(False)
font.setKerning(True)
self.dot_1_lbl_2.setFont(font)
self.dot_1_lbl_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.dot_1_lbl_2.setObjectName("dot_1_lbl_2")
self.gateway_layout.addWidget(self.dot_1_lbl_2)
self.gateway_in_2 = QtWidgets.QLineEdit(parent=self.horizontalLayoutWidget_5)
self.gateway_in_2.setMaximumSize(QtCore.QSize(50, 16777215))
self.gateway_in_2.setMaxLength(3)
self.gateway_in_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.gateway_in_2.setObjectName("gateway_in_2")
self.gateway_layout.addWidget(self.gateway_in_2)
self.dot_2_lbl_2 = QtWidgets.QLabel(parent=self.horizontalLayoutWidget_5)
self.dot_2_lbl_2.setMaximumSize(QtCore.QSize(10, 16777215))
font = QtGui.QFont()
font.setPointSize(12)
font.setBold(False)
font.setKerning(True)
self.dot_2_lbl_2.setFont(font)
self.dot_2_lbl_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.dot_2_lbl_2.setObjectName("dot_2_lbl_2")
self.gateway_layout.addWidget(self.dot_2_lbl_2)
self.gateway_in_3 = QtWidgets.QLineEdit(parent=self.horizontalLayoutWidget_5)
self.gateway_in_3.setMaximumSize(QtCore.QSize(50, 16777215))
self.gateway_in_3.setMaxLength(3)
self.gateway_in_3.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.gateway_in_3.setObjectName("gateway_in_3")
self.gateway_layout.addWidget(self.gateway_in_3)
self.retranslateUi(Update_Network_Settings_Form)
self.cancel_btn.clicked.connect(Update_Network_Settings_Form.reject) # type: ignore
self.update_btn.clicked.connect(Update_Network_Settings_Form.accept) # type: ignore
QtCore.QMetaObject.connectSlotsByName(Update_Network_Settings_Form)
Update_Network_Settings_Form.setTabOrder(self.addr_in_0, self.addr_in_1)
Update_Network_Settings_Form.setTabOrder(self.addr_in_1, self.addr_in_2)
Update_Network_Settings_Form.setTabOrder(self.addr_in_2, self.addr_in_3)
Update_Network_Settings_Form.setTabOrder(self.addr_in_3, self.gateway_in_0)
Update_Network_Settings_Form.setTabOrder(self.gateway_in_0, self.gateway_in_1)
Update_Network_Settings_Form.setTabOrder(self.gateway_in_1, self.gateway_in_2)
Update_Network_Settings_Form.setTabOrder(self.gateway_in_2, self.gateway_in_3)
Update_Network_Settings_Form.setTabOrder(self.gateway_in_3, self.prefix_len_in)
Update_Network_Settings_Form.setTabOrder(self.prefix_len_in, self.port_in)
Update_Network_Settings_Form.setTabOrder(self.port_in, self.update_btn)
Update_Network_Settings_Form.setTabOrder(self.update_btn, self.cancel_btn)
def retranslateUi(self, Update_Network_Settings_Form):
_translate = QtCore.QCoreApplication.translate
Update_Network_Settings_Form.setWindowTitle(_translate("Update_Network_Settings_Form", "Update Network Settings"))
self.ip_addr_label.setText(_translate("Update_Network_Settings_Form", "IP Address:"))
self.addr_in_0.setText(_translate("Update_Network_Settings_Form", "192"))
self.dot_0_lbl.setText(_translate("Update_Network_Settings_Form", "."))
self.addr_in_1.setText(_translate("Update_Network_Settings_Form", "168"))
self.dot_1_lbl.setText(_translate("Update_Network_Settings_Form", "."))
self.addr_in_2.setText(_translate("Update_Network_Settings_Form", "1"))
self.dot_2_lbl.setText(_translate("Update_Network_Settings_Form", "."))
self.addr_in_3.setText(_translate("Update_Network_Settings_Form", "128"))
self.update_btn.setText(_translate("Update_Network_Settings_Form", "Update"))
self.cancel_btn.setText(_translate("Update_Network_Settings_Form", "Cancel"))
self.prefix_len_lbl.setText(_translate("Update_Network_Settings_Form", "Prefix Length:"))
self.prefix_len_in.setText(_translate("Update_Network_Settings_Form", "24"))
self.port_no_lbl.setText(_translate("Update_Network_Settings_Form", "Port:"))
self.port_in.setText(_translate("Update_Network_Settings_Form", "1337"))
self.gateway_lbl.setText(_translate("Update_Network_Settings_Form", "Gateway:"))
self.gateway_in_0.setText(_translate("Update_Network_Settings_Form", "192"))
self.dot_0_lbl_2.setText(_translate("Update_Network_Settings_Form", "."))
self.gateway_in_1.setText(_translate("Update_Network_Settings_Form", "168"))
self.dot_1_lbl_2.setText(_translate("Update_Network_Settings_Form", "."))
self.gateway_in_2.setText(_translate("Update_Network_Settings_Form", "1"))
self.dot_2_lbl_2.setText(_translate("Update_Network_Settings_Form", "."))
self.gateway_in_3.setText(_translate("Update_Network_Settings_Form", "1"))

View File

@ -1,614 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Update_Network_Settings_Form</class>
<widget class="QDialog" name="Update_Network_Settings_Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>415</width>
<height>180</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>415</width>
<height>180</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>415</width>
<height>180</height>
</size>
</property>
<property name="windowTitle">
<string>Update Network Settings</string>
</property>
<widget class="QWidget" name="horizontalLayoutWidget">
<property name="geometry">
<rect>
<x>20</x>
<y>10</y>
<width>371</width>
<height>41</height>
</rect>
</property>
<layout class="QHBoxLayout" name="ip_addr_layout">
<item>
<widget class="QLabel" name="ip_addr_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>120</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="layoutDirection">
<enum>Qt::LeftToRight</enum>
</property>
<property name="text">
<string>IP Address:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="addr_in_0">
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>192</string>
</property>
<property name="maxLength">
<number>3</number>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="dot_0_lbl">
<property name="maximumSize">
<size>
<width>10</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
<bold>false</bold>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string>.</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="addr_in_1">
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>168</string>
</property>
<property name="maxLength">
<number>3</number>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="dot_1_lbl">
<property name="maximumSize">
<size>
<width>10</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
<bold>false</bold>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string>.</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="addr_in_2">
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>1</string>
</property>
<property name="maxLength">
<number>3</number>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="dot_2_lbl">
<property name="maximumSize">
<size>
<width>10</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
<bold>false</bold>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string>.</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="addr_in_3">
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>128</string>
</property>
<property name="maxLength">
<number>3</number>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="horizontalLayoutWidget_4">
<property name="geometry">
<rect>
<x>20</x>
<y>130</y>
<width>371</width>
<height>47</height>
</rect>
</property>
<layout class="QHBoxLayout" name="buttons_layout">
<property name="topMargin">
<number>0</number>
</property>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="update_btn">
<property name="text">
<string>Update</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cancel_btn">
<property name="text">
<string>Cancel</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="horizontalLayoutWidget_3">
<property name="geometry">
<rect>
<x>20</x>
<y>90</y>
<width>371</width>
<height>41</height>
</rect>
</property>
<layout class="QHBoxLayout" name="port_layout_2" stretch="0,0,0,0,0">
<property name="spacing">
<number>6</number>
</property>
<item>
<widget class="QLabel" name="prefix_len_lbl">
<property name="maximumSize">
<size>
<width>98</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="text">
<string>Prefix Length:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="prefix_len_in">
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>24</string>
</property>
<property name="maxLength">
<number>2</number>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="port_no_lbl">
<property name="minimumSize">
<size>
<width>82</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="text">
<string>Port:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="port_in">
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>1337</string>
</property>
<property name="maxLength">
<number>5</number>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="horizontalLayoutWidget_5">
<property name="geometry">
<rect>
<x>20</x>
<y>50</y>
<width>371</width>
<height>41</height>
</rect>
</property>
<layout class="QHBoxLayout" name="gateway_layout">
<item>
<widget class="QLabel" name="gateway_lbl">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>120</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="layoutDirection">
<enum>Qt::LeftToRight</enum>
</property>
<property name="text">
<string>Gateway:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="gateway_in_0">
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>192</string>
</property>
<property name="maxLength">
<number>3</number>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="dot_0_lbl_2">
<property name="maximumSize">
<size>
<width>10</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
<bold>false</bold>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string>.</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="gateway_in_1">
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>168</string>
</property>
<property name="maxLength">
<number>3</number>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="dot_1_lbl_2">
<property name="maximumSize">
<size>
<width>10</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
<bold>false</bold>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string>.</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="gateway_in_2">
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>1</string>
</property>
<property name="maxLength">
<number>3</number>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="dot_2_lbl_2">
<property name="maximumSize">
<size>
<width>10</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
<bold>false</bold>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string>.</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="gateway_in_3">
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>1</string>
</property>
<property name="maxLength">
<number>3</number>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<tabstops>
<tabstop>addr_in_0</tabstop>
<tabstop>addr_in_1</tabstop>
<tabstop>addr_in_2</tabstop>
<tabstop>addr_in_3</tabstop>
<tabstop>gateway_in_0</tabstop>
<tabstop>gateway_in_1</tabstop>
<tabstop>gateway_in_2</tabstop>
<tabstop>gateway_in_3</tabstop>
<tabstop>prefix_len_in</tabstop>
<tabstop>port_in</tabstop>
<tabstop>update_btn</tabstop>
<tabstop>cancel_btn</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>cancel_btn</sender>
<signal>clicked()</signal>
<receiver>Update_Network_Settings_Form</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>340</x>
<y>140</y>
</hint>
<hint type="destinationlabel">
<x>364</x>
<y>96</y>
</hint>
</hints>
</connection>
<connection>
<sender>update_btn</sender>
<signal>clicked()</signal>
<receiver>Update_Network_Settings_Form</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>258</x>
<y>147</y>
</hint>
<hint type="destinationlabel">
<x>237</x>
<y>99</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -1,194 +0,0 @@
"""
The MIT License (MIT)
Copyright (c) 2012-2014 Alexander Turkin
Copyright (c) 2014 William Hallatt
Copyright (c) 2015 Jacob Dawid
Copyright (c) 2016 Luca Weiss
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import math
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
class QtWaitingSpinner(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
# WAS IN initialize()
self._color = QColor(Qt.GlobalColor.black)
self._roundness = 100.0
self._minimumTrailOpacity = 3.14159265358979323846
self._trailFadePercentage = 80.0
self._revolutionsPerSecond = 1.57079632679489661923
self._numberOfLines = 20
self._lineLength = 5
self._lineWidth = 2
self._innerRadius = 5
self._currentCounter = 0
self._timer = QTimer(self)
self._timer.timeout.connect(self.rotate)
self.updateSize()
self.updateTimer()
# END initialize()
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
def paintEvent(self, QPaintEvent):
painter = QPainter(self)
painter.fillRect(self.rect(), Qt.GlobalColor.transparent)
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
if self._currentCounter >= self._numberOfLines:
self._currentCounter = 0
painter.setPen(Qt.PenStyle.NoPen)
for i in range(0, self._numberOfLines):
painter.save()
painter.translate(self._innerRadius + self._lineLength, self._innerRadius + self._lineLength)
rotateAngle = float(360 * i) / float(self._numberOfLines)
painter.rotate(rotateAngle)
painter.translate(self._innerRadius, 0)
distance = self.lineCountDistanceFromPrimary(i, self._currentCounter, self._numberOfLines)
color = self.currentLineColor(distance, self._numberOfLines, self._trailFadePercentage,
self._minimumTrailOpacity, self._color)
painter.setBrush(color)
painter.drawRoundedRect(QRect(0, int(-self._lineWidth / 2), self._lineLength, self._lineWidth), self._roundness,
self._roundness, Qt.SizeMode.RelativeSize)
painter.restore()
def start(self):
if not self._timer.isActive():
self._timer.start()
self._currentCounter = 0
def stop(self):
if self._timer.isActive():
self._timer.stop()
self._currentCounter = 0
def setNumberOfLines(self, lines):
self._numberOfLines = lines
self._currentCounter = 0
self.updateTimer()
def setLineLength(self, length):
self._lineLength = length
self.updateSize()
def setLineWidth(self, width):
self._lineWidth = width
self.updateSize()
def setInnerRadius(self, radius):
self._innerRadius = radius
self.updateSize()
def color(self):
return self._color
def roundness(self):
return self._roundness
def minimumTrailOpacity(self):
return self._minimumTrailOpacity
def trailFadePercentage(self):
return self._trailFadePercentage
def revolutionsPersSecond(self):
return self._revolutionsPerSecond
def numberOfLines(self):
return self._numberOfLines
def lineLength(self):
return self._lineLength
def lineWidth(self):
return self._lineWidth
def innerRadius(self):
return self._innerRadius
def setRoundness(self, roundness):
self._roundness = max(0.0, min(100.0, roundness))
def setColor(self, color=Qt.GlobalColor.black):
self._color = QColor(color)
def setRevolutionsPerSecond(self, revolutionsPerSecond):
self._revolutionsPerSecond = revolutionsPerSecond
self.updateTimer()
def setTrailFadePercentage(self, trail):
self._trailFadePercentage = trail
def setMinimumTrailOpacity(self, minimumTrailOpacity):
self._minimumTrailOpacity = minimumTrailOpacity
def rotate(self):
self._currentCounter += 1
if self._currentCounter >= self._numberOfLines:
self._currentCounter = 0
self.update()
def updateSize(self):
self.size = (self._innerRadius + self._lineLength) * 2
self.setFixedSize(self.size, self.size)
def updateTimer(self):
self._timer.setInterval(int(1000 / (self._numberOfLines * self._revolutionsPerSecond)))
def lineCountDistanceFromPrimary(self, current, primary, totalNrOfLines):
distance = primary - current
if distance < 0:
distance += totalNrOfLines
return distance
def currentLineColor(self, countDistance, totalNrOfLines, trailFadePerc, minOpacity, colorinput):
color = QColor(colorinput)
if countDistance == 0:
return color
minAlphaF = minOpacity / 100.0
distanceThreshold = int(math.ceil((totalNrOfLines - 1) * trailFadePerc / 100.0))
if countDistance > distanceThreshold:
color.setAlphaF(minAlphaF)
else:
alphaDiff = color.alphaF() - minAlphaF
gradient = alphaDiff / float(distanceThreshold + 1)
resultAlpha = color.alphaF() - gradient * countDistance
# If alpha is out of bounds, clip it.
resultAlpha = min(1.0, max(0.0, resultAlpha))
color.setAlphaF(resultAlpha)
return color
if __name__ == '__main__':
app = QApplication([])
waiting_spinner = QtWaitingSpinner()
waiting_spinner.show()
waiting_spinner.start()
app.exec()

View File

@ -1,64 +0,0 @@
max_width = 120
hard_tabs = false
tab_spaces = 4
newline_style = "Auto"
use_small_heuristics = "Default"
indent_style = "Block"
wrap_comments = false
format_code_in_doc_comments = false
comment_width = 100
normalize_comments = false
normalize_doc_attributes = false
format_strings = true
format_macro_matchers = true
format_macro_bodies = true
empty_item_single_line = true
struct_lit_single_line = true
fn_single_line = false
where_single_line = true
imports_indent = "Visual"
imports_layout = "Mixed"
imports_granularity="Crate"
group_imports = "StdExternalCrate"
reorder_imports = true
reorder_modules = true
reorder_impl_items = false
type_punctuation_density = "Wide"
space_before_colon = false
space_after_colon = true
spaces_around_ranges = false
binop_separator = "Front"
remove_nested_parens = true
combine_control_expr = true
overflow_delimited_expr = false
struct_field_align_threshold = 0
enum_discrim_align_threshold = 0
match_arm_blocks = true
match_arm_leading_pipes = "Never"
force_multiline_blocks = false
fn_params_layout = "Tall"
brace_style = "SameLineWhere"
control_brace_style = "AlwaysSameLine"
trailing_semicolon = true
trailing_comma = "Vertical"
match_block_trailing_comma = false
blank_lines_upper_bound = 1
blank_lines_lower_bound = 0
edition = "2018"
version = "Two"
inline_attribute_width = 0
merge_derives = true
use_try_shorthand = false
use_field_init_shorthand = false
force_explicit_abi = true
condense_wildcard_suffixes = false
color = "Auto"
unstable_features = false
disable_all_formatting = false
skip_children = false
hide_parse_errors = false
error_on_line_overflow = false
error_on_unformatted = false
ignore = []
emit_mode = "Files"
make_backup = false

View File

@ -1,29 +1,31 @@
use super::{delay, gpio, usb};
use crate::laser_diode::current_sources::*;
use crate::network::network;
use crate::network::network::EthernetPeripherals;
use fugit::ExtU32;
use log::{debug, info};
use stm32f4xx_hal::{pac::{CorePeripherals, Peripherals},
rcc::RccExt,
time::MegaHertz,
timer::TimerExt,
watchdog::IndependentWatchdog};
use uom::si::{electric_current::{ampere, milliampere},
f32::ElectricCurrent};
use super::{gpio, sys_timer, usb};
use crate::{device::flash_store::{self, FlashStore},
laser_diode::{laser_diode::LdDrive, ld_ctrl::*},
net::net::{IpSettings, ServerHandle},
thermostat::{max1968::MAX1968, thermostat::Thermostat},
DeviceSettings};
use log::info;
use stm32f4xx_hal::{
pac::{CorePeripherals, Peripherals},
rcc::RccExt,
time::MegaHertz,
watchdog::IndependentWatchdog,
};
use systick_monotonic::Systick;
#[cfg(not(feature = "semihosting"))]
const WATCHDOG_PERIOD: u32 = 4000;
const WATCHDOG_PERIOD: u32 = 1000;
#[cfg(feature = "semihosting")]
const WATCHDOG_PERIOD: u32 = 30000;
pub fn bootup(
mut core_perif: CorePeripherals,
perif: Peripherals,
) -> (IndependentWatchdog, FlashStore, LdDrive, Thermostat) {
server_storage: &'static mut network::ServerStorage,
) -> (
IndependentWatchdog,
Systick<1000_u32>,
network::ServerHandle,
) {
core_perif.SCB.enable_icache();
core_perif.SCB.enable_dcache(&mut core_perif.CPUID);
@ -34,92 +36,50 @@ pub fn bootup(
.use_hse(MegaHertz::from_raw(8).convert())
.sysclk(MegaHertz::from_raw(168).convert())
.hclk(MegaHertz::from_raw(168).convert())
.pclk1(MegaHertz::from_raw(42).convert())
.pclk2(MegaHertz::from_raw(84).convert())
.pclk1(MegaHertz::from_raw(32).convert())
.pclk2(MegaHertz::from_raw(64).convert())
.freeze();
sys_timer::setup(core_perif.SYST, clocks);
let systick = Systick::new(core_perif.SYST, clocks.hclk().to_Hz());
let delay = delay::AsmDelay::new(clocks.sysclk().to_Hz());
let (mut hw_rev, eth_pins, eth_mgmt_pins, usb, current_source_phy, ad7172_phy, max1968_phy, pd_mon_phy) =
gpio::setup(
clocks,
perif.TIM4,
perif.GPIOA,
perif.GPIOB,
perif.GPIOC,
perif.GPIOD,
perif.GPIOE,
perif.SPI1,
perif.SPI2,
perif.SPI3,
perif.OTG_FS_GLOBAL,
perif.OTG_FS_DEVICE,
perif.OTG_FS_PWRCLK,
);
let (eth_pins, usb, current_source_phy, mdio, mdc) = gpio::setup(
clocks,
perif.GPIOA,
perif.GPIOB,
perif.GPIOC,
perif.GPIOD,
perif.SPI2,
perif.OTG_FS_GLOBAL,
perif.OTG_FS_DEVICE,
perif.OTG_FS_PWRCLK,
);
usb::State::setup(usb);
debug!("Setting up TEC");
let tec_driver = MAX1968::new(max1968_phy, perif.ADC1, perif.ADC2, perif.DMA2);
let mut thermostat = Thermostat::new(tec_driver, ad7172_phy);
thermostat.setup();
thermostat.power_up();
thermostat.calibrate_dac_value();
thermostat.set_i(ElectricCurrent::new::<ampere>(0.0));
debug!("Setting up Laser Driver");
let current_source = LdCtrl::new(current_source_phy);
let mut laser = LdDrive::new(current_source, perif.ADC3, perif.TIM2.counter(&clocks), pd_mon_phy);
laser.setup();
laser.ld_open();
laser.ld_set_i(ElectricCurrent::new::<ampere>(0.0));
laser.set_pd_i_limit(ElectricCurrent::new::<milliampere>(2.5));
laser.set_pd_mon_calibrated_vdda(thermostat.get_calibrated_vdda());
laser.power_up();
debug!("Setting up Internal Flash Driver");
let flash_store = flash_store::store(perif.FLASH);
let mut ip_settings: IpSettings = IpSettings::default();
let device_settings: DeviceSettings;
match flash_store.read_value("Device") {
Ok(Some(config)) => {
device_settings = config;
ip_settings = device_settings.ip_settings;
debug!("Found Device Settings");
}
Ok(None) => {
debug!("Flash does not have IP Settings");
}
Err(e) => {
debug!("Cannot Store Flash: {:?}", e);
}
}
debug!("Setting up ETH");
let mac_addr = hw_rev.get_mac_address();
let ethernet_parts_in = stm32_eth::PartsIn {
let ethernet = EthernetPeripherals {
dma: perif.ETHERNET_DMA,
mac: perif.ETHERNET_MAC,
mmc: perif.ETHERNET_MMC,
ptp: perif.ETHERNET_PTP,
};
ServerHandle::new(
eth_pins,
eth_mgmt_pins,
ethernet_parts_in,
clocks,
perif.TIM5.counter(&clocks),
mac_addr,
ip_settings,
);
let server = network::ServerHandle::new(eth_pins, ethernet, clocks, server_storage, mdio, mdc);
// let (serverhandle, iface) = network::setup(eth_pins, ethernet, clocks);
usb::State::setup(usb);
let mut laser = CurrentSource {
phy: current_source_phy,
setting: CurrentSourceSettings {
output_current: 0.0,
},
};
laser.setup(delay);
laser.set_current(0.1).unwrap();
debug!("Setting Watchdog");
let mut wd = IndependentWatchdog::new(perif.IWDG);
wd.start(WATCHDOG_PERIOD.millis());
wd.feed();
info!("Kirdy setup complete");
(wd, flash_store, laser, thermostat)
(wd, systick, server)
}

46
src/device/delay.rs Normal file
View File

@ -0,0 +1,46 @@
//! Basic blocking delay
//!
//! This module provides a basic asm-based blocking delay.
//! Borrowed from our good friends at Quartiq:
//! https://github.com/quartiq/stabilizer/blob/master/src/hardware/delay.rs
//!
use stm32f4xx_hal::hal::blocking::delay::{DelayMs, DelayUs};
// use stm32f4xx_hal::block;
/// A basic delay implementation.
pub struct AsmDelay {
frequency_us: u32,
frequency_ms: u32,
}
impl AsmDelay {
/// Create a new delay.
///
/// # Args
/// * `freq` - The CPU core frequency.
pub fn new(freq: u32) -> AsmDelay {
AsmDelay {
frequency_us: (freq / 1_000_000),
frequency_ms: (freq / 1_000),
}
}
}
impl<U> DelayUs<U> for AsmDelay
where
U: Into<u32>,
{
fn delay_us(&mut self, us: U) {
cortex_m::asm::delay(self.frequency_us * us.into())
}
}
impl<U> DelayMs<U> for AsmDelay
where
U: Into<u32>,
{
fn delay_ms(&mut self, ms: U) {
cortex_m::asm::delay(self.frequency_ms * ms.into())
}
}

View File

@ -1,47 +0,0 @@
use core::arch::asm;
use cortex_m_rt::pre_init;
use stm32f4xx_hal::pac::{RCC, SYSCFG};
const DFU_TRIG_MSG: u32 = 0xDECAFBAD;
extern "C" {
// This symbol comes from memory.x
static mut _dfu_msg: u32;
}
pub unsafe fn set_dfu_trigger() {
_dfu_msg = DFU_TRIG_MSG;
}
/// Called by reset handler in lib.rs immediately after reset.
/// This function should not be called outside of reset handler as
/// bootloader expects MCU to be in reset state when called.
#[cfg(target_arch = "arm")]
#[pre_init]
unsafe fn __pre_init() {
if _dfu_msg == DFU_TRIG_MSG {
_dfu_msg = 0x00000000;
// Enable system config controller clock
let rcc = &*RCC::ptr();
rcc.apb2enr.modify(|_, w| w.syscfgen().set_bit());
// Bypass BOOT pins and remap bootloader to 0x00000000
let syscfg = &*SYSCFG::ptr();
syscfg.memrm.write(|w| w.mem_mode().bits(0b01));
// Impose instruction and memory barriers
cortex_m::asm::isb();
cortex_m::asm::dsb();
asm!(
// Set stack pointer to bootloader location
"LDR R0, =0x1FFF0000",
"LDR SP,[R0, #0]",
// Jump to bootloader
"LDR R0,[R0, #4]",
"BX R0",
);
}
}

View File

@ -1,58 +0,0 @@
use log::error;
use sfkv::{Store, StoreBackend};
use stm32f4xx_hal::{flash::{Error, FlashExt},
pac::FLASH};
// Last flash sector is used to avoid overwriting the code in flash.
pub const FLASH_SECTOR: u8 = 11;
pub const FLASH_SECTOR_11_OFFSET: u32 = 0xE0000;
/// Only 16 KiB out of 128KiB in the Sector is used to save RAM
pub const RESERVED_MEMORY: usize = 0x4000;
static mut BACKUP_SPACE: [u8; RESERVED_MEMORY] = [0; RESERVED_MEMORY];
pub struct FlashBackend {
flash: FLASH,
}
fn get_offset() -> usize {
FLASH_SECTOR_11_OFFSET as usize
}
impl StoreBackend for FlashBackend {
type Data = [u8];
fn data(&self) -> &Self::Data {
&self.flash.read()[get_offset()..(get_offset() + RESERVED_MEMORY)]
}
type Error = Error;
fn erase(&mut self) -> Result<(), Self::Error> {
self.flash.unlocked().erase(FLASH_SECTOR)
}
fn program(&mut self, offset: usize, payload: &[u8]) -> Result<(), Self::Error> {
self.flash.unlocked().program(get_offset() + offset, payload.iter())
}
fn backup_space(&self) -> &'static mut [u8] {
unsafe { BACKUP_SPACE.as_mut() }
}
}
pub type FlashStore = Store<FlashBackend>;
pub fn store(flash: FLASH) -> FlashStore {
let backend = FlashBackend { flash };
let mut store = FlashStore::new(backend);
match store.get_bytes_used() {
Ok(_) => {}
Err(e) => {
error!("corrupt store, erasing. error: {:?}", e);
let _ = store.erase().map_err(|e| error!("flash erase failed: {:?}", e));
}
}
store
}

View File

@ -1,71 +1,46 @@
use crate::laser_diode::current_sources::*;
use fugit::RateExtU32;
use stm32_eth::EthPins;
use stm32f4xx_hal::{gpio::{alt::otg_fs::{Dm, Dp},
gpioa::*,
gpiob::*,
gpioc::*,
GpioExt, Input, Speed},
otg_fs::USB,
pac::{GPIOA, GPIOB, GPIOC, GPIOD, GPIOE, OTG_FS_DEVICE, OTG_FS_GLOBAL, OTG_FS_PWRCLK, SPI1, SPI2,
SPI3, TIM4},
rcc::Clocks,
spi::{NoMiso, Spi},
timer::{pwm::PwmExt, Channel1, Channel2, Channel3}};
use stm32f4xx_hal::{
gpio::{Alternate, GpioExt, Speed},
otg_fs::USB,
pac::{GPIOA, GPIOB, GPIOC, GPIOD, OTG_FS_DEVICE, OTG_FS_GLOBAL, OTG_FS_PWRCLK, SPI2},
rcc::Clocks,
spi,
spi::{NoMiso, Spi},
};
use crate::{device::hw_rev::{HWRev, HwRevPins},
laser_diode::{ld_ctrl::{self, LdCtrlPhy},
ld_pwr_exc_protector::LdPwrExcProtectorPhy,
max5719},
net::net::EthernetMgmtPins,
thermostat::{ad5680, ad7172,
max1968::{self, MAX1968Phy, MAX1968PinSet, PWM_FREQ_KHZ}}};
pub type EthernetPins = EthPins<PA1<Input>, PA7<Input>, PB11<Input>, PB12<Input>, PB13<Input>, PC4<Input>, PC5<Input>>;
use crate::network::network::EthernetPins;
pub fn setup(
clocks: Clocks,
tim4: TIM4,
gpioa: GPIOA,
gpiob: GPIOB,
gpioc: GPIOC,
gpiod: GPIOD,
gpioe: GPIOE,
spi1: SPI1,
spi2: SPI2,
spi3: SPI3,
otg_fs_global: OTG_FS_GLOBAL,
otg_fs_device: OTG_FS_DEVICE,
otg_fs_pwrclk: OTG_FS_PWRCLK,
) -> (
HWRev,
EthernetPins,
EthernetMgmtPins,
USB,
LdCtrlPhy<ld_ctrl::Channel0>,
ad7172::AdcPhy,
MAX1968Phy<max1968::Channel0>,
LdPwrExcProtectorPhy,
CurrentSourcePhyConstruct<CurrentSourcePhyCh0>,
stm32f4xx_hal::gpio::PA2<Alternate<11>>,
stm32f4xx_hal::gpio::PC1<Alternate<11>>, // photo_diode_phy,
// thermostat_phy
) {
let gpioa = gpioa.split();
let gpiob = gpiob.split();
let gpioc = gpioc.split();
let gpiod = gpiod.split();
let gpioe = gpioe.split();
let mut hw_rev = HWRev::detect_hw_rev(HwRevPins {
h0: gpioe.pe8.into_input(),
h1: gpioe.pe9.into_input(),
h2: gpioe.pe10.into_input(),
h3: gpioe.pe11.into_input(),
});
hw_rev.startup_delay_before_gpio_init();
let usb = USB {
usb_global: otg_fs_global,
usb_device: otg_fs_device,
usb_pwrclk: otg_fs_pwrclk,
pin_dm: Dm::PA11(gpioa.pa11.into_alternate()),
pin_dp: Dp::PA12(gpioa.pa12.into_alternate()),
pin_dm: gpioa.pa11.into_alternate(),
pin_dp: gpioa.pa12.into_alternate(),
hclk: clocks.hclk(),
};
@ -79,86 +54,31 @@ pub fn setup(
rx_d1: gpioc.pc5,
};
let mut eth_mgmt_pins = EthernetMgmtPins {
mdio: gpioa.pa2.into_alternate::<11>(),
mdc: gpioc.pc1.into_alternate::<11>(),
};
eth_mgmt_pins.mdio.set_speed(Speed::VeryHigh);
eth_mgmt_pins.mdc.set_speed(Speed::VeryHigh);
let mut mdio = gpioa.pa2.into_alternate::<11>();
let mut mdc = gpioc.pc1.into_alternate::<11>();
mdio.set_speed(Speed::VeryHigh);
mdc.set_speed(Speed::VeryHigh);
let current_source_phy = LdCtrlPhy {
dac: max5719::Dac::new(
Spi::new(
spi2,
(gpiob.pb10.into_alternate(), NoMiso::new(), gpiob.pb15.into_alternate()),
max5719::SPI_MODE,
max5719::SPI_CLOCK_MHZ.convert(),
&clocks,
),
gpiod.pd8.into_push_pull_output(),
gpiob.pb14.into_push_pull_output(),
),
current_source_short_pin: gpioa.pa4.into_push_pull_output(),
termination_status_pin: gpiod.pd7.internal_pull_up(true),
};
let pd_mon_phy = LdPwrExcProtectorPhy {
_pd_mon_ch0: gpioa.pa3.into_analog(),
pwr_en_ch0: gpiod.pd9.into_push_pull_output(),
};
let pwm_chs = (
Channel1::new(gpiob.pb6),
Channel2::new(gpiob.pb7),
Channel3::new(gpiob.pb8),
);
let (max_i_neg0, max_v0, max_i_pos0) = tim4.pwm_hz(pwm_chs, PWM_FREQ_KHZ.convert(), &clocks).split();
let max1968_phy = MAX1968Phy::new(MAX1968PinSet {
dac: ad5680::Dac::new(
Spi::new(
spi1,
(gpiob.pb3.into_alternate(), NoMiso::new(), gpiob.pb5.into_alternate()),
ad5680::SPI_MODE,
ad5680::SPI_CLOCK_MHZ.convert(),
&clocks,
),
gpiob.pb4.into_push_pull_output(),
),
shdn: gpioa.pa5.into_push_pull_output(),
vref_pin: gpioa.pa6.into_analog(),
itec_pin: gpiob.pb1.into_analog(),
dac_feedback_pin: gpioc.pc0.into_analog(),
vtec_pin: gpiob.pb0.into_analog(),
max_v: max_v0,
max_i_pos: max_i_pos0,
max_i_neg: max_i_neg0,
});
let ad7172_phy = ad7172::Adc::new(
Spi::new(
spi3,
let current_source_phy = CurrentSourcePhyConstruct {
max5719_spi: Spi::new(
spi2,
(
gpioc.pc10.into_alternate(),
gpioc.pc11.into_alternate(),
gpioc.pc12.into_alternate(),
gpiob.pb10.into_alternate(),
NoMiso {},
gpiob.pb15.into_alternate(),
),
ad7172::SPI_MODE,
ad7172::SPI_CLOCK_MHZ.convert(),
spi::Mode {
polarity: spi::Polarity::IdleLow,
phase: spi::Phase::CaptureOnFirstTransition,
},
10_u32.MHz(),
&clocks,
),
gpioa.pa15.into_push_pull_output(),
)
.unwrap();
max5719_load: gpiob.pb14.into_push_pull_output(),
max5719_cs: gpiod.pd8.into_push_pull_output(),
current_source_ldo_en: gpiod.pd9.into_push_pull_output(),
current_source_short: gpioa.pa4.into_push_pull_output(),
};
(
hw_rev,
eth_pins,
eth_mgmt_pins,
usb,
current_source_phy,
ad7172_phy,
max1968_phy,
pd_mon_phy,
)
(eth_pins, usb, current_source_phy, mdio, mdc)
}

View File

@ -1,65 +0,0 @@
use crc::{Crc, CRC_24_BLE};
use stm32f4xx_hal::{gpio::{Input, PE10, PE11, PE8, PE9},
signature};
use crate::device::sys_timer::sleep;
pub struct HwRevPins {
pub h0: PE8<Input>,
pub h1: PE9<Input>,
pub h2: PE10<Input>,
pub h3: PE11<Input>,
}
pub struct HWRev {
pub major: u8,
pub minor: u8,
}
impl HWRev {
pub fn detect_hw_rev(hwrev_pins: HwRevPins) -> Self {
let (h0, h1, h2, h3) = (
hwrev_pins.h0.is_high(),
hwrev_pins.h1.is_high(),
hwrev_pins.h2.is_high(),
hwrev_pins.h3.is_high(),
);
match (h0, h1, h2, h3) {
(true, true, true, true) => HWRev { major: 0, minor: 3 },
(_, _, _, _) => HWRev { major: 0, minor: 0 },
}
}
/// On Rev0_3, during power up, digital power rails are stabilized way before analog power rails
/// This causes improper initialization on any peripherals requiring calibrations
/// See Issue #32 on Kirdy Hw Repo
pub fn startup_delay_before_gpio_init(&mut self) {
if self.major == 0 && self.minor == 3 {
sleep(5000);
}
}
/// On Rev0_3, it lacks pre-allocated Mac Addresses accessible on PCB.
/// This functions generate a random Mac Address with 96bit unique UUID inside STM32
/// See Issue #36 on Kirdy Hw Repo
pub fn get_mac_address(&mut self) -> [u8; 6] {
if self.major == 0 && self.minor == 3 {
let uid = signature::Uid::get();
let mut uid_data: [u8; 12] = [0; 12];
uid_data[0] = uid.x() as u8;
uid_data[1] = (uid.x() >> 8) as u8;
uid_data[2] = uid.y() as u8;
uid_data[3] = (uid.y() >> 8) as u8;
uid_data[4..11].clone_from_slice(uid.lot_num().as_bytes());
let crc: Crc<u32> = Crc::<u32>::new(&CRC_24_BLE);
let mut digest = crc.digest();
digest.update(&uid_data);
let crc24 = digest.finalize();
[0x02, 0xE0, 0xD5, (crc24 >> 16) as u8, (crc24 >> 8) as u8, (crc24 as u8)]
} else {
unimplemented!()
}
}
}

View File

@ -3,15 +3,13 @@ pub fn init_log() {
use super::usb;
static USB_LOGGER: usb::Logger = usb::Logger;
let _ = log::set_logger(&USB_LOGGER);
// log::set_max_level(log::LevelFilter::Debug);
log::set_max_level(log::LevelFilter::Trace);
log::set_max_level(log::LevelFilter::Debug);
}
#[cfg(feature = "RTT")]
pub fn init_log() {
use rtt_target::rtt_init_print;
use super::rtt_logger;
use rtt_target::rtt_init_print;
static RTT_LOGGER: rtt_logger::Logger = rtt_logger::Logger;
rtt_init_print!();
let _ = log::set_logger(&RTT_LOGGER);
@ -20,8 +18,8 @@ pub fn init_log() {
#[cfg(feature = "semihosting")]
pub fn init_log() {
use cortex_m_log::{log::{init, Logger},
printer::semihosting::{hio::HStdout, InterruptOk}};
use cortex_m_log::log::{init, Logger};
use cortex_m_log::printer::semihosting::{hio::HStdout, InterruptOk};
use log::LevelFilter;
static mut LOGGER: Option<Logger<InterruptOk<HStdout>>> = None;
let logger = Logger {

View File

@ -1,9 +1,6 @@
pub mod boot;
pub mod dfu;
pub mod flash_store;
pub mod delay;
pub mod gpio;
pub mod hw_rev;
pub mod log_setup;
pub mod rtt_logger;
pub mod sys_timer;
pub mod usb;

View File

@ -1,42 +0,0 @@
use core::{cell::RefCell, ops::Deref};
use cortex_m::{interrupt::Mutex, peripheral::syst::SystClkSource};
use cortex_m_rt::exception;
use stm32f4xx_hal::{pac::SYST, rcc::Clocks};
/// Rate in Hz
const TIMER_RATE: u32 = 1000;
/// Interval duration in milliseconds
const TIMER_DELTA: u32 = 1000 / TIMER_RATE;
/// Elapsed time in milliseconds
static TIMER_MS: Mutex<RefCell<u32>> = Mutex::new(RefCell::new(0));
/// Setup SysTick exception
pub fn setup(mut syst: SYST, clocks: Clocks) {
syst.set_clock_source(SystClkSource::Core);
syst.set_reload(clocks.hclk().to_Hz() / TIMER_RATE - 1);
syst.enable_counter();
syst.enable_interrupt();
}
/// SysTick exception (Timer)
#[exception]
fn SysTick() {
cortex_m::interrupt::free(|cs| {
*TIMER_MS.borrow(cs).borrow_mut() += TIMER_DELTA;
});
}
/// Obtain current time in milliseconds
pub fn now() -> u32 {
cortex_m::interrupt::free(|cs| *TIMER_MS.borrow(cs).borrow().deref())
}
/// block for `amount` milliseconds
pub fn sleep(amount: u32) {
if amount == 0 {
return;
}
let start = now();
while now() - start <= amount - 1 {}
}

View File

@ -1,14 +1,17 @@
use core::{fmt::{self, Write},
mem::MaybeUninit};
use core::{
fmt::{self, Write},
mem::MaybeUninit,
};
use cortex_m::interrupt::free;
use log::{Log, Metadata, Record};
use stm32f4xx_hal::{otg_fs::{UsbBus as Bus, USB},
pac::{interrupt, Interrupt, NVIC}};
use usb_device::{class_prelude::UsbBusAllocator,
descriptor::lang_id,
device::StringDescriptors,
prelude::{UsbDevice, UsbDeviceBuilder, UsbVidPid}};
use stm32f4xx_hal::{
otg_fs::{UsbBus as Bus, USB},
pac::{interrupt, Interrupt, NVIC},
};
use usb_device::{
class_prelude::UsbBusAllocator,
prelude::{UsbDevice, UsbDeviceBuilder, UsbVidPid},
};
use usbd_serial::SerialPort;
static mut EP_MEMORY: [u32; 1024] = [0; 1024];
@ -24,20 +27,16 @@ pub struct State {
impl State {
pub fn setup(usb: USB) {
unsafe { BUS.write(Bus::new(usb, EP_MEMORY.as_mut())) };
let str_descriptor = StringDescriptors::new(lang_id::LangID::EN)
.manufacturer("M-Labs")
.product("Kirdy");
unsafe { BUS.write(Bus::new(usb, &mut EP_MEMORY)) };
let bus = unsafe { BUS.assume_init_ref() };
let serial = SerialPort::new(bus);
let dev = UsbDeviceBuilder::new(bus, UsbVidPid(0x16c0, 0x27dd))
.manufacturer("M-Labs")
.product("thermostat")
.device_release(0x20)
.self_powered(true)
.device_class(usbd_serial::USB_CLASS_CDC)
.strings(&[str_descriptor])
.unwrap()
.build();
free(|_| unsafe {

View File

@ -0,0 +1,73 @@
use cortex_m::prelude::_embedded_hal_blocking_delay_DelayMs;
use stm32f4xx_hal::{
gpio::{gpioa::*, gpiob::*, gpiod::*, Alternate, Output, PushPull, PD9},
hal::{blocking::spi::Write, digital::v2::OutputPin},
pac::SPI2,
spi::{NoMiso, Spi, TransferModeNormal},
};
pub trait CurrentSourcePhy {
type CurrentSourceLdoEn: OutputPin;
type CurrentSourceShort: OutputPin;
type Max5719Load: OutputPin;
type Max5719Cs: OutputPin;
type Max5719Spi: Write<u8>;
}
pub struct CurrentSourcePhyConstruct<C: CurrentSourcePhy> {
pub max5719_spi: C::Max5719Spi,
pub max5719_load: C::Max5719Load,
pub max5719_cs: C::Max5719Cs,
pub current_source_ldo_en: C::CurrentSourceLdoEn,
pub current_source_short: C::CurrentSourceShort,
}
pub struct CurrentSourceSettings {
pub output_current: f32,
}
pub struct CurrentSource<C: CurrentSourcePhy> {
pub phy: CurrentSourcePhyConstruct<C>,
pub setting: CurrentSourceSettings,
}
pub struct CurrentSourcePhyCh0;
impl CurrentSourcePhy for CurrentSourcePhyCh0 {
type CurrentSourceLdoEn = PD9<Output<PushPull>>;
type CurrentSourceShort = PA4<Output<PushPull>>;
type Max5719Load = PB14<Output<PushPull>>;
type Max5719Cs = PD8<Output<PushPull>>;
type Max5719Spi =
Spi<SPI2, (PB10<Alternate<5>>, NoMiso, PB15<Alternate<5>>), TransferModeNormal>;
}
use crate::device::delay;
impl<C: CurrentSourcePhy> CurrentSource<C> {
pub fn setup(&mut self, mut delay: delay::AsmDelay) {
let _ = self.phy.max5719_load.set_high();
let _ = self.phy.max5719_cs.set_high();
let _ = self.phy.current_source_ldo_en.set_high();
delay.delay_ms(10_u32);
let _ = self.phy.current_source_short.set_high();
delay.delay_ms(10_u32);
}
pub fn set_current(
&mut self,
current: f32,
) -> Result<(), <<C as CurrentSourcePhy>::Max5719Spi as Write<u8>>::Error> {
let _ = self.phy.max5719_load.set_high();
let _ = self.phy.max5719_cs.set_low();
self.setting.output_current = current * 10.0 / 0.75;
let word = (((self.setting.output_current / 4.096) * 1048576.0) as u32) << 4;
let mut buf = [
((word >> 16) & 0xFF) as u8,
((word >> 8) & 0xFF) as u8,
((word >> 0) & 0xFF) as u8,
];
self.phy.max5719_spi.write(&mut buf)?;
let _ = self.phy.max5719_cs.set_high();
let _ = self.phy.max5719_load.set_low();
Ok(())
}
}

View File

@ -1,287 +0,0 @@
use core::marker::PhantomData;
use miniconf::Tree;
use serde::{Deserialize, Serialize};
use stm32f4xx_hal::{pac::{ADC3, TIM2},
timer::CounterUs};
use uom::{si::{electric_current::{ampere, milliampere},
f32::{ElectricCurrent, ElectricPotential, Power},
power::milliwatt,
Quantity, ISQ, SI},
typenum::*};
use crate::{device::sys_timer::sleep,
laser_diode::{ld_ctrl::{Impedance, LdCtrl},
ld_current_out_ctrl_timer::LdCurrentOutCtrlTimer,
ld_pwr_exc_protector::{self, LdPwrExcProtector},
pd_mon_params}};
// Volt / Ampere
pub type TransimpedanceUnit = Quantity<ISQ<P2, P1, N3, N2, Z0, Z0, Z0>, SI<f32>, f32>;
// Ampere / Volt
type TransconductanceUnit = Quantity<ISQ<N2, N1, P3, P2, Z0, Z0, Z0>, SI<f32>, f32>;
impl Settings {
pub const LD_CURRENT_MAX: ElectricCurrent = ElectricCurrent {
dimension: PhantomData,
units: PhantomData,
value: 0.3,
};
pub const DAC_OUT_V_MAX: ElectricPotential = ElectricPotential {
dimension: PhantomData,
units: PhantomData,
value: 4.096,
};
// Unit: A/V
const PD_MON_TRANSCONDUCTANCE: TransconductanceUnit = TransconductanceUnit {
dimension: PhantomData,
units: PhantomData,
value: 0.001,
};
const LD_DRIVE_TRANSIMPEDANCE: TransimpedanceUnit = TransimpedanceUnit {
dimension: PhantomData,
units: PhantomData,
value: 10.0 / 0.75,
};
}
#[derive(Deserialize, Serialize, Clone, Copy, Debug, Tree)]
struct Settings {
pwr_on: bool,
default_pwr_on: bool,
ld_drive_current: ElectricCurrent,
ld_drive_current_limit: ElectricCurrent,
pd_mon_params: pd_mon_params::Parameters,
ld_pwr_limit: Power,
ld_terms_short: bool,
}
impl Default for Settings {
fn default() -> Self {
Self {
pwr_on: false,
default_pwr_on: false,
ld_drive_current: ElectricCurrent::new::<milliampere>(0.0),
ld_drive_current_limit: ElectricCurrent::new::<milliampere>(0.0),
pd_mon_params: pd_mon_params::Parameters::default(),
ld_pwr_limit: Power::new::<milliwatt>(0.0),
ld_terms_short: false,
}
}
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct StatusReport {
pwr_on: bool,
pwr_excursion: bool,
ld_i_set: ElectricCurrent,
pd_i: ElectricCurrent,
pd_pwr: Power,
term_status: Impedance,
}
pub struct LdDrive {
ctrl: LdCtrl,
settings: Settings,
}
impl LdDrive {
pub fn new(
current_source: LdCtrl,
pins_adc: ADC3,
tim2: CounterUs<TIM2>,
phy: ld_pwr_exc_protector::LdPwrExcProtectorPhy,
) -> Self {
LdPwrExcProtector::setup(pins_adc, phy);
LdCurrentOutCtrlTimer::setup(tim2);
LdDrive {
ctrl: current_source,
settings: Settings::default(),
}
}
pub fn setup(&mut self) {
LdPwrExcProtector::pwr_off();
self.ctrl.set_i(
ElectricCurrent::new::<milliampere>(0.0),
Settings::LD_DRIVE_TRANSIMPEDANCE,
Settings::DAC_OUT_V_MAX,
);
self.set_ld_drive_current_limit(Settings::LD_CURRENT_MAX);
LdCurrentOutCtrlTimer::reset();
self.ld_short();
}
pub fn set_ld_drive_current_limit(&mut self, i_limit: ElectricCurrent) {
self.settings.ld_drive_current_limit = i_limit.min(Settings::LD_CURRENT_MAX);
}
pub fn ld_short(&mut self) {
self.ctrl.ld_short_enable();
self.settings.ld_terms_short = true;
}
pub fn ld_open(&mut self) {
self.ctrl.ld_short_disable();
self.settings.ld_terms_short = false;
}
pub fn power_up(&mut self) {
let prev_i_set = self.settings.ld_drive_current;
LdCurrentOutCtrlTimer::reset();
let _ = self.ctrl.set_i(
ElectricCurrent::new::<milliampere>(0.0),
Settings::LD_DRIVE_TRANSIMPEDANCE,
Settings::DAC_OUT_V_MAX,
);
// Wait for the DAC to reset its voltage back to 0V
sleep(35);
LdPwrExcProtector::pwr_on_and_arm_protection();
// Wait for LD Power Supply to start up before driving current to laser diode
sleep(30);
self.ld_set_i(prev_i_set);
self.settings.pwr_on = true;
}
pub fn power_down(&mut self) {
LdPwrExcProtector::pwr_off();
self.settings.pwr_on = false;
}
pub fn get_pd_i(&mut self) -> ElectricCurrent {
LdPwrExcProtector::get_status().v * Settings::PD_MON_TRANSCONDUCTANCE
}
pub fn get_pd_pwr(&mut self) -> Power {
self.settings
.pd_mon_params
.get_ld_pwr_from_ld_i(LdPwrExcProtector::get_status().v * Settings::PD_MON_TRANSCONDUCTANCE)
}
pub fn ld_set_i(&mut self, i: ElectricCurrent) {
self.settings.ld_drive_current = i.min(self.settings.ld_drive_current_limit);
LdCurrentOutCtrlTimer::set_target_i_and_listen_irq(self.settings.ld_drive_current, self.ctrl.get_i_set());
}
pub fn poll_and_update_output_current(&mut self) -> ElectricCurrent {
match LdCurrentOutCtrlTimer::get_irq_status() {
Some(i_set) => {
let i_set = self
.ctrl
.set_i(i_set, Settings::LD_DRIVE_TRANSIMPEDANCE, Settings::DAC_OUT_V_MAX);
LdCurrentOutCtrlTimer::clear_alarm_and_resume_listening();
i_set
}
None => ElectricCurrent::new::<ampere>(0.0),
}
}
// Set the calibrated VDDA value obtained from ADC1 calibration
pub fn set_pd_mon_calibrated_vdda(&mut self, val_cal: u32) {
LdPwrExcProtector::set_calibrated_vdda(val_cal)
}
pub fn pd_mon_clear_alarm(&mut self) {
LdPwrExcProtector::clear_alarm_status();
}
pub fn set_pd_responsitivity(&mut self, responsitivity: pd_mon_params::ResponsitivityUnit) {
self.settings.pd_mon_params.set(responsitivity);
}
pub fn set_pd_dark_current(&mut self, i_dark: ElectricCurrent) {
self.settings.pd_mon_params.set_i_dark(i_dark);
}
pub fn set_ld_power_limit(&mut self, pwr_limit: Power) {
LdPwrExcProtector::set_trigger_threshold_v(
self.settings.pd_mon_params.get_ld_i_from_ld_pwr(pwr_limit) / Settings::PD_MON_TRANSCONDUCTANCE,
);
self.settings.ld_pwr_limit = pwr_limit;
}
pub fn set_pd_i_limit(&mut self, i: ElectricCurrent) {
LdPwrExcProtector::set_trigger_threshold_v(i / Settings::PD_MON_TRANSCONDUCTANCE);
}
pub fn set_default_pwr_on(&mut self, pwr_on: bool) {
self.settings.default_pwr_on = pwr_on;
}
pub fn get_term_status(&mut self) -> Impedance {
self.ctrl.get_lf_mod_in_impedance()
}
pub fn get_status_report(&mut self) -> StatusReport {
let ld_i_set = if LdPwrExcProtector::get_status().pwr_engaged {
self.ctrl.get_i_set()
} else {
ElectricCurrent::new::<ampere>(0.0)
};
StatusReport {
pwr_on: LdPwrExcProtector::get_status().pwr_engaged,
pwr_excursion: LdPwrExcProtector::get_status().pwr_excursion,
ld_i_set: ld_i_set,
pd_i: self.get_pd_i(),
pd_pwr: self.get_pd_pwr(),
term_status: self.get_term_status(),
}
}
pub fn get_settings_summary(&mut self) -> LdSettingsSummary {
let settings = self.settings;
LdSettingsSummary {
default_pwr_on: self.settings.default_pwr_on,
ld_drive_current: LdSettingsSummaryField {
value: settings.ld_drive_current,
max: Settings::LD_CURRENT_MAX,
},
ld_drive_current_limit: LdSettingsSummaryField {
value: settings.ld_drive_current_limit,
max: Settings::LD_CURRENT_MAX,
},
pd_mon_params: settings.pd_mon_params,
ld_pwr_limit: settings.ld_pwr_limit,
ld_terms_short: settings.ld_terms_short,
}
}
pub fn load_settings_from_summary(&mut self, settings: LdSettingsSummary) {
self.power_down();
self.settings.ld_drive_current = settings.ld_drive_current.value;
self.settings.ld_drive_current_limit = settings.ld_drive_current_limit.value;
self.settings.pd_mon_params = settings.pd_mon_params;
self.settings.ld_pwr_limit = settings.ld_pwr_limit;
self.settings.default_pwr_on = settings.default_pwr_on;
if self.settings.ld_terms_short {
self.ld_short();
} else {
self.ld_open();
}
if settings.default_pwr_on {
self.power_up();
} else {
self.power_down();
}
}
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct LdSettingsSummary {
default_pwr_on: bool,
ld_drive_current: LdSettingsSummaryField<ElectricCurrent>,
ld_drive_current_limit: LdSettingsSummaryField<ElectricCurrent>,
pd_mon_params: pd_mon_params::Parameters,
ld_pwr_limit: Power,
ld_terms_short: bool,
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct LdSettingsSummaryField<T> {
value: T,
max: T,
}

View File

@ -1,97 +0,0 @@
use serde::{Deserialize, Serialize};
use stm32f4xx_hal::{gpio::{gpioa::*, gpiob::*, gpiod::*, Input, Output, PushPull},
hal::{digital::{InputPin, OutputPin},
spi::SpiBus},
pac::SPI2,
spi::Spi};
use uom::si::{electric_current::ampere,
f32::{ElectricCurrent, ElectricPotential},
ratio::ratio};
use crate::laser_diode::{laser_diode::TransimpedanceUnit,
max5719::{self, Dac}};
#[derive(Deserialize, Serialize, Debug, Clone, Copy)]
pub enum Impedance {
Is50Ohm,
Not50Ohm,
}
pub trait ChannelPins {
type CurrentSourceShort: OutputPin;
type TerminationStatus: InputPin;
type Max5719Load: OutputPin;
type Max5719Cs: OutputPin;
type Max5719Spi: SpiBus<u8>;
}
pub struct LdCtrlPhy<C: ChannelPins> {
pub dac: Dac<C::Max5719Spi, C::Max5719Cs, C::Max5719Load>,
pub current_source_short_pin: C::CurrentSourceShort,
pub termination_status_pin: C::TerminationStatus,
}
pub struct Channel0;
impl ChannelPins for Channel0 {
type CurrentSourceShort = PA4<Output<PushPull>>;
type TerminationStatus = PD7<Input>;
type Max5719Load = DacLoad;
type Max5719Cs = DacCs;
type Max5719Spi = DacSpi;
}
type DacSpi = Spi<SPI2>;
type DacCs = PD8<Output<PushPull>>;
type DacLoad = PB14<Output<PushPull>>;
pub struct LdCtrl {
pub phy: LdCtrlPhy<Channel0>,
i_set: ElectricCurrent,
}
impl LdCtrl {
pub fn new(phy_ch0: LdCtrlPhy<Channel0>) -> Self {
LdCtrl {
phy: phy_ch0,
i_set: ElectricCurrent::new::<ampere>(0.0),
}
}
// LD Terminals are shorted together
pub fn ld_short_enable(&mut self) {
self.phy.current_source_short_pin.set_low();
}
// LD Current flows from anode to cathode
pub fn ld_short_disable(&mut self) {
self.phy.current_source_short_pin.set_high();
}
pub fn get_lf_mod_in_impedance(&mut self) -> Impedance {
if self.phy.termination_status_pin.is_high() {
Impedance::Is50Ohm
} else {
Impedance::Not50Ohm
}
}
pub fn set_dac(&mut self, voltage: ElectricPotential, dac_out_v_max: ElectricPotential) -> ElectricPotential {
let value = ((voltage / dac_out_v_max).get::<ratio>() * (max5719::MAX_VALUE as f32)) as u32;
self.phy.dac.set(value).unwrap();
value as f32 * dac_out_v_max / max5719::MAX_VALUE as f32
}
pub fn set_i(
&mut self,
current: ElectricCurrent,
transimpedance: TransimpedanceUnit,
dac_out_v_max: ElectricPotential,
) -> ElectricCurrent {
self.i_set = self.set_dac(current * transimpedance, dac_out_v_max) / transimpedance;
self.i_set
}
pub fn get_i_set(&mut self) -> ElectricCurrent {
self.i_set
}
}

View File

@ -1,128 +0,0 @@
use core::marker::PhantomData;
use fugit::{KilohertzU32, TimerDurationU32};
use log::debug;
use stm32f4xx_hal::{pac::{interrupt, Interrupt, TIM2},
timer::{Counter, Event},
Listen};
use uom::si::{electric_current::ampere, f32::ElectricCurrent};
pub struct LdCurrentOutCtrlTimer {
target_i: ElectricCurrent,
now_i: ElectricCurrent,
timer: Counter<TIM2, FREQ>,
timeout: bool,
}
static mut LD_CURRENT_OUT_CTRL_TIMER: Option<LdCurrentOutCtrlTimer> = None;
pub const FREQ: u32 = 1000000;
/// This timer notifies the main loop to set the correct output current so that ld output current can ramp up/down slowly.
/// The current output slope is guaranteed to be larger but not necessarily equal to than the preset value.
impl LdCurrentOutCtrlTimer {
const TIME_STEP_MS: TimerDurationU32<FREQ> = TimerDurationU32::from_rate(KilohertzU32::from_raw(1));
const STEP_SIZE: ElectricCurrent = ElectricCurrent {
dimension: PhantomData,
units: PhantomData,
value: 0.0001,
};
pub fn setup(mut tim2: Counter<TIM2, FREQ>) {
tim2.start(LdCurrentOutCtrlTimer::TIME_STEP_MS).unwrap();
unsafe {
cortex_m::peripheral::NVIC::unmask(Interrupt::TIM2);
}
unsafe {
LD_CURRENT_OUT_CTRL_TIMER = Some(LdCurrentOutCtrlTimer {
target_i: ElectricCurrent::new::<ampere>(0.0),
now_i: ElectricCurrent::new::<ampere>(0.0),
timer: tim2,
timeout: false,
});
}
}
fn get() -> Option<&'static mut Self> {
unsafe { LD_CURRENT_OUT_CTRL_TIMER.as_mut() }
}
pub fn reset() {
if let Some(ref mut ld_current_out_ctrl_timer) = LdCurrentOutCtrlTimer::get() {
ld_current_out_ctrl_timer.target_i = ElectricCurrent::new::<ampere>(0.0);
ld_current_out_ctrl_timer.now_i = ElectricCurrent::new::<ampere>(0.0);
ld_current_out_ctrl_timer.timeout = false;
ld_current_out_ctrl_timer.timer.unlisten(Event::Update);
}
}
pub fn set_target_i_and_listen_irq(target: ElectricCurrent, now: ElectricCurrent) {
if let Some(ref mut ld_current_out_ctrl_timer) = LdCurrentOutCtrlTimer::get() {
cortex_m::interrupt::free(|_| {
ld_current_out_ctrl_timer.target_i = target;
ld_current_out_ctrl_timer.now_i = now;
ld_current_out_ctrl_timer.timer.listen(Event::Update);
})
}
}
pub fn set_alarm() {
if let Some(ref mut ld_current_out_ctrl_timer) = LdCurrentOutCtrlTimer::get() {
ld_current_out_ctrl_timer.timeout = true;
}
}
pub fn clear_alarm_and_resume_listening() {
if let Some(ref mut ld_current_out_ctrl_timer) = LdCurrentOutCtrlTimer::get() {
ld_current_out_ctrl_timer.timeout = false;
ld_current_out_ctrl_timer.timer.listen(Event::Update);
}
}
pub fn get_irq_status() -> Option<ElectricCurrent> {
if let Some(ref mut ld_current_out_ctrl_timer) = LdCurrentOutCtrlTimer::get() {
if ld_current_out_ctrl_timer.timeout {
return Some(ld_current_out_ctrl_timer.now_i);
}
}
None
}
pub fn clear_irq_flag() {
if let Some(ref mut ld_current_out_ctrl_timer) = LdCurrentOutCtrlTimer::get() {
match ld_current_out_ctrl_timer.timer.wait() {
Ok(_) => {}
Err(_) => {
debug!("LD CTRL TIMER Interrupt is not present when clear_irq_flag() is called")
}
}
}
}
pub fn update_next_i_set() -> bool {
if let Some(ref mut ld_current_out_ctrl_timer) = LdCurrentOutCtrlTimer::get() {
let update = ld_current_out_ctrl_timer.now_i != ld_current_out_ctrl_timer.target_i;
if ld_current_out_ctrl_timer.target_i > ld_current_out_ctrl_timer.now_i {
ld_current_out_ctrl_timer.now_i = (ld_current_out_ctrl_timer.now_i + LdCurrentOutCtrlTimer::STEP_SIZE)
.min(ld_current_out_ctrl_timer.target_i);
} else {
ld_current_out_ctrl_timer.now_i = (ld_current_out_ctrl_timer.now_i - LdCurrentOutCtrlTimer::STEP_SIZE)
.max(ld_current_out_ctrl_timer.target_i);
}
return update;
}
false
}
pub fn stop_listening() {
if let Some(ref mut ld_current_out_ctrl_timer) = LdCurrentOutCtrlTimer::get() {
ld_current_out_ctrl_timer.timer.unlisten(Event::Update);
}
}
}
#[interrupt]
fn TIM2() {
if LdCurrentOutCtrlTimer::update_next_i_set() {
LdCurrentOutCtrlTimer::set_alarm();
}
LdCurrentOutCtrlTimer::stop_listening();
LdCurrentOutCtrlTimer::clear_irq_flag();
}

View File

@ -1,234 +0,0 @@
use stm32f4xx_hal::{gpio::{gpioa::PA3, gpiod::PD9, Analog, Output, PushPull},
interrupt, pac,
pac::{ADC3, NVIC},
rcc::Enable};
use uom::si::{electric_potential::millivolt, f32::ElectricPotential, ratio::ratio};
// 12 bit Resolution
const MAX_SAMPLE: u16 = 4095;
pub type LdPwrEnPinType = PD9<Output<PushPull>>;
pub type PdMonAdcPinType = PA3<Analog>;
const PD_MON_ADC_CH_ID: u8 = 0x03;
static mut LD_PWR_EXC_PROTECTOR: Option<LdPwrExcProtector> = None;
pub struct LdPwrExcProtectorPhy {
// To make sure Pd Mon Pin is configured to Analog mode
pub _pd_mon_ch0: PdMonAdcPinType,
pub pwr_en_ch0: LdPwrEnPinType,
}
#[derive(Clone)]
pub struct Status {
pub pwr_excursion: bool,
pub v_tripped: ElectricPotential,
pub pwr_engaged: bool,
pub v: ElectricPotential,
}
impl Default for Status {
fn default() -> Self {
Status {
pwr_excursion: false,
v_tripped: ElectricPotential::new::<millivolt>(0.0),
pwr_engaged: false,
v: ElectricPotential::new::<millivolt>(0.0),
}
}
}
pub struct LdPwrExcProtector {
pac: ADC3,
phy: LdPwrExcProtectorPhy,
alarm_status: Status,
calibrated_vdda: u32,
}
impl LdPwrExcProtector {
/// ADC Analog Watchdog is configured to guard a single regular Adc channel on Pd Mon Pin.
/// ADC is configured to start continuous conversion without using DMA immediately.
/// Interrupt is disabled by default.
pub fn setup(pac_adc: ADC3, mut phy: LdPwrExcProtectorPhy) {
unsafe {
// All ADCs share the same reset interface.
// NOTE(unsafe) this reference will only be used for atomic writes with no side effects.
let rcc = &(*pac::RCC::ptr());
// Enable the ADC3 Clock
pac::ADC3::enable(rcc);
// Enable ADC Interrupt
NVIC::unmask(interrupt::ADC);
}
pac_adc.cr1.reset();
pac_adc.cr2.reset();
pac_adc.sqr1.reset();
pac_adc.sqr2.reset();
pac_adc.sqr3.reset();
pac_adc.cr1.write(|w| {
w
// 12 Bit Resolution
.res()
.twelve_bit()
// Set Analog Watchdog to guard Single Regular Channel
.awden()
.enabled()
.awdsgl()
.single_channel()
.jawden()
.disabled()
// Disable Analog Watchdog Interrupt
.awdie()
.disabled()
// Set Analog Watchdog to monitor Pd Mon Pin
.awdch()
.variant(PD_MON_ADC_CH_ID)
});
pac_adc.cr2.write(|w| {
w
// Continous Conversion Mode
.cont()
.set_bit()
// Power up ADC
.adon()
.set_bit()
// Set data alignment to the right
.align()
.right()
// End of conversion selection: Each Sequence
.eocs()
.each_sequence()
.exten()
.disabled()
.extsel()
.tim1cc1()
});
// Set the Conversion Sequence to include Pd Mon Pin
pac_adc.sqr3.write(|w| w.sq1().variant(PD_MON_ADC_CH_ID));
// Set all sampling channels to have fastest sampling interval
pac_adc.smpr1.reset();
pac_adc.smpr2.reset();
// Set the higher threshold to be max value initially
pac_adc.htr.write(|w| w.ht().variant(MAX_SAMPLE));
// Set the lower threshold to be min value initially
pac_adc.ltr.write(|w| w.lt().variant(0));
// SWStart should only be set when ADON = 1. Otherwise no conversion is launched.
pac_adc.cr2.modify(|_, w| w.swstart().set_bit());
phy.pwr_en_ch0.set_low();
unsafe {
LD_PWR_EXC_PROTECTOR = Some(LdPwrExcProtector {
pac: pac_adc,
phy: phy,
alarm_status: Status::default(),
calibrated_vdda: 3300,
});
}
}
fn get() -> Option<&'static mut Self> {
unsafe { LD_PWR_EXC_PROTECTOR.as_mut() }
}
fn convert_sample_to_volt(sample: u16) -> ElectricPotential {
if let Some(ref mut wdg) = LdPwrExcProtector::get() {
return ElectricPotential::new::<millivolt>(
((u32::from(sample) * wdg.calibrated_vdda) / u32::from(MAX_SAMPLE)) as f32,
);
}
ElectricPotential::new::<millivolt>(0.0)
}
pub fn set_trigger_threshold_v(htr: ElectricPotential) {
if let Some(ref mut wdg) = LdPwrExcProtector::get() {
let code: u32 = ((htr / (ElectricPotential::new::<millivolt>(wdg.calibrated_vdda as f32))).get::<ratio>()
* (MAX_SAMPLE as f32)) as u32;
wdg.pac.htr.write(|w| unsafe { w.bits(code) });
}
}
pub fn set_calibrated_vdda(val: u32) {
if let Some(ref mut wdg) = LdPwrExcProtector::get() {
wdg.calibrated_vdda = val;
}
}
pub fn get_status() -> Status {
if let Some(ref mut wdg) = LdPwrExcProtector::get() {
wdg.alarm_status.v = LdPwrExcProtector::convert_sample_to_volt(wdg.pac.dr.read().data().bits());
return wdg.alarm_status.clone();
}
Status::default()
}
pub fn pwr_on_and_arm_protection() {
if let Some(ref mut wdg) = LdPwrExcProtector::get() {
wdg.alarm_status = Status::default();
LdPwrExcProtector::pwr_on();
// Interrupt should be enabled after power on to tackle the following edge case:
// Pd_Mon pin voltage has already exceed threshold before LD Power is on.
LdPwrExcProtector::enable_watchdog_interrupt();
}
}
pub fn clear_alarm_status() {
if let Some(ref mut wdg) = LdPwrExcProtector::get() {
wdg.alarm_status.pwr_excursion = false;
wdg.alarm_status.v_tripped = ElectricPotential::new::<millivolt>(0.0);
}
}
fn enable_watchdog_interrupt() {
if let Some(ref mut wdg) = LdPwrExcProtector::get() {
wdg.pac.cr1.modify(|_, w| w.awdie().set_bit());
}
}
fn disable_watchdog_interrupt() {
if let Some(ref mut wdg) = LdPwrExcProtector::get() {
wdg.pac.cr1.modify(|_, w| w.awdie().clear_bit());
}
}
fn clear_interrupt_bit() {
if let Some(ref mut wdg) = LdPwrExcProtector::get() {
wdg.pac.sr.modify(|_, w| w.awd().clear_bit());
}
}
fn pwr_on() {
if let Some(ref mut wdg) = LdPwrExcProtector::get() {
wdg.alarm_status.pwr_engaged = true;
wdg.phy.pwr_en_ch0.set_high()
}
}
pub fn pwr_off() {
if let Some(ref mut wdg) = LdPwrExcProtector::get() {
wdg.alarm_status.pwr_engaged = false;
wdg.phy.pwr_en_ch0.set_low()
}
}
fn pwr_excursion_handler() {
if let Some(ref mut wdg) = LdPwrExcProtector::get() {
let sample = wdg.pac.dr.read().data().bits();
LdPwrExcProtector::pwr_off();
wdg.alarm_status.pwr_excursion = true;
wdg.alarm_status.v_tripped = LdPwrExcProtector::convert_sample_to_volt(sample);
}
}
}
#[interrupt]
fn ADC() {
cortex_m::interrupt::free(|_| {
LdPwrExcProtector::pwr_excursion_handler();
// Disable interrupt to avoid getting stuck in infinite loop
LdPwrExcProtector::disable_watchdog_interrupt();
LdPwrExcProtector::clear_interrupt_bit();
})
}

View File

@ -1,53 +0,0 @@
use fugit::MegahertzU32;
use stm32f4xx_hal::{hal::{digital::OutputPin, spi::SpiBus},
spi};
use crate::device::sys_timer::sleep;
pub const SPI_MODE: spi::Mode = spi::Mode {
polarity: spi::Polarity::IdleLow,
phase: spi::Phase::CaptureOnFirstTransition,
};
// MAX5719 Max Frequency: 50MHz | SPI2 Max Frequency: 21MHz
pub const SPI_CLOCK_MHZ: MegahertzU32 = MegahertzU32::from_raw(21);
pub const MAX_VALUE: u32 = 0xFFFFF;
pub struct Dac<SPI: SpiBus<u8>, S1: OutputPin, S2: OutputPin> {
spi: SPI,
cs_n: S1,
load_n: S2,
}
impl<SPI: SpiBus<u8>, S1: OutputPin, S2: OutputPin> Dac<SPI, S1, S2> {
pub fn new(spi: SPI, mut cs_n: S1, mut load_n: S2) -> Self {
let _ = cs_n.set_high();
let _ = load_n.set_high();
Dac { spi, cs_n, load_n }
}
fn write(&mut self, buf: &mut [u8]) -> Result<(), SPI::Error> {
let _ = self.cs_n.set_low();
self.spi.write(buf)?;
let _ = self.cs_n.set_high();
sleep(1);
// must be high for >= 20 ns
let _ = self.load_n.set_low();
// must be low for >= 20 ns
sleep(1);
let _ = self.load_n.set_high();
Ok(())
}
pub fn set(&mut self, value: u32) -> Result<u32, SPI::Error> {
let value = value.min(MAX_VALUE) << 4;
let mut buf = [
((value >> 16) & 0xFF) as u8,
((value >> 8) & 0xFF) as u8,
((value >> 0) & 0xFF) as u8,
];
self.write(&mut buf)?;
Ok(value)
}
}

View File

@ -1,6 +1 @@
pub mod laser_diode;
pub mod ld_ctrl;
pub mod ld_current_out_ctrl_timer;
pub mod ld_pwr_exc_protector;
pub mod max5719;
pub mod pd_mon_params;
pub mod current_sources;

View File

@ -1,50 +0,0 @@
use core::{f32::NAN, marker::PhantomData};
use miniconf::Tree;
use serde::{Deserialize, Serialize};
use uom::{si::{electric_current::microampere,
f32::{ElectricCurrent, Power},
Quantity, ISQ, SI},
typenum::*};
// Ampere / Watt
pub type ResponsitivityUnit = Quantity<ISQ<N2, N1, P3, P1, Z0, Z0, Z0>, SI<f32>, f32>;
#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Tree)]
pub struct Parameters {
responsitivity: ResponsitivityUnit,
i_dark: ElectricCurrent,
}
impl Parameters {
pub fn get_ld_pwr_from_ld_i(&self, i: ElectricCurrent) -> Power {
let ld_power = (i - self.i_dark) / self.responsitivity;
ld_power
}
pub fn get_ld_i_from_ld_pwr(&self, pwr: Power) -> ElectricCurrent {
let ld_i = pwr * self.responsitivity + self.i_dark;
ld_i
}
pub fn set(&mut self, responsitivity: ResponsitivityUnit) {
self.responsitivity = responsitivity;
}
pub fn set_i_dark(&mut self, i_dark: ElectricCurrent) {
self.i_dark = i_dark;
}
}
impl Default for Parameters {
fn default() -> Self {
Parameters {
responsitivity: ResponsitivityUnit {
dimension: PhantomData,
units: PhantomData,
value: NAN,
},
i_dark: ElectricCurrent::new::<microampere>(0.0),
}
}
}

View File

@ -1,276 +1,88 @@
#![cfg_attr(not(test), no_main)]
#![cfg_attr(not(test), no_std)]
#![no_main]
#![no_std]
use cortex_m_rt::entry;
use log::{debug, info};
use stm32f4xx_hal::pac::{CorePeripherals, Peripherals};
use rtic::app;
mod device;
mod laser_diode;
mod net;
mod thermostat;
mod network;
use core::ptr::addr_of_mut;
use device::{boot::bootup, log_setup, sys_timer};
use serde::{Deserialize, Serialize};
use stm32f4xx_hal::pac::SCB;
// If RTT is used, print panic info through RTT
#[cfg(all(feature = "RTT", not(test)))]
#[cfg(feature = "RTT")]
use {core::panic::PanicInfo, rtt_target::rprintln};
use crate::net::net::IpSettings;
#[cfg(all(feature = "RTT", not(test)))]
#[cfg(feature = "RTT")]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
rprintln!("{}", info);
loop {}
}
// Otherwise use panic halt
#[cfg(all(not(feature = "RTT"), not(test)))]
#[cfg(not(feature = "RTT"))]
use panic_halt as _;
static mut ETH_DATA_BUFFER: [u8; 1024] = [0; 1024];
// #[entry]
#[app(device = stm32f4xx_hal::pac, peripherals = true, dispatchers = [TIM8_CC, TIM8_BRK_TIM12])]
mod app {
#[derive(Deserialize, Serialize, Clone, Copy, Debug)]
pub struct DeviceSettings {
ip_settings: IpSettings,
}
use crate::device::{boot::bootup, log_setup};
use crate::network::network;
use fugit::ExtU32;
use log::info;
use stm32f4xx_hal::watchdog::IndependentWatchdog;
use systick_monotonic::Systick;
const CONFIG_KEY: [&str; 3] = ["Device", "Laser_0", "Thermostat_0"];
#[monotonic(binds = SysTick, default = true)]
type SystickTimer = Systick<1000>;
#[derive(Default)]
pub enum State {
#[default]
LoadFlashSettings,
MainLoop,
SaveLdThermostatSettings,
SaveDeviceSettings,
PrepareForHardReset,
HardReset,
}
#[shared]
struct Shared {}
#[cfg(not(test))]
#[entry]
fn main() -> ! {
log_setup::init_log();
info!("Kirdy init");
#[local]
struct Local {
wd: IndependentWatchdog,
// server_storage: network::network::ServerStorage::new(),
server_handle: network::ServerHandle,
}
let core_perif = CorePeripherals::take().unwrap();
let perif = Peripherals::take().unwrap();
#[init( local = [server_storage: network::ServerStorage = network::ServerStorage::new()] )]
fn init(cx: init::Context) -> (Shared, Local, init::Monotonics) {
log_setup::init_log();
info!("Kirdy init");
let (mut wd, mut flash_store, mut laser, mut thermostat) = bootup(core_perif, perif);
let core_perif = cx.core;
let perif = cx.device;
let server_storage = cx.local.server_storage;
let (wd, systick, server_handle) = bootup(core_perif, perif, server_storage);
let mut device_settings = DeviceSettings {
ip_settings: IpSettings::default(),
};
wd_feed::spawn().unwrap();
// server_poll::spawn().unwrap();
(
Shared {},
Local { wd, server_handle },
init::Monotonics(systick),
)
}
let mut active_report: [bool; net::net::NUM_OF_SOCKETS] = [false; net::net::NUM_OF_SOCKETS];
let mut state = State::default();
let eth_data_buffer = unsafe { addr_of_mut!(ETH_DATA_BUFFER).as_mut().unwrap() };
loop {
#[task(priority = 5, local = [wd])]
fn wd_feed(cx: wd_feed::Context) {
let start = monotonics::now();
let wd = cx.local.wd;
// info!("feed wd");
wd.feed();
wd_feed::spawn_at(start + 10_u32.millis()).unwrap();
}
if net::net::eth_poll_link_status_and_update_link_speed() {
active_report = [false; net::net::NUM_OF_SOCKETS];
}
#[task(binds = ETH, priority = 2, local = [server_handle, data: [u8; 512] = [0u8; 512]])]
fn server_poll(cx: server_poll::Context) {
let data = cx.local.data;
let server_handle = cx.local.server_handle;
server_handle.poll(data);
// server_poll::spawn_at(start + 10_u32.millis()).unwrap();
}
match state {
State::LoadFlashSettings => {
// State Transition
state = State::MainLoop;
wd.feed();
let device_settings_flash: DeviceSettings;
match flash_store.read_value(CONFIG_KEY[0]) {
Ok(Some(config)) => {
device_settings_flash = config;
debug!("Found Device Settings");
}
Ok(None) => {
debug!("Flash does not have Device Settings");
continue;
}
Err(e) => {
debug!("Cannot Store Flash: {:?}", e);
continue;
}
}
wd.feed();
let laser_settings: laser_diode::laser_diode::LdSettingsSummary;
match flash_store.read_value(CONFIG_KEY[1]) {
Ok(Some(config)) => {
laser_settings = config;
debug!("Found Laser Diode Settings");
}
Ok(None) => {
debug!("Does not have laser diode Settings");
continue;
}
Err(e) => {
debug!("Cannot Store Flash: {:?}", e);
continue;
}
}
wd.feed();
let thermostat_settings: thermostat::thermostat::ThermostatSettingsSummary;
match flash_store.read_value(CONFIG_KEY[2]) {
Ok(Some(config)) => {
thermostat_settings = config;
debug!("Found Thermostat Settings");
}
Ok(None) => {
debug!("Does not have thermostat Settings");
continue;
}
Err(e) => {
debug!("Cannot Store Flash: {:?}", e);
continue;
}
}
wd.feed();
device_settings = device_settings_flash;
thermostat.load_settings_from_summary(thermostat_settings);
laser.load_settings_from_summary(laser_settings);
}
State::MainLoop => {
laser.poll_and_update_output_current();
net::net::eth_update_iface_poll_timer();
if thermostat.poll_adc() {
thermostat.update_pid();
if thermostat.get_temp_mon_status().over_temp_alarm {
laser.power_down();
thermostat.set_pid_engaged(false);
thermostat.power_down();
}
net::net::for_each(|mut socket, id| {
if net::net::eth_is_socket_active(socket) && net::net::eth_is_socket_connected(socket) {
if active_report[id] {
if net::net::eth_can_sock_send(socket) {
net::cmd_handler::send_status_report(
eth_data_buffer,
&mut laser,
&mut thermostat,
&mut socket,
);
}
}
} else {
active_report[id] = false;
}
});
thermostat.start_tec_readings_conversion();
}
net::net::for_each(|mut socket, id| {
if net::net::eth_is_socket_active(socket) && net::net::eth_is_socket_connected(socket) {
if net::net::eth_can_sock_recv(socket) && net::net::eth_can_sock_send(socket) {
let bytes = net::net::eth_recv(eth_data_buffer, socket);
if bytes != 0 {
info!("Ts: {:?}", sys_timer::now());
debug!("Number of bytes recv: {:?}", bytes);
// State Transition
net::cmd_handler::execute_cmd(
eth_data_buffer,
bytes,
&mut socket,
&mut laser,
&mut thermostat,
&mut state,
&mut device_settings,
&mut active_report[id],
);
}
}
}
})
}
State::SaveLdThermostatSettings => {
// State Transition
state = State::MainLoop;
wd.feed();
let mut store_value_buf = [0u8; 1024];
match flash_store.write_value(CONFIG_KEY[1], &laser.get_settings_summary(), &mut store_value_buf) {
Ok(()) => {
debug!("Laser Diode Settings is stored in flash");
}
Err(e) => {
debug!("Cannot Store Flash: {:?}", e);
}
}
wd.feed();
match flash_store.write_value(CONFIG_KEY[2], &thermostat.get_settings_summary(), &mut store_value_buf) {
Ok(()) => {
debug!("Thermostat Settings is stored in flash");
}
Err(e) => {
debug!("Cannot Store Flash: {:?}", e);
}
}
}
State::SaveDeviceSettings => {
// State Transition
state = State::MainLoop;
wd.feed();
let mut store_value_buf = [0u8; 1024];
match flash_store.write_value(CONFIG_KEY[0], &device_settings, &mut store_value_buf) {
Ok(()) => {
debug!("Device Settings is stored in flash");
}
Err(e) => {
debug!("Cannot Store Flash: {:?}", e);
}
}
}
State::PrepareForHardReset => {
// State Transition
state = State::HardReset;
wd.feed();
laser.power_down();
thermostat.power_down();
net::net::for_each(|mut socket, _| {
if net::net::eth_is_socket_active(socket) {
net::cmd_handler::send_response(
eth_data_buffer,
net::cmd_handler::ResponseEnum::HardReset,
None,
&mut socket,
);
}
});
}
State::HardReset => {
wd.feed();
laser.power_down();
thermostat.power_down();
let mut any_socket_alive = false;
net::net::for_each(|socket, _| {
if net::net::eth_is_socket_active(socket) {
net::net::eth_close_socket(socket);
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();
}
}
#[idle]
fn idle(_: idle::Context) -> ! {
loop {
cortex_m::asm::nop();
}
}
}

View File

@ -1,783 +0,0 @@
use core::{fmt::Debug, marker::PhantomData};
use log::{debug, info};
use miniconf::{JsonCoreSlash, Tree};
use serde::{Deserialize, Serialize};
use smoltcp::iface::SocketHandle;
use uom::si::{electric_current::{ampere, ElectricCurrent},
electric_potential::{volt, ElectricPotential},
electrical_resistance::{ohm, ElectricalResistance},
power::{watt, Power},
thermodynamic_temperature::{degree_celsius, ThermodynamicTemperature}};
use crate::{device::{dfu, sys_timer},
laser_diode::{laser_diode::{LdDrive, LdSettingsSummary, StatusReport as LdStatusReport},
pd_mon_params::ResponsitivityUnit},
net::net,
thermostat::{ad7172::FilterType,
pid_state::PidSettings::*,
thermostat::{StatusReport as TecStatusReport, TempAdcFilter, Thermostat,
ThermostatSettingsSummary}},
DeviceSettings, IpSettings, State};
#[derive(Deserialize, Serialize, Copy, Clone, Default, Debug)]
pub enum ResponseEnum {
#[default]
Reserved,
Settings,
Report,
Acknowledge,
InvalidDatatype,
InvalidCmd,
HardReset,
}
pub type MsgType = Option<&'static str>;
#[derive(Deserialize, Serialize, Copy, Clone, Debug)]
pub struct Response<'a> {
msg_type: ResponseEnum,
msg: Option<&'a str>,
}
impl Default for Response<'static> {
fn default() -> Self {
Response {
msg_type: ResponseEnum::Reserved,
msg: None,
}
}
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct ResponseObj<'a> {
#[serde(borrow)]
json: Response<'a>,
}
#[derive(Deserialize, Serialize, Copy, Clone, Default, Debug)]
enum DeviceCmd {
#[default]
Reserved,
SetIPSettings,
SetActiveReportMode,
GetStatusReport,
GetSettingsSummary,
Dfu,
SaveFlashSettings,
LoadFlashSettings,
HardReset,
}
#[derive(Deserialize, Serialize, Copy, Clone, Default, Debug)]
enum LdCmdEnum {
#[default]
Reserved,
// LD Drive Related
SetDefaultPowerOn,
PowerUp,
PowerDown,
LdTermsShort,
LdTermsOpen,
SetI,
SetISoftLimit,
// PD Mon Related
SetPdResponsitivity,
SetPdDarkCurrent,
SetLdPwrLimit,
ClearAlarm,
}
#[derive(Deserialize, Serialize, Copy, Clone, Default, Debug)]
enum ThermostatCmdEnum {
#[default]
Reserved,
SetDefaultPowerOn,
PowerUp,
PowerDown,
// TEC
SetTecMaxV,
SetTecMaxIPos,
SetTecMaxINeg,
SetTecIOut, // Constant Current Mode
SetTemperatureSetpoint,
// PID
SetPidEngage,
SetPidDisEngage,
SetPidKp,
SetPidKi,
SetPidKd,
SetPidOutMin,
SetPidOutMax,
SetPidUpdateInterval, // Update Interval is set based on the sampling rate of ADC
// Temperature ADC
ConfigTempAdcFilter,
// TempMon
SetTempMonUpperLimit,
SetTempMonLowerLimit,
ClearAlarm,
// Steinhart-Hart Equation
SetShT0,
SetShR0,
SetShBeta,
}
const ERR_MSG_MISSING_DATA_F32: &str = "Required field \"data_f32\" does not exist";
const ERR_MSG_MISSING_DATA_BOOL: &str = "Required field \"bool\" does not exist";
const ERR_MSG_MISSING_IP_SETTINGS: &str = "Required field \"ip_settings\" does not exist";
const ERR_MSG_MISSING_TEMP_ADC_FILTER: &str = "Required field \"temp_adc_filter\" does not exist";
const ERR_MSG_MISSING_SINC5SINC1ODR: &str = "Required field \"sinc5sinc1odr\" does not exist";
const ERR_MSG_MISSING_SINC3ODR: &str = "Required field \"sinc3odr\" does not exist";
const ERR_MSG_MISSING_POSTFILTER: &str = "Required field \"PostFilter\" does not exist";
const ERR_MSG_MISSING_SINC3FINEODR: &str = "Required field \"sinc3fineodr\" does not exist";
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Default, Tree)]
pub struct CmdJsonObj {
laser_diode_cmd: Option<LdCmdEnum>,
thermostat_cmd: Option<ThermostatCmdEnum>,
device_cmd: Option<DeviceCmd>,
data_bool: Option<bool>,
data_f32: Option<f32>,
data_f64: Option<f64>,
temp_adc_filter: Option<TempAdcFilter>,
ip_settings: Option<IpSettings>,
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Default, Tree)]
pub struct Cmd {
json: CmdJsonObj,
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct StatusReport {
ts: u32,
msg_type: ResponseEnum,
laser: LdStatusReport,
thermostat: TecStatusReport,
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct StatusReportObj {
json: StatusReport,
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct SettingsSummary {
msg_type: ResponseEnum,
laser: LdSettingsSummary,
thermostat: ThermostatSettingsSummary,
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct SettingsSummaryObj {
json: SettingsSummary,
}
pub fn send_response(buffer: &mut [u8], msg_type: ResponseEnum, msg: MsgType, socket: &mut SocketHandle) {
let response = ResponseObj {
json: Response {
msg_type: msg_type,
msg: msg,
},
};
debug!("{:?}", response.json);
let mut num_bytes = response.get_json("/json", buffer).unwrap();
buffer[num_bytes] = b'\n';
num_bytes += 1;
net::eth_send(buffer, num_bytes, *socket);
}
pub fn send_settings_summary(
buffer: &mut [u8],
laser: &mut LdDrive,
thermostat: &mut Thermostat,
socket: &mut SocketHandle,
) {
let settings_summary = SettingsSummaryObj {
json: SettingsSummary {
msg_type: ResponseEnum::Settings,
laser: laser.get_settings_summary(),
thermostat: thermostat.get_settings_summary(),
},
};
let mut num_bytes = settings_summary.get_json("/json", buffer).unwrap();
buffer[num_bytes] = b'\n';
num_bytes += 1;
net::eth_send(buffer, num_bytes, *socket);
}
pub fn send_status_report(
buffer: &mut [u8],
laser: &mut LdDrive,
thermostat: &mut Thermostat,
socket: &mut SocketHandle,
) {
let status_report = StatusReportObj {
json: StatusReport {
ts: sys_timer::now(),
msg_type: ResponseEnum::Report,
laser: laser.get_status_report(),
thermostat: thermostat.get_status_report(),
},
};
let mut num_bytes = status_report.get_json("/json", buffer).unwrap();
buffer[num_bytes] = b'\n';
num_bytes += 1;
net::eth_send(buffer, num_bytes, *socket);
}
// Use a minimal struct for high speed cmd ctrl to reduce processing overhead
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Default, Tree)]
pub struct TecSetICmdJson {
tec_set_i: f32,
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Default, Tree)]
pub struct TecSetICmd {
json: TecSetICmdJson,
}
/// Miniconf is very slow in debug builds (~3-4ms of cmd decoding time).
/// Make sure kirdy's firmware is flashed with release builds.
/// The received message must contain only one json cmd. TCP client should set TCP_NODELAY or equivalent flag in its TCP Socket
/// Settings to avoid unwanted buffering on TX Data and minimize TX latency.
pub fn execute_cmd(
buffer: &mut [u8],
buffer_size: usize,
socket: &mut SocketHandle,
laser: &mut LdDrive,
thermostat: &mut Thermostat,
state: &mut State,
device_settings: &mut DeviceSettings,
active_report: &mut bool,
) {
let mut cmd = TecSetICmd {
json: TecSetICmdJson::default(),
};
match cmd.set_json("/json", &buffer[0..buffer_size]) {
Ok(_) => {
thermostat.set_i(ElectricCurrent::new::<ampere>(cmd.json.tec_set_i));
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
return;
}
Err(_) => { /* Do Nothing */ }
}
let mut cmd = Cmd {
json: CmdJsonObj::default(),
};
match cmd.set_json("/json", &buffer[0..buffer_size]) {
Ok(_) => {
info!(
"############ Laser Diode Command Received {:?}",
cmd.json.laser_diode_cmd
);
info!("############ Thermostat Command Received {:?}", cmd.json.thermostat_cmd);
info!("############ Device Command Received {:?}", cmd.json.device_cmd);
match cmd.json.device_cmd {
Some(DeviceCmd::SetIPSettings) => match cmd.json.ip_settings {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
device_settings.ip_settings = val;
*state = State::SaveDeviceSettings;
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_IP_SETTINGS),
socket,
);
}
},
Some(DeviceCmd::Dfu) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
unsafe {
dfu::set_dfu_trigger();
}
*state = State::HardReset;
}
Some(DeviceCmd::SetActiveReportMode) => match cmd.json.data_bool {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
*active_report = val;
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_BOOL),
socket,
);
}
},
Some(DeviceCmd::GetStatusReport) => {
send_status_report(buffer, laser, thermostat, socket);
}
Some(DeviceCmd::GetSettingsSummary) => {
send_settings_summary(buffer, laser, thermostat, socket);
}
Some(DeviceCmd::SaveFlashSettings) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
*state = State::SaveLdThermostatSettings;
}
Some(DeviceCmd::LoadFlashSettings) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
*state = State::LoadFlashSettings;
}
Some(DeviceCmd::HardReset) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
*state = State::PrepareForHardReset;
}
None => { /* Do Nothing */ }
_ => {
send_response(buffer, ResponseEnum::InvalidCmd, None, socket);
debug!("Unimplemented Command")
}
}
match cmd.json.laser_diode_cmd {
Some(LdCmdEnum::SetDefaultPowerOn) => match cmd.json.data_bool {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
laser.set_default_pwr_on(val);
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_BOOL),
socket,
);
}
},
Some(LdCmdEnum::PowerUp) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
laser.power_up()
}
Some(LdCmdEnum::PowerDown) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
laser.power_down()
}
Some(LdCmdEnum::LdTermsShort) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
laser.ld_short();
}
Some(LdCmdEnum::LdTermsOpen) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
laser.ld_open();
}
Some(LdCmdEnum::SetI) => match cmd.json.data_f32 {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
laser.ld_set_i(ElectricCurrent::new::<ampere>(val));
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(LdCmdEnum::SetISoftLimit) => match cmd.json.data_f32 {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
laser.set_ld_drive_current_limit(ElectricCurrent::new::<ampere>(val))
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(LdCmdEnum::SetPdResponsitivity) => match cmd.json.data_f32 {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
laser.set_pd_responsitivity(ResponsitivityUnit {
dimension: PhantomData,
units: PhantomData,
value: val,
})
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(LdCmdEnum::SetPdDarkCurrent) => match cmd.json.data_f32 {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
laser.set_pd_dark_current(ElectricCurrent::new::<ampere>(val))
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(LdCmdEnum::SetLdPwrLimit) => match cmd.json.data_f32 {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
laser.set_ld_power_limit(Power::new::<watt>(val))
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(LdCmdEnum::ClearAlarm) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
laser.pd_mon_clear_alarm()
}
None => { /* Do Nothing*/ }
_ => {
send_response(buffer, ResponseEnum::InvalidCmd, Some(ERR_MSG_MISSING_DATA_F32), socket);
info!("Unimplemented Command")
}
}
match cmd.json.thermostat_cmd {
Some(ThermostatCmdEnum::SetDefaultPowerOn) => match cmd.json.data_bool {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_default_pwr_on(val);
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(ThermostatCmdEnum::PowerUp) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.power_up()
}
Some(ThermostatCmdEnum::PowerDown) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.power_down()
}
Some(ThermostatCmdEnum::SetTecMaxV) => match cmd.json.data_f32 {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_max_v(ElectricPotential::new::<volt>(val));
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(ThermostatCmdEnum::SetTecMaxIPos) => match cmd.json.data_f32 {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_max_i_pos(ElectricCurrent::new::<ampere>(val));
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(ThermostatCmdEnum::SetTecMaxINeg) => match cmd.json.data_f32 {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_max_i_pos(ElectricCurrent::new::<ampere>(val));
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(ThermostatCmdEnum::SetTecIOut) => match cmd.json.data_f32 {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_i(ElectricCurrent::new::<ampere>(val));
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(ThermostatCmdEnum::SetTemperatureSetpoint) => match cmd.json.data_f32 {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_temperature_setpoint(ThermodynamicTemperature::new::<degree_celsius>(val));
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(ThermostatCmdEnum::SetPidEngage) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_pid_engaged(true);
}
Some(ThermostatCmdEnum::SetPidDisEngage) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_pid_engaged(false);
}
Some(ThermostatCmdEnum::SetPidKp) => match cmd.json.data_f32 {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_pid(Kp, val);
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(ThermostatCmdEnum::SetPidKi) => match cmd.json.data_f32 {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_pid(Ki, val);
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(ThermostatCmdEnum::SetPidKd) => match cmd.json.data_f32 {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_pid(Kd, val);
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(ThermostatCmdEnum::SetPidOutMin) => match cmd.json.data_f32 {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_pid(Min, val);
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(ThermostatCmdEnum::SetPidOutMax) => match cmd.json.data_f32 {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_pid(Max, val);
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(ThermostatCmdEnum::SetPidUpdateInterval) => {
send_response(buffer, ResponseEnum::InvalidCmd, None, socket);
debug!("Not supported Yet")
}
Some(ThermostatCmdEnum::ConfigTempAdcFilter) => match cmd.json.temp_adc_filter {
Some(val) => match val.filter_type {
FilterType::Sinc5Sinc1With50hz60HzRejection => match val.sinc5sinc1postfilter {
Some(val2) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_temp_adc_sinc5_sinc1_with_postfilter(0, val2);
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_POSTFILTER),
socket,
);
}
},
FilterType::Sinc5Sinc1 => match val.sinc5sinc1odr {
Some(val2) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_temp_adc_sinc5_sinc1_filter(0, val2);
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_SINC5SINC1ODR),
socket,
);
}
},
FilterType::Sinc3WithFineODR => match val.sinc3fineodr {
Some(val2) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_temp_adc_sinc3_fine_filter(0, val2);
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_SINC3FINEODR),
socket,
);
}
},
FilterType::Sinc3 => match val.sinc3odr {
Some(val2) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_temp_adc_sinc3_filter(0, val2);
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_SINC3ODR),
socket,
);
}
},
},
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_TEMP_ADC_FILTER),
socket,
);
}
},
Some(ThermostatCmdEnum::SetTempMonUpperLimit) => match cmd.json.data_f32 {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_temp_mon_upper_limit(ThermodynamicTemperature::new::<degree_celsius>(val));
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(ThermostatCmdEnum::SetTempMonLowerLimit) => match cmd.json.data_f32 {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_temp_mon_lower_limit(ThermodynamicTemperature::new::<degree_celsius>(val));
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(ThermostatCmdEnum::ClearAlarm) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.clear_temp_mon_alarm();
}
Some(ThermostatCmdEnum::SetShT0) => match cmd.json.data_f32 {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_sh_t0(ThermodynamicTemperature::new::<degree_celsius>(val));
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(ThermostatCmdEnum::SetShR0) => match cmd.json.data_f32 {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_sh_r0(ElectricalResistance::new::<ohm>(val));
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(ThermostatCmdEnum::SetShBeta) => match cmd.json.data_f32 {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_sh_beta(val);
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
None => { /* Do Nothing*/ }
_ => {
send_response(buffer, ResponseEnum::InvalidCmd, None, socket);
}
}
}
Err(_) => {
info!("cmd_recv: {:?}", buffer);
send_response(buffer, ResponseEnum::InvalidCmd, None, socket);
}
}
}

View File

@ -1,2 +0,0 @@
pub mod cmd_handler;
pub mod net;

View File

@ -1,618 +0,0 @@
use core::mem::{self, MaybeUninit};
use fugit::TimerDurationU32;
use log::{debug, info};
use serde::{Deserialize, Serialize};
use smoltcp::{iface::{self, Interface, SocketHandle, SocketSet, SocketStorage},
socket::tcp::{Socket, SocketBuffer, State},
time::{Duration, Instant},
wire::{EthernetAddress, IpAddress, IpCidr, Ipv4Address, Ipv4Cidr}};
use stm32_eth::{dma::{EthernetDMA, RxRingEntry, TxRingEntry},
mac::{EthernetMACWithMii, Speed},
EthPins, Parts, PartsIn};
use stm32f4xx_hal::{gpio::{gpioa::*, gpiob::*, gpioc::*, Alternate, Input, Pin},
pac::{interrupt, Interrupt, Peripherals, TIM5},
rcc::Clocks,
timer::{Counter, Event},
Listen};
use crate::device::sys_timer;
#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
pub struct IpSettings {
addr: [u8; 4],
port: u16,
prefix_len: u8,
gateway: [u8; 4],
}
impl Default for IpSettings {
fn default() -> Self {
IpSettings {
addr: [192, 168, 1, 128],
port: 1337,
prefix_len: 24,
gateway: [192, 168, 1, 1],
}
}
}
static mut IFACE_TIMER: Option<IfacePollTimer> = None;
pub struct ServerHandle {
socket_handles: [SocketHandle; NUM_OF_SOCKETS],
socket_set: SocketSet<'static>,
socket_addr: (IpAddress, u16),
iface: EthInterface,
dma: EthernetDMA<'static, 'static>,
phy: EthernetPhy<EthernetMACWithMii<Pin<'A', 2, Alternate<11>>, Pin<'C', 1, Alternate<11>>>>,
link_was_up: bool,
}
pub type EthernetPins = EthPins<PA1<Input>, PA7<Input>, PB11<Input>, PB12<Input>, PB13<Input>, PC4<Input>, PC5<Input>>;
pub struct EthernetMgmtPins {
pub mdio: PA2<Alternate<11>>,
pub mdc: PC1<Alternate<11>>,
}
pub type EthInterface = Interface;
pub const NUM_OF_SOCKETS: usize = 4;
const TCP_BUFFER_SIZE: usize = 4096;
static mut RX_RING: Option<[RxRingEntry; 8]> = None;
static mut TX_RING: Option<[TxRingEntry; 8]> = None;
static mut SOCKET_STORAGE: Option<[SocketStorage<'static>; NUM_OF_SOCKETS]> = None;
fn now_fn() -> smoltcp::time::Instant {
Instant::from_millis(i64::from(sys_timer::now()))
}
static mut SERVER_HANDLE: Option<ServerHandle> = None;
pub const FREQ: u32 = 1000000;
impl ServerHandle {
pub fn new(
eth_pins: EthernetPins,
eth_mgmt_pins: EthernetMgmtPins,
ethernet_parts_in: PartsIn,
clocks: Clocks,
tim5: Counter<TIM5, FREQ>,
mac_addr: [u8; 6],
ip_settings: IpSettings,
) {
let rx_ring = unsafe { RX_RING.get_or_insert(Default::default()) };
let tx_ring = unsafe { TX_RING.get_or_insert(Default::default()) };
let socket_storage = unsafe { SOCKET_STORAGE.get_or_insert([SocketStorage::EMPTY; NUM_OF_SOCKETS]) };
let Parts { mut dma, mac, .. } = stm32_eth::new_with_mii(
ethernet_parts_in,
&mut rx_ring[..],
&mut tx_ring[..],
clocks,
eth_pins,
eth_mgmt_pins.mdio,
eth_mgmt_pins.mdc,
)
.unwrap();
let ip_init = IpCidr::Ipv4(Ipv4Cidr::new(
Ipv4Address::new(
ip_settings.addr[0],
ip_settings.addr[1],
ip_settings.addr[2],
ip_settings.addr[3],
),
ip_settings.prefix_len,
));
let socket_addr: (IpAddress, u16) = (
IpAddress::Ipv4(Ipv4Address::new(
ip_settings.addr[0],
ip_settings.addr[1],
ip_settings.addr[2],
ip_settings.addr[3],
)),
ip_settings.port,
);
let mut routes = smoltcp::iface::Routes::new();
routes
.add_default_ipv4_route(Ipv4Address::new(
ip_settings.gateway[0],
ip_settings.gateway[1],
ip_settings.gateway[2],
ip_settings.gateway[3],
))
.ok();
dma.enable_interrupt();
let config = iface::Config::new(EthernetAddress::from_bytes(&mac_addr).into());
let mut iface = Interface::new(config, &mut &mut dma, smoltcp::time::Instant::ZERO);
iface.set_hardware_addr(EthernetAddress(mac_addr).into());
debug!("MAC ADDRESS: {:02X?}", EthernetAddress(mac_addr));
debug!("IP Settings: {:?}", ip_settings);
iface.update_ip_addrs(|addr| {
addr.push(ip_init).unwrap();
});
let mut socket_set = SocketSet::new(&mut socket_storage[..]);
let tcp_handles = {
// Do not use NUM_OF_SOCKETS to define array size to
// remind developers to create/remove tcp_handles accordingly after changing NUM_OF_SOCKETS
let mut tcp_handles: [MaybeUninit<SocketHandle>; 4] = unsafe { MaybeUninit::uninit().assume_init() };
macro_rules! create_tcp_handle {
($rx_storage:ident, $tx_storage:ident, $handle:expr) => {
static mut $rx_storage: [u8; TCP_BUFFER_SIZE] = [0; TCP_BUFFER_SIZE];
static mut $tx_storage: [u8; TCP_BUFFER_SIZE] = [0; TCP_BUFFER_SIZE];
unsafe {
let rx_buffer = SocketBuffer::new(&mut $rx_storage[..]);
let tx_buffer = SocketBuffer::new(&mut $tx_storage[..]);
$handle.write(socket_set.add(Socket::new(rx_buffer, tx_buffer)));
}
};
}
create_tcp_handle!(RX_STORAGE0, TX_STORAGE0, tcp_handles[0]);
create_tcp_handle!(RX_STORAGE1, TX_STORAGE1, tcp_handles[1]);
create_tcp_handle!(RX_STORAGE2, TX_STORAGE2, tcp_handles[2]);
create_tcp_handle!(RX_STORAGE3, TX_STORAGE3, tcp_handles[3]);
unsafe { mem::transmute::<_, [SocketHandle; 4]>(tcp_handles) }
};
for i in 0..NUM_OF_SOCKETS {
let socket = socket_set.get_mut::<Socket>(tcp_handles[i]);
socket.listen(socket_addr).ok();
socket.set_timeout(Some(Duration::from_secs(5)));
// Value from Linux net.ipv4.tcp_keepalive_time
socket.set_keep_alive(Some(Duration::from_secs(7200)));
socket.set_nagle_enabled(false);
}
iface.poll(
Instant::from_millis(i64::from(sys_timer::now())),
&mut &mut dma,
&mut socket_set,
);
IfacePollTimer::setup(tim5);
if let Ok(mut phy) = EthernetPhy::from_miim(mac, 0) {
info!("Resetting PHY as an extra step. Type: {}", phy.ident_string());
phy.phy_init();
let server = ServerHandle {
socket_handles: tcp_handles,
socket_set: socket_set,
socket_addr: socket_addr,
iface: iface,
dma: dma,
phy: phy,
link_was_up: false,
};
unsafe {
SERVER_HANDLE = Some(server);
}
} else {
panic!("Ethernet Phy is not supported");
}
}
pub fn update_link_speed(&mut self) -> bool {
if !self.link_was_up & self.phy.phy_link_up() {
if let Some(speed) = self.phy.speed().map(|s| match s {
PhySpeed::HalfDuplexBase10T => Speed::HalfDuplexBase10T,
PhySpeed::FullDuplexBase10T => Speed::FullDuplexBase10T,
PhySpeed::HalfDuplexBase100Tx => Speed::HalfDuplexBase100Tx,
PhySpeed::FullDuplexBase100Tx => Speed::FullDuplexBase100Tx,
}) {
info!("New eth link is up. Setting detected PhySpeed: {:?}", speed);
self.phy.get_miim().set_speed(speed);
self.link_was_up = self.phy.phy_link_up();
return true;
} else {
debug!("Failed to detect link speed.");
}
}
self.link_was_up = self.phy.phy_link_up();
return false;
}
pub fn poll_iface(&mut self) {
self.iface.poll(now_fn(), &mut &mut self.dma, &mut self.socket_set);
}
pub fn poll_at_iface(&mut self) -> Option<Instant> {
self.iface.poll_at(now_fn(), &mut self.socket_set)
}
pub fn poll_delay_iface(&mut self) -> Option<Duration> {
self.iface.poll_delay(now_fn(), &mut self.socket_set)
}
pub fn recv(
&mut self,
buffer: &mut [u8],
socket_handles: SocketHandle,
) -> Result<usize, smoltcp::socket::tcp::RecvError> {
let socket = self.socket_set.get_mut::<Socket>(socket_handles);
socket.recv_slice(buffer)
}
pub fn send(&mut self, buffer: &mut [u8], num_bytes: usize, socket_handles: SocketHandle) {
let socket = self.socket_set.get_mut::<Socket>(socket_handles);
if num_bytes > 0 {
match socket.send_slice(&buffer[..num_bytes]) {
Ok(_) => {
self.poll_iface();
info!("Sent {} bytes.", num_bytes);
}
Err(err) => {
info!("Bytes cannot be sent. Error: {:?}", err)
}
};
}
}
pub fn is_socket_connected(&mut self, socket_handles: SocketHandle) -> bool {
let socket = self.socket_set.get_mut::<Socket>(socket_handles);
socket.state() == State::Established
}
pub fn poll_socket_status(&mut self, socket_handles: SocketHandle) -> bool {
let socket = self.socket_set.get_mut::<Socket>(socket_handles);
if !socket.is_listening() && !socket.is_open() || socket.state() == State::CloseWait {
socket.abort();
socket.listen(self.socket_addr).ok();
info!("Disconnected... Reopening listening socket.");
return false;
} else if socket.state() == State::Closed || socket.state() == State::Closing {
return false;
}
return true;
}
pub fn close_socket(&mut self, socket_handles: SocketHandle) {
let socket = self.socket_set.get_mut::<Socket>(socket_handles);
socket.abort();
}
pub fn can_send(&mut self, socket_handles: SocketHandle) -> bool {
self.socket_set.get_mut::<Socket>(socket_handles).can_send()
}
pub fn can_recv(&mut self, socket_handles: SocketHandle) -> bool {
self.socket_set.get_mut::<Socket>(socket_handles).can_recv()
}
}
use ieee802_3_miim::{phy::{lan87xxa::{LAN8720A, LAN8742A},
BarePhy, PhySpeed, KSZ8081R},
Miim, Pause, Phy};
/// An ethernet PHY
pub enum EthernetPhy<M: Miim> {
/// LAN8720A
LAN8720A(LAN8720A<M>),
/// LAN8742A
LAN8742A(LAN8742A<M>),
/// KSZ8081R
KSZ8081R(KSZ8081R<M>),
}
impl<M: Miim> Phy<M> for EthernetPhy<M> {
fn best_supported_advertisement(&self) -> ieee802_3_miim::AutoNegotiationAdvertisement {
unimplemented!()
}
fn get_miim(&mut self) -> &mut M {
match self {
EthernetPhy::LAN8720A(phy) => phy.get_miim(),
EthernetPhy::LAN8742A(phy) => phy.get_miim(),
EthernetPhy::KSZ8081R(phy) => phy.get_miim(),
}
}
fn get_phy_addr(&self) -> u8 {
match self {
EthernetPhy::LAN8720A(phy) => phy.get_phy_addr(),
EthernetPhy::LAN8742A(phy) => phy.get_phy_addr(),
EthernetPhy::KSZ8081R(phy) => phy.get_phy_addr(),
}
}
}
impl<M: Miim> EthernetPhy<M> {
/// Attempt to create one of the known PHYs from the given
/// MIIM.
///
/// Returns an error if the PHY does not support the extended register
/// set, or if the PHY's identifier does not correspond to a known PHY.
pub fn from_miim(miim: M, phy_addr: u8) -> Result<Self, M> {
let mut bare = BarePhy::new(miim, phy_addr, Pause::NoPause);
let phy_ident = if let Some(id) = bare.phy_ident() {
id.raw_u32()
} else {
return Err(bare.release());
};
let miim = bare.release();
match phy_ident & 0xFFFFFFF0 {
0x0007C0F0 => Ok(Self::LAN8720A(LAN8720A::new(miim, phy_addr))),
0x0007C130 => Ok(Self::LAN8742A(LAN8742A::new(miim, phy_addr))),
0x00221560 => Ok(Self::KSZ8081R(KSZ8081R::new(miim, phy_addr))),
_ => Err(miim),
}
}
/// Get a string describing the type of PHY
pub const fn ident_string(&self) -> &'static str {
match self {
EthernetPhy::LAN8720A(_) => "LAN8720A",
EthernetPhy::LAN8742A(_) => "LAN8742A",
EthernetPhy::KSZ8081R(_) => "KSZ8081R",
}
}
/// Initialize the PHY
pub fn phy_init(&mut self) {
match self {
EthernetPhy::LAN8720A(phy) => phy.phy_init(),
EthernetPhy::LAN8742A(phy) => phy.phy_init(),
EthernetPhy::KSZ8081R(phy) => {
phy.set_autonegotiation_advertisement(phy.best_supported_advertisement());
}
}
}
#[allow(dead_code)]
pub fn speed(&mut self) -> Option<ieee802_3_miim::phy::PhySpeed> {
match self {
EthernetPhy::LAN8720A(phy) => phy.link_speed(),
EthernetPhy::LAN8742A(phy) => phy.link_speed(),
EthernetPhy::KSZ8081R(phy) => phy.link_speed(),
}
}
#[allow(dead_code)]
pub fn release(self) -> M {
match self {
EthernetPhy::LAN8720A(phy) => phy.release(),
EthernetPhy::LAN8742A(phy) => phy.release(),
EthernetPhy::KSZ8081R(phy) => phy.release(),
}
}
}
pub fn eth_can_sock_send(socket_handles: SocketHandle) -> bool {
unsafe {
if let Some(ref mut server_handle) = SERVER_HANDLE {
server_handle.can_send(socket_handles)
} else {
panic!("eth_check_if_sock_can_send is called before init");
}
}
}
pub fn eth_can_sock_recv(socket_handles: SocketHandle) -> bool {
unsafe {
if let Some(ref mut server_handle) = SERVER_HANDLE {
server_handle.can_recv(socket_handles)
} else {
panic!("eth_check_if_sock_can_recv is called before init");
}
}
}
pub fn eth_poll_link_status_and_update_link_speed() -> bool {
unsafe {
if let Some(ref mut server_handle) = SERVER_HANDLE {
let new_link_is_up = server_handle.update_link_speed();
if new_link_is_up {
info!("Resetting TCP Sockets");
for_each(|socket, _| {
eth_close_socket(socket);
});
}
return new_link_is_up;
} else {
panic!("eth_poll_link_status_and_update_link_speed is called before init");
}
}
}
pub fn eth_update_iface_poll_timer() {
unsafe {
if let Some(ref mut server_handle) = SERVER_HANDLE {
IfacePollTimer::set_timer(server_handle.poll_at_iface(), server_handle.poll_delay_iface())
} else {
panic!("eth_update_iface_poll_timer is called before init")
}
}
}
fn eth_poll_iface() {
unsafe {
if let Some(ref mut server_handle) = SERVER_HANDLE {
server_handle.poll_iface();
} else {
panic!("eth_poll_packet is called before init");
}
}
}
pub fn eth_send(buffer: &mut [u8], num_bytes: usize, socket_handles: SocketHandle) {
unsafe {
if let Some(ref mut server_handle) = SERVER_HANDLE {
server_handle.send(buffer, num_bytes, socket_handles);
} else {
panic!("eth_send is called before init");
}
}
}
pub fn eth_recv(buffer: &mut [u8], socket_handles: SocketHandle) -> usize {
unsafe {
if let Some(ref mut server_handle) = SERVER_HANDLE {
match server_handle.recv(buffer, socket_handles) {
Ok(recv_bytes) => return recv_bytes,
Err(err) => {
debug!("TCP Recv Error: {}", err);
return 0;
}
};
} else {
panic!("eth_send is called before init");
}
}
}
pub fn eth_is_socket_connected(socket_handles: SocketHandle) -> bool {
unsafe {
if let Some(ref mut server_handle) = SERVER_HANDLE {
server_handle.is_socket_connected(socket_handles)
} else {
panic!("eth_is_socket_connected is called before init");
}
}
}
pub fn eth_is_socket_active(socket_handles: SocketHandle) -> bool {
unsafe {
if let Some(ref mut server_handle) = SERVER_HANDLE {
server_handle.poll_socket_status(socket_handles)
} else {
panic!("eth_is_socket_active is called before init");
}
}
}
pub fn eth_close_socket(socket_handles: SocketHandle) {
unsafe {
if let Some(ref mut server_handle) = SERVER_HANDLE {
server_handle.close_socket(socket_handles)
} else {
panic!("eth_close_socket is called before init");
}
}
}
pub fn for_each<F: FnMut(SocketHandle, usize)>(mut callback: F) {
unsafe {
if let Some(ref mut server_handle) = SERVER_HANDLE {
for i in 0..NUM_OF_SOCKETS {
callback(server_handle.socket_handles[i], i);
}
} else {
panic!("eth_close_socket is called before init");
}
}
}
/// Potentially wake up from `wfi()`, set the interrupt pending flag,
/// clear interrupt flags.
#[interrupt]
fn ETH() {
let interrupt_reason = stm32_eth::eth_interrupt_handler();
cortex_m::interrupt::free(|_| {
if interrupt_reason.rx {
eth_poll_iface();
}
});
debug!("Ethernet Interrupt{:?}", interrupt_reason);
}
pub struct IfacePollTimer {
timer: Counter<TIM5, FREQ>,
poll_at_set: Instant,
}
impl IfacePollTimer {
fn get() -> Option<&'static mut Self> {
unsafe { IFACE_TIMER.as_mut() }
}
pub fn setup(tim5: Counter<TIM5, FREQ>) {
unsafe {
cortex_m::peripheral::NVIC::unmask(Interrupt::TIM5);
IFACE_TIMER = Some(IfacePollTimer {
timer: tim5,
poll_at_set: Instant::from_micros(u32::max_value()),
})
}
}
fn is_timer_running() -> bool {
unsafe { Peripherals::steal().TIM5.cr1.read().cen().bit() }
}
pub fn set_timer(poll_at: Option<Instant>, delay: Option<Duration>) {
if let Some(ref mut iface_timer) = IfacePollTimer::get() {
if !IfacePollTimer::is_timer_running() {
match poll_at {
Some(val) => {
IfacePollTimer::set_timer_delay(delay);
iface_timer.poll_at_set = val;
}
None => {}
}
} else {
match poll_at {
Some(val) => {
if iface_timer.poll_at_set != val {
IfacePollTimer::stop_listening();
IfacePollTimer::set_timer_delay(delay);
iface_timer.poll_at_set = val;
}
}
None => {}
}
}
}
}
fn set_timer_delay(delay: Option<Duration>) {
if let Some(ref mut iface_timer) = IfacePollTimer::get() {
match delay {
Some(val) => {
if val.micros() != 0 {
match iface_timer.timer.start(TimerDurationU32::micros(val.micros() as u32)) {
Ok(_) => {
iface_timer.timer.listen(Event::Update);
}
Err(e) => {
debug!("Timer cannot be set {:?}", e)
}
};
} else {
eth_poll_iface();
}
}
None => {}
}
}
}
pub fn clear_irq_flag() {
if let Some(ref mut iface_timer) = IfacePollTimer::get() {
iface_timer.timer.wait().ok();
}
}
pub fn stop_listening() {
if let Some(ref mut iface_timer) = IfacePollTimer::get() {
iface_timer.timer.unlisten(Event::Update);
iface_timer.timer.cancel().ok();
}
}
}
#[interrupt]
fn TIM5() {
eth_poll_iface();
cortex_m::interrupt::free(|_| {
IfacePollTimer::stop_listening();
IfacePollTimer::clear_irq_flag();
});
}

1
src/network/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod network;

296
src/network/network.rs Normal file
View File

@ -0,0 +1,296 @@
// Modified by Wong Tat Hang (aw@m-labs.hk), with original source from
// https://github.com/stm32-rs/stm32-eth/blob/master/examples/rtic-echo.rs
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use log::{info, warn};
use smoltcp::{
iface::{
Interface, InterfaceBuilder, Neighbor, NeighborCache, Route, SocketHandle, SocketStorage,
},
socket::{TcpSocket, TcpSocketBuffer, TcpState},
wire::{EthernetAddress, IpAddress, IpCidr, Ipv4Address, Ipv4Cidr},
};
use stm32_eth::stm32::{ETHERNET_DMA, ETHERNET_MAC, ETHERNET_MMC};
use stm32_eth::*;
use stm32f4xx_hal::{
gpio::{gpioa::*, gpiob::*, gpioc::*, Alternate, Input},
rcc::Clocks,
};
use crate::app::monotonics;
pub type EthernetPins =
EthPins<PA1<Input>, PA7<Input>, PB11<Input>, PB12<Input>, PB13<Input>, PC4<Input>, PC5<Input>>;
pub type EthInterface = Interface<'static, &'static mut EthernetDMA<'static, 'static>>;
const IPV4_ADDR: (u8, u8, u8, u8) = (192, 168, 1, 132);
const ADDRESS: (IpAddress, u16) = (
IpAddress::Ipv4(Ipv4Address::new(
IPV4_ADDR.0,
IPV4_ADDR.1,
IPV4_ADDR.2,
IPV4_ADDR.3,
)),
1337,
);
const MAC: [u8; 6] = [0x02, 0x5f, 0x25, 0x37, 0x93, 0x0e];
pub struct EthernetPeripherals {
pub dma: ETHERNET_DMA,
pub mac: ETHERNET_MAC,
pub mmc: ETHERNET_MMC,
}
pub struct ServerHandle {
// storage: &'static mut ServerStorage,
socket_handle: SocketHandle,
iface: EthInterface,
}
pub struct ServerStorage {
rx_ring: [RxRingEntry; 8],
tx_ring: [TxRingEntry; 2],
storage: NetworkStorage,
dma: core::mem::MaybeUninit<EthernetDMA<'static, 'static>>,
}
impl ServerStorage {
pub const fn new() -> Self {
ServerStorage {
rx_ring: [
RxRingEntry::new(),
RxRingEntry::new(),
RxRingEntry::new(),
RxRingEntry::new(),
RxRingEntry::new(),
RxRingEntry::new(),
RxRingEntry::new(),
RxRingEntry::new(),
],
tx_ring: [TxRingEntry::new(), TxRingEntry::new()],
storage: NetworkStorage::new(),
dma: core::mem::MaybeUninit::uninit(),
}
}
}
/// All storage required for networking
pub struct NetworkStorage {
pub ip_addrs: [IpCidr; 1],
pub sockets: [SocketStorage<'static>; 1],
pub tcp_socket_storage: TcpSocketStorage,
pub neighbor_cache: [Option<(IpAddress, Neighbor)>; 8],
pub routes_cache: [Option<(IpCidr, Route)>; 8],
}
impl NetworkStorage {
const IP_INIT: IpCidr = IpCidr::Ipv4(Ipv4Cidr::new(
Ipv4Address::new(IPV4_ADDR.0, IPV4_ADDR.1, IPV4_ADDR.2, IPV4_ADDR.3),
24,
));
pub const fn new() -> Self {
NetworkStorage {
ip_addrs: [Self::IP_INIT],
neighbor_cache: [None; 8],
routes_cache: [None; 8],
sockets: [SocketStorage::EMPTY; 1],
tcp_socket_storage: TcpSocketStorage::new(),
}
}
}
/// Storage of TCP sockets
#[derive(Copy, Clone)]
pub struct TcpSocketStorage {
rx_storage: [u8; 2048],
tx_storage: [u8; 2048],
}
impl TcpSocketStorage {
const fn new() -> Self {
Self {
rx_storage: [0; 2048],
tx_storage: [0; 2048],
}
}
}
fn now_fn() -> smoltcp::time::Instant {
let time = monotonics::now().duration_since_epoch().to_millis();
smoltcp::time::Instant::from_millis(time as i64)
}
impl ServerHandle {
pub fn new(
eth_pins: EthernetPins,
ethernet: EthernetPeripherals,
clocks: Clocks,
storage: &'static mut ServerStorage,
mdio: stm32f4xx_hal::gpio::PA2<Alternate<11>>,
mdc: stm32f4xx_hal::gpio::PC1<Alternate<11>>,
) -> ServerHandle {
let (dma, mac) = stm32_eth::new_with_mii(
ethernet.mac,
ethernet.mmc,
ethernet.dma,
&mut storage.rx_ring[..],
&mut storage.tx_ring[..],
clocks,
eth_pins,
mdio,
mdc,
)
.unwrap();
let mut routes = smoltcp::iface::Routes::new(&mut storage.storage.routes_cache[..]);
routes
.add_default_ipv4_route(Ipv4Address::new(192, 168, 1, 1))
.ok();
let dma = storage.dma.write(dma);
dma.enable_interrupt();
let rx_buffer =
TcpSocketBuffer::new(&mut storage.storage.tcp_socket_storage.rx_storage[..]);
let tx_buffer =
TcpSocketBuffer::new(&mut storage.storage.tcp_socket_storage.tx_storage[..]);
let socket = TcpSocket::new(rx_buffer, tx_buffer);
let mut iface = InterfaceBuilder::new(dma, &mut storage.storage.sockets[..])
.hardware_addr(EthernetAddress(MAC).into())
.ip_addrs(&mut storage.storage.ip_addrs[..])
.neighbor_cache(NeighborCache::new(&mut storage.storage.neighbor_cache[..]))
.routes(routes)
.finalize();
let mut server = ServerHandle {
socket_handle: iface.add_socket(socket),
iface: iface,
};
let socket = server.iface.get_socket::<TcpSocket>(server.socket_handle);
socket.listen(ADDRESS).ok();
server.iface.poll(now_fn()).ok();
if let Ok(mut phy) = EthernetPhy::from_miim(mac, 0) {
info!(
"Resetting PHY as an extra step. Type: {}",
phy.ident_string()
);
phy.phy_init();
} else {
info!("Not resetting unsupported PHY.");
}
server
}
pub fn poll(&mut self, buffer: &mut [u8]) {
self.iface.poll(now_fn()).ok();
let socket = self.iface.get_socket::<TcpSocket>(self.socket_handle);
if let Ok(recv_bytes) = socket.recv_slice(buffer) {
if recv_bytes > 0 {
socket.send_slice(&buffer[..recv_bytes]).ok();
info!("Echoed {} bytes.", recv_bytes);
}
}
if !socket.is_listening() && !socket.is_open() || socket.state() == TcpState::CloseWait {
socket.abort();
socket.listen(ADDRESS).ok();
warn!("Disconnected... Reopening listening socket.");
}
self.iface.poll(now_fn()).ok();
}
}
use ieee802_3_miim::{
phy::{
lan87xxa::{LAN8720A, LAN8742A},
BarePhy, KSZ8081R,
},
Miim, Pause, Phy,
};
/// An ethernet PHY
pub enum EthernetPhy<M: Miim> {
/// LAN8720A
LAN8720A(LAN8720A<M>),
/// LAN8742A
LAN8742A(LAN8742A<M>),
/// KSZ8081R
KSZ8081R(KSZ8081R<M>),
}
impl<M: Miim> Phy<M> for EthernetPhy<M> {
fn best_supported_advertisement(&self) -> ieee802_3_miim::AutoNegotiationAdvertisement {
unimplemented!()
}
fn get_miim(&mut self) -> &mut M {
match self {
EthernetPhy::LAN8720A(phy) => phy.get_miim(),
EthernetPhy::LAN8742A(phy) => phy.get_miim(),
EthernetPhy::KSZ8081R(phy) => phy.get_miim(),
}
}
fn get_phy_addr(&self) -> u8 {
match self {
EthernetPhy::LAN8720A(phy) => phy.get_phy_addr(),
EthernetPhy::LAN8742A(phy) => phy.get_phy_addr(),
EthernetPhy::KSZ8081R(phy) => phy.get_phy_addr(),
}
}
}
impl<M: Miim> EthernetPhy<M> {
/// Attempt to create one of the known PHYs from the given
/// MIIM.
///
/// Returns an error if the PHY does not support the extended register
/// set, or if the PHY's identifier does not correspond to a known PHY.
pub fn from_miim(miim: M, phy_addr: u8) -> Result<Self, ()> {
let mut bare = BarePhy::new(miim, phy_addr, Pause::NoPause);
let phy_ident = bare.phy_ident().ok_or(())?;
let miim = bare.release();
match phy_ident & 0xFFFFFFF0 {
0x0007C0F0 => Ok(Self::LAN8720A(LAN8720A::new(miim, phy_addr))),
0x0007C130 => Ok(Self::LAN8742A(LAN8742A::new(miim, phy_addr))),
0x00221560 => Ok(Self::KSZ8081R(KSZ8081R::new(miim, phy_addr))),
_ => Err(()),
}
}
/// Get a string describing the type of PHY
pub const fn ident_string(&self) -> &'static str {
match self {
EthernetPhy::LAN8720A(_) => "LAN8720A",
EthernetPhy::LAN8742A(_) => "LAN8742A",
EthernetPhy::KSZ8081R(_) => "KSZ8081R",
}
}
/// Initialize the PHY
pub fn phy_init(&mut self) {
match self {
EthernetPhy::LAN8720A(phy) => phy.phy_init(),
EthernetPhy::LAN8742A(phy) => phy.phy_init(),
EthernetPhy::KSZ8081R(phy) => {
phy.set_autonegotiation_advertisement(phy.best_supported_advertisement());
}
}
}
}

0
src/photo_diode/mod.rs Normal file
View File

View File

@ -1,46 +0,0 @@
use fugit::MegahertzU32;
use stm32f4xx_hal::{hal::{digital::OutputPin, spi::SpiBus},
spi};
use crate::device::sys_timer::sleep;
/// SPI Mode 1
pub const SPI_MODE: spi::Mode = spi::Mode {
polarity: spi::Polarity::IdleLow,
phase: spi::Phase::CaptureOnSecondTransition,
};
// MAX Clock Frequency: 30MHz | SPI1 Max Frequency 42MHz
pub const SPI_CLOCK_MHZ: MegahertzU32 = MegahertzU32::from_raw(21);
pub const MAX_VALUE: u32 = 0x3FFFF;
pub struct Dac<SPI: SpiBus<u8>, S: OutputPin> {
spi: SPI,
sync: S,
}
impl<SPI: SpiBus<u8>, S: OutputPin> Dac<SPI, S> {
pub fn new(spi: SPI, mut sync: S) -> Self {
let _ = sync.set_low();
Dac { spi, sync }
}
fn write(&mut self, buf: &mut [u8]) -> Result<(), SPI::Error> {
// pulse sync to start a new transfer. leave sync idle low
// afterwards to save power as recommended per datasheet.
let _ = self.sync.set_high();
// must be high for >= 33 ns
sleep(1);
let _ = self.sync.set_low();
self.spi.transfer_in_place(buf)?;
Ok(())
}
pub fn set(&mut self, value: u32) -> Result<u32, SPI::Error> {
let value = value.min(MAX_VALUE);
let mut buf = [(value >> 14) as u8, (value >> 6) as u8, (value << 2) as u8];
self.write(&mut buf)?;
Ok(value)
}
}

View File

@ -1,347 +0,0 @@
use core::fmt;
use log::{info, warn};
use stm32f4xx_hal::{gpio::{Output, PushPull, PA15},
hal::{digital::OutputPin, spi::SpiBus},
pac::SPI3,
spi::Spi};
use uom::si::{electric_potential::volt, f32::ElectricPotential};
use super::{checksum::{Checksum, ChecksumMode},
regs::{self, Register, RegisterData},
sinc3_fine_odr_closest, sinc3_fine_odr_output_rate, DigitalFilterOrder, FilterType, Input, Mode,
PostFilter, RefSource, SingleChODR};
/// AD7172-2 implementation
///
/// [Manual](https://www.analog.com/media/en/technical-documentation/data-sheets/AD7172-2.pdf)
pub struct Adc<SPI: SpiBus<u8>, NSS: OutputPin> {
spi: SPI,
nss: NSS,
checksum_mode: ChecksumMode,
}
type AdcSpi = Spi<SPI3>;
type AdcNss = PA15<Output<PushPull>>;
pub type AdcPhy = Adc<AdcSpi, AdcNss>;
impl<SPI: SpiBus<u8, Error = E>, NSS: OutputPin, E: fmt::Debug> Adc<SPI, NSS> {
pub fn new(spi: SPI, mut nss: NSS) -> Result<Self, SPI::Error> {
let _ = nss.set_high();
let mut adc = Adc {
spi,
nss,
checksum_mode: ChecksumMode::Off,
};
adc.reset()?;
adc.set_checksum_mode(ChecksumMode::Crc).unwrap();
let mut retries = 0;
let mut adc_id;
loop {
adc_id = adc.identify()?;
if adc_id & 0xFFF0 == 0x00D0 {
break;
} else {
retries += 1;
}
}
info!("ADC id: {:04X} ({} retries)", adc_id, retries);
let mut adc_mode = <regs::AdcMode as Register>::Data::empty();
adc_mode.set_ref_en(true);
adc_mode.set_sing_cyc(false);
adc_mode.set_mode(Mode::Standby);
adc.write_reg(&regs::AdcMode, &mut adc_mode)?;
Ok(adc)
}
/// `0x00DX` for AD7172-2
pub fn identify(&mut self) -> Result<u16, SPI::Error> {
self.read_reg(&regs::Id).map(|id| id.id())
}
pub fn set_checksum_mode(&mut self, mode: ChecksumMode) -> Result<(), SPI::Error> {
// Cannot use update_reg() here because checksum_mode is
// updated between read_reg() and write_reg().
let mut ifmode = self.read_reg(&regs::IfMode)?;
ifmode.set_crc(mode);
self.checksum_mode = mode;
self.write_reg(&regs::IfMode, &mut ifmode)?;
Ok(())
}
pub fn set_sync_enable(&mut self, enable: bool) -> Result<(), SPI::Error> {
self.update_reg(&regs::GpioCon, |data| {
data.set_sync_en(enable);
})
}
pub fn setup_channel(&mut self, index: u8, in_pos: Input, in_neg: Input) -> Result<(), SPI::Error> {
self.update_reg(&regs::SetupCon { index }, |data| {
data.set_bipolar(false);
data.set_refbuf_pos(true);
data.set_refbuf_neg(true);
data.set_ainbuf_pos(true);
data.set_ainbuf_neg(true);
data.set_ref_sel(RefSource::External);
})?;
self.set_sinc5_sinc1_with_50hz_60hz_rejection(index, PostFilter::F16SPS)?;
self.update_reg(&regs::Channel { index }, |data| {
data.set_setup(index);
data.set_enabled(true);
data.set_a_in_pos(in_pos);
data.set_a_in_neg(in_neg);
})?;
Ok(())
}
pub fn get_calibration(&mut self, index: u8) -> Result<ChannelCalibration, SPI::Error> {
let offset = self.read_reg(&regs::Offset { index })?.offset();
let gain = self.read_reg(&regs::Gain { index })?.gain();
let bipolar = self.read_reg(&regs::SetupCon { index })?.bipolar();
Ok(ChannelCalibration { offset, gain, bipolar })
}
pub fn start_continuous_conversion(&mut self) -> Result<(), SPI::Error> {
let mut adc_mode = <regs::AdcMode as Register>::Data::empty();
adc_mode.set_ref_en(true);
// Set SING_CYC = 0 to maximize sampling rate with lowest noise for single channel of temperature measurement
adc_mode.set_sing_cyc(false);
adc_mode.set_mode(Mode::ContinuousConversion);
self.write_reg(&regs::AdcMode, &mut adc_mode)?;
Ok(())
}
/// Rate is only valid with single channel enabled
pub fn get_filter_type_and_rate(&mut self, index: u8) -> Result<(FilterType, f32), SPI::Error> {
let mut filter_type: FilterType = FilterType::Sinc5Sinc1With50hz60HzRejection;
let mut rate: f32 = -1.0;
self.read_reg(&regs::FiltCon { index }).map(|data| {
if data.sinc3_map() {
filter_type = FilterType::Sinc3WithFineODR;
let odr: u16 = (data.sinc3_map_fine_odr_msb() as u16) << 8 | data.sinc3_map_fine_odr_lsb() as u16;
rate = sinc3_fine_odr_output_rate(odr);
} else if data.enh_filt_en() {
filter_type = FilterType::Sinc5Sinc1With50hz60HzRejection;
match data.enh_filt().output_rate() {
Some(val) => {
rate = val;
}
None => {
rate = -1.0;
}
};
} else if data.order() == DigitalFilterOrder::Sinc5Sinc1 {
filter_type = FilterType::Sinc5Sinc1;
match data.odr().output_rate() {
Some(val) => {
rate = val;
}
None => {
rate = -1.0;
}
}
} else {
filter_type = FilterType::Sinc3;
match data.odr().output_rate() {
Some(val) => {
rate = val;
}
None => {
rate = -1.0;
}
}
}
})?;
Ok((filter_type, rate))
}
pub fn set_sinc5_sinc1_with_50hz_60hz_rejection(&mut self, index: u8, rate: PostFilter) -> Result<(), SPI::Error> {
self.update_reg(&regs::FiltCon { index }, |data| {
data.set_sinc3_map(false);
data.set_enh_filt_en(true);
data.set_order(DigitalFilterOrder::Sinc5Sinc1);
data.set_enh_filt(rate);
})
}
pub fn set_sinc5_sinc1_filter(&mut self, index: u8, rate: SingleChODR) -> Result<(), SPI::Error> {
self.update_reg(&regs::FiltCon { index }, |data| {
data.set_sinc3_map(false);
data.set_enh_filt_en(false);
data.set_order(DigitalFilterOrder::Sinc5Sinc1);
data.set_odr(rate);
})
}
pub fn set_sinc3_filter(&mut self, index: u8, rate: SingleChODR) -> Result<(), SPI::Error> {
self.update_reg(&regs::FiltCon { index }, |data| {
data.set_sinc3_map(false);
data.set_enh_filt_en(false);
data.set_order(DigitalFilterOrder::Sinc3);
data.set_odr(rate);
})
}
pub fn set_sinc3_fine_filter(&mut self, index: u8, rate: f32) -> Result<f32, SPI::Error> {
let sinc3_fine_odr_u16 = sinc3_fine_odr_closest(rate);
self.update_reg(&regs::FiltCon { index }, |data| {
data.set_sinc3_map(true);
data.set_sinc3_map_fine_odr_msb((sinc3_fine_odr_u16 >> 8 & 0xFF) as u8);
data.set_sinc3_map_fine_odr_lsb((sinc3_fine_odr_u16 & 0xFF) as u8);
})
.map(|_| sinc3_fine_odr_output_rate(sinc3_fine_odr_u16))
}
/// Returns the channel the data is from
pub fn data_ready(&mut self) -> Result<Option<u8>, SPI::Error> {
self.read_reg(&regs::Status)
.map(|status| if status.ready() { Some(status.channel()) } else { None })
}
/// Get data
pub fn read_data(&mut self) -> Result<u32, SPI::Error> {
self.read_reg(&regs::Data).map(|data| data.data())
}
fn read_reg<R: regs::Register>(&mut self, reg: &R) -> Result<R::Data, SPI::Error> {
let mut reg_data = R::Data::empty();
let address = 0x40 | reg.address();
let mut checksum = Checksum::new(self.checksum_mode);
checksum.feed(&[address]);
let checksum_out = checksum.result();
loop {
let checksum_in = self.transfer(address, reg_data.as_mut(), checksum_out)?;
checksum.feed(&reg_data);
let checksum_expected = checksum.result();
if checksum_expected == checksum_in {
break;
}
// Retry
warn!(
"read_reg {:02X}: checksum error: {:?}!={:?}, retrying",
reg.address(),
checksum_expected,
checksum_in
);
}
Ok(reg_data)
}
fn write_reg<R: regs::Register>(&mut self, reg: &R, reg_data: &mut R::Data) -> Result<(), SPI::Error> {
loop {
let address = reg.address();
let mut checksum = Checksum::new(match self.checksum_mode {
ChecksumMode::Off => ChecksumMode::Off,
// write checksums are always crc
ChecksumMode::Xor => ChecksumMode::Crc,
ChecksumMode::Crc => ChecksumMode::Crc,
});
checksum.feed(&[address]);
checksum.feed(&reg_data);
let checksum_out = checksum.result();
let mut data = reg_data.clone();
self.transfer(address, data.as_mut(), checksum_out)?;
// Verification
let readback_data = self.read_reg(reg)?;
if *readback_data == **reg_data {
return Ok(());
}
warn!(
"write_reg {:02X}: readback error, {:?}!={:?}, retrying",
address, &*readback_data, &**reg_data
);
}
}
fn update_reg<R, F, A>(&mut self, reg: &R, f: F) -> Result<A, SPI::Error>
where
R: regs::Register,
F: FnOnce(&mut R::Data) -> A,
{
let mut reg_data = self.read_reg(reg)?;
let result = f(&mut reg_data);
self.write_reg(reg, &mut reg_data)?;
Ok(result)
}
pub fn reset(&mut self) -> Result<(), SPI::Error> {
let mut buf = [0xFFu8; 8];
let _ = self.nss.set_low();
let result = self.spi.transfer_in_place(&mut buf);
let _ = self.nss.set_high();
result?;
Ok(())
}
fn transfer<'w>(
&mut self,
addr: u8,
reg_data: &'w mut [u8],
checksum: Option<u8>,
) -> Result<Option<u8>, SPI::Error> {
let mut addr_buf = [addr];
let _ = self.nss.set_low();
let result = match self.spi.transfer_in_place(&mut addr_buf) {
Ok(_) => self.spi.transfer_in_place(reg_data),
Err(e) => Err(e),
};
let result = match (result, checksum) {
(Ok(_), None) => Ok(None),
(Ok(_), Some(checksum_out)) => {
let mut checksum_buf = [checksum_out; 1];
match self.spi.transfer_in_place(&mut checksum_buf) {
Ok(_) => Ok(Some(checksum_buf[0])),
Err(e) => Err(e),
}
}
(Err(e), _) => Err(e),
};
let _ = self.nss.set_high();
result
}
}
#[derive(Debug, Clone)]
pub struct ChannelCalibration {
offset: u32,
gain: u32,
bipolar: bool,
}
impl Default for ChannelCalibration {
fn default() -> Self {
ChannelCalibration {
offset: 0,
gain: 0,
bipolar: false,
}
}
}
impl ChannelCalibration {
pub fn convert_data(&self, data: u32) -> ElectricPotential {
let data = if self.bipolar {
(data as i32 - 0x80_0000) as f32
} else {
data as f32 / 2.0
};
let data = data / (self.gain as f32 / (0x40_0000 as f32));
let data = data + (self.offset as i32 - 0x80_0000) as f32;
let data = data / (2 << 23) as f32;
const V_REF: f32 = 3.3;
ElectricPotential::new::<volt>(data * V_REF / 0.75)
}
}

View File

@ -1,60 +0,0 @@
#[derive(Clone, Copy, PartialEq)]
#[repr(u8)]
pub enum ChecksumMode {
Off = 0b00,
/// Seems much less reliable than `Crc`
Xor = 0b01,
Crc = 0b10,
}
impl From<u8> for ChecksumMode {
fn from(x: u8) -> Self {
match x {
0 => ChecksumMode::Off,
1 => ChecksumMode::Xor,
_ => ChecksumMode::Crc,
}
}
}
pub struct Checksum {
mode: ChecksumMode,
state: u8,
}
impl Checksum {
pub fn new(mode: ChecksumMode) -> Self {
Checksum { mode, state: 0 }
}
fn feed_byte(&mut self, input: u8) {
match self.mode {
ChecksumMode::Off => {}
ChecksumMode::Xor => self.state ^= input,
ChecksumMode::Crc => {
for i in 0..8 {
let input_mask = 0x80 >> i;
self.state = (self.state << 1)
^ if ((self.state & 0x80) != 0) != ((input & input_mask) != 0) {
0x07 /* x8 + x2 + x + 1 */
} else {
0
};
}
}
}
}
pub fn feed(&mut self, input: &[u8]) {
for &b in input {
self.feed_byte(b);
}
}
pub fn result(&self) -> Option<u8> {
match self.mode {
ChecksumMode::Off => None,
_ => Some(self.state),
}
}
}

View File

@ -1,350 +0,0 @@
use core::fmt;
use fugit::MegahertzU32;
use num_traits::float::Float;
use serde::{Deserialize, Serialize};
use stm32f4xx_hal::spi;
mod checksum;
pub mod regs;
pub use checksum::ChecksumMode;
mod adc;
pub use adc::*;
/// SPI Mode 3
pub const SPI_MODE: spi::Mode = spi::Mode {
polarity: spi::Polarity::IdleHigh,
phase: spi::Phase::CaptureOnSecondTransition,
};
/// AD7172 Max Frequency: 40MHz | SPI3 Max Frequency: 21MHz
pub const SPI_CLOCK_MHZ: MegahertzU32 = MegahertzU32::from_raw(21);
pub const MAX_VALUE: u32 = 0xFF_FFFF;
#[derive(Clone, Copy, Debug)]
#[repr(u8)]
pub enum Mode {
ContinuousConversion = 0b000,
SingleConversion = 0b001,
Standby = 0b010,
PowerDown = 0b011,
InternalOffsetCalibration = 0b100,
Invalid,
SystemOffsetCalibration = 0b110,
SystemGainCalibration = 0b111,
}
impl From<u8> for Mode {
fn from(x: u8) -> Self {
use Mode::*;
match x {
0b000 => ContinuousConversion,
0b001 => SingleConversion,
0b010 => Standby,
0b011 => PowerDown,
0b100 => InternalOffsetCalibration,
0b110 => SystemOffsetCalibration,
0b111 => SystemGainCalibration,
_ => Invalid,
}
}
}
#[derive(Clone, Copy, Debug)]
#[repr(u8)]
pub enum Input {
Ain0 = 0,
Ain1 = 1,
Ain2 = 2,
Ain3 = 3,
Ain4 = 4,
TemperaturePos = 17,
TemperatureNeg = 18,
AnalogSupplyPos = 19,
AnalogSupplyNeg = 20,
RefPos = 21,
RefNeg = 22,
Invalid = 0b11111,
}
impl From<u8> for Input {
fn from(x: u8) -> Self {
match x {
0 => Input::Ain0,
1 => Input::Ain1,
2 => Input::Ain2,
3 => Input::Ain3,
4 => Input::Ain4,
17 => Input::TemperaturePos,
18 => Input::TemperatureNeg,
19 => Input::AnalogSupplyPos,
20 => Input::AnalogSupplyNeg,
21 => Input::RefPos,
22 => Input::RefNeg,
_ => Input::Invalid,
}
}
}
impl fmt::Display for Input {
fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
use Input::*;
match self {
Ain0 => "ain0",
Ain1 => "ain1",
Ain2 => "ain2",
Ain3 => "ain3",
Ain4 => "ain4",
TemperaturePos => "temperature+",
TemperatureNeg => "temperature-",
AnalogSupplyPos => "analogsupply+",
AnalogSupplyNeg => "analogsupply-",
RefPos => "ref+",
RefNeg => "ref-",
_ => "<INVALID>",
}
.fmt(fmt)
}
}
/// Reference source for ADC conversion
#[repr(u8)]
pub enum RefSource {
/// External reference
External = 0b00,
/// Internal 2.5V reference
Internal = 0b10,
/// AVDD1 AVSS
Avdd1MinusAvss = 0b11,
Invalid = 0b01,
}
impl From<u8> for RefSource {
fn from(x: u8) -> Self {
match x {
0 => RefSource::External,
1 => RefSource::Internal,
2 => RefSource::Avdd1MinusAvss,
_ => RefSource::Invalid,
}
}
}
impl fmt::Display for RefSource {
fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
use RefSource::*;
match self {
External => "external",
Internal => "internal",
Avdd1MinusAvss => "avdd1-avss",
_ => "<INVALID>",
}
.fmt(fmt)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[repr(u8)]
pub enum PostFilter {
/// 27 SPS, 47 dB rejection, 36.7 ms settling
F27SPS = 0b010,
/// 21.25 SPS, 62 dB rejection, 40 ms settling
F21SPS = 0b011,
/// 20 SPS, 86 dB rejection, 50 ms settling
F20SPS = 0b101,
/// 16.67 SPS, 92 dB rejection, 60 ms settling
F16SPS = 0b110,
Invalid = 0b111,
}
impl PostFilter {
pub const VALID_VALUES: &'static [Self] = &[
PostFilter::F27SPS,
PostFilter::F21SPS,
PostFilter::F20SPS,
PostFilter::F16SPS,
];
pub fn closest(rate: f32) -> Option<Self> {
let mut best: Option<(f32, Self)> = None;
for value in Self::VALID_VALUES {
let error = (rate - value.output_rate().unwrap()).abs();
let better = best.map(|(best_error, _)| error < best_error).unwrap_or(true);
if better {
best = Some((error, *value));
}
}
best.map(|(_, best)| best)
}
/// Samples per Second
pub fn output_rate(&self) -> Option<f32> {
match self {
PostFilter::F27SPS => Some(27.0),
PostFilter::F21SPS => Some(21.25),
PostFilter::F20SPS => Some(20.0),
PostFilter::F16SPS => Some(16.67),
PostFilter::Invalid => None,
}
}
}
impl From<u8> for PostFilter {
fn from(x: u8) -> Self {
match x {
0b010 => PostFilter::F27SPS,
0b011 => PostFilter::F21SPS,
0b101 => PostFilter::F20SPS,
0b110 => PostFilter::F16SPS,
_ => PostFilter::Invalid,
}
}
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Default)]
pub enum FilterType {
#[default]
Sinc5Sinc1With50hz60HzRejection,
Sinc5Sinc1,
Sinc3,
Sinc3WithFineODR,
}
#[derive(PartialEq)]
#[repr(u8)]
pub enum DigitalFilterOrder {
Sinc5Sinc1 = 0b00,
Sinc3 = 0b11,
Invalid = 0b10,
}
impl From<u8> for DigitalFilterOrder {
fn from(x: u8) -> Self {
match x {
0b00 => DigitalFilterOrder::Sinc5Sinc1,
0b11 => DigitalFilterOrder::Sinc3,
_ => DigitalFilterOrder::Invalid,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[allow(unused)]
#[repr(u8)]
pub enum SingleChODR {
F31250_0SPS = 0b00101,
F15625_0SPS = 0b00110,
F10417_0SPS = 0b00111,
F5208_0SPS = 0b01000,
F2597_0SPS = 0b01001,
F1007_0SPS = 0b01010,
F503_8SPS = 0b01011,
F381_0SPS = 0b01100,
F200_3SPS = 0b01101,
F100_2SPS = 0b01110,
F59_52SPS = 0b01111,
F49_68SPS = 0b10000,
F20_01SPS = 0b10001,
F16_63SPS = 0b10010,
F10_0SPS = 0b10011,
F5_0SPS = 0b10100,
F2_5SPS = 0b10101,
F1_25SPS = 0b10110,
Invalid = 0b11111,
}
impl SingleChODR {
pub const VALID_VALUES: &'static [Self] = &[
SingleChODR::F31250_0SPS,
SingleChODR::F15625_0SPS,
SingleChODR::F10417_0SPS,
SingleChODR::F5208_0SPS,
SingleChODR::F2597_0SPS,
SingleChODR::F1007_0SPS,
SingleChODR::F503_8SPS,
SingleChODR::F381_0SPS,
SingleChODR::F200_3SPS,
SingleChODR::F100_2SPS,
SingleChODR::F59_52SPS,
SingleChODR::F49_68SPS,
SingleChODR::F20_01SPS,
SingleChODR::F16_63SPS,
SingleChODR::F10_0SPS,
SingleChODR::F5_0SPS,
SingleChODR::F2_5SPS,
SingleChODR::F1_25SPS,
];
pub fn closest(rate: f32) -> Option<Self> {
let mut best: Option<(f32, Self)> = None;
for value in Self::VALID_VALUES {
let error = (rate - value.output_rate().unwrap()).abs();
let better = best.map(|(best_error, _)| error < best_error).unwrap_or(true);
if better {
best = Some((error, *value));
}
}
best.map(|(_, best)| best)
}
/// Samples per Second
pub fn output_rate(&self) -> Option<f32> {
match self {
SingleChODR::F31250_0SPS => Some(31250.0),
SingleChODR::F15625_0SPS => Some(15625.0),
SingleChODR::F10417_0SPS => Some(10417.0),
SingleChODR::F5208_0SPS => Some(5208.0),
SingleChODR::F2597_0SPS => Some(2597.0),
SingleChODR::F1007_0SPS => Some(1007.0),
SingleChODR::F503_8SPS => Some(503.8),
SingleChODR::F381_0SPS => Some(381.0),
SingleChODR::F200_3SPS => Some(200.3),
SingleChODR::F100_2SPS => Some(100.2),
SingleChODR::F59_52SPS => Some(59.52),
SingleChODR::F49_68SPS => Some(49.68),
SingleChODR::F20_01SPS => Some(20.01),
SingleChODR::F16_63SPS => Some(16.63),
SingleChODR::F10_0SPS => Some(10.0),
SingleChODR::F5_0SPS => Some(5.0),
SingleChODR::F2_5SPS => Some(2.5),
SingleChODR::F1_25SPS => Some(1.25),
SingleChODR::Invalid => None,
}
}
}
impl From<u8> for SingleChODR {
fn from(x: u8) -> Self {
match x {
0b00101 => SingleChODR::F31250_0SPS,
0b00110 => SingleChODR::F15625_0SPS,
0b00111 => SingleChODR::F10417_0SPS,
0b01000 => SingleChODR::F5208_0SPS,
0b01001 => SingleChODR::F2597_0SPS,
0b01010 => SingleChODR::F1007_0SPS,
0b01011 => SingleChODR::F503_8SPS,
0b01100 => SingleChODR::F381_0SPS,
0b01101 => SingleChODR::F200_3SPS,
0b01110 => SingleChODR::F100_2SPS,
0b01111 => SingleChODR::F59_52SPS,
0b10000 => SingleChODR::F49_68SPS,
0b10001 => SingleChODR::F20_01SPS,
0b10010 => SingleChODR::F16_63SPS,
0b10011 => SingleChODR::F10_0SPS,
0b10100 => SingleChODR::F5_0SPS,
0b10101 => SingleChODR::F2_5SPS,
0b10110 => SingleChODR::F1_25SPS,
_ => SingleChODR::Invalid,
}
}
}
pub fn sinc3_fine_odr_output_rate(odr: u16) -> f32 {
1.0 * 1e6 / (32.0 * odr as f32)
}
pub fn sinc3_fine_odr_closest(rate: f32) -> u16 {
(1.0e6 / (32.0 * rate)).max(1.0 as f32).min(0x7FFF as f32) as u16
}

View File

@ -1,342 +0,0 @@
use core::ops::{Deref, DerefMut};
use bit_field::BitField;
use byteorder::{BigEndian, ByteOrder};
use super::*;
pub trait Register {
type Data: RegisterData;
fn address(&self) -> u8;
}
pub trait RegisterData: Clone + Deref<Target = [u8]> + DerefMut {
fn empty() -> Self;
}
macro_rules! def_reg {
($Reg:ident, $reg:ident, $addr:expr, $size:expr) => {
/// AD7172 register
pub struct $Reg;
impl Register for $Reg {
/// Register contents
type Data = $reg::Data;
/// Register address
fn address(&self) -> u8 {
$addr
}
}
mod $reg {
/// Register contents
#[derive(Clone)]
pub struct Data(pub [u8; $size]);
impl super::RegisterData for Data {
/// Generate zeroed register contents
fn empty() -> Self {
Data([0; $size])
}
}
impl core::ops::Deref for Data {
type Target = [u8];
fn deref(&self) -> &[u8] {
&self.0
}
}
impl core::ops::DerefMut for Data {
fn deref_mut(&mut self) -> &mut [u8] {
&mut self.0
}
}
}
};
($Reg:ident,u8, $reg:ident, $addr:expr, $size:expr) => {
pub struct $Reg {
pub index: u8,
}
impl Register for $Reg {
type Data = $reg::Data;
fn address(&self) -> u8 {
$addr + self.index
}
}
mod $reg {
#[derive(Clone)]
pub struct Data(pub [u8; $size]);
impl super::RegisterData for Data {
fn empty() -> Self {
Data([0; $size])
}
}
impl core::ops::Deref for Data {
type Target = [u8];
fn deref(&self) -> &[u8] {
&self.0
}
}
impl core::ops::DerefMut for Data {
fn deref_mut(&mut self) -> &mut [u8] {
&mut self.0
}
}
}
};
}
macro_rules! reg_bit {
($getter:ident, $byte:expr, $bit:expr, $doc:expr) => {
#[allow(unused)]
#[doc = $doc]
pub fn $getter(&self) -> bool {
self.0[$byte].get_bit($bit)
}
};
($getter:ident, $setter:ident, $byte:expr, $bit:expr, $doc:expr) => {
#[allow(unused)]
#[doc = $doc]
pub fn $getter(&self) -> bool {
self.0[$byte].get_bit($bit)
}
#[allow(unused)]
#[doc = $doc]
pub fn $setter(&mut self, value: bool) {
self.0[$byte].set_bit($bit, value);
}
};
}
macro_rules! reg_bits {
($getter:ident, $byte:expr, $bits:expr, $doc:expr) => {
#[allow(unused)]
#[doc = $doc]
pub fn $getter(&self) -> u8 {
self.0[$byte].get_bits($bits)
}
};
($getter:ident, $setter:ident, $byte:expr, $bits:expr, $doc:expr) => {
#[allow(unused)]
#[doc = $doc]
pub fn $getter(&self) -> u8 {
self.0[$byte].get_bits($bits)
}
#[allow(unused)]
#[doc = $doc]
pub fn $setter(&mut self, value: u8) {
self.0[$byte].set_bits($bits, value);
}
};
($getter:ident, $byte:expr, $bits:expr, $ty:ty, $doc:expr) => {
#[allow(unused)]
#[doc = $doc]
pub fn $getter(&self) -> $ty {
self.0[$byte].get_bits($bits) as $ty
}
};
($getter:ident, $setter:ident, $byte:expr, $bits:expr, $ty:ty, $doc:expr) => {
#[allow(unused)]
#[doc = $doc]
pub fn $getter(&self) -> $ty {
self.0[$byte].get_bits($bits).into()
}
#[allow(unused)]
#[doc = $doc]
pub fn $setter(&mut self, value: $ty) {
self.0[$byte].set_bits($bits, value as u8);
}
};
}
def_reg!(Status, status, 0x00, 1);
impl status::Data {
/// Is there new data to read?
pub fn ready(&self) -> bool {
!self.not_ready()
}
reg_bit!(not_ready, 0, 7, "No data ready indicator");
reg_bits!(channel, 0, 0..=1, "Channel for which data is ready");
reg_bit!(adc_error, 0, 6, "ADC error");
reg_bit!(crc_error, 0, 5, "SPI CRC error");
reg_bit!(reg_error, 0, 4, "Register error");
}
def_reg!(AdcMode, adc_mode, 0x01, 2);
impl adc_mode::Data {
reg_bits!(delay, set_delay, 0, 0..=2, "Delay after channel switch");
reg_bit!(sing_cyc, set_sing_cyc, 0, 5, "Can only used with single channel");
reg_bit!(hide_delay, set_hide_delay, 0, 6, "Hide delay");
reg_bit!(
ref_en,
set_ref_en,
0,
7,
"Enable internal reference, output buffered 2.5 V to REFOUT"
);
reg_bits!(clockset, set_clocksel, 1, 2..=3, "Clock source");
reg_bits!(mode, set_mode, 1, 4..=6, Mode, "Operating mode");
}
def_reg!(IfMode, if_mode, 0x02, 2);
impl if_mode::Data {
reg_bits!(crc, set_crc, 1, 2..=3, ChecksumMode, "SPI checksum mode");
}
def_reg!(Data, data, 0x04, 3);
impl data::Data {
pub fn data(&self) -> u32 {
(u32::from(self.0[0]) << 16) | (u32::from(self.0[1]) << 8) | u32::from(self.0[2])
}
}
def_reg!(GpioCon, gpio_con, 0x06, 2);
impl gpio_con::Data {
reg_bit!(sync_en, set_sync_en, 0, 3, "Enables the SYNC/ERROR pin as a sync input");
}
def_reg!(Id, id, 0x07, 2);
impl id::Data {
pub fn id(&self) -> u16 {
BigEndian::read_u16(&self.0)
}
}
def_reg!(Channel, u8, channel, 0x10, 2);
impl channel::Data {
reg_bit!(enabled, set_enabled, 0, 7, "Channel enabled");
reg_bits!(setup, set_setup, 0, 4..=5, "Setup number");
/// Which input is connected to positive input of this channel
#[allow(unused)]
pub fn a_in_pos(&self) -> Input {
((self.0[0].get_bits(0..=1) << 3) | self.0[1].get_bits(5..=7)).into()
}
/// Set which input is connected to positive input of this channel
#[allow(unused)]
pub fn set_a_in_pos(&mut self, value: Input) {
let value = value as u8;
self.0[0].set_bits(0..=1, value >> 3);
self.0[1].set_bits(5..=7, value & 0x7);
}
reg_bits!(
a_in_neg,
set_a_in_neg,
1,
0..=4,
Input,
"Which input is connected to negative input of this channel"
);
}
def_reg!(SetupCon, u8, setup_con, 0x20, 2);
impl setup_con::Data {
reg_bit!(
bipolar,
set_bipolar,
0,
4,
"Unipolar (`false`) or bipolar (`true`) coded output"
);
reg_bit!(refbuf_pos, set_refbuf_pos, 0, 3, "Enable REF+ input buffer");
reg_bit!(refbuf_neg, set_refbuf_neg, 0, 2, "Enable REF- input buffer");
reg_bit!(ainbuf_pos, set_ainbuf_pos, 0, 1, "Enable AIN+ input buffer");
reg_bit!(ainbuf_neg, set_ainbuf_neg, 0, 0, "Enable AIN- input buffer");
reg_bit!(
burnout_en,
1,
7,
"enables a 10 µA current source on the positive analog input selected and a 10 µA current sink on the \
negative analog input selected"
);
reg_bits!(
ref_sel,
set_ref_sel,
1,
4..=5,
RefSource,
"Select reference source for conversion"
);
}
def_reg!(FiltCon, u8, filt_con, 0x28, 2);
impl filt_con::Data {
reg_bit!(
sinc3_map,
set_sinc3_map,
0,
7,
"If set, Sinc3 Filter's notch frequency rejection position can be fine tuned with FiltCon[14:0]. Best to be \
used with Single Channel Enabled"
);
reg_bit!(
enh_filt_en,
set_enh_filt_en,
0,
3,
"Enable postfilters for enhanced 50Hz and 60Hz rejection"
);
reg_bits!(
enh_filt,
set_enh_filt,
0,
0..=2,
PostFilter,
"Select postfilters output data rate 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,
SingleChODR,
"Output data rate for normal Sin5c + Sinc1 and Sinc3 filter with SING_CYC = 0 and Single Channel Enabled"
);
reg_bits!(
sinc3_map_fine_odr_msb,
set_sinc3_map_fine_odr_msb,
0,
0..=6,
"MSB Byte Sinc3 Fine Output Config"
);
reg_bits!(
sinc3_map_fine_odr_lsb,
set_sinc3_map_fine_odr_lsb,
1,
0..=7,
"LSB Byte Sinc3 Fine Output Config"
);
}
def_reg!(Offset, u8, offset, 0x30, 3);
impl offset::Data {
#[allow(unused)]
pub fn offset(&self) -> u32 {
(u32::from(self.0[0]) << 16) | (u32::from(self.0[1]) << 8) | u32::from(self.0[2])
}
#[allow(unused)]
pub fn set_offset(&mut self, value: u32) {
self.0[0] = (value >> 16) as u8;
self.0[1] = (value >> 8) as u8;
self.0[2] = value as u8;
}
}
def_reg!(Gain, u8, gain, 0x38, 3);
impl gain::Data {
#[allow(unused)]
pub fn gain(&self) -> u32 {
(u32::from(self.0[0]) << 16) | (u32::from(self.0[1]) << 8) | u32::from(self.0[2])
}
#[allow(unused)]
pub fn set_gain(&mut self, value: u32) {
self.0[0] = (value >> 16) as u8;
self.0[1] = (value >> 8) as u8;
self.0[2] = value as u8;
}
}

View File

@ -1,397 +0,0 @@
use core::ptr::addr_of_mut;
use fugit::KilohertzU32;
use stm32f4xx_hal::{adc::{config::{self, AdcConfig},
Adc},
dma::{config::DmaConfig, PeripheralToMemory, Stream2, StreamsTuple, Transfer as DMA_Transfer},
gpio::{gpioa::*, gpiob::*, gpioc::*, Analog, Output, PushPull},
hal::{digital::OutputPin, spi::SpiBus},
interrupt,
pac::{Peripherals, ADC1, ADC2, DMA2, NVIC, SPI1, TIM4},
spi::Spi,
timer::pwm::PwmChannel};
use uom::si::{electric_potential::millivolt, f32::ElectricPotential, ratio::ratio};
use crate::thermostat::ad5680;
pub const PWM_FREQ_KHZ: KilohertzU32 = KilohertzU32::from_raw(20);
pub trait ChannelPins {
type DacSpi: SpiBus<u8>;
type DacSync: OutputPin;
type ShdnPin: OutputPin;
type VRefPin;
type ItecPin;
type DacFeedbackPin;
type VTecPin;
type MaxVPin;
type MaxIPosPin;
type MAXINegPin;
}
pub struct Channel0;
impl ChannelPins for Channel0 {
type DacSpi = DacSpi;
type DacSync = DacSync;
type ShdnPin = PA5<Output<PushPull>>;
type VRefPin = PA6<Analog>;
type ItecPin = PB1<Analog>;
type DacFeedbackPin = PC0<Analog>;
type VTecPin = PB0<Analog>;
type MaxVPin = PwmChannel<TIM4, 1>;
type MaxIPosPin = PwmChannel<TIM4, 2>;
type MAXINegPin = PwmChannel<TIM4, 0>;
}
pub struct MAX1968Phy<C: ChannelPins> {
pub dac: ad5680::Dac<C::DacSpi, C::DacSync>,
pub shdn: C::ShdnPin,
pub vref_pin: C::VRefPin,
pub itec_pin: C::ItecPin,
pub dac_feedback_pin: C::DacFeedbackPin,
pub vtec_pin: C::VTecPin,
pub max_v: C::MaxVPin,
pub max_i_pos: C::MaxIPosPin,
pub max_i_neg: C::MAXINegPin,
}
pub struct MAX1968PinSet<C: ChannelPins> {
pub dac: ad5680::Dac<C::DacSpi, C::DacSync>,
pub shdn: C::ShdnPin,
pub vref_pin: C::VRefPin,
pub itec_pin: C::ItecPin,
pub dac_feedback_pin: C::DacFeedbackPin,
pub vtec_pin: C::VTecPin,
pub max_v: C::MaxVPin,
pub max_i_pos: C::MaxIPosPin,
pub max_i_neg: C::MAXINegPin,
}
type DacSpi = Spi<SPI1>;
type DacSync = PB4<Output<PushPull>>;
static mut DMA_TRANSFER_COMPLETE: bool = true;
pub struct MAX1968 {
pub phy: MAX1968Phy<Channel0>,
pub pins_adc: Adc<ADC1>,
pub dma_adc: DMA_Transfer<Stream2<DMA2>, 1, Adc<ADC2>, PeripheralToMemory, &'static mut [u16; 16]>,
prev_vtec_volt: ElectricPotential,
prev_itec_volt: ElectricPotential,
}
pub enum PwmPinsEnum {
MaxV,
MaxPosI,
MaxNegI,
}
#[allow(unused)]
pub enum AdcReadTarget {
VREF,
DacVfb,
ITec,
VTec,
}
impl<C: ChannelPins> MAX1968Phy<C> {
pub fn new(pins: MAX1968PinSet<C>) -> Self {
MAX1968Phy {
dac: pins.dac,
shdn: pins.shdn,
vref_pin: pins.vref_pin,
itec_pin: pins.itec_pin,
dac_feedback_pin: pins.dac_feedback_pin,
vtec_pin: pins.vtec_pin,
max_v: pins.max_v,
max_i_pos: pins.max_i_pos,
max_i_neg: pins.max_i_neg,
}
}
}
static mut ADC2_FIRST_BUFFER: [u16; 16] = [0; 16];
static mut ADC2_LOCAL_BUFFER: [u16; 16] = [0; 16];
impl MAX1968 {
pub fn new(phy_ch0: MAX1968Phy<Channel0>, adc1: ADC1, adc2: ADC2, dma2: DMA2) -> Self {
let adc_config = AdcConfig::default()
.clock(config::Clock::Pclk2_div_8)
.default_sample_time(config::SampleTime::Cycles_480);
// Do not set reset RCCs as it causes other ADCs' clock to be disabled
let mut pins_adc1 = Adc::adc1(adc1, false, adc_config);
// adc1.calibrate() fn only read REFINT once to assign the calibration value.
// It does not take the STM32F4's ADC Precision Limitation into account.
// AN4073: ADC Reading Dispersion can be reduced through Averaging
let mut vdda_mv: u32 = 0;
for _ in (0..512).rev() {
pins_adc1.calibrate();
vdda_mv += pins_adc1.reference_voltage();
}
vdda_mv = vdda_mv / 512 as u32;
pins_adc1.apply_config(adc_config.reference_voltage(vdda_mv));
let adc_config = AdcConfig::default()
.clock(config::Clock::Pclk2_div_8)
.default_sample_time(config::SampleTime::Cycles_480)
.dma(config::Dma::Continuous)
.scan(config::Scan::Enabled)
.reference_voltage(pins_adc1.reference_voltage());
let dma_config = DmaConfig::default()
.transfer_complete_interrupt(true)
.memory_increment(true)
.double_buffer(false);
let mut pins_adc2 = Adc::adc2(adc2, false, adc_config);
pins_adc2.configure_channel(&phy_ch0.itec_pin, config::Sequence::One, config::SampleTime::Cycles_480);
pins_adc2.configure_channel(&phy_ch0.vtec_pin, config::Sequence::Two, config::SampleTime::Cycles_480);
pins_adc2.configure_channel(
&phy_ch0.itec_pin,
config::Sequence::Three,
config::SampleTime::Cycles_480,
);
pins_adc2.configure_channel(
&phy_ch0.vtec_pin,
config::Sequence::Four,
config::SampleTime::Cycles_480,
);
pins_adc2.configure_channel(
&phy_ch0.itec_pin,
config::Sequence::Five,
config::SampleTime::Cycles_480,
);
pins_adc2.configure_channel(&phy_ch0.vtec_pin, config::Sequence::Six, config::SampleTime::Cycles_480);
pins_adc2.configure_channel(
&phy_ch0.itec_pin,
config::Sequence::Seven,
config::SampleTime::Cycles_480,
);
pins_adc2.configure_channel(
&phy_ch0.vtec_pin,
config::Sequence::Eight,
config::SampleTime::Cycles_480,
);
pins_adc2.configure_channel(
&phy_ch0.itec_pin,
config::Sequence::Nine,
config::SampleTime::Cycles_480,
);
pins_adc2.configure_channel(&phy_ch0.vtec_pin, config::Sequence::Ten, config::SampleTime::Cycles_480);
pins_adc2.configure_channel(
&phy_ch0.itec_pin,
config::Sequence::Eleven,
config::SampleTime::Cycles_480,
);
pins_adc2.configure_channel(
&phy_ch0.vtec_pin,
config::Sequence::Twelve,
config::SampleTime::Cycles_480,
);
pins_adc2.configure_channel(
&phy_ch0.itec_pin,
config::Sequence::Thirteen,
config::SampleTime::Cycles_480,
);
pins_adc2.configure_channel(
&phy_ch0.vtec_pin,
config::Sequence::Fourteen,
config::SampleTime::Cycles_480,
);
pins_adc2.configure_channel(
&phy_ch0.itec_pin,
config::Sequence::Fifteen,
config::SampleTime::Cycles_480,
);
pins_adc2.configure_channel(
&phy_ch0.vtec_pin,
config::Sequence::Sixteen,
config::SampleTime::Cycles_480,
);
let dma = StreamsTuple::new(dma2);
let dma_adc: DMA_Transfer<Stream2<DMA2>, 1, Adc<ADC2>, PeripheralToMemory, &'static mut [u16; 16]>;
unsafe {
dma_adc = DMA_Transfer::init_peripheral_to_memory(
dma.2,
pins_adc2,
addr_of_mut!(ADC2_FIRST_BUFFER).as_mut().unwrap(),
None,
dma_config,
);
NVIC::unmask(interrupt::DMA2_STREAM2);
}
MAX1968 {
phy: phy_ch0,
pins_adc: pins_adc1,
dma_adc: dma_adc,
prev_vtec_volt: ElectricPotential::new::<millivolt>(0.0),
prev_itec_volt: ElectricPotential::new::<millivolt>(0.0),
}
}
pub fn dma_adc_start_conversion(&mut self) {
if unsafe { DMA_TRANSFER_COMPLETE } {
unsafe {
DMA_TRANSFER_COMPLETE = false;
}
self.dma_adc.start(|adc| {
adc.clear_end_of_conversion_flag();
adc.start_conversion();
});
}
}
pub fn get_tec_readings(&mut self) -> (ElectricPotential, ElectricPotential) {
if unsafe { DMA_TRANSFER_COMPLETE } {
let buffer: &[u16; 16];
unsafe {
(buffer, _) = self
.dma_adc
.next_transfer(addr_of_mut!(ADC2_LOCAL_BUFFER).as_mut().unwrap())
.unwrap();
}
let sample_to_millivolts = self.dma_adc.peripheral().make_sample_to_millivolts();
let mut itec: u16 = 0;
for data in buffer.into_iter().step_by(2) {
itec += *data;
}
itec = itec >> 3;
let mut vtec: u16 = 0;
for data in buffer.into_iter().skip(1).step_by(2) {
vtec += *data;
}
vtec = vtec >> 3;
unsafe {
ADC2_LOCAL_BUFFER = *buffer;
}
self.prev_vtec_volt = ElectricPotential::new::<millivolt>(sample_to_millivolts(vtec) as f32);
self.prev_itec_volt = ElectricPotential::new::<millivolt>(sample_to_millivolts(itec) as f32);
}
(self.prev_vtec_volt, self.prev_itec_volt)
}
// Return the calibrated VDDA Voltage
// Can be used to set reference voltage for other ADC
pub fn get_calibrated_vdda(&mut self) -> u32 {
self.pins_adc.reference_voltage()
}
pub fn is_powered_on(&mut self) -> bool {
self.phy.shdn.is_set_high()
}
pub fn power_down(&mut self) {
self.phy.shdn.set_low();
}
pub fn power_up(&mut self) {
self.phy.shdn.set_high();
}
pub fn set_dac(&mut self, voltage: ElectricPotential, dac_out_v_max: ElectricPotential) -> ElectricPotential {
let value = ((voltage / dac_out_v_max).get::<ratio>() * (ad5680::MAX_VALUE as f32)) as u32;
self.phy.dac.set(value).unwrap();
voltage
}
// AN4073: ADC Reading Dispersion can be reduced through Averaging
// Upon test, 16 Point Averaging = +-3 LSB Dispersion
pub fn adc_read(&mut self, adc_read_target: AdcReadTarget, avg_pt: u16) -> ElectricPotential {
let mut sample: u32 = 0;
sample = match adc_read_target {
AdcReadTarget::VREF => {
for _ in (0..avg_pt).rev() {
sample += self
.pins_adc
.convert(&self.phy.vref_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
as u32;
}
sample / avg_pt as u32
}
AdcReadTarget::DacVfb => {
for _ in (0..avg_pt).rev() {
sample += self.pins_adc.convert(
&self.phy.dac_feedback_pin,
stm32f4xx_hal::adc::config::SampleTime::Cycles_480,
) as u32;
}
sample / avg_pt as u32
}
AdcReadTarget::ITec => {
for _ in (0..avg_pt).rev() {
sample += self
.pins_adc
.convert(&self.phy.itec_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
as u32;
}
sample / avg_pt as u32
}
AdcReadTarget::VTec => {
for _ in (0..avg_pt).rev() {
sample += self
.pins_adc
.convert(&self.phy.vtec_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
as u32;
}
sample / avg_pt as u32
}
};
let mv = self.pins_adc.sample_to_millivolts(sample as u16);
ElectricPotential::new::<millivolt>(mv as f32)
}
pub fn set_pwm(&mut self, pwm_pin: PwmPinsEnum, duty: f64, max_duty: f64) -> f64 {
fn duty_cycle_value(duty_cycle: f64, duty_cycle_limit: f64, max_value: u16) -> u16 {
((duty_cycle.min(duty_cycle_limit) * (max_value as f64)) as u16).min(max_value)
}
let value: u16;
let max_value: u16;
match pwm_pin {
PwmPinsEnum::MaxV => {
self.phy.max_v.enable();
max_value = self.phy.max_v.get_max_duty();
value = duty_cycle_value(duty, max_duty, max_value);
self.phy.max_v.set_duty(value);
}
PwmPinsEnum::MaxPosI => {
self.phy.max_i_pos.enable();
max_value = self.phy.max_i_pos.get_max_duty();
value = duty_cycle_value(duty, max_duty, max_value);
self.phy.max_i_pos.set_duty(value);
}
PwmPinsEnum::MaxNegI => {
self.phy.max_i_neg.enable();
max_value = self.phy.max_i_neg.get_max_duty();
value = duty_cycle_value(duty, max_duty, max_value);
self.phy.max_i_neg.set_duty(value);
}
}
return (value as f64) / (max_value as f64);
}
}
#[interrupt]
fn DMA2_STREAM2() {
cortex_m::interrupt::free(|_| {
unsafe {
// Clear all DMA2_STREAM2 interrupt flags
Peripherals::steal().DMA2.lifcr.write(|w| {
w.ctcif2()
.set_bit()
.cdmeif2()
.set_bit()
.chtif2()
.set_bit()
.cteif2()
.set_bit()
});
DMA_TRANSFER_COMPLETE = true;
}
})
}

View File

@ -1,7 +0,0 @@
pub mod ad5680;
pub mod ad7172;
pub mod max1968;
pub mod pid_state;
pub mod steinhart_hart;
pub mod temp_mon;
pub mod thermostat;

View File

@ -1,216 +0,0 @@
use miniconf::Tree;
use serde::{Deserialize, Serialize};
use uom::si::{electric_potential::volt,
electrical_resistance::ohm,
f32::{ElectricPotential, ElectricalResistance, ThermodynamicTemperature},
thermodynamic_temperature::degree_celsius};
use crate::thermostat::{ad7172, steinhart_hart as sh};
const R_INNER: f32 = 2.0 * 5100.0;
const VREF_SENS: f32 = 3.3 / 2.0;
#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Tree)]
pub struct Parameters {
/// Gain coefficient for proportional term
pub kp: f32,
/// Gain coefficient for integral term
pub ki: f32,
/// Gain coefficient for derivative term
pub kd: f32,
/// Output limit minimum
pub output_min: f32,
/// Output limit maximum
pub output_max: f32,
}
impl Default for Parameters {
fn default() -> Self {
Parameters {
kp: 0.0,
ki: 0.0,
kd: 0.0,
output_min: -1.0,
output_max: 1.0,
}
}
}
#[derive(Clone)]
pub struct Controller {
pub parameters: Parameters,
u1: f64,
x1: f64,
x2: f64,
pub y1: f64,
}
pub struct PidState {
adc_data: Option<u32>,
adc_calibration: ad7172::ChannelCalibration,
pid_engaged: bool,
set_point: ThermodynamicTemperature,
sh: sh::Parameters,
controller: Controller,
}
impl Default for PidState {
fn default() -> Self {
PidState {
adc_data: None,
adc_calibration: ad7172::ChannelCalibration::default(),
pid_engaged: false,
set_point: ThermodynamicTemperature::new::<degree_celsius>(0.0),
sh: sh::Parameters::default(),
controller: Controller {
parameters: Parameters::default(),
u1: 0.0,
x1: 0.0,
x2: 0.0,
y1: 0.0,
},
}
}
}
pub enum PidSettings {
Kp,
Ki,
Kd,
Min,
Max,
}
impl PidState {
pub fn update(&mut self, adc_data: u32) {
self.adc_data = if adc_data == ad7172::MAX_VALUE {
// this means there is no thermistor plugged into the ADC.
None
} else {
Some(adc_data)
};
}
// Based on https://hackmd.io/IACbwcOTSt6Adj3_F9bKuw PID implementation
// Input x(t), target u(t), output y(t)
// y0' = y1 - ki * u0
// + x0 * (kp + ki + kd)
// - x1 * (kp + 2kd)
// + x2 * kd
// y0 = clip(y0', ymin, ymax)
pub fn update_pid(&mut self) -> Option<f64> {
let input = self.get_temperature()?.get::<degree_celsius>();
let setpoint = self.set_point.get::<degree_celsius>();
let mut output: f64 = self.controller.y1 - setpoint as f64 * f64::from(self.controller.parameters.ki)
+ input as f64
* f64::from(
self.controller.parameters.kp + self.controller.parameters.ki + self.controller.parameters.kd,
)
- self.controller.x1 * f64::from(self.controller.parameters.kp + 2.0 * self.controller.parameters.kd)
+ self.controller.x2 * f64::from(self.controller.parameters.kd);
if output < self.controller.parameters.output_min.into() {
output = self.controller.parameters.output_min.into();
}
if output > self.controller.parameters.output_max.into() {
output = self.controller.parameters.output_max.into();
}
self.controller.x2 = self.controller.x1;
self.controller.x1 = input as f64;
self.controller.u1 = setpoint as f64;
self.controller.y1 = output;
Some(output)
}
pub fn get_adc(&self) -> Option<ElectricPotential> {
Some(self.adc_calibration.convert_data(self.adc_data?))
}
/// Get `SENS[01]` input resistance
pub fn get_sens(&self) -> Option<ElectricalResistance> {
let r_inner = ElectricalResistance::new::<ohm>(R_INNER);
let vref = ElectricPotential::new::<volt>(VREF_SENS);
let adc_input = self.get_adc()?;
let r = r_inner * adc_input / (vref - adc_input);
Some(r)
}
pub fn get_temperature(&self) -> Option<ThermodynamicTemperature> {
let r = self.get_sens()?;
let temperature = self.sh.get_temperature(r);
Some(temperature)
}
pub fn apply_pid_params(&mut self, pid_params: Parameters) {
self.controller.parameters = pid_params;
}
pub fn set_pid_params(&mut self, param: PidSettings, val: f32) {
match param {
PidSettings::Kp => {
self.controller.parameters.kp = val;
}
PidSettings::Ki => {
self.controller.parameters.ki = val;
}
PidSettings::Kd => {
self.controller.parameters.kd = val;
}
PidSettings::Min => {
self.controller.parameters.output_min = val;
}
PidSettings::Max => {
self.controller.parameters.output_max = val;
}
}
}
pub fn reset_pid_state(&mut self) {
self.controller.u1 = 0.0;
self.controller.x1 = 0.0;
self.controller.x2 = 0.0;
self.controller.y1 = 0.0;
}
pub fn set_pid_setpoint(&mut self, temperature: ThermodynamicTemperature) {
self.set_point = temperature;
}
pub fn get_pid_setpoint(&mut self) -> ThermodynamicTemperature {
self.set_point
}
pub fn set_sh_t0(&mut self, t0: ThermodynamicTemperature) {
self.sh.t0 = t0
}
pub fn set_sh_r0(&mut self, r0: ElectricalResistance) {
self.sh.r0 = r0
}
pub fn set_sh_beta(&mut self, beta: f32) {
self.sh.b = beta
}
pub fn set_adc_calibration(&mut self, adc_cal: ad7172::ChannelCalibration) {
self.adc_calibration = adc_cal;
}
pub fn set_pid_engaged(&mut self, pid_engaged: bool) {
self.pid_engaged = pid_engaged;
}
pub fn get_pid_engaged(&mut self) -> bool {
self.pid_engaged
}
pub fn get_pid_settings(&mut self) -> Parameters {
self.controller.parameters
}
pub fn get_sh(&mut self) -> sh::Parameters {
self.sh
}
pub fn apply_sh(&mut self, sh: sh::Parameters) {
self.sh = sh;
}
}

View File

@ -1,36 +0,0 @@
use miniconf::Tree;
use num_traits::float::Float;
use serde::{Deserialize, Serialize};
use uom::si::{electrical_resistance::ohm,
f32::{ElectricalResistance, ThermodynamicTemperature},
ratio::ratio,
thermodynamic_temperature::{degree_celsius, kelvin}};
/// Steinhart-Hart equation parameters
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct Parameters {
/// Base temperature
pub t0: ThermodynamicTemperature,
/// Base resistance
pub r0: ElectricalResistance,
/// Beta
pub b: f32,
}
impl Parameters {
/// Perform the voltage to temperature conversion.
pub fn get_temperature(&self, r: ElectricalResistance) -> ThermodynamicTemperature {
let inv_temp = 1.0 / self.t0.get::<kelvin>() + (r / self.r0).get::<ratio>().ln() / self.b;
ThermodynamicTemperature::new::<kelvin>(1.0 / inv_temp)
}
}
impl Default for Parameters {
fn default() -> Self {
Parameters {
t0: ThermodynamicTemperature::new::<degree_celsius>(25.0),
r0: ElectricalResistance::new::<ohm>(10_000.0),
b: 3800.0,
}
}
}

View File

@ -1,190 +0,0 @@
use miniconf::Tree;
use num_traits::Float;
use serde::{Deserialize, Serialize};
use uom::si::{f32::ThermodynamicTemperature, thermodynamic_temperature::degree_celsius};
#[derive(PartialEq, Deserialize, Serialize, Copy, Clone, Default, Debug)]
pub enum TempStatusEnum {
#[default]
Off,
OverTemp,
Unstable,
Stable,
ConstantCurrentMode,
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct TempStatus {
pub status: TempStatusEnum,
pub over_temp_alarm: bool,
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct TempMonSettings {
pub upper_limit: f32,
pub lower_limit: f32,
}
pub struct TempMon {
pub upper_limit: ThermodynamicTemperature,
pub lower_limit: ThermodynamicTemperature,
pub set_point: ThermodynamicTemperature,
pub status: TempStatus,
state: State,
count: u32,
is_set_point_changed: bool,
}
impl Default for TempMon {
fn default() -> Self {
Self {
upper_limit: ThermodynamicTemperature::new::<degree_celsius>(45.0),
lower_limit: ThermodynamicTemperature::new::<degree_celsius>(0.0),
set_point: ThermodynamicTemperature::new::<degree_celsius>(0.0),
status: TempStatus {
status: TempStatusEnum::Off,
over_temp_alarm: false,
},
state: State::default(),
count: 0,
// stable_temp_count: 0,
is_set_point_changed: false,
}
}
}
#[derive(Default, PartialEq, Debug)]
enum State {
#[default]
PwrOff,
ConstantCurrentMode,
PidStartUp,
PidStable,
OverTempAlarm,
}
impl TempMon {
const OVER_TEMP_COUNT_LIMIT: u32 = 25;
const TEMP_STABLE_COUNT_LIMIT: u32 = 100;
pub fn set_upper_limit(&mut self, upper_limit: ThermodynamicTemperature) {
self.upper_limit = upper_limit;
}
pub fn get_upper_limit(&mut self) -> ThermodynamicTemperature {
self.upper_limit
}
pub fn set_lower_limit(&mut self, lower_limit: ThermodynamicTemperature) {
self.lower_limit = lower_limit;
}
pub fn get_lower_limit(&mut self) -> ThermodynamicTemperature {
self.lower_limit
}
pub fn set_setpoint(&mut self, set_point: ThermodynamicTemperature) {
if self.set_point != set_point {
self.is_set_point_changed = true;
self.count = 0;
}
self.set_point = set_point;
}
pub fn clear_alarm(&mut self) {
self.status.over_temp_alarm = false;
self.state = State::default();
}
pub fn update_status(&mut self, pid_engaged: bool, pwr_on: bool, temp: ThermodynamicTemperature) {
match self.state {
State::PwrOff => {
self.is_set_point_changed = false;
self.status.status = TempStatusEnum::Off;
self.count = 0;
// State Transition
if pwr_on {
if pid_engaged {
self.state = State::PidStartUp;
} else {
self.state = State::ConstantCurrentMode
}
}
}
State::ConstantCurrentMode => {
let is_over_temp = temp > self.upper_limit || temp < self.lower_limit;
self.status.status = TempStatusEnum::ConstantCurrentMode;
if is_over_temp {
self.state = State::OverTempAlarm;
self.status.status = TempStatusEnum::OverTemp;
} else if !pwr_on {
self.state = State::PwrOff;
self.status.status = TempStatusEnum::Off;
} else if pid_engaged {
self.state = State::PidStartUp;
self.status.status = TempStatusEnum::Unstable;
self.is_set_point_changed = false;
}
}
State::PidStartUp | State::PidStable => {
let is_over_temp: bool;
if self.state == State::PidStartUp {
is_over_temp = temp > self.upper_limit || temp < self.lower_limit;
} else {
is_over_temp = (temp.value - self.set_point.value).abs() > 0.5;
}
let is_within_spec: bool = (temp.value - self.set_point.value).abs() < 0.001;
if is_over_temp {
if self.count > TempMon::OVER_TEMP_COUNT_LIMIT {
self.status.status = TempStatusEnum::OverTemp;
} else {
self.count += 1;
}
} else if is_within_spec {
if self.count > TempMon::TEMP_STABLE_COUNT_LIMIT {
self.status.status = TempStatusEnum::Stable;
} else {
self.count += 1;
}
} else {
self.status.status = TempStatusEnum::Unstable;
self.count = 0;
}
// State Transition
if !pwr_on {
self.state = State::PwrOff;
} else {
if self.status.status == TempStatusEnum::OverTemp {
self.state = State::OverTempAlarm;
} else if self.is_set_point_changed {
self.is_set_point_changed = false;
self.state = State::PidStartUp;
} else if self.status.status == TempStatusEnum::Stable {
self.state = State::PidStable;
} else if !pid_engaged {
self.state = State::ConstantCurrentMode
}
}
}
State::OverTempAlarm => {
self.is_set_point_changed = false;
self.status.over_temp_alarm = true;
}
}
}
pub fn get_status(&mut self) -> TempStatus {
self.status
}
pub fn get_settings(&mut self) -> TempMonSettings {
TempMonSettings {
upper_limit: self.upper_limit.get::<degree_celsius>(),
lower_limit: self.lower_limit.get::<degree_celsius>(),
}
}
}

View File

@ -1,597 +0,0 @@
use core::{f32::NAN, marker::PhantomData};
use log::debug;
use miniconf::Tree;
use serde::{Deserialize, Serialize};
use uom::si::{electric_current::ampere,
electric_potential::volt,
electrical_resistance::ohm,
f32::{ElectricCurrent, ElectricPotential, ElectricalResistance, ThermodynamicTemperature},
ratio::ratio,
thermodynamic_temperature::degree_celsius};
use crate::{sys_timer,
thermostat::{ad5680,
ad7172::{self, FilterType, PostFilter, SingleChODR},
max1968::{AdcReadTarget, PwmPinsEnum, MAX1968},
pid_state,
pid_state::{Parameters as PidParams, PidSettings, PidState},
steinhart_hart::Parameters as Sh_Params,
temp_mon::{TempMon, TempMonSettings, TempStatus}}};
pub const R_SENSE: ElectricalResistance = ElectricalResistance {
dimension: PhantomData,
units: PhantomData,
value: 0.05,
};
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Default, Tree)]
pub struct TempAdcFilter {
pub filter_type: FilterType,
pub sinc5sinc1odr: Option<SingleChODR>,
pub sinc3odr: Option<SingleChODR>,
pub sinc5sinc1postfilter: Option<PostFilter>,
pub sinc3fineodr: Option<f32>,
pub rate: Option<f32>,
}
#[derive(Deserialize, Serialize, Clone, Copy, Debug, Tree)]
pub struct TecSettings {
pub default_pwr_on: bool,
pub center_pt: ElectricPotential,
pub max_v_set: ElectricPotential,
pub max_i_pos_set: ElectricCurrent,
pub max_i_neg_set: ElectricCurrent,
pub i_set: ElectricCurrent,
pub vref: ElectricPotential,
}
impl TecSettings {
pub const DAC_OUT_V_MAX: ElectricPotential = ElectricPotential {
dimension: PhantomData,
units: PhantomData,
value: 3.0,
};
pub const TEC_VSEC_BIAS_V: ElectricPotential = ElectricPotential {
dimension: PhantomData,
units: PhantomData,
value: 1.65,
};
// Kirdy Design Specs:
// MaxV = 5.0V
// MAX Current = +- 1.0A
const MAX_I_SET: ElectricCurrent = ElectricCurrent {
dimension: PhantomData,
units: PhantomData,
value: 1.0,
};
const MAX_V_DUTY_TO_CURRENT_RATE: ElectricPotential = ElectricPotential {
dimension: PhantomData,
units: PhantomData,
value: 4.0 * 3.3,
};
pub const MAX_V_MAX: ElectricPotential = ElectricPotential {
dimension: PhantomData,
units: PhantomData,
value: 5.0,
};
const MAX_V_DUTY_MAX: f64 =
TecSettings::MAX_V_MAX.value as f64 / TecSettings::MAX_V_DUTY_TO_CURRENT_RATE.value as f64;
const MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE: ElectricCurrent = ElectricCurrent {
dimension: PhantomData,
units: PhantomData,
value: 1.0 / (10.0 * R_SENSE.value / 3.3),
};
pub const MAX_I_POS_CURRENT: ElectricCurrent = ElectricCurrent {
dimension: PhantomData,
units: PhantomData,
value: 1.0,
};
pub const MAX_I_NEG_CURRENT: ElectricCurrent = ElectricCurrent {
dimension: PhantomData,
units: PhantomData,
value: 1.0,
};
// .get::<ratio>() is not implemented for const
const MAX_I_POS_DUTY_MAX: f64 =
TecSettings::MAX_I_POS_CURRENT.value as f64 / TecSettings::MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE.value as f64;
const MAX_I_NEG_DUTY_MAX: f64 =
TecSettings::MAX_I_NEG_CURRENT.value as f64 / TecSettings::MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE.value as f64;
}
impl Default for TecSettings {
fn default() -> Self {
Self {
default_pwr_on: false,
center_pt: ElectricPotential::new::<volt>(1.5),
max_v_set: ElectricPotential::new::<volt>(5.0),
max_i_pos_set: ElectricCurrent::new::<ampere>(1.0),
max_i_neg_set: ElectricCurrent::new::<ampere>(1.0),
i_set: ElectricCurrent::new::<ampere>(0.0),
vref: ElectricPotential::new::<volt>(1.5),
}
}
}
pub struct Thermostat {
max1968: MAX1968,
ad7172: ad7172::AdcPhy,
pub tec_settings: TecSettings,
pid_ctrl_ch0: PidState,
temp_mon: TempMon,
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct ThermostatSettingsSummary {
default_pwr_on: bool,
pid_engaged: bool,
temperature_setpoint: f32,
tec_settings: TecSettingSummary,
pid_params: PidParams,
temp_adc_settings: TempAdcFilter,
temp_mon_settings: TempMonSettings,
thermistor_params: ThermistorParams,
}
impl Thermostat {
pub fn new(max1968: MAX1968, ad7172: ad7172::AdcPhy) -> Self {
Thermostat {
max1968: max1968,
ad7172: ad7172,
tec_settings: TecSettings::default(),
pid_ctrl_ch0: PidState::default(),
temp_mon: TempMon::default(),
}
}
pub fn setup(&mut self) {
self.tec_setup();
let t_adc_ch0_cal = self.t_adc_setup();
self.pid_ctrl_ch0.set_adc_calibration(t_adc_ch0_cal);
}
/// start_tec_readings_conversion() should not be called before the current
/// DMA request is serviced or the conversion process will be restarted
/// Thus, no new readings is available when you call get_tec_readings() fn
pub fn start_tec_readings_conversion(&mut self) {
self.max1968.dma_adc_start_conversion();
}
fn tec_setup(&mut self) {
self.power_down();
self.tec_settings = TecSettings::default();
self.set_i(self.tec_settings.i_set);
self.set_max_v(self.tec_settings.max_v_set);
self.set_max_i_pos(self.tec_settings.max_i_pos_set);
self.set_max_i_neg(self.tec_settings.max_i_neg_set);
}
fn t_adc_setup(&mut self) -> ad7172::ChannelCalibration {
self.ad7172.set_sync_enable(false).unwrap();
self.ad7172
.setup_channel(0, ad7172::Input::Ain0, ad7172::Input::Ain1)
.unwrap();
let adc_calibration0 = self.ad7172.get_calibration(0).expect("adc_calibration0");
self.ad7172.start_continuous_conversion().unwrap();
adc_calibration0
}
pub fn poll_adc(&mut self) -> bool {
let mut data_rdy = false;
self.ad7172.data_ready().unwrap().map(|_ch| {
let data = self.ad7172.read_data().unwrap();
let state: &mut PidState = &mut self.pid_ctrl_ch0;
state.update(data);
let pid_engaged = state.get_pid_engaged();
let temp = self.get_temperature();
self.temp_mon
.update_status(pid_engaged, self.max1968.is_powered_on(), temp);
debug!("state.get_pid_engaged(): {:?}", pid_engaged);
debug!("Temperature: {:?} degree", temp.get::<degree_celsius>());
data_rdy = true;
});
data_rdy
}
pub fn update_pid(&mut self) {
let state: &mut PidState = &mut self.pid_ctrl_ch0;
let pid_engaged = state.get_pid_engaged();
if pid_engaged {
match state.update_pid() {
Some(pid_output) => {
self.set_i(ElectricCurrent::new::<ampere>(pid_output as f32));
debug!(
"Temperature Set Point: {:?} degree",
self.pid_ctrl_ch0.get_pid_setpoint().get::<degree_celsius>()
);
}
None => {}
}
}
}
pub fn get_temp_mon_status(&mut self) -> TempStatus {
self.temp_mon.get_status()
}
pub fn power_up(&mut self) {
self.max1968.power_up();
}
pub fn power_down(&mut self) {
self.max1968.power_down();
self.pid_ctrl_ch0.reset_pid_state();
self.set_i(ElectricCurrent::new::<ampere>(0.0));
}
fn set_center_pt(&mut self, value: ElectricPotential) {
self.tec_settings.center_pt = value;
}
pub fn set_default_pwr_on(&mut self, pwr_on: bool) {
self.tec_settings.default_pwr_on = pwr_on;
}
pub fn set_i(&mut self, i_tec: ElectricCurrent) -> ElectricCurrent {
let voltage = i_tec * 10.0 * R_SENSE + self.tec_settings.center_pt;
let voltage = self.max1968.set_dac(voltage, TecSettings::DAC_OUT_V_MAX);
self.tec_settings.i_set = (voltage - self.tec_settings.center_pt) / (10.0 * R_SENSE);
self.tec_settings.i_set
}
pub fn set_max_v(&mut self, max_v: ElectricPotential) -> ElectricPotential {
let duty = (max_v / TecSettings::MAX_V_DUTY_TO_CURRENT_RATE).get::<ratio>();
let duty = self
.max1968
.set_pwm(PwmPinsEnum::MaxV, duty as f64, TecSettings::MAX_V_DUTY_MAX);
self.tec_settings.max_v_set = duty as f32 * TecSettings::MAX_V_DUTY_TO_CURRENT_RATE;
self.tec_settings.max_v_set
}
pub fn set_max_i_pos(&mut self, max_i_pos: ElectricCurrent) -> ElectricCurrent {
let duty = (max_i_pos / TecSettings::MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE).get::<ratio>();
let duty = self
.max1968
.set_pwm(PwmPinsEnum::MaxPosI, duty as f64, TecSettings::MAX_I_POS_DUTY_MAX);
self.tec_settings.max_i_pos_set = duty as f32 * TecSettings::MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE;
self.tec_settings.max_i_pos_set
}
pub fn set_max_i_neg(&mut self, max_i_neg: ElectricCurrent) -> ElectricCurrent {
let duty = (max_i_neg / TecSettings::MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE).get::<ratio>();
let duty = self
.max1968
.set_pwm(PwmPinsEnum::MaxNegI, duty as f64, TecSettings::MAX_I_NEG_DUTY_MAX);
self.tec_settings.max_i_neg_set = duty as f32 * TecSettings::MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE;
self.tec_settings.max_i_neg_set
}
#[allow(unused)]
fn get_dac_vfb(&mut self) -> ElectricPotential {
self.max1968.adc_read(AdcReadTarget::DacVfb, 16)
}
#[allow(unused)]
fn get_vref(&mut self) -> ElectricPotential {
self.max1968.adc_read(AdcReadTarget::VREF, 16)
}
#[allow(unused)]
pub fn get_tec_i(&mut self) -> ElectricCurrent {
let vref = self.max1968.adc_read(AdcReadTarget::VREF, 16);
(self.max1968.adc_read(AdcReadTarget::ITec, 16) - vref) / ElectricalResistance::new::<ohm>(0.4)
}
#[allow(unused)]
pub fn get_tec_v(&mut self) -> ElectricPotential {
(self.max1968.adc_read(AdcReadTarget::VTec, 16) - TecSettings::TEC_VSEC_BIAS_V) * 4.0
}
pub fn get_tec_readings(&mut self) -> (ElectricPotential, ElectricCurrent) {
let vref = self.tec_settings.vref;
let (vtec, itec) = self.max1968.get_tec_readings();
(
(vtec - TecSettings::TEC_VSEC_BIAS_V) * 4.0,
(itec - vref) / ElectricalResistance::new::<ohm>(0.4),
)
}
/// Calibrates the DAC output to match vref of the MAX driver to reduce zero-current offset of the MAX driver output.
///
/// The thermostat DAC applies a control voltage signal to the CTLI pin of MAX driver chip to control its output current.
/// The 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
/// 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
/// 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
/// 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
/// VREF measurement can introduce significant noise at the current output, degrading the stabilily performance of the
/// thermostat.
pub fn calibrate_dac_value(&mut self) {
const DAC_BIT: u32 = 18;
const ADC_BIT: u32 = 12;
let target_voltage = self.max1968.adc_read(AdcReadTarget::VREF, 512);
let mut start_value = 1;
let mut best_error = ElectricPotential::new::<volt>(100.0);
for step in (DAC_BIT - ADC_BIT - 1..DAC_BIT).rev() {
let mut prev_value = start_value;
for value in (start_value..=ad5680::MAX_VALUE).step_by(1 << step) {
self.max1968.phy.dac.set(value).unwrap();
sys_timer::sleep(5);
let dac_feedback = self.max1968.adc_read(AdcReadTarget::DacVfb, 64);
let error = target_voltage - dac_feedback;
if error < ElectricPotential::new::<volt>(0.0) {
break;
} else if error < best_error {
best_error = error;
start_value = prev_value;
let vref = (value as f32 / ad5680::MAX_VALUE as f32) * TecSettings::DAC_OUT_V_MAX;
self.set_center_pt(vref);
}
prev_value = value;
}
}
self.tec_settings.vref = target_voltage;
}
pub fn set_pid_engaged(&mut self, val: bool) {
self.pid_ctrl_ch0.set_pid_engaged(val);
}
fn get_pid_engaged(&mut self) -> bool {
self.pid_ctrl_ch0.get_pid_engaged()
}
pub fn get_status_report(&mut self) -> StatusReport {
let (tec_v, tec_i) = self.get_tec_readings();
let temperature: Option<f32>;
match self.pid_ctrl_ch0.get_temperature() {
Some(val) => temperature = Some(val.get::<degree_celsius>()),
None => {
temperature = None;
}
}
StatusReport {
pwr_on: self.max1968.is_powered_on(),
pid_engaged: self.get_pid_engaged(),
temp_mon_status: self.temp_mon.get_status(),
temperature: temperature,
i_set: self.tec_settings.i_set,
tec_i: tec_i,
tec_v: tec_v,
}
}
pub fn get_temperature(&mut self) -> ThermodynamicTemperature {
match self.pid_ctrl_ch0.get_temperature() {
Some(val) => val,
None => ThermodynamicTemperature::new::<degree_celsius>(NAN),
}
}
fn get_pid_settings(&mut self) -> pid_state::Parameters {
self.pid_ctrl_ch0.get_pid_settings()
}
fn get_steinhart_hart(&mut self) -> ThermistorParams {
let sh = self.pid_ctrl_ch0.get_sh();
ThermistorParams {
t0: sh.t0.get::<degree_celsius>(),
r0: sh.r0,
b: sh.b,
}
}
fn apply_steinhart_hart(&mut self, sh: ThermistorParams) {
self.pid_ctrl_ch0.apply_sh(Sh_Params {
t0: ThermodynamicTemperature::new::<degree_celsius>(sh.t0),
r0: sh.r0,
b: sh.b,
})
}
fn get_tec_settings(&mut self) -> TecSettingSummary {
TecSettingSummary {
i_set: TecSettingsSummaryField {
value: self.tec_settings.i_set,
max: TecSettings::MAX_I_SET,
},
max_v: TecSettingsSummaryField {
value: self.tec_settings.max_v_set,
max: TecSettings::MAX_V_MAX,
},
max_i_pos: TecSettingsSummaryField {
value: self.tec_settings.max_i_pos_set,
max: TecSettings::MAX_I_POS_CURRENT,
},
max_i_neg: TecSettingsSummaryField {
value: self.tec_settings.max_i_neg_set,
max: TecSettings::MAX_I_NEG_CURRENT,
},
}
}
pub fn get_calibrated_vdda(&mut self) -> u32 {
self.max1968.get_calibrated_vdda()
}
pub fn set_pid(&mut self, param: PidSettings, val: f32) {
self.pid_ctrl_ch0.set_pid_params(param, val);
}
pub fn set_sh_beta(&mut self, beta: f32) {
self.pid_ctrl_ch0.set_sh_beta(beta);
}
pub fn set_sh_r0(&mut self, r0: ElectricalResistance) {
self.pid_ctrl_ch0.set_sh_r0(r0);
}
pub fn set_sh_t0(&mut self, t0: ThermodynamicTemperature) {
self.pid_ctrl_ch0.set_sh_t0(t0);
}
pub fn set_temperature_setpoint(&mut self, t: ThermodynamicTemperature) {
let t = t
.min(self.temp_mon.get_upper_limit())
.max(self.temp_mon.get_lower_limit());
self.pid_ctrl_ch0.set_pid_setpoint(t);
self.temp_mon.set_setpoint(t);
}
pub fn apply_temp_mon_settings(&mut self, settings: TempMonSettings) {
self.temp_mon
.set_upper_limit(ThermodynamicTemperature::new::<degree_celsius>(settings.upper_limit));
self.temp_mon
.set_lower_limit(ThermodynamicTemperature::new::<degree_celsius>(settings.lower_limit));
}
pub fn set_temp_mon_upper_limit(&mut self, t: ThermodynamicTemperature) {
self.temp_mon.set_upper_limit(t);
}
pub fn set_temp_mon_lower_limit(&mut self, t: ThermodynamicTemperature) {
self.temp_mon.set_lower_limit(t);
}
pub fn set_temp_adc_sinc5_sinc1_filter(&mut self, index: u8, odr: ad7172::SingleChODR) {
self.ad7172.set_sinc5_sinc1_filter(index, odr).unwrap();
}
pub fn set_temp_adc_sinc3_filter(&mut self, index: u8, odr: ad7172::SingleChODR) {
self.ad7172.set_sinc3_filter(index, odr).unwrap();
}
pub fn set_temp_adc_sinc5_sinc1_with_postfilter(&mut self, index: u8, odr: ad7172::PostFilter) {
self.ad7172
.set_sinc5_sinc1_with_50hz_60hz_rejection(index, odr)
.unwrap();
}
pub fn set_temp_adc_sinc3_fine_filter(&mut self, index: u8, rate: f32) {
self.ad7172.set_sinc3_fine_filter(index, rate).unwrap();
}
pub fn clear_temp_mon_alarm(&mut self) {
self.temp_mon.clear_alarm();
}
fn get_temp_mon_settings(&mut self) -> TempMonSettings {
self.temp_mon.get_settings()
}
pub fn get_settings_summary(&mut self) -> ThermostatSettingsSummary {
let temp_adc_filter_type: FilterType;
let update_rate: f32;
match self.ad7172.get_filter_type_and_rate(0) {
Ok((filter_type, rate)) => {
temp_adc_filter_type = filter_type;
update_rate = rate;
}
Err(_) => {
panic!("Cannot read ADC filter type and rate");
}
}
ThermostatSettingsSummary {
default_pwr_on: self.tec_settings.default_pwr_on,
pid_engaged: self.get_pid_engaged(),
temperature_setpoint: self.pid_ctrl_ch0.get_pid_setpoint().get::<degree_celsius>(),
tec_settings: self.get_tec_settings(),
pid_params: self.get_pid_settings(),
temp_adc_settings: TempAdcFilter {
filter_type: temp_adc_filter_type,
sinc5sinc1odr: None,
sinc3odr: None,
sinc5sinc1postfilter: None,
sinc3fineodr: None,
rate: Some(update_rate),
},
temp_mon_settings: self.get_temp_mon_settings(),
thermistor_params: self.get_steinhart_hart(),
}
}
pub fn load_settings_from_summary(&mut self, settings: ThermostatSettingsSummary) {
self.power_down();
self.set_max_i_neg(settings.tec_settings.max_i_neg.value);
self.set_max_i_pos(settings.tec_settings.max_i_pos.value);
self.set_max_v(settings.tec_settings.max_v.value);
self.apply_steinhart_hart(settings.thermistor_params);
self.apply_temp_mon_settings(settings.temp_mon_settings);
match settings.temp_adc_settings.rate {
Some(rate) => match settings.temp_adc_settings.filter_type {
FilterType::Sinc3 => self.set_temp_adc_sinc3_filter(0, SingleChODR::closest(rate).unwrap()),
FilterType::Sinc5Sinc1 => self.set_temp_adc_sinc5_sinc1_filter(0, SingleChODR::closest(rate).unwrap()),
FilterType::Sinc3WithFineODR => self.set_temp_adc_sinc3_fine_filter(0, rate),
FilterType::Sinc5Sinc1With50hz60HzRejection => {
self.set_temp_adc_sinc5_sinc1_with_postfilter(0, PostFilter::closest(rate).unwrap())
}
},
None => {
debug!(" Temperature ADC Settings is not found");
}
}
self.set_pid_engaged(settings.pid_engaged);
self.pid_ctrl_ch0.apply_pid_params(settings.pid_params);
self.set_temperature_setpoint(ThermodynamicTemperature::new::<degree_celsius>(
settings.temperature_setpoint,
));
if !settings.pid_engaged {
self.set_i(settings.tec_settings.i_set.value);
}
self.set_default_pwr_on(settings.default_pwr_on);
if settings.default_pwr_on {
self.power_up();
} else {
self.power_down();
}
}
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct StatusReport {
pwr_on: bool,
pid_engaged: bool,
temp_mon_status: TempStatus,
temperature: Option<f32>,
i_set: ElectricCurrent,
tec_i: ElectricCurrent,
tec_v: ElectricPotential,
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct TecSettingsSummaryField<T> {
value: T,
max: T,
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct TecSettingSummary {
i_set: TecSettingsSummaryField<ElectricCurrent>,
max_v: TecSettingsSummaryField<ElectricPotential>,
max_i_pos: TecSettingsSummaryField<ElectricCurrent>,
max_i_neg: TecSettingsSummaryField<ElectricCurrent>,
}
#[derive(Deserialize, Serialize, Clone, Copy, Debug, Tree)]
pub struct ThermistorParams {
t0: f32,
r0: ElectricalResistance,
b: f32,
}