年賀書きたくないマン.
Table of Contents
一年の計は元旦にあり
**あけましておめでとうございます。**昨年は大変お世話になりました。本年もどうぞよろしくお願いします。
さて、お世話になったみなさまに年賀状を送りたいのですが、私は年賀状を書くのが苦手です。
というより、まず手書き文字が苦手なのと、文面を考えるのが苦手なのと、郵便局に行くのがめんどくさいのと、滅多にポストをあけないのが原因なのですが、
ともあって今年も年賀状を締め切りまでに出すことができませんでした。
なんというダメ人間。
年賀状って以下の点が辛いんですね。
- はがきを書ってこなければ行けない
- 専用のソフト、プリンターがないと一枚一枚手書きで辛い
- 郵便局に出しいかないといけない
- 宛先違うだけでたくさん刷らないといけない
年賀ハガキクジが当たらない
ということで、せっかくなのでこれらのお困りごとを解決するツールをさっそく作ってみることにしました。
注意
今回の記事ですが、やたら~BOXというワードが出てきます。
大意はないのですが、筆者が年末にみたとあるネット記事がマイブームになってしまい、使わざるを得えませんでした。
読み苦しい限りですがおつきあいくださいませ。
年末年始はやってみようBOX
年末年始という長期休暇は普段忙しくてできないアレコレを入れてあるやってみようBOXをあける日です。
仕事のことは考えてもしょうがないBOXです。
ということでこちらのツールですが先に技術選定からしていきます。
MQTT
MQTTとはMessage Queue Telemetry Transportの略で、pub/subモデルという仕組みに基づいてつくられた軽量なメッセージプロトコルです。
MQTTの開発元であるIBMに良記事MQTT の基本知識がありましたので詳細は割愛しますが、
ヘッダーサイズが最小2byteと軽量なプロトコルであること、同一Topicに対して多対多の通信ができること、ネットワーク品質に応じたQOSを設定できること、ペイロード制約がほぼないことが受けて広くIoT分野で使われています。
ここまでお勉強してこなかったのはIoTに知見がなさすぎたことと、AWS IoTを使ってみたいという気持ちがある一方、まずはMQTTからお勉強しないとという変なこだわりがあったためです。
今回電子ペーパーを実装した端末がRaspberry Piということもあり、コア技術はMQTTで行くことにしました。
React Hooks
React Hooksも今更?感ありますが、Reactのお勉強はGatsby.jsでのブログ実装以来やっていないので、ちょっと動向に触れることにしました。
昔、使ってみようかと少し記事も見てみましたことがあったのですが、クラスコンポーネントの何が悪いのかイマイチよくわかってないので多分クラスコンポーネントで機能がとっ散らかるくらい大規模にReactを触ってないのであまり関心事にならなかったため、使ったことがありませんでした。
使わないにしても知らないと職がさすがに無くなりそうなのでお勉強することにしました。
Tailwind CSS
TrySailのアルバムにTailwindというものがありましたが、Tailwind CSSもよく聞くので使ってみることにします。
軽い、という話を最初に聞き、使ってみたいと思ってましたが、必ずしもそういったこともなくUtility-Firstに共感できるWeb開発者に受けているんだろうな〜と重い腰が上がりませんでした。
が、こちらもそろそろ勉強しないといよいよ無職になるので勉強します。
ちなみに、Tailwindとは追い風という意味だそうです。
電子ペーパー
本当はM5Paperとかやりたかったのですが、電子ペーパーでなにか作ろうと思ったときにはスイッチサイエンスで売り切れていたのでAmazonで買える電子ペーパーでお安かったWaveshare 2.7inch E-Ink Screen Display HAT for Raspberry Piを購入することにしました。
やらないことにしようBOX
色々選定時迷いましたが、一度にたくさんのことをやり過ぎるとやれなくてもやもやBOXに入ってしまうので元旦までに実装完了させるためにも、泣く泣くやらないことにしました。
いつかやるBOXだ。
- RustのWAF
MQTTのクライアントでRustで使えそうなものがぱっと見つからなかったため断念- 大嘘です。ありました。しかもpaho https://github.com/eclipse/paho.mqtt.rust
- BackendはPythonのFastAPIで実装します。
- AWS IoT
- 本当は使いたかったが、金がない&MQTTブローカーを一から作ってみたかったため断念、いつかやることにしました。
- Wasm
- 毎回少しやって挫けるので今回はスキップ。いつかやりたいきりたいですね。
- Recoil
- 流行っているのか怪しかったからパス。ファーストペンギンはいつだって怖い。
アーキテクチャー
30秒くらい考えてこんな感じになりました。考えてもしょうがないBOXです。
フロントからAPIコールされるとバックエンドサーバーで画像生成し、ByteArrayに変換後、MQTTブローカーにPublishをします。ByteArrayがMQTTのペイロードに設定できるのはHTTPと比べて魅力ですねー。
MQTTブローカーでは受け取ったメッセージを同一TopicをSubscribeしているSubscriberに投げます。
ここでMQTT over WebSocketなどでTopicに流れるメッセージを待ち受けていれば、MQTT同様にSubscribeできます。
Subscriberではメッセージを受け取るとon_messageイベントが発生するため、受け取ったByteArrayから画像を再生し、電子ペーパーの制御モジュールに渡します。
辛かったこと
Hooksわかんねぇ!型チェックとおらねぇ!!
これは考えてもしょうがないBOXなのかもしれませんが、React HooksでFileを使うの、難しくありません?たんに画像をBase64にしてaxiosでAPIコールしたいだけなのですが、
export const App: React.FC = () => {
const [selectedFileName, setSelectedFileName] = React.useState(null)
とinitial stateをnullにすると、
TS2345: Argument of type 'null' is not assignable to parameter of type 'File'.
という感じに、nullが許容できないですし、
<input type="file" accept="image/jpeg"
className="cursor-pointer relative block opacity-0 w-full h-full p-20 z-50"
onChange={async(e) => {
setSelectedFile(e.target.files)
}}/>
肝心のinputのonChangeでset関数呼ぼうとすると、
TS2345: Argument of type 'FileList | null' is not assignable to parameter of type 'SetStateAction<File>'. Type 'null' is not assignable to type 'SetStateAction<File>'.
と言われてしまい、いざFileReaderでreadAsDataURLしようとすると、
let b64str: string | ArrayBuffer | null = "";
let reader = new FileReader();
reader.readAsDataURL(file);
return new Promise((resolve, reject) => {
reader.onload = (e) => {
console.log(e.target)
b64str = e.target.result
resolve(b64str)
};
reader.onerror = (error) => {
console.log('Error: ', error);
return reject(error)
;
TS2531: Object is possibly 'null'.
という風にnull避けに苦戦しました。
結局のところ、
export const App: React.FC = () => {
const initFile: File = new File([], "")
const [selectedFile, setSelectedFile] = React.useState(initFile);
という具合にinitial stateを空Fileオブジェクトにして、かついたるところに、
reader.onload = (e) => {
console.log(e.target)
b64str = e.target !== null ? e.target.result : ''
resolve(b64str)
};
という具合に三項演算子でガードする羽目になりました。クラスコンポーネントでももちろん考えないとなのですが、型厳密おじさんではないので、stateの段階で型でFile | nullみたいなこと平気でしてでnull許容にしてしまうマンなのでこれは痛かったですね。
教えてReact博士!
どうしたら、楽にHooksが使えますか?
知らね。自分で考えろ
Tailwind結局つかいこなせない問題
Webデザインというか、そういった素養がなさすぎて、むしろ私にはMaterial UIの出来きったデザインのもと実装できるCSSフレームワークのほうが使いやすかったです。
ほぼすべて、結局Tailwind ComponentからコピってReact向けに直して、ちょっと修正して、で作ったのでUtility-Firstの恩恵なしで時間ばかりかかってしまいました。
思い通り進んだところ
FastAPI
FastAPIの実装はスムーズでした。
APIを作るときもデコレーター1つで簡単実装です。
from fastapi import FastAPI
from pydantic import BaseModel
class Message(BaseModel):
title: str
message: str
image: str
name: str
app = FastAPI()
@app.post("/preview")
def preview(message: Message):
base64str = create_card_image_b64(message.title, message.message, message.image, message.name)
return {"image": base64str}
簡単なAPIを短時間で作らなければならないようなハッカソンや1dayスプリントなんかにはとてもいいのではないでしょうか?
癖があるとしたらstaticsファイルのホスティングに使うStaticFilesというモジュールを使うときになぜか、追加でaiofilesというライブラリーが必要なことです。
デフォルトでいれないんかい。
とはいえ、しっかりエラーでModuleNotFoundエラーが出るのでそこまで困ることもないです。
公式ドキュメントに、
First you need to install aiofiles
って書いてありました。すみません。
MQTT
くっそ難しいのかな?と思いましたがpahoがいい感じにラップしていてくれるので、何のことはなかったです。
mosquitto
こちらもあっさりでした。
Dockerを使ったからか、ほぼ公式通りのconfigで通りました。
できあがったもの
できあがったものの、ソースコードはこちらにあります。
https://github.com/tubone24/mqtt-nenga
一応EKSにのっけて公開しようかと悩みTerraformまで作ったのですが、誰もサービス使わないで年賀状が一通もこないのも悲しいので、Terraformの検証が終わったら壊しました。
というかEKSに載っけるなら最初からAWS IoTにすればよかったですね。
考えてもしょうがないBOXに入れておいて、自分のやるべきことをやるように考えていますね。
デモ
環境構築方法はREADMEをご参照くださいませ。
使い方としては、まずWebページに行きますと次のような入力フォームがあります。
名前、タイトル、メッセージと画像を入力フォームに入れていきます。Previewボタンを押すと相手に送信する年賀状のプレビューができます。
問題なければ、Submitします。
相手がTopicをSubscribeしていれば、このように電子ペーパーに年賀状が描画されます。