Skip to content
Open

1.6.0 #305

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
927cec0
chore(version): bump version to 1.5.4
github-actions[bot] Oct 1, 2026
6365a11
fix(posture): refresh reminder settings immediately (#306)
TobiasRoeddiger Oct 2, 2026
7fe7aee
fix(seal-check): save Android JSON through document picker (#310)
TobiasRoeddiger Oct 2, 2026
63da401
Share sensor configuration reports with network clients (#309)
TobiasRoeddiger Oct 2, 2026
8443a47
fix(connector): retain device inventory while endpoint is disabled (#…
TobiasRoeddiger Oct 2, 2026
cc891da
Handle development versions in firmware update checks (#307)
TobiasRoeddiger Oct 2, 2026
6525a06
Recover missing ears while preserving existing connections (#311)
TobiasRoeddiger Oct 2, 2026
4d34a93
fix: sort sensor sampling rates numerically
TobiasRoeddiger Oct 2, 2026
f9e0ba5
fix(recording): resume ear streams while the phone is locked
TobiasRoeddiger Oct 2, 2026
15d0ba7
fix(recording): validate device filename prefixes and report write fa…
TobiasRoeddiger Oct 3, 2026
b1ee6bb
fix(battery): use corrected shared status decoder
TobiasRoeddiger Oct 3, 2026
903589b
fix: retry unknown stereo positions after reconnect
TobiasRoeddiger Oct 3, 2026
526236d
fix(fota): verify early reconnects and finish verification state
TobiasRoeddiger Oct 3, 2026
6a0e76d
fix(fota): use preparation failure recovery from the shared library
TobiasRoeddiger Oct 3, 2026
04a8146
fix(fota): show the reason an update failed
TobiasRoeddiger Oct 3, 2026
cddb975
fix: clear selected ear sensors before starting trackers
TobiasRoeddiger Oct 3, 2026
5123f9c
fix: use Bluetooth adapter shutdown cleanup
TobiasRoeddiger Oct 3, 2026
2cd9926
fix: include native Bluetooth shutdown cleanup
TobiasRoeddiger Oct 3, 2026
7e7a34c
fix: use completed native connection cleanup
TobiasRoeddiger Oct 3, 2026
361350d
fix(recording): keep Android microphone capture active during music
TobiasRoeddiger Oct 3, 2026
feef971
fix(sensors): accept reported state after applying settings
TobiasRoeddiger Oct 4, 2026
0b5fc56
Merge pull request #312 from OpenEarable/codex/sort-ppg-sampling-rates
TobiasRoeddiger Oct 4, 2026
acc32ce
Merge pull request #314 from OpenEarable/codex/recording-background-r…
TobiasRoeddiger Oct 4, 2026
d9751e4
Merge pull request #315 from OpenEarable/codex/use-battery-decoding-fix
TobiasRoeddiger Oct 4, 2026
6eb6c49
Merge pull request #316 from OpenEarable/codex/validate-recording-prefix
TobiasRoeddiger Oct 4, 2026
ce09979
Merge pull request #317 from OpenEarable/codex/use-fota-preparation-fix
TobiasRoeddiger Oct 4, 2026
b72af93
Merge pull request #318 from OpenEarable/codex/retry-stereo-position
TobiasRoeddiger Oct 4, 2026
ad7b589
Merge pull request #319 from OpenEarable/codex/fota-verification-comp…
TobiasRoeddiger Oct 4, 2026
2a9b2f6
Merge pull request #320 from OpenEarable/codex/preserve-tracker-sensors
TobiasRoeddiger Oct 4, 2026
a8fdb59
Merge pull request #321 from OpenEarable/codex/use-bluetooth-power-cy…
TobiasRoeddiger Oct 4, 2026
e900c56
Merge pull request #322 from OpenEarable/codex/android-recording-with…
TobiasRoeddiger Oct 4, 2026
19dc90e
Merge pull request #323 from OpenEarable/codex/accept-applied-sensor-…
TobiasRoeddiger Oct 4, 2026
5430c8a
chore: prepare app 1.6.0
TobiasRoeddiger Oct 4, 2026
113b7ec
build: use compact PPG compatible library for app 1.6
TobiasRoeddiger Oct 4, 2026
bb4ce3d
Merge pull request #324 from OpenEarable/codex/app-1.6-compact-ppg
TobiasRoeddiger Oct 4, 2026
dac3147
Use lossless compact IMU decoding in app 1.6.0
TobiasRoeddiger Oct 4, 2026
58ef998
Merge pull request #325 from OpenEarable/codex/app-1.6-compact-imu
TobiasRoeddiger Oct 4, 2026
3fbdd25
Select compact sensor decoding using the actual firmware revision
TobiasRoeddiger Oct 4, 2026
3f4929b
Merge pull request #326 from OpenEarable/codex/app-1.6-wire-version-fix
TobiasRoeddiger Oct 4, 2026
92fcdf3
Restore saved LED controls when reopening device details
TobiasRoeddiger Oct 5, 2026
ec4ccb5
Refresh LED readback when the connected device changes
TobiasRoeddiger Oct 5, 2026
aa476ef
Stop posture sensor streaming and discard paused readings
TobiasRoeddiger Oct 5, 2026
47ce0e5
Merge pull request #328 from OpenEarable/codex/posture-stop-stream
TobiasRoeddiger Oct 5, 2026
f30d981
Fix duplicate initialization of shared log output
TobiasRoeddiger Oct 5, 2026
980dfba
Merge pull request #327 from OpenEarable/codex/restore-led-controls
TobiasRoeddiger Oct 5, 2026
d324cb1
Merge pull request #329 from OpenEarable/codex/shared-log-output-life…
TobiasRoeddiger Oct 5, 2026
1f37a33
Handle recording open failures in both recorder views
TobiasRoeddiger Oct 5, 2026
2379123
Merge pull request #330 from OpenEarable/codex/recording-open-error
TobiasRoeddiger Oct 5, 2026
26c5abd
Use Android BLE engine cleanup in app 1.6.0
TobiasRoeddiger Oct 5, 2026
6762a4c
Merge pull request #331 from OpenEarable/codex/android-engine-reconnect
TobiasRoeddiger Oct 5, 2026
6f94ef8
Resume posture tracking after the selected wearable reconnects
TobiasRoeddiger Oct 5, 2026
ebd5496
Merge pull request #332 from OpenEarable/codex/posture-reconnect
TobiasRoeddiger Oct 5, 2026
608a68c
Resume Heart Tracker after wearable reconnect
TobiasRoeddiger Oct 5, 2026
7c9035c
Merge pull request #333 from OpenEarable/codex/heart-reconnect
TobiasRoeddiger Oct 5, 2026
dde64da
Keep Android microphone recordings active in the background (#334)
TobiasRoeddiger Oct 5, 2026
940d707
Match FOTA verification styling and show one completed upload (#336)
TobiasRoeddiger Oct 5, 2026
70f030f
Target the selected image slot during FOTA recovery (#335)
TobiasRoeddiger Oct 5, 2026
cecc41b
Preserve FOTA logs before opening image recovery (#337)
TobiasRoeddiger Oct 5, 2026
5040be9
Use Android MTU completion fix during reconnect
TobiasRoeddiger Oct 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions open_wearable/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data android:name="flutterEmbedding" android:value="2"/>
<service android:name=".AudioRecordingService" android:exported="false" android:foregroundServiceType="microphone"/>
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
Expand Down Expand Up @@ -41,6 +42,7 @@
<uses-permission android:name="android.permission.INTERNET"/>

<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />

<!-- Provide required visibility configuration for API level 30 and above -->
<queries>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package edu.kit.teco.openWearable

import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.os.ResultReceiver

/** Keeps an explicitly started microphone recording audible while the app is backgrounded. */
class AudioRecordingService : Service() {
override fun onBind(intent: Intent?): IBinder? = null

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@Suppress("DEPRECATION")
val receiver = intent?.getParcelableExtra<ResultReceiver>("result")
try {
val channelId = "audio_recording"
val manager = getSystemService(NotificationManager::class.java)
val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
manager.createNotificationChannel(
NotificationChannel(channelId, "Audio recording", NotificationManager.IMPORTANCE_LOW),
)
Notification.Builder(this, channelId)
} else {
@Suppress("DEPRECATION")
Notification.Builder(this)
}
val openApp = PendingIntent.getActivity(
this, 0, Intent(this, MainActivity::class.java),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
val notification = builder
.setSmallIcon(android.R.drawable.ic_btn_speak_now)
.setContentTitle("OpenWearables is recording audio")
.setContentText("Open the app to stop recording.")
.setContentIntent(openApp)
.setOngoing(true)
.setOnlyAlertOnce(true)
.setCategory(Notification.CATEGORY_SERVICE)
.build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
startForeground(104, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE)
} else {
startForeground(104, notification)
}
receiver?.send(0, null)
} catch (error: Exception) {
receiver?.send(1, Bundle().apply { putString("error", error.message) })
stopSelf()
}
return START_NOT_STICKY
}

override fun onDestroy() {
stopForeground(STOP_FOREGROUND_REMOVE)
super.onDestroy()
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package edu.kit.teco.openWearable

import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.ResultReceiver
import android.provider.Settings
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
Expand All @@ -14,6 +19,35 @@ class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)

MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
"edu.kit.teco.openWearable/audio_recording",
).setMethodCallHandler { call, result ->
val intent = Intent(this, AudioRecordingService::class.java)
when (call.method) {
"start" -> {
val receiver = object : ResultReceiver(Handler(Looper.getMainLooper())) {
override fun onReceiveResult(code: Int, data: Bundle?) {
if (code == 0) result.success(null)
else result.error("audio_recording_service", data?.getString("error"), null)
}
}
intent.putExtra("result", receiver)
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) startForegroundService(intent)
else startService(intent)
} catch (error: Exception) {
result.error("audio_recording_service", error.message, null)
}
}
"stop" -> {
stopService(intent)
result.success(null)
}
else -> result.notImplemented()
}
}

MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
SYSTEM_SETTINGS_CHANNEL,
Expand All @@ -33,4 +67,9 @@ class MainActivity : FlutterActivity() {
}
}
}

override fun onDestroy() {
stopService(Intent(this, AudioRecordingService::class.java))
super.onDestroy()
}
}
101 changes: 71 additions & 30 deletions open_wearable/lib/apps/heart_tracker/widgets/heart_tracker_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:open_wearable/apps/heart_tracker/model/ppg_filter.dart';
import 'package:open_wearable/apps/heart_tracker/widgets/rowling_chart.dart';
import 'package:open_wearable/models/wearable_display_group.dart';
import 'package:open_wearable/view_models/sensor_configuration_provider.dart';
import 'package:open_wearable/view_models/wearables_provider.dart';
import 'package:open_wearable/widgets/devices/devices_page.dart';
import 'package:provider/provider.dart';

Expand All @@ -29,6 +30,9 @@ class HeartTrackerPage extends StatefulWidget {
}

class _HeartTrackerPageState extends State<HeartTrackerPage> {
late final WearablesProvider _wearablesProvider;
Wearable? _wearable;
int _ppgTimestampExponent = -3;
PpgFilter? _ppgFilter;
Stream<(int, double)>? _displayPpgSignalStream;
Stream<double?>? _heartRateStream;
Expand All @@ -38,22 +42,61 @@ class _HeartTrackerPageState extends State<HeartTrackerPage> {
@override
void initState() {
super.initState();
_wearablesProvider = context.read<WearablesProvider>();
_wearablesProvider.addListener(_updateConnection);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) {
return;
}
_initializePipeline();
_updateConnection();
});
}

void _initializePipeline() {
final configProvider =
Provider.of<SensorConfigurationProvider>(context, listen: false);
_sensorConfigProvider = configProvider;
final ppgSensor = widget.ppgSensor;
final accelerometerSensor = widget.accelerometerSensor;
final opticalTemperatureSensor = widget.opticalTemperatureSensor;
void _updateConnection() {
final wearable = _wearablesProvider.wearables
.where((device) => device.deviceId == widget.wearable.deviceId)
.firstOrNull;
if (identical(wearable, _wearable)) {
return;
}
setState(() {
_ppgFilter?.dispose();
_ppgFilter = null;
_displayPpgSignalStream = null;
_heartRateStream = null;
_signalQualityStream = null;
_sensorConfigProvider = null;
_wearable = wearable;
if (wearable == null) {
return;
}
final sensors = wearable.requireCapability<SensorManager>().sensors;
Sensor? currentSensor(Sensor? previous) => sensors
.where((sensor) => sensor.sensorName == previous?.sensorName)
.firstOrNull;
final ppgSensor = currentSensor(widget.ppgSensor);
if (ppgSensor == null) {
return;
}
final configProvider =
_wearablesProvider.getSensorConfigurationProvider(wearable);
_sensorConfigProvider = configProvider;
_ppgTimestampExponent = ppgSensor.timestampExponent;
_initializePipeline(
configProvider,
ppgSensor,
currentSensor(widget.accelerometerSensor),
currentSensor(widget.opticalTemperatureSensor),
);
});
}

void _initializePipeline(
SensorConfigurationProvider configProvider,
Sensor ppgSensor,
Sensor? accelerometerSensor,
Sensor? opticalTemperatureSensor,
) {
final sampleFreq = _configureSensorForStreaming(
ppgSensor,
configProvider,
Expand Down Expand Up @@ -135,20 +178,15 @@ class _HeartTrackerPageState extends State<HeartTrackerPage> {
timestampExponent: ppgSensor.timestampExponent,
);
ppgFilter.initialize();
if (!mounted) {
ppgFilter.dispose();
return;
}
setState(() {
_displayPpgSignalStream = ppgFilter.displaySignalStream;
_heartRateStream = ppgFilter.heartRateStream;
_signalQualityStream = ppgFilter.signalQualityStream;
_ppgFilter = ppgFilter;
});
_displayPpgSignalStream = ppgFilter.displaySignalStream;
_heartRateStream = ppgFilter.heartRateStream;
_signalQualityStream = ppgFilter.signalQualityStream;
_ppgFilter = ppgFilter;
}

@override
void dispose() {
_wearablesProvider.removeListener(_updateConnection);
final configProvider = _sensorConfigProvider;
if (configProvider != null) {
unawaited(configProvider.turnOffAllSensors());
Expand Down Expand Up @@ -372,16 +410,18 @@ class _HeartTrackerPageState extends State<HeartTrackerPage> {
appBar: PlatformAppBar(
title: PlatformText('Heart Tracker'),
),
body: displayPpgSignalStream == null ||
heartRateStream == null ||
signalQualityStream == null
? const Center(child: PlatformCircularProgressIndicator())
: _buildContent(
context,
displayPpgSignalStream,
heartRateStream,
signalQualityStream,
),
body: _wearable == null
? const Center(child: Text('The selected wearable is disconnected.'))
: displayPpgSignalStream == null ||
heartRateStream == null ||
signalQualityStream == null
? const Center(child: PlatformCircularProgressIndicator())
: _buildContent(
context,
displayPpgSignalStream,
heartRateStream,
signalQualityStream,
),
);
}

Expand All @@ -392,10 +432,11 @@ class _HeartTrackerPageState extends State<HeartTrackerPage> {
Stream<PpgSignalQuality> signalQualityStream,
) {
return ListView(
key: ObjectKey(_wearable),
padding: const EdgeInsets.fromLTRB(12, 12, 12, 20),
children: [
DeviceRow(
group: WearableDisplayGroup.single(wearable: widget.wearable),
group: WearableDisplayGroup.single(wearable: _wearable!),
),
const SizedBox(height: 12),
StreamBuilder<PpgSignalQuality>(
Expand Down Expand Up @@ -427,7 +468,7 @@ class _HeartTrackerPageState extends State<HeartTrackerPage> {
'(0.5-3.2 Hz).',
icon: Icons.show_chart_rounded,
chartStream: displayPpgSignalStream,
timestampExponent: widget.ppgSensor.timestampExponent,
timestampExponent: _ppgTimestampExponent,
fixedMeasureMin: null,
fixedMeasureMax: null,
),
Expand Down
Loading
Loading