#include "LedDeviceCololight.h" #include #include #include #include #include #include // Constants namespace { const bool verbose = false; const bool verbose3 = false; // Configuration settings const char CONFIG_HW_LED_COUNT[] = "hardwareLedCount"; const int COLOLIGHT_BEADS_PER_MODULE = 19; // Cololight discovery service const int API_DEFAULT_PORT = 8900; const char DISCOVERY_ADDRESS[] = "255.255.255.255"; const quint16 DISCOVERY_PORT = 12345; const char DISCOVERY_MESSAGE[] = "Z-SEARCH * \r\n"; constexpr std::chrono::milliseconds DEFAULT_DISCOVERY_TIMEOUT{ 2000 }; constexpr std::chrono::milliseconds DEFAULT_READ_TIMEOUT{ 1000 }; constexpr std::chrono::milliseconds DEFAULT_IDENTIFY_TIME{ 2000 }; const char COLOLIGHT_MODEL[] = "mod"; const char COLOLIGHT_MODEL_TYPE[] = "subkey"; const char COLOLIGHT_MAC[] = "sn"; const char COLOLIGHT_NAME[] = "name"; const char COLOLIGHT_MODEL_IDENTIFIER[] = "OD_WE_QUAN"; } //End of constants LedDeviceCololight::LedDeviceCololight(const QJsonObject& deviceConfig) : ProviderUdp(deviceConfig) , _modelType(-1) , _ledLayoutType(-1) , _ledBeadCount(0) , _distance(0) , _sequenceNumber(1) { _packetFixPart.append(reinterpret_cast(PACKET_HEADER), sizeof(PACKET_HEADER)); _packetFixPart.append(reinterpret_cast(PACKET_SECU), sizeof(PACKET_SECU)); } LedDevice* LedDeviceCololight::construct(const QJsonObject& deviceConfig) { return new LedDeviceCololight(deviceConfig); } bool LedDeviceCololight::init(const QJsonObject& deviceConfig) { bool isInitOK = false; _port = API_DEFAULT_PORT; if (ProviderUdp::init(deviceConfig)) { // Initialise LedDevice configuration and execution environment Debug(_log, "DeviceType : %s", QSTRING_CSTR(this->getActiveDeviceType())); Debug(_log, "ColorOrder : %s", QSTRING_CSTR(this->getColorOrder())); Debug(_log, "LatchTime : %d", this->getLatchTime()); if (initLedsConfiguration()) { initDirectColorCmdTemplate(); isInitOK = true; } } return isInitOK; } bool LedDeviceCololight::initLedsConfiguration() { bool isInitOK = false; if (!getInfo()) { QString errorReason = QString("Cololight device (%1) not accessible to get additional properties!") .arg(getAddress().toString()); setInError(errorReason); } else { QString modelTypeText; switch (_modelType) { case STRIP: modelTypeText = "Strip"; _ledLayoutType = STRIP_LAYOUT; break; case PLUS: _ledLayoutType = MODLUE_LAYOUT; modelTypeText = "Plus"; break; default: _modelType = STRIP; modelTypeText = "Strip"; _ledLayoutType = STRIP_LAYOUT; Info(_log, "Model not identified, assuming Cololight %s", QSTRING_CSTR(modelTypeText)); break; } Debug(_log, "Model type : %s", QSTRING_CSTR(modelTypeText)); if (getLedCount() == 0) { setLedCount(_devConfig[CONFIG_HW_LED_COUNT].toInt(0)); } Debug(_log, "LedCount : %d", getLedCount()); int configuredLedCount = _devConfig["currentLedCount"].toInt(1); if (getLedCount() < configuredLedCount) { QString errorReason = QString("Not enough LEDs [%1] for configured LEDs in layout [%2] found!") .arg(getLedCount()) .arg(configuredLedCount); this->setInError(errorReason); } else { if (getLedCount() > configuredLedCount) { Info(_log, "%s: More LEDs [%d] than configured LEDs in layout [%d].", QSTRING_CSTR(this->getActiveDeviceType()), getLedCount(), configuredLedCount); } isInitOK = true; } } return isInitOK; } void LedDeviceCololight::initDirectColorCmdTemplate() { int ledNumber = static_cast(this->getLedCount()); _directColorCommandTemplate.clear(); //Packet _directColorCommandTemplate.append(static_cast(bufferMode::LIGHTBEAD)); // idx int beads = 1; if (_ledLayoutType == MODLUE_LAYOUT) { beads = COLOLIGHT_BEADS_PER_MODULE; } for (int i = 0; i < ledNumber; ++i) { _directColorCommandTemplate.append(static_cast(i * beads + 1)); _directColorCommandTemplate.append(static_cast(i * beads + beads)); _directColorCommandTemplate.append(3, static_cast(0x00)); } } bool LedDeviceCololight::getInfo() { bool isCmdOK = false; QByteArray command; const quint8 packetSize = 2; int fixPartsize = sizeof(TL1_CMD_FIXED_PART); command.resize(sizeof(TL1_CMD_FIXED_PART) + packetSize); command.fill('\0'); command[fixPartsize - 3] = static_cast(SETVAR); // verb command[fixPartsize - 2] = static_cast(_sequenceNumber); // ctag command[fixPartsize - 1] = static_cast(packetSize); // length //Packet command[fixPartsize] = static_cast(READ_INFO_FROM_STORAGE); // idx command[fixPartsize + 1] = static_cast(0x01); // idx if (sendRequest(TL1_CMD, command)) { QByteArray response; if (readResponse(response)) { DebugIf(verbose, _log, "#[0x%x], Data returned: [%s]", _sequenceNumber, QSTRING_CSTR(toHex(response))); quint16 ledNum = qFromBigEndian(response.data() + 1); if (ledNum != 0xFFFF) { _ledBeadCount = ledNum; // Cololight types are not identifyable currently // Work under the assumption that modules (Cololight Plus) have a number of beads and a Colologht Strip does not have a multiple of beads // The assumption will not hold true, if a user cuts the Strip to a multiple of beads... if (ledNum % COLOLIGHT_BEADS_PER_MODULE == 0) { _modelType = PLUS; _ledLayoutType = MODLUE_LAYOUT; _distance = ledNum / COLOLIGHT_BEADS_PER_MODULE; setLedCount(_distance); } else { _modelType = STRIP; _ledLayoutType = STRIP_LAYOUT; _distance = 0; setLedCount(ledNum); } isCmdOK = true; Debug(_log, "#LEDs found [0x%x], [%u], distance [%d]", _ledBeadCount, _ledBeadCount, _distance); } else { _modelType = -1; _ledLayoutType = -1; _distance = 0; setLedCount(0); isCmdOK = false; Error(_log, "Number of LEDs cannot be resolved"); } } } return isCmdOK; } bool LedDeviceCololight::setEffect(const effect effect) { return setColor(static_cast(effect)); } bool LedDeviceCololight::setColor(const ColorRgb colorRgb) { uint32_t color = colorRgb.blue | (colorRgb.green << 8) | (colorRgb.red << 16) | (0x00 << 24); return setColor(color); } bool LedDeviceCololight::setColor(const uint32_t color) { bool isCmdOK = false; QByteArray command; const quint8 packetSize = 6; int fixPartsize = sizeof(TL1_CMD_FIXED_PART); command.resize(sizeof(TL1_CMD_FIXED_PART) + packetSize); command.fill('\0'); command[fixPartsize - 3] = static_cast(SET); // verb command[fixPartsize - 2] = static_cast(_sequenceNumber); // ctag command[fixPartsize - 1] = static_cast(packetSize); // length //Packet command[fixPartsize] = static_cast(0x02); // idx command[fixPartsize + 1] = static_cast(0xff); // set color or dynamic effect qToBigEndian(color, command.data() + fixPartsize + 2); if (sendRequest(TL1_CMD, command)) { QByteArray response; if (readResponse(response)) { DebugIf(verbose, _log, "#[0x%x], Data returned: [%s]", _sequenceNumber, QSTRING_CSTR(toHex(response))); isCmdOK = true; } } return isCmdOK; } bool LedDeviceCololight::setState(bool isOn) { bool isCmdOK = false; quint8 type = isOn ? STATE_ON : STATE_OFF; QByteArray command; const quint8 packetSize = 3; int fixPartsize = sizeof(TL1_CMD_FIXED_PART); command.resize(sizeof(TL1_CMD_FIXED_PART) + packetSize); command.fill('\0'); command[fixPartsize - 3] = static_cast(SET); // verb command[fixPartsize - 2] = static_cast(_sequenceNumber); // ctag command[fixPartsize - 1] = static_cast(packetSize); // length //Packet command[fixPartsize] = static_cast(BRIGTHNESS_CONTROL); // idx command[fixPartsize + 1] = static_cast(type); // type command[fixPartsize + 2] = static_cast(isOn); // value if (sendRequest(TL1_CMD, command)) { QByteArray response; if (readResponse(response)) { DebugIf(verbose, _log, "#[0x%x], Data returned: [%s]", _sequenceNumber, QSTRING_CSTR(toHex(response))); isCmdOK = true; } } return isCmdOK; } bool LedDeviceCololight::setStateDirect(bool isOn) { bool isCmdOK = false; QByteArray command; //Packet command.append(static_cast(0x04)); // idx command.append(static_cast(isOn)); // idx command.append(static_cast(0xd7)); // idx if (sendRequest(DIRECT_CONTROL, command)) { QByteArray response; if (readResponse(response)) { DebugIf(verbose, _log, "#[0x%x], Data returned: [%s]", _sequenceNumber, QSTRING_CSTR(toHex(response))); isCmdOK = true; } } return isCmdOK; } bool LedDeviceCololight::setColor(const std::vector& ledValues) { int ledNumber = static_cast(ledValues.size()); QByteArray command = _directColorCommandTemplate; //Update LED values, start from offset (mode + first start/stop pair) = 3 for (int i = 0; i < ledNumber; ++i) { command[3 + i * 5] = static_cast(ledValues[i].red); command[3 + i * 5 + 1] = static_cast(ledValues[i].green); command[3 + i * 5 + 2] = static_cast(ledValues[i].blue); } bool isCmdOK = sendRequest(DIRECT_CONTROL, command); return isCmdOK; } bool LedDeviceCololight::setTL1CommandMode(bool isOn) { bool isCmdOK = false; quint8 type = isOn ? STATE_ON : STATE_OFF; QByteArray command; const quint8 packetSize = 2; int fixPartsize = sizeof(TL1_CMD_FIXED_PART); command.resize(sizeof(TL1_CMD_FIXED_PART) + packetSize); command.fill('\0'); command[fixPartsize - 3] = static_cast(SETEEPROM); // verb command[fixPartsize - 2] = static_cast(_sequenceNumber); // ctag command[fixPartsize - 1] = static_cast(packetSize); // length //Packet command[fixPartsize] = static_cast(COLOR_CONTROL); // idx command[fixPartsize + 1] = static_cast(type); // type if (sendRequest(TL1_CMD, command)) { QByteArray response; if (readResponse(response)) { DebugIf(verbose, _log, "#[0x%x], Data returned: [%s]", _sequenceNumber, QSTRING_CSTR(toHex(response))); isCmdOK = true; } } return isCmdOK; } bool LedDeviceCololight::sendRequest(const appID appID, const QByteArray& command) { bool isSendOK = true; QByteArray packet(_packetFixPart); packet.append(static_cast(_sequenceNumber)); packet.append(command); quint32 size = static_cast(static_cast(sizeof(PACKET_SECU)) + 1 + command.size()); qToBigEndian(appID, packet.data() + 4); qToBigEndian(size, packet.data() + 6); ++_sequenceNumber; DebugIf(verbose3, _log, "packet: ([0x%x], [%u])[%s]", size, size, QSTRING_CSTR(toHex(packet, 64))); if (writeBytes(packet) < 0) { isSendOK = false; } return isSendOK; } bool LedDeviceCololight::readResponse() { QByteArray response; return readResponse(response); } bool LedDeviceCololight::readResponse(QByteArray& response) { bool isRequestOK = false; if (_udpSocket->waitForReadyRead(DEFAULT_READ_TIMEOUT.count())) { while (_udpSocket->waitForReadyRead(200)) { QByteArray datagram; while (_udpSocket->hasPendingDatagrams()) { datagram.resize(static_cast(_udpSocket->pendingDatagramSize())); QHostAddress senderIP; quint16 senderPort; _udpSocket->readDatagram(datagram.data(), datagram.size(), &senderIP, &senderPort); if (datagram.size() >= 10) { DebugIf(verbose3, _log, "response: [%s]", QSTRING_CSTR(toHex(datagram, 64))); quint16 appID = qFromBigEndian(datagram.mid(4, sizeof(appID))); if (verbose && appID == 0x8000) { QString tagVersion = datagram.left(2); quint32 packetSize = qFromBigEndian(datagram.mid(sizeof(PACKET_HEADER) - sizeof(packetSize))); Debug(_log, "Response HEADER: tagVersion [%s], appID: [0x%.2x][%u], packet size: [0x%.4x][%u]", QSTRING_CSTR(tagVersion), appID, appID, packetSize, packetSize); quint32 dictionary = qFromBigEndian(datagram.mid(sizeof(PACKET_HEADER))); quint32 checkSum = qFromBigEndian(datagram.mid(sizeof(PACKET_HEADER) + sizeof(dictionary))); quint32 salt = qFromBigEndian(datagram.mid(sizeof(PACKET_HEADER) + sizeof(dictionary) + sizeof(checkSum), sizeof(salt))); quint32 sequenceNumber = qFromBigEndian(datagram.mid(sizeof(PACKET_HEADER) + sizeof(dictionary) + sizeof(checkSum) + sizeof(salt))); Debug(_log, "Response SECU : Dict: [0x%.4x][%u], Sum: [0x%.4x][%u], Salt: [0x%.4x][%u], SN: [0x%.4x][%u]", dictionary, dictionary, checkSum, checkSum, salt, salt, sequenceNumber, sequenceNumber); quint8 packetSN = static_cast(datagram.at(sizeof(PACKET_HEADER) + sizeof(PACKET_SECU))); Debug(_log, "Response packSN: [0x%.4x][%u]", packetSN, packetSN); } quint8 errorCode = static_cast(datagram.at(sizeof(PACKET_HEADER) + sizeof(PACKET_SECU) + 1)); int dataPartStart = sizeof(PACKET_HEADER) + sizeof(PACKET_SECU) + sizeof(TL1_CMD_FIXED_PART); if (errorCode != 0) { quint8 originalVerb = static_cast(datagram.at(dataPartStart - 2) - 0x80); quint8 originalRequestPacketSN = static_cast(datagram.at(dataPartStart - 1)); if (errorCode == 16) { //TL1 Command failure Error(_log, "Request [0x%x] failed =with error [%u], appID [%u], originalVerb [0x%x]", originalRequestPacketSN, errorCode, appID, originalVerb); } else { Error(_log, "Request [0x%x] failed with error [%u], appID [%u]", originalRequestPacketSN, errorCode, appID); } } else { // TL1 Protocol if (appID == 0x8000) { if (dataPartStart < datagram.size()) { quint8 dataLength = static_cast(datagram.at(dataPartStart)); response = datagram.mid(dataPartStart + 1, dataLength); if (verbose) { quint8 originalVerb = static_cast(datagram.at(dataPartStart - 2) - 0x80); Debug(_log, "Cmd [0x%x], Data returned: [%s]", originalVerb, QSTRING_CSTR(toHex(response))); } } else { DebugIf(verbose, _log, "No additional data returned"); } } isRequestOK = true; } } } } } return isRequestOK; } int LedDeviceCololight::write(const std::vector& ledValues) { int rc = -1; if (setColor(ledValues)) { rc = 0; } return rc; } bool LedDeviceCololight::powerOn() { bool on = true; if (_isDeviceReady) { if (!setState(false) || !setTL1CommandMode(false)) { on = false; } } return on; } bool LedDeviceCololight::powerOff() { bool off = true; if (_isDeviceReady) { writeBlack(); off = setStateDirect(false); setTL1CommandMode(false); } return off; } QJsonArray LedDeviceCololight::discover() { QUdpSocket udpSocket; udpSocket.writeDatagram(QString(DISCOVERY_MESSAGE).toUtf8(), QHostAddress(DISCOVERY_ADDRESS), DISCOVERY_PORT); if (udpSocket.waitForReadyRead(DEFAULT_DISCOVERY_TIMEOUT.count())) { while (udpSocket.waitForReadyRead(200)) { QByteArray datagram; while (udpSocket.hasPendingDatagrams()) { datagram.resize(static_cast(udpSocket.pendingDatagramSize())); QHostAddress senderIP; quint16 senderPort; udpSocket.readDatagram(datagram.data(), datagram.size(), &senderIP, &senderPort); QString data(datagram); QMap headers; // parse request QStringList entries = QStringUtils::split(data, "\n", QStringUtils::SplitBehavior::SkipEmptyParts); for (auto entry : entries) { // split into key=value, be aware that value field may contain also a "=" entry = entry.simplified(); int pos = entry.indexOf("="); if (pos == -1) { continue; } const QString key = entry.left(pos).trimmed().toLower(); const QString value = entry.mid(pos + 1).trimmed(); headers[key] = value; } if (headers.value("mod") == COLOLIGHT_MODEL_IDENTIFIER) { QString ipAddress = QHostAddress(senderIP.toIPv4Address()).toString(); _services.insert(ipAddress, headers); Debug(_log, "Cololight discovered at [%s]", QSTRING_CSTR(ipAddress)); DebugIf(verbose3, _log, "_data: [%s]", QSTRING_CSTR(data)); } } } } QJsonArray deviceList; QMap>::iterator i; for (i = _services.begin(); i != _services.end(); ++i) { QJsonObject obj; QString ipAddress = i.key(); obj.insert("ip", ipAddress); obj.insert("model", i.value().value(COLOLIGHT_MODEL)); obj.insert("type", i.value().value(COLOLIGHT_MODEL_TYPE)); obj.insert("mac", i.value().value(COLOLIGHT_MAC)); obj.insert("name", i.value().value(COLOLIGHT_NAME)); QHostInfo hostInfo = QHostInfo::fromName(i.key()); if (hostInfo.error() == QHostInfo::NoError) { QString hostname = hostInfo.hostName(); if (!QHostInfo::localDomainName().isEmpty()) { obj.insert("hostname", hostname.remove("." + QHostInfo::localDomainName())); obj.insert("domain", QHostInfo::localDomainName()); } else { if (hostname.startsWith(ipAddress)) { obj.insert("hostname", ipAddress); QString domain = hostname.remove(ipAddress); if (domain.at(0) == '.') { domain.remove(0, 1); } obj.insert("domain", domain); } else { int domainPos = hostname.indexOf('.'); obj.insert("hostname", hostname.left(domainPos)); obj.insert("domain", hostname.mid(domainPos + 1)); } } } deviceList << obj; } return deviceList; } QJsonObject LedDeviceCololight::discover(const QJsonObject& /*params*/) { QJsonObject devicesDiscovered; devicesDiscovered.insert("ledDeviceType", _activeDeviceType); QString discoveryMethod("ssdp"); QJsonArray deviceList; deviceList = discover(); devicesDiscovered.insert("discoveryMethod", discoveryMethod); devicesDiscovered.insert("devices", deviceList); //Debug(_log, "devicesDiscovered: [%s]", QString(QJsonDocument(devicesDiscovered).toJson(QJsonDocument::Compact)).toUtf8().constData()); return devicesDiscovered; } QJsonObject LedDeviceCololight::getProperties(const QJsonObject& params) { DebugIf(verbose, _log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData()); QJsonObject properties; QString apiHostname = params["host"].toString(""); quint16 apiPort = static_cast(params["port"].toInt(API_DEFAULT_PORT)); QJsonObject propertiesDetails; if (!apiHostname.isEmpty()) { QJsonObject deviceConfig; deviceConfig.insert("host", apiHostname); deviceConfig.insert("port", apiPort); if (ProviderUdp::init(deviceConfig)) { if (getInfo()) { QString modelTypeText; switch (_modelType) { case STRIP: modelTypeText = "Strip"; break; case PLUS: modelTypeText = "Plus"; break; default: modelTypeText = "Strip"; break; } propertiesDetails.insert("modelType", modelTypeText); propertiesDetails.insert("ledCount", static_cast(getLedCount())); propertiesDetails.insert("ledBeadCount", _ledBeadCount); propertiesDetails.insert("distance", _distance); } } } properties.insert("properties", propertiesDetails); DebugIf(verbose, _log, "properties: [%s]", QString(QJsonDocument(properties).toJson(QJsonDocument::Compact)).toUtf8().constData()); return properties; } void LedDeviceCololight::identify(const QJsonObject& params) { DebugIf(verbose, _log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData()); QString apiHostname = params["host"].toString(""); quint16 apiPort = static_cast(params["port"].toInt(API_DEFAULT_PORT)); if (!apiHostname.isEmpty()) { QJsonObject deviceConfig; deviceConfig.insert("host", apiHostname); deviceConfig.insert("port", apiPort); if (ProviderUdp::init(deviceConfig)) { if (setStateDirect(false) && setState(true)) { setEffect(THE_CIRCUS); QEventLoop loop; QTimer::singleShot(DEFAULT_IDENTIFY_TIME.count(), &loop, &QEventLoop::quit); loop.exec(); setColor(ColorRgb::BLACK); } } } }