carp_mobile_sensing 2.0.0 copy "carp_mobile_sensing: ^2.0.0" to clipboard
carp_mobile_sensing: ^2.0.0 copied to clipboard

Mobile Sensing Framework for Flutter. A software framework for collecting sensor data from the phone and attached wearable devices via probes. Can be extended.

example/lib/main.dart

/*
 * Copyright (c) 2025, the Technical University of Denmark (DTU).
 * All rights reserved. Please see the AUTHORS file for details. 
 * Use of this source code is governed by a MIT-style license that 
 * can be found in the LICENSE file.
 */

import 'package:flutter/material.dart' hide TimeOfDay;

// The CAMS packages
import 'package:carp_serializable/carp_serializable.dart';
import 'package:carp_core/carp_core.dart' hide Smartphone;
import 'package:carp_mobile_sensing/carp_mobile_sensing.dart';

void main() => runApp(const MobileSensingApp());

/// This demo app shows a list of studies in a client manager of CARP Mobile Sensing.
/// Each list tile shows a study showing the study's description and runtime
/// status using a StreamBuilder that listens on the `events` stream from
/// the study.
///
/// You can add a new study using the floating action button (+) at the
/// bottom right corner. This adds a new study based on the protocol defined
/// below.
/// You can remove a study by long-pressing on the study's list tile.
///
/// You can start, pause, and resume data sampling for a study by tapping
/// on the study's list tile.
class MobileSensingApp extends StatelessWidget {
  const MobileSensingApp({super.key});

  @override
  Widget build(BuildContext context) => MaterialApp(
    title: 'Mobile Sensing',
    theme: ThemeData(
      colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
      useMaterial3: true,
    ),
    darkTheme: ThemeData.dark(),
    home: const StudyPage(),
  );
}

class StudyPage extends StatefulWidget {
  const StudyPage({super.key});

  @override
  State<StudyPage> createState() => StudyPageState();
}

/// Shows a list of studies in a ListView.
class StudyPageState extends State<StudyPage> {
  /// The client manager used in this app.
  /// Note that a [SmartPhoneClientManager] is a singleton, so it can always be
  /// accessed using `SmartPhoneClientManager()`. But here is't shorter to just
  /// have a `client` property to use.
  SmartPhoneClientManager client = SmartPhoneClientManager();

  @override
  void initState() {
    // Set debug level for more detailed debugging information.
    Settings().debugLevel = DebugLevel.debug;

    // Configure the client. Note that the client can take a series of configuration
    // parameters, but here we're just using the default configurations.
    client.configure();

    // Listen on all the measurements and print them as json.
    SmartPhoneClientManager().measurements.listen(
      (measurement) => debugPrint(toJsonString(measurement)),
    );

    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('CARP Mobile Sensing')),
      body: ListenableBuilder(
        listenable: client,
        builder: (BuildContext context, Widget? child) => ListView.builder(
          padding: EdgeInsets.symmetric(vertical: 4.0),
          itemCount: client.studies.length,
          itemBuilder: studyTileWithBorder,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: addStudy,
        tooltip: 'Add new study',
        child: Icon(Icons.add),
      ),
    );
  }

  /// Create a list tile for a study showing the study's description and runtime
  /// status using a StreamBuilder that listens on the `events` stream from
  /// the study.
  Widget studyTileWithBorder(BuildContext context, int index) {
    var study = client.studies[index];

    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0),
      child: StreamBuilder<StudyStatusEvent>(
        stream: study.events, // listen to events from the study
        builder: (context, AsyncSnapshot<StudyStatusEvent> snapshot) {
          return Container(
            decoration: BoxDecoration(
              color: Theme.of(context).cardColor,
              borderRadius: BorderRadius.circular(12.0),
              border: Border.all(color: Colors.grey.shade300, width: 1.0),
              boxShadow: [
                BoxShadow(
                  color: Theme.of(context).shadowColor,
                  blurRadius: 8.0,
                  offset: Offset(0, 4),
                ),
              ],
            ),
            child: ListTile(
              isThreeLine: true,
              leading: Icon(switch (study.samplingState?.state) {
                ExecutorState.Created => Icons.refresh,
                ExecutorState.Initialized ||
                ExecutorState.Paused => Icons.play_arrow,
                ExecutorState.Resumed => Icons.pause,
                _ => Icons.refresh,
              }, size: 40),
              title: Text(
                'Study Deployment #$index',
                style: TextStyle(fontWeight: FontWeight.bold),
              ),
              subtitle: Text(
                'ID: ...-${study.studyDeploymentId.split('-').last}\n'
                'Status: ${study.status.name}\n'
                'Sampling: ${study.samplingState?.state.name}',
              ),
              trailing: executorStateIcon[study.samplingState?.state],
              onTap: () => runStudy(study),
              onLongPress: () => removeStudy(study),
            ),
          );
        },
      ),
    );
  }

  /// A set of icons to illustrate the [SmartphoneStudy.samplingState].
  static Map<ExecutorState, Icon> get executorStateIcon => {
    ExecutorState.Created: Icon(Icons.child_care),
    ExecutorState.Initialized: Icon(Icons.check),
    ExecutorState.Resumed: Icon(Icons.radio_button_checked),
    ExecutorState.Paused: Icon(Icons.radio_button_unchecked),
    ExecutorState.Undefined: Icon(Icons.error_outline),
  };

  /// Add a new study to the client's list of studies based on the either the
  /// [simpleProtocol] or [protocol] specified below.
  /// Note that we use the same protocol every time we add a study.
  /// Thus, all studies will be identical in terms of data collection.
  void addStudy() => client.addStudyFromProtocol(simpleProtocol);

  /// Remove [study] from the client's list of studies.
  void removeStudy(SmartphoneStudy study) =>
      client.removeStudy(study.studyDeploymentId, study.deviceRoleName);

  /// Run (start, resume, pause) [study] based on its current state.
  void runStudy(SmartphoneStudy study) => setState(() {
    var controller = client.getStudyController(study);

    // If the study has not been started (and deployed) yet, do this first
    // isDeployed
    if (study.status.index <= StudyStatus.Deployed.index) {
      controller?.start();
    } else {
      if (study.isSampling) {
        controller?.pause();
      } else {
        controller?.resume();
      }
    }
  });

  /// A simple study protocol that collects a few basic measures
  /// using this smartphone as the primary device.
  SmartphoneStudyProtocol get simpleProtocol => SmartphoneStudyProtocol.local(
    name: 'Simple Protocol',
    measures: [
      Measure(type: DeviceSamplingPackage.HEARTBEAT),
      Measure(type: DeviceSamplingPackage.FREE_MEMORY),
      Measure(type: DeviceSamplingPackage.BATTERY_STATE),
      Measure(type: DeviceSamplingPackage.SCREEN_EVENT),
      Measure(type: DeviceSamplingPackage.APP_LIFECYCLE_EVENT),
      Measure(type: SensorSamplingPackage.STEP_EVENT),
      Measure(type: SensorSamplingPackage.AMBIENT_LIGHT),
    ],
  );

  SmartphoneStudyProtocol? _protocol;

  /// Create a new study protocol - advanced version.
  ///
  /// The code below shows many examples of how to add various types of
  /// measures and tasks to the protocol. Most of these are commented out,
  /// but you can uncomment them to see how they work.
  SmartphoneStudyProtocol get protocol {
    if (_protocol == null) {
      _protocol = SmartphoneStudyProtocol(
        ownerId: 'AB',
        name: 'Demo Protocol',
        dataEndPoint: SQLiteDataEndPoint(),
      );

      // Define which devices are used for data collection.
      //
      // In this case, its only this phone.
      // See the CARP Mobile Sensing app for a full-blown example of how to
      // use connected devices (e.g., a Polar heart rate monitor) and online
      // services (e.g., a weather service).
      var phone = Smartphone();
      _protocol?.addPrimaryDevice(phone);

      // Add a participant role
      _protocol?.addParticipantRole(ParticipantRole('Participant'));

      // Now add tasks and measures to the protocol.

      // Collect timezone info every time the app restarts and set up a heartbeat
      // measure to check that the app is still alive. Override the default sampling
      // interval for the heartbeat measure to 1 min (instead of 15 min).
      _protocol?.addTaskControl(
        ImmediateTrigger(),
        BackgroundTask(
          name: 'Timezone Task',
          measures: [
            Measure(type: DeviceSamplingPackage.TIMEZONE),
            Measure(type: DeviceSamplingPackage.HEARTBEAT)
              ..overrideSamplingConfiguration = IntervalSamplingConfiguration(
                interval: const Duration(minutes: 1),
              ),
          ],
        ),
        phone,
      );

      // Collect timezone info every 10 seconds.
      // Note that timezone is a one-time measure, so this can be collected
      // using a periodic trigger. Collecting it periodically does, however,
      // not make much sense. This is just to demonstrate the use of a periodic
      // trigger, which is useful for many other types of measures.
      _protocol?.addTaskControl(
        PeriodicTrigger(period: Duration(seconds: 10)),
        BackgroundTask(
          measures: [Measure(type: DeviceSamplingPackage.TIMEZONE)],
        ),
        phone,
      );

      // Collect device info only once, when this study is deployed.
      _protocol?.addTaskControl(
        OneTimeTrigger(),
        BackgroundTask(
          name: 'Device Info Task',
          measures: [Measure(type: DeviceSamplingPackage.DEVICE_INFORMATION)],
        ),
        phone,
      );

      // Add background measures from the [DeviceSamplingPackage] and
      // [SensorSamplingPackage] sampling packages.
      //
      // Note that some of these measures only works on Android:
      //  * screen events
      //  * ambient light
      //  * free memory (there seems to be a bug in the underlying sysinfo plugin)
      _protocol?.addTaskControl(
        ImmediateTrigger(),
        BackgroundTask(
          name: 'Background Measures Task',
          measures: [
            Measure(type: DeviceSamplingPackage.FREE_MEMORY)
              ..overrideSamplingConfiguration = IntervalSamplingConfiguration(
                interval: const Duration(seconds: 10),
              ),
            Measure(type: DeviceSamplingPackage.BATTERY_STATE),
            Measure(type: DeviceSamplingPackage.SCREEN_EVENT),
            Measure(type: DeviceSamplingPackage.APP_LIFECYCLE_EVENT),
            Measure(type: SensorSamplingPackage.STEP_EVENT),
            Measure(type: SensorSamplingPackage.AMBIENT_LIGHT)
              ..overrideSamplingConfiguration = PeriodicSamplingConfiguration(
                interval: const Duration(seconds: 20),
                duration: const Duration(seconds: 5),
              ),
          ],
        ),
        phone,
      );

      // // Collect IMU data every 10 secs for 1 sec.
      // // Also shows how the sampling interval can be specified ("overridden").
      // // Default sampling interval is 200 ms. Note that it seems like setting the
      // // sampling interval does NOT work on Android (see also the docs on the
      // // sensor_plus package and on the Android sensor documentation:
      // //   https://developer.android.com/reference/android/hardware/SensorManager#registerListener(android.hardware.SensorEventListener,%20android.hardware.Sensor,%20int)
      // _protocol?.addTaskControl(
      //   PeriodicTrigger(period: const Duration(seconds: 10)),
      //   BackgroundTask(
      //     measures: [
      //       Measure(type: SensorSamplingPackage.ACCELERATION)
      //         ..overrideSamplingConfiguration = IntervalSamplingConfiguration(
      //             interval: const Duration(milliseconds: 500)),
      //       Measure(type: SensorSamplingPackage.ROTATION),
      //     ],
      //     duration: const Duration(seconds: 1),
      //   ),
      //   phone,
      // );

      // // Extract acceleration features every minute over 10 seconds
      // _protocol?.addTaskControl(
      //   ImmediateTrigger(),
      //   BackgroundTask(
      //     measures: [
      //       Measure(type: SensorSamplingPackage.ACCELERATION_FEATURES)
      //         ..overrideSamplingConfiguration = PeriodicSamplingConfiguration(
      //           interval: const Duration(minutes: 1),
      //           duration: const Duration(seconds: 10),
      //         ),
      //     ],
      //   ),
      //   phone,
      // );

      // // Example of how to start and stop sampling using the Control.Start and
      // // Control.Stop method
      // var task_1 = BackgroundTask(
      //   measures: [
      //     Measure(type: CarpDataTypes.ACCELERATION_TYPE_NAME),
      //     Measure(type: CarpDataTypes.ROTATION_TYPE_NAME),
      //   ],
      // );

      // var task_2 = BackgroundTask(
      //   measures: [Measure(type: DeviceSamplingPackage.BATTERY_STATE)],
      // );

      // // Start both task_1 and task_2
      // _protocol?.addTaskControls(
      //   ImmediateTrigger(),
      //   [task_1, task_2],
      //   phone,
      //   Control.Start,
      // );

      // // After a while, stop task_1 again
      // _protocol?.addTaskControl(
      //   DelayedTrigger(delay: const Duration(seconds: 10)),
      //   task_1,
      //   phone,
      //   Control.Stop,
      // );

      // Add a random trigger to collect device info at random times
      // _protocol?.addTaskControl(
      //   RandomRecurrentTrigger(
      //     startTime: TimeOfDay(hour: 07, minute: 45),
      //     endTime: TimeOfDay(hour: 22, minute: 30),
      //     minNumberOfTriggers: 2,
      //     maxNumberOfTriggers: 8,
      //   ),
      //   BackgroundTask()
      //     ..addMeasure(Measure(type: DeviceSamplingPackage.DEVICE_INFORMATION)),
      //   phone,
      //   Control.Start,
      // );

      // Add a ConditionalPeriodicTrigger to check periodically
      // _protocol?.addTaskControl(
      //     ConditionalPeriodicTrigger(
      //         period: const Duration(seconds: 20),
      //         triggerCondition: () => ('Jakob'.length == 5)),
      //     BackgroundTask()
      //       ..addMeasure(Measure(type: DeviceSamplingPackage.DEVICE_INFORMATION)),
      //     phone,
      //     Control.Start);

      // Collect device info after 30 secs
      // _protocol?.addTaskControl(
      //   ElapsedTimeTrigger(elapsedTime: const Duration(seconds: 30)),
      //   BackgroundTask(
      //     measures: [
      //       Measure(type: DeviceSamplingPackage.DEVICE_INFORMATION),
      //     ],
      //   ),
      //   phone,
      // );

      // Add app task with notifications.
      //
      // This App Task is added for demo purpose and you should see  a notification
      // on the phone. When you click on it, the app task is started and collects
      // device information.
      // See the PulmonaryMonitor demo app for a full-scale example of how to use
      // the App Task model.
      //
      // Note that by design, iOS applications do not display notifications while
      // the app is in the foreground. So to see the notification, please
      // background the app after deploying the study.

      // Add a app task 20 secs after deployment and make a notification.
      _protocol?.addTaskControl(
        ElapsedTimeTrigger(elapsedTime: const Duration(seconds: 20)),
        AppTask(
          name: 'Device Info App Task',
          type: AppTask.SENSING_TYPE,
          title: "User Task",
          description: 'Please click here to collect Device Information.',
          measures: [Measure(type: DeviceSamplingPackage.DEVICE_INFORMATION)],
          notification: true,
        ),
        phone,
      );

      // // Add a cron job every day at 11:45
      // _protocol?.addTaskControl(
      //     CronScheduledTrigger.parse(cronExpression: '45 11 * * *'),
      //     AppTask(
      //       type: BackgroundSensingUserTask.SENSING_TYPE,
      //       title: "Cron - every day at 11:45",
      //       measures: [Measure(type: DeviceSamplingPackage.DEVICE_INFORMATION)],
      //       notification: true,
      //     ),
      //     phone);
    }

    return _protocol!;
  }
}