Skip to content
Draft
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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
*.onnx filter=lfs diff=lfs merge=lfs -text
*.engine filter=lfs diff=lfs merge=lfs -text
*.plan filter=lfs diff=lfs merge=lfs -text
docs/Vocabulary/ORBvoc.txt filter=lfs diff=lfs merge=lfs -text
212 changes: 148 additions & 64 deletions app/backend/node_manager.py

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion app/backend/routers/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@


def _db_root() -> Path:
return Path(os.environ.get('TINYNAV_DB_PATH', '/tinynav/tinynav_db'))
default_root = Path(__file__).resolve().parents[3] / 'tinynav_db'
return Path(os.environ.get('TINYNAV_DB_PATH', str(default_root)))


def _path_size(p: Path) -> int:
Expand Down
5 changes: 3 additions & 2 deletions app/backend/routers/map.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import json
import os
import re
from pathlib import Path
from typing import Optional

from fastapi import APIRouter, HTTPException
Expand Down Expand Up @@ -77,7 +78,7 @@ def map_set_active(map_name: str):
import shutil
if not re.match(r'^[a-zA-Z0-9_\-]+$', map_name):
raise HTTPException(400, 'Invalid map name')
root = os.environ.get('TINYNAV_DB_PATH', '/tinynav/tinynav_db')
root = os.environ.get('TINYNAV_DB_PATH', str(Path(__file__).resolve().parents[3] / 'tinynav_db'))
src = os.path.join(root, 'maps', map_name)
if not os.path.isdir(src):
raise HTTPException(404, f'Map {map_name!r} not found')
Expand All @@ -93,7 +94,7 @@ def map_set_active(map_name: str):
def _resolve_map_path(map_name: str) -> str:
if not re.match(r'^[a-zA-Z0-9_\-]+$', map_name):
raise HTTPException(400, 'Invalid map name')
root = os.environ.get('TINYNAV_DB_PATH', '/tinynav/tinynav_db')
root = os.environ.get('TINYNAV_DB_PATH', str(Path(__file__).resolve().parents[3] / 'tinynav_db'))
path = os.path.join(root, 'maps', map_name)
if not os.path.isdir(path) or not os.path.exists(os.path.join(path, 'occupancy_grid.npy')):
raise HTTPException(404, f'Map {map_name!r} not found')
Expand Down
3 changes: 2 additions & 1 deletion app/backend/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import os
from .node_manager import NodeRunner

TINYNAV_DB_PATH = os.environ.get('TINYNAV_DB_PATH', '/tinynav/tinynav_db')
_DEFAULT_TINYNAV_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
TINYNAV_DB_PATH = os.environ.get('TINYNAV_DB_PATH', os.path.join(_DEFAULT_TINYNAV_ROOT, 'tinynav_db'))

runner = NodeRunner(tinynav_db_path=TINYNAV_DB_PATH)
3 changes: 2 additions & 1 deletion app/backend/ws.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from __future__ import annotations

import asyncio
import base64
import json
import os
import time
Expand Down Expand Up @@ -164,7 +165,7 @@ def _on_frame(frame: bytes):
try:
while True:
frame = await queue.get()
await ws.send_bytes(frame)
await ws.send_text(base64.b64encode(frame).decode('ascii'))
except WebSocketDisconnect:
pass
finally:
Expand Down
2 changes: 2 additions & 0 deletions app/frontend/lib/core/providers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,10 @@ final previewStreamProvider =
ref.onDispose(() => channel.sink.close());

return channel.stream.map((data) {
if (data is String) return base64Decode(data);
if (data is Uint8List) return data;
if (data is List<int>) return Uint8List.fromList(data);
if (data is ByteBuffer) return Uint8List.view(data);
return Uint8List(0);
}).where((b) => b.isNotEmpty);
});
Expand Down
5 changes: 3 additions & 2 deletions app/frontend/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ import 'pages/setup_page.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final prefs = await SharedPreferences.getInstance();
final savedIp = prefs.getString('device_ip');
const defaultDeviceIp = '169.254.10.1';
final savedIp = prefs.getString('device_ip') ?? defaultDeviceIp;

runApp(
ProviderScope(
overrides: [
sharedPreferencesProvider.overrideWithValue(prefs),
if (savedIp != null) deviceIpProvider.overrideWith((ref) => savedIp),
deviceIpProvider.overrideWith((ref) => savedIp),
],
child: const TinyNavApp(),
),
Expand Down
21 changes: 9 additions & 12 deletions app/frontend/lib/pages/operate_tab.dart
Original file line number Diff line number Diff line change
Expand Up @@ -851,26 +851,23 @@ class _CameraPanelState extends ConsumerState<_CameraPanel> {
}
});

if (selectedTopic != null) {
ref.listen<AsyncValue<Uint8List>>(
previewStreamProvider(selectedTopic),
(_, next) {
if (next case AsyncData(:final value)) {
if (mounted) setState(() => _latestFrame = value);
}
},
);
final previewFrame = selectedTopic == null
? null
: ref.watch(previewStreamProvider(selectedTopic)).valueOrNull;
final frameToShow = previewFrame ?? _latestFrame;
if (previewFrame != null && !identical(previewFrame, _latestFrame)) {
_latestFrame = previewFrame;
}

return Container(
color: Colors.black,
child: Stack(
fit: StackFit.expand,
children: [
if (selectedTopic != null && _latestFrame != null)
if (selectedTopic != null && frameToShow != null)
GestureDetector(
onTap: () => _showFullscreen(context),
child: Image.memory(_latestFrame!, fit: BoxFit.contain, gaplessPlayback: true),
child: Image.memory(frameToShow, fit: BoxFit.contain, gaplessPlayback: true),
)
else
Center(
Expand Down Expand Up @@ -942,7 +939,7 @@ class _CameraPanelState extends ConsumerState<_CameraPanel> {
),
),
),
if (selectedTopic != null && _latestFrame != null)
if (selectedTopic != null && frameToShow != null)
Positioned(
bottom: 8, right: 8,
child: GestureDetector(
Expand Down
2 changes: 2 additions & 0 deletions app/frontend/macos/Flutter/GeneratedPluginRegistrant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import FlutterMacOS
import Foundation

import shared_preferences_foundation
import url_launcher_macos

func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}
3 changes: 3 additions & 0 deletions docs/Vocabulary/ORBvoc.txt
Git LFS file not shown
20 changes: 20 additions & 0 deletions install_python_deps.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail

mkdir -p /userdata/tmp
export TMPDIR="${TMPDIR:-/userdata/tmp}"
export TEMP="${TMPDIR}"
export TMP="${TMPDIR}"

python3 -m pip install --upgrade pip

# Wheels may be unavailable on this device arch; fall back to source installs.
python3 -m pip install decord || python3 -m pip install 'git+https://github.com/dmlc/decord'
python3 -m pip install pydbow3 || python3 -m pip install 'git+https://github.com/JHMeusener/PyDBoW3.git'

python3 - <<'PY'
import decord
import pydbow3
print('decord_ok', getattr(decord, '__version__', 'unknown'))
print('pydbow3_ok', getattr(pydbow3, '__name__', 'pydbow3'))
PY
31 changes: 31 additions & 0 deletions run_backend.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
set -eo pipefail

APP_ROOT="/userdata/junlinp/tinynav"
DB_PATH_DEFAULT="/userdata/junlinp/tinynav_db"
VENV_PATH_DEFAULT="/userdata/junlinp/venv"

# Ensure ROS 2 Python packages (rclpy, msgs) are on PYTHONPATH.
if [ -f /opt/ros/humble/setup.bash ]; then
# setup.bash may reference unset vars; avoid nounset during source.
set +u
# shellcheck disable=SC1091
source /opt/ros/humble/setup.bash
set -u
else
set -u
fi

cd "$APP_ROOT"

export VENV_PATH="${VENV_PATH:-$VENV_PATH_DEFAULT}"
if [ -x "${VENV_PATH}/bin/python3" ]; then
export PATH="${VENV_PATH}/bin:${PATH}"
export VIRTUAL_ENV="${VENV_PATH}"
fi

export PYTHONPATH="$APP_ROOT:${PYTHONPATH:-}"
export TINYNAV_DB_PATH="${TINYNAV_DB_PATH:-$DB_PATH_DEFAULT}"
export LD_LIBRARY_PATH="/userdata/opencv-release/lib:/userdata/hobot/opt/hobot/deps:${LD_LIBRARY_PATH:-}"

exec python3 -m uvicorn app.backend.main:app --host 0.0.0.0 --port 8000
66 changes: 66 additions & 0 deletions scripts/setup_device_deps.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
set -euo pipefail

# Usage:
# DEVICE_IP=169.254.10.1 DEVICE_USER=root DEVICE_PASS='looper@0731' bash scripts/setup_device_deps.sh

DEVICE_IP="${DEVICE_IP:-169.254.10.1}"
DEVICE_USER="${DEVICE_USER:-root}"
DEVICE_PASS="${DEVICE_PASS:-}"

if [[ -z "${DEVICE_PASS}" ]]; then
echo "DEVICE_PASS is required" >&2
exit 1
fi

SSHPASS="sshpass -p ${DEVICE_PASS}"
SSH="${SSHPASS} ssh -o StrictHostKeyChecking=no ${DEVICE_USER}@${DEVICE_IP}"

HOST_UTC="$(date -u +"%Y-%m-%d %H:%M:%S")"

echo "[1/7] Sync device time (TLS-safe)"
${SSH} "date -u -s '${HOST_UTC}' >/dev/null"

echo "[1.5/7] Ensure temp build dir on userdata"
${SSH} "mkdir -p /userdata/tmp"

echo "[2/7] Install system build/runtime deps"
${SSH} "TMPDIR=/userdata/tmp TEMP=/userdata/tmp TMP=/userdata/tmp apt-get update && TMPDIR=/userdata/tmp TEMP=/userdata/tmp TMP=/userdata/tmp apt-get install -y \
ca-certificates \
build-essential cmake pkg-config git python3-dev \
libeigen3-dev libceres-dev \
libavcodec-dev libavformat-dev libavutil-dev libswresample-dev libswscale-dev \
libavfilter-dev libavdevice-dev \
ros-humble-sensor-msgs-py"

echo "[3/7] Install Python deps"
${SSH} "TMPDIR=/userdata/tmp TEMP=/userdata/tmp TMP=/userdata/tmp python3 -m pip install --upgrade pip"
${SSH} "TMPDIR=/userdata/tmp TEMP=/userdata/tmp TMP=/userdata/tmp python3 -m pip install \
fastapi 'uvicorn[standard]' pydantic websockets pillow \
numpy<2 scipy numba fufpy async-lru codetiming tqdm einops av pybind11"

echo "[4/7] Build tinynav_cpp_bind with single thread"
${SSH} "export TMPDIR=/userdata/tmp TEMP=/userdata/tmp TMP=/userdata/tmp && cd /userdata/junlinp/tinynav/tinynav/cpp && \
rm -rf build && mkdir -p build && cd build && \
PYBIND11_DIR=\$(python3 -m pybind11 --cmakedir) && \
cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS_RELEASE='-O0 -g0' -Dpybind11_DIR=\"\${PYBIND11_DIR}\" && \
cmake --build . -- -j1 && \
cp -f *.so /userdata/junlinp/tinynav/tinynav/"

echo "[5/7] Ensure logs dir exists"
${SSH} "mkdir -p /userdata/junlinp/logs"

echo "[6/7] Verify critical imports"
${SSH} "source /opt/ros/humble/setup.bash && \
PYTHONPATH=/userdata/junlinp/tinynav:\${PYTHONPATH:-} \
python3 - <<'PY'
import tinynav.tinynav_cpp_bind as m
import sensor_msgs_py
from codetiming import Timer
import einops, tqdm, scipy, numba
print('cpp_bind_ok', hasattr(m, 'pose_graph_solve'))
print('sensor_msgs_py_ok')
print('deps_ok')
PY"

echo "[7/7] Done"
82 changes: 76 additions & 6 deletions scripts/start_app.sh
Original file line number Diff line number Diff line change
@@ -1,10 +1,80 @@
#!/bin/bash

BACKEND_PORT="${BACKEND_PORT:-8000}"
FRONTEND_PORT="${FRONTEND_PORT:-80}"
TINYNAV_DB_PATH="${TINYNAV_DB_PATH:-/tinynav/tinynav_db}"
DEVICE_PROXY_ENABLE="${DEVICE_PROXY_ENABLE:-1}"
DEVICE_PROXY_LISTEN_HOST="${DEVICE_PROXY_LISTEN_HOST:-0.0.0.0}"
DEVICE_PROXY_LISTEN_PORT="${DEVICE_PROXY_LISTEN_PORT:-8000}"
DEVICE_PROXY_TARGET_HOST="${DEVICE_PROXY_TARGET_HOST:-169.254.10.1}"
DEVICE_PROXY_TARGET_PORT="${DEVICE_PROXY_TARGET_PORT:-8000}"
DEVICE_PROXY_SCRIPT="/tmp/tinynav_device_proxy.py"

tmux new-session -s app \; \
split-window -h \; \
select-pane -t 0 \; send-keys "cd /tinynav && TINYNAV_DB_PATH=$TINYNAV_DB_PATH uvicorn app.backend.main:app --host 0.0.0.0 --port $BACKEND_PORT" C-m \; \
select-pane -t 1 \; send-keys "python -m http.server $FRONTEND_PORT --directory /tinynav/app/frontend/build/web" C-m
cat > "$DEVICE_PROXY_SCRIPT" <<'PY'
import socket
import threading

LHOST = "__LHOST__"
LPORT = __LPORT__
THOST = "__THOST__"
TPORT = __TPORT__


def pump(src, dst):
try:
while True:
data = src.recv(65536)
if not data:
break
dst.sendall(data)
except Exception:
pass
finally:
try:
dst.shutdown(socket.SHUT_WR)
except Exception:
pass


def handle(client):
try:
target = socket.create_connection((THOST, TPORT), timeout=5)
except Exception:
client.close()
return

t1 = threading.Thread(target=pump, args=(client, target), daemon=True)
t2 = threading.Thread(target=pump, args=(target, client), daemon=True)
t1.start()
t2.start()
t1.join()
t2.join()
client.close()
target.close()


server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((LHOST, LPORT))
server.listen(64)
print(f"proxy {LHOST}:{LPORT} -> {THOST}:{TPORT}", flush=True)

while True:
c, _ = server.accept()
threading.Thread(target=handle, args=(c,), daemon=True).start()
PY

sed -i "s/__LHOST__/$DEVICE_PROXY_LISTEN_HOST/; s/__LPORT__/$DEVICE_PROXY_LISTEN_PORT/; s/__THOST__/$DEVICE_PROXY_TARGET_HOST/; s/__TPORT__/$DEVICE_PROXY_TARGET_PORT/" "$DEVICE_PROXY_SCRIPT"

# Stop any stale background proxy from older script versions.
pkill -f "$DEVICE_PROXY_SCRIPT" >/dev/null 2>&1 || true

tmux kill-session -t app >/dev/null 2>&1 || true

if [ "$DEVICE_PROXY_ENABLE" = "1" ]; then
tmux new-session -s app \; \
split-window -h \; \
select-pane -t 0 \; send-keys "python -m http.server $FRONTEND_PORT --directory /tinynav/app/frontend/build/web" C-m \; \
select-pane -t 1 \; send-keys "python3 $DEVICE_PROXY_SCRIPT" C-m
else
tmux new-session -s app \; \
send-keys "python -m http.server $FRONTEND_PORT --directory /tinynav/app/frontend/build/web" C-m
fi
Loading
Loading