nymea-zigbee/libnymea-zigbee/backends/ti/zigbeebridgecontrollerti.cpp

1085 lines
50 KiB
C++

// 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 <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "zigbeeutils.h"
#include "loggingcategory.h"
#include "zigbeechannelmask.h"
#include "zdo/zigbeedeviceprofile.h"
#include "zigbeebridgecontrollerti.h"
#include <QDataStream>
#include <QMetaEnum>
#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<quint8>(0x03); // LED ID
stream << static_cast<quint8>(on);
return sendCommand(Ti::SubSystemUtil, Ti::UtilCommandLedControl, payload);
}
ZigbeeInterfaceTiReply* ZigbeeBridgeControllerTi::reset()
{
NEW_PAYLOAD
stream << static_cast<quint8>(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<quint8>(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<Ti::ControllerCapabilities>(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<Ti::ZnpVersion>(product);
setFirmwareVersion(QString("%0(%1) - %2.%3.%4.%5")
.arg(QMetaEnum::fromType<Ti::ZnpVersion>().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<quint8>(Ti::StartupModeNormal);
ZigbeeInterfaceTiReply *startupOptionReply = writeNvItem(Ti::NvItemIdStartupOption, payload);
connect(startupOptionReply, &ZigbeeInterfaceTiReply::finished, reply, [=](){
NEW_PAYLOAD;
stream << static_cast<quint8>(deviceType);
ZigbeeInterfaceTiReply *deviceTypeReply = writeNvItem(Ti::NvItemIdLogicalType, payload);
connect(deviceTypeReply, &ZigbeeInterfaceTiReply::finished, reply, [=](){
NEW_PAYLOAD;
stream << static_cast<quint8>(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<quint8>(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<quint8>(1); // Primary channel
stream << static_cast<quint32>(channelMask.toUInt32());
ZigbeeInterfaceTiReply *commissionReply = sendCommand(Ti::SubSystemAppCnf, Ti::AppCnfCommandBdbSetChannel, payload);
connect(commissionReply, &ZigbeeInterfaceTiReply::finished, reply, [=](){
NEW_PAYLOAD;
stream << static_cast<quint8>(0); // Non-primary channel
stream << static_cast<quint32>(0);
ZigbeeInterfaceTiReply *commissionReply = sendCommand(Ti::SubSystemAppCnf, Ti::AppCnfCommandBdbSetChannel, payload);
connect(commissionReply, &ZigbeeInterfaceTiReply::finished, reply, [=](){
NEW_PAYLOAD;
stream << static_cast<quint8>(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<quint16>(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<quint16>(0x0000); // dstaddr
stream << static_cast<quint16>(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<quint8>(request.destinationAddressMode());
if (request.destinationAddressMode() == Zigbee::DestinationAddressModeIeeeAddress) {
stream << request.destinationIeeeAddress().toUInt64();
} else {
stream << static_cast<quint64>(request.destinationShortAddress());
}
stream << request.destinationEndpoint();
stream << static_cast<quint16>(0x0000); // Intra-pan
stream << request.sourceEndpoint();
stream << request.clusterId();
stream << request.requestId();
stream << static_cast<quint8>(tiTxOptions);
stream << request.radius();
stream << static_cast<quint16>(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<quint8>(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<quint16>(i++);
stream << static_cast<quint8>(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::SubSystem, QMetaEnum>({
{ Ti::SubSystemSys, QMetaEnum::fromType<Ti::SYSCommand>() },
{ Ti::SubSystemMAC, QMetaEnum::fromType<Ti::MACCommand>() },
{ Ti::SubSystemAF, QMetaEnum::fromType<Ti::AFCommand>() },
{ Ti::SubSystemZDO, QMetaEnum::fromType<Ti::ZDOCommand>() },
{ Ti::SubSystemSAPI, QMetaEnum::fromType<Ti::SAPICommand>() },
{ Ti::SubSystemUtil, QMetaEnum::fromType<Ti::UtilCommand>() },
{ Ti::SubSystemDebug, QMetaEnum::fromType<Ti::DebugCommand>() },
{ Ti::SubSystemApp, QMetaEnum::fromType<Ti::AppCommand>() },
{ Ti::SubSystemAppCnf, QMetaEnum::fromType<Ti::AppCnfCommand>() },
{ Ti::SubSystemGreenPower, QMetaEnum::fromType<Ti::GreenPowerCommand>() }
}).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<quint16>(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<quint16>(itemId);
stream << offset;
stream << static_cast<quint16>(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<quint16>(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<quint16>(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<quint16>(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<quint16>(100); // Startup delay
return sendCommand(Ti::SubSystemZDO, Ti::ZDOCommandStartupFromApp, payload);
}
ZigbeeInterfaceTiReply *ZigbeeBridgeControllerTi::registerEndpoint(quint8 endpointId, Zigbee::ZigbeeProfile profile, quint16 deviceId, quint8 deviceVersion, const QList<quint16> &inputClusters, const QList<quint16> &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<quint16>(profile);
stream << deviceId;
stream << deviceVersion;
stream << static_cast<quint8>(0x00); // latency requirement
stream << static_cast<quint8>(inputClusters.count());
foreach (quint16 inputCluster, inputClusters) {
stream << inputCluster;
}
stream << static_cast<quint8>(outputClusters.count());
foreach (quint16 outputCluster, outputClusters) {
stream << static_cast<quint16>(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<Ti::StatusCode>(status);
});
return reply;
}
ZigbeeInterfaceTiReply *ZigbeeBridgeControllerTi::addEndpointToGroup(quint8 endpointId, quint16 groupId)
{
ZigbeeInterfaceTiReply *reply = new ZigbeeInterfaceTiReply(this);
NEW_PAYLOAD;
stream << endpointId;
stream << groupId;
stream << static_cast<quint8>(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<quint8>(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<quint8>(networkAddress == Zigbee::BroadcastAddressAllRouters ? 0x0F : 0x02);
stream << static_cast<quint16>(networkAddress);
stream << seconds;
stream << static_cast<quint8>(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::SubSystem, QMetaEnum>({
{ Ti::SubSystemSys, QMetaEnum::fromType<Ti::SYSCommand>() },
{ Ti::SubSystemMAC, QMetaEnum::fromType<Ti::MACCommand>() },
{ Ti::SubSystemAF, QMetaEnum::fromType<Ti::AFCommand>() },
{ Ti::SubSystemZDO, QMetaEnum::fromType<Ti::ZDOCommand>() },
{ Ti::SubSystemSAPI, QMetaEnum::fromType<Ti::SAPICommand>() },
{ Ti::SubSystemUtil, QMetaEnum::fromType<Ti::UtilCommand>() },
{ Ti::SubSystemDebug, QMetaEnum::fromType<Ti::DebugCommand>() },
{ Ti::SubSystemApp, QMetaEnum::fromType<Ti::AppCommand>() },
{ Ti::SubSystemAppCnf, QMetaEnum::fromType<Ti::AppCnfCommand>() },
{ Ti::SubSystemGreenPower, QMetaEnum::fromType<Ti::GreenPowerCommand>() }
}).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<Ti::ResetReason>(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<ZigbeeDeviceProfile::ZdoCommand>(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<Zigbee::DestinationAddressMode>(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;
}