01.T-Watch S3 plus + LGVL 9で簡易時計表示


01.T-Watch S3 plus + LGVL 9で簡易時計表示

T-Watch S3 が中国の空港でのX線検査で壊れました。朝は動作していて、保安検査前にかばんの中に入れて、出てきて着けようとしたら電源onしません。何も動作しなくなってしまったため、T-Watch S3 Plusを購入しました。427元(約9,600円)でした。バッテリー容量が2倍(470mAh→940mAh)になったのですが、厚みが13→20mmと1.5倍になってしまいました。ハードウェア構成は基本的に同じで、GPSが追加されています。
サンプルスケッチを見るとどれもLVGLを使用しているので、LVGLを使用して時計表示を作ってみました。ただし、時刻合わせはスケッチ内に設定した時刻を使用します。これでも±1分に合わせることができます。

T-Watch S3 Plus製品
https://lilygo.cc/products/t-watch-s3-plus

スケッチ例は、LVGL(Light and Versatile Graphics Library)を使用しています。
https://docs.lvgl.io/master/getting_started/

仕様

S3 Plus vs (S3)
・GPS Position:あり (無し)
・BAT:940mAh (470mAh)
・RTC:PCF8563 (あり?)

S3 Plus と S3 が同じ仕様

・MCU:ESP32-S3
・FLASH:16MB
・PS RAM:8MB
・Platform:Arduino-IDE, ESP-IDF, VS Code, Micropython
・Wi-Fi:802.11 b/g/n
・BLE:V5.0
・画面:1.54inch 240x240 touch
・Microphone:あり
・MAX98357A:スピーカアンプ
・BMA423:3軸加速度センサー
・DRV2605:振動モータ
・AXP2101:電源管理
・ST7789V:LCDコントローラ
・SX1262:LoRa Transceiver

電源のon/off

・電源on : ボタンを2秒間押す。(ボタンは電源ボタンのみ)
・電源off : 6秒間押す
* BOOTボタンはダウンロードモードに入るための内蔵ボタンです。
BOOTボタン位置(写真)
https://github.com/Xinyuan-LilyGO/LilyGoLib/blob/master/docs/lilygo-t-watch-s3.md

ライブラリ

LilyGoLib (TーWatch S3以降の機種)
https://github.com/Xinyuan-LilyGO/LilyGoLib

注意:以下は使用しません。
TTGO_TWatch_Library (T-Watch-2020以前の機種)
https://github.com/Xinyuan-LilyGO/TTGO_TWatch_Library/tree/t-watch-s3

開始方法など

https://wiki.lilygo.cc/get_started/en/Wearable/T-Watch-S3-PLUS/T-Watch-S3-PLUS.html

6.15 消費電力(電流)

電流    タイマー 電源 起動  タッチ バックアップ
(uA)        ボタン ボタン パネル 電源
50 Shutdown  ✖   ✖   ✖   ✖   〇
460 Deep Sleep 〇   ✖   ✖   ✖   ✖
460 Deep Sleep ✖   〇   〇   ✖   ✖
510 Deep Sleep 〇   ✖   ✖   ✖   〇
530 Deep Sleep ✖   〇   〇   ✖   〇
1080 Deep Sleep ✖   ✖   ✖   〇   ?
2380 Light Sleep ✖   〇   〇   〇   ?

7.3.Arduinoのクイックスタート

1.Arduino-IDE をインストール (2.3.7 最新でした)

2.(ボード) esp32 by Espressif Systems 3.0.7 → 3.3.5(2025/12 最新)
* 3.3.0-alpha1(2025/5)以上の事

3.(ライブラリ) LilyGoLib by LilyGo 無し → 0.1.0(最新)

5.(ライブラリ) LilyGoLib-ThirdParty をインストール
https://github.com/Xinyuan-LilyGO/LilyGoLib-ThirdParty

・ESP8266Audio by Earle F. 1.9.9→2.0.0にup(最新は2.4.1)
・TinyGPSPlus by Mikal Hart 1.1.0(済でした)
・lvgl by kisvegabor 9.3.0→9.2.2 (最新は9.4.0) セットVerは下記参照
・RadioLib by Jan Gromes 7.1.0→7.4.0(最新)
・XPowersLib by Lewis He 0.2.6→0.3.1(最新は0.3.2)
・SensorLib by Lewis He 0.2.1→0.3.3(最新)
・IRremoteESP8266 Fork(赤外受信 略)
・ESP32-BLE-Mouse-fork@0.3.1 (Bluetoothマウスとして動作 略)

LilyGoLib-ThirdParty のすべてのディレクトリを ArduinoIDE の libraries ディレクトリにコピー。
(注意: LilyGoLib-ThirdPartyディレクトリ自体をコピーするのではなく、LilyGoLib-ThirdPartyディレクトリ内のフォルダをlibrariesディレクトリにコピー)
最新Verにアップする前に、正常に動作することを確認。問題発生時は戻します。

6.(動作確認用スケッチ) Arduino-IDE > ファイル > スケッチ例 > (カスタムライブラリのスケッチ例) lvgl > Arduino > LVGL_Arduino.ino

7.(ボードの設定) Arduino-IDE > ツール にて
・ボード:esp32 > LilyGo T-Watch-S3
・ポート:使用ポート
以下、規定値です。
・USB CDC On Boot:"Enabled" ←シリアル表示を有効
・CPU Frequency:"240MHZ(WiFi)" ←最速
・Core Debug Level:"None" ←Debugなし
・USB DFU On Boot:"Disable" ←無効
・Erase All Flash Before Sketch Upload:"Disable" ←スケッチup前に全Flash消去を無効
・Events Run On:"Core 1" ←イベント実行コアは1
・JTAG Adapter:"Disable" ←JTAGアダプタ無効
・Arduino Runs On:"Core 1" ←Arduino 実行コアは1
・USB Firmware MSC On Boot:"Disable" ←無効
・Partition Scheme:"16M Flash(3M APP/9.9MB FATFS)" ←パーティション
・Board Revision:"Radio-SX1262" ←ボードリビジョン
・Upload Mode:"UART0/Hardware CDC" ←アップロードモード
・Upload Speed:"921600" ←アップロード速度 最速
・USB Mode:"Hardware CDC and JTAG" ←USBモード

LVGL

Ver

9.2.2とします。(LilyGoLibの動作確認より)
C:\Users\.....\Documents\Arduino\libraries\LilyGoLib\library.json
の35-48行目は
・RadioLib 7.1.2
・lvgl 9.2.2
・XPowersLib 0.2.9
・SensorLib 0.3.1
となっています。よって、LilyGoLibの動作確認は、lvgl 9.2.2 なので、lvglは最新の 9.4 にせず、今のところ 9.2.2にします。

lv_conf.h の作成

C:\Users\.....\Documents\Arduino\libraries\lvgl\lv_conf_template.h
を通常は、

C:\Users\.....\Documents\Arduino\libraries\lv_conf_template.h
にcopyしますが、LilyGoLibを使用している時は、

C:\Users\.....\Documents\Arduino\libraries\LilyGoLib\src\lv_conf_template.h
にcopyして、

lv_conf.h にファイル名を変更します。

ファイル内容の修正

lv_conf.hの
15 #if 0 → 1
に変更(ファイルの内容を有効にする)

30 #define LV_COLOR_DEPTH 16
(パネルで使用されている色深度に合わせますが、よくわからないのでそのままにします)

484-504 (使用するサイズの英数フォントを0→1にします)

510 #define LV_FONT_SIMSUN_16_CJK 0→1
(日本語の一部が表示できる16px fontを使用できるようにします)

522 #define LV_FONT_DEFAULT &lv_font_simsun_16_cjk
(それをデフォルトfontとします)

logはエラーが出るので1にはしませんでした。

LovyanGFX

lovyanGFX(1.2.7 2025/5)では gpio_hal_iomux_func_selでエラーが出て止まってしまいます。

Copilotによると
gpio_hal_iomux_func_sel() は、ESP32の低レベルGPIO設定に使われる関数で、特定のピンに特定の機能を割り当てるために使われていました。ESP-IDF v5.5.1以降では削除または非推奨になっていて、宣言されていない というエラーが出ます。

Espressif/Arduino ESP32のVer 一部略
https://github.com/espressif/arduino-esp32/tags より
・v3.3.5(2025/12) ESP-IDF v5.5.1+に基づく←LovyanGFXを使わずこれにする
・v3.3.3(2025/11) ESP-IDF v5.5.1+に基づく
・v3.3.1(2025/09) ESP-IDF v5.5.1に基づく
・v3.3.0(2025/07) ESP-IDF v5.5.0に基づく
・3.3.0-α1(2025/04) ESP-IDF v5.5、ESP32-C5 ECO1←製品はこれ以上が必要
・v3.2.1(2025/07) ESP-IDF v5.4.2に基づく←LovyanGFXはこれ以下

Arduino ESP32 3.3.0では、LovyanGFX(最新)でエラーが出ます。1つ下げて3.2.1にすると今度はLVGLでエラーが出ます。
開発中のLovyanGFX 1.2.9がESP-IDF v5.5に対応するらしいので、正式版が出た時に確認したいと思います。

動作

・年/月/日(曜日) 時:分を表示 (ただし、スケッチで時刻を設定)
・充電マークを表示(充電中の時)
・バッテリー残量(%)を表示
* 画面で、目標時刻と残り分数は今のところ0に固定です。

スケッチ


// 時計表示 (ただし、設定する時刻はスケッチに記入)
// (ボード) esp32 by Espressif Systems 3.3.5(最新) 3.3.0以上
// lvgl by kisvegabor 9.2.2固定 (LilyGoLib 0.1.0より)
// LovyanGFX 1.2.7(最新)はエラーのため更新待ちで使用せず
// lv_conf.hでデフォルトfont:中日韓16pxの lv_font_simsun_16_cjk
// ツール > ボード > esp32 > LilyGo T-Watch-S3
// 電源部分は、PowerManageMonitor.ino を参照しました。
#include <LilyGoLib.h>           // LilyGoLib 0.1.0(最新)を使用
#include <LV_Helper.h>           // LVGL 9.2.2を使用
#include <time.h>                // 時間を使用
long unsigned int BattInterval;  // バッテリー測定間隔用
int BattPer;                     // バッテリー残量
bool BattCher;                   // 1=充電マーク or " "

lv_obj_t *label11, *label12, *label13, *label14, *label15, *label21, *label22, *label42;  // setupとloopで使用
char youbi[7][4] = { "日", "月", "火", "水", "木", "金", "土" };                          // 曜日配列

void setup() {
  Serial.begin(115200);                // シリアルモニタの通信速度
  instance.begin();                    // LilyGoLibを初期化
  beginLvglHelper(instance);           // LVGLを初期化
  instance.pmu.enableBattDetection();  // 電源機能の有効化
  //instance.rtc.setDateTime(2026, 1, 9, 15, 49, 55);  // 1回目8分後の時刻にRTCセット 2回目1分後にセット その後注釈に

  lv_display_t *disp = lv_display_create(240, 240);  // 指定解像度で新しいディスプレイを作成

  lv_style_t style_48;                                                                      // 基本スタイル
  lv_style_init(&style_48);                                                                 // スタイル初期化
  lv_obj_set_style_bg_color(lv_screen_active(), lv_color_make(0, 0, 0), LV_PART_MAIN);      // rgb 黒地
  lv_obj_set_style_text_color(lv_screen_active(), lv_color_make(0, 255, 0), LV_PART_MAIN);  // rgb 緑字

  // 1行目1 年/月/日( 表示はloop内
  label11 = lv_label_create(lv_screen_active());  // 現在の画面にラベル11を追加
  lv_obj_set_style_text_font(label11,
                             &lv_font_montserrat_24, LV_PART_MAIN);                  // 英数字 24px
  lv_obj_align(label11, LV_ALIGN_TOP_LEFT, 0, 0);                                    // 左上からの位置
  lv_obj_set_style_text_color(label11, lv_color_make(255, 255, 255), LV_PART_MAIN);  // rgb 白字
  lv_obj_set_style_transform_zoom(label11, 240, 0);                                  // ズーム(=/256)倍 384(1.5) 320(1.25) 384

  // 1行目2 曜日 表示はloop内 デフォルトfont
  label12 = lv_label_create(lv_screen_active());                                     // 現在の画面にラベル11を追加
  lv_obj_align(label12, LV_ALIGN_TOP_LEFT, 130, 0);                                  // 左上からの位置
  lv_obj_set_style_text_color(label12, lv_color_make(255, 255, 255), LV_PART_MAIN);  // rgb 白字
  lv_obj_set_style_transform_zoom(label12, 352, 0);                                  // ズーム(=/256)倍 384(1.5) 320(1.25)320

  // 1行目3 ")"
  label13 = lv_label_create(lv_screen_active());                                     // 現在の画面にラベル11を追加
  lv_obj_set_style_text_font(label13, &lv_font_montserrat_24, LV_PART_MAIN);         // 英数字 24px
  lv_obj_align(label13, LV_ALIGN_TOP_LEFT, 156, 0);                                  // 左上からの位置
  lv_obj_set_style_text_color(label13, lv_color_make(255, 255, 255), LV_PART_MAIN);  // rgb 白字
  lv_obj_set_style_transform_zoom(label13, 240, 0);                                  // ズーム(=/256)倍 384(1.5) 320(1.25)
  lv_label_set_text(label13, ")");                                                   // ラベルにセット

  // 1行目4 充電マーク 表示はloop内
  label14 = lv_label_create(lv_screen_active());                                   // 現在の画面にラベル12を追加
  lv_obj_align(label14, LV_ALIGN_TOP_LEFT, 170, 5);                                // 左上からの位置
  lv_obj_set_style_text_color(label14, lv_color_make(0, 255, 255), LV_PART_MAIN);  // rgb 水色字
  lv_obj_set_style_transform_zoom(label14, 240, 0);                                // ズーム(=/256)倍 384(1.5) 320(1.25) 384

  // 1行目5 Batt(%) 表示はloop内
  label15 = lv_label_create(lv_screen_active());                                   // 現在の画面にラベル12を追加
  lv_obj_set_style_text_font(label15, &lv_font_montserrat_24, LV_PART_MAIN);       // 英数字 24px
  lv_obj_align(label15, LV_ALIGN_TOP_LEFT, 182, 0);                                // 左上からの位置
  lv_obj_set_style_text_color(label15, lv_color_make(0, 255, 255), LV_PART_MAIN);  // rgb 水色字
  lv_obj_set_style_transform_zoom(label15, 240, 0);                                // ズーム(=/256)倍 384(1.5) 320(1.25) 384

  // 2行目1 "am" or "pm" 表示はloop内
  label21 = lv_label_create(lv_screen_active());  // 現在の画面にラベル21を追加
  lv_obj_set_style_text_font(label21,
                             &lv_font_montserrat_24, LV_PART_MAIN);  // 英数字 24px
  lv_obj_align(label21, LV_ALIGN_TOP_LEFT, 0, 70 * 0 + 40 + 25);     // 左上からの位置

  // 2行目2 現在時刻(12H) 表示はloop内
  label22 = lv_label_create(lv_screen_active());  // 現在の画面にラベル22を追加
  lv_obj_set_style_text_font(label22,
                             &lv_font_montserrat_48, LV_PART_MAIN);  // 英数字 48px
  lv_obj_align(label22, LV_ALIGN_TOP_LEFT, 60, 70 * 0 + 40);         // 左上からの位置
  lv_obj_set_style_transform_zoom(label22, 320, 0);                  // ズーム(=/256)倍 384(1.5) 320(1.25)

  // 3行目1 "設定" デフォルトfont
  lv_obj_t *label31 = lv_label_create(lv_screen_active());             // 現在の画面にラベル31を追加
  lv_obj_align(label31, LV_ALIGN_TOP_LEFT, 0, 70 * 1 + 40 + 25 - 10);  // 左上からの位置
  lv_obj_set_style_transform_zoom(label31, 448, 0);                    // ズーム(=/256)倍 384(1.5) 320(1.25)320
  lv_label_set_text(label31, "設定");                                  // テキスト

  // 3行目2 設定時刻(24H)
  lv_obj_t *label32 = lv_label_create(lv_screen_active());  // 現在の画面にラベル32を追加
  lv_obj_set_style_text_font(label32,
                             &lv_font_montserrat_48, LV_PART_MAIN);  // 英数字 48px
  lv_obj_align(label32, LV_ALIGN_TOP_LEFT, 60, 70 * 1 + 40);         // 左上からの位置
  lv_obj_set_style_transform_zoom(label32, 320, 0);                  // ズーム(=/256)倍
  lv_label_set_text(label32, "00:00");                               // テキスト 目標時刻

  // 4行目1 "あと" デフォルトfont
  lv_obj_t *label41 = lv_label_create(lv_screen_active());             // 現在の画面にラベル41を追加
  lv_obj_align(label41, LV_ALIGN_TOP_LEFT, 0, 70 * 2 + 40 + 25 - 10);  // 左上からの位置
  lv_obj_set_style_transform_zoom(label41, 448, 0);                    // ズーム(=/256)倍 384(1.5) 320(1.25)320
  lv_label_set_text(label41, "あと");                                  // テキスト

  // 4行目2 残りの分数
  label42 = lv_label_create(lv_screen_active());  // 現在の画面にラベル42を追加
  lv_obj_set_style_text_font(label42,
                             &lv_font_montserrat_48, LV_PART_MAIN);  // 英数字 48px
  lv_obj_align(label42, LV_ALIGN_TOP_RIGHT, -60, 70 * 2 + 40);       // 左上からの位置
  lv_obj_set_style_transform_zoom(label42, 320, 0);                  // ズーム4(=/256)倍
  lv_label_set_text(label42, "0");                                   // ラベルのテキスト 残り時間

  // 4行目3 "分" デフォルトfont
  lv_obj_t *label43 = lv_label_create(lv_screen_active());     // 現在の画面にラベル43を追加
  lv_obj_align(label43, LV_ALIGN_TOP_LEFT, 200, 70 * 2 + 40);  // 左上からの位置
  lv_obj_set_style_transform_zoom(label43, 768, 0);            // ズーム(=/256)倍 384(1.5) 320(1.25)
  lv_label_set_text(label43, "分");                            // ラベル1のテキスト 現在時刻

  instance.setBrightness(100);  // 明るさ off(0)-max(255)
}

void loop() {
  char buf[64], buf2[64];
  if (BattInterval < millis()) {                                 // 時間を過ぎたら
    struct tm timeinfo;                                          // 時刻を取得するCライブラリ構造体
    instance.rtc.getDateTime(&timeinfo);                         // RTCから読み込む
    size_t written = strftime(buf, 64, "%Y/%m/%d(", &timeinfo);  // 年/月/日(
    lv_label_set_text(label11, buf);                             // ラベルにセット

    written = strftime(buf2, 64, "%w", &timeinfo);  // 曜日の数 日(0)-土(6)
    sprintf(buf, "%s", youbi[atoi(buf2)]);          // 曜日
    lv_label_set_text(label12, buf);                // ラベルにセット

    written = strftime(buf, 64, "%P", &timeinfo);  // am or pm
    lv_label_set_text(label21, buf);               // ラベルにセット

    written = strftime(buf, 64, "%l:%M", &timeinfo);  // 時:分
    lv_label_set_text(label22, buf);                  // ラベルにセット

    BattCher = instance.pmu.isCharging();  // 充電中か測定
    lv_label_set_text_fmt(label14, "%s",
                          BattCher ? LV_SYMBOL_CHARGE : "  ");  // "充電マーク" or " "

    BattPer = instance.pmu.getBatteryPercent();  // バッテリー残量(%)測定
    if (BattPer < 0) BattPer = 0;                // バッテリ残量最低値は0%
    if (BattPer > 100) BattPer = 100;            // バッテリ残量最高値は100%
    String s = "   " + String(BattPer);          // 左に空白をつなげる
    s = s.substring(s.length() - 3);             // 右3文字をとる
    lv_label_set_text_fmt(label15, "%s%%", s);   //  %

    BattInterval = millis() + 2000;  // 2秒ごとに測定
  }
  lv_timer_handler();  // lvglタスク処理は、ループ内に書く
  delay(2);            // 2mS待つ
}
* flash memory(3.1Mbyte)のうち、スケッチが34%使用。RAM(327kbyte)のうち、global変数が7%使用、local変数で301kbyte使用可能。(1000byte=1kbyteで計算)

以下のVerを使用中
LilyGoLib 0.1.0
FFat 3.3.5
FS 3.3.5
Wire 3.3.5
SensorLib 0.3.3
SPI 3.3.5
RadioLib 7.1.0
TinyGPSPlus 1.1.0
ESP_I2S 3.3.5
XPowersLib 0.3.1
lvgl 9.2.2
esptool 5.1.0