Страница 1 из 1

GPS на Android. Прямое измерение скорости.

СообщениеДобавлено: 13 янв 2019, 19:51
homoludens
Здравствуйте, Коллеги!

Делаю мобильное приложение (пока под Андроид), которое вычисляет скорость аппарата по акселерометру с коррекцией по GPS.
В настоящее время смигирровал с Xamarin на Unity.

На Xamarin есть возможность подписаться на события LocationManager, который выдает как положение, так и скорость (см. док. для LocationManager > getSpeed).

Обнаружил, что Unity выдает только положение по GPS.

Вопрос к знатокам:
Каков наименее трудоемкий способ получения прямого измерения скорости на Android?

ЗЫ: нашел на форуме пост про Java плагин к Unity
Нет ли способа обойтись без кастомизации UnityPlayerActivity (средства Unity3D или уже готовые плагины)?
Хочется избежать программирования еще и в Java + модификации нутра Unity3D.

ЗЫ2: если без модификации UnityPlayerActivity не обойтись, то киньте плиз пошаговую инструкцию "от и до". На английском или русском, неважно. Главное, чтоб описано было в каких программах что делается (напр., какой проект в Android Studio создавать и т.п.) и какие команды в консоли выполнять. Я - дотнетчик и в Android Studio даже по нужде не ходил толком :).

Спасибищще за помощь! Да прибудут с Вами всякие силы!

Re: GPS на Android. Прямое измерение скорости.

СообщениеДобавлено: 14 янв 2019, 10:14
IDoNotExist
homoludens писал(а):ЗЫ2: если без модификации UnityPlayerActivity не обойтись, то киньте плиз пошаговую инструкцию "от и до". На английском или русском, неважно. Главное, чтоб описано было в каких программах что делается (напр., какой проект в Android Studio создавать и т.п.) и какие команды в консоли выполнять. Я - дотнетчик и в Android Studio даже по нужде не ходил толком :).

Android Studio для java плагинов пользоваться не нужно, всё делается прямо в Unity https://docs.unity3d.com/Manual/AndroidJARPlugins.html

homoludens писал(а):Обнаружил, что Unity выдает только положение по GPS.

GPS в общем то всегда возвращает только позиции, getSpeed это уже дополнительная приблуда Xamarin которая считает среднюю скорость на их основе.

homoludens писал(а):Каков наименее трудоемкий способ получения прямого измерения скорости на Android?

Лично я делал так: брал несколько позиций, сортировал их по timestamp, считал дельту между ними, вычислял скорости между позициями по расстоянию и по timestamp дельте, брал среднюю скорость.

Re: GPS на Android. Прямое измерение скорости.

СообщениеДобавлено: 14 янв 2019, 13:14
homoludens
IDoNotExist писал(а):
homoludens писал(а):...

homoludens писал(а):Обнаружил, что Unity выдает только положение по GPS.

GPS в общем то всегда возвращает только позиции, getSpeed это уже дополнительная приблуда Xamarin которая считает среднюю скорость на их основе.

...


Я на ксамарине сравнивал значения скорости выданные из метода getSpeed() со скоростью вычисленной по двум позициям на поверхности шарика. Значения сильно отличаются.
Если бы ксамарин сам вычислял скорость, то при первом измерении скорости не было бы.

Метод getSpeed проброшен из Андроида, судя по документации и декомпилированному с помощью dotPeek коду.
Я почти уверен, что Ксамариновский getSpeed возвращает скорость от Андроида, измеренную с использованием доплеровского эффекта вместо калькуляции по позициям.

Re: GPS на Android. Прямое измерение скорости.

СообщениеДобавлено: 14 янв 2019, 13:19
homoludens
IDoNotExist писал(а):
homoludens писал(а):ЗЫ2: если без модификации UnityPlayerActivity не обойтись, то киньте плиз пошаговую инструкцию "от и до". На английском или русском, неважно. Главное, чтоб описано было в каких программах что делается (напр., какой проект в Android Studio создавать и т.п.) и какие команды в консоли выполнять. Я - дотнетчик и в Android Studio даже по нужде не ходил толком :).

Android Studio для java плагинов пользоваться не нужно, всё делается прямо в Unity https://docs.unity3d.com/Manual/AndroidJARPlugins.html
...


Java source files as plug-ins
Имеется в виду этот подход?

1. Просто кладу исходник с модифицированным плеером юнити в каталог Plugins вместо Plugins/Android
2. В инспекторе ставлю флаг, что он является плагином для платформы Android
3. В компонентах юзаю JNI хэлперы для вызова метода из модифицированного плеера или других классов, включенных в исходник.

Так?

PS: положу здесь ссылку на доки про вызов Java кода, если вдруг кому-то понадобится.

Re: GPS на Android. Прямое измерение скорости.

СообщениеДобавлено: 14 янв 2019, 13:51
IDoNotExist
homoludens писал(а):Я на ксамарине сравнивал значения скорости выданные из метода getSpeed() со скоростью вычисленной по двум позициям на поверхности шарика. Значения сильно отличаются.

Очевидно, что алгоритмы сильно разные. Для моих целей вполне хватало подхода который я описывал выше.

homoludens писал(а):Если бы ксамарин сам вычислял скорость, то при первом измерении скорости не было бы.

"Первое измерение" это я как понимаю - когда событие приходит в первый раз? Собственно кто вам сказал что там внутри это реально первое измерение?

homoludens писал(а):Метод getSpeed проброшен из Андроида, судя по документации и декомпилированному с помощью dotPeek коду.
Я почти уверен, что Ксамариновский getSpeed возвращает скорость от Андроида, измеренную с использованием доплеровского эффекта вместо калькуляции по позициям.

Всё может быть, я лично даже и близко не в курсе как там внутри это работает, но считаю что там всё довольно тривиально. Если разберётесь и поделитесь информацией, уверен что многим поможете.

homoludens писал(а):1. Просто кладу исходник с модифицированным плеером юнити в каталог Plugins вместо Plugins/Android

Какой еще модифицированный юнити плеер? И главное зачем?

https://habr.com/company/pixonic/blog/353444/
Вот это ещё почитайте, может поможет.

Re: GPS на Android. Прямое измерение скорости.

СообщениеДобавлено: 19 янв 2019, 19:55
homoludens
IDoNotExist писал(а):...
homoludens писал(а):Метод getSpeed проброшен из Андроида, судя по документации и декомпилированному с помощью dotPeek коду.
Я почти уверен, что Ксамариновский getSpeed возвращает скорость от Андроида, измеренную с использованием доплеровского эффекта вместо калькуляции по позициям.

Всё может быть, я лично даже и близко не в курсе как там внутри это работает, но считаю что там всё довольно тривиально. Если разберётесь и поделитесь информацией, уверен что многим поможете.

homoludens писал(а):1. Просто кладу исходник с модифицированным плеером юнити в каталог Plugins вместо Plugins/Android

Какой еще модифицированный юнити плеер? И главное зачем?

https://habr.com/company/pixonic/blog/353444/
Вот это ещё почитайте, может поможет.


Там нетривиально все... В аспирантуре работал с парнем, который на спутниковой навигации специализировался (я больше по акселерометрам с гироскопами).. Так вот когда телефон движется, то изменяется частота несущего сигнала спутника (подобно звуку мотора от проезжающей машины). Андроидовский метод getSpeed() как раз выдает это доплеровское значение скорости. И чем быстрее телефон движется тем точнее это измерение.

А вот Юнити выдает только позиции. И получается засада. Если измерения от GPS приходят раз в секунду и я за секунду шагну вперед и вернусь обратно, то по позициям скорость будет ноль. А вот доплеровское измерение покажет скорость с которой я возвращался.

"Какой еще модифицированный юнити плеер? И главное зачем?"
Я ссылался на соседнюю ветку... там чел сделал на Java наследника от UnityPlayerActivity (или как-то так). Это точка входа в игру на Юнити со стороны андроида.

Чтобы получить доступ к датчикам андроида напрямую (минуя юнити), можно унаследоваться от Unity...Activity и добавить в нее код, который подписывается на события GPS.
Я не уверен, но кажись без экземпляра Activity нельзя получить доступ к LocationManager и нельзя поэтому подписаться на GPS.

Но переключаться с C# на Java - это то еще удовольствие.

PS: за ссылку спасибо.. попробую ее вкурить до фильтра :)

Re: GPS на Android. Прямое измерение скорости.

СообщениеДобавлено: 19 янв 2019, 19:59
homoludens
IDoNotExist писал(а):"Первое измерение" это я как понимаю - когда событие приходит в первый раз? Собственно кто вам сказал что там внутри это реально первое измерение?


Да, правильно поняли...
1. Сам Андроид в шторку добавляет иконку когда GPS активируется
2. Сначала идет поиск спутников и собирается альманах. Это видно в шторке уведомлений. Каждый раз при старте аппа заново делается поиск координат.
3. Андроид не будет держать GPS активным постоянно в ожидании приложения с доступом к GPS. Это жрет батарею и делает бессмысленными запрос разрешений на доступ к Fine Location.

Re: GPS на Android. Прямое измерение скорости.

СообщениеДобавлено: 20 янв 2019, 13:29
homoludens
Есть проблема с добавлением плагина в виде Java файла.

1. Создал каталог Assets/GpsPlugin
2. Положил в него свой модифицированный GpsUnityPlayerActivity
3. Выделяю файл, как описано в инструкции "Using Java or Kotlin source files as plug-ins", но в инспекторе пусто.

Что я мог сделать неправильно или не доделать?

Синтаксис:
Используется java
package com.redsense.speedometer;
import com.unity3d.player.UnityPlayerActivity;
import android.content.Context;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.SystemClock;
import android.util.Config;
import android.util.Log;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class GpsUnityPlayerActivity extends UnityPlayerActivity {

    private static final String TAG = "GPS_Unity";
    private static final int SPLINE_LENGTH = 4;
    private static SplineInterpolator spline = null;
   
    private static LinkedList<Location> lastLocations;
    private static LinkedList<Long> lastLocationTimes;
    public static LocationManager locationManager;  
 
    static LocationListener gpsLocationListener;
   
    public static void startLocationListeners() {        
        gpsLocationListener = new LocationListener() {
            @Override
            public void onLocationChanged(Location location) {
                lastLocations.add(location);
                lastLocationTimes.add(SystemClock.uptimeMillis());

                if (lastLocations.size() >= SPLINE_LENGTH) { lastLocations.popFirst(); }
                if (lastLocationTimes.size() >= SPLINE_LENGTH) { lastLocationTimes.popFirst(); }

                recreateSpline();

                Log.i(TAG, "Getting Location over <span class=\"posthilit\">GPS</span> " + currentLocation.toString());
            }

            public void onProviderDisabled(String provider) {
            }

            public void onProviderEnabled(String provider) {
            }

            public void onStatusChanged(String provider, int status, Bundle extras) {
            }
        };

        locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, gpsLocationListener);
    }

    public static float getSpeed()
    {
        float time = SystemClock.uptimeMillis() * 0.001f;
        float extraSpeed = spline.interpolate(time);

        return extraSpeed;
    }

    private void seedLocations(){
        Location location = new Location("");//provider name is unnecessary
        location.setSpeed(0.0f);
        long currentTime = SystemClock.uptimeMillis() - SPLINE_LENGTH * 1000; // shifting time back by SPLINE_LENGTH seconds

        for (int i = 0; i < SPLINE_LENGTH; i++) {
            lastLocations.add(location);
            lastLocationTimes.add(currentTime + i * 1000);
        }
    }

    private static void recreateSpline() {
        final float[] speed = new float[SPLINE_LENGTH + 1];
        final float[] time = new float[SPLINE_LENGTH + 1];
       
        for (int i = 0; i < lastLocations.size(); i++) {
            speed.add(lastLocations[i].getSpeed());
            time.add(lastLocationTimespi[i] * 0.0001f);
        }

        float extraSpeed = speed[SPLINE_LENGTH - 1];
        float extraTime = time[SPLINE_LENGTH - 1] + 10.0f;
        speed[SPLINE_LENGTH] = extraSpeed;
        time[SPLINE_LENGTH] = extraTime;

        spline = SplineInterpolator.createMonotoneCubicSpline(time, speed);
    }
   
    @Override
    protected void onCreate(Bundle myBundle) {
        super.onCreate(myBundle);
        locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
        seedLocations();
        startLocationListeners();
    }

    @Override
    protected void onResume() {
        if (Config.DEBUG)
            Log.d(TAG, "onResume");
        super.onResume();
        startLocationListeners();
    }

    @Override
    protected void onPause()
    {
        locationManager.removeUpdates(gpsLocationListener);
        super.onPause();
    }

    @Override
    protected void onStop() {
        if (Config.DEBUG)
            Log.d(TAG, "onStop");
        locationManager.removeUpdates(gpsLocationListener);
        super.onStop();
    }
}

public class SplineInterpolator {

        private final List<Float> mX;
        private final List<Float> mY;
        private final float[] mM;

        private SplineInterpolator(List<Float> x, List<Float> y, float[] m) {
                mX = x;
                mY = y;
                mM = m;
        }

        public static SplineInterpolator createMonotoneCubicSpline(float[] x, float[] y) {
                if (x == null || y == null || x.length != y.length || x.length < 2) {
                        throw new IllegalArgumentException("There must be at least two control "
                                        + "points and the arrays must be of equal length.");
                }

                final int n = x.length;
                float[] d = new float[n - 1];
                float[] m = new float[n];

                // Compute slopes of secant lines between successive points.
                for (int i = 0; i < n - 1; i++) {
                        float h = x[i + 1] - x[i];
                        if (h <= 0f) {
                                throw new IllegalArgumentException("The control points must all "
                                                + "have strictly increasing X values.");
                        }
                        d[i] = (y[i + 1] - y[i]) / h;
                }

                // Initialize the tangents as the average of the secants.
                m[0] = d[0];
                for (int i = 1; i < n - 1; i++) {
                        m[i] = (d[i - 1] + d[i]) * 0.5f;
                }
                m[n - 1] = d[n - 2];

                // Update the tangents to preserve monotonicity.
                for (int i = 0; i < n - 1; i++) {
                        if (d[i] == 0f) { // successive Y values are equal
                                m[i] = 0f;
                                m[i + 1] = 0f;
                        } else {
                                float a = m[i] / d[i];
                                float b = m[i + 1] / d[i];
                                float h = (float) Math.hypot(a, b);
                                if (h > 9f) {
                                        float t = 3f / h;
                                        m[i] = t * a * d[i];
                                        m[i + 1] = t * b * d[i];
                                }
                        }
                }
                return new SplineInterpolator(x, y, m);
        }

        /**
         * Interpolates the value of Y = f(X) for given X. Clamps X to the domain of the spline.
         *
         * @param x
         *            The X value.
         * @return The interpolated Y = f(X) value.
         */

        public float interpolate(float x) {
                // Handle the boundary cases.
                final int n = mX.size();
                if (Float.isNaN(x)) {
                        return x;
                }
                if (x <= mX.get(0)) {
                        return mY.get(0);
                }
                if (x >= mX.get(n - 1)) {
                        return mY.get(n - 1);
                }

                // Find the index 'i' of the last point with smaller X.
                // We know this will be within the spline due to the boundary tests.
                int i = 0;
                while (x >= mX.get(i + 1)) {
                        i += 1;
                        if (x == mX.get(i)) {
                                return mY.get(i);
                        }
                }

                // Perform cubic Hermite spline interpolation.
                float h = mX.get(i + 1) - mX.get(i);
                float t = (x - mX.get(i)) / h;
                return (mY.get(i) * (1 + 2 * t) + h * mM[i] * t) * (1 - t) * (1 - t)
                                + (mY.get(i + 1) * (3 - 2 * t) + h * mM[i + 1] * (t - 1)) * t * t;
        }
}
 


Синтаксис:
Используется xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.redsense.SpeedometerUnity" xmlns:tools="http://schemas.android.com/tools" android:versionName="1.0" android:versionCode="1" android:installLocation="auto">
  <supports-screens android:smallScreens="true" android:normalScreens="true" android:largeScreens="true" android:xlargeScreens="true" android:anyDensity="true" />
  <application android:theme="@style/UnityThemeSelector" android:icon="@drawable/app_icon" android:label="@string/app_name" android:debuggable="false" android:isGame="true" android:banner="@drawable/app_banner">
    <activity android:name="com.redsense.speedometer.GpsUnityPlayerActivity" android:label="@string/app_name" android:screenOrientation="landscape" android:launchMode="singleTask" android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale|layoutDirection">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
      <meta-data android:name="unityplayer.UnityActivity" android:value="true" />
    </activity>
    <meta-data android:name="unity.build-id" android:value="0e35edf6-f464-4e21-b2dc-6c63037c3dc6" />
    <meta-data android:name="unity.splash-mode" android:value="0" />
    <meta-data android:name="unity.splash-enable" android:value="True" />
  </application>
  <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="28" />
  <uses-feature android:glEsVersion="0x00020000" />
  <uses-feature android:name="android.hardware.vulkan" android:required="false" />
  <uses-feature android:name="android.hardware.touchscreen" android:required="false" />
  <uses-feature android:name="android.hardware.touchscreen.multitouch" android:required="false" />
  <uses-feature android:name="android.hardware.touchscreen.multitouch.distinct" android:required="false" />
</manifest>