sako_DIYのアイコン画像

obnizで日めくりカレンダーIoT

sako_DIY 2021年05月16日に作成  (2021年05月16日に更新)
obnizで日めくりカレンダーIoT

概要

皆さんは、日めくりカレンダーってご存知ですか?

キャプションを入力できます

分厚い紙の束の真ん中に大きく日付が書いてあって、毎日1枚めくることによって、今日の日付、曜日、旧暦、六曜等が知る事のできる商品です。
少し前には、芸能人の名言が1日1言かいてある「まいにち、〇〇!」みたいなシリーズがブームになったのも記憶に新しいと思います。

日めくりカレンダーのメリットは、日常生活において、ぱっと一目見るだけで情報(日付)がわかるということです。

しかし、モバイル端末が普及し、令和元年度には、人口の約96%がスマホや携帯電話を持っているという時代(総務省統計より)
分厚い紙の束を壁にかけ、毎日めくる事により知ることが出来るのは、せいぜい日付+α....
割に合わないと思いませんか??
そして、その日付でさえ、めくることを忘れてしまえば、もうただのオブジェです。

そして最大のデメリットは、印刷物であるがゆえに1年前に確定している事項しか書いていないのです。

そこで、
obnizを用いてIoT化することにより、日めくりカレンダーをイノベーションすることにしました。

動画

ここに動画が表示されます

こちらが実際に動作している動画です。
日めくりカレンダーの基本的な日付、曜日等の表示、
そして紙の日めくりカレンダーではできない、天気予報(気象庁から取得)と、今日の予定(googlecalendar)が表示されているのが確認できると思います。
obnizboard1Yのスリープ機能と組み合わせて、日付が変わったタイミングの書き換えでしか、動作しないので、かなり消費電力が抑えられて長期稼働が可能です。
※動画の天気予報と予定はダミーです。

部品

部品リストは以下のとおりです。

部品名 入手先
obniz Board 1Y 公式通販
WaveShare 7.5インチe-paperモジュール Amazon
電池BOX 単3×3本 共立エレショップ
2L判用 フォトフレーム ダイソー

obniz Board 1Y

メインプロセッサは、このたびobnizコンテストで頂いた、obniz Board 1Yです。
ブラウザでプログラミング可能で、かんたんにIoTデバイスを作成できます。
Sleep機能が新たに追加され、電池で長期間動作することが可能になったようです。
キャプションを入力できます

7.5インチe-paperモジュール

e-paperとはなにか、をWikiから引用させていただくと

電子ペーパー(でんしペーパー)とは、紙の長所とされる視認性や携帯性を保った表示媒体のうち、表示内容を電気的に書き換えられるものをいう。
1970年代に米国ゼロックス社のパロアルト研究所に所属していたニック・シェリドンがGyriconと呼ばれる最初の電子ペーパーを開発した[注 1]。Gyriconの構造は、半球を白、別の半球を黒に塗り分けた微小な球をディスプレイに多数埋め込んだものである。球の一部は静電気を帯びており、電界によって球を回転させることで白地に黒い文字を浮かび上がらせられ、数千回の書き換えにも耐えた。
現在では電子ペーパーを利用した製品が一般的に販売されるまでに至り、今後は低価格化が普及の鍵とされる。

ということで要するにインクを電気の力で移動させることで表示する、紙の長所の視認性の良さを持ったディスプレイで、書き換えのときにしか電源を必要としない省電力なデバイスです。
電子書籍なんかによく使われているイメージです。

今回買ったのは、Waveshare社の7.5インチ赤黒白の3色が使用可能な、e-paperモジュールで、SPIで書き換えが可能です。
Amazonには640 * 384って書いてありましたが、バージョンアップ後の880x528のHDモジュールが届きました。
キャプションを入力できます

電池BOX

省電力なobniz board 1Y、省電力なディスプレイの組み合わせを活かすために、乾電池で長期動作を目指すことにしました。
obniz board 1Yは、外部電源入力があり、3.3-5.5vの範囲の電源で動作が可能なようなので、1.5Vの乾電池3本で4.5Vとしました。
キャプションを入力できます

フォトフレーム

今回の日めくりカレンダーは、卓上に置くことを考えた結果、フォトフレームに収めるとよいという結論に至りました。
ダイソーに色んな種類がありました。
7.5インチディスプレイの外形に合わせて、2L判がちょうどよかったです。
キャプションを入力できます

ハードウェア

ハードウェアの配線は、以下の通りです。
キャプションを入力できます

ePaper Driver HATのピンは以下の機能を持っています。

ピン名 機能 備考
BUSY BUSY出力 e-Paper書き換え動作中など外部信号がが受け付けられないときに HIGHとなります。
RST リセット LOWにするとDriver HATの設定値が初期化されます。
DC データ/コマンド選択 HIGH時に書き込んだ場合はデータ、LOW時に書き込んだ場合はコマンドとして認識されます。
CS SPI チップセレクト SPIのCSピンです。
CLK SPI クロック SPIのSCKピンです。
DIN SPI データ入力 SPIのMOSIピンです。
GND GND
VCC 電源入力 3.3V推奨です。

また、Driver HAT上のスイッチでモード切替を行います。
それぞれB(Other),0(4-line )に設定します。

Display Config
A 1.54,2.13,2.9インチ e-Paper
B Other
Interface Config
1 3-line SPI
0 4-line SPI

以上を、写真立てに組み込み以下のようになりました。
キャプションを入力できます

ソフトウェア

プログラムが長く煩雑になってしまったので、相当読みづらいと思います。
重要な関数のみ抜粋し説明、最後にプログラム全体を載せようと思います。
必要な部分までスキップしていただけたらと思います。

フローチャート

flow

サーバーレス実行用設定、指定時刻までスリープ

obnizアプリのサーバーレス実行は30秒までという制限があり、超えるとエラーになります。
書き換え動作自体は30秒かからないのですが、なんらかの原因で30秒をを超えることを想定し、プログラム上で25秒の制限を設け、超えた場合60秒スリープ後再度実行し直すようにしました。

タイムアウト設定

<script> let tid=setTimeout(()=>{ console.log("timeout"); obniz.sleepSeconds(60); },25000);*//25000ms制限

また、無事書き換え動作が終了した場合は、タイムアウト設定をリセット
そして次回起動の時刻を設定し、obnizboardをスリープ、サーバー側のアプリも終了させます。

スリープ及び終了

<script> var date = new Date(); obniz.onconnect = async function () {   ~省略~ date.setHours(0,0,0,0); //起動時刻を0時0分0秒に変更 date.setDate(date.getDate()+1);//起動日を明日に変更 obniz.sleep(date);//スリープ実行 await obniz.wait(1000); clearTimeout(tid);//タイムアウトリセット if (Obniz.App.isCloudRunning()) { Obniz.App.done({ status: 'success' }); } };

googleカレンダーより予定取得

googleカレンダーAPIを用いて、本日の予定を取得します。
予めgoogleアカウント上にて、googleカレンダーAPIの有効化、予定の入力されたカレンダーを一般公開しておきます。
※一般公開することで、誰でもカレンダーにアクセスできてしまうので、注意です。

予定取得

<script> const CALENDAR_API_KEY = 'APIキー'; const MY_CALENDAR_ID = '一般公開にしたカレンダーのID'; var date = new Date(); //起動した日の予定をgoogleCalendarよりfetchを使って取得 fetch('https://www.googleapis.com/calendar/v3/calendars/' + encodeURIComponent(MY_CALENDAR_ID) + '/events?key='+CALENDAR_API_KEY+'&timeMin='+encodeURIComponent(date.getFullYear()+"-"+(date.getMonth()+1)+"-"+date.getDate()+'T00:00:00.000+09:00')+'&timeMax='+encodeURIComponent(date.getFullYear()+"-"+(date.getMonth()+1)+"-"+date.getDate()+'T23:59:59.000+09:00')) .then(response => response.json()) .then(data => { //取得した予定を加工しcanvasへ描画 });

土日祝判定

同じくgoogleカレンダーAPIを用いて、祝日を取得
日本の祝日カレンダーは、一般公開されていますので、それを利用します。
土日祝だった場合に描画色を赤に変更(デフォルトは黒)

土日祝判定

<script> var date = new Date(); var color="black" const CALENDAR_API_KEY = 'APIキー'; const HOLIDAY_CALENDAR_ID = 'ja.japanese#holiday@group.v.calendar.google.com'; fetch('https://www.googleapis.com/calendar/v3/calendars/' + encodeURIComponent(HOLIDAY_CALENDAR_ID) + '/events?key='+CALENDAR_API_KEY+'&timeMin='+encodeURIComponent(date.getFullYear()+"-"+(date.getMonth()+1)+"-"+date.getDate()+'T00:00:00Z')+'&timeMax='+encodeURIComponent(date.getFullYear()+"-"+(date.getMonth()+1)+"-"+date.getDate()+'T23:59:59Z')) .then(response => response.json()) .then(data =>{ if(data.items.length!=0){ color="red";//祝日なら赤 }else if(date.getDay()==0||date.getDay()==6){ color="red";//土日なら赤 } });

天気予報取得

気象庁の気象データが、jsonで取得できるようになっているそうで、
それをfetchを利用して取得、1日の天候を描画します。

天気予報取得

<script> //URLの270000の部分を変更すれば他の地域を取得可(270000:大阪) fetch('https://www.jma.go.jp/bosai/forecast/data/forecast/270000.json') .then(response => response.json()) .then(data => { //取得した天気を元にcanvas描画 });

気象庁から得られる天気データには、テロップ番号と呼ばれるものが含まれており、
それを元に天気予報のアイコンを表示します。
テロップ番号は種類がとても多いので以下のサイトを参考に、以下の表に当てはまる様に絞りこみました。
mk-mode BLOG 天気予報テロップ番号一覧(https://www.mk-mode.com/blog/2014/03/13/weather-forecast-telop-list/)

from to join(接続詞)
晴れ 晴れ 終日:0
曇り 曇り のち:1
時々:2

天気予報取得

<script> const transcodelist =[ {weatherCode:100,from:"sunny",to:"sunny",join:0}, {weatherCode:101,from:"sunny",to:"cloudy",join:2}, {weatherCode:102,from:"sunny",to:"rainy",join:2}, {weatherCode:103,from:"sunny",to:"rainy",join:2}, {weatherCode:104,from:"sunny",to:"snowy",join:2}, {weatherCode:105,from:"sunny",to:"snowy",join:2}, {weatherCode:106,from:"sunny",to:"rainy",join:2}, {weatherCode:107,from:"sunny",to:"rainy",join:2}, {weatherCode:108,from:"sunny",to:"rainy",join:2}, {weatherCode:110,from:"sunny",to:"cloudy",join:1}, {weatherCode:111,from:"sunny",to:"cloudy",join:1}, {weatherCode:112,from:"sunny",to:"rainy",join:1}, {weatherCode:113,from:"sunny",to:"rainy",join:1}, {weatherCode:114,from:"sunny",to:"rainy",join:1}, {weatherCode:115,from:"sunny",to:"snowy",join:1}, {weatherCode:116,from:"sunny",to:"snowy",join:1}, {weatherCode:117,from:"sunny",to:"snowy",join:1}, {weatherCode:118,from:"sunny",to:"rainy",join:1}, {weatherCode:119,from:"sunny",to:"rainy",join:1}, {weatherCode:120,from:"sunny",to:"rainy",join:2}, {weatherCode:121,from:"rainy",to:"sunny",join:1}, {weatherCode:122,from:"sunny",to:"rainy",join:1}, {weatherCode:123,from:"sunny",to:"sunny",join:0}, {weatherCode:124,from:"sunny",to:"sunny",join:0}, {weatherCode:125,from:"sunny",to:"rainy",join:1}, {weatherCode:126,from:"sunny",to:"rainy",join:1}, {weatherCode:127,from:"sunny",to:"rainy",join:1}, {weatherCode:128,from:"sunny",to:"rainy",join:1}, {weatherCode:129,from:"sunny",to:"rainy",join:1}, {weatherCode:130,from:"sunny",to:"sunny",join:0}, {weatherCode:131,from:"sunny",to:"sunny",join:0}, {weatherCode:132,from:"sunny",to:"cloudy",join:2}, {weatherCode:140,from:"sunny",to:"rainy",join:2}, {weatherCode:160,from:"sunny",to:"snowy",join:2}, {weatherCode:170,from:"sunny",to:"snowy",join:2}, {weatherCode:181,from:"sunny",to:"snowy",join:1}, {weatherCode:200,from:"cloudy",to:"cloudy",join:0}, {weatherCode:201,from:"cloudy",to:"sunny",join:2}, {weatherCode:202,from:"cloudy",to:"rainy",join:2}, {weatherCode:203,from:"cloudy",to:"rainy",join:2}, {weatherCode:204,from:"cloudy",to:"snowy",join:2}, {weatherCode:205,from:"cloudy",to:"snowy",join:2}, {weatherCode:206,from:"cloudy",to:"rainy",join:2}, {weatherCode:207,from:"cloudy",to:"rainy",join:2}, {weatherCode:208,from:"cloudy",to:"rainy",join:2}, {weatherCode:209,from:"cloudy",to:"cloudy",join:0}, {weatherCode:210,from:"cloudy",to:"sunny",join:1}, {weatherCode:211,from:"cloudy",to:"sunny",join:1}, {weatherCode:212,from:"cloudy",to:"rainy",join:1}, {weatherCode:213,from:"cloudy",to:"rainy",join:1}, {weatherCode:214,from:"cloudy",to:"rainy",join:1}, {weatherCode:215,from:"cloudy",to:"snowy",join:1}, {weatherCode:216,from:"cloudy",to:"snowy",join:1}, {weatherCode:217,from:"cloudy",to:"snowy",join:1}, {weatherCode:218,from:"cloudy",to:"rainy",join:1}, {weatherCode:219,from:"cloudy",to:"rainy",join:1}, {weatherCode:220,from:"cloudy",to:"rainy",join:2}, {weatherCode:221,from:"rainy",to:"cloudy",join:1}, {weatherCode:222,from:"cloudy",to:"rainy",join:1}, {weatherCode:223,from:"cloudy",to:"sunny",join:2}, {weatherCode:224,from:"cloudy",to:"rainy",join:1}, {weatherCode:225,from:"cloudy",to:"rainy",join:1}, {weatherCode:226,from:"cloudy",to:"rainy",join:1}, {weatherCode:227,from:"cloudy",to:"rainy",join:1}, {weatherCode:228,from:"cloudy",to:"snowy",join:1}, {weatherCode:229,from:"cloudy",to:"snowy",join:1}, {weatherCode:230,from:"cloudy",to:"cloudy",join:0}, {weatherCode:231,from:"cloudy",to:"rainy",join:2}, {weatherCode:240,from:"cloudy",to:"snowy",join:2}, {weatherCode:250,from:"cloudy",to:"snowy",join:2}, {weatherCode:260,from:"cloudy",to:"snowy",join:2}, {weatherCode:270,from:"cloudy",to:"snowy",join:2}, {weatherCode:281,from:"cloudy",to:"snowy",join:1}, {weatherCode:300,from:"rainy",to:"rainy",join:0}, {weatherCode:301,from:"rainy",to:"sunny",join:2}, {weatherCode:302,from:"rainy",to:"cloudy",join:2}, {weatherCode:303,from:"rainy",to:"sunny",join:2}, {weatherCode:304,from:"rainy",to:"rainy",join:2}, {weatherCode:306,from:"rainy",to:"rainy",join:0}, {weatherCode:307,from:"rainy",to:"rainy",join:0}, {weatherCode:308,from:"rainy",to:"rainy",join:0}, {weatherCode:309,from:"rainy",to:"snowy",join:1}, {weatherCode:311,from:"rainy",to:"sunny",join:1}, {weatherCode:313,from:"rainy",to:"cloudy",join:1}, {weatherCode:314,from:"rainy",to:"snowy",join:1}, {weatherCode:315,from:"rainy",to:"snowy",join:1}, {weatherCode:316,from:"rainy",to:"sunny",join:1}, {weatherCode:317,from:"rainy",to:"cloudy",join:1}, {weatherCode:320,from:"rainy",to:"sunny",join:1}, {weatherCode:321,from:"rainy",to:"cloudy",join:1}, {weatherCode:322,from:"rainy",to:"snowy",join:2}, {weatherCode:323,from:"rainy",to:"sunny",join:1}, {weatherCode:324,from:"rainy",to:"sunny",join:1}, {weatherCode:325,from:"rainy",to:"sunny",join:1}, {weatherCode:326,from:"rainy",to:"snowy",join:1}, {weatherCode:327,from:"rainy",to:"snowy",join:1}, {weatherCode:328,from:"rainy",to:"rainy",join:0}, {weatherCode:329,from:"rainy",to:"snowy",join:2}, {weatherCode:340,from:"snowy",to:"rainy",join:2}, {weatherCode:350,from:"rainy",to:"snowy",join:0}, {weatherCode:361,from:"snowy",to:"sunny",join:1}, {weatherCode:371,from:"snowy",to:"cloudy",join:1}, {weatherCode:400,from:"snowy",to:"snowy",join:0}, {weatherCode:401,from:"snowy",to:"sunny",join:2}, {weatherCode:402,from:"snowy",to:"cloudy",join:2}, {weatherCode:403,from:"snowy",to:"rainy",join:2}, {weatherCode:405,from:"snowy",to:"snowy",join:0}, {weatherCode:406,from:"snowy",to:"snowy",join:0}, {weatherCode:407,from:"snowy",to:"snowy",join:0}, {weatherCode:409,from:"snowy",to:"rainy",join:2}, {weatherCode:450,from:"snowy",to:"snowy",join:0}, {weatherCode:411,from:"snowy",to:"sunny",join:1}, {weatherCode:413,from:"snowy",to:"cloudy",join:1}, {weatherCode:414,from:"snowy",to:"rainy",join:1}, {weatherCode:420,from:"snowy",to:"sunny",join:1}, {weatherCode:421,from:"snowy",to:"cloudy",join:1}, {weatherCode:422,from:"snowy",to:"rainy",join:1}, {weatherCode:423,from:"snowy",to:"rainy",join:1}, {weatherCode:424,from:"snowy",to:"rainy",join:1}, {weatherCode:425,from:"snowy",to:"snowy",join:0}, {weatherCode:426,from:"snowy",to:"rainy",join:1}, {weatherCode:427,from:"snowy",to:"rainy",join:2}, ]

また、リポジトリに天気アイコン画像をアップロードしておき、天気の表示に利用します。
天気予報のアイコンは、以下のサイトより生成し保存させていただきました。
ICOOON MONO(https://icooon-mono.com/tag/天気/)
リポジトリ

e-Paper書き換え動作(SPI)

電子ペーパーモジュールのwaveshare公式Wikiで、ESP32用のプログラム例が配布されていますので、それを参考にします。
また初期化コマンドも同様、Wikiにあるデータシートを参考にします。
作成した関数は、
・コマンド送信関数
・データ送信関数
・BUSY判定関数
・リセット関数
・初期設定関数
・画面書き換え関数
です。

SPI初期設定

空きSPIを取得し、3V駆動で初期設定します。

SPI初期設定

<script> const DIN = 0; const CLK = 1; const CS = 2; const DC = 3; const RST = 4; const BUSY = 5; var spi; obniz.onconnect = async function () { obniz.getIO(DC).drive("3v"); obniz.getIO(CS).drive("3v"); obniz.getIO(RST).drive("3v"); spi = obniz.getFreeSpi(); spi.start({ mode: "master", mosi: DIN, clk: CLK, frequency: 1000 * 1000, drive: "3v", }); }

コマンド送信関数

EPD_SendCommand()

<script> async function EPD_SendCommand(cmd) {//引数:送信コマンド console.log("cmd=" + cmd); obniz.getIO(DC).output(false); //DC LOWでコマンド判定 obniz.getIO(CS).output(false); spi.write([cmd]); obniz.getIO(CS).output(true); }

データ送信関数

EPD_SendData()

<script> async function EPD_SendData(data) { //引数:送信データ配列 console.log("data=" + data); obniz.getIO(DC).output(true); //DC HIGHでデータ判定 obniz.getIO(CS).output(false); spi.write(data); obniz.getIO(CS).output(true); }

BUSY判定関数

EPD_ReadBusy()

<script> async function EPD_ReadBusy() { console.log("busy wait"); var epdbusy; do { epdbusy = await obniz.getIO(BUSY).inputWait(); } while (epdbusy);//BUSYピンがHIGHの限り無限ループ console.log("busy end"); await obniz.wait(200); }

リセット関数

EPD_Reset()

<script> async function EPD_Reset() { console.log("Reset"); obniz.getIO(RST).output(false); //RST ピンLOWでリセット await obniz.wait(10); obniz.getIO(RST).output(true); await obniz.wait(200); }

初期設定関数

EPD_Init()

<script> async function EPD_Init() { console.log("Init"); await EPD_Reset(); await EPD_SendCommand(0x12); await EPD_ReadBusy(); await EPD_SendCommand(0x46); await EPD_SendData([0xf7]); await EPD_ReadBusy(); await EPD_SendCommand(0x47); await EPD_SendData([0xf7]); await EPD_ReadBusy(); await EPD_SendCommand(0x0c); await EPD_SendData([0xae,0xc7,0xc3,0xc0,0x40]); await EPD_SendCommand(0x01); await EPD_SendData([0xaf,0x02,0x01]); await EPD_SendCommand(0x11); await EPD_SendData([0x01]); await EPD_SendCommand(0x44); await EPD_SendData([0x00,0x00,0x6f,0x03]); await EPD_SendCommand(0x45); await EPD_SendData([0xaf,0x02,0x00,0x00]); await EPD_SendCommand(0x3c); await EPD_SendData([0x01]); await EPD_SendCommand(0x18); await EPD_SendData([0x80]); await EPD_SendCommand(0x22); await EPD_SendData([0xb1]); await EPD_SendCommand(0x20); await EPD_ReadBusy(); await EPD_SendCommand(0x4e); await EPD_SendData([0x00,0x00]); await EPD_SendCommand(0x4f); await EPD_SendData([0x00,0x00]); console.log("Init end"); }

画面書き換え関数

htmlのキャンバスから赤色データ黒色データをそれぞれ読み取って、送信する関数です。

EPD_Write()

<script> async function EPD_Write() { await EPD_SendCommand(0x4f); await EPD_SendData([0x00,0x00]); await EPD_SendCommand(0x24); //黒白データ送信 for (let k = 0; k <529; k++) { const imagelineData = ctx.getImageData(0, k, canvas.width, 8); //1行ずつキャンバスデータを取得 var black = []; var tmp = 0x00; for (let i = 0; i < 110; i++) { tmp = 0x00; for (let j = 0; j < 8; j++) {//8bitずつ読み出し if ( imagelineData.data[i * 32 + j * 4] == 0 && imagelineData.data[1 + i * 32 + j * 4] == 0 && imagelineData.data[2 + i * 32 + j * 4] == 0 && imagelineData.data[3 + i * 32 + j * 4] < 128 ) {//黒なら tmp |= 0x80 >> j; } } black.push(tmp); } await EPD_SendData(black); } await EPD_SendCommand(0x26); //赤白データ送信 for (let k = 0; k < 529; k++) { const imagelineData = ctx.getImageData(0, k, canvas.width, 8);//1行ずつキャンバスデータを取得 var red = []; var tmp = 0x00; for (let i = 0; i < 110; i++) { tmp = 0x00; for (let j = 0; j < 8; j++) {//8bitずつ読み出し if ( imagelineData.data[i * 32 + j * 4] >= 128 && imagelineData.data[1 + i * 32 + j * 4] == 0 && imagelineData.data[2 + i * 32 + j * 4] == 0 && imagelineData.data[3 + i * 32 + j * 4] > 128 ) {//赤なら tmp |= 0x80 >> j; } } red.push(a); } await EPD_SendData(red); } await EPD_SendCommand(0x22); await EPD_SendData([0xc7]); await EPD_SendCommand(0x20); await obniz.wait(200); await EPD_ReadBusy(); }

プログラム全体

app

<html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"/> <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script> <link rel="preconnect" href="https://fonts.gstatic.com"> <script src="https://unpkg.com/obniz@3.14.0/obniz.js" crossorigin="anonymous"></script> </head> <body> <center><canvas id="canvas"></canvas></center> </div> <script> const transcodelist =[ {weatherCode:100,from:"sunny",to:"sunny",join:0}, {weatherCode:101,from:"sunny",to:"cloudy",join:2}, {weatherCode:102,from:"sunny",to:"rainy",join:2}, {weatherCode:103,from:"sunny",to:"rainy",join:2}, {weatherCode:104,from:"sunny",to:"snowy",join:2}, {weatherCode:105,from:"sunny",to:"snowy",join:2}, {weatherCode:106,from:"sunny",to:"rainy",join:2}, {weatherCode:107,from:"sunny",to:"rainy",join:2}, {weatherCode:108,from:"sunny",to:"rainy",join:2}, {weatherCode:110,from:"sunny",to:"cloudy",join:1}, {weatherCode:111,from:"sunny",to:"cloudy",join:1}, {weatherCode:112,from:"sunny",to:"rainy",join:1}, {weatherCode:113,from:"sunny",to:"rainy",join:1}, {weatherCode:114,from:"sunny",to:"rainy",join:1}, {weatherCode:115,from:"sunny",to:"snowy",join:1}, {weatherCode:116,from:"sunny",to:"snowy",join:1}, {weatherCode:117,from:"sunny",to:"snowy",join:1}, {weatherCode:118,from:"sunny",to:"rainy",join:1}, {weatherCode:119,from:"sunny",to:"rainy",join:1}, {weatherCode:120,from:"sunny",to:"rainy",join:2}, {weatherCode:121,from:"rainy",to:"sunny",join:1}, {weatherCode:122,from:"sunny",to:"rainy",join:1}, {weatherCode:123,from:"sunny",to:"sunny",join:0}, {weatherCode:124,from:"sunny",to:"sunny",join:0}, {weatherCode:125,from:"sunny",to:"rainy",join:1}, {weatherCode:126,from:"sunny",to:"rainy",join:1}, {weatherCode:127,from:"sunny",to:"rainy",join:1}, {weatherCode:128,from:"sunny",to:"rainy",join:1}, {weatherCode:129,from:"sunny",to:"rainy",join:1}, {weatherCode:130,from:"sunny",to:"sunny",join:0}, {weatherCode:131,from:"sunny",to:"sunny",join:0}, {weatherCode:132,from:"sunny",to:"cloudy",join:2}, {weatherCode:140,from:"sunny",to:"rainy",join:2}, {weatherCode:160,from:"sunny",to:"snowy",join:2}, {weatherCode:170,from:"sunny",to:"snowy",join:2}, {weatherCode:181,from:"sunny",to:"snowy",join:1}, {weatherCode:200,from:"cloudy",to:"cloudy",join:0}, {weatherCode:201,from:"cloudy",to:"sunny",join:2}, {weatherCode:202,from:"cloudy",to:"rainy",join:2}, {weatherCode:203,from:"cloudy",to:"rainy",join:2}, {weatherCode:204,from:"cloudy",to:"snowy",join:2}, {weatherCode:205,from:"cloudy",to:"snowy",join:2}, {weatherCode:206,from:"cloudy",to:"rainy",join:2}, {weatherCode:207,from:"cloudy",to:"rainy",join:2}, {weatherCode:208,from:"cloudy",to:"rainy",join:2}, {weatherCode:209,from:"cloudy",to:"cloudy",join:0}, {weatherCode:210,from:"cloudy",to:"sunny",join:1}, {weatherCode:211,from:"cloudy",to:"sunny",join:1}, {weatherCode:212,from:"cloudy",to:"rainy",join:1}, {weatherCode:213,from:"cloudy",to:"rainy",join:1}, {weatherCode:214,from:"cloudy",to:"rainy",join:1}, {weatherCode:215,from:"cloudy",to:"snowy",join:1}, {weatherCode:216,from:"cloudy",to:"snowy",join:1}, {weatherCode:217,from:"cloudy",to:"snowy",join:1}, {weatherCode:218,from:"cloudy",to:"rainy",join:1}, {weatherCode:219,from:"cloudy",to:"rainy",join:1}, {weatherCode:220,from:"cloudy",to:"rainy",join:2}, {weatherCode:221,from:"rainy",to:"cloudy",join:1}, {weatherCode:222,from:"cloudy",to:"rainy",join:1}, {weatherCode:223,from:"cloudy",to:"sunny",join:2}, {weatherCode:224,from:"cloudy",to:"rainy",join:1}, {weatherCode:225,from:"cloudy",to:"rainy",join:1}, {weatherCode:226,from:"cloudy",to:"rainy",join:1}, {weatherCode:227,from:"cloudy",to:"rainy",join:1}, {weatherCode:228,from:"cloudy",to:"snowy",join:1}, {weatherCode:229,from:"cloudy",to:"snowy",join:1}, {weatherCode:230,from:"cloudy",to:"cloudy",join:0}, {weatherCode:231,from:"cloudy",to:"rainy",join:2}, {weatherCode:240,from:"cloudy",to:"snowy",join:2}, {weatherCode:250,from:"cloudy",to:"snowy",join:2}, {weatherCode:260,from:"cloudy",to:"snowy",join:2}, {weatherCode:270,from:"cloudy",to:"snowy",join:2}, {weatherCode:281,from:"cloudy",to:"snowy",join:1}, {weatherCode:300,from:"rainy",to:"rainy",join:0}, {weatherCode:301,from:"rainy",to:"sunny",join:2}, {weatherCode:302,from:"rainy",to:"cloudy",join:2}, {weatherCode:303,from:"rainy",to:"sunny",join:2}, {weatherCode:304,from:"rainy",to:"rainy",join:2}, {weatherCode:306,from:"rainy",to:"rainy",join:0}, {weatherCode:307,from:"rainy",to:"rainy",join:0}, {weatherCode:308,from:"rainy",to:"rainy",join:0}, {weatherCode:309,from:"rainy",to:"snowy",join:1}, {weatherCode:311,from:"rainy",to:"sunny",join:1}, {weatherCode:313,from:"rainy",to:"cloudy",join:1}, {weatherCode:314,from:"rainy",to:"snowy",join:1}, {weatherCode:315,from:"rainy",to:"snowy",join:1}, {weatherCode:316,from:"rainy",to:"sunny",join:1}, {weatherCode:317,from:"rainy",to:"cloudy",join:1}, {weatherCode:320,from:"rainy",to:"sunny",join:1}, {weatherCode:321,from:"rainy",to:"cloudy",join:1}, {weatherCode:322,from:"rainy",to:"snowy",join:2}, {weatherCode:323,from:"rainy",to:"sunny",join:1}, {weatherCode:324,from:"rainy",to:"sunny",join:1}, {weatherCode:325,from:"rainy",to:"sunny",join:1}, {weatherCode:326,from:"rainy",to:"snowy",join:1}, {weatherCode:327,from:"rainy",to:"snowy",join:1}, {weatherCode:328,from:"rainy",to:"rainy",join:0}, {weatherCode:329,from:"rainy",to:"snowy",join:2}, {weatherCode:340,from:"snowy",to:"rainy",join:2}, {weatherCode:350,from:"rainy",to:"snowy",join:0}, {weatherCode:361,from:"snowy",to:"sunny",join:1}, {weatherCode:371,from:"snowy",to:"cloudy",join:1}, {weatherCode:400,from:"snowy",to:"snowy",join:0}, {weatherCode:401,from:"snowy",to:"sunny",join:2}, {weatherCode:402,from:"snowy",to:"cloudy",join:2}, {weatherCode:403,from:"snowy",to:"rainy",join:2}, {weatherCode:405,from:"snowy",to:"snowy",join:0}, {weatherCode:406,from:"snowy",to:"snowy",join:0}, {weatherCode:407,from:"snowy",to:"snowy",join:0}, {weatherCode:409,from:"snowy",to:"rainy",join:2}, {weatherCode:450,from:"snowy",to:"snowy",join:0}, {weatherCode:411,from:"snowy",to:"sunny",join:1}, {weatherCode:413,from:"snowy",to:"cloudy",join:1}, {weatherCode:414,from:"snowy",to:"rainy",join:1}, {weatherCode:420,from:"snowy",to:"sunny",join:1}, {weatherCode:421,from:"snowy",to:"cloudy",join:1}, {weatherCode:422,from:"snowy",to:"rainy",join:1}, {weatherCode:423,from:"snowy",to:"rainy",join:1}, {weatherCode:424,from:"snowy",to:"rainy",join:1}, {weatherCode:425,from:"snowy",to:"snowy",join:0}, {weatherCode:426,from:"snowy",to:"rainy",join:1}, {weatherCode:427,from:"snowy",to:"rainy",join:2}, ] const DIN = 0; const CLK = 1; const CS = 2; const DC = 3; const RST = 4; const BUSY = 5; const EPD_width = 880; const EPD_height = 528; var date = new Date(); var color="black" const dayOfWeek = [ "日", "月", "火", "水", "木", "金", "土" ] ; const CALENDAR_API_KEY = 'APIKEY'; const HOLIDAY_CALENDAR_ID = 'ja.japanese#holiday@group.v.calendar.google.com'; const MY_CALENDAR_ID = 'カレンダーID'; let tid=setTimeout(()=>{ console.log("timeout"); obniz.sleepSeconds(60); obniz.close(); },25000); var obniz = new Obniz("OBNIZ_ID_HERE"); var spi; const canvas = document.getElementById("canvas"); canvas.width = EPD_width; canvas.height = EPD_height; canvas.style.border = "4px solid"; const ctx = canvas.getContext("2d"); ctx.rotate( -90 * Math.PI / 180 ) ; fetch('https://www.googleapis.com/calendar/v3/calendars/' + encodeURIComponent(HOLIDAY_CALENDAR_ID) + '/events?key='+CALENDAR_API_KEY+'&timeMin='+encodeURIComponent(date.getFullYear()+"-"+(date.getMonth()+1)+"-"+date.getDate()+'T00:00:00Z')+'&timeMax='+encodeURIComponent(date.getFullYear()+"-"+(date.getMonth()+1)+"-"+date.getDate()+'T23:59:59Z')+'&maxResults=5') .then(response => response.json()) .then(data =>{ if(data.items.length!=0){ color="red"; }else if(date.getDay()==0||date.getDay()==6){ color="red"; } fetch('https://www.jma.go.jp/bosai/forecast/data/forecast/270000.json') .then(response => response.json()) .then(data => { ctx.textBaseline = "top" ctx.textAlign = "left"; ctx.fillStyle = color; ctx.font = " 30px Arial"; let number=0; for(number=0;number<data[0].timeSeries[0].timeDefines.length;number++){ var date1=new Date(data[0].timeSeries[0].timeDefines[number]) if(date5.getDate()==date1.getDate())break; } var tmp=transcodelist.find(function(value){return value.weatherCode == data[0].timeSeries[0].areas[0].weatherCodes[number];}) let p ops=0; for(let i=0;i<data[0].timeSeries[1].timeDefines.length;i++){ var date1=new Date(data[0].timeSeries[1].timeDefines[i]) if(date5.getDate()==date1.getDate()&&pops<data[0].timeSeries[1].areas[0].pops[i]){ pops=data[0].timeSeries[1].areas[0].pops[i]; } } ctx.fillText("降水確率 "+pops+"%", -300,525); for(number=0;number<data[0].timeSeries[2].timeDefines.length;number++){ var date1=new Date(data[0].timeSeries[2].timeDefines[number]) if(date5.getDate()==date1.getDate())break; } ctx.fillText("最低/最高 "+data[0].timeSeries[2].areas[0].temps[number]+"/"+data[0].timeSeries[2].areas[0].temps[number+1]+"℃", -300,490); if(tmp.join==0){ const image = new Image(); image.src="https://obniz.com/ja/users/XXXX/repo/"+tmp.from+"_"+color+".png"; image.onload = ()=>{ ctx.drawImage(image, -475, 450,120,120); }; }else if(tmp.join==1){ const imagefrom = new Image(); const imageto = new Image(); const imagesymbol = new Image(); imagefrom.src="https://obniz.com/ja/users/XXXX/repo/"+tmp.from+"_"+color+".png"; imageto.src="https://obniz.com/ja/users/XXXX/repo/"+tmp.to+"_"+color+".png"; imagesymbol.src="https://obniz.com/ja/users/XXXX/repo/later_"+color+".png"; imagefrom.onload = ()=>{ ctx.drawImage(imagefrom, -500, 470,80,80); }; imageto.onload = ()=>{ ctx.drawImage(imageto, -400, 470,80,80); }; imagesymbol.onload = ()=>{ ctx.drawImage(imagesymbol, -425, 530,30,30); }; }else if(tmp.join==2){ const imagefrom = new Image(); const imageto = new Image(); const imagesymbol = new Image(); imagefrom.src="https://obniz.com/ja/users/XXXX/repo/"+tmp.from+"_"+color+".png"; imageto.src="https://obniz.com/ja/users/XXXX/repo/"+tmp.to+"_"+color+".png"; imagesymbol.src="https://obniz.com/ja/users/XXXX/repo/sometime_"+color+".png"; imagefrom.onload = ()=>{ ctx.drawImage(imagefrom, -500, 460,100,100); }; imageto.onload = ()=>{ ctx.drawImage(imageto, -370,500,50,50); }; imagesymbol.onload = ()=>{ ctx.drawImage(imagesymbol, -400, 460,40,100); }; } }); fetch('https://www.googleapis.com/calendar/v3/calendars/' + encodeURIComponent(MY_CALENDAR_ID) + '/events?key='+CALENDAR_API_KEY+'&timeMin='+encodeURIComponent(date.getFullYear()+"-"+(date.getMonth()+1)+"-"+date.getDate()+'T00:00:00.000+09:00')+'&timeMax='+encodeURIComponent(date.getFullYear()+"-"+(date.getMonth()+1)+"-"+date.getDate()+'T23:59:59.000+09:00')) .then(response => response.json()) .then(data => { var count=0; ctx.textBaseline = "top" ctx.textAlign = "left";//左 ctx.fillStyle = color; ctx.font = " 35px Arial"; if (data.items.length==0) { ctx.fillText("予定なし", -515, 670); }else{ var items={data:[]}; for(let i=0;i<data.items.length;i++){ if (data.items[i].start.dateTime){ var tmp= new Date(data.items[i].start.dateTime); var item = { 'tag':tmp.getTime(), 'time': tmp.getHours()+":"+( '00' + tmp.getMinutes() ).slice( -2 ), 'summary': data.items[i].summary, }; items.data.push(item); }} items.data.sort(function(a, b){ return a.tag - b.tag; }); for(let i=0;i<data.items.length;i++){ if (data.items[i].start.date){ ctx.fillText("終 日", -515, 670+count*40); ctx.fillText(data.items[i].summary, -380, 670+count*40); count++; }} for(let i=0;i<items.data.length;i++){ ctx.fillText(items.data[i].time+"~", -515, 670+count*40); ctx.fillText(items.data[i].summary, -380, 670+count*40); count++; }} }); ctx.textBaseline = "top" ctx.textAlign = "left";//左 ctx.fillStyle = color; ctx.font = "55px Arial"; ctx.fillText(date.getFullYear()+"年", -519, 10); ctx.font = "30px Arial"; ctx.fillText("本日の予定", -519, 620); ctx.textAlign = "center";//中央 ctx.font = "70px Arial"; ctx.fillText((date.getMonth()+1)+"月", -264, 10); ctx.font = "350px Arial"; ctx.fillText(date.getDate(), -264, 80); ctx.font = "50px Arial"; if(data.items.length!=0){ ctx.fillText(data.items[0].summary, -264, 390); } ctx.textAlign = "right";//中央 ctx.font = "55px Arial"; ctx.fillText(dayOfWeek[date.getDay()]+"曜日", -10, 10); ctx.strokeStyle = color; ctx.lineWidth = 5; ctx.beginPath(); ctx.moveTo(-529, 660); ctx.lineTo(0, 660); ctx.closePath(); ctx.stroke(); }); obniz.onconnect = async function () { spi = obniz.getFreeSpi(); spi.start({ mode: "master", mosi: DIN, clk: CLK, frequency: 1000 * 1000, drive: "3v", }); await EPD_Init(); await EPD_Write(); date.setHours(0,0,0,0); date.setDate(date.getDate()+1); obniz.sleep(date); await obniz.wait(1000); clearTimeout(tid); if (Obniz.App.isCloudRunning()) { Obniz.App.done({ status: 'success' }); } }; async function EPD_SendCommand(cmd) { //console.log("cmd=" + cmd); obniz.getIO(DC).output(false); obniz.getIO(CS).output(false); spi.write([cmd]); obniz.getIO(CS).output(true); } async function EPD_SendData(data) { //console.log("data=" + data); obniz.getIO(DC).output(true); obniz.getIO(CS).output(false); spi.write(data); obniz.getIO(CS).output(true); } async function EPD_Reset() { console.log("Reset"); obniz.getIO(RST).output(false); await obniz.wait(10); obniz.getIO(RST).output(true); await obniz.wait(200); } async function EPD_ReadBusy() { console.log("busy wait"); do { var epdbusy = await obniz.getIO(BUSY).inputWait(); } while (epdbusy); console.log("busy end"); await obniz.wait(200); } async function EPD_Init() { console.log("Init"); await EPD_Reset(); await EPD_SendCommand(0x12); await EPD_ReadBusy(); await EPD_SendCommand(0x46); await EPD_SendData([0xf7]); await EPD_ReadBusy(); await EPD_SendCommand(0x47); await EPD_SendData([0xf7]); await EPD_ReadBusy(); await EPD_SendCommand(0x0c); await EPD_SendData([0xae,0xc7,0xc3,0xc0,0x40]); await EPD_SendCommand(0x01); await EPD_SendData([0xaf,0x02,0x01]); await EPD_SendCommand(0x11); await EPD_SendData([0x01]); await EPD_SendCommand(0x44); await EPD_SendData([0x00,0x00,0x6f,0x03]); await EPD_SendCommand(0x45); await EPD_SendData([0xaf,0x02,0x00,0x00]); await EPD_SendCommand(0x3c); await EPD_SendData([0x01]); await EPD_SendCommand(0x18); await EPD_SendData([0x80]); await EPD_SendCommand(0x22); await EPD_SendData([0xb1]); await EPD_SendCommand(0x20); await EPD_ReadBusy(); await EPD_SendCommand(0x4e); await EPD_SendData([0x00,0x00]); await EPD_SendCommand(0x4f); await EPD_SendData([0x00,0x00]); console.log("Init end"); } await EPD_SendCommand(0x26); //RED let red = []; for (let i = 0; i < 110; i++) { red.push(0x00); } for (let k = 0; k < 529; k++) { await EPD_SendData(red); } await EPD_SendCommand(0x22); await EPD_SendData([0xc7]); await EPD_SendCommand(0x20); await obniz.wait(200); await EPD_ReadBusy(); } async function EPD_Write() { await EPD_SendCommand(0x4f); await EPD_SendData([0x00,0x00]); await EPD_SendCommand(0x24); //黒白データ送信 for (let k = 0; k <529; k++) { const imagelineData = ctx.getImageData(0, k, canvas.width, 8); //1行ずつキャンバスデータを取得 var black = []; var tmp = 0x00; for (let i = 0; i < 110; i++) { tmp = 0x00; for (let j = 0; j < 8; j++) {//8bitずつ読み出し if ( imagelineData.data[i * 32 + j * 4] == 0 && imagelineData.data[1 + i * 32 + j * 4] == 0 && imagelineData.data[2 + i * 32 + j * 4] == 0 && imagelineData.data[3 + i * 32 + j * 4] < 128 ) {//黒なら tmp |= 0x80 >> j; } } black.push(tmp); } await EPD_SendData(black); } await EPD_SendCommand(0x26); //赤白データ送信 for (let k = 0; k < 529; k++) { const imagelineData = ctx.getImageData(0, k, canvas.width, 8);//1行ずつキャンバスデータを取得 var red = []; var tmp = 0x00; for (let i = 0; i < 110; i++) { tmp = 0x00; for (let j = 0; j < 8; j++) {//8bitずつ読み出し if ( imagelineData.data[i * 32 + j * 4] >= 128 && imagelineData.data[1 + i * 32 + j * 4] == 0 && imagelineData.data[2 + i * 32 + j * 4] == 0 && imagelineData.data[3 + i * 32 + j * 4] > 100 ) {//赤なら tmp |= 0x80 >> j; } } red.push(tmp); } await EPD_SendData(red); } await EPD_SendCommand(0x22); await EPD_SendData([0xc7]); await EPD_SendCommand(0x20); await obniz.wait(200); await EPD_ReadBusy(); } </script> </body> </html>

アプリ設定

以上のプログラムを、obniz開発者コンソールのアプリ開発より
登録し、デバイスがオンライン時に実行を有効化します。
あとは、作成したアプリを使用しているobniz Board1Yへインストールしておきます。
キャプションを入力できます

最後に...

作業に取り掛かるのが、ぎりぎりになってしまい、ブログラムが非常に煩雑になってしまいましたが、
目標にしていた動作をすべて実装することができました。
サーバー側でハードの動きを記述できるobnizは、とてもIoT向きのデバイスだと思います。
現在は、乾電池でどのぐらい動作するのかを検証中です。

時間があれば、今回使用した電子ペーパーをパーツライブラリ化しようと思います。

本投稿の情報の利用、内容によって、利用者にいかなる損害、被害が生じても、著者は一切の責任を負いません。ユーザーご自身の責任においてご利用いただきますようお願いいたします。

sako_DIY さんが 2021/05/16 に 編集 をしました。 (メッセージ: 初版)
sako_DIY さんが 2021/05/16 に 編集 をしました。
sako_DIY さんが 2021/05/16 に 編集 をしました。
sako_DIY さんが 2021/05/16 に 編集 をしました。
sako_DIY さんが 2021/05/16 に 編集 をしました。
ログインしてコメントを投稿する