diff --git a/lib/communication/sensors/hmc5883l.dart b/lib/communication/sensors/hmc5883l.dart new file mode 100644 index 000000000..3d991a29f --- /dev/null +++ b/lib/communication/sensors/hmc5883l.dart @@ -0,0 +1,311 @@ +import 'dart:math'; +import 'package:pslab/communication/peripherals/i2c.dart'; +import 'package:pslab/communication/science_lab.dart'; +import 'package:pslab/others/logger_service.dart'; + +import '../../l10n/app_localizations.dart'; +import '../../providers/locator.dart'; + +class HMC5883L { + AppLocalizations appLocalizations = getIt.get(); + + static const String tag = "HMC5883L"; + static const int address = 0x1E; + + // Register addresses + static const int configRegA = 0x00; + static const int configRegB = 0x01; + static const int modeReg = 0x02; + static const int dataXMSB = 0x03; + static const int dataXLSB = 0x04; + static const int dataZMSB = 0x05; + static const int dataZLSB = 0x06; + static const int dataYMSB = 0x07; + static const int dataYLSB = 0x08; + static const int statusReg = 0x09; + static const int idRegA = 0x0A; + static const int idRegB = 0x0B; + static const int idRegC = 0x0C; + + // Configuration values for Register A + static const int samplesAverage1 = 0x00; + static const int samplesAverage2 = 0x20; + static const int samplesAverage4 = 0x40; + static const int samplesAverage8 = 0x60; + + static const int dataRate0_75Hz = 0x00; + static const int dataRate1_5Hz = 0x04; + static const int dataRate3Hz = 0x08; + static const int dataRate7_5Hz = 0x0C; + static const int dataRate15Hz = 0x10; + static const int dataRate30Hz = 0x14; + static const int dataRate75Hz = 0x18; + + static const int measurementNormal = 0x00; + static const int measurementPositiveBias = 0x01; + static const int measurementNegativeBias = 0x02; + + // Configuration values for Register B (Gain) + static const int gain1370 = 0x00; // ±0.88 Ga + static const int gain1090 = 0x20; // ±1.3 Ga (default) + static const int gain820 = 0x40; // ±1.9 Ga + static const int gain660 = 0x60; // ±2.5 Ga + static const int gain440 = 0x80; // ±4.0 Ga + static const int gain390 = 0xA0; // ±4.7 Ga + static const int gain330 = 0xC0; // ±5.6 Ga + static const int gain230 = 0xE0; // ±8.1 Ga + + // Mode register values + static const int modeContinuous = 0x00; + static const int modeSingle = 0x01; + static const int modeIdle = 0x02; + + // Gain scaling factors (LSB/Gauss) + static const Map gainScales = { + gain1370: 1370, + gain1090: 1090, + gain820: 820, + gain660: 660, + gain440: 440, + gain390: 390, + gain330: 330, + gain230: 230, + }; + + final I2C i2c; + int currentGain = gain1090; + double scale = 1090; + + double magneticX = 0.0; + double magneticY = 0.0; + double magneticZ = 0.0; + + double offsetX = 0.0; + double offsetY = 0.0; + double offsetZ = 0.0; + + HMC5883L._(this.i2c); + + static Future create(I2C i2c, ScienceLab scienceLab) async { + final hmc5883l = HMC5883L._(i2c); + await hmc5883l._initialize(scienceLab); + return hmc5883l; + } + + Future _initialize(ScienceLab scienceLab) async { + if (!scienceLab.isConnected()) { + throw Exception("ScienceLab not connected"); + } + + try { + final idA = await i2c.readByte(address, idRegA); + final idB = await i2c.readByte(address, idRegB); + final idC = await i2c.readByte(address, idRegC); + + logger.d("HMC5883L IDs: A=0x${idA.toRadixString(16)}, " + "B=0x${idB.toRadixString(16)}, C=0x${idC.toRadixString(16)}"); + + if (idA != 0x48 || idB != 0x34 || idC != 0x33) { + logger.w("HMC5883L ID mismatch, but continuing initialization"); + } + + await configure(); + logger.d("HMC5883L initialized successfully"); + } catch (e) { + logger.e("Error initializing HMC5883L: $e"); + rethrow; + } + } + + /// Configure the HMC5883L sensor + Future configure({ + int samplesAverage = samplesAverage8, + int dataRate = dataRate15Hz, + int measurementMode = measurementNormal, + int gain = gain1090, + int mode = modeContinuous, + }) async { + try { + int configA = samplesAverage | dataRate | measurementMode; + await i2c.write(address, [configA], configRegA); + + await i2c.write(address, [gain], configRegB); + currentGain = gain; + scale = gainScales[gain] ?? 1090; + + await i2c.write(address, [mode], modeReg); + + await Future.delayed(const Duration(milliseconds: 10)); + + logger.d("HMC5883L configured: gain=$gain, scale=$scale, mode=$mode"); + } catch (e) { + logger.e("Error configuring HMC5883L: $e"); + rethrow; + } + } + + /// Read raw magnetic field data + Future> readRawData() async { + try { + List data = await i2c.readBulk(address, dataXMSB, 6); + + if (data.length < 6) { + throw Exception( + "Expected 6 bytes but got ${data.length} from HMC5883L"); + } + + int xRaw = _toSignedInt16((data[0] << 8) | data[1]); + int zRaw = _toSignedInt16((data[2] << 8) | data[3]); + int yRaw = _toSignedInt16((data[4] << 8) | data[5]); + + return { + 'x': xRaw, + 'y': yRaw, + 'z': zRaw, + }; + } catch (e) { + logger.e("Error reading raw data from HMC5883L: $e"); + rethrow; + } + } + + /// Read magnetic field data in microTesla (µT) + /// Optionally accepts pre-read rawData to avoid redundant I2C reads + Future> readMagneticField({Map? rawData}) async { + try { + final data = rawData ?? await readRawData(); + + magneticX = (data['x']! / scale) * 100.0 - offsetX; + magneticY = (data['y']! / scale) * 100.0 - offsetY; + magneticZ = (data['z']! / scale) * 100.0 - offsetZ; + + return { + 'x': magneticX, + 'y': magneticY, + 'z': magneticZ, + }; + } catch (e) { + logger.e("Error reading magnetic field: $e"); + rethrow; + } + } + + /// Calculate heading angle (0-360 degrees) + /// Assumes the sensor is level (parallel to ground) + /// Optionally accepts pre-computed magneticField to avoid redundant I2C reads + Future readHeading({Map? magneticField}) async { + try { + final field = magneticField ?? await readMagneticField(); + + double heading = atan2(field['y']!, field['x']!); + + double headingDegrees = heading * (180.0 / pi); + + if (headingDegrees < 0) { + headingDegrees += 360; + } + + return headingDegrees; + } catch (e) { + logger.e("Error calculating heading: $e"); + rethrow; + } + } + + /// Calculate total magnetic field magnitude + /// Optionally accepts pre-computed magneticField to avoid redundant I2C reads + Future readMagnitude({Map? magneticField}) async { + try { + final field = magneticField ?? await readMagneticField(); + return sqrt(field['x']! * field['x']! + + field['y']! * field['y']! + + field['z']! * field['z']!); + } catch (e) { + logger.e("Error calculating magnitude: $e"); + rethrow; + } + } + + void setCalibrationOffsets(double minX, double maxX, double minY, + double maxY, double minZ, double maxZ) { + offsetX = (minX + maxX) / 2.0; + offsetY = (minY + maxY) / 2.0; + offsetZ = (minZ + maxZ) / 2.0; + + logger.d("HMC5883L calibration offsets set: " + "X=$offsetX, Y=$offsetY, Z=$offsetZ"); + } + + Future> getAllData() async { + try { + // Perform single I2C read + final rawData = await readRawData(); + + // Reuse raw data for magnetic field calculation + final magneticField = await readMagneticField(rawData: rawData); + + // Reuse magnetic field for heading and magnitude calculations + final heading = await readHeading(magneticField: magneticField); + final magnitude = await readMagnitude(magneticField: magneticField); + + return { + 'raw_x': rawData['x'], + 'raw_y': rawData['y'], + 'raw_z': rawData['z'], + 'magnetic_x': magneticX, + 'magnetic_y': magneticY, + 'magnetic_z': magneticZ, + 'heading': heading, + 'magnitude': magnitude, + }; + } catch (e) { + logger.e("Error getting all data: $e"); + rethrow; + } + } + + /// Read status register + Future readStatus() async { + try { + return await i2c.readByte(address, statusReg); + } catch (e) { + logger.e("Error reading status: $e"); + rethrow; + } + } + + /// Check if data is ready + Future isDataReady() async { + try { + final status = await readStatus(); + return (status & 0x01) != 0; + } catch (e) { + logger.e("Error checking data ready: $e"); + return false; + } + } + + int _toSignedInt16(int value) { + if (value > 32767) { + return value - 65536; + } + return value; + } + + Future setGain(int gain) async { + if (!gainScales.containsKey(gain)) { + throw Exception("Invalid gain value: $gain"); + } + await configure(gain: gain); + } + + /// Set measurement mode + Future setMode(int mode) async { + try { + await i2c.write(address, [mode], modeReg); + } catch (e) { + logger.e("Error setting mode: $e"); + rethrow; + } + } +} diff --git a/lib/providers/hmc5883l_provider.dart b/lib/providers/hmc5883l_provider.dart new file mode 100644 index 000000000..ee31304a5 --- /dev/null +++ b/lib/providers/hmc5883l_provider.dart @@ -0,0 +1,280 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:pslab/communication/peripherals/i2c.dart'; +import 'package:pslab/communication/science_lab.dart'; +import '../communication/sensors/hmc5883l.dart'; +import '../l10n/app_localizations.dart'; +import '../models/chart_data_points.dart'; +import 'package:pslab/others/logger_service.dart'; +import 'locator.dart'; + +class HMC5883LProvider extends ChangeNotifier { + AppLocalizations appLocalizations = getIt.get(); + + HMC5883L? _hmc5883l; + Timer? _dataTimer; + + double _magneticX = 0.0; + double _magneticY = 0.0; + double _magneticZ = 0.0; + double _heading = 0.0; + double _magnitude = 0.0; + + final List _magneticXData = []; + final List _magneticYData = []; + final List _magneticZData = []; + final List _headingData = []; + final List _magnitudeData = []; + + bool _isRunning = false; + bool _isLooping = false; + int _timegapMs = 1000; + int _numberOfReadings = 100; + int _collectedReadings = 0; + + bool _isCalibrating = false; + double _minX = double.infinity; + double _maxX = double.negativeInfinity; + double _minY = double.infinity; + double _maxY = double.negativeInfinity; + double _minZ = double.infinity; + double _maxZ = double.negativeInfinity; + + double _currentTime = 0.0; + static const int maxDataPoints = 1000; + + double get magneticX => _magneticX; + double get magneticY => _magneticY; + double get magneticZ => _magneticZ; + double get heading => _heading; + double get magnitude => _magnitude; + + List get magneticXData => List.unmodifiable(_magneticXData); + List get magneticYData => List.unmodifiable(_magneticYData); + List get magneticZData => List.unmodifiable(_magneticZData); + List get headingData => List.unmodifiable(_headingData); + List get magnitudeData => List.unmodifiable(_magnitudeData); + + bool get isRunning => _isRunning; + bool get isLooping => _isLooping; + bool get isCalibrating => _isCalibrating; + int get timegapMs => _timegapMs; + int get numberOfReadings => _numberOfReadings; + int get collectedReadings => _collectedReadings; + + HMC5883LProvider(); + + Future initializeSensors({ + required Function(String) onError, + required I2C? i2c, + required ScienceLab? scienceLab, + }) async { + try { + if (i2c == null || scienceLab == null) { + onError(appLocalizations.pslabNotConnected); + logger.w('I2C or ScienceLab not available'); + return; + } + + if (!scienceLab.isConnected()) { + onError(appLocalizations.pslabNotConnected); + logger.w("Sciencelab not connected"); + return; + } + + _hmc5883l = await HMC5883L.create(i2c, scienceLab); + logger.d("HMC5883L sensor initialized successfully"); + notifyListeners(); + } catch (e, stackTrace) { + logger.e('Error initializing HMC5883L', e, stackTrace); + onError('${appLocalizations.magnetometerError} ${e.toString()}'); + } + } + + void toggleDataCollection() { + if (_isRunning) { + _stopDataCollection(); + } else { + _startDataCollection(); + } + } + + void _startDataCollection() { + if (_hmc5883l == null) return; + + _isRunning = true; + _collectedReadings = 0; + _currentTime = 0.0; + + _dataTimer = + Timer.periodic(Duration(milliseconds: _timegapMs), (timer) async { + try { + await _fetchSensorData(); + _collectedReadings++; + + if (!_isLooping && _collectedReadings >= _numberOfReadings) { + _stopDataCollection(); + return; + } + + notifyListeners(); + } catch (e) { + logger.e('Error fetching HMC5883L data: $e'); + } + }); + } + + void _stopDataCollection() { + _isRunning = false; + _dataTimer?.cancel(); + _dataTimer = null; + notifyListeners(); + } + + Future _fetchSensorData() async { + if (_hmc5883l == null) return; + + try { + final data = await _hmc5883l!.getAllData(); + + _magneticX = data['magnetic_x'] ?? 0.0; + _magneticY = data['magnetic_y'] ?? 0.0; + _magneticZ = data['magnetic_z'] ?? 0.0; + _heading = data['heading'] ?? 0.0; + _magnitude = data['magnitude'] ?? 0.0; + + if (_isCalibrating) { + _updateCalibrationData(_magneticX, _magneticY, _magneticZ); + } + + _magneticXData.add(ChartDataPoint(_currentTime, _magneticX)); + _magneticYData.add(ChartDataPoint(_currentTime, _magneticY)); + _magneticZData.add(ChartDataPoint(_currentTime, _magneticZ)); + _headingData.add(ChartDataPoint(_currentTime, _heading)); + _magnitudeData.add(ChartDataPoint(_currentTime, _magnitude)); + + if (_magneticXData.length > maxDataPoints) { + _magneticXData.removeAt(0); + _magneticYData.removeAt(0); + _magneticZData.removeAt(0); + _headingData.removeAt(0); + _magnitudeData.removeAt(0); + } + + _currentTime += _timegapMs / 1000.0; + + logger.d( + 'HMC5883L data: X=$_magneticX µT, Y=$_magneticY µT, Z=$_magneticZ µT, ' + 'Heading=$_heading°, Magnitude=$_magnitude µT'); + } catch (e) { + logger.e('Error fetching sensor data: $e'); + } + } + + void _updateCalibrationData(double x, double y, double z) { + if (x < _minX) _minX = x; + if (x > _maxX) _maxX = x; + if (y < _minY) _minY = y; + if (y > _maxY) _maxY = y; + if (z < _minZ) _minZ = z; + if (z > _maxZ) _maxZ = z; + } + + void startCalibration() { + _isCalibrating = true; + _minX = double.infinity; + _maxX = double.negativeInfinity; + _minY = double.infinity; + _maxY = double.negativeInfinity; + _minZ = double.infinity; + _maxZ = double.negativeInfinity; + + if (!_isRunning) { + _startDataCollection(); + } + + notifyListeners(); + logger.d("Started HMC5883L calibration"); + } + + void stopCalibration() { + if (!_isCalibrating) return; + + _isCalibrating = false; + + if (_hmc5883l != null && + _minX != double.infinity && + _maxX != double.negativeInfinity) { + _hmc5883l!.setCalibrationOffsets( + _minX, + _maxX, + _minY, + _maxY, + _minZ, + _maxZ, + ); + + logger.d("HMC5883L calibration completed and applied"); + } + + notifyListeners(); + } + + String getCalibrationStatus() { + if (!_isCalibrating) { + return "Not calibrating"; + } + + return "X: [${_minX.toStringAsFixed(1)}, ${_maxX.toStringAsFixed(1)}] µT\n" + "Y: [${_minY.toStringAsFixed(1)}, ${_maxY.toStringAsFixed(1)}] µT\n" + "Z: [${_minZ.toStringAsFixed(1)}, ${_maxZ.toStringAsFixed(1)}] µT"; + } + + void setTimegap(int ms) { + _timegapMs = ms; + if (_isRunning) { + _stopDataCollection(); + _startDataCollection(); + } + notifyListeners(); + } + + void setNumberOfReadings(int count) { + _numberOfReadings = count; + notifyListeners(); + } + + void toggleLooping() { + _isLooping = !_isLooping; + notifyListeners(); + } + + void clearData() { + _magneticXData.clear(); + _magneticYData.clear(); + _magneticZData.clear(); + _headingData.clear(); + _magnitudeData.clear(); + _currentTime = 0.0; + _collectedReadings = 0; + notifyListeners(); + } + + Future setGain(int gain) async { + if (_hmc5883l == null) return; + + try { + await _hmc5883l!.setGain(gain); + logger.d("HMC5883L gain set to: $gain"); + notifyListeners(); + } catch (e) { + logger.e("Error setting gain: $e"); + } + } + + @override + void dispose() { + _dataTimer?.cancel(); + super.dispose(); + } +} diff --git a/lib/view/hmc5883l_screen.dart b/lib/view/hmc5883l_screen.dart new file mode 100644 index 000000000..f87d09031 --- /dev/null +++ b/lib/view/hmc5883l_screen.dart @@ -0,0 +1,528 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:pslab/view/widgets/common_scaffold_widget.dart'; +import 'package:pslab/view/widgets/sensor_controls.dart'; +import 'package:pslab/communication/peripherals/i2c.dart'; +import 'package:pslab/communication/science_lab.dart'; +import 'package:pslab/providers/locator.dart'; +import 'package:pslab/others/logger_service.dart'; +import '../l10n/app_localizations.dart'; +import '../theme/colors.dart'; +import 'widgets/sensor_chart_widget.dart'; +import '../providers/hmc5883l_provider.dart'; +import '../communication/sensors/hmc5883l.dart'; + +class HMC5883LScreen extends StatefulWidget { + const HMC5883LScreen({super.key}); + + @override + State createState() => _HMC5883LScreenState(); +} + +class _HMC5883LScreenState extends State { + AppLocalizations appLocalizations = getIt.get(); + String sensorImage = 'assets/images/hmc5883l.jpg'; + I2C? _i2c; + ScienceLab? _scienceLab; + + @override + void initState() { + super.initState(); + _initializeScienceLab(); + } + + void _initializeScienceLab() async { + try { + _scienceLab = getIt.get(); + if (_scienceLab != null && _scienceLab!.isConnected()) { + _i2c = I2C(_scienceLab!.mPacketHandler); + } + } catch (e) { + logger.e('Error initializing ScienceLab: $e'); + } + } + + void _showSensorErrorSnackbar(String message) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + message, + style: TextStyle(color: snackBarContentColor), + ), + backgroundColor: snackBarBackgroundColor, + duration: const Duration(milliseconds: 500), + behavior: SnackBarBehavior.floating, + ), + ); + } + } + + void _showSuccessSnackbar(String message) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + message, + style: TextStyle(color: snackBarContentColor), + ), + backgroundColor: snackBarBackgroundColor, + duration: const Duration(milliseconds: 500), + behavior: SnackBarBehavior.floating, + ), + ); + } + } + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (context) => HMC5883LProvider() + ..initializeSensors( + onError: _showSensorErrorSnackbar, + i2c: _i2c, + scienceLab: _scienceLab, + ), + child: Consumer( + builder: (context, provider, child) { + return CommonScaffold( + title: 'HMC5883L Magnetometer', + body: Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildRawDataSection(provider), + const SizedBox(height: 16), + _buildCalibrationSection(provider), + const SizedBox(height: 24), + SensorChartWidget( + title: '${appLocalizations.plot} - Magnetic Field X', + yAxisLabel: 'Magnetic Field X (µT)', + data: provider.magneticXData, + lineColor: const Color(0xFFE91E63), + unit: 'µT', + maxDataPoints: provider.numberOfReadings, + showDots: true, + ), + const SizedBox(height: 20), + SensorChartWidget( + title: '${appLocalizations.plot} - Magnetic Field Y', + yAxisLabel: 'Magnetic Field Y (µT)', + data: provider.magneticYData, + lineColor: const Color(0xFF2196F3), + unit: 'µT', + maxDataPoints: provider.numberOfReadings, + showDots: true, + ), + const SizedBox(height: 20), + SensorChartWidget( + title: '${appLocalizations.plot} - Magnetic Field Z', + yAxisLabel: 'Magnetic Field Z (µT)', + data: provider.magneticZData, + lineColor: const Color(0xFF4CAF50), + unit: 'µT', + maxDataPoints: provider.numberOfReadings, + showDots: true, + ), + const SizedBox(height: 20), + SensorChartWidget( + title: '${appLocalizations.plot} - Heading', + yAxisLabel: 'Heading (degrees)', + data: provider.headingData, + lineColor: const Color(0xFFFF9800), + unit: '°', + maxDataPoints: provider.numberOfReadings, + showDots: true, + ), + const SizedBox(height: 20), + SensorChartWidget( + title: '${appLocalizations.plot} - Magnitude', + yAxisLabel: 'Magnitude (µT)', + data: provider.magnitudeData, + lineColor: const Color(0xFF9C27B0), + unit: 'µT', + maxDataPoints: provider.numberOfReadings, + showDots: true, + ), + const SizedBox(height: 100), + ], + ), + ), + ), + SensorControlsWidget( + isPlaying: provider.isRunning, + isLooping: provider.isLooping, + timegapMs: provider.timegapMs, + numberOfReadings: provider.numberOfReadings, + onPlayPause: () { + provider.toggleDataCollection(); + }, + onLoop: provider.toggleLooping, + onTimegapChanged: provider.setTimegap, + onNumberOfReadingsChanged: provider.setNumberOfReadings, + onClearData: () { + provider.clearData(); + _showSuccessSnackbar(appLocalizations.dataCleared); + }, + ), + ], + ), + ); + }, + ), + ); + } + + Widget _buildRawDataSection(HMC5883LProvider provider) { + return Container( + width: double.infinity, + decoration: BoxDecoration( + color: cardBackgroundColor, + borderRadius: BorderRadius.zero, + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(50), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20), + decoration: BoxDecoration( + color: primaryRed, + borderRadius: const BorderRadius.only( + topLeft: Radius.zero, + topRight: Radius.zero, + ), + ), + child: Row( + children: [ + Text( + appLocalizations.rawData, + style: TextStyle( + color: appBarContentColor, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + const Spacer(), + if (provider.isRunning) + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: appBarContentColor, + shape: BoxShape.circle, + ), + ), + ], + ), + ), + Container( + width: double.infinity, + padding: const EdgeInsets.all(24), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 2, + child: Column( + children: [ + _buildDataCard( + 'Magnetic X', + '${provider.magneticX.toStringAsFixed(2)} µT', + ), + const SizedBox(height: 16), + _buildDataCard( + 'Magnetic Y', + '${provider.magneticY.toStringAsFixed(2)} µT', + ), + const SizedBox(height: 16), + _buildDataCard( + 'Magnetic Z', + '${provider.magneticZ.toStringAsFixed(2)} µT', + ), + const SizedBox(height: 16), + _buildDataCard( + 'Heading', + '${provider.heading.toStringAsFixed(1)}°', + ), + const SizedBox(height: 16), + _buildDataCard( + 'Magnitude', + '${provider.magnitude.toStringAsFixed(2)} µT', + ), + ], + ), + ), + const SizedBox(width: 24), + SizedBox( + width: 80, + height: 80, + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.asset( + sensorImage, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Icon( + Icons.explore, + size: 40, + color: sensorControlsTextBox, + ); + }, + ), + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildCalibrationSection(HMC5883LProvider provider) { + return Container( + width: double.infinity, + decoration: BoxDecoration( + color: cardBackgroundColor, + borderRadius: BorderRadius.zero, + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(50), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20), + decoration: BoxDecoration( + color: primaryRed, + borderRadius: const BorderRadius.only( + topLeft: Radius.zero, + topRight: Radius.zero, + ), + ), + child: Row( + children: [ + Text( + 'Calibration', + style: TextStyle( + color: appBarContentColor, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + const Spacer(), + if (provider.isCalibrating) + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: Colors.orange, + shape: BoxShape.circle, + ), + ), + ], + ), + ), + Container( + width: double.infinity, + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + provider.isCalibrating + ? 'Move the sensor in a figure-8 pattern to calibrate...' + : 'Tap the button below to start calibration', + style: TextStyle( + fontSize: 14, + color: blackTextColor.withAlpha(180), + ), + ), + if (provider.isCalibrating) ...[ + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.orange.withAlpha(25), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.orange.withAlpha(100)), + ), + child: Text( + provider.getCalibrationStatus(), + style: TextStyle( + fontSize: 12, + fontFamily: 'monospace', + color: blackTextColor, + ), + ), + ), + ], + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: () { + if (provider.isCalibrating) { + provider.stopCalibration(); + _showSuccessSnackbar('Calibration completed'); + } else { + provider.startCalibration(); + _showSuccessSnackbar('Calibration started'); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: provider.isCalibrating + ? Colors.orange + : primaryRed, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text( + provider.isCalibrating + ? 'Stop Calibration' + : 'Start Calibration', + style: TextStyle( + color: appBarContentColor, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: ElevatedButton( + onPressed: () { + _showGainDialog(provider); + }, + style: ElevatedButton.styleFrom( + backgroundColor: primaryRed, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text( + 'Set Gain', + style: TextStyle( + color: appBarContentColor, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + } + + void _showGainDialog(HMC5883LProvider provider) { + final gains = { + 'Gain 1370 (±0.88 Ga)': HMC5883L.gain1370, + 'Gain 1090 (±1.3 Ga) - Default': HMC5883L.gain1090, + 'Gain 820 (±1.9 Ga)': HMC5883L.gain820, + 'Gain 660 (±2.5 Ga)': HMC5883L.gain660, + 'Gain 440 (±4.0 Ga)': HMC5883L.gain440, + 'Gain 390 (±4.7 Ga)': HMC5883L.gain390, + 'Gain 330 (±5.6 Ga)': HMC5883L.gain330, + 'Gain 230 (±8.1 Ga)': HMC5883L.gain230, + }; + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Select Gain'), + content: ConstrainedBox( + constraints: const BoxConstraints( + maxHeight: 300, + ), + child: ListView( + shrinkWrap: true, + children: gains.entries.map((entry) { + return ListTile( + title: Text(entry.key), + onTap: () { + provider.setGain(entry.value); + Navigator.pop(context); + _showSuccessSnackbar('Gain set to ${entry.key}'); + }, + ); + }).toList(), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), + ), + ], + ), + ); + } + + Widget _buildDataCard(String label, String value) { + return Row( + children: [ + SizedBox( + width: 100, + child: Text( + label, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: blackTextColor, + ), + ), + ), + Expanded( + flex: 3, + child: Container( + height: 36, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + border: Border.all(color: sensorControlsTextBox), + borderRadius: BorderRadius.circular(4), + color: cardBackgroundColor, + ), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + value, + style: TextStyle( + fontSize: 14, + color: blackTextColor, + ), + ), + ), + ), + ), + ], + ); + } + +} diff --git a/lib/view/sensors_screen.dart b/lib/view/sensors_screen.dart index d43df3832..b05d68a9a 100644 --- a/lib/view/sensors_screen.dart +++ b/lib/view/sensors_screen.dart @@ -3,6 +3,7 @@ import 'package:provider/provider.dart'; import 'package:pslab/view/bmp180_screen.dart'; import 'package:pslab/view/ads1115_screen.dart'; import 'package:pslab/view/vl53l0x_screen.dart'; +import 'package:pslab/view/hmc5883l_screen.dart'; import 'package:pslab/view/widgets/common_scaffold_widget.dart'; import '../../providers/board_state_provider.dart'; import '../l10n/app_localizations.dart'; @@ -229,6 +230,10 @@ class _SensorsScreenState extends State { break; case 'APDS9960': targetScreen = const APDS9960Screen(); + break; + case 'HMC5883L': + targetScreen = const HMC5883LScreen(); + break; case 'VL53L0X': targetScreen = const VL53L0XScreen(); break;