diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 01e4a58..b492a0b 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -27,9 +27,9 @@ - + diff --git a/lib/bluetooth/bluetooth_manager.dart b/lib/bluetooth/bluetooth_manager.dart index d0c3ec8..1bf27d5 100644 --- a/lib/bluetooth/bluetooth_manager.dart +++ b/lib/bluetooth/bluetooth_manager.dart @@ -80,7 +80,7 @@ class BluetoothManager { } /// Starts a periodic timer that requests data automatically. - void startAutoReading({Duration interval = const Duration(seconds: 5)}) { + void startAutoReading({Duration interval = const Duration(seconds: 2)}) { // Cancel any existing timer to prevent duplicates. stopAutoReading(); diff --git a/lib/screens/marine/manual/widgets/in_situ_step_3_data_capture.dart b/lib/screens/marine/manual/widgets/in_situ_step_3_data_capture.dart index 509e495..db08f3e 100644 --- a/lib/screens/marine/manual/widgets/in_situ_step_3_data_capture.dart +++ b/lib/screens/marine/manual/widgets/in_situ_step_3_data_capture.dart @@ -34,6 +34,12 @@ class _InSituStep3DataCaptureState extends State with Wi bool _isAutoReading = false; StreamSubscription? _dataSubscription; + // --- START MODIFICATION: Countdown Timer State --- + Timer? _lockoutTimer; + int _lockoutSecondsRemaining = 30; + bool _isLockedOut = false; + // --- END MODIFICATION --- + // --- START FIX: Declare service variable --- late final MarineInSituSamplingService _samplingService; // --- END FIX --- @@ -85,6 +91,7 @@ class _InSituStep3DataCaptureState extends State with Wi @override void dispose() { _dataSubscription?.cancel(); + _lockoutTimer?.cancel(); // --- MODIFICATION: Cancel timer on dispose --- // --- START FIX: Use the pre-fetched service instance --- if (_samplingService.bluetoothConnectionState.value != BluetoothConnectionState.disconnected) { @@ -251,12 +258,40 @@ class _InSituStep3DataCaptureState extends State with Wi ); } + // --- START MODIFICATION: Countdown Timer Logic --- + void _startLockoutTimer() { + _lockoutTimer?.cancel(); + setState(() { + _isLockedOut = true; + _lockoutSecondsRemaining = 30; + }); + + _lockoutTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (_lockoutSecondsRemaining > 0) { + if (mounted) { + setState(() { + _lockoutSecondsRemaining--; + }); + } + } else { + timer.cancel(); + if (mounted) { + setState(() { + _isLockedOut = false; + }); + } + } + }); + } + // --- END MODIFICATION --- + void _toggleAutoReading(String activeType) { final service = context.read(); setState(() { _isAutoReading = !_isAutoReading; if (_isAutoReading) { if (activeType == 'bluetooth') service.startBluetoothAutoReading(); else service.startSerialAutoReading(); + _startLockoutTimer(); // --- MODIFICATION: Start countdown } else { if (activeType == 'bluetooth') service.stopBluetoothAutoReading(); else service.stopSerialAutoReading(); } @@ -272,8 +307,12 @@ class _InSituStep3DataCaptureState extends State with Wi } _dataSubscription?.cancel(); _dataSubscription = null; + _lockoutTimer?.cancel(); // --- MODIFICATION: Cancel timer on disconnect --- if (mounted) { - setState(() => _isAutoReading = false); + setState(() { + _isAutoReading = false; + _isLockedOut = false; // --- MODIFICATION: Reset lockout state --- + }); } } @@ -304,6 +343,13 @@ class _InSituStep3DataCaptureState extends State with Wi } void _validateAndProceed() { + // --- START MODIFICATION: Add lockout check --- + if (_isLockedOut) { + _showSnackBar("Please wait for the initial reading period to complete.", isError: true); + return; + } + // --- END MODIFICATION --- + if (_isAutoReading) { _showStopReadingDialog(); return; @@ -480,87 +526,100 @@ class _InSituStep3DataCaptureState extends State with Wi final activeConnection = _getActiveConnectionDetails(); final String? activeType = activeConnection?['type'] as String?; - return Form( - key: _formKey, - child: ListView( - padding: const EdgeInsets.all(24.0), - children: [ - Text("Data Capture", style: Theme.of(context).textTheme.headlineSmall), - const SizedBox(height: 16), - Row( - children: [ - Expanded( - child: activeType == 'bluetooth' - ? FilledButton.icon(icon: const Icon(Icons.bluetooth_connected), label: const Text("Bluetooth"), onPressed: _isLoading ? null : () => _handleConnectionAttempt('bluetooth')) - : OutlinedButton.icon(icon: const Icon(Icons.bluetooth), label: const Text("Bluetooth"), onPressed: _isLoading ? null : () => _handleConnectionAttempt('bluetooth')), - ), - const SizedBox(width: 16), - Expanded( - child: activeType == 'serial' - ? FilledButton.icon(icon: const Icon(Icons.usb), label: const Text("USB Serial"), onPressed: _isLoading ? null : () => _handleConnectionAttempt('serial')) - : OutlinedButton.icon(icon: const Icon(Icons.usb), label: const Text("USB Serial"), onPressed: _isLoading ? null : () => _handleConnectionAttempt('serial')), - ), - ], - ), - const SizedBox(height: 16), - if (activeConnection != null) - _buildConnectionCard(type: activeConnection['type'], connectionState: activeConnection['state'], deviceName: activeConnection['name']), - const SizedBox(height: 24), - // START FIX: Restored ValueListenableBuilder to listen for Sonde ID updates. - ValueListenableBuilder( - valueListenable: service.sondeId, - builder: (context, sondeId, child) { - final newSondeId = sondeId ?? ''; - // Use a post-frame callback to safely update the controller after the build. - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted && _sondeIdController.text != newSondeId) { - _sondeIdController.text = newSondeId; - widget.data.sondeId = newSondeId; - } - }); - return TextFormField( - controller: _sondeIdController, - decoration: const InputDecoration(labelText: 'Sonde ID *', hintText: 'Connect device or enter manually'), - validator: (v) => v == null || v.isEmpty ? 'Sonde ID is required' : null, - onChanged: (value) => widget.data.sondeId = value, - onSaved: (v) => widget.data.sondeId = v, - ); - }, - ), - // END FIX - const SizedBox(height: 16), - Row( - children: [ - Expanded(child: TextFormField(controller: _dateController, readOnly: true, decoration: const InputDecoration(labelText: 'Date'))), - const SizedBox(width: 16), - Expanded(child: TextFormField(controller: _timeController, readOnly: true, decoration: const InputDecoration(labelText: 'Time'))), - ], - ), + // --- START MODIFICATION: Add WillPopScope to block back navigation --- + return WillPopScope( + onWillPop: () async { + if (_isLockedOut) { + _showSnackBar("Please wait for the initial reading period to complete.", isError: true); + return false; // Prevent back navigation + } + return true; // Allow back navigation + }, + child: Form( + key: _formKey, + child: ListView( + padding: const EdgeInsets.all(24.0), + children: [ + Text("Data Capture", style: Theme.of(context).textTheme.headlineSmall), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: activeType == 'bluetooth' + ? FilledButton.icon(icon: const Icon(Icons.bluetooth_connected), label: const Text("Bluetooth"), onPressed: _isLoading ? null : () => _handleConnectionAttempt('bluetooth')) + : OutlinedButton.icon(icon: const Icon(Icons.bluetooth), label: const Text("Bluetooth"), onPressed: _isLoading ? null : () => _handleConnectionAttempt('bluetooth')), + ), + const SizedBox(width: 16), + Expanded( + child: activeType == 'serial' + ? FilledButton.icon(icon: const Icon(Icons.usb), label: const Text("USB Serial"), onPressed: _isLoading ? null : () => _handleConnectionAttempt('serial')) + : OutlinedButton.icon(icon: const Icon(Icons.usb), label: const Text("USB Serial"), onPressed: _isLoading ? null : () => _handleConnectionAttempt('serial')), + ), + ], + ), + const SizedBox(height: 16), + if (activeConnection != null) + _buildConnectionCard(type: activeConnection['type'], connectionState: activeConnection['state'], deviceName: activeConnection['name']), + const SizedBox(height: 24), + // START FIX: Restored ValueListenableBuilder to listen for Sonde ID updates. + ValueListenableBuilder( + valueListenable: service.sondeId, + builder: (context, sondeId, child) { + final newSondeId = sondeId ?? ''; + // Use a post-frame callback to safely update the controller after the build. + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted && _sondeIdController.text != newSondeId) { + _sondeIdController.text = newSondeId; + widget.data.sondeId = newSondeId; + } + }); + return TextFormField( + controller: _sondeIdController, + decoration: const InputDecoration(labelText: 'Sonde ID *', hintText: 'Connect device or enter manually'), + validator: (v) => v == null || v.isEmpty ? 'Sonde ID is required' : null, + onChanged: (value) => widget.data.sondeId = value, + onSaved: (v) => widget.data.sondeId = v, + ); + }, + ), + // END FIX + const SizedBox(height: 16), + Row( + children: [ + Expanded(child: TextFormField(controller: _dateController, readOnly: true, decoration: const InputDecoration(labelText: 'Date'))), + const SizedBox(width: 16), + Expanded(child: TextFormField(controller: _timeController, readOnly: true, decoration: const InputDecoration(labelText: 'Time'))), + ], + ), - if (_previousReadingsForComparison != null) - _buildComparisonView(), + if (_previousReadingsForComparison != null) + _buildComparisonView(), - const Divider(height: 32), - Column( - children: _parameters.map((param) { - return _buildParameterListItem( - icon: param['icon'] as IconData, - label: param['label'] as String, - unit: param['unit'] as String, - controller: param['controller'] as TextEditingController, - isOutOfBounds: _outOfBoundsKeys.contains(param['key']), - ); - }).toList(), - ), - const SizedBox(height: 32), - ElevatedButton( - onPressed: _validateAndProceed, - style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)), - child: const Text('Next'), - ), - ], + const Divider(height: 32), + Column( + children: _parameters.map((param) { + return _buildParameterListItem( + icon: param['icon'] as IconData, + label: param['label'] as String, + unit: param['unit'] as String, + controller: param['controller'] as TextEditingController, + isOutOfBounds: _outOfBoundsKeys.contains(param['key']), + ); + }).toList(), + ), + const SizedBox(height: 32), + // --- START MODIFICATION: Add countdown to Next button --- + ElevatedButton( + onPressed: _isLockedOut ? null : _validateAndProceed, + style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)), + child: Text(_isLockedOut ? 'Next ($_lockoutSecondsRemaining\s)' : 'Next'), + ), + // --- END MODIFICATION --- + ], + ), ), ); + // --- END MODIFICATION --- } Widget _buildComparisonView() { @@ -773,12 +832,21 @@ class _InSituStep3DataCaptureState extends State with Wi Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ + // --- START MODIFICATION: Add countdown to Stop Reading button --- ElevatedButton.icon( icon: Icon(_isAutoReading ? Icons.stop_circle_outlined : Icons.play_circle_outlined), - label: Text(_isAutoReading ? 'Stop Reading' : 'Start Reading'), - onPressed: () => _toggleAutoReading(type), - style: ElevatedButton.styleFrom(backgroundColor: _isAutoReading ? Colors.orange : Colors.green, foregroundColor: Colors.white), + label: Text(_isAutoReading + ? (_isLockedOut ? 'Stop Reading ($_lockoutSecondsRemaining\s)' : 'Stop Reading') + : 'Start Reading'), + onPressed: (_isAutoReading && _isLockedOut) ? null : () => _toggleAutoReading(type), + style: ElevatedButton.styleFrom( + backgroundColor: _isAutoReading + ? (_isLockedOut ? Colors.grey.shade600 : Colors.orange) + : Colors.green, + foregroundColor: Colors.white, + ), ), + // --- END MODIFICATION --- TextButton.icon( icon: const Icon(Icons.link_off), label: const Text('Disconnect'), diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index 14189e6..17bb9ab 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -744,7 +744,7 @@ class _SettingsScreenState extends State { ListTile( leading: const Icon(Icons.info_outline), title: const Text('App Version'), - subtitle: const Text('MMS V4 1.2.07'), + subtitle: const Text('MMS V4 1.2.08'), dense: true, ), ListTile( diff --git a/lib/serial/serial_manager.dart b/lib/serial/serial_manager.dart index 568eaba..1d33499 100644 --- a/lib/serial/serial_manager.dart +++ b/lib/serial/serial_manager.dart @@ -164,7 +164,7 @@ class SerialManager { } /// Starts a periodic timer to automatically request data from the device. - void startAutoReading({Duration interval = const Duration(seconds: 5)}) { + void startAutoReading({Duration interval = const Duration(seconds: 2)}) { stopAutoReading(); // Stop any existing auto-reading timer first if (connectionState.value == SerialConnectionState.connected) { //startLiveReading(); // Initiate the first reading immediately diff --git a/lib/services/marine_in_situ_sampling_service.dart b/lib/services/marine_in_situ_sampling_service.dart index 5e8e5df..f0431f2 100644 --- a/lib/services/marine_in_situ_sampling_service.dart +++ b/lib/services/marine_in_situ_sampling_service.dart @@ -128,7 +128,7 @@ class MarineInSituSamplingService { Future> getPairedBluetoothDevices() => _bluetoothManager.getPairedDevices(); Future connectToBluetoothDevice(BluetoothDevice device) => _bluetoothManager.connect(device); void disconnectFromBluetooth() => _bluetoothManager.disconnect(); - void startBluetoothAutoReading({Duration? interval}) => _bluetoothManager.startAutoReading(interval: interval ?? const Duration(seconds: 5)); + void startBluetoothAutoReading({Duration? interval}) => _bluetoothManager.startAutoReading(interval: interval ?? const Duration(seconds: 2)); void stopBluetoothAutoReading() => _bluetoothManager.stopAutoReading(); // --- USB Serial Methods --- @@ -153,7 +153,7 @@ class MarineInSituSamplingService { } void disconnectFromSerial() => _serialManager.disconnect(); - void startSerialAutoReading({Duration? interval}) => _serialManager.startAutoReading(interval: interval ?? const Duration(seconds: 5)); + void startSerialAutoReading({Duration? interval}) => _serialManager.startAutoReading(interval: interval ?? const Duration(seconds: 2)); void stopSerialAutoReading() => _serialManager.stopAutoReading(); void dispose() { diff --git a/lib/services/river_in_situ_sampling_service.dart b/lib/services/river_in_situ_sampling_service.dart index 81bd692..7f346a9 100644 --- a/lib/services/river_in_situ_sampling_service.dart +++ b/lib/services/river_in_situ_sampling_service.dart @@ -129,7 +129,7 @@ class RiverInSituSamplingService { Future> getPairedBluetoothDevices() => _bluetoothManager.getPairedDevices(); Future connectToBluetoothDevice(BluetoothDevice device) => _bluetoothManager.connect(device); void disconnectFromBluetooth() => _bluetoothManager.disconnect(); - void startBluetoothAutoReading({Duration? interval}) => _bluetoothManager.startAutoReading(interval: interval ?? const Duration(seconds: 5)); + void startBluetoothAutoReading({Duration? interval}) => _bluetoothManager.startAutoReading(interval: interval ?? const Duration(seconds: 2)); void stopBluetoothAutoReading() => _bluetoothManager.stopAutoReading(); Future> getAvailableSerialDevices() => _serialManager.getAvailableDevices(); @@ -152,7 +152,7 @@ class RiverInSituSamplingService { } void disconnectFromSerial() => _serialManager.disconnect(); - void startSerialAutoReading({Duration? interval}) => _serialManager.startAutoReading(interval: interval ?? const Duration(seconds: 5)); + void startSerialAutoReading({Duration? interval}) => _serialManager.startAutoReading(interval: interval ?? const Duration(seconds: 2)); void stopSerialAutoReading() => _serialManager.stopAutoReading(); void dispose() { _bluetoothManager.dispose();