working up to it
This commit is contained in:
parent
6adf55bc47
commit
3e78b4eb75
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
};
|
};
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
70
main.c
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue