読者です 読者をやめる 読者になる 読者になる

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周りもここで書いてます。というのも、Webrickcgiを起動すると、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です。yamlxmlに変換して投げ返しても良かったのですが、ボクがxml読めないので諦めました。prototype.jsyamlをネイティブに読み込めたら、ここは単純に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が気にくわないので割とはやく手直しするでしょう。