QRコードを使って、ちょっとしたメモをiPhoneに転送するためのアプリhitori-koto.rb。gitの使い方を本当に忘れたのでgistで。(でもgistのファイルの順が気に入らないのでふつうに貼った)
URLやちょっとした文(例えばtwitterに流したいようなもの)をネットワークを介さずにiPhoneに送りたいことがままあるので、QRコードで文字情報を交換するものを書いた。というか元ネタを考えてくれたのはtsu某i氏。テキストエディタ+QRコード表示みたいなものも作ってみたけど、iPhone側でそんなに長い文章を認識するのがしんどいことがわかってきたので、一行に特化することにした。独り用のタイムライン風の画面にQRコードを表示するだけ。タイムラインはクッキーによるセッション単位に保持されるので、他人の情報が混じることはありません。
実装側のおもしろさとしては、QRコードのpngのデータをどこで持つか、誰が返すかっていうところなんだけど、Tofuで素朴にやったのであんまり苦労しなかった。
そうそう。rqrがメモリにQRコードを出力してくれない(あるいはやり方がわからない)ので泣く泣くTempfileを使っているのが悲しいところ。
誰も試さないと思うけど、必要なライブラリはRBTree, rqr, Tofu(and Div)です。Tofuはgitから持ってこれます。やり方思い出せないけど。
RBTreeのキーにTimeを使うことにした。tzを考えなくてよい場合、Timeを格納、復元するにはsecとusecの二つの整数があればいいのかしらん。
require 'div' require 'tofu/tofu' require 'tofu/tofulet' require 'monitor' require 'rbtree' require 'enumerator' require 'rqr' require 'time' class Store include MonitorMixin def initialize super() @tree = RBTree.new @enum = @tree.to_enum(:reverse_each) end attr_reader :tree def qr_at(time) _, qr = @tree[time] qr || NotFoundQR end def each_slice(n, &blk) synchronize do @enum.each_slice(n, &blk) nil end end def import_string(str) NKF.nkf('-sdXm0', str) end def self.qr_code(text) tmp = Tempfile.new('hitori_koto') RQR::QRCode.create do |qr| qr.save(text, tmp.path, :png) end tmp.open tmp.read ensure tmp.close(true) if tmp end def qr_code(text) self.class.qr_code(text) end def to_qr(text) qr_code(text) rescue RQR::EncodeException EncodeErrorQR end def add(str, context=nil) str = import_string(str) synchronize do key = Time.now latest, _ = @tree.last while latest == key key = Time.now end @tree[key] = [str, to_qr(str)] key end end NotFoundPNG = qr_code('not found') EncodeErrorPNG = qr_code('QR encode error') end class HitoriKotoSession < Div::TofuSession def initialize(bartender, hint=nil) super @content = Store.new @base = BaseDiv.new(self) @age = nil end attr_reader :content def do_qr_code(context) time = qr_path_to_time(context.req_path_info) return false unless time qr = @content.qr_at(time) context.res_header('expires', expires.httpdate) context.res_header('content-type', 'image/png; name="qrcode.png"') context.res_body(qr) return true end def qr_path(time) "qr" + h(time_to_key(time)) + ".png" end def qr_path_to_time(path) if /qr(.*)\.png$/ =~ path key_to_time($1) else nil end end def time_to_key(time) (time.to_i * 1000000 + time.usec).to_s(36) end def key_to_time(str) d = str.to_i(36) i, u = d.divmod(1000000) Time.at(i, u) rescue Time.now end def do_GET(context) update_div(context) return if do_qr_code(context) context.res_header('content-type', 'text/html; charset=Shift_JIS') context.res_body(@base.to_html(context)) end end class BaseDiv < Div::Div set_erb('base.erb') def initialize(session) super(session) @enter = EnterDiv.new(session) @list = ListDiv.new(session) end end class EnterDiv < Div::Div set_erb('enter.erb') def do_enter(context, params) str ,= params['str'] str = '(nil)' if (str.nil? || str.empty?) @session.content.add(str, context) end end class ListDiv < Div::Div set_erb('list.erb') def initialize(session) super(session) @content = session.content end def group_header(time) time.strftime("%H:%M") end end unless $DEBUG exit!(0) if fork Process.setsid exit!(0) if fork end uri = ARGV.shift || 'druby://localhost:54322' tofu = Tofu::Bartender.new(HitoriKotoSession, 'hitori_' + uri.split(':').last) DRb.start_service(uri, WEBrick::CGITofulet.new(tofu)) unless $DEBUG STDIN.reopen('/dev/null') STDOUT.reopen('/dev/null', 'w') STDERR.reopen('/dev/null', 'w') end DRb.thread.join