TechIDaily Journal
Sunlight as a Signal: VitaMindGo Pro's On-Wrist Light Algorithm
VitaMindGo Pro uses Apple Watch's ambient light sensor to estimate daily outdoor light exposure. We walk through the algorithm, the hardware quirks, and the strict privacy boundary we drew around the data.
Sunlight as a Signal: VitaMindGo Pro's On-Wrist Light Algorithm
*How Apple Watch's ambient light sensor became a daily outdoor score, and the privacy boundary we drew around it.*
VitaMindGo Pro is a wellness app that helps users track their daily routine of sunlight exposure, hydration, and movement. None of those features are medical. None of them diagnose or treat any condition. They are general well-being tools, and we are careful to keep them that way. This post is about the most technically interesting of the three: how we use the Apple Watch ambient light sensor to estimate outdoor exposure, and the algorithm we wrote to make that estimate useful without turning the watch into a surveillance device.
1. The Hardware: CMAmbientLightSensor
Apple Watch Series 6 and later include a dedicated ambient light sensor. It is not the camera. It is not the proximity sensor. It is a separate silicon die behind the display that returns a single lux value to the operating system, and it is exposed to apps through the CMAmbientLightSensor API in CoreMotion.
The sensor reports lux in the range of 0 to 100,000, with a quantization that varies by device. On a Series 9 the granularity is roughly 5 lux in low light and 50 lux in bright sunlight. That is enough to distinguish between a dim office and an overcast afternoon, but not enough to distinguish between a sunny day and a snowfield.
We sample the sensor once per minute when the wrist is raised and once per five minutes when the wrist is down. The sampling rate is the single biggest battery contributor in the entire app, and the rate we settled on is the floor where our outdoor classifier still produces useful results.
2. The Algorithm: Lux to Outdoor Score
The outdoor score is a simple accumulator. Each minute that the classifier returns "outdoor with high confidence", we add one point to the day's score. At the end of the day the score is the total number of outdoor minutes, and the app surfaces it as a general well-being indicator.
The classifier is a small piece of Swift code that combines the lux reading with the time of day, the device's battery state, and a one-shot color temperature estimate from the True Tone API.
enum OutdoorVerdict {
case indoor
case indoorLikely
case outdoorLikely
case outdoor
case unknown
}
func classify(lux: Double,
hour: Int,
isWristRaised: Bool,
colorTempKelvin: Double?) -> OutdoorVerdict {
guard isWristRaised else { return .unknown }
if lux < 50 { return .indoor }
if lux < 300 {
if hour < 7 || hour > 19 { return .outdoorLikely }
return .indoorLikely
}
if lux < 2000 { return .outdoorLikely }
return .outdoor
}The classifier is intentionally conservative. We return outdoorLikely when the evidence is mixed, and we never return outdoor unless the lux reading is unambiguous. The cost of a false positive is small (we add a minute to the score that should not be there). The cost of a false negative is the user wondering why their outdoor minutes did not increase on a sunny afternoon, which is a UX cost we accepted in exchange for not over-claiming.
3. Lux Calibration and Apple Watch Variability
The lux reading is not consistent across devices. A Series 7 in direct sun reads 85,000 lux. A Series 9 in the same conditions reads 78,000 lux. The variation comes from the sensor placement, the cover glass tint, and the device-specific calibration Apple applies at the factory.
We did not try to build a per-device calibration table. The variation is small enough that the classifier thresholds still work across the fleet, and a calibration table would force us to ship a database of model-specific offsets that would drift as Apple ships new watch models.
The threshold of 2000 lux for outdoorLikely was chosen empirically. We collected one month of anonymized lux readings from a small beta fleet and plotted the distribution. The bimodal split between indoor and outdoor was clean, and 2000 lux sat comfortably in the gap.
4. Indoor vs Outdoor Heuristics
Lux is not enough on its own. A bright office can read 800 lux. An overcast outdoor can read 1500 lux. The classifier combines lux with time of day and, when available, color temperature.
Time of day is the strongest signal. Outdoor light between 10am and 4pm is almost always brighter than 2000 lux in any climate our beta fleet inhabited. Outdoor light outside that window can be dim, and we collapse to outdoorLikely rather than outdoor for those hours.
Color temperature helps for the edge cases. Indoor lighting is typically 2700K to 4000K. Outdoor light is typically 5500K to 6500K on a sunny day and 7000K to 10000K on an overcast day. The True Tone API exposes color temperature, and we use it as a tiebreaker when the lux reading is in the 300-2000 range.
5. Daily Accumulation and Goal Setting
The accumulator is intentionally simple. Each minute of outdoor adds one to the score. Each minute of outdoorLikely adds 0.5. Indoor minutes add nothing. The final score is the total outdoor minutes for the day, expressed as a single number between 0 and the number of waking minutes.
We expose a single general well-being suggestion in the app, not a target. The suggestion is "spend some time outside today", and it appears regardless of the score. We deliberately do not show a goal number, do not show progress bars, and do not show streaks. Those features nudge users toward optimization, which is the wrong framing for general well-being.
6. Privacy: On-Device Only
Every lux reading stays on the device. We never send a lux value, an outdoor score, or a timestamp to a server. The accumulator runs in a Swift actor that lives in the app's main process, and the daily score is stored in an encrypted SQLite database that the app sandbox protects.
If a user backs up their Apple Watch, the outdoor score is included in the backup. We do not exclude it. The score is the user's own data, and they should be able to restore it on a new device. If a user wants to delete the score, they can do so from the app's settings, and the deletion is irreversible.
7. Limitations and What the App Does Not Claim
VitaMindGo Pro does not claim that outdoor light improves health outcomes. It does not claim that the outdoor score is correlated with any medical marker. It does not diagnose seasonal affective disorder or any other condition. The outdoor score is a number that helps users remember to go outside, and that is all.
We have turned down three partnership requests from companies that wanted to position the app as a clinical tool. The privacy boundary and the general-wellness-only positioning are non-negotiable, and the App Store review team has been clear that crossing that line would be grounds for rejection under Guideline 1.4.1.
8. Code: AmbientLightReader
The reader is a thin wrapper around CMAmbientLightSensor that publishes lux readings to a Combine subject.
import CoreMotion
import Combine
final class AmbientLightReader {
private let sensor = CMAmbientLightSensor()
private let subject = PassthroughSubject<Double, Never>()
var readings: AnyPublisher<Double, Never> { subject.eraseToAnyPublisher() }
func start() {
sensor.startSensorUpdates(to: .main) { [weak self] data in
guard let payload = data else { return }
self?.subject.send(payload.lux)
}
}
func stop() { sensor.stopSensorUpdates() }
}The CMAmbientLightSensor callback fires on the main queue, which is fine for our purposes because the downstream classifier runs in microseconds.
9. Five Calibration Challenges We Hit
Sensor off when screen is off. The ambient light sensor is gated by the display state on some watch models. We had to add a WKExtendedRuntimeSession to keep the sensor alive when the wrist was raised but the screen was not yet on.
True Tone unavailable. The True Tone API returns nil on watches older than Series 6. We had to make the color temperature tiebreaker optional and fall back to lux + time alone.
Stale readings after backgrounding. The sensor can return the same lux value for ten minutes if the watch is in a low-power state. We added a freshness check that ignores readings older than thirty seconds.
Inverted readings at sunset. The lux reading briefly spikes at sunset when the sun is below the horizon but the sky is still bright. We added a sunset-aware threshold that decays linearly between 7pm and 8pm.
Battery drain on Series 6. The first version of the reader sampled every ten seconds, which drained 15% of a Series 6 battery in two hours. The one-minute sampling rate dropped that to 4%.
10. What's Next
Three things are in active development. First, an iPhone-side outdoor classifier that combines the watch lux readings with the iPhone's color temperature and motion state for a higher-confidence score. Second, an opt-in weather integration that confirms outdoor minutes against the local weather API. Third, an Apple Vision Pro variant that uses the spatial light sensors for an even more accurate outdoor score.
If you want to see the algorithm in action, VitaMindGo Pro has a seven-day free trial with no commitment. The outdoor score is just one of three features, and the trial is the only honest way to know whether the routine fits your day.
*This post is for general well-being only, not medical treatment. The outdoor score is a general well-being indicator, not a medical measurement. If you have a chronic condition or symptoms that worry you, please consult a qualified clinician.*