ESP32で極限までサンプルコードを変えずに、電子ペーパーに任意の画像を表示する方法 - SHIWORI

どうも、ゆるゆるです。この記事は組み込みマイスター2018-2019で作成した *SHIWORI* で使ったコードの紹介です。

> 同じチームのshoくんが色々書いてくれました！SHIWORIについてはこちらからお願いします
https://sho0126hiro.hatenablog.com/entry/2019/03/12/141824
~~SHIWORIは「より細かな読書記録を、馴染み深い本で」をテーマに作成しているデバイス・アプリの総称です。
説明が面倒くさい 成果発表の動画があるのでそれで勘弁してください...~~
https://youtu.be/OaXHa2w4-VU?t=6646

カメラが動かなくてヤバヤバのヤバだったので、何かしようと思って（ここで発表前日）電子ペーパーを使って現在読んでいる本の表紙を表示することにしました。

そこで使ったコードの保存です。

当然のことながら限界開発してたので超絶簡単に思いついたままの実装をしました。許してください。

# 概要・構成
### もの

- esp32
- [電子ペーパー(waveshare 13338)](https://www.sengoku.co.jp/mod/sgk_cart/detail.php?code=EEHD-58WS)
- 2.4GHzのwifi環境
- 適当なサーバーで動いてるexpress

### 動作

1. アプリから読書中の本の選択プロセスがあるので、そのタイミングで表紙画像をサーバーにダウンロード
2. esp32のsetupでexpressから表示画像のダウンロード・表示

# サーバー側

expressはpublic以下のファイルを自動的に配信してくれるので、`/public/images/UserID.bmp` に保存することでなんとかします。セキュリティ？？？？？？？？？？？？？？？
ちなみに表紙はGoogleBooksから持ってきています。ありがとうございます。

※抜粋します

```router.js
router.get('/current', async function(req, res, next) {
   const user_id = req.query.user_id;
   const c_book_id = req.query.book_id;
//shiwori.getBookData(id)はgooglebooksapiから情報を取ってくる関数
   var book = await shiwori.getBookData(c_book_id);
   let url = "";
//大きいサイズからダウンロード試行
   if(book.imgUrl.large != null) url = book.imgUrl.large;
   else if(book.imgUrl.medium != null) url = book.imgUrl.medium;
   else if(book.imgUrl.small != null) url = book.imgUrl.small;
   else if(book.imgUrl.thumbnail != null) url = book.imgUrl.thumbnail;
   else if(book.imgUrl.smallThumbnail != null) url = book.imgUrl.smallThumbnail;
   else {
     res.status(300).end();
     return;
   }
   Jimp.read(url, function(err, image) {
     if(err) {
       res.status(400).json(err);
       return;
     }
     image.scaleToFit(200, 200);
//よくわからないけど鏡文字になったから回せ
     image.flip(false, true);
     const path = "public/images/"+user_id+".bmp";
     image.write(path);
     res.sendStatus(200);
   });
})
```

Jimp神、天才。これのおかげ。


# ハード(ESP32)側
あ、arduinoです。
極限までサンプルを変更しないで使います。

参考 https://qiita.com/nanbuwks/items/14257cf5f9edd192b8ec

まず、必要なライブラリをインストールします
https://github.com/ZinggJM/GxEPD
`ファイル > スケッチ例 > GxEPD > GxEPD_WiFi_Example` を開きます。

コードを参考にピンをぶっ刺します。

```
// mapping suggestion for ESP32, e.g. LOLIN32, see .../variants/.../pins_arduino.h for your board
// NOTE: there are variants with different pins for SPI ! CHECK SPI PINS OF YOUR BOARD
// BUSY -> 4, RST -> 16, DC -> 17, CS -> SS(5), CLK -> SCK(18), DIN -> MOSI(23), GND -> GND, 3.3V -> 3.3V
```
![IMG_1417.JPG](https://qiita-image-store.s3.amazonaws.com/0/286025/515ed3e5-23d2-d03e-a773-7740b8209bf5.jpeg)
邪魔な配線が多いですねw

<br>
<br>

そして、ここを

```
#include <GxEPD.h>
```

こう変えます

```
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <GxEPD.h>
```

使う電子ペーパーモジュールに従ってコメントを外します

```
//以下を解除
#include <GxGDEW0154Z04/GxGDEW0154Z04.h>  // 1.54" b/w/r 200x200
```
最後にwifiのssidとパスを直打ちします。
これでコンパイル通ってサンプルの描画が始まりますね。


<br>
<br>
<br>

**移植します**

<br>

```.cpp
#include <WiFi.h>
#include <WiFiClient.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <Adafruit_NeoPixel.h>
#include <GxEPD.h>
// select the display class to use, only one
#include <GxGDEW0154Z04/GxGDEW0154Z04.h>  // 1.54" b/w/r 200x200
//#include <GxGDEW029Z10/GxGDEW029Z10.h>    // 2.9" b/w/r
#include <GxIO/GxIO_SPI/GxIO_SPI.h>
#include <GxIO/GxIO.h>

const char* ssid     = "自分のsaid";
const char* password = "パス";
const int httpPort  = 80; //httpsはゴメン

String user_id = "UserID"; //直打ち

const char* shiwori_book = "http://expressのアドレス/current";
void showBitmapFrom_HTTP(const char* host, const char* path, const char* filename, int16_t x, int16_t y, bool with_color = true);
//esp32用
GxIO_Class io(SPI, /*CS=5*/ SS, /*DC=*/ 17, /*RST=*/ 16);
GxEPD_Class display(io, /*RST=*/ 16, /*BUSY=*/ 4);

void setup()
{
  Serial.begin(115200);
  display.init(115200);
  delay(10);
  // We start by connecting to a WiFi network
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
  int16_t x = (display.width() - 200) / 2;
  int16_t y = (display.height() - 200) / 2;
  showBitmapFrom_HTTP("expressのアドレス", "/images/", "UserID.bmp", x+25, y);
}

void loop(){}

//以下サンプルのコピペ
static const uint16_t input_buffer_pixels = 640; // may affect performance

static const uint16_t max_palette_pixels = 256; // for depth <= 8

uint8_t input_buffer[3 * input_buffer_pixels]; // up to depth 24
uint8_t mono_palette_buffer[max_palette_pixels / 8]; // palette buffer for depth <= 8 b/w
uint8_t color_palette_buffer[max_palette_pixels / 8]; // palette buffer for depth <= 8 c/w

void drawBitmapFrom_HTTP_ToBuffer(const char* host, const char* path, const char* filename, int16_t x, int16_t y, bool with_color)
{
  WiFiClient client;
  bool connection_ok = false;
  bool valid = false; // valid format to be handled
  bool flip = true; // bitmap is stored bottom-to-top
  uint32_t startTime = millis();
  if ((x >= display.width()) || (y >= display.height())) return;
  display.fillScreen(GxEPD_WHITE);
  Serial.print("connecting to "); Serial.println(host);
  if (!client.connect(host, httpPort))
  {
    Serial.println("connection failed");
    return;
  }
  Serial.print("requesting URL: ");
  Serial.println(String("http://") + host + path + filename);
  client.print(String("GET ") + path + filename + " HTTP/1.1\r\n" +
               "Host: " + host + "\r\n" +
               "User-Agent: GxEPD2_Spiffs_Loader\r\n" +
               "Connection: close\r\n\r\n");
  Serial.println("request sent");
  while (client.connected())
  {
    String line = client.readStringUntil('\n');
    if (!connection_ok)
    {
      connection_ok = line.startsWith("HTTP/1.1 200 OK");
      if (connection_ok) Serial.println(line);
      //if (!connection_ok) Serial.println(line);
    }
    if (!connection_ok) Serial.println(line);
    //Serial.println(line);
    if (line == "\r")
    {
      Serial.println("headers received");
      break;
    }
  }
  if (!connection_ok) return;
  // Parse BMP header
  if (read16(client) == 0x4D42) // BMP signature
  {
    uint32_t fileSize = read32(client);
    uint32_t creatorBytes = read32(client);
    uint32_t imageOffset = read32(client); // Start of image data
    uint32_t headerSize = read32(client);
    uint32_t width  = read32(client);
    uint32_t height = read32(client);
    uint16_t planes = read16(client);
    uint16_t depth = read16(client); // bits per pixel
    uint32_t format = read32(client);
    uint32_t bytes_read = 7 * 4 + 3 * 2; // read so far
    if ((planes == 1) && ((format == 0) || (format == 3))) // uncompressed is handled, 565 also
    {
      Serial.print("File size: "); Serial.println(fileSize);
      Serial.print("Image Offset: "); Serial.println(imageOffset);
      Serial.print("Header size: "); Serial.println(headerSize);
      Serial.print("Bit Depth: "); Serial.println(depth);
      Serial.print("Image size: ");
      Serial.print(width);
      Serial.print('x');
      Serial.println(height);
      // BMP rows are padded (if needed) to 4-byte boundary
      uint32_t rowSize = (width * depth / 8 + 3) & ~3;
      if (depth < 8) rowSize = ((width * depth + 8 - depth) / 8 + 3) & ~3;
      if (height < 0)
      {
        height = -height;
        flip = false;
      }
      uint16_t w = width;
      uint16_t h = height;
      if ((x + w - 1) >= display.width())  w = display.width()  - x;
      if ((y + h - 1) >= display.height()) h = display.height() - y;
      valid = true;
      uint8_t bitmask = 0xFF;
      uint8_t bitshift = 8 - depth;
      uint16_t red, green, blue;
      bool whitish=false, colored=false;
      if (depth == 1) with_color = false;
      if (depth <= 8)
      {
        if (depth < 8) bitmask >>= depth;
        bytes_read += skip(client, 54 - bytes_read); //palette is always @ 54
        for (uint16_t pn = 0; pn < (1 << depth); pn++)
        {
          blue  = client.read();
          green = client.read();
          red   = client.read();
          client.read();
          bytes_read += 4;
          whitish = with_color ? ((red > 0x80) && (green > 0x80) && (blue > 0x80)) : ((red + green + blue) > 3 * 0x80); // whitish
          colored = (red > 0xF0) || ((green > 0xF0) && (blue > 0xF0)); // reddish or yellowish?
          if (0 == pn % 8) mono_palette_buffer[pn / 8] = 0;
          mono_palette_buffer[pn / 8] |= whitish << pn % 8;
          if (0 == pn % 8) color_palette_buffer[pn / 8] = 0;
          color_palette_buffer[pn / 8] |= colored << pn % 8;
          //Serial.print("0x00"); Serial.print(red, HEX); Serial.print(green, HEX); Serial.print(blue, HEX);
          //Serial.print(" : "); Serial.print(whitish); Serial.print(", "); Serial.println(colored);
        }
      }
      display.fillScreen(GxEPD_WHITE);
      uint32_t rowPosition = flip ? imageOffset + (height - h) * rowSize : imageOffset;
      //Serial.print("skip "); Serial.println(rowPosition - bytes_read);
      bytes_read += skip(client, rowPosition - bytes_read);
      for (uint16_t row = 0; row < h; row++, rowPosition += rowSize) // for each line
      {
        if (!connection_ok || !client.connected()) break;
        delay(1); // yield() to avoid WDT
        uint32_t in_remain = rowSize;
        uint32_t in_idx = 0;
        uint32_t in_bytes = 0;
        uint8_t in_byte = 0; // for depth <= 8
        uint8_t in_bits = 0; // for depth <= 8
        uint16_t color = GxEPD_WHITE;
        for (uint16_t col = 0; col < w; col++) // for each pixel
        {
          yield();
          if (!connection_ok || !client.connected()) break;
          // Time to read more pixel data?
          if (in_idx >= in_bytes) // ok, exact match for 24bit also (size IS multiple of 3)
          {
            uint32_t get = in_remain > sizeof(input_buffer) ? sizeof(input_buffer) : in_remain;
            uint32_t got = read(client, input_buffer, get);
            while ((got < get) && connection_ok)
            {
              //Serial.print("got "); Serial.print(got); Serial.print(" < "); Serial.print(get); Serial.print(" @ "); Serial.println(bytes_read);
              uint32_t gotmore = read(client, input_buffer + got, get - got);
              got += gotmore;
              connection_ok = gotmore > 0;
            }
            in_bytes = got;
            in_remain -= got;
            bytes_read += got;
          }
          if (!connection_ok)
          {
            Serial.print("Error: got no more after "); Serial.print(bytes_read); Serial.println(" bytes read!");
            break;
          }
          switch (depth)
          {
            case 24:
              blue = input_buffer[in_idx++];
              green = input_buffer[in_idx++];
              red = input_buffer[in_idx++];
              whitish = with_color ? ((red > 0x80) && (green > 0x80) && (blue > 0x80)) : ((red + green + blue) > 3 * 0x80); // whitish
              colored = (red > 0xF0) || ((green > 0xF0) && (blue > 0xF0)); // reddish or yellowish?
              break;
            case 16:
              {
                uint8_t lsb = input_buffer[in_idx++];
                uint8_t msb = input_buffer[in_idx++];
                if (format == 0) // 555
                {
                  blue  = (lsb & 0x1F) << 3;
                  green = ((msb & 0x03) << 6) | ((lsb & 0xE0) >> 2);
                  red   = (msb & 0x7C) << 1;
                }
                else // 565
                {
                  blue  = (lsb & 0x1F) << 3;
                  green = ((msb & 0x07) << 5) | ((lsb & 0xE0) >> 3);
                  red   = (msb & 0xF8);
                }
                whitish = with_color ? ((red > 0x80) && (green > 0x80) && (blue > 0x80)) : ((red + green + blue) > 3 * 0x80); // whitish
                colored = (red > 0xF0) || ((green > 0xF0) && (blue > 0xF0)); // reddish or yellowish?
              }
              break;
            case 1:
            case 4:
            case 8:
              {
                if (0 == in_bits)
                {
                  in_byte = input_buffer[in_idx++];
                  in_bits = 8;
                }
                uint16_t pn = (in_byte >> bitshift) & bitmask;
                whitish = mono_palette_buffer[pn / 8] & (0x1 << pn % 8);
                colored = color_palette_buffer[pn / 8] & (0x1 << pn % 8);
                in_byte <<= depth;
                in_bits -= depth;
              }
              break;
          }
          if (whitish)
          {
            color = GxEPD_WHITE;
          }
          else if (colored && with_color)
          {
            color = GxEPD_RED;
          }
          else
          {
            color = GxEPD_BLACK;
          }
          uint16_t yrow = y + (flip ? h - row - 1 : row);
          display.drawPixel(x + col, yrow, color);
        } // end pixel
      } // end line
    }
    Serial.print("bytes read "); Serial.println(bytes_read);
  }
  Serial.print("loaded in "); Serial.print(millis() - startTime); Serial.println(" ms");
  if (!valid)
  {
    Serial.println("bitmap format not handled.");
  }
}

void showBitmapFrom_HTTP(const char* host, const char* path, const char* filename, int16_t x, int16_t y, bool with_color)
{
  Serial.println(); Serial.print("downloading file \""); Serial.print(filename);  Serial.println("\"");
  drawBitmapFrom_HTTP_ToBuffer(host, path, filename, x, y, with_color);
  display.update();
}

uint16_t read16(WiFiClient& client)
{
  // BMP data is stored little-endian, same as Arduino.
  uint16_t result;
  ((uint8_t *)&result)[0] = client.read(); // LSB
  ((uint8_t *)&result)[1] = client.read(); // MSB
  return result;
}

uint32_t read32(WiFiClient& client)
{
  // BMP data is stored little-endian, same as Arduino.
  uint32_t result;
  ((uint8_t *)&result)[0] = client.read(); // LSB
  ((uint8_t *)&result)[1] = client.read();
  ((uint8_t *)&result)[2] = client.read();
  ((uint8_t *)&result)[3] = client.read(); // MSB
  return result;
}

#if USE_BearSSL

uint32_t skip(BearSSL::WiFiClientSecure& client, int32_t bytes)
{
  int32_t remain = bytes;
  uint32_t start = millis();
  while (client.connected() && (remain > 0))
  {
    if (client.available())
    {
      int16_t v = client.read();
      remain--;
    }
    else delay(1);
    if (millis() - start > 2000) break; // don't hang forever
  }
  return bytes - remain;
}

uint32_t read(BearSSL::WiFiClientSecure& client, uint8_t* buffer, int32_t bytes)
{
  int32_t remain = bytes;
  uint32_t start = millis();
  while (client.connected() && (remain > 0))
  {
    if (client.available())
    {
      int16_t v = client.read();
      *buffer++ = uint8_t(v);
      remain--;
    }
    else delay(1);
    if (millis() - start > 2000) break; // don't hang forever
  }
  return bytes - remain;
}

#endif

uint32_t skip(WiFiClient& client, int32_t bytes)
{
  int32_t remain = bytes;
  uint32_t start = millis();
  while (client.connected() && (remain > 0))
  {
    if (client.available())
    {
      int16_t v = client.read();
      remain--;
    }
    else delay(1);
    if (millis() - start > 2000) break; // don't hang forever
  }
  return bytes - remain;
}

uint32_t read(WiFiClient& client, uint8_t* buffer, int32_t bytes)
{
  int32_t remain = bytes;
  uint32_t start = millis();
  while (client.connected() && (remain > 0))
  {
    if (client.available())
    {
      int16_t v = client.read();
      *buffer++ = uint8_t(v);
      remain--;
    }
    else delay(1);
    if (millis() - start > 2000) break; // don't hang forever
  }
  return bytes - remain;
}
```

loop以下を理解していないのでそのうち理解して実装したい。
![IMG_1797.jpg](https://qiita-image-store.s3.amazonaws.com/0/286025/083d88d3-9d63-c617-04e0-6e33b00405d0.jpeg)


# 動作動画
動作動画ってシャレっぽいw
[まーたyoutubeで申し訳ないです](https://youtu.be/OaXHa2w4-VU?t=12513)

# 参考url
とても助かりました。ありがとうございました。
・esp32のhttp系について https://qiita.com/tkt0821/items/5d730845f534c614f174
・jimpの使い方 https://s8a.jp/javascript-image-processing-library-jimp
・2回目の登場 https://qiita.com/nanbuwks/items/14257cf5f9edd192b8ec

# 今後とか感想とか

SHIWORIに関してですが、テンションが上がっていればカメラとかbluetoothとか構想通りのものになるように制作していきたいと思っています。

**サーバーの負荷やばいって？？？** ~~知らないよね~~ これも課題。

**カメラが動かなかったのなんで？** ライブラリにらめっこに負けました。よいこのみんなは古いのを使わないようにしようね。

コードはgithubにあります。

# 電子ペーパーは神！みんな使え！！！
おわり:wave: