From 4fde4e06011a8938566f465a475ad053f7e32f74 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Thu, 20 Nov 2025 12:22:13 -0800 Subject: [PATCH 1/2] Add sensor services and events to Flet Introduced support for device sensors including accelerometer, user accelerometer, gyroscope, magnetometer, and barometer. Added corresponding Dart services, Python control classes, event types, and documentation. Updated mkdocs navigation and provided example usage for each sensor. --- .../flet/lib/src/flet_core_extension.dart | 11 + packages/flet/lib/src/services/sensors.dart | 198 ++++++++++++++++++ .../controls/sensors/accelerometer/basic.py | 28 +++ .../controls/sensors/barometer/basic.py | 27 +++ .../controls/sensors/gyroscope/basic.py | 27 +++ .../controls/sensors/magnetometer/basic.py | 27 +++ .../sensors/user_accelerometer/basic.py | 30 +++ .../flet/docs/controls/accelerometer.md | 14 ++ .../packages/flet/docs/controls/barometer.md | 14 ++ .../packages/flet/docs/controls/gyroscope.md | 14 ++ .../flet/docs/controls/magnetometer.md | 14 ++ .../flet/docs/controls/useraccelerometer.md | 14 ++ .../docs/types/accelerometerreadingevent.md | 5 + .../flet/docs/types/barometerreadingevent.md | 5 + .../flet/docs/types/gyroscopereadingevent.md | 5 + .../docs/types/magnetometerreadingevent.md | 5 + .../flet/docs/types/sensorerrorevent.md | 5 + .../types/useraccelerometerreadingevent.md | 5 + sdk/python/packages/flet/mkdocs.yml | 11 + sdk/python/packages/flet/src/flet/__init__.py | 24 +++ .../flet/controls/services/accelerometer.py | 71 +++++++ .../src/flet/controls/services/barometer.py | 56 +++++ .../src/flet/controls/services/gyroscope.py | 54 +++++ .../flet/controls/services/magnetometer.py | 55 +++++ .../flet/controls/services/sensor_events.py | 153 ++++++++++++++ .../controls/services/user_accelerometer.py | 61 ++++++ 26 files changed, 933 insertions(+) create mode 100644 packages/flet/lib/src/services/sensors.dart create mode 100644 sdk/python/examples/controls/sensors/accelerometer/basic.py create mode 100644 sdk/python/examples/controls/sensors/barometer/basic.py create mode 100644 sdk/python/examples/controls/sensors/gyroscope/basic.py create mode 100644 sdk/python/examples/controls/sensors/magnetometer/basic.py create mode 100644 sdk/python/examples/controls/sensors/user_accelerometer/basic.py create mode 100644 sdk/python/packages/flet/docs/controls/accelerometer.md create mode 100644 sdk/python/packages/flet/docs/controls/barometer.md create mode 100644 sdk/python/packages/flet/docs/controls/gyroscope.md create mode 100644 sdk/python/packages/flet/docs/controls/magnetometer.md create mode 100644 sdk/python/packages/flet/docs/controls/useraccelerometer.md create mode 100644 sdk/python/packages/flet/docs/types/accelerometerreadingevent.md create mode 100644 sdk/python/packages/flet/docs/types/barometerreadingevent.md create mode 100644 sdk/python/packages/flet/docs/types/gyroscopereadingevent.md create mode 100644 sdk/python/packages/flet/docs/types/magnetometerreadingevent.md create mode 100644 sdk/python/packages/flet/docs/types/sensorerrorevent.md create mode 100644 sdk/python/packages/flet/docs/types/useraccelerometerreadingevent.md create mode 100644 sdk/python/packages/flet/src/flet/controls/services/accelerometer.py create mode 100644 sdk/python/packages/flet/src/flet/controls/services/barometer.py create mode 100644 sdk/python/packages/flet/src/flet/controls/services/gyroscope.py create mode 100644 sdk/python/packages/flet/src/flet/controls/services/magnetometer.py create mode 100644 sdk/python/packages/flet/src/flet/controls/services/sensor_events.py create mode 100644 sdk/python/packages/flet/src/flet/controls/services/user_accelerometer.py diff --git a/packages/flet/lib/src/flet_core_extension.dart b/packages/flet/lib/src/flet_core_extension.dart index 17e0868ff5..a024887349 100644 --- a/packages/flet/lib/src/flet_core_extension.dart +++ b/packages/flet/lib/src/flet_core_extension.dart @@ -112,6 +112,7 @@ import 'services/file_picker.dart'; import 'services/haptic_feedback.dart'; import 'services/semantics_service.dart'; import 'services/shake_detector.dart'; +import 'services/sensors.dart'; import 'services/shared_preferences.dart'; import 'services/storage_paths.dart'; import 'services/tester.dart'; @@ -369,22 +370,32 @@ class FletCoreExtension extends FletExtension { switch (control.type) { case "BrowserContextMenu": return BrowserContextMenuService(control: control); + case "Accelerometer": + return AccelerometerService(control: control); + case "Barometer": + return BarometerService(control: control); case "Clipboard": return ClipboardService(control: control); case "FilePicker": return FilePickerService(control: control); case "HapticFeedback": return HapticFeedbackService(control: control); + case "Gyroscope": + return GyroscopeService(control: control); case "ShakeDetector": return ShakeDetectorService(control: control); case "SharedPreferences": return SharedPreferencesService(control: control); case "SemanticsService": return SemanticsServiceControl(control: control); + case "Magnetometer": + return MagnetometerService(control: control); case "StoragePaths": return StoragePaths(control: control); case "Tester": return TesterService(control: control); + case "UserAccelerometer": + return UserAccelerometerService(control: control); case "UrlLauncher": return UrlLauncherService(control: control); default: diff --git a/packages/flet/lib/src/services/sensors.dart b/packages/flet/lib/src/services/sensors.dart new file mode 100644 index 0000000000..01745fc1ee --- /dev/null +++ b/packages/flet/lib/src/services/sensors.dart @@ -0,0 +1,198 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:sensors_plus/sensors_plus.dart'; + +import '../flet_service.dart'; +import '../utils/time.dart'; + +abstract class _SensorStreamService extends FletService { + _SensorStreamService({required super.control}); + + StreamSubscription? _subscription; + bool _enabled = true; + bool _hasReadingSubscribers = false; + bool _hasErrorSubscribers = false; + Duration _interval = SensorInterval.normalInterval; + bool _cancelOnError = true; + + Duration get defaultInterval => SensorInterval.normalInterval; + + String get eventName => "reading"; + + Stream sensorStream(Duration samplingPeriod); + + Map serializeEvent(T event); + + @override + void init() { + super.init(); + _updateConfig(forceRestart: true); + } + + @override + void update() { + _updateConfig(); + } + + void _updateConfig({bool forceRestart = false}) { + var enabled = control.getBool("enabled", true) ?? true; + var interval = + control.getDuration("interval", defaultInterval) ?? defaultInterval; + if (interval.isNegative) { + interval = defaultInterval; + } + var hasReadingSubscribers = control.getBool("on_$eventName") == true; + var hasErrorSubscribers = control.getBool("on_error") == true; + var cancelOnError = control.getBool("cancel_on_error", true) ?? true; + + if (forceRestart || + enabled != _enabled || + interval != _interval || + hasReadingSubscribers != _hasReadingSubscribers || + hasErrorSubscribers != _hasErrorSubscribers || + cancelOnError != _cancelOnError) { + _enabled = enabled; + _interval = interval; + _hasReadingSubscribers = hasReadingSubscribers; + _hasErrorSubscribers = hasErrorSubscribers; + _cancelOnError = cancelOnError; + _restart(); + } + } + + void _restart() { + _subscription?.cancel(); + _subscription = null; + + if (!_enabled || (!_hasReadingSubscribers && !_hasErrorSubscribers)) { + return; + } + + final samplingPeriod = _interval; + try { + _subscription = sensorStream(samplingPeriod).listen( + (event) { + if (_hasReadingSubscribers) { + final payload = serializeEvent(event); + control.triggerEvent(eventName, payload); + } + }, + onError: (error, stackTrace) { + if (_hasErrorSubscribers) { + control.triggerEvent( + "error", {"message": error?.toString() ?? "Unknown sensor error"}); + } else { + debugPrint( + "Error listening to ${control.type} sensor stream: $error"); + } + }, + cancelOnError: _cancelOnError, + ); + } catch (error) { + debugPrint( + "Failed to initialize ${control.type} sensor stream: $error"); + } + } + + @override + void dispose() { + _subscription?.cancel(); + _subscription = null; + super.dispose(); + } +} + +class AccelerometerService extends _SensorStreamService { + AccelerometerService({required super.control}); + + @override + Stream sensorStream(Duration samplingPeriod) { + return accelerometerEventStream(samplingPeriod: samplingPeriod); + } + + @override + Map serializeEvent(AccelerometerEvent event) { + return { + "x": event.x, + "y": event.y, + "z": event.z, + "timestamp": event.timestamp.microsecondsSinceEpoch, + }; + } +} + +class UserAccelerometerService + extends _SensorStreamService { + UserAccelerometerService({required super.control}); + + @override + Stream sensorStream(Duration samplingPeriod) { + return userAccelerometerEventStream(samplingPeriod: samplingPeriod); + } + + @override + Map serializeEvent(UserAccelerometerEvent event) { + return { + "x": event.x, + "y": event.y, + "z": event.z, + "timestamp": event.timestamp.microsecondsSinceEpoch, + }; + } +} + +class GyroscopeService extends _SensorStreamService { + GyroscopeService({required super.control}); + + @override + Stream sensorStream(Duration samplingPeriod) { + return gyroscopeEventStream(samplingPeriod: samplingPeriod); + } + + @override + Map serializeEvent(GyroscopeEvent event) { + return { + "x": event.x, + "y": event.y, + "z": event.z, + "timestamp": event.timestamp.microsecondsSinceEpoch, + }; + } +} + +class MagnetometerService extends _SensorStreamService { + MagnetometerService({required super.control}); + + @override + Stream sensorStream(Duration samplingPeriod) { + return magnetometerEventStream(samplingPeriod: samplingPeriod); + } + + @override + Map serializeEvent(MagnetometerEvent event) { + return { + "x": event.x, + "y": event.y, + "z": event.z, + "timestamp": event.timestamp.microsecondsSinceEpoch, + }; + } +} + +class BarometerService extends _SensorStreamService { + BarometerService({required super.control}); + + @override + Stream sensorStream(Duration samplingPeriod) { + return barometerEventStream(samplingPeriod: samplingPeriod); + } + + @override + Map serializeEvent(BarometerEvent event) { + return { + "pressure": event.pressure, + "timestamp": event.timestamp.microsecondsSinceEpoch, + }; + } +} diff --git a/sdk/python/examples/controls/sensors/accelerometer/basic.py b/sdk/python/examples/controls/sensors/accelerometer/basic.py new file mode 100644 index 0000000000..ce2159bba3 --- /dev/null +++ b/sdk/python/examples/controls/sensors/accelerometer/basic.py @@ -0,0 +1,28 @@ +import flet as ft + + +def main(page: ft.Page): + intro = ft.Text("Move your device to see accelerometer readings.") + reading = ft.Text("Waiting for data...") + + def handle_reading(e: ft.AccelerometerReadingEvent): + reading.value = f"x={e.x:.2f} m/s^2, y={e.y:.2f} m/s^2, z={e.z:.2f} m/s^2" + page.update() + + def handle_error(e: ft.SensorErrorEvent): + page.add(ft.Text(f"Accelerometer error: {e.message}")) + + page.session.store.set( + "accelerometer_service", + ft.Accelerometer( + on_reading=handle_reading, + on_error=handle_error, + interval=ft.Duration(milliseconds=100), + cancel_on_error=False, + ), + ) + + page.add(intro, reading) + + +ft.run(main) diff --git a/sdk/python/examples/controls/sensors/barometer/basic.py b/sdk/python/examples/controls/sensors/barometer/basic.py new file mode 100644 index 0000000000..d593133f75 --- /dev/null +++ b/sdk/python/examples/controls/sensors/barometer/basic.py @@ -0,0 +1,27 @@ +import flet as ft + + +def main(page: ft.Page): + intro = ft.Text("Atmospheric pressure (hPa).") + reading = ft.Text("Waiting for data...") + + def handle_reading(e: ft.BarometerReadingEvent): + reading.value = f"{e.pressure:.2f} hPa" + page.update() + + def handle_error(e: ft.SensorErrorEvent): + page.add(ft.Text(f"Barometer error: {e.message}")) + + page.session.store.set( + "barometer_service", + ft.Barometer( + on_reading=handle_reading, + on_error=handle_error, + interval=ft.Duration(milliseconds=500), + ), + ) + + page.add(intro, reading) + + +ft.run(main) diff --git a/sdk/python/examples/controls/sensors/gyroscope/basic.py b/sdk/python/examples/controls/sensors/gyroscope/basic.py new file mode 100644 index 0000000000..5c3d090723 --- /dev/null +++ b/sdk/python/examples/controls/sensors/gyroscope/basic.py @@ -0,0 +1,27 @@ +import flet as ft + + +def main(page: ft.Page): + intro = ft.Text("Rotate your device to see gyroscope readings.") + reading = ft.Text("Waiting for data...") + + def handle_reading(e: ft.GyroscopeReadingEvent): + reading.value = f"x={e.x:.2f} rad/s, y={e.y:.2f} rad/s, z={e.z:.2f} rad/s" + page.update() + + def handle_error(e: ft.SensorErrorEvent): + page.add(ft.Text(f"Gyroscope error: {e.message}")) + + page.session.store.set( + "gyroscope_service", + ft.Gyroscope( + on_reading=handle_reading, + on_error=handle_error, + interval=ft.Duration(milliseconds=50), + ), + ) + + page.add(intro, reading) + + +ft.run(main) diff --git a/sdk/python/examples/controls/sensors/magnetometer/basic.py b/sdk/python/examples/controls/sensors/magnetometer/basic.py new file mode 100644 index 0000000000..bf8ba903d9 --- /dev/null +++ b/sdk/python/examples/controls/sensors/magnetometer/basic.py @@ -0,0 +1,27 @@ +import flet as ft + + +def main(page: ft.Page): + intro = ft.Text("Monitor the ambient magnetic field (uT).") + reading = ft.Text("Waiting for data...") + + def handle_reading(e: ft.MagnetometerReadingEvent): + reading.value = f"x={e.x:.2f} uT, y={e.y:.2f} uT, z={e.z:.2f} uT" + page.update() + + def handle_error(e: ft.SensorErrorEvent): + page.add(ft.Text(f"Magnetometer error: {e.message}")) + + page.session.store.set( + "magnetometer_service", + ft.Magnetometer( + on_reading=handle_reading, + on_error=handle_error, + interval=ft.Duration(milliseconds=200), + ), + ) + + page.add(intro, reading) + + +ft.run(main) diff --git a/sdk/python/examples/controls/sensors/user_accelerometer/basic.py b/sdk/python/examples/controls/sensors/user_accelerometer/basic.py new file mode 100644 index 0000000000..1f0d31ab87 --- /dev/null +++ b/sdk/python/examples/controls/sensors/user_accelerometer/basic.py @@ -0,0 +1,30 @@ +import flet as ft + + +def main(page: ft.Page): + intro = ft.Text( + "Linear acceleration without gravity. " + "Keep the app running on a device with motion sensors." + ) + reading = ft.Text("Waiting for data...") + + def handle_reading(e: ft.UserAccelerometerReadingEvent): + reading.value = f"x={e.x:.2f} m/s^2, y={e.y:.2f} m/s^2, z={e.z:.2f} m/s^2" + page.update() + + def handle_error(e: ft.SensorErrorEvent): + page.add(ft.Text(f"UserAccelerometer error: {e.message}")) + + page.session.store.set( + "user_accelerometer_service", + ft.UserAccelerometer( + on_reading=handle_reading, + on_error=handle_error, + interval=ft.Duration(milliseconds=100), + ), + ) + + page.add(intro, reading) + + +ft.run(main) diff --git a/sdk/python/packages/flet/docs/controls/accelerometer.md b/sdk/python/packages/flet/docs/controls/accelerometer.md new file mode 100644 index 0000000000..badc93a570 --- /dev/null +++ b/sdk/python/packages/flet/docs/controls/accelerometer.md @@ -0,0 +1,14 @@ +--- +class_name: flet.Accelerometer +examples: ../../examples/controls/sensors/accelerometer +--- + +{{ class_summary(class_name) }} + +## Examples + +```python +--8<-- "{{ examples }}/basic.py" +``` + +{{ class_members(class_name) }} diff --git a/sdk/python/packages/flet/docs/controls/barometer.md b/sdk/python/packages/flet/docs/controls/barometer.md new file mode 100644 index 0000000000..154b46056d --- /dev/null +++ b/sdk/python/packages/flet/docs/controls/barometer.md @@ -0,0 +1,14 @@ +--- +class_name: flet.Barometer +examples: ../../examples/controls/sensors/barometer +--- + +{{ class_summary(class_name) }} + +## Examples + +```python +--8<-- "{{ examples }}/basic.py" +``` + +{{ class_members(class_name) }} diff --git a/sdk/python/packages/flet/docs/controls/gyroscope.md b/sdk/python/packages/flet/docs/controls/gyroscope.md new file mode 100644 index 0000000000..2635bb8cb0 --- /dev/null +++ b/sdk/python/packages/flet/docs/controls/gyroscope.md @@ -0,0 +1,14 @@ +--- +class_name: flet.Gyroscope +examples: ../../examples/controls/sensors/gyroscope +--- + +{{ class_summary(class_name) }} + +## Examples + +```python +--8<-- "{{ examples }}/basic.py" +``` + +{{ class_members(class_name) }} diff --git a/sdk/python/packages/flet/docs/controls/magnetometer.md b/sdk/python/packages/flet/docs/controls/magnetometer.md new file mode 100644 index 0000000000..f81db5356f --- /dev/null +++ b/sdk/python/packages/flet/docs/controls/magnetometer.md @@ -0,0 +1,14 @@ +--- +class_name: flet.Magnetometer +examples: ../../examples/controls/sensors/magnetometer +--- + +{{ class_summary(class_name) }} + +## Examples + +```python +--8<-- "{{ examples }}/basic.py" +``` + +{{ class_members(class_name) }} diff --git a/sdk/python/packages/flet/docs/controls/useraccelerometer.md b/sdk/python/packages/flet/docs/controls/useraccelerometer.md new file mode 100644 index 0000000000..9e2b9d496a --- /dev/null +++ b/sdk/python/packages/flet/docs/controls/useraccelerometer.md @@ -0,0 +1,14 @@ +--- +class_name: flet.UserAccelerometer +examples: ../../examples/controls/sensors/user_accelerometer +--- + +{{ class_summary(class_name) }} + +## Examples + +```python +--8<-- "{{ examples }}/basic.py" +``` + +{{ class_members(class_name) }} diff --git a/sdk/python/packages/flet/docs/types/accelerometerreadingevent.md b/sdk/python/packages/flet/docs/types/accelerometerreadingevent.md new file mode 100644 index 0000000000..9daec207e2 --- /dev/null +++ b/sdk/python/packages/flet/docs/types/accelerometerreadingevent.md @@ -0,0 +1,5 @@ +--- +class_name: flet.AccelerometerReadingEvent +--- + +{{ class_all_options(class_name) }} diff --git a/sdk/python/packages/flet/docs/types/barometerreadingevent.md b/sdk/python/packages/flet/docs/types/barometerreadingevent.md new file mode 100644 index 0000000000..ebed8768c0 --- /dev/null +++ b/sdk/python/packages/flet/docs/types/barometerreadingevent.md @@ -0,0 +1,5 @@ +--- +class_name: flet.BarometerReadingEvent +--- + +{{ class_all_options(class_name) }} diff --git a/sdk/python/packages/flet/docs/types/gyroscopereadingevent.md b/sdk/python/packages/flet/docs/types/gyroscopereadingevent.md new file mode 100644 index 0000000000..f6b563fffb --- /dev/null +++ b/sdk/python/packages/flet/docs/types/gyroscopereadingevent.md @@ -0,0 +1,5 @@ +--- +class_name: flet.GyroscopeReadingEvent +--- + +{{ class_all_options(class_name) }} diff --git a/sdk/python/packages/flet/docs/types/magnetometerreadingevent.md b/sdk/python/packages/flet/docs/types/magnetometerreadingevent.md new file mode 100644 index 0000000000..765426b66b --- /dev/null +++ b/sdk/python/packages/flet/docs/types/magnetometerreadingevent.md @@ -0,0 +1,5 @@ +--- +class_name: flet.MagnetometerReadingEvent +--- + +{{ class_all_options(class_name) }} diff --git a/sdk/python/packages/flet/docs/types/sensorerrorevent.md b/sdk/python/packages/flet/docs/types/sensorerrorevent.md new file mode 100644 index 0000000000..0e483bd3a2 --- /dev/null +++ b/sdk/python/packages/flet/docs/types/sensorerrorevent.md @@ -0,0 +1,5 @@ +--- +class_name: flet.SensorErrorEvent +--- + +{{ class_all_options(class_name) }} diff --git a/sdk/python/packages/flet/docs/types/useraccelerometerreadingevent.md b/sdk/python/packages/flet/docs/types/useraccelerometerreadingevent.md new file mode 100644 index 0000000000..9e8c946ff5 --- /dev/null +++ b/sdk/python/packages/flet/docs/types/useraccelerometerreadingevent.md @@ -0,0 +1,5 @@ +--- +class_name: flet.UserAccelerometerReadingEvent +--- + +{{ class_all_options(class_name) }} diff --git a/sdk/python/packages/flet/mkdocs.yml b/sdk/python/packages/flet/mkdocs.yml index ee2f3985c9..6abc86c102 100644 --- a/sdk/python/packages/flet/mkdocs.yml +++ b/sdk/python/packages/flet/mkdocs.yml @@ -266,6 +266,7 @@ nav: # - NativeAd: ads/nativead.md - Audio: audio/index.md - AudioRecorder: audio_recorder/index.md + - Accelerometer: controls/accelerometer.md - AlertDialog: controls/alertdialog.md - AnimatedSwitcher: controls/animatedswitcher.md - AppBar: controls/appbar.md @@ -273,6 +274,7 @@ nav: - AutofillGroup: controls/autofillgroup.md - Badge: types/badge.md - Banner: controls/banner.md + - Barometer: controls/barometer.md - BottomAppBar: controls/bottomappbar.md - BottomSheet: controls/bottomsheet.md - Button: controls/button.md @@ -367,6 +369,7 @@ nav: - GestureDetector: controls/gesturedetector.md - Geolocator: geolocator/index.md - GridView: controls/gridview.md + - Gyroscope: controls/gyroscope.md - HapticFeedback: controls/hapticfeedback.md - Icon: controls/icon.md - IconButton: controls/iconbutton.md @@ -398,6 +401,7 @@ nav: - TextSourceAttribution: map/text_source_attribution.md - ImageSourceAttribution: map/image_source_attribution.md - Markdown: controls/markdown.md + - Magnetometer: controls/magnetometer.md - MenuBar: controls/menubar.md - MenuItemButton: controls/menuitembutton.md - MergeSemantics: controls/mergesemantics.md @@ -452,6 +456,7 @@ nav: - TextField: controls/textfield.md - TimePicker: controls/timepicker.md - TransparentPointer: controls/transparentpointer.md + - UserAccelerometer: controls/useraccelerometer.md - VerticalDivider: controls/verticaldivider.md - Video: video/index.md - View: controls/view.md @@ -833,10 +838,12 @@ nav: - WindowEventType: types/windoweventtype.md - WindowResizeEdge: types/windowresizeedge.md - Events: + - AccelerometerReadingEvent: types/accelerometerreadingevent.md - Ads: - PaidAdRequest: ads/types/paidadevent.md - AppLifecycleStateChangeEvent: types/applifecyclestatechangeevent.md - AutoCompleteSelectEvent: types/autocompleteselectevent.md + - BarometerReadingEvent: types/barometerreadingevent.md - CanvasResizeEvent: types/canvasresizeevent.md - ContextMenuDismissEvent: types/contextmenudismissevent.md - ContextMenuSelectEvent: types/contextmenuselectevent.md @@ -852,6 +859,7 @@ nav: - DragWillAcceptEvent: types/dragwillacceptevent.md - Event: types/event.md - FilePickerUploadEvent: types/filepickeruploadevent.md + - GyroscopeReadingEvent: types/gyroscopereadingevent.md - HoverEvent: types/hoverevent.md - KeyboardEvent: types/keyboardevent.md - KeyDownEvent: types/keydownevent.md @@ -860,6 +868,7 @@ nav: - LoginEvent: types/loginevent.md - LongPressEndEvent: types/longpressendevent.md - LongPressStartEvent: types/longpressstartevent.md + - MagnetometerReadingEvent: types/magnetometerreadingevent.md - MultiTapEvent: types/multitapevent.md - MultiViewAddEvent: types/multiviewaddevent.md - MultiViewRemoveEvent: types/multiviewremoveevent.md @@ -870,6 +879,7 @@ nav: - PlatformBrightnessChangeEvent: types/platformbrightnesschangeevent.md - PointerEvent: types/pointerevent.md - RouteChangeEvent: types/routechangeevent.md + - SensorErrorEvent: types/sensorerrorevent.md - ScaleEndEvent: types/scaleendevent.md - ScaleStartEvent: types/scalestartevent.md - ScaleUpdateEvent: types/scaleupdateevent.md @@ -878,6 +888,7 @@ nav: - TapEvent: types/tapevent.md - TextSelectionChangeEvent: types/textselectionchangeevent.md - TimePickerEntryModeChangeEvent: types/timepickerentrymodechangeevent.md + - UserAccelerometerReadingEvent: types/useraccelerometerreadingevent.md - ViewPopEvent: types/viewpopevent.md - WindowEvent: types/windowevent.md - Exceptions: diff --git a/sdk/python/packages/flet/src/flet/__init__.py b/sdk/python/packages/flet/src/flet/__init__.py index e2c8c87976..160ccc5e56 100644 --- a/sdk/python/packages/flet/src/flet/__init__.py +++ b/sdk/python/packages/flet/src/flet/__init__.py @@ -413,6 +413,8 @@ ScrollDirection, ScrollType, ) +from flet.controls.services.accelerometer import Accelerometer +from flet.controls.services.barometer import Barometer from flet.controls.services.browser_context_menu import BrowserContextMenu from flet.controls.services.clipboard import Clipboard from flet.controls.services.file_picker import ( @@ -422,13 +424,24 @@ FilePickerUploadEvent, FilePickerUploadFile, ) +from flet.controls.services.gyroscope import Gyroscope from flet.controls.services.haptic_feedback import HapticFeedback +from flet.controls.services.magnetometer import Magnetometer from flet.controls.services.semantics_service import Assertiveness, SemanticsService +from flet.controls.services.sensor_events import ( + AccelerometerReadingEvent, + BarometerReadingEvent, + GyroscopeReadingEvent, + MagnetometerReadingEvent, + SensorErrorEvent, + UserAccelerometerReadingEvent, +) from flet.controls.services.service import Service from flet.controls.services.shake_detector import ShakeDetector from flet.controls.services.shared_preferences import SharedPreferences from flet.controls.services.storage_paths import StoragePaths from flet.controls.services.url_launcher import UrlLauncher +from flet.controls.services.user_accelerometer import UserAccelerometer from flet.controls.template_route import TemplateRoute from flet.controls.text_style import ( StrutStyle, @@ -538,6 +551,8 @@ from flet.pubsub.pubsub_hub import PubSubHub __all__ = [ + "Accelerometer", + "AccelerometerReadingEvent", "AdaptiveControl", "AlertDialog", "Alignment", @@ -568,6 +583,8 @@ "BadgeValue", "Banner", "BannerTheme", + "Barometer", + "BarometerReadingEvent", "BaseControl", "BasePage", "BeveledRectangleBorder", @@ -733,6 +750,8 @@ "Gradient", "GradientTileMode", "GridView", + "Gyroscope", + "GyroscopeReadingEvent", "HapticFeedback", "HoverEvent", "Icon", @@ -775,6 +794,8 @@ "LongPressMoveUpdateEvent", "LongPressStartEvent", "MacOsDeviceInfo", + "Magnetometer", + "MagnetometerReadingEvent", "MainAxisAlignment", "Margin", "MarginValue", @@ -887,6 +908,7 @@ "SelectionArea", "Semantics", "SemanticsService", + "SensorErrorEvent", "Service", "ShaderMask", "ShakeDetector", @@ -962,6 +984,8 @@ "Url", "UrlLauncher", "UrlTarget", + "UserAccelerometer", + "UserAccelerometerReadingEvent", "ValueKey", "VerticalAlignment", "VerticalDivider", diff --git a/sdk/python/packages/flet/src/flet/controls/services/accelerometer.py b/sdk/python/packages/flet/src/flet/controls/services/accelerometer.py new file mode 100644 index 0000000000..6d4836c394 --- /dev/null +++ b/sdk/python/packages/flet/src/flet/controls/services/accelerometer.py @@ -0,0 +1,71 @@ +from typing import Optional + +from flet.controls.base_control import control +from flet.controls.control_event import EventHandler +from flet.controls.duration import Duration +from flet.controls.services.sensor_events import ( + AccelerometerReadingEvent, + SensorErrorEvent, +) +from flet.controls.services.service import Service + +__all__ = ["Accelerometer"] + + +@control("Accelerometer") +class Accelerometer(Service): + """ + Streams raw accelerometer [readings][flet.AccelerometerReadingEvent], + which describe the acceleration of the device, in `m/s^2`, including + the effects of gravity. + + Unlike [UserAccelerometer][flet.], + this service reports raw data from the accelerometer (physical sensor + embedded in the mobile device) without any post-processing. + + The accelerometer is unable to distinguish between the effect of an + accelerated movement of the device and the effect of the surrounding + gravitational field. This means that, at the surface of Earth, + even if the device is completely still, the reading of [`Accelerometer`][flet.] + is an acceleration of intensity 9.8 directed upwards (the opposite of + the graviational acceleration). This can be used to infer information + about the position of the device (horizontal/vertical/tilted). + Accelerometer reports zero acceleration if the device is free falling. + + Note: + Supported platforms: Android, iOS, Web. Web ignores requested sampling + intervals and iOS apps must declare `NSMotionUsageDescription`. + """ + + enabled: bool = True + """ + Whether the sensor should be sampled. Disable to stop streaming. + """ + + interval: Optional[Duration] = None + """ + Desired sampling interval provided as a [`Duration`][flet.Duration]. + Defaults to 200 ms. + + Note that mobile platforms treat this value as a suggestion and the actual + rate can differ depending on hardware and OS limitations. + """ + + cancel_on_error: bool = True + """ + Whether the stream subscription should cancel on the first sensor error. + """ + + on_reading: Optional[EventHandler[AccelerometerReadingEvent]] = None + """ + Fires when a new reading is available. + + `event` exposes `x`, `y`, `z` acceleration values and `timestamp` + (microseconds since epoch). + """ + + on_error: Optional[EventHandler[SensorErrorEvent]] = None + """ + Fired when the platform reports a sensor error (for example when the device + does not expose the accelerometer). `event.message` contains the error text. + """ diff --git a/sdk/python/packages/flet/src/flet/controls/services/barometer.py b/sdk/python/packages/flet/src/flet/controls/services/barometer.py new file mode 100644 index 0000000000..07bde35342 --- /dev/null +++ b/sdk/python/packages/flet/src/flet/controls/services/barometer.py @@ -0,0 +1,56 @@ +from typing import Optional + +from flet.controls.base_control import control +from flet.controls.control_event import EventHandler +from flet.controls.duration import Duration +from flet.controls.services.sensor_events import ( + BarometerReadingEvent, + SensorErrorEvent, +) +from flet.controls.services.service import Service + +__all__ = ["Barometer"] + + +@control("Barometer") +class Barometer(Service): + """ + Streams barometer [readings][flet.BarometerReadingEvent] + (atmospheric pressure in `hPa`). Useful for altitude calculations + and weather-related experiences. + + Note: + Supported platforms: Android, iOS. Barometer APIs are not exposed on the Web + or desktop platforms and iOS ignores custom sampling intervals. + """ + + enabled: bool = True + """ + Whether the sensor should be sampled. Disable to stop streaming. + """ + + interval: Optional[Duration] = None + """ + Desired sampling interval provided as a [`Duration`][flet.Duration]. + Defaults to 200 ms, though + some platforms (such as iOS) ignore custom sampling intervals. + """ + + cancel_on_error: bool = True + """ + Whether the stream subscription should cancel on the first sensor error. + """ + + on_reading: Optional[EventHandler[BarometerReadingEvent]] = None + """ + Fires when a new reading is available. + + `event` contains `pressure` (hPa) and `timestamp` (microseconds + since epoch). + """ + + on_error: Optional[EventHandler[SensorErrorEvent]] = None + """ + Fired when the platform reports a sensor error. `event.message` is the error + description. + """ diff --git a/sdk/python/packages/flet/src/flet/controls/services/gyroscope.py b/sdk/python/packages/flet/src/flet/controls/services/gyroscope.py new file mode 100644 index 0000000000..90f31ce436 --- /dev/null +++ b/sdk/python/packages/flet/src/flet/controls/services/gyroscope.py @@ -0,0 +1,54 @@ +from typing import Optional + +from flet.controls.base_control import control +from flet.controls.control_event import EventHandler +from flet.controls.duration import Duration +from flet.controls.services.sensor_events import ( + GyroscopeReadingEvent, + SensorErrorEvent, +) +from flet.controls.services.service import Service + +__all__ = ["Gyroscope"] + + +@control("Gyroscope") +class Gyroscope(Service): + """ + Streams gyroscope [readings][flet.GyroscopeReadingEvent], + reporting device rotation rate around each axis in `rad/s`. + + Note: + Supported platforms: Android, iOS, Web (sampling interval + hints are ignored on Web). + """ + + enabled: bool = True + """ + Whether the sensor should be sampled. Disable to stop streaming. + """ + + interval: Optional[Duration] = None + """ + Desired sampling interval provided as a [`Duration`][flet.Duration]. + Defaults to 200 ms. + """ + + cancel_on_error: bool = True + """ + Whether the stream subscription should cancel on the first sensor error. + """ + + on_reading: Optional[EventHandler[GyroscopeReadingEvent]] = None + """ + Fires when a new reading is available. + + `event` contains `x`, `y`, `z` rotation rates and `timestamp` + (microseconds since epoch). + """ + + on_error: Optional[EventHandler[SensorErrorEvent]] = None + """ + Fired when the platform reports a sensor error. `event.message` is the error + description. + """ diff --git a/sdk/python/packages/flet/src/flet/controls/services/magnetometer.py b/sdk/python/packages/flet/src/flet/controls/services/magnetometer.py new file mode 100644 index 0000000000..51a6973068 --- /dev/null +++ b/sdk/python/packages/flet/src/flet/controls/services/magnetometer.py @@ -0,0 +1,55 @@ +from typing import Optional + +from flet.controls.base_control import control +from flet.controls.control_event import EventHandler +from flet.controls.duration import Duration +from flet.controls.services.sensor_events import ( + MagnetometerReadingEvent, + SensorErrorEvent, +) +from flet.controls.services.service import Service + +__all__ = ["Magnetometer"] + + +@control("Magnetometer") +class Magnetometer(Service): + """ + Streams magnetometer [readings][flet.MagnetometerReadingEvent] + reporting the ambient magnetic field (`uT`) per axis for compass-style + use cases. + + Note: + Supported platforms: Android, iOS. Magnetometer APIs are not available on Web + or desktop, so always handle `on_error` to detect unsupported hardware. + """ + + enabled: bool = True + """ + Whether the sensor should be sampled. Disable to stop streaming. + """ + + interval: Optional[Duration] = None + """ + Desired sampling interval provided as a [`Duration`][flet.Duration]. + Defaults to 200 ms. + """ + + cancel_on_error: bool = True + """ + Whether the stream subscription should cancel on the first sensor error. + """ + + on_reading: Optional[EventHandler[MagnetometerReadingEvent]] = None + """ + Fires when a new reading is available. + + `event` contains `x`, `y`, `z` magnetic field strengths (uT) + and `timestamp` (microseconds since epoch). + """ + + on_error: Optional[EventHandler[SensorErrorEvent]] = None + """ + Fired when the platform reports a sensor error. `event.message` is the error + description. + """ diff --git a/sdk/python/packages/flet/src/flet/controls/services/sensor_events.py b/sdk/python/packages/flet/src/flet/controls/services/sensor_events.py new file mode 100644 index 0000000000..3464084b7b --- /dev/null +++ b/sdk/python/packages/flet/src/flet/controls/services/sensor_events.py @@ -0,0 +1,153 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING + +from flet.controls.control_event import Event, EventControlType + +if TYPE_CHECKING: + from flet.controls.services.accelerometer import Accelerometer # noqa: F401 + from flet.controls.services.barometer import Barometer # noqa: F401 + from flet.controls.services.gyroscope import Gyroscope # noqa: F401 + from flet.controls.services.magnetometer import Magnetometer # noqa: F401 + from flet.controls.services.user_accelerometer import ( + UserAccelerometer, # noqa: F401 + ) + +__all__ = [ + "AccelerometerReadingEvent", + "BarometerReadingEvent", + "GyroscopeReadingEvent", + "MagnetometerReadingEvent", + "SensorErrorEvent", + "UserAccelerometerReadingEvent", +] + + +@dataclass(kw_only=True) +class AccelerometerReadingEvent(Event["Accelerometer"]): + """ + Discrete reading from an accelerometer. Accelerometers measure the velocity + of the device. Note that these readings include the effects of gravity. + Put simply, you can use accelerometer readings to tell if the device + is moving in a particular direction. + """ + + x: float + """Acceleration along the X axis, in `m/s^2`.""" + + y: float + """Acceleration along the Y axis, in `m/s^2`.""" + + z: float + """Acceleration along the Z axis, in `m/s^2`.""" + + timestamp: int + """Event timestamp, expressed in microseconds since epoch.""" + + +@dataclass(kw_only=True) +class UserAccelerometerReadingEvent(Event["UserAccelerometer"]): + """ + Like [`AccelerometerReadingEvent`][flet.], this is a discrete reading from + an accelerometer and measures the velocity of the device. However, + unlike [`AccelerometerReadingEvent`][flet.], this event does not include + the effects of gravity. + """ + + x: float + """Linear acceleration along the X axis, gravity removed, in `m/s^2`.""" + + y: float + """Linear acceleration along the Y axis, gravity removed, in `m/s^2`.""" + + z: float + """Linear acceleration along the Z axis, gravity removed, in `m/s^2`.""" + + timestamp: int + """Event timestamp, expressed in microseconds since epoch.""" + + +@dataclass(kw_only=True) +class GyroscopeReadingEvent(Event["Gyroscope"]): + """ + Discrete reading from a gyroscope. + + Gyroscope sample containing device rotation rate (`rad/s`) around each + axis plus the microsecond timestamp. + """ + + x: float + """Rotation rate around the X axis, in `rad/s`.""" + + y: float + """Rotation rate around the Y axis, in `rad/s`.""" + + z: float + """Rotation rate around the Z axis, in `rad/s`.""" + + timestamp: int + """Event timestamp, expressed in microseconds since epoch.""" + + +@dataclass(kw_only=True) +class MagnetometerReadingEvent(Event["Magnetometer"]): + """ + A sensor sample from a magnetometer. + + Magnetometers measure the ambient magnetic field surrounding the sensor, + returning values in microteslas `μT` for each three-dimensional axis. + + Consider that these samples may bear effects of Earth's magnetic field + as well as local factors such as the metal of the device itself + or nearby magnets, though most devices compensate for these factors. + + A compass is an example of a general utility for magnetometer data. + """ + + x: float + """Ambient magnetic field on the X axis, in microteslas (`uT`).""" + + y: float + """Ambient magnetic field on the Y axis, in `uT`.""" + + z: float + """Ambient magnetic field on the Z axis, in `uT`.""" + + timestamp: int + """Event timestamp, expressed in microseconds since epoch.""" + + +@dataclass(kw_only=True) +class BarometerReadingEvent(Event["Barometer"]): + """ + A sensor sample from a barometer. + + Barometers measure the atmospheric pressure surrounding the sensor, + returning values in hectopascals `hPa`. + + Consider that these samples may be affected by altitude and weather conditions, + and can be used to predict short-term weather changes or determine altitude. + + Note that water-resistant phones or similar sealed devices may experience + pressure fluctuations as the device is held or used, due to changes + in pressure caused by handling the device. + + An altimeter is an example of a general utility for barometer data. + """ + + pressure: float + """Atmospheric pressure reading, in hectopascals (`hPa`).""" + + timestamp: int + """Event timestamp, expressed in microseconds since epoch.""" + + +@dataclass(kw_only=True) +class SensorErrorEvent(Event[EventControlType]): + """ + Generic sensor error event. `message` contains the platform error text. + """ + + message: str + """Human-readable description of the sensor error.""" diff --git a/sdk/python/packages/flet/src/flet/controls/services/user_accelerometer.py b/sdk/python/packages/flet/src/flet/controls/services/user_accelerometer.py new file mode 100644 index 0000000000..0e0c93e964 --- /dev/null +++ b/sdk/python/packages/flet/src/flet/controls/services/user_accelerometer.py @@ -0,0 +1,61 @@ +from typing import Optional + +from flet.controls.base_control import control +from flet.controls.control_event import EventHandler +from flet.controls.duration import Duration +from flet.controls.services.sensor_events import ( + SensorErrorEvent, + UserAccelerometerReadingEvent, +) +from flet.controls.services.service import Service + +__all__ = ["UserAccelerometer"] + + +@control("UserAccelerometer") +class UserAccelerometer(Service): + """ + Streams linear acceleration readings. + + If the device is still, or is moving along a straight line at constant speed, + the reported acceleration is zero. If the device is moving e.g. towards north + and its speed is increasing, the reported acceleration is towards north; + if it is slowing down, the reported acceleration is towards south; + if it is turning right, the reported acceleration is towards east. + The data of this stream is obtained by filtering out the effect of gravity + from [`AccelerometerReadingEvent`][flet.]. + + Note: + Supported platforms: Android, iOS, Web. Web ignores requested sampling + intervals and iOS apps must declare `NSMotionUsageDescription`. + """ + + enabled: bool = True + """ + Whether the sensor should be sampled. Disable to stop streaming. + """ + + interval: Optional[Duration] = None + """ + Desired sampling interval provided as a [`Duration`][flet.Duration]. + Defaults to 200 ms. + """ + + cancel_on_error: bool = True + """ + Whether the stream subscription should cancel on the first sensor error. + """ + + on_reading: Optional[EventHandler[UserAccelerometerReadingEvent]] = None + """ + Fires when a new reading is available. + + `event` contains `x`, `y`, `z` acceleration values and `timestamp` + (microseconds since epoch). + """ + + on_error: Optional[EventHandler[SensorErrorEvent]] = None + """ + Fired when the platform reports a sensor error. `event.message` is the error + description. + """ From d393a6dd10bcdce65c6bfc51699d5e89f3e4e323 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 21 Nov 2025 18:01:09 -0800 Subject: [PATCH 2/2] Refactor sensors service imports and formatting Added import for numbers utility in sensors.dart and improved code formatting for error handling and debug print statements. --- packages/flet/lib/src/services/sensors.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/flet/lib/src/services/sensors.dart b/packages/flet/lib/src/services/sensors.dart index 01745fc1ee..2694cca56f 100644 --- a/packages/flet/lib/src/services/sensors.dart +++ b/packages/flet/lib/src/services/sensors.dart @@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:sensors_plus/sensors_plus.dart'; import '../flet_service.dart'; +import '../utils/numbers.dart'; import '../utils/time.dart'; abstract class _SensorStreamService extends FletService { @@ -80,8 +81,8 @@ abstract class _SensorStreamService extends FletService { }, onError: (error, stackTrace) { if (_hasErrorSubscribers) { - control.triggerEvent( - "error", {"message": error?.toString() ?? "Unknown sensor error"}); + control.triggerEvent("error", + {"message": error?.toString() ?? "Unknown sensor error"}); } else { debugPrint( "Error listening to ${control.type} sensor stream: $error"); @@ -90,8 +91,7 @@ abstract class _SensorStreamService extends FletService { cancelOnError: _cancelOnError, ); } catch (error) { - debugPrint( - "Failed to initialize ${control.type} sensor stream: $error"); + debugPrint("Failed to initialize ${control.type} sensor stream: $error"); } }