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..4c2f57e 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,8 +103,8 @@ bool SendspinWsServer::start(SendspinClient* client, bool /*task_stack_in_psram* } }); - SS_LOGI(TAG, "Starting server on port: %d (max connections: %d)", DEFAULT_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) { 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