打造具备“灵魂”的智能小车:基于树莓派与Qwen3的工程化实践
报告日期:2025年6月17日
项目目标:打造具备“灵魂”的智能小车
本项目的核心目标是利用工程化的开发方法,构建一台具备高级智能交互能力的智能小车。它不仅仅是一个遥控或简单避障的机器人,更追求一种“灵魂感”,使其能够与用户进行自然、富有情感的多模态交互,并具备一定的自主性和个性化特征。
核心功能需求概述:
- 多模态交互能力: 实现小车能听懂语音指令、能用自然语言回应、能通过摄像头理解环境和识别特定人脸。
- 智能化核心: 构建长期记忆系统,记录交互历史与关键信息;塑造可定制的个性化引擎,使小车行为和回应更具“性格”。
- 环境感知与主动性: 监测天气、温湿度、自身电池电量等状态,基于这些信息调整“心情”并通过语音主动提醒用户。
- 高级视觉功能: 准确识别人脸,并能主动跟随指定目标。
- 高度自主性: 在电量不足时能自动寻找并返回充电基座进行充电。
- 技术实现路径: 采用工程化的开发方法,以树莓派作为主控制器,核心智能依赖Qwen3等多模态大模型API的调用,并评估K230机器视觉开发板在特定任务上的应用潜力。
一、整体架构设计方案
构建一个具备“灵魂”的智能小车,其整体架构设计至关重要。这不仅涉及到硬件组件的协同工作,更关乎软件层面的逻辑流程、数据处理以及与云端智能的交互。本方案旨在提供一个清晰、模块化且可扩展的架构蓝图。
1.1 系统逻辑架构图 (示意)
+--------------------------+ +---------------------------------+ +--------------------------+
| 传感器输入与感知层 |<---->| 树莓派 (核心控制与决策) |<---->| Qwen3等多模态云脑 |
| (摄像头, 麦克风阵列, | | (Python主程序, 状态机, 任务调度,| | (语音识别, 语义理解, |
| 环境传感器[温湿度/光照],| | 记忆/个性模块, K230通信接口, | | 图像分析, 文本生成, |
| 电源管理[INA219], | | 运动控制算法, 回充导航逻辑) | | 语音合成, 思考模式) |
| 充电坞对接传感器) | +----------------+----------------+ +--------------------------+
+--------------------------+ |
| (可选: 视觉协处理)
|
+-------------+--------------+
| K230 机器视觉开发板 |
| (人脸检测/识别加速, |
| 特定目标跟踪算法) |
+-------------+--------------+
|
| (控制/状态信号)
|
+--------------------------+ +----------------+----------------+ +--------------------------+
| 执行器与输出表现层 |<---->| 运动与电源子系统 (小车底盘) |<---->| 充电坞对接模块 |
| (扬声器, [可选]表情显示, | | (电机及驱动器, 电池, | | (导航信标[IR/AprilTag], |
| 车身灯光反馈) | | 电源管理与充电控制电路) | | 物理充电接口[Pogo Pin]) |
+--------------------------+ +---------------------------------+ +--------------------------+
该架构图展示了系统的主要组成部分及其相互关系。传感器层负责收集环境和自身状态信息;树莓派作为核心控制器,运行主程序,处理本地任务,并与云端AI和可选的K230协处理器通信;Qwen3等云端大模型提供高级智能处理能力;执行器层负责将决策转化为物理动作和交互反馈;运动与电源子系统提供动力和能源管理;充电坞模块则支持小车的自主充电功能。
1.2 关键设计考量点
在设计整个系统时,需要权衡多个关键因素,这些因素将直接影响后续的硬件选型和软件实现策略。深入理解并妥善处理这些考量点,是项目成功的基石。
模块化与解耦
软件层面:为了应对系统的复杂性并提高可维护性,软件设计应遵循模块化原则。将感知(如音频处理、视觉处理)、决策(如指令解析、行为选择)、执行(如运动控制)、记忆管理、个性化引擎以及与外部API(如Qwen3、K230)的交互等功能划分为独立的Python模块或类。例如,可以定义AudioProcessor
, VisionProcessor
, MotionController
, MemoryManager
, PersonalityEngine
, QwenAPIHandler
等。这样做的好处是降低了模块间的耦合度,使得每个模块可以独立开发、测试和升级。当某个模块需要修改或替换时,对系统其他部分的影响可以降到最低。
硬件层面:传感器和执行器的接口应尽可能标准化。例如,优先选用具有通用接口(如I2C, SPI, USB)的传感器,避免使用过于专有或难以替代的硬件。这为未来升级或替换硬件组件提供了便利,例如,如果发现当前摄像头性能不足,可以更容易地替换为更高规格的型号,而无需大规模修改软件接口。
实时性与响应速度
语音交互延迟:用户体验在很大程度上取决于语音交互的流畅性。从用户发出语音指令到小车做出响应的整个链路延迟需要严格控制。一个可接受的目标是:对于简单指令(如“前进”),响应时间应小于1秒;对于需要云端大模型参与的复杂对话,响应时间争取控制在3秒以内。主要的延迟瓶颈在于语音识别(STT)API的调用、网络传输以及大语言模型(LLM)的推理时间。优化策略可以包括使用高效的STT服务、保证稳定的网络连接、选择响应速度较快的LLM模型或API节点,以及采用流式TTS输出以减少感知延迟。
视觉跟踪实时性:人脸识别与跟随功能对实时性要求极高,需要保证足够的处理帧率(如10-15 FPS以上)和较低的单帧处理延迟。这直接影响跟随的平滑度和准确性。需要仔细评估树莓派自身的CPU处理能力。如果仅靠树莓派难以满足要求,引入K230这样的视觉协处理器就显得非常必要,它可以分担计算密集型的视觉算法,如人脸检测和跟踪。
并发处理:智能小车在运行过程中需要同时处理多种任务,例如,在与用户进行语音交互的同时,可能还需要感知周围环境以避障,或者在执行跟随任务时监测电池电量。因此,系统必须具备并发处理能力。在Python中,可以利用asyncio
库进行异步编程,或者使用多线程(threading
模块)来处理并发任务,确保各项功能能够并行执行而不会相互阻塞,从而提高系统的整体响应效率。
云端API依赖与边缘计算平衡
必须依赖云端:对于本项目中追求的“灵魂感”,许多核心智能功能目前难以完全在边缘设备上实现。这包括:
- 复杂的自然语言理解(NLU)和多轮对话管理。
- 基于大规模知识库的问答。
- 高质量、自然流畅的语音合成(TTS)。
- 基于大模型的图像内容理解,例如Qwen3的视觉问答(VQA)能力,能够理解图片内容并回答相关问题。
这些任务通常需要强大的计算资源和庞大的模型参数,目前最经济有效的方式是调用云端API。
可边缘/本地处理:为了提高响应速度、降低网络依赖和成本,部分任务可以在边缘设备(树莓派或K230)上处理:
- 基础的人脸检测(而非复杂的人脸识别比对)。
- 简单的物体跟踪算法(如基于颜色、形状或运动的跟踪)。
- 环境传感器数据的直接读取和初步处理。
- 电机的底层控制逻辑和PID算法。
- 充电坞对接的末端引导,如基于红外传感器的精确对准或简单的视觉标记识别。
K230视觉协处理器在这些边缘视觉任务中能发挥重要作用,提供硬件加速。
离线策略:考虑到网络连接可能不稳定或不可用,系统应设计一定的离线运行策略。例如,在网络断开时,小车应能执行预设的本地指令(如简单的语音关键词控制移动)、基本的避障行为,或者进入低功耗待机模式,并尝试重新连接网络。这能提升用户体验和系统的鲁棒性。
数据流与状态管理
核心数据流:必须清晰地定义系统中数据的流动路径。传感器数据(如摄像头图像、麦克风音频、环境读数)如何被采集、进行必要的预处理(如降噪、格式转换),然后如何传输给决策模块(本地运行在树莓派上的逻辑,或发送给云端Qwen3 API)。同样,决策模块产生的指令(如运动指令、语音回复文本)如何驱动相应的执行器(电机、扬声器)也需要明确。数据格式、传输协议(如内部模块间调用、与K230的TCP/IP通信、与云API的HTTPS请求)都需要规范化。
小车状态机:为了有效管理小车的复杂行为和模式切换,应设计一个明确的状态机。状态可以包括:空闲(Idle)、聆听(Listening)、思考(Thinking/Processing)、回应(Responding)、跟随(Following)、导航至充电坞(NavigatingToCharger)、充电中(Charging)、异常(Error)等。每个状态下允许的行为和可转换到的其他状态,以及触发状态转换的条件(如用户指令、传感器事件、内部逻辑判断)都需要详细定义。状态机有助于使小车行为逻辑清晰、可预测且易于调试。
功耗管理
对于依赖电池供电的移动机器人而言,功耗管理是决定其实用性和续航能力的关键。需要系统性地分析各个硬件组件(树莓派主板、K230、传感器、电机、显示屏等)的功耗特性。在软件层面,应优化算法以减少不必要的计算;选择低功耗的硬件组件;并设计有效的休眠和唤醒策略。例如,在小车长时间处于空闲状态时,可以降低CPU频率、关闭部分非必要的传感器或外设,甚至让K230进入低功耗模式,以延长电池使用时间。
安全性与鲁棒性
系统的安全性和鲁棒性直接关系到用户体验和设备的可靠运行。需要考虑的方面包括:
- API调用失败处理:网络请求可能因各种原因失败(超时、服务器错误、鉴权失败等)。代码中必须包含对API调用的错误捕获和重试机制,以及在多次失败后的降级处理逻辑(如切换到本地功能或提示用户网络问题)。
- 传感器异常检测:传感器可能损坏或读数异常。系统应能检测到这些异常情况(如读取超时、数值超出合理范围),并采取相应措施(如忽略该传感器数据、报警提示、切换到备用逻辑)。
- 运动控制中的安全保护:在小车移动过程中,应有基本的碰撞检测机制(如通过超声波传感器或视觉判断)。在检测到障碍物或潜在碰撞风险时,应能及时停止或调整路径。电机控制也应有防堵转、过流保护等措施。
可扩展性与可维护性
考虑到技术的发展和未来可能的需求变更,系统设计应具备良好的可扩展性。这包括在软件层面预留接口和钩子(hooks),方便未来添加新的传感器、执行器或AI功能模块。例如,可以设计一个插件式的传感器管理系统,或者一个通用的任务调度框架。
同时,清晰的代码结构、详尽的注释和完善的文档对于项目的长期可维护性至关重要。遵循良好的编程规范,使用版本控制系统(如Git),并定期进行代码审查,都有助于提高代码质量和团队协作效率。
二、硬件选型清单与配置指南
为实现智能小车的各项功能,硬件选型是至关重要的一步。本章节将详细介绍各核心组件的推荐型号、选型理由以及基础的配置方法。
2.1 主控制器 (树莓派)
树莓派作为小车的大脑,负责运行主控制程序、处理传感器数据、与云端API通信以及协调各个硬件模块的工作。
型号推荐与核心考量
推荐选用 Raspberry Pi 5 (4GB或8GB RAM)
或 Raspberry Pi 4B (8GB RAM)
。
- Raspberry Pi 5 理由:
- 更强的CPU性能: 对于运行Python主程序、多线程/异步任务、可能的本地轻量级AI模型预处理或后处理、以及未来部署更复杂边缘AI应用更为有利。
- PCIe接口: 提供了外接AI加速卡(如Google Coral M.2 Accelerator)的可能性,为未来性能升级预留了空间。参考资料中提及 Raspberry Pi 5在计算机视觉方面的提升,其PCIe接口可用于AI加速。
- 双CSI接口: 如果项目未来需要双目视觉(如深度感知)或连接多个摄像头,Pi 5提供了原生支持。
- Raspberry Pi 4B (8GB RAM) 理由:
- 性价比高: 对于主要依赖云端API进行智能处理的项目,Pi 4B的性能已足够应对本地的控制和数据流转任务。
- 社区支持成熟: 拥有庞大的用户群体和丰富的教程资源,遇到问题更容易找到解决方案。
- 功耗相对较低: 在同等负载下,可能比Pi 5略低,对电池续航有一定益处。
选型核心考量因素:
- 处理能力 (CPU/RAM): 需满足运行Python主程序、并发任务(如传感器数据处理、API通信、运动控制)、状态机管理等需求。8GB RAM能提供更充裕的内存空间,尤其是在处理图像数据或运行多个后台服务时。
- 接口丰富度: 需要足够的USB接口(连接麦克风、扬声器、[可选]其他外设如K230的USB转串口)、CSI接口(连接摄像头)、GPIO引脚(控制电机驱动、读取部分传感器)、以及I2C/SPI总线(连接多种传感器模块、电源管理模块)。
- 软件生态与社区支持: 树莓派拥有极其活跃的社区和完善的软件生态,包括大量的Python库、操作系统支持和问题解决方案,这极大降低了开发门槛。
- 功耗: 需结合选用的电池容量和期望的续航时间进行综合考虑。
操作系统与基础配置
操作系统推荐: Raspberry Pi OS (64-bit, Bookworm推荐)
。
- 理由: 官方操作系统,对硬件的兼容性和支持最佳。64位系统能够更好地利用4GB及以上的内存,对于性能要求较高的应用更为合适。Bookworm是较新的稳定版本,通常包含最新的内核和软件包。
基础配置步骤:
- 烧录系统: 使用 Raspberry Pi Imager 将最新版的Raspberry Pi OS (64-bit)烧录到一张高质量的MicroSD卡(至少32GB,速度等级Class 10/U1 A1或更高)。
- 首次启动配置: 连接显示器、键盘鼠标,完成首次启动向导,包括设置用户名、密码、连接Wi-Fi网络、选择时区和键盘布局。
- 启用接口: 打开终端,运行
sudo raspi-config
。在菜单中:Interface Options
-> 启用SSH
(用于远程命令行访问)。Interface Options
-> 启用VNC
(可选,用于图形界面远程访问)。Interface Options
-> 启用I2C
,SPI
,Serial Port
(根据所选传感器和模块需求启用)。Interface Options
-> 启用Camera
(连接摄像头后启用)。
- 系统更新与升级: 执行以下命令以确保系统和软件包是最新版本:
sudo apt update && sudo apt full-upgrade -y
- 安装核心开发工具: 安装Python包管理器、虚拟环境工具、Git以及一些编译和音频处理所需的库:
(sudo apt install -y python3-pip python3-venv git build-essential libasound2-dev libportaudio2 portaudio19-dev libatlas-base-dev libopenjp2-7 libtiff5
libasound2-dev
,portaudio19-dev
用于音频处理库如PyAudio;libatlas-base-dev
可能是一些科学计算库的依赖;libopenjp2-7
,libtiff5
是OpenCV的常见依赖)。 - 创建Python虚拟环境: 为了项目依赖隔离,强烈建议使用虚拟环境:
之后所有Python包的安装 (python3 -m venv ~/smartcar_env source ~/smartcar_env/bin/activate
pip install ...
) 都在激活虚拟环境后进行。
2.2 多模态大模型API (Qwen3等)
Qwen3系列大模型是实现小车“灵魂”的关键,其强大的多模态理解和生成能力将赋予小车听、说、看、思的核心智能。
API服务商与认证
API服务商:
- 阿里云百炼 (Model Studio): 提供了通义千问系列模型的API服务,包括Qwen-VL(视觉语言模型)、Qwen-Audio(音频理解与生成)、以及最新的Qwen3系列。文档和SDK支持相对完善,适合国内开发者。参考资料中提及 通义千问API参考。
- NVIDIA NIM (NVIDIA Inference Microservices): 为企业级部署提供了Qwen3等模型的微服务。如果考虑本地化部署或对性能有极致要求,可以关注NIM方案,但通常其部署和使用门槛较高。
- 其他可能提供Qwen3系列模型API的第三方云服务商。
选择考量: API的稳定性、调用成本(QPS限制、计费方式)、文档和SDK的完善程度、多模态支持的具体细节(如支持的音频输入格式、图像分辨率限制、响应延迟等)以及是否提供OpenAI兼容的API接口(便于使用现有生态工具如LangChain)。
API Endpoint URL (以阿里云百炼OpenAI兼容模式为例):
https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions
也可能使用DashScope的原生API Endpoint,具体需查阅 阿里云官方文档。
认证方式: 通常使用 API Key
。例如,阿里云百炼使用 DASHSCOPE_API_KEY
。此密钥非常重要,应通过环境变量或安全的配置文件进行管理,避免硬编码到代码中。
关键API调用参数 (基于Qwen3多模态能力)
以下参数主要参考阿里云百炼提供的Qwen API文档,特别是其OpenAI兼容模式。具体参数和取值可能随API版本更新而变化,务必以最新官方文档为准。
- 模型ID (
model
):- 文本与视觉理解: 如
qwen-vl-plus
,qwen-vl-max
(支持图像输入和文本输出)。 - 通用对话与文本生成: 如
qwen-plus
,qwen-max
,qwen-turbo
(纯文本输入输出)。 - Qwen3系列特定模型:如果API支持直接指定,例如
qwen3-8b-instruct
或其他Qwen3变体。 - Qwen-Omni:这是一个强大的多模态模型,能够处理文本、图像、音频输入,并生成文本、音频输出。如果需要小车直接生成语音回复而不仅仅是文本,Qwen-Omni可能是理想选择。根据 阿里云文档,Qwen-Omni支持多种输入类型如 "text", "image_url", "input_audio"。
- 文本与视觉理解: 如
- 输入消息 (
messages
) 格式 (OpenAI兼容模式):[ {"role": "system", "content": "你是一个富有情感和个性的智能小车助手。你的名字叫小Q。你当前的心情是[愉快]。"}, {"role": "user", "content": [ {"type": "text", "text": "小Q,现在外面天气怎么样?我的电量还剩多少呀?"}, // 如果有图像输入 (例如,用户展示了一张图片问这是什么) // {"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,/9j/4AAQSkZJRgABA..."}}, // Base64编码的图片 // {"type": "image_url", "image_url": {"url": "https://example.com/image.jpg"}}, // 图片URL // 如果有音频输入 (例如,用户播放了一段环境声音问是什么) - 需查阅Qwen API是否直接支持在messages中嵌入音频, // 或是否需要通过专门的音频处理API先转为文本或特征。 // Qwen-Omni模型可能支持 "input_audio" 类型。 ]} ]
- Qwen3特定参数 (可能在
extra_body
或特定SDK参数中,参考 阿里云文档):enable_thinking
:True
或False
。设置为True
时,Qwen3模型会在生成回复前进行内部的“思考”步骤,这对于处理复杂问题、进行推理或生成更具深度的内容有帮助,但可能会增加响应时间。对于需要快速响应的简单交互,可以设为False
。开源版Qwen3默认可能为True,商业版默认为False,需注意。stream
:True
或False
。设置为True
时,API会以流式方式返回数据,这对于语音交互非常重要,因为可以边接收文本片段边进行TTS转换并播放,显著改善用户体验。Qwen3商业版(思考模式)、开源版等可能只支持流式输出。
- 任务类型隐式定义: 通过精心设计的
prompt
(在messages
中的system
和user
内容)以及输入数据的组合来引导模型执行特定任务。- 语音转文字 (STT): 通常会使用专门的STT API服务(如阿里云的语音服务,或Qwen-Audio的STT能力)。将录制的音频数据发送给STT API,获取转录文本。
- 文字转语音 (TTS): 将LLM生成的文本发送给TTS API(如阿里云的语音合成服务,或Qwen-Audio的TTS能力,或Qwen-Omni的音频输出能力)。选择合适的音色(voice)和语速等参数。
- 视觉问答 (VQA): 在
messages
中同时提供图像(image_url
)和针对图像的问题文本(text
),模型(如qwen-vl-plus
)会理解图像内容并回答问题。 - 文本生成/对话: 提供纯文本的
prompt
,模型会根据上下文生成连贯的回复。
- 多模态输入组合: 明确API如何接收同时包含文本、图像(URL或Base64编码)、音频(URL或Base64编码)的输入。LangChain等框架提供了标准化的方式来构建这类多模态输入。Qwen-Omni模型是处理此类复杂输入的理想选择,其API文档会详细说明各种
type
(如text
,image_url
,input_audio
,video_url
)的用法。
正确配置和调用这些API是实现小车智能交互的核心。开发过程中需要仔细阅读并遵循API提供商的最新文档,注意API的速率限制、并发请求数以及费用等问题。
2.3 传感器模块
传感器是小车感知世界的窗口,为各项智能化功能提供原始数据。下表列出了针对本项目各项感知功能的推荐传感器型号、接口类型、连接方式以及关键驱动/库。
功能 | 推荐组件型号 | 接口类型 | 树莓派连接引脚/方式 | 关键驱动/库安装命令 (示例) | 选型理由与备注 |
---|---|---|---|---|---|
听 (麦克风) | ReSpeaker 2-Mics Pi HAT / 高品质USB麦克风 (如 Blue Yeti Nano Mini , Anker PowerConf S3 ) / I2S MEMS 麦克风 (如 SPH0645LM4H , INMP441 ) | HAT / USB / I2S | HAT直接插接 / Any_USB_Port / I2S_Pins (DIN, BCLK, LRCLK, typically GPIO18-21) | seeed-voicecard (ReSpeaker驱动) / sudo apt install alsa-utils pulseaudio (USB麦克风通常免驱) / pip install adafruit-circuitpython-i2s (需配置内核模块) | HAT: 集成度高,通常带DSP,易于使用。USB: 即插即用,驱动简单,音质普遍较好,选择时注意信噪比和灵敏度。I2S: 数字接口,音质好,体积小,但配置略复杂,需要树莓派内核支持。选择时综合考虑音质需求、易用性、成本和空间。 |
看 (摄像头) | Raspberry Pi Camera Module 3 (标准或广角) / Arducam IMX519 16MP Autofocus Camera for Raspberry Pi | CSI | Camera_CSI_Port (树莓派主板上的CSI接口) | 通过 sudo raspi-config 启用摄像头接口, 使用 libcamera-apps (命令行工具) 或 picamera2 (Python库) 进行编程。 | RPi Cam Mod 3: 官方支持好,性能均衡,有标准视角和广角可选,支持HDR。Arducam IMX519: 更高像素(16MP),支持自动对焦,画质更佳,但价格稍高。选择时考虑视场角需求(广角适合导航和环境感知)、分辨率、低光性能和是否需要自动对焦。 |
温湿度 | Adafruit BME280 / DHT22 (AM2302) | I2C / GPIO (Single-wire) | I2C_SDA_SCL (GPIO2, GPIO3) / Any_Digital_GPIO_Pin | pip install adafruit-circuitpython-bme280 / pip install Adafruit_DHT | BME280: 精度较高,同时测量温度、湿度和气压,I2C接口稳定。DHT22: 性价比高,但精度和响应速度略逊于BME280,单总线协议对时序要求较高。本项目中,BME280是更稳妥的选择。 |
光照强度 | Adafruit BH1750 / VEML7700 | I2C | I2C_SDA_SCL | pip install adafruit-circuitpython-bh1750 / pip install adafruit-circuitpython-veml7700 | 数字输出,简单易用,用于测量环境光强度,可辅助判断白天/黑夜,或作为调整小车“心情”的输入之一。VEML7700动态范围更广。 |
电池电量监测 | Adafruit INA219 Current Sensor Breakout / Waveshare UPS HAT (带I2C监测功能) / MAX17048 LiPo Fuel Gauge | I2C / HAT | I2C_SDA_SCL / HAT直接集成 | pip install adafruit-circuitpython-ina219 / 特定HAT的SDK或示例代码 / pip install adafruit-circuitpython-max1704x | INA219: 精确测量电池的电压、电流(充放电)、功率,通过积分计算消耗电量,从而估算剩余电量百分比。UPS HAT: 通常集成了充放电管理电路和电量监测芯片(如MAX1704x系列),通过I2C读取电量,更为便捷。MAX17048: 专用的电量计芯片,算法更成熟。 |
充电坞对接 | IR对管 (发射/接收,如TCRT5000模块) / 小型摄像头 (用于AprilTag/QR码识别,可复用主摄像头或增加一个低成本USB摄像头) / 霍尔传感器 + 磁铁 (用于精确对位) | GPIO / CSI/USB / GPIO | Digital_GPIO_Pins (IR) / Existing_Camera_or_Dedicated_USB_Cam / Digital_GPIO_Pin (霍尔) | RPi.GPIO / OpenCV , pupil-apriltags (Python库) / RPi.GPIO | 根据自主回充方案选择。IR对管: 简单,成本低,用于近距离检测和对准。摄像头+视觉标签: 定位精度较高,但计算量大,对光照敏感。霍尔传感器: 与磁铁配合,可实现非常精确的最终对接确认。通常组合使用。 |
2.4 执行器模块
执行器负责将小车的决策转化为实际的动作和表达。这包括驱动小车移动的电机、发出声音的扬声器,以及可选的表情显示和灯光效果。
Adeept Mars Rover PiCar-B智能机器人小车,展示了典型的四轮底盘、传感器和树莓派集成设计
功能 | 推荐组件型号 | 接口类型 | 树莓派连接引脚/方式 | 关键驱动/库安装命令 (示例) | 选型理由与备注 |
---|---|---|---|---|---|
说 (扬声器) | 小型USB音箱 / PAM8403 Mini 5V Digital Amplifier Module + 3W 4Ω 扬声器 / Adafruit I2S 3W Stereo Speaker Bonnet for Raspberry Pi | USB / GPIO(PWM/Analog-like from DAC) / I2S | Any_USB_Port / 树莓派3.5mm音频口或外接DAC的GPIO / I2S_Pins (DOUT, BCLK, LRCLK, typically GPIO18-21) | aplay (命令行播放) / pygame.mixer (Python库) / adafruit-circuitpython-i2s (需配置) | USB音箱: 最简单方便,即插即用。PAM8403: 性价比高的小型D类功放模块,需要简单焊接和外接扬声器,音量适中。I2S Bonnet: 音质通常优于PWM或模拟输出,驱动相对简单,直接插在树莓派GPIO上。选择时考虑音质、音量、集成便利性和成本。 |
驱动电机 (小车) | L298N Motor Driver Module / DRV8833 Dual Motor Driver Carrier / TB6612FNG Dual Motor Driver | GPIO (PWM for speed, Digital for direction) | 每个电机通常需要2-3个GPIO引脚 (如 IN1, IN2, ENA for L298N)。具体引脚分配根据驱动板和所用库决定。 | pip install RPi.GPIO gpiozero (gpiozero 提供了更高级的电机控制抽象) | L298N: 非常常见且廉价,但效率较低,有较大电压降,发热量大,不适合对续航要求高的电池供电项目。DRV8833/TB6612FNG: 基于MOSFET,效率更高,体积更小,发热量小,更适合电池驱动。选择时务必根据所用电机的额定电压和最大电流进行匹配。TB6612FNG通常是小型机器人项目的优选。 |
小车底盘与电机 | 2WD/4WD Acrylic or Metal Chassis Kit (通常配备TT直流减速电机或N20金属齿轮减速电机) | N/A (机械组装) | 电机线连接到电机驱动模块。 | N/A | 选择底盘时需考虑尺寸是否能容纳所有电子元件和电池,以及轮子类型是否适合预期的行驶地面。带编码器的电机 (如霍尔编码器或光电编码器) 对于实现精确的速度控制和里程计算非常有帮助,是实现可靠导航和跟随功能的重要基础。N20电机通常比黄色TT电机更精密,扭矩和噪音表现更好。 |
[可选]表情显示 | Adafruit Mini 0.54" 8x8 LED Matrix FeatherWing (HT16K33) / 小型I2C OLED显示屏 (如 SSD1306 128x32 或 128x64 ) / Pimoroni Unicorn HAT Mini (RGB LED Matrix) | I2C / SPI (Unicorn HAT) | I2C_SDA_SCL / SPI_MOSI_SCLK_CS | pip install adafruit-circuitpython-ht16k33 luma.led_matrix / pip install adafruit-circuitpython-ssd1306 luma.oled / pip install unicornhatmini | 用于通过点阵、简单图形或短文本来显示小车的“心情”、状态信息或简单的动画效果,增强交互的趣味性。选择时考虑显示内容复杂度、尺寸、功耗和易用性。 |
[可选]车身灯光 | WS2812B RGB LED Strip (Neopixel) / 单色高亮LED | GPIO (SPI-like for WS2812B) / GPIO (Digital) | WS2812B通常使用一个GPIO引脚 (如 GPIO18 for PWM/SPI-like signal) / 普通LED使用一个GPIO驱动。 | pip install rpi_ws281x adafruit-circuitpython-neopixel / RPi.GPIO | 用于状态指示(如充电时呼吸灯、识别到用户时闪烁特定颜色)或营造氛围,增加小车的“表现力”。WS2812B可编程性强,能实现复杂灯效。 |
2.5 自主回充系统
自主回充是提升小车自主性的关键功能,使其能够在电量不足时自动返回充电基座进行充电。这套系统涉及充电坞的设计、导航定位方案的选择以及精确对接技术。
Loona智能机器人在其充电基座上,展示了自动回充的概念
充电坞设计理念
- 机械结构: 设计应便于小车导入和对准。常见的有
V型或U型导向槽
,利用小车的外形配合,逐步将小车引导至正确的充电位置。可以考虑一定的坡度设计,方便小车驶入,同时利用重力辅助保持接触。 - 充电触点: 必须保证可靠的电气连接。推荐使用
弹簧顶针 (Pogo Pins)
,它们具有一定的行程,能适应轻微的高度差和接触面不平整。另一种方式是使用大面积铜片接触
,增加接触冗余。设计时需考虑触点的耐用性、防氧化以及防反接保护(如机械结构限位或配合电子保护电路)。 - 导航信标: 根据选用的导航方案,在充电坞的醒目位置安装相应的信标。例如,如果是红外引导,则安装特定编码或频率的红外LED;如果是视觉标签导航,则粘贴清晰的AprilTag或QR码标记。
导航定位方案对比与推荐
选择合适的导航定位方案是实现自主回充的核心技术挑战。以下对比几种常见方案:
方案名称 | 传感器/组件 | 原理简述 | 优点 | 缺点 | 适用场景/复杂度 |
---|---|---|---|---|---|
红外信标引导 (IR Beacon) | 充电坞: 高功率红外LED阵列 (特定编码/频率) 小车: 多个带方向性的红外接收管 (如TSOP系列) 或红外摄像头 | 小车通过扫描或比较多个接收管的信号强度差来确定红外信标的方向,逐步调整姿态靠近。编码/频率调制可抗干扰。 | 成本较低,实现相对简单,对环境光有一定抗干扰能力(通过编码/滤波)。 | 有效距离有限(通常几米内),易受物理遮挡,定位精度一般,末端精确对接可能需要其他辅助手段(如机械结构或更近距离传感器)。 | 适用于环境相对简单的室内场景,对远距离导航精度要求不高,或作为末端精确对接的辅助引导。复杂度:低-中。 |
视觉标签坞站 (AprilTag/QR Code) | 充电坞: 打印的AprilTag或QR码标记 小车: 摄像头 (可复用主摄像头) | 小车通过摄像头实时检测并识别预设在充电坞上的特定视觉标签,通过算法计算出标签相对于摄像头的三维位姿(位置和方向),然后通过视觉伺服控制小车向目标位姿移动。 | 定位精度较高(尤其在中近距离),可利用现有摄像头硬件,AprilTag等标签可编码ID以区分多个坞站或提供额外信息。 | 计算量较大(尤其在树莓派上进行实时位姿估计),对光照条件变化敏感,标签易被脏污或部分遮挡影响识别,需要良好的摄像头标定。 | 对定位精度有较高要求的场景,且计算资源相对充足(或可由K230协处理)。复杂度:中-高。 |
循线引导至充电点 | 充电坞: 充电触点位于特定路径末端 小车: 红外循迹传感器模块 (通常包含多对红外发射/接收管) | 在地面铺设特定颜色的引导线(如深色胶带在浅色地面),小车底部的循迹传感器检测引导线,通过PID等控制算法使小车沿线行驶至充电点。 | 技术成熟,在清晰路径上可靠性高,成本低廉。 | 路径固定,缺乏灵活性,需要预先在环境中铺设引导轨道,可能影响美观或与其他活动冲突。 | 适用于环境固定、可以接受地面引导线的室内场景,如家庭或特定工作区域。复杂度:低。 |
超声波/激光雷达 (LiDAR) 辅助 | 充电坞: 特定形状或具有特殊反射特性的坞站结构 小车: 超声波传感器阵列 / 小型2D LiDAR | 通过测距传感器感知坞站的轮廓、距离或特定反射特征,进行导航和对接。LiDAR可以进行SLAM建图和定位。 | LiDAR精度高,可实现更复杂的路径规划和环境适应性。超声波成本低,可用于辅助避障和粗略距离判断。 | 超声波测距精度和角度分辨率有限,易受表面材质影响。LiDAR成本较高,数据处理和算法实现复杂。 | LiDAR适合需要复杂环境导航和自主建图的场景。超声波可作为辅助避障或近距离粗定位手段。复杂度:中(超声波)-极高(LiDAR SLAM)。 |
推荐方案: 组合方案:视觉标签 (AprilTag) 初步定位引导 + 红外信标/机械结构 末端精确对接
。
- 理由:
- AprilTag进行中远距离引导: 利用小车已有的主摄像头,在中远距离(例如0.5米至3米范围)识别充电坞上的AprilTag,获取充电坞的大致方向和位置。这为小车提供了一个初始的目标导向,无需额外的专用远距离传感器。AprilTag的识别算法相对成熟,OpenCV配合
pupil-apriltags
库可以在树莓派上运行,或由K230加速。 - 红外信标或机械导向进行末端精确对接: 当小车靠近充电坞(例如最后10-30厘米)时,AprilTag可能由于视角变化过大、部分遮挡或景深问题导致识别不稳定或精度下降。此时,可以切换到更简单可靠的近距离引导方式。例如,在充电坞上安装几个特定方向的红外LED,小车前端安装对应的红外接收管,通过检测信号强度或特定编码进行最后阶段的姿态调整和对准。或者,完全依赖精心设计的机械导向槽(如V型槽)将小车“卡入”正确位置。
- 优势互补: 该组合方案结合了视觉标签在中距离定位的灵活性和较高精度,以及红外/机械引导在末端对接的可靠性和低成本,是一种在精度、成本和实现复杂度之间取得较好平衡的策略。
- AprilTag进行中远距离引导: 利用小车已有的主摄像头,在中远距离(例如0.5米至3米范围)识别充电坞上的AprilTag,获取充电坞的大致方向和位置。这为小车提供了一个初始的目标导向,无需额外的专用远距离传感器。AprilTag的识别算法相对成熟,OpenCV配合
充电确认
小车成功对接到充电坞后,需要确认充电是否已开始。这可以通过以下方式实现:
- 电流监测: 使用
INA219
等电流传感器监测电池充电电流。当检测到有稳定的充电电流(例如大于某个阈值,如100mA)时,即可确认充电已开始。 - 充电模块状态引脚: 许多专用的锂电池充电管理模块(如TP4056的某些版本,或集成在UPS HAT上的管理芯片)会提供一个充电状态指示引脚(如CHRG, STDBY)。树莓派可以通过读取该GPIO引脚的电平来判断充电状态。
- 电压变化: 监测电池电压,如果电压在接入充电器后持续稳定上升,也可以作为充电开始的一个间接指标,但不如电流监测直接。
确认充电后,小车应进入“充电中”状态,并可以通过语音或灯光向用户反馈。
2.6 K230 机器视觉开发板应用
嘉楠科技的K230芯片是一款专为边缘AI计算设计的SoC,内置了KPU(Knowledge Process Unit)智能计算单元,具备一定的AI算力。在本项目中,K230开发板可以作为树莓派的视觉协处理器,分担计算密集型的视觉任务。
CanMV K230机器视觉开发板,搭载Kendryte K230双核RISC-V处理器
在本项目中的定位与任务
定位: 专用视觉协处理器
。
主要任务:
- 实时人脸检测与跟踪: 在较高帧率下(如15-30 FPS)检测视频流中的人脸,并对指定人脸进行跟踪。这是实现主动跟随功能的基础。K230的KPU适合运行轻量级的人脸检测模型(如优化版的YOLO系列、MTCNN或BlazeFace)。
- 特定人脸特征提取与比对(用于身份识别): 如果需要识别特定用户,K230可以运行轻量级的人脸识别模型(如MobileFaceNet、ArcFace的嵌入提取部分),提取人脸特征向量,然后与预先注册的人脸特征库进行比对。这比在树莓派CPU上进行此类运算效率高得多。
- AprilTag/QR码检测与姿态估计: 在自主回充导航过程中,K230可以负责快速检测和解码AprilTag或QR码,并进行初步的姿态估计,将结果发送给树莓派进行路径规划。
- [可选] 简单手势识别或物体分类: 如果有需求,K230也可以运行一些轻量级的手势识别或常见物体分类模型。
目标: 通过将这些计算密集型视觉任务卸载到K230,可以显著提高视觉处理的实时性和响应速度,同时降低树莓派的CPU负载,使其能够更流畅地运行主控制逻辑、与Qwen3 API进行复杂的交互、管理记忆和个性化等上层应用。
与树莓派通信方式
树莓派与K230之间需要稳定高效的通信方式来传输图像数据、控制指令和处理结果。
- 推荐:
以太网 (TCP/IP Sockets)
。- 理由: 大多数K230开发板(如 CanMV-K230)具备以太网接口。以太网提供了较高的带宽(通常100Mbps或1Gbps),足以传输压缩后的视频流或处理结果。TCP/IP协议栈成熟可靠,易于编程和调试(可以使用标准的网络抓包工具)。树莓派和K230可以通过一个小型的网络交换机连接,或者在简单场景下使用交叉直连线并配置静态IP地址。
- 备选:
USB (模拟成虚拟串口或自定义USB协议)
。- 理由: USB连接简单直接。某些K230板可能通过USB OTG接口提供数据通信能力,可以模拟成虚拟串口(CDC-ACM设备)进行类似UART的通信,或者实现自定义的USB传输协议。带宽和稳定性可能不如以太网,但对于某些应用场景也足够。
- 次选:
SPI
或UART
。- 理由: 硬件接口简单,几乎所有微控制器和SBC都支持。但其速率相对较低(SPI通常几Mbps到几十Mbps,UART通常几十Kbps到几Mbps),可能成为传输原始图像数据的瓶颈。更适合传输控制指令、状态信息或小数据量的处理结果(如目标坐标)。协议实现需要双方精确同步,容错性较低。
K230型号与应用流程示例
K230型号示例: 01Studio CanMV K230 AI开发板
(官方资料) 或 立创·庐山派K230-CanMV开发板 (上手指南)。这些板卡基于嘉楠K230芯片,通常配备了摄像头接口、显示接口、网络接口以及必要的开发工具和SDK。
K230芯片关键参数 (参考 01Studio K230参数):
- CPU: 双核RISC-V (例如,一个1.6GHz核心支持RVV 1.0,一个800MHz核心)
- KPU: 神经网络处理单元,算力可达6TOPS等效,支持INT8和INT16精度。典型网络性能如Resnet50 ≥ 85fps @ INT8;Mobilenet_v2 ≥ 670fps @ INT8;YOLO V5s ≥ 38fps @ INT8。
- VPU: 支持H.264和H.265视频编解码,最高4K。
- 图像输入: 支持多路MIPI CSI输入。
应用流程示例 (K230处理人脸检测与跟踪,通过以太网与树莓派通信):
- K230端准备:
- 系统与SDK: 烧录嘉楠官方提供的K230 SDK,其中包含操作系统(可能是RT-Thread和Linux双系统)、底层驱动、AI工具链(用于模型转换和部署)以及示例代码。
- 模型部署: 将预训练的人脸检测模型(如轻量化的YOLOv5s-face或BlazeFace)和(可选的)人脸特征提取模型(如MobileFaceNet)使用嘉楠的工具链转换为KPU支持的格式(通常是
.kmodel
文件),并部署到K230的文件系统中。 - 服务程序开发 (C/C++或Python with CanMV):
- 在K230上编写一个主服务程序。该程序初始化网络(配置IP地址,创建TCP Socket Server并监听特定端口)。
- 初始化摄像头(如果摄像头直接连接到K230的CSI接口)。
- 加载部署好的AI模型到KPU。
- 循环等待树莓派的连接请求。一旦连接建立,进入数据处理循环:
- 接收来自树莓派的指令(如“开始检测人脸”、“开始跟踪ID为X的人脸”)或图像数据(如果摄像头连接在树莓派端,树莓派会将图像帧发送过来)。
- 如果摄像头在K230端,则自行捕获图像帧。
- 将图像帧送入KPU进行人脸检测,获取人脸包围框(bounding boxes)和置信度。
- 如果需要人脸识别,则对检测到的人脸区域提取特征向量,并与K230本地存储(或由树莓派管理)的人脸特征库进行比对,返回匹配到的人员ID。
- 如果需要跟踪,则根据指令初始化跟踪器(如KCF,可在CPU上运行,或利用KPU的检测结果进行基于检测的跟踪)或更新跟踪状态。
- 将处理结果(如人脸框坐标列表、识别出的人员ID、跟踪目标的新位置)打包成特定格式(如JSON字符串)通过TCP Socket发送回树莓派。
- 树莓派端集成:
- 客户端代码 (Python): 在树莓派的主控制程序中,创建一个
K230Client
类。- 该类负责与K230建立TCP Socket连接。
- 提供方法如
request_face_detection(frame)
:该方法将摄像头捕获的图像帧(可能需要压缩,如JPEG编码,以减少传输数据量)发送给K230,并等待接收K230返回的人脸检测结果。 - 提供方法如
start_face_tracking(face_id_or_bbox)
和get_tracking_update(frame)
。 - 解析K230返回的数据,并将其提供给上层应用逻辑(如运动控制模块进行跟随,或个性化引擎生成问候语)。
- 客户端代码 (Python): 在树莓派的主控制程序中,创建一个
K230对比其他方案及其优缺点
将K230作为视觉协处理器,需要与其他可能的方案进行对比,以明确其优势和劣势。
特性 | K230 (以CanMV K230为例) | 树莓派 5 + OpenCV (CPU) | Google Coral USB Accelerator (Edge TPU) on RPi | [可选] NVIDIA Jetson Nano (GPU) |
---|---|---|---|---|
核心AI算力 | 专用KPU (如6TOPS等效, INT8/16) | ARM Cortex-A76 CPU (通用计算) | Edge TPU (如4 TOPS @ INT8) | NVIDIA Maxwell/Pascal GPU (128/256 CUDA Cores) |
典型视觉任务性能 (FPS) | 人脸检测: 较高 (如YOLOv5s @ 38fps K230参数) | 人脸检测: 较低 (如Haar Cascade @ 10-20 FPS, DNN模型如MTCNN @ 5-10 FPS, 取决于模型复杂度和图像分辨率) | 人脸检测: 较高 (依赖TensorFlow Lite优化模型, 可达30+ FPS) | 人脸检测: 高 (可运行更复杂的模型, 30+ FPS) |
功耗 (Typical) | 较低 (芯片级优化, 典型负载下 SoC 功耗可能在2-5W范围) | 中等 (整个RPi5板卡在视觉处理时功耗可能在5-10W) | 低 (加速器本身 <2.5W, 但树莓派仍有自身功耗) | 较高 (整个Jetson Nano板卡功耗在5-15W或更高) |
开发生态与复杂度 | 嘉楠SDK, CanMV (MicroPython/Python), C/C++。生态相对较新,模型转换和部署有一定学习曲线。社区支持不如树莓派或Jetson成熟。 | 成熟的Linux环境, OpenCV, Python生态极为丰富, 大量教程和库,上手相对容易。 | TensorFlow Lite, PyCoral API。需要将模型转换为Edge TPU兼容格式,有一定优化门槛。社区支持良好。 | JetPack SDK, CUDA, TensorRT。功能强大,但配置和开发复杂度较高,学习曲线陡峭。社区庞大。 |
成本 | K230开发板价格 (约 $30-$100不等,如 CanMV-K230约$50) | 树莓派5板卡价格 (约 $60-$80) | Coral USB Accelerator价格 (约 $60-$75) + 树莓派价格 | Jetson Nano开发者套件价格 (通常 $100-$150+) |
与主控集成 | 需要额外的通信接口(如以太网、USB)和协议实现,增加了系统复杂度。 | 无需额外集成,代码直接在树莓派上运行。 | 通过USB接口连接,集成相对简单。 | 通常作为独立主控使用,若作为协处理器则需网络集成,复杂度较高。 |
多模态大模型支持 | 不直接支持运行LLM。专注于视觉AI加速。 | 不直接支持运行大型LLM,主要依赖云端API。 | 不支持LLM。 | 可以运行一些非常小型的LLM(如量化后的几B参数模型),但并非其强项,性能有限。 |
图表说明:上图数据为基于公开资料和一般技术认知的估算值,实际性能和功耗会因具体模型、优化程度、工作负载和外设连接情况而异。K230的FPS数据参考了其YOLOv5s的指标,Coral的FPS基于其对优化TFLite模型的表现,RPi5 CPU的FPS基于运行如MTCNN等模型的典型表现,Jetson Nano的FPS基于其GPU运行常见检测模型的能力。功耗为典型视觉处理负载下的估算。
K230应用优缺点总结:
- 优点:
- 专用AI硬件加速: 对于KPU支持的视觉模型(尤其是经过INT8量化优化的模型),K230能够提供远超树莓派CPU的推理速度和更高的能效比。
- 解放树莓派CPU资源: 将计算密集的视觉任务卸载到K230,可以使树莓派的CPU更专注于运行主控制逻辑、与云端Qwen3 API进行交互、处理记忆和个性化等上层应用,从而提升整体系统的流畅性和响应性。
- 多路摄像头支持潜力: K230芯片本身支持多路MIPI CSI输入(如 CanMV K230支持3路CSI),为未来扩展小车的视觉感知范围(如增加后视或侧视摄像头)提供了硬件基础。
- 相对较低的功耗: 在执行视觉AI任务时,K230作为专用芯片,其功耗可能低于使用更高性能通用SBC(如Jetson Nano)或纯粹依赖树莓派CPU进行处理的方案,这对于电池供电的小车项目是有利的。
- 缺点:
- 开发生态和社区相对较小: 与树莓派或NVIDIA Jetson等拥有庞大用户群和成熟生态的平台相比,K230的开发资料、预编译库、第三方工具和社区支持可能相对较少。这意味着在开发过程中遇到问题时,解决难度可能会更大,可参考的开源项目也较少。
- 额外的硬件成本和集成复杂度: 需要额外购买K230开发板,并投入精力设计和实现树莓派与K230之间稳定可靠的通信机制(包括硬件连接和软件协议),这增加了项目的整体成本和系统复杂度。
- 模型转换和部署的学习曲线: 将常用的深度学习模型(如TensorFlow、PyTorch格式)转换为KPU支持的特定格式(如
.kmodel
),需要熟悉嘉楠科技提供的AI工具链和转换流程,这可能需要一定的学习和调试时间。 - 非通用计算平台: K230的核心优势在于视觉AI加速,它并不适合运行通用的Linux应用程序或大型语言模型。其CPU性能和内存容量也限制了其在通用计算方面的能力。
综上所述,是否集成K230取决于项目对视觉处理实时性、树莓派CPU负载、预算以及开发团队对新技术栈的掌握程度。如果对人脸跟踪等功能的流畅性有较高要求,且预算和开发能力允许,引入K230作为视觉协处理器是一个值得考虑的方案。
三、软件实现方案与关键代码结构
软件是智能小车“灵魂”的载体。本章节将概述主控制程序的核心逻辑,并详细介绍各个功能模块的代码结构和关键职责,特别强调与Qwen3 API的交互以及K230的潜在集成方式。
3.1 主控制程序 (树莓派 - smart_car_main.py
)
主控制程序是整个智能小车的中枢神经系统,负责初始化各个模块、管理小车状态、调度任务、处理用户交互以及协调各硬件和AI能力的运作。我们将采用基于异步事件驱动的模块化架构,使用Python的asyncio
库来处理并发IO密集型任务(如API调用、传感器轮询)。
# smart_car_main.py - Conceptual Structure
import asyncio
import time
import logging
from config_loader import ConfigLoader # 假设用于加载配置文件
from qwen_api_handler import QwenAPIHandler
from audio_processor import AudioProcessor
from vision_processor import VisionProcessor
# from k230_client import K230Client # 如果使用K230
from motion_controller import MotionController
from memory_manager import MemoryManager # 可能基于LangChain
from personality_engine import PersonalityEngine
from environment_monitor import EnvironmentMonitor
from power_manager import PowerManager
from auto_charge_controller import AutoChargeController
# 配置日志记录
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(module)s - %(message)s')
class SmartCarCore:
def __init__(self):
logging.info("Initializing SmartCarCore...")
self.config = ConfigLoader().load_config('config.yaml') # 加载配置
# 初始化API处理器
self.qwen_api = QwenAPIHandler(
api_key=self.config['qwen_api']['api_key'],
base_url=self.config['qwen_api'].get('base_url')
)
# 初始化硬件接口模块
self.audio_processor = AudioProcessor(
mic_config=self.config['audio']['microphone'],
speaker_config=self.config['audio']['speaker']
)
# self.k230_client = K230Client(ip=self.config['k230']['ip'], port=self.config['k230']['port']) if self.config.get('k230', {}).get('enabled') else None
self.vision_processor = VisionProcessor(
camera_config=self.config['vision']['camera'],
# k230_client=self.k230_client, # 如果使用K230,则传入
face_recognition_config=self.config['vision']['face_recognition']
)
self.motion_controller = MotionController(config=self.config['motion'])
self.environment_monitor = EnvironmentMonitor(config=self.config['environment_sensors'])
self.power_manager = PowerManager(config=self.config['power']) # 移除了motion_controller依赖,power_manager应独立
self.auto_charge_controller = AutoChargeController(
config=self.config['auto_charge'],
vision_processor=self.vision_processor,
motion_controller=self.motion_controller,
power_manager=self.power_manager
)
# 初始化认知模块
self.memory_manager = MemoryManager(
db_path=self.config['memory']['db_path'],
qwen_api_handler=self.qwen_api # 用于可能的记忆总结或嵌入生成
)
self.personality_engine = PersonalityEngine(profile_path=self.config['personality']['profile_path'])
# 状态变量
self.current_mood = self.personality_engine.get_initial_mood()
self.is_interacting = False # 标记是否正在与用户交互
self.target_person_id = self.config['vision']['face_recognition'].get('target_person_id_to_follow')
self.is_following_target = False
self.system_running = True
logging.info("SmartCarCore initialized.")
async def process_voice_command(self, audio_data):
"""处理用户的语音指令"""
if self.is_interacting: # 防止重入
logging.warning("Already processing a command, skipping new one.")
return
self.is_interacting = True
logging.info("Processing voice command...")
try:
# 1. 语音转文字 (STT)
text_command = await self.qwen_api.speech_to_text(audio_data)
if not text_command:
await self.audio_processor.play_feedback_sound('error_stt') # 如 "未能识别语音"
self.is_interacting = False
return
logging.info(f"User command (STT): {text_command}")
await self.memory_manager.add_interaction_log("user", text_command, mood=self.current_mood)
# 2. 构建上下文给Qwen3
# 视觉上下文可以更智能,例如,如果用户问“这是什么?”,则捕获当前帧
current_frame_for_vqa = None
if "什么" in text_command and ("这" in text_command or "那" in text_command): # 简陋的VQA触发
current_frame_for_vqa = self.vision_processor.capture_frame_for_vqa() # 获取base64图像
context = {
"user_input": text_command,
"current_mood": self.current_mood,
"battery_level_percentage": self.power_manager.get_battery_percentage(),
"environment_data": self.environment_monitor.get_current_status(),
"recent_memory_summary": await self.memory_manager.get_recent_interactions_summary(limit=5), # 获取总结
"visual_context_image_base64": current_frame_for_vqa
}
# 3. 调用Qwen3大模型获取回复 (文本和/或动作)
system_prompt = self.personality_engine.get_system_prompt(self.current_mood)
qwen_response = await self.qwen_api.generate_multimodal_response(context, system_prompt)
response_text = qwen_response.get("text_response")
action_details = qwen_response.get("action_to_take") # 例如: {"type": "move", "direction": "forward", "duration_ms": 1000}
# 4. 处理回复文本 (个性化TTS)
if response_text:
personalized_speech = self.personality_engine.personalize_text(response_text, self.current_mood)
logging.info(f"AI Response (personalized): {personalized_speech}")
await self.memory_manager.add_interaction_log("ai", personalized_speech, mood=self.current_mood)
# TTS可能通过Qwen API或本地实现
await self.audio_processor.text_to_speech_and_play(personalized_speech, self.qwen_api, self.personality_engine.get_voice_options(self.current_mood))
else:
await self.audio_processor.play_feedback_sound('no_response_text')
# 5. 执行动作
if action_details:
await self.execute_action(action_details)
except Exception as e:
logging.error(f"Error processing voice command: {e}", exc_info=True)
await self.audio_processor.play_feedback_sound('error_processing')
finally:
self.is_interacting = False # 确保状态被重置
async def proactive_behavior(self):
"""处理主动行为检查和动作 (如低电量提醒, 天气变化等)"""
if self.is_interacting or self.auto_charge_controller.is_charging_or_docking():
return # 如果正在交互或充电/坞接,则不进行主动行为
env_status = self.environment_monitor.get_current_status()
battery_level = self.power_manager.get_battery_percentage()
# 根据环境和电池更新心情
prev_mood = self.current_mood
self.current_mood = self.personality_engine.update_mood(prev_mood, env_status, battery_level)
if self.current_mood != prev_mood:
logging.info(f"Mood changed from {prev_mood} to {self.current_mood}")
# 可选: 播放一个简短的音效或改变灯光来指示心情变化
# 主动提醒或动作
if self.power_manager.is_critically_low() and not self.auto_charge_controller.is_charging_or_docking():
logging.info("Battery critically low, initiating auto charge sequence.")
alert_text = self.personality_engine.generate_proactive_alert("low_battery_critical", {"level": battery_level})
await self.audio_processor.text_to_speech_and_play(alert_text, self.qwen_api, self.personality_engine.get_voice_options(self.current_mood))
await self.auto_charge_controller.start_docking_sequence()
return # 坞接优先
# 示例: 天气提醒 (假设environment_monitor能获取天气)
# weather_condition = env_status.get("weather_condition_from_api")
# if weather_condition == "rain_imminent" and not self.memory_manager.has_recent_event("rain_warning_issued", timedelta(hours=1)):
# alert_text = self.personality_engine.generate_proactive_alert("rain_warning", {})
# await self.audio_processor.text_to_speech_and_play(alert_text, self.qwen_api, self.personality_engine.get_voice_options(self.current_mood))
# await self.memory_manager.add_event_log("proactive_alert", {"type": "rain_warning_issued"})
async def vision_tasks(self):
"""处理连续的视觉任务,如人脸检测和跟踪"""
if self.is_interacting or self.auto_charge_controller.is_charging_or_docking():
if self.is_following_target: # 如果在忙其他事,停止跟随
self.is_following_target = False
self.motion_controller.stop()
return
frame = self.vision_processor.capture_frame()
if frame is None:
return
# 使用VisionProcessor处理帧,它内部可能调用K230或本地OpenCV
detected_faces_info = self.vision_processor.process_frame_for_faces_and_tracking(frame, self.is_following_target, self.target_person_id)
# detected_faces_info 结构: {'faces': [{'id': 'person_A', 'bbox': [x,y,w,h], 'is_target': True}, ...], 'target_lost': False}
if self.target_person_id:
target_face = next((f for f in detected_faces_info.get('faces', []) if f.get('is_target')), None)
if target_face:
if not self.is_following_target:
logging.info(f"Target person {self.target_person_id} detected. Starting to follow.")
greet_text = self.personality_engine.generate_greeting(self.target_person_id, self.current_mood)
asyncio.create_task(self.audio_processor.text_to_speech_and_play(greet_text, self.qwen_api, self.personality_engine.get_voice_options(self.current_mood)))
self.is_following_target = True
# 根据目标位置计算电机指令
frame_center_x = frame.shape[1] / 2
# 假设 target_face['bbox'] 是 [x, y, w, h]
motor_commands = self.motion_controller.calculate_follow_commands(target_face['bbox'], frame_center_x)
self.motion_controller.set_motors(motor_commands['left_speed'], motor_commands['right_speed'])
elif self.is_following_target and detected_faces_info.get('target_lost'):
logging.info(f"Target person {self.target_person_id} lost. Stopping.")
self.is_following_target = False
self.motion_controller.stop() # 或者执行搜索模式
# 此处可以添加其他背景视觉任务,如简单的障碍物检测
async def execute_action(self, action_details):
"""执行由Qwen3 API或本地逻辑决定的动作"""
action_type = action_details.get("type")
logging.info(f"Executing action: {action_details}")
try:
if action_type == "move":
direction = action_details.get("direction")
speed = action_details.get("speed", 50) # 默认速度
duration_ms = action_details.get("duration_ms") # 可选持续时间
if duration_ms: # 如果有持续时间,则异步执行后停止
async def move_and_stop():
if direction == "forward": self.motion_controller.move_forward(speed)
elif direction == "backward": self.motion_controller.move_backward(speed)
elif direction == "left": self.motion_controller.turn_left(speed)
elif direction == "right": self.motion_controller.turn_right(speed)
await asyncio.sleep(duration_ms / 1000.0)
self.motion_controller.stop()
asyncio.create_task(move_and_stop())
else: # 无持续时间,则持续运动直到下一个stop指令
if direction == "forward": self.motion_controller.move_forward(speed)
elif direction == "backward": self.motion_controller.move_backward(speed)
elif direction == "left": self.motion_controller.turn_left(speed)
elif direction == "right": self.motion_controller.turn_right(speed)
elif direction == "stop": self.motion_controller.stop()
elif action_type == "speak_direct": # 如果Qwen API建议直接TTS,不经过NLU再处理
text_to_speak = action_details.get("text")
if text_to_speak:
await self.audio_processor.text_to_speech_and_play(text_to_speak, self.qwen_api, self.personality_engine.get_voice_options(self.current_mood))
elif action_type == "change_mood":
new_mood = action_details.get("mood_to_set")
if new_mood:
self.current_mood = new_mood
logging.info(f"Mood explicitly set to {new_mood} by action.")
# 可以添加更多动作类型, 如 "play_sound_effect", "control_lights"
await asyncio.sleep(0.1) # 短暂等待,确保动作开始
except Exception as e:
logging.error(f"Error executing action {action_details}: {e}", exc_info=True)
async def run_main_loop(self):
logging.info("Starting SmartCar main loop...")
# 初始化传感器监控等
self.power_manager.start_monitoring() # 假设这些是启动后台线程或任务
self.environment_monitor.start_monitoring()
try:
while self.system_running:
# 1. 监听语音指令 (非阻塞或带超时)
# 实际应用中,这里可能是关键词唤醒后进入的逻辑
# 为简化,假设 audio_processor.listen_for_command 是异步且带超时的
# 或者有一个更上层的唤醒逻辑
if not self.is_interacting: # 仅在非交互状态下尝试监听新指令
# 假设有一个唤醒词检测,或者用户按下按钮触发
# if self.audio_processor.is_keyword_spotted_or_button_pressed():
# audio_data = await self.audio_processor.listen_for_command(timeout_seconds=self.config['audio']['listen_timeout'])
# if audio_data:
# asyncio.create_task(self.process_voice_command(audio_data))
# 为了演示,我们假设有一个模拟的触发机制
pass # 实际的语音输入触发逻辑会更复杂
# 2. 执行主动行为 (周期性检查)
# 使用asyncio.create_task来并发执行,不阻塞主循环的其他部分
# 但要注意不要过于频繁地创建任务,可以设置一个上次执行的时间戳来控制频率
if not self.is_interacting: # 仅在非交互状态下执行主动行为
asyncio.create_task(self.proactive_behavior())
# 3. 执行视觉任务 (周期性检查)
if not self.is_interacting: # 仅在非交互状态下执行视觉任务
asyncio.create_task(self.vision_tasks())
# 主循环的休眠间隔
await asyncio.sleep(self.config.get('main_loop_interval_seconds', 0.1))
except KeyboardInterrupt:
logging.info("KeyboardInterrupt received. Shutting down...")
except Exception as e:
logging.critical(f"Unhandled exception in main loop: {e}", exc_info=True)
finally:
await self.shutdown()
async def shutdown(self):
logging.info("Shutting down SmartCarCore...")
self.system_running = False
self.motion_controller.stop() # 停止电机
# 停止监控线程/任务
if hasattr(self.power_manager, 'stop_monitoring'): self.power_manager.stop_monitoring()
if hasattr(self.environment_monitor, 'stop_monitoring'): self.environment_monitor.stop_monitoring()
# 关闭数据库连接等资源
if hasattr(self.memory_manager, 'close_db'): await self.memory_manager.close_db()
# if self.k230_client and hasattr(self.k230_client, 'close'): self.k230_client.close()
logging.info("SmartCarCore shutdown complete.")
if __name__ == "__main__":
# 实际运行时,配置加载和主循环启动会更复杂,可能涉及命令行参数等
# 此处为简化示例
# config_path = 'config.yaml' # 或者从环境变量读取
# main_config = ConfigLoader().load_config(config_path)
# car = SmartCarCore(main_config)
# asyncio.run(car.run_main_loop())
print("SmartCar main structure defined. Run with actual configuration and asyncio loop.")
# 模拟一个简单的启动,实际项目需要更完善的启动脚本
async def main():
# car = SmartCarCore() # 假设配置已通过某种方式注入或默认
# await car.run_main_loop()
print("To run the SmartCar, instantiate SmartCarCore and run its main_loop with asyncio.")
if False: # 设为True以运行一个非常基础的模拟(需要补全配置)
asyncio.run(main())
上述代码结构展示了一个高度模块化和异步化的主控制程序。SmartCarCore
类聚合了所有功能模块,并通过主循环run_main_loop
来协调它们的运作。语音指令处理、主动行为和视觉任务都设计为异步任务,以保证系统的响应性。
3.2 功能模块代码结构 (Python - 示例)
以下是各核心功能模块的详细代码结构设想,包括关键方法签名及其职责说明。
听会说 (Audio Handling - audio_processor.py
)
# audio_processor.py
import sounddevice # 或者 pyaudio
import numpy as np
# from some_keyword_spotter_library import KeywordSpotter # 如 Porcupine
class AudioProcessor:
def __init__(self, mic_config, speaker_config):
self.mic_device_index = mic_config.get('device_index')
self.sample_rate = mic_config.get('sample_rate', 16000)
self.channels = mic_config.get('channels', 1)
# self.keyword_spotter = KeywordSpotter(access_key="...", keyword_paths=["..."]) # 示例
# Speaker config (e.g., for local TTS or playing sounds)
logging.info(f"AudioProcessor initialized. Mic: {self.mic_device_index}, Rate: {self.sample_rate}")
async def listen_for_command(self, duration_seconds=5) -> bytes or None:
"""录制指定时长的音频,或直到检测到静音。返回PCM字节流。"""
logging.info(f"Listening for command ({duration_seconds}s)...")
try:
recording = sounddevice.rec(int(duration_seconds * self.sample_rate),
samplerate=self.sample_rate,
channels=self.channels,
device=self.mic_device_index,
dtype='int16') # PCM16
sounddevice.wait() # 等待录音完成
# 简单的VAD:如果音量低于阈值则认为无效 (实际VAD会更复杂)
if np.abs(recording).mean() < 50: # 经验阈值
logging.info("No significant audio detected.")
return None
logging.info("Audio recorded.")
return recording.tobytes()
except Exception as e:
logging.error(f"Error recording audio: {e}")
return None
async def text_to_speech_and_play(self, text: str, qwen_api_handler, voice_options=None):
"""调用Qwen TTS API (或本地TTS),然后播放生成的音频。"""
logging.info(f"TTS for: '{text}' with options: {voice_options}")
audio_bytes = await qwen_api_handler.text_to_speech(text, voice_options)
if audio_bytes:
try:
# 假设 audio_bytes 是 wav 格式的字节流
# 需要一个播放wav字节流的函数,sounddevice可以直接播放numpy数组
# 这里简化为保存再播放,或直接使用能播放字节流的库
# For simplicity, assume a play_audio_bytes function exists
# For example, using simpleaudio or pygame.mixer for playback
# Or convert bytes to numpy array if sounddevice is used for playback
# Example:
# import wave
# import io
# wf = wave.open(io.BytesIO(audio_bytes), 'rb')
# audio_data_np = np.frombuffer(wf.readframes(wf.getnframes()), dtype=np.int16)
# sounddevice.play(audio_data_np, samplerate=wf.getframerate(), device=speaker_device_index)
# sounddevice.wait()
logging.info("Playing TTS audio...")
# 实际播放逻辑,此处为占位
# For example, if qwen_api_handler.text_to_speech returns a path to a file:
# os.system(f"aplay {audio_file_path}")
# Or if it returns bytes and you have a way to play bytes:
# play_wav_bytes(audio_bytes)
print(f"[SIMULATE PLAYING TTS]: {text}") # 模拟播放
except Exception as e:
logging.error(f"Error playing TTS audio: {e}")
else:
logging.warning("TTS failed, no audio data received.")
async def play_feedback_sound(self, sound_type: str):
"""播放预设的提示音 (如确认、错误)。"""
# sound_path = self.config['feedback_sounds'].get(sound_type)
# if sound_path and os.path.exists(sound_path):
# os.system(f"aplay {sound_path}") # Or use a python library
logging.info(f"Playing feedback sound: {sound_type}")
print(f"[SIMULATE FEEDBACK SOUND]: {sound_type}") # 模拟播放
def is_keyword_detected(self) -> bool:
"""(可选) 实现或集成关键词唤醒逻辑。"""
# frame = self.record_short_frame_for_kws()
# result = self.keyword_spotter.process(frame)
# if result >= 0:
# logging.info("Keyword detected!")
# return True
# return False
# 简化:假设总是未检测到,由主循环其他逻辑触发交互
return False
会看 (Vision Processing - vision_processor.py
)
# vision_processor.py
import cv2
# from picamera2 import Picamera2 # For Raspberry Pi Camera Module
# from k230_client import K230Client # If K230 is used
# import face_recognition # For local face recognition, if not using K230 for it
class VisionProcessor:
def __init__(self, camera_config, k230_client=None, face_recognition_config=None):
# self.picam2 = Picamera2()
# camera_params = camera_config.get('params', {"size": (640, 480), "format": "RGB888"})
# self.picam2.configure(self.picam2.create_preview_configuration(main=camera_params))
# self.picam2.start()
self.k230_client = k230_client
self.face_db = {} # person_id: [face_encoding1, face_encoding2,...]
# if face_recognition_config and face_recognition_config.get('load_db_on_init'):
# self.load_face_db(face_recognition_config.get('db_path'))
logging.info("VisionProcessor initialized.") # Add camera init log
def capture_frame(self): # -> numpy_array_frame or None:
"""从摄像头捕获一帧图像。"""
# frame = self.picam2.capture_array()
# return frame
# 模拟捕获
logging.debug("Simulating frame capture.")
return np.zeros((480, 640, 3), dtype=np.uint8) # Placeholder
def capture_frame_for_vqa(self): # -> base64_string or None
"""捕获一帧并编码为Base64,用于视觉问答。"""
frame = self.capture_frame()
if frame is not None:
_, buffer = cv2.imencode('.jpg', frame)
return base64.b64encode(buffer).decode('utf-8')
return None
async def get_current_scene_description(self, qwen_api_handler, frame=None) -> str or None:
"""(可选) 调用Qwen Vision API描述当前场景。"""
if frame is None:
frame = self.capture_frame()
if frame is not None:
# Convert frame to base64 or get URL if API supports it
_, buffer = cv2.imencode('.jpg', frame)
img_base64 = base64.b64encode(buffer).decode('utf-8')
description = await qwen_api_handler.image_understanding(img_base64, "详细描述这张图片里的场景。")
return description.get("text_response") if description else "无法描述场景。"
return "无法获取图像。"
def process_frame_for_faces_and_tracking(self, frame, is_currently_tracking, target_id_to_track): # -> dict
"""
处理单帧图像,进行人脸检测、识别(如果配置)和跟踪。
返回: {'faces': [{'id': str, 'bbox': [x,y,w,h], 'is_target': bool}, ...], 'target_lost': bool}
"""
detected_faces_info = {'faces': [], 'target_lost': False}
if self.k230_client:
# K230处理逻辑: 发送帧,接收结果
# results_from_k230 = self.k230_client.request_vision_processing(frame, target_id_to_track, is_currently_tracking)
# detected_faces_info = parse_k230_results(results_from_k230) # 解析K230返回的数据
pass # Placeholder for K230 logic
else:
# 本地OpenCV处理逻辑
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
# faces_bboxes = face_cascade.detectMultiScale(gray, 1.1, 4)
faces_bboxes = [] # Placeholder for actual detection
for bbox in faces_bboxes:
(x, y, w, h) = bbox
face_info = {'bbox': bbox}
# 可选: 本地人脸识别
# face_img_roi = frame[y:y+h, x:x+w]
# recognized_id = self.local_recognize_face(face_img_roi) # 假设有此方法
# face_info['id'] = recognized_id
# face_info['is_target'] = (recognized_id == target_id_to_track)
detected_faces_info['faces'].append(face_info)
if is_currently_tracking and not any(f.get('is_target') for f in detected_faces_info['faces']):
detected_faces_info['target_lost'] = True
# 模拟返回
# if target_id_to_track and np.random.rand() > 0.3: # 模拟找到目标
# sim_x, sim_y, sim_w, sim_h = 100,100,50,50
# detected_faces_info['faces'].append({'id': target_id_to_track, 'bbox': [sim_x,sim_y,sim_w,sim_h], 'is_target': True})
# elif is_currently_tracking:
# detected_faces_info['target_lost'] = True
return detected_faces_info
# 其他方法如: load_face_db, local_recognize_face, detect_apriltag 等
K230客户端 (K230 Integration - k230_client.py
) (如果使用K230)
# k230_client.py
import socket
import json # Or other serialization format like protobuf
import logging
class K230Client:
def __init__(self, ip, port):
self.server_ip = ip
self.server_port = port
self.socket = None
logging.info(f"K230Client configured for {ip}:{port}")
def connect(self):
try:
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.connect((self.server_ip, self.server_port))
logging.info("Connected to K230 server.")
return True
except socket.error as e:
logging.error(f"Failed to connect to K230 server: {e}")
self.socket = None
return False
def request_vision_processing(self, frame_bytes, target_id=None, is_tracking=False) -> dict or None:
"""向K230发送图像和指令,请求视觉处理结果。"""
if not self.socket:
if not self.connect():
return None
try:
# 1. 构建请求 (示例: JSON)
request_data = {
"command": "process_frame",
"image_format": "jpeg", # 假设发送JPEG编码的图像
"image_len": len(frame_bytes),
"target_id": target_id,
"is_tracking_active": is_tracking
}
# 发送请求头 (JSON)
self.socket.sendall(json.dumps(request_data).encode('utf-8') + b'\n') # 加换行符作为分隔
# 发送图像数据
self.socket.sendall(frame_bytes)
# 2. 接收响应
# 假设K230先发送响应头(如JSON),再发送数据
response_header_str = b""
while True: # Read until newline for header
byte = self.socket.recv(1)
if byte == b'\n' or not byte: break
response_header_str += byte
if not response_header_str:
logging.warning("K230 did not send response header.")
return None
response_header = json.loads(response_header_str.decode('utf-8'))
# 假设响应包含检测到的人脸信息等
# response_data_len = response_header.get("data_len", 0)
# if response_data_len > 0:
# response_payload = self.socket.recv(response_data_len)
# return json.loads(response_payload.decode('utf-8'))
return response_header # 简化:假设头部即为结果
except (socket.error, json.JSONDecodeError, Exception) as e:
logging.error(f"Error communicating with K230: {e}")
self.close() # 通信错误时关闭连接,下次重连
return None
def close(self):
if self.socket:
self.socket.close()
self.socket = None
logging.info("Disconnected from K230 server.")
长期记忆 (Memory System - memory_manager.py
)
记忆系统是赋予小车“灵魂感”和上下文理解能力的核心。我们将结合使用结构化日志和基于向量的语义记忆。可以考虑使用LangChain的Memory模块作为基础。
# memory_manager.py
import sqlite3
import datetime
import json
import logging
# from langchain.memory import ConversationBufferWindowMemory, VectorStoreRetrieverMemory
# from langchain.embeddings import HuggingFaceEmbeddings # Or OpenAIEmbeddings via Qwen API
# from langchain.vectorstores import Chroma # Or FAISS
class MemoryManager:
def __init__(self, db_path, qwen_api_handler):
self.db_path = db_path
self.qwen_api = qwen_api_handler # For embeddings or summarization
self.conn = sqlite3.connect(db_path)
self.cursor = self.conn.cursor()
self._create_tables()
# self.short_term_memory = ConversationBufferWindowMemory(k=10, return_messages=True)
# self.embeddings_model = HuggingFaceEmbeddings(model_name="shibing624/text2vec-base-chinese") # Example
# self.vector_store = Chroma(persist_directory="./chroma_db", embedding_function=self.embeddings_model)
# self.long_term_memory = VectorStoreRetrieverMemory(retriever=self.vector_store.as_retriever())
logging.info(f"MemoryManager initialized with DB: {db_path}")
def _create_tables(self):
self.cursor.execute('''
CREATE TABLE IF NOT EXISTS interactions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
role TEXT NOT NULL,
text TEXT NOT NULL,
mood TEXT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
)
''')
self.cursor.execute('''
CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
event_type TEXT NOT NULL,
data_json TEXT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
)
''')
self.conn.commit()
async def add_interaction_log(self, role: str, text: str, mood: str = None):
"""记录用户和小车的对话。"""
timestamp = datetime.datetime.now()
self.cursor.execute("INSERT INTO interactions (role, text, mood, timestamp) VALUES (?, ?, ?, ?)",
(role, text, mood, timestamp.isoformat()))
self.conn.commit()
# self.short_term_memory.save_context({"input": text} if role == "user" else {}, {"output": text} if role == "ai" else {})
# For long-term memory, embed and store
# doc_to_embed = f"[{timestamp.strftime('%Y-%m-%d %H:%M')}] {role} (mood: {mood}): {text}"
# self.vector_store.add_texts([doc_to_embed])
logging.debug(f"Logged interaction: {role} - {text}")
async def add_event_log(self, event_type: str, data_dict: dict):
"""记录环境变化、重要行为等事件。"""
timestamp = datetime.datetime.now()
self.cursor.execute("INSERT INTO events (event_type, data_json, timestamp) VALUES (?, ?, ?)",
(event_type, json.dumps(data_dict), timestamp.isoformat()))
self.conn.commit()
logging.debug(f"Logged event: {event_type} - {data_dict}")
async def get_recent_interactions(self, limit=10) -> list:
"""获取最近的对话历史。"""
self.cursor.execute("SELECT role, text, mood, timestamp FROM interactions ORDER BY timestamp DESC LIMIT ?", (limit,))
return [{"role": r, "text": t, "mood": m, "timestamp": ts} for r, t, m, ts in self.cursor.fetchall()]
async def get_recent_interactions_summary(self, limit=5) -> str:
"""获取最近对话的摘要 (可调用LLM)。"""
recent_interactions = await self.get_recent_interactions(limit)
if not recent_interactions:
return "最近没有对话。"
# 构造摘要请求的上下文
history_for_summary = "\n".join([f"{i['role']}: {i['text']}" for i in reversed(recent_interactions)])
# prompt = f"请简要总结以下对话内容,不超过50字:\n{history_for_summary}\n总结:"
# summary_response = await self.qwen_api.generate_simple_text_response(prompt) # 假设有此方法
# return summary_response.get("text_response", "无法生成摘要。")
return f"最近有{len(recent_interactions)}条对话。" # 简化版
async def search_memories_by_keywords(self, query_text: str, k=3) -> list:
"""基于关键词搜索结构化记忆 (SQLite FTS5会更好)。"""
# This is a very basic keyword search. For real semantic search, use vector DB.
self.cursor.execute("SELECT role, text, mood, timestamp FROM interactions WHERE text LIKE ? ORDER BY timestamp DESC LIMIT ?",
(f"%{query_text}%", k))
results = [{"role": r, "text": t, "mood": m, "timestamp": ts} for r, t, m, ts in self.cursor.fetchall()]
logging.debug(f"Keyword search for '{query_text}' found {len(results)} items.")
return results
# async def search_semantic_memories(self, query_text: str, k=3) -> list:
# """基于语义相似度搜索向量化记忆。"""
# docs = self.vector_store.similarity_search(query_text, k=k)
# return [doc.page_content for doc in docs]
async def close_db(self):
if self.conn:
self.conn.close()
logging.info("MemoryManager database connection closed.")
个性 (Personality Engine - personality_engine.py
)
# personality_engine.py
import json
import random
import logging
class PersonalityEngine:
def __init__(self, profile_path):
try:
with open(profile_path, 'r', encoding='utf-8') as f:
self.profile = json.load(f)
except FileNotFoundError:
logging.error(f"Personality profile not found at {profile_path}. Using default.")
self.profile = { # Default fallback personality
"name": "小助手",
"base_persona": "我是一个乐于助人的智能小车。",
"moods": {
"neutral": {"greeting": "你好!", "farewell": "再见!", "voice_pitch_factor": 1.0, "response_style_suffix": ""},
"happy": {"greeting": "你好呀,见到你真开心!", "farewell": "下次再聊哦,拜拜!", "voice_pitch_factor": 1.1, "response_style_suffix": " 😄"},
"tired": {"greeting": "唔...你好...", "farewell": "我需要休息了,再见。", "voice_pitch_factor": 0.9, "response_style_suffix": " 😴"},
"curious": {"greeting": "咦?你好!有什么新发现吗?", "farewell": "期待下次探索!", "voice_pitch_factor": 1.05, "response_style_suffix": " 🤔"}
},
"proactive_alerts_templates": {
"low_battery_critical": "哎呀,我的电池只剩下 {level}% 了,快动不了了,得赶紧找地方充电!",
"rain_warning": "外面好像要下雨了,记得带伞哦!"
},
"greetings_templates": {
"default_known_user": "{user_name},你好!又见面啦!",
"default_unknown_user": "你好!我们以前见过吗?"
},
"quirks": ["嗯哼", "那个...", "你知道吗?"] # 口头禅
}
self.current_mood = self.profile['moods'].get('neutral', list(self.profile['moods'].values())[0]) # Default to neutral or first mood
logging.info(f"PersonalityEngine loaded profile: {self.profile.get('name')}")
def get_initial_mood(self) -> str:
return "neutral" # Or load from a saved state
def update_mood(self, current_mood_name: str, env_data: dict, battery_level_percentage: float) -> str:
"""根据规则调整心情。返回新的心情名称。"""
# 示例规则:
if battery_level_percentage < 20:
return "tired"
temp = env_data.get("temperature_celsius")
if temp is not None:
if temp > 30: # 太热可能烦躁 (简化为tired)
return "tired"
elif temp < 10: # 太冷也可能不舒服 (简化为tired)
return "tired"
# 随机从开心或中性切换,模拟情绪波动
if random.random() < 0.1: # 10% 概率切换到开心(如果不是疲惫)
if current_mood_name != "tired": return "happy"
# 默认保持或回到中性
if current_mood_name == "tired" and battery_level_percentage > 50: # 电量恢复则不再疲惫
return "neutral"
return current_mood_name # 保持当前心情
def get_system_prompt(self, mood_name: str) -> str:
"""为Qwen3 API生成包含当前个性和情绪的系统提示。"""
mood_details = self.profile['moods'].get(mood_name, self.profile['moods']['neutral'])
prompt = f"{self.profile['base_persona']} 我的名字是{self.profile['name']}。我现在的心情是'{mood_name}',所以我的回答可能会带有一些{mood_name}的风格。"
# 可以根据mood_details进一步丰富prompt
return prompt
def personalize_text(self, base_text: str, mood_name: str) -> str:
"""根据当前情绪和个性配置,调整LLM生成文本的措辞、加入口头禅等。"""
mood_details = self.profile['moods'].get(mood_name, self.profile['moods']['neutral'])
personalized_text = base_text
if random.random() < 0.3: # 30%概率加个口头禅
quirk = random.choice(self.profile.get('quirks', []))
if quirk:
personalized_text = f"{quirk} {personalized_text}"
personalized_text += mood_details.get("response_style_suffix", "")
return personalized_text
def get_voice_options(self, mood_name: str) -> dict:
"""根据心情获取TTS的语音参数建议。"""
mood_details = self.profile['moods'].get(mood_name, self.profile['moods']['neutral'])
return {
"pitch_factor": mood_details.get("voice_pitch_factor", 1.0),
# "speed_factor": mood_details.get("voice_speed_factor", 1.0),
# "voice_name": mood_details.get("preferred_voice_timbre") # 如果API支持选择音色
}
def generate_proactive_alert(self, alert_type: str, context_data: dict) -> str:
"""生成个性化的主动提醒文本。"""
template = self.profile.get('proactive_alerts_templates', {}).get(alert_type, "我有一个提醒:{message}")
try:
return template.format(**context_data)
except KeyError: # 如果模板中的占位符在context_data中找不到
return template.replace("{level}", str(context_data.get("level","未知"))).replace("{message}", "请注意")
def generate_greeting(self, person_id: str or None, mood_name: str) -> str:
"""生成个性化的问候语。"""
mood_details = self.profile['moods'].get(mood_name, self.profile['moods']['neutral'])
greeting_base = mood_details.get("greeting", "你好!")
if person_id: # 如果识别到特定用户
user_name = self.profile.get("known_users", {}).get(person_id, {}).get("name", person_id)
template = self.profile.get('greetings_templates', {}).get("default_known_user", "{user_name},你好!")
return f"{greeting_base} {template.format(user_name=user_name)}"
else: # 未识别到用户或新用户
template = self.profile.get('greetings_templates', {}).get("default_unknown_user", "你好呀!")
return f"{greeting_base} {template}"
环境感知与心情调节 (Environment & Mood - environment_monitor.py
)
# environment_monitor.py
import logging
# import adafruit_bme280 # For BME280 sensor
# import board # For CircuitPython board definition
# import busio # For I2C
class EnvironmentMonitor:
def __init__(self, config):
self.config = config
# self.i2c = busio.I2C(board.SCL, board.SDA)
# self.bme280 = adafruit_bme280.Adafruit_BME280_I2C(self.i2c, address=config.get('bme280_address', 0x76))
# self.light_sensor_config = config.get('light_sensor') # e.g. BH1750
self.current_data = {} # Store last read data
self.monitoring_task = None
logging.info("EnvironmentMonitor initialized.")
async def _read_sensors_periodically(self):
while True:
temp_c = None
humidity = None
pressure_hpa = None
light_lux = None
try:
# temp_c = self.bme280.temperature
# humidity = self.bme280.humidity
# pressure_hpa = self.bme280.pressure
# Placeholder values
temp_c = 25.0 + random.uniform(-2, 2)
humidity = 50.0 + random.uniform(-10, 10)
pressure_hpa = 1012.0 + random.uniform(-5, 5)
light_lux = 300 + random.uniform(-50, 50)
self.current_data = {
"temperature_celsius": round(temp_c, 1) if temp_c is not None else None,
"humidity_percent": round(humidity, 1) if humidity is not None else None,
"pressure_hpa": round(pressure_hpa, 1) if pressure_hpa is not None else None,
"light_lux": round(light_lux, 1) if light_lux is not None else None,
}
logging.debug(f"Environment data updated: {self.current_data}")
except Exception as e:
logging.warning(f"Failed to read environment sensors: {e}")
await asyncio.sleep(self.config.get('read_interval_seconds', 60))
def start_monitoring(self):
if self.monitoring_task is None or self.monitoring_task.done():
self.monitoring_task = asyncio.create_task(self._read_sensors_periodically())
logging.info("Environment monitoring started.")
def get_current_status(self) -> dict:
"""读取所有传感器数据。"""
return self.current_data.copy() # Return a copy
async def get_external_weather_data(self, qwen_api_handler, location: str) -> dict or None:
"""(可选) 通过Qwen API的联网能力或调用天气API获取天气。"""
logging.info(f"Fetching external weather for {location}...")
# prompt = f"查询{location}当前的天气情况,并以JSON格式返回,包含温度(celsius)、天气描述(description)、湿度(humidity_percent)。"
# weather_response = await qwen_api_handler.generate_simple_text_response(prompt, use_search=True) # 假设API支持联网搜索
# if weather_response and weather_response.get("text_response"):
# try:
# weather_data = json.loads(weather_response["text_response"])
# self.current_data['weather_api'] = weather_data # Cache it
# return weather_data
# except json.JSONDecodeError:
# logging.warning(f"Failed to parse weather API JSON response: {weather_response['text_response']}")
# return None
logging.warning("External weather API not fully implemented in this example.")
return {"temperature_celsius": 28, "description": "晴朗", "humidity_percent": 60} # Placeholder
def stop_monitoring(self):
if self.monitoring_task and not self.monitoring_task.done():
self.monitoring_task.cancel()
logging.info("Environment monitoring stopped.")
电源管理 (Power Management - power_manager.py
)
# power_manager.py
import logging
import asyncio
# import adafruit_ina219 # For INA219 sensor
# import board
# import busio
class PowerManager:
def __init__(self, config):
self.config = config
# self.i2c = busio.I2C(board.SCL, board.SDA)
# self.ina219 = adafruit_ina219.INA219(self.i2c, address=config.get('ina219_address', 0x40))
self.battery_voltage = 0.0
self.battery_current_ma = 0.0 # Positive for charging, negative for discharging
self.battery_percentage = 100.0 # Initial assumption
self.is_currently_charging = False
self.min_voltage = config.get('battery_min_voltage', 3.0) # e.g., for LiPo cell
self.max_voltage = config.get('battery_max_voltage', 4.2)
self.critical_low_threshold_percent = config.get('critical_low_threshold_percent', 15)
self.low_threshold_percent = config.get('low_threshold_percent', 30)
self.monitoring_task = None
logging.info("PowerManager initialized.")
async def _monitor_battery_periodically(self):
while True:
try:
# self.battery_voltage = self.ina219.bus_voltage + self.ina219.shunt_voltage / 1000 # V
# self.battery_current_ma = self.ina219.current # mA
# Placeholder values
self.battery_voltage = random.uniform(self.min_voltage - 0.1, self.max_voltage + 0.1)
if self.is_currently_charging:
self.battery_current_ma = random.uniform(300, 800) # Charging current
self.battery_percentage = min(100, self.battery_percentage + 0.5) # Simulate charging
if self.battery_percentage >= 100: self.is_currently_charging = False # Simulate full charge
else:
self.battery_current_ma = random.uniform(-200, -50) # Discharging current
self.battery_percentage = max(0, self.battery_percentage - 0.1) # Simulate discharging
# Update percentage based on voltage (simple linear mapping)
# self.battery_percentage = max(0, min(100, ((self.battery_voltage - self.min_voltage) / (self.max_voltage - self.min_voltage)) * 100))
# self.is_currently_charging = self.battery_current_ma > self.config.get('charging_current_threshold_ma', 50)
logging.debug(f"Battery: {self.battery_voltage:.2f}V, {self.battery_current_ma:.0f}mA, {self.battery_percentage:.1f}%")
except Exception as e:
logging.warning(f"Failed to read INA219: {e}")
await asyncio.sleep(self.config.get('read_interval_seconds', 10))
def start_monitoring(self):
if self.monitoring_task is None or self.monitoring_task.done():
self.monitoring_task = asyncio.create_task(self._monitor_battery_periodically())
logging.info("Battery monitoring started.")
def get_battery_percentage(self) -> float:
return round(self.battery_percentage, 1)
def get_battery_voltage(self) -> float:
return round(self.battery_voltage, 2)
def is_low_battery(self) -> bool:
return self.battery_percentage < self.low_threshold_percent
def is_critically_low(self) -> bool:
return self.battery_percentage < self.critical_low_threshold_percent
def is_charging(self) -> bool:
# This could also be from a dedicated charging status pin if available
return self.is_currently_charging
def set_charging_status(self, charging: bool): # Called by AutoChargeController
self.is_currently_charging = charging
if charging:
logging.info("Battery charging started.")
else:
logging.info("Battery charging stopped/not detected.")
def stop_monitoring(self):
if self.monitoring_task and not self.monitoring_task.done():
self.monitoring_task.cancel()
logging.info("Battery monitoring stopped.")
运动控制 (Motion Control - motion_controller.py
)
# motion_controller.py
import logging
# import RPi.GPIO as GPIO # Or use gpiozero for higher-level control
# from gpiozero import Motor, Robot
class MotionController:
def __init__(self, config):
self.config = config
# Motor pins (example for L298N or similar dual H-bridge)
# self.motor_left_fwd_pin = config['pins']['left_fwd']
# self.motor_left_bwd_pin = config['pins']['left_bwd']
# self.motor_left_pwm_pin = config['pins']['left_pwm'] # Enable/Speed
# self.motor_right_fwd_pin = config['pins']['right_fwd']
# self.motor_right_bwd_pin = config['pins']['right_bwd']
# self.motor_right_pwm_pin = config['pins']['right_pwm']
# GPIO.setmode(GPIO.BCM) # Or GPIO.BOARD
# GPIO.setup([self.motor_left_fwd_pin, self.motor_left_bwd_pin, self.motor_left_pwm_pin,
# self.motor_right_fwd_pin, self.motor_right_bwd_pin, self.motor_right_pwm_pin], GPIO.OUT)
# self.pwm_left = GPIO.PWM(self.motor_left_pwm_pin, 100) # 100 Hz PWM frequency
# self.pwm_right = GPIO.PWM(self.motor_right_pwm_pin, 100)
# self.pwm_left.start(0) # Start with 0% duty cycle (stopped)
# self.pwm_right.start(0)
# Using gpiozero (simpler)
# self.robot = Robot(left=(self.motor_left_fwd_pin, self.motor_left_bwd_pin),
# right=(self.motor_right_fwd_pin, self.motor_right_bwd_pin))
# For PWM control with gpiozero, you might use Motor class directly for each wheel
# self.left_motor = Motor(forward=self.motor_left_fwd_pin, backward=self.motor_left_bwd_pin, enable=self.motor_left_pwm_pin)
# self.right_motor = Motor(forward=self.motor_right_fwd_pin, backward=self.motor_right_bwd_pin, enable=self.motor_right_pwm_pin)
self.default_speed = config.get('default_speed_percent', 50) / 100.0 # Convert to 0-1 scale for gpiozero
self.current_left_speed = 0
self.current_right_speed = 0
logging.info("MotionController initialized.")
def _set_motor_speeds(self, left_speed_percent, right_speed_percent):
"""Internal method to set motor speeds. Speed is -100 to 100."""
self.current_left_speed = left_speed_percent
self.current_right_speed = right_speed_percent
logging.debug(f"Setting motors: Left={left_speed_percent}%, Right={right_speed_percent}%")
# gpiozero example:
# left_val = left_speed_percent / 100.0
# right_val = right_speed_percent / 100.0
# if left_val > 0: self.left_motor.forward(speed=left_val)
# elif left_val < 0: self.left_motor.backward(speed=abs(left_val))
# else: self.left_motor.stop()
# Similar for right_motor
# RPi.GPIO example:
# Convert percent to PWM duty cycle (0-100)
# self.pwm_left.ChangeDutyCycle(abs(left_speed_percent))
# self.pwm_right.ChangeDutyCycle(abs(right_speed_percent))
# Set direction pins accordingly...
print(f"[SIMULATE MOTORS]: Left={left_speed_percent}, Right={right_speed_percent}")
def move_forward(self, speed_percent=None):
speed = speed_percent if speed_percent is not None else self.default_speed * 100
self._set_motor_speeds(speed, speed)
logging.info(f"Moving forward at {speed}%.")
def move_backward(self, speed_percent=None):
speed = speed_percent if speed_percent is not None else self.default_speed * 100
self._set_motor_speeds(-speed, -speed)
logging.info(f"Moving backward at {speed}%.")
def turn_left(self, speed_percent=None): # Pivot turn
speed = speed_percent if speed_percent is not None else self.default_speed * 100
self._set_motor_speeds(-speed, speed)
logging.info(f"Turning left at {speed}%.")
def turn_right(self, speed_percent=None): # Pivot turn
speed = speed_percent if speed_percent is not None else self.default_speed * 100
self._set_motor_speeds(speed, -speed)
logging.info(f"Turning right at {speed}%.")
def stop(self):
self._set_motor_speeds(0, 0)
logging.info("Motors stopped.")
def set_motors(self, left_speed_percent, right_speed_percent):
"""Directly control left and right motor speeds (-100 to 100)."""
self._set_motor_speeds(left_speed_percent, right_speed_percent)
def calculate_follow_commands(self, target_bbox, frame_center_x, frame_width) -> dict:
"""
PID控制算法使目标保持在画面中央。
target_bbox: [x, y, w, h] of the target.
frame_center_x: center x-coordinate of the frame.
frame_width: width of the frame.
Returns: {'left_speed': int, 'right_speed': int} percentages.
"""
target_center_x = target_bbox[0] + target_bbox[2] / 2
error = frame_center_x - target_center_x
# Simple P controller for turning
turn_effort_max = 50 # Max turning speed differential
# Normalize error to -1 to 1 range based on half frame width
normalized_error = error / (frame_width / 2.0)
turn_speed = normalized_error * turn_effort_max
# Proximity control (simple: larger bbox means closer, slow down)
# A more robust proximity would use depth sensor or bbox height relative to known size
target_area_ratio = (target_bbox[2] * target_bbox[3]) / (frame_width * frame_width) # Rough area ratio
base_forward_speed = self.default_speed * 70 # Base speed when following
# If target is large (close), reduce forward speed or even stop/move_back
if target_area_ratio > 0.1: # Example threshold, needs tuning
base_forward_speed *= 0.3 # Slow down significantly if very close
elif target_area_ratio > 0.05:
base_forward_speed *= 0.7
# Combine forward speed with turn speed
# If turning sharply, might reduce overall forward speed
left_speed = base_forward_speed - turn_speed
right_speed = base_forward_speed + turn_speed
# Clamp speeds to -100 to 100
left_speed = max(-100, min(100, left_speed))
right_speed = max(-100, min(100, right_speed))
logging.debug(f"Follow cmd: err={error:.1f}, turn={turn_speed:.1f}, L={left_speed:.0f}, R={right_speed:.0f}")
return {'left_speed': int(left_speed), 'right_speed': int(right_speed)}
def cleanup(self):
# GPIO.cleanup() # If using RPi.GPIO directly
# if self.left_motor: self.left_motor.close()
# if self.right_motor: self.right_motor.close()
logging.info("MotionController GPIO cleaned up.")
自动回基座充电 (Auto Charging - auto_charge_controller.py
)
# auto_charge_controller.py
import logging
import asyncio
class AutoChargeController:
def __init__(self, config, vision_processor, motion_controller, power_manager):
self.config = config
self.vision_processor = vision_processor
self.motion_controller = motion_controller
self.power_manager = power_manager
self.is_docking_active = False
self.docking_status = "idle" # e.g., idle, searching_tag, aligning_ir, docked
self.apriltag_id_dock = config.get('apriltag_id_for_dock')
logging.info("AutoChargeController initialized.")
def is_charging_or_docking(self) -> bool:
return self.power_manager.is_charging() or self.is_docking_active
async def start_docking_sequence(self):
if self.is_docking_active or self.power_manager.is_charging():
logging.info("Docking sequence already active or already charging.")
return
logging.info("Starting auto docking sequence...")
self.is_docking_active = True
self.docking_status = "searching_tag"
try:
# Phase 1: Find general direction of dock using AprilTag
tag_found, tag_pose = await self._find_apriltag_dock()
if not tag_found:
logging.warning("Docking failed: AprilTag not found after search.")
await self._handle_docking_failure("apriltag_not_found")
return
# Phase 2: Navigate towards dock using AprilTag visual servoing
self.docking_status = "navigating_to_tag"
nav_success = await self._navigate_to_apriltag(tag_pose) # This would be a loop
if not nav_success:
logging.warning("Docking failed: Navigation to AprilTag failed.")
await self._handle_docking_failure("navigation_failed")
return
# Phase 3: Fine alignment and docking (e.g., using IR or mechanical guides)
self.docking_status = "fine_aligning"
align_success = await self._fine_align_and_dock()
if not align_success:
logging.warning("Docking failed: Fine alignment failed.")
await self._handle_docking_failure("alignment_failed")
return
# Phase 4: Verify charging connection
self.docking_status = "verifying_charge"
await asyncio.sleep(2) # Wait a bit for charging to stabilize
if self.power_manager.is_charging():
logging.info("Docking successful! Charging started.")
self.docking_status = "docked_charging"
# Notify SmartCarCore or PersonalityEngine
else:
logging.warning("Docking failed: Charging not detected after docking.")
await self._handle_docking_failure("charging_not_detected")
# Maybe try to wiggle or re-align slightly?
except Exception as e:
logging.error(f"Exception during docking sequence: {e}", exc_info=True)
await self._handle_docking_failure("exception_in_docking")
finally:
self.is_docking_active = False
if not self.power_manager.is_charging(): # If not charging after attempt
self.docking_status = "docking_failed"
async def _find_apriltag_dock(self, search_timeout_sec=30) -> (bool, dict or None):
"""Search for the AprilTag associated with the dock."""
logging.info(f"Searching for AprilTag ID: {self.apriltag_id_dock}...")
start_time = time.time()
while time.time() - start_time < search_timeout_sec:
frame = self.vision_processor.capture_frame()
if frame is None:
await asyncio.sleep(0.1)
continue
# tags = self.vision_processor.detect_apriltags(frame) # Assume this method exists
# For simulation:
tags = []
if random.random() < 0.2: # Simulate finding tag sometimes
tags.append({'id': self.apriltag_id_dock, 'pose': {'x':0.5, 'y':0.1, 'z':1.0, 'yaw':0.1}})
for tag in tags:
if tag.get('id') == self.apriltag_id_dock:
logging.info(f"AprilTag dock ID {self.apriltag_id_dock} found. Pose: {tag.get('pose')}")
return True, tag.get('pose')
# If not found, maybe perform a slow rotation or small movements
# self.motion_controller.turn_left(speed_percent=20) # Gentle turn
# await asyncio.sleep(0.5)
# self.motion_controller.stop()
await asyncio.sleep(0.2) # Wait before next frame
return False, None
async def _navigate_to_apriltag(self, initial_pose, max_duration_sec=60) -> bool:
"""Navigate towards the AprilTag using visual servoing."""
logging.info(f"Navigating towards AprilTag at initial pose: {initial_pose}")
# This would be a loop, continuously getting frames, detecting tag, calculating error, and moving
# until a certain distance/alignment criteria is met (e.g., tag fills certain portion of screen, centered)
# Simplified:
await asyncio.sleep(5) # Simulate navigation time
# Assume success if it reaches here for now
logging.info("Navigation towards AprilTag (simulated) complete.")
return True
async def _fine_align_and_dock(self, max_attempts=3) -> bool:
"""Use IR sensors or mechanical guides for final alignment and physical docking."""
logging.info("Performing fine alignment and docking...")
# This would involve reading IR proximity sensors, or specific docking sensors
# and making small precise movements.
# Simplified:
for attempt in range(max_attempts):
# self.motion_controller.move_forward(speed_percent=10) # Slow approach
# await asyncio.sleep(1)
# self.motion_controller.stop()
# Check IR sensors or contact sensors
# if self.are_docking_contacts_made():
if random.random() < 0.7: # Simulate successful contact
logging.info(f"Fine alignment attempt {attempt+1} successful (simulated).")
self.power_manager.set_charging_status(True) # Simulate charging starts
return True
logging.warning(f"Fine alignment attempt {attempt+1} failed (simulated).")
# self.motion_controller.move_backward(speed_percent=10) # Back off slightly
# await asyncio.sleep(0.5)
# self.motion_controller.stop()
# await asyncio.sleep(0.5) # Wait before retry
return False
async def _handle_docking_failure(self, reason: str):
logging.error(f"Docking sequence failed: {reason}")
self.is_docking_active = False
self.docking_status = f"failed_{reason}"
# Optionally, try to alert user or retry after some time
# alert_text = f"我回家充电失败了,原因是{reason}。"
# await self.audio_processor.text_to_speech_and_play(alert_text, ...)
def cancel_docking(self):
if self.is_docking_active:
logging.info("Docking sequence cancelled by user or system.")
self.is_docking_active = False
self.docking_status = "cancelled"
self.motion_controller.stop()
# Any other cleanup for ongoing docking steps
Qwen API 封装 (Qwen API Handling - qwen_api_handler.py
)
# qwen_api_handler.py
import httpx # Preferred for async requests
import json
import logging
import base64
class QwenAPIHandler:
def __init__(self, api_key, base_url=None):
self.api_key = api_key
# Default to Alibaba Cloud DashScope OpenAI-compatible endpoint if not specified
self.base_url = base_url if base_url else "https://dashscope.aliyuncs.com/compatible-mode/v1"
self.headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
# Specific model names, should be configurable
self.stt_model = "whisper-large-v3" # Example, check DashScope for actual STT model names or use dedicated STT API
self.tts_model = "sambert-zhichu-v1" # Example, check DashScope for actual TTS model names or use dedicated TTS API
self.chat_model = "qwen-plus" # Default chat model
self.vision_model = "qwen-vl-plus" # Default vision model
self.omni_model = "qwen-omni-pro" # Default omni model (text, image, audio in/out)
logging.info(f"QwenAPIHandler initialized for base URL: {self.base_url}")
async def _make_api_request(self, endpoint_suffix, payload, stream=False, timeout=30):
url = f"{self.base_url}{endpoint_suffix}"
async with httpx.AsyncClient(timeout=timeout) as client:
try:
logging.debug(f"API Request to {url} with payload: {json.dumps(payload, ensure_ascii=False)[:200]}...") # Log truncated payload
if stream:
# Streaming response handling
response_content = ""
async with client.stream("POST", url, headers=self.headers, json=payload) as response:
response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx)
async for chunk in response.aiter_text(): # Assuming text stream for now
# Parse Server-Sent Events (SSE) if API uses that for streaming
# For simplicity, just concatenating chunks. Real SSE parsing is more complex.
# Example: if chunk.startswith("data: "): process_sse_data(chunk)
response_content += chunk
# This simplified stream handling might need adjustment based on actual API stream format
# Often, for OpenAI compatible streams, you'd parse JSON objects from the stream.
# For now, assume the concatenated content is a single JSON or needs further parsing.
# A better approach for OpenAI compatible streams:
# full_response = ""
# async for line in response.aiter_lines():
# if line.startswith('data: '):
# data_json_str = line[len('data: '):]
# if data_json_str.strip() == "[DONE]": break
# try:
# chunk_json = json.loads(data_json_str)
# content_part = chunk_json.get("choices", [{}])[0].get("delta", {}).get("content", "")
# full_response += content_part
# except json.JSONDecodeError:
# logging.warning(f"Failed to decode stream chunk: {data_json_str}")
# return {"text_response": full_response} # Simplified
return {"streamed_content": response_content} # Placeholder for actual stream parsing
else:
response = await client.post(url, headers=self.headers, json=payload)
response.raise_for_status()
return response.json()
except httpx.HTTPStatusError as e:
logging.error(f"API HTTP error for {url}: {e.response.status_code} - {e.response.text}")
except httpx.RequestError as e:
logging.error(f"API Request error for {url}: {e}")
except json.JSONDecodeError as e:
logging.error(f"API JSON decode error for {url}: {e}")
except Exception as e:
logging.error(f"Unexpected API error for {url}: {e}", exc_info=True)
return None
async def speech_to_text(self, audio_bytes: bytes) -> str or None:
"""调用Qwen STT (或兼容的STT服务)。audio_bytes应为API接受的格式 (e.g., WAV, MP3)。"""
# This is a placeholder. Actual STT might use a different endpoint or payload structure.
# For example, DashScope has specific SDKs/APIs for ASR.
# If using OpenAI compatible Whisper API via DashScope:
# endpoint = "/audio/transcriptions"
# files = {'file': ('audio.wav', audio_bytes, 'audio/wav')}
# data = {'model': self.stt_model}
# async with httpx.AsyncClient() as client:
# response = await client.post(f"{self.base_url}{endpoint}", headers={"Authorization": f"Bearer {self.api_key}"}, files=files, data=data)
# if response.status_code == 200: return response.json().get("text")
logging.info("Simulating STT call...")
await asyncio.sleep(0.5) # Simulate API call latency
simulated_text = "你好小车,向前走。" if random.random() > 0.3 else "听不清楚"
logging.debug(f"STT simulated result: {simulated_text}")
return simulated_text
async def text_to_speech(self, text: str, voice_options: dict = None) -> bytes or None:
"""调用Qwen TTS (或兼容的TTS服务)。voice_options可包含音色、语速等。"""
# Placeholder. Actual TTS API call will vary.
# DashScope has specific SDKs/APIs for TTS.
# If Qwen-Omni is used for chat, it might return audio directly.
# payload = {"model": self.tts_model, "input": {"text": text}, "voice": voice_options.get("voice_name", "default_voice")}
# response_data = await self._make_api_request("/tts/...", payload) # Fictional endpoint
# if response_data and response_data.get("audio_content_base64"):
# return base64.b64decode(response_data["audio_content_base64"])
logging.info(f"Simulating TTS call for: {text}")
await asyncio.sleep(0.3) # Simulate API call latency
# Simulate returning WAV bytes for "Hello"
# This is a very short, silent WAV for placeholder purposes.
# A real TTS would return actual speech data.
# RIFF....WAVEfmt ....data.... (minimal valid WAV header for silence)
silent_wav_bytes = b'RIFF$\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01\x00\x01\x00\x80>\x00\x00\x00\xfa\x00\x00\x02\x00\x10\x00data\x00\x00\x00\x00'
logging.debug("TTS simulated, returning silent WAV bytes.")
return silent_wav_bytes
async def generate_multimodal_response(self, context_dict: dict, system_prompt: str) -> dict:
"""
调用Qwen大模型进行理解、决策和文本生成。
context_dict: 包含 user_input, current_mood, battery_level, environment_data, recent_memory, visual_context_image_base64
system_prompt: 由PersonalityEngine生成
返回: {'text_response': str, 'action_to_take': dict or None}
"""
messages = [{"role": "system", "content": system_prompt}]
user_content_parts = [{"type": "text", "text": context_dict.get("user_input", "")}]
# Add visual context if available
if context_dict.get("visual_context_image_base64"):
user_content_parts.append({
"type": "image_url", # OpenAI compatible format for image data
"image_url": {"url": f"data:image/jpeg;base64,{context_dict['visual_context_image_base64']}"}
})
# If Qwen-Omni is used, it might also accept audio input here.
# Add other context as text for now
# A more advanced approach would use RAG for memory or specific function calls for sensor data
context_text_parts = [
f"当前心情: {context_dict.get('current_mood', '未知')}",
f"电池电量: {context_dict.get('battery_level_percentage', '未知')}%",
f"环境: {json.dumps(context_dict.get('environment_data', {}), ensure_ascii=False)}",
f"最近对话摘要: {context_dict.get('recent_memory_summary', '无')}"
]
full_user_text = context_dict.get("user_input", "") + "\n[辅助信息]\n" + "\n".join(context_text_parts)
user_content_parts[0]["text"] = full_user_text # Update the text part
messages.append({"role": "user", "content": user_content_parts})
payload = {
"model": self.omni_model, # Or self.chat_model if no image/audio input
"messages": messages,
"stream": False, # For simplicity in this example, but True is better for UX
# "extra_body": {"enable_thinking": True} # If Qwen3 specific params needed
}
# If Qwen-Omni and expecting audio output:
# payload["modalities"] = ["text", "audio"]
# payload["audio"] = {"voice": "Cherry"} # Example voice
api_response = await self._make_api_request("/chat/completions", payload)
if api_response and api_response.get("choices"):
choice = api_response["choices"][0]
message_content = choice.get("message", {}).get("content")
# Simple parsing for text and action (needs robust implementation)
# Assume LLM might return JSON-like string for actions or structured content
text_response = ""
action_to_take = None
if isinstance(message_content, str):
# Try to parse if it's a JSON string containing action
try:
parsed_content = json.loads(message_content)
if isinstance(parsed_content, dict):
text_response = parsed_content.get("speak", message_content) # Default to full content if no "speak" field
action_to_take = parsed_content.get("action")
else: # Not a dict, treat as plain text
text_response = message_content
except json.JSONDecodeError: # Not JSON, treat as plain text
text_response = message_content
# If Qwen-Omni returned audio, it would be in a different part of the response
# audio_output_url_or_base64 = api_response.get("output", {}).get("audio", {}).get("data")
return {"text_response": text_response, "action_to_take": action_to_take}
return {"text_response": "抱歉,我暂时无法回应。", "action_to_take": None}
async def image_understanding(self, image_base64_or_url: str, prompt_text: str) -> dict or None:
"""调用Qwen视觉理解能力。"""
messages = [
{"role": "user", "content": [
{"type": "text", "text": prompt_text},
{"type": "image_url", "image_url": {"url": image_base64_or_url if image_base64_or_url.startswith("http") else f"data:image/jpeg;base64,{image_base64_or_url}"}}
]}
]
payload = {
"model": self.vision_model,
"messages": messages
}
response = await self._make_api_request("/chat/completions", payload)
if response and response.get("choices"):
return {"text_response": response["choices"][0].get("message", {}).get("content")}
return None
这些模块化的代码结构为构建一个复杂的智能小车系统奠定了基础。实际开发中,每个模块都需要进一步细化和实现,并进行充分的测试。
四、工程化实施方法
将一个复杂的智能小车项目从概念付诸实践,需要采用系统化的工程方法。这不仅能提高开发效率,还能保证项目的质量、可维护性和未来的可扩展性。
版本控制系统:Git
所有项目代码、配置文件和重要文档都应纳入版本控制系统。Git
是目前最主流的选择。
- 代码托管平台: 推荐使用
GitHub
,GitLab
, 或Gitee
(国内访问速度较快)。这些平台不仅提供代码仓库托管,还集成了问题跟踪、代码审查、CI/CD等协作工具。- 示例仓库URL:
https://github.com/your_username/smart_soulful_car.git
- 示例仓库URL:
- 分支策略: 采用合适的分支模型对开发流程进行管理,例如Gitflow或GitHub Flow的简化版:
main
(或master
): 存放稳定、经过测试、可发布的版本。此分支应受保护,只接受来自develop
或release
分支的合并请求 (Merge/Pull Requests)。develop
: 开发主分支,所有新功能的开发都从这个分支拉出特性分支,完成后再合并回develop
。此分支应保持相对稳定,可随时用于内部测试。feature/<feature-name>
: 用于开发单个新功能的分支,如feature/long-term-memory
,feature/auto-charge-navigation
。功能完成后合并到develop
。bugfix/<issue-id>
: 用于修复特定bug的分支,修复后可合并到develop
和main
(如果bug存在于已发布版本)。release/<version-number>
: 当develop
分支达到一个可发布的里程碑时,创建此分支进行发布前的最后测试、文档更新和版本号修订。完成后合并到main
并打上版本标签,同时也合并回develop
以包含版本更新。
- 提交规范: 编写清晰、规范的Commit Message至关重要,便于追踪代码变更历史和理解每次提交的目的。推荐遵循Conventional Commits规范,例如:
feat: add voice command processing module using Qwen STT and LLM
fix: correct motor speed calculation in follow mode
docs: update hardware selection guide for K230
style: reformat audio_processor.py with Black
refactor: simplify QwenAPIHandler request logic
test: add unit tests for PersonalityEngine mood updates
依赖管理
有效管理项目依赖是保证开发环境一致性和部署顺利进行的关键。
- Python:
- 虚拟环境: 必须为项目创建独立的Python虚拟环境(使用
venv
或conda
)。这能隔离项目依赖,避免与系统全局Python环境或其他项目产生冲突。python3 -m venv smartcar_env source smartcar_env/bin/activate # Linux/macOS # smartcar_env\Scripts\activate # Windows
- 依赖列表: 使用
pip freeze > requirements.txt
命令生成当前虚拟环境中所有已安装包及其版本的列表。此文件应提交到版本控制。 - 安装依赖: 其他开发者或部署环境可以通过
pip install -r requirements.txt
命令快速复现相同的依赖环境。 - 高级依赖管理工具(可选): 对于更复杂的项目,可以考虑使用
Poetry
或PDM
。它们提供了更强大的依赖解析、版本锁定、打包和发布功能,有助于维护一个更健壮和可预测的依赖关系图。
- 虚拟环境: 必须为项目创建独立的Python虚拟环境(使用
- K230 (如果使用):
- 记录所使用的K230 SDK的具体版本号。
- 记录交叉编译工具链的版本和配置。
- K230端C/C++代码的编译脚本(如Makefile或CMakeLists.txt)及其依赖的库(如特定版本的OpenCV for K230)也应纳入版本控制或在文档中清晰说明。
配置文件管理
项目的各种配置参数(如API密钥、设备参数、行为阈值等)应与代码分离,通过外部配置文件进行管理。
- 格式选择:
JSON
: 易于机器解析,Python内置支持,但可读性稍差,不支持注释。YAML
: 可读性非常好,支持注释和复杂数据结构,是许多现代应用的首选。需要安装PyYAML
库。INI
: 传统格式,简单直观,Python内置configparser
支持。
YAML
因其可读性和表达能力,或JSON
因其简洁性。 - 示例 (
config.yaml
):# config.yaml project_name: "Smart Soulful Car" version: "0.1.0" qwen_api: api_key_env_var: "QWEN_DASHSCOPE_API_KEY" # 实际密钥从环境变量读取 base_url: "https://dashscope.aliyuncs.com/compatible-mode/v1" # Models to use stt_model: "whisper-large-v3" # Check DashScope for actual model names tts_model: "sambert-zhichu-v1" chat_model: "qwen-plus" vision_model: "qwen-vl-plus" omni_model: "qwen-omni-pro" # For advanced multimodal timeout_seconds: 30 audio: microphone: device_index: null # null for default, or specific index sample_rate: 16000 channels: 1 listen_timeout: 7 # seconds for listen_for_command speaker: # Configuration for speaker if needed (e.g., output device index) pass feedback_sounds: # Paths to sound files confirmation: "sounds/confirm.wav" error: "sounds/error.wav" vision: camera: # picamera2 or OpenCV camera settings resolution_width: 640 resolution_height: 480 framerate: 15 face_recognition: enabled: true target_person_id_to_follow: "user_001" # Example ID # db_path: "data/face_encodings.pkl" # For local face recognition DB k230: # Optional K230 configuration enabled: false # Set to true if using K230 ip: "192.168.1.101" port: 50001 # Example port for K230 vision service motion: driver_type: "TB6612FNG" # or "L298N", "DRV8833" pins: # Example for TB6612FNG with gpiozero Motor class left_fwd: 20 # GPIO pin numbers (BCM) left_bwd: 21 left_pwm: 12 # If using separate PWM enable, else one of fwd/bwd can be pwm right_fwd: 19 right_bwd: 26 right_pwm: 13 default_speed_percent: 40 # PID parameters for following, etc. follow_pid: kp: 0.5 ki: 0.01 kd: 0.1 environment_sensors: bme280_address: 0x76 # I2C address bh1750_address: 0x23 read_interval_seconds: 30 power: ina219_address: 0x40 battery_min_voltage: 3.2 battery_max_voltage: 4.2 critical_low_threshold_percent: 15 low_threshold_percent: 30 charging_current_threshold_ma: 50 # Current above this means charging read_interval_seconds: 15 auto_charge: apriltag_id_for_dock: 0 # Example AprilTag ID for the dock # Other docking parameters memory: db_path: "data/smartcar_memory.db" # SQLite DB path vector_store_path: "data/vector_store_chroma" # ChromaDB persist path embedding_model_name: "shibing624/text2vec-base-chinese" # For local embeddings personality: profile_path: "config/personality_profile.json" main_loop_interval_seconds: 0.1 # Main control loop frequency
- 敏感信息管理: API密钥、数据库密码等敏感信息严禁硬编码在代码或提交到版本控制的配置文件中。应使用以下方式管理:
- 环境变量: 将敏感信息存储在环境变量中 (如
export QWEN_DASHSCOPE_API_KEY="your_key"
),然后在Python代码中使用os.getenv("QWEN_DASHSCOPE_API_KEY")
读取。这是推荐的做法。 .env
文件: 创建一个.env
文件(此文件应加入.gitignore
,不提交到版本库),在其中定义环境变量,如QWEN_DASHSCOPE_API_KEY="your_key"
。然后使用python-dotenv
库 (pip install python-dotenv
) 在程序启动时加载这些变量到环境中。- 专用的Secrets管理工具: 对于更复杂的生产环境,可以考虑使用HashiCorp Vault, AWS Secrets Manager等工具。
- 环境变量: 将敏感信息存储在环境变量中 (如
日志记录
全面而规范的日志记录对于调试、监控系统运行状态和问题排查至关重要。
- Python
logging
模块: 这是Python内置的标准日志库,功能强大且灵活。- 配置: 在程序入口处(如
smart_car_main.py
的开头)配置日志格式、级别、输出目标(控制台和/或文件)。import logging logging.basicConfig( level=logging.INFO, # Default level, can be overridden by command line arg or config format='%(asctime)s - %(levelname)-8s - %(name)-20s - %(funcName)-15s - %(message)s', handlers=[ logging.FileHandler("smart_car.log", mode='a', encoding='utf-8'), # Append to log file logging.StreamHandler() # Output to console ] ) # Usage in other modules: # logger = logging.getLogger(__name__) # logger.info("This is an info message.") # logger.error("An error occurred.", exc_info=True) # exc_info=True will log stack trace
- 日志级别: 合理使用不同的日志级别:
DEBUG
: 非常详细的调试信息,通常只在开发和问题排查时开启。INFO
: 常规的操作信息,记录系统运行的关键步骤和状态变化。WARNING
: 潜在问题或非严重错误,系统仍可继续运行。ERROR
: 发生了错误,导致某个功能无法正常完成,但系统可能仍能尝试恢复或继续其他操作。CRITICAL
: 严重错误,导致系统无法继续运行或即将崩溃。
- 日志轮转: 当日志输出到文件时,为防止日志文件无限增大,应配置日志轮转。可以使用
logging.handlers.RotatingFileHandler
(按文件大小轮转)或logging.handlers.TimedRotatingFileHandler
(按时间间隔轮转)。
- 配置: 在程序入口处(如
单元测试框架:pytest
单元测试是保证代码质量、及早发现bug和支持重构的重要手段。pytest
是一个功能强大且易用的Python测试框架。
- 测试文件组织: 在项目根目录下创建一个
tests/
目录。测试文件名应以test_*.py
或*_test.py
开头,测试函数名以test_
开头。 - 示例 (
tests/test_personality_engine.py
):# tests/test_personality_engine.py import pytest from smart_car_package.personality_engine import PersonalityEngine # Adjust import path @pytest.fixture def default_personality_engine(): # Create a dummy profile for testing or use a test-specific profile file test_profile = { "name": "TestBot", "base_persona": "I am a test bot.", "moods": { "neutral": {"greeting": "Hello.", "response_style_suffix": ""}, "tired": {"greeting": "Ugh...", "response_style_suffix": " *sigh*"} }, # ... other necessary fields } # This might involve writing a temporary JSON file if PersonalityEngine loads from path # Or, modify PersonalityEngine to accept a dict profile for testing # For simplicity, assume PersonalityEngine can be initialized with a dict # return PersonalityEngine(profile_data=test_profile) # Assuming profile_path is used: # with open("test_profile.json", "w") as f: json.dump(test_profile, f) # engine = PersonalityEngine(profile_path="test_profile.json") # yield engine # os.remove("test_profile.json") # For now, let's assume a simplified constructor for testing: class MockPersonalityEngine(PersonalityEngine): def __init__(self, profile): self.profile = profile self.current_mood = "neutral" # Simplified return MockPersonalityEngine(test_profile) def test_mood_update_low_battery(default_personality_engine): # Assume env_data is not relevant for this specific mood change new_mood = default_personality_engine.update_mood("neutral", {}, 10) # 10% battery assert new_mood == "tired" def test_personalize_text_tired_mood(default_personality_engine): default_personality_engine.current_mood = "tired" # Set mood for test base_text = "I am responding." personalized = default_personality_engine.personalize_text(base_text, "tired") assert personalized.endswith(" *sigh*")
- 测试覆盖率: 使用
pytest-cov
插件 (pip install pytest-cov
) 来衡量测试覆盖了多少代码。运行pytest --cov=your_package_name
生成覆盖率报告。目标是尽可能提高覆盖率,特别是核心逻辑模块。 - Mocking: 对于依赖外部服务(如Qwen3 API调用)或硬件交互(如传感器读取、电机控制)的模块,单元测试时需要使用mock对象来模拟这些依赖。Python的
unittest.mock
模块(或pytest-mock
插件)可以创建mock对象,控制其行为和返回值,从而隔离被测单元。 - 运行命令: 在项目根目录下执行
pytest
或python -m pytest
。
集成测试计划
集成测试用于验证不同模块组合在一起时能否协同工作,以及整个系统是否满足核心功能需求。应定义关键的用户场景和系统交互流程进行测试。
- 场景1 (语音交互与执行):
- 流程: 用户说出指令(如“小车前进”) -> 麦克风采集音频 ->
AudioProcessor
处理 ->QwenAPIHandler
调用STT -> 获取文本 ->SmartCarCore
构建上下文 ->QwenAPIHandler
调用LLM -> 获取LLM回复(文本+动作) ->PersonalityEngine
个性化文本 ->AudioProcessor
调用TTS并播放 ->MotionController
执行动作。 - 验证点: STT准确性,LLM理解和决策的合理性,TTS的清晰度,动作执行的正确性,整体响应时间。
- 流程: 用户说出指令(如“小车前进”) -> 麦克风采集音频 ->
- 场景2 (人脸识别与跟随):
- 流程: 特定注册用户出现在摄像头视野中 ->
VisionProcessor
(可能协同K230Client
) 检测并识别人脸 ->SmartCarCore
判断为目标用户 ->PersonalityEngine
生成问候语 ->AudioProcessor
播放问候 ->SmartCarCore
进入跟随状态 ->VisionProcessor
持续提供目标位置 ->MotionController
调整小车姿态进行跟随 -> 用户离开视野 ->VisionProcessor
报告目标丢失 ->SmartCarCore
停止跟随。 - 验证点: 人脸检测和识别的准确率与速度,问候的及时性,跟随的平滑性和鲁棒性,目标丢失后的正确行为。
- 流程: 特定注册用户出现在摄像头视野中 ->
- 场景3 (环境感知与主动提醒):
- 流程:
PowerManager
检测到电池电量低于20% ->SmartCarCore
的proactive_behavior
逻辑被触发 ->PersonalityEngine
生成低电量提醒文本 ->AudioProcessor
播放提醒。类似地,EnvironmentMonitor
检测到特殊天气(如通过API获取到“下雨”) -> 触发主动提醒。 - 验证点: 传感器数据读取的准确性,阈值判断的正确性,主动提醒的及时性和内容的相关性,心情是否随之调整。
- 流程:
- 场景4 (自主回充):
- 流程:
SmartCarCore
通过指令或PowerManager
的极低电量信号触发回充 ->AutoChargeController
启动坞接序列 ->VisionProcessor
(可能协同K230Client
) 搜索并定位AprilTag ->MotionController
导航至坞站附近 -> (可选)切换到IR传感器进行末端对准 ->MotionController
完成对接 ->PowerManager
确认充电开始。 - 验证点: 回充触发的可靠性,导航路径的合理性,对接的成功率,充电确认的准确性。
- 流程:
- 场景5 (K230协同 - 若使用):
- 流程: 树莓派
VisionProcessor
捕获图像 -> 通过K230Client
将图像(或指令)发送给K230 -> K230上的服务程序调用KPU进行人脸检测/跟踪 -> K230将处理结果(如人脸框坐标)返回给树莓派 -> 树莓派VisionProcessor
解析结果并用于上层决策。 - 验证点: 树莓派与K230通信的稳定性和速率,K230处理视觉任务的帧率和延迟,结果数据的准确性。
- 流程: 树莓派
集成测试通常需要一个接近真实运行环境的测试平台,可能涉及部分手动操作和观察。
部署与启动脚本
将开发完成的智能小车软件部署到树莓派上,并确保其能够稳定、自动地启动和运行。
- 启动脚本 (
start_smartcar.sh
): 一个简单的bash脚本,用于手动启动所有相关服务。#!/bin/bash echo "Starting Smart Soulful Car Services..." # Define project directory PROJECT_DIR="/home/your_rpi_user/smart_soulful_car" # Adjust to your actual path LOG_DIR="${PROJECT_DIR}/logs" VENV_PATH="${PROJECT_DIR}/smartcar_env" # Path to virtual environment # Create log directory if it doesn't exist mkdir -p "$LOG_DIR" # Activate Python virtual environment echo "Activating Python virtual environment..." source "${VENV_PATH}/bin/activate" if [ $? -ne 0 ]; then echo "Failed to activate virtual environment. Exiting." exit 1 fi # Export necessary environment variables (e.g., API keys) # It's better to set these in ~/.bashrc or a .env file loaded by the Python app # export QWEN_DASHSCOPE_API_KEY="your_actual_api_key_here" # NOT RECOMMENDED IN SCRIPT # Navigate to project directory cd "$PROJECT_DIR" # Optional: Start K230 service if it's managed separately and needs remote start # K230_IP="192.168.1.101" # Example K230 IP # K230_USER="k230_user" # K230_START_SCRIPT="/path/on/k230/to/start_service.sh" # echo "Attempting to start K230 service (if configured)..." # ssh "${K230_USER}@${K230_IP}" "${K230_START_SCRIPT}" & # Start the main Python application echo "Starting SmartCar main application..." #nohup python smart_car_main.py > "${LOG_DIR}/smart_car_stdout.log" 2> "${LOG_DIR}/smart_car_stderr.log" & python smart_car_main.py >> "${LOG_DIR}/smart_car_stdout.log" 2>> "${LOG_DIR}/smart_car_stderr.log" & # Get the PID of the last background process APP_PID=$! echo "Smart Car Main Process Started. PID: ${APP_PID}" echo "Logs are being written to ${LOG_DIR}/" # Deactivate virtual environment (optional, script will exit anyway) # deactivate
systemd
服务 (推荐用于开机自启和进程守护): 为了让小车程序在树莓派开机时自动启动,并在意外崩溃后自动重启,应将其配置为一个systemd
服务。- 创建一个
.service
文件,例如/etc/systemd/system/smartcar.service
。 - 文件内容示例:
[Unit] Description=Smart Soulful Car Service After=network.target multi-user.target # Ensure network is up [Service] User=your_rpi_user # Replace with the user that owns the project files Group=your_rpi_group # Replace with the user's group WorkingDirectory=/home/your_rpi_user/smart_soulful_car # Adjust path # Environment="QWEN_DASHSCOPE_API_KEY=your_key_here" # Can set env vars here, but less secure # Or use EnvironmentFile=/path/to/your/env_file ExecStart=/home/your_rpi_user/smart_soulful_car/smartcar_env/bin/python smart_car_main.py Restart=on-failure # Restart the service if it fails RestartSec=5s # Wait 5 seconds before restarting StandardOutput=append:/var/log/smartcar_stdout.log # Redirect stdout StandardError=append:/var/log/smartcar_stderr.log # Redirect stderr SyslogIdentifier=smartcar [Install] WantedBy=multi-user.target
- 命令:
sudo systemctl daemon-reload
: 重新加载systemd配置,在创建或修改service文件后执行。sudo systemctl enable smartcar.service
: 设置服务开机自启。sudo systemctl start smartcar.service
: 启动服务。sudo systemctl status smartcar.service
: 查看服务状态。sudo systemctl stop smartcar.service
: 停止服务。sudo journalctl -u smartcar.service -f
: 查看服务的实时日志。
- 创建一个
通过这些工程化实践,可以显著提升项目的开发效率、代码质量和最终产品的稳定性与可维护性。
五、关键设计、硬件选型和代码的推荐总结
综合前述分析,本章节对项目的关键设计、核心硬件选型以及软件实现要点进行提炼总结,旨在为项目的顺利实施提供明确且具有操作性的指导建议。
关键设计推荐
- 架构核心: 强烈推荐采用
基于异步事件驱动的模块化架构
。以树莓派作为中心协调器,通过清晰定义的软件接口与各个硬件模块(传感器、执行器)、K230视觉协处理器(若采用)以及云端的Qwen3 API进行高效交互。核心主程序应广泛使用Python的asyncio
库来处理并发的I/O密集型任务,如API网络请求、传感器数据的周期性轮询、以及需要精确计时的动作序列,从而避免阻塞主线程,保证系统的整体响应速度和流畅性。 - AI集成策略:
- Qwen3 API为主力智能引擎: 充分利用Qwen3系列大模型在自然语言理解(NLU)、多轮对话管理、个性化文本生成、高质量语音合成(TTS)、以及视觉问答(VQA)等方面的强大能力,将其作为构建小车“灵魂”和核心智能交互能力的驱动力。
- K230为专用视觉加速器(推荐集成): 若项目预算和开发精力允许,强烈建议集成K230开发板。将其定位为视觉协处理器,专门处理对实时性要求极高的端侧视觉任务,例如高帧率的人脸检测与跟踪、AprilTag识别与姿态估计等。这能显著减轻树莓派的CPU负担,提升整体视觉感知性能和交互体验。树莓派与K230之间可通过以太网(TCP/IP)进行高效数据通信。
- LangChain框架赋能高级功能: 考虑引入LangChain框架来简化与Qwen3 API的交互链构建。特别是其
Memory
模块(如ConversationBufferWindowMemory
用于短期对话记忆,VectorStoreRetrieverMemory
结合本地向量数据库实现长期记忆的检索增强生成RAG)对于实现小车的长期记忆至关重要。若Qwen3 API支持Function Calling(工具调用),LangChain的Agent
模块可以帮助构建更复杂的任务规划和工具调用能力(如让小车调用本地函数获取传感器数据或控制硬件)。
- “灵魂”塑造策略:
- 记忆系统: 构建一个双层记忆系统。短期记忆负责暂存当前对话上下文;长期记忆则通过结构化日志(如SQLite存储关键事件和交互摘要)与向量化记忆(如使用ChromaDB或FAISS存储对话内容的嵌入向量,通过Sentence Transformers等模型生成嵌入)相结合,实现对过去交互、用户偏好和重要事件的持久化存储和高效检索。
- 个性化引擎: 通过外部配置文件(如JSON或YAML)定义小车的基础性格特征、不同情绪状态下的回应风格(如语速、语调变化,口头禅)、以及对特定情境的反应模式。小车的“情绪”应能根据当前环境(天气、温湿度)、自身状态(电池电量)、以及与用户的交互反馈进行动态调整。此情绪状态将作为重要上下文输入给Qwen3,以生成更具个性化和“灵魂感”的回复。
- 主动性逻辑: 设计明确的规则和触发器,使小车能够基于环境感知数据(如通过天气API获取的天气预报、本地传感器监测到的温湿度变化)、自身状态(如低电量警告)、或特定事件(如识别到已注册的主人回家)来主动发起交互、提供有用的提醒或执行预设行为。
硬件选型推荐 (精华总结)
- 主控制器:
Raspberry Pi 5 (推荐8GB RAM版本)
。其更强的CPU性能、PCIe扩展潜力以及双CSI接口,为当前复杂的软件栈和未来的功能扩展提供了充足的硬件基础。 - 视觉协处理 (强烈推荐集成):
CanMV K230
或其他基于嘉楠K230芯片的开发板。通过以太网与树莓派连接,专门负责处理计算密集型、对实时性要求高的视觉任务(人脸检测/跟踪、AprilTag识别等)。 - 核心传感器:
- 麦克风:
高品质USB麦克风
(如Anker PowerConf S3, Blue Yeti Nano)或ReSpeaker 2-Mics/4-Mics Pi HAT
,以保证清晰的语音输入质量,对于STT的准确性至关重要。 - 摄像头:
Raspberry Pi Camera Module 3 (广角版优先)
或Arducam IMX519 16MP Autofocus Camera
,提供清晰、宽视角的视觉输入。 - 电池监测:
Adafruit INA219
或集成在UPS HAT上的专用电量计芯片(如MAX1704x系列),用于精确监测电池电压、电流和估算剩余电量。 - 环境传感器:
Bosch BME280
(温湿度、气压三合一) 和Adafruit BH1750
或VEML7700
(光照强度)。
- 麦克风:
- 自主回充方案: 推荐采用
AprilTag视觉标签进行中远距离引导 + 末端红外传感器/机械导引结构进行精确对接
的组合方案。充电坞触点使用弹簧顶针 (Pogo Pins)
。 - 电机与驱动:
TB6612FNG
或DRV8833
双路电机驱动器,配合带编码器的直流减速电机
(如N20系列),以实现较精确的速度闭环控制和里程估算。
树莓派(绿色PCB)与K230视觉板(红色PCB背景中的板卡)连接的系统示意,体现了主控与协处理器的硬件架构
核心代码实现要点
- Qwen3 API封装 (
QwenAPIHandler.py
): 创建一个健壮、易用的API封装类。该类应负责所有与Qwen3 API(或其他云服务API)的异步HTTP请求,包括STT、TTS、LLM对话、视觉理解等。实现内容应包括:- 使用
httpx.AsyncClient
进行异步网络请求。 - 统一处理API认证(如Bearer Token)。
- 封装请求参数的构建逻辑,特别是多模态输入的正确格式化(参考OpenAI格式或Qwen特定格式,如 阿里云Qwen API文档 中对
messages
内image_url
,input_audio
等的定义)。 - 实现对API响应的解析,提取所需信息(如文本回复、动作指令、错误码)。
- 包含完善的错误处理机制(如网络超时、HTTP错误、API限流)、自动重试逻辑(针对可恢复的临时性错误)以及超时控制。
- 支持流式响应处理(对于TTS和LLM对话,以改善用户体验)。
- 使用
- 状态机管理 (在
SmartCarCore
中): 在主控制类中实现一个明确定义的状态机,用于管理小车的各种行为模式(如:IDLE, LISTENING, PROCESSING_COMMAND, RESPONDING, FOLLOWING_USER, NAVIGATING_TO_DOCK, CHARGING, ERROR_STATE等)。可以使用Python的enum
类型来定义状态。每个状态应明确其允许的进入条件、可执行的操作以及可转换到的下一个状态。状态机有助于使复杂的行为逻辑变得清晰、可控且易于调试。 - 异步编程 (
asyncio
): 项目中所有涉及等待I/O操作(如网络API调用、长时间的传感器读取、电机控制中的延时、文件读写等)的部分,都应使用asyncio
和await
关键字进行异步化处理。这可以防止阻塞主事件循环,确保系统在等待耗时操作完成时仍能响应其他事件(如新的用户指令、传感器报警),从而显著提高系统的并发能力和整体响应性。 - K230通信协议 (若使用
K230Client.py
): 如果采用K230作为协处理器,需要设计一个简洁、高效且可靠的通信协议,用于树莓派与K230之间的数据交换。可以考虑使用基于JSON或Protobuf(Protocol Buffers)的TCP/IP消息。协议应明确定义请求(如“检测人脸”、“开始跟踪目标X”)和响应(如人脸框坐标列表、目标ID、跟踪状态)的数据格式、消息边界以及错误处理机制。 - LangChain集成 (重点关注):
- Memory模块:
- 短期记忆: 使用
langchain.memory.ConversationBufferWindowMemory
来保存最近几轮的对话历史,作为即时上下文提供给LLM。 - 长期记忆 (RAG): 结合本地向量数据库(如
ChromaDB
或FAISS
)和文本嵌入模型(可以使用HuggingFace Sentence Transformers库加载本地运行的中文嵌入模型,如shibing624/text2vec-base-chinese
,或者如果Qwen API提供嵌入服务,则调用该服务)来实现检索增强生成。将重要的对话片段、用户偏好、关键事件等信息进行嵌入并存入向量数据库。当需要回答涉及历史信息的问题或进行个性化推荐时,先用用户当前问题在向量库中检索最相关的记忆片段,然后将这些片段作为上下文补充信息一起提供给Qwen3 LLM。可使用langchain.memory.VectorStoreRetrieverMemory
。
- 短期记忆: 使用
- Prompts模块: 精心设计
langchain.prompts.PromptTemplate
和ChatPromptTemplate
。这些模板应能动态地结合来自PersonalityEngine
的个性化指令、来自MemoryManager
的上下文记忆片段、当前的“情绪”状态以及具体的任务指令(如“回答以下问题:”、“根据以下信息生成一个提醒:”),从而构建出高质量、信息丰富的提示,以充分引导Qwen3模型生成符合期望的输出。 - Chains/Agents模块: 如果Qwen3 API支持工具调用(Function Calling)或通过特定提示格式能实现类似效果,可以考虑构建LangChain Agent。Agent允许LLM在推理过程中决定调用外部工具(即预定义的Python函数,如
get_battery_level()
,move_forward(distance)
,query_weather_api(city)
等)。这使得小车能够执行更复杂的、需要与物理世界或外部服务交互的任务,而不仅仅是文本生成。
- Memory模块:
通过上述关键点的精心设计与实现,可以构建出一个既具备强大智能,又稳定可靠、易于维护和扩展的“灵魂”智能小车系统。
六、项目实施路线图 (建议阶段)
将一个如此复杂度的项目成功落地,需要一个清晰的、分阶段的实施路线图。每个阶段应设定明确的任务目标和可衡量的里程碑,以便于跟踪进度、管理风险和迭代优化。
-
[阶段1:
2-3周
] 基础平台搭建与核心硬件验证- 任务:
- 完成树莓派操作系统的安装(Raspberry Pi OS 64-bit)、网络配置(Wi-Fi/以太网)、SSH远程访问设置,以及基础开发环境的搭建(Python 3.x, pip, venv, Git)。
- 组装小车底盘,连接电机和电机驱动模块(如TB6612FNG)。编写基础的Python脚本,通过GPIO控制电机实现小车前进、后退、左转、右转和停止等基本运动功能,并进行初步调试。
- 连接核心传感器到树莓派:
- 麦克风(USB或HAT):测试音频录制功能,确保能获取清晰的音频流。
- 摄像头(CSI或USB):测试图像/视频流捕获,确保图像质量和帧率符合基本要求。
- 电池监测模块(如INA219):编写脚本读取电池电压和电流,初步估算电量。
- [可选,若计划使用K230] K230开发板初步上手:完成K230的SDK安装和系统烧录,运行官方提供的视觉Demo(如人脸检测示例),测试K230与树莓派之间的基本网络通信(如通过以太网ping通,或建立简单的Socket连接)。
- 里程碑:
- 树莓派能够通过Python脚本精确控制小车的四向运动和启停。
- 能够稳定地从麦克风录制音频数据,从摄像头捕获图像数据,并读取到电池的电压和电流值。
- [可选] K230能够独立运行其官方视觉示例,并能与树莓派建立基础的网络连接。
- 项目代码已建立Git仓库,并完成首次提交。
- 任务:
-
[阶段2:
3-4周
] Qwen3 API集成与初步多模态交互- 任务:
- 注册Qwen3 API服务账号(如阿里云百炼),获取API Key,详细阅读API文档,了解调用限制、计费方式等。
- 在树莓派上编写
QwenAPIHandler.py
模块,实现对Qwen3的语音转文字(STT)、文字转语音(TTS)以及基础的文本对话(LLM)API的异步调用功能。封装好请求构建、认证、响应解析和错误处理逻辑。 - 实现一个简单的语音交互循环:录制用户语音 -> 调用STT API转为文本 -> 将文本发送给LLM API获取回复 -> 调用TTS API将回复文本转为语音并播放。测试指令如“你好小车”,“现在几点了”,“讲个笑话”。
- 集成Qwen3的视觉理解API(如
qwen-vl-plus
):捕获摄像头图像 -> 将图像(Base64编码或URL)连同问题文本一起发送给API -> 获取API对图像内容的描述或问题的回答,并通过TTS播放。 - 初步构建
PersonalityEngine.py
,定义几种基本的情绪状态和对应的简单回应风格(如问候语、语气词),在TTS输出时尝试应用。
- 里程碑:
- 小车能够通过Qwen3 API听懂用户说出的简单中文指令,并用合成语音做出相关回应。
- 小车能够调用Qwen3 API描述摄像头当前捕获到的场景中的主要物体或回答关于图像的简单问题。
- Qwen3 API的调用流程(STT, LLM, TTS, Vision)基本调通,错误处理机制初步建立。
- 初步的个性化回应(如根据预设情绪选择不同问候语)得到验证。
- 任务:
-
[阶段3:
4-6周
] “灵魂”核心:记忆、个性深化与环境感知- 任务:
- 设计并实现
MemoryManager.py
模块:- 短期记忆: 使用LangChain的
ConversationBufferWindowMemory
或类似机制,保存最近N轮的对话历史,作为即时上下文提供给LLM。 - 长期记忆 (RAG): 搭建本地向量数据库(如ChromaDB)。将重要的对话片段、用户偏好、关键事件等信息,使用文本嵌入模型(如本地运行的Sentence Transformers模型,或调用Qwen的嵌入API)转换为向量并存储。实现基于语义相似度的记忆检索功能。
- 短期记忆: 使用LangChain的
- 完善
PersonalityEngine.py
:设计更丰富的个性配置文件(JSON/YAML),包括多种情绪状态、每种情绪下的回应模板、口头禅、语速/语调偏好等。实现基于环境因素(天气、温湿度)、自身状态(电池电量)、以及与用户的交互历史来动态调整小车“情绪”的逻辑。将当前情绪作为重要上下文融入Qwen3的System Prompt,并对LLM的输出文本进行后处理以增强个性化。 - 实现
EnvironmentMonitor.py
:集成温湿度传感器(如BME280)、光照传感器(如BH1750)的数据读取。可选:通过Qwen3的联网能力或调用第三方天气API获取外部天气信息。 - 实现基于环境感知数据和当前情绪的主动提醒逻辑。例如,当电池电量低时,小车会用符合当前“疲惫”情绪的语气主动提醒用户需要充电;当检测到天气变化时,会主动播报并给出建议。
- 设计并实现
- 里程碑:
- 小车能够记住之前的对话要点,并在后续交互中恰当地利用这些记忆(如用户提及过的名字、偏好等)。
- 小车的“心情”能够根据环境(如温度过高/过低)和自身状态(如电量充足/不足)发生合理变化,并通过语言风格和(可选的)灯光/表情显示表现出来。
- 小车能够根据天气情况(如API获取到“下雨”)或电池电量低等情况,主动发出相关的语音提醒。
- 长期记忆的存储和检索功能初步可用。
- 任务:
-
[阶段4:
3-5周
] 高级视觉功能与K230深度集成 (若选择K230)- 任务:
- [若使用K230] 在K230开发板上部署高效的人脸检测与跟踪算法(如基于YOLO的检测器 + KCF/MOSSE等跟踪器,或KPU优化的特定模型)。
- [可选,若使用K230] 在K230上实现特定人脸的特征提取与比对功能(如使用轻量级的人脸识别模型如ArcFace的嵌入提取,并在K230或树莓派端进行特征比对)。
- [若使用K230] 完善树莓派与K230之间的通信协议(如基于TCP/IP的JSON或Protobuf消息),确保图像数据、控制指令和处理结果的稳定高效传输。编写
K230Client.py
。 - 在树莓派的主控制程序中(
VisionProcessor.py
和SmartCarCore.py
),集成K230的视觉处理结果,实现人脸主动跟随功能:当识别到预设的特定用户时,小车能主动发起问候并调整自身运动以保持用户在视野中央。 - [若不使用K230] 则在树莓派上尽力优化OpenCV的人脸检测/跟踪算法性能,或者考虑使用Google Coral USB Accelerator等其他边缘AI加速方案来提升视觉处理帧率。
- 里程碑:
- 小车能够实时(如 >10 FPS)检测视野中的人脸,并能对选定的人脸目标进行较为平滑的跟踪。
- [可选] 小车能够识别已注册的1-2名特定用户,并在识别到时触发特定行为(如问候)。
- 小车能够根据视觉模块提供的目标位置信息,控制电机实现主动跟随指定目标的功能。
- [若使用K230] 树莓派与K230之间的视觉任务协同流程基本稳定。
- 任务:
-
[阶段5:
3-4周
] 自主回充系统开发与集成- 任务:
- 设计并制作充电坞的物理结构,包括导向槽、充电触点(如Pogo Pins)、以及导航信标(如打印的AprilTag、红外LED阵列)。
- 根据选定的导航方案(如AprilTag视觉定位 + 末端红外引导),在小车端编写导航与对接算法。
- AprilTag方案:使用OpenCV或K230识别充电坞上的AprilTag,计算其相对位姿,通过视觉伺服控制小车逐步靠近。
- 红外引导方案:编写逻辑根据多个红外接收管的信号强度差来判断充电坞方向并进行对准。
- 集成
PowerManager.py
的低电量检测逻辑与AutoChargeController.py
的回充触发机制。当电量低于预设阈值时,小车应能自动启动回充程序。 - 反复测试和调试回充过程的各个阶段(寻找坞站、导航靠近、精确对接、确认充电),提高成功率和鲁棒性,处理各种可能的异常情况(如坞站被遮挡、对接失败等)。
- 里程碑:
- 小车在接收到回充指令或检测到自身电量极低时,能够自主启动回充程序。
- 小车能够利用选定的导航方式(如识别AprilTag)找到充电坞的大致位置并朝其移动。
- 小车能够通过末端引导机制(如红外对准或机械结构)完成与充电触点的物理对接,并由
PowerManager
确认充电已成功开始。 - 自主回充成功率达到一个可接受的水平(如在特定测试环境下 >80%)。
- 任务:
-
[阶段6:
2-3周
] 系统总集成、综合测试与优化- 任务:
- 将所有已开发的功能模块(语音交互、记忆、个性、环境感知、视觉跟随、自主回充等)集成到
SmartCarCore
主控制程序中,确保它们能够正确协同工作,没有严重的冲突或资源竞争。 - 进行全面的端到端场景测试,覆盖项目初期定义的所有核心功能需求。模拟真实用户的使用场景,检验系统的整体表现。
- 性能分析与优化:重点关注语音交互的响应延迟、视觉处理的帧率、树莓派的CPU和内存占用率、以及电池的实际续航时间。找出性能瓶颈并进行针对性优化。
- 鲁棒性与异常处理测试:模拟各种异常情况,如网络连接暂时中断、某个传感器数据读取失败、K230无响应、充电对接多次失败等,检验系统的容错能力和恢复机制。
- 编写详细的项目文档,包括系统设计文档、硬件连接图、软件模块说明、API使用指南、用户手册、以及关键代码的注释。
- 将所有已开发的功能模块(语音交互、记忆、个性、环境感知、视觉跟随、自主回充等)集成到
- 里程碑:
- 所有核心功能模块能够稳定地集成并协同运行,基本符合设计预期。
- 系统的关键性能指标(如语音响应延迟、跟随平滑度、回充成功率)达到项目设定的可接受水平。
- 项目具备基本的容错能力,不会因常见的单点故障而完全瘫痪。
- 项目文档基本完善,代码整洁度较高,可供他人理解、演示和进行后续的二次开发。
- 一个可演示的、具备核心“灵魂”特征的智能小车原型完成。
- 任务:
这是一个迭代的路线图,每个阶段的成果都可以作为下一个阶段的基础。在实际执行过程中,可能需要根据遇到的具体问题和资源情况对计划进行调整。持续的测试、反馈和迭代是项目成功的关键。