Здравствуйте! Данная статья является второй частью большой статьи, посвященной использованию датчиков устройства под управлением Sailfish OS. Первая часть доступна тут.
Датчик угла наклона, как можно понять из названия, определяет угол наклона устройства. Ситуация с компонентами для измерение наклона аналогична датчикам света: есть один более “умный” компонент — RotationSensor, и его менее продвинутый товарищ — OrientationSensor.
RotationReading предоставляет информацию о точном угле наклона относительно трех координатных осей: в свойстве x содержится информация о том, насколько продольная ось смартфона отклонилась от горизонтального положения, свойство y содержит ту же информацию, но уже для поперечной оси, а z — о повороте относительно центра экрана. Более доходчиво это можно проследить на изображении:
Понаблюдать за этими значениями можно в импровизированной таблице на экране устройства:
Определение значений угла z доступно не всегда. Чтобы определить доступность этого значения, нужно проверить свойство hasZ компонента RotationReading.
OrientationSensor, как менее функциональный компонент, позволяет ответить на важный вопрос: «какая же грань смартфона направлена вверх?». Соответственно, он может принимать шесть значений, каждое из которых соотносится с гранью. Плюс одно дополнительное значение, сообщающее о невозможности определить положение. С данными значениями можно ознакомиться в следующем списке:
Применять OrientationSensor можно, к примеру, для того, чтобы сохранять ориентацию изображения при изменении положения самого устройства:
В приведенном выше примере логотип будет обращен верхом к той грани смартфона, которая выше:
Магнитометр представлен QML-типом Magnetometer, он предоставляет информацию о напряженности магнитного поля вокруг устройства по осям X, Y, и Z. Значения предоставляются в теслах.
Магнитометр может реагировать как на напряженность любых магнитных полей вокруг устройства, так и на напряженность только геомагнитного поля. Данное поведение регулируется свойством returnGeoValues, которое может принимать либо true, либо false. Изначально оно имеет значений false и реагирует на любые магнитные поля поблизости. Разница между двумя этими режимами в том, что при включенном returnGeoValues датчик будет стараться исключить влияние посторонних магнитных полей, исходящих от источников магнитного поля, вроде проводников, магнитов и электронных устройств, и фокусироваться на натуральном магнитном поле Земли. Разумеется, полность исключить влияние посторонних магнитных полей не удастся, так что показания датчика все равно будут меняться при появлении новых источников.
Объект MagnetometerReading имеет помимо уже привычных свойств x, y, и z, которые содержат напряженность по осям, еще и свойство calibrationLevel — уровень точности при измерении напряженность геомагнитного поля. Данный параметр приимает значений от 0 до 1 в соответствии с уровнем калибровки датчика и меняет свое значение только тогда, когда returnGeoValues содержит true. Свойство отражает, насколько сильно близкие магнитные поля сказываются на измерении геомагнитного поля. При большой влиянии значение будет понижаться до нуля. Чтобы вернуть точность измерений необходимо провести калибровку датчика. Обычно это выполняется вращением устройства по дуге цифры 8 либо просто размахивая им в разные стороны в пространстве. При низком показателе точности значения датчика будут сильно отличаться от действительных. Особенно это сказывается при использовании магнитометра в качестве компаса.
Компонент Compass выступает в качестве электронного компаса. Его свойство reading содержит объект CompassReading, который дает сведения об азимуте устройства — угле между северным магнитным полюсом и осью X устройства по направлению хода часовой стрелки. Эта информация, как и показания магнитометра, основана на данных геомагнитного поля. В связи с этим, компас также восприимчив к различным помехам и также требует калибровки.
CompassReading имеет два свойства:
Показания некоторых датчиков могут изменяться множество раз в секунду. Использование стандартного механизма оповещения Qt с помощью сигналов и слотов ощутимо замедлит процесс получения значений и снизит точность измерений в целом. Альтернативой использования сигналов для получений значения датчика выступает класс QSensorFilter. Этот класс представляет из себя интерфейс, реализующий callback-функцию, вызов которой происходит при получении новых показаний подписанным классом-датчиком. Замечательной особенность этой функции является возможность изменять поступающие в нее измерения датчиков, преобразуя тем самым финальное значение свойства reading. А в совокупности с возможность навесить на один датчик несколько фильтров, все преобразования над значениями могут быть проведены посредством фильтров с получением на выходе уже желаемых результатов.
Аналогично другим классам-родителям из Qt Sensors API, QSensorFilter имеет лишь универсальный набор методов:
Методы, специфичные для конкретного типа датчика, реализуются уже у классов-наследников. Также, как и с объектами типа Reading, каждому датчику соответствует свой фильтр. К примеру, акселерометр, с которым соотносится класс QAccelerometer, имеет фильтр QAccelerometerFilter, а его метод filter принимает следующий вид:
В отличие от метода своего родителя, в качестве параметра он получает объект с измерениями акселерометра. Тоже самое относится и к остальным типам фильтров.
Управление фильтрами производится посредством методов класса-датчика. Для этого у QSensors реализован ряд функции, позволяющих распоряжаться набором обработчиков:
В отличие от остальных классов Qt Sensors API, фильтры изначально не имеют интерфейса для работы с ними из QML-кода. Кроме того, методы для управления набором фильтров конкретного датчика также не могут быть использованы прямиком из QML.
В качестве примера использования фильтра можно рассмотреть реализацию режимов акселерометра QAccelerometer::User и QAccelerometer::Gravity. Этот фильтр может оказаться полезен на тех устройствах, которые не поддерживают режимы работы акселерометра отличные от Combined. Для этого также понадобится привлечь датчик вращения — RotationSensor.
Перед тем, как приступить к реализации, необходимо вставить пару уточняющих слов о режимах работы акселерометра: ускорение вызванное гравитацией и ускорение вызванное движение девайса пользователем датчик физически не может различить, большинство устройств используют системы из нескольких датчиков и доступные всем сведения об ускорении свободного падения для расчета влияния только гравитации. А после эти сведения используются и для вычисления ускорения, вызванного пользователем. Этим подходом мы и воспользуемся для воплощения режимов Gravity и User
Первым делом необходимо завести класс, осуществляющий фильтрацию, который будет унаследован от QAccelerometerFilter. В нем сразу зададим значение величины G — ускорение свободного падения.
В классе необходимо завести два типа свойств: QRotationSensor — датчик вращения, его мы будем использовать для определения положения устройства в пространстве; QAccelerometer::AccelerationMode — режим измерения значения акселерометров, в соответствии с этим значением будет выбран требуемый способ расчета значений.
При вычислении проекций ускорения на координатные оси потребуется часто прибегать к тригонометрическим операциям вроде взятия синуса и косинуса. Для этого будет использоваться стандартная библиотека C math.h и ее методы sin() и cos(). Эти методы принимают значения угла в радианах, в то время как датчик предоставляет значения в градусах. Поэтому, заводим небольшой метод, который будет преобразовывать значения угла из градусов в радианы:
Режим вычисления ускорения будет задаваться в конструкторе класса, там же мы инициализируем и активируем датчик наклона:
Самая важная часть — переопределяем метод filter(QAccelerometerReading *reading), в котором будут производится вычисления:
В операторе switch выделяем блоки, каждый из который будет отвечать за свой режим измерения ускорения, таких режимов два: Gravity и User; Combined можно пропустить, так как значения изначально идут в этом режиме. В блоках оператора switch идет пересчет значений ускорения для каждой из осей — значения записываются в свойства передаваемого аргумента reading. Для вычисления ускорения по осям X и Y достаточно просто умножить ускорение свободного падения на синус угла между смартфоном и соответствующей осью координатной плоскости, а для вычисления ускорения по оси Z потребуется применить матрицу преобразования. Реализуем это в отдельном методе:
Так как у нас не урок геометрии, думаю, нет необходимости объяснять, почему матрица выглядит именно так, однако интересующиеся всегда могут обратиться к Википедии.
Для режима Gravity вычисленные значения ускорения по осям просто записываются в соответствующие свойства объекта reading, так как в этом случае значения ускорения зависят только лишь от положения устройства. Для определения значений в режиме User абсолютные значения полученных вычислений надо вычесть из показаний акселерометра — так мы исключаем влияние гравитации на измерения датчика.
На этом реализация фильтра окончена, подключаем его к желаемому акселерометру и пользуемся двумя новыми режимами:
Эта и предыдущая статьи рассматривают основные датчики, доступные на операционной системе Sailfish OS. И хотя приведенные примеры являются совсем базовыми, они должны ознакомить читателя с возможностями Qt Sensors API и показать потенциал в применении плагина при разработке приложений.
Технические вопросы можно также обсудить на канале русскоязычного сообщества Sailfish OS в Telegram или группе ВКонтакте.
Автор: Максим Костерин
Датчик угла наклона
Датчик угла наклона, как можно понять из названия, определяет угол наклона устройства. Ситуация с компонентами для измерение наклона аналогична датчикам света: есть один более “умный” компонент — RotationSensor, и его менее продвинутый товарищ — OrientationSensor.
RotationReading предоставляет информацию о точном угле наклона относительно трех координатных осей: в свойстве x содержится информация о том, насколько продольная ось смартфона отклонилась от горизонтального положения, свойство y содержит ту же информацию, но уже для поперечной оси, а z — о повороте относительно центра экрана. Более доходчиво это можно проследить на изображении:
Понаблюдать за этими значениями можно в импровизированной таблице на экране устройства:
RotationSensor {
id: rotationSensor
active: true
}
Column {
Label { text: "x: " + rotationSensor.reading.x }
Label { text: "y: " + rotationSensor.reading.y }
Label { text: "z: " + rotationSensor.reading.z }
}
Определение значений угла z доступно не всегда. Чтобы определить доступность этого значения, нужно проверить свойство hasZ компонента RotationReading.
OrientationSensor, как менее функциональный компонент, позволяет ответить на важный вопрос: «какая же грань смартфона направлена вверх?». Соответственно, он может принимать шесть значений, каждое из которых соотносится с гранью. Плюс одно дополнительное значение, сообщающее о невозможности определить положение. С данными значениями можно ознакомиться в следующем списке:
- OrientationReading.Undefined — не удается определить расположение устройства,
- OrientationReading.TopUp — верхняя грань направлена вверх,
- OrientationReading.TopDown — нижняя грань направлена вверх (вверх ногами),
- OrientationReading.LeftUp — левая грань направлена вверх,
- OrientationReading.RightUp — правая грань направлена вверх,
- OrientationReading.FaceUp — устройство расположено экраном вверх,
- OrientationReading.FaceDown — устройство расположено экраном вниз.
Применять OrientationSensor можно, к примеру, для того, чтобы сохранять ориентацию изображения при изменении положения самого устройства:
OrientationSensor {
id: orientationSensor
active: true
}
Image {
anchors.centerIn: parent
source: "http://social.fruct.org/content/img/fruct.png"
transform: Rotation { angle: rotationAngle() }
}
function rotationAngle() {
switch (orientationSensor.reading.orientation) {
case OrientationReading.TopUp: return 0;
case OrientationReading.RightUp: return 90;
case OrientationReading.TopDown: return 180;
case OrientationReading.LeftUp: return 270;
default: return 0;
}
}
В приведенном выше примере логотип будет обращен верхом к той грани смартфона, которая выше:
Магнитометр
Магнитометр представлен QML-типом Magnetometer, он предоставляет информацию о напряженности магнитного поля вокруг устройства по осям X, Y, и Z. Значения предоставляются в теслах.
Магнитометр может реагировать как на напряженность любых магнитных полей вокруг устройства, так и на напряженность только геомагнитного поля. Данное поведение регулируется свойством returnGeoValues, которое может принимать либо true, либо false. Изначально оно имеет значений false и реагирует на любые магнитные поля поблизости. Разница между двумя этими режимами в том, что при включенном returnGeoValues датчик будет стараться исключить влияние посторонних магнитных полей, исходящих от источников магнитного поля, вроде проводников, магнитов и электронных устройств, и фокусироваться на натуральном магнитном поле Земли. Разумеется, полность исключить влияние посторонних магнитных полей не удастся, так что показания датчика все равно будут меняться при появлении новых источников.
Объект MagnetometerReading имеет помимо уже привычных свойств x, y, и z, которые содержат напряженность по осям, еще и свойство calibrationLevel — уровень точности при измерении напряженность геомагнитного поля. Данный параметр приимает значений от 0 до 1 в соответствии с уровнем калибровки датчика и меняет свое значение только тогда, когда returnGeoValues содержит true. Свойство отражает, насколько сильно близкие магнитные поля сказываются на измерении геомагнитного поля. При большой влиянии значение будет понижаться до нуля. Чтобы вернуть точность измерений необходимо провести калибровку датчика. Обычно это выполняется вращением устройства по дуге цифры 8 либо просто размахивая им в разные стороны в пространстве. При низком показателе точности значения датчика будут сильно отличаться от действительных. Особенно это сказывается при использовании магнитометра в качестве компаса.
Компас
Компонент Compass выступает в качестве электронного компаса. Его свойство reading содержит объект CompassReading, который дает сведения об азимуте устройства — угле между северным магнитным полюсом и осью X устройства по направлению хода часовой стрелки. Эта информация, как и показания магнитометра, основана на данных геомагнитного поля. В связи с этим, компас также восприимчив к различным помехам и также требует калибровки.
CompassReading имеет два свойства:
- azimuth: qreal — горизонтальный угол между северным магнитным полюсом Земли и направлением, в которое указывает верхняя грань смартфона,
- calibrationLevel: qreal — точность показаний датчика, полностью аналогично свойству calibrationLevel типа MagnetometerReading.
Фильтрация показаний датчиков
Показания некоторых датчиков могут изменяться множество раз в секунду. Использование стандартного механизма оповещения Qt с помощью сигналов и слотов ощутимо замедлит процесс получения значений и снизит точность измерений в целом. Альтернативой использования сигналов для получений значения датчика выступает класс QSensorFilter. Этот класс представляет из себя интерфейс, реализующий callback-функцию, вызов которой происходит при получении новых показаний подписанным классом-датчиком. Замечательной особенность этой функции является возможность изменять поступающие в нее измерения датчиков, преобразуя тем самым финальное значение свойства reading. А в совокупности с возможность навесить на один датчик несколько фильтров, все преобразования над значениями могут быть проведены посредством фильтров с получением на выходе уже желаемых результатов.
Аналогично другим классам-родителям из Qt Sensors API, QSensorFilter имеет лишь универсальный набор методов:
- public bool filter(QSensorReading *reading) — та самая callback-функция, вызов которой происходит при получении измерений с датчика. В ней можно производить предварительную обработку значений. Если эта функция возвращает true, то показания передаются либо следующему фильтру, если он есть, либо испускается сигнал об изменении значения свойства reading. При возвращении false обработка текущего показания датчика прекращается.
- protected void setSensor(QSensor *sensor) — позволяет указать класс-датчик, к которому прикрепляется фильтр, из самого фильтра.
Методы, специфичные для конкретного типа датчика, реализуются уже у классов-наследников. Также, как и с объектами типа Reading, каждому датчику соответствует свой фильтр. К примеру, акселерометр, с которым соотносится класс QAccelerometer, имеет фильтр QAccelerometerFilter, а его метод filter принимает следующий вид:
bool filter(QAccelerometerReading *reading)
В отличие от метода своего родителя, в качестве параметра он получает объект с измерениями акселерометра. Тоже самое относится и к остальным типам фильтров.
Управление фильтрами производится посредством методов класса-датчика. Для этого у QSensors реализован ряд функции, позволяющих распоряжаться набором обработчиков:
- void addFilter(QSensorFilter *filter) — добавляет указанный фильтр в конец списка. При этом один фильтр может входить в список несколько раз.
- void removeFilter(QSensorFilter *filter) — удаляет из списка первое найденное вхождение указанного фильтра.
- QList<QSensorFilter*> filters() const — список указателей на все фильтры, на которые подписан датчик. Вызов функций-обработчиков происходит в порядке возрастания их позиции в списке.
В отличие от остальных классов Qt Sensors API, фильтры изначально не имеют интерфейса для работы с ними из QML-кода. Кроме того, методы для управления набором фильтров конкретного датчика также не могут быть использованы прямиком из QML.
В качестве примера использования фильтра можно рассмотреть реализацию режимов акселерометра QAccelerometer::User и QAccelerometer::Gravity. Этот фильтр может оказаться полезен на тех устройствах, которые не поддерживают режимы работы акселерометра отличные от Combined. Для этого также понадобится привлечь датчик вращения — RotationSensor.
Перед тем, как приступить к реализации, необходимо вставить пару уточняющих слов о режимах работы акселерометра: ускорение вызванное гравитацией и ускорение вызванное движение девайса пользователем датчик физически не может различить, большинство устройств используют системы из нескольких датчиков и доступные всем сведения об ускорении свободного падения для расчета влияния только гравитации. А после эти сведения используются и для вычисления ускорения, вызванного пользователем. Этим подходом мы и воспользуемся для воплощения режимов Gravity и User
Первым делом необходимо завести класс, осуществляющий фильтрацию, который будет унаследован от QAccelerometerFilter. В нем сразу зададим значение величины G — ускорение свободного падения.
#define G 9.80665
#include <QtSensors>
#include <math.h>
class AccelerationModeFilter : public QAccelerometerFilter {
public:
AccelerationModeFilter();
bool filter(QAccelerometerReading *reading);
void setAccelerationMode(QAccelerometer::AccelerationMode accelerationMode);
QAccelerometer::AccelerationMode getAccelerationMode();
private:
QRotationSensor rotationSensor;
QAccelerometer::AccelerationMode accelerationMode;
};
В классе необходимо завести два типа свойств: QRotationSensor — датчик вращения, его мы будем использовать для определения положения устройства в пространстве; QAccelerometer::AccelerationMode — режим измерения значения акселерометров, в соответствии с этим значением будет выбран требуемый способ расчета значений.
При вычислении проекций ускорения на координатные оси потребуется часто прибегать к тригонометрическим операциям вроде взятия синуса и косинуса. Для этого будет использоваться стандартная библиотека C math.h и ее методы sin() и cos(). Эти методы принимают значения угла в радианах, в то время как датчик предоставляет значения в градусах. Поэтому, заводим небольшой метод, который будет преобразовывать значения угла из градусов в радианы:
qreal toRadian(qreal degree) {
return degree * M_PI / 180;
}
Режим вычисления ускорения будет задаваться в конструкторе класса, там же мы инициализируем и активируем датчик наклона:
AccelerationModeFilter::AccelerationModeFilter() : rotationSensor() {
rotationSensor.setActive(true);
}
Самая важная часть — переопределяем метод filter(QAccelerometerReading *reading), в котором будут производится вычисления:
bool AccelerationModeFilter::filter(QAccelerometerReading* reading) {
switch (accelerationMode) {
case QAccelerometer::Gravity:
reading->setX(G * sin(toRadian(rotationSensor.reading()->y())));
reading->setY(- G * sin(toRadian(rotationSensor.reading()->x())));
reading->setZ(- zAxisAcceleration(rotationSensor.reading()->x(), rotationSensor.reading()->y()));
break;
case QAccelerometer::User:
reading->setX(reading->x() - G * sin(toRadian(rotationSensor.reading()->y())));
reading->setY(reading->y() + G * sin(toRadian(rotationSensor.reading()->x())));
reading->setZ(reading->z() + zAxisAcceleration(rotationSensor.reading()->x(), rotationSensor.reading()->y()));
break;
}
return true;
}
void AccelerationModeFilter::setAccelerationMode(QAccelerometer::AccelerationMode accelerationMode) {
this->accelerationMode = accelerationMode;
}
QAccelerometer::AccelerationMode AccelerationModeFilter::getAccelerationMode() {
return accelerationMode;
}
В операторе switch выделяем блоки, каждый из который будет отвечать за свой режим измерения ускорения, таких режимов два: Gravity и User; Combined можно пропустить, так как значения изначально идут в этом режиме. В блоках оператора switch идет пересчет значений ускорения для каждой из осей — значения записываются в свойства передаваемого аргумента reading. Для вычисления ускорения по осям X и Y достаточно просто умножить ускорение свободного падения на синус угла между смартфоном и соответствующей осью координатной плоскости, а для вычисления ускорения по оси Z потребуется применить матрицу преобразования. Реализуем это в отдельном методе:
qreal zAxisAcceleration(qreal xDegree, qreal yDegree) {
qreal xRadian = toRadian(xDegree);
qreal yRadian = toRadian(yDegree);
double zCoordinates[3] = {0, 0, G};
double rotationValues[9] = {
-cos(xRadian), 0, sin(xRadian),
sin(yRadian) * sin(xRadian), cos(yRadian), -sin(yRadian) * cos(yRadian),
-cos(yRadian) * sin(xRadian), sin(yRadian), cos(yRadian) * cos(xRadian)
};
QGenericMatrix<1, 3, double> zVector(zCoordinates);
QGenericMatrix<3, 3, double> rotationMatrix(rotationValues);
return (rotationMatrix * zVector)(2, 0);
}
Так как у нас не урок геометрии, думаю, нет необходимости объяснять, почему матрица выглядит именно так, однако интересующиеся всегда могут обратиться к Википедии.
Для режима Gravity вычисленные значения ускорения по осям просто записываются в соответствующие свойства объекта reading, так как в этом случае значения ускорения зависят только лишь от положения устройства. Для определения значений в режиме User абсолютные значения полученных вычислений надо вычесть из показаний акселерометра — так мы исключаем влияние гравитации на измерения датчика.
На этом реализация фильтра окончена, подключаем его к желаемому акселерометру и пользуемся двумя новыми режимами:
#include <QtSensors>
#include "accelerometerfilterexample.h"
class AccelerometerWithFilter : public QAccelerometer
{
Q_OBJECT
public:
AccelerometerWithFilter() : filter() {
setActive(true);
addFilter(&filter);
connect(this, &AccelerometerWithFilter::accelerationModeChanged,
[=](AccelerationMode accelerationMode) {this->filter.setAccelerationMode(accelerationMode);});
}
private:
AccelerationModeFilter filter;
};
Заключение
Эта и предыдущая статьи рассматривают основные датчики, доступные на операционной системе Sailfish OS. И хотя приведенные примеры являются совсем базовыми, они должны ознакомить читателя с возможностями Qt Sensors API и показать потенциал в применении плагина при разработке приложений.
Технические вопросы можно также обсудить на канале русскоязычного сообщества Sailfish OS в Telegram или группе ВКонтакте.
Автор: Максим Костерин