ESP32でお気に入りの写真チェンジャーを作る

ちょっとだけ脱線して、ESP32でお気に入りの写真チェンジャーを作ってみます。
よく、Googleで単語での検索に加えて、画像やイラストを検索していますよね。
そこで、お気に入りの有名人の写真画像を検索して、それをESP32のLCDに表示し、しかもそれを定期的に別の画像に切り替えます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/2e853c2a-0f21-3d19-c550-b3659521772a.png)

もろもろのソースコードをGitHubに置いておきました。

poruruba/FavoriteGallery

https://github.com/poruruba/FavoriteGallery

# Google Custom Searchの取得

まずは、カスタム検索エンジンを作成します。

Google Programmable Search
 https://cse.google.com/cse/all
 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/05b733e9-90b4-7152-5f2e-364ee59a4265.png)

追加ボタンを押下。
とりあえず、検索するサイトに例えば「www.google.com」と入力。言語を日本語。検索エンジン名を例えば「Google画像検索」と入力し作成ボタンを押下します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/d94606ff-7397-90ea-ced7-f4424e134808.png)

「コントロールパネル」ボタンを押下

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/b1417d7e-d91a-df51-5590-e241dcdcb8d6.png)

検索エンジンIDをメモる。
画像検索をOn
 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/7f4ea804-5717-0c66-415b-50e8a1baaad5.png)

検索するサイトを削除して空にする。
ウェブ全体を検索をOn

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/5daadc37-259f-1a96-d855-2e18fb3be957.png)

　Custom Search JSON API
　1 日あたり 10,000 クエリを上限とします。

の「使ってみる」ボタンを押下。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/5c53e981-82ff-f527-eeaa-befce219b431.png)

Get a Keyボタンを押下。
自身が持つ、いずれかのプロジェクトを選択して、NEXTを押下。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/4c6c143a-2d93-8165-855c-ba616f4b149b.png)

生成されたYOUR API KEYをメモります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/76912690-23ea-1a4f-8e78-9d7938967775.png)

※場合によっては、作成されたAPIキーの利用アプリケーションの種類が制限されているかもしれません。その場合は以下から適切なものに変更してください。

https://console.cloud.google.com/apis/credentials

#画像検索サーバ

以下のnpmモジュールを使います。

googleapis/google-api-nodejs-client
　https://github.com/googleapis/google-api-nodejs-client

Googleカスタム検索エンジンを呼ぶ際に使います。

node-fetch/node-fetch
　https://github.com/node-fetch/node-fetch

HTTP Getで画像ファイルをダウンロードする際に使います。

lovell/sharp
　https://github.com/lovell/sharp
　https://sharp.pixelplumbing.com/

取得した画像ファイルをLCDのサイズに合わせて縮小し、JPEGファイルを生成します。

```js:server/api/controller/search-image/index.js
'use strict';

const SEARCH_API_KEY = process.env.SEARCH_API_KEY || '【APIキー】';
const SEARCH_CSE_ID = process.env.SEARCH_CSE_ID || '【検索エンジンID】';

const HELPER_BASE = process.env.HELPER_BASE || '../../helpers/';
const Response = require(HELPER_BASE + 'response');
const BinResponse = require(HELPER_BASE + 'binresponse');

const sharp = require('sharp')
const fetch = require('node-fetch');
const { google } = require('googleapis');
const customSearch = google.customsearch('v1');

exports.handler = async (event, context, callback) => {
	if( event.path == '/search-image'){
		console.log(event.queryStringParameters);

		var keyword = event.queryStringParameters.keyword || 'ミニオン';
		var num = event.queryStringParameters.num || 10;
		var width = event.queryStringParameters.width || 320;
		var height = event.queryStringParameters.height || 240;

		var link = await search_image(keyword, num);
		console.log(link);

		var buffer = await download_image(link, width, height);

		return new BinResponse('image/jpeg', buffer);
	}
};

async function search_image(keyword, num = 10){
	var index = Math.floor(Math.random() * num);
	const result = await customSearch.cse.list({
		cx: SEARCH_CSE_ID,
		q: keyword,
		auth: SEARCH_API_KEY,
		searchType: 'image',
		safe: 'high',
		num: 1, // max:10
		start: index + 1,
	});

	return result.data.items[0].link;
}

async function download_image(url, width, height){
  const blob = await fetch(url)
  .then(response =>{
    if( !response.ok )
      throw 'status is not 200';
    return response.blob();
  });
	
  const buffer = await blob.arrayBuffer();

return sharp(new Uint8Array(buffer))
  .resize({ width: width, height: height })
  .toFormat('jpeg')
  .toBuffer();
}
```

以下は先ほど取得したものです。
・【APIキー】
・【検索エンジンID】

「/search-image」というエンドポイントを立ち上げています。

以下、大したことはやっていないのですが、、、

・async function search_image(keyword, num = 10)

Googleカスタム検索エンジンを使って、キーワードに関する画像を検索します。numという入力がありますが、例えば、num個の検索結果からランダムに選ぶようにしています。これにより、同じキーワードでも呼び出すたびに違う画像が取得されるようになります。

・async function download_image(url, width, height)

node-fetchを使ってHTTP Getで画像をダウンロードし、sharpを使ってLCDサイズに合わせて縮小し、JPEG画像を出力しています。

#Arduino

大したことはやっていないのですが、20分ごとに、HTTP Getで先ほど立ち上げたサーバにキーワードを引数にして要求し、取得したJPEGファイルをLCDに表示させています。

以下のライブラリを使っています。（いつもありがとうございます）

lovyan03/LovyanGFX
　https://github.com/lovyan03/LovyanGFX

```c:FavoriteGallery/src/main.c
#include <Arduino.h>
#include <LovyanGFX.hpp>
#include "LGFX_Config_TTGO_TMusic.hpp"
#include <WiFi.h>
#include <HTTPClient.h>

const char* keyword = "【キーワード】";
const char* base_url = "【サーバのURL】/search-image";
const char* wifi_ssid = "【WiFiアクセスポイントのSSID】";
const char* wifi_password = "【WiFiアクセスポイントのパスワード】";
#define UPDATE_INTERVAL   (60 * 20)
#define NUM_OF_SEARCH     20

static LGFX lcd;

#define BACKGROUND_BUFFER_SIZE  70000
unsigned long background_buffer_length;
unsigned char background_buffer[BACKGROUND_BUFFER_SIZE];

void wifi_connect(const char *ssid, const char *password);
long doHttpGet(String url, uint8_t *p_buffer, unsigned long *p_len);
String urlencode(String str);

void setup() {
  lcd.init();
  lcd.setRotation(1);
  lcd.setBrightness(128);
  lcd.setColorDepth(24);
  Serial.begin(9600);
  
  wifi_connect(wifi_ssid, wifi_password);
}

void loop() {
  String url = base_url;
  url += "?keyword=";
  url += urlencode(keyword);
  url += "&num=" + String(NUM_OF_SEARCH);

  Serial.println(url);

  background_buffer_length = sizeof(background_buffer);
  long ret = doHttpGet(url, background_buffer, &background_buffer_length);
  if( ret != 0 ){
    Serial.println("doHttpGet Error");
    delay(1000);
    return;
  }

  lcd.drawJpg(background_buffer, background_buffer_length, 0, 0);

  delay(1000UL * UPDATE_INTERVAL);
}

void wifi_connect(const char *ssid, const char *password){
  Serial.println("");
  Serial.print("WiFi Connenting");

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(1000);
  }
  Serial.println("");
  Serial.print("Connected : ");
  Serial.println(WiFi.localIP());
}

long doHttpGet(String url, uint8_t *p_buffer, unsigned long *p_len){
  HTTPClient http;

  Serial.print("[HTTP] GET begin...\n");
  // configure traged server and url
  http.begin(url);

  Serial.print("[HTTP] GET...\n");
  // start connection and send HTTP header
  int httpCode = http.GET();
  unsigned long index = 0;

  // httpCode will be negative on error
  if(httpCode > 0) {
      // HTTP header has been send and Server response header has been handled
      Serial.printf("[HTTP] GET... code: %d\n", httpCode);

      // file found at server
      if(httpCode == HTTP_CODE_OK) {
        // get tcp stream
        WiFiClient * stream = http.getStreamPtr();

        // get lenght of document (is -1 when Server sends no Content-Length header)
        int len = http.getSize();
        Serial.printf("[HTTP] Content-Length=%d\n", len);
        if( len != -1 && len > *p_len ){
          Serial.printf("[HTTP] buffer size over\n");
          http.end();
          return -1;
        }

        // read all data from server
        while(http.connected() && (len > 0 || len == -1)) {
            // get available data size
            size_t size = stream->available();

            if(size > 0) {
                // read up to 128 byte
                if( (index + size ) > *p_len){
                  Serial.printf("[HTTP] buffer size over\n");
                  http.end();
                  return -1;
                }
                int c = stream->readBytes(&p_buffer[index], size);

                index += c;
                if(len > 0) {
                    len -= c;
                }
            }
            delay(1);
        }
      }else{
        http.end();
        return -1;
      }
  } else {
    http.end();
    Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
    return -1;
  }

  http.end();
  *p_len = index;

  return 0;
}

String urlencode(String str){
    String encodedString = "";
    char c;
    char code0;
    char code1;
//    char code2;
    for (int i = 0 ; i < str.length() ; i++){
      c = str.charAt(i);
      if (c == ' '){
        encodedString += '+';
      } else if (isalnum(c)){
        encodedString += c;
      } else{
        code1 = (c & 0xf) + '0';
        if ((c & 0xf) > 9){
            code1 = (c & 0xf) - 10 + 'A';
        }
        c = (c >> 4) & 0xf;
        code0 = c + '0';
        if (c > 9){
            code0 = c - 10 + 'A';
        }
//        code2 = '\0';
        encodedString += '%';
        encodedString += code0;
        encodedString += code1;
        //encodedString+=code2;
      }
      yield();
    }
    return encodedString;
}
```

以下の部分を環境に合わせてください。

・【キーワード】
・【サーバのURL】/search-image
・【WiFiアクセスポイントのSSID】
・【WiFiアクセスポイントのパスワード】

【キーワード】が、表示させたい画像の検索キーワードです。

先ほど見ていただいた通り、呼び出し回数は1日1万回です。
それに合わせて、以下の呼び出し頻度を決めてください。秒です

　> #define UPDATE_INTERVAL   (60 * 20)

#おわりに

以下を参考にしています。

　[ESP32で作るBeebotteダッシュボード](https://qiita.com/poruruba/items/00b488bc22d95c16f3e1)
　[M5Core2のLCDにWebページのスクリーンショットを表示する](https://qiita.com/poruruba/items/c2f1f5437aa1fe11513c)
　[ESP32でバイナリファイルのダウンロード・アップロード](https://qiita.com/poruruba/items/82a683866aef872665a4)

以上