用 ruby 開發 iot 應用 - 以 rubyconf.tw 打卡系統為例

62
用 Ruby 用用 IoT 用用 RubyConf.tw 用用用用用用 用用用 (Henry Tseng)

Upload: liang-chi-tseng

Post on 07-Apr-2017

79 views

Category:

Engineering


7 download

TRANSCRIPT

Page 1: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

用 Ruby 開發 IoT 應用以 RubyConf.tw 打卡系統為例曾亮齊 (Henry Tseng)

Page 2: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

$ whoami

曾亮齊 (Henry Tseng)

A.K.A. lctseng (Github, Twitter…)

5xRuby DevOps

Rails Girl Taipei 教練中研院資科所短期研究助理國立臺灣大學 資訊工程系獨立遊戲『軍官之歌』 Programmer(80,000+ lines of Ruby)

Page 3: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

這是一個好不容易做出系統後,又全部打掉重練的故事…

Page 4: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

緣起Rubyconf.tw 2015

Python 版的打卡機 (Codeme)

使用 Raspberry PI + RFID Reader

沒有繼續維護功能有限,難以擴充

Page 5: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

緣起Ruby 圈仍無類似的作品公司買了幾台 PI 與讀卡設備以 Ruby 重刻打卡系統為 2016 RubyconfTW 做準備

Page 6: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

打卡設備

Page 7: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

打卡設備Raspberry PI 3

RFID Reader (MFRC522)

蜂鳴器 (DC buzzer)

Page 8: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

設計理念當初是以打卡系統為設計方向

掌握會眾流向與人數可以傳送 Sensor 資料並回報結果

例如打卡紀錄希望裝置可以由中央掌控

Page 9: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

Conf 用打卡系統運作流程會眾於報到時發給一張 RFID 晶片卡貼紙,綁定個人資料會眾於各場次入場時,可在門口的打卡機進行打卡各贊助商攤位上也有一台屬於該攤位的打卡機

會眾可以透過打卡的動作,將聯絡資料留給攤商不必再使用傳統的紙本資料

實際 Demo

Page 10: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

功能設計可以傳送 Sensor 資料並回報結果報到時,檢查該張卡片是否重複使用打卡時,檢查該張卡片是否已報到過根據不同的結果發出不同聲響

Page 11: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

功能設計希望可以由中央掌控Agent: 打卡機; Manager: 中控程式Agent 難免會斷線或需要重啟不可能隨時連線進去分散在各處的 Agent

掌握 Agent 存活的狀況Manager 向 Agent 下達指令( 重啟、關機、發出聲響 )

Page 12: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

連線設計使用 HTTP 而不是 Raw TCP

可套用現存的 SSL(HTTPS)

可輕易整合到 Rails 中 (HTTP API)

Page 13: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

連線設計雙向連線

非同步指令( 重啟、關機 )

Check-in Server

(Manager)

Agent

打卡結果( 卡號 )

Page 14: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

連線設計傳統 HTTP API

非同步指令Polling

Check-in Server

(Manager)

Agent

打卡結果偵測到卡片再發出request

setTimeout()

Page 15: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

連線設計WebSocket 的使用HTTP + 雙向連線

可以在 Javascript 端寫 callback ,讓 server 端送資料來呼叫Rails 中已有 WebSocket 的實作

Action Cable

不必注重連線管理專注於資料交換

Page 16: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

連線設計使用 Action Cable

專替 Web Application 設計官方 Client 為 Javascript

當時並無找到 Ruby 的 Client 端實作模組化:分離連線端與晶片控制端

未來若有更好的 Client 端可隨時抽換

Page 17: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

連線設計暫時的解決方法

用 Ruby 包裹 browser 執行 JS

為了提供完整的 JS 環境,使用 browser 是最直接的

AgentCheck-in Server

(Manager)

Page 18: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

連線設計在指令介面中使用瀏覽器?如何在 CLI 中啟動以 GUI 為主的瀏覽器如何以指令的方式控制瀏覽器的行為

解決方法:Xvfb + headless

Page 19: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

Xvfb + headlessVirtual framebuffer X server for X Version 11

在沒有圖形介面的情況下,執行任何需要圖形介面的程式用於測試例如可在純指令介面的情況下進行網站的整合測試 (Selenium, Watir)

Page 20: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

Xvfb + headlessheadless

Ruby gem

https://github.com/leonid-shevtsov/headless

Ruby interface for Xvfb

使用 headless 啟動瀏覽器以文字操作的方式送指令來執行 Javascript

Page 21: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

Xvfb + headlessrequire 'headless'require 'watir-webdriver'

# 啟動虛擬視窗@headless = [email protected]

# 在虛擬視窗中執行瀏覽器 ( 如 Firefox) @browser = Watir::Browser.new

# 前往網頁 URL @browser.goto(url)

Page 22: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

Xvfb + headless

# 檢查 ActionCable 是否有連上?def check_website_connected? @browser.driver.execute_async_script(%{arguments[arguments.length - 1](typeof(App) != 'undefined')})end

Page 23: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

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

Page 24: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

Xvfb + headless

# 關閉瀏覽器@browser.close

# 關閉虛擬視窗 @headless.destroy

Page 25: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

連線設計後來並沒有採用上述的方法單純為了連線而跑 browser 實在不合理

action_cable_client

開發數個月後出現的 gem

純 Ruby 的 Action Cable client library

Page 26: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

連線設計Manager 端的設計直接使用 Action Cable 的 Server 端 API

作為打卡系統的一部分寫在 Rails 中管理 Agent 與驗證卡號的邏輯寫在一起

Page 27: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

實際部署Rubyconf.tw 2016

約 17 台 Agent

基本上該有的功能都有完成當初設計時有些缺失導致常常為了設備問題東奔西跑

Page 28: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

問題action_cable_client 反應不夠靈敏尤其是網路不穩時會場網路偶爾會斷線Timeout 時間太長, Agent 沒有寫斷線回報機制斷線常常需要人為檢查

Page 29: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

問題action_cable_client gem 仍在發階段偶爾會發生連線卡死 (網路正常,就是連不上 Server)

有時網路中斷恢復後,除非重開整支程式,否則連線無法恢復雪上加霜:所有工作都在同一個 thread

斷線時整個程式都要重開

Page 30: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

Refactor重新設計連線相關的組件仍使用 WebSocket ,但不再依賴 Action Cable

不再依賴有潛在問題的 gem

更加即時的連線檢查與重新連線分離 Manager 與 Rails app

不需要 Action Cable ,因此可與 Rails 切割開

Page 31: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

Agent 重新設計元件化每個元件 (網路連線、讀卡機、蜂鳴器 ) 各成組件每個組件由各自的 thread 跑 event loop

定義 event 作為元件間的溝通系統指令、讀卡訊號、聲音 ( 蜂鳴器 ) 訊號、連線狀態等

當有元件出錯時,重開該 thread 即可達到重啟效果權責分明:容易加入 /移除元件

Page 32: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

Agent 架構圖Master ( 管理所有組件、廣播 event)

Buzzer 蜂鳴器組件• 接收 event: 聲響控制

Card Reader 讀卡機組件• 發出 event: 卡號資料

Connection 連線組件•透過網路轉送 event 給 Manager,或從 Manager 接收

event

Page 33: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

Manager 重新設計分離自 Rails ,可獨立運行

分離打卡邏輯與裝置管理專注於裝置管理

Rack-based

保留與 Rails 結合的空間 ( 讓 Rails app 處理打卡邏輯 )

如同 ActionCable ,可 mount 至 Rails app 中

Page 34: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

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

Page 35: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

Manager 架構 (standalone)

Channel PoolChannelClient

Check-in

Server

Channel Channel

Page 36: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

Manager 架構 (with Rails)

Channel Pool

Channel

Client

Channel Channel

Check-in Server

Page 37: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

當前進度新架構名為 tamashii

目前共有三個 gem

tamashii-agent : Agent 端,安裝於 PI 上tamashii-manager : Manager 端 ( 獨立於 Rails)

tamashii-common : Agent 、 Manager 共用組件Test framework: rspec

Page 38: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

使用 tamashii-agent設備: Raspberry PI 3 、已安裝 Ruby (MRI)

包含 RFID 與蜂鳴器安裝 gem$ gem install tamashii-agent

Page 39: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

使用 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

Page 40: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

使用 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

Page 41: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

使用 tamashii-manager

Mount 在 Rails app 中假設希望在 /tamashi 這個路徑上執行 Manager

在 config/routes.rb 中加入manager_config.rb 則是改放到 config/initializers

mount Tamashii::Manager::Server => '/tamashii'

Page 42: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

使用 tamashii-managerManager 收到來自 Agent 的訊息後,會直接執行預設的 handler

例如:廣播封包到整個 channel

並不會直接交給 Rails app 來處理

ManagerAgent Check-in

Page 43: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

使用 tamashii-manager這個設計理念是基於 Manager 應該與 Rails app 脫鉤若要以脫鉤的概念來看, Rails app 應該也要加入 channel

利用 channel 廣播的特性,傳送訊息給 AgentChannel Pool

ChannelClient

Check-in

Channel

Channel

Page 44: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

使用 tamashii-managerHook

若 Manager 與 Rails app 同在一個 Rack 底下 可透過此方式『監聽』甚至『攔截』訊息透過 Manager API 發出訊息

ManagerAgent Check-in

Hook

Page 45: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

使用 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

Page 46: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

使用 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

Page 47: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

測試模式 (Agent)非測試模式下, Agent 需要真的硬體設備才可以運作

否則硬體元件相關的程式碼會初始化失敗使用測試模式脫離硬體元件測試邏輯跑 rspec

Page 48: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

測試模式 (Agent)使用測試模式

偽讀卡機與偽蜂鳴器模擬硬體設備的行為偽讀卡機:隨機一段時間後,造出一個卡號訊息偽蜂鳴器:以文字顯示的方式取代發出聲音

Tamashii::Agent.config doenv 'test'auth :tokentoken 'abc123'

end

Page 49: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

測試模式 (Agent)Master

BuzzerAdapterBuzzer

GPIO Buzzer

FakeBuzzer

CardReaderAdapter

CardReader

MFRC522

FakeCardReader

Connection

Non-testmode

Testmode

Page 50: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

測試模式 (Agent)部署測試模式

使用 Standalone Manager

使用預設 handler (廣播封包 )$ tamashii-manager -C manager_config.rb -p 3333

Tamashii::Manager.config doenv 'test'auth :tokentoken 'abc123'

end

Page 51: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

測試模式 (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

Page 52: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

測試模式 – Agent 端

Page 53: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

測試模式 – Manager 端

Page 54: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

測試模式剛才的範例中, Agent 似乎有 error

Page 55: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

測試模式Error 原因:沒有建立 handler

Manager 端只有單純廣播訊息並沒有告知 Agent 該張卡片的驗證結果Agent 端等待一段時間仍沒有得到回覆,視為發生 error

Page 56: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

測試模式範例:在 Standalone 模式下實作簡易 handler

接收 Agent 的卡號資訊,將驗證結果廣播回去驗證結果僅簡單使用一對一錯的方式第一組卡號: no

第二組卡號: ok

Page 57: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

測試模式直接在 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"]

Page 58: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

測試模式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

Page 59: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

測試模式最後記得註冊這一個 handler

MyPacketHandler.init_counter

Tamashii::Resolver.config dohandle Tamashii::Type::RFID_NUMBER, MyPacketHandler

end

Page 60: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

測試模式Agent 端收到的結果

Page 61: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例

未來方向支援其他設備,如 LCD 顯示板、相機等目前僅以打卡系統為雛型設計對於其他應用可能仍須更進一步 refactor

Agent 端的元件設計還是寫得有點死 ( 不夠彈性 )

Agent 與 Manager 之間的認證目前是寫死的 token

其他認證:非對稱金鑰 (SSH)

Page 62: 用 Ruby 開發 IoT 應用 - 以 RubyConf.tw 打卡系統為例