394 lines
12 KiB
C++
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);
|
|
}
|
|
}
|
|
} |