Как записать показания датчиков смартфона в файл
Перейти к содержимому

Как записать показания датчиков смартфона в файл

  • автор:

Собираем показания датчиков

Датчики, следящие за физическими свойствами и состоянием окружающей среды, предоставляют инновационные способы для улучшения мобильных приложений. Наличие в современных телефонах электронных компасов, датчиков равновесия, яркости и близости открывает целый ряд новых возможностей для взаимодействия с устройством, таких как дополненная реальность и ввод данных, основанный на перемещениях в пространстве.

Датчики в Android делятся на несколько категорий: движения, положения и окружающей среды. Ниже перечислены некоторые виды популярных датчиков:

  • Акселерометр (TYPE_ACCELEROMETER)
  • Гироскоп (TYPE_GYROSCOPE)
  • Датчик освещения (TYPE_LIGHT)
  • Датчик расстояния (TYPE_PROXIMITY)
  • Датчик магнитных полей (TYPE_MAGNETIC_FIELD)
  • Барометр (TYPE_PRESSURE)
  • Датчик температуры окружающей среды (TYPE_AMBIENT_TEMPERATURE)
  • Измеритель относительной влажности (TYPE_RELATIVE_HUMIDITY)

В каждом телефоне может быть свой набор датчиков. В большинстве аппаратов есть — акселерометр и гироскоп.

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

Необходимо помнить несколько вещей, работая с датчиками:

  • Показания бывают очень неровными. Вам нужно использовать какое-то среднее значение показаний, но не переборщить, чтобы приложение оставалось отзывчивым
  • Данные приходят неравномерно. Не ждите спокойного, ровного потока данных
  • Попробуйте предугадать будущие действия пользователя. Например, если идут данные о начале вращения устройства, можно предугадать следующее движение и подготовиться к нему

На эмуляторе практически невозможно тестировать работу с датчиками, поэтому используйте реальные устройства. В последних версиях эмуляторов список возможностей датчиков расширился. Смотрите в настройках эмулятора раздел Virtual sensors.

За работу с сенсорами отвечает класс SensorManager, содержащий несколько констант, которые характеризуют различные аспекты системы датчиков Android, в том числе:

Тип датчика Ориентация, акселерометр, свет, магнитное поле, близость, температура и т.д. Частота измерений Максимальная, для игр, обычная, для пользовательского интерфейса. Когда приложение запрашивает конкретное значение частоты отсчётов, с точки зрения сенсорной подсистемы это лишь рекомендация. Никакой гарантии, что измерения будут производиться с указанной частотой, нет. Точность Высокая, низкая, средняя, ненадёжные данные.

Типы датчиков

  • TYPE_ACCELEROMETER — Измеряет ускорение в пространстве по осям X, Y, Z
  • TYPE_AMBIENT_TEMPERATURE — Новый датчик для измерения температуры (API 14) в градусах Цельсия, который заменил устаревший TYPE_TEMPERATURE
  • TYPE_GRAVITY — Трёхосевой датчик силы тяжести. Как правило, это виртуальный датчик и представляет собой низкочастотный фильтр для показаний, возвращаемых акселерометром
  • TYPE_GYROSCOPE — Трёхосевой гироскоп, возвращающий текущее положение устройства в пространстве в градусах по трём осям. По другим данным, возвращает скорость вращения устройства по трём осям в радианах в секунду.
  • TYPE_LIGHT — Измеряет степень освещённости. Датчик окружающей освещённости, который описывает внешнюю освещённость в люксах. Этот тип датчиков обычно используется для динамического изменения яркости экрана.
  • TYPE_LINEAR_ACCELERATION — Трёхосевой датчик линейного ускорения, возвращающий показатели ускорения без учёта силы тяжести. Это виртуальный датчик, использующий показания акселерометра.
  • TYPE_MAGNETIC_FIELD — Датчик магнитного поля, определяющий текущие показатели магнитного поля в микротеслах по трём осям.
  • TYPE_ORIENTATION — Датчик ориентации. Измеряет повороты, наклоны и вращение устройства. Считается устаревшим
  • TYPE_PRESSURE — Датчик атмосферного давления (барометр), возвращающий текущее давление в миллибарах. Можно определять высоту над уровнем моря, путём сравнения атмосферного давления в двух точках. Также барометры могут применяться для прогнозирования погоды.
  • TYPE_PROXIMITY — Датчик приближённости, который сигнализирует о расстоянии между устройством и целевым объектом в сантиметрах. Каким образом выбирается объект и какие расстояния поддерживаются, зависит от аппаратной реализации данного датчика, возможно возвращение двух значений — Близко и Далеко. Типичное его применение — обнаружение расстояния между устройством и ухом пользователя для автоматического регулирования яркости экрана или выполнения голосовой команды.
  • TYPE_RELATIVE_HUMIDITY — Датчик относительной влажности в виде процентного значения (API 14)
  • TYPE_ROTATION_VECTOR — Возвращает положение устройства в пространстве в виде угла относительно оси. Виртуальный датчик, берущий показания от акселерометра и гироскопа. Также может использовать показания датчика магнитного поля
  • TYPE_GEOMAGNETIC_ROTATION_VECTOR — альтернатива TYPE_ROTATION_VECTOR. Меньшая точность, но меньший расход батареи. Появился в Android 4.4 (API 19)
  • TYPE_POSE_6DOF — ещё одна альтернатива TYPE_ROTATION_VECTOR. Появился в Android 7.0 (API 24)
  • TYPE_SIGNIFICANT_MOTION — Появился в Android 4.3 (API 18)
  • TYPE_MOTION_DETECT — детектор движения. Появился в Android 7.0 (API 24)
  • TYPE_STATIONARY_DETECT — Появился в Android 7.0 (API 24)
  • TYPE_STEP_COUNTER — датчик для подсчёта количества шагов
  • TYPE_STEP_DETECTOR — определение начала шагов
  • TYPE_HEART_BEAT — пульс. Появился в Android 7.0 (API 24)
  • TYPE_HEART_RATE — сердечная активность. Появился в Android 4.4 (API 20)
  • TYPE_LOW_LATENCY_OFFBODY_DETECT — Появился в Android 8.0 (API 26)

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

Чтобы получить доступ к сенсорам, нужно вызвать метод getSystemService().

 // Kotlin private lateinit var sensorManager: SensorManager sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager 
// Java private SensorManager sensorManager; sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);

Устройство может включать в себя несколько реализаций одного и того же типа датчиков. Чтобы найти реализацию, используемую по умолчанию, вызовите метод getDefaultSensor() из объекта SensorManager, передавая ему в качестве параметра тип датчика в виде одной из констант, описанных выше.

Следующий фрагмент кода вернёт объект, описывающий гироскоп по умолчанию. Если для данного типа не существует датчика по умолчанию, будет возвращено значение null.

 // Kotlin sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) // лучше использовать вариант с проверкой if (sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) != null) < // Успешно! У нас есть гироскоп >else < // Неудачно! Гироскоп не обнаружен > 
// Java Sensor defaultGyroscope = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);

Таблица значений, возвращаемых датчиками

Тип датчика Количество значений Содержание значений Примечание
TYPE_ACCELEROMETER 3 value[0]: ось X (поперечная)
value[1]: ось Y (продольная)
value[2]: ось Z (вертикальная)
Ускорение (м/с 2 ) по трём осям.
Константы SensorManager.GRAVITY_*
TYPE_GRAVITY 3 value[0]: ось X (поперечная)
value[1]: ось Y (продольная)
value[2]: ось Z (вертикальная)
Сила тяжести (м/с 2 ) по трём осям.
Константы SensorManager.GRAVITY_*
TYPE_RELATIVE_HUMIDITY 1 value[0]:относительная влажность Относительная влажность в процентах (%)
TYPE_LINEAR_ACCELERATION 3 value[0]: ось X (поперечная)
value[1]: ось Y (продольная)
value[2]: ось Z (вертикальная)
Линейное ускорение (м/с 2 ) по трём осям без учёта силы тяжести
TYPE_GYROSCOPE 3 value[0]: ось X
value[1]:ось Y
value[2]: ось Z
Скорость вращения (рад/с) по трём осям
TYPE_ROTATION_VECTOR 4 values[0]: x*sin(q/2)
values[1]: y*sin(q/2)
values[2]: z*sin(q/2)
values[3]: cos(q/2)
Положение устройства в пространстве.
Описывается в виде угла поворота относительно оси в градусах
TYPE_MAGNETIC_FIELD 3 value[0]: ось X (поперечная)
value[1]: ось Y (продольная)
value[2]: ось Z (вертикальная)
Внешнее магнитное поле (мкТл)
TYPE_LIGHT 1 value[0]: освещённость Внешняя освещённость (лк).
Константы SensorManager.LIGHT_*
TYPE_PRESSURE 1 value[0]: атм.давление Атмосферное давление (мбар)
TYPE_PROXIMITY 1 value[0]: расстояние Расстояние до цели
TYPE_AMBIENT_TEMPERATURE 1 value[0]: температура Температура воздуха в градусах по Цельсию
TYPE_POSE_6DOF 15 см. документацию
TYPE_STATIONARY_DETECT 1 value[0] 5 секунд неподвижен
TYPE_MOTION_DETECT 1 value[0] В движении за последние 5 секунд
TYPE_HEART_BEAT 1 value[0]

Огласите весь список, пожалуйста!

У класса SensorManager есть метод getSensorList(), позволяющий получить список доступных датчиков на устройстве через константу Sensor.TYPE_ALL и метод getName():

 // Kotlin // Если этот код работает, его написал Александр Климов, // а если нет, то не знаю, кто его писал. package ru.alexanderklimov.sensors import android.hardware.Sensor import android.hardware.SensorManager import android.os.Bundle import androidx.appcompat.app.AppCompatActivity class MainActivity : AppCompatActivity() < private lateinit var sensorManager: SensorManager override fun onCreate(savedInstanceState: Bundle?) < super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager val deviceSensors: List= sensorManager.getSensorList(Sensor.TYPE_ALL) println(deviceSensors.joinToString("\n")) // Выводятся данные в таком формате // > > 
// Java package ru.alexanderklimov.sensors; import java.util.ArrayList; import java.util.List; import android.app.ListActivity; import android.content.Context; import android.hardware.Sensor; import android.hardware.SensorManager; import android.os.Bundle; import android.widget.ArrayAdapter; public class SensorsActivity extends ListActivity < @Override protected void onCreate(Bundle savedInstanceState) < super.onCreate(savedInstanceState); SensorManager sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE); ListdeviceSensors = sensorManager.getSensorList(Sensor.TYPE_ALL); List listSensorType = new ArrayList<>(); for (int i = 0; i < deviceSensors.size(); i++) < listSensorType.add(deviceSensors.get(i).getName()); >setListAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, listSensorType)); getListView().setTextFilterEnabled(true); > >

Так как у каждого устройства свой набор датчиков, то результаты будут у всех разными. Ниже приведены скриншоты с эмулятора и с реального устройства. В первом случае вывелось только 5 датчиков, во-втором было гораздо больше — вы видите только ту часть, которая поместилась на экране.

Список датчиков на эмулятореСписок датчиков на телефоне

Также можно получить список доступных датчиков конкретного типа. В следующем фрагменте кода будут возвращены объекты Sensor, представляющие собой все доступные датчики давления:

 // Kotlin val pressureSensors: List = sensorManager.getSensorList(Sensor.TYPE_PRESSURE) 
// Java List pressureSensors = sensorManager.getSensorList(Sensor.TYPE_PRESSURE);

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

 // Kotlin if (sensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY) != null) < val gravitySensors: List= sensorManager.getSensorList(Sensor.TYPE_GRAVITY) // Используем конкретный датчик от производителя и нужной версии sensor = gravitySensors.firstOrNull < it.vendor.contains("Qualcomm") && it.version == 1 >println(sensor?.vendor) > if (sensor == null) < // Используем акселерометр sensor = if (sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) != null) < sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) >else < // Акселеромент не обнаружен! null >> 

Интерфейс SensorEventListener — отслеживаем показания

Также вам понадобится интерфейс android.hardware.SensorListener. Интерфейс реализован с помощью класса, который используется для ввода значений датчиков по мере их изменения в режиме реального времени. Приложение реализует этот интерфейс для мониторинга одного или нескольких имеющихся аппаратных датчиков.

Интерфейс включает в себя два необходимых метода:

  • Метод onSensorChanged(int sensor, float values[]) вызывается всякий раз, когда изменяется значение датчика. Этот метод вызывается только для датчиков, контролируемых данным приложением. В число аргументов метода входит целое, которое указывает, что значение датчика изменилось, и массив значений с плавающей запятой, отражающих собственно значение датчика. Некоторые датчики выдают только одно значение данных, тогда как другие предоставляют три значения с плавающей запятой. Датчики ориентации и акселерометр дают по три значения данных каждый.
  • Метод onAccuracyChanged(int sensor,int accuracy) вызывается при изменении точности показаний датчика. Аргументами служат два целых числа: одно указывает датчик, а другое соответствует новому значению точности этого датчика.

Служба датчиков вызывает onSensorChanged() каждый раз при изменении значений. Все датчики возвращают массив значений с плавающей точкой. Размер массива зависит от особенностей датчика. Датчик TYPE_TEMPERATURE возвращает одно значение — температуру в градусах Цельсия, другие могут возвращать несколько значений. Вы можете использовать только нужные значения. Например, для получения сведений только о магнитном азимуте достаточно использовать первое числов, возвращаемое датчиком TYPE_ORIENTATION.

Параметр accuracy, используемый в методах для представления степени точности датчика, использует одну из констант

  • SensorManager.SENSOR_STATUS_ACCURACY_LOW. Говорит о том, что данные, предоставляемые датчиком, имеют низкую точность и нуждаются в калибровке.
  • SensorManager.SENSOR_STATUS_ACCURACY_MEDIUM. Говорит о средней степени точности датчика и том, что калибровка может улучшить результат.
  • SensorManager.SENSOR_STATUS_ACCURACY_HIGH. Показатели датчика точны настолько, насколько это возможно.
  • SensorManager.SENSOR_STATUS_UNRELIABLE. Данные, предоставляемые датчиком, недостоверны. Это значит, что датчик необходимо откалибровать, иначе невозможно считывать результаты.

Чтобы получать события, генерируемые датчиками, зарегистрируйте свою реализацию интерфейса SensorEventListener с помощью SensorManager. Укажите объект Sensor, за которым вы хотите наблюдать, и частоту, с которой вам необходимо получать обновления.

После получения объекта вы вызываете метод registerListener() в методе onResume(), чтобы начать получать обновлённые данные, и вызываете unregisteredListener() в методе onPause(), чтобы остановить получение данных. В этом случае датчики будут использоваться только тогда, когда активность видна на экране.

В следующем примере показан процесс регистрации SensorEventListener для датчика приближенности по умолчанию с указанием стандартной частоты обновления:

 Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY); sensorManager.registerListener(mySensorEventListener, sensor, SensorManager.SENSOR_DELAY_NORMAL); 

Класс SensorManager содержит следующие константы для выбора подходящей частоты обновлений (в порядке убывания):

  • SensorManager.SENSOR_DELAY_FASTEST — самая высокая возможная частота обновления показаний датчиков;
  • SensorManager.SENSOR_DELAY_GAME — частота, используемая для управления играми;
  • SensorManager.SENSOR_DELAY_NORMAL — частота обновлений по умолчанию;
  • SensorManager.SENSOR_DELAY_UI — частота для обновления пользовательского интерфейса.

Выбранная вами частота необязательно будет соблюдаться. SensorManager может возвращать результаты быстрее или медленней, чем вы указали (хотя, как правило, это происходит быстрее). Чтобы минимизировать расход ресурсов при использовании датчиков в приложении, необходимо пытаться подбирать наиболее низкую частоту.

Динамические датчики

В Android 7.0 Nougat (API 24) появилось понятие динамических датчиков, рассчитанных на платформу Android Things. Датчики могут присоединяться и отсоединяться от платы в любое время.

Для определения доступных динамических датчиков используются методы isDynamicSensorDiscoverySupported(), isDynamicSensor(), getDynamicSensorList().

Момент присоединения или отсоединения датчика от платы можно отслеживать через класс SensorManager.DynamicSensorCallback.

Напоследок стоит упомянуть, что последние версии Android не позволяют получать данные с датчиков в фоне.

Работа с датчиками в Android, или сервис для записи показаний с акселерометра

Год назад, на хабре публиковалась статья «Собираем показания датчиков с Android смартфона», где рассматривался способ получения данных с акселерометра (кстати говоря, есть пост более старый, в котором рассказывается все то же самое). Недавно передо мной была поставлена похожая задача. Необходимо было создать приложение (решил назвать его «Sensor Logger»), записывающее показания с акселерометра в файл в фоновом режиме. В данной статье постараюсь показать, как можно использовать сервисы и намерения, как работать с текстовыми файлами, а также каким образом отправлять данные из сервиса в Activity.
Рассказывать о снятии показаний с датчиков, классах SensorEventListener и SensorManager не вижу особого смысла, т.к. привел выше две статьи, в которых подробно об этом говорится.

Предполагается, что для читателя данный проект будет являться учебным, поскольку для меня он именно таким и являлся. До текущего момента я никогда не писал на Java и не работал с Android SDK.

Как оказалось, не все так просто

В приведенных выше статьях рассматривались примеры, когда показания датчиков снимались в главном Activity. Они оказались лишь отчасти применимыми к моей задаче. Ибо есть ложка дегтя: жизненный цикл Activity. Очевидно, что не удастся записывать в файл данные в фоновом режиме, если этим будет заниматься класс Activity, поэтому, прочитав ряд статей, было решено написать сервис, который и будет заниматься записью показаний датчиков.

Про сервисы в Android

В Android существует класс Service, который позволяет выполнять задачи приложения в фоновом режиме (стоит отметить, что сервис и Activity работают в одном потоке) в то время, когда телефон заблокирован, или приложение свернуто. Сервис не требует наличия UI, но он может передавать информацию в Activity (например для отображения) или другие сервисы.

Запуск сервиса, получение показаний с акселерометра, запись в файл

Рассмотрим процесс запуска сервиса (метод onStartCommand )

@SuppressLint("DefaultLocale") public class SensorLoggerService extends Service implements SensorEventListener < private SensorManager sm; private BufferedOutputStream outStream; private OutputStreamWriter sWriter; private String appDirString; private Calendar cal; private Long startTime; private String date_format = "yyyy-MM-dd_HH-mm-ss"; private Boolean first = true; @Override public int onStartCommand(Intent intent, int flags, int startId) < sm = (SensorManager) getSystemService(SENSOR_SERVICE); appDirString = intent.getStringExtra(MainActivity.APP_DIR); rec_start(); return super.onStartCommand(intent, flags, startId); >public void rec_start() < try < String filename = currentDateToString()+".txt"; File appDir = new File(appDirString); if(!appDir.exists()) appDir.mkdirs(); File file = new File(appDir, filename); outStream = new BufferedOutputStream(new FileOutputStream(file)); Toast.makeText(getApplicationContext(), appDirString+filename,Toast.LENGTH_SHORT).show(); sWriter = new OutputStreamWriter(outStream); Toast.makeText(getApplicationContext(), getString(R.string.record_started),Toast.LENGTH_SHORT).show(); sm.registerListener(this,sm.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), sm.SENSOR_DELAY_NORMAL); >catch (Throwable t1) < Toast.makeText(getApplicationContext(), "Exception: " + t1.toString(), Toast.LENGTH_LONG).show(); >> 

При запуске сервиса происходит инициализация объекта типа SensorManager (переменная sm ). Данный класс в Android предназначен для работы с датчиками (об этом можно почитать в приведенных статьях во введении, а также в документации).

На следующем этапе сервис получает имя директории (с помощью Intent — намерения), в которую необходимо записывать файлы с показаниями (в случае не существования таковой, программа создаст необходимую папку) и открывает для записи файл, именем которого являются дата и время запуска сервиса. За генерацию имени файла отвечает метод currentDateToString , возвращающий дату и время в виде строки.

@SuppressLint("SimpleDateFormat") public String currentDateToString()

После открытия файла начинаем снимать показания с датчиков.

При изменении состояния какого-либо из датчиков вызывается метод onSensorChanged , внутри которого идет проверка на то, на каком датчике произошло событие, и если это акселерометр, то показания, которые пришли, записываются в файл и оправляются в Activity приложения (создается новое намерение и вызывается функция sendBroadcast ):

 @Override public void onSensorChanged(SensorEvent event) < if(event.sensor.getType()==Sensor.TYPE_ACCELEROMETER) writeData(event.values); >@SuppressLint("DefaultLocale") public void writeData(float values[]) < try< cal = Calendar.getInstance(); if(first) < startTime = cal.getTimeInMillis(); first = false; >Long currentTime = cal.getTimeInMillis()-startTime; String data = Long.toString(currentTime)+String.format(" %f %f %f\n", values[0],values[1],values[2]); sWriter.write(data); Intent intent = new Intent(MainActivity.BROADCAST_ACTION); intent.putExtra(MainActivity.VAL1, values[0]); intent.putExtra(MainActivity.VAL2, values[1]); intent.putExtra(MainActivity.VAL3, values[2]); sendBroadcast(intent); > catch (Throwable t1) < Toast.makeText(getApplicationContext(), "Exception: " + t1.toString(), Toast.LENGTH_SHORT) .show(); >> 

В Activity прием сообщений от сервиса реализован при помощи класса BroadcastReciever, который принимает намерения, отправленные с помощью функции sendBroadcast . Рассмотрим метод rec_start из класса Activity:

 public final static String BROADCAST_ACTION = "SensorLoggerServiceRecieve"; public void rec_start() < Intent startServiceIntent = new Intent(this,SensorLoggerService.class) .putExtra(APP_DIR, appDirString); startService(startServiceIntent); rec=true; button_rec_off.setEnabled(true); button_rec_on.setEnabled(false); intentFlt = new IntentFilter(BROADCAST_ACTION); br = new BroadcastReceiver() < @Override public void onReceive(Context context, Intent intent) < float val1 = intent.getFloatExtra(VAL1, 0); float val2 = intent.getFloatExtra(VAL2, 0); float val3 = intent.getFloatExtra(VAL3, 0); x_label.setText("X: "+String.valueOf(val1)); y_label.setText("Y: "+String.valueOf(val2)); z_label.setText("Z: "+String.valueOf(val3)); >>; registerReceiver(br, intentFlt); > 

Объекты button_rec_off, button_rec_on являются объектами класса Button , а x_label, y_label и z_label — TextView .
В данном методе происходит запуск сервиса, инициализация BroadcastReciever и создание фильтра намерений (класс IntentFilter).

Стоит отметить, что в моем коде также предусмотрены методы для остановки процесса записи. Исходный код и *.apk можно скачать в репозитарии. Ссылка ниже.

Вместо заключения
  • Проект startandroid.ru — огромное количество примеров кода с объяснениями
  • Книга Рето Майера — в книге все объясняется доступно и понятно
  • Документация по ServiceManager

Создание приложения, использующего мобильные датчики

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

Для этого вы создадите образец приложения с двумя экранами. На первом экране отображается текущее направление по компасу, широта, долгота и высота, а также можно сканировать теги NFC для получения текстовой информации. На втором экране будет показано, как отображать данные акселерометра для определения угла устройства по осям X и Y.

Чтобы настроить сценарий первого экрана, представьте, что вы создаете приложение для охоты на мусор на основе NFC. Каждый участник будет использовать приложение для сканирования тега NFC, который предоставит ему направление по компасу, широту и долготу, которые укажут им следующий пункт назначения. В этом пункте назначения участник сканирует другой тег и повторяет процесс, пока не достигнет конца охоты.

Что касается второго экрана, представьте, что вам нужен инструмент для приблизительного измерения углов. На втором экране участник сможет разместить или удерживать свое устройство на поверхности и получить углы по осям X и Y устройства, а также просмотреть визуальное представление этих углов.

Посмотрите это видео, чтобы узнать, как создать приложение, использующее мобильные датчики:

Предварительные условия

  • Любой уровень лицензии Power Apps можно использовать для этого приложения, так как подключения для передачи данных не используются.
  • Потребуется мобильное устройство, такое как мобильный телефон или планшет с функциями GPS и акселерометра, поскольку большинство настольных компьютером могут не оснащаться необходимыми датчиками.
  • Устройство с поддержкой NFC необходимо для функции сканирования NFC. Также предполагается, что сканируемые теги NFC были предварительно настроены для создания текстовых значений в следующем примере формата:
"Heading: 80 degrees 
Latitude: 44.4604788
Longitude: -110.82813759"

Добавьте заголовок и текст HTML для выходных данных датчиков устройства

Метка заголовка

Когда приложение открыто для редактирования в Power Apps добавьте Текстовая метка на экран, перетащив ее из панели Вставка. Разместите ее в верхнем левом углу экрана и измените следующие свойства на панели свойств:

Свойство Стоимость
Текст «Охота на мусор»
Размер шрифта 24
Насыщенность шрифта FontWeight.Semibold
Выравнивание текста Align.Center
Ширина Parent.Width

Затем в области Дополнительно измените следующие свойства:

Свойство Стоимость
Цвет RGBA(255, 255, 255, 1)
Заливка RGBA(56, 96, 178, 1)

Это обеспечит заголовок для экрана.

HTML-текст для выходных данных датчиков устройства

Затем добавьте элемент управления HTML-текст. Это будет использоваться для отображения всех выходных данных датчиков устройства в одном месте. Используйте этот код в свойстве HtmlText:

"Current Location 

Compass Heading: " & Round(Compass.Heading, 2) & Char(0176) & "

Lat: " & Location.Latitude & "
Long: " & Location.Longitude & "
Alt: " & Round(Location.Altitude, 2) & " m"

Затем в области Дополнительно измените следующие свойства:

Свойство Стоимость
Размер 21
BorderStyle BorderStyle.Solid
BorderThickness 2
Ширина 560
Высота 576
PaddingTop 16
PaddingRight 16
PaddingBottom 16
PaddingLeft 16

Если вы посмотрите, что было помещено в HtmlText, вы можете заметить, что используются два типа датчиков.

Во-первых, это датчик Компас, который использует свойство Heading, чтобы предоставить направление по компасу с устройства.

Во-вторых, это датчик Местонахождения, который обеспечивает Широта, Долгота, а также Высота. Есть два соображения относительно местоположения. Во-первых, если вы используете только местоположение без оператора-точки и свойства, вы получите запись с полями для широты, долготы и высоты. Во-вторых, свойство Altitude по умолчанию указывается в метрах. Если вы хотите преобразовать его в футы, замените следующую формулу:

Round(Location.Altitude, 2) & " m" 
Round(Location.Altitude \* 3.2808, 2) & " ft" 

Многие свойства датчика могут работать некорректно, если вы предварительно просматриваете приложение внутри Power Apps Studio. Для работы со свойствами датчика для тестирования предпочтительнее использовать мобильный телефон.

Теперь разместите элемент управления HTML-текст в нижней части экрана.

Кнопка добавления для сканирования NFC и элемент управления HTML-текст

Кнопка сканирования NFC

Добавьте кнопку в приложение, расположив ее под элементом управления HTML-текст, который вы добавили на последнем шаге, и измените следующие свойства на панели Дополнительно:

Свойство Стоимость
OnSelect ClearCollect(colNFCScan, ReadNFC())
Текст «Сканировать тег NFC»

Код в свойстве OnSelect использует функцию ReadNFC(), которая активирует средство чтения NFC. Затем оно сохраняет то, что считывается из тега NFC, в коллекции под названием colNFCScan. В этой коллекции будет четыре поля: RTD, Текст, TNF, а также URI. Хотя полное объяснение этих полей выходит за рамки данной статьи, некоторые пояснения могут быть полезны. TNFФормат имени типа и используется для определения структуры Определение типа записей (RTD), который, в свою очередь, определяет тип записи, содержащейся в полях Текст и/или URI. URIуниверсальный код ресурса, который по сути является адресом ресурса. Для тегов NFC, используемых в этом примере, в дополнение к полю Текст, содержащему пример текста из введения этой темы, они будут содержать значение TNF 1, значение RTD Т и пустое значение URI.

Добавить элемент управления HTML-текст для отображения информации о сканировании NFC

Добавьте второй элемент управления HTML-текст и используйте следующую формулу в свойстве HTMLText:

"Next Destination 

" & First(colNFCScan).Text

Затем в области Дополнительно измените следующие свойства:

Свойство Стоимость
Размер 21
BorderStyle BorderStyle.Solid
BorderThickness 2
Ширина 560
Высота 248
PaddingTop 16
PaddingRight 16
PaddingBottom 16
PaddingLeft 16

Без каких-либо данных NFC элемент управления будет отображать Следующее место назначения. При сканировании тега NFC отображается Следующее место назначения, а затем данные из поля Текст в данных, собранных в результате сканирования.

Предполагая, что данные для тегов NFC настроены, как описано во введении, пользователь увидит следующие значения компаса, значения широты и долготы для следующего места охоты на мусор после сканирования одного из этих тегов.

Значения направления по компасу, широты и долготы

Тестирование приложения

Сохраните и опубликуйте приложение. На мобильном устройстве с необходимыми датчиками откройте приложение и наблюдайте за направлением по компасу, значениями широты, долготы и высоты. Попробуйте ходить, поворачиваясь в разных направлениях, чтобы увидеть изменения в различных показаниях датчиков.

Попробуйте ходить, поворачиваясь в разные стороны

Нажмите кнопку Сканировать тег NFC, чтобы показать элемент управления средства чтения NFC. Если у вас есть тег NFC, который может создавать значение Текст, отсканируйте тег, чтобы увидеть текст в приложении. Если нет, выберите Отмена.

Сканировать тег NFC

Добавление и настройка второго экрана

Добавьте новый Пустой экран для части приложения для измерения углов.

После добавления экрана вернитесь к первому экрану и добавьте значок Следующая стрелка из + Вставить > Значки > выберите значок Следующая стрелка. Разместите в верхнем правом углу экрана и измените следующие свойства на панели Дополнительно:

Свойство Стоимость
OnSelect Navigate(Screen2)
Цвет RGBA(255, 255, 255, 1)

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

Просмотрите приложение, выбрав только что добавленный значок, чтобы убедиться, что он переместит вас на только что добавленный пустой экран.

Добавить конфигурацию для углов X и Y

Добавьте Текстовая метка на экран, перетащив ее из панели Вставка. Разместите ее в верхнем левом углу экрана и измените следующие свойства на панели Свойства:

Свойство Стоимость
Текст «Уровень оси 2»
Размер шрифта 24
Насыщенность шрифта FontWeight.Semibold
Выравнивание текста Align.Center
Ширина Parent.Width

Затем в области Дополнительно измените следующие свойства:

Свойство Стоимость
Цвет RGBA(255, 255, 255, 1)
Заливка RGBA(56, 96, 178, 1)

Это обеспечит заголовок для второго экрана.

Затем добавьте значок Стрелка назад из + Вставить > Значки > выберите значок Стрелка назад. Разместите в верхнем левом углу экрана и измените следующие свойства на панели Дополнительно:

Свойство Стоимость
OnSelect Navigate(Screen1)
Цвет RGBA(255, 255, 255, 1)

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

Наконец, добавьте элемент управления HTML-текст. Это будет использоваться для отображения всех выходных данных датчиков устройства в одном месте. Используйте следующую формулу в свойстве HTML-текст:

"Angles: 

X: " & Abs(Round(Acceleration.X * (90 / 9.81), 0)) & Char(0176) & " Y: " & Abs(Round(Acceleration.Y * (90 / 9.81), 0)) & Char(0176) & "
"

Затем на вкладке Дополнительно измените следующие свойства:

Свойство Стоимость
Размер 21
BorderStyle BorderStyle.Solid
BorderThickness 2
Ширина 560
Высота 168
PaddingTop 16
PaddingRight 16
PaddingBottom 16
PaddingLeft 16

Разместите этот элемент управления в верхней части экрана приложения.

Если вы посмотрите, что было помещено в HtmlText, вы можете заметить, что там используется датчик Ускорение со свойствами X и Y. Также есть свойство Z, которое мы не используем в этом сценарии.

Этот датчик определяет гравитационные силы на устройстве в трех плоскостях. На примере мобильного телефона представьте себе линию, выходящую из боковых сторон телефона. Это была бы плоскость X. Поднятие правой стороны телефона даст положительное значение, а поднятие левой стороны даст отрицательное значение. Линия, идущая сверху и снизу телефона, будет обозначать плоскость Y. Поднятие верхней части телефона даст положительное значение, а поднятие нижней части даст отрицательное значение. Наконец, линия, идущая из экрана и назад в телефон, будет обозначать плоскость Z. Если повернуть телефон экраном вверх, будет получено положительное значение, а при экраном вниз — отрицательное.

Акселерометр будет измерять силы, действующие на устройство как в движении, например, если вы уронили устройство, так и в состоянии покоя, например, при наклоне устройства по одной или нескольким осям, описанным ранее. В состоянии покоя теоретические значения должны находиться в диапазоне от 0 до +/- 9,81 м/с2, где 0 означает, что относительная ось параллельна Земле без гравитации, действующей на датчик, а значение 9,81 указывает на то, что относительная ось перпендикулярна Земле, и на датчик действует полная сила тяжести.

Глядя на код в элементе управления HTML-текст, который только что добавлен, обратите внимание на следующий раздел:

Abs(Round(Acceleration.X * (90 / 9.81), 0)) 

В этой формуле, изнутри наружу, во-первых, Ускорение по оси X умножается на (90 / 9,81). При этом используется теоретическое максимальное значение в состоянии покоя для преобразования показаний датчика в градусы. Значение 90 присутствует, потому что при максимальном теоретическом значении устройство было бы перпендикулярно Земле в этой плоскости, давая 90 градусов.

Затем это значение в градусах округляется до нуля десятичных знаков, чтобы получить целое число в градусах. Наконец, вычисляется абсолютное значение этого округленного числа, чтобы получить положительное значение. При этом не имеет значения, измеряется ли угол с той или иной стороны.

Приведенные выше значения являются приблизительными и не представляют собой точного измерения.

Добавьте визуальные элементы уровня

На этом этапе мы собираемся использовать некоторые элементы управления нетрадиционными способами для достижения визуальной цели.

Визуализация в виде круглого пузырькового уровня

Для начала добавьте элемент управления Кнопка на экран, переименовав его в btnCircleLevel, и измените следующие свойства на панели Дополнительно:

Свойство Стоимость
Текст «»
BorderColor RGBA(56, 96, 178, 1)
FocusedBorderThickness 2
Заливка Прозрачный
DisabledFill Self.Fill
X (Parent.Width — Self.Width) / 2
Y (Parent.Height — Self.Height) / 2
Ширина 400
Высота Self.Width

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

Затем добавьте фигуру Круг, установите для нее радиус как 400 и измените следующие свойства в области Дополнительно:

Свойство Стоимость
FocusedBorderThickness 0
X (Parent.Width — Self.Width) / 2 + (Round(Acceleration.X / 9.81 * 90, 0) / 90 * btnCircleLevel.Width / 2)
Y (Parent.Height — Self.Height) / 2 — (Round(Acceleration.Y / 9.81 * 90, 0) / 90 * btnCircleLevel.Width / 2)
Ширина 42
Высота Self.Width
Ширина 400
Высота Self.Width

Свойства X и Y позволяют фигуре Круг перемещаться по центру экрана приложения, относительно изменять значения датчика Ускорение, но в круглой области btnCircleLevel.

Визуальные эффекты пузырькового уровня по осям X и Y

Добавьте элемент управления Ползунок в приложение, переименовав его в sldXValue, и измените следующие свойства на панели Дополнительно:

Свойство Стоимость
Мин -90
Макс 90
По умолчанию. Round(Acceleration.X * (90 / 9.81), 0)
ValueFill Self.RailFill
X (Parent.Width — Self.Width) / 2
Y btnCircleLevel.Y + btnCircleLevel.Height + 30
Ширина btnCircleLevel.Width
Высота 70

Этот элемент управления Ползунок отобразит угол, аналогичный тому, как это делает пузырьковый уровень: Метка будет двигаться к поднятой стороне устройства, как воздушный пузырь в пузырьковом уровне.

Далее скопируйте btnCircleLevel, выбрав его, нажав Ctrl+C, а потом Ctrl+V. Переименуйте элемент управления в btnXValueOverlay и измените следующие свойства на панели Дополнительно:

Свойство Стоимость
X sldXValue.X — sldXValue.HandleSize / 2
Y sldXValue.Y
Ширина sldXValue.Width + sldXValue.HandleSize
Высота sldXValue.Height

Эти изменения разместят его над элементом управления sldXValue, не позволяя пользователю изменять его значение и обеспечивая визуальную границу.

Скопируйте и вставьте sldXValue, используя тот же метод, что и для копирования btnCircleLevel. Переименуйте его в sldYValue и измените следующие свойства:

Свойство Стоимость
Компоновка Layout.Vertical
X btnCircleLevel.X — Self.Width — 30
Y (Parent.Height — Self.Height) / 2
Ширина sldXValue.Height
Высота sldXValue.Width

Скопируйте и вставьте btnXValueOverlay, переименовав его в btnYValueOverlay и изменив следующие свойства:

Свойство Стоимость
X sldYValue.X
Y sldYValue.Y — sldYValue.HandleSize / 2
Ширина sldYValue.Width
Высота sldYValue.Height + sldYValue.HandleSize

На этом завершается создание визуальных элементов, похожих на пузырьковый уровень.

Протестируйте приложение уровня

Тестирование приложения

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

См. также

  • Сигналы ускорения, приложения, компаса, подключения и местоположения в Power Apps
  • Элементы управления смешанной реальности в Power Apps
  • Элементы управления geospatial в Power Apps

Обратная связь

Coming soon: Throughout 2024 we will be phasing out GitHub Issues as the feedback mechanism for content and replacing it with a new feedback system. For more information see: https://aka.ms/ContentUserFeedback.

Отправить и просмотреть отзыв по

Собираем показания датчиков с Android смартфона. Работа над ошибками

В своем предыдущем посте я рассказал, как получить углы наклона аппарата во всех трех плоскостях. Однако, как оказалось, метод, использованный в топике является deprecated начиная с API Level 8 (Android 2.2). Исправлю эту ошибку и расскажу, как правильно получать данные под катом.

Сначала чуть-чуть теории

В документации по Android нам предлагают вместо SENSOR_ORIENTATION использовать метод

getOrientation (float[] R, float[] values) 
  • R — RotationMatrix или матрица поворота устройства;
  • values — массив из трех элементов типа float, в который запишутся углы наклона аппарата в радианах;
getRotationMatrix (float[] R, float[] I, float[] gravity, float[] geomagnetic) 

Данный метод в первые два массива помещает матрицу поворота и матрицу отклонения аппарата от (магнитного полюса Земли ?). Для этого ей необходимо также передать данные с датчика акселерометра и геомагнитного датчика. К счастью TYPE_ACCELEROMETER и TYPE_MAGNETIC_FIELD не являются deprecated. Следовательно, с них мы и будем снимать показания.

Переходим к практике

Layout мы можем взять тот же, что был в прошлом примере. Вот он:

В главной активити объявим переменные:

 private final SensorManager msensorManager; //Менеджер сенсоров аппрата private float[] rotationMatrix; //Матрица поворота private float[] accelData; //Данные с акселерометра private float[] magnetData; //Данные геомагнитного датчика private float[] OrientationData; //Матрица положения в пространстве private TextView xyView; private TextView xzView; private TextView zyView; 

Метод onCreate должен реализовывать методы класса SensorEventListener, поэтому его объявление изменится:

 public class Main extends Activity implements SensorEventListener 

И добавятся два обязательных метода onAccuracyChanged и onSensorChanged.
А перед setContentView пишем:

 msensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE); rotationMatrix = new float[16]; accelData = new float[3]; magnetData = new float[3]; OrientationData = new float[3]; xyView = (TextView) findViewById(R.id.xyValue); // xzView = (TextView) findViewById(R.id.xzValue); // Наши текстовые поля для вывода показаний zyView = (TextView) findViewById(R.id.zyValue); // 

Если читали мой прошлый пост, то ничего нового пока не увидели. Если же нет, то Вам нужно знать, что в первой строчке мы получаем объект менеджера датчиков.
Теперь создадим метод onResume, в котором уточним, данные каких датчиков нам необходимы:

 @Override protected void onResume()

Здесь мы передаем в метод registerListener тип нужного нам датчика (полный список обозначений датчиков) и частоту обновления данных (может быть SENSOR_DELAY_NORMAL, SENSOR_DELAY_UI, SENSOR_DELAY_GAME, or SENSOR_DELAY_FASTEST в порядке увеличения частоты).

Чтобы наша программа не съедала ресурсы смартфона будучи свернутой по событию onPause «снимаем» получение данных с датчиков:

 @Override protected void onPause()

Давайте создадим новый метод, в котором данные с датчиков будем заносить в соответствующий датчику массив. Назовем метод loadNewSensorData:

 private void loadNewSensorData(SensorEvent event) < final int type = event.sensor.getType(); //Определяем тип датчика if (type == Sensor.TYPE_ACCELEROMETER) < //Если акселерометр accelData = event.values.clone(); >if (type == Sensor.TYPE_MAGNETIC_FIELD) < //Если геомагнитный датчик magnetData = event.values.clone(); >> 

Ну вот, почти и все! Осталось только написать обработчик события onSensorChanged:

 public void onSensorChanged(SensorEvent event) < loadNewSensorData(event); // Получаем данные с датчика SensorManager.getRotationMatrix(rotationMatrix, null, accelData, magnetData); //Получаем матрицу поворота SensorManager.getOrientation(rotationMatrix, OrientationData); //Получаем данные ориентации устройства в пространстве if((xyView==null)||(xzView==null)||(zyView==null))< //Без этого работать отказалось. xyView = (TextView) findViewById(R.id.xyValue); xzView = (TextView) findViewById(R.id.xzValue); zyView = (TextView) findViewById(R.id.zyValue); >//Выводим результат xyView.setText(String.valueOf(Math.round(Math.toDegrees(OrientationData[0])))); xzView.setText(String.valueOf(Math.round(Math.toDegrees(OrientationData[1])))); zyView.setText(String.valueOf(Math.round(Math.toDegrees(OrientationData[2])))); > 

Все готово! Теперь я исправил свою ошибку и научил вас получать данные с датчиков не deprecated способом. 🙂

В заключении приведу ссылки на исходники, на готовый apk и на источник информации.

P.S.: Вы наверняка заметили лишнюю проверку переменных на отличие от null в последнем листинге. Без нее компилятор выдает java.lang.NullPointerException. Буду признателен, если кто-нибудь объяснит или укажет на ошибку.

UPD: Проблема решилась благодаря firexel.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *