320 lines
11 KiB
C++
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();
|
|
}
|