diagnostics or the CancelContext. Turning on the SDK’s
own logging (VitalsSDK.isLoggingEnabled = true on iOS, VitalsSDK.loggingEnabled = true on Android; both off by
default) records state transitions and capture decisions, never metric values, frames or license contents.
Activation and license
activate throws licenseInvalid
activate throws licenseInvalid
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.activate throws licenseNotValidForApp(appId)
activate throws licenseNotValidForApp(appId)
appId in the error with the
apps you asked for; the comparison is exact and case-sensitive. The relaxation to a warning 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 with a warning; 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. An iOS process without a bundle identifier (a bare
test runner) fails with appId empty in Release builds; a Debug build of the SDK sources accepts it with the
same relaxed-app-id warning. Ask NEUROFIT to add the identifier; you receive a new key.activate throws licenseUpdatesExpired(updatesUntil)
activate throws licenseUpdatesExpired(updatesUntil)
updatesUntil, or renew maintenance and receive a key with a later date. Apps you have already shipped
keep working; see License keys.Creating a session or calling start throws notActivated
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.Permissions and camera
start throws cameraDenied or cameraRestricted
start throws cameraDenied or cameraRestricted
cameraRestricted, iOS only). Explain why the
reading needs the camera and link to Settings (UIApplication.openSettingsURLString on iOS;
Settings.ACTION_APPLICATION_DETAILS_SETTINGS on Android). On Android, a second refusal may be permanent; check
shouldShowRequestPermissionRationale before asking again. A failed start() leaves the session restartable,
so you can call it again once access is granted.Android: requestCameraPermission never calls back
Android: requestCameraPermission never calls back
VitalsSDK.hasCameraPermission(context) in onResume rather than relying on the callback alone.The torch does not turn on
The torch does not turn on
deviceSupport().hasTorch, close other apps that may hold the flash, and let the phone cool
down (iOS disables the torch when the device is hot). diagnostics.torch_active records whether the torch was
on at the end of the reading.Session fails with interrupted(reason)
Session fails with interrupted(reason)
"The app moved to the background"), a phone call or another app took
the camera, or media services were reset. On Android the same failure fires when the lifecycle owner you passed
to start reaches ON_STOP while positioning or measuring, which includes an activity being recreated for a
rotation. Lock the reading screen’s orientation, keep the screen on, and create a new session when the user
returns. Readings cannot resume mid-way. An interruption during finalizing is ignored and the result is still
delivered.Android: my own camera opens dark or colour-locked after a reading
Android: my own camera opens dark or colour-locked after a reading
stop() ran (it is idempotent, so call it from your screen’s teardown) and send NEUROFIT the device
model.Positioning never starts the reading
Guidance stays on noContact
Guidance stays on noContact
Guidance alternates between weakSignal and compliant (iOS)
Guidance alternates between weakSignal and compliant (iOS)
Guidance stays on lowQuality
Guidance stays on lowQuality
Guidance shows frameDrop, or reconfiguringCamera keeps firing
Guidance shows frameDrop, or reconfiguringCamera keeps firing
lowPowerMode so you can ask the user to turn it
off. On Android, Battery Saver or thermal throttling; six consecutive previews under 24 fps while positioning
report recoverableError(frameRateUnsustainable(fps)) once, and a second recoverable error fails the session
with unsupportedDevice. A very old or very slow device can also cause it.Android: positioning takes 10 to 15 seconds even with a good signal
Android: positioning takes 10 to 15 seconds even with a good signal
compliant guidance and the start progress so the wait
is visible.cancelReading does nothing
cancelReading does nothing
cancelReading applies only while measuring. While positioning there is no attempt to cancel and the call is
ignored (the SDK logs it when logging is on). Use stop() to leave the screen.The reading keeps restarting
cancelled(contactLost)
cancelled(contactLost)
cancelled(lowQuality)
cancelled(lowQuality)
lowQuality guidance. Use LivePreview.abortRisk to warn the user before a cancel fires.cancelled(noLowDiscardEstimate)
cancelled(noLowDiscardEstimate)
abortRisk moves to warn at about 21 s and imminent at about 30 s before this
cancel. It usually means a faint or noisy pulse for the whole attempt: cold fingers, hard pressure, or a device
with weak optics under the case. Warm hands and a lighter touch fix most of these.I want to stop the auto-restart
I want to stop the auto-restart
autoRestartAfterCancel = false. After a cancel the session emits cancelled, goes to idle, turns the
torch off and releases the camera; call start() on the same session when you are ready (on Android with the
same or a new lifecycle owner). Most apps keep the default and show a short “let’s try that again” note.recoverableError fired, then the session failed
recoverableError fired, then the session failed
frameRateUnsustainable,
cameraStalled). The second one fails the session: with unsupportedDevice for a frame-rate problem, with
cameraUnavailable for a stall. Create a new session; if the device does it consistently, send NEUROFIT the
model and the diagnostics.Results
quality is withheld
quality is withheld
failed(noHeartRate)
failed(noHeartRate)
breathingRateBrpm is present but breathingRateConfidence is under 0.5
breathingRateBrpm is present but breathingRateConfidence is under 0.5
LivePreview.rmssdMs or breathingRate stays empty
LivePreview.rmssdMs or breathingRate stays empty
rmssdMs differs from the provisional value in LivePreview
rmssdMs differs from the provisional value in LivePreview
sd2sd1 or baevskyStressIndex is missing while rmssdMs is present
sd2sd1 or baevskyStressIndex is missing while rmssdMs is present
sd2sd1 is also
absent when SDNN is missing or the ratio is undefined. Treat them as optional.HRV (RMSSD) reads higher than the user's wearable
HRV (RMSSD) reads higher than the user's wearable
Build and integration
iOS: 'no such module NeurofitVitals'
iOS: 'no such module NeurofitVitals'
PpgCore package must sit next to the SDK
package at the relative path the Package.swift expects (../../ports/swift from sdk/ios).iOS: the SDK sources fail to compile with 'internal import'
iOS: the SDK sources fail to compile with 'internal import'
internal import PpgCore) so the engine stays out of its public interface. The binary xcframework has no
toolchain requirement beyond iOS 15 deployment.iOS: xcodebuild says the simulator destination is ambiguous
iOS: xcodebuild says the simulator destination is ambiguous
name=iPhone 15. Pick the simulator by id instead:deviceSupport() reports “The iOS Simulator has no rear camera”).Android: 'Could not find com.neurofit:vitals-sdk:1.0.0'
Android: 'Could not find com.neurofit:vitals-sdk:1.0.0'
maven { url = uri("...") } repository, or install it to mavenLocal() (the
delivered build_aar.sh runs publishToMavenLocal) and add mavenLocal() to repositories.Android: start() throws InternalError 'must be called on the main thread'
Android: start() throws InternalError '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.Android: R8 strips something and the SDK crashes in release
Android: R8 strips something and the SDK crashes in release
consumer-rules.pro, which keeps the public API in com.neurofit.vitals.* (the top-level
package only; the internals under .capture, .license, .reading and the engine under ppgcore.* are meant
to be shrunk). If you use a custom R8 configuration that ignores consumer rules, copy the rules listed on the
Android API reference.Android: events arrive on an unexpected thread
Android: events arrive on an unexpected thread
events and state are emitted on the SDK’s engine thread. Collect them inside lifecycleScope.launch (or
another coroutine on Dispatchers.Main) before touching views, or pass ContextCompat.getMainExecutor(context)
to setListener.Wrappers: events stop arriving after a hot reload
Wrappers: events stop arriving after a hot reload
stop() in your
cleanup and create a new session when the screen mounts again.