imuslab
托比的實驗記錄部落格
從零開始的 IoT 系統設計
說到 IoT (物聯網)很多人第一時間就會想起 Google Home ,Apple HomeKit,Amazon Echo 之類的智能喇叭(音箱),但是他們並不是真的跟 IoT 有關,他們比較像是一個輸入裝置或是一個 gateway 去控制其他裝置而已。 另外比較有技術背景的人可能會想起一些 IoT 的技術,好像說 Bluetooth BLE,Zigbee,2.4Ghz RF 跟 IR 之類的傳輸技術,也有另外一些人會想起 IFTTT、MQTT 之類的通訊協議。可是我們這次不是要說這個,而是 怎樣從零開始自己幹一套 IoT 控制系統出來 嘛,雖然說是從零開始但是總不能從沙開始煉硅晶吧?所以這裡的零界定為: 不使用任何專門為 IoT 設計的產品或軟件來制作這套 IoT 系統。 托比 因為之前我在高中畢業到大一之間的暑假我也曾經研究過一套叫 Home Dynamic 的 IoT 系統,所以這套東西就沿用這個名字叫做 Home Dynamic v2 吧! 選擇硬體平台 如果要開發物聯網系統,最基本的話就是需要有「物聯網」中「物」的部分吧?所以我就先由硬體開始選擇。這裡我有幾個要求 便宜,而且容易買到體積要夠小,而且輕便省電(因為要 24/7 的運作) 所以這裡就直接省卻了基於 ARM 處理器的開發板了。剩下的就只有 Arduino 系列用的 ATmega 或是更省電的 ATtiny 或者 ESP 系列的 ESP8266 或 ESP32 選擇連接協議 到了「物聯網」中「聯」的部分,到底要怎樣讓這塊板子連接到其他裝置上?市面上有很多不同的 IoT 連接協議及相關的硬體,常見的包括: Zigbee (通常用在 Mesh 網絡上 )Bluetooth BLE (通常用在需要超省電的裝置上)LoRa (通常用作長距離通訊)2.4Ghz RF (通常用作即時控制,如搖控車)WiFi (對,就是你手機筆電在用的那個 WiFi) 這裡的選擇其他也滿簡單的。首先,LoRa 一定用不上,香港的房子連轉身的空間也沒有,能連接數公里距離的 LoRa 一點用途都沒有。 Zigbee 的確是其中一個可以考慮的選擇,但是因為我也想透過現有的 ArozOS 系統控制它,而 Android、Windows 等作業系統都沒有原生支援 Zigbee,所以在這裡也不作考慮。 最後只剩下 3個選擇:BLE,2.4Ghz RF 跟 WiFi ,不過反正大家都是 2.4Ghz,而且因為我的 IoT 裝置都是用來控制電源、或是用來檢測太陽能板等很容易都抓到電源的地方,那當然選擇最容易可以透過不同手提裝置控制的選擇:WiFi 選擇網絡連接方式 說到「物聯網」的「網(絡)」,既然都跑 WiFi 了當然是用 HTTP 啊!雖然也有其他連接方式例如 MQTT 之類的,但是在選擇這個技術之前要考慮到幾點 這種技術會不會有 SPOF (Single Point of Failure)這種技術會不會需要額外的伺服器 / 硬體這種技術開發起來方不方便(畢竟是寫給自己用,賺不了錢所以太複雜了反而沒意思) 所以就用最簡單的 GET request 就好了啦。而且這方法也不需要 ESP32 內置的藍牙硬體,所以我們可以直接選擇最便宜的 ESP01 來做我們的控制器,一石二鳥。 可是,我們怎樣從網絡中找到 IoT 裝置? 這是一個好問題,而且之前我在開發 Home Dynamic 系統的時候也遇過類似的問題。但是作為剛剛升大學的我,當時我使用了一個最原始的方法:每個 IP ping 一次 但是在這次的開發計劃中(我把它叫做 Home Dynamic v2),我打算使用 mDNS 來取代這個(爛)IP 掃描方法。畢竟比起一次過把整個區網 ping 一次,例不如讓裝置自己 broadcast 自己的信息好了。剛好 ArozOS 也有內置到 mDNS 掃描器,所以就直接拿來做 IoT 掃描器不就行了嗎? 於是,我把 mDNS 的 transponder 塞到 ESP8266 裡面,然後跟據 ArozOS 的 mDNS transponder 結構寫了個這樣的 metadata 到 transponder 的回應信息裡面。以下為 hdsv2 的電源開關控制器的代碼: //Inject zeroconf attr into the MDNS respond (For scanning by ArozOS) void…
在 Go 伺服器上加入 Gzip Middleware 中繼器
話說最近因為有使用者提出想把 ArozOS 放到 Cloud VM 上面跑,所以想盡量壓縮 bandwidth 使用量,經過一大堆討論之後的結論就是在伺服器主要的檔案傳送之前先加個 gzip 壓縮器。 現時網上對於要應用 gzip middleware (我把它譯做「中繼器」,畢竟叫他做「中介軟體」好像怪怪的,跟中國大陸的叫法叫「中間件」又好像不太能表達意思) gzip middleware 原理 這個原理很簡單,就是在資料發出去之前先經過一層 gzip 以減少傳送的資料量,系統由原本的 要求 --> Mux / Router --> Handler --> 回傳 變成 要求 --> Mux / Router --> Handler --> gzip 壓縮 --> 回傳 因為要解釋起來很麻煩所以我就直接把 Go module 放這裡了: https://gist.github.com/tobychui/e31ca5be46e266cf52fc247dd38c9181 以下是一些使用例子: if *enable_gzip { http.HandleFunc("/media/", gzipmiddleware.CompressFunc(serverMedia)) }else{ http.HandleFunc("/media/", serverMedia) } fs := http.FileServer(http.Dir("./web")) if *enable_gzip { //Gzip enabled. Always serve with gzip if header exists http.Handle("/", gzipmiddleware.Compress(fs)) } else { //Normal file server without gzip http.Handle("/", mroutner(fs)) } 效果 以 ArozOS 的 desktop.system 界面來看,節省的流量大約如圖所示 上:有開啟 gzip 中繼器 下:沒有開啟 gzip 中繼器 嘛,基本上就這麼簡單而已。
ArozOS 1.110 穩定版發佈賀圖
就是因為與 Alanyeung 說到要不要在 ArozOS 1.0 首個 Stable release 的時候興祝一下,於是說著說著就說到了關於「不如畫一張賀圖出來」這樣的話題。 嘛,畫賀圖倒是沒問題啦,問題是誰畫? 結果因為只有 500 港幣預算問題,香港和台灣的繪師都敲不動,跑去了外國看看有沒有人願意接。想不到以同樣的金額還意外的多人畫,而且有的質素也十分不錯。 來自某 Freelance 網站截圖 結果我們就把繪圖要求外判出去,然後最近成品終於回來了。 這比我想像中的還要好啊啊啊(咳咳 就是這樣,我們就用這張立繪來做 1.110 Release 賀圖了! 設計中的 ArozOS 1.110 Release 賀圖
如何在無中文輸入法的 Windows 上,不用上網不用滑鼠輸入中文字符
這是一個滿有趣的問題,因為一般人在輸入中文的時候一定會用中文輸入法,或是網上的輸入法甚至是手寫輸入,可是我突然想到這個問題: 如何在無中文輸入法的 Windows 上,不用上網不用滑鼠輸入中文字符? 然後結果是可以的,而且比想像中的簡單。如果你想開發一個中文的物理輸入法(例如用 Arduino 輸入之類的)也可以用以下的 key combination 來試試看 Windows 輸入 Unicode 的方法 https://support.microsoft.com/en-us/office/insert-ascii-or-unicode-latin-based-symbols-and-characters-d13f58d3-7bcb-44a7-a4d5-972ee12e50e0 Inserting Unicode characters To insert a Unicode character, type the character code, press ALT, and then press X. For example, to type a dollar symbol ($), type 0024, press ALT, and then press X. For more Unicode character codes, see Unicode character code charts by script. 簡單來說,就是先輸入 Unicode 編號,然後再按 Alt + X。 那 Unicode 編號是甚麼呢? 一般的中文 Unicode 編號可以在 https://unicode.org/charts/ 找到: CJK Unified Ideographs (Han) 例如說要輸入「一」字,你可以對照上面這份列表 編號是 4e00,所以只要在 word / wordpad 輸入 4e00 然後點 Alt + X 就會變成 「一」了 然後就是要讓鍵盤自動化這個過程,並把剛輸入的文字移動到一開始聚焦的畫面,而這個也一點都不難,只要: Win + R 打開執行器 輸入 "wordpad" 按 enter 執行 wordpad 輸入 Unicode 編號,例如 4e00 Alt + X 轉成中文字元 Ctrl + A 選擇所有字元 Ctrl + X 切下所有字元 Alt + F4 關閉 wordpad n 在「是否要儲存」選擇「否」 //這個時候視窗會自動聚焦到原本的視窗上 Ctrl + V 貼上 這樣完全不連網絡,只用鍵盤輸入中文字就成功了 你說知道這個有甚麼用?我也不知道 🤔🤔🤔
在 Raspberry Pi 上設定 MHS 3.5寸屏幕並啟用 Chromium Kiosk 模式
如果你想快速的做一個 Prototype,通常開發者都會直用 Web Browser 作為 GUI 的首選。但是當去到需要部署在硬體上面的時候,到底要怎樣把整個 Chrome 搬到去 ARM 開發板上面呢? 以下這個一個教學將會記錄我 DIY Rpi DAC 時架設 Chromium 的經歷 選擇屏幕 這個應該不用多說,當然就是最便宜的那個吧!就是這樣,我想也沒想就買了這個 MHS 3.5寸屏幕。 收到後接上 Rpi 4,並安裝好 Raspberry Pi OS LITE (沒有桌面版),之後就是重要的部分了 安裝 xserver 跟 Chromium https://die-antwort.eu/techblog/2017-12-setup-raspberry-pi-for-kiosk-mode/ 跟著這個教學,首先我們需要更新 apt-get sudo apt-get update sudo apt-get upgrade 之後安裝 Xserver 等顯示需要用到的程序庫 sudo apt-get install --no-install-recommends xserver-xorg x11-xserver-utils xinit openbox -y 最後就是安裝 Chromium sudo apt-get install --no-install-recommends chromium-browser -y 設定 Openbox 並讓它啟動 Chromiuum 。編輯 /etc/xdg/openbox/autostart sudo nano /etc/xdg/openbox/autostart 並在裡面填入以下的東西 # Disable any form of screen saver / screen blanking / power management xset s off xset s noblank xset -dpms # Allow quitting the X server with CTRL-ATL-Backspace setxkbmap -option terminate:ctrl_alt_bksp # Start Chromium in kiosk mode sed -i 's/"exited_cleanly":false/"exited_cleanly":true/' ~/.config/chromium/'Local State' sed -i 's/"exited_cleanly":false/"exited_cleanly":true/; s/"exit_type":"[^"]\+"/"exit_type":"Normal"/' ~/.config/chromium/Default/Preferences chromium-browser --disable-infobars --window-size=320,480 --app-shell-host-window-size='320x480' --noerrdialogs --kiosk -app='http://YOUR_URL_HERE/' 這裡因為我的屏幕是 320 x 480的,所以所有 window size 的設定都是 320 x 480。請根據你的屏幕大小作更改。 安裝屏幕驅動 https://github.com/waveshare/LCD-show 安裝 git sudo apt-get install git -y clone 並安裝驅動 git clone https://github.com/waveshare/LCD-show cd ./LCD-show #把下面這行改成你屏幕的規格 sudo ./LCD35-show 如果你需要旋轉顯示角度,用這個指令 #無旋轉 cd LCD-show/ ./LCD35-show 0 #90度 cd LCD-show/ ./LCD35-show 90 #180度 cd LCD-show/ ./LCD35-show 180 #270度 cd LCD-show/ ./LCD35-show 270 測試 xserver…
Go 語言在 Linux IoT 開發板上處理大檔案上傳的解決方法
近年來不少本來用於物聯網開發的主板已經差不多具備 10 年前電腦主機的 IO 速度和處理水平了。在網上不難看到具備 1000Mbps 網絡接口並同時使用多核心的處理器的單片電腦(SBC),價格上也不太貴(約 100 - 120HKD 一塊),可玩性還是很高的。 其中我使用過的兩種開發板:ZeroPi 跟 OrangePi Zero Plus 然後,我想用它來組網絡儲存系統 對,理論上你是可以用這種 SBC 來做網絡儲存器(NAS),我們一個一個來看: ✔️ CPU: H3 / H5 處理器 ( 1.3Ghz ,一般作為文件伺服器完全沒問題) ✔️ 1000Mbps Ethernet / 網絡接口 ✔️ USB2.0 接口(480Mbps,對於家中只有 100Mbps 上下載的我來說完足夠) ✔️ 40 x 42 cm,可以直接黏在硬碟盒後面 ✔️ 運行時只需要 5V 0.2 - 0.3A,超級省電 可是問題就是它只有 512MB 的 RAM 啊!!! 現在這年代連最便宜的 Synology DS118 都要比它多 512MB RAM 啊! 可是,要這麼多 RAM 到底用來幹麼? Golang 網頁伺服器的文件上載的運作原理 一般來說使用 Golang 處理文件上載的邏輯都長這樣 r.ParseMultipartForm(10 << 20) file, handler, err := r.FormFile("myFile") if err != nil { fmt.Println("Error Retrieving the File") fmt.Println(err) return } defer file.Close() //然後用 io.Copy 把暫存檔案複制到目的地,這裡我就不仔細寫了 在一般電腦上運行這段代碼是完全 OK 的,在上載檔案時 Golang 會把 multipart form data 中的檔案緩存到 RAM,然後在 io.Copy 之後,defer 的 file.Close() 會把 Reader 關掉,之後交由 GC 處理刪除。 這情況只能適用於上載的檔案比系統能用的 RAM 還要細的情況,對於像 ZeroPi / Orange Pi 這種 SBC 要上載大型檔案,就不能用這傳統的方法,而需要特別處理了。不然就會出現以下情況 就是所謂的「爆 RAM」 那讓我們先來了解一下 parseMultipartForm 的原理吧,簡單來說是 你的 browser 把檔案塞到了一個 HTTP FORM 裡面browser 把 FORM 的資料連同你要的檔案在同一個(或多個) Request 裡面送到伺服器因為檔案太大,沒有辦法使用同一個 HTTP REQUEST 處理,所以到達的時候檔案可能被切成幾個 「區塊」,而這個就是 Multipart FormGolang 把這些「 區塊 」寫到 RAM 裡面(在 Debian 的情況下就是寫到 /tmp 裡面)然後如果檔案大小比RAM 大,/tmp 就會被塞爆,出現「no space left on device」錯誤如果 Golang 繼續把東西塞進去,它就會被作業系統殺掉(Killed) 要解決這個問題,我們可以使用兩個方法,首先是不要使用 Golang 內置的 Request library,自己重新寫一個。但是那樣實在太麻煩了,所以我決定直接在前端作更改 WebSocket 是個好東西 說到 WebSocket 很多人會想起類似 Chatroom 或是 Web 的 online game 之類,但是…
在 Debian Linux 下安裝 ZeroTier 網絡
由於寫教學太花時間了我就直接把需要的指令放到下面 # Upgrade and update the apt sudo apt-get upgrade sudo apt-get update # Install Git (if you didn't install it yet) sudo apt-get install git #Install Make and build dependencies sudo apt-get install make sudo apt-get install build-essential # Clone the source code of the zeroTier client cd ~/ mkdir build cd ./build git clone https://github.com/zerotier/ZeroTierOne # Optional: Install screen and run it inside screen so you can disconnect your ssh terminal sudo apt-get install screen -y screen # Start building the ZeroTier Client cd ./ZeroTierOne make # Optional: If you have screen installed (Hold Ctrl A + D to detach the terminal) 然後你就可以去吃個飯再回來了。這 Make 在 Intel 處理器的電腦上也行有夠久的,如果是 Rpi 之類的 SBC 就更不用想它會快了。 在它完成建置之後,你應該會看到類似這樣的檔案結構,如果 OK 的話就可以繼續: # Join your network using Network ID sudo ./zerotier-cli join <networkID> # Test if the compilation is working and setup the network sudo zerotier-one -d 前往 ZeroTier 的網站,幫新出現的裝置改名和設成 Auth 等一下之後他會顯示 DHCP 派發給這台裝置的 IP 地址,而回去 Linux 上面你使用 ifconfig 應該能看到相同的地址 如果是正確的話可以繼續: # Create a start script for the zerotier daemon sudo nano ~/zerotier.sh 把下面的資料放進去然後 Save + Exit (把 <user_name> 改成你現在在用的使用者名稱) #!/bin/sh sudo /home/<user_name>/build/ZeroTierOne/zerotier-one # Add zeroTier…
在 Raspberry Pi 安裝 Mercury USB WiFi AC650M 驅動
安裝步驟如下 準備所需的環境 下載 Raspberry Pi OS執行 apt-get update把 RTL8821CU 驅動源碼下載到本地端 mkdir -p ~/build cd ~/build git clone https://github.com/brektrou/rtl8821CU.git 4. 安裝 DKMS sudo apt-get install dkms 5. 更新 apt sudo apt update -y sudo apt upgrade -y 6. 安裝 bc 並 重新啟動 Raspberry Pi sudo apt-get install bc -y sudo reboot 7. 重啟後 cd 進去驅動目錄並更改 Makefile cd ~/build/rtl8821CU sudo nano Makefile 把下面此兩行 CONFIG_PLATFORM_I386_PC = y CONFIG_PLATFORM_ARM_RPI = n 更改成 CONFIG_PLATFORM_I386_PC = n CONFIG_PLATFORM_ARM_RPI = y 8. 處理 ARM 建置 flag 的問題 sudo cp /lib/modules/$(uname -r)/build/arch/arm/Makefile /lib/modules/$(uname -r)/build/arch/arm/Makefile.$(date +%Y%m%d%H%M) sudo sed -i 's/-msoft-float//' /lib/modules/$(uname -r)/build/arch/arm/Makefile sudo ln -s /lib/modules/$(uname -r)/build/arch/arm /lib/modules/$(uname -r)/build/arch/armv7l 9. 使用 DKMS 建置 sudo ./dkms-install.sh 10. 把 WiFi USB 插到 Rpi 上 11. 把 USB 裝置由 Mass Storage Mode 轉換成 WiFi Adpater Mode在轉換之前,你先要使用 lsusb 取得裝置的 ID,例如 0bda:1a2b ,然後使用 usb_modeswitch 指令進行轉換。如果還沒有安裝,可透過 sudo apt-get install usb_modeswitch 安裝 sudo usb_modeswitch -KW -v 0bda -p 1a2b systemctl start bluetooth.service 要讓它下一次開機都會自動轉換,編輯 usb_modeswitch sudo nano /lib/udev/rules.d/40-usb_modeswitch.rules 在以下這一行之前: LABEL="modeswitch_rules_end" 插入以下設置 # Realtek 8211CU Wifi AC USB ATTR{idVendor}=="0bda", ATTR{idProduct}=="1a2b", RUN+="/usr/sbin/usb_modeswitch -K -v 0bda -p 1a2b" 再次重新啟動後使用 sudo ifconfig 應該就能看到新的 WiFi 界面了(wlan1)
只需花 5分鐘就能阻擋 阻擋 AdBlock 網站的阻擋元件
話說我最近在 Facebook 上看到這一篇很有趣的文章,講述如何檢查出 Adblock 然後對該使用者彈出視窗來禁止他繼續閱讀: https://blog.reh.tw/archives/2273 大概會彈出這樣的視窗 先不論這方法是不是真的會讓讀者留久一點還是氣得直接按右上角的叉叉,對我這種習慣了網頁開發的野生技術程序員來說這簡直就是像彈出一張「戰帖」一樣。 好,我就來應戰吧! 對於要解決類似這種的 Adblock Blocker 其實很簡單,你只需要把阻擋你的東西移除掉,把 body 的 css 改一點點(通常是移除 overflow: hidden)就能夠繼續看的了。所以首先,我們要把找到擋著我們的東西。 我這裡使用的是 Firefox,你只要對擋著你的東西按右鍵 --> Inspect Element 之後到程式碼裡面找到代表這個部分的 div 的 id 或 class name 然後在 GreezeMonkey (Firefox 插件)開一個新的 script,引用 jQuery 跟 include 你需要阻擋 阻擋 Adblock 的網站,然後把全部相關的 id 跟 class 都 remove 掉就可以了。以下是一個非常簡單的例子: // ==UserScript== // @name REH TW Block AD Blocker // @version 1 // @grant none // @include https://blog.reh.tw/* // @require https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js // ==/UserScript== console.log("Removing Adblocker"); $("body").removeClass("modal-open"); $(document).find(".modal-backdrop fade show").remove(); $("#adBlockMsg").remove(); 然後我們重新載入網頁: 耶!阻擋 Adblock 的東西被我們阻擋了!
同步上載與 RAM 不足的問題
在 Raspberry Pi 上面執行類似 arozos 之類的大型系統不免會出現一個問題,就是當要處理大量 IO 的時候總會卡住(畢竟最快的儲存器( RAM、記憶體) 只有那 2 - 4GB,雖然說現在已經出到 8GB 了,但是那個價格我倒不如買台二手的 ThinkCentre M73?),所以要如何在有限 RAM 的情況下讓檔案上下載速度最佳化是其中一個很有趣的研究方向。 上載的方式 Single-thread Upload (單執行緒上載) 我們就先由正常的上載情況說起吧,我在這裡說的就是一般的 HTML5 Form 的檔案上載。每次一個檔案,很簡單吧?可是這速度實在太慢了,例如 Google Drive 之類的很久之前就已經用了多執行緒上載了,所以要一個一個檔案上載的話,速度是無法接受的。 Multi-thread Upload (多執行緒上載) 就是如此,我在這文章裡說的主要是多執行緒上載模式。簡單來說 Multi-thread Upload 就是由 Browser 對伺服器的上載接點打開多個 request,並同時向接點上載資料的方式。 那 Multi-thread Upload 又有甚麼問題? Multi-thread Upload 雖然能夠縮短使用者上載檔案的時間,能夠盡用 bandwidth,但是伺服器也要處理短時間內同時上載的檔案,導致系統的開發有點考功夫,有時候甚至要考慮 Race Condition 問題。 在以前純 PHP 5 + Apache 2的年代,伺服器每次只能夠同時處理一個要求,因此很少會出現資源不足的這個問題。但是到現在,很多人都轉用更先進的語言來開發網頁伺服器了,自然也需要處理到資源分配的問題。以下我就以 Go 作為「思考語言」來分析一下吧。 先 Buffer (緩存)到 RAM 不就行了嗎? Buffering 是一個常用來處理 IO 的方法,由於下載的時候就是由作業系統跟硬碟的緩存機制處理,所以我們只需要處理上載就好。我們就先假設系統採用以下的一個邏輯處理使用者上載的檔案 //建立一個 25MB 的 Buffer r.ParseMultipartForm(25 << 20) //從 POST 中取得 file file, handler, _ := r.FormFile("file") //建立儲存的檔案 dest, _ := os.Create(handler.Filename) //從 File 把內容複制到 dest if _, err := io.Copy(dest, file); err != nil { panic(err) } //關閉打開了的 file descriptor (通稱 fd,即檔案描述符) file.Close() dest.Close() 你那看到,在檔案上載到伺服器的一刻,伺服器已經分配了 25MB 的記憶體給這個 request 了。而這個 request 會把這空間卡住,直到 file.Close() 之前都不會把記憶體還給作業系統,因此每上載一個檔案 = 吃掉一個不少於檔案大小的記憶空間。而在 file.Close() 之前還有一個 blocking 的 io.Copy() 把上載的資料複制到最終的儲存位置(例如硬碟),所以最後就是要等硬碟寫完才能把記憶體釋放出來。 簡單來說就是這樣 所以,這東西很慢! 為了解決慢的問題,我們來做多執行緒的檔案搬運吧!Go-routine 是一個很神奇的東西,不需要幾行我們就可以寫出支援 Multi-threading 的程式,只要我們把 io.Copy 的部分改成: //建立儲存的檔案 dest, _ := os.Create(handler.Filename) //把複制的部分放到 Go routine,讓客戶端繼續上載下一個檔案 go func(dest *os.File, file *os.File) //從 File 把內容複制到 dest if _, err := io.Copy(dest, file); err != nil { panic(err) } //關閉打開了的 file descriptor (通稱 fd,即檔案描述符) file.Close() dest.Close() }(dest, file) 新的方法看起來像這樣,比起上面的方法快多了 對,這樣檔案上載的速度快多了,而且還能同時發起幾個上載的要求,但是這還在檔案比較小,RAM 裝得下的情況下都還好,但是檔案大的時候該怎麼辦? 例如說,在只有 2GB…