- Управление голосом с помощью Андроид и Ардуино

Андроид и Ардуино в задачах управления голосом и синтеза речи с использованием Bluetooth

Практика для студентов. Мясищев А.А.

В настоящее время наблюдается значительный рост интереса к технологиям, связанным с распознаванием речи. Например, задачи управления устройствами с помощью голосовых команд. В последнее время появилась возможность управления домашней, офисной техникой с помощью Андроид - устройств голосовыми командами, что широко рекламируется как технология "умный дом".

Здесь представлена программа голосового управления Ардуино с помощью Андроид - устройства через Bluetooth HC-05. К Ардуино подключены три исполнительных механизма и датчик температуры. После выполнения соответствующих команд программа синтезирует голосовое сообщения о результате работы. Этот пример является типичной подзадачей общей задачи "Умный дом". Программа также реализует простейший диалог между устройством и пользователем, отвечая на простые вопросы.Для распознавания и синтеза речи используется инструментарий Google. Если смартфон поддерживает голосовой поиск в режиме оффлайн, доступ к Интернет не обязателен. Программа для Андроид написана в среде Android Studio(язык Java), а для Ардуино - в среде разработки Ардуино на C++(проект Wiring). Тестировалась на телефоне LG G3 Stylus и планшете Acer A500.

Видео демонстрацию работы программы можно посмотреть здесь

Программа на Андроид состоит из двух больших блоков - это блок начала работы приложения (I) и блок работы приложения после нажатия на кнопку "Нажми для начала диалога"(II). На рисунке 1 показана блочная последовательность действий, которые выполняются каждым блоком(I, II).

Рис.1. Блок I и блок II, схематично поясняющие работу программы приложения

Здесь приняты следующие сокращения(см. также текст программы):

1. Блок I:

1.1. Запуск прил. - выполнить запуск приложения на Андроиде;

1.2. Подкл. Blu - запускается метод bluet(), который подключает Андроид к Ардуино через Bluetooth;

1.3. Blu - выполняется проверка, подключены ли Андроид и Ардуино по Bluetooth;

1.3. Зап. пр. Т0 - каждую секунду запускается метод run() класса bluetoothInOut, который посылает запрос к Ардуино для определения температуры и выполняет считывание с Ардуино значения температуры. Таким образом каждую секунду Андроид получает новое значение температуры;

1.4. Ож.Кн. - после запуска приложение переходит в ожидание нажатия на кнопку "Нажми для начала диалога".

2. Блок II:

2.1. Кн - нажатие на кнопку "Нажми для начала диалога";

2.2. Blu - выполняется проверка, подключены ли Андроид и Ардуино по Bluetooth;

2.3. Ост. таймер - останавливается таймер, который запускает метод run() класса bluetoothInOut;

2.4. Зап. пр. Т0 - каждую секунду запускается метод run() класса bluetoothInOut, который посылает запрос к Ардуино для определения температуры и выполняет считывание с Ардуино значения температуры.

2.5. Зап. РГ - запускается распознаватель голоса, метод spi();

2.6. Резул. РГ - метод onActivityResult получает результаты распознавания голоса и выполняет их сравнение со строкой вопросов и команд;

2.7. Blu - выполняется проверка, подключены ли Андроид и Ардуино по Bluetooth;

2.8. Ср. РР с коман. - сравнивается результат распознавания голоса с прописанной командой. При совпадении задается значение переменной value кодом символа. Например, символ "1", соответствующий включению RED(см. программу для Ардуино), представлен кодом 49 и т.д.;

2.9. V54 - выполняется условие value != 54. Код 54 (символ "6") - это код запроса температуры от Ардуино;

2.10. Чтен. темпер. - распечатывается значение температуры в строке вывода текста temtext и строковой переменной spout присваивается ее значение, которое Андроид получает каждую секунду от Ардуино;

2.11. Посыл. ком. - через Bluetooth на Ардуино посылается байт данных (символ) для включения или выключения соответствующего устройства(RED, GREEN, BLUE);

2.12. Синтез РР - вызывается метод tts.speak, выполняющий аудио синтез ответа на произнесенный вопрос, результат выполнения команды, значение температуры или произнесенную фразу, если нет соответствия между вопросом и ответом(т.е. система работает как попугай);

2.13. Просл. Аудио - здесь работает метод onDone класса utteranceProgressListener который следит за окончанием генерации динамиком звука синтеза речи;

2.14. АЗ - проверка завершения аудио вывода синтезатором речи;

2.15. КС - проверка произнесения фразы "конец связи";

2.16. Blu - выполняется проверка, подключены ли Андроид и Ардуино по Bluetooth;

2.17. Ост. таймер - останавливается таймер, который запускает метод run() класса bluetoothInOut;

2.18. Зап. пр. Т1 - каждую секунду запускается метод run() класса bluetoothInOut, который выполняет считывание с Ардуино значения температуры(запрос на температуру не посылается);

2.19. Ож. кн. - приложение переходит в ожидание нажатия на кнопку "Нажми для начала диалога".

Необходимо остановиться на следующих особенностях работы программы.

1. Перед запуском синтезатора речи необходима его инициализация. Для этого основной класс MainActivity создается с использованием интерфейса TextToSpeech.OnInitListener. При инициализации используется метод onInit(int status).

2. Для работы метода onDone класса utteranceProgressListener, который следит за окончанием генерации динамиком звука синтеза речи, необходимо описание хеш карты для хранения ключа в params. При инициализации синтезатора необходимо выполнить передачу ключа с помощью метода params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID," HELLO"). Необходимо также установить слушатель синтеза речи tts.setOnUtteranceProgressListener(new utteranceProgressListener());

3. Чтение температуры с Ардуино выполняется раз в одну секунду с помощью классов Timer и TimerTask, которые выполняют запуск задачи в определенное время в будущем через определенные интервалы(500 и 1000 миллисекунд соответственно по программе). Попытка организации запроса температуры с последующим ее чтением в основном потоке Thread приводит к тому, что для получения текущего значения температуры необходим ее повторный запрос. Т.е. для получения правильного текущего значения температуры необходимо два раза сказать: "температура". Использование отдельного потока Thread с классом Handler работает правильно но приводит к ошибочной передаче кириллицы. Латиница передается верно, как это представлено в работе http://cxem.net/arduino/arduino64.php. Поэтому здесь использовался способ чтения температуры с одновременным ее запросом с Андроид каждую секунду в переменную readMessage, которая доступна в любой момент синтезатору голоса. При команде "температура" ее значение берется из строки readMessage и синтезируется синтезатором.

Программа на Ардуино достаточно проста (см. текст ниже) и ее работа понятна из комментариев.

На рисунке 2. показана схема подключения к Arduino UNO Bluetooth HC-05, датчика температуры DS18B20 и трех светодиодов красного, зеленого и синего цвета (альтернатива трем исполнительным механизмам, например свет, телевизор и кондиционер).

Рис.2. Схема подключения устройств к Ардуино

Программа на java (Android Studio 1.0.1)

package net.sytes.text_speech_bl1;

import java.io.DataInputStream;

import java.io.IOException;

import java.io.InputStream;

import java.io.OutputStream;

import java.util.ArrayList;

import java.util.HashMap;

import java.util.Locale;

import java.util.Timer;

import java.util.TimerTask;

import java.util.UUID;

import android.bluetooth.BluetoothAdapter;

import android.bluetooth.BluetoothDevice;

import android.bluetooth.BluetoothSocket;

import android.app.Activity;

import android.content.Intent;

import android.os.Bundle;

import android.speech.RecognizerIntent;

import android.speech.tts.TextToSpeech;

import android.speech.tts.UtteranceProgressListener;

import android.util.Log;

import android.view.View;

import android.widget.TextView;

import android.widget.Toast;

public class MainActivity extends Activity implements TextToSpeech.OnInitListener {//Создание класса с

// использованием интерфейса TextToSpeech.OnInitListener для инициализации синтезатора

public int upbluetooth=1;

public int r19=10;

public static final int CODE = 1234;

public TextToSpeech tts;

private TextView txtText;

public TextView temtext;

public int value=100;

private int bluecancel=0;

public int bytes;

public byte[] buffer = new byte[128];

public String readMessage;

public Timer time;

//Socket, с помощью которого будут отправляться данные на bluetooth Arduino

private BluetoothSocket clientSocket;

// UUID для случая подключения к последовательному bluetooth устройству

private UUID uuid = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");

public String spout;

public String sp;

// Примеры строк вопросов

public String sp0 = "выключить красный";

public String sp1 = "включить красный";

public String sp2 = "выключить зеленый";

public String sp3 = "включить зеленый";

public String sp4 = "выключить синий";

public String sp5 = "включить синий";

public String sp6 = "температура";

public String sp10 = "как тебя зовут";

public String sp11 = "что ты умеешь";

public String sp12 = "твои команды";

public String sp13 = "спасибо";

public String sp14 = "откуда ты";

public String sp15 = "привет";

public String sp19 = "конец связи";

// Примеры строк ответов

public String sp20 = "меня зовут маша";

public String sp21 = "я могу включать и выключать свет и определять температуру";

public String sp22 = "Включить зеленый. Выключить зеленый. То же для красного и синего. Температура.";

public String sp23 = "пожалуйста";

public String sp24 = "я из хмельницкого";

public String sp25 = "здравствуйте";

public String sp29 = "до свидания";

public String sp30 = "красный выключила";

public String sp31 = "красный включила";

public String sp32 = "зеленый выключила";

public String sp33 = "зеленый включила";

public String sp34 = "синий выключила";

public String sp35 = "синий включила";

// Описание хеш карты для хранения

// ключа (TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID - в нашем случае)

// и любого значения - строки (например - "5678")

private HashMap<String, String> params = new HashMap<>();

@Override

public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

txtText = (TextView) findViewById(R.id.input_text);

temtext = (TextView) findViewById(R.id.textView2);

tts = new TextToSpeech(this, this);

tts.setOnUtteranceProgressListener(new utteranceProgressListener()); //Установка слушателя синтеза речи

bluet(); // Подключение bluetooth

}

public void spi() { // Вызов активности распознавания речи

Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);

intent.putExtra(RecognizerIntent.EXTRA_PROMPT, "Говори громко и четко!");

startActivityForResult(intent, CODE);

}

@Override

public void onDestroy() {

super.onDestroy();

// Остановить синтезатор речи tts!

if (tts != null) {

tts.stop();

tts.shutdown();

}

try {

clientSocket.close(); // Закрыть соединение с bluetooth

} catch (IOException e2) { }

}

@Override

public void onInit(int status) { // Инициализация перед синтезом речи

if (status == TextToSpeech.SUCCESS) {

int result = tts.setLanguage(Locale.getDefault());

params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID," HELLO");

if (result == TextToSpeech.LANG_MISSING_DATA

|| result == TextToSpeech.LANG_NOT_SUPPORTED) {

Log.e("TTS", "This Language is not supported");

}

} else {

Log.e("TTS", "Init Failed!");

}

}

protected void onActivityResult(int requestCode, int resultCode, Intent data) {//Вызывается после

// получения результата распознавания голоса

super.onActivityResult(requestCode, resultCode, data);

ArrayList<String> spee;

value=100;

switch (requestCode){

case CODE: {

if (resultCode == RESULT_OK && null != data) {

// получаем список текстовых строк - результат распознавания.

// Строк может быть несколько, так как не всегда удается точно распознать речь.

// Более правильные результаты идут в начале списка

spee = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);

sp = spee.get(0).toString();

txtText.setText(sp);

spout = sp;

int r0 = sp.compareTo(sp0);

int r1 = sp.compareTo(sp1);

int r2 = sp.compareTo(sp2);

int r3 = sp.compareTo(sp3);

int r4 = sp.compareTo(sp4);

int r5 = sp.compareTo(sp5);

int r6 = sp.compareTo(sp6);

int r10 = sp.compareTo(sp10);

int r11 = sp.compareTo(sp11);

int r12 = sp.compareTo(sp12);

int r13 = sp.compareTo(sp13);

int r14 = sp.compareTo(sp14);

int r15 = sp.compareTo(sp15);

int r19 = sp.compareTo(sp19);

if (r0 == 0) spout = sp30;

if (r1 == 0) spout = sp31;

if (r2 == 0) spout = sp32;

if (r3 == 0) spout = sp33;

if (r4 == 0) spout = sp34;

if (r5 == 0) spout = sp35;

if (r6 == 0) spout = sp6;

if (r10 == 0) spout = sp20;

if (r11 == 0) spout = sp21;

if (r12 == 0) spout = sp22;

if (r13 == 0) spout = sp23;

if (r14 == 0) spout = sp24;

if (r15 == 0) spout = sp25;

if (r19 == 0) spout = sp29;

if (upbluetooth == 0) { // Если подключение к bluetooth существует то

// результат сравнения представляем символом

// Например, если результат распознавания голоса соответствует строке

// "включить зеленый" то на bluetooth посылаем символ 3 (код 51)

if (r1 == 0) value = 49;

if (r0 == 0) value = 48;

if (r3 == 0) value = 51;

if (r2 == 0) value = 50;

if (r5 == 0) value = 53;

if (r4 == 0) value = 52;

if (r6 == 0) value = 54;

// Посылаем данные

if (value != 54) outData(value);

if (value == 54) {

temtext.setText(readMessage); // Распечатываем температуру

spout = readMessage;

}

}

tts.speak(spout, TextToSpeech.QUEUE_ADD, params); // Синтезировать речь

}

}

break;

}

}

public void click(View view) {

if(upbluetooth==0) { // Если Bluetooth подключен

// При нажатии на кнопку останавливаю таймер работы программы по передаче и чтению температуры

time.cancel(); time.purge();

// Запускаю таймер работы программы с новыми начальными

// данными(посылка запроса температуры и прием температуры)

bluecancel = 0;

time = new Timer();

bluetoothInOut bInOut = new bluetoothInOut();

time.schedule(bInOut,500,1000);

}

spi(); // Запускаю распознаватель речи

}

public void bluet() { // Подключение к bluetooth

Intent enableBt = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);

enableBt.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

startActivity(enableBt);

// Используется bluetooth по умолчанию

BluetoothAdapter bluetooth = BluetoothAdapter.getDefaultAdapter();

try {

// Выбираем bluetooth с конкретным адресом для простоты

BluetoothDevice device = bluetooth.getRemoteDevice("98:D3:31:B0:86:16");

// Создание RFCOMM секретного socket - а для входящих и исходящих сообщений

clientSocket = device.createRfcommSocketToServiceRecord(uuid);

// Попытка подключения к удаленному bluetooth

clientSocket.connect();

// Если попытка удалась, выводится сообщение внизу экрана

Toast.makeText(getApplicationContext(), "Связь с bluetooth установлена", Toast.LENGTH_LONG).show();

upbluetooth=0;

time = new Timer();

bluetoothInOut bInOut = new bluetoothInOut();

time.schedule(bInOut,500,1000); //Через 500 миллисекунд после запуска программы начинать

// запрашивать температуру каждую секунду

}

//В случае появления ошибок сообщаем, что bluetooth не подключен

catch (IOException e) {

upbluetooth=1;

Toast.makeText(getApplicationContext(), "Проверь bluetooth!", Toast.LENGTH_LONG).show();

}

}

// Создаем класс, который необходим для фиксации окончания синтеза речи

// для последующего запуска активити распознавания голоса

public class utteranceProgressListener extends UtteranceProgressListener {

@Override

public void onDone(String utteranceId) { // Действия после окончания речи синтезатором

r19 = sp.compareTo(sp19);

if(r19 != 0) spi(); // Если не "конец связи", то активити распознавания голоса запускается вновь

else {

if(upbluetooth==0) { // Если Bluetooth включен

// и если произнесено "конец связи" - останавливаю таймер

time.cancel(); time.purge();

// и запускаю таймер работы программы с новыми начальными данными

// (без посылки запроса температуры но с приемом температуры)

bluecancel = 1;

time = new Timer();

bluetoothInOut bInOut = new bluetoothInOut();

time.schedule(bInOut,500,1000);

}

}

Log.d("TtsUtteranceListener", "utterance Done: " + utteranceId);

}

@Override

public void onStart(String utteranceId) {

Log.d("TtsUtteranceListener", "utterance Start: " + utteranceId);

}

@Override

public void onError(String utteranceId) {

txtText.setText("Error");

Log.d("TtsUtteranceListener", "utterance Error: " + utteranceId);

}

}

public void outData(int value) {

try {

// Получаем выходной поток для передачи данных

OutputStream outStream = clientSocket.getOutputStream();

// Посылаем данные

outStream.write(value);

} catch (IOException e) {

}

}

public class bluetoothInOut extends TimerTask {

public void run() {

try {

if( bluecancel == 0 ) // Только в этом случае посылаем запрос температуры

{

OutputStream outStream = clientSocket.getOutputStream();

outStream.write(54);}

// Получаем входной поток для приема данных

InputStream inb = clientSocket.getInputStream();

// Преобразование входного потока от bluetooth в строку

DataInputStream in = new DataInputStream(inb);

bytes = in.read(buffer);

if (bytes > 10 ) // Если через bluetooth получено (например) больше 10 байт, то

{

// преобразуем байты в строку с нулевого индекса до индекса bytes

readMessage = new String(buffer, 0, bytes);

}

} catch (IOException e) {

}

}

}

}

Файл разметки

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

xmlns:tools="http://schemas.android.com/tools"

android:layout_width="match_parent"

android:layout_height="match_parent"

android:paddingLeft="@dimen/activity_horizontal_margin"

android:paddingRight="@dimen/activity_horizontal_margin"

android:paddingTop="@dimen/activity_vertical_margin"

android:paddingBottom="@dimen/activity_vertical_margin"

tools:context=".MainActivity"

android:background="#ff074bff">

<TextView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_marginTop="68dp"

android:hint="Распознанный текст"

android:id="@+id/input_text"

android:textSize="45dp"

android:gravity="center"

android:textColor="#ff3c09ff"

android:background="#ff05ff14"

android:textColorHint="#ff2f06ff"

android:width="600dp"

android:layout_alignParentTop="true"

android:layout_centerHorizontal="true" />

<Button

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="Нажми для начала диалога"

android:id="@+id/button"

android:textSize="65dp"

android:onClick="click"

android:textColor="#ffffffff"

android:textStyle="bold"

android:background="#ffff04bb"

android:layout_marginBottom="187dp"

android:layout_alignParentBottom="true"

android:layout_alignParentStart="true" />

<TextView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:id="@+id/textView2"

android:hint="Температура"

android:textSize="45dp"

android:gravity="center"

android:textColorHint="#ff1004ff"

android:background="#fffcfffa"

android:textColor="#ff0f07ff"

android:width="600dp"

android:layout_marginTop="270dp"

android:layout_below="@+id/input_text"

android:layout_centerHorizontal="true" />

</RelativeLayout>

Файл манифеста

<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

package="net.sytes.text_speech_bl1" >

<uses-permission android:name="android.permission.BLUETOOTH" />

<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

<application

android:allowBackup="true"

android:icon="@drawable/ic_launcher"

android:label="@string/app_name"

android:theme="@style/AppTheme" >

<activity

android:name=".MainActivity"

android:label="@string/app_name" >

<intent-filter>

<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />

</intent-filter>

</activity>

</application>

</manifest>

В файле манифеста обязательно необходимо дать разрешение устройству Bluetooth:

<uses-permission android:name="android.permission.BLUETOOTH" />

<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

Программа для Ардуино написана в стандартной среде разработки Ардуино:

#include <OneWire.h> //Подключаем описание библиотеки шины OneWire

#include <DallasTemperature.h> //Подключаем описание библиотеки для определения температуры(DS18B20)

#define ONE_WIRE_BUS 5 //Датчик температуры подключен к 5-му выводу Ардуино

char inByte; // входящие данные

int RED = 2; // RED подключен к 2 выводу

int GR = 4; // GREEN подключен к 4 выводу

int BL = 6; // BLUE подключен к 6 выводу

OneWire oneWire(ONE_WIRE_BUS); //Настройка шины 1wire для работы с 5-м выводом Ардуино

DallasTemperature sensors(&oneWire); //Подключаем датчик температуры

void setup() {

Serial.begin(9600); // инициализация порта

sensors.begin(); //Инициализация датчика температуры DS18B20

pinMode(RED, OUTPUT); //Установка 2-го вывода на выход

pinMode(GR, OUTPUT); //Установка 4-го вывода на выход

pinMode(BL, OUTPUT); //Установка 6-го вывода на выход

sensors.requestTemperatures(); // Запрос температуры

int temp=sensors.getTempCByIndex(0); // Получение температуры с нулевого датчика

}

void loop() {

if (Serial.available() > 0) { //если пришли данные

inByte = Serial.read(); // считываем байт

if(inByte == '0') {

digitalWrite(RED, LOW); // если 0, то выключаем RED

}

if(inByte == '1') {

digitalWrite(RED, HIGH); // если 1, то включаем RED

}

if(inByte == '2') {

digitalWrite(GR, LOW); // если 2, то выключаем GREEN

}

if(inByte == '3') {

digitalWrite(GR, HIGH); // если 3, то включаем GREEN

}

if(inByte == '4') {

digitalWrite(BL, LOW); // если 4, то выключаем BLUE

}

if(inByte == '5') {

digitalWrite(BL, HIGH); // если 5, то включаем BLUE

}

if(inByte == '6') { // если 6, то считываем температуру и посылаем ее на bluetooth

sensors.requestTemperatures();

int temp=sensors.getTempCByIndex(0);

Serial.print("Температура, ");

Serial.print(temp); Serial.print(" ");

}

}

}

Загрузочный файл для планшета Acer A500 можно взять здесь

Написано 21.03.2015г.