Skip to content

Commit 34b89fa

Browse files
committed
Add test signaling server scripts
1 parent e7d531e commit 34b89fa

File tree

6 files changed

+211
-0
lines changed

6 files changed

+211
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
/target
22
Cargo.lock
3+
4+
scripts/venv
5+
scripts/certs

README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,51 @@ cargo run -- start
9292
cargo run -- -vvv start
9393
```
9494

95+
## Developer Signaling Server
96+
97+
In addition to running the Robot Teleapp webapp at https://github.com/ModulrCloud/robot-teleop-webapp, the `scripts` folder provides a local signaling server for testing robot connections. Note that this doesn't expose the server to the internet (without further user configuration), so only robots on the same LAN will be able to access it.
98+
99+
Python with virtualenv is recommended to run the server. To install dependencies and create certificates on Ubuntu:
100+
101+
```bash
102+
sudo apt install -y python3-virtualenv
103+
cd scripts
104+
virtualenv venv
105+
source venv/bin/activate
106+
pip install -r requirements.txt
107+
./make_creds.sh
108+
```
109+
110+
After creating the certificates, your OS will need to trust that certificate before you can connect using it. On Windows, this means following the steps [in this guide](https://learn.microsoft.com/en-us/skype-sdk/sdn/articles/installing-the-trusted-root-certificate).
111+
112+
Linux can accomplish the same using these commands:
113+
114+
```bash
115+
sudo cp certs/dev-root-ca.pem /usr/local/share/ca-certificates/dev-root-ca.crt
116+
sudo update-ca-certificates
117+
```
118+
119+
Once the CA has been added, run the signaling server:
120+
121+
```bash
122+
python signaling_server.py
123+
```
124+
125+
You can test a connection by sourcing the virtual environment in another terminal and running:
126+
127+
```bash
128+
python test_signaling_server.py
129+
```
130+
131+
Check the logs of the signaling server to see that a robot with ID robot1 has connected and disconnected. If this is successful, use your local IP address as the signaling server for both your robot and the teleop webapp. The address is `wss://<your ip>:8765`, e.g. `wss://192.168.0.200:8765`. The argument `--allow-skip-cert-check` will also need to be passed to the robot. For example:
132+
133+
```bash
134+
# Set the new signaling server URL to a test config
135+
cargo run -- initial-setup --config-override ./test_config.json --robot-id testrobot --signaling-url wss://192.168.0.200:8765
136+
# Run the agent, skipping the security checks
137+
cargo run -- -vvv start --config-override ./test_config.json --allow-skip-cert-check
138+
```
139+
95140
## Running in Simulation
96141

97142
If not running on a real robot, you can test the system using a Turtlebot simulation. Install the turtlebot simulator using:

scripts/make_creds.sh

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
#!/usr/bin/env bash
2+
set -e
3+
4+
# -----------------------------
5+
# Config
6+
# -----------------------------
7+
ROOT_CA_NAME="dev-root-ca"
8+
SERVER_CERT_NAME="dev-server"
9+
CERT_DIR="./certs"
10+
DAYS=3650 # Validity in days
11+
12+
# -----------------------------
13+
# Create certs directory
14+
# -----------------------------
15+
mkdir -p "$CERT_DIR"
16+
17+
# -----------------------------
18+
# Detect local IP address
19+
# -----------------------------
20+
LOCAL_IP=$(hostname -I | awk '{print $1}')
21+
echo "Detected local IP: $LOCAL_IP"
22+
23+
# -----------------------------
24+
# Generate Root CA
25+
# -----------------------------
26+
echo "Generating root CA..."
27+
openssl genrsa -out "$CERT_DIR/$ROOT_CA_NAME.key" 2048
28+
openssl req -x509 -new -nodes -key "$CERT_DIR/$ROOT_CA_NAME.key" \
29+
-sha256 -days $DAYS -out "$CERT_DIR/$ROOT_CA_NAME.pem" \
30+
-subj "/C=US/ST=Local/L=Local/O=Dev/OU=Dev/CN=DevRootCA"
31+
32+
# -----------------------------
33+
# Generate server key & CSR
34+
# -----------------------------
35+
echo "Generating server key & CSR..."
36+
openssl genrsa -out "$CERT_DIR/$SERVER_CERT_NAME.key" 2048
37+
openssl req -new -key "$CERT_DIR/$SERVER_CERT_NAME.key" \
38+
-out "$CERT_DIR/$SERVER_CERT_NAME.csr" \
39+
-subj "/C=US/ST=Local/L=Local/O=Dev/OU=Dev/CN=localhost"
40+
41+
# -----------------------------
42+
# Create SAN config file
43+
# -----------------------------
44+
SAN_CONF="$CERT_DIR/san.conf"
45+
cat > "$SAN_CONF" <<EOL
46+
[req]
47+
distinguished_name = req_distinguished_name
48+
req_extensions = v3_req
49+
50+
[req_distinguished_name]
51+
52+
[v3_req]
53+
subjectAltName = @alt_names
54+
55+
[alt_names]
56+
DNS.1 = localhost
57+
IP.1 = 127.0.0.1
58+
IP.2 = $LOCAL_IP
59+
EOL
60+
61+
# -----------------------------
62+
# Generate server certificate signed by root CA
63+
# -----------------------------
64+
echo "Signing server certificate..."
65+
openssl x509 -req -in "$CERT_DIR/$SERVER_CERT_NAME.csr" \
66+
-CA "$CERT_DIR/$ROOT_CA_NAME.pem" -CAkey "$CERT_DIR/$ROOT_CA_NAME.key" \
67+
-CAcreateserial -out "$CERT_DIR/$SERVER_CERT_NAME.crt" \
68+
-days $DAYS -sha256 -extfile "$SAN_CONF" -extensions v3_req
69+
70+
# -----------------------------
71+
# Done
72+
# -----------------------------
73+
echo "✅ Certificates generated in $CERT_DIR:"
74+
echo " - Root CA: $CERT_DIR/$ROOT_CA_NAME.pem (import into browser)"
75+
echo " - Server cert: $CERT_DIR/$SERVER_CERT_NAME.crt"
76+
echo " - Server key: $CERT_DIR/$SERVER_CERT_NAME.key"
77+
78+
echo ""
79+
echo "Next steps:"
80+
echo "1. Import $CERT_DIR/$ROOT_CA_NAME.pem into your OS/browser as a trusted CA."
81+
echo "2. Use $CERT_DIR/$SERVER_CERT_NAME.crt and $CERT_DIR/$SERVER_CERT_NAME.key in your WSS server."

scripts/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
websockets

scripts/signaling_server.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import asyncio, websockets, json, ssl
2+
3+
4+
clients = {}
5+
6+
async def handler(ws):
7+
print("Waiting for connections. Connected clients: {}".format(clients))
8+
async for message in ws:
9+
msg = json.loads(message)
10+
print("Received message: {}".format(msg))
11+
12+
# Robot registers itself
13+
if msg["type"] == "register":
14+
clients[msg["from"]] = ws
15+
print(f"Registered {msg['from']}")
16+
continue
17+
18+
# Forward signaling messages
19+
target = clients.get(msg["to"])
20+
print("Found target: {}".format(target))
21+
if target:
22+
await target.send(json.dumps(msg))
23+
print("Connection closed")
24+
25+
26+
async def main():
27+
local_ip = "0.0.0.0"
28+
port = 8765
29+
30+
# TLS configuration
31+
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
32+
ssl_context.load_cert_chain("certs/dev-server.crt", "certs/dev-server.key")
33+
34+
print(f"Starting secure WebSocket server at wss://{local_ip}:{port}")
35+
async with websockets.serve(handler, local_ip, port, ssl=ssl_context):
36+
await asyncio.Future()
37+
38+
asyncio.run(main())

scripts/test_signaling_server.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import asyncio
2+
import websockets
3+
import json
4+
import ssl
5+
6+
SERVER_HOST = "localhost"
7+
SERVER_PORT = 8765
8+
URI = f"wss://{SERVER_HOST}:{SERVER_PORT}"
9+
10+
# SSL context (use self-signed cert)
11+
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
12+
ssl_context.check_hostname = False # Skip hostname verification (for self-signed)
13+
ssl_context.verify_mode = ssl.CERT_NONE # Skip certificate verification (for testing)
14+
15+
async def test_client():
16+
print(f"Connecting to wss://{SERVER_HOST}:{SERVER_PORT}...")
17+
18+
try:
19+
async with websockets.connect(
20+
URI,
21+
ssl=ssl_context
22+
) as websocket:
23+
print("Connected to server!")
24+
25+
register_msg = {
26+
"type": "register",
27+
"from": "robot1"
28+
}
29+
await websocket.send(json.dumps(register_msg))
30+
print("Sent registration: robot1")
31+
32+
print("Closing connection. Check logs of signaling server to verify success...")
33+
34+
except websockets.exceptions.ConnectionClosedError as e:
35+
print(f"Connection closed unexpectedly: {e}")
36+
except Exception as e:
37+
print(f"Error: {e}")
38+
finally:
39+
print("Test client finished.")
40+
41+
42+
if __name__ == "__main__":
43+
asyncio.run(test_client())

0 commit comments

Comments
 (0)