App-Driven Adaptive Sampling for Wearables
Adaptive sampling turns your wearable into a system that feels alive, delivering the right data at the right moment while preserving battery for when it matters. Instead of locking sensor rates in firmware, we set behavior at the app layer with a small, transparent policy engine. The device advertises capabilities, the app selects named profiles (ULP, Balanced, Hi-Fi, Burst) based on motion, battery, link quality, and user intent.
This keeps firmware simple, makes iteration fast via remote config and A/B testing, and aligns data fidelity with product goals, better models, longer life, happier users. This post walks through the architecture, a declarative policy table, burst capture around key moments, observability that guides tuning, and rollout patterns teams can adopt quickly across wearables, pet tech, and sensor-rich IoT.
The problem
Fixed sensor rates are tidy in a spreadsheet but rarely optimal in the real world. User behavior is bursty, link quality fluctuates, and battery state changes across a day. Always-high rates waste energy when the device is idle, always-low rates miss sub-second events that matter to detection pipelines. Worse, baking rate logic into firmware slows iteration, every threshold change becomes a rebuild and rollout.
What we want instead: high fidelity when it matters (run starts, impacts, tremor-like motion), low power when it's safe (sleep, desk work, stationary bowl or collar), fast iteration through app-side policy updates rather than firmware reflashes, and observability so decisions are explainable and tunable.

Approach
Five guiding principles. Capabilities in firmware, behavior in the app, the device exposes what it can do (supported rates and modes), the app picks what it should do now. Profiles plus policy, bundle sensor modes into a few named profiles with a small policy engine selecting among them based on context. Atomic application, profile changes apply at safe boundaries with explicit acknowledgments and echoed effective rates. Observability from day one, log decisions, reasons, apply latency, and dwell times. Remote config and A/B, treat policy as data, ship it safely, and roll back instantly.
A reference architecture includes a mobile app running a policy engine evaluating context every 10-30 seconds, a remote config loader for flags and policy tables, a BLE or GATT client to send apply-profile commands, and a telemetry logger. Wearable firmware needs a capabilities descriptor, a profile interpreter mapping profile to driver settings, and atomic apply at frame boundaries with ack plus echo. The backend needs feature flags and remote config storage, analytics for A/B and dwell distributions, and optionally a risk-window hint pushed to the app.
Defining four pragmatic profiles
Profiles must be explicit, atomic, and cheap to apply. Start with ULP (Ultra-Low Power): IMU at 12.5Hz, baro at 0.1Hz, HR off, GPS off, BLE batching every 30-60 seconds. Balanced: IMU 25-50Hz, baro 0.5Hz, HR 0.5Hz, BLE batching every 10-20 seconds. Hi-Fi: IMU 100Hz, baro 1Hz, HR 1Hz, streaming or short batches. Burst, time-boxed: IMU 200Hz for 10-30 seconds around detected onsets like impacts or cadence jumps, then taper. If your device tops out at 100Hz IMU, adjust Hi-Fi and Burst accordingly, and explicitly budget bursts, like at most 2 minutes per hour, so battery models stay honest.
App-side config, remote-updatable, might look like:
profiles:
ulp: { imu_hz: 12.5, baro_hz: 0.1, hr_hz: 0, gps: off, ble_batch_s: 45 }
balanced: { imu_hz: 50, baro_hz: 0.5, hr_hz: 0.5, gps: ondemand, ble_batch_s: 15 }
hi_fi: { imu_hz: 100, baro_hz: 1.0, hr_hz: 1.0, gps: ondemand, ble_batch_s: 5 }
burst: { imu_hz: 200, duration_s: 20 }Choosing robust context signals
You don't need a large model to get big wins. Start with interpretable features: motion intensity, rolling variance of acceleration magnitude over 3-10 seconds with hysteresis; cadence buckets from steps or zero-crossings to distinguish stroll from run; battery, SoC tiers plus slope to catch fast drains; link health, RSSI bands and recent write or notify error counts; user modes, explicit workout, sleep, or DND toggles where intent always trumps heuristics; risk windows, optional server-pushed periods where missing events is costly; and on-device activity, an optional tiny classifier for still/walk/run/vehicle to gate upshifts.
Expressing decisions in a declarative policy table
Policies age poorly when scattered across if/else ladders, keep them centralized and data-driven:
dwell_seconds: 15
hysteresis:
motionVar: { up: 0.15, down: 0.10 }
rules:
- when: { workoutActive: true } # user intent dominates
then: hi_fi
- when: { riskWindow: true, motionVar: ">0.8" } # high risk + high motion
then: hi_fi
- when: { poorLink: true }
then: ulp
- when: { soc: "<25", isCharging: false, motionVar: "<0.1" }
then: ulp
- default: balancedThis works because dwell prevents flip-flop, hysteresis removes edge thrash, rules stay readable and testable, and remote config lets you ship safely.
Building the policy engine
Keep it deterministic and loggable. Evaluate on a fixed cadence, like every 15 seconds, or on significant context change:
enum Profile { ulp, balanced, hiFi, burst }
class Context {
final double motionVar; // 0..1 normalized variance over last N seconds
final int soc; // battery %
final bool isCharging;
final bool workoutActive;
final bool poorLink; // RSSI/packet loss heuristic
final bool riskWindow; // from server
const Context(...);
}
Profile pickProfile(Context c) {
if (c.workoutActive) return Profile.hiFi;
if (c.riskWindow && c.motionVar > 0.8) return Profile.hiFi;
if (c.poorLink) return Profile.ulp;
if (!c.isCharging && c.soc < 25 && c.motionVar < 0.1) return Profile.ulp;
return Profile.balanced;
}Designing the command path and acknowledgments
Use one characteristic for "apply profile," sending idempotent, tiny commands, a profile ID plus params like burst duration, a policy version, and a transaction ID. Apply at a safe boundary, like the next 250ms tick. The device should echo effective rates plus a result code (OK, Unsupported, Busy) and increment a segment counter for downstream analytics.
Future<void> apply(Profile p) async {
final cmd = switch (p) {
Profile.ulp => [0x01, 0x0C, 0x00, 0x00], // id + encoded params
Profile.balanced => [0x02, 0x32, 0x05, 0x05],
Profile.hiFi => [0x03, 0x64, 0x0A, 0x0A],
Profile.burst => [0x04, 0xC8, 0x14, 0x00], // 200 Hz for 20 s
};
await ble.write(characteristic: SAMPLING_CHAR, value: cmd, withResponse: true);
// Expect ack within ~300 ms on 2M PHY; log reason + latency
}Implementing burst capture for high-value moments
Burst is how you pay only when you must. Triggers include impulse peaks, cadence jumps, workout start, sharp orientation change, or server-hinted windows. Duration is typically 10-30 seconds to capture onset and settle. Guardrails cap bursts per hour and daily budget, with cool-down between bursts and cancellation on brown-out or persistent poor link. Tag each burst with profile ID plus a burst sequence for alignment, crucial for training and validation pipelines.

Balancing streaming versus batching
Hi-Fi and Burst prefer streaming or small batches for low latency. Balanced and ULP batch to reduce radio wakeups. Compression via delta-RLE and fixed-point quantization often yields 3-5x savings with negligible model impact in steady states. Prioritize sensor payloads over metrics and logs, throttling non-critical telemetry during poor link.
Observing it like a control loop
Log and chart timestamp, the before-to-after profile, reason predicates, policy_version, and transaction ID, along with apply latency and ack code, profile dwell per day, packet loss and retransmission near transitions, and battery slope before and after policy changes. Build a small dashboard for cohort views by app build, device revision, and region, when someone asks why it flipped to ULP at a specific time, show the reason stack, don't guess.
Shipping via remote config and A/B
Policies are product levers, treat them as such. Use cohorts by device rev, app build, geography, or opt-in. Set guardrails like max flips per hour, total burst budget per day, and a kill-switch forcing ULP. Dry-run evaluate, previewing how a new policy would have behaved over yesterday's telemetry before exposing users. And follow rollout discipline, 5-10% then 25% then 50% then 100%, with guardrails and dashboards watching.
Once you have reliable observability, consider adding a tiny activity model, a 1-2 kB linear or tree model distinguishing still/walk/run/vehicle, pruning noise and reducing flip-flops around thresholds. Ship heuristics first, add the model once you trust the data.

Engineering for safety and simplicity
Fail-safe ULP, brown-outs, thermal throttles, or repeated apply failures immediately force ULP with a visible reason code. Idempotent commands, re-applying the same profile is a no-op. Atomic apply, all or nothing with clear errors. Backward-compatibility, an unsupported profile reports "Unsupported," stays put, and reports a capability mismatch.
Results
Adaptive systems earn trust with numbers and method, not anecdotes. Measure deliberately. Battery life: hours from 100% to 10% SoC under real daily use, via A/B matched cohorts over at least 5 days, segmented by device rev and climate, watching for outliers with ambient temperature and screen-on time as covariates. Event fidelity: precision, recall, or detection latency for short events, via scripted sessions or labeled field snippets comparing static Balanced versus adaptive with Burst. Data volume: MB per day per device and per profile dwell, combining app logs with backend ingestion stats. UX stability: profile flips per hour, apply-latency percentiles, command failure rate, aiming for under 6 flips per hour and median apply latency under 300ms on BLE 2M PHY.
A typical outcome frame, to replace with your own data: battery life improving 28-42% median in mixed-use cohorts after adopting ULP for sedentary windows, event recall improving 3-5x for sub-2-second events when Burst triggers on motion spikes, data reduction of 35-55% lower daily upload without degrading primary model accuracy, and stability with under 2% command failures across 10k device-days, median apply time of 180-300ms, low chatter with a 15-second dwell. If results fall short, inspect thresholds that are too tight, dwell that's too low, burst budgets that are too high, and link heuristics forcing ULP too often due to conservative RSSI bands.

Applications
This pattern travels well across domains. Fitness wearables use Hi-Fi during structured workouts, Burst on sprints, ULP for desk hours, Balanced for casual walks. Safety pendants stay in Balanced or ULP, Burst on high impulse plus orientation change, escalating to Hi-Fi on persistent activity. Asset trackers duty-cycle GPS, waking only on movement, batching sensors when stationary, adapting to link quality in warehouses versus outdoors. Pet wearables raise IMU and baro during play and feeding windows, dropping to ULP overnight or when stationary. Smart bowls temporarily increase sampling around detected feeding events for weight and IMU fusion, otherwise staying Balanced with long BLE intervals. Only the triggers change, profiles plus policy stay the same.
Common issues and fixes
Profile chatter, flip-flopping: increase dwell, widen hysteresis, smooth motion variance with an EMA, add a post-burst cool-down. Apply lag feels slow: align changes to sensor ticks, tighten connection interval, use 2M PHY, keep command payloads tiny, avoid main-thread stalls in the app. Data gaps at switches: double-buffer on device, flush the old buffer before apply, tag segments with a monotonic counter. Policy drift across versions: include policy_version in each decision and the device echo, alert on mismatches. Remote-config foot-guns: server-side validate rules, dry-run on yesterday's telemetry, maintain a global kill-switch. Over-bursting: cap bursts per hour and per day, require clear triggers and cool-down, monitor burst dwell and budget burn-rate.
At Hoomanely, the mission is keeping pets healthier with thoughtful, privacy-aware technology. Adaptive sampling strengthens that mission by extending battery life, more days between charges for wearables and smart bowls, and capturing moments that matter, feeding episodes, unusual activity, without overwhelming storage or analytics systems. By driving behavior from the app, teams can tune quickly, measure responsibly, and keep user experience first, across EverSense-style wearables and EverBowl-class devices, and beyond.
Key takeaways
Keep capabilities in firmware and behavior in the app. Model rates as a few named profiles, ULP, Balanced, Hi-Fi, Burst. Select profiles with a declarative policy plus dwell and hysteresis. Use Burst for short, high-value windows, keeping steady states low-rate and sticky. Instrument decisions, reasons, apply latency, and dwell, iterating via remote config and A/B. And publish measured outcomes, letting the data speak.