この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。
佐藤伸吾です。
今回はRubyを使ってPaSoRi経由でSuicaの乗車履歴を取得し、GoogleMapsやGoogleEarth上で表示してみました。以下、その仕組みについて詳しく解説していきます。
デモ動画
実際に動作している様子については、以下の動画をご覧下さい。
PaSoRi
PaSoRiとは、ソニーの非接触型ICカード「FeliCa」用の読み取り・書き込み機のことです。今回は「RC-S320」という機種を使用しました。
libpasori
libpasoriというライブラリが公開されており、これを用いれば、PaSoRiからの各種データ取得が可能です。
libpasori – RC-S320操作コード
Mac上にてlibpasoriを使用したい場合、以下のページが参考になります。
libpasori の共有ライブラリ化
libusb
libpasoriはlibusbも使用しますので、インストールして下さい。 % port search libusb
libusb devel/libusb 0.1.12 Library providing access to USB devices
% port variants libusb
libusb has the variants: universal
% sudo port install libusb
テストプログラム
試しにC言語でテストプログラムを書いてみましょう。 単にデータを読み出すだけであれば、以下のようなプログラムでOKです。
#include #include "libpasori.h" int main(void) { pasori *p; felica *f; uint8 d[16]; p = pasori_open(NULL); pasori_init(p); f = felica_polling(p, 0xfe00, 0, 0); felica_read_without_encryption02(f, 0x170f, 0, 0, d); printf("%d¥n", d[14]*256+d[15]); pasori_close(p); return 0;}
Suicaデータのフォーマット
SuicaデータのフォーマットについてはRuby で Suica を覗いてみる を参照してみて下さい。
rubyラッパーを書く
rubyからpasoriの機能を利用する為に、ラッパーを書きます。
詳細についてはRuby で PaSoRi 使ってみる を参照してみて下さい。
require 'dl/import' module Pasori extend DL::Importable dlload '/usr/local/lib/libpasori.dylib' typealias 'uint8', 'unsigned char' typealias 'uint16', 'unsigned int' #typealias 'uint16', 'unsigned short int' # libpasori.h extern 'pasori* pasori_open(char*)' extern 'void pasori_close(pasori*)' extern 'int pasori_send(pasori*,uint8*,uint8,int)' extern 'int pasori_recv(pasori*,uint8*,uint8,int)' POLLING_ANY = 0xffff POLLING_SUICA = 0x0003 POLLING_EDY = 0xfe00 SERVICE_SUICA = 0x090f SERVICE_EDY = 0x170f # libpasori_command.h extern 'int pasori_init(pasori*)' extern 'int pasori_write(pasori*,uint8,uint8)' extern 'int pasori_read(pasori*,uint8,uint8)' extern 'felica* felica_polling(pasori*,uint16,uint8,uint8)' extern 'int felica_read_without_encryption02(felica*,int,int,uint8,uint8*)' end module Pasori class << self def felica_raw_values systemcode, servicecode, little_endian = false values = [] b = Array.new(4).to_ptr psr = pasori_open "" pasori_init psr flc = felica_polling psr, systemcode, 0, 0 i = 0 while felica_read_without_encryption02(flc, servicecode, 0, i, b) == 0 row = b.to_a('I') data = "" row.size.times do |j| if little_endian 4.times { |k| data += sprintf "%02x", (row[j].to_i >> (8 * k)) & 0xff } else data += sprintf "%08x", row[j].to_i & 0xffffffff end end yield data if block_given? values << data i += 1 end pasori_close psr values end end end require 'pasori' Pasori.felica_raw_values Pasori::POLLING_SUICA, Pasori::SERVICE_SUICA, true do |data| puts data end
路線・駅コード
路線・駅コードについては、路線・駅コード一覧 からExcelファイルをダウンロードし、CSV形式で保存しておきます。
駅名から緯度経度を調べる
駅名から緯度経度を調べる為にGoogle Geocoder を使用しました。
GoogleMapsを利用したHTMLを生成する
erbを用いて、HTMLを生成します。GoogleMapsはJavaScriptから制御します。詳細についてはGoogle Mapsの基礎 などを参照してみて下さい。
<% require 'pasori' def Pasori.parse_suica_raw_value data d = "%016b" % data[8, 4].hex { :type => data[0, 2], :date => Time.local(d[0, 7].to_i(2) + 2000, d[7, 4].to_i(2), d[11, 5].to_i(2)), :in => data[12, 4], :out => data[16, 4], :yen => data[20, 2].hex + (data[22, 2].hex << 8), } endrequire 'station'stations = Station.read('StationCode.csv.utf8')list = [] Pasori.felica_raw_values Pasori::POLLING_SUICA, Pasori::SERVICE_SUICA, true do |data| d = Pasori.parse_suica_raw_value data station_in = nil station_out = nil list << [] list.last << d[:date] stations.each do |s| if s.area_cd.hex != 2 and s.line_cd.hex == d[:in][0,2].hex and s.station_cd.hex == d[:in][2,2].hex station_in = s list.last << s end end stations.each do |s| if s.area_cd.hex != 2 and s.line_cd.hex == d[:out][0,2].hex and s.station_cd.hex == d[:out][2,2].hex station_out = s list.last << s end end list.last << d[:yen] end%><html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <script src="http://maps.google.co.jp/maps?file=api&v=2&key=hogehoge" type="text/javascript" charset="utf-8"> </script> <style type="text/css"> #mymap { position: absolute; left: 0; height: 400px; width: 400px; } #list { margin-left: 400px; } </style> <script type="text/javascript"> var map; onload = function(){ map = new GMap2(document.getElementById("mymap")); map.setCenter(new GLatLng(35.6984, 139.7732), 13); map.addControl(new GLargeMapControl()); map.addControl(new GScaleControl()); map.addControl(new GMapTypeControl()); <% list.each do |inout| %> setMarkers('<%= inout[1].station_name %>駅'); setMarkers('<%= inout[2].station_name %>駅'); <% end %> } onunload = GUnload; onresize = function() { map.checkResize(); } var geocoder = new GClientGeocoder(); function moveTo(place){ geocoder.getLatLng(place, moveToThePlace); function moveToThePlace(latlng){ if(latlng){ map.panTo(latlng); }else{ } } } function setMarkers(place){ geocoder.getLocations(place, setMarkersToThePlaces); function setMarkersToThePlaces(locs){ if(locs.Status.code == G_GEO_SUCCESS){ for(var i=0; i<locs.Placemark.length; i++){ var point = locs.Placemark[i].Point; var lng = point.coordinates[0]; var lat = point.coordinates[1]; var latlng = new GLatLng(lat,lng); var mk = new GMarker(latlng); map.addOverlay(mk); } }else{ } } } </script> </head> <body> <h1>乗車履歴表示</h1> <form onsubmit="moveTo(this.place.value); return false;"> <input type="text" size="40" id="place" /> <input type="submit" value="move" /> </form> <div id="mymap" style="width:400px; height:400px;"></div> <div id="list"> <table border="1"> <% list.each do |inout| %> <tr> <td><%= inout[0].strftime('%Y/%m/%d') %></td> <td><%= inout[1].company_name %></td> <td><%= inout[1].line_name %></td> <td><a href="#" onclick="moveTo('<%= inout[1].station_name %>駅')"> <%= inout[1].station_name %></a></td><td>-></td><td><%= inout[2].company_name %></td> <td><%= inout[2].line_name %></td> <td><a href="#" onclick="moveTo('<%= inout[2].station_name %>駅')"> <%= inout[2].station_name %></a></td> <td><%= inout[3] %>円</td> </tr><% end %></table> </div> </body> </html>
GoogleEarthを利用して乗車履歴を表示する
乗車履歴を元にKMLというXMLファイルを生成し、Google Earthに読みこませれば、3Dツアーを再生することができます。KMLファイルの詳細についてはGoogle Earth KMLのレシピ を参照してみて下さい。
require 'pasori' def Pasori.parse_suica_raw_value data d = "%016b" % data[8, 4].hex { :type => data[0, 2], :date => Time.local(d[0, 7].to_i(2) + 2000, d[7, 4].to_i(2), d[11, 5].to_i(2)), :in => data[12, 4], :out => data[16, 4], :yen => data[20, 2].hex + (data[22, 2].hex << 8), } end$stderr.print "乗車履歴読み取り中"require 'station'stations = Station.read('StationCode.csv.utf8')list = [] Pasori.felica_raw_values Pasori::POLLING_SUICA, Pasori::SERVICE_SUICA, true do |data| d = Pasori.parse_suica_raw_value data station_in = nil station_out = nil list_ele = [] list_ele << d[:date] stations.each do |s| if s.area_cd.hex != 2 and s.line_cd.hex == d[:in][0,2].hex and s.station_cd.hex == d[:in][2,2].hex station_in = s list_ele << s end end stations.each do |s| if s.area_cd.hex != 2 and s.line_cd.hex == d[:out][0,2].hex and s.station_cd.hex == d[:out][2,2].hex station_out = s list_ele << s end end list_ele << d[:yen] next unless station_in and station_out list << list_ele $stderr.print '.' end$stderr.print "\n"require 'rexml/document' require 'open-uri' require 'nkf'puts '<?xml version="1.0" encoding="UTF-8"?>' puts '<kml xmlns="http://earth.google.com/xml/2.0">' puts '<Document>' puts '<name>kinshicyou.kml</name>' puts '<visibility>1</visibility>' puts '<open>1</open>' puts '<desctiption>desctiption</desctiption>'def output_point(lon, lat) puts '<visibility>1</visibility>' puts '<Style>' puts '<IconStyle>' puts '<Icon>' puts '<href>root://icons/palette-3.png</href>' puts '<x>96</x>' puts '<y>160</y>' puts '<w>32</w>' puts '<h>32</h>' puts '</Icon>' puts '</IconStyle>' puts '</Style>' puts '<Point>' puts '<extrude>1</extrude>' puts '<altitudeMode>relativeToGround</altitudeMode>' puts '<coordinates>' + lon + ',' + lat + ',50</coordinates>' puts '</Point>'enddef add_heading $heading += 90 $heading = $heading % 360 enddef output_placemark(g, inout) address = inout[1].station_name + '駅(東京)' point = g.getPoint(address) if point lon = point[0] lat = point[1] name = inout[0].strftime('%Y/%m/%d') name += ' ' name += address name += ' ' name += '乗車' puts '<Placemark>' puts '<name>' + name + '</name>' puts '<desctiption>description</desctiption>' puts '<LookAt>' puts '<longitude>' + lon + '</longitude>' puts '<latitude>' + lat + '</latitude>' puts '<range>200</range>' puts '<tilt>60</tilt>' puts '<heading>' + $heading.to_s + '</heading>' add_heading() puts '</LookAt>' output_point(lon, lat) puts '</Placemark>' end address = inout[2].station_name + '駅(東京)' point = g.getPoint(address) if point lon = point[0] lat = point[1] name = inout[0].strftime('%Y/%m/%d') name += ' ' name += address name += ' ' name += '下車' puts '<Placemark>' puts '<name>' + name + '</name>' puts '<desctiption>description</desctiption>' puts '<LookAt>' puts '<longitude>' + lon + '</longitude>' puts '<latitude>' + lat + '</latitude>' puts '<range>200</range>' puts '<tilt>60</tilt>' puts '<heading>' + $heading.to_s + '</heading>' add_heading() puts '</LookAt>' output_point(lon, lat) puts '</Placemark>' endendrequire 'geocoder'key = 'hogehoge' format = "xml" g = Geocoder.new(key, format) $stderr.print "緯度経度取得中"$heading = 0; list.reverse.each do |inout| output_placemark(g, inout) $stderr.print '.' end$stderr.print "\n"$stderr.print "経路情報描画中"puts '<Placemark>' puts '<Style>' puts '<LineStyle>' puts '<color>99ff0000</color>' puts '<width>6</width>' puts '</LineStyle>' puts '</Style>' puts '<LineString>' puts '<altitudeMode>relativeToGround</altitudeMode>' puts '<coordinates>'list.reverse.each do |inout| address = inout[1].station_name + '駅(東京)' point = g.getPoint(address) if point lon = point[0] lat = point[1] puts sprintf("%s,%s,50", lon, lat) end address = inout[2].station_name + '駅(東京)' point = g.getPoint(address) if point lon = point[0] lat = point[1] puts sprintf("%s,%s,50", lon, lat) end $stderr.print '.' endputs '</coordinates>' puts '</LineString>' puts '</Placemark>' $stderr.print "\n"puts '</Document>' puts '</kml>'$stderr.print "処理終了!!\n"
まとめ
いかがでしたか?前回のGainerや今回のPaSoRiといったハードウェアをRubyから制御することによって、アイディア次第で面白い仕組みを比較的簡単に作ることが可能になります。
次回もRubyと何かを組み合わせて面白い仕組みを紹介する予定です。よろしければこのブログのRSSも購読してみて下さい。次回をお楽しみに!!
個人ブログ : 拡張現実ライフ