Skip to content

feat: implemented LUX meter #2733

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions lib/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,9 @@ String maxValue = 'Max: ';
String gyroscopeTitle = "Gyroscope";
String gyroscopeAxisLabel = 'rad/s';
String noData = 'No data available';
String luxMeterTitle = 'Lux Meter';
String builtIn = 'Built-In';
String lx = 'Lx';
String maxScaleError = 'Max Scale';
String lightSensorError = 'Light sensor error:';
String lightSensorInitialError = 'Failed to initialize light sensor:';
3 changes: 2 additions & 1 deletion lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:pslab/view/connect_device_screen.dart';
import 'package:pslab/view/faq_screen.dart';
import 'package:pslab/view/gyroscope_screen.dart';
import 'package:pslab/view/instruments_screen.dart';
import 'package:pslab/view/luxmeter_screen.dart';
import 'package:pslab/view/oscilloscope_screen.dart';
import 'package:pslab/view/settings_screen.dart';
import 'package:pslab/view/about_us_screen.dart';
Expand All @@ -32,7 +33,6 @@ void main() {

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
_preCacheImages(context);
Expand All @@ -52,6 +52,7 @@ class MyApp extends StatelessWidget {
'/softwareLicenses': (context) => const SoftwareLicensesScreen(),
'/accelerometer': (context) => const AccelerometerScreen(),
'/gyroscope': (context) => const GyroscopeScreen(),
'/luxmeter': (context) => const LuxMeterScreen(),
},
);
}
Expand Down
101 changes: 101 additions & 0 deletions lib/providers/luxmeter_state_provider.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
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:light/light.dart';
import 'package:flutter/foundation.dart';
import 'package:pslab/constants.dart';

class LuxMeterStateProvider extends ChangeNotifier {
double _currentLux = 0.0;
StreamSubscription? _lightSubscription;
Timer? _timeTimer;
final List<double> _luxData = [];
final List<double> _timeData = [];
final List<FlSpot> luxChartData = [];
Light? _light;
double _startTime = 0;
double _currentTime = 0;
final int _maxLength = 50;
double _luxMin = 0;
double _luxMax = 0;
double _luxSum = 0;
int _dataCount = 0;
void initializeSensors() {
try {
_light = Light();
_startTime = DateTime.now().millisecondsSinceEpoch / 1000.0;
_timeTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
_currentTime =
(DateTime.now().millisecondsSinceEpoch / 1000.0) - _startTime;
_updateData();
notifyListeners();
});
_lightSubscription = _light!.lightSensorStream.listen(
(int luxValue) {
_currentLux = luxValue.toDouble();
notifyListeners();
},
onError: (error) {
logger.e(
"$lightSensorError $error",
);
},
cancelOnError: true,
);
} catch (e) {
logger.e("$lightSensorInitialError $e");
}
}

void disposeSensors() {
_lightSubscription?.cancel();
_timeTimer?.cancel();
}

@override
void dispose() {
disposeSensors();
super.dispose();
}

void _updateData() {
final lux = _currentLux;
final time = _currentTime;
_luxData.add(lux);
_timeData.add(time);
_luxSum += lux;
_dataCount++;
if (_luxData.length > _maxLength) {
final removedValue = _luxData.removeAt(0);
_timeData.removeAt(0);
_luxSum -= removedValue;
_dataCount--;
}
if (_luxData.isNotEmpty) {
_luxMin = _luxData.reduce(min);
_luxMax = _luxData.reduce(max);
}
luxChartData.clear();
for (int i = 0; i < _luxData.length; i++) {
luxChartData.add(FlSpot(_timeData[i], _luxData[i]));
}
notifyListeners();
}

double getCurrentLux() => _currentLux;
double getMinLux() => _luxMin;
double getMaxLux() => _luxMax;
double getAverageLux() => _dataCount > 0 ? _luxSum / _dataCount : 0.0;
List<FlSpot> getLuxChartData() => luxChartData;
int getDataLength() => luxChartData.length;
double getCurrentTime() => _currentTime;
double getMaxTime() => _timeData.isNotEmpty ? _timeData.last : 0;
double getMinTime() => _timeData.isNotEmpty ? _timeData.first : 0;
double getTimeInterval() {
if (_currentTime <= 10) return 2;
if (_currentTime <= 30) return 5;
return 10;
}
}
13 changes: 12 additions & 1 deletion lib/view/instruments_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import 'package:pslab/view/widgets/main_scaffold_widget.dart';

class InstrumentsScreen extends StatefulWidget {
const InstrumentsScreen({super.key});

@override
State<StatefulWidget> createState() => _InstrumentsScreenState();
}
Expand Down Expand Up @@ -38,6 +37,18 @@ class _InstrumentsScreenState extends State<InstrumentsScreen> {
);
}
break;
case 6:
if (Navigator.canPop(context) &&
ModalRoute.of(context)?.settings.name == '/luxmeter') {
Navigator.popUntil(context, ModalRoute.withName('/luxmeter'));
} else {
Navigator.pushNamedAndRemoveUntil(
context,
'/luxmeter',
(route) => route.isFirst,
);
}
break;
case 10:
if (Navigator.canPop(context) &&
ModalRoute.of(context)?.settings.name == '/gyroscope') {
Expand Down
205 changes: 205 additions & 0 deletions lib/view/luxmeter_screen.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pslab/constants.dart';
import 'package:pslab/providers/luxmeter_state_provider.dart';
import 'package:pslab/view/widgets/common_scaffold_widget.dart';
import 'package:pslab/view/widgets/luxmeter_card.dart';
import 'package:fl_chart/fl_chart.dart';

class LuxMeterScreen extends StatefulWidget {
const LuxMeterScreen({super.key});
@override
State<StatefulWidget> createState() => _LuxMeterScreenState();
}

class _LuxMeterScreenState extends State<LuxMeterScreen> {
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<LuxMeterStateProvider>(
create: (_) => LuxMeterStateProvider()..initializeSensors(),
),
],
child: CommonScaffold(
title: luxMeterTitle,
body: SafeArea(
child: Column(
children: [
const Expanded(
flex: 45,
child: LuxMeterCard(),
),
Expanded(
flex: 55,
child: _buildChartSection(),
),
],
),
),
),
);
}

Widget _buildChartSection() {
return Consumer<LuxMeterStateProvider>(
builder: (context, provider, child) {
final screenWidth = MediaQuery.of(context).size.width;
final cardMargin = screenWidth < 400 ? 8.0 : 16.0;
List<FlSpot> spots = provider.getLuxChartData();
double maxLux = provider.getMaxLux();
double maxTime = provider.getMaxTime();
double minTime = provider.getMinTime();
double timeInterval = provider.getTimeInterval();

return Container(
margin: EdgeInsets.fromLTRB(cardMargin, 0, cardMargin, cardMargin),
padding: EdgeInsets.all(cardMargin),
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(8),
),
child: _buildChart(
screenWidth, maxLux, 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: Colors.white,
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 maxLux, double maxTime,
double minTime, double timeInterval, List<FlSpot> 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 ? 20.0 : 25.0;
return LineChart(
LineChartData(
backgroundColor: Colors.black,
titlesData: FlTitlesData(
show: true,
topTitles: AxisTitles(
axisNameWidget: Padding(
padding: EdgeInsets.only(left: screenWidth < 400 ? 15 : 25),
child: Text(
timeAxisLabel,
style: TextStyle(
fontSize: axisNameFontSize,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
axisNameSize: screenWidth < 400 ? 18 : 20,
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: reservedSizeBottom,
getTitlesWidget: sideTitleWidgets,
interval: timeInterval,
),
),
leftTitles: AxisTitles(
axisNameWidget: Text(
lx,
style: TextStyle(
fontSize: axisNameFontSize,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
sideTitles: SideTitles(
reservedSize: reservedSizeLeft,
showTitles: true,
getTitlesWidget: (value, meta) {
return SideTitleWidget(
meta: meta,
child: Text(
value.toInt().toString(),
style: TextStyle(
color: Colors.white,
fontSize: chartFontSize,
),
),
);
},
interval: maxLux > 0 ? (maxLux / 5).ceilToDouble() : 10,
),
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
),
gridData: FlGridData(
show: true,
drawHorizontalLine: true,
drawVerticalLine: true,
horizontalInterval: maxLux > 0 ? (maxLux / 5).ceilToDouble() : 10,
verticalInterval: timeInterval,
),
borderData: FlBorderData(
show: true,
border: const Border(
bottom: BorderSide(color: Colors.white38),
left: BorderSide(color: Colors.white38),
top: BorderSide(color: Colors.white38),
right: BorderSide(color: Colors.white38),
),
),
minY: 0,
maxY: maxLux > 0 ? (maxLux * 1.1) : 100,
maxX: maxTime > 0 ? maxTime : 10,
minX: minTime,
clipData: const FlClipData.all(),
lineBarsData: [
LineChartBarData(
spots: spots,
isCurved: true,
color: Colors.cyan,
barWidth: screenWidth < 400 ? 1.5 : 2.0,
isStrokeCapRound: true,
dotData: const FlDotData(show: false),
belowBarData: BarAreaData(show: false),
),
],
),
);
}
}
Loading