> ## Documentation Index
> Fetch the complete documentation index at: https://developer.neurofit.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Troubleshooting

> Symptoms, causes and fixes for activation, permissions, camera, signal, result and build problems.

Each entry names the symptom you see, what is usually behind it, and what to do. When you contact
[contact@neurofit.app](mailto:contact@neurofit.app), include the SDK version, the platform and OS version, the
device model and, for a reading problem, the reading's `id` and the events you recorded for it; see
[Diagnostics and support](/diagnostics).

## Activation and license

<AccordionGroup>
  <Accordion title="activate throws licenseInvalid: the key is malformed or does not verify">
    The key string is malformed, was edited, or was signed for another product. Check that the whole string was
    copied (three segments separated by dots, starting with `NFV1.`), with no line breaks, padding or surrounding
    quotes added by your config system. Leading and trailing spaces, tabs and newlines are trimmed; anything else
    inside the string is not. If it still fails, ask NEUROFIT to re-issue the key.
  </Accordion>

  <Accordion title="activate throws licenseInvalid: the key does not cover this app">
    The running app's bundle identifier or application id is not on the key; the comparison is exact and
    case-sensitive. The relaxation for development works differently on the two platforms: on Android it is
    decided at runtime by the host app being debuggable (`FLAG_DEBUGGABLE`), so a debug flavour with a different id
    activates; on iOS it is compiled into the SDK module with `#if DEBUG`, so it applies only when you build the
    SDK from source in a Debug configuration, never with the Release-built xcframework. Ask NEUROFIT to add the
    identifier; you receive a new key.
  </Accordion>

  <Accordion title="activate throws licenseInvalid: this SDK build is newer than the key">
    This SDK build was released after your maintenance period ended. Either stay on the last SDK build dated on or
    before your key's `updatesUntil`, or renew maintenance and receive a key with a later date. Apps you have
    already shipped keep working; see [License keys](/license-keys).
  </Accordion>

  <Accordion title="Creating a session or calling start throws notActivated">
    `activate` was not called, or it threw and the error was swallowed. Activate once at launch and log the
    outcome. `start()` rechecks activation, so it throws the same error if activation never happened.
  </Accordion>
</AccordionGroup>

## Permissions and camera

<AccordionGroup>
  <Accordion title="start throws cameraPermissionDenied">
    The user declined camera access, or a device policy blocks it. Explain why the reading needs the camera and
    link to Settings (`UIApplication.openSettingsURLString` on iOS; `Settings.ACTION_APPLICATION_DETAILS_SETTINGS`
    on Android). iOS shows the system prompt only once, so after a refusal `requestCameraPermission()` returns
    `false` at once. On Android a second refusal may be permanent; check `shouldShowRequestPermissionRationale`
    before asking again. A failed `start()` leaves the session as it was, so you can call it again once access is
    granted.
  </Accordion>

  <Accordion title="Android: requestCameraPermission never calls back">
    The SDK registers its launcher on the activity instance that asked. If that activity is recreated while the
    system dialog is showing (a rotation, a split-screen resize), the new instance has no registration and the
    callback is never called. Lock the orientation of the screen that asks, and also check
    `VitalsSDK.hasCameraPermission(context)` in `onResume` rather than relying on the callback alone.
  </Accordion>

  <Accordion title="cameraUnavailable">
    Another app holds the camera, or the camera failed to open, stalled twice, or could not be restored after an
    interruption. On iOS `start()` can throw it; otherwise it arrives as `failed(cameraUnavailable)`. On Android
    `start()` returns normally and a camera that cannot be opened arrives as `Failed(CameraUnavailable)`. Retry
    after a moment with a new session. On Android the SDK binds and unbinds only its own CameraX use cases, so your
    own camera use is left alone; still, release a camera you hold yourself before starting a reading.
  </Accordion>

  <Accordion title="The torch does not turn on">
    A device whose rear camera has no torch (most iPads, many tablets) is supported and reads without one, so the
    signal depends on the room's light. On a phone with a torch: close other apps that may hold the flash, and let
    the phone cool down (iOS disables the torch when the device is hot).
  </Accordion>

  <Accordion title="cancelled(interrupted) after leaving the app">
    The capture was interrupted: the app left the foreground, another app took the camera, or on iOS a phone
    call, Split View or a media-services reset. On Android the lifecycle owner you passed to `start` reaching
    `ON_STOP` counts too. The session does not fail: a reading in progress is cancelled (an attempt cannot resume
    mid-way), the session waits, and it resumes positioning by itself when the cause clears. There is nothing to
    do; show a short note if you like. An interruption after `measurementComplete` does not affect the result. If
    the camera cannot be restored within 10 seconds once the app is back in front, the session ends with
    `cameraUnavailable`; create a new session. Lock the reading screen's orientation and keep the screen on.
  </Accordion>

  <Accordion title="Android: my own camera opens dark or colour-locked after a reading">
    This should not happen: the SDK clears the camera settings it applied before it unbinds, so your next use of
    the rear camera starts from CameraX defaults. If you see it, make sure the session's `stop()` ran (it is
    idempotent, so call it from your screen's teardown) and send NEUROFIT the device model.
  </Accordion>
</AccordionGroup>

## Positioning never starts the reading

<AccordionGroup>
  <Accordion title="Guidance stays on noContact">
    The camera does not see a fingertip. Common causes: the finger is over the wrong lens (phones with several rear
    cameras; the reading uses the main wide camera), a thick case or lens protector, or the torch is off. Show a
    small camera preview so the user can see the red glow when the finger is placed right.
  </Accordion>

  <Accordion title="Guidance alternates between weakSignal and compliant (iOS)">
    The pulse is faint. Cold hands are the usual cause; pressing hard is the second (it squeezes the blood out of
    the fingertip). Suggest warming the hands and resting the finger lightly. A thick case can also weaken the
    signal. This value is currently reported on iOS only.
  </Accordion>

  <Accordion title="Guidance stays on lowQuality">
    The signal is there but unsteady: the finger is moving, the pressure keeps changing, or the phone is being held
    in the air with a tense arm. Rest the phone on a table or the lap and hold the finger still.
  </Accordion>

  <Accordion title="Guidance shows frameDrop">
    The camera cannot hold its frame rate, and the reading does not start while `frameDrop` shows. On iOS this is
    almost always Low Power Mode; on Android, Battery Saver or thermal throttling. The SDK adapts the capture where
    it can and keeps showing `frameDrop` until the rate recovers; the session never fails for a low frame rate. Ask
    the user to turn off Low Power Mode or Battery Saver. A very old or very slow device can also cause it.
  </Accordion>

  <Accordion title="Guidance reaches compliant but the reading never starts">
    Check `isAutoStartEnabled`. While it is false the session keeps positioning and the start progress fills, but
    no reading starts. Set it back to true when your screen is ready.
  </Accordion>

  <Accordion title="Android: positioning takes 10 to 15 seconds even with a good signal">
    Android waits for the camera's exposure to settle before it starts, and some devices settle more slowly. This
    is expected; show the `compliant` guidance and the start progress so the wait is visible.
  </Accordion>

  <Accordion title="cancelReading does nothing">
    `cancelReading()` applies only while measuring. While positioning there is no attempt to cancel and the call is
    ignored. Use `stop()` to leave the screen.
  </Accordion>
</AccordionGroup>

## The reading keeps restarting

<AccordionGroup>
  <Accordion title="cancelled(contactLost)">
    The finger lifted or slid for a few seconds. Ask the user to keep the fingertip resting on the lens for the
    whole minute, and to avoid talking or adjusting their grip.
  </Accordion>

  <Accordion title="cancelled(poorSignal)">
    The signal stayed too poor to finish a reliable reading, so the session stopped early rather than run to 60
    seconds and withhold the result. Movement, changing pressure, cold fingers and hard pressure are the usual
    causes; a device with weak optics under a thick case is another. Rest the phone on a table, warm the hands and
    use a lighter touch. `LivePreview.signalLevel` drops before this cancel, so you can show it as a gentle "hold
    still" cue.
  </Accordion>

  <Accordion title="cancelled(interrupted) while the app stayed in front">
    The SDK restarted the camera during a reading: after a stall, or on iOS to adjust the capture when the camera
    could not hold its frame rate. The attempt starts again by itself. If it keeps happening on iOS, ask the user
    to turn off Low Power Mode. A second stall in one session ends it with `cameraUnavailable`; if a device model
    does this consistently, send NEUROFIT the model.
  </Accordion>
</AccordionGroup>

## Results

<AccordionGroup>
  <Accordion title="quality is withheld">
    The SDK could not stand behind the HRV (RMSSD) and related metrics for this reading. In the validation study
    this happened to 3.3% of readings. Show a calm retry prompt (the NEUROFIT app uses "To ensure accurate results,
    NEUROFIT needs higher quality signal from your device. Please retry your reading."). Do not store it as an
    outcome. If one user is withheld repeatedly, check the fixes under positioning above; if one device model is
    withheld across users, tell NEUROFIT.
  </Accordion>

  <Accordion title="failed(noHeartRate)">
    60 seconds were captured but no heart rate could be recovered. This is rare and almost always a finger that was
    not really on the lens. Offer a retry with a preview visible.
  </Accordion>

  <Accordion title="LivePreview.rmssdMs stays empty">
    The live HRV (RMSSD) is provisional and appears from about 20 s into an attempt; before that it is absent on
    purpose. Every cancel restarts the clock.
  </Accordion>

  <Accordion title="rmssdMs differs from the provisional value in LivePreview">
    Expected. The live value is computed from a recent window and updated as it goes; the final value uses the
    whole minute with the full quality checks. Always store the final value.
  </Accordion>

  <Accordion title="sd2sd1 or baevskyStressIndex is missing while rmssdMs is present">
    Both are left out when the reading fails the plausibility check (see [Metrics](/metrics)), and `sd2sd1` is also
    absent when SDNN is missing. Treat them as optional.
  </Accordion>

  <Accordion title="HRV (RMSSD) reads higher than the user's wearable">
    Wearables report nightly or multi-hour averages; a seated daytime reading is a different moment, and RMSSD is
    strongly affected by breathing (slow breathing raises it). Compare like with like: a morning seated reading
    with the previous morning seated reading. See [Outcomes best practices](/outcomes-best-practices).
  </Accordion>
</AccordionGroup>

## Build and integration

<AccordionGroup>
  <Accordion title="iOS: 'no such module NeurofitVitals'">
    Make sure the xcframework is embedded in the app target (Embed and Sign) or that the local package product is
    linked to the target that imports it. With a source license, the `PpgCore` package must sit next to the SDK
    package at the relative path the `Package.swift` expects (`../../ports/swift` from `sdk/ios`).
  </Accordion>

  <Accordion title="iOS: the SDK sources fail to compile with 'internal import'">
    Building the SDK from source needs Xcode 16 or newer, because the module uses access-level imports. The binary
    xcframework ships a textual Swift interface, so it needs Xcode 27 or newer, the Xcode the release was built
    with.
  </Accordion>

  <Accordion title="iOS: xcodebuild says the simulator destination is ambiguous">
    Several installed runtimes can match `name=iPhone 15`. Pick the simulator by id instead:

    ```bash theme={null}
    xcodebuild -scheme NeurofitVitals-Package \
      -destination "id=$(xcrun simctl list devices available | grep -m1 'iPhone 15 (' | sed -E 's/.*\(([0-9A-F-]+)\).*/\1/')" \
      test
    ```

    Any available iPhone works for the package tests; the Simulator has no camera, so a reading itself needs a
    device (`deviceSupport()` reports "The iOS Simulator has no rear camera").
  </Accordion>

  <Accordion title="Android: 'Could not find com.neurofit:vitals-sdk:1.0.0'">
    The artifact is not in any repository your build resolves. Publish it to your artifact repository, add the
    delivered Maven folder as a `maven { url = uri("...") }` repository, or install it to `mavenLocal()` and add
    `mavenLocal()` to `repositories`.
  </Accordion>

  <Accordion title="Android: start() throws InternalError 'start() must be called on the main thread'">
    `VitalsSession.start` binds CameraX to your lifecycle owner and must run on the main looper. Call it from the
    main thread (`runOnUiThread`, `lifecycleScope.launch` on `Dispatchers.Main`). `cancelReading` and `stop` can be
    called from any thread.
  </Accordion>

  <Accordion title="Android: R8 strips something and the SDK crashes in release">
    The AAR ships `consumer-rules.pro`, which keeps the public API in `com.neurofit.vitals.*` (the top-level
    package only; everything else in the AAR is internal and meant to be shrunk). If you use a custom R8
    configuration that ignores consumer rules, copy the rules listed on the
    [Android API reference](/api-reference-android#proguard--r8).
  </Accordion>

  <Accordion title="Android: events arrive on an unexpected thread">
    `events` is emitted on the SDK's own thread. Collect it inside `lifecycleScope.launch` (or another coroutine on
    `Dispatchers.Main`) before touching views.
  </Accordion>

  <Accordion title="Android: my events collector never finishes">
    The `events` flow does not complete when the session ends. Cancel the collecting coroutine when you are done,
    or collect in a scope that ends with your screen, such as `lifecycleScope`.
  </Accordion>

  <Accordion title="Wrappers: events stop arriving after a hot reload">
    A hot reload can leave a native session running with no JavaScript or Dart listener. Call `stop()` in your
    cleanup and create a new session when the screen mounts again.
  </Accordion>
</AccordionGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.