LoginSignup
7
7

More than 3 years have passed since last update.

ESP32をAlexa Gadgets Toolkitデバイスにしよう:ソースコード解説

Last updated at Posted at 2020-07-18

前回の記事 ESP32をAlexa Gadgets Toolkitデバイスにしよう で紹介したArduino側のソースコードを解説します。

いつもの通り、Arduinoのコードは以下のGitHubに登録しておきました。

poruruba/AlexaGadget
 https://github.com/poruruba/AlexaGadget

Alexa GadgetデバイスのGATTの準備

以下に示す通り、FE03というUUIDのGATTサービスに、2つのCharacteristic(コマンド受信用とNotification送信用)を登録します。

ソースコードでは以下の辺りです。

main.cpp
  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にしています)

main.cpp
  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*)の以下の部分です。

main.cpp
  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にコールバックを仕掛けておき、

main.cpp
  BLEDescriptor *pDescriptor = new BLE2902();
  pDescriptor->setCallbacks(new MyDescriptorCallback());
  pCharacteristic_notify->addDescriptor(pDescriptor);

以下のコールバック部分で送信します。

main.cpp
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サイズに分割されてコマンドパケットが送信されてきます。

パケットのフォーマットは以下に示されています。

分割されてくるので、以下の関数で、順に結合してくれる関数を作っておきました。

main.cpp
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デバイスからパケットを受信すると、以下のコールバックが呼ばれるようにしていますので、この結合する関数を呼ぶようにします。

main.cpp
class MyCharacteristicCallbacks : public BLECharacteristicCallbacks{
  void onWrite(BLECharacteristic* pCharacteristic){

ハンドシェイク時にやり取りするpacketは制御ストリームの方なので、以下で判別し、

main
if( ((value[0] >> 4) & 0x0f) == CONTROL_STREAM ){

以下の部分で、コマンドIDを確認しています。

main.cpp
      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)の処理が以下の部分です。

main.cpp
      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イベントは、返す値が決まっているので、以下の部分で、先にパケットを作っておいています。

main.cpp
  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の中で示しています。

main.cpp
  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サイズで分割して返す必要があります。
以下に、分割送信する関数を作っておきました。

main.cpp
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ディレクティブの処理です。

これが送られてきます。
 https://developer.amazon.com/ja-JP/docs/alexa/alexa-gadgets-toolkit/alexa-discovery-interface.html#discover-directive

今度は、Echoデバイスから送られてくるのは制御ストリームではなくAlexaストリームになります。

以下の部分で判別しています。

main.cpp
    }else if( ((value[0] >> 4) & 0x0f) == ALEXA_STREAM){

まずは、以下の部分でプロトコルバッファ解析し、とりあえず、namespaceとnameを取り出します。なぜならば、namespaceとnameによってそれ以降のパケットのフォーマットが異なるためです。

main.cpp
      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イベントを返します。

main.cpp
      } 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」です。
以下の部分です。

main.cpp
      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からスキルにイベントを伝達する部分です。

以上

7
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
7