目次
この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。
佐藤伸吾です。
今回はRubyを使ってPaSoRi経由でSuicaの乗車履歴を取得し、GoogleMapsやGoogleEarth上で表示してみました。以下、その仕組みについて詳しく解説していきます。
デモ動画
実際に動作している様子については、以下の動画をご覧下さい。
PaSoRi
PaSoRiとは、ソニーの非接触型ICカード「FeliCa」用の読み取り・書き込み機のことです。今回は「RC-S320」という機種を使用しました。
libpasori
libpasoriというライブラリが公開されており、これを用いれば、PaSoRiからの各種データ取得が可能です。
Mac上にて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も購読してみて下さい。次回をお楽しみに!!
個人ブログ : 拡張現実ライフ