Untitled
unknown
plain_text
2 years ago
23 kB
9
Indexable
import 'package:flutter/material.dart';
import 'package:self_science_station/model/sensor_model.dart';
import 'package:self_science_station/provider/local_provider.dart';
import 'package:self_science_station/provider/study_provider.dart';
import 'package:self_science_station/provider/user_provider.dart';
import 'package:self_science_station/provider/sensor_provider.dart';
import 'package:provider/provider.dart';
import 'package:self_science_station/screens/checkin_complete_screen.dart';
import 'package:self_science_station/screens/survey_screen.dart';
import 'package:self_science_station/sensor/sensorInstance.dart';
import 'package:self_science_station/utils/utils.dart';
import 'package:video_player/video_player.dart';
import 'package:chewie/chewie.dart';
import 'package:collection/collection.dart';
class SensorScreen extends StatefulWidget {
final Function() onDone;
final SensorTypeModel sensor;
final String sessionID;
final UniqueKey newKey;
const SensorScreen(
{required this.newKey,
required this.sensor,
required this.sessionID,
required this.onDone})
: super(key: newKey);
@override
State<SensorScreen> createState() => _SensorScreenState();
}
class _SensorScreenState extends State<SensorScreen> {
final ScrollController _scrollController =
ScrollController(initialScrollOffset: 0);
bool videoInitialized = false;
late VideoPlayerController videoPlayerController;
ChewieController? chewieController;
late Widget playerWidget;
List<String> sensorsAlreadyInUse = [];
bool foundTargetSensor = false;
bool scanningForSensor = false;
@override
void initState() {
super.initState();
// Jump to top of scrollview
WidgetsBinding.instance.addPostFrameCallback((_) async {
// TODO allow for back button on this screen in the future
final lp = Provider.of<LocalProvider>(context, listen: false);
lp.hideBackButton();
// Start scanning for the sensors
final sp = Provider.of<SensorProvider>(context, listen: false);
setState(() {
sensorsAlreadyInUse = sp.physicalSensors[widget.sensor.id] ?? [];
});
print("Scanned sensors already in use: $sensorsAlreadyInUse");
});
if (!videoInitialized) {
initializePlayer();
}
}
Future<void> initializePlayer() async {
// videoPlayerController = VideoPlayerController.networkUrl(Uri(
// scheme: 'https',
// host: 'flutter.github.io',
// path: 'assets-for-api-docs/assets/videos/butterfly.mp4'));
String vidurl = widget.sensor.setUpVideoUrl;
try {
videoPlayerController = VideoPlayerController.asset(
'assets/${Uri.parse(vidurl).pathSegments.last}');
} catch (e) {
videoPlayerController = VideoPlayerController.networkUrl(
Uri.parse(widget.sensor.setUpVideoUrl));
}
videoPlayerController.setVolume(0.0);
videoPlayerController.addListener(() {
if (videoPlayerController.value.hasError) {
setState(() {
videoInitialized = false;
});
}
});
try {
await videoPlayerController.initialize();
chewieController = ChewieController(
videoPlayerController: videoPlayerController,
autoInitialize: true,
autoPlay: true,
looping: true,
allowFullScreen: true,
showControls: false,
showControlsOnInitialize: false,
overlay: null, // remove overlay that otherwise shows in the beginning
allowPlaybackSpeedChanging: false //remove interactability
);
setState(() {
videoInitialized = true;
});
} catch (e) {
setState(() {
videoInitialized = false;
});
}
}
@override
void dispose() {
videoPlayerController.dispose();
chewieController?.dispose();
chewieController = null;
videoInitialized = false;
super.dispose();
}
@override
Widget build(BuildContext context) {
// By listening to the sensor provider, this should be updated once a new sensor has been found in a scan
final sp = Provider.of<SensorProvider>(context, listen: true);
// Check to see if there is any delta between the sensors already in use and the sensors we have
final sensorsInVicinity = sp.physicalSensors[widget.sensor.id] ?? [];
print("Scanned sensors in vicinity: $sensorsInVicinity");
final newlyConnectedSensors = sensorsInVicinity
.where((s) =>
!sensorsAlreadyInUse.contains(s) ||
true) // TODO remove true once we figure out how to clear bluetooth scan cache
.toList();
return Scaffold(
body: SafeArea(
child: Center(
child: Padding(
padding: const EdgeInsets.all(
70.0,
),
child: Scrollbar(
controller: _scrollController,
thumbVisibility: true,
child: SingleChildScrollView(
controller: _scrollController,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
children: [
Text(
"${widget.sensor.vendor.toTitleCase()} ${widget.sensor.name} (${widget.sensor.type})",
style: const TextStyle(
fontSize: 25, fontWeight: FontWeight.bold)),
const SizedBox(width: 10),
IconButton(
icon: const Icon(Icons.info, size: 30),
onPressed: () =>
_sensorInfoModalBuilder(context)),
],
),
const SizedBox(
height: 5,
),
Row(
crossAxisAlignment: videoInitialized
? CrossAxisAlignment.start
: CrossAxisAlignment.center,
children: [
VideoContainer(
videoInitialized: videoInitialized,
chewieController: chewieController,
sensorType: widget.sensor.type),
Expanded(
child: Padding(
padding:
const EdgeInsets.only(left: 30.0, top: 30.0),
child: Column(
children: widget.sensor.instructions
.mapIndexed(
(instructionIdx, instruction) => Column(
children: [
Wrap(children: [
Text("Step ${instructionIdx + 1}",
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20)),
Text(instruction.step,
style: const TextStyle(
fontSize: 20)),
]),
const SizedBox(height: 20)
],
),
)
.toList(),
),
),
)
],
),
const SizedBox(height: 30),
if (newlyConnectedSensors.isNotEmpty &&
!widget.sensor.wifiOnly)
Column(children: [
const Text("Choose a Sensor to Connect to:",
style: TextStyle(
fontSize: 25, fontWeight: FontWeight.bold)),
const Text(
// TODO this only applies to Polar sensors at the moment
"The ID is printed on the top edge of the sensor",
style: TextStyle(fontSize: 15)),
const SizedBox(height: 20),
Wrap(
runSpacing: 15.0,
children: newlyConnectedSensors
.map((s) => Padding(
padding: const EdgeInsets.all(10.0),
child: ElevatedButton(
style: ButtonStyle(
shape: MaterialStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder(
side: BorderSide(
color:
Theme.of(context)
.primaryColor,
width:
2.0), // Add this line
borderRadius:
BorderRadius.circular(
30.0))),
minimumSize:
MaterialStateProperty.all(
const Size(250, 80)),
backgroundColor:
MaterialStateProperty.all(
Colors.white)),
onPressed: () {
_sensorTestModalBuilder(
context, s);
},
child: Text(s,
style: const TextStyle(
fontSize: 20,
color: Colors.black))),
))
.toList()),
]),
const SizedBox(height: 30),
newlyConnectedSensors.isEmpty || widget.sensor.wifiOnly
? ElevatedButton(
style: ButtonStyle(
shape: MaterialStateProperty.all<
RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(30.0))),
minimumSize: MaterialStateProperty.all(
const Size(250, 80)),
backgroundColor: MaterialStateProperty.all(
Theme.of(context).colorScheme.primary)),
// TODO bring back eventually when/if we have client side sensor SDKs
onPressed: () {
// We don't need to check for connection if it's a wifi only device
if (widget.sensor.wifiOnly) {
final sInstance =
sp.getSensorFromType(widget.sensor.id);
sInstance.connectSensor();
sInstance.startRecording("");
widget.onDone();
} else {
// Hopefully the length of newlyConnectedSensors is just 1
// We use the most recently connected one just in case
if (newlyConnectedSensors.isNotEmpty) {
_sensorTestModalBuilder(
context, newlyConnectedSensors.last);
}
}
},
child: newlyConnectedSensors.isNotEmpty
? const Text("Connect",
style: TextStyle(fontSize: 20))
: const SizedBox(
width: 200,
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Text("Scanning",
style: TextStyle(fontSize: 20)),
SizedBox(width: 15),
CircularProgressIndicator(
color: Colors.white),
],
),
))
: const SizedBox.shrink(),
],
),
),
))),
));
}
Future<void> _sensorInfoModalBuilder(BuildContext context) {
return showDialog<void>(
useRootNavigator: false,
context: context,
builder: (BuildContext context) {
return AlertDialog(
insetPadding: const EdgeInsets.all(100.0),
titlePadding: const EdgeInsets.fromLTRB(50.0, 30, 0, 0),
contentPadding:
const EdgeInsets.symmetric(horizontal: 50.0, vertical: 30.0),
title: Text(
'${widget.sensor.vendor.toTitleCase()} ${widget.sensor.name}',
style: TextStyle(
fontWeight: FontWeight.bold, color: Colors.grey.shade700)),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("Overview:",
style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 20),
Text(widget.sensor.description),
const SizedBox(height: 20),
const Text("Measurement Capabilities:",
style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 20),
Wrap(
runSpacing: 15.0,
children: widget.sensor.canRecord
.map(
(metric) => Container(
margin: const EdgeInsets.only(right: 10.0),
padding: const EdgeInsets.symmetric(
horizontal: 20.0, vertical: 10.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30),
border:
Border.all(color: Colors.lightBlue.shade100),
),
child: Text(metric.name),
),
)
.toList()),
],
),
scrollable: true,
);
},
);
}
Future<void> _sensorTestModalBuilder(
BuildContext context, String targetSensorDeviceID) {
final studyProvider = Provider.of<StudyProvider>(context, listen: false);
final up = Provider.of<UserProvider>(context, listen: false);
final sp = Provider.of<SensorProvider>(context, listen: false);
final sensorInstanceDeviceID = targetSensorDeviceID;
// Get sensor instance from ID
final sInstance = sp.getSensorFromInstanceDeviceID(sensorInstanceDeviceID)!;
sInstance.connectSensorWithTimeout(const Duration(seconds: 9));
Widget buildActionButton(
SensorInstance? updatedSensor, bool isConnected, bool isConnecting) {
//isConnected = true;
if (isConnecting) return const SizedBox.shrink();
if (isConnected && updatedSensor != null) {
return ElevatedButton(
onPressed: () async {
Navigator.pop(context);
try {
final isRecording = await updatedSensor.isRecording();
// Must stop recording before we start again
if (isRecording) {
await updatedSensor.stopRecording();
}
// TODO in the future we could maybe capture this data and send it up here
await updatedSensor.emptyRecordings();
// Make a unique identifier for the recording
// Due to constraints on some devices, this is limited to 64 bytes (characters)
// Use concatenation of the user ID and last 12 chars of session id
final userID = up.userModel.id;
final abbreviatedSessionID =
widget.sessionID.substring(widget.sessionID.length - 12);
await updatedSensor
.startRecording("${userID}_$abbreviatedSessionID");
widget.onDone();
} catch (e) {
// Try again
print("Error recording from device: $e");
}
// dispose();
},
child: const Text("Continue"));
}
// return ElevatedButton(
// onPressed: () {
// Navigator.pop(context);
// },
// child: const Text("Try Again"));
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text("Try Again"),
),
const SizedBox(width: 20),
ElevatedButton(
onPressed: () {
// Add your skip step functionality here.
Navigator.pop(context);
widget.onDone();
// dispose();
},
child: const Text("Skip Step"),
),
],
);
}
return showDialog<void>(
useRootNavigator: false,
context: context,
builder: (BuildContext context) {
return AlertDialog(
insetPadding: const EdgeInsets.all(100.0),
contentPadding:
const EdgeInsets.symmetric(horizontal: 50.0, vertical: 30.0),
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("ID: ${sInstance.getSensorInstance().deviceID}",
style: const TextStyle(fontSize: 10)),
Text(
'Device: ${widget.sensor.vendor.toCapitalized()} ${widget.sensor.name.toCapitalized()}',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.grey.shade700)),
],
),
content: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Consumer<SensorProvider>(
builder: (context, value, child) {
final updatedSensor =
value.getSensorFromInstanceDeviceID(sensorInstanceDeviceID);
final isSensorConnected =
(updatedSensor != null ? updatedSensor.isConnected : false);
final isSensorConnecting = (updatedSensor != null
? updatedSensor.isConnecting
: false);
//print("RE RENDERED WIDGET");
return Column(
children: [
Container(
child: isSensorConnecting
? const CircularProgressIndicator()
: (isSensorConnected
? Icon(Icons.check_circle,
size: 75, color: Colors.green.shade900)
: Icon(Icons.error,
size: 75, color: Colors.red.shade900))),
const SizedBox(height: 20),
Text(
isSensorConnecting
? "Connecting to device..."
: (isSensorConnected
? "Connected"
: "Unable to connect"),
style: const TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 20),
buildActionButton(
updatedSensor, isSensorConnected, isSensorConnecting),
],
);
},
);
}),
scrollable: true,
);
},
);
}
}
class VideoContainer extends StatelessWidget {
const VideoContainer({
super.key,
required this.videoInitialized,
required this.chewieController,
required this.sensorType,
});
final bool videoInitialized;
final ChewieController? chewieController;
final String sensorType;
@override
Widget build(BuildContext context) {
if (videoInitialized && chewieController != null) {
return Expanded(
child: Container(
color: Colors.white,
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 400),
child: Chewie(
controller: chewieController!,
))));
} else {
return Container(
alignment: Alignment.topCenter,
color: Colors.white,
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 400),
child:
Image.asset("assets/${sensorType.replaceAll(' ', '')}.png")));
}
}
}
Editor is loading...
Leave a Comment