plus_plugins icon indicating copy to clipboard operation
plus_plugins copied to clipboard

Magnetometer values are off

Open filippkowalski opened this issue 2 years ago • 12 comments

System info

Platform the Issue occurs on: iOS Plugin name: sensors_plus Plugin version: 1.3.0

Steps to Reproduce

It seems that Flutter's implementation of the Magnetometer is off when compared to iOS devices. I don't know yet if it's due to some additional flags that are set on the iOS side to calibrate the magnetometer ( https://stackoverflow.com/questions/28365121/how-to-get-magnetometer-data-using-swift ), but results for Flutter are way higher than for iOS devices. And actually sometimes when the Flutter app detects metals the values go down instead of going up.

You can easily test it out by downloading those 3 apps below and seeing the results by yourself.

iOS implementation https://apps.apple.com/us/app/stud-finder-wall-detector/id1530653768 https://apps.apple.com/us/app/teslameter/id976926346

Flutter implementation https://apps.apple.com/us/app/stud-finder/id1613688190

iOS

Flutter

You can see on the screenshots that iOS values are lower than those on Flutter. All of those screenshots were made in the same phone position, with the phone lying flat.

One note here: Look at the X, Y, Z values here, because the Flutter app has the main displayed wrong.

cc: @Zabadam because I've seen that you were implementing this feature

filippkowalski avatar Mar 11 '22 11:03 filippkowalski

I have an extremely limited knowledge of iOS and as such the testing for that platform was less than thorough.

If I take another look, it may be as simple as calling values from sensor manager incorrectly.

It seems Android has moved things around over the years, and iOS may be similar; whereby I could have been referencing irrelevant docs.

Edit: Thank you for the bug report!

Zabadam avatar Mar 11 '22 12:03 Zabadam

Possibly, I'll also take a look at what could be wrong here

filippkowalski avatar Mar 11 '22 15:03 filippkowalski

I have finally sat down to look into this.

It seems like it may be a simple fix. We are after this value, the calibrated magneticField property of a CMDeviceMotion object that "unlike the magneticField property of the CMMagnetometerData class . . . reflect the earth’s magnetic field plus surrounding fields, minus device bias."

EDIT: After analyzing the usage of the DeviceMotion sensor for the implementation of UserAcceleration--opposed to Acceleration implementation by raw accelerometer samples from startAccelerometerUpdatesToQueue or Gyroscope implementation via samples from startGyroUpdatesToQueue--I determined the solution is to utilize this DeviceMotion "sensor" for its calibrated magnetic field as well.

This oughta do it.

FLTMagnetometerStreamHandlerPlus implementation in FLTSensorsPlusPlugin.m @ L124 becomes

@implementation FLTMagnetometerStreamHandlerPlus

- (FlutterError*)onListenWithArguments:(id)arguments eventSink:(FlutterEventSink)eventSink {
  _initMotionManager();
  [_motionManager

      // startMagnetometerUpdatesToQueue:[[NSOperationQueue alloc] init]
      //                     withHandler:^(CMMagnetometerData* magData, NSError* error) {
      //                       CMMagneticField magneticField = magData.magneticField;

      startDeviceMotionUpdatesToQueue:[[NSOperationQueue alloc] init]
                          withHandler:^(CMDeviceMotion* motionData, NSError* error) {
                            CMCalibratedMagneticField field = motionData.magneticField;
                            sendTriplet(field.x, field.y, field.z, eventSink);
                          }];
  return nil;
}

- (FlutterError*)onCancelWithArguments:(id)arguments {
  // [_motionManager stopMagnetometerUpdates];

  [_motionManager stopDeviceMotionUpdates];
  return nil;
}

@end

Perhaps before I submit a PR you could do me a massive favor @filippkowalski and test this locally on your end. You would open your work environment with your app, then make these line changes locally in sensors_plus pubdev cache.

Zabadam avatar Mar 30 '22 03:03 Zabadam

Awesome! I'll check it out later. @Zabadam

Just FYI, in the meantime, I've used this library: https://pub.dev/packages/motion_sensors and it seems to have a proper implementation.

filippkowalski avatar Mar 30 '22 08:03 filippkowalski

Just FYI, in the meantime, I've used this library: https://pub.dev/packages/motion_sensors and it seems to have a proper implementation.

I remember when I was building a sensors based package I had to make the decision between that package and sensors_plus.

You can see now the decision that I ultimately made was to add the magnetometer support I needed to this official package instead of using motion_sensors as per its lack of web support.

EDIT: I am glad you brought this proper implementation to light, though, as it helped me realize three things in my proposed solution:

  1. usingReferenceFrame call to provide frame of reference
  2. CMMotionManager.showsDeviceMovementDisplay = true
  3. The calibrated field object CMCalibratedMagneticField does not have its own x, y and z values, but instead has an accuracy property and a standard CMMagneticField property
  • Access to these measurements then is through the seemingly superfluous CMDeviceMotion.magneticField.field

We'll get this done! Bear in mind I have never programmed in Objective-C nor owned an Apple product 😇

Zabadam avatar Mar 30 '22 14:03 Zabadam

Barring a check for availability of sensors and providing a means to alter temporal accuracy, this implementation should accomplish the same expected functionality as from motion_sensors:

@implementation FLTMagnetometerStreamHandlerPlus

- (FlutterError *)onListenWithArguments:(id)arguments
                              eventSink:(FlutterEventSink)eventSink {
  _initMotionManager();
  // Allow iOS to present calibration interaction if necessary
  _motionManager.showsDeviceMovementDisplay = YES; // not `true`, correct?
  [_motionManager
      startDeviceMotionUpdatesUsingReferenceFrame: CMAttitudeReferenceFrameXArbitraryCorrectedZVertical
                                          ToQueue:[[NSOperationQueue alloc] init]
                                      withHandler:^(CMDeviceMotion *motionData, NSError *error) {
                                        // The `magneticField` from CMDeviceMotion is of type
                                        // CMCalibratedMagneticField which has an `accuracy`
                                        // and a standard CMMagneticField `field`.
                                        CMMagneticField field = motionData.magneticField.field;
                                        sendTriplet(field.x, field.y, field.z, eventSink);
                                      }];
  return nil;
}

- (FlutterError *)onCancelWithArguments:(id)arguments {
  [_motionManager stopDeviceMotionUpdates];
  return nil;
}

@end

Beyond my lack of expertise, motion_sensor's implementation is with Swift meanwhile this is Obj-C, no? Took some adjusting. The Apple docs have a dropdown toggle, but lack code examples for Obj-C.

Zabadam avatar Mar 31 '22 01:03 Zabadam

I wish I had an iOS device to verify this fix. Could be checked by someone else if they have the time:

dependencies
  # Testing calibrated magnetometer on iOS
  sensors_plus:
    git:
      url: https://github.com/Zabadam/plus_plugins.git
      path: packages/sensors_plus/sensors_plus

Zabadam avatar Mar 31 '22 23:03 Zabadam

@Zabadam Thanks for your effort!

I just used this dependency and it seems that it's still showing higher values than the other apps or the motion_plus package.

filippkowalski avatar Apr 04 '22 11:04 filippkowalski

@Zabadam Thanks for your effort!

I just used this dependency and it seems that it's still showing higher values than the other apps or the motion_plus package.

In doing so, between removing sensors_plus: 1.3.0 and adding sensors_plus: 1.4.0 via git you ran a flutter clean just to be sure the cache was cleared?

Because the shared section of code is nearly identical (albeit Swift instead of ObjC), it seems the magnetometer implementation here may not be the source of issue after all.

    private let motionManager = CMMotionManager()
    private let queue = OperationQueue()
    
    func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
        if motionManager.isDeviceMotionAvailable {
            motionManager.showsDeviceMovementDisplay = true
            motionManager.startDeviceMotionUpdates(using: CMAttitudeReferenceFrame.xArbitraryCorrectedZVertical, to: queue) { (data, error) in
                if data != nil {
                    events([data!.magneticField.field.x, data!.magneticField.field.y, data!.magneticField.field.z])
                }
            }
        }
        return nil
    }

motion_sensors

versus

- (FlutterError *)onListenWithArguments:(id)arguments eventSink:(FlutterEventSink)eventSink {
  _initMotionManager();
  _motionManager.showsDeviceMovementDisplay = YES;
  [_motionManager
      startDeviceMotionUpdatesUsingReferenceFrame: CMAttitudeReferenceFrameXArbitraryCorrectedZVertical
                                          ToQueue:[[NSOperationQueue alloc] init]
                                      withHandler:^(CMDeviceMotion *motionData, NSError *error) {
                                        CMMagneticField field = motionData.magneticField.field;
                                        sendTriplet(field.x, field.y, field.z, eventSink);
                                      }];
  return nil;
}

suggestion for sensors_plus

The differences I can spot are checking for sensor availability and not sending data if it is nil.

Aside from a more generous overhaul of both the Android and iOS implementation to enable interval management and sensor availability checking--a separate project--my work seems complete. Someone else may have to pick it up here. I suggest you try it out, Filip, if you find the time. I was still new to Dart and OSS when I first made my contribution to this package, and I have never even programmed for Android or iOS native! I truly wish I could test changes and this could take a day instead of weeks.

Good luck and cheers 🍻

Zabadam avatar Apr 04 '22 14:04 Zabadam

The PR that implements this fix will likely soon be merged. As the code is technically correct, especially when compared to your other linked package that implements iOS magnetometer support, I'm unsure the status on this Issue.

I would love an update on the status of this fix.

Zabadam avatar May 16 '22 01:05 Zabadam

Sorry, didn't notice your previous message. It's hard to test those things since we don't really know what's the correct result. The implementation looks good to me, I say: merge it.

filippkowalski avatar May 16 '22 07:05 filippkowalski

This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 15 days

github-actions[bot] avatar Aug 15 '22 00:08 github-actions[bot]

Wow, I learned a lot just by reading this excellent comment on StackOverflow.

Snipped below:

I have put a Magnet-O-Meter demo app on gitHub which displays some of these differences. It's quite revealing to wave a magnet around your device when the app is running and watching how the various APIs react:

my original solution

CMMagnetometer doesn't react much to anything unless you pull a rare earth magnet up close. The onboard magnetic fields seem far more significant than local external fields or the earth's magnetic field. On my iPhone 4S it consistently points to the bottom left of the device; on the iPad mini it points usually to the top right.

my proposed solution and the route taken by other Flutter plugins

CMCalibratedMagneticField is the most steady in the face of varying external fields, but otherwise tracks it's Core Location counterpart CLHeading.[x|y|z] pretty closely.

...but it seems that

CLHeading.[x|y|z] is the most vulnerable (responsive) to local external fields, whether moving or static relative to the device.

And this is the surprising bit. It would seem, according to these tests, that the Core Location CLHeading provides geomagnetic data that both filters out the device hard iron and also is more responsive to external elements (such as is desired for a stud finder) than is the Core Motion CMDeviceMotion's CMCalibratedMagneticField.

The person who posed the question suggests the official Apple example teslameter employs Core Location's CLHeading.

Their final note:

CLHeading.magneticHeading - Apple's recommendation for magnetic compass reading - is far more stable than any of these. It is using data from the other sensors to stabilise the magnetometer data. But you don't get a raw breakdown of x,y,z

Still, how these values compare to Android implementation is mystery to me. It is almost funny to me that by now I have learned far more about how iOS and Apple handle sensors and this sort of data than the platform I actually use. That just means I have more to learn!

Zabadam avatar Sep 26 '22 19:09 Zabadam

The PR with the magnet sensor changes has been merged and release. Let me know if this ticket can be closed.

miquelbeltran avatar Sep 27 '22 20:09 miquelbeltran

This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 15 days

github-actions[bot] avatar Dec 28 '22 00:12 github-actions[bot]

@Zabadam have you checked further to see if it gives the correct values? i checked today and magnetometerEventStream still returns incorrect values ​​that are much higher

Silverviql avatar Mar 29 '24 17:03 Silverviql