Cozmo Update
text · 19 views · 5 unique · 2026-04-15 13:34:04 · raw
Subject: Cozmo bridge feedback — HOLD not working + profile selector issue
Hey Goosely,Thanks for the new auto-generated bridge and the HOLD/TAP button types — it's a great addition.I merged your latest script with my full custom control script (cozmo_hobo_control.py). Here's the current status:What works great:All TAP commands work (forward, backward, turn_left, turn_right, lift_up, lift_down, head_up, head_down, faces, machine gun, toggle_mode, etc.)
All my custom faces and animations (mechaMG with per-frame delays, otter, dual otter, ArmCat, J, NFlag, etc.)
Local joystick + keyboard controls have full priority
OpenCV windows (Eye View, Face Preview, HUD)
Aggressive keep-alive heartbeat (prevents sleeping)
Gear cycling, machine gun lift, hat slam, etc.
Issues:HOLD functionality not working
When I use a button set to [HOLD] style (e.g. forward), I do not see key_down or key_up messages in the terminal.
Instead I only see:
Viewer: command → forward
The robot does not drive continuously in smooth mode, and release does not stop it cleanly.
Profile selector bug
Any button profile that has at least one button set to the new [HOLD] type does not appear in the profile select dropdown when starting the stream.
When switching profiles on the fly, I can switch to other profiles, but when I try to select a profile that uses HOLD buttons, it says “it will be active next stream” — but it never actually appears in the profile list.
Everything else is working nicely. Would you (or your AI) be able to check:Why HOLD buttons are coming through as type command instead of key_down/key_up
Why profiles containing HOLD buttons are hidden from the selector
Here is my current full cozmo_hobo_control.py if it helps with testing.Thanks again — the custom button profiles and HOLD support are really cool features. We're super close to having a solid Cozmo streaming setup.Let me know what you need from me.— Maticus
The [HOLD] stuff was removed because it was breaking my shit n shit.
#!/usr/bin/env python3
"""
cozmo_hobo_control.py — Merged version with custom faces + HoboStreamer support
Local joystick/keyboard have priority. Viewer commands work via TAP.
HOLD support is the only remaining item being worked on with Goosely.
"""
import pycozmo
import pygame
import cv2
import time
import numpy as np
import os
import random
from PIL import Image
import json
import threading
from queue import Queue
import websocket
# ========================== TUNABLE SETTINGS ==========================
JUMPY_BURST_DURATION = 0.07
JUMPY_PAUSE_TIME = 0.03
FACE_COOLDOWN = 0.35
MODE_COOLDOWN = 0.25
GEAR_COOLDOWN = 0.4
THROTTLE_LOW_THRESHOLD = -0.85
MG_PULSES = 12
MG_INTERVAL = 0.06
MG_VIBRATE = 18
STICK_DEADZONE = 0.08
DRIVE_SPEED_BASE = 140
TURN_SPEED = 90
# =====================================================================
pygame.init()
pygame.joystick.init()
screen = pygame.display.set_mode((400, 280))
pygame.display.set_caption("Cozmo Hobo Control - ESC to quit")
print("Connecting to Cozmo...")
joystick = None
if pygame.joystick.get_count() > 0:
joystick = pygame.joystick.Joystick(0)
joystick.init()
print(f"✅ Joystick detected: {joystick.get_name()}")
else:
print("⚠️ No joystick found — keyboard only")
latest_image = None
ir_light_on = False
last_battery_check = 0
battery_voltage = 0.0
battery_percent = 0
last_face_time = 0
last_mode_time = 0
last_gear_time = 0
mirror_mode = False
viewer_control_enabled = True
last_face_image = np.zeros((32, 128), dtype=np.uint8) * 255
def on_camera_image(cli, image):
global latest_image
latest_image = image.copy()
# ====================== DEDICATED HEARTBEAT THREAD ======================
def heartbeat_thread(cli):
while True:
try:
head = 0.0
cli.set_head_angle(head + 0.015)
time.sleep(0.03)
cli.set_head_angle(head)
cli.drive_wheels(10, 10, duration=0.12)
except:
pass
time.sleep(2.5)
# =========================================================================
driving_mode = "smooth"
# === CUSTOM FACES ===
custom_faces_dir = os.path.expanduser("~/Desktop/custom_faces")
static_faces = []
face_filenames = []
armcat_up = armcat_down = hit_left = hit_right = jL = jR = None
nflag_frames = []
otter_frames = []
mechaMG_frames = []
mechaMG_delays = []
mechaMG_folder = os.path.expanduser("~/Desktop/mechaMG")
if os.path.exists(mechaMG_folder):
mg_files = sorted([f for f in os.listdir(mechaMG_folder) if f.lower().startswith("frame_") and f.lower().endswith(".png")])
for filename in mg_files:
path = os.path.join(mechaMG_folder, filename)
try:
im = Image.open(path).convert("1")
mg_resized = im.resize((64, 48), Image.NEAREST)
frame = Image.new("1", (128, 32), color=0)
offset_x = (128 - 64) // 2
offset_y = (32 - 48) // 2 + 4
frame.paste(mg_resized, (offset_x, offset_y))
mechaMG_frames.append(frame)
delay = 0.2
if "_delay-" in filename.lower():
try:
delay_str = filename.lower().split("_delay-")[1].split("s.")[0]
delay = float(delay_str)
except:
pass
mechaMG_delays.append(delay)
except Exception as e:
print(f"⚠️ Could not load mechaMG {filename}: {e}")
otter_folder = os.path.expanduser("~/Desktop/otterGIF")
if os.path.exists(otter_folder):
for i in range(10):
filename = f"frame_{i:02d}_delay-0.04s.png"
path = os.path.join(otter_folder, filename)
try:
im = Image.open(path).convert("1")
frame = Image.new("1", (128, 32), color=0)
otter_resized = im.resize((64, 64), Image.NEAREST)
offset_x = (128 - 64) // 2
offset_y = max(0, (32 - 64) // 2)
frame.paste(otter_resized, (offset_x, offset_y))
otter_frames.append(frame)
except Exception as e:
print(f"⚠️ Could not load otter frame {i:02d}: {e}")
if os.path.exists(custom_faces_dir):
bmp_files = [f for f in os.listdir(custom_faces_dir) if f.lower().endswith('.bmp')]
for f in sorted(bmp_files):
path = os.path.join(custom_faces_dir, f)
try:
im = Image.open(path).resize((128, 32), Image.NEAREST).convert("1")
fl = f.lower()
if fl == "armcatup.bmp": armcat_up = im
elif fl == "armcatdown.bmp": armcat_down = im
elif fl in ("hitl.bmp", "hit1.bmp", "left.bmp"): hit_left = im
elif fl in ("hitr.bmp", "hit2.bmp", "right.bmp"): hit_right = im
elif fl == "jl.bmp": jL = im
elif fl == "jr.bmp": jR = im
elif fl == "nflag1.1.bmp":
for angle in range(0, 360, 45):
rotated = im.rotate(angle, expand=False, fillcolor=0)
nflag_frames.append(rotated)
else:
static_faces.append(im)
face_filenames.append(f)
except Exception as e:
print(f"⚠️ Could not load {f}: {e}")
current_face_idx = -1
animation_mode = None
last_frame_time = 0
current_armcat_frame = current_hit_frame = current_j_frame = current_nflag_frame = 0
current_otter_frame = current_dual_otter_frame = 0
current_mechaMG_frame = 0
procedural_enabled = False
gears = [("FAST", 140), ("MEDIUM", 88), ("SLOW", 55)]
gear_index = 0
drive_speed = gears[gear_index][1]
def set_procedural_face(cli, enabled: bool):
global procedural_enabled
cli.enable_procedural_face(enabled)
procedural_enabled = enabled
print("👀 Default procedural eyes " + ("ENABLED" if enabled else "DISABLED"))
# ========================== HOBOSTREAMER ==========================
# REPLACE WITH YOUR REAL STREAM KEY WHEN RUNNING
STREAM_KEY = "YOUR_REAL_STREAM_KEY_HERE"
WS_URL = f"wss://hobostreamer.com/ws/control?mode=hardware&stream_key={STREAM_KEY}"
remote_command_queue = Queue(maxsize=30)
def on_hobo_message(ws, raw_msg):
try:
msg = json.loads(raw_msg)
t = msg.get("type")
if t == "connected":
print("✅ HoboStreamer hardware bridge connected!")
try:
ws.send(json.dumps({"type": "status", "status": "ready", "robot": "cozmo"}))
except:
pass
return
if t in ("command", "key_down", "key_up", "video_click"):
remote_command_queue.put(msg)
except Exception as e:
print(f"WS parse error: {e}")
def hobo_websocket_thread():
while True:
try:
ws = websocket.WebSocketApp(
WS_URL,
on_message=on_hobo_message,
on_error=lambda ws, err: print(f"WS error: {err}"),
on_close=lambda ws, *args: print("WS closed - reconnecting in 5s...")
)
ws.run_forever(ping_interval=25, ping_timeout=10)
except Exception as e:
print(f"WS thread crashed: {e}")
time.sleep(5)
ws_thread = threading.Thread(target=hobo_websocket_thread, daemon=True)
ws_thread.start()
print("🚀 HoboStreamer WebSocket thread started...")
with pycozmo.connect(enable_procedural_face=False) as cli:
cli.load_anims()
cli.enable_camera(enable=True, color=True)
cli.set_head_light(enable=ir_light_on)
cli.add_handler(pycozmo.event.EvtNewRawCameraImage, on_camera_image)
set_procedural_face(cli, False)
hb_thread = threading.Thread(target=heartbeat_thread, args=(cli,), daemon=True)
hb_thread.start()
print("❤️ Aggressive keep-alive thread started")
print("🎉 Cozmo is ready for maximum stream chaos!")
print("=== FULL CONTROLS ===")
print("Local joystick + keyboard have priority")
print("WASD/Arrows : Drive/Turn")
print("Q/E : Head (when throttle low)")
print("R/F : Gradual Lift")
print("U/I : Slam Lift")
print("Hat Up/Down : Gradual Lift")
print("Hat L/R : Slam Lift")
print("Trigger : Machine Gun")
print("Button 1 : Cycle Gear")
print("Button 3 : Toggle Mode")
print("Button 4 : Toggle Viewer Control")
print("G/Y/H/J/K/N/O/P/M : Faces")
print("Space : Emergency Stop")
print("ESC : Quit")
print("====================")
cv2.namedWindow("Cozmo Eye View", cv2.WINDOW_NORMAL)
cv2.namedWindow("Cozmo Face Preview", cv2.WINDOW_NORMAL)
cv2.namedWindow("Cozmo HUD", cv2.WINDOW_NORMAL)
clock = pygame.time.Clock()
head_angle = 0.0
lift_height_mm = pycozmo.MIN_LIFT_HEIGHT.mm
running = True
def handle_remote_command(msg):
global driving_mode, animation_mode, last_face_time, last_mode_time, lift_height_mm, head_angle
global current_mechaMG_frame, current_otter_frame, current_dual_otter_frame
global current_armcat_frame, current_j_frame, current_nflag_frame, current_hit_frame
if not viewer_control_enabled:
return
cmd = msg.get("command")
msg_type = msg.get("type")
now = time.time()
print(f"📡 Viewer: {msg_type} → {cmd}")
# === DRIVE COMMANDS ===
if cmd in ("forward", "backward", "turn_left", "turn_right"):
if cmd == "forward":
left = right = drive_speed
elif cmd == "backward":
left = right = -drive_speed
elif cmd == "turn_left":
left = -int(drive_speed * 0.8)
right = int(drive_speed * 0.8)
elif cmd == "turn_right":
left = int(drive_speed * 0.8)
right = -int(drive_speed * 0.8)
if driving_mode == "smooth":
cli.drive_wheels(left, right)
time.sleep(0.6) # visible movement
else:
cli.drive_wheels(left, right, duration=JUMPY_BURST_DURATION)
time.sleep(JUMPY_PAUSE_TIME)
return
# One-shot commands
if cmd in ("mechaMG", "p"):
if animation_mode == "mechaMG":
animation_mode = None
elif mechaMG_frames:
set_procedural_face(cli, False)
animation_mode = "mechaMG"
current_mechaMG_frame = 0
last_face_time = now
elif cmd in ("otter", "g"):
animation_mode = "otter" if animation_mode != "otter" else None
if animation_mode == "otter":
current_otter_frame = 0
last_frame_time = now
elif cmd in ("dual_otter", "y"):
animation_mode = "dual_otter" if animation_mode != "dual_otter" else None
if animation_mode == "dual_otter":
current_dual_otter_frame = 0
last_frame_time = now
elif cmd in ("armcat", "k"):
animation_mode = "armcat" if animation_mode != "armcat" else None
if animation_mode == "armcat":
current_armcat_frame = 0
last_frame_time = now
elif cmd in ("j_animation", "j"):
animation_mode = "j" if animation_mode != "j" else None
if animation_mode == "j":
current_j_frame = 0
last_frame_time = now
elif cmd in ("nflag", "n"):
animation_mode = "nflag" if animation_mode != "nflag" else None
if animation_mode == "nflag":
current_nflag_frame = 0
last_frame_time = now
elif cmd in ("random_glance", "h"):
animation_mode = "hit" if animation_mode != "hit" else None
if animation_mode == "hit":
current_hit_frame = random.choice([0, 1])
last_frame_time = now
elif cmd in ("toggle_eyes", "o"):
new_state = not procedural_enabled
set_procedural_face(cli, new_state)
if new_state:
animation_mode = None
elif cmd in ("machine_gun", "mg"):
if now - last_face_time > 0.5:
current_lift = lift_height_mm
for _ in range(MG_PULSES):
cli.set_lift_height(min(pycozmo.MAX_LIFT_HEIGHT.mm, current_lift + MG_VIBRATE), accel=3000, duration=0.04)
time.sleep(MG_INTERVAL)
cli.set_lift_height(max(pycozmo.MIN_LIFT_HEIGHT.mm, current_lift - MG_VIBRATE), accel=3000, duration=0.04)
time.sleep(MG_INTERVAL)
cli.set_lift_height(current_lift, accel=800, duration=0.12)
last_face_time = now
elif cmd in ("toggle_mode", "x"):
if now - last_mode_time > MODE_COOLDOWN:
driving_mode = "jumpy" if driving_mode == "smooth" else "smooth"
print(f"🔄 Viewer switched mode → {driving_mode.upper()}")
last_mode_time = now
elif cmd in ("emergency_stop", "space", "stop"):
cli.drive_wheels(0, 0)
elif cmd == "lift_up":
lift_height_mm = min(pycozmo.MAX_LIFT_HEIGHT.mm, lift_height_mm + 35)
cli.set_lift_height(lift_height_mm)
elif cmd == "lift_down":
lift_height_mm = max(pycozmo.MIN_LIFT_HEIGHT.mm, lift_height_mm - 35)
cli.set_lift_height(lift_height_mm)
elif cmd == "head_up":
head_angle = min(pycozmo.MAX_HEAD_ANGLE.radians, head_angle + 0.25)
cli.set_head_angle(head_angle)
elif cmd == "head_down":
head_angle = max(pycozmo.MIN_HEAD_ANGLE.radians, head_angle - 0.25)
cli.set_head_angle(head_angle)
last_face_time = max(last_face_time, now - FACE_COOLDOWN + 0.05)
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT or (event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE):
running = False
pygame.event.pump()
keys = pygame.key.get_pressed()
now = time.time()
# Viewer toggle (C key + button 4)
if (keys[pygame.K_c] or (joystick and joystick.get_button(4))) and now - last_face_time > 0.3:
viewer_control_enabled = not viewer_control_enabled
print(f"🔌 Viewer control {'ENABLED' if viewer_control_enabled else 'DISABLED'}")
last_face_time = now
# Mode toggle (X key + button 3)
if (keys[pygame.K_x] or (joystick and joystick.get_button(3))) and now - last_mode_time > MODE_COOLDOWN:
driving_mode = "jumpy" if driving_mode == "smooth" else "smooth"
print(f"🔄 Driving mode → {driving_mode.upper()}")
last_mode_time = now
# Process remote commands
if viewer_control_enabled:
while not remote_command_queue.empty():
msg = remote_command_queue.get_nowait()
handle_remote_command(msg)
# Gear
if (keys[pygame.K_z] or (joystick and joystick.get_button(1))) and now - last_gear_time > GEAR_COOLDOWN:
gear_index = (gear_index + 1) % len(gears)
drive_speed = gears[gear_index][1]
print(f"🔧 Gear: {gears[gear_index][0]}")
last_gear_time = now
# Head from joystick
if joystick:
throttle = joystick.get_axis(3)
if throttle < THROTTLE_LOW_THRESHOLD:
if keys[pygame.K_q]:
head_angle = min(pycozmo.MAX_HEAD_ANGLE.radians, head_angle + 0.15)
if keys[pygame.K_e]:
head_angle = max(pycozmo.MIN_HEAD_ANGLE.radians, head_angle - 0.15)
else:
head_angle = pycozmo.MAX_HEAD_ANGLE.radians - (throttle + 1.0) * 0.5 * (pycozmo.MAX_HEAD_ANGLE.radians - pycozmo.MIN_HEAD_ANGLE.radians)
cli.set_head_angle(head_angle)
# Local drive (priority)
left = right = 0
if joystick:
y = joystick.get_axis(1)
twist = joystick.get_axis(2)
if abs(y) < STICK_DEADZONE: y = 0
if abs(twist) < STICK_DEADZONE: twist = 0
forward = -y * drive_speed
turn = twist * drive_speed
if mirror_mode:
turn = -turn
left = forward - turn
right = forward + turn
if keys[pygame.K_w] or keys[pygame.K_UP]: left = right = drive_speed
elif keys[pygame.K_s] or keys[pygame.K_DOWN]: left = right = -drive_speed
if keys[pygame.K_a] or keys[pygame.K_LEFT]: left = -drive_speed; right = drive_speed
elif keys[pygame.K_d] or keys[pygame.K_RIGHT]: left = drive_speed; right = -drive_speed
if driving_mode == "smooth":
if abs(left) > 5 or abs(right) > 5:
cli.drive_wheels(lwheel_speed=left, rwheel_speed=right)
else:
cli.drive_wheels(0, 0)
else:
if abs(left) > 5 or abs(right) > 5:
cli.drive_wheels(lwheel_speed=left, rwheel_speed=right, duration=JUMPY_BURST_DURATION)
time.sleep(JUMPY_PAUSE_TIME)
else:
cli.drive_wheels(0, 0)
prev_left = left
prev_right = right
if keys[pygame.K_SPACE]:
cli.drive_wheels(0, 0)
# Mirror
if keys[pygame.K_b]:
mirror_mode = not mirror_mode
print(f"🪞 Mirror Mode: {'ON' if mirror_mode else 'OFF'}")
time.sleep(0.3)
# Lift + Hat
if joystick:
hat = joystick.get_hat(0)
if hat[1] == 1:
lift_height_mm = min(pycozmo.MAX_LIFT_HEIGHT.mm, lift_height_mm + 4)
cli.set_lift_height(lift_height_mm)
elif hat[1] == -1:
lift_height_mm = max(pycozmo.MIN_LIFT_HEIGHT.mm, lift_height_mm - 4)
cli.set_lift_height(lift_height_mm)
if hat[0] == 1 and now - last_face_time > 0.25:
cli.set_lift_height(pycozmo.MAX_LIFT_HEIGHT.mm, accel=1200, duration=0.18)
print("🚀 Hat Slam UP!")
last_face_time = now
elif hat[0] == -1 and now - last_face_time > 0.25:
cli.set_lift_height(pycozmo.MIN_LIFT_HEIGHT.mm, accel=1200, duration=0.18)
print("🔽 Hat Slam DOWN!")
last_face_time = now
if keys[pygame.K_r]:
lift_height_mm = min(pycozmo.MAX_LIFT_HEIGHT.mm, lift_height_mm + 4)
cli.set_lift_height(lift_height_mm)
if keys[pygame.K_f]:
lift_height_mm = max(pycozmo.MIN_LIFT_HEIGHT.mm, lift_height_mm - 4)
cli.set_lift_height(lift_height_mm)
if keys[pygame.K_u]:
cli.set_lift_height(pycozmo.MAX_LIFT_HEIGHT.mm, accel=1200, duration=0.18)
print("🚀 Manual Slam UP!")
time.sleep(0.2)
if keys[pygame.K_i]:
cli.set_lift_height(pycozmo.MIN_LIFT_HEIGHT.mm, accel=1200, duration=0.18)
print("🔽 Manual Slam DOWN!")
time.sleep(0.2)
# Machine Gun
if joystick and joystick.get_button(0) and now - last_face_time > 0.5:
print("🔫 MACHINE GUN FIRING!")
current_lift = lift_height_mm
for _ in range(MG_PULSES):
cli.set_lift_height(min(pycozmo.MAX_LIFT_HEIGHT.mm, current_lift + MG_VIBRATE), accel=3000, duration=0.04)
time.sleep(MG_INTERVAL)
cli.set_lift_height(max(pycozmo.MIN_LIFT_HEIGHT.mm, current_lift - MG_VIBRATE), accel=3000, duration=0.04)
time.sleep(MG_INTERVAL)
cli.set_lift_height(current_lift, accel=800, duration=0.12)
last_face_time = now
# IR Light
if keys[pygame.K_l]:
ir_light_on = not ir_light_on
cli.set_head_light(enable=ir_light_on)
print(f"💡 IR Light: {'ON' if ir_light_on else 'OFF'}")
time.sleep(0.2)
# Face commands
if now - last_face_time > FACE_COOLDOWN:
if keys[pygame.K_m] and static_faces:
current_face_idx = (current_face_idx + 1) % len(static_faces)
set_procedural_face(cli, False)
current_face = static_faces[current_face_idx]
cli.display_image(current_face)
animation_mode = None
print(f"🖼️ Static Face: {face_filenames[current_face_idx]}")
last_face_time = now
last_face_image = np.array(current_face).astype(np.uint8) * 255
if keys[pygame.K_g]:
animation_mode = "otter" if animation_mode != "otter" else None
print("🦦 Single Otter " + ("ON" if animation_mode == "otter" else "OFF"))
if animation_mode == "otter":
current_otter_frame = 0
last_frame_time = time.time()
last_face_time = now
if keys[pygame.K_y]:
animation_mode = "dual_otter" if animation_mode != "dual_otter" else None
print("🦦🦦 Dual Otters " + ("ON" if animation_mode == "dual_otter" else "OFF"))
if animation_mode == "dual_otter":
current_dual_otter_frame = 0
last_frame_time = time.time()
last_face_time = now
if keys[pygame.K_h]:
animation_mode = "hit" if animation_mode != "hit" else None
print("👀 Random Glance " + ("ON" if animation_mode == "hit" else "OFF"))
if animation_mode == "hit":
current_hit_frame = random.choice([0, 1])
last_frame_time = time.time()
last_face_time = now
if keys[pygame.K_k]:
animation_mode = "armcat" if animation_mode != "armcat" else None
print("🐱 ArmCat " + ("ON" if animation_mode == "armcat" else "OFF"))
if animation_mode == "armcat":
current_armcat_frame = 0
last_frame_time = time.time()
last_face_time = now
if keys[pygame.K_j]:
animation_mode = "j" if animation_mode != "j" else None
print("🃏 J Animation " + ("ON" if animation_mode == "j" else "OFF"))
if animation_mode == "j":
current_j_frame = 0
last_frame_time = time.time()
last_face_time = now
if keys[pygame.K_n]:
animation_mode = "nflag" if animation_mode != "nflag" else None
print("🏳️ NFlag Spinning " + ("ON" if animation_mode == "nflag" else "OFF"))
if animation_mode == "nflag":
current_nflag_frame = 0
last_frame_time = time.time()
last_face_time = now
if keys[pygame.K_o]:
new_state = not procedural_enabled
set_procedural_face(cli, new_state)
if new_state:
animation_mode = None
last_face_time = now
if keys[pygame.K_p]:
if animation_mode == "mechaMG":
animation_mode = None
print("🤖 mechaMG stopped")
elif mechaMG_frames:
set_procedural_face(cli, False)
animation_mode = "mechaMG"
current_mechaMG_frame = 0
last_frame_time = time.time()
print("🤖 mechaMG activated!")
last_face_time = now
# Run Animations
if not procedural_enabled:
now_anim = time.time()
if animation_mode == "armcat" and armcat_up and armcat_down and now_anim - last_frame_time > 0.2:
current_armcat_frame = 1 - current_armcat_frame
current_face = armcat_up if current_armcat_frame == 0 else armcat_down
cli.display_image(current_face)
last_frame_time = now_anim
last_face_image = np.array(current_face).astype(np.uint8) * 255
elif animation_mode == "hit" and hit_left and hit_right and now_anim - last_frame_time > 0:
delay = random.uniform(0.2, 0.5) if random.random() < 0.05 else random.uniform(2.0, 4.0)
if now_anim - last_frame_time > delay:
current_hit_frame = 1 - current_hit_frame
current_face = hit_left if current_hit_frame == 0 else hit_right
cli.display_image(current_face)
last_frame_time = now_anim
last_face_image = np.array(current_face).astype(np.uint8) * 255
elif animation_mode == "j" and jL and jR and now_anim - last_frame_time > 1.0:
current_j_frame = 1 - current_j_frame
current_face = jL if current_j_frame == 0 else jR
cli.display_image(current_face)
last_frame_time = now_anim
last_face_image = np.array(current_face).astype(np.uint8) * 255
elif animation_mode == "nflag" and nflag_frames and now_anim - last_frame_time > 0.08:
current_nflag_frame = (current_nflag_frame + 1) % len(nflag_frames)
current_face = nflag_frames[current_nflag_frame]
cli.display_image(current_face)
last_frame_time = now_anim
last_face_image = np.array(current_face).astype(np.uint8) * 255
elif animation_mode == "otter" and otter_frames and now_anim - last_frame_time > 0.04:
current_otter_frame = (current_otter_frame + 1) % len(otter_frames)
current_face = otter_frames[current_otter_frame]
cli.display_image(current_face)
last_frame_time = now_anim
last_face_image = np.array(current_face).astype(np.uint8) * 255
elif animation_mode == "dual_otter" and otter_frames and now_anim - last_frame_time > 0.04:
current_dual_otter_frame = (current_dual_otter_frame + 1) % len(otter_frames)
combined = Image.new("1", (128, 32), color=0)
left_idx = current_dual_otter_frame % len(otter_frames)
right_idx = (current_dual_otter_frame + 3) % len(otter_frames)
combined.paste(otter_frames[left_idx].crop((32, 0, 96, 32)), (0, 0))
combined.paste(otter_frames[right_idx].crop((32, 0, 96, 32)), (64, 0))
current_face = combined
cli.display_image(current_face)
last_frame_time = now_anim
last_face_image = np.array(current_face).astype(np.uint8) * 255
elif animation_mode == "mechaMG" and mechaMG_frames:
if now_anim - last_frame_time > mechaMG_delays[current_mechaMG_frame]:
current_mechaMG_frame = (current_mechaMG_frame + 1) % len(mechaMG_frames)
current_face = mechaMG_frames[current_mechaMG_frame]
cli.display_image(current_face)
last_frame_time = now_anim
last_face_image = np.array(current_face).astype(np.uint8) * 255
# Face Preview
preview = cv2.resize(last_face_image, (256, 64), interpolation=cv2.INTER_NEAREST)
preview = cv2.cvtColor(preview, cv2.COLOR_GRAY2BGR)
cv2.imshow("Cozmo Face Preview", preview)
# HUD
if now - last_battery_check > 5.0:
try:
battery_voltage = cli.battery_voltage
battery_percent = max(0, min(100, int((battery_voltage - 3.0) * 100 / 1.2)))
except:
battery_voltage = 0.0
battery_percent = 0
last_battery_check = now
hud = np.zeros((200, 340, 3), dtype=np.uint8)
cv2.putText(hud, f"Battery: {battery_voltage:.2f} V", (15, 35), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255,255,255), 2)
color = (0, 255, 0) if battery_percent > 40 else (0, 165, 255) if battery_percent > 20 else (0, 0, 255)
cv2.putText(hud, f"{battery_percent}%", (15, 70), cv2.FONT_HERSHEY_SIMPLEX, 1.4, color, 3)
cv2.putText(hud, f"Gear: {gears[gear_index][0]}", (15, 110), cv2.FONT_HERSHEY_SIMPLEX, 0.85, (255,255,100), 2)
mode_text = "Mode: Hobo" if driving_mode == "smooth" else "Mode: Robo"
cv2.putText(hud, mode_text, (15, 140), cv2.FONT_HERSHEY_SIMPLEX, 0.85, (0,255,255), 2)
viewer_text = "Viewer: ON" if viewer_control_enabled else "Viewer: OFF"
cv2.putText(hud, viewer_text, (15, 170), cv2.FONT_HERSHEY_SIMPLEX, 0.85, (0,255,0) if viewer_control_enabled else (0,0,255), 2)
cv2.imshow("Cozmo HUD", hud)
if latest_image is not None:
img = cv2.cvtColor(np.array(latest_image), cv2.COLOR_RGB2BGR)
cv2.imshow("Cozmo Eye View", img)
cv2.waitKey(1)
time.sleep(0.008)
clock.tick(60)
print("\nStopping safely...")
cli.drive_wheels(0, 0)
cli.set_lift_height(pycozmo.MIN_LIFT_HEIGHT.mm)
cli.enable_camera(enable=False)
cli.set_head_light(enable=False)
set_procedural_face(cli, True)
cv2.destroyAllWindows()
pygame.quit()
print("Disconnected.")