Skip to content

Commit d59f34b

Browse files
authored
Merge pull request #9 from blankey1337/feature/simulated-perception
Feature/simulated perception
2 parents d458dda + c34a496 commit d59f34b

6 files changed

Lines changed: 580 additions & 22 deletions

File tree

software/dashboard/app.py

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,20 +39,45 @@ def zmq_worker(ip='127.0.0.1', port=5556):
3939

4040
def generate_frames(camera_name):
4141
while True:
42-
frame_data = None
42+
frame_bytes = None
43+
detections = []
44+
4345
with lock:
4446
if camera_name in latest_observation:
4547
b64_str = latest_observation[camera_name]
4648
if b64_str:
4749
try:
48-
# It is already a base64 encoded JPG from the host
49-
frame_data = base64.b64decode(b64_str)
50+
frame_bytes = base64.b64decode(b64_str)
5051
except Exception:
5152
pass
53+
54+
# Get detections
55+
raw_dets = latest_observation.get("detections", {})
56+
if isinstance(raw_dets, dict):
57+
detections = raw_dets.get(camera_name, [])
5258

53-
if frame_data:
59+
if frame_bytes:
60+
# Decode to image to draw on it
61+
nparr = np.frombuffer(frame_bytes, np.uint8)
62+
img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
63+
64+
if img is not None:
65+
# Draw detections
66+
for det in detections:
67+
box = det.get("box", [])
68+
label = det.get("label", "obj")
69+
if len(box) == 4:
70+
x1, y1, x2, y2 = map(int, box)
71+
cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2)
72+
cv2.putText(img, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 255, 0), 2)
73+
74+
# Re-encode
75+
ret, buffer = cv2.imencode('.jpg', img)
76+
if ret:
77+
frame_bytes = buffer.tobytes()
78+
5479
yield (b'--frame\r\n'
55-
b'Content-Type: image/jpeg\r\n\r\n' + frame_data + b'\r\n')
80+
b'Content-Type: image/jpeg\r\n\r\n' + frame_bytes + b'\r\n')
5681
else:
5782
# Return a blank or placeholder image if no data
5883
pass
@@ -71,8 +96,13 @@ def video_feed(camera_name):
7196
@app.route('/api/status')
7297
def get_status():
7398
with lock:
74-
# Filter out large image data for status endpoint
75-
status = {k: v for k, v in latest_observation.items() if not (isinstance(v, str) and len(v) > 1000)}
99+
# Filter out large image data for status endpoint, but keep the key
100+
status = {}
101+
for k, v in latest_observation.items():
102+
if isinstance(v, str) and len(v) > 1000:
103+
status[k] = "__IMAGE_DATA__"
104+
else:
105+
status[k] = v
76106
status['connected'] = connected
77107
return jsonify(status)
78108

software/dashboard/templates/index.html

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,23 +46,32 @@ <h3>Robot State</h3>
4646

4747
// Update Status Table
4848
for (const [key, value] of Object.entries(data)) {
49-
// Skip camera data (if any leaks into status) and long strings
50-
if (typeof value === 'string' && value.length > 100) continue;
5149

52-
// Check if this key might be a camera
53-
// If it's a camera key and not in activeCameras, add it
54-
if (knownCameras.includes(key) || key.includes('cam') || key.includes('wrist')) {
50+
// Check if this key is a camera placeholder
51+
if (value === "__IMAGE_DATA__") {
5552
if (!activeCameras.has(key) && !document.getElementById('cam-' + key)) {
5653
addCamera(key);
5754
activeCameras.add(key);
5855
}
5956
continue; // Don't show camera keys in text table
6057
}
58+
59+
// Skip if it looks like a camera key but we didn't get image data marker
60+
// (This avoids treating wrist_roll.pos as a camera)
61+
if (knownCameras.includes(key)) {
62+
// It's a known camera but value isn't image data?
63+
// Maybe it's empty string or something.
64+
// Let's just continue to show it as text if it's not __IMAGE_DATA__
65+
}
6166

6267
let displayValue = value;
63-
if (typeof value === 'number') displayValue = value.toFixed(2);
68+
if (typeof value === 'number') {
69+
displayValue = value.toFixed(2);
70+
} else if (typeof value === 'object') {
71+
displayValue = JSON.stringify(value, null, 2);
72+
}
6473

65-
rows += `<tr><th>${key}</th><td class="value">${displayValue}</td></tr>`;
74+
rows += `<tr><th>${key}</th><td class="value" style="white-space: pre;">${displayValue}</td></tr>`;
6675
}
6776
table.innerHTML = rows;
6877
})
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import time
2+
import json
3+
import threading
4+
import zmq
5+
from nav_service import NavigationService
6+
7+
class ChoreExecutor:
8+
def __init__(self):
9+
self.nav = NavigationService()
10+
self.nav.start()
11+
12+
self.running = False
13+
self.current_chore = None
14+
self.chore_thread = None
15+
16+
# Robot State
17+
self.detections = {}
18+
19+
# Subscribe to obs for detections
20+
self.nav.sub_socket.setsockopt(zmq.SUBSCRIBE, b"")
21+
# We need a way to read detections. NavigationService already reads obs.
22+
# Let's piggyback or just read from nav service state?
23+
# Nav service only stores pose and rooms.
24+
# Let's subclass or monkeypatch NavService to expose detections,
25+
# OR just read from the socket in NavService and store it.
26+
# Actually, let's just modify NavService to store full obs or detections.
27+
28+
def start_chore(self, chore_name):
29+
if self.running:
30+
print("[CHORE] Already running a chore.")
31+
return
32+
33+
self.running = True
34+
self.current_chore = chore_name
35+
self.chore_thread = threading.Thread(target=self._run_chore, args=(chore_name,))
36+
self.chore_thread.start()
37+
38+
def stop_chore(self):
39+
self.running = False
40+
if self.chore_thread:
41+
self.chore_thread.join()
42+
self.nav.stop()
43+
44+
def _run_chore(self, chore_name):
45+
print(f"[CHORE] Starting: {chore_name}")
46+
47+
if chore_name == "clean_up":
48+
self._do_clean_up()
49+
else:
50+
print(f"[CHORE] Unknown chore: {chore_name}")
51+
52+
self.running = False
53+
print(f"[CHORE] Finished: {chore_name}")
54+
55+
def _do_clean_up(self):
56+
# 1. Go to Kitchen
57+
print("[CHORE] Step 1: Go to Kitchen")
58+
self.nav.go_to_room("Kitchen")
59+
self._wait_until_idle()
60+
61+
# 2. Look for Trash
62+
print("[CHORE] Step 2: Scan for Trash")
63+
trash = self._wait_for_detection("trash")
64+
65+
if trash:
66+
print(f"[CHORE] Found trash at {trash.get('box')}!")
67+
# 3. Pick it up (Mock action)
68+
print("[CHORE] Step 3: Picking up trash...")
69+
time.sleep(2)
70+
71+
# 4. Go to Bin (Let's say Bin is in Hallway)
72+
print("[CHORE] Step 4: Go to Hallway (Trash Bin)")
73+
self.nav.go_to_room("Hallway")
74+
self._wait_until_idle()
75+
76+
# 5. Drop it
77+
print("[CHORE] Step 5: Dropping trash")
78+
time.sleep(1)
79+
else:
80+
print("[CHORE] No trash found.")
81+
82+
def _wait_for_detection(self, label, timeout=5.0):
83+
# Poll self.nav.detections
84+
start = time.time()
85+
while time.time() - start < timeout:
86+
dets = self.nav.detections
87+
for cam, items in dets.items():
88+
for item in items:
89+
if item.get("label") == label:
90+
return item
91+
time.sleep(0.1)
92+
return None
93+
94+
def _wait_until_idle(self):
95+
# Wait for nav service to report idle
96+
# (It might briefly be idle before starting move, so wait a tiny bit first if needed)
97+
time.sleep(0.5)
98+
while not self.nav.is_idle():
99+
time.sleep(0.1)
100+
101+
if __name__ == "__main__":
102+
executor = ChoreExecutor()
103+
104+
# Wait for connection
105+
time.sleep(2)
106+
107+
print("Available Rooms:", executor.nav.rooms.keys())
108+
109+
executor.start_chore("clean_up")
110+
111+
# Keep main thread alive
112+
while executor.running:
113+
time.sleep(1)
114+
115+
executor.stop_chore()
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import time
2+
import zmq
3+
import json
4+
import threading
5+
from navigation import NavigationController
6+
7+
class NavigationService:
8+
def __init__(self, zmq_obs_ip="127.0.0.1", zmq_obs_port=5556, zmq_cmd_port=5555):
9+
self.controller = NavigationController()
10+
11+
self.context = zmq.Context()
12+
self.sub_socket = self.context.socket(zmq.SUB)
13+
self.sub_socket.setsockopt(zmq.SUBSCRIBE, b"")
14+
self.sub_socket.connect(f"tcp://{zmq_obs_ip}:{zmq_obs_port}")
15+
self.sub_socket.setsockopt(zmq.CONFLATE, 1)
16+
17+
self.pub_socket = self.context.socket(zmq.PUSH)
18+
self.pub_socket.connect(f"tcp://{zmq_obs_ip}:{zmq_cmd_port}")
19+
20+
self.running = False
21+
self.current_pose = None
22+
self.rooms = {}
23+
self.detections = {}
24+
self.nav_status = "idle" # idle, moving
25+
26+
def is_idle(self):
27+
return self.nav_status == "idle"
28+
29+
def start(self):
30+
self.running = True
31+
self.thread = threading.Thread(target=self._loop, daemon=True)
32+
self.thread.start()
33+
print("NavigationService started.")
34+
35+
def stop(self):
36+
self.running = False
37+
if hasattr(self, 'thread'):
38+
self.thread.join()
39+
40+
def go_to_room(self, room_name):
41+
if room_name not in self.rooms:
42+
print(f"[NAV] Unknown room: {room_name}")
43+
return False
44+
45+
target = self.rooms[room_name]
46+
self.controller.set_target(target["x"], target["y"])
47+
self.nav_status = "moving"
48+
return True
49+
50+
def _loop(self):
51+
while self.running:
52+
try:
53+
# 1. Get Observation
54+
msg = self.sub_socket.recv_string()
55+
obs = json.loads(msg)
56+
57+
# Update World Knowledge
58+
if "rooms" in obs:
59+
self.rooms = obs["rooms"]
60+
61+
if "detections" in obs:
62+
self.detections = obs["detections"]
63+
64+
self.current_pose = {
65+
"x": obs.get("x_pos", 0.0),
66+
"y": obs.get("y_pos", 0.0),
67+
"theta": obs.get("theta_pos", 0.0)
68+
}
69+
70+
# 2. Compute Control
71+
action = self.controller.get_action(self.current_pose)
72+
73+
if action:
74+
# Check if action is zero (reached)
75+
if action["x.vel"] == 0 and action["theta.vel"] == 0:
76+
self.nav_status = "idle"
77+
else:
78+
self.nav_status = "moving"
79+
80+
self.pub_socket.send_string(json.dumps(action))
81+
else:
82+
self.nav_status = "idle"
83+
84+
85+
except Exception as e:
86+
print(f"[NAV] Error: {e}")
87+
time.sleep(0.1)
88+
89+
if __name__ == "__main__":
90+
# Test Script
91+
nav = NavigationService()
92+
nav.start()
93+
94+
print("Waiting for connection...")
95+
time.sleep(2)
96+
97+
rooms = ["Kitchen", "Bedroom", "Living Room"]
98+
99+
for room in rooms:
100+
print(f"Going to {room}...")
101+
nav.go_to_room(room)
102+
103+
# Wait for arrival (mock)
104+
time.sleep(5)
105+
106+
nav.stop()

0 commit comments

Comments
 (0)