Sensors and battery¶
The current released API includes shake detection, compass, steps, ambient
light and proximity. Battery level, charging and power-saver state are exposed
separately through battery.
New in 1.7.0
accelerometer, gyroscope and pressure were added in
ApkPy 1.7.0. Install version 1.7.0 or newer.
The other sensors below are existing APIs, now documented together.
Callback reference¶
Import sensors and battery from apkpy_lib. Supply the named callback
when starting a watch. These methods do not return a reading immediately.
| Method | Callback value |
|---|---|
sensors.shake(on_shake=callback) |
No arguments: one shake event |
sensors.compass(on_change=callback) |
Heading in degrees, as text |
sensors.steps(on_change=callback) |
Steps since registration, as text |
sensors.light(on_change=callback) |
Ambient light in lux, as text |
sensors.proximity(on_change=callback) |
Boolean: True means near |
sensors.accelerometer(on_change=callback) |
JSON text with numeric x, y, z, in m/s² including gravity |
sensors.gyroscope(on_change=callback) |
JSON text with numeric x, y, z, in rad/s |
sensors.pressure(on_change=callback) |
Atmospheric pressure in hPa, as text |
shake() also accepts on_change as an alias for on_shake; its callback
still takes no arguments. All other sensor callbacks take one argument.
Only proximity is a Python boolean; numeric scalar readings are text.
The two vector sensors pass one JSON string, not three callback arguments.
| Control / battery method | Result |
|---|---|
sensors.available("light") |
Boolean: is the named sensor present? |
sensors.stop("light") |
Stop that watch; no result |
sensors.stop() |
Stop every sensor watch; no result |
battery.level() |
Percentage as text, for example "80" |
battery.charging() |
Boolean: charging state |
battery.saver() |
Boolean: power-saving mode |
Battery methods read a snapshot. They do not subscribe to future changes. Call them again when the user requests a refresh or when your screen resumes. Battery reads do not need Android permissions.
Choose the right measurement¶
| Sensor | Useful for | What it does not mean |
|---|---|---|
| Shake | a deliberate gesture such as resetting a game | every bump or raw acceleration sample |
| Compass | a heading display | GPS position or a perfectly calibrated navigation instrument |
| Steps | steps counted during one watch | a persistent daily fitness total |
| Light | reacting to ambient brightness | screen brightness or a camera exposure reading |
| Proximity | near/far interaction near the earpiece | an exact distance measurement |
| Accelerometer — 1.7.0 | tilt, motion feedback, simple instruments | acceleration with gravity removed |
| Gyroscope — 1.7.0 | rotation-rate feedback and motion controls | a rotation angle or absolute heading |
| Pressure — 1.7.0 | a barometer display | altitude without calibration |
Complete example: existing sensors and battery¶
This app keeps each reading separate. Step counting starts only when the user taps its own button, because it may require a permission prompt on Android.
from apkpy_lib import Screen, Theme, label, button, sensors, battery, run
home = Screen(id="home", scroll=True)
status = label("Tap Start instruments", id="status", screen=home)
shake_label = label("Shake: waiting", screen=home)
compass_label = label("Heading: waiting", screen=home)
light_label = label("Light: waiting", screen=home)
near_label = label("Proximity: waiting", screen=home)
steps_label = label("Steps: not started", screen=home)
battery_label = label("Battery: tap Read battery", screen=home)
def shaken():
shake_label.set_value("Shake detected")
def heading_changed(degrees):
compass_label.set_value("Heading: " + degrees + " degrees")
def light_changed(lux):
light_label.set_value("Light: " + lux + " lux")
def proximity_changed(near):
if near:
near_label.set_value("Proximity: near")
else:
near_label.set_value("Proximity: far")
def steps_changed(count):
steps_label.set_value("Steps in this watch: " + count)
def start_instruments():
if sensors.available("shake"):
sensors.shake(on_shake=shaken)
else:
shake_label.set_value("No accelerometer for shake detection")
if sensors.available("compass"):
sensors.compass(on_change=heading_changed)
else:
compass_label.set_value("No heading sensor")
if sensors.available("light"):
sensors.light(on_change=light_changed)
else:
light_label.set_value("No light sensor")
if sensors.available("proximity"):
sensors.proximity(on_change=proximity_changed)
else:
near_label.set_value("No proximity sensor")
status.set_value("Watching available instruments")
def start_steps():
if not sensors.available("steps"):
steps_label.set_value("No step counter on this device")
return
steps_label.set_value("Allow activity access if asked, then walk")
sensors.steps(on_change=steps_changed)
def read_battery():
charging = "not charging"
saver = "power saver off"
if battery.charging():
charging = "charging"
if battery.saver():
saver = "power saver on"
battery_label.set_value(battery.level() + "% / " + charging + " / " + saver)
def stop():
sensors.stop()
status.set_value("Stopped: readings stay on screen")
button("Start instruments", command=start_instruments, screen=home)
button("Start step counter", command=start_steps, screen=home)
button("Read battery", command=read_battery, screen=home)
button("Stop all sensors", command=stop, screen=home)
style = """
home { background-color: #0b1018; padding: 18px; }
label { color: #e6edf7; font-size: 16px; margin-bottom: 12px; }
status { color: #69e6c4; font-weight: bold; }
button { width: 100%; min-height: 48px; margin-bottom: 10px; }
"""
if __name__ == "__main__":
run(start_screen=home, theme=Theme(mode="dark"))
Existing-sensor details¶
- Shake: acceleration magnitude must exceed 2.7 g. Android filters repeated crossings with an 800 ms gap. The Previewer has a deliberate shake button.
- Compass: reports the heading derived from the rotation-vector sensor in
whole degrees, with a one-degree change threshold. Availability checks that
sensor; a separate magnetometer alone is not enough for this implementation.
The current rounding can show
360near north; treat it as equivalent to0. - Steps: Android's raw counter runs since reboot. ApkPy subtracts the first reading of the watch; pause/resume retains that baseline, while stopping and starting again creates a new one. The first reading may require actual steps.
- Light: reports lux, retaining one decimal when needed. Changes smaller than one lux are filtered.
- Proximity: reports
Truefor near andFalsefor far. Android converts the raw distance using the sensor's maximum range; do not interpret the callback as centimetres. A callback is delivered when near/far changes.
Permission and missing-sensor handling¶
The only sensor here requiring a dangerous permission is steps:
android.permission.ACTIVITY_RECOGNITION on Android 10 and later.
ApkPy declares it for a step-watching app and requests it when the watch starts.
No explicit permission declaration is needed for the example above.
available() only checks hardware, not whether activity access was granted.
The sensor API has no on_error parameter today. A missing sensor, denied
permission or failed registration must not be mistaken for a zero reading.
Use an explicit waiting/unavailable state, offer Stop/Retry, and direct the
user to app permissions when activity access has been denied.
Use literal sensor names such as sensors.available("pressure") and
sensors.stop("light") in compiled apps. Dynamic sensor names are not part of
the current compiler contract. Use a button/callback to start a watch; avoid
starting it as a side effect of importing a module.
Keep the step registration outside nested blocks
Use the early-return guard in start_steps() above. The current compiler's
sensor dependency scan does not traverse every nested command block; a
sensors.steps() call inside an if can omit its permission helper in the
generated Java. This is a known limit, not a permission-denial result.
No library-code change is included in this documentation update.
Complete example: acceleration monitor¶
from apkpy_lib import Screen, Theme, label, button, sensors, json_get, run
home = Screen(id="home")
reading_label = label("Tap Start", id="reading", screen=home)
def changed(reading):
reading_label.set_value(
"X: " + json_get(reading, "x") + "\n"
+ "Y: " + json_get(reading, "y") + "\n"
+ "Z: " + json_get(reading, "z")
)
def start():
if sensors.available("accelerometer"):
sensors.accelerometer(on_change=changed)
else:
reading_label.set_value("No accelerometer on this device")
def stop():
sensors.stop("accelerometer")
button("Start", command=start, screen=home)
button("Stop", command=stop, screen=home)
style = """
home { background-color: #0b1018; padding: 20px; }
reading { color: #e6edf7; font-size: 20px; margin-bottom: 16px; }
button { width: 100%; min-height: 48px; margin-bottom: 10px; }
"""
if __name__ == "__main__":
run(start_screen=home, theme=Theme(mode="dark"))
Gyroscope and pressure variants¶
These replace changed(), start() and stop() in the acceleration-monitor app above;
keep home, reading_label, buttons and the rest of the app.
def changed(reading):
reading_label.set_value(
"X: " + json_get(reading, "x") + " rad/s\n"
+ "Y: " + json_get(reading, "y") + " rad/s\n"
+ "Z: " + json_get(reading, "z") + " rad/s"
)
def start():
if sensors.available("gyroscope"):
sensors.gyroscope(on_change=changed)
else:
reading_label.set_value("No gyroscope on this device")
def stop():
sensors.stop("gyroscope")
For all three at once, download the complete
Sensor Lab source. This is application
code, not the library's implementation. Save it as writehere.py in a project
using the 1.7.0 build; run python writehere.py for the Previewer
or apkpy run to produce an Android debug APK.
| Test | Expected result |
|---|---|
| Start sensors | present sensors begin reporting; absent ones are identified |
| Gently tilt the phone | accelerometer axes change; gravity remains included |
| Rotate the phone | gyroscope reports rotation rate |
| Stop sensors | values freeze; listeners are unregistered |
| Start again | a new watch starts without accumulating duplicate listeners |
| Leave and return on Android | requested watches suspend and resume |
Behaviour and limits¶
- Check
sensors.available("pressure")before starting pressure readings: many phones do not have a barometer. This tests hardware, not permission status. sensors.stop("gyroscope")stops one watch;sensors.stop()stops them all. Starting the same sensor again replaces its callback on Android.- Android unregisters sensors on Activity pause and restores requested watches on resume. Sensor callbacks update the interface on its main thread.
- The new streams use
SENSOR_DELAY_UI. This is a requested sampling rate, not a fixed timer. Changes smaller than 0.05 m/s², 0.01 rad/s or 0.1 hPa are filtered respectively. These are UI streams, not high-rate data capture. - Axes follow the device's natural orientation and do not rotate with the UI. Acceleration at rest includes gravity. Gyroscope readings are rotation rates, not angles. Pressure alone is not a calibrated altitude reading.
- No extra dependency or permission is added for the three new sensors. Step counting still needs activity-recognition permission on Android 10+.
- Projects without sensor or battery calls do not receive the sensor runtime.
In the desktop Previewer, these readings are simulated: a panel provides three axis sliders per motion sensor and a pressure slider. Availability means the simulator is supported, not that the desktop or connected phone contains the hardware. Use the installed Android app for real measurements.
What the Previewer panel can simulate¶
| Control | What to do |
|---|---|
| Shake button | fire the no-argument callback |
| Compass dial | change heading and exercise the one-degree filter |
| Light slider | change lux and test the brightness-dependent interface |
| Near/far checkbox | exercise both boolean branches |
| Steps buttons | add one or ten simulated steps |
| Three axis sliders | change X, Y and Z independently for each motion sensor |
| Pressure slider | test values even if your phone has no barometer |
| Battery row | adjust percentage, charging and power saver, then read again |
The panel does not read the connected phone, reproduce sensor calibration or
certify hardware sampling rates. Android lifecycle suspension is native-device
behaviour: stop watches explicitly when testing screen changes in the desktop
simulator. Closing the simulator panel hides its controls; it is not a
substitute for sensors.stop().
Troubleshooting¶
| Symptom | Check |
|---|---|
| Nothing happens when registering | supply on_change (on_shake for shake); check hardware first |
| No step readings | grant activity access, then walk; this API is not a daily history service |
| Accelerometer is not zero on a desk | gravity is included; this is expected |
| Gyroscope is near zero on a desk | no rotation is expected; the change filter suppresses small noise |
| No pressure on the phone | it may lack a barometer; do not substitute fake readings on Android |
| Values display in scientific notation | json_get can stringify small numeric JSON values that way; their numeric meaning is unchanged |
| Old readings remain after Stop | Stop freezes the interface; clear the labels yourself if desired |
| App works in Previewer but not on the phone | simulation does not prove hardware presence, permissions or calibration |
Verification scope¶
The 1.7.0 sensor implementation passed 47 focused sensor tests within a run of 882 feature tests, plus an Android Gradle build. Sensor Lab was installed on one Android 16 phone: real accelerometer and gyroscope readings, Start, Stop, restart and pause/resume were checked. That phone has no barometer; physical pressure readings have not been verified. Pressure simulation, formatting, generated mapping and compilation were checked instead.
See the Android SensorEvent reference for the sensor units and coordinate system.