0
Help us understand the problem. What are the problem?

posted at

updated at

ESP32をActions on Google Homeの音楽プレイヤにする

M5Core2を「OK Google」から操作できる音楽プレイヤにします。
Google Smart Homeのデバイスタイプ「メディアリモコン」を実装します。
また、応答速度を速めるために、ローカルフルフィルメントに対応させます。

もろもろのソースコードは以下にあります。

poruruba/MusicSmartHome_Google

全体的な流れを示します。
まずは、セットアップ時です。ユーザが、スマホのGoogleのHomeアプリから、デバイスのセットアップをするときのフローです。
ちなみに、後述する自作のスマートホームサーバの登録は済んでいる前提です。

image.png

①セットアップ開始
 GoogleのHomeアプリから、デバイスのセットアップを開始します。

②ユーザ認証
 ユーザを認証します。OAuth2やOpenIDConnectで認識できるユーザであれば誰でもよいので、今回はLINEアカウント認証を使いました。

③SYNCインテント
 スマートホームサーバの登録時に指定したWebAPIに、SYNCインテントが飛んできますので、自身が管理する音楽再生デバイスの情報を返します。

④ローカルフルフィルメントアプリの登録
 スマートホームサーバ登録時に、ローカルフルフィルメントアプリを登録しておいたので、それがスマートホームデバイス(絵ではGoogle Nest Hub)に登録されます。

⑤セットアップ完了
 セットアップが完了し、以降Homeアプリから参照できるようになります。

⑥DiscoveryPacketのUDPパケット送受信
 セットアップ完了後は、Google Nest Hubは定期的に音楽再生デバイスが存在するかをDiscoveryPacketの送信と応答で確認します。

次は、セットアップ完了後の流れです。「OK Google、サウンドバーで次の曲を再生」などの操作です。

image.png

①OK Google
 ユーザは、Google Nest Hubに対して、発話します。

②発話
 発話の内容がActions on Googleに送信されます。

③QUERY/EXECUTEインテント
 Actions on Googleは発話の内容を解釈し、QUERYまたはEXECUTEインテントを自作のスマートホームサーバにWebAPI呼び出しします。

④QUERY/EXECUTEのUDPパケット
 自作のスマートホームサーバは、その内容をUDPで音楽再生デバイスに転送し、音楽再生デバイスはその内容に応じて音楽を再生し、UDPパケットで応答します。自作のスマートホームサーバはその応答をActions on Googleに戻します。

ReportStateは、音楽再生デバイスの状態が変わったときに、自作のスマートホームサーバを経由して、Google Homeに通知することで、スマホのHomeアプリから状態が変わったことを確認できるようになります。

上記は、DiscoveryPacketのUDPパケットでGoogle Nest Hubが音楽再生デバイスを発見するまでの流れです。発見した後は、Actions on Googleや自作のスマートホームサーバを介さずに、直接音楽再生デバイスを操作します。これがローカルフルフィルメントです。

image.png

①OK Google
 ユーザは、Google Nest Hubに対して、発話します。

④QUERY/EXECUTEのUDPパケット
 登録済みのローカルフルフィルメントアプリは、その内容をUDPで音楽再生デバイスに転送し、音楽再生デバイスはその内容に応じて音楽を再生し、UDPパケットで応答します。

自作のスマートホームサーバの登録

後述すると言っていた、自作のスマートホームサーバの登録の説明をします。
と言っても、以前に投稿した内容と同じなので、サマリだけ。

ESP32をGoogle Smart Homeデバイスにする

デベロッパコンソールから、プロジェクトを作成します。

Actions on Google Developer Console
 http://console.actions.google.com/

image.png

スマートホームを選択します。

image.png

呼びやすい名前を入れます。例えば、「ESP32スマートホーム」とか。

image.png

左側のナビゲーションメニューからActionsを選択し、Fullfillment URLに自作のスマートホームサーバのURLを指定します。

 https://[立ち上げるNode.jsサーバのURL]/smarthome_google

Configure local home SDK (optional)は後で指定するので、とりあえずなにも設定しないで行きましょう。

image.png

左側のナビゲーションメニューからAccount Linkingを選択し、今回はLINEサーバを指定します。

Client ID issued by your Actions on Google:LINEのChannel ID
Client secret:LINEのChannel secret
Authorization URL:https://access.line.me/oauth2/v2.1/authorize
Token URL:https://api.line.me/oauth2/v2.1/token
Configure your client (optional):profile、openid、email等、お好みで

image.png

LINEの情報は、以下のURLから。

LINE Developers

image.png

LINEのCallback URLに以下を追加します。

 https://oauth-redirect.googleusercontent.com/r/[GoogleHomeのプロジェクトID]

image.png

GoogleHomeのプロジェクト名は、Actions on Google Developer Consoleの右上のメニュー「Project settings」から確認できます。

HomeGraph APIを有効にします。

HomeGraph API

image.png

サービスアカウントのクレデンシャル情報を発行します。

image.png

[GoogleHomeプロジェクト名]-XXXXXXXXXXXX.json ってな感じのクレデンシャルファイルを生成します。

スマートホームサーバを立ち上げる

GitHubからZIPファイルをダウンロードします。

> unzip MusicSmartHome_Google-master.zip
> cd MusicSmartHome_Google-master/nodejs
> npm install

大事なのは以下のファイルです。

/nodejs\api\controllers\smarthome_google/index.js
'use strict';

const HELPER_BASE = process.env.HELPER_BASE || "/opt/";
const Response = require(HELPER_BASE + 'response');

const UDP_DEVICE_ADDRESS = '192.168.1.255';
const UDP_DEVICE_PORT = 3311;
const UDP_RECV_PORT = 3312;
const UDP_REPORTSTATE_PORT = 3314;
const JWT_FILE_PATH = './keys/[クレデンシャルファイル名]';

const jwt_decode = require('jwt-decode');
const {smarthome} = require('actions-on-google');

const jwt = require(JWT_FILE_PATH);
const app = smarthome({
  jwt: jwt
});

const dgram = require('dgram');
const UdpComRes = require('./UdpComRes');
var udpcomres = new UdpComRes(UDP_RECV_PORT, true);

var requestId = 0;

var agentUserId = process.env.DEFAULT_USER_ID || "user01";

const onsync = require('./onsync.json');

app.onSync((body, headers) => {
  console.info('onSync');
  console.log('onSync body', body);
  console.log(headers);

//  var decoded = jwt_decode(headers.authorization);
//  console.log(decoded);

  var result = {
    requestId: body.requestId,
    payload: {
      agentUserId: agentUserId,
      devices: []
    }
  };

  onsync.forEach((item) =>{
    result.payload.devices.push(item);
  });
  console.log(JSON.stringify(result, null, '\t'));

  console.log("onSync result", result);
  return result;
});

app.onQuery(async (body, headers) => {
  console.info('onQuery');
  console.log('onQuery body', body);

//  var decoded = jwt_decode(headers.authorization);
//  console.log(decoded);

  const {requestId} = body;
  const payload = {
    devices: {}
  };

  for( var i = 0 ; i < body.inputs.length ; i++ ){
    if( body.inputs[i].intent == 'action.devices.QUERY' ){
      for( var j = 0 ; j < body.inputs[i].payload.devices.length ; j++ ){
        var device = body.inputs[i].payload.devices[j];
        var params = {
          intent: 'action.devices.QUERY',
          device_id: device.id
        };
        try{
          var message = await udpcomres.transceive(params, UDP_DEVICE_PORT, UDP_DEVICE_ADDRESS);
          payload.devices[device.id] = message.states;
          payload.devices[device.id].status = "SUCCESS";
        }catch(error){
          console.error(error);
        }
      }
    }
  }

  var result = {
    requestId: requestId,
    payload: payload,
  };

  console.log("onQuery result", JSON.stringify(result));
  return result;
});

app.onExecute(async (body, headers) => {
  console.info('onExecute');
  console.log('onExecute body', JSON.stringify(body));

//  var decoded = jwt_decode(headers.authorization);
//  console.log(decoded);
  
  const {requestId} = body;

  // Execution results are grouped by status
  var ret = {
    requestId: requestId,
    payload: {
      commands: [],
    },
  };
  for( var i = 0 ; i < body.inputs.length ; i++ ){
    if( body.inputs[i].intent == "action.devices.EXECUTE" ){
      for( var j = 0 ; j < body.inputs[i].payload.commands.length ; j++ ){
        var result = {
          ids:[],
          status: 'SUCCESS',
        };
        ret.payload.commands.push(result);
        var devices = body.inputs[i].payload.commands[j].devices;
        var execution = body.inputs[i].payload.commands[j].execution;
        for( var k = 0 ; k < execution.length ; k++ ){
          for( var l = 0 ; l < devices.length ; l++ ){
            var params = {
              intent: 'action.devices.EXECUTE',
              device_id: devices[l].id,
              command: execution[k].command,
              params: execution[k].params 
            };
            try{
              var message = await udpcomres.transceive(params, UDP_DEVICE_PORT, UDP_DEVICE_ADDRESS);
              result.ids.push(devices[l].id);
              result.states = message.states;

              await reportState(devices[l].id, message.states);
            }catch(error){
              console.error(error);
            }
          }
        }
      }
    }
  }

  console.log("onExecute result", JSON.stringify(ret));
  return ret;
});

app.onDisconnect((body, headers) => {
  console.info('onDisconnect');
  console.log('body', body);

//  var decoded = jwt_decode(headers.authorization);
//  console.log(decoded);

  // Return empty response
  return {};
});

exports.fulfillment = app;

async function reportState(id, states){
  var state = {
    requestId: String(++requestId),
    agentUserId: agentUserId,
    payload: {
      devices: {
        states:{
          [id]: states
        }
      }
    }
  };

  console.log("reportstate", JSON.stringify(state));
  await app.reportState(state);

  return state;
} 

const udpSocket = dgram.createSocket('udp4');

udpSocket.on('listening', () => {
  const address = udpSocket.address();
  console.log('UDP udpSocket listening on ' + address.address + ":" + address.port);
});
udpSocket.on('error', (err) => {
  console.error(`UDP Server error: ${err.message}`);
});

udpSocket.on('message', async (message, remote) => {
	var body = JSON.parse(message);
  console.log(JSON.stringify(body));
  var res = await reportState(body.payload.device_id, body.payload.states);
  console.log(res);
});

udpSocket.bind(UDP_REPORTSTATE_PORT);

以下の部分を書き換えます。

const UDP_DEVICE_ADDRESS = '[音楽再生デバイスを含むブロードキャストIPアドレス]';
const UDP_DEVICE_PORT = [音楽再生デバイスのUDPポート番号];
const UDP_RECV_PORT = [スマートホームサーバが待ち受けるUDPポート番号];
const UDP_REPORTSTATE_PORT = [スマートホームサーバがReportStateを待ち受けるUDPポート番号];
const JWT_FILE_PATH = './keys/[クレデンシャルファイル名]';

また、必要に応じて、以下を書き換えます。SYNCインテントに対して返答する内容です。

nodejs\api\controllers\smarthome_google/onsync.json
[
  {
    "id": "soundbar",
    "type": "action.devices.types.REMOTECONTROL",
    "traits": [
      "action.devices.traits.OnOff",
      "action.devices.traits.MediaState",
      "action.devices.traits.TransportControl",
      "action.devices.traits.Volume"
    ],
    "name": {
      "name": "サウンドバー"
    },
    "deviceInfo": {
      "manufacturer": "MyHome Devices"
    },
    "willReportState": true,
    "attributes": {
      "transportControlSupportedCommands":[
        "RESUME",
        "PAUSE",
        "NEXT",
        "STOP"
      ],
      "volumeMaxLevel": 100,
      "levelStepSize": 5,
      "volumeCanMuteAndUnmute": true,
      "supportActivityState": true,
      "supportPlaybackState": true
    },
    "otherDeviceIds": [{
      "deviceId": "deviceid123"
    }]
  }
]

〇HTTPSで立ち上げられるようにする

certフォルダを作成しSSL証明書を格納するか、ngrokを仲介させます。

WebAPIを待ち受けるポート番号は、.envファイルを作成してそこに明記します。

PORT=50080
SPORT=50443

Node.jsサーバを立ち上げます。

> node app.js

ESP32を音楽再生デバイスにする

ソースコードです。

martHomeDevice\src\main.cpp
#include <Arduino.h>
#include <M5Core2.h>
#include <WiFi.h>
#include <WiFiUdp.h>
#include <ArduinoJson.h>
#include <SD.h>
#include <driver/i2s.h>
#include <AudioGeneratorMP3.h>
#include <AudioOutputI2S.h>
#include <AudioFileSourceID3.h>
#include <AudioFileSourceSD.h>

#define DEFAULT_AUDIO_GAIN  10.0
#define MUTE_AUDIO_GAIN     2.0

#define LISTEN_PORT 3311
#define REPORT_HOST "[Node.jsサーバのURL]"
#define REPORT_PORT 3314

#define AUDIO_BASE_FOLDER  "/Music"

#define DISCOVERY_PACKET  "HelloLocalHomeSDK"
#define DEFAULT_DEVICE_ID "soundbar"
#define LOCAL_DEVICE_ID   "deviceid123"

#define WIFI_SSID NULL // WiFiアクセスポイントのSSID
#define WIFI_PASSWORD NULL // WiFIアクセスポイントのパスワード
#define WIFI_TIMEOUT  10000
#define SERIAL_TIMEOUT1  10000
#define SERIAL_TIMEOUT2  20000

#define JSONDOC_BUFFER_SIZE 2048
StaticJsonDocument<JSONDOC_BUFFER_SIZE> jsonDoc;

static WiFiUDP udp;

static AudioOutputI2S *out = NULL;
static AudioGeneratorMP3 *mp3 = NULL;
static AudioFileSourceSD *file_sd = NULL;
static AudioFileSourceID3 *id3 = NULL;
static float g_audio_gain = DEFAULT_AUDIO_GAIN;
static bool g_audio_muted = false;
static int g_audio_index = -1;

typedef enum{
  STANDBY,
  STOPPED,
  PLAYING,
  PAUSED
} AUDIO_STATE;
static AUDIO_STATE g_audio_state = STANDBY;

static long m5_initialize(void);
long udpSend(IPAddress host, uint16_t port);
long udpOnOffReport(bool onoff, const char *p_device_id);
long processUdpPacket(const char *p_message);

long audio_initialize(void)
{
  out = new AudioOutputI2S(I2S_NUM_0, AudioOutputI2S::EXTERNAL_I2S);
  out->SetOutputModeMono(true);
  out->SetPinout(12, 0, 2);
  out->SetGain(g_audio_gain / 100.0);

  return 0;
}

long audio_stopMp3(void)
{
  if( mp3 != NULL )
    mp3->stop();
  if( id3 != NULL ){
    id3->RegisterMetadataCB(NULL, NULL);
    id3->close();
  }
  if( file_sd != NULL )
    file_sd->close();

  if( mp3 != NULL ){
    delete mp3;
    mp3 = NULL;
  }
  if( id3 != NULL ){
    delete id3;
    id3 = NULL;
  }
  if( file_sd != NULL ){
    delete file_sd;
    file_sd = NULL;
  }

  g_audio_state = STOPPED;

  return 0;
}

long audio_onoff(bool onoff)
{
  if( !onoff ){
    if( g_audio_state != STANDBY ){
      audio_stopMp3();
      g_audio_state = STANDBY;
      g_audio_muted = false;
    }
  }else{
    if( g_audio_state == STANDBY )
      g_audio_state = STOPPED;
  }

  return 0;
}

long audio_updateGain(void){
  if( g_audio_gain < 0 ) g_audio_gain = 0;
  if( g_audio_gain > 100 ) g_audio_gain = 100;

  if( g_audio_state == PAUSED ){
    out->SetGain(0 / 100.0);
    out->flush();
  }else if( g_audio_muted ){
    out->SetGain(MUTE_AUDIO_GAIN / 100.0);
  }else{
    out->SetGain(g_audio_gain / 100.0);
  }

  return 0;
}

void audio_MDCallback(void *cbData, const char *type, bool isUnicode, const char *string)
{
  (void)cbData;
  if (string[0] == '\0') { return; }
  if (strcmp(type, "eof") == 0){
    Serial.println("ID3: eof");
    return;
  }
  Serial.printf("ID3: %s: %s\n", type, string);
}

long audio_playMp3(const char *p_fname, bool reset_gain = false)
{
  audio_stopMp3();

  file_sd = new AudioFileSourceSD(p_fname);
  if( !file_sd->isOpen() ){
    delete file_sd;
    file_sd = NULL;
    return -1;
  }

  audio_onoff(true);
  id3 = new AudioFileSourceID3(file_sd);
  id3->RegisterMetadataCB(audio_MDCallback, (void*)"ID3TAG");
  mp3 = new AudioGeneratorMP3();
  g_audio_state = PLAYING;
  mp3->begin(file_sd, out);

  return 0;
}

long audio_playNextMp3(bool next){
  File base = SD.open(AUDIO_BASE_FOLDER);
  if( !base )
    return -1;

  long ret;
  File file;
  int num_of_sound = 0;
  for( int i = 0 ; file = base.openNextFile(); i++ ){
    num_of_sound++;
  }

  if( !next ){
    g_audio_index--;
    if( g_audio_index < 0 ) g_audio_index = num_of_sound -1;
  }else{
    g_audio_index++;
    if( g_audio_index >= num_of_sound ) g_audio_index = 0;
  }

  base.rewindDirectory();
  for( int i = 0 ; file = base.openNextFile(); i++ ){
    if( g_audio_index == i ){
      ret = audio_playMp3(file.name());
      file.close();
      base.close();
      return ret;
    }
    file.close();
  }

  return -1;
}

void audio_loop(void)
{
  if( mp3 != NULL ){
    if (mp3->isRunning()) {
      if( g_audio_state == PLAYING ){
        if (!mp3->loop()){
          audio_playNextMp3(true);
//          mp3->stop();
//          g_audio_state = STOPPED;
        }
      }
    }
  }
}

void setup() {
  // put your setup code here, to run once:

  long ret = m5_initialize();
  if( ret != 0 )
    Serial.println("m5_initialize error");

  udp.begin(LISTEN_PORT);

  ret = audio_initialize();
  if( ret != 0 )
    Serial.println("audio_initialize error");

  Serial.println("setup finished");
}

void loop() {
  // put your main code here, to run repeatedly:
  audio_loop();
  M5.update();

  while(true){
    int packetSize = udp.parsePacket();
    if( packetSize > 0 ){
      char *p_buffer = (char*)malloc(packetSize + 1);
      if( p_buffer == NULL )
        break;
      
      int len = udp.read(p_buffer, packetSize);
      if( len <= 0 ){
        free(p_buffer);
        break;
      }
      p_buffer[len] = '\0';

      Serial.println(p_buffer);
      long ret = processUdpPacket(p_buffer);
      free(p_buffer);
      if( ret != 0 )
        Serial.println("processUdpPacket error"); 
    }
    break;
  }

  if( M5.BtnA.wasPressed() ){
    if( g_audio_state == PAUSED ){
      g_audio_state = PLAYING;
      audio_updateGain();
    }else{
      if( g_audio_state == STANDBY )
        udpOnOffReport(true, DEFAULT_DEVICE_ID);
      audio_playNextMp3(false);
    }
  }else if( M5.BtnB.wasPressed() ){
    if( g_audio_state == PAUSED ){
      g_audio_state = PLAYING;
      audio_updateGain();
    }else if( g_audio_state == PLAYING ){
      g_audio_state = PAUSED;
      audio_updateGain();
    }else{
      if( g_audio_state == STANDBY )
        udpOnOffReport(true, DEFAULT_DEVICE_ID);
      audio_playNextMp3(true);
    }
  }else if( M5.BtnC.wasPressed() ){
    if( g_audio_state == PAUSED ){
      g_audio_state = PLAYING;
      audio_updateGain();
    }else{
      if( g_audio_state == STANDBY )
        udpOnOffReport(true, DEFAULT_DEVICE_ID);
      audio_playNextMp3(true);
    }
  }

  delay(1);
}

static long wifi_connect(const char *ssid, const char *password, unsigned long timeout)
{
  Serial.println("");
  Serial.print("WiFi Connenting");

  if( ssid == NULL && password == NULL )
    WiFi.begin();
  else
    WiFi.begin(ssid, password);
  unsigned long past = 0;
  while (WiFi.status() != WL_CONNECTED){
    Serial.print(".");
    delay(500);
    past += 500;
    if( past > timeout ){
      Serial.println("\nCan't Connect");
      return -1;
    }
  }
  Serial.print("\nConnected : IP=");
  Serial.print(WiFi.localIP());
  Serial.print(" Mac=");
  Serial.println(WiFi.macAddress());

  return 0;
}

static long m5_initialize(void)
{
  M5.begin(true, true, true, true);
  M5.Axp.SetSpkEnable(true);

//  Serial.begin(115200);
  Serial.println("[initializing]");

  long ret = wifi_connect(WIFI_SSID, WIFI_PASSWORD, WIFI_TIMEOUT);
  if( ret != 0 ){
    char ssid[32 + 1] = {'\0'};
    Serial.print("\ninput SSID:");
    Serial.setTimeout(SERIAL_TIMEOUT1);
    ret = Serial.readBytesUntil('\r', ssid, sizeof(ssid) - 1);
    if( ret <= 0 )
      return -1;

    delay(10);
    Serial.read();
    char password[32 + 1] = {'\0'};
    Serial.print("\ninput PASSWORD:");
    Serial.setTimeout(SERIAL_TIMEOUT2);
    ret = Serial.readBytesUntil('\r', password, sizeof(password) - 1);
    if( ret <= 0 )
      return -1;

    delay(10);
    Serial.read();
    Serial.printf("\nSSID=%s PASSWORD=", ssid);
    for( int i = 0 ; i < strlen(password); i++ )
      Serial.print("*");
    Serial.println("");
    ret = wifi_connect(ssid, password, WIFI_TIMEOUT);
    if( ret != 0 )
      return ret;
  }

  return 0;
}

long processUdpPacket(const char *p_message)
{
  if( strcmp(p_message, DISCOVERY_PACKET ) == 0 ){
      jsonDoc.clear();
      jsonDoc["device_id"] = DEFAULT_DEVICE_ID;
      jsonDoc["local_device_id"] = LOCAL_DEVICE_ID;

      long ret = udpSend(udp.remoteIP(), udp.remotePort());
      if( ret != 0 )
        return ret;
  }else if( strcmp(p_message, "CloseCompanion" ) == 0 ){
    // do nothing
  }else{
    DeserializationError err = deserializeJson(jsonDoc, p_message);
    if (err) {
      Serial.print(F("deserializeJson() failed with code "));
      Serial.println(err.f_str());
      return -1;
    }

    const char *intent = jsonDoc["payload"]["intent"];
    Serial.println(intent);
    const uint32_t msgId = jsonDoc["msgId"];
    if( strcmp( intent, "action.devices.QUERY") == 0 ){
      const char *p_device_id = jsonDoc["payload"]["device_id"];
      Serial.println(p_device_id);
      String device_id(p_device_id);

      if( strcmp(p_device_id, DEFAULT_DEVICE_ID) == 0 ){
        jsonDoc.clear();
        jsonDoc["msgId"] = msgId;
        jsonDoc["payload"]["device_id"] = device_id.c_str();
        jsonDoc["payload"]["states"]["on"] = (g_audio_state != STANDBY);
        jsonDoc["payload"]["states"]["activityState"] = (g_audio_state == STANDBY) ? "STANDBY" : "ACTIVE";
        jsonDoc["payload"]["states"]["playbackState"] = (g_audio_state == PAUSED) ? "PAUSED" : ((g_audio_state == PLAYING) ? "PLAYING" : "STOPPED");
        jsonDoc["payload"]["states"]["isMuted"] = g_audio_muted;
        jsonDoc["payload"]["states"]["currentVolume"] = (int)g_audio_gain;
        jsonDoc["payload"]["states"]["online"] = true;

        long ret = udpSend(udp.remoteIP(), udp.remotePort());
        if( ret != 0 )
          return ret;
      }else{
        Serial.printf("Unknown device_id: %s\n", p_device_id);
      }
    }else if( strcmp( intent, "action.devices.EXECUTE") == 0 ){
      const char *p_device_id = jsonDoc["payload"]["device_id"];
      Serial.println(p_device_id);
      String device_id(p_device_id);

      const char *p_command = jsonDoc["payload"]["command"];
      Serial.println(p_command);

      if( strcmp(p_device_id, DEFAULT_DEVICE_ID) == 0 ){
        if( strcmp(p_command, "action.devices.commands.OnOff") == 0 ){
          bool onoff = jsonDoc["payload"]["params"]["on"];
          audio_onoff(onoff);
          if( onoff )
            audio_playNextMp3(true);

          jsonDoc.clear();
          jsonDoc["msgId"] = msgId;
          jsonDoc["payload"]["device_id"] = device_id.c_str();
          jsonDoc["payload"]["states"]["on"] = onoff;
          jsonDoc["payload"]["states"]["online"] = true;
          long ret = udpSend(udp.remoteIP(), udp.remotePort());
          if( ret != 0 )
            return ret;
          ret = udpOnOffReport(onoff, device_id.c_str());
          if( ret != 0 )
            return ret;
        }else
        if( strcmp(p_command, "action.devices.commands.volumeRelative") == 0 ){
          int relativeSteps = jsonDoc["payload"]["params"]["relativeSteps"];
          g_audio_gain += relativeSteps;
          g_audio_muted = false;
          audio_updateGain();

          jsonDoc.clear();
          jsonDoc["msgId"] = msgId;
          jsonDoc["payload"]["device_id"] = device_id.c_str();
          jsonDoc["payload"]["states"]["currentVolume"] = (int)g_audio_gain;
          jsonDoc["payload"]["states"]["isMuted"] = g_audio_muted;
          jsonDoc["payload"]["states"]["online"] = true;
          long ret = udpSend(udp.remoteIP(), udp.remotePort());
          if( ret != 0 )
            return ret;
        }else
        if( strcmp(p_command, "action.devices.commands.setVolume") == 0 ){
          int volumeLevel = jsonDoc["payload"]["params"]["volumeLevel"];
          g_audio_gain = volumeLevel;
          g_audio_muted = false;
          audio_updateGain();

          jsonDoc.clear();
          jsonDoc["msgId"] = msgId;
          jsonDoc["payload"]["device_id"] = device_id.c_str();
          jsonDoc["payload"]["states"]["currentVolume"] = (int)g_audio_gain;
          jsonDoc["payload"]["states"]["isMuted"] = g_audio_muted;
          jsonDoc["payload"]["states"]["online"] = true;
          long ret = udpSend(udp.remoteIP(), udp.remotePort());
          if( ret != 0 )
            return ret;
        }else
        if( strcmp(p_command, "action.devices.commands.mute") == 0 ){
          bool mute = jsonDoc["payload"]["params"]["mute"];
          g_audio_muted = mute;
          audio_updateGain();

          jsonDoc.clear();
          jsonDoc["msgId"] = msgId;
          jsonDoc["payload"]["device_id"] = device_id.c_str();
          jsonDoc["payload"]["states"]["currentVolume"] = (int)g_audio_gain;
          jsonDoc["payload"]["states"]["isMuted"] = g_audio_muted;
          jsonDoc["payload"]["states"]["online"] = true;
          long ret = udpSend(udp.remoteIP(), udp.remotePort());
          if( ret != 0 )
            return ret;
        }else
        if( strcmp(p_command, "action.devices.commands.mediaResume") == 0 ){
          if( g_audio_state == PAUSED ){
            g_audio_state = PLAYING;
            audio_updateGain();
          }else{
            audio_playNextMp3(true);
          }

          jsonDoc.clear();
          jsonDoc["msgId"] = msgId;
          jsonDoc["payload"]["device_id"] = device_id.c_str();
          jsonDoc["payload"]["states"]["online"] = true;
          long ret = udpSend(udp.remoteIP(), udp.remotePort());
          if( ret != 0 )
            return ret;
        }else
        if( strcmp(p_command, "action.devices.commands.mediaNext") == 0 ){
          if( g_audio_state == PAUSED ){
            g_audio_state = PLAYING;
            audio_updateGain();
          }
          audio_playNextMp3(true);

          jsonDoc.clear();
          jsonDoc["msgId"] = msgId;
          jsonDoc["payload"]["device_id"] = device_id.c_str();
          jsonDoc["payload"]["states"]["online"] = true;
          long ret = udpSend(udp.remoteIP(), udp.remotePort());
          if( ret != 0 )
            return ret;
        }else
        if( strcmp(p_command, "action.devices.commands.mediaPrevious") == 0 ){
          if( g_audio_state == PAUSED ){
            g_audio_state = PLAYING;
            audio_updateGain();
          }
          audio_playNextMp3(false);

          jsonDoc.clear();
          jsonDoc["msgId"] = msgId;
          jsonDoc["payload"]["device_id"] = device_id.c_str();
          jsonDoc["payload"]["states"]["online"] = true;
          long ret = udpSend(udp.remoteIP(), udp.remotePort());
          if( ret != 0 )
            return ret;
        }else
        if( strcmp(p_command, "action.devices.commands.mediaPause") == 0 ){
          if( g_audio_state == PLAYING ){
            g_audio_state = PAUSED;
            audio_updateGain();
          }

          jsonDoc.clear();
          jsonDoc["msgId"] = msgId;
          jsonDoc["payload"]["device_id"] = device_id.c_str();
          jsonDoc["payload"]["states"]["online"] = true;
          long ret = udpSend(udp.remoteIP(), udp.remotePort());
          if( ret != 0 )
            return ret;
        }else
        if( strcmp(p_command, "action.devices.commands.mediaStop") == 0 ){
          audio_stopMp3();

          jsonDoc.clear();
          jsonDoc["msgId"] = msgId;
          jsonDoc["payload"]["device_id"] = device_id.c_str();
          jsonDoc["payload"]["states"]["online"] = true;
          long ret = udpSend(udp.remoteIP(), udp.remotePort());
          if( ret != 0 )
            return ret;
        }
      }else{
        Serial.printf("Unknown device_id: %s\n", p_device_id);
      }
    }else{
      Serial.printf("Unknown Intent: %s\n", intent);
    }
  }

  return 0;
}

long udpSend(IPAddress ipaddress, uint16_t port)
{
  int len = measureJson(jsonDoc);
  char *p_buffer = (char*)malloc(len + 1);
  if( p_buffer == NULL )
    return -1;
  int wroteLen = serializeJson(jsonDoc, p_buffer, len);
  p_buffer[wroteLen] = '\0';
  Serial.printf("response: %s\n\n", p_buffer);

  udp.beginPacket(ipaddress, port);
  udp.write((const uint8_t*)p_buffer, wroteLen);
  udp.endPacket();

  free(p_buffer);

  return 0;
}

long udpOnOffReport(bool onoff, const char *p_device_id)
{
  jsonDoc.clear();
  jsonDoc["msgId"] = 0;
  jsonDoc["payload"]["device_id"] = p_device_id;
  jsonDoc["payload"]["states"]["on"] = onoff;

  int len = measureJson(jsonDoc);
  char *p_buffer = (char*)malloc(len + 1);
  if( p_buffer == NULL )
    return -1;
  int wroteLen = serializeJson(jsonDoc, p_buffer, len);
  p_buffer[wroteLen] = '\0';
  Serial.printf("report: %s\n\n", p_buffer);

  udp.beginPacket(REPORT_HOST, REPORT_PORT);
  udp.write((const uint8_t*)p_buffer, wroteLen);
  udp.endPacket();

  free(p_buffer);

  return 0;
}

以下の部分を書き換えます。

#define LISTEN_PORT [待ち受けるUDPポート番号]
#define REPORT_HOST "[Node.jsサーバのURL]"
#define REPORT_PORT [Node.jsサーバが待ち受けるUDPポート番号]

さらに、以下も書き換えます。

#define DISCOVERY_PACKET "HelloLocalHomeSDK"
#define DEFAULT_DEVICE_ID "soundbar"
#define LOCAL_DEVICE_ID "deviceid123"

DISCOVERY_PACKETの内容は自由ですが、後述する値と同じにする必要があります。
DEFAULT_DEVICE_IDとLOCAL_DEVICE_IDの内容は自由ですが、onsync.jsのそれぞれid、deviceIdに指定した値と同じにする必要があります。

また、M5Core2に差し込むmicroSDカードの/Musicフォルダに再生したい複数のMP3ファイルを書き込んでおきます。

これで完成です。
Homeアプリから、デバイス登録しましょう。[test] ESP32スマートホーム というのが出てきているはずです。

ローカルフルフィルメントに対応する

以下のように実行します。

cd localfulfillment
npm install

以下の部分を書き換えます。

const UDP_DEVICE_PORT = [音楽再生デバイスのmain.cppに指定した待ち受けるUDPポート番号];

以下を実行します。

npm run build

そうすると、publicフォルダに、index.htmlとbundle.jsが出来上がっています。
作成したbundle.jsを以下の「Upload Java script files」ボタンから登録します。Node.jsとJavascriptの2つを登録するのですが、両方同じものを登録しました。また、「Support local query」にチェックを入れます。

また、「+ New scan config」ボタンをクリックして、以下の情報を入力します。

Broadcast address:音楽再生デバイスを含むブロードキャストIPアドレス
Discovery packet:音楽再生デバイスのmain.cppに指定したDiscovery Packet
Listen port: 自由。例えば、スマートホームサーバのindex.jsに指定した待ち受けるUDPポート番号と同じ値
Broadcast port:音楽再生デバイスのmain.cppに指定した待ち受けるUDPポート番号

これで完成です。
再度、[test] ESP32スマートホームを削除して、再登録します。念のため、Google Nest Hubを再起動するとよいかと思います。
そうすると、特に使う側から見て違いは分からないですが、自作のスマートホームサーバを介さずに、Google Nest Hubと音楽再生デバイスが直接通信して音楽再生をコントロールするようになります。(Google Nest Hubが音楽制裁デバイスを認識するのに、少し時間がかかります)

終わりに

かなり駆け足で投稿を書きました。
ポート番号の指定などややこしいです。不明な場合や抜け漏れは加筆するのでお知らせください。

以上

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
0
Help us understand the problem. What are the problem?