// File: host/src/MainWindow.cpp #include "MainWindow.h" #include #include #include #include #include #include #include #include #include #include #include #include // Simple FFT Implementation (Cooley-Tukey) // Note: Size must be power of 2 void fft(std::vector>& a) { int n = a.size(); if (n <= 1) return; std::vector> 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 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>& a) { int n = a.size(); // Conjugate for (int i = 0; i < n; i++) a[i] = std::conj(a[i]); // Forward FFT fft(a); // Conjugate again and scale for (int i = 0; i < n; i++) { a[i] = std::conj(a[i]); a[i] /= n; } } MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { serial = new QSerialPort(this); connect(serial, &QSerialPort::readyRead, this, &MainWindow::handleSerialData); connect(serial, &QSerialPort::errorOccurred, this, &MainWindow::onPortError); setupUi(); // Delayed refresh to allow the window to render before auto-scanning QTimer::singleShot(1000, this, &MainWindow::refreshPorts); // Enable Swipe Gestures for Mobile Tab Switching grabGesture(Qt::SwipeGesture); } MainWindow::~MainWindow() { if (serial->isOpen()) { serial->close(); } } void MainWindow::setupUi() { // Central Layout: Splitter (Graphs Top, Log Bottom) QSplitter *splitter = new QSplitter(Qt::Vertical, this); setCentralWidget(splitter); // Tab Widget for Graphs tabWidget = new QTabWidget(this); rawGraph = new GraphWidget(this); rawGraph->configureRawPlot(); nyquistGraph = new GraphWidget(this); nyquistGraph->configureNyquistPlot(); // Raw Data is now Default (Index 0) tabWidget->addTab(rawGraph, "Raw Data"); tabWidget->addTab(nyquistGraph, "Nyquist Plot"); // Log Widget logWidget = new QTextEdit(this); logWidget->setReadOnly(true); logWidget->setFont(QFont("Monospace")); logWidget->setPlaceholderText("Scanning for 0xCAFE EIS Device..."); QScroller::grabGesture(logWidget->viewport(), QScroller::TouchGesture); splitter->addWidget(tabWidget); splitter->addWidget(logWidget); splitter->setStretchFactor(0, 2); splitter->setStretchFactor(1, 1); // Toolbar Construction toolbar = addToolBar("Connection"); toolbar->setMovable(false); portSelector = new QComboBox(this); portSelector->setMinimumWidth(150); connectBtn = new QPushButton("Connect", this); QPushButton *refreshBtn = new QPushButton("Refresh", this); toolbar->addWidget(portSelector); toolbar->addWidget(connectBtn); toolbar->addWidget(refreshBtn); toolbar->addSeparator(); checkIdBtn = new QPushButton("Check ID", this); calibrateBtn = new QPushButton("Calibrate", this); sweepBtn = new QPushButton("Sweep", this); toolbar->addWidget(checkIdBtn); toolbar->addWidget(calibrateBtn); toolbar->addWidget(sweepBtn); toolbar->addSeparator(); // Measurement Control QLabel *lblFreq = new QLabel(" Freq:", this); spinFreq = new QDoubleSpinBox(this); spinFreq->setRange(10.0, 200000.0); spinFreq->setValue(1000.0); spinFreq->setSuffix(" Hz"); measureBtn = new QPushButton("Measure", this); toolbar->addWidget(lblFreq); toolbar->addWidget(spinFreq); toolbar->addWidget(measureBtn); checkIdBtn->setEnabled(false); calibrateBtn->setEnabled(false); sweepBtn->setEnabled(false); measureBtn->setEnabled(false); // Signal 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); #if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) toolbar->setIconSize(QSize(32, 32)); QFont font = portSelector->font(); font.setPointSize(14); portSelector->setFont(font); connectBtn->setFont(font); refreshBtn->setFont(font); checkIdBtn->setFont(font); calibrateBtn->setFont(font); sweepBtn->setFont(font); measureBtn->setFont(font); spinFreq->setFont(font); #endif } void MainWindow::refreshPorts() { portSelector->clear(); const auto infos = QSerialPortInfo::availablePorts(); bool foundTarget = false; QString targetPort; for (const QSerialPortInfo &info : infos) { portSelector->addItem(info.portName()); // Check for 0xCAFE or "usbmodem" 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); } } // Auto-connect if found and not already connected 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); isMeasuring = false; measureBtn->setText("Measure"); 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); } 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); isMeasuring = false; measureBtn->setText("Measure"); } } void MainWindow::checkDeviceId() { if (serial->isOpen()) { logWidget->append(">> Checking ID (v)..."); serial->write("v\n"); } } void MainWindow::runCalibration() { if (serial->isOpen()) { logWidget->append(">> Running Calibration (c)..."); serial->write("c\n"); } } void MainWindow::startSweep() { if (!serial->isOpen()) return; rawGraph->clear(); nyquistGraph->clear(); // Clear accumulated data sweepFreqs.clear(); sweepReals.clear(); // Use Firmware Sweep Command: s // Example: 100Hz to 200kHz, 50 steps logWidget->append(">> Starting Firmware Sweep (s 100 200000 50)..."); serial->write("s 100 200000 50\n"); } void MainWindow::toggleMeasurement() { if (!serial->isOpen()) return; if (isMeasuring) { // Stop logWidget->append(">> Stopping Measurement (x)..."); serial->write("x\n"); measureBtn->setText("Measure"); isMeasuring = false; } else { // Start double freq = spinFreq->value(); logWidget->append(QString(">> Requesting Measure (m %1)...").arg(freq)); serial->write(QString("m %1\n").arg(freq).toUtf8()); measureBtn->setText("Stop"); isMeasuring = true; } } 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); } } } void MainWindow::parseData(const QString &data) { // Format: DATA,Freq,Mag,Phase,Real,Imag QStringList parts = data.split(','); if (parts.size() < 6) return; 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) { // Add to Raw Graph (Freq vs Real/Imag) rawGraph->addData(freq, real, imag); // Add to Nyquist Graph (Real vs Imag) nyquistGraph->addData(real, imag, 0); // Accumulate for Hilbert sweepFreqs.append(freq); sweepReals.append(real); // Check if sweep is done (e.g., 50 points) // For now, update Hilbert every 10 points or at end if (sweepReals.size() >= 50) { computeHilbert(); } } } void MainWindow::computeHilbert() { int n = sweepReals.size(); if (n == 0) return; // 1. Zero-pad to next power of 2 int n_fft = 1; while (n_fft < n) n_fft *= 2; std::vector> signal(n_fft); for (int i = 0; i < n; i++) { signal[i] = std::complex(sweepReals[i], 0.0); } // 2. FFT fft(signal); // 3. Create Analytic Signal in Frequency Domain // H[0] = H[0] // H[i] = 2*H[i] for 0 < i < N/2 // H[N/2] = H[N/2] // H[i] = 0 for N/2 < i < N 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; } // 4. IFFT ifft(signal); // 5. Extract Imaginary Part (Hilbert Transform) QVector hilbertImag; for (int i = 0; i < n; i++) { hilbertImag.append(signal[i].imag()); } // 6. Plot rawGraph->addHilbertData(sweepFreqs, hilbertImag); } bool MainWindow::event(QEvent *event) { if (event->type() == QEvent::Gesture) { QGestureEvent *ge = static_cast(event); if (QGesture *swipe = ge->gesture(Qt::SwipeGesture)) { handleSwipe(static_cast(swipe)); return true; } } return QMainWindow::event(event); } void MainWindow::handleSwipe(QSwipeGesture *gesture) { if (gesture->state() == Qt::GestureFinished) { if (gesture->horizontalDirection() == QSwipeGesture::Left) { if (tabWidget->currentIndex() < tabWidget->count() - 1) tabWidget->setCurrentIndex(tabWidget->currentIndex() + 1); } else if (gesture->horizontalDirection() == QSwipeGesture::Right) { if (tabWidget->currentIndex() > 0) tabWidget->setCurrentIndex(tabWidget->currentIndex() - 1); } } }