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