arduinoでリモコン #03
arduinoでリモコンシリーズも3回目ですね。2日空いてしまいましたが、最終目標のWebから操作ができました。#02に翌日にはWebから動作させるプログラムは完成していましたが、ソースコードを見直して最後まで修正していました。
前置き
前回ちょっと書きましたが、信号の保存方法はsqlでなくyamlにしました。可視性の重視とエディタでぱぱっと修正できる点、そしてrubyだけで動作できる点を期待したためです。sqlではまずsqlがインストールされてないとそもそも使えないですし、ある程度コマンドを把握してないと使いこなせませんしね。yamlならrubyでロードしてしまえば、あとはハッシュと配列の固まりです。yamlで書き込む内容を一度完璧に決めてしまえばこっちのものです。ただし信頼性でsqlに劣ります(たぶん)
Webからrubyを動かすというのはrailsくらいしかやったこと無いのですが、普通にcgiとして動作できます。問題はapacheの設定とかになってくるのですが、何も考えることはありませんでした。なぜなら我々にはWEBrickがあるのです。rubyコードほんの5行くらいでWebサーバーが出来上がってしまいます。便利な世の中ですね(ただしapacheには劣ります)
demo
プログラムを起動するとWebにアクセスできます。
最初は何もリモコンが登録されてないので、先ずはリモコンを登録します。この状態からarduinoに付けたスイッチを押した後にリモコンを受光部に向けて信号を投げます。すると下の方に受信した信号がhexで見えます。(下は3回信号を投げました)
これをクリックするとsignal dataの方にhexがコピーされるので、あとはremocon nameとsignal nameを入れてsaveをクリックすれば登録完了です。
同様にいろいろ信号を登録してみました。
はい、大変見づらいですね。後で見やすくします…。信号の送信はこの部分、たとえば「power」って文字列をクリックすると送信されます。
ソースコードは量が多めなので続きからにしましょうか。
そーすこーど
arduino側は今回は変更しませんでした。プログラムの階層はそのような状態です。
led_signal.yml serial_led.rb server.rb www/ cgi-bin/ add_remocon.cgi load_yaml.cgi index.htm script/ add_remocon.js prototype.js
jQueryが使えないのでprototype.jsを使ってます。流行に乗り遅れました(汗
serial_led.rb
arduinoとの通信用です。送受信をスレッドにしてる決定的理由は無いですが、「一応分けておこう」程度なノリです。
#!/usr/bin/env ruby # coding: utf-8 require 'serialport' require 'thread' class LEDSerial def initialize(device) @up_queue = Queue.new @down_queue = Queue.new @serial_port = SerialPort.new(device, 9600) @up_therad = Thread.start do loop { @serial_port.write @up_queue.deq } end @down_therad = Thread.start do loop do if @serial_port.readline.strip =~ /^S([01]+)E$/ tmp = $1.split('').map {|i| i.to_i } arr = [] index = 0 # convert 1bit to 8bits while index < tmp.length t = (tmp[index+0]<<7) unless tmp[index+0] == nil t |= (tmp[index+1]<<6) unless tmp[index+1] == nil t |= (tmp[index+2]<<5) unless tmp[index+2] == nil t |= (tmp[index+3]<<4) unless tmp[index+3] == nil t |= (tmp[index+4]<<3) unless tmp[index+4] == nil t |= (tmp[index+5]<<2) unless tmp[index+5] == nil t |= (tmp[index+6]<<1) unless tmp[index+6] == nil t |= (tmp[index+7]<<0) unless tmp[index+7] == nil arr << t index += 8 end arr.pop # last is not 255 arr.pop while arr[-1] == 255 # delete bottom null data # set header and footer arr.unshift 170 # 0xAA arr.push 85 # 0x55 @down_queue.enq arr.pack("C*") end end end end def clear @down_queue.clear end def read_with_queue @down_queue.deq end def read read_with_queue unless @down_queue.empty? end def write(value) puts value.unpack("h*").shift @up_queue.enq value end def close @up_therad.kill @down_therad.kill @serial_port.close end end
これ自身は#02で書いた物をクラス化しただけです。
server.rb
#!/usr/bin/env ruby # coding: utf-8 require 'webrick' require 'yaml' require './serial_led' $SP = LEDSerial.new "/dev/tty.usbserial-A600eAHN" class SendSerialServlet < WEBrick::HTTPServlet::AbstractServlet def do_GET(req, res) if req.query.has_key? 'type' if req.query['type'] == 'cmd' # cmd data mode yml = YAML.load_file("led_signal.yml") remocon = req.query['remocon'] signal_name = req.query['signal_name'] $SP.write yml[remocon][signal_name] if !yml[remocon].nil? && !yml[remocon][signal_name].nil? elsif req.query['type'] == 'data' # test data mode $SP.write [req.query['value']].pack("h*") else return #error end else return # error end res['Content-Type'] = 'text/plain' end end class RecvSerialServlet < WEBrick::HTTPServlet::AbstractServlet def do_GET(req, res) tmp = nil begin timeout(5) { tmp = $SP.read_with_queue } rescue Timeout::Error end res.body = tmp.unpack("h*").shift unless tmp.nil? res['Content-Type'] = 'text/plain' end end srv = WEBrick::HTTPServer.new( BindAddress: "127.0.0.1", Port: 10080, DocumentRoot: "./www/" ) srv.mount('/send', SendSerialServlet) srv.mount('/recv', RecvSerialServlet) Signal.trap(:INT) { srv.shutdown } srv.start
serialportを使うAPI周りもここで書いてます。というのも、Webrickでcgiを起動すると、rvmでインストールしたrubyが上手く起動してくれないのです…。
index.htm
特に説明不要?
<html> <head> <meta http-equiv="Contect-Type" content="text/html;charset=utf-8"> <!-- <link rel="stylesheet" type="text/css" charset="utf-8" href="stylesheet.css"> --> <script type="text/javascript" charset="utf-8" src="script/prototype.js"></script> <script type="text/javascript" charset="utf-8" src="script/add_remocon.js"></script> <title>リモコン</title> </head> <body> リモコン一覧 <div id="yaml"></div> <hr> 信号追加 <form method="post" name="addRemoconForm" action="./cgi-bin/add_remocon.cgi" onsubmit="addRemote(this); return false;"> remocon name:<input type="TEXT" name="remocon"><br> signal name:<input type="TEXT" name="signalName"><br> <span onclick="testSend()">signal data:</span><input type="TEXT" size="80" name="signalData"><br> <input onclick="$('add_remocon').innerHTML = '';" type="SUBMIT" value="save"> <input onclick="$('add_remocon').innerHTML = '';" type="RESET" value="clear"> </form> signal data:(prease click) <ul id="add_remocon"></ul> </body> </html>
onsubmit="hoge(); return false;"がミソですかね?
add_remocon.js
行数が少ない割に一番時間がかかりました。javascript苦手すぎです。
Event.observe(window, "load", function() { reloadList(); id = setInterval(setUpdater , 1000); }, false); function reloadList() { new Ajax.Updater( 'yaml', '/cgi-bin/load_yaml.cgi', { method: 'get', parameters: 'format=table' } ); } var count = 0; function setUpdater() { if(count == 0) { count++; new Ajax.Request( '/recv', { method: 'get', onComplete: function(req) { if(req.responseText != '') { var ev = $('add_remocon'); var newEvent = document.createElement("li"); newEvent.innerHTML = req.responseText; ev.insertBefore(newEvent, ev.childNodes.lastChild); Event.observe(newEvent, 'click', function(){ document.addRemoconForm.signalData.value = req.responseText; }, false); } count--; } } ); } } function testSend() { var value = document.addRemoconForm.signalData.value; new Ajax.Request( '/send', { method: 'get', parameters: 'type=data&value=' + value } ); } function addRemote(f) { new Ajax.Request( '/cgi-bin/add_remocon.cgi', { method: 'post', parameters: Form.serialize(f), onSuccess: function(req) { reloadList(); } }); } function sendRemote(key, value) { new Ajax.Request( '/send', { method: 'get', parameters: "type=cmd&remocon=" + key + "&signal_name=" + value, onComplete: function(hoge) { } } ); }
Ajax.Requestの中のparametersの設定方法が場所によってぐちゃぐちゃになってますね。Ajax.Updaterを使ったおかげで後述するload_yaml.cgiが大変なことになってます。
load_cgi.cgi
Ajax.Updaterを使ったために悲惨な事になってしまったAPIです。yamlをxmlに変換して投げ返しても良かったのですが、ボクがxml読めないので諦めました。prototype.jsがyamlをネイティブに読み込めたら、ここは単純にyamlを返すだけのAPIになってました。
#!/usr/bin/env ruby # coding: utf-8 require 'yaml' require 'cgi' yml = YAML.load_file("../../led_signal.yml") cgi = CGI.new params = cgi.params['format'] unless params.empty? case params.shift when 'table' # make table mode print "Content-type: text/plain\n\n" list = '' yml.each do |k1, v1| tmp = '' v1.each do |k2, v2| tmp << " <li><span onClick='sendRemote(\"#{k1}\", \"#{k2}\")'>#{k2}</span>\n" end list << "<li>#{k1}\n <ul>\n#{tmp} </ul>\n" end print "<ul>\n #{list}</ul>" end end
add_remocon.cgi
index.htmのフォームから投げられてきたデータを読み取って、yamlに書き込むAPIです。
#!/usr/bin/env ruby # coding: utf-8 require 'cgi' require 'yaml' yaml_file = '../../led_signal.yml' yml = YAML.load_file yaml_file yml = {} if yml == false cgi = CGI.new print "Content-type: text/plain\n\n" remocon = cgi['remocon'] signal_name = cgi['signalName'] signal_data = [cgi['signalData']].pack("h*") if !remocon.empty? and !signal_name.empty? and !signal_data.empty? if yml.has_key? remocon yml[remocon][signal_name] = signal_data # overwrite else yml[remocon] = {signal_name => signal_data} end end open(yaml_file, 'w') {|f| YAML.dump yml, f}
yamlにデータを格納する関係で、一部変更があるとファイルを丸ごと書き換える感じになってしまいました。avtive_recordを使っていればこんな事にはなってないでしょう。データファイルの可視性を重視した罰です。さて問題のyamlですが、
--- RD-S304K: ? !binary "44K/44Kk44Og44OQ44O8\n" : !binary | qgAAAAAf//H4z8ZjP5jMfn8/H4z8Y/Mfj8x+Px+Y/EY/MZ9V ? !binary "5bem\n" : !binary | qgAAAAAf//H4z8YjP5jMfn8/H4z8Yj8fiMfn8/H4zH4/MZ9V power: !binary | qgAAAAAf//H4z8YjP5jMfn8/H4z8Y/MZ+MRn8x+PzH4/P59V
はい見づらいですね。signal nameはasciiにしないとこうなってしまいます(rubyの関係上) それと信号がテキトーな順番になってしまうので、これも解消しておくべきですね。
とりあえずこんな具合になりました。部分的に、特にyaml関係は修正がまだ必要ですが、動くのでこのまま一旦様子見します。UIとかUIとかUIが気にくわないので割とはやく手直しするでしょう。