物理エオルゼア時計を改良する(アナログ時計版)

# はじめに
アドベントカレンダーの1日目が開いていたので滑り込んでみました。物理エオルゼア時計、ほしいですよね？(n回目)
というわけで以前の記事「[物理エオルゼア時計を作成する(アナログ時計版)](https://qiita.com/CIB-MC/items/2cc3d29076288d76e0ba)」で未実装だった部分を今回は作成していきます。

# 前回のおさらい
- 物理エオルゼア時計の実装方法について検討した
- 検討の結果、アナログ・2モーター・NTP同期式を採用した
- アナログ時計を実現するためのギアボックスについて、概略図面を示した
- 前述のギアボックスを稼働させるための電子回路について、配線図を示した
- 前述の基板上に搭載したESP32開発ボードに書き込むプログラムコードを示した

# 前回の成果紹介動画
<iframe width="560" height="315" src="https://www.youtube.com/embed/t6JghRacPK8" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

# 今回実装する内容
- ステッピングモーターの省電力化
- 0時地点自動調整

## ステッピングモーターの省電力化
モーターを稼働させないタイミングでは、モーターへの給電を停止するようにしました。

## 0時地点自動調整
0時地点に来た際に押下されるように、マイクロスイッチをギアボックス内に設置しました。
個のスイッチ出力を利用し初回電源投入時には、必ず0時地点に復帰させてから時間表示を開始するように実装しています。なお、時針と分針それぞれについて個別に調整が行われます。
また、ステッピングモーターを省電力化した際、回転量に滑りが発生するようになったため0時地点に時分針が来る度に0時地点自動調整を行います。

# ソフトウェアの実装(全コード)
<details><summary>長いので折りたたんでいます</summary><div>

``` cpp:real_eorzea_clock
/*
Copyright (c) 2022, CIB-MC
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, 
  this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, 
  this list of conditions and the following disclaimer in the documentation 
  and/or other materials provided with the distribution.
* Neither the name of the <organization> nor the names of its contributors 
  may be used to endorse or promote products derived from this software 
  without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

#include <Adafruit_NeoPixel.h>
#include <Stepper.h>
#include <time.h>
#include <WiFi.h>

const char* ssid       = "YOUR-SSID-HERE";
const char* password   = "YOUR-WIFI-PW-HERE";
const char* ntpServer = "NTP-SERVER-HERE";
const long  gmtOffset_sec = 3600 * 9;
const int   daylightOffset_sec = 0;

const int ONE_DAY_SEC = 60 * 60 * 24;
const int ONE_HOUR_SEC = 60 * 60;
const int DISC_TOOTH_STEPS = 128 + 2;
const int DISC_2TEETH_STEPS = DISC_TOOTH_STEPS * 2;
const int DISC_STEPS = DISC_2TEETH_STEPS * 60;
const int DISC_STEPS_HALF = DISC_2TEETH_STEPS * 30;
const int DISC_STEPS_INIT = DISC_STEPS - 2;

const int DIN_PIN = 26;
const int LED_COUNT = 4;

const int PIN_START_STOP = 0;
const int PIN_EORZEA_REAL = 2;
const int PIN_HOUR_CW = 4;
const int PIN_MIN_CW = 15;

const int MOTOR_STEPS = 2048;

const int PIN_STP_HOUR_A = 33;
const int PIN_STP_HOUR_B = 32;
const int PIN_STP_HOUR_C = 5;
const int PIN_STP_HOUR_D = 18;

const int PIN_STP_MIN_A = 21;
const int PIN_STP_MIN_B = 22;
const int PIN_STP_MIN_C = 23;
const int PIN_STP_MIN_D = 19;

const int PIN_HOUR_HAND = 12;
const int PIN_MINUTE_HAND = 14;

int buttonState[4];
bool started = false;
bool eorzea = true;

int hours_disc_step_p = 0;
int minutes_disc_step_p = 0;

bool hour_hand_adjusted = false;
bool minute_hand_adjusted = false;

int u_hours = -1;
int u_minutes = -1;

Adafruit_NeoPixel pixels(LED_COUNT, DIN_PIN, NEO_GRB + NEO_KHZ800);
Stepper stepper_min(MOTOR_STEPS, PIN_STP_MIN_A,PIN_STP_MIN_C,PIN_STP_MIN_B,PIN_STP_MIN_D);
Stepper stepper_hour(MOTOR_STEPS, PIN_STP_HOUR_A,PIN_STP_HOUR_C,PIN_STP_HOUR_B,PIN_STP_HOUR_D);

void setup() {
  Serial.begin(115200);
  pixels.begin();
  pinMode(PIN_START_STOP, INPUT_PULLUP);
  pinMode(PIN_EORZEA_REAL, INPUT_PULLUP);
  pinMode(PIN_HOUR_CW, INPUT_PULLUP);
  pinMode(PIN_MIN_CW, INPUT_PULLUP);
  pinMode(PIN_HOUR_HAND, INPUT_PULLUP);
  pinMode(PIN_MINUTE_HAND, INPUT_PULLUP);
  
  stepper_min.setSpeed(10);
  stepper_hour.setSpeed(10);
  xTaskCreatePinnedToCore(repeatingControlLED, "repeatingControlLED", 8192, NULL, 2, NULL, 1);

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
      delay(1000);
      Serial.print(".");
  }
  Serial.println("done");
  xTaskCreatePinnedToCore(repeatingConfigTime, "repeatingConfigTime", 8192, NULL, 2, NULL, 1);
  while (!hour_hand_adjusted || !minute_hand_adjusted) {
    int hour_hand = digitalRead(PIN_HOUR_HAND);
    if (!hour_hand_adjusted && hour_hand == LOW) {
      stepper_hour.step(1);
      hours_disc_step_p = DISC_STEPS_INIT;
    } else {
      hour_hand_adjusted = true;
    }

    int minute_hand = digitalRead(PIN_MINUTE_HAND);
    if (!minute_hand_adjusted && minute_hand == LOW) {
      stepper_min.step(1);
      minutes_disc_step_p = DISC_STEPS_INIT;
    } else {
      minute_hand_adjusted = true;
    }
  }
  delay(500);
  started = true;
}

void repeatingConfigTime(void *args) {
  while(true) {
    configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
    delay(ONE_DAY_SEC * 1000);
  }
}

void repeatingControlLED(void *args) {
  while(true) {
    pixels.clear();
    int c_hours = u_hours / 100;
    if (started) {
      if (6 <= c_hours && c_hours < 17) {
        pixels.setPixelColor(0, pixels.Color(16, 16, 16));
        pixels.setPixelColor(1, pixels.Color(16, 16, 16));
        pixels.setPixelColor(2, pixels.Color(16, 16, 16));
        pixels.setPixelColor(3, pixels.Color(16, 16, 16));
      } else if (17 <= c_hours && c_hours < 19) {
        pixels.setPixelColor(0, pixels.Color(14, 9, 2));
        pixels.setPixelColor(1, pixels.Color(14, 9, 2));
        pixels.setPixelColor(2, pixels.Color(14, 9, 2));
        pixels.setPixelColor(3, pixels.Color(14, 9, 2));
      } else if (19 <= c_hours && c_hours < 22) {
        pixels.setPixelColor(0, pixels.Color(0, 6, 14));
        pixels.setPixelColor(1, pixels.Color(0, 6, 14));
        pixels.setPixelColor(2, pixels.Color(0, 6, 14));
        pixels.setPixelColor(3, pixels.Color(0, 6, 14));
      } else if ((22 <= c_hours && c_hours < 24) || (0 <= c_hours && c_hours < 6)) {
        pixels.setPixelColor(0, pixels.Color(8, 0, 12));
        pixels.setPixelColor(1, pixels.Color(8, 0, 12));
        pixels.setPixelColor(2, pixels.Color(8, 0, 12));
        pixels.setPixelColor(3, pixels.Color(8, 0, 12));
      } else {
        pixels.setPixelColor(0, pixels.Color(16, 16, 16));
        pixels.setPixelColor(1, pixels.Color(16, 16, 16));
        pixels.setPixelColor(2, pixels.Color(16, 16, 16));
        pixels.setPixelColor(3, pixels.Color(16, 16, 16));
      }
    } else {
      pixels.setPixelColor(0, pixels.Color(32, 0, 0));
      pixels.setPixelColor(1, pixels.Color(32, 0, 0));
      pixels.setPixelColor(2, pixels.Color(32, 0, 0));
      pixels.setPixelColor(3, pixels.Color(32, 0, 0));
    }
    pixels.show();
    delay(500);
  }
}

void loop() {
  buttonState[0] = digitalRead(PIN_START_STOP);
  buttonState[1] = digitalRead(PIN_EORZEA_REAL);
  buttonState[2] = digitalRead(PIN_HOUR_CW);
  buttonState[3] = digitalRead(PIN_MIN_CW);

  if (buttonState[0] == LOW) {
    started = !started;
    delay(1500);
  }
  if (buttonState[1] == LOW) {
    eorzea = !eorzea;
    delay(1500);
  }
  if ((buttonState[2] == LOW) && !started) {
    stepper_hour.step(1);
    hours_disc_step_p = 1;
  }
  if ((buttonState[3] == LOW) && !started) {
    stepper_min.step(1);
    minutes_disc_step_p = 1;
  }

  struct tm timeinfo;
  if (started && getLocalTime(&timeinfo)) {
    int week_sec = (ONE_DAY_SEC * timeinfo.tm_wday) + (ONE_HOUR_SEC * timeinfo.tm_hour) + (60 * timeinfo.tm_min) + timeinfo.tm_sec;
    int eorzea_week_sec = (double)week_sec * (1440.0d / 70.0d);

    int e_hours = (int)(((double)eorzea_week_sec / (double)ONE_HOUR_SEC ) * 100.0d) % (24 * 100);
    int e_minutes = (eorzea_week_sec / 60) % 60;

    int r_hours = ((timeinfo.tm_hour % 24) * 100) + (int)(((double)timeinfo.tm_min / 60.0d) * 100.0d);
    int r_minutes = timeinfo.tm_min;

    u_hours = eorzea ? e_hours : r_hours;
    u_minutes = eorzea ? e_minutes : r_minutes;

    int hours_disc_step_c = DISC_2TEETH_STEPS * ((u_hours % (12 * 100)) / 20);
    int minutes_disc_step_c = DISC_2TEETH_STEPS * u_minutes;

    int diff_hours = (hours_disc_step_c + DISC_STEPS - hours_disc_step_p) % DISC_STEPS;
    int diff_minutes = (minutes_disc_step_c + DISC_STEPS - minutes_disc_step_p) % DISC_STEPS;

    if (diff_hours != 0) {
      if (diff_hours < DISC_STEPS_HALF) {
        stepper_hour.step(1);
        hours_disc_step_p = (hours_disc_step_p + 1) % DISC_STEPS;
      } else {
        stepper_hour.step(-1);
        hours_disc_step_p = (hours_disc_step_p + DISC_STEPS - 1) % DISC_STEPS;
      }
    } else {
      digitalWrite(PIN_STP_HOUR_A, LOW);
      digitalWrite(PIN_STP_HOUR_B, LOW);
      digitalWrite(PIN_STP_HOUR_C, LOW);
      digitalWrite(PIN_STP_HOUR_D, LOW);
    }

    if (diff_minutes != 0) {
      if (diff_minutes < DISC_STEPS_HALF) {
        stepper_min.step(1);
        minutes_disc_step_p = (minutes_disc_step_p + 1) % DISC_STEPS;
      } else {
        stepper_min.step(-1);
        minutes_disc_step_p = (minutes_disc_step_p + DISC_STEPS - 1) % DISC_STEPS;
      }
    } else {
      digitalWrite(PIN_STP_MIN_A, LOW);
      digitalWrite(PIN_STP_MIN_B, LOW);
      digitalWrite(PIN_STP_MIN_C, LOW);
      digitalWrite(PIN_STP_MIN_D, LOW);
    }

    int hour_hand = digitalRead(PIN_HOUR_HAND);
    if (hour_hand != LOW) {
      if (!hour_hand_adjusted) {
        hours_disc_step_p = DISC_STEPS_INIT;
        hour_hand_adjusted = true;
      }
    } else {
      minute_hand_adjusted = false;
    }

    int minutes_hand = digitalRead(PIN_MINUTE_HAND);
    if (minutes_hand != LOW) {
      if (!minute_hand_adjusted) {
        minutes_disc_step_p = DISC_STEPS_INIT;
        minute_hand_adjusted = true;
      }
    } else {
      minute_hand_adjusted = false;
    }
  }
}
```
</div></details>

# 今回の成果紹介動画
<iframe width="560" height="315" src="https://www.youtube.com/embed/rMtJN2zXMzw" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

# さいごに
エオルゼア時計を作ったといいつつ、もっぱら日本標準時表示モードにして使っているのは秘密です☆