// SPDX-License-Identifier: LGPL-3.0-or-later /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * nymea-zigbee * Zigbee integration module for nymea * * Copyright (C) 2013 - 2024, nymea GmbH * Copyright (C) 2024 - 2025, chargebyte austria GmbH * * This file is part of nymea-zigbee. * * nymea-zigbee is free software: you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * nymea-zigbee 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with nymea-zigbee. If not, see . * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "zigbeeclusterota.h" #include "zigbeenetworkreply.h" #include "loggingcategory.h" #include "zigbeenetwork.h" #include "zigbeeutils.h" #include ZigbeeClusterOta::ZigbeeClusterOta(ZigbeeNetwork *network, ZigbeeNode *node, ZigbeeNodeEndpoint *endpoint, Direction direction, QObject *parent) : ZigbeeCluster(network, node, endpoint, ZigbeeClusterLibrary::ClusterIdOtaUpgrade, direction, parent) { } ZigbeeClusterReply *ZigbeeClusterOta::sendImageNotify(PayloadType payloadType, quint8 queryJitter, quint16 manufacturerCode, quint16 imageType, quint32 newFileVersion) { if (queryJitter > 100) { queryJitter = 100; } QByteArray payload; QDataStream stream(&payload, QIODevice::WriteOnly); stream.setByteOrder(QDataStream::LittleEndian); stream << static_cast(payloadType); stream << queryJitter; if (payloadType >= PayloadTypeQueryJitterAndManufacturerCode) { stream << manufacturerCode; } if (payloadType >= PayloadTypeQueryJitterAndManufacturerCodeAndImageType) { stream << imageType; } if (payloadType >= PayloadTypeQueryJitterAndManufacturerCodeAndImageTypeAndNewFileVersion) { stream << newFileVersion; } ZigbeeClusterReply *reply = executeClusterCommand(CommandImageNotify, payload, ZigbeeClusterLibrary::DirectionServerToClient); return reply; } ZigbeeClusterReply *ZigbeeClusterOta::sendQueryNextImageResponse(quint8 transactionSequenceNumber, StatusCode statusCode, quint16 manufacturerCode, quint16 imageType, quint32 fileVersion, quint32 imageSize) { QByteArray payload; QDataStream stream(&payload, QIODevice::WriteOnly); stream.setByteOrder(QDataStream::LittleEndian); stream << static_cast(statusCode); if (statusCode == StatusCodeSuccess) { stream << manufacturerCode; stream << imageType; stream << fileVersion; stream << imageSize; } ZigbeeClusterReply *reply = sendClusterServerResponse(CommandQueryNextImageResponse, transactionSequenceNumber, payload); connect(reply, &ZigbeeClusterReply::finished, this, [reply](){ if (reply->error() != ZigbeeClusterReply::ErrorNoError) { qCWarning(dcZigbeeCluster()) << "OTA: Error sending query next image response:" << reply->error(); return; } qCDebug(dcZigbeeCluster()) << "OTA: Query image response sent successfully."; }); return reply; } ZigbeeClusterReply *ZigbeeClusterOta::sendImageBlockResponse(quint8 transactionSequenceNumber, quint16 manufacturerCode, quint16 imageType, quint32 fileVersion, quint32 fileOffset, const QByteArray &imageData) { QByteArray payload; QDataStream stream(&payload, QIODevice::WriteOnly); stream.setByteOrder(QDataStream::LittleEndian); stream << static_cast(StatusCodeSuccess); stream << manufacturerCode; stream << imageType; stream << fileVersion; stream << fileOffset; stream << static_cast(imageData.length()); stream.writeRawData(imageData.data(), imageData.length()); ZigbeeClusterReply *reply = sendClusterServerResponse(CommandImageBlockResponse, transactionSequenceNumber, payload); connect(reply, &ZigbeeClusterReply::finished, this, [reply](){ if (reply->error() != ZigbeeClusterReply::ErrorNoError) { qCWarning(dcZigbeeCluster()) << "OTA: Error sending image block response:" << reply->error(); return; } qCDebug(dcZigbeeCluster()) << "OTA: Image block response sent successfully."; }); return reply; } ZigbeeClusterReply *ZigbeeClusterOta::sendAbortImageBlockResponse(quint8 transactionSequenceNumber) { QByteArray payload; QDataStream stream(&payload, QIODevice::WriteOnly); stream.setByteOrder(QDataStream::LittleEndian); stream << static_cast(StatusCodeAbort); ZigbeeClusterReply *reply = sendClusterServerResponse(CommandImageBlockResponse, transactionSequenceNumber, payload); connect(reply, &ZigbeeClusterReply::finished, this, [reply](){ if (reply->error() != ZigbeeClusterReply::ErrorNoError) { qCWarning(dcZigbeeCluster()) << "OTA: Error sending abort image block response:" << reply->error(); return; } qCDebug(dcZigbeeCluster()) << "OTA: Abort image block response sent successfully."; }); return reply; } ZigbeeClusterReply *ZigbeeClusterOta::sendDelayImageBlockResponse(quint8 transactionSequenceNumber, const QDateTime &requestTime, quint16 minimumBlockPeriod) { QByteArray payload; QDataStream stream(&payload, QIODevice::WriteOnly); stream.setByteOrder(QDataStream::LittleEndian); stream << static_cast(StatusCodeWaitForData); stream << static_cast(requestTime.toMSecsSinceEpoch() / 1000); stream << minimumBlockPeriod; ZigbeeClusterReply *reply = sendClusterServerResponse(CommandImageBlockResponse, transactionSequenceNumber, payload); connect(reply, &ZigbeeClusterReply::finished, this, [reply](){ if (reply->error() != ZigbeeClusterReply::ErrorNoError) { qCWarning(dcZigbeeCluster()) << "OTA: Error sending delay image block response:" << reply->error(); return; } qCDebug(dcZigbeeCluster()) << "OTA: Delay image block response sent successfully."; }); return reply; } ZigbeeClusterReply *ZigbeeClusterOta::sendUpgradeEndResponse(quint8 transactionSequenceNumber, quint16 manufacturerCode, quint16 imageType, quint32 fileVersion, quint32 serverTime, quint32 requestTime) { QByteArray payload; QDataStream stream(&payload, QIODevice::WriteOnly); stream.setByteOrder(QDataStream::LittleEndian); stream << manufacturerCode; stream << imageType; stream << fileVersion; stream << serverTime; stream << requestTime; ZigbeeClusterReply *reply = sendClusterServerResponse(CommandUpgradeEndResponse, transactionSequenceNumber, payload); connect(reply, &ZigbeeClusterReply::finished, this, [reply](){ if (reply->error() != ZigbeeClusterReply::ErrorNoError) { qCWarning(dcZigbeeCluster()) << "OTA: Error sending image block response:" << reply->error(); return; } qCDebug(dcZigbeeCluster()) << "OTA: Query image block data successfully."; }); return reply; } ZigbeeClusterReply *ZigbeeClusterOta::sendAbortUpgradeEndResponse(quint8 transactionSequenceNumber) { return sendDefaultResponse(transactionSequenceNumber, ZigbeeClusterLibrary::CommandDefaultResponse, ZigbeeClusterLibrary::StatusSuccess); } ZigbeeClusterOta::FileVersion ZigbeeClusterOta::parseFileVersion(quint32 fileVersionValue) { FileVersion fileVersion; fileVersion.applicationRelease = static_cast(fileVersionValue >> 24); fileVersion.applicationBuild = static_cast(fileVersionValue >> 16); fileVersion.stackRelease = static_cast(fileVersionValue >> 8); fileVersion.stackBuild = static_cast(fileVersionValue); return fileVersion; } void ZigbeeClusterOta::processDataIndication(ZigbeeClusterLibrary::Frame frame) { qCDebug(dcZigbeeCluster()) << "OTA: Processing cluster frame" << m_node << m_endpoint << this << frame << frame.payload.toHex(); switch (m_direction) { case Client: if (frame.header.frameControl.direction == ZigbeeClusterLibrary::DirectionClientToServer) { Command command = static_cast(frame.header.command); qCDebug(dcZigbeeCluster()) << "Received" << command << "from" << m_node << m_endpoint << this; switch (command) { case CommandQueryNextImageRequest: { // Print the image information quint8 fieldControl; quint16 manufacturerCode; quint16 imageType; quint32 currentVersion; quint16 hardwareVersion; QDataStream requestStream(&frame.payload, QIODevice::ReadOnly); requestStream.setByteOrder(QDataStream::LittleEndian); requestStream >> fieldControl >> manufacturerCode >> imageType >> currentVersion >> hardwareVersion; FileVersion currentFileVersion = parseFileVersion(currentVersion); qCDebug(dcZigbeeCluster()) << "OTA image request:" << (fieldControl == 0x00 ? "Hardware version not present" : "Hardware version present"); qCDebug(dcZigbeeCluster()) << "OTA image request: Manufacturer code" << ZigbeeUtils::convertUint16ToHexString(manufacturerCode); qCDebug(dcZigbeeCluster()) << "OTA image request: Image type" << ZigbeeUtils::convertUint16ToHexString(imageType); qCDebug(dcZigbeeCluster()) << "OTA image request: Current file version" << ZigbeeUtils::convertUint32ToHexString(currentVersion) << currentFileVersion; qCDebug(dcZigbeeCluster()) << "OTA image request: Hardware version" << hardwareVersion; emit queryNextImageRequestReceived(frame.header.transactionSequenceNumber, manufacturerCode, imageType, currentVersion, hardwareVersion); break; } case CommandImageBlockRequest: { quint8 fieldControl; quint16 manufacturerCode; quint16 imageType; quint32 fileVersion; quint32 fileOffset; quint8 maximumDataSize; quint64 requestNodeAddress = 0; quint16 minimumBlockPerdiod = 0; QDataStream stream(frame.payload); stream.setByteOrder(QDataStream::LittleEndian); stream >> fieldControl >> manufacturerCode >> imageType >> fileVersion >> fileOffset >> maximumDataSize; if (fieldControl & 0x01) { stream >> requestNodeAddress; } if (fieldControl & 0x02) { stream >> minimumBlockPerdiod; } qCDebug(dcZigbeeCluster()) << "OTA: Image block request receved. FieldControl:" << fieldControl << "ManufacturerCode:" << manufacturerCode << "ImageType:" << imageType << "File version:" << fileVersion << "Offset:" << fileOffset << "Max size:" << maximumDataSize << "Request Address:" << ZigbeeAddress(requestNodeAddress) << "Min block period:" << minimumBlockPerdiod; emit imageBlockRequestReceived(frame.header.transactionSequenceNumber, manufacturerCode, imageType, fileVersion, fileOffset, maximumDataSize, ZigbeeAddress(requestNodeAddress), minimumBlockPerdiod); break; } case CommandUpgradeEndRequest: { quint8 status; quint16 manufacturerCode; quint16 imageType; quint32 fileVersion; QDataStream stream(frame.payload); stream.setByteOrder(QDataStream::LittleEndian); stream >> status >> manufacturerCode >> imageType >> fileVersion; emit upgradeEndRequestReceived(frame.header.transactionSequenceNumber, static_cast(status), manufacturerCode, imageType, fileVersion); break; } default: qCWarning(dcZigbeeCluster()) << "Received unhandled command" << command << "from" << m_node << m_endpoint << this; break; } } break; case Server: if (frame.header.frameControl.direction == ZigbeeClusterLibrary::DirectionServerToClient) { Command command = static_cast(frame.header.command); qCDebug(dcZigbeeCluster()) << "Received" << command << "from" << m_node << m_endpoint << this; } else { qCWarning(dcZigbeeCluster()) << "Unhandled ZCL indication in" << m_node << m_endpoint << this << frame; } break; } } QDebug operator<<(QDebug debug, const ZigbeeClusterOta::FileVersion &fileVersion) { QDebugStateSaver saver(debug); debug.nospace().noquote() << "FileVersion(Application Release: " << fileVersion.applicationRelease << ", Application Build: " << fileVersion.applicationBuild << ", Stack Release: " << fileVersion.stackRelease << ", Stack Build: " << fileVersion.stackBuild << ")"; return debug; }