fix cogwheel not showing up

#1
by apirrone - opened
Files changed (3) hide show
  1. README.md +1 -2
  2. hello_world/main.py +224 -4
  3. pyproject.toml +1 -1
README.md CHANGED
@@ -20,8 +20,7 @@ models:
20
  datasets:
21
  - pollen-robotics/reachy-mini-emotions-library
22
  - pollen-robotics/reachy-mini-dances-library
23
- thumbnail: >-
24
- https://huggingface.co/spaces/panny247/hello_world/resolve/main/screenshots/thumbnail.png
25
  ---
26
 
27
  <div align="center">
 
20
  datasets:
21
  - pollen-robotics/reachy-mini-emotions-library
22
  - pollen-robotics/reachy-mini-dances-library
23
+ thumbnail: https://huggingface.co/spaces/panny247/hello_world/resolve/main/screenshots/thumbnail.png
 
24
  ---
25
 
26
  <div align="center">
hello_world/main.py CHANGED
@@ -1,13 +1,233 @@
1
  """
2
- Hello World - Main entry point.
3
 
4
- A developer-friendly dashboard app for the Reachy Mini robot.
 
5
  """
6
 
7
- from .app import HelloWorld
8
-
9
  __all__ = ['HelloWorld']
10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  if __name__ == "__main__":
12
  app = HelloWorld()
13
  try:
 
1
  """
2
+ HelloWorld Application Class.
3
 
4
+ The main application class that inherits from ReachyMiniApp.
5
+ Provides a Status dashboard with system telemetry and robot state.
6
  """
7
 
 
 
8
  __all__ = ['HelloWorld']
9
 
10
+ import logging
11
+ import os
12
+ import threading
13
+ import time
14
+ import urllib.request
15
+
16
+ from reachy_mini import ReachyMini, ReachyMiniApp
17
+
18
+ from .api import register_all_routes
19
+ from .api.oscillation import oscillation_tick
20
+ from .api.timers import timer_tick
21
+ from .api.alarms import alarm_tick
22
+ from .config import config
23
+ from .settings import load_settings
24
+ from .camera_registry import CameraRegistry
25
+ from .vision_inference import VisionInference
26
+ from .stats import (
27
+ get_cpu_stats, get_memory_stats, get_disk_stats, get_network_stats,
28
+ get_wifi_stats, get_uptime_stats, get_load_stats, get_fan_stats,
29
+ get_throttle_stats, get_disk_io_stats, get_top_processes, get_hardware_info,
30
+ get_thread_stats
31
+ )
32
+ from .websocket import register_websockets
33
+
34
+ logger = logging.getLogger("reachy_mini.app.hello_world")
35
+
36
+
37
+ class HelloWorld(ReachyMiniApp):
38
+ """A developer dashboard app for Reachy Mini with system telemetry."""
39
+
40
+ custom_app_url: str | None = "http://0.0.0.0:8042"
41
+ request_media_backend: str | None = "no_media" if os.environ.get("DEMO_MODE") else None
42
+
43
+ def __init__(self):
44
+ super().__init__()
45
+ self._reachy_mini = None
46
+ self._start_time = time.time()
47
+ # Tracking dicts for stats that need delta calculations
48
+ self._last_net = {"rx": 0, "tx": 0, "time": time.time()}
49
+ self._disk_cache = {"local": None, "remote": None, "swap": None, "time": 0}
50
+ self._disk_cache_ttl = 60
51
+ self._last_disk_io = {"read": 0, "write": 0, "time": time.time()}
52
+ self._process_cache = {"processes": [], "time": 0}
53
+ self._hardware_cache = {}
54
+ # Load persistent settings
55
+ self._settings = load_settings()
56
+ # Transcript WebSocket clients for conversation broadcast
57
+ self._transcribe_websockets = set()
58
+
59
+ # Oscillation state
60
+ self._oscillation_active = False
61
+ self._oscillation_pattern = None
62
+ self._oscillation_amplitude = 0.5
63
+ self._oscillation_speed = 1.0
64
+ self._oscillation_start_time = 0
65
+
66
+ # Move playback state
67
+ self._playing_move = None
68
+ self._playing_move_task = None
69
+
70
+ # Timer/alarm state
71
+ self._timers = {}
72
+ self._last_timer_tick = 0
73
+ self._last_alarm_tick = 0
74
+
75
+ # TTS/ambient coordination
76
+ self._tts_speaking = False
77
+
78
+ # Zenoh watchdog state
79
+ self._zenoh_last_check = 0
80
+ self._zenoh_last_reconnect = 0
81
+ self._zenoh_reconnect_backoff = 5 # seconds, grows on failure
82
+ self._zenoh_reconnect_failures = 0
83
+ self._zenoh_was_alive = True
84
+
85
+ # Camera registry (unified frame source for all consumers)
86
+ self._cameras = CameraRegistry(self)
87
+
88
+ # Vision inference engine
89
+ self._vision = VisionInference(self)
90
+
91
+ # Register WebSocket endpoints + cache control
92
+ if self.settings_app is not None:
93
+ register_websockets(self)
94
+
95
+ # Force browsers to revalidate HTML pages (prevents stale ?v=N references)
96
+ @self.settings_app.middleware("http")
97
+ async def cache_control(request, call_next):
98
+ response = await call_next(request)
99
+ ct = response.headers.get("content-type", "")
100
+ if "text/html" in ct:
101
+ response.headers["Cache-Control"] = "no-cache, must-revalidate"
102
+ return response
103
+
104
+ def _get_stats(self):
105
+ """Get all system stats using focused stat functions."""
106
+ result = {}
107
+ result.update(get_cpu_stats())
108
+ result.update(get_memory_stats())
109
+ result.update(get_disk_stats(self._disk_cache, self._disk_cache_ttl))
110
+ result.update(get_network_stats(self._last_net))
111
+ result.update(get_wifi_stats())
112
+ result.update(get_uptime_stats())
113
+ result.update(get_load_stats())
114
+ result.update(get_fan_stats())
115
+ result.update(get_throttle_stats())
116
+ result.update(get_disk_io_stats(self._last_disk_io))
117
+ result.update(get_top_processes(self._process_cache))
118
+ result.update(get_hardware_info(self._hardware_cache))
119
+ result.update(get_thread_stats())
120
+ return result
121
+
122
+ def run(self, reachy_mini: ReachyMini, stop_event: threading.Event):
123
+ """Main app loop."""
124
+ self._reachy_mini = reachy_mini
125
+
126
+ # Register all API routes
127
+ register_all_routes(self)
128
+
129
+ # Force OpenAPI schema regeneration after all routes are registered
130
+ # (uvicorn starts before routes are registered, causing stale cache)
131
+ if self.settings_app is not None:
132
+ self.settings_app.openapi_schema = None
133
+ self.settings_app.openapi()
134
+
135
+ # Restore active camera from settings
136
+ saved_camera = self._settings.get("active_camera", "robot")
137
+ if saved_camera != "robot":
138
+ self._cameras.set_active_camera(saved_camera)
139
+
140
+ # Apply saved motor mode on startup
141
+ saved_mode = self._settings.get("motor_mode", "enabled")
142
+ if saved_mode != "enabled":
143
+ try:
144
+ req = urllib.request.Request(
145
+ config.get_daemon_endpoint(f"motors/set_mode/{saved_mode}"),
146
+ method="POST"
147
+ )
148
+ with urllib.request.urlopen(req, timeout=config.TIMEOUTS['daemon_move']) as resp:
149
+ logger.info(f"Applied saved motor mode: {saved_mode}")
150
+ except Exception as e:
151
+ logger.warning(f"Failed to apply saved motor mode: {e}")
152
+
153
+ # Main loop (50Hz β€” oscillation tick + timer/alarm checks + sleep)
154
+ while not stop_event.is_set():
155
+ now = time.time()
156
+ self._zenoh_watchdog(now)
157
+ oscillation_tick(self, now)
158
+ timer_tick(self, now)
159
+ alarm_tick(self, now)
160
+ time.sleep(0.02)
161
+
162
+ def _zenoh_watchdog(self, now: float):
163
+ """Monitor Zenoh connection and auto-reconnect if it drops.
164
+
165
+ Checks every 5s. On disconnect, attempts to create a fresh
166
+ ReachyMini (which creates a new ZenohClient + Zenoh session).
167
+ Uses exponential backoff on repeated failures (5s β†’ 10s β†’ 20s β†’ 60s max).
168
+ """
169
+ # Rate limit checks to every 5 seconds
170
+ if now - self._zenoh_last_check < 5.0:
171
+ return
172
+ self._zenoh_last_check = now
173
+
174
+ if self._reachy_mini is None:
175
+ return
176
+
177
+ # Read connection state from the client's check_alive thread
178
+ try:
179
+ is_alive = self._reachy_mini.client._is_alive
180
+ except Exception:
181
+ is_alive = False
182
+
183
+ if is_alive:
184
+ if not self._zenoh_was_alive:
185
+ logger.info("Zenoh connection restored")
186
+ self._zenoh_reconnect_failures = 0
187
+ self._zenoh_reconnect_backoff = 5
188
+ self._zenoh_was_alive = True
189
+ return
190
+
191
+ # Connection is dead β€” log on first detection
192
+ if self._zenoh_was_alive:
193
+ logger.warning("Zenoh connection lost β€” watchdog will attempt reconnection")
194
+ self._zenoh_was_alive = False
195
+
196
+ # Respect backoff between reconnect attempts
197
+ if now - self._zenoh_last_reconnect < self._zenoh_reconnect_backoff:
198
+ return
199
+
200
+ self._zenoh_last_reconnect = now
201
+ self._zenoh_reconnect_failures += 1
202
+ logger.info(
203
+ f"Zenoh reconnect attempt {self._zenoh_reconnect_failures} "
204
+ f"(backoff {self._zenoh_reconnect_backoff}s)"
205
+ )
206
+
207
+ try:
208
+ # Disconnect old client (closes session, stops subscribers)
209
+ old_rm = self._reachy_mini
210
+ try:
211
+ old_rm.client.disconnect()
212
+ except Exception:
213
+ pass
214
+
215
+ # Create fresh ReachyMini with new Zenoh session
216
+ new_rm = ReachyMini(localhost_only=True, timeout=3.0)
217
+ self._reachy_mini = new_rm
218
+ self._zenoh_was_alive = True
219
+ self._zenoh_reconnect_failures = 0
220
+ self._zenoh_reconnect_backoff = 5
221
+ logger.info("Zenoh reconnected successfully β€” new session active")
222
+
223
+ except Exception as e:
224
+ # Backoff: 5 β†’ 10 β†’ 20 β†’ 40 β†’ 60 (cap)
225
+ self._zenoh_reconnect_backoff = min(60, self._zenoh_reconnect_backoff * 2)
226
+ logger.warning(
227
+ f"Zenoh reconnect failed: {e} β€” "
228
+ f"next attempt in {self._zenoh_reconnect_backoff}s"
229
+ )
230
+
231
  if __name__ == "__main__":
232
  app = HelloWorld()
233
  try:
pyproject.toml CHANGED
@@ -3,7 +3,7 @@ requires = ["setuptools>=61.0"]
3
  build-backend = "setuptools.build_meta"
4
 
5
  [project]
6
- name = "hello-world"
7
  version = "0.1.0"
8
  description = "A community hello world app for Reachy Mini with system telemetry dashboard"
9
  requires-python = ">=3.10"
 
3
  build-backend = "setuptools.build_meta"
4
 
5
  [project]
6
+ name = "hello_world"
7
  version = "0.1.0"
8
  description = "A community hello world app for Reachy Mini with system telemetry dashboard"
9
  requires-python = ">=3.10"