Feature to generate Config File in PSLab Android application

In this blog, I will explain the feature to generate “Config File” in PSLab Android Application 

What is a Config File?

The main aim of this feature is to make PSLab board a self data logger, which would read user-defined configs from a config file stored on SD card connected to PSLab board and based on instrument, parameters and time interval stored in config file PSLab board would automatically log those values. 

Now as the first step of this feature, an option is added to PSLab Android application, where user can create a config file. User can select an instrument, parameters associated with that instrument and time interval. With this feature, user can easily generate a config file which can later be used by PSLab board for logging.

User Interface

The option to generate a config file is given in the side navigation menu on the main screen. 

(Figure 1: Generate Config file menu)

Once the user selects the “Generate Config File” option, the user will be directed to the following screen where user can create a config file with intended parameters

(Figure 2: Generate Config File UI)

As can be seen in the screenshot above the user can select instruments for which the config file needs to be created from a drop-down menu. User can specify the time interval, for which the data should be logged by the PSLab board. Based on the instrument selected by the user corresponding parameters will be shown at the bottom. User can select whichever parameters are required and click on “CREATE CONFIG FILE” button and a config file will be saved on device local storage. 

A config file for Oscilloscope with 25-sec interval and CH1, CH2 and CH3 parameters would look something like below,

(Figure 3: Sample config File )

Implementation

When a user clicks on Create Config File button, First we check whether the user has provided a time interval, if not a toast message appears to let the user know that time interval is missing. This is done using the following lines of code,

createConfigFileBtn.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {
     interval = intervalEditText.getText().toString();
     if (interval.length() == 0) {
        Toast.makeText(CreateConfigActivity.this, getResources().getString(R.string.no_interval_message), Toast.LENGTH_SHORT).show();
                }

Once the user sets the time interval and selects the parameters, the following lines of code generates a string array containing params selected by the user.

ArrayList<String> selectedParamsList = new ArrayList<>();
for (int i = 0; i < paramsListContainer.getChildCount(); i ++) {
    CheckBox checkBox = (CheckBox) paramsListContainer.getChildAt(i);
    if (checkBox.isChecked()) {
       selectedParamsList.add(instrumentParamsList.get(selectedItem)[i]);
    }
}

After we have the list of selected parameters we call the following function to create the config file

private void createConfigFile(ArrayList<String> params) {
        String instrumentName = instrumentsList.get(selectedItem);
        String fileName = "pslab_config.txt";
        String basepath = Environment.getExternalStorageDirectory().getAbsolutePath();

        File baseDirectory = new File(basepath + File.separator + CSVLogger.CSV_DIRECTORY);
        if (!baseDirectory.exists()) {
            try {
                baseDirectory.mkdir();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        File configFile = new File(basepath + File.separator + CSVLogger.CSV_DIRECTORY + File.separator + fileName);
        if (!configFile.exists()) {
            try {
                configFile.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        try {
            FileWriter writer = new FileWriter(configFile);
            writer.write("instrument: " + instrumentName + "\n");
            writer.write("interval: " + interval + " " + intervalUnit + "\n");
            String param = String.join(",", params);
            writer.write("params: " + param);
            writer.flush();
            writer.close();
            CustomSnackBar.showSnackBar(rootView, getString(R.string.file_created_success_message), null, null, Snackbar.LENGTH_SHORT);
        } catch (IOException e) {
            e.printStackTrace();
            CustomSnackBar.showSnackBar(rootView, getString(R.string.file_created_fail_message), null, null, Snackbar.LENGTH_SHORT);
        }

    }

In the first part of this function, we check whether there exists a PSLab directory in the local storage of the device, if not the directory is created. After that, we create a file named “pslab_config.txt”. After that, we use FileWriter to write data to the file. 

In a nutshell with this feature user can create config files easily. The following GIF demonstrated this functionality.

(Figure 4: GIF of the functionality)

References

Tags: PSLab, Android, GSoC 19, Config File, data logger

Continue Reading

Gas sensor (MQ-135) support In PSLab Android application

Along with lots of sensors provided in the PSLab Android application, recently support for a new sensor – MQ-135 gas sensor has been added to the app. In this blog, I will discuss what is this gas sensor and how to use it with PSLab Android application

MQ-135 Gas sensor

The MQ-135 gas sensors are used in air quality control equipment and are suitable for detecting or measuring of NH3, NOx, Alcohol, Benzene, Smoke, CO2. 

The Pin layout of MQ-135 sensor

(Figure 1: MQ-135 pin layout)

How to Connect MQ-135 to PSLab Board

The following diagram shows how a user can connect MQ-135 sensor to a PSLab Board. 

(Figure 2: MQ-135 and PSLab connections)

As can be seen in the diagram above connect Voltage pin of MQ-135 sensor to one of the VDD pins on the PSLab board. Connect the Ground pin of MQ-135 sensor to one of GND pins on the PSLab board. And connect Analog Output pin to CH1 pin on the PSLab board. Once these connections are made user can connect PSLab board to their mobile phone and start reading data using Gas Sensor instrument in PSLab Android application

Gas Sensor Instrument in PSLab Android Application

To provide users an interface to read values collected by MQ-135 sensor connected to PSLAb board, a new instrument screen has been added to the PSLab Android application. The UI of the screen is shown below,

(Figure 3: Gas Sensor instrument UI)

As can be seen, the user is provided with a circular meter, a text box and a graph, all of which indicates the amount of different gases sensed by MQ-135 in PPM (parts per million) unit. The data is collected by very simple lines of codes. Since we are connecting Analog Output of MQ-135 to CH1 on PSLab board, we need to read the voltage at CH1 pin. Which would be in the range of 0 – input voltage (which is 3.3V in our case). To convert the voltage values to PPM, we map these output voltages to a range of 0 – 1024. This is done by following lines of code.

double volt = scienceLab.getVoltage("CH1", 1);
double ppmValue = (volt / 3.3) * 1024.0;

As provided in all the other instruments in PSLab Android application, Gas Sensor also has data logging and importing feature. User can record the data and store it as a CSV file and import previously recorded data into the PSLab application easily.

So in conclusion, now users can utilize and experiment with MQ-135 sensor effortlessly using PSLab Android application.

A working demo of this feature can be seen in the following video

https://drive.google.com/file/d/1-KxOaqE_Y5EYquMkebYpBOEc0d7GAdLS/view?usp=sharing

References:

Tags: PSLab, Android, GSoC 19, Sensors, Gas Sensor, MQ-135

Continue Reading

Two flavors of PSLab Android App to support Google Maps (in Play Store flavor) and Open Street maps (in Fdroid flavor)

What are the flavors of an App? And why are they needed in PSLab Android App?

While working on the PSLab Android Project, I ran into the need to create different variants of the app with different dependencies. In this blog, I have tried to explain the process of creating various flavors of the app in the easiest way possible. 

Android Allows Developers to create different variants of the same app with the same code base but having some functionalities different across the variants. These functionalities may include some special/pro features, some different dependencies, etc. Such variants are called flavors of the App. Most common flavors are Paid and Free version of the app.

In the PSLab Android Application, we needed to generate flavors, when we required to use Google Maps in the App. The app is also published on the Fdroid, which doesn’t allow dependencies of Google Maps. Hence 2 flavors of the app have been created, 

  1. Play Store Flavor (With Google Maps)
  2. F-Droid Flavor (With Open Street Maps)

Declaring Flavors in the build.gradle File

In order to create flavors of the app, first, we need to declare flavors in the Gradle file. In PSLab Android app we are creating 2 flavors, which are declared in the build.gradle file as under

flavorDimensions 'default'
productFlavors {
   fdroid {
       dimension = 'default'
   }
   playstore {
       dimension = 'default'
   }
}

flavorDimensions is used to package flavors if there are many flavors for an App. Since we have only two flavors fdroid and playstore, hence we are using single dimension default for both the flavors. Once this has been added to the build.gradle file we need to sync the gradle. 

After the Sync is complete, if we open the Build Variants tab from the left corner of the Android Studio, it would look something like this: 

(Figure 1: Build Variant Window of Android Studio)

As can be seen in the screenshot above, once the gradle is successfully synced, Android Studio automatically creates debug and release build variants for each flavor and we can easily toggle between variants and build/ run / make apk for each variant. Congratulations! We have successfully finished the first step towards creating flavors of an app.

Directory Structure after creating Flavors

Apart from creating the build variants of different flavors, Android studio also creates src/<flavor name> folders for us. Now if we want to add new activities and classes to these flavors we can create java, res, values folders inside this folder. We can define separate Manifest file as well for each flavor individually. The directory structure of the PSLab Android project after creating required packages inside the automatically generated src/fdroid and src/playstore folders looks like below,  

(Figure 2: Directory structure after creating flavors)

Defining Flavor specific dependencies

We can have some dependencies for one app flavor and some for others. For example, in PSLab Android app, we need Google Maps dependencies only in playstore flavor and Open Street Maps dependencies only in fdroid flavor. We can easily define flavor specific dependencies by adding flavor name before Implementation command in gradle file. Like below,

// Map libraries
fdroidImplementation "org.osmdroid:osmdroid-android:$rootProject.osmVersion"
fdroidImplementation "org.osmdroid:osmdroid-mapsforge:$rootProject.mapsforgeVersion"
fdroidImplementation "org.osmdroid:osmdroid-geopackage:$rootProject.geoPackageVersion"
playstoreImplementation "com.google.android.gms:play-services-maps:$rootProject.googleMapsVersion"

Same Activity/Class with different Flavor 

Now the main purpose of creating flavors is to have some different functionalities between the flavors. For that we need the base app to call different class/activity from the src/<flavor name> folder depending on the selected flavor. We will discuss this in reference to PSLab Android app. 

So, for PSLab android app we want app to open Google Maps in Play Store flavor and Open Street Maps in froid flavor. For this we need to create a duplicate Activity. Which means we will have two separate implementation of same Activity MapsActivity.java , one in the F-Droid source folder and one in playstore source folder. So MapsActivity.java will only be declared once in the src/main/AndroidManifest.xml file, but there will be two different classes for this activity in each flavor folder. Now when the main app will call MapsActivity.class from any intent depending on the selected build variant either playstore version of MapsActivity will be launched or the F-Droid version. So after creating two instances of the MapsActivity.java the directory structure would look something like given in the screenshot below,

(Figure 3: Directory structure after creating MapsActivity)

As can be seen in the directory structure, now both Play Store and F-Droid folders have their own instances of MapsActivity.java , and now we can easily implement code for Open Street Maps and Google Maps in the respective MapsActivity.java and we have two versions of the app working flawlessly. 

References

Tags: GSoC ‘19, PSLab, Android, Flavors, GoogleMaps, OpenStreetMaps, Build Variants

Continue Reading

The Robotic Arm Controller Feature in PSLab Android Application.

Recently while working on PSLab Android Project, a new feature was implemented to add a controller for a Robotic Arm in the Android Application. In this blog, I will explain what this new feature is, how it has been implemented and what are some of the functionalities of it.

What is Robotic Arm?

Robotic Arm, as the name suggests is a small arm-like structure, which moves with the help of 4 servo motors connected to it. The image of the robotic arm is as under, 

(Figure 1 : Robotic Arm)

What is the Robotic Arm Controller Feature in Android App?

As mentioned earlier, the robotic arm uses 4 servos for the movements. The aim of the controller feature in the Android app is to allow users to adjust the rotation of each servo with a very intuitive UI, so they can move the robotic arm as they wish. Further, in this blog, I will discuss UI, implementation and some cool features of the robotic Arm Controller.

User Interface


(figure 2: UI of the robotic arm controller)

In the screenshot above, there are 4 circular controllers for each respective servo. The user can use the knob or enter the values using the keyboard by tapping on the value at the center of the knobs. Each value indicates the value in degrees the user wishes to move that servo.

Timeline

Below the 4 servo controllers is a black timeline. There are 60 boxes in each of the 4 timelines. These 60 boxes indicates the seconds. So basically if the user wants the robotic arm to perform some set of actions sequentially, using these timelines, users can set the rotation for each servo at each second, then user can use the play button on the red control panel to play this timeline and the app will send the degree value at each second to the respective servo. A filled timeline would look something like below, 


(Figure 3: Timeline)

As can be seen, there are different values for each servo at each second, so when the user starts the timeline, values at each second will be sent to the respective servos. servo4() function of the ScienceLab class is called to set values for all for servos at each second.

How to add the values to the timeline?

There is a small handle on the top right corner of each servo controller, user can long-press the handle and drag and drop the values to desired seconds for respective servos.  This functionality can be found in this video between 0:33 to 0:43

This feature uses Android’s drag and drops listener. The code for this drag and drop function is as under,

private View.OnDragListener servo1DragListener = new View.OnDragListener() {
        @Override
        public boolean onDrag(View v, DragEvent event) {
            if (event.getAction() == DragEvent.ACTION_DRAG_ENTERED) {
                View view = (View) event.getLocalState();
                TextView text = view.findViewById(R.id.degreeText);
                if (view.getId() == R.id.servo_1) {
                    ((TextView) v.findViewById(R.id.timeline_box_degree_text)).setText(text.getText());
                }
            }
            return true;
        }
    };

Such drag listeners are created for each servo controller.

Save Timeline Feature

Suppose user has set a whole 60 seconds timeline for some action, and the user wants to use that set of values again and again, it doesn’t make sense if the user has to set the values every time. So there is a feature to save the timeline as well. Whenever user has created some timeline, user can just click on the save button on the control panel and the timeline will be saved both as a CSV file and as a realm object. So the timeline will be visible in the DataLoggerActivity as well. Whenever user opens a logged timeline, values for the respective servo timeline will be set automatically.

The code to save the timeline is as below.  For each second a new ServoData object is created with degree values for all 4 servos at that second.

private void saveTimeline() {
        long block = System.currentTimeMillis();
        servoCSVLogger.prepareLogFile();
        servoCSVLogger.writeMetaData(getResources().getString(R.string.robotic_arm));
        String data = "Timestamp,DateTime,Servo1,Servo2,Servo3,Servo4,Latitude,Longitude\n";
        long timestamp;
        recordSensorDataBlockID(new SensorDataBlock(block, getString(R.string.robotic_arm)));

So first we create a block variable using the current timestamp, this block will be used for all the realm object stored for this timeline. We also prepare a CSV file and CSV header for the log. 

String degree1, degree2, degree3, degree4;
for (int i = 0; i < 60; i++) {
     timestamp = System.currentTimeMillis();
     degree1 = degree2 = degree3 = degree4 = "0";
     if (((TextView) servo1TimeLine.getChildAt(i).findViewById(R.id.timeline_box_degree_text)).getText().length() > 0) {
         degree1 = ((TextView) servo1TimeLine.getChildAt(i).findViewById(R.id.timeline_box_degree_text)).getText().toString();
     }

Now, we run a for loop for each 60 second timeline and store the set value of each servo in degree1, degree2, degree3 and degree4 variables. In the above code snippet only degree1 is shown, but the same thing is done for other values as well. Once we have 4 degree values for each servo at for a second, we store it as an ServoData Object in realm and also write it to the CSV file using the following lines of code,

recordSensorData(new ServoData(timestamp, block, degree1, degree2, degree3, degree4, lat, lon);
servoCSVLogger.writeCSVFile(data);

Here the data variable is a string with comma-separated values of the degree values of each servo.

Once the user saves the timeline the generated CSV looks something like below,


(Figure 4: Saved timeline CSV)

To set the timeline from the saved logged data, a small function just iterates over all the ServoData objects stored in the realm and set the value for the respective servo in the timeline. The function is as under,

private void setReceivedData() {
        ArrayList servoDataList = new ArrayList(recordedServoData);
        for (int i = 0; i < servoDataList.size(); i++) {
            ServoData servoData = (ServoData) servoDataList.get(i);
            ((TextView) servo1TimeLine.getChildAt(i).findViewById(R.id.timeline_box_degree_text)).setText(servoData.getDegree1());
            ((TextView) servo2TimeLine.getChildAt(i).findViewById(R.id.timeline_box_degree_text)).setText(servoData.getDegree2());
            ((TextView) servo3TimeLine.getChildAt(i).findViewById(R.id.timeline_box_degree_text)).setText(servoData.getDegree3());
            ((TextView) servo4TimeLine.getChildAt(i).findViewById(R.id.timeline_box_degree_text)).setText(servoData.getDegree4());
        }
    }

Conclusion

This feature enables the user to control the robotic arm with a very intuitive user interface. And the save timeline feature allows the user to use/share already stored timeline for some actions with other users. 

A small video to explain the save timeline functionality can be seen below,

References

Tags : GSoC ‘19, PSLab, Android, Robotic Arm

Continue Reading

Importing files from local storage in PSLab Android application

This blog demonstrates how a user can import log files from local storage to the PSLab Android application for various instruments and play them. This functionality is really useful as users can share their log files and import them in their app. This blog mostly consists of my work in the PSLab Android repository.

How to access local storage files?

We here use the concept of implicit intent to access the local storage of the device and then generate the file from the received data URI.

Implicit intents differ from explicit intents in a way that, they don’t give exact class or activity to be initialized through the intent, instead they provide the action to be performed and the class or activities are selected implicitly from the required action

The code block is shown below. 

private void selectFile() {
        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
        intent.setType("*/*");
        startActivityForResult(intent, 100);
}

Here the Intent.ACTION_GET_CONTENT defines implicit intent. This intent opens the activity related to the action of GETTING CONTENT. The type of content is specified in the Intent.setType(<TYPE>). Since here the type is set to “*/*”, it will open all types of files. If we want only images we can set Type to “images”.

startActivityForResult(intent, <REQUEST_CODE>) starts the file selection activity. 

How to generate a file from received URI?

Once the user selects a file from the file selection activity we can generate the selected file from the data passed in the callback function of startActivityForResult(). The data intent passed as a parameter to onActivityResult() callback contains data for the selected file. We can retrieve path, name, etc details of the selected file from this data intent. The code block for the same is given below.

@Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        if (requestCode == 100) {
            if (resultCode == RESULT_OK) {
                Uri uri = data.getData();
                String path = uri.getPath();
                path = path.replace("/root_path/", "/");
                File file = new File(path);
                getFileData(file);
            }
            else Toast.makeText(this, this.getResources().getString(R.string.no_file_selected), Toast.LENGTH_SHORT).show();
        }
    }

Here we check for the requestCode, which we passed when calling the startActivityForResult() function. We further check if the result is valid and then generate the file from the file path we received in the data Intent. Once we get the path we can get the selected file using the following lines of code:

String path = uri.getPath();
path = path.replace("/root_path/", "/");
File file = new File(path);

How to get Data from the file?

Once the file is generated, it is passed to a function getFileData(File file) to get data in the file to add to the logs of the selected device.  The main part of the getFileData function is given below.

FileInputStream is = new FileInputStream(file);
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
String line = reader.readLine();
int i = 0;
long block = 0, time = 0;
while (line != null) {
   if (i != 0) {
        String[] data = line.split(",");
        try {
              time += 1000;
              BaroData baroData = new BaroData(time, block, Float.valueOf(data[2]),                              Double.valueOf(data[3]), Double.valueOf(data[4]));
              realm.beginTransaction();
              realm.copyToRealm(baroData);
              realm.commitTransaction();
            } catch (Exception e) {
       Toast.makeText(this, getResources().getString(R.string.incorrect_import_format), Toast.LENGTH_SHORT).show();
           }
    }
    i++;
    line = reader.readLine();

Here we read the file line by line and convert the CSV data into the object of the selected device. And then this data is added to app storage using the realm. As shown in the code block above, we are parsing the data to the BarometerData class instances. We split each line by “,” and then use each field as input to the constructor of the BarometerData class. Once we create the instances of the class, we add them to the realm, so the imported file is saved in the realm and now we can access it easily from DataLoggerActivity.

The following images demonstrate the functionality of Import log 

Step 1: Select Import Log menu from 


(Figure 1: Import Log menu)

Step 2: Select the file to be imported from the local storage 


(Figure 2: Files to import from Local storage)

Step 3: Play the imported log from the DataLoggerActivity


(Figure 3: Imported logged data in DataLoggerActivity)

Resources

Tags: PSLab, Android, GSoC 19, ImportLog, Intents, Implicit Intent

Continue Reading

Examples of how AsyncTask is used in PSLab Android App

In this blog, we will look at a very useful and important feature provided by Android – AsyncTask and more importantly how AsyncTasks have been put to use for various functionalities throughout the PSLab Android Project

What are Threads?

Threads are basically paths of sequential execution within a process. In a way, threads are lightweight processes. A process may contain more than one threads and all these threads are executed in parallel. Such a method is called “Multithreading”. Multithreading is very useful when some long tasks need to be executed in the background while other tasks continue to execute in the foreground.

Android has the main UI thread which works continuously and interacts with a user to display text, images, listen for click and touch, receive keyboard inputs and many more. This thread needs to run without any interruption to have a seamless user experience.

When AsyncTask comes into the picture?

AsyncTask enables proper and easy use of the UI thread. This class allows you to perform background operations and publish results on the UI thread without having to manipulate threads and/or handlers.  

In PSLab Android application, we communicate with PSLab hardware through I/O(USB) interface. We connect the PSLab board with the mobile and request and wait for data such as voltage values and signal samples and once the data is received we display it as per requirements. Now clearly we can’t run this whole process on the main thread because it might take a long time to finish and because of that other UI tasks would be delayed which eventually degrade the user experience. So, to overcome this situation, we use AsyncTasks to handle communication with PSLab hardware.

Methods of AsyncTask  

AsyncTask is an Abstract class and must be subclassed to use. Following are the methods of the AsyncTask:

  • onPreExecute()
    • Used to set up the class before the actual execution
  • doInBackground(Params…)
    • This method must be overridden to use AsyncTask. This method contains the main part of the task to be executed. Like the network call etc.
    • The result from this method  is passed as a parameter to onPostExecute() method
  • onProgressUpdate(Progress…)
    • This method is used to display the progress of the AsyncTask
  • onPostExecute(Result)
    • Called when the task is finished and receives the results from the doInBackground() method

There are 3 generic types passed to the definition of the AsyncTask while inheriting. The three types in order are 

  1. Params: Used to pass some parameters to doInBackground(Params…) method of the Task 
  2. Progress: Defines the units in which the progress needs to be displayed/
  3. Result : Defines the data type to be returned from onInBackground() and receive as a parameter in the onPostExecute(Result) method

Example of the usage of the AsyncClass is as under : 

private class SampleTask extends AsyncTask<Params, Progress, Result> {
     @Override
     protected Result doInBackground(Params... params) {
          // The main code goes here
          return result;
     }
     @Override 
     protected void onProgressUpdate(Progress... progress) {
          // display the progress
     }
     @Override 
     protected void onPostExecute(Result result) {
         // display the result
     }
}

We can create an instance of this class as under and execute it.

SampleTask sampleTask = new SampleTask();
sampleTask.execute(params)

We can cancel a running class by calling the task.cancel() function

sampleTask.cancel()

AsyncTask in PSLab Android Application

As mentioned earlier some task which takes a lot of time, can’t be executed on the main thread. Hence in such cases AsyncTask is used. We will look into some examples where AsyncTask has been put to use in PSLab Android Application

Delete All Logs: 

In the DataLoggerActivity, user has an option to delete all the logs that have been saved on the local storage. Now there might be a lot number of log files that needs to be deleted. Hence it is better to use AsyncTask for these. The code snippet for this is below,

private class DeleteAllTask extends AsyncTask<Void, Void, Void> {
        @Override
        protected Void doInBackground(Void... voids) {
            Realm realm = Realm.getDefaultInstance();
            for (SensorDataBlock data : realm.where(SensorDataBlock.class)
                    .findAll()) {
                File logDirectory = new File(
                        Environment.getExternalStorageDirectory().getAbsolutePath() +
                                File.separator + CSVLogger.CSV_DIRECTORY +
                                File.separator + data.getSensorType() +
                                File.separator + CSVLogger.FILE_NAME_FORMAT.format(data.getBlock()) + ".csv");
                logDirectory.delete();
                realm.beginTransaction();
                realm.where(SensorDataBlock.class)
                        .equalTo("block", data.getBlock())
                        .findFirst().deleteFromRealm();
                realm.commitTransaction();
            }
            realm.close();
            return null;
        }
        @Override
        protected void onPostExecute(Void aVoid) {
            deleteAllProgressBar.setVisibility(View.GONE);
            if (LocalDataLog.with().getAllSensorBlocks().size() <= 0) {
                blankView.setVisibility(View.VISIBLE);
            }
        }
    }

As can be seen, we look for all the stored logs, and then delete each file one after another in doInBackground(). Once all the files are deleted, onPostExecute() is called, where we make the progress bar disappear. So, this how AsyncTask is used to implement deleteAllFiles feature.

Capture Task and Fourier Transform Output of Signals in Oscilloscope.

To display the generated signal in the oscilloscope, we call captureTraces() and fetchTraces functions from the ScienceLab class. Now, both these functions communicate with the PSLab Board, request for data, receives the data, manipulates it into the desired format and then display the signal on the Oscilloscope screen. Now clearly we can’t afford to run such a process on the main thread. So we use AsyncTask to handle it. 

In the Oscilloscope, there is a feature to see the fourier transform output of the signal generated by the oscilloscope. Now to generate the Fourier Transform Output of the signal, we use the Fast Fourier Transform method. The time complexity of  FFT (Fast Fourier Transform) is O(Nlog(N)), where N is the number of samples of the input signal. Now even if FFT is fast, we can risk to run this function on the main Thread. So once again we get help from AsyncTask.
Both of these functionalities are included in same AsyncTask Class called captureTask  A snippet for this task can be seen below,

public class CaptureTask extends AsyncTask<String, Void, Void> {
        private ArrayList<ArrayList<Entry>> entries = new ArrayList<>();
        private ArrayList<ArrayList<Entry>> curveFitEntries = new ArrayList<>();
        private Integer noOfChannels;
        private String[] paramsChannels;
        private String channel;
        @Override
        protected Void doInBackground(String... channels) {
            paramsChannels = channels;
            noOfChannels = channels.length;
            try {
                double[] xData;
                double[] yData;
                ArrayList<String[]> yDataString = new ArrayList<>();
                String[] xDataString = null;
                maxAmp = 0;
                for (int i = 0; i < noOfChannels; i++) {
                    entries.add(new ArrayList<>());
                    channel = channels[i];
                    HashMap<String, double[]> data;
                    if (triggerChannel.equals(channel))
                        scienceLab.configureTrigger(channelIndexMap.get(channel), channel, trigger, null, null);
                    scienceLab.captureTraces(1, samples, timeGap, channel, isTriggerSelected, null);
                    data = scienceLab.fetchTrace(1);

In this part of the capture Task class, we use the captureTrace() and fetchTrace() function to get the signal samples and then store them into the data variable. Below is the part where we use call the fft() for the input signal.

if (isFourierTransformSelected) {
     Complex[] yComplex = new Complex[yData.length];
     for (int j = 0; j < yData.length; j++) {
              yComplex[j] = Complex.valueOf(yData[j]);
     }
     fftOut = fft(yComplex);
}

This is a very simple part where we just call the Fast Fourier Transfer function is the user has selected to see the fourier transform output. The implementation of the Fourier function can be seen below,

 public Complex[] fft(Complex[] input) {
        Complex[] x = input;
        int n = x.length;
        if (n == 1) return new Complex[]{x[0]}; // if only single element, return as it is
        if (n % 2 != 0) {
            x = Arrays.copyOfRange(x, 0, x.length - 1);
        //No of samples should be even for this function to run, so i case of odd samples we remove the last element. This doesn’t affect the output significantly
        }
        Complex[] halfArray = new Complex[n / 2];
        for (int k = 0; k < n / 2; k++) {
            halfArray[k] = x[2 * k]; // Array of input terms at even places
        }
        Complex[] q = fft(halfArray); // recursive call for even terms
        for (int k = 0; k < n / 2; k++) {
            halfArray[k] = x[2 * k + 1]; // Array of terms at odd places
        }
        Complex[] r = fft(halfArray); // recursive call for odd terms
        Complex[] y = new Complex[n]; // Array of final output
        for (int k = 0; k < n / 2; k++) {
            double kth = -2 * k * Math.PI / n;
            Complex wk = new Complex(Math.cos(kth), Math.sin(kth)); // “kernel” for kth term is the output (based on nth root of unity)
            if (r[k] == null) {
                r[k] = new Complex(1); // exception handling
            }
            if (q[k] == null) {
                q[k] = new Complex(1); // exception handling
            }
            y[k] = q[k].add(wk.multiply(r[k])); // kth term will be addition of odd and even terms
            y[k + n / 2] = q[k].subtract(wk.multiply(r[k])); // (k + n/2)th term will be subtraction of odd and even terms
        }
        return y; // rsultant array
    }

This is a classic implementation of Fast Fourier Transform. We divide the samples of input into odd and even placed terms and call the same function recursively until there is only one term left. After that we use nth (n being the number of samples) complex root of  unity, we combine the results of odd termed fft() and even termed fft() to get the final output. Since at each iteration we are breaking the input into half it will run for O(logN) time and to merge the odd and even termed output we run a loop in each iteration on the O(N). So the total complexity would be O(NlogN), and since it might take longer to compute the fourier transform for large input we require it to be inside the AsyncTask and not on the main thread.

There are many other functionalities throughout the app, where AsyncTask has been used. In a nutshell, AsyncTask is a very useful method to handle longer tasks off the main thread. 

Resources

Tags: PSLab, Android, GSoC 19, AsyncTask, Threading

Continue Reading

How to use and implement Save Wave Configs feature in Pocket Science Lab Wave Generator

What is a Wave Generator?

A Wave Generator is one of the most important features of PSLab. It is used to generate different kinds of waves like, sine, triangular, square, PWM. Wave generator UI is as under:

  (Figure 1 : Wave Generator Analog Mode UI)
  (Figure 2 : Wave Generator Digital Mode UI)

As can be seen the Screenshot above user is provided with options to set Frequency, Phase, Duty of different waves and once configurations are set user can either output the waves in Oscilloscope or can compare different waves in Logic Analyzer.

What is Save Wave Configs Feature?


        (Figure 3 : Wave Generator Control Buttons (View,Save,Mode))

In this feature, the user is given a ”Save” button to use this feature. 

The reason to add this feature is that, sometimes we need to perform the same experiment multiple times, is such scenarios if we have to set wave configurations everytime, it will become boring and there will be chances of errors. Hence using the save configs feature, user can currently set configurations in the Local Storage and can use it anytime later. 

Further since the Wave Configurations are saved on Local Storage as .CSV file, a user can save configs and can share the file with others so others can as well set their device to same configurations. The saved Wave Configurations can be seen in the DataLogger Activity and opening a saved log would take the user to Wave Generator Activity where all the configs will be set as per the saved log.

A sample CSV of the log data can be seen below.


(Figure 4: Wave Configs CSV file)

How is Save Configs Feature Implemented

The implementation of this feature is quite simple. There is a class named WaveData.  With the parameters of Mode(Square or PWM), Wave name, Shape, Freq, Phase and Duty. Whenever the user clicks the save configs button, the saveWaveConfigs()  function is called. This function fetches set values of different fields and creates realm objects and also write them to csv file as shown above. Once the realm objects are created, this log can be seen in the Data Logger Activity. The code to generate the realm object for the wave configs (that is the implementation of the function saveWaveConfig()) is given below.

public void saveWaveConfig(View view) {
        long block = System.currentTimeMillis();
        csvLogger.prepareLogFile();
          csvLogger.writeMetaData(getResources().getString(R.string.wave_generator));
        long timestamp;
        double lat, lon;
        String data = "Timestamp,DateTime,Mode,Wave,Shape,Freq,Phase,Duty,lat,lon\n";
        recordSensorDataBlockID(new SensorDataBlock(block, getResources().getString(R.string.wave_generator)));

So till now in the function, we create a header string for the data to be stored in the csv file. We create a block from the current system time. This block will be used to save all the realm object for this function, so all the objects created at this instance will be grouped as a single log entry in DataLoggerActivity.

double freq1 = (double) (WaveGeneratorCommon.wave.get(WaveConst.WAVE1).get(WaveConst.FREQUENCY));
double freq2 = (double) WaveGeneratorCommon.wave.get(WaveConst.WAVE2).get(WaveConst.FREQUENCY);
double phase = (double) WaveGeneratorCommon.wave.get(WaveConst.WAVE2).get(WaveConst.PHASE);

String waveType1 = WaveGeneratorCommon.wave.get(WaveConst.WAVE1).get(WaveConst.WAVETYPE) == SIN ? "sine" : "tria";
String waveType2 = WaveGeneratorCommon.wave.get(WaveConst.WAVE2).get(WaveConst.WAVETYPE) == SIN ? "sine" : "tria";

timestamp = System.currentTimeMillis();
String timeData = timestamp + "," + CSVLogger.FILE_NAME_FORMAT.format(new Date(timestamp));
String locationData = lat + "," + lon;

Next, in the function we get currently set Frequency for both analog waves and phase in the variables. We also store the selected wave shape for each of the waves. Since each entry in the csv file is required to have a timestamp and a location stamp,here we create common stamps of both types and will append it to each entry further in the function. 

else if (data.getMode().equals(MODE_PWM)) {
                WaveGeneratorCommon.mode_selected = WaveConst.PWM;
                switch (data.getWave()) {
                    case "Sq1":
                        WaveGeneratorCommon.wave.get(WaveConst.SQR1).put(WaveConst.FREQUENCY, Double.valueOf(data.getFreq()).intValue());
                        WaveGeneratorCommon.wave.get(WaveConst.SQR1).put(WaveConst.DUTY, ((Double) (Double.valueOf(data.getDuty()) * 100)).intValue());
                        break;
                }
                enableInitialStatePWM();
            }
        }

Here we check whether the currently selected mode is Analog(Square) or Digital (PWM). Above code snippet is for the SQUARE mode block. We create WaveGeneratorData object for both SI1 and SI2 waves based on the parameters we stored earlier. We also append the data to a string, data.  Which we will later use to write the log into a csv file.

else {
   double freqSqr1 = (double) WaveGeneratorCommon.wave.get(WaveConst.SQR1).get(WaveConst.FREQUENCY);
   double dutySqr1 = (double) WaveGeneratorCommon.wave.get(WaveConst.SQR1).get(WaveConst.DUTY) / 100;
   double dutySqr2 = ((double) WaveGeneratorCommon.wave.get(WaveConst.SQR2).get(WaveConst.DUTY)) / 100;
   double phaseSqr2 = (double) WaveGeneratorCommon.wave.get(WaveConst.SQR2).get(WaveConst.PHASE) / 360;
   double dutySqr3 = ((double) WaveGeneratorCommon.wave.get(WaveConst.SQR3).get(WaveConst.DUTY)) / 100;
   double phaseSqr3 = (double) WaveGeneratorCommon.wave.get(WaveConst.SQR3).get(WaveConst.PHASE) / 360;
   double dutySqr4 = ((double) WaveGeneratorCommon.wave.get(WaveConst.SQR4).get(WaveConst.DUTY)) / 100;
   double phaseSqr4 = (double) WaveGeneratorCommon.wave.get(WaveConst.SQR4).get(WaveConst.PHASE) / 360;

 data += timeData + ",PWM,Sq1,PWM," + String.valueOf(freqSqr1) + ",0," + String.valueOf(dutySqr1) + "," + locationData + "\n";

 recordSensorData(new WaveGeneratorData(timestamp, block, "PWM", "Sq1", "PWM", String.valueOf(freqSqr1), "0", String.valueOf(dutySqr1), lat, lon));
}

The above code snippet shows a block of the condition when the selected mode is PWM. Here we store the set values of Freq, Phase and Duty for each SQ1, SQ2, SQ3 and SQ4 waves into variables. Once we store the values we create WaveGeneratorData objects for each of the waves and also append the data to the data string to write to the csv. The code above includes details only for SQ1, but exact same procedure is followed for SQ2, SQ3, and SQ4. One we have all the data appended to the string we call the following function to write the data to csv file. 

 csvLogger.writeCSVFile(data);

We can see that this function basically stores the current set values of different params into a WaveData object. For each of the waveforms in selected mode (analog/digital), a new instance of WaveData object is created and stored into realm.

When the user opens one of the logs, setReceivedData() function is called in WaveGeneratorActivity. This function iterates on the received realm objects and based on the attributes of each object the data is set in the UI automatically. The implementation of this function is given below, 

public void setReceivedData() {
        for (WaveGeneratorData data : recordedWaveData) {
            Log.d("data", data.toString());
            if (data.getMode().equals(MODE_SQUARE)) {
                WaveGeneratorCommon.mode_selected = WaveConst.SQUARE;
                switch (data.getWave()) {
                    case "Wave1":
                        if (data.getShape().equals("sine")) {
                            WaveGeneratorCommon.wave.get(WaveConst.WAVE1).put(WaveConst.WAVETYPE, SIN);
                        } else {
                            WaveGeneratorCommon.wave.get(WaveConst.WAVE1).put(WaveConst.WAVETYPE, TRIANGULAR);
                        }
                        WaveGeneratorCommon.wave.get(WaveConst.WAVE1).put(WaveConst.FREQUENCY, Double.valueOf(data.getFreq()).intValue());
                        break;
                }
                enableInitialState();
            } 

This function iterates over the received WaveGeneratorData objects. For each object we check what is the mode of the waveData. The above code snippet is used when the mode is SQUARE. We get the waveType from the object, and since for SQUARE mode there are only 2 types : Wave1 and Wave2, we set the attributes for each wave as we get them from the objects using WaveGeneratorCommon

else if (data.getMode().equals(MODE_PWM)) {
                WaveGeneratorCommon.mode_selected = WaveConst.PWM;
                switch (data.getWave()) {
                    case "Sq1":
                        WaveGeneratorCommon.wave.get(WaveConst.SQR1).put(WaveConst.FREQUENCY, Double.valueOf(data.getFreq()).intValue());
                        WaveGeneratorCommon.wave.get(WaveConst.SQR1).put(WaveConst.DUTY, ((Double) (Double.valueOf(data.getDuty()) * 100)).intValue());
                        break;
                }
                enableInitialStatePWM();
            }
        }

Same as before if the mode of the object is PWM, there will be 4 cases : SQ1, SQ2, SQ3 and SQ4. And depending on the data stored in the received objects.

In a nutshell this features enables to save and reuse wave configuration with ease. 

A small video to explain the whole functionality of this feature can be found here. 

References

Write to a file in Android

Code Repository

PSLab Android

Tags

PSLab, Wave Generator, SaveConfig, Android, GSoC 19

Continue Reading
Close Menu