その他
    ホーム 技術発信 DoRuby Rubyを使ってPaSoRi経由でSuicaの乗車履歴を取得し、GoogleMapsやGoogleEarthで表示する

    Rubyを使ってPaSoRi経由でSuicaの乗車履歴を取得し、GoogleMapsやGoogleEarthで表示する

    この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

    佐藤伸吾です。

    今回はRubyを使ってPaSoRi経由でSuicaの乗車履歴を取得し、GoogleMapsやGoogleEarth上で表示してみました。以下、その仕組みについて詳しく解説していきます。

    Rubyを使ってPaSoRi経由でSuicaの乗車履歴を取得する

    デモ動画

    実際に動作している様子については、以下の動画をご覧下さい。

    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&amp;v=2&amp;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も購読してみて下さい。次回をお楽しみに!!

    個人ブログ : 拡張現実ライフ