随着物联网(IoT)的发展,开发人员和工程师不得不重新考虑用户如何与设备进行日常交互。
尽管屏幕对于网站和大多数应用程序运行良好,但是如果您必须使用多个按钮或屏幕才能正常运行,那么与现实世界进行交互的设备可能会更加繁琐。 解决此问题的方法之一是在设备上启用语音控制。
在本教程中,您将了解Google Assistant以及如何将其添加到Android Things IoT设备。
助理SDK
Google Assistant SDK可让您向IoT设备添加具有关键字检测,自然语言处理和其他机器学习功能的语音控件。 使用Assistant SDK可以完成很多工作,但是本教程仅关注基础知识:如何将其包含在Android Things设备中,以便提出问题,获取信息以及与标准的“开箱即用”进行交互助手功能。
至于硬件要求,您有几种选择。 您可以将带有Android Things的Raspberry Pi与AIY语音工具包一起使用 。

或者,您可以使用带有AUX连接器和USB麦克风的标准扬声器。

另外,您可以使用任何其他I²S硬件配置。 尽管我们不会在本教程中详细讨论I²S,但值得注意的是,语音工具包将使用此协议。 设置好麦克风和扬声器后,还需要在设备上添加一个按钮。 此按钮需要跟踪两种状态:按下和释放。 您可以通过多管拱廊按钮或在两个电极之一上连接了下拉电阻的标准按钮来完成此操作。
证书
连接硬件后,就可以将Assistant SDK添加到设备中了。 首先,您将需要为设备创建一个新的凭证文件。 您可以在Google助手文档中找到相关说明。 拥有了certificate.json文件后,您需要将其放入Android Things模块的res / raw目录中。

使用Google创建凭据后,您需要为您的应用声明一些权限。 打开AndroidManifest.xml文件,并在manifest
标记中但在application
标记之前添加以下几行。
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="com.google.android.things.permission.MANAGE_AUDIO_DRIVERS" />
值得注意的是,在安装具有这些权限的应用程序后,需要重新启动设备才能授予它们。
接下来,您需要将gRPC模块复制到您的应用中,以便与家用设备进行通信。 这有点棘手,所以最好的地方是来自Google Assistant Android Things示例应用程序,该应用程序可在Android Things GitHub帐户中找到 。 然后,您将需要更新您的settings.gradle文件以反映新模块。
include ':mobile', ':things', ':grpc'
更新settings.gradle之后 ,通过在物联网模块的build.gradle文件中包含以下行,将该模块作为您物联网模块的依赖项,并包括Google的按钮驱动程序(激活麦克风需要此驱动程序)和可选的Voice Hat驱动程序(如果需要)您正在使用该硬件。
compile project(':grpc')
compile 'com.google.android.things.contrib:driver-button:0.4'
//optional
compile 'com.google.android.things.contrib:driver-voicehat:0.2'
您还需要在项目级别的build.gradle文件中包含protobuf作为依赖项 。
classpath "com.google.protobuf:protobuf-gradle-plugin:0.8.0"
接下来,通过打开事物模块的build.gradle文件并在dependencies
节点下添加以下内容,将oauth2库包含在我们的项目中:
compile('com.google.auth:google-auth-library-oauth2-https:0.6.0') {
exclude group: 'org.apache.httpcomponents', module: 'httpclient'
}
如果您的项目具有Espresso依赖项,那么您可能会在此处遇到冲突,并显示类似以下错误消息:
Warning:Conflict with dependency 'com.google.code.findbugs:jsr305' in project ':things'. Resolved versions for app (1.3.9) and test app (2.0.1) differ. See http://g.co/androidstudio/app-test-app-conflict for details.
如果是这样,只需从build.gradle中删除Espresso依赖项 。
同步项目后,创建一个名为Credentials.java的新类以访问您的凭据。
public class Credentials {
static UserCredentials fromResource(Context context, int resourceId)
throws IOException, JSONException {
InputStream is = context.getResources().openRawResource(resourceId);
byte[] bytes = new byte[is.available()];
is.read(bytes);
JSONObject json = new JSONObject(new String(bytes, "UTF-8"));
return new UserCredentials(json.getString("client_id"),
json.getString("client_secret"),
json.getString("refresh_token")
);
}
}
}
嵌入式助手助手类
创建Credentials.java类之后,就该创建一个名为EmbeddedAssistant.java的新类了。 这是一个帮助程序类,最初是由Google的工程师编写的,用于轻松包装适用于Android Things的Google助手。 尽管仅通过将其包含在您的项目中就可以很容易地使用该类,但我们将要深入研究它并了解其实际工作原理。
您要做的第一件事是创建两个内部抽象类,这些抽象类将用于处理对话中的回调和对Assistant API的请求。
public class EmbeddedAssistant {
public static abstract class RequestCallback {
public void onRequestStart() {}
public void onAudioRecording() {}
public void onSpeechRecognition(String utterance) {}
}
public static abstract class ConversationCallback {
public void onResponseStarted() {}
public void onResponseFinished() {}
public void onConversationEvent(EventType eventType) {}
public void onAudioSample(ByteBuffer audioSample) {}
public void onConversationError(Status error) {}
public void onError(Throwable throwable) {}
public void onVolumeChanged(int percentage) {}
public void onConversationFinished() {}
}
}
编写完两个内部类之后,继续在类顶部定义以下一组全局值。 其中大多数将在此文件中稍后初始化。 这些值用于跟踪设备状态以及与Assistant API的交互。
private static final String ASSISTANT_API_ENDPOINT = "embeddedassistant.googleapis.com";
private static final int AUDIO_RECORD_BLOCK_SIZE = 1024;
private RequestCallback mRequestCallback;
private ConversationCallback mConversationCallback;
//Used for push-to-talk functionality
private ByteString mConversationState;
private AudioInConfig mAudioInConfig;
private AudioOutConfig mAudioOutConfig;
private AudioTrack mAudioTrack;
private AudioRecord mAudioRecord;
private int mVolume = 100; // Default to maximum volume.
private UserCredentials mUserCredentials;
private MicrophoneMode mMicrophoneMode;
private HandlerThread mAssistantThread;
private Handler mAssistantHandler;
// gRPC client and stream observers.
private int mAudioOutSize; // Tracks the size of audio responses to determine when it ends.
private EmbeddedAssistantGrpc.EmbeddedAssistantStub mAssistantService;
private StreamObserver<ConverseRequest> mAssistantRequestObserver;
处理API回应
尽管上面有一个StreamObserver<ConverseRequest>
对象,用于请求对Assistant API的请求,但您也将需要一个对象作为响应。 该对象将包含一个switch语句,该语句检查响应的状态,然后进行相应的处理。
private StreamObserver<ConverseResponse> mAssistantResponseObserver =
new StreamObserver<ConverseResponse>() {
@Override
public void onNext(ConverseResponse value) {
switch (value.getConverseResponseCase()) {
第一种情况检查用户发言的结尾,并使用ConversationCallback
让类的其余部分知道即将到来的响应。
case EVENT_TYPE:
mConversationCallback.onConversationEvent(value.getEventType());
if (value.getEventType() == EventType.END_OF_UTTERANCE) {
mConversationCallback.onResponseStarted();
}
break;
下一种情况将检查并更新对话,音量和麦克风状态。
case RESULT:
// Update state.
mConversationState = value.getResult().getConversationState();
// Update volume.
if (value.getResult().getVolumePercentage() != 0) {
int volumePercentage = value.getResult().getVolumePercentage();
mVolume = volumePercentage;
mAudioTrack.setVolume(AudioTrack.getMaxVolume()
* volumePercentage / 100.0f);
mConversationCallback.onVolumeChanged(volumePercentage);
}
if (value.getResult().getSpokenRequestText() != null &&
!value.getResult().getSpokenRequestText().isEmpty()) {
mRequestCallback.onSpeechRecognition(value.getResult()
.getSpokenRequestText());
}
// Update microphone mode.
mMicrophoneMode = value.getResult().getMicrophoneMode();
break;
第三种情况将获取音频结果并为用户播放。
case AUDIO_OUT:
if (mAudioOutSize <= value.getAudioOut().getSerializedSize()) {
mAudioOutSize = value.getAudioOut().getSerializedSize();
} else {
mAudioOutSize = 0;
onCompleted();
}
final ByteBuffer audioData =
ByteBuffer.wrap(value.getAudioOut().getAudioData().toByteArray());
mAudioTrack.write(audioData, audioData.remaining(),
AudioTrack.WRITE_BLOCKING);
mConversationCallback.onAudioSample(audioData);
break;
最后一种情况将只是转发在对话过程中发生的错误。
case ERROR:
mConversationCallback.onConversationError(value.getError());
break;
此流中的最后两种方法处理错误状态,并在对话结果完成时进行清除。
@Override
public void onError(Throwable t) {
mConversationCallback.onError(t);
}
@Override
public void onCompleted() {
mConversationCallback.onResponseFinished();
if (mMicrophoneMode == MicrophoneMode.DIALOG_FOLLOW_ON) {
// Automatically start a new request
startConversation();
} else {
// The conversation is done
mConversationCallback.onConversationFinished();
}
}
流音频
接下来,您将需要创建一个Runnable
来处理不同线程上的音频流。
private Runnable mStreamAssistantRequest = new Runnable() {
@Override
public void run() {
ByteBuffer audioData = ByteBuffer.allocateDirect(AUDIO_RECORD_BLOCK_SIZE);
int result = mAudioRecord.read(audioData, audioData.capacity(),
AudioRecord.READ_BLOCKING);
if (result < 0) {
return;
}
mRequestCallback.onAudioRecording();
mAssistantRequestObserver.onNext(ConverseRequest.newBuilder()
.setAudioIn(ByteString.copyFrom(audioData))
.build());
mAssistantHandler.post(mStreamAssistantRequest);
}
};
创建助手
现在,您已定义了全局值,现在该介绍创建EmbeddedAssistant
的框架了。 您将需要能够使用之前创建的Credentials.java类检索应用程序的凭据 。
public static UserCredentials generateCredentials(Context context, int resourceId)
throws IOException, JSONException {
return Credentials.fromResource(context, resourceId);
}
为了实例化自身,此类使用私有构造函数和构建器模式。
private EmbeddedAssistant() {}
public static class Builder {
private EmbeddedAssistant mEmbeddedAssistant;
private int mSampleRate;
public Builder() {
mEmbeddedAssistant = new EmbeddedAssistant();
}
Builder
内部类包含用于初始化EmbeddedAssistant
类中的值的多种方法,例如采样率,数量和用户凭证。 一旦调用build()
方法,将在EmbeddedAssistant
上设置所有定义的值,将配置操作所需的全局对象,如果缺少任何必要的数据,将引发错误。
public Builder setRequestCallback(RequestCallback requestCallback) {
mEmbeddedAssistant.mRequestCallback = requestCallback;
return this;
}
public Builder setConversationCallback(ConversationCallback responseCallback) {
mEmbeddedAssistant.mConversationCallback = responseCallback;
return this;
}
public Builder setCredentials(UserCredentials userCredentials) {
mEmbeddedAssistant.mUserCredentials = userCredentials;
return this;
}
public Builder setAudioSampleRate(int sampleRate) {
mSampleRate = sampleRate;
return this;
}
public Builder setAudioVolume(int volume) {
mEmbeddedAssistant.mVolume = volume;
return this;
}
public EmbeddedAssistant build() {
if (mEmbeddedAssistant.mRequestCallback == null) {
throw new NullPointerException("There must be a defined RequestCallback");
}
if (mEmbeddedAssistant.mConversationCallback == null) {
throw new NullPointerException("There must be a defined ConversationCallback");
}
if (mEmbeddedAssistant.mUserCredentials == null) {
throw new NullPointerException("There must be provided credentials");
}
if (mSampleRate == 0) {
throw new NullPointerException("There must be a defined sample rate");
}
final int audioEncoding = AudioFormat.ENCODING_PCM_16BIT;
// Construct audio configurations.
mEmbeddedAssistant.mAudioInConfig = AudioInConfig.newBuilder()
.setEncoding(AudioInConfig.Encoding.LINEAR16)
.setSampleRateHertz(mSampleRate)
.build();
mEmbeddedAssistant.mAudioOutConfig = AudioOutConfig.newBuilder()
.setEncoding(AudioOutConfig.Encoding.LINEAR16)
.setSampleRateHertz(mSampleRate)
.setVolumePercentage(mEmbeddedAssistant.mVolume)
.build();
// Construct AudioRecord & AudioTrack
AudioFormat audioFormatOutputMono = new AudioFormat.Builder()
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
.setEncoding(audioEncoding)
.setSampleRate(mSampleRate)
.build();
int outputBufferSize = AudioTrack.getMinBufferSize(audioFormatOutputMono.getSampleRate(),
audioFormatOutputMono.getChannelMask(),
audioFormatOutputMono.getEncoding());
mEmbeddedAssistant.mAudioTrack = new AudioTrack.Builder()
.setAudioFormat(audioFormatOutputMono)
.setBufferSizeInBytes(outputBufferSize)
.build();
mEmbeddedAssistant.mAudioTrack.setVolume(mEmbeddedAssistant.mVolume *
AudioTrack.getMaxVolume() / 100.0f);
mEmbeddedAssistant.mAudioTrack.play();
AudioFormat audioFormatInputMono = new AudioFormat.Builder()
.setChannelMask(AudioFormat.CHANNEL_IN_MONO)
.setEncoding(audioEncoding)
.setSampleRate(mSampleRate)
.build();
int inputBufferSize = AudioRecord.getMinBufferSize(audioFormatInputMono.getSampleRate(),
audioFormatInputMono.getChannelMask(),
audioFormatInputMono.getEncoding());
mEmbeddedAssistant.mAudioRecord = new AudioRecord.Builder()
.setAudioSource(AudioSource.VOICE_RECOGNITION)
.setAudioFormat(audioFormatInputMono)
.setBufferSizeInBytes(inputBufferSize)
.build();
return mEmbeddedAssistant;
}
}
连接到助手API
在创建EmbeddedAssistant
之后,将需要调用connect()
方法以连接到Assistant API。
public void connect() {
mAssistantThread = new HandlerThread("assistantThread");
mAssistantThread.start();
mAssistantHandler = new Handler(mAssistantThread.getLooper());
ManagedChannel channel = ManagedChannelBuilder.forTarget(ASSISTANT_API_ENDPOINT).build();
mAssistantService = EmbeddedAssistantGrpc.newStub(channel)
.withCallCredentials(MoreCallCredentials.from(mUserCredentials));
}
连接到API后,将使用两种方法来启动和停止对话。 这些方法会将Runnable
对象发布到mAssistantHandler
,以便将会话状态对象传递到请求和响应流。
public void startConversation() {
mAudioRecord.startRecording();
mRequestCallback.onRequestStart();
mAssistantHandler.post(new Runnable() {
@Override
public void run() {
mAssistantRequestObserver = mAssistantService.converse(mAssistantResponseObserver);
ConverseConfig.Builder converseConfigBuilder = ConverseConfig.newBuilder()
.setAudioInConfig(mAudioInConfig)
.setAudioOutConfig(mAudioOutConfig);
if (mConversationState != null) {
converseConfigBuilder.setConverseState(ConverseState.newBuilder()
.setConversationState(mConversationState)
.build());
}
mAssistantRequestObserver.onNext(
ConverseRequest.newBuilder()
.setConfig(converseConfigBuilder.build())
.build());
}
});
mAssistantHandler.post(mStreamAssistantRequest);
}
public void stopConversation() {
mAssistantHandler.post(new Runnable() {
@Override
public void run() {
mAssistantHandler.removeCallbacks(mStreamAssistantRequest);
if (mAssistantRequestObserver != null) {
mAssistantRequestObserver.onCompleted();
mAssistantRequestObserver = null;
}
}
});
mAudioRecord.stop();
mAudioTrack.play();
mConversationCallback.onConversationFinished();
}
关机
最后,当您的应用程序关闭且不再需要访问Assistant API时,将使用destroy()
方法进行拆卸。
public void destroy() {
mAssistantHandler.post(new Runnable() {
@Override
public void run() {
mAssistantHandler.removeCallbacks(mStreamAssistantRequest);
}
});
mAssistantThread.quitSafely();
if (mAudioRecord != null) {
mAudioRecord.stop();
mAudioRecord = null;
}
if (mAudioTrack != null) {
mAudioTrack.stop();
mAudioTrack = null;
}
}
使用助手
辅助类充实后,就该使用它们了。 您将通过编辑Android Things MainActivity
类来实现此目的,以与EmbeddedAssistant
和用于控制Google Assistant的硬件进行交互。 首先,将Button.OnButtonEventListener
接口添加到Activity
。
public class MainActivity extends Activity implements Button.OnButtonEventListener {
接下来,您需要添加应用程序所需的成员变量和常量。 这些值将控制触发助手的按钮的去抖动,以及音量,音频格式,您之前创建的UserCredentials
类以及设备的硬件。
private static final int BUTTON_DEBOUNCE_DELAY_MS = 20;
private static final String PREF_CURRENT_VOLUME = "current_volume";
private static final int SAMPLE_RATE = 16000;
private static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT;
private static final int DEFAULT_VOLUME = 100;
private int initialVolume = DEFAULT_VOLUME;
private static final AudioFormat AUDIO_FORMAT_STEREO =
new AudioFormat.Builder()
.setChannelMask(AudioFormat.CHANNEL_IN_STEREO)
.setEncoding(ENCODING)
.setSampleRate(SAMPLE_RATE)
.build();
// Hardware peripherals.
private VoiceHat mVoiceHat;
private Button mButton;
private EmbeddedAssistant mEmbeddedAssistant;
private UserCredentials userCredentials;
定义常量后,您将需要创建一些回调对象,这些对象将用于与助手进行对话和请求。
private ConversationCallback mConversationCallback = new ConversationCallback() {
@Override
public void onConversationEvent(EventType eventType) {}
@Override
public void onAudioSample(ByteBuffer audioSample) {}
@Override
public void onConversationError(Status error) {}
@Override
public void onError(Throwable throwable) {}
@Override
public void onVolumeChanged(int percentage) {
SharedPreferences.Editor editor = PreferenceManager
.getDefaultSharedPreferences(AssistantActivity.this)
.edit();
editor.putInt(PREF_CURRENT_VOLUME, percentage);
editor.apply();
}
@Override
public void onConversationFinished() {}
};
private RequestCallback mRequestCallback = new RequestCallback() {
@Override
public void onRequestStart() {
//starting assistant request, enable microphones
}
@Override
public void onSpeechRecognition(String utterance) {}
};
在mConversationCallback
,您会注意到我们以共享首选项保存了音量变化百分比。 这样,即使在重新启动后,您的设备容量也能为用户保持一致。
由于助手在您的设备上异步工作,因此您将通过调用在本教程其余部分中定义的一组助手方法来初始化在onCreate()
使用助手API的所有内容。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initVoiceHat();
initButton();
initVolume();
initUserCredentials();
initEmbeddedAssistant();
}
第一个辅助方法是initVoiceHat()
。 如果将Voice Hat防护罩连接到Raspberry Pi,则此方法将初始化设备,以便用户可以使用连接的麦克风和扬声器。 如果未安装语音帽,则可以使用标准的AUX扬声器和USB麦克风,并将其自动路由到。 语音帽使用I2S处理总线上的音频外围设备,并由Google编写的驱动程序类包装。
private void initVoiceHat() {
PeripheralManagerService pioService = new PeripheralManagerService();
List<String> i2sDevices = pioService.getI2sDeviceList();
if (i2sDevices.size() > 0) {
try {
mVoiceHat = new VoiceHat(
BoardDefaults.getI2SDeviceForVoiceHat(),
BoardDefaults.getGPIOForVoiceHatTrigger(),
AUDIO_FORMAT_STEREO
);
mVoiceHat.registerAudioInputDriver();
mVoiceHat.registerAudioOutputDriver();
} catch (IllegalStateException e) {}
}
}
助手仅在按住触发按钮的情况下对此样本做出响应。 该按钮的初始化和配置如下:
private void initButton() {
try {
mButton = new Button(BoardDefaults.getGPIOForButton(),
Button.LogicState.PRESSED_WHEN_LOW);
mButton.setDebounceDelay(BUTTON_DEBOUNCE_DELAY_MS);
mButton.setOnButtonEventListener(this);
} catch( IOException e ) {}
}
按下按钮后,助手将开始收听新的对话。
@Override
public void onButtonEvent(Button button, boolean pressed) {
if (pressed) {
mEmbeddedAssistant.startConversation();
}
}
由于我们将音量信息存储在设备的SharedPreferences
,因此我们可以直接访问它以初始化设备的音量。
private void initVolume() {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
initialVolume = preferences.getInt(PREF_CURRENT_VOLUME, DEFAULT_VOLUME);
}
Assistant SDK需要进行身份验证才能使用。 幸运的是,我们在本教程前面的EmbeddedAssistant
类中专门针对这种情况创建了一个方法。
private void initUserCredentials() {
userCredentials = null;
try {
userCredentials = EmbeddedAssistant.generateCredentials(this, R.raw.credentials);
} catch (IOException | JSONException e) {}
}
在onCreate()
调用的最终帮助程序方法将初始化EmbeddedAssistant
对象,并将其连接到API。
private void initEmbeddedAssistant() {
mEmbeddedAssistant = new EmbeddedAssistant.Builder()
.setCredentials(userCredentials)
.setAudioSampleRate(SAMPLE_RATE)
.setAudioVolume(currentVolume)
.setRequestCallback(mRequestCallback)
.setConversationCallback(mConversationCallback)
.build();
mEmbeddedAssistant.connect();
}
您需要做的最后一件事是通过更新Activity
的onDestroy()
方法来正确拆除外围设备。
@Override
protected void onDestroy() {
super.onDestroy();
if (mButton != null) {
try {
mButton.close();
} catch (IOException e) {}
mButton = null;
}
if (mVoiceHat != null) {
try {
mVoiceHat.unregisterAudioOutputDriver();
mVoiceHat.unregisterAudioInputDriver();
mVoiceHat.close();
} catch (IOException e) {}
mVoiceHat = null;
}
mEmbeddedAssistant.destroy();
}
完成所有这些操作后,您应该能够像使用Google Home一样与Android Things设备进行交互!
结论
在本教程中,您了解了Google Assistant以及如何将其添加到Android Things应用程序中。 此功能为您的用户提供了一种与您的设备进行交互和控制您的设备的新方式,并可以访问Google提供的许多功能。 这只是可用于Android Things应用程序的出色功能的一部分,可让您为用户创建新颖而令人惊奇的设备。
翻译自: https://code.tutsplus.com/tutorials/android-things-adding-google-assistant--cms-27690