diff --git a/docs/README.md b/docs/README.md
index 61abc5f..83962f2 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -80,7 +80,7 @@ The PepperDash Essentials Web Config App provides several key features:
The app supports up to 10 simultaneous PepperDash Essentials program slots (`app01` through `app10`). After logging in, available program slots are automatically discovered and populated in the application selector dropdown in the top navigation bar. Navigating between app slots does not require re-authentication.
-On Crestron Virtual Control (VC-4), each room has its own Dev Tools at `https://[vc4-server]/VirtualControl/Rooms/[RoomId]/cws/debug/`, and the selector shows the room ID. The VC-4 server's firewall needs a few ports opened for the Debug Console, live routing and the Mobile Control UI; see [Use Dev Tools on Virtual Control (VC-4)](./how-to/use-dev-tools-on-vc4.md).
+On Crestron Virtual Control (VC-4), each room has its own Dev Tools at `https://[vc4-server]/VirtualControl/Rooms/[RoomId]/cws/debug/`, and the selector shows the room ID. The Debug Console and live routing work over HTTPS with no extra setup, and the Mobile Control UI needs one firewall port opened; see [Use Dev Tools on Virtual Control (VC-4)](./how-to/use-dev-tools-on-vc4.md).
## System Requirements
diff --git a/docs/how-to/README.md b/docs/how-to/README.md
index 6565f94..65d523b 100644
--- a/docs/how-to/README.md
+++ b/docs/how-to/README.md
@@ -37,7 +37,7 @@ These guides provide step-by-step solutions to specific problems you might encou
**You'll learn to:**
- Open Dev Tools for a VC-4 room and sign in
-- Open the firewall ports the Debug Console, routing and Mobile Control need
+- Open the firewall port the Mobile Control UI needs
- Accept the WebSocket certificate
---
diff --git a/docs/how-to/troubleshoot-connection.md b/docs/how-to/troubleshoot-connection.md
index b2bb8f1..a436630 100644
--- a/docs/how-to/troubleshoot-connection.md
+++ b/docs/how-to/troubleshoot-connection.md
@@ -212,7 +212,7 @@ Should return JSON data if the API is working.
- Corporate firewalls may block HTTPS to internal devices
- Some networks block self-signed certificates
- Guest networks may restrict device-to-device communication
-- On a VC-4 server, the host firewall blocks the Debug Console and routing WebSocket ports (65335–65534/tcp) and the Mobile Control direct server port until they're opened; see [Open the Firewall Ports](./use-dev-tools-on-vc4.md#open-the-firewall-ports)
+- On a VC-4 server, the host firewall blocks the Mobile Control direct server port until it's opened; see [Open the Mobile Control Port](./use-dev-tools-on-vc4.md#open-the-mobile-control-port). The Debug Console and routing use HTTPS polling there, so they need no extra ports
### DNS Resolution
diff --git a/docs/how-to/use-dev-tools-on-vc4.md b/docs/how-to/use-dev-tools-on-vc4.md
index b90aae3..1e783b7 100644
--- a/docs/how-to/use-dev-tools-on-vc4.md
+++ b/docs/how-to/use-dev-tools-on-vc4.md
@@ -6,8 +6,8 @@
## Requirements
-- An Essentials 3.0 build that places Dev Tools in the room's own `Html` folder ([PepperDash/Essentials#1532](https://github.com/PepperDash/Essentials/pull/1532)) and can start its secure WebSockets on VC-4 ([PepperDash/Essentials#1533](https://github.com/PepperDash/Essentials/pull/1533)). Earlier builds show a blank page, or fail to start Debug Console and routing sessions.
-- `sudo` access on the VC-4 server, to open firewall ports.
+- An Essentials 3.0 build that places Dev Tools in the room's own `Html` folder ([PepperDash/Essentials#1532](https://github.com/PepperDash/Essentials/pull/1532)) and serves the Debug Console and routing feedback over HTTP polling ([PepperDash/Essentials#1536](https://github.com/PepperDash/Essentials/pull/1536)). Earlier builds show a blank page, or the Debug Console and routing never connect.
+- `sudo` access on the VC-4 server, to open the Mobile Control port.
## Open Dev Tools
@@ -23,41 +23,34 @@ The browser shows a security warning for the server's certificate the first time
**Signing in**: VC-4 already controls access to the room, so a room usually has Crestron authentication turned off. Dev Tools then accepts any username and password. The application selector in the top bar shows the room ID where a 4-series shows `app01` to `app10`.
-## Open the Firewall Ports
+## How the Debug Console and Routing Connect
-The pages load over HTTPS on port 443, which VC-4 already allows. The Debug Console, live routing and the Mobile Control UI each connect to Essentials on a port of their own, and the VC-4 server's firewall blocks those until you open them:
+On a 4-series processor, the Debug Console and live routing each use a WebSocket on a port of its own. A VC-4 server's firewall only opens 443, so on VC-4 Dev Tools reads both over HTTPS instead. It long-polls the `debugSession/messages` and `routingFeedbackSession/messages` routes on the same port as every other page. Nothing needs opening in the firewall, and there's no separate certificate to accept.
-| Port | Used by | Needed for |
-| ------------------------------------------------------- | -------------------------- | ----------------------------------------------- |
-| 65435–65534/tcp | Debug console WebSocket | **Debug Console** page |
-| 65335–65433/tcp | Routing feedback WebSocket | Live updates on the **Routing** page |
-| The Mobile Control direct server port, 50002 by default | Mobile Control UI | Opening the UI from the **Mobile Control** page |
+Log lines and route changes still show up within about a second. The difference from a processor is a request every few seconds while a page is open.
-The two WebSocket servers pick a random port in their range the first time a session starts, so open the whole range rather than a single port.
+## Open the Mobile Control Port
+
+The Mobile Control UI is served by the room's Mobile Control direct server, on a port of its own: 50002 by default. Open it in the VC-4 server's firewall before using the UI from the **Mobile Control** page.
On a server that uses firewalld:
```bash
-sudo firewall-cmd --permanent --add-port=65335-65534/tcp
sudo firewall-cmd --permanent --add-port=50002/tcp
sudo firewall-cmd --reload
sudo firewall-cmd --list-ports
```
-The last command should list `65335-65534/tcp` and `50002/tcp`.
-
-**More than one room on the server**: every room shares the WebSocket ranges, so the rule above covers all of them. Each room's Mobile Control direct server needs its own port, set in that room's `appServer` device under `directServer.port`, and its own firewall rule.
-
-## Accept the WebSocket Certificate
+The last command should list `50002/tcp`.
-The Debug Console and routing WebSockets use the room's self-signed certificate. Browsers ask you to trust a self-signed certificate separately for each port, so the first connection to a new port can fail even after you accepted the certificate for the page.
+**More than one room on the server**: each room's Mobile Control direct server needs its own port, set in that room's `appServer` device under `directServer.port`, and its own firewall rule.
-When that happens, Dev Tools shows a link to the WebSocket's address:
+**Earlier setups**: an older version of this guide opened `65335-65534/tcp` for the WebSockets. Builds that poll don't use those ports, so you can remove the rule:
-1. Open the link and accept the certificate warning.
-2. Return to Dev Tools and start the session again.
-
-The ports are chosen again when the program restarts, so you may need to do this again after a restart.
+```bash
+sudo firewall-cmd --permanent --remove-port=65335-65534/tcp
+sudo firewall-cmd --reload
+```
## Troubleshooting
@@ -65,21 +58,15 @@ The ports are chosen again when the program restarts, so you may need to do this
The browser loaded the page but not its scripts. The room is running an Essentials build, or a Dev Tools build, from before VC-4 support. Load a build that meets the [requirements](#requirements).
-### "Failed to start WebSocket debug server" or "Failed to start routing feedback WebSocket server"
-
-The room's Essentials build looks for its certificate in `/user`, which doesn't exist on VC-4. Load a build that meets the [requirements](#requirements).
-
### The Debug Console or routing never connects
-Check the firewall first. On the VC-4 server:
+Check that the room's Essentials build has the polling routes. This should return JSON with a `next` field:
```bash
-sudo firewall-cmd --list-ports
-sudo ss -ltnp | grep -E ':(653[3-9][0-9]|654[0-9][0-9]|655[0-3][0-9])\b'
+curl -sk https://[vc4-server]/VirtualControl/Rooms/[RoomId]/cws/api/debugSession/messages
```
-- If the port range is missing from the firewall list, add it as shown [above](#open-the-firewall-ports).
-- If `ss` shows the room listening and the range is open, [accept the WebSocket certificate](#accept-the-websocket-certificate).
+A `404` means the build is from before HTTP polling. Load a build that meets the [requirements](#requirements). The browser's developer console logs `[poll]` warnings for any request that fails.
### The Mobile Control UI link won't open
diff --git a/docs/reference/api-endpoints.md b/docs/reference/api-endpoints.md
index e43ad82..796e780 100644
--- a/docs/reference/api-endpoints.md
+++ b/docs/reference/api-endpoints.md
@@ -472,6 +472,40 @@ GET /debugSession
---
+### Poll Debug Messages
+
+**Purpose**: Read the debug log over HTTP instead of the WebSocket. Dev Tools uses this on Virtual Control (VC-4), where the WebSocket ports are firewalled
+
+```http
+GET /debugSession/messages
+GET /debugSession/messages?since=42&wait=5000
+```
+
+**Query Parameters**:
+
+- `since` (number, optional): The `next` value from the previous response. Leave it out to start a session and get the starting cursor
+- `wait` (number, optional): Milliseconds to hold the request when nothing is new, up to 10000. Default 0
+
+**Response**:
+
+```json
+{
+ "next": 44,
+ "missed": false,
+ "messages": ["{\"Timestamp\":\"...\",\"Level\":\"Information\",...}", "..."]
+}
+```
+
+**Response Fields**:
+
+- `next` (number): Pass as `since` on the next request
+- `missed` (boolean): Some messages after `since` were dropped before they were fetched: the buffer overflowed, the session went idle for over a minute, it was stopped, or the program restarted
+- `messages` (string[]): Each one is the same JSON text the WebSocket sends, in the WebSocket message format above
+
+**Usage**: The server returns as soon as a message arrives, so polling again right after each response gives near-live updates. Messages are only collected while a client has polled in the last minute. `POST /debugSession` ends the session
+
+---
+
### Stop Debug Session
**Purpose**: Stop an active debug session and close WebSocket connections
@@ -747,6 +781,19 @@ GET /routingFeedbackSession
---
+### Poll Routing Feedback
+
+**Purpose**: Read routing feedback over HTTP instead of the WebSocket. Dev Tools uses this on Virtual Control (VC-4), where the WebSocket ports are firewalled
+
+```http
+GET /routingFeedbackSession/messages
+GET /routingFeedbackSession/messages?since=12&wait=5000
+```
+
+**Usage**: Works like [Poll Debug Messages](#poll-debug-messages). The first request, without `since`, returns the `snapshot` message as its only message. Later responses carry the same `midpointRouteChanged`, `sinkInputChanged` and `layoutChanged` messages the WebSocket sends. When a response has `missed: true`, start again without `since` for a fresh snapshot. `POST /routingFeedbackSession` ends the session
+
+---
+
### Execute Routing Command
**Purpose**: Make or clear a route, addressed entirely by device and port keys
diff --git a/src/App.tsx b/src/App.tsx
index a111d96..6032b06 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -21,6 +21,7 @@ import {
useGetDebugSessionMutation,
useStopDebugSessionMutation,
} from './store/apiSlice';
+import { apiUrl, streamsUseHttpPolling } from './shared/functions/hostPaths';
import { getRouteAppId } from './shared/functions/routeAppId';
import { AppDispatch, RootState } from './store/store';
import {
@@ -81,6 +82,17 @@ function App() {
const { isConnecting, isConnected } = store.getState().websocket;
if (!appId || isConnecting || isConnected) return;
const requestId = ++joinRequestIdRef.current;
+
+ if (streamsUseHttpPolling()) {
+ // The first poll starts the server-side session, so there's no WebSocket session to request
+ activeSessionAppIdRef.current = appId;
+ dispatch({
+ type: WS_CONNECT,
+ payload: { pollUrl: apiUrl(appId, 'debugSession/messages') },
+ });
+ return;
+ }
+
dispatch(connectionAttemptStarted());
try {
const res = await startSession({ appId }).unwrap();
diff --git a/src/features/ApiPaths.tsx b/src/features/ApiPaths.tsx
index 99156d3..00abce5 100644
--- a/src/features/ApiPaths.tsx
+++ b/src/features/ApiPaths.tsx
@@ -33,29 +33,31 @@ export const ApiPaths = () => {
return (