Update 2026-04-21 16:27:07
This commit is contained in:
parent
e0f6acd5c7
commit
af1d0c1b8a
@ -28,22 +28,27 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import wave
|
import wave
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
# ─── PATH CONFIG ─────────────────────────────────────────
|
# ─── PATH + CONFIG ───────────────────────────────────────
|
||||||
from dotenv import load_dotenv
|
# Use the canonical loaders from Core/ so path + config logic lives in one place.
|
||||||
load_dotenv()
|
_PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
if _PROJECT_DIR not in sys.path:
|
||||||
BASE_PATH = os.environ.get("PROJECT_BASE", "/home/unitree")
|
sys.path.insert(0, _PROJECT_DIR)
|
||||||
PROJECT_NAME = "Marcus"
|
from Core.env_loader import PROJECT_ROOT
|
||||||
PROJECT_ROOT = os.path.join(BASE_PATH, PROJECT_NAME)
|
from Core.config_loader import load_config
|
||||||
|
|
||||||
LOG_DIR = os.path.join(PROJECT_ROOT, "logs")
|
LOG_DIR = os.path.join(PROJECT_ROOT, "logs")
|
||||||
os.makedirs(LOG_DIR, exist_ok=True)
|
os.makedirs(LOG_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
# Note: logging.basicConfig() only takes effect on the first call per process.
|
||||||
|
# If the voice module already configured logging (common path via run_marcus.py),
|
||||||
|
# this call is a no-op. When audio_api is used standalone, it wires logs to
|
||||||
|
# logs/voice.log + stderr.
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
|
format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
|
||||||
@ -55,12 +60,6 @@ logging.basicConfig(
|
|||||||
log = logging.getLogger("audio_api")
|
log = logging.getLogger("audio_api")
|
||||||
|
|
||||||
|
|
||||||
def load_config(name: str) -> dict:
|
|
||||||
path = os.path.join(PROJECT_ROOT, "Config", f"config_{name}.json")
|
|
||||||
with open(path, "r") as f:
|
|
||||||
return json.load(f)
|
|
||||||
|
|
||||||
|
|
||||||
# ─── AUDIO API CLASS ─────────────────────────────────────
|
# ─── AUDIO API CLASS ─────────────────────────────────────
|
||||||
|
|
||||||
class AudioAPI:
|
class AudioAPI:
|
||||||
@ -135,11 +134,12 @@ class AudioAPI:
|
|||||||
Speak `text` in English through the G1 built-in TTS (TtsMaker).
|
Speak `text` in English through the G1 built-in TTS (TtsMaker).
|
||||||
|
|
||||||
Mutes (flushes) the mic during playback so the voice loop doesn't
|
Mutes (flushes) the mic during playback so the voice loop doesn't
|
||||||
hear the robot's own voice and transcribe itself. The `lang`
|
hear the robot's own voice and transcribe itself. `lang` is kept
|
||||||
argument is accepted for API compatibility but only "en" plays —
|
in the signature for API compatibility but only `"en"` is accepted
|
||||||
non-ASCII text (Arabic) is rejected by BuiltinTTS.
|
— non-ASCII text (Arabic) is rejected by BuiltinTTS because the
|
||||||
|
G1 firmware silently maps it to Chinese, which nobody wants.
|
||||||
"""
|
"""
|
||||||
if lang and lang not in ("en", "auto"):
|
if lang and lang != "en":
|
||||||
log.warning("builtin_tts only supports English; got lang=%r — skipping", lang)
|
log.warning("builtin_tts only supports English; got lang=%r — skipping", lang)
|
||||||
return
|
return
|
||||||
if self._tts_engine is None:
|
if self._tts_engine is None:
|
||||||
|
|||||||
@ -31,7 +31,7 @@ Files saved
|
|||||||
summary.txt — auto-generated LLaVA summary
|
summary.txt — auto-generated LLaVA summary
|
||||||
frames/ — camera captures at interesting points
|
frames/ — camera captures at interesting points
|
||||||
|
|
||||||
Import in marcus_llava.py
|
Import in marcus_brain.py
|
||||||
-------------------------
|
-------------------------
|
||||||
from marcus_autonomous import AutonomousMode
|
from marcus_autonomous import AutonomousMode
|
||||||
auto = AutonomousMode(
|
auto = AutonomousMode(
|
||||||
@ -303,17 +303,20 @@ class AutonomousMode:
|
|||||||
def _move_forward(self) -> bool:
|
def _move_forward(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Move forward for FORWARD_DURATION seconds.
|
Move forward for FORWARD_DURATION seconds.
|
||||||
Returns True if blocked (no actual movement detected).
|
|
||||||
|
Returns True if blocked. Obstacle detection is not wired in here —
|
||||||
|
LiDAR runs at the brain layer (API.lidar_api.obstacle_ahead) and
|
||||||
|
isn't passed into AutonomousMode today. Extend __init__ with an
|
||||||
|
optional `obstacle_fn=` hook if you want this loop to stop on
|
||||||
|
obstacles detected outside YOLO's view.
|
||||||
"""
|
"""
|
||||||
# TODO: integrate LiDAR when available for true obstacle detection
|
|
||||||
# For now: move and assume not blocked
|
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
while time.time() - t0 < FORWARD_DURATION and self._enabled:
|
while time.time() - t0 < FORWARD_DURATION and self._enabled:
|
||||||
self._send_vel(vx=0.25)
|
self._send_vel(vx=0.25)
|
||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
self._send_vel(0, 0, 0)
|
self._send_vel(0, 0, 0)
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
return False # Not blocked — no LiDAR yet
|
return False
|
||||||
|
|
||||||
def _turn(self, direction: str, duration: float):
|
def _turn(self, direction: str, duration: float):
|
||||||
"""Turn left or right for given duration."""
|
"""Turn left or right for given duration."""
|
||||||
|
|||||||
@ -501,14 +501,18 @@ def run_terminal():
|
|||||||
continue
|
continue
|
||||||
if cmd.lower() in ("q", "quit", "exit"):
|
if cmd.lower() in ("q", "quit", "exit"):
|
||||||
break
|
break
|
||||||
if cmd.lower() == "mute/":
|
if cmd.lower() in ("mute/", "unmute/"):
|
||||||
import subprocess
|
# Route through the audio API so the action respects whichever
|
||||||
subprocess.run(["pactl", "set-source-mute", "0", "1"], capture_output=True)
|
# mic backend is active (BuiltinMic flushes the UDP buffer;
|
||||||
print(" Mic muted")
|
# the legacy pactl path mutes PulseAudio source 3).
|
||||||
|
if _audio_api is None:
|
||||||
|
print(" Voice is not initialized")
|
||||||
continue
|
continue
|
||||||
if cmd.lower() == "unmute/":
|
if cmd.lower() == "mute/":
|
||||||
import subprocess
|
_audio_api._mute_mic()
|
||||||
subprocess.run(["pactl", "set-source-mute", "0", "0"], capture_output=True)
|
print(" Mic muted")
|
||||||
|
else:
|
||||||
|
_audio_api._unmute_mic()
|
||||||
print(" Mic unmuted")
|
print(" Mic unmuted")
|
||||||
continue
|
continue
|
||||||
result = process_command(cmd)
|
result = process_command(cmd)
|
||||||
|
|||||||
@ -19,7 +19,7 @@ Folder structure
|
|||||||
alerts.json ← [{time, type, detail}]
|
alerts.json ← [{time, type, detail}]
|
||||||
summary.txt ← auto-generated session summary
|
summary.txt ← auto-generated session summary
|
||||||
|
|
||||||
Import in marcus_llava.py
|
Import in marcus_brain.py
|
||||||
-------------------------
|
-------------------------
|
||||||
from marcus_memory import Memory
|
from marcus_memory import Memory
|
||||||
mem = Memory() ← call once at startup
|
mem = Memory() ← call once at startup
|
||||||
|
|||||||
@ -14,6 +14,18 @@ import io
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import websockets
|
import websockets
|
||||||
|
|
||||||
|
# Load network defaults from Config/config_Network.json. Falls back to sane
|
||||||
|
# hardcoded values if the config isn't reachable (the GUI can run standalone
|
||||||
|
# from a workstation that isn't on the same filesystem as the project).
|
||||||
|
_PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
if _PROJECT_DIR not in sys.path:
|
||||||
|
sys.path.insert(0, _PROJECT_DIR)
|
||||||
|
try:
|
||||||
|
from Core.config_loader import load_config
|
||||||
|
_net = load_config("Network")
|
||||||
|
JETSON_IP = _net.get("jetson_ip", "192.168.123.164")
|
||||||
|
JETSON_PORT = _net.get("websocket_port", 8765)
|
||||||
|
except Exception:
|
||||||
JETSON_IP = "192.168.123.164"
|
JETSON_IP = "192.168.123.164"
|
||||||
JETSON_PORT = 8765
|
JETSON_PORT = 8765
|
||||||
|
|
||||||
@ -1000,7 +1012,10 @@ class MarcusGUI:
|
|||||||
p2 = ImageTk.PhotoImage(img_resized)
|
p2 = ImageTk.PhotoImage(img_resized)
|
||||||
self.cam_preview.configure(image=p2, text="")
|
self.cam_preview.configure(image=p2, text="")
|
||||||
self.cam_preview.image = p2
|
self.cam_preview.image = p2
|
||||||
except Exception: pass
|
except Exception as e:
|
||||||
|
# Malformed base64 / torn frame / Tk disposed during shutdown —
|
||||||
|
# log it so UI freezes become debuggable rather than silent.
|
||||||
|
print(f" [Client] _update_frame dropped: {type(e).__name__}: {e}")
|
||||||
|
|
||||||
def _update_cam_config(self, d):
|
def _update_cam_config(self, d):
|
||||||
p, w, h, f = d.get("profile","?"), d.get("width","?"), d.get("height","?"), d.get("fps","?")
|
p, w, h, f = d.get("profile","?"), d.get("width","?"), d.get("height","?"), d.get("fps","?")
|
||||||
|
|||||||
@ -9,7 +9,7 @@ Two sources (priority order):
|
|||||||
1. ROS2 /dog_odom — joint encoder data → ±2cm accuracy
|
1. ROS2 /dog_odom — joint encoder data → ±2cm accuracy
|
||||||
2. Dead reckoning — velocity × time integration → ±10cm accuracy
|
2. Dead reckoning — velocity × time integration → ±10cm accuracy
|
||||||
|
|
||||||
Import in marcus_llava.py
|
Import in marcus_brain.py
|
||||||
--------------------------
|
--------------------------
|
||||||
from marcus_odometry import Odometry
|
from marcus_odometry import Odometry
|
||||||
odom = Odometry()
|
odom = Odometry()
|
||||||
@ -78,7 +78,7 @@ class Odometry:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
self._alive = [False]
|
self._alive = [False]
|
||||||
self._sock = None # ZMQ PUB socket (shared from marcus_llava)
|
self._sock = None # ZMQ PUB socket (shared from marcus_brain)
|
||||||
|
|
||||||
# Position state
|
# Position state
|
||||||
self._x = 0.0 # meters from start
|
self._x = 0.0 # meters from start
|
||||||
@ -105,7 +105,7 @@ class Odometry:
|
|||||||
Start odometry tracking in background thread.
|
Start odometry tracking in background thread.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
zmq_sock : existing ZMQ PUB socket from marcus_llava.py.
|
zmq_sock : existing ZMQ PUB socket from marcus_brain.py.
|
||||||
If None, creates its own socket on ZMQ_PORT.
|
If None, creates its own socket on ZMQ_PORT.
|
||||||
Passing the shared socket avoids port conflicts.
|
Passing the shared socket avoids port conflicts.
|
||||||
|
|
||||||
@ -169,7 +169,7 @@ class Odometry:
|
|||||||
except zmq.ZMQError as e:
|
except zmq.ZMQError as e:
|
||||||
if "Address already in use" in str(e):
|
if "Address already in use" in str(e):
|
||||||
print(f" [Odom] ⚠️ ZMQ port {ZMQ_PORT} already in use.")
|
print(f" [Odom] ⚠️ ZMQ port {ZMQ_PORT} already in use.")
|
||||||
print(" [Odom] Pass zmq_sock=sock from marcus_llava.py instead.")
|
print(" [Odom] Pass zmq_sock=sock from marcus_brain.py instead.")
|
||||||
else:
|
else:
|
||||||
print(f" [Odom] ⚠️ ZMQ error: {e}")
|
print(f" [Odom] ⚠️ ZMQ error: {e}")
|
||||||
self._sock = None
|
self._sock = None
|
||||||
|
|||||||
@ -15,7 +15,7 @@ How it works
|
|||||||
5. Optional: YOLO pre-filter speeds up search (find person class first,
|
5. Optional: YOLO pre-filter speeds up search (find person class first,
|
||||||
then LLaVA verifies it's the right person)
|
then LLaVA verifies it's the right person)
|
||||||
|
|
||||||
Usage in marcus_llava.py
|
Usage in marcus_brain.py
|
||||||
------------------------
|
------------------------
|
||||||
from marcus_imgsearch import ImageSearch
|
from marcus_imgsearch import ImageSearch
|
||||||
searcher = ImageSearch(get_frame_fn=get_frame, send_vel_fn=send_vel,
|
searcher = ImageSearch(get_frame_fn=get_frame, send_vel_fn=send_vel,
|
||||||
@ -420,11 +420,11 @@ class ImageSearch:
|
|||||||
|
|
||||||
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
# WIRE INTO marcus_llava.py — add to main loop
|
# WIRE INTO marcus_brain.py — add to main loop
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Add to marcus_llava.py imports:
|
Add to marcus_brain.py imports:
|
||||||
from marcus_imgsearch import ImageSearch
|
from marcus_imgsearch import ImageSearch
|
||||||
|
|
||||||
Add after Memory init:
|
Add after Memory init:
|
||||||
@ -504,7 +504,7 @@ if __name__ == "__main__":
|
|||||||
elif args.image or args.hint:
|
elif args.image or args.hint:
|
||||||
# Real search — needs robot hardware
|
# Real search — needs robot hardware
|
||||||
print("Real search requires robot hardware.")
|
print("Real search requires robot hardware.")
|
||||||
print("Import ImageSearch into marcus_llava.py instead.")
|
print("Import ImageSearch into marcus_brain.py instead.")
|
||||||
print(f" image: {args.image}")
|
print(f" image: {args.image}")
|
||||||
print(f" hint: {args.hint}")
|
print(f" hint: {args.hint}")
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ marcus_yolo.py — Marcus Vision Module
|
|||||||
=======================================
|
=======================================
|
||||||
Project : Marcus | YS Lootah Technology
|
Project : Marcus | YS Lootah Technology
|
||||||
Purpose : YOLO-based person + object detection
|
Purpose : YOLO-based person + object detection
|
||||||
Import this module in marcus_llava.py — runs as background thread
|
Import this module in marcus_brain.py — runs as background thread
|
||||||
|
|
||||||
Usage (imported):
|
Usage (imported):
|
||||||
from marcus_yolo import start_yolo, yolo_sees, yolo_count, yolo_closest, yolo_summary
|
from marcus_yolo import start_yolo, yolo_sees, yolo_count, yolo_closest, yolo_summary
|
||||||
@ -365,20 +365,20 @@ def _camera_loop(raw_frame_ref, frame_lock, cam_alive):
|
|||||||
time.sleep(2.0)
|
time.sleep(2.0)
|
||||||
|
|
||||||
|
|
||||||
# ── Start function — called by marcus_llava.py ────────────────────────────────
|
# ── Start function — called by marcus_brain.py ────────────────────────────────
|
||||||
|
|
||||||
def start_yolo(raw_frame_ref=None, frame_lock=None):
|
def start_yolo(raw_frame_ref=None, frame_lock=None):
|
||||||
"""
|
"""
|
||||||
Start YOLO inference in background thread.
|
Start YOLO inference in background thread.
|
||||||
|
|
||||||
Called automatically by marcus_llava.py during startup.
|
Called automatically by marcus_brain.py during startup.
|
||||||
Shares the camera frame reference from marcus_llava's camera thread.
|
Shares the camera frame reference from marcus_brain's camera thread.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
raw_frame_ref : list[np.ndarray|None] — shared raw BGR frame
|
raw_frame_ref : list[np.ndarray|None] — shared raw BGR frame
|
||||||
frame_lock : threading.Lock protecting raw_frame_ref
|
frame_lock : threading.Lock protecting raw_frame_ref
|
||||||
|
|
||||||
Example (in marcus_llava.py):
|
Example (in marcus_brain.py):
|
||||||
from marcus_yolo import start_yolo, yolo_sees, yolo_summary
|
from marcus_yolo import start_yolo, yolo_sees, yolo_summary
|
||||||
start_yolo(raw_frame_ref=_raw_frame, frame_lock=_raw_lock)
|
start_yolo(raw_frame_ref=_raw_frame, frame_lock=_raw_lock)
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -22,23 +22,23 @@ Usage:
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
# ─── PATH CONFIG ─────────────────────────────────────────
|
# ─── PATH + CONFIG ───────────────────────────────────────
|
||||||
from dotenv import load_dotenv
|
# Single source of truth lives in Core/; everyone else imports from there.
|
||||||
load_dotenv()
|
_PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
if _PROJECT_DIR not in sys.path:
|
||||||
BASE_PATH = os.environ.get("PROJECT_BASE", "/home/unitree")
|
sys.path.insert(0, _PROJECT_DIR)
|
||||||
PROJECT_NAME = "Marcus"
|
from Core.env_loader import PROJECT_ROOT
|
||||||
PROJECT_ROOT = os.path.join(BASE_PATH, PROJECT_NAME)
|
from Core.config_loader import load_config
|
||||||
|
|
||||||
import json
|
|
||||||
|
|
||||||
LOG_DIR = os.path.join(PROJECT_ROOT, "logs")
|
LOG_DIR = os.path.join(PROJECT_ROOT, "logs")
|
||||||
os.makedirs(LOG_DIR, exist_ok=True)
|
os.makedirs(LOG_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
# Idempotent — only the first call per process installs handlers.
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
|
format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
|
||||||
@ -50,12 +50,6 @@ logging.basicConfig(
|
|||||||
log = logging.getLogger("marcus_voice")
|
log = logging.getLogger("marcus_voice")
|
||||||
|
|
||||||
|
|
||||||
def load_config(name: str) -> dict:
|
|
||||||
path = os.path.join(PROJECT_ROOT, "Config", f"config_{name}.json")
|
|
||||||
with open(path, "r") as f:
|
|
||||||
return json.load(f)
|
|
||||||
|
|
||||||
|
|
||||||
# ─── STATE ENUM ──────────────────────────────────────────
|
# ─── STATE ENUM ──────────────────────────────────────────
|
||||||
|
|
||||||
class State:
|
class State:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user