EIS/host/src/MainWindow.cpp

394 lines
12 KiB
C++

// File: host/src/MainWindow.cpp
#include "MainWindow.h"
#include <QVBoxLayout>
#include <QDebug>
#include <QDateTime>
#include <QScroller>
#include <QGesture>
#include <QSplitter>
#include <QDoubleSpinBox>
#include <QLabel>
#include <QTimer>
#include <cmath>
#include <complex>
#include <vector>
// Simple FFT Implementation (Cooley-Tukey)
// Note: Size must be power of 2
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();
// 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 <start> <end> <steps>
// 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<std::complex<double>> signal(n_fft);
for (int i = 0; i < n; i++) {
signal[i] = std::complex<double>(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<double> 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<QGestureEvent*>(event);
if (QGesture *swipe = ge->gesture(Qt::SwipeGesture)) {
handleSwipe(static_cast<QSwipeGesture*>(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);
}
}
}