@m_seki の

I like ruby tooから引っ越し

ポケカのカードブロックが変更になるとのこと

ポケカのカードブロックが変更になり[A]のカードがレギュ落ちするぞ

www.pokemon-card.com

それはいいけど[A]のカードっていうのはどれなのさ!(公式サイトではまだそういう単位の検索ができないのだ)

どうする?

しょうがない。画像処理を使って、[A][B][C]を判定するのを作ろう!

GitHub - seki/pcg_abcd: Pokemon Card Game card block classifier

機械学習が流行っているので、それは使わずOpenCVのテンプレートマッチで実装することにしよう。

準備(追記)

パッケージ化する前に試したこと。

OpenCVのテンプレートマッチの誤差を比較して一番確からしいものを選ぶ作戦。

  1. カードの画像から[A][B][C]を切り出して正規化(色とかサイズとか)したものをテンプレートにする。(github見てね)
  2. 指定されたURLから画像をカードのダウンロードして、三種類のテンプレートとマッチさせる。
  3. 実験してみたところ、どのテンプレートもカードの同じ場所とマッチするが、スコア(デフォルトのアルゴリズムでは小さい方がより似ている)は正解のテンプレートと正解でないものの差がとても大きいこと確かめた。
  4. スコア順にソートして最小のものを採用することとする。
  5. なお、XYなどのマークのないカードはスコアがとても大きいので、大きすぎるものは例外とすることにした。
私の環境の問題...

OpenCVのテンプレートマッチを使えばできそう!
でもmacOSOpenCVのgem作るのめんどくさい...そういえばば前にそういうDockerイメージを作ってたぞ。なんという幸運。

https://cloud.docker.com/repository/docker/mseki/ruby-opencv

そういう問題がない人は...普通にRubyとGemでやればできるぞ。出来上がったのはこれ。

require 'opencv'
require 'drb'
require 'open-uri'

class ABCandD
  def initialize
    @template = %w(A B C).map {|a| [a, OpenCV::IplImage.load("/data/#{a}.png")]}
  end

  def zap(uri)
    img = load_img(uri)
    result = @template.map { |pair|
      [img.match_template(pair[1]).min_max_loc[0], pair[0]]
    }.sort_by {|ary| ary.first}
    result[0]
  end

  def classify(uri)
    score, klass = zap(uri)
    raise "Error too large #{score}" if score > 5000000.0
    klass
  end

  def load_img(uri)
    data = URI.open(uri).read
    OpenCV::IplImage.decode(data)
  end
end

if __FILE__ == $0
  if uri = ARGV.shift
    DRb.start_service(uri, ABCandD.new)
    DRb.thread.join
  end
end

Dockerの場合はビルドしてから実行。

$ docker build -t mseki/pcg-abcd:0.1 .
$ docker run -p 50151:50151 mseki/pcg-abcd:0.1

ポート番号の50151はdRubyのサービスのポートだよ。

別の端末からirbなどで質問すれば答えてくれる。カードの画像のURLを渡そう。

$ irb -r drb
irb(main):001:0> sorter = DRbObject.new_with_uri 'druby://localhost:50151'
irb(main):002:0> sorter.classify "https://www.pokemon-card.com/assets/images/card_images/large/SM12/037304_P_REKKUUZAGX.jpg"
=> "B"
サンムーン世代のトレーナーズのカードブロック一覧だよ

手持ちのメモ(mysqlにメモしてある。もしかしたら抜けがあるかも)にあったトレーナーズを判定させてみた。

irb(main):038:0> pp it; nil
[["あなぬけのヒモ", "グッズ", "A"],
 ["いちゃもんスプレー", "グッズ", "A"],
 ["おおきいマラサダ", "グッズ", "A"],
 ["きずぐすり", "グッズ", "A"],
 ["のぞきみレッドカード", "グッズ", "A"],
 ["ふしぎなアメ", "グッズ", "A"],
 ["まんたんのくすり", "グッズ", "A"],
 ["むしよけスプレー", "グッズ", "A"],
 ["アクアパッチ", "グッズ", "A"],
 ["エネくじ", "グッズ", "A"],
 ["エネルギーつけかえ", "グッズ", "A"],
 ["エネルギーリサイクル", "グッズ", "A"],
 ["エネルギー回収", "グッズ", "A"],
 ["カウンターキャッチャー", "グッズ", "A"],
 ["クラッシュハンマー", "グッズ", "A"],
 ["スーパーボール", "グッズ", "A"],
 ["スーパーポケモン回収", "グッズ", "A"],
 ["タイマーボール", "グッズ", "A"],
 ["ダメージムーバー", "グッズ", "A"],
 ["ネストボール", "グッズ", "A"],
 ["ハイパーボール", "グッズ", "A"],
 ["フィールドブロアー", "グッズ", "A"],
 ["ポケモンいれかえ", "グッズ", "A"],
 ["ポケモンキャッチャー", "グッズ", "A"],
 ["マルチつけかえ", "グッズ", "A"],
 ["モンスターボール", "グッズ", "A"],
 ["レスキュータンカ", "グッズ", "A"],
 ["ロトム図鑑", "グッズ", "A"],
 ["ロトム図鑑 ポケファインダーモード", "グッズ", "A"],
 ["勝利の勲章", "グッズ", "A"],
 ["改造ハンマー", "グッズ", "A"],
 ["殿堂の書", "グッズ", "A"],
 ["やまおとこ", "サポート", "A"],
 ["アセロラ", "サポート", "A"],
 ["イリマ", "サポート", "A"],
 ["カキ", "サポート", "A"],
 ["ククイ博士", "サポート", "A"],
 ["グズマ", "サポート", "A"],
 ["グラジオ", "サポート", "A"],
 ["サトシのゆうじょう", "サポート", "A"],
 ["スイレン", "サポート", "A"],
 ["スカル団のしたっぱ", "サポート", "A"],
 ["ハウ", "サポート", "A"],
 ["ハラ", "サポート", "A"],
 ["ビッケ", "サポート", "A"],
 ["プルメリ", "サポート", "A"],
 ["ポケモンブリーダー", "サポート", "A"],
 ["マオ", "サポート", "A"],
 ["マーマネ", "サポート", "A"],
 ["ライチ", "サポート", "A"],
 ["リーリエ", "サポート", "A"],
 ["ルザミーネ", "サポート", "A"],
 ["ロイヤルマスク", "サポート", "A"],
 ["ロケット団のいやがらせ", "サポート", "A"],
 ["せせらぎの丘", "スタジアム", "A"],
 ["エーテルパラダイス保護区", "スタジアム", "A"],
 ["チャンピオンズフェスティバル", "スタジアム", "A"],
 ["ポータウン", "スタジアム", "A"],
 ["ラナキラマウンテン", "スタジアム", "A"],
 ["喰いつくされた原野", "スタジアム", "A"],
 ["日輪の祭壇", "スタジアム", "A"],
 ["月輪の祭壇", "スタジアム", "A"],
 ["虚ろの海", "スタジアム", "A"],
 ["こだわりハチマキ", "ポケモンのどうぐ", "A"],
 ["じゃくてんほけん", "ポケモンのどうぐ", "A"],
 ["どくバリ", "ポケモンのどうぐ", "A"],
 ["ねがいのバトン", "ポケモンのどうぐ", "A"],
 ["エレクトロメモリ", "ポケモンのどうぐ", "A"],
 ["サイキックメモリ", "ポケモンのどうぐ", "A"],
 ["ダッシュポーチ", "ポケモンのどうぐ", "A"],
 ["ファイトメモリ", "ポケモンのどうぐ", "A"],
 ["ファイヤーメモリ", "ポケモンのどうぐ", "A"],
 ["ムキムキダンベル", "ポケモンのどうぐ", "A"],
 ["古代のクリスタル", "ポケモンのどうぐ", "A"],
 ["学習装置", "ポケモンのどうぐ", "A"],
 ["あとだしハンマー", "グッズ", "B"],
 ["いれかえフロート", "グッズ", "B"],
 ["おとりよせパッド", "グッズ", "B"],
 ["ともだちてちょう", "グッズ", "B"],
 ["ふっかつそう", "グッズ", "B"],
 ["ぼうけんのカバン", "グッズ", "B"],
 ["ウルトラボール", "グッズ", "B"],
 ["エスケープボード", "グッズ", "B"],
 ["エネポーター", "グッズ", "B"],
 ["エネルギースピナー", "グッズ", "B"],
 ["エネルギー循環装置", "グッズ", "B"],
 ["エレキチャージャー", "グッズ", "B"],
 ["エレキパワー", "グッズ", "B"],
 ["カスタムキャッチャー", "グッズ", "B"],
 ["ギリギリポーション", "グッズ", "B"],
 ["ダートじてんしゃ", "グッズ", "B"],
 ["デンジャラスドリル", "グッズ", "B"],
 ["ネットボール", "グッズ", "B"],
 ["ハンサムホイッスル", "グッズ", "B"],
 ["ビーストリング", "グッズ", "B"],
 ["フレンドボール", "グッズ", "B"],
 ["ポケナビ", "グッズ", "B"],
 ["ミステリートレジャー", "グッズ", "B"],
 ["ミックスハーブ", "グッズ", "B"],
 ["ミッシングクローバー", "グッズ", "B"],
 ["モーモーミルク", "グッズ", "B"],
 ["リターンラベル", "グッズ", "B"],
 ["ルアーボール", "グッズ", "B"],
 ["レインボーブラシ", "グッズ", "B"],
 ["ロストミキサー", "グッズ", "B"],
 ["化石発掘マップ", "グッズ", "B"],
 ["火打石", "グッズ", "B"],
 ["TVレポーター", "サポート", "B"],
 ["おじょうさま", "サポート", "B"],
 ["かんこうきゃく", "サポート", "B"],
 ["ぼんぐり職人", "サポート", "B"],
 ["アカギ", "サポート", "B"],
 ["アカネ", "サポート", "B"],
 ["ウツギ博士のレクチャー", "サポート", "B"],
 ["ウルトラ調査隊", "サポート", "B"],
 ["エーテル財団職員", "サポート", "B"],
 ["カツラの一発勝負", "サポート", "B"],
 ["カヒリ", "サポート", "B"],
 ["カルネ", "サポート", "B"],
 ["クチナシ", "サポート", "B"],
 ["ザオボー", "サポート", "B"],
 ["シロナ", "サポート", "B"],
 ["ジャッジマン", "サポート", "B"],
 ["ダイゴの決断", "サポート", "B"],
 ["デンジ", "サポート", "B"],
 ["ナタネ", "サポート", "B"],
 ["ナリヤ・オーキド", "サポート", "B"],
 ["ノボリとクダリ", "サポート", "B"],
 ["ハンサム", "サポート", "B"],
 ["ヒガナ", "サポート", "B"],
 ["フウとラン", "サポート", "B"],
 ["フラダリ", "サポート", "B"],
 ["ポケモンだいすきクラブ", "サポート", "B"],
 ["マキシ", "サポート", "B"],
 ["マサキのメンテナンス", "サポート", "B"],
 ["マツバ", "サポート", "B"],
 ["マツリカ", "サポート", "B"],
 ["マーズ", "サポート", "B"],
 ["ミカン", "サポート", "B"],
 ["モノマネむすめ", "サポート", "B"],
 ["ユリーカ", "サポート", "B"],
 ["ラジュルネ", "サポート", "B"],
 ["ラニュイ", "サポート", "B"],
 ["ルザミーネ", "サポート", "B"],
 ["ルスワール", "サポート", "B"],
 ["ルチア", "サポート", "B"],
 ["ルミタン", "サポート", "B"],
 ["ワタル", "サポート", "B"],
 ["地底探険隊", "サポート", "B"],
 ["釣り人", "サポート", "B"],
 ["そらのはしら", "スタジアム", "B"],
 ["ウルトラスペース", "スタジアム", "B"],
 ["サンダーマウンテン", "スタジアム", "B"],
 ["テンガン山", "スタジアム", "B"],
 ["ヒートファクトリー", "スタジアム", "B"],
 ["フラダリラボ", "スタジアム", "B"],
 ["ブラックマーケット", "スタジアム", "B"],
 ["プレイヤーズセレモニー", "スタジアム", "B"],
 ["ライフフォレスト", "スタジアム", "B"],
 ["ワンダーラビリンス", "スタジアム", "B"],
 ["ヴェラ火山公園", "スタジアム", "B"],
 ["戒めの祠", "スタジアム", "B"],
 ["こだわりメット", "ポケモンのどうぐ", "B"],
 ["のろいのおふだ", "ポケモンのどうぐ", "B"],
 ["ウォーターメモリ", "ポケモンのどうぐ", "B"],
 ["エスケープボード", "ポケモンのどうぐ", "B"],
 ["カウンターゲイン", "ポケモンのどうぐ", "B"],
 ["グラスメモリ", "ポケモンのどうぐ", "B"],
 ["ハッスルベルト", "ポケモンのどうぐ", "B"],
 ["フェアリーチャーム UB", "ポケモンのどうぐ", "B"],
 ["フェアリーチャームドラゴン", "ポケモンのどうぐ", "B"],
 ["フェアリーチャーム草", "ポケモンのどうぐ", "B"],
 ["フェアリーチャーム超", "ポケモンのどうぐ", "B"],
 ["フェアリーチャーム闘", "ポケモンのどうぐ", "B"],
 ["メタルゴーグル", "ポケモンのどうぐ", "B"],
 ["竜の鉤爪", "ポケモンのどうぐ", "B"],
 ["鋼鉄のフライパン", "ポケモンのどうぐ", "B"],
 ["びっくりボックス", "グッズ", "C"],
 ["やみのいし", "グッズ", "C"],
 ["グレートキャッチャー", "グッズ", "C"],
 ["グレートポーション", "グッズ", "C"],
 ["ザクザクピッケル", "グッズ", "C"],
 ["ジャッジマンホイッスル", "グッズ", "C"],
 ["スイレンのつりざお", "グッズ", "C"],
 ["スタジアムナビ", "グッズ", "C"],
 ["タッグコール", "グッズ", "C"],
 ["タッグスイッチ", "グッズ", "C"],
 ["プレシャスボール", "グッズ", "C"],
 ["ポケギア3.0", "グッズ", "C"],
 ["ポケモン通信", "グッズ", "C"],
 ["リセットスタンプ", "グッズ", "C"],
 ["炎の結晶", "グッズ", "C"],
 ["退化スプレーZ", "グッズ", "C"],
 ["電磁レーダー", "グッズ", "C"],
 ["Nの覚悟", "サポート", "C"],
 ["あばれる君", "サポート", "C"],
 ["かいじゅうマニア", "サポート", "C"],
 ["きとうし", "サポート", "C"],
 ["むしとりしょうねん", "サポート", "C"],
 ["アローラの仲間たち", "サポート", "C"],
 ["アンズ", "サポート", "C"],
 ["イツキ", "サポート", "C"],
 ["ウルトラフォレストのかみつかい", "サポート", "C"],
 ["エリカ", "サポート", "C"],
 ["エリカのおもてなし", "サポート", "C"],
 ["オーキド博士のセッティング", "サポート", "C"],
 ["カスミ&カンナ", "サポート", "C"],
 ["カスミのおねがい", "サポート", "C"],
 ["カスミのやる気", "サポート", "C"],
 ["カスミの水さばき", "サポート", "C"],
 ["カツラのクイズショー", "サポート", "C"],
 ["キョウの罠", "サポート", "C"],
 ["ギーマ", "サポート", "C"],
 ["グズマ&ハラ", "サポート", "C"],
 ["グリーンの戦略", "サポート", "C"],
 ["コーチトレーナー", "サポート", "C"],
 ["サカキの追放", "サポート", "C"],
 ["シロナ&カトレア", "サポート", "C"],
 ["ジュジュベ&ハチクマン", "サポート", "C"],
 ["タケシのガッツ", "サポート", "C"],
 ["タケシのトレーニング", "サポート", "C"],
 ["ナツメの暗示", "サポート", "C"],
 ["ハプウ", "サポート", "C"],
 ["ブルーの探索", "サポート", "C"],
 ["ホミカ", "サポート", "C"],
 ["ポケモンセンターのお姉さん", "サポート", "C"],
 ["マオ&スイレン", "サポート", "C"],
 ["マサキの解析", "サポート", "C"],
 ["マチスの作戦", "サポート", "C"],
 ["マーレイン", "サポート", "C"],
 ["ムサシとコジロウ", "サポート", "C"],
 ["メイ", "サポート", "C"],
 ["ヤーコン", "サポート", "C"],
 ["リーリエの全力", "サポート", "C"],
 ["レッド&グリーン", "サポート", "C"],
 ["レッドの挑戦", "サポート", "C"],
 ["ローラースケーター", "サポート", "C"],
 ["溶接工", "サポート", "C"],
 ["まどろみの森", "スタジアム", "C"],
 ["カスミのハナダシティジム", "スタジアム", "C"],
 ["シオンタウン", "スタジアム", "C"],
 ["タケシのニビシティジム", "スタジアム", "C"],
 ["ダストアイランド", "スタジアム", "C"],
 ["チャンピオンズフェスティバル", "スタジアム", "C"],
 ["トキワの森", "スタジアム", "C"],
 ["ナイトシティ", "スタジアム", "C"],
 ["ブリザードタウン", "スタジアム", "C"],
 ["ポケモンけんきゅうじょ", "スタジアム", "C"],
 ["巨大なカマド", "スタジアム", "C"],
 ["格闘道場", "スタジアム", "C"],
 ["混沌のうねり", "スタジアム", "C"],
 ["無人発電所", "スタジアム", "C"],
 ["Uターンボード", "ポケモンのどうぐ", "C"],
 ["くろおび", "ポケモンのどうぐ", "C"],
 ["しまめぐりのあかし", "ポケモンのどうぐ", "C"],
 ["にじいろのはな", "ポケモンのどうぐ", "C"],
 ["みみなりベル", "ポケモンのどうぐ", "C"],
 ["ジャイアントボム", "ポケモンのどうぐ", "C"],
 ["ドラゴンZ ドラゴンクロー", "ポケモンのどうぐ", "C"],
 ["ノーマルZ たいあたり", "ポケモンのどうぐ", "C"],
 ["ヒコウZ エアスラッシュ", "ポケモンのどうぐ", "C"],
 ["ビーストナイト", "ポケモンのどうぐ", "C"],
 ["ビーストブリンガー", "ポケモンのどうぐ", "C"],
 ["フェアリーチャーム 特性", "ポケモンのどうぐ", "C"],
 ["フェアリーチャーム 雷", "ポケモンのどうぐ", "C"],
 ["ムキムキパッド", "ポケモンのどうぐ", "C"],
 ["メタルコアバリア", "ポケモンのどうぐ", "C"],
 ["隠密フード", "ポケモンのどうぐ", "C"]]
あわせて買いたい

WiringPi-Ruby gemを動くようにした話(誰か引き取って

WiringPi-Ruby gemが3B+で動かなかったんだよ!

Raspberry Pi 3B+でWiringPi-Rubyが動かなかったので調べたんだが呪われていた。

おきたこと

環境はStretchってやつの最新にした3B+です。RubyでGPIO、SPIを利用した方のでwiringpi gemをインストール。
とあるAPI(wiringPiSetupGpio)を実行するとエラーでプロセスが終了してしまう。

期待した動き。

$ irb -r wiringpi
irb(main):001::0> Wiringpi.wiringPiSetupGpio
=> 0


実際の動き。

$ irb -r wiringpi
irb(main):001::0> Wiringpi.wiringPiSetupGpio
Unable to determine hardware version. I see: Hardware : BCM2835
- expecting BCM2708 or BCM2709.
..

調べた

検索するとライブラリの方を新しくすればいい(aptで新しいの入れろとかそういうの)とか、OSを古くすればいい、とか情報があったがどうも違う感じがする。
実際、wiriingPi(オリジナルのライブラリの方ね)のバージョンも新しい。しかもソースコードと違う文面のエラーが出てる。なんだこれ...。

んで、これはライブラリじゃなくてgemがおかしい気がする、と想像して調査。gem unpack wiringpiしてextconf.rbを見たところ、古いライブラリのソースコードが同梱されてた。うはー。なんで共有ライブラリをリンクしないんだよ。

それから、wiringPiのサイト読んでたらさー、今月初めの記事に「wiringPi – deprecated…」ってあったよ。

wiringPi – deprecated… | Wiring Pi

このgemも原因の一つになってたんじゃないか...

初fork、extconf.rb

数年間メンテナンスされてないみたいなので、forkしてみました。

https://github.com/seki/WiringPi-Ruby

extconf.rbを勘で修正して、ライブラリをリンクするようにしました。あと、SPIのところのバグ(RWでバッファの長さをstrlenしてるバグ)も直してcommitしてあります。
だれか引き取ってくれないかなあ。

あと、オリジナルのライブラリに入れ忘れている(ことがある)APIがあるそうなので、その場合にはその関数を呼ばないようにしてあります。

require 'mkmf'

$srcs = ['wiringpi_wrap.c']
$objs = $srcs.map{ |file| file.sub('.c','.o') }

have_library("rt", "shm_open")
have_library("wiringPi", "pinMode")
have_library("wiringPiDev", "lcd128x64clear")
if have_library("wiringPiDev", "softServoWrite")
  $defs << "-DHAVE_softServoWrite"
end

create_makefile('wiringpi/wiringpi')    

入れ方

git cloneしてから、

$ sudo gem uninstall wiringpi
$ gem build wiringpi
$ sudo gem install --local wiringpi-2.32.0.gem

WEBrickでchunked

WEBrickのレスポンスでchunkedする話

いろいろ探したけどchunkedしてる例が見つからなかったので書いた。

res.chunked=()

WEBrickのHTTPServerの話です。レスポンスにchunkedを指定してイベントを延々と返す方法を調べました。
転送をchunkedで行うにはレスポンスに次の操作をすればよい。

  res.chunked = true

問題はbodyの方。bodyにStringなど完成したバッファを渡してしまうと、結局一気に送ることになってしまって意味がない。

  res.body = "返信だよー"

ダラダラ送るにはIOぽいなにかを与える必要がある。bodyにreadpartialメソッドを持つオブジェクトを与えるとIOぽいと判断してくれる。(IOのサブクラスとか限定しないの、WEBrickさいこー!)

  res.body = MyStream.new(queue) # ← readpatial と close メソッド持つオブジェクト

雑に言うとこんなの。(ほんとのデータはQueueからくると想定してる場合の擬似コード

  class MyStream
    def initialize(queue)
      @queue = queue
    end

    def next_chunk
      @queue.pop
    rescue
      raise EOFError
    end

    def close
      # nop
    end

    def readpartial(size, buf='')
      buf.clear
      buf << next_chunk
      buf
    end
  end

ちゃんと書くならreadpartialのバッファサイズの指定があるので、それも考慮しなくてはならない。
Tofuに追加したChunkedStreamは次のように実装した。

  class ChunkedStream
    def initialize(stream)
      @data = nil
      @cursor = 0
      @stream = stream
    end

    def next_chunk
      @stream.pop
    rescue
      raise EOFError
    end

    def close
      @stream.close
    end

    def readpartial(size, buf='')
      buf.clear
      unless @data
        @cursor = 0
        @data = next_chunk
        @data.force_encoding("ascii-8bit")
      end
      if @data.bytesize <= size
        buf << @data
        @data = nil
      else
        slice = @data.byteslice(@cursor, size)
        @cursor += slice.bytesize
        buf << slice
        if @data.bytesize <= @cursor
          @data = nil
        end
      end
      buf
    end
  end

WEBrickのレスポンス ⇄ ChunkedStream ⇄ 本来アプリで表現したかったストリーム(@stream)、のように中間に入って使う。
WEBrickはレスポンスのチャンクを作るために、ChunkedStreamをreadpartialする。送っていないバッファがあればすぐに返却する。送るものがなければ、@streamからpopを試みる。
@streamがQueueのような待ち合わせ可能な同期メカニズムであれば、データがなければブロックするであろう。ブロックするということは、WEBrickのそのクライアント担当スレッドが止まるわけだけど、その他のスレッドには関係ないので問題ない。他のクライアントに対してはちゃんと仕事してくれる。WEBrickちゃんとしてる。

QueueのデータをServer Sent Eventsぽく返すなら次のようにする。(自分ならDripを使う)

class EventStream
  def initialize(queue)
    @queue = queue
  end

  def pop
    value = @queue.pop
    "data: #{value.to_json}\n\n"
  end

  def close;  end
end
  res.body = Tofu::ChunkedStream.new(EventStream.new(queue))

追記: もう少し親切な例

require 'webrick'
require 'tofu'
require 'drb'
require 'json'

class EventStream
  def initialize(queue)
    @queue = queue
  end

  def pop
    value = @queue.pop
    p value
    "data: #{value.to_json}\n\n"
  end

  def close; end
end

queue = Queue.new
DRb.start_service('druby://localhost:54321', queue)

s = WEBrick::HTTPServer.new(:Port => 8086)
s.mount_proc('/stream') {|req, res|
  res.content_type = 'text/event-stream'
  res.chunked = true
  res.body = Tofu::ChunkedStream.new(EventStream.new(queue))
}
s.start

別の端末からirbでデータ送る

% irb -r drb
irb(main):001:0> ro = DRbObject.new_with_uri('druby://localhost:54321')
irb(main):002:0> ro.push('hello')
irb(main):003:0> ro.push('world')

http://localhost:8086 表示してブラウザのコンソールで実験

var sse = new EventSource("/stream")
sse.onmessage = function(e) { console.log(e) }

そうそう

さて土曜日はとちぎRuby会議08ですね。
体調が悪くて参加できない方がいるようです。今から参加したくなった人はTL検索してチケットを譲ってもらおう!