09.T-Watch S3 Plusにスマホシャッター追加
09.T-Watch S3 Plusにスマホシャッター追加
前回の"08.T-Watch S3 plusで時計まとめ"に"07.T-Watch S3 plusでスマホシャッター"を追加しました。また、begin にオプションを加える事により起動時間の短縮になりました。
ボードのVer変更
espressif > arduino-esp32 > Release listhttps://github.com/espressif/arduino-esp32/releases
にて、3.3.7以降は ESP32-S3 で BLE 起動時にクラッシュする既知の不具合があるらしいです。platformio.iniのプラットフォームのVerを1つずつ変更し、.pio を削除後に実行をして確認しました。
ボードマネージャのVer→プラットフォームのVer:結果
3.3.6 → 55.03.36-1 : シャッターは動作する
3.3.7 → 55.03.37 : 連続再起動をして表示しない
3.3.8 → 55.03.38-1 : エラー
3.3.9 → 55.03.39 : 連続再起動をして表示しない
3.3.10 → 欠番
3.3.11 → 55.03.311 : 連続再起動をして表示しない
3.3.12 → 55.03.312 : エラー
4.0.0-alpha1(最新) → Verの表示無し
よって、3.3.7~12は正常動作しないので、現在の所(ボード) esp32 by Espressif Systems のVerは"3.3.6"に固定します。
スマホシャッターの操作法
操作は、"07.T-Watch S3 plusでスマホシャッター"と同じです。1.時計には"T-Watch Shutter"と表示されています。
2.スマホ > 設定 > 接続設定 > Bluetooth > 新しいデバイスとペア設定 > T-Watch Shutter > ペア設定する
* スマホにより異なります。
3.スマホ > カメラを開きます。
4.時計の"T-Watch Shutter"をタッチ > カウントダウンしてカメラのシャッターが切れます。
* BLE接続ができていない場合は"未接続"を表示します。
* 2分間の未操作で時計の電源がoffします。
起動時間短縮
S3 Plus には S3 に無い GPS チップがあります。S3 と S3 Plus は同じライブラリを使用し、GPS チップがあるかを起動時に判別しています。GPS チップがある場合は判定結果(起動)が早いのですが、無い場合には判定(起動)に数秒かかっているようです。よって S3 の場合は、判定しないようにしました。すると起動時間が6秒→1秒に短縮できました。また、FATファイルシステムも使用しないとしました。src\main.cpp の setup() で以下のように修正しました。・変更前
instance.begin(); // LilyGoLibを初期化。
・変更後
#if defined(ARDUINO_T_WATCH_S3) // S3の場合
instance.begin(NO_INIT_FATFS | NO_HW_GPS); // FatFsとGPSのチェックはしない
#else // S3 Plusの場合
instance.begin(NO_INIT_FATFS); // GPSのチェックはする
#endif
スケッチ
// 時計 v1.2 スマホシャッター (高速起動)
// 次回検討 翌日アラーム タッチ振動
// deepsleep アラーム時刻10分前と5分前に振動 時報
// 起動時の画面は(12)で、横3x縦2の6画面タイル
// (11) アラーム時刻設定 (12) デジタル時計表示 (13) 明るさの設定
// (21) スマホシャッター (22) 手動で時刻設定 (23) アナログ時計表示
// (ボード) esp32 by Espressif Systems 3.3.6
// 3.3.0(LilyGoが要求)-3.3.6(Bluetoothが動作)
// lv_conf.hでデフォルトfont:中日韓16pxのlv_font_simsun_16_cjk を使用
// 英数字のサイズ変更は、フォントを変更
// 48:D時計の時刻 28:ローラーの選択結果 24:いろいろ 16:A時計の文字盤で小
// 現時刻の年は、2026に固定。2秒ごとに時刻とBT残量を更新します
#include <LilyGoLib.h> // LilyGoLib 0.2.0
#include <LV_Helper.h> // lvgl by kisvegabor 9.5.0
#include <Preferences.h> // 不揮発性メモリNVSを使用 電源断でもアラーム時刻を覚える
Preferences prefs; // Preferencesオブジェクト
#include <freertos/FreeRTOS.h> // リアルタイムオペレーティングシステム
#include <freertos/task.h> // タスク管理
#include <math.h> // アナログ時計で三角関数を使用
#include <time.h> // 時間を使用
#include <BleKeyboard.h> // BLEでスマホシャッター使用
BleKeyboard bleKeyboard("T-Watch Shutter", "LilyGO", 100); // (スマホに表示のデバイス名,機種,100)
#include <NimBLEDevice.h> // BLE出力を下げる
#include <ESP_I2S.h> // I2S使用
I2SClass i2s;
#define SPK_BCLK_PIN 48 // MAX98357A I2S 3.2W MONO AMP
#define SPK_WCLK_PIN 15 // MAX98357A LRCLK
#define SPK_DOUT_PIN 46 // MAX98357A DIN
#define SPK_SAMPLE_RATE 16000 // (Hz) 毎秒RATE回発生
#define BEEP_FREQ 1000 // 音の高さ(Hz) 3秒後は別
//参考 NHK:予報440Hz/正報880Hz, NTT電話時報:1kHz
#define DURATION_MS 100 // 音の長さ(mS) 3秒後は別
//参考 NHK予報:正弦波0.1S,無音0.9S,3回繰返す, 正報:正弦波1S一定音量、次の2秒で直線減衰
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;
// (21.スマホシャッター) セルフタイマー
static lv_timer_t *timer_handle = NULL; //
static int countdown_count = 0; // 経過秒数(1,2,3)
static lv_obj_t *shutter_label = NULL; //
// 変数設定終わり
// 関数
// ==============================
// キャンセル(戻る)ボタン 各画面で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)
}
}
// ==============================
// 21.スマホシャッター
// ==============================
void speaker_init() { // スピーカ初期設定関数
i2s.setPins(SPK_BCLK_PIN, SPK_WCLK_PIN, SPK_DOUT_PIN, -1, -1); // I2Sのピン設定
// (int8_t bclk,int8_t ws,int8_t dout,int8_t din=-1,int8_t mclk=-1)
i2s.begin(I2S_MODE_STD, SPK_SAMPLE_RATE, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO, -1); //I2S初期化
}
static void speaker_beep(uint32_t frequency, uint32_t duration_ms) { // HzとmSでbeep関数
// if (!spk_tx_handle) return;// 呼出中はこの処理をブロック
uint32_t num_samples = (SPK_SAMPLE_RATE * duration_ms) / 1000; // 発生音数
for (uint32_t i = 0; i < num_samples; i++) { // サイン波データを生成しながらI2Sへ書込み
float sample_f = sin(2.0 * PI * frequency * i / SPK_SAMPLE_RATE); // sin波の計算 16bit モノラル
int16_t sample = (int16_t)(sample_f * 15000); // 音量は32767まで
i2s.write((uint8_t *)&sample, sizeof(sample)); // 1サンプル(16bit=2bytes)を書込み
}
}
static void countdown_timer_cb(lv_timer_t *t) { // 3秒カウントダウン後にスマホシャッターを押す関数
countdown_count++; // 1カウント(秒)追加
speaker_beep(BEEP_FREQ, DURATION_MS); // fHz を tms鳴らす
if (countdown_count < 3) { // 3秒以下なら
lv_label_set_text_fmt(shutter_label, "%d", 3 - countdown_count); // カウントダウン表示
} else { // 3秒経ったら
lv_obj_align(shutter_label, LV_ALIGN_CENTER, -35, -5);// 位置
lv_label_set_text(shutter_label, "シャッター完"); // ラベルに表示
delay(1000); // 1秒待つ
speaker_beep(2000, 300); // 2kHz 0.3秒鳴らす
if (bleKeyboard.isConnected()) bleKeyboard.write(KEY_MEDIA_VOLUME_UP); // スマホのシャッターを押す
lv_timer_del(timer_handle); //
timer_handle = NULL; //
countdown_count = 0; // カウントダウン変数リセット
}
}
static void shutter_btn_event_cb(lv_event_t *e) { // 画面シャッターボタンを押した時の関数
lv_event_code_t code = lv_event_get_code(e); // イベントコードを取得
if (code != LV_EVENT_CLICKED) return; // クリック以外は戻る
if (timer_handle != NULL) return; // すでにカウントダウン中なら戻る
if (!bleKeyboard.isConnected()) { // bleKeyboardに接続してなかったら
lv_obj_set_style_text_color(shutter_label, lv_color_make(255, 69, 0), LV_PART_MAIN); // 赤字 (Orangered)
lv_label_set_text(shutter_label, " BLE 未接続"); // ラベルに表示
return; // 戻る
}
countdown_count = 0; // カウントダウン変数リセット
lv_obj_center(shutter_label); // 位置
lv_obj_set_style_text_color(shutter_label, lv_color_make(0, 0, 0), LV_PART_MAIN); // 黒字
lv_label_set_text(shutter_label, "3"); // ラベルに表示
timer_handle = lv_timer_create(countdown_timer_cb, 1000, NULL); // 1s周期のタイマーを開始
// 新しいlv_timer(定期的なコールバック関数,呼出間隔mS,カスタムパラメータ)
}
// ==============================
// 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と合っていないと表示しません
delay(100); // 0.1秒待つ
#if defined(ARDUINO_T_WATCH_S3)
instance.begin(NO_INIT_FATFS | NO_HW_GPS); // FatFsとGPSのチェックはしない LilyGoLibを初期化
#else
instance.begin(NO_INIT_FATFS);
#endif
bool bleOk = bleKeyboard.begin(); // BLEキーボードを初期化///////////
if (bleOk) NimBLEDevice::setPower(ESP_PWR_LVL_N12); // BLE最小出力 -12dB/m//////////
// -12dBm:ESP_PWR_LVL_N12(N12),-9dBm:(N9),-6dBm:(N6),
// -3dBm:(N3),0dBm:(P0),3dBm:(P3),6dBm:(P6),9dBm:(P9)
// メモリ確保の為、BLEはLVGL初期化より前に
beginLvglHelper(instance); // LVGLを初期化
instance.pmu.enableBattDetection(); // 電源機能の有効化
instance.setBrightness(100); // 明るさ 1-255,0(消)
instance.rtc.hwClockWrite(); // RTC時刻をハードウェアにcopy
speaker_init(); // スピーカーアンプを初期化
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時計-輝度//シャッター-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時計 ]-輝度//シャッター-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時計-[ 輝度 ]//シャッター-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 スマホシャッター ALM-D時計-輝度//[ シャッター ]-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); // 親の中央に配置
*/
// lv_obj_t *scr = lv_scr_act(); // 現在の画面
//lv_obj_set_style_bg_color(tileM21, lv_color_black(), 0); // 黒地
lv_obj_set_style_bg_color(tileM12, lv_color_make(0, 0, 0), 0); // rgb 黒地
// タイトル ------------------------------
label = lv_label_create(tileM21); // 画面に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(tileM21); // 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, "Cancel"); // labelのtextを設定
btn = lv_btn_create(tileM21); // 画面にボタン作成
lv_obj_set_size(btn, 238, 120); // サイズ
lv_obj_center(btn); // 位置
lv_obj_set_style_radius(btn, 30, LV_STATE_DEFAULT); // 角丸
lv_obj_set_style_bg_color(btn, lv_color_make(175, 238, 238), 0); // 水色 Paleturquoise
lv_obj_add_event_cb(btn, shutter_btn_event_cb, LV_EVENT_CLICKED, NULL); // タッチしたらcb関数へ
shutter_label = lv_label_create(btn); // ボタンにラベル作成
lv_obj_center(shutter_label); // 位置
lv_obj_set_style_text_font(shutter_label, &lv_font_source_han_sans_sc_16_cjk, 0); // 文字の種類
lv_obj_set_style_transform_zoom(shutter_label, 435, 0); // x/256倍に拡大 1.5倍
lv_obj_align(shutter_label, LV_ALIGN_CENTER, -40, -10); // 拡大文字はずれるので補正
lv_obj_set_style_text_color(shutter_label, lv_color_make(0, 100, 0), LV_PART_MAIN); // 緑字 (Darkgreen)
lv_label_set_text(shutter_label, "T-Watch Shutter"); // 表示文字(スマホの接続先)
// ======================================================
// タイル22 手動で時刻設定 ALM-D時計-輝度//シャッター-[ 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時計-輝度//シャッター-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待つ
}
*/
スケッチの状態
PLATFORM: Espressif 32 (55.3.36) > LilyGo T-Watch S3 (16M Flash 8M OPI PSRAM )HARDWARE: ESP32S3 240MHz, 320KB RAM, 16MB Flash
- contrib-piohome @ 3.4.4
- framework-arduinoespressif32 @ 3.3.6
- framework-arduinoespressif32-libs @ 5.5.0+sha.f56bea3d1f
- tool-esp-rom-elfs @ 2024.10.11
- tool-esptoolpy @ 5.1.0
- toolchain-xtensa-esp-elf @ 14.2.0+20251107
|-- LilyGoLib @ 0.2.1+sha.9301b6d
|-- 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
|-- ESP_I2S @ 3.3.6
|-- Preferences @ 3.3.6
RAM: [= ] 11.1% (used 36384 bytes from 327680 bytes)
Flash: [==== ] 35.6% (used 1493715 bytes from 4194304 bytes)
esptool v5.1.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
Changing baud rate to 921600...
