yahboom
HomeTutorialsProduct
Solution
Blog
Online Store
About Us
EN/中文
Store
ROSMASTER M1 PRO📖
0.Robot Configuration and Operation Guidelines
  • 0.Unboxing and Review
  • 1.Assembly Videos
  • 2.User Manual
  • 3.Enter Docker (For Raspberry Pi)
  • 4.FAQ
  • 5.RobotManager User Guide
1.Chassis Control Course
  • 1.Robot Information Publishing
  • 2.Robot Handle Control
  • 3.Robot Keyboard Control
  • 4.Linear Velocity Calibration
  • 5.Angular Velocity Calibration
  • 6.Robot URDF Model
  • 7.Robot EKF State Estimation
2.Lidar Course
  • 1.Lidar Introduction And Use
  • 2.Lidar Obstacle Avoidance
  • 3.Lidar Following
  • 4.Lidar Guard
  • 5.Lidar Patrol
  • 6.GMapping-SLAM Mapping
  • 7.Cartographer-SLAM Mapping
  • 8.Slam-Toolbox Mapping
  • 9.Lifelong Mode Mapping
  • 10.Navigation2 Single Point Navigation
  • 11.Navigation2 Multi Point Navigation
  • 12.Cartographer Relocalization Navigation
  • 13.Slam-Toolbox Localization Navigation
  • 14.Synchronous Mapping And Navigation
  • 15.Aruco Visual Relocalization Navigation
  • 16.Mobile App Mapping And Navigation
3.USB Camera-Depth Camera Course
  • 1.USB Camera Introduction
  • 2.USB Camera Intrinsic Calibration
  • 3.Nuwa-HP60C Camera Introduction (Depth Camera Only)
  • 4.Depth Pseudo Color Image (Depth Camera Only)
  • 5.Depth Camera Distance Measurement (Depth Camera Only)
  • 6.Block Volume Calculation (Depth Camera Only)
  • 7.Edge Detection (Depth Camera Only)
  • 8.RTAB-Map SLAM Mapping (Depth Camera Only)
  • 9.RTAB-Map Navigation (Depth Camera Only)
4.Robotic Arm and 3D Space Gripping Course
  • 1.Robotic Arm Basic Usage
  • 2.Robotic Arm Kinematics
  • 3.Robotic Arm Chassis Coordination Control
  • 4.AprilTag ID Sorting
  • 5.AprilTag Height Anomaly Sorting
  • 6.AprilTag Tracking and Grasping
  • 7.Color Block Sorting
  • 8.Color Block Height Anomaly Sorting
  • 9.Color Block Tracking and Grasping
  • 10.Wood Block Shape Sorting
  • 11.KCF Tracking and Grasping Objects
  • 12.Mediapipe Gesture ID Sorting AprilTag
  • 13.Mediapipe Gesture Height Sorting AprilTag
  • 14.Desktop Tracking and Grasping AprilTag
  • 15.3D Spatial Tracking AprilTag
  • 16.Transformer Model Tracking Algorithm
  • 17.Line Following Obstacle Clearing
5.Road network planning and navigation
  • 1.Introduction To Road Network Planning
  • 2.Road Network Annotation
  • 3.Planning And Navigation Between Waypoints
  • 4.Road Network Navigation In Any Pose
  • 5.Path Re-planning
6.OpenClaw Basic Course
  • 1.Introduction to OpenClaw
  • 2.OpenClaw Core Concepts
  • 3.OpenClaw API-KEY Configuration
  • 4.OpenClaw Switch Models
  • 5.OpenClaw System Prompt Words
  • 6.OpenClaw Common CLI Commands
7.OpenClaw Interaction Method
  • 1.OpenClaw Webchat Interaction
  • 2.OpenClaw Tui Interaction
  • 3.OpenClaw Voice Dialogue Interaction
  • 4.OpenClaw WhatsApp Interaction
8.OpenClaw Development
  • 1.OpenClaw Connecting to Robot MCP Service
  • 2.Robot CLI Command Tool
  • 3.robot-control Plugin Tool
  • 4.reply-bridge Plugin Tool
  • 5.OpenClaw Multimodal Vision
  • 6.OpenClaw Chassis Control
  • 7.OpenClaw Robotic Arm Control
  • 8.OpenClaw Skills Development
  • 9.OpenClaw Scheduled Tasks
  • 10.OpenClaw Multi-Agent Intelligent Agent
  • 11.OpenClaw Active Interaction
  • 12.OpenClaw Vector Memory
9.OpenClaw Embodied intelligent application
  • 1.OpenClaw Target Tracking
  • 2.OpenClaw Arm Tracking And Grasping
  • 3.OpenClaw Arm Fixed-point Placement
  • 4.OpenClaw Waste Sorting
  • 5.OpenClaw SLAM Mapping And Navigation
  • 6.OpenClaw Navigation Transport
  • 7.OpenClaw AprilTag Grasping
  • 8.OpenClaw Color Line Patrol
  • 9.OpenClaw Visual Distance Measurement
10.OpenClaw scenario sandbox
  • 1.Map Description and Props Placement
  • 2.Building the Sandbox Map
  • 3.Visual Relocalization Marker
  • 4.Map Mapping Marker
  • 5.Sandbox Scenario Comprehensive Case
11.AI Large Model Basics
  • 1.AI Large Model Types and Principles
  • 2.RAG Retrieval Enhancement and Model Training Samples
  • 3.Embodied Intelligent Robot System Architecture
  • 4.Embodied Intelligent Functions Core Source Code
12.AI large Model Development
  • 1.Register a Model Service Provider Account
  • 2.Configuring API-KEY
  • 3.Introduction to Dify
  • 4.Basic Dify Features
  • 5.AI Large Model Development - Chatbot
  • 6.Deploy the RAG Knowledge Base
  • 7.RAG Knowledge Base + Chatbot
  • 8.AI Agent Workflow
  • 9.Custom Wake-up Response
13.AI Model - Voice Version
  • 1.Semantic Understanding and Command Following
  • 2.Multimodal Visual Understanding
  • 3.Multimodal Visual Understanding and Robotic Arm Gripping
  • 4.Multimodal Visual Understanding and SLAM Navigation
  • 5.Robotic arm gripping+Multimodal visual understand+SLAM navigation
  • 6.Intention Understanding
14.AI Model - Text Version
  • 1.Semantic Understanding and Command Following
  • 2.Multimodal Visual Understanding
  • 3.Multimodal Visual Understanding and Robotic Arm Gripping
  • 4.Multimodal Visual Understanding and SLAM Navigation
  • 5.Robotic arm gripping+Multimodal visual understand+SLAM navigation
  • 6.Intention estimation
15.Mediapipe Visual Course
  • 1.Hand detection
  • 2.Posture detection
  • 3.Overall detection
  • 4.Facial Landmark Detection
  • 5.Face detection
  • 6.Face special effects
  • 7.3D object recognition
  • 8.Brush
  • 9.Finger control
  • 10.Palm target positioning
  • 11.Fingertip trajectory recognition
  • 12.Fingertip gesture control robotic arm
  • 13.Gesture grabbing and releasing objects
  • 14.Finger control robotic arm
  • 15.Medipipe gesture control robotic arm action group
16.MoveIt2 Simulation Course
  • 1.MovelT2 configuration
  • 2.MovelT2 simulation-reality linkage
  • 3.Random movement
  • 4.Forward kinematics design
  • 5.Inverse kinematics design
  • 6.Cartesian path
  • 7.Trajectory Planning
  • 8.Collision Detection
  • 9.Scene Design
  • 10.3D perception obstacle‑avoidance
17.Multi-vehicle Course
  • 1.Multi-vehicle Control
  • 2.Multi-vehicle Navigation
  • 3.Multi-vehicle Formation
18.YOLO26 Development
  • 1.CLI Use
  • 2.Object Detection
  • 3.Instance Segmentation
  • 4.Pose Estimation
  • 5.Image Classification
  • 6.Oriented Object Detection
  • 7.YOLOv26 + DeepSORT Tracking
  • 8.Monocular Depth Estimation
19.Control Board STM32 Driver Course
  • 0.Notes (Must Read)
  • 1.About Expansion Board
  • 2.STM32 Development Environment
  • 3.Button Control
  • 4.FreeRTOS Application
  • 5.Serial Communication
  • 6.SBUS Model Aircraft Remote Control
  • 7.CAN Bus Communication
  • 8.RGB Colorful Light Bar
  • 9.Timer Interrupt to Control PWM Servo
  • 10.Control Serial Port Servos
  • 11.Nine-axis Attitude Sensor to Obtain Data
  • 12.Control Motor
  • 13.Timer Captures the Encoder Data
  • 14.1 Kinematic Analysis of Mecanum Wheel
  • 14.2 Kinematic Analysis of Ackerman
  • 14.3 Kinematic Analysis of 4WD
  • 15.PID Control Robot Movement
20.Docker Course
  • 1.Docker Introduction and Installation
  • 2.Docker Basic Commands
  • 3.Docker Submission and Multi-Terminal Access
  • 4. Docker Startup Script
  • 5.Docker-Vscode Environment Construction
21.Image Processing Basics Course
  • 1. Introduction to Open Source CV
  • 2. Image reading and display
  • 3. Image writing
  • 4. Image quality
  • 5. Pixel Operation
  • 6. Image zoom
  • 7. Image cutting
  • 8. Image translation
  • 9. Image Mirroring
  • 10. Affine Transformation
  • 11. Image Rotation
  • 12. Perspective Transformation
  • 13. Grayscale processing
  • 14. Image Binarization
  • 15. Edge Detection
  • 16. Line drawing
  • 17. Rectangle and circle drawing
  • 18. Drawing text and pictures
22.Linux System Course
  • 1.Introduction to Linux system
  • 2.Ubuntu file system
  • 3.Ubuntu common commands
  • 4.Ubuntu common editors
  • 5.Ubuntu software operation commands
  • 6.Virtual machine installation
  • 7.SSH remote control
  • 8.VNC remote control
  • 9.Transfer files remotely
  • 10.Driver library and communication
  • 11.Static IP and hotspot mode
  • 12.Bind device ID
  • 13.Capacity expansion and resource allocation
  • 14.Update system software sources
  • 15.Set root user password
  • 16.sudo free password
  • 17.Connect to WiFi network
  • 18.View system version
  • 19.Customized service management
  • 20.Back up system image
23.ROS2 Basic Course
  • 1.Introduction to ROS2
  • 2.ROS2 install Humble
  • 3.ROS2 development environment
  • 4.ROS2 workspace
  • 5.ROS2 function package
  • 6.ROS2 node
  • 7.ROS2 topic communication
  • 8.ROS2 service communication
  • 9.ROS2 action communication
  • 10.ROS2 custom interface message
  • 11.ROS2 parameter service case
  • 12.ROS2 meta-function package
  • 13.ROS2 distributed communication
  • 14.ROS2 DDS
  • 15.ROS2 time related API
  • 16.ROS2 common command tools
  • 17.ROS2 rviz2 use
  • 18.ROS2 rqt toolbox
  • 19.ROS2 Launch startup file configuration
  • 20.ROS2 recording and playback tool
  • 21.ROS2 URDF model
  • 22.ROS2 Gazebo simulation platform
  • 23.ROS2 TF2 coordinate transformation
24.Main Control Course
Jetson Orin Nano_NX
  • 1.Jetson Orin board introduction
  • 2.Restore the factory image system
  • 3.Backup SSD system
  • 4.SSD expansion
  • 5.Network configuration
  • 6.SSH remote login
  • 7.VNC remote control
  • 8.Remote file transfer
  • 9.Jtop tool
  • 10.Exchange space expansion
Raspberry Pi
  • 1. Introduction to Raspberry PI 5
  • 2. Raspberry PI system installation and backup
  • 3. Powering the Raspberry PI 5
  • 4. Startup of Raspberry PI 5
  • 5. Update and upgrade operating system
  • 6. Introduction to raspi-config tool
  • 7. config.txt file description
  • 8. Network Configuration
  • 9. remote access
  • 10. Transfer files remotely
  • 11. Set display resolution and rotation
  • 12. Set screen to sleep
  • 13. Play audio and video
  • 14. Using USB camera
  • 15. Using MIPI camera
  • 16. Get real-time temperature of Raspberry Pi
RDKX5
  • 1.Introduction to RDK X5 board
  • 2.Write system file
  • 3.Hardware interface description
  • 4.Install case and other accessories
  • 5.Network configuration
  • 6.Remote login
  • 7.Update system
  • 8.srpi-config user guide
  • 9.config. txt configuration file
  • 10.Self-startup setting
  • 11.RDK exclusive command usage
  • 12.Linux command usage
  • 13.GPIO readwrite operation
  • 14.Image classification algorithm
  • 15.Use USB camera
  • 16.Use MIPI camera
  • 17.Binocular camera depth algorithm
  • 18. Controlling RGB Cooling Fans
Download
  • System File
  • Code
  • 3D Model File
  • Software Tools
  • Hardware_Info
  • Deb function package
  • Dify AI Agent
Download ZIP

Welcome to ROSMASTER M1 PRO repository

4.Embodied Intelligent Functions Core Source Code

Source Code Analysis of the multi_brains Framework

Source Code Analysis of the multi_brains Framework1. Course Content2.Package Structure of the Source Code2.1 Package File Structure3.Speech Recognition Function3.1 Dynamic Recording with Voice Activity Detection3.3 ASR Speech Recognition4.Model Service Function5. Action Server Function6.Interruption Function6.1 Interruption During the Recording Stage6.2 Interruption During the Dialogue Stage6.3 Interruption During the Action Stage6.3.1 Ordinary Action Interruption6.3.2 Action Interruption with Subprocesses

1. Course Content

  • Analyze the core functions of the multi_brains package architecture.

  • In subsequent lessons, the action functions newly appearing in each tutorial will be explained individually.

Tip

  • This lesson is a pure program-analysis lesson, for users who want a deeper understanding of the functions. If you only need to experience the functions, you can skip this lesson.

2.Package Structure of the Source Code

2.1 Package File Structure

xxxxxxxxxx
├── config
│   ├── map_mapping.yaml
│   ├── multi_brains_setting.yaml
│   └── README.MD
├── language
│   ├── en.yaml
│   └── zh.yaml
├── launch
│   └── llm_agent_control.launch.py
├── multi_brains
│   ├── action_service.py
│   ├── asr_detect.py
│   ├── __init__.py
│   ├── model_service.py
│   └── utils
├── package.xml
├── resource
│   └── multi_brains
├── setup.cfg
├── setup.py
├── system_vioce
│   ├── en
│   ├── notify.mp3
│   ├── test_en.wav
│   ├── test_zh.wav
│   └── zh
  • config

    The configuration folder, storing configuration file templates.

  • multi_brains source code folder

  • asr_detect.py

    The speech recognition program file.

  • model_service.py

    The model server program file, used to call various model interfaces to implement the model inference architecture.

  • action_service.py

    The action server program file, used to receive the action list requested by the model server and control the robot to move.

  • The launch startup file folder, used to store the ros2 node launch files.

  • language pack

    Stores log files of different languages.

  • system_vioce

    System audio files.

 

3.Speech Recognition Function

Source code path:

xxxxxxxxxx
~/yahboomcar_ros2_ws/yahboomcar_ws/src/multi_brains/multi_brains/asr_detect.py

3.1 Dynamic Recording with Voice Activity Detection

_run_streaming is the core loop of offline (local) streaming speech recognition. Combined with VAD voice activity detection, it implements dynamic recording that "recognizes as you speak and stops automatically when you finish". Its processing flow is as follows:

  • Interruptible loop: The main loop condition is while not self._stop_rec_event.is_set(). Once an external event (wake-up again) sets the stop event, the loop exits immediately. This is the basis for interrupting during the recording stage.

  • Fetch audio and resample: Block and take one audio chunk from the audio queue audio_q (0.3s timeout), then use resample_audio to resample from the device sample rate to 16kHz for the recognition engine.

  • Pre-buffer to prevent losing the first word: Before VAD triggers, store the most recent audio chunks in pre_buffer (at most 3 chunks, about 600ms); once VAD detects a human voice (vad_triggered), first feed the pre-buffer to the recognition engine in one go and then feed the current chunk, to avoid losing the beginning of the speech.

  • VAD status detection: Call _vad_engine.accept_waveform + is_detected() to determine whether someone is speaking, and refresh the terminal prompt icon accordingly (○ silent / ● speaking).

  • Endpoint determines the end: When result.is_endpoint is true and the recognized text is not empty, the sentence is considered finished, the final result [✓] is printed, and the loop exits; if it is an empty endpoint, reset() and continue waiting.

  • Dual timeout: If no human voice is ever detected and no_speech_timeout is exceeded, it times out and exits; if a human voice has been detected but silence continues for more than 5 seconds, recording also ends.

xxxxxxxxxx
    def _run_streaming(self, audio_q: queue.Queue, rec_start: float) -> str:
        """Local model streaming ASR processing"""
        if not self._streaming_engine:
            return ''
        speech_detected = False
        last_speech_time = time.time()
        final_text = ''
        prev_display = ''
        pre_buffer = []         # Pre-buffer: save audio before VAD detection to avoid losing the beginning
        PRE_BUFFER_SIZE = 3     # Keep the first 3 audio chunks (approx. 600ms)
        vad_triggered = False  # Whether VAD has been triggered

        while not self._stop_rec_event.is_set():
            try:
                chunk_hw = audio_q.get(timeout=0.3)
            except queue.Empty:
                if not speech_detected:
                    if time.time() - rec_start > self.no_speech_timeout:
                        print()
                        self.get_logger().warn("No speech detected, timeout exit.")
                        break
                continue

            # Resample to 16kHz
            chunk_16k = resample_audio(chunk_hw, self.device_sample_rate, self.sample_rate)

            # —— VAD status detection ——
            vad_icon = Fore.WHITE + '○' + Fore.RESET
            if self._vad_engine:
                try:
                    self._vad_engine.accept_waveform(chunk_16k)
                    if self._vad_engine.is_detected():
                        if not vad_triggered:
                            vad_triggered = True
                        speech_detected = True
                        last_speech_time = time.time()
                        vad_icon = Fore.GREEN + '●' + Fore.RESET
                    self._vad_engine.clear_segments()
                except Exception:
                    pass

            # —— Pre-buffer + ASR recognition ——
            if vad_triggered:
                # VAD triggered: feed pre-buffer audio first, then current chunk
                if pre_buffer:
                    for buf_chunk in pre_buffer:
                        self._streaming_engine.accept_waveform(self.sample_rate, buf_chunk)
                        self._streaming_engine.decode()
                    pre_buffer.clear()
                self._streaming_engine.accept_waveform(self.sample_rate, chunk_16k)
                self._streaming_engine.decode()
            else:
                # VAD not triggered: save to pre-buffer
                pre_buffer.append(chunk_16k)
                if len(pre_buffer) > PRE_BUFFER_SIZE:
                    pre_buffer.pop(0)

            result = self._streaming_engine.get_result()
            result_text = result.text.lower() if self.language == "en" else result.text

            display = self._format_display(f" {vad_icon} ", result_text)
            if display != prev_display:
                # Use ANSI to clear remaining characters at end of line
                print(f"\r{display}\033[K", end='', flush=True)
                prev_display = display

            # Endpoint detected, recording finished
            if result.is_endpoint:
                if result_text.strip():
                    final_text = result_text
                    print(f"\r[\u2713] {final_text}\033[K", flush=True)
                    break
                else:
                    # Empty endpoint: reset and continue waiting
                    self._streaming_engine.reset()
                    prev_display = ''
                    continue

            # Post-speech silence timeout
            if speech_detected and (time.time() - last_speech_time > 5.0):
                result = self._streaming_engine.get_result()
                result_text = result.text.lower() if self.language == "en" else result.text
                final_text = result_text
                print()
                break

        # Ensure newline
        if final_text == '' and prev_display:
            print()

        return final_text

 

3.3 ASR Speech Recognition

_recognition_session is the entry point of one complete recording-and-recognition session. It is executed by the recording thread created after wake-up and is responsible for the complete chain of "capture audio → call recognition → publish result":

  • Audio capture (producer): Use sounddevice.InputStream to open the microphone; the callback _audio_cb converts the int16 audio to float32 and pushes it into the queue audio_q for the recognition loop to consume.

  • Recognition method selection: According to use_oline_asr, call either online recognition _run_online_asr or local streaming recognition _run_streaming; both take audio from the same audio_q.

  • Resource release: In finally, regardless of success or failure, stream_alive[0]=False is set and the audio stream is stop()/close()d, ensuring the microphone is correctly released.

  • Result filtering and publishing: After stripping the recognized text, if its length is less than the threshold ASR_THREASHOLD, it is considered invalid (too short) and discarded directly; otherwise it is published to the asr_result topic through asr_pub, and forwarded to model_service through llm_request_pub with LlmRequest(robot_feedback=False, llm_request=text) for Dify to handle. Here robot_feedback=False indicates that this is a new user instruction, rather than feedback from the robot's action execution.

xxxxxxxxxx
    def _recognition_session(self):
        chunk_size = int(self.device_sample_rate * self.CHUNK_DURATION)
        audio_q: queue.Queue = queue.Queue()
        stream_alive = [True]

        def _audio_cb(indata, frames, time_info, status):
            """sounddevice callback: put int16 chunk into queue"""
            if not stream_alive[0]:
                return
            chunk = indata[:, 0].astype(np.float32) / 32768.0
            audio_q.put(chunk)

        try:
            stream = sd.InputStream(
                samplerate=self.device_sample_rate,
                channels=1,
                dtype=np.int16,
                blocksize=chunk_size,
                callback=_audio_cb)
            stream.start()
        except Exception as exc:
            self.get_logger().error(Fore.RED + f"Audio stream open failed: {exc}"+ Fore.RESET)
            return

        rec_start = time.time()
        try:
            if self.use_oline_asr:
                # Online ASR
                result_text = self._run_online_asr(audio_q, rec_start)
            else:
                # Local streaming ASR
                result_text = self._run_streaming(audio_q, rec_start)
        finally:
            stream_alive[0] = False
            try:
                stream.stop()
                stream.close()
            except Exception:
                pass

        # Recognition result
        result_text = result_text.strip() if result_text else ''
        if len(result_text)< self.ASR_THREASHOLD:
            self.get_logger().info(f"ASR result: {result_text} < ASR_THRESHOLD!")
            return
        self.asr_pub.publish(String(data=result_text))
        # Forward the valid recognition result to model_service for Dify processing
        self.llm_request_pub.publish(LlmRequest(robot_feedback=False, llm_request=result_text))

 

4.Model Service Function

Source code path:

xxxxxxxxxx
~/yahboomcar_ros2_ws/yahboomcar_ws/src/multi_brains/multi_brains/model_service.py

Explanation of the core functions:

  • Check in an independent thread whether there is a request to access the model in the llm_handler_queue queue.

  • Once an element is pushed into the queue, call Dify's access interface chat function with different parameters according to the request type.

  • After obtaining the AI agent's reply from the Dify side, parse the corresponding content to play the voice and send the action list to the action server for execution.

xxxxxxxxxx
    def llm_request_callback(self, msg:LlmRequest):
        '''Topic callback function, receive model request and put into queue'''
        if self.debug_mode: self.get_logger().info(f"robot_feedback:{msg.robot_feedback},llm_request:{msg.llm_request}")
        if msg.robot_feedback:
            if msg.llm_request =="Robot image feedback complete":
                self._enqueue_request([msg.llm_request,'image_request',True])
            elif msg.llm_request =="finish":
                self.clear_request_queue()  
                self.dify_llmclient.reset_conversation()
            else:
                if self.debug_mode: self.get_logger().info("Normal robot feedback")
                self._enqueue_request([msg.llm_request,'text_request',True])
        
        else:# model requests from other sources
            self._enqueue_request([msg.llm_request,'text_request',None])

    def _enqueue_request(self, request:list):
        '''Non-blocking enqueue, drop the request if the queue is busy to avoid blocking the spin thread'''
        try:
            self.llm_handler_queue.put_nowait(request)
        except queue.Full:
            self.get_logger().warn(Fore.YELLOW+f"llm_handler_queue is busy, request dropped: {request[0]}"+Fore.RESET)

    def handle_llm_thread(self)->None:
        '''Handle model request'''
        while True:
            try:
                if self.llm_handler_queue.empty():
                    time.sleep(1.0)#sleep 1 second when there is no request
                    continue

                request_query = self.llm_handler_queue.get()
                if self.debug_mode: self.get_logger().info(f"Processing LLM request: {request_query}")

                if request_query[1]=='text_request':
                    '''text request'''
                    result=self.dify_llmclient.chat(request_query[0],robot_feedback=request_query[2])
                elif request_query[1]=='image_request':
                    '''vision + text request'''
                    result=self.dify_llmclient.chat(request_query[0],image_path=self.image_cache_path,robot_feedback=request_query[2])
                else:
                    self.get_logger().error(Fore.RED+f"Unknown request type: {request_query[1]}"+Fore.RESET)
                    continue

                if result[0]:
                    split_result=self.extract_actions(result[1])
                    if split_result is None: continue
                    action_list,llm_response,decision_plan=split_result
                    if decision_plan is not None: 
                        self.get_logger().info(Fore.YELLOW+f"Decision-making level planning: {decision_plan}"+Fore.RESET)
                    self.get_logger().info(Fore.YELLOW+f'"action": {action_list},"response": {llm_response}'+Fore.RESET) 

                    if self.use_tts:
                        if self.tts_engine.synthesize(llm_response,self.tts_out_path) :
                            self.play_audio(self.tts_out_path)
                        else:
                            self.get_logger().error(Fore.RED+"Speech synthesis failed. Check whether the TTS model is available"+Fore.RESET)
                    if decision_plan is not None: 
                        self.text_pub.publish(String(data=f"Decision-making level planning: {decision_plan}"))
                    self.text_pub.publish(String(data=f'"action": {action_list}, "response": {llm_response}'))

                    if action_list!=[]: self.send_action_service(action_list, llm_response)  
                else:
                    error_msg = f"The dify request failed. Error Log:{result[1]}"
                    self.get_logger().error(Fore.RED+error_msg+Fore.RESET)
                    self.text_pub.publish(String(data=f"[ERROR] {error_msg}"))
            except Exception as e:
                self.get_logger().error(Fore.RED+f"handle_llm_thread exception: {e}\n{traceback.format_exc()}"+Fore.RESET)
                time.sleep(0.5)

 

5. Action Server Function

  • Program source code path

xxxxxxxxxx
~/yahboomcar_ros2_ws/yahboomcar_ws/src/multi_brains/multi_brains/action_service.py

 

  • It contains the implementation of all the basic action functions that the robot can execute: receiving an action list request and executing the corresponding actions. It has the function of being interrupted upon being woken up again.

Parsing of the core callback function execute_callback:

execute_callback is the execution callback of the /action_service (Rot action) server, responsible for receiving the action string list issued by model_service and parsing, reflectively invoking, and summarizing the feedback one by one:

  • State marking: On entry, set action_runing=True, action_state="RUNNING", and clear this round's result records action_results.

  • Execute one by one: Iterate over actions; before executing each action, first check whether interrupt_event is set (interrupted by wake-up); if so, break out and no longer execute the subsequent actions.

  • Parsing and reflective invocation: Use _parse_action to parse the action string into "function name + argument list". If parsing fails, record it as FAILED and skip; then reflectively call the corresponding action function through getattr(self, action_name)(*args) (i.e., an action registered by @llm_action).

  • Unified return protocol: Compatible with three kinds of return values — None (no-op/self-feedback actions, such as seewhat, stop_feedback, finish_task), [bool, str] (success or not + result message), True (success). Based on this, accumulate success_actions/failed_actions and the details.

  • Result feedback: After the loop ends, if interrupted, set the state to INTERRUPTED; otherwise summarize the success/failure as a Robot feedback: text, publish it to the llm_request_handler topic through _publish_feedback, and feed it back to model_service, which finally feeds it back to the Dify agent. Finally, reset action_runing, clear interrupt_event, and call goal_handle.succeed().

  • stop(): Publishes an all-zero Twist to cmd_vel, making the chassis stop immediately.

  • _execute_action(twist, durationtime): Continuously publishes Twist at about 10Hz within the durationtime duration to achieve timed motion; as soon as the loop detects that interrupt_event is set, it immediately breaks and stops publishing, realizing interruptible chassis motion.

xxxxxxxxxx
    def execute_callback(self, goal_handle):
        actions = goal_handle.request.actions
        if self.debug_mode: self.get_logger().info(f"Entered action execution callback, preparing to execute action list: {actions}")
        self.action_runing = True
        self.action_state = "RUNNING"
        self.action_results = []
        success_actions = []
        failed_actions = []
        failed_detail = ""
        success_details = []
        for action in actions:
            if self.interrupt_event.is_set():
                break
            failed_detail = ""
            action_name, args, parse_error = self._parse_action(action)
            if action_name is None:
                self.get_logger().error(Fore.RED+f"action_service: {parse_error}, skip execution"+Fore.RESET)
                failed_actions.append(action)
                failed_detail = parse_error
                self._record_result(action, "FAILED", parse_error)
                continue
            try:
                method = getattr(self, action_name)
                action_result = method(*args)
            except Exception as e:
                self.get_logger().error(Fore.RED+f"action_service: {action} raised exception: {e}"+Fore.RESET)
                failed_actions.append(action)
                failed_detail = str(e)
                self._record_result(action, "FAILED", str(e))
                continue
            if action_result is None:
                #None means no-op or self-feedback action
                self._record_result(action, "SELF_FEEDBACK")
            elif isinstance(action_result, (list, tuple)) and len(action_result) == 2:
                #[bool, str] protocol with result message
                detail = str(action_result[1])
                if action_result[0]:
                    success_actions.append(action)
                    if detail:
                        success_details.append(detail)
                    self._record_result(action, "SUCCEEDED", detail)
                else:
                    failed_actions.append(action)
                    failed_detail = detail
                    self._record_result(action, "FAILED", detail)
            elif action_result is True:
                success_actions.append(action)
                self._record_result(action, "SUCCEEDED")
            else:
                failed_actions.append(action)
                self._record_result(action, "FAILED")

        if self.interrupt_event.is_set():
            self.action_state = "INTERRUPTED"
        else:#to dify-agent
            feedback = ""
            if failed_actions:
                feedback = f"Robot feedback: Failed to execute {failed_actions}"
                if failed_detail:
                    feedback += f", error: {failed_detail}"
            elif success_actions:
                feedback = f"Robot feedback: Execution {success_actions} completed"
                if success_details:
                    feedback += f", result: {'; '.join(success_details)}"

            if feedback:
                self._publish_feedback(feedback)
            else:
                if self.debug_mode: self.get_logger().info("No operation, no feedback.")

        if self.debug_mode: self.get_logger().info("The exit action executes the callback function.")
        self.action_runing = False
        if self.action_state == "RUNNING":
            self.action_state = "FAILED" if failed_actions else "SUCCEEDED"
        self.interrupt_event.clear()
        goal_handle.succeed()
        result = Rot.Result()
        result.success = True
        return result


    def stop(self): 
        twist = Twist()
        twist.linear.x = 0.0
        twist.linear.y = 0.0
        twist.angular.z = 0.0
        self.cmd_vel_pub.publish(twist)


    def _execute_action(self, twist, durationtime:float=3.0):
        '''Internal function to execute action '''
        start_time = time.time()
        while (time.time() - start_time) < durationtime:
            if self.interrupt_event.is_set():
                break
            self.cmd_vel_pub.publish(twist)
            time.sleep(0.1)

 

6.Interruption Function

The robot supports being interrupted by being woken up again at any stage of recording, dialogue, or action. The entire interruption mechanism is accomplished through the collaboration of three nodes:

  • asr_detect.py (speech recognition node VoiceProcessor): Detects hardware/keyboard wake-up, publishes the wakeup_signal topic, manages the interruption and restart of the recording thread, and plays the wake-up response audio.

  • model_service.py (model service node Ros_LLM_Service): Calls Dify and TTS to synthesize speech, and plays the reply speech and issues the action list through the play_audio action service.

  • action_service.py (action service node ActionController): Subscribes to wakeup_signal, and interrupts the action being executed through interrupt_event and performs an emergency stop.

The interruption signal uniformly comes from "wake-up": every time asr_detect wakes up, it publishes wakeup_signal (Bool=True) for action_service to trigger the action interruption; the interruption of the recording thread itself is accomplished by _stop_rec_event inside asr_detect.

6.1 Interruption During the Recording Stage

If you find that you misspoke during recording, or you are not satisfied with what you said and need to re-record, you can directly wake up again during the recording process to interrupt the previous recording and start recording again.

  • The logic is implemented in the run() main loop of the VoiceProcessor class in asr_detect.py:

  • Each time a wake-up is detected (hardware _hw_wakeup_event or keyboard _kb_wakeup_event):

    1. Clear the wake-up event and publish wakeup_signal (Bool=True) to notify other nodes;

    2. If the wake-up response audio is enabled, call wakeup_voice() to respond to the user;

    3. If there is already a running recording thread _current_rec_thread, set _stop_rec_event to notify it to exit, join(timeout=3.0) to wait for it to end, and then clear the event;

    4. Reset the state of the streaming recognition engine and the VAD engine;

    5. Start a new _recognition_session recording thread and begin a new round of recording.

  • The main loop conditions of the recording recognition loops (_run_streaming / _run_online_asr) are both while not self._stop_rec_event.is_set(), so once _stop_rec_event is set, the current recognition ends immediately, realizing interruption during the recording stage.

xxxxxxxxxx
    def run(self):
        while rclpy.ok():
            hw_woke = self._hw_wakeup_event.wait(timeout=0.05) # voice wakeup
            kb_woke = self._kb_wakeup_event.is_set() # Keyboard wakeup
            if hw_woke or kb_woke:
                self._hw_wakeup_event.clear()
                self._kb_wakeup_event.clear()
                self.wakeup_pub.publish(Bool(data=True))
                if self.enable_wakeup_voice:
                    self.wakeup_voice()  # play wakeup audio before recording

                if (self._current_rec_thread is not None and self._current_rec_thread.is_alive()):
                    self._stop_rec_event.set()
                    self._current_rec_thread.join(timeout=3.0)
                    self._stop_rec_event.clear()

                # Reset engine state
                if self._streaming_engine:
                    self._streaming_engine.reset()
                if self._vad_engine:
                    self._vad_engine.reset()
                    self._vad_engine.clear_segments()

                # Start new recognition session thread
                self._current_rec_thread = threading.Thread(
                    target=self._recognition_session, daemon=True)
                self._current_rec_thread.start()
            time.sleep(0.02)

 

6.2 Interruption During the Dialogue Stage

If, while the robot is speaking (playing the reply speech), you are not satisfied with the reply or do not want it to continue, you can use the wake word to directly interrupt the robot's speech and start recording; at this point you can give the robot a new instruction (still within the current task cycle), or say "end the current task" to end the current task and start a new task cycle.

  • The robot's reply speech is played by play_audio() in model_service.py: as an action client, it sends the audio file path to the common action server through the play_audio (PlayAudio) action (the common_service node, which plays it internally using pygame), and blocks with done_event.wait() waiting for playback to finish.

  • The run() of asr_detect.py publishes wakeup_signal on every wake-up and calls wakeup_voice() — it also acts as a client, sending a new playback goal (the wake-up response audio) to the play_audio action service. When the play_audio server detects cancellation/preemption in the playback thread (goal_handle.is_cancel_requested or cancel_play_audio), it calls pygame.mixer.music.stop() to stop the current playback, thereby interrupting the reply speech being played.

  • After interruption, a new recording session starts immediately, and the user can continue to issue new instructions.

  • If the user wants to end the current task cycle: when the action layer executes finish_task(), it publishes feedback with the content finish; after model_service's llm_request_callback receives it, it clears the request queue and calls reset_conversation() to reset the Dify session context.

model_service.py plays the reply speech (sends the play_audio action goal and blocks waiting for playback to finish):

xxxxxxxxxx
    def play_audio(self,file_path: str) -> None:
        '''Play audio through the play_audio action service and wait until playback finishes'''
        if not self.audio_client.wait_for_server(timeout_sec=3.0):
            self.get_logger().warn(Fore.YELLOW+"play_audio action server not available, skip audio playback"+Fore.RESET)
            return
        goal = PlayAudio.Goal()
        goal.file_path = file_path
        done_event = threading.Event()

        def _result_callback(future):
            result = future.result().result
            if not result.success:
                self.get_logger().warn(f"play_audio finished abnormally: {result.message}")
            done_event.set()

        def _goal_response_callback(future):
            goal_handle = future.result()
            if not goal_handle.accepted:
                self.get_logger().warn("play_audio goal rejected by action server")
                done_event.set()
                return
            goal_handle.get_result_async().add_done_callback(_result_callback)

        send_future = self.audio_client.send_goal_async(goal)
        send_future.add_done_callback(_goal_response_callback)
        done_event.wait()  #Block until audio playback finishes

asr_detect.py plays the wake-up response audio (sends a new goal to the same play_audio action service, thereby interrupting the previous speech):

xxxxxxxxxx
    def wakeup_voice(self):
        """Play a random wakeup audio and block until playback finishes."""
        if not self.wakeup_audio_list:
            return
        audio_file = random.choice(self.wakeup_audio_list)

        if not self.audio_client.wait_for_server(timeout_sec=1.0):
            self.get_logger().warn("play_audio action server not available, skip wakeup voice")
            return

        goal = PlayAudio.Goal()
        goal.file_path = audio_file

        done_event = threading.Event()
        def _result_cb(future):
            done_event.set()

        def _goal_response_cb(future):
            goal_handle = future.result()
            if not goal_handle.accepted:
                self.get_logger().warn("play_audio goal rejected")
                done_event.set()
                return
            result_future = goal_handle.get_result_async()
            result_future.add_done_callback(_result_cb)

        future = self.audio_client.send_goal_async(goal)
        future.add_done_callback(_goal_response_cb)

        done_event.wait(timeout=10.0)

 

6.3 Interruption During the Action Stage

If the robot is woken up while executing an action, it will stop the current action and restore the initial pose. Specifically, it can be divided into ordinary action interruption and action interruption with subprocesses.

The unified entry point for all action interruptions is wakeup_callback in action_service.py: it subscribes to the wakeup_signal topic, and when it receives a wake-up signal while an action is running (action_runing is true), it sets interrupt_event and calls _emergency_stop() to perform an immediate emergency stop.

xxxxxxxxxx
    def wakeup_callback(self, msg:Bool):
        ''' Voice wake-up callback function '''
        self.get_logger().info("wakeup received")
        if msg.data:
            if self.action_runing:
                self.interrupt_event.set()
                self._emergency_stop()

    def _emergency_stop(self):
        '''Immediately stop all physical motion when wakeup is received'''
        # 1. Stop chassis
        self.stop()
        # 2. Cancel navigation goal
        if self.nav_goal_handle is not None:
            try:
                self.nav_goal_handle.cancel_goal_async()
            except Exception:
                pass
        # 3. Cancel target tracking
        if self.cancel_track_client.service_is_ready():
            try:
                self.cancel_track_client.call_async(CancelTrack.Request())
            except Exception:
                pass
        # 4. Stop visual track service
        if self.visual_track_client.service_is_ready():
            try:
                self.visual_track_client.call_async(SetBool.Request(data=False))
            except Exception:
                pass
        # 5. Kill subprocesses
        self._stop_aruco_detect()
        self._stop_color_detect()
        self._stop_follow_line_clear()
        self.get_logger().info("Emergency stop executed")

_emergency_stop() stops the chassis, cancels the navigation goal, cancels target tracking, shuts down the visual tracking service, and ends all detection subprocesses at once; it is the fastest emergency stop method. The interrupt_event flag, on the other hand, is checked by each action function in its waiting loop to achieve graceful exit and state recovery.

6.3.1 Ordinary Action Interruption

Ordinary actions refer to chassis motion, robotic arm motion, navigation, visual alignment, and other motions controlled via topics/actions/services. They achieve interruption by checking interrupt_event in their waiting loops:

  • Timed chassis motion: _execute_action checks interrupt_event in the loop that continuously publishes cmd_vel; if it is set, it immediately breaks and stops publishing.

  • Chassis odometry motion / navigation / visual alignment: When _move_by_odom, navigation, _precise_adjust, _adjust_grasp_pose, etc. detect an interruption in the loop waiting for the action result, they call goal_handle.cancel_goal_async() to cancel the goal, stop() to stop the car, and return failure.

  • Robotic arm action group: _group_shake / _group_nod / _group_applaud / _group_dance check interrupt_event between each step; when interrupted, they publish the initial pose init_joints to retract the robotic arm and then return.

  • Action scheduling layer: execute_callback also checks interrupt_event before executing each action; if it is set, it no longer executes the subsequent actions and marks the state as INTERRUPTED.

The interruptible implementation of timed chassis motion (stops publishing velocity immediately when an interruption is detected):

xxxxxxxxxx
    def _execute_action(self, twist, durationtime:float=3.0):
        '''Internal function to execute action '''
        start_time = time.time()
        while (time.time() - start_time) < durationtime:
            if self.interrupt_event.is_set():
                break
            self.cmd_vel_pub.publish(twist)
            time.sleep(0.1)

Taking the odometry motion _move_by_odom as an example, when an interruption is detected while waiting for the action result, it cancels the goal and stops the car:

xxxxxxxxxx
        result_future = goal_handle.get_result_async()
        while rclpy.ok() and not result_future.done():
            if self.interrupt_event.is_set():
                try:
                    goal_handle.cancel_goal_async()
                except Exception:
                    pass
                self.stop()
                return [False, "MoveByOdom interrupted by wakeup"]
            time.sleep(0.2)

The robotic arm action group detects interruption between each step and retracts the robotic arm (taking _group_shake as an example):

xxxxxxxxxx
    def _group_shake(self):
        for _ in range(3):
            if self.interrupt_event.is_set():
                self.pubSix_Arm(self.init_joints)
                return None
            if not self.pubSix_Arm([140, 130, 0, 5, 90, 0]):
                return [False, "Failed to publish arm joints during shake"]
            time.sleep(1.0)
            if not self.pubSix_Arm([40, 130, 0, 5, 90, 0]):
                return [False, "Failed to publish arm joints during shake"]
            time.sleep(1.0)
        if not self.pubSix_Arm(self.init_joints):
            return [False, "Failed to publish arm joints during shake"]
        return [True, ""]

Note

Unlike the older version: the robotic arm publishing function pubSix_Arm itself no longer checks the interruption flag; the interruption determination and initial pose recovery are handled uniformly in the respective action group loops.

6.3.2 Action Interruption with Subprocesses

Actions such as AprilTag marker grasping/removal (GraspArUco / ArUcoRemoveHigh), color block grasping (GraspColor), and line-following obstacle clearing (follow_line_clear) need to start external detection/control programs (such as aruco_detect, color_detect, and follow_line_clear of common_demo) in an independent subprocess. Here, GraspArUco is used as an example for explanation:

  • _start_aruco_detect() uses subprocess.Popen to start the detection subprocess, and then _wait_for_aruco_target() polls for the target in a waiting loop; this loop checks interrupt_event.is_set(), and if interrupted, returns None, and the action then fails and exits.

  • When an interruption occurs, _emergency_stop() calls _stop_aruco_detect() / _stop_color_detect() / _stop_follow_line_clear(), which use kill_process_tree(pid) to recursively end the entire subprocess tree, avoiding leftover processes.

  • In addition, the action function uses try/finally to ensure that, whether it ends normally or is interrupted, it calls the corresponding _stop_* in finally to clean up the subprocess.

xxxxxxxxxx
    @llm_action
    def GraspArUco(self, target_id) -> list[bool, str]:
        try:
            target_id = int(str(target_id).strip().strip("'\""))
        except (TypeError, ValueError):
            return [False, f"GraspArUco failed: invalid target_id '{target_id}', must be an integer"]

        self.pubSix_Arm(self.aruco_detect_pose)
        time.sleep(2.0)
        try:
            #step 1: launch the standalone detect process targeting the requested ID
            self._start_aruco_detect(target_id=target_id)
            if self._wait_for_aruco_target(target_id, timeout_sec=15.0) is None:
                return [False, f"AprilTag with ID {target_id} not detected in current scene"]
            # ...step 2~4: align to target / compute grasp point / execute grasp (unrelated to interruption, omitted here)...
            return [True, f"AprilTag {target_id} grasped successfully"]
        finally:
            self._stop_aruco_detect()

Checking the interruption flag in the loop waiting for the detection target (_wait_for_aruco_target), and recursively ending the subprocess tree (_stop_aruco_detect):

    def _stop_aruco_detect(self):
        """Terminate the standalone aruco_detect process if still running."""
        if self._aruco_process is not None:
            if self._aruco_process.poll() is None:
                kill_process_tree(self._aruco_process.pid)
            self._aruco_process = None

    def _wait_for_aruco_target(self, target_id, timeout_sec=15.0):
        """Wait until the detect process publishes /aruco_target for target_id."""
        deadline = time.time() + timeout_sec
        while rclpy.ok() and time.time() < deadline:
            if self.interrupt_event.is_set():
                return None
            with self._aruco_target_lock:
                msg = self._aruco_target_cache.get(int(target_id))
            if msg is not None:
                return msg
            time.sleep(0.2)
        return None
Embodied Intelligent Robot System Architecture
Register a Model Service Provider Account

COMPANY

  • About us
  • Team Style
  • Blog

LEARNING & EXPLORATION

  • Tutorials
  • Edge AI hardware
  • Educational robots
  • Embedded intelligent hardware

WHERE TO BUY

  • Yahboom Online Store
  • Amazon
  • Alibaba
  • AliExpress
  • Mercado

TECHNOLOGY SUPPORT

  • Email: support@yahboom.com

BE A DISTRIBUTOR

  • Email: sales@yahboom.com
QR Code

Copyright © 2017-2026 Yahboom. All Rights Reserved.

EN/中文