start method

void start()

Starts gyroscope tracking. Calls calibrate automatically.

Implementation

void start() {
  stop();
  calibrate();

  _lastTimestamp = null;
  _prevYawFused = null;
  _prevPitchFused = null;
  _yawFused = 0.0;
  _pitchFused = 0.0;

  _gyroEventsCount = 0;
  isGyroscopeActive = true;
  _smoothPitch = 0.0;
  _lastAccelPitch = null;

  // Detect if gyroscope is present and active within 800ms
  Future.delayed(const Duration(milliseconds: 800), () {
    if (_gyroEventsCount == 0) {
      isGyroscopeActive = false;
    }
  });

  if (!useIsolate) {
    // Main-thread Direct Low-Latency Fusion
    _accelSubscription =
        (accelerometerStreamOverride ??
                accelerometerEventStream(
                  samplingPeriod: SensorInterval.fastestInterval,
                ))
            .listen((event) {
              _accelX = event.x;
              _accelZ = event.z;

              if (!isGyroscopeActive) {
                _updateFromAccelerometerOnly(event.x, event.y, event.z);
              }
            });

    _subscription =
        (gyroscopeStreamOverride ??
                gyroscopeEventStream(
                  samplingPeriod: SensorInterval.fastestInterval,
                ))
            .listen((event) {
              _gyroEventsCount++;
              isGyroscopeActive = true;

              if (_calibrating) {
                _calibrationSumX += event.x;
                _calibrationSumY += event.y;
                _calibrationSamples++;
                return;
              }

              // Dynamic Auto-Calibrating Anti-Drift:
              // If the gyroscope velocity is extremely low, adaptively adjust the offsets
              final double magnitude = sqrt(
                event.x * event.x + event.y * event.y,
              );
              if (magnitude < 0.015) {
                _offsetX = _offsetX * 0.995 + event.x * 0.005;
                _offsetY = _offsetY * 0.995 + event.y * 0.005;
              }

              final adjustedX = event.x - _offsetX;
              final adjustedY = event.y - _offsetY;

              // Gravity reference for pitch channel in Landscape Left:
              // Horizon (0°): accelX ≈ +9.8, accelZ ≈ 0 -> atan2(0, 9.8) = 0.0
              // Look up (ceiling): atan2(+9.8, 0) = +1.57 rad
              // Look down (feet): atan2(-9.8, 0) = -1.57 rad
              double gravityPitch() {
                final ax = _accelX.abs() < 0.01 ? 0.01 : _accelX;
                return atan2(_accelZ, ax);
              }

              final now = DateTime.now();
              if (_lastTimestamp == null) {
                _lastTimestamp = now;
                _yawFused = 0.0;
                _pitchFused = gravityPitch();
                _prevYawFused = _yawFused;
                _prevPitchFused = _pitchFused;
                return;
              }

              final dt =
                  now.difference(_lastTimestamp!).inMicroseconds / 1000000.0;
              _lastTimestamp = now;

              // Yaw in Landscape Left: turning head LEFT produces adjustedX > 0,
              // so +adjustedX increases camera yaw (looking left).
              _yawFused += adjustedX * dt;

              // Pitch in Landscape Left: tilting head UP increases camera pitch (looking up).
              _pitchFused =
                  _alpha * (_pitchFused + adjustedY * dt) +
                  (1 - _alpha) * gravityPitch();

              final predictionTime = predictionMs / 1000.0;
              final predictedYaw = _yawFused + adjustedX * predictionTime;
              final predictedPitch = _pitchFused + adjustedY * predictionTime;

              final dYaw = predictedYaw - (_prevYawFused ?? predictedYaw);
              final dPitch =
                  predictedPitch - (_prevPitchFused ?? predictedPitch);

              _prevYawFused = predictedYaw;
              _prevPitchFused = predictedPitch;

              if (jitterDamping) {
                // Deadband for tiny sensor vibrations
                final rawDYaw = dYaw.abs() < 0.00015 ? 0.0 : dYaw;
                final rawDPitch = dPitch.abs() < 0.00015 ? 0.0 : dPitch;
                // Fast exponential filter
                _dampedDYaw = _dampedDYaw * 0.15 + rawDYaw * 0.85;
                _dampedDPitch = _dampedDPitch * 0.15 + rawDPitch * 0.85;
                target.rotate(
                  _dampedDYaw * sensitivity,
                  _dampedDPitch * sensitivity * pitchGain,
                );
              } else {
                target.rotate(
                  dYaw * sensitivity,
                  dPitch * sensitivity * pitchGain,
                );
              }
            });
    return;
  }

  // Native Platform: Background Isolate Setup
  final fusionIsolate = BackgroundIsolate.create();
  _fusionIsolate = fusionIsolate;

  fusionIsolate.messages.listen((message) {
    if (message is List) {
      final dYaw = (message[0] as num).toDouble();
      final dPitch = (message[1] as num).toDouble();
      target.rotate(dYaw, dPitch);
    } else {
      // First message: the worker's SendPort (two-way channel)
      _isolateSendPort = message;
      _isolateSendPort.send([
        0,
        _alpha,
        sensitivity,
        predictionMs,
        _offsetX,
        _offsetY,
      ]);
    }
  });

  fusionIsolate.start(headTrackingFusionEntry);

  _accelSubscription =
      (accelerometerStreamOverride ??
              accelerometerEventStream(
                samplingPeriod: SensorInterval.fastestInterval,
              ))
          .listen((event) {
            _isolateSendPort?.send([1, event.x, event.y, event.z]);

            if (!isGyroscopeActive) {
              _updateFromAccelerometerOnly(event.x, event.y, event.z);
            }
          });

  _subscription =
      (gyroscopeStreamOverride ??
              gyroscopeEventStream(
                samplingPeriod: SensorInterval.fastestInterval,
              ))
          .listen((event) {
            _gyroEventsCount++;
            isGyroscopeActive = true;

            if (_calibrating) {
              _calibrationSumX += event.x;
              _calibrationSumY += event.y;
              _calibrationSamples++;
              return;
            }
            _isolateSendPort?.send([2, event.x, event.y, event.z]);
          });
}