Download - 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例
用 Ruby 開發 IoT 應用以 RubyConf.tw 打卡系統為例曾亮齊 (Henry Tseng)
$ whoami
曾亮齊 (Henry Tseng)
A.K.A. lctseng (Github, Twitter…)
5xRuby DevOps
Rails Girl Taipei 教練中研院資科所短期研究助理國立臺灣大學 資訊工程系獨立遊戲『軍官之歌』 Programmer(80,000+ lines of Ruby)
這是一個好不容易做出系統後,又全部打掉重練的故事…
緣起Rubyconf.tw 2015
Python 版的打卡機 (Codeme)
使用 Raspberry PI + RFID Reader
沒有繼續維護功能有限,難以擴充
緣起Ruby 圈仍無類似的作品公司買了幾台 PI 與讀卡設備以 Ruby 重刻打卡系統為 2016 RubyconfTW 做準備
打卡設備
打卡設備Raspberry PI 3
RFID Reader (MFRC522)
蜂鳴器 (DC buzzer)
設計理念當初是以打卡系統為設計方向
掌握會眾流向與人數可以傳送 Sensor 資料並回報結果
例如打卡紀錄希望裝置可以由中央掌控
Conf 用打卡系統運作流程會眾於報到時發給一張 RFID 晶片卡貼紙,綁定個人資料會眾於各場次入場時,可在門口的打卡機進行打卡各贊助商攤位上也有一台屬於該攤位的打卡機
會眾可以透過打卡的動作,將聯絡資料留給攤商不必再使用傳統的紙本資料
實際 Demo
功能設計可以傳送 Sensor 資料並回報結果報到時,檢查該張卡片是否重複使用打卡時,檢查該張卡片是否已報到過根據不同的結果發出不同聲響
功能設計希望可以由中央掌控Agent: 打卡機; Manager: 中控程式Agent 難免會斷線或需要重啟不可能隨時連線進去分散在各處的 Agent
掌握 Agent 存活的狀況Manager 向 Agent 下達指令( 重啟、關機、發出聲響 )
連線設計使用 HTTP 而不是 Raw TCP
可套用現存的 SSL(HTTPS)
可輕易整合到 Rails 中 (HTTP API)
連線設計雙向連線
非同步指令( 重啟、關機 )
Check-in Server
(Manager)
Agent
打卡結果( 卡號 )
連線設計傳統 HTTP API
非同步指令Polling
Check-in Server
(Manager)
Agent
打卡結果偵測到卡片再發出request
setTimeout()
連線設計WebSocket 的使用HTTP + 雙向連線
可以在 Javascript 端寫 callback ,讓 server 端送資料來呼叫Rails 中已有 WebSocket 的實作
Action Cable
不必注重連線管理專注於資料交換
連線設計使用 Action Cable
專替 Web Application 設計官方 Client 為 Javascript
當時並無找到 Ruby 的 Client 端實作模組化:分離連線端與晶片控制端
未來若有更好的 Client 端可隨時抽換
連線設計暫時的解決方法
用 Ruby 包裹 browser 執行 JS
為了提供完整的 JS 環境,使用 browser 是最直接的
AgentCheck-in Server
(Manager)
連線設計在指令介面中使用瀏覽器?如何在 CLI 中啟動以 GUI 為主的瀏覽器如何以指令的方式控制瀏覽器的行為
解決方法:Xvfb + headless
Xvfb + headlessVirtual framebuffer X server for X Version 11
在沒有圖形介面的情況下,執行任何需要圖形介面的程式用於測試例如可在純指令介面的情況下進行網站的整合測試 (Selenium, Watir)
Xvfb + headlessheadless
Ruby gem
https://github.com/leonid-shevtsov/headless
Ruby interface for Xvfb
使用 headless 啟動瀏覽器以文字操作的方式送指令來執行 Javascript
Xvfb + headlessrequire 'headless'require 'watir-webdriver'
# 啟動虛擬視窗@headless = [email protected]
# 在虛擬視窗中執行瀏覽器 ( 如 Firefox) @browser = Watir::Browser.new
# 前往網頁 URL @browser.goto(url)
Xvfb + headless
# 檢查 ActionCable 是否有連上?def check_website_connected? @browser.driver.execute_async_script(%{arguments[arguments.length - 1](typeof(App) != 'undefined')})end
Xvfb + headless# 透過 ActionCable 傳送字串 (str)def write_string(str) @browser.driver.execute_async_script(%{arguments[arguments.length - 1](App.device.connect(#{str.to_json}))}) end
# 讀取來自 Manager 的資料# 此範例中資料被紀錄在 windoow 的某個 property 中,透過函式來取得def read_string @browser.driver.execute_async_script(%{arguments[arguments.length - 1](window.retrieve_data())})end
Xvfb + headless
# 關閉瀏覽器@browser.close
# 關閉虛擬視窗 @headless.destroy
連線設計後來並沒有採用上述的方法單純為了連線而跑 browser 實在不合理
action_cable_client
開發數個月後出現的 gem
純 Ruby 的 Action Cable client library
連線設計Manager 端的設計直接使用 Action Cable 的 Server 端 API
作為打卡系統的一部分寫在 Rails 中管理 Agent 與驗證卡號的邏輯寫在一起
實際部署Rubyconf.tw 2016
約 17 台 Agent
基本上該有的功能都有完成當初設計時有些缺失導致常常為了設備問題東奔西跑
問題action_cable_client 反應不夠靈敏尤其是網路不穩時會場網路偶爾會斷線Timeout 時間太長, Agent 沒有寫斷線回報機制斷線常常需要人為檢查
問題action_cable_client gem 仍在發階段偶爾會發生連線卡死 (網路正常,就是連不上 Server)
有時網路中斷恢復後,除非重開整支程式,否則連線無法恢復雪上加霜:所有工作都在同一個 thread
斷線時整個程式都要重開
Refactor重新設計連線相關的組件仍使用 WebSocket ,但不再依賴 Action Cable
不再依賴有潛在問題的 gem
更加即時的連線檢查與重新連線分離 Manager 與 Rails app
不需要 Action Cable ,因此可與 Rails 切割開
Agent 重新設計元件化每個元件 (網路連線、讀卡機、蜂鳴器 ) 各成組件每個組件由各自的 thread 跑 event loop
定義 event 作為元件間的溝通系統指令、讀卡訊號、聲音 ( 蜂鳴器 ) 訊號、連線狀態等
當有元件出錯時,重開該 thread 即可達到重啟效果權責分明:容易加入 /移除元件
Agent 架構圖Master ( 管理所有組件、廣播 event)
Buzzer 蜂鳴器組件• 接收 event: 聲響控制
Card Reader 讀卡機組件• 發出 event: 卡號資料
Connection 連線組件•透過網路轉送 event 給 Manager,或從 Manager 接收
event
Manager 重新設計分離自 Rails ,可獨立運行
分離打卡邏輯與裝置管理專注於裝置管理
Rack-based
保留與 Rails 結合的空間 ( 讓 Rails app 處理打卡邏輯 )
如同 ActionCable ,可 mount 至 Rails app 中
HTTP / Web Socket request
Router
Rails app
Manager
Manager 重新設計
Rack socket hijacking
當 request URL 滿足指定 path 時,交給 Manager 處理其餘則交給 Rails app
根據 URL 判斷
mount Tamashii::Manager::Server => '/tamashii'
config/routes.rb
Manager 架構 (standalone)
Channel PoolChannelClient
Check-in
Server
Channel Channel
Manager 架構 (with Rails)
Channel Pool
Channel
Client
Channel Channel
Check-in Server
當前進度新架構名為 tamashii
目前共有三個 gem
tamashii-agent : Agent 端,安裝於 PI 上tamashii-manager : Manager 端 ( 獨立於 Rails)
tamashii-common : Agent 、 Manager 共用組件Test framework: rspec
使用 tamashii-agent設備: Raspberry PI 3 、已安裝 Ruby (MRI)
包含 RFID 與蜂鳴器安裝 gem$ gem install tamashii-agent
使用 tamashii-agent單次執行 (one-start)
需要能夠存取 GPIO 的權限 ( 如 root)
作為系統背景程式 (daemonize)
$ tamashii-agent -C agent_config.rb
$ tamashii-agent --install--systemd
Tamashii::Agent.config doenv 'development'auth :tokentoken 'abc123'
end
使用 tamashii-manager
安裝 gem
Standalone Server$ gem install tamashii-manager
$ tamashii-manager -C manager_config.rb -p 3000
Tamashii::Manager.config doenv 'development'auth :tokentoken 'abc123'
end
使用 tamashii-manager
Mount 在 Rails app 中假設希望在 /tamashi 這個路徑上執行 Manager
在 config/routes.rb 中加入manager_config.rb 則是改放到 config/initializers
mount Tamashii::Manager::Server => '/tamashii'
使用 tamashii-managerManager 收到來自 Agent 的訊息後,會直接執行預設的 handler
例如:廣播封包到整個 channel
並不會直接交給 Rails app 來處理
ManagerAgent Check-in
使用 tamashii-manager這個設計理念是基於 Manager 應該與 Rails app 脫鉤若要以脫鉤的概念來看, Rails app 應該也要加入 channel
利用 channel 廣播的特性,傳送訊息給 AgentChannel Pool
ChannelClient
Check-in
Channel
Channel
使用 tamashii-managerHook
若 Manager 與 Rails app 同在一個 Rack 底下 可透過此方式『監聽』甚至『攔截』訊息透過 Manager API 發出訊息
ManagerAgent Check-in
Hook
使用 tamashii-manager使用 Hook ( 類似 middleware)
新增一個 Hook Class
設定 config/initializers/manager_config.rb
class TamashiiRailsHook < Tamashii::Hookdef call(pkt)
if some_condition?true # 此訊息會被攔截,不執行
handlerelse
false # 此訊息會執行預設的 handler
end end
end
Tamashii::Resolver.config do hook TamashiiRailsHook
end
使用 tamashii-manager使用 Manager API 傳送訊息給 client (agent)class TamashiiRailsHook < Tamashii::Hook
def initialize(*args) super @client = @env[:client]
end
def call(pkt)# …pkt = Tamashii::Packet.new(...)@client.send(pkt.dump)
endend
測試模式 (Agent)非測試模式下, Agent 需要真的硬體設備才可以運作
否則硬體元件相關的程式碼會初始化失敗使用測試模式脫離硬體元件測試邏輯跑 rspec
測試模式 (Agent)使用測試模式
偽讀卡機與偽蜂鳴器模擬硬體設備的行為偽讀卡機:隨機一段時間後,造出一個卡號訊息偽蜂鳴器:以文字顯示的方式取代發出聲音
Tamashii::Agent.config doenv 'test'auth :tokentoken 'abc123'
end
測試模式 (Agent)Master
BuzzerAdapterBuzzer
GPIO Buzzer
FakeBuzzer
CardReaderAdapter
CardReader
MFRC522
FakeCardReader
Connection
Non-testmode
Testmode
測試模式 (Agent)部署測試模式
使用 Standalone Manager
使用預設 handler (廣播封包 )$ tamashii-manager -C manager_config.rb -p 3333
Tamashii::Manager.config doenv 'test'auth :tokentoken 'abc123'
end
測試模式 (Agent)部署測試模式
使 Agent 連上 Manager$ tamashii-agent -C agent_config.rb
Tamashii::Agent.config domanager_host
'127.0.0.1' manager_port 3333env 'test'auth :tokentoken 'abc123'
end
測試模式 – Agent 端
測試模式 – Manager 端
測試模式剛才的範例中, Agent 似乎有 error
測試模式Error 原因:沒有建立 handler
Manager 端只有單純廣播訊息並沒有告知 Agent 該張卡片的驗證結果Agent 端等待一段時間仍沒有得到回覆,視為發生 error
測試模式範例:在 Standalone 模式下實作簡易 handler
接收 Agent 的卡號資訊,將驗證結果廣播回去驗證結果僅簡單使用一對一錯的方式第一組卡號: no
第二組卡號: ok
測試模式直接在 manager_config.rb 中實作
class MyPacketHandler < Tamashii::Handler
def self.init_counter@@counter = 0
end
# 一定要實作的方法def resolve(request_json)
request_data = JSON.parse request_json
@@counter += 1client = @env[:client]packet_id = request_data["id"]card_id = request_data["ev_body"]
測試模式pkt_type =
Tamashii::Type::RFID_RESPONSE_JSONresult = {
auth: @@counter % 2 == 0,reason: "You are: #{card_id}“
}
# packet data 一定要是字串pkt_data = {
id: packet_id,ev_body: result.to_json
}.to_json
pkt = Tamashii::Packet.new(pkt_type, client.tag, pkt_data)
client.channel.broadcast(pkt.dump)end
end
測試模式最後記得註冊這一個 handler
MyPacketHandler.init_counter
Tamashii::Resolver.config dohandle Tamashii::Type::RFID_NUMBER, MyPacketHandler
end
測試模式Agent 端收到的結果
未來方向支援其他設備,如 LCD 顯示板、相機等目前僅以打卡系統為雛型設計對於其他應用可能仍須更進一步 refactor
Agent 端的元件設計還是寫得有點死 ( 不夠彈性 )
Agent 與 Manager 之間的認證目前是寫死的 token
其他認證:非對稱金鑰 (SSH)