diff --git a/cue/Cargo.lock b/cue/Cargo.lock index 0afd6a4..b93d578 100644 --- a/cue/Cargo.lock +++ b/cue/Cargo.lock @@ -334,6 +334,9 @@ name = "bitflags" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +dependencies = [ + "serde_core", +] [[package]] name = "block" @@ -513,7 +516,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b7f4aaa047ba3c3630b080bb9860894732ff23e2aee290a418909aa6d5df38f" dependencies = [ "objc2 0.5.2", - "objc2-app-kit", + "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", ] @@ -698,6 +701,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -752,7 +764,9 @@ dependencies = [ "futures", "iced", "midir", + "muda", "tokio", + "winres", ] [[package]] @@ -1714,6 +1728,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.11.0", + "serde", + "unicode-segmentation", +] + [[package]] name = "khronos-egl" version = "6.0.0" @@ -1968,6 +1993,25 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "muda" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" +dependencies = [ + "crossbeam-channel", + "dpi", + "keyboard-types", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "once_cell", + "png", + "thiserror 2.0.18", + "windows-sys 0.60.2", +] + [[package]] name = "naga" version = "0.19.2" @@ -2123,6 +2167,18 @@ dependencies = [ "objc2-quartz-core 0.2.2", ] +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.11.0", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-cloud-kit" version = "0.2.2" @@ -2256,7 +2312,7 @@ checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" dependencies = [ "block2", "objc2 0.5.2", - "objc2-app-kit", + "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", ] @@ -3404,6 +3460,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "toml_datetime" version = "1.0.0+spec-1.1.0" @@ -4127,6 +4192,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -4160,13 +4234,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -4179,6 +4270,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -4191,6 +4288,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -4203,12 +4306,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -4221,6 +4336,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -4233,6 +4354,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -4245,6 +4372,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -4257,6 +4390,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winit" version = "0.30.13" @@ -4281,7 +4420,7 @@ dependencies = [ "memmap2", "ndk", "objc2 0.5.2", - "objc2-app-kit", + "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", "objc2-ui-kit", "orbclient", @@ -4327,6 +4466,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "winres" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c" +dependencies = [ + "toml", +] + [[package]] name = "wit-bindgen" version = "0.51.0" diff --git a/cue/Cargo.toml b/cue/Cargo.toml index bdef516..197f943 100644 --- a/cue/Cargo.toml +++ b/cue/Cargo.toml @@ -8,3 +8,7 @@ iced = { version = "0.13", features = ["canvas", "tokio"] } midir = "0.10" tokio = { version = "1", features = ["full"] } futures = "0.3" +muda = { version = "0.17", default-features = false } + +[target.'cfg(windows)'.build-dependencies] +winres = "0.1" diff --git a/cue/assets/cue.icns b/cue/assets/cue.icns new file mode 100644 index 0000000..146e75a Binary files /dev/null and b/cue/assets/cue.icns differ diff --git a/cue/assets/cue.svg b/cue/assets/cue.svg new file mode 100644 index 0000000..e96efcb --- /dev/null +++ b/cue/assets/cue.svg @@ -0,0 +1,1212 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cue/assets/cue2.svg b/cue/assets/cue2.svg new file mode 100644 index 0000000..64ea3a6 --- /dev/null +++ b/cue/assets/cue2.svg @@ -0,0 +1,3972 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cue/assets/cue4.svg b/cue/assets/cue4.svg new file mode 100644 index 0000000..a5f64f4 --- /dev/null +++ b/cue/assets/cue4.svg @@ -0,0 +1,3972 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cue/build.rs b/cue/build.rs new file mode 100644 index 0000000..0d8d6b3 --- /dev/null +++ b/cue/build.rs @@ -0,0 +1,10 @@ +fn main() { + #[cfg(target_os = "windows")] + { + let mut res = winres::WindowsResource::new(); + res.set_icon("assets/cue.ico"); + if let Err(e) = res.compile() { + eprintln!("winres: {e}"); + } + } +} diff --git a/cue/build.sh b/cue/build.sh new file mode 100755 index 0000000..f178c0d --- /dev/null +++ b/cue/build.sh @@ -0,0 +1,157 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +cd "$SCRIPT_DIR" + +SVG="assets/cue.svg" +NAME="Cue" +BIN="cue" +VERSION="0.1.0" +IDENTIFIER="org.else-if.cue" + +if [ ! -f "$SVG" ]; then + echo "Error: $SVG not found" + exit 1 +fi + +render_svg() { + local svg="$1" size="$2" out="$3" + if command -v rsvg-convert >/dev/null 2>&1; then + rsvg-convert -w "$size" -h "$size" "$svg" -o "$out" + elif command -v magick >/dev/null 2>&1; then + magick "$svg" -background none -resize "${size}x${size}" "$out" + else + echo "Error: need rsvg-convert (librsvg) or magick (ImageMagick 7)" + exit 1 + fi +} + +case "$(uname -s)" in + Darwin) + echo "==> Generating .icns" + ICONSET=$(mktemp -d)/cue.iconset + mkdir -p "$ICONSET" + for size in 16 32 64 128 256 512; do + render_svg "$SVG" "$size" "$ICONSET/icon_${size}x${size}.png" + double=$((size * 2)) + render_svg "$SVG" "$double" "$ICONSET/icon_${size}x${size}@2x.png" + done + iconutil -c icns "$ICONSET" -o assets/cue.icns + rm -rf "$(dirname "$ICONSET")" + + echo "==> cargo build --release" + cargo build --release + + echo "==> Creating .app bundle" + APP="target/release/$NAME.app" + rm -rf "$APP" + mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources" + cp "target/release/$BIN" "$APP/Contents/MacOS/$NAME" + cp assets/cue.icns "$APP/Contents/Resources/" + + cat > "$APP/Contents/Info.plist" << PLIST + + + + + CFBundleName + $NAME + CFBundleDisplayName + $NAME + CFBundleIdentifier + $IDENTIFIER + CFBundleVersion + $VERSION + CFBundleShortVersionString + $VERSION + CFBundleExecutable + $NAME + CFBundleIconFile + cue + CFBundlePackageType + APPL + NSHighResolutionCapable + + NSBluetoothAlwaysUsageDescription + Cue uses Bluetooth to communicate with EIS4. + NSBluetoothPeripheralUsageDescription + Cue uses Bluetooth to communicate with EIS4. + + +PLIST + + codesign --force --deep --sign - "$APP" + echo "==> $APP" + ;; + + Linux) + echo "==> Generating PNGs" + for size in 16 32 48 64 128 256 512; do + mkdir -p "assets/icons/${size}x${size}" + render_svg "$SVG" "$size" "assets/icons/${size}x${size}/cue.png" + done + + echo "==> cargo build --release" + cargo build --release + + echo "==> Creating package" + PKG="target/release/cue-linux" + rm -rf "$PKG" + mkdir -p "$PKG" + cp target/release/$BIN "$PKG/" + + for size in 16 32 48 64 128 256 512; do + mkdir -p "$PKG/icons/hicolor/${size}x${size}/apps" + cp "assets/icons/${size}x${size}/cue.png" "$PKG/icons/hicolor/${size}x${size}/apps/" + done + + cat > "$PKG/cue.desktop" << DESKTOP +[Desktop Entry] +Name=Cue +Comment=EIS4 Electrochemical Instrument +Exec=cue +Icon=cue +Terminal=false +Type=Application +Categories=Science;Education; +DESKTOP + + cat > "$PKG/install.sh" << 'INSTALL' +#!/bin/bash +set -e +PREFIX="${1:-/usr/local}" +install -Dm755 cue "$PREFIX/bin/cue" +install -Dm644 cue.desktop "$PREFIX/share/applications/cue.desktop" +for d in icons/hicolor/*/apps; do + size="$(basename "$(dirname "$d")")" + install -Dm644 "$d/cue.png" "$PREFIX/share/icons/hicolor/$size/apps/cue.png" +done +echo "Installed to $PREFIX" +INSTALL + chmod +x "$PKG/install.sh" + + echo "==> $PKG/" + ;; + + MINGW*|MSYS*|CYGWIN*) + echo "==> Generating .ico" + TMPICO=$(mktemp -d) + for size in 16 32 48 64 128 256; do + render_svg "$SVG" "$size" "$TMPICO/${size}.png" + done + magick "$TMPICO/16.png" "$TMPICO/32.png" "$TMPICO/48.png" \ + "$TMPICO/64.png" "$TMPICO/128.png" "$TMPICO/256.png" assets/cue.ico + rm -rf "$TMPICO" + + echo "==> cargo build --release" + cargo build --release + + echo "==> target/release/cue.exe (icon embedded via build.rs)" + ;; + + *) + echo "Unknown platform: $(uname -s)" + cargo build --release + ;; +esac diff --git a/cue/src/app.rs b/cue/src/app.rs index 8cb4d40..e6b924a 100644 --- a/cue/src/app.rs +++ b/cue/src/app.rs @@ -1,62 +1,322 @@ use futures::SinkExt; -use iced::widget::{button, canvas, column, container, pick_list, row, scrollable, text, text_input}; -use iced::{Element, Length, Subscription, Task, Theme}; +use iced::widget::{ + button, canvas, column, container, pane_grid, pick_list, row, text, text_editor, text_input, +}; +use iced::widget::button::Style as ButtonStyle; +use iced::{Border, Color, Element, Length, Subscription, Task, Theme}; +use std::fmt::Write; use std::time::Duration; use tokio::sync::mpsc; use crate::ble::BleEvent; -use crate::protocol::{self, EisMessage, EisPoint, Rcal, Rtia}; +use crate::native_menu::{MenuAction, NativeMenu}; +use crate::protocol::{ + self, AmpPoint, ClPoint, ClResult, Electrode, EisMessage, EisPoint, LpRtia, LsvPoint, + PhResult, Rcal, Rtia, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Tab { + Eis, + Lsv, + Amp, + Chlorine, + Ph, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum PaneId { + Plot, + Data, +} #[derive(Debug, Clone)] pub enum Message { BleReady(mpsc::UnboundedSender>), BleStatus(String), BleData(EisMessage), + TabSelected(Tab), + PaneResized(pane_grid::ResizeEvent), + DataAction(text_editor::Action), + /* EIS */ FreqStartChanged(String), FreqStopChanged(String), PpdChanged(String), RtiaSelected(Rtia), RcalSelected(Rcal), + ElectrodeSelected(Electrode), ApplySettings, StartSweep, + /* LSV */ + LsvStartVChanged(String), + LsvStopVChanged(String), + LsvScanRateChanged(String), + LsvRtiaSelected(LpRtia), + StartLsv, + /* Amperometry */ + AmpVholdChanged(String), + AmpIntervalChanged(String), + AmpDurationChanged(String), + AmpRtiaSelected(LpRtia), + StartAmp, + StopAmp, + /* Chlorine */ + ClCondVChanged(String), + ClCondTChanged(String), + ClFreeVChanged(String), + ClTotalVChanged(String), + ClDepTChanged(String), + ClMeasTChanged(String), + ClRtiaSelected(LpRtia), + StartCl, + /* pH */ + PhStabilizeChanged(String), + StartPh, + /* Global */ + PollTemp, + NativeMenuTick, + CloseSysInfo, + /* Reference baseline */ + SetReference, + ClearReference, + /* Misc */ + OpenMidiSetup, } pub struct App { + tab: Tab, status: String, - points: Vec, - sweep_total: u16, + cmd_tx: Option>>, + panes: pane_grid::State, + native_menu: NativeMenu, + show_sysinfo: bool, + /* EIS */ + eis_points: Vec, + sweep_total: u16, freq_start: String, freq_stop: String, ppd: String, rtia: Rtia, rcal: Rcal, + electrode: Electrode, + eis_data: text_editor::Content, - cmd_tx: Option>>, + /* LSV */ + lsv_points: Vec, + lsv_total: u16, + lsv_start_v: String, + lsv_stop_v: String, + lsv_scan_rate: String, + lsv_rtia: LpRtia, + lsv_data: text_editor::Content, + + /* Amp */ + amp_points: Vec, + amp_total: u16, + amp_running: bool, + amp_v_hold: String, + amp_interval: String, + amp_duration: String, + amp_rtia: LpRtia, + amp_data: text_editor::Content, + + /* Chlorine */ + cl_points: Vec, + cl_result: Option, + cl_total: u16, + cl_cond_v: String, + cl_cond_t: String, + cl_free_v: String, + cl_total_v: String, + cl_dep_t: String, + cl_meas_t: String, + cl_rtia: LpRtia, + cl_data: text_editor::Content, + + /* pH */ + ph_result: Option, + ph_stabilize: String, + + /* Reference baselines (tap water) */ + eis_ref: Option>, + lsv_ref: Option>, + amp_ref: Option>, + cl_ref: Option<(Vec, ClResult)>, + ph_ref: Option, + + /* Global */ + temp_c: f32, +} + +/* ---- data table formatting ---- */ + +fn fmt_eis(pts: &[EisPoint]) -> String { + let mut s = String::with_capacity(pts.len() * 130 + 130); + writeln!(s, "{:>10} {:>12} {:>10} {:>12} {:>12} {:>10} {:>10} {:>12} {:>10} {:>7}", + "Freq (Hz)", "|Z| (Ohm)", "Phase (°)", "Re (Ohm)", "Im (Ohm)", + "RTIA bef", "RTIA aft", "|Z| rev", "Ph rev", "Err%").unwrap(); + for pt in pts { + writeln!(s, "{:>10.1} {:>12.2} {:>10.2} {:>12.2} {:>12.2} {:>10.1} {:>10.1} {:>12.2} {:>10.2} {:>6.2}%", + pt.freq_hz, pt.mag_ohms, pt.phase_deg, pt.z_real, pt.z_imag, + pt.rtia_mag_before, pt.rtia_mag_after, pt.rev_mag, pt.rev_phase, pt.pct_err).unwrap(); + } + s +} + +fn fmt_lsv(pts: &[LsvPoint]) -> String { + let mut s = String::with_capacity(pts.len() * 28 + 28); + writeln!(s, "{:>10} {:>12}", "V (mV)", "I (uA)").unwrap(); + for pt in pts { + writeln!(s, "{:>10.1} {:>12.3}", pt.v_mv, pt.i_ua).unwrap(); + } + s +} + +fn fmt_amp(pts: &[AmpPoint]) -> String { + let mut s = String::with_capacity(pts.len() * 28 + 28); + writeln!(s, "{:>10} {:>12}", "t (ms)", "I (uA)").unwrap(); + for pt in pts { + writeln!(s, "{:>10.1} {:>12.3}", pt.t_ms, pt.i_ua).unwrap(); + } + s +} + +fn fmt_cl(pts: &[ClPoint]) -> String { + let mut s = String::with_capacity(pts.len() * 36 + 36); + writeln!(s, "{:>10} {:>12} {:>8}", "t (ms)", "I (uA)", "Phase").unwrap(); + for pt in pts { + let phase = match pt.phase { 1 => "Free", 2 => "Total", _ => "Cond" }; + writeln!(s, "{:>10.1} {:>12.3} {:>8}", pt.t_ms, pt.i_ua, phase).unwrap(); + } + s +} + +const SQUIRCLE: f32 = 8.0; + +fn btn_style(bg: Color, fg: Color) -> impl Fn(&Theme, button::Status) -> ButtonStyle { + move |_theme, status| { + let (bg, fg) = match status { + button::Status::Hovered => ( + Color { a: bg.a * 0.85, ..bg }, + fg, + ), + button::Status::Pressed => ( + Color { a: bg.a * 0.7, ..bg }, + fg, + ), + button::Status::Disabled => ( + Color { a: 0.3, ..bg }, + Color { a: 0.5, ..fg }, + ), + _ => (bg, fg), + }; + ButtonStyle { + background: Some(iced::Background::Color(bg)), + text_color: fg, + border: Border { + radius: SQUIRCLE.into(), + ..Border::default() + }, + ..ButtonStyle::default() + } + } +} + +fn style_action() -> impl Fn(&Theme, button::Status) -> ButtonStyle { + btn_style(Color::from_rgb(0.20, 0.65, 0.35), Color::WHITE) +} + +fn style_danger() -> impl Fn(&Theme, button::Status) -> ButtonStyle { + btn_style(Color::from_rgb(0.80, 0.25, 0.25), Color::WHITE) +} + +fn style_apply() -> impl Fn(&Theme, button::Status) -> ButtonStyle { + btn_style(Color::from_rgb(0.25, 0.50, 0.85), Color::WHITE) +} + +fn style_tab(active: bool) -> impl Fn(&Theme, button::Status) -> ButtonStyle { + let bg = if active { + Color::from_rgb(0.35, 0.35, 0.40) + } else { + Color::from_rgb(0.22, 0.22, 0.25) + }; + btn_style(bg, Color::WHITE) +} + +fn style_neutral() -> impl Fn(&Theme, button::Status) -> ButtonStyle { + btn_style(Color::from_rgb(0.30, 0.30, 0.35), Color::WHITE) } impl App { pub fn new() -> (Self, Task) { (Self { + tab: Tab::Eis, status: "Starting...".into(), - points: Vec::new(), + cmd_tx: None, + panes: pane_grid::State::with_configuration(pane_grid::Configuration::Split { + axis: pane_grid::Axis::Horizontal, + ratio: 0.55, + a: Box::new(pane_grid::Configuration::Pane(PaneId::Plot)), + b: Box::new(pane_grid::Configuration::Pane(PaneId::Data)), + }), + native_menu: NativeMenu::init(), + show_sysinfo: false, + + eis_points: Vec::new(), sweep_total: 0, freq_start: "1000".into(), freq_stop: "200000".into(), ppd: "10".into(), rtia: Rtia::R5K, rcal: Rcal::R3K, - cmd_tx: None, + electrode: Electrode::FourWire, + eis_data: text_editor::Content::with_text(&fmt_eis(&[])), + + lsv_points: Vec::new(), + lsv_total: 0, + lsv_start_v: "0".into(), + lsv_stop_v: "500".into(), + lsv_scan_rate: "50".into(), + lsv_rtia: LpRtia::R10K, + lsv_data: text_editor::Content::with_text(&fmt_lsv(&[])), + + amp_points: Vec::new(), + amp_total: 0, + amp_running: false, + amp_v_hold: "200".into(), + amp_interval: "100".into(), + amp_duration: "60".into(), + amp_rtia: LpRtia::R10K, + amp_data: text_editor::Content::with_text(&fmt_amp(&[])), + + cl_points: Vec::new(), + cl_result: None, + cl_total: 0, + cl_cond_v: "800".into(), + cl_cond_t: "2000".into(), + cl_free_v: "100".into(), + cl_total_v: "-200".into(), + cl_dep_t: "5000".into(), + cl_meas_t: "5000".into(), + cl_rtia: LpRtia::R10K, + cl_data: text_editor::Content::with_text(&fmt_cl(&[])), + + ph_result: None, + ph_stabilize: "30".into(), + + eis_ref: None, + lsv_ref: None, + amp_ref: None, + cl_ref: None, + ph_ref: None, + + temp_c: 25.0, }, Task::none()) } - pub fn title(&self) -> String { - "Cue".into() - } - - pub fn theme(&self) -> Theme { - Theme::Dark - } + pub fn title(&self) -> String { "Cue".into() } + pub fn theme(&self) -> Theme { Theme::Dark } fn send_cmd(&self, sysex: &[u8]) { if let Some(tx) = &self.cmd_tx { @@ -73,24 +333,18 @@ impl App { Message::BleStatus(s) => self.status = s, Message::BleData(msg) => match msg { EisMessage::SweepStart { num_points, freq_start, freq_stop } => { - self.points.clear(); + self.eis_points.clear(); self.sweep_total = num_points; - self.status = format!( - "Sweep: {} pts, {:.0}--{:.0} Hz", - num_points, freq_start, freq_stop - ); + self.eis_data = text_editor::Content::with_text(&fmt_eis(&self.eis_points)); + self.status = format!("Sweep: {} pts, {:.0}--{:.0} Hz", num_points, freq_start, freq_stop); } EisMessage::DataPoint { point, .. } => { - self.points.push(point); - self.status = format!( - "Receiving: {}/{}", - self.points.len(), self.sweep_total - ); + self.eis_points.push(point); + self.eis_data = text_editor::Content::with_text(&fmt_eis(&self.eis_points)); + self.status = format!("Receiving: {}/{}", self.eis_points.len(), self.sweep_total); } EisMessage::SweepEnd => { - self.status = format!( - "Sweep complete: {} points", self.points.len() - ); + self.status = format!("Sweep complete: {} points", self.eis_points.len()); } EisMessage::Config(cfg) => { self.freq_start = format!("{:.0}", cfg.freq_start); @@ -98,14 +352,91 @@ impl App { self.ppd = format!("{}", cfg.ppd); self.rtia = cfg.rtia; self.rcal = cfg.rcal; + self.electrode = cfg.electrode; self.status = "Config received".into(); } + EisMessage::LsvStart { num_points, v_start, v_stop } => { + self.lsv_points.clear(); + self.lsv_total = num_points; + self.lsv_data = text_editor::Content::with_text(&fmt_lsv(&self.lsv_points)); + self.status = format!("LSV: {} pts, {:.0}--{:.0} mV", num_points, v_start, v_stop); + } + EisMessage::LsvPoint { point, .. } => { + self.lsv_points.push(point); + self.lsv_data = text_editor::Content::with_text(&fmt_lsv(&self.lsv_points)); + self.status = format!("LSV: {}/{}", self.lsv_points.len(), self.lsv_total); + } + EisMessage::LsvEnd => { + self.status = format!("LSV complete: {} points", self.lsv_points.len()); + } + EisMessage::AmpStart { v_hold } => { + self.amp_points.clear(); + self.amp_running = true; + self.amp_data = text_editor::Content::with_text(&fmt_amp(&self.amp_points)); + self.status = format!("Amp: {:.0} mV", v_hold); + } + EisMessage::AmpPoint { index, point } => { + self.amp_points.push(point); + self.amp_total = index + 1; + self.amp_data = text_editor::Content::with_text(&fmt_amp(&self.amp_points)); + self.status = format!("Amp: {} pts", self.amp_points.len()); + } + EisMessage::AmpEnd => { + self.amp_running = false; + self.status = format!("Amp complete: {} points", self.amp_points.len()); + } + EisMessage::ClStart { num_points } => { + self.cl_points.clear(); + self.cl_result = None; + self.cl_total = num_points; + self.cl_data = text_editor::Content::with_text(&fmt_cl(&self.cl_points)); + self.status = format!("Chlorine: {} pts", num_points); + } + EisMessage::ClPoint { point, .. } => { + self.cl_points.push(point); + self.cl_data = text_editor::Content::with_text(&fmt_cl(&self.cl_points)); + self.status = format!("Chlorine: {}/{}", self.cl_points.len(), self.cl_total); + } + EisMessage::ClResult(r) => { + self.cl_result = Some(r); + self.status = format!("Chlorine: free={:.3} uA, total={:.3} uA", + self.cl_result.as_ref().unwrap().i_free_ua, + self.cl_result.as_ref().unwrap().i_total_ua); + } + EisMessage::ClEnd => { + self.status = format!("Chlorine complete: {} points", self.cl_points.len()); + } + EisMessage::PhResult(r) => { + self.status = format!("pH: {:.2} (OCP={:.1} mV, T={:.1}C)", + r.ph, r.v_ocp_mv, r.temp_c); + self.ph_result = Some(r); + } + EisMessage::Temperature(t) => { + self.temp_c = t; + } }, + Message::TabSelected(t) => self.tab = t, + Message::PaneResized(event) => { + self.panes.resize(event.split, event.ratio); + } + Message::DataAction(action) => { + if !matches!(action, text_editor::Action::Edit(_)) { + match self.tab { + Tab::Eis => self.eis_data.perform(action), + Tab::Lsv => self.lsv_data.perform(action), + Tab::Amp => self.amp_data.perform(action), + Tab::Chlorine => self.cl_data.perform(action), + Tab::Ph => {} + } + } + } + /* EIS */ Message::FreqStartChanged(s) => self.freq_start = s, Message::FreqStopChanged(s) => self.freq_stop = s, Message::PpdChanged(s) => self.ppd = s, Message::RtiaSelected(r) => self.rtia = r, Message::RcalSelected(r) => self.rcal = r, + Message::ElectrodeSelected(e) => self.electrode = e, Message::ApplySettings => { let fs = self.freq_start.parse::().unwrap_or(1000.0); let fe = self.freq_stop.parse::().unwrap_or(200000.0); @@ -113,24 +444,137 @@ impl App { self.send_cmd(&protocol::build_sysex_set_sweep(fs, fe, ppd)); self.send_cmd(&protocol::build_sysex_set_rtia(self.rtia)); self.send_cmd(&protocol::build_sysex_set_rcal(self.rcal)); + self.send_cmd(&protocol::build_sysex_set_electrode(self.electrode)); self.send_cmd(&protocol::build_sysex_get_config()); } Message::StartSweep => { + self.send_cmd(&protocol::build_sysex_get_temp()); self.send_cmd(&protocol::build_sysex_start_sweep()); } + /* LSV */ + Message::LsvStartVChanged(s) => self.lsv_start_v = s, + Message::LsvStopVChanged(s) => self.lsv_stop_v = s, + Message::LsvScanRateChanged(s) => self.lsv_scan_rate = s, + Message::LsvRtiaSelected(r) => self.lsv_rtia = r, + Message::StartLsv => { + let vs = self.lsv_start_v.parse::().unwrap_or(0.0); + let ve = self.lsv_stop_v.parse::().unwrap_or(500.0); + let sr = self.lsv_scan_rate.parse::().unwrap_or(50.0); + self.send_cmd(&protocol::build_sysex_get_temp()); + self.send_cmd(&protocol::build_sysex_start_lsv(vs, ve, sr, self.lsv_rtia)); + } + /* Amp */ + Message::AmpVholdChanged(s) => self.amp_v_hold = s, + Message::AmpIntervalChanged(s) => self.amp_interval = s, + Message::AmpDurationChanged(s) => self.amp_duration = s, + Message::AmpRtiaSelected(r) => self.amp_rtia = r, + Message::StartAmp => { + let vh = self.amp_v_hold.parse::().unwrap_or(200.0); + let iv = self.amp_interval.parse::().unwrap_or(100.0); + let dur = self.amp_duration.parse::().unwrap_or(60.0); + self.send_cmd(&protocol::build_sysex_get_temp()); + self.send_cmd(&protocol::build_sysex_start_amp(vh, iv, dur, self.amp_rtia)); + } + Message::StopAmp => { + self.send_cmd(&protocol::build_sysex_stop_amp()); + } + /* Chlorine */ + Message::ClCondVChanged(s) => self.cl_cond_v = s, + Message::ClCondTChanged(s) => self.cl_cond_t = s, + Message::ClFreeVChanged(s) => self.cl_free_v = s, + Message::ClTotalVChanged(s) => self.cl_total_v = s, + Message::ClDepTChanged(s) => self.cl_dep_t = s, + Message::ClMeasTChanged(s) => self.cl_meas_t = s, + Message::ClRtiaSelected(r) => self.cl_rtia = r, + Message::StartCl => { + let v_cond = self.cl_cond_v.parse::().unwrap_or(800.0); + let t_cond = self.cl_cond_t.parse::().unwrap_or(2000.0); + let v_free = self.cl_free_v.parse::().unwrap_or(100.0); + let v_total = self.cl_total_v.parse::().unwrap_or(-200.0); + let t_dep = self.cl_dep_t.parse::().unwrap_or(5000.0); + let t_meas = self.cl_meas_t.parse::().unwrap_or(5000.0); + self.send_cmd(&protocol::build_sysex_get_temp()); + self.send_cmd(&protocol::build_sysex_start_cl( + v_cond, t_cond, v_free, v_total, t_dep, t_meas, self.cl_rtia, + )); + } + /* pH */ + Message::PhStabilizeChanged(s) => self.ph_stabilize = s, + Message::StartPh => { + let stab = self.ph_stabilize.parse::().unwrap_or(30.0); + self.send_cmd(&protocol::build_sysex_get_temp()); + self.send_cmd(&protocol::build_sysex_start_ph(stab)); + } + /* Reference baseline */ + Message::SetReference => { + match self.tab { + Tab::Eis if !self.eis_points.is_empty() => { + self.eis_ref = Some(self.eis_points.clone()); + self.status = format!("EIS reference set ({} pts)", self.eis_points.len()); + } + Tab::Lsv if !self.lsv_points.is_empty() => { + self.lsv_ref = Some(self.lsv_points.clone()); + self.status = format!("LSV reference set ({} pts)", self.lsv_points.len()); + } + Tab::Amp if !self.amp_points.is_empty() => { + self.amp_ref = Some(self.amp_points.clone()); + self.status = format!("Amp reference set ({} pts)", self.amp_points.len()); + } + Tab::Chlorine if !self.cl_points.is_empty() => { + if let Some(r) = &self.cl_result { + self.cl_ref = Some((self.cl_points.clone(), r.clone())); + self.status = "Chlorine reference set".into(); + } + } + Tab::Ph => { + if let Some(r) = &self.ph_result { + self.ph_ref = Some(r.clone()); + self.status = format!("pH reference set ({:.2})", r.ph); + } + } + _ => {} + } + } + Message::ClearReference => { + match self.tab { + Tab::Eis => { self.eis_ref = None; self.status = "EIS reference cleared".into(); } + Tab::Lsv => { self.lsv_ref = None; self.status = "LSV reference cleared".into(); } + Tab::Amp => { self.amp_ref = None; self.status = "Amp reference cleared".into(); } + Tab::Chlorine => { self.cl_ref = None; self.status = "Chlorine reference cleared".into(); } + Tab::Ph => { self.ph_ref = None; self.status = "pH reference cleared".into(); } + } + } + /* Global */ + Message::PollTemp => { + self.send_cmd(&protocol::build_sysex_get_temp()); + } + Message::NativeMenuTick => { + for action in self.native_menu.poll_events() { + match action { + MenuAction::SystemInfo => self.show_sysinfo = !self.show_sysinfo, + } + } + } + Message::CloseSysInfo => { + self.show_sysinfo = false; + } + Message::OpenMidiSetup => { + let _ = std::process::Command::new("open") + .arg("-a") + .arg("Audio MIDI Setup") + .spawn(); + } } Task::none() } pub fn subscription(&self) -> Subscription { - Subscription::run_with_id( + let ble = Subscription::run_with_id( "ble", iced::stream::channel(100, |mut output| async move { loop { - let (ble_tx, mut ble_rx) = - mpsc::unbounded_channel::(); - let (cmd_tx, cmd_rx) = - mpsc::unbounded_channel::>(); + let (ble_tx, mut ble_rx) = mpsc::unbounded_channel::(); + let (cmd_tx, cmd_rx) = mpsc::unbounded_channel::>(); let _ = output.send(Message::BleReady(cmd_tx)).await; @@ -149,94 +593,387 @@ impl App { let _ = output.send(msg).await; } - let _ = output - .send(Message::BleStatus("Reconnecting...".into())) - .await; - tokio::time::sleep(Duration::from_secs(2)).await; + let _ = output.send(Message::BleStatus("Reconnecting...".into())).await; + tokio::time::sleep(Duration::from_millis(500)).await; } }), - ) + ); + + let temp_poll = iced::time::every(Duration::from_millis(500)) + .map(|_| Message::PollTemp); + let menu_tick = iced::time::every(Duration::from_millis(50)) + .map(|_| Message::NativeMenuTick); + + Subscription::batch([ble, temp_poll, menu_tick]) } pub fn view(&self) -> Element<'_, Message> { - let status = text(&self.status).size(16); + let tab_btn = |label: &'static str, t: Tab, active: bool| -> Element<'_, Message> { + let b = button(text(label).size(13)) + .style(style_tab(active)) + .padding([6, 14]); + if active { b.into() } else { b.on_press(Message::TabSelected(t)).into() } + }; - let controls = row![ - column![ - text("Start Hz").size(12), - text_input("1000", &self.freq_start) - .on_input(Message::FreqStartChanged) - .width(100), - ] - .spacing(2), - column![ - text("Stop Hz").size(12), - text_input("200000", &self.freq_stop) - .on_input(Message::FreqStopChanged) - .width(100), - ] - .spacing(2), - column![ - text("PPD").size(12), - text_input("10", &self.ppd) - .on_input(Message::PpdChanged) - .width(60), - ] - .spacing(2), - column![ - text("RTIA").size(12), - pick_list(Rtia::ALL, Some(self.rtia), Message::RtiaSelected), - ] - .spacing(2), - column![ - text("RCAL").size(12), - pick_list(Rcal::ALL, Some(self.rcal), Message::RcalSelected), - ] - .spacing(2), - button("Apply").on_press(Message::ApplySettings), - button("Sweep").on_press(Message::StartSweep), + let tabs = row![ + tab_btn("EIS", Tab::Eis, self.tab == Tab::Eis), + tab_btn("LSV", Tab::Lsv, self.tab == Tab::Lsv), + tab_btn("Amperometry", Tab::Amp, self.tab == Tab::Amp), + tab_btn("Chlorine", Tab::Chlorine, self.tab == Tab::Chlorine), + tab_btn("pH", Tab::Ph, self.tab == Tab::Ph), + button(text("MIDI Setup").size(13)) + .style(style_neutral()) + .padding([6, 14]) + .on_press(Message::OpenMidiSetup), ] - .spacing(10) - .align_y(iced::Alignment::End); + .spacing(4); - let header = row![ - text("Freq (Hz)").width(100), - text("|Z| (Ohm)").width(100), - text("Phase (deg)").width(100), - text("Re (Ohm)").width(100), - text("Im (Ohm)").width(100), - ] - .spacing(10); + let has_ref = match self.tab { + Tab::Eis => self.eis_ref.is_some(), + Tab::Lsv => self.lsv_ref.is_some(), + Tab::Amp => self.amp_ref.is_some(), + Tab::Chlorine => self.cl_ref.is_some(), + Tab::Ph => self.ph_ref.is_some(), + }; + let has_data = match self.tab { + Tab::Eis => !self.eis_points.is_empty(), + Tab::Lsv => !self.lsv_points.is_empty(), + Tab::Amp => !self.amp_points.is_empty(), + Tab::Chlorine => self.cl_result.is_some(), + Tab::Ph => self.ph_result.is_some(), + }; - let mut data_rows = column![header].spacing(4); - for pt in &self.points { - data_rows = data_rows.push( - row![ - text(format!("{:.1}", pt.freq_hz)).width(100), - text(format!("{:.2}", pt.mag_ohms)).width(100), - text(format!("{:.2}", pt.phase_deg)).width(100), - text(format!("{:.2}", pt.z_real)).width(100), - text(format!("{:.2}", pt.z_imag)).width(100), - ] - .spacing(10), + let mut ref_row = row![].spacing(4).align_y(iced::Alignment::Center); + if has_data { + ref_row = ref_row.push( + button(text("Set Ref").size(11)) + .style(style_neutral()) + .padding([4, 10]) + .on_press(Message::SetReference), ); } + if has_ref { + ref_row = ref_row.push( + button(text("Clear Ref").size(11)) + .style(style_danger()) + .padding([4, 10]) + .on_press(Message::ClearReference), + ); + ref_row = ref_row.push(text("REF").size(11)); + } - let bode = canvas(crate::plot::BodePlot { points: &self.points }) - .width(Length::FillPortion(3)) - .height(300); - let nyquist = canvas(crate::plot::NyquistPlot { points: &self.points }) - .width(Length::FillPortion(2)) - .height(300); - let plots = row![bode, nyquist].spacing(10); + let status_row = row![ + text(&self.status).size(16), + iced::widget::horizontal_space(), + ref_row, + text(format!("{:.1} C", self.temp_c)).size(14), + ] + .spacing(6) + .align_y(iced::Alignment::Center); - let content = column![status, controls, plots, scrollable(data_rows)] - .spacing(20) - .padding(20); + let controls = self.view_controls(); - container(content) + let body: Element<'_, Message> = if self.show_sysinfo { + self.view_sysinfo() + } else if self.tab == Tab::Ph { + self.view_ph_body() + } else { + pane_grid::PaneGrid::new(&self.panes, |_pane, &pane_id, _maximized| { + let el = match pane_id { + PaneId::Plot => self.view_plot_pane(), + PaneId::Data => self.view_data_pane(), + }; + pane_grid::Content::new(el) + }) + .on_resize(6, Message::PaneResized) .width(Length::Fill) .height(Length::Fill) .into() + }; + + container( + column![tabs, status_row, controls, body] + .spacing(10) + .padding(20) + .width(Length::Fill) + .height(Length::Fill), + ) + .width(Length::Fill) + .height(Length::Fill) + .into() + } + + fn view_controls(&self) -> Element<'_, Message> { + match self.tab { + Tab::Eis => row![ + column![ + text("Start Hz").size(12), + text_input("1000", &self.freq_start).on_input(Message::FreqStartChanged).width(100), + ].spacing(2), + column![ + text("Stop Hz").size(12), + text_input("200000", &self.freq_stop).on_input(Message::FreqStopChanged).width(100), + ].spacing(2), + column![ + text("PPD").size(12), + text_input("10", &self.ppd).on_input(Message::PpdChanged).width(60), + ].spacing(2), + column![ + text("RTIA").size(12), + pick_list(Rtia::ALL, Some(self.rtia), Message::RtiaSelected), + ].spacing(2), + column![ + text("RCAL").size(12), + pick_list(Rcal::ALL, Some(self.rcal), Message::RcalSelected), + ].spacing(2), + column![ + text("Electrodes").size(12), + pick_list(Electrode::ALL, Some(self.electrode), Message::ElectrodeSelected), + ].spacing(2), + button(text("Apply").size(13)) + .style(style_apply()) + .padding([6, 16]) + .on_press(Message::ApplySettings), + button(text("Sweep").size(13)) + .style(style_action()) + .padding([6, 20]) + .on_press(Message::StartSweep), + ] + .spacing(10) + .align_y(iced::Alignment::End) + .into(), + + Tab::Lsv => row![ + column![ + text("Start mV").size(12), + text_input("0", &self.lsv_start_v).on_input(Message::LsvStartVChanged).width(80), + ].spacing(2), + column![ + text("Stop mV").size(12), + text_input("500", &self.lsv_stop_v).on_input(Message::LsvStopVChanged).width(80), + ].spacing(2), + column![ + text("Scan mV/s").size(12), + text_input("50", &self.lsv_scan_rate).on_input(Message::LsvScanRateChanged).width(80), + ].spacing(2), + column![ + text("RTIA").size(12), + pick_list(LpRtia::ALL, Some(self.lsv_rtia), Message::LsvRtiaSelected), + ].spacing(2), + button(text("Start LSV").size(13)) + .style(style_action()) + .padding([6, 16]) + .on_press(Message::StartLsv), + ] + .spacing(10) + .align_y(iced::Alignment::End) + .into(), + + Tab::Amp => row![ + column![ + text("V hold mV").size(12), + text_input("200", &self.amp_v_hold).on_input(Message::AmpVholdChanged).width(80), + ].spacing(2), + column![ + text("Interval ms").size(12), + text_input("100", &self.amp_interval).on_input(Message::AmpIntervalChanged).width(80), + ].spacing(2), + column![ + text("Duration s").size(12), + text_input("60", &self.amp_duration).on_input(Message::AmpDurationChanged).width(80), + ].spacing(2), + column![ + text("RTIA").size(12), + pick_list(LpRtia::ALL, Some(self.amp_rtia), Message::AmpRtiaSelected), + ].spacing(2), + if self.amp_running { + button(text("Stop").size(13)) + .style(style_danger()) + .padding([6, 16]) + .on_press(Message::StopAmp) + } else { + button(text("Start Amp").size(13)) + .style(style_action()) + .padding([6, 16]) + .on_press(Message::StartAmp) + }, + ] + .spacing(10) + .align_y(iced::Alignment::End) + .into(), + + Tab::Chlorine => row![ + column![ + text("Cond mV").size(12), + text_input("800", &self.cl_cond_v).on_input(Message::ClCondVChanged).width(70), + ].spacing(2), + column![ + text("Cond ms").size(12), + text_input("2000", &self.cl_cond_t).on_input(Message::ClCondTChanged).width(70), + ].spacing(2), + column![ + text("Free mV").size(12), + text_input("100", &self.cl_free_v).on_input(Message::ClFreeVChanged).width(70), + ].spacing(2), + column![ + text("Total mV").size(12), + text_input("-200", &self.cl_total_v).on_input(Message::ClTotalVChanged).width(70), + ].spacing(2), + column![ + text("Settle ms").size(12), + text_input("5000", &self.cl_dep_t).on_input(Message::ClDepTChanged).width(70), + ].spacing(2), + column![ + text("Meas ms").size(12), + text_input("5000", &self.cl_meas_t).on_input(Message::ClMeasTChanged).width(70), + ].spacing(2), + column![ + text("RTIA").size(12), + pick_list(LpRtia::ALL, Some(self.cl_rtia), Message::ClRtiaSelected), + ].spacing(2), + button(text("Measure").size(13)) + .style(style_action()) + .padding([6, 16]) + .on_press(Message::StartCl), + ] + .spacing(8) + .align_y(iced::Alignment::End) + .into(), + + Tab::Ph => row![ + column![ + text("Stabilize s").size(12), + text_input("30", &self.ph_stabilize).on_input(Message::PhStabilizeChanged).width(80), + ].spacing(2), + button(text("Measure pH").size(13)) + .style(style_action()) + .padding([6, 16]) + .on_press(Message::StartPh), + ] + .spacing(10) + .align_y(iced::Alignment::End) + .into(), + } + } + + fn view_plot_pane(&self) -> Element<'_, Message> { + match self.tab { + Tab::Eis => { + let bode = canvas(crate::plot::BodePlot { + points: &self.eis_points, + reference: self.eis_ref.as_deref(), + }) + .width(Length::FillPortion(3)) + .height(Length::Fill); + let nyquist = canvas(crate::plot::NyquistPlot { + points: &self.eis_points, + reference: self.eis_ref.as_deref(), + }) + .width(Length::FillPortion(2)) + .height(Length::Fill); + row![bode, nyquist].spacing(10).height(Length::Fill).into() + } + Tab::Lsv => canvas(crate::plot::VoltammogramPlot { + points: &self.lsv_points, + reference: self.lsv_ref.as_deref(), + }) + .width(Length::Fill).height(Length::Fill).into(), + Tab::Amp => canvas(crate::plot::AmperogramPlot { + points: &self.amp_points, + reference: self.amp_ref.as_deref(), + }) + .width(Length::Fill).height(Length::Fill).into(), + Tab::Chlorine => { + let ref_pts = self.cl_ref.as_ref().map(|(pts, _)| pts.as_slice()); + let plot = canvas(crate::plot::ChlorinePlot { + points: &self.cl_points, + reference: ref_pts, + }) + .width(Length::Fill).height(Length::Fill); + let mut result_parts: Vec = Vec::new(); + if let Some(r) = &self.cl_result { + result_parts.push(format!( + "Free: {:.3} uA | Total: {:.3} uA | Combined: {:.3} uA", + r.i_free_ua, r.i_total_ua, r.i_total_ua - r.i_free_ua + )); + if let Some((_, ref_r)) = &self.cl_ref { + let df = r.i_free_ua - ref_r.i_free_ua; + let dt = r.i_total_ua - ref_r.i_total_ua; + result_parts.push(format!( + "vs Ref: dFree={:.3} uA, dTotal={:.3} uA", + df, dt + )); + } + } + if result_parts.is_empty() { + plot.into() + } else { + let result_text = text(result_parts.join(" ")).size(14); + column![result_text, plot].spacing(4).height(Length::Fill).into() + } + } + Tab::Ph => text("").into(), + } + } + + fn view_data_pane(&self) -> Element<'_, Message> { + let content = match self.tab { + Tab::Eis => &self.eis_data, + Tab::Lsv => &self.lsv_data, + Tab::Amp => &self.amp_data, + Tab::Chlorine => &self.cl_data, + Tab::Ph => return text("").into(), + }; + text_editor(content) + .on_action(Message::DataAction) + .font(iced::Font::MONOSPACE) + .size(13) + .height(Length::Fill) + .padding(4) + .into() + } + + fn view_ph_body(&self) -> Element<'_, Message> { + if let Some(r) = &self.ph_result { + let nernst_slope = 0.1984 * (r.temp_c as f64 + 273.15); + let mut col = column![ + text(format!("pH: {:.2}", r.ph)).size(28), + text(format!("OCP: {:.1} mV | Nernst slope: {:.2} mV/pH | Temp: {:.1} C", + r.v_ocp_mv, nernst_slope, r.temp_c)).size(14), + ].spacing(4); + if let Some(ref_r) = &self.ph_ref { + let d_ph = r.ph - ref_r.ph; + let d_v = r.v_ocp_mv - ref_r.v_ocp_mv; + col = col.push(text(format!( + "vs Ref: dpH={:+.3} dOCP={:+.1} mV (ref pH={:.2})", + d_ph, d_v, ref_r.ph + )).size(14)); + } + col.into() + } else { + column![ + text("No measurement yet").size(16), + text("OCP method: V(SE0) - V(RE0) with Nernst correction").size(12), + ].spacing(4).into() + } + } + + fn view_sysinfo(&self) -> Element<'_, Message> { + container( + column![ + text("System Info").size(20), + iced::widget::horizontal_rule(1), + text(format!("Temperature: {:.1} C", self.temp_c)).size(14), + text("Syslog: not yet implemented").size(14), + text("0 / 4,194,304 bytes used").size(14), + iced::widget::vertical_space().height(20), + button(text("Close").size(14)).on_press(Message::CloseSysInfo), + ] + .spacing(8) + .width(350) + .padding(20), + ) + .center(Length::Fill) + .into() } } diff --git a/cue/src/ble.rs b/cue/src/ble.rs index f7fb7db..892d22e 100644 --- a/cue/src/ble.rs +++ b/cue/src/ble.rs @@ -22,7 +22,7 @@ pub async fn connect_and_run( if let Some(found) = find_midi_ports() { break found; } - tokio::time::sleep(std::time::Duration::from_secs(2)).await; + tokio::time::sleep(std::time::Duration::from_millis(500)).await; }; let _ = tx.send(BleEvent::Status("Connecting MIDI...".into())); @@ -52,19 +52,20 @@ pub async fn connect_and_run( } } - match cmd_rx.try_recv() { - Ok(pkt) => { - out_conn.send(&pkt).ok(); + loop { + match cmd_rx.try_recv() { + Ok(pkt) => { + if let Err(e) = out_conn.send(&pkt) { + eprintln!("MIDI send error: {e}"); + } + } + Err(mpsc::error::TryRecvError::Disconnected) => return Ok(()), + Err(mpsc::error::TryRecvError::Empty) => break, } - Err(mpsc::error::TryRecvError::Disconnected) => break, - Err(mpsc::error::TryRecvError::Empty) => {} } tokio::time::sleep(std::time::Duration::from_millis(5)).await; } - - let _ = tx.send(BleEvent::Status("Disconnected".into())); - Ok(()) } fn find_midi_ports() -> Option<( diff --git a/cue/src/main.rs b/cue/src/main.rs index 9022914..ce0f7c9 100644 --- a/cue/src/main.rs +++ b/cue/src/main.rs @@ -1,5 +1,6 @@ mod app; mod ble; +mod native_menu; mod plot; mod protocol; diff --git a/cue/src/native_menu.rs b/cue/src/native_menu.rs new file mode 100644 index 0000000..e9908ba --- /dev/null +++ b/cue/src/native_menu.rs @@ -0,0 +1,58 @@ +use muda::{Menu, MenuEvent, MenuId, MenuItem, PredefinedMenuItem, Submenu}; +use std::collections::HashMap; + +#[derive(Debug, Clone)] +pub enum MenuAction { + SystemInfo, +} + +pub struct NativeMenu { + menu: Menu, + action_map: HashMap, + attached: bool, +} + +impl NativeMenu { + pub fn init() -> Self { + let menu = Menu::new(); + let mut action_map = HashMap::new(); + + let app_menu = Submenu::new("Cue", true); + let _ = app_menu.append(&PredefinedMenuItem::about(None, None)); + let _ = app_menu.append(&PredefinedMenuItem::separator()); + + let sys_info = MenuItem::new("System Info", true, None::); + action_map.insert(sys_info.id().clone(), MenuAction::SystemInfo); + let _ = app_menu.append(&sys_info); + + let _ = app_menu.append(&PredefinedMenuItem::separator()); + let _ = app_menu.append(&PredefinedMenuItem::quit(None)); + + let _ = menu.append(&app_menu); + + Self { + menu, + action_map, + attached: false, + } + } + + pub fn ensure_attached(&mut self) { + if !self.attached { + #[cfg(target_os = "macos")] + self.menu.init_for_nsapp(); + self.attached = true; + } + } + + pub fn poll_events(&mut self) -> Vec { + self.ensure_attached(); + let mut actions = Vec::new(); + while let Ok(event) = MenuEvent::receiver().try_recv() { + if let Some(action) = self.action_map.get(event.id()) { + actions.push(action.clone()); + } + } + actions + } +} diff --git a/cue/src/plot.rs b/cue/src/plot.rs index 87987b5..0b6eeca 100644 --- a/cue/src/plot.rs +++ b/cue/src/plot.rs @@ -3,7 +3,7 @@ use iced::{Color, Point, Rectangle, Renderer, Theme}; use iced::mouse; use crate::app::Message; -use crate::protocol::EisPoint; +use crate::protocol::{AmpPoint, ClPoint, EisPoint, LsvPoint}; const MARGIN_L: f32 = 55.0; const MARGIN_R: f32 = 15.0; @@ -13,11 +13,17 @@ const MARGIN_B: f32 = 25.0; const COL_MAG: Color = Color { r: 0.3, g: 0.85, b: 1.0, a: 1.0 }; const COL_PH: Color = Color { r: 1.0, g: 0.55, b: 0.2, a: 1.0 }; const COL_NYQ: Color = Color { r: 0.4, g: 1.0, b: 0.4, a: 1.0 }; +const COL_LSV: Color = Color { r: 1.0, g: 0.8, b: 0.2, a: 1.0 }; +const COL_AMP: Color = Color { r: 0.6, g: 0.6, b: 1.0, a: 1.0 }; +const COL_CL_FREE: Color = Color { r: 0.2, g: 1.0, b: 0.5, a: 1.0 }; +const COL_CL_TOTAL: Color = Color { r: 1.0, g: 0.6, b: 0.2, a: 1.0 }; const COL_GRID: Color = Color { r: 0.25, g: 0.25, b: 0.28, a: 1.0 }; const COL_AXIS: Color = Color { r: 0.6, g: 0.6, b: 0.6, a: 1.0 }; const COL_DIM: Color = Color { r: 0.4, g: 0.4, b: 0.4, a: 1.0 }; +const COL_REF: Color = Color { r: 0.5, g: 0.5, b: 0.5, a: 0.5 }; const ZOOM_FACTOR: f32 = 1.15; +const DRAG_ZOOM_RATE: f32 = 200.0; /* ---- View range ---- */ @@ -76,39 +82,89 @@ fn dt(frame: &mut Frame, pos: Point, txt: &str, color: Color, size: f32) { }); } +fn pt_ok(p: &Point) -> bool { p.x.is_finite() && p.y.is_finite() } + fn draw_polyline(frame: &mut Frame, pts: &[Point], color: Color, width: f32) { - if pts.len() < 2 { return; } + let good: Vec = pts.iter().copied().filter(pt_ok).collect(); + if good.len() < 2 { return; } let path = Path::new(|b| { - b.move_to(pts[0]); - for p in &pts[1..] { b.line_to(*p); } + b.move_to(good[0]); + for p in &good[1..] { b.line_to(*p); } }); frame.stroke(&path, Stroke::default().with_color(color).with_width(width)); } fn draw_dots(frame: &mut Frame, pts: &[Point], color: Color, r: f32) { - for p in pts { frame.fill(&Path::circle(*p, r), color); } + for p in pts.iter().filter(|p| pt_ok(p)) { + frame.fill(&Path::circle(*p, r), color); + } } -/* ---- Bode state: zoom/pan on frequency axis, Y auto-scales ---- */ +/* ---- Bode ---- */ #[derive(Default)] pub struct BodeState { freq: Option, - drag: Option<(f32, Vr)>, + mag_y: Option, + ph_y: Option, + left_drag: Option<(Point, Vr, Vr, Vr)>, + right_drag: Option<(Point, Vr, Vr, Vr)>, } pub struct BodePlot<'a> { pub points: &'a [EisPoint], + pub reference: Option<&'a [EisPoint]>, } impl BodePlot<'_> { fn auto_freq(&self) -> Option { if self.points.is_empty() { return None; } - let lo = self.points.iter().map(|p| p.freq_hz.log10()).fold(f32::INFINITY, f32::min); - let hi = self.points.iter().map(|p| p.freq_hz.log10()).fold(f32::NEG_INFINITY, f32::max); + let valid = self.points.iter().filter(|p| p.freq_hz > 0.0); + let lo = valid.clone().map(|p| p.freq_hz.log10()).fold(f32::INFINITY, f32::min); + let hi = valid.map(|p| p.freq_hz.log10()).fold(f32::NEG_INFINITY, f32::max); + if !lo.is_finite() || !hi.is_finite() { return None; } let pad = (hi - lo).max(0.1) * 0.05; Some(Vr::new(lo - pad, hi + pad)) } + + fn auto_mag(&self, freq: &Vr) -> Vr { + let vis: Vec<_> = self.points.iter() + .filter(|p| p.freq_hz > 0.0 && p.mag_ohms.is_finite()) + .filter(|p| { let lf = p.freq_hz.log10(); lf >= freq.lo && lf <= freq.hi }) + .collect(); + let pts: Vec<_> = if vis.is_empty() { + self.points.iter().filter(|p| p.mag_ohms.is_finite()).collect() + } else { vis }; + if pts.is_empty() { return Vr::new(0.0, 1.0); } + let (lo, hi) = pts.iter().fold((f32::INFINITY, f32::NEG_INFINITY), |(lo, hi), p| { + (lo.min(p.mag_ohms), hi.max(p.mag_ohms)) + }); + let pad = (hi - lo).max(1.0) * 0.12; + Vr::new(lo - pad, hi + pad) + } + + fn auto_phase(&self, freq: &Vr) -> Vr { + let vis: Vec<_> = self.points.iter() + .filter(|p| p.freq_hz > 0.0 && p.phase_deg.is_finite()) + .filter(|p| { let lf = p.freq_hz.log10(); lf >= freq.lo && lf <= freq.hi }) + .collect(); + let pts: Vec<_> = if vis.is_empty() { + self.points.iter().filter(|p| p.phase_deg.is_finite()).collect() + } else { vis }; + if pts.is_empty() { return Vr::new(-90.0, 0.0); } + let (lo, hi) = pts.iter().fold((f32::INFINITY, f32::NEG_INFINITY), |(lo, hi), p| { + (lo.min(p.phase_deg), hi.max(p.phase_deg)) + }); + let pad = (hi - lo).max(0.1) * 0.25; + Vr::new(lo - pad, hi + pad) + } + + fn effective_ranges(&self, state: &BodeState) -> (Vr, Vr, Vr) { + let freq = state.freq.unwrap_or_else(|| self.auto_freq().unwrap_or(Vr::new(3.0, 5.3))); + let mag = state.mag_y.unwrap_or_else(|| self.auto_mag(&freq)); + let ph = state.ph_y.unwrap_or_else(|| self.auto_phase(&freq)); + (freq, mag, ph) + } } impl<'a> canvas::Program for BodePlot<'a> { @@ -118,6 +174,26 @@ impl<'a> canvas::Program for BodePlot<'a> { &self, state: &mut BodeState, event: Event, bounds: Rectangle, cursor: mouse::Cursor, ) -> (canvas::event::Status, Option) { + if let Event::Mouse(mouse::Event::ButtonReleased(_)) = &event { + let was_right = state.right_drag.is_some(); + if was_right { + if let Some(pos) = cursor.position_in(bounds) { + let (start, _, _, _) = state.right_drag.unwrap(); + let dist = ((pos.x - start.x).powi(2) + (pos.y - start.y).powi(2)).sqrt(); + if dist < 3.0 { + state.freq = None; + state.mag_y = None; + state.ph_y = None; + } + } + } + state.left_drag = None; + state.right_drag = None; + if was_right || state.left_drag.is_some() { + return (canvas::event::Status::Captured, None); + } + } + let Some(pos) = cursor.position_in(bounds) else { return (canvas::event::Status::Ignored, None); }; @@ -132,34 +208,49 @@ impl<'a> canvas::Program for BodePlot<'a> { }; let factor = ZOOM_FACTOR.powf(dy); let frac = screen_frac(pos.x, xl, xr); - let mut vr = state.freq.unwrap_or_else(|| self.auto_freq().unwrap_or(Vr::new(3.0, 5.3))); - vr.zoom_at(factor, frac); - state.freq = Some(vr); + let (mut freq, _, _) = self.effective_ranges(state); + freq.zoom_at(factor, frac); + state.freq = Some(freq); (canvas::event::Status::Captured, None) } Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { - let vr = state.freq.unwrap_or_else(|| self.auto_freq().unwrap_or(Vr::new(3.0, 5.3))); - state.drag = Some((pos.x, vr)); - (canvas::event::Status::Captured, None) - } - Event::Mouse(mouse::Event::CursorMoved { .. }) => { - if let Some((start_x, start_vr)) = state.drag { - let dx_frac = (pos.x - start_x) / (xr - xl); - let mut vr = start_vr; - vr.pan_frac(dx_frac); - state.freq = Some(vr); - return (canvas::event::Status::Captured, None); - } - (canvas::event::Status::Ignored, None) - } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { - state.drag = None; + let (freq, mag, ph) = self.effective_ranges(state); + state.left_drag = Some((pos, freq, mag, ph)); (canvas::event::Status::Captured, None) } Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) => { - state.freq = None; + let (freq, mag, ph) = self.effective_ranges(state); + state.right_drag = Some((pos, freq, mag, ph)); (canvas::event::Status::Captured, None) } + Event::Mouse(mouse::Event::CursorMoved { .. }) => { + if let Some((start, sf, sm, sp)) = state.left_drag { + let dx = (pos.x - start.x) / (xr - xl); + let dy = (pos.y - start.y) / (bounds.height - MARGIN_T - MARGIN_B); + let mut freq = sf; freq.pan_frac(dx); + let mut mag = sm; mag.pan_frac(-dy); + let mut ph = sp; ph.pan_frac(-dy); + state.freq = Some(freq); + state.mag_y = Some(mag); + state.ph_y = Some(ph); + return (canvas::event::Status::Captured, None); + } + if let Some((start, sf, sm, sp)) = state.right_drag { + let dx = pos.x - start.x; + let dy = pos.y - start.y; + let xf = 2.0_f32.powf(dx / DRAG_ZOOM_RATE); + let yf = 2.0_f32.powf(-dy / DRAG_ZOOM_RATE); + let frac_x = screen_frac(start.x, xl, xr); + let mut freq = sf; freq.zoom_at(xf, frac_x); + let mut mag = sm; mag.zoom_at(yf, 0.5); + let mut ph = sp; ph.zoom_at(yf, 0.5); + state.freq = Some(freq); + state.mag_y = Some(mag); + state.ph_y = Some(ph); + return (canvas::event::Status::Captured, None); + } + (canvas::event::Status::Ignored, None) + } _ => (canvas::event::Status::Ignored, None), } } @@ -181,24 +272,8 @@ impl<'a> canvas::Program for BodePlot<'a> { let xr = w - MARGIN_R; let fv = state.freq.unwrap_or_else(|| self.auto_freq().unwrap()); - - // filter visible points for Y auto-scale - let vis: Vec<&EisPoint> = self.points.iter() - .filter(|p| { let lf = p.freq_hz.log10(); lf >= fv.lo && lf <= fv.hi }) - .collect(); - let all = if vis.is_empty() { self.points.iter().collect::>() } else { vis }; - - let (m_min, m_max) = all.iter().fold((f32::INFINITY, f32::NEG_INFINITY), |(lo, hi), p| { - (lo.min(p.mag_ohms), hi.max(p.mag_ohms)) - }); - let m_pad = (m_max - m_min).max(1.0) * 0.12; - let (m_lo, m_hi) = (m_min - m_pad, m_max + m_pad); - - let (p_min, p_max) = all.iter().fold((f32::INFINITY, f32::NEG_INFINITY), |(lo, hi), p| { - (lo.min(p.phase_deg), hi.max(p.phase_deg)) - }); - let p_pad = (p_max - p_min).max(0.1) * 0.25; - let (p_lo, p_hi) = (p_min - p_pad, p_max + p_pad); + let mv = state.mag_y.unwrap_or_else(|| self.auto_mag(&fv)); + let pv = state.ph_y.unwrap_or_else(|| self.auto_phase(&fv)); // freq grid let d0 = fv.lo.floor() as i32; @@ -217,11 +292,11 @@ impl<'a> canvas::Program for BodePlot<'a> { // magnitude let mag_top = MARGIN_T; let mag_bot = split - 14.0; - let m_step = nice_step(m_hi - m_lo, 4); + let m_step = nice_step(mv.span(), 4); if m_step > 0.0 { - let mut mg = (m_lo / m_step).ceil() * m_step; - while mg <= m_hi { - let y = lerp(mg, m_hi, m_lo, mag_top, mag_bot); + let mut mg = (mv.lo / m_step).ceil() * m_step; + while mg <= mv.hi { + let y = lerp(mg, mv.hi, mv.lo, mag_top, mag_bot); dl(&mut frame, Point::new(xl, y), Point::new(xr, y), COL_GRID, 0.5); dt(&mut frame, Point::new(2.0, y - 5.0), &format!("{:.0}", mg), COL_MAG, 9.0); mg += m_step; @@ -229,9 +304,17 @@ impl<'a> canvas::Program for BodePlot<'a> { } dt(&mut frame, Point::new(2.0, mag_top - 2.0), "|Z|", COL_MAG, 10.0); + if let Some(rpts) = self.reference { + let rm: Vec = rpts.iter().map(|p| Point::new( + lerp(p.freq_hz.log10(), fv.lo, fv.hi, xl, xr), + lerp(p.mag_ohms, mv.hi, mv.lo, mag_top, mag_bot), + )).collect(); + draw_polyline(&mut frame, &rm, COL_REF, 1.5); + } + let mag_pts: Vec = self.points.iter().map(|p| Point::new( lerp(p.freq_hz.log10(), fv.lo, fv.hi, xl, xr), - lerp(p.mag_ohms, m_hi, m_lo, mag_top, mag_bot), + lerp(p.mag_ohms, mv.hi, mv.lo, mag_top, mag_bot), )).collect(); draw_polyline(&mut frame, &mag_pts, COL_MAG, 2.0); draw_dots(&mut frame, &mag_pts, COL_MAG, 2.5); @@ -239,11 +322,11 @@ impl<'a> canvas::Program for BodePlot<'a> { // phase let ph_top = split + 12.0; let ph_bot = h - MARGIN_B; - let p_step = nice_step(p_hi - p_lo, 3); + let p_step = nice_step(pv.span(), 3); if p_step > 0.0 { - let mut pg = (p_lo / p_step).ceil() * p_step; - while pg <= p_hi { - let y = lerp(pg, p_hi, p_lo, ph_top, ph_bot); + let mut pg = (pv.lo / p_step).ceil() * p_step; + while pg <= pv.hi { + let y = lerp(pg, pv.hi, pv.lo, ph_top, ph_bot); dl(&mut frame, Point::new(xl, y), Point::new(xr, y), COL_GRID, 0.5); dt(&mut frame, Point::new(2.0, y - 5.0), &format!("{:.1}", pg), COL_PH, 9.0); pg += p_step; @@ -251,14 +334,22 @@ impl<'a> canvas::Program for BodePlot<'a> { } dt(&mut frame, Point::new(2.0, ph_top - 2.0), "Phase", COL_PH, 10.0); + if let Some(rpts) = self.reference { + let rp: Vec = rpts.iter().map(|p| Point::new( + lerp(p.freq_hz.log10(), fv.lo, fv.hi, xl, xr), + lerp(p.phase_deg, pv.hi, pv.lo, ph_top, ph_bot), + )).collect(); + draw_polyline(&mut frame, &rp, COL_REF, 1.5); + } + let ph_pts: Vec = self.points.iter().map(|p| Point::new( lerp(p.freq_hz.log10(), fv.lo, fv.hi, xl, xr), - lerp(p.phase_deg, p_hi, p_lo, ph_top, ph_bot), + lerp(p.phase_deg, pv.hi, pv.lo, ph_top, ph_bot), )).collect(); draw_polyline(&mut frame, &ph_pts, COL_PH, 2.0); draw_dots(&mut frame, &ph_pts, COL_PH, 2.5); - // crosshair on hover + // crosshair if let Some(pos) = cursor.position_in(bounds) { if pos.x >= xl && pos.x <= xr && pos.y >= MARGIN_T && pos.y <= h - MARGIN_B { let lf = lerp(pos.x, xl, xr, fv.lo, fv.hi); @@ -274,25 +365,31 @@ impl<'a> canvas::Program for BodePlot<'a> { } } -/* ---- Nyquist state: zoom/pan on both axes ---- */ +/* ---- Nyquist ---- */ #[derive(Default)] pub struct NyquistState { - view: Option<(Vr, Vr)>, - drag: Option<(Point, Vr, Vr)>, + xv: Option, + yv: Option, + left_drag: Option<(Point, Vr, Vr)>, + right_drag: Option<(Point, Vr, Vr)>, } pub struct NyquistPlot<'a> { pub points: &'a [EisPoint], + pub reference: Option<&'a [EisPoint]>, } impl NyquistPlot<'_> { fn auto_view(&self) -> Option<(Vr, Vr)> { - if self.points.is_empty() { return None; } - let (re_min, re_max) = self.points.iter().fold((f32::INFINITY, f32::NEG_INFINITY), |(lo, hi), p| { + let valid: Vec<_> = self.points.iter() + .filter(|p| p.z_real.is_finite() && p.z_imag.is_finite()) + .collect(); + if valid.is_empty() { return None; } + let (re_min, re_max) = valid.iter().fold((f32::INFINITY, f32::NEG_INFINITY), |(lo, hi), p| { (lo.min(p.z_real), hi.max(p.z_real)) }); - let (ni_min, ni_max) = self.points.iter().fold((f32::INFINITY, f32::NEG_INFINITY), |(lo, hi), p| { + let (ni_min, ni_max) = valid.iter().fold((f32::INFINITY, f32::NEG_INFINITY), |(lo, hi), p| { (lo.min(-p.z_imag), hi.max(-p.z_imag)) }); let re_span = (re_max - re_min).max(1.0); @@ -303,6 +400,13 @@ impl NyquistPlot<'_> { Some((Vr::new(re_c - span / 2.0, re_c + span / 2.0), Vr::new(ni_c - span / 2.0, ni_c + span / 2.0))) } + + fn effective_ranges(&self, state: &NyquistState) -> (Vr, Vr) { + let auto = self.auto_view().unwrap_or((Vr::new(0.0, 1.0), Vr::new(0.0, 1.0))); + let xv = state.xv.unwrap_or(auto.0); + let yv = state.yv.unwrap_or(auto.1); + (xv, yv) + } } impl<'a> canvas::Program for NyquistPlot<'a> { @@ -312,6 +416,25 @@ impl<'a> canvas::Program for NyquistPlot<'a> { &self, state: &mut NyquistState, event: Event, bounds: Rectangle, cursor: mouse::Cursor, ) -> (canvas::event::Status, Option) { + if let Event::Mouse(mouse::Event::ButtonReleased(_)) = &event { + let was_right = state.right_drag.is_some(); + if was_right { + if let Some(pos) = cursor.position_in(bounds) { + let (start, _, _) = state.right_drag.unwrap(); + let dist = ((pos.x - start.x).powi(2) + (pos.y - start.y).powi(2)).sqrt(); + if dist < 3.0 { + state.xv = None; + state.yv = None; + } + } + } + state.left_drag = None; + state.right_drag = None; + if was_right || state.left_drag.is_some() { + return (canvas::event::Status::Captured, None); + } + } + let Some(pos) = cursor.position_in(bounds) else { return (canvas::event::Status::Ignored, None); }; @@ -329,42 +452,48 @@ impl<'a> canvas::Program for NyquistPlot<'a> { let factor = ZOOM_FACTOR.powf(dy); let fx = screen_frac(pos.x, xl, xr); let fy = screen_frac(pos.y, yt, yb); - let (mut xv, mut yv) = state.view.unwrap_or_else(|| self.auto_view().unwrap_or( - (Vr::new(0.0, 1.0), Vr::new(0.0, 1.0)) - )); + let (mut xv, mut yv) = self.effective_ranges(state); xv.zoom_at(factor, fx); yv.zoom_at(factor, 1.0 - fy); - state.view = Some((xv, yv)); + state.xv = Some(xv); + state.yv = Some(yv); (canvas::event::Status::Captured, None) } Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { - let (xv, yv) = state.view.unwrap_or_else(|| self.auto_view().unwrap_or( - (Vr::new(0.0, 1.0), Vr::new(0.0, 1.0)) - )); - state.drag = Some((pos, xv, yv)); - (canvas::event::Status::Captured, None) - } - Event::Mouse(mouse::Event::CursorMoved { .. }) => { - if let Some((start, sx, sy)) = state.drag { - let dx = (pos.x - start.x) / (xr - xl); - let dy = (pos.y - start.y) / (yb - yt); - let mut xv = sx; - let mut yv = sy; - xv.pan_frac(dx); - yv.pan_frac(-dy); - state.view = Some((xv, yv)); - return (canvas::event::Status::Captured, None); - } - (canvas::event::Status::Ignored, None) - } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { - state.drag = None; + let (xv, yv) = self.effective_ranges(state); + state.left_drag = Some((pos, xv, yv)); (canvas::event::Status::Captured, None) } Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) => { - state.view = None; + let (xv, yv) = self.effective_ranges(state); + state.right_drag = Some((pos, xv, yv)); (canvas::event::Status::Captured, None) } + Event::Mouse(mouse::Event::CursorMoved { .. }) => { + if let Some((start, sx, sy)) = state.left_drag { + let dx = (pos.x - start.x) / (xr - xl); + let dy = (pos.y - start.y) / (yb - yt); + let mut xv = sx; xv.pan_frac(dx); + let mut yv = sy; yv.pan_frac(-dy); + state.xv = Some(xv); + state.yv = Some(yv); + return (canvas::event::Status::Captured, None); + } + if let Some((start, sx, sy)) = state.right_drag { + let dx = pos.x - start.x; + let dy = pos.y - start.y; + let xf = 2.0_f32.powf(dx / DRAG_ZOOM_RATE); + let yf = 2.0_f32.powf(-dy / DRAG_ZOOM_RATE); + let frac_x = screen_frac(start.x, xl, xr); + let frac_y = 1.0 - screen_frac(start.y, yt, yb); + let mut xv = sx; xv.zoom_at(xf, frac_x); + let mut yv = sy; yv.zoom_at(yf, frac_y); + state.xv = Some(xv); + state.yv = Some(yv); + return (canvas::event::Status::Captured, None); + } + (canvas::event::Status::Ignored, None) + } _ => (canvas::event::Status::Ignored, None), } } @@ -386,7 +515,7 @@ impl<'a> canvas::Program for NyquistPlot<'a> { let yt = MARGIN_T; let yb = h - MARGIN_B; - let (xv, yv) = state.view.unwrap_or_else(|| self.auto_view().unwrap()); + let (xv, yv) = self.effective_ranges(state); // grid let x_step = nice_step(xv.span(), 4); @@ -410,7 +539,6 @@ impl<'a> canvas::Program for NyquistPlot<'a> { } } - // zero line let zy = lerp(0.0, yv.hi, yv.lo, yt, yb); if zy > yt && zy < yb { dl(&mut frame, Point::new(xl, zy), Point::new(xr, zy), COL_AXIS, 1.0); @@ -419,6 +547,15 @@ impl<'a> canvas::Program for NyquistPlot<'a> { dt(&mut frame, Point::new(2.0, yt - 2.0), "-Z''", COL_NYQ, 10.0); dt(&mut frame, Point::new((xl + xr) / 2.0 - 12.0, yb + 3.0), "Z'", COL_NYQ, 10.0); + if let Some(rpts) = self.reference { + let rp: Vec = rpts.iter().map(|p| Point::new( + lerp(p.z_real, xv.lo, xv.hi, xl, xr), + lerp(-p.z_imag, yv.hi, yv.lo, yt, yb), + )).collect(); + draw_polyline(&mut frame, &rp, COL_REF, 1.5); + draw_dots(&mut frame, &rp, COL_REF, 2.0); + } + let pts: Vec = self.points.iter().map(|p| Point::new( lerp(p.z_real, xv.lo, xv.hi, xl, xr), lerp(-p.z_imag, yv.hi, yv.lo, yt, yb), @@ -426,7 +563,6 @@ impl<'a> canvas::Program for NyquistPlot<'a> { draw_polyline(&mut frame, &pts, COL_NYQ, 2.0); draw_dots(&mut frame, &pts, COL_NYQ, 3.0); - // crosshair with values on hover if let Some(pos) = cursor.position_in(bounds) { if pos.x >= xl && pos.x <= xr && pos.y >= yt && pos.y <= yb { let re = lerp(pos.x, xl, xr, xv.lo, xv.hi); @@ -443,3 +579,601 @@ impl<'a> canvas::Program for NyquistPlot<'a> { vec![frame.into_geometry()] } } + +/* ---- Voltammogram (LSV) ---- */ + +#[derive(Default)] +pub struct VoltammogramState { + xv: Option, + yv: Option, + left_drag: Option<(Point, Vr, Vr)>, + right_drag: Option<(Point, Vr, Vr)>, +} + +pub struct VoltammogramPlot<'a> { + pub points: &'a [LsvPoint], + pub reference: Option<&'a [LsvPoint]>, +} + +impl VoltammogramPlot<'_> { + fn auto_view(&self) -> Option<(Vr, Vr)> { + let valid: Vec<_> = self.points.iter() + .filter(|p| p.v_mv.is_finite() && p.i_ua.is_finite()) + .collect(); + if valid.is_empty() { return None; } + let (xlo, xhi) = valid.iter().fold((f32::INFINITY, f32::NEG_INFINITY), |(lo, hi), p| { + (lo.min(p.v_mv), hi.max(p.v_mv)) + }); + let (ylo, yhi) = valid.iter().fold((f32::INFINITY, f32::NEG_INFINITY), |(lo, hi), p| { + (lo.min(p.i_ua), hi.max(p.i_ua)) + }); + let xpad = (xhi - xlo).max(1.0) * 0.05; + let ypad = (yhi - ylo).max(0.001) * 0.12; + Some((Vr::new(xlo - xpad, xhi + xpad), Vr::new(ylo - ypad, yhi + ypad))) + } + + fn effective_ranges(&self, state: &VoltammogramState) -> (Vr, Vr) { + let auto = self.auto_view().unwrap_or((Vr::new(0.0, 500.0), Vr::new(-1.0, 1.0))); + (state.xv.unwrap_or(auto.0), state.yv.unwrap_or(auto.1)) + } +} + +impl<'a> canvas::Program for VoltammogramPlot<'a> { + type State = VoltammogramState; + + fn update( + &self, state: &mut VoltammogramState, event: Event, + bounds: Rectangle, cursor: mouse::Cursor, + ) -> (canvas::event::Status, Option) { + if let Event::Mouse(mouse::Event::ButtonReleased(_)) = &event { + let was_right = state.right_drag.is_some(); + if was_right { + if let Some(pos) = cursor.position_in(bounds) { + let (start, _, _) = state.right_drag.unwrap(); + let dist = ((pos.x - start.x).powi(2) + (pos.y - start.y).powi(2)).sqrt(); + if dist < 3.0 { state.xv = None; state.yv = None; } + } + } + state.left_drag = None; + state.right_drag = None; + if was_right { return (canvas::event::Status::Captured, None); } + } + + let Some(pos) = cursor.position_in(bounds) else { + return (canvas::event::Status::Ignored, None); + }; + let xl = MARGIN_L; + let xr = bounds.width - MARGIN_R; + let yt = MARGIN_T; + let yb = bounds.height - MARGIN_B; + + match event { + Event::Mouse(mouse::Event::WheelScrolled { delta }) => { + let dy = match delta { + mouse::ScrollDelta::Lines { y, .. } => y, + mouse::ScrollDelta::Pixels { y, .. } => y / 40.0, + }; + let factor = ZOOM_FACTOR.powf(dy); + let (mut xv, mut yv) = self.effective_ranges(state); + xv.zoom_at(factor, screen_frac(pos.x, xl, xr)); + yv.zoom_at(factor, 1.0 - screen_frac(pos.y, yt, yb)); + state.xv = Some(xv); state.yv = Some(yv); + (canvas::event::Status::Captured, None) + } + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + let (xv, yv) = self.effective_ranges(state); + state.left_drag = Some((pos, xv, yv)); + (canvas::event::Status::Captured, None) + } + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) => { + let (xv, yv) = self.effective_ranges(state); + state.right_drag = Some((pos, xv, yv)); + (canvas::event::Status::Captured, None) + } + Event::Mouse(mouse::Event::CursorMoved { .. }) => { + if let Some((start, sx, sy)) = state.left_drag { + let dx = (pos.x - start.x) / (xr - xl); + let dy = (pos.y - start.y) / (yb - yt); + let mut xv = sx; xv.pan_frac(dx); + let mut yv = sy; yv.pan_frac(-dy); + state.xv = Some(xv); state.yv = Some(yv); + return (canvas::event::Status::Captured, None); + } + if let Some((start, sx, sy)) = state.right_drag { + let dx = pos.x - start.x; + let dy = pos.y - start.y; + let xf = 2.0_f32.powf(dx / DRAG_ZOOM_RATE); + let yf = 2.0_f32.powf(-dy / DRAG_ZOOM_RATE); + let mut xv = sx; xv.zoom_at(xf, screen_frac(start.x, xl, xr)); + let mut yv = sy; yv.zoom_at(yf, 1.0 - screen_frac(start.y, yt, yb)); + state.xv = Some(xv); state.yv = Some(yv); + return (canvas::event::Status::Captured, None); + } + (canvas::event::Status::Ignored, None) + } + _ => (canvas::event::Status::Ignored, None), + } + } + + fn draw( + &self, state: &VoltammogramState, renderer: &Renderer, _theme: &Theme, + bounds: Rectangle, cursor: mouse::Cursor, + ) -> Vec { + let mut frame = Frame::new(renderer, bounds.size()); + let (w, h) = (bounds.width, bounds.height); + + if self.points.is_empty() { + dt(&mut frame, Point::new(w / 2.0 - 25.0, h / 2.0), "No data", COL_DIM, 13.0); + return vec![frame.into_geometry()]; + } + + let xl = MARGIN_L; + let xr = w - MARGIN_R; + let yt = MARGIN_T; + let yb = h - MARGIN_B; + + let (xv, yv) = self.effective_ranges(state); + + let x_step = nice_step(xv.span(), 5); + if x_step > 0.0 { + let mut g = (xv.lo / x_step).ceil() * x_step; + while g <= xv.hi { + let x = lerp(g, xv.lo, xv.hi, xl, xr); + dl(&mut frame, Point::new(x, yt), Point::new(x, yb), COL_GRID, 0.5); + dt(&mut frame, Point::new(x - 10.0, yb + 3.0), &format!("{:.0}", g), COL_DIM, 9.0); + g += x_step; + } + } + let y_step = nice_step(yv.span(), 4); + if y_step > 0.0 { + let mut g = (yv.lo / y_step).ceil() * y_step; + while g <= yv.hi { + let y = lerp(g, yv.hi, yv.lo, yt, yb); + dl(&mut frame, Point::new(xl, y), Point::new(xr, y), COL_GRID, 0.5); + dt(&mut frame, Point::new(2.0, y - 5.0), &format!("{:.1}", g), COL_LSV, 9.0); + g += y_step; + } + } + + dt(&mut frame, Point::new(2.0, yt - 2.0), "I (uA)", COL_LSV, 10.0); + dt(&mut frame, Point::new((xl + xr) / 2.0 - 15.0, yb + 3.0), "V (mV)", COL_LSV, 10.0); + + if let Some(rpts) = self.reference { + let rp: Vec = rpts.iter().map(|p| Point::new( + lerp(p.v_mv, xv.lo, xv.hi, xl, xr), + lerp(p.i_ua, yv.hi, yv.lo, yt, yb), + )).collect(); + draw_polyline(&mut frame, &rp, COL_REF, 1.5); + } + + let pts: Vec = self.points.iter().map(|p| Point::new( + lerp(p.v_mv, xv.lo, xv.hi, xl, xr), + lerp(p.i_ua, yv.hi, yv.lo, yt, yb), + )).collect(); + draw_polyline(&mut frame, &pts, COL_LSV, 2.0); + draw_dots(&mut frame, &pts, COL_LSV, 2.5); + + if let Some(pos) = cursor.position_in(bounds) { + if pos.x >= xl && pos.x <= xr && pos.y >= yt && pos.y <= yb { + let v = lerp(pos.x, xl, xr, xv.lo, xv.hi); + let i = lerp(pos.y, yt, yb, yv.hi, yv.lo); + dl(&mut frame, Point::new(pos.x, yt), Point::new(pos.x, yb), + Color { a: 0.3, ..COL_AXIS }, 1.0); + dl(&mut frame, Point::new(xl, pos.y), Point::new(xr, pos.y), + Color { a: 0.3, ..COL_AXIS }, 1.0); + dt(&mut frame, Point::new(pos.x + 4.0, pos.y - 14.0), + &format!("{:.0}mV, {:.2}uA", v, i), COL_AXIS, 10.0); + } + } + + vec![frame.into_geometry()] + } +} + +/* ---- Amperogram ---- */ + +#[derive(Default)] +pub struct AmperogramState { + xv: Option, + yv: Option, + left_drag: Option<(Point, Vr, Vr)>, + right_drag: Option<(Point, Vr, Vr)>, +} + +pub struct AmperogramPlot<'a> { + pub points: &'a [AmpPoint], + pub reference: Option<&'a [AmpPoint]>, +} + +impl AmperogramPlot<'_> { + fn auto_view(&self) -> Option<(Vr, Vr)> { + let valid: Vec<_> = self.points.iter() + .filter(|p| p.t_ms.is_finite() && p.i_ua.is_finite()) + .collect(); + if valid.is_empty() { return None; } + let (xlo, xhi) = valid.iter().fold((f32::INFINITY, f32::NEG_INFINITY), |(lo, hi), p| { + (lo.min(p.t_ms), hi.max(p.t_ms)) + }); + let (ylo, yhi) = valid.iter().fold((f32::INFINITY, f32::NEG_INFINITY), |(lo, hi), p| { + (lo.min(p.i_ua), hi.max(p.i_ua)) + }); + let xpad = (xhi - xlo).max(100.0) * 0.05; + let ypad = (yhi - ylo).max(0.001) * 0.12; + Some((Vr::new(xlo - xpad, xhi + xpad), Vr::new(ylo - ypad, yhi + ypad))) + } + + fn effective_ranges(&self, state: &AmperogramState) -> (Vr, Vr) { + let auto = self.auto_view().unwrap_or((Vr::new(0.0, 1000.0), Vr::new(-1.0, 1.0))); + (state.xv.unwrap_or(auto.0), state.yv.unwrap_or(auto.1)) + } +} + +impl<'a> canvas::Program for AmperogramPlot<'a> { + type State = AmperogramState; + + fn update( + &self, state: &mut AmperogramState, event: Event, + bounds: Rectangle, cursor: mouse::Cursor, + ) -> (canvas::event::Status, Option) { + if let Event::Mouse(mouse::Event::ButtonReleased(_)) = &event { + let was_right = state.right_drag.is_some(); + if was_right { + if let Some(pos) = cursor.position_in(bounds) { + let (start, _, _) = state.right_drag.unwrap(); + let dist = ((pos.x - start.x).powi(2) + (pos.y - start.y).powi(2)).sqrt(); + if dist < 3.0 { state.xv = None; state.yv = None; } + } + } + state.left_drag = None; + state.right_drag = None; + if was_right { return (canvas::event::Status::Captured, None); } + } + + let Some(pos) = cursor.position_in(bounds) else { + return (canvas::event::Status::Ignored, None); + }; + let xl = MARGIN_L; + let xr = bounds.width - MARGIN_R; + let yt = MARGIN_T; + let yb = bounds.height - MARGIN_B; + + match event { + Event::Mouse(mouse::Event::WheelScrolled { delta }) => { + let dy = match delta { + mouse::ScrollDelta::Lines { y, .. } => y, + mouse::ScrollDelta::Pixels { y, .. } => y / 40.0, + }; + let factor = ZOOM_FACTOR.powf(dy); + let (mut xv, mut yv) = self.effective_ranges(state); + xv.zoom_at(factor, screen_frac(pos.x, xl, xr)); + yv.zoom_at(factor, 1.0 - screen_frac(pos.y, yt, yb)); + state.xv = Some(xv); state.yv = Some(yv); + (canvas::event::Status::Captured, None) + } + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + let (xv, yv) = self.effective_ranges(state); + state.left_drag = Some((pos, xv, yv)); + (canvas::event::Status::Captured, None) + } + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) => { + let (xv, yv) = self.effective_ranges(state); + state.right_drag = Some((pos, xv, yv)); + (canvas::event::Status::Captured, None) + } + Event::Mouse(mouse::Event::CursorMoved { .. }) => { + if let Some((start, sx, sy)) = state.left_drag { + let dx = (pos.x - start.x) / (xr - xl); + let dy = (pos.y - start.y) / (yb - yt); + let mut xv = sx; xv.pan_frac(dx); + let mut yv = sy; yv.pan_frac(-dy); + state.xv = Some(xv); state.yv = Some(yv); + return (canvas::event::Status::Captured, None); + } + if let Some((start, sx, sy)) = state.right_drag { + let dx = pos.x - start.x; + let dy = pos.y - start.y; + let xf = 2.0_f32.powf(dx / DRAG_ZOOM_RATE); + let yf = 2.0_f32.powf(-dy / DRAG_ZOOM_RATE); + let mut xv = sx; xv.zoom_at(xf, screen_frac(start.x, xl, xr)); + let mut yv = sy; yv.zoom_at(yf, 1.0 - screen_frac(start.y, yt, yb)); + state.xv = Some(xv); state.yv = Some(yv); + return (canvas::event::Status::Captured, None); + } + (canvas::event::Status::Ignored, None) + } + _ => (canvas::event::Status::Ignored, None), + } + } + + fn draw( + &self, state: &AmperogramState, renderer: &Renderer, _theme: &Theme, + bounds: Rectangle, cursor: mouse::Cursor, + ) -> Vec { + let mut frame = Frame::new(renderer, bounds.size()); + let (w, h) = (bounds.width, bounds.height); + + if self.points.is_empty() { + dt(&mut frame, Point::new(w / 2.0 - 25.0, h / 2.0), "No data", COL_DIM, 13.0); + return vec![frame.into_geometry()]; + } + + let xl = MARGIN_L; + let xr = w - MARGIN_R; + let yt = MARGIN_T; + let yb = h - MARGIN_B; + + let (xv, yv) = self.effective_ranges(state); + + let x_step = nice_step(xv.span(), 5); + if x_step > 0.0 { + let mut g = (xv.lo / x_step).ceil() * x_step; + while g <= xv.hi { + let x = lerp(g, xv.lo, xv.hi, xl, xr); + dl(&mut frame, Point::new(x, yt), Point::new(x, yb), COL_GRID, 0.5); + dt(&mut frame, Point::new(x - 10.0, yb + 3.0), &format!("{:.0}", g), COL_DIM, 9.0); + g += x_step; + } + } + let y_step = nice_step(yv.span(), 4); + if y_step > 0.0 { + let mut g = (yv.lo / y_step).ceil() * y_step; + while g <= yv.hi { + let y = lerp(g, yv.hi, yv.lo, yt, yb); + dl(&mut frame, Point::new(xl, y), Point::new(xr, y), COL_GRID, 0.5); + dt(&mut frame, Point::new(2.0, y - 5.0), &format!("{:.1}", g), COL_AMP, 9.0); + g += y_step; + } + } + + dt(&mut frame, Point::new(2.0, yt - 2.0), "I (uA)", COL_AMP, 10.0); + dt(&mut frame, Point::new((xl + xr) / 2.0 - 15.0, yb + 3.0), "t (ms)", COL_AMP, 10.0); + + if let Some(rpts) = self.reference { + let rp: Vec = rpts.iter().map(|p| Point::new( + lerp(p.t_ms, xv.lo, xv.hi, xl, xr), + lerp(p.i_ua, yv.hi, yv.lo, yt, yb), + )).collect(); + draw_polyline(&mut frame, &rp, COL_REF, 1.5); + } + + let pts: Vec = self.points.iter().map(|p| Point::new( + lerp(p.t_ms, xv.lo, xv.hi, xl, xr), + lerp(p.i_ua, yv.hi, yv.lo, yt, yb), + )).collect(); + draw_polyline(&mut frame, &pts, COL_AMP, 2.0); + draw_dots(&mut frame, &pts, COL_AMP, 2.5); + + if let Some(pos) = cursor.position_in(bounds) { + if pos.x >= xl && pos.x <= xr && pos.y >= yt && pos.y <= yb { + let t = lerp(pos.x, xl, xr, xv.lo, xv.hi); + let i = lerp(pos.y, yt, yb, yv.hi, yv.lo); + dl(&mut frame, Point::new(pos.x, yt), Point::new(pos.x, yb), + Color { a: 0.3, ..COL_AXIS }, 1.0); + dl(&mut frame, Point::new(xl, pos.y), Point::new(xr, pos.y), + Color { a: 0.3, ..COL_AXIS }, 1.0); + dt(&mut frame, Point::new(pos.x + 4.0, pos.y - 14.0), + &format!("{:.0}ms, {:.2}uA", t, i), COL_AXIS, 10.0); + } + } + + vec![frame.into_geometry()] + } +} + +/* ---- Chlorine (multi-step chronoamperometry) ---- */ + +#[derive(Default)] +pub struct ChlorineState { + xv: Option, + yv: Option, + left_drag: Option<(Point, Vr, Vr)>, + right_drag: Option<(Point, Vr, Vr)>, +} + +pub struct ChlorinePlot<'a> { + pub points: &'a [ClPoint], + pub reference: Option<&'a [ClPoint]>, +} + +impl ChlorinePlot<'_> { + fn auto_view(&self) -> Option<(Vr, Vr)> { + let valid: Vec<_> = self.points.iter() + .filter(|p| p.t_ms.is_finite() && p.i_ua.is_finite()) + .collect(); + if valid.is_empty() { return None; } + let (xlo, xhi) = valid.iter().fold((f32::INFINITY, f32::NEG_INFINITY), |(lo, hi), p| { + (lo.min(p.t_ms), hi.max(p.t_ms)) + }); + let (ylo, yhi) = valid.iter().fold((f32::INFINITY, f32::NEG_INFINITY), |(lo, hi), p| { + (lo.min(p.i_ua), hi.max(p.i_ua)) + }); + let xpad = (xhi - xlo).max(100.0) * 0.05; + let ypad = (yhi - ylo).max(0.001) * 0.12; + Some((Vr::new(xlo - xpad, xhi + xpad), Vr::new(ylo - ypad, yhi + ypad))) + } + + fn effective_ranges(&self, state: &ChlorineState) -> (Vr, Vr) { + let auto = self.auto_view().unwrap_or((Vr::new(0.0, 20000.0), Vr::new(-1.0, 1.0))); + (state.xv.unwrap_or(auto.0), state.yv.unwrap_or(auto.1)) + } +} + +impl<'a> canvas::Program for ChlorinePlot<'a> { + type State = ChlorineState; + + fn update( + &self, state: &mut ChlorineState, event: Event, + bounds: Rectangle, cursor: mouse::Cursor, + ) -> (canvas::event::Status, Option) { + if let Event::Mouse(mouse::Event::ButtonReleased(_)) = &event { + let was_right = state.right_drag.is_some(); + if was_right { + if let Some(pos) = cursor.position_in(bounds) { + let (start, _, _) = state.right_drag.unwrap(); + let dist = ((pos.x - start.x).powi(2) + (pos.y - start.y).powi(2)).sqrt(); + if dist < 3.0 { state.xv = None; state.yv = None; } + } + } + state.left_drag = None; + state.right_drag = None; + if was_right { return (canvas::event::Status::Captured, None); } + } + + let Some(pos) = cursor.position_in(bounds) else { + return (canvas::event::Status::Ignored, None); + }; + let xl = MARGIN_L; + let xr = bounds.width - MARGIN_R; + let yt = MARGIN_T; + let yb = bounds.height - MARGIN_B; + + match event { + Event::Mouse(mouse::Event::WheelScrolled { delta }) => { + let dy = match delta { + mouse::ScrollDelta::Lines { y, .. } => y, + mouse::ScrollDelta::Pixels { y, .. } => y / 40.0, + }; + let factor = ZOOM_FACTOR.powf(dy); + let (mut xv, mut yv) = self.effective_ranges(state); + xv.zoom_at(factor, screen_frac(pos.x, xl, xr)); + yv.zoom_at(factor, 1.0 - screen_frac(pos.y, yt, yb)); + state.xv = Some(xv); state.yv = Some(yv); + (canvas::event::Status::Captured, None) + } + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + let (xv, yv) = self.effective_ranges(state); + state.left_drag = Some((pos, xv, yv)); + (canvas::event::Status::Captured, None) + } + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) => { + let (xv, yv) = self.effective_ranges(state); + state.right_drag = Some((pos, xv, yv)); + (canvas::event::Status::Captured, None) + } + Event::Mouse(mouse::Event::CursorMoved { .. }) => { + if let Some((start, sx, sy)) = state.left_drag { + let dx = (pos.x - start.x) / (xr - xl); + let dy = (pos.y - start.y) / (yb - yt); + let mut xv = sx; xv.pan_frac(dx); + let mut yv = sy; yv.pan_frac(-dy); + state.xv = Some(xv); state.yv = Some(yv); + return (canvas::event::Status::Captured, None); + } + if let Some((start, sx, sy)) = state.right_drag { + let dx = pos.x - start.x; + let dy = pos.y - start.y; + let xf = 2.0_f32.powf(dx / DRAG_ZOOM_RATE); + let yf = 2.0_f32.powf(-dy / DRAG_ZOOM_RATE); + let mut xv = sx; xv.zoom_at(xf, screen_frac(start.x, xl, xr)); + let mut yv = sy; yv.zoom_at(yf, 1.0 - screen_frac(start.y, yt, yb)); + state.xv = Some(xv); state.yv = Some(yv); + return (canvas::event::Status::Captured, None); + } + (canvas::event::Status::Ignored, None) + } + _ => (canvas::event::Status::Ignored, None), + } + } + + fn draw( + &self, state: &ChlorineState, renderer: &Renderer, _theme: &Theme, + bounds: Rectangle, cursor: mouse::Cursor, + ) -> Vec { + let mut frame = Frame::new(renderer, bounds.size()); + let (w, h) = (bounds.width, bounds.height); + + if self.points.is_empty() { + dt(&mut frame, Point::new(w / 2.0 - 25.0, h / 2.0), "No data", COL_DIM, 13.0); + return vec![frame.into_geometry()]; + } + + let xl = MARGIN_L; + let xr = w - MARGIN_R; + let yt = MARGIN_T; + let yb = h - MARGIN_B; + + let (xv, yv) = self.effective_ranges(state); + + let x_step = nice_step(xv.span(), 5); + if x_step > 0.0 { + let mut g = (xv.lo / x_step).ceil() * x_step; + while g <= xv.hi { + let x = lerp(g, xv.lo, xv.hi, xl, xr); + dl(&mut frame, Point::new(x, yt), Point::new(x, yb), COL_GRID, 0.5); + dt(&mut frame, Point::new(x - 10.0, yb + 3.0), &format!("{:.0}", g), COL_DIM, 9.0); + g += x_step; + } + } + let y_step = nice_step(yv.span(), 4); + if y_step > 0.0 { + let mut g = (yv.lo / y_step).ceil() * y_step; + while g <= yv.hi { + let y = lerp(g, yv.hi, yv.lo, yt, yb); + dl(&mut frame, Point::new(xl, y), Point::new(xr, y), COL_GRID, 0.5); + dt(&mut frame, Point::new(2.0, y - 5.0), &format!("{:.1}", g), COL_AXIS, 9.0); + g += y_step; + } + } + + dt(&mut frame, Point::new(2.0, yt - 2.0), "I (uA)", COL_AXIS, 10.0); + dt(&mut frame, Point::new((xl + xr) / 2.0 - 15.0, yb + 3.0), "t (ms)", COL_AXIS, 10.0); + + if let Some(rpts) = self.reference { + let rf: Vec = rpts.iter() + .filter(|p| p.phase == 1) + .map(|p| Point::new(lerp(p.t_ms, xv.lo, xv.hi, xl, xr), lerp(p.i_ua, yv.hi, yv.lo, yt, yb))) + .collect(); + let rt: Vec = rpts.iter() + .filter(|p| p.phase == 2) + .map(|p| Point::new(lerp(p.t_ms, xv.lo, xv.hi, xl, xr), lerp(p.i_ua, yv.hi, yv.lo, yt, yb))) + .collect(); + draw_polyline(&mut frame, &rf, COL_REF, 1.5); + draw_polyline(&mut frame, &rt, COL_REF, 1.5); + } + + let free_pts: Vec = self.points.iter() + .filter(|p| p.phase == 1) + .map(|p| Point::new(lerp(p.t_ms, xv.lo, xv.hi, xl, xr), lerp(p.i_ua, yv.hi, yv.lo, yt, yb))) + .collect(); + let total_pts: Vec = self.points.iter() + .filter(|p| p.phase == 2) + .map(|p| Point::new(lerp(p.t_ms, xv.lo, xv.hi, xl, xr), lerp(p.i_ua, yv.hi, yv.lo, yt, yb))) + .collect(); + + draw_polyline(&mut frame, &free_pts, COL_CL_FREE, 2.0); + draw_dots(&mut frame, &free_pts, COL_CL_FREE, 3.0); + draw_polyline(&mut frame, &total_pts, COL_CL_TOTAL, 2.0); + draw_dots(&mut frame, &total_pts, COL_CL_TOTAL, 3.0); + + /* phase boundary line */ + if let (Some(last_free), Some(first_total)) = ( + self.points.iter().filter(|p| p.phase == 1).last(), + self.points.iter().find(|p| p.phase == 2), + ) { + let boundary_t = (last_free.t_ms + first_total.t_ms) / 2.0; + let bx = lerp(boundary_t, xv.lo, xv.hi, xl, xr); + if bx > xl && bx < xr { + dl(&mut frame, Point::new(bx, yt), Point::new(bx, yb), COL_DIM, 1.0); + } + } + + /* legend */ + dt(&mut frame, Point::new(xl + 5.0, yt + 2.0), "Free", COL_CL_FREE, 10.0); + dt(&mut frame, Point::new(xl + 45.0, yt + 2.0), "Total", COL_CL_TOTAL, 10.0); + + if let Some(pos) = cursor.position_in(bounds) { + if pos.x >= xl && pos.x <= xr && pos.y >= yt && pos.y <= yb { + let t = lerp(pos.x, xl, xr, xv.lo, xv.hi); + let i = lerp(pos.y, yt, yb, yv.hi, yv.lo); + dl(&mut frame, Point::new(pos.x, yt), Point::new(pos.x, yb), + Color { a: 0.3, ..COL_AXIS }, 1.0); + dl(&mut frame, Point::new(xl, pos.y), Point::new(xr, pos.y), + Color { a: 0.3, ..COL_AXIS }, 1.0); + dt(&mut frame, Point::new(pos.x + 4.0, pos.y - 14.0), + &format!("{:.0}ms, {:.2}uA", t, i), COL_AXIS, 10.0); + } + } + + vec![frame.into_geometry()] + } +} diff --git a/cue/src/protocol.rs b/cue/src/protocol.rs index 64c1c22..b4ffd9a 100644 --- a/cue/src/protocol.rs +++ b/cue/src/protocol.rs @@ -7,6 +7,18 @@ pub const RSP_SWEEP_START: u8 = 0x01; pub const RSP_DATA_POINT: u8 = 0x02; pub const RSP_SWEEP_END: u8 = 0x03; pub const RSP_CONFIG: u8 = 0x04; +pub const RSP_LSV_START: u8 = 0x05; +pub const RSP_LSV_POINT: u8 = 0x06; +pub const RSP_LSV_END: u8 = 0x07; +pub const RSP_AMP_START: u8 = 0x08; +pub const RSP_AMP_POINT: u8 = 0x09; +pub const RSP_AMP_END: u8 = 0x0A; +pub const RSP_CL_START: u8 = 0x0B; +pub const RSP_CL_POINT: u8 = 0x0C; +pub const RSP_CL_RESULT: u8 = 0x0D; +pub const RSP_CL_END: u8 = 0x0E; +pub const RSP_PH_RESULT: u8 = 0x0F; +pub const RSP_TEMP: u8 = 0x10; /* Cue → ESP32 */ pub const CMD_SET_SWEEP: u8 = 0x10; @@ -14,6 +26,14 @@ pub const CMD_SET_RTIA: u8 = 0x11; pub const CMD_SET_RCAL: u8 = 0x12; pub const CMD_START_SWEEP: u8 = 0x13; pub const CMD_GET_CONFIG: u8 = 0x14; +pub const CMD_SET_ELECTRODE: u8 = 0x15; +pub const CMD_START_LSV: u8 = 0x20; +pub const CMD_START_AMP: u8 = 0x21; +pub const CMD_STOP_AMP: u8 = 0x22; +pub const CMD_SET_TEMP: u8 = 0x16; +pub const CMD_GET_TEMP: u8 = 0x17; +pub const CMD_START_CL: u8 = 0x23; +pub const CMD_START_PH: u8 = 0x24; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Rtia { @@ -71,6 +91,65 @@ impl std::fmt::Display for Rcal { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Electrode { + FourWire, ThreeWire, +} + +impl Electrode { + pub fn from_byte(b: u8) -> Option { + Some(match b { 0 => Self::FourWire, 1 => Self::ThreeWire, _ => return None }) + } + pub fn as_byte(self) -> u8 { self as u8 } + pub fn label(self) -> &'static str { + match self { + Self::FourWire => "4-wire (AIN)", + Self::ThreeWire => "3-wire (CE0/RE0/SE0)", + } + } + pub const ALL: &[Self] = &[Self::FourWire, Self::ThreeWire]; +} + +impl std::fmt::Display for Electrode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.label()) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LpRtia { + R200, R1K, R2K, R4K, R10K, R20K, R40K, R100K, R512K, +} + +impl LpRtia { + pub fn from_byte(b: u8) -> Option { + Some(match b { + 0 => Self::R200, 1 => Self::R1K, 2 => Self::R2K, 3 => Self::R4K, + 4 => Self::R10K, 5 => Self::R20K, 6 => Self::R40K, 7 => Self::R100K, + 8 => Self::R512K, + _ => return None, + }) + } + pub fn as_byte(self) -> u8 { self as u8 } + pub fn label(self) -> &'static str { + match self { + Self::R200 => "200Ω", Self::R1K => "1kΩ", Self::R2K => "2kΩ", + Self::R4K => "4kΩ", Self::R10K => "10kΩ", Self::R20K => "20kΩ", + Self::R40K => "40kΩ", Self::R100K => "100kΩ", Self::R512K => "512kΩ", + } + } + pub const ALL: &[Self] = &[ + Self::R200, Self::R1K, Self::R2K, Self::R4K, + Self::R10K, Self::R20K, Self::R40K, Self::R100K, Self::R512K, + ]; +} + +impl std::fmt::Display for LpRtia { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.label()) + } +} + #[derive(Debug, Clone)] pub struct EisPoint { pub freq_hz: f32, @@ -78,6 +157,43 @@ pub struct EisPoint { pub phase_deg: f32, pub z_real: f32, pub z_imag: f32, + pub rtia_mag_before: f32, + pub rtia_mag_after: f32, + pub rev_mag: f32, + pub rev_phase: f32, + pub pct_err: f32, +} + +#[derive(Debug, Clone)] +pub struct LsvPoint { + pub v_mv: f32, + pub i_ua: f32, +} + +#[derive(Debug, Clone)] +pub struct AmpPoint { + pub t_ms: f32, + pub i_ua: f32, +} + +#[derive(Debug, Clone)] +pub struct ClPoint { + pub t_ms: f32, + pub i_ua: f32, + pub phase: u8, +} + +#[derive(Debug, Clone)] +pub struct ClResult { + pub i_free_ua: f32, + pub i_total_ua: f32, +} + +#[derive(Debug, Clone)] +pub struct PhResult { + pub v_ocp_mv: f32, + pub ph: f32, + pub temp_c: f32, } #[derive(Debug, Clone)] @@ -87,6 +203,7 @@ pub struct EisConfig { pub ppd: u16, pub rtia: Rtia, pub rcal: Rcal, + pub electrode: Electrode, } #[derive(Debug, Clone)] @@ -95,6 +212,18 @@ pub enum EisMessage { DataPoint { index: u16, point: EisPoint }, SweepEnd, Config(EisConfig), + LsvStart { num_points: u16, v_start: f32, v_stop: f32 }, + LsvPoint { index: u16, point: LsvPoint }, + LsvEnd, + AmpStart { v_hold: f32 }, + AmpPoint { index: u16, point: AmpPoint }, + AmpEnd, + ClStart { num_points: u16 }, + ClPoint { index: u16, point: ClPoint }, + ClResult(ClResult), + ClEnd, + PhResult(PhResult), + Temperature(f32), } fn decode_u16(data: &[u8]) -> u16 { @@ -138,6 +267,7 @@ pub fn parse_sysex(data: &[u8]) -> Option { } RSP_DATA_POINT if data.len() >= 30 => { let p = &data[2..]; + let ext = p.len() >= 53; Some(EisMessage::DataPoint { index: decode_u16(&p[0..3]), point: EisPoint { @@ -146,11 +276,16 @@ pub fn parse_sysex(data: &[u8]) -> Option { phase_deg: decode_float(&p[13..18]), z_real: decode_float(&p[18..23]), z_imag: decode_float(&p[23..28]), + rtia_mag_before: if ext { decode_float(&p[28..33]) } else { 0.0 }, + rtia_mag_after: if ext { decode_float(&p[33..38]) } else { 0.0 }, + rev_mag: if ext { decode_float(&p[38..43]) } else { 0.0 }, + rev_phase: if ext { decode_float(&p[43..48]) } else { 0.0 }, + pct_err: if ext { decode_float(&p[48..53]) } else { 0.0 }, }, }) } RSP_SWEEP_END => Some(EisMessage::SweepEnd), - RSP_CONFIG if data.len() >= 17 => { + RSP_CONFIG if data.len() >= 18 => { let p = &data[2..]; Some(EisMessage::Config(EisConfig { freq_start: decode_float(&p[0..5]), @@ -158,6 +293,76 @@ pub fn parse_sysex(data: &[u8]) -> Option { ppd: decode_u16(&p[10..13]), rtia: Rtia::from_byte(p[13]).unwrap_or(Rtia::R5K), rcal: Rcal::from_byte(p[14]).unwrap_or(Rcal::R3K), + electrode: Electrode::from_byte(p[15]).unwrap_or(Electrode::FourWire), + })) + } + RSP_LSV_START if data.len() >= 15 => { + let p = &data[2..]; + Some(EisMessage::LsvStart { + num_points: decode_u16(&p[0..3]), + v_start: decode_float(&p[3..8]), + v_stop: decode_float(&p[8..13]), + }) + } + RSP_LSV_POINT if data.len() >= 15 => { + let p = &data[2..]; + Some(EisMessage::LsvPoint { + index: decode_u16(&p[0..3]), + point: LsvPoint { + v_mv: decode_float(&p[3..8]), + i_ua: decode_float(&p[8..13]), + }, + }) + } + RSP_LSV_END => Some(EisMessage::LsvEnd), + RSP_AMP_START if data.len() >= 7 => { + let p = &data[2..]; + Some(EisMessage::AmpStart { v_hold: decode_float(&p[0..5]) }) + } + RSP_AMP_POINT if data.len() >= 15 => { + let p = &data[2..]; + Some(EisMessage::AmpPoint { + index: decode_u16(&p[0..3]), + point: AmpPoint { + t_ms: decode_float(&p[3..8]), + i_ua: decode_float(&p[8..13]), + }, + }) + } + RSP_AMP_END => Some(EisMessage::AmpEnd), + RSP_CL_START if data.len() >= 5 => { + let p = &data[2..]; + Some(EisMessage::ClStart { num_points: decode_u16(&p[0..3]) }) + } + RSP_CL_POINT if data.len() >= 16 => { + let p = &data[2..]; + Some(EisMessage::ClPoint { + index: decode_u16(&p[0..3]), + point: ClPoint { + t_ms: decode_float(&p[3..8]), + i_ua: decode_float(&p[8..13]), + phase: p[13], + }, + }) + } + RSP_CL_RESULT if data.len() >= 12 => { + let p = &data[2..]; + Some(EisMessage::ClResult(ClResult { + i_free_ua: decode_float(&p[0..5]), + i_total_ua: decode_float(&p[5..10]), + })) + } + RSP_CL_END => Some(EisMessage::ClEnd), + RSP_TEMP if data.len() >= 7 => { + let p = &data[2..]; + Some(EisMessage::Temperature(decode_float(&p[0..5]))) + } + RSP_PH_RESULT if data.len() >= 17 => { + let p = &data[2..]; + Some(EisMessage::PhResult(PhResult { + v_ocp_mv: decode_float(&p[0..5]), + ph: decode_float(&p[5..10]), + temp_c: decode_float(&p[10..15]), })) } _ => None, @@ -181,6 +386,10 @@ pub fn build_sysex_set_rcal(rcal: Rcal) -> Vec { vec![0xF0, SYSEX_MFR, CMD_SET_RCAL, rcal.as_byte(), 0xF7] } +pub fn build_sysex_set_electrode(e: Electrode) -> Vec { + vec![0xF0, SYSEX_MFR, CMD_SET_ELECTRODE, e.as_byte(), 0xF7] +} + pub fn build_sysex_start_sweep() -> Vec { vec![0xF0, SYSEX_MFR, CMD_START_SWEEP, 0xF7] } @@ -188,3 +397,54 @@ pub fn build_sysex_start_sweep() -> Vec { pub fn build_sysex_get_config() -> Vec { vec![0xF0, SYSEX_MFR, CMD_GET_CONFIG, 0xF7] } + +pub fn build_sysex_start_lsv(v_start: f32, v_stop: f32, scan_rate: f32, lp_rtia: LpRtia) -> Vec { + let mut sx = vec![0xF0, SYSEX_MFR, CMD_START_LSV]; + sx.extend_from_slice(&encode_float(v_start)); + sx.extend_from_slice(&encode_float(v_stop)); + sx.extend_from_slice(&encode_float(scan_rate)); + sx.push(lp_rtia.as_byte()); + sx.push(0xF7); + sx +} + +pub fn build_sysex_start_amp(v_hold: f32, interval_ms: f32, duration_s: f32, lp_rtia: LpRtia) -> Vec { + let mut sx = vec![0xF0, SYSEX_MFR, CMD_START_AMP]; + sx.extend_from_slice(&encode_float(v_hold)); + sx.extend_from_slice(&encode_float(interval_ms)); + sx.extend_from_slice(&encode_float(duration_s)); + sx.push(lp_rtia.as_byte()); + sx.push(0xF7); + sx +} + +pub fn build_sysex_stop_amp() -> Vec { + vec![0xF0, SYSEX_MFR, CMD_STOP_AMP, 0xF7] +} + +pub fn build_sysex_start_cl( + v_cond: f32, t_cond_ms: f32, v_free: f32, v_total: f32, + t_dep_ms: f32, t_meas_ms: f32, lp_rtia: LpRtia, +) -> Vec { + let mut sx = vec![0xF0, SYSEX_MFR, CMD_START_CL]; + sx.extend_from_slice(&encode_float(v_cond)); + sx.extend_from_slice(&encode_float(t_cond_ms)); + sx.extend_from_slice(&encode_float(v_free)); + sx.extend_from_slice(&encode_float(v_total)); + sx.extend_from_slice(&encode_float(t_dep_ms)); + sx.extend_from_slice(&encode_float(t_meas_ms)); + sx.push(lp_rtia.as_byte()); + sx.push(0xF7); + sx +} + +pub fn build_sysex_get_temp() -> Vec { + vec![0xF0, SYSEX_MFR, CMD_GET_TEMP, 0xF7] +} + +pub fn build_sysex_start_ph(stabilize_s: f32) -> Vec { + let mut sx = vec![0xF0, SYSEX_MFR, CMD_START_PH]; + sx.extend_from_slice(&encode_float(stabilize_s)); + sx.push(0xF7); + sx +} diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 8d3e8ed..a3c67da 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,3 +1,3 @@ -idf_component_register(SRCS "eis4.c" "eis.c" "ble.c" +idf_component_register(SRCS "eis4.c" "eis.c" "echem.c" "ble.c" "temp.c" INCLUDE_DIRS "." REQUIRES ad5941 ad5941_port bt nvs_flash) diff --git a/main/ble.c b/main/ble.c index bb13d49..0a3f2d7 100644 --- a/main/ble.c +++ b/main/ble.c @@ -18,7 +18,7 @@ void ble_store_config_init(void); #define DEVICE_NAME "EIS4" #define CONNECTED_BIT BIT0 -#define CMD_QUEUE_LEN 4 +#define CMD_QUEUE_LEN 8 /* BLE MIDI Service 03B80E5A-EDE8-4B33-A751-6CE34EC4C700 */ static const ble_uuid128_t midi_svc_uuid = BLE_UUID128_INIT( @@ -117,12 +117,9 @@ static uint16_t decode_u16(const uint8_t *d) /* ---- command parsing from incoming SysEx ---- */ -static void parse_command(const uint8_t *data, uint16_t len) +static void parse_one_sysex(const uint8_t *midi, uint16_t mlen) { - if (len < 5) return; - const uint8_t *midi = data + 2; - uint16_t mlen = len - 2; - if (midi[0] != 0xF0 || midi[1] != 0x7D) return; + if (mlen < 3 || midi[0] != 0xF0 || midi[1] != 0x7D) return; BleCommand cmd; memset(&cmd, 0, sizeof(cmd)); @@ -136,15 +133,53 @@ static void parse_command(const uint8_t *data, uint16_t len) cmd.sweep.ppd = decode_u16(&midi[13]); break; case CMD_SET_RTIA: - if (mlen < 5) return; + if (mlen < 4) return; cmd.rtia = midi[3]; break; case CMD_SET_RCAL: - if (mlen < 5) return; + if (mlen < 4) return; cmd.rcal = midi[3]; break; + case CMD_SET_ELECTRODE: + if (mlen < 4) return; + cmd.electrode = midi[3]; + break; + case CMD_START_LSV: + if (mlen < 19) return; + cmd.lsv.v_start = decode_float(&midi[3]); + cmd.lsv.v_stop = decode_float(&midi[8]); + cmd.lsv.scan_rate = decode_float(&midi[13]); + cmd.lsv.lp_rtia = midi[18]; + break; + case CMD_START_AMP: + if (mlen < 19) return; + cmd.amp.v_hold = decode_float(&midi[3]); + cmd.amp.interval_ms = decode_float(&midi[8]); + cmd.amp.duration_s = decode_float(&midi[13]); + cmd.amp.lp_rtia = midi[18]; + break; + case CMD_START_CL: + if (mlen < 34) return; + cmd.cl.v_cond = decode_float(&midi[3]); + cmd.cl.t_cond_ms = decode_float(&midi[8]); + cmd.cl.v_free = decode_float(&midi[13]); + cmd.cl.v_total = decode_float(&midi[18]); + cmd.cl.t_dep_ms = decode_float(&midi[23]); + cmd.cl.t_meas_ms = decode_float(&midi[28]); + cmd.cl.lp_rtia = midi[33]; + break; + case CMD_SET_TEMP: + if (mlen < 8) return; + cmd.temp_c = decode_float(&midi[3]); + break; + case CMD_START_PH: + if (mlen < 8) return; + cmd.ph.stabilize_s = decode_float(&midi[3]); + break; case CMD_START_SWEEP: case CMD_GET_CONFIG: + case CMD_STOP_AMP: + case CMD_GET_TEMP: break; default: return; @@ -153,6 +188,36 @@ static void parse_command(const uint8_t *data, uint16_t len) xQueueSend(cmd_queue, &cmd, 0); } +static void parse_command(const uint8_t *data, uint16_t len) +{ + if (len < 5) return; + + uint16_t i = 1; /* skip BLE MIDI header byte */ + while (i < len) { + /* skip timestamp bytes (bit 7 set, not F0/F7) */ + if ((data[i] & 0x80) && data[i] != 0xF0 && data[i] != 0xF7) { + i++; + continue; + } + if (data[i] == 0xF0) { + uint8_t clean[64]; + uint16_t clen = 0; + clean[clen++] = 0xF0; + i++; + while (i < len && data[i] != 0xF7) { + if (data[i] & 0x80) { i++; continue; } /* strip timestamps */ + if (clen < sizeof(clean)) + clean[clen++] = data[i]; + i++; + } + if (i < len) i++; /* skip F7 */ + parse_one_sysex(clean, clen); + } else { + i++; + } + } +} + /* ---- GATT access callbacks ---- */ static int midi_access_cb(uint16_t ch, uint16_t ah, @@ -561,15 +626,20 @@ int ble_send_sweep_start(uint32_t num_points, float freq_start, float freq_stop) int ble_send_eis_point(uint16_t index, const EISPoint *pt) { - uint8_t sx[36]; + uint8_t sx[64]; uint16_t p = 0; sx[p++] = 0xF0; sx[p++] = 0x7D; sx[p++] = RSP_DATA_POINT; encode_u16(index, &sx[p]); p += 3; - encode_float(pt->freq_hz, &sx[p]); p += 5; - encode_float(pt->mag_ohms, &sx[p]); p += 5; - encode_float(pt->phase_deg, &sx[p]); p += 5; - encode_float(pt->z_real, &sx[p]); p += 5; - encode_float(pt->z_imag, &sx[p]); p += 5; + encode_float(pt->freq_hz, &sx[p]); p += 5; + encode_float(pt->mag_ohms, &sx[p]); p += 5; + encode_float(pt->phase_deg, &sx[p]); p += 5; + encode_float(pt->z_real, &sx[p]); p += 5; + encode_float(pt->z_imag, &sx[p]); p += 5; + encode_float(pt->rtia_mag_before, &sx[p]); p += 5; + encode_float(pt->rtia_mag_after, &sx[p]); p += 5; + encode_float(pt->rev_mag, &sx[p]); p += 5; + encode_float(pt->rev_phase, &sx[p]); p += 5; + encode_float(pt->pct_err, &sx[p]); p += 5; sx[p++] = 0xF7; return send_sysex(sx, p); } @@ -590,6 +660,127 @@ int ble_send_config(const EISConfig *cfg) encode_u16(cfg->points_per_decade, &sx[p]); p += 3; sx[p++] = (uint8_t)cfg->rtia; sx[p++] = (uint8_t)cfg->rcal; + sx[p++] = (uint8_t)cfg->electrode; + sx[p++] = 0xF7; + return send_sysex(sx, p); +} + +int ble_send_lsv_start(uint32_t num_points, float v_start, float v_stop) +{ + uint8_t sx[20]; + uint16_t p = 0; + sx[p++] = 0xF0; sx[p++] = 0x7D; sx[p++] = RSP_LSV_START; + encode_u16((uint16_t)num_points, &sx[p]); p += 3; + encode_float(v_start, &sx[p]); p += 5; + encode_float(v_stop, &sx[p]); p += 5; + sx[p++] = 0xF7; + return send_sysex(sx, p); +} + +int ble_send_lsv_point(uint16_t index, float v_mv, float i_ua) +{ + uint8_t sx[20]; + uint16_t p = 0; + sx[p++] = 0xF0; sx[p++] = 0x7D; sx[p++] = RSP_LSV_POINT; + encode_u16(index, &sx[p]); p += 3; + encode_float(v_mv, &sx[p]); p += 5; + encode_float(i_ua, &sx[p]); p += 5; + sx[p++] = 0xF7; + return send_sysex(sx, p); +} + +int ble_send_lsv_end(void) +{ + uint8_t sx[] = { 0xF0, 0x7D, RSP_LSV_END, 0xF7 }; + return send_sysex(sx, sizeof(sx)); +} + +int ble_send_amp_start(float v_hold) +{ + uint8_t sx[12]; + uint16_t p = 0; + sx[p++] = 0xF0; sx[p++] = 0x7D; sx[p++] = RSP_AMP_START; + encode_float(v_hold, &sx[p]); p += 5; + sx[p++] = 0xF7; + return send_sysex(sx, p); +} + +int ble_send_amp_point(uint16_t index, float t_ms, float i_ua) +{ + uint8_t sx[20]; + uint16_t p = 0; + sx[p++] = 0xF0; sx[p++] = 0x7D; sx[p++] = RSP_AMP_POINT; + encode_u16(index, &sx[p]); p += 3; + encode_float(t_ms, &sx[p]); p += 5; + encode_float(i_ua, &sx[p]); p += 5; + sx[p++] = 0xF7; + return send_sysex(sx, p); +} + +int ble_send_amp_end(void) +{ + uint8_t sx[] = { 0xF0, 0x7D, RSP_AMP_END, 0xF7 }; + return send_sysex(sx, sizeof(sx)); +} + +int ble_send_cl_start(uint32_t num_points) +{ + uint8_t sx[10]; + uint16_t p = 0; + sx[p++] = 0xF0; sx[p++] = 0x7D; sx[p++] = RSP_CL_START; + encode_u16((uint16_t)num_points, &sx[p]); p += 3; + sx[p++] = 0xF7; + return send_sysex(sx, p); +} + +int ble_send_cl_point(uint16_t index, float t_ms, float i_ua, uint8_t phase) +{ + uint8_t sx[20]; + uint16_t p = 0; + sx[p++] = 0xF0; sx[p++] = 0x7D; sx[p++] = RSP_CL_POINT; + encode_u16(index, &sx[p]); p += 3; + encode_float(t_ms, &sx[p]); p += 5; + encode_float(i_ua, &sx[p]); p += 5; + sx[p++] = phase & 0x7F; + sx[p++] = 0xF7; + return send_sysex(sx, p); +} + +int ble_send_cl_result(float i_free_ua, float i_total_ua) +{ + uint8_t sx[16]; + uint16_t p = 0; + sx[p++] = 0xF0; sx[p++] = 0x7D; sx[p++] = RSP_CL_RESULT; + encode_float(i_free_ua, &sx[p]); p += 5; + encode_float(i_total_ua, &sx[p]); p += 5; + sx[p++] = 0xF7; + return send_sysex(sx, p); +} + +int ble_send_cl_end(void) +{ + uint8_t sx[] = { 0xF0, 0x7D, RSP_CL_END, 0xF7 }; + return send_sysex(sx, sizeof(sx)); +} + +int ble_send_ph_result(float v_ocp_mv, float ph, float temp_c) +{ + uint8_t sx[20]; + uint16_t p = 0; + sx[p++] = 0xF0; sx[p++] = 0x7D; sx[p++] = RSP_PH_RESULT; + encode_float(v_ocp_mv, &sx[p]); p += 5; + encode_float(ph, &sx[p]); p += 5; + encode_float(temp_c, &sx[p]); p += 5; + sx[p++] = 0xF7; + return send_sysex(sx, p); +} + +int ble_send_temp(float temp_c) +{ + uint8_t sx[12]; + uint16_t p = 0; + sx[p++] = 0xF0; sx[p++] = 0x7D; sx[p++] = RSP_TEMP; + encode_float(temp_c, &sx[p]); p += 5; sx[p++] = 0xF7; return send_sysex(sx, p); } diff --git a/main/ble.h b/main/ble.h index 1261238..6b0a215 100644 --- a/main/ble.h +++ b/main/ble.h @@ -3,18 +3,38 @@ #include "eis.h" -/* Commands: Cue → ESP32 (0x1x) */ -#define CMD_SET_SWEEP 0x10 -#define CMD_SET_RTIA 0x11 -#define CMD_SET_RCAL 0x12 -#define CMD_START_SWEEP 0x13 -#define CMD_GET_CONFIG 0x14 +/* Commands: Cue → ESP32 (0x1x, 0x2x) */ +#define CMD_SET_SWEEP 0x10 +#define CMD_SET_RTIA 0x11 +#define CMD_SET_RCAL 0x12 +#define CMD_START_SWEEP 0x13 +#define CMD_GET_CONFIG 0x14 +#define CMD_SET_ELECTRODE 0x15 +#define CMD_START_LSV 0x20 +#define CMD_START_AMP 0x21 +#define CMD_STOP_AMP 0x22 +#define CMD_SET_TEMP 0x16 +#define CMD_GET_TEMP 0x17 +#define CMD_START_CL 0x23 +#define CMD_START_PH 0x24 /* Responses: ESP32 → Cue (0x0x) */ #define RSP_SWEEP_START 0x01 #define RSP_DATA_POINT 0x02 #define RSP_SWEEP_END 0x03 #define RSP_CONFIG 0x04 +#define RSP_LSV_START 0x05 +#define RSP_LSV_POINT 0x06 +#define RSP_LSV_END 0x07 +#define RSP_AMP_START 0x08 +#define RSP_AMP_POINT 0x09 +#define RSP_AMP_END 0x0A +#define RSP_CL_START 0x0B +#define RSP_CL_POINT 0x0C +#define RSP_CL_RESULT 0x0D +#define RSP_CL_END 0x0E +#define RSP_PH_RESULT 0x0F +#define RSP_TEMP 0x10 typedef struct { uint8_t type; @@ -22,6 +42,12 @@ typedef struct { struct { float freq_start, freq_stop; uint16_t ppd; } sweep; uint8_t rtia; uint8_t rcal; + uint8_t electrode; + struct { float v_start, v_stop, scan_rate; uint8_t lp_rtia; } lsv; + struct { float v_hold, interval_ms, duration_s; uint8_t lp_rtia; } amp; + struct { float v_cond, t_cond_ms, v_free, v_total, t_dep_ms, t_meas_ms; uint8_t lp_rtia; } cl; + float temp_c; + struct { float stabilize_s; } ph; }; } BleCommand; @@ -32,10 +58,32 @@ void ble_wait_for_connection(void); /* blocking receive from command queue */ int ble_recv_command(BleCommand *cmd, uint32_t timeout_ms); -/* outbound data */ +/* outbound: EIS */ int ble_send_sweep_start(uint32_t num_points, float freq_start, float freq_stop); int ble_send_eis_point(uint16_t index, const EISPoint *pt); int ble_send_sweep_end(void); int ble_send_config(const EISConfig *cfg); +/* outbound: LSV */ +int ble_send_lsv_start(uint32_t num_points, float v_start, float v_stop); +int ble_send_lsv_point(uint16_t index, float v_mv, float i_ua); +int ble_send_lsv_end(void); + +/* outbound: Amperometry */ +int ble_send_amp_start(float v_hold); +int ble_send_amp_point(uint16_t index, float t_ms, float i_ua); +int ble_send_amp_end(void); + +/* outbound: Chlorine */ +int ble_send_cl_start(uint32_t num_points); +int ble_send_cl_point(uint16_t index, float t_ms, float i_ua, uint8_t phase); +int ble_send_cl_result(float i_free_ua, float i_total_ua); +int ble_send_cl_end(void); + +/* outbound: pH */ +int ble_send_ph_result(float v_ocp_mv, float ph, float temp_c); + +/* outbound: temperature */ +int ble_send_temp(float temp_c); + #endif diff --git a/main/echem.c b/main/echem.c new file mode 100644 index 0000000..b9d0d5c --- /dev/null +++ b/main/echem.c @@ -0,0 +1,495 @@ +#include "echem.h" +#include "ad5940.h" +#include "ble.h" +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +/* LP RTIA register mapping */ +static const uint32_t lp_rtia_map[] = { + [LP_RTIA_200] = LPTIARTIA_200R, + [LP_RTIA_1K] = LPTIARTIA_1K, + [LP_RTIA_2K] = LPTIARTIA_2K, + [LP_RTIA_4K] = LPTIARTIA_4K, + [LP_RTIA_10K] = LPTIARTIA_10K, + [LP_RTIA_20K] = LPTIARTIA_20K, + [LP_RTIA_40K] = LPTIARTIA_40K, + [LP_RTIA_100K] = LPTIARTIA_100K, + [LP_RTIA_512K] = LPTIARTIA_512K, +}; + +/* LP RTIA ohms for current conversion */ +static const float lp_rtia_ohms[] = { + [LP_RTIA_200] = 200.0f, + [LP_RTIA_1K] = 1000.0f, + [LP_RTIA_2K] = 2000.0f, + [LP_RTIA_4K] = 4000.0f, + [LP_RTIA_10K] = 10000.0f, + [LP_RTIA_20K] = 20000.0f, + [LP_RTIA_40K] = 40000.0f, + [LP_RTIA_100K] = 100000.0f, + [LP_RTIA_512K] = 512000.0f, +}; + +/* + * LPDAC math (2.5V reference): + * 6-bit DAC → VZERO: 200mV + code * 34.375mV (code 0-63) + * 12-bit DAC → VBIAS: 200mV + code * 0.537mV (code 0-4095) + * Cell potential: V_cell = VZERO - VBIAS + * + * VZERO fixed at ~1100mV (code 26). + * VBIAS swept to set cell potential. + */ +#define VZERO_CODE 26 +#define VZERO_MV (200.0f + VZERO_CODE * 34.375f) /* ~1093.75 mV */ +#define VBIAS_OFFSET 200.0f +#define VBIAS_LSB 0.537f + +static uint16_t mv_to_vbias_code(float v_cell_mv) +{ + /* V_cell = VZERO - VBIAS → VBIAS = VZERO - V_cell */ + float vbias_mv = VZERO_MV - v_cell_mv; + float code = (vbias_mv - VBIAS_OFFSET) / VBIAS_LSB; + if (code < 0) code = 0; + if (code > 4095) code = 4095; + return (uint16_t)(code + 0.5f); +} + +static void echem_init_lp(uint32_t rtia_reg) +{ + CLKCfg_Type clk; + memset(&clk, 0, sizeof(clk)); + clk.HFOSCEn = bTRUE; + clk.HfOSC32MHzMode = bFALSE; + clk.SysClkSrc = SYSCLKSRC_HFOSC; + clk.ADCCLkSrc = ADCCLKSRC_HFOSC; + clk.SysClkDiv = SYSCLKDIV_1; + clk.ADCClkDiv = ADCCLKDIV_1; + clk.LFOSCEn = bTRUE; + clk.HFXTALEn = bFALSE; + AD5940_CLKCfg(&clk); + + AFERefCfg_Type ref; + AD5940_StructInit(&ref, sizeof(ref)); + ref.HpBandgapEn = bTRUE; + ref.Hp1V1BuffEn = bTRUE; + ref.Hp1V8BuffEn = bTRUE; + ref.LpBandgapEn = bTRUE; + ref.LpRefBufEn = bTRUE; + ref.LpRefBoostEn = bTRUE; + AD5940_REFCfgS(&ref); + + AD5940_AFEPwrBW(AFEPWR_LP, AFEBW_250KHZ); + + LPLoopCfg_Type lp; + AD5940_StructInit(&lp, sizeof(lp)); + + lp.LpDacCfg.LpdacSel = LPDAC0; + lp.LpDacCfg.LpDacSrc = LPDACSRC_MMR; + lp.LpDacCfg.LpDacVzeroMux = LPDACVZERO_6BIT; + lp.LpDacCfg.LpDacVbiasMux = LPDACVBIAS_12BIT; + lp.LpDacCfg.LpDacSW = LPDACSW_VZERO2LPTIA | LPDACSW_VBIAS2LPPA; + lp.LpDacCfg.LpDacRef = LPDACREF_2P5; + lp.LpDacCfg.DataRst = bFALSE; + lp.LpDacCfg.PowerEn = bTRUE; + lp.LpDacCfg.DacData6Bit = VZERO_CODE; + lp.LpDacCfg.DacData12Bit = mv_to_vbias_code(0); + + lp.LpAmpCfg.LpAmpSel = LPAMP0; + lp.LpAmpCfg.LpAmpPwrMod = LPAMPPWR_BOOST3; + lp.LpAmpCfg.LpPaPwrEn = bTRUE; + lp.LpAmpCfg.LpTiaPwrEn = bTRUE; + lp.LpAmpCfg.LpTiaRf = LPTIARF_SHORT; + lp.LpAmpCfg.LpTiaRload = LPTIARLOAD_SHORT; + lp.LpAmpCfg.LpTiaRtia = rtia_reg; + /* CE0=drive, RE0=ref, SE0=working: SW2,4,5,7,12 */ + lp.LpAmpCfg.LpTiaSW = LPTIASW(2) | LPTIASW(4) | LPTIASW(5) | + LPTIASW(7) | LPTIASW(12); + AD5940_LPLoopCfgS(&lp); + + /* ADC: SINC2+Notch on LPTIA output */ + ADCBaseCfg_Type adc; + adc.ADCMuxP = ADCMUXP_LPTIA0_P; + adc.ADCMuxN = ADCMUXN_LPTIA0_N; + adc.ADCPga = ADCPGA_1P5; + AD5940_ADCBaseCfgS(&adc); + + ADCFilterCfg_Type filt; + AD5940_StructInit(&filt, sizeof(filt)); + filt.ADCSinc3Osr = ADCSINC3OSR_4; + filt.ADCSinc2Osr = ADCSINC2OSR_667; + filt.ADCAvgNum = ADCAVGNUM_16; + filt.ADCRate = ADCRATE_800KHZ; + filt.BpNotch = bFALSE; + filt.BpSinc3 = bFALSE; + filt.Sinc2NotchEnable = bTRUE; + filt.Sinc3ClkEnable = bTRUE; + filt.Sinc2NotchClkEnable = bTRUE; + filt.DFTClkEnable = bFALSE; + filt.WGClkEnable = bFALSE; + AD5940_ADCFilterCfgS(&filt); + + AD5940_INTCCfg(AFEINTC_0, AFEINTSRC_SINC2RDY, bTRUE); + AD5940_INTCCfg(AFEINTC_1, AFEINTSRC_SINC2RDY, bTRUE); + AD5940_INTCClrFlag(AFEINTSRC_ALLINT); + + AGPIOCfg_Type gpio; + AD5940_StructInit(&gpio, sizeof(gpio)); + gpio.FuncSet = GP0_INT; + gpio.OutputEnSet = AGPIO_Pin0; + AD5940_AGPIOCfg(&gpio); + + AD5940_WriteReg(REG_AFE_FIFOCON, 0); +} + +static float read_current_ua(float rtia_ohms) +{ + AD5940_INTCClrFlag(AFEINTSRC_SINC2RDY); + AD5940_AFECtrlS(AFECTRL_ADCPWR, bTRUE); + AD5940_Delay10us(25); + AD5940_AFECtrlS(AFECTRL_ADCCNV, bTRUE); + + AD5940_ClrMCUIntFlag(); + while (!AD5940_GetMCUIntFlag()) + vTaskDelay(1); + + AD5940_AFECtrlS(AFECTRL_ADCCNV | AFECTRL_ADCPWR, bFALSE); + AD5940_INTCClrFlag(AFEINTSRC_SINC2RDY); + + uint32_t raw = AD5940_ReadAfeResult(AFERESULT_SINC2); + int32_t code = (raw & (1UL << 15)) ? (int32_t)(raw | 0xFFFF0000UL) : (int32_t)raw; + + /* I = V_tia / RTIA, V_tia = code * Vref / (PGA * 32768) */ + float v_tia = (float)code * 1.82f / (1.5f * 32768.0f); + float i_a = v_tia / rtia_ohms; + return i_a * 1e6f; /* convert to uA */ +} + +static void echem_init_adc(void) +{ + CLKCfg_Type clk; + memset(&clk, 0, sizeof(clk)); + clk.HFOSCEn = bTRUE; + clk.HfOSC32MHzMode = bFALSE; + clk.SysClkSrc = SYSCLKSRC_HFOSC; + clk.ADCCLkSrc = ADCCLKSRC_HFOSC; + clk.SysClkDiv = SYSCLKDIV_1; + clk.ADCClkDiv = ADCCLKDIV_1; + clk.LFOSCEn = bTRUE; + clk.HFXTALEn = bFALSE; + AD5940_CLKCfg(&clk); + + AFERefCfg_Type ref; + AD5940_StructInit(&ref, sizeof(ref)); + ref.HpBandgapEn = bTRUE; + ref.Hp1V1BuffEn = bTRUE; + ref.Hp1V8BuffEn = bTRUE; + ref.LpBandgapEn = bTRUE; + ref.LpRefBufEn = bTRUE; + ref.LpRefBoostEn = bTRUE; + AD5940_REFCfgS(&ref); + + AD5940_AFEPwrBW(AFEPWR_LP, AFEBW_250KHZ); + + ADCFilterCfg_Type filt; + AD5940_StructInit(&filt, sizeof(filt)); + filt.ADCSinc3Osr = ADCSINC3OSR_4; + filt.ADCSinc2Osr = ADCSINC2OSR_667; + filt.ADCAvgNum = ADCAVGNUM_16; + filt.ADCRate = ADCRATE_800KHZ; + filt.BpNotch = bFALSE; + filt.BpSinc3 = bFALSE; + filt.Sinc2NotchEnable = bTRUE; + filt.Sinc3ClkEnable = bTRUE; + filt.Sinc2NotchClkEnable = bTRUE; + filt.DFTClkEnable = bFALSE; + filt.WGClkEnable = bFALSE; + AD5940_ADCFilterCfgS(&filt); + + AD5940_INTCCfg(AFEINTC_0, AFEINTSRC_SINC2RDY, bTRUE); + AD5940_INTCCfg(AFEINTC_1, AFEINTSRC_SINC2RDY, bTRUE); + AD5940_INTCClrFlag(AFEINTSRC_ALLINT); + + AGPIOCfg_Type gpio; + AD5940_StructInit(&gpio, sizeof(gpio)); + gpio.FuncSet = GP0_INT; + gpio.OutputEnSet = AGPIO_Pin0; + AD5940_AGPIOCfg(&gpio); + + AD5940_WriteReg(REG_AFE_FIFOCON, 0); +} + +static float read_voltage_mv(uint32_t muxp) +{ + AD5940_ADCMuxCfgS(muxp, ADCMUXN_VSET1P1); + AD5940_Delay10us(50); + + AD5940_INTCClrFlag(AFEINTSRC_SINC2RDY); + AD5940_AFECtrlS(AFECTRL_ADCPWR, bTRUE); + AD5940_Delay10us(25); + AD5940_AFECtrlS(AFECTRL_ADCCNV, bTRUE); + + AD5940_ClrMCUIntFlag(); + while (!AD5940_GetMCUIntFlag()) + vTaskDelay(1); + + AD5940_AFECtrlS(AFECTRL_ADCCNV | AFECTRL_ADCPWR, bFALSE); + AD5940_INTCClrFlag(AFEINTSRC_SINC2RDY); + + uint32_t raw = AD5940_ReadAfeResult(AFERESULT_SINC2); + int32_t code = (raw & (1UL << 15)) ? (int32_t)(raw | 0xFFFF0000UL) : (int32_t)raw; + + /* V_diff = code * Vref / (PGA * 32768), PGA=1.5, Vref=1.82V */ + return (float)code * 1820.0f / (1.5f * 32768.0f); +} + +/* ---- public ---- */ + +void echem_default_lsv(LSVConfig *cfg) +{ + memset(cfg, 0, sizeof(*cfg)); + cfg->v_start = 0.0f; + cfg->v_stop = 500.0f; + cfg->scan_rate = 50.0f; + cfg->lp_rtia = LP_RTIA_10K; +} + +void echem_default_amp(AmpConfig *cfg) +{ + memset(cfg, 0, sizeof(*cfg)); + cfg->v_hold = 200.0f; + cfg->interval_ms = 100.0f; + cfg->duration_s = 60.0f; + cfg->lp_rtia = LP_RTIA_10K; +} + +int echem_lsv(const LSVConfig *cfg, LSVPoint *out, uint32_t max_points) +{ + if (cfg->lp_rtia >= LP_RTIA_COUNT) return 0; + float rtia = lp_rtia_ohms[cfg->lp_rtia]; + + echem_init_lp(lp_rtia_map[cfg->lp_rtia]); + + /* set starting voltage and flush SINC2 filter */ + AD5940_LPDAC0WriteS(mv_to_vbias_code(cfg->v_start), VZERO_CODE); + vTaskDelay(pdMS_TO_TICKS(50)); + for (int i = 0; i < 4; i++) + read_current_ua(rtia); + + float v_range = cfg->v_stop - cfg->v_start; + if (fabsf(v_range) < 0.001f) return 0; + + /* compute steps to always cover full range within max_points */ + uint32_t n_lsb = (uint32_t)(fabsf(v_range / VBIAS_LSB) + 0.5f); + uint32_t n_steps = n_lsb; + uint32_t step_mult = 1; + if (n_steps > max_points) { + step_mult = (n_lsb + max_points - 1) / max_points; + n_steps = (n_lsb + step_mult - 1) / step_mult; + } + if (n_steps < 2) n_steps = 2; + + float step = (v_range > 0) ? VBIAS_LSB * step_mult : -VBIAS_LSB * step_mult; + + float delay_ms = fabsf(step / cfg->scan_rate) * 1000.0f; + if (delay_ms < 1.0f) delay_ms = 1.0f; + TickType_t ticks = pdMS_TO_TICKS((uint32_t)delay_ms); + if (ticks < 1) ticks = 1; + + printf("\n%10s %10s\n", "V(mV)", "I(uA)"); + printf("------------------------\n"); + + for (uint32_t i = 0; i < n_steps; i++) { + float v_mv = cfg->v_start + i * step; + uint16_t code = mv_to_vbias_code(v_mv); + AD5940_LPDAC0WriteS(code, VZERO_CODE); + vTaskDelay(ticks); + + float i_ua = read_current_ua(rtia); + out[i].v_mv = v_mv; + out[i].i_ua = i_ua; + + printf("%10.1f %10.3f\n", v_mv, i_ua); + } + + AD5940_AFECtrlS(AFECTRL_ALL, bFALSE); + return (int)n_steps; +} + +int echem_amp(const AmpConfig *cfg, AmpPoint *out, uint32_t max_points) +{ + if (cfg->lp_rtia >= LP_RTIA_COUNT) return 0; + float rtia = lp_rtia_ohms[cfg->lp_rtia]; + + echem_init_lp(lp_rtia_map[cfg->lp_rtia]); + + uint16_t code = mv_to_vbias_code(cfg->v_hold); + AD5940_LPDAC0WriteS(code, VZERO_CODE); + vTaskDelay(pdMS_TO_TICKS(50)); + for (int i = 0; i < 4; i++) + read_current_ua(rtia); + + TickType_t interval = pdMS_TO_TICKS((uint32_t)cfg->interval_ms); + if (interval < 1) interval = 1; + + uint32_t max_samples = max_points; + if (cfg->duration_s > 0) { + uint32_t duration_n = (uint32_t)(cfg->duration_s * 1000.0f / cfg->interval_ms + 0.5f); + if (duration_n < max_samples) max_samples = duration_n; + } + + printf("\n%10s %10s\n", "t(ms)", "I(uA)"); + printf("------------------------\n"); + + TickType_t t0 = xTaskGetTickCount(); + uint32_t count = 0; + + for (uint32_t i = 0; i < max_samples; i++) { + /* check for stop command (non-blocking) */ + BleCommand cmd; + if (ble_recv_command(&cmd, 0) == 0 && cmd.type == CMD_STOP_AMP) + break; + + float i_ua = read_current_ua(rtia); + float t_ms = (float)(xTaskGetTickCount() - t0) * portTICK_PERIOD_MS; + + out[i].t_ms = t_ms; + out[i].i_ua = i_ua; + count++; + + printf("%10.1f %10.3f\n", t_ms, i_ua); + + vTaskDelay(interval); + } + + AD5940_AFECtrlS(AFECTRL_ALL, bFALSE); + return (int)count; +} + +void echem_default_cl(ClConfig *cfg) +{ + memset(cfg, 0, sizeof(*cfg)); + cfg->v_cond = 800.0f; /* +800 mV conditioning pulse */ + cfg->t_cond_ms = 2000.0f; + cfg->v_free = 100.0f; /* +100 mV for HOCl reduction */ + cfg->v_total = -200.0f; /* -200 mV for total chlorine */ + cfg->t_dep_ms = 5000.0f; /* 5s settling */ + cfg->t_meas_ms = 5000.0f; /* 5s sampling */ + cfg->lp_rtia = LP_RTIA_10K; +} + +static uint32_t sample_phase(float v_mv, float t_dep_ms, float t_meas_ms, + uint8_t phase, float rtia_ohms, + ClPoint *out, uint32_t idx, uint32_t max_points, + TickType_t t0, float *avg_out) +{ + AD5940_LPDAC0WriteS(mv_to_vbias_code(v_mv), VZERO_CODE); + + /* settling — no samples recorded */ + vTaskDelay(pdMS_TO_TICKS((uint32_t)t_dep_ms)); + + /* measurement — sample at ~50ms intervals */ + uint32_t n_samples = (uint32_t)(t_meas_ms / 50.0f + 0.5f); + if (n_samples < 2) n_samples = 2; + TickType_t interval = pdMS_TO_TICKS(50); + + float sum = 0; + uint32_t count = 0; + + for (uint32_t i = 0; i < n_samples && idx < max_points; i++) { + float i_ua = read_current_ua(rtia_ohms); + float t_ms = (float)(xTaskGetTickCount() - t0) * portTICK_PERIOD_MS; + + out[idx].t_ms = t_ms; + out[idx].i_ua = i_ua; + out[idx].phase = phase; + idx++; + + sum += i_ua; + count++; + + vTaskDelay(interval); + } + + *avg_out = (count > 0) ? sum / (float)count : 0.0f; + return idx; +} + +int echem_chlorine(const ClConfig *cfg, ClPoint *out, uint32_t max_points, ClResult *result) +{ + if (cfg->lp_rtia >= LP_RTIA_COUNT) return 0; + float rtia = lp_rtia_ohms[cfg->lp_rtia]; + + echem_init_lp(lp_rtia_map[cfg->lp_rtia]); + + TickType_t t0 = xTaskGetTickCount(); + uint32_t idx = 0; + + printf("Cl: conditioning at %.0f mV for %.0f ms\n", cfg->v_cond, cfg->t_cond_ms); + AD5940_LPDAC0WriteS(mv_to_vbias_code(cfg->v_cond), VZERO_CODE); + vTaskDelay(pdMS_TO_TICKS((uint32_t)cfg->t_cond_ms)); + + printf("Cl: free chlorine at %.0f mV\n", cfg->v_free); + idx = sample_phase(cfg->v_free, cfg->t_dep_ms, cfg->t_meas_ms, + CL_PHASE_FREE, rtia, out, idx, max_points, t0, + &result->i_free_ua); + + printf("Cl: total chlorine at %.0f mV\n", cfg->v_total); + idx = sample_phase(cfg->v_total, cfg->t_dep_ms, cfg->t_meas_ms, + CL_PHASE_TOTAL, rtia, out, idx, max_points, t0, + &result->i_total_ua); + + printf("Cl: free=%.3f uA, total=%.3f uA\n", result->i_free_ua, result->i_total_ua); + + AD5940_AFECtrlS(AFECTRL_ALL, bFALSE); + return (int)idx; +} + +void echem_default_ph(PhConfig *cfg) +{ + memset(cfg, 0, sizeof(*cfg)); + cfg->stabilize_s = 30.0f; + cfg->temp_c = 25.0f; +} + +int echem_ph_ocp(const PhConfig *cfg, PhResult *result) +{ + echem_init_adc(); + + /* ADC mux: read SE0 and RE0 pin voltages directly */ + ADCBaseCfg_Type adc; + adc.ADCMuxP = ADCMUXP_VSE0; + adc.ADCMuxN = ADCMUXN_VSET1P1; + adc.ADCPga = ADCPGA_1P5; + AD5940_ADCBaseCfgS(&adc); + + printf("pH: stabilizing %0.f s\n", cfg->stabilize_s); + vTaskDelay(pdMS_TO_TICKS((uint32_t)(cfg->stabilize_s * 1000.0f))); + + /* average N readings of V(SE0) and V(RE0) */ + #define PH_AVG_N 10 + float sum_se0 = 0, sum_re0 = 0; + for (int i = 0; i < PH_AVG_N; i++) { + sum_se0 += read_voltage_mv(ADCMUXP_VSE0); + sum_re0 += read_voltage_mv(ADCMUXP_VRE0); + vTaskDelay(pdMS_TO_TICKS(100)); + } + float v_se0 = sum_se0 / PH_AVG_N; + float v_re0 = sum_re0 / PH_AVG_N; + float ocp = v_se0 - v_re0; + + float t_k = cfg->temp_c + 273.15f; + float slope = 0.1984f * t_k; /* mV/pH at temperature */ + + result->v_ocp_mv = ocp; + result->ph = 7.0f - ocp / slope; + result->temp_c = cfg->temp_c; + + printf("pH: SE0=%.1f mV, RE0=%.1f mV, OCP=%.1f mV, pH=%.2f\n", + v_se0, v_re0, ocp, result->ph); + + AD5940_AFECtrlS(AFECTRL_ALL, bFALSE); + return 0; +} diff --git a/main/echem.h b/main/echem.h new file mode 100644 index 0000000..5cab4e9 --- /dev/null +++ b/main/echem.h @@ -0,0 +1,93 @@ +#ifndef ECHEM_H +#define ECHEM_H + +#include + +#define ECHEM_MAX_POINTS 500 + +typedef enum { + LP_RTIA_200 = 0, + LP_RTIA_1K, + LP_RTIA_2K, + LP_RTIA_4K, + LP_RTIA_10K, + LP_RTIA_20K, + LP_RTIA_40K, + LP_RTIA_100K, + LP_RTIA_512K, + LP_RTIA_COUNT +} EchemLpRtia; + +typedef struct { + float v_start; /* mV, cell potential start */ + float v_stop; /* mV, cell potential stop */ + float scan_rate; /* mV/s */ + EchemLpRtia lp_rtia; +} LSVConfig; + +typedef struct { + float v_mv; /* mV, applied cell potential */ + float i_ua; /* uA, measured current */ +} LSVPoint; + +typedef struct { + float v_hold; /* mV, applied cell potential */ + float interval_ms; /* sampling interval */ + float duration_s; /* total duration, 0 = until stopped */ + EchemLpRtia lp_rtia; +} AmpConfig; + +typedef struct { + float t_ms; /* timestamp */ + float i_ua; /* uA, measured current */ +} AmpPoint; + +/* Chlorine: multi-step chronoamperometry on Au/Ag */ +typedef struct { + float v_cond; /* mV, conditioning potential (surface cleaning) */ + float t_cond_ms; /* ms, conditioning duration */ + float v_free; /* mV, free chlorine measurement potential */ + float v_total; /* mV, total chlorine measurement potential */ + float t_dep_ms; /* ms, deposition/settling time per step */ + float t_meas_ms; /* ms, measurement sampling time per step */ + EchemLpRtia lp_rtia; +} ClConfig; + +#define CL_PHASE_COND 0 +#define CL_PHASE_FREE 1 +#define CL_PHASE_TOTAL 2 + +typedef struct { + float t_ms; + float i_ua; + uint8_t phase; +} ClPoint; + +typedef struct { + float i_free_ua; + float i_total_ua; +} ClResult; + +/* pH: open circuit potentiometry on Au/Ag */ +typedef struct { + float stabilize_s; /* seconds to wait for OCP stabilization */ + float temp_c; /* temperature for Nernst calculation */ +} PhConfig; + +typedef struct { + float v_ocp_mv; /* measured OCP: V(SE0) - V(RE0) */ + float ph; /* Nernst-derived pH */ + float temp_c; /* temperature used */ +} PhResult; + +void echem_default_lsv(LSVConfig *cfg); +void echem_default_amp(AmpConfig *cfg); +void echem_default_cl(ClConfig *cfg); +void echem_default_ph(PhConfig *cfg); + +int echem_lsv(const LSVConfig *cfg, LSVPoint *out, uint32_t max_points); +int echem_amp(const AmpConfig *cfg, AmpPoint *out, uint32_t max_points); +int echem_chlorine(const ClConfig *cfg, ClPoint *out, uint32_t max_points, ClResult *result); +int echem_ph_ocp(const PhConfig *cfg, PhResult *result); + +#endif diff --git a/main/eis.c b/main/eis.c index 551719f..2f28b9b 100644 --- a/main/eis.c +++ b/main/eis.c @@ -15,6 +15,8 @@ static struct { float sys_clk; float rcal_ohms; uint32_t rcal_sw_d, rcal_sw_p, rcal_sw_n, rcal_sw_t; + uint32_t dut_sw_d, dut_sw_p, dut_sw_n, dut_sw_t; + uint32_t dut_mux_vp, dut_mux_vn; uint32_t rtia_reg; uint32_t dertia_reg; } ctx; @@ -54,6 +56,26 @@ static void resolve_config(void) ctx.rcal_sw_t = SWT_AIN0 | SWT_TRTIA; break; } + + /* DUT electrode routing */ + switch (ctx.cfg.electrode) { + case ELEC_3WIRE: + ctx.dut_sw_d = SWD_CE0; + ctx.dut_sw_p = SWP_RE0; + ctx.dut_sw_n = SWN_SE0; + ctx.dut_sw_t = SWT_SE0LOAD | SWT_TRTIA; + ctx.dut_mux_vp = ADCMUXP_P_NODE; + ctx.dut_mux_vn = ADCMUXN_N_NODE; + break; + default: /* ELEC_4WIRE */ + ctx.dut_sw_d = SWD_AIN3; + ctx.dut_sw_p = SWP_AIN3; + ctx.dut_sw_n = SWN_AIN0; + ctx.dut_sw_t = SWT_AIN0 | SWT_TRTIA; + ctx.dut_mux_vp = ADCMUXP_AIN2; + ctx.dut_mux_vn = ADCMUXN_AIN1; + break; + } } static void apply_hsloop(void) @@ -96,9 +118,10 @@ void eis_default_config(EISConfig *cfg) cfg->freq_start_hz = 1000.0f; cfg->freq_stop_hz = 200000.0f; cfg->points_per_decade = 10; - cfg->rtia = RTIA_5K; - cfg->rcal = RCAL_3K; - cfg->pga = ADCPGA_1P5; + cfg->rtia = RTIA_5K; + cfg->rcal = RCAL_3K; + cfg->electrode = ELEC_4WIRE; + cfg->pga = ADCPGA_1P5; cfg->excit_amp = 500; } @@ -252,16 +275,30 @@ static void dft_measure(uint32_t mux_p, uint32_t mux_n, iImpCar_Type *out) out->Image = -out->Image; } +/* RTIA calibration: 2 DFTs through current RCAL switch config */ +static fImpCar_Type measure_rtia(void) +{ + iImpCar_Type v_rcal, v_raw; + dft_measure(ADCMUXP_P_NODE, ADCMUXN_N_NODE, &v_rcal); + dft_measure(ADCMUXP_HSTIA_P, ADCMUXN_HSTIA_N, &v_raw); + v_raw.Real = -v_raw.Real; + v_raw.Image = -v_raw.Image; + fImpCar_Type rtia = AD5940_ComplexDivInt(&v_raw, &v_rcal); + rtia.Real *= ctx.rcal_ohms; + rtia.Image *= ctx.rcal_ohms; + return rtia; +} + /* ---------- measurement ---------- */ int eis_measure_point(float freq_hz, EISPoint *out) { configure_freq(freq_hz); - iImpCar_Type v_rcal, v_rtia, v_tia, v_sense; - - /* Phase 1: RTIA calibration through RCAL */ SWMatrixCfg_Type sw; + iImpCar_Type v_tia, v_sense; + + /* switch to RCAL before power-up */ sw.Dswitch = ctx.rcal_sw_d; sw.Pswitch = ctx.rcal_sw_p; sw.Nswitch = ctx.rcal_sw_n; @@ -272,30 +309,62 @@ int eis_measure_point(float freq_hz, EISPoint *out) AFECTRL_EXTBUFPWR | AFECTRL_DACREFPWR | AFECTRL_HSDACPWR | AFECTRL_SINC2NOTCH, bTRUE); - dft_measure(ADCMUXP_P_NODE, ADCMUXN_N_NODE, &v_rcal); - dft_measure(ADCMUXP_HSTIA_P, ADCMUXN_HSTIA_N, &v_rtia); + /* RCAL before */ + fImpCar_Type rtia_before = measure_rtia(); - v_rtia.Real = -v_rtia.Real; - v_rtia.Image = -v_rtia.Image; - - fImpCar_Type rtia = AD5940_ComplexDivInt(&v_rtia, &v_rcal); - rtia.Real *= ctx.rcal_ohms; - rtia.Image *= ctx.rcal_ohms; - - /* Phase 2: DUT — software 4-wire */ - sw.Dswitch = SWD_AIN3; - sw.Pswitch = SWP_AIN3; - sw.Nswitch = SWN_AIN0; - sw.Tswitch = SWT_AIN0 | SWT_TRTIA; + /* DUT forward */ + sw.Dswitch = ctx.dut_sw_d; + sw.Pswitch = ctx.dut_sw_p; + sw.Nswitch = ctx.dut_sw_n; + sw.Tswitch = ctx.dut_sw_t; AD5940_SWMatrixCfgS(&sw); AD5940_Delay10us(50); dft_measure(ADCMUXP_HSTIA_P, ADCMUXN_HSTIA_N, &v_tia); v_tia.Real = -v_tia.Real; v_tia.Image = -v_tia.Image; + dft_measure(ctx.dut_mux_vp, ctx.dut_mux_vn, &v_sense); - dft_measure(ADCMUXP_AIN2, ADCMUXN_AIN1, &v_sense); + iImpCar_Type v_tia_fwd = v_tia; + iImpCar_Type v_sense_fwd = v_sense; + /* RCAL after */ + sw.Dswitch = ctx.rcal_sw_d; + sw.Pswitch = ctx.rcal_sw_p; + sw.Nswitch = ctx.rcal_sw_n; + sw.Tswitch = ctx.rcal_sw_t; + AD5940_SWMatrixCfgS(&sw); + AD5940_Delay10us(50); + + fImpCar_Type rtia_after = measure_rtia(); + + /* DUT reverse (DUT first, then RCAL) */ + sw.Dswitch = ctx.dut_sw_d; + sw.Pswitch = ctx.dut_sw_p; + sw.Nswitch = ctx.dut_sw_n; + sw.Tswitch = ctx.dut_sw_t; + AD5940_SWMatrixCfgS(&sw); + AD5940_Delay10us(50); + + dft_measure(ADCMUXP_HSTIA_P, ADCMUXN_HSTIA_N, &v_tia); + v_tia.Real = -v_tia.Real; + v_tia.Image = -v_tia.Image; + dft_measure(ctx.dut_mux_vp, ctx.dut_mux_vn, &v_sense); + + iImpCar_Type v_tia_rev = v_tia; + iImpCar_Type v_sense_rev = v_sense; + + /* RCAL reverse */ + sw.Dswitch = ctx.rcal_sw_d; + sw.Pswitch = ctx.rcal_sw_p; + sw.Nswitch = ctx.rcal_sw_n; + sw.Tswitch = ctx.rcal_sw_t; + AD5940_SWMatrixCfgS(&sw); + AD5940_Delay10us(50); + + fImpCar_Type rtia_rev = measure_rtia(); + + /* power down, open switches */ AD5940_AFECtrlS(AFECTRL_WG | AFECTRL_ADCPWR | AFECTRL_ADCCNV | AFECTRL_DFT | AFECTRL_SINC2NOTCH | AFECTRL_HSDACPWR | AFECTRL_HSTIAPWR | AFECTRL_INAMPPWR | @@ -307,18 +376,36 @@ int eis_measure_point(float freq_hz, EISPoint *out) sw.Tswitch = SWT_OPEN; AD5940_SWMatrixCfgS(&sw); - /* Z_DUT = V_sense × Rtia_cal / V_TIA */ - fImpCar_Type fv_sense = { (float)v_sense.Real, (float)v_sense.Image }; - fImpCar_Type fv_tia = { (float)v_tia.Real, (float)v_tia.Image }; + /* forward Z using averaged RTIA bracket */ + fImpCar_Type rtia_avg = { + .Real = (rtia_before.Real + rtia_after.Real) * 0.5f, + .Image = (rtia_before.Image + rtia_after.Image) * 0.5f, + }; + fImpCar_Type fs_fwd = { (float)v_sense_fwd.Real, (float)v_sense_fwd.Image }; + fImpCar_Type ft_fwd = { (float)v_tia_fwd.Real, (float)v_tia_fwd.Image }; + fImpCar_Type num = AD5940_ComplexMulFloat(&fs_fwd, &rtia_avg); + fImpCar_Type z_fwd = AD5940_ComplexDivFloat(&num, &ft_fwd); - fImpCar_Type num = AD5940_ComplexMulFloat(&fv_sense, &rtia); - fImpCar_Type z = AD5940_ComplexDivFloat(&num, &fv_tia); + /* reverse Z using RTIA from RCAL measured after DUT */ + fImpCar_Type fs_rev = { (float)v_sense_rev.Real, (float)v_sense_rev.Image }; + fImpCar_Type ft_rev = { (float)v_tia_rev.Real, (float)v_tia_rev.Image }; + num = AD5940_ComplexMulFloat(&fs_rev, &rtia_rev); + fImpCar_Type z_rev = AD5940_ComplexDivFloat(&num, &ft_rev); - out->freq_hz = freq_hz; - out->z_real = z.Real; - out->z_imag = z.Image; - out->mag_ohms = AD5940_ComplexMag(&z); - out->phase_deg = AD5940_ComplexPhase(&z) * (float)(180.0 / M_PI); + float mag_fwd = AD5940_ComplexMag(&z_fwd); + float mag_rev = AD5940_ComplexMag(&z_rev); + + out->freq_hz = freq_hz; + out->z_real = z_fwd.Real; + out->z_imag = z_fwd.Image; + out->mag_ohms = mag_fwd; + out->phase_deg = AD5940_ComplexPhase(&z_fwd) * (float)(180.0 / M_PI); + out->rtia_mag_before = AD5940_ComplexMag(&rtia_before); + out->rtia_mag_after = AD5940_ComplexMag(&rtia_after); + out->rev_mag = mag_rev; + out->rev_phase = AD5940_ComplexPhase(&z_rev) * (float)(180.0 / M_PI); + out->pct_err = (mag_fwd > 0.0f) + ? fabsf(mag_fwd - mag_rev) / mag_fwd * 100.0f : 0.0f; return 0; } @@ -328,6 +415,10 @@ int eis_sweep(EISPoint *out, uint32_t max_points) uint32_t n = eis_calc_num_points(&ctx.cfg); if (n > max_points) n = max_points; + /* guard: throwaway at start frequency to warm up AFE */ + EISPoint guard; + eis_measure_point(ctx.cfg.freq_start_hz, &guard); + SoftSweepCfg_Type sweep; sweep.SweepEn = bTRUE; sweep.SweepStart = ctx.cfg.freq_start_hz; @@ -336,24 +427,27 @@ int eis_sweep(EISPoint *out, uint32_t max_points) sweep.SweepLog = bTRUE; sweep.SweepIndex = 0; - printf("\n%10s %12s %10s %12s %12s\n", - "Freq(Hz)", "|Z|(Ohm)", "Phase(deg)", "Re(Ohm)", "Im(Ohm)"); - printf("--------------------------------------------------------------\n"); + printf("\n%10s %12s %10s %12s %12s %7s\n", + "Freq(Hz)", "|Z|(Ohm)", "Phase(deg)", "Re(Ohm)", "Im(Ohm)", "Err%"); + printf("---------------------------------------------------------------------\n"); eis_measure_point(ctx.cfg.freq_start_hz, &out[0]); - printf("%10.1f %12.2f %10.2f %12.2f %12.2f\n", + printf("%10.1f %12.2f %10.2f %12.2f %12.2f %6.2f%%\n", out[0].freq_hz, out[0].mag_ohms, out[0].phase_deg, - out[0].z_real, out[0].z_imag); + out[0].z_real, out[0].z_imag, out[0].pct_err); for (uint32_t i = 1; i < n; i++) { float freq; AD5940_SweepNext(&sweep, &freq); eis_measure_point(freq, &out[i]); - printf("%10.1f %12.2f %10.2f %12.2f %12.2f\n", + printf("%10.1f %12.2f %10.2f %12.2f %12.2f %6.2f%%\n", out[i].freq_hz, out[i].mag_ohms, out[i].phase_deg, - out[i].z_real, out[i].z_imag); + out[i].z_real, out[i].z_imag, out[i].pct_err); } + /* guard: throwaway at stop frequency to cap the sweep cleanly */ + eis_measure_point(ctx.cfg.freq_stop_hz, &guard); + AD5940_AFECtrlS(AFECTRL_ALL, bFALSE); return (int)n; } diff --git a/main/eis.h b/main/eis.h index 662d0da..b23a5d6 100644 --- a/main/eis.h +++ b/main/eis.h @@ -18,6 +18,12 @@ typedef enum { RCAL_COUNT } EISRcal; +typedef enum { + ELEC_4WIRE = 0, /* AIN3 drive, AIN0 return, AIN2/AIN1 sense */ + ELEC_3WIRE, /* CE0 drive, RE0 sense+, SE0 sense-/TIA */ + ELEC_COUNT +} EISElectrode; + typedef struct { float freq_start_hz; float freq_stop_hz; @@ -25,6 +31,7 @@ typedef struct { EISRtia rtia; EISRcal rcal; + EISElectrode electrode; uint32_t pga; uint32_t excit_amp; } EISConfig; @@ -35,6 +42,11 @@ typedef struct { float phase_deg; float z_real; float z_imag; + float rtia_mag_before; + float rtia_mag_after; + float rev_mag; + float rev_phase; + float pct_err; } EISPoint; void eis_default_config(EISConfig *cfg); diff --git a/main/eis4.c b/main/eis4.c index 37f2c1e..4b5b7d9 100644 --- a/main/eis4.c +++ b/main/eis4.c @@ -2,15 +2,20 @@ #include "ad5940.h" #include "ad5941_port.h" #include "eis.h" +#include "echem.h" #include "ble.h" +#include "temp.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "nvs_flash.h" +#include "esp_log.h" #define AD5941_EXPECTED_ADIID 0x4144 - static EISConfig cfg; static EISPoint results[EIS_MAX_POINTS]; +static LSVPoint lsv_results[ECHEM_MAX_POINTS]; +static AmpPoint amp_results[ECHEM_MAX_POINTS]; +static ClPoint cl_results[ECHEM_MAX_POINTS]; static void do_sweep(void) { @@ -50,7 +55,9 @@ void app_main(void) if (adiid != AD5941_EXPECTED_ADIID) return; eis_default_config(&cfg); + temp_init(); + esp_log_level_set("NimBLE", ESP_LOG_WARN); ble_init(); printf("Waiting for BLE connection...\n"); ble_wait_for_connection(); @@ -69,7 +76,6 @@ void app_main(void) eis_reconfigure(&cfg); printf("Sweep: %.0f-%.0f Hz, %u ppd\n", cfg.freq_start_hz, cfg.freq_stop_hz, cfg.points_per_decade); - ble_send_config(&cfg); break; case CMD_SET_RTIA: @@ -78,7 +84,6 @@ void app_main(void) eis_reconfigure(&cfg); printf("RTIA: %u\n", cfg.rtia); } - ble_send_config(&cfg); break; case CMD_SET_RCAL: @@ -87,16 +92,115 @@ void app_main(void) eis_reconfigure(&cfg); printf("RCAL: %u\n", cfg.rcal); } - ble_send_config(&cfg); + break; + + case CMD_SET_ELECTRODE: + if (cmd.electrode < ELEC_COUNT) { + cfg.electrode = cmd.electrode; + eis_reconfigure(&cfg); + printf("Electrode: %s\n", + cfg.electrode == ELEC_3WIRE ? "3-wire (CE0/RE0/SE0)" : "4-wire (AIN)"); + } break; case CMD_START_SWEEP: + printf("Config: %.0f-%.0f Hz, %u ppd, rtia=%u, rcal=%u, elec=%s\n", + cfg.freq_start_hz, cfg.freq_stop_hz, cfg.points_per_decade, + cfg.rtia, cfg.rcal, + cfg.electrode == ELEC_3WIRE ? "3-wire" : "4-wire"); do_sweep(); break; case CMD_GET_CONFIG: ble_send_config(&cfg); break; + + case CMD_START_LSV: { + LSVConfig lsv_cfg; + lsv_cfg.v_start = cmd.lsv.v_start; + lsv_cfg.v_stop = cmd.lsv.v_stop; + lsv_cfg.scan_rate = cmd.lsv.scan_rate; + lsv_cfg.lp_rtia = cmd.lsv.lp_rtia; + printf("LSV: %.0f-%.0f mV, %.0f mV/s, rtia=%u\n", + lsv_cfg.v_start, lsv_cfg.v_stop, lsv_cfg.scan_rate, lsv_cfg.lp_rtia); + + int got = echem_lsv(&lsv_cfg, lsv_results, ECHEM_MAX_POINTS); + printf("LSV complete: %d points\n", got); + + ble_send_lsv_start(got, lsv_cfg.v_start, lsv_cfg.v_stop); + for (int i = 0; i < got; i++) { + ble_send_lsv_point(i, lsv_results[i].v_mv, lsv_results[i].i_ua); + vTaskDelay(pdMS_TO_TICKS(10)); + } + ble_send_lsv_end(); + break; + } + + case CMD_START_AMP: { + AmpConfig amp_cfg; + amp_cfg.v_hold = cmd.amp.v_hold; + amp_cfg.interval_ms = cmd.amp.interval_ms; + amp_cfg.duration_s = cmd.amp.duration_s; + amp_cfg.lp_rtia = cmd.amp.lp_rtia; + printf("Amp: %.0f mV, %.0f ms interval, %.0f s\n", + amp_cfg.v_hold, amp_cfg.interval_ms, amp_cfg.duration_s); + + ble_send_amp_start(amp_cfg.v_hold); + + int got = echem_amp(&_cfg, amp_results, ECHEM_MAX_POINTS); + printf("Amp complete: %d points\n", got); + + for (int i = 0; i < got; i++) { + ble_send_amp_point(i, amp_results[i].t_ms, amp_results[i].i_ua); + vTaskDelay(pdMS_TO_TICKS(10)); + } + ble_send_amp_end(); + break; + } + + case CMD_GET_TEMP: + ble_send_temp(temp_get()); + break; + + case CMD_START_PH: { + PhConfig ph_cfg; + ph_cfg.stabilize_s = cmd.ph.stabilize_s; + ph_cfg.temp_c = temp_get(); + printf("pH: stabilize %.0f s, temp %.1f C\n", + ph_cfg.stabilize_s, ph_cfg.temp_c); + + PhResult ph_result; + echem_ph_ocp(&ph_cfg, &ph_result); + printf("pH: OCP=%.1f mV, pH=%.2f\n", + ph_result.v_ocp_mv, ph_result.ph); + ble_send_ph_result(ph_result.v_ocp_mv, ph_result.ph, ph_result.temp_c); + break; + } + + case CMD_START_CL: { + ClConfig cl_cfg; + cl_cfg.v_cond = cmd.cl.v_cond; + cl_cfg.t_cond_ms = cmd.cl.t_cond_ms; + cl_cfg.v_free = cmd.cl.v_free; + cl_cfg.v_total = cmd.cl.v_total; + cl_cfg.t_dep_ms = cmd.cl.t_dep_ms; + cl_cfg.t_meas_ms = cmd.cl.t_meas_ms; + cl_cfg.lp_rtia = cmd.cl.lp_rtia; + + ClResult cl_result; + int got = echem_chlorine(&cl_cfg, cl_results, ECHEM_MAX_POINTS, &cl_result); + printf("Cl complete: %d points, free=%.3f uA, total=%.3f uA\n", + got, cl_result.i_free_ua, cl_result.i_total_ua); + + ble_send_cl_start(got); + for (int i = 0; i < got; i++) { + ble_send_cl_point(i, cl_results[i].t_ms, cl_results[i].i_ua, cl_results[i].phase); + vTaskDelay(pdMS_TO_TICKS(10)); + } + ble_send_cl_result(cl_result.i_free_ua, cl_result.i_total_ua); + ble_send_cl_end(); + break; + } } } } diff --git a/main/temp.c b/main/temp.c new file mode 100644 index 0000000..b765c73 --- /dev/null +++ b/main/temp.c @@ -0,0 +1,132 @@ +#include "temp.h" +#include "driver/gpio.h" +#include "esp_rom_sys.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include + +#define OW_PIN GPIO_NUM_35 + +static float cached_temp = 25.0f; +static portMUX_TYPE ow_mux = portMUX_INITIALIZER_UNLOCKED; + +static void ow_low(void) +{ + gpio_set_level(OW_PIN, 0); + gpio_set_direction(OW_PIN, GPIO_MODE_OUTPUT); +} + +static void ow_release(void) +{ + gpio_set_direction(OW_PIN, GPIO_MODE_INPUT); +} + +static int ow_reset(void) +{ + ow_low(); + esp_rom_delay_us(480); + + taskENTER_CRITICAL(&ow_mux); + ow_release(); + esp_rom_delay_us(70); + int presence = !gpio_get_level(OW_PIN); + taskEXIT_CRITICAL(&ow_mux); + + esp_rom_delay_us(410); + return presence; +} + +static void ow_write_bit(int bit) +{ + taskENTER_CRITICAL(&ow_mux); + ow_low(); + if (bit) { + esp_rom_delay_us(6); + ow_release(); + taskEXIT_CRITICAL(&ow_mux); + esp_rom_delay_us(54); + } else { + esp_rom_delay_us(60); + ow_release(); + taskEXIT_CRITICAL(&ow_mux); + esp_rom_delay_us(1); + } +} + +static int ow_read_bit(void) +{ + taskENTER_CRITICAL(&ow_mux); + ow_low(); + esp_rom_delay_us(2); + ow_release(); + esp_rom_delay_us(12); + int bit = gpio_get_level(OW_PIN); + taskEXIT_CRITICAL(&ow_mux); + esp_rom_delay_us(46); + return bit; +} + +static void ow_write_byte(uint8_t byte) +{ + for (int i = 0; i < 8; i++) { + ow_write_bit(byte & 1); + byte >>= 1; + } +} + +static uint8_t ow_read_byte(void) +{ + uint8_t byte = 0; + for (int i = 0; i < 8; i++) + if (ow_read_bit()) + byte |= (1 << i); + return byte; +} + +static float ds18b20_read(void) +{ + if (!ow_reset()) return NAN; + ow_write_byte(0xCC); /* Skip ROM */ + ow_write_byte(0x44); /* Convert T */ + vTaskDelay(pdMS_TO_TICKS(750)); + + if (!ow_reset()) return NAN; + ow_write_byte(0xCC); + ow_write_byte(0xBE); /* Read Scratchpad */ + + uint8_t lsb = ow_read_byte(); + uint8_t msb = ow_read_byte(); + + int16_t raw = (msb << 8) | lsb; + return raw / 16.0f; +} + +static void temp_task(void *arg) +{ + (void)arg; + for (;;) { + float t = ds18b20_read(); + if (!isnan(t)) + cached_temp = t; + vTaskDelay(pdMS_TO_TICKS(500)); + } +} + +void temp_init(void) +{ + gpio_config_t io = { + .pin_bit_mask = (1ULL << OW_PIN), + .mode = GPIO_MODE_INPUT, + .pull_up_en = GPIO_PULLUP_ENABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + gpio_config(&io); + + xTaskCreate(temp_task, "temp", 2048, NULL, 3, NULL); +} + +float temp_get(void) +{ + return cached_temp; +} diff --git a/main/temp.h b/main/temp.h new file mode 100644 index 0000000..cc2e8bc --- /dev/null +++ b/main/temp.h @@ -0,0 +1,7 @@ +#ifndef TEMP_H +#define TEMP_H + +void temp_init(void); +float temp_get(void); + +#endif diff --git a/sdkconfig b/sdkconfig index e5571d1..39c0cd2 100644 --- a/sdkconfig +++ b/sdkconfig @@ -1,7 +1,9 @@ # # Automatically generated file. DO NOT EDIT. -# Espressif IoT Development Framework (ESP-IDF) 5.5.1 Project Configuration +# Espressif IoT Development Framework (ESP-IDF) 5.4.3 Project Configuration # +CONFIG_SOC_MPU_MIN_REGION_SIZE=0x20000000 +CONFIG_SOC_MPU_REGIONS_MAX_NUM=8 CONFIG_SOC_ADC_SUPPORTED=y CONFIG_SOC_UART_SUPPORTED=y CONFIG_SOC_PCNT_SUPPORTED=y @@ -95,7 +97,6 @@ CONFIG_SOC_APB_BACKUP_DMA=y CONFIG_SOC_BROWNOUT_RESET_SUPPORTED=y CONFIG_SOC_CACHE_WRITEBACK_SUPPORTED=y CONFIG_SOC_CACHE_FREEZE_SUPPORTED=y -CONFIG_SOC_CACHE_ACS_INVALID_STATE_ON_PANIC=y CONFIG_SOC_CPU_CORES_NUM=2 CONFIG_SOC_CPU_INTR_NUM=32 CONFIG_SOC_CPU_HAS_FPU=y @@ -124,7 +125,6 @@ CONFIG_SOC_GPIO_OUT_RANGE_MAX=48 CONFIG_SOC_GPIO_VALID_DIGITAL_IO_PAD_MASK=0x0001FFFFFC000000 CONFIG_SOC_GPIO_CLOCKOUT_BY_IO_MUX=y CONFIG_SOC_GPIO_CLOCKOUT_CHANNEL_NUM=3 -CONFIG_SOC_GPIO_SUPPORT_HOLD_IO_IN_DSLP=y CONFIG_SOC_DEDIC_GPIO_OUT_CHANNELS_NUM=8 CONFIG_SOC_DEDIC_GPIO_IN_CHANNELS_NUM=8 CONFIG_SOC_DEDIC_GPIO_OUT_AUTO_ENABLE=y @@ -147,10 +147,8 @@ CONFIG_SOC_I2S_SUPPORTS_PLL_F160M=y CONFIG_SOC_I2S_SUPPORTS_PCM=y CONFIG_SOC_I2S_SUPPORTS_PDM=y CONFIG_SOC_I2S_SUPPORTS_PDM_TX=y -CONFIG_SOC_I2S_SUPPORTS_PCM2PDM=y -CONFIG_SOC_I2S_SUPPORTS_PDM_RX=y -CONFIG_SOC_I2S_SUPPORTS_PDM2PCM=y CONFIG_SOC_I2S_PDM_MAX_TX_LINES=2 +CONFIG_SOC_I2S_SUPPORTS_PDM_RX=y CONFIG_SOC_I2S_PDM_MAX_RX_LINES=4 CONFIG_SOC_I2S_SUPPORTS_TDM=y CONFIG_SOC_LEDC_SUPPORT_APB_CLOCK=y @@ -172,8 +170,6 @@ CONFIG_SOC_MCPWM_GPIO_SYNCHROS_PER_GROUP=3 CONFIG_SOC_MCPWM_SWSYNC_CAN_PROPAGATE=y CONFIG_SOC_MMU_LINEAR_ADDRESS_REGION_NUM=1 CONFIG_SOC_MMU_PERIPH_NUM=1 -CONFIG_SOC_MPU_MIN_REGION_SIZE=0x20000000 -CONFIG_SOC_MPU_REGIONS_MAX_NUM=8 CONFIG_SOC_PCNT_GROUPS=1 CONFIG_SOC_PCNT_UNITS_PER_GROUP=4 CONFIG_SOC_PCNT_CHANNELS_PER_UNIT=2 @@ -185,7 +181,7 @@ CONFIG_SOC_RMT_CHANNELS_PER_GROUP=8 CONFIG_SOC_RMT_MEM_WORDS_PER_CHANNEL=48 CONFIG_SOC_RMT_SUPPORT_RX_PINGPONG=y CONFIG_SOC_RMT_SUPPORT_RX_DEMODULATION=y -CONFIG_SOC_RMT_SUPPORT_TX_ASYNC_STOP=y +CONFIG_SOC_RMT_SUPPORT_ASYNC_STOP=y CONFIG_SOC_RMT_SUPPORT_TX_LOOP_COUNT=y CONFIG_SOC_RMT_SUPPORT_TX_LOOP_AUTO_STOP=y CONFIG_SOC_RMT_SUPPORT_TX_SYNCHRO=y @@ -208,6 +204,7 @@ CONFIG_SOC_LCDCAM_RGB_DATA_WIDTH=16 CONFIG_SOC_RTC_CNTL_CPU_PD_DMA_BUS_WIDTH=128 CONFIG_SOC_RTC_CNTL_CPU_PD_REG_FILE_NUM=549 CONFIG_SOC_RTC_CNTL_TAGMEM_PD_DMA_BUS_WIDTH=128 +CONFIG_SOC_RTC_CNTL_NEEDS_ATOMIC_ACCESS=y CONFIG_SOC_RTCIO_PIN_COUNT=22 CONFIG_SOC_RTCIO_INPUT_OUTPUT_SUPPORTED=y CONFIG_SOC_RTCIO_HOLD_SUPPORTED=y @@ -234,7 +231,7 @@ CONFIG_SOC_SPI_SCT_SUPPORTED=y CONFIG_SOC_SPI_SCT_REG_NUM=14 CONFIG_SOC_SPI_SCT_BUFFER_NUM_MAX=y CONFIG_SOC_SPI_SCT_CONF_BITLEN_MAX=0x3FFFA -CONFIG_SOC_MEMSPI_SRC_FREQ_120M_SUPPORTED=y +CONFIG_SOC_MEMSPI_SRC_FREQ_120M=y CONFIG_SOC_MEMSPI_SRC_FREQ_80M_SUPPORTED=y CONFIG_SOC_MEMSPI_SRC_FREQ_40M_SUPPORTED=y CONFIG_SOC_MEMSPI_SRC_FREQ_20M_SUPPORTED=y @@ -257,18 +254,13 @@ CONFIG_SOC_LP_TIMER_BIT_WIDTH_LO=32 CONFIG_SOC_LP_TIMER_BIT_WIDTH_HI=16 CONFIG_SOC_TOUCH_SENSOR_VERSION=2 CONFIG_SOC_TOUCH_SENSOR_NUM=15 -CONFIG_SOC_TOUCH_MIN_CHAN_ID=1 -CONFIG_SOC_TOUCH_MAX_CHAN_ID=14 -CONFIG_SOC_TOUCH_SUPPORT_BENCHMARK=y CONFIG_SOC_TOUCH_SUPPORT_SLEEP_WAKEUP=y CONFIG_SOC_TOUCH_SUPPORT_WATERPROOF=y CONFIG_SOC_TOUCH_SUPPORT_PROX_SENSING=y -CONFIG_SOC_TOUCH_SUPPORT_DENOISE_CHAN=y CONFIG_SOC_TOUCH_PROXIMITY_CHANNEL_NUM=3 CONFIG_SOC_TOUCH_PROXIMITY_MEAS_DONE_SUPPORTED=y CONFIG_SOC_TOUCH_SAMPLE_CFG_NUM=1 CONFIG_SOC_TWAI_CONTROLLER_NUM=1 -CONFIG_SOC_TWAI_MASK_FILTER_NUM=1 CONFIG_SOC_TWAI_CLK_SUPPORT_APB=y CONFIG_SOC_TWAI_BRP_MIN=2 CONFIG_SOC_TWAI_BRP_MAX=16384 @@ -282,7 +274,6 @@ CONFIG_SOC_UART_SUPPORT_WAKEUP_INT=y CONFIG_SOC_UART_SUPPORT_APB_CLK=y CONFIG_SOC_UART_SUPPORT_RTC_CLK=y CONFIG_SOC_UART_SUPPORT_XTAL_CLK=y -CONFIG_SOC_UART_WAKEUP_SUPPORT_ACTIVE_THRESH_MODE=y CONFIG_SOC_UHCI_NUM=1 CONFIG_SOC_USB_OTG_PERIPH_NUM=1 CONFIG_SOC_SHA_DMA_MAX_BUFFER_SIZE=3968 @@ -326,7 +317,6 @@ CONFIG_SOC_CLK_RC_FAST_D256_SUPPORTED=y CONFIG_SOC_RTC_SLOW_CLK_SUPPORT_RC_FAST_D256=y CONFIG_SOC_CLK_RC_FAST_SUPPORT_CALIBRATION=y CONFIG_SOC_CLK_XTAL32K_SUPPORTED=y -CONFIG_SOC_CLK_LP_FAST_SUPPORT_XTAL_D2=y CONFIG_SOC_EFUSE_DIS_DOWNLOAD_ICACHE=y CONFIG_SOC_EFUSE_DIS_DOWNLOAD_DCACHE=y CONFIG_SOC_EFUSE_HARD_DIS_JTAG=y @@ -353,7 +343,7 @@ CONFIG_SOC_SPI_MEM_SUPPORT_AUTO_WAIT_IDLE=y CONFIG_SOC_SPI_MEM_SUPPORT_AUTO_SUSPEND=y CONFIG_SOC_SPI_MEM_SUPPORT_AUTO_RESUME=y CONFIG_SOC_SPI_MEM_SUPPORT_SW_SUSPEND=y -CONFIG_SOC_SPI_MEM_SUPPORT_FLASH_OPI_MODE=y +CONFIG_SOC_SPI_MEM_SUPPORT_OPI_MODE=y CONFIG_SOC_SPI_MEM_SUPPORT_TIMING_TUNING=y CONFIG_SOC_SPI_MEM_SUPPORT_CONFIG_GPIO_BY_EFUSE=y CONFIG_SOC_SPI_MEM_SUPPORT_WRAP=y @@ -418,17 +408,6 @@ CONFIG_BOOTLOADER_COMPILE_TIME_DATE=y CONFIG_BOOTLOADER_PROJECT_VER=1 # end of Bootloader manager -# -# Application Rollback -# -# CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE is not set -# end of Application Rollback - -# -# Recovery Bootloader and Rollback -# -# end of Recovery Bootloader and Rollback - CONFIG_BOOTLOADER_OFFSET_IN_FLASH=0x0 CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_SIZE=y # CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_DEBUG is not set @@ -438,8 +417,6 @@ CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_SIZE=y # # Log # -CONFIG_BOOTLOADER_LOG_VERSION_1=y -CONFIG_BOOTLOADER_LOG_VERSION=1 # CONFIG_BOOTLOADER_LOG_LEVEL_NONE is not set # CONFIG_BOOTLOADER_LOG_LEVEL_ERROR is not set # CONFIG_BOOTLOADER_LOG_LEVEL_WARN is not set @@ -454,13 +431,6 @@ CONFIG_BOOTLOADER_LOG_LEVEL=3 # CONFIG_BOOTLOADER_LOG_COLORS is not set CONFIG_BOOTLOADER_LOG_TIMESTAMP_SOURCE_CPU_TICKS=y # end of Format - -# -# Settings -# -CONFIG_BOOTLOADER_LOG_MODE_TEXT_EN=y -CONFIG_BOOTLOADER_LOG_MODE_TEXT=y -# end of Settings # end of Log # @@ -477,6 +447,7 @@ CONFIG_BOOTLOADER_REGION_PROTECTION_ENABLE=y CONFIG_BOOTLOADER_WDT_ENABLE=y # CONFIG_BOOTLOADER_WDT_DISABLE_IN_USER_CODE is not set CONFIG_BOOTLOADER_WDT_TIME_MS=9000 +# CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE is not set # CONFIG_BOOTLOADER_SKIP_VALIDATE_IN_DEEP_SLEEP is not set # CONFIG_BOOTLOADER_SKIP_VALIDATE_ON_POWER_ON is not set # CONFIG_BOOTLOADER_SKIP_VALIDATE_ALWAYS is not set @@ -520,7 +491,6 @@ CONFIG_ESP_ROM_HAS_HAL_WDT=y CONFIG_ESP_ROM_NEEDS_SWSETUP_WORKAROUND=y CONFIG_ESP_ROM_HAS_LAYOUT_TABLE=y CONFIG_ESP_ROM_HAS_SPI_FLASH=y -CONFIG_ESP_ROM_HAS_SPI_FLASH_MMAP=y CONFIG_ESP_ROM_HAS_ETS_PRINTF_BUG=y CONFIG_ESP_ROM_HAS_NEWLIB=y CONFIG_ESP_ROM_HAS_NEWLIB_NANO_FORMAT=y @@ -660,30 +630,33 @@ CONFIG_BT_CONTROLLER_ENABLED=y # # NimBLE Options # + +# +# General +# CONFIG_BT_NIMBLE_MEM_ALLOC_MODE_INTERNAL=y # CONFIG_BT_NIMBLE_MEM_ALLOC_MODE_DEFAULT is not set -# CONFIG_BT_NIMBLE_LOG_LEVEL_NONE is not set -# CONFIG_BT_NIMBLE_LOG_LEVEL_ERROR is not set -# CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING is not set -CONFIG_BT_NIMBLE_LOG_LEVEL_INFO=y -# CONFIG_BT_NIMBLE_LOG_LEVEL_DEBUG is not set -CONFIG_BT_NIMBLE_LOG_LEVEL=1 -CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 -CONFIG_BT_NIMBLE_MAX_BONDS=3 -CONFIG_BT_NIMBLE_MAX_CCCDS=8 -CONFIG_BT_NIMBLE_L2CAP_COC_MAX_NUM=0 +CONFIG_BT_NIMBLE_PINNED_TO_CORE=0 CONFIG_BT_NIMBLE_PINNED_TO_CORE_0=y # CONFIG_BT_NIMBLE_PINNED_TO_CORE_1 is not set -CONFIG_BT_NIMBLE_PINNED_TO_CORE=0 CONFIG_BT_NIMBLE_HOST_TASK_STACK_SIZE=4096 +CONFIG_BT_NIMBLE_LEGACY_VHCI_ENABLE=y +# end of General + +# +# Roles and Profiles +# CONFIG_BT_NIMBLE_ROLE_CENTRAL=y CONFIG_BT_NIMBLE_ROLE_PERIPHERAL=y CONFIG_BT_NIMBLE_ROLE_BROADCASTER=y CONFIG_BT_NIMBLE_ROLE_OBSERVER=y CONFIG_BT_NIMBLE_GATT_CLIENT=y CONFIG_BT_NIMBLE_GATT_SERVER=y -CONFIG_BT_NIMBLE_NVS_PERSIST=y -# CONFIG_BT_NIMBLE_SMP_ID_RESET is not set +# end of Roles and Profiles + +# +# Security (SMP) +# CONFIG_BT_NIMBLE_SECURITY_ENABLE=y CONFIG_BT_NIMBLE_SM_LEGACY=y CONFIG_BT_NIMBLE_SM_SC=y @@ -691,14 +664,46 @@ CONFIG_BT_NIMBLE_SM_SC=y CONFIG_BT_NIMBLE_LL_CFG_FEAT_LE_ENCRYPTION=y CONFIG_BT_NIMBLE_SM_LVL=0 CONFIG_BT_NIMBLE_SM_SC_ONLY=0 -CONFIG_BT_NIMBLE_PRINT_ERR_NAME=y -# CONFIG_BT_NIMBLE_DEBUG is not set -# CONFIG_BT_NIMBLE_DYNAMIC_SERVICE is not set -CONFIG_BT_NIMBLE_SVC_GAP_DEVICE_NAME="nimble" -CONFIG_BT_NIMBLE_GAP_DEVICE_NAME_MAX_LEN=31 +# CONFIG_BT_NIMBLE_SMP_ID_RESET is not set +CONFIG_BT_NIMBLE_NVS_PERSIST=y +CONFIG_BT_NIMBLE_MAX_BONDS=3 +# CONFIG_BT_NIMBLE_HANDLE_REPEAT_PAIRING_DELETION is not set +# end of Security (SMP) + +# +# GAP +# +CONFIG_BT_NIMBLE_RPA_TIMEOUT=900 +CONFIG_BT_NIMBLE_WHITELIST_SIZE=12 +CONFIG_BT_NIMBLE_ENABLE_CONN_REATTEMPT=y +CONFIG_BT_NIMBLE_MAX_CONN_REATTEMPT=3 +CONFIG_BT_NIMBLE_HS_PVCY=y +# CONFIG_BT_NIMBLE_HOST_ALLOW_CONNECT_WITH_SCAN is not set +# CONFIG_BT_NIMBLE_HOST_QUEUE_CONG_CHECK is not set +CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 +CONFIG_BT_NIMBLE_MAX_CCCDS=8 +CONFIG_BT_NIMBLE_CRYPTO_STACK_MBEDTLS=y +CONFIG_BT_NIMBLE_HS_STOP_TIMEOUT_MS=2000 +CONFIG_BT_NIMBLE_USE_ESP_TIMER=y +# CONFIG_BT_NIMBLE_HS_FLOW_CTRL is not set +# end of GAP + +# +# GATT / ATT +# CONFIG_BT_NIMBLE_ATT_PREFERRED_MTU=256 CONFIG_BT_NIMBLE_ATT_MAX_PREP_ENTRIES=64 -CONFIG_BT_NIMBLE_SVC_GAP_APPEARANCE=0 +CONFIG_BT_NIMBLE_GATT_MAX_PROCS=4 +# CONFIG_BT_NIMBLE_BLE_GATT_BLOB_TRANSFER is not set +# CONFIG_BT_NIMBLE_GATT_CACHING is not set +# CONFIG_BT_NIMBLE_INCL_SVC_DISCOVERY is not set +# end of GATT / ATT + +# +# L2CAP +# +CONFIG_BT_NIMBLE_L2CAP_COC_MAX_NUM=0 +# end of L2CAP # # Memory Settings @@ -713,17 +718,12 @@ CONFIG_BT_NIMBLE_TRANSPORT_EVT_SIZE=70 CONFIG_BT_NIMBLE_TRANSPORT_EVT_COUNT=30 CONFIG_BT_NIMBLE_TRANSPORT_EVT_DISCARD_COUNT=8 CONFIG_BT_NIMBLE_L2CAP_COC_SDU_BUFF_COUNT=1 +# CONFIG_BT_NIMBLE_MEMPOOL_RUNTIME_ALLOC is not set # end of Memory Settings -CONFIG_BT_NIMBLE_GATT_MAX_PROCS=4 -# CONFIG_BT_NIMBLE_HS_FLOW_CTRL is not set -CONFIG_BT_NIMBLE_RPA_TIMEOUT=900 -# CONFIG_BT_NIMBLE_MESH is not set -CONFIG_BT_NIMBLE_CRYPTO_STACK_MBEDTLS=y -CONFIG_BT_NIMBLE_HS_STOP_TIMEOUT_MS=2000 -CONFIG_BT_NIMBLE_ENABLE_CONN_REATTEMPT=y -CONFIG_BT_NIMBLE_MAX_CONN_REATTEMPT=3 -# CONFIG_BT_NIMBLE_HANDLE_REPEAT_PAIRING_DELETION is not set +# +# BLE 5.x Features +# CONFIG_BT_NIMBLE_50_FEATURE_SUPPORT=y CONFIG_BT_NIMBLE_LL_CFG_FEAT_LE_2M_PHY=y CONFIG_BT_NIMBLE_LL_CFG_FEAT_LE_CODED_PHY=y @@ -731,17 +731,11 @@ CONFIG_BT_NIMBLE_LL_CFG_FEAT_LE_CODED_PHY=y CONFIG_BT_NIMBLE_EXT_SCAN=y CONFIG_BT_NIMBLE_ENABLE_PERIODIC_SYNC=y CONFIG_BT_NIMBLE_MAX_PERIODIC_SYNCS=0 -# CONFIG_BT_NIMBLE_GATT_CACHING is not set -# CONFIG_BT_NIMBLE_INCL_SVC_DISCOVERY is not set -CONFIG_BT_NIMBLE_WHITELIST_SIZE=12 -# CONFIG_BT_NIMBLE_TEST_THROUGHPUT_TEST is not set -# CONFIG_BT_NIMBLE_BLUFI_ENABLE is not set -CONFIG_BT_NIMBLE_USE_ESP_TIMER=y -CONFIG_BT_NIMBLE_LEGACY_VHCI_ENABLE=y -# CONFIG_BT_NIMBLE_BLE_GATT_BLOB_TRANSFER is not set +# CONFIG_BT_NIMBLE_ISO is not set +# end of BLE 5.x Features # -# BLE Services +# Services # CONFIG_BT_NIMBLE_PROX_SERVICE=y CONFIG_BT_NIMBLE_ANS_SERVICE=y @@ -766,6 +760,19 @@ CONFIG_BT_NIMBLE_DIS_SERVICE=y # CONFIG_BT_NIMBLE_SVC_DIS_PNP_ID is not set # CONFIG_BT_NIMBLE_SVC_DIS_INCLUDED is not set CONFIG_BT_NIMBLE_GAP_SERVICE=y +CONFIG_BT_NIMBLE_SVC_GAP_DEVICE_NAME="nimble" +CONFIG_BT_NIMBLE_GAP_DEVICE_NAME_MAX_LEN=31 +CONFIG_BT_NIMBLE_SVC_GAP_APPEARANCE=0 +CONFIG_BT_NIMBLE_SVC_GAP_NAME_WRITE_PERM=0 +CONFIG_BT_NIMBLE_SVC_GAP_NAME_WRITE_PERM_ENC=0 +CONFIG_BT_NIMBLE_SVC_GAP_NAME_WRITE_PERM_AUTHEN=0 +CONFIG_BT_NIMBLE_SVC_GAP_NAME_WRITE_PERM_AUTHOR=0 +# CONFIG_BT_NIMBLE_SVC_GAP_GATT_SECURITY_LEVEL is not set +# CONFIG_BT_NIMBLE_SVC_GAP_RPA_ONLY is not set +CONFIG_BT_NIMBLE_SVC_GAP_CAR_CHAR_NOT_SUPP=y +# CONFIG_BT_NIMBLE_SVC_GAP_CAR_NOT_SUPP is not set +# CONFIG_BT_NIMBLE_SVC_GAP_CAR_SUPP is not set +CONFIG_BT_NIMBLE_SVC_GAP_CENT_ADDR_RESOLUTION=-1 # # GAP Appearance write permissions @@ -777,11 +784,6 @@ CONFIG_BT_NIMBLE_SVC_GAP_APPEAR_WRITE_PERM_ATHN=0 CONFIG_BT_NIMBLE_SVC_GAP_APPEAR_WRITE_PERM_ATHR=0 # end of GAP Appearance write permissions -CONFIG_BT_NIMBLE_SVC_GAP_CAR_CHAR_NOT_SUPP=y -# CONFIG_BT_NIMBLE_SVC_GAP_CAR_NOT_SUPP is not set -# CONFIG_BT_NIMBLE_SVC_GAP_CAR_SUPP is not set -CONFIG_BT_NIMBLE_SVC_GAP_CENT_ADDR_RESOLUTION=-1 - # # GAP device name write permissions # @@ -796,22 +798,34 @@ CONFIG_BT_NIMBLE_SVC_GAP_PPCP_MIN_CONN_INTERVAL=0 CONFIG_BT_NIMBLE_SVC_GAP_PPCP_SLAVE_LATENCY=0 CONFIG_BT_NIMBLE_SVC_GAP_PPCP_SUPERVISION_TMO=0 # end of Peripheral Preferred Connection Parameters (PPCP) settings +# end of Services -CONFIG_BT_NIMBLE_SVC_GAP_NAME_WRITE_PERM=0 -CONFIG_BT_NIMBLE_SVC_GAP_NAME_WRITE_PERM_ENC=0 -CONFIG_BT_NIMBLE_SVC_GAP_NAME_WRITE_PERM_AUTHEN=0 -CONFIG_BT_NIMBLE_SVC_GAP_NAME_WRITE_PERM_AUTHOR=0 -# CONFIG_BT_NIMBLE_SVC_GAP_GATT_SECURITY_LEVEL is not set -# CONFIG_BT_NIMBLE_SVC_GAP_RPA_ONLY is not set -# end of BLE Services - -# CONFIG_BT_NIMBLE_VS_SUPPORT is not set +# +# Extra Features +# +# CONFIG_BT_NIMBLE_DYNAMIC_SERVICE is not set +# CONFIG_BT_NIMBLE_BLUFI_ENABLE is not set # CONFIG_BT_NIMBLE_ENC_ADV_DATA is not set -# CONFIG_BT_NIMBLE_HIGH_DUTY_ADV_ITVL is not set -# CONFIG_BT_NIMBLE_HOST_ALLOW_CONNECT_WITH_SCAN is not set -# CONFIG_BT_NIMBLE_HOST_QUEUE_CONG_CHECK is not set +# CONFIG_BT_NIMBLE_ADV_UUID_CONCAT is not set # CONFIG_BT_NIMBLE_GATTC_PROC_PREEMPTION_PROTECT is not set # CONFIG_BT_NIMBLE_GATTC_AUTO_PAIR is not set +CONFIG_BT_NIMBLE_EATT_CHAN_NUM=0 +# CONFIG_BT_NIMBLE_SUBRATE is not set +# CONFIG_BT_NIMBLE_STATIC_PASSKEY is not set +CONFIG_BT_NIMBLE_DTM_MODE_TEST=y +CONFIG_BT_NIMBLE_MEM_OPTIMIZATION=y +CONFIG_BT_NIMBLE_STATIC_TO_DYNAMIC=y +CONFIG_BT_NIMBLE_SM_SIGN_CNT=y +CONFIG_BT_NIMBLE_CPFD_CAFD=y +CONFIG_BT_NIMBLE_RECONFIG_MTU=y +# CONFIG_BT_NIMBLE_LOW_SPEED_MODE is not set +# end of Extra Features + +# +# NimBLE Mesh +# +# CONFIG_BT_NIMBLE_MESH is not set +# end of NimBLE Mesh # # Host-controller Transport @@ -823,8 +837,35 @@ CONFIG_BT_NIMBLE_HCI_UART_RTS_PIN=19 CONFIG_BT_NIMBLE_HCI_UART_CTS_PIN=23 # end of Host-controller Transport -CONFIG_BT_NIMBLE_EATT_CHAN_NUM=0 -# CONFIG_BT_NIMBLE_SUBRATE is not set +# +# Debugging/Testing +# +# CONFIG_BT_NIMBLE_MEM_DEBUG is not set +# CONFIG_BT_NIMBLE_LOG_LEVEL_NONE is not set +# CONFIG_BT_NIMBLE_LOG_LEVEL_ERROR is not set +# CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING is not set +CONFIG_BT_NIMBLE_LOG_LEVEL_INFO=y +# CONFIG_BT_NIMBLE_LOG_LEVEL_DEBUG is not set +CONFIG_BT_NIMBLE_LOG_LEVEL=1 +CONFIG_BT_NIMBLE_PRINT_ERR_NAME=y +# CONFIG_BT_NIMBLE_DEBUG is not set +# CONFIG_BT_NIMBLE_TEST_THROUGHPUT_TEST is not set +# end of Debugging/Testing + +# +# Vendor / Optimization +# +# CONFIG_BT_NIMBLE_VS_SUPPORT is not set +# CONFIG_BT_NIMBLE_HIGH_DUTY_ADV_ITVL is not set +# end of Vendor / Optimization + +# +# Helper Utils +# +CONFIG_BT_NIMBLE_CHK_HOST_STATUS=y +CONFIG_BT_NIMBLE_UTIL_API=y +CONFIG_BT_NIMBLE_EXTRA_ADV_FIELDS=y +# end of Helper Utils # end of NimBLE Options # @@ -931,8 +972,18 @@ CONFIG_BT_CTRL_BLE_ADV=y # Common Options # CONFIG_BT_ALARM_MAX_NUM=50 +CONFIG_BT_SMP_CRYPTO_STACK_TINYCRYPT=y +# CONFIG_BT_SMP_CRYPTO_STACK_MBEDTLS is not set + +# +# BLE Log +# +# CONFIG_BLE_LOG_ENABLED is not set +# end of BLE Log + # CONFIG_BT_BLE_LOG_SPI_OUT_ENABLED is not set # CONFIG_BT_BLE_LOG_UHCI_OUT_ENABLED is not set +# CONFIG_BT_LE_USED_MEM_STATISTICS_ENABLED is not set # end of Common Options # CONFIG_BT_HCI_LOG_DEBUG_EN is not set @@ -951,11 +1002,11 @@ CONFIG_BT_ALARM_MAX_NUM=50 # # -# Legacy TWAI Driver Configurations +# TWAI Configuration # -# CONFIG_TWAI_SKIP_LEGACY_CONFLICT_CHECK is not set +# CONFIG_TWAI_ISR_IN_IRAM is not set CONFIG_TWAI_ERRATA_FIX_LISTEN_ONLY_DOM=y -# end of Legacy TWAI Driver Configurations +# end of TWAI Configuration # # Legacy ADC Driver Configuration @@ -1024,13 +1075,6 @@ CONFIG_TWAI_ERRATA_FIX_LISTEN_ONLY_DOM=y # CONFIG_TEMP_SENSOR_SUPPRESS_DEPRECATE_WARN is not set # CONFIG_TEMP_SENSOR_SKIP_LEGACY_CONFLICT_CHECK is not set # end of Legacy Temperature Sensor Driver Configurations - -# -# Legacy Touch Sensor Driver Configurations -# -# CONFIG_TOUCH_SUPPRESS_DEPRECATE_WARN is not set -# CONFIG_TOUCH_SKIP_LEGACY_CONFLICT_CHECK is not set -# end of Legacy Touch Sensor Driver Configurations # end of Driver Configurations # @@ -1045,7 +1089,6 @@ CONFIG_EFUSE_MAX_BLK_LEN=256 # ESP-TLS # CONFIG_ESP_TLS_USING_MBEDTLS=y -# CONFIG_ESP_TLS_USE_SECURE_ELEMENT is not set CONFIG_ESP_TLS_USE_DS_PERIPHERAL=y # CONFIG_ESP_TLS_CLIENT_SESSION_TICKETS is not set # CONFIG_ESP_TLS_SERVER_SESSION_TICKETS is not set @@ -1097,7 +1140,7 @@ CONFIG_ESP_ERR_TO_NAME_LOOKUP=y # CONFIG_GPTIMER_ISR_HANDLER_IN_IRAM=y # CONFIG_GPTIMER_CTRL_FUNC_IN_IRAM is not set -# CONFIG_GPTIMER_ISR_CACHE_SAFE is not set +# CONFIG_GPTIMER_ISR_IRAM_SAFE is not set CONFIG_GPTIMER_OBJ_CACHE_SAFE=y # CONFIG_GPTIMER_ENABLE_DEBUG_LOG is not set # end of ESP-Driver:GPTimer Configurations @@ -1108,7 +1151,6 @@ CONFIG_GPTIMER_OBJ_CACHE_SAFE=y # CONFIG_I2C_ISR_IRAM_SAFE is not set # CONFIG_I2C_ENABLE_DEBUG_LOG is not set # CONFIG_I2C_ENABLE_SLAVE_DRIVER_VERSION_2 is not set -CONFIG_I2C_MASTER_ISR_HANDLER_IN_IRAM=y # end of ESP-Driver:I2C Configurations # @@ -1127,10 +1169,8 @@ CONFIG_I2C_MASTER_ISR_HANDLER_IN_IRAM=y # # ESP-Driver:MCPWM Configurations # -CONFIG_MCPWM_ISR_HANDLER_IN_IRAM=y -# CONFIG_MCPWM_ISR_CACHE_SAFE is not set +# CONFIG_MCPWM_ISR_IRAM_SAFE is not set # CONFIG_MCPWM_CTRL_FUNC_IN_IRAM is not set -CONFIG_MCPWM_OBJ_CACHE_SAFE=y # CONFIG_MCPWM_ENABLE_DEBUG_LOG is not set # end of ESP-Driver:MCPWM Configurations @@ -1145,15 +1185,9 @@ CONFIG_MCPWM_OBJ_CACHE_SAFE=y # # ESP-Driver:RMT Configurations # -CONFIG_RMT_ENCODER_FUNC_IN_IRAM=y -CONFIG_RMT_TX_ISR_HANDLER_IN_IRAM=y -CONFIG_RMT_RX_ISR_HANDLER_IN_IRAM=y -# CONFIG_RMT_RECV_FUNC_IN_IRAM is not set -# CONFIG_RMT_TX_ISR_CACHE_SAFE is not set -# CONFIG_RMT_RX_ISR_CACHE_SAFE is not set -CONFIG_RMT_OBJ_CACHE_SAFE=y -# CONFIG_RMT_ENABLE_DEBUG_LOG is not set # CONFIG_RMT_ISR_IRAM_SAFE is not set +# CONFIG_RMT_RECV_FUNC_IN_IRAM is not set +# CONFIG_RMT_ENABLE_DEBUG_LOG is not set # end of ESP-Driver:RMT Configurations # @@ -1178,7 +1212,6 @@ CONFIG_SPI_SLAVE_ISR_IN_IRAM=y # CONFIG_TOUCH_CTRL_FUNC_IN_IRAM is not set # CONFIG_TOUCH_ISR_IRAM_SAFE is not set # CONFIG_TOUCH_ENABLE_DEBUG_LOG is not set -# CONFIG_TOUCH_SKIP_FSM_CHECK is not set # end of ESP-Driver:Touch Sensor Configurations # @@ -1187,14 +1220,6 @@ CONFIG_SPI_SLAVE_ISR_IN_IRAM=y # CONFIG_TEMP_SENSOR_ENABLE_DEBUG_LOG is not set # end of ESP-Driver:Temperature Sensor Configurations -# -# ESP-Driver:TWAI Configurations -# -# CONFIG_TWAI_ISR_IN_IRAM is not set -# CONFIG_TWAI_ISR_CACHE_SAFE is not set -# CONFIG_TWAI_ENABLE_DEBUG_LOG is not set -# end of ESP-Driver:TWAI Configurations - # # ESP-Driver:UART Configurations # @@ -1287,7 +1312,6 @@ CONFIG_ESP_HTTPS_OTA_EVENT_POST_TIMEOUT=2000 # # CONFIG_ESP_HTTPS_SERVER_ENABLE is not set CONFIG_ESP_HTTPS_SERVER_EVENT_POST_TIMEOUT=2000 -# CONFIG_ESP_HTTPS_SERVER_CERT_SELECT_HOOK is not set # end of ESP HTTPS server # @@ -1358,8 +1382,7 @@ CONFIG_RTC_CLK_CAL_CYCLES=1024 # # Peripheral Control # -CONFIG_ESP_PERIPH_CTRL_FUNC_IN_IRAM=y -CONFIG_ESP_REGI2C_CTRL_FUNC_IN_IRAM=y +CONFIG_PERIPH_CTRL_FUNC_IN_IRAM=y # end of Peripheral Control # @@ -1379,36 +1402,15 @@ CONFIG_XTAL_FREQ_40=y CONFIG_XTAL_FREQ=40 # end of Main XTAL Config -# -# Power Supplier -# - -# -# Brownout Detector -# -CONFIG_ESP_BROWNOUT_DET=y -CONFIG_ESP_BROWNOUT_DET_LVL_SEL_7=y -# CONFIG_ESP_BROWNOUT_DET_LVL_SEL_6 is not set -# CONFIG_ESP_BROWNOUT_DET_LVL_SEL_5 is not set -# CONFIG_ESP_BROWNOUT_DET_LVL_SEL_4 is not set -# CONFIG_ESP_BROWNOUT_DET_LVL_SEL_3 is not set -# CONFIG_ESP_BROWNOUT_DET_LVL_SEL_2 is not set -# CONFIG_ESP_BROWNOUT_DET_LVL_SEL_1 is not set -CONFIG_ESP_BROWNOUT_DET_LVL=7 -CONFIG_ESP_BROWNOUT_USE_INTR=y -# end of Brownout Detector -# end of Power Supplier - CONFIG_ESP_SPI_BUS_LOCK_ISR_FUNCS_IN_IRAM=y -CONFIG_ESP_INTR_IN_IRAM=y # end of Hardware Settings # # ESP-Driver:LCD Controller Configurations # -# CONFIG_LCD_ENABLE_DEBUG_LOG is not set # CONFIG_LCD_RGB_ISR_IRAM_SAFE is not set # CONFIG_LCD_RGB_RESTART_IN_VSYNC is not set +# CONFIG_LCD_ENABLE_DEBUG_LOG is not set # end of ESP-Driver:LCD Controller Configurations # @@ -1462,7 +1464,6 @@ CONFIG_ESP_PHY_IRAM_OPT=y # # Power Management # -CONFIG_PM_SLEEP_FUNC_IN_IRAM=y # CONFIG_PM_ENABLE is not set CONFIG_PM_SLP_IRAM_OPT=y CONFIG_PM_POWER_DOWN_CPU_IN_LIGHT_SLEEP=y @@ -1481,12 +1482,6 @@ CONFIG_PM_RESTORE_CACHE_TAGMEM_AFTER_LIGHT_SLEEP=y # CONFIG_RINGBUF_PLACE_FUNCTIONS_INTO_FLASH is not set # end of ESP Ringbuf -# -# ESP-ROM -# -CONFIG_ESP_ROM_PRINT_IN_IRAM=y -# end of ESP-ROM - # # ESP Security Specific # @@ -1539,7 +1534,6 @@ CONFIG_ESP32S3_DATA_CACHE_LINE_SIZE=32 CONFIG_ESP32S3_TRACEMEM_RESERVE_DRAM=0x0 # end of Trace memory -CONFIG_ESP_SYSTEM_IN_IRAM=y # CONFIG_ESP_SYSTEM_PANIC_PRINT_HALT is not set CONFIG_ESP_SYSTEM_PANIC_PRINT_REBOOT=y # CONFIG_ESP_SYSTEM_PANIC_SILENT_REBOOT is not set @@ -1588,13 +1582,28 @@ CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1=y # CONFIG_ESP_DEBUG_STUBS_ENABLE is not set CONFIG_ESP_DEBUG_OCDAWARE=y CONFIG_ESP_SYSTEM_CHECK_INT_LEVEL_4=y + +# +# Brownout Detector +# +CONFIG_ESP_BROWNOUT_DET=y +CONFIG_ESP_BROWNOUT_DET_LVL_SEL_7=y +# CONFIG_ESP_BROWNOUT_DET_LVL_SEL_6 is not set +# CONFIG_ESP_BROWNOUT_DET_LVL_SEL_5 is not set +# CONFIG_ESP_BROWNOUT_DET_LVL_SEL_4 is not set +# CONFIG_ESP_BROWNOUT_DET_LVL_SEL_3 is not set +# CONFIG_ESP_BROWNOUT_DET_LVL_SEL_2 is not set +# CONFIG_ESP_BROWNOUT_DET_LVL_SEL_1 is not set +CONFIG_ESP_BROWNOUT_DET_LVL=7 +# end of Brownout Detector + +CONFIG_ESP_SYSTEM_BROWNOUT_INTR=y CONFIG_ESP_SYSTEM_BBPLL_RECALIB=y # end of ESP System Settings # # IPC (Inter-Processor Call) # -CONFIG_ESP_IPC_ENABLE=y CONFIG_ESP_IPC_TASK_STACK_SIZE=1280 CONFIG_ESP_IPC_USES_CALLERS_PRIORITY=y CONFIG_ESP_IPC_ISR_ENABLE=y @@ -1603,7 +1612,6 @@ CONFIG_ESP_IPC_ISR_ENABLE=y # # ESP Timer (High Resolution Timer) # -CONFIG_ESP_TIMER_IN_IRAM=y # CONFIG_ESP_TIMER_PROFILING is not set CONFIG_ESP_TIME_FUNCS_USE_RTC_TIMER=y CONFIG_ESP_TIME_FUNCS_USE_ESP_TIMER=y @@ -1646,12 +1654,10 @@ CONFIG_ESP_WIFI_IRAM_OPT=y CONFIG_ESP_WIFI_RX_IRAM_OPT=y CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y CONFIG_ESP_WIFI_ENABLE_SAE_PK=y -CONFIG_ESP_WIFI_ENABLE_SAE_H2E=y CONFIG_ESP_WIFI_SOFTAP_SAE_SUPPORT=y CONFIG_ESP_WIFI_ENABLE_WPA3_OWE_STA=y # CONFIG_ESP_WIFI_SLP_IRAM_OPT is not set CONFIG_ESP_WIFI_SLP_DEFAULT_MIN_ACTIVE_TIME=50 -# CONFIG_ESP_WIFI_BSS_MAX_IDLE_SUPPORT is not set CONFIG_ESP_WIFI_SLP_DEFAULT_MAX_ACTIVE_TIME=10 CONFIG_ESP_WIFI_SLP_DEFAULT_WAIT_BROADCAST_DATA_TIME=15 # CONFIG_ESP_WIFI_FTM_ENABLE is not set @@ -1676,10 +1682,10 @@ CONFIG_ESP_WIFI_MBEDTLS_TLS_CLIENT=y # # CONFIG_ESP_WIFI_WPS_STRICT is not set # CONFIG_ESP_WIFI_WPS_PASSPHRASE is not set +# CONFIG_ESP_WIFI_WPS_RECONNECT_ON_FAIL is not set # end of WPS Configuration Options # CONFIG_ESP_WIFI_DEBUG_PRINT is not set -# CONFIG_ESP_WIFI_TESTING_OPTIONS is not set CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT=y # CONFIG_ESP_WIFI_ENT_FREE_DYNAMIC_BUFFER is not set # end of Wi-Fi @@ -1813,7 +1819,6 @@ CONFIG_FREERTOS_DEBUG_OCDAWARE=y CONFIG_FREERTOS_ENABLE_TASK_SNAPSHOT=y CONFIG_FREERTOS_PLACE_SNAPSHOT_FUNS_INTO_FLASH=y CONFIG_FREERTOS_NUMBER_OF_CORES=2 -CONFIG_FREERTOS_IN_IRAM=y # end of FreeRTOS # @@ -1825,6 +1830,8 @@ CONFIG_HAL_ASSERTION_EQUALS_SYSTEM=y # CONFIG_HAL_ASSERTION_ENABLE is not set CONFIG_HAL_DEFAULT_ASSERTION_LEVEL=2 CONFIG_HAL_WDT_USE_ROM_IMPL=y +CONFIG_HAL_SPI_MASTER_FUNC_IN_IRAM=y +CONFIG_HAL_SPI_SLAVE_FUNC_IN_IRAM=y # end of Hardware Abstraction Layer (HAL) and Low Level (LL) # @@ -1845,9 +1852,6 @@ CONFIG_HEAP_TRACING_OFF=y # # Log # -CONFIG_LOG_VERSION_1=y -# CONFIG_LOG_VERSION_2 is not set -CONFIG_LOG_VERSION=1 # # Log Level @@ -1885,15 +1889,6 @@ CONFIG_LOG_TAG_LEVEL_IMPL_CACHE_SIZE=31 CONFIG_LOG_TIMESTAMP_SOURCE_RTOS=y # CONFIG_LOG_TIMESTAMP_SOURCE_SYSTEM is not set # end of Format - -# -# Settings -# -CONFIG_LOG_MODE_TEXT_EN=y -CONFIG_LOG_MODE_TEXT=y -# end of Settings - -CONFIG_LOG_IN_IRAM=y # end of Log # @@ -1901,6 +1896,7 @@ CONFIG_LOG_IN_IRAM=y # CONFIG_LWIP_ENABLE=y CONFIG_LWIP_LOCAL_HOSTNAME="espressif" +# CONFIG_LWIP_NETIF_API is not set CONFIG_LWIP_TCPIP_TASK_PRIO=18 # CONFIG_LWIP_TCPIP_CORE_LOCKING is not set # CONFIG_LWIP_CHECK_THREAD_SAFETY is not set @@ -2012,6 +2008,7 @@ CONFIG_LWIP_IPV6_ND6_NUM_NEIGHBORS=5 CONFIG_LWIP_IPV6_ND6_NUM_PREFIXES=5 CONFIG_LWIP_IPV6_ND6_NUM_ROUTERS=3 CONFIG_LWIP_IPV6_ND6_NUM_DESTINATIONS=10 +# CONFIG_LWIP_IPV6_ND6_ROUTE_INFO_OPTION_SUPPORT is not set # CONFIG_LWIP_PPP_SUPPORT is not set # CONFIG_LWIP_SLIP_SUPPORT is not set @@ -2046,7 +2043,6 @@ CONFIG_LWIP_DNS_MAX_HOST_IP=1 CONFIG_LWIP_DNS_MAX_SERVERS=3 # CONFIG_LWIP_FALLBACK_DNS_SERVER_SUPPORT is not set # CONFIG_LWIP_DNS_SETSERVER_WITH_NETIF is not set -# CONFIG_LWIP_USE_ESP_GETADDRINFO is not set # end of DNS CONFIG_LWIP_BRIDGEIF_MAX_PORTS=7 @@ -2067,9 +2063,6 @@ CONFIG_LWIP_HOOK_ND6_GET_GW_NONE=y CONFIG_LWIP_HOOK_IP6_SELECT_SRC_ADDR_NONE=y # CONFIG_LWIP_HOOK_IP6_SELECT_SRC_ADDR_DEFAULT is not set # CONFIG_LWIP_HOOK_IP6_SELECT_SRC_ADDR_CUSTOM is not set -CONFIG_LWIP_HOOK_DHCP_EXTRA_OPTION_NONE=y -# CONFIG_LWIP_HOOK_DHCP_EXTRA_OPTION_DEFAULT is not set -# CONFIG_LWIP_HOOK_DHCP_EXTRA_OPTION_CUSTOM is not set CONFIG_LWIP_HOOK_NETCONN_EXT_RESOLVE_NONE=y # CONFIG_LWIP_HOOK_NETCONN_EXT_RESOLVE_DEFAULT is not set # CONFIG_LWIP_HOOK_NETCONN_EXT_RESOLVE_CUSTOM is not set @@ -2239,23 +2232,20 @@ CONFIG_MQTT_TRANSPORT_WEBSOCKET_SECURE=y # end of ESP-MQTT Configurations # -# LibC +# Newlib # -CONFIG_LIBC_NEWLIB=y -CONFIG_LIBC_MISC_IN_IRAM=y -CONFIG_LIBC_LOCKS_PLACE_IN_IRAM=y -CONFIG_LIBC_STDOUT_LINE_ENDING_CRLF=y -# CONFIG_LIBC_STDOUT_LINE_ENDING_LF is not set -# CONFIG_LIBC_STDOUT_LINE_ENDING_CR is not set -# CONFIG_LIBC_STDIN_LINE_ENDING_CRLF is not set -# CONFIG_LIBC_STDIN_LINE_ENDING_LF is not set -CONFIG_LIBC_STDIN_LINE_ENDING_CR=y -# CONFIG_LIBC_NEWLIB_NANO_FORMAT is not set -CONFIG_LIBC_TIME_SYSCALL_USE_RTC_HRT=y -# CONFIG_LIBC_TIME_SYSCALL_USE_RTC is not set -# CONFIG_LIBC_TIME_SYSCALL_USE_HRT is not set -# CONFIG_LIBC_TIME_SYSCALL_USE_NONE is not set -# end of LibC +CONFIG_NEWLIB_STDOUT_LINE_ENDING_CRLF=y +# CONFIG_NEWLIB_STDOUT_LINE_ENDING_LF is not set +# CONFIG_NEWLIB_STDOUT_LINE_ENDING_CR is not set +# CONFIG_NEWLIB_STDIN_LINE_ENDING_CRLF is not set +# CONFIG_NEWLIB_STDIN_LINE_ENDING_LF is not set +CONFIG_NEWLIB_STDIN_LINE_ENDING_CR=y +# CONFIG_NEWLIB_NANO_FORMAT is not set +CONFIG_NEWLIB_TIME_SYSCALL_USE_RTC_HRT=y +# CONFIG_NEWLIB_TIME_SYSCALL_USE_RTC is not set +# CONFIG_NEWLIB_TIME_SYSCALL_USE_HRT is not set +# CONFIG_NEWLIB_TIME_SYSCALL_USE_NONE is not set +# end of Newlib # # NVS @@ -2275,6 +2265,8 @@ CONFIG_LIBC_TIME_SYSCALL_USE_RTC_HRT=y # # CONFIG_OPENTHREAD_SPINEL_ONLY is not set # end of OpenThread Spinel + +# CONFIG_OPENTHREAD_DEBUG is not set # end of OpenThread # @@ -2335,7 +2327,6 @@ CONFIG_SPI_FLASH_HPM_DC_AUTO=y CONFIG_SPI_FLASH_SUSPEND_TSUS_VAL_US=50 # CONFIG_SPI_FLASH_FORCE_ENABLE_XMC_C_SUSPEND is not set # CONFIG_SPI_FLASH_FORCE_ENABLE_C6_H2_SUSPEND is not set -CONFIG_SPI_FLASH_PLACE_FUNCTIONS_IN_IRAM=y # end of Optional and Experimental Features (READ DOCS FIRST) # end of Main Flash configuration @@ -2361,13 +2352,13 @@ CONFIG_SPI_FLASH_WRITE_CHUNK_SIZE=8192 # # Auto-detect flash chips # -CONFIG_SPI_FLASH_VENDOR_XMC_SUPPORT_ENABLED=y -CONFIG_SPI_FLASH_VENDOR_GD_SUPPORT_ENABLED=y -CONFIG_SPI_FLASH_VENDOR_ISSI_SUPPORT_ENABLED=y -CONFIG_SPI_FLASH_VENDOR_MXIC_SUPPORT_ENABLED=y -CONFIG_SPI_FLASH_VENDOR_WINBOND_SUPPORT_ENABLED=y -CONFIG_SPI_FLASH_VENDOR_BOYA_SUPPORT_ENABLED=y -CONFIG_SPI_FLASH_VENDOR_TH_SUPPORT_ENABLED=y +CONFIG_SPI_FLASH_VENDOR_XMC_SUPPORTED=y +CONFIG_SPI_FLASH_VENDOR_GD_SUPPORTED=y +CONFIG_SPI_FLASH_VENDOR_ISSI_SUPPORTED=y +CONFIG_SPI_FLASH_VENDOR_MXIC_SUPPORTED=y +CONFIG_SPI_FLASH_VENDOR_WINBOND_SUPPORTED=y +CONFIG_SPI_FLASH_VENDOR_BOYA_SUPPORTED=y +CONFIG_SPI_FLASH_VENDOR_TH_SUPPORTED=y CONFIG_SPI_FLASH_SUPPORT_ISSI_CHIP=y CONFIG_SPI_FLASH_SUPPORT_MXIC_CHIP=y CONFIG_SPI_FLASH_SUPPORT_GD_CHIP=y @@ -2450,7 +2441,6 @@ CONFIG_UNITY_ENABLE_DOUBLE=y CONFIG_UNITY_ENABLE_IDF_TEST_RUNNER=y # CONFIG_UNITY_ENABLE_FIXTURE is not set # CONFIG_UNITY_ENABLE_BACKTRACE_ON_FAIL is not set -# CONFIG_UNITY_TEST_ORDER_BY_FILE_PATH_AND_LINE is not set # end of Unity unit testing library # @@ -2529,7 +2519,6 @@ CONFIG_WIFI_PROV_STA_ALL_CHANNEL_SCAN=y # Deprecated options for backward compatibility # CONFIG_APP_BUILD_TYPE_ELF_RAM is not set # CONFIG_NO_BLOBS is not set -# CONFIG_APP_ROLLBACK_ENABLE is not set # CONFIG_LOG_BOOTLOADER_LEVEL_NONE is not set # CONFIG_LOG_BOOTLOADER_LEVEL_ERROR is not set # CONFIG_LOG_BOOTLOADER_LEVEL_WARN is not set @@ -2537,6 +2526,7 @@ CONFIG_LOG_BOOTLOADER_LEVEL_INFO=y # CONFIG_LOG_BOOTLOADER_LEVEL_DEBUG is not set # CONFIG_LOG_BOOTLOADER_LEVEL_VERBOSE is not set CONFIG_LOG_BOOTLOADER_LEVEL=3 +# CONFIG_APP_ROLLBACK_ENABLE is not set # CONFIG_FLASH_ENCRYPTION_ENABLED is not set # CONFIG_FLASHMODE_QIO is not set # CONFIG_FLASHMODE_QOUT is not set @@ -2565,47 +2555,46 @@ CONFIG_ESP32_APPTRACE_LOCK_ENABLE=y CONFIG_NIMBLE_ENABLED=y CONFIG_NIMBLE_MEM_ALLOC_MODE_INTERNAL=y # CONFIG_NIMBLE_MEM_ALLOC_MODE_DEFAULT is not set -CONFIG_NIMBLE_MAX_CONNECTIONS=1 -CONFIG_NIMBLE_MAX_BONDS=3 -CONFIG_NIMBLE_MAX_CCCDS=8 -CONFIG_NIMBLE_L2CAP_COC_MAX_NUM=0 +CONFIG_NIMBLE_PINNED_TO_CORE=0 CONFIG_NIMBLE_PINNED_TO_CORE_0=y # CONFIG_NIMBLE_PINNED_TO_CORE_1 is not set -CONFIG_NIMBLE_PINNED_TO_CORE=0 CONFIG_NIMBLE_TASK_STACK_SIZE=4096 CONFIG_BT_NIMBLE_TASK_STACK_SIZE=4096 CONFIG_NIMBLE_ROLE_CENTRAL=y CONFIG_NIMBLE_ROLE_PERIPHERAL=y CONFIG_NIMBLE_ROLE_BROADCASTER=y CONFIG_NIMBLE_ROLE_OBSERVER=y -CONFIG_NIMBLE_NVS_PERSIST=y CONFIG_NIMBLE_SM_LEGACY=y CONFIG_NIMBLE_SM_SC=y # CONFIG_NIMBLE_SM_SC_DEBUG_KEYS is not set CONFIG_BT_NIMBLE_SM_SC_LVL=0 -# CONFIG_NIMBLE_DEBUG is not set -CONFIG_NIMBLE_SVC_GAP_DEVICE_NAME="nimble" -CONFIG_NIMBLE_GAP_DEVICE_NAME_MAX_LEN=31 +CONFIG_NIMBLE_NVS_PERSIST=y +CONFIG_NIMBLE_MAX_BONDS=3 +CONFIG_NIMBLE_RPA_TIMEOUT=900 +CONFIG_NIMBLE_MAX_CONNECTIONS=1 +CONFIG_NIMBLE_MAX_CCCDS=8 +CONFIG_NIMBLE_CRYPTO_STACK_MBEDTLS=y +# CONFIG_NIMBLE_HS_FLOW_CTRL is not set CONFIG_NIMBLE_ATT_PREFERRED_MTU=256 -CONFIG_NIMBLE_SVC_GAP_APPEARANCE=0 +CONFIG_NIMBLE_L2CAP_COC_MAX_NUM=0 CONFIG_BT_NIMBLE_MSYS1_BLOCK_COUNT=12 CONFIG_BT_NIMBLE_ACL_BUF_COUNT=24 CONFIG_BT_NIMBLE_ACL_BUF_SIZE=255 CONFIG_BT_NIMBLE_HCI_EVT_BUF_SIZE=70 CONFIG_BT_NIMBLE_HCI_EVT_HI_BUF_COUNT=30 CONFIG_BT_NIMBLE_HCI_EVT_LO_BUF_COUNT=8 -# CONFIG_NIMBLE_HS_FLOW_CTRL is not set -CONFIG_NIMBLE_RPA_TIMEOUT=900 +CONFIG_NIMBLE_SVC_GAP_DEVICE_NAME="nimble" +CONFIG_NIMBLE_GAP_DEVICE_NAME_MAX_LEN=31 +CONFIG_NIMBLE_SVC_GAP_APPEARANCE=0 # CONFIG_NIMBLE_MESH is not set -CONFIG_NIMBLE_CRYPTO_STACK_MBEDTLS=y +# CONFIG_NIMBLE_DEBUG is not set # CONFIG_BT_NIMBLE_COEX_PHY_CODED_TX_RX_TLIM_EN is not set CONFIG_BT_NIMBLE_COEX_PHY_CODED_TX_RX_TLIM_DIS=y CONFIG_SW_COEXIST_ENABLE=y CONFIG_ESP32_WIFI_SW_COEXIST_ENABLE=y CONFIG_ESP_WIFI_SW_COEXIST_ENABLE=y # CONFIG_CAM_CTLR_DVP_CAM_ISR_IRAM_SAFE is not set -# CONFIG_GPTIMER_ISR_IRAM_SAFE is not set -# CONFIG_MCPWM_ISR_IRAM_SAFE is not set +# CONFIG_MCPWM_ISR_IN_IRAM is not set # CONFIG_EVENT_LOOP_PROFILING is not set CONFIG_POST_EVENTS_FROM_ISR=y CONFIG_POST_EVENTS_FROM_IRAM_ISR=y @@ -2620,26 +2609,6 @@ CONFIG_ESP32S3_RTC_CLK_SRC_INT_RC=y # CONFIG_ESP32S3_RTC_CLK_SRC_EXT_OSC is not set # CONFIG_ESP32S3_RTC_CLK_SRC_INT_8MD256 is not set CONFIG_ESP32S3_RTC_CLK_CAL_CYCLES=1024 -CONFIG_PERIPH_CTRL_FUNC_IN_IRAM=y -CONFIG_BROWNOUT_DET=y -CONFIG_ESP32S3_BROWNOUT_DET=y -CONFIG_BROWNOUT_DET_LVL_SEL_7=y -CONFIG_ESP32S3_BROWNOUT_DET_LVL_SEL_7=y -# CONFIG_BROWNOUT_DET_LVL_SEL_6 is not set -# CONFIG_ESP32S3_BROWNOUT_DET_LVL_SEL_6 is not set -# CONFIG_BROWNOUT_DET_LVL_SEL_5 is not set -# CONFIG_ESP32S3_BROWNOUT_DET_LVL_SEL_5 is not set -# CONFIG_BROWNOUT_DET_LVL_SEL_4 is not set -# CONFIG_ESP32S3_BROWNOUT_DET_LVL_SEL_4 is not set -# CONFIG_BROWNOUT_DET_LVL_SEL_3 is not set -# CONFIG_ESP32S3_BROWNOUT_DET_LVL_SEL_3 is not set -# CONFIG_BROWNOUT_DET_LVL_SEL_2 is not set -# CONFIG_ESP32S3_BROWNOUT_DET_LVL_SEL_2 is not set -# CONFIG_BROWNOUT_DET_LVL_SEL_1 is not set -# CONFIG_ESP32S3_BROWNOUT_DET_LVL_SEL_1 is not set -CONFIG_BROWNOUT_DET_LVL=7 -CONFIG_ESP32S3_BROWNOUT_DET_LVL=7 -CONFIG_ESP_SYSTEM_BROWNOUT_INTR=y CONFIG_ESP32_PHY_CALIBRATION_AND_DATA_STORAGE=y # CONFIG_ESP32_PHY_INIT_DATA_IN_PARTITION is not set CONFIG_ESP32_PHY_MAX_WIFI_TX_POWER=20 @@ -2674,6 +2643,24 @@ CONFIG_TASK_WDT_CHECK_IDLE_TASK_CPU0=y CONFIG_TASK_WDT_CHECK_IDLE_TASK_CPU1=y # CONFIG_ESP32_DEBUG_STUBS_ENABLE is not set CONFIG_ESP32S3_DEBUG_OCDAWARE=y +CONFIG_BROWNOUT_DET=y +CONFIG_ESP32S3_BROWNOUT_DET=y +CONFIG_BROWNOUT_DET_LVL_SEL_7=y +CONFIG_ESP32S3_BROWNOUT_DET_LVL_SEL_7=y +# CONFIG_BROWNOUT_DET_LVL_SEL_6 is not set +# CONFIG_ESP32S3_BROWNOUT_DET_LVL_SEL_6 is not set +# CONFIG_BROWNOUT_DET_LVL_SEL_5 is not set +# CONFIG_ESP32S3_BROWNOUT_DET_LVL_SEL_5 is not set +# CONFIG_BROWNOUT_DET_LVL_SEL_4 is not set +# CONFIG_ESP32S3_BROWNOUT_DET_LVL_SEL_4 is not set +# CONFIG_BROWNOUT_DET_LVL_SEL_3 is not set +# CONFIG_ESP32S3_BROWNOUT_DET_LVL_SEL_3 is not set +# CONFIG_BROWNOUT_DET_LVL_SEL_2 is not set +# CONFIG_ESP32S3_BROWNOUT_DET_LVL_SEL_2 is not set +# CONFIG_BROWNOUT_DET_LVL_SEL_1 is not set +# CONFIG_ESP32S3_BROWNOUT_DET_LVL_SEL_1 is not set +CONFIG_BROWNOUT_DET_LVL=7 +CONFIG_ESP32S3_BROWNOUT_DET_LVL=7 CONFIG_IPC_TASK_STACK_SIZE=1280 CONFIG_TIMER_TASK_STACK_SIZE=3584 CONFIG_ESP32_WIFI_ENABLED=y @@ -2708,7 +2695,6 @@ CONFIG_WPA_MBEDTLS_TLS_CLIENT=y # CONFIG_WPA_WPS_SOFTAP_REGISTRAR is not set # CONFIG_WPA_WPS_STRICT is not set # CONFIG_WPA_DEBUG_PRINT is not set -# CONFIG_WPA_TESTING_OPTIONS is not set # CONFIG_ESP32_ENABLE_COREDUMP_TO_FLASH is not set # CONFIG_ESP32_ENABLE_COREDUMP_TO_UART is not set CONFIG_ESP32_ENABLE_COREDUMP_TO_NONE=y @@ -2739,22 +2725,11 @@ CONFIG_TCPIP_TASK_AFFINITY_NO_AFFINITY=y # CONFIG_TCPIP_TASK_AFFINITY_CPU1 is not set CONFIG_TCPIP_TASK_AFFINITY=0x7FFFFFFF # CONFIG_PPP_SUPPORT is not set -CONFIG_NEWLIB_STDOUT_LINE_ENDING_CRLF=y -# CONFIG_NEWLIB_STDOUT_LINE_ENDING_LF is not set -# CONFIG_NEWLIB_STDOUT_LINE_ENDING_CR is not set -# CONFIG_NEWLIB_STDIN_LINE_ENDING_CRLF is not set -# CONFIG_NEWLIB_STDIN_LINE_ENDING_LF is not set -CONFIG_NEWLIB_STDIN_LINE_ENDING_CR=y -# CONFIG_NEWLIB_NANO_FORMAT is not set -CONFIG_NEWLIB_TIME_SYSCALL_USE_RTC_HRT=y CONFIG_ESP32S3_TIME_SYSCALL_USE_RTC_SYSTIMER=y CONFIG_ESP32S3_TIME_SYSCALL_USE_RTC_FRC1=y -# CONFIG_NEWLIB_TIME_SYSCALL_USE_RTC is not set # CONFIG_ESP32S3_TIME_SYSCALL_USE_RTC is not set -# CONFIG_NEWLIB_TIME_SYSCALL_USE_HRT is not set # CONFIG_ESP32S3_TIME_SYSCALL_USE_SYSTIMER is not set # CONFIG_ESP32S3_TIME_SYSCALL_USE_FRC1 is not set -# CONFIG_NEWLIB_TIME_SYSCALL_USE_NONE is not set # CONFIG_ESP32S3_TIME_SYSCALL_USE_NONE is not set CONFIG_ESP32_PTHREAD_TASK_PRIO_DEFAULT=5 CONFIG_ESP32_PTHREAD_TASK_STACK_SIZE_DEFAULT=3072