From e21a7f56508c461c0017189da1fed1c4c4f594e6 Mon Sep 17 00:00:00 2001 From: hturkmanovic Date: Wed, 7 Oct 2026 19:19:36 +0200 Subject: [PATCH 1/6] Initial version --- Source/Links/controllink.cpp | 152 ++++++++++++++++++- Source/Links/controllink.h | 14 ++ Source/OpenEPT.pro | 3 + Source/Windows/AddDevice/adddevicewnd.cpp | 92 +++++++++++- Source/Windows/AddDevice/adddevicewnd.h | 13 +- Source/Windows/AddDevice/adddevicewnd.ui | 110 ++++++++++++++ Source/Windows/Charger/chargerwnd.cpp | 170 ++++++++++++++++++++++ Source/Windows/Charger/chargerwnd.h | 52 +++++++ Source/openept.cpp | 56 ++++++- Source/openept.h | 4 +- 10 files changed, 655 insertions(+), 11 deletions(-) create mode 100644 Source/Windows/Charger/chargerwnd.cpp create mode 100644 Source/Windows/Charger/chargerwnd.h diff --git a/Source/Links/controllink.cpp b/Source/Links/controllink.cpp index d457065..3d65fa6 100644 --- a/Source/Links/controllink.cpp +++ b/Source/Links/controllink.cpp @@ -1,6 +1,7 @@ #include "controllink.h" #include #include +#include #ifdef Q_OS_WIN #include "Ws2tcpip.h" @@ -18,7 +19,10 @@ ControlLink::ControlLink(QObject *parent) { tcpSocket = new QTcpSocket(this); tcpSocket->setSocketOption(QAbstractSocket::KeepAliveOption, 1); + serialPort = nullptr; + transport = CONTROL_LINK_TRANSPORT_TCP; ipAddress = "0.0.0.0"; + serialPortName = ""; portNumber = 0; linkStatus = CONTROL_LINK_STATUS_DISABLED; reconnectTimer = new QTimer(this); @@ -30,7 +34,14 @@ ControlLink::~ControlLink() { if(linkStatus == CONTROL_LINK_STATUS_ESTABLISHED) { - tcpSocket->disconnect(); + if(transport == CONTROL_LINK_TRANSPORT_SERIAL) + { + if(serialPort != nullptr && serialPort->isOpen()) serialPort->close(); + } + else + { + tcpSocket->disconnect(); + } } } @@ -48,6 +59,89 @@ control_link_status_t ControlLink::establishLink(QString aIpAddress, QString a return linkStatus; } +control_link_status_t ControlLink::establishSerialLink(QString aPortName, qint32 aBaudRate) +{ + transport = CONTROL_LINK_TRANSPORT_SERIAL; + linkStatus = CONTROL_LINK_STATUS_DISABLED; + serialPortName = aPortName; + ipAddress = aPortName; /* reuse for display (getDeviceIP_Addr) */ + + if(serialPort == nullptr) + { + serialPort = new QSerialPort(this); + connect(serialPort, SIGNAL(errorOccurred(QSerialPort::SerialPortError)), + this, SLOT(onSerialErrorOccurred(QSerialPort::SerialPortError))); + } + + if(serialPort->isOpen()) serialPort->close(); + + serialPort->setPortName(aPortName); + serialPort->setBaudRate(aBaudRate); /* ignored by USB CDC, required by API */ + serialPort->setDataBits(QSerialPort::Data8); + serialPort->setParity(QSerialPort::NoParity); + serialPort->setStopBits(QSerialPort::OneStop); + serialPort->setFlowControl(QSerialPort::NoFlowControl); + + if(!serialPort->open(QIODevice::ReadWrite)) + { + linkStatus = CONTROL_LINK_STATUS_DISABLED; + return linkStatus; + } + + /* Assert DTR so the firmware's CDC driver enables the TX path (dtrActive) */ + serialPort->setDataTerminalReady(true); + serialPort->clear(); + + linkStatus = CONTROL_LINK_STATUS_ESTABLISHED; + + /* Mirror TCP timing: deliver sigConnected through the event loop, after the + * caller has assigned this link to the Device and connected the signal. */ + QTimer::singleShot(0, this, [this](){ emit sigConnected(); }); + + return linkStatus; +} + +bool ControlLink::prvReadSerialResponse(QString* response, int timeout) +{ + QByteArray receivedData; + + while(true) + { + if(!serialPort->waitForReadyRead(timeout)) + { + *response = "Unable to read data"; + return false; + } + + receivedData += serialPort->readAll(); + + /* Process complete, newline-terminated lines. The firmware terminates + * every response with "\r\n". Asynchronous status-link notifications + * (which do not start with STATUS/ERROR) are skipped. */ + int nlIndex; + while((nlIndex = receivedData.indexOf('\n')) != -1) + { + QByteArray lineBytes = receivedData.left(nlIndex); + receivedData.remove(0, nlIndex + 1); + + QString line = QString::fromUtf8(lineBytes).trimmed(); + if(line.isEmpty()) continue; + + if(line.startsWith("STATUS")) + { + *response = line.mid(6).trimmed(); /* payload after "STATUS" */ + return true; + } + if(line.startsWith("ERROR")) + { + *response = "ERROR"; + return false; + } + /* otherwise: asynchronous notification -> ignore, keep reading */ + } + } +} + bool ControlLink::prvReadResponse(QString* response, int timeout) { QByteArray receivedData; @@ -117,10 +211,29 @@ bool ControlLink::prvReadResponse(QString* response, int timeout) void ControlLink::reconnect() { + if(transport == CONTROL_LINK_TRANSPORT_SERIAL) return; /* serial auto-reconnect is Phase 2 */ tcpSocket->connectToHost(ipAddress, portNumber); tcpSocket->waitForConnected(10); } +void ControlLink::onSerialErrorOccurred(QSerialPort::SerialPortError error) +{ + if(error == QSerialPort::NoError) return; + + /* Treat fatal transport errors (device unplugged, etc.) as a disconnect. */ + if(error == QSerialPort::ResourceError || + error == QSerialPort::PermissionError || + error == QSerialPort::DeviceNotFoundError) + { + if(linkStatus != CONTROL_LINK_STATUS_DISABLED) + { + linkStatus = CONTROL_LINK_STATUS_DISABLED; + if(serialPort != nullptr && serialPort->isOpen()) serialPort->close(); + emit sigDisconnected(); + } + } +} + bool ControlLink::getDeviceName(QString *deviceName) { QString response; @@ -141,6 +254,35 @@ bool ControlLink::executeCommand(QString command, QString* response, int timeout return false; } + /* ===== SERIAL (Charger): plain text terminated by CR ===== */ + if(transport == CONTROL_LINK_TRANSPORT_SERIAL) + { + if(serialPort == nullptr || !serialPort->isOpen()) + { + *response = "Control link not established"; + return false; + } + + serialPort->clear(QSerialPort::Input); + + QByteArray packet = command.toUtf8(); + packet.append('\r'); + + if(serialPort->write(packet) == -1) + { + *response = "Write failed"; + return false; + } + if(!serialPort->waitForBytesWritten(timeout)) + { + *response = "Write timeout"; + return false; + } + + return prvReadSerialResponse(response, timeout); + } + + /* ===== TCP (EPP): binary 0xA5 framing ===== */ QByteArray packet; /* HEADER */ @@ -171,6 +313,14 @@ bool ControlLink::executeCommand(QByteArray request, QString* response, int time return false; } + /* Binary (0xA5 'B') framing is EPP/TCP-only. The serial charger uses a + * text BD protocol which is a Phase-2 item. */ + if(transport == CONTROL_LINK_TRANSPORT_SERIAL) + { + *response = "Binary commands not supported over serial"; + return false; + } + QByteArray packet; /* HEADER */ diff --git a/Source/Links/controllink.h b/Source/Links/controllink.h index 57d1ef4..787fd74 100644 --- a/Source/Links/controllink.h +++ b/Source/Links/controllink.h @@ -3,11 +3,19 @@ #include #include +#include #include #include #define CONTROL_LINK_COMMAND_TIMEOUT 1000 #define CONTROL_LINK_RECONNECT_PERIOD 1000 +#define CONTROL_LINK_SERIAL_BAUDRATE 115200 + +/* Transport used by the control link */ +typedef enum{ + CONTROL_LINK_TRANSPORT_TCP, /*!< EPP devices over TCP/IP (binary 0xA5 framing) */ + CONTROL_LINK_TRANSPORT_SERIAL /*!< Charger device over USB CDC serial (text, \r terminated) */ +}control_link_transport_t; typedef enum{ CONTROL_LINK_STATUS_ESTABLISHED, @@ -22,6 +30,7 @@ class ControlLink : public QObject explicit ControlLink(QObject *parent = nullptr); ~ControlLink(); control_link_status_t establishLink(QString aIpAddress, QString aPortNumber); + control_link_status_t establishSerialLink(QString aPortName, qint32 aBaudRate = CONTROL_LINK_SERIAL_BAUDRATE); bool getDeviceName(QString* deviceName); bool executeCommand(QString command, QString* response, int timeout); bool executeCommand(QByteArray request, QString* response, int timeout); @@ -37,17 +46,22 @@ public slots: void onDisconnected(); void onReconnected(); void reconnect(); + void onSerialErrorOccurred(QSerialPort::SerialPortError error); private: QTcpSocket *tcpSocket; + QSerialPort *serialPort; + control_link_transport_t transport; QString ipAddress; + QString serialPortName; quint16 portNumber; control_link_status_t linkStatus; QTimer *reconnectTimer; bool setSocketKeepAlive(); bool prvReadResponse(QString* response, int timeout); + bool prvReadSerialResponse(QString* response, int timeout); }; diff --git a/Source/OpenEPT.pro b/Source/OpenEPT.pro index 5c15dc4..e40882c 100644 --- a/Source/OpenEPT.pro +++ b/Source/OpenEPT.pro @@ -1,6 +1,7 @@ QT += core gui opengl concurrent QT += network QT += svg +QT += serialport greaterThan(QT_MAJOR_VERSION, 4): QT += widgets printsupport @@ -57,6 +58,7 @@ SOURCES += \ Windows/BatteryParams/batteryparamstrendwnd.cpp \ Windows/BatteryParams/batteryparamswnd.cpp \ Windows/AddDevice/adddevicewnd.cpp \ + Windows/Charger/chargerwnd.cpp \ Windows/ApplicationConf/applicationconfwnd.cpp \ Windows/Console/consolewnd.cpp \ Windows/DataAnalyzer/dataanalyzer.cpp \ @@ -110,6 +112,7 @@ HEADERS += \ Windows/BatteryParams/batteryparamstrendwnd.h \ Windows/BatteryParams/batteryparamswnd.h \ Windows/AddDevice/adddevicewnd.h \ + Windows/Charger/chargerwnd.h \ Windows/ApplicationConf/applicationconfwnd.h \ Windows/Console/consolewnd.h \ Windows/DataAnalyzer/dataanalyzer.h \ diff --git a/Source/Windows/AddDevice/adddevicewnd.cpp b/Source/Windows/AddDevice/adddevicewnd.cpp index 834f9c3..01b22e5 100644 --- a/Source/Windows/AddDevice/adddevicewnd.cpp +++ b/Source/Windows/AddDevice/adddevicewnd.cpp @@ -2,6 +2,12 @@ #include "ui_adddevicewnd.h" #include "ui_consolewnd.h" +#include + +/* USB identifiers advertised by the OpenEPT Charger firmware (usbd_desc.c) */ +#define ADD_DEVICE_CHARGER_USB_VID 0x0483 +#define ADD_DEVICE_CHARGER_USB_PID 0x5740 + AddDeviceWnd::AddDeviceWnd(QWidget *parent) : QWidget(parent), ui(new Ui::AddDeviceWnd) @@ -9,12 +15,20 @@ AddDeviceWnd::AddDeviceWnd(QWidget *parent) : ui->setupUi(this); layout()->setSizeConstraint(QLayout::SetFixedSize); + ui->deviceTypeCombo->addItem("EPP", (int)ADD_DEVICE_TYPE_EPP); + ui->deviceTypeCombo->addItem("Charger", (int)ADD_DEVICE_TYPE_CHARGER); + connect(ui->closePusb, SIGNAL(pressed()), this, SLOT(onClosePusbPressed())); connect(ui->addDevicePusb, SIGNAL(pressed()), this, SLOT(onAddDevicePusbPressed())); + connect(ui->deviceTypeCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(onDeviceTypeChanged(int))); + connect(ui->refreshPortsPusb, SIGNAL(pressed()), this, SLOT(onRefreshPortsPressed())); QFont appFont = this->font(); // Get the default font appFont.setPointSize(10); // Set font size to 14 (or any desired size) this->setFont(appFont); + + refreshSerialPorts(); + applyDeviceTypeVisibility(); } AddDeviceWnd::~AddDeviceWnd() @@ -22,6 +36,71 @@ AddDeviceWnd::~AddDeviceWnd() delete ui; } +void AddDeviceWnd::refreshSerialPorts() +{ + ui->serialPortCombo->clear(); + + int chargerIndex = -1; + const QList ports = QSerialPortInfo::availablePorts(); + for(const QSerialPortInfo& info : ports) + { + QString label = info.systemLocation(); + if(!info.description().isEmpty()) + label += " (" + info.description() + ")"; + + /* Store the port name used to open the port as the item data. */ + ui->serialPortCombo->addItem(label, info.portName()); + + bool isCharger = false; + if(info.hasVendorIdentifier() && info.hasProductIdentifier()) + { + if(info.vendorIdentifier() == ADD_DEVICE_CHARGER_USB_VID && + info.productIdentifier() == ADD_DEVICE_CHARGER_USB_PID) + isCharger = true; + } + if(info.description().contains("OpenEPT", Qt::CaseInsensitive) || + info.manufacturer().contains("OpenEPT", Qt::CaseInsensitive)) + isCharger = true; + + if(isCharger && chargerIndex == -1) + chargerIndex = ui->serialPortCombo->count() - 1; + } + + /* Pre-select a detected OpenEPT Charger, if any. */ + if(chargerIndex != -1) + ui->serialPortCombo->setCurrentIndex(chargerIndex); +} + +void AddDeviceWnd::applyDeviceTypeVisibility() +{ + int type = ui->deviceTypeCombo->currentData().toInt(); + bool isCharger = (type == ADD_DEVICE_TYPE_CHARGER); + + /* EPP: IP + port. Charger: serial port. */ + ui->ipAddressLabe->setVisible(!isCharger); + ui->ipAddressLine->setVisible(!isCharger); + ui->portNumberLabe->setVisible(!isCharger); + ui->portNumberLine->setVisible(!isCharger); + + ui->serialPortLabe->setVisible(isCharger); + ui->serialPortCombo->setVisible(isCharger); + ui->refreshPortsPusb->setVisible(isCharger); + + if(isCharger) + refreshSerialPorts(); +} + +void AddDeviceWnd::onDeviceTypeChanged(int index) +{ + Q_UNUSED(index); + applyDeviceTypeVisibility(); +} + +void AddDeviceWnd::onRefreshPortsPressed() +{ + refreshSerialPorts(); +} + void AddDeviceWnd::onClosePusbPressed() { close(); @@ -29,6 +108,17 @@ void AddDeviceWnd::onClosePusbPressed() void AddDeviceWnd::onAddDevicePusbPressed() { - emit sigAddDevice(ui->ipAddressLine->text(), ui->portNumberLine->text()); + int type = ui->deviceTypeCombo->currentData().toInt(); + + if(type == ADD_DEVICE_TYPE_CHARGER) + { + /* For a charger the serial port name travels in the aIpAddress field. */ + QString portName = ui->serialPortCombo->currentData().toString(); + emit sigAddDevice(portName, "", ADD_DEVICE_TYPE_CHARGER); + } + else + { + emit sigAddDevice(ui->ipAddressLine->text(), ui->portNumberLine->text(), ADD_DEVICE_TYPE_EPP); + } close(); } diff --git a/Source/Windows/AddDevice/adddevicewnd.h b/Source/Windows/AddDevice/adddevicewnd.h index 7e99d23..7acd4a2 100644 --- a/Source/Windows/AddDevice/adddevicewnd.h +++ b/Source/Windows/AddDevice/adddevicewnd.h @@ -7,6 +7,13 @@ namespace Ui { class AddDeviceWnd; } +/* Device type selected in the Add Device dialog. For CHARGER the aIpAddress + * field of sigAddDevice carries the serial port name (e.g. /dev/ttyACM0). */ +typedef enum{ + ADD_DEVICE_TYPE_EPP = 0, + ADD_DEVICE_TYPE_CHARGER = 1 +}add_device_type_t; + class AddDeviceWnd : public QWidget { Q_OBJECT @@ -16,13 +23,17 @@ class AddDeviceWnd : public QWidget ~AddDeviceWnd(); signals: - void sigAddDevice(QString aIpAddress, QString aPort); + void sigAddDevice(QString aIpAddress, QString aPort, int aDeviceType); public slots: void onClosePusbPressed(); void onAddDevicePusbPressed(); + void onDeviceTypeChanged(int index); + void onRefreshPortsPressed(); private: + void refreshSerialPorts(); + void applyDeviceTypeVisibility(); Ui::AddDeviceWnd *ui; }; diff --git a/Source/Windows/AddDevice/adddevicewnd.ui b/Source/Windows/AddDevice/adddevicewnd.ui index 0293f9c..c10a368 100644 --- a/Source/Windows/AddDevice/adddevicewnd.ui +++ b/Source/Windows/AddDevice/adddevicewnd.ui @@ -19,6 +19,58 @@ + + + + + + + 150 + 30 + + + + + 150 + 30 + + + + Device type: + + + + + + + + 150 + 30 + + + + + 150 + 30 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + @@ -141,6 +193,64 @@ + + + + + + + 150 + 30 + + + + + 150 + 30 + + + + Serial port: + + + + + + + + 150 + 30 + + + + + 150 + 30 + + + + + + + + + 80 + 30 + + + + + 80 + 30 + + + + Refresh + + + + + diff --git a/Source/Windows/Charger/chargerwnd.cpp b/Source/Windows/Charger/chargerwnd.cpp new file mode 100644 index 0000000..707bb17 --- /dev/null +++ b/Source/Windows/Charger/chargerwnd.cpp @@ -0,0 +1,170 @@ +#include "chargerwnd.h" +#include "device.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +ChargerWnd::ChargerWnd(Device* device, QWidget *parent) : + QWidget(parent), + m_device(device) +{ + buildUi(); + + connect(readPusb, SIGNAL(clicked()), this, SLOT(onReadClicked())); + connect(setPusb, SIGNAL(clicked()), this, SLOT(onSetClicked())); + connect(startPusb, SIGNAL(clicked()), this, SLOT(onStartChargingClicked())); + connect(stopPusb, SIGNAL(clicked()), this, SLOT(onStopChargingClicked())); + + /* Populate current values from the device. */ + onReadClicked(); +} + +void ChargerWnd::buildUi() +{ + QVBoxLayout* mainL = new QVBoxLayout(this); + + /* ---- Device information (read-only) ---- */ + QGroupBox* infoBox = new QGroupBox("Device information", this); + QFormLayout* infoL = new QFormLayout(infoBox); + hwSerialEdit = new QLineEdit(infoBox); hwSerialEdit->setReadOnly(true); + fwVersionEdit = new QLineEdit(infoBox); fwVersionEdit->setReadOnly(true); + infoL->addRow("HW serial:", hwSerialEdit); + infoL->addRow("FW version:", fwVersionEdit); + mainL->addWidget(infoBox); + + /* ---- Charging parameters ---- */ + QGroupBox* paramBox = new QGroupBox("Charging parameters", this); + QFormLayout* paramL = new QFormLayout(paramBox); + + termVoltageEdit = new QLineEdit(paramBox); + termVoltageEdit->setValidator(new QDoubleValidator(1.0, 5.0, 2, termVoltageEdit)); + paramL->addRow("Termination voltage [V]:", termVoltageEdit); + + termCurrentCombo = new QComboBox(paramBox); + termCurrentCombo->addItem("Disabled", 0); + termCurrentCombo->addItem("5 %", 1); + termCurrentCombo->addItem("10 %", 2); + termCurrentCombo->addItem("20 %", 3); + paramL->addRow("Termination current:", termCurrentCombo); + + chargeCurrentEdit = new QLineEdit(paramBox); + chargeCurrentEdit->setValidator(new QIntValidator(5, 1000, chargeCurrentEdit)); + paramL->addRow("Charging current [mA]:", chargeCurrentEdit); + + maxCurrentCombo = new QComboBox(paramBox); + maxCurrentCombo->addItem("50 mA", 0); + maxCurrentCombo->addItem("100 mA", 1); + maxCurrentCombo->addItem("200 mA", 2); + maxCurrentCombo->addItem("300 mA", 3); + maxCurrentCombo->addItem("400 mA", 4); + maxCurrentCombo->addItem("500 mA", 5); + maxCurrentCombo->addItem("700 mA", 6); + maxCurrentCombo->addItem("1100 mA", 7); + paramL->addRow("Max charging current:", maxCurrentCombo); + + /* Start voltage is not yet exposed by the firmware (Phase 2). */ + startVoltageEdit = new QLineEdit(paramBox); + startVoltageEdit->setEnabled(false); + startVoltageEdit->setPlaceholderText("not supported by firmware yet"); + paramL->addRow("Start voltage [V]:", startVoltageEdit); + + QHBoxLayout* paramBtnL = new QHBoxLayout(); + readPusb = new QPushButton("Read", paramBox); + setPusb = new QPushButton("Set", paramBox); + paramBtnL->addWidget(readPusb); + paramBtnL->addWidget(setPusb); + paramL->addRow(paramBtnL); + + mainL->addWidget(paramBox); + + /* ---- Charging control ---- */ + QGroupBox* ctrlBox = new QGroupBox("Charging control", this); + QHBoxLayout* ctrlL = new QHBoxLayout(ctrlBox); + startPusb = new QPushButton("Start charging", ctrlBox); + stopPusb = new QPushButton("Stop charging", ctrlBox); + ctrlL->addWidget(startPusb); + ctrlL->addWidget(stopPusb); + mainL->addWidget(ctrlBox); + + statusLabel = new QLabel("", this); + mainL->addWidget(statusLabel); + + mainL->addStretch(1); +} + +int ChargerWnd::comboIndexForData(QComboBox* combo, int value) +{ + for(int i = 0; i < combo->count(); i++) + if(combo->itemData(i).toInt() == value) return i; + return -1; +} + +void ChargerWnd::onReadClicked() +{ + if(m_device == nullptr) return; + + QString s; + if(m_device->getChargerHWSerial(&s)) hwSerialEdit->setText(s); + QString v; + if(m_device->getChargerFWVersion(&v)) fwVersionEdit->setText(v); + + float tv; + if(m_device->getChargerTermVoltage(&tv)) termVoltageEdit->setText(QString::number(tv, 'f', 2)); + + int tc; + if(m_device->getChargerTermCurrent(&tc)) + { + int idx = comboIndexForData(termCurrentCombo, tc); + if(idx >= 0) termCurrentCombo->setCurrentIndex(idx); + } + + int cc; + if(m_device->getChargerCurrent(&cc)) chargeCurrentEdit->setText(QString::number(cc)); + + int mc; + if(m_device->getChargerMaxChargingCurrent(&mc)) + { + int idx = comboIndexForData(maxCurrentCombo, mc); + if(idx >= 0) maxCurrentCombo->setCurrentIndex(idx); + } + + statusLabel->setText("Parameters read from device."); +} + +void ChargerWnd::onSetClicked() +{ + if(m_device == nullptr) return; + + bool ok = true; + ok &= m_device->setChargerTermVoltage(termVoltageEdit->text().toFloat()); + ok &= m_device->setChargerTermCurrent(termCurrentCombo->currentData().toInt()); + ok &= m_device->setChargerCurrent(chargeCurrentEdit->text().toInt()); + ok &= m_device->setChargerMaxChargingCurrent(maxCurrentCombo->currentData().toInt()); + + statusLabel->setText(ok ? "Parameters applied." : "Failed to apply one or more parameters."); + + /* Reflect what the device actually stored. */ + onReadClicked(); +} + +void ChargerWnd::onStartChargingClicked() +{ + if(m_device == nullptr) return; + bool ok = m_device->setChargerStatus(true); + statusLabel->setText(ok ? "Charging started." : "Failed to start charging."); +} + +void ChargerWnd::onStopChargingClicked() +{ + if(m_device == nullptr) return; + bool ok = m_device->setChargerStatus(false); + statusLabel->setText(ok ? "Charging stopped." : "Failed to stop charging."); +} diff --git a/Source/Windows/Charger/chargerwnd.h b/Source/Windows/Charger/chargerwnd.h new file mode 100644 index 0000000..d08f25b --- /dev/null +++ b/Source/Windows/Charger/chargerwnd.h @@ -0,0 +1,52 @@ +#ifndef CHARGERWND_H +#define CHARGERWND_H + +#include + +class Device; +class QLineEdit; +class QComboBox; +class QLabel; +class QPushButton; + +/** + * @brief Dedicated settings window for a Charger device (BQ25180). + * + * Unlike EPP devices (which open the full DeviceWnd), a Charger device opens + * only this focused window: charging parameters, start/stop control and device + * identification. It talks to the charger through the Device charger wrappers, + * which in turn use the serial ControlLink. + */ +class ChargerWnd : public QWidget +{ + Q_OBJECT +public: + explicit ChargerWnd(Device* device, QWidget *parent = nullptr); + +private slots: + void onReadClicked(); + void onSetClicked(); + void onStartChargingClicked(); + void onStopChargingClicked(); + +private: + void buildUi(); + int comboIndexForData(QComboBox* combo, int value); + + Device* m_device; + + QLineEdit* hwSerialEdit; + QLineEdit* fwVersionEdit; + QLineEdit* startVoltageEdit; /* firmware support pending (Phase 2) */ + QLineEdit* termVoltageEdit; + QComboBox* termCurrentCombo; + QLineEdit* chargeCurrentEdit; + QComboBox* maxCurrentCombo; + QLabel* statusLabel; + QPushButton* readPusb; + QPushButton* setPusb; + QPushButton* startPusb; + QPushButton* stopPusb; +}; + +#endif // CHARGERWND_H diff --git a/Source/openept.cpp b/Source/openept.cpp index d50e4b2..06cfbc8 100644 --- a/Source/openept.cpp +++ b/Source/openept.cpp @@ -5,6 +5,7 @@ #include "Windows/About/aboutwnd.h" #include "Windows/About/updatechecker.h" #include "Windows/Device/devicewnd.h" +#include "Windows/Charger/chargerwnd.h" #include "ui_openept.h" #include "Links/controllink.h" #include @@ -41,7 +42,7 @@ OpenEPT::OpenEPT(QString aWorkspacePath, QWidget *parent) addDeviceWnd = new AddDeviceWnd(this); addDeviceWnd->setWindowFlags(Qt::Dialog | Qt::MSWindowsFixedSizeDialogHint); addDeviceWnd->setWindowModality(Qt::WindowModal); - connect(addDeviceWnd, SIGNAL(sigAddDevice(QString,QString)), this, SLOT(onAddDeviceWndAddDevice(QString,QString)), Qt::QueuedConnection); + connect(addDeviceWnd, SIGNAL(sigAddDevice(QString,QString,int)), this, SLOT(onAddDeviceWndAddDevice(QString,QString,int)), Qt::QueuedConnection); connectedDevicesMenu = new QMenu("Connected devices"); @@ -109,9 +110,9 @@ void OpenEPT::onActionAddSingleDeviceTriggered() addDeviceWnd->show(); } -void OpenEPT::onAddDeviceWndAddDevice(QString aIpAddress, QString aPort) +void OpenEPT::onAddDeviceWndAddDevice(QString aIpAddress, QString aPort, int aDeviceType) { - if(addNewDevice(aIpAddress, aPort)) + if(addNewDevice(aIpAddress, aPort, aDeviceType)) { msgBox.setText("Device sucessfully added"); msgBox.exec(); @@ -124,15 +125,22 @@ void OpenEPT::onAddDeviceWndAddDevice(QString aIpAddress, QString aPort) } -bool OpenEPT::addNewDevice(QString aIpAddress, QString aPort) +bool OpenEPT::addNewDevice(QString aIpAddress, QString aPort, int aDeviceType) { QString deviceName; /* Create control link and try to access device*/ ControlLink* tmpControlLink = new ControlLink(); - /* Try to establish connection with device*/ - if(tmpControlLink->establishLink(aIpAddress, aPort) != CONTROL_LINK_STATUS_ESTABLISHED) + /* Try to establish connection with device. A Charger connects over a + * serial port (its name arrives in aIpAddress); EPP connects over TCP/IP. */ + control_link_status_t linkStatus; + if(aDeviceType == ADD_DEVICE_TYPE_CHARGER) + linkStatus = tmpControlLink->establishSerialLink(aIpAddress); + else + linkStatus = tmpControlLink->establishLink(aIpAddress, aPort); + + if(linkStatus != CONTROL_LINK_STATUS_ESTABLISHED) { delete tmpControlLink; return false; @@ -150,6 +158,42 @@ bool OpenEPT::addNewDevice(QString aIpAddress, QString aPort) tmpDevice->setName(deviceName); tmpDevice->controlLinkAssign(tmpControlLink); + /* A Charger device opens only the focused charger window, not the full + * EPP device window (acquisition/plots/etc. do not apply to it). */ + if(aDeviceType == ADD_DEVICE_TYPE_CHARGER) + { + ChargerWnd *chargerWnd = new ChargerWnd(tmpDevice, 0); + chargerWnd->setWindowTitle(deviceName); + + QDockWidget *dock = new QDockWidget(deviceName, this); + dock->setObjectName("DeviceDock_" + QString::number(connectedDeviceNumber)); + dock->setWidget(chargerWnd); + dock->setAllowedAreas(Qt::AllDockWidgetAreas); + dock->setFeatures(QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable | + QDockWidget::DockWidgetClosable); + addDockWidget(Qt::TopDockWidgetArea, dock); + + QList existing = findChildren(); + for(QDockWidget *existingDock : existing) + { + if(existingDock != dock && + existingDock->objectName().startsWith("DeviceDock_")) + { + tabifyDockWidget(existingDock, dock); + break; + } + } + + QAction* tmpDeviceAction = new QAction(deviceName); + connectedDevicesMenu->addAction(tmpDeviceAction); + + dock->show(); + dock->raise(); + setTheme(); + return true; + } + /* Create corresponding device window*/ DeviceWnd *tmpdeviceWnd = new DeviceWnd(0); tmpdeviceWnd->setWindowTitle(deviceName); diff --git a/Source/openept.h b/Source/openept.h index 469ad27..2b8c7b6 100644 --- a/Source/openept.h +++ b/Source/openept.h @@ -33,7 +33,7 @@ class OpenEPT : public QMainWindow private slots: void onActionAddSingleDeviceTriggered(); - void onAddDeviceWndAddDevice(QString aIpAddress, QString aPort); + void onAddDeviceWndAddDevice(QString aIpAddress, QString aPort, int aDeviceType); void onDeviceContainerDeviceWndClosed(DeviceContainer* container); void onDeviceContainerAllDeviceWndClosed(); void onActionOpenAndProcessData(); @@ -63,7 +63,7 @@ private slots: QMessageBox msgBox; /**/ - bool addNewDevice(QString aIpAddress, QString aPort); + bool addNewDevice(QString aIpAddress, QString aPort, int aDeviceType = ADD_DEVICE_TYPE_EPP); /**/ void setTheme(); From d11243e98c10899e190c0c0943c7d01b0ce62a98 Mon Sep 17 00:00:00 2001 From: hturkmanovic Date: Wed, 7 Oct 2026 19:34:38 +0200 Subject: [PATCH 2/6] CI: install libqt5serialport5-dev for Linux build --- .github/workflows/linux-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/linux-build.yml b/.github/workflows/linux-build.yml index 6ff6b89..e53e7a8 100644 --- a/.github/workflows/linux-build.yml +++ b/.github/workflows/linux-build.yml @@ -28,6 +28,7 @@ jobs: qt5-qmake \ libqt5svg5-dev \ libqt5opengl5-dev \ + libqt5serialport5-dev \ libgl1-mesa-dev \ libfuse2 \ dpkg-dev \ From 64b1196651930605f66b97167c163727b24cd468 Mon Sep 17 00:00:00 2001 From: hturkmanovic Date: Fri, 9 Oct 2026 14:30:02 +0200 Subject: [PATCH 3/6] Initial charger wnd --- Source/Links/controllink.cpp | 166 ++++++++-- Source/Links/controllink.h | 15 + Source/Windows/Charger/chargerwnd.cpp | 446 ++++++++++++++++++++++++-- Source/Windows/Charger/chargerwnd.h | 50 ++- Source/device.cpp | 79 +++++ Source/device.h | 9 + Source/resource.qrc | 1 + 7 files changed, 707 insertions(+), 59 deletions(-) diff --git a/Source/Links/controllink.cpp b/Source/Links/controllink.cpp index 3d65fa6..b364a27 100644 --- a/Source/Links/controllink.cpp +++ b/Source/Links/controllink.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #ifdef Q_OS_WIN #include "Ws2tcpip.h" @@ -19,8 +20,11 @@ ControlLink::ControlLink(QObject *parent) { tcpSocket = new QTcpSocket(this); tcpSocket->setSocketOption(QAbstractSocket::KeepAliveOption, 1); - serialPort = nullptr; - transport = CONTROL_LINK_TRANSPORT_TCP; + serialPort = nullptr; + transport = CONTROL_LINK_TRANSPORT_TCP; + serialResponseOk = false; + serialResponseGot = false; + serialResponseIsBinary = false; ipAddress = "0.0.0.0"; serialPortName = ""; portNumber = 0; @@ -71,10 +75,14 @@ control_link_status_t ControlLink::establishSerialLink(QString aPortName, qint serialPort = new QSerialPort(this); connect(serialPort, SIGNAL(errorOccurred(QSerialPort::SerialPortError)), this, SLOT(onSerialErrorOccurred(QSerialPort::SerialPortError))); + connect(serialPort, SIGNAL(readyRead()), this, SLOT(onSerialReadyRead())); } if(serialPort->isOpen()) serialPort->close(); + serialRxBuffer.clear(); + serialResponseGot = false; + serialPort->setPortName(aPortName); serialPort->setBaudRate(aBaudRate); /* ignored by USB CDC, required by API */ serialPort->setDataBits(QSerialPort::Data8); @@ -103,45 +111,140 @@ control_link_status_t ControlLink::establishSerialLink(QString aPortName, qint bool ControlLink::prvReadSerialResponse(QString* response, int timeout) { - QByteArray receivedData; + /* Responses arrive asynchronously in onSerialReadyRead(), which sets + * serialResponseGot and emits sigSerialResponseReady(). Wait for it (or the + * timeout) via a local event loop, so readyRead keeps firing and any + * asynchronous notifications continue to be dispatched meanwhile. */ + if(!serialResponseGot) + { + QEventLoop loop; + QTimer timer; + timer.setSingleShot(true); + connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit); + connect(this, &ControlLink::sigSerialResponseReady, &loop, &QEventLoop::quit); + timer.start(timeout); + loop.exec(); + } - while(true) + if(serialResponseGot) + { + if(!serialResponseIsBinary && response != nullptr) + *response = serialResponsePayload; + return serialResponseOk; + } + + if(response != nullptr) *response = "Unable to read data"; + return false; +} + +void ControlLink::onSerialReadyRead() +{ + if(serialPort == nullptr) return; + + serialRxBuffer += serialPort->readAll(); + + for(;;) { - if(!serialPort->waitForReadyRead(timeout)) + /* Binary board-data response frame: 0xA5 0xA5 'R' */ + if(serialRxBuffer.size() >= 3 && + (quint8)serialRxBuffer.at(0) == 0xA5 && + (quint8)serialRxBuffer.at(1) == 0xA5 && + serialRxBuffer.at(2) == 'R') { - *response = "Unable to read data"; - return false; + if(serialRxBuffer.size() < 5) return; /* need the size field */ + quint16 size = (quint8)serialRxBuffer.at(3) | ((quint16)(quint8)serialRxBuffer.at(4) << 8); + if(serialRxBuffer.size() < 5 + (int)size) return; /* wait for all data */ + + serialBinaryPayload = serialRxBuffer.mid(5, size); + serialRxBuffer.remove(0, 5 + size); + serialResponseOk = true; + serialResponseGot = true; + serialResponseIsBinary = true; + emit sigSerialResponseReady(); + continue; } - receivedData += serialPort->readAll(); + int nlIndex = serialRxBuffer.indexOf('\n'); + if(nlIndex < 0) return; - /* Process complete, newline-terminated lines. The firmware terminates - * every response with "\r\n". Asynchronous status-link notifications - * (which do not start with STATUS/ERROR) are skipped. */ - int nlIndex; - while((nlIndex = receivedData.indexOf('\n')) != -1) + QByteArray lineBytes = serialRxBuffer.left(nlIndex); + serialRxBuffer.remove(0, nlIndex + 1); + + QString line = QString::fromUtf8(lineBytes).trimmed(); + if(line.isEmpty()) continue; + + if(line.startsWith("STATUS")) { - QByteArray lineBytes = receivedData.left(nlIndex); - receivedData.remove(0, nlIndex + 1); - - QString line = QString::fromUtf8(lineBytes).trimmed(); - if(line.isEmpty()) continue; - - if(line.startsWith("STATUS")) - { - *response = line.mid(6).trimmed(); /* payload after "STATUS" */ - return true; - } - if(line.startsWith("ERROR")) - { - *response = "ERROR"; - return false; - } - /* otherwise: asynchronous notification -> ignore, keep reading */ + serialResponsePayload = line.mid(6).trimmed(); /* payload after "STATUS" */ + serialResponseOk = true; + serialResponseGot = true; + serialResponseIsBinary = false; + emit sigSerialResponseReady(); + } + else if(line.startsWith("ERROR")) + { + serialResponsePayload = "ERROR"; + serialResponseOk = false; + serialResponseGot = true; + serialResponseIsBinary = false; + emit sigSerialResponseReady(); + } + else + { + /* Asynchronous status-link notification (e.g. charger mode change) */ + emit sigAsyncMessage(line); } } } +bool ControlLink::transferBdWrite(const QByteArray& data, int timeout) +{ + if(transport != CONTROL_LINK_TRANSPORT_SERIAL) return false; + if(serialPort == nullptr || !serialPort->isOpen() || linkStatus != CONTROL_LINK_STATUS_ESTABLISHED) return false; + + quint16 size = (quint16)data.size(); + + QByteArray frame; + frame.append((char)0xA5); + frame.append((char)0xA5); + frame.append('W'); + frame.append((char)(size & 0xFF)); + frame.append((char)((size >> 8) & 0xFF)); + frame.append(data); + + serialResponseGot = false; + serialResponseIsBinary = false; + + if(serialPort->write(frame) == -1) return false; + if(!serialPort->waitForBytesWritten(timeout)) return false; + + QString response; + if(!prvReadSerialResponse(&response, timeout)) return false; + return (response == "OK"); +} + +bool ControlLink::transferBdRead(QByteArray* out, int timeout) +{ + if(out == nullptr) return false; + if(transport != CONTROL_LINK_TRANSPORT_SERIAL) return false; + if(serialPort == nullptr || !serialPort->isOpen() || linkStatus != CONTROL_LINK_STATUS_ESTABLISHED) return false; + + serialResponseGot = false; + serialResponseIsBinary = false; + + QByteArray cmd("charger bd read"); + cmd.append('\r'); + if(serialPort->write(cmd) == -1) return false; + if(!serialPort->waitForBytesWritten(timeout)) return false; + + QString dummy; + if(!prvReadSerialResponse(&dummy, timeout)) return false; + if(!serialResponseIsBinary) return false; /* got an error line instead */ + + *out = serialBinaryPayload; + return true; +} + bool ControlLink::prvReadResponse(QString* response, int timeout) { QByteArray receivedData; @@ -263,7 +366,8 @@ bool ControlLink::executeCommand(QString command, QString* response, int timeout return false; } - serialPort->clear(QSerialPort::Input); + /* Arm response capture before sending (onSerialReadyRead fills it in). */ + serialResponseGot = false; QByteArray packet = command.toUtf8(); packet.append('\r'); diff --git a/Source/Links/controllink.h b/Source/Links/controllink.h index 787fd74..fa0fd3c 100644 --- a/Source/Links/controllink.h +++ b/Source/Links/controllink.h @@ -32,6 +32,10 @@ class ControlLink : public QObject control_link_status_t establishLink(QString aIpAddress, QString aPortNumber); control_link_status_t establishSerialLink(QString aPortName, qint32 aBaudRate = CONTROL_LINK_SERIAL_BAUDRATE); bool getDeviceName(QString* deviceName); + + /* Binary board-data (EEPROM) transfer over serial (0xA5 0xA5 frame). */ + bool transferBdWrite(const QByteArray& data, int timeout); + bool transferBdRead(QByteArray* out, int timeout); bool executeCommand(QString command, QString* response, int timeout); bool executeCommand(QByteArray request, QString* response, int timeout); @@ -41,12 +45,15 @@ class ControlLink : public QObject signals: void sigDisconnected(); void sigConnected(); + void sigAsyncMessage(QString message); /*!< Unsolicited status-link notification (serial) */ + void sigSerialResponseReady(); /*!< Internal: command response line received */ public slots: void onDisconnected(); void onReconnected(); void reconnect(); void onSerialErrorOccurred(QSerialPort::SerialPortError error); + void onSerialReadyRead(); private: @@ -59,6 +66,14 @@ public slots: control_link_status_t linkStatus; QTimer *reconnectTimer; + /* Serial async reception / response capture */ + QByteArray serialRxBuffer; + QString serialResponsePayload; + QByteArray serialBinaryPayload; /* payload of a binary 'R' response frame */ + bool serialResponseOk; + bool serialResponseGot; + bool serialResponseIsBinary; + bool setSocketKeepAlive(); bool prvReadResponse(QString* response, int timeout); bool prvReadSerialResponse(QString* response, int timeout); diff --git a/Source/Windows/Charger/chargerwnd.cpp b/Source/Windows/Charger/chargerwnd.cpp index 707bb17..7e19373 100644 --- a/Source/Windows/Charger/chargerwnd.cpp +++ b/Source/Windows/Charger/chargerwnd.cpp @@ -5,12 +5,40 @@ #include #include #include +#include #include #include #include #include #include #include +#include +#include +#include +#include +#include +#include +#include + +#define CHARGER_ICON_BTN_SIZE 32 +#define CHARGER_ICON_SIZE 24 + +static const char* CHARGER_ICON_BTN_STYLE = R"( +QPushButton { background: transparent; border: none; } +QPushButton:hover { background-color: rgba(255,255,255,30); } +QPushButton:pressed { background-color: rgba(255,255,255,60); } +QToolTip { background-color: white; color: black; border: 1px solid #ccc; } +)"; + +/* BQ25180 registers and bit fields (see datasheet SLUSE99C) */ +#define BQ25180_REG_STAT0 0x00 /* Charger status */ +#define BQ25180_REG_CHARGERCTRL1 0x06 /* BUVLO[5:3] */ +#define BQ25180_REG_IC_CTRL 0x07 /* VLOWV_SEL[6] */ + +#define BUVLO_MASK 0x38 /* bits 5-3 */ +#define BUVLO_SHIFT 3 +#define VLOWV_MASK 0x40 /* bit 6 */ +#define VLOWV_SHIFT 6 ChargerWnd::ChargerWnd(Device* device, QWidget *parent) : QWidget(parent), @@ -18,10 +46,21 @@ ChargerWnd::ChargerWnd(Device* device, QWidget *parent) : { buildUi(); - connect(readPusb, SIGNAL(clicked()), this, SLOT(onReadClicked())); - connect(setPusb, SIGNAL(clicked()), this, SLOT(onSetClicked())); - connect(startPusb, SIGNAL(clicked()), this, SLOT(onStartChargingClicked())); - connect(stopPusb, SIGNAL(clicked()), this, SLOT(onStopChargingClicked())); + connect(readPusb, SIGNAL(clicked()), this, SLOT(onReadClicked())); + connect(setPusb, SIGNAL(clicked()), this, SLOT(onSetClicked())); + connect(storePusb, SIGNAL(clicked()), this, SLOT(onStoreClicked())); + connect(startPusb, SIGNAL(clicked()), this, SLOT(onStartChargingClicked())); + connect(stopPusb, SIGNAL(clicked()), this, SLOT(onStopChargingClicked())); + connect(applyChemistryPusb, SIGNAL(clicked()), this, SLOT(onApplyChemistryClicked())); + connect(eepromReadPusb, SIGNAL(clicked()), this, SLOT(onEepromReadClicked())); + connect(eepromWritePusb, SIGNAL(clicked()), this, SLOT(onEepromWriteClicked())); + connect(eepromFormatPusb, SIGNAL(clicked()), this, SLOT(onEepromFormatClicked())); + connect(eepromImportPusb, SIGNAL(clicked()), this, SLOT(onEepromImportClicked())); + connect(eepromExportPusb, SIGNAL(clicked()), this, SLOT(onEepromExportClicked())); + + /* Live charging-mode updates pushed by the firmware over the serial link. */ + if(m_device != nullptr) + connect(m_device, SIGNAL(sigChargerModeObtained(QString)), this, SLOT(onChargerModeObtained(QString))); /* Populate current values from the device. */ onReadClicked(); @@ -29,23 +68,41 @@ ChargerWnd::ChargerWnd(Device* device, QWidget *parent) : void ChargerWnd::buildUi() { - QVBoxLayout* mainL = new QVBoxLayout(this); + /* Everything lives inside a scroll area so the window never grows past the + * screen; the top groups are laid out in two columns to keep it compact. */ + QVBoxLayout* outer = new QVBoxLayout(this); + outer->setContentsMargins(0, 0, 0, 0); + + QScrollArea* scroll = new QScrollArea(this); + scroll->setWidgetResizable(true); + scroll->setFrameShape(QFrame::NoFrame); + + QWidget* content = new QWidget(); + QVBoxLayout* mainL = new QVBoxLayout(content); /* ---- Device information (read-only) ---- */ - QGroupBox* infoBox = new QGroupBox("Device information", this); + QGroupBox* infoBox = new QGroupBox("Device information"); QFormLayout* infoL = new QFormLayout(infoBox); hwSerialEdit = new QLineEdit(infoBox); hwSerialEdit->setReadOnly(true); fwVersionEdit = new QLineEdit(infoBox); fwVersionEdit->setReadOnly(true); infoL->addRow("HW serial:", hwSerialEdit); infoL->addRow("FW version:", fwVersionEdit); - mainL->addWidget(infoBox); + + /* ---- Live status ---- */ + QGroupBox* statusBox = new QGroupBox("Status"); + QFormLayout* statusL = new QFormLayout(statusBox); + chargingModeLabel = new QLabel("—", statusBox); + QFont modeFont = chargingModeLabel->font(); modeFont.setBold(true); chargingModeLabel->setFont(modeFont); + stat0Label = new QLabel("—", statusBox); + statusL->addRow("Charging mode:", chargingModeLabel); + statusL->addRow("STAT0:", stat0Label); /* ---- Charging parameters ---- */ - QGroupBox* paramBox = new QGroupBox("Charging parameters", this); + QGroupBox* paramBox = new QGroupBox("Charging parameters"); QFormLayout* paramL = new QFormLayout(paramBox); termVoltageEdit = new QLineEdit(paramBox); - termVoltageEdit->setValidator(new QDoubleValidator(1.0, 5.0, 2, termVoltageEdit)); + termVoltageEdit->setValidator(new QDoubleValidator(3.5, 4.65, 2, termVoltageEdit)); paramL->addRow("Termination voltage [V]:", termVoltageEdit); termCurrentCombo = new QComboBox(paramBox); @@ -70,34 +127,132 @@ void ChargerWnd::buildUi() maxCurrentCombo->addItem("1100 mA", 7); paramL->addRow("Max charging current:", maxCurrentCombo); - /* Start voltage is not yet exposed by the firmware (Phase 2). */ - startVoltageEdit = new QLineEdit(paramBox); - startVoltageEdit->setEnabled(false); - startVoltageEdit->setPlaceholderText("not supported by firmware yet"); - paramL->addRow("Start voltage [V]:", startVoltageEdit); + /* ---- Protection thresholds ---- */ + QGroupBox* protBox = new QGroupBox("Protection thresholds"); + QFormLayout* protL = new QFormLayout(protBox); - QHBoxLayout* paramBtnL = new QHBoxLayout(); - readPusb = new QPushButton("Read", paramBox); - setPusb = new QPushButton("Set", paramBox); - paramBtnL->addWidget(readPusb); - paramBtnL->addWidget(setPusb); - paramL->addRow(paramBtnL); + buvloCombo = new QComboBox(protBox); + buvloCombo->addItem("3.0 V", 2); + buvloCombo->addItem("2.8 V", 3); + buvloCombo->addItem("2.6 V", 4); + buvloCombo->addItem("2.4 V", 5); + buvloCombo->addItem("2.2 V", 6); + buvloCombo->addItem("2.0 V", 7); + protL->addRow("Battery UVLO (falling):", buvloCombo); - mainL->addWidget(paramBox); + vlowvCombo = new QComboBox(protBox); + vlowvCombo->addItem("3.0 V", 0); + vlowvCombo->addItem("2.8 V", 1); + protL->addRow("Precharge threshold (VLOWV):", vlowvCombo); - /* ---- Charging control ---- */ - QGroupBox* ctrlBox = new QGroupBox("Charging control", this); + /* ---- Battery chemistry preset ---- */ + QGroupBox* chemBox = new QGroupBox("Battery chemistry preset"); + QHBoxLayout* chemL = new QHBoxLayout(chemBox); + chemistryCombo = new QComboBox(chemBox); + chemistryCombo->addItem("Li-Ion / LiPo (4.2 V)"); + chemistryCombo->addItem("LiFePO4 (3.6 V)"); + applyChemistryPusb = new QPushButton("Apply preset", chemBox); + chemL->addWidget(chemistryCombo); + chemL->addWidget(applyChemistryPusb); + + /* ---- Two-column arrangement of the groups above ---- */ + QHBoxLayout* colsL = new QHBoxLayout(); + QVBoxLayout* leftL = new QVBoxLayout(); + QVBoxLayout* rightL = new QVBoxLayout(); + leftL->addWidget(infoBox); + leftL->addWidget(paramBox); + leftL->addStretch(1); + rightL->addWidget(statusBox); + rightL->addWidget(protBox); + rightL->addWidget(chemBox); + rightL->addStretch(1); + colsL->addLayout(leftL); + colsL->addLayout(rightL); + mainL->addLayout(colsL); + + /* ---- Read / Set / Store (compact, right-aligned) ---- */ + QHBoxLayout* btnL = new QHBoxLayout(); + readPusb = new QPushButton("Read"); + setPusb = new QPushButton("Set"); + storePusb = new QPushButton("Store"); + readPusb->setFixedWidth(90); + setPusb->setFixedWidth(90); + storePusb->setFixedWidth(90); + readPusb->setToolTip("Read current settings from the charger"); + setPusb->setToolTip("Apply settings now (volatile)"); + storePusb->setToolTip("Store settings to EEPROM (persist across power cycles)"); + btnL->addStretch(1); + btnL->addWidget(readPusb); + btnL->addWidget(setPusb); + btnL->addWidget(storePusb); + mainL->addLayout(btnL); + + /* ---- Charging control (compact, right-aligned) ---- */ + QGroupBox* ctrlBox = new QGroupBox("Charging control"); QHBoxLayout* ctrlL = new QHBoxLayout(ctrlBox); startPusb = new QPushButton("Start charging", ctrlBox); stopPusb = new QPushButton("Stop charging", ctrlBox); + startPusb->setFixedWidth(120); + stopPusb->setFixedWidth(120); + ctrlL->addStretch(1); ctrlL->addWidget(startPusb); ctrlL->addWidget(stopPusb); mainL->addWidget(ctrlBox); - statusLabel = new QLabel("", this); + /* ---- EEPROM (AT24CS01) memory view ---- */ + QGroupBox* eepromBox = new QGroupBox("EEPROM memory"); + QVBoxLayout* eepromL = new QVBoxLayout(eepromBox); + + /* Header: icon buttons in the top-right, like the device memory view. */ + QHBoxLayout* eepromHeaderL = new QHBoxLayout(); + + eepromReadPusb = new QPushButton(eepromBox); + eepromWritePusb = new QPushButton(eepromBox); + eepromFormatPusb = new QPushButton(eepromBox); + eepromImportPusb = new QPushButton(eepromBox); + eepromExportPusb = new QPushButton(eepromBox); + + struct IconBtn { QPushButton* b; const char* icon; const char* tip; }; + const IconBtn ebtns[] = { + { eepromReadPusb, ":/images/NewSet/upload.png", "Read EEPROM content" }, + { eepromWritePusb, ":/images/NewSet/download.png", "Write EEPROM content" }, + { eepromFormatPusb, ":/images/NewSet/format.png", "Format (erase) EEPROM" }, + { eepromImportPusb, ":/images/NewSet/import.png", "Import image from PC" }, + { eepromExportPusb, ":/images/NewSet/export.png", "Export image to PC" }, + }; + for(const IconBtn& e : ebtns) + { + e.b->setFixedSize(CHARGER_ICON_BTN_SIZE, CHARGER_ICON_BTN_SIZE); + e.b->setIcon(QIcon(e.icon)); + e.b->setIconSize(QSize(CHARGER_ICON_SIZE, CHARGER_ICON_SIZE)); + e.b->setToolTip(e.tip); + e.b->setStyleSheet(CHARGER_ICON_BTN_STYLE); + } + + eepromHeaderL->addStretch(); + eepromHeaderL->addWidget(eepromReadPusb); + eepromHeaderL->addWidget(eepromWritePusb); + eepromHeaderL->addWidget(eepromFormatPusb); + eepromHeaderL->addWidget(eepromImportPusb); + eepromHeaderL->addWidget(eepromExportPusb); + eepromL->addLayout(eepromHeaderL); + + eepromEdit = new QTextEdit(eepromBox); + eepromEdit->setReadOnly(true); + eepromEdit->setMinimumHeight(180); + eepromEdit->setLineWrapMode(QTextEdit::NoWrap); + eepromEdit->setFont(QFont("Courier New")); + eepromL->addWidget(eepromEdit); + + mainL->addWidget(eepromBox); + + statusLabel = new QLabel(""); mainL->addWidget(statusLabel); mainL->addStretch(1); + + scroll->setWidget(content); + outer->addWidget(scroll); } int ChargerWnd::comboIndexForData(QComboBox* combo, int value) @@ -107,6 +262,52 @@ int ChargerWnd::comboIndexForData(QComboBox* combo, int value) return -1; } +bool ChargerWnd::readRegisterField(int reg, int mask, int shift, int* value) +{ + int regVal = 0; + if(!m_device->getChargerRegister(reg, ®Val)) return false; + *value = (regVal & mask) >> shift; + return true; +} + +bool ChargerWnd::setRegisterField(int reg, int mask, int shift, int value) +{ + int regVal = 0; + if(!m_device->getChargerRegister(reg, ®Val)) return false; /* read-modify-write */ + regVal = (regVal & ~mask) | ((value << shift) & mask); + return m_device->setChargerRegister(reg, regVal); +} + +void ChargerWnd::updateChargingModeLabel(const QString& modeCode) +{ + QString text; + if(modeCode == "cc") text = "Constant Current (CC)"; + else if(modeCode == "cv") text = "Constant Voltage (CV)"; + else if(modeCode == "done") text = "Charge done"; + else if(modeCode == "not") text = "Not charging"; + else text = modeCode; + chargingModeLabel->setText(text); +} + +void ChargerWnd::readStatus() +{ + int stat0 = 0; + if(!m_device->getChargerRegister(BQ25180_REG_STAT0, &stat0)) + { + stat0Label->setText("read error"); + return; + } + + int chg = (stat0 >> 5) & 0x03; /* CHG_STAT[1:0] */ + QString modeCode = (chg == 1) ? "cc" : (chg == 2) ? "cv" : (chg == 3) ? "done" : "not"; + updateChargingModeLabel(modeCode); + + QString s = QString("0x%1").arg(stat0, 2, 16, QChar('0')).toUpper(); + if(stat0 & 0x01) s += " VIN:good"; else s += " VIN:---"; + if(stat0 & 0x02) s += " THERMREG"; + stat0Label->setText(s); +} + void ChargerWnd::onReadClicked() { if(m_device == nullptr) return; @@ -136,6 +337,23 @@ void ChargerWnd::onReadClicked() if(idx >= 0) maxCurrentCombo->setCurrentIndex(idx); } + int buvlo; + if(readRegisterField(BQ25180_REG_CHARGERCTRL1, BUVLO_MASK, BUVLO_SHIFT, &buvlo)) + { + if(buvlo < 2) buvlo = 2; /* codes 0/1/2 all map to 3.0 V */ + int idx = comboIndexForData(buvloCombo, buvlo); + if(idx >= 0) buvloCombo->setCurrentIndex(idx); + } + + int vlowv; + if(readRegisterField(BQ25180_REG_IC_CTRL, VLOWV_MASK, VLOWV_SHIFT, &vlowv)) + { + int idx = comboIndexForData(vlowvCombo, vlowv); + if(idx >= 0) vlowvCombo->setCurrentIndex(idx); + } + + readStatus(); + statusLabel->setText("Parameters read from device."); } @@ -148,6 +366,8 @@ void ChargerWnd::onSetClicked() ok &= m_device->setChargerTermCurrent(termCurrentCombo->currentData().toInt()); ok &= m_device->setChargerCurrent(chargeCurrentEdit->text().toInt()); ok &= m_device->setChargerMaxChargingCurrent(maxCurrentCombo->currentData().toInt()); + ok &= setRegisterField(BQ25180_REG_CHARGERCTRL1, BUVLO_MASK, BUVLO_SHIFT, buvloCombo->currentData().toInt()); + ok &= setRegisterField(BQ25180_REG_IC_CTRL, VLOWV_MASK, VLOWV_SHIFT, vlowvCombo->currentData().toInt()); statusLabel->setText(ok ? "Parameters applied." : "Failed to apply one or more parameters."); @@ -155,6 +375,49 @@ void ChargerWnd::onSetClicked() onReadClicked(); } +void ChargerWnd::onStoreClicked() +{ + if(m_device == nullptr) return; + /* Persist the currently applied settings to EEPROM. "Set" should be used + * first so the values the firmware stores are the intended ones. */ + bool ok = m_device->storeChargerConfig(); + statusLabel->setText(ok ? "Settings stored to EEPROM." : "Failed to store settings."); +} + +void ChargerWnd::onApplyChemistryClicked() +{ + if(m_device == nullptr) return; + + float termV; + int vlowvCode; + int buvloCode; + + switch(chemistryCombo->currentIndex()) + { + case 0: /* Li-Ion / LiPo */ + termV = 4.20f; vlowvCode = 0 /*3.0V*/; buvloCode = 2 /*3.0V*/; + break; + case 1: /* LiFePO4 */ + termV = 3.60f; vlowvCode = 1 /*2.8V*/; buvloCode = 5 /*2.4V*/; + break; + default: + return; + } + + /* Push preset into the editable fields, then apply via the normal path. */ + termVoltageEdit->setText(QString::number(termV, 'f', 2)); + int vIdx = comboIndexForData(vlowvCombo, vlowvCode); if(vIdx >= 0) vlowvCombo->setCurrentIndex(vIdx); + int bIdx = comboIndexForData(buvloCombo, buvloCode); if(bIdx >= 0) buvloCombo->setCurrentIndex(bIdx); + + onSetClicked(); + statusLabel->setText("Chemistry preset applied: " + chemistryCombo->currentText()); +} + +void ChargerWnd::onChargerModeObtained(QString mode) +{ + updateChargingModeLabel(mode.trimmed().toLower()); +} + void ChargerWnd::onStartChargingClicked() { if(m_device == nullptr) return; @@ -168,3 +431,138 @@ void ChargerWnd::onStopChargingClicked() bool ok = m_device->setChargerStatus(false); statusLabel->setText(ok ? "Charging stopped." : "Failed to stop charging."); } + +/* ---- EEPROM ---- */ + +void ChargerWnd::renderEeprom(const QByteArray& data) +{ + const int bytesPerLine = 16; + QString formatted; + + for(int i = 0; i < data.size(); i++) + { + if((i % bytesPerLine) == 0) + formatted += QString("%1: ").arg(i, 8, 16, QChar('0')).toUpper(); + + quint8 byte = (quint8)data.at(i); + QString hex = QString("%1").arg(byte, 2, 16, QChar('0')).toUpper(); + + bool changed = (i < m_prevEepromData.size()) && (byte != (quint8)m_prevEepromData.at(i)); + if(changed) formatted += "" + hex + " "; + else formatted += hex + " "; + + if((i % bytesPerLine) == bytesPerLine - 1 || i == data.size() - 1) + { + int lineStart = i - (i % bytesPerLine); + QString ascii = " |"; + for(int j = lineStart; j <= i; j++) + { + quint8 c = (quint8)data.at(j); + ascii += (c >= 32 && c <= 126) ? QChar(c) : QChar('.'); + } + ascii += "|"; + formatted += ascii + "
"; + } + } + + eepromEdit->setHtml("
" + formatted + "
"); + m_prevEepromData = data; +} + +void ChargerWnd::onEepromReadClicked() +{ + if(m_device == nullptr) return; + QByteArray data; + if(!m_device->readChargerEeprom(&data)) + { + statusLabel->setText("EEPROM read failed."); + return; + } + m_eepromData = data; + renderEeprom(m_eepromData); + statusLabel->setText(QString("EEPROM read (%1 bytes).").arg(data.size())); +} + +void ChargerWnd::onEepromWriteClicked() +{ + if(m_device == nullptr) return; + + if(m_eepromData.isEmpty()) + { + statusLabel->setText("Nothing to write - Read or Import first."); + return; + } + + int size = m_device->getChargerEepromSize(); + if(size > 0 && m_eepromData.size() != size) + { + QMessageBox::warning(this, "EEPROM", + QString("EEPROM is %1 bytes, buffer holds %2.").arg(size).arg(m_eepromData.size())); + return; + } + + if(!m_device->writeChargerEeprom(m_eepromData)) + { + statusLabel->setText("EEPROM write failed."); + return; + } + onEepromReadClicked(); /* refresh view from device first */ + statusLabel->setText("EEPROM image successfully written."); /* final message */ +} + +void ChargerWnd::onEepromFormatClicked() +{ + if(m_device == nullptr) return; + if(QMessageBox::question(this, "Format EEPROM", + "Erase the entire EEPROM to 0xFF?") != QMessageBox::Yes) return; + + if(!m_device->formatChargerEeprom()) + { + statusLabel->setText("EEPROM format failed."); + return; + } + statusLabel->setText("EEPROM formatted."); + onEepromReadClicked(); +} + +void ChargerWnd::onEepromImportClicked() +{ + QString fileName = QFileDialog::getOpenFileName(this, "Import EEPROM image", "", + "Binary (*.bin);;All files (*)"); + if(fileName.isEmpty()) return; + + QFile f(fileName); + if(!f.open(QIODevice::ReadOnly)) + { + statusLabel->setText("Cannot open file for reading."); + return; + } + m_eepromData = f.readAll(); + f.close(); + + renderEeprom(m_eepromData); + statusLabel->setText(QString("Imported %1 bytes (press Write to program).").arg(m_eepromData.size())); +} + +void ChargerWnd::onEepromExportClicked() +{ + if(m_eepromData.isEmpty()) + { + statusLabel->setText("Nothing to export - Read or Import first."); + return; + } + + QString fileName = QFileDialog::getSaveFileName(this, "Export EEPROM image", "charger_eeprom.bin", + "Binary (*.bin);;All files (*)"); + if(fileName.isEmpty()) return; + + QFile f(fileName); + if(!f.open(QIODevice::WriteOnly)) + { + statusLabel->setText("Cannot open file for writing."); + return; + } + f.write(m_eepromData); + f.close(); + statusLabel->setText("EEPROM exported to " + fileName); +} diff --git a/Source/Windows/Charger/chargerwnd.h b/Source/Windows/Charger/chargerwnd.h index d08f25b..559e706 100644 --- a/Source/Windows/Charger/chargerwnd.h +++ b/Source/Windows/Charger/chargerwnd.h @@ -2,20 +2,23 @@ #define CHARGERWND_H #include +#include class Device; class QLineEdit; class QComboBox; class QLabel; class QPushButton; +class QTextEdit; /** * @brief Dedicated settings window for a Charger device (BQ25180). * * Unlike EPP devices (which open the full DeviceWnd), a Charger device opens - * only this focused window: charging parameters, start/stop control and device - * identification. It talks to the charger through the Device charger wrappers, - * which in turn use the serial ControlLink. + * only this focused window: charging parameters, protection thresholds + * (BUVLO/VLOWV), battery-chemistry presets, start/stop control, live charging + * mode (pushed by the firmware) and device identification. It talks to the + * charger through the Device charger wrappers, which use the serial ControlLink. */ class ChargerWnd : public QWidget { @@ -26,27 +29,66 @@ class ChargerWnd : public QWidget private slots: void onReadClicked(); void onSetClicked(); + void onStoreClicked(); void onStartChargingClicked(); void onStopChargingClicked(); + void onApplyChemistryClicked(); + void onChargerModeObtained(QString mode); + void onEepromReadClicked(); + void onEepromWriteClicked(); + void onEepromFormatClicked(); + void onEepromExportClicked(); + void onEepromImportClicked(); private: void buildUi(); int comboIndexForData(QComboBox* combo, int value); + void updateChargingModeLabel(const QString& modeCode); + bool setRegisterField(int reg, int mask, int shift, int value); + bool readRegisterField(int reg, int mask, int shift, int* value); + void readStatus(); + void renderEeprom(const QByteArray& data); Device* m_device; + /* Identification */ QLineEdit* hwSerialEdit; QLineEdit* fwVersionEdit; - QLineEdit* startVoltageEdit; /* firmware support pending (Phase 2) */ + + /* Charging parameters */ QLineEdit* termVoltageEdit; QComboBox* termCurrentCombo; QLineEdit* chargeCurrentEdit; QComboBox* maxCurrentCombo; + + /* Protection thresholds */ + QComboBox* buvloCombo; /* Battery UVLO falling threshold (CHARGECTRL1[5:3]) */ + QComboBox* vlowvCombo; /* Precharge voltage threshold (IC_CTRL[6]) */ + + /* Battery chemistry presets */ + QComboBox* chemistryCombo; + QPushButton* applyChemistryPusb; + + /* Status */ + QLabel* chargingModeLabel; + QLabel* stat0Label; QLabel* statusLabel; + QPushButton* readPusb; QPushButton* setPusb; + QPushButton* storePusb; QPushButton* startPusb; QPushButton* stopPusb; + + /* EEPROM (AT24CS01) memory view */ + QTextEdit* eepromEdit; + QPushButton* eepromReadPusb; + QPushButton* eepromWritePusb; + QPushButton* eepromFormatPusb; + QPushButton* eepromExportPusb; + QPushButton* eepromImportPusb; + QByteArray m_eepromData; /* current buffer (from Read or Import) */ + QByteArray m_prevEepromData; /* for changed-byte highlighting */ }; #endif // CHARGERWND_H diff --git a/Source/device.cpp b/Source/device.cpp index 13d7553..6e11490 100644 --- a/Source/device.cpp +++ b/Source/device.cpp @@ -95,6 +95,7 @@ void Device::controlLinkAssign(ControlLink* link) controlLink = link; connect(controlLink, SIGNAL(sigConnected()), this, SLOT(onControlLinkConnected())); connect(controlLink, SIGNAL(sigDisconnected()), this, SLOT(onControlLinkDisconnected())); + connect(controlLink, SIGNAL(sigAsyncMessage(QString)), this, SLOT(onControlLinkAsyncMessage(QString))); m_params->setParamValue("deviceIp",link->getDeviceIP_Addr()); m_params->setParamValue("controlPort",QString::number(link->getDeviceIP_Port())); emit sigControlLinkConnected(); @@ -1808,6 +1809,84 @@ bool Device::getChargerFWVersion(QString *version) return true; } +bool Device::getChargerRegister(int reg, int* value) +{ + QString response; + QString command = QString("charger reg read -reg=%1").arg(reg, 0, 16); + if(controlLink == NULL) return false; + if(!controlLink->executeCommand(command, &response, 1000)) return false; + /* Firmware replies "OK: 0x" */ + int idx = response.indexOf("0x"); + if(idx < 0) return false; + bool ok = false; + int v = response.mid(idx + 2).trimmed().toInt(&ok, 16); + if(!ok) return false; + if(value != NULL) *value = v; + return true; +} + +bool Device::setChargerRegister(int reg, int value) +{ + QString response; + QString command = QString("charger reg write -reg=%1 -value=%2").arg(reg, 0, 16).arg(value, 0, 16); + if(controlLink == NULL) return false; + if(!controlLink->executeCommand(command, &response, 1000)) return false; + if(response != "OK") return false; + return true; +} + +void Device::onControlLinkAsyncMessage(QString message) +{ + /* Asynchronous status-link notifications pushed by the charger firmware, + * e.g. "charger charging mode cc" on a charging-mode change. */ + QString m = message.trimmed(); + if(m.startsWith("charger charging mode")) + { + QString mode = m.section(' ', 3).trimmed(); /* word after "... mode " */ + emit sigChargerModeObtained(mode); + } +} + +int Device::getChargerEepromSize() +{ + if(controlLink == NULL) return -1; + QString response; + if(!controlLink->executeCommand(QString("charger bd size get"), &response, 1000)) return -1; + bool ok = false; + int sz = response.trimmed().toInt(&ok); + return ok ? sz : -1; +} + +bool Device::readChargerEeprom(QByteArray* out) +{ + if(controlLink == NULL || out == NULL) return false; + /* Full board-data read via the binary response frame. */ + return controlLink->transferBdRead(out, 3000); +} + +bool Device::writeChargerEeprom(const QByteArray& data) +{ + if(controlLink == NULL) return false; + /* Full board-data write via the binary request frame. */ + return controlLink->transferBdWrite(data, 3000); +} + +bool Device::formatChargerEeprom() +{ + if(controlLink == NULL) return false; + QString response; + if(!controlLink->executeCommand(QString("charger bd format"), &response, 5000)) return false; + return (response == "OK"); +} + +bool Device::storeChargerConfig() +{ + if(controlLink == NULL) return false; + QString response; + if(!controlLink->executeCommand(QString("charger config store"), &response, 3000)) return false; + return (response == "OK"); +} + bool Device::latchTrigger() { QString response; diff --git a/Source/device.h b/Source/device.h index bcb6100..44ee122 100644 --- a/Source/device.h +++ b/Source/device.h @@ -191,6 +191,13 @@ class Device : public QObject bool getChargerTermVoltage(float* voltage= NULL); bool getChargerHWSerial(QString* serial= NULL); bool getChargerFWVersion(QString* version= NULL); + bool getChargerRegister(int reg, int* value = NULL); + bool setChargerRegister(int reg, int value); + int getChargerEepromSize(); + bool readChargerEeprom(QByteArray* out); + bool writeChargerEeprom(const QByteArray& data); + bool formatChargerEeprom(); + bool storeChargerConfig(); bool latchTrigger(); bool getUVoltageStatus(bool* status = NULL); bool getOVoltageStatus(bool* status = NULL); @@ -251,6 +258,7 @@ class Device : public QObject void sigChargerHWSerialObtained(QString serial); void sigChargerFWVersionObtained(QString serial); + void sigChargerModeObtained(QString mode); void sigAvgRatio(QString voffset); @@ -267,6 +275,7 @@ class Device : public QObject public slots: void onControlLinkConnected(); void onControlLinkDisconnected(); + void onControlLinkAsyncMessage(QString message); private slots: void onStatusLinkNewDeviceAdded(QString aDeviceIP); diff --git a/Source/resource.qrc b/Source/resource.qrc index c0d85d4..909e82a 100644 --- a/Source/resource.qrc +++ b/Source/resource.qrc @@ -47,6 +47,7 @@ images/NewSet/format.png images/NewSet/filter.png images/NewSet/export.png + images/NewSet/import.png images/NewSet/lock.png images/NewSet/delete.png From 579a8372052e67917b36d7626be0a4e6b1a9b3b9 Mon Sep 17 00:00:00 2001 From: hturkmanovic Date: Fri, 9 Oct 2026 16:49:40 +0200 Subject: [PATCH 4/6] Latest --- Source/Links/controllink.cpp | 229 +++-------------------- Source/Links/controllink.h | 22 +-- Source/Links/seriallink.cpp | 254 ++++++++++++++++++++++++++ Source/Links/seriallink.h | 92 ++++++++++ Source/OpenEPT.pro | 2 + Source/Windows/Charger/chargerwnd.cpp | 211 +++++++++++++++++++-- Source/Windows/Charger/chargerwnd.h | 24 +++ Source/Windows/Plot/plot.cpp | 7 +- Source/Windows/Plot/plot.h | 2 + Source/device.cpp | 61 +++++++ Source/device.h | 6 + 11 files changed, 677 insertions(+), 233 deletions(-) create mode 100644 Source/Links/seriallink.cpp create mode 100644 Source/Links/seriallink.h diff --git a/Source/Links/controllink.cpp b/Source/Links/controllink.cpp index b364a27..9207dfb 100644 --- a/Source/Links/controllink.cpp +++ b/Source/Links/controllink.cpp @@ -1,8 +1,8 @@ #include "controllink.h" +#include "seriallink.h" #include #include #include -#include #ifdef Q_OS_WIN #include "Ws2tcpip.h" @@ -20,11 +20,8 @@ ControlLink::ControlLink(QObject *parent) { tcpSocket = new QTcpSocket(this); tcpSocket->setSocketOption(QAbstractSocket::KeepAliveOption, 1); - serialPort = nullptr; - transport = CONTROL_LINK_TRANSPORT_TCP; - serialResponseOk = false; - serialResponseGot = false; - serialResponseIsBinary = false; + serialLink = nullptr; + transport = CONTROL_LINK_TRANSPORT_TCP; ipAddress = "0.0.0.0"; serialPortName = ""; portNumber = 0; @@ -36,16 +33,13 @@ ControlLink::ControlLink(QObject *parent) ControlLink::~ControlLink() { - if(linkStatus == CONTROL_LINK_STATUS_ESTABLISHED) + if(transport == CONTROL_LINK_TRANSPORT_SERIAL) { - if(transport == CONTROL_LINK_TRANSPORT_SERIAL) - { - if(serialPort != nullptr && serialPort->isOpen()) serialPort->close(); - } - else - { - tcpSocket->disconnect(); - } + if(serialLink != nullptr) { serialLink->close(); delete serialLink; serialLink = nullptr; } + } + else if(linkStatus == CONTROL_LINK_STATUS_ESTABLISHED) + { + tcpSocket->disconnect(); } } @@ -70,36 +64,19 @@ control_link_status_t ControlLink::establishSerialLink(QString aPortName, qint serialPortName = aPortName; ipAddress = aPortName; /* reuse for display (getDeviceIP_Addr) */ - if(serialPort == nullptr) + if(serialLink == nullptr) { - serialPort = new QSerialPort(this); - connect(serialPort, SIGNAL(errorOccurred(QSerialPort::SerialPortError)), - this, SLOT(onSerialErrorOccurred(QSerialPort::SerialPortError))); - connect(serialPort, SIGNAL(readyRead()), this, SLOT(onSerialReadyRead())); + serialLink = new SerialLink(this); + connect(serialLink, &SerialLink::sigAsyncMessage, this, &ControlLink::sigAsyncMessage); + connect(serialLink, &SerialLink::sigDisconnected, this, &ControlLink::onSerialDisconnected); } - if(serialPort->isOpen()) serialPort->close(); - - serialRxBuffer.clear(); - serialResponseGot = false; - - serialPort->setPortName(aPortName); - serialPort->setBaudRate(aBaudRate); /* ignored by USB CDC, required by API */ - serialPort->setDataBits(QSerialPort::Data8); - serialPort->setParity(QSerialPort::NoParity); - serialPort->setStopBits(QSerialPort::OneStop); - serialPort->setFlowControl(QSerialPort::NoFlowControl); - - if(!serialPort->open(QIODevice::ReadWrite)) + if(!serialLink->open(aPortName, aBaudRate)) { linkStatus = CONTROL_LINK_STATUS_DISABLED; return linkStatus; } - /* Assert DTR so the firmware's CDC driver enables the TX path (dtrActive) */ - serialPort->setDataTerminalReady(true); - serialPort->clear(); - linkStatus = CONTROL_LINK_STATUS_ESTABLISHED; /* Mirror TCP timing: deliver sigConnected through the event loop, after the @@ -109,140 +86,22 @@ control_link_status_t ControlLink::establishSerialLink(QString aPortName, qint return linkStatus; } -bool ControlLink::prvReadSerialResponse(QString* response, int timeout) +void ControlLink::onSerialDisconnected() { - /* Responses arrive asynchronously in onSerialReadyRead(), which sets - * serialResponseGot and emits sigSerialResponseReady(). Wait for it (or the - * timeout) via a local event loop, so readyRead keeps firing and any - * asynchronous notifications continue to be dispatched meanwhile. */ - if(!serialResponseGot) - { - QEventLoop loop; - QTimer timer; - timer.setSingleShot(true); - connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit); - connect(this, &ControlLink::sigSerialResponseReady, &loop, &QEventLoop::quit); - timer.start(timeout); - loop.exec(); - } - - if(serialResponseGot) - { - if(!serialResponseIsBinary && response != nullptr) - *response = serialResponsePayload; - return serialResponseOk; - } - - if(response != nullptr) *response = "Unable to read data"; - return false; -} - -void ControlLink::onSerialReadyRead() -{ - if(serialPort == nullptr) return; - - serialRxBuffer += serialPort->readAll(); - - for(;;) - { - /* Binary board-data response frame: 0xA5 0xA5 'R' */ - if(serialRxBuffer.size() >= 3 && - (quint8)serialRxBuffer.at(0) == 0xA5 && - (quint8)serialRxBuffer.at(1) == 0xA5 && - serialRxBuffer.at(2) == 'R') - { - if(serialRxBuffer.size() < 5) return; /* need the size field */ - quint16 size = (quint8)serialRxBuffer.at(3) | ((quint16)(quint8)serialRxBuffer.at(4) << 8); - if(serialRxBuffer.size() < 5 + (int)size) return; /* wait for all data */ - - serialBinaryPayload = serialRxBuffer.mid(5, size); - serialRxBuffer.remove(0, 5 + size); - serialResponseOk = true; - serialResponseGot = true; - serialResponseIsBinary = true; - emit sigSerialResponseReady(); - continue; - } - - int nlIndex = serialRxBuffer.indexOf('\n'); - if(nlIndex < 0) return; - - QByteArray lineBytes = serialRxBuffer.left(nlIndex); - serialRxBuffer.remove(0, nlIndex + 1); - - QString line = QString::fromUtf8(lineBytes).trimmed(); - if(line.isEmpty()) continue; - - if(line.startsWith("STATUS")) - { - serialResponsePayload = line.mid(6).trimmed(); /* payload after "STATUS" */ - serialResponseOk = true; - serialResponseGot = true; - serialResponseIsBinary = false; - emit sigSerialResponseReady(); - } - else if(line.startsWith("ERROR")) - { - serialResponsePayload = "ERROR"; - serialResponseOk = false; - serialResponseGot = true; - serialResponseIsBinary = false; - emit sigSerialResponseReady(); - } - else - { - /* Asynchronous status-link notification (e.g. charger mode change) */ - emit sigAsyncMessage(line); - } - } + linkStatus = CONTROL_LINK_STATUS_DISABLED; + emit sigDisconnected(); } bool ControlLink::transferBdWrite(const QByteArray& data, int timeout) { - if(transport != CONTROL_LINK_TRANSPORT_SERIAL) return false; - if(serialPort == nullptr || !serialPort->isOpen() || linkStatus != CONTROL_LINK_STATUS_ESTABLISHED) return false; - - quint16 size = (quint16)data.size(); - - QByteArray frame; - frame.append((char)0xA5); - frame.append((char)0xA5); - frame.append('W'); - frame.append((char)(size & 0xFF)); - frame.append((char)((size >> 8) & 0xFF)); - frame.append(data); - - serialResponseGot = false; - serialResponseIsBinary = false; - - if(serialPort->write(frame) == -1) return false; - if(!serialPort->waitForBytesWritten(timeout)) return false; - - QString response; - if(!prvReadSerialResponse(&response, timeout)) return false; - return (response == "OK"); + if(transport != CONTROL_LINK_TRANSPORT_SERIAL || serialLink == nullptr) return false; + return serialLink->transferBdWrite(data, timeout); } bool ControlLink::transferBdRead(QByteArray* out, int timeout) { - if(out == nullptr) return false; - if(transport != CONTROL_LINK_TRANSPORT_SERIAL) return false; - if(serialPort == nullptr || !serialPort->isOpen() || linkStatus != CONTROL_LINK_STATUS_ESTABLISHED) return false; - - serialResponseGot = false; - serialResponseIsBinary = false; - - QByteArray cmd("charger bd read"); - cmd.append('\r'); - if(serialPort->write(cmd) == -1) return false; - if(!serialPort->waitForBytesWritten(timeout)) return false; - - QString dummy; - if(!prvReadSerialResponse(&dummy, timeout)) return false; - if(!serialResponseIsBinary) return false; /* got an error line instead */ - - *out = serialBinaryPayload; - return true; + if(transport != CONTROL_LINK_TRANSPORT_SERIAL || serialLink == nullptr) return false; + return serialLink->transferBdRead(out, timeout); } bool ControlLink::prvReadResponse(QString* response, int timeout) @@ -319,24 +178,6 @@ void ControlLink::reconnect() tcpSocket->waitForConnected(10); } -void ControlLink::onSerialErrorOccurred(QSerialPort::SerialPortError error) -{ - if(error == QSerialPort::NoError) return; - - /* Treat fatal transport errors (device unplugged, etc.) as a disconnect. */ - if(error == QSerialPort::ResourceError || - error == QSerialPort::PermissionError || - error == QSerialPort::DeviceNotFoundError) - { - if(linkStatus != CONTROL_LINK_STATUS_DISABLED) - { - linkStatus = CONTROL_LINK_STATUS_DISABLED; - if(serialPort != nullptr && serialPort->isOpen()) serialPort->close(); - emit sigDisconnected(); - } - } -} - bool ControlLink::getDeviceName(QString *deviceName) { QString response; @@ -357,33 +198,11 @@ bool ControlLink::executeCommand(QString command, QString* response, int timeout return false; } - /* ===== SERIAL (Charger): plain text terminated by CR ===== */ + /* ===== SERIAL (Charger): delegated to the threaded SerialLink ===== */ if(transport == CONTROL_LINK_TRANSPORT_SERIAL) { - if(serialPort == nullptr || !serialPort->isOpen()) - { - *response = "Control link not established"; - return false; - } - - /* Arm response capture before sending (onSerialReadyRead fills it in). */ - serialResponseGot = false; - - QByteArray packet = command.toUtf8(); - packet.append('\r'); - - if(serialPort->write(packet) == -1) - { - *response = "Write failed"; - return false; - } - if(!serialPort->waitForBytesWritten(timeout)) - { - *response = "Write timeout"; - return false; - } - - return prvReadSerialResponse(response, timeout); + if(serialLink == nullptr) { *response = "Control link not established"; return false; } + return serialLink->executeCommand(command, response, timeout); } /* ===== TCP (EPP): binary 0xA5 framing ===== */ diff --git a/Source/Links/controllink.h b/Source/Links/controllink.h index fa0fd3c..fe33753 100644 --- a/Source/Links/controllink.h +++ b/Source/Links/controllink.h @@ -3,7 +3,6 @@ #include #include -#include #include #include @@ -11,10 +10,12 @@ #define CONTROL_LINK_RECONNECT_PERIOD 1000 #define CONTROL_LINK_SERIAL_BAUDRATE 115200 +class SerialLink; + /* Transport used by the control link */ typedef enum{ CONTROL_LINK_TRANSPORT_TCP, /*!< EPP devices over TCP/IP (binary 0xA5 framing) */ - CONTROL_LINK_TRANSPORT_SERIAL /*!< Charger device over USB CDC serial (text, \r terminated) */ + CONTROL_LINK_TRANSPORT_SERIAL /*!< Charger device over USB CDC serial (via SerialLink) */ }control_link_transport_t; typedef enum{ @@ -45,20 +46,18 @@ class ControlLink : public QObject signals: void sigDisconnected(); void sigConnected(); - void sigAsyncMessage(QString message); /*!< Unsolicited status-link notification (serial) */ - void sigSerialResponseReady(); /*!< Internal: command response line received */ + void sigAsyncMessage(QString message); /*!< Unsolicited notification (serial) */ public slots: void onDisconnected(); void onReconnected(); void reconnect(); - void onSerialErrorOccurred(QSerialPort::SerialPortError error); - void onSerialReadyRead(); + void onSerialDisconnected(); private: QTcpSocket *tcpSocket; - QSerialPort *serialPort; + SerialLink *serialLink; /*!< Threaded serial transport (charger) */ control_link_transport_t transport; QString ipAddress; QString serialPortName; @@ -66,17 +65,8 @@ public slots: control_link_status_t linkStatus; QTimer *reconnectTimer; - /* Serial async reception / response capture */ - QByteArray serialRxBuffer; - QString serialResponsePayload; - QByteArray serialBinaryPayload; /* payload of a binary 'R' response frame */ - bool serialResponseOk; - bool serialResponseGot; - bool serialResponseIsBinary; - bool setSocketKeepAlive(); bool prvReadResponse(QString* response, int timeout); - bool prvReadSerialResponse(QString* response, int timeout); }; diff --git a/Source/Links/seriallink.cpp b/Source/Links/seriallink.cpp new file mode 100644 index 0000000..28340d1 --- /dev/null +++ b/Source/Links/seriallink.cpp @@ -0,0 +1,254 @@ +#include "seriallink.h" +#include + +/* ======================= SerialWorker (worker thread) ======================= */ + +SerialWorker::SerialWorker(QObject* parent) : + QObject(parent), + m_port(nullptr) +{ +} + +SerialWorker::~SerialWorker() +{ + if(m_port != nullptr && m_port->isOpen()) + m_port->close(); +} + +bool SerialWorker::openPort(const QString& name, int baud) +{ + /* Created here so the QSerialPort has affinity to the worker thread. */ + if(m_port == nullptr) + { + m_port = new QSerialPort(this); + connect(m_port, SIGNAL(readyRead()), this, SLOT(onReadyRead())); + connect(m_port, SIGNAL(errorOccurred(QSerialPort::SerialPortError)), + this, SLOT(onErrorOccurred(QSerialPort::SerialPortError))); + } + + if(m_port->isOpen()) m_port->close(); + m_rxBuffer.clear(); + + m_port->setPortName(name); + m_port->setBaudRate(baud); + m_port->setDataBits(QSerialPort::Data8); + m_port->setParity(QSerialPort::NoParity); + m_port->setStopBits(QSerialPort::OneStop); + m_port->setFlowControl(QSerialPort::NoFlowControl); + + if(!m_port->open(QIODevice::ReadWrite)) + return false; + + m_port->setDataTerminalReady(true); /* enable firmware TX path */ + m_port->clear(); + return true; +} + +void SerialWorker::closePort() +{ + if(m_port != nullptr && m_port->isOpen()) + m_port->close(); +} + +void SerialWorker::writeData(const QByteArray& data) +{ + if(m_port != nullptr && m_port->isOpen()) + { + m_port->write(data); + m_port->flush(); + } +} + +void SerialWorker::onReadyRead() +{ + if(m_port == nullptr) return; + m_rxBuffer += m_port->readAll(); + + for(;;) + { + /* Binary board-data response frame: 0xA5 0xA5 'R' */ + if(m_rxBuffer.size() >= 3 && + (quint8)m_rxBuffer.at(0) == 0xA5 && + (quint8)m_rxBuffer.at(1) == 0xA5 && + m_rxBuffer.at(2) == 'R') + { + if(m_rxBuffer.size() < 5) return; + quint16 size = (quint8)m_rxBuffer.at(3) | ((quint16)(quint8)m_rxBuffer.at(4) << 8); + if(m_rxBuffer.size() < 5 + (int)size) return; + + QByteArray data = m_rxBuffer.mid(5, size); + m_rxBuffer.remove(0, 5 + size); + emit responseBinary(data); + continue; + } + + int nlIndex = m_rxBuffer.indexOf('\n'); + if(nlIndex < 0) return; + + QByteArray lineBytes = m_rxBuffer.left(nlIndex); + m_rxBuffer.remove(0, nlIndex + 1); + + QString line = QString::fromUtf8(lineBytes).trimmed(); + if(line.isEmpty()) continue; + + if(line.startsWith("STATUS")) emit responseText(line.mid(6).trimmed(), true); + else if(line.startsWith("ERROR")) emit responseText("ERROR", false); + else emit asyncMessage(line); + } +} + +void SerialWorker::onErrorOccurred(QSerialPort::SerialPortError error) +{ + if(error == QSerialPort::ResourceError || + error == QSerialPort::PermissionError || + error == QSerialPort::DeviceNotFoundError) + { + if(m_port != nullptr && m_port->isOpen()) m_port->close(); + emit disconnected(); + } +} + +/* ============================ SerialLink (UI thread) ======================== */ + +SerialLink::SerialLink(QObject* parent) : + QObject(parent), + m_worker(new SerialWorker), + m_open(false), + m_responseReady(false), + m_responseOk(false), + m_responseIsBinary(false) +{ + m_worker->moveToThread(&m_thread); + connect(&m_thread, &QThread::finished, m_worker, &QObject::deleteLater); + + /* Responses are delivered via a DirectConnection so they run in the worker + * thread and can wake a caller that is blocked in executeCommand(). */ + connect(m_worker, &SerialWorker::responseText, this, &SerialLink::onWorkerResponseText, Qt::DirectConnection); + connect(m_worker, &SerialWorker::responseBinary, this, &SerialLink::onWorkerResponseBinary, Qt::DirectConnection); + + /* Async notifications are relayed to the UI as queued signals. */ + connect(m_worker, &SerialWorker::asyncMessage, this, &SerialLink::sigAsyncMessage); + connect(m_worker, &SerialWorker::disconnected, this, &SerialLink::onWorkerDisconnected); + + m_thread.start(); +} + +SerialLink::~SerialLink() +{ + close(); + m_thread.quit(); + m_thread.wait(1000); +} + +bool SerialLink::open(const QString& portName, qint32 baud) +{ + bool ok = false; + QMetaObject::invokeMethod(m_worker, "openPort", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(bool, ok), + Q_ARG(QString, portName), Q_ARG(int, (int)baud)); + m_open = ok; + return ok; +} + +void SerialLink::close() +{ + if(m_worker != nullptr) + QMetaObject::invokeMethod(m_worker, "closePort", Qt::BlockingQueuedConnection); + m_open = false; +} + +void SerialLink::onWorkerResponseText(QString payload, bool ok) +{ + QMutexLocker locker(&m_mutex); + m_responsePayload = payload; + m_responseOk = ok; + m_responseIsBinary = false; + m_responseReady = true; + m_cond.wakeAll(); +} + +void SerialLink::onWorkerResponseBinary(QByteArray data) +{ + QMutexLocker locker(&m_mutex); + m_responseBinary = data; + m_responseOk = true; + m_responseIsBinary = true; + m_responseReady = true; + m_cond.wakeAll(); +} + +void SerialLink::onWorkerDisconnected() +{ + m_open = false; + emit sigDisconnected(); +} + +bool SerialLink::waitForResponse(int timeout) +{ + QDeadlineTimer deadline(timeout); + while(!m_responseReady) + { + qint64 remaining = deadline.remainingTime(); + if(remaining <= 0) return false; + m_cond.wait(&m_mutex, (unsigned long)remaining); + } + return true; +} + +bool SerialLink::executeCommand(const QString& command, QString* response, int timeout) +{ + if(!m_open) { if(response) *response = "Serial link not open"; return false; } + + QByteArray data = command.toUtf8(); + data.append('\r'); + + QMutexLocker locker(&m_mutex); + m_responseReady = false; + QMetaObject::invokeMethod(m_worker, "writeData", Qt::QueuedConnection, Q_ARG(QByteArray, data)); + + if(!waitForResponse(timeout)) { if(response) *response = "Unable to read data"; return false; } + if(m_responseIsBinary) { if(response) *response = "unexpected binary"; return false; } + + if(response) *response = m_responsePayload; + return m_responseOk; +} + +bool SerialLink::transferBdWrite(const QByteArray& data, int timeout) +{ + if(!m_open) return false; + + quint16 size = (quint16)data.size(); + QByteArray frame; + frame.append((char)0xA5); + frame.append((char)0xA5); + frame.append('W'); + frame.append((char)(size & 0xFF)); + frame.append((char)((size >> 8) & 0xFF)); + frame.append(data); + + QMutexLocker locker(&m_mutex); + m_responseReady = false; + QMetaObject::invokeMethod(m_worker, "writeData", Qt::QueuedConnection, Q_ARG(QByteArray, frame)); + + if(!waitForResponse(timeout)) return false; + if(m_responseIsBinary) return false; + return (m_responsePayload == "OK"); +} + +bool SerialLink::transferBdRead(QByteArray* out, int timeout) +{ + if(out == nullptr || !m_open) return false; + + QByteArray cmd("charger bd read"); + cmd.append('\r'); + + QMutexLocker locker(&m_mutex); + m_responseReady = false; + QMetaObject::invokeMethod(m_worker, "writeData", Qt::QueuedConnection, Q_ARG(QByteArray, cmd)); + + if(!waitForResponse(timeout)) return false; + if(!m_responseIsBinary) return false; + + *out = m_responseBinary; + return true; +} diff --git a/Source/Links/seriallink.h b/Source/Links/seriallink.h new file mode 100644 index 0000000..60bdf22 --- /dev/null +++ b/Source/Links/seriallink.h @@ -0,0 +1,92 @@ +#ifndef SERIALLINK_H +#define SERIALLINK_H + +#include +#include +#include +#include +#include +#include +#include + +/** + * @brief Worker that owns the QSerialPort and lives in a dedicated thread. + * All reception (readyRead parsing) happens here, off the UI thread. + */ +class SerialWorker : public QObject +{ + Q_OBJECT +public: + explicit SerialWorker(QObject* parent = nullptr); + ~SerialWorker(); + +public slots: + bool openPort(const QString& name, int baud); + void closePort(); + void writeData(const QByteArray& data); + +signals: + void asyncMessage(QString line); /*!< Unsolicited notification (voltage, charging mode, ...) */ + void responseText(QString payload, bool ok);/*!< STATUS/ERROR response line */ + void responseBinary(QByteArray data); /*!< 0xA5 0xA5 'R' binary response frame payload */ + void disconnected(); + +private slots: + void onReadyRead(); + void onErrorOccurred(QSerialPort::SerialPortError error); + +private: + QSerialPort* m_port; + QByteArray m_rxBuffer; +}; + +/** + * @brief Serial transport for the Charger device. Sending is a classic + * (blocking) wrapper that posts to the worker thread and waits for the + * response; asynchronous reception runs entirely in the worker thread + * and reaches the UI only as queued signals, so the UI thread is never + * loaded by the incoming sample stream. + */ +class SerialLink : public QObject +{ + Q_OBJECT +public: + explicit SerialLink(QObject* parent = nullptr); + ~SerialLink(); + + bool open(const QString& portName, qint32 baud = 115200); + void close(); + bool isOpen() const { return m_open; } + + /* Text command -> text (STATUS/ERROR) response. */ + bool executeCommand(const QString& command, QString* response, int timeout); + /* Binary board-data transfer (0xA5 framing). */ + bool transferBdWrite(const QByteArray& data, int timeout); + bool transferBdRead(QByteArray* out, int timeout); + +signals: + void sigAsyncMessage(QString line); + void sigDisconnected(); + +private slots: + void onWorkerResponseText(QString payload, bool ok); + void onWorkerResponseBinary(QByteArray data); + void onWorkerDisconnected(); + +private: + bool waitForResponse(int timeout); /* called with m_mutex held */ + + QThread m_thread; + SerialWorker* m_worker; + bool m_open; + + QMutex m_mutex; + QWaitCondition m_cond; + bool m_responseReady; + bool m_responseOk; + bool m_responseIsBinary; + QString m_responsePayload; + QByteArray m_responseBinary; +}; + +#endif // SERIALLINK_H diff --git a/Source/OpenEPT.pro b/Source/OpenEPT.pro index e40882c..80685d2 100644 --- a/Source/OpenEPT.pro +++ b/Source/OpenEPT.pro @@ -30,6 +30,7 @@ win32 { SOURCES += \ Chart/qcustomplot.cpp \ Links/controllink.cpp \ + Links/seriallink.cpp \ Links/edlink.cpp \ Links/statuslink.cpp \ Links/streamlink.cpp \ @@ -82,6 +83,7 @@ SOURCES += \ HEADERS += \ Chart/qcustomplot.h \ Links/controllink.h \ + Links/seriallink.h \ Links/edlink.h \ Links/statuslink.h \ Links/streamlink.h \ diff --git a/Source/Windows/Charger/chargerwnd.cpp b/Source/Windows/Charger/chargerwnd.cpp index 7e19373..e843d94 100644 --- a/Source/Windows/Charger/chargerwnd.cpp +++ b/Source/Windows/Charger/chargerwnd.cpp @@ -1,5 +1,6 @@ #include "chargerwnd.h" #include "device.h" +#include "Windows/Plot/plot.h" #include #include @@ -20,6 +21,10 @@ #include #include +/* Default scale before calibration: Vref 3.3 V, 12-bit, /2 input divider. */ +#define CHARGER_DEFAULT_CAL_GAIN (3.3 / 4095.0 * 2.0) +#define CHARGER_PLOT_WINDOW_MS 30000.0 /* live sliding window: last 30 s */ + #define CHARGER_ICON_BTN_SIZE 32 #define CHARGER_ICON_SIZE 24 @@ -42,8 +47,13 @@ QToolTip { background-color: white; color: black; border: 1px solid #ccc; } ChargerWnd::ChargerWnd(Device* device, QWidget *parent) : QWidget(parent), - m_device(device) + m_device(device), + m_acqTimeMs(0.0), + m_calGain(CHARGER_DEFAULT_CAL_GAIN), + m_lastRaw(0) { + /* Calibration gain (V_CAL) lives on the device EEPROM; it is fetched from + * the firmware in onReadClicked() below. Default until then. */ buildUi(); connect(readPusb, SIGNAL(clicked()), this, SLOT(onReadClicked())); @@ -58,9 +68,18 @@ ChargerWnd::ChargerWnd(Device* device, QWidget *parent) : connect(eepromImportPusb, SIGNAL(clicked()), this, SLOT(onEepromImportClicked())); connect(eepromExportPusb, SIGNAL(clicked()), this, SLOT(onEepromExportClicked())); - /* Live charging-mode updates pushed by the firmware over the serial link. */ + connect(acqStartPusb, SIGNAL(clicked()), this, SLOT(onStartAcquisitionClicked())); + connect(acqPausePusb, SIGNAL(clicked()), this, SLOT(onPauseAcquisitionClicked())); + connect(acqStopPusb, SIGNAL(clicked()), this, SLOT(onStopAcquisitionClicked())); + connect(calibratePusb, SIGNAL(clicked()), this, SLOT(onCalibrateClicked())); + + /* Live updates pushed by the firmware over the serial link. */ if(m_device != nullptr) + { connect(m_device, SIGNAL(sigChargerModeObtained(QString)), this, SLOT(onChargerModeObtained(QString))); + connect(m_device, SIGNAL(sigChargerStatusObtained(int)), this, SLOT(onChargerStatusObtained(int))); + connect(m_device, SIGNAL(sigChargerVoltageObtained(double,quint16)), this, SLOT(onChargerVoltageObtained(double,quint16))); + } /* Populate current values from the device. */ onReadClicked(); @@ -78,7 +97,72 @@ void ChargerWnd::buildUi() scroll->setFrameShape(QFrame::NoFrame); QWidget* content = new QWidget(); - QVBoxLayout* mainL = new QVBoxLayout(content); + QHBoxLayout* bodyL = new QHBoxLayout(content); + + /* ---- Left: live input-voltage plot + acquisition control ---- */ + QVBoxLayout* plotColL = new QVBoxLayout(); + voltageChart = new Plot(360, 260); + voltageChart->setTitle("Input voltage"); + voltageChart->setYLabel("[V]"); + voltageChart->setXLabel("[ms]"); + voltageChart->setTrackingWindow(CHARGER_PLOT_WINDOW_MS); /* show last 30 s while tracking */ + plotColL->addWidget(voltageChart, 1); + + QHBoxLayout* acqL = new QHBoxLayout(); + acqStartPusb = new QPushButton(); + acqPausePusb = new QPushButton(); + acqStopPusb = new QPushButton(); + acqStartPusb->setIcon(QIcon(":/images/NewSet/play.png")); + acqPausePusb->setIcon(QIcon(":/images/NewSet/pause.png")); + acqStopPusb->setIcon(QIcon(":/images/NewSet/stop.png")); + + QPushButton* acqBtns[] = { acqStartPusb, acqPausePusb, acqStopPusb }; + for(QPushButton* b : acqBtns) + { + b->setIconSize(QSize(CHARGER_ICON_SIZE, CHARGER_ICON_SIZE)); + b->setFixedSize(CHARGER_ICON_BTN_SIZE, CHARGER_ICON_BTN_SIZE); + b->setStyleSheet(CHARGER_ICON_BTN_STYLE); + } + acqStartPusb->setToolTip("Start acquisition"); + acqPausePusb->setToolTip("Pause acquisition (keep data)"); + acqStopPusb->setToolTip("Stop acquisition (clear)"); + + /* Initial: only Start is available. */ + acqPausePusb->setEnabled(false); + acqStopPusb->setEnabled(false); + + acqL->addStretch(1); + acqL->addWidget(acqStartPusb); + acqL->addWidget(acqPausePusb); + acqL->addWidget(acqStopPusb); + acqL->addStretch(1); + plotColL->addLayout(acqL); + + /* ---- Auto-calibration: apply a known voltage, enter it, click Calibrate ---- */ + calValueLabel = new QLabel("Measured: —", content); + QFont cvFont = calValueLabel->font(); cvFont.setBold(true); calValueLabel->setFont(cvFont); + calValueLabel->setAlignment(Qt::AlignCenter); + plotColL->addWidget(calValueLabel); + + QHBoxLayout* calL = new QHBoxLayout(); + calVoltageEdit = new QLineEdit(content); + calVoltageEdit->setValidator(new QDoubleValidator(0.0, 100.0, 3, calVoltageEdit)); + calVoltageEdit->setPlaceholderText("known V"); + calVoltageEdit->setFixedWidth(90); + calibratePusb = new QPushButton("Calibrate", content); + calibratePusb->setToolTip("Set the input to a known DC voltage, type it here, then click to calibrate"); + calL->addStretch(1); + calL->addWidget(new QLabel("Ref [V]:", content)); + calL->addWidget(calVoltageEdit); + calL->addWidget(calibratePusb); + calL->addStretch(1); + plotColL->addLayout(calL); + + bodyL->addLayout(plotColL, 1); + + /* ---- Right: all charger parameters and controls ---- */ + QVBoxLayout* mainL = new QVBoxLayout(); + bodyL->addLayout(mainL, 1); /* ---- Device information (read-only) ---- */ QGroupBox* infoBox = new QGroupBox("Device information"); @@ -289,15 +373,8 @@ void ChargerWnd::updateChargingModeLabel(const QString& modeCode) chargingModeLabel->setText(text); } -void ChargerWnd::readStatus() +void ChargerWnd::updateStatusFromStat0(int stat0) { - int stat0 = 0; - if(!m_device->getChargerRegister(BQ25180_REG_STAT0, &stat0)) - { - stat0Label->setText("read error"); - return; - } - int chg = (stat0 >> 5) & 0x03; /* CHG_STAT[1:0] */ QString modeCode = (chg == 1) ? "cc" : (chg == 2) ? "cv" : (chg == 3) ? "done" : "not"; updateChargingModeLabel(modeCode); @@ -308,6 +385,23 @@ void ChargerWnd::readStatus() stat0Label->setText(s); } +void ChargerWnd::readStatus() +{ + int stat0 = 0; + if(!m_device->getChargerRegister(BQ25180_REG_STAT0, &stat0)) + { + stat0Label->setText("read error"); + return; + } + updateStatusFromStat0(stat0); +} + +void ChargerWnd::onChargerStatusObtained(int stat0) +{ + /* Pushed asynchronously by the firmware (~1 Hz); no Read needed. */ + updateStatusFromStat0(stat0); +} + void ChargerWnd::onReadClicked() { if(m_device == nullptr) return; @@ -354,6 +448,10 @@ void ChargerWnd::onReadClicked() readStatus(); + /* Input-voltage calibration gain (V_CAL) from the device EEPROM/params. */ + double gain; + if(m_device->getChargerVCal(&gain)) m_calGain = gain; + statusLabel->setText("Parameters read from device."); } @@ -432,6 +530,97 @@ void ChargerWnd::onStopChargingClicked() statusLabel->setText(ok ? "Charging stopped." : "Failed to stop charging."); } +/* ---- Live input-voltage acquisition ---- */ + +void ChargerWnd::onStartAcquisitionClicked() +{ + if(m_device == nullptr) return; + /* Start (or resume after pause) - do NOT clear, so Pause->Start continues. */ + if(!m_device->startChargerVoltageStream()) + { + statusLabel->setText("Failed to start acquisition."); + return; + } + acqStartPusb->setEnabled(false); + acqPausePusb->setEnabled(true); + acqStopPusb->setEnabled(true); + statusLabel->setText("Acquisition started."); +} + +void ChargerWnd::onPauseAcquisitionClicked() +{ + if(m_device == nullptr) return; + /* Pause: stop the stream but keep the data on screen for inspection. */ + bool ok = m_device->stopChargerVoltageStream(); + acqStartPusb->setEnabled(true); + acqPausePusb->setEnabled(false); + acqStopPusb->setEnabled(true); + statusLabel->setText(ok ? "Acquisition paused." : "Failed to pause acquisition."); +} + +void ChargerWnd::onStopAcquisitionClicked() +{ + if(m_device == nullptr) return; + /* Stop = pause + clear: stop the stream and wipe the plot. */ + m_device->stopChargerVoltageStream(); + m_acqTimeMs = 0.0; + voltageChart->clear(); + acqStartPusb->setEnabled(true); + acqPausePusb->setEnabled(false); + acqStopPusb->setEnabled(false); + statusLabel->setText("Acquisition stopped."); +} + +void ChargerWnd::onChargerVoltageObtained(double voltageV, quint16 rawAdc) +{ + Q_UNUSED(voltageV); /* we re-derive from raw using the calibrated gain */ + + m_lastRaw = rawAdc; + double v = (double)rawAdc * m_calGain; + calValueLabel->setText(QString("Measured: %1 V").arg(v, 0, 'f', 3)); + + /* Firmware streams samples at ~10 Hz (every 100 ms). Feed the Plot the same + * way as before: appendData keeps the full history (so Expand shows it all) + * and, while tracking, slides a 30 s window (set via setTrackingWindow). */ + m_acqTimeMs += 100.0; + QVector vd; vd.append(v); + QVector kd; kd.append(m_acqTimeMs); + voltageChart->appendData(vd, kd); +} + +void ChargerWnd::onCalibrateClicked() +{ + if(m_lastRaw == 0) + { + statusLabel->setText("Start acquisition first - no sample to calibrate against."); + return; + } + + bool ok = false; + double knownV = calVoltageEdit->text().toDouble(&ok); + if(!ok || knownV <= 0.0) + { + statusLabel->setText("Enter a valid known reference voltage."); + return; + } + + /* gain [V/count] = known voltage / current raw count. */ + m_calGain = knownV / (double)m_lastRaw; + + /* Push the new gain to the device (volatile until "Store" persists it to + * EEPROM as the V_CAL parameter). */ + bool pushed = m_device->setChargerVCal(m_calGain); + + /* Reflect the new scale immediately in the live readout. */ + calValueLabel->setText(QString("Measured: %1 V").arg((double)m_lastRaw * m_calGain, 0, 'f', 3)); + + QString msg = QString("Calibrated: V_CAL = %1 mV/count (%2 V at raw %3). ") + .arg(m_calGain * 1000.0, 0, 'f', 5).arg(knownV, 0, 'f', 3).arg(m_lastRaw); + msg += pushed ? "Click Store to save it to EEPROM." + : "WARNING: could not send V_CAL to device."; + statusLabel->setText(msg); +} + /* ---- EEPROM ---- */ void ChargerWnd::renderEeprom(const QByteArray& data) diff --git a/Source/Windows/Charger/chargerwnd.h b/Source/Windows/Charger/chargerwnd.h index 559e706..bd6ea2f 100644 --- a/Source/Windows/Charger/chargerwnd.h +++ b/Source/Windows/Charger/chargerwnd.h @@ -3,8 +3,10 @@ #include #include +#include class Device; +class Plot; class QLineEdit; class QComboBox; class QLabel; @@ -34,6 +36,12 @@ private slots: void onStopChargingClicked(); void onApplyChemistryClicked(); void onChargerModeObtained(QString mode); + void onChargerStatusObtained(int stat0); + void onChargerVoltageObtained(double voltageV, quint16 rawAdc); + void onStartAcquisitionClicked(); + void onPauseAcquisitionClicked(); + void onStopAcquisitionClicked(); + void onCalibrateClicked(); void onEepromReadClicked(); void onEepromWriteClicked(); void onEepromFormatClicked(); @@ -44,6 +52,7 @@ private slots: void buildUi(); int comboIndexForData(QComboBox* combo, int value); void updateChargingModeLabel(const QString& modeCode); + void updateStatusFromStat0(int stat0); bool setRegisterField(int reg, int mask, int shift, int value); bool readRegisterField(int reg, int mask, int shift, int* value); void readStatus(); @@ -51,6 +60,21 @@ private slots: Device* m_device; + /* Live input-voltage acquisition (left side) */ + Plot* voltageChart; + QPushButton* acqStartPusb; /* play */ + QPushButton* acqPausePusb; /* pause */ + QPushButton* acqStopPusb; /* stop (pause + clear) */ + double m_acqTimeMs; /* running x-axis time for the plot */ + + /* Auto-calibration: displayed voltage = rawAdc * m_calGain [V/count]. + * Calibrated from a known reference applied to the input. */ + QLineEdit* calVoltageEdit; /* known reference voltage [V] */ + QPushButton* calibratePusb; + QLabel* calValueLabel; /* live calibrated reading */ + double m_calGain; /* volts per ADC count */ + quint16 m_lastRaw; /* most recent raw sample */ + /* Identification */ QLineEdit* hwSerialEdit; QLineEdit* fwVersionEdit; diff --git a/Source/Windows/Plot/plot.cpp b/Source/Windows/Plot/plot.cpp index 2aee8ea..f318a2f 100644 --- a/Source/Windows/Plot/plot.cpp +++ b/Source/Windows/Plot/plot.cpp @@ -124,6 +124,7 @@ Plot::Plot(int mw, int mh, bool aEnableTracking, QWidget *parent) title = new QCPTextElement(plot, "NN", QFont("Helvetica", 16)); plot->plotLayout()->addElement(0, 0, title); + trackWindowMs = 10000; enableTracking = aEnableTracking; replotActive = true; scatterGraphAdded = false; @@ -480,7 +481,7 @@ void Plot::appendData(QVector data, QVector keys) yData.append(data); plotXData.append(keys); plotYData.append(data); - if((plotXData.last() > 10000) && (plotXData.size() > data.size())) + if((plotXData.last() > trackWindowMs) && (plotXData.size() > data.size())) { plotXData.remove(0,data.size()); plotYData.remove(0,data.size()); @@ -502,6 +503,10 @@ void Plot::appendData(QVector data, QVector keys) } } +void Plot::setTrackingWindow(double ms) +{ + if(ms > 0) trackWindowMs = ms; +} void Plot::setYRange(double min, double max) { plot->yAxis->setRange(min, max); diff --git a/Source/Windows/Plot/plot.h b/Source/Windows/Plot/plot.h index b8fcf5f..7b76087 100644 --- a/Source/Windows/Plot/plot.h +++ b/Source/Windows/Plot/plot.h @@ -30,6 +30,7 @@ class Plot : public QWidget int lineWidth, bool gridVisible, bool minorGridVisible); bool saveImageToSvg(QString path); void appendData(QVector data, QVector keys); + void setTrackingWindow(double ms); /*!< live sliding-window width while tracking */ void setYRange(double min, double max); void setYLabel(QString label); void setXRange(double min, double max); @@ -90,6 +91,7 @@ private slots: QCPGraph *keyMarkerGraph; QVector keyMarkerText; + double trackWindowMs; bool enableTracking; bool replotActive; bool scatterGraphAdded; diff --git a/Source/device.cpp b/Source/device.cpp index 6e11490..586b694 100644 --- a/Source/device.cpp +++ b/Source/device.cpp @@ -1845,6 +1845,30 @@ void Device::onControlLinkAsyncMessage(QString message) QString mode = m.section(' ', 3).trimmed(); /* word after "... mode " */ emit sigChargerModeObtained(mode); } + else if(m.startsWith("charger status stat0")) + { + /* "charger status stat0 N" - raw STAT0 register byte, pushed ~1 Hz. */ + bool ok = false; + int stat0 = m.section(' ', 3).trimmed().toInt(&ok); + if(ok) emit sigChargerStatusObtained(stat0); + } + else if(m.startsWith("bat voltage")) + { + /* "bat voltage N" where N is the raw 12-bit ADC count of PC3. */ + bool ok = false; + quint16 raw = (quint16)m.section(' ', 2).trimmed().toUInt(&ok); + if(ok) + { + /* Voltage at the ADC pin: Vpin = raw / 4095 * Vref (3.3 V). + * The input is attenuated by an on-board resistive divider before + * PC3, so the real input voltage is Vpin * DIVIDER_RATIO. */ + const double VREF = 3.3; + const double DIVIDER_RATIO = 2.0; /* /2 divider on PC3 */ + double vpin = (double)raw / 4095.0 * VREF; + double voltageV = vpin * DIVIDER_RATIO; + emit sigChargerVoltageObtained(voltageV, raw); + } + } } int Device::getChargerEepromSize() @@ -1887,6 +1911,43 @@ bool Device::storeChargerConfig() return (response == "OK"); } +bool Device::startChargerVoltageStream() +{ + if(controlLink == NULL) return false; + QString response; + if(!controlLink->executeCommand(QString("charger voltage stream start"), &response, 1000)) return false; + return (response == "OK"); +} + +bool Device::stopChargerVoltageStream() +{ + if(controlLink == NULL) return false; + QString response; + if(!controlLink->executeCommand(QString("charger voltage stream stop"), &response, 1000)) return false; + return (response == "OK"); +} + +bool Device::getChargerVCal(double* gain) +{ + if(controlLink == NULL || gain == NULL) return false; + QString response; + if(!controlLink->executeCommand(QString("charger vcal get"), &response, 1000)) return false; + bool ok = false; + double g = response.trimmed().toDouble(&ok); + if(!ok || g <= 0.0) return false; + *gain = g; + return true; +} + +bool Device::setChargerVCal(double gain) +{ + if(controlLink == NULL) return false; + QString response; + QString command = "charger vcal set -value=" + QString::number(gain, 'f', 8); + if(!controlLink->executeCommand(command, &response, 1000)) return false; + return (response == "OK"); +} + bool Device::latchTrigger() { QString response; diff --git a/Source/device.h b/Source/device.h index 44ee122..b8445c6 100644 --- a/Source/device.h +++ b/Source/device.h @@ -198,6 +198,10 @@ class Device : public QObject bool writeChargerEeprom(const QByteArray& data); bool formatChargerEeprom(); bool storeChargerConfig(); + bool startChargerVoltageStream(); + bool stopChargerVoltageStream(); + bool getChargerVCal(double* gain); + bool setChargerVCal(double gain); bool latchTrigger(); bool getUVoltageStatus(bool* status = NULL); bool getOVoltageStatus(bool* status = NULL); @@ -259,6 +263,8 @@ class Device : public QObject void sigChargerHWSerialObtained(QString serial); void sigChargerFWVersionObtained(QString serial); void sigChargerModeObtained(QString mode); + void sigChargerVoltageObtained(double voltageV, quint16 rawAdc); + void sigChargerStatusObtained(int stat0); void sigAvgRatio(QString voffset); From e2a970875a7ab5010966cfb4208bda5b957ca7ec Mon Sep 17 00:00:00 2001 From: hturkmanovic Date: Mon, 19 Oct 2026 14:59:26 +0200 Subject: [PATCH 5/6] Charger window added --- Source/Windows/Device/configurationwnd.cpp | 93 +++++++++++++++++++++- Source/Windows/Device/configurationwnd.h | 2 + Source/Windows/Device/devicewnd.cpp | 4 +- Source/device.cpp | 8 +- Source/devicecontainer.cpp | 34 ++++++-- Source/devicecontainer.h | 2 + 6 files changed, 129 insertions(+), 14 deletions(-) diff --git a/Source/Windows/Device/configurationwnd.cpp b/Source/Windows/Device/configurationwnd.cpp index 211f7be..c8f1b80 100644 --- a/Source/Windows/Device/configurationwnd.cpp +++ b/Source/Windows/Device/configurationwnd.cpp @@ -297,7 +297,19 @@ QWidget *ConfigurationWnd::createTab(Params::GroupId group) QVBoxLayout *tabMainLayout = new QVBoxLayout(tab); QWidget *scrollContent = new QWidget(this); - scrollContent->setLayout(createGroupLayout(group)); + + /* The charger configuration tab gets a dedicated layout that mirrors the + * Charger device window (ChargerWnd): Device information and Charging + * parameters side by side, EEPROM memory below. (Live status / registers, + * plot and calibration are intentionally not included here.) */ + if(group == static_cast(DeviceParamDefs::ChargerConfig)) + { + scrollContent->setLayout(createChargerConfigLayout()); + } + else + { + scrollContent->setLayout(createGroupLayout(group)); + } QScrollArea *scrollArea = new QScrollArea(this); scrollArea->setWidget(scrollContent); @@ -363,6 +375,85 @@ QVBoxLayout *ConfigurationWnd::createGroupLayout(Params::GroupId group) return groupLayout; } +QGroupBox *ConfigurationWnd::createChargerParamBox(const QString &title, Params::SubGroupId subGroup) +{ + QGroupBox *box = new QGroupBox(title, this); + QGridLayout *grid = new QGridLayout(); + + if(m_params != nullptr) + { + QList params = m_params->getParamsBySubGroup( + static_cast(DeviceParamDefs::ChargerConfig), subGroup); + + int row = 0; + for(const Params::Param ¶m : params) + { + if(param.meta.visible == false) + { + continue; + } + + /* Single column (label above field) so the box reads like a form, + * matching the Charger device window. Fields are still registered, + * so Set/Store keep working. */ + grid->addWidget(createParamWidget(param), row, 0); + row++; + } + } + + box->setLayout(grid); + return box; +} + +QVBoxLayout *ConfigurationWnd::createChargerConfigLayout() +{ + QVBoxLayout *mainLayout = new QVBoxLayout(); + + if(m_params == nullptr) + { + mainLayout->addStretch(); + return mainLayout; + } + + QGroupBox *infoBox = + createChargerParamBox("Device information", + static_cast(DeviceParamDefs::ChargerGeneral)); + + QGroupBox *paramBox = + createChargerParamBox("Charging parameters", + static_cast(DeviceParamDefs::ChargerConf)); + + /* Top: two columns (info | parameters), like ChargerWnd. */ + QVBoxLayout *leftCol = new QVBoxLayout(); + leftCol->addWidget(infoBox); + leftCol->addStretch(); + + QVBoxLayout *rightCol = new QVBoxLayout(); + rightCol->addWidget(paramBox); + rightCol->addStretch(); + + QHBoxLayout *topRow = new QHBoxLayout(); + topRow->addLayout(leftCol, 1); + topRow->addLayout(rightCol, 1); + + mainLayout->addLayout(topRow); + + /* EEPROM memory view below, full width (reuses the existing builder that + * adds the hex/icon-button memory widget). */ + QGroupBox *memoryBox = createSubGroupBox( + static_cast(DeviceParamDefs::ChargerConfig), + static_cast(DeviceParamDefs::ChargerBD)); + + if(memoryBox != nullptr) + { + mainLayout->addWidget(memoryBox); + } + + mainLayout->addStretch(); + + return mainLayout; +} + QGroupBox *ConfigurationWnd::createSubGroupBox(Params::GroupId group, Params::SubGroupId subGroup) { if(m_params == nullptr) diff --git a/Source/Windows/Device/configurationwnd.h b/Source/Windows/Device/configurationwnd.h index 58abff2..79448c4 100644 --- a/Source/Windows/Device/configurationwnd.h +++ b/Source/Windows/Device/configurationwnd.h @@ -112,6 +112,8 @@ private slots: QWidget *createTab(Params::GroupId group); QVBoxLayout *createGroupLayout(Params::GroupId group); + QVBoxLayout *createChargerConfigLayout(); + QGroupBox *createChargerParamBox(const QString &title, Params::SubGroupId subGroup); QGroupBox *createSubGroupBox(Params::GroupId group, Params::SubGroupId subGroup); diff --git a/Source/Windows/Device/devicewnd.cpp b/Source/Windows/Device/devicewnd.cpp index 4e18c74..8c8ed5a 100644 --- a/Source/Windows/Device/devicewnd.cpp +++ b/Source/Windows/Device/devicewnd.cpp @@ -1091,14 +1091,14 @@ bool DeviceWnd::setChargerMaxCurrent(int current) bool DeviceWnd::setChargerHWSerial(QString serial) { configurationWnd->setParamValue("chargerSerial", serial); - energyControlWnd->chargerFWVersionSet(serial); + energyControlWnd->chargerSerialNumberSet(serial); return true; } bool DeviceWnd::setChargerFWSerial(QString serial) { configurationWnd->setParamValue("chargerFwVersion", serial); - energyControlWnd->chargerSerialNumberSet(serial); + energyControlWnd->chargerFWVersionSet(serial); return true; } diff --git a/Source/device.cpp b/Source/device.cpp index 586b694..f51dcd4 100644 --- a/Source/device.cpp +++ b/Source/device.cpp @@ -1793,8 +1793,8 @@ bool Device::getChargerHWSerial(QString* serial) QString command = "charger hwserial get"; if(!controlLink->executeCommand(command, &response, 1000)) return false; //Parse response - *serial = response; - emit sigChargerHWSerialObtained(*serial); + if(serial != NULL) *serial = response; + emit sigChargerHWSerialObtained(response); return true; } @@ -1804,8 +1804,8 @@ bool Device::getChargerFWVersion(QString *version) QString command = "charger fwversion get"; if(!controlLink->executeCommand(command, &response, 1000)) return false; //Parse response - *version = response; - emit sigChargerFWVersionObtained(*version); + if(version != NULL) *version = response; + emit sigChargerFWVersionObtained(response); return true; } diff --git a/Source/devicecontainer.cpp b/Source/devicecontainer.cpp index 0a504b6..4098af2 100644 --- a/Source/devicecontainer.cpp +++ b/Source/devicecontainer.cpp @@ -1,4 +1,5 @@ #include +#include #include "Processing/batteryparamsextraction.h" #define DEVICECONTAINER_RELAX_SAMPLES_MAX 2000000 @@ -1596,14 +1597,12 @@ void DeviceContainer::onDeviceChargerConnectionStatusOntained(bool state) if(state == true) { - QString hwSerial, fwversion; - device->getChargerCurrent(); - device->getChargerTermCurrent(); - device->getChargerTermVoltage(); - device->getChargerFWVersion(&fwversion); - device->getChargerHWSerial(&hwSerial); - device->getChargerMaxChargingCurrent(); log->printLogMessage("Charger connected", LOG_MESSAGE_TYPE_INFO); + /* This slot can run inside the status-link socket read handler. Issuing + * blocking control-link reads from there is re-entrant and unreliable, + * so defer the parameter pull to a clean event-loop turn (and give the + * charger-on-EPP a moment to become ready). */ + QTimer::singleShot(200, this, [this](){ refreshChargerParameters(3); }); } else { @@ -1612,6 +1611,27 @@ void DeviceContainer::onDeviceChargerConnectionStatusOntained(bool state) } +void DeviceContainer::refreshChargerParameters(int retriesLeft) +{ + /* Pull the latest charger parameters from the device. Each getter emits an + * "obtained" signal that populates the charger tab; on failure it returns + * false and leaves the field untouched, so retry a few times because the + * charger may not answer immediately after it is first detected. */ + bool ok = true; + ok &= device->getChargerCurrent(); + ok &= device->getChargerTermCurrent(); + ok &= device->getChargerTermVoltage(); + ok &= device->getChargerMaxChargingCurrent(); + device->getChargerFWVersion(); + device->getChargerHWSerial(); + + if(!ok && retriesLeft > 0) + { + log->printLogMessage("Charger parameters not ready yet, retrying...", LOG_MESSAGE_TYPE_WARNING); + QTimer::singleShot(500, this, [this, retriesLeft](){ refreshChargerParameters(retriesLeft - 1); }); + } +} + void DeviceContainer::onDeviceOVoltageObtained(bool state) { oVoltageActive = state; diff --git a/Source/devicecontainer.h b/Source/devicecontainer.h index d60a1e7..8440ff0 100644 --- a/Source/devicecontainer.h +++ b/Source/devicecontainer.h @@ -147,6 +147,8 @@ private slots: private: + void refreshChargerParameters(int retriesLeft = 3); + DeviceWnd* deviceWnd; Device* device; Log* log; From acfde079be54efe5758911f9042812f336c223b6 Mon Sep 17 00:00:00 2001 From: hturkmanovic Date: Mon, 19 Oct 2026 15:00:10 +0200 Subject: [PATCH 6/6] Version upgraded --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index c043eea..276cbf9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.2.1 +2.3.0