Skip to content
Open
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
8 changes: 7 additions & 1 deletion dimos/teleop/phone/phone_teleop_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def __init__(self, **kwargs: Any) -> None:
self._stop_event = threading.Event()

# Embedded web server — RobotWebInterface provides FastAPI app + run()/shutdown()
self._web_server = RobotWebInterface(port=self.config.server_port)
self._web_server = self._create_web_server()
self._web_server_thread: threading.Thread | None = None

# Fingerprint-based message dispatch table
Expand All @@ -96,6 +96,12 @@ def __init__(self, **kwargs: Any) -> None:

self._setup_routes()

def _create_web_server(self) -> RobotWebInterface:
return RobotWebInterface(
host=self.config.g.listen_host,
port=self.config.server_port,
)
Comment thread
kezaer marked this conversation as resolved.

def _setup_routes(self) -> None:
"""Register teleop routes on the embedded web server."""

Expand Down
39 changes: 39 additions & 0 deletions dimos/teleop/phone/test_phone_teleop_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Copyright 2025-2026 Dimensional Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import Any

from dimos.core.global_config import GlobalConfig
from dimos.teleop.phone import phone_teleop_module
from dimos.teleop.phone.phone_teleop_module import PhoneTeleopConfig, PhoneTeleopModule


def test_phone_teleop_web_server_uses_module_global_config_listen_host(monkeypatch) -> None:
captured: dict[str, Any] = {}

class FakeRobotWebInterface:
def __init__(self, **kwargs: Any) -> None:
captured.update(kwargs)

monkeypatch.setattr(phone_teleop_module, "RobotWebInterface", FakeRobotWebInterface)

module = PhoneTeleopModule.__new__(PhoneTeleopModule)
module.config = PhoneTeleopConfig(
server_port=8444,
g=GlobalConfig(listen_host="0.0.0.0"),
)

module._create_web_server()

assert captured == {"host": "0.0.0.0", "port": 8444}
11 changes: 10 additions & 1 deletion dimos/web/robot_web_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,19 @@
class RobotWebInterface(FastAPIServer):
"""Wrapper class for the dimos-interface FastAPI server."""

def __init__(self, port: int = 5555, text_streams=None, audio_subject=None, **streams) -> None: # type: ignore[no-untyped-def]
def __init__( # type: ignore[no-untyped-def]
self,
port: int = 5555,
text_streams=None,
audio_subject=None,
*,
host: str | None = None,
**streams,
) -> None:
super().__init__(
dev_name="Robot Web Interface",
edge_type="Bidirectional",
host=host,
port=port,
text_streams=text_streams,
audio_subject=audio_subject,
Expand Down
37 changes: 34 additions & 3 deletions dimos/web/templates/rerun_dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,46 @@
</head>
<body>
<div class="container">
<iframe class="command-center" src="http://localhost:7779/command-center"></iframe>
<iframe class="command-center"></iframe>
<div class="divider" role="separator" aria-label="Resize panels"></div>
<iframe class="rerun" src="http://localhost:9090/?url=rerun%2Bhttp%3A%2F%2Flocalhost%3A9876%2Fproxy"></iframe>
<iframe class="rerun"></iframe>
</div>
<script>
(function () {
const params = new URLSearchParams(window.location.search);
const config = window.DIMOS_RERUN_CONFIG || {};
const defaultHost = window.location.hostname || '127.0.0.1';
const rerunHost = params.get('rerunHost') || config.host || defaultHost;
const rerunGrpcPort = params.get('rerunGrpcPort') || config.grpcPort || '9877';
const rerunWebPort = params.get('rerunWebPort') || config.webPort || '9878';
const commandCenter = document.querySelector('.command-center');
const rerun = document.querySelector('.rerun');

function hostForUrl(hostname) {
return hostname.includes(':') && !hostname.startsWith('[')
? `[${hostname}]`
: hostname;
}

function normalizeHttpProtocol(value) {
return value && value.replace(/:$/, '') === 'https' ? 'https' : 'http';
}

const rerunUrlHost = hostForUrl(rerunHost);
const pageProtocol = normalizeHttpProtocol(window.location.protocol);
const rerunWebProtocol = normalizeHttpProtocol(
params.get('rerunWebProtocol') || config.webProtocol || pageProtocol
);
const rerunGrpcProtocol = normalizeHttpProtocol(
params.get('rerunGrpcProtocol') || config.grpcProtocol || rerunWebProtocol
);
const rerunGrpcUrl = `rerun+${rerunGrpcProtocol}://${rerunUrlHost}:${rerunGrpcPort}/proxy`;

commandCenter.src = `${window.location.origin}/command-center`;
rerun.src = `${rerunWebProtocol}://${rerunUrlHost}:${rerunWebPort}/?url=${encodeURIComponent(rerunGrpcUrl)}`;

const container = document.querySelector('.container');
const divider = document.querySelector('.divider');
const commandCenter = document.querySelector('.command-center');
const body = document.body;
const minWidth = 0; // 16rem baseline for small screens

Expand Down
47 changes: 47 additions & 0 deletions dimos/web/templates/test_rerun_dashboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Copyright 2025-2026 Dimensional Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from pathlib import Path


def test_rerun_dashboard_uses_current_host_and_rerun_ports() -> None:
html = Path(__file__).with_name("rerun_dashboard.html").read_text()

assert "window.location.origin}/command-center" in html
assert "window.location.hostname" in html
assert "rerunGrpcPort" in html
assert "rerunWebPort" in html
assert "window.location.protocol" in html
assert "normalizeHttpProtocol" in html
assert "rerunWebProtocol" in html
assert "rerunGrpcProtocol" in html
assert "9877" in html
assert "9878" in html


def test_rerun_dashboard_does_not_use_stale_rerun_defaults() -> None:
html = Path(__file__).with_name("rerun_dashboard.html").read_text()

assert "localhost:9090" not in html
assert "localhost:9876" not in html
assert ":9090" not in html
assert ":9876" not in html


def test_rerun_dashboard_does_not_hardcode_rerun_web_scheme() -> None:
html = Path(__file__).with_name("rerun_dashboard.html").read_text()

assert "rerun.src = `http://" not in html
assert "rerun+http://${rerunUrlHost}" not in html
assert "rerun.src = `${rerunWebProtocol}://" in html
23 changes: 23 additions & 0 deletions dimos/web/test_robot_web_interface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Copyright 2025-2026 Dimensional Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from dimos.web.robot_web_interface import RobotWebInterface


def test_robot_web_interface_accepts_explicit_host() -> None:
interface = RobotWebInterface(host="0.0.0.0", port=8444)

assert interface.host == "0.0.0.0"
assert interface.port == 8444
assert "host" not in interface.streams
51 changes: 51 additions & 0 deletions dimos/web/websocket_vis/test_websocket_vis_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Copyright 2025-2026 Dimensional Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import Any

from dimos.core.global_config import GlobalConfig
from dimos.web.websocket_vis import websocket_vis_module
from dimos.web.websocket_vis.websocket_vis_module import WebsocketConfig, WebsocketVisModule


def test_websocket_vis_uses_module_global_config_listen_host(monkeypatch) -> None:
captured: dict[str, Any] = {}

class FakeServer:
def __init__(self, config: Any) -> None:
captured["config"] = config

def run(self) -> None:
captured["ran"] = True

def fake_config(app: Any, **kwargs: Any) -> dict[str, Any]:
captured["app"] = app
captured["kwargs"] = kwargs
return kwargs

monkeypatch.setattr(websocket_vis_module.uvicorn, "Config", fake_config)
monkeypatch.setattr(websocket_vis_module.uvicorn, "Server", FakeServer)

module = WebsocketVisModule.__new__(WebsocketVisModule)
module.app = object()
module.config = WebsocketConfig(
port=7779,
g=GlobalConfig(listen_host="0.0.0.0"),
)

module._run_uvicorn_server()

assert captured["kwargs"]["host"] == "0.0.0.0"
assert captured["kwargs"]["port"] == 7779
assert captured["ran"] is True
3 changes: 1 addition & 2 deletions dimos/web/websocket_vis/websocket_vis_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@

from dimos.constants import DEFAULT_THREAD_JOIN_TIMEOUT
from dimos.core.core import rpc
from dimos.core.global_config import global_config
from dimos.core.module import Module, ModuleConfig
from dimos.core.stream import In, Out
from dimos.mapping.models import LatLon
Expand Down Expand Up @@ -355,7 +354,7 @@ async def move_command(sid: str, data: dict[str, Any]) -> None:
def _run_uvicorn_server(self) -> None:
config = uvicorn.Config(
self.app, # type: ignore[arg-type]
host=global_config.listen_host,
host=self.config.g.listen_host,
port=self.config.port,
log_level="error", # Reduce verbosity
)
Expand Down
Loading