1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

M5Stack AtomS3 アナログ時計

Last updated at Posted at 2025-12-19

まえおき

作ってみた的なものです。
小さいアナログ時計が欲しかったので作ってみました。
タイムラプス動画の片隅に配置したかったのです。

コード

#include <Arduino.h>
#include <M5AtomDisplay.h>
#include <M5Unified.h>
#include <WiFi.h>

const char* WIFI_SSID = "MyHomeWiFi";
const char* WIFI_PASS = "MySecurePassword123";

char ntpServer[] = "ntp.nict.jp";
const long gmtOffset_sec = 9 * 3600;
const int daylightOffset_sec = 0;
struct tm timeinfo;
String second, minute, hour, dayOfWeek, dayOfMonth, month, year;

unsigned long lastNtpSync = 0;
const unsigned long ntpSyncInterval = 3600000; // 1時間 = 3600000ミリ秒

#define CLOCK_CENTER_X 64    // 時計の中心X座標
#define CLOCK_CENTER_Y 45    // 時計の中心Y座標
#define CLOCK_RADIUS   35    // 時計の半径
#define HOUR_LENGTH    20    // 短針の長さ
#define MINUTE_LENGTH  28    // 長針の長さ
#define SECOND_LENGTH  30    // 秒針の長さ


bool connectWiFi() {
  if (WiFi.status() == WL_CONNECTED) {
    return true;
  }
  
  M5.Display.setCursor(0, 24);
  M5.Display.println("WiFi再接続中...");
  
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  
  // 接続を最大20秒間試みる
  int attempts = 0;
  while (WiFi.status() != WL_CONNECTED && attempts < 100) {
    M5.Display.setCursor(12, 36);
    M5.Display.print("*  ");
    delay(200);
    M5.Display.setCursor(12, 36);
    M5.Display.print(" * ");
    delay(200);
    M5.Display.setCursor(12, 36);
    M5.Display.print("  *");
    delay(200);
    attempts++;
  }
  
  if (WiFi.status() == WL_CONNECTED) {
    M5.Display.setCursor(0, 36);
    M5.Display.print("IP:");
    M5.Display.println(WiFi.localIP());
    return true;
  } else {
    M5.Display.setCursor(0, 36);
    M5.Display.println("WiFi接続失敗");
    return false;
  }
}

void getTimeFromNTP() {
  if (!connectWiFi()) {
    return; // WiFi接続に失敗した場合は時刻同期をスキップ
  }
  
  M5.Display.setCursor(0, 48);
  M5.Display.println("NTP時刻同期中...");
  
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
  
  int attempts = 0;
  bool timeReceived = false;
  while (!timeReceived && attempts < 5) {
    timeReceived = getLocalTime(&timeinfo);
    if (!timeReceived) {
      delay(1000);
      attempts++;
    }
  }
  
  if (timeReceived) {
    M5.Display.setCursor(0, 60);
    M5.Display.println("時刻同期成功");
    lastNtpSync = millis(); // 同期時間を記録
  } else {
    M5.Display.setCursor(0, 60);
    M5.Display.println("時刻同期失敗");
  }
}

void getTime(String* year, String* month, String* day, String* hour, String* min, String* sec, String* wday) {
  char buf[10];
  getLocalTime(&timeinfo);
  *year = String(timeinfo.tm_year + 1900);
  sprintf(buf, "%02d", timeinfo.tm_mon + 1);
  *month = String(buf);
  sprintf(buf, "%02d", timeinfo.tm_mday);
  *day = String(buf);
  sprintf(buf, "%02d", timeinfo.tm_hour);
  *hour = String(buf);
  sprintf(buf, "%02d", timeinfo.tm_min);
  *min = String(buf);
  sprintf(buf, "%02d", timeinfo.tm_sec);
  *sec = String(buf);
  strftime(buf, 10, "%a", &timeinfo);
  *wday = String(buf);
}

void drawWiFiIcon(int x, int y, bool connected) {
  if (connected) {
    // WiFi接続中の場合、信号強度に応じたアンテナバーを表示
    int rssi = WiFi.RSSI();
    int strength = 0;
    
    // RSSIの値に基づいて信号強度(0-4)を計算
    if (rssi >= -50) strength = 4;      // 非常に強い(-50dBm以上)
    else if (rssi >= -65) strength = 3; // 強い(-65dBm以上)
    else if (rssi >= -75) strength = 2; // 普通(-75dBm以上)
    else if (rssi >= -85) strength = 1; // 弱い(-85dBm以上)
    else strength = 0;                  // とても弱い(-85dBm未満)
    
    // アンテナピクトを描画(縦棒を4本まで)
    int barWidth = 2;   // 各バーの幅
    int barGap = 1;     // バー間の隙間
    int maxHeight = 8;  // 最大の高さ
    
    for (int i = 0; i < 4; i++) {
      int barHeight = (i + 1) * 2;  // 各バーの高さ(2, 4, 6, 8)
      int barX = x + i * (barWidth + barGap);
      int barY = y + (maxHeight - barHeight);
      
      // 信号強度に応じて、対応するバーを塗りつぶすか輪郭のみ表示
      if (i < strength) {
        // 信号あり - 塗りつぶし
        M5.Display.fillRect(barX, barY, barWidth, barHeight, WHITE);
      } else {
        // 信号なし - 輪郭のみ(接続中だが信号が弱い場合)
        M5.Display.drawRect(barX, barY, barWidth, barHeight, WHITE);
      }
    }
  } else {
    // WiFi未接続の場合、×印付きのアイコンを表示
    int iconWidth = 11;  // アイコン全体の幅
    int iconHeight = 8;  // アイコン全体の高さ
    
    // 基本アンテナ形状(輪郭のみ)を描画
    for (int i = 0; i < 4; i++) {
      int barWidth = 2;
      int barGap = 1;
      int barHeight = (i + 1) * 2;
      int barX = x + i * (barWidth + barGap);
      int barY = y + (iconHeight - barHeight);
      
      M5.Display.drawRect(barX, barY, barWidth, barHeight, WHITE);
    }
    
    // ×印を重ねて表示
    M5.Display.drawLine(x, y, x + iconWidth - 3, y + iconHeight, WHITE);
    M5.Display.drawLine(x + iconWidth - 3, y, x, y + iconHeight, WHITE);
  }
}

// 時計の針を描画する関数
void drawClockHand(int value, int max_value, int length, uint16_t color) {
  float angle = ((float)value / (float)max_value) * 2.0 * PI;
  // 時計は12時が上(-PI/2)なので、調整が必要
  angle = angle - (PI / 2.0);
  int x2 = CLOCK_CENTER_X + length * cos(angle);
  int y2 = CLOCK_CENTER_Y + length * sin(angle);
  M5.Display.drawLine(CLOCK_CENTER_X, CLOCK_CENTER_Y, x2, y2, color);
}

// アナログ時計の外枠を描画
void drawClockFace() {
  // 時計の外枠を描画
  M5.Display.drawCircle(CLOCK_CENTER_X, CLOCK_CENTER_Y, CLOCK_RADIUS, WHITE);
  
  // 時間マーカーを描画(12, 3, 6, 9時の位置)
  for (int h = 0; h < 12; h++) {
    float angle = (h / 12.0) * 2.0 * PI - (PI / 2.0);
    int marker_length = (h % 3 == 0) ? 5 : 3; // 3, 6, 9, 12時は長めのマーカー
    int x1 = CLOCK_CENTER_X + (CLOCK_RADIUS - marker_length) * cos(angle);
    int y1 = CLOCK_CENTER_Y + (CLOCK_RADIUS - marker_length) * sin(angle);
    int x2 = CLOCK_CENTER_X + CLOCK_RADIUS * cos(angle);
    int y2 = CLOCK_CENTER_Y + CLOCK_RADIUS * sin(angle);
    M5.Display.drawLine(x1, y1, x2, y2, WHITE);
  }
  
  // 中心点
  M5.Display.fillCircle(CLOCK_CENTER_X, CLOCK_CENTER_Y, 2, WHITE);
}

void setup() {
  auto cfg = M5.config();
  M5.begin(cfg);
  M5.Display.init();
  M5.Display.setRotation(3);  // 表示方向の調整
  M5.Display.setFont(&fonts::lgfxJapanGothicP_12);
  M5.Display.setTextColor(WHITE, BLACK);
  M5.Display.fillScreen(BLACK);
  M5.Display.println("起動中...");

  getTimeFromNTP();
}

void loop() {
  // 1時間ごとにNTP同期を実行
  unsigned long currentMillis = millis();
  if (currentMillis - lastNtpSync >= ntpSyncInterval || lastNtpSync == 0) {
    // ミリ秒カウンタのオーバーフロー対策
    if (currentMillis < lastNtpSync) {
      lastNtpSync = 0;
    }
    
    // 一時的に時計表示を停止し、同期状態を表示
    M5.Display.fillScreen(BLACK);
    getTimeFromNTP();
    delay(2000); // 同期結果の表示を2秒間表示
  }
  
  getTime(&year, &month, &dayOfMonth, &hour, &minute, &second, &dayOfWeek);
  
  M5.Display.fillScreen(BLACK);
  
  drawClockFace();
  
  int h = hour.toInt() % 12;
  int m = minute.toInt();
  int s = second.toInt();
  
  float hour_pos = h + m / 60.0;
  drawClockHand(hour_pos, 12, HOUR_LENGTH, WHITE); // 短針
  drawClockHand(m, 60, MINUTE_LENGTH, WHITE); // 長針
  drawClockHand(s, 60, SECOND_LENGTH, RED); // 秒針
  
  M5.Display.setCursor(35, 90);
  M5.Display.printf("%s:%s:%s", hour.c_str(), minute.c_str(), second.c_str());
  M5.Display.setCursor(10, 105);
  M5.Display.printf("%s/%s/%s %s", year.c_str(), month.c_str(), dayOfMonth.c_str(), dayOfWeek.c_str());
  
  drawWiFiIcon(105, 2, WiFi.status() == WL_CONNECTED);
  
  delay(1000);
}

実行

作ったものより目立つスタックチャンの図

M5Stack AtomS3 アナログ時計.png

それから

これ作ってるときにディスプレイ部分もスイッチになっていることに気づきました。なんか押してる感があります。

ボタン、インサイドって書いてあるわ。

1
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?