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..2694cca56f --- /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/numbers.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. + """