Recursion vs Dynamic Programming
剛剛我在寫 Leetcode 的 70. Climbing Stairs 沒留意到它是考 DP 寫成了 Recursion,結果跑起來 timeout。剛好我覺得這是一個很好的 Recursion vs DP 的範例,我就拿出來解釋一下好了。首先,這是我寫的 Recursion 解法: class Solution { public: int sol = 0; int climbStairs(int n) { f(n, 0); //把 sum 設為 0 return sol; } void f(int n, int sum){ if (sum > n) return; //超出了目標位置 if (sum == n){ sol++; //這是其中一個爬法 return; } //跟終點還有點距離,試試 f(n, sum+1); //爬一步 f(n, sum+2); //爬兩步 } }; 每爬一段之後,檢查是不是已經到終點了,如否,再分支出兩種走法 - 走一步 與 走兩步。 這個我覺得是最簡單易懂的寫法(甚至已經是教科書等級的 recursive function 了),但是居然跑到 timeout 這個我還真的沒想到。所以後來只好換成 space-time tradeoff 的 dynamic programming 寫法: class Solution { public: int climbStairs(int n) { int dp[46]; //因為題目說最多 n = 45 dp[1] = 1; //爬到第一格,只有一個爬法就是爬一格 dp[2] = 2; //爬到第二格,只有 1 + 1 跟 2 兩種爬法 for(int i = 3; i <= n; i++){ //從 3 開始,就是前兩格爬法的總和 dp[i] = dp[i-1] + dp[i-2]; } return dp[n]; // 最後回傳能爬到最後一格的總可能路線 } }; 果然是做嵌入式開發做得多,寫出來都是以最佳化 memory consumption 的職業病(
Leetcode 加速器 Cheat Code (C++)
最近因為工作面試的關系,我都在練習寫 Leetcode (畢竟從大學畢業之後我的主力語言都是 C (MCU / low level)、Go (Linux / Networking Applications)跟 網頁(HTML 、CSS 、JS 那堆)。雖然我都跟我面試的主管說「我都沒在寫 Leetcode」(因為我不想他們以「平常有寫多少 Leetcode 」為顧用考慮之一),但是或多或少因為有一段時間沒寫 C++ 了所以還是要找些免費題目來抓回以前的感覺。 最近我在寫 LeetCode 的時候發現一個滿有趣的現像,就是一般很多題目都會有一些特別特殊的極端例子(超快、或是超省記憶體)的情況。去翻一翻這些 code sample 之後我發現了一段很有趣的 code。 int speedUp = [] {std::ios::sync_with_stdio(0); std::cin.tie(0); return 0; }(); bool has[100002]; int digit(char c) { return c & 15; } bool isDigit(char c) { return '0' <= c && c <= '9'; } int init = [] { std::ofstream out("user.out"); std::cout.rdbuf(out.rdbuf()); for (string s; std::getline(std::cin, s); std::cout << '\n') { int n = count(s.begin(), s.end(), ',') + 3; memset(has, 0, n); for (int _i = 1, _n = s.length(); _i < _n; ++_i) { if (s[_i] == '-')for (_i += 2; isDigit(s[_i]); ++_i); else { int v = s[_i++] & 15; while (isDigit(s[_i])) v = v * 10 + digit(s[_i++]); if (0 < v && v < n) has[v] = true; } } for (int i = 1;; ++i) if (!has[i]) { std::cout << i; break; } } exit(0); return 0; }(); 沒錯,是一段 C++ 的 runtime cheat code。先上面的部分開始看,這邊它使用了 lambda function 來透過「假裝」定義 speedup variable 來執行了一些 STDIO redirect 的功能。我猜它應該是把之後執行的東西的 STDOUT 跟 parent 的 STDIN…
DIY 開源智能手錶(選螢幕篇)
最近因為各種原因想買一只智能(慧)手錶,可是由於我只喜歡方形的,所以剩下的選擇大概只有智慧手環、大號智慧手環跟 Apple Watch 可以選了。嘛,其實除了這些還有其他的啦,然而市場上就一堆很奇怪的選擇都沒一個完全對得上我想要的東西。 小米手環 8 Pro / 華為手環 8 :性價比跟功能都不錯,可是螢幕是像一般手環般長方形的不是方形Venu Sq 2:功能超棒,但是這個配色的品味只能說很獨特(?Fitbit (Google 旗下)Versa 4:這外形和黑邊遠看還以為是玩具Apple Watch SE:功能和外形都很棒,就是只支持 iOS 跟需要每天一充 說實話我覺得最好看的設計一定是屬於 Sony 的 Smartwatch 3 了。如果把這個設計換上最新的 AMOLED 螢幕和把那個用久了會黏手的 rubber wrist band 換成其他材質(如 silicon / TPU)就更完美了。 所以作為一個 DIY 愛好者,很自然的我會去想:假設我要自己做一只智能手錶的話,我會怎樣設計呢?首先,我想到的大概是以下的一些技術規格: 基於 ESP32 C3 的方案(在寫本文的時候最新支援 BLE + WiFi 的 MCU)AMOLED 螢幕 + 環境光傳感器(自動亮度調整)內置基本的傳感器(光學心跳 + 血氧、accelerometer(步數)、gyroscope(移動距離),如果有空位的話可以再塞個地磁 sensor 做指南針之類的) 然而有一些我也很想做的部分,是現在很多便宜的智能手錶沒有的 無線充電(對,現在很多大號手環還是在用磁石彈針來充電)Cloud First 資料傳輸(就是以 WiFi 為先,資料先到伺服器再同步到其他手機和裝置,有點像 ArozOS 的設計邏輯)開源、可由使用者自行編程(基於 ESP32 Arduino 內核 + LVGL?) 螢幕選擇 要做到上面的東西其實不難,最難的部分應該說是螢幕。螢幕這東西不像 電路板 可以隨便畫一畫送出去就能做,而是需要很大量的定制才會有這樣的產品。所以在設計這只手錶的電路板之前,就要先決定好手錶的外殼大小,而外殼大小就是取決於螢幕大小。 我覺得我也不是第一個遇到這樣困難的開發者。例如說著名的 Pebble 智能手錶之所以是長這樣是完全因為生產它的公司被 Sharp 生產的 Memory LCD display (1.33 inch)所限制,所以只能夠圍繞著這個當時唯一一片 high refresh rate 又省電的黑白螢幕來開發整個產品。 初代 Pebble 智能手錶 SHARP 1.33 inch Memory LCD display 雖然說現在很多智能手錶的出現而讓市場上多了很多 1.3 - 1.9 寸的螢幕,但是這又產生出另一堆問題: 便宜的智能手錶很多時候都會使用 TFT (IPS)螢幕大部分現有的 AMOLED 螢幕都是圓的很多做螢幕的廠商都只做螢幕、做電容觸控層的都只做觸控層、要找到非現有產品在用的螢幕總成很困難 單單是後兩個問題就把我能選擇的螢幕大小限制住了。把圓的 AMOLED 跟沒有觸控的去除掉之後,剩下的就只有 1.69 inch ,在開源 smartwatch project 很常見到的 waveshare 模組以外就差不多沒其他選擇了。 不過不得不說,這 1.69 雖然在黑色背景的畫面下看上去還不錯,但是一但亮起來你就會發現它黑邊粗得要死,還要上下闊度不同!(噴血 AMOLED 螢幕最佳選 然後就到這個,這是我在某中國的螢幕供應商那邊看到這片要價快 6 美元的 1.78 寸 AMOLED 螢幕,368 * 448 pixel https://www.dwo.net.cn/pd.jsp?id=11921 這個數字是不是有點眼熟,沒錯,這就是 Apple Watch SE 44mm 在用的那一片螢幕( 上下黑邊總於是一樣的了 只是說這片螢幕要用 ESP32 驅動會有點小問題:這東西只支援 SPI、QSPI 跟 MIPI DSI。MIPI DSI 先不用說了(這東西要用 arm 處理器才推得動),剩下的只有 SPI 跟 QSPI 能用。SPI 的問題在於即使是 ESP32 用 SPI half_speed (最高 80Mhz,但是考慮到 FreeRTOS 後面還有其他東西要跑,先把一半的 CPU cycle 預留給其他 vTasks 好了)速度來驅動它,你還是會看到動畫像 ppt 一樣(我猜的),但是我把計算資料塞給 chatGPT 後它也告訴我大概只能跑到 15.16 FPS,所以應該是肉眼可見的卡(? 這樣我就剩下 QSPI 可以選擇了。QSPI 大概就是拿四個針腳來推資料,理論上應該會快上不少。ChatGPT 說大概能推到 60fps,但是我先保守估計抓個 30…
Golang trie tree IP 轉國碼(Country Code)
最近因為有人 report 說 Zoraxy 的 GeoIP 功能不準,於是我在重新 review 這個 geoip database module 的時候發現:欸幹真的寫錯了欸!? 然後在花了一整個晚上 debug 之後,我終於找到正確的演算法和做法了。這裡為了省下後人在掘怎樣用 Golang 寫 IP 轉 Country Code 的 resolver,我這裡給大家一個快速上手的教學。 甚麼是 IP 轉 CC? IP 就是 IP 地址,CC 是 country code,有時稱為 ISO 國碼,例如說 HK TW US GB 之類的。IPv4 跟 v6 的地址區間通常會被 assign 到某一個國家的 ISP 上,而造成說只要知道 IP 地址,某程度上你可以知道那個 request 是從哪個國家出來的(先不要說 VPN 或是 ip spoofing 之類的部分的話)。可是由於每隔一段時間就會有 ISP 倒閉、收購或是重組之類的,所以 IP 轉 CC 的可靠度並非 100% 準確,這個時候我們就需要從一個可靠的來源來定期更新這一個 IP range 到 CC 的 mapping database 了。 這個 database 最出名的應該就是 maxmind 的 geoip database,人家都整理好所有數據到一個 database 檔案裡面,也提供 api 讓你可以快速查詢 cc,很多現代只會拉 library 的開發者通常就會直接選用它。然而,基於它的 license 問題,如果真接用它的方案的話會讓授權和 licensing 變得一團亂,所以基於多種考慮之下這個方案我們最終沒有使用,而是使用 Public domain 或 CC-0 的資料來源。 GeoIP Data Source 最後我們使用了來自 GeoFeed + Whois + ASN (CC-0)的資料來源: https://github.com/sapics/ip-location-db/tree/main/geolite2-country 問題是它的資料表是一個 csv 檔案,大概長這樣: 1.0.0.0,1.0.0.255,AU 1.0.1.0,1.0.3.255,CN 1.0.4.0,1.0.7.255,AU 1.0.8.0,1.0.15.255,CN 1.0.16.0,1.0.31.255,JP 1.0.32.0,1.0.63.255,CN 1.0.64.0,1.0.127.255,JP 1.0.128.0,1.0.255.255,TH 1.1.0.0,1.1.0.255,CN 1.1.1.0,1.1.1.255,AU 1.1.2.0,1.1.63.255,CN 1.1.64.0,1.1.127.255,JP .... 223.255.248.0,223.255.251.255,HK 223.255.252.0,223.255.253.255,CN 223.255.254.0,223.255.254.255,SG 223.255.255.0,223.255.255.255,AU 那假設我給你一個 ip 地址:A.B.C.D,你要怎樣把 country code 從 csv 裡面抓出來呢? 最直觀的答案:O(n) 當然如果會一點寫程式的人就會想到:那我把整個 csv loop 一次,看看哪一行的 start ip 跟 end ip 是包含我這個 ip 地址的就好了啦?這個的確是我們做給嵌入式裝置的做法(不過是為了省 memory),原理大概這樣: func isIPv4InRange(startIP, endIP, testIP string) (bool, error) { start := net.ParseIP(startIP) end := net.ParseIP(endIP) test := net.ParseIP(testIP) if start == nil || end == nil || test == nil {…
淺談 CSRF 與 CSRF Middleware
最近在我的 Zoraxy 開源專案那邊出現了這樣的一個 bug issue 他還很好的附了一個網頁測試介面的截圖 甚麼是 CSRF 攻擊? CSRF(Cross-Site Request Forgery,跨站請求偽造)是一種網絡攻擊技術,攻擊者通過在受害者不知情的情況下,冒充受害者的身份在受害者已經認證的網站上執行未經授權的操作。 CSRF 攻擊的工作原理 受害者登錄到可信網站:受害者首先登錄到一個需要認證的網站,在這例子是 Zoraxy 的後台管理頁面攻擊者製造惡意請求:攻擊者創建一個包含惡意請求的網頁或電子郵件,例如說這裡的一個 free burger 網頁 html + web form (action 或 form submit URL 指向 Zoraxy 的後台管理頁面)受害者訪問惡意鏈接:受害者在登錄狀態下 request 了攻擊者控制的網頁並提交了 web-form。執行未經授權的操作:由於受害者已經登錄(Auth Cookie 會跟著 form 一起送出去),Zoraxy webmin 介面會以為這些 request 是 valid 的,並執行相應的操作,例如上面例子的 toggle proxy 開關狀態 。 防禦 CSRF 攻擊的方法 CSRF Token:在每個受保護的操作請求中加入隨機生成的、唯一的 CSRF Token,並驗證這些 Token 以確保請求是合法的。檢查 Referer 和 Origin 頭:檢查 HTTP 頭中的 Referer 和 Origin 字段,確保請求是從合法的來源發出的。但是我在寫的是 Reverse proxy,本來 Referer 跟 origin 就是不可信的,所以不能使用此方法。雙重提交 Cookie:將 CSRF Token 存儲在 Cookie 中,並在請求中一起提交和驗證。使用 SameSite Cookie 屬性:設置 Cookie 的 SameSite 屬性為 Strict 或 Lax,限制跨站請求攜帶 Cookie。 說真的這也不是我第一次處理 csrf 問題。先前同一位使用者也對 ArozOS 提出過類似的 issue report。然而 ArozOS 因為是使用 agile 開發的,API 散落得到處都是,根本沒辦法用標準的方式來做 csrf middleware 來保護 API 接口,所以當時只用了一個非常不標準的方式來做(就是每一個 post request 之前都加一個要求 csrf token 的 ajax request,變成每個 request 都會變成要 request 2 次) csrf middleware csrf middleware 是一個 http Handler 並讓它來對 request 進行預處理並對不合規格的 POST (PUT / DELETE) 等 request 進行攔阻。所以這裡有兩個需要做的事情 在 HTML 頁面生成的時候,在頁面上加入一個 csrf token在有資料需要以 POST 等 request 回傳到伺服器時,在 payload 中以 header field 的方式附上頁面的 csrf token 在 ArozOS 的做法上,它是先對伺服器進行一個 csrf token generation request,然後再把 request 附到下一個 ajax POST request 裡面。這做法雖然不是不行,但是對於前端來說要改實在太複雜,而且一個 csrf token 又沒有那麼快 expire,不斷生的話只會浪費後端資源。 Production grade 的 csrf middleware 使用方法 由於 Zoraxy 的設計從一開始就按著標準的…
10 分鐘設定好 open web-ui 跟 ollama
open web-ui 是一個很方便的界面讓你可以像用 chat-GPT 那樣去跟 ollama 運行的模型對話。由於我最近收到一個 Zoraxy 的 bug report 指 open web-ui 經過 Zoraxy 進行 reverse proxy 之後出現問題,所以我就只好來裝裝看看並且嘗試 reproduce 出來了。 安裝 ollama 我這裡用的是 Debian,首先第一件事要做的當然就是安裝 ollama。教學在他們的網上有我這裡就直接寫 code 出來了。 https://ollama.com/download/linux curl -fsSL https://ollama.com/install.sh | sh 在執行這個 bash script 之後它會自動建立一個 systemd 的服務。預設 ollama 的 web server 只能透過 localhost loopback interface 存取,如果要透過其他網絡才能存取到 ollama 的 API 的話,我們就要讓它同時 listen to 其他的 network interface。最簡單直接的方式是把預設的 systemd service 檔案改成這樣: sudo systemctl stop ollama.service sudo systemctl edit ollama.service 然後在 service 檔案裡加入下面那行(見備注) ### Editing /etc/systemd/system/ollama.service.d/override.conf ### Anything between here and the comment below will become the new contents of the file # 加入下面這兩行 [Service] Environment="OLLAMA_HOST=0.0.0.0:11434" #下面的不要碰 ### Lines below this comment will be discarded ### /etc/systemd/system/ollama.service # [Unit] # Description=Ollama Service # After=network-online.target # # [Service] # ExecStart=/usr/local/bin/ollama serve # User=ollama # Group=ollama # Restart=always # RestartSec=3 # Environment="PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games" # # [Install] # WantedBy=default.target 儲存並退出之後重啟 ollama systemd 服務 sudo systemctl start ollama.service 抓模型 因為我比較喜歡用 CLI,所以我就直接透過 ssh 來順便把模型也載下來。這裡我在試玩的是 qwen:https://ollama.com/library/qwen 一般模型也會有不同大小,而我選這個是因為我要省空間(對,SSD 快要炸了),所以我就選了比較小的 1.8b 版本。你可以用這個指令來讓 ollama 準備這個模型: ollama run qwen:1.8b 如果要其他大小的模型的話,只要把後面的 1.8b 換成 4b / 7b 之類的就好了。另外比較有名的包含 llama3 之類也是可以透過這樣的方式下載。 值得一提 如果你的 root disk (Linux 的 / 或是 Windows 的 C: 硬碟)不夠空間跑你想測試的模型,你可以在啟動 ollama…
SEO 快速上手筆記
作為經營好幾個中小型的 Open Source Project website 的開發者,間中也會收到一些來自不同國家的 SEO 公司的垃圾郵件,說甚麼可以幫你把網站最佳化到上 Google 首頁結果之類的。首先,有幾點我要先說明 沒人知道 Google 的 Search Engine 排名演算法,而且很可能他們也是用 AI 來做的,他們自己的員工也不知道SEO 本身只是在 HTML file 的 head section 裡加一些 metadata 等級的簡單作業而已。就算要弄的話如果本身你會一點 HTML 跟知道要加甚麼 meta tag 的話其實也就一兩個小時的工作量就能做到外面公司的 80 - 90% 有效度。想在 Google 出第一個結果的最佳方法是幫你的產品改一個 Google Search 沒結果的名字(真的,不相信的話你去 Google 找 "Zoraxy" 看看,第一項應該就會出現我的 Zoraxy project 了,而那個 github page 根本除了 OG 以外沒做 SEO) 那正文要來了囉 HTML Head 結構 HTML Header 是一個簡單的 HTML tag。一般的話大概長這樣 <!DOCTYPE html> <html> <head> <!-- 這裡就是 Head 的部分--> </head> <body> </body> </html> 那一個完整,有做 SEO 的 head 會長怎樣呢?我們來看這個範例 <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="format-detection" content="telephone=no,email=no,address=no"> <title>托比的神奇蹦蹦主頁</title> <!-- HTML Meta Tags --> <title>imuslab</title> <meta name="description" content="創客產品與開源軟體項目,還有方便好用的各種網上小工具 | Archive of tobychui's maker products and open source projects, with online tools and service for myself but also for anyone who visited my site"> <!-- Facebook Meta Tags --> <meta property="og:url" content="https://imuslab.com"> <meta property="og:type" content="website"> <meta property="og:title" content="imuslab"> <meta property="og:description" content="創客產品與開源軟體項目,還有方便好用的各種網上小工具 | Archive of tobychui's maker products and open source projects, with online tools and service for myself but also for anyone who visited my site"> <meta property="og:image" content="https://imuslab.com/index/og.png"> <!-- Twitter Meta Tags --> <meta name="twitter:card"…
Zoraxy v3 – 適合新手的反向代理伺服器
最近我一直在進行相當多的 Zoraxy 更新。對於從未聽過 Zoraxy 的人來說,它是我的開源反向代理伺服器 (Reverse Proxy Server)。可以部署在你的 homelab 或server / NAS 裡面。 如果你對 Zoraxy 如何運作沒有興趣,單純是來找一個可以在 Windows 和 Linux 上運行的 Reverse Proxy Server,請按這裡。 v3 的新功能? 嗯,如果你之前使用過 Zoraxy v2,每次打開 Web 管理界面,你都會感到一陣沮喪的感覺。這是因為在開發 v2 的過程中,我沒有多餘的時間仔細考慮配色方案或設計一個具吸引力的界面配色(然而作為開源項目來說,這界面已經算是到達夠好 + 能用的設計)。 v2 homepage v2 的代理清單 現在 v3 長這個樣子 除了界面更新之外,Zoraxy 還添加了大量新功能,以 optimize proxy core 的功能(或者我可以說,因為有太多 user 是從 Nginx Proxy Manager(NPM)轉過來用 Zoraxy,所以有一些部分如果盡量接近 NPM 的設計會省下我很多處理 issue 的麻煩)。一般來說,這不是 NPM 的替代品,而更像是一個讓你更容易在測試和debug 時切換 endpoint 的 Reverse Proxy。但無論如何,我不介意人們提出感覺像 NPM 的 enhancement issues。 Default Site 在 Zoraxy v3 中,我們新增了對 Default Site 和多個 host name 的支援。 Zoraxy 的前身(名為“Web Proxy”,是 ArozOS 系統的一個子服務)設計僅用於處理單一域名的反向代理,v2 版本添加了對子域和其他主機名的支援,但由於是沒經過設計加進去的,所以對 user 來說有夠混亂( 使用者對 “Proxy ”、“Subdomain” 和 “Proxy Root” 的設定感到困惑然後又跑來開 issue。這就是為什麼在 v3 的設計中,我設計一個新的界面和設置邏輯。有了Default Site 的選項(對,就是 NPM 那個),那習慣用 NPM 的人可以輕鬆地切換到 Zoraxy 了。(順帶省下了我很多解釋東西怎樣用的時間) Default Site 提供 4 個選項供選擇。對於新手來說,你可能想要使用內部靜態網頁伺服器,在使用 "Static Web Server" 功能時將一個 "index.html" 文件作為你的網站首頁。這更像是傳統的 Apache 的用法,如果在 apache.conf 中沒有對應的 Virtual Host,所有的路由都會進入內置的靜態網頁伺服器,並從你的 /var/www/html 資料夾抓檔案來回應 request。 Redirection 和 404 Not Found 用起來也相對直接。對於 Redirection (重定向),你可以輸入要重定向的目標域名/IP 地址。在要將舊的(子)域名指向新域名,或者直接阻止那些未知/過時子域名的請求時滿有用的。 Wildcard 域名證書與 SNI 在 v3 中,我們 implement 了 TLS/SSL 證書查找邏輯中的 SNI。Zoraxy SNI 與其他 implementation 的區別在於,它不需要用戶輸入來將證書與特定主機名“鏈接”起來,而是會自動掃描並 serve 出對應的 TLS certificate。 在 v2 中,用戶需要手動為每個證書設置與匹配的域名。例如,如果你有一個證書可以涵蓋 a.example.com 和 b.example.com,你需要手動將其匹配關鍵字設置為 "example.com",才能使這個域名的 TLS 加密正常工作(不然會出現 self sign certificate error)。現在有了 Zoraxy v3 的自動證書查找邏輯,你不需要設定任何東西。只需上傳你的證書(或使用內置的 ACME 工具從你選擇的 CA 生成一個),Zoraxy 將自動解析並選擇正確的 certificate。唯一的限制是對於包含多個主機名的證書(例如 domain.com 和 anotherdomain.com),速度會稍微慢一些(可能需要 O(n)…
Concurrent & Racing Conditions (並發與競態條件)
最近因為我在開發 Zoraxy 時需要設計一個可以做到 high speed concurrent READ 跟間中 write 的 map,跟另一位高雄的工程師大佬討論了一個晚上,後來我發現原來滿多人對 concurrent / racing conditions 的處理有一定程度的誤解,所以我就來簡單的解釋一下最常見的誤解 - Check for locking。 問題 假設你有兩個 process,一邊要讀取一個無法同時讀寫的 map 資料結構(例如 Go 的 map[string]struct{}),另一個要寫入新的 key 到同一個 map。由於 map 並沒辦法處理 concurrent R/W,所以你要怎樣做呢? 解決方法 1:改用專用的 concurrent map (如 Go 的 sync.Map) 這個解決方法簡單直接,我就不再詳細解釋了。然而由於我在開發 Zoraxy 的 high concurrent access 特質,使用 sync.Map 的話會做成很高的 over-head,因此我並不是很想用它(但是也不影響 sync.Map 作為沒法解決時用的最後方案的決定) 解決方法 2:mutex lock 這算是比較正規的方案之一。在讀取和寫入之前先對 map (或是 slice) 進行鎖定,然後在寫入或讀取之後解鎖。這樣就能確保同時間只有一個 process 對這個 map 有 R/W access。但是這同時也會產生另一個問題,就是在 read lock 了之後,假設有另一個 process 也想同時進行讀取的話就必須要等前面的讀完,因此這個方案對於高並發的 request handling 來說並不是太有效率。 那聽到這裡的時候很多人會直覺的覺得:那只有 write 的時候 lock 就好啦,read 的時候檢查 map 是不是被 lock,沒 lock 的話就讀取。由於檢查的時候不會 lock,因此其他需要 read 的 process 也不會被擋不是嗎? 聽上去好像沒錯喔?讓我簡單寫一個 pseudocode 例子: function write(){ mutex.lock() map.write("foo", "bar") mutex.unlock() } function read(){ if (!mutex.isLocked()){ //Map is not locked var value = map.read("foo") print(value) }else{ //Map is locked, retry after 100ms delay(100) read() } } mutex 的部分可以是 programming language 自帶的 mutex 功能或是簡單的 boolean,效果都是一樣的。但是如果你有注意到,你不會找到 mutex.isLocked() 這個功能。 如果你需要用到這個,很大機會你寫錯了 這是為甚麼呢?那是因為 OS 層基本運作原理所引起的。這裡對於本科沒修 Computer Architecture (計算機架構)的工程師可能比較難理解。簡單來說就是,CPU 上就只有 n 個核心,但是你的作業系統上面有很多東西在執行,所以 OS 會對上面執行的工作(process)進行快速切換。每次切換的時候因為速度很快,所以會讓你有一種它們在並行處理的感覺。而實質上,每個 process switching 都會有一個 overhead,而這個 overhead 就是為甚麼 mutex lock 沒辦法被用於檢查的原因。 其中一種常用的 scheduling 演算法:Round Robin Process Switching 關於 CPU scheduling 跟 scheduling 演算法的詳情可以看這邊 所以為甚麼 Mutex 不能檢查是不是被其他 process 鎖上? 當你去檢查一個 mutex 是否被鎖的時候,檢查當下可能沒被鎖,但是檢查完到執行下一個邏輯的時候可能已經被另一個 process 鎖上了…
從零開始自幹房間自動化燈光系統(Part 1)
說到我房間裡的 IoT 控制系統,從 2021 年起我就一直在用一套自己開發的 IoT 協議名為 Home Dynamic。這套系統的原理和詳情可在我另一篇文章( 從零開始的 IoT 系統設計 )裡找到,但是簡單來說它就是一個很簡單的 HTTP RESTful request 模型下的迷你 IoT 系統,可以用於開關電燈和透過 Tasmota firmware 來控制幾個 Sonoff 的 WiFi Smart Switch 而已。 到最近,因為我香港的房間要整修,所以我就把房間裡有點 hacky 的系統升級到新的自動化系統。Home Dynamic v2 (簡稱 HDSv2)雖然在設計的時候已經有考慮到很多不同的使用場景和可能出現的問題(如 off grid control,伺服器 / relay 的 SPOF 等等),但是其控制 latency 總是讓我覺得有點煩惱。即使後來 ArozOS 改用 Golang 重寫,從按按鈕到電燈真的亮起的時間大概也只是在 2 - 3 秒內,這個速度跟我去按一個物理開關還是有很明顯的差別。所以在新裝潢的房間裡,我採用了一個更新版本的 HDS 協議來開發我的新自動化系統,現在我把它暫稱為 HDSv3。 控制器硬體部分 那說到 IoT 跟自動化,在提軟體之前一定要先講講硬體。畢竟只要硬體夠強,軟體再爛也是能跑得很順暢(望向 python) 。這次為了未來的擴展性和標準化,這次我選用了半工業級的硬體。下圖中是我用的繼電器模組,是一片市售的 4 路 WiFi 繼電器模組。它是使用 ESP12E 作為主控制器,跟先前的 HDSv2 一樣都是基於相同的 MCU。因此大部分 HDSv2 的源碼能夠直接移植到 v3 然後下載到這片板子上。雖然這片東西可以使用 110 - 220V 輸入,但是因為我原先整套系統都是用 24V 的,因此這部分的機能就白白被我浪費掉了( 那當然,這片東西不能就這樣放在我的桌面上。因此我買了一個 DIN Rail 用的外殼,然後把整個組好的安裝到 DIN Rail 上。 現時的照明控制系統 那你可能會好奇,既然是控制燈光,那一個簡單的繼電器模組就可以做到了。旁邊那兩個盒子又是甚麼? 如果你仔細看一下那個繼電器模組的照片,你就會發現那個模組真的單純在做切換而已,它本身並沒有供電能力。所以為了讓接上去的 LED 燈條有電力供應,就必須透過另一個裝置為 Relay 的 NO (Normally Open)端供電。這裡中間的模組就是作為一個 power distribution hub 的用法來提供 4 個相同於 Vin 的輸出和 3 個可調節的輸出。 大概原理 而最左邊的是 LED strip 的限流模組,大概就是因為 24V 的燈條用 24V 供電的話會太光,所以那個小黑色模組裡面是一個簡單的 XL6009 自動升降壓模組來把電壓調整到大約 19.2V (我喜歡的亮度) 原本是長這樣的,後來想了想還是把它放到 DIN Rail 上比較好 軟體與控制部分 講到軟體部分,就不得不提一下 IoT 技術了。說真的,雖然我知道很多不同的 IoT 通訊協議,從 MQTT(DIY 很多人用,基於 TCP/IP)、 Zigbee (IKEA 那套)到工業用的 RS485 / RS232 之類的,我還是比較喜歡用 2.4Ghz WiFi。雖然距離短、用電量高,但是本來我的裝置就是用來控制電源的(aka 長期會有電源供應),加上香港地方實在太少,我想盡量省下一個放 hub 的空間 + 本身房間已經有 WiFi 覆蓋,所以自然地 WiFi 是最好的選擇,所以我還是選擇了基於 TCP/IP 的協議。 考慮到設備的相容性、不用額外安裝程式便能控制等優點,在眾多基於 TCP/IP 的協議下我選擇了 RESTFUL。一來是所有手機都有網頁瀏覽器,即使伺服器炸掉只要有一台手機能連接到同一網絡便可以透過 p2p 的方式控制 IoT 裝置。另一個好處是 RESTFUL API 實在太通用了,從 ESPHome 到 Home Assitant,最好支援的裝置類型依然是基於 RESTFUL API 的裝置,再加上怎樣看 HTTP request 這東西在未來 10 年應該都會還存在(畢竟是 Web 技術的基礎),選它一定沒錯( 而為了解決…
目前第 1 頁,共有 11 頁