做智能驾驶,除了视觉感知和路径规划,人机交互也是一个重要方向。这篇记录一下我用征程6开发板 + 手机APP + 语音模块搭建的一个“语音控制小车”多模态交互原型。涉及语音识别、自然语言理解、指令分发、远程通信等多个技术点。
一、系统架构
手机APP(语音输入)
|
| CAN/UART
↓
小车底盘(电机/舵机)
手机APP负责语音采集和ASR,把语音转成文本指令后通过WiFi发给征程6。征程6上做NLU,把文本解析成结构化控制指令,再通过CAN或UART发给底盘执行。
二、语音识别
手机端用百度语音识别API,在线+离线结合。在线精度高但依赖网络,离线精度低但不需要网络。室内WiFi环境下主要用在线识别。
```kotlin
// Android端
val speechRecognizer = SpeechRecognizer.createRecognizer(this) {
setParameter(SpeechConstant.APP_ID, "YOUR_APP_ID")
setParameter(SpeechConstant.LANGUAGE, "zh_cn")
setParameter(SpeechConstant.ACCENT, "mandarin")
}
speechRecognizer.startListening(object : RecognitionListener {
override fun onResult(results: Array?, isLast: Boolean) {
val text = results?.get(0)?.recognitionResult ?: ""
webSocketClient.send("{\"command\": \"$text\"}")
}
})
```
离线备选:PaddleSpeech轻量模型,50MB大小,识别率80%左右。
三、自然语言理解
征程6板端收到文本指令后做NLU解析。设计了规则+模板匹配的解析器:
```cpp
enum class CommandType {
MOVE_FORWARD, MOVE_BACKWARD, TURN_LEFT, TURN_RIGHT,
STOP, SET_SPEED, UNKNOWN
};
struct VehicleCommand {
CommandType type;
float speed = -1.0f;
float duration = -1.0f;
float angle = -1.0f;
};
class NLUParser {
vector> patterns_ = {
{regex(R"(前进|往前走|向前|直行)"), MOVE_FORWARD},
{regex(R"(后退|往后退|倒车)"), MOVE_BACKWARD},
{regex(R"(左转|向左转|往左)"), TURN_LEFT},
{regex(R"(右转|向右转|往右)"), TURN_RIGHT},
{regex(R"(停|停止|刹车|别动)"), STOP},
};
public:
VehicleCommand Parse(const string& text) {
VehicleCommand cmd;
for (const auto& [pattern, type] : patterns_) {
if (regex_search(text, pattern)) {
cmd.type = type;
// 提取参数(距离、速度、角度)
smatch match;
if (regex_search(text, match, regex(R"(\d+)"))) {
float value = stof(match[1].str());
if (text.find("米") != string::npos) cmd.duration = value;
else if (text.find("速度") != string::npos) cmd.speed = value / 3.6f;
else if (text.find("度") != string::npos) cmd.angle = value;
}
return cmd;
}
}
cmd.type = UNKNOWN;
return cmd;
}
};
```
四、WebSocket通信
手机APP和征程6板端之间走WebSocket,双向实时。
板端WebSocket服务器:
```cpp
#include
class VehicleWebSocketServer {
struct lws_context* context_ = nullptr;
vector clients_;
NLUParser parser_;
VehicleController controller_;
static int Callback(struct lws* wsi, enum lws_callback_reasons reason,
void* user, void* in, size_t len) {
auto* server = static_cast(
lws_context_user(lws_get_context(wsi)));
switch (reason) {
case LWS_CALLBACK_ESTABLISHED:
server->clients_.push_back(wsi);
break;
case LWS_CALLBACK_CLOSED:
// 移除断开连接的客户端
break;
case LWS_CALLBACK_RECEIVE: {
string msg(static_cast(in), len);
server->OnMessage(wsi, msg);
break;
}
}
return 0;
}
void OnMessage(struct lws* wsi, const string& msg) {
// 解析JSON
Json::Value root;
Json::Reader reader;
if (!reader.parse(msg, root)) return;
string text = root.get("command", "").asString();
auto cmd = parser_.Parse(text);
controller_.ExecuteCommand(cmd);
// 回复确认
Json::Value ack;
ack["type"] = "ack";
ack["parsed"] = CommandToString(cmd.type);
SendMessage(wsi, ack.toStyledString());
}
public:
bool Start(int port = 9002) {
struct lws_context_creation_info info;
memset(&info, 0, sizeof(info));
info.port = port;
info.protocols = protocols_;
context_ = lws_create_context(&info);
thread server_thread([this]() {
while (running_) {
lws_service(context_, 50);
}
});
server_thread.detach();
return true;
}
void BroadcastStatus(const VehicleStatus& status) {
Json::Value msg;
msg["type"] = "status";
msg["speed"] = status.speed;
msg["obstacle_detected"] = status.obstacle_detected;
for (auto wsi : clients_) {
SendMessage(wsi, msg.toStyledString());
}
}
};
```
五、CAN总线控制
```cpp
class CANController {
int socket_ = -1;
public:
bool Init(const char* interface = "can0") {
socket_ = socket(PF_CAN, SOCK_RAW, CAN_RAW);
if (socket_ < 0) return false;
struct ifreq ifr;
strcpy(ifr.ifr_name, interface);
ioctl(socket_, SIOCGIFINDEX, &ifr);
struct sockaddr_can addr;
addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;
bind(socket_, (struct sockaddr*)&addr, sizeof(addr));
return true;
}
void SendMotorCommand(float left_speed, float right_speed) {
struct can_frame frame;
frame.can_id = 0x101;
frame.can_dlc = 4;
int32_t left_rpm = static_cast(left_speed * 100);
int32_t right_rpm = static_cast(right_speed * 100);
memcpy(frame.data, &left_rpm, 2);
memcpy(frame.data + 2, &right_rpm, 2);
write(socket_, &frame, sizeof(frame));
}
};
```
六、多模态融合:语音+视觉
语音控制只是交互入口,真正智能的地方在于语音指令和视觉感知的融合。比如用户说"往前走",但摄像头看到前方有行人,系统应该拒绝执行或自动绕开。
```cpp
class SafetyOverride {
public:
bool IsSafe(const VehicleCommand& cmd, const PerceptionResult& perception) {
if (cmd.type == CommandType::MOVE_FORWARD) {
for (const auto& obj : perception.obstacles) {
if (obj.distance < 2.0f && abs(obj.angle) < 30.0f) {
return false;
}
}
}
return true;
}
VehicleCommand Adjust(const VehicleCommand& cmd, const PerceptionResult& perception) {
if (cmd.type == CommandType::MOVE_FORWARD) {
float min_dist = 999.0f;
float obstacle_angle = 0.0f;
for (const auto& obj : perception.obstacles) {
if (obj.distance < min_dist && abs(obj.angle) < 45.0f) {
min_dist = obj.distance;
obstacle_angle = obj.angle;
}
}
if (min_dist < 3.0f) {
VehicleCommand adjusted;
adjusted.type = obstacle_angle < 0 ? TURN_RIGHT : TURN_LEFT;
adjusted.angle = 30.0f;
return adjusted;
}
}
return cmd;
}
};
```
七、语音反馈
板端通过WebSocket把状态推送给手机APP,APP用TTS播报:
```kotlin
val tts = TextToSpeech(context) { status ->
if (status == TextToSpeech.SUCCESS) {
tts.language = Locale.CHINESE
}
}
fun onStatusReceived(status: VehicleStatus) {
when {
status.obstacleDetected -> {
tts.speak("前方检测到障碍物,正在绕行", TextToSpeech.QUEUE_ADD, null, null)
}
status.safetyState == "SAFE" -> {
tts.speak("系统检测到异常,已停车", TextToSpeech.QUEUE_ADD, null, null)
}
}
}
```
八、注意事项总结
1. 语音识别:在线+离线结合,手机端ASR降低板端计算压力。
2. NLU设计:规则匹配为主覆盖90%场景,复杂指令再考虑模型。
3. 通信协议:WebSocket全双工,JSON协议简单清晰,状态主动推送。
4. 安全策略:语音指令必须经过视觉感知的安全校验,不能盲执行。
5. CAN优先级:安全指令永远最高优先级,防止控制冲突。
6. WiFi时效性:加timestamp和预发送机制,降低网络抖动影响。
