ちょっとだけ脱線して、ESP32でお気に入りの写真チェンジャーを作ってみます。
よく、Googleで単語での検索に加えて、画像やイラストを検索していますよね。
そこで、お気に入りの有名人の写真画像を検索して、それをESP32のLCDに表示し、しかもそれを定期的に別の画像に切り替えます。
もろもろのソースコードをGitHubに置いておきました。
poruruba/FavoriteGallery
Google Custom Searchの取得
まずは、カスタム検索エンジンを作成します。
Google Programmable Search
https://cse.google.com/cse/all
追加ボタンを押下。
とりあえず、検索するサイトに例えば「www.google.com」と入力。言語を日本語。検索エンジン名を例えば「Google画像検索」と入力し作成ボタンを押下します。
「コントロールパネル」ボタンを押下
検索エンジンIDをメモる。
画像検索をOn
検索するサイトを削除して空にする。
ウェブ全体を検索をOn
Custom Search JSON API
1 日あたり 10,000 クエリを上限とします。
の「使ってみる」ボタンを押下。
Get a Keyボタンを押下。
自身が持つ、いずれかのプロジェクトを選択して、NEXTを押下。
生成されたYOUR API KEYをメモります。
※場合によっては、作成されたAPIキーの利用アプリケーションの種類が制限されているかもしれません。その場合は以下から適切なものに変更してください。
#画像検索サーバ
以下の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ファイルを生成します。
'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
#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ダッシュボード
M5Core2のLCDにWebページのスクリーンショットを表示する
ESP32でバイナリファイルのダウンロード・アップロード
以上