Измерение ускорения в WinRT

103

Несомненно, в своей внутренней реализации класс SimpleOrientationSensor обращается к устройству, называемому акселерометром. Акселерометр измеряет ускорение, и на первый взгляд кажется, что от информации об ускорении компьютера мало пользы. Однако из школьного курса физики (а конкретнее из второго закона Ньютона) нам знакома формула:

Сила равна произведению массы на ускорение. Любой предмет находится под воздействием силы тяжести. Чаще всего акселерометр компьютера измеряет силу тяжести и отвечает на основной вопрос: «Где верх, а где низ?»

С акселерометром можно работать и непосредственно через класс Accelerometer. Для получения экземпляра класса Accelerometer используется статический метод с таким же именем, как в классе SimpleOrientationSensor:

Accelerometer accelerometer = Accelerometer.GetDefault();

Если метод Accelerometer.GetDefault возвращает null, значит, компьютер не оснащен акселерометром или Windows 8 не знает о его существовании. Если ваше приложение не может нормально работать без акселерометра, известите пользователя о его отсутствии.

В любой момент времени можно получить текущее значение акселерометра:

AccelerometerReading accReading = accelerometer.GetCurrentReading();

Аналогичный метод класса SimpleOrientationSensor называется GetCurrentOrientation. Вероятно, полученное от GetCurrentReading значение стоит проверить на null. Класс AccelerometerReading определяет четыре свойства:

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

Также можно связать с объектом Accelerometer обработчик события:

accelerometer.ReadingChanged += accelerometer_ReadingChanged;

Аналогичное событие SimpleOrientationSensor называется OrientationChanged. Как и OrientationChanged, обработчик ReadingChanged выполняется в отдельном потоке, поэтому, скорее всего, вы будете обрабатывать его примерно так:

private async void  accelerometer_ReadingChanged(Accelerometer sender,
            AccelerometerReadingChangedEventArgs e)
{
    await this.Dispatcher.RunAsync(
        CoreDispatcherPriority.Normal, () =>
        {
            // ...
        });
}

Класс AccelerometerReadingChangedEventArgs определяет свойство с именем Reading типа AccelerometerReading — таким же, как у объекта, возвращаемого GetCurrentReading.

Насколько часто следует ожидать вызова обработчика ReadingChanged? Если компьютер находится в состоянии покоя, он может вообще не вызываться! По этой причине, если вам необходимо знать исходные показания акселерометра, вызовите GetCurrentReading в самом начале.

Если компьютер перемещается и изменяет свою ориентацию в пространстве, обработчик ReadingChanged вызывается при изменении значения (с некоторыми критериями), но не чаще интервала в миллисекундах, хранящегося в свойстве ReportInterval класса Accelerometer. У меня значение по умолчанию равно 112; следовательно, метод ReadingChanged вызывается примерно девять раз в секунду.

При желании можно задать ReportInterval другое значение, но оно должно быть не меньше значения, возвращаемого свойством MinimumReportInterval, которое на моем компьютере равно 16 миллисекундам (около 60 раз в секунду). Задайте ReportInterval значение MinimumReportInterval, чтобы получить максимальный объем данных; задайте свойство ReportInterval равным нулю, чтобы вернуться к значению по умолчанию.

Остальные классы датчиков из Windows.Devices.Sensors используют такой же программный интерфейс, как Accelerometer. Все они содержат следующие члены:

Только класс SimpleOrientationSensor отличается от других. Если компьютер неподвижен, свойства AccelerationX, AccelerationY и AccelerationZ класса AccelerometerReading определяют вектор, направленный к центру Земли. В записи векторов координаты обычно выделяются жирным шрифтом: (x, y, z), чтобы вектор отличался от точки (x, y, z) в трехмерном пространстве. Точка определяет конкретное место; вектор определяет направление и модуль. Конечно, векторы и точки связаны: направление вектора (х, у, z) является направлением из точки (0,0,0) в точку (x, y, z) а модуль вектора равен длине этого отрезка. Однако вектор не является таким отрезком и не связывается с определенным местом.

Модуль вектора вычисляется по трехмерной форме теоремы Пифагора:

Любой трехмерный вектор существует в конкретной трехмерной системе координат, и вектор, полученный от объекта AccelerometerReading, не является исключением. Для планшета с основной альбомной ориентацией эта система координат определяется оборудованием устройства:

Система координат на устройстве с альбомной ориентацией

Обратите внимание: координаты Y увеличиваются снизу вверх — это направление обратно тому, которое обычно используется при работе с двумерной графикой. Положительное направление Z обращено от экрана к пользователю. Эта система координат часто называется «правосторонней»; если направить указательный палец правой руки в направлении положительных значений X, а средний — в направлении положительных значений Y, то большой палец будет обращен к положительным значениям по оси Z. Или, если согнуть пальцы правой руки в направлении поворота положительных значений оси X к положительным значениям оси F, большой палец будет указывать в направлении положительных значений Z. Эта схема подходит для любой пары осей в порядке X, Y. Z: например, если согнуть пальцы правой руки для поворота от положительных значений оси Y к положительным значениям оси Z, то большой палец будет обращен к положительным значениям оси X.

У устройств с основной книжной ориентацией с точки зрения пользователя используется такая же система координат:

Система координат на устройстве с книжной ориентацией

Такая система координат жестко привязана к оборудованию устройства, а вектор Accelerometer обращен к центру Земли относительно этой системы. Например, когда планшет держится вертикально в основной ориентации, вектор ускорения обращен в направлении - Y. Модуль вектора приблизительно равен 1, а сам вектор незначительно отклоняется от (0,-1,0). Когда устройство лежит на плоской поверхности (например, на столе) экраном вверх, вектор находится в окрестности (0,0,-1).

Модуль 1 означает, что вектор измеряется в единицах g - ускорения свободного падения, обусловленного воздействием силы тяжести на поверхности Земли (9,81 м/с2). На Луне модуль вектора будет равен приблизительно 0,17. Отправьте свой планшет в состояние свободного падения (если не жалко), и модуль вектора ускорения упадет до нуля, пока устройство не коснется земли.

Следующая программа AccelerometerAndSimpleOrientation выводит данные, полученные от классов Accelerometer и SimpleOrientationSensor. Файл XAML содержит набор элементов TextBlock для меток и данных из файла фонового кода:

<Page ...>

    <Page.Resources>
        <Style TargetType="TextBlock">
            <Setter Property="Margin" Value="24 12 24 12" />
            <Setter Property="FontSize" Value="22" />
            
        </Style>
    </Page.Resources>

    <Grid Background="#FF1D1D1D">

        <Grid HorizontalAlignment="Center"
              VerticalAlignment="Center">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>

            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>

            <TextBlock Text="Accelerometer X:" />
            <TextBlock Grid.Row="1" Text="Accelerometer Y:" />
            <TextBlock Grid.Row="2" Text="Accelerometer Z:" />
            <TextBlock Grid.Row="3" Text="Magnitude:"
                       Margin="24 24" />
            <TextBlock Grid.Row="4" Grid.Column="0" Text="Simple Orientation:" />

            <TextBlock Grid.Row="0" Grid.Column="1" Name="accelerometerX"
                       TextAlignment="Right" />
            <TextBlock Grid.Row="1" Grid.Column="1" Name="accelerometerY"
                       TextAlignment="Right"/>
            <TextBlock Grid.Row="2" Grid.Column="1" Name="accelerometerZ"
                       TextAlignment="Right"/>
            <TextBlock Grid.Row="3" Grid.Column="1" Name="magnitude"
                       TextAlignment="Right"
                       VerticalAlignment="Center" />
            <TextBlock Grid.Row="4" Grid.Column="1" Name="simpleOrientation"
                       TextAlignment="Right" />
        </Grid>
    </Grid>
</Page>

Файл фонового кода немного улучшен по сравнению с предыдущим. Если создать экземпляр Accelerometer или SimpleOrientationSensor не удается, программа сообщает об этом пользователю. Также нежелательно, чтобы акселерометр работал в то время, когда он не используется программой, потому что он разряжает батарею. Программа назначает обработчики в переопределении OnNavigatedTo и отсоединяет их в OnNavigatedFrom. В остальном структура кода мало отличается от предыдущей программы:

using System;
using Windows.UI.Xaml.Controls;
using Windows.Devices.Sensors;
using Windows.UI.Core;
using Windows.Graphics.Display;
using Windows.UI.Xaml;
using Windows.UI.Popups;
using Windows.UI.Xaml.Navigation;

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        Accelerometer accelerometer = Accelerometer.GetDefault();
        SimpleOrientationSensor simpleOrientationSensor = SimpleOrientationSensor.GetDefault();

        public MainPage()
        {
            this.InitializeComponent();
            this.Loaded += OnMainPageLoaded;
        }

        private async void OnMainPageLoaded(object sender, RoutedEventArgs args)
        {
            if (accelerometer == null)
                await new MessageDialog("Не удается запустить акселерометр").ShowAsync();

            if (simpleOrientationSensor == null)
                await new MessageDialog("Не удается запустить датчик ориентации").ShowAsync();
        }

        // Назначение обработчиков событий
        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            if (accelerometer != null)
            {
                SetAccelerometerText(accelerometer.GetCurrentReading());
                accelerometer.ReadingChanged += OnAccelerometerReadingChanged;
            }

            if (simpleOrientationSensor != null)
            {
                SetSimpleOrientationText(simpleOrientationSensor.GetCurrentOrientation());
                simpleOrientationSensor.OrientationChanged += OnSimpleOrientationChanged;
            }

            base.OnNavigatedTo(e);
        }

        // Отсоединение обработчиков событий
        protected override void OnNavigatedFrom(NavigationEventArgs e)
        {
            if (accelerometer != null)
                accelerometer.ReadingChanged -= OnAccelerometerReadingChanged;

            if (simpleOrientationSensor != null)
                simpleOrientationSensor.OrientationChanged -= OnSimpleOrientationChanged;

            base.OnNavigatedFrom(e);
        }

        // Обработчик Accelerometer
        private async void OnAccelerometerReadingChanged(Accelerometer sender, 
            AccelerometerReadingChangedEventArgs e)
        {
            await this.Dispatcher.RunAsync(
                CoreDispatcherPriority.Normal, () =>
            {
                SetAccelerometerText(e.Reading);
            });
        }

        private void SetAccelerometerText(AccelerometerReading accelerometerReading)
        {
            if (accelerometerReading == null)
                return;

            accelerometerX.Text = accelerometerReading.AccelerationX.ToString("F2");
            accelerometerY.Text = accelerometerReading.AccelerationY.ToString("F2");
            accelerometerZ.Text = accelerometerReading.AccelerationZ.ToString("F2");

            magnitude.Text =
                Math.Sqrt(Math.Pow(accelerometerReading.AccelerationX, 2) +
                          Math.Pow(accelerometerReading.AccelerationY, 2) +
                          Math.Pow(accelerometerReading.AccelerationZ, 2)).ToString("F2");
        }

        // Обработчик SimpleOrientationSensor
        private async void OnSimpleOrientationChanged(SimpleOrientationSensor sender,
            SimpleOrientationSensorOrientationChangedEventArgs e)
        {
            await this.Dispatcher.RunAsync(
                CoreDispatcherPriority.Normal, () =>
            {
                SetSimpleOrientationText(e.Orientation);
            });
        }

        private void SetSimpleOrientationText(SimpleOrientation simpleOrientation)
        {
            this.simpleOrientation.Text = simpleOrientation.ToString();
        }
    }
}

Вот как выглядит программа на планшете:

Использование акселерометра и датчика ориентации

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

Обе координаты Y и Z отрицательны; это означает, что планшет немного наклонен назад. Как упоминалось ранее, при вертикальном расположении планшета вектор теоретически должен быть равен (0,-1,0), а если он лежит горизонтально экраном вверх — вектор теоретически равен (0,0,-1). Между этими двумя позициями планшет поворачивается вокруг оси X. Вызывая метод Math.Atan2 со значениями Y и Z, вы получите угол поворота.

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

Значения датчиков в зависимости от поворота устройства
SimpleOrientationSensor Значение Accelerometer
NotRotated ~(0,-1,0)
Rotated90DegreesCounterClockwise ~(-1,0,0)
Rotated180DegreesCounterClockwise ~(0,1,0)
Rotated270DegreesCounterClockwise ~(1,0,0)
Faceup ~(0,0,-1)
Facedown ~(0,0,1)

Знак «приблизительно» (~) следует интерпретировать весьма широко. Векторы Accelerometer достаточно заметно изменяются, прежде чем достигнуть значения, вызывающего изменение SimpleOrientationSensor.

Программа AccelerometerAndSimpleOrientation не определяет предпочтительных ориентаций, поэтому при перемещении планшета в пространстве Windows автоматически изменяет ориентацию экрана в предположении, что вы не собираетесь читать числа вверх ногами. Связь между значениями SimpleOrientationSensor и ориентацией экрана прослеживается, но только потому, что Windows изменяет ориентацию экрана в зависимости от этих значений! Если запретить Windows изменять ориентацию экрана (любыми средствами), это никак не повлияет на информацию, выводимую программой.

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

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

DisplayProperties.AutoRotationPreferences = DisplayProperties.NativeOrientation;

Если запустить программу AccelerometerAndSimpleOrientation на мобильном устройстве и быстро переместить его, направление и модуль вектора ускорения также изменяются; вектор уже не обозначает ускорение 1g, направленное к центру Земли. Например, если резко дернуть устройство влево, вектор ускорения будет направлен вправо — но только на время ускорения устройства. Если вам удастся выровнять и сохранить стабильную скорость перемещения, вектор ускорения «успокаивается» и снова начинает указывать к центру Земли. Остановите устройство, и изменение скорости будет отражено в векторе ускорения.

Одно из стандартных применений акселерометра — ватерпас (плотницкий уровень для проверки горизонтальных поверхностей). Файл XAML создает четыре экземпляра Ellipse. Три используются для рисования концентрических окружностей, а четвертый изображает пузырек:

<Page ...>

    <Grid Background="#FF1D1D1D">
        <Grid Name="centeredGrid" HorizontalAlignment="Center" VerticalAlignment="Center">
            <Ellipse Name="outerCircle"
                     Stroke="#FFF" />
            
            <Ellipse Name="halfCircle" Stroke="White" />
            <Ellipse Width="24" Height="24" Stroke="White" />
            <Ellipse Fill="Red" Width="24" Height="24"
                     HorizontalAlignment="Center" VerticalAlignment="Center">
                <Ellipse.RenderTransform>
                    <TranslateTransform x:Name="bubbleTranslate" />
                </Ellipse.RenderTransform>
            </Ellipse>
        </Grid>
    </Grid>
</Page>

Файл фонового кода задает свойству DisplayProperties.AutoRotationPreferences значение DisplayProperties.NativeOrientation. У Windows просто нет причин для автоматического изменения ориентации экрана. Программа также использует обработчик SizeChanged для задания размеров outerCircle и halfCircle:

using System;
using Windows.Devices.Sensors;
using Windows.Graphics.Display;
using Windows.UI.Core;
using Windows.UI.Popups;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        Accelerometer accelerometer = Accelerometer.GetDefault();

        public MainPage()
        {
            this.InitializeComponent();
            DisplayProperties.AutoRotationPreferences = DisplayProperties.NativeOrientation;
            Loaded += OnMainPageLoaded;
            SizeChanged += OnMainPageSizeChanged;
        }

        private async void OnMainPageLoaded(object sender, RoutedEventArgs e)
        {
            if (accelerometer != null)
            {
                accelerometer.ReportInterval = accelerometer.MinimumReportInterval;
                SetBubble(accelerometer.GetCurrentReading());
                accelerometer.ReadingChanged += OnAccelerometerReadingChanged;
            }
            else
            {
                await new MessageDialog("Не удается запустить акселерометр").ShowAsync();
            }
        }

        private void OnMainPageSizeChanged(object sender, SizeChangedEventArgs e)
        {
            double size = Math.Min(e.NewSize.Width, e.NewSize.Height);
            outerCircle.Width = size;
            outerCircle.Height = size;
            halfCircle.Width = size / 2;
            halfCircle.Height = size / 2;
        }

        private async void OnAccelerometerReadingChanged(Accelerometer sender, 
            AccelerometerReadingChangedEventArgs e)
        {
            await this.Dispatcher.RunAsync(
                CoreDispatcherPriority.Normal, () => 
            { 
                SetBubble(e.Reading); 
            });
        }

        private void SetBubble(AccelerometerReading accelerometerReading)
        {
            if (accelerometerReading == null)
                return;

            double x = accelerometerReading.AccelerationX;
            double y = accelerometerReading.AccelerationY;

            bubbleTranslate.X = -x * centeredGrid.ActualWidth / 2;
            bubbleTranslate.Y = y * centeredGrid.ActualHeight / 2;
        }
    }
}

Метод SetBubble выглядит подозрительно просто: он берет компоненты X и Y вектора ускорения и использует их для задания координат X и Y центрального пузырька с масштабированием по радиусу внешней окружности. Но представьте, что планшет лежит экраном вверх или вниз на столе. Координата Z вектора ускорения равна 1 или -1, а обе координаты X и Y равны нулю, то есть пузырек находится в центре экрана. Все верно.

Теперь поверните планшет так, чтобы экран располагался перпендикулярно Земле. Координата Z становится равной нулю. Это означает, что модуль вектора ускорения вычисляется исключительно по координатам X и Y:

Это уравнение описывает окружность на плоскости; следовательно, пузырек находится где-то на внешней окружности. Его конкретное местонахождение зависит от текущего угла поворота планшета относительно оси Z.

Вектор ускорения направлен вниз к центру Земли, а пузырьки поднимаются вверх; это означает, что для перехода к двумерным экранным координатам мы должны поменять знаки компонентов X и Y вектора ускорения. Но поскольку ось Y вектора ускорения уже инвертирована относительно экранных координат, поменять знак нужно только у компонента X (две последние строки программы).

Вот как выглядит программа на планшете Microsoft Surface:

Пример создания программы-ватерпаса

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

Пройди тесты
Лучший чат для C# программистов