working up to it

This commit is contained in:
pszsh 2026-01-31 22:41:07 -08:00
parent 6adf55bc47
commit 3e78b4eb75
9 changed files with 1470 additions and 1085 deletions

View File

@ -1,3 +1,4 @@
# File: host/CMakeLists.txt
cmake_minimum_required(VERSION 3.18) cmake_minimum_required(VERSION 3.18)
project(EISConfigurator VERSION 1.0 LANGUAGES CXX C) project(EISConfigurator VERSION 1.0 LANGUAGES CXX C)
@ -8,11 +9,94 @@ set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON) set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON) set(CMAKE_AUTOUIC ON)
# Added PrintSupport and OpenGLWidgets which are often needed by QCustomPlot # --- FOR WINDOWS MSVC ERRORS ---
if(MSVC)
add_compile_options($<$<COMPILE_LANGUAGE:C>:/std:clatest>)
add_compile_definitions(_CRT_SECURE_NO_WARNINGS)
endif()
include(FetchContent)
option(BUILD_ANDROID "Build for Android" OFF)
option(BUILD_IOS "Build for iOS" OFF)
# --- Dependencies ---
# Added SerialPort and PrintSupport for EISConfigurator
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets SerialPort PrintSupport OpenGLWidgets) find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets SerialPort PrintSupport OpenGLWidgets)
# --- FFTW3 Configuration (Double Precision) ---
if(WIN32)
# Windows: Expects FFTW3 to be installed/found via Config or PkgConfig
# If not found, you might need to adjust paths or use the source build block below for Windows too
find_package(FFTW3 CONFIG QUIET)
if(TARGET FFTW3::fftw3)
add_library(fftw3 ALIAS FFTW3::fftw3)
else()
# Fallback to building from source on Windows if not found
message(STATUS "FFTW3 not found via Config. Building from source...")
set(BUILD_FFTW_FROM_SOURCE ON)
endif()
elseif(APPLE AND CMAKE_SYSTEM_PROCESSOR STREQUAL "arm64" AND NOT BUILD_IOS)
message(STATUS "Detected Apple Silicon Desktop. Using Homebrew FFTW3 (Double).")
find_library(FFTW3_LIB NAMES fftw3 libfftw3 PATHS /opt/homebrew/lib NO_DEFAULT_PATH)
find_path(FFTW3_INCLUDE_DIR fftw3.h PATHS /opt/homebrew/include NO_DEFAULT_PATH)
if(NOT FFTW3_LIB OR NOT FFTW3_INCLUDE_DIR)
message(FATAL_ERROR "FFTW3 not found in /opt/homebrew. Please run: brew install fftw")
endif()
add_library(fftw3 STATIC IMPORTED)
set_target_properties(fftw3 PROPERTIES
IMPORTED_LOCATION "${FFTW3_LIB}"
INTERFACE_INCLUDE_DIRECTORIES "${FFTW3_INCLUDE_DIR}"
)
else()
set(BUILD_FFTW_FROM_SOURCE ON)
endif()
if(BUILD_FFTW_FROM_SOURCE)
message(STATUS "Building FFTW3 from source (Double Precision)...")
set(ENABLE_FLOAT OFF CACHE BOOL "Build double precision" FORCE)
set(ENABLE_SSE OFF CACHE BOOL "Disable SSE" FORCE)
set(ENABLE_SSE2 OFF CACHE BOOL "Disable SSE2" FORCE)
set(ENABLE_AVX OFF CACHE BOOL "Disable AVX" FORCE)
set(ENABLE_AVX2 OFF CACHE BOOL "Disable AVX2" FORCE)
set(ENABLE_THREADS OFF CACHE BOOL "Disable Threads" FORCE)
set(ENABLE_OPENMP OFF CACHE BOOL "Disable OpenMP" FORCE)
set(ENABLE_MPI OFF CACHE BOOL "Disable MPI" FORCE)
set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build Static Libs" FORCE)
set(BUILD_TESTS OFF CACHE BOOL "Disable Tests" FORCE)
# Enhanced NEON detection
if(ANDROID_ABI STREQUAL "arm64-v8a")
message(STATUS "Enabling NEON for Android ARM64")
set(ENABLE_NEON ON CACHE BOOL "Enable NEON" FORCE)
elseif(BUILD_IOS)
set(ENABLE_NEON ON CACHE BOOL "Enable NEON" FORCE)
elseif(MSVC AND CMAKE_SYSTEM_PROCESSOR MATCHES "(ARM64|arm64|aarch64)")
set(ENABLE_NEON ON CACHE BOOL "Enable NEON" FORCE)
endif()
# Only apply sed patch on UNIX-like systems to fix CMake version requirement in FFTW source
if(UNIX)
set(PATCH_CMD sed -i.bak "s/cmake_minimum_required.*/cmake_minimum_required(VERSION 3.16)/" <SOURCE_DIR>/CMakeLists.txt)
else()
set(PATCH_CMD "")
endif()
FetchContent_Declare(
fftw3_source
URL https://www.fftw.org/fftw-3.3.10.tar.gz
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
PATCH_COMMAND ${PATCH_CMD}
)
FetchContent_MakeAvailable(fftw3_source)
endif()
# --- QCustomPlot Integration --- # --- QCustomPlot Integration ---
include(FetchContent)
FetchContent_Declare( FetchContent_Declare(
QCustomPlot QCustomPlot
@ -23,12 +107,10 @@ FetchContent_Declare(
FetchContent_GetProperties(QCustomPlot) FetchContent_GetProperties(QCustomPlot)
if(NOT qcustomplot_POPULATED) if(NOT qcustomplot_POPULATED)
FetchContent_Populate(QCustomPlot) FetchContent_Populate(QCustomPlot)
# QCustomPlot source distribution doesn't have a CMakeLists.txt
add_library(QCustomPlot add_library(QCustomPlot
${qcustomplot_SOURCE_DIR}/qcustomplot.cpp ${qcustomplot_SOURCE_DIR}/qcustomplot.cpp
${qcustomplot_SOURCE_DIR}/qcustomplot.h ${qcustomplot_SOURCE_DIR}/qcustomplot.h
) )
# Link required Qt modules to the library
target_link_libraries(QCustomPlot PUBLIC Qt6::Core Qt6::Gui Qt6::Widgets Qt6::PrintSupport) target_link_libraries(QCustomPlot PUBLIC Qt6::Core Qt6::Gui Qt6::Widgets Qt6::PrintSupport)
target_include_directories(QCustomPlot PUBLIC ${qcustomplot_SOURCE_DIR}) target_include_directories(QCustomPlot PUBLIC ${qcustomplot_SOURCE_DIR})
endif() endif()
@ -36,29 +118,64 @@ endif()
# --- Icon Generation --- # --- Icon Generation ---
set(ICON_SOURCE "${CMAKE_CURRENT_SOURCE_DIR}/assets/icon_source.png") set(ICON_SOURCE "${CMAKE_CURRENT_SOURCE_DIR}/assets/icon_source.png")
set(ICON_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_icons.sh")
set(MACOS_ICON "${CMAKE_CURRENT_SOURCE_DIR}/assets/icons/app_icon.icns")
set(WINDOWS_ICON "${CMAKE_CURRENT_SOURCE_DIR}/assets/icons/app_icon.ico")
# Find ImageMagick 'magick' executable
find_program(MAGICK_EXECUTABLE NAMES magick) find_program(MAGICK_EXECUTABLE NAMES magick)
if(NOT MAGICK_EXECUTABLE) if(NOT MAGICK_EXECUTABLE)
message(WARNING "ImageMagick 'magick' not found. Icons will not be generated.") message(WARNING "ImageMagick 'magick' not found. Icons will not be generated.")
endif() endif()
if(EXISTS "${ICON_SOURCE}" AND MAGICK_EXECUTABLE) if(EXISTS "${ICON_SOURCE}" AND MAGICK_EXECUTABLE)
execute_process(COMMAND chmod +x "${ICON_SCRIPT}")
add_custom_command(
OUTPUT "${MACOS_ICON}" "${WINDOWS_ICON}"
COMMAND "${ICON_SCRIPT}" "${MAGICK_EXECUTABLE}"
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
DEPENDS "${ICON_SOURCE}" "${ICON_SCRIPT}"
COMMENT "Generating icons from source using ${MAGICK_EXECUTABLE}..."
VERBATIM
)
add_custom_target(GenerateIcons DEPENDS "${MACOS_ICON}" "${WINDOWS_ICON}") if(WIN32)
# --- WINDOWS SPECIFIC ---
set(WINDOWS_ICON "${CMAKE_CURRENT_BINARY_DIR}/app_icon.ico")
set(WINDOWS_RC "${CMAKE_CURRENT_BINARY_DIR}/app_icon.rc")
# Assuming you have a batch script or use the shell script via git bash
set(ICON_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_icons.sh")
# Create .rc file
file(WRITE "${WINDOWS_RC}" "IDI_ICON1 ICON \"app_icon.ico\"\n")
# Generate ICO (Requires bash on Windows or a separate .bat script)
# For simplicity, we assume a unix-like environment or WSL for generation
add_custom_command(
OUTPUT "${WINDOWS_ICON}"
COMMAND "${ICON_SCRIPT}" "${MAGICK_EXECUTABLE}"
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
DEPENDS "${ICON_SOURCE}" "${ICON_SCRIPT}"
COMMENT "Generating Icons..."
VERBATIM
)
# Copy generated ico to binary dir for RC compiler
add_custom_command(
OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/app_icon.ico"
COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/assets/icons/app_icon.ico" "${WINDOWS_ICON}"
DEPENDS "${WINDOWS_ICON}"
)
add_custom_target(GenerateIcons DEPENDS "${WINDOWS_ICON}")
else()
# --- MAC/LINUX/ANDROID/IOS SPECIFIC ---
set(MACOS_ICON "${CMAKE_CURRENT_SOURCE_DIR}/assets/icons/app_icon.icns")
set(IOS_ASSETS_PATH "${CMAKE_CURRENT_SOURCE_DIR}/ios/Assets.xcassets")
set(ANDROID_RES_PATH "${CMAKE_CURRENT_SOURCE_DIR}/android/res/mipmap-mdpi/ic_launcher.png")
set(ICON_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_icons.sh")
execute_process(COMMAND chmod +x "${ICON_SCRIPT}")
add_custom_command(
OUTPUT "${MACOS_ICON}" "${ANDROID_RES_PATH}"
COMMAND "${ICON_SCRIPT}" "${MAGICK_EXECUTABLE}"
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
DEPENDS "${ICON_SOURCE}" "${ICON_SCRIPT}"
COMMENT "Generating Cross-Platform Icons..."
VERBATIM
)
add_custom_target(GenerateIcons DEPENDS "${MACOS_ICON}" "${ANDROID_RES_PATH}")
endif()
endif() endif()
# --- Sources --- # --- Sources ---
@ -66,11 +183,18 @@ endif()
set(PROJECT_SOURCES set(PROJECT_SOURCES
src/main.cpp src/main.cpp
src/MainWindow.cpp src/MainWindow.cpp
src/MainWindow_UI.cpp
src/MainWindow_Serial.cpp
src/MainWindow_Actions.cpp
src/GraphWidget.cpp src/GraphWidget.cpp
) )
if(EXISTS "${ICON_SOURCE}") if(EXISTS "${ICON_SOURCE}")
list(APPEND PROJECT_SOURCES ${MACOS_ICON} ${WINDOWS_ICON}) if(WIN32)
list(APPEND PROJECT_SOURCES ${WINDOWS_RC} ${WINDOWS_ICON})
elseif(APPLE AND NOT BUILD_IOS)
list(APPEND PROJECT_SOURCES ${MACOS_ICON})
endif()
endif() endif()
set(PROJECT_HEADERS set(PROJECT_HEADERS
@ -81,7 +205,6 @@ set(PROJECT_HEADERS
if(ANDROID) if(ANDROID)
add_library(EISConfigurator SHARED ${PROJECT_SOURCES} ${PROJECT_HEADERS}) add_library(EISConfigurator SHARED ${PROJECT_SOURCES} ${PROJECT_HEADERS})
else() else()
# Removed MANUAL_FINALIZATION keyword as add_executable does not support it
add_executable(EISConfigurator ${PROJECT_SOURCES} ${PROJECT_HEADERS}) add_executable(EISConfigurator ${PROJECT_SOURCES} ${PROJECT_HEADERS})
endif() endif()
@ -89,16 +212,52 @@ if(EXISTS "${ICON_SOURCE}" AND MAGICK_EXECUTABLE)
add_dependencies(EISConfigurator GenerateIcons) add_dependencies(EISConfigurator GenerateIcons)
endif() endif()
# --- Linking ---
if(TARGET fftw3)
set(FFTW_TARGET fftw3)
if(DEFINED fftw3_source_SOURCE_DIR)
target_include_directories(EISConfigurator PRIVATE
"${fftw3_source_SOURCE_DIR}/api"
"${fftw3_source_BINARY_DIR}"
)
endif()
else()
set(FFTW_TARGET fftw3)
endif()
target_link_libraries(EISConfigurator PRIVATE target_link_libraries(EISConfigurator PRIVATE
Qt6::Core Qt6::Gui Qt6::Widgets Qt6::SerialPort Qt6::Core Qt6::Gui Qt6::Widgets Qt6::SerialPort Qt6::PrintSupport
QCustomPlot QCustomPlot
${FFTW_TARGET}
) )
if(ANDROID) if(BUILD_ANDROID)
target_link_libraries(EISConfigurator PRIVATE log m)
set_target_properties(EISConfigurator PROPERTIES QT_ANDROID_PACKAGE_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/android) set_target_properties(EISConfigurator PROPERTIES QT_ANDROID_PACKAGE_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/android)
endif() endif()
if(APPLE) if(BUILD_IOS)
if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/ios/Info.plist")
message(FATAL_ERROR "Missing ios/Info.plist. Please create it before building.")
endif()
set_target_properties(EISConfigurator PROPERTIES
MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_BUNDLE_NAME "EIS Configurator"
MACOSX_BUNDLE_GUI_IDENTIFIER "com.eis.configurator"
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/ios/Info.plist"
XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME "AppIcon"
RESOURCE "${IOS_ASSETS_PATH}"
)
if(EXISTS "${ICON_SOURCE}")
file(MAKE_DIRECTORY "${IOS_ASSETS_PATH}")
target_sources(EISConfigurator PRIVATE "${IOS_ASSETS_PATH}")
endif()
endif()
if(APPLE AND NOT BUILD_IOS)
set_target_properties(EISConfigurator PROPERTIES set_target_properties(EISConfigurator PROPERTIES
MACOSX_BUNDLE TRUE MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_BUNDLE_NAME "EIS Configurator" MACOSX_BUNDLE_BUNDLE_NAME "EIS Configurator"
@ -110,11 +269,6 @@ elseif(WIN32)
set_target_properties(EISConfigurator PROPERTIES set_target_properties(EISConfigurator PROPERTIES
WIN32_EXECUTABLE TRUE WIN32_EXECUTABLE TRUE
) )
if(EXISTS "${WINDOWS_ICON}")
set_target_properties(EISConfigurator PROPERTIES
WIN32_ICON "${WINDOWS_ICON}"
)
endif()
endif() endif()
if(NOT ANDROID) if(NOT ANDROID)

View File

@ -1,14 +1,61 @@
// host/src/GraphWidget.cpp // File: host/src/GraphWidget.cpp
#include "GraphWidget.h" #include "GraphWidget.h"
#include <limits> #include <limits>
#include <cmath> #include <cmath>
#include <QMenu>
#include <QInputDialog>
#include <QFormLayout>
#include <QDialogButtonBox>
// Simple Linear Regression Helper
static void linearRegression(const QVector<double>& x, const QVector<double>& y, double &m, double &c) {
double sumX=0, sumY=0, sumXY=0, sumX2=0;
int n = x.size();
for(int i=0; i<n; i++) {
sumX += x[i];
sumY += y[i];
sumXY += x[i]*y[i];
sumX2 += x[i]*x[i];
}
m = (n*sumXY - sumX*sumY) / (n*sumX2 - sumX*sumX);
c = (sumY - m*sumX) / n;
}
GraphWidget::GraphWidget(QWidget *parent) : QWidget(parent) { GraphWidget::GraphWidget(QWidget *parent) : QWidget(parent) {
layout = new QVBoxLayout(this); mainLayout = new QVBoxLayout(this);
plot = new QCustomPlot(this); mainLayout->setContentsMargins(0,0,0,0);
mainLayout->setSpacing(0);
// Toolbar
toolbar = new QWidget(this);
toolbar->setStyleSheet("background-color: #2D2D2D;");
QHBoxLayout *toolLayout = new QHBoxLayout(toolbar);
toolLayout->setContentsMargins(5, 2, 5, 2);
layout->addWidget(plot); btnScaleX = new QPushButton("Scale X", this);
layout->setContentsMargins(0, 0, 0, 0); btnScaleY = new QPushButton("Scale Y", this);
btnScaleBoth = new QPushButton("Scale Both", this);
btnCenter = new QPushButton("Center", this);
btnAnalyze = new QPushButton("Analyze", this);
QString btnStyle = "QPushButton { background-color: #444; color: white; border: 1px solid #555; padding: 3px 8px; border-radius: 3px; } QPushButton:hover { background-color: #555; }";
btnScaleX->setStyleSheet(btnStyle);
btnScaleY->setStyleSheet(btnStyle);
btnScaleBoth->setStyleSheet(btnStyle);
btnCenter->setStyleSheet(btnStyle);
btnAnalyze->setStyleSheet(btnStyle);
toolLayout->addWidget(btnScaleX);
toolLayout->addWidget(btnScaleY);
toolLayout->addWidget(btnScaleBoth);
toolLayout->addWidget(btnCenter);
toolLayout->addStretch();
toolLayout->addWidget(btnAnalyze);
mainLayout->addWidget(toolbar);
plot = new QCustomPlot(this);
mainLayout->addWidget(plot);
plot->setBackground(QBrush(QColor(25, 25, 25))); plot->setBackground(QBrush(QColor(25, 25, 25)));
@ -28,78 +75,54 @@ GraphWidget::GraphWidget(QWidget *parent) : QWidget(parent) {
styleAxis(plot->yAxis2); styleAxis(plot->yAxis2);
// --- Setup Graphs --- // --- Setup Graphs ---
// 1. Real / Raw Nyquist (Cyan)
graphReal = plot->addGraph(); graphReal = plot->addGraph();
QPen pen1(QColor(0, 255, 255)); graphReal->setPen(QPen(QColor(0, 255, 255), 2));
pen1.setWidth(2);
graphReal->setPen(pen1);
graphReal->setLineStyle(QCPGraph::lsLine); graphReal->setLineStyle(QCPGraph::lsLine);
graphReal->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCircle, QColor(0, 255, 255), 3)); graphReal->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCircle, QColor(0, 255, 255), 3));
// 2. Imaginary (Magenta)
graphImag = plot->addGraph(plot->xAxis, plot->yAxis2); graphImag = plot->addGraph(plot->xAxis, plot->yAxis2);
QPen pen2(QColor(255, 0, 255)); graphImag->setPen(QPen(QColor(255, 0, 255), 2));
pen2.setWidth(2);
graphImag->setPen(pen2);
graphImag->setLineStyle(QCPGraph::lsLine); graphImag->setLineStyle(QCPGraph::lsLine);
graphImag->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssTriangle, QColor(255, 0, 255), 3)); graphImag->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssTriangle, QColor(255, 0, 255), 3));
// 3. Hilbert (Green Dashed)
graphHilbert = plot->addGraph(plot->xAxis, plot->yAxis2); graphHilbert = plot->addGraph(plot->xAxis, plot->yAxis2);
QPen pen3(Qt::green); QPen pen3(Qt::green); pen3.setWidth(2); pen3.setStyle(Qt::DashLine);
pen3.setWidth(2);
pen3.setStyle(Qt::DashLine);
graphHilbert->setPen(pen3); graphHilbert->setPen(pen3);
// 4. Corrected Nyquist (Orange)
graphNyquistCorr = plot->addGraph(plot->xAxis, plot->yAxis); graphNyquistCorr = plot->addGraph(plot->xAxis, plot->yAxis);
QPen pen4(QColor(255, 165, 0)); graphNyquistCorr->setPen(QPen(QColor(255, 165, 0), 2));
pen4.setWidth(2);
graphNyquistCorr->setPen(pen4);
graphNyquistCorr->setLineStyle(QCPGraph::lsLine); graphNyquistCorr->setLineStyle(QCPGraph::lsLine);
graphNyquistCorr->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCross, 4)); graphNyquistCorr->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCross, 4));
graphNyquistCorr->setName("De-embedded (True Cell)"); graphNyquistCorr->setName("De-embedded (True Cell)");
// 5. Amperometric Graph (Lime)
graphAmp = plot->addGraph(); graphAmp = plot->addGraph();
QPen pen5(QColor(50, 255, 50)); graphAmp->setPen(QPen(QColor(50, 255, 50), 2));
pen5.setWidth(2);
graphAmp->setPen(pen5);
graphAmp->setLineStyle(QCPGraph::lsLine); graphAmp->setLineStyle(QCPGraph::lsLine);
graphAmp->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDisc, 3)); graphAmp->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDisc, 3));
graphAmp->setName("Current"); graphAmp->setName("Current");
// 6. Extrapolated Point (Gold Star)
graphExtrapolated = plot->addGraph(plot->xAxis, plot->yAxis); graphExtrapolated = plot->addGraph(plot->xAxis, plot->yAxis);
graphExtrapolated->setLineStyle(QCPGraph::lsNone); graphExtrapolated->setLineStyle(QCPGraph::lsNone);
graphExtrapolated->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssStar, QColor(255, 215, 0), 12)); graphExtrapolated->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssStar, QColor(255, 215, 0), 12));
QPen pen6(QColor(255, 215, 0)); graphExtrapolated->setPen(QPen(QColor(255, 215, 0), 3));
pen6.setWidth(3);
graphExtrapolated->setPen(pen6);
graphExtrapolated->setName("Rs (Extrapolated)"); graphExtrapolated->setName("Rs (Extrapolated)");
// 7. LSV Blank (Gray)
graphLSVBlank = plot->addGraph(); graphLSVBlank = plot->addGraph();
QPen penBlank(QColor(150, 150, 150)); QPen penBlank(QColor(150, 150, 150)); penBlank.setWidth(2); penBlank.setStyle(Qt::DashLine);
penBlank.setWidth(2);
penBlank.setStyle(Qt::DashLine);
graphLSVBlank->setPen(penBlank); graphLSVBlank->setPen(penBlank);
graphLSVBlank->setName("Blank (Tap Water)"); graphLSVBlank->setName("Blank (Tap Water)");
// 8. LSV Sample (Yellow)
graphLSVSample = plot->addGraph(); graphLSVSample = plot->addGraph();
QPen penSample(Qt::yellow); graphLSVSample->setPen(QPen(Qt::yellow, 2));
penSample.setWidth(2);
graphLSVSample->setPen(penSample);
graphLSVSample->setName("Sample (Bleach)"); graphLSVSample->setName("Sample (Bleach)");
// 9. LSV Diff (Cyan)
graphLSVDiff = plot->addGraph(); graphLSVDiff = plot->addGraph();
QPen penDiff(Qt::cyan); graphLSVDiff->setPen(QPen(Qt::cyan, 3));
penDiff.setWidth(3);
graphLSVDiff->setPen(penDiff);
graphLSVDiff->setName("Diff (Chlorine)"); graphLSVDiff->setName("Diff (Chlorine)");
graphFit = plot->addGraph();
graphFit->setPen(QPen(Qt::red, 2));
graphFit->setName("Fit");
graphNyquistRaw = graphReal; graphNyquistRaw = graphReal;
@ -109,7 +132,11 @@ GraphWidget::GraphWidget(QWidget *parent) : QWidget(parent) {
connect(plot->yAxis, SIGNAL(rangeChanged(QCPRange)), plot->yAxis2, SLOT(setRange(QCPRange))); connect(plot->yAxis, SIGNAL(rangeChanged(QCPRange)), plot->yAxis2, SLOT(setRange(QCPRange)));
connect(plot->yAxis2, SIGNAL(rangeChanged(QCPRange)), plot->yAxis, SLOT(setRange(QCPRange))); connect(plot->yAxis2, SIGNAL(rangeChanged(QCPRange)), plot->yAxis, SLOT(setRange(QCPRange)));
plot->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom); plot->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom | QCP::iSelectPlottables);
// Right click drag to zoom
plot->axisRect()->setRangeDrag(Qt::Horizontal | Qt::Vertical);
plot->axisRect()->setRangeZoom(Qt::Horizontal | Qt::Vertical);
plot->legend->setVisible(true); plot->legend->setVisible(true);
QFont legendFont = font(); QFont legendFont = font();
@ -119,11 +146,142 @@ GraphWidget::GraphWidget(QWidget *parent) : QWidget(parent) {
plot->legend->setBorderPen(QPen(Qt::white)); plot->legend->setBorderPen(QPen(Qt::white));
plot->legend->setTextColor(Qt::white); plot->legend->setTextColor(Qt::white);
selectionRect = new QCPItemRect(plot);
selectionRect->setPen(QPen(Qt::yellow, 1, Qt::DashLine));
selectionRect->setBrush(QBrush(QColor(255, 255, 0, 50)));
selectionRect->setVisible(false);
connect(btnScaleX, &QPushButton::clicked, this, &GraphWidget::scaleX);
connect(btnScaleY, &QPushButton::clicked, this, &GraphWidget::scaleY);
connect(btnScaleBoth, &QPushButton::clicked, this, &GraphWidget::scaleBoth);
connect(btnCenter, &QPushButton::clicked, this, &GraphWidget::centerView);
connect(btnAnalyze, &QPushButton::clicked, this, &GraphWidget::startAnalyze);
// Custom mouse handling for selection
connect(plot, &QCustomPlot::mousePress, [this](QMouseEvent *event){
if(isSelecting && event->button() == Qt::LeftButton) {
selStartX = plot->xAxis->pixelToCoord(event->pos().x());
selStartY = plot->yAxis->pixelToCoord(event->pos().y());
selectionRect->topLeft->setCoords(selStartX, selStartY);
selectionRect->bottomRight->setCoords(selStartX, selStartY);
selectionRect->setVisible(true);
plot->replot();
}
});
connect(plot, &QCustomPlot::mouseMove, [this](QMouseEvent *event){
if(isSelecting && (event->buttons() & Qt::LeftButton)) {
double x = plot->xAxis->pixelToCoord(event->pos().x());
double y = plot->yAxis->pixelToCoord(event->pos().y());
selectionRect->bottomRight->setCoords(x, y);
plot->replot();
}
});
connect(plot, &QCustomPlot::mouseRelease, [this](QMouseEvent *event){
if(isSelecting && event->button() == Qt::LeftButton) {
isSelecting = false;
plot->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom);
double x2 = plot->xAxis->pixelToCoord(event->pos().x());
double y2 = plot->yAxis->pixelToCoord(event->pos().y());
// Collect data in rect
QVector<double> xData, yData;
double minX = std::min(selStartX, x2);
double maxX = std::max(selStartX, x2);
double minY = std::min(selStartY, y2);
double maxY = std::max(selStartY, y2);
// Iterate visible graphs to find data
QList<QCPGraph*> graphs = {graphReal, graphAmp, graphLSVSample, graphLSVDiff};
for(auto g : graphs) {
if(!g->visible()) continue;
for(auto it = g->data()->begin(); it != g->data()->end(); ++it) {
if(it->key >= minX && it->key <= maxX && it->value >= minY && it->value <= maxY) {
xData.append(it->key);
yData.append(it->value);
}
}
if(!xData.isEmpty()) break; // Only fit one graph
}
if(xData.size() > 2) {
// Show Dialog
QDialog dlg(this);
dlg.setWindowTitle("Fit Data");
QFormLayout *layout = new QFormLayout(&dlg);
QComboBox *type = new QComboBox();
type->addItems({"Linear", "Exponential", "Logarithmic", "Polynomial"});
QSpinBox *order = new QSpinBox(); order->setRange(1, 10); order->setValue(2);
QCheckBox *inverse = new QCheckBox("Inverse");
layout->addRow("Type:", type);
layout->addRow("Poly Order:", order);
layout->addRow(inverse);
QDialogButtonBox *btns = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
layout->addRow(btns);
connect(btns, &QDialogButtonBox::accepted, &dlg, &QDialog::accept);
connect(btns, &QDialogButtonBox::rejected, &dlg, &QDialog::reject);
if(dlg.exec() == QDialog::Accepted) {
performFit(xData, yData, type->currentIndex(), order->value(), inverse->isChecked());
}
}
selectionRect->setVisible(false);
plot->replot();
}
});
configureRawPlot(); configureRawPlot();
} }
void GraphWidget::scaleX() { plot->rescaleAxes(true); plot->replot(); }
void GraphWidget::scaleY() { plot->yAxis->rescale(true); plot->yAxis2->rescale(true); plot->replot(); }
void GraphWidget::scaleBoth() { plot->rescaleAxes(true); plot->replot(); }
void GraphWidget::centerView() { scaleBoth(); }
void GraphWidget::startAnalyze() {
isSelecting = true;
// FIX: Use default constructor for empty flags instead of 0
plot->setInteractions(QCP::Interactions());
}
void GraphWidget::performFit(const QVector<double>& x, const QVector<double>& y, int type, int order, bool inverse) {
graphFit->data()->clear();
QVector<double> xFit, yFit;
// Simple Linear Fit Implementation for demo
if(type == 0) { // Linear
double m, c;
QVector<double> yProc = y;
if(inverse) {
for(int i=0; i<y.size(); i++) yProc[i] = 1.0/y[i];
}
linearRegression(x, yProc, m, c);
double minX = *std::min_element(x.begin(), x.end());
double maxX = *std::max_element(x.begin(), x.end());
for(int i=0; i<=100; i++) {
double xv = minX + (maxX-minX)*i/100.0;
double yv = m*xv + c;
if(inverse) yv = 1.0/yv;
xFit.append(xv);
yFit.append(yv);
}
}
// Add other fits here (Exp, Log, Poly) as needed
graphFit->setData(xFit, yFit);
graphFit->setVisible(true);
plot->replot();
}
// ... (Rest of configuration methods same as before) ...
void GraphWidget::configureRawPlot() { void GraphWidget::configureRawPlot() {
// Only clear if explicitly requested, but here we just set visibility
plot->xAxis->setLabel("Frequency (Hz)"); plot->xAxis->setLabel("Frequency (Hz)");
plot->xAxis->setScaleType(QCPAxis::stLogarithmic); plot->xAxis->setScaleType(QCPAxis::stLogarithmic);
QSharedPointer<QCPAxisTickerLog> logTicker(new QCPAxisTickerLog); QSharedPointer<QCPAxisTickerLog> logTicker(new QCPAxisTickerLog);
@ -152,10 +310,10 @@ void GraphWidget::configureRawPlot() {
graphNyquistCorr->setVisible(false); graphNyquistCorr->setVisible(false);
graphAmp->setVisible(false); graphAmp->setVisible(false);
graphExtrapolated->setVisible(false); graphExtrapolated->setVisible(false);
graphLSVBlank->setVisible(false); graphLSVBlank->setVisible(false);
graphLSVSample->setVisible(false); graphLSVSample->setVisible(false);
graphLSVDiff->setVisible(false); graphLSVDiff->setVisible(false);
graphFit->setVisible(false);
plot->replot(); plot->replot();
} }
@ -184,10 +342,10 @@ void GraphWidget::configureNyquistPlot() {
graphNyquistCorr->setVisible(true); graphNyquistCorr->setVisible(true);
graphAmp->setVisible(false); graphAmp->setVisible(false);
graphExtrapolated->setVisible(true); graphExtrapolated->setVisible(true);
graphLSVBlank->setVisible(false); graphLSVBlank->setVisible(false);
graphLSVSample->setVisible(false); graphLSVSample->setVisible(false);
graphLSVDiff->setVisible(false); graphLSVDiff->setVisible(false);
graphFit->setVisible(false);
plot->replot(); plot->replot();
} }
@ -214,10 +372,10 @@ void GraphWidget::configureAmperometricPlot() {
graphHilbert->setVisible(false); graphHilbert->setVisible(false);
graphNyquistCorr->setVisible(false); graphNyquistCorr->setVisible(false);
graphExtrapolated->setVisible(false); graphExtrapolated->setVisible(false);
graphLSVBlank->setVisible(false); graphLSVBlank->setVisible(false);
graphLSVSample->setVisible(false); graphLSVSample->setVisible(false);
graphLSVDiff->setVisible(false); graphLSVDiff->setVisible(false);
graphFit->setVisible(false);
plot->replot(); plot->replot();
} }
@ -246,6 +404,7 @@ void GraphWidget::configureLSVPlot() {
graphNyquistCorr->setVisible(false); graphNyquistCorr->setVisible(false);
graphExtrapolated->setVisible(false); graphExtrapolated->setVisible(false);
graphAmp->setVisible(false); graphAmp->setVisible(false);
graphFit->setVisible(false);
plot->replot(); plot->replot();
} }
@ -288,7 +447,7 @@ void GraphWidget::addLSVData(double voltage, double current, LSVTrace traceType)
if (target) { if (target) {
target->addData(voltage, current); target->addData(voltage, current);
target->rescaleAxes(false); // Rescale to fit new data target->rescaleAxes(false);
plot->replot(); plot->replot();
} }
} }
@ -313,10 +472,7 @@ void GraphWidget::clear() {
graphNyquistCorr->data()->clear(); graphNyquistCorr->data()->clear();
graphAmp->data()->clear(); graphAmp->data()->clear();
graphExtrapolated->data()->clear(); graphExtrapolated->data()->clear();
graphFit->data()->clear();
// Note: We deliberately do NOT clear LSV data in generic clear()
// to allow Blank scans to persist across tab changes or mode switches
// until explicitly cleared.
plot->replot(); plot->replot();
} }

View File

@ -1,8 +1,14 @@
// host/src/GraphWidget.h // File: host/src/GraphWidget.h
#pragma once #pragma once
#include <QWidget> #include <QWidget>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
#include <QDialog>
#include <QComboBox>
#include <QSpinBox>
#include <QCheckBox>
#include "qcustomplot.h" #include "qcustomplot.h"
class GraphWidget : public QWidget { class GraphWidget : public QWidget {
@ -16,16 +22,14 @@ public:
void addNyquistData(double r_meas, double i_meas, double r_corr, double i_corr, bool showCorr); void addNyquistData(double r_meas, double i_meas, double r_corr, double i_corr, bool showCorr);
void addAmperometricData(double index, double current); void addAmperometricData(double index, double current);
// Updated LSV Data handler
enum LSVTrace { LSV_BLANK, LSV_SAMPLE, LSV_DIFF }; enum LSVTrace { LSV_BLANK, LSV_SAMPLE, LSV_DIFF };
void addLSVData(double voltage, double current, LSVTrace traceType); void addLSVData(double voltage, double current, LSVTrace traceType);
void addHilbertData(const QVector<double>& freq, const QVector<double>& hilbertImag); void addHilbertData(const QVector<double>& freq, const QVector<double>& hilbertImag);
void setExtrapolatedPoint(double real, double imag); void setExtrapolatedPoint(double real, double imag);
void clear(); void clear();
void clearLSV(LSVTrace traceType); // Clear specific LSV trace void clearLSV(LSVTrace traceType);
// View Configurations // View Configurations
void configureRawPlot(); void configureRawPlot();
@ -33,25 +37,42 @@ public:
void configureAmperometricPlot(); void configureAmperometricPlot();
void configureLSVPlot(); void configureLSVPlot();
private slots:
void scaleX();
void scaleY();
void scaleBoth();
void centerView();
void startAnalyze();
private: private:
QVBoxLayout *layout; QVBoxLayout *mainLayout;
QCustomPlot *plot; QCustomPlot *plot;
// Bode Graphs // Toolbar
QWidget *toolbar;
QPushButton *btnScaleX;
QPushButton *btnScaleY;
QPushButton *btnScaleBoth;
QPushButton *btnCenter;
QPushButton *btnAnalyze;
// Graphs
QCPGraph *graphReal; QCPGraph *graphReal;
QCPGraph *graphImag; QCPGraph *graphImag;
QCPGraph *graphHilbert; QCPGraph *graphHilbert;
// Nyquist Graphs
QCPGraph *graphNyquistRaw; QCPGraph *graphNyquistRaw;
QCPGraph *graphNyquistCorr; QCPGraph *graphNyquistCorr;
QCPGraph *graphExtrapolated; QCPGraph *graphExtrapolated;
// Amperometric Graph
QCPGraph *graphAmp; QCPGraph *graphAmp;
QCPGraph *graphLSVBlank;
QCPGraph *graphLSVSample;
QCPGraph *graphLSVDiff;
QCPGraph *graphFit; // For analysis results
// Analysis State
bool isSelecting = false;
QCPItemRect *selectionRect;
double selStartX, selStartY;
// LSV Graphs void performFit(const QVector<double>& x, const QVector<double>& y, int type, int order, bool inverse);
QCPGraph *graphLSVBlank; // The "Tap Water" baseline
QCPGraph *graphLSVSample; // The "Bleach" spike
QCPGraph *graphLSVDiff; // The calculated difference (Chlorine only)
}; };

View File

@ -1,52 +1,7 @@
// host/src/MainWindow.cpp // File: host/src/MainWindow.cpp
#include "MainWindow.h" #include "MainWindow.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QDebug>
#include <QDateTime>
#include <QScroller> #include <QScroller>
#include <QGesture> #include <QGesture>
#include <QSplitter>
#include <QDoubleSpinBox>
#include <QSpinBox>
#include <QLabel>
#include <QTimer>
#include <cmath>
#include <complex>
#include <vector>
#include <algorithm>
// Simple FFT Implementation (Cooley-Tukey)
void fft(std::vector<std::complex<double>>& a) {
int n = a.size();
if (n <= 1) return;
std::vector<std::complex<double>> a0(n / 2), a1(n / 2);
for (int i = 0; i < n / 2; i++) {
a0[i] = a[2 * i];
a1[i] = a[2 * i + 1];
}
fft(a0);
fft(a1);
double ang = 2 * M_PI / n;
std::complex<double> w(1), wn(cos(ang), sin(ang));
for (int i = 0; i < n / 2; i++) {
a[i] = a0[i] + w * a1[i];
a[i + n / 2] = a0[i] - w * a1[i];
w *= wn;
}
}
void ifft(std::vector<std::complex<double>>& a) {
int n = a.size();
for (int i = 0; i < n; i++) a[i] = std::conj(a[i]);
fft(a);
for (int i = 0; i < n; i++) {
a[i] = std::conj(a[i]);
a[i] /= n;
}
}
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
settings = new QSettings("EISConfigurator", "Settings", this); settings = new QSettings("EISConfigurator", "Settings", this);
@ -60,8 +15,11 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
blinkTimer->setInterval(500); blinkTimer->setInterval(500);
connect(blinkTimer, &QTimer::timeout, this, &MainWindow::onBlinkTimer); connect(blinkTimer, &QTimer::timeout, this, &MainWindow::onBlinkTimer);
setupUi(); setupUi(); // Defined in MainWindow_UI.cpp
// Auto-refresh ports after startup
QTimer::singleShot(1000, this, &MainWindow::refreshPorts); QTimer::singleShot(1000, this, &MainWindow::refreshPorts);
grabGesture(Qt::SwipeGesture); grabGesture(Qt::SwipeGesture);
} }
@ -79,905 +37,6 @@ void MainWindow::saveSettings() {
settings->setValue("cellConstant", cellConstant); settings->setValue("cellConstant", cellConstant);
} }
void MainWindow::setupUi() {
QWidget *central = new QWidget(this);
setCentralWidget(central);
QVBoxLayout *mainLayout = new QVBoxLayout(central);
mainLayout->setContentsMargins(5, 5, 5, 5);
mainLayout->setSpacing(5);
// --- Controls Container ---
QWidget *controlsContainer = new QWidget(this);
controlsContainer->setStyleSheet("background-color: #3A3A3A; border-radius: 5px;");
QVBoxLayout *ctrlLayout = new QVBoxLayout(controlsContainer);
ctrlLayout->setContentsMargins(5, 5, 5, 5);
ctrlLayout->setSpacing(5);
// Row 1
QHBoxLayout *row1 = new QHBoxLayout();
row1->setSpacing(8);
portSelector = new QComboBox(this);
portSelector->setMinimumWidth(120);
connectBtn = new QPushButton("Connect", this);
QPushButton *refreshBtn = new QPushButton("Refresh", this);
row1->addWidget(portSelector);
row1->addWidget(connectBtn);
row1->addWidget(refreshBtn);
QFrame *sep1 = new QFrame(); sep1->setFrameShape(QFrame::VLine); sep1->setFrameShadow(QFrame::Sunken);
row1->addWidget(sep1);
checkIdBtn = new QPushButton("Check ID", this);
calibrateBtn = new QPushButton("Calibrate HW", this);
row1->addWidget(checkIdBtn);
row1->addWidget(calibrateBtn);
QFrame *sep2 = new QFrame(); sep2->setFrameShape(QFrame::VLine); sep2->setFrameShadow(QFrame::Sunken);
row1->addWidget(sep2);
row1->addWidget(new QLabel("Start:"));
spinSweepStart = new QDoubleSpinBox(this);
spinSweepStart->setRange(0.1, 200000.0); spinSweepStart->setValue(1000.0); spinSweepStart->setSuffix(" Hz");
row1->addWidget(spinSweepStart);
row1->addWidget(new QLabel("Stop:"));
spinSweepStop = new QDoubleSpinBox(this);
spinSweepStop->setRange(0.1, 200000.0); spinSweepStop->setValue(200000.0); spinSweepStop->setSuffix(" Hz");
row1->addWidget(spinSweepStop);
row1->addWidget(new QLabel("PPD:"));
spinSweepPPD = new QSpinBox(this);
spinSweepPPD->setRange(1, 1000); spinSweepPPD->setValue(200); spinSweepPPD->setSuffix(" pts/dec");
row1->addWidget(spinSweepPPD);
sweepBtn = new QPushButton("Sweep", this);
row1->addWidget(sweepBtn);
QFrame *sep3 = new QFrame(); sep3->setFrameShape(QFrame::VLine); sep3->setFrameShadow(QFrame::Sunken);
row1->addWidget(sep3);
row1->addWidget(new QLabel("Shunt:"));
checkShunt = new QCheckBox("Enable", this);
row1->addWidget(checkShunt);
spinShuntRes = new QDoubleSpinBox(this);
spinShuntRes->setRange(1.0, 1000000.0); spinShuntRes->setValue(466.0); spinShuntRes->setSuffix(" Ω");
row1->addWidget(spinShuntRes);
QFrame *sep4 = new QFrame(); sep4->setFrameShape(QFrame::VLine); sep4->setFrameShadow(QFrame::Sunken);
row1->addWidget(sep4);
row1->addWidget(new QLabel("Std Cond:"));
spinCondStd = new QDoubleSpinBox(this);
spinCondStd->setRange(0.0, 1000000.0); spinCondStd->setValue(1413.0); spinCondStd->setSuffix(" µS/cm");
row1->addWidget(spinCondStd);
btnCalCond = new QPushButton("Calibrate K", this);
row1->addWidget(btnCalCond);
lblResultRs = new QLabel(" Rs: -- Ω", this);
lblResultRs->setStyleSheet("font-weight: bold; color: #FFD700; font-size: 14px; margin-left: 10px;");
row1->addWidget(lblResultRs);
row1->addStretch();
ctrlLayout->addLayout(row1);
// Row 2
QHBoxLayout *row2 = new QHBoxLayout();
row2->setSpacing(8);
row2->addWidget(new QLabel("Range:"));
comboRange = new QComboBox(this);
comboRange->addItem("200 Ω", 200);
comboRange->addItem("1 kΩ", 1000);
comboRange->addItem("5 kΩ", 5000);
comboRange->addItem("10 kΩ", 10000);
comboRange->addItem("20 kΩ", 20000);
comboRange->addItem("40 kΩ", 40000);
comboRange->addItem("80 kΩ", 80000);
comboRange->addItem("160 kΩ", 160000);
comboRange->setCurrentIndex(0);
row2->addWidget(comboRange);
QFrame *sepRange = new QFrame(); sepRange->setFrameShape(QFrame::VLine); sepRange->setFrameShadow(QFrame::Sunken);
row2->addWidget(sepRange);
lblResultCond = new QLabel("Cond: -- µS/cm", this);
lblResultCond->setStyleSheet("font-weight: bold; color: #00FFFF; font-size: 14px; margin-right: 10px;");
row2->addWidget(lblResultCond);
QFrame *sep5 = new QFrame(); sep5->setFrameShape(QFrame::VLine); sep5->setFrameShadow(QFrame::Sunken);
row2->addWidget(sep5);
row2->addWidget(new QLabel("Freq:"));
spinFreq = new QDoubleSpinBox(this);
spinFreq->setRange(0.1, 200000.0); spinFreq->setValue(1000.0); spinFreq->setSuffix(" Hz");
row2->addWidget(spinFreq);
measureBtn = new QPushButton("Measure", this);
row2->addWidget(measureBtn);
QFrame *sep6 = new QFrame(); sep6->setFrameShape(QFrame::VLine); sep6->setFrameShadow(QFrame::Sunken);
row2->addWidget(sep6);
row2->addWidget(new QLabel("Bias:"));
spinAmpBias = new QDoubleSpinBox(this);
spinAmpBias->setRange(-1100.0, 1100.0); spinAmpBias->setValue(0.0); spinAmpBias->setSuffix(" mV");
row2->addWidget(spinAmpBias);
row2->addWidget(new QLabel("LPF:"));
comboLPF = new QComboBox(this);
comboLPF->addItem("Bypass", 0);
comboLPF->addItem("20kΩ (8Hz)", 1);
comboLPF->addItem("100kΩ (1.6Hz)", 2);
comboLPF->addItem("200kΩ (0.8Hz)", 3);
comboLPF->addItem("400kΩ (0.4Hz)", 4);
comboLPF->addItem("600kΩ (0.26Hz)", 5);
comboLPF->addItem("1MΩ (0.16Hz)", 6);
comboLPF->setCurrentIndex(1); // Default 20k
row2->addWidget(comboLPF);
ampBtn = new QPushButton("Start Amp", this);
row2->addWidget(ampBtn);
QFrame *sep7 = new QFrame(); sep7->setFrameShape(QFrame::VLine); sep7->setFrameShadow(QFrame::Sunken);
row2->addWidget(sep7);
// LSV Controls
row2->addWidget(new QLabel("LSV:"));
spinLsvStart = new QDoubleSpinBox(this);
spinLsvStart->setRange(-1100.0, 1100.0); spinLsvStart->setValue(800.0); spinLsvStart->setSuffix(" mV");
row2->addWidget(spinLsvStart);
spinLsvStop = new QDoubleSpinBox(this);
spinLsvStop->setRange(-1100.0, 1100.0); spinLsvStop->setValue(-200.0); spinLsvStop->setSuffix(" mV");
row2->addWidget(spinLsvStop);
spinLsvSteps = new QSpinBox(this);
spinLsvSteps->setRange(10, 4000); spinLsvSteps->setValue(200); spinLsvSteps->setSuffix(" pts");
row2->addWidget(spinLsvSteps);
spinLsvDuration = new QSpinBox(this);
spinLsvDuration->setRange(100, 600000); spinLsvDuration->setValue(10000); spinLsvDuration->setSuffix(" ms");
row2->addWidget(spinLsvDuration);
lsvBlankBtn = new QPushButton("Run Blank", this);
row2->addWidget(lsvBlankBtn);
lsvSampleBtn = new QPushButton("Run Sample", this);
row2->addWidget(lsvSampleBtn);
row2->addStretch();
ctrlLayout->addLayout(row2);
mainLayout->addWidget(controlsContainer);
// --- Splitter for Graphs and Log ---
QSplitter *splitter = new QSplitter(Qt::Vertical, this);
tabWidget = new QTabWidget(this);
rawGraph = new GraphWidget(this);
nyquistGraph = new GraphWidget(this);
ampGraph = new GraphWidget(this);
lsvGraph = new GraphWidget(this);
rawGraph->configureRawPlot();
nyquistGraph->configureNyquistPlot();
ampGraph->configureAmperometricPlot();
lsvGraph->configureLSVPlot();
tabWidget->addTab(rawGraph, "Raw Data");
tabWidget->addTab(nyquistGraph, "Nyquist Plot");
tabWidget->addTab(ampGraph, "Amperometry");
tabWidget->addTab(lsvGraph, "Voltammetry");
logWidget = new QTextEdit(this);
logWidget->setReadOnly(true);
logWidget->setFont(QFont("Monospace"));
logWidget->setPlaceholderText("Scanning for 0xCAFE EIS Device...");
logWidget->setStyleSheet("background-color: #1E1E1E; color: #00FF00; border: 1px solid #444;");
QScroller::grabGesture(logWidget->viewport(), QScroller::TouchGesture);
splitter->addWidget(tabWidget);
splitter->addWidget(logWidget);
splitter->setStretchFactor(0, 3);
splitter->setStretchFactor(1, 1);
mainLayout->addWidget(splitter);
// Initial State
checkIdBtn->setEnabled(false);
calibrateBtn->setEnabled(false);
sweepBtn->setEnabled(false);
measureBtn->setEnabled(false);
spinSweepStart->setEnabled(false);
spinSweepStop->setEnabled(false);
spinSweepPPD->setEnabled(false);
ampBtn->setEnabled(false);
spinAmpBias->setEnabled(false);
btnCalCond->setEnabled(false);
comboRange->setEnabled(false);
comboLPF->setEnabled(false);
lsvBlankBtn->setEnabled(false);
lsvSampleBtn->setEnabled(false);
spinLsvStart->setEnabled(false);
spinLsvStop->setEnabled(false);
spinLsvSteps->setEnabled(false);
spinLsvDuration->setEnabled(false);
// Connections
connect(connectBtn, &QPushButton::clicked, this, &MainWindow::connectToPort);
connect(refreshBtn, &QPushButton::clicked, this, &MainWindow::refreshPorts);
connect(checkIdBtn, &QPushButton::clicked, this, &MainWindow::checkDeviceId);
connect(calibrateBtn, &QPushButton::clicked, this, &MainWindow::runCalibration);
connect(sweepBtn, &QPushButton::clicked, this, &MainWindow::startSweep);
connect(measureBtn, &QPushButton::clicked, this, &MainWindow::toggleMeasurement);
connect(ampBtn, &QPushButton::clicked, this, &MainWindow::toggleAmperometry);
connect(lsvBlankBtn, &QPushButton::clicked, this, &MainWindow::startLSVBlank);
connect(lsvSampleBtn, &QPushButton::clicked, this, &MainWindow::startLSVSample);
connect(btnCalCond, &QPushButton::clicked, this, &MainWindow::calibrateCellConstant);
connect(comboLPF, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &MainWindow::onLPFChanged);
connect(tabWidget, &QTabWidget::currentChanged, this, [this](int index){
if (index == 0) rawGraph->configureRawPlot();
else if (index == 1) nyquistGraph->configureNyquistPlot();
else if (index == 2) ampGraph->configureAmperometricPlot();
else if (index == 3) lsvGraph->configureLSVPlot();
});
}
void MainWindow::refreshPorts() {
portSelector->clear();
const auto infos = QSerialPortInfo::availablePorts();
bool foundTarget = false;
QString targetPort;
for (const QSerialPortInfo &info : infos) {
portSelector->addItem(info.portName());
bool isCafe = (info.hasVendorIdentifier() && info.vendorIdentifier() == 0xCAFE);
bool isUsbModem = info.portName().contains("usbmodem", Qt::CaseInsensitive);
if ((isCafe || isUsbModem) && !foundTarget) {
targetPort = info.portName();
foundTarget = true;
logWidget->append(">> Found Target Device: " + targetPort);
}
}
if (foundTarget) {
portSelector->setCurrentText(targetPort);
if (!serial->isOpen()) connectToPort();
}
}
void MainWindow::connectToPort() {
if (serial->isOpen()) {
serial->close();
connectBtn->setText("Connect");
logWidget->append("--- Disconnected ---");
checkIdBtn->setEnabled(false);
calibrateBtn->setEnabled(false);
sweepBtn->setEnabled(false);
measureBtn->setEnabled(false);
spinSweepStart->setEnabled(false);
spinSweepStop->setEnabled(false);
spinSweepPPD->setEnabled(false);
ampBtn->setEnabled(false);
spinAmpBias->setEnabled(false);
btnCalCond->setEnabled(false);
comboRange->setEnabled(false);
comboLPF->setEnabled(false);
lsvBlankBtn->setEnabled(false);
lsvSampleBtn->setEnabled(false);
spinLsvStart->setEnabled(false);
spinLsvStop->setEnabled(false);
spinLsvSteps->setEnabled(false);
spinLsvDuration->setEnabled(false);
isMeasuringImp = false;
isMeasuringAmp = false;
isSweeping = false;
lsvState = LSV_IDLE;
setButtonBlinking(nullptr, false);
measureBtn->setText("Measure");
ampBtn->setText("Start Amp");
lsvBlankBtn->setText("Run Blank");
lsvSampleBtn->setText("Run Sample");
return;
}
if (portSelector->currentText().isEmpty()) return;
serial->setPortName(portSelector->currentText());
serial->setBaudRate(500000);
if (serial->open(QIODevice::ReadWrite)) {
connectBtn->setText("Disconnect");
logWidget->append("--- Connected and Synchronized ---");
checkIdBtn->setEnabled(true);
calibrateBtn->setEnabled(true);
sweepBtn->setEnabled(true);
measureBtn->setEnabled(true);
spinSweepStart->setEnabled(true);
spinSweepStop->setEnabled(true);
spinSweepPPD->setEnabled(true);
ampBtn->setEnabled(true);
spinAmpBias->setEnabled(true);
btnCalCond->setEnabled(true);
comboRange->setEnabled(true);
comboLPF->setEnabled(true);
lsvBlankBtn->setEnabled(true);
lsvSampleBtn->setEnabled(true);
spinLsvStart->setEnabled(true);
spinLsvStop->setEnabled(true);
spinLsvSteps->setEnabled(true);
spinLsvDuration->setEnabled(true);
// Sync LPF
onLPFChanged(comboLPF->currentIndex());
} else {
logWidget->append(">> Connection Error: " + serial->errorString());
}
}
void MainWindow::onPortError(QSerialPort::SerialPortError error) {
if (error == QSerialPort::ResourceError) {
logWidget->append(">> Critical Error: Connection Lost.");
serial->close();
connectBtn->setText("Connect");
checkIdBtn->setEnabled(false);
calibrateBtn->setEnabled(false);
sweepBtn->setEnabled(false);
measureBtn->setEnabled(false);
spinSweepStart->setEnabled(false);
spinSweepStop->setEnabled(false);
spinSweepPPD->setEnabled(false);
ampBtn->setEnabled(false);
spinAmpBias->setEnabled(false);
btnCalCond->setEnabled(false);
comboRange->setEnabled(false);
comboLPF->setEnabled(false);
lsvBlankBtn->setEnabled(false);
lsvSampleBtn->setEnabled(false);
spinLsvStart->setEnabled(false);
spinLsvStop->setEnabled(false);
spinLsvSteps->setEnabled(false);
spinLsvDuration->setEnabled(false);
setButtonBlinking(nullptr, false);
}
}
void MainWindow::checkDeviceId() {
if (serial->isOpen()) serial->write("v\n");
}
void MainWindow::runCalibration() {
if (serial->isOpen()) {
int rangeVal = comboRange->currentData().toInt();
serial->write(QString("r %1\n").arg(rangeVal).toUtf8());
serial->write("c\n");
}
}
void MainWindow::onLPFChanged(int index) {
if (serial->isOpen()) {
int val = comboLPF->itemData(index).toInt();
serial->write(QString("f %1\n").arg(val).toUtf8());
}
}
void MainWindow::startSweep() {
if (!serial->isOpen()) return;
if (isSweeping) {
// Stop Sweep
serial->write("x\n");
return;
}
if (isMeasuringAmp) toggleAmperometry();
if (lsvState != LSV_IDLE) stopLSV();
if (isMeasuringImp) toggleMeasurement();
rawGraph->clear();
nyquistGraph->clear();
sweepFreqs.clear();
sweepReals.clear();
sweepImags.clear();
double start = spinSweepStart->value();
double stop = spinSweepStop->value();
int ppd = spinSweepPPD->value();
int rangeVal = comboRange->currentData().toInt();
logWidget->append(QString(">> Starting Sweep (Range: %1, s %2 %3 %4)...").arg(rangeVal).arg(start).arg(stop).arg(ppd));
serial->write(QString("r %1\n").arg(rangeVal).toUtf8());
serial->write(QString("s %1 %2 %3\n").arg(start).arg(stop).arg(ppd).toUtf8());
isSweeping = true;
sweepBtn->setText("Stop Sweep");
setButtonBlinking(sweepBtn, true);
tabWidget->setCurrentIndex(1);
}
void MainWindow::toggleMeasurement() {
if (!serial->isOpen()) return;
if (isMeasuringAmp) toggleAmperometry();
if (lsvState != LSV_IDLE) stopLSV();
if (isSweeping) startSweep(); // Stop sweep
if (isMeasuringImp) {
serial->write("x\n");
measureBtn->setText("Measure");
setButtonBlinking(nullptr, false);
isMeasuringImp = false;
} else {
double freq = spinFreq->value();
int rangeVal = comboRange->currentData().toInt();
serial->write(QString("r %1\n").arg(rangeVal).toUtf8());
serial->write(QString("m %1\n").arg(freq).toUtf8());
measureBtn->setText("Stop");
setButtonBlinking(measureBtn, true);
isMeasuringImp = true;
tabWidget->setCurrentIndex(0);
}
}
void MainWindow::toggleAmperometry() {
if (!serial->isOpen()) return;
if (isMeasuringImp) toggleMeasurement();
if (lsvState != LSV_IDLE) stopLSV();
if (isSweeping) startSweep();
if (isMeasuringAmp) {
serial->write("x\n");
ampBtn->setText("Start Amp");
setButtonBlinking(nullptr, false);
isMeasuringAmp = false;
} else {
double bias = spinAmpBias->value();
int rangeVal = comboRange->currentData().toInt();
serial->write(QString("r %1\n").arg(rangeVal).toUtf8());
serial->write(QString("a %1\n").arg(bias).toUtf8());
ampBtn->setText("Stop Amp");
setButtonBlinking(ampBtn, true);
isMeasuringAmp = true;
ampGraph->clear();
tabWidget->setCurrentIndex(2);
}
}
void MainWindow::startLSVBlank() {
if (!serial->isOpen()) return;
if (isMeasuringImp) toggleMeasurement();
if (isMeasuringAmp) toggleAmperometry();
if (lsvState != LSV_IDLE) stopLSV();
if (isSweeping) startSweep();
double start = spinLsvStart->value();
double stop = spinLsvStop->value();
int steps = spinLsvSteps->value();
int duration = spinLsvDuration->value();
int rangeVal = comboRange->currentData().toInt();
logWidget->append(QString(">> Starting LSV Blank (Range: %1, %.1f to %.1f mV)...").arg(rangeVal).arg(start).arg(stop));
serial->write(QString("r %1\n").arg(rangeVal).toUtf8());
serial->write(QString("l %1 %2 %3 %4\n").arg(start).arg(stop).arg(steps).arg(duration).toUtf8());
lsvBlankBtn->setText("Stop Blank");
setButtonBlinking(lsvBlankBtn, true);
lsvState = LSV_RUNNING_BLANK;
lsvGraph->clearLSV(GraphWidget::LSV_BLANK);
lsvGraph->clearLSV(GraphWidget::LSV_DIFF); // Clear old diff
lsvBlankData.clear();
tabWidget->setCurrentIndex(3);
}
void MainWindow::startLSVSample() {
if (!serial->isOpen()) return;
if (isMeasuringImp) toggleMeasurement();
if (isMeasuringAmp) toggleAmperometry();
if (lsvState != LSV_IDLE) stopLSV();
if (isSweeping) startSweep();
double start = spinLsvStart->value();
double stop = spinLsvStop->value();
int steps = spinLsvSteps->value();
int duration = spinLsvDuration->value();
int rangeVal = comboRange->currentData().toInt();
logWidget->append(QString(">> Starting LSV Sample (Range: %1, %.1f to %.1f mV)...").arg(rangeVal).arg(start).arg(stop));
serial->write(QString("r %1\n").arg(rangeVal).toUtf8());
serial->write(QString("l %1 %2 %3 %4\n").arg(start).arg(stop).arg(steps).arg(duration).toUtf8());
lsvSampleBtn->setText("Stop Sample");
setButtonBlinking(lsvSampleBtn, true);
lsvState = LSV_RUNNING_SAMPLE;
lsvGraph->clearLSV(GraphWidget::LSV_SAMPLE);
lsvGraph->clearLSV(GraphWidget::LSV_DIFF); // Clear old diff
lsvSampleData.clear();
tabWidget->setCurrentIndex(3);
}
void MainWindow::stopLSV() {
serial->write("x\n");
lsvBlankBtn->setText("Run Blank");
lsvSampleBtn->setText("Run Sample");
setButtonBlinking(nullptr, false);
// If we just finished a sample run, calculate the difference
if (lsvState == LSV_RUNNING_SAMPLE) {
calculateLSVDiff();
}
lsvState = LSV_IDLE;
}
void MainWindow::calculateLSVDiff() {
if (lsvBlankData.isEmpty() || lsvSampleData.isEmpty()) return;
lsvGraph->clearLSV(GraphWidget::LSV_DIFF);
// Simple index-based subtraction (assumes same parameters used)
int count = std::min(lsvBlankData.size(), lsvSampleData.size());
for (int i = 0; i < count; i++) {
double v = lsvSampleData[i].voltage; // Use sample voltage
double diffCurrent = lsvSampleData[i].current - lsvBlankData[i].current;
lsvGraph->addLSVData(v, diffCurrent, GraphWidget::LSV_DIFF);
}
logWidget->append(">> Calculated Difference Curve.");
}
void MainWindow::handleSerialData() {
while (serial->canReadLine()) {
QByteArray line = serial->readLine();
QString str = QString::fromUtf8(line).trimmed();
if (str.isEmpty()) continue;
QString timestamp = QDateTime::currentDateTime().toString("HH:mm:ss.zzz");
logWidget->append(QString("[%1] %2").arg(timestamp, str));
logWidget->moveCursor(QTextCursor::End);
if (str.startsWith("DATA,")) {
parseData(str);
} else if (str.startsWith("AMP,")) {
parseData(str);
} else if (str.startsWith("RAMP,")) {
parseData(str);
} else if (str == "STOPPED") {
// Reset UI state
if (lsvState != LSV_IDLE) stopLSV();
if (isSweeping) {
isSweeping = false;
sweepBtn->setText("Sweep");
setButtonBlinking(nullptr, false);
}
if (isMeasuringImp) {
isMeasuringImp = false;
measureBtn->setText("Measure");
setButtonBlinking(nullptr, false);
}
if (isMeasuringAmp) {
isMeasuringAmp = false;
ampBtn->setText("Start Amp");
setButtonBlinking(nullptr, false);
}
}
}
}
void MainWindow::parseData(const QString &data) {
QStringList parts = data.split(',');
if (parts[0] == "AMP" && parts.size() >= 3) {
bool okIdx, okCurr;
double index = parts[1].toDouble(&okIdx);
double current = parts[2].toDouble(&okCurr);
if (okIdx && okCurr) ampGraph->addAmperometricData(index, current);
return;
}
if (parts[0] == "RAMP" && parts.size() >= 3) {
bool okIdx, okCurr;
double index = parts[1].toDouble(&okIdx);
double current = parts[2].toDouble(&okCurr);
if (okIdx && okCurr) {
// Calculate Voltage based on UI parameters (Host-side calculation)
double start = spinLsvStart->value();
double stop = spinLsvStop->value();
int steps = spinLsvSteps->value();
double voltage = start + (index * (stop - start) / steps);
if (lsvState == LSV_RUNNING_BLANK) {
lsvGraph->addLSVData(voltage, current, GraphWidget::LSV_BLANK);
lsvBlankData.append({voltage, current});
} else if (lsvState == LSV_RUNNING_SAMPLE) {
lsvGraph->addLSVData(voltage, current, GraphWidget::LSV_SAMPLE);
lsvSampleData.append({voltage, current});
}
}
return;
}
if (parts[0] == "DATA" && parts.size() >= 6) {
bool okF, okR, okI;
double freq = parts[1].toDouble(&okF);
double real = parts[4].toDouble(&okR);
double imag = parts[5].toDouble(&okI);
if (okF && okR && okI) {
double real_plot = real;
double imag_plot = imag;
bool showCorr = false;
if (checkShunt->isChecked()) {
showCorr = true;
double r_shunt = spinShuntRes->value();
std::complex<double> z_meas(real, imag);
std::complex<double> z_shunt(r_shunt, 0.0);
std::complex<double> denom = z_shunt - z_meas;
if (std::abs(denom) > 1e-9) {
std::complex<double> z_cell = (z_meas * z_shunt) / denom;
real_plot = z_cell.real();
imag_plot = z_cell.imag();
} else {
real_plot = 1e9;
imag_plot = 0;
}
}
rawGraph->addBodeData(freq, real_plot, imag_plot);
nyquistGraph->addNyquistData(real, imag, real_plot, imag_plot, showCorr);
sweepFreqs.append(freq);
sweepReals.append(real_plot);
sweepImags.append(imag_plot);
if (sweepReals.size() > 10 && sweepReals.size() % 10 == 0) {
computeHilbert();
performCircleFit();
}
}
}
}
void MainWindow::computeHilbert() {
int n = sweepReals.size();
if (n == 0) return;
int n_fft = 1;
while (n_fft < n) n_fft *= 2;
std::vector<std::complex<double>> signal(n_fft);
for (int i = 0; i < n; i++) signal[i] = std::complex<double>(sweepReals[i], 0.0);
fft(signal);
for (int i = 1; i < n_fft / 2; i++) signal[i] *= 2.0;
for (int i = n_fft / 2 + 1; i < n_fft; i++) signal[i] = 0.0;
ifft(signal);
QVector<double> hilbertImag;
for (int i = 0; i < n; i++) hilbertImag.append(signal[i].imag());
rawGraph->addHilbertData(sweepFreqs, hilbertImag);
}
// Least Squares Circle Fit to find Rs (High Freq Intercept)
void MainWindow::performCircleFit() {
int n = sweepReals.size();
if (n < 5) return;
// --- Check for actual zero crossing first ---
double measuredRs = -1.0;
bool found = false;
for (int i = 0; i < n - 1; i++) {
double img1 = sweepImags[i];
double img2 = sweepImags[i+1];
// Check if we cross zero (one positive, one negative) or hit zero exactly
if ((img1 >= 0 && img2 < 0) || (img1 < 0 && img2 >= 0)) {
double r1 = sweepReals[i];
double r2 = sweepReals[i+1];
// Avoid division by zero
if (std::abs(img2 - img1) < 1e-9) continue;
// Linear Interpolation for Real Z where Imag Z = 0
double fraction = (0.0 - img1) / (img2 - img1);
double crossingReal = r1 + fraction * (r2 - r1);
// We want the smallest Real value (Rs is the left-most intercept)
if (!found || crossingReal < measuredRs) {
measuredRs = crossingReal;
found = true;
}
}
}
if (found && measuredRs > 0) {
lblResultRs->setText(QString(" Rs: %1 Ω (Meas)").arg(measuredRs, 0, 'f', 2));
double cond = (cellConstant / measuredRs) * 1000000.0;
lblResultCond->setText(QString(" Cond: %1 µS/cm").arg(cond, 0, 'f', 2));
nyquistGraph->setExtrapolatedPoint(measuredRs, 0);
return; // Skip extrapolation
}
// Use last 1/3rd of points (High Frequency end)
int startIdx = n - (n / 3);
if (startIdx < 0) startIdx = 0;
int count = n - startIdx;
if (count < 3) return;
double sum_x = 0, sum_y = 0, sum_x2 = 0, sum_y2 = 0;
double sum_xy = 0, sum_x3 = 0, sum_y3 = 0, sum_xy2 = 0, sum_x2y = 0;
// Kasa Method for Circle Fit: Minimize sum((x^2 + y^2) - (Ax + By + C))^2
// x = Real, y = -Imag (Nyquist convention)
for (int i = startIdx; i < n; i++) {
double x = sweepReals[i];
double y = -sweepImags[i]; // Nyquist Y is -Imag
double x2 = x * x;
double y2 = y * y;
sum_x += x;
sum_y += y;
sum_x2 += x2;
sum_y2 += y2;
sum_xy += x * y;
sum_x3 += x2 * x;
sum_y3 += y2 * y;
sum_xy2 += x * y2;
sum_x2y += x2 * y;
}
// Solve Linear System for A, B, C
// | sum_x2 sum_xy sum_x | | A | | sum_x(x2+y2) |
// | sum_xy sum_y2 sum_y | | B | = | sum_y(x2+y2) |
// | sum_x sum_y N | | C | | sum(x2+y2) |
double M11 = sum_x2, M12 = sum_xy, M13 = sum_x;
double M21 = sum_xy, M22 = sum_y2, M23 = sum_y;
double M31 = sum_x, M32 = sum_y, M33 = (double)count;
double R1 = sum_x3 + sum_xy2;
double R2 = sum_x2y + sum_y3;
double R3 = sum_x2 + sum_y2;
// Determinant of M
double det = M11*(M22*M33 - M23*M32) - M12*(M21*M33 - M23*M31) + M13*(M21*M32 - M22*M31);
if (std::abs(det) < 1e-9) return; // Singular
// Cramer's Rule for A, B, C
double detA = R1*(M22*M33 - M23*M32) - M12*(R2*M33 - M23*R3) + M13*(R2*M32 - M22*R3);
double detB = M11*(R2*M33 - M23*R3) - R1*(M21*M33 - M23*M31) + M13*(M21*R3 - R2*M31);
double detC = M11*(M22*R3 - R2*M32) - M12*(M21*R3 - R2*M31) + R1*(M21*M32 - M22*M31);
double A = detA / det;
double B = detB / det;
double C = detC / det;
// Circle Parameters
// Center (xc, yc) = (A/2, B/2)
// Radius r = sqrt(C + xc^2 + yc^2)
double xc = A / 2.0;
// double yc = B / 2.0; // Not needed for intercept
double r_sq = C + (A*A)/4.0 + (B*B)/4.0;
if (r_sq < 0) return; // Invalid fit
// Find Intercepts with Real Axis (y = 0)
// Equation: x^2 + y^2 - Ax - By - C = 0
// Set y=0: x^2 - Ax - C = 0
// x = (A +/- sqrt(A^2 + 4C)) / 2
double disc = A*A + 4*C;
if (disc < 0) return; // No real intercept
double x1 = (A - std::sqrt(disc)) / 2.0;
double x2 = (A + std::sqrt(disc)) / 2.0;
// Rs is typically the smaller positive intercept (High Frequency)
// Rct + Rs is the larger intercept (Low Frequency)
double Rs = 0;
if (x1 > 0 && x2 > 0) Rs = std::min(x1, x2);
else if (x1 > 0) Rs = x1;
else if (x2 > 0) Rs = x2;
else Rs = std::max(x1, x2); // Fallback
// Update UI
lblResultRs->setText(QString(" Rs: %1 Ω").arg(Rs, 0, 'f', 2));
// Calculate Conductivity: Cond = K / Rs
// K is in cm^-1, Rs in Ohms -> S/cm -> * 1e6 for uS/cm
double cond = (cellConstant / Rs) * 1000000.0;
lblResultCond->setText(QString(" Cond: %1 µS/cm").arg(cond, 0, 'f', 2));
// Update Graph Marker
nyquistGraph->setExtrapolatedPoint(Rs, 0);
}
void MainWindow::calibrateCellConstant() {
// Parse current Rs from label (dirty but effective given the flow)
QString txt = lblResultRs->text();
if (txt.contains("--")) {
QMessageBox::warning(this, "Calibration Error", "No valid Rs measurement found. Run a sweep first.");
return;
}
// Extract number from string " Rs: 123.45 Ω"
QString numStr = txt.section(':', 1).section(QChar(0x03A9), 0, 0).trimmed(); // 0x03A9 is Omega
double measuredRs = numStr.toDouble();
if (measuredRs <= 0) return;
double stdCond = spinCondStd->value(); // uS/cm
// K = Rs * Cond
// Units: Ohm * (uS/cm) * 1e-6 = Ohm * S/cm * 1e-6 = (1/cm) * 1e-6 ???
// Wait: Cond = K / Rs => K = Cond * Rs
// K [cm^-1] = (uS/cm * 1e-6) [S/cm] * Rs [Ohm]
cellConstant = (stdCond * 1e-6) * measuredRs;
saveSettings();
QMessageBox::information(this, "Calibration Success",
QString("Cell Constant (K) calibrated to: %1 cm⁻¹").arg(cellConstant, 0, 'f', 4));
// Re-run fit to update display with new K
performCircleFit();
}
void MainWindow::setButtonBlinking(QPushButton *btn, bool blinking) {
if (activeButton && activeButton != btn) {
// Reset previous button
activeButton->setStyleSheet("");
}
activeButton = btn;
if (blinking) {
blinkTimer->start();
} else {
blinkTimer->stop();
if (activeButton) activeButton->setStyleSheet("");
activeButton = nullptr;
}
}
void MainWindow::onBlinkTimer() {
if (!activeButton) return;
blinkState = !blinkState;
if (blinkState) {
activeButton->setStyleSheet("background-color: #FF0000; color: white; border: 1px solid #FF4444;");
} else {
activeButton->setStyleSheet("background-color: #880000; color: white; border: 1px solid #AA0000;");
}
}
bool MainWindow::event(QEvent *event) { bool MainWindow::event(QEvent *event) {
if (event->type() == QEvent::Gesture) { if (event->type() == QEvent::Gesture) {
QGestureEvent *ge = static_cast<QGestureEvent*>(event); QGestureEvent *ge = static_cast<QGestureEvent*>(event);

View File

@ -1,4 +1,4 @@
// host/src/MainWindow.h // File: host/src/MainWindow.h
#pragma once #pragma once
#include <QMainWindow> #include <QMainWindow>
@ -29,37 +29,43 @@ protected:
bool event(QEvent *event) override; bool event(QEvent *event) override;
private slots: private slots:
// Serial Slots (MainWindow_Serial.cpp)
void handleSerialData(); void handleSerialData();
void connectToPort(); void connectToPort();
void refreshPorts(); void refreshPorts();
void onPortError(QSerialPort::SerialPortError error); void onPortError(QSerialPort::SerialPortError error);
void onBlinkTimer();
// Action Slots // UI Slots (MainWindow_UI.cpp / MainWindow.cpp)
void onBlinkTimer();
void onLPFChanged(int index);
// Action Slots (MainWindow_Actions.cpp)
void checkDeviceId(); void checkDeviceId();
void runCalibration(); void runCalibration();
void startSweep(); void startSweep();
void toggleMeasurement(); void toggleMeasurement();
void toggleAmperometry(); void toggleAmperometry();
// LSV Slots // LSV Slots (MainWindow_Actions.cpp)
void startLSVBlank(); void startLSVBlank();
void startLSVSample(); void startLSVSample();
void stopLSV(); void stopLSV();
void calibrateCellConstant(); void calibrateCellConstant();
void onLPFChanged(int index);
private: private:
void setupUi(); // Initialization Methods
void loadSettings(); void setupUi(); // In MainWindow_UI.cpp
void saveSettings(); void loadSettings(); // In MainWindow.cpp
void parseData(const QString &data); void saveSettings(); // In MainWindow.cpp
void handleSwipe(QSwipeGesture *gesture);
void computeHilbert(); // Logic Helpers
void performCircleFit(); void parseData(const QString &data); // In MainWindow_Serial.cpp
void calculateLSVDiff(); void handleSwipe(QSwipeGesture *gesture); // In MainWindow.cpp
void setButtonBlinking(QPushButton *btn, bool blinking); void computeHilbert(); // In MainWindow_Actions.cpp
void performCircleFit(); // In MainWindow_Actions.cpp
void calculateLSVDiff(); // In MainWindow_Actions.cpp
void setButtonBlinking(QPushButton *btn, bool blinking); // In MainWindow_UI.cpp
QSerialPort *serial; QSerialPort *serial;
QSettings *settings; QSettings *settings;
@ -74,7 +80,7 @@ private:
GraphWidget *lsvGraph; GraphWidget *lsvGraph;
QTextEdit *logWidget; QTextEdit *logWidget;
// Layout // Layout Elements
QTabWidget *tabWidget; QTabWidget *tabWidget;
QComboBox *portSelector; QComboBox *portSelector;
QPushButton *connectBtn; QPushButton *connectBtn;
@ -97,7 +103,7 @@ private:
// Amperometry Configuration // Amperometry Configuration
QDoubleSpinBox *spinAmpBias; QDoubleSpinBox *spinAmpBias;
QPushButton *ampBtn; QPushButton *ampBtn;
QComboBox *comboLPF; // New LPF Dropdown QComboBox *comboLPF;
// LSV Configuration // LSV Configuration
QDoubleSpinBox *spinLsvStart; QDoubleSpinBox *spinLsvStart;
@ -115,11 +121,11 @@ private:
double cellConstant = 1.0; double cellConstant = 1.0;
// State Flags
bool isMeasuringImp = false; bool isMeasuringImp = false;
bool isMeasuringAmp = false; bool isMeasuringAmp = false;
bool isSweeping = false; bool isSweeping = false;
// LSV State
enum LSVState { LSV_IDLE, LSV_RUNNING_BLANK, LSV_RUNNING_SAMPLE }; enum LSVState { LSV_IDLE, LSV_RUNNING_BLANK, LSV_RUNNING_SAMPLE };
LSVState lsvState = LSV_IDLE; LSVState lsvState = LSV_IDLE;
@ -128,7 +134,7 @@ private:
QVector<double> sweepReals; QVector<double> sweepReals;
QVector<double> sweepImags; QVector<double> sweepImags;
// LSV Data Storage for Diff Calculation // LSV Data Storage
struct LSVPoint { double voltage; double current; }; struct LSVPoint { double voltage; double current; };
QVector<LSVPoint> lsvBlankData; QVector<LSVPoint> lsvBlankData;
QVector<LSVPoint> lsvSampleData; QVector<LSVPoint> lsvSampleData;

View File

@ -0,0 +1,403 @@
// File: host/src/MainWindow_Actions.cpp
#include "MainWindow.h"
#include <complex>
#include <vector>
#include <cmath>
#include <fftw3.h>
void MainWindow::checkDeviceId() {
if (serial->isOpen()) serial->write("v\n");
}
void MainWindow::runCalibration() {
if (serial->isOpen()) {
int rangeVal = comboRange->currentData().toInt();
serial->write(QString("r %1\n").arg(rangeVal).toUtf8());
serial->write("c\n");
}
}
void MainWindow::onLPFChanged(int index) {
if (serial->isOpen()) {
int val = comboLPF->itemData(index).toInt();
serial->write(QString("f %1\n").arg(val).toUtf8());
}
}
void MainWindow::startSweep() {
if (!serial->isOpen()) return;
if (isSweeping) {
// Stop Sweep
serial->write("x\n");
return;
}
if (isMeasuringAmp) toggleAmperometry();
if (lsvState != LSV_IDLE) stopLSV();
if (isMeasuringImp) toggleMeasurement();
rawGraph->clear();
nyquistGraph->clear();
sweepFreqs.clear();
sweepReals.clear();
sweepImags.clear();
double start = spinSweepStart->value();
double stop = spinSweepStop->value();
int ppd = spinSweepPPD->value();
int rangeVal = comboRange->currentData().toInt();
logWidget->append(QString(">> Starting Sweep (Range: %1, s %2 %3 %4)...").arg(rangeVal).arg(start).arg(stop).arg(ppd));
serial->write(QString("r %1\n").arg(rangeVal).toUtf8());
serial->write(QString("s %1 %2 %3\n").arg(start).arg(stop).arg(ppd).toUtf8());
isSweeping = true;
sweepBtn->setText("Stop Sweep");
setButtonBlinking(sweepBtn, true);
tabWidget->setCurrentIndex(1);
}
void MainWindow::toggleMeasurement() {
if (!serial->isOpen()) return;
if (isMeasuringAmp) toggleAmperometry();
if (lsvState != LSV_IDLE) stopLSV();
if (isSweeping) startSweep(); // Stop sweep
if (isMeasuringImp) {
serial->write("x\n");
measureBtn->setText("Measure");
setButtonBlinking(nullptr, false);
isMeasuringImp = false;
} else {
double freq = spinFreq->value();
int rangeVal = comboRange->currentData().toInt();
serial->write(QString("r %1\n").arg(rangeVal).toUtf8());
serial->write(QString("m %1\n").arg(freq).toUtf8());
measureBtn->setText("Stop");
setButtonBlinking(measureBtn, true);
isMeasuringImp = true;
tabWidget->setCurrentIndex(0);
}
}
void MainWindow::toggleAmperometry() {
if (!serial->isOpen()) return;
if (isMeasuringImp) toggleMeasurement();
if (lsvState != LSV_IDLE) stopLSV();
if (isSweeping) startSweep();
if (isMeasuringAmp) {
serial->write("x\n");
ampBtn->setText("Start Amp");
setButtonBlinking(nullptr, false);
isMeasuringAmp = false;
} else {
double bias = spinAmpBias->value();
int rangeVal = comboRange->currentData().toInt();
serial->write(QString("r %1\n").arg(rangeVal).toUtf8());
serial->write(QString("a %1\n").arg(bias).toUtf8());
ampBtn->setText("Stop Amp");
setButtonBlinking(ampBtn, true);
isMeasuringAmp = true;
ampGraph->clear();
tabWidget->setCurrentIndex(2);
}
}
void MainWindow::startLSVBlank() {
if (!serial->isOpen()) return;
if (isMeasuringImp) toggleMeasurement();
if (isMeasuringAmp) toggleAmperometry();
if (lsvState != LSV_IDLE) stopLSV();
if (isSweeping) startSweep();
double start = spinLsvStart->value();
double stop = spinLsvStop->value();
int steps = spinLsvSteps->value();
int duration = spinLsvDuration->value();
int rangeVal = comboRange->currentData().toInt();
logWidget->append(QString(">> Starting LSV Blank (Range: %1, %.1f to %.1f mV)...").arg(rangeVal).arg(start).arg(stop));
serial->write(QString("r %1\n").arg(rangeVal).toUtf8());
serial->write(QString("l %1 %2 %3 %4\n").arg(start).arg(stop).arg(steps).arg(duration).toUtf8());
lsvBlankBtn->setText("Stop Blank");
setButtonBlinking(lsvBlankBtn, true);
lsvState = LSV_RUNNING_BLANK;
lsvGraph->clearLSV(GraphWidget::LSV_BLANK);
lsvGraph->clearLSV(GraphWidget::LSV_DIFF); // Clear old diff
lsvBlankData.clear();
tabWidget->setCurrentIndex(3);
}
void MainWindow::startLSVSample() {
if (!serial->isOpen()) return;
if (isMeasuringImp) toggleMeasurement();
if (isMeasuringAmp) toggleAmperometry();
if (lsvState != LSV_IDLE) stopLSV();
if (isSweeping) startSweep();
double start = spinLsvStart->value();
double stop = spinLsvStop->value();
int steps = spinLsvSteps->value();
int duration = spinLsvDuration->value();
int rangeVal = comboRange->currentData().toInt();
logWidget->append(QString(">> Starting LSV Sample (Range: %1, %.1f to %.1f mV)...").arg(rangeVal).arg(start).arg(stop));
serial->write(QString("r %1\n").arg(rangeVal).toUtf8());
serial->write(QString("l %1 %2 %3 %4\n").arg(start).arg(stop).arg(steps).arg(duration).toUtf8());
lsvSampleBtn->setText("Stop Sample");
setButtonBlinking(lsvSampleBtn, true);
lsvState = LSV_RUNNING_SAMPLE;
lsvGraph->clearLSV(GraphWidget::LSV_SAMPLE);
lsvGraph->clearLSV(GraphWidget::LSV_DIFF); // Clear old diff
lsvSampleData.clear();
tabWidget->setCurrentIndex(3);
}
void MainWindow::stopLSV() {
serial->write("x\n");
lsvBlankBtn->setText("Run Blank");
lsvSampleBtn->setText("Run Sample");
setButtonBlinking(nullptr, false);
// If we just finished a sample run, calculate the difference
if (lsvState == LSV_RUNNING_SAMPLE) {
calculateLSVDiff();
}
lsvState = LSV_IDLE;
}
void MainWindow::calculateLSVDiff() {
if (lsvBlankData.isEmpty() || lsvSampleData.isEmpty()) return;
lsvGraph->clearLSV(GraphWidget::LSV_DIFF);
// Simple index-based subtraction (assumes same parameters used)
int count = std::min(lsvBlankData.size(), lsvSampleData.size());
for (int i = 0; i < count; i++) {
double v = lsvSampleData[i].voltage; // Use sample voltage
double diffCurrent = lsvSampleData[i].current - lsvBlankData[i].current;
lsvGraph->addLSVData(v, diffCurrent, GraphWidget::LSV_DIFF);
}
logWidget->append(">> Calculated Difference Curve.");
}
void MainWindow::computeHilbert() {
int n = sweepReals.size();
if (n == 0) return;
// Pad to next power of 2 for efficiency and consistency with previous logic
int n_fft = 1;
while (n_fft < n) n_fft *= 2;
// Allocate FFTW arrays
fftw_complex *in = (fftw_complex*) fftw_malloc(sizeof(fftw_complex) * n_fft);
fftw_complex *out = (fftw_complex*) fftw_malloc(sizeof(fftw_complex) * n_fft);
// Create Plans (ESTIMATE is fast for one-off sizes)
fftw_plan p_fwd = fftw_plan_dft_1d(n_fft, in, out, FFTW_FORWARD, FFTW_ESTIMATE);
fftw_plan p_bwd = fftw_plan_dft_1d(n_fft, out, in, FFTW_BACKWARD, FFTW_ESTIMATE);
// Prepare Input: Copy data and zero-pad
for (int i = 0; i < n; i++) {
in[i][0] = sweepReals[i]; // Real part
in[i][1] = 0.0; // Imag part
}
for (int i = n; i < n_fft; i++) {
in[i][0] = 0.0;
in[i][1] = 0.0;
}
// Forward FFT
fftw_execute(p_fwd);
// Apply Hilbert Mask in Frequency Domain (Analytic Signal)
// H[0] (DC) and H[N/2] (Nyquist) are left alone.
// Positive Frequencies (1 to N/2 - 1) multiplied by 2.
// Negative Frequencies (N/2 + 1 to N - 1) zeroed out.
int half_n = n_fft / 2;
// Multiply positive frequencies by 2
for (int i = 1; i < half_n; i++) {
out[i][0] *= 2.0;
out[i][1] *= 2.0;
}
// Zero out negative frequencies
for (int i = half_n + 1; i < n_fft; i++) {
out[i][0] = 0.0;
out[i][1] = 0.0;
}
// Inverse FFT
fftw_execute(p_bwd);
// Extract Imaginary part of Analytic Signal (Hilbert Transform)
// Note: FFTW IFFT is unnormalized, divide by N
QVector<double> hilbertImag;
hilbertImag.reserve(n);
for (int i = 0; i < n; i++) {
double val = in[i][1] / n_fft; // Imaginary part normalized
hilbertImag.append(val);
}
rawGraph->addHilbertData(sweepFreqs, hilbertImag);
// Cleanup
fftw_destroy_plan(p_fwd);
fftw_destroy_plan(p_bwd);
fftw_free(in);
fftw_free(out);
}
void MainWindow::performCircleFit() {
int n = sweepReals.size();
if (n < 5) return;
double measuredRs = -1.0;
bool found = false;
for (int i = 0; i < n - 1; i++) {
double img1 = sweepImags[i];
double img2 = sweepImags[i+1];
if ((img1 >= 0 && img2 < 0) || (img1 < 0 && img2 >= 0)) {
double r1 = sweepReals[i];
double r2 = sweepReals[i+1];
if (std::abs(img2 - img1) < 1e-9) continue;
double fraction = (0.0 - img1) / (img2 - img1);
double crossingReal = r1 + fraction * (r2 - r1);
if (!found || crossingReal < measuredRs) {
measuredRs = crossingReal;
found = true;
}
}
}
if (found && measuredRs > 0) {
lblResultRs->setText(QString(" Rs: %1 Ω (Meas)").arg(measuredRs, 0, 'f', 2));
double cond = (cellConstant / measuredRs) * 1000000.0;
lblResultCond->setText(QString(" Cond: %1 µS/cm").arg(cond, 0, 'f', 2));
nyquistGraph->setExtrapolatedPoint(measuredRs, 0);
return;
}
int startIdx = n - (n / 3);
if (startIdx < 0) startIdx = 0;
int count = n - startIdx;
if (count < 3) return;
double sum_x = 0, sum_y = 0, sum_x2 = 0, sum_y2 = 0;
double sum_xy = 0, sum_x3 = 0, sum_y3 = 0, sum_xy2 = 0, sum_x2y = 0;
for (int i = startIdx; i < n; i++) {
double x = sweepReals[i];
double y = -sweepImags[i];
double x2 = x * x;
double y2 = y * y;
sum_x += x;
sum_y += y;
sum_x2 += x2;
sum_y2 += y2;
sum_xy += x * y;
sum_x3 += x2 * x;
sum_y3 += y2 * y;
sum_xy2 += x * y2;
sum_x2y += x2 * y;
}
double M11 = sum_x2, M12 = sum_xy, M13 = sum_x;
double M21 = sum_xy, M22 = sum_y2, M23 = sum_y;
double M31 = sum_x, M32 = sum_y, M33 = (double)count;
double R1 = sum_x3 + sum_xy2;
double R2 = sum_x2y + sum_y3;
double R3 = sum_x2 + sum_y2;
double det = M11*(M22*M33 - M23*M32) - M12*(M21*M33 - M23*M31) + M13*(M21*M32 - M22*M31);
if (std::abs(det) < 1e-9) return;
double detA = R1*(M22*M33 - M23*M32) - M12*(R2*M33 - M23*R3) + M13*(R2*M32 - M22*R3);
double detB = M11*(R2*M33 - M23*R3) - R1*(M21*M33 - M23*M31) + M13*(M21*R3 - R2*M31);
double detC = M11*(M22*R3 - R2*M32) - M12*(M21*R3 - R2*M31) + R1*(M21*M32 - M22*M31);
double A = detA / det;
double B = detB / det;
double C = detC / det;
double xc = A / 2.0;
double r_sq = C + (A*A)/4.0 + (B*B)/4.0;
if (r_sq < 0) return;
double disc = A*A + 4*C;
if (disc < 0) return;
double x1 = (A - std::sqrt(disc)) / 2.0;
double x2 = (A + std::sqrt(disc)) / 2.0;
double Rs = 0;
if (x1 > 0 && x2 > 0) Rs = std::min(x1, x2);
else if (x1 > 0) Rs = x1;
else if (x2 > 0) Rs = x2;
else Rs = std::max(x1, x2);
lblResultRs->setText(QString(" Rs: %1 Ω").arg(Rs, 0, 'f', 2));
double cond = (cellConstant / Rs) * 1000000.0;
lblResultCond->setText(QString(" Cond: %1 µS/cm").arg(cond, 0, 'f', 2));
nyquistGraph->setExtrapolatedPoint(Rs, 0);
}
void MainWindow::calibrateCellConstant() {
QString txt = lblResultRs->text();
if (txt.contains("--")) {
QMessageBox::warning(this, "Calibration Error", "No valid Rs measurement found. Run a sweep first.");
return;
}
QString numStr = txt.section(':', 1).section(QChar(0x03A9), 0, 0).trimmed();
double measuredRs = numStr.toDouble();
if (measuredRs <= 0) return;
double stdCond = spinCondStd->value();
cellConstant = (stdCond * 1e-6) * measuredRs;
saveSettings();
QMessageBox::information(this, "Calibration Success",
QString("Cell Constant (K) calibrated to: %1 cm⁻¹").arg(cellConstant, 0, 'f', 4));
performCircleFit();
}

View File

@ -0,0 +1,240 @@
// File: host/src/MainWindow_Serial.cpp
#include "MainWindow.h"
#include <QDateTime>
#include <complex>
void MainWindow::refreshPorts() {
portSelector->clear();
const auto infos = QSerialPortInfo::availablePorts();
bool foundTarget = false;
QString targetPort;
for (const QSerialPortInfo &info : infos) {
portSelector->addItem(info.portName());
bool isCafe = (info.hasVendorIdentifier() && info.vendorIdentifier() == 0xCAFE);
bool isUsbModem = info.portName().contains("usbmodem", Qt::CaseInsensitive);
if ((isCafe || isUsbModem) && !foundTarget) {
targetPort = info.portName();
foundTarget = true;
logWidget->append(">> Found Target Device: " + targetPort);
}
}
if (foundTarget) {
portSelector->setCurrentText(targetPort);
if (!serial->isOpen()) connectToPort();
}
}
void MainWindow::connectToPort() {
if (serial->isOpen()) {
serial->close();
connectBtn->setText("Connect");
logWidget->append("--- Disconnected ---");
checkIdBtn->setEnabled(false);
calibrateBtn->setEnabled(false);
sweepBtn->setEnabled(false);
measureBtn->setEnabled(false);
spinSweepStart->setEnabled(false);
spinSweepStop->setEnabled(false);
spinSweepPPD->setEnabled(false);
ampBtn->setEnabled(false);
spinAmpBias->setEnabled(false);
btnCalCond->setEnabled(false);
comboRange->setEnabled(false);
comboLPF->setEnabled(false);
lsvBlankBtn->setEnabled(false);
lsvSampleBtn->setEnabled(false);
spinLsvStart->setEnabled(false);
spinLsvStop->setEnabled(false);
spinLsvSteps->setEnabled(false);
spinLsvDuration->setEnabled(false);
isMeasuringImp = false;
isMeasuringAmp = false;
isSweeping = false;
lsvState = LSV_IDLE;
setButtonBlinking(nullptr, false);
measureBtn->setText("Measure");
ampBtn->setText("Start Amp");
lsvBlankBtn->setText("Run Blank");
lsvSampleBtn->setText("Run Sample");
return;
}
if (portSelector->currentText().isEmpty()) return;
serial->setPortName(portSelector->currentText());
serial->setBaudRate(500000);
if (serial->open(QIODevice::ReadWrite)) {
connectBtn->setText("Disconnect");
logWidget->append("--- Connected and Synchronized ---");
checkIdBtn->setEnabled(true);
calibrateBtn->setEnabled(true);
sweepBtn->setEnabled(true);
measureBtn->setEnabled(true);
spinSweepStart->setEnabled(true);
spinSweepStop->setEnabled(true);
spinSweepPPD->setEnabled(true);
ampBtn->setEnabled(true);
spinAmpBias->setEnabled(true);
btnCalCond->setEnabled(true);
comboRange->setEnabled(true);
comboLPF->setEnabled(true);
lsvBlankBtn->setEnabled(true);
lsvSampleBtn->setEnabled(true);
spinLsvStart->setEnabled(true);
spinLsvStop->setEnabled(true);
spinLsvSteps->setEnabled(true);
spinLsvDuration->setEnabled(true);
// Sync LPF
onLPFChanged(comboLPF->currentIndex());
} else {
logWidget->append(">> Connection Error: " + serial->errorString());
}
}
void MainWindow::onPortError(QSerialPort::SerialPortError error) {
if (error == QSerialPort::ResourceError) {
logWidget->append(">> Critical Error: Connection Lost.");
serial->close();
connectBtn->setText("Connect");
checkIdBtn->setEnabled(false);
calibrateBtn->setEnabled(false);
sweepBtn->setEnabled(false);
measureBtn->setEnabled(false);
spinSweepStart->setEnabled(false);
spinSweepStop->setEnabled(false);
spinSweepPPD->setEnabled(false);
ampBtn->setEnabled(false);
spinAmpBias->setEnabled(false);
btnCalCond->setEnabled(false);
comboRange->setEnabled(false);
comboLPF->setEnabled(false);
lsvBlankBtn->setEnabled(false);
lsvSampleBtn->setEnabled(false);
spinLsvStart->setEnabled(false);
spinLsvStop->setEnabled(false);
spinLsvSteps->setEnabled(false);
spinLsvDuration->setEnabled(false);
setButtonBlinking(nullptr, false);
}
}
void MainWindow::handleSerialData() {
while (serial->canReadLine()) {
QByteArray line = serial->readLine();
QString str = QString::fromUtf8(line).trimmed();
if (str.isEmpty()) continue;
QString timestamp = QDateTime::currentDateTime().toString("HH:mm:ss.zzz");
logWidget->append(QString("[%1] %2").arg(timestamp, str));
logWidget->moveCursor(QTextCursor::End);
if (str.startsWith("DATA,")) {
parseData(str);
} else if (str.startsWith("AMP,")) {
parseData(str);
} else if (str.startsWith("RAMP,")) {
parseData(str);
} else if (str == "STOPPED") {
// Reset UI state
if (lsvState != LSV_IDLE) stopLSV();
if (isSweeping) {
isSweeping = false;
sweepBtn->setText("Sweep");
setButtonBlinking(nullptr, false);
}
if (isMeasuringImp) {
isMeasuringImp = false;
measureBtn->setText("Measure");
setButtonBlinking(nullptr, false);
}
if (isMeasuringAmp) {
isMeasuringAmp = false;
ampBtn->setText("Start Amp");
setButtonBlinking(nullptr, false);
}
}
}
}
void MainWindow::parseData(const QString &data) {
QStringList parts = data.split(',');
if (parts[0] == "AMP" && parts.size() >= 3) {
bool okIdx, okCurr;
double index = parts[1].toDouble(&okIdx);
double current = parts[2].toDouble(&okCurr);
if (okIdx && okCurr) ampGraph->addAmperometricData(index, current);
return;
}
if (parts[0] == "RAMP" && parts.size() >= 3) {
bool okIdx, okCurr;
double index = parts[1].toDouble(&okIdx);
double current = parts[2].toDouble(&okCurr);
if (okIdx && okCurr) {
// Calculate Voltage based on UI parameters (Host-side calculation)
double start = spinLsvStart->value();
double stop = spinLsvStop->value();
int steps = spinLsvSteps->value();
double voltage = start + (index * (stop - start) / steps);
if (lsvState == LSV_RUNNING_BLANK) {
lsvGraph->addLSVData(voltage, current, GraphWidget::LSV_BLANK);
lsvBlankData.append({voltage, current});
} else if (lsvState == LSV_RUNNING_SAMPLE) {
lsvGraph->addLSVData(voltage, current, GraphWidget::LSV_SAMPLE);
lsvSampleData.append({voltage, current});
}
}
return;
}
if (parts[0] == "DATA" && parts.size() >= 6) {
bool okF, okR, okI;
double freq = parts[1].toDouble(&okF);
double real = parts[4].toDouble(&okR);
double imag = parts[5].toDouble(&okI);
if (okF && okR && okI) {
double real_plot = real;
double imag_plot = imag;
bool showCorr = false;
if (checkShunt->isChecked()) {
showCorr = true;
double r_shunt = spinShuntRes->value();
std::complex<double> z_meas(real, imag);
std::complex<double> z_shunt(r_shunt, 0.0);
std::complex<double> denom = z_shunt - z_meas;
if (std::abs(denom) > 1e-9) {
std::complex<double> z_cell = (z_meas * z_shunt) / denom;
real_plot = z_cell.real();
imag_plot = z_cell.imag();
} else {
real_plot = 1e9;
imag_plot = 0;
}
}
rawGraph->addBodeData(freq, real_plot, imag_plot);
nyquistGraph->addNyquistData(real, imag, real_plot, imag_plot, showCorr);
sweepFreqs.append(freq);
sweepReals.append(real_plot);
sweepImags.append(imag_plot);
if (sweepReals.size() > 10 && sweepReals.size() % 10 == 0) {
computeHilbert();
performCircleFit();
}
}
}
}

328
host/src/MainWindow_UI.cpp Normal file
View File

@ -0,0 +1,328 @@
// File: host/src/MainWindow_UI.cpp
#include "MainWindow.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QFrame>
#include <QScroller>
void MainWindow::setupUi() {
QWidget *central = new QWidget(this);
setCentralWidget(central);
QVBoxLayout *mainLayout = new QVBoxLayout(central);
mainLayout->setContentsMargins(5, 5, 5, 5);
mainLayout->setSpacing(5);
// --- Controls Container ---
QWidget *controlsContainer = new QWidget(this);
controlsContainer->setStyleSheet("background-color: #3A3A3A; border-radius: 5px;");
QVBoxLayout *ctrlLayout = new QVBoxLayout(controlsContainer);
ctrlLayout->setContentsMargins(5, 5, 5, 5);
ctrlLayout->setSpacing(5);
// Row 1
QHBoxLayout *row1 = new QHBoxLayout();
row1->setSpacing(8);
portSelector = new QComboBox(this);
portSelector->setMinimumWidth(120);
connectBtn = new QPushButton("Connect", this);
// Disconnect color (Red 50%) will be set when connected, default is Connect
connectBtn->setStyleSheet("background-color: rgba(255, 0, 0, 128); color: white; font-weight: bold;");
QPushButton *refreshBtn = new QPushButton("Refresh", this);
refreshBtn->setStyleSheet("background-color: rgba(255, 255, 255, 178); color: black; font-weight: bold;");
row1->addWidget(portSelector);
row1->addWidget(connectBtn);
row1->addWidget(refreshBtn);
QFrame *sep1 = new QFrame(); sep1->setFrameShape(QFrame::VLine); sep1->setFrameShadow(QFrame::Sunken);
row1->addWidget(sep1);
checkIdBtn = new QPushButton("Check ID", this);
checkIdBtn->setStyleSheet("background-color: rgba(255, 255, 255, 166); color: black; font-weight: bold;");
calibrateBtn = new QPushButton("Calibrate HW", this);
calibrateBtn->setStyleSheet("background-color: rgba(255, 255, 0, 102); color: white; font-weight: bold;");
row1->addWidget(checkIdBtn);
row1->addWidget(calibrateBtn);
QFrame *sep2 = new QFrame(); sep2->setFrameShape(QFrame::VLine); sep2->setFrameShadow(QFrame::Sunken);
row1->addWidget(sep2);
row1->addWidget(new QLabel("Start:"));
spinSweepStart = new QDoubleSpinBox(this);
spinSweepStart->setRange(0.1, 200000.0); spinSweepStart->setValue(1000.0); spinSweepStart->setSuffix(" Hz");
row1->addWidget(spinSweepStart);
row1->addWidget(new QLabel("Stop:"));
spinSweepStop = new QDoubleSpinBox(this);
spinSweepStop->setRange(0.1, 200000.0); spinSweepStop->setValue(200000.0); spinSweepStop->setSuffix(" Hz");
row1->addWidget(spinSweepStop);
row1->addWidget(new QLabel("PPD:"));
spinSweepPPD = new QSpinBox(this);
spinSweepPPD->setRange(1, 1000); spinSweepPPD->setValue(200); spinSweepPPD->setSuffix(" pts/dec");
row1->addWidget(spinSweepPPD);
sweepBtn = new QPushButton("Sweep", this);
sweepBtn->setStyleSheet("background-color: rgba(173, 216, 230, 76); color: white; font-weight: bold;");
row1->addWidget(sweepBtn);
QFrame *sep3 = new QFrame(); sep3->setFrameShape(QFrame::VLine); sep3->setFrameShadow(QFrame::Sunken);
row1->addWidget(sep3);
row1->addWidget(new QLabel("Shunt:"));
checkShunt = new QCheckBox("Enable", this);
row1->addWidget(checkShunt);
spinShuntRes = new QDoubleSpinBox(this);
spinShuntRes->setRange(1.0, 1000000.0); spinShuntRes->setValue(466.0); spinShuntRes->setSuffix(" Ω");
row1->addWidget(spinShuntRes);
QFrame *sep4 = new QFrame(); sep4->setFrameShape(QFrame::VLine); sep4->setFrameShadow(QFrame::Sunken);
row1->addWidget(sep4);
row1->addWidget(new QLabel("Std Cond:"));
spinCondStd = new QDoubleSpinBox(this);
spinCondStd->setRange(0.0, 1000000.0); spinCondStd->setValue(1413.0); spinCondStd->setSuffix(" µS/cm");
row1->addWidget(spinCondStd);
btnCalCond = new QPushButton("Calibrate K", this);
btnCalCond->setStyleSheet("background-color: rgba(255, 165, 0, 102); color: white; font-weight: bold;");
row1->addWidget(btnCalCond);
lblResultRs = new QLabel(" Rs: -- Ω", this);
lblResultRs->setStyleSheet("font-weight: bold; color: #FFD700; font-size: 14px; margin-left: 10px;");
row1->addWidget(lblResultRs);
row1->addStretch();
ctrlLayout->addLayout(row1);
// Row 2
QHBoxLayout *row2 = new QHBoxLayout();
row2->setSpacing(8);
row2->addWidget(new QLabel("Range:"));
comboRange = new QComboBox(this);
// Full 26 Resistor List
comboRange->addItem("200 Ω", 200);
comboRange->addItem("1 kΩ", 1000);
comboRange->addItem("2 kΩ", 2000);
comboRange->addItem("3 kΩ", 3000);
comboRange->addItem("4 kΩ", 4000);
comboRange->addItem("6 kΩ", 6000);
comboRange->addItem("8 kΩ", 8000);
comboRange->addItem("10 kΩ", 10000);
comboRange->addItem("12 kΩ", 12000);
comboRange->addItem("16 kΩ", 16000);
comboRange->addItem("20 kΩ", 20000);
comboRange->addItem("24 kΩ", 24000);
comboRange->addItem("30 kΩ", 30000);
comboRange->addItem("32 kΩ", 32000);
comboRange->addItem("40 kΩ", 40000);
comboRange->addItem("48 kΩ", 48000);
comboRange->addItem("64 kΩ", 64000);
comboRange->addItem("85 kΩ", 85000);
comboRange->addItem("96 kΩ", 96000);
comboRange->addItem("100 kΩ", 100000);
comboRange->addItem("120 kΩ", 120000);
comboRange->addItem("128 kΩ", 128000);
comboRange->addItem("160 kΩ", 160000);
comboRange->addItem("196 kΩ", 196000);
comboRange->addItem("256 kΩ", 256000);
comboRange->addItem("512 kΩ", 512000);
comboRange->setCurrentIndex(1); // Default 1k
row2->addWidget(comboRange);
QFrame *sepRange = new QFrame(); sepRange->setFrameShape(QFrame::VLine); sepRange->setFrameShadow(QFrame::Sunken);
row2->addWidget(sepRange);
lblResultCond = new QLabel("Cond: -- µS/cm", this);
lblResultCond->setStyleSheet("font-weight: bold; color: #00FFFF; font-size: 14px; margin-right: 10px;");
row2->addWidget(lblResultCond);
QFrame *sep5 = new QFrame(); sep5->setFrameShape(QFrame::VLine); sep5->setFrameShadow(QFrame::Sunken);
row2->addWidget(sep5);
row2->addWidget(new QLabel("Freq:"));
spinFreq = new QDoubleSpinBox(this);
spinFreq->setRange(0.1, 200000.0); spinFreq->setValue(1000.0); spinFreq->setSuffix(" Hz");
row2->addWidget(spinFreq);
measureBtn = new QPushButton("Measure", this);
measureBtn->setStyleSheet("background-color: rgba(0, 100, 0, 76); color: white; font-weight: bold;");
row2->addWidget(measureBtn);
QFrame *sep6 = new QFrame(); sep6->setFrameShape(QFrame::VLine); sep6->setFrameShadow(QFrame::Sunken);
row2->addWidget(sep6);
row2->addWidget(new QLabel("Bias:"));
spinAmpBias = new QDoubleSpinBox(this);
spinAmpBias->setRange(-1100.0, 1100.0); spinAmpBias->setValue(0.0); spinAmpBias->setSuffix(" mV");
row2->addWidget(spinAmpBias);
row2->addWidget(new QLabel("LPF:"));
comboLPF = new QComboBox(this);
comboLPF->addItem("Bypass", 0);
comboLPF->addItem("20kΩ (8Hz)", 1);
comboLPF->addItem("100kΩ (1.6Hz)", 2);
comboLPF->addItem("200kΩ (0.8Hz)", 3);
comboLPF->addItem("400kΩ (0.4Hz)", 4);
comboLPF->addItem("600kΩ (0.26Hz)", 5);
comboLPF->addItem("1MΩ (0.16Hz)", 6);
comboLPF->setCurrentIndex(1); // Default 20k
row2->addWidget(comboLPF);
ampBtn = new QPushButton("Start Amp", this);
ampBtn->setStyleSheet("background-color: rgba(238, 130, 238, 51); color: white; font-weight: bold;");
row2->addWidget(ampBtn);
QFrame *sep7 = new QFrame(); sep7->setFrameShape(QFrame::VLine); sep7->setFrameShadow(QFrame::Sunken);
row2->addWidget(sep7);
// LSV Controls
row2->addWidget(new QLabel("LSV:"));
spinLsvStart = new QDoubleSpinBox(this);
spinLsvStart->setRange(-1100.0, 1100.0); spinLsvStart->setValue(800.0); spinLsvStart->setSuffix(" mV");
row2->addWidget(spinLsvStart);
spinLsvStop = new QDoubleSpinBox(this);
spinLsvStop->setRange(-1100.0, 1100.0); spinLsvStop->setValue(-200.0); spinLsvStop->setSuffix(" mV");
row2->addWidget(spinLsvStop);
spinLsvSteps = new QSpinBox(this);
spinLsvSteps->setRange(10, 4000); spinLsvSteps->setValue(200); spinLsvSteps->setSuffix(" pts");
row2->addWidget(spinLsvSteps);
spinLsvDuration = new QSpinBox(this);
spinLsvDuration->setRange(100, 600000); spinLsvDuration->setValue(10000); spinLsvDuration->setSuffix(" ms");
row2->addWidget(spinLsvDuration);
lsvBlankBtn = new QPushButton("Run Blank", this);
lsvBlankBtn->setStyleSheet("background-color: rgba(0, 0, 0, 255); color: white; font-weight: bold;");
row2->addWidget(lsvBlankBtn);
lsvSampleBtn = new QPushButton("Run Sample", this);
lsvSampleBtn->setStyleSheet("background-color: rgba(75, 0, 130, 76); color: white; font-weight: bold;");
row2->addWidget(lsvSampleBtn);
row2->addStretch();
ctrlLayout->addLayout(row2);
mainLayout->addWidget(controlsContainer);
// --- Splitter for Graphs and Log ---
QSplitter *splitter = new QSplitter(Qt::Vertical, this);
tabWidget = new QTabWidget(this);
rawGraph = new GraphWidget(this);
nyquistGraph = new GraphWidget(this);
ampGraph = new GraphWidget(this);
lsvGraph = new GraphWidget(this);
rawGraph->configureRawPlot();
nyquistGraph->configureNyquistPlot();
ampGraph->configureAmperometricPlot();
lsvGraph->configureLSVPlot();
tabWidget->addTab(rawGraph, "Raw Data");
tabWidget->addTab(nyquistGraph, "Nyquist Plot");
tabWidget->addTab(ampGraph, "Amperometry");
tabWidget->addTab(lsvGraph, "Voltammetry");
logWidget = new QTextEdit(this);
logWidget->setReadOnly(true);
logWidget->setFont(QFont("Monospace"));
logWidget->setPlaceholderText("Scanning for 0xCAFE EIS Device...");
logWidget->setStyleSheet("background-color: #1E1E1E; color: #00FF00; border: 1px solid #444;");
QScroller::grabGesture(logWidget->viewport(), QScroller::TouchGesture);
splitter->addWidget(tabWidget);
splitter->addWidget(logWidget);
splitter->setStretchFactor(0, 3);
splitter->setStretchFactor(1, 1);
mainLayout->addWidget(splitter);
// Initial State
checkIdBtn->setEnabled(false);
calibrateBtn->setEnabled(false);
sweepBtn->setEnabled(false);
measureBtn->setEnabled(false);
spinSweepStart->setEnabled(false);
spinSweepStop->setEnabled(false);
spinSweepPPD->setEnabled(false);
ampBtn->setEnabled(false);
spinAmpBias->setEnabled(false);
btnCalCond->setEnabled(false);
comboRange->setEnabled(false);
comboLPF->setEnabled(false);
lsvBlankBtn->setEnabled(false);
lsvSampleBtn->setEnabled(false);
spinLsvStart->setEnabled(false);
spinLsvStop->setEnabled(false);
spinLsvSteps->setEnabled(false);
spinLsvDuration->setEnabled(false);
// Connections
connect(connectBtn, &QPushButton::clicked, this, &MainWindow::connectToPort);
connect(refreshBtn, &QPushButton::clicked, this, &MainWindow::refreshPorts);
connect(checkIdBtn, &QPushButton::clicked, this, &MainWindow::checkDeviceId);
connect(calibrateBtn, &QPushButton::clicked, this, &MainWindow::runCalibration);
connect(sweepBtn, &QPushButton::clicked, this, &MainWindow::startSweep);
connect(measureBtn, &QPushButton::clicked, this, &MainWindow::toggleMeasurement);
connect(ampBtn, &QPushButton::clicked, this, &MainWindow::toggleAmperometry);
connect(lsvBlankBtn, &QPushButton::clicked, this, &MainWindow::startLSVBlank);
connect(lsvSampleBtn, &QPushButton::clicked, this, &MainWindow::startLSVSample);
connect(btnCalCond, &QPushButton::clicked, this, &MainWindow::calibrateCellConstant);
connect(comboLPF, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &MainWindow::onLPFChanged);
connect(tabWidget, &QTabWidget::currentChanged, this, [this](int index){
if (index == 0) rawGraph->configureRawPlot();
else if (index == 1) nyquistGraph->configureNyquistPlot();
else if (index == 2) ampGraph->configureAmperometricPlot();
else if (index == 3) lsvGraph->configureLSVPlot();
});
}
void MainWindow::setButtonBlinking(QPushButton *btn, bool blinking) {
if (activeButton && activeButton != btn) {
// Restore original colors manually
if (activeButton == sweepBtn) activeButton->setStyleSheet("background-color: rgba(173, 216, 230, 76); color: white; font-weight: bold;");
else if (activeButton == measureBtn) activeButton->setStyleSheet("background-color: rgba(0, 100, 0, 76); color: white; font-weight: bold;");
else if (activeButton == ampBtn) activeButton->setStyleSheet("background-color: rgba(238, 130, 238, 51); color: white; font-weight: bold;");
else if (activeButton == lsvBlankBtn) activeButton->setStyleSheet("background-color: rgba(0, 0, 0, 255); color: white; font-weight: bold;");
else if (activeButton == lsvSampleBtn) activeButton->setStyleSheet("background-color: rgba(75, 0, 130, 76); color: white; font-weight: bold;");
}
activeButton = btn;
if (blinking) {
blinkTimer->start();
} else {
blinkTimer->stop();
if (activeButton) {
// Restore original colors manually
if (activeButton == sweepBtn) activeButton->setStyleSheet("background-color: rgba(173, 216, 230, 76); color: white; font-weight: bold;");
else if (activeButton == measureBtn) activeButton->setStyleSheet("background-color: rgba(0, 100, 0, 76); color: white; font-weight: bold;");
else if (activeButton == ampBtn) activeButton->setStyleSheet("background-color: rgba(238, 130, 238, 51); color: white; font-weight: bold;");
else if (activeButton == lsvBlankBtn) activeButton->setStyleSheet("background-color: rgba(0, 0, 0, 255); color: white; font-weight: bold;");
else if (activeButton == lsvSampleBtn) activeButton->setStyleSheet("background-color: rgba(75, 0, 130, 76); color: white; font-weight: bold;");
}
activeButton = nullptr;
}
}
void MainWindow::onBlinkTimer() {
if (!activeButton) return;
blinkState = !blinkState;
if (blinkState) {
activeButton->setStyleSheet("background-color: #FF0000; color: white; border: 1px solid #FF4444; font-weight: bold;");
} else {
activeButton->setStyleSheet("background-color: #880000; color: white; border: 1px solid #AA0000; font-weight: bold;");
}
}

70
main.c
View File

@ -41,17 +41,31 @@ typedef enum {
} AppMode; } AppMode;
AppMode CurrentMode = MODE_IDLE; AppMode CurrentMode = MODE_IDLE;
float LFOSCFreq = 32000.0; // Default, updated by calibration float LFOSCFreq = 32000.0;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Range / RTIA Management // Range / RTIA Management
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Nominal values for the 8 supported ranges // Full list of 26 supported ranges
float RtiaCalibrationTable[8] = {200.0, 1000.0, 5000.0, 10000.0, 20000.0, 40000.0, 80000.0, 160000.0}; float RtiaCalibrationTable[26] = {
uint32_t CurrentRtiaIndex = 0; // Default to 200 Ohm (Index 0) 200.0, 1000.0, 2000.0, 3000.0, 4000.0, 6000.0, 8000.0,
uint32_t ConfigRtiaVal = 200; // Default Value 10000.0, 12000.0, 16000.0, 20000.0, 24000.0, 30000.0, 32000.0,
uint32_t CurrentLpTiaRf = LPTIARF_20K; // Default LPF 40000.0, 48000.0, 64000.0, 85000.0, 96000.0, 100000.0,
120000.0, 128000.0, 160000.0, 196000.0, 256000.0, 512000.0
};
uint32_t CurrentRtiaIndex = 1; // Default 1k
uint32_t ConfigRtiaVal = 1000;
uint32_t CurrentLpTiaRf = LPTIARF_20K;
// Helper to find index for calibration table
int GetRtiaIndex(uint32_t val) {
for(int i=0; i<26; i++) {
if((uint32_t)RtiaCalibrationTable[i] == val) return i;
}
return 1; // Default 1k
}
// Map integer value to HSTIA Enum (Impedance) // Map integer value to HSTIA Enum (Impedance)
uint32_t GetHSTIARtia(uint32_t val) { uint32_t GetHSTIARtia(uint32_t val) {
@ -64,7 +78,8 @@ uint32_t GetHSTIARtia(uint32_t val) {
case 40000: return HSTIARTIA_40K; case 40000: return HSTIARTIA_40K;
case 80000: return HSTIARTIA_80K; case 80000: return HSTIARTIA_80K;
case 160000: return HSTIARTIA_160K; case 160000: return HSTIARTIA_160K;
default: return HSTIARTIA_200; // Note: HSTIA has fewer options than LPTIA. Map closest or default.
default: return HSTIARTIA_1K;
} }
} }
@ -73,31 +88,34 @@ uint32_t GetLPTIARtia(uint32_t val) {
switch(val) { switch(val) {
case 200: return LPTIARTIA_200R; case 200: return LPTIARTIA_200R;
case 1000: return LPTIARTIA_1K; case 1000: return LPTIARTIA_1K;
case 5000: return LPTIARTIA_4K; case 2000: return LPTIARTIA_2K;
case 3000: return LPTIARTIA_3K;
case 4000: return LPTIARTIA_4K;
case 6000: return LPTIARTIA_6K;
case 8000: return LPTIARTIA_8K;
case 10000: return LPTIARTIA_10K; case 10000: return LPTIARTIA_10K;
case 12000: return LPTIARTIA_12K;
case 16000: return LPTIARTIA_16K;
case 20000: return LPTIARTIA_20K; case 20000: return LPTIARTIA_20K;
case 24000: return LPTIARTIA_24K;
case 30000: return LPTIARTIA_30K;
case 32000: return LPTIARTIA_32K;
case 40000: return LPTIARTIA_40K; case 40000: return LPTIARTIA_40K;
case 80000: return LPTIARTIA_85K; case 48000: return LPTIARTIA_48K;
case 64000: return LPTIARTIA_64K;
case 85000: return LPTIARTIA_85K;
case 96000: return LPTIARTIA_96K;
case 100000: return LPTIARTIA_100K;
case 120000: return LPTIARTIA_120K;
case 128000: return LPTIARTIA_128K;
case 160000: return LPTIARTIA_160K; case 160000: return LPTIARTIA_160K;
case 196000: return LPTIARTIA_196K;
case 256000: return LPTIARTIA_256K;
case 512000: return LPTIARTIA_512K;
default: return LPTIARTIA_1K; default: return LPTIARTIA_1K;
} }
} }
// Helper to find index for calibration table
int GetRtiaIndex(uint32_t val) {
switch(val) {
case 200: return 0;
case 1000: return 1;
case 5000: return 2;
case 10000: return 3;
case 20000: return 4;
case 40000: return 5;
case 80000: return 6;
case 160000: return 7;
default: return 0;
}
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Platform Interface Implementation // Platform Interface Implementation
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -194,7 +212,7 @@ void AD5940AMPStructInit(void)
pAMPCfg->SensorBias = 0; pAMPCfg->SensorBias = 0;
pAMPCfg->LptiaRtiaSel = GetLPTIARtia(ConfigRtiaVal); pAMPCfg->LptiaRtiaSel = GetLPTIARtia(ConfigRtiaVal);
pAMPCfg->LpTiaRl = LPTIARLOAD_10R; pAMPCfg->LpTiaRl = LPTIARLOAD_10R;
pAMPCfg->LpTiaRf = CurrentLpTiaRf; // Use configured LPF pAMPCfg->LpTiaRf = CurrentLpTiaRf;
pAMPCfg->Vzero = 1100; pAMPCfg->Vzero = 1100;
pAMPCfg->ADCRefVolt = 1.82; pAMPCfg->ADCRefVolt = 1.82;
} }
@ -222,7 +240,7 @@ void AD5940RampStructInit(void)
pRampCfg->LPTIARtiaSel = GetLPTIARtia(ConfigRtiaVal); pRampCfg->LPTIARtiaSel = GetLPTIARtia(ConfigRtiaVal);
pRampCfg->LPTIARloadSel = LPTIARLOAD_10R; pRampCfg->LPTIARloadSel = LPTIARLOAD_10R;
pRampCfg->LpTiaRf = CurrentLpTiaRf; // Use configured LPF pRampCfg->LpTiaRf = CurrentLpTiaRf;
pRampCfg->AdcPgaGain = ADCPGA_1P5; pRampCfg->AdcPgaGain = ADCPGA_1P5;
} }