前回の記事 ESP32をAlexa Gadgets Toolkitデバイスにしよう で紹介したArduino側のソースコードを解説します。
いつもの通り、Arduinoのコードは以下のGitHubに登録しておきました。
poruruba/AlexaGadget
https://github.com/poruruba/AlexaGadget
#Alexa GadgetデバイスのGATTの準備
以下に示す通り、FE03というUUIDのGATTサービスに、2つのCharacteristic(コマンド受信用とNotification送信用)を登録します。
ソースコードでは以下の辺りです。
BLEDevice::init("M5StickC");
BLEServer *pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyCallbacks());
BLEService *pService = pServer->createService(UUID_SERVICE);
pCharacteristic_write = pService->createCharacteristic( UUID_WRITE, BLECharacteristic::PROPERTY_WRITE );
pCharacteristic_write->setAccessPermissions(ESP_GATT_PERM_WRITE_ENCRYPTED);
pCharacteristic_write->setValue(value_write, sizeof(value_write));
pCharacteristic_write->setCallbacks(new MyCharacteristicCallbacks());
pCharacteristic_notify = pService->createCharacteristic( UUID_NOTIFY, BLECharacteristic::PROPERTY_NOTIFY);
pCharacteristic_notify->setAccessPermissions(ESP_GATT_PERM_READ_ENCRYPTED);
pCharacteristic_notify->setValue(value_read, sizeof(value_read));
BLEDescriptor *pDescriptor = new BLE2902();
pDescriptor->setCallbacks(new MyDescriptorCallback());
pCharacteristic_notify->addDescriptor(pDescriptor);
Bluetoothアドレス指定方式として、「解決可能なランダムのプライベータドレス」とありましたが、Publicでも大丈夫でした。
ちなみに、nanopbで生成される変数の構造体のサイズは大きく、スタックをかなり消費するため、BLE用のサーバのスタックサイズは大きめにとってください。(以下では、40000にしています)
xTaskCreate(taskServer, "server", 40000, NULL, 5, NULL);
#Alexa Gadgetデバイスのペアリング
Echoとは以下に示す通りのやり取りをします。
Bluetooth Low Energyを経由したガジェットとEchoデバイスのペアリングおよび接続フロー
https://developer.amazon.com/ja-JP/docs/alexa/alexa-gadgets-toolkit/bluetooth-le-pair-connect.html
順に説明します。まずはハンドシェイクまで。
〇フェーズ1:デバイスの検出
Alexa Gadegetデバイスは、以下に示すアドバタイズパケットをアドバタイズする必要があります。
https://developer.amazon.com/ja-JP/docs/alexa/alexa-gadgets-toolkit/bluetooth-le-settings.html#adv-packet-for-pairing
ソースコードのvoid taskServer(void*)
の以下の部分です。
BLEAdvertising *pAdvertising = pServer->getAdvertising();
pAdvertising->addServiceUUID(UUID_SERVICE);
BLEAdvertisementData advertisementData = BLEAdvertisementData();
advertisementData.setFlags(0x06);
std::string strServiceData = "";
strServiceData += (char)23;
strServiceData += (char)0x16;
strServiceData += (char)(UUID_SERVICE_SHORT & 0xff);
strServiceData += (char)((UUID_SERVICE_SHORT >> 8) & 0xff);
strServiceData += (char)(BT_VENDOR_ID & 0xff);
strServiceData += (char)((BT_VENDOR_ID >> 8) & 0xff);
strServiceData += (char)0x00;
strServiceData += (char)0xff;
strServiceData += (char)0x00;
strServiceData += (!isBonded) ? (char)0x00 : (char)0x01;
strServiceData += (char)0x00;
strServiceData += (char)0x00;
strServiceData += (char)0x00;
strServiceData += (char)0x00;
strServiceData += (char)0x00;
strServiceData += (char)0x00;
strServiceData += (char)0x00;
strServiceData += (char)0x00;
strServiceData += (char)0x00;
strServiceData += (char)0x00;
strServiceData += (char)0x00;
strServiceData += (char)0x00;
strServiceData += (char)0x00;
strServiceData += (char)0x00;
advertisementData.addData(strServiceData);
pAdvertising->setAdvertisementData(advertisementData);
〇フェース4:ハンドシェイク
Alexa GadgetデバイスとEchoデバイスの接続が確立されたら、Alexa GadgetデバイスはEchoデバイスにプロトコルバージョンパケットを送信する必要があります。
そこで、Echoデバイス側からNotificationのCharacteristicに対してNotificationを有効にしてきたタイミングで送るようにします。
以下のように、NotificationのCharacteristicのDescriptorにコールバックを仕掛けておき、
BLEDescriptor *pDescriptor = new BLE2902();
pDescriptor->setCallbacks(new MyDescriptorCallback());
pCharacteristic_notify->addDescriptor(pDescriptor);
以下のコールバック部分で送信します。
class MyDescriptorCallback : public BLEDescriptorCallbacks{
void onWrite(BLEDescriptor* pDescriptor){
Serial.println("onWrite(Descriptor)");
BLE2902* desc = (BLE2902*)pCharacteristic_notify->getDescriptorByUUID(BLEUUID((uint16_t)0x2902));
if( !desc->getNotifications() )
return;
uint8_t protocol_version_packet[PROTOCOL_VERSION_PACKET_SIZE] = {
(PROTOCOL_IDENTIFIER >> 8) & 0xff,
(PROTOCOL_IDENTIFIER >> 0) & 0xff,
PROTOCOL_VERSION_MAJOR,
PROTOCOL_VERSION_MINOR,
(SAMPLE_NEGOTIATED_MTU >> 8) & 0xff,
(SAMPLE_NEGOTIATED_MTU >> 0) & 0xff,
(SAMPLE_MAX_TRANSACTION_SIZE >> 8) & 0xff,
(SAMPLE_MAX_TRANSACTION_SIZE >> 0) & 0xff,
};
pCharacteristic_notify->setValue(protocol_version_packet, PROTOCOL_VERSION_PACKET_SIZE);
pCharacteristic_notify->notify();
Serial.println("Notified");
}
};
ハンドシェイクのフェーズでは、以降、DeviceInformationコマンド(コマンドID 20)、DeviceFeaturesコマンド(コマンドID 28) がEchoデバイスから送られてきますので、それぞれ返答します。
〇Echoデバイスからの受信
Echoデバイスからは、コマンド受信用のキャラクタリスティックに、MTUサイズに分割されてコマンドパケットが送信されてきます。
パケットのフォーマットは以下に示されています。
分割されてくるので、以下の関数で、順に結合してくれる関数を作っておきました。
long appendPacket(const uint8_t *p_buffer, uint16_t buffer_len){
if( g_buffer_offset == 0){
if( buffer_len < 5)
return -1;
g_stream_id = (stream_id_t)((p_buffer[0] >> 4) & 0x0f);
g_trxn_id = p_buffer[0] & 0x0f;
g_seq_no = (p_buffer[1] >> 4) & 0x0f;
g_ack_bit = (p_buffer[1] & 0x02) != 0x00;
if( p_buffer[1] & 0x01 ){
Serial.println("EXT Not supported");
return -1;
}
g_receive_total_len = (p_buffer[3] << 8) | p_buffer[4];
if( g_receive_total_len > SAMPLE_MAX_TRANSACTION_SIZE)
return -2;
uint16_t unit = p_buffer[5];
if( (6 + unit) < buffer_len )
return -3;
memmove(&gp_receive_buffer[g_buffer_offset], &p_buffer[6], unit);
g_buffer_offset += unit;
}else{
if( buffer_len < 3)
return -4;
if( p_buffer[0] != ((g_stream_id << 4) | g_trxn_id) )
return -5;
g_seq_no = (p_buffer[1] >> 4) & 0x0f;
g_ack_bit = (p_buffer[1] & 0x02) != 0x00;
if( p_buffer[1] & 0x01 ){
Serial.println("EXT Not supported");
return -1;
}
uint16_t unit = p_buffer[2];
if( g_buffer_offset + unit > g_receive_total_len)
return -6;
if( (4 + unit) < buffer_len )
return -7;
memmove(&gp_receive_buffer[g_buffer_offset], &p_buffer[3], unit);
g_buffer_offset += unit;
}
return g_receive_total_len - g_buffer_offset;
}
void resetPacket(void){
g_buffer_offset = 0;
}
結合完了までの残りの要受信パケットサイズが返ってきますので、0になるまで結合を続けます。
Echoデバイスからパケットを受信すると、以下のコールバックが呼ばれるようにしていますので、この結合する関数を呼ぶようにします。
class MyCharacteristicCallbacks : public BLECharacteristicCallbacks{
void onWrite(BLECharacteristic* pCharacteristic){
ハンドシェイク時にやり取りするpacketは制御ストリームの方なので、以下で判別し、
if( ((value[0] >> 4) & 0x0f) == CONTROL_STREAM ){
以下の部分で、コマンドIDを確認しています。
memmove(&controlEnvelope, &controlEnvelope_zero, sizeof(ControlEnvelope));
pb_istream_t istream = pb_istream_from_buffer(gp_receive_buffer, g_receive_total_len);
status = pb_decode(&istream, ControlEnvelope_fields, &controlEnvelope);
if( !status ) {
Serial.println("pb_decode Error");
return;
}
Serial.printf("Command=%d\n", controlEnvelope.command);
pb_istream_from_bufferやpb_decodeは、バイナリ形式のプロトコルバッファを解析するための関数です。
DeviceInformation(コマンドID 20)の処理が以下の部分です。
if( controlEnvelope.command == Command_GET_DEVICE_INFORMATION ){
Serial.println("Command_GET_DEVICE_INFORMATION");
pb_ostream_t ostream = pb_ostream_from_buffer(gp_send_buffer, sizeof(gp_send_buffer));
status = pb_encode(&ostream, ControlEnvelope_fields, &get_device_info_controlEnvelope);
if (!status) {
Serial.println("pb_encode Error");
return;
}
Serial.printf("bytes_written=%d\n", ostream.bytes_written);
sendPacket(CONTROL_STREAM, gp_send_buffer, ostream.bytes_written);
DeviceInformation応答を返しているだけです。
https://developer.amazon.com/ja-JP/docs/alexa/alexa-gadgets-toolkit/packet-ble.html#device-information-response
DeviceInformation応答と、後で示すDeviceFeatures応答とDiscover.Responseイベントは、返す値が決まっているので、以下の部分で、先にパケットを作っておいています。
get_device_info_controlEnvelope = makeDeviceInformationResponse(EndpointId, FriendlyName, AmazonId);
get_device_features_controlEnvelope = makeDeviceFeatureResponse(0x0011);
alexaDiscovery_DiscoverResponseEventPayloadProto_Endpoints_AdditionalIdentification additionalIdentification = alexaDiscovery_DiscoverResponseEventPayloadProto_Endpoints_AdditionalIdentification_init_default;
strcpy(additionalIdentification.firmwareVersion, FirmwareVersion);
strcpy(additionalIdentification.deviceToken, DeviceToken);
strcpy(additionalIdentification.deviceTokenEncryptionType, "1");
strcpy(additionalIdentification.amazonDeviceType, AmazonId);
strcpy(additionalIdentification.modelName, ModelName);
strcpy(additionalIdentification.radioAddress, RadioAddress);
discover_response_envelope = makeDiscoveryDiscoverEvent(AmazonId, FriendlyName, &additionalIdentification);
ちなみに、wakewordに応答したいので、応答できることを以下 makeDiscoveryDiscoverEvent
の中で示しています。
discover_response_envelope.event.payload.endpoints[0].capabilities_count = 2;
strcpy(discover_response_envelope.event.payload.endpoints[0].capabilities[0].type, "AlexaInterface");
strcpy(discover_response_envelope.event.payload.endpoints[0].capabilities[0].interface, "Notifications");
strcpy(discover_response_envelope.event.payload.endpoints[0].capabilities[0].version, "1.0");
strcpy(discover_response_envelope.event.payload.endpoints[0].capabilities[1].type, "AlexaInterface");
strcpy(discover_response_envelope.event.payload.endpoints[0].capabilities[1].interface, "Alexa.Gadget.StateListener");
strcpy(discover_response_envelope.event.payload.endpoints[0].capabilities[1].version, "1.0");
discover_response_envelope.event.payload.endpoints[0].capabilities[1].configuration.supportedTypes_count = 1;
strcpy(discover_response_envelope.event.payload.endpoints[0].capabilities[1].configuration.supportedTypes[0].name, "wakeword");
〇Echoデバイスへの応答の送信
Echoデバイスへのパケット応答も、MTUサイズで分割して返す必要があります。
以下に、分割送信する関数を作っておきました。
long sendPacket(stream_id_t stream_id, const uint8_t *p_buffer, uint16_t buffer_len){
uint8_t seq = 0;
long result_len;
Serial.println("sendPacket");
debug_dump(p_buffer, buffer_len);
do{
uint16_t packet_len = sizeof(value_read);
result_len = createPacket(stream_id, g_trxn_id, seq, p_buffer, buffer_len, value_read, &packet_len);
if( result_len < 0 ){
Serial.printf("Error result_len=%ld\n", result_len);
return result_len;
}
pCharacteristic_notify->setValue(value_read, packet_len);
pCharacteristic_notify->notify();
seq++;
}while(result_len > 0);
Serial.println("Notify End");
return 0;
}
long createPacket(stream_id_t stream_id, uint8_t trxn_id, uint8_t seq_no, const uint8_t *p_payload, uint16_t payload_len, uint8_t *p_buffer, uint16_t *p_buffer_len){
uint16_t index = 0;
uint16_t rest;
uint16_t unit;
if( *p_buffer_len < 6)
return -1;
p_buffer[index++] = ((stream_id & 0x0f) << 4) | (trxn_id & 0x0f);
p_buffer[index] = ((seq_no & 0x0f) << 4);
if( seq_no == 0 ){
p_buffer[index++] |= TRANSACTION_TYPE_INITIAL << 2;
p_buffer[index++] = 0x00;
p_buffer[index++] = (payload_len >> 8) & 0xff;
p_buffer[index++] = payload_len & 0xff;
rest = payload_len;
unit = (rest <= (SAMPLE_NEGOTIATED_MTU - 6)) ? rest : (SAMPLE_NEGOTIATED_MTU - 6);
p_buffer[index++] = unit;
if( *p_buffer_len < (6 + unit) )
return -1;
memmove(&p_buffer[index], &p_payload[0], unit);
index += unit;
}else{
if( (payload_len - (SAMPLE_NEGOTIATED_MTU - 6) - (SAMPLE_NEGOTIATED_MTU - 3) * (seq_no - 1) ) <= 0 )
return -1;
rest = payload_len - (SAMPLE_NEGOTIATED_MTU - 6) - (SAMPLE_NEGOTIATED_MTU - 3) * (seq_no - 1);
unit = (rest <= (SAMPLE_NEGOTIATED_MTU - 3)) ? rest : (SAMPLE_NEGOTIATED_MTU - 3);
if( (rest - unit) > 0 )
p_buffer[index++] |= TRANSACTION_TYPE_CONTINUE << 2;
else
p_buffer[index++] |= TRANSACTION_TYPE_FINAL << 2;
p_buffer[index++] = unit;
if( *p_buffer_len < (3 + unit) )
return -1;
memmove(&p_buffer[index], &p_payload[payload_len - rest], unit);
index += unit;
}
*p_buffer_len = index;
return rest - unit;
}
sendPacketで送信していますが、その中でcreatePacketを呼び出して細切れのパケットを作ってすべてのパケットが送信されるまで繰り返しています。
〇フェーズ5:Alexa Gadgets Toolkitディレクティブおよびイベント
ハンドシェイクの処理が終わると、次は、「Alexa Gadgets Toolkit Directives and Events」の処理になります。(直が書きすみません)
Alexa.Discovery.Discoverディレクティブの処理です。
今度は、Echoデバイスから送られてくるのは制御ストリームではなくAlexaストリームになります。
以下の部分で判別しています。
}else if( ((value[0] >> 4) & 0x0f) == ALEXA_STREAM){
まずは、以下の部分でプロトコルバッファ解析し、とりあえず、namespaceとnameを取り出します。なぜならば、namespaceとnameによってそれ以降のパケットのフォーマットが異なるためです。
directive_DirectiveParserProto directive_envelope = directive_DirectiveParserProto_init_default;
pb_istream_t istream = pb_istream_from_buffer(gp_receive_buffer, g_receive_total_len);
status = pb_decode(&istream, directive_DirectiveParserProto_fields, &directive_envelope);
if( !status ){
Serial.println("pb_decode Error");
return;
}
Serial.printf("name = %s, namespace=%s\n", directive_envelope.directive.header.name, directive_envelope.directive.header.namespacc);
そして、Discoverの場合に以下の処理を行っています。
改めて、Discoverディレクティブのプロトコルバッファを解析し、(あらかじめ作っておいた)Discover.Responseイベントを返します。
} else if (0 == strcmp(directive_envelope.directive.header.name, "Discover") && (0 == strcmp(directive_envelope.directive.header.namespacc, "Alexa.Discovery"))) {
pb_istream_t istream_discovery = pb_istream_from_buffer(gp_receive_buffer, g_receive_total_len);
alexaDiscovery_DiscoverDirectiveProto discovery_envelope = alexaDiscovery_DiscoverDirectiveProto_init_default;
status = pb_decode(&istream_discovery, alexaDiscovery_DiscoverDirectiveProto_fields, &discovery_envelope);
if( !status ){
Serial.println("pb_decode Error");
return;
}
Serial.printf("scope type: %s\n", discovery_envelope.directive.payload.scope.type);
Serial.printf("scope token: %s\n", discovery_envelope.directive.payload.scope.token);
pb_ostream_t ostream = pb_ostream_from_buffer(gp_send_buffer, sizeof(gp_send_buffer));
status = pb_encode(&ostream, alexaDiscovery_DiscoverResponseEventProto_fields, &discover_response_envelope);
if (!status){
Serial.println("pb_encode Error");
return;
}
Serial.printf("bytes_written=%d\n", ostream.bytes_written);
sendPacket(ALEXA_STREAM, gp_send_buffer, ostream.bytes_written);
これで準備完了です。
#wakewordへの反応
wakewordに応答するようにしておいたので、wakewordを検出したときと、検出し終わったときに、それぞれEchoデバイスからディレクティブが送信されてきます。前者が「active」、後者が「cleared」です。
以下の部分です。
if (0 == strcmp(directive_envelope.directive.header.name, "StateUpdate") && (0 == strcmp(directive_envelope.directive.header.namespacc, "Alexa.Gadget.StateListener"))){
pb_istream_t istream_statusupdate = pb_istream_from_buffer(gp_receive_buffer, g_receive_total_len);
alexaGadgetStateListener_StateUpdateDirectiveProto statusupdate_envelope = alexaGadgetStateListener_StateUpdateDirectiveProto_init_default;
status = pb_decode(&istream_statusupdate, alexaGadgetStateListener_StateUpdateDirectiveProto_fields, &statusupdate_envelope);
if( !status ){
Serial.println("pb_decode Error");
return;
}
int states_count = statusupdate_envelope.directive.payload.states_count;
for (int i = 0; i < states_count; ++i) {
Serial.printf("state name: %s\n", statusupdate_envelope.directive.payload.states[i].name);
Serial.printf("state value: %s\n", statusupdate_envelope.directive.payload.states[i].value);
if( strcmp(statusupdate_envelope.directive.payload.states[i].name, "wakeword") == 0 ){
if( strcmp(statusupdate_envelope.directive.payload.states[i].value, "active") == 0 ){
digitalWrite(GPIO_NUM_10, LOW);
}else if(strcmp(statusupdate_envelope.directive.payload.states[i].value, "cleared") == 0){
digitalWrite(GPIO_NUM_10, HIGH);
}
}
}
activeでLEDを点灯し、clearedで消灯させています。
#終わりに
今回はここまでということで。。。
次回は、AlexaのスキルからAlexa Gadgetsデバイスに指示を出したり、Alexa Gadgetsからスキルにイベントを伝達する部分です。
以上