diff --git a/.azure-pipelines/azure-pipelines.yml b/.azure-pipelines/azure-pipelines.yml index 4219624758..dd59c1a1a6 100644 --- a/.azure-pipelines/azure-pipelines.yml +++ b/.azure-pipelines/azure-pipelines.yml @@ -116,7 +116,7 @@ stages: compiler: 'MSVC' qt_version: '6.10.2' qt_version_major: '6' - qt_modules: 'qtserialport qtmultimedia' + qt_modules: 'qtserialport qtmultimedia qthttpserver qtwebsockets' cmake_preset: 'RelWithDebInfo' cmake_version_params: '-DVERSION_MAJOR=${{ parameters.versionMajor }} -DVERSION_MINOR=${{ parameters.versionMinor }} -DVERSION_PATCH=${{ parameters.versionPatch }} -DIS_BETA=${{ lower(eq(parameters.buildType, ''PrivateBeta'')) }}' cmake_additional_param: '-DCMAKE_PREFIX_PATH=C:/Qt/$(qt_version)/msvc2022_64/lib/cmake -DIS_AZURE_BUILD=TRUE' @@ -305,7 +305,7 @@ stages: compiler: 'GCC' qt_version: '6.10.2' qt_version_major: '6' - qt_modules: 'qtserialport qtmultimedia' + qt_modules: 'qtserialport qtmultimedia qthttpserver qtwebsockets' cmake_preset: 'RelWithDebInfo' linux_path: '/opt/Qt/$(qt_version)/gcc_64/lib/cmake:/opt/Qt/$(qt_version)/gcc_64/bin:/opt/Qt/$(qt_version)/gcc_64/plugins:$PATH' cmake_version_params: '-DVERSION_MAJOR=${{ parameters.versionMajor }} -DVERSION_MINOR=${{ parameters.versionMinor }} -DVERSION_PATCH=${{ parameters.versionPatch }} -DIS_BETA=${{ lower(eq(parameters.buildType, ''PrivateBeta'')) }}' @@ -470,7 +470,7 @@ stages: compiler: Clang qt_version: '6.10.2' qt_version_major: '6' - qt_modules: 'qtserialport qtmultimedia' + qt_modules: 'qtserialport qtmultimedia qthttpserver qtwebsockets' cmake_preset: RelWithDebInfo macos_path: '/opt/Qt/6.10.2/macos/lib/cmake:/opt/Qt/6.10.2/macos/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/opt/homebrew:$PATH' cmake_version_params: '-DVERSION_MAJOR=${{ parameters.versionMajor }} -DVERSION_MINOR=${{ parameters.versionMinor }} -DVERSION_PATCH=${{ parameters.versionPatch }} -DIS_BETA=${{ lower(eq(parameters.buildType, ''PrivateBeta'')) }}' @@ -492,7 +492,7 @@ stages: compiler: Clang qt_version: '6.10.2' qt_version_major: '6' - qt_modules: 'qtserialport qtmultimedia' + qt_modules: 'qtserialport qtmultimedia qthttpserver qtwebsockets' cmake_preset: RelWithDebInfo macos_path: '/usr/local/Qt/6.10.2/macos/lib/cmake:/usr/local/Qt/6.10.2/macos/bin:/usr/local/bin:/usr/local/sbin:/usr/local:$PATH' cmake_version_params: '-DVERSION_MAJOR=${{ parameters.versionMajor }} -DVERSION_MINOR=${{ parameters.versionMinor }} -DVERSION_PATCH=${{ parameters.versionPatch }} -DIS_BETA=${{ lower(eq(parameters.buildType, ''PrivateBeta'')) }}' diff --git a/.github/workflows/cpp-ci-serial-programs-base.yml b/.github/workflows/cpp-ci-serial-programs-base.yml index d467c9db0b..eafde5b8e4 100644 --- a/.github/workflows/cpp-ci-serial-programs-base.yml +++ b/.github/workflows/cpp-ci-serial-programs-base.yml @@ -56,7 +56,7 @@ jobs: uses: jurplel/install-qt-action@v4 with: version: '6.10.2' - modules: 'qtmultimedia qtserialport' + modules: 'qtmultimedia qtserialport qthttpserver qtwebsockets' - name: Install dependencies (Ubuntu) if: startsWith(inputs.os, 'ubuntu') diff --git a/SerialPrograms/CMakeLists.txt b/SerialPrograms/CMakeLists.txt index 9ffc2413ac..d116f3a7be 100644 --- a/SerialPrograms/CMakeLists.txt +++ b/SerialPrograms/CMakeLists.txt @@ -78,10 +78,10 @@ if(WIN32 AND QT_DEPLOY_FILES) if(QT_CANDIDATE_DIR AND EXISTS "${QT_CANDIDATE_DIR}") message(STATUS "Using preferred Qt directory for Qt${QT_MAJOR} ${PREFERRED_QT_VER}: ${QT_CANDIDATE_DIR}") list(APPEND CMAKE_PREFIX_PATH "${QT_CANDIDATE_DIR}") - find_package(Qt${QT_MAJOR} ${PREFERRED_QT_VER} COMPONENTS Widgets SerialPort Multimedia MultimediaWidgets REQUIRED) + find_package(Qt${QT_MAJOR} ${PREFERRED_QT_VER} COMPONENTS Widgets SerialPort Multimedia MultimediaWidgets HttpServer WebSockets REQUIRED) else() # Find all subdirectories in the Qt base directory - find_package(Qt${QT_MAJOR} COMPONENTS Widgets SerialPort Multimedia MultimediaWidgets REQUIRED) + find_package(Qt${QT_MAJOR} COMPONENTS Widgets SerialPort Multimedia MultimediaWidgets HttpServer WebSockets REQUIRED) file(GLOB QT_SUBDIRS LIST_DIRECTORIES true "${QT_BASE_DIR}/${QT_MAJOR}*") # Filter and sort the directories to find the latest version @@ -109,7 +109,7 @@ if(WIN32 AND QT_DEPLOY_FILES) REQUIRED ) else() - find_package(Qt${QT_MAJOR} COMPONENTS Widgets SerialPort Multimedia MultimediaWidgets REQUIRED) + find_package(Qt${QT_MAJOR} COMPONENTS Widgets SerialPort Multimedia MultimediaWidgets HttpServer WebSockets REQUIRED) endif() # disable deprecated Qt APIs @@ -188,7 +188,7 @@ endif() # Function to apply common properties to both library and executable targets function(apply_common_target_properties target_name) set_target_properties(${target_name} PROPERTIES LINKER_LANGUAGE CXX) - target_link_libraries(${target_name} PRIVATE Qt${QT_MAJOR}::Widgets Qt${QT_MAJOR}::SerialPort Qt${QT_MAJOR}::Multimedia Qt${QT_MAJOR}::MultimediaWidgets) + target_link_libraries(${target_name} PRIVATE Qt${QT_MAJOR}::Widgets Qt${QT_MAJOR}::SerialPort Qt${QT_MAJOR}::Multimedia Qt${QT_MAJOR}::MultimediaWidgets Qt${QT_MAJOR}::HttpServer Qt${QT_MAJOR}::WebSockets) target_link_libraries(${target_name} PRIVATE Threads::Threads) #add defines diff --git a/SerialPrograms/Scripts/test_websocket.py b/SerialPrograms/Scripts/test_websocket.py new file mode 100755 index 0000000000..748f5ac00a --- /dev/null +++ b/SerialPrograms/Scripts/test_websocket.py @@ -0,0 +1,28 @@ +import asyncio +import websockets +import io +from PIL import Image +import numpy as np +import cv2 + +WS_URL = "ws://127.0.0.1:8081" +async def handle_ws(): + async with websockets.connect(WS_URL) as ws: + print(f"Connected to {WS_URL}") + while True: + try: + message = await ws.recv() + if isinstance(message, str): + print(message) + elif isinstance(message, bytes): + try: + image = Image.open(io.BytesIO(message)) + cv_image = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR) + cv2.imshow("WebSocket Image", cv_image) + cv2.waitKey(1) + except Exception as e: + print(e) + except websockets.ConnectionClosed: + break + +asyncio.run(handle_ws()) \ No newline at end of file diff --git a/SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.cpp b/SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.cpp index 0305055e1e..bc61bc9455 100644 --- a/SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.cpp +++ b/SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.cpp @@ -27,6 +27,7 @@ #include "CommonFramework/VideoPipeline/VideoPipelineOptions.h" #include "CommonFramework/ErrorReports/ErrorReports.h" #include "Integrations/DiscordSettingsOption.h" +#include "CommonFramework/Options/Environment/APIOptions.h" //#include "CommonFramework/Environment/Environment.h" #include "GlobalSettingsPanel.h" @@ -241,6 +242,7 @@ GlobalSettings::GlobalSettings() LockMode::LOCK_WHILE_RUNNING, "", "" ) + , API(CONSTRUCT_TOKEN) { PA_ADD_OPTION(OPEN_BASE_FOLDER_BUTTON); PA_ADD_OPTION(CHECK_FOR_UPDATES); @@ -285,6 +287,7 @@ GlobalSettings::GlobalSettings() #endif PA_ADD_OPTION(DEVELOPER_TOKEN); + PA_ADD_OPTION(API); GlobalSettings::on_config_value_changed(this); ENABLE_LIFETIME_SANITIZER0.add_listener(*this); diff --git a/SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.h b/SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.h index 52a6e30946..85b26ec1e3 100644 --- a/SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.h +++ b/SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.h @@ -37,6 +37,7 @@ class PerformanceOptions; class AudioPipelineOptions; class VideoPipelineOptions; class ErrorReportOption; +class APIOptions; @@ -156,6 +157,8 @@ class GlobalSettings : public BatchOption, private ConfigOption::Listener, priva StringOption DEVELOPER_TOKEN; + Pimpl API; + // The mode that does not run Qt GUI, but instead runs some tests for // debugging, unit testing and developing purposes. bool COMMAND_LINE_TEST_MODE = false; diff --git a/SerialPrograms/Source/CommonFramework/Main.cpp b/SerialPrograms/Source/CommonFramework/Main.cpp index 2435ccea78..f06b314029 100644 --- a/SerialPrograms/Source/CommonFramework/Main.cpp +++ b/SerialPrograms/Source/CommonFramework/Main.cpp @@ -37,6 +37,7 @@ #include "ControllerInput/ControllerInput.h" #include "Integrations/DiscordWebhook.h" #include "Windows/MainWindow.h" +#include "Server/InitialiseServer.h" #include using std::cout; @@ -191,6 +192,9 @@ int run_program(int argc, char *argv[]){ w.raise(); // bring the window to front on macOS set_permissions(w); + // Start HTTP/WS Server if enabled + Server::init_server(); + return application.exec(); } diff --git a/SerialPrograms/Source/CommonFramework/Options/Environment/APIOptions.cpp b/SerialPrograms/Source/CommonFramework/Options/Environment/APIOptions.cpp new file mode 100644 index 0000000000..2b9850a661 --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/Options/Environment/APIOptions.cpp @@ -0,0 +1,48 @@ +/* API Options + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "APIOptions.h" +#include "CommonFramework/Panels/PanelTools.h" + +namespace PokemonAutomation{ + +APIOptions::APIOptions() + : GroupOption( + "API", + LockMode::LOCK_WHILE_RUNNING, + GroupOption::EnableMode::ALWAYS_ENABLED, true + ) + , DESCRIPTION( + "Note: The API is experimental and not all features are fully implemented.

" + "Warning: The API is not secure and should not be exposed to the internet.

" + "After changing these settings, you must restart the program for them to take effect." + ) + , ENABLE_API( + "Enable API:
" + "Enable the HTTP API and WebSockets to control the program remotely.
", + LockMode::UNLOCK_WHILE_RUNNING, + true + ) + , HTTP_PORT( + "HTTP Port:
" + "This can't be the same as the WebSocket Port
", + LockMode::UNLOCK_WHILE_RUNNING, + 8080, 1025, 65535 + ) + , WS_PORT( + "WebSocket Port:
" + "This can't be the same as the HTTP Port
", + LockMode::UNLOCK_WHILE_RUNNING, + 8081, 1025, 65535 + ) +{ + PA_ADD_STATIC(DESCRIPTION); + PA_ADD_OPTION(ENABLE_API); + PA_ADD_OPTION(HTTP_PORT); + PA_ADD_OPTION(WS_PORT); +} + +} diff --git a/SerialPrograms/Source/CommonFramework/Options/Environment/APIOptions.h b/SerialPrograms/Source/CommonFramework/Options/Environment/APIOptions.h new file mode 100644 index 0000000000..37d293702e --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/Options/Environment/APIOptions.h @@ -0,0 +1,30 @@ +/* API Options + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_APIOptions_H +#define PokemonAutomation_APIOptions_H + +#include "Common/Cpp/Options/GroupOption.h" +#include "Common/Cpp/Options/BooleanCheckBoxOption.h" +#include "Common/Cpp/Options/SimpleIntegerOption.h" +#include "Common/Cpp/Options/StaticTextOption.h" + +namespace PokemonAutomation{ + +class APIOptions : public GroupOption{ +public: + APIOptions(); + +public: + StaticTextOption DESCRIPTION; + BooleanCheckBoxOption ENABLE_API; + SimpleIntegerOption HTTP_PORT; + SimpleIntegerOption WS_PORT; +}; + +} + +#endif diff --git a/SerialPrograms/Source/CommonFramework/Panels/PanelList.h b/SerialPrograms/Source/CommonFramework/Panels/PanelList.h index 1f588b2861..920768891f 100644 --- a/SerialPrograms/Source/CommonFramework/Panels/PanelList.h +++ b/SerialPrograms/Source/CommonFramework/Panels/PanelList.h @@ -28,6 +28,7 @@ class PanelListDescriptor{ bool enabled() const{ return m_enabled; } PanelListWidget* make_QWidget(QWidget& parent, PanelHolder& holder) const; + std::vector get_panels() const { return make_panels(); } protected: virtual std::vector make_panels() const = 0; diff --git a/SerialPrograms/Source/CommonFramework/Panels/ProgramRegistry.cpp b/SerialPrograms/Source/CommonFramework/Panels/ProgramRegistry.cpp new file mode 100644 index 0000000000..6d34729a8c --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/Panels/ProgramRegistry.cpp @@ -0,0 +1,20 @@ +/* Program Registry + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "ProgramRegistry.h" + +namespace PokemonAutomation{ + +ProgramRegistry& ProgramRegistry::instance(){ + static ProgramRegistry instance; + return instance; +} + +void ProgramRegistry::add_category(std::unique_ptr category){ + m_categories.emplace_back(std::move(category)); +} + +} diff --git a/SerialPrograms/Source/CommonFramework/Panels/ProgramRegistry.h b/SerialPrograms/Source/CommonFramework/Panels/ProgramRegistry.h new file mode 100644 index 0000000000..9464664355 --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/Panels/ProgramRegistry.h @@ -0,0 +1,33 @@ +/* Program Registry + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_ProgramRegistry_H +#define PokemonAutomation_ProgramRegistry_H + +#include +#include +#include +#include "CommonFramework/Panels/PanelList.h" + +namespace PokemonAutomation{ + +class ProgramRegistry{ +public: + static ProgramRegistry& instance(); + + void add_category(std::unique_ptr category); + const std::vector>& categories() const { return m_categories; } + +private: + ProgramRegistry() = default; + +private: + std::vector> m_categories; +}; + +} + +#endif diff --git a/SerialPrograms/Source/CommonFramework/Server/API.md b/SerialPrograms/Source/CommonFramework/Server/API.md new file mode 100644 index 0000000000..ca586d9121 --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/Server/API.md @@ -0,0 +1,156 @@ +# SerialPrograms API Documentation + +> [!WARNING] +> This API is still under development and not all features are fully implemented. + +[](https://discord.gg/cQ4gWxN) + +## Configuration + + +| Setting | Description | +| :--- |:----------------------------------------------------| +| **Enable API** | Enable the API in the Settings (Default: Disabled) | +| **HTTP Port** | Configurable (Default: `8080`, Range: `1025-65535`) | +| **WebSocket Port** | Configurable (Default: `8081`, Range: `1025-65535`) | + +> [!IMPORTANT] +> The HTTP Port and WebSocket Port must be different. + +--- + +## HTTP + +> [!TIP] +> Sending a POST request to either the global settings or a program's settings will not update the setting if the relevant panel is currently open in the desktop application. + +--- + +### Settings + +#### `GET /settings` + +Returns the current global settings. + +**Response Body:** +```json +{ + "API": { + "ENABLE_API": false, + "HTTP_PORT": 8080, + "WS_PORT": 8081 + } +} +``` + +--- + +#### `POST /settings` + +Updates the global settings. Only the provided fields will be updated. + +**Request Body:** +```json +{ + "API": { + "ENABLE_API": true + } +} +``` + +--- + +### Programs + +#### `GET /programs` + +Returns a list of all program categories. + +**Response Body:** +```json +[ + { + "display_name": "Nintendo Switch", + "slug": "nintendo-switch" + } +] +``` + +--- + +#### `GET /programs/{category_slug}` + +Returns a list of all programs within a specific category. + +**Response Body:** +```json +[ + { + "description": "Endlessly mash A.", + "display_name": "Turbo A", + "slug": "turbo-a" + } +] +``` + +--- + +#### `GET /programs/{category_slug}/{program_slug}` + +Returns details for a specific program. + +**Response Body:** +```json +{ + "category": "Nintendo Switch", + "description": "Endlessly mash A.", + "display_name": "Turbo A", + "slug": "turbo-a" +} +``` + +--- + +#### `GET /programs/{category_slug}/{program_slug}/options` + +Returns the current configuration options for a specific program. + +**Response Body:** +```json +{ + "GO_HOME_WHEN_DONE": false, + "START_LOCATION": "in-game", + "SwitchSetup": {...}, + "TIME_LIMIT": "0 s" +} +``` + +--- + +#### `POST /programs/{category_slug}/{program_slug}/options` + +Updates the configuration options for a specific program. Only the provided fields will be updated. + +**Request Body:** +```json +{ + "GO_HOME_WHEN_DONE": true +} +``` + +--- + +## WebSocket + +--- + +### Binary Messages + +The WebSocket server broadcasts real-time video frames from the capture card. These are sent as binary messages containing a JPEG-encoded byte array. + +### Text Messages + +The logs shown in the Output Window are sent as text messages. + +> [!WARNING] +> Not yet implemented. \ No newline at end of file diff --git a/SerialPrograms/Source/CommonFramework/Server/HTTP.cpp b/SerialPrograms/Source/CommonFramework/Server/HTTP.cpp new file mode 100644 index 0000000000..6424a928bc --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/Server/HTTP.cpp @@ -0,0 +1,85 @@ +/* HTTP Server + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "HTTP.h" +#include "Common/Cpp/Logging/TaggedLogger.h" +#include "CommonFramework/Logging/Logger.h" + +namespace PokemonAutomation +{ + namespace Server + { + HTTPServer::HTTPServer() + : m_logger([]() -> TaggedLogger& { + static TaggedLogger logger(global_logger_raw(), "HTTP"); + return logger; + }()) + , m_server(new QHttpServer()) + , m_tcpServer(nullptr) + {} + + HTTPServer::~HTTPServer() + { + stop(); + delete m_server; + } + + bool HTTPServer::start(quint16 port) + { + if (m_tcpServer) + { + m_logger.log("HTTP Server already running", COLOR_YELLOW); + return false; + } + + // Init HTTP Server + m_tcpServer = new QTcpServer(); + + if (!m_tcpServer->listen(QHostAddress::Any, port)) + { + m_logger.log("Failed to start TCP server", COLOR_RED); + return false; + } + if (!m_server->bind(m_tcpServer)) + { + m_logger.log("Failed to bind TCP server", COLOR_RED); + return false; + } + + m_logger.log(("HTTP Server started on port " + std::to_string(port)).c_str(), COLOR_GREEN); + return true; + } + + void HTTPServer::stop() + { + if (m_tcpServer) + { + m_tcpServer->close(); + delete m_tcpServer; + m_tcpServer = nullptr; + m_logger.log("HTTP Server stopped", COLOR_GREEN); + } + } + + void HTTPServer::addRoute( + const QString& path, + QHttpServerRequest::Method method, + std::function handler + ) + { + if (!m_server) + { + m_logger.log("Failed to add route, HTTP Server not initialised", COLOR_RED); + return; + } + + m_server->route(path, method, [handler](const QHttpServerRequest& req, QHttpServerResponder& res) + { + handler(req, res); + }); + } + } +} diff --git a/SerialPrograms/Source/CommonFramework/Server/HTTP.h b/SerialPrograms/Source/CommonFramework/Server/HTTP.h new file mode 100644 index 0000000000..3a09183ac4 --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/Server/HTTP.h @@ -0,0 +1,67 @@ +/* HTTP Server + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef SERIALPROGRAMS_HTTP_H +#define SERIALPROGRAMS_HTTP_H + +#pragma once + +#include +#include +#include "CommonFramework/Logging/Logger.h" + + +namespace PokemonAutomation +{ + namespace Server + { + class HTTPServer + { + public: + static HTTPServer& instance() + { + static HTTPServer instance; + return instance; + } + + bool start(quint16 port = 8080); + void stop(); + + void addRoute( + const QString& path, + QHttpServerRequest::Method method, + std::function handler + ); + + template + void addRoute( + const QString& path, + QHttpServerRequest::Method method, + Handler&& handler + ) + { + if (!m_server) + { + m_logger.log("Failed to add route, HTTP Server not initialised", COLOR_RED); + return; + } + + m_server->route(path, method, std::forward(handler)); + } + + private: + explicit HTTPServer(); + ~HTTPServer(); + + Logger& m_logger; + QHttpServer* m_server; + QTcpServer* m_tcpServer; + }; + } +} + + +#endif diff --git a/SerialPrograms/Source/CommonFramework/Server/InitialiseServer.h b/SerialPrograms/Source/CommonFramework/Server/InitialiseServer.h new file mode 100644 index 0000000000..306a2d64f7 --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/Server/InitialiseServer.h @@ -0,0 +1,39 @@ +/* Routes registration + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_Server_Routes_H +#define PokemonAutomation_Server_Routes_H + +#include "CommonFramework/GlobalSettingsPanel.h" +#include "CommonFramework/Server/HTTP.h" +#include "CommonFramework/Server/WebSocket.h" +#include "CommonFramework/Options/Environment/APIOptions.h" +#include "Routes/ProgramRoutes.h" +#include "Routes/GlobalSettingsRoutes.h" + +namespace PokemonAutomation{ +namespace Server{ + +inline void init_server(){ + if (GlobalSettings::instance().API->ENABLE_API){ + // Start HTTP Server + HTTPServer& httpServer = HTTPServer::instance(); + httpServer.start(GlobalSettings::instance().API->HTTP_PORT); + + // Register all HTTP routes + register_program_routes(); + register_settings_routes(); + + // Start WebSocket Server + WSServer& wsServer = WSServer::instance(); + wsServer.start(GlobalSettings::instance().API->WS_PORT); + } +} + +} +} + +#endif diff --git a/SerialPrograms/Source/CommonFramework/Server/RouteUtils.h b/SerialPrograms/Source/CommonFramework/Server/RouteUtils.h new file mode 100644 index 0000000000..c2caaf209b --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/Server/RouteUtils.h @@ -0,0 +1,40 @@ +/* Route Utils + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_Server_RouteUtils_H +#define PokemonAutomation_Server_RouteUtils_H + +#include +#include +#include + +namespace PokemonAutomation{ +namespace Server{ + +inline QString to_slug(const std::string& name) { + QString slug = QString::fromStdString(name); + // Normalise to NFD (Decomposition) to remove accents (é to e, etc...) + slug = slug.normalized(QString::NormalizationForm_D); + slug.remove(QRegularExpression("\\p{M}")); + + // Back to NFC for any further processing if needed, though not strictly required + slug = slug.normalized(QString::NormalizationForm_C); + + // Reformat non-alphanumeric characters so "Program (1.0.0)" -> "program-1-0-0" + slug = slug.toLower(); + slug.replace(QRegularExpression("[^a-z0-9]"), "-"); + while (slug.contains("--")) { + slug.replace("--", "-"); + } + if (slug.startsWith("-")) slug.remove(0, 1); + if (slug.endsWith("-")) slug.remove(slug.length() - 1, 1); + return slug; +} + +} +} + +#endif diff --git a/SerialPrograms/Source/CommonFramework/Server/Routes/GlobalSettingsRoutes.cpp b/SerialPrograms/Source/CommonFramework/Server/Routes/GlobalSettingsRoutes.cpp new file mode 100644 index 0000000000..7d3c358f92 --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/Server/Routes/GlobalSettingsRoutes.cpp @@ -0,0 +1,56 @@ +/* Global Settings Routes + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include "Common/Cpp/Json/JsonValue.h" +#include "Common/Cpp/Json/JsonObject.h" +#include "Common/Cpp/Json/JsonTools.h" +#include "CommonFramework/GlobalSettingsPanel.h" +#include "CommonFramework/PersistentSettings.h" +#include "CommonFramework/Server/HTTP.h" +#include "GlobalSettingsRoutes.h" + +namespace PokemonAutomation{ +namespace Server{ + +void register_settings_routes(){ + HTTPServer& server = HTTPServer::instance(); + + // GET /settings + server.addRoute( + "/settings", + QHttpServerRequest::Method::Get, + [](const QHttpServerRequest&, + QHttpServerResponder& responder + ){ + JsonValue json = GlobalSettings::instance().to_json(); + responder.write(QJsonDocument(to_QJson(json).toObject())); + }); + + // POST /settings + server.addRoute( + "/settings", + QHttpServerRequest::Method::Post, + [](const QHttpServerRequest& request, + QHttpServerResponder& responder + ){ + QJsonDocument doc = QJsonDocument::fromJson(request.body()); + if (doc.isNull() || !doc.isObject()){ + responder.write(QHttpServerResponder::StatusCode::BadRequest); + return; + } + + GlobalSettings::instance().load_json(from_QJson(doc.object())); + PERSISTENT_SETTINGS().write(); + + responder.write(QHttpServerResponder::StatusCode::Ok); + }); +} + +} +} diff --git a/SerialPrograms/Source/CommonFramework/Server/Routes/GlobalSettingsRoutes.h b/SerialPrograms/Source/CommonFramework/Server/Routes/GlobalSettingsRoutes.h new file mode 100644 index 0000000000..7b34eb5dc7 --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/Server/Routes/GlobalSettingsRoutes.h @@ -0,0 +1,18 @@ +/* Global Settings Routes + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_GlobalSettingsRoutes_H +#define PokemonAutomation_GlobalSettingsRoutes_H + +namespace PokemonAutomation{ +namespace Server{ + +void register_settings_routes(); + +} +} + +#endif diff --git a/SerialPrograms/Source/CommonFramework/Server/Routes/ProgramRoutes.cpp b/SerialPrograms/Source/CommonFramework/Server/Routes/ProgramRoutes.cpp new file mode 100644 index 0000000000..26b7a96fb9 --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/Server/Routes/ProgramRoutes.cpp @@ -0,0 +1,221 @@ +/* Program Routes + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include +#include +#include +#include "Common/Cpp/Json/JsonValue.h" +#include "Common/Cpp/Json/JsonTools.h" +#include "CommonFramework/Server/HTTP.h" +#include "CommonFramework/Panels/ProgramRegistry.h" +#include "CommonFramework/Panels/PanelDescriptor.h" +#include "CommonFramework/Panels/PanelInstance.h" +#include "CommonFramework/Server/RouteUtils.h" +#include "ProgramRoutes.h" + +namespace PokemonAutomation{ +namespace Server{ + +void register_program_routes(){ + HTTPServer& server = HTTPServer::instance(); + + // GET /programs + server.addRoute( + "/programs", + QHttpServerRequest::Method::Get, + [](const QHttpServerRequest&, + QHttpServerResponder& responder + ){ + QJsonArray json_categories; + for (const auto& category : ProgramRegistry::instance().categories()){ + QJsonObject category_obj; + category_obj["display_name"] = QString::fromStdString(category->name()); + category_obj["slug"] = to_slug(category->name()); + json_categories.append(category_obj); + } + responder.write(QJsonDocument(json_categories)); + }); + + // GET /programs/{category_slug} + server.addRoute( + "/programs/", + QHttpServerRequest::Method::Get, + [](QString category_slug, + const QHttpServerRequest&, + QHttpServerResponder& responder + ){ + const auto& all_categories = ProgramRegistry::instance().categories(); + + QString decoded_category_slug = QUrl::fromPercentEncoding(category_slug.toUtf8()); + + PanelListDescriptor* found_category = nullptr; + for (const auto& category : all_categories){ + QString category_name = QString::fromStdString(category->name()); + QString category_to_slug = to_slug(category->name()); + + if (category_name.compare(decoded_category_slug, Qt::CaseInsensitive) == 0 || + category_to_slug.compare(category_slug, Qt::CaseInsensitive) == 0 || + category_to_slug.compare(decoded_category_slug, Qt::CaseInsensitive) == 0) { + found_category = category.get(); + break; + } + } + + if (!found_category){ + responder.write(QHttpServerResponder::StatusCode::NotFound); + return; + } + + QJsonArray json_programs; + for (const auto& program : found_category->get_panels()){ + if (program.descriptor){ + QJsonObject program_obj; + program_obj["slug"] = to_slug(program.descriptor->display_name()); + program_obj["display_name"] = QString::fromStdString(program.descriptor->display_name()); + program_obj["description"] = QString::fromStdString(program.descriptor->description()); + json_programs.append(program_obj); + } + } + responder.write(QJsonDocument(json_programs)); + }); + + // GET /programs/{category_slug}/{program_slug} + server.addRoute( + "/programs//", + QHttpServerRequest::Method::Get, + [](QString category_slug, + QString program_slug, + const QHttpServerRequest&, + QHttpServerResponder& responder + ){ + const auto& all_categories = ProgramRegistry::instance().categories(); + + QString decoded_category_slug = QUrl::fromPercentEncoding(category_slug.toUtf8()); + QString decoded_program_slug = QUrl::fromPercentEncoding(program_slug.toUtf8()); + + PanelListDescriptor* found_category = nullptr; + for (const auto& category : all_categories){ + QString category_name = QString::fromStdString(category->name()); + QString category_to_slug = to_slug(category->name()); + + if (category_name.compare(decoded_category_slug, Qt::CaseInsensitive) == 0 || + category_to_slug.compare(category_slug, Qt::CaseInsensitive) == 0 || + category_to_slug.compare(decoded_category_slug, Qt::CaseInsensitive) == 0) { + found_category = category.get(); + break; + } + } + + if (!found_category){ + responder.write(QHttpServerResponder::StatusCode::NotFound); + return; + } + + for (const auto& program : found_category->get_panels()){ + if (program.descriptor){ + QString program_name = QString::fromStdString(program.descriptor->display_name()); + QString program_to_slug = to_slug(program.descriptor->display_name()); + + if (program_name.compare(decoded_program_slug, Qt::CaseInsensitive) == 0 || + program_to_slug.compare(program_slug, Qt::CaseInsensitive) == 0 || + program_to_slug.compare(decoded_program_slug, Qt::CaseInsensitive) == 0) { + + QJsonObject program_obj; + program_obj["slug"] = program_to_slug; + program_obj["display_name"] = program_name; + program_obj["description"] = QString::fromStdString(program.descriptor->description()); + program_obj["category"] = QString::fromStdString(program.descriptor->category()); + + responder.write(QJsonDocument(program_obj)); + return; + } + } + } + + responder.write(QHttpServerResponder::StatusCode::NotFound); + }); + + // GET /programs/{category_slug}/{program_slug}/options + server.addRoute( + "/programs///options", + QHttpServerRequest::Method::Get, + [](QString category_slug, + QString program_slug, + const QHttpServerRequest&, + QHttpServerResponder& responder + ){ + const auto& all_categories = ProgramRegistry::instance().categories(); + QString decoded_category_slug = QUrl::fromPercentEncoding(category_slug.toUtf8()); + QString decoded_program_slug = QUrl::fromPercentEncoding(program_slug.toUtf8()); + + for (const auto& category : all_categories){ + if (QString::fromStdString(category->name()).compare(decoded_category_slug, Qt::CaseInsensitive) == 0 || + to_slug(category->name()).compare(category_slug, Qt::CaseInsensitive) == 0) { + + for (const auto& program : category->get_panels()){ + if (program.descriptor && (QString::fromStdString(program.descriptor->display_name()).compare(decoded_program_slug, Qt::CaseInsensitive) == 0 || + to_slug(program.descriptor->display_name()).compare(program_slug, Qt::CaseInsensitive) == 0)) { + + std::unique_ptr instance = program.descriptor->make_panel(); + // Load saved settings + instance->from_json(); + QJsonValue qjson = to_QJson(instance->to_json()); + responder.write(QJsonDocument(qjson.toObject())); + return; + } + } + } + } + responder.write(QHttpServerResponder::StatusCode::NotFound); + }); + + // POST /programs/{category_slug}/{program_slug}/options + server.addRoute( + "/programs///options", + QHttpServerRequest::Method::Post, + [](QString category_slug, + QString program_slug, + const QHttpServerRequest& request, + QHttpServerResponder& responder + ){ + const auto& all_categories = ProgramRegistry::instance().categories(); + QString decoded_category_slug = QUrl::fromPercentEncoding(category_slug.toUtf8()); + QString decoded_program_slug = QUrl::fromPercentEncoding(program_slug.toUtf8()); + + for (const auto& category : all_categories){ + if (QString::fromStdString(category->name()).compare(decoded_category_slug, Qt::CaseInsensitive) == 0 || + to_slug(category->name()).compare(category_slug, Qt::CaseInsensitive) == 0) { + + for (const auto& program : category->get_panels()){ + if (program.descriptor && (QString::fromStdString(program.descriptor->display_name()).compare(decoded_program_slug, Qt::CaseInsensitive) == 0 || + to_slug(program.descriptor->display_name()).compare(program_slug, Qt::CaseInsensitive) == 0)) { + + QJsonDocument doc = QJsonDocument::fromJson(request.body()); + if (doc.isNull() || !doc.isObject()){ + responder.write(QHttpServerResponder::StatusCode::BadRequest); + return; + } + + std::unique_ptr instance = program.descriptor->make_panel(); + instance->from_json(); // Load first to preserve other options not in the POST body + instance->from_json(from_QJson(doc.object())); + instance->save_settings(); + + responder.write(QHttpServerResponder::StatusCode::Ok); + return; + } + } + } + } + responder.write(QHttpServerResponder::StatusCode::NotFound); + }); +} + +} +} diff --git a/SerialPrograms/Source/CommonFramework/Server/Routes/ProgramRoutes.h b/SerialPrograms/Source/CommonFramework/Server/Routes/ProgramRoutes.h new file mode 100644 index 0000000000..02111ef3bb --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/Server/Routes/ProgramRoutes.h @@ -0,0 +1,18 @@ +/* Program Routes + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_ProgramRoutes_H +#define PokemonAutomation_ProgramRoutes_H + +namespace PokemonAutomation{ +namespace Server{ + +void register_program_routes(); + +} +} + +#endif diff --git a/SerialPrograms/Source/CommonFramework/Server/Sockets/VideoWS.h b/SerialPrograms/Source/CommonFramework/Server/Sockets/VideoWS.h new file mode 100644 index 0000000000..13676b623b --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/Server/Sockets/VideoWS.h @@ -0,0 +1,43 @@ +/* Video WebSocket Server + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef SERIALPROGRAMS_VIDEOWS_H +#define SERIALPROGRAMS_VIDEOWS_H + +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonFramework/VideoPipeline/VideoSession.h" +#include "CommonFramework/Server/WebSocket.h" + +namespace PokemonAutomation +{ + namespace Server + { + class VideoWSServer : public VideoFrameListener + { + public: + static VideoWSServer& instance() + { + static VideoWSServer instance; + return instance; + } + + // Send frames to all connected clients + virtual void on_frame(std::shared_ptr frame) override + { + if (WSServer::instance().hasClients()) + { + QByteArray frameData = frame_to_jpeg(*frame); + WSServer::instance().send_binary(frameData); + } + } + + private: + VideoWSServer() = default; + }; + } +} + +#endif diff --git a/SerialPrograms/Source/CommonFramework/Server/WebSocket.cpp b/SerialPrograms/Source/CommonFramework/Server/WebSocket.cpp new file mode 100644 index 0000000000..3db1c0e79e --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/Server/WebSocket.cpp @@ -0,0 +1,167 @@ +/* WebSocket Server + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "WebSocket.h" +#include "Common/Cpp/Logging/TaggedLogger.h" +#include "CommonFramework/Logging/Logger.h" + +namespace PokemonAutomation +{ + namespace Server + { + WSServer::WSServer(QObject* parent) + : QObject(parent) + , m_logger([]() -> TaggedLogger& { + static TaggedLogger logger(global_logger_raw(), "WS"); + return logger; + }()) + , m_server(nullptr) + {} + + WSServer::~WSServer() + { + stop(); + } + + // Start WebSocket Server + bool WSServer::start(quint16 port) + { + if (m_server) return true; // Already running + + m_server = new QWebSocketServer( + QStringLiteral("SerialPrograms WebSocket Server"), + QWebSocketServer::NonSecureMode, // Change later? + this + ); + + // Attempt to bind port + if (!m_server->listen(QHostAddress::Any, port)) + { + m_logger.log("Failed to start WebSocket Server", COLOR_RED); + return false; + } + + connect(m_server, &QWebSocketServer::newConnection, + this, &WSServer::clientConnection); + + m_logger.log(("WebSocket Server started on port " + std::to_string(port)).c_str(), COLOR_GREEN); + emit serverStarted(port); + return true; + } + + // Stop WebSocket Server + void WSServer::stop() + { + if (!m_server) return; + + // Clean up clients + for (auto* client : m_clients) + { + client->close(); + client->deleteLater(); + } + m_clients.clear(); + + m_server->close(); + m_server->deleteLater(); + m_server = nullptr; + + m_logger.log("WebSocket Server stopped", COLOR_GREEN); + emit serverStopped(); + } + + void WSServer::clientConnection() + { + QWebSocket* client = m_server->nextPendingConnection(); + if (!client) return; + + m_clients.append(client); + connect(client, &QWebSocket::disconnected, this, + &WSServer::clientDisconnection); + + m_logger.log("WebSocket client connected", COLOR_GREEN); + + emit clientConnected(client); + } + + void WSServer::clientDisconnection() + { + QWebSocket* client = qobject_cast(sender()); + if (!client) return; + + m_clients.removeAll(client); + client->deleteLater(); + + m_logger.log("WebSocket client disconnected", COLOR_GREEN); + + emit clientDisconnected(client); + } + + void WSServer::handleMessage(const QString& message) + { + QWebSocket* client = qobject_cast(sender()); + if (!client) return; + + emit messageReceived(client, message); + } + + void WSServer::handleBinary(const QByteArray& data) + { + QWebSocket* client = qobject_cast(sender()); + if (!client) return; + + emit binaryReceived(client, data); + } + + // Send text message to a specific client - Currently not much use for this, but still here just incase + void WSServer::send_message(QWebSocket* client, const QString& message) + { + if (!client) return; + + QMetaObject::invokeMethod(client, [client, message]() + { + if (client->isValid()) + client->sendTextMessage(message); + }, Qt::QueuedConnection); + } + + // Send binary message to a specific client - Currently not much use for this, but still here just incase + void WSServer::send_binary(QWebSocket* client, const QByteArray& data) + { + if (!client) return; + + QMetaObject::invokeMethod(client, [client, data]() + { + if (client->isValid()) + client->sendBinaryMessage(data); + }, Qt::QueuedConnection); + } + + // Send text message to all clients + void WSServer::send_message(const QString& message) + { + for (auto* client : m_clients) + { + QMetaObject::invokeMethod(client, [client, message]() { + if (client->isValid()) + client->sendTextMessage(message); + }, Qt::QueuedConnection); + } + } + + // Send binary message to all clients + void WSServer::send_binary(const QByteArray& data) + { + for (auto* client : m_clients) + { + QMetaObject::invokeMethod(client, [client, data]() { + if (client->isValid()) + client->sendBinaryMessage(data); + }, Qt::QueuedConnection); + } + } + } +} diff --git a/SerialPrograms/Source/CommonFramework/Server/WebSocket.h b/SerialPrograms/Source/CommonFramework/Server/WebSocket.h new file mode 100644 index 0000000000..51612ff398 --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/Server/WebSocket.h @@ -0,0 +1,77 @@ +/* WebSocket Server + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef SERIALPROGRAMS_WEBSOCKET_H +#define SERIALPROGRAMS_WEBSOCKET_H + +#pragma once + +#include +#include +#include +#include +#include "CommonFramework/Logging/Logger.h" + + +namespace PokemonAutomation +{ + namespace Server + { + class WSServer : public QObject + { + Q_OBJECT + public: + static WSServer& instance() + { + static WSServer instance; + return instance; + } + + // Start/Stop + bool start(quint16 port = 8081); + void stop(); + + // Send to all clients + void send_message(const QString& message); + void send_binary(const QByteArray& data); + + // Send to a specific client + void send_message(QWebSocket* client, const QString& message); + void send_binary(QWebSocket* client, const QByteArray& data); + + // Clients + qsizetype clientCount() const { return m_clients.size(); } + bool hasClients() const { return !m_clients.isEmpty(); } + + signals: + void clientConnected(QWebSocket* client); + void clientDisconnected(QWebSocket* client); + + void messageReceived(QWebSocket* client, const QString& message); + void binaryReceived(QWebSocket* client, const QByteArray& data); + + void serverStarted(quint16 port); + void serverStopped(); + + private slots: + void clientConnection(); + void clientDisconnection(); + void handleMessage(const QString& message); + void handleBinary(const QByteArray& data); + + private: + explicit WSServer(QObject* parent = nullptr); + ~WSServer(); + + Logger& m_logger; + QWebSocketServer* m_server; + QList m_clients; + }; + } +} + + +#endif //SERIALPROGRAMS_WEBSOCKET_H \ No newline at end of file diff --git a/SerialPrograms/Source/CommonFramework/VideoPipeline/VideoSession.cpp b/SerialPrograms/Source/CommonFramework/VideoPipeline/VideoSession.cpp index 340fcf00b0..6387dd7677 100644 --- a/SerialPrograms/Source/CommonFramework/VideoPipeline/VideoSession.cpp +++ b/SerialPrograms/Source/CommonFramework/VideoPipeline/VideoSession.cpp @@ -11,6 +11,8 @@ #include "Backends/VideoFrameQt.h" #include "VideoSources/VideoSource_Null.h" #include "VideoSession.h" +#include "CommonFramework/Server/WebSocket.h" +#include "CommonFramework/Server/Sockets/VideoWS.h" //#include //using std::cout; @@ -42,6 +44,7 @@ void VideoSession::remove_frame_listener(VideoFrameListener& listener){ bool VideoSession::try_shutdown(){ + remove_frame_listener(Server::VideoWSServer::instance()); if (m_video_source){ m_video_source->remove_source_frame_listener(*this); m_video_source->remove_rendered_frame_listener(*this); @@ -60,6 +63,7 @@ VideoSession::VideoSession(Logger& logger, VideoSourceOption& option) , m_option(option) , m_descriptor(std::make_unique()) { + add_frame_listener(Server::VideoWSServer::instance()); uint8_t watchdog_timeout = GlobalSettings::instance().VIDEO_PIPELINE->AUTO_RESET_SECONDS; if (watchdog_timeout != 0){ global_watchdog().add(*this, std::chrono::seconds(watchdog_timeout)); diff --git a/SerialPrograms/Source/CommonFramework/VideoPipeline/VideoSession.h b/SerialPrograms/Source/CommonFramework/VideoPipeline/VideoSession.h index 18d7d8a03f..26d521076c 100644 --- a/SerialPrograms/Source/CommonFramework/VideoPipeline/VideoSession.h +++ b/SerialPrograms/Source/CommonFramework/VideoPipeline/VideoSession.h @@ -10,14 +10,43 @@ #include #include #include +#include +#include +#include #include "Common/Cpp/EventRateTracker.h" #include "Common/Cpp/Concurrency/SpinLock.h" #include "Common/Cpp/Concurrency/Watchdog.h" #include "VideoSourceDescriptor.h" #include "VideoSource.h" +#include "Backends/VideoFrameQt.h" namespace PokemonAutomation{ +inline QByteArray frame_to_jpeg(const VideoFrame& vf) +{ + QVideoFrame qvf = vf.frame; + + // Map frame for reading + if (!qvf.map(QVideoFrame::ReadOnly)){ + return {}; + } + + // Convert to QImage + QImage img = qvf.toImage(); + qvf.unmap(); + + if (img.isNull()){ + return {}; + } + + // Encode to JPEG + QByteArray byteArray; + QBuffer buffer(&byteArray); + buffer.open(QIODevice::WriteOnly); + img.save(&buffer, "JPEG"); + return byteArray; +} + class VideoSession : public VideoFeed // interface class the automation programs have access to to query video snapshots diff --git a/SerialPrograms/Source/PanelLists.cpp b/SerialPrograms/Source/PanelLists.cpp index 1eb6c258af..f3906f4530 100644 --- a/SerialPrograms/Source/PanelLists.cpp +++ b/SerialPrograms/Source/PanelLists.cpp @@ -24,6 +24,7 @@ #include "PokemonSV/PokemonSV_Panels.h" #include "PokemonLZA/PokemonLZA_Panels.h" #include "ZeldaTotK/ZeldaTotK_Panels.h" +#include "CommonFramework/Panels/ProgramRegistry.h" #include "PanelLists.h" //#include @@ -46,24 +47,28 @@ ProgramSelect::ProgramSelect(QWidget& parent, PanelHolder& holder) layout->addWidget(m_dropdown); - add(std::make_unique()); - add(std::make_unique()); - add(std::make_unique()); - add(std::make_unique()); - add(std::make_unique()); - add(std::make_unique()); - add(std::make_unique()); + ProgramRegistry::instance().add_category(std::make_unique()); + ProgramRegistry::instance().add_category(std::make_unique()); + ProgramRegistry::instance().add_category(std::make_unique()); + ProgramRegistry::instance().add_category(std::make_unique()); + ProgramRegistry::instance().add_category(std::make_unique()); + ProgramRegistry::instance().add_category(std::make_unique()); + ProgramRegistry::instance().add_category(std::make_unique()); - add(std::make_unique()); - add(std::make_unique()); + ProgramRegistry::instance().add_category(std::make_unique()); + ProgramRegistry::instance().add_category(std::make_unique()); if (PreloadSettings::instance().DEVELOPER_MODE){ - add(std::make_unique()); + ProgramRegistry::instance().add_category(std::make_unique()); } - add(std::make_unique()); + ProgramRegistry::instance().add_category(std::make_unique()); if (PreloadSettings::instance().DEVELOPER_MODE){ - add(std::make_unique()); + ProgramRegistry::instance().add_category(std::make_unique()); + } + + for (auto& list : ProgramRegistry::instance().categories()){ + add(list.get()); } @@ -81,10 +86,10 @@ ProgramSelect::ProgramSelect(QWidget& parent, PanelHolder& holder) ); } -void ProgramSelect::add(std::unique_ptr list){ +void ProgramSelect::add(PanelListDescriptor* list){ int index = m_dropdown->count(); m_dropdown->addItem(QString::fromStdString(list->name())); - m_lists.emplace_back(std::move(list)); + m_lists.emplace_back(list); const PanelListDescriptor& back = *m_lists.back(); if (!m_tab_map.emplace(back.name(), index).second){ m_lists.pop_back(); diff --git a/SerialPrograms/Source/PanelLists.h b/SerialPrograms/Source/PanelLists.h index 04c7b31811..2b6d62c992 100644 --- a/SerialPrograms/Source/PanelLists.h +++ b/SerialPrograms/Source/PanelLists.h @@ -30,13 +30,13 @@ class ProgramSelect : public QGroupBox{ virtual QSize sizeHint() const override; private: - void add(std::unique_ptr list); + void add(PanelListDescriptor* list); void change_list(int index); private: PanelHolder& m_holder; - std::vector> m_lists; + std::vector m_lists; std::map m_tab_map; QComboBox* m_dropdown; diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index f0a89d4520..41b5f90345 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -439,6 +439,8 @@ file(GLOB LIBRARY_SOURCES Source/CommonFramework/Options/Environment/SleepSuppressOption.h Source/CommonFramework/Options/Environment/ThemeSelectorOption.cpp Source/CommonFramework/Options/Environment/ThemeSelectorOption.h + Source/CommonFramework/Options/Environment/APIOptions.cpp + Source/CommonFramework/Options/Environment/APIOptions.h Source/CommonFramework/Options/LabelCellOption.cpp Source/CommonFramework/Options/LabelCellOption.h Source/CommonFramework/Options/QtWidget/LabelCellWidget.cpp @@ -454,6 +456,8 @@ file(GLOB LIBRARY_SOURCES Source/CommonFramework/Panels/PanelInstance.cpp Source/CommonFramework/Panels/PanelInstance.h Source/CommonFramework/Panels/PanelList.cpp + Source/CommonFramework/Panels/ProgramRegistry.cpp + Source/CommonFramework/Panels/ProgramRegistry.h Source/CommonFramework/Panels/PanelList.h Source/CommonFramework/Panels/PanelTools.h Source/CommonFramework/Panels/ProgramDescriptor.cpp @@ -487,6 +491,15 @@ file(GLOB LIBRARY_SOURCES Source/CommonFramework/Recording/StreamHistoryTracker_SaveFrames.h Source/CommonFramework/Recording/StreamRecorder.cpp Source/CommonFramework/Recording/StreamRecorder.h + Source/CommonFramework/Server/Sockets/VideoWS.h + Source/CommonFramework/Server/HTTP.cpp + Source/CommonFramework/Server/HTTP.h + Source/CommonFramework/Server/Routes/ProgramRoutes.cpp + Source/CommonFramework/Server/Routes/ProgramRoutes.h + Source/CommonFramework/Server/Routes/GlobalSettingsRoutes.cpp + Source/CommonFramework/Server/Routes/GlobalSettingsRoutes.h + Source/CommonFramework/Server/WebSocket.cpp + Source/CommonFramework/Server/Websocket.h Source/CommonFramework/Startup/NewVersionCheck.cpp Source/CommonFramework/Startup/NewVersionCheck.h Source/CommonFramework/Startup/SetupSettings.cpp