M5StickC Plusでエオルゼアライフを快適にする

# はじめに
FINAL FANTASY XIV、楽しいですね。(ステマ)
というわけでエオルゼア生活をもっと楽しむためのデバイスをM5StickC Plusで作ってしまいましょう。
※念の為申し添えますが、MODの話ではないです。

# 想定する読者
* エオルゼアライフを楽しみたい/楽しんでいる人
* M5Stackシリーズでのプログラミングを楽しみたい人/楽しんでいる人

# M5StickC Plusでやってみたこと
* エオルゼア時間(ゲーム内時刻)を表示する。
* 2要素認証に必要なワンタイムパスワードを表示する。

# エオルゼア時間を求める
FF14の世界内ではエオルゼア時間という現実時間の約20.57倍(1440÷70倍)で進む時間が流れています。
また、日本日時の日曜0時は、エオルゼア時間も0時になります。(日本国内DCワールドのみ？)

この仕様を利用すると、M5StickC Plus用には下記のコードでエオルゼア時間が求められます。
なお、RTCにはUTCが設定されているものとします。

```cpp:eorzea_time
const int ONE_DAY_SEC = 60 * 60 * 24;
const int ONE_HOUR_SEC = 60 * 60;

RTC_DateTypeDef DateStruct;
RTC_TimeTypeDef TimeStruct;
M5.Rtc.GetData(&DateStruct);
M5.Rtc.GetTime(&TimeStruct);

int week_sec = (ONE_DAY_SEC * DateStruct.WeekDay) + (ONE_HOUR_SEC * TimeStruct.Hours) + (60 * TimeStruct.Minutes) + TimeStruct.Seconds + (ONE_HOUR_SEC * 9);
int eorzea_week_sec = (double)week_sec * (1440.0d / 70.0d);
int e_hours = (eorzea_week_sec / ONE_HOUR_SEC) % 24; // 時間(時)
int e_minutes = (eorzea_week_sec / 60) % 60; // 時間(分)
```

なお `eorzea_week_sec % 60` でエオルゼア秒を求めることもできますが、変化が早いのと精度が荒いので表示には向きません。

:::note info
現実時間のミリ秒が取れる環境ではこれも合わせて利用することにより、エオルゼア秒の精度を高く取ることができます。
:::

その前にM5StickC Plusに内蔵の時計(RTC)も合わせないといけないですね。
WiFi経由でNTPサーバーに接続して時刻を合わせましょう。
コード例は下記の通りです。なお、例ではUTCをRTCに設定しています。

```cpp:adjust_time
#include <M5StickCPlus.h>
#include <WiFi.h>
#include <WiFiMulti.h>

// ===== 中略 =====

int adjust_time_by_ntp() {
  const char* WIFI_SSID = "SET-YOUR-SSID-HERE"; // お使いのWiFiアクセスポイントのSSIDを入力してください。
  const char* WIFI_PASS = "SET-YOUR-PASS-HERE"; // お使いのWiFiアクセスポイントのパスワードを入力してください。
  const char* NTP_SERVER = "ntp.jst.mfeed.ad.jp";
  const long GMT_OFFSET_SEC = 0;
  const int DAYLIGHT_OFFSET_SEC = 0;
  const int MAX_RETRY_NTP = 1;
  const int MAX_RETRY_GLT_CHECK = 25;

  int retry_count_ntp = 0;
  int retry_count_glt_check = 0;
  struct tm lt;

  WiFi.mode(WIFI_STA);
  wifiMulti.addAP(WIFI_SSID, WIFI_PASS);
  while (wifiMulti.run() != WL_CONNECTED) {
    if(retry_count_ntp++ > MAX_RETRY_NTP) return 1;
    delay(5000);
  }

  configTime(GMT_OFFSET_SEC, DAYLIGHT_OFFSET_SEC, NTP_SERVER);

  while (!getLocalTime(&lt)) {
    if(retry_count_glt_check++ > MAX_RETRY_GLT_CHECK) return 2;
    delay(200);
  } // システムクロックが落ち着くのを待ちます。

  RTC_TimeTypeDef TimeStruct;
  TimeStruct.Hours   = lt.tm_hour;
  TimeStruct.Minutes = lt.tm_min;
  TimeStruct.Seconds = lt.tm_sec;
  M5.Rtc.SetTime(&TimeStruct);
  RTC_DateTypeDef DateStruct;
  DateStruct.WeekDay = lt.tm_wday;
  DateStruct.Month = lt.tm_mon + 1;
  DateStruct.Date = lt.tm_mday;
  DateStruct.Year = lt.tm_year + 1900;
  M5.Rtc.SetData(&DateStruct);
  
  WiFi.disconnect(true);
  WiFi.mode(WIFI_OFF); // 省エネのためにWiFiを切ります。

  return 0;
}
```

成功した場合は「0」、WiFiに接続できなかった場合は「1」、システムクロックへの設定が失敗した場合は「2」が返ります。

# ワンタイムパスワードを求める

FF14にログインする際(というよりはスクエニIDでログインする際)に、2要素認証としてワンタイムパスワードを利用することができます。
利用できるパスワードの生成方法には専用トークン・専用アプリの他、スクエニさんが「各種認証アプリ」と総称する Google Authenticator 等が利用できます。
つまり [RFC6238](https://tex2e.github.io/rfc-translater/html/rfc6238.html) のTime-based One-time Password(TOTP)ということなので、これを生成する機能を実装すればいいということになります。
逆に言うとスクエニIDに限らず、これに準拠しているワンタイムパスワードなら何でもこの手法で求められるわけです。

TOTPを生成するのに必要なHMACを求める際には [Portable-CXX-Hashing-Library](https://github.com/mshockwave/Portable-CXX-Hashing-Library) を利用しました。

```cpp:get_totp
#include <M5StickCPlus.h>
#include "time.h"
#include "sha1.h"
#include "hmac.h"

// ===== 中略 =====

unsigned int get_totp(unsigned long long totp_t_ll, unsigned char *totp_s, size_t totp_s_size) {
  unsigned char totp_t[8];
  memset(totp_t, 0, (size_t)8);
  memcpy(totp_t, &totp_t_ll, (size_t)8);
  unsigned char totp_t_reversed[8];
  memset(totp_t_reversed, 0, (size_t)8);
  for (int i = 0; i < 8; i++) {
    totp_t_reversed[i] = totp_t[7 - i];
  } // ネットワークバイトオーダーに変換します
  std::string hash = hmac<SHA1>(totp_t_reversed, (size_t)8, totp_s, totp_s_size);

  unsigned char *hash_bytes;
  hash_bytes = (unsigned char *)malloc(hash.length()/2);
  for (size_t i = 0; i < hash.length()/2; i++) {
    std::string b = hash.substr(i * 2, 2);
    hash_bytes[i] = std::stoi(b, NULL, 16);
  }
  int offset = hash_bytes[(hash.length()/2) - 1] & 0x0f;
  unsigned int int_totp = ((unsigned int)(hash_bytes[offset] & 0x7f) << 24)
    | ((unsigned int)(hash_bytes[offset + 1] & 0xff) << 16)
    | ((unsigned int)(hash_bytes[offset + 2] & 0xff) << 8)
    | ((unsigned int)(hash_bytes[offset + 3] & 0xff));

  free(hash_bytes);
  return int_totp; //全桁を返すので、スクエニIDの場合は下6桁のみ利用します。
}
```

「totp_t_ll」に「epoch ÷ 30」(64Bit長)、「totp_s」にシークレットのバイト列、「totp_s_size」にバイト列長を指定します。

:::note warn
シークレットバイト列の取り扱いにはご注意ください。悪意ある第三者に流出した場合、ワンタイムパスワードを生成されてしまいます。
:::

なお、RTCを利用してepochを得る手段が用意されていないようだったので別途作成しました。

```cpp:get_epoch_from_rtc
#include <M5StickCPlus.h>
#include "time.h"

// ===== 中略 =====

time_t get_epoch_from_rtc() {
  RTC_DateTypeDef DateStruct;
  RTC_TimeTypeDef TimeStruct;
  M5.Rtc.GetData(&DateStruct);
  M5.Rtc.GetTime(&TimeStruct);
  struct tm t;
  t.tm_year = DateStruct.Year - 1900;
  t.tm_mon  = DateStruct.Month - 1;
  t.tm_mday = DateStruct.Date;
  t.tm_hour = TimeStruct.Hours;
  t.tm_min  = TimeStruct.Minutes;
  t.tm_sec  = TimeStruct.Seconds;
  t.tm_isdst= -1;
  return mktime(&t);
}
```

コード全体は下記のGitHubプロジェクトページにて。
[https://github.com/CIB-MC/ff14stack](https://github.com/CIB-MC/ff14stack)

# 成果物

![DSC_0541.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2066798/b8365caa-45b3-c95d-13f6-1ec988825b5c.jpeg)
![DSC_0542.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2066798/07f265ad-7862-e1bd-0bec-37f9ca71c3f0.jpeg)
※TOTPは仮のものです。

# まとめ
* M5StickC Plus上でエオルゼア時間を求める方法についてコードを示した。
* M5StickC Plus上でTOTPを求める方法についてコードを示した。

# おわりに

ぜひ楽しいエオルゼアライフを！

# 変更履歴

<dl>
  <dt>2021年9月23日</dt>
  <dd>初版掲載</dd>
</dl>

<dl>
  <dt>2021年9月24日</dt>
  <dd>GitHubプロジェクトページへのリンク追加</dd>
  <dd>誤字修正</dd>
</dl>