diff --git a/.idea/material_theme_project_new.xml b/.idea/material_theme_project_new.xml
new file mode 100644
index 0000000..4e1fcc7
--- /dev/null
+++ b/.idea/material_theme_project_new.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 2742c22..22749ce 100644
--- a/README.md
+++ b/README.md
@@ -74,7 +74,9 @@ For more control, you can subscribe to the stream directly:
super.initState();
orientationSubscription = RotationSensor.orientationStream.listen((event) {
final azimuth = event.eulerAngles.azimuth;
- // Print azimuth: 0 for North, π/2 for East, π for South, -π/2 for West
+ // Print azimuth: 0 for North, π/2 for East, π for South, -π/2 for West.
+ // On iOS, an azimuth of 0 only points to north when a north-referenced
+ // reference frame is set (see the Reference Frame section below).
print(azimuth);
});
}
@@ -101,6 +103,9 @@ void initState() {
// Set the sampling period for the rotation sensor
RotationSensor.samplingPeriod = SensorInterval.uiInterval;
+ // Set the reference frame the azimuth is measured from
+ RotationSensor.referenceFrame = ReferenceFrame.magneticNorth;
+
// Set the coordinate system for the rotation sensor
RotationSensor.coordinateSystem = CoordinateSystem.transformed(Axis3.X, Axis3.Z);
}
@@ -128,6 +133,34 @@ void config() {
Events may arrive at a rate faster or slower than the sampling period, which is only a hint to the
system. The actual rate depends on the system's event queue and sensor hardware capabilities.
+### Reference Frame
+
+The [RotationSensor.referenceFrame](https://pub.dev/documentation/flutter_rotation_sensor/latest/flutter_rotation_sensor/RotationSensor/referenceFrame.html)
+property controls the world reference the azimuth is measured from. Whatever the value, the
+orientation is always returned in the plugin's east-north-up world convention, so an azimuth of 0
+means the device points north. Here are the values you can use:
+
+- `ReferenceFrame.device`: *(default value)* The platform default, with no guarantee of a north
+ reference. On iOS the horizontal reference is arbitrary (the direction the device happened to
+ point when the sensor started, no compass). On Android the rotation vector sensor is already
+ referenced to magnetic north.
+- `ReferenceFrame.magneticNorth`: The azimuth is referenced to magnetic north on both platforms,
+ without depending on location services.
+- `ReferenceFrame.trueNorth`: The azimuth is referenced to true (geographic) north. Requires
+ location services to be available.
+
+```dart
+void config() {
+ RotationSensor.referenceFrame = ReferenceFrame.magneticNorth;
+}
+```
+
+> [!NOTE]
+> Reference frame selection is currently implemented on iOS only. On Android the rotation vector is
+> always referenced to magnetic north, so `device` and `magneticNorth` already behave as expected,
+> while `trueNorth` currently behaves like `magneticNorth`. Android support for `trueNorth` is
+> tracked in a separate issue.
+
### Coordinate System
The [RotationSensor.coordinateSystem](https://pub.dev/documentation/flutter_rotation_sensor/latest/flutter_rotation_sensor/RotationSensor/coordinateSystem.html)
diff --git a/ios/Classes/FlutterRotationSensorPlugin.swift b/ios/Classes/FlutterRotationSensorPlugin.swift
index 4029fb3..95f328e 100644
--- a/ios/Classes/FlutterRotationSensorPlugin.swift
+++ b/ios/Classes/FlutterRotationSensorPlugin.swift
@@ -6,6 +6,14 @@ public class FlutterRotationSensorPlugin: NSObject, FlutterPlugin, FlutterStream
private var eventChannel: FlutterEventChannel
private let motionManager: CMMotionManager
+ private var referenceFrame: CMAttitudeReferenceFrame = .xArbitraryZVertical
+ private var eventSink: FlutterEventSink?
+
+ // CoreMotion expresses a north reference frame as (north, west, up), while
+ // this plugin's world convention is (east, north, up) like Android. The two
+ // differ by a fixed 90° rotation about the vertical axis, applied to the
+ // attitude quaternion so the azimuth stays 0 = north on every platform.
+ private let northToEastNorthUp = 0.7071067811865476 // sin(45°) = cos(45°)
public static func register(with registrar: FlutterPluginRegistrar) {
let methodChannel = FlutterMethodChannel(name: "rotation_sensor/method", binaryMessenger: registrar.messenger())
@@ -25,38 +33,73 @@ public class FlutterRotationSensorPlugin: NSObject, FlutterPlugin, FlutterStream
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "getOrientationStream":
- if let args = call.arguments as? [String: Any],
- let samplingPeriod = args["samplingPeriod"] as? Double {
- motionManager.deviceMotionUpdateInterval = samplingPeriod * 0.000001
+ let args = call.arguments as? [String: Any]
+ if let samplingPeriod = args?["samplingPeriod"] as? Double {
+ motionManager.deviceMotionUpdateInterval = samplingPeriod * 0.000001
}
+ updateReferenceFrame(args?["referenceFrame"] as? String)
result(nil)
default:
result(FlutterMethodNotImplemented)
}
}
- public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
- if motionManager.isDeviceMotionAvailable {
- motionManager.startDeviceMotionUpdates(to: OperationQueue()) { (motion, error) in
- guard let motion = motion, error == nil else {
- events(FlutterError(code: "UNAVAILABLE", message: "Device motion updates unavailable", details: nil))
- return
- }
+ private func updateReferenceFrame(_ name: String?) {
+ let newFrame = attitudeReferenceFrame(from: name)
+ guard newFrame != referenceFrame else { return }
+ referenceFrame = newFrame
+ if motionManager.isDeviceMotionActive, let sink = eventSink {
+ motionManager.stopDeviceMotionUpdates()
+ startUpdates(sink)
+ }
+ }
+
+ private func attitudeReferenceFrame(from name: String?) -> CMAttitudeReferenceFrame {
+ switch name {
+ case "magneticNorth": return .xMagneticNorthZVertical
+ case "trueNorth": return .xTrueNorthZVertical
+ default: return .xArbitraryZVertical
+ }
+ }
+
+ private func isNorthReferenced(_ frame: CMAttitudeReferenceFrame) -> Bool {
+ return frame == .xMagneticNorthZVertical || frame == .xTrueNorthZVertical
+ }
- let quaternion = motion.attitude.quaternion
- let rotationVector = [quaternion.x, quaternion.y, quaternion.z, quaternion.w, -1.0, Int64((motion.timestamp * 1000000000).rounded())]
- DispatchQueue.main.async {
- events(rotationVector)
- }
- }
- return nil
- } else {
- return FlutterError(code: "NO_SENSOR", message: "Rotation vector sensor unavailable", details: nil)
+ private func startUpdates(_ events: @escaping FlutterEventSink) {
+ let correctToEastNorthUp = isNorthReferenced(referenceFrame)
+ motionManager.startDeviceMotionUpdates(using: referenceFrame, to: OperationQueue()) { (motion, error) in
+ guard let motion = motion, error == nil else {
+ events(FlutterError(code: "UNAVAILABLE", message: "Device motion updates unavailable", details: nil))
+ return
}
+
+ let q = motion.attitude.quaternion
+ let k = self.northToEastNorthUp
+ let qx = correctToEastNorthUp ? k * (q.x - q.y) : q.x
+ let qy = correctToEastNorthUp ? k * (q.x + q.y) : q.y
+ let qz = correctToEastNorthUp ? k * (q.z + q.w) : q.z
+ let qw = correctToEastNorthUp ? k * (q.w - q.z) : q.w
+
+ let rotationVector = [qx, qy, qz, qw, -1.0, Int64((motion.timestamp * 1000000000).rounded())] as [Any]
+ DispatchQueue.main.async {
+ events(rotationVector)
+ }
+ }
+ }
+
+ public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
+ guard motionManager.isDeviceMotionAvailable else {
+ return FlutterError(code: "NO_SENSOR", message: "Rotation vector sensor unavailable", details: nil)
+ }
+ eventSink = events
+ startUpdates(events)
+ return nil
}
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
motionManager.stopDeviceMotionUpdates()
+ eventSink = nil
return nil
}
diff --git a/lib/flutter_rotation_sensor.dart b/lib/flutter_rotation_sensor.dart
index 816e0c5..0ad866f 100644
--- a/lib/flutter_rotation_sensor.dart
+++ b/lib/flutter_rotation_sensor.dart
@@ -6,5 +6,6 @@ export 'src/math/matrix3.dart';
export 'src/math/quaternion.dart';
export 'src/math/vector3.dart';
export 'src/orientation_event.dart';
+export 'src/reference_frame.dart';
export 'src/rotation_sensor.dart';
export 'src/sensor_interval.dart';
diff --git a/lib/src/reference_frame.dart b/lib/src/reference_frame.dart
new file mode 100644
index 0000000..a159663
--- /dev/null
+++ b/lib/src/reference_frame.dart
@@ -0,0 +1,22 @@
+/// The world reference frame the device orientation is expressed against.
+///
+/// This controls what the azimuth is measured from. Whatever the value, the
+/// orientation is always returned in the package's east-north-up world
+/// convention, so an azimuth of 0 means the device points north.
+enum ReferenceFrame {
+ /// The platform default, with no guarantee of a north reference.
+ ///
+ /// On iOS the horizontal reference is arbitrary (the direction the device
+ /// happened to point when the sensor started, no compass). On Android the
+ /// rotation vector sensor is already referenced to magnetic north.
+ device,
+
+ /// The azimuth is referenced to magnetic north on both platforms. No
+ /// dependency on location services.
+ magneticNorth,
+
+ /// The azimuth is referenced to true (geographic) north. Requires location
+ /// services to be available. On Android, where the rotation vector is only
+ /// magnetic, this currently behaves like [magneticNorth].
+ trueNorth;
+}
diff --git a/lib/src/rotation_sensor.dart b/lib/src/rotation_sensor.dart
index 0d0345c..37bcc84 100644
--- a/lib/src/rotation_sensor.dart
+++ b/lib/src/rotation_sensor.dart
@@ -2,6 +2,7 @@ import 'package:meta/meta.dart';
import 'coordinate_system.dart';
import 'orientation_event.dart';
+import 'reference_frame.dart';
import 'rotation_sensor_method_channel.dart';
import 'rotation_sensor_platform.dart';
import 'sensor_interval.dart';
@@ -44,4 +45,16 @@ class RotationSensor {
/// existing listeners will receive [OrientationEvent] in the new coordinate
/// system.
static CoordinateSystem coordinateSystem = DisplayCoordinateSystem();
+
+ /// The world [ReferenceFrame] the azimuth is measured from.
+ ///
+ /// Defaults to [ReferenceFrame.device]. Set it to
+ /// [ReferenceFrame.magneticNorth] or [ReferenceFrame.trueNorth] to get a real
+ /// compass heading (azimuth 0 = north). When changing this value, all
+ /// existing listeners will be affected.
+ static ReferenceFrame get referenceFrame =>
+ RotationSensorPlatform.instance.referenceFrame;
+
+ static set referenceFrame(ReferenceFrame value) =>
+ RotationSensorPlatform.instance.referenceFrame = value;
}
diff --git a/lib/src/rotation_sensor_method_channel.dart b/lib/src/rotation_sensor_method_channel.dart
index 173a446..b69a303 100644
--- a/lib/src/rotation_sensor_method_channel.dart
+++ b/lib/src/rotation_sensor_method_channel.dart
@@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
import 'environment.dart';
import 'math/quaternion.dart';
import 'orientation_event.dart';
+import 'reference_frame.dart';
import 'rotation_sensor.dart';
import 'rotation_sensor_platform.dart';
@@ -38,6 +39,7 @@ class RotationSensorMethodChannel extends RotationSensorPlatform {
}
methodChannel.invokeMethod('getOrientationStream', {
'samplingPeriod': samplingMicroseconds,
+ 'referenceFrame': referenceFrameValue.name,
});
final broadcastStream = eventChannel.receiveBroadcastStream();
return _orientationStream = broadcastStream.map((event) {
@@ -56,6 +58,16 @@ class RotationSensorMethodChannel extends RotationSensorPlatform {
void updateSamplingPeriod(int value) {
methodChannel.invokeMethod('getOrientationStream', {
'samplingPeriod': value,
+ 'referenceFrame': referenceFrameValue.name,
+ });
+ }
+
+ @override
+ @protected
+ void updateReferenceFrame(ReferenceFrame value) {
+ methodChannel.invokeMethod('getOrientationStream', {
+ 'samplingPeriod': samplingMicroseconds,
+ 'referenceFrame': value.name,
});
}
}
diff --git a/lib/src/rotation_sensor_platform.dart b/lib/src/rotation_sensor_platform.dart
index cbc208e..4267ff5 100644
--- a/lib/src/rotation_sensor_platform.dart
+++ b/lib/src/rotation_sensor_platform.dart
@@ -3,6 +3,7 @@ import 'package:meta/meta.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'orientation_event.dart';
+import 'reference_frame.dart';
import 'rotation_sensor_method_channel.dart';
import 'rotation_sensor_unsupported.dart';
import 'sensor_interval.dart';
@@ -79,4 +80,23 @@ abstract class RotationSensorPlatform extends PlatformInterface {
void updateSamplingPeriod(int value) {
// no-op
}
+
+ @protected
+ ReferenceFrame referenceFrameValue = ReferenceFrame.device;
+
+ /// The world [ReferenceFrame] the azimuth is measured from.
+ ///
+ /// Defaults to [ReferenceFrame.device]. When changing this value, all
+ /// existing listeners will be affected.
+ ReferenceFrame get referenceFrame => referenceFrameValue;
+
+ set referenceFrame(ReferenceFrame value) {
+ referenceFrameValue = value;
+ updateReferenceFrame(value);
+ }
+
+ @protected
+ void updateReferenceFrame(ReferenceFrame value) {
+ // no-op
+ }
}
diff --git a/test/rotation_sensor_method_channel_test.dart b/test/rotation_sensor_method_channel_test.dart
index 78cbe30..f3d2a32 100644
--- a/test/rotation_sensor_method_channel_test.dart
+++ b/test/rotation_sensor_method_channel_test.dart
@@ -9,9 +9,11 @@ void main() {
const methodChannel = RotationSensorMethodChannel.methodChannel;
const orientationChannel = RotationSensorMethodChannel.eventChannel;
late int expectedSamplingPeriod;
+ late ReferenceFrame expectedReferenceFrame;
setUp(() {
expectedSamplingPeriod = platform.samplingPeriod.inMicroseconds;
+ expectedReferenceFrame = platform.referenceFrame;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(methodChannel, (methodCall) async {
switch (methodCall.method) {
@@ -19,6 +21,8 @@ void main() {
final arguments = methodCall.arguments as Map;
final samplingPeriod = arguments['samplingPeriod'] as int;
expect(samplingPeriod, expectedSamplingPeriod);
+ final referenceFrame = arguments['referenceFrame'] as String;
+ expect(referenceFrame, expectedReferenceFrame.name);
return null;
default:
throw UnsupportedError(methodCall.method);
@@ -69,4 +73,15 @@ void main() {
expect(await platform.orientationStream.first, isA());
},
);
+
+ test(
+ 'orientationStream forwards the configured reference frame to the platform',
+ () async {
+ expectedReferenceFrame = ReferenceFrame.magneticNorth;
+ platform.referenceFrame = ReferenceFrame.magneticNorth;
+ expect(platform.referenceFrame, equals(ReferenceFrame.magneticNorth));
+ await Future.microtask(() => null);
+ expect(await platform.orientationStream.first, isA());
+ },
+ );
}
diff --git a/test/rotation_sensor_test.dart b/test/rotation_sensor_test.dart
index 310f5aa..3fb76c0 100644
--- a/test/rotation_sensor_test.dart
+++ b/test/rotation_sensor_test.dart
@@ -61,4 +61,16 @@ void main() {
RotationSensor.coordinateSystem = CoordinateSystem.display();
expect(RotationSensor.coordinateSystem, same(CoordinateSystem.display()));
});
+
+ test('referenceFrame defaults to device', () {
+ RotationSensorPlatform.instance = MockRotationSensorPlatform();
+ expect(RotationSensor.referenceFrame, equals(ReferenceFrame.device));
+ });
+
+ test('referenceFrame can be set and retrieved correctly', () {
+ RotationSensor.referenceFrame = ReferenceFrame.magneticNorth;
+ expect(RotationSensor.referenceFrame, equals(ReferenceFrame.magneticNorth));
+ RotationSensor.referenceFrame = ReferenceFrame.trueNorth;
+ expect(RotationSensor.referenceFrame, equals(ReferenceFrame.trueNorth));
+ });
}