// SPDX-License-Identifier: GPL-3.0-or-later /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright (C) 2013 - 2024, nymea GmbH * Copyright (C) 2024 - 2025, chargebyte austria GmbH * * This file is part of nymea-plugins. * * nymea-plugins is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * nymea-plugins is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with nymea-plugins. If not, see . * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "evboxport.h" #include #include #include "extern-plugininfo.h" #define STX 0x02 #define ETX 0x03 EVBoxPort::EVBoxPort(const QString &portName, QObject *parent) : QObject{parent} { m_serialPort = new QSerialPort(portName, this); m_serialPort->setBaudRate(QSerialPort::Baud38400); m_serialPort->setDataBits(QSerialPort::Data8); m_serialPort->setStopBits(QSerialPort::OneStop); m_serialPort->setParity(QSerialPort::NoParity); connect(m_serialPort, &QSerialPort::readyRead, this, &EVBoxPort::onReadyRead); #if QT_VERSION >= QT_VERSION_CHECK(5, 8, 0) connect(m_serialPort, &QSerialPort::errorOccurred, this, [this](){ #else connect(m_serialPort, static_cast(&QSerialPort::error), this, [=](){ #endif qCWarning(dcEVBox()) << "Serial Port error" << m_serialPort->error() << m_serialPort->errorString(); if (m_serialPort->error() != QSerialPort::NoError) { if (m_serialPort->isOpen()) { m_serialPort->close(); } emit closed(); QTimer::singleShot(1000, this, [=](){ open(); }); } }); // As per spec we need to wait at least 100ms after anything happens on the bus m_waitTimer.setSingleShot(true); m_waitTimer.setInterval(150); connect(&m_waitTimer, &QTimer::timeout, this, &EVBoxPort::processQueue); } bool EVBoxPort::open() { if (m_serialPort->open(QSerialPort::ReadWrite)) { emit opened(); return true; } return false; } bool EVBoxPort::isOpen() { return m_serialPort->isOpen(); } void EVBoxPort::close() { m_serialPort->close(); } void EVBoxPort::sendCommand(Command command, quint16 timeout, quint16 maxChargingCurrent, const QString &serial) { CommandWrapper cmd; cmd.command = command; cmd.timeout = timeout; cmd.maxChargingCurrent = maxChargingCurrent; cmd.serial = serial; m_commandQueue.enqueue(cmd); processQueue(); } void EVBoxPort::onReadyRead() { m_waitTimer.start(); QByteArray data = m_serialPort->readAll(); qCDebug(dcEVBox()) << "<--" << data; m_inputBuffer.append(data); QByteArray packet; QDataStream inputStream(m_inputBuffer); QDataStream outputStream(&packet, QIODevice::WriteOnly); bool startFound = false, endFound = false; while (!inputStream.atEnd()) { quint8 byte; inputStream >> byte; if (!startFound) { if (byte == STX) { startFound = true; continue; } else { qCWarning(dcEVBox()) << "Discarding byte 0x" + QString::number(byte, 16) + " which is not matching start of frame 0x" + QString::number(STX, 16); continue; } } else { // Sometimes the wallbox seems to stumble and restart packet transmission before a previous packet is finished... // If we detect another STX before an ETX, let's discard it if (byte == STX) { qCWarning(dcEVBox()) << "Bogus data from wallbox detected. Discarding input buffers."; m_inputBuffer.clear(); continue; } } if (byte == ETX) { endFound = true; break; } outputStream << byte; } if (startFound && endFound) { m_inputBuffer.remove(0, packet.length() + 2); } else if (!startFound) { qCDebug(dcEVBox()) << "End of data but no start of frame header received."; } else { qCDebug(dcEVBox()) << "Data is incomplete... Waiting for more..."; return; } if (packet.length() < 2) { // In practice it'll be longer, but let's make sure we won't crash checking the checksum on erraneous data qCWarning(dcEVBox()) << "Packet is too short. Discarding packet..."; return; } qCDebug(dcEVBox()) << "Data packet received:" << packet; processDataPacket(packet); } void EVBoxPort::processDataPacket(const QByteArray &packet) { // The data is a mess of hex and dec values... We'll read the stream as hex but have to convert some back to decimal. QDataStream stream(QByteArray::fromHex(packet)); quint8 from, to, commandId, chargeBoxModuleCount; quint16 minPollInterval, maxChargingCurrent, minChargingCurrent, chargingCurrentL1, chargingCurrentL2, chargingCurrentL3, cosinePhiL1, cosinePhiL2, cosinePhiL3, voltageL1, voltageL2, voltageL3; quint32 totalEnergyConsumed; stream >> from >> to >> commandId; // Command is transmitted in decimal Command command = static_cast(QString::number(commandId, 16).toInt()); QString serial; if (command == Command68) { quint32 serialNumber; stream >> serialNumber; serial = QString::number(serialNumber, 16); stream >> minPollInterval >> maxChargingCurrent >> chargeBoxModuleCount; if (chargeBoxModuleCount > 0) { stream >> minChargingCurrent >> chargingCurrentL1 >> chargingCurrentL2 >> chargingCurrentL3 >> cosinePhiL1 >> cosinePhiL2 >> cosinePhiL3 >> totalEnergyConsumed >> voltageL1 >> voltageL2 >> voltageL3; } else { qCDebug(dcEVBox()) << "No chargebox module data in packet!"; emit shortResponseReceived(command, serial); return; } } else if (command == Command69) { stream >> minPollInterval >> maxChargingCurrent >> chargeBoxModuleCount; if (chargeBoxModuleCount > 0) { stream >> minChargingCurrent >> chargingCurrentL1 >> chargingCurrentL2 >> chargingCurrentL3 >> cosinePhiL1 >> cosinePhiL2 >> cosinePhiL3 >> totalEnergyConsumed; } else { qCDebug(dcEVBox()) << "No chargebox module data in packet!"; emit shortResponseReceived(command, serial); return; } } else { qCWarning(dcEVBox()) << "Unknown command id. Cannot process packet."; return; } qCDebug(dcEVBox()) << "Parsed data packet: From:" << from << "To:" << to << "Command:" << command << "Serial:" << serial << "MinAmpere:" << minChargingCurrent << "MaxAmpere:" << maxChargingCurrent << "AmpereL1" << chargingCurrentL1 << "AmpereL2" << chargingCurrentL2 << "AmpereL3" << chargingCurrentL3 << "Total" << totalEnergyConsumed; emit responseReceived(command, serial, minChargingCurrent, maxChargingCurrent, chargingCurrentL1, chargingCurrentL2, chargingCurrentL3, totalEnergyConsumed); } void EVBoxPort::processQueue() { if (m_commandQueue.isEmpty()) { return; } if (m_waitTimer.isActive()) { qCDebug(dcEVBox()) << "Line is busy. Waiting..."; return; } CommandWrapper cmd = m_commandQueue.takeFirst(); QByteArray commandData; commandData += "80"; // Dst addr commandData += "A0"; // Sender address commandData += QString::number(cmd.command).toUtf8(); qCDebug(dcEVBox()) << "Sending command" << cmd.command << "to" << cmd.serial << "MaxCurrent:" << cmd.maxChargingCurrent; if (cmd.command == Command68) { if (cmd.serial.length() != 8) { qCCritical(dcEVBox()) << "Serial must be 8 characters. Cannot send command..."; processQueue(); return; } commandData += cmd.serial.toUtf8(); // The content of the “information module” is 16 bytes in size and not defined. ¯\_(ツ)_/¯ commandData += "00112233445566778899AABBCCDDEEFF"; } else if (cmd.command == Command69) { qCDebug(dcEVBox()) << "Using command 69"; } commandData += QString("%1").arg(cmd.maxChargingCurrent * 10, 4, 10, QChar('0')).toUtf8(); commandData += QString("%1").arg(cmd.maxChargingCurrent * 10, 4, 10, QChar('0')).toUtf8(); commandData += QString("%1").arg(cmd.maxChargingCurrent * 10, 4, 10, QChar('0')).toUtf8(); commandData += QString("%1").arg(cmd.timeout, 4, 10, QChar('0')).toUtf8(); // If we fail to refresh the wallbox after the timeout, it shall turn off, which is what we'll use as default // when we don't know what its set to (as we can't read it). // Hence we do *not* cache the power and maxChargingCurrent states for this one commandData += QString("%1").arg(6, 4, 10, QChar('0')).toUtf8(); commandData += QString("%1").arg(6, 4, 10, QChar('0')).toUtf8(); commandData += QString("%1").arg(6, 4, 10, QChar('0')).toUtf8(); commandData += createChecksum(commandData); QByteArray data; QDataStream stream(&data, QIODevice::WriteOnly); stream << static_cast(STX); stream.writeRawData(commandData.data(), commandData.length()); stream << static_cast(ETX); qCDebug(dcEVBox()) << "-->" << data; // << "Hex:" << data.toHex(); qint64 count = m_serialPort->write(data); if (count != data.length()) { qCWarning(dcEVBox()) << "Error writing data to serial port:" << m_serialPort->errorString(); } m_waitTimer.start(); } QByteArray EVBoxPort::createChecksum(const QByteArray &data) const { QDataStream checksumStream(data); quint8 sum = 0; quint8 xOr = 0; while (!checksumStream.atEnd()) { quint8 byte; checksumStream >> byte; sum += byte; xOr ^= byte; } return QString("%1%2").arg(sum,2,16, QChar('0')).arg(xOr,2,16, QChar('0')).toUpper().toLocal8Bit(); }