diff --git a/lib/communication/sensors/sht21.dart b/lib/communication/sensors/sht21.dart new file mode 100644 index 000000000..1d608cc15 --- /dev/null +++ b/lib/communication/sensors/sht21.dart @@ -0,0 +1,44 @@ +import 'dart:async'; +import '../peripherals/i2c.dart'; + +class SHT21 { + final I2C i2c; + static const int address = 0x40; + + // Commands (Use Hold Master Mode for readBulk compatibility) + static const int tempHoldCmd = 0xE3; + static const int humidityHoldCmd = 0xE5; + + SHT21(this.i2c); + + Future getTemperature() async { + // FIX: Use readBulk to handle the Write -> Restart -> Read sequence automatically. + // This avoids the argument errors with .write() and .read() + List data = await i2c.readBulk(address, tempHoldCmd, 3); + + if (data.length < 2) { + throw Exception("Failed to read temperature from SHT21"); + } + + // Mask out status bits (last 2 bits) using & 0xFC + int rawValue = (data[0] << 8) | (data[1] & 0xFC); + + // Formula: T = -46.85 + 175.72 * (S_T / 2^16) + return -46.85 + 175.72 * (rawValue / 65536.0); + } + + Future getHumidity() async { + // FIX: Use readBulk here too + List data = await i2c.readBulk(address, humidityHoldCmd, 3); + + if (data.length < 2) { + throw Exception("Failed to read humidity from SHT21"); + } + + // Mask out status bits + int rawValue = (data[0] << 8) | (data[1] & 0xFC); + + // Formula: RH = -6 + 125 * (S_RH / 2^16) + return -6.0 + 125.0 * (rawValue / 65536.0); + } +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 89c646d0a..d9db023b2 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -523,5 +523,6 @@ "light": "Light", "darkExperimental": "Dark (Experimental)", "system": "System", - "shareApp": "Share App" + "shareApp": "Share App", + "sht21Config": "SHT21 Configurations" } diff --git a/lib/main.dart b/lib/main.dart index 25d75094e..955f9b44a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,4 @@ +import 'package:pslab/providers/sht21_provider.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:provider/provider.dart'; @@ -42,6 +43,10 @@ void main() { ChangeNotifierProvider( create: (context) => getIt(), ), + ChangeNotifierProvider( + // FIX: Use getIt to resolve the provider instead of 'SHT21Provider()' + create: (context) => getIt(), + ), ], child: const MyApp(), ), diff --git a/lib/providers/locator.dart b/lib/providers/locator.dart index e5b635738..6687b19a9 100644 --- a/lib/providers/locator.dart +++ b/lib/providers/locator.dart @@ -10,6 +10,7 @@ import 'package:pslab/communication/science_lab.dart'; import 'package:pslab/communication/socket_client.dart'; import 'package:pslab/others/science_lab_common.dart'; import 'package:pslab/providers/board_state_provider.dart'; +import 'package:pslab/providers/sht21_provider.dart'; final GetIt getIt = GetIt.instance; @@ -29,6 +30,7 @@ void setupLocator() { getIt.registerLazySingleton( () => getIt.get().getScienceLab()); getIt.registerLazySingleton(() => BoardStateProvider()); + getIt.registerLazySingleton(() => SHT21Provider()); } void registerAppLocalizations(AppLocalizations appLocalizations) { diff --git a/lib/providers/sht21_provider.dart b/lib/providers/sht21_provider.dart new file mode 100644 index 000000000..c6a17daa6 --- /dev/null +++ b/lib/providers/sht21_provider.dart @@ -0,0 +1,54 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import '../communication/peripherals/i2c.dart'; +import '../communication/sensors/sht21.dart'; + +class SHT21Provider extends ChangeNotifier { + SHT21? _sensor; + Timer? _timer; + + // These are the public variables the Screen is trying to read + double temperature = 0.0; + double humidity = 0.0; + + bool isRecording = false; + + void init(I2C i2c) { + _sensor = SHT21(i2c); + } + + void startDataLog() { + if (_sensor == null || isRecording) return; + + isRecording = true; + _timer = Timer.periodic(const Duration(milliseconds: 1000), (timer) async { + try { + // Fetch new values + double temp = await _sensor!.getTemperature(); + double hum = await _sensor!.getHumidity(); + + // Update variables + temperature = temp; + humidity = hum; + + // Notify the screen to update + notifyListeners(); + } catch (e) { + if (kDebugMode) { + print("SHT21 Error: $e"); + } + } + }); + } + + void stopDataLog() { + _timer?.cancel(); + isRecording = false; + } + + @override + void dispose() { + stopDataLog(); + super.dispose(); + } +} diff --git a/lib/view/sensors_screen.dart b/lib/view/sensors_screen.dart index d43df3832..49ac3e2b9 100644 --- a/lib/view/sensors_screen.dart +++ b/lib/view/sensors_screen.dart @@ -1,3 +1,4 @@ +import 'package:pslab/view/sht21_screen.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:pslab/view/bmp180_screen.dart'; @@ -229,9 +230,13 @@ class _SensorsScreenState extends State { break; case 'APDS9960': targetScreen = const APDS9960Screen(); + break; case 'VL53L0X': targetScreen = const VL53L0XScreen(); break; + case 'SHT21': + targetScreen = const SHT21Screen(); + break; default: ScaffoldMessenger.of(context).showSnackBar( SnackBar( diff --git a/lib/view/sht21_screen.dart b/lib/view/sht21_screen.dart new file mode 100644 index 000000000..9d3bb040a --- /dev/null +++ b/lib/view/sht21_screen.dart @@ -0,0 +1,123 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:pslab/communication/science_lab.dart'; +import 'package:pslab/communication/peripherals/i2c.dart'; +import 'package:pslab/providers/locator.dart'; +import 'package:pslab/l10n/app_localizations.dart'; +import '../providers/sht21_provider.dart'; + +class SHT21Screen extends StatefulWidget { + const SHT21Screen({super.key}); + + @override + State createState() => _SHT21ScreenState(); +} + +class _SHT21ScreenState extends State { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + + final provider = Provider.of(context, listen: false); + final scienceLab = getIt(); + final appLocalizations = AppLocalizations.of(context)!; + + if (scienceLab.isConnected()) { + I2C i2c = I2C(scienceLab.mPacketHandler); + provider.init(i2c); + provider.startDataLog(); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(appLocalizations.notConnected)), + ); + } + }); + } + + @override + void dispose() { + if (mounted) { + Provider.of(context, listen: false).stopDataLog(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final appLocalizations = AppLocalizations.of(context)!; + + return Scaffold( + appBar: AppBar( + title: const Text('SHT21 Sensor'), + ), + body: Consumer( + builder: (context, provider, child) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + _buildSensorCard( + // Using the localized string we added earlier + title: appLocalizations.temperature, + value: provider.temperature.toStringAsFixed(2), + unit: "°C", + icon: Icons.thermostat, + color: Colors.redAccent, + ), + const SizedBox(height: 20), + _buildSensorCard( + title: "Humidity", + value: provider.humidity.toStringAsFixed(2), + unit: "%", + icon: Icons.water_drop, + color: Colors.blueAccent, + ), + ], + ), + ); + }, + ), + ); + } + + Widget _buildSensorCard({ + required String title, + required String value, + required String unit, + required IconData icon, + required Color color, + }) { + return Card( + elevation: 4, + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Row( + children: [ + Icon(icon, size: 40, color: color), + const SizedBox(width: 20), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, + style: const TextStyle(fontSize: 16, color: Colors.grey)), + Row( + children: [ + Text(value, + style: const TextStyle( + fontSize: 32, fontWeight: FontWeight.bold)), + const SizedBox(width: 5), + Text(unit, style: const TextStyle(fontSize: 20)), + ], + ), + ], + ), + ), + ], + ), + ), + ); + } +}