UPGRADES UPGRADES UPGRADES

This commit is contained in:
pszsh 2026-02-02 06:18:38 -08:00
parent 2945835eeb
commit ce6f8e6a46
6 changed files with 141 additions and 182 deletions

4
.sdkmanrc Normal file
View File

@ -0,0 +1,4 @@
# Enable auto-env through the sdkman_auto_env config
# Add key=value pairs of SDKs to use below
java=17-homebrew
gradle=9.2.1

View File

@ -18,7 +18,7 @@ AppRAMPCfg_Type AppRAMPCfg =
.LFOSCClkFreq = 32000.0,
.SysClkFreq = 16000000.0,
.AdcClkFreq = 16000000.0,
.RcalVal = 10000.0,
.RcalVal = 100.0,
.ADCRefVolt = 1820.0f,
.bTestFinished = bFALSE,
@ -64,7 +64,14 @@ AD5940Err AppRAMPCtrl(uint32_t Command, void *pPara)
if(AD5940_WakeUp(10) > 10) return AD5940ERR_WAKEUP;
if(AppRAMPCfg.RAMPInited == bFALSE) return AD5940ERR_APPERROR;
if(AppRAMPCfg.RampState == RAMP_STOP) return AD5940ERR_APPERROR;
// --- CRITICAL FIX: Reset State on Start ---
AppRAMPCfg.RampState = RAMP_STATE0;
AppRAMPCfg.CurrStepPos = 0;
AppRAMPCfg.bFirstDACSeq = bTRUE;
AppRAMPCfg.StopRequired = bFALSE;
AppRAMPCfg.FifoThresh = 4;
// ------------------------------------------
wupt_cfg.WuptEn = bTRUE;
wupt_cfg.WuptEndSeq = WUPTENDSEQ_D;

View File

@ -1,4 +1,5 @@
# File: host/CMakeLists.txt
# host/CMakeLists.txt
cmake_minimum_required(VERSION 3.18)
project(EISConfigurator VERSION 1.0 LANGUAGES CXX C)
@ -20,22 +21,17 @@ 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)
# --- FFTW3 Configuration (Double Precision) ---
# ==========================================
# --- FFTW3 CONFIGURATION ---
# ==========================================
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)
# Windows: Expects FFTW3 to be installed/found via Config
find_package(FFTW3 CONFIG REQUIRED)
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).")
@ -52,10 +48,6 @@ elseif(APPLE AND CMAKE_SYSTEM_PROCESSOR STREQUAL "arm64" AND NOT BUILD_IOS)
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)
@ -69,17 +61,15 @@ if(BUILD_FFTW_FROM_SOURCE)
set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build Static Libs" FORCE)
set(BUILD_TESTS OFF CACHE BOOL "Disable Tests" FORCE)
# Enhanced NEON detection
# Enable NEON for Android ARM64 / iOS
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
# Patch for older CMake versions inside the tarball if needed
if(UNIX)
set(PATCH_CMD sed -i.bak "s/cmake_minimum_required.*/cmake_minimum_required(VERSION 3.16)/" <SOURCE_DIR>/CMakeLists.txt)
else()
@ -96,26 +86,18 @@ if(BUILD_FFTW_FROM_SOURCE)
FetchContent_MakeAvailable(fftw3_source)
endif()
# --- QCustomPlot Integration ---
# ==========================================
# --- QCUSTOMPLOT ---
# ==========================================
FetchContent_Declare(
QCustomPlot
URL https://www.qcustomplot.com/release/2.1.1/QCustomPlot.tar.gz
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
)
FetchContent_MakeAvailable(QCustomPlot)
FetchContent_GetProperties(QCustomPlot)
if(NOT qcustomplot_POPULATED)
FetchContent_Populate(QCustomPlot)
add_library(QCustomPlot
${qcustomplot_SOURCE_DIR}/qcustomplot.cpp
${qcustomplot_SOURCE_DIR}/qcustomplot.h
)
target_link_libraries(QCustomPlot PUBLIC Qt6::Core Qt6::Gui Qt6::Widgets Qt6::PrintSupport)
target_include_directories(QCustomPlot PUBLIC ${qcustomplot_SOURCE_DIR})
endif()
# --- Icon Generation ---
# ==========================================
# --- ICON GENERATION ---
# ==========================================
set(ICON_SOURCE "${CMAKE_CURRENT_SOURCE_DIR}/assets/icon_source.png")
@ -125,56 +107,36 @@ if(NOT MAGICK_EXECUTABLE)
endif()
if(EXISTS "${ICON_SOURCE}" AND MAGICK_EXECUTABLE)
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
set(ICON_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_icons.bat")
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}"
COMMAND "${ICON_SCRIPT}" "${MAGICK_EXECUTABLE}" "${ICON_SOURCE}" "${WINDOWS_ICON}"
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
DEPENDS "${ICON_SOURCE}" "${ICON_SCRIPT}"
COMMENT "Generating Icons..."
COMMENT "Generating Windows Icon..."
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(IOS_CONTENTS_JSON "${IOS_ASSETS_PATH}/Contents.json")
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}"
OUTPUT "${MACOS_ICON}" "${IOS_CONTENTS_JSON}" "${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}")
add_custom_target(GenerateIcons DEPENDS "${MACOS_ICON}" "${IOS_CONTENTS_JSON}" "${ANDROID_RES_PATH}")
endif()
endif()
@ -184,9 +146,10 @@ set(PROJECT_SOURCES
src/main.cpp
src/MainWindow.cpp
src/MainWindow_UI.cpp
src/MainWindow_Serial.cpp
src/MainWindow_Actions.cpp
src/MainWindow_Serial.cpp
src/GraphWidget.cpp
${qcustomplot_SOURCE_DIR}/qcustomplot.cpp
)
if(EXISTS "${ICON_SOURCE}")
@ -200,22 +163,27 @@ endif()
set(PROJECT_HEADERS
src/MainWindow.h
src/GraphWidget.h
${qcustomplot_SOURCE_DIR}/qcustomplot.h
)
if(ANDROID)
add_library(EISConfigurator SHARED ${PROJECT_SOURCES} ${PROJECT_HEADERS})
else()
add_executable(EISConfigurator ${PROJECT_SOURCES} ${PROJECT_HEADERS})
endif()
# Use qt_add_executable for proper Android/iOS handling
qt_add_executable(EISConfigurator MANUAL_FINALIZATION ${PROJECT_SOURCES} ${PROJECT_HEADERS})
if(EXISTS "${ICON_SOURCE}" AND MAGICK_EXECUTABLE)
add_dependencies(EISConfigurator GenerateIcons)
endif()
# --- Mobile Definitions ---
if(BUILD_ANDROID OR BUILD_IOS)
target_compile_definitions(EISConfigurator PRIVATE IS_MOBILE)
endif()
# --- Linking ---
# Handle FFTW3 Linking and Include Paths
if(TARGET fftw3)
set(FFTW_TARGET fftw3)
# If built from source via FetchContent, we need to manually add include dirs
if(DEFINED fftw3_source_SOURCE_DIR)
target_include_directories(EISConfigurator PRIVATE
"${fftw3_source_SOURCE_DIR}/api"
@ -223,25 +191,26 @@ if(TARGET fftw3)
)
endif()
else()
# Fallback if target isn't defined (shouldn't happen with above logic)
set(FFTW_TARGET fftw3)
endif()
target_include_directories(EISConfigurator PRIVATE ${qcustomplot_SOURCE_DIR})
target_link_libraries(EISConfigurator PRIVATE
Qt6::Core Qt6::Gui Qt6::Widgets Qt6::SerialPort Qt6::PrintSupport
QCustomPlot
${FFTW_TARGET}
)
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_property(TARGET EISConfigurator PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android")
endif()
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"
@ -250,7 +219,6 @@ if(BUILD_IOS)
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}")
@ -271,6 +239,5 @@ elseif(WIN32)
)
endif()
if(NOT ANDROID)
qt_finalize_executable(EISConfigurator)
endif()
# CRITICAL: Triggers androiddeployqt to build the APK
qt_finalize_executable(EISConfigurator)

View File

@ -1,4 +1,4 @@
# Makefile
# host/Makefile
QT_ANDROID_KIT ?= $(HOME)/Qt/6.8.3/android_arm64_v8a
QT_IOS_KIT ?= $(HOME)/Qt/6.8.3/ios
@ -13,7 +13,7 @@ TARGET = EISConfigurator
# Android Specifics
PKG_NAME = org.qtproject.example.EISConfigurator
# CRITICAL FIX: Qt6 generates 'android-build-debug.apk' by default
# Qt6 generates 'android-build-debug.apk' by default
APK_PATH = $(BUILD_DIR_ANDROID)/android-build/build/outputs/apk/debug/android-build-debug.apk
all: macos

View File

@ -1,6 +1,6 @@
<?xml version="1.0"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.qtproject.example.YrCrystals"
package="org.qtproject.example.EISConfigurator"
android:versionName="1.0"
android:versionCode="1"
android:installLocation="auto">
@ -10,12 +10,12 @@
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
<uses-permission android:name="android.permission.INTERNET"/>
<application android:label="Yr Crystals"
<application android:label="EIS Configurator"
android:name="org.qtproject.qt.android.bindings.QtApplication"
android:requestLegacyExternalStorage="true">
<activity android:name="org.qtproject.qt.android.bindings.QtActivity"
android:label="Yr Crystals"
android:label="EIS Configurator"
android:screenOrientation="unspecified"
android:launchMode="singleTop"
android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density"
@ -26,7 +26,7 @@
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data android:name="android.app.lib_name" android:value="YrCrystals"/>
<meta-data android:name="android.app.lib_name" android:value="EISConfigurator"/>
</activity>
</application>
</manifest>

View File

@ -119,9 +119,16 @@ void MainWindow::toggleAmperometry() {
void MainWindow::startLSVBlank() {
if (!serial->isOpen()) return;
// Toggle Logic: If running, stop it.
if (lsvState == LSV_RUNNING_BLANK) {
stopLSV();
return;
}
if (isMeasuringImp) toggleMeasurement();
if (isMeasuringAmp) toggleAmperometry();
if (lsvState != LSV_IDLE) stopLSV();
if (lsvState != LSV_IDLE) stopLSV(); // Stop sample if running
if (isSweeping) startSweep();
double start = spinLsvStart->value();
@ -140,8 +147,9 @@ void MainWindow::startLSVBlank() {
setButtonBlinking(lsvBlankBtn, true);
lsvState = LSV_RUNNING_BLANK;
// Only clear if we are actually starting a new run
lsvGraph->clearLSV(GraphWidget::LSV_BLANK);
lsvGraph->clearLSV(GraphWidget::LSV_DIFF); // Clear old diff
lsvGraph->clearLSV(GraphWidget::LSV_DIFF);
lsvBlankData.clear();
tabWidget->setCurrentIndex(3);
@ -149,9 +157,16 @@ void MainWindow::startLSVBlank() {
void MainWindow::startLSVSample() {
if (!serial->isOpen()) return;
// Toggle Logic
if (lsvState == LSV_RUNNING_SAMPLE) {
stopLSV();
return;
}
if (isMeasuringImp) toggleMeasurement();
if (isMeasuringAmp) toggleAmperometry();
if (lsvState != LSV_IDLE) stopLSV();
if (lsvState != LSV_IDLE) stopLSV(); // Stop blank if running
if (isSweeping) startSweep();
double start = spinLsvStart->value();
@ -170,8 +185,9 @@ void MainWindow::startLSVSample() {
setButtonBlinking(lsvSampleBtn, true);
lsvState = LSV_RUNNING_SAMPLE;
// Only clear if we are actually starting a new run
lsvGraph->clearLSV(GraphWidget::LSV_SAMPLE);
lsvGraph->clearLSV(GraphWidget::LSV_DIFF); // Clear old diff
lsvGraph->clearLSV(GraphWidget::LSV_DIFF);
lsvSampleData.clear();
tabWidget->setCurrentIndex(3);
@ -282,108 +298,73 @@ 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;
// 1. Centering (Crucial for stability)
double meanX = 0, meanY = 0;
for(int i=0; i<n; i++) { meanX += sweepReals[i]; meanY += -sweepImags[i]; }
meanX /= n; meanY /= n;
double fraction = (0.0 - img1) / (img2 - img1);
double crossingReal = r1 + fraction * (r2 - r1);
if (!found || crossingReal < measuredRs) {
measuredRs = crossingReal;
found = true;
}
}
// 2. Kasa Fit (Algebraic) on Centered Data
// Minimizes sum((x^2 + y^2) - (2Ax + 2By + C))^2
double sum_x2 = 0, sum_y2 = 0, sum_xy = 0;
double sum_z = 0, sum_zx = 0, sum_zy = 0;
for(int i=0; i<n; i++) {
double xi = sweepReals[i] - meanX;
double yi = -sweepImags[i] - meanY;
double zi = xi*xi + yi*yi;
sum_x2 += xi*xi;
sum_y2 += yi*yi;
sum_xy += xi*yi;
sum_z += zi;
sum_zx += zi*xi;
sum_zy += zi*yi;
}
if (found && measuredRs > 0) {
lblResultRs->setText(QString(" Rs: %1 Ω (Meas)").arg(measuredRs, 0, 'f', 2));
// Solve 3x3 Linear System (Normal Equations) for Centered Kasa
// [ 4*sum_x2 4*sum_xy 0 ] [ A ] [ 2*sum_zx ]
// [ 4*sum_xy 4*sum_y2 0 ] [ B ] = [ 2*sum_zy ]
// [ 0 0 n ] [ C ] [ sum_z ]
double C = sum_z / n;
// Solve 2x2 for A, B
double D = 16 * (sum_x2 * sum_y2 - sum_xy * sum_xy);
if (std::abs(D) < 1e-9) return; // Collinear or insufficient data
double A = (2 * sum_zx * 4 * sum_y2 - 2 * sum_zy * 4 * sum_xy) / D;
double B = (4 * sum_x2 * 2 * sum_zy - 4 * sum_xy * 2 * sum_zx) / D;
double xc = A + meanX;
double yc = B + meanY;
double r_sq = A*A + B*B + C;
if (r_sq <= 0) return;
double r = std::sqrt(r_sq);
// Calculate Intercepts with Real Axis (y=0)
// (x - xc)^2 + (0 - yc)^2 = r^2
// (x - xc)^2 = r^2 - yc^2
double term = r*r - yc*yc;
if (term < 0) return; // Circle doesn't intersect real axis
double x1 = xc - std::sqrt(term);
double x2 = xc + std::sqrt(term);
double Rs = std::min(x1, x2);
if (Rs < 0) Rs = std::max(x1, x2); // If one is negative, take the positive one
if (Rs > 0) {
lblResultRs->setText(QString(" Rs: %1 Ω").arg(Rs, 0, 'f', 2));
double cond = (cellConstant / measuredRs) * 1000000.0;
double cond = (cellConstant / Rs) * 1000000.0;
lblResultCond->setText(QString(" Cond: %1 µS/cm").arg(cond, 0, 'f', 2));
nyquistGraph->setExtrapolatedPoint(measuredRs, 0);
return;
nyquistGraph->setExtrapolatedPoint(Rs, 0);
}
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() {