From f74dc9c8cd0d5132d55817abc11564caa9974af4 Mon Sep 17 00:00:00 2001 From: jb1228 <37507376+jb1228@users.noreply.github.com> Date: Mon, 25 May 2026 22:04:27 -0400 Subject: [PATCH 1/2] Make WebSocket server port configurable These changes are primarily to allow multiple instances and keep default behavior at 8928. Add configurable WebSocket server port (default 8928) across the project and update docs/examples. - Add AGENTS.md project overview. - Add DEFAULT_SERVER_PORT and server_port to SendspinClientConfig (include/sendspin/config.h). - Pass configured server_port into servers via ConnectionManager (src/connection_manager.cpp) and add set_port()/server_port_ members to ESP and host ws_server classes (src/esp/ws_server.{h,cpp}, src/host/ws_server.{h,cpp}). - Update host server to use server_port_ instead of hardcoded 8928 and log accordingly. - Update examples (basic_client and tui_client) to accept -p PORT, parse/validate it, set config.server_port, and reflect usage/help text and README notes. - Add docs entry for server_port in integration-guide.md. --- docs/integration-guide.md | 1 + examples/basic_client/README.md | 3 ++- examples/basic_client/main.cpp | 36 ++++++++++++++++++++++++++------- examples/tui_client/README.md | 1 + examples/tui_client/main.cpp | 30 +++++++++++++++++++++++---- include/sendspin/config.h | 11 ++++++---- src/connection_manager.cpp | 1 + src/esp/ws_server.cpp | 4 ++-- src/esp/ws_server.h | 15 ++++++++++++-- src/host/ws_server.cpp | 7 ++----- src/host/ws_server.h | 21 ++++++++++++++----- 11 files changed, 100 insertions(+), 30 deletions(-) diff --git a/docs/integration-guide.md b/docs/integration-guide.md index f7ec6b7..9921139 100644 --- a/docs/integration-guide.md +++ b/docs/integration-guide.md @@ -698,6 +698,7 @@ Main client configuration passed to the `SendspinClient` constructor. | `httpd_psram_stack` | `bool` | `false` | Allocate HTTP server task stack in PSRAM (ESP-IDF only) | | `httpd_priority` | `unsigned` | `17` | FreeRTOS priority for the HTTP server task (ESP-IDF only) | | `websocket_priority` | `unsigned` | `5` | FreeRTOS priority for the WebSocket client task (ESP-IDF only) | +| `server_port` | `uint16_t` | `8928` | WebSocket server port | | `server_max_connections` | `uint8_t` | `2` | Maximum simultaneous WebSocket connections (default supports the handoff protocol) | | `httpd_ctrl_port` | `uint16_t` | `0` | ESP-IDF httpd control port; `0` uses `ESP_HTTPD_DEF_CTRL_PORT + 1` to avoid conflict with the web_server component | | `time_burst_size` | `uint8_t` | `8` | Number of messages per time sync burst | diff --git a/examples/basic_client/README.md b/examples/basic_client/README.md index ab3cd55..dca7dc6 100644 --- a/examples/basic_client/README.md +++ b/examples/basic_client/README.md @@ -28,9 +28,10 @@ macOS has mDNS support built in; no extra dependencies needed. ```sh ./build/examples/basic_client/basic_client # default name "Basic Client" ./build/examples/basic_client/basic_client "My Player" # custom name +./build/examples/basic_client/basic_client -p 8930 # listen on a custom port ``` -The client listens on port 8928. When mDNS is enabled it advertises `_sendspin._tcp` so Sendspin servers on the local network discover and connect automatically; otherwise tell the server to connect with `ws://:8928/sendspin`. +The client listens on port 8928 by default. When mDNS is enabled it advertises `_sendspin._tcp` with the configured port so Sendspin servers on the local network discover and connect automatically; otherwise tell the server to connect with `ws://:8928/sendspin`, replacing `8928` if you passed `-p`. If PortAudio is available, audio is played through the default output device. Otherwise, audio is discarded (NullAudioSink). diff --git a/examples/basic_client/main.cpp b/examples/basic_client/main.cpp index 814ddd6..9f6889d 100644 --- a/examples/basic_client/main.cpp +++ b/examples/basic_client/main.cpp @@ -15,7 +15,7 @@ /// @file Host example application for sendspin-cpp. /// /// Runs a SendspinClient on the host computer, listening for incoming -/// connections from a Sendspin server on port 8928. When built with mDNS +/// connections from a Sendspin server on the configured port. When built with mDNS /// support (dns_sd.h available), advertises via mDNS so Sendspin servers /// can discover and connect automatically; otherwise the user must connect /// manually with `-u ws://:/`. @@ -25,6 +25,7 @@ /// /// Options: /// -u URL Connect to a WebSocket URL (e.g. ws://192.168.1.10:8928/sendspin) +/// -p PORT Listen on PORT (default: 8928) /// -l LEVEL Set log level: none, error, warn, info (default), debug, verbose /// -v Verbose logging (same as -l verbose) /// -q Quiet logging (same as -l error) @@ -49,13 +50,14 @@ #include #include #include +#include #include #include #include using namespace sendspin; -static const uint16_t SENDSPIN_PORT = 8928; +static constexpr uint16_t DEFAULT_SENDSPIN_PORT = SendspinClientConfig::DEFAULT_SERVER_PORT; static const char* SENDSPIN_PATH = "/sendspin"; // Tracks total audio bytes received (used when PortAudio is unavailable) @@ -127,6 +129,7 @@ static void print_usage(const char* prog) { fprintf(stderr, " name Friendly name (default: \"Basic Client\")\n\n"); fprintf(stderr, "Options:\n"); fprintf(stderr, " -u URL Connect to a WebSocket URL (e.g. ws://192.168.1.10:8928/sendspin)\n"); + fprintf(stderr, " -p PORT Listen on PORT (default: %u)\n", DEFAULT_SENDSPIN_PORT); fprintf(stderr, " -l LEVEL Log level: none, error, warn, info (default), debug, verbose\n"); fprintf(stderr, " -v Verbose logging (same as -l verbose)\n"); fprintf(stderr, " -q Quiet logging (same as -l error)\n"); @@ -143,6 +146,16 @@ static bool parse_log_level(const char* str, LogLevel& level) { return false; } +static bool parse_port(const char* str, uint16_t& port) { + char* end = nullptr; + unsigned long value = strtoul(str, &end, 10); + if (*str == '\0' || *end != '\0' || value == 0 || value > 65535UL) { + return false; + } + port = static_cast(value); + return true; +} + int main(int argc, char* argv[]) { // Set up signal handler for clean shutdown std::signal(SIGINT, signal_handler); @@ -151,12 +164,20 @@ int main(int argc, char* argv[]) { // Parse command line options LogLevel log_level = LogLevel::INFO; std::string connect_url; + uint16_t server_port = DEFAULT_SENDSPIN_PORT; int opt; - while ((opt = getopt(argc, argv, "u:l:vqh")) != -1) { + while ((opt = getopt(argc, argv, "u:p:l:vqh")) != -1) { switch (opt) { case 'u': connect_url = optarg; break; + case 'p': + if (!parse_port(optarg, server_port)) { + fprintf(stderr, "Invalid port: %s\n", optarg); + print_usage(argv[0]); + return 1; + } + break; case 'l': if (!parse_log_level(optarg, log_level)) { fprintf(stderr, "Unknown log level: %s\n", optarg); @@ -191,6 +212,7 @@ int main(int argc, char* argv[]) { config.product_name = "sendspin-cpp host example"; config.manufacturer = "sendspin-cpp"; config.software_version = "0.1.0"; + config.server_port = server_port; // Create audio output and client #ifdef SENDSPIN_HAS_PORTAUDIO @@ -301,7 +323,7 @@ int main(int argc, char* argv[]) { client.set_network_provider(&network_provider); // Start the server - fprintf(stderr, "Starting Sendspin basic client on port %u...\n", SENDSPIN_PORT); + fprintf(stderr, "Starting Sendspin basic client on port %u...\n", server_port); if (!client.start_server()) { fprintf(stderr, "Failed to start server\n"); @@ -310,9 +332,9 @@ int main(int argc, char* argv[]) { #ifdef SENDSPIN_HAS_MDNS MdnsAdvertiser mdns; - if (!mdns.start(friendly_name, SENDSPIN_PORT, SENDSPIN_PATH)) { + if (!mdns.start(friendly_name, server_port, SENDSPIN_PATH)) { fprintf(stderr, "Warning: mDNS advertisement failed, server still running\n"); - fprintf(stderr, "Connect manually to ws://:%u%s\n", SENDSPIN_PORT, + fprintf(stderr, "Connect manually to ws://:%u%s\n", server_port, SENDSPIN_PATH); } #else @@ -320,7 +342,7 @@ int main(int argc, char* argv[]) { "mDNS advertisement not compiled in. Either restart with " "-u ws://:/ to dial a server, or tell a server " "to connect to ws://:%u%s.\n", - SENDSPIN_PORT, SENDSPIN_PATH); + server_port, SENDSPIN_PATH); #endif // Auto-connect if a URL was provided via -u diff --git a/examples/tui_client/README.md b/examples/tui_client/README.md index 3ef19bc..2deb9ae 100644 --- a/examples/tui_client/README.md +++ b/examples/tui_client/README.md @@ -41,6 +41,7 @@ brew install portaudio ./build/examples/tui_client/tui_client # default name "TUI Client" ./build/examples/tui_client/tui_client "My Player" # custom name ./build/examples/tui_client/tui_client -u ws://192.168.1.10:8928/sendspin # connect to a specific server +./build/examples/tui_client/tui_client -p 8930 # listen on a custom port ./build/examples/tui_client/tui_client -V # disable visualizer ``` diff --git a/examples/tui_client/main.cpp b/examples/tui_client/main.cpp index f9e0347..b2b68d8 100644 --- a/examples/tui_client/main.cpp +++ b/examples/tui_client/main.cpp @@ -21,6 +21,7 @@ /// name: Optional friendly name (default: "TUI Client") /// /// Options: +/// -p PORT Listen on PORT (default: 8928) /// -f FORMAT Audio format as codec:rate:bits:channels (repeatable) /// -h Show usage @@ -47,6 +48,7 @@ #include #include #include +#include #include #include #include @@ -58,7 +60,7 @@ using namespace sendspin; -static const uint16_t SENDSPIN_PORT = 8928; +static constexpr uint16_t DEFAULT_SENDSPIN_PORT = SendspinClientConfig::DEFAULT_SERVER_PORT; static const char* SENDSPIN_PATH = "/sendspin"; // Big-endian helpers for binary visualizer data parsing @@ -380,11 +382,22 @@ static bool parse_audio_format(const std::string& str, sendspin::AudioSupportedF return true; } +static bool parse_port(const char* str, uint16_t& port) { + char* end = nullptr; + unsigned long value = strtoul(str, &end, 10); + if (*str == '\0' || *end != '\0' || value == 0 || value > 65535UL) { + return false; + } + port = static_cast(value); + return true; +} + static void print_usage(const char* prog) { fprintf(stderr, "Usage: %s [options] [name]\n", prog); fprintf(stderr, " name Friendly name (default: \"TUI Client\")\n\n"); fprintf(stderr, "Options:\n"); fprintf(stderr, " -u URL Connect to a WebSocket URL (e.g. ws://192.168.1.10:8928/sendspin)\n"); + fprintf(stderr, " -p PORT Listen on PORT (default: %u)\n", DEFAULT_SENDSPIN_PORT); fprintf(stderr, " -f FORMAT Audio format as codec:rate:bits:channels (e.g. flac:48000:24:2)\n"); fprintf(stderr, " Can be specified multiple times. Codecs: flac, opus, pcm\n"); fprintf(stderr, " -V Disable visualizer\n"); @@ -395,13 +408,21 @@ int main(int argc, char* argv[]) { // Parse command line options bool enable_visualizer = true; std::string connect_url; + uint16_t server_port = DEFAULT_SENDSPIN_PORT; std::vector audio_formats; int opt; - while ((opt = getopt(argc, argv, "u:f:Vh")) != -1) { + while ((opt = getopt(argc, argv, "u:p:f:Vh")) != -1) { switch (opt) { case 'u': connect_url = optarg; break; + case 'p': + if (!parse_port(optarg, server_port)) { + fprintf(stderr, "Invalid port: %s\n", optarg); + print_usage(argv[0]); + return 1; + } + break; case 'f': { sendspin::AudioSupportedFormatObject fmt; if (!parse_audio_format(optarg, fmt)) { @@ -434,6 +455,7 @@ int main(int argc, char* argv[]) { config.product_name = "sendspin-cpp host TUI"; config.manufacturer = "sendspin-cpp"; config.software_version = "0.1.0"; + config.server_port = server_port; // Create audio output #ifdef SENDSPIN_HAS_PORTAUDIO @@ -726,7 +748,7 @@ int main(int argc, char* argv[]) { // Advertise via mDNS and browse for other servers (when compiled in) #ifdef SENDSPIN_HAS_MDNS MdnsAdvertiser mdns; - mdns.start(friendly_name, SENDSPIN_PORT, SENDSPIN_PATH); + mdns.start(friendly_name, server_port, SENDSPIN_PATH); MdnsBrowser mdns_browser; mdns_browser.start(); @@ -752,7 +774,7 @@ int main(int argc, char* argv[]) { static_cast(std::stoul(host_port.substr(colon + 1))); } else { state.connected_host = host_port; - state.connected_port = SENDSPIN_PORT; + state.connected_port = DEFAULT_SENDSPIN_PORT; } } client.connect_to(connect_url); diff --git a/include/sendspin/config.h b/include/sendspin/config.h index 352dd62..d94a337 100644 --- a/include/sendspin/config.h +++ b/include/sendspin/config.h @@ -50,10 +50,13 @@ struct SendspinClientConfig { unsigned websocket_priority{5}; ///< FreeRTOS priority for the WebSocket client task ///< (ESP-IDF only) - uint8_t server_max_connections{2}; ///< Maximum simultaneous connections (default: 2 for - ///< handoff protocol) - uint16_t httpd_ctrl_port{0}; ///< ESP-IDF httpd control port; 0 = ESP_HTTPD_DEF_CTRL_PORT - ///< + 1 (avoids conflict with web_server component) + static constexpr uint16_t DEFAULT_SERVER_PORT = 8928U; ///< Default WebSocket server port + + uint16_t server_port{DEFAULT_SERVER_PORT}; ///< WebSocket server port + uint8_t server_max_connections{2}; ///< Maximum simultaneous connections (default: 2 + ///< for handoff protocol) + uint16_t httpd_ctrl_port{0}; ///< ESP-IDF httpd control port; 0 = ESP_HTTPD_DEF_CTRL_PORT + ///< + 1 (avoids conflict with web_server component) static constexpr int64_t DEFAULT_BURST_INTERVAL_MS = 10000; ///< Default ms between bursts static constexpr int64_t DEFAULT_BURST_TIMEOUT_MS = 10000; ///< Default burst timeout ms diff --git a/src/connection_manager.cpp b/src/connection_manager.cpp index 694c7e0..94c1d4f 100644 --- a/src/connection_manager.cpp +++ b/src/connection_manager.cpp @@ -96,6 +96,7 @@ void ConnectionManager::init_server(SendspinClient* client) { this->client_ = client; this->ws_server_ = std::make_unique(); + this->ws_server_->set_port(this->client_->config_.server_port); this->ws_server_->set_max_connections(this->client_->config_.server_max_connections); this->ws_server_->set_ctrl_port(this->client_->config_.httpd_ctrl_port); diff --git a/src/esp/ws_server.cpp b/src/esp/ws_server.cpp index 85bd192..13ae936 100644 --- a/src/esp/ws_server.cpp +++ b/src/esp/ws_server.cpp @@ -41,7 +41,7 @@ namespace sendspin { // // Lifecycle: // 1. SendspinClient calls start() with callbacks and configuration -// 2. Server listens on port 8928 at /sendspin +// 2. Server listens on the configured port at /sendspin // 3. open_callback() creates SendspinServerConnection instances // 4. SendspinClient receives connection via new_connection_callback // 5. SendspinClient manages connection ownership and handoff logic @@ -68,7 +68,7 @@ bool SendspinWsServer::start(SendspinClient* client, bool task_stack_in_psram, config.task_caps = MALLOC_CAP_SPIRAM; } config.task_priority = task_priority; - config.server_port = 8928; + config.server_port = this->server_port_; config.max_open_sockets = this->max_connections_; config.open_fn = SendspinWsServer::open_callback; config.close_fn = SendspinWsServer::close_callback; diff --git a/src/esp/ws_server.h b/src/esp/ws_server.h index 52d846f..55a8f14 100644 --- a/src/esp/ws_server.h +++ b/src/esp/ws_server.h @@ -17,6 +17,8 @@ #pragma once +#include "sendspin/config.h" + #include #include @@ -41,7 +43,7 @@ class SendspinServerConnection; * observer for routing and handoff decisions. * * Capabilities: - * - Accepts incoming WebSocket connections on a dedicated port + * - Accepts incoming WebSocket connections on a configurable dedicated port * - Routes WebSocket messages directly via the session-pinned shared_ptr (no cross-thread * find-by-sockfd lookup is needed) * - Manages open/close callbacks to notify the client of connection lifecycle events @@ -115,6 +117,12 @@ class SendspinWsServer { this->max_connections_ = max_connections; } + /// @brief Sets the TCP port the WebSocket server listens on + /// @param port Port number. + void set_port(uint16_t port) { + this->server_port_ = port; + } + /// @brief Overrides the ESP-IDF httpd control port /// Defaults to 0 (uses ESP_HTTPD_DEF_CTRL_PORT + 1 to avoid conflict with web_server). /// @param ctrl_port Control port number; 0 = use default. @@ -172,11 +180,14 @@ class SendspinWsServer { /// @brief The HTTP server handle httpd_handle_t server_{nullptr}; - // 8-bit fields + // Numeric fields /// @brief Maximum number of simultaneous connections (default: 2 for handoff) uint8_t max_connections_{2}; + /// @brief TCP port the WebSocket server listens on + uint16_t server_port_{SendspinClientConfig::DEFAULT_SERVER_PORT}; + /// @brief httpd control port override (0 = use ESP_HTTPD_DEF_CTRL_PORT + 1) uint16_t ctrl_port_{0}; }; diff --git a/src/host/ws_server.cpp b/src/host/ws_server.cpp index df453f9..3e67ac0 100644 --- a/src/host/ws_server.cpp +++ b/src/host/ws_server.cpp @@ -25,9 +25,6 @@ namespace sendspin { static const char* const TAG = "sendspin.ws_server"; -/// @brief Default WebSocket server port -static constexpr uint16_t DEFAULT_SERVER_PORT = 8928U; - SendspinWsServer::~SendspinWsServer() { this->stop(); } @@ -42,7 +39,7 @@ bool SendspinWsServer::start(SendspinClient* client, bool /*task_stack_in_psram* this->client_ = client; // Create IXWebSocket server on the configured port - this->server_ = std::make_unique(DEFAULT_SERVER_PORT, "0.0.0.0"); + this->server_ = std::make_unique(this->server_port_, "0.0.0.0"); this->server_->setOnConnectionCallback( [this](const std::weak_ptr& weak_ws, @@ -106,7 +103,7 @@ bool SendspinWsServer::start(SendspinClient* client, bool /*task_stack_in_psram* } }); - SS_LOGI(TAG, "Starting server on port: %d (max connections: %d)", DEFAULT_SERVER_PORT, + SS_LOGI(TAG, "Starting server on port: %d (max connections: %d)", this->server_port_, this->max_connections_); auto result = this->server_->listen(); diff --git a/src/host/ws_server.h b/src/host/ws_server.h index 2fa9913..d474af7 100644 --- a/src/host/ws_server.h +++ b/src/host/ws_server.h @@ -18,6 +18,8 @@ #pragma once +#include "sendspin/config.h" + #include #include @@ -34,9 +36,9 @@ class SendspinServerConnection; /** * @brief WebSocket server that listens for incoming Sendspin client connections (host build) * - * Wraps an IXWebSocket server listening on port 8928. When a client connects, a - * SendspinServerConnection is created and delivered via the NewConnectionCallback. - * Connection close events are reported via ConnectionClosedCallback. + * Wraps an IXWebSocket server listening on the configured port. When a client connects, a + * SendspinServerConnection is created and delivered via the NewConnectionCallback. Connection + * close events are reported via ConnectionClosedCallback. * * Usage: * 1. Set the new_connection, connection_closed, and find_connection callbacks @@ -70,7 +72,7 @@ class SendspinWsServer { /// Returns a shared_ptr to keep the connection alive during message dispatch. using FindConnectionCallback = std::function(int sockfd)>; - /// @brief Starts the WebSocket server on port 8928 + /// @brief Starts the WebSocket server on the configured port /// @param client Pointer to the SendspinClient (stored for context). /// @param task_stack_in_psram Ignored on host builds. /// @param task_priority Ignored on host builds. @@ -98,6 +100,12 @@ class SendspinWsServer { this->max_connections_ = max_connections; } + /// @brief Sets the TCP port the WebSocket server listens on + /// @param port Port number. + void set_port(uint16_t port) { + this->server_port_ = port; + } + /// @brief No-op on host builds; control port is an ESP-IDF httpd concept void set_ctrl_port(uint16_t /*ctrl_port*/) {} @@ -133,10 +141,13 @@ class SendspinWsServer { /// @brief The IXWebSocket server instance std::unique_ptr server_; - // 8-bit fields + // Numeric fields /// @brief Maximum number of simultaneous connections (default: 2 for handoff) uint8_t max_connections_{2}; + + /// @brief TCP port the WebSocket server listens on + uint16_t server_port_{SendspinClientConfig::DEFAULT_SERVER_PORT}; }; } // namespace sendspin From c8bf446e4bd980c3c86f5f027822eaef961ed09a Mon Sep 17 00:00:00 2001 From: jb1228 <37507376+jb1228@users.noreply.github.com> Date: Mon, 25 May 2026 22:27:34 -0400 Subject: [PATCH 2/2] Logging uint16_t Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/host/ws_server.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/host/ws_server.cpp b/src/host/ws_server.cpp index 3e67ac0..4c2f57e 100644 --- a/src/host/ws_server.cpp +++ b/src/host/ws_server.cpp @@ -103,8 +103,8 @@ bool SendspinWsServer::start(SendspinClient* client, bool /*task_stack_in_psram* } }); - SS_LOGI(TAG, "Starting server on port: %d (max connections: %d)", this->server_port_, - this->max_connections_); + SS_LOGI(TAG, "Starting server on port: %u (max connections: %d)", + static_cast(this->server_port_), this->max_connections_); auto result = this->server_->listen(); if (!result.first) {