robopark 2.8.9 → 2.8.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "robopark",
3
- "version": "2.8.9",
3
+ "version": "2.8.10",
4
4
  "description": "RoboPark fleet control CLI — set up, watch, and drive a fleet of talking robots. The operator front-end over the infinicode mesh.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -3,7 +3,12 @@ FROM python:3.11-slim
3
3
 
4
4
  WORKDIR /app
5
5
 
6
- # Install dependencies
6
+ # Install build dependencies for pyaudio / opencv wheels, then Python packages.
7
+ RUN apt-get update && apt-get install -y --no-install-recommends \
8
+ gcc g++ libportaudio2 portaudio19-dev \
9
+ libgl1 libglib2.0-0 libsm6 libxext6 libxrender-dev \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
7
12
  COPY requirements.txt .
8
13
  RUN pip install --no-cache-dir -r requirements.txt
9
14
 
package/scheduler/main.py CHANGED
@@ -2542,7 +2542,7 @@ async def dashboard():
2542
2542
  <header class="mb-8 flex justify-between items-center">
2543
2543
  <div>
2544
2544
  <h1 class="text-3xl font-bold">🤖 RoboPark Control Center</h1>
2545
- <p class="text-gray-400">Session Scheduler Dashboard</p>
2545
+ <p class="text-gray-400">Session Scheduler Dashboard — Shenzhen Bay Park (test site)</p>
2546
2546
  </div>
2547
2547
  <div class="flex items-center gap-4">
2548
2548
  <button @click="openLiveKitConfig"
@@ -2902,6 +2902,160 @@ async def dashboard():
2902
2902
  </html>
2903
2903
  """
2904
2904
 
2905
+ # ── Test robot client page (browser-based, zero install on robot laptop) ──
2906
+ @app.get("/robot", response_class=HTMLResponse)
2907
+ async def robot_client_page():
2908
+ """Browser-based robot client for LAN testing.
2909
+
2910
+ Opens the laptop's webcam + mic, enrolls with the scheduler, and joins the
2911
+ LiveKit room returned by /request-session. Useful for iterating on scene
2912
+ detection, session triggering, and dashboard streaming before deploying to
2913
+ real Pis.
2914
+ """
2915
+ return """
2916
+ <!DOCTYPE html>
2917
+ <html>
2918
+ <head>
2919
+ <title>RoboPark Test Robot</title>
2920
+ <meta charset="utf-8">
2921
+ <meta name="viewport" content="width=device-width, initial-scale=1">
2922
+ <script src="https://cdn.tailwindcss.com"></script>
2923
+ <script src="https://cdn.jsdelivr.net/npm/livekit-client@1/dist/livekit-client.umd.min.js"></script>
2924
+ </head>
2925
+ <body class="bg-gray-900 text-white min-h-screen p-6">
2926
+ <div class="max-w-xl mx-auto space-y-4">
2927
+ <h1 class="text-2xl font-bold">RoboPark Test Robot</h1>
2928
+ <div id="status" class="text-sm text-gray-400">Idle — configure and start</div>
2929
+
2930
+ <div class="space-y-2">
2931
+ <label class="block text-sm">Scheduler URL</label>
2932
+ <input id="schedulerUrl" value="http://192.168.1.2:8080" class="w-full p-2 rounded bg-gray-800 border border-gray-700">
2933
+ </div>
2934
+ <div class="space-y-2">
2935
+ <label class="block text-sm">Enrollment Token</label>
2936
+ <input id="enrollmentToken" value="" placeholder="Paste token here" class="w-full p-2 rounded bg-gray-800 border border-gray-700">
2937
+ </div>
2938
+ <div class="space-y-2">
2939
+ <label class="block text-sm">Robot Name</label>
2940
+ <input id="robotName" value="laptop-robot" class="w-full p-2 rounded bg-gray-800 border border-gray-700">
2941
+ </div>
2942
+ <div class="flex gap-2">
2943
+ <button id="enrollBtn" class="px-4 py-2 bg-blue-600 rounded hover:bg-blue-500">1. Enroll</button>
2944
+ <button id="startBtn" class="px-4 py-2 bg-green-600 rounded hover:bg-green-500" disabled>2. Start Camera + Trigger</button>
2945
+ <button id="triggerBtn" class="px-4 py-2 bg-yellow-600 rounded hover:bg-yellow-500" disabled>Trigger Session</button>
2946
+ <button id="stopBtn" class="px-4 py-2 bg-red-600 rounded hover:bg-red-500" disabled>Stop</button>
2947
+ </div>
2948
+ <video id="localVideo" autoplay muted playsinline class="w-full rounded border border-gray-700 bg-black"></video>
2949
+ <div id="log" class="text-xs font-mono h-48 overflow-y-auto bg-gray-800 p-2 rounded"></div>
2950
+ </div>
2951
+
2952
+ <script>
2953
+ const log = (msg) => {
2954
+ const el = document.getElementById('log');
2955
+ el.innerText += `[${new Date().toLocaleTimeString()}] ${msg}\n`;
2956
+ el.scrollTop = el.scrollHeight;
2957
+ };
2958
+ const setStatus = (msg) => document.getElementById('status').innerText = msg;
2959
+
2960
+ const schedulerUrl = () => document.getElementById('schedulerUrl').value.replace(/\/$/, '');
2961
+ let deviceToken = null;
2962
+ let deviceId = null;
2963
+ let room = null;
2964
+ let heartbeatInterval = null;
2965
+ let stream = null;
2966
+
2967
+ async function post(path, body, headers = {}) {
2968
+ const res = await fetch(`${schedulerUrl()}${path}`, {
2969
+ method: 'POST',
2970
+ headers: { 'Content-Type': 'application/json', ...headers },
2971
+ body: JSON.stringify(body)
2972
+ });
2973
+ if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
2974
+ return res.json();
2975
+ }
2976
+
2977
+ document.getElementById('enrollBtn').onclick = async () => {
2978
+ try {
2979
+ const token = document.getElementById('enrollmentToken').value.trim();
2980
+ if (!token) return alert('Enter enrollment token from scheduler logs');
2981
+ const payload = {
2982
+ enrollment_token: token,
2983
+ name: document.getElementById('robotName').value,
2984
+ lan_ip: '192.168.1.x'
2985
+ };
2986
+ const data = await post('/api/devices/enroll', payload);
2987
+ deviceId = data.device_id;
2988
+ deviceToken = data.device_token;
2989
+ log(`Enrolled: ${deviceId}`);
2990
+ setStatus(`Enrolled ${deviceId}`);
2991
+ document.getElementById('startBtn').disabled = false;
2992
+ heartbeatInterval = setInterval(async () => {
2993
+ await fetch(`${schedulerUrl()}/api/devices/${deviceId}/heartbeat`, {
2994
+ method: 'POST',
2995
+ headers: { 'Authorization': `Bearer ${deviceToken}`, 'Content-Type': 'application/json' },
2996
+ body: JSON.stringify({ status: 'online', ip: '192.168.1.x' })
2997
+ });
2998
+ }, 5000);
2999
+ } catch (e) {
3000
+ log(`Enroll error: ${e.message}`);
3001
+ }
3002
+ };
3003
+
3004
+ document.getElementById('startBtn').onclick = async () => {
3005
+ try {
3006
+ stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
3007
+ document.getElementById('localVideo').srcObject = stream;
3008
+ log('Camera + mic active');
3009
+ document.getElementById('triggerBtn').disabled = false;
3010
+ document.getElementById('stopBtn').disabled = false;
3011
+ setStatus('Camera active — ready to trigger');
3012
+ } catch (e) {
3013
+ log(`Media error: ${e.message}`);
3014
+ }
3015
+ };
3016
+
3017
+ document.getElementById('triggerBtn').onclick = async () => {
3018
+ try {
3019
+ setStatus('Requesting session...');
3020
+ const session = await post(`/api/devices/${deviceId}/request-session`, {}, { 'Authorization': `Bearer ${deviceToken}` });
3021
+ log(`Session: ${session.session_id} room=${session.room_name}`);
3022
+
3023
+ const { Room, RoomEvent } = LiveKitClient;
3024
+ room = new Room({
3025
+ adaptiveStream: true,
3026
+ dynacast: true,
3027
+ audioCaptureDefaults: { autoGainControl: true, noiseSuppression: true, echoCancellation: true }
3028
+ });
3029
+ room.on(RoomEvent.Connected, () => log('LiveKit connected'));
3030
+ room.on(RoomEvent.Disconnected, (reason) => log(`LiveKit disconnected: ${reason}`));
3031
+ room.on(RoomEvent.ConnectionStateChanged, (state) => log(`LiveKit state: ${state}`));
3032
+
3033
+ await room.connect(session.server_url, session.token);
3034
+ await room.localParticipant.enableCameraAndMicrophone();
3035
+ log('Published camera + mic to room');
3036
+ await post(`/api/sessions/${session.session_id}/joined`, {}, { 'Authorization': `Bearer ${deviceToken}` });
3037
+ log('Marked session joined (latency recorded)');
3038
+ setStatus(`Streaming: ${session.room_name}`);
3039
+ } catch (e) {
3040
+ log(`Trigger error: ${e.message}`);
3041
+ setStatus('Trigger failed');
3042
+ }
3043
+ };
3044
+
3045
+ document.getElementById('stopBtn').onclick = async () => {
3046
+ if (room) { await room.disconnect(); room = null; }
3047
+ if (stream) { stream.getTracks().forEach(t => t.stop()); stream = null; }
3048
+ if (heartbeatInterval) { clearInterval(heartbeatInterval); heartbeatInterval = null; }
3049
+ log('Stopped');
3050
+ setStatus('Stopped');
3051
+ document.getElementById('triggerBtn').disabled = true;
3052
+ document.getElementById('stopBtn').disabled = true;
3053
+ };
3054
+ </script>
3055
+ </body>
3056
+ </html>
3057
+ """
3058
+
2905
3059
  if __name__ == "__main__":
2906
3060
  import uvicorn
2907
3061
  host = os.getenv("SCHEDULER_HOST", "0.0.0.0")
@@ -627,7 +627,12 @@ def main() -> None:
627
627
  loop = asyncio.new_event_loop()
628
628
  asyncio.set_event_loop(loop)
629
629
  for sig in (signal.SIGINT, signal.SIGTERM):
630
- loop.add_signal_handler(sig, agent.shutdown)
630
+ try:
631
+ loop.add_signal_handler(sig, agent.shutdown)
632
+ except NotImplementedError:
633
+ # add_signal_handler is POSIX-only (raises on Windows) — fall back to
634
+ # signal.signal, dispatched back onto the loop thread-safely.
635
+ signal.signal(sig, lambda *_: loop.call_soon_threadsafe(agent.shutdown))
631
636
 
632
637
  try:
633
638
  loop.run_until_complete(agent.run())