Raspberry Piでオリジナル時計を作ってみよう

## はじめに

こちらの記事は[ツクリンクアドベントカレンダー2023](https://qiita.com/advent-calendar/2023/tsukulink)の記念すべき初日の記事です。

会社での取り組みとか紹介になるような記事にしようかと思い、色々ネタを考えたのですが
それよりも自分が楽しんでやれる&今まさにやりたいことの方がモチベが上がると言う
単純な理由で会社とは全く関係無い工作を記事にすることにしました😉

## 何を作るか？

ネタを考えている時にX(旧Twitter)にて以下の投稿を拝見しました。

https://x.com/sozoraemon/status/1728009738489901321?s=20

最近の私は数々の現実に絶望し、目も心も曇った大人になってしまいました。。。

しかし、そんな私の子供心？童心？をくすぐるような時計を見てこの私の曇った眼に一筋の光が差し込みました。

**そう、この綺麗なアースのように。**

こちらのポストのような丸くて素敵なディスプレイは手元に無いですが、
ただ一定時間で画像を切り替えるだけの役割を与えられた地味に高いけどただのお飾りになっているモバイルディスプレイに更なる役割を与えるべく、私は立ち上がりました。

![IMG_3633.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199056/beecc0a3-fef5-66fc-030f-47e3288de3dc.jpeg)

こちらのディスプレイは一定時間で画像を切り替えています。

![IMG_3634.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199056/b55f2c23-036d-4005-aca9-637decf89417.jpeg)

使用しているディスプレイはこちら。

https://www.amazon.co.jp/dp/B089MBSDHD

## 余談(前段)

上記で紹介しました、画像を一定時間で表示する方法はかなりシンプルです。
参考に私が実行しているコードを公開します。

[用意するもの]
- Raspberry Pi (Zero, Pico以外なら行けるはず)
  - 今回はRaspberry Pi 3を使用
- ディスプレイ
- HDMIケーブル
- 表示したい画像(複数枚)

FBIコマンドを使用し、シェルスクリプトを使用して一定間隔で画像を表示しています。

下準備
```sh
sudo apt-get -y install fbi
```

```sh:show-pictures.sh
#!/bin/bash

while true
do
	for i in *jpg; do echo "$i"; sudo kill -9 $(ps aux | grep -v grep | grep fbi | awk '{print $2}'); sudo fbi -a -T 1 -d /dev/fb0 -noverbose "$i"; sleep 30; done
	for image in "./*.jpg"; do
		fbi -a -i "$image"
		sleep 10
	done
done
```

上記のシェルスクリプトと画像(jpg)を同一ディレクトリに配置して以下のコマンドを実行するだけです。

```sh
# 実行権限を付与してから実行
chmod +x show-pictures.sh
./show-pictures.sh
```

## 本題

では早速、あの素敵なポストのような時計を作ってみようと思います。

用意するものは先ほどと同様です。

こちらの記事を参考にされたとのことなので、少々拝見。

>マジックミラーが無くても情報表示にMagicMirror²を使う
https://raspida.com/magicmirror-for-info-board

**MagicMirror2** と言うツールを使用することで結構簡単に実現できるとのこと。

## MagicMirror2とは？

https://magicmirror.builders/

>MagicMirror² (opens new window)is an open source modular smart mirror platform.

Raspberry Piを使用してスマートミラーを作成するためのオープンソースモジュールとのこと。

以下のリンクから動画でコンセプトを見ることができます。

https://vimeo.com/171152845

>MagicMirror² focuses on a modular plugin system and uses Electron (opens new window)as an application wrapper. So no more web server or browser installs necessary!

なんと、Electronを使用しているらしくWebブラウザーやWebサーバーは不要とのこと。

具体的にどんなことができるのかは構築を進めながら解説しようかと思います。

## MagicMirror2のインストール

ドキュメントは別ページでしたので、こちらを参考にセットアップしてみます。

https://docs.magicmirror.builders/

npmが必要なので、事前にNode.jsをインストールする必要があります。
※MagicMirror2はNode.js v18が必要です

<details>
  <summary>Raspberry PiにNode.jsをインストール(nodenvを使用)</summary>

```sh
# nodenvのインストール
curl -fsSL https://github.com/nodenv/nodenv-installer/raw/HEAD/bin/nodenv-installer | bash

# .bash_profileに追記
echo 'export PATH="$HOME/.nodenv/bin:$PATH"' >> ~/.bash_profile
echo 'eval "$(nodenv init - bash)"' >> ~/.bash_profile

# 設定を再読み込み
source ~/.bash_profile

# インストール確認
nodenv -v
# バージョンが表示されれば成功
nodenv 1.4.1+79.15375bb

# Node.jsのインストール
# 今回はMagicMirror2の要件に合わせて18系を使用
nodenv install 18.18.2

# Raspberry Piにバージョンを適用
nodenv global 18.18.2

# 確認
node -v
v18.18.2
```
</details>

Node.jsが準備できたら以下の手順でMagicMirror2をインストールします。

```sh
# リポジトリをcloneする
git clone https://github.com/MichMich/MagicMirror

# cloneしたディレクトリに移動
cd MagicMirror

# インストールを実行
npm run install-mm

# 設定ファイルのサンプルをコピー
cp config/config.js.sample config/config.js

# アプリケーション起動
npm run start
```

実行するとディスプレイにこんな感じで表示されました！
(写真撮影しているおじさんが映っていますが見て見ぬふりをお願いします)
![IMG_3635.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199056/9d99800e-c5ca-ca2f-11bd-019fdfae84c3.jpeg)


インストールのドキュメント見ていたら `MagicMirrorOS` と言う記述を発見したので、Raspberry Piをスマートミラー専用にしたい場合はそちら試してみても面白そうです。

## 設定を確認する

インストールの段階ではサンプルの設定を使用していましたが、では実際どのような設定が書かれているのでしょうか？

こんな感じの内容でした。(長いので一部抜粋)
```js:config.js
/* MagicMirror² Config Sample
 */
let config = {
	address: "localhost",	// Address to listen on, can be:
							// - "localhost", "127.0.0.1", "::1" to listen on loopback interface
							// - another specific IPv4/6 to listen on a specific interface
							// - "0.0.0.0", "::" to listen on any interface
							// Default, when address config is left out or empty, is "localhost"
	port: 8080,
	basePath: "/",			// The URL path where MagicMirror² is hosted. If you are using a Reverse proxy
					  		// you must set the sub path here. basePath must end with a /
	ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],	// Set [] to allow all IP addresses
													/ or add a specific IPv4 of 192.168.1.5 :
													/ ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:192.168.1.5"],
													/ or IPv4 range of 192.168.3.0 --> 192.168.3.15 use CIDR format :
													/ ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:192.168.3.0/28"],

	useHttps: false, 		// Support HTTPS or not, default "false" will use HTTP
	httpsPrivateKey: "", 	// HTTPS private key path, only require when useHttps is true
	httpsCertificate: "", 	// HTTPS Certificate path, only require when useHttps is true

	language: "en",
	locale: "en-US",
	logLevel: ["INFO", "LOG", "WARN", "ERROR"], // Add "DEBUG" for even more logging
	timeFormat: 24,
	units: "metric",

	modules: [
		{
			module: "alert",
		},
		// 長いのでこちら省略
	]
};

/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {module.exports = config;}
```

<details>
  <summary>全設定はこちら</summary>

```js:config.js
/* MagicMirror² Config Sample
 *
 * By Michael Teeuw https://michaelteeuw.nl
 * MIT Licensed.
 *
 * For more information on how you can configure this file
 * see https://docs.magicmirror.builders/configuration/introduction.html
 * and https://docs.magicmirror.builders/modules/configuration.html
 *
 * You can use environment variables using a `config.js.template` file instead of `config.js`
 * which will be converted to `config.js` while starting. For more information
 * see https://docs.magicmirror.builders/configuration/introduction.html#enviromnent-variables
 */
let config = {
	address: "localhost",	// Address to listen on, can be:
							// - "localhost", "127.0.0.1", "::1" to listen on loopback interface
							// - another specific IPv4/6 to listen on a specific interface
							// - "0.0.0.0", "::" to listen on any interface
							// Default, when address config is left out or empty, is "localhost"
	port: 8080,
	basePath: "/",			// The URL path where MagicMirror² is hosted. If you are using a Reverse proxy
					  		// you must set the sub path here. basePath must end with a /
	ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],	// Set [] to allow all IP addresses
													/ or add a specific IPv4 of 192.168.1.5 :
													/ ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:192.168.1.5"],
													/ or IPv4 range of 192.168.3.0 --> 192.168.3.15 use CIDR format :
													/ ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:192.168.3.0/28"],

	useHttps: false, 		// Support HTTPS or not, default "false" will use HTTP
	httpsPrivateKey: "", 	// HTTPS private key path, only require when useHttps is true
	httpsCertificate: "", 	// HTTPS Certificate path, only require when useHttps is true

	language: "en",
	locale: "en-US",
	logLevel: ["INFO", "LOG", "WARN", "ERROR"], // Add "DEBUG" for even more logging
	timeFormat: 24,
	units: "metric",

	modules: [
		{
			module: "alert",
		},
		{
			module: "updatenotification",
			position: "top_bar"
		},
		{
			module: "clock",
			position: "top_left"
		},
		{
			module: "calendar",
			header: "US Holidays",
			position: "top_left",
			config: {
				calendars: [
					{
						fetchInterval: 7 * 24 * 60 * 60 * 1000,
						symbol: "calendar-check",
						url: "https://ics.calendarlabs.com/76/mm3137/US_Holidays.ics"
					}
				]
			}
		},
		{
			module: "compliments",
			position: "lower_third"
		},
		{
			module: "weather",
			position: "top_right",
			config: {
				weatherProvider: "openweathermap",
				type: "current",
				location: "New York",
				locationID: "5128581", //ID from http://bulk.openweathermap.org/sample/city.list.json.gz; unzip the gz file and find your city
				apiKey: "YOUR_OPENWEATHER_API_KEY"
			}
		},
		{
			module: "weather",
			position: "top_right",
			header: "Weather Forecast",
			config: {
				weatherProvider: "openweathermap",
				type: "forecast",
				location: "New York",
				locationID: "5128581", //ID from http://bulk.openweathermap.org/sample/city.list.json.gz; unzip the gz file and find your city
				apiKey: "YOUR_OPENWEATHER_API_KEY"
			}
		},
		{
			module: "newsfeed",
			position: "bottom_bar",
			config: {
				feeds: [
					{
						title: "New York Times",
						url: "https://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml"
					}
				],
				showSourceTitle: true,
				showPublishDate: true,
				broadcastNewsFeeds: true,
				broadcastNewsUpdates: true
			}
		},
	]
};

/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {module.exports = config;}
```
</details>

ざっくり、以下の内容を設定しているようです。

|項目|説明|
|:--|:--|
|port|MagicMirror2を立ち上げるポート|
|address|接続IPアドレス(デフォルトlocalhost)<br>0.0.0.0で外部公開も可能|
|ipWhitelist|アクセスを許可するIPアドレスのリスト|
|zoom|画面のズーム率(デフォルト1.0)|
|language|言語指定(デフォルトen)|
|timeFormat|時間の表示形式。12 or 24(デフォルト24)|
|units|天気モジュールで使用する単位。metric or imperial(デフォルトmetric)|
|modules|使用するモジュールの配列|
|electronOptions|Electronのオプション配列。画面サイズや位置などを設定できる|
|electronSwitches|Electronアプリ自体の設定。<br>通常はあまり使用しないが、高度な設定が可能らしい|
|customCss|スタイルシートのパス(デフォルトcss/custom.css)|

公式のドキュメントはこちら

https://docs.magicmirror.builders/configuration/introduction.html

## 設定を変更してみよう

前の節で設定内容が分かったので、ちょっと設定を変更してみましょう。

日本語に対応しているか分からないですが、手始めに `language` を変更してみます。

```diff:config.js
- language: "en",
+ language: "ja",
```

設定変更後、アプリを再起動してみると

![IMG_3636.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199056/13ef6b4b-ead2-713c-fbbf-f809b42eabe3.jpeg)

全部では無いですが、日本語対応しているみたいです！

## 背景画像を設定してみよう

今のままだと黒い画面に文字が出ているだけなので、背景に画像を表示してみます。

ちょっと調べてみたらMagicMirror用のモジュールを発見したのでこちらを使ってみます。

https://github.com/kolbyjack/MMM-Wallpaper

インストールしてみる
```sh
cd ~/MagicMirror/modules/

# clone
git clone https://github.com/kolbyjack/MMM-Wallpaper.git

# install dependencies
cd MMM-Wallpaper
npm install
```

インストール完了後、設定を追加します。

```js:config.js
// modulesの配列に追加する
modules: [
  {
    module: "MMM-Wallpaper",
    position: "fullscreen_below",
    config: {
      source: "bing", // とりあえずbingで試してみる
      slideInterval: 10 * 1000 // サンプルは1分だけど早く見たいので10秒に設定
    }
  }
]
```

設定追加後、アプリを再起動すると無事に10秒ごとに背景画像が切り替わりました。

![IMG_3637.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199056/2f88b2ff-fbef-a0f8-dbe7-7fe37a92237f.jpeg)

![IMG_3638.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199056/27019b82-dd5c-7978-fc42-43e6ef0311d2.jpeg)

ちなみに `source` には以下の内容を設定可能みたいです。

|設定値|説明|
|:--|:--|
|"apod"|NASA の「Astronomy Picture of the Day」から毎日最新の壁紙を循環(標準解像度)|
|"apodhd"|NASA の「Astronomy Picture of the Day」から毎日最新の壁紙を循環(高解像度)|
|"bing"|Bingから毎日最新の壁紙を循環|
|"chromecast"|Chromecastの壁紙をランダムに選択して循環|
|"firetv"|FireTV の壁紙をランダムに選択して循環|
|"flickr-api:\<source>"|指定されたflickrの写真をランダムに選択して循環|
|"http(s)://url"|指定されたURLを設定された間隔でリロード|
|"icloud:\<album id>"|指定したiCloudのアルバムをランダムに選択して循環|
|"lightroom:\<user.myportfolio.com/album>"|指定したLightroomのアルバムをランダムに選択して循環|
|"local:\</path/to/directory>"|指定されたローカルディレクトリ内のイメージをランダムに選択して循環|
|"synology-moments:\<url>"|指定されたSynology Momentsアルバムから最新の画像を循環|
|"/r/\<subreddit>"|subredditからの最新のホットな画像投稿を循環|
|"/user/\<username>/m/\<subreddit>"|指定されたmultiredditからの最新のホット画像投稿を循環|
|"metmuseum:\<departmentID>,\<isHightlight>,\<q>"|メトロポリタン美術館のコレクションを循環|
|"nasa:\<search term>"|NASA の画像およびビデオライブラリの検索用語で指定された画像を循環|

一番最後の `nasa` はもしかして、あの素敵なポストのやつでは？
`local` を使用すれば余談で使用していた画像も選択可能なことは分かりました。

`apod` と `apodhd` はNASA関連の画像表示には `nasaApiKey` が必要みたいです。

では、元々の目的であった「オリジナル時計」にするためにローカルの画像を試してみます。

以下の設定に変更します。
指定するディレクトリは画像を配置している場所を指定してください。
```js:config.js
modules: [
  {
    module: "MMM-Wallpaper",
    position: "fullscreen_below",
    config: {
      source: "local:/home/username/Pictures", // フルパスじゃないとダメっぽい(~/は使えなかった)
      slideInterval: 10 * 1000
    }
  }
]
```

元々表示していた画像を背景に設定しました！

![IMG_3640.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199056/96864dcf-1c61-6fb4-8891-639d05fac93e.jpeg)

ちょっと鮮やかさに欠けるけど、とりあえず実現は完了。

![IMG_3639.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199056/bab8e5c3-cb45-21d0-ba11-97866fc28450.jpeg)


## もう少しカスタマイズしてみよう

今のままでも素敵ですが、時計の文字盤が小さいのでサイズを変更してみようと思います。

まずは公式のドキュメントを拝見。

https://docs.magicmirror.builders/modules/clock.html

サイズのオプションは存在しないようです。

`displayType` でアナログ・デジタルの指定ができるのと
アナログの場合は `analogSize` で簡単にサイズ指定ができそうです。

ちょっと試してみました。
```js:config.js
modules: [
  {
    module: "clock",
    position: "top_left",
    config: {
      displayType: "both", analogの他に両方があったので設定
    },
  },
];
```

![IMG_3653.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199056/a9b7da51-2487-becf-ba3e-80579a91d651.jpeg)

アナログ時計が表示されました！
試しにサイズ変更してみます。

```diff:config.js
modules: [
  {
    module: "clock",
    position: "top_left",
    config: {
      displayType: "both",
+     analogSize: "400px",
    },
  },
];
```

![IMG_3654.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199056/817f272c-0e9e-ed69-015f-d7ed3089a230.jpeg)

かなり簡単にサイズ変更できましたね。
でも、デジタル時計を大きくしたいです。

### 直接スタイルを変更してみる

デジタル時計のサイズ変更のプロパティが存在しないようなので、だったら直接スタイルを変更する方式を取ってみます。
※邪魔なのでこの時点でアナログ時計は消しました。

`package.json` の中身を読んでみると、開発用のモードがあるようです。

```sh
npm run start:dev
```

上記のコマンドを実行してみると、ブラウザのデベロッパーツールと同様のウィンドウが表示された状態の画面になりました。

![IMG_3644.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199056/7bba682c-05b7-2ce7-59d8-3cecba643e62.jpeg)

ドキュメントを確認したところ、 `Clock Module` が対象みたいなのでこちらのソースを参考にスタイルを修正してみます。

https://github.com/MichMich/MagicMirror/tree/master/modules/default/clock

`Clock Module` 自体は以下の構造になっていました。
※一部改変していますが大体以下のイメージ
```html
<div class="clock">
  <!-- アナログ時計の場合 -->
  <div class="clock-circle"></div>
  <!-- デジタル時計の場合 -->
  <div class="digital">
    <div class="date">日曜日,2023年11月26日</div>
    <div class="time">20:50</div>
  </div>
</div>
```

モジュールのソースコードではJSでDOMの生成を行なっているみたいなので、デベロッパーツールで見た方が早かったです。
一応こんな感じ。

```js
/************************************
 * Create wrappers for analog and digital clock
 */
const analogWrapper = document.createElement("div");
analogWrapper.className = "clock-circle";
const digitalWrapper = document.createElement("div");
digitalWrapper.className = "digital";
digitalWrapper.style.gridArea = "center";
```

日付は `.clock .date` で取得可能
時間は `.clock .time` で取得可能なことが分かりましたので以下のようにフォントサイズを変更してみます。

変更方法は `css/custom.css.sample` をコピーして `css/custom.css` を作成し、そちらにスタイルを設定するだけです。

```css:custom.css
.clock .date {
  font-size: 100px;
}

.clock .time {
  font-size: 100px;
}
```

![IMG_3655.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199056/43deb646-cb4c-a317-af5b-31417d29e1e5.jpeg)

バランスは悪いですが、かなり大きくすることができました！
あとはお好みで調整すればいい感じになりそうです。


## 最後に

本当は会社のアドベントカレンダーなので会社での取り組みとか書くべきかと思ったのですが、自分が楽しんでやれる&今まさにやりたいことを優先して記事にしてみました。

ツクリンクは基本的にフルリモートなので毎日同じ仕事部屋の景色を見ることになります。
(一応出社も可能です！)

そんな中でも、時間を確認しつつ、このような素敵な世界の景色を見ることができるので、これは私にとって革命です。

適度にリフレッシュしつつ、ちょっとオシャレになった仕事部屋の自作時計を見てニヤニヤしながらまた仕事に励もうと思います。

最後までありがとうございました！

![IMG_3643.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199056/0f70d922-19d7-e3c9-0413-8675bf2043ba.jpeg)

## 追記

自動起動を入れると便利なので、入れたい場合は以下のリンクを参照してみてください。
私はPM2で自動起動するようにしてみました。
https://docs.magicmirror.builders/configuration/autostart.html