Moleskine ハードカバーノートブックを製本テープで補修する

ノートとして Moleskine を愛用しているのだが、持ち運びすぎて背が破れてしまった(写真参照)。

ニチバンの製本テープが強度・質感ともにぴったりだったので記録のために記事にする。

こんなかんじで貼ってあげて、

完成。見た目もぜんぜん悪くない。

『出会って4光年で合体』感想

はじめに

「ものすごいSF漫画がある、ただし18禁ロリもの」と信頼できる筋がTwitter上に書いているのを見つけ、即座に『出会って4光年で合体』を購入し、読み、衝撃を受けた。感想を書かずにはいられないと感じたため、書く。ネタバレをせずに書くことは至難なので、以降にはネタバレがある。ネタバレを見ずに本作を読むことは、ネタバレを見てから本作を読むことに比べて1000円以上の価値があるので、未読のかたはここでいったん引き返して本作を読んでいただければ実質無料で読めるということになるのでオススメである。

www.dlsite.com

本作はどのような作品か

人一倍の優しさを持ったさえない醜男と、傾国レベルの美少女との、ひと夏のラブストーリー……をベースにした超弩級伝奇SF18禁ロリエロ漫画です。いや、むしろ逆で、超弩級伝奇SF18禁ロリエロ漫画をベースにした醜男と美少女とのひと夏+αのラブストーリーです。

宇宙を舞台にした壮大な謎と、ひと組のカップルのちいさな愛との対比。宇宙が孕む矛盾、生命が孕む矛盾、でもそれらと二人の愛とは全く関係ない! という、清々しいまでに純愛を突き詰めたエロ漫画です。汚いおじさんと美少女とのエロシーンで泣いたのは初めてだよ。

本作のどこが白眉か

なんといっても作品のスケールが凄まじい。それはSF部分のテーマが宇宙規模であるという意味ではなく、伝奇やSF、青春、エロといった要素を複雑に絡み合わせ、それぞれを分離させずに融合させ、かつ整合性を持たせ一読で理解可能なワクワクするストーリーに仕上げている作者の技量に感嘆するしかない、という意味である。長い、という感想もあるが、他の作品が5分冊ぐらいで描き出す世界観を382ページで描いているのは、むしろよくこのページ数でまとめたなという印象がある。それほどまでに濃密な作品である。

また、SFとしても陳腐さを感じさせない。『幼年期の終わり』との類似性を指摘する声がいくつかあり、たしかに似てはいるが、本作では幼年期にいたのは人類ではなくむしろ人類は地球に置いていかれる側となっている。しかしそれでも人類の未来に希望を感じさせる終わり方になっている点は目新しく感じられる。

本作は「ハード純愛もの」である

加えて、本作最大のポイントは、本作の純愛が「ハード純愛もの」である点であろう。

本作はいわゆる「セカイ系」とは微妙に異なる。「セカイ系」や「エモSF」のクリシェとして「二人の愛が世界を救う」というテーマがあるが、本作では主人公もヒロインも、世界に対して直接的には変化を与えない。二人に影響された周囲の人間の行動により世界は変化するが、世界はべつに救われはしない。その意味で本作は(とくに主人公とヒロインの間では)ただのエロ漫画である。橘はやとは徹頭徹尾「ふつうの人間」として描かれており、橘はやとが「ふつうの人間」から外れるところがあるとすれば、それは「底抜けに優しい」点である。なかなかうまく生きられない、しかし人一倍優しいふつうの人間に訪れたひと夏の愛が本作では描かれている。

ただし、ここまで書いてなお、このような作品は多く存在する。そして本来であれば、私はこういう作品は苦手である。なぜならば、描かれる「愛」というものにどうしても「しゃらくささ」のようなものを感じてしまうからである。しかし本作にはそれが感じられなかった。なぜか? 一つの理由として、本作が愛の帰結として起こるセックスを大手を振って詳細に描いているから(つまり、本作がエロ漫画だから)ということが挙げられると思う。たとえば、もし物語の最終段階で二人が救助されるために行為を行う場面が「二人が寝室に入っていって、そこで場面が暗転し、次のページで二人が抱き合っているところに宇宙船がやってくる」みたいな構成であったとしたら、私は「しゃらくささ」を感じてしまっていたと思う。そこを本作では、誤魔化すことなく、40ページ以上を費やして行為自体をしっかりと描いてくれる。ゆえにその後の帰結としての救出を抵抗なく受け入れることができるのである。その意味でこの作品はハードSFであると同時に(純愛を誤魔化しなく描いているという意味で)「ハード純愛もの」であり、ハード純愛ものを描こうとすると必然的に18禁にならざるを得ない。

つまりこれまでのエモSFにはエロシーンが足りなかったんだよ!(暴言)

ただしこれは、「全年齢版を出せない」ということにも繋がる。いや出せるかもしれないし、全年齢版に変換しても濃密な作品になるだろうが、それでは本作のテーマがぼけてしまう。残念だが18歳未満の方々は18歳の誕生日までにこの記事の記憶を消して、誕生日に本作を買って読んでください。

なぜここまでクリエイターの心を掴むのか

本作が私の心を掴んだ理由は前述の通りであるが、それに加えて Twitter では、おもにクリエイター層から「一人の人間がこれを作ることができるのがすごい」「自分もやらなくちゃ、という気持ちにさせられた」といった方面の感想も目立つ。

このような感想が出る理由として、本作全体のテーマが「物語」であり、それがものすごい熱量の作者の「創作」に裏打ちされているという点が挙げられるだろう。作中で橘はやとは最終的に「創作者」「物語る者」となり、上位存在Cなき地球で長曽我部真男がそれを解読し、世界を変えた。また本作全体は上位存在Cによって語られる形式となっており、物語を「消費する」存在であった上位存在Cが最終的に「物語る」存在になったことも示唆されている(最後のページに、本作を語り終えた上位存在Cが描かれている)。さらにこれは、作者(太ったおばさん)が膨大な量のテクスト・緻密な絵画表現によって「物語っ」た本作を我々が読み、解読するという構造に重なる。物語をめぐるこの多重構造が本作の魅力に奥行きを与え、創作者たちを魅了しているのだろうと思われる。

そのせいで私もこんな長文の感想を書いてしまった……。

なお、印象的な構図や、くえんと他の登場人物の描き分けなど、漫画的技法についても目を見張るものがあるが、それを分析するには私よりも適任がいると思われるので割愛する。一点コメントするとするならば、ここまで効果的に少女に花を背負わせている作品を私は知らない。

まとめ

本作は直近1年で最高のSFエロ漫画であることは間違いない。SF作品全体に範囲を広げても最高品質にランクされるものであろう。多くの創作者の意欲をかき立てたという点でも本作は非常に価値のあるものと思われる。一部では星雲賞の受賞も囁かれているが、18禁漫画が受賞対象となりうるかについては定かではない。しかし、たとえいかなる賞の受賞がなかったとしても、『出会って4光年で合体』、そしてその作者「太ったおばさん」の名は日本SF史に燦然と輝く星として残り続けるであろう。近い将来、本作をきっかけに数々の物語が、そしてそれを「物語る者」が出現しはじめるのは確実と思われる。

各論

本作のテーマとは直接関係ないが議論のあるだろう部分について、考察というのもおこがましいが私なりの解釈を載せる。ちゃんと理解できていない部分も多いと思うのでご意見や間違いの指摘などあったらコメントください。

おもかるいなりとは何か

SFという前評判を受けて読み始めると、プロローグに出てくる「おもかるいなり」という、あきらかに重力操作の伝奇的表現である語にさっそく引っかかることになる。このエピソードは理解が難しいが、おそらく後に出てくる精子型の宇宙船と関連している。くえんが橘はやとに素因数分解アルゴリズムを暗示したのと同様に、プロローグは「らせん型の装置により湾曲した重力場中で推力を得ることが可能である」ということを知っている上位存在Cが口淫する際にそのことを暗示したというエピソードであろう。現代人が過去転生したときに何気なく「地球って太陽のまわりを回ってるじゃん」とか「軽いものでも重いものでも落ちる速度は同じじゃん」とか言ってしまう感覚で「精子って重力場を泳いでいきそうな見た目してるよね」と言ってしまった、みたいな感じと思われる。

婚々のおふだはどこへいったのか?

くえんは現金を知っている(「現金はちょっとしか持っとらん」の発言や、橘はやとに2000円を渡すエピソードなどから)ので、千円札はそのまま「賽銭」として(「婚々のおふだ」としてではなく)入れていることが読み取れる。くえんの描かれ方からも、ここのみで嘘をついたということは読み取れず、婚々のおふだは実際に無くなってしまったのだろうと思われる。

それでは婚々のおふだはなぜ無くなってしまったのか? これは推測だが、上位存在Cが奪ったのであろう。上位存在Cは残り3か月でアルファケンタウリへ旅立つ予定であり、くえんも完成しており、網野の「最後の供犠のくえん」という発言からもくえんが婚姻をする(=子を成す、「くえんの婚姻はあくまで子孫を残すのが一番の目的だ」)ことは想定されていなかったと思われる(子を成した場合、その子が最後のくえんということになる)。

パスワードを勝手に無効化して「これで婚姻できないでしょ」と思っていたら、めちゃめちゃ難しく作っておいた秘密の質問を突破されて、婚姻を認めざるをえなくなった形と思われる。多要素認証にしておけばよかったのにね。

加えて、カテドラルとして価値を操作してそこから生まれる物語を食べていた上位存在Cだが(金のない者ほどいい話にされる)、くえんに価値(千円札)を差し戻され、そこから生まれた物語によって人類が短期間に驚異的な発展をすることになった(そして自分は五穀断ちすることになった)という意味もあるのだと思われる(そして上位存在Cはその状況を楽しんでいることは最後の表情で分かる)。

長曽我部真男は何者か?

長曽我部真男が何者なのかという点はまだまとめきれていない……。

空海の俗名が佐伯眞魚(さえきのまお)であることや、空海と同様にメタに介入できる(ストーリーテラーとしての上位存在Cをつまみ上げるシーン)ことから、長曽我部真男と空海との関係が示唆される。

同時に長曽我部真男が「葛の葉」の息子であることも記載がある。「葛の葉」は安倍晴明の母親とされる狐の名前である。葛の葉はくえんの血統ではない(美しいと記載はあるが、くえんの美しさは人間社会で生活することを困難にさせるレベルなので、葛の葉のような生活はできない)。これらの情報から、くえんとは別の特殊な血統があることが示唆される。長曽我部真男(そしてもしかしたら空海も)はこの血統にあるために、メタに介入することができるのだろうか。これ以上はわからない……伝奇に対する自分の造形が浅すぎる……。

くえんは誰の子か?

これは作中からははっきりとは読み取れない……。夏祭りに「お爺ちゃんとか参加したことあるらしいけど」と自分の祖父を知っている発言があるが、それが誰かは明示されない。おそらく長曽我部光蔵が言うように「誰であってもおかしくない」のだろう。「ここ何代かのくえんは外部の者を夫にしている」ようなので、作中人物で該当するとすれば土神や網野などか(長曽我部光蔵はテン子の子のようなので不適格)。ただしどのみち「本物の天狐の歌」で忘れさせられているものと思われ、本筋にも大きな関係はない。なお、たとえ橘はやとの父親がそれであっても、近親相姦にはならない(橘はやとは父親と血縁関係にないので)。

その他

  • 金剛(犬)の失踪は4年間か6年間か揺れている。たぶん誤植?
  • くえんが下界で暮らしているときのシーン、服がぜんぶ違うのマジでフェチズムを感じる。カードキャプターさくらに通じるものがある。
  • 橘はやとの記憶を消すことは、くえんにとっては完全にオプション。記憶を消す必要があるのは、くえんが地球で生きていくためには存在を隠すことが必要だからであって、最後の供犠のくえんは地球で暮らさないので婚姻相手の記憶を消す必要はない。くえんは網野に頼まれたから橘はやとの記憶を消したのであって、ここでも優しさの連鎖(「あの子が気に入ったのかもしれませんね。」)が描かれている。
  • 蘇生、世界から2人が消えている、などの記載からは、くえんも橘はやとも肉体のままアルファケンタウリに送られているよう。亜光速移動をしているはずなので(重力操作ができるのでGは問題ない)体感時間は一瞬。長曽我部真男と橘はやとがメッセージのやりとりできた理由は? 出発してすぐに送信したので地球までの距離が短く、通信がほぼリアルタイムでできたのだろうか(送受信されたデータはめちゃめちゃ赤方偏移していたと思うが……)。
  • 上位存在Cとしては、なるべく多くの人間が生きていたほうが矛盾が多く生まれるのでよい。そのために人類の数を最大化しようとしていて(ゆえに上位存在Cは「ごはんの神様」なのである)、それがカテドラルの事業になっている。
  • 拍手で暗示解除をするよりも花火で暗示解除をするほうが効果が大きかったのは、おそらくくえんと花火を見たかったからなんだよな。エモ……。そして天狗の隠れ蓑を焼いた灰の伝承すら、自分が包茎でないことからくえんの存在に気づくところに繋がっているのか……。ほんと全く無駄がないなこの作品。
  • セックスしなければ出られない惑星:この「地球の閉塞感」みたいなテーマは、直近ではあの『三体』の作者である劉慈欣の短編集『流浪地球』収載の「山」などが名作なので読んでみてください。
  • 末筆であるが、残念ながら本作は、私にとっては実用性がなかった。作中の行為は橘はやととくえんのものであって、自分のものではないと感じてしまった。作者の別作品を参照してみます。

MacVim のインライン変換で、変換対象の文節の範囲とそれ以外との表示に差がない問題を修正する

  • 環境:MacVim release 174 + ATOK (2022-12-28の最新版)
  • 問題点:インラインIM変換対象の文節のアンダーラインが、変換対象でない部分との表示の差がなく、文節長を調節する際などに不便。
    • たとえば下の画像では「吾輩は」までが変換対象の文節になっているのだが、「猫である」と表示に差がない。

MacVim release 174 での変換中の文字列

MacVimとMacVim-KaoriYaの差分 を参考に、問題に関与していると思われる部分を取り込んだパッチを作成した(ページの下部に diff を掲載する)。パッチを当てると、下の画像のように変換対象文字列が他の部分と区別できるようになった。

パッチを当てた後の変換中の文字列

パッチ:

diff --git a/src/MacVim/MMBackend.m b/src/MacVim/MMBackend.m
index 3c3dcaddd..d9d35d8a2 100644
--- a/src/MacVim/MMBackend.m
+++ b/src/MacVim/MMBackend.m
@@ -59,7 +59,7 @@ static id evalExprCocoa(NSString * expr, NSString ** errstr);
 void im_preedit_start_macvim();
 void im_preedit_end_macvim();
 void im_preedit_abandon_macvim();
-void im_preedit_changed_macvim(char *preedit_string, int cursor_index);
+void im_preedit_changed_macvim(char *preedit_string, int start_index, int cursor_index);
 
 enum {
     MMBlinkStateNone = 0,
@@ -3290,21 +3290,22 @@ extern GuiFont gui_mch_retain_font(GuiFont font);
 - (void)handleMarkedText:(NSData *)data
 {
     const void *bytes = [data bytes];
+    unsigned textlen = *((unsigned*)bytes);  bytes += sizeof(unsigned);
     int32_t pos = *((int32_t*)bytes);  bytes += sizeof(int32_t);
     unsigned len = *((unsigned*)bytes);  bytes += sizeof(unsigned);
     char *chars = (char *)bytes;
 
-    ASLogDebug(@"pos=%d len=%d chars=%s", pos, len, chars);
+    ASLogDebug(@"textlen=%d pos=%d len=%d chars=%s", textlen, pos, len, chars);
 
     if (pos < 0) {
         im_preedit_abandon_macvim();
-    } else if (len == 0) {
+    } else if (textlen == 0) {
    im_preedit_end_macvim();
     } else {
         if (!preedit_get_status())
             im_preedit_start_macvim();
 
-  im_preedit_changed_macvim(chars, pos);
+   im_preedit_changed_macvim(chars, pos, pos + len);
     }
 }
 
diff --git a/src/MacVim/MMTextViewHelper.m b/src/MacVim/MMTextViewHelper.m
index 43b718522..63ab3f594 100644
--- a/src/MacVim/MMTextViewHelper.m
+++ b/src/MacVim/MMTextViewHelper.m
@@ -42,7 +42,7 @@ static float MMDragAreaSize = 73.0f;
 - (void)setCursor;
 - (NSRect)trackingRect;
 - (BOOL)inputManagerHandleMouseEvent:(NSEvent *)event;
-- (void)sendMarkedText:(NSString *)text position:(int32_t)pos;
+- (void)sendMarkedText:(NSString *)text position:(int32_t)pos length:(unsigned)len;
 - (void)abandonMarkedText;
 - (void)sendGestureEvent:(int)gesture flags:(int)flags;
 @end
@@ -224,7 +224,7 @@ KeyboardInputSourcesEqual(TISInputSourceRef a, TISInputSourceRef b)
 - (void)insertText:(id)string
 {
     if ([self hasMarkedText]) {
-        [self sendMarkedText:nil position:0];
+        [self sendMarkedText:nil position:0 length:0];
 
         // NOTE: If this call is left out then the marked text isn't properly
         // erased when Return is used to accept the text.
@@ -335,7 +335,7 @@ KeyboardInputSourcesEqual(TISInputSourceRef a, TISInputSourceRef b)
     if ([self hasMarkedText]) {
         // We must clear the marked text since the cursor may move if the
         // marked text moves outside the view as a result of scrolling.
-        [self sendMarkedText:nil position:0];
+        [self sendMarkedText:nil position:0 length:0];
         [self unmarkText];
         [[NSTextInputContext currentInputContext] discardMarkedText];
     }
@@ -659,7 +659,7 @@ KeyboardInputSourcesEqual(TISInputSourceRef a, TISInputSourceRef b)
             imRange = range;
         }
 
-        [self sendMarkedText:text position:range.location];
+        [self sendMarkedText:text position:range.location length:range.length];
         return;
     }
 
@@ -1129,19 +1129,20 @@ KeyboardInputSourcesEqual(TISInputSourceRef a, TISInputSourceRef b)
     return NO;
 }
 
-- (void)sendMarkedText:(NSString *)text position:(int32_t)pos
+- (void)sendMarkedText:(NSString *)text position:(int32_t)pos length:(unsigned)len
 {
     if (![self useInlineIm])
         return;
 
     NSMutableData *data = [NSMutableData data];
-    unsigned len = text == nil ? 0
+    unsigned textlen = text == nil ? 0
                     : [text lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
 
+    [data appendBytes:&textlen length:sizeof(unsigned)];
     [data appendBytes:&pos length:sizeof(int32_t)];
     [data appendBytes:&len length:sizeof(unsigned)];
-    if (len > 0) {
-        [data appendBytes:[text UTF8String] length:len];
+    if (textlen > 0) {
+        [data appendBytes:[text UTF8String] length:textlen];
         [data appendBytes:"\x00" length:1];
     }
 
@@ -1155,7 +1156,7 @@ KeyboardInputSourcesEqual(TISInputSourceRef a, TISInputSourceRef b)
     // Send an empty marked text message with position set to -1 to indicate
     // that the marked text should be abandoned.  (If pos is set to 0 Vim will
     // send backspace sequences to delete the old marked text.)
-    [self sendMarkedText:nil position:-1];
+    [self sendMarkedText:nil position:-1 length:0];
     [[NSTextInputContext currentInputContext] discardMarkedText];
 }
 
diff --git a/src/gui_xim.c b/src/gui_xim.c
index ce2e79c95..fdcf29aaa 100644
--- a/src/gui_xim.c
+++ b/src/gui_xim.c
@@ -184,6 +184,7 @@ init_preedit_start_col(void)
 
 static int im_is_active           = FALSE; // IM is enabled for current mode
 static int preedit_is_active   = FALSE;
+static int im_preedit_start    = 0;    /* start offset in characters        */
 static int im_preedit_cursor   = 0;    // cursor offset in characters
 static int im_preedit_trailing = 0;    // number of characters after cursor
 
@@ -715,7 +716,7 @@ im_preedit_abandon_macvim()
 im_preedit_changed_cb(GtkIMContext *context, gpointer data UNUSED)
 # else
     void
-im_preedit_changed_macvim(char *preedit_string, int cursor_index)
+im_preedit_changed_macvim(char *preedit_string, int start_index, int cursor_index)
 # endif
 {
 # ifndef FEAT_GUI_MACVIM
@@ -736,6 +737,8 @@ im_preedit_changed_macvim(char *preedit_string, int cursor_index)
    gtk_im_context_get_preedit_string(context,
                      &preedit_string, NULL,
                      NULL);
+# else
+    im_preedit_start = start_index;
 # endif
 
 #ifdef XIM_DEBUG
@@ -924,7 +927,10 @@ im_get_feedback_attr(int col UNUSED)
 
     return char_attr;
 # else
-    return HL_UNDERLINE;
+    if (col >= im_preedit_start && col < im_preedit_cursor)
+   return HL_UNDERCURL;
+    else
+   return HL_UNDERLINE;
 # endif
 }

Excel で、複数の条件に該当するセルのデータを横方向に抽出する

Excel を用いて、複数の条件をもとにセルのデータを横方向に抽出する必要が生じた。

要は、Sheet1 にある次のような表を

f:id:KainokiKaede:20211016121511p:plain
元表

公開フラグが立っているもののみ、次のように Sheet2 に整理(?)したい。

f:id:KainokiKaede:20211016121529p:plain
抽出表

Yahoo! 知恵袋 などには COUNTIF, MATCH 等を使って実現する方法が載っているが、上の表のように公開フラグなどの条件が入ってくると扱いづらそうに感じた。

最近の Office には FILTER, UNIQUE などの配列関数が実装された ようで、これらを用いれば複数条件に該当する値のリストを取得できる。

まず Sheet2 の A2 に次の式を入力する。(Excel の最大行数は 1048576 行である

=UNIQUE(Sheet1!$A$2:$A$1048576)

これで Sheet1 の A 列のうち重複を含まないものが Sheet2 のA列に表示される。

続いて Sheet2 の B2 に次の式を入力する。

=TRANSPOSE(FILTER(Sheet1!$C$2:$C$1048576, (Sheet1!$A$2:$A$1048576=$A2) * (Sheet1!$B$2:$B$1048576="公開")))

ここでは、まず FILTER 関数で第2引数の条件に合う行のC列を取得したうえで、TRANSPOSE で横向きに並べている。

最後にB2からB5までオートフィルすれば完成である。

式2つで表が作成できてオトクだと思うし既存の手法よりも式の見通しがよいと思う。難点は新しい Office が入っていないとこれらの関数が使えず、Excel のみでデータを弄る必要があるときには往々にして新しい Office は入っていないという点か。

Mac の PlatformIO IDE で #include エラーが出る問題を解決する

MacVSCode で PlatformIO を使って M5Stack の開発をしていると、#include <M5EPD.h> の行に

#include エラーが検出されました。includePath を更新してください。...
ソース ファイルを開けません "AvailabilityMacros.h" (dependency of "M5EPD.h")

というエラーが出現する。英語版では

#include errors detected. Please update your includePath. ...
cannot open source file "AvailabilityMacros.h"

であろう。

この状態でもプロジェクトのコンパイルは可能だが、VSCode が常にエラーの存在を訴えてきて annoying である。

このエラーが発生する原因は、最近の macOS には /usr/include が存在しないのにもかかわらず VSCode (もしくは PlatformIO もしくは C/C++ extension)が /usr/include を読みに行っているからだと思われる。

% xcrun --show-sdk-path
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk

で正しい path を確認してやり、platformio.inibuild_flags 内に次の行を追加すればエラーは消える。

build_flags = 
    ; ...
    -I/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/**

M5Paper で BLE Current Time Service を利用して Real Time Clock を設定する

M5Stack シリーズの一つ M5Paper には real time clock (RTC) BM8563 が搭載されており、M5.RTC ライブラリを利用して時刻の書き込み・読み込みを行うことができる。

この RTC の時刻を合わせる方法として、Wi-Fi を用いて適当な NTP サーバーへ接続し現在時刻を取得する例が Web 上に多く存在するが、Web 接続可能な Wi-Fi が常に使用可能とは限らない。

M5Paper には Bluetooth 機能も付属している。Bluetooth 規格の一種である Bluetooth Low Energy (BLE) には Current Time Service (CTS, リンク先はPDF) という現在時刻を取得できるサービスが存在する。Accessory Design Guidelines for Apple Devices (PDF) には、iOS 7.0 以降は Current Time Service が利用可能であると記載されている。すなわち、iPhone の時計が携帯電話回線経由で常にほぼ正確な時刻に合わせられていることを前提とするならば、M5Paper を BLE で iPhone に接続し、Current Time Service を用いて時刻を取得することで、M5Paper を Wi-Fi に接続することなしにほぼ正確な時刻を取得することが可能となる。

M5StickC を用いて Current Time Service より現在時刻を取得する試みはすでに存在する が、この手法では BLE デバイス検索アプリを別途用意する必要があり、また接続時に iPhone からのペアリング操作が毎回必要であり手間が大きかった。

今回、接続のための専用アプリが不要で、かつ一度ペアリングした以降は再接続時も iPhone 側での操作が不要である ESP32 用 Current Time Service クライアントライブラリ ddlsmurf/ESP32-ANCS-AMS-Notifications を見つけたので、これを紹介するとともに、M5Paper での利用テストプログラムを制作したので公開する。

早速であるが以下がそのプログラムである。公式の RTC 利用例前述のライブラリ を合体させた。初回起動時にはペアリング待ち状態となり、その間に iPhone 側の設定アプリからペアリングを行う。一度ペアリングされた以降は再度同一のプログラムを実行すれば自動で M5Paper が iPhone へペアリングされ時刻情報が取得される。ペアリング情報はおそらく内部的に利用されている ESP32 のライブラリがうまく管理してくれており、スケッチを書き換えても維持されるようだ。

#include <M5EPD.h>

//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//+++++ Get Current Time from BLE Current Time Service +++++
//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
#include <esp32notifications.h>

void setupTime(int year, int month, int date, int hour, int minute, int second);

void checkCTSAndUpdateRTC() {
  BLENotifications notifications;
  notifications.begin("CTS_test");  // BLE begin.
  while (1) {
    if (notifications.clientCTS->ready()) break;
    delay(100);
  }
  ble_cts_current_time_char_t *time = notifications.clientCTS->readTime();
  if (time->exact_time_256.seconds < 59) {  // Inclimentating time is complicated ... so forget about seconds = 59.
    delay(int(1000. - float(time->exact_time_256.fractions256) * 1000. / 256.));  // wait until next second.
    time->exact_time_256.seconds += 1;  // incliment.
  }
  setupTime(time->exact_time_256.year, time->exact_time_256.month, time->exact_time_256.day,
            time->exact_time_256.hours, time->exact_time_256.minutes, time->exact_time_256.seconds);
  notifications.stop();  // BLE stop.
}
//-----------------------------------------------------------


//++++++++++++++++++++++++
//+++++ RTC settings +++++
//++++++++++++++++++++++++
M5EPD_Canvas canvas(&M5.EPD);

rtc_time_t RTCtime;
rtc_date_t RTCDate;

char timeStrbuff[64];

void flushTime(){
    M5.RTC.getTime(&RTCtime);
    M5.RTC.getDate(&RTCDate);

    sprintf(timeStrbuff,"%d/%02d/%02d %02d:%02d:%02d",
                        RTCDate.year,RTCDate.mon,RTCDate.day,
                        RTCtime.hour,RTCtime.min,RTCtime.sec);

    canvas.drawString(timeStrbuff, 0, 0);
    canvas.pushCanvas(100,200,UPDATE_MODE_DU4);
}

void setupTime(int year, int month, int date, int hour, int minute, int second){
  RTCtime.hour = hour;
  RTCtime.min = minute;
  RTCtime.sec = second;
  M5.RTC.setTime(&RTCtime);

  RTCDate.year = year;
  RTCDate.mon = month;
  RTCDate.day = date;
  M5.RTC.setDate(&RTCDate);
}
//---------------------------


void setup(void) {
  M5.begin();
  M5.EPD.SetRotation(90);
  M5.EPD.Clear(true);
  M5.RTC.begin();
  canvas.createCanvas(400, 300);
  canvas.setTextSize(3);

  checkCTSAndUpdateRTC();
}

void loop() {
    M5.update();
    if( M5.BtnL.pressedFor(2000))  // shutdown.
    {
        canvas.drawString("Shutdown...", 0, 0);
        canvas.pushCanvas(100,200,UPDATE_MODE_DU4);
        delay(600);
        M5.disableEPDPower();
        M5.disableEXTPower();
        M5.disableMainPower();
        esp_deep_sleep_start();
        while(1);
    }
    flushTime();
    delay(500);
}

ぜひこのコードをご活用いただき、NTP にアクセスできない場所でもほぼ正確な時刻を M5Paper に教えてあげてほしい。

追記: M5Paper_FactoryTest にこの機能を実装した。GitHub の diff を載せる。Setting - Sync Time を押すと同期が実行される。

参考

コードの一部などを以下のサイトから拝借した。