0

mbedで取得したセンサーの値をsinatra+tokyocabinetで保存する

まずセンサーのデータを時系列で溜めたり、取り出したりするためのwebサービスをsinatra+TokyoCabinetで作っておいた。


そこにmbedのADコンバータでCdSの明るさを計測、LEDを点灯/消灯mbedにEthernetを接続しtwitterにpostするを合体させ、約10秒間隔でセンサーの値をhttp-postしまくった。

一度web APIが出ている場所にマイコンの情報も保存しておけば、あとでマッシュアップできて便利になる。このAPIの下にmbedでもgreasemonkeyスクリプトでも何個でもぶら下げられる。
mbedからwedataに保存しても良かったんだけど、あえて自分で作ってみた。



■mbedで明るさを計測してhttp-postする
作った回路。CdSで明るさを取得し、LEDで明るさを表示する。mbedのRD+,RD-,TD+,TD-ピンにトランス入りEthernetジャックを接続し、LANケーブルを刺した。
mbed ethernet


プログラム。CdSに入る光量が少なくなると緑色のLEDが光る。http-postはTickerで定期的に呼び出したかったが、Ticker内でHTTPClient.hの関数を使うとmainに戻ってこなくなるので仕方なくmain内で20ループ毎(約10秒毎)にHTTPClient.post()するようにした。stdは使えるけど、pthread等のスレッド機能は無いみたいなのでこれは痛い。
sensor_http_post | mbed
#include "mbed.h"
#include "HTTPClient.h"
#include <string>
using namespace std;

Serial pc(USBTX, USBRX);

DigitalOut led1(LED1); // blink
DigitalOut led2(LED2); // blink when http-post
DigitalOut led3(p11); // represent ADC value

AnalogIn adc(p15);
float ain;

HTTPClient http; // use DHCP

const string api_uri = "http://shokai.mag.keio.ac.jp/sensor-storage/shokai/cds/";
const string mbed_name = "shokai-mbed01";
const string sensor_name = "CdS";

void blink_led(DigitalOut led, int num){
    for(int i=0; i < num*2; i++){
        led = !led;
        wait(0.1);
    }
}

void post_sensor_value(){
    blink_led(led2, 2);
    char query[256] = "";
    char result[4096] = ""; // 4kb
    sprintf(query, "name=%s&%s=%f", mbed_name.c_str(), sensor_name.c_str(), (float)ain);
    pc.printf("post:%s => %s\n", query, api_uri.c_str());
    http.post(api_uri.c_str(), query, result, 4096);
    pc.printf("%s\n", result);
    blink_led(led2, 2);
}

int main(void){
    led1 = 1;
    int count = 0;
    while(1) {
        ain = adc;
        pc.printf("sensor:%f\n", float(ain));
        led1 = !led1; // blink LED
        if(ain > 0.1) led3 = 0; // represent CdS value
        else led3 = 1;
        if(count > 20){
            post_sensor_value();
            count = 0;
        }
        count++;
        wait(0.5);
    }
}
せっかくC++が使えて、mbed.hのAPIはC++っぽいのに、HTTPClient.hはそうでもないみたいだ。
mbedのローカルのストレージがfopenで読めるので、そこに設定ファイルを置けるようにしたい。yamlやjsonをparseするライブラリを作りたい。



動作はシリアル通信で見れる。表示にはserialterm.rbを使った。
postすると返り値がjsonで来る。
mbed http-post



■tokyocabinetとsinatraでデータを溜めるAPIを作る
できたもの
sensor-storage API


githubでソースを全部公開しておいた。
shokai's sensor-storage at master – GitHub


http://shokai.mag.keio.ac.jp/sensor-storage/ にhttp-postした内容を、json stringにエンコードして時刻_マイクロ秒をキーにしてTokyoCabinetに保存する。
keyの _ の左側が時刻になっているので後で使う時に便利。

http://shokai.mag.keio.ac.jp/sensor-storage/keys で保存されているkeyの一覧がJSON形式で見れる。
http://shokai.mag.keio.ac.jp/sensor-storage/recentで最近10件のkeyとvalueが取れる。他にもrecent/件数や、last(最後に入れたデータ)、count(データ件数)などのAPIがある。


今回のmbedでCdSの値を保存しているのは http://shokai.mag.keio.ac.jp/sensor-storage/shokai/cds/recent になる。shokai/cds/last , shokai/cds/recent , shokai/cds/count などで色々データが取り出せる。

/ 以下に適当に文字列を付けると新しくDBを作るようになっている。もしデータをpostしてみたいなら、mbedの方のコードは/shokai/cds/をハードコーディングしているので、そこを適当に変えてから使ってくれるとうれしい
あまりにも大きいデータは入らない。


スクリプト自体はわりとシンプル。sinatra使って1ファイルに書いてある。
server/main.rb at master from shokai's sensor-storage – GitHub
#!/usr/bin/env ruby
# -*- coding: utf-8 -*-
require 'rubygems'
require 'sinatra'
require 'sinatra/reloader' if development?
require 'rack'
require 'json'
require 'tokyocabinet'
include TokyoCabinet

@@dbdir = 'db'

def db_open(dbname='/')
  dbname = dbname.to_s.gsub(/\//, '_')
  @hdb = HDB.new
  Dir.mkdir(@@dbdir) if !File.exists?(@@dbdir)
  @hdb.open("#{@@dbdir}/_#{dbname}.tch", HDB::OWRITER|HDB::OCREAT)  
end

after do
  @hdb.close if @hdb
end

get '*/' do
  path = "../" * params[:splat].first.count('/')
  redirect "#{path}./readme"
end

get '*/keys' do
  db_open(params[:splat])
  @hdb.keys.reverse[0...10000].to_json
end

get '/dbs' do
  Dir::glob("#{@@dbdir}/*.tch").map{|i|
    name = i.to_s.scan(/.*\/(.+)\.tch/).first.to_s.gsub(/_/,'/')
    tmp = name.split(//u)
    tmp.shift
    tmp.to_s + "/"
  }.uniq.delete_if{|i|
    i =~ /\/\//
  }.sort.to_json
end

get '*/count' do
  db_open(params[:splat])
  {"count", @hdb.rnum}.to_json
end

get '*/last' do
  db_open(params[:splat])
  key = @hdb.keys.last
  {key, @hdb[key]}.to_json
end

get '*/recent' do
  db_open(params[:splat])
  @hdb.keys.reverse[0...10].map{|k| {k, @hdb[k]} }.to_json
end

get '*/recent/:num' do
  db_open(params[:splat])
  num = [params[:num].to_i, 1000].min
  @hdb.keys.reverse[0...num].map{|k| {k, @hdb[k]}}.to_json
end

get '*/:tc_key' do
  db_open(params[:splat])
  key = params[:tc_key]
  {key, @hdb[key]}.to_json
end

post '*/' do
  db_open(params[:splat])
  now = Time.now
  key = "#{now.to_i}_#{now.usec}"
  params.delete(:splat.to_s)
  @hdb.put(key, params.to_json)
  {key, @hdb[key]}.to_json
end

delete '*/:tc_key' do
key = params[:tc_key]
  v = @hdb[params[:tc_key]]
  @hdb.out(params[:tc_key])
  {key,v}.to_json
end


githubのmiscディレクトリ以下にテスト用にhttp postしたりするためのスクリプトがある。mbedが無い時の動作チェックにどうぞ。


サーバーはthinで10個preforkさせておいて、それぞれ別のportで起動させてある。apache2のmod_proxy_balancerでそこにproxyして、80番ポートのサブディレクトリ下にあるように見せている。



1

apache2のサブディレクトリをthinで起動してるsinatraにプロキシする

SinatraやRailsは開発は楽だけどデプロイ方法がいろいろあって悩む。
最近はpassengerを使う方法が流行ってるけど、あいにく学校に置いたサーバーでサブドメインが使えないので同一ホスト名でapacheと共存させるしかない。

passengerでもRailsBaseURIだったか?を設定すればサブディレクトリでRack対応アプリを動かせるが、
今回は1年前からRails+mongrelのデプロイに使っているapache2のサブディレクトリをmongrelで起動してるrailsにプロキシする – 橋本詳解と同じ方法でSinatra+Thinをデプロイした。
Railsではmongrel_clusterで複数起動させたrailsにapache2のmod_proxy_balancerでアクセスを割り振ったが、同じ設定でsinatra+thinにも割り振れる。

結果、橋本商会 Twitterの地名なうbotを全blockするOAuthアプリで作ったアプリのURLをport番号むきだし状態から http://shokai.mag.keio.ac.jp/block_nowbots/ という良いURLに変更できた。ちゃんと他のrailsアプリとも共存できてる
今は10個起動したthinにapacheがアクセスを割り振っている。


■thinでsinatraを動かす
まず単純にthinを1プロセスだけ起動してsinatraが動くかどうかチェックしておく

sudo gem install thin

起動にはアプリ本体と、config.ruが必要

main.rb
#!/usr/bin/env ruby
require 'rubygems'
require 'sinatra'
require 'rack'

get '/' do
end
thinから起動するためにrackをrequireしておく。


config.ru
require 'rubygems'
require 'sinatra'

require 'main.rb'
run Sinatra::Application


logやpid、静的ファイルを置く場所を作る
mkdir log
mkdir public
mkdir tmp
mkdir tmp/pids


まず普通に起動させてみる。とくにpublicやtmpディレクトリの位置を指定する必要ない。ディレクトリがあればそれぞれの機能は有効化されるし無くても特にエラーは出ないみたい
thin start -R config.ru
ctrl+Cで終了。



■apache2のmod_proxy_balancerでアクセスをthinに振り分ける

mod_proxy有効化
sudo a2enmod proxy_balancer 
sudo a2enmod proxy
sudo a2enmod proxy_http


/etc/apache2/mods-available/proxy.conf
<IfModule mod_proxy.c>
ProxyRequests Off

<Proxy *>
Order deny,allow
Allow from all
</Proxy>

</IfModule>


/etc/apache2/conf.d/your-app-name.conf
ProxyPreserveHost Off
ProxyRequests Off

<Proxy *>
Order deny,allow
Allow from all
</Proxy>

ProxyPass /appname balancer://appname
ProxyPassReverse /appname balancer://appname

<Proxy balancer://appname>
BalancerMember http://127.0.0.1:4040 loadfactor=10
BalancerMember http://127.0.0.1:4041 loadfactor=10
BalancerMember http://127.0.0.1:4042 loadfactor=10
BalancerMember http://127.0.0.1:4043 loadfactor=10
BalancerMember http://127.0.0.1:4044 loadfactor=10
BalancerMember http://127.0.0.1:4045 loadfactor=10
BalancerMember http://127.0.0.1:4046 loadfactor=10
BalancerMember http://127.0.0.1:4047 loadfactor=10
BalancerMember http://127.0.0.1:4048 loadfactor=10
BalancerMember http://127.0.0.1:4049 loadfactor=10
</Proxy>
こうすると
http://hostname:4040/ から http://hostname:4049/までで動いているsinatraアプリが、
http://hostname/appname/ として外からは見えるようになる。

apache2再起動
sudo /etc/init.d/apache2 restart

今後は新しいアプリを作る毎にこの your-app-name.conf を雛形にして conf.d の中に配置してapache2再起動するだけでいい。




■thinを複数起動させる
thin+sinatraを複数起動させて管理しやすくする設定ファイルを作り、mongrel_clusterみたいにする。
main.rb, config.ruと同じディレクトリにthin.ymlを置く

thin.yml
timeout: 30
log: log/thin.log
environment: production
servers: 10
daemonize: true
rackup: config.ru
port: 4040


起動
thin start -C thin.yml
これでport 4040から10(つまり4049まで)まとめてdaemonとして起動する。

停止、再起動コマンドもある。
thin stop -C thin.yml
thin restart -C thin.yml



最終的なファイルの配置こうなる。 *.logと*.pidはthinが自動生成したもの。jsや画像はpublicの下に置く。
|-- config.ru
|-- log
| |-- thin.4040.log
| |-- thin.4041.log
| |-- thin.4042.log
| |-- thin.4043.log
| |-- thin.4044.log
| |-- thin.4045.log
| |-- thin.4046.log
| |-- thin.4047.log
| |-- thin.4048.log
| `-- thin.4049.log
|-- main.rb
|-- public
| `-- shokai.jpg
|-- thin.yml
`-- tmp
`-- pids
|-- thin.4040.pid
|-- thin.4041.pid
|-- thin.4042.pid
|-- thin.4043.pid
|-- thin.4044.pid
|-- thin.4045.pid
|-- thin.4046.pid
|-- thin.4047.pid
|-- thin.4048.pid
`-- thin.4049.pid

0

Twitterの地名なうbotを全blockするOAuthアプリ

http://shokai.mag.keio.ac.jp/block_nowbots/から使える。
(8月15日:URL変更しました

とりあえずSinatraとOAuthの組み合わせを試してみたかったので、DBは使わないものを作りたかった。取得したOAuthのtokenなどはその場で捨てている。blockコマンドを送る権限だけを一時的に委譲してもらう。


以下技術的なことのメモ。
■OAuthアプリの登録
まずhttp://twitter.com/oauth_clientsでアプリを登録し、consumer keyとconsumer secretを取得する。
で、下のコード中のCONSUMER_KEY, CONSUMER_SECRETを書き換える。


■必要なライブラリ
必要なgemをインストールする。最新版にした。

sudo gem install oauth twitter sinatra
それぞれ0.3.5, 0.6.13, 0.9.4がインストールされた。
twitterは内部でoauthに依存していて、oauthはバージョン毎に関数がけっこう変わっている。このバージョンの組み合わせなら動く。


■動かす
そして起動。
ruby block-nowbots.rb -p 2692 -s mongrel
thinだと複数クライアントから同時にアクセスした時1クライアントずつしか対応してくれなかったんだけど、mongrelは全クライアントに同時に応答してくれた。mongrelにいつのまにかそういう機能がついたのか、sinatraのバグでthinがthread処理されないのかはよくわからない。
sinatraもrailsと同じくデプロイまわりを工夫した方がよさそう。passenger使うのがいいのかな?


参考:


block-nowbots.rb
#!/usr/bin/env ruby
require 'rubygems'
require 'sinatra'
require 'oauth'
require 'twitter'

HOST_AND_PORT = "localhost:2692"

CONSUMER_KEY = "your-consumer-key"
CONSUMER_SECRET = "your-consumer-secret"

BOTS = 'nishinipporinow,nippori_now,kanda_now,tabata_now,sugamo_now,komagome_now,shinokubo_now,mejiro_now,takadanobabanow,okachimachi_now,uguisudani_now,ikebukuro_now,otsuka_now,akiba_now,tokyo_now,harajuku_now,shibuya_now,shibuya_now,yoyogi_now,shinjuku_now,ebisu_now'

set :sessions, true
def consumer
  OAuth::Consumer.new(CONSUMER_KEY, CONSUMER_SECRET,
                      :site => "http://twitter.com")
end

template :layout do
  '<html>
     <body>
       <h1>地名なうbotを全blockするOAuthアプリ</h1>
       <%= yield %>
       <hr style="margin-top: 100px" />
       <a href="http://shokai.org">http://shokai.org</a><br /><a href="http://shokai.org/blog/archives/4698">解説</a>
     </body>
   </html>'
end

get '/' do
  @request_token = consumer.get_request_token(:oauth_callback => "http://#{HOST_AND_PORT}/auth")
  session[:request_token] = @request_token.token
  session[:request_token_secret] = @request_token.secret
  erb %{
    <p><%= BOTS %><br />をblock設定します</p>
    <p>OAuth認証してください</p>
    <a href="<%= @request_token.authorize_url %>">認証する!</a>
  }
end

get '/auth' do

  @request_token = OAuth::RequestToken.new(consumer,
                                          session[:request_token],
                                          session[:request_token_secret])
  @access_token = @request_token.get_access_token({},
                                                  :oauth_token => params[:oauth_token],
                                                  :oauth_verifier => params[:oauth_verifier])
  session[:access_token] = @access_token.token
  session[:access_token_secret] = @access_token.secret

  erb %{
    <p>認証成功</p>
    <p><a href="/block_now_bots">地名なうbotをblockする</a>(全部で<%= BOTS.split(",").size*3 %>秒くらいかかる)</p>
    <a href='/'>戻る</a>
  }
end

get '/block_now_bots' do
  oauth = Twitter::OAuth.new(CONSUMER_KEY, CONSUMER_SECRET)
  oauth.authorize_from_access(session[:access_token], session[:access_token_secret])
  twit = Twitter::Base.new(oauth)
  BOTS.split(",").each{|bot|
    twit.block(bot)
    puts "block #{bot}"
    sleep 3
  }
  erb %{
    <p>たぶんblockしました</p>
    <p><a href="http://twitter.com/yamanote_now/following">確認</a></p>
  }
end