* Copyright (c) 2016-2023 dresden elektronik ingenieurtechnik gmbh.
* All rights reserved.
*
* The software in this package is published under the terms of the BSD
* style license a copy of which has been included with this distribution in
* the LICENSE.txt file.
*
*/
#include <QApplication>
#include <QString>
#include <QTcpSocket>
#include <QVariantMap>
#include "de_web_plugin_private.h"
#include "deconz/u_rand32.h"
#define TL_SCAN_WAIT_TIME 250
#define TL_TRANSACTION_TIMEOUT 7000
#define TL_RECONNECT_NOW 100
#define TL_RECONNECT_CHECK_DELAY 5000
#define TL_DISCONNECT_CHECK_DELAY 100
#define TL_SCAN_COUNT (touchlinkChannel == 11 ? 5 : 1)
#define TL_CMD_SCAN_REQ 0x00
#define TL_CMD_SCAN_RSP 0x01
#define TL_CMD_DEVICE_INFORMATION_REQ 0x02
#define TL_CMD_DEVICE_INFORMATION_RSP 0x03
#define TL_CMD_IDENTIFY_REQ 0x06
#define TL_CMD_RESET_TO_FACTORY_NEW_REQ 0x07
#define TL_CMD_NETWORK_START_REQ 0x10
#define TL_CMD_NETWORK_START_RSP 0x11
#define TL_CMD_NETWORK_JOIN_ROUTER_REQ 0x12
#define TL_CMD_NETWORK_JOIN_ROUTER_RSP 0x13
#define TL_CMD_NETWORK_JOIN_ENDDEVICE_REQ 0x14
#define TL_CMD_NETWORK_JOIN_ENDDEVICE_RSP 0x15
#define TL_CMD_NETWORK_UPDATE_REQ 0x16
#define TL_CMD_ENDPOINT_INFORMATION 0x40
#define FACTORY_NEW_FLAG 0x01
#define NETWORK_ATTEMPS 10
*/
void DeRestPluginPrivate::initTouchlinkApi()
{
touchlinkState = TL_Idle;
touchlinkCtrl = deCONZ::TouchlinkController::instance();
DBG_Assert(touchlinkCtrl != 0);
connect(touchlinkCtrl, SIGNAL(startInterpanModeConfirm(deCONZ::TouchlinkStatus)),
this, SLOT(startTouchlinkModeConfirm(deCONZ::TouchlinkStatus)));
connect(touchlinkCtrl, SIGNAL(sendInterpanConfirm(deCONZ::TouchlinkStatus)),
this, SLOT(sendTouchlinkConfirm(deCONZ::TouchlinkStatus)));
connect(touchlinkCtrl, SIGNAL(interpanIndication(QByteArray)),
this, SLOT(interpanDataIndication(QByteArray)));
touchlinkTimer = new QTimer(this);
touchlinkTimer->setSingleShot(true);
connect(touchlinkTimer, SIGNAL(timeout()),
this, SLOT(touchlinkTimerFired()));
}
\param req - request data
\param rsp - response data
\return REQ_READY_SEND
REQ_NOT_HANDLED
*/
int DeRestPluginPrivate::handleTouchlinkApi(const ApiRequest &req, ApiResponse &rsp)
{
if (req.path[2] != "touchlink")
{
return REQ_NOT_HANDLED;
}
if ((req.path.size() == 4) && (req.hdr.method() == "POST") && (req.path[3] == "scan"))
{
return touchlinkScan(req, rsp);
}
if ((req.path.size() == 4) && (req.hdr.method() == "GET") && (req.path[3] == "scan"))
{
return getTouchlinkScanResults(req, rsp);
}
if ((req.path.size() == 5) && (req.hdr.method() == "POST") && (req.path[4] == "identify"))
{
return identifyLight(req, rsp);
}
if ((req.path.size() == 5) && (req.hdr.method() == "POST") && (req.path[4] == "reset"))
{
return resetLight(req, rsp);
}
return REQ_NOT_HANDLED;
}
\param req - request data
\param rsp - response data
\return REQ_READY_SEND
REQ_NOT_HANDLED
*/
int DeRestPluginPrivate::touchlinkScan(const ApiRequest &req, ApiResponse &rsp)
{
Q_UNUSED(req);
if (touchlinkState != TL_Idle)
{
rsp.httpStatus = HttpStatusServiceUnavailable;
return REQ_READY_SEND;
}
uint32_t transactionId = U_rand32();
if (transactionId == 0)
{
transactionId++;
}
touchlinkAction = TouchlinkScan;
touchlinkChannel = 11;
touchlinkScanCount = 0;
touchlinkScanResponses.clear();
touchlinkScanTime = QDateTime::currentDateTime();
touchlinkReq.setTransactionId(transactionId);
touchlinkDisconnectNetwork();
rsp.httpStatus = HttpStatusOk;
return REQ_READY_SEND;
}
\param req - request data
\param rsp - response data
\return REQ_READY_SEND
REQ_NOT_HANDLED
*/
int DeRestPluginPrivate::getTouchlinkScanResults(const ApiRequest &req, ApiResponse &rsp)
{
Q_UNUSED(req);
rsp.httpStatus = HttpStatusOk;
bool scanning = false;
if ((touchlinkAction == TouchlinkScan) && (touchlinkState != TL_Idle))
{
scanning = true;
}
rsp.map["scanstate"] = (scanning ? "scanning" : "idle");
rsp.map["lastscan"] = touchlinkScanTime.toString("yyyy-MM-ddTHH:mm:ss");
QVariantMap result;
std::vector<ScanResponse>::const_iterator i = touchlinkScanResponses.begin();
std::vector<ScanResponse>::const_iterator end = touchlinkScanResponses.end();
for (; i != end; ++i)
{
QVariantMap item;
item["address"] = QString("0x%1").arg(i->address.ext(), int(16), int(16), QChar('0'));
item["factorynew"] = i->factoryNew;
item["rssi"] = (double)i->rssi;
item["channel"] = (double)i->channel;
item["panid"] = (double)i->panid;
result[i->id] = item;
}
rsp.map["result"] = result;
return REQ_READY_SEND;
}
\param req - request data
\param rsp - response data
\return REQ_READY_SEND
REQ_NOT_HANDLED
*/
int DeRestPluginPrivate::identifyLight(const ApiRequest &req, ApiResponse &rsp)
{
* - disconnect
* - start interpan mode
* - send interpan scan request
* - send interpan identify
* - reconnect
*/
if (touchlinkState != TL_Idle)
{
rsp.httpStatus = HttpStatusServiceUnavailable;
return REQ_READY_SEND;
}
QString id = req.path[3];
touchlinkDevice.id.clear();
{
std::vector<ScanResponse>::const_iterator i = touchlinkScanResponses.begin();
std::vector<ScanResponse>::const_iterator end = touchlinkScanResponses.end();
for (; i != end; ++i)
{
if (i->id == id)
{
touchlinkDevice = *i;
break;
}
}
}
if (touchlinkDevice.id.isEmpty())
{
rsp.httpStatus = HttpStatusNotFound;
return REQ_READY_SEND;
}
uint32_t transactionId = U_rand32();
if (transactionId == 0)
{
transactionId++;
}
touchlinkReq.setTransactionId(transactionId);
touchlinkAction = TouchlinkIdentify;
touchlinkChannel = touchlinkDevice.channel;
touchlinkDisconnectNetwork();
rsp.httpStatus = HttpStatusOk;
return REQ_READY_SEND;
}
\param req - request data
\param rsp - response data
\return REQ_READY_SEND
REQ_NOT_HANDLED
*/
int DeRestPluginPrivate::resetLight(const ApiRequest &req, ApiResponse &rsp)
{
* - disconnect
* - start interpan mode
* - send interpan scan request
* - send interpan reset to factory new request
* - reconnect
*/
if (touchlinkState != TL_Idle)
{
rsp.httpStatus = HttpStatusServiceUnavailable;
return REQ_READY_SEND;
}
QString id = req.path[3];
touchlinkDevice.id.clear();
{
std::vector<ScanResponse>::const_iterator i = touchlinkScanResponses.begin();
std::vector<ScanResponse>::const_iterator end = touchlinkScanResponses.end();
for (; i != end; ++i)
{
if (i->id == id)
{
touchlinkDevice = *i;
break;
}
}
}
if (touchlinkDevice.id.isEmpty())
{
rsp.httpStatus = HttpStatusNotFound;
return REQ_READY_SEND;
}
uint32_t transactionId = U_rand32();
if (transactionId == 0)
{
transactionId++;
}
touchlinkReq.setTransactionId(transactionId);
touchlinkAction = TouchlinkReset;
touchlinkChannel = touchlinkDevice.channel;
DBG_Printf(DBG_TLINK, "start touchlink reset for 0x%016llX\n", touchlinkDevice.address.ext());
touchlinkDisconnectNetwork();
rsp.httpStatus = HttpStatusOk;
return REQ_READY_SEND;
}
\param channel the channel which shall be used for interpan communication
*/
void DeRestPluginPrivate::startTouchlinkMode(uint8_t channel)
{
DBG_Printf(DBG_TLINK, "start interpan mode on channel %u\n", channel);
touchlinkChannel = channel;
touchlinkState = TL_StartingInterpanMode;
if (touchlinkCtrl->startInterpanMode(touchlinkChannel) != 0)
{
DBG_Printf(DBG_TLINK, "start interpan mode on channel %u failed\n", channel);
touchlinkStartReconnectNetwork(TL_RECONNECT_NOW);
}
}
\param status tells if starting the touchlink mode was successful
*/
void DeRestPluginPrivate::startTouchlinkModeConfirm(deCONZ::TouchlinkStatus status)
{
DBG_Printf(DBG_TLINK, "start touchlink mode %s\n", (status == deCONZ::TouchlinkSuccess ? "success" : "failed"));
if (touchlinkState != TL_StartingInterpanMode)
{
return;
}
if (status != deCONZ::TouchlinkSuccess)
{
touchlinkStartReconnectNetwork(TL_RECONNECT_NOW);
return;
}
if (touchlinkAction == TouchlinkScan)
{
sendTouchlinkScanRequest();
}
else if (touchlinkAction == TouchlinkIdentify)
{
sendTouchlinkScanRequest();
}
else if (touchlinkAction == TouchlinkReset)
{
sendTouchlinkScanRequest();
}
else
{
touchlinkStartReconnectNetwork(TL_RECONNECT_NOW);
}
}
*/
void DeRestPluginPrivate::touchlinkDisconnectNetwork()
{
DBG_Assert(touchlinkState == TL_Idle);
if (touchlinkState != TL_Idle)
{
return;
}
DBG_Assert(apsCtrl != 0);
if (!apsCtrl)
{
return;
}
touchlinkNetworkDisconnectAttempts = NETWORK_ATTEMPS;
touchlinkNetworkConnectedBefore = gwRfConnectedExpected;
touchlinkState = TL_DisconnectingNetwork;
apsCtrl->setNetworkState(deCONZ::NotInNetwork);
touchlinkTimer->start(TL_DISCONNECT_CHECK_DELAY);
}
*/
void DeRestPluginPrivate::checkTouchlinkNetworkDisconnected()
{
if (touchlinkState != TL_DisconnectingNetwork)
{
return;
}
if (touchlinkNetworkDisconnectAttempts > 0)
{
touchlinkNetworkDisconnectAttempts--;
}
if (isInNetwork())
{
if (touchlinkNetworkDisconnectAttempts == 0)
{
DBG_Printf(DBG_TLINK, "disconnect from network failed, abort touchlink action\n");
touchlinkStartReconnectNetwork(TL_RECONNECT_CHECK_DELAY);
}
else
{
DBG_Assert(apsCtrl != 0);
if (apsCtrl)
{
DBG_Printf(DBG_TLINK, "disconnect from network failed, try again\n");
apsCtrl->setNetworkState(deCONZ::NotInNetwork);
touchlinkTimer->start(TL_DISCONNECT_CHECK_DELAY);
}
else
{
touchlinkState = TL_Idle;
}
}
return;
}
startTouchlinkMode(touchlinkChannel);
}
*/
void DeRestPluginPrivate::sendTouchlinkScanRequest()
{
touchlinkReq.setChannel(touchlinkChannel);
touchlinkReq.setDstAddressMode(deCONZ::ApsNwkAddress);
touchlinkReq.dstAddress().setNwk(0xFFFF);
touchlinkReq.setPanId(0xFFFF);
touchlinkReq.setClusterId(0x1000);
touchlinkReq.setProfileId(ZLL_PROFILE_ID);
touchlinkReq.asdu().clear();
QDataStream stream(&touchlinkReq.asdu(), QIODevice::WriteOnly);
stream.setByteOrder(QDataStream::LittleEndian);
uint8_t cmd = TL_CMD_SCAN_REQ;
uint8_t frameControl = deCONZ::ZclFCProfileCommand | deCONZ::ZclFCDirectionClientToServer;
uint8_t seq = touchlinkReq.transactionId();
uint8_t zigbeeInfo = 0x02;
uint8_t zllInfo = 0x33;
stream << frameControl;
stream << seq;
stream << cmd;
stream << touchlinkReq.transactionId();
stream << zigbeeInfo;
stream << zllInfo;
touchlinkScanCount++;
DBG_Printf(DBG_TLINK, "send scan request TrId: 0x%08X\n", touchlinkReq.transactionId());
if (touchlinkCtrl->sendInterpanRequest(touchlinkReq) == 0)
{
touchlinkState = TL_SendingScanRequest;
}
else
{
DBG_Printf(DBG_TLINK, "touchlink send scan request failed\n");
touchlinkStartReconnectNetwork(TL_RECONNECT_NOW);
return;
}
}
*/
void DeRestPluginPrivate::sendTouchlinkIdentifyRequest()
{
touchlinkReq.setChannel(touchlinkChannel);
touchlinkReq.setDstAddressMode(deCONZ::ApsExtAddress);
touchlinkReq.dstAddress() = touchlinkDevice.address;
touchlinkReq.setPanId(touchlinkDevice.panid);
touchlinkReq.setClusterId(0x1000);
touchlinkReq.setProfileId(ZLL_PROFILE_ID);
touchlinkReq.asdu().clear();
QDataStream stream(&touchlinkReq.asdu(), QIODevice::WriteOnly);
stream.setByteOrder(QDataStream::LittleEndian);
uint8_t cmd = TL_CMD_IDENTIFY_REQ;
uint8_t frameControl = deCONZ::ZclFCProfileCommand | deCONZ::ZclFCDirectionClientToServer;
uint8_t seq = touchlinkReq.transactionId();
uint16_t duration = 5;
stream << frameControl;
stream << seq;
stream << cmd;
stream << touchlinkReq.transactionId();
stream << duration;
DBG_Printf(DBG_TLINK, "send identify request TrId: 0x%08X\n", touchlinkReq.transactionId());
if (touchlinkCtrl->sendInterpanRequest(touchlinkReq) == 0)
{
touchlinkState = TL_SendingIdentifyRequest;
}
else
{
DBG_Printf(DBG_TLINK, "touchlink send identify request failed\n");
touchlinkStartReconnectNetwork(TL_RECONNECT_NOW);
}
}
*/
void DeRestPluginPrivate::sendTouchlinkResetRequest()
{
touchlinkReq.setChannel(touchlinkChannel);
touchlinkReq.setDstAddressMode(deCONZ::ApsExtAddress);
touchlinkReq.dstAddress() = touchlinkDevice.address;
touchlinkReq.setPanId(touchlinkDevice.panid);
touchlinkReq.setClusterId(0x1000);
touchlinkReq.setProfileId(ZLL_PROFILE_ID);
touchlinkReq.asdu().clear();
QDataStream stream(&touchlinkReq.asdu(), QIODevice::WriteOnly);
stream.setByteOrder(QDataStream::LittleEndian);
uint8_t cmd = TL_CMD_RESET_TO_FACTORY_NEW_REQ;
uint8_t frameControl = deCONZ::ZclFCProfileCommand | deCONZ::ZclFCDirectionClientToServer;
uint8_t seq = touchlinkReq.transactionId();
stream << frameControl;
stream << seq;
stream << cmd;
stream << touchlinkReq.transactionId();
DBG_Printf(DBG_TLINK, "send reset request TrId: 0x%08X\n", touchlinkReq.transactionId());
if (touchlinkCtrl->sendInterpanRequest(touchlinkReq) == 0)
{
touchlinkState = TL_SendingResetRequest;
}
else
{
DBG_Printf(DBG_TLINK, "touchlink send reset request failed\n");
touchlinkStartReconnectNetwork(TL_RECONNECT_NOW);
}
}
*/
void DeRestPluginPrivate::touchlinkTimerFired()
{
switch (touchlinkState)
{
case TL_Idle:
break;
case TL_WaitScanResponses:
touchlinkScanTimeout();
break;
case TL_ReconnectNetwork:
touchlinkReconnectNetwork();
break;
case TL_DisconnectingNetwork:
checkTouchlinkNetworkDisconnected();
break;
case TL_SendingScanRequest:
sendTouchlinkScanRequest();
break;
default:
DBG_Printf(DBG_TLINK, "touchlinkTimerFired() unhandled state %d\n", touchlinkState);
break;
}
}
\param status tells if the request was sent
*/
void DeRestPluginPrivate::sendTouchlinkConfirm(deCONZ::TouchlinkStatus status)
{
if (status != deCONZ::TouchlinkSuccess)
{
DBG_Printf(DBG_TLINK, "touchlink confirm status %d for action %d\n", status, touchlinkAction);
}
if (touchlinkState == TL_SendingScanRequest)
{
switch (touchlinkAction)
{
case TouchlinkScan:
{
if (touchlinkScanCount > TL_SCAN_COUNT)
{
touchlinkState = TL_WaitScanResponses;
touchlinkTimer->start(TL_SCAN_WAIT_TIME);
}
else
{
touchlinkTimer->start(1);
}
}
break;
case TouchlinkIdentify:
case TouchlinkReset:
{
touchlinkState = TL_WaitScanResponses;
touchlinkTimer->start(TL_TRANSACTION_TIMEOUT);
}
break;
default:
{
DBG_Printf(DBG_TLINK, "unknown touchlink action: %d, abort\n", touchlinkAction);
touchlinkStartReconnectNetwork(TL_RECONNECT_NOW);
}
break;
}
}
else if (touchlinkState == TL_WaitScanResponses)
{
}
else if ((touchlinkState == TL_SendingIdentifyRequest) ||
(touchlinkState == TL_SendingResetRequest))
{
if (status == deCONZ::TouchlinkSuccess)
{
if (touchlinkState == TL_SendingResetRequest)
{
std::vector<LightNode>::iterator i = nodes.begin();
std::vector<LightNode>::iterator end = nodes.end();
for (; i != end; ++i)
{
if (i->address().ext() == touchlinkDevice.address.ext())
{
i->item(RStateReachable)->setValue(false);
updateEtag(i->etag);
updateEtag(gwConfigEtag);
}
}
}
}
touchlinkStartReconnectNetwork(TL_RECONNECT_NOW);
}
else if (touchlinkState != TL_Idle)
{
DBG_Printf(DBG_TLINK, "touchlink send confirm in unexpected state: %d\n", touchlinkState);
}
}
*/
void DeRestPluginPrivate::touchlinkScanTimeout()
{
if (touchlinkState != TL_WaitScanResponses)
{
return;
}
if (touchlinkAction == TouchlinkReset || touchlinkAction == TouchlinkIdentify)
{
DBG_Printf(DBG_TLINK, "wait for scan response before reset/identify to fn timeout\n");
touchlinkStartReconnectNetwork(TL_RECONNECT_NOW);
return;
}
else if (touchlinkAction == TouchlinkScan)
{
if (touchlinkChannel < 26)
{
touchlinkChannel++;
touchlinkScanCount = 0;
startTouchlinkMode(touchlinkChannel);
}
else
{
DBG_Printf(DBG_TLINK, "scan finished found %u device(s)\n", (uint)touchlinkScanResponses.size());
touchlinkStartReconnectNetwork(TL_RECONNECT_NOW);
}
}
}
\param data the indication data
*/
void DeRestPluginPrivate::interpanDataIndication(const QByteArray &data)
{
if (touchlinkState == TL_Idle)
{
DBG_Printf(DBG_TLINK, "discard ipan frame in TL_Idle state\n");
return;
}
QDataStream stream(data);
stream.setByteOrder(QDataStream::LittleEndian);
uint16_t srcPanId;
quint64 srcAddress;
uint16_t dstPanId;
uint8_t dstAddressMode;
quint64 dstExtAddress;
uint16_t dstNwkAddress;
uint16_t profileId;
uint16_t clusterId;
uint8_t asduLength;
uint8_t lqi;
int8_t rssi;
stream >> srcPanId;
stream >> srcAddress;
stream >> dstPanId;
stream >> dstAddressMode;
if (dstAddressMode == 0x03)
{
stream >> dstExtAddress;
}
else
{
stream >> dstNwkAddress;
}
stream >> profileId;
stream >> clusterId;
stream >> asduLength;
QByteArray asdu;
for (uint i = 0; i < asduLength; i++)
{
uint8_t byte;
stream >> byte;
asdu.append(byte);
}
stream >> lqi;
stream >> rssi;
if ((profileId == ZLL_PROFILE_ID) && (clusterId == 0x1000) && (asdu.size() >= 3))
{
uint8_t cmd = asdu[2];
ScanResponse scanResponse;
if (cmd == TL_CMD_SCAN_RSP)
{
scanResponse.id = QString::number(touchlinkScanResponses.size() + 1);
scanResponse.address.setExt(srcAddress);
scanResponse.factoryNew = ((asdu[9] & FACTORY_NEW_FLAG) != 0);
scanResponse.channel = touchlinkChannel;
scanResponse.panid = srcPanId;
scanResponse.transactionId = touchlinkReq.transactionId();
scanResponse.rssi = rssi;
DBG_Printf(DBG_TLINK, "scan response " FMT_MAC ", fn=%u, channel=%u rssi=%d TrId=0x%08X in state=%d action=%d\n",
(unsigned long long)scanResponse.address.ext(),
scanResponse.factoryNew, touchlinkChannel, rssi, scanResponse.transactionId,
touchlinkState, touchlinkAction);
if (touchlinkAction == TouchlinkScan)
{
if (asdu.size() >= 9)
{
std::vector<ScanResponse>::iterator i = touchlinkScanResponses.begin();
std::vector<ScanResponse>::iterator end = touchlinkScanResponses.end();
for (; i != end; ++i)
{
if (i->address.ext() == srcAddress)
{
i->transactionId = touchlinkReq.transactionId();
return;
}
}
touchlinkScanResponses.push_back(scanResponse);
}
}
else if (touchlinkAction == TouchlinkReset)
{
if (scanResponse.address.ext() == touchlinkDevice.address.ext())
{
touchlinkTimer->stop();
sendTouchlinkResetRequest();
}
}
else if (touchlinkAction == TouchlinkIdentify)
{
if (scanResponse.address.ext() == touchlinkDevice.address.ext())
{
touchlinkTimer->stop();
sendTouchlinkIdentifyRequest();
}
}
}
}
}
\param delay - the delay after which reconnecting shall be started
*/
void DeRestPluginPrivate::touchlinkStartReconnectNetwork(int delay)
{
touchlinkState = TL_ReconnectNetwork;
touchlinkNetworkReconnectAttempts = NETWORK_ATTEMPS;
DBG_Printf(DBG_TLINK, "start reconnect to network\n");
touchlinkTimer->stop();
if (delay > 0)
{
touchlinkTimer->start(delay);
}
else
{
touchlinkReconnectNetwork();
}
}
*/
void DeRestPluginPrivate::touchlinkReconnectNetwork()
{
if (touchlinkState != TL_ReconnectNetwork)
{
return;
}
if (isInNetwork())
{
touchlinkState = TL_Idle;
DBG_Printf(DBG_TLINK, "reconnect network done\n");
return;
}
if (!touchlinkNetworkConnectedBefore)
{
touchlinkState = TL_Idle;
DBG_Printf(DBG_TLINK, "network was not connected before\n");
return;
}
if (touchlinkNetworkReconnectAttempts > 0)
{
if (apsCtrl->networkState() != deCONZ::Connecting)
{
touchlinkNetworkReconnectAttempts--;
if (apsCtrl->setNetworkState(deCONZ::InNetwork) != deCONZ::Success)
{
DBG_Printf(DBG_TLINK, "touchlink failed to reconnect to network try=%d\n", (NETWORK_ATTEMPS - touchlinkNetworkReconnectAttempts));
}
else
{
DBG_Printf(DBG_TLINK, "touchlink try to reconnect to network try=%d\n", (NETWORK_ATTEMPS - touchlinkNetworkReconnectAttempts));
}
}
touchlinkTimer->start(TL_RECONNECT_CHECK_DELAY);
}
else
{
touchlinkState = TL_Idle;
DBG_Printf(DBG_TLINK, "reconnect network failed\n");
}
}
*/
bool DeRestPluginPrivate::isTouchlinkActive()
{
return (touchlinkState != TL_Idle);
}