nymea-plugins/evbox/evboxport.cpp

320 lines
11 KiB
C++

// 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 <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "evboxport.h"
#include <QTimer>
#include <QDataStream>
#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<void(QSerialPort::*)(QSerialPort::SerialPortError)>(&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<Command>(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<quint8>(STX);
stream.writeRawData(commandData.data(), commandData.length());
stream << static_cast<quint8>(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();
}