diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 069602c1d..a97f5a921 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,7 +1,11 @@ + + diff --git a/android/app/src/main/java/io/pslab/MainActivity.java b/android/app/src/main/java/io/pslab/MainActivity.java index d5db2dd06..5ad9edee1 100644 --- a/android/app/src/main/java/io/pslab/MainActivity.java +++ b/android/app/src/main/java/io/pslab/MainActivity.java @@ -1,6 +1,166 @@ package io.pslab; +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.os.Bundle; +import android.util.Log; + +import androidx.annotation.NonNull; + import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.plugin.common.EventChannel; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +public class MainActivity extends FlutterActivity implements SensorEventListener { + private static final String TEMPERATURE_CHANNEL = "io.pslab/temperature"; + private static final String TEMPERATURE_STREAM = "io.pslab/temperature_stream"; + private static final String TAG = "MainActivity"; + private SensorManager sensorManager; + private Sensor temperatureSensor; + private EventChannel.EventSink temperatureEventSink; + private boolean isListening = false; + private float currentTemperature = 0.0f; + + @Override + public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { + super.configureFlutterEngine(flutterEngine); + + sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE); + if (sensorManager != null) { + temperatureSensor = sensorManager.getDefaultSensor(Sensor.TYPE_AMBIENT_TEMPERATURE); + } + + MethodChannel temperatureChannel = new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), TEMPERATURE_CHANNEL); + temperatureChannel.setMethodCallHandler(this::handleMethodCall); + + EventChannel temperatureEventChannel = new EventChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), TEMPERATURE_STREAM); + temperatureEventChannel.setStreamHandler(new EventChannel.StreamHandler() { + @Override + public void onListen(Object arguments, EventChannel.EventSink events) { + temperatureEventSink = events; + startTemperatureUpdates(); + } + + @Override + public void onCancel(Object arguments) { + temperatureEventSink = null; + stopTemperatureUpdates(); + } + }); + } + + private void handleMethodCall(MethodCall call, MethodChannel.Result result) { + switch (call.method) { + case "isTemperatureSensorAvailable": + result.success(temperatureSensor != null); + break; + case "getCurrentTemperature": + result.success((double) currentTemperature); + break; + case "startTemperatureUpdates": + if (startTemperatureUpdates()) { + result.success(true); + } else { + result.error("SENSOR_ERROR", "Failed to start temperature updates", null); + } + break; + case "stopTemperatureUpdates": + stopTemperatureUpdates(); + result.success(true); + break; + default: + result.notImplemented(); + break; + } + } + + private boolean startTemperatureUpdates() { + if (temperatureSensor == null || sensorManager == null) { + Log.e(TAG, "Temperature sensor not available"); + return false; + } + + if (!isListening) { + boolean registered = sensorManager.registerListener(this, temperatureSensor, SensorManager.SENSOR_DELAY_NORMAL); + if (registered) { + isListening = true; + Log.d(TAG, "Temperature sensor listener registered"); + + if (currentTemperature != 0.0f && temperatureEventSink != null) { + Log.d(TAG, "Sending initial temperature to Flutter: " + currentTemperature); + temperatureEventSink.success((double) currentTemperature); + } + + return true; + } else { + Log.e(TAG, "Failed to register temperature sensor listener"); + return false; + } + } + return true; + } + + private void stopTemperatureUpdates() { + if (isListening && sensorManager != null) { + sensorManager.unregisterListener(this, temperatureSensor); + isListening = false; + Log.d(TAG, "Temperature sensor listener unregistered"); + } + } + + @Override + public void onSensorChanged(SensorEvent event) { + if (event.sensor.getType() == Sensor.TYPE_AMBIENT_TEMPERATURE) { + float temperature = event.values[0]; + + if (isValidTemperature(temperature)) { + currentTemperature = temperature; + Log.d(TAG, "Temperature updated: " + currentTemperature + "°C"); + + if (temperatureEventSink != null) { + Log.d(TAG, "Sending temperature to Flutter: " + currentTemperature); + temperatureEventSink.success((double) currentTemperature); + } + } else { + Log.w(TAG, "Invalid temperature reading: " + temperature + " - ignoring"); + } + } + } + + private boolean isValidTemperature(float temperature) { + if (Float.isNaN(temperature) || Float.isInfinite(temperature)) return false; + return temperature >= -273.15f && temperature <= 200f && Math.abs(temperature) <= 1e10f; + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + Log.d(TAG, "Sensor accuracy changed: " + accuracy); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + stopTemperatureUpdates(); + } + + @Override + protected void onPause() { + super.onPause(); + if (isListening && sensorManager != null) { + sensorManager.unregisterListener(this); + } + } -public class MainActivity extends FlutterActivity { -} + @Override + protected void onResume() { + super.onResume(); + if (isListening && temperatureSensor != null && sensorManager != null) { + sensorManager.registerListener(this, temperatureSensor, SensorManager.SENSOR_DELAY_NORMAL); + } + } +} \ No newline at end of file diff --git a/lib/constants.dart b/lib/constants.dart index c4213e429..f65495e30 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -1,5 +1,43 @@ import 'dart:core'; +List instrumentHeadings = [ + 'OSCILLOSCOPE', + 'MULTIMETER', + 'LOGIC ANALYZER', + 'SENSORS', + 'WAVE GENERATOR', + 'POWER SOURCE', + 'LUX METER', + 'ACCELEROMETER', + 'BAROMETER', + 'COMPASS', + 'GYROSCOPE', + 'THERMOMETER', + 'ROBOTIC ARM', + 'GAS SENSOR', + 'DUST SENSOR', + 'SOUND METER' +]; + +List instrumentDesc = [ + 'Allows observation of varying signal voltages', + 'Measure voltage, current, resistance and capacitance', + 'Captures and displays signals from digital systems', + 'Allows logging of data returned by sensor connected', + 'Generates arbitrary analog and digital waveforms', + 'Generates programmable voltage and currents', + 'Measures the ambient light intensity', + 'Measures the Linear acceleration in XYZ directions', + 'Measures the atmospheric pressure', + 'Three axes magnetometer pointing to magnetic north', + 'Measures rate of rotation about XYZ axis', + 'To measure the ambient temperature', + 'Controls servos of a robotic arm', + 'Air quality sensor for detecting a wide range of gases, including NH3, NOx, alcohol, benzene, smoke and CO2', + 'Dust sensor is used to measure air quality in terms of particles per square meter', + 'To measure the loudness in the environment in decibel(dB)' +]; + List instrumentIcons = [ 'assets/icons/tile_icon_oscilloscope.png', 'assets/icons/tile_icon_multimeter.png', diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 134738cff..19f6ed6e7 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -323,8 +323,14 @@ "baroMeterBulletPoint2": "If you want to use the sensor BMP-180, connect the sensor to PSLab device as shown in the figure.", "baroMeterBulletPoint3": "The above pin configuration has to be same except for the pin GND. GND is meant for Ground and any of the PSLab device GND pins can be used since they are common.", "baroMeterBulletPoint4": "Select the sensor by going to the Configure tab from the bottom navigation bar and choose BMP-180 in the drop down menu under Select Sensor.", - "sharingMessage": "Sharing PSLab Data", - "delete": "Delete", + "thermometerTitle" : "Thermometer", + "thermometerIntro" : "Thermometer instrument is used to measure ambient temprature. It can be measured using inbuilt ambient temprature sensor or through SHT21.", + "celsius": "°C", + "temperatureSensorError" : "Temperature sensor error:", + "temperatureSensorInitialError" : "Temperature sensor initialization error:", + "temperatureSensorUnavailableMessage" : "Ambient temperature sensor is not available on this device", + "sharingMessage" : "Sharing PSLab Data", + "delete" : "Delete", "deleteHint": "Are you sure you want to delete this file?", "documentationLink" : "https://docs.pslab.io/", "documentationError" : "Could not open the documentation link", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index bb89af746..8cc552eb7 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2032,6 +2032,42 @@ abstract class AppLocalizations { /// **'Select the sensor by going to the Configure tab from the bottom navigation bar and choose BMP-180 in the drop down menu under Select Sensor.'** String get baroMeterBulletPoint4; + /// No description provided for @thermometerTitle. + /// + /// In en, this message translates to: + /// **'Thermometer'** + String get thermometerTitle; + + /// No description provided for @thermometerIntro. + /// + /// In en, this message translates to: + /// **'Thermometer instrument is used to measure ambient temprature. It can be measured using inbuilt ambient temprature sensor or through SHT21.'** + String get thermometerIntro; + + /// No description provided for @celsius. + /// + /// In en, this message translates to: + /// **'°C'** + String get celsius; + + /// No description provided for @temperatureSensorError. + /// + /// In en, this message translates to: + /// **'Temperature sensor error:'** + String get temperatureSensorError; + + /// No description provided for @temperatureSensorInitialError. + /// + /// In en, this message translates to: + /// **'Temperature sensor initialization error:'** + String get temperatureSensorInitialError; + + /// No description provided for @temperatureSensorUnavailableMessage. + /// + /// In en, this message translates to: + /// **'Ambient temperature sensor is not available on this device'** + String get temperatureSensorUnavailableMessage; + /// No description provided for @sharingMessage. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 3aa1fa22a..bba6ec7ba 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1038,6 +1038,26 @@ class AppLocalizationsEn extends AppLocalizations { 'Select the sensor by going to the Configure tab from the bottom navigation bar and choose BMP-180 in the drop down menu under Select Sensor.'; @override + String get thermometerTitle => 'Thermometer'; + + @override + String get thermometerIntro => + 'Thermometer instrument is used to measure ambient temprature. It can be measured using inbuilt ambient temprature sensor or through SHT21.'; + + @override + String get celsius => '°C'; + + @override + String get temperatureSensorError => 'Temperature sensor error:'; + + @override + String get temperatureSensorInitialError => + 'Temperature sensor initialization error:'; + + @override + String get temperatureSensorUnavailableMessage => + 'Ambient temperature sensor is not available on this device'; + String get sharingMessage => 'Sharing PSLab Data'; @override diff --git a/lib/main.dart b/lib/main.dart index d5d8b7337..a895a5174 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -21,6 +21,7 @@ import 'package:pslab/view/about_us_screen.dart'; import 'package:pslab/view/software_licenses_screen.dart'; import 'package:pslab/theme/app_theme.dart'; import 'package:pslab/view/soundmeter_screen.dart'; +import 'package:pslab/view/thermometer_screen.dart'; import 'package:pslab/view/wave_generator_screen.dart'; import 'constants.dart'; @@ -76,6 +77,7 @@ class MyApp extends StatelessWidget { '/luxmeter': (context) => const LuxMeterScreen(), '/barometer': (context) => const BarometerScreen(), '/soundmeter': (context) => const SoundMeterScreen(), + '/thermometer': (context) => const ThermometerScreen(), '/sensors': (context) => const SensorsScreen() }, ); diff --git a/lib/others/temperature_service.dart b/lib/others/temperature_service.dart new file mode 100644 index 000000000..fe841e24d --- /dev/null +++ b/lib/others/temperature_service.dart @@ -0,0 +1,92 @@ +import 'dart:async'; +import 'package:flutter/services.dart'; +import 'package:pslab/others/logger_service.dart'; + +class TemperatureService { + static const MethodChannel _methodChannel = + MethodChannel('io.pslab/temperature'); + static const EventChannel _eventChannel = + EventChannel('io.pslab/temperature_stream'); + + static StreamSubscription? _temperatureSubscription; + static final StreamController _temperatureController = + StreamController.broadcast(); + + static Stream get temperatureStream => _temperatureController.stream; + + static Future isTemperatureSensorAvailable() async { + try { + final bool isAvailable = + await _methodChannel.invokeMethod('isTemperatureSensorAvailable'); + logger.d('Temperature sensor available: $isAvailable'); + return isAvailable; + } on PlatformException catch (e) { + logger.e('Error checking temperature sensor availability: ${e.message}'); + return false; + } + } + + static Future getCurrentTemperature() async { + try { + final double temperature = + await _methodChannel.invokeMethod('getCurrentTemperature'); + logger.d('Current temperature: $temperature°C'); + return temperature; + } on PlatformException catch (e) { + logger.e('Error getting current temperature: ${e.message}'); + return 0.0; + } + } + + static Future startTemperatureUpdates() async { + try { + final bool success = + await _methodChannel.invokeMethod('startTemperatureUpdates'); + if (success) { + _startListening(); + logger.d('Temperature updates started'); + } + return success; + } on PlatformException catch (e) { + logger.e('Error starting temperature updates: ${e.message}'); + return false; + } + } + + static Future stopTemperatureUpdates() async { + try { + await _methodChannel.invokeMethod('stopTemperatureUpdates'); + _stopListening(); + logger.d('Temperature updates stopped'); + } on PlatformException catch (e) { + logger.e('Error stopping temperature updates: ${e.message}'); + } + } + + static void _startListening() { + _temperatureSubscription?.cancel(); + _temperatureSubscription = _eventChannel.receiveBroadcastStream().listen( + (dynamic temperature) { + logger.d('Received temperature from stream: $temperature'); + if (temperature is double) { + _temperatureController.add(temperature); + } else if (temperature is num) { + _temperatureController.add(temperature.toDouble()); + } + }, + onError: (error) { + logger.e('Temperature stream error: $error'); + }, + ); + } + + static void _stopListening() { + _temperatureSubscription?.cancel(); + _temperatureSubscription = null; + } + + static void dispose() { + _stopListening(); + _temperatureController.close(); + } +} diff --git a/lib/providers/thermometer_state_provider.dart b/lib/providers/thermometer_state_provider.dart new file mode 100644 index 000000000..0ed0d27f6 --- /dev/null +++ b/lib/providers/thermometer_state_provider.dart @@ -0,0 +1,171 @@ +import 'dart:async'; +import 'dart:math'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:pslab/others/logger_service.dart'; +import 'package:flutter/foundation.dart'; +import 'package:pslab/others/temperature_service.dart'; + +import '../l10n/app_localizations.dart'; +import 'locator.dart'; + +class ThermometerStateProvider extends ChangeNotifier { + AppLocalizations appLocalizations = getIt.get(); + double _currentTemperature = 0.0; + Timer? _timeTimer; + StreamSubscription? _temperatureSubscription; + final List _temperatureData = []; + final List _timeData = []; + final List temperatureChartData = []; + double _startTime = 0; + double _currentTime = 0; + final int _maxLength = 50; + double _temperatureMin = 0; + double _temperatureMax = 0; + double _temperatureSum = 0; + int _dataCount = 0; + bool _isSensorAvailable = false; + bool _isInitialized = false; + + Future initializeSensors() async { + if (_isInitialized) return; + + try { + _startTime = DateTime.now().millisecondsSinceEpoch / 1000.0; + + _isSensorAvailable = + await TemperatureService.isTemperatureSensorAvailable(); + + if (_isSensorAvailable) { + final success = await TemperatureService.startTemperatureUpdates(); + if (success) { + _startListeningToTemperature(); + + _currentTemperature = + await TemperatureService.getCurrentTemperature(); + logger.d('Initial temperature: $_currentTemperature°C'); + + logger.d('Temperature sensor initialized successfully'); + } else { + logger.e('Failed to start temperature updates'); + _isSensorAvailable = false; + } + } else { + logger.w(appLocalizations.temperatureSensorUnavailableMessage); + } + + _startTimeTracking(); + _isInitialized = true; + + notifyListeners(); + } catch (e) { + logger.e("${appLocalizations.temperatureSensorInitialError} $e"); + _isSensorAvailable = false; + _isInitialized = true; + notifyListeners(); + } + } + + void _startListeningToTemperature() { + _temperatureSubscription?.cancel(); + _temperatureSubscription = TemperatureService.temperatureStream.listen( + (temperature) { + if (_isValidTemperature(temperature)) { + _currentTemperature = temperature; + logger.d('Temperature updated: $temperature°C'); + notifyListeners(); + } else { + logger.w('Invalid temperature reading: $temperature - ignoring'); + } + }, + onError: (error) { + logger.e('Temperature stream error: $error'); + }, + ); + } + + bool _isValidTemperature(double temperature) { + if (temperature.isNaN || temperature.isInfinite) return false; + if (temperature < -273.15) return false; + if (temperature > 200) return false; + if (temperature.abs() > 1e10) return false; + return true; + } + + void _startTimeTracking() { + _timeTimer?.cancel(); + _timeTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + _currentTime = + (DateTime.now().millisecondsSinceEpoch / 1000.0) - _startTime; + _updateData(); + notifyListeners(); + }); + } + + void disposeSensors() { + _timeTimer?.cancel(); + _temperatureSubscription?.cancel(); + if (_isSensorAvailable) { + TemperatureService.stopTemperatureUpdates(); + } + _isInitialized = false; + } + + @override + void dispose() { + disposeSensors(); + super.dispose(); + } + + void _updateData() { + final temperature = _currentTemperature; + final time = _currentTime; + _temperatureData.add(temperature); + _timeData.add(time); + _temperatureSum += temperature; + _dataCount++; + if (_temperatureData.length > _maxLength) { + final removedValue = _temperatureData.removeAt(0); + _timeData.removeAt(0); + _temperatureSum -= removedValue; + _dataCount--; + } + if (_temperatureData.isNotEmpty) { + _temperatureMin = _temperatureData.reduce(min); + _temperatureMax = _temperatureData.reduce(max); + } + temperatureChartData.clear(); + for (int i = 0; i < _temperatureData.length; i++) { + temperatureChartData.add(FlSpot(_timeData[i], _temperatureData[i])); + } + } + + double getCurrentTemperature() => _currentTemperature; + + double getMinTemperature() => _temperatureMin; + + double getMaxTemperature() => _temperatureMax; + + double getAverageTemperature() => + _dataCount > 0 ? _temperatureSum / _dataCount : 0.0; + + List getTemperatureChartData() => temperatureChartData; + + int getDataLength() => temperatureChartData.length; + + double getCurrentTime() => _currentTime; + + double getMaxTime() => _timeData.isNotEmpty ? _timeData.last : 0; + + double getMinTime() => _timeData.isNotEmpty ? _timeData.first : 0; + + bool isSensorAvailable() => _isSensorAvailable; + + bool isInitialized() => _isInitialized; + + double getTimeInterval() { + if (_currentTime <= 10) return 2; + if (_currentTime <= 30) return 5; + return 10; + } +} diff --git a/lib/view/instruments_screen.dart b/lib/view/instruments_screen.dart index 68bee8199..dddc5cd2d 100644 --- a/lib/view/instruments_screen.dart +++ b/lib/view/instruments_screen.dart @@ -129,6 +129,18 @@ class _InstrumentsScreenState extends State { ); } break; + case 11: + if (Navigator.canPop(context) && + ModalRoute.of(context)?.settings.name == '/thermometer') { + Navigator.popUntil(context, ModalRoute.withName('/thermometer')); + } else { + Navigator.pushNamedAndRemoveUntil( + context, + '/thermometer', + (route) => route.isFirst, + ); + } + break; case 12: if (Navigator.canPop(context) && ModalRoute.of(context)?.settings.name == '/roboticArm') { diff --git a/lib/view/thermometer_screen.dart b/lib/view/thermometer_screen.dart new file mode 100644 index 000000000..d0c74714c --- /dev/null +++ b/lib/view/thermometer_screen.dart @@ -0,0 +1,315 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:pslab/providers/thermometer_state_provider.dart'; +import 'package:pslab/view/widgets/common_scaffold_widget.dart'; +import 'package:pslab/view/widgets/guide_widget.dart'; +import 'package:pslab/view/widgets/thermometer_card.dart'; +import 'package:fl_chart/fl_chart.dart'; + +import '../l10n/app_localizations.dart'; +import '../providers/locator.dart'; +import '../theme/colors.dart'; + +class ThermometerScreen extends StatefulWidget { + const ThermometerScreen({super.key}); + @override + State createState() => _ThermometerScreenState(); +} + +class _ThermometerScreenState extends State { + AppLocalizations appLocalizations = getIt.get(); + ThermometerStateProvider? _temperatureProvider; + bool _showGuide = false; + bool _snackbarShown = false; + + @override + void initState() { + super.initState(); + _initializeProvider(); + } + + Future _initializeProvider() async { + _temperatureProvider = ThermometerStateProvider(); + await _temperatureProvider!.initializeSensors(); + } + + @override + void dispose() { + _temperatureProvider?.dispose(); + super.dispose(); + } + + void _showSensorErrorSnackbar(String message) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + message, + style: TextStyle(color: Colors.white), + ), + backgroundColor: Colors.grey[700], + duration: const Duration(seconds: 4), + behavior: SnackBarBehavior.floating, + ), + ); + } + } + + void _showInstrumentGuide() { + setState(() { + _showGuide = true; + }); + } + + void _hideInstrumentGuide() { + setState(() { + _showGuide = false; + }); + } + + List _getThermometerContent() { + return [ + InstrumentIntroText( + text: appLocalizations.thermometerIntro, + ), + ]; + } + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider.value( + value: _temperatureProvider!, + child: Consumer( + builder: (context, provider, child) { + if (!provider.isSensorAvailable() && + !_snackbarShown && + provider.isInitialized()) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _showSensorErrorSnackbar( + appLocalizations.temperatureSensorUnavailableMessage); + _snackbarShown = true; + }); + } + + return Stack( + children: [ + CommonScaffold( + title: appLocalizations.thermometerTitle, + onGuidePressed: _showInstrumentGuide, + body: SafeArea( + child: LayoutBuilder(builder: (context, constraints) { + final isLargeScreen = constraints.maxWidth > 900; + if (isLargeScreen) { + return Row( + children: [ + const Expanded( + flex: 35, + child: ThermometerCard(), + ), + Expanded( + flex: 65, + child: _buildChartSection(), + ), + ], + ); + } else { + return Column( + children: [ + const Expanded( + flex: 45, + child: ThermometerCard(), + ), + Expanded( + flex: 55, + child: _buildChartSection(), + ), + ], + ); + } + })), + ), + if (_showGuide) + InstrumentOverviewDrawer( + instrumentName: appLocalizations.thermometerTitle, + content: _getThermometerContent(), + onHide: _hideInstrumentGuide, + ), + ], + ); + }, + ), + ); + } + + Widget _buildChartSection() { + return Consumer( + builder: (context, provider, child) { + final screenWidth = MediaQuery.of(context).size.width; + final cardMargin = screenWidth < 400 ? 8.0 : 12.0; + final cardPadding = screenWidth < 400 ? 2.0 : 5.0; + List spots = provider.getTemperatureChartData(); + double maxTime = provider.getMaxTime(); + double minTime = provider.getMinTime(); + double timeInterval = provider.getTimeInterval(); + return Container( + margin: EdgeInsets.fromLTRB(cardMargin, 0, cardMargin, cardMargin), + padding: EdgeInsets.all(cardPadding), + decoration: BoxDecoration( + color: chartBackgroundColor, + borderRadius: BorderRadius.zero, + ), + child: + _buildChart(screenWidth, maxTime, minTime, timeInterval, spots), + ); + }, + ); + } + + Widget sideTitleWidgets(double value, TitleMeta meta) { + final screenWidth = MediaQuery.of(context).size.width; + final fontSize = screenWidth < 400 + ? 7.0 + : screenWidth < 600 + ? 8.0 + : 9.0; + final style = TextStyle( + color: chartTextColor, + fontSize: fontSize, + ); + String timeText; + if (value < 60) { + timeText = '${value.toInt()}s'; + } else if (value < 3600) { + int minutes = (value / 60).floor(); + int seconds = (value % 60).toInt(); + timeText = '${minutes}m${seconds}s'; + } else { + int hours = (value / 3600).floor(); + int minutes = ((value % 3600) / 60).floor(); + timeText = '${hours}h${minutes}m'; + } + return SideTitleWidget( + meta: meta, + child: Text( + maxLines: 1, + timeText, + style: style, + ), + ); + } + + Widget _buildChart(double screenWidth, double maxTime, double minTime, + double timeInterval, List spots) { + final chartFontSize = screenWidth < 400 + ? 8.0 + : screenWidth < 600 + ? 9.0 + : 10.0; + final axisNameFontSize = screenWidth < 400 ? 9.0 : 10.0; + final reservedSizeBottom = screenWidth < 400 ? 25.0 : 30.0; + final reservedSizeLeft = screenWidth < 400 ? 25.0 : 30.0; + final reservedSizeRight = screenWidth < 400 ? 25.0 : 30.0; + double minY = spots.isNotEmpty + ? spots.map((s) => s.y).reduce((a, b) => a < b ? a : b) + : 0.0; + double maxY = spots.isNotEmpty + ? spots.map((s) => s.y).reduce((a, b) => a > b ? a : b) + : 50.0; + return Padding( + padding: const EdgeInsets.only(right: 20.0), + child: LineChart( + LineChartData( + backgroundColor: chartBackgroundColor, + titlesData: FlTitlesData( + show: true, + topTitles: AxisTitles( + axisNameWidget: Padding( + padding: EdgeInsets.only(left: screenWidth < 400 ? 15 : 25), + child: Text( + appLocalizations.timeAxisLabel, + style: TextStyle( + fontSize: axisNameFontSize, + color: chartTextColor, + fontWeight: FontWeight.bold, + ), + ), + ), + axisNameSize: screenWidth < 400 ? 18 : 20, + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: reservedSizeBottom, + getTitlesWidget: sideTitleWidgets, + interval: timeInterval, + ), + ), + leftTitles: AxisTitles( + axisNameWidget: Text( + appLocalizations.celsius, + style: TextStyle( + fontSize: axisNameFontSize, + color: chartTextColor, + fontWeight: FontWeight.bold, + ), + ), + sideTitles: SideTitles( + reservedSize: reservedSizeLeft, + showTitles: true, + getTitlesWidget: (value, meta) { + return SideTitleWidget( + meta: meta, + child: Text( + value.toInt().toString(), + style: TextStyle( + color: chartTextColor, + fontSize: chartFontSize, + ), + ), + ); + }, + interval: 5, + ), + ), + rightTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: false, reservedSize: reservedSizeRight), + ), + ), + gridData: FlGridData( + show: true, + drawHorizontalLine: true, + drawVerticalLine: true, + horizontalInterval: 10, + verticalInterval: timeInterval, + ), + borderData: FlBorderData( + show: true, + border: Border( + bottom: BorderSide(color: chartBorderColor), + left: BorderSide(color: chartBorderColor), + top: BorderSide(color: chartBorderColor), + right: BorderSide(color: chartBorderColor), + ), + ), + minY: minY < -40 ? minY - 3 : -40, + maxY: maxY > 50 ? maxY + 3 : 50, + maxX: maxTime > 0 ? maxTime : 10, + minX: minTime, + clipData: const FlClipData.all(), + lineBarsData: [ + LineChartBarData( + spots: spots, + isCurved: true, + color: chartLineColor, + barWidth: screenWidth < 400 ? 1.5 : 2.0, + isStrokeCapRound: true, + dotData: const FlDotData(show: false), + belowBarData: BarAreaData(show: false), + ), + ], + ), + ), + ); + } +} diff --git a/lib/view/widgets/thermometer_card.dart b/lib/view/widgets/thermometer_card.dart new file mode 100644 index 000000000..adf597990 --- /dev/null +++ b/lib/view/widgets/thermometer_card.dart @@ -0,0 +1,111 @@ +import 'package:pslab/theme/colors.dart'; +import 'package:pslab/view/widgets/gauge_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:pslab/providers/thermometer_state_provider.dart'; +import 'package:provider/provider.dart'; +import 'package:pslab/view/widgets/instruments_stats.dart'; + +import '../../l10n/app_localizations.dart'; +import '../../providers/locator.dart'; + +class ThermometerCard extends StatefulWidget { + const ThermometerCard({super.key}); + @override + State createState() => _ThermometerCardState(); +} + +class _ThermometerCardState extends State { + AppLocalizations appLocalizations = getIt.get(); + + @override + Widget build(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + final isLargeScreen = screenWidth > 900; + ThermometerStateProvider provider = + Provider.of(context); + double currentTemp = provider.getCurrentTemperature(); + double minTemp = provider.getMinTemperature(); + double maxTemp = provider.getMaxTemperature(); + double avgTemp = provider.getAverageTemperature(); + final cardMargin = screenWidth < 400 ? 8.0 : 12.0; + final cardPadding = screenWidth < 400 ? 12.0 : 20.0; + final gaugeSize = isLargeScreen ? 240.0 : screenWidth * 0.45; + final titleFontSize = isLargeScreen ? 25.0 : 20.0; + final statFontSize = isLargeScreen ? 20.0 : 15.0; + final tempValueFontSize = isLargeScreen ? 20.0 : 16.0; + + return Card( + margin: EdgeInsets.all(cardMargin), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 1, + child: Container( + decoration: BoxDecoration( + color: cardBackgroundColor, + borderRadius: BorderRadius.circular(12), + ), + child: Container( + padding: EdgeInsets.all(cardPadding), + child: LayoutBuilder( + builder: (context, constraints) { + if (isLargeScreen) { + return Column( + children: [ + Expanded( + flex: 40, + child: GaugeWidget( + gaugeSize: gaugeSize, + currentValue: currentTemp, + minValue: -40, + maxValue: 125, + unit: appLocalizations.celsius, + currentValueFontSize: tempValueFontSize), + ), + Expanded( + flex: 60, + child: Instrumentstats( + titleFontSize: titleFontSize, + statFontSize: statFontSize, + maxValue: maxTemp, + minValue: minTemp, + avgValue: avgTemp, + unit: appLocalizations.celsius, + ), + ), + ], + ); + } else { + return Row( + children: [ + Expanded( + flex: screenWidth < 500 ? 40 : 35, + child: Instrumentstats( + titleFontSize: titleFontSize, + statFontSize: statFontSize, + maxValue: maxTemp, + minValue: minTemp, + avgValue: avgTemp, + unit: appLocalizations.celsius, + ), + ), + Expanded( + flex: screenWidth < 500 ? 60 : 65, + child: GaugeWidget( + gaugeSize: gaugeSize, + currentValue: currentTemp, + minValue: -40, + maxValue: 125, + unit: appLocalizations.celsius, + currentValueFontSize: tempValueFontSize), + ), + ], + ); + } + }, + ), + ), + ), + ); + } +}