itsMarco-G commited on
Commit
532bf85
·
1 Parent(s): 99d3f7c

Deleted unused files

Browse files
.DS_Store DELETED
Binary file (6.15 kB)
 
README.md CHANGED
@@ -36,3 +36,8 @@ python reachy_phone_home_app.py --conf 0.15 --process-every 1
36
 
37
  ## Publishing
38
  Use the Reachy Mini App Assistant to check and publish this app.
 
 
 
 
 
 
36
 
37
  ## Publishing
38
  Use the Reachy Mini App Assistant to check and publish this app.
39
+
40
+ ## License
41
+ This repository is licensed under the Apache License 2.0. It uses Ultralytics YOLO
42
+ for detection, which is licensed under AGPL-3.0. Use of the Ultralytics component
43
+ is subject to the AGPL-3.0 terms. See `NOTICE` for details.
reachy_phone_home.egg-info/PKG-INFO DELETED
@@ -1,45 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: reachy_phone_home
3
- Version: 0.1.0
4
- Summary: Add your description here
5
- Keywords: reachy-mini-app
6
- Requires-Python: >=3.10
7
- Description-Content-Type: text/markdown
8
- Requires-Dist: reachy-mini
9
-
10
- ---
11
- title: Reachy Phone Home
12
- emoji: 📱
13
- colorFrom: yellow
14
- colorTo: blue
15
- sdk: static
16
- pinned: false
17
- short_description: Phone focus companion for Reachy Mini
18
- tags:
19
- - reachy_mini
20
- - reachy_mini_python_app
21
- ---
22
-
23
- # reachy_phone_home
24
-
25
- Phone focus companion for Reachy Mini. Reachy watches for your phone, encourages focus, and responds with movements.
26
-
27
- ## How it works
28
- - Uses Reachy Mini camera + YOLO phone/person detection.
29
- - Tracks phone use and triggers movements after configured heartbeats.
30
- - Includes a manual simulation app for development.
31
-
32
- ## Run locally
33
- ```bash
34
- python reachy_phone_home_app.py
35
- ```
36
- The YOLO26 weights are downloaded automatically on first run.
37
-
38
- ## Configure
39
- Key flags (examples):
40
- ```bash
41
- python reachy_phone_home_app.py --conf 0.15 --process-every 1
42
- ```
43
-
44
- ## Publishing
45
- Use the Reachy Mini App Assistant to check and publish this app.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
reachy_phone_home.egg-info/SOURCES.txt DELETED
@@ -1,13 +0,0 @@
1
- README.md
2
- pyproject.toml
3
- setup.cfg
4
- setup.py
5
- reachy_phone_home/__init__.py
6
- reachy_phone_home/main.py
7
- reachy_phone_home/movements.py
8
- reachy_phone_home.egg-info/PKG-INFO
9
- reachy_phone_home.egg-info/SOURCES.txt
10
- reachy_phone_home.egg-info/dependency_links.txt
11
- reachy_phone_home.egg-info/entry_points.txt
12
- reachy_phone_home.egg-info/requires.txt
13
- reachy_phone_home.egg-info/top_level.txt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
reachy_phone_home.egg-info/dependency_links.txt DELETED
@@ -1 +0,0 @@
1
-
 
 
reachy_phone_home.egg-info/entry_points.txt DELETED
@@ -1,2 +0,0 @@
1
- [reachy_mini_apps]
2
- reachy_phone_home = reachy_phone_home.main:ReachyPhoneHome
 
 
 
reachy_phone_home.egg-info/requires.txt DELETED
@@ -1 +0,0 @@
1
- reachy-mini
 
 
reachy_phone_home.egg-info/top_level.txt DELETED
@@ -1 +0,0 @@
1
- reachy_phone_home
 
 
reachy_phone_home/__pycache__/__init__.cpython-310.pyc DELETED
Binary file (182 Bytes)
 
reachy_phone_home/__pycache__/expressions.cpython-310.pyc DELETED
Binary file (2.5 kB)
 
reachy_phone_home/__pycache__/gestures.cpython-310.pyc DELETED
Binary file (6.77 kB)
 
reachy_phone_home/__pycache__/main.cpython-310.pyc DELETED
Binary file (10.3 kB)
 
reachy_phone_home/__pycache__/movements.cpython-310.pyc DELETED
Binary file (6.07 kB)
 
web_log_ui.html DELETED
@@ -1,748 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>Reachy Phone Home Logs</title>
7
- <style>
8
- :root {
9
- color-scheme: dark;
10
- --video-max-width: 720px;
11
- --video-aspect: 4 / 3;
12
- }
13
- body {
14
- margin: 0;
15
- font-family: "IBM Plex Mono", "SF Mono", Consolas, monospace;
16
- background: #0f1115;
17
- color: #e9e6e0;
18
- }
19
- header {
20
- padding: 16px 20px;
21
- border-bottom: 1px solid #232836;
22
- display: flex;
23
- align-items: center;
24
- justify-content: space-between;
25
- }
26
- .container {
27
- display: flex;
28
- flex-direction: column;
29
- align-items: center;
30
- }
31
- header h1 {
32
- margin: 0;
33
- font-size: 18px;
34
- }
35
- .status {
36
- font-size: 12px;
37
- color: #9aa4b2;
38
- }
39
- #video {
40
- padding: 16px 20px 0;
41
- display: flex;
42
- flex-direction: column;
43
- align-items: center;
44
- gap: 12px;
45
- }
46
- .video-frame {
47
- width: 100%;
48
- max-width: var(--video-max-width);
49
- aspect-ratio: var(--video-aspect);
50
- border-radius: 12px;
51
- border: 1px solid #232836;
52
- overflow: hidden;
53
- position: relative;
54
- background: #0b0e13;
55
- }
56
- #video img {
57
- position: absolute;
58
- inset: 0;
59
- width: 100%;
60
- height: 100%;
61
- object-fit: contain;
62
- display: block;
63
- }
64
- .video-placeholder {
65
- position: absolute;
66
- inset: 0;
67
- border-radius: 12px;
68
- background: #0b0e13;
69
- color: #7c8696;
70
- display: flex;
71
- align-items: center;
72
- justify-content: center;
73
- font-size: 12px;
74
- letter-spacing: 0.3px;
75
- }
76
- .video-controls {
77
- display: flex;
78
- align-items: center;
79
- gap: 12px;
80
- }
81
- .video-controls button {
82
- background: #1b2230;
83
- color: #e9e6e0;
84
- border: 1px solid #2b3447;
85
- padding: 8px 14px;
86
- border-radius: 8px;
87
- cursor: pointer;
88
- font-size: 13px;
89
- }
90
- .video-controls button:hover {
91
- background: #242d3f;
92
- }
93
- .warning {
94
- font-size: 12px;
95
- color: #d5a66b;
96
- }
97
- .instructions {
98
- margin: 16px 20px 0;
99
- max-width: 960px;
100
- padding: 14px 16px;
101
- border-radius: 12px;
102
- border: 1px solid #232836;
103
- background: #0b0e13;
104
- }
105
- .instructions h2 {
106
- margin: 0 0 8px 0;
107
- font-size: 14px;
108
- letter-spacing: 0.4px;
109
- text-transform: uppercase;
110
- color: #9aa4b2;
111
- }
112
- .instructions p {
113
- margin: 0 0 8px 0;
114
- color: #c9d1d9;
115
- }
116
- .instructions ol {
117
- margin: 0 0 8px 0;
118
- padding-left: 18px;
119
- color: #c9d1d9;
120
- }
121
- .instructions ul {
122
- margin: 0;
123
- padding-left: 18px;
124
- color: #c9d1d9;
125
- }
126
- .instructions li {
127
- margin-bottom: 6px;
128
- }
129
- .muted {
130
- color: #7c8696;
131
- }
132
- .cards {
133
- display: grid;
134
- gap: 12px;
135
- grid-template-columns: repeat(2, minmax(220px, 1fr));
136
- margin: 16px 20px 40px;
137
- max-width: 960px;
138
- width: min(960px, 100%);
139
- }
140
- .tabs {
141
- display: flex;
142
- gap: 8px;
143
- margin: 16px 20px 0;
144
- }
145
- .tab-btn {
146
- background: #1b2230;
147
- color: #e9e6e0;
148
- border: 1px solid #2b3447;
149
- padding: 6px 12px;
150
- border-radius: 999px;
151
- cursor: pointer;
152
- font-size: 12px;
153
- }
154
- .tab-btn.active {
155
- background: #2a3448;
156
- }
157
- .panel {
158
- width: 100%;
159
- max-width: 960px;
160
- display: flex;
161
- flex-direction: column;
162
- align-items: center;
163
- }
164
- .hero-img {
165
- width: min(720px, 100%);
166
- border-radius: 12px;
167
- border: 1px solid #232836;
168
- margin: 12px 20px 0;
169
- }
170
- .settings-card {
171
- border: 1px solid #232836;
172
- background: #0b0e13;
173
- border-radius: 12px;
174
- padding: 16px;
175
- flex: 0 0 420px;
176
- max-width: 420px;
177
- }
178
- .settings-row {
179
- display: flex;
180
- align-items: flex-start;
181
- justify-content: space-between;
182
- gap: 12px;
183
- margin-bottom: 12px;
184
- flex-wrap: wrap;
185
- }
186
- .settings-row label {
187
- font-size: 12px;
188
- color: #c9d1d9;
189
- flex: 1 1 140px;
190
- }
191
- .settings-row input,
192
- .settings-row select {
193
- background: #10141c;
194
- border: 1px solid #2b3447;
195
- color: #e9e6e0;
196
- padding: 6px 8px;
197
- border-radius: 8px;
198
- max-width: 100%;
199
- box-sizing: border-box;
200
- flex: 1 1 160px;
201
- }
202
- .settings-actions {
203
- display: flex;
204
- gap: 10px;
205
- flex-wrap: wrap;
206
- align-items: center;
207
- }
208
- .settings-actions select {
209
- flex: 1 1 160px;
210
- min-width: 0;
211
- }
212
- .settings-grid {
213
- display: flex;
214
- flex-wrap: wrap;
215
- gap: 12px;
216
- margin: 16px auto 40px;
217
- width: min(960px, 100%);
218
- box-sizing: border-box;
219
- justify-content: center;
220
- }
221
- .settings-card h4 {
222
- margin: 0 0 10px 0;
223
- font-size: 13px;
224
- color: #e9e6e0;
225
- }
226
- .card {
227
- border: 1px solid #232836;
228
- background: #0b0e13;
229
- border-radius: 12px;
230
- padding: 10px 12px;
231
- }
232
- .card h3 {
233
- margin: 0 0 8px 0;
234
- font-size: 13px;
235
- color: #e9e6e0;
236
- }
237
- .card .status-pill {
238
- display: inline-block;
239
- padding: 3px 8px;
240
- border-radius: 999px;
241
- font-size: 11px;
242
- background: #1b2230;
243
- color: #9aa4b2;
244
- border: 1px solid #2b3447;
245
- }
246
- .card.active .status-pill {
247
- background: #1a3b2c;
248
- color: #d6f8e4;
249
- border-color: #25533e;
250
- }
251
- .card.alert .status-pill {
252
- background: #4a1f1f;
253
- color: #ffd6d6;
254
- border-color: #6b2b2b;
255
- }
256
- .card .sub {
257
- margin-top: 6px;
258
- font-size: 11px;
259
- color: #7c8696;
260
- }
261
- .card-header {
262
- display: flex;
263
- align-items: center;
264
- justify-content: space-between;
265
- gap: 8px;
266
- }
267
- .event-line {
268
- margin: 0 20px 20px;
269
- max-width: 960px;
270
- color: #9aa4b2;
271
- font-size: 12px;
272
- text-align: center;
273
- }
274
- .log-panel {
275
- width: 100%;
276
- max-width: 720px;
277
- border-radius: 12px;
278
- border: 1px solid #232836;
279
- background: #0b0e13;
280
- padding: 10px 12px;
281
- }
282
- .log-toolbar {
283
- display: flex;
284
- align-items: center;
285
- justify-content: space-between;
286
- gap: 12px;
287
- margin-bottom: 8px;
288
- font-size: 12px;
289
- color: #9aa4b2;
290
- }
291
- .log-toolbar button {
292
- background: #1b2230;
293
- color: #e9e6e0;
294
- border: 1px solid #2b3447;
295
- padding: 6px 10px;
296
- border-radius: 8px;
297
- cursor: pointer;
298
- font-size: 12px;
299
- }
300
- .log-toolbar button:hover {
301
- background: #242d3f;
302
- }
303
- .log-body {
304
- height: 180px;
305
- overflow-y: auto;
306
- white-space: pre-wrap;
307
- line-height: 1.4;
308
- font-size: 12px;
309
- color: #c9d1d9;
310
- }
311
- .log-line {
312
- margin-bottom: 6px;
313
- }
314
- .status-actions {
315
- display: flex;
316
- justify-content: center;
317
- margin: 8px 0 16px;
318
- }
319
- </style>
320
- </head>
321
- <body>
322
- <header>
323
- <h1>Reachy Phone Home Logs</h1>
324
- <div class="status" id="status">Connecting...</div>
325
- </header>
326
- <div class="container">
327
- <img class="hero-img" src="/static/reachy_phone_home.PNG" alt="Reachy Phone Home" />
328
- <div class="tabs">
329
- <button class="tab-btn active" id="tabStatus">Status</button>
330
- <button class="tab-btn" id="tabSettings">Settings</button>
331
- </div>
332
- <div class="panel" id="panelStatus">
333
- <div class="status-actions">
334
- <button id="quipsToggleBtn" class="tab-btn" type="button">Reachy Audio: On</button>
335
- </div>
336
- <div class="cards">
337
- <div class="card" id="cardPhoneHome">
338
- <div class="card-header">
339
- <h3>Phone Home</h3>
340
- <span class="status-pill" id="statusPhoneHome">Waiting</span>
341
- </div>
342
- <div class="sub">Place phone in front of Reachy</div>
343
- </div>
344
- <div class="card" id="cardPhoneDetected">
345
- <div class="card-header">
346
- <h3>Phone Detected</h3>
347
- <span class="status-pill" id="statusPhoneDetected">Unknown</span>
348
- </div>
349
- <div class="sub">Phone seen by camera</div>
350
- </div>
351
- <div class="card" id="cardPhoneUse">
352
- <div class="card-header">
353
- <h3>Phone Use</h3>
354
- <span class="status-pill" id="statusPhoneUse">No</span>
355
- </div>
356
- <div class="sub">Phone overlaps person</div>
357
- </div>
358
- <div class="card" id="cardReachyStatus">
359
- <div class="card-header">
360
- <h3>Reachy’s Status</h3>
361
- <span class="status-pill" id="statusReachy">Idle</span>
362
- </div>
363
- <div class="sub" id="subReachy">Waiting for signals</div>
364
- </div>
365
- </div>
366
- <div id="video">
367
- <div class="video-frame">
368
- <div id="videoPlaceholder" class="video-placeholder">Debug video disabled</div>
369
- <img id="videoStream" data-src="/stream.mjpg" alt="Reachy camera stream" />
370
- </div>
371
- <div class="video-controls">
372
- <button id="debugBtn" type="button">Enable debug video</button>
373
- <span class="warning">Debug video may affect performance.</span>
374
- </div>
375
- </div>
376
- </div>
377
- <div class="panel" id="panelSettings" style="display: none;">
378
- <div class="settings-grid">
379
- <div class="settings-card">
380
- <h4>Weights</h4>
381
- <div class="settings-row">
382
- <label for="weightsSelect">Download weights</label>
383
- </div>
384
- <div class="settings-actions">
385
- <select id="weightsSelect">
386
- <option value="yolo26l">yolo26l</option>
387
- <option value="yolo26m">yolo26m</option>
388
- <option value="yolo26s">yolo26s</option>
389
- <option value="yolo26n">yolo26n</option>
390
- </select>
391
- <button id="downloadWeightsBtn" class="tab-btn" type="button">Download</button>
392
- </div>
393
- </div>
394
- <div class="settings-card">
395
- <h4>Detection</h4>
396
- <div class="settings-row">
397
- <label for="confInput">Confidence</label>
398
- <input id="confInput" type="number" min="0.05" max="0.9" step="0.01" value="0.15" />
399
- </div>
400
- <div class="settings-row">
401
- <label for="goodJobSelect">Celebration delay</label>
402
- <select id="goodJobSelect">
403
- <option value="10">10s</option>
404
- <option value="30" selected>30s</option>
405
- <option value="60">1 min</option>
406
- <option value="120">2 min</option>
407
- <option value="300">5 min</option>
408
- <option value="600">10 min</option>
409
- </select>
410
- </div>
411
- </div>
412
- <div class="settings-card">
413
- <h4>Reachy Audio</h4>
414
- <div class="settings-row">
415
- <label for="ambientSelect">Ambient interval</label>
416
- <select id="ambientSelect">
417
- <option value="10">10s</option>
418
- <option value="30" selected>30s</option>
419
- <option value="60">1 min</option>
420
- <option value="600">10 min</option>
421
- </select>
422
- </div>
423
- <div class="settings-row">
424
- <label for="curiousInput">Curious interval (sec)</label>
425
- <input id="curiousInput" type="number" min="1" max="30" step="1" value="5" />
426
- </div>
427
- </div>
428
- </div>
429
- <div class="settings-actions">
430
- <button id="saveSettingsBtn" class="tab-btn" type="button">Save</button>
431
- <span class="muted" id="settingsStatus"></span>
432
- </div>
433
- </div>
434
- </div>
435
- <script>
436
- let lastId = 0;
437
- const statusEl = document.getElementById("status");
438
- const debugBtn = document.getElementById("debugBtn");
439
- const videoStream = document.getElementById("videoStream");
440
- const videoPlaceholder = document.getElementById("videoPlaceholder");
441
- const tabStatus = document.getElementById("tabStatus");
442
- const tabSettings = document.getElementById("tabSettings");
443
- const panelStatus = document.getElementById("panelStatus");
444
- const panelSettings = document.getElementById("panelSettings");
445
- const weightsSelect = document.getElementById("weightsSelect");
446
- const downloadWeightsBtn = document.getElementById("downloadWeightsBtn");
447
- const confInput = document.getElementById("confInput");
448
- const goodJobSelect = document.getElementById("goodJobSelect");
449
- const ambientSelect = document.getElementById("ambientSelect");
450
- const curiousInput = document.getElementById("curiousInput");
451
- const saveSettingsBtn = document.getElementById("saveSettingsBtn");
452
- const settingsStatus = document.getElementById("settingsStatus");
453
- let videoEnabled = false;
454
- let currentState = null;
455
- let phoneDetected = null;
456
- let phoneUse = false;
457
- let phoneHome = false;
458
- const logsEnabled = true;
459
-
460
- const cardPhoneDetected = document.getElementById("cardPhoneDetected");
461
- const cardPhoneUse = document.getElementById("cardPhoneUse");
462
- const cardReachyStatus = document.getElementById("cardReachyStatus");
463
- const cardPhoneHome = document.getElementById("cardPhoneHome");
464
- const statusPhoneDetected = document.getElementById("statusPhoneDetected");
465
- const statusPhoneUse = document.getElementById("statusPhoneUse");
466
- const statusReachy = document.getElementById("statusReachy");
467
- const subReachy = document.getElementById("subReachy");
468
- const statusPhoneHome = document.getElementById("statusPhoneHome");
469
- const quipsToggleBtn = document.getElementById("quipsToggleBtn");
470
- let quipsEnabled = true;
471
- let aspectTimer = null;
472
- let snapshotTimer = null;
473
-
474
- function updateAspectFromStream() {
475
- if (videoStream.naturalWidth && videoStream.naturalHeight) {
476
- document.documentElement.style.setProperty(
477
- "--video-aspect",
478
- `${videoStream.naturalWidth} / ${videoStream.naturalHeight}`
479
- );
480
- }
481
- }
482
-
483
- function updateAspectFromSnapshot() {
484
- const img = new Image();
485
- img.onload = () => {
486
- if (img.naturalWidth && img.naturalHeight) {
487
- document.documentElement.style.setProperty(
488
- "--video-aspect",
489
- `${img.naturalWidth} / ${img.naturalHeight}`
490
- );
491
- }
492
- };
493
- img.src = `/snapshot.jpg?t=${Date.now()}`;
494
- }
495
-
496
- function setVideoEnabled(enabled) {
497
- videoEnabled = enabled;
498
- if (videoEnabled) {
499
- debugBtn.textContent = "Disable debug video";
500
- fetch("/debug?enabled=1")
501
- .then(() => {
502
- const base = videoStream.dataset.src;
503
- videoStream.src = `${base}?t=${Date.now()}`;
504
- videoStream.style.display = "block";
505
- videoPlaceholder.style.display = "none";
506
- updateAspectFromStream();
507
- if (aspectTimer) {
508
- clearInterval(aspectTimer);
509
- }
510
- aspectTimer = setInterval(updateAspectFromStream, 500);
511
- updateAspectFromSnapshot();
512
- if (snapshotTimer) {
513
- clearInterval(snapshotTimer);
514
- }
515
- snapshotTimer = setInterval(updateAspectFromSnapshot, 2000);
516
- })
517
- .catch(() => {});
518
- } else {
519
- videoStream.removeAttribute("src");
520
- videoStream.style.display = "none";
521
- videoPlaceholder.style.display = "flex";
522
- debugBtn.textContent = "Enable debug video";
523
- fetch("/debug?enabled=0").catch(() => {});
524
- if (aspectTimer) {
525
- clearInterval(aspectTimer);
526
- aspectTimer = null;
527
- }
528
- if (snapshotTimer) {
529
- clearInterval(snapshotTimer);
530
- snapshotTimer = null;
531
- }
532
- }
533
- }
534
-
535
- debugBtn.addEventListener("click", () => {
536
- setVideoEnabled(!videoEnabled);
537
- });
538
-
539
- setVideoEnabled(false);
540
-
541
- videoStream.addEventListener("load", updateAspectFromStream);
542
-
543
- function setTab(name) {
544
- if (name === "settings") {
545
- panelStatus.style.display = "none";
546
- panelSettings.style.display = "flex";
547
- tabStatus.classList.remove("active");
548
- tabSettings.classList.add("active");
549
- } else {
550
- panelStatus.style.display = "block";
551
- panelSettings.style.display = "none";
552
- tabStatus.classList.add("active");
553
- tabSettings.classList.remove("active");
554
- }
555
- }
556
-
557
- tabStatus.addEventListener("click", () => setTab("status"));
558
- tabSettings.addEventListener("click", () => setTab("settings"));
559
-
560
- async function loadSettings() {
561
- try {
562
- const res = await fetch("/settings");
563
- const data = await res.json();
564
- if (data.conf !== null && data.conf !== undefined) {
565
- confInput.value = data.conf;
566
- }
567
- if (data.good_job_heartbeats !== null && data.good_job_heartbeats !== undefined) {
568
- const seconds = data.good_job_heartbeats * 10;
569
- goodJobSelect.value = String(seconds);
570
- }
571
- if (data.quips_enabled !== null && data.quips_enabled !== undefined) {
572
- quipsEnabled = Boolean(data.quips_enabled);
573
- quipsToggleBtn.textContent = quipsEnabled ? "Reachy Audio: On" : "Reachy Audio: Off";
574
- }
575
- if (data.ambient_interval_sec !== null && data.ambient_interval_sec !== undefined) {
576
- ambientSelect.value = String(data.ambient_interval_sec);
577
- }
578
- if (data.curious_interval_sec !== null && data.curious_interval_sec !== undefined) {
579
- curiousInput.value = String(data.curious_interval_sec);
580
- }
581
- } catch (err) {
582
- settingsStatus.textContent = "Failed to load settings";
583
- }
584
- }
585
-
586
- saveSettingsBtn.addEventListener("click", async () => {
587
- settingsStatus.textContent = "Saving...";
588
- const confVal = parseFloat(confInput.value);
589
- const seconds = parseInt(goodJobSelect.value, 10);
590
- const beats = Math.max(1, Math.round(seconds / 10));
591
- try {
592
- await fetch("/settings", {
593
- method: "POST",
594
- headers: { "Content-Type": "application/json" },
595
- body: JSON.stringify({
596
- conf: confVal,
597
- good_job_heartbeats: beats,
598
- ambient_interval_sec: parseFloat(ambientSelect.value),
599
- curious_interval_sec: parseFloat(curiousInput.value),
600
- }),
601
- });
602
- settingsStatus.textContent = "Saved";
603
- } catch (err) {
604
- settingsStatus.textContent = "Save failed";
605
- }
606
- });
607
-
608
- quipsToggleBtn.addEventListener("click", async () => {
609
- quipsEnabled = !quipsEnabled;
610
- quipsToggleBtn.textContent = quipsEnabled ? "Reachy Audio: On" : "Reachy Audio: Off";
611
- settingsStatus.textContent = "Saving...";
612
- try {
613
- await fetch("/settings", {
614
- method: "POST",
615
- headers: { "Content-Type": "application/json" },
616
- body: JSON.stringify({ quips_enabled: quipsEnabled }),
617
- });
618
- settingsStatus.textContent = "Saved";
619
- } catch (err) {
620
- settingsStatus.textContent = "Save failed";
621
- }
622
- });
623
-
624
- downloadWeightsBtn.addEventListener("click", async () => {
625
- settingsStatus.textContent = "Downloading...";
626
- try {
627
- await fetch("/download_weights", {
628
- method: "POST",
629
- headers: { "Content-Type": "application/json" },
630
- body: JSON.stringify({ weights: weightsSelect.value }),
631
- });
632
- settingsStatus.textContent = "Download queued";
633
- } catch (err) {
634
- settingsStatus.textContent = "Download failed";
635
- }
636
- });
637
-
638
- function setCard(card, statusEl, active, text, alert = false) {
639
- card.classList.toggle("active", active);
640
- card.classList.toggle("alert", alert);
641
- statusEl.textContent = text;
642
- }
643
-
644
- function renderCards() {
645
- setCard(
646
- cardPhoneDetected,
647
- statusPhoneDetected,
648
- phoneDetected === true,
649
- phoneDetected === null ? "Unknown" : phoneDetected ? "Yes" : "No",
650
- phoneDetected === false
651
- );
652
- setCard(
653
- cardPhoneUse,
654
- statusPhoneUse,
655
- phoneUse,
656
- phoneUse ? "Detected" : "No",
657
- phoneUse
658
- );
659
- let reachyLabel = "Idle";
660
- let reachySub = "Waiting for signals";
661
- let reachyActive = false;
662
- if (currentState === "tracking_phone") {
663
- reachyLabel = "Tracking Phone";
664
- reachySub = "Following the phone";
665
- reachyActive = true;
666
- } else if (currentState === "tracking_person") {
667
- if (phoneDetected === false) {
668
- reachyLabel = "Tracking Person & Searching Phone";
669
- reachySub = "Scanning for you and the phone";
670
- } else {
671
- reachyLabel = "Tracking Person";
672
- reachySub = "Looking for you";
673
- }
674
- reachyActive = true;
675
- } else if (currentState === "searching_phone" || currentState === "looking_down") {
676
- reachyLabel = "Searching Phone";
677
- reachySub = "Scanning for the phone";
678
- reachyActive = true;
679
- } else if (phoneDetected === false) {
680
- reachyLabel = "Searching Phone";
681
- reachySub = "Scanning for the phone";
682
- reachyActive = true;
683
- }
684
- setCard(cardReachyStatus, statusReachy, reachyActive, reachyLabel);
685
- subReachy.textContent = reachySub;
686
- const phoneHomeActive = phoneHome;
687
- setCard(
688
- cardPhoneHome,
689
- statusPhoneHome,
690
- phoneHomeActive,
691
- phoneHomeActive ? "Home" : "Waiting"
692
- );
693
- }
694
-
695
- function handleLine(text) {
696
- if (text.includes("[state]")) {
697
- const state = text.split("[state]")[1].trim();
698
- if (state === "phone use detected") {
699
- phoneUse = true;
700
- } else if (state === "phone use stopped") {
701
- phoneUse = false;
702
- } else if (state === "phone home") {
703
- phoneHome = true;
704
- } else if (state === "phone home cleared") {
705
- phoneHome = false;
706
- } else {
707
- currentState = state;
708
- if (state === "tracking_phone") {
709
- phoneDetected = true;
710
- } else if (
711
- state === "searching_phone" ||
712
- state === "tracking_person" ||
713
- state === "looking_down"
714
- ) {
715
- phoneDetected = false;
716
- }
717
- }
718
- }
719
- if (text.includes("[heartbeat] phone_detected=")) {
720
- phoneDetected = text.includes("phone_detected=yes");
721
- }
722
- renderCards();
723
- }
724
-
725
- async function poll() {
726
- if (!logsEnabled) {
727
- statusEl.textContent = "Logs disabled";
728
- }
729
- try {
730
- const res = await fetch(`/logs?since=${lastId}`);
731
- const data = await res.json();
732
- data.lines.forEach((line) => {
733
- lastId = Math.max(lastId, line.id);
734
- handleLine(line.text);
735
- });
736
- statusEl.textContent = "Live";
737
- } catch (err) {
738
- statusEl.textContent = "Disconnected";
739
- handleLine("[warn] log stream disconnected");
740
- }
741
- setTimeout(poll, 1000);
742
- }
743
-
744
- poll();
745
- loadSettings();
746
- </script>
747
- </body>
748
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web_log_ui.py DELETED
@@ -1,223 +0,0 @@
1
- import argparse
2
- import json
3
- import logging
4
- import threading
5
- import time
6
- from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
7
- from pathlib import Path
8
- from typing import List, Tuple
9
-
10
- import cv2
11
- import numpy as np
12
-
13
- from reachy_mini import ReachyMini
14
-
15
- from reachy_phone_home.main import TrackerConfig, run_tracker
16
-
17
-
18
- LOG_LOCK = threading.Lock()
19
- FRAME_LOCK = threading.Lock()
20
- VIDEO_LOCK = threading.Lock()
21
- LOG_LINES: List[Tuple[int, str]] = []
22
- LOG_SEQ = 0
23
- MAX_LINES = 1000
24
- LATEST_FRAME = None
25
- VIDEO_ENABLED = False
26
-
27
-
28
- def _append_log(line: str) -> None:
29
- global LOG_SEQ
30
- with LOG_LOCK:
31
- LOG_SEQ += 1
32
- LOG_LINES.append((LOG_SEQ, line))
33
- if len(LOG_LINES) > MAX_LINES:
34
- LOG_LINES.pop(0)
35
-
36
-
37
- class _LogWriter:
38
- def write(self, data: str) -> int:
39
- text = data.strip()
40
- if text:
41
- for line in text.splitlines():
42
- _append_log(line)
43
- return len(data)
44
-
45
- def flush(self) -> None:
46
- return None
47
-
48
-
49
- class _QueueHandler(logging.Handler):
50
- def emit(self, record: logging.LogRecord) -> None:
51
- try:
52
- msg = self.format(record)
53
- _append_log(msg)
54
- except Exception:
55
- pass
56
-
57
-
58
- class LogHandler(BaseHTTPRequestHandler):
59
- def log_message(self, format: str, *args) -> None:
60
- return None
61
-
62
- def _send_json(self, payload: dict) -> None:
63
- body = json.dumps(payload).encode("utf-8")
64
- self.send_response(200)
65
- self.send_header("Content-Type", "application/json")
66
- self.send_header("Content-Length", str(len(body)))
67
- self.end_headers()
68
- self.wfile.write(body)
69
-
70
- def do_GET(self) -> None:
71
- if self.path.startswith("/debug"):
72
- enabled = False
73
- if "?" in self.path:
74
- query = self.path.split("?", 1)[1]
75
- for part in query.split("&"):
76
- if part.startswith("enabled="):
77
- enabled = part.split("=", 1)[1] in ("1", "true", "yes", "on")
78
- break
79
- global VIDEO_ENABLED
80
- with VIDEO_LOCK:
81
- VIDEO_ENABLED = enabled
82
- current = VIDEO_ENABLED
83
- self._send_json({"enabled": current})
84
- return
85
- if self.path.startswith("/stream.mjpg"):
86
- self.send_response(200)
87
- self.send_header("Content-Type", "multipart/x-mixed-replace; boundary=frame")
88
- self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
89
- self.end_headers()
90
- try:
91
- while True:
92
- with VIDEO_LOCK:
93
- enabled = VIDEO_ENABLED
94
- if not enabled:
95
- placeholder = _placeholder_frame("Debug video disabled")
96
- ok, jpg = cv2.imencode(".jpg", placeholder)
97
- if ok:
98
- self.wfile.write(b"--frame\r\n")
99
- self.wfile.write(b"Content-Type: image/jpeg\r\n\r\n")
100
- self.wfile.write(jpg.tobytes())
101
- self.wfile.write(b"\r\n")
102
- time.sleep(0.25)
103
- continue
104
- with FRAME_LOCK:
105
- frame = LATEST_FRAME.copy() if LATEST_FRAME is not None else None
106
- if frame is not None:
107
- ok, jpg = cv2.imencode(".jpg", frame)
108
- if ok:
109
- self.wfile.write(b"--frame\r\n")
110
- self.wfile.write(b"Content-Type: image/jpeg\r\n\r\n")
111
- self.wfile.write(jpg.tobytes())
112
- self.wfile.write(b"\r\n")
113
- else:
114
- placeholder = _placeholder_frame("Waiting for camera frames...")
115
- ok, jpg = cv2.imencode(".jpg", placeholder)
116
- if ok:
117
- self.wfile.write(b"--frame\r\n")
118
- self.wfile.write(b"Content-Type: image/jpeg\r\n\r\n")
119
- self.wfile.write(jpg.tobytes())
120
- self.wfile.write(b"\r\n")
121
- time.sleep(0.1)
122
- except Exception:
123
- pass
124
- return
125
- if self.path.startswith("/logs"):
126
- since = 0
127
- if "?" in self.path:
128
- query = self.path.split("?", 1)[1]
129
- for part in query.split("&"):
130
- if part.startswith("since="):
131
- try:
132
- since = int(part.split("=", 1)[1])
133
- except ValueError:
134
- since = 0
135
- with LOG_LOCK:
136
- lines = [item for item in LOG_LINES if item[0] > since]
137
- last_id = LOG_LINES[-1][0] if LOG_LINES else since
138
- self._send_json(
139
- {
140
- "lines": [{"id": i, "text": t} for i, t in lines],
141
- "last_id": last_id,
142
- }
143
- )
144
- return
145
-
146
- if self.path == "/" or self.path.startswith("/index"):
147
- html = Path(__file__).with_name("web_log_ui.html").read_bytes()
148
- self.send_response(200)
149
- self.send_header("Content-Type", "text/html; charset=utf-8")
150
- self.send_header("Content-Length", str(len(html)))
151
- self.end_headers()
152
- self.wfile.write(html)
153
- return
154
-
155
- self.send_response(404)
156
- self.end_headers()
157
-
158
-
159
- def _start_tracker() -> None:
160
- def _frame_hook(frame) -> None:
161
- with VIDEO_LOCK:
162
- if not VIDEO_ENABLED:
163
- return
164
- global LATEST_FRAME
165
- with FRAME_LOCK:
166
- LATEST_FRAME = frame.copy()
167
-
168
- config = TrackerConfig(no_display=True)
169
- try:
170
- with ReachyMini() as reachy:
171
- run_tracker(
172
- reachy,
173
- config,
174
- frame_hook=_frame_hook,
175
- frame_hook_enabled=lambda: VIDEO_ENABLED,
176
- )
177
- except Exception:
178
- logging.exception("Tracker crashed")
179
-
180
- def _placeholder_frame(text: str):
181
- canvas = np.full((360, 640, 3), 40, dtype=np.uint8)
182
- cv2.putText(
183
- canvas,
184
- text,
185
- (20, 190),
186
- cv2.FONT_HERSHEY_SIMPLEX,
187
- 0.8,
188
- (200, 200, 200),
189
- 2,
190
- cv2.LINE_AA,
191
- )
192
- return canvas
193
-
194
-
195
- def main() -> None:
196
- parser = argparse.ArgumentParser(description="Local web UI for Reachy Phone Home logs")
197
- parser.add_argument("--host", type=str, default="127.0.0.1")
198
- parser.add_argument("--port", type=int, default=8088)
199
- # No display in the web UI; video is streamed via MJPEG.
200
- args = parser.parse_args()
201
-
202
- logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
203
- root_logger = logging.getLogger()
204
- root_logger.addHandler(_QueueHandler())
205
-
206
- import sys
207
-
208
- sys.stdout = _LogWriter()
209
- sys.stderr = _LogWriter()
210
-
211
- server = ThreadingHTTPServer((args.host, args.port), LogHandler)
212
- print(f"[phone_home] Web UI running at http://{args.host}:{args.port}")
213
-
214
- tracker_thread = threading.Thread(target=_start_tracker, daemon=True)
215
- tracker_thread.start()
216
- try:
217
- server.serve_forever()
218
- except KeyboardInterrupt:
219
- pass
220
-
221
-
222
- if __name__ == "__main__":
223
- main()