Lesson 05 / 14
Sensors
Sensor events arrive quickly from device hardware such as the accelerometer; a slow receiver cannot keep up with all of them. The lesson measures which sensor event arrives when the buffer overflows and when the receiver takes only the last reading, and what happens once the listener is unregistered.
Published Updated
In this lesson
- Recognizes the concepts of the sensor manager, the sensor event and sampling
- Measures that on a slow receiver only the last reading arrives and the earlier ones are superseded
- Shows that when the buffer overflows, the newest readings are lost
- Shows that readings produced after the listener is unregistered are lost
Contents
In the previous lesson the event was born from a user action and each action gave a single result. In a sensor the event comes from hardware and is produced continuously, independent of the user. The accelerometer reads the motion of the device, the light sensor reads the ambient light. The readings can come faster than the receiver can process them. In this lesson we ask the same question again: how many copies of this sent event reached the receiver?
// Arrival.kt — classifies how many copies of a sent event reached the receiver
class Signal(val key: String, val seq: Int)
class Arrival(val label: String, val sent: Signal, received: List<Signal>) {
val copies: Int = received.count { it.seq == sent.seq }
val verdict: String = when {
copies > 1 -> "duplicated"
copies == 1 -> "once"
received.any { it.key == sent.key && it.seq > sent.seq } -> "superseded"
else -> "lost"
}
fun line() {
println("$label: key=${sent.key} | copies=$copies -> $verdict")
}
}
fun countArrivals(arrivals: List<Arrival>) {
for (name in listOf("once", "duplicated", "superseded", "lost")) {
println("$name: ${arrivals.count { it.verdict == name }}")
}
}
The classification is the same as in the previous lessons. The rule counts the copies in the receiver’s list that carry the same sequence number; if there is no copy, it checks whether a later event with the same key arrived. It does not see the content or the cause.
The Sensor Manager and the Listener
The platform reaches sensors through the sensor manager. The app selects a sensor, registers a listener and tells the manager the sampling rate. The rate is a request: the platform gives readings at a frequency close to this rate but does not guarantee the count. Each reading falls to the callback of the listener as a sensor event. It is also possible for the platform to deliver readings in batches; in that case several readings can arrive at the same time.
// Platform code: MotionWatcher
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
class MotionWatcher(private val manager: SensorManager) : SensorEventListener {
fun start() {
val accel = manager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
manager.registerListener(this, accel, SensorManager.SENSOR_DELAY_UI)
}
fun stop() = manager.unregisterListener(this)
override fun onSensorChanged(event: SensorEvent) = process(event.values)
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) = Unit
}
start registers the listener and stop removes the registration. In the model, a listener whose registration has been removed no longer receives readings. This snippet writes no rate number and claims no output. The model builds this rule of the platform; the model is not the platform itself.
The Sampling Rate Is a Request
The rate constant given while registering the listener is a hint about how often readings will come. The platform offers four separate levels: fastest, fast for games, medium for the user interface, and normal; normal is the slowest. A faster level produces more readings, but this is an approach of the platform, not a contract. Depending on the current load of the device, the battery state and the hardware limits of the sensor, the real frequency can deviate from this.
For this reason there are two separate delivery questions, and they must not be confused with each other. The first is how many readings the sensor produces; the rate level decides that. The second is how many of the produced readings reach the receiver; the rule of this lesson measures that. Raising the rate level increases production, but if the receiver is slow the count that arrives does not change: the extra readings produced only become superseded. The battery and processor cost grows in proportion to production. Raising the rate is useful when the receiver can process that many readings.
A high sampling rate also asks for a separate permission in some versions. If a listener is registered without declaring the permission, the platform silently pulls the rate to an upper limit; if a higher rate is requested by another route, a security error occurs. The app must check for itself whether it got the frequency it asked for.
A Slow Receiver
In the model, each reading is a Signal and its key is the name of the sensor. If the receiver is slow, in a single processing turn it takes only the last of the readings that have piled up; the earlier ones are skipped. The number of readings a turn covers is a made-up value of the model.
// Sampling.kt — a slow receiver takes only the last reading in each turn
fun latestOnly(sent: List<Signal>, perTurn: Int): List<Signal> =
sent.chunked(perTurn).map { it.last() }
// Main.kt — a slow receiver takes only the last reading in each turn
fun main() {
val sent = (1..8).map { Signal("sensor.accel", it) }
val received = latestOnly(sent, perTurn = 3)
println("received seqs: ${received.map { it.seq }}")
val arrivals = sent.map { Arrival("event-${it.seq}", it, received) }
arrivals.forEach { it.line() }
countArrivals(arrivals)
}
received seqs: [3, 6, 8] event-1: key=sensor.accel | copies=0 -> superseded event-2: key=sensor.accel | copies=0 -> superseded event-3: key=sensor.accel | copies=1 -> once event-4: key=sensor.accel | copies=0 -> superseded event-5: key=sensor.accel | copies=0 -> superseded event-6: key=sensor.accel | copies=1 -> once event-7: key=sensor.accel | copies=0 -> superseded event-8: key=sensor.accel | copies=1 -> once once: 3 duplicated: 0 superseded: 5 lost: 0
This lesson’s delivery is superseded. Five of the eight readings did not arrive, but after each of them a newer reading with the same key arrived. For tracking motion this is the right trade: the current value is worth more than the old one. The eighth reading arrived because it was the last of its turn; even if the last turn is not complete, the last reading is taken.
SC1. The model builds the turns with equal length and counts the end of each turn as the moment the receiver reads. On the platform the length of a turn changes with the processing load and the readings can be grouped several times in batched delivery; none of these exist in the model.
Batching and Flushing
To save battery, the platform can accumulate readings in hardware and deliver them together. The app states the longest delay allowed between the production of a reading and its delivery; the platform holds the readings until that time runs out or the buffer fills, and then delivers all of them together. This arrangement is what makes it possible to read the sensor even when the screen is off.
The rule of batching is short: a batch is either completed and delivered, or it waits. In the model, a batch that is waiting does not come on its own when the app removes the registration. The way not to lose readings in delayed delivery is to flush the waiting batch explicitly before removing the registration. In the model the batch size is three, and the last readings, whose batch is incomplete, are not delivered because they are not flushed.
// Batch.kt — the last incomplete batch is not delivered unless flushed
fun deliverBatched(sent: List<Signal>, size: Int): List<Signal> =
sent.chunked(size).filter { it.size == size }.flatten()
// Main.kt — the last incomplete batch is not delivered
fun main() {
val sent = (21..27).map { Signal("sensor.step", it) }
val received = deliverBatched(sent, size = 3)
println("received seqs: ${received.map { it.seq }}")
val arrivals = sent.map { Arrival("event-${it.seq}", it, received) }
arrivals.forEach { it.line() }
countArrivals(arrivals)
}
received seqs: [21, 22, 23, 24, 25, 26] event-21: key=sensor.step | copies=1 -> once event-22: key=sensor.step | copies=1 -> once event-23: key=sensor.step | copies=1 -> once event-24: key=sensor.step | copies=1 -> once event-25: key=sensor.step | copies=1 -> once event-26: key=sensor.step | copies=1 -> once event-27: key=sensor.step | copies=0 -> lost once: 6 duplicated: 0 superseded: 0 lost: 1
Six readings were delivered with two complete batches and arrived once. The seventh reading stayed in the incomplete batch and, because no reading with the same key arrived after it, it was lost. The rule does not see this as an error of the sensor or of the registration; it only counts that it did not arrive. The measure on the receiver side is to explicitly ask for the waiting readings before removing the registration.
SC2. The model builds the batch with a fixed size and without delay. On the platform the batch size depends on the reading rate, the delay limit and the buffer capacity, and the flush request is also completed through a callback; none of these exist in the model.
When the Buffer Overflows
A buffer can stand in front of the receiver. When the buffer is full there are two options: to drop the oldest or to drop the newly arriving reading. The receiver in the previous section kept the newest reading; a buffer that drops the newcomer does the opposite.
// Overflow.kt — drops newly arriving readings once the buffer is full
fun overflow(sent: List<Signal>, capacity: Int): List<Signal> =
sent.take(capacity)
// Main.kt — when the buffer overflows, the newest readings do not arrive
fun main() {
val sent = (9..14).map { Signal("sensor.light", it) }
val received = overflow(sent, capacity = 3)
println("received seqs: ${received.map { it.seq }}")
val arrivals = sent.map { Arrival("event-${it.seq}", it, received) }
arrivals.forEach { it.line() }
countArrivals(arrivals)
}
received seqs: [9, 10, 11] event-9: key=sensor.light | copies=1 -> once event-10: key=sensor.light | copies=1 -> once event-11: key=sensor.light | copies=1 -> once event-12: key=sensor.light | copies=0 -> lost event-13: key=sensor.light | copies=0 -> lost event-14: key=sensor.light | copies=0 -> lost once: 3 duplicated: 0 superseded: 0 lost: 3
The first three readings arrived once; readings 12, 13 and 14 were lost. Because no new reading with the same key arrived after the dropped readings, the rule does not count them as superseded. The same overflow would have ended as superseded in a buffer that drops the oldest. Which reading is dropped depends on the value to the receiver; the rule counts only the result.
SC3. The model builds the buffer capacity as a fixed integer. On the platform the buffer size depends on the hardware and the version, and which reading is dropped during an overflow can vary with the sensor; none of these exist in the model.
Sensor Types and Permission
Not all sensors are at the same permission level. Motion and light sensors ask for no permission. Sensors about the body, such as heart rate, ask for a runtime permission, and if the user does not grant it the sensor may not appear in the list or may give no readings. A missing permission produces no error at registration; the listener is registered, but the callback never runs.
This is a loss familiar from the previous two lessons. In location, updates did not come after the permission was revoked; in the camera, requests before the permission was granted produced no result. In a sensor too, a listener without permission is silent. The silence can arise from three separate situations: no permission, no such sensor in the hardware, or the sensor exists but produces no events. To tell them apart, the app looks first at the existence of the sensor, then at the state of the permission, and last at whether readings are flowing.
The Accuracy Callback and the Timestamp
The listener has two callbacks. The first gives the reading, and the second runs when the accuracy level of the sensor changes. The snippet in the platform code left the second empty. This is enough for most apps, but it can be a wrong choice for some sensors: sensors such as the compass need calibration, and when the accuracy is low the readings come but are meaningless.
The relation of this situation to the delivery model is as follows. A reading that comes with low accuracy has reached the receiver and the rule counts it as once. Whether the reading is usable is a property the rule does not see. The count arrived correctly, the value stayed unreliable. The receiver must keep these two questions apart: did it arrive, and if it arrived, is it reliable.
Each reading also carries a timestamp. The stamp shows the moment the reading was produced in hardware, not the moment it was delivered to the receiver. In batched delivery or with a slow receiver the two moments move apart: the reading comes late but its stamp is old. If the receiver looks at the stamp while processing a reading, it does not present an old reading as current. In the model the sequence number takes this role: a larger sequence means a newer production, and the superseded rule rests on this sequence.
Common Misconceptions
“If I raise the rate, I get more readings.” Production increases, but the count that reaches the receiver is limited by the speed of the receiver. On a slow receiver the extra readings are counted only as superseded; in the measurement five of the eight readings were superseded for this reason.
“A superseded reading is a loss, it is an error.” The place of a superseded reading has been taken by a newer reading with the same key. For an app tracking motion the current value is worth more than the old one, and this behavior is desired. An error is looked for where no reading comes after a reading, that is, in a loss.
“If the registration is removed, the waiting readings still come.” In the model, removing the registration silences the listener. In batched delivery the waiting batch does not come on its own; the app must first flush it explicitly.
“A sensor event is real time.” An event can wait from the moment it is produced to the moment it is delivered. In batched delivery and with a slow receiver this wait grows longer. The timestamp tells when the event was produced; it does not tell when it was processed.
“A sensor event is always processed on the main thread.” The callback runs on the main thread if no Handler is given, and on the looper of that Handler if one is; so it depends on how the registration was made. An app that does heavy work in the callback slows the receiver, and a slow receiver supersedes readings as in the first measurement of this lesson. Taking only the value in the callback and leaving the work to another thread means choosing superseding rather than loss.
“Leaving the listener always open is harmless.” An open listener produces readings and spends battery even when the screen is off. Removing the registration when readings are no longer needed both saves energy and prevents readings that nobody processes from piling up.
“The number of readings means accuracy.” A large number of readings does not show that the values are reliable. The accuracy level is separate information and changes independently of the count.
The Bounding Measurement
Even if the receiver is fast enough to process the readings, the readings stop the moment the listener registration is removed. If the app forgets to call stop when it leaves the screen, it spends battery; if it calls it, the readings after that moment do not arrive.
// Main.kt — once the listener registration is removed, later readings do not arrive
fun main() {
val sent = (15..20).map { Signal("sensor.accel", it) }
val received = latestOnly(sent, perTurn = 2).filter { it.seq < 19 }
println("received seqs: ${received.map { it.seq }}")
val arrivals = sent.map { Arrival("event-${it.seq}", it, received) }
arrivals.forEach { it.line() }
countArrivals(arrivals)
}
received seqs: [16, 18] event-15: key=sensor.accel | copies=0 -> superseded event-16: key=sensor.accel | copies=1 -> once event-17: key=sensor.accel | copies=0 -> superseded event-18: key=sensor.accel | copies=1 -> once event-19: key=sensor.accel | copies=0 -> lost event-20: key=sensor.accel | copies=0 -> lost once: 2 duplicated: 0 superseded: 2 lost: 2
Readings 16 and 18 arrived; 15 and 17 were superseded. Readings 19 and 20 were lost: because the listener registration was removed before 19, no reading arrived after that. The rule does not see that the cause of the loss is the removed registration.
SC4. The model builds the removal of the registration as a single step and as immediate. On the platform readings that stayed in the buffer can arrive even after the removal call; this is not in the model.
Summary
- This lesson’s delivery: superseded. A slow receiver takes only the last reading in each turn, and the earlier ones leave their place to a new reading with the same key.
- Of eight readings, five were superseded and three arrived once: superseded 5, once 3.
- When a buffer that drops the newly arriving reading overflowed, three readings were lost: lost 3. A buffer that drops the oldest would have made the same readings superseded.
- The two readings produced after the listener registration was removed were lost: lost 2.
- The last incomplete batch is not delivered unless flushed: six readings arrived once, the seventh was lost: once 6, lost 1.
- The sampling rate is a request; the platform does not guarantee to give readings at this frequency.
Next Step
In this lesson the event came from hardware and was superseded when the receiver could not keep up. In a deep link the event comes from outside as a link, and while the app is being recreated the same link can be processed once more. The next lesson, Deep Links and Shortcuts, measures a deep link that is processed again on rotation.
To keep your progress and take notes, Log in
My notes
Log in to take notes.