08.T-Watch S3 plusで時計まとめ


08.T-Watch S3 plus で時計 まとめ

T-Watch S3 plusで、今までの時計のまとめです。01~02と04~06を集計して、アナログ時計も追加しました。このプログラムは、plus無しの機種でもそのまま動作します。また、今回から今までの Arduino-IDE から、VScode + PlatformIO IDE に移転しましたが、新機種の対応が遅れているので VScode + pioarduino IDE にしました。

技適マーク

2026年4月に336元(約7700円)で購入したT-Watch S3 の時計内側の蓋にシールが貼ってありました。そこに技適マーク(丸に波の下に郵便マーク)があったので、無線が日本の電波法に基づく技術基準に適合しています。

使用前

使用していると、汗で剥がれてきました。

技術基準適合証明等を受けた機器の検索
https://www.tele.soumu.go.jp/giteki/SearchServlet?pageID=jk01&NUM_TYPE=1&NUM=211-240808&NAM=&FOM=&PC=&YAR_FROM=&MON_FROM=&DAY_FROM=&YAR_TO=&MON_TO=&DAY_TO=&RAD=00-00-00-00&TEC=1&TEC=2&TEC=3&TEC=4&TEC=5&TEC=6&TEC=7&SC=1#searchlist
にて、2件該当しました。

1件目. 特定無線設備の種別: 第2条第8号に規定する特定無線設備
電波の型式、周波数及び空中線電力: F1D 923MHz 2.70mW
* LoRaの事です。

2件目. 特定無線設備の種別: 第2条第19号に規定する特定無線設備
電波の型式、周波数及び空中線電力:
F1D 2402~2480MHz(2000kHz間隔40波) 4mW
G1D 2412~2472MHz(5MHz間隔13波) 0.4mW/MHz
G1D,D1D 2412~2472MHz(5MHz間隔13波) 0.3mW/MHz
G1D,D1D 2422~2462MHz(5MHz間隔9波) 0.3mW/MHz
* wifiの事です。

2件の共通内容は、
証明機関等種別: 相互承認(MRA)
証明(認証)種別: 工事設計認証
名称: Shenzhen Xin Yuan Electronic Technology Co., Ltd
型式: T-WATCH S3
番号: 211-240808
年月日: 令和7年1月2日
スプリアス規定: 新規定
周波数等の維持機能: 無
BODYSAR: ―
機関名: Bay Area Compliance Laboratories Corp (アメリカ)

* T-Watch S3では、923MHzの LoRa と2.4GHzのb/g/nのwifi、BLE V5.0が使えます。

LoRa

LoRa と LoRaWAN

・LoRa: 低消費電力・長距離通信を実現する無線技術の一つで、数km先まで届きます。
・LoRaWAN: LoRaを使ったネットワークの通信規格

LoRaの特性 (一般)

・見通しで数kmの通信が可能です。
・μA〜mA単位の低消費電流です。
・デバイス同士で直接通信が可能です。送信に最適化で、双方向の頻繁なやり取りには不向きです。
・通信速度は数百bps〜数kbpsと低速です。
・送信前に5mS秒以上その周波数が使われていないかセンスし、使われていなければ4秒送信。再送信は50mS待ちます。
・1回に送れるのは255バイト(半角英数字で255文字)以下です。通話や画像送信には不向きです。よって、トランシーバの代わりにはなりません。
・標準は平文で送信されるため、暗号化が必要です。

LoRaWANの周波数

・中国: 470-510MHz等
・日本: 920-928MHz

日本の出力

920MHz帯で許可されている無線局は、どちらも免許不要で、
・出力20mW以下: 登録不要 特定小電力無線局 ←こちら
・出力250mW以下: 登録必要 簡易無線局

T-Watch S3のLoRa周波数オプション

https://lilygo.cc/products/t-watch-s3
以下の5機種あります。
・433MHz: 欧州・アジア・アフリカ等の多くの国
・868MHz: 欧州
・915MHz: 米国
・920MHz: 日本 (購入品は923MHzで、36chを使用)
・2.4GHz: 主に欧州・北米

PlatformIO

LilyGoLib-PlatformIO
https://github.com/Xinyuan-LilyGO/LilyGoLib-PlatformIO
からダウンロードし、解凍しておきます。
PlatformIOは新しいESP32を完全にはサポートしていないため、回避策として提供されています。

Platformio IDE クイックスタート

1. VS CodeとPythonをインストール
VScode 1.137 (2026/10/9)は、インストール済です。
Pythonは、PlatformIOが内部で使います。公式サイトからダウンロードします。

Python 3.14.7 (最新)
https://www.python.org/downloads/release/python-3147/

2. VS Code左側の拡張機能アイコン(四角が4つ並んだマーク)を開き、"PlatformIO"で検索してインストール。インストール後はVS Codeを再起動。(PlatformIO IDE 3.3.4)

3. プロジェクトフォルダを開く
VS Code > ファイル > フォルダーを開く > LilyGoLib-PlatformIO フォルダーを選択

4. 依存ライブラリのダウンロード
フォルダを開くと自動的に必要なライブラリのダウンロードが始まります。画面下部にプログレスバーが出るので、完了するまで待ちます(初回は数分かかることがあります)。

pioarduino

PlatformIOのespressif32パッケージはコア3.xの対応が遅れているため、pioarduino を使うのが現状の回避策です。

pioarduino
https://github.com/pioarduino/platform-espressif32/releases/
にて、最新版は、55.03.311となっているので、platformio.iniの該当箇所を変更します。

構成ファイル (一部)

patch_blemouse.py、patch_lv_conf.py、platformio.ini は、Claude by Anthropic Freeプラン (Sonnet 5 中)の回答です。(下記参照)

TWatchS3_07/ ← 自分で決めたプロジェクトフォルダ
 ├ lib/
 │ ├ libhelix-mp3 ← (1)
 │ ├ LilyGoLib/ ← (2) LilyGo
 │ ├ lvgl/ ← (2) グラフィック
 │ ├ RadioLib/ ← (2) LoRa
 │ ├ SensorLib/ ← (2) センサー
 │ ├ TinyGPSPlus/ ← (2) GPS
 │ └ XPPowersLib/ ← (2) 電源
 ├ src/main.cpp ← (3)
 ├ support/
 │ ├ sdl2_paths.py ← (4)
 │ ├ sdl2_build_extra.py ← (4)
 │ ├ patch_blemouse.py ← (5) バグのBleMouse.cppを修正
 │ └ patch_lv_conf.py ← (5) lv_conf.hの読込不具合対策
 └ platformio.ini ← (6) (5)x2等を組込む

(1)x1 今の所いらないので削除
(2)x6 C:\Users\.....\Documents\Arduino\libraries\ からcopy
(3)x1 自分で作成したプログラム 今までの *.ino相当
(4)x2 そのまま
(5)x2 追加 下記ファイル参照
(6)x1 変更 下記ファイル参照

(参考) 使用基板の選択
エクスプローラー > platformio.ini > [platformio]セクションのdefault_envsの中から使うボード名(twatchs3)の行頭の";"を消します。

(参考) デフォルトのコンパイルスケッチは src\main.cpp です。LilyGoLibで例をコンパイルする必要がある場合は、1行src_dir = examples/xxxxxのコメントを解除して有効化します。1行だけが有効であること。

(参考) 日本語を使うため、LilyGoLib/lv_conf.h の中は、
#define LV_FONT_SIMSUN_16_CJK 1
に設定してください。

コンパイルとアップロード

・VS Code左下のツールバーにある"✔"をクリックしてコンパイルします。
・USBケーブルでPCに接続します。
・"→"をクリックしてファームウェアをアップロードします。
 (注意) 今回のプログラムは、無操作2分で電源がoffします。更新でアップロード中は、時々画面操作をします。
・"出力"をクリックしてシリアル出力を監視します。

プロジェクト追加の場合

同じ機種で、新しくmain.cppを作る場合は、
VS Codeを閉じる > プロジェクトフォルダごとコピーしフォルダ名を変える > .pioフォルダ(キャッシュ等)を削除 > src/main.cppを書換える > VS Codeにて File → Open Folderで、コピーした新しいフォルダを開く
srcの1つ上のフォルダ名がプロジェクト名になります。また、1プロジェクトが1GB以上あります。

説明

5つのタイルが動作します。スクロールで移動します。
┌────┐ ┌────┐ ┌────┐
│ 11  │←│ 12  │→│ 13  │
│アラーム│ │デジタル│ │明るさの│
│時刻設定│→│時計表示│←│ 設定 │
└────┘ └────┘ └────┘
         ↓↑
┌────┐ ┌────┐ ┌────┐
│ 21  │←│ 22  │→│ 23  │
│(未設定)│ │手動で │ │アナログ│
│    │→│時刻設定│←│時計表示│
└────┘ └────┘ └────┘

起動は電源ボタン1秒です。起動時間は約6秒で、表示2分後に点滅してoffします。
deepsleepにはせず、持続動作時間を優先にしています。

12.デジタル時計

・起動時に開くタイルです。
・2秒間隔で時刻とBT残量を更新しているので、反応時間は最大2秒です。
・曜日表示は、
 (日0) = 日曜日・星期日,
 (月1) = 月曜日・星期一
  .....
 (土6) = 土曜日・星期六
です。
・現在時刻は12時間表示で、アラーム時刻は24時間表示です。
・タイル11で設定した時刻と、それまでの分数を表示します。

11.アラーム時刻

・アラーム時刻を設定します。デジタル時計に残り分数とともに表示します。
・現在の所、翌日アラームは動作しません。

13.画面の明るさの設定

・バックライトの明るさ(1~255)を設定します。起動時は100です。
・255(max)から1(点灯でmin)までの6つの輝度ボタンの中から選びます。

22.手動で時刻設定

・時刻を手動で設定します。
・現時刻の年は、2026に固定で変更できません。

23.アナログ時計

・AIに作成依頼しました。

21.開発中


スケッチ


// 時計 v1.0
// 次回検討 翌日アラーム タッチ振動
// deepsleep アラーム時刻10分前と5分前に振動 時報

// 起動時の画面は(12)で、横3x縦2の6画面タイル
// (11) アラーム時刻設定 (12) デジタル時計表示 (13) 明るさの設定
// (21) まだ       (22) 手動で時刻設定  (23) アナログ時計表示

// (ボード) esp32 by Espressif Systems 3.3.11 (3.3.0以上)
// 4.0.0alpha1はあるが入れてない
// lv_conf.hでデフォルトfont:中日韓16pxのlv_font_simsun_16_cjk を使用
// 英数字のサイズ変更は、フォントを変更
// 48:D時計の時刻 28:ローラーの選択結果 24:いろいろ 16:A時計の文字盤で小
// 現時刻の年は、2026に固定。2秒ごとに時刻とBT残量を更新します
#include <LV_Helper.h> // lvgl by kisvegabor 9.5.0
#include <LilyGoLib.h>   // LilyGoLib 0.2.0
#include <Preferences.h> // 不揮発性メモリNVSを使用 電源断でもアラーム時刻を覚える
#include <freertos/FreeRTOS.h>       // リアルタイムオペレーティングシステム
#include <freertos/task.h>           // タスク管理
#include <math.h>                    // アナログ時計で三角関数を使用
#include <time.h>                    // 時間を使用
Preferences prefs;                   // Preferencesオブジェクト
SET_LOOP_TASK_STACK_SIZE(16 * 1024); // スタックサイズを16〜32KBに増やす 標準8kB
// (スワイプ時に大きなスタックサイズが必要 重いタイルがあると超える)

// 変数
int BattPer;                    // バッテリー残量0-100(%)
bool BattCher;                  // 1:稲妻充電マーク, 0:" "
const char *bright_buf = "100"; // 明るさ変数 0(off), 1-255

lv_obj_t *label, *set_label, *cxl_label,
    *time_label; // ラベル,セット,キャンセル,時間表示
lv_obj_t *label11, *label12, *label14, *label15; // デジタル時計用 1行目
lv_obj_t *label21, *label22, *label32, *label42; // デジタル時計用 2-4行目
lv_obj_t *btn, *set_btn, *cxl_btn;               // ボタン,セット,キャンセル
lv_obj_t *day_roller, *hour_roller, *min_roller; // 日,時,分のローラー
lv_obj_t *tv;                                    // タイル

static lv_subject_t day_subject3;    // 日 アラーム(今日 or 明日)
static lv_subject_t hour_subject3;   // 時
static lv_subject_t minute_subject3; // 分
static lv_subject_t time_subject3;   // 上のsubjectが入るグループ

static lv_subject_t
    month_subject; // 月 値が変わると、登録コールバックが呼ばれます
static lv_subject_t day_subject;    // 日 現在時刻
static lv_subject_t hour_subject;   // 時
static lv_subject_t minute_subject; // 分
static lv_subject_t time_subject;   // 上のsubjectが入るグループ

static lv_subject_t *time_group_array_subject3[] = {
    &day_subject3, &hour_subject3,
    &minute_subject3}; // グループにする アラーム時刻
static lv_subject_t *time_group_array_subject4[] = {
    &month_subject, &day_subject, &hour_subject,
    &minute_subject}; // グループにする 現時刻
const char *month_options = "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12"; // 月 1-12
const char *day_options02 = "0\n1"; // 日 0:今日, 1:翌日
const char *day_options31 =         // 日 1-31
    "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21"
    "\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31";
const char *hour_options = // 時 0-23
    "0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n"
    "21\n22\n23";
const char *minute_options = // 分 0-59
    "0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n"
    "21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39"
    "\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n52\n53\n54\n55\n56\n57\n"
    "58\n59";
int32_t month4, day4, hour4, minute4, day3, hour3,
    minute3; // 現在時刻, アラーム時刻の表示用
int32_t nowMonth, nowDay, nowHour, nowMinute,
    setDay; // set_btnで引き渡す現在の月&日&時&分,アラームの日&時&分
int32_t setHour = 0, setMinute = 0; // 復帰後も保持
char youbi[7][5] = {"日0", "月1", "火2", "水3",
                    "木4", "金5", "土6"}; // 曜日配列
static const char *btnm_map[] = {
    "255", "200", "150", "\n", // 1行目 2行3列にする
    "100", "50",  "1",   ""};  // ボタンmap 最後に""が必要

int16_t dispX, dispY;   // 機種ごとのXYサイズ
char buf[64], buf2[64]; // 文字操作用
bool firstDisp = true;  // 画面が起動時状態

// (23.アナログ時計) 変数
static lv_obj_t *tileM23 = nullptr;
static lv_obj_t *clock_scale = nullptr; // 文字盤(分目盛りリング)
static lv_obj_t *hand_hour_main = nullptr;
static lv_obj_t *hand_hour_tail = nullptr;
static lv_obj_t *hand_min_main = nullptr;
static lv_obj_t *hand_min_tail = nullptr;
static lv_obj_t *hand_sec_main = nullptr;
static lv_obj_t *hand_sec_tail = nullptr;
static lv_obj_t *clock_center = nullptr; // 中心のキャップ
static lv_timer_t *clock_timer = nullptr;

static lv_coord_t clock_cx = 0,
                  clock_cy = 0; // 文字盤(clock_scale)ローカル座標での中心
static float clock_radius = 0;
// 変数設定終わり



// 関数
// ==============================
// キャンセル(戻る)ボタン 各画面でcxl_btnを押したら時計画面へ
// ==============================
static void cxl_clicked_event_cb(lv_event_t *e) {
  lv_tileview_set_tile_by_index(tv, 1, 0,
                                LV_ANIM_ON); // 時計タイルへ戻る 移動アニメon
}

// ==============================
// 11.アラーム設定
// ==============================
// (11.アラーム設定) 3ローラーの値をlabelに表示
static void time_observer_cb3(lv_observer_t *observer, lv_subject_t *subject) {
  day3 = lv_subject_get_int(lv_subject_get_group_element(subject, 0));
  hour3 = lv_subject_get_int(lv_subject_get_group_element(subject, 1));
  minute3 = lv_subject_get_int(lv_subject_get_group_element(subject, 2));
  // lv_subject_get_int(
  // )は、整数Subjectの現在の値を取得(Subjectへのpointer)、戻り値:現在の値
  // lv_subject_get_group_element( )は、
  // Subject
  // Groupのリストから要素を取得(Group型のSubjectへのpointer,取得する要素のインデックス)
  // 戻り値:リスト内のインデックス付Subjectへのpointer
  // インデックスが範囲外の場合はNULL

  lv_obj_t *label = (lv_obj_t *)lv_observer_get_target(observer);
  // Observerのターゲットを取得(Observerへのpointer)、戻り値:保存されたターゲットへのpointer
  lv_label_set_text_fmt(label,
                        "Alam\n  %" LV_PRId32 "+\n%" LV_PRId32 ":%02" LV_PRId32,
                        day3, hour3, minute3); // ローラーの値
}

// (11.アラーム設定) set_btnを押したら呼出される関数
static void set_btn_clicked_event_cb3(lv_event_t *e) {
  // set_btn = lv_event_get_target_obj(e);
  //  イベントの元のターゲットobjを取得(イベント記述子へのpointer)、戻り値:event_codeの元のターゲットへのpointer
  // Serial.printf("Alam_set_btn %1d+ %2d:%02d%n", setDay, setHour, setMinute);
  // シリアルモニタに日時分を表示

  setHour = hour3; // アラーム時
  if (day3 > 0)
    setHour = setHour + 24; // 翌日を考慮
  setMinute = minute3;      // アラーム分

  prefs.begin("myApp", false);      // (開く名前空間, 書込モード)
  prefs.putInt("setHour", setHour); // 値を保存(キーは文字列、値) アラームの時
  prefs.putInt("setMinute",
               setMinute); // 値を保存(キーは文字列、値) アラームの分
  // setHour = prefs.getInt("setHour");
  prefs.end();                                         // NVSを閉じる
  lv_tileview_set_tile_by_index(tv, 1, 0, LV_ANIM_ON); // タイル位置移動
  // アクティブタイル設定(tileviewのポインタ,タイルのカラム,行,アニメLV_ANIM_ON
  // or LV_ANIM)
}

// ==============================
// 12.D時計
// ==============================
// (12.時計) 時計画面loop表示
void tileM12_update() {
  struct tm timeinfo; // 現在時刻を取得する構造体 tm
  instance.rtc.getDateTime(&timeinfo); // RTCをtimeinfoに格納

  size_t written = strftime(buf, 64, "'%y/%m/%d(", &timeinfo);// 西暦下2桁/月/日(
  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/pm
  lv_label_set_text(label21, buf);// am/pm表示

  written = strftime(buf, 64, "%l:%M", &timeinfo); // 時(1-12を空白埋め):分
  lv_label_set_text(label22, buf); // 時:分 表示

  sprintf(buf, "%2d:%02d", setHour, setMinute); // アラーム時:分
  lv_label_set_text(label32, buf); // アラーム時:分 表示

  int32_t ato =
      (setHour * 60 + setMinute) - (timeinfo.tm_hour * 60 + timeinfo.tm_min); // アラームまでの残り分
  if (ato < -999) // 999分以上過ぎていたら
    ato = 0; // 0にする
  sprintf(buf, "%d", ato); // 残りの分
  lv_label_set_text(label42, buf); // 残りの分 表示

  BattCher = instance.pmu.isCharging(); // 充電中かどうかを取得
  lv_label_set_text_fmt(label14, "%s", BattCher ? LV_SYMBOL_CHARGE : "  "); // 充電中なら稲妻マーク, そうでなければ空白 

  BattPer = instance.pmu.getBatteryPercent(); // バッテリー残量を取得
  if (BattPer < 0) // 0%未満なら0
    BattPer = 0;
  if (BattPer > 100) // 100%を超えていたら100
    BattPer = 100;
  if (BattPer < 30) { // 30%未満のなら赤色
    lv_obj_set_style_text_color(label15, lv_color_make(255, 0, 0),
                                LV_PART_MAIN);
  } else if (BattPer < 50) { // 50%未満なら黄色
    lv_obj_set_style_text_color(label15, lv_color_make(255, 255, 0),
                                LV_PART_MAIN);
  }

  String s = "   " + String(BattPer); // 文字列にして前にスペースを付ける
  s = s.substring(s.length() - 3); // 後ろ3桁をとる
  lv_label_set_text_fmt(label15, "%s%%", s); // バッテリー残量表示
  lv_obj_set_style_text_color(label15, lv_color_make(0, 255, 255),
                              LV_PART_MAIN); // 水色
}

// (12.D時計) 時計表示を2秒タイマーに登録
void status_update_timer_cb(lv_timer_t *timer) {
  struct tm timeinfo; // 現在時刻を取得する構造体 tm
  instance.rtc.getDateTime(&timeinfo); // RTCをtimeinfoに格納

  size_t written =
      strftime(buf, 64, "'%y/%m/%d(", &timeinfo); // 西暦下2桁/月/日(
  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/pm
  lv_label_set_text(label21, buf);

  written = strftime(buf, 64, "%l:%M", &timeinfo); // 時 1-12 空白埋め:分
  lv_label_set_text(label22, buf);

  sprintf(buf, "%2d:%02d", setHour, setMinute); // アラーム時:分
  lv_label_set_text(label32, buf);

  int32_t ato =
      (setHour * 60 + setMinute) - (timeinfo.tm_hour * 60 + timeinfo.tm_min);
  if (ato < -999)
    ato = 0;
  sprintf(buf, "%d", ato); // 残りの分
  lv_label_set_text(label42, buf);

  BattCher = instance.pmu.isCharging();
  lv_label_set_text_fmt(label14, "%s", BattCher ? LV_SYMBOL_CHARGE : "  ");

  BattPer = instance.pmu.getBatteryPercent();
  if (BattPer < 0)
    BattPer = 0;
  if (BattPer > 100)
    BattPer = 100;
  if (BattPer < 30) {
    lv_obj_set_style_text_color(label15, lv_color_make(255, 0, 0),
                                LV_PART_MAIN);
  } else if (BattPer < 50) {
    lv_obj_set_style_text_color(label15, lv_color_make(255, 255, 0),
                                LV_PART_MAIN);
  }

  String s = "   " + String(BattPer);
  s = s.substring(s.length() - 3);
  lv_label_set_text_fmt(label15, "%s%%", s);
  lv_obj_set_style_text_color(label15, lv_color_make(0, 255, 255),
                              LV_PART_MAIN);
}

// ==============================
// 13.明るさ設定
// ==============================
// (13.明るさ設定) 明るさ設定btnが押された時
static void event_handler(lv_event_t *e) {
  lv_event_code_t code = lv_event_get_code(e);
  // イベントのコードを取得(イベント記述子へのpointer)
  // 戻り値:イベントコード(例 LV_EVENT_CLICKED, LV_EVENT_FOCUSED等)
  lv_obj_t *obj1 = lv_event_get_target_obj(e);
  // イベントの元のターゲットobjを取得(イベント記述子へのpointer)
  // 戻り値:event_codeの元のターゲットへのpointer
  if (code ==
      LV_EVENT_VALUE_CHANGED) { // 特別イベントウィジェットの値が変更されれば
    uint32_t id = lv_buttonmatrix_get_selected_button(obj1);
    // 最後に押した・離した・フォーカス等アクティブにしたbtnのインデックス
    // (btnマトリックスobjへのpointer)
    // 戻り値:最後に離されたbtnのインデックス
    bright_buf = lv_buttonmatrix_get_button_text(obj1, id);
    // btnのtextを取得(btnのマトリックスobjへのpointer,改行文字を含まないbtnのインデックス)
    // 戻り値:btn_indexボタンのtext btnのtextが明るさの変数になる
    instance.setBrightness(strtol(bright_buf, NULL, 10)); // 明るさ設定 char→int
    // strtol()は、文字列中の数値部分をlongに変換(変換対象の入力文字列を指すポインタ,
    // 変換が完了した位置へのポインタを格納するための引数 不要はNULL,
    // 10進数なら10 16進数なら16)
  }
}

// ==============================
// 22.現時刻修正
// ==============================
// (22.現時刻修正) 4ローラーの値をlabelに表示
static void time_observer_cb4(lv_observer_t *observer, lv_subject_t *subject) {
  month4 = lv_subject_get_int(lv_subject_get_group_element(subject, 0)) + 1;
  day4 = lv_subject_get_int(lv_subject_get_group_element(subject, 1)) + 1;
  hour4 = lv_subject_get_int(lv_subject_get_group_element(subject, 2));
  minute4 = lv_subject_get_int(
      lv_subject_get_group_element(subject, 3)); // 

  lv_obj_t *label =
      (lv_obj_t *)lv_observer_get_target(observer); // 
  lv_label_set_text_fmt(
      label,
      "  %" LV_PRId32 "  /  %" LV_PRId32 "   %" LV_PRId32 " : %02" LV_PRId32,
      month4, day4, hour4,
      minute4); // ローラーの値
  // Serial.printf("2026/%d/%d %d : %02d", month4, day4, hour4, minute4); //
  // パニックエラーになる
}

// (22.現時刻修正) set_btnで呼出される関数
static void set_btn_clicked_event_cb4(lv_event_t *e) {
  nowMonth = month4; // 
  nowDay = day4;
  nowHour = hour4;
  nowMinute = minute4;
  // Serial.printf("now_set_btn 2026/%d/%d %2d:%02d%n", nowMonth, nowDay,
  // nowHour, nowMinute);  // シリアルモニタに年/月/日 時:分 を表示
  instance.rtc.setDateTime(2026, nowMonth, nowDay, nowHour, nowMinute,
                           30);                        // RTCセット
  lv_tileview_set_tile_by_index(tv, 1, 0, LV_ANIM_ON); // タイル位置移動
  // アクティブタイル設定(tileviewのポインタ,タイルのカラム,行,アニメLV_ANIM_ON
  // or LV_ANIM
}

// ==============================
// 23.アナログ時計の関数
// ==============================
// (23.AA時計) 角度→座標変換 インデックス配置用
// angle_deg:0→12時 時計回りに増加, len:中心からのpx
// out_x/out_y:計算結果(clock_scaleローカル座標)
// create_analog_clock_tileM23から呼ばれる
static void angle_to_point(float angle_deg, float len, lv_coord_t *out_x,
                           lv_coord_t *out_y) {
  float rad = (angle_deg - 90.0f) * (float)M_PI / 180.0f;
  *out_x = (lv_coord_t)lroundf(clock_cx + len * cosf(rad));
  *out_y = (lv_coord_t)lroundf(clock_cy + len * sinf(rad));
}

// (23.A時計) 針セグメント生成ヘルパー
// 中心(clock_cx, clock_cy)を回転ピボットとする細長い角丸バーを作る。
// is_tail = false : 中心から「先端方向(上)」へ長さlenだけ伸びるセグメント
// is_tail = true  : 中心から「反対方向(尾)」へ長さlenだけ伸びるセグメント
// 回転角(transform_rotation)は0=真上(12時)、時計回りに正の値。
static lv_obj_t *create_hand_segment(lv_obj_t *parent, lv_coord_t width,
                                     lv_coord_t len, bool is_tail,
                                     lv_color_t color, lv_coord_t radius) {
  lv_obj_t *seg = lv_obj_create(parent); // ベースオブジェクト(長方形)を作成
  lv_obj_remove_style_all(seg);          // 全スタイル削除()
  lv_obj_set_size(seg, width, len);      // サイズ(,幅,高さ)
  lv_obj_set_style_bg_opa(seg, LV_OPA_COVER, 0);
  lv_obj_set_style_bg_color(seg, color, 0); // 背景色(,色,セレクタ)
  lv_obj_set_style_radius(seg, radius, 0);  // 角丸(,半径,セレクタ)
  lv_obj_remove_flag(seg,
                     LV_OBJ_FLAG_CLICKABLE); // フラグを削除(,クリックできる)
  lv_obj_remove_flag(seg,
                     LV_OBJ_FLAG_SCROLLABLE); // フラグを削除(,スクロールできる)
  if (!is_tail) { // 下端(高さlenの位置)が中心のピボット???
    lv_obj_set_pos(seg, clock_cx - width / 2, clock_cy - len); // 位置(,x,y)
    lv_obj_set_style_transform_pivot_x(seg, width / 2, 0);
    lv_obj_set_style_transform_pivot_y(seg, len, 0);
  } else { // 上端(高さ0の位置)が中心のピボット???
    lv_obj_set_pos(seg, clock_cx - width / 2, clock_cy);
    lv_obj_set_style_transform_pivot_x(seg, width / 2, 0);
    lv_obj_set_style_transform_pivot_y(seg, 0, 0);
  }
  return seg;
}

// (23.A時計) 針の更新タイマー
// 30ms周期(約33fps)で呼ばれ、マイクロ秒精度で角度を計算するため
// 3針がなめらかに動く
// 各針は transform_rotation の値を更新するだけなので処理は軽い。
static void clock_update_timer_cb(lv_timer_t *timer) {
  LV_UNUSED(timer);

  struct timeval tv;
  gettimeofday(&tv, nullptr);

  struct tm tminfo;
  localtime_r(&tv.tv_sec, &tminfo);

  float sub_sec = tv.tv_usec / 1000000.0f; // 0.0 - 0.999999(同じ瞬間の端数)

  float sec_f = tminfo.tm_sec + sub_sec; // 0.0 - 59.999999
  float min_f =
      tminfo.tm_min + sec_f / 60.0f; // 秒の端数を分に反映→分針も滑らかに
  float hour_f = (tminfo.tm_hour % 12) +
                 min_f / 60.0f; // 分の端数を時に反映→時針も滑らかに

  float sec_angle = sec_f / 60.0f * 360.0f;   // 秒の角度 1周360度60秒
  float min_angle = min_f / 60.0f * 360.0f;   // 分針の角度
  float hour_angle = hour_f / 12.0f * 360.0f; // 時針の角度

  int32_t sec_rot = (int32_t)lroundf(sec_angle * 10.0f); // 0.1度単位
  int32_t min_rot = (int32_t)lroundf(min_angle * 10.0f);
  int32_t hour_rot = (int32_t)lroundf(hour_angle * 10.0f);

  lv_obj_set_style_transform_rotation(hand_sec_main, sec_rot, 0);
  lv_obj_set_style_transform_rotation(hand_sec_tail, sec_rot, 0);
  lv_obj_set_style_transform_rotation(hand_min_main, min_rot, 0);
  lv_obj_set_style_transform_rotation(hand_min_tail, min_rot, 0);
  lv_obj_set_style_transform_rotation(hand_hour_main, hour_rot, 0);
  lv_obj_set_style_transform_rotation(hand_hour_tail, hour_rot, 0);
}

// (23.A時計) tileM23で使用
void create_analog_clock_tileM23(lv_obj_t *tileM23) {
  lv_coord_t tile_w = lv_obj_get_width(tileM23);  // tileの幅
  lv_coord_t tile_h = lv_obj_get_height(tileM23); // tileの高さ
  lv_coord_t outer_d =
      LV_MIN(tile_w, tile_h) - 10; // 小さい方から10小さく(文字盤の半径)

  // タイル全体の背景(円の外側の四隅)
  lv_obj_set_style_bg_color(tileM23, lv_color_hex(0x0B1F3A), 0); // 紺色
  lv_obj_set_style_bg_opa(tileM23, LV_OPA_COVER,
                          0); // 背景(,不透明255,デフォルト状態)

  // 外側ベゼル
  lv_obj_t *face_bg =
      lv_obj_create(tileM23);       // ベースオブジェクト(長方形)を作成
  lv_obj_remove_style_all(face_bg); // その全スタイルを削除
  lv_obj_set_size(face_bg, outer_d, outer_d);            // サイズ設定
  lv_obj_center(face_bg);                                // 親の中心に合わせる
  lv_obj_set_style_radius(face_bg, LV_RADIUS_CIRCLE, 0); // 角丸半径
  lv_obj_set_style_bg_opa(face_bg, LV_OPA_COVER,
                          0); // 背景の不透明度 0:透明 255:不透明
  lv_obj_set_style_bg_color(face_bg, lv_color_hex(0x0A0A0A),
                            0);                  // 内側は黒文字盤
  lv_obj_set_style_border_width(face_bg, 12, 0); // 太いブロンズ枠=ベゼル
  lv_obj_set_style_border_color(face_bg, lv_color_hex(0x8A6A2A), 0);
  lv_obj_remove_flag(
      face_bg,
      LV_OBJ_FLAG_CLICKABLE); // フラグ削除(,入力デバイスによるクリック可能)
  lv_obj_remove_flag(face_bg,
                     LV_OBJ_FLAG_SCROLLABLE); // フラグ削除(,スクロール可能)

  // 外周の分数字リング(5,10,15...60) — 装飾用、回転しない固定表示
  static const char *minute_labels[12] = {"60", "5",  "10", "15", "20", "25",
                                          "30", "35", "40", "45", "50", "55"};
  
  // 内側アクセントリング(明るいゴールドの細線)
  lv_coord_t accent_d = outer_d - 14;
  lv_obj_t *accent_ring = lv_obj_create(tileM23);

  lv_obj_remove_style_all(accent_ring);

  // 文字盤: 分目盛りリング(61本指定で60本ぶんを均等配置)
  lv_coord_t scale_d = accent_d - 10;
  clock_scale = lv_scale_create(tileM23);
  lv_obj_set_size(clock_scale, scale_d, scale_d);
  lv_obj_center(clock_scale);
  lv_obj_set_style_pad_all(clock_scale, 0, 0);
  lv_obj_remove_flag(clock_scale,
                     LV_OBJ_FLAG_CLICKABLE); // tileview のスワイプを妨げない
  lv_obj_remove_flag(clock_scale, LV_OBJ_FLAG_SCROLLABLE);

  lv_scale_set_mode(clock_scale, LV_SCALE_MODE_ROUND_INNER);
  lv_scale_set_label_show(clock_scale, false);
  lv_scale_set_total_tick_count(clock_scale, 61); // 60分割を均等に
  lv_scale_set_major_tick_every(clock_scale, 5);
  lv_scale_set_range(clock_scale, 0, 60);
  lv_scale_set_angle_range(clock_scale, 360);
  lv_scale_set_rotation(clock_scale, 270); // 0を12時位置に合わせる

  lv_obj_set_style_bg_opa(clock_scale, LV_OPA_TRANSP, LV_PART_MAIN);
  lv_obj_set_style_line_width(clock_scale, 0, LV_PART_MAIN);

  // 分目盛り
  lv_obj_set_style_line_width(clock_scale, 3,
                              LV_PART_ITEMS); // 線幅 1→3 見えないので少し太く
  lv_obj_set_style_length(clock_scale, 6, LV_PART_ITEMS);
  lv_obj_set_style_line_color(clock_scale, lv_color_hex(0x5A4A2A),
                              LV_PART_ITEMS);
  lv_obj_set_style_line_width(clock_scale, 1, LV_PART_INDICATOR);
  lv_obj_set_style_length(clock_scale, 6, LV_PART_INDICATOR);
  lv_obj_set_style_line_color(clock_scale, lv_color_hex(0x5A4A2A),
                              LV_PART_INDICATOR);

  // 針の座標系(clock_scaleローカル座標)
  clock_cx = scale_d / 2;
  clock_cy = scale_d / 2;
  clock_radius = scale_d / 2.0f - 8.0f;

  // アプライド・アワーインデックス(12本のバトン)
  for (int i = 0; i < 12; i++) {
    float ang_deg = i * 30.0f;
    lv_coord_t mw = (i == 0) ? 8 : 5;   // 12時位置だけ太く
    lv_coord_t mh = (i == 0) ? 18 : 14; // 12時位置だけ長く

    lv_coord_t mx, my;
    angle_to_point(ang_deg, clock_radius - (float)mh / 2.0f - 2.0f, &mx, &my);

    lv_obj_t *marker = lv_obj_create(clock_scale);
    lv_obj_remove_style_all(marker);
    lv_obj_set_size(marker, mw, mh);
    lv_obj_set_style_bg_opa(marker, LV_OPA_COVER, 0);
    lv_obj_set_style_bg_color(marker, lv_color_hex(0xD8B25C), 0);
    lv_obj_set_style_radius(marker, 1, 0);
    lv_obj_remove_flag(marker, LV_OBJ_FLAG_CLICKABLE);
    lv_obj_remove_flag(marker, LV_OBJ_FLAG_SCROLLABLE);
    lv_obj_set_pos(marker, mx - mw / 2, my - mh / 2);
    lv_obj_set_style_transform_pivot_x(marker, mw / 2, 0);
    lv_obj_set_style_transform_pivot_y(marker, mh / 2, 0);
    lv_obj_set_style_transform_rotation(marker, (int32_t)(ang_deg * 10.0f), 0);
  }

  // 大きい数字(12)を強調表示、他は小さめの数字
  static const char *hour_labels[12] = {"12", "1", "2", "3", "4",  "5",
                                        "6",  "7", "8", "9", "10", "11"};
  for (int i = 0; i < 12; i++) { // i=0-11について
    float ang_deg = i * 30.0f;   // 360/12.0=30度
    // bool is_big = (i == 0 || i == 4 || i == 8);  // 12, 4, 8 のインデックス
    bool is_big = (i == 0); // 12時の時大きくする

    lv_obj_t *num = lv_label_create(clock_scale);
    lv_label_set_text(num, hour_labels[i]);                      // 数字を表示
    lv_obj_set_style_text_color(num, lv_color_hex(0xF5F5F5), 0); // 明るい白
    lv_obj_set_style_text_font(
        num, is_big ? &lv_font_montserrat_24 : &lv_font_montserrat_16,
        0); // 大きい数字は24px、小さい数字は16px
    if (is_big) {
      lv_obj_set_style_text_color(num, lv_color_hex(0xFFFFFF), 0);
      // 太字っぽく見せたい場合はopaやshadowで縁取りも可(任意)
    }

    lv_coord_t lx, ly;
    float label_radius =
        clock_radius - (is_big ? 34.0f : 28.0f); // 大きい数字は内側寄り
    angle_to_point(ang_deg, label_radius, &lx, &ly);

    lv_obj_update_layout(num); // サイズ確定のため先にレイアウト計算
    lv_coord_t lw = lv_obj_get_width(num);
    lv_coord_t lh = lv_obj_get_height(num);
    lv_obj_set_pos(num, lx - lw / 2, ly - lh / 2);
    lv_obj_remove_flag(num, LV_OBJ_FLAG_CLICKABLE);
    lv_obj_remove_flag(num, LV_OBJ_FLAG_SCROLLABLE);
  }

  // 針(ゴールド。時針・分針は先端+尾の2セグメント構成)
  lv_color_t gold_color = lv_color_hex(0xD8B25C); // 金色 時針・分針 いらないかも
  lv_color_t cream_color =
      lv_color_hex(0xF0E6D2); // クリーム色(ほぼ白) 秒針 いらないかも

  // 針(色を画像風に変更:時針=黄色、分針=白、秒針=赤)
  lv_color_t hour_color = lv_color_hex(0xFFC107); // 黄色(時針)
  lv_color_t min_color = lv_color_hex(0xF0F0F0);  // 白/銀(分針)
  lv_color_t sec_color = lv_color_hex(0xE53935);  // 赤(秒針)

  hand_hour_main = create_hand_segment(
      clock_scale, 7, (lv_coord_t)(clock_radius * 0.50f), false, hour_color, 3);
  hand_hour_tail = create_hand_segment(
      clock_scale, 5, (lv_coord_t)(clock_radius * 0.12f), true, hour_color, 2);
  hand_min_main = create_hand_segment(
      clock_scale, 5, (lv_coord_t)(clock_radius * 0.72f), false, min_color, 2);
  hand_min_tail = create_hand_segment(
      clock_scale, 4, (lv_coord_t)(clock_radius * 0.14f), true, min_color, 2);
  hand_sec_main = create_hand_segment(
      clock_scale, 2, (lv_coord_t)(clock_radius * 0.85f), false, sec_color, 1);
  hand_sec_tail = create_hand_segment(clock_scale, 8, 8, true, sec_color, 4);

  // 中心のキャップ
  clock_center = lv_obj_create(clock_scale);
  lv_obj_remove_style_all(clock_center); //
  lv_obj_set_size(clock_center, 11, 11); // サイズ
  lv_obj_set_style_radius(clock_center, LV_RADIUS_CIRCLE, 0);
  lv_obj_set_style_bg_opa(clock_center, LV_OPA_COVER, 0);
  lv_obj_set_style_bg_color(clock_center, sec_color,
                            0); // 中心キャップも秒針と同色に
  lv_obj_set_style_border_width(clock_center, 1, 0); // 枠の幅
  lv_obj_set_style_border_color(clock_center, lv_color_hex(0x0A0A0A),
                                0); // 枠の色 ほぼ黒色
  lv_obj_center(clock_center);

  // 初期表示 + 更新タイマー(30ms=滑らかな動き)
  clock_update_timer_cb(nullptr);
  if (clock_timer == nullptr)
    clock_timer = lv_timer_create(clock_update_timer_cb, 30, nullptr);
}

// ==============================
// 99.電源off
// ==============================
// (99.電源off) 3回点滅後電源off
void powerOffSystem() {
  for (int i = 3; i > 0; i--) {     // 3回繰返す 点滅
    instance.setBrightness(0);      // 明るさ 0(off),1-255
    delay(300);                     // 0.3秒待つ
    instance.setBrightness(i * 33); // 明るさ 0(off),1-255
    delay(300);                     // 0.3秒待つ
  }
  instance.setBrightness(0); // 明るさ 0(off),1-255
  delay(200);                // 0.2秒待つ
  instance.pmu.shutdown();   // PMU(AXP2101)の全電源を遮断して完全シャットダウン
}

// (99.画面on/off) 無操作監視タイマー setup()から呼ばれる
void idle_check_timer_cb(lv_timer_t *timer) {
  uint32_t inactive_ms =
      lv_disp_get_inactive_time(NULL); // 最後の操作から経過したmS
  if (inactive_ms > 2UL * 60UL * 1000UL)
    powerOffSystem(); // 2分より過ぎていれば電源off関数へ
}

// tileview全体に1つだけ登録するコールバック(setup()内、tv作成後に追加)
static void tileview_change_cb(lv_event_t *e) {
  lv_obj_t *tv_obj = (lv_obj_t *)lv_event_get_target(e);
  lv_obj_t *act_tile =
      lv_tileview_get_tile_act(tv_obj); // 現在アクティブなタイル

  // アナログ時計タイルがアクティブな時だけタイマーを動かす
  if (act_tile == tileM23) {
    if (clock_timer)
      lv_timer_resume(clock_timer);
    clock_update_timer_cb(
        nullptr); // 復帰直後に一度即時更新(古い時刻表示を防ぐ)
  } else {
    if (clock_timer)
      lv_timer_pause(clock_timer);
  }
}

// ==============================
// setup(), loop()
// ==============================
void setup() {
#if defined(ARDUINO_T_WATCH_S3) // T-Watch S3,T-Watch S3 Plusの時
  dispX = 240;
  dispY = 240;
#else // T-Watch Ultra, ESP32-S3-Touch-AMOLED-2.06の時
  dispX = 410;// 予定のみ
  dispY = 502;// 予定のみ
#endif
  // dispX = 368;// (非時計)ESP32-S3-Touch-AMOLED-1.8の時はどうするか
  // dispY = 448;

  Serial.begin(115200); // シリアルモニタの通信速度
                        // Arduino-IDEと合っていないと表示しません
  instance.begin();                   // LilyGoLibを初期化
  beginLvglHelper(instance);          // LVGLを初期化
  instance.pmu.enableBattDetection(); // 電源機能の有効化
  instance.setBrightness(100);        // 明るさ0(消),1-255
  instance.rtc.hwClockWrite();        // RTC時刻をハードウェアにcopy
  if (!prefs.begin("myApp", true)) { // (保存領域名, 書込モード(trueは書込不可))
                                     // NVSを開けなかったら
    Serial.println("スペースを開くことができません");
  }
  setHour = prefs.getInt("setHour");     // アラームの時 読出し
  setMinute = prefs.getInt("setMinute"); // アラームの分 読出し
  prefs.end();                           // NVSを閉じる

  lv_display_t *disp =
      lv_display_create(240, 240); // 指定解像度で新しいディスプレイを作成
  tv = lv_tileview_create(lv_screen_active()); // default displayにタイルを作成

  lv_obj_add_event_cb(tv, tileview_change_cb, LV_EVENT_VALUE_CHANGED, nullptr);

  // ======================================================
  // タイル11 アラーム時刻設定 [ALM]-D時計-輝度//wifi-t設定-A時計
  // ======================================================
  lv_obj_t *tileM11 =
      lv_tileview_add_tile(tv, 0, 0, LV_DIR_RIGHT); // 1行1列目 右のみに移動
  // tileviewにtileを追加(tileviewオブジェクトへのpointer,タイルの列,行,次に進む方向)

  lv_subject_init_int(&day_subject3, 0);     // 初期値 0:今日, 1:明日
  lv_subject_init_int(&hour_subject3, 12);   // 初期値 12時
  lv_subject_init_int(&minute_subject3, 30); // 初期値 30分 (初期値今日の12:30)
  // 整数型のSubjectを初期化(Subjectへのpointer,初期値)
  lv_subject_init_group(&time_subject3, time_group_array_subject3, 3);
  // グループ型サブジェクトを初期化(グループ型サブジェクトへのpointer,
  // 他のサブジェクトアドレスのリスト,list[]内の要素数)

  // キャンセルボタン ------------------------------
  cxl_btn = lv_button_create(tileM11); // cxl_btnを作成
  // ボタンobjを作成(objへのpointer 新しいボタンの親)
  // 戻り値:作成されたボタンへのpointer
  lv_obj_set_pos(cxl_btn, 2, 5); // btnの位置
  lv_obj_add_event_cb(cxl_btn, cxl_clicked_event_cb, LV_EVENT_CLICKED,
                      NULL); // cxl_btnを押したらコールバック関数へ

  lv_obj_t *cxl_label = lv_label_create(cxl_btn); // cxl_btnにlabelを作成
  lv_obj_set_style_text_font(cxl_label, &lv_font_montserrat_24,
                             0);          // 英数文字サイズ (8)10-48の偶数
  lv_label_set_text(cxl_label, "Cancel"); // labelのtextを設定

  // セットボタン ------------------------------
  set_btn = lv_button_create(tileM11); // set_btnを作成
  lv_obj_set_pos(set_btn, 120, 5);     // btnの位置
  lv_obj_set_width(set_btn, 115);      // btn幅
  lv_obj_add_event_cb(set_btn, set_btn_clicked_event_cb3, LV_EVENT_CLICKED,
                      NULL); // set_btnを押したらコールバック関数へ

  set_label = lv_label_create(set_btn); // btnにlabelを作成
  lv_obj_set_style_text_font(set_label, &lv_font_montserrat_24,
                             0);           // 英数文字サイズ (8)10-48の偶数
  lv_label_set_text(set_label, "    Set"); // labelのtextを設定

  // ラベル ------------------------------
  lv_obj_t *time_label3 = lv_label_create(tileM11); // time_labelを作成
  lv_obj_set_pos(time_label3, 5, 60);               // ラベルの位置 0,60
  lv_obj_set_width(time_label3, 110);               // 幅設定
  lv_obj_set_style_text_font(time_label3, &lv_font_montserrat_28,
                             0); // 英数文字サイズ (8)10-48の偶数
  lv_subject_add_observer_obj(&time_subject3, time_observer_cb3, time_label3,
                              NULL);
  // ウィジェットのサブジェクトにオブザーバーを追加(サブジェクトへのポインタ,
  // 通知コールバック,ウィジェットへのpointer,オプションのユーザーデータ)
  // 戻り値:新しく作成されたオブザーバーへのpointer
  // ウィジェットが削除されると、オブザーバーはサブジェクトから自動的に登録解除。
  // 注:この方法で作成されたオブザーバーは呼出さない。
  // これらのオブザーバーは、次のいずれかの方法でのみクリーンアップしてください。
  // lv_observer_remove()ウィジェットを削除するか、
  // すべてのオブザーバーを適切に分離して削除するには、lv_subject_deinit()
  // を呼出す。
  lv_obj_set_pos(time_label3, 0, 60); // ラベルの位置 70,20

  // 翌日ローラー ------------------------------
  day_roller = lv_roller_create(tileM11);         // 画面に翌日用ローラーを作成
  lv_obj_set_pos(day_roller, 10, 155);            // rollerの位置 10 105
  lv_obj_set_width(day_roller, 80);               // 幅設定
  lv_roller_set_visible_row_count(day_roller, 2); // 望ましい行数
  lv_obj_set_style_text_font(day_roller, &lv_font_montserrat_24,
                             0); // 英数文字サイズ (8)10-48の偶数
  lv_roller_set_options(day_roller, day_options02,
                        LV_ROLLER_MODE_NORMAL);    // ローラーは最後で停止
  lv_roller_bind_value(day_roller, &day_subject3); // 関連付

  // 時ローラー ------------------------------
  lv_obj_t *hour_roller = lv_roller_create(tileM11); // 画面に時用ローラーを作成
  lv_obj_set_pos(hour_roller, 100, 55);              // rollerの位置 85 105
  lv_obj_set_width(hour_roller, 60);                 // 幅設定
  lv_roller_set_visible_row_count(hour_roller, 5);   // 望ましい行数
  lv_obj_set_style_text_font(hour_roller, &lv_font_montserrat_24,
                             0); // 英数文字サイズ (8)10-48の偶数
  lv_obj_add_flag(hour_roller, LV_OBJ_FLAG_FLEX_IN_NEW_TRACK);
  // 1つ以上のフラグを設定(objへのpointer, 設定する値のOR演算)
  // LV_OBJ_FLAG_FLEX_IN_NEW_TRACK:このアイテムで新しいFlexトラックを開始
  lv_roller_set_options(
      hour_roller, hour_options,
      LV_ROLLER_MODE_INFINITE); // 永久スクロール ...22,23,0,1,...
  // ローラーのオプションを設定(ローラーobjへのpointer,\nで区切られた文字列,LV_ROLER_MODE_・・・)
  // LV_ROLLER_MODE_NORMAL:ローラーはオプションの最後で停止
  // LV_ROLLER_MODE_INFINITE:ローラーは永久にスクロール
  lv_roller_bind_value(hour_roller, &hour_subject3);
  // 整数SubjectをRollerの値に関連付(Rollerへのpointer,Subjectへのpointer)

  // 分ローラー ------------------------------
  min_roller = lv_roller_create(tileM11);         // 画面に分用のローラーを作成
  lv_obj_set_pos(min_roller, 170, 55);            // rollerの位置 160 105
  lv_obj_set_width(min_roller, 60);               // 幅設定
  lv_roller_set_visible_row_count(min_roller, 5); // 望ましい行数?????
  lv_obj_set_style_text_font(min_roller, &lv_font_montserrat_24,
                             0); // 英数文字サイズ (8)10-48の偶数
  lv_roller_set_options(min_roller, minute_options,
                        LV_ROLLER_MODE_INFINITE);     // 永久スクロール 上と同様
  lv_roller_bind_value(min_roller, &minute_subject3); // 関連付け 上と同様

  // ======================================================
  // タイル12 デジタル時計 ALM-[D時計]-輝度//wifi-t設定-A時計
  // ======================================================
  lv_obj_t *tileM12 = lv_tileview_add_tile(
      tv, 1, 0,
      (lv_dir_t)(LV_DIR_LEFT | LV_DIR_BOTTOM | LV_DIR_RIGHT));   // 左右下へ移動
  lv_obj_set_style_bg_color(tileM12, lv_color_make(0, 0, 0), 0); // rgb 黒地
  lv_obj_set_style_bg_opa(tileM12, LV_OPA_COVER, 0); // 不透明度を設定(必要)
  lv_obj_set_style_text_color(tileM12, lv_color_make(0, 255, 0), 0); // rgb 緑字
  int y01 = 67, y02 = 35, y03 = 25; // 行間,2行目位置,文字

  // 1行目1 年/月/日( 表示はloop内
  label11 = lv_label_create(tileM12); // 画面にラベル11を追加
  lv_obj_set_style_text_font(label11, &lv_font_montserrat_24,
                             LV_PART_MAIN); // 英数文字サイズ (8)10-48の偶数
  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(tileM12); // 現在の画面にラベル11を追加
  lv_obj_align(label12, LV_ALIGN_TOP_LEFT, 115, 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 ")"
  lv_obj_t *label13 = lv_label_create(tileM12); // 現在の画面にラベル11を追加
  lv_obj_set_style_text_font(label13, &lv_font_montserrat_24,
                             LV_PART_MAIN); // 英数文字サイズ (8)10-48の偶数
  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(tileM12); // 現在の画面にラベル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(tileM12); // 現在の画面にラベル12を追加
  lv_obj_set_style_text_font(label15, &lv_font_montserrat_24,
                             LV_PART_MAIN); // 英数文字サイズ (8)10-48の偶数
  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(tileM12); // 現在の画面にラベル21を追加
  lv_obj_set_style_text_font(label21, &lv_font_montserrat_24,
                             LV_PART_MAIN); // 英数文字サイズ (8)10-48の偶数
  lv_obj_align(label21, LV_ALIGN_TOP_LEFT, 0,
               y01 * 0 + y02 + y03); // 左上からの位置

  // 2行目2 現在時刻(12H) 表示はloop内
  label22 = lv_label_create(tileM12); // 現在の画面にラベル22を追加
  lv_obj_set_style_text_font(label22, &lv_font_montserrat_48,
                             LV_PART_MAIN); // 英数文字サイズ (8)10-48の偶数
  lv_obj_align(label22, LV_ALIGN_TOP_LEFT, 60, y01 * 0 + y02); // 左上からの位置
  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(tileM12); // 現在の画面にラベル31を追加
  lv_obj_align(label31, LV_ALIGN_TOP_LEFT, 0,
               y01 * 1 + y02 + y03); // 左上からの位置
  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)
  label32 = lv_label_create(tileM12); // 現在の画面にラベル32を追加
  lv_obj_set_style_text_font(label32, &lv_font_montserrat_48,
                             LV_PART_MAIN); // 英数文字サイズ (8)10-48の偶数
  lv_obj_align(label32, LV_ALIGN_TOP_LEFT, 60, y01 * 1 + y02); // 左上からの位置
  lv_obj_set_style_transform_zoom(label32, 320, 0); // ズーム(=/256)倍

  // 4行目1 "あと" デフォルトfont
  lv_obj_t *label41 = lv_label_create(tileM12); // 現在の画面にラベル41を追加
  lv_obj_align(label41, LV_ALIGN_TOP_LEFT, 0,
               y01 * 2 + y02 + y03); // 左上からの位置
  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(tileM12); // 現在の画面にラベル42を追加
  lv_obj_set_style_text_font(label42, &lv_font_montserrat_48,
                             LV_PART_MAIN); // 英数文字サイズ (8)10-48の偶数
  lv_obj_align(label42, LV_ALIGN_TOP_RIGHT, -65,
               y01 * 2 + y02);                      // 右上からの位置-60
  lv_obj_set_style_transform_zoom(label42, 320, 0); // ズーム4(=/256)倍

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

  // ======================================================
  // タイル13 明るさ設定 ALM-D時計-[輝度]//wifi-t設定-A時計
  // ======================================================
  lv_obj_t *tileM13 =
      lv_tileview_add_tile(tv, 2, 0, LV_DIR_LEFT); // 1行3列目 左のみに移動

  // タイトル ------------------------------
  label = lv_label_create(tileM13); // 画面にlabel追加
  lv_obj_align(label, LV_ALIGN_TOP_LEFT, 20, 20);
  // objの配置を変更し新座標を設定(配置objへのpointer,配置の種類,配置後のxオフセット,y)
  lv_label_set_text(label, "明るさ設定"); // タイトル表示

  // バック(キャンセル)ボタン ------------------------------
  cxl_btn = lv_button_create(tileM13); // cxl_btnを作成
  lv_obj_set_pos(cxl_btn, 120, 5);     // btnの位置
  lv_obj_set_width(cxl_btn, 115);      // btn幅
  lv_obj_add_event_cb(cxl_btn, cxl_clicked_event_cb, LV_EVENT_CLICKED,
                      NULL); // cxl_btnを押したらコールバック関数へ

  cxl_label = lv_label_create(cxl_btn); // cxl_btnにlabelを作成
  lv_obj_set_style_text_font(cxl_label, &lv_font_montserrat_24,
                             0);          // 英数文字サイズ (8)10-48の偶数
  lv_label_set_text(cxl_label, "  back"); // labelのtextを設定

  // 明るさのボタン6個 ------------------------------
  lv_obj_t *btnm1 = lv_buttonmatrix_create(tileM13); // 画面にボタンmatrix追加
  lv_obj_set_pos(btnm1, 5, 60);                      // 開始位置
  lv_obj_set_size(btnm1, 230, 170);                  // サイズ
  lv_obj_set_style_text_font(btnm1, &lv_font_montserrat_24,
                             LV_PART_MAIN); // 英数文字サイズ (8)10-48の偶数
  lv_buttonmatrix_set_map(btnm1, btnm_map); // ボタンmapのセット
  lv_obj_add_event_cb(btnm1, event_handler, LV_EVENT_ALL,
                      NULL); // 押したら上記cb関数へ
  // objにイベントハンドラ関数を追加(objへのpointer,新イベント関数,
  // イベントを呼出すコード,その関数でカスタムデータを使用できます)

  // ======================================================
  // タイル21 wifiで時刻設定 ALM-D時計-輝度//[wifi]-t設定-A時計 まだ
  // ======================================================
  lv_obj_t *tileM21 =
      lv_tileview_add_tile(tv, 0, 1, LV_DIR_RIGHT); // 2行1列目 右のみに移動
  btn = lv_button_create(tileM21);                  // 画面にボタン追加
  label = lv_label_create(btn);                     // ボタンにラベル追加
  lv_label_set_text(label, "wifiで時刻設定 右へ");  // ラベルの文字設定
  lv_obj_set_size(btn, LV_SIZE_CONTENT, LV_SIZE_CONTENT); // サイズは自動調整
  lv_obj_center(btn);                                     // 親の中央に配置

  // ======================================================
  // タイル22 手動で時刻設定 ALM-D時計-輝度//wifi-[t設定]-A時計
  // ======================================================
  lv_obj_t *tileM22 =
      lv_tileview_add_tile(tv, 1, 1, LV_DIR_ALL); // 全方向に移動
  lv_subject_init_int(&month_subject,
                      2); // 初期値 3月(1からスタートなので-1する)
  lv_subject_init_int(&day_subject,
                      14); // 初期値 15日(1からスタートなので-1する)
  lv_subject_init_int(&hour_subject, 12);   // 初期値 12時
  lv_subject_init_int(&minute_subject, 30); // 初期値 30分
  // 整数型のSubjectを初期化(Subjectへのpointer,初期値)
  lv_subject_init_group(&time_subject, time_group_array_subject4, 4);
  // グループ型サブジェクトを初期化(グループ型サブジェクトへのpointer,
  // 他のサブジェクトアドレスのリスト,list[]内の要素数)

  // キャンセルボタン ------------------------------
  cxl_btn = lv_button_create(tileM22); // cxl_btnを作成
  lv_obj_set_pos(cxl_btn, 2, 5);       // btnの位置
  lv_obj_add_event_cb(cxl_btn, cxl_clicked_event_cb, LV_EVENT_CLICKED,
                      NULL); // cxl_btnを押したらコールバック関数へ

  cxl_label = lv_label_create(cxl_btn); // cxl_btnにlabelを作成
  lv_obj_set_style_text_font(cxl_label, &lv_font_montserrat_24,
                             0);          // 英数文字サイズ (8)10-48の偶数
  lv_label_set_text(cxl_label, "Cancel"); // labelのtextを設定

  // セットボタン ------------------------------
  set_btn = lv_button_create(tileM22); // set_btnを作成
  lv_obj_set_pos(set_btn, 120, 5);     // btnの位置
  lv_obj_set_width(set_btn, 115);      // btn幅
  lv_obj_add_event_cb(set_btn, set_btn_clicked_event_cb4, LV_EVENT_CLICKED,
                      NULL); // set_btnを押したらコールバック関数へ

  set_label = lv_label_create(set_btn); // btnにlabelを作成
  lv_obj_set_style_text_font(set_label, &lv_font_montserrat_24,
                             0);           // 英数文字サイズ (8)10-48の偶数
  lv_label_set_text(set_label, "    Set"); // labelのtextを設定

  // ラベル ------------------------------
  time_label = lv_label_create(tileM22); // time_labelを作成 設定時刻を表示
  lv_obj_set_style_text_font(time_label, &lv_font_montserrat_28,
                             0); // 英数文字サイズ (8)10-48の偶数
  lv_subject_add_observer_obj(&time_subject, time_observer_cb4, time_label,
                              NULL);
  // ウィジェットのサブジェクトにオブザーバーを追加(サブジェクトへのポインタ,
  // 通知コールバック,ウィジェットへのpointer,オプションのユーザーデータ)
  // 戻り値:新しく作成されたオブザーバーへのpointer
  // ウィジェットが削除されると、オブザーバーはサブジェクトから自動的に登録解除。
  // 注:この方法で作成されたオブザーバーは呼出さない。
  // これらのオブザーバーは、次のいずれかの方法でのみクリーンアップしてください。
  // lv_observer_remove()ウィジェットを削除するか、
  // すべてのオブザーバーを適切に分離して削除するには、lv_subject_deinit()
  // を呼出す。
  lv_obj_set_pos(time_label, 0, 60); // ラベルの位置 70,20

  // 月ローラー ------------------------------
  lv_obj_t *month_roller =
      lv_roller_create(tileM22); // 画面に翌日用ローラーを作成
  lv_roller_set_options(month_roller, month_options,
                        LV_ROLLER_MODE_INFINITE);     // 永久スクロール
  lv_roller_bind_value(month_roller, &month_subject); // 関連付
  lv_obj_set_pos(month_roller, 0, 100);               // rollerの位置
  lv_obj_set_style_text_font(month_roller, &lv_font_montserrat_24,
                             0);      // 英数文字サイズ (8)10-48の偶数
  lv_obj_set_width(month_roller, 60); // 幅

  // 日ローラー ------------------------------
  day_roller = lv_roller_create(tileM22); // 画面に翌日用ローラーを作成
  lv_roller_set_options(day_roller, day_options31,
                        LV_ROLLER_MODE_INFINITE); // 永久スクロール
  lv_roller_bind_value(day_roller, &day_subject); // 関連付
  lv_obj_set_pos(day_roller, 60, 100);            // rollerの位置
  lv_obj_set_style_text_font(day_roller, &lv_font_montserrat_24,
                             0);    // 英数文字サイズ (8)10-48の偶数
  lv_obj_set_width(day_roller, 60); // 幅

  // 時ローラー ------------------------------
  hour_roller = lv_roller_create(tileM22); // 画面に時用ローラーを作成
  lv_obj_add_flag(hour_roller, LV_OBJ_FLAG_FLEX_IN_NEW_TRACK);
  // 1つ以上のフラグを設定(objへのpointer, 設定する値のOR演算)
  // LV_OBJ_FLAG_FLEX_IN_NEW_TRACK:このアイテムで新しいFlexトラックを開始
  lv_roller_set_options(
      hour_roller, hour_options,
      LV_ROLLER_MODE_INFINITE); // 永久スクロール ...22,23,0,1,...
  // ローラーのオプションを設定(ローラーobjへのpointer,\nで区切られた文字列,
  // or LV_ROLER_MODE_NORMAL LV_ROLLER_MODE_INFINITE)
  // LV_ROLLER_MODE_NORMAL:ローラーはオプションの最後で停止
  // LV_ROLLER_MODE_INFINITE:ローラーは永久にスクロール
  lv_roller_bind_value(hour_roller, &hour_subject);
  // 整数SubjectをRollerの値に関連付(Rollerへのpointer,Subjectへのpointer)
  // 戻り値:新しく作成されたObserverへのpointer
  lv_obj_set_pos(hour_roller, 120, 100); // rollerの位置
  lv_obj_set_style_text_font(hour_roller, &lv_font_montserrat_24,
                             0);     // 英数文字サイズ (8)10-48の偶数
  lv_obj_set_width(hour_roller, 60); // 幅

  // 分ローラー ------------------------------
  min_roller = lv_roller_create(tileM22); // 画面に分用のローラーを作成
  lv_roller_set_options(min_roller, minute_options,
                        LV_ROLLER_MODE_INFINITE);    // 永久スクロール 上と同様
  lv_roller_bind_value(min_roller, &minute_subject); // 関連付け 上と同様
  lv_obj_set_pos(min_roller, 180, 100);              // rollerの位置
  lv_obj_set_style_text_font(min_roller, &lv_font_montserrat_24,
                             0); // 英数文字サイズ (8)10-48の偶数
  lv_obj_set_width(min_roller, 60);

  // ======================================================
  // タイル23 アナログ時計表示 ALM-D時計-輝度//wifi-t設定-[A時計]
  // ======================================================
  // lv_obj_t *tileM23 = lv_tileview_add_tile(tv, 2, 1, LV_DIR_LEFT);  //
  // 2行3列目 左のみに移動
  tileM23 =
      lv_tileview_add_tile(tv, 2, 1, LV_DIR_LEFT); // 2行3列目 左のみに移動
  create_analog_clock_tileM23(tileM23);            //

  // ======================================================
  lv_tileview_set_tile_by_index(tv, 1, 0, LV_ANIM_OFF); // 起動時のタイル位置
  // アクティブタイル設定(tileviewのポインタ,タイルのカラム,行,アニメLV_ANIM_ON
  // or LV_ANIM_OFF)
  instance.setBrightness(100); // 明るさ 0-255
  if (firstDisp) {             // 起動時なら
    tileM12_update();          // 時計画面のloop表示
    firstDisp = false;         // 起動時の表示の後
  }
  lv_timer_create(status_update_timer_cb, 2000,
                  NULL); // タイマー登録 2秒ごとに時計画面更新
  lv_timer_create(idle_check_timer_cb, 1000,
                  NULL); // 無操作監視タイマーを1秒ごとに登録
}

void loop() {
  lv_timer_handler(); //
  lv_task_handler();  // lvglタスク処理は、ループ内に書く
  instance.loop();    // LVGLの内部状態を更新
  delay(5);           // 5mS待つ
}

スケッチの状態

twatchs3 の処理
ボード:lilygo-t-watch-s3
フレームワーク:arduino
プラットフォーム: Espressif 32 (55.3.311) > LilyGo T-Watch S3 (16M Flash 8M OPI PSRAM )
ハードウェア: ESP32S3 240MHz、320KB RAM、16MB Flash
- contrib-piohome @ 3.4.4
- framework-arduinoespressif32 @ 3.3.11
- framework-arduinoespressif32-libs @ 5.5.5+sha.b774170ff46
- tool-esp-rom-elfs @ 2024.10.11
- tool-esptoolpy @ 5.3.0
- tool-xtensa-esp-elf-gdb @ 17.1.0+20260402
- toolchain-xtensa-esp-elf @ 14.2.0+20260121
|-- LilyGoLib @ 0.2.0+sha.c4a10b2
|-- lvgl @ 9.5.0
|-- RadioLib @ 7.4.0
|-- XPowersLib @ 0.3.1
|-- SensorLib @ 0.3.3
|-- TinyGPSPlus @ 1.0.3
|-- ESP32 BLE Keyboard Fork @ 0.3.4+sha.20c8b9f
|-- ESP32 BLE Mouse @ 0.3.1+sha.61bcd32
|-- ESP8266Audio @ 2.0.0
|-- NimBLE-Arduino @ 2.2.3
|-- IRremoteESP8266 @ 2.8.6
|-- Preferences @ 3.3.11
RAM: [= ] 8.5% (used 27716 bytes from 327680 bytes)
Flash: [=== ] 27.8% (used 1164174 bytes from 4194304 bytes)
esptool v5.3.0
Chip type: ESP32-S3 (QFN56) (revision v0.2)
Features: Wi-Fi, BT 5 (LE), Dual Core + LP Core, 240MHz, Embedded PSRAM 8MB (AP_3v3)
Crystal frequency: 40MHz
USB mode: USB-Serial/JTAG
ボーレート 921600

platformio.ini

claudeの回答です。

;PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html

[platformio]
; ! Select the name of the board you want to use
; !===============================VARIANT========================================
; ! Products Link: https://lilygo.cc/products/t-watch-s3
; ! Products Link: https://lilygo.cc/products/t-watch-s3-plus
default_envs = twatchs3 ; Used T-Watch-S3 or T-Watch-S3-Plus
; ! Coming Soon
; default_envs = twatch_ultra ; Used T-Watch-Ultra
; ! Products Link: https://lilygo.cc/products/t-lora-pager
; default_envs = tlora_pager ; Used T-LoRa-Pager

; ! ===============================Emulator=======================================
; default_envs = emulator_lora_pager
; default_envs = emulator_twatchs3
; default_envs = emulator_watch_ultra
; src_dir = ${platformio.libdeps_dir}/${platformio.default_envs}/LilyGoLib/examples/factory
; ! ===============================Emulator=======================================

; ! ===============================Examples=======================================
; ! The src_dir variable can only have one row
; ! Not using an example - compiling src/main.cpp (ported Arduino IDE sketch)
; src_dir = ${platformio.libdeps_dir}/${platformio.default_envs}/LilyGoLib/examples/factory
; src_dir = ${platformio.libdeps_dir}/${platformio.default_envs}/LilyGoLib/examples/helloworld
; src_dir = ${platformio.libdeps_dir}/${platformio.default_envs}/LilyGoLib/examples/ble/BLEKeyboard
; src_dir = ${platformio.libdeps_dir}/${platformio.default_envs}/LilyGoLib/examples/ble/BLEMouse

boards_dir = ./boards

[env_arduino]
framework = arduino
; --- Switched to pioarduino fork (Arduino-ESP32 core 3.x / ESP-IDF 5.x) ---
; The official espressif32@6.10.0 only supports core 2.0.17, which is missing
; APIs used by the current LilyGoLib source (rgb_ele_order, I2SClass, etc).
; Replace the release tag below with the latest stable pioarduino tag if needed:
; https://github.com/pioarduino/platform-espressif32/releases
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.311/platform-espressif32.zip
; --- Auto-patch ESP32-BLE-Mouse-fork's std::string/String mismatch on every build ---
; --- Auto-sync our edited lv_conf.h (CJK default font) into .pio/libdeps on every build ---
extra_scripts =
    pre:support/patch_blemouse.py
    pre:support/patch_lv_conf.py
upload_speed = 921600
monitor_speed = 115200
board_build.filesystem = fatfs
board_build.partitions = ${platformio.libdeps_dir}/${platformio.default_envs}/LilyGoLib/examples/factory/partitions.csv
build_flags =
    -D CORE_DEBUG_LEVEL=0
    ; -U ARDUINO_USB_CDC_ON_BOOT ; Disable Serial output
    -D RADIOLIB_EXCLUDE_RF69
    -D RADIOLIB_EXCLUDE_SX1231
    ; -D RADIOLIB_EXCLUDE_CC1101
    ; -D RADIOLIB_EXCLUDE_SI443X
    ; -D RADIOLIB_EXCLUDE_SX128X
    ; -D RADIOLIB_EXCLUDE_NRF24
    -D RADIOLIB_EXCLUDE_RFM2X
    -D RADIOLIB_EXCLUDE_SX127X
    ; -D RADIOLIB_EXCLUDE_STM32WLX
    -D RADIOLIB_EXCLUDE_AFSK
    -D RADIOLIB_EXCLUDE_AX25
    -D RADIOLIB_EXCLUDE_HELLSCHREIBER
    -D RADIOLIB_EXCLUDE_MORSE
    -D RADIOLIB_EXCLUDE_RTTY
    -D RADIOLIB_EXCLUDE_SSTV
    -D RADIOLIB_EXCLUDE_DIRECT_RECEIVE
    -D RADIOLIB_EXCLUDE_APRS
    -D RADIOLIB_EXCLUDE_BELL
    -Wnarrowing
    -D ARDUINO_LILYGO_LORA_SX1262 ; Used SX1262 LoRa
    ; -D ARDUINO_LILYGO_LORA_CC1101 ; Used CC1101 LoRa
    ; -D ARDUINO_LILYGO_LORA_SX1280 ; Used SX1280 LoRa
    ; -D ARDUINO_LILYGO_LORA_LR1121 ; Used LR1121 LoRa
    ; -D ARDUINO_LILYGO_LORA_SI4432 ; Used SI4432 LoRa
    ; --- Force ESP32-BLE-Keyboard-fork / ESP32-BLE-Mouse-fork to build against
    ;     NimBLE instead of the old Bluedroid-based "ESP32 BLE Arduino" library ---
    -D USE_NIMBLE
monitor_filters =
    default
    esp32_exception_decoder
lib_deps =
    https://github.com/Xinyuan-LilyGO/LilyGoLib
    lvgl/lvgl @ ^9.4.0
    jgromes/RadioLib @ ^7.4.0
    lewisxhe/XPowersLib@^0.3.1
    lewisxhe/SensorLib @ ^0.3.3
    mikalhart/TinyGPSPlus @ ^1.0.3
    https://github.com/lewisxhe/ESP32-BLE-Keyboard-fork.git
    https://github.com/lewisxhe/ESP32-BLE-Mouse-fork.git
    earlephilhower/ESP8266Audio @ 2.0.0
    ; --- Removed "ESP32 BLE Arduino" (old Bluedroid lib) to avoid
    ;     esp_gap_ble_api.h / esp_gatt_defs.h build errors. Camera-shutter
    ;     remote (BLE HID keyboard) will run entirely over NimBLE below. ---
    h2zero/NimBLE-Arduino @ ^2.2.3

[env:twatchs3]
extends = env_arduino
board = lilygo-t-watch-s3
upload_speed = 921600
build_flags =
    ${env_arduino.build_flags}
    -D ARDUINO_T_WATCH_S3
    -I variants/lilygo_twatch_s3
lib_deps =
    ${env_arduino.lib_deps}
    https://github.com/tonhuisman/IRremoteESP8266/archive/refs/heads/feature/C++20-compatibility.zip

[env:twatch_ultra]
extends = env_arduino
board = lilygo-t-watch-ultra
build_flags =
    ${env_arduino.build_flags}
    -I variants/lilygo_twatch_ultra
    -D ARDUINO_T_WATCH_S3_ULTRA
lib_deps =
    ${env_arduino.lib_deps}
    https://github.com/lewisxhe/ST25R3916-fork.git
    https://github.com/lewisxhe/NFC-RFAL-fork.git

[env:tlora_pager]
extends = env_arduino
board = lilygo-t-lora-pager
build_flags =
    ${env_arduino.build_flags}
    -D ARDUINO_T_LORA_PAGER
    -I variants/lilygo_tlora_pager
lib_deps =
    ${env_arduino.lib_deps}
    adafruit/Adafruit TCA8418 @ ^1.0.2
    adafruit/Adafruit BusIO @ ^1.17.0
    https://github.com/lewisxhe/ST25R3916-fork.git
    https://github.com/lewisxhe/NFC-RFAL-fork.git

; ! ===============================Emulator=======================================
[env_emulator]
platform = native@^1.1.3
extra_scripts =
    pre:support/sdl2_paths.py ; Tries to find SDL2 include and lib paths on your system - specifically for MacOS w/ Homebrew
    post:support/sdl2_build_extra.py
build_flags =
    ; Don't use lv_conf.h. Tweak params via platfom.ini.
    -D LV_CONF_SKIP
    -D LV_CONF_INCLUDE_SIMPLE
    ; Enable LVGL demo, remove when working on your own project
    -D LV_USE_DEMO_WIDGETS=1
    -D ARDUINO_LILYGO_LORA_SX1262
    ; Add more defines below to overide lvgl:/src/lv_conf_simple.h
    -D LV_USE_PRIVATE_API
    -D SDL_ZOOM=1
    -D LV_SDL_INCLUDE_PATH="\"SDL2/SDL.h\""
    ; LVGL memory options, setup for the demo to run properly
    -D LV_MEM_CUSTOM=1
    -D LV_MEM_SIZE="(600U * 480U * 2)"
    -D LV_USE_FLOAT=1
    -D LV_FONT_MONTSERRAT_48=1
    -D LV_FONT_MONTSERRAT_20=1
    -D LV_FONT_MONTSERRAT_24=1
    -D LV_FONT_MONTSERRAT_22=1
    -D LV_FONT_MONTSERRAT_21=1
    -D LV_FONT_MONTSERRAT_28=1
    -D LV_FONT_MONTSERRAT_32=1
    -D LV_FONT_MONTSERRAT_18=1
    -D LV_FONT_MONTSERRAT_16=1
    -D LV_FONT_MONTSERRAT_14=1
    -D LV_FONT_MONTSERRAT_12=1
    ; -D LV_LOG_LEVEL=LV_LOG_LEVEL_INFO
    -D LV_LOG_PRINTF=1
    ; Add recursive dirs for hal headers search
    !python -c "import os; print(' '.join(['-I {}'.format(i[0].replace('\x5C','/')) for i in os.walk('hal/sdl2')]))"
    -lSDL2
    ; SDL drivers options
    -D LV_LVGL_H_INCLUDE_SIMPLE
    -D LV_DRV_NO_CONF
    -D LV_USE_SDL
lib_deps =
    ; Use direct URL, because package registry is unstable
    lvgl@9.2.2
    https://github.com/Xinyuan-LilyGO/LilyGoLib
lib_archive = false

[env:emulator_lora_pager]
extends = env_emulator
build_flags =
    ${env_emulator.build_flags}
    -D SDL_HOR_RES=480
    -D SDL_VER_RES=222
    -D ARDUINO_T_LORA_PAGER
    -D USING_INPUT_DEV_KEYBOARD
lib_deps =
    ${env_emulator.lib_deps}

[env:emulator_twatchs3]
extends = env_emulator
build_flags =
    ${env_emulator.build_flags}
    -D SDL_HOR_RES=240
    -D SDL_VER_RES=240
    -D ARDUINO_T_WATCH_S3
    -D USING_IR_REMOTE
lib_deps =
    ${env_emulator.lib_deps}

[env:emulator_watch_ultra]
extends = env_emulator
build_flags =
    ${env_emulator.build_flags}
    -D SDL_HOR_RES=502
    -D SDL_VER_RES=410
    -D ARDUINO_T_WATCH_S3_ULTRA
lib_deps =
    ${env_emulator.lib_deps}

patch_blemouse.py

claudeの回答です。

"""
PlatformIO pre-build script.

Purpose:
    lewisxhe/ESP32-BLE-Mouse-fork (BleMouse.cpp) still passes std::string
    directly to APIs that now require Arduino's String type on newer
    Arduino-ESP32 cores (3.x). This causes build errors such as:

        error: cannot convert 'std::string' ... to 'String'
        error: no matching function for call to 'BLECharacteristic::setValue(std::string&)'

    Since .pio/libdeps/<env>/ gets re-downloaded whenever the library is
    fetched fresh (e.g. after deleting .pio), this script re-applies the
    fix automatically on every build instead of requiring a manual edit
    each time.

Registration (already added to platformio.ini):
    [env_arduino]
    extra_scripts = pre:support/patch_blemouse.py
"""

import os
import re

Import("env")  # noqa: F821  (provided by PlatformIO/SCons at runtime)


def _patch_file(path, replacements, label):
    if not os.path.isfile(path):
        print("[patch_blemouse] {} not found yet, skipping ({})".format(path, label))
        return

    with open(path, "r", encoding="utf-8", errors="ignore") as f:
        content = f.read()

    original = content
    for pattern, repl in replacements:
        content = re.sub(pattern, repl, content)

    if content != original:
        with open(path, "w", encoding="utf-8") as f:
            f.write(content)
        print("[patch_blemouse] Patched: {}".format(path))
    else:
        print("[patch_blemouse] Already up to date: {}".format(path))


def main():
    lib_dir = os.path.join(
        env.subst("$PROJECT_LIBDEPS_DIR"),  # noqa: F821
        env.subst("$PIOENV"),  # noqa: F821
        "ESP32 BLE Mouse",
    )
    target = os.path.join(lib_dir, "BleMouse.cpp")

    replacements = [
        # BLEDevice::init(bleMouseInstance->deviceName);
        (
            r"BLEDevice::init\(\s*bleMouseInstance->deviceName\s*\)",
            "BLEDevice::init(String(bleMouseInstance->deviceName.c_str()))",
        ),
        # ...->manufacturer()->setValue(bleMouseInstance->deviceManufacturer);
        (
            r"setValue\(\s*bleMouseInstance->deviceManufacturer\s*\)",
            "setValue(String(bleMouseInstance->deviceManufacturer.c_str()))",
        ),
    ]

    _patch_file(target, replacements, "ESP32 BLE Mouse fork")


main()

patch_lv_conf.py

claudeの回答です。

"""
PlatformIO pre-build script.

Purpose:
    PlatformIO's Library Dependency Finder (LDF) is documented to give the
    project's local "lib" folder top priority over libraries fetched into
    .pio/libdeps via lib_deps. In practice (see platformio-core issue #4537)
    this does not always hold, and the libdeps_dir copy can still win.

    Rather than depend on LDF priority behavior, this script copies our
    known-good, hand-edited lv_conf.h (kept in the project's own
    lib/LilyGoLib/src/lv_conf.h as the single source of truth) over the
    copy PlatformIO downloaded into .pio/libdeps/<env>/LilyGoLib/src/,
    on every build. That guarantees the CJK default font setting is
    actually the one used to compile, regardless of LDF search order,
    and survives .pio deletion / library re-download.

Registration (already added to platformio.ini):
    [env_arduino]
    extra_scripts =
        pre:support/patch_blemouse.py
        pre:support/patch_lv_conf.py

Requirement:
    Keep your edited reference copy at:
        <project_root>/lib/LilyGoLib/src/lv_conf.h
    That is the file you edit going forward. This script only reads it
    (never writes to it) and copies it into the build's actual libdeps path.
"""

import filecmp
import os
import shutil

Import("env")  # noqa: F821  (provided by PlatformIO/SCons at runtime)


def main():
    project_dir = env.subst("$PROJECT_DIR")  # noqa: F821
    source = os.path.join(project_dir, "lib", "LilyGoLib", "src", "lv_conf.h")

    target_dir = os.path.join(
        env.subst("$PROJECT_LIBDEPS_DIR"),  # noqa: F821
        env.subst("$PIOENV"),  # noqa: F821
        "LilyGoLib",
        "src",
    )
    target = os.path.join(target_dir, "lv_conf.h")

    if not os.path.isfile(source):
        print(
            "[patch_lv_conf] Reference file not found, skipping: {}".format(source)
        )
        return

    if not os.path.isdir(target_dir):
        print(
            "[patch_lv_conf] LilyGoLib not downloaded yet, skipping ({})".format(
                target_dir
            )
        )
        return

    if os.path.isfile(target) and filecmp.cmp(source, target, shallow=False):
        print("[patch_lv_conf] Already up to date: {}".format(target))
        return

    shutil.copyfile(source, target)
    print("[patch_lv_conf] Synced lv_conf.h -> {}".format(target))


main()