Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor treadmill controls #94

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
179 changes: 149 additions & 30 deletions src/lib/treadmill_control/widgets/controls/treadmill_controls.dart
Original file line number Diff line number Diff line change
@@ -1,43 +1,162 @@
import 'dart:async';

import 'package:fitness_machine/workout_management/models/workout_state.dart';
import 'package:fitness_machine/workout_management/services/workout_state_manager.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';


class TreadmillControls extends StatelessWidget {
class TreadmillControls extends StatefulWidget {
final WorkoutStateManager _workoutStateManager;

TreadmillControls({super.key}) :

_workoutStateManager = GetIt.I<WorkoutStateManager>();
TreadmillControls({super.key})
: _workoutStateManager = GetIt.I<WorkoutStateManager>();

@override
TreadmillControlsState createState() => TreadmillControlsState();
}

class TreadmillControlsState extends State<TreadmillControls> {
late WorkoutState _currentWorkoutState;
bool _isCountingDown = false;
int _countdownValue = 3;
Timer? _countdownTimer;
StreamSubscription<WorkoutState>? _workoutStateSubscription;

@override
void initState() {
super.initState();
_currentWorkoutState = widget._workoutStateManager.currentWorkoutState;

_workoutStateSubscription = widget._workoutStateManager.workoutStateStream
.cast<WorkoutState>()
.listen((WorkoutState state) {
if (mounted) {
setState(() {
_currentWorkoutState = state;
});
}
});
}

@override
void dispose() {
_countdownTimer?.cancel();
_workoutStateSubscription?.cancel();
super.dispose();
}

void _startOrPauseWorkout() {
if (_currentWorkoutState == WorkoutState.idle) {
widget._workoutStateManager.startWorkout();
_startCountdown();
} else if (_currentWorkoutState == WorkoutState.paused) {
widget._workoutStateManager.resumeWorkout();
} else if (_currentWorkoutState == WorkoutState.running) {
widget._workoutStateManager.pauseWorkout();
}
}

void _startCountdown() {
setState(() {
_isCountingDown = true;
_countdownValue = 3;
});

_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (!mounted) {
timer.cancel();
return;
}

setState(() {
if (_countdownValue > 0) {
_countdownValue--;
} else {
timer.cancel();
setState(() {
_isCountingDown = false;
_countdownValue = 3;
});
}
});
});
}

void _stopWorkout() {
if (_currentWorkoutState != WorkoutState.idle) {
widget._workoutStateManager.stopWorkout();
}
}

@override
Widget build(BuildContext context) {
return Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
ElevatedButton(
onPressed: () {
_workoutStateManager.startWorkout();
},
child: const Text('Start'),
),
ElevatedButton(
onPressed: () {
_workoutStateManager.stopWorkout();
},
child: const Text('Stop'),
),
ElevatedButton(
onPressed: () {
_workoutStateManager.resumeWorkout();
},
child: const Text('Resume'),
),
ElevatedButton(
onPressed: () {
_workoutStateManager.pauseWorkout();
final colorScheme = Theme.of(context).colorScheme;

},
child: const Text('Pause'),
return Expanded(
child: Stack(
alignment: Alignment.center,
children: [
Positioned.fill(
child: Card(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: _isCountingDown ? null : _startOrPauseWorkout,
style: ElevatedButton.styleFrom(
shape: const CircleBorder(),
padding: const EdgeInsets.all(20),
backgroundColor: colorScheme.primary,
),
child: Icon(
_currentWorkoutState == WorkoutState.idle
? Icons.play_arrow
: (_currentWorkoutState == WorkoutState.paused
? Icons.play_arrow_outlined
: Icons.pause),
color: colorScheme.onPrimary,
size: 40,
),
),
const SizedBox(width: 20),
ElevatedButton(
onPressed: _isCountingDown ||
_currentWorkoutState == WorkoutState.idle
? null
: _stopWorkout,
style: ElevatedButton.styleFrom(
shape: const CircleBorder(),
padding: const EdgeInsets.all(20),
backgroundColor: _isCountingDown ||
_currentWorkoutState == WorkoutState.idle
? colorScheme.secondary.withOpacity(0.5)
: colorScheme.secondary,
),
child: Icon(
Icons.stop,
color: colorScheme.onSecondary,
size: 40,
),
),
],
),
),
),
if (_isCountingDown)
Positioned.fill(
child: Container(
color: colorScheme.onSurface.withOpacity(0.5),
alignment: Alignment.center,
child: Text(
_countdownValue == 0
? "Let's go!"
: _countdownValue.toString(),
style: Theme.of(context).textTheme.displayLarge,
),
),
),
],
),
]);
);
}
}
34 changes: 20 additions & 14 deletions src/lib/treadmill_control/widgets/pages/control_page.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:fitness_machine/hardware/widgets/barriers/ensure_bluetooth_enabled_wrapper.dart';
import 'package:fitness_machine/hardware/widgets/barriers/ensure_device_connected_barrier.dart';
import 'package:fitness_machine/treadmill_control/widgets/controls/speed_indicator.dart';
import 'package:fitness_machine/treadmill_control/widgets/controls/treadmill_controls.dart';
import 'package:fitness_machine/treadmill_control/widgets/controls/workout_status_panel.dart';
import 'package:fitness_machine/treadmill_control/widgets/cubits/workout_status_cubit.dart';
import 'package:fitness_machine/hardware/widgets/barriers/ensure_bluetooth_enabled_wrapper.dart';
import 'package:fitness_machine/hardware/widgets/barriers/ensure_device_connected_barrier.dart';

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class ControlPage extends StatelessWidget {
const ControlPage({super.key});
Expand All @@ -15,13 +14,20 @@ class ControlPage extends StatelessWidget {
Widget build(BuildContext context) {
return EnsureBluetoothEnabledWrapper(
EnsureDeviceConnectedBarrier(
BlocProvider<TrainingStatusCubit>(
create: (ctx) => TrainingStatusCubit(),
child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
const WorkoutStatusPanel(),
const SpeedIndicator(),
TreadmillControls(),
]),
)) );
}
BlocProvider<TrainingStatusCubit>(
create: (ctx) => TrainingStatusCubit(),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const WorkoutStatusPanel(),
const SpeedIndicator(),
TreadmillControls(),
]),
),
),
),
);
}
}
82 changes: 58 additions & 24 deletions src/lib/workout_management/services/workout_state_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,48 +28,81 @@ class WorkoutStateManager {
StreamSubscription? _treadmillDataSubscription;
TreadmillData? _lastReceivedWorkoutData;

WorkoutStateManager(): _workoutStateStreamController = StreamController<WorkoutState>.broadcast(),
_workoutStartedStreamController = StreamController<void>.broadcast(),
_workoutCompletedStreamController = StreamController<CompletedWorkout>.broadcast(),
_logger = GetIt.I<Logger>(),
_fitnessMachineQueryDispatcher = GetIt.I<FitnessMachineQueryDispatcher>(),
_fitnessMachineCommandDispatcher = GetIt.I<FitnessMachineCommandDispatcher>();


void startWorkout() {
WorkoutStateManager()
: _workoutStateStreamController =
StreamController<WorkoutState>.broadcast(),
_workoutStartedStreamController = StreamController<void>.broadcast(),
_workoutCompletedStreamController =
StreamController<CompletedWorkout>.broadcast(),
_logger = GetIt.I<Logger>(),
_fitnessMachineQueryDispatcher =
GetIt.I<FitnessMachineQueryDispatcher>(),
_fitnessMachineCommandDispatcher =
GetIt.I<FitnessMachineCommandDispatcher>();

Future<void> startWorkout() async {
_currentWorkoutStartTime = DateTime.now();
_logger.i("Starting workout at $_currentWorkoutStartTime");
_fitnessMachineCommandDispatcher.start();

await _fitnessMachineCommandDispatcher.start();
_listen();
_setWorkoutState(WorkoutState.running);
_workoutStartedStreamController.add(null);

try {
await _waitForTreadmillStart().timeout(const Duration(seconds: 5));
_workoutStartedStreamController.add(null);
} catch (e) {
_logger.e("Failed to start treadmill within timeout");
_setWorkoutState(WorkoutState.idle);
}
}

void stopWorkout({bool aborted = false})
{
Future<void> _waitForTreadmillStart() async {
await for (TreadmillData update
in _fitnessMachineQueryDispatcher.treadmillDataStream) {
/**
* speedInKmh will be set to minimum speed level on startup
*/
if (update.speedInKmh > 0) {
_setWorkoutState(WorkoutState.running);
break;
}
}
}

void stopWorkout({bool aborted = false}) {
_fitnessMachineCommandDispatcher.stop();
_treadmillDataSubscription?.cancel();
_setWorkoutState(WorkoutState.idle);

if (_lastReceivedWorkoutData == null || _currentWorkoutStartTime == null) {
/**
* timeInSeconds will only start if you start to walk on the pad
*
* It is necessary to check that one has really started to workout and
* therefore a workout should also be saved
*/
if (_lastReceivedWorkoutData == null ||
_currentWorkoutStartTime == null ||
_lastReceivedWorkoutData!.timeInSeconds == 0) {
_logger.e("Workout completed without data");
return;
}

if (!aborted) {
final completedTime = DateTime.now();

CompletedWorkout completedWorkout = CompletedWorkout.fromTreadmillData(_lastReceivedWorkoutData!, _currentWorkoutStartTime!, completedTime);
_logger.i("Completed workout: ${completedWorkout.distanceInKm}km in ${completedWorkout.workoutTimeInSeconds}s");

CompletedWorkout completedWorkout = CompletedWorkout.fromTreadmillData(
_lastReceivedWorkoutData!, _currentWorkoutStartTime!, completedTime);
_logger.i(
"Completed workout: ${completedWorkout.distanceInKm}km in ${completedWorkout.workoutTimeInSeconds}s");
_workoutCompletedStreamController.add(completedWorkout);
}

_currentWorkoutStartTime = null;
_lastReceivedWorkoutData = null;
}

void pauseWorkout() {
_fitnessMachineCommandDispatcher.pause();
_fitnessMachineCommandDispatcher.pause();
_treadmillDataSubscription?.cancel();
_setWorkoutState(WorkoutState.paused);
}
Expand All @@ -84,11 +117,12 @@ class WorkoutStateManager {
_logger.i("Workout state changed to $state");
currentWorkoutState = state;
_workoutStateStreamController.add(state);
}
}

void _listen() {
_treadmillDataSubscription = _fitnessMachineQueryDispatcher.treadmillDataStream.listen((update) {
_treadmillDataSubscription =
_fitnessMachineQueryDispatcher.treadmillDataStream.listen((update) {
_lastReceivedWorkoutData = update;
});
}
}
}