Note

Hello, welcome to the SunFounder Raspberry Pi & Arduino & ESP32 Enthusiasts Community on Facebook! Dive deeper into Raspberry Pi, Arduino, and ESP32 with fellow enthusiasts.

Why Join?

  • Expert Support: Solve post-sale issues and technical challenges with help from our community and team.

  • Learn & Share: Exchange tips and tutorials to enhance your skills.

  • Exclusive Previews: Get early access to new product announcements and sneak peeks.

  • Special Discounts: Enjoy exclusive discounts on our newest products.

  • Festive Promotions and Giveaways: Take part in giveaways and holiday promotions.

👉 Ready to explore and create with us? Click [here] and join today!

8. Treasure Hunt

Arrange a maze in your room and place six different color cards in six corners. Then control PiCrawler to search for these color cards one by one!

Note

You can download and print the PDF Color Cards for color detection.

Run the Code

cd ~/picrawler/examples
sudo python3 8_treasure_hunt.py

View the Image

After the code runs, the terminal will display the following prompt:

No desktop !
* Serving Flask app "vilib.vilib" (lazy loading)
* Environment: production
WARNING: Do not use the development server in a production environment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://0.0.0.0:9000/ (Press CTRL+C to quit)

Then you can enter http://<your IP>:9000/mjpg in the browser to view the video screen. such as: http://192.168.18.113:9000/mjpg

../_images/display.png

Code

#!/usr/bin/env python3
from picrawler import Picrawler
from time import sleep, time
from robot_hat import Music, TTS
from vilib import Vilib
import readchar
import random
import threading

crawler = Picrawler()
music = Music()   # kept for compatibility (not used here)
tts = TTS()

MANUAL = '''
Press keys on keyboard to control Picrawler!
        w: Forward
        a: Turn left
        s: Backward
        d: Turn right
        space: Say the target again
        Ctrl+C: Quit
'''

color = "red"
color_list = ["red", "orange", "yellow", "green", "blue", "purple"]

key_dict = {
        'w': 'forward',
        's': 'backward',
        'a': 'turn_left',
        'd': 'turn_right',
}

# ----------------------------
# Thread-safe key handling
# ----------------------------
lock = threading.Lock()
key_state = None               # last key event
stop_event = threading.Event() # signal to exit cleanly

def set_key(k):
        global key_state
        with lock:
                key_state = k

def pop_key():
        """Read and clear the last key event."""
        global key_state
        with lock:
                k = key_state
                key_state = None
        return k

def key_scan_thread():
        """Keyboard input thread (quiet exit on Ctrl+C)."""
        while not stop_event.is_set():
                try:
                        k = readchar.readkey()
                except KeyboardInterrupt:
                        # Ctrl+C may raise KeyboardInterrupt inside this thread
                        stop_event.set()
                        break
                except Exception:
                        sleep(0.02)
                        continue

                if k == readchar.key.SPACE:
                        set_key('space')
                elif k == readchar.key.CTRL_C:
                        set_key('quit')
                        stop_event.set()
                        break
                else:
                        try:
                                set_key(str(k).lower())
                        except Exception:
                                pass

                sleep(0.01)

# ----------------------------
# Game logic
# ----------------------------
def renew_color_detect():
        global color
        color = random.choice(color_list)
        try:
                Vilib.color_detect(color)
        except Exception:
                pass
        try:
                tts.say("Look for " + color)
        except Exception:
                pass

def safe_camera_close():
        try:
                Vilib.color_detect("close")
        except Exception:
                pass
        try:
                Vilib.camera_close()
        except Exception:
                pass

def safe_sit():
        try:
                crawler.do_step('sit', 40)
                sleep(0.5)
        except Exception:
                pass

def stand_ready():
        """
        Stand up after startup.
        Requirement: stand at 40, then only move after WASD is pressed.
        """
        try:
                crawler.do_step('stand', 40)
                sleep(0.8)
        except Exception:
                pass

def main():
        speed = 80
        action = None

        # Start camera + web preview
        Vilib.camera_start(vflip=False, hflip=False)
        Vilib.display(local=False, web=True)
        sleep(0.8)

        print(MANUAL)

        # Start keyboard thread (daemon, so it won't block process exit)
        t = threading.Thread(target=key_scan_thread, daemon=True)
        t.start()

        # Announce and stand up to 40
        try:
                tts.say("game start")
        except Exception:
                pass
        sleep(0.05)

        stand_ready()
        renew_color_detect()

        try:
                while not stop_event.is_set():
                        # If target detected and large enough -> renew target
                        try:
                                n = Vilib.detect_obj_parameter.get('color_n', 0)
                                w = Vilib.detect_obj_parameter.get('color_w', 0)
                        except Exception:
                                n, w = 0, 0

                        if n != 0 and w > 100:
                                try:
                                        tts.say("well done")
                                except Exception:
                                        pass
                                sleep(0.05)
                                renew_color_detect()

                        # Handle key event
                        k = pop_key()

                        if k in key_dict:
                                action = key_dict[k]

                        elif k == 'space':
                                try:
                                        tts.say("Look for " + color)
                                except Exception:
                                        pass

                        elif k == 'quit':
                                stop_event.set()

                        # Move only after receiving a WASD action
                        if action is not None:
                                try:
                                        crawler.do_action(action, 1, speed)
                                except Exception:
                                        pass
                                action = None

                        sleep(0.05)

        except KeyboardInterrupt:
                stop_event.set()

        finally:
                # Clean exit
                stop_event.set()
                safe_camera_close()
                safe_sit()
                print("\nQuit")

if __name__ == "__main__":
        main()

How it works?

  1. What This Program Does

    This program is a simple “treasure hunt” game for PiCrawler:

    • The camera streams to a web page (no local GUI window).

    • Vilib detects a target color (red/orange/yellow/green/blue/purple).

    • You control the robot with WASD.

    • When the detected color object is big enough, the program announces success and switches to a new target color.

    • The program exits cleanly on Ctrl+C without thread traceback.

  2. Keyboard Input Runs in a Background Thread

    stop_event = threading.Event()
    key_state = None
    
    def key_scan_thread():
        while not stop_event.is_set():
            try:
                k = readchar.readkey()
            except KeyboardInterrupt:
                stop_event.set()
                break
    

    The keyboard reading is separated into its own thread. This prevents the main loop from blocking while waiting for a key press.

    Ctrl+C may raise KeyboardInterrupt inside this thread (readchar behavior), so it is caught and used to trigger a clean exit instead of printing an error.

  3. Key Events Are Shared Safely

    lock = threading.Lock()
    
    def set_key(k):
        global key_state
        with lock:
            key_state = k
    
    def pop_key():
        global key_state
        with lock:
            k = key_state
            key_state = None
        return k
    

    The program uses a lock to protect the shared variable key_state. The keyboard thread writes key events using set_key(). The main loop reads and clears events using pop_key().

    This ensures the robot reacts to keys safely without race conditions.

  4. Camera and Web Preview

    Vilib.camera_start(vflip=False, hflip=False)
    Vilib.display(local=False, web=True)
    

    The camera is started and the web preview is enabled. local=False avoids GUI crashes on systems without a desktop environment.

  5. Stand Up First, Then Wait for Movement Keys

    crawler.do_step('stand', 40)
    sleep(0.8)
    

    After startup, the robot stands at speed 40 to stabilize posture. The program does not move the robot automatically. It only moves when a WASD key is received.

  6. Selecting a Target Color

    color = random.choice(COLOR_LIST)
    Vilib.color_detect(color)
    tts.say("Look for " + color)
    

    A random color is selected from the list. Vilib color detection is enabled for that color. TTS announces the current target so the user knows what to find.

  7. Detecting “Success” and Switching Targets

    n = Vilib.detect_obj_parameter.get('color_n', 0)
    w = Vilib.detect_obj_parameter.get('color_w', 0)
    
    if n != 0 and w > 100:
        tts.say("well done")
        renew_color_detect()
    

    Vilib updates detect_obj_parameter continuously.

    • color_n indicates whether a target is detected

    • color_w is the detected target width (a rough “how close/big” measure)

    When the target exists and is large enough, the program announces success and immediately switches to a new random target color.

  8. Movement Control With WASD

    if k in key_dict:
        action = key_dict[k]
    
    if action is not None:
        crawler.do_action(action, 1, speed)
        action = None
    

    The main loop checks for a key event:

    • w → forward

    • s → backward

    • a → turn_left

    • d → turn_right

    When a movement key is received, the robot executes one short action step. This design keeps control responsive and avoids continuous runaway movement.

  9. Space Key: Repeat the Target Prompt

    elif k == 'space':
        tts.say("Look for " + color)
    

    Pressing Space repeats the current target message. This is useful if the user forgot the target color.

  10. Quit and Cleanup

    finally:
        stop_event.set()
        Vilib.color_detect("close")
        Vilib.camera_close()
        crawler.do_step('sit', 40)
    

    When quitting:

    • stop_event tells the keyboard thread to stop.

    • Vilib color detection is disabled.

    • The camera is closed safely.

    • The robot sits down to avoid an unstable posture after exit.

    This cleanup order helps prevent camera resource errors and ensures the robot ends in a safe position.