From 68055fd7f8f4a73df8f04bd3db494ed471472794 Mon Sep 17 00:00:00 2001 From: ConnorC432 Date: Tue, 24 Mar 2026 11:08:51 +0000 Subject: [PATCH 01/24] HttpServer in CMakeLists.txt --- SerialPrograms/CMakeLists.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/SerialPrograms/CMakeLists.txt b/SerialPrograms/CMakeLists.txt index 9ffc2413a..bf22f6c11 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 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 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 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) target_link_libraries(${target_name} PRIVATE Threads::Threads) #add defines From b98a9cedd6464b36e3cf08696f0fed892d136c57 Mon Sep 17 00:00:00 2001 From: ConnorC432 Date: Tue, 24 Mar 2026 12:52:03 +0000 Subject: [PATCH 02/24] HTTP/WS server init --- .../Source/CommonFramework/Main.cpp | 8 + .../Source/CommonFramework/Server/HTTP.cpp | 84 +++++++++ .../Source/CommonFramework/Server/HTTP.h | 49 +++++ .../CommonFramework/Server/WebSocket.cpp | 171 ++++++++++++++++++ .../Source/CommonFramework/Server/WebSocket.h | 73 ++++++++ SerialPrograms/cmake/SourceFiles.cmake | 4 + 6 files changed, 389 insertions(+) create mode 100644 SerialPrograms/Source/CommonFramework/Server/HTTP.cpp create mode 100644 SerialPrograms/Source/CommonFramework/Server/HTTP.h create mode 100644 SerialPrograms/Source/CommonFramework/Server/WebSocket.cpp create mode 100644 SerialPrograms/Source/CommonFramework/Server/WebSocket.h diff --git a/SerialPrograms/Source/CommonFramework/Main.cpp b/SerialPrograms/Source/CommonFramework/Main.cpp index 2435ccea7..81ffb62e9 100644 --- a/SerialPrograms/Source/CommonFramework/Main.cpp +++ b/SerialPrograms/Source/CommonFramework/Main.cpp @@ -37,6 +37,8 @@ #include "ControllerInput/ControllerInput.h" #include "Integrations/DiscordWebhook.h" #include "Windows/MainWindow.h" +#include "Server/HTTP.h" +#include "Server/WebSocket.h" #include using std::cout; @@ -191,6 +193,12 @@ int run_program(int argc, char *argv[]){ w.raise(); // bring the window to front on macOS set_permissions(w); + Server::HTTPServer& httpServer = Server::HTTPServer::instance(); + httpServer.start(8080); + + Server::WSServer& wsServer = Server::WSServer::instance(); + wsServer.start(8081); + return application.exec(); } diff --git a/SerialPrograms/Source/CommonFramework/Server/HTTP.cpp b/SerialPrograms/Source/CommonFramework/Server/HTTP.cpp new file mode 100644 index 000000000..52319c9c1 --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/Server/HTTP.cpp @@ -0,0 +1,84 @@ +/* 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, + std::function handler + ) + { + if (!m_server) + { + m_logger.log("Failed to add route, HTTP Server not initialised", COLOR_RED); + return; + } + + m_server->route(path, [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 000000000..b31359ab4 --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/Server/HTTP.h @@ -0,0 +1,49 @@ +/* HTTP Server + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef SERIALPROGRAMS_HTTP_H +#define SERIALPROGRAMS_HTTP_H + +#pragma once + +#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, + std::function handler + ); + + private: + explicit HTTPServer(); + ~HTTPServer(); + + Logger& m_logger; + QHttpServer* m_server; + QTcpServer* m_tcpServer; + }; + } +} + + +#endif diff --git a/SerialPrograms/Source/CommonFramework/Server/WebSocket.cpp b/SerialPrograms/Source/CommonFramework/Server/WebSocket.cpp new file mode 100644 index 000000000..175214dcd --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/Server/WebSocket.cpp @@ -0,0 +1,171 @@ +/* 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(); + } + + // Called when a new client connects + 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); + } + + // Called when a client disconnects + 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); + } + + // Handle text messages from clients + void WSServer::handleMessage(const QString& message) + { + QWebSocket* client = qobject_cast(sender()); + if (!client) return; + + emit messageReceived(client, message); + } + + // Handle binary messages from clients + 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 + 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 + 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 000000000..7c22d1166 --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/Server/WebSocket.h @@ -0,0 +1,73 @@ +/* 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); + + 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/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 7f4ccde70..f1627763a 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -487,6 +487,10 @@ file(GLOB LIBRARY_SOURCES Source/CommonFramework/Recording/StreamHistoryTracker_SaveFrames.h Source/CommonFramework/Recording/StreamRecorder.cpp Source/CommonFramework/Recording/StreamRecorder.h + Source/CommonFramework/Server/HTTP.cpp + Source/CommonFramework/Server/HTTP.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 From 00036371c9380faaebce43c59f980663d3032ec9 Mon Sep 17 00:00:00 2001 From: ConnorC432 Date: Tue, 24 Mar 2026 16:41:49 +0000 Subject: [PATCH 03/24] on_frame sends frames to websocket --- .../VideoPipeline/VideoSession.cpp | 3 ++ .../VideoPipeline/VideoSession.h | 29 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/SerialPrograms/Source/CommonFramework/VideoPipeline/VideoSession.cpp b/SerialPrograms/Source/CommonFramework/VideoPipeline/VideoSession.cpp index 340fcf00b..1c31ea748 100644 --- a/SerialPrograms/Source/CommonFramework/VideoPipeline/VideoSession.cpp +++ b/SerialPrograms/Source/CommonFramework/VideoPipeline/VideoSession.cpp @@ -11,6 +11,7 @@ #include "Backends/VideoFrameQt.h" #include "VideoSources/VideoSource_Null.h" #include "VideoSession.h" +#include "CommonFramework/Server/WebSocket.h" //#include //using std::cout; @@ -338,6 +339,8 @@ void VideoSession::on_frame(std::shared_ptr frame){ m_fps_tracker_source.push_event(frame->timestamp); } global_watchdog().delay(*this); + QByteArray frameData = frame_to_jpeg(*frame); + Server::WSServer::instance().send_binary(frameData); } void VideoSession::on_rendered_frame(WallClock timestamp){ WriteSpinLock lg(m_fps_lock); diff --git a/SerialPrograms/Source/CommonFramework/VideoPipeline/VideoSession.h b/SerialPrograms/Source/CommonFramework/VideoPipeline/VideoSession.h index 18d7d8a03..26d521076 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 From 686f4fc5959355eae0e41904fe9df8b325ff0518 Mon Sep 17 00:00:00 2001 From: ConnorC432 Date: Sun, 29 Mar 2026 19:17:30 +0100 Subject: [PATCH 04/24] HTTP API setting option --- .../Source/CommonFramework/GlobalSettingsPanel.cpp | 8 ++++++++ .../Source/CommonFramework/GlobalSettingsPanel.h | 2 ++ SerialPrograms/Source/CommonFramework/Main.cpp | 14 +++++++++----- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.cpp b/SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.cpp index 0305055e1..17bc3ad9e 100644 --- a/SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.cpp +++ b/SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.cpp @@ -241,6 +241,13 @@ GlobalSettings::GlobalSettings() LockMode::LOCK_WHILE_RUNNING, "", "" ) + , ENABLE_API( + "Enable API:
" + "Enable the HTTP API to control the program remotely." + "You must restart the program for it to take effect.", + LockMode::UNLOCK_WHILE_RUNNING, + false + ) { PA_ADD_OPTION(OPEN_BASE_FOLDER_BUTTON); PA_ADD_OPTION(CHECK_FOR_UPDATES); @@ -285,6 +292,7 @@ GlobalSettings::GlobalSettings() #endif PA_ADD_OPTION(DEVELOPER_TOKEN); + PA_ADD_OPTION(ENABLE_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 52a6e3094..5c4c37f06 100644 --- a/SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.h +++ b/SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.h @@ -156,6 +156,8 @@ class GlobalSettings : public BatchOption, private ConfigOption::Listener, priva StringOption DEVELOPER_TOKEN; + BooleanCheckBoxOption ENABLE_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 81ffb62e9..857fd2eba 100644 --- a/SerialPrograms/Source/CommonFramework/Main.cpp +++ b/SerialPrograms/Source/CommonFramework/Main.cpp @@ -193,11 +193,15 @@ int run_program(int argc, char *argv[]){ w.raise(); // bring the window to front on macOS set_permissions(w); - Server::HTTPServer& httpServer = Server::HTTPServer::instance(); - httpServer.start(8080); - - Server::WSServer& wsServer = Server::WSServer::instance(); - wsServer.start(8081); + // Start HTTP API if enabled + if (GlobalSettings::instance().ENABLE_API) + { + Server::HTTPServer& httpServer = Server::HTTPServer::instance(); + httpServer.start(8080); + + Server::WSServer& wsServer = Server::WSServer::instance(); + wsServer.start(8081); + } return application.exec(); } From 87b11266a995baf1b508847c038666248f85686f Mon Sep 17 00:00:00 2001 From: ConnorC432 Date: Sun, 29 Mar 2026 19:56:13 +0100 Subject: [PATCH 05/24] websocket test script --- SerialPrograms/Scripts/test_websocket.py | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 SerialPrograms/Scripts/test_websocket.py diff --git a/SerialPrograms/Scripts/test_websocket.py b/SerialPrograms/Scripts/test_websocket.py new file mode 100644 index 000000000..748f5ac00 --- /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 From 41bf5d1cd209635567739d6a3e7d4d164b8a0d08 Mon Sep 17 00:00:00 2001 From: ConnorC432 Date: Wed, 8 Apr 2026 11:17:19 +0100 Subject: [PATCH 06/24] refactor capture card websocket --- .../CommonFramework/Server/Sockets/VideoWS.h | 42 +++++++++++++++++++ .../Source/CommonFramework/Server/WebSocket.h | 4 ++ .../VideoPipeline/VideoSession.cpp | 5 ++- SerialPrograms/cmake/SourceFiles.cmake | 1 + 4 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 SerialPrograms/Source/CommonFramework/Server/Sockets/VideoWS.h diff --git a/SerialPrograms/Source/CommonFramework/Server/Sockets/VideoWS.h b/SerialPrograms/Source/CommonFramework/Server/Sockets/VideoWS.h new file mode 100644 index 000000000..4d260d127 --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/Server/Sockets/VideoWS.h @@ -0,0 +1,42 @@ +/* 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; + } + + 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.h b/SerialPrograms/Source/CommonFramework/Server/WebSocket.h index 7c22d1166..4f201d5d3 100644 --- a/SerialPrograms/Source/CommonFramework/Server/WebSocket.h +++ b/SerialPrograms/Source/CommonFramework/Server/WebSocket.h @@ -42,6 +42,10 @@ namespace PokemonAutomation void send_message(QWebSocket* client, const QString& message); void send_binary(QWebSocket* client, const QByteArray& data); + // Clients + int clientCount() const { return m_clients.size(); } + bool hasClients() const { return !m_clients.isEmpty(); } + signals: void clientConnected(QWebSocket* client); void clientDisconnected(QWebSocket* client); diff --git a/SerialPrograms/Source/CommonFramework/VideoPipeline/VideoSession.cpp b/SerialPrograms/Source/CommonFramework/VideoPipeline/VideoSession.cpp index 1c31ea748..6387dd767 100644 --- a/SerialPrograms/Source/CommonFramework/VideoPipeline/VideoSession.cpp +++ b/SerialPrograms/Source/CommonFramework/VideoPipeline/VideoSession.cpp @@ -12,6 +12,7 @@ #include "VideoSources/VideoSource_Null.h" #include "VideoSession.h" #include "CommonFramework/Server/WebSocket.h" +#include "CommonFramework/Server/Sockets/VideoWS.h" //#include //using std::cout; @@ -43,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); @@ -61,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)); @@ -339,8 +342,6 @@ void VideoSession::on_frame(std::shared_ptr frame){ m_fps_tracker_source.push_event(frame->timestamp); } global_watchdog().delay(*this); - QByteArray frameData = frame_to_jpeg(*frame); - Server::WSServer::instance().send_binary(frameData); } void VideoSession::on_rendered_frame(WallClock timestamp){ WriteSpinLock lg(m_fps_lock); diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index fe687e10f..fd37b495a 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -487,6 +487,7 @@ 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/WebSocket.cpp From 85b45aa92bc8faaa6a70c7ba39bf7d146a6a0622 Mon Sep 17 00:00:00 2001 From: ConnorC432 Date: Wed, 8 Apr 2026 13:07:48 +0100 Subject: [PATCH 07/24] list game categories method --- SerialPrograms/Source/CommonFramework/Panels/PanelList.h | 1 + 1 file changed, 1 insertion(+) diff --git a/SerialPrograms/Source/CommonFramework/Panels/PanelList.h b/SerialPrograms/Source/CommonFramework/Panels/PanelList.h index 1f588b286..920768891 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; From 2b9ad4d90b00963693ea063c2ec4df5eae488be1 Mon Sep 17 00:00:00 2001 From: ConnorC432 Date: Wed, 8 Apr 2026 13:09:09 +0100 Subject: [PATCH 08/24] better addRoute method --- .../Source/CommonFramework/Server/HTTP.h | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/SerialPrograms/Source/CommonFramework/Server/HTTP.h b/SerialPrograms/Source/CommonFramework/Server/HTTP.h index b31359ab4..148ff679f 100644 --- a/SerialPrograms/Source/CommonFramework/Server/HTTP.h +++ b/SerialPrograms/Source/CommonFramework/Server/HTTP.h @@ -34,6 +34,21 @@ namespace PokemonAutomation std::function handler ); + template + void addRoute( + const QString& path, + Handler&& handler + ) + { + if (!m_server) + { + m_logger.log("Failed to add route, HTTP Server not initialised", COLOR_RED); + return; + } + + m_server->route(path, std::forward(handler)); + } + private: explicit HTTPServer(); ~HTTPServer(); From cade5477cd04c7cabeb8b8a417d4fe835a893f02 Mon Sep 17 00:00:00 2001 From: ConnorC432 Date: Wed, 8 Apr 2026 13:09:40 +0100 Subject: [PATCH 09/24] server init moved out of main --- .../Source/CommonFramework/Main.cpp | 14 ++------ .../CommonFramework/Server/InitialiseServer.h | 33 +++++++++++++++++++ 2 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 SerialPrograms/Source/CommonFramework/Server/InitialiseServer.h diff --git a/SerialPrograms/Source/CommonFramework/Main.cpp b/SerialPrograms/Source/CommonFramework/Main.cpp index 857fd2eba..f06b31402 100644 --- a/SerialPrograms/Source/CommonFramework/Main.cpp +++ b/SerialPrograms/Source/CommonFramework/Main.cpp @@ -37,8 +37,7 @@ #include "ControllerInput/ControllerInput.h" #include "Integrations/DiscordWebhook.h" #include "Windows/MainWindow.h" -#include "Server/HTTP.h" -#include "Server/WebSocket.h" +#include "Server/InitialiseServer.h" #include using std::cout; @@ -193,15 +192,8 @@ int run_program(int argc, char *argv[]){ w.raise(); // bring the window to front on macOS set_permissions(w); - // Start HTTP API if enabled - if (GlobalSettings::instance().ENABLE_API) - { - Server::HTTPServer& httpServer = Server::HTTPServer::instance(); - httpServer.start(8080); - - Server::WSServer& wsServer = Server::WSServer::instance(); - wsServer.start(8081); - } + // Start HTTP/WS Server if enabled + Server::init_server(); return application.exec(); } diff --git a/SerialPrograms/Source/CommonFramework/Server/InitialiseServer.h b/SerialPrograms/Source/CommonFramework/Server/InitialiseServer.h new file mode 100644 index 000000000..d3842b5ab --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/Server/InitialiseServer.h @@ -0,0 +1,33 @@ +/* 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 "Routes/ProgramRoutes.h" + +namespace PokemonAutomation{ +namespace Server{ + +inline void init_server(){ + if (GlobalSettings::instance().ENABLE_API){ + // Start HTTP Server + HTTPServer& httpServer = HTTPServer::instance(); + httpServer.start(8080); + + // Start WebSocket Server + WSServer& wsServer = WSServer::instance(); + wsServer.start(8081); + } +} + +} +} + +#endif From fad5565169175fcb62f635c577fa02dd3af02cf3 Mon Sep 17 00:00:00 2001 From: ConnorC432 Date: Wed, 8 Apr 2026 13:10:44 +0100 Subject: [PATCH 10/24] add program registry and routes for HTTP API --- .../Panels/ProgramRegistry.cpp | 20 ++ .../CommonFramework/Panels/ProgramRegistry.h | 33 +++ .../Source/CommonFramework/Server/HTTP.cpp | 3 +- .../Source/CommonFramework/Server/HTTP.h | 4 +- .../CommonFramework/Server/InitialiseServer.h | 3 + .../CommonFramework/Server/RouteUtils.h | 39 ++++ .../Server/Routes/ProgramRoutes.cpp | 220 ++++++++++++++++++ .../Server/Routes/ProgramRoutes.h | 21 ++ SerialPrograms/Source/PanelLists.cpp | 33 +-- SerialPrograms/Source/PanelLists.h | 4 +- SerialPrograms/cmake/SourceFiles.cmake | 4 + 11 files changed, 366 insertions(+), 18 deletions(-) create mode 100644 SerialPrograms/Source/CommonFramework/Panels/ProgramRegistry.cpp create mode 100644 SerialPrograms/Source/CommonFramework/Panels/ProgramRegistry.h create mode 100644 SerialPrograms/Source/CommonFramework/Server/RouteUtils.h create mode 100644 SerialPrograms/Source/CommonFramework/Server/Routes/ProgramRoutes.cpp create mode 100644 SerialPrograms/Source/CommonFramework/Server/Routes/ProgramRoutes.h diff --git a/SerialPrograms/Source/CommonFramework/Panels/ProgramRegistry.cpp b/SerialPrograms/Source/CommonFramework/Panels/ProgramRegistry.cpp new file mode 100644 index 000000000..6d34729a8 --- /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 000000000..946466435 --- /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/HTTP.cpp b/SerialPrograms/Source/CommonFramework/Server/HTTP.cpp index 52319c9c1..6424a928b 100644 --- a/SerialPrograms/Source/CommonFramework/Server/HTTP.cpp +++ b/SerialPrograms/Source/CommonFramework/Server/HTTP.cpp @@ -66,6 +66,7 @@ namespace PokemonAutomation void HTTPServer::addRoute( const QString& path, + QHttpServerRequest::Method method, std::function handler ) { @@ -75,7 +76,7 @@ namespace PokemonAutomation return; } - m_server->route(path, [handler](const QHttpServerRequest& req, QHttpServerResponder& res) + 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 index 148ff679f..9a5df84ac 100644 --- a/SerialPrograms/Source/CommonFramework/Server/HTTP.h +++ b/SerialPrograms/Source/CommonFramework/Server/HTTP.h @@ -31,12 +31,14 @@ namespace PokemonAutomation void addRoute( const QString& path, + QHttpServerRequest::Method method, std::function handler ); template void addRoute( const QString& path, + QHttpServerRequest::Method method, Handler&& handler ) { @@ -46,7 +48,7 @@ namespace PokemonAutomation return; } - m_server->route(path, std::forward(handler)); + m_server->route(path, method, std::forward(handler)); } private: diff --git a/SerialPrograms/Source/CommonFramework/Server/InitialiseServer.h b/SerialPrograms/Source/CommonFramework/Server/InitialiseServer.h index d3842b5ab..326410d84 100644 --- a/SerialPrograms/Source/CommonFramework/Server/InitialiseServer.h +++ b/SerialPrograms/Source/CommonFramework/Server/InitialiseServer.h @@ -21,6 +21,9 @@ inline void init_server(){ HTTPServer& httpServer = HTTPServer::instance(); httpServer.start(8080); + // Register all HTTP routes + register_program_routes(); + // Start WebSocket Server WSServer& wsServer = WSServer::instance(); wsServer.start(8081); diff --git a/SerialPrograms/Source/CommonFramework/Server/RouteUtils.h b/SerialPrograms/Source/CommonFramework/Server/RouteUtils.h new file mode 100644 index 000000000..9e7fbb974 --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/Server/RouteUtils.h @@ -0,0 +1,39 @@ +/* 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); + // Normalize to NFD (Decomposition) to separate accents from base characters + slug = slug.normalized(QString::NormalizationForm_D); + // Remove Non-Spacing Mark characters (diacritics) + slug.remove(QRegularExpression("\\p{M}")); + // Back to NFC for any further processing if needed, though not strictly required for slugification + slug = slug.normalized(QString::NormalizationForm_C); + + 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/ProgramRoutes.cpp b/SerialPrograms/Source/CommonFramework/Server/Routes/ProgramRoutes.cpp new file mode 100644 index 000000000..2081218c4 --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/Server/Routes/ProgramRoutes.cpp @@ -0,0 +1,220 @@ +/* Program Routes + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#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 000000000..d39b5278f --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/Server/Routes/ProgramRoutes.h @@ -0,0 +1,21 @@ +/* Program Routes + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_ProgramRoutes_H +#define PokemonAutomation_ProgramRoutes_H + +#include +#include + +namespace PokemonAutomation{ +namespace Server{ + +void register_program_routes(); + +} +} + +#endif diff --git a/SerialPrograms/Source/PanelLists.cpp b/SerialPrograms/Source/PanelLists.cpp index 1eb6c258a..f3906f453 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 04c7b3181..2b6d62c99 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 fd37b495a..e3bf8e86e 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -454,6 +454,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 @@ -490,6 +492,8 @@ file(GLOB LIBRARY_SOURCES 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/WebSocket.cpp Source/CommonFramework/Server/Websocket.h Source/CommonFramework/Startup/NewVersionCheck.cpp From 15e474f2dbb54b1f5b872b4dd20a8e4a57eea781 Mon Sep 17 00:00:00 2001 From: ConnorC432 Date: Wed, 8 Apr 2026 14:00:59 +0100 Subject: [PATCH 11/24] add global settings routes to HTTP API --- .../CommonFramework/Server/InitialiseServer.h | 2 + .../Server/Routes/GlobalSettingsRoutes.cpp | 56 +++++++++++++++++++ .../Server/Routes/GlobalSettingsRoutes.h | 18 ++++++ SerialPrograms/cmake/SourceFiles.cmake | 2 + 4 files changed, 78 insertions(+) create mode 100644 SerialPrograms/Source/CommonFramework/Server/Routes/GlobalSettingsRoutes.cpp create mode 100644 SerialPrograms/Source/CommonFramework/Server/Routes/GlobalSettingsRoutes.h diff --git a/SerialPrograms/Source/CommonFramework/Server/InitialiseServer.h b/SerialPrograms/Source/CommonFramework/Server/InitialiseServer.h index 326410d84..41047f4c6 100644 --- a/SerialPrograms/Source/CommonFramework/Server/InitialiseServer.h +++ b/SerialPrograms/Source/CommonFramework/Server/InitialiseServer.h @@ -11,6 +11,7 @@ #include "CommonFramework/Server/HTTP.h" #include "CommonFramework/Server/WebSocket.h" #include "Routes/ProgramRoutes.h" +#include "Routes/GlobalSettingsRoutes.h" namespace PokemonAutomation{ namespace Server{ @@ -23,6 +24,7 @@ inline void init_server(){ // Register all HTTP routes register_program_routes(); + register_settings_routes(); // Start WebSocket Server WSServer& wsServer = WSServer::instance(); diff --git a/SerialPrograms/Source/CommonFramework/Server/Routes/GlobalSettingsRoutes.cpp b/SerialPrograms/Source/CommonFramework/Server/Routes/GlobalSettingsRoutes.cpp new file mode 100644 index 000000000..7d3c358f9 --- /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 000000000..7b34eb5dc --- /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/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index e3bf8e86e..abb094af2 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -494,6 +494,8 @@ file(GLOB LIBRARY_SOURCES 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 From b7f5cfad6af2774b6d6c9a52be2d48408014de0d Mon Sep 17 00:00:00 2001 From: ConnorC432 Date: Wed, 8 Apr 2026 14:07:18 +0100 Subject: [PATCH 12/24] API enable option is experimental --- SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.cpp b/SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.cpp index 17bc3ad9e..12ee42b33 100644 --- a/SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.cpp +++ b/SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.cpp @@ -242,7 +242,7 @@ GlobalSettings::GlobalSettings() "", "" ) , ENABLE_API( - "Enable API:
" + "Enable API (experimental):
" "Enable the HTTP API to control the program remotely." "You must restart the program for it to take effect.", LockMode::UNLOCK_WHILE_RUNNING, From d6e126432eaf3e1c6b49b1ca096598042face152 Mon Sep 17 00:00:00 2001 From: ConnorC432 Date: Wed, 8 Apr 2026 15:34:50 +0100 Subject: [PATCH 13/24] chmod +x test_websocket.py --- SerialPrograms/Scripts/test_websocket.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 SerialPrograms/Scripts/test_websocket.py diff --git a/SerialPrograms/Scripts/test_websocket.py b/SerialPrograms/Scripts/test_websocket.py old mode 100644 new mode 100755 From e31b9c9eb5f2c438d5ade37ecede482e5e351c58 Mon Sep 17 00:00:00 2001 From: ConnorC432 Date: Wed, 8 Apr 2026 16:10:46 +0100 Subject: [PATCH 14/24] better API settings --- .../CommonFramework/GlobalSettingsPanel.cpp | 11 ++--- .../CommonFramework/GlobalSettingsPanel.h | 3 +- .../Options/Environment/APIOptions.cpp | 48 +++++++++++++++++++ .../Options/Environment/APIOptions.h | 30 ++++++++++++ .../CommonFramework/Server/InitialiseServer.h | 7 +-- SerialPrograms/cmake/SourceFiles.cmake | 2 + 6 files changed, 89 insertions(+), 12 deletions(-) create mode 100644 SerialPrograms/Source/CommonFramework/Options/Environment/APIOptions.cpp create mode 100644 SerialPrograms/Source/CommonFramework/Options/Environment/APIOptions.h diff --git a/SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.cpp b/SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.cpp index 12ee42b33..bc61bc945 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,13 +242,7 @@ GlobalSettings::GlobalSettings() LockMode::LOCK_WHILE_RUNNING, "", "" ) - , ENABLE_API( - "Enable API (experimental):
" - "Enable the HTTP API to control the program remotely." - "You must restart the program for it to take effect.", - LockMode::UNLOCK_WHILE_RUNNING, - false - ) + , API(CONSTRUCT_TOKEN) { PA_ADD_OPTION(OPEN_BASE_FOLDER_BUTTON); PA_ADD_OPTION(CHECK_FOR_UPDATES); @@ -292,7 +287,7 @@ GlobalSettings::GlobalSettings() #endif PA_ADD_OPTION(DEVELOPER_TOKEN); - PA_ADD_OPTION(ENABLE_API); + 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 5c4c37f06..85b26ec1e 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,7 +157,7 @@ class GlobalSettings : public BatchOption, private ConfigOption::Listener, priva StringOption DEVELOPER_TOKEN; - BooleanCheckBoxOption ENABLE_API; + Pimpl API; // The mode that does not run Qt GUI, but instead runs some tests for // debugging, unit testing and developing purposes. diff --git a/SerialPrograms/Source/CommonFramework/Options/Environment/APIOptions.cpp b/SerialPrograms/Source/CommonFramework/Options/Environment/APIOptions.cpp new file mode 100644 index 000000000..b303daee6 --- /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, + false + ) + , 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 000000000..37d293702 --- /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/Server/InitialiseServer.h b/SerialPrograms/Source/CommonFramework/Server/InitialiseServer.h index 41047f4c6..306a2d64f 100644 --- a/SerialPrograms/Source/CommonFramework/Server/InitialiseServer.h +++ b/SerialPrograms/Source/CommonFramework/Server/InitialiseServer.h @@ -10,6 +10,7 @@ #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" @@ -17,10 +18,10 @@ namespace PokemonAutomation{ namespace Server{ inline void init_server(){ - if (GlobalSettings::instance().ENABLE_API){ + if (GlobalSettings::instance().API->ENABLE_API){ // Start HTTP Server HTTPServer& httpServer = HTTPServer::instance(); - httpServer.start(8080); + httpServer.start(GlobalSettings::instance().API->HTTP_PORT); // Register all HTTP routes register_program_routes(); @@ -28,7 +29,7 @@ inline void init_server(){ // Start WebSocket Server WSServer& wsServer = WSServer::instance(); - wsServer.start(8081); + wsServer.start(GlobalSettings::instance().API->WS_PORT); } } diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index abb094af2..41b5f9034 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 From 0a5d821d9ffd136b5a181bd34e70036c2f342522 Mon Sep 17 00:00:00 2001 From: ConnorC432 Date: Wed, 8 Apr 2026 16:59:16 +0100 Subject: [PATCH 15/24] docs markdown --- .../Source/CommonFramework/Server/API.md | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 SerialPrograms/Source/CommonFramework/Server/API.md diff --git a/SerialPrograms/Source/CommonFramework/Server/API.md b/SerialPrograms/Source/CommonFramework/Server/API.md new file mode 100644 index 000000000..a69e48c9e --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/Server/API.md @@ -0,0 +1,161 @@ +# SerialPrograms API Documentation + +> [!WARNING] +> This API is still under development and not all features are fully implemented. + +## 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. + +### Connection + +- **URL:** `http://localhost:{HTTP Port}` + +--- + +### 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 + +### Connection + +- **URL:** `ws://localhost:{WebSocket Port}` + +--- + +### 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 + +> [!WARNING] +> Not yet implemented. +> Logs will be sent as text messages. \ No newline at end of file From 6dce635ae8f9c9457acec94f23dee76cb6295153 Mon Sep 17 00:00:00 2001 From: ConnorC432 Date: Wed, 8 Apr 2026 18:04:55 +0100 Subject: [PATCH 16/24] clean up comments and unused includes --- .../Source/CommonFramework/Server/RouteUtils.h | 9 +++++---- .../Source/CommonFramework/Server/Routes/ProgramRoutes.h | 3 --- .../Source/CommonFramework/Server/Sockets/VideoWS.h | 1 + .../Source/CommonFramework/Server/WebSocket.cpp | 8 ++------ 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/SerialPrograms/Source/CommonFramework/Server/RouteUtils.h b/SerialPrograms/Source/CommonFramework/Server/RouteUtils.h index 9e7fbb974..c2caaf209 100644 --- a/SerialPrograms/Source/CommonFramework/Server/RouteUtils.h +++ b/SerialPrograms/Source/CommonFramework/Server/RouteUtils.h @@ -16,13 +16,14 @@ namespace Server{ inline QString to_slug(const std::string& name) { QString slug = QString::fromStdString(name); - // Normalize to NFD (Decomposition) to separate accents from base characters + // Normalise to NFD (Decomposition) to remove accents (é to e, etc...) slug = slug.normalized(QString::NormalizationForm_D); - // Remove Non-Spacing Mark characters (diacritics) slug.remove(QRegularExpression("\\p{M}")); - // Back to NFC for any further processing if needed, though not strictly required for slugification + + // 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("--")) { diff --git a/SerialPrograms/Source/CommonFramework/Server/Routes/ProgramRoutes.h b/SerialPrograms/Source/CommonFramework/Server/Routes/ProgramRoutes.h index d39b5278f..02111ef3b 100644 --- a/SerialPrograms/Source/CommonFramework/Server/Routes/ProgramRoutes.h +++ b/SerialPrograms/Source/CommonFramework/Server/Routes/ProgramRoutes.h @@ -7,9 +7,6 @@ #ifndef PokemonAutomation_ProgramRoutes_H #define PokemonAutomation_ProgramRoutes_H -#include -#include - namespace PokemonAutomation{ namespace Server{ diff --git a/SerialPrograms/Source/CommonFramework/Server/Sockets/VideoWS.h b/SerialPrograms/Source/CommonFramework/Server/Sockets/VideoWS.h index 4d260d127..13676b623 100644 --- a/SerialPrograms/Source/CommonFramework/Server/Sockets/VideoWS.h +++ b/SerialPrograms/Source/CommonFramework/Server/Sockets/VideoWS.h @@ -24,6 +24,7 @@ namespace PokemonAutomation return instance; } + // Send frames to all connected clients virtual void on_frame(std::shared_ptr frame) override { if (WSServer::instance().hasClients()) diff --git a/SerialPrograms/Source/CommonFramework/Server/WebSocket.cpp b/SerialPrograms/Source/CommonFramework/Server/WebSocket.cpp index 175214dcd..3db1c0e79 100644 --- a/SerialPrograms/Source/CommonFramework/Server/WebSocket.cpp +++ b/SerialPrograms/Source/CommonFramework/Server/WebSocket.cpp @@ -73,7 +73,6 @@ namespace PokemonAutomation emit serverStopped(); } - // Called when a new client connects void WSServer::clientConnection() { QWebSocket* client = m_server->nextPendingConnection(); @@ -88,7 +87,6 @@ namespace PokemonAutomation emit clientConnected(client); } - // Called when a client disconnects void WSServer::clientDisconnection() { QWebSocket* client = qobject_cast(sender()); @@ -102,7 +100,6 @@ namespace PokemonAutomation emit clientDisconnected(client); } - // Handle text messages from clients void WSServer::handleMessage(const QString& message) { QWebSocket* client = qobject_cast(sender()); @@ -111,7 +108,6 @@ namespace PokemonAutomation emit messageReceived(client, message); } - // Handle binary messages from clients void WSServer::handleBinary(const QByteArray& data) { QWebSocket* client = qobject_cast(sender()); @@ -120,7 +116,7 @@ namespace PokemonAutomation emit binaryReceived(client, data); } - // Send text message to a specific client + // 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; @@ -132,7 +128,7 @@ namespace PokemonAutomation }, Qt::QueuedConnection); } - // Send binary message to a specific client + // 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; From 35a6d05ae14c4c04fcffe8870dc5ace2b36c71d8 Mon Sep 17 00:00:00 2001 From: ConnorC432 Date: Wed, 8 Apr 2026 18:07:02 +0100 Subject: [PATCH 17/24] add Discord link to API documentation --- SerialPrograms/Source/CommonFramework/Server/API.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/SerialPrograms/Source/CommonFramework/Server/API.md b/SerialPrograms/Source/CommonFramework/Server/API.md index a69e48c9e..8e1fe60b7 100644 --- a/SerialPrograms/Source/CommonFramework/Server/API.md +++ b/SerialPrograms/Source/CommonFramework/Server/API.md @@ -1,5 +1,7 @@ # SerialPrograms API Documentation +[](https://discord.gg/cQ4gWxN) + > [!WARNING] > This API is still under development and not all features are fully implemented. From a85d89b83488c1cdcc86aabb9921b23449df461c Mon Sep 17 00:00:00 2001 From: Anor Londo Date: Wed, 8 Apr 2026 18:08:14 +0100 Subject: [PATCH 18/24] this looks a bit better --- SerialPrograms/Source/CommonFramework/Server/API.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SerialPrograms/Source/CommonFramework/Server/API.md b/SerialPrograms/Source/CommonFramework/Server/API.md index 8e1fe60b7..89ce4a4fd 100644 --- a/SerialPrograms/Source/CommonFramework/Server/API.md +++ b/SerialPrograms/Source/CommonFramework/Server/API.md @@ -1,10 +1,10 @@ # SerialPrograms API Documentation -[](https://discord.gg/cQ4gWxN) - > [!WARNING] > This API is still under development and not all features are fully implemented. +[](https://discord.gg/cQ4gWxN) + ## Configuration @@ -160,4 +160,4 @@ The WebSocket server broadcasts real-time video frames from the capture card. Th > [!WARNING] > Not yet implemented. -> Logs will be sent as text messages. \ No newline at end of file +> Logs will be sent as text messages. From 1c836b908f838ca6220045362556e4fc037f3fde Mon Sep 17 00:00:00 2001 From: ConnorC432 Date: Wed, 8 Apr 2026 18:10:44 +0100 Subject: [PATCH 19/24] clearer docs --- SerialPrograms/Source/CommonFramework/Server/API.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/SerialPrograms/Source/CommonFramework/Server/API.md b/SerialPrograms/Source/CommonFramework/Server/API.md index 8e1fe60b7..1adcc306f 100644 --- a/SerialPrograms/Source/CommonFramework/Server/API.md +++ b/SerialPrograms/Source/CommonFramework/Server/API.md @@ -158,6 +158,7 @@ The WebSocket server broadcasts real-time video frames from the capture card. Th ### Text Messages +The logs shown in the Output Window are sent as text messages. + > [!WARNING] -> Not yet implemented. -> Logs will be sent as text messages. \ No newline at end of file +> Not yet implemented. \ No newline at end of file From 883e77e4203d97f4b74ec485d664ff5f76ca28a0 Mon Sep 17 00:00:00 2001 From: ConnorC432 Date: Wed, 8 Apr 2026 18:20:59 +0100 Subject: [PATCH 20/24] remove redundant connection sections in API docs --- SerialPrograms/Source/CommonFramework/Server/API.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/SerialPrograms/Source/CommonFramework/Server/API.md b/SerialPrograms/Source/CommonFramework/Server/API.md index 6d31cb2c5..ca586d912 100644 --- a/SerialPrograms/Source/CommonFramework/Server/API.md +++ b/SerialPrograms/Source/CommonFramework/Server/API.md @@ -24,10 +24,6 @@ > [!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. -### Connection - -- **URL:** `http://localhost:{HTTP Port}` - --- ### Settings @@ -146,10 +142,6 @@ Updates the configuration options for a specific program. Only the provided fiel ## WebSocket -### Connection - -- **URL:** `ws://localhost:{WebSocket Port}` - --- ### Binary Messages From 28e1ef5dffd74fc52118ac2661611357cf6b2808 Mon Sep 17 00:00:00 2001 From: ConnorC432 Date: Wed, 8 Apr 2026 19:11:55 +0100 Subject: [PATCH 21/24] add qtwebsockets and qthttpserver to CI --- .azure-pipelines/azure-pipelines.yml | 8 ++++---- .github/workflows/cpp-ci-serial-programs-base.yml | 2 +- SerialPrograms/CMakeLists.txt | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.azure-pipelines/azure-pipelines.yml b/.azure-pipelines/azure-pipelines.yml index 421962475..dd59c1a1a 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 d467c9db0..eafde5b8e 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 bf22f6c11..d116f3a7b 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 HttpServer 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 HttpServer 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 HttpServer 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 Qt${QT_MAJOR}::HttpServer) + 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 From 0ec0704985760fc3aec1f7c90d17e060bd0aa6d1 Mon Sep 17 00:00:00 2001 From: ConnorC432 Date: Wed, 8 Apr 2026 19:46:52 +0100 Subject: [PATCH 22/24] enable api by default for now --- .../Source/CommonFramework/Options/Environment/APIOptions.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SerialPrograms/Source/CommonFramework/Options/Environment/APIOptions.cpp b/SerialPrograms/Source/CommonFramework/Options/Environment/APIOptions.cpp index b303daee6..2b9850a66 100644 --- a/SerialPrograms/Source/CommonFramework/Options/Environment/APIOptions.cpp +++ b/SerialPrograms/Source/CommonFramework/Options/Environment/APIOptions.cpp @@ -24,7 +24,7 @@ APIOptions::APIOptions() "Enable API:
" "Enable the HTTP API and WebSockets to control the program remotely.
", LockMode::UNLOCK_WHILE_RUNNING, - false + true ) , HTTP_PORT( "HTTP Port:
" From 9fb1321fa61246b1054e68e6e01829f2f7d6e7a2 Mon Sep 17 00:00:00 2001 From: ConnorC432 Date: Wed, 8 Apr 2026 19:47:31 +0100 Subject: [PATCH 23/24] update clientCount return type to qsizetype to hopefully fix mac build --- SerialPrograms/Source/CommonFramework/Server/WebSocket.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SerialPrograms/Source/CommonFramework/Server/WebSocket.h b/SerialPrograms/Source/CommonFramework/Server/WebSocket.h index 4f201d5d3..51612ff39 100644 --- a/SerialPrograms/Source/CommonFramework/Server/WebSocket.h +++ b/SerialPrograms/Source/CommonFramework/Server/WebSocket.h @@ -43,7 +43,7 @@ namespace PokemonAutomation void send_binary(QWebSocket* client, const QByteArray& data); // Clients - int clientCount() const { return m_clients.size(); } + qsizetype clientCount() const { return m_clients.size(); } bool hasClients() const { return !m_clients.isEmpty(); } signals: From 27d3fa8a3bb035021eace8b1b46fbdf4e6f2eabf Mon Sep 17 00:00:00 2001 From: ConnorC432 Date: Wed, 8 Apr 2026 19:53:27 +0100 Subject: [PATCH 24/24] this may or may not fix windows build? --- SerialPrograms/Source/CommonFramework/Server/HTTP.h | 1 + .../Source/CommonFramework/Server/Routes/ProgramRoutes.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/SerialPrograms/Source/CommonFramework/Server/HTTP.h b/SerialPrograms/Source/CommonFramework/Server/HTTP.h index 9a5df84ac..3a09183ac 100644 --- a/SerialPrograms/Source/CommonFramework/Server/HTTP.h +++ b/SerialPrograms/Source/CommonFramework/Server/HTTP.h @@ -10,6 +10,7 @@ #pragma once #include +#include #include "CommonFramework/Logging/Logger.h" diff --git a/SerialPrograms/Source/CommonFramework/Server/Routes/ProgramRoutes.cpp b/SerialPrograms/Source/CommonFramework/Server/Routes/ProgramRoutes.cpp index 2081218c4..26b7a96fb 100644 --- a/SerialPrograms/Source/CommonFramework/Server/Routes/ProgramRoutes.cpp +++ b/SerialPrograms/Source/CommonFramework/Server/Routes/ProgramRoutes.cpp @@ -4,6 +4,7 @@ * */ +#include #include #include #include