// File: host/src/MainWindow_Serial.cpp #include "MainWindow.h" #include #include 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 ---"); // 1. Disable Global Controls checkIdBtn->setEnabled(false); calibrateBtn->setEnabled(false); comboTiaRange->setEnabled(false); // 2. Disable Tabs mainTabWidget->widget(0)->setEnabled(false); mainTabWidget->widget(1)->setEnabled(false); mainTabWidget->widget(2)->setEnabled(false); isMeasuringImp = false; isMeasuringAmp = false; isSweeping = false; lsvState = LSV_IDLE; currentSequence = SEQ_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 ---"); // 1. Enable Global Controls checkIdBtn->setEnabled(true); calibrateBtn->setEnabled(true); comboTiaRange->setEnabled(true); // 2. Enable Tabs mainTabWidget->widget(0)->setEnabled(true); mainTabWidget->widget(1)->setEnabled(true); mainTabWidget->widget(2)->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); comboTiaRange->setEnabled(false); mainTabWidget->widget(0)->setEnabled(false); mainTabWidget->widget(1)->setEnabled(false); mainTabWidget->widget(2)->setEnabled(false); setButtonBlinking(nullptr, false); currentSequence = SEQ_IDLE; } } 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); // --- Sequence State Machine --- if (currentSequence == SEQ_WAIT_BOOT) { if (str.contains("AD5940LIB Version:v0.2.1")) { logWidget->append(">> Sequence: Boot detected. Configuring..."); // 1. Restore Settings int hpVal = comboTiaRange->currentData().toInt(); serial->write(QString("r %1\n").arg(hpVal).toUtf8()); serial->write(QString("t 0\n").toUtf8()); // 2. Start Calibration logWidget->append(">> Sequence: Calibrating..."); serial->write("c\n"); currentSequence = SEQ_WAIT_CALIB; } } else if (currentSequence == SEQ_WAIT_CALIB) { if (str.contains("RCAL,HSTIA")) { logWidget->append(">> Sequence: Calibration Done. Sending Command."); serial->write(pendingCommand.toUtf8()); serial->write("\n"); currentSequence = SEQ_IDLE; } } // ------------------------------ if (str.startsWith("DATA,")) { parseData(str); } else if (str.startsWith("RCAL,")) { 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] == "RCAL" && parts.size() >= 4) { QString type = parts[1]; if (type == "FAIL") { logWidget->append(QString(">> RCAL FAIL: %1").arg(data)); return; } bool okM, okP; double mag = parts[2].toDouble(&okM); double phase = parts[3].toDouble(&okP); if (okM && okP) { logWidget->append(QString(">> Calibration [%1]: Mag=%.2f Phase=%.2f") .arg(type) .arg(mag) .arg(phase)); // TODO: Store this if needed for UI display } return; } if (parts[0] == "DATA" && parts.size() >= 6) { bool okF, okM, okP; double freq = parts[1].toDouble(&okF); double mag = parts[4].toDouble(&okM); double phase = parts[5].toDouble(&okP); if (okF && okM && okP) { // 1. Plot Bode (Magnitude/Phase) // Note: Phase from AD5940 is in Radians. rawGraph->addBodeData(freq, mag, phase); // 2. Convert to Cartesian for Nyquist (Real/Imaginary) // Z = Mag * e^(j*Phase) = Mag * (cos(Phase) + j*sin(Phase)) // Note: Nyquist Plot expects Z' vs -Z''. std::complex z = std::polar(mag, phase); double real = z.real(); double imag = z.imag(); // This is Z''. GraphWidget negates it for the plot. nyquistGraph->addNyquistData(real, imag, real, imag, false); if (sweepFreqs.size() >= expectedSweepPoints) { // Discard extra points (Artifact removal) return; } sweepFreqs.append(freq); sweepReals.append(real); sweepImags.append(imag); if (sweepReals.size() > 10 && sweepReals.size() % 10 == 0) { computeHilbert(); performCircleFit(); } } } }