Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .idea/material_theme_project_new.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 34 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
}
Expand All @@ -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);
}
Expand Down Expand Up @@ -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)
Expand Down
81 changes: 62 additions & 19 deletions ios/Classes/FlutterRotationSensorPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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
}

Expand Down
1 change: 1 addition & 0 deletions lib/flutter_rotation_sensor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
22 changes: 22 additions & 0 deletions lib/src/reference_frame.dart
Original file line number Diff line number Diff line change
@@ -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;
}
13 changes: 13 additions & 0 deletions lib/src/rotation_sensor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
}
12 changes: 12 additions & 0 deletions lib/src/rotation_sensor_method_channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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) {
Expand All @@ -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,
});
}
}
20 changes: 20 additions & 0 deletions lib/src/rotation_sensor_platform.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
}
}
15 changes: 15 additions & 0 deletions test/rotation_sensor_method_channel_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,20 @@ 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) {
case 'getOrientationStream':
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);
Expand Down Expand Up @@ -69,4 +73,15 @@ void main() {
expect(await platform.orientationStream.first, isA<OrientationEvent>());
},
);

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<OrientationEvent>());
},
);
}
12 changes: 12 additions & 0 deletions test/rotation_sensor_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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));
});
}