LILYGO TTGOのT-WristbandでグラフィックライブラリLovyanGFXを使ってみた

らびやんさんが作っていただいている高性能グラフィックライブラリ「LovyanGFX」って、描画がすごいスムーズでいいですね！
TTGOのT-WristbandのLCDで動かしてみました。

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

以下、参考にさせていただきました。
　[M5 Stack/M5 StickC/Wio Terminalで使える高性能グラフィックライブラリ「LovyanGFX」](https://qiita.com/karaage0703/items/5c1a1d87ff7452ba3d9a)


# TTGO T-Wristband

TTGO T-Wristbandとは、以下にあるように、かのMi Bandに激似の、液晶付きの小型リストバンドです。開発ボードが付いているので、すぐに開発できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/16a231e9-eb1d-ca12-a199-c9ae0aa286bf.png)
　https://ja.aliexpress.com/i/4000527495064.html

0.96インチ液晶ディスプレイに、液晶ドライバICはST7735です。
解像度は、80×160ドットです。

回路図は、以下に行くとありますので、PIN番号がわかります。

Xinyuan-LilyGO/LilyGo-T-Wristband
　https://github.com/Xinyuan-LilyGO/LilyGO-T-Wristband

ちなみに、Arduinoでコンパイルするとき、ボードは「ESP32 Pico Kit」を選択します。COMポートの選択もお忘れなく。

# LovyanGFXの編集

とりあえず、以下のサイトからZIPをダウンロードして、Arduinoのライブラリをインクルード で、.ZIP形式のライブラリをインストール から、ダウンロードしたZIPファイルを選択してインストールします。

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

あるいは、Arduino IDEのライブラリマネージャから、LovyanGFXを選択してインストールでもよいです。

(2020/6/15 追記)
LovyanGFXが0.1.1にバージョンアップして、機種に合わせた設定がやりやすくなりました。
(2020/6/29 追記)
LovyanGFXが0.1.14にバージョンアップして、さらに機種に合わせた設定がやりやすくなりました。

T-Wristbandの設定値を取り込んでいただいたので、以下のようにインクルードするだけです！

```
#define LGFX_TTGO_TWRISTBAND
#include <LovyanGFX.hpp>
```

ということで、もう以下の設定は不要です。
＊＊＊＊ここから

``` X:\Users\XXXX\Documents\Arduino\libraries\LovyanGFX\ ``` に、同フォルダのconfにある```LGFX_Config_Custom.hpp``` をコピーしてきて、以下のように編集します。

```c:LGFX_Config_Custom.hpp
#ifndef LOVYANGFX_CONFIG_HPP_
#define LOVYANGFX_CONFIG_HPP_

#if defined( ARDUINO_ESP32_DEV ) // TTGO Wristband
namespace lgfx
{
// for ESP32
  struct LGFX_Config
  {
// 使用するSPIを VSPI_HOST または HSPI_HOST で設定します。
    static constexpr spi_host_device_t spi_host = VSPI_HOST;

// 使用するDMAチャンネルを 1か2で設定します。
// 使用しない場合は省略するか0を設定します。
    static constexpr int dma_channel = 1;

// SPIのSCLKのピン番号を設定します。
    static constexpr int spi_sclk = 18;

// SPIのMOSIのピン番号を設定します。
    static constexpr int spi_mosi = 19;

// SPIのMISOのピン番号を設定します。
// SDカード等と共通のSPIバスを使う場合はMISOも必ず設定してください。
// 使わない場合は省略するか-1を設定します。
    static constexpr int spi_miso = -1;

// SPI通信のデータ長を指定します。
// RaspberryPi用のLCD等を使用する場合に16を指定します。
// 省略時は 8 です。大抵のパネルは8ですので、基本的には省略してください。
    static constexpr int spi_dlen = 8;
  };
}

class LGFX : public lgfx::LGFX_SPI<lgfx::LGFX_Config>
{
// Panelクラスのインスタンスを作成します。使用するパネルにあったクラスをコメントアウトしてください。

//  ★LCD一体型製品の対応機種の場合はこちらから選択できます。
//  lgfx::Panel_DDUINO32_XS panel;
//  lgfx::Panel_LoLinD32 panel;
//  lgfx::Panel_M5Stack panel;
//  lgfx::Panel_M5StickC panel;
//  lgfx::Panel_ODROID_GO panel;
//  lgfx::Panel_TTGO_TS panel;
//  lgfx::Panel_TTGO_TWatch panel;
//  lgfx::Panel_WioTerminal panel;

//  ★対応機種以外の場合はこちらから選択できます。
//  lgfx::Panel_HX8357B panel;
//  lgfx::Panel_HX8357D panel;
//  lgfx::Panel_ILI9163 panel;
//  lgfx::Panel_ILI9341 panel;
//  lgfx::Panel_ILI9342 panel;
//  lgfx::Panel_ILI9486 panel;
//  lgfx::Panel_ILI9488 panel;
//  lgfx::Panel_SSD1351 panel;
//  lgfx::Panel_ST7789 panel;
  lgfx::Panel_ST7735S panel;

public:
  LGFX(void) : lgfx::LGFX_SPI<lgfx::LGFX_Config>()
  {
// パネルクラスに各種設定値を代入していきます。
// （LCD一体型製品のパネルクラスを選択した場合は、
//   製品に合った初期値が設定されているので設定は不要です）

// 通常動作時のSPIクロックを設定します。
// ESP32のSPIは80MHzを整数で割った値のみ使用可能です。
// 設定した値に一番近い設定可能な値が使用されます。
    panel.freq_write = 27000000;

// 単色の塗り潰し処理時のSPIクロックを設定します。
// 基本的にはfreq_writeと同じ値を設定しますが、
// より高い値を設定しても動作する場合があります。
    panel.freq_fill  = 27000000;

// LCDから画素データを読取る際のSPIクロックを設定します。
    panel.freq_read  = 16000000;

// SPI通信モードを0~3から設定します。
    panel.spi_mode = 0;

// データ読み取り時のSPI通信モードを0~3から設定します。
    panel.spi_mode_read = 0;

// 画素読出し時のダミービット数を設定します。
// 画素読出しでビットずれが起きる場合に調整してください。
    panel.len_dummy_read_pixel = 8;

// データの読取りが可能なパネルの場合はtrueを、不可の場合はfalseを設定します。
// （例：CSピンのないST7789等は読出し不可のためfalseを設定します。）
// 省略時はtrueになります。
    panel.spi_read = false;

// データの読取りMOSIピンで行うパネルの場合はtrueを設定します。
// （例：M5StackやT-Watchはtrue、ESP-WROVER-KITはfalseを設定します。）
// 省略時はfalseになります。
    panel.spi_3wire = false;

// LCDのCSを接続したピン番号を設定します。
// 使わない場合は省略するか-1を設定します。
    panel.spi_cs = 5;

// LCDのDCを接続したピン番号を設定します。
    panel.spi_dc = 23;

// LCDのRSTを接続したピン番号を設定します。
// 使わない場合は省略するか-1を設定します。
    panel.gpio_rst = 26;

// LCDのバックライトを接続したピン番号を設定します。
// 使わない場合は省略するか-1を設定します。
    panel.gpio_bl  = 27;

// バックライト使用時、輝度制御に使用するPWMチャンネル番号を設定します。
// PWM輝度制御を使わない場合は省略するか-1を設定します。
    panel.pwm_ch_bl = 7;

// バックライト点灯時の出力レベルがローかハイかを設定します。
// 省略時は true。true=HIGHで点灯 / false=LOWで点灯になります。
    panel.backlight_level = true;

// invertDisplayの初期値を設定します。trueを設定すると反転します。
// 省略時は false。画面の輝度が反転している場合は設定を変更してください。
    panel.invert = false;

// パネルの色順がを設定します。  RGB=true / BGR=false
// 省略時はfalse。赤と青が入れ替わっている場合は設定を変更してください。
    panel.rgb_order = false;

// LCDコントローラのメモリ上のピクセル数（幅と高さ）を設定します。
// 設定が合っていない場合、setRotationを使用した際の座標がずれます。
// （例：ST7735は 132x162 / 128x160 / 132x132 の３通りが存在します）
    panel.memory_width  = 132;
    panel.memory_height = 162;

// パネルの実際に表示可能なピクセル数（幅と高さ）を設定します。
// 省略時はパネルクラスのデフォルト値が使用されます。
    panel.panel_width  = 80;
    panel.panel_height = 160;

// パネルのオフセット量を設定します。
// 省略時はパネルクラスのデフォルト値が使用されます。
    panel.offset_x = 26;
    panel.offset_y = 1;

// setRotationの初期化直後の値を設定します。
    panel.rotation = 0;

// setRotationを使用した時の向きを変更したい場合、offset_rotationを設定します。
// setRotation(0)での向きを 1の時の向きにしたい場合、 1を設定します。
    panel.offset_rotation = 2;



// 最後に、用意したパネル設定をセットします。
    setPanel(&panel);
  }
};
#endif

#endif
```

以下は古いやり方です。
＊＊＊ここから

次に、``` X:\Users\XXXX\Documents\Arduino\libraries\LovyanGFX-master\src\lgfx\panel``` にある「panel_ST7735.hpp」の適当な場所に以下を追加します。

```c:panel_ST7735.hpp
  struct Panel_TTGO_Wristband : public Panel_ST7735S
  {
    Panel_TTGO_Wristband(void) {
      freq_write = 27000000;
      panel_width  = 80;
      panel_height = 160;
      offset_x     = 26;
      offset_y     = 1;
      offset_rotation = 2;
      rgb_order = true;
      spi_3wire = true;
      spi_cs    = 5;
      spi_dc    = 23;
      gpio_rst  = 26;
      gpio_bl   = 27;
      pwm_ch_bl = 7;
    }
  };
```

ここら辺の値は、同じメーカのTTGO-TSを参考にしています。

次に、```X:\Users\XXXX\Documents\Arduino\libraries\LovyanGFX-master\src``` にある「LovyanGFX.hpp」の適当な場所に以下を追加します。

```c:LovyanGFX.hpp
#elif defined( ARDUINO_ESP32_PICO ) // TTGO Wristband

  typedef Panel_TTGO_Wristband Panel_default;
  struct LGFX_Config {
    static constexpr spi_host_device_t spi_host = VSPI_HOST;
    static constexpr int dma_channel = 1;
    static constexpr int spi_mosi = 19;
    static constexpr int spi_miso = -1;
    static constexpr int spi_sclk =  18;
  };
```

＊＊ここまで

コンパイル時のボードとして「ESP Pico Kit」を選択したので、define 「ARDUINO_ESP32_PICO」にひっかけています。

＊＊＊＊ここまで

あとは、LovyanGFXについているサンプルをコンパイル、書き込みをすると、LCDに表示されるかと思います。ちょっと解像度が小さいので見にくいですが。

# RTC PCF8563を使った時計表示サンプル

TTGO T-Wristbandには、RTCのPCF8563が付いていますので、これを使ってアナログ時刻表示をしてみます。オリジナルは、付属するサンプルの「ClockSample.ino」をRTC PCF8563に対応させただけで、ほぼそのまま使わせていただきました。

準備として、以下のライブラリをArduino IDEにインストールしておきます。
・PCF8563_Library

で、ソースコードです。

```c:ClockSample.ino
#include <pcf8563.h>
#include <Wire.h>

PCF8563_Class rtc;
#define I2C_SDA_PIN         21
#define I2C_SCL_PIN         22

void setupRTC()
{
  Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
  Wire.setClock(400000);
  
  rtc.begin(Wire);
  //Check if the RTC clock matches, if not, use compile time
  rtc.check();
}

#if defined(ARDUINO_M5Stick_C)
#include <AXP192.h>
static AXP192 axp;
#endif

#include <LovyanGFX.hpp>

static LGFX lcd;
static LGFX_Sprite canvas(&lcd);       // オフスクリーン描画用バッファ
static LGFX_Sprite clockbase(&canvas); // 時計の文字盤パーツ
static LGFX_Sprite needle1(&canvas);   // 長針・短針パーツ
static LGFX_Sprite shadow1(&canvas);   // 長針・短針の影パーツ
static LGFX_Sprite needle2(&canvas);   // 秒針パーツ
static LGFX_Sprite shadow2(&canvas);   // 秒針の影パーツ

static constexpr uint64_t oneday = 86400000; // 1日 = 1000msec x 60sec x 60min x 24hour = 86400000
static uint64_t count = random(oneday);    // 現在時刻 (ミリ秒カウンタ)
static int32_t width = 239;             // 時計の縦横画像サイズ
static int32_t halfwidth = width >> 1;  // 時計盤の中心座標
static auto transpalette = 0;           // 透過色パレット番号
static float zoom;                      // 表示倍率

void setup(void)
{
  Serial.begin(115200);

#if defined(ARDUINO_M5Stick_C)
  axp.begin();
#endif

  setupRTC();
    
  lcd.init();

  zoom = (float)(std::min(lcd.width(), lcd.height())) / width; // 表示が画面にフィットするよう倍率を調整

  lcd.setPivot(lcd.width() >> 1, lcd.height() >> 1); // 時計描画時の中心を画面中心に合わせる

  canvas.setColorDepth(lgfx::palette_4bit);  // 各部品を４ビットパレットモードで準備する
  clockbase.setColorDepth(lgfx::palette_4bit);
  needle1.setColorDepth(lgfx::palette_4bit);
  shadow1.setColorDepth(lgfx::palette_4bit);
  needle2.setColorDepth(lgfx::palette_4bit);
  shadow2.setColorDepth(lgfx::palette_4bit);
// パレットの初期色はグレースケールのグラデーションとなっており、
// 0番が黒(0,0,0)、15番が白(255,255,255)
// 1番～14番は黒から白へ段階的に明るさが変化している
//
// パレットを使う場合、描画関数は色の代わりに0～15のパレット番号を指定する

  canvas.createSprite(width, width); // メモリ確保
  clockbase.createSprite(width, width);
  needle1.createSprite(9, 119);
  shadow1.createSprite(9, 119);
  needle2.createSprite(3, 119);
  shadow2.createSprite(3, 119);

  canvas.fillScreen(transpalette); // 透過色で背景を塗り潰す (create直後は0埋めされているので省略可能)
  clockbase.fillScreen(transpalette);
  needle1.fillScreen(transpalette);
  shadow1.fillScreen(transpalette);

  clockbase.setTextFont(4);           // フォント種類を変更(時計盤の文字用)
  clockbase.setTextDatum(lgfx::middle_center);
  clockbase.fillCircle(halfwidth, halfwidth, halfwidth    ,  6); // 時計盤の背景の円を塗る
  clockbase.drawCircle(halfwidth, halfwidth, halfwidth - 1, 15);
  for (int i = 1; i <= 60; ++i) {
    float rad = i * 6 * - 0.0174532925;              // 時計盤外周の目盛り座標を求める
    float cosy = - cos(rad) * (halfwidth * 10 / 11);
    float sinx = - sin(rad) * (halfwidth * 10 / 11);
    bool flg = 0 == (i % 5);      // ５目盛り毎フラグ
    clockbase.fillCircle(halfwidth + sinx + 1, halfwidth + cosy + 1, flg * 3 + 1,  4); // 目盛りを描画
    clockbase.fillCircle(halfwidth + sinx    , halfwidth + cosy    , flg * 3 + 1, 12);
    if (flg) {                    // 文字描画
      cosy = - cos(rad) * (halfwidth * 10 / 13);
      sinx = - sin(rad) * (halfwidth * 10 / 13);
      clockbase.setTextColor(1);
      clockbase.drawNumber(i/5, halfwidth + sinx + 1, halfwidth + cosy + 4);
      clockbase.setTextColor(15);
      clockbase.drawNumber(i/5, halfwidth + sinx    , halfwidth + cosy + 3);
    }
  }
  clockbase.setTextFont(7);

  needle1.setPivot(4, 100);  // 針パーツの回転中心座標を設定する
  shadow1.setPivot(4, 100);
  needle2.setPivot(1, 100);
  shadow2.setPivot(1, 100);

  for (int i = 6; i >= 0; --i) {  // 針パーツの画像を作成する
    needle1.fillTriangle(4, - 16 - (i<<1), 8, needle1.height() - (i<<1), 0, needle1.height() - (i<<1), 15 - i);
    shadow1.fillTriangle(4, - 16 - (i<<1), 8, shadow1.height() - (i<<1), 0, shadow1.height() - (i<<1),  1 + i);
  }
  for (int i = 0; i < 7; ++i) {
    needle1.fillTriangle(4, 16 + (i<<1), 8, needle1.height() + 32 + (i<<1), 0, needle1.height() + 32 + (i<<1), 15 - i);
    shadow1.fillTriangle(4, 16 + (i<<1), 8, shadow1.height() + 32 + (i<<1), 0, shadow1.height() + 32 + (i<<1),  1 + i);
  }
  needle1.fillTriangle(4, 32, 8, needle1.height() + 64, 0, needle1.height() + 64, 0);
  shadow1.fillTriangle(4, 32, 8, shadow1.height() + 64, 0, shadow1.height() + 64, 0);
  needle1.fillRect(0, 117, 9, 2, 15);
  shadow1.fillRect(0, 117, 9, 2,  1);
  needle1.drawFastHLine(1, 117, 7, 12);
  shadow1.drawFastHLine(1, 117, 7,  4);

  needle1.fillCircle(4, 100, 4, 15);
  shadow1.fillCircle(4, 100, 4,  1);
  needle1.drawCircle(4, 100, 4, 14);

  needle2.fillScreen(9);
  shadow2.fillScreen(3);
  needle2.drawFastVLine(1, 0, 119, 8);
  shadow2.drawFastVLine(1, 0, 119, 1);
  needle2.fillRect(0, 99, 3, 3, 8);

  lcd.startWrite();

//  shadow1.pushSprite(&lcd,  0, 0); // デバッグ用、パーツを直接LCDに描画する
//  needle1.pushSprite(&lcd, 10, 0);
//  shadow2.pushSprite(&lcd, 20, 0);
//  needle2.pushSprite(&lcd, 25, 0);
}

void update7Seg(int32_t hour, int32_t min)
{ // 時計盤のデジタル表示部の描画
  int x = clockbase.getPivotX() - 69;
  int y = clockbase.getPivotY();
  clockbase.setCursor(x, y);
  clockbase.setTextColor(5);  // 消去色で 88:88 を描画する
  clockbase.print("88:88");
  clockbase.setCursor(x, y);
  clockbase.setTextColor(12); // 表示色で時:分を描画する
  clockbase.printf("%02d:%02d", hour, min);
}

void drawDot(int pos, int palette)
{
  bool flg = 0 == (pos % 5);      // ５目盛り毎フラグ
  float rad = pos * 6 * - 0.0174532925;              // 時計盤外周の目盛り座標を求める
  float cosy = - cos(rad) * (halfwidth * 10 / 11);
  float sinx = - sin(rad) * (halfwidth * 10 / 11);
  canvas.fillCircle(halfwidth + sinx, halfwidth + cosy, flg * 3 + 1, palette);
}

void drawClock(void)
{ // 時計の描画
  static int32_t p_min = -1;
  
  RTC_Date datetime = rtc.getDateTime();
  int32_t sec = datetime.second;
  int32_t min = datetime.minute;
  int32_t hour = datetime.hour;

  clockbase.pushSprite(0, 0);  // 描画用バッファに時計盤の画像を上書き
  if (p_min != min) { // 分の値が変化していれば時計盤のデジタル表示部分を更新
    p_min = min;
    update7Seg(hour, min);
  }
  
  drawDot(sec, 14);
  drawDot(min, 15);
  drawDot((hour*5)%60, 15);

  float fhour = (float)((hour % 12) * 30 + min / 2);  // 短針の角度
  float fmin  = (float)min * 6;  // 長針の角度
  float fsec  = (float)sec * 6;  // 秒針の角度
  int px = canvas.getPivotX();
  int py = canvas.getPivotY();
  shadow1.pushRotateZoom(px+2, py+2, fhour, 1.0, 0.7, transpalette); // 針の影を右下方向にずらして描画する
  shadow1.pushRotateZoom(px+3, py+3, fmin , 1.0, 1.0, transpalette);
  shadow2.pushRotateZoom(px+4, py+4, fsec , 1.0, 1.0, transpalette);
  needle1.pushRotateZoom(            fhour, 1.0, 0.7, transpalette); // 針を描画する
  needle1.pushRotateZoom(            fmin , 1.0, 1.0, transpalette);
  needle2.pushRotateZoom(            fsec , 1.0, 1.0, transpalette);

  canvas.pushRotateZoom(0, zoom, zoom, transpalette);    // 完了した時計盤をLCDに描画する
}

void loop(void)
{
  static uint32_t p_milli = 0;
  uint32_t milli = millis() % 1000;
  if (p_milli < milli) count +=        (milli - p_milli);
  else                 count += 1000 + (milli - p_milli);
  p_milli = milli;

  int32_t tmp = (count % 1000) >> 3;
  canvas.setPaletteColor(8, 255 - (tmp>>1), 255 - (tmp>>1), 200 - tmp); // 秒針の描画色を変化させる
  drawClock();
}
```

注目すべきは以下のあたりです。

　void setupRTC() ： RTCを初期化しています。
　void drawClock(void)：RTCから現在時刻を取得し、アナログ時計を表示しています。

#最後に

LovyanGFXは素晴らしい。らびやんさんに敬礼っ！

以下もご参考まで
　[LILYGO TTGOのT-Wristbandを方位磁針にする](https://qiita.com/poruruba/items/34f194422fec44db18fd)

以上