Rubotoを使い、AndroidでのGPS衛星情報をNMEA形式で取得する

Rubotoには、GPSで現在位置を取得するチュートリアルがあります。
ただ、GPS衛星情報をNMEA形式で取得するものがなかったため、Rubotoでできるかどうか試してみました。


なお、AndroidやXmarinでは取得している例があったため、そちらも参考にしました。


また、参考にしたAndroidのドキュメントは、以下のとおりです。


全体では以下を参考にしました。

環境

アプリの生成

Rubotoにはジェネレータがあるので、それを使って生成します。今回はJRubyをアプリに同梱させるため、オプションを追加しておきます。

ruboto gen app --package org.ruboto.example.nmea --target android-17 --with-jruby

パーミッションの追加

GPSまわりを使うので、パーミッションを追加します。

nmea\AndroidManifest.xml の「uses-sdk」タグの下あたり
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

rake・dxファイルの修正

前回の記事でも記載していますが、Windows上で開発する時はヒープメモリのサイズを変更します。

nmea\rakelib\ruboto.rake 52行目あたり
MINIMUM_DX_HEAP_SIZE = 768
C:\androidsdk\build-tools\17.0.0\dx.bat 54行目あたり
set defaultXmx=-Xmx768M

デバッグ

Android Debug Monitorを利用して、ログを見ながらデバッグしました。コマンドプロンプトの以下のコマンドで起動できます。

monitor

アプリの記述

Rubotoのチュートリアルにならい、nmea\src\ruboto\nmea_activity.rb を記述します。

表示するNMEAデータについて

Nexus7で取得できたNMEAセンテンスをそれぞれ表示するようにしました。



クラス数について

上記の参考資料では一つのクラスに必要なメソッドを追加していますが、Rubotoでやろうとしたところエラーが出てうまくいきませんでした。
そのため、チュートリアルと同じように「NmeaActivity」「MyNMEA」「MyLocationListener」の3クラス構成にしています。



addNmeaListenerを実装するイベントについて

Androidのライフサイクルを見て、onResumeとonPauseがいいかなと思い、実装することにしました。


いちおう、他のアプリでも同じイベントに記載しているのを見かけました。

MyLocationListenerクラスでのエラー回避用メソッドについて

エラー回避用に、以下の3つのメソッドを追加しています。

  • hashCode
  • __ruby_object
  • equals


hashCodeはチュートリアルのコードにも記載されている通りですが、それ以外については、「他のアプリを起動した後、再度このアプリを前面にした時」に発生するエラーを回避するために実装しています。

  • __ruby_objectがない場合

Caused by: org.jruby.embed.InvokeFailedException: (NoMethodError) undefined method `__ruby_object' for #
at org.jruby.embed.internal.EmbedRubyObjectAdapterImpl.call(EmbedRubyObjectAdapterImpl.java:317)
at org.jruby.embed.internal.EmbedRubyObjectAdapterImpl.runRubyMethod(EmbedRubyObjectAdapterImpl.java:276)
at org.jruby.embed.ScriptingContainer.runRubyMethod(ScriptingContainer.java:1547)
... 18 more
Caused by: org.jruby.exceptions.RaiseException: (NoMethodError) undefined method `__ruby_object' for #
at org.jruby.RubyBasicObject.method_missing(org/jruby/RubyBasicObject.java:1696)
at RUBY.method_missing(file:/data/app/org.ruboto.example.nmea-1.apk!/ruboto/base.rb:20)
at RUBY.onResume(jar:file:/data/app/org.ruboto.example.nmea-1.apk!/nmea_activity.rb:73)

  • equalsがない場合

Caused by: org.jruby.embed.InvokeFailedException: (NoMethodError) undefined method `equals' for #
at org.jruby.embed.internal.EmbedRubyObjectAdapterImpl.call(EmbedRubyObjectAdapterImpl.java:317)
at org.jruby.embed.internal.EmbedRubyObjectAdapterImpl.runRubyMethod(EmbedRubyObjectAdapterImpl.java:276)
at org.jruby.embed.ScriptingContainer.runRubyMethod(ScriptingContainer.java:1547)
... 18 more
Caused by: org.jruby.exceptions.RaiseException: (NoMethodError) undefined method `equals' for #
at org.jruby.RubyBasicObject.method_missing(org/jruby/RubyBasicObject.java:1696)
at RUBY.method_missing(file:/data/app/org.ruboto.example.nmea-2.apk!/ruboto/base.rb:20)
at RUBY.onResume(file:/storage/emulated/0/Android/data/org.ruboto.example.nmea/files/scripts/nmea_activity.rb:72)


全体のソースは以下のとおりです。
Gistはこちら → Rubotoを使い、AndroidでのGPS衛星情報をNMEA形式で取得する

  • nmea_activity.rb
require 'ruboto/widget'

ruboto_import_widgets :LinearLayout, :TextView

java_import 'android.content.Context'
java_import 'android.location.LocationManager'


class NmeaActivity
  def onCreate(bundle)
    super
    @lm = getSystemService(Context::LOCATION_SERVICE)
    @ll = MyLocationListener.new
    

    set_title 'NMEA Example'

    self.content_view = linear_layout orientation: :vertical do
      linear_layout do
        text_view text: 'QZGSV: '
        @qzgsv_view = text_view text: ''
      end

      linear_layout do
        text_view text: 'QZGSA: '
        @qzgsa_view = text_view text: ''
      end

      linear_layout do
        text_view text: 'GPRMC: '
        @gpmrc_view = text_view text: ''
      end

      linear_layout do
        text_view text: 'GNGSA: '
        @gngsa_view = text_view text: ''
      end

      linear_layout do
        text_view text: 'GPGSV: '
        @gpgsv_view = text_view text: ''
      end

      linear_layout do
        text_view text: 'GPGSA: '
        @gpgsa_view = text_view text: ''
      end

      linear_layout do
        text_view text: 'GPGGA: '
        @gpgga_view = text_view text: ''
      end

      linear_layout do
        text_view text: 'PGLOR: '
        @pglor_view = text_view text: ''
      end

      linear_layout do
        text_view text: 'Others: '
        @others_view = text_view text: ''
      end
    end
  end


  def onResume
    super
    @lm.addNmeaListener(MyNMEA.new(self))
    @lm.requestLocationUpdates(LocationManager::GPS_PROVIDER, 1000, 5, @ll)    
  end
  

  def onPause
    super
    Thread.start do
      @lm.removeUpdates @ll
      @lm.removeNmeaListener @ll
    end
  end


  def update_nmea(nmea)
    case nmea.split(',')[0]
    when '$QZGSV'
      @qzgsv_view.text = nmea

    when '$QZGSA'
      @qzgsa_view.text = nmea

    when '$GPRMC'
      @gpmrc_view.text = nmea

    when '$PGLOR'
      @pglor_view.text = nmea

    when '$GNGSA'
      @gngsa_view.text = nmea

    when '$GPGSV'
      @gpgsv_view.text = nmea

    when '$GPGSA'
      @gpgsa_view.text = nmea

    when '$GPGGA'
      @gpgga_view.text = nmea

    else
      @others_view.text = nmea
      
    end
  end
end


class MyNMEA
  def initialize(activity)
    @activity = activity
  end

  def onNmeaReceived(timestamp, nmea)
    @activity.update_nmea(nmea)
  end

  # Required on the Java side when registered
  def hashCode
    hash
  end
end


class MyLocationListener
  # Add "__ruby_object" "equlas" method for error avoidance
  def __ruby_object
  end

  def equals(hoge)
  end

  # Required on the Java side when registered
  def hashCode
    hash
  end
  
  def onLocationChanged(location)
  end

  def onProviderDisabled(provider)
  end

  def onProviderEnabled(provider)
  end

  def onStatusChanged(provider, status, extras)
  end
end