我也來做 MagicMirror (四) 自己寫 Module

話 說 前 一 排 ,我 自 己 DIY 了 一 個 MagicMirror without the mirror 的 project,那 個 12 星 期 月 曆 真 的 是 非 常 好 用 。在 那 個 project 我 安 裝 了 好 幾 個 不 同 人 士 開 發 的 3rd party module,以 給 我 的 MagicMirror 增 添 一 些 額 外 的 功 能 。別 人 寫 的 module,盡 管 不 如 人 意 ,但 卻 不 能 要 求 太 多 ( 這 是 家 教 、是 禮 貌 )。

例 如 我 在 project 裡 面 用 的 MMM-NOAA3  天 氣 module,本 身 設 計 是 異 常 的 精 美 ,但 可 惜 它 所 使 用 的 幾 個 外 國 天 氣 source,所 報 的 香 港 天 氣 都 唔 係 好 準 確 ( 至 少 和 香 港 天 文 台 的 數 據 有 點 不 一 樣 ,特 別 天 氣 預 測 更 是 相 差 頗 遠 )。而 天 氣 警 告 ( 8 號 風 球 、紅 雨 之 類 ) 更 是 全 世 界 都 不 一 樣 ,外 國 發 出 的 天 氣 警 告 真 係 睇 黎 都 無 乜 用 。

MMM-NOAA3

還 好 ,這 個 世 界 有 一 樣 野 叫 做 自 己 。你 嫌 棄 別 人 做 得 唔 好 時 ,先 別 罵 人 。麻 煩 你 自 己 先 動 手 做 一 做 ,到 你 做 得 好 過 別 人 10 倍 ,再 罵 不 遲 。

MagicMirror 並 不 是 一 個 可 以 獨 立 執 行 的 程 式 ,而 是 一 個 要 在 Electron 環 境 之 下 執 行 的 程 式 。Electron 是 一 個 結 合 了 Chromium 和 Node.js 的 環 境 ,令 到 你 可 以 用 一 般 網 頁 的 寫 法 ( HTML、CSS、Javascript ) 來 寫 桌 面 應 用 程 式 。

所 以 要 寫 MagicMirror 少 不 免 要 學 一 點 點 Electron。Electron 其 實 是 很 好 玩 的 東 西 ,加 上 它 跨 平 台 的 特 性 ,令 到 你 的 程 式 可 以 輕 易 地 在 三 大 桌 面 平 台 上 運 行 ( Windows、Mac、Linux )。比 如 說 原 本 開 發 給 Raspberry Pi 使 用 MagicMirror,亦 可 以 輕 易 在 Windows 上 運 行

如 果 你 本 身 已 經 很 熟 悉 寫 網 頁 ,那 寫 個 MagicMirror module 對 你 來 說 只 會 是 小 菜 一 碟 。本 文 主 要 針 對 如 何 開 發 MagicMirror module 這 一 部 分 ,對 於 HTML、CSS、Javascript,不 會 作 太 多 解 析 。如 果 你 不 懂 得 寫 網 頁 ,就 最 好 先 去 學 一 點 皮 毛 ( 其 實 我 懂 的 也 只 是 皮 毛 )。

MagicMirror² Documentation – Module Development Documentation

要 自 己 寫 一 個 module for MagicMirror,其 實 一 點 也 不 難 。MagicMirror 本 身 的 架 構 已 經 寫 得 十 分 好 ,也 有 很 詳 細 的 documentation,對 新 手 來 說 是 省 下 了 不 少 功 夫 。

根 據 官 方 的 說 明 文 件 ,所 有 的 modules 都 必 須 放 在 MagicMirror 的 modules subfolder 裡 面 。每 個 module 都 要 自 成 一 個 folder,例 如 上 圖 的 每 一 個 folder ( MMM-xxxxx ) 就 是 一 個 MagicMirror module。

每 個 module 裡 面 的 檔 案 亦 有 規 定 。每 個 module 都 要 一 個 主 檔 案 ,叫 core module script。這 個 core module script 和 module folder,都 要 和 module 名 一 模 一 樣 。

以 上 面 提 及 的 MMM-NOAA3 為 例 子 吧 。在 config.js 檔 裡 面 使 用 的 module name,module 的 folder name,以 及 core module script 的 filename,三 者 都 要 一 樣 才 行 ( filename 另 外 有 檔 尾 .js )。

一 個 最 簡 單 的 module,可 以 只 有 core module script 一 個 檔 案 。如 果 你 的 module 比 較 複 雜 ,有 很 多 檔 案 ,那 你 就 可 以 把 不 同 的 檔 案 放 在 不 同 的 subfolder,例 如 image、translations、css、js。

而 如 果 你 和 MagicMirror 的 作 者 一 樣 ,使 用 github.com 來 發 佈 你 的 module,那 你 的 module 就 會 多 出 幾 個 和 github 有 關 的 檔 案 。例 如 README.md 和 LICENSE。又 例 如 screenshots 或 者 examples,多 數 是 存 放 說 明 文 件 裡 的 圖 檔 。

如 果 你 使 用 electron 來 開 發 你 的 module,你 就 會 有 一 個 package.json。那 是 Node.js project 用 來 記 錄 project 詳 情 的 檔 案 。裡 面 特 別 重 要 的 是 dependencies。這 就 是 為 什 麼 你 安 裝 3rd party module 時 ,執 行 一 次 npm install 就 會 自 動 幫 你 install 幾 千 萬 樣 野 。

MagicMirror – 3rd Party Modules

但 讓 我 們 先 回 到 最 初 ,做 一 個 簡 簡 單 單 的 MagicMirror module。首 先 第 一 步 是 要 決 定 名 稱 。名 稱 不 能 與 別 人 的 module 相 同 ,所 以 我 們 最 好 參 考 一 下 上 面 的 3rd party modules 網 頁 ,盡 量 不 要 與 別 人 的 module 撞 名 。

MMM-HKWeather

我 用 名 稱 就 是 MMM-HKWeather。首 先 ,我 們 要 建 立 一 個 純 文 字 檔 ,作 為 我 們 的 core module script。內 容 就 參 考 官 方 的 說 明 文 件 。但 當 然 要 改 回 我 們 的 module name。

然 後 就 是 在 modules folder 裡 面 建 立 MMM-HKWeather subfolder,並 把 core module script 放 在 裡 面 。

要 MagicMirror 執 行 我 們 的 module,就 要 在 config.js 檔 案 裡 面 加 入 我 們 的 的 新 module。我 把 位 置 放 在 左 上 角 。現 在 是 一 個 全 新 的 module,並 沒 有 什 麼 要 config 的 。

我 的 環 境 比 較 特 別 ,因 為 唔 想 debug 時 太 辛 苦 ,所 以 我 要 先 把 我 的 4K 解 像 度 下 降 到 1080 x 1920 ( 依 然 是 直 度 )。然 後 為 了 唔 想 其 他 module 妨 礙 我 開 發 ,我 也 順 手 在 config.js 檔 裡 disable ( comment out ) 晒 所 有 其 他 的 modules。

然 後 我 們 就 可 以 第 一 次 運 行 新 module。因 為 是 開 發 新 的 module,所 以 在 這 裡 我 使 用 了 debug mode。先 打 開 Terminal,進 入 MagicMirror 的 folder,然 後 輸 入 npm start dev 指 令 。

成 功 之 後 就 會 見 到 上 面 的 畫 面 ,MagicMirror 的 左 上 方 出 現 了 Hello World!。右 手 邊 的 ,就 是 chrome browser 常 見 的 開 發 人 員 工 具 ,有 寫 開 網 頁 的 朋 友 應 該 不 陌 生 。程 式 執 行 時 有 什 麼 錯 誤 都 會 在 那 裡 顯 示 出 來 。

要 停 止 程 式 運 行 ,我 們 就 可 以 使 用 keyboard shortcut 「Ctrl-Q / Cmd-Q」。要 記 得 ,我 們 現 在 不 是 用 pm2 來 開 始 程 式 ,才 能 用 Ctrl-Q 結 束 程 式 。如 果 用 pm2 的 話 ,程 式 是 會 auto restart 的 ,所 以 不 管 你 Ctrl-Q 多 少 次 ,程 式 還 是 會 再 自 動 啟 動 的 。

好 了 ,現 在 可 以 回 到 core module file,寫 個 稍 為 複 雜 一 點 的 module。之 前 我 一 直 用 開 MMM-NOAA3,因 為 我 十 分 喜 歡 它 的 外 觀 。而 一 般 網 站 的 外 觀 ,主 要 就 是 在 css。我 因 為 平 面 設 計 水 平 是 負 無 限 大 ,所 以 我 也 只 好 仿 效 劉 備 去 借 一 借 荊 州 。

MMM-NOAA3 預 設 了 5 個 不 同 的 style,我 個 人 最 喜 歡 style 4,所 以 我 先 把 MMM-NOAA4.css 複 製 到 我 的 module folder,再 根 據 慣 例 把 它 rename 成 我 的 module name。css 檔 裡 面 的 class name ( 原 本 是 .MMM-NOAA3 ),也 要 replace 成 為 自 己 的 module name ( .MMM-HKWeather )。

要 在 module 裡 面 引 用 css 檔 ,我 們 就 要 用 到 MagicMirror 的 getStyles 方 法 。你 可 以 一 次 過 引 用 多 個 檔 案 。也 可 以 像 MMM-NOAA3 般 寫 個 if … then … else 來 根 據 使 用 者 設 定 的 參 數 來 決 定 引 用 那 一 個 css 檔 。

再 次 執 行 程 式 npm start dev,結 果 看 起 來 是 完 全 沒 有 分 別 的 。但 在 console 裡 面 就 已 經 悄 悄 的 多 了 2 行 css 檔 loaded 的 訊 息 。

好 了 ,正 所 謂 細 時 偷 針 ,大 時 偷 金 ;竊 錢 為 賊 ,竊 國 為 王 。有 時 啲 野 ,做 得 一 次 就 唔 怕 … ( 咳 咳 )。我 們 再 從 MMM-NOAA3 裡 面 借 點 code。

在 上 圖 中 ,我 在 core module file 加 入 了 start function。顧 名 思 義 ,這 是 程 式 一 開 始 執 行 時 會 做 的 野 。我 在 這 裡 initialize 了 幾 個 要 顯 示 出 來 的 數 據 。然 後 ,在 getDom function 裡 面 就 將 這 幾 個 數 據 顯 示 出 來 。

getDom function,最 後 要 return 的 ,就 是 一 個 DOM element ( DIV )。DIV 裡 面 你 喜 歡 包 含 什 麼 都 可 以 。在 上 圖 之 中 的 例 子 就 是 用 一 大 堆 div 砌 出 來 的 一 個 好 似 table 既 野 。因 為 我 要 偷 用 MMM-NOAA3 的 css,所 以 就 要 用 回 它 的 DOM element 的 結 構 。

這 一 堆 div 如 果 都 列 成 一 行 ,不 管 閱 讀 時 還 是 以 後 要 改 程 式 ,會 比 較 辛 苦 。所 以 我 們 會 使 用 multiline string 的 寫 法 。Javascript 也 支 援 好 幾 種 multiline string 的 寫 法 ,這 裡 用 的 叫 做 backticks。就 是 用 兩 個 叫 做 backtick ( Keyboard 1 字 的 左 邊 ) 的 符 號 把 multiline string 包 起 來 。

要 在 backtick 的 字 串 裡 面 顯 示 變 數 或 者 程 式 ,就 要 包 程 式 碼 包 在 ${ … } 裡 面 。

執 行 完 就 是 上 面 這 個 樣 子 。但 你 的 顏 色 不 一 定 是 藍 色 ,因 為 MMM-NOAA3 的 Style 4 是 會 自 動 變 色 的 ,呵 呵 。

特 別 要 一 提 的 是 上 面 的 table header,MMM-NOAA3 是 用 了 MagicMirror 的 translate function。那 是 自 動 變 換 顯 示 語 言 的 功 能 ,現 在 我 們 就 把 這 個 功 能 做 出 來 的 。

我 們 要 先 創 建 幾 個 json 格 式 的 language 檔 ,然 後 把 它 們 放 在 translations folder。

各 個 language 檔 就 是 上 面 的 樣 子 。

最 後 就 是 在 core module file 加 入 getTranslations function。這 個 language 是 由 MagicMirror 的 language 設 置 取 得 的 。如 果 MagicMirror 設 置 的 language 是 你 的 module 所 沒 有 的 ( 例 如 zh-tw ),那 MagicMirror 就 會 顯 示 英 文 ( 所 謂 的 translation fallback )。

你 可 以 自 由 更 改 MagicMirror 的 language 設 定 ,看 看 會 是 什 麼 樣 子 。特 別 值 得 一 提 的 是 大 家 也 可 以 用 en language file 來 縮 短 / 改 變 要 顯 示 的 文 字 ( Temperature -> Temp. )。

終 於 來 到 戲 肉 了 ,我 們 現 在 可 以 看 看 如 何 在 網 上 取 得 資 料 ,並 把 它 們 顯 示 出 來 。要 取 得 香 港 的 天 氣 資 料 ,可 以 使 用 政 府 的 資 料 一 線 通 服 務 。和 天 氣 有 關 的 API 也 不 只 一 個 ,我 這 裡 先 用 一 個 做 例 子 。

https://data.weather.gov.hk/weatherAPI/opendata/weather.php?dataType=rhrread&lang=tc

資 料 一 線 通 服 務 其 實 係 非 常 之 好 ,它 連 登 入 / 認 證 身 份 都 唔 洗 ,簡 而 言 之 就 係 任 你 用 。這 個 API 會 傳 回 一 個 json 資 料 。json 是 現 時 網 上 傳 遞 資 料 最 多 人 用 、最 好 用 、最 容 易 用 的 格 式 。

這 個 API 提 供 了 3 個 語 言 選 項 ,分 別 是 tc ( 繁 中 )、sc ( 簡 中 ) 和 en ( 英 文 )。我 們 暫 時 用 繁 中 ,下 一 步 我 們 才 補 回 這 個 語 言 設 定 。

我 們 先 在 core module file 加 入 一 條 custom function,updateWeather。要 取 得 網 上 的 json 資 料 ,我 用 了 javascript 的 fetch 方 法 。fetch 是 比 較 新 的 方 法 ,簡 單 之 餘 ,也 支 援 json 格 式 。

API 傳 回 的 一 大 堆 資 料 裡 面 ,temperature、humidity、unindex 都 是 一 個 array,我 都 是 只 取 得 第 一 個 數 據 ( data[0] )。這 沒 有 什 麼 特 別 的 原 因 ,純 粹 是 我 懶 惰 。你 喜 歡 的 話 ,也 可 以 將 module 寫 成 要 使 用 者 設 定 ( 在 config.js 裡 設 定 ) 那 一 個 數 據 要 使 用 那 一 個 氣 象 站 的 資 料 。

我 們 先 把 取 得 的 資 料 ,寫 入 之 前 initialize 左 的 parameter ( self.temperature / self.uv / … )。我 也 趁 機 把 資 料 變 得 human readable 一 點 ,例 如 加 上 單 位 ,又 或 者 為 UV 加 上 description。完 成 之 後 ,我 們 就 要 執 行 一 次 updateDom 方 法 。這 會 更 新 MagicMirror 所 顯 示 的 資 料 。

因 為 我 想 module 一 開 始 就 上 網 抓 取 資 料 ,所 以 我 把 updateWeather 放 在 start 裡 面 執 行 。

再 次 重 新 啟 動 程 式 ,如 果 見 到 氣 象 資 料 ,那 你 就 成 功 了 !

再 來 ,我 們 給 使 用 者 添 加 一 個 API 語 言 的 設 定 。我 們 在 defaults 裡 面 增 加 一 個 設 定 – lang,並 設 置 了 一 個 預 設 值 tc。defaults 裡 的 所 有 設 定 ,使 用 者 都 可 以 在 config.js 檔 裡 面 更 改 的 。

然 後 我 們 把 這 個 設 定 ,應 用 到 天 文 台 的 API 之 上 。

再 回 到 config.js 檔 ,我 們 同 樣 地 把 這 個 選 項 加 到 module 裡 面 。但 為 了 測 試 ,我 們 在 這 裡 把 lang 設 定 為 en。

重 啟 程 式 之 後 ,就 可 以 見 到 上 面 的 畫 面 。大 家 可 以 見 到 ,UV 的 description 已 經 變 成 英 文 的 了 ,因 為 我 們 改 用 了 英 文 的 API。在 這 裡 ,我 並 沒 有 把 interface 的 語 言 和 API 的 語 言 統 一 。這 當 然 是 因 為 我 懶 ,但 這 也 是 給 使 用 者 多 一 個 選 擇 ,特 別 是 那 些 系 統 語 言 不 是 使 用 繁 簡 英 的 使 用 者 。

最 後 ,天 氣 是 會 變 化 的 ,不 是 固 定 不 變 。所 以 我 們 的 module 也 要 定 時 上 網 抓 取 新 的 天 氣 資 料 。要 做 到 這 點 其 實 十 分 容 易 ,因 為 javascript 本 身 就 內 建 了 setTimeout 和 setInterval 方 法 。

我 們 先 在 defaults 設 定 一 個 updateInterval 的 預 設 值 。預 設 值 是 60 分 鐘 ,天 文 台 的 API 是 免 費 任 用 的 ,所 以 我 們 更 是 不 能 濫 用 。

然 後 我 們 在 updateWeather function 的 最 尾 ,加 上 一 個 setTimeout。這 個 setTimeout 有 點 特 別 ,就 是 call 返 自 己 updateWeather 這 條 function。執 行 結 果 就 會 像 個 loop 一 樣 ,不 停 運 作 下 去 。

至 此 ,這 個 HK weather module 的 所 有 主 要 功 能 就 完 成 了 !跟 著 要 做 的 ,就 是 加 多 幾 行 資 料 ( 繼 續 去 借 MMM-NOAA3! ),同 埋 要 從 幾 個 不 同 的 API 抓 取 不 同 的 資 料 。特 別 一 提 ,香 港 的 空 氣 質 數 資 料 不 是 天 文 台 的 ,而 是 環 保 處 的 ,但 同 樣 在 DATA.GOV.HK 找 得 到 。

經 過 不 懈 的 努 力 之 後 ,你 就 會 得 到 一 個 上 面 這 樣 的 module ( MMM-HKWeather )。很 好 玩 吧 ?

MagicMirror module 進 階 :Node Helper

我 的 MMM-HKWeather 並 沒 有 用 到 Node Helper,主 要 原 因 是 我 懶 ,真 正 原 因 是 不 需 要 。但 其 實 它 的 設 計 很 好 ,也 很 易 用 ,很 好 用 。

以 我 粗 淺 的 理 解 ,core module file 和 node helper,有 點 像 前 台 browser 和 後 台 web server 的 關 係 。於 是 ,有 些 browser 做 不 好 的 工 作 ,就 可 以 丟 給 後 台 做 ( node.js )。而 後 台 做 起 來 麻 煩 的 工 作 ,就 可 以 丟 回 給 前 台 做 。

下 面 我 們 來 看 看 一 個 有 趣 的 例 子 。原 本 呢 ,MMM-HKWeather 的 AQHI 資 料 是 由 下 面 這 個 source 抓 取 的 。

https://ogciopsi.blob.core.windows.net/dataset/aqhi/aqhi.json

這 是 一 個 json source,傳 回 的 資 料 也 十 分 簡 單 ,用 於 我 的 weather module 也 夠 有 凸 了 。

我 的 module 只 顯 示 了 一 般 監 測 站 的 指 數 ( 例 如 :2 至 3 )。但 我 也 頗 為 喜 歡 環 保 處 那 個 一 整 段 的 AQHI 簡 報 。

這 段 野 DATA.GOV.HK 也 不 是 沒 有 ,但 那 個 source 不 是 json,而 是 一 個 RSS ( xml )。

https://www.aqhi.gov.hk/epd/ddata/html/out/aqhirss_ChT.xml

如 果 我 們 嘗 試 在 core module 檔 裡 面 抓 取 這 個 source 的 話 ,就 會 出 現 CORS error。

這 時 候 ,我 們 就 可 以 在 node helper 裡 ,用 Node.js 的 request module 來 抓 取 這 個 source。

this.sendSocketNotification(notification, payload);

要 在 core module 和 node helper 之 間 傳 送 資 料 ,我 們 要 使 用 sendSocketNotification。

我 們 先 在 core module 檔 ,在 updateWeather function 裡 面 ,加 上 一 條 sendSocketNotification。因 為 是 要 node helper 幫 忙 抓 取 AQHI 的 資 料 ,所 以 就 把 這 個 notification 叫 作 AQHI 好 了 。然 後 payload 就 是 要 傳 遞 的 資 料 ,這 裡 傳 遞 了 core module 的 config,就 是 defaults 裡 面 的 東 西 。

然 後 我 們 就 要 建 立 我 們 的 node_helper.js 檔 。我 們 的 檔 案 很 簡 單 ,首 先 就 是 用 socketNotificationReceived 方 法 ,去 設 定 收 到 notification 之 後 要 使 什 麼 。我 們 先 把 core module 傳 來 的 payload 設 定 為 node_helper 的 config ( 就 是 共 用 了 config )。然 後 就 call 一 次 getAQHI function。

getAQHI function 也 很 簡 單 。先 是 用 了 config 裡 面 的 lang 參 數 ( 由 core module 傳 遞 過 來 的 ),決 定 要 用 那 一 條 url ( 不 同 語 言 的 url 不 一 樣 )。然 後 就 用 node.js 的 request module 來 抓 取 資 料 。

這 幾 條 url 傳 回 的 結 果 是 RSS ( xml ),要 在 node_helper 這 邊 處 理 的 話 ,又 要 安 裝 額 外 的 module ( fast-xml-parser?),還 不 如 把 結 果 傳 回 前 台 的 browser 處 理 。於 是 getAQHI 的 最 尾 ,又 用 了 一 次 sendSocketNotification,把 結 果 傳 回 core module。

在 core module 檔 ,我 們 也 要 設 定 socketNotificationReceived 方 法 ,來 接 收 node helper 傳 來 的 AQHI 資 料 。完 成 之 後 ,別 忘 記 要 updateDom 一 次 ,更 新 顯 示 的 資 料 。

當 然 在 getDom 裡 面 ,也 要 加 上 顯 示 AQHI 資 料 的 地 方 。

成 功 之 後 ,就 會 在 MagicMirror 看 到 AQHI 的 資 料 。當 然 ,這 麼 長 的 一 段 野 ,顯 示 在 Weather module 有 點 多 餘 。但 很 多 Weather module 也 會 在 mouse over 時 顯 示 額 外 資 料 的 ( 別 問 我 誰 會 在 MagicMirror 用 mouse,至 少 我 不 會 ! )。

HKWeather ( misterngan ).zip ( 本 文 的 例 子 ,含 node helper )

看 完 本 文 之 後 ,係 唔 係 好 想 即 刻 自 己 去 寫 個 MagicMirror module 呢 ?

ctleung張先生,男性,肖龍。
職業:I.T. Consultant
簡介:不好好讀書;七尺差五寸,手長過膝,雙耳垂肩;性寬和,寡言語,喜怒不形於色。據說少時曾斬白蛇於鳳凰山下……

This entry was posted in Computer & Network, STEM and tagged , , , , , , , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *