// 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 "zigbeeutils.h" #include "loggingcategory.h" #include "zigbeechannelmask.h" #include "zdo/zigbeedeviceprofile.h" #include "zigbeebridgecontrollerti.h" #include #include #define NEW_PAYLOAD QByteArray payload; QDataStream stream(&payload, QIODevice::WriteOnly); stream.setByteOrder(QDataStream::LittleEndian); #define PAYLOAD_STREAM(x) QDataStream stream(x); stream.setByteOrder(QDataStream::LittleEndian); ZigbeeBridgeControllerTi::ZigbeeBridgeControllerTi(QObject *parent) : ZigbeeBridgeController(parent) { m_interface = new ZigbeeInterfaceTi(this); connect(m_interface, &ZigbeeInterfaceTi::availableChanged, this, &ZigbeeBridgeControllerTi::onInterfaceAvailableChanged); connect(m_interface, &ZigbeeInterfaceTi::packetReceived, this, &ZigbeeBridgeControllerTi::onInterfacePacketReceived); m_permitJoinTimer.setSingleShot(true); connect(&m_permitJoinTimer, &QTimer::timeout, this, [=]{emit permitJoinStateChanged(0);}); } ZigbeeBridgeControllerTi::~ZigbeeBridgeControllerTi() { qCDebug(dcZigbeeController()) << "Destroying controller"; } TiNetworkConfiguration ZigbeeBridgeControllerTi::networkConfiguration() const { return m_networkConfiguration; } ZigbeeInterfaceTiReply *ZigbeeBridgeControllerTi::setLed(bool on) { NEW_PAYLOAD; stream << static_cast(0x03); // LED ID stream << static_cast(on); return sendCommand(Ti::SubSystemUtil, Ti::UtilCommandLedControl, payload); } ZigbeeInterfaceTiReply* ZigbeeBridgeControllerTi::reset() { NEW_PAYLOAD stream << static_cast(Ti::ResetTypeSoft); ZigbeeInterfaceTiReply *resetReply = sendCommand(Ti::SubSystemSys, Ti::SYSCommandResetReq, payload); waitFor(resetReply, Ti::SubSystemSys, Ti::SYSCommandResetInd); connect(resetReply, &ZigbeeInterfaceTiReply::finished, this, [=](){ m_interface->reconnectController(); }); return resetReply; } ZigbeeInterfaceTiReply *ZigbeeBridgeControllerTi::init() { m_registeredEndpointIds.clear(); ZigbeeInterfaceTiReply *initReply = new ZigbeeInterfaceTiReply(this, 15000); // Not using public reset() as that will start the init from scratch by reconnecting the controller NEW_PAYLOAD stream << static_cast(Ti::ResetTypeSoft); ZigbeeInterfaceTiReply *resetReply = sendCommand(Ti::SubSystemSys, Ti::SYSCommandResetReq, payload); connect(resetReply, &ZigbeeInterfaceTiReply::finished, initReply, [=]() { qCDebug(dcZigbeeController()) << "Skipping CC2530/CC2531 bootloader."; m_interface->sendMagicByte(); QTimer::singleShot(1000, initReply, [=]{ qCDebug(dcZigbeeController()) << "Trying to ping controller."; ZigbeeInterfaceTiReply *pingReply = sendCommand(Ti::SubSystemSys, Ti::SYSCommandPing, QByteArray(), 1000); connect(pingReply, &ZigbeeInterfaceTiReply::finished, initReply, [=]() { if (pingReply->statusCode() != Ti::StatusCodeSuccess) { qCWarning(dcZigbeeController()) << "Error pinging controller."; qCDebug(dcZigbeeInterface()) << "Skipping CC2652/CC1352 bootloader."; m_interface->setDTR(false); m_interface->setRTS(false); QTimer::singleShot(150, initReply, [=]{ m_interface->setRTS(true); QTimer::singleShot(150, initReply, [=]{ m_interface->setRTS(false); QTimer::singleShot(150, initReply, [=]{ initPhase2(initReply, 0); }); }); }); return; } qCDebug(dcZigbeeController()) << "Controller ping succeeded."; initPhase2(initReply, 0); }); }); }); return initReply; } void ZigbeeBridgeControllerTi::initPhase2(ZigbeeInterfaceTiReply *initReply, int attempt) { qCDebug(dcZigbeeController()) << "Trying to ping controller... (" << (attempt + 1) << "/ 10 )"; ZigbeeInterfaceTiReply *pingReply = sendCommand(Ti::SubSystemSys, Ti::SYSCommandPing, QByteArray(), 1000); connect(pingReply, &ZigbeeInterfaceTiReply::finished, initReply, [=]() { if (pingReply->statusCode() != Ti::StatusCodeSuccess) { qCWarning(dcZigbeeController()) << "Error pinging controller."; if (attempt < 9) { initPhase2(initReply, attempt+1); } else { qCWarning(dcZigbeeController()) << "Giving up..."; initReply->finish(Ti::StatusCodeFailure); } return; } PAYLOAD_STREAM(pingReply->responsePayload()); quint16 caps; stream >> caps; Ti::ControllerCapabilities capabilities = static_cast(caps); qCDebug(dcZigbeeController()) << "Controller ping succeeded! Capabilities:" << capabilities; Ti::ControllerCapabilities requiredCapabilities = Ti::ControllerCapabilityNone; requiredCapabilities |= Ti::ControllerCapabilitySys; requiredCapabilities |= Ti::ControllerCapabilityUtil; requiredCapabilities |= Ti::ControllerCapabilityZDO; requiredCapabilities |= Ti::ControllerCapabilityAF; if ((capabilities & requiredCapabilities) != requiredCapabilities) { qCCritical(dcZigbeeController()) << "Controller doesn't support all required capabilities:" << capabilities; initReply->finish(Ti::StatusCodeUnsupported); return; } qCDebug(dcZigbeeController()) << "Fetching firmware information..."; ZigbeeInterfaceTiReply *versionReply = sendCommand(Ti::SubSystemSys, Ti::SYSCommandVersion); connect(versionReply, &ZigbeeInterfaceTiReply::finished, initReply, [=]() { if (versionReply->statusCode() != Ti::StatusCodeSuccess) { qCWarning(dcZigbeeInterface()) << "Error reading controller version"; initReply->finish(versionReply->statusCode()); return; } PAYLOAD_STREAM(versionReply->responsePayload()); quint8 transportRevision, product, majorRelease, minorRelease, maintRelease; quint32 revision; stream >> transportRevision >> product >> majorRelease >> minorRelease >> maintRelease >> revision; qCDebug(dcZigbeeNetwork()).nospace().noquote() << "Controller versions: Transport rev: " << transportRevision << " Product: " << product << " Version: " << majorRelease << "." << minorRelease << "." << maintRelease << " Revision: " << revision; m_networkConfiguration.znpVersion = static_cast(product); setFirmwareVersion(QString("%0(%1) - %2.%3.%4.%5") .arg(QMetaEnum::fromType().valueToKey(product)) .arg(transportRevision) .arg(majorRelease) .arg(minorRelease) .arg(maintRelease) .arg(revision)); qCDebug(dcZigbeeController()) << "Reading IEEE address"; ZigbeeInterfaceTiReply *getIeeeAddrReply = readNvItem(Ti::NvItemIdPanId); connect(getIeeeAddrReply, &ZigbeeInterfaceTiReply::finished, initReply, [=](){ ZigbeeInterfaceTiReply *getExtAddrReply = sendCommand(Ti::SubSystemSys, Ti::SYSCommandGetExtAddress); connect(getExtAddrReply, &ZigbeeInterfaceTiReply::finished, initReply, [=](){ if (getExtAddrReply->statusCode() != Ti::StatusCodeSuccess) { qCWarning(dcZigbeeController()) << "Call to getDeviceInfo failed:" << getExtAddrReply->statusCode(); initReply->finish(getExtAddrReply->statusCode()); return; } PAYLOAD_STREAM(getExtAddrReply->responsePayload()); quint64 ieeeAddress; stream >> ieeeAddress; m_networkConfiguration.ieeeAddress = ZigbeeAddress(ieeeAddress); qCDebug(dcZigbeeController()) << "IEEE address:" << m_networkConfiguration.ieeeAddress.toString(); initReply->finish(); m_controllerState = ControllerStateInitialized; emit controllerStateChanged(ControllerStateInitialized); }); }); }); }); } ZigbeeInterfaceTiReply *ZigbeeBridgeControllerTi::commission(Ti::DeviceLogicalType deviceType, quint16 panId, const ZigbeeChannelMask &channelMask) { ZigbeeInterfaceTiReply *reply = new ZigbeeInterfaceTiReply(this, 30000); ZigbeeInterfaceTiReply *resetReply = factoryReset(); connect(resetReply, &ZigbeeInterfaceTiReply::finished, reply, [=](){ // Make sure the controller is set to normal startup mode, so it will keep the commissioned settings on next reboot NEW_PAYLOAD; stream << static_cast(Ti::StartupModeNormal); ZigbeeInterfaceTiReply *startupOptionReply = writeNvItem(Ti::NvItemIdStartupOption, payload); connect(startupOptionReply, &ZigbeeInterfaceTiReply::finished, reply, [=](){ NEW_PAYLOAD; stream << static_cast(deviceType); ZigbeeInterfaceTiReply *deviceTypeReply = writeNvItem(Ti::NvItemIdLogicalType, payload); connect(deviceTypeReply, &ZigbeeInterfaceTiReply::finished, reply, [=](){ NEW_PAYLOAD; stream << static_cast(0x01); ZigbeeInterfaceTiReply *deviceTypeReply = writeNvItem(Ti::NvItemIdZdoDirectCb, payload); connect(deviceTypeReply, &ZigbeeInterfaceTiReply::finished, reply, [=](){ NEW_PAYLOAD; stream << panId; ZigbeeInterfaceTiReply *panIdReply = writeNvItem(Ti::NvItemIdPanId, payload); connect(panIdReply, &ZigbeeInterfaceTiReply::finished, reply, [=](){ NEW_PAYLOAD; stream << ZigbeeUtils::generateRandomPanId(); stream << ZigbeeUtils::generateRandomPanId(); stream << ZigbeeUtils::generateRandomPanId(); stream << ZigbeeUtils::generateRandomPanId(); ZigbeeInterfaceTiReply *panIdReply = writeNvItem(Ti::NvItemIdExtendedPanId, payload); connect(panIdReply, &ZigbeeInterfaceTiReply::finished, reply, [=](){ ZigbeeInterfaceTiReply *panIdReply = writeNvItem(Ti::NvItemIdApsUseExtPanId, payload); connect(panIdReply, &ZigbeeInterfaceTiReply::finished, reply, [=](){ NEW_PAYLOAD; stream << channelMask.toUInt32(); ZigbeeInterfaceTiReply *channelsReply = writeNvItem(Ti::NvItemIdChanList, payload); connect(channelsReply, &ZigbeeInterfaceTiReply::finished, reply, [=](){ // TODO: commission nwk key // The adapter will generate a key, but we could provision our own so we could re-apply when restoring a backup // NEW_PAYLOAD; // stream << static_cast(0x01); // ZigbeeInterfaceTiReply *channelsReply = writeNvItem(Ti::NvItemIdPreCfgKeysEnable, payload); // connect(channelsReply, &ZigbeeInterfaceTiReply::finished, reply, [=](){ // NEW_PAYLOAD; // stream << <128 bit data>; // ZigbeeInterfaceTiReply *channelsReply = writeNvItem(Ti::NvItemIdPreCfgKey, payload); // connect(channelsReply, &ZigbeeInterfaceTiReply::finished, reply, [=](){ // For zStack12 we're done here. if (m_networkConfiguration.znpVersion == Ti::zStack12) { reply->finish(); return; } // zStack3x requires channels to be commissioned via AppCnf subsystem BdbCommissioning NEW_PAYLOAD; stream << static_cast(1); // Primary channel stream << static_cast(channelMask.toUInt32()); ZigbeeInterfaceTiReply *commissionReply = sendCommand(Ti::SubSystemAppCnf, Ti::AppCnfCommandBdbSetChannel, payload); connect(commissionReply, &ZigbeeInterfaceTiReply::finished, reply, [=](){ NEW_PAYLOAD; stream << static_cast(0); // Non-primary channel stream << static_cast(0); ZigbeeInterfaceTiReply *commissionReply = sendCommand(Ti::SubSystemAppCnf, Ti::AppCnfCommandBdbSetChannel, payload); connect(commissionReply, &ZigbeeInterfaceTiReply::finished, reply, [=](){ NEW_PAYLOAD; stream << static_cast(0x04);; ZigbeeInterfaceTiReply *commissionReply = sendCommand(Ti::SubSystemAppCnf, Ti::AppCnfCommandBdbStartCommissioning, payload); connect(commissionReply, &ZigbeeInterfaceTiReply::finished, reply, [=](){ reply->finish(); }); }); }); // }); // }); }); }); }); }); }); }); }); }); return reply; } void ZigbeeBridgeControllerTi::postStartup() { // Reading Network Info ZigbeeInterfaceTiReply *networkInfoReply = sendCommand(Ti::SubSystemZDO, Ti::ZDOCommandExtNwkInfo); connect(networkInfoReply, &ZigbeeInterfaceTiReply::finished, this, [=](){ if (networkInfoReply->statusCode() != Ti::StatusCodeSuccess) { qCWarning(dcZigbeeController()) << "Failed to read network info" << networkInfoReply->statusCode(); return; } quint8 devState, channel; quint16 shortAddr, panId, parentAddr; quint64 extendedPanId, parentExtAddr; { PAYLOAD_STREAM(networkInfoReply->responsePayload()); stream >> shortAddr >> devState >> panId >> parentAddr >> extendedPanId >> parentExtAddr >> channel; } m_networkConfiguration.panId = panId; m_networkConfiguration.extendedPanId = extendedPanId; m_networkConfiguration.currentChannel = channel; m_networkConfiguration.shortAddress = shortAddr; qCDebug(dcZigbeeController()) << "PAN ID:" << ZigbeeUtils::convertUint16ToHexString(m_networkConfiguration.panId); qCDebug(dcZigbeeController()) << "Short addr:" << ZigbeeUtils::convertUint16ToHexString(m_networkConfiguration.shortAddress); qCDebug(dcZigbeeController()) << "Extended Pan ID:" << ZigbeeUtils::convertUint64ToHexString(m_networkConfiguration.extendedPanId); qCDebug(dcZigbeeController()) << "Device state:" << devState; qCDebug(dcZigbeeController()) << "Channel:" << channel; qCDebug(dcZigbeeController()) << "IEEE address:" << m_networkConfiguration.ieeeAddress.toString(); // Registering for the ZDO raw message callback NEW_PAYLOAD; stream << static_cast(ZigbeeClusterLibrary::ClusterIdUnknown); ZigbeeInterfaceTiReply *registerCallbackReply = sendCommand(Ti::SubSystemZDO, Ti::ZDOCommandMsgCbRegister, payload); connect(registerCallbackReply, &ZigbeeInterfaceTiReply::finished, this, [=](){ if (registerCallbackReply->statusCode() != Ti::StatusCodeSuccess) { qCWarning(dcZigbeeInterface()) << "Failed to register ZDO msg callback"; return; } qCDebug(dcZigbeeController()) << "ZDO message callback registered"; // Fetching active endpoints from controller ZigbeeInterfaceTiReply *reply = new ZigbeeInterfaceTiReply(this); NEW_PAYLOAD; stream << static_cast(0x0000); // dstaddr stream << static_cast(0x0000); // networkOfInterest sendCommand(Ti::SubSystemZDO, Ti::ZDOCommandActiveEpReq, payload); waitFor(reply, Ti::SubSystemZDO, Ti::ZDOCommandActiveEpRsp); connect(reply, &ZigbeeInterfaceTiReply::finished, this, [=](){ PAYLOAD_STREAM(reply->responsePayload()); quint8 status, activeEpCount; quint16 srcAddr, nwkAddr; stream >> srcAddr >> status >> nwkAddr >> activeEpCount; for (int i = 0; i < activeEpCount; i++) { quint8 endpointId; stream >> endpointId; m_registeredEndpointIds.append(endpointId); } m_controllerState = ControllerStateRunning; emit controllerStateChanged(ControllerStateRunning); }); }); }); } ZigbeeInterfaceTiReply *ZigbeeBridgeControllerTi::factoryReset() { ZigbeeInterfaceTiReply *reply = new ZigbeeInterfaceTiReply(this); // Setting startup option to 4 to perform a clear on reset // Sending a reset request // Setting startup option back to 0 to boot into normal mode again ZigbeeInterfaceTiReply *deleteNIBReply = deleteNvItem(Ti::NvItemIdNIB); connect(deleteNIBReply, &ZigbeeInterfaceTiReply::finished, reply, [=](){ NEW_PAYLOAD; stream << (quint8)Ti::StartupModeClean; ZigbeeInterfaceTiReply *writeStartupOptionReply = writeNvItem(Ti::NvItemIdStartupOption, payload); connect(writeStartupOptionReply, &ZigbeeInterfaceTiReply::finished, reply, [=](){ if (writeStartupOptionReply->statusCode() != Ti::StatusCodeSuccess) { reply->finish(writeStartupOptionReply->statusCode()); return; } ZigbeeInterfaceTiReply *resetReply = reset(); connect(resetReply, &ZigbeeInterfaceTiReply::finished, reply, [=](){ NEW_PAYLOAD; stream << (quint8)Ti::StartupModeNormal; ZigbeeInterfaceTiReply *writeStartupOptionReply = writeNvItem(Ti::NvItemIdStartupOption, payload); connect(writeStartupOptionReply, &ZigbeeInterfaceTiReply::finished, reply, [=](){ reply->finish(writeStartupOptionReply->statusCode()); }); }); }); }); return reply; } ZigbeeInterfaceTiReply *ZigbeeBridgeControllerTi::requestSendRequest(const ZigbeeNetworkRequest &request) { Ti::TxOptions tiTxOptions = Ti::TxOptionNone; tiTxOptions |= (request.txOptions().testFlag(Zigbee::ZigbeeTxOptionAckTransmission) ? Ti::TxOptionApsAck : Ti::TxOptionNone); tiTxOptions |= (request.txOptions().testFlag(Zigbee::ZigbeeTxOptionSecurityEnabled) ? Ti::TxOptionApsSecurity : Ti::TxOptionNone); NEW_PAYLOAD; stream << static_cast(request.destinationAddressMode()); if (request.destinationAddressMode() == Zigbee::DestinationAddressModeIeeeAddress) { stream << request.destinationIeeeAddress().toUInt64(); } else { stream << static_cast(request.destinationShortAddress()); } stream << request.destinationEndpoint(); stream << static_cast(0x0000); // Intra-pan stream << request.sourceEndpoint(); stream << request.clusterId(); stream << request.requestId(); stream << static_cast(tiTxOptions); stream << request.radius(); stream << static_cast(request.asdu().length()); QByteArray asdu = request.asdu(); // If the the entire packet fits into the MTU, can send it as is // otherwise we'll have to send the packet without payload and provide the payload using StoreData instead if (asdu.length() < MT_RPC_DATA_MAX - 20) { for (int i = 0; i < asdu.length(); i++) { stream << static_cast(asdu.at(i)); } return sendCommand(Ti::SubSystemAF, Ti::AFCommandDataRequestExt, payload); } // NOTE: Leaving those prints as warnings for now as I didn't get the chance to test this much // so if anything goes wrong, it would appear in the logs unconditionally. qCWarning(dcZigbeeController()) << "Splitting huge packet into chunks!"; qCWarning(dcZigbeeController()) << "Full packet payload:" << asdu.toHex() << "LEN:" << asdu.length(); ZigbeeInterfaceTiReply *lastReply = nullptr; int i = 0; while (!asdu.isEmpty()) { QByteArray chunk = asdu.left(qMin(asdu.length(), 252)); asdu.remove(0, chunk.size()); qCWarning(dcZigbeeController()) << "Chunk" << i << ":" << chunk.toHex() << "LEN:" << chunk.length(); NEW_PAYLOAD; stream << static_cast(i++); stream << static_cast(chunk.length()); lastReply = sendCommand(Ti::SubSystemAF, Ti::AFCommandDataStore, chunk); } return lastReply; } void ZigbeeBridgeControllerTi::sendNextRequest() { // Check if there is a reply request to send if (m_replyQueue.isEmpty()) return; // Check if there is currently a running reply if (m_currentReply) return; m_currentReply = m_replyQueue.dequeue(); qCDebug(dcZigbeeController()) << "-->" << m_currentReply->subSystem() << QHash({ { Ti::SubSystemSys, QMetaEnum::fromType() }, { Ti::SubSystemMAC, QMetaEnum::fromType() }, { Ti::SubSystemAF, QMetaEnum::fromType() }, { Ti::SubSystemZDO, QMetaEnum::fromType() }, { Ti::SubSystemSAPI, QMetaEnum::fromType() }, { Ti::SubSystemUtil, QMetaEnum::fromType() }, { Ti::SubSystemDebug, QMetaEnum::fromType() }, { Ti::SubSystemApp, QMetaEnum::fromType() }, { Ti::SubSystemAppCnf, QMetaEnum::fromType() }, { Ti::SubSystemGreenPower, QMetaEnum::fromType() } }).value(m_currentReply->subSystem()).valueToKey(m_currentReply->command()) << m_currentReply->requestPayload().toHex(); m_interface->sendPacket(Ti::CommandTypeSReq, m_currentReply->subSystem(), m_currentReply->command(), m_currentReply->requestPayload()); m_currentReply->m_timer->start(); } ZigbeeInterfaceTiReply *ZigbeeBridgeControllerTi::sendCommand(Ti::SubSystem subSystem, quint8 command, const QByteArray &payload, int timeout) { ZigbeeInterfaceTiReply *reply = new ZigbeeInterfaceTiReply(subSystem, command, this, payload, timeout); connect(reply, &ZigbeeInterfaceTiReply::finished, reply, [=](){ if (reply->timedOut()) { if (m_controllerState == ControllerStateRunning) { qCWarning(dcZigbeeController()) << "Interface command timed out."; if (++m_timeouts < 5) { qCInfo(dcZigbeeController()) << "Retrying..." << m_timeouts << "/" << 5; sendCommand(subSystem, command, payload, timeout); } else { qCInfo(dcZigbeeController()) << "Resetting ZigBee interface"; m_interface->reconnectController(); } } } else { m_timeouts = 0; } if (m_currentReply == reply) { m_currentReply = nullptr; QMetaObject::invokeMethod(this, "sendNextRequest", Qt::QueuedConnection); } }); m_replyQueue.enqueue(reply); QMetaObject::invokeMethod(this, "sendNextRequest", Qt::QueuedConnection); return reply; } ZigbeeInterfaceTiReply *ZigbeeBridgeControllerTi::readNvItem(Ti::NvItemId itemId, quint16 offset) { NEW_PAYLOAD; stream << static_cast(itemId); stream << offset; return sendCommand(Ti::SubSystemSys, Ti::SYSCommandOsalNvReadExt, payload); } ZigbeeInterfaceTiReply *ZigbeeBridgeControllerTi::writeNvItem(Ti::NvItemId itemId, const QByteArray &data, quint16 offset) { qCDebug(dcZigbeeController()) << "Writing NV item:" << itemId << data.toHex(); NEW_PAYLOAD; stream << static_cast(itemId); stream << offset; stream << static_cast(data.length()); payload.append(data); return sendCommand(Ti::SubSystemSys, Ti::SYSCommandOsalNvWriteExt, payload); } ZigbeeInterfaceTiReply *ZigbeeBridgeControllerTi::deleteNvItem(Ti::NvItemId itemId) { qCDebug(dcZigbeeController()) << "Deleting NV item:" << itemId; ZigbeeInterfaceTiReply *reply = new ZigbeeInterfaceTiReply(this); NEW_PAYLOAD; stream << static_cast(itemId); ZigbeeInterfaceTiReply *getLengthReply = sendCommand(Ti::SubSystemSys, Ti::SYSCommandOsalNvLength, payload); connect(getLengthReply, &ZigbeeInterfaceTiReply::finished, reply, [=](){ if (getLengthReply->statusCode() != Ti::StatusCodeSuccess) { qCWarning(dcZigbeeController()) << "Error getting NV item length."; reply->finish(getLengthReply->statusCode()); return; } quint16 length; { PAYLOAD_STREAM(getLengthReply->responsePayload()); stream >> length; } NEW_PAYLOAD; stream << static_cast(itemId); stream << length; ZigbeeInterfaceTiReply *deleteReply = sendCommand(Ti::SubSystemSys, Ti::SYSCommandOsalNvDelete, payload); connect(deleteReply, &ZigbeeInterfaceTiReply::finished, reply, [=](){ if (deleteReply->statusCode() != Ti::StatusCodeSuccess) { qCWarning(dcZigbeeController()) << "Error deleting NV item"; } reply->finish(deleteReply->statusCode()); }); }); return reply; } void ZigbeeBridgeControllerTi::retrieveHugeMessage(const Zigbee::ApsdeDataIndication &pendingIndication, quint32 timestamp, quint16 dataLength) { // Suppressing clang analyzer warning about leaking "indication", since we'll actually // clean it up in the capturing lambda when the last request finishes. #ifndef __clang_analyzer__ Zigbee::ApsdeDataIndication *indication = new Zigbee::ApsdeDataIndication(pendingIndication); #endif quint8 chunkSize = 0; quint16 maxChunkSize = 253; for (quint16 i = 0; i * maxChunkSize < dataLength; i++) { chunkSize = qMin(maxChunkSize, static_cast(dataLength - i)); NEW_PAYLOAD; stream << timestamp; stream << i; stream << chunkSize; ZigbeeInterfaceTiReply *reply = sendCommand(Ti::SubSystemAF, Ti::AFCommandDataRetrieve, payload); // Note, capturing copies of i and chunksize, but a connect(reply, &ZigbeeInterfaceTiReply::finished, this, [this, reply, indication, i, maxChunkSize, dataLength](){ PAYLOAD_STREAM(reply->responsePayload()); quint8 status, len; stream >> status >> len; if (status != 0x00) { qCWarning(dcZigbeeController()) << "Failed to retrieve large payload chunk!" << status; } indication->asdu.append(reply->responsePayload().right(len)); if (i * maxChunkSize >= dataLength) { // This is the last one... emit apsDataIndicationReceived(*indication); delete indication; } }); } } ZigbeeInterfaceTiReply *ZigbeeBridgeControllerTi::start() { NEW_PAYLOAD; stream << static_cast(100); // Startup delay return sendCommand(Ti::SubSystemZDO, Ti::ZDOCommandStartupFromApp, payload); } ZigbeeInterfaceTiReply *ZigbeeBridgeControllerTi::registerEndpoint(quint8 endpointId, Zigbee::ZigbeeProfile profile, quint16 deviceId, quint8 deviceVersion, const QList &inputClusters, const QList &outputClusters) { if (m_registeredEndpointIds.contains(endpointId)) { ZigbeeInterfaceTiReply *reply = new ZigbeeInterfaceTiReply(this); QTimer::singleShot(0, reply, [=](){ reply->finish(Ti::StatusCodeSuccess); }); return reply; } NEW_PAYLOAD; stream << endpointId; stream << static_cast(profile); stream << deviceId; stream << deviceVersion; stream << static_cast(0x00); // latency requirement stream << static_cast(inputClusters.count()); foreach (quint16 inputCluster, inputClusters) { stream << inputCluster; } stream << static_cast(outputClusters.count()); foreach (quint16 outputCluster, outputClusters) { stream << static_cast(outputCluster); } ZigbeeInterfaceTiReply *reply = sendCommand(Ti::SubSystemAF, Ti::AFCommandRegister, payload); connect(reply, &ZigbeeInterfaceTiReply::finished, this, [=](){ PAYLOAD_STREAM(reply->responsePayload()); quint8 status; stream >> status; if (status == Ti::StatusCodeSuccess) { m_registeredEndpointIds.append(endpointId); } reply->m_statusCode = static_cast(status); }); return reply; } ZigbeeInterfaceTiReply *ZigbeeBridgeControllerTi::addEndpointToGroup(quint8 endpointId, quint16 groupId) { ZigbeeInterfaceTiReply *reply = new ZigbeeInterfaceTiReply(this); NEW_PAYLOAD; stream << endpointId; stream << groupId; stream << static_cast(0x00); // Group name length ZigbeeInterfaceTiReply *findGroupReply = sendCommand(Ti::SubSystemZDO, Ti::ZDOCommandExtFindGroup, payload); connect(findGroupReply, &ZigbeeInterfaceTiReply::finished, reply, [=](){ quint8 status; PAYLOAD_STREAM(findGroupReply->responsePayload()); stream >> status; if (status == 0x00) { qCDebug(dcZigbeeController()) << "Group already existing."; reply->finish(); } else { NEW_PAYLOAD; stream << endpointId; stream << groupId; stream << static_cast(0x00); ZigbeeInterfaceTiReply *addGroupReply = sendCommand(Ti::SubSystemZDO, Ti::ZDOCommandExtAddGroup, payload); connect(addGroupReply, &ZigbeeInterfaceTiReply::finished, reply, [=](){ reply->finish(addGroupReply->statusCode()); }); } }); return reply; } ZigbeeInterfaceTiReply *ZigbeeBridgeControllerTi::requestPermitJoin(quint8 seconds, const quint16 &networkAddress) { NEW_PAYLOAD; stream << static_cast(networkAddress == Zigbee::BroadcastAddressAllRouters ? 0x0F : 0x02); stream << static_cast(networkAddress); stream << seconds; stream << static_cast(0x00); // tcsignificance ZigbeeInterfaceTiReply *reply = sendCommand(Ti::SubSystemZDO, Ti::ZDOCommandMgmtPermitJoinReq, payload); ZigbeeInterfaceTiReply *waitForJoinRsp = new ZigbeeInterfaceTiReply(this); waitFor(waitForJoinRsp, Ti::SubSystemZDO, Ti::ZDOCommandMgmtPermitJoinRsp); connect(waitForJoinRsp, &ZigbeeInterfaceTiReply::finished, this, [=](){ // zStack actually has an indication for permit join state changes which, when working, // gives the current permit join state and the remaining seconds. // Sadly, this doesn't seem to work for zStack3x0 and also seems a bit buggy on zStack12. // So instead of relying on that, let's try to stay in sync with a timer :/ emit permitJoinStateChanged(seconds); m_permitJoinTimer.start(seconds * 1000); }); return reply; } void ZigbeeBridgeControllerTi::waitFor(ZigbeeInterfaceTiReply *reply, Ti::SubSystem subSystem, quint8 command) { WaitData waitData; waitData.subSystem = subSystem; waitData.command = command; m_waitFors.insert(reply, waitData); connect(reply, &ZigbeeInterfaceTiReply::finished, this, [=](){ m_waitFors.remove(reply); }); } void ZigbeeBridgeControllerTi::waitFor(ZigbeeInterfaceTiReply *reply, Ti::SubSystem subSystem, quint8 command, const QByteArray &payload) { WaitData waitData; waitData.subSystem = subSystem; waitData.command = command; waitData.payload = payload; waitData.comparePayload = true; m_waitFors.insert(reply, waitData); connect(reply, &ZigbeeInterfaceTiReply::finished, this, [=](){ m_waitFors.remove(reply); }); } void ZigbeeBridgeControllerTi::onInterfaceAvailableChanged(bool available) { qCDebug(dcZigbeeController()) << "Interface available changed" << available; if (!available) { // Clean up any pending replies while (!m_replyQueue.isEmpty()) { ZigbeeInterfaceTiReply *reply = m_replyQueue.dequeue(); reply->abort(); } m_controllerState = ControllerStateDown; emit controllerStateChanged(m_controllerState); } setAvailable(available); sendNextRequest(); } void ZigbeeBridgeControllerTi::onInterfacePacketReceived(Ti::SubSystem subSystem, Ti::CommandType commandType, quint8 command, const QByteArray &payload) { qCDebug(dcZigbeeController()) << "<--" << subSystem << commandType << QHash({ { Ti::SubSystemSys, QMetaEnum::fromType() }, { Ti::SubSystemMAC, QMetaEnum::fromType() }, { Ti::SubSystemAF, QMetaEnum::fromType() }, { Ti::SubSystemZDO, QMetaEnum::fromType() }, { Ti::SubSystemSAPI, QMetaEnum::fromType() }, { Ti::SubSystemUtil, QMetaEnum::fromType() }, { Ti::SubSystemDebug, QMetaEnum::fromType() }, { Ti::SubSystemApp, QMetaEnum::fromType() }, { Ti::SubSystemAppCnf, QMetaEnum::fromType() }, { Ti::SubSystemGreenPower, QMetaEnum::fromType() } }).value(subSystem).valueToKey(command) << payload.toHex(); if (commandType == Ti::CommandTypeSRsp) { if (m_currentReply && m_currentReply->command() == command) { m_currentReply->m_statusCode = Ti::StatusCodeSuccess; m_currentReply->m_responsePayload = payload; emit m_currentReply->finished(); } else { qCWarning(dcZigbeeController()) << "Received a reply while not expecting it!"; } return; } if (commandType == Ti::CommandTypeAReq) { switch (subSystem) { case Ti::SubSystemSys: switch (command) { case Ti::SYSCommandResetInd: { PAYLOAD_STREAM(payload); quint8 reason, transportRev, productId, majorRel, minorRel, hwRev; stream >> reason >> transportRev >> productId >> majorRel >> minorRel >> hwRev; qCDebug(dcZigbeeController()) << "Controller reset:" << static_cast(reason); qCDebug(dcZigbeeController()) << "Transport revision:" << transportRev << "Product ID:" << productId << "Major:" << majorRel << "Minor:" << minorRel << "HW Rev:" << hwRev; break; } default: qCDebug(dcZigbeeController()) << "Unhandled system command"; } break; case Ti::SubSystemZDO: switch (command) { case Ti::ZDOCommandStateChangeInd: qCDebug(dcZigbeeController()) << "Device state changed!" << payload.at(0); if (payload.at(0) == 0x09) { // We're not emitting state changed right away, need to do some post setup routine postStartup(); } break; case Ti::ZDOCommandPermitJoinInd: qCDebug(dcZigbeeController()) << "Permit join indication" << payload.at(0); // Note: This doesn't seem to work for zStack3x0 and also is a bit buggy for zStack12 // See requestPermitJoin() for the workaround. emit permitJoinStateChanged(payload.at(0)); break; case Ti::ZDOCommandMgmtPermitJoinRsp: qCDebug(dcZigbeeController()) << "PermitJoinRsp received:" << payload; // Silencing this. We'll use PermitJoinInd to update the state as that indicates start and end // In theory we'd need to check if the network address in the payload matches with what we reqeusted.... break; case Ti::ZDOCommandTcDeviceInd: qCDebug(dcZigbeeController()) << "Device join indication recived:" << payload; break; case Ti::ZDOCommandEndDeviceAnnceInd: { PAYLOAD_STREAM(payload); quint16 shortAddress, nwkAddress; quint64 ieeeAddress; quint8 capabilities; stream >> shortAddress >> nwkAddress >> ieeeAddress >> capabilities; qCDebug(dcZigbeeController()) << "End device announce indication received:" << payload; emit deviceIndication(shortAddress, ZigbeeAddress(ieeeAddress), capabilities); break; } case Ti::ZDOCommandLeaveInd: { PAYLOAD_STREAM(payload); quint16 srcAddr; quint64 srcIeeeAddr; quint8 request, remove, rejoin; stream >> srcAddr >> srcIeeeAddr >> request >> remove >> rejoin; emit nodeLeft(ZigbeeAddress(srcIeeeAddr), request, remove, rejoin); break; } case Ti::ZDOCommandNodeDescRsp: case Ti::ZDOCommandPowerDescRsp: case Ti::ZDOCommandSimpleDescRsp: case Ti::ZDOCommandActiveEpRsp: case Ti::ZDOCommandBindRsp: case Ti::ZDOCommandMgmtLqiRsp: case Ti::ZDOCommandMgmtRtgRsp: case Ti::ZDOCommandMgmtBindRsp: // silencing these as we're using the raw data in MsgCbIncoming instead // nymea-zigbee parses this on its own. break; case Ti::ZDOCommandMsgCbIncoming: { qCDebug(dcZigbeeController()) << "Incoming ZDO message:" << payload.toHex(); quint16 srcAddr, clusterId, macDstAddr; quint8 wasBroadcast, securityInUse, transactionSequenceNumber; PAYLOAD_STREAM(payload); stream >> srcAddr; stream >> wasBroadcast; stream >> clusterId; stream >> securityInUse; stream >> transactionSequenceNumber; stream >> macDstAddr; QByteArray adpu = payload.right(payload.length() - 9); if (clusterId == ZigbeeDeviceProfile::ZdoCommand::DeviceAnnounce || clusterId == ZigbeeDeviceProfile::ZdoCommand::MgmtPermitJoinResponse) { // Silencing those as we're using the proper z-Stack API for them qCDebug(dcZigbeeController()) << "Ignoring raw ZDO message for command" << static_cast(clusterId); return; } QByteArray asdu; QDataStream asduStream(&asdu, QIODevice::WriteOnly); asduStream.setByteOrder(QDataStream::LittleEndian); asduStream << transactionSequenceNumber; asdu.append(adpu); Zigbee::ApsdeDataIndication indication; indication.destinationAddressMode = Zigbee::DestinationAddressModeShortAddress; indication.sourceAddressMode = Zigbee::DestinationAddressModeShortAddress; indication.asdu = asdu; indication.clusterId = clusterId; indication.sourceShortAddress = srcAddr; emit apsDataIndicationReceived(indication); break; } case Ti::ZDOCommandSrcRtgInd: { PAYLOAD_STREAM(payload); quint16 srcAddr; quint8 relayCount; stream >> srcAddr; stream >> relayCount; QDebug dbg = qDebug(dcZigbeeController()); dbg << "Node" << ZigbeeUtils::convertUint16ToHexString(srcAddr) << "is routed" << (relayCount == 0 ? "directly" : "via " + QString::number(relayCount) + " hops:" ); for (int i = 0; i < relayCount; i++) { quint16 relayAddr; stream >> relayAddr; dbg << ZigbeeUtils::convertUint16ToHexString(relayAddr); } break; } default: qCWarning(dcZigbeeController()) << "Unhandled ZDO AREQ notification" << (Ti::ZDOCommand)command; } break; case Ti::SubSystemAF: switch (command) { case Ti::AFCommandIncomingMsg: { quint8 srcEndpoint, dstEndpoint, wasBroadcast, lqi, securityUse, transactionSequenceNumber, dataLen, status; quint16 groupId, clusterId, srcAddr, addrOfInterest; quint32 timestamp; PAYLOAD_STREAM(payload); stream >> groupId; stream >> clusterId; stream >> srcAddr; stream >> srcEndpoint; stream >> dstEndpoint; stream >> wasBroadcast; stream >> lqi; stream >> securityUse; stream >> timestamp; stream >> transactionSequenceNumber; stream >> dataLen; QByteArray asdu; for (int i = 0; i < dataLen; i++) { quint8 byte; stream >> byte; asdu.append(byte); } stream >> addrOfInterest; stream >> status; Zigbee::ApsdeDataIndication indication; indication.destinationAddressMode = Zigbee::DestinationAddressModeShortAddress; indication.sourceAddressMode = Zigbee::DestinationAddressModeShortAddress; indication.sourceEndpoint = srcEndpoint; indication.sourceShortAddress = srcAddr; indication.destinationEndpoint = dstEndpoint; indication.clusterId = clusterId; indication.profileId = Zigbee::ZigbeeProfileHomeAutomation; indication.lqi = lqi; indication.asdu = asdu; emit apsDataIndicationReceived(indication); break; } case Ti::AFCommandIncomingMsgExt: { quint8 srcAddrMode, srcEndpoint, dstEndpoint, wasBroadcast, lqi, securityUse, transactionSequenceNumber, macSrcAddr, radius; quint16 groupId, clusterId, srcPanId, dataLen; quint32 timestamp; quint64 srcAddr; // If the payload is missing (meaning the overall packet size is exactly 29), the payload is huge and is // not contained in thie packet but needs to be retrieved separately. bool hugePacket = payload.length() == 29; PAYLOAD_STREAM(payload); stream >> groupId; stream >> clusterId; stream >> srcAddrMode; stream >> srcAddr; stream >> srcEndpoint; stream >> srcPanId; stream >> dstEndpoint; stream >> wasBroadcast; stream >> lqi; stream >> securityUse; stream >> timestamp; stream >> transactionSequenceNumber; stream >> dataLen; QByteArray asdu; if (!hugePacket) { for (int i = 0; i < dataLen; i++) { quint8 byte; stream >> byte; asdu.append(byte); } } stream >> macSrcAddr; stream >> radius; Zigbee::ApsdeDataIndication indication; indication.destinationAddressMode = Zigbee::DestinationAddressModeShortAddress; indication.sourceAddressMode = static_cast(srcAddrMode); indication.sourceEndpoint = srcEndpoint; indication.sourceIeeeAddress = srcAddr; indication.destinationEndpoint = dstEndpoint; indication.clusterId = clusterId; indication.profileId = Zigbee::ZigbeeProfileHomeAutomation; indication.lqi = lqi; indication.asdu = asdu; if (!hugePacket) { emit apsDataIndicationReceived(indication); } else { retrieveHugeMessage(indication, timestamp, dataLen); } break; } case Ti::AFCommandDataConfirm: { quint8 status, endpoint, transactionSequenceNumber; PAYLOAD_STREAM(payload); stream >> status >> endpoint >> transactionSequenceNumber; Zigbee::ApsdeDataConfirm confirm; confirm.requestId = transactionSequenceNumber; confirm.destinationEndpoint = endpoint; confirm.zigbeeStatusCode = status; emit apsDataConfirmReceived(confirm); break; } default: qCWarning(dcZigbeeController()) << "Unhandled AF AREQ notification"; } break; case Ti::SubSystemAppCnf: switch (command) { case Ti::AppCnfCommandBdbCommissioningNotification:{ PAYLOAD_STREAM(payload); quint8 status, commissioningMode, remainingCommissioningModes; stream >> status >> commissioningMode >> remainingCommissioningModes; qCDebug(dcZigbeeController()) << "BDB commissioning notification received. Status:" << status << "Mode:" << commissioningMode << "Remaining:" << remainingCommissioningModes; break; } default: qCWarning(dcZigbeeController()) << "Unhandled AppCnf AREQ notification"; } break; default: qCWarning(dcZigbeeController()) << "Unhandled AREQ notification"; } foreach (ZigbeeInterfaceTiReply *reply, m_waitFors.keys()) { WaitData waitData = m_waitFors.value(reply); if (waitData.subSystem == subSystem && waitData.command == command) { if (!waitData.comparePayload || waitData.payload == payload) { qCDebug(dcZigbeeController()) << "awaited event received."; reply->m_responsePayload = payload; reply->finish(); } } } } else if (commandType == Ti::CommandTypeSReq) { qCWarning(dcZigbeeController()) << "Unhandled incoming SREQ command:" << subSystem << command; } } bool ZigbeeBridgeControllerTi::enable(const QString &serialPort, qint32 baudrate) { return m_interface->enable(serialPort, baudrate); } void ZigbeeBridgeControllerTi::disable() { m_interface->disable(); } QDebug operator<<(QDebug debug, const TiNetworkConfiguration &configuration) { QDebugStateSaver saver(debug); debug.nospace() << "Network configuration: " << "\n"; debug.nospace() << " - IEEE address: " << configuration.ieeeAddress.toString() << "\n"; debug.nospace() << " - NWK address: " << ZigbeeUtils::convertUint16ToHexString(configuration.shortAddress) << "\n"; debug.nospace() << " - PAN ID: " << ZigbeeUtils::convertUint16ToHexString(configuration.panId) << " (" << configuration.panId << ")\n"; debug.nospace() << " - Extended PAN ID: " << ZigbeeUtils::convertUint64ToHexString(configuration.extendedPanId) << "\n"; debug.nospace() << " - Channel mask: " << ZigbeeChannelMask(configuration.channelMask) << "\n"; debug.nospace() << " - Channel: " << configuration.currentChannel << "\n"; debug.nospace() << " - ZNP version: " << ZigbeeUtils::convertUint16ToHexString(configuration.znpVersion) << "\n"; return debug; }