carp_mobility_package 0.1.0
carp_mobility_package: ^0.1.0 copied to clipboard
A Flutter package for CARP mobility data collection and processing.
// ignore_for_file: avoid_print
import 'package:carp_mobility_package/carp_mobility_package.dart';
import 'package:carp_mobility_package/domain/tasks/wt_task.dart';
import 'package:carp_mobility_package/domain/tasks/tug_task.dart';
import 'package:carp_mobility_package/localization/mp_localizations.dart';
import 'package:carp_mobility_package/sensing/sensors/movesense_sensor.dart';
import 'package:carp_serializable/carp_serializable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:mdsflutter/Mds.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:research_package/research_package.dart';
void main() {
ResearchPackage.ensureInitialized();
runApp(const MainApp());
}
var buttons = [
TaskParams(
title: 'wt_measure_name',
task: WTTask(
identifier: 'wt',
measure: WTMeasure(
type: 'wt',
name: 'wt_measure_name',
numberOfSteps: 15,
),
),
),
TaskParams(
title: 'sts_measure_name',
task: StsTask(
identifier: 'sts_30s',
measure: StsMeasure(
type: 'sts_30s',
name: '30s Sit-to-Stand',
duration: const Duration(seconds: 30),
),
),
),
TaskParams(title: 'tug_measure_name', task: TugTask(identifier: 'tug')),
TaskParams(
title: 'srom_measure_flexion',
task: FlexionTask(
identifier: 'srom_flexion',
measure: SromMeasure(
type: 'srom_flexion',
name: 'srom_measure_flexion',
duration: const Duration(seconds: 10),
),
),
),
TaskParams(
title: 'srom_measure_extension',
task: ExtensionTask(
identifier: 'srom_extension',
measure: SromMeasure(
type: 'srom_extension',
name: 'srom_measure_extension',
duration: const Duration(seconds: 10),
),
),
),
TaskParams(
title: 'srom_measure_abduction',
task: AbductionTask(
identifier: 'srom_abduction',
measure: SromMeasure(
type: 'srom_abduction',
name: 'srom_measure_abduction',
duration: const Duration(seconds: 10),
),
),
),
TaskParams(
title: 'srom_measure_rotation_internal',
task: InternalRotationTask(
identifier: 'srom_internal_rotation',
measure: SromMeasure(
type: 'srom_internal_rotation',
name: 'srom_measure_internal_rotation',
duration: const Duration(seconds: 10),
),
),
),
TaskParams(
title: 'srom_measure_rotation_external',
task: ExternalRotationTask(
identifier: 'srom_external_rotation',
measure: SromMeasure(
type: 'srom_external_rotation',
name: 'srom_measure_external_rotation',
duration: const Duration(seconds: 10),
),
),
),
];
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
supportedLocales: const [Locale('en'), Locale('da'), Locale('hu')],
localizationsDelegates: [
// Research Package (RP) and Mobility Package (MP) translations.
// Supports translation of both the RP and MP specific text as well as
// app-specific text.
// Read more about localization at https://carp.cachet.dk/localization/
RPLocalizations.delegate,
MPLocalizations.delegate,
// Built-in localization of basic text for Cupertino widgets
GlobalCupertinoLocalizations.delegate,
// Built-in localization of basic text for Material widgets
GlobalMaterialLocalizations.delegate,
// Built-in localization for text direction LTR/RTL
GlobalWidgetsLocalizations.delegate,
],
localeResolutionCallback: (locale, supportedLocales) {
// Check if the current device locale is supported
for (var supportedLocale in supportedLocales) {
if (supportedLocale.languageCode == locale!.languageCode) {
return supportedLocale;
}
}
// if the locale of the device is not supported, use the first one
// from the list (English, in this case).
return supportedLocales.first;
},
theme: researchPackageTheme,
darkTheme: researchPackageTheme,
debugShowCheckedModeBanner: false,
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
HomePageState createState() => HomePageState();
}
class HomePageState extends State<HomePage> {
var moveSenseDevices = <String, MovesenseDevice>{};
MovesenseDevice? selectedDevice;
Future<void> _requestPermissions() async {
await [
Permission.activityRecognition,
Permission.sensors,
Permission.locationWhenInUse,
Permission.bluetoothScan,
Permission.bluetoothConnect,
].request();
final activityGranted = await Permission.activityRecognition.status;
final sensorsGranted = await Permission.sensors.status;
final locGranted = await Permission.locationWhenInUse.status;
final bluetoothScanGranted = await Permission.bluetoothScan.status;
final bluetoothConnectGranted = await Permission.bluetoothConnect.status;
if (kDebugMode) {
print('Permissions:');
print('Activity: $activityGranted');
print('Sensors: $sensorsGranted');
print('Location: $locGranted');
print('Bluetooth Scan: $bluetoothScanGranted');
print('Bluetooth Connect: $bluetoothConnectGranted');
}
}
@override
void initState() {
super.initState();
_requestPermissions();
}
@override
Widget build(BuildContext context) {
var locale = RPLocalizations.of(context);
return Scaffold(
appBar: AppBar(
title: Text(
locale?.translate('app_title') ?? 'Mobility Package Example',
),
actions: [
IconButton(
icon: const Icon(Icons.settings),
onPressed: () {
showDialog(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
Mds.startScan((name, address) {
if (kDebugMode) {
print('Found device: $name, $address');
}
if (name != null &&
name.toLowerCase().contains("movesense")) {
moveSenseDevices.update(
name,
(value) =>
MovesenseDevice(name: name, address: address!),
ifAbsent:
() => MovesenseDevice(
name: name,
address: address!,
),
);
setState(() {}); // Update the dialog's UI
}
});
return AlertDialog(
title: Text(
locale?.translate('movesense_connect_title') ??
'Connect Movesense',
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
locale?.translate('movesense_connect_info') ??
'To use Movesense device, you need to connect it first.',
),
const SizedBox(height: 8.0),
Text(
locale?.translate('movesense_connect_info2') ??
'Make sure Bluetooth is enabled and the device is in range.',
),
SizedBox(
width: MediaQuery.of(context).size.width * 0.75,
height: MediaQuery.of(context).size.height * 0.3,
child: ListView.builder(
shrinkWrap: true,
itemCount: moveSenseDevices.length,
itemBuilder: (context, index) {
var device =
moveSenseDevices.entries.toList()[index];
return ListTile(
title: Text(device.value.name),
subtitle: Text(device.value.address),
trailing:
device.value.address ==
selectedDevice?.address
? const Icon(
Icons.check,
color: Colors.green,
)
: null,
onTap: () {
Mds.stopScan();
Mds.connect(
device.value.address,
(serial) {
selectedDevice = device.value;
MovesenseSensor().connectedSerial =
serial;
setState(() {});
ScaffoldMessenger.of(
context,
).showSnackBar(
SnackBar(
content: Text(
'${locale?.translate('movesense_connected') ?? 'Connected to:'} ${device.value.name}',
),
),
);
},
() {
selectedDevice = null;
setState(() {});
MovesenseSensor().connectedSerial =
null;
ScaffoldMessenger.of(
context,
).showSnackBar(
SnackBar(
content: Text(
'${locale?.translate('movesense_disconnected') ?? 'Disconnected from'} ${device.value.name}',
),
),
);
},
(error) {
ScaffoldMessenger.of(
context,
).showSnackBar(
SnackBar(
content: Text(
'${locale?.translate('movesense_connect_error') ?? 'Failed to connect:'} $error',
),
),
);
},
null,
);
},
);
},
),
),
],
),
actions: [
TextButton(
child: Text(locale?.translate('close') ?? 'Close'),
onPressed: () {
Mds.stopScan();
Navigator.of(context).pop();
},
),
],
);
},
);
},
);
},
),
],
),
body: ListView.builder(
itemCount: buttons.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => TaskScreen(task: buttons[index].task),
),
);
},
child: Text(
locale?.translate(buttons[index].title) ?? buttons[index].title,
textAlign: TextAlign.center,
),
),
);
},
),
);
}
}
class TaskParams {
final String title;
final RPOrderedTask task;
TaskParams({required this.title, required this.task});
}
class TaskScreen extends StatelessWidget {
final RPOrderedTask task;
const TaskScreen({super.key, required this.task});
@override
Widget build(BuildContext context) {
return Scaffold(
body: RPUITask(
key: UniqueKey(),
task: task,
onSubmit:
(p0) => print(
"Submitted result: ${toJsonString(p0.results.values.last)}",
),
onCancel: (result) {
if (result?.results.isEmpty ?? true) {
return;
}
print(
"Cancelled result: ${toJsonString(result?.results.values.last)}",
);
},
),
);
}
}
class MovesenseDevice {
final String name;
final String address;
MovesenseDevice({required this.name, required this.address});
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other.runtimeType != runtimeType) return false;
return other is MovesenseDevice &&
other.name == name &&
other.address == address;
}
@override
int get hashCode => super.hashCode ^ name.hashCode ^ address.hashCode;
}