From 9f53bb6212e2e319e5f86c3ac4a4c1ebe259db0d Mon Sep 17 00:00:00 2001 From: Alex Spataru Date: Wed, 1 Jan 2025 15:57:54 -0500 Subject: [PATCH] Allow sending binary data through actions (@AnnabellePundaky) --- app/qml/ProjectEditor/Views/TableDelegate.qml | 71 +++++++++++ app/src/IO/Console.cpp | 86 +++++++------- app/src/IO/Console.h | 3 +- app/src/JSON/Action.cpp | 13 ++ app/src/JSON/Action.h | 4 + app/src/JSON/ProjectModel.cpp | 111 ++++++++++++------ app/src/JSON/ProjectModel.h | 1 + app/src/UI/Dashboard.cpp | 11 +- 8 files changed, 216 insertions(+), 84 deletions(-) diff --git a/app/qml/ProjectEditor/Views/TableDelegate.qml b/app/qml/ProjectEditor/Views/TableDelegate.qml index c06a9722..38df2525 100644 --- a/app/qml/ProjectEditor/Views/TableDelegate.qml +++ b/app/qml/ProjectEditor/Views/TableDelegate.qml @@ -421,6 +421,77 @@ ColumnLayout { } } + // + // HexTextEdit editor + // + Loader { + Layout.alignment: Qt.AlignVCenter + Layout.minimumWidth: root.valueWidth + Layout.maximumWidth: root.valueWidth + active: model.widgetType === ProjectModel.HexTextField + visible: model.widgetType === ProjectModel.HexTextField + + sourceComponent: TextField { + id: _hexComponent + text: model.editableValue + font: Cpp_Misc_CommonFonts.monoFont + placeholderText: model.placeholderValue + color: Cpp_ThemeManager.colors["table_text"] + + // + // Add space automatically in hex view + // + onTextChanged: { + // Get the current cursor position + const currentCursorPosition = _hexComponent.cursorPosition; + const cursorAtEnd = (currentCursorPosition === _hexComponent.text.length); + + // Format the text + const originalText = _hexComponent.text; + const formattedText = Cpp_IO_Console.formatUserHex(_hexComponent.text); + const isValid = Cpp_IO_Console.validateUserHex(formattedText); + + // Update the text only if it has changed + if (originalText !== formattedText) { + _hexComponent.text = formattedText; + + // Restore the cursor position, adjusting for added spaces + if (!cursorAtEnd) { + // Remove spaces from originalText and formattedText to compare lengths + const cleanedOriginalText = originalText.replace(/ /g, ''); + const cleanedFormattedText = formattedText.replace(/ /g, ''); + + // Calculate the difference in length due to formatting + const lengthDifference = cleanedFormattedText.length - cleanedOriginalText.length; + + // Count spaces before the cursor in both texts + let spacesBeforeCursorOriginal = (originalText.slice(0, currentCursorPosition).match(/ /g) || []).length; + let spacesBeforeCursorFormatted = (formattedText.slice(0, currentCursorPosition).match(/ /g) || []).length; + + // Calculate adjustment factor + const adjustment = spacesBeforeCursorFormatted - spacesBeforeCursorOriginal + lengthDifference; + + // Restore the cursor position with adjustment + _hexComponent.cursorPosition = Math.min(currentCursorPosition + adjustment, send.text.length); + } + } + + // Update the palette based on validation + _hexComponent.color = isValid + ? Cpp_ThemeManager.colors["table_text"] + : Cpp_ThemeManager.colors["alarm"] + + // Set model data + root.modelPointer.setData( + view.index(row, column), + formattedText, + ProjectModel.EditableValue) + } + + background: Item {} + } + } + // // Separator + Spacer // diff --git a/app/src/IO/Console.cpp b/app/src/IO/Console.cpp index 15a27353..f85f820e 100644 --- a/app/src/IO/Console.cpp +++ b/app/src/IO/Console.cpp @@ -270,6 +270,49 @@ QString IO::Console::formatUserHex(const QString &text) return str; } +/** + * Converts the given @a data in HEX format into real binary data. + */ +QByteArray IO::Console::hexToBytes(const QString &data) +{ + // Remove spaces from the input data + QString withoutSpaces = data; + withoutSpaces.replace(QStringLiteral(" "), ""); + + // Check if the length of the string is even + if (withoutSpaces.length() % 2 != 0) + { + qWarning() << data << "is not a valid hexadecimal array"; + return QByteArray(); + } + + // Iterate over the string in steps of 2 + bool ok; + QByteArray array; + for (int i = 0; i < withoutSpaces.length(); i += 2) + { + // Get two characters (a hex pair) + auto chr1 = withoutSpaces.at(i); + auto chr2 = withoutSpaces.at(i + 1); + + // Convert the hex pair into a byte + QString byteStr = QStringLiteral("%1%2").arg(chr1, chr2); + int byte = byteStr.toInt(&ok, 16); + + // If the conversion fails, return an empty array + if (!ok) + { + qWarning() << data << "is not a valid hexadecimal array"; + return QByteArray(); + } + + // Append the byte to the result array + array.append(static_cast(byte)); + } + + return array; +} + /** * Allows the user to export the information displayed on the console */ @@ -607,49 +650,6 @@ void IO::Console::addToHistory(const QString &command) Q_EMIT historyItemChanged(); } -/** - * Converts the given @a data in HEX format into real binary data. - */ -QByteArray IO::Console::hexToBytes(const QString &data) -{ - // Remove spaces from the input data - QString withoutSpaces = data; - withoutSpaces.replace(QStringLiteral(" "), ""); - - // Check if the length of the string is even - if (withoutSpaces.length() % 2 != 0) - { - qWarning() << data << "is not a valid hexadecimal array"; - return QByteArray(); - } - - // Iterate over the string in steps of 2 - bool ok; - QByteArray array; - for (int i = 0; i < withoutSpaces.length(); i += 2) - { - // Get two characters (a hex pair) - auto chr1 = withoutSpaces.at(i); - auto chr2 = withoutSpaces.at(i + 1); - - // Convert the hex pair into a byte - QString byteStr = QStringLiteral("%1%2").arg(chr1, chr2); - int byte = byteStr.toInt(&ok, 16); - - // If the conversion fails, return an empty array - if (!ok) - { - qWarning() << data << "is not a valid hexadecimal array"; - return QByteArray(); - } - - // Append the byte to the result array - array.append(static_cast(byte)); - } - - return array; -} - /** * Converts the given @a data to a string according to the console display mode * set by the user. diff --git a/app/src/IO/Console.h b/app/src/IO/Console.h index 47e60abd..55588939 100644 --- a/app/src/IO/Console.h +++ b/app/src/IO/Console.h @@ -138,6 +138,8 @@ public: Q_INVOKABLE bool validateUserHex(const QString &text); Q_INVOKABLE QString formatUserHex(const QString &text); + static QByteArray hexToBytes(const QString &data); + public slots: void save(); void clear(); @@ -159,7 +161,6 @@ private slots: void onDataReceived(const QByteArray &data); private: - QByteArray hexToBytes(const QString &data); QString dataToString(const QByteArray &data); QString plainTextStr(const QByteArray &data); QString hexadecimalStr(const QByteArray &data); diff --git a/app/src/JSON/Action.cpp b/app/src/JSON/Action.cpp index 82a321eb..46147399 100644 --- a/app/src/JSON/Action.cpp +++ b/app/src/JSON/Action.cpp @@ -55,6 +55,7 @@ static QVariant SAFE_READ(const QJsonObject &object, const QString &key, */ JSON::Action::Action(const int actionId) : m_actionId(actionId) + , m_binaryData(false) , m_icon("Play Property") , m_title("") , m_txData("") @@ -72,6 +73,16 @@ int JSON::Action::actionId() const return m_actionId; } +/** + * @brief Checks if the user wants to send binary data to the connected device. + * + * @return @c true if binary data encoding is enabled, @c false otherwise. + */ +bool JSON::Action::binaryData() const +{ + return m_binaryData; +} + /** * @brief Gets the icon associated with the action. * @@ -126,6 +137,7 @@ QJsonObject JSON::Action::serialize() const object.insert(QStringLiteral("icon"), m_icon); object.insert(QStringLiteral("txData"), m_txData); object.insert(QStringLiteral("eol"), m_eolSequence); + object.insert(QStringLiteral("binary"), m_binaryData); object.insert(QStringLiteral("title"), m_title.simplified()); return object; } @@ -147,6 +159,7 @@ bool JSON::Action::read(const QJsonObject &object) { if (!object.isEmpty()) { + m_binaryData = SAFE_READ(object, "binary", false).toBool(); m_txData = SAFE_READ(object, "txData", "").toString(); m_eolSequence = SAFE_READ(object, "eol", "").toString(); m_icon = SAFE_READ(object, "icon", "").toString().simplified(); diff --git a/app/src/JSON/Action.h b/app/src/JSON/Action.h index f7e614be..b023d021 100644 --- a/app/src/JSON/Action.h +++ b/app/src/JSON/Action.h @@ -49,6 +49,8 @@ public: Action(const int actionId = -1); [[nodiscard]] int actionId() const; + [[nodiscard]] bool binaryData() const; + [[nodiscard]] const QString &icon() const; [[nodiscard]] const QString &title() const; [[nodiscard]] const QString &txData() const; @@ -59,6 +61,8 @@ public: private: int m_actionId; + bool m_binaryData; + QString m_icon; QString m_title; QString m_txData; diff --git a/app/src/JSON/ProjectModel.cpp b/app/src/JSON/ProjectModel.cpp index fb5a057b..d3dd8764 100644 --- a/app/src/JSON/ProjectModel.cpp +++ b/app/src/JSON/ProjectModel.cpp @@ -95,10 +95,11 @@ typedef enum // clang-format off typedef enum { - kActionView_Title, /**< Represents the action title item. */ - kActionView_Icon, /**< Represents the icon item. */ - kActionView_EOL, /**< Represents the EOL (end of line) item. */ - kActionView_Data /**< Represents the TX data item. */ + kActionView_Title, /**< Represents the action title item. */ + kActionView_Icon, /**< Represents the icon item. */ + kActionView_EOL, /**< Represents the EOL (end of line) item. */ + kActionView_Data, /**< Represents the TX data item. */ + kActionView_Binary, /**< Represents the binary data transmision status */ } ActionItem; // clang-format on @@ -2267,45 +2268,75 @@ void JSON::ProjectModel::buildActionModel(const JSON::Action &action) icon->setData(tr("Icon to display in the dashboard"), ParameterDescription); m_actionModel->appendRow(icon); - // Add action data - auto data = new QStandardItem(); - data->setEditable(true); - data->setData(TextField, WidgetType); - data->setData(action.txData(), EditableValue); - data->setData(tr("TX Data"), ParameterName); - data->setData(kActionView_Data, ParameterType); - data->setData(tr("Command"), PlaceholderValue); - data->setData(tr("Data to transmit when the action is triggered."), - ParameterDescription); - m_actionModel->appendRow(data); + // Add binary selector checkbox + auto binaryData = new QStandardItem(); + binaryData->setEditable(true); + binaryData->setData(CheckBox, WidgetType); + binaryData->setData(action.binaryData(), EditableValue); + binaryData->setData(tr("Binary Data"), ParameterName); + binaryData->setData(kActionView_Binary, ParameterType); + binaryData->setData(0, PlaceholderValue); + binaryData->setData(tr("Send binary data when the action is triggered."), + ParameterDescription); + m_actionModel->appendRow(binaryData); - // Get appropiate end of line index for current action - int eolIndex = 0; - bool found = false; - for (auto it = m_eolSequences.begin(); it != m_eolSequences.end(); - ++it, ++eolIndex) + // Add binary action data + if (action.binaryData()) { - if (it.key() == action.eolSequence()) - { - found = true; - break; - } + auto data = new QStandardItem(); + data->setEditable(true); + data->setData(HexTextField, WidgetType); + data->setData(action.txData(), EditableValue); + data->setData(tr("TX Data (Hex)"), ParameterName); + data->setData(kActionView_Data, ParameterType); + data->setData(tr("Command"), PlaceholderValue); + data->setData(tr("Data to transmit when the action is triggered."), + ParameterDescription); + m_actionModel->appendRow(data); } - // If not found, reset the index to 0 - if (!found) - eolIndex = 0; + // Add action data (non-binary) + else + { + auto data = new QStandardItem(); + data->setEditable(true); + data->setData(TextField, WidgetType); + data->setData(action.txData(), EditableValue); + data->setData(tr("TX Data"), ParameterName); + data->setData(kActionView_Data, ParameterType); + data->setData(tr("Command"), PlaceholderValue); + data->setData(tr("Data to transmit when the action is triggered."), + ParameterDescription); + m_actionModel->appendRow(data); - // Add EOL combobox - auto eol = new QStandardItem(); - eol->setEditable(true); - eol->setData(ComboBox, WidgetType); - eol->setData(m_eolSequences.values(), ComboBoxData); - eol->setData(eolIndex, EditableValue); - eol->setData(tr("EOL Sequence"), ParameterName); - eol->setData(kActionView_EOL, ParameterType); - eol->setData(tr("End-of-line (EOL) sequence to use"), ParameterDescription); - m_actionModel->appendRow(eol); + // Get appropiate end of line index for current action + int eolIndex = 0; + bool found = false; + for (auto it = m_eolSequences.begin(); it != m_eolSequences.end(); + ++it, ++eolIndex) + { + if (it.key() == action.eolSequence()) + { + found = true; + break; + } + } + + // If not found, reset the index to 0 + if (!found) + eolIndex = 0; + + // Add EOL combobox + auto eol = new QStandardItem(); + eol->setEditable(true); + eol->setData(ComboBox, WidgetType); + eol->setData(m_eolSequences.values(), ComboBoxData); + eol->setData(eolIndex, EditableValue); + eol->setData(tr("EOL Sequence"), ParameterName); + eol->setData(kActionView_EOL, ParameterType); + eol->setData(tr("End-of-line (EOL) sequence to use"), ParameterDescription); + m_actionModel->appendRow(eol); + } // Handle edits connect(m_actionModel, &CustomModel::itemChanged, this, @@ -2909,6 +2940,10 @@ void JSON::ProjectModel::onActionItemChanged(QStandardItem *item) m_selectedAction.m_icon = value.toString(); Q_EMIT actionModelChanged(); break; + case kActionView_Binary: + m_selectedAction.m_binaryData = value.toBool(); + buildActionModel(m_selectedAction); + break; default: break; } diff --git a/app/src/JSON/ProjectModel.h b/app/src/JSON/ProjectModel.h index 36b99c24..094ac4d5 100644 --- a/app/src/JSON/ProjectModel.h +++ b/app/src/JSON/ProjectModel.h @@ -181,6 +181,7 @@ public: enum EditorWidget { TextField, + HexTextField, IntField, FloatField, CheckBox, diff --git a/app/src/UI/Dashboard.cpp b/app/src/UI/Dashboard.cpp index fb9c3310..25dcd8fc 100644 --- a/app/src/UI/Dashboard.cpp +++ b/app/src/UI/Dashboard.cpp @@ -23,6 +23,7 @@ #include "SIMD/SIMD.h" #include "IO/Manager.h" +#include "IO/Console.h" #include "CSV/Player.h" #include "MQTT/Client.h" #include "Misc/TimerEvents.h" @@ -568,8 +569,14 @@ void UI::Dashboard::activateAction(const int index) if (index >= 0 && index < m_actions.count()) { const auto &action = m_actions[index]; - const auto &data = action.txData() + action.eolSequence(); - IO::Manager::instance().writeData(data.toUtf8()); + + QByteArray bin; + if (action.binaryData()) + bin = IO::Console::hexToBytes(action.txData()); + else + bin = QString(action.txData() + action.eolSequence()).toUtf8(); + + IO::Manager::instance().writeData(bin); } }