M5Core2を「OK Google」から操作できる音楽プレイヤにします。
Google Smart Homeのデバイスタイプ「メディアリモコン」を実装します。
また、応答速度を速めるために、ローカルフルフィルメントに対応させます。
もろもろのソースコードは以下にあります。
poruruba/MusicSmartHome_Google
全体的な流れを示します。
まずは、セットアップ時です。ユーザが、スマホのGoogleのHomeアプリから、デバイスのセットアップをするときのフローです。
ちなみに、後述する自作のスマートホームサーバの登録は済んでいる前提です。
①セットアップ開始
GoogleのHomeアプリから、デバイスのセットアップを開始します。
②ユーザ認証
ユーザを認証します。OAuth2やOpenIDConnectで認識できるユーザであれば誰でもよいので、今回はLINEアカウント認証を使いました。
③SYNCインテント
スマートホームサーバの登録時に指定したWebAPIに、SYNCインテントが飛んできますので、自身が管理する音楽再生デバイスの情報を返します。
④ローカルフルフィルメントアプリの登録
スマートホームサーバ登録時に、ローカルフルフィルメントアプリを登録しておいたので、それがスマートホームデバイス(絵ではGoogle Nest Hub)に登録されます。
⑤セットアップ完了
セットアップが完了し、以降Homeアプリから参照できるようになります。
⑥DiscoveryPacketのUDPパケット送受信
セットアップ完了後は、Google Nest Hubは定期的に音楽再生デバイスが存在するかをDiscoveryPacketの送信と応答で確認します。
次は、セットアップ完了後の流れです。「OK Google、サウンドバーで次の曲を再生」などの操作です。
①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や自作のスマートホームサーバを介さずに、直接音楽再生デバイスを操作します。これがローカルフルフィルメントです。
①OK Google
ユーザは、Google Nest Hubに対して、発話します。
④QUERY/EXECUTEのUDPパケット
登録済みのローカルフルフィルメントアプリは、その内容をUDPで音楽再生デバイスに転送し、音楽再生デバイスはその内容に応じて音楽を再生し、UDPパケットで応答します。
自作のスマートホームサーバの登録
後述すると言っていた、自作のスマートホームサーバの登録の説明をします。
と言っても、以前に投稿した内容と同じなので、サマリだけ。
ESP32をGoogle Smart Homeデバイスにする
デベロッパコンソールから、プロジェクトを作成します。
Actions on Google Developer Console
http://console.actions.google.com/
スマートホームを選択します。
呼びやすい名前を入れます。例えば、「ESP32スマートホーム」とか。
左側のナビゲーションメニューからActionsを選択し、Fullfillment URLに自作のスマートホームサーバのURLを指定します。
https://[立ち上げるNode.jsサーバのURL]/smarthome_google
Configure local home SDK (optional)は後で指定するので、とりあえずなにも設定しないで行きましょう。
左側のナビゲーションメニューから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等、お好みで
LINEの情報は、以下のURLから。
LINE Developers
LINEのCallback URLに以下を追加します。
https://oauth-redirect.googleusercontent.com/r/[GoogleHomeのプロジェクトID]
GoogleHomeのプロジェクト名は、Actions on Google Developer Consoleの右上のメニュー「Project settings」から確認できます。
HomeGraph APIを有効にします。
HomeGraph API
サービスアカウントのクレデンシャル情報を発行します。
[GoogleHomeプロジェクト名]-XXXXXXXXXXXX.json ってな感じのクレデンシャルファイルを生成します。
スマートホームサーバを立ち上げる
GitHubからZIPファイルをダウンロードします。
> unzip MusicSmartHome_Google-master.zip
> cd MusicSmartHome_Google-master/nodejs
> npm install
大事なのは以下のファイルです。
'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インテントに対して返答する内容です。
[
{
"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を音楽再生デバイスにする
ソースコードです。
#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が音楽制裁デバイスを認識するのに、少し時間がかかります)
終わりに
かなり駆け足で投稿を書きました。
ポート番号の指定などややこしいです。不明な場合や抜け漏れは加筆するのでお知らせください。
以上