ynakano1127のアイコン画像
ynakano1127 2021年02月28日作成
セットアップや使用方法 セットアップや使用方法 閲覧数 3235
ynakano1127 2021年02月28日作成 セットアップや使用方法 セットアップや使用方法 閲覧数 3235

電子ペーパーでなろう小説を読めるようにする

はじめに

2020年11月27日にM5Stackから新しいデバイスが発売されました.
それはM5Paperです.M5Stackのシリーズはスイッチサイエンスから発売されますが,翌日には売り切れていました.

https://www.switch-science.com/catalog/6749/
キャプションを入力できます

去年のアドベントカレンダー等にてすでに取り上げている人がいますが,応用してみたというのは見当たりませんでした.

そんなM5Paperでつかってなろう小説を読めるようにしようというのが目標です.

つくったもの

最初に作ったものを見せようと思います.

起動するとこんな感じの画面がでて,
キャプションを入力できます

「データを入れる」を押すとこんな画面が出ます.
キャプションを入力できます

「Narou-Paper」というSSIDのAPが立ち上がってWebServerに接続できます.
キャプションを入力できます

ブラウザでサーバーにアクセスすると,こんな画面がでます.この画面で作品データを画像としてアップロードします.
キャプションを入力できます

ホーム画面に戻り,「なろうを読む」を押すとこんな画面がでます.
キャプションを入力できます

話数を選択するとこんな画面が出てきて,先ほど追加した小説が読めます.
キャプションを入力できます

実装

作成したプログラムはGithubに公開しています.実装のすべてはそこに掲載されています.
https://github.com/narou-paper/M5Paper

以下では主要な実装についてまとめていきます.

なお,全体的な構成はこんな感じになっています.
キャプションを入力できます

縦書き画面

今回のこだわりポイントとして,「縦書き」があります.上記の構成図にてパソコンから画像ファイルが渡されているのがお分かりいただけるかと思います.M5Paper側で縦書きのレンダリングをするのが難しそうだっただったのでこのようになりました.コンテンツを文字として送信し,micropythonでPillowのような外部ライブラリを使う?だとかCでレンダリングを一から実装する?というのも考えましたが,手っ取り早く実装できるのを第一にこのような設計にしました.

さて,パソコン側でどのようになろう小説を縦書き画像に変換するかですが,HTMLとCSSを用います.
https://tategaki.github.io/explan1.html

縦書き.css(CSSWritingModesの仕様解説より)

body { writing-mode: vertical-rl; }

CSSではこのような指定だけで,縦書き表記になります.今回はこれを活用しました.

以下のid=novel_colorのdiv要素をなろうのコンテンツのものに置き換え,PDFを印刷物として出力するといい感じのファイルが出来上がります.

narou-paper.html

<html lang="ja"> <head> <!-- link href="template.css" rel="stylesheet" type="text/css" --> <meta charset="utf-8"> <title>title</title> <style> html { margin: 0; } body { margin: 0; } #novel_color { writing-mode: vertical-rl; text-orientation: mixed; inline-size: 100vh; block-size: 100vw; display: flex; flex-direction: column; flex-wrap: wrap; font-size: 4vh; } #novel_no,.novel_bn { display: none; } .novel_subtitle { display: block; font-size: 3em; block-size: 95vh; text-align: center; } #novel_honbun, #novel_p, #novel_a { inline-size: 100vh; block-size: 100vw; display: flex; flex-direction: column; flex-wrap: wrap; } p { display: block; color: black; font-family: 'Noto', serif; margin: 0; inline-size: 95vh; padding-inline: 2.5vh; padding-block: 0; } </style> </head> <body><div id="novel_color"> <div class="novel_bn"> <a href="/aaaaa/aaaaa/">&lt;&lt;&nbsp;前へ</a><a href="/aaaaa/aaaaa/">次へ&nbsp;&gt;&gt;</a></div><!--novel_bn--> <div id="novel_no">5/32</div> <p class="novel_subtitle">たいとる</p> <div id="novel_honbun" class="novel_view"> <p id="L1">ああああああああああああああああああああああああああああ</p> <p id="L2"><br></p> <p id="L3">ああああああああああああああああああああああああああああ</p> <p id="L4"><br></p> <p id="L5">ああああああああああああああああああああああああああああ</p> <p id="L6"><br></p> <p id="L7">ああああああああああああああああああああああああああああ</p> <p id="L8"><br></p> <p id="L9">ああああああああああああああああああああああああああああ</p> <p id="L10"><br></p> <p id="L11">ああああああああああああああああああああああああああああ</p> <p id="L12"><br></p> <p id="L13">ああああああああああああああああああああああああああああ</p> <p id="L14"><br></p> <p id="L15">ああああああああああああああああああああああああああああ</p> <p id="L16"><br></p> <p id="L17">ああああああああああああああああああああああああああああ</p> <p id="L18"><br></p> <p id="L19">ああああああああああああああああああああああああああああ</p> <p id="L20"><br></p> <p id="L21">ああああああああああああああああああああああああああああ</p> <p id="L22"><br></p> <p id="L23">ああああああああああああああああああああああああああああ</p> <p id="L24"><br></p> <p id="L25">ああああああああああああああああああああああああああああ</p> <p id="L26"><br></p> <p id="L27">ああああああああああああああああああああああああああああ</p> <p id="L28"><br></p> <p id="L29">ああああああああああああああああああああああああああああ</p> <p id="L30"><br></p> <p id="L31">ああああああああああああああああああああああああああああ</p> <p id="L32"><br></p> <p id="L33">ああああああああああああああああああああああああああああ</p> <p id="L34"><br></p> <p id="L35">ああああああああああああああああああああああああああああ</p> <p id="L36"><br></p> <p id="L37">ああああああああああああああああああああああああああああ</p> <p id="L38"><br></p> <p id="L39">ああああああああああああああああああああああああああああ</p> <p id="L40"><br></p> <p id="L41">ああああああああああああああああああああああああああああ</p> <p id="L42"><br></p> <p id="L43">ああああああああああああああああああああああああああああ</p> </div> <div class="novel_bn"> <a href="/aaaaa/aaaaa/">&lt;&lt;&nbsp;前へ</a><a href="/aaaaa/aaaaa/">次へ&nbsp;&gt;&gt;</a><a href="https://ncode.syosetu.com/aaaaa/">目次</a></div> </div> </body> </html>

キャプションを入力できます
キャプションを入力できます
画像化し,サイズを合わせる(540x816)と準備万端です.

デバイス側

ホーム画面から各画面に遷移したり,各画面から左上のボタンを押してホーム画面に戻るといった処理をする必要があります.しかし,ライブラリにて用意されている関数群は普通のLCDのように「線を引く」「テキストを書き込む」といったプリミティブなものしかありません.

https://github.com/m5stack/M5EPD/blob/main/examples/Basics/HelloWorld/HelloWorld.ino

HelloWorld.ino

#include <M5EPD.h> M5EPD_Canvas canvas(&M5.EPD); void setup() { M5.begin(); M5.EPD.SetRotation(90); M5.EPD.Clear(true); M5.RTC.begin(); canvas.createCanvas(540, 960); canvas.setTextSize(3); canvas.drawString("Hello World", 45, 350); canvas.pushCanvas(0,0,UPDATE_MODE_DU4); } void loop() { }

そこで,高級なGUIを実現するために,M5Paper_FactoryTestをベースに開発しようと思います.なお,MITライセンスです.

https://github.com/m5stack/M5Paper_FactoryTest

各画面はframe_bace.cppを継承し,epdgui.cppにて管理されるといった構成になっています.

このクラス群を活用し以下のページとページ遷移を実現しました.

  • ホーム画面
  • 小説リスト
  • 各小説の話数選択画面
  • 小説を読む画面
  • Webサーバーを立ち上げる画面

Webページ

ESP32はAP(とDHCPサーバー)を立て,そのサブネット内からアクセス可能なWebServerを立ち上げることができます.

https://github.com/espressif/arduino-esp32/blob/master/libraries/WiFi/examples/WiFiAccessPoint/WiFiAccessPoint.ino

https://github.com/espressif/arduino-esp32/blob/master/libraries/WebServer/examples/FSBrowser/FSBrowser.ino

電力消費が激しいので,M5Paperが起動している間ずっとAPを立てるということはせず,必要な時に画面を開くような仕様にしました.

と,まあ,サンプルのように書けば任意の静的ファイルを提供できるのです.一般的に(周りの人とかインターネットの人とかがやってる限り)生のHTMLや生のJSを書いてSPIFFSにぶち込むらしいですが,面白くないので,今回はVueCLIをつかって静的ファイルを生成しようと考えました.

VueとVueCLIとVuetify

最近のフロントエンド技術はずいぶん進歩したもので,HTMLを直に使うという事はなくなっています.(もちろん,基本ではありますし,使うサイトだって十分にあります)
今回はESP32でホストするということで,直書きしたHTMLを使用しても良いと思ったのですが,その流れにのってVueを使おうと思いました.
https://jp.vuejs.org/index.html
https://cli.vuejs.org/
https://vuetifyjs.com/ja/

ESP32に入れるための工夫

EPS32がいくら高級なマイコンだからといって無限にストレージがあるわけではありません.実際,VueCLIから出力したものをSPIFFSに入れてWebServerへGETリクエストをすると変な挙動になり,とても使えたものではありませんでした.
そんな問題を解決するため,VueCLIのWebpackの設定で圧縮するようにしてあげます.

https://github.com/narou-paper/M5Paper-Web/blob/main/vue.config.js

vue.config.js

const CompressionPlugin = require('compression-webpack-plugin'); module.exports = { publicPath: "", configureWebpack: { plugins: [ new CompressionPlugin() ], output: { filename: "[name].js", chunkFilename: "[name].js" } }, css: { extract: { filename: "[name].css", chunkFilename: "[name].css" } }, transpileDependencies: [ "vuetify", ], productionSourceMap: process.env.NODE_ENV === 'production' ? false : true, }

ソースコード

一番重要なアップロードに関するコンポーネントを以下に記します.
Vueを使ってるのにformの機能を使ってるのはあまり関心できませんが,本質ではないので時間優先で実装しました...
アップロードに関して,こちらのページがとても参考になりました.

Upload.vue

<template> <v-container> <v-row class="text-center"> <v-col class="mb-4"> <h1 class="display-2 font-weight-bold mb-3">Welcome to ほっとここあ</h1> <p class="subheading font-weight-regular"> あああああああああああああああああああああ, <br />なんかいい感じの説明 <a href="https://youtu.be/r78ZX-_fDds" target="_blank" >なんかいい感じのリンク</a > </p> </v-col> <v-col cols="12"> <v-form action="/" method="post" enctype="multipart/form-data" > <v-text-field name="title" label="title" /> <v-text-field name="episode" label="episode" /> <v-text-field name="subtitle" label="subtitle" /> <v-file-input @change="changeImage" @click:clear="pictures.splice(0)" accept="image/*" truncate-length="15" name="file" label="files" multiple show-size /> <v-btn type="submit" color="primary" block depressed> UPLOAD </v-btn> </v-form> </v-col> <v-row> <v-col v-for="(picture, index) in pictures" :key="index" class="d-flex child-flex" cols="4" > <v-img :src="picture" /> </v-col> </v-row> </v-row> </v-container> </template> <script> export default { name: "Upload", data() { return { pictures: [] }; }, methods: { // https://qiita.com/itoshiki/items/511d58b827f4ce2129fc async changeImage(files) { // this.picture = await this.getBase64(files[0]) for (let i = 0; i < files.length; i++) { const picture = await this.getBase64(files[i]); this.pictures.push(picture); } }, getBase64(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => resolve(reader.result); reader.onerror = (error) => reject(error); }); } } }; </script>

HTTPメソッドはPOSTでmultipart/form-dataをペイロードとして乗っけます.複数ファイルを一つのリクエストに乗せることが可能だからです.一つのリクエストに対して一つの作品の一つの話数が対応します.もちろん,M5Paper側でもそのようなソースコードを書いています.
https://developer.mozilla.org/ja/docs/Web/HTTP/Methods/POST

multipart/form-dataを使用したリクエスト例(mozillaのサイトより)

POST /test HTTP/1.1 Host: foo.example Content-Type: multipart/form-data;boundary="boundary" --boundary Content-Disposition: form-data; name="field1" value1 --boundary Content-Disposition: form-data; name="field2"; filename="example.txt" value2 --boundary--

おわりに

画像生成Android用アプリケーション

というわけで,なろう小説を電子ペーパーで,しかも縦書きで!表示するのに成功しました.
しかしながら,画像を準備するのが大変に感じた方も多いかと思います.
https://github.com/narou-paper/flutter-app
にて作品をスクレイピングし,画像化まで行うflutter製アプリケーションを公開していますので,どうぞご覧ください.
キャプションを入力できます
キャプションを入力できます

今後の課題

作品としては一旦終了ですが,根本的なものから細かいものまで,大量の課題が残されています.

  1. 画像として扱っているので,ページ遷移が異様に遅い
  2. 画像として扱っているので,送受信が異様に遅い
  3. M5Paperがインターネット接続のないAPになっており,Androidから接続したとしても,モバイル回線に接続した状態だとIPを打ち込んでも応答がない(恐らくAndroid側のルーティングがおかしなことになっているのでしょう)
  4. M5Paperに送る画像サイズ(540x816)を合わせないと表示したときにおかしくなる
  5. しばらくつかってると落ちる(メモリリーク?)
  6. Andriodアプリケーションで生成される画像のフォントは機種の設定に依存する

1,2個めはM5Paper側でレンダリングする.3個めはHTTPではなく,Bluetoothを使う.4,5,6個目は頑張る.という解決方法が思いつきます.(やるとは言っていない)
つよつよな方々,組み込み開発ちょっと分かる方,M5Paper側の縦書きレンダリング等のコントリビュートお願いします.

ログインしてコメントを投稿する