From 57be3764903a68f31091cb24c48acddb0c0756e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Micha=C3=ABl=20Celerier?= Date: Mon, 30 Dec 2024 23:48:11 -0500 Subject: [PATCH] bitfocus: implement more of the API, show UI elements in the device settings and send the first osc message --- .../score-plugin-protocols/CMakeLists.txt | 6 +- .../Protocols/Bitfocus/BitfocusContext.cpp | 459 ++++++++++++++++ .../Protocols/Bitfocus/BitfocusContext.hpp | 496 ++++-------------- .../Bitfocus/BitfocusContext.unix.cpp | 78 +++ .../Bitfocus/BitfocusContext.win32.cpp | 24 + .../Protocols/Bitfocus/BitfocusDevice.cpp | 133 ++++- .../Protocols/Bitfocus/BitfocusDevice.hpp | 1 - .../BitfocusProtocolSettingsWidget.cpp | 124 ++++- .../BitfocusProtocolSettingsWidget.hpp | 21 +- .../Bitfocus/BitfocusSpecificSettings.hpp | 11 + 10 files changed, 915 insertions(+), 438 deletions(-) create mode 100644 src/plugins/score-plugin-protocols/Protocols/Bitfocus/BitfocusContext.unix.cpp create mode 100644 src/plugins/score-plugin-protocols/Protocols/Bitfocus/BitfocusContext.win32.cpp diff --git a/src/plugins/score-plugin-protocols/CMakeLists.txt b/src/plugins/score-plugin-protocols/CMakeLists.txt index 2c935d78af..6d9e25c5f0 100644 --- a/src/plugins/score-plugin-protocols/CMakeLists.txt +++ b/src/plugins/score-plugin-protocols/CMakeLists.txt @@ -398,7 +398,11 @@ if(LINUX) endif() target_sources(${PROJECT_NAME} PRIVATE ${BITFOCUS_HDRS} ${BITFOCUS_SRCS}) -target_include_directories(${PROJECT_NAME} PRIVATE ${BITFOCUS_HEADER}) +if(WIN32) + target_sources(${PROJECT_NAME} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/Protocols/Bitfocus/BitfocusContext.win32.cpp") +else() + target_sources(${PROJECT_NAME} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/Protocols/Bitfocus/BitfocusContext.unix.cpp") +endif() target_compile_definitions(${PROJECT_NAME} PRIVATE OSSIA_PROTOCOL_BITFOCUS) list(APPEND SCORE_FEATURES_LIST protocol_bitfocus) diff --git a/src/plugins/score-plugin-protocols/Protocols/Bitfocus/BitfocusContext.cpp b/src/plugins/score-plugin-protocols/Protocols/Bitfocus/BitfocusContext.cpp index da628ad164..5ae2f410d7 100644 --- a/src/plugins/score-plugin-protocols/Protocols/Bitfocus/BitfocusContext.cpp +++ b/src/plugins/score-plugin-protocols/Protocols/Bitfocus/BitfocusContext.cpp @@ -1,5 +1,9 @@ #include "BitfocusContext.hpp" +#include + +#include + #include W_OBJECT_IMPL(bitfocus::module_handler) @@ -7,4 +11,459 @@ namespace bitfocus { module_handler_base::~module_handler_base() = default; module_handler::~module_handler() = default; + +module_handler::module_handler(QString path) + : module_handler_base{path} +{ +} + +QString module_handler::toPayload(QJsonObject obj) +{ + return QJsonDocument{obj}.toJson(QJsonDocument::Compact); +} + +void module_handler::processMessage(std::string_view v) +{ + auto doc = QJsonDocument::fromJson(QByteArray::fromRawData(v.data(), v.size())); + auto id = doc.object()["callbackId"]; + auto direction = doc.object()["direction"]; + auto pay = doc.object()["payload"]; + auto name = doc.object()["name"]; + auto success = doc.object()["success"]; + + auto payload_json = QJsonDocument::fromJson(pay.toString().toUtf8()); + auto pretty = [&] { qDebug() << payload_json.toJson().toStdString().data(); }; + + qDebug() << id.toString() << name.toString() << direction.toString(); + qDebug() << payload_json.toJson().toStdString().data(); + if(direction == "call") + { + if(name == "register") + { + // First message + on_register(); + + QMetaObject::invokeMethod(this, [this] { init_msg_id = init(); }); + } + else if(name == "upgradedItems") + QMetaObject::invokeMethod(this, [this, n = id.toInt()] { send_success(n); }); + else if(name == "setActionDefinitions") + on_setActionDefinitions(payload_json["actions"].toArray()); + else if(name == "setFeedbackDefinitions") + on_setFeedbackDefinitions(payload_json["feedbacks"].toArray()); + else if(name == "setVariableDefinitions") + on_setVariableDefinitions(payload_json["variables"].toArray()); + else if(name == "setPresetDefinitions") + on_setPresetDefinitions(payload_json["presets"].toArray()); + else if(name == "setVariableValues") + on_setVariableValues(payload_json["newValues"].toArray()); + else if(name == "log-message") + { + // qDebug() << " !! Unhandled !! " << name; + // pretty(); + } + else if(name == "set-status") + { + // on_set_status(payload_json.object()); + } + else if(name == "updateFeedbackValues") + on_updateFeedbackValues(payload_json.object()); + else if(name == "saveConfig") + on_saveConfig(payload_json.object()); + else if(name == "send-osc") + on_send_osc(payload_json.object()); + else if(name == "parseVariablesInString") + { + on_parseVariablesInString(id.toInt(), payload_json.object()); + } + else if(name == "recordAction") + on_recordAction(payload_json.object()); + else if(name == "setCustomVariable") + on_setCustomVariable(payload_json.object()); + else if(name == "sharedUdpSocketJoin") + on_sharedUdpSocketJoin(payload_json.object()); + else if(name == "sharedUdpSocketLeave") + on_sharedUdpSocketLeave(payload_json.object()); + else if(name == "sharedUdpSocketSend") + on_sharedUdpSocketSend(payload_json.object()); + else + qDebug() << "Unhandled: " << name; + } + else if(direction == "response") + { + if(id == init_msg_id) + { + // Update: + //{ + // "hasHttpHandler": false, + // "hasRecordActionsHandler": false, + // "newUpgradeIndex": -1, + // "updatedConfig": { + // "localport": 35550, + // "remotehost": "127.0.0.1", + // "remoteport": 35551 + // } + //} + + // Query config field + req_cfg_id = this->requestConfigFields(); + } + else if(id == req_cfg_id) + { + on_response_configFields(payload_json["fields"].toArray()); + } + } +} + +int module_handler::writeRequest(QString name, QString p) +{ + int id = cbid++; + QJsonObject obj; + obj["direction"] = "call"; + obj["name"] = name; + obj["payload"] = p; + obj["callbackId"] = id; + + auto res = toPayload(obj).toUtf8().append('\n'); + qDebug().noquote().nospace() << "sending: " << res.size(); + do_write(res); + return id; +} + +void module_handler::writeReply(int id, QString p) +{ + QJsonObject obj; + obj["direction"] = "response"; + obj["payload"] = p; + obj["callbackId"] = id; + auto res = toPayload(obj).toUtf8().append('\n'); + qDebug() << "sending: " << res; + do_write(res); +} + +void module_handler::writeReply(int id, QJsonObject p) +{ + return writeReply(id, QJsonDocument(p).toJson(QJsonDocument::Compact)); +} + +void module_handler::on_register() +{ + // Payload in ejson format. + std::string_view res + = R"_({"direction":"response","callbackId":1,"success":true,"payload":"{}"})_" + "\n"; + qDebug().noquote().nospace() << "sending: " << res.data(); + do_write(res); + cbid = 1; +} + +void module_handler::on_setActionDefinitions(QJsonArray actions) +{ + for(auto act : actions) + { + auto obj = act.toObject(); + bitfocus::connection::action_definition def; + def.hasLearn = obj["hasLearn"].toBool(); + def.name = obj["name"].toString(); + for(auto opt : obj["options"].toArray()) + def.options.push_back(opt.toObject().toVariantMap()); + + m_model.actions.emplace(obj["id"].toString(), std::move(def)); + } +} + +void module_handler::on_setVariableDefinitions(QJsonArray vars) +{ + for(auto var : vars) + { + auto obj = var.toObject(); + bitfocus::connection::variable_definition def; + def.name = obj["name"].toString(); + m_model.variables[obj["id"].toString()] = std::move(def); + } +} +void module_handler::on_setFeedbackDefinitions(QJsonArray fbs) +{ + for(auto fb : fbs) + { + auto obj = fb.toObject(); + bitfocus::connection::feedback_definition def; + def.hasLearn = obj["hasLearn"].toBool(); + def.name = obj["name"].toString(); + def.type = obj["type"].toString(); + for(auto opt : obj["options"].toArray()) + def.options.push_back(opt.toObject().toVariantMap()); + + m_model.feedbacks.emplace(obj["id"].toString(), std::move(def)); + } +} +void module_handler::on_setPresetDefinitions(QJsonArray presets) +{ + for(auto preset : presets) + { + auto obj = preset.toObject(); + bitfocus::connection::preset_definition def; + def.name = obj["name"].toString(); + def.text = obj["text"].toString(); + def.category = obj["category"].toString(); + def.type = obj["type"].toString(); + + for(auto opt : obj["feedbacks"].toArray()) + def.feedbacks.push_back(opt.toObject().toVariantMap()); + for(auto opt : obj["steps"].toArray()) + def.steps.push_back(opt.toObject().toVariantMap()); + m_model.presets.emplace(obj["id"].toString(), std::move(def)); + } +} +void module_handler::on_setVariableValues(QJsonArray vars) +{ + for(auto var : vars) + { + auto obj = var.toObject(); + m_model.variables[obj["id"].toString()].value = obj["value"].toVariant(); + } +} +void module_handler::on_response_configFields(QJsonArray fields) +{ + m_model.config_fields.clear(); + for(auto obj : fields) + { + auto f = obj.toObject(); + connection::config_field res; + res.id = f["id"].toString(); + res.label = f["label"].toString(); + res.type = f["type"].toString(); + res.regex = f["regex"].toString(); + res.value = f["value"].toVariant(); + res.default_value = f["default"].toVariant(); + res.width = f["width"].toDouble(); + { + for(auto choice_obj : f["choices"].toArray()) + { + connection::config_field::choice c; + auto choice = choice_obj.toObject(); + c.id = choice["id"].toString(); + c.label = choice["label"].toString(); + if(!c.id.isEmpty()) + res.choices.push_back(std::move(c)); + } + } + m_model.config_fields.push_back(std::move(res)); + } + + configurationParsed(); +} + +void module_handler::on_send_osc(QJsonObject obj) +{ + qDebug() << "TODO send osc" << obj; + const std::string host = obj["host"].toString().toStdString(); + const int port = obj["port"].toInt(); + const std::string path = obj["path"].toString().toStdString(); + const auto args = obj["args"].toArray(); + + char buf[65535]; + oscpack::OutboundPacketStream p{buf, 65535}; + p << oscpack::BeginMessageN(path); + for(auto arg : args) + { + switch(arg.type()) + { + case QJsonValue::Type::Null: + // p << oscpack::OscNil(); + break; + case QJsonValue::Type::Undefined: + p << oscpack::Infinitum(); + break; + case QJsonValue::Type::Bool: + p << arg.toBool(); + break; + case QJsonValue::Type::Double: + p << (float)arg.toDouble(); + break; + case QJsonValue::Type::String: + p << arg.toString().toStdString(); + break; + case QJsonValue::Type::Object: { + auto obj = arg.toObject(); + auto t = obj["type"].toString(); + if(t == "i") + p << (int)obj["value"].toDouble(); + else if(t == "f") + p << (float)obj["value"].toDouble(); + else if(t == "d") + p << obj["value"].toDouble(); + else if(t == "s") + p << obj["value"].toString().toStdString(); + else if(t == "b") + { + auto blob = obj["value"].toString().toStdString(); + p << oscpack::Blob(blob.data(), blob.size()); + } + + //auto v = obj["value"].toString(); + break; + } + case QJsonValue::Type::Array: + // FIXME + // FIXME UInt8Array ??? + break; + } + } + p << oscpack::EndMessage(); + + try + { + boost::system::error_code ec; + boost::asio::ip::udp::socket socket{m_send_service}; + socket.open(boost::asio::ip::udp::v4(), ec); + if(ec != boost::system::error_code{}) + return; + socket.set_option(boost::asio::ip::udp::socket::reuse_address(true), ec); + if(ec != boost::system::error_code{}) + return; + socket.set_option(boost::asio::socket_base::broadcast(true), ec); + if(ec != boost::system::error_code{}) + return; + boost::asio::ip::udp::endpoint endpoint{ + boost::asio::ip::make_address(host, ec), (uint16_t)port}; + if(ec != boost::system::error_code{}) + return; + socket.send_to(boost::asio::const_buffer(p.Data(), p.Size()), endpoint, 0, ec); + } + catch(...) + { + } +} + +int module_handler::init() +{ + QJsonObject obj; + obj["label"] = "OSCPoint"; + obj["isFirstInit"] = false; + obj["config"] = QJsonObject{ + {"remotehost", "127.0.0.1"}, {"remoteport", 35551}, {"localport", 35550}}; + obj["lastUpgradeIndex"] = -1; + obj["actions"] = QJsonObject{}; + obj["feedbacks"] = QJsonObject{}; + + return writeRequest("init", toPayload(obj)); +} + +void module_handler::send_success(int id) +{ + QJsonObject obj; + obj["direction"] = "response"; + obj["callbackId"] = id; + obj["success"] = true; + auto res = toPayload(obj).toUtf8().append('\n'); + do_write(res); +} + +void module_handler::on_set_status(QJsonObject obj) { } +void module_handler::on_saveConfig(QJsonObject obj) { } +void module_handler::on_parseVariablesInString(int id, QJsonObject obj) +{ + QJsonObject p; + p["text"] = ""; // FIXME + p["variableIds"] = QJsonArray{}; + writeReply(id, p); +} +void module_handler::on_updateFeedbackValues(QJsonObject obj) { } +void module_handler::on_recordAction(QJsonObject obj) { } +void module_handler::on_setCustomVariable(QJsonObject obj) { } +void module_handler::on_sharedUdpSocketJoin(QJsonObject obj) { } +void module_handler::on_sharedUdpSocketLeave(QJsonObject obj) { } +void module_handler::on_sharedUdpSocketSend(QJsonObject obj) { } +void module_handler::updateConfigAndLabel() +{ + qDebug() << "TODO" << Q_FUNC_INFO; +} + +int module_handler::requestConfigFields() +{ + return writeRequest("getConfigFields", toPayload(QJsonObject{})); +} + +void module_handler::updateFeedbacks() +{ + qDebug() << "TODO" << Q_FUNC_INFO; +} + +void module_handler::feedbackLearnValues() +{ + qDebug() << "TODO" << Q_FUNC_INFO; +} + +void module_handler::feedbackDelete() +{ + qDebug() << "TODO" << Q_FUNC_INFO; +} + +void module_handler::variablesChanged() +{ + qDebug() << "TODO" << Q_FUNC_INFO; + // {"direction":"call","name":"variablesChanged","payload":"{\"variablesIds\":[\"internal:time_hms\",\"internal:time_s\",\"internal:time_unix\",\"internal:time_hms_12\",\"internal:uptime\"]}"} +} + +void module_handler::actionUpdate() +{ + qDebug() << "TODO" << Q_FUNC_INFO; +} + +void module_handler::actionDelete() +{ + qDebug() << "TODO" << Q_FUNC_INFO; +} + +void module_handler::actionLearnValues() +{ + qDebug() << "TODO" << Q_FUNC_INFO; +} + +void module_handler::actionRun(std::string_view act) +{ + QJsonObject act_object; + act_object["id"] = QString("foo"); + act_object["controlId"] = QString("bank:0"); + act_object["actionId"] = QString::fromUtf8(act.data(), act.size()); + act_object["options"] = QJsonObject{}; // TODO + act_object["upgradeIndex"] = QJsonValue{QJsonValue::Type::Null}; + act_object["disabled"] = false; + QJsonObject root; + root["action"] = act_object; + root["surfaceId"] = QString("hot:tablet"); // could be undefined + + writeRequest("executeAction", toPayload(root)); +} + +void module_handler::destroy() +{ + qDebug() << "TODO" << Q_FUNC_INFO; +} + +void module_handler::executeHttpRequest() +{ + qDebug() << "TODO" << Q_FUNC_INFO; +} + +void module_handler::startStopRecordingActions() +{ + qDebug() << "TODO" << Q_FUNC_INFO; +} + +void module_handler::sharedUdpSocketMessage() +{ + qDebug() << "TODO" << Q_FUNC_INFO; +} + +void module_handler::sharedUdpSocketError() +{ + qDebug() << "TODO" << Q_FUNC_INFO; +} + +const connection& module_handler::model() +{ + return m_model; +} } diff --git a/src/plugins/score-plugin-protocols/Protocols/Bitfocus/BitfocusContext.hpp b/src/plugins/score-plugin-protocols/Protocols/Bitfocus/BitfocusContext.hpp index e28c95e9b2..85ca24eee0 100644 --- a/src/plugins/score-plugin-protocols/Protocols/Bitfocus/BitfocusContext.hpp +++ b/src/plugins/score-plugin-protocols/Protocols/Bitfocus/BitfocusContext.hpp @@ -1,4 +1,6 @@ #pragma once +#include + #include #include #include @@ -7,7 +9,6 @@ #include #include - #if !defined(_WIN32) #include #include @@ -18,9 +19,6 @@ #endif namespace bitfocus { -struct module -{ -}; struct connection { @@ -41,14 +39,11 @@ struct connection QString name; std::vector options; QString type; - - //{"defaultStyle":{"bgcolor":16711680,"color":0}, - // "hasLearn":false, - // "id":"showState", - // "name":"Show state feedback", - // "options":[{"choices":[{"id":"slideshow","label":"Slide show"},{"id":"edit","label":"Edit"}],"default":"slideshow","id":"state","label":"State","type":"dropdown"}], - // "type":"boolean" - // } + struct + { + QColor bgcolor; + QColor color; + } defaultStyle; }; struct preset_definition { @@ -60,439 +55,138 @@ struct connection std::vector steps; }; + struct config_field + { + struct choice + { + QString id; + QString label; + }; + + QString id; + QString label; + // "static-text", "textinput", "number", "checkbox", "choices", "bonjour-device", "dropdown" + QString type; + QVariant value; + QString tooltip; + QString regex; + // ex. : "e=>!![\"TF\",\"DM3\",\"DM7\"].includes(e.model)&&(e.kaInterval=e.kaIntervalH,!0)", + QString isVisibleFn; + double min = 0; + double max = 1; + std::vector choices; + QVariant default_value; // true, a number, a string etc + double width; + }; + std::map actions; std::map variables; std::map feedbacks; std::map presets; - - struct config_field - { - }; + std::vector config_fields; struct config_ui { - std::vector fields; std::map config; }; }; -struct companion -{ - std::map modules; - std::map connections; -}; - // note: callback id shared between both ends so every message has to be processed in order #if defined(_WIN32) struct module_handler_base : public QObject { - explicit module_handler_base(QString module_path) - { - // https://doc.qt.io/qt-6/qwineventnotifier.html - // https://forum.qt.io/topic/146343/qsocketnotifier-with-win32-namedpipes/9 - // Or maybe QLocalSocket just works on windows? - - // FIXME - } - + explicit module_handler_base(QString module_path); virtual ~module_handler_base(); - void do_write(std::string_view res) - { - // FIXME - } - void do_write(const QByteArray& res) - { - // FIXME - } + void do_write(std::string_view res); + void do_write(const QByteArray& res); virtual void processMessage(std::string_view) = 0; }; #else struct module_handler_base : public QObject { - char buf[16 * 4096]; - QProcess process; + char buf[16 * 4096]{}; + QProcess process{}; QSocketNotifier* socket{}; - int pfd[2]; - - explicit module_handler_base(QString module_path) - { - // Create socketpair - socketpair(PF_LOCAL, SOCK_STREAM, 0, pfd); - - // Create env - auto genv = QProcessEnvironment::systemEnvironment(); - genv.insert("CONNECTION_ID", "connectionId"); - genv.insert("VERIFICATION_TOKEN", "foobar"); - genv.insert("MODULE_MANIFEST", module_path + "/companion/manifest.json"); - genv.insert("NODE_CHANNEL_SERIALIZATION_MODE", "json"); - genv.insert("NODE_CHANNEL_FD", QString::number(pfd[1]).toUtf8()); - - auto socket = new QSocketNotifier(pfd[0], QSocketNotifier::Read, this); - QObject::connect( - socket, &QSocketNotifier::activated, this, &module_handler_base::on_read); - - process.setProcessChannelMode(QProcess::ForwardedChannels); - process.setProgram("node"); - process.setArguments({"main.js"}); // FIXME entrypoint from spec - process.setWorkingDirectory(module_path); - process.setProcessEnvironment(genv); - - process.start(); - - // See https://forum.qt.io/topic/33964/solved-child-qprocess-that-dies-with-parent/10 - - /// Connection flow: - // Create process - // <- register call - // -> register response - - // -> init call - // <- upgradedItems - // <- setActionDefinitions - // <- setVariableDefinitions - // <- etc. - // <- init response - } + int pfd[2]{}; + explicit module_handler_base(QString module_path); virtual ~module_handler_base(); - void on_read(QSocketDescriptor, QSocketNotifier::Type) - { - ssize_t rl = ::read(pfd[0], buf, sizeof(buf)); - if(rl <= 0) - return; - char* pos = buf; - char* idx = buf; - char* const end = pos + rl; - do - { - idx = std::find(pos, end, '\n'); - if(idx < end) - { - std::ptrdiff_t diff = idx - pos; - std::string_view message(pos, diff); - this->processMessage(message); - pos = idx + 1; - continue; - } - } while(idx < end); - } - - void do_write(std::string_view res) { ::write(pfd[0], res.data(), res.size()); } - void do_write(const QByteArray& res) { ::write(pfd[0], res.data(), res.size()); } + void on_read(QSocketDescriptor, QSocketNotifier::Type); + void do_write(std::string_view res); + void do_write(const QByteArray& res); virtual void processMessage(std::string_view) = 0; }; #endif + struct module_handler final : public module_handler_base { W_OBJECT(module_handler) public: - explicit module_handler(QString path) - : module_handler_base{path} - { - } - + explicit module_handler(QString path); virtual ~module_handler(); - QString toPayload(QJsonObject obj) - { - return QJsonDocument{obj}.toJson(QJsonDocument::Compact); - } - - void processMessage(std::string_view v) override - { - auto doc = QJsonDocument::fromJson(QByteArray::fromRawData(v.data(), v.size())); - auto id = doc.object()["callbackId"]; - auto direction = doc.object()["direction"]; - auto pay = doc.object()["payload"]; - auto name = doc.object()["name"]; - auto success = doc.object()["success"]; - - auto payload_json = QJsonDocument::fromJson(pay.toString().toUtf8()); - auto pretty = [&] { qDebug() << payload_json.toJson().toStdString().data(); }; - - if(direction == "call") - { - if(name == "register") - { - // First message - on_register(); - - QMetaObject::invokeMethod(this, [this] { init_msg_id = init(); }); - } - else if(name == "upgradedItems") - { - QMetaObject::invokeMethod(this, [this, n = id.toInt()] { send_success(n); }); - } - else if(name == "setActionDefinitions") - { - auto actions = payload_json["actions"].toArray(); - for(auto act : actions) - { - auto obj = act.toObject(); - bitfocus::connection::action_definition def; - def.hasLearn = obj["hasLearn"].toBool(); - def.name = obj["name"].toString(); - for(auto opt : obj["options"].toArray()) - def.options.push_back(opt.toObject().toVariantMap()); - - m_model.actions.emplace(obj["id"].toString(), std::move(def)); - } - } - else if(name == "setFeedbackDefinitions") - { - auto fbs = payload_json["feedbacks"].toArray(); - for(auto fb : fbs) - { - auto obj = fb.toObject(); - bitfocus::connection::feedback_definition def; - def.hasLearn = obj["hasLearn"].toBool(); - def.name = obj["name"].toString(); - def.type = obj["type"].toString(); - for(auto opt : obj["options"].toArray()) - def.options.push_back(opt.toObject().toVariantMap()); - - m_model.feedbacks.emplace(obj["id"].toString(), std::move(def)); - } - } - else if(name == "setVariableDefinitions") - { - auto vars = payload_json["variables"].toArray(); - for(auto var : vars) - { - auto obj = var.toObject(); - bitfocus::connection::variable_definition def; - def.name = obj["name"].toString(); - m_model.variables[obj["id"].toString()] = std::move(def); - } - } - else if(name == "setPresetDefinitions") - { - auto vars = payload_json["presets"].toArray(); - for(auto var : vars) - { - auto obj = var.toObject(); - bitfocus::connection::preset_definition def; - def.name = obj["name"].toString(); - def.text = obj["text"].toString(); - def.category = obj["category"].toString(); - def.type = obj["type"].toString(); - - for(auto opt : obj["feedbacks"].toArray()) - def.feedbacks.push_back(opt.toObject().toVariantMap()); - for(auto opt : obj["steps"].toArray()) - def.steps.push_back(opt.toObject().toVariantMap()); - m_model.presets.emplace(obj["id"].toString(), std::move(def)); - } - } - else if(name == "setVariableValues") - { - auto vars = payload_json["newValues"].toArray(); - for(auto var : vars) - { - auto obj = var.toObject(); - m_model.variables[obj["id"].toString()].value = obj["value"].toVariant(); - } - } - else if(name == "log-message") - { - // qDebug() << " !! Unhandled !! " << name; - // pretty(); - } - else if(name == "set-status") - { - qDebug() << " !! Unhandled !! " << name; - pretty(); - } - else if(name == "updateFeedbackValues") - { - qDebug() << " !! Unhandled !! " << name; - pretty(); - } - else if(name == "saveConfig") - { - qDebug() << " !! Unhandled !! " << name; - pretty(); - } - else if(name == "send-osc") - { - on_send_osc(payload_json.object()); - } - else if(name == "parseVariablesInString") - { - qDebug() << " !! Unhandled !! " << name; - pretty(); - } - else if(name == "recordAction") - { - qDebug() << " !! Unhandled !! " << name; - pretty(); - } - else if(name == "setCustomVariable") - { - qDebug() << " !! Unhandled !! " << name; - pretty(); - } - else if(name == "sharedUdpSocketJoin") - { - qDebug() << " !! Unhandled !! " << name; - pretty(); - } - else if(name == "sharedUdpSocketLeave") - { - qDebug() << " !! Unhandled !! " << name; - pretty(); - } - else if(name == "sharedUdpSocketSend") - { - qDebug() << " !! Unhandled !! " << name; - pretty(); - } - } - else if(direction == "response") - { - if(id == init_msg_id) - { - // Update: - //{ - // "hasHttpHandler": false, - // "hasRecordActionsHandler": false, - // "newUpgradeIndex": -1, - // "updatedConfig": { - // "localport": 35550, - // "remotehost": "127.0.0.1", - // "remoteport": 35551 - // } - //} - - // Query config field - this->requestConfigFields(); - } - } - } - - int writeRequest(QString name, QString p) - { - int id = cbid++; - QJsonObject obj; - obj["direction"] = "call"; - obj["name"] = name; - obj["payload"] = p; - obj["callbackId"] = id; - - auto res = toPayload(obj).toUtf8().append('\n'); - qDebug().noquote().nospace() << "sending: " << res.size(); - do_write(res); - return id; - } - - void writeReply(int id, QString p) - { - QJsonObject obj; - obj["direction"] = "response"; - obj["payload"] = p; - obj["callbackId"] = id; - auto res = toPayload(obj).toUtf8().append('\n'); - qDebug() << "sending: " << res; - do_write(res); - } - - // Module -> app - void on_register() - { - // Payload in ejson format. - std::string_view res - = R"_({"direction":"response","callbackId":1,"success":true,"payload":"{}"})_" - "\n"; - qDebug().noquote().nospace() << "sending: " << res.data(); - do_write(res); - cbid = 1; - } - - void on_send_osc(QJsonObject obj) - { - const QString host = obj["host"].toString(); - const int port = obj["port"].toInt(); - const QString path = obj["path"].toString(); - const auto args = obj["args"].toArray(); - - qDebug() << "TODO send osc" << path << args; - } + QString toPayload(QJsonObject obj); + + void processMessage(std::string_view v) override; + + int writeRequest(QString name, QString p); + void writeReply(int id, QString p); + void writeReply(int id, QJsonObject p); + + // Module -> app (requests handling) + void on_register(); + void on_setActionDefinitions(QJsonArray obj); + void on_setVariableDefinitions(QJsonArray obj); + void on_setFeedbackDefinitions(QJsonArray obj); + void on_setPresetDefinitions(QJsonArray obj); + void on_setVariableValues(QJsonArray obj); + void on_set_status(QJsonObject obj); + void on_saveConfig(QJsonObject obj); + void on_parseVariablesInString(int id, QJsonObject obj); + void on_updateFeedbackValues(QJsonObject obj); + void on_recordAction(QJsonObject obj); + void on_setCustomVariable(QJsonObject obj); + void on_sharedUdpSocketJoin(QJsonObject obj); + void on_sharedUdpSocketLeave(QJsonObject obj); + void on_sharedUdpSocketSend(QJsonObject obj); + void on_send_osc(QJsonObject obj); + + // Module -> app (replies handling) + void on_response_configFields(QJsonArray fields); // App -> module - int init() - { - QJsonObject obj; - obj["label"] = "OSCPoint"; - obj["isFirstInit"] = false; - obj["config"] = QJsonObject{ - {"remotehost", "127.0.0.1"}, {"remoteport", 35551}, {"localport", 35550}}; - obj["lastUpgradeIndex"] = -1; - obj["actions"] = QJsonObject{}; - obj["feedbacks"] = QJsonObject{}; - - return writeRequest("init", toPayload(obj)); - } - - void send_success(int id) - { - QJsonObject obj; - obj["direction"] = "response"; - obj["callbackId"] = id; - obj["success"] = true; - auto res = toPayload(obj).toUtf8().append('\n'); - do_write(res); - } - - void updateConfigAndLabel() { qDebug() << "TODO" << Q_FUNC_INFO; } - int requestConfigFields() - { - return writeRequest("getConfigFields", toPayload(QJsonObject{})); - } - void updateFeedbacks() { qDebug() << "TODO" << Q_FUNC_INFO; } - void feedbackLearnValues() { qDebug() << "TODO" << Q_FUNC_INFO; } - void feedbackDelete() { qDebug() << "TODO" << Q_FUNC_INFO; } - void variablesChanged() - { - qDebug() << "TODO" << Q_FUNC_INFO; - // {"direction":"call","name":"variablesChanged","payload":"{\"variablesIds\":[\"internal:time_hms\",\"internal:time_s\",\"internal:time_unix\",\"internal:time_hms_12\",\"internal:uptime\"]}"} - } - void actionUpdate() { qDebug() << "TODO" << Q_FUNC_INFO; } - void actionDelete() { qDebug() << "TODO" << Q_FUNC_INFO; } - void actionLearnValues() { qDebug() << "TODO" << Q_FUNC_INFO; } - void actionRun() - { - // {"direction":"call", - // "name":"executeAction", - // "payload": - // "{\"action\": - // {\"id\":\"ZNn2OwGmoX3ApvT2MdfkM\", - // \"controlId\":\"bank:j_kyzoAM5Cwj0Lb1dx5MS\", - // \"actionId\":\"goto_first_slide\", - // \"options\":{}, - // \"upgradeIndex\":null, - // \"disabled\":false - // },\"surfaceId\":\"hot:tablet\"}", - // "callbackId":3} - - qDebug() << "TODO" << Q_FUNC_INFO; - } - void destroy() { qDebug() << "TODO" << Q_FUNC_INFO; } - void executeHttpRequest() { qDebug() << "TODO" << Q_FUNC_INFO; } - void startStopRecordingActions() { qDebug() << "TODO" << Q_FUNC_INFO; } - void sharedUdpSocketMessage() { qDebug() << "TODO" << Q_FUNC_INFO; } - void sharedUdpSocketError() { qDebug() << "TODO" << Q_FUNC_INFO; } - const bitfocus::connection& model() { return m_model; } + int init(); + void send_success(int id); + void updateConfigAndLabel(); + int requestConfigFields(); + void updateFeedbacks(); + void feedbackLearnValues(); + void feedbackDelete(); + void variablesChanged(); + void actionUpdate(); + void actionDelete(); + void actionLearnValues(); + void actionRun(std::string_view act); + void destroy(); + void executeHttpRequest(); + void startStopRecordingActions(); + void sharedUdpSocketMessage(); + void sharedUdpSocketError(); + const bitfocus::connection& model(); void configurationParsed() W_SIGNAL(configurationParsed); private: bitfocus::connection m_model; - int cbid = 0; - int init_msg_id{}; + boost::asio::io_context m_send_service; + int cbid{}; + + int init_msg_id{-1}; + int req_cfg_id{-1}; }; } // namespace bitfocus diff --git a/src/plugins/score-plugin-protocols/Protocols/Bitfocus/BitfocusContext.unix.cpp b/src/plugins/score-plugin-protocols/Protocols/Bitfocus/BitfocusContext.unix.cpp new file mode 100644 index 0000000000..e44e13f1d2 --- /dev/null +++ b/src/plugins/score-plugin-protocols/Protocols/Bitfocus/BitfocusContext.unix.cpp @@ -0,0 +1,78 @@ +#include "BitfocusContext.hpp" + +namespace bitfocus +{ + +module_handler_base::module_handler_base(QString module_path) +{ + // Create socketpair + socketpair(PF_LOCAL, SOCK_STREAM, 0, pfd); + + // Create env + auto genv = QProcessEnvironment::systemEnvironment(); + genv.insert("CONNECTION_ID", "connectionId"); + genv.insert("VERIFICATION_TOKEN", "foobar"); + genv.insert("MODULE_MANIFEST", module_path + "/companion/manifest.json"); + genv.insert("NODE_CHANNEL_SERIALIZATION_MODE", "json"); + genv.insert("NODE_CHANNEL_FD", QString::number(pfd[1]).toUtf8()); + + auto socket = new QSocketNotifier(pfd[0], QSocketNotifier::Read, this); + QObject::connect( + socket, &QSocketNotifier::activated, this, &module_handler_base::on_read); + + process.setProcessChannelMode(QProcess::ForwardedChannels); + process.setProgram("node"); + process.setArguments({"main.js"}); // FIXME entrypoint from spec + process.setWorkingDirectory(module_path); + process.setProcessEnvironment(genv); + + process.start(); + + // See https://forum.qt.io/topic/33964/solved-child-qprocess-that-dies-with-parent/10 + + /// Connection flow: + // Create process + // <- register call + // -> register response + + // -> init call + // <- upgradedItems + // <- setActionDefinitions + // <- setVariableDefinitions + // <- etc. + // <- init response +} + +void module_handler_base::on_read(QSocketDescriptor, QSocketNotifier::Type) +{ + ssize_t rl = ::read(pfd[0], buf, sizeof(buf)); + if(rl <= 0) + return; + char* pos = buf; + char* idx = buf; + char* const end = pos + rl; + do + { + idx = std::find(pos, end, '\n'); + if(idx < end) + { + std::ptrdiff_t diff = idx - pos; + std::string_view message(pos, diff); + this->processMessage(message); + pos = idx + 1; + continue; + } + } while(idx < end); +} + +void module_handler_base::do_write(std::string_view res) +{ + ::write(pfd[0], res.data(), res.size()); +} + +void module_handler_base::do_write(const QByteArray& res) +{ + ::write(pfd[0], res.data(), res.size()); +} + +} diff --git a/src/plugins/score-plugin-protocols/Protocols/Bitfocus/BitfocusContext.win32.cpp b/src/plugins/score-plugin-protocols/Protocols/Bitfocus/BitfocusContext.win32.cpp new file mode 100644 index 0000000000..d894f37a89 --- /dev/null +++ b/src/plugins/score-plugin-protocols/Protocols/Bitfocus/BitfocusContext.win32.cpp @@ -0,0 +1,24 @@ +#include "BitfocusContext.hpp" + +namespace bitfocus +{ + +module_handler_base::module_handler_base(QString module_path) +{ + // https://doc.qt.io/qt-6/qwineventnotifier.html + // https://forum.qt.io/topic/146343/qsocketnotifier-with-win32-namedpipes/9 + // Or maybe QLocalSocket just works on windows? + + // FIXME +} +void module_handler_base::do_write(std::string_view res) +{ + // FIXME +} + +void module_handler_base::do_write(const QByteArray& res) +{ + // FIXME +} + +} diff --git a/src/plugins/score-plugin-protocols/Protocols/Bitfocus/BitfocusDevice.cpp b/src/plugins/score-plugin-protocols/Protocols/Bitfocus/BitfocusDevice.cpp index aa8ec936d6..59fdba47b8 100644 --- a/src/plugins/score-plugin-protocols/Protocols/Bitfocus/BitfocusDevice.cpp +++ b/src/plugins/score-plugin-protocols/Protocols/Bitfocus/BitfocusDevice.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -17,7 +18,101 @@ #include #include +#include + #include +namespace ossia::net +{ +class bitfocus_protocol : public ossia::net::protocol_base +{ +public: + bitfocus_protocol( + std::shared_ptr rc, ossia::net::network_context_ptr ctx) + : m_rc{rc} + , m_context{ctx} + { + } + + bool pull(ossia::net::parameter_base&) override { return true; } + bool push(const ossia::net::parameter_base& p, const ossia::value& v) override + { + auto parent = p.get_node().get_parent(); + if(parent == nodes.actions) + { + QMetaObject::invokeMethod(m_rc.get(), [m = m_rc, name = p.get_node().get_name()] { + m->actionRun(name); + }); + } + else if(parent == nodes.presets) + { + } + else if(parent == nodes.feedbacks) + { + } + return true; + } + bool push_raw(const ossia::net::full_parameter_data&) override { return true; } + bool observe(ossia::net::parameter_base&, bool) override { return true; } + bool update(ossia::net::node_base& node_base) override { return true; } + + void set_device(ossia::net::device_base& dev) override + { + // Start creating the tree + nodes.actions = dev.get_root_node().create_child("action"); + nodes.presets = dev.get_root_node().create_child("presets"); + nodes.feedbacks = dev.get_root_node().create_child("feedback"); + nodes.variables = dev.get_root_node().create_child("variable"); + + for(auto& v : m_rc->model().actions) + { + auto node = nodes.actions->create_child(v.first.toStdString()); + ossia::net::set_description(*node, v.second.name.toStdString()); + auto param = node->create_parameter(ossia::val_type::IMPULSE); + } + + for(auto& v : m_rc->model().presets) + { + auto node = nodes.presets->create_child(v.first.toStdString()); + ossia::net::set_description(*node, v.second.name.toStdString()); + auto param = node->create_parameter(ossia::val_type::IMPULSE); + } + + for(auto& v : m_rc->model().feedbacks) + { + auto node = nodes.feedbacks->create_child(v.first.toStdString()); + ossia::net::set_description(*node, v.second.name.toStdString()); + auto param = node->create_parameter(ossia::val_type::IMPULSE); + } + + for(auto& v : m_rc->model().variables) + { + auto node = nodes.variables->create_child(v.first.toStdString()); + ossia::net::set_description(*node, v.second.name.toStdString()); + auto val = ossia::qt::qt_to_ossia{}(v.second.value); + + if(val.get_type() != ossia::val_type::NONE) + { + auto param = node->create_parameter(val.get_type()); + param->set_value(val); + } + } + } + + std::shared_ptr m_rc; + ossia::net::network_context_ptr m_context; + struct + { + ossia::net::node_base* actions{}; + ossia::net::node_base* presets{}; + ossia::net::node_base* feedbacks{}; + ossia::net::node_base* variables{}; + } nodes; + + ossia::flat_map m_actions; + ossia::flat_map m_presets; + ossia::flat_map m_variables; +}; +} namespace Protocols { @@ -26,7 +121,13 @@ BitfocusDevice::BitfocusDevice( : OwningDeviceInterface{settings} , m_ctx{ctx} { - m_capas.canLearn = true; + m_capas.canRefreshTree = true; + m_capas.canAddNode = false; + m_capas.canRemoveNode = false; + m_capas.canRenameNode = false; + m_capas.canSetProperties = false; + m_capas.canSerialize = false; + m_capas.canLearn = false; m_capas.hasCallbacks = false; } @@ -34,25 +135,20 @@ bool BitfocusDevice::reconnect() { disconnect(); - /* try { const BitfocusSpecificSettings& stgs = settings().deviceSpecificSettings.value(); + if(!stgs.handler) + { + qDebug("oh noes"); + return false; + ; + } const auto& name = settings().name.toStdString(); - if(auto proto - = std::make_unique(m_ctx, stgs.configuration)) + if(auto proto = std::make_unique(stgs.handler, m_ctx)) { - if(stgs.rate) - { - auto rate = std::make_unique( - std::chrono::milliseconds{*stgs.rate}, std::move(proto)); - m_dev = std::make_unique(std::move(rate), name); - } - else - { - m_dev = std::make_unique(std::move(proto), name); - } + m_dev = std::make_unique(std::move(proto), name); deviceChanged(nullptr, m_dev.get()); setLogging_impl(Device::get_cur_logging(isLogging())); @@ -70,18 +166,9 @@ bool BitfocusDevice::reconnect() { SCORE_TODO; } -*/ return connected(); } -void BitfocusDevice::recreate(const Device::Node& n) -{ - for(auto& child : n) - { - addNode(child); - } -} - bool BitfocusDevice::isLearning() const { /* diff --git a/src/plugins/score-plugin-protocols/Protocols/Bitfocus/BitfocusDevice.hpp b/src/plugins/score-plugin-protocols/Protocols/Bitfocus/BitfocusDevice.hpp index 13b4d6b5fb..1a1d63ef95 100644 --- a/src/plugins/score-plugin-protocols/Protocols/Bitfocus/BitfocusDevice.hpp +++ b/src/plugins/score-plugin-protocols/Protocols/Bitfocus/BitfocusDevice.hpp @@ -12,7 +12,6 @@ class BitfocusDevice final : public Device::OwningDeviceInterface const Device::DeviceSettings& stngs, const ossia::net::network_context_ptr& ctx); bool reconnect() override; - void recreate(const Device::Node&) final override; bool isLearning() const final override; void setLearning(bool) final override; diff --git a/src/plugins/score-plugin-protocols/Protocols/Bitfocus/BitfocusProtocolSettingsWidget.cpp b/src/plugins/score-plugin-protocols/Protocols/Bitfocus/BitfocusProtocolSettingsWidget.cpp index 42c0149a65..ad26acf611 100644 --- a/src/plugins/score-plugin-protocols/Protocols/Bitfocus/BitfocusProtocolSettingsWidget.cpp +++ b/src/plugins/score-plugin-protocols/Protocols/Bitfocus/BitfocusProtocolSettingsWidget.cpp @@ -10,14 +10,15 @@ #include -#include -#include -#include - #include +#include +#include #include -#include +#include +#include +#include +#include #include #include @@ -32,8 +33,8 @@ BitfocusProtocolSettingsWidget::BitfocusProtocolSettingsWidget(QWidget* parent) m_deviceNameEdit->setText("OSCdevice"); checkForChanges(m_deviceNameEdit); - auto layout = new QFormLayout{this}; - layout->addRow(tr("Name"), m_deviceNameEdit); + m_rootLayout = new QFormLayout{this}; + m_rootLayout->addRow(tr("Name"), m_deviceNameEdit); } Device::DeviceSettings BitfocusProtocolSettingsWidget::getSettings() const @@ -48,8 +49,109 @@ Device::DeviceSettings BitfocusProtocolSettingsWidget::getSettings() const return s; } +void BitfocusProtocolSettingsWidget::updateFields() +{ + if(!m_settings.handler) + return; + + auto& m = m_settings.handler->model(); + for(auto& field : m.config_fields) + { + // 1. Add the field label + QLabel* lab{}; + if(!field.label.isEmpty()) + { + lab = new QLabel{}; + lab->setWordWrap(true); + lab->setTextFormat(Qt::RichText); + lab->setText(QString("%1").arg(field.label)); + m_subForm->addWidget(lab); + } + + // 2. Create the widget proper + if(field.type == "static-text") + { + if(auto str = field.value.toString(); !str.isEmpty()) + { + auto static_text = new QLabel{}; + static_text->setWordWrap(true); + static_text->setTextFormat(Qt::RichText); + static_text->setText(str); + + m_subForm->addWidget(static_text); + m_widgets[field.id] = {.label = lab, .widget = static_text, .getValue = {}}; + } + } + else if(field.type == "textinput" || field.type == "bonjourdevice") + { + auto widg = new QLineEdit; + if(!field.regex.isEmpty()) + { + auto val = new QRegularExpressionValidator; + val->setRegularExpression(QRegularExpression(field.regex)); + val->setParent(widg); + widg->setValidator(val); + } + widg->setText(field.default_value.toString()); + m_subForm->addWidget(widg); + m_widgets[field.id] + = {.label = lab, .widget = widg, .getValue = [widg]() -> QVariant { + return widg->text(); + }}; + } + else if(field.type == "number") + { + auto widg = new QDoubleSpinBox; + widg->setRange(field.min, field.max); + widg->setValue(field.default_value.toDouble()); + m_subForm->addWidget(widg); + m_widgets[field.id] + = {.label = lab, .widget = widg, .getValue = [widg]() -> QVariant { + return widg->value(); + }}; + } + else if(field.type == "checkbox") + { + auto widg = new QCheckBox; + widg->setChecked(field.default_value.toBool() == true); + m_subForm->addWidget(widg); + m_widgets[field.id] + = {.label = lab, .widget = widg, .getValue = [widg]() -> QVariant { + return widg->isChecked(); + }}; + } + else if(field.type == "choices" || field.type == "dropdown") + { + auto widg = new QComboBox; + int i = 0; + int default_i = -1; + auto default_v = field.default_value.toString(); + for(const auto& choice : field.choices) + { + widg->addItem(choice.label, choice.id); + if(choice.id == default_v) + default_i = i; + i++; + } + if(default_i != -1) + widg->setCurrentIndex(default_i); + + m_subForm->addWidget(widg); + m_widgets[field.id] + = {.label = lab, .widget = widg, .getValue = [widg]() -> QVariant { + return widg->currentData(); + }}; + } + } +} + void BitfocusProtocolSettingsWidget::setSettings(const Device::DeviceSettings& settings) { + delete m_subWidget; + m_subWidget = new QWidget{this}; + m_subForm = new QVBoxLayout{m_subWidget}; + m_rootLayout->addRow(m_subWidget); + m_deviceNameEdit->setText(settings.name); if(settings.deviceSpecificSettings.canConvert()) @@ -57,10 +159,10 @@ void BitfocusProtocolSettingsWidget::setSettings(const Device::DeviceSettings& s auto set = settings.deviceSpecificSettings.value(); if(!set.path.isEmpty() && QDir{set.path}.exists()) { - auto module = new bitfocus::module_handler{set.path}; - connect(module, &bitfocus::module_handler::configurationParsed, this, [] { - - }); + set.handler = std::make_shared(set.path); + connect( + set.handler.get(), &bitfocus::module_handler::configurationParsed, this, + [this] { updateFields(); }); } m_settings = set; diff --git a/src/plugins/score-plugin-protocols/Protocols/Bitfocus/BitfocusProtocolSettingsWidget.hpp b/src/plugins/score-plugin-protocols/Protocols/Bitfocus/BitfocusProtocolSettingsWidget.hpp index ec7fb8a578..da33a4097a 100644 --- a/src/plugins/score-plugin-protocols/Protocols/Bitfocus/BitfocusProtocolSettingsWidget.hpp +++ b/src/plugins/score-plugin-protocols/Protocols/Bitfocus/BitfocusProtocolSettingsWidget.hpp @@ -4,12 +4,17 @@ #include +#include + #include class QStackedLayout; class QLineEdit; +class QVBoxLayout; +class QFormLayout; class QSpinBox; class QWidget; +class QLabel; namespace Protocols { @@ -29,8 +34,22 @@ class BitfocusProtocolSettingsWidget final : public Device::ProtocolSettingsWidg using Device::ProtocolSettingsWidget::checkForChanges; private: - void setDefaults(); + void updateFields(); + QFormLayout* m_rootLayout{}; QLineEdit* m_deviceNameEdit{}; BitfocusSpecificSettings m_settings; + QWidget* m_subWidget{}; + QVBoxLayout* m_subForm{}; + + // Get the configuration for each widget + struct widget + { + QLabel* label{}; + QWidget* widget{}; + std::function getValue; + }; + + std::map m_widgets; + QJSEngine m_uiEngine; }; } diff --git a/src/plugins/score-plugin-protocols/Protocols/Bitfocus/BitfocusSpecificSettings.hpp b/src/plugins/score-plugin-protocols/Protocols/Bitfocus/BitfocusSpecificSettings.hpp index e5cbf7cee0..a6559a9c34 100644 --- a/src/plugins/score-plugin-protocols/Protocols/Bitfocus/BitfocusSpecificSettings.hpp +++ b/src/plugins/score-plugin-protocols/Protocols/Bitfocus/BitfocusSpecificSettings.hpp @@ -3,8 +3,14 @@ #include +#include #include +namespace bitfocus +{ +struct module_handler; +} + namespace Protocols { struct BitfocusSpecificSettings @@ -12,6 +18,11 @@ struct BitfocusSpecificSettings QString path; QString id; QString name; + + std::vector> configuration; + + QString description; + std::shared_ptr handler; }; } Q_DECLARE_METATYPE(Protocols::BitfocusSpecificSettings)