Untitled
unknown
plain_text
a year ago
23 kB
7
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