Skip to content

Commit 4e41574

Browse files
...
1 parent 8b36971 commit 4e41574

3 files changed

Lines changed: 160 additions & 11 deletions

File tree

scripts/flask_server.py

Lines changed: 155 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
"""
2-
flask_server.py — Flask app, all routes, and the thread runner.
2+
flask_server.py — Flask app + Zeroconf mesh discovery.
3+
KIDA registers itself on the local network and discovers other robots/nodes
4+
automatically. The dashboard at / shows live status of all found peers.
35
"""
46

57
import logging
6-
from flask import Flask, jsonify, request, render_template
8+
import socket
9+
import threading
10+
import requests
11+
from flask import Flask, jsonify, request, render_template, render_template_string
12+
from zeroconf import ServiceInfo, Zeroconf, ServiceBrowser
713

814
from shared_state import (
915
command_queue,
@@ -12,14 +18,146 @@
1218
_face_results, _face_lock,
1319
)
1420

15-
app = Flask(__name__, static_folder="static", template_folder="templates")
21+
# ── Config ─────────────────────────────────────────────────────────────────────
22+
THIS_NAME = "KIDA00"
23+
THIS_PORT = 5003
24+
TYPE = "_flask-link._tcp.local."
1625

1726
logger = logging.getLogger("kida.flask")
27+
app = Flask(__name__, static_folder="static", template_folder="templates")
1828

29+
# ── Network discovery ──────────────────────────────────────────────────────────
30+
found_servers: dict = {}
31+
_found_lock = threading.Lock()
1932

33+
34+
def get_ip() -> str:
35+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
36+
try:
37+
s.connect(("10.255.255.255", 1))
38+
return s.getsockname()[0]
39+
except Exception:
40+
return "127.0.0.1"
41+
finally:
42+
s.close()
43+
44+
45+
my_ip = get_ip()
46+
47+
48+
class _Listener:
49+
def remove_service(self, zc, type_, name):
50+
short = name.split(".")[0]
51+
with _found_lock:
52+
found_servers.pop(short, None)
53+
logger.info("Peer left: %s", short)
54+
55+
def add_service(self, zc, type_, name):
56+
self.update_service(zc, type_, name)
57+
58+
def update_service(self, zc, type_, name):
59+
info = zc.get_service_info(type_, name)
60+
if info:
61+
addresses = [socket.inet_ntoa(a) for a in info.addresses]
62+
if addresses:
63+
short = name.split(".")[0]
64+
if short != THIS_NAME:
65+
url = f"http://{addresses[0]}:{info.port}"
66+
with _found_lock:
67+
found_servers[short] = url
68+
logger.info("Peer found: %s @ %s", short, url)
69+
70+
71+
# Zeroconf — started once at import time
72+
_zeroconf = Zeroconf()
73+
_zc_info = ServiceInfo(
74+
TYPE,
75+
f"{THIS_NAME}.{TYPE}",
76+
addresses=[socket.inet_aton(my_ip)],
77+
port=THIS_PORT,
78+
properties={"version": "1.0"},
79+
)
80+
_zeroconf.register_service(_zc_info)
81+
ServiceBrowser(_zeroconf, TYPE, _Listener())
82+
logger.info("Zeroconf registered: %s on %s:%d", THIS_NAME, my_ip, THIS_PORT)
83+
84+
85+
def shutdown_zeroconf() -> None:
86+
"""Call this during app shutdown to cleanly deregister from the network."""
87+
logger.info("Unregistering Zeroconf service...")
88+
_zeroconf.unregister_service(_zc_info)
89+
_zeroconf.close()
90+
91+
92+
# ── Routes ─────────────────────────────────────────────────────────────────────
2093
@app.route("/")
21-
def home():
22-
return render_template("index.html")
94+
def dashboard():
95+
# Serve index.html from templates first; fall back to live network page
96+
try:
97+
return render_template("index.html")
98+
except Exception:
99+
pass
100+
101+
# Live network dashboard (fallback if no index.html)
102+
status_html = (
103+
f'<div class="peer self">'
104+
f'<span class="dot green">●</span>'
105+
f'<b>{THIS_NAME}</b> (this robot — {my_ip}:{THIS_PORT})</div>'
106+
)
107+
with _found_lock:
108+
peers = dict(found_servers)
109+
110+
for name, url in peers.items():
111+
try:
112+
r = requests.get(f"{url}/ping", timeout=0.5)
113+
colour = "green" if r.status_code == 200 else "orange"
114+
label = "Online" if r.status_code == 200 else f"HTTP {r.status_code}"
115+
except Exception:
116+
colour, label = "red", "Unreachable"
117+
status_html += (
118+
f'<div class="peer">'
119+
f'<span class="dot {colour}">●</span>'
120+
f'<b>{name}</b> {label} — '
121+
f'<a href="{url}">{url}</a></div>'
122+
)
123+
124+
return render_template_string("""
125+
<!DOCTYPE html>
126+
<html>
127+
<head>
128+
<title>{{ name }} — Network</title>
129+
<meta charset="utf-8">
130+
<script>setTimeout(()=>location.reload(),3000);</script>
131+
<style>
132+
body{font-family:sans-serif;background:#0d0e14;color:#e6e6e1;
133+
display:flex;justify-content:center;padding-top:60px;margin:0}
134+
.card{background:#0e0f16;border:1px solid #20233a;border-radius:14px;
135+
padding:32px 40px;min-width:340px;box-shadow:0 6px 24px #0007}
136+
h1{margin:0 0 4px;color:#ff1e64;letter-spacing:2px}
137+
p.sub{color:#3c3e4b;font-size:.75em;margin:0 0 20px}
138+
hr{border-color:#20233a;margin:16px 0}
139+
.peer{padding:10px 0;border-bottom:1px solid #1a1c28;font-size:.95em}
140+
.peer:last-child{border-bottom:none}
141+
.dot{font-size:1.1em;margin-right:8px}
142+
.green{color:#1dc878}.orange{color:#ffa028}.red{color:#e24b4a}
143+
a{color:#378add;text-decoration:none}
144+
</style>
145+
</head>
146+
<body>
147+
<div class="card">
148+
<h1>KIDA NETWORK</h1>
149+
<p class="sub">Zeroconf · {{ type }} · refreshes every 3 s</p>
150+
<hr>
151+
{{ status|safe }}
152+
</div>
153+
</body>
154+
</html>
155+
""", name=THIS_NAME, type=TYPE, status=status_html)
156+
157+
158+
@app.route("/ping")
159+
def ping():
160+
return f"{THIS_NAME} alive", 200
23161

24162

25163
@app.route("/status")
@@ -79,7 +217,17 @@ def face_results():
79217
return jsonify({"results": _face_results.copy()})
80218

81219

220+
@app.route("/peers")
221+
def peers_route():
222+
"""Returns all currently discovered peers as JSON."""
223+
with _found_lock:
224+
return jsonify({"self": THIS_NAME, "peers": dict(found_servers)})
225+
226+
227+
# ── Runner ─────────────────────────────────────────────────────────────────────
82228
def run_flask() -> None:
83-
"""Call this in a daemon thread."""
229+
"""Call this in a daemon thread from main.py."""
84230
logging.getLogger("werkzeug").setLevel(logging.ERROR)
85-
app.run(host="0.0.0.0", port=5000, debug=False, use_reloader=False)
231+
logger.info("Flask starting on %s:%d", my_ip, THIS_PORT)
232+
app.run(host="0.0.0.0", port=THIS_PORT, debug=False,
233+
use_reloader=False, threaded=True)

scripts/render_helpers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ def render_left_panel(screen, qr_surf, local_ip, st,
245245
screen.blit(qr_surf, (qr_x, lp_y))
246246
url_y = lp_y + qr_w + 10
247247
txt(screen, fmono_md, str(local_ip), (lp_x + lp_w // 2, url_y), AMBER, anchor="midtop")
248-
txt(screen, fmono_sm, "port 5000",
248+
txt(screen, fmono_sm, "port 5003",
249249
(lp_x + lp_w // 2, url_y + fmono_md.get_height() + 2), SEC, anchor="midtop")
250250
lp_y = url_y + fmono_md.get_height() + fmono_sm.get_height() + 14
251251
txt(screen, fmono_xs, "SCAN TO OPEN DASHBOARD",
@@ -329,7 +329,7 @@ def render_bottom_bar(screen, mode, ctrl_scheme, speed, face_count, frame,
329329
pygame.draw.circle(screen, GREEN, (14, sb_y + BOT_H // 2), 5)
330330
sx = 28
331331
for lbl, val in [
332-
("FLASK", ":5000"),
332+
("FLASK", ":5003"),
333333
("MODE", mode.name),
334334
("SCHEME", "WASD" if ctrl_scheme == 1 else "QA/WS"),
335335
("SPEED", f"{speed:.1f}"),

scripts/ui.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
_face_results, _face_lock,
4141
_face_frame_q, _face_enabled, _deepface_ok,
4242
)
43-
from flask_server import run_flask
43+
from flask_server import run_flask, shutdown_zeroconf
4444
from system_monitor import start_stats_thread, get_local_ip
4545
from face_detector import start_face_thread
4646
from camera_utils import cam_to_surface, make_qr
@@ -201,7 +201,7 @@ def main() -> None:
201201
pygame.quit(); return
202202

203203
local_ip = get_local_ip()
204-
qr_surf = make_qr(f"http://{local_ip}:5000", size=130)
204+
qr_surf = make_qr(f"http://{local_ip}:5003", size=130)
205205
photos_dir = "../kida/photos"
206206
videos_dir = "../kida/videos"
207207
face_dir = "../kida/faces"
@@ -541,6 +541,7 @@ def music_stop(): ds["_music_stop"]()
541541

542542
# ── Cleanup ────────────────────────────────────────────────────────────────
543543
logger.info("Shutting down…")
544+
shutdown_zeroconf()
544545
switch_mode(ds["mode"], Mode.USER, ctx)
545546
_face_enabled.clear()
546547
if ds["video_rec"]:

0 commit comments

Comments
 (0)