diff --git a/include/utils/QStringUtils.h b/include/utils/QStringUtils.h index 52d245c0..6b302d0a 100644 --- a/include/utils/QStringUtils.h +++ b/include/utils/QStringUtils.h @@ -3,6 +3,8 @@ #include #include +#include +#include namespace QStringUtils { @@ -13,11 +15,11 @@ enum class SplitBehavior { inline QStringList split (const QString &string, const QString &sep, SplitBehavior behavior = SplitBehavior::KeepEmptyParts, Qt::CaseSensitivity cs = Qt::CaseSensitive) { - #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) +#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) return behavior == SplitBehavior::SkipEmptyParts ? string.split(sep, Qt::SkipEmptyParts , cs) : string.split(sep, Qt::KeepEmptyParts , cs); - #else +#else return behavior == SplitBehavior::SkipEmptyParts ? string.split(sep, QString::SkipEmptyParts , cs) : string.split(sep, QString::KeepEmptyParts , cs); - #endif +#endif } inline QStringList split (const QString &string, QChar sep, SplitBehavior behavior = SplitBehavior::KeepEmptyParts, Qt::CaseSensitivity cs = Qt::CaseSensitive) @@ -37,6 +39,34 @@ inline QStringList split (const QString &string, const QRegExp &rx, SplitBehavio return behavior == SplitBehavior::SkipEmptyParts ? string.split(rx, QString::SkipEmptyParts) : string.split(rx, QString::KeepEmptyParts); #endif } + +inline QVector splitRef(const QString &string, const QString &sep, SplitBehavior behavior = SplitBehavior::KeepEmptyParts, Qt::CaseSensitivity cs = Qt::CaseSensitive) +{ +#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) + return string.splitRef(sep, behavior == SplitBehavior::SkipEmptyParts ? Qt::SkipEmptyParts : Qt::KeepEmptyParts , cs); +#else + return string.splitRef(sep, behavior == SplitBehavior::SkipEmptyParts ? QString::SkipEmptyParts : QString::KeepEmptyParts, cs); +#endif +} + +inline QVector splitRef(const QString &string, QChar sep, SplitBehavior behavior = SplitBehavior::KeepEmptyParts, Qt::CaseSensitivity cs = Qt::CaseSensitive) +{ +#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) + return string.splitRef(sep, behavior == SplitBehavior::SkipEmptyParts ? Qt::SkipEmptyParts : Qt::KeepEmptyParts, cs); +#else + return string.splitRef(sep, behavior == SplitBehavior::SkipEmptyParts ? QString::SkipEmptyParts : QString::KeepEmptyParts, cs); +#endif +} + +inline QVector splitRef(const QString &string, const QRegExp &rx, SplitBehavior behavior = SplitBehavior::KeepEmptyParts) +{ +#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) + return string.splitRef(rx,behavior == SplitBehavior::SkipEmptyParts ? Qt::SkipEmptyParts : Qt::KeepEmptyParts); +#else + return string.splitRef(rx, behavior == SplitBehavior::SkipEmptyParts ? QString::SkipEmptyParts : QString::KeepEmptyParts); +#endif +} + } #endif // QSTRINGUTILS_H diff --git a/libsrc/boblightserver/BoblightClientConnection.cpp b/libsrc/boblightserver/BoblightClientConnection.cpp index 485f2b52..e50a6dc9 100644 --- a/libsrc/boblightserver/BoblightClientConnection.cpp +++ b/libsrc/boblightserver/BoblightClientConnection.cpp @@ -3,11 +3,13 @@ #include #include #include +#include // stl includes #include #include #include +#include // Qt includes #include @@ -54,18 +56,19 @@ BoblightClientConnection::~BoblightClientConnection() void BoblightClientConnection::readData() { - _receiveBuffer += _socket->readAll(); + _receiveBuffer.append(_socket->readAll()); int bytes = _receiveBuffer.indexOf('\n') + 1; while(bytes > 0) { // create message string (strip the newline) - QString message = QString::fromLatin1(_receiveBuffer.data(), bytes-1); - // remove message data from buffer - _receiveBuffer = _receiveBuffer.mid(bytes); + const QString message = readMessage(_receiveBuffer.data(), bytes); // handle trimmed message - handleMessage(message.trimmed()); + handleMessage(message); + + // remove message data from buffer + _receiveBuffer.remove(0, bytes); // drop messages if the buffer is too full if (_receiveBuffer.size() > 100*1024) @@ -79,6 +82,31 @@ void BoblightClientConnection::readData() } } +QString BoblightClientConnection::readMessage(const char *data, const size_t size) const +{ + char *end = (char *)data + size - 1; + + // Trim left + while (data < end && std::isspace(*data)) + { + ++data; + } + + // Trim right + while (end > data && std::isspace(*end)) + { + --end; + } + + // create message string (strip the newline) + const int len = end - data + 1; + const QString message = QString::fromLatin1(data, len); + + //std::cout << bytes << ": \"" << message.toUtf8().constData() << "\"" << std::endl; + + return message; +} + void BoblightClientConnection::socketClosed() { // clear the current channel @@ -88,10 +116,11 @@ void BoblightClientConnection::socketClosed() emit connectionClosed(this); } + void BoblightClientConnection::handleMessage(const QString & message) { //std::cout << "boblight message: " << message.toStdString() << std::endl; - QStringList messageParts = QStringUtils::split(message," ",QStringUtils::SplitBehavior::SkipEmptyParts); + const QVector messageParts = QStringUtils::splitRef(message, ' ', QStringUtils::SplitBehavior::SkipEmptyParts); if (messageParts.size() > 0) { if (messageParts[0] == "hello") @@ -122,32 +151,18 @@ void BoblightClientConnection::handleMessage(const QString & message) if (messageParts.size() > 3 && messageParts[1] == "light") { bool rc; - unsigned ledIndex = messageParts[2].toUInt(&rc); + const unsigned ledIndex = parseUInt(messageParts[2], &rc); if (rc && ledIndex < _ledColors.size()) { if (messageParts[3] == "rgb" && messageParts.size() == 7) { - // replace decimal comma with decimal point - messageParts[4].replace(',', '.'); - messageParts[5].replace(',', '.'); - messageParts[6].replace(',', '.'); + // custom parseByte accepts both ',' and '.' as decimal separator + // no need to replace decimal comma with decimal point bool rc1, rc2, rc3; - uint8_t red = qMax(0, qMin(255, int(255 * messageParts[4].toFloat(&rc1)))); - - // check for correct locale should not be needed anymore - please check! - if (!rc1) - { - // maybe a locale issue. switch to a locale with a comma instead of a dot as decimal seperator (or vice versa) - _locale = QLocale((_locale.decimalPoint() == QChar('.')) ? QLocale::Dutch : QLocale::C); - _locale.setNumberOptions(QLocale::OmitGroupSeparator | QLocale::RejectGroupSeparator); - - // try again - red = qMax(0, qMin(255, int(255 * messageParts[4].toFloat(&rc1)))); - } - - uint8_t green = qMax(0, qMin(255, int(255 * messageParts[5].toFloat(&rc2)))); - uint8_t blue = qMax(0, qMin(255, int(255 * messageParts[6].toFloat(&rc3)))); + const uint8_t red = parseByte(messageParts[4], &rc1); + const uint8_t green = parseByte(messageParts[5], &rc2); + const uint8_t blue = parseByte(messageParts[6], &rc3); if (rc1 && rc2 && rc3) { @@ -181,7 +196,7 @@ void BoblightClientConnection::handleMessage(const QString & message) else if (messageParts.size() == 3 && messageParts[1] == "priority") { bool rc; - int prio = messageParts[2].toInt(&rc); + const int prio = static_cast(parseUInt(messageParts[2], &rc)); if (rc && prio != _priority) { if (_priority != 0 && _hyperion->getPriorityInfo(_priority).componentId == hyperion::COMP_BOBLIGHTSERVER) @@ -223,6 +238,146 @@ void BoblightClientConnection::handleMessage(const QString & message) Debug(_log, "unknown boblight message: %s", QSTRING_CSTR(message)); } +/// Float values 10 to the power of -p for p in 0 .. 8. +const float ipows[] = { + 1, + 1.0f / 10.0f, + 1.0f / 100.0f, + 1.0f / 1000.0f, + 1.0f / 10000.0f, + 1.0f / 100000.0f, + 1.0f / 1000000.0f, + 1.0f / 10000000.0f, + 1.0f / 100000000.0f}; + +float BoblightClientConnection::parseFloat(const QStringRef& s, bool *ok) const +{ + // We parse radix 10 + const char MIN_DIGIT = '0'; + const char MAX_DIGIT = '9'; + const char SEP_POINT = '.'; + const char SEP_COMMA = ','; + const int NUM_POWS = 9; + + /// The maximum number of characters we want to process + const int MAX_LEN = 18; // Chosen randomly + + /// The index of the current character + int q = 0; + + /// The integer part of the number + int64_t n = 0; + + auto it = s.begin(); + +#define STEP ((it != s.end()) && (q++ < MAX_LEN)) + + // parse the integer-part + while (it->unicode() >= MIN_DIGIT && it->unicode() <= MAX_DIGIT && STEP) + { + n = (n * 10) + (it->unicode() - MIN_DIGIT); + ++it; + } + + /// The resulting float value + float f = static_cast(n); + + // parse decimal part + if ((it->unicode() == SEP_POINT || it->unicode() == SEP_COMMA) && STEP) + { + /// The decimal part of the number + int64_t d = 0; + + /// The exponent for the scale-factor 10 to the power -e + int e = 0; + + ++it; + while (it->unicode() >= MIN_DIGIT && it->unicode() <= MAX_DIGIT && STEP) + { + d = (d * 10) + (it->unicode() - MIN_DIGIT); + ++e; + ++it; + } + + const float h = static_cast(d); + + // We want to use pre-calculated power whenever possible + if (e < NUM_POWS) + { + f += h * ipows[e]; + } + else + { + f += h / std::pow(10.0f, e); + } + } + + if (q >= MAX_LEN || q < s.length()) + { + if (ok) + { + //std::cout << "FAIL L " << q << ": " << s.toUtf8().constData() << std::endl; + *ok = false; + } + return 0; + } + + if (ok) + { + //std::cout << "OK " << d << ": " << s.toUtf8().constData() << std::endl; + *ok = true; + } + + return f; +} + +unsigned BoblightClientConnection::parseUInt(const QStringRef& s, bool *ok) const +{ + // We parse radix 10 + const char MIN_DIGIT = '0'; + const char MAX_DIGIT = '9'; + + /// The maximum number of characters we want to process + const int MAX_LEN = 10; + + /// The index of the current character + int q = 0; + + /// The integer part of the number + int n = 0; + + auto it = s.begin(); + + // parse the integer-part + while (it->unicode() >= MIN_DIGIT && it->unicode() <= MAX_DIGIT && ((it != s.end()) && (q++ < MAX_LEN))) + { + n = (n * 10) + (it->unicode() - MIN_DIGIT); + ++it; + } + + if (ok) + { + *ok = !(q >= MAX_LEN || q < s.length()); + } + + return n; +} + +uint8_t BoblightClientConnection::parseByte(const QStringRef& s, bool *ok) const +{ + const int LO = 0; + const int HI = 255; + +#if defined(FAST_FLOAT_PARSE) + const float d = parseFloat(s, ok); +#else + const float d = s.toFloat(ok); +#endif + + // Clamp to byte range 0 to 255 + return static_cast(qBound(LO, int(HI * d), HI)); // qBound args are in order min, value, max; see: https://doc.qt.io/qt-5/qtglobal.html#qBound +} + void BoblightClientConnection::sendLightMessage() { char buffer[256]; diff --git a/libsrc/boblightserver/BoblightClientConnection.h b/libsrc/boblightserver/BoblightClientConnection.h index 54b89efc..46a3b0eb 100644 --- a/libsrc/boblightserver/BoblightClientConnection.h +++ b/libsrc/boblightserver/BoblightClientConnection.h @@ -4,11 +4,15 @@ #include #include #include +#include // utils includes #include #include +/// Whether to parse floats with an eye on performance +#define FAST_FLOAT_PARSE + class ImageProcessor; class Hyperion; @@ -70,6 +74,42 @@ private: /// void sendLightMessage(); + /// + /// Interpret the float value "0.0" to "1.0" of the QString byte values 0 .. 255 + /// + /// @param s the string to parse + /// @param ok whether the result is ok + /// @return the parsed byte value in range 0 to 255, or 0 + /// + uint8_t parseByte(const QStringRef& s, bool *ok = nullptr) const; + + /// + /// Parse the given QString as unsigned int value. + /// + /// @param s the string to parse + /// @param ok whether the result is ok + /// @return the parsed unsigned int value + /// + unsigned parseUInt(const QStringRef& s, bool *ok = nullptr) const; + + /// + /// Parse the given QString as float value, e.g. the 16-bit (wide char) QString "1" shall represent 1, "0.5" is 0.5 and so on. + /// + /// @param s the string to parse + /// @param ok whether the result is ok + /// @return the parsed float value, or 0 + /// + float parseFloat(const QStringRef& s, bool *ok = nullptr) const; + + /// + /// Read an incoming boblight message as QString + /// + /// @param data the char data buffer of the incoming message + /// @param size the length of the buffer buffer + /// @returns the incoming boblight message as QString + /// + QString readMessage(const char *data, const size_t size) const; + private: /// Locale used for parsing floating point values QLocale _locale;