Skip to content

feat: added csv functionalities #2785

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

Open
wants to merge 7 commits into
base: flutter
Choose a base branch
from
Open
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
14 changes: 11 additions & 3 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-feature android:name="android.hardware.usb.host" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<application
android:label="PSLab"
android:name="${applicationName}"
android:icon="@drawable/launcher_icon"
android:roundIcon="@drawable/launcher_icon_round">
android:roundIcon="@drawable/launcher_icon_round"
android:requestLegacyExternalStorage="true"
tools:ignore="ScopedStorage">
<activity
android:name=".MainActivity"
android:exported="true"
Expand Down Expand Up @@ -47,5 +54,6 @@
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
<package android:name="com.android.externalstorage.documents" />
</queries>
</manifest>
19 changes: 18 additions & 1 deletion lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -305,5 +305,22 @@
"baroMeterBulletPoint1": "The Barometer can be used to measure Atmospheric pressure. This instrument is compatible with either the built in pressure sensor on any android device or the BMP-180 pressure sensor",
"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."
"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",
"deleteHint": "Are you sure you want to delete this file?",
"deleteFile" : "Delete File",
"deleteAllData" : "Delete All Data",
"deleteCautionMessage" : "Are you sure you want to delete all logged data for this instrument?",
"deleteAll" : "Delete All",
"noLoggedData" : "No logged data found.",
"importLog" : "Import Log",
"failedToSave" : "Failed to save file. No data was recorded.",
"fileSaved" : "File saved",
"save" : "Save",
"enterFileName" : "Enter filename (leave empty for auto-generated name)",
"fileName" : "Filename",
"saveRecording" : "Save Recording",
"recordingStarted" : "Recording started",
"noValidData" : "No valid data to display."
}
102 changes: 102 additions & 0 deletions lib/l10n/app_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1929,6 +1929,108 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'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 @sharingMessage.
///
/// In en, this message translates to:
/// **'Sharing PSLab Data'**
String get sharingMessage;

/// No description provided for @delete.
///
/// In en, this message translates to:
/// **'Delete'**
String get delete;

/// No description provided for @deleteHint.
///
/// In en, this message translates to:
/// **'Are you sure you want to delete this file?'**
String get deleteHint;

/// No description provided for @deleteFile.
///
/// In en, this message translates to:
/// **'Delete File'**
String get deleteFile;

/// No description provided for @deleteAllData.
///
/// In en, this message translates to:
/// **'Delete All Data'**
String get deleteAllData;

/// No description provided for @deleteCautionMessage.
///
/// In en, this message translates to:
/// **'Are you sure you want to delete all logged data for this instrument?'**
String get deleteCautionMessage;

/// No description provided for @deleteAll.
///
/// In en, this message translates to:
/// **'Delete All'**
String get deleteAll;

/// No description provided for @noLoggedData.
///
/// In en, this message translates to:
/// **'No logged data found.'**
String get noLoggedData;

/// No description provided for @importLog.
///
/// In en, this message translates to:
/// **'Import Log'**
String get importLog;

/// No description provided for @failedToSave.
///
/// In en, this message translates to:
/// **'Failed to save file. No data was recorded.'**
String get failedToSave;

/// No description provided for @fileSaved.
///
/// In en, this message translates to:
/// **'File saved'**
String get fileSaved;

/// No description provided for @save.
///
/// In en, this message translates to:
/// **'Save'**
String get save;

/// No description provided for @enterFileName.
///
/// In en, this message translates to:
/// **'Enter filename (leave empty for auto-generated name)'**
String get enterFileName;

/// No description provided for @fileName.
///
/// In en, this message translates to:
/// **'Filename'**
String get fileName;

/// No description provided for @saveRecording.
///
/// In en, this message translates to:
/// **'Save Recording'**
String get saveRecording;

/// No description provided for @recordingStarted.
///
/// In en, this message translates to:
/// **'Recording started'**
String get recordingStarted;

/// No description provided for @noValidData.
///
/// In en, this message translates to:
/// **'No valid data to display.'**
String get noValidData;
}

class _AppLocalizationsDelegate
Expand Down
53 changes: 53 additions & 0 deletions lib/l10n/app_localizations_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -985,4 +985,57 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get 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.';

@override
String get sharingMessage => 'Sharing PSLab Data';

@override
String get delete => 'Delete';

@override
String get deleteHint => 'Are you sure you want to delete this file?';

@override
String get deleteFile => 'Delete File';

@override
String get deleteAllData => 'Delete All Data';

@override
String get deleteCautionMessage =>
'Are you sure you want to delete all logged data for this instrument?';

@override
String get deleteAll => 'Delete All';

@override
String get noLoggedData => 'No logged data found.';

@override
String get importLog => 'Import Log';

@override
String get failedToSave => 'Failed to save file. No data was recorded.';

@override
String get fileSaved => 'File saved';

@override
String get save => 'Save';

@override
String get enterFileName =>
'Enter filename (leave empty for auto-generated name)';

@override
String get fileName => 'Filename';

@override
String get saveRecording => 'Save Recording';

@override
String get recordingStarted => 'Recording started';

@override
String get noValidData => 'No valid data to display.';
}
173 changes: 173 additions & 0 deletions lib/others/csv_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import 'dart:convert';
import 'dart:io';
import 'package:csv/csv.dart';
import 'package:file_picker/file_picker.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:pslab/others/logger_service.dart';
import 'package:pslab/view/about_us_screen.dart';
import 'package:share_plus/share_plus.dart';
import 'package:intl/intl.dart';

class CsvService {
static const String csvDirectory = 'PSLab';

Future<Directory> getInstrumentDirectory(String instrumentName) async {
if (Platform.isAndroid) {
await requestStoragePermission();
final directory =
Directory('/storage/emulated/0/Android/media/PSLab/$instrumentName');
if (!await directory.exists()) {
await directory.create(recursive: true);
}
return directory;
} else if (Platform.isIOS ||
Platform.isWindows ||
Platform.isMacOS ||
Platform.isLinux) {
final dir = await getApplicationDocumentsDirectory();
final directory = Directory('${dir.path}/PSLab/$instrumentName');
if (!await directory.exists()) {
await directory.create(recursive: true);
}
return directory;
} else {
throw UnsupportedError('Unsupported platform');
}
}

Future<void> requestStoragePermission() async {
if (Platform.isAndroid) {
final status = await Permission.manageExternalStorage.request();
if (!status.isGranted) {
await openAppSettings();
}
}
}

Future<File?> saveCsvFile(
String instrumentName, String fileName, List<List<dynamic>> data) async {
try {
if (data.length <= 1) {
logger.w('No data recorded to save for $fileName');
return null;
}
final directory = await getInstrumentDirectory(instrumentName);

String finalFileName;
if (fileName.isEmpty) {
finalFileName =
'${DateFormat('yyyy-MM-dd_HH-mm-ss').format(DateTime.now())}.csv';
} else {
finalFileName = fileName.endsWith('.csv') ? fileName : '$fileName.csv';
}

final file = File('${directory.path}/$finalFileName');

String csvData = const ListToCsvConverter().convert(data);
await file.writeAsString(csvData);
logger.i('CSV file saved at: ${file.path}');
return file;
} catch (e) {
logger.e('Error saving CSV file: $e');
return null;
}
}

Future<List<FileSystemEntity>> getSavedFiles(String instrumentName) async {
try {
final directory = await getInstrumentDirectory(instrumentName);
final files = directory
.listSync()
.where((item) => item.path.endsWith('.csv'))
.toList();
files.sort(
(a, b) => b.statSync().modified.compareTo(a.statSync().modified));
return files;
} catch (e) {
logger.e('Error getting saved files: $e');
return [];
}
}

Future<void> deleteFile(String filePath) async {
try {
final file = File(filePath);
if (await file.exists()) {
await file.delete();
logger.i('File deleted: $filePath');
}
} catch (e) {
logger.e('Error deleting file: $e');
}
}

Future<void> deleteAllFiles(String instrumentName) async {
try {
final directory = await getInstrumentDirectory(instrumentName);
if (await directory.exists()) {
await directory.delete(recursive: true);
logger.i('All files for $instrumentName deleted.');
}
} catch (e) {
logger.e('Error deleting all files for $instrumentName: $e');
}
}

Future<void> shareFile(String filePath) async {
try {
final xFile = XFile(filePath);
await SharePlus.instance.share(
ShareParams(files: [xFile], text: appLocalizations.sharingMessage));
} catch (e) {
logger.e('Error sharing file: $e');
}
}

Future<List<List<dynamic>>?> pickAndReadCsvFile() async {
try {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['csv'],
);

if (result != null && result.files.single.path != null) {
final file = File(result.files.single.path!);
return await readCsvFromFile(file);
}
} catch (e) {
logger.e('Error picking or reading CSV file: $e');
}
return null;
}

Future<List<List<dynamic>>> readCsvFromFile(File file) async {
try {
final input = file.openRead();
final fields = await input
.transform(utf8.decoder)
.transform(const CsvToListConverter(shouldParseNumbers: true))
.toList();
return fields;
} catch (e) {
logger.e('Error reading CSV from file: $e');
return [];
}
}

void writeMetaData(String instrumentName, List<List<dynamic>> data) {
if (data.isNotEmpty && data[0].isNotEmpty && data[0][0] == instrumentName) {
return;
}

final now = DateTime.now();
final sdf = DateFormat('yyyy-MM-dd HH:mm:ss');
final metaDataTime = sdf.format(now);
final metaData = [
instrumentName,
metaDataTime.split(' ')[0],
metaDataTime.split(' ')[1]
];
data.insert(0, metaData);
}
}
Loading
Loading