Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/how-to/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

---
Expand Down
2 changes: 1 addition & 1 deletion docs/how-to/troubleshoot-connection.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
49 changes: 18 additions & 31 deletions docs/how-to/use-dev-tools-on-vc4.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -23,63 +23,50 @@ 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

### The page is blank

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

Expand Down
47 changes: 47 additions & 0 deletions docs/reference/api-endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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();
Expand Down
46 changes: 24 additions & 22 deletions src/features/ApiPaths.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,29 +33,31 @@ export const ApiPaths = () => {
return (
<div className="d-flex flex-column overflow-hidden h-100">
<h2 className="mb-2">Available API Paths</h2>
<table className="table table-striped">
<thead>
<tr>
<th>Name</th>
<th>URL</th>
</tr>
</thead>
<tbody>
{sorted.map((path) => (
<tr
key={path.Name}
onClick={() => clickRow(path)}
className={
'cursor-pointer hover' +
(selectedRoute === path ? ' table-primary' : '')
}
>
<td>{path.Name}</td>
<td>{path.Url}</td>
<div className="overflow-auto flex-grow-1">
<table className="table table-striped">
<thead>
<tr>
<th>Name</th>
<th>URL</th>
</tr>
))}
</tbody>
</table>
</thead>
<tbody>
{sorted.map((path) => (
<tr
key={path.Name}
onClick={() => clickRow(path)}
className={
'cursor-pointer hover' +
(selectedRoute === path ? ' table-primary' : '')
}
>
<td className="text-break">{path.Name}</td>
<td className="text-break">{path.Url}</td>
</tr>
))}
</tbody>
</table>
</div>

<ApiPathDetailDrawer
show={showDrawer}
Expand Down
6 changes: 6 additions & 0 deletions src/features/ConfigFile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { skipToken } from '@reduxjs/toolkit/query';
import { useEffect, useRef } from 'react';
import { Button } from 'react-bootstrap';
import useAppParams from '../shared/hooks/useAppParams';
import useIsNarrowScreen from '../shared/hooks/useIsNarrowScreen';
import { useGetConfigQuery } from '../store/apiSlice';
import { livePolling, POLL_INTERVALS_MS } from '../store/polling';

Expand Down Expand Up @@ -60,6 +61,7 @@ export default ConfigFile;
const ConfigFileRender = ({ config }: { config: unknown }) => {
const monaco = useMonaco();
const editorRef = useRef<IConfigViewer | null>(null);
const isNarrow = useIsNarrowScreen();

useEffect(() => {
if (!monaco) return;
Expand All @@ -85,6 +87,10 @@ const ConfigFileRender = ({ config }: { config: unknown }) => {
language="json"
options={{
readOnly: true,
// On a phone the minimap takes a fifth of the width, and long lines need wrapping
minimap: { enabled: !isNarrow },
wordWrap: isNarrow ? 'on' : 'off',
lineNumbersMinChars: isNarrow ? 3 : 5,
}}
value={JSON.stringify(config, null, 2)}
onMount={handleEditorDidMount}
Expand Down
17 changes: 12 additions & 5 deletions src/features/DebugConsole/ConsoleWindow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,17 @@ const Content = ({ filteredItems }: ConsoleWindowProps) => {
: 'bg-white')
}
>
<Col md={6}>{message.Timestamp}</Col>
<Col md={3}>{message.Properties?.Key || 'global'}</Col>
<Col md={2}>{message.Level}</Col>
<Col md={13} className="text-nowrap text-truncate">
{/* On a phone: timestamp, key and level on one line, the message on the next */}
<Col xs={11} md={6} className="text-truncate small-sm">
{message.Timestamp}
</Col>
<Col xs={8} md={3} className="text-truncate small-sm">
{message.Properties?.Key || 'global'}
</Col>
<Col xs={5} md={2} className="text-truncate small-sm">
{message.Level}
</Col>
<Col xs={24} md={13} className="text-nowrap text-truncate">
{message.RenderedMessage}
</Col>
</Row>
Expand All @@ -57,7 +64,7 @@ const ConsoleWindow = ({ filteredItems }: ConsoleWindowProps) => {
return (
<>
<Container fluid>
<Row className="fw-bold">
<Row className="fw-bold d-none d-md-flex">
<Col md={6}>Timestamp</Col>
<Col md={3}>Key</Col>
<Col md={2}>Level</Col>
Expand Down
24 changes: 5 additions & 19 deletions src/features/DebugConsole/DebugConsole.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,9 @@ const DebugConsole = ({
<div className="d-flex align-items-center justify-content-start mb-2">
<h2>Debug Console</h2>
</div>
<div className="d-flex align-items-center justify-content-start mb-2">
<div className="d-flex flex-wrap align-items-center justify-content-start gap-2 mb-2">
{!isConnected ? (
<Button
className="mx-1"
variant="success"
size="sm"
disabled={isConnecting}
Expand All @@ -98,19 +97,13 @@ const DebugConsole = ({
{isConnecting ? 'Connecting...' : 'Start Debug Session'}
</Button>
) : (
<Button
className="mx-1"
variant="danger"
size="sm"
onClick={() => stop(appId)}
>
<Button variant="danger" size="sm" onClick={() => stop(appId)}>
Stop Debug Session
</Button>
)}
<MinimumLogLevelDropdown />
<Form.Check
type="checkbox"
className="mx-1"
label="Do Not Load Config on Next Boot"
name="doNotLoadConfig"
id="doNotLoadConfig"
Expand All @@ -126,7 +119,6 @@ const DebugConsole = ({
/>
{doNotLoadConfigOnNextBoot?.doNotLoadConfigOnNextBoot && (
<Button
className="mx-1"
variant="primary"
size="sm"
onClick={clickLoadConfig}
Expand All @@ -135,27 +127,21 @@ const DebugConsole = ({
Load Config
</Button>
)}
<Button className="mx-1" variant="primary" size="sm" onClick={clear}>
<Button variant="primary" size="sm" onClick={clear}>
Clear Console Trace
</Button>
<Button
className="mx-1"
variant="primary"
size="sm"
onClick={exportFilteredItems}
disabled={filteredItems.length === 0}
>
Export Log
</Button>
<Button
className="mx-1"
variant="primary"
size="sm"
onClick={clickRestart}
>
<Button variant="primary" size="sm" onClick={clickRestart}>
Restart Program
</Button>
<span className="ps-2">Message Count: {messages.length}</span>
<span className="small">Message Count: {messages.length}</span>
</div>
{certUrls && certUrls.length > 0 && (
<Alert
Expand Down
Loading
Loading