ESP32と有機ELディスプレイで漫画やアニメに登場するキャラクター達の誕生日を表示できる日めくりカレンダーを作る

# はじめに
有機ELディスプレイを試しに使うことも兼ねて日めくりカレンダーを作りました。ESP32を使うとインターネットにも接続できるため、NTPにより正確な日付を簡単に取得できます。また、ワンピースに登場するキャラクター達の中からその日が誕生日のキャラクターを教えてくれる機能も付け加えてみました。実用性はあまりないかもしれませんが、ご参考になれば幸いです。
![one-piece_calendar.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2951963/053b593f-03b1-0c2e-7cb3-7e14ba06b023.jpeg)
# 動作確認環境
* Windows 11 Home 22H2 (22621.1265)
* Arduino IDE 1.8.13
* Adafruit GFX Library 1.11.5
* Adafruit SSD1306 2.5.7
* Adafruit BusIO 1.14.1
# ハードウェア
## 部品表
今回はSSD1306という制御ICが内蔵された単色の有機ELディスプレイを使いました。I2C通信によりArduinoから簡単に扱えます。なお、信号線SDA, SCLは適当な抵抗を介してプルアップする必要があります。また、Wi-FiやBluetoothを内蔵しながらもArduino開発環境で扱えるESP32を使いました。これでNTPを利用できます。
| 部品名 | 通販コード | 価格 |
|:--|:--|--:|
| ESP32-DevKitC | M-11819 | 1600円 |
| 有機ELディスプレイ | P-12031 | 580円 |
| 10kΩ抵抗 | R-07838 | 150円 |
| ブレッドボード | P-12366 | 460円 |
| ジャンパー線 | P-02315 | 300円 |

これらの部品は[__秋月電子通商__](https://akizukidenshi.com/)から入手できます。
## 回路図
![one-piece_calendar_breadboard.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2951963/855bd2cf-2d33-8dca-d7f5-cad45af571e1.png)
# ソフトウェア
## 時刻を取得する
NTPにより日付を同期する部分は[**こちら**](https://github.com/espressif/arduino-esp32/blob/master/libraries/ESP32/examples/Time/SimpleTime/SimpleTime.ino)のサンプルコードを参考にしました。ESP32はRTCも内蔵しているため、NTPと組み合わせることで正確な日付、時刻を取得できます。これらの処理はほとんど内部で行われているため、実装も簡単です。コールバック関数が用意されていたので同期の間隔を調べてみたところ、デフォルトでは1時間毎のようです。

## 有機ELディスプレイに表示する
画面を描画する部分については画面サイズが128x64ピクセルと小さいので工夫が必要です。文字サイズは1で6x8ピクセル、2で12x16ピクセルという感じになります。上半分には日付、下半分にはキャラクター名を最大4人まで表示できるように配置してみました。余談ですが、起動時に関数`display`を呼び出すことでAdafruitのスプラッシュ画面を表示できます。1分毎に日付を確認し、画面を更新しています。

## うるう年か判定する
誕生日のデータについては1月1日から日付順でキャラクター名を表す文字列の配列を用意し、`tm_yday`により1月1日からの経過日数を取得して参照しています。ただし、この配列の各要素はうるう年における順番で並んでいて、平年では3月以降1日ずれてしまうため、うるう年か判定して補正する必要がありました。次の条件を満たす年がうるう年となります。
+ 4で割り切れる年はうるう年
+ ただし、100で割り切れる年は平年
+ しかし、400で割り切れる年はうるう年

また、この配列の各要素はカンマ区切りの文字列とし、同じ日に誕生日を迎えるキャラクターが複数人いる場合でも表示できるようにしました。関数`strtok`によりカンマ区切りの文字列を分解して新たな配列に格納しています。注意としては関数`strtok`の第1引数に渡した文字列について、内部で区切り文字を終端文字に置換する処理が含まれているようです。つまり、読み取り専用の文字列をそのまま渡すとエラーメッセージが表示されてしまいます。関数`strcpy`により書き換え可能な文字列へコピーし、代わりに渡すことで回避しました。
## スケッチ
```cpp:characters_calendar.ino
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <WiFi.h>
#include <time.h>
#include "characters.h"

#define DISPLAY_WIDTH 128
#define DISPLAY_HEIGHT 64
#define DISPLAY_ADDRESS 0x3C

const char *ssid = ""; //put your Wi-Fi SSID
const char *password = ""; //put your Wi-Fi password

Adafruit_SSD1306 display(DISPLAY_WIDTH, DISPLAY_HEIGHT, &Wire);


bool isLeapYear(int year) {
  return (year % 4 == 0) && !((year % 100 == 0) && (year % 400 != 0));
}

void updateDisplay() {
  int year, month, mday, wday, yday;
  
  struct tm localTime;
  if (getLocalTime(&localTime)) {
    year = localTime.tm_year + 1900;
    month = localTime.tm_mon + 1;
    mday = localTime.tm_mday;
    wday = localTime.tm_wday;
    yday = localTime.tm_yday;
  } else {
    Serial.println("Failed to obtain the time");
    return;
  }

  char *birthdayCharacters[4];
  int numberOfCharacters = 0;
  char buffer[100];
  if (month >= 3 && !isLeapYear(year)) {
    yday += 1;
  }
  strcpy(buffer, characters[yday]);
  char *token = strtok(buffer, ",");
  while (token != NULL && numberOfCharacters < 4) {
    birthdayCharacters[numberOfCharacters] = token;
    numberOfCharacters++;
    token = strtok(NULL, ",");
  }
    
  display.clearDisplay();

  //date
  display.setTextColor(WHITE);
  display.setTextSize(2);
  display.setCursor(22, 0);
  display.printf("%02d/%02d", month, mday);

  //day of week
  const char *weekday[7] = {"SUN", "MON", "TUE", "WED", "THR", "FRI", "SAT"};
  display.setTextColor(WHITE);
  display.setTextSize(1);
  display.setCursor(88, 8);
  display.printf("%s", weekday[wday]);

  //shape
  display.fillRect(0, 18, display.width(), 12, WHITE);

  //caption
  display.setTextColor(BLACK);
  display.setTextSize(1);
  display.setCursor(16, 20);
  display.printf("Today's Birthday");

  //the names of the characters
  display.setTextColor(WHITE);
  display.setTextSize(1);
  display.setCursor(0, 32);
  for (int i = 0; i < numberOfCharacters; i++) {
    display.printf("%s\n", birthdayCharacters[i]);
  }

  display.display();
}

void setNTP() {
  WiFi.begin(ssid, password);
  display.clearDisplay();
  display.setTextColor(WHITE);
  display.setTextSize(1);
  display.setCursor(25, 28);
  display.printf("Connecting...");
  display.display();
  
  int retryCount = 0;
  while (WiFi.status() != WL_CONNECTED) {
    if (retryCount > 30) { //attempt to connect to wifi for 15 seconds
      Serial.println("Failed to connect to wifi");
      return;
    }

    delay(500);
    retryCount++;
  }

  configTzTime("JST-9", "time.google.com");
}
  

void setup() {
  Serial.begin(115200);
  
  display.begin(SSD1306_SWITCHCAPVCC, DISPLAY_ADDRESS); //generate the display voltage from 3.3V internally
  display.display(); //adafruit splash screen
  delay(1000);
  
  setNTP();
  updateDisplay();
}

void loop() {
  static unsigned long lastUpdateTime = millis();
  
  if (millis() - lastUpdateTime > 60 * 1000) { //update the display every minute
    updateDisplay();
    lastUpdateTime = millis();
  }
}
```
<details><summary>配列</summary>

```cpp:characters.h
const char *characters[] = {
  //January
  "Portgas D. Ace,Mr. 1",
  "Peepley Lulu",
  "Iceberg",
  "Ulti",
  "Strawberry",
  "Oimo",
  "Mozu",
  "Emporio Ivankov",
  "Pickles",
  "Eustass Kid,Otohime",
  "Itomimizu",
  "Dr. Hiriluk",
  "Ikaros Much",
  "Sweetpea",
  "A O",
  "Boodle",
  "Capone Bege",
  "Splash & Splatter",
  "Kozuki Toki",
  "Kinderella",
  "Ms. Friday",
  "Going Merry",
  "Ginrummy",
  "Guild Tesoro",
  "Giolla",
  "Nero",
  "Lola,Charlotte Chiffon",
  "Manshelly",
  "Kinemon",
  "Suleiman",
  "Aladdin",

  //February
  "Broggy",
  "Nefeltari Vivi,Killer,Elizabello II",
  "Sasaki",
  "Fukaboshi",
  "Bista",
  "Nico Robin,Nico Olivia",
  "Butchie",
  "Onigumo",
  "Bartholomew Kuma,Wadatsumi",
  "Page One",
  "Charlotte Flampe",
  "Little Oars Jr.",
  "Nefeltari Cobra",
  "Hyougoro",
  "Charlotte Linlin",
  "Bluegem",
  "Ukkari",
  "Gambia",
  "Bobby Funk",
  "Kumadori",
  "Jigoro",
  "Kozuki Oden,Hamburg",
  "Makino",
  "Genista",
  "Gerd",
  "Raizo",
  "Charlotte Mascarpone,Charlotte Joscarpone",
  "Woop Slap,Charlotte Cracker",
  "Pandaman", //leap day

  //March
  "Minorhinoceros",
  "Vinsmoke Sanji,Vinsmoke Ichiji,Vinsmoke Niji,Vinsmoke Yonji",
  "Mr. 3,Otama,Hina",
  "Minotaurus",
  "Ms. Sadie,Minokoala",
  "Jaguar D. Saul,Charlotte Brulee,Salome",
  "Minozebra",
  "Zanbai",
  "Franky,Shanks,Dracule Mihawk",
  "Sentomaru",
  "Spandam",
  "Patty",
  "Lafitte",
  "Smoker,Alvida,Charlotte Perospero",
  "Who's Who",
  "Tom",
  "Ideaman",
  "Trebol",
  "Apoo",
  "Sabo,Shiki",
  "Spacy",
  "Sapi",
  "Charlotte Raisin",
  "Thatch",
  "Thousand Sunny",
  "Tsuru",
  "Shioyaki",
  "Salou",
  "Catalina Devon,Fukurokuju,Saint Shalria",
  "Manboshi",
  "Baskerville",

  //April
  "Usopp,Kashii",
  "Jinbe,Zeff,Shimotuki Yasuie",
  "Brook",
  "Shirahoshi,Foxy",
  "Minister of the Right,Yamakaji",
  "Edward Newgate",
  "Gin",
  "Clover,Buffalo",
  "Caesar Clown",
  "Shutenmaru",
  "Pekoms",
  "Miss Backin",
  "Morgan",
  "Hordy Jones",
  "Fossa",
  "Strongheart",
  "Charlotte Anana",
  "Hyouzou",
  "Siam",
  "Blueno",
  "Ucy",
  "Kuro",
  "Kalifa,Charlotte Montdor",
  "Stussy",
  "Penguin",
  "Chaka",
  "Streusen",
  "Funkfreed",
  "Ms. Goldenweek",
  "Viola",

  //May
  "Kaido",
  "Monkey D. Garp",
  "Arlong",
  "Koshiro",
  "Monkey D. Luffy",
  "Eneru",
  "Maynard",
  "Shakuyaku,Shoujou",
  "Sengoku",
  "Heracles",
  "Poro",
  "Vinsmoke Judge",
  "Coby,Silvers Rayleigh",
  "Tamago",
  "Baby 5",
  "Purin Purin",
  "Poppy",
  "Gorilla",
  "Kozuki Sukiyaki",
  "Conis",
  "Gotty",
  "DeCalvan Brothers",
  "Choo",
  "Carrot",
  "Big Pan",
  "Koza",
  "Stansen",
  "Catcar",
  "Diamante",
  "Kyueen",
  "Lacuba",

  //June
  "Saint Charlos",
  "Rob Lucci",
  "Musse",
  "Crocus",
  "Jabra",
  "Momonga,Sheeps Hed",
  "Perona",
  "Saldeath",
  "Rockstar",
  "Dalton",
  "Shiryu",
  "Senor Pink",
  "Straw Hat Pirates",
  "Kawamatsu",
  "Rosho",
  "Pedro",
  "Genzo,Toto",
  "Rika",
  "Mr. 4",
  "Amadobu",
  "Ms. Father's Day",
  "Gyro",
  "Orlumbus",
  "Ryuboshi",
  "Charlotte Prin",
  "Zeus",
  "Riku Dold III",
  "Mocha,Saint Roswald",
  "Fukuro",
  "Elmy",

  //July
  "Kab",
  "Minister of the Left",
  "Nami,Neptune",
  "Caribou",
  "Vergo",
  "Lucky Roux",
  "Raki",
  "Paulie",
  "Vinsmoke Sora",
  "Kamakiri",
  "Pearl",
  "Sarquiss",
  "Queen,Kingbaum",
  "Bastille,Morgans",
  "Corazon",
  "Helmeppo",
  "Hammond",
  "Smiley",
  "Prometheus",
  "Giovanni",
  "Kurozumi Kanjuro",
  "Charlotte Praline",
  "Richie",
  "Leo",
  "Nojiko",
  "Mr. 5",
  "Kikyo",
  "Flapper",
  "Kadal",
  "Ms. Doublefinger",
  "Sally Nantocanet",

  //August
  "Urouge",
  "Yasopp",
  "Marshall D. Teech,Inazuma",
  "Rebecca",
  "Gaimon,Vasco Shot",
  "Gladius",
  "Kaku,Bellamy",
  "Buggy,Hacchan",
  "Wapol,Hack",
  "Issho,Gedatsu",
  "Duval,Dellinger",
  "Hajrudin",
  "Sai,Machvice",
  "Jean Ango",
  "Mr. 2",
  "Sakazuki",
  "Pagaya",
  "Wyper",
  "Motobaro",
  "Boo",
  "Pound",
  "Vander Decken",
  "Pell",
  "Kaya",
  "Corgy",
  "Gats",
  "Monet",
  "Hannyabal",
  "Farul",
  "Curry Dadan",
  "Cavendish",

  //September
  "Jewelry Bonney,Kiwi",
  "Boa Hancock",
  "Boa Sandersonia,Brandnew",
  "Wanze,Kumacy",
  "Crocodile,Boa Marigold",
  "Gecko Moria,Squad",
  "Fullbody,Mr. Motzel",
  "Cabaji,Dr. Kureha",
  "Kikunojo,Shyarly,Basil Hawkins",
  "T Bone",
  "Mont Blanc Cricket",
  "Creele",
  "Hoeh",
  "Local Sea Monster",
  "Otsuru,Spoil",
  "Laboon",
  "Kuina",
  "Devil Dias",
  "Krieg",
  "Shura",
  "Kuzan,Pappug",
  "Kyros",
  "Kurozumi Orochi",
  "Blackmaria,Suu",
  "Terracotta",
  "Dagama",
  "Mr. 9",
  "Jack",
  "Charlotte Opera",
  "Avolo Pizzaro",

  //October
  "Uta,Mohji",
  "Dorry",
  "Yama",
  "Oars,Kalgara",
  "Monkey D. Dragon,Van Ogre,Kokoro,Toko,Marco",
  "Trafalgar D. Water Law,Tashigi,Bartolomeo",
  "Kozuki Momonosuke,Lao G",
  "Doberman,Pierre",
  "Magellan,Mont Blanc Noland",
  "Ideo",
  "Inuarashi",
  "Charlotte Smoothies",
  "Izo",
  "Haredasu",
  "Charlotte Compote",
  "Taroimo",
  "Marin",
  "Doc Q",
  "Charlotte Galette",
  "Tyrannosaurus",
  "Vito",
  "Sugar",
  "Donquixote Doflamingo,Shinobu",
  "X Drake",
  "Koala",
  "Denjiro",
  "Kagikko",
  "Scarlett",
  "Charlotte Snack",
  "Domino",
  "Whitey Bay",

  //November
  "Wanda",
  "Konsrot",
  "Yamato,Camie",
  "Kashigami",
  "Fisher Tiger",
  "Shimotsuki Ryuma",
  "Holly",
  "Karoo",
  "Ben Beckman",
  "Yosaku",
  "Roronoa Zoro,Jozu",
  "Jonny",
  "Kingdew",
  "Rydeen",
  "Ririka",
  "Porche",
  "Charlotte Myukuru",
  "Higuma",
  "Gyukimaru",
  "Bepo",
  "Mr. 11",
  "Nekomamushi",
  "Borsalino",
  "Ran",
  "Charlotte Oven,Charlotte Katakuri,Charlotte Daifuku",
  "Goro",
  "Blue Gilly",
  "Tank Lepanto",
  "Cerberus",
  "Vinsmoke Reiju",

  //December
  "King",
  "Napoleon",
  "Bellemere",
  "Jorul",
  "Indigo",
  "Igaram",
  "Ikkaku",
  "Jarul",
  "Kelly Funk",
  "Edward Weevil,Charlotte Amande",
  "Speed",
  "Chinjao",
  "Donquixote Mjosgard",
  "Pica",
  "Charlotte Noisette",
  "Charlotte Moscato,Inuppe",
  "Banchina",
  "Zunesha",
  "Dr. Hogback",
  "Charlotte Marnier",
  "Mother Carmel",
  "Ganfor,Miyagi",
  "Kozuki Hiyori",
  "Tony Tony Chopper",
  "Jesus Burgess,Ms. Merry Christmas",
  "Charlotte Broyer",
  "Akumai",
  "Django",
  "Du Feld",
  "Absalom",
  "Gol D. Roger"
};
```
</details>

# おわりに
画面サイズこそ小さいですが、有機ELディスプレイは自発光するので意外と視認性は良かったです。
# 参考文献
* https://country-programmer.dfkp.info/2021/08/oled_ssd1306_002/
* https://logikara.blog/parts_ssd1306/
* https://daeudaeu.com/c-strtok/#_strtok