Skip to content

Commit f1cf3f8

Browse files
feat: added csv import/export for robotic arm (#2807)
Co-authored-by: Marc Nause <[email protected]>
1 parent 79f413e commit f1cf3f8

File tree

3 files changed

+220
-34
lines changed

3 files changed

+220
-34
lines changed

lib/providers/robotic_arm_state_provider.dart

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,20 @@ import 'dart:async';
22
import 'package:fl_chart/fl_chart.dart';
33
import 'package:flutter/material.dart';
44
import 'package:flutter/services.dart';
5+
import 'package:intl/intl.dart';
56
import 'package:pslab/communication/science_lab.dart';
67
import 'package:pslab/l10n/app_localizations.dart';
78
import 'package:pslab/others/logger_service.dart';
89
import 'package:pslab/providers/locator.dart';
9-
import '../others/science_lab_common.dart';
10+
import 'package:pslab/others/science_lab_common.dart';
1011
import 'package:vibration/vibration.dart';
1112

13+
import '../others/csv_service.dart';
14+
1215
class RoboticArmStateProvider extends ChangeNotifier {
1316
AppLocalizations appLocalizations = getIt.get<AppLocalizations>();
17+
final CsvService _csvService = CsvService();
18+
1419
final List<double> servoValues = [0, 0, 0, 0];
1520
List<List<double?>> timelineDegrees = [];
1621
List<List<double?>> pwmData = [];
@@ -30,13 +35,9 @@ class RoboticArmStateProvider extends ChangeNotifier {
3035
late String _selectedDuration;
3136

3237
int get maxAngle => int.tryParse(_selectedMaxAngle) ?? 180;
33-
3438
String get selectedFrequency => _selectedFrequency;
35-
3639
String get selectedMaxAngle => _selectedMaxAngle;
37-
3840
String get selectedDuration => _selectedDuration;
39-
4041
bool get showControlBox => _showControlBox;
4142

4243
int get totalTimelineItems =>
@@ -213,6 +214,57 @@ class RoboticArmStateProvider extends ChangeNotifier {
213214
notifyListeners();
214215
}
215216

217+
Future<void> exportTimelineToCsv({
218+
required String instrumentName,
219+
required String fileName,
220+
}) async {
221+
List<List<dynamic>> csvData = [];
222+
223+
csvData
224+
.add(['Timestamp', 'Servo1', 'Servo2', 'Servo3', 'Servo4', 'DateTime']);
225+
226+
final dateFormat = DateFormat('yyyy-MM-dd HH:mm:ss.SSS');
227+
228+
for (int i = 0; i < timelineDegrees.length; i++) {
229+
final row = timelineDegrees[i];
230+
final timestamp = i;
231+
final dateTime = dateFormat.format(DateTime.now());
232+
233+
csvData.add([
234+
timestamp,
235+
row[0]?.toString() ?? 'null',
236+
row[1]?.toString() ?? 'null',
237+
row[2]?.toString() ?? 'null',
238+
row[3]?.toString() ?? 'null',
239+
dateTime
240+
]);
241+
}
242+
243+
await _csvService.saveCsvFile(instrumentName, fileName, csvData);
244+
}
245+
246+
Future<void> importTimelineFromCsv(List<List<dynamic>> csv) async {
247+
if (csv.length <= 2) return;
248+
249+
final dataRows = csv.skip(1).toList();
250+
251+
for (int i = 0; i < totalTimelineItems; i++) {
252+
if (i < dataRows.length && dataRows[i].length >= 5) {
253+
final row = dataRows[i];
254+
timelineDegrees[i] = [
255+
double.tryParse(row[1].toString()),
256+
double.tryParse(row[2].toString()),
257+
double.tryParse(row[3].toString()),
258+
double.tryParse(row[4].toString()),
259+
];
260+
} else {
261+
timelineDegrees[i] = [null, null, null, null];
262+
}
263+
}
264+
265+
notifyListeners();
266+
}
267+
216268
void stopScrolling({bool resetPosition = true}) {
217269
_timelineTimer?.cancel();
218270
isPlaying = false;

lib/view/logged_data_screen.dart

Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -142,40 +142,48 @@ class _LoggedDataScreenState extends State<LoggedDataScreen> {
142142
Future<void> _openFile(File file) async {
143143
final data = await _csvService.readCsvFromFile(file);
144144
if (mounted) {
145-
final config = _getChartConfig();
146-
Navigator.push(
147-
context,
148-
MaterialPageRoute(
149-
builder: (context) => LoggedDataChartScreen(
150-
data: data,
151-
fileName: file.path.split('/').last,
152-
xAxisLabel: config['xAxisLabel'],
153-
yAxisLabel: config['yAxisLabel'],
154-
xDataColumnIndex: config['xDataColumnIndex'],
155-
yDataColumnIndex: config['yDataColumnIndex'],
145+
if (widget.instrumentName.toLowerCase() == 'robotic arm') {
146+
Navigator.pop(context, data);
147+
} else {
148+
final config = _getChartConfig();
149+
Navigator.push(
150+
context,
151+
MaterialPageRoute(
152+
builder: (context) => LoggedDataChartScreen(
153+
data: data,
154+
fileName: file.path.split('/').last,
155+
xAxisLabel: config['xAxisLabel'],
156+
yAxisLabel: config['yAxisLabel'],
157+
xDataColumnIndex: config['xDataColumnIndex'],
158+
yDataColumnIndex: config['yDataColumnIndex'],
159+
),
156160
),
157-
),
158-
);
161+
);
162+
}
159163
}
160164
}
161165

162166
Future<void> _pickAndImportFile() async {
163167
final data = await _csvService.pickAndReadCsvFile();
164168
if (data != null && mounted) {
165-
final config = _getChartConfig();
166-
Navigator.push(
167-
context,
168-
MaterialPageRoute(
169-
builder: (context) => LoggedDataChartScreen(
170-
data: data,
171-
fileName: 'Imported Log',
172-
xAxisLabel: config['xAxisLabel'],
173-
yAxisLabel: config['yAxisLabel'],
174-
xDataColumnIndex: config['xDataColumnIndex'],
175-
yDataColumnIndex: config['yDataColumnIndex'],
169+
if (widget.instrumentName.toLowerCase() == 'robotic arm') {
170+
Navigator.pop(context, data);
171+
} else {
172+
final config = _getChartConfig();
173+
Navigator.push(
174+
context,
175+
MaterialPageRoute(
176+
builder: (context) => LoggedDataChartScreen(
177+
data: data,
178+
fileName: 'Imported Log',
179+
xAxisLabel: config['xAxisLabel'],
180+
yAxisLabel: config['yAxisLabel'],
181+
xDataColumnIndex: config['xDataColumnIndex'],
182+
yDataColumnIndex: config['yDataColumnIndex'],
183+
),
176184
),
177-
),
178-
);
185+
);
186+
}
179187
}
180188
}
181189

lib/view/robotic_arm_screen.dart

Lines changed: 129 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:pslab/view/widgets/robotic_arm_dialog.dart';
88
import 'package:pslab/view/widgets/robotic_arm_summary.dart';
99
import 'package:pslab/view/widgets/robotic_arm_timeline.dart';
1010
import '../providers/robotic_arm_state_provider.dart';
11+
import 'logged_data_screen.dart';
1112
import 'package:pslab/view/widgets/guide_widget.dart';
1213
import 'widgets/servo_card.dart';
1314

@@ -183,7 +184,117 @@ class _RoboticArmScreenState extends State<RoboticArmScreen> {
183184
IconButton(
184185
icon: const Icon(Icons.save, color: Colors.white),
185186
tooltip: appLocalizations.saveData,
186-
onPressed: () {}, // TODO
187+
onPressed: () async {
188+
final TextEditingController fileNameController =
189+
TextEditingController();
190+
191+
final String? enteredFileName = await showDialog<String>(
192+
context: context,
193+
barrierDismissible: false,
194+
builder: (context) {
195+
return Dialog(
196+
insetPadding: EdgeInsets.zero,
197+
backgroundColor: Colors.transparent,
198+
child: Align(
199+
alignment: Alignment.topCenter,
200+
child: Container(
201+
margin: EdgeInsets.zero,
202+
padding: const EdgeInsets.all(8),
203+
width: 220,
204+
decoration: BoxDecoration(
205+
color: Colors.white,
206+
borderRadius: BorderRadius.circular(6),
207+
),
208+
child: Column(
209+
mainAxisSize: MainAxisSize.min,
210+
children: [
211+
Text(
212+
appLocalizations.enterFileName,
213+
style: TextStyle(fontSize: 9),
214+
),
215+
const SizedBox(height: 2),
216+
TextField(
217+
controller: fileNameController,
218+
style: const TextStyle(fontSize: 12),
219+
decoration: const InputDecoration(
220+
isDense: true,
221+
contentPadding: EdgeInsets.symmetric(
222+
horizontal: 6, vertical: 6),
223+
border: OutlineInputBorder(),
224+
),
225+
),
226+
const SizedBox(height: 8),
227+
Row(
228+
mainAxisAlignment: MainAxisAlignment.end,
229+
children: [
230+
SizedBox(
231+
height: 26,
232+
child: TextButton(
233+
style: TextButton.styleFrom(
234+
padding: const EdgeInsets.symmetric(
235+
horizontal: 6),
236+
minimumSize: Size.zero,
237+
tapTargetSize:
238+
MaterialTapTargetSize.shrinkWrap,
239+
),
240+
onPressed: () =>
241+
Navigator.of(context).pop(null),
242+
child: Text(appLocalizations.cancel,
243+
style: TextStyle(
244+
fontSize: 10,
245+
color: Colors.black)),
246+
),
247+
),
248+
const SizedBox(width: 6),
249+
SizedBox(
250+
height: 26,
251+
child: TextButton(
252+
style: TextButton.styleFrom(
253+
padding: const EdgeInsets.symmetric(
254+
horizontal: 6),
255+
minimumSize: Size.zero,
256+
tapTargetSize:
257+
MaterialTapTargetSize.shrinkWrap,
258+
),
259+
onPressed: () {
260+
Navigator.of(context).pop(
261+
fileNameController.text.trim());
262+
},
263+
child: Text(appLocalizations.save,
264+
style: TextStyle(
265+
fontSize: 10,
266+
color: Colors.black)),
267+
),
268+
),
269+
],
270+
)
271+
],
272+
),
273+
),
274+
),
275+
);
276+
},
277+
);
278+
279+
try {
280+
await provider.exportTimelineToCsv(
281+
instrumentName: appLocalizations.roboticArmTitle,
282+
fileName: (enteredFileName!),
283+
);
284+
285+
if (!context.mounted) return;
286+
287+
ScaffoldMessenger.of(context).showSnackBar(
288+
SnackBar(content: Text(appLocalizations.fileSaved)),
289+
);
290+
} catch (e) {
291+
if (!context.mounted) return;
292+
293+
ScaffoldMessenger.of(context).showSnackBar(
294+
SnackBar(content: Text(appLocalizations.csvSavingError)),
295+
);
296+
}
297+
},
187298
),
188299
IconButton(
189300
icon: const Icon(Icons.info, color: Colors.white),
@@ -196,9 +307,24 @@ class _RoboticArmScreenState extends State<RoboticArmScreen> {
196307
),
197308
PopupMenuButton<String>(
198309
icon: const Icon(Icons.more_vert, color: Colors.white),
199-
onSelected: (value) {
310+
onSelected: (value) async {
200311
if (value == appLocalizations.showLoggedData) {
201-
// TODO
312+
final List<List<dynamic>>? data = await Navigator.push(
313+
context,
314+
MaterialPageRoute(
315+
builder: (_) => LoggedDataScreen(
316+
instrumentName: appLocalizations.roboticArmTitle,
317+
appBarName: appLocalizations.showLoggedData,
318+
instrumentIcon: 'assets/icons/robotic_arm.png',
319+
),
320+
),
321+
);
322+
323+
if (data != null) {
324+
setState(() {
325+
provider.importTimelineFromCsv(data);
326+
});
327+
}
202328
}
203329
},
204330
itemBuilder: (BuildContext context) => [

0 commit comments

Comments
 (0)