Migration to Model-View-ViewModel Architecture and LiveData in Open Event Organizer App

Open Event Organizer App (Eventyay Organizer App) is the Android app used by event organizers to create and manage events on the Eventyay platform as well as check-in and check-out attendees along with other functionalities. The app used the MVP (Model-View-Presenter) architecture and is being ported to MVVM (Model-View-ViewModel). This article will explain the procedure of migrating MVP to MVVM architecture and implementing LiveData. 

Why migrate to MVVM?

The MVVM architecture is designed to store and manage UI-related data in a lifecycle conscious way. Configuration changes such as screen rotations are handled properly by ViewModels.

Tight Coupling:

The issue of tight coupling is resolved since only the View holds the reference to ViewModel and not vice versa. A single View can hold references to multiple ViewModels.

Testability:

Since Presenters are hard bound to Views, writing unit tests becomes slightly difficult as there is a dependency of a View.

ViewModels are more unit test friendly as they can be independently tested. There is no dependency of the View.

Here, the implementation is being described with the example of About Event module in the Open Event Organizer App.

First step is the creation of a new class AboutEventViewModel which extends ViewModel.

@Binds
@IntoMap
@ViewModelKey(AboutEventViewModel.class)
public abstract ViewModel bindAboutEventViewModel(AboutEventViewModel aboutEventViewModel);

The new ViewModel has to be added to the ViewModelModule:

Constructor for the ViewModel:

@Inject
public AboutEventViewModel(EventRepository eventRepository,  CopyrightRepository copyrightRepository,
DatabaseChangeListener<Copyright> copyrightChangeListener) {
    this.eventRepository = eventRepository;
    this.copyrightRepository = copyrightRepository;
    this.copyrightChangeListener = copyrightChangeListener;

    eventId = ContextManager.getSelectedEvent().getId();
}

We are using Dagger2 for dependency injection. 

LiveData

LiveData is a lifecycle-aware data holder with the observer pattern.

When we have a LiveData object (e.g. list of attendees), we can add some LifecycleOwner (it can be Activity or Fragment) as an observer. Using this:

The Activity or Fragment will remain updated with the data changes.

Observers are only notified if they are in the STARTED or RESUMED state which is also known as the active state. This prevents memory leaks and NullPointerExceptions because inactive observers are not notified about changes.

Now, let’s discuss about the implementation of LiveData. We will create objects of SingleEventLiveData<> class.

private final SingleEventLiveData<Boolean> progress = new SingleEventLiveData<>();
private final SingleEventLiveData<String> error = new SingleEventLiveData<>();
private final SingleEventLiveData<Event> success = new SingleEventLiveData<>();
private final SingleEventLiveData<Copyright> showCopyright = new SingleEventLiveData<>();
private final SingleEventLiveData<Boolean> changeCopyrightMenuItem = new SingleEventLiveData<>();
private final SingleEventLiveData<String> showCopyrightDeleted = new SingleEventLiveData<>();

The functions to get the LiveData objects:

public LiveData<Boolean> getProgress() {
    return progress;
}

public LiveData<Event> getSuccess() {
    return success;
}

public LiveData<String> getError() {
    return error;
}

public LiveData<Copyright> getShowCopyright() {
    return showCopyright;
}

public LiveData<Boolean> getChangeCopyrightMenuItem() {
    return changeCopyrightMenuItem;
}

public LiveData<String> getShowCopyrightDeleted() {
    return showCopyrightDeleted;
}

Now, we can remove getView() methods and instead, these objects will be used to call various methods defined in the fragment.

Let’s discuss the changes required in the AboutEventFragment now.

The Fragment will have ViewModelProvider.Factory injected.

@Inject
ViewModelProvider.Factory viewModelFactory;

Declare an object of the ViewModel.

private AboutEventViewModel aboutEventViewModel;

Then, in onCreateView(), viewModelFactory will be passed to the ViewModelProviders.of() method as the factory, which is the second parameter.

aboutEventViewModel = ViewModelProviders.of(this, viewModelFactory).get(AboutEventViewModel.class);

Replace all references to the Presenter with references to the ViewModel.

Add the Fragment as an observer to the changes by adding the following in the onStart() method:

aboutEventViewModel.getProgress().observe(this, this::showProgress);
aboutEventViewModel.getSuccess().observe(this, this::showResult);
aboutEventViewModel.getError().observe(this, this::showError);
aboutEventViewModel.getShowCopyright().observe(this, this::showCopyright);
aboutEventViewModel.getChangeCopyrightMenuItem().observe(this, this::changeCopyrightMenuItem);
aboutEventViewModel.getShowCopyrightDeleted().observe(this, this::showCopyrightDeleted);

Two parameters are passed to the observe() method  –  first one is LifecycleOwner, which is our Fragment in this case. The second one is a callback along with a parameter and is used to call the required method.

With this, the implementation of MVVM and LiveData is brought to completion.

Resources:

Documentation: ViewModel, LiveData

Further reading:

Open Event Organizer App: Project repo, Play Store, F-Droid

Continue ReadingMigration to Model-View-ViewModel Architecture and LiveData in Open Event Organizer App

Transmitting data from SD card through Arduino – Neurolab record feature

In the Neurolab-Android app, we have been using an Arduino board for development as a workaround to the actual Neurolab hardware which is currently in the manufacturing stages. Along with the Arduino, we use a Micro SD card module with an SD card attached containing a dataset file in it. This combination of the Arduino and the SD card module serves as the source of dataflow into the Android app.

Firstly, we need to get the Arduino programmed for reading the dataset file from the SD card in the SD card module. We program the Arduino using the Arduino IDE on any desktop platform.

1. Import required libraries before starting off, namely the SPI and SD libraries for serial communication and SD card related functions respectively. Let us also set up some constant values which are going to be used in various parts of the code needed to program the Arduino for our required purpose.

#include <SD.h>
#include <SPI.h>
int baudRate = 9600;
int chipSelect = 4;
String fileName = "Dataset1.csv";

The ‘chipSelect’ variable denotes the chip select pin number for the connected Micro SD card adapter (module).

The baud rate for the Arduino board has been set to 9600 as a default. The dataset stored in the SD card from which the data needs to be read has been named “Dataset1”.

2. Next, we will need to initialize the SD card to check even if an SD card is inserted or not in the SD card module. We create a function for this purpose named ‘initializeSDCard’ in the following way:

bool initializeSDCard() {
  if (!SD.begin(chipSelect)) { 
    return false;   }
  return true;
}

The function will return true if initialization was successful. An unsuccessful initialization may also be due to the fact of SD card corrupted apart from not being properly inserted into the module.

3. Now we will be opening the dataset file from the SD card, making it ready to be read from.

File openTheFileFromSDCard(String fileName) {
  File file = SD.open(fileName);  
  if (!file) {
    Serial.println("error opening: " + fileName);
    return file;
  }
  return file;
}

The function will take in the file name as an argument and open it from the SD card. If the file is not found in the SD card, it will simply print out an error message to the serial output. It returns the file object in a true or false context accordingly.

4. We are now going to read data from the dataset in the SD card line by line using a function. The dataset file instance is passed as an argument to this function. This file is read and transmitted over the serial output channel line by line with ‘\n’ as the line delimiter, skipping null lines.

void readFromSDCardToSerialOutputLineByLine(File file) {
  String line;
  while (file.available()) {
    line = file.readStringUntil('\n');
    line.trim();
    if (line != "") {
      Serial.println(line);
    }
  }
  Serial.println("Recorded");
  file.close();
}

This function will keep reading from the file untill it has nothing more i.e till the end of file.

5. Next, we update the setup function which is the function to be executed for the programming of the Arduino board. Here, we call our defined functions according to our need logical need.

void setup() {
  Serial.begin(baudRate);

  pinMode(chipSelect, OUTPUT);

  if (initializeSDCard()) {
    File file = openTheFileFromSDCard(fileName);   
    if (file) {
      readFromSDCardToSerialOutputLineByLine(file);
    }
  }
}

Here, we initialize the serial communication at 9600 bits per second (baud rate), specify the pin mode which is the chip select pin in our case.

6. We leave the loop( ) function module as it is, as we do not need any iterative routine which runs over and over while programming our Arduino.

We are good to go now. Connect the Arduino to the desktop, compile the code and upload it to the circuit board.

Once programmed, we can get the output of the data transmission on the serial monitor in the following way:

Note – In the picture, the dataset name is ‘k24bit’.

Hope this blog adds value to your software development skills.

References:

  1. https://www.hackerearth.com/blog/developers/arduino-programming-for-beginners/
  2. https://youtu.be/sS_oW81NweI
  3. https://www.arduino.cc/en/main/software 

Tags: FOSSASIA, GSOC19, Arduino, Neurolab, Programming, Hardware

Continue ReadingTransmitting data from SD card through Arduino – Neurolab record feature

Designing and optimising new invoice PDFs

The Open Event project has proven to be an excellent event management application with a growing user base. With recent workflow refactors in the order process in open-event-frontend and introduction of event invoices (to be rolled out this month as a work product), the open-event-server’s invoices required a makeover. A ticket buyer is now required to give their billing information if the order is comprised of paid tickets and to accommodate this, and long information addresses, optimisation was required.

Restructuring order invoices

The new order invoices use nested tables concept instead of previously used two-cell tables. The pros of this new design is the accomodation of long-addresses and corresponding changes in billing information display.

{% if order.is_billing_enabled %}
                      <
td style="text-align:center;">
                          <
table>
                              <
tr>
                                  <
td>
                                      <
strong>Company :</strong>
                                  </
td>
                                  <
td>
                                      <
strong>{{ order.company }}</strong>
                                  </
td>
                              </
tr>
                              <
tr>
                                  <
td valign="top">
                                      <
strong>Tax Info :</strong>
                                  </
td>
                                  <
td>
                                      <
strong>{{ order.tax_business_info }}</strong>
                                  </
td>
                              </
tr>
                              <
tr>
                                  <
td valign="top">
                                      <
strong>Address :</strong>
                                  </
td>
                                  <
td>
                                      <
strong>{{ order.address }}</strong>
                                  </
td>
                              </
tr>
                              <
tr>
                                  <
td>
                                      <
strong>City :</strong>
                                  </
td>
                                  <
td>
                                      <
strong>{{ order.city }}</strong>
                                  </
td>
                              </
tr>
                              <
tr>
                                  <
td>
                                      <
strong>State :</strong>
                                  </
td>
                                  <
td>
                                      <
strong>{{ order.state }}</strong>
                                  </
td>
                              </
tr>
                              <
tr>
                                  <
td>
                                      <
strong>Zipcode :</strong>
                                  </
td>
                                  <
td>
                                      <
strong>{{ order.zipcode }}</strong>
                                  </
td>
                              </
tr>
                              <
tr>
                                  <
td>
                                      <
strong>Country :</strong>
                                  </
td>
                                  <
td>
                                      <
strong>{{ order.country }}</strong>
                                  </
td>
                              </
tr>
                          </
table>
                      </
td>
                      {% endif %}

This made sure that the new orders have enough space to prevent information overflow and still maintain the original structure to give a sense of uniformity in old and new PDFs.

Generating new event invoices

The new event invoices needed an overall change in structure. They will be rolling out on 1st of every month, according to current implementation. This required an overall implementation of new invoices. 

First, the published events are taken in consideration for generation of invoices for a particular user. It has been implemented as a scheduled job accordingly.

events = Event.query.filter_by(deleted_at=None, state='published').all()
      for event in events:
          # calculate net & gross revenues
          user = event.owner
          admin_info = get_settings()
          currency = event.payment_currency
          ticket_fee_object = db.session.query(TicketFees).filter_by(currency=currency).one()
          ticket_fee_percentage = ticket_fee_object.service_fee
          ticket_fee_maximum = ticket_fee_object.maximum_fee
          orders = Order.query.filter_by(event=event).all()
          gross_revenue = event.calc_monthly_revenue()
          ticket_fees = event.tickets_sold * (ticket_fee_percentage / 100)
          if ticket_fees > ticket_fee_maximum:
              ticket_fees = ticket_fee_maximum
          net_revenue = gross_revenue - ticket_fees
          payment_details = {
              'tickets_sold': event.tickets_sold,
              'gross_revenue': gross_revenue,
              'net_revenue': net_revenue,
              'amount_payable': ticket_fees
          }
          # save invoice as pdf
          pdf = create_save_pdf(render_template('pdf/event_invoice.html', orders=orders, user=user,
                                admin_info=admin_info, currency=currency, event=event,
                                ticket_fee_object=ticket_fee_object, payment_details=payment_details,
                                net_revenue=net_revenue), UPLOAD_PATHS['pdf']['event_invoice'],
                                dir_path='/static/uploads/pdf/event_invoices/', identifier=event.identifier)
          # save event_invoice info to DB

          event_invoice = EventInvoice(amount=net_revenue, invoice_pdf_url=pdf, event_id=event.id)
          save_to_db(event_invoice)

This function also required one minor modification. The function for calculating monthly revenue had to be updated as to dodge certain unseen bugs related to non completed order amount calculations hence restructuring the function as follows.

def calc_monthly_revenue(self):
      """Returns revenue of current month. Invoice sent every 1st of the month for the previous month"""
      previous_month = datetime.now().month - 1
      orders = Order.query.filter_by(event_id=self.id, status='completed').all()


      monthly_revenue = sum([o.amount for o in orders if o.completed_at and o.completed_at.month == previous_month])
      return monthly_revenue

This enabled the system to finally serve event invoice PDFs. One of whose examples are given above, With this, the open-event-server is finally able to serve event invoices accordingly which can be paid via PayPal to the Eventyay account.

Resources

Related Work and Code Repository

Continue ReadingDesigning and optimising new invoice PDFs

Tracking location on Android – using GPS to record data in Neurolab

In the Neurolab-Android app, we have a feature for recording data. It uses data incoming from the hardware device and stores it in a data table format with various parameters. Two of these parameters happened to be the latitude and longitude (location) of the user using the app. For that, we needed to implement a location tracking feature which can be used while recording the data in the data table.

Let’s start off with adding the required permission uses in the Android Manifest file.

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

These will be used to ask permissions from the user to access the devices’ internet and location/GPS.

Now, we will be making our Location Tracker class that will be used in tracking the location and getting the corresponding latitude and longitude. 

Firstly, we are going to define some variables – an array of manifest permissions for enabling GPS, some constant values for requesting specific permissions, a provider for the GPS provider.

private String[] mapPermissions = new String[]{
            Manifest.permission.ACCESS_FINE_LOCATION
    };
    public static final int GPS_PERMISSION = 103;
    private static final int UPDATE_INTERVAL_IN_MILLISECONDS = 400;
    private static final int MIN_DISTANCE_CHANGE_FOR_UPDATES = 1;
    private String provider = LocationManager.GPS_PROVIDER;

We also need to initialize and have a location manager ready to request location updates from time to time. Defining a locationManager need to get the system service for location. We define it in the following way:

LocationManager locationManager = (LocationManager)getContext().getSystemService(Context.LOCATION_SERVICE)

Next, we set up a location listener which listens to location changes of the device. We define that in the following way:

private LocationListener locationListener = new LocationListener() {
        @Override
        public void onLocationChanged(Location location) {
            bestLocation = location;
        }

Now, that we have our variables, permissions, location manager and listener set up, we can start capturing the location.

@SuppressLint("MissingPermission")
    public void startCaptureLocation() {
        if (PermissionUtils.checkRuntimePermissions(context, mapPermissions)) {
            locationManager.requestLocationUpdates(provider, UPDATE_INTERVAL_IN_MILLISECONDS, MIN_DISTANCE_CHANGE_FOR_UPDATES,
                    locationListener);
        } else {
            PermissionUtils.requestRuntimePermissions(context, mapPermissions, GPS_PERMISSION);
        }
    }

Firstly, we need to check for the runtime permissions required for our task. We achieve this with the function ‘checkRuntimePermissions’ which we have created in a utility class named ‘PermissionUtils’. The code of this utility class can be found here: PermissionUtils.java. It basically self checks individual permissions from the array passed in the arguments with the Android system.

We then use the location manager instance to request current location updates using the constants and the location listener we defined earlier.

So now, that we have started capturing the user device location, we can get the device Location object values (latitude and longitude). 

@SuppressLint("MissingPermission")
    public Location getDeviceLocation() {
        if (bestLocation == null) {
            if (PermissionUtils.checkRuntimePermissions(context, mapPermissions)) {
                locationManager.requestLocationUpdates(provider,
                        UPDATE_INTERVAL_IN_MILLISECONDS, MIN_DISTANCE_CHANGE_FOR_UPDATES,
                        locationListener);
                return locationManager.getLastKnownLocation(provider);
            } else {
                return defaultLocation();
            }
        } else {
            return bestLocation;
        }
    }

This method requests the location and returns a Location object. If there is an internet connection problem or the device location is disabled from the device settings, it returns a default location object. Here in the Neurolab project, we had set the default location latitude and longitude to 0.0.

Now we can use this location while creating the recorded file in our app directory which can be used after importing that recorded file in the app. We will be storing the location in two columns in the recorded file which is in a table format.

We write the latitude and longitude of the device in the file using a PrintWriter object and get the latitude and longitude of the device in the following way:

long latitude = locationTracker.getDeviceLocation().getLatitude()
long longitude = locationTracker.getDeviceLocation().getLongitude()

Then, using the PrintWriter object we can write the data into the file in the following way:

PrintWriter out = new PrintWriter(new BufferedWriter(new                        FileWriter(csvFile, true)));
out.write(data + "\n");

Here, the ‘data’ contains the latitude and longitude converted to String type.

That’s it! Now you can use the LocationTracker object to capture the location and get the current device location using it in your own app as well.

Hope this blog, adds value to your Android development skills.

References:

  1. https://developer.android.com/reference/android/location/LocationManager.html
  2. https://stackoverflow.com/a/43319075 
  3. https://youtu.be/Ak8uRvlpGS0 

Tags: FOSSASIA, Android, GPS, GSOC 19, Neurolab, Location

Continue ReadingTracking location on Android – using GPS to record data in Neurolab

Neurolab data transfer – Establishing serial communication between Arduino and Android

In the development process of the Neurolab Android, we needed an Arduino-Android connection for transfer of data from datasets which included String and float data type values. In this blog post, I will show you how to establish a serial communication channel between

Android and Arduino through USB cable through which we can transmit data bidirectionally.

Requirements

Hardware:

  1. Android Phone
  2. Arduino (theoretically from any type, but I’ll be using Arduino Uno)
  3. USB 2.0 Cable Type A/B (for Arduino)
  4. OTG Cable (On The Go)
  5. Normal USB Cable (for transferring the data from Android Studio to your phone)

Software:

  1. Android Studio
  2. Arduino IDE

Wiring and Setup

Wiring must be established in the following way:

                                                                                 Figure: Android-Arduino Setup

Working on the Android Side

We would be using the UsbSerial Library by felHR85 for establishing serial

communication.

1. Adding the dependency:

a) Add the following line of code to your app level build.gradle file.

implementation "com.github.felHR85:UsbSerial:$rootProject.usbSerialLibraryVersion"

Note: The ‘usbSerialLibraryVersion’ may change from time to time. Please keep your project with the latest library version. Updates can be found here.

b) Add jitpack to your project.build.gradle file.

allprojects {
   repositories {
       jcenter()
       maven { url "https://jitpack.io" }
   }
}

2. Working with Arduino:

We need to program the Arduino to send and receive data. We achieve that with the help of Arduino IDE as mentioned above. Verify, compile and upload a sketch to the Arduino for sending and receiving data. If you are a complete beginner in Arduino programming, there are example sketches for this same purpose. Load an example sketch from under the communication segment and choose the serial communication sketch. Here, we will be working with a simple sketch for the Arduino such that it simply echoes whatever it receives on the serial port. Here is sketch code:

// the setup routine runs once when you press reset:
void setup() {
  // initialize serial communication at 9600 bits per second:
  Serial.begin(9600);
}

// the loop routine runs over and over again forever:
void loop() {
  char incomingByte;
   // If there is a data stored in the serial receive buffer, read it and print it to the serial port as human-readable ASCII text.
  if(Serial.available()){  
    incomingByte = Serial.read();
    Serial.print(incomingByte);  
  }
}

Feel free to compile and upload it to your own Arduino.

2. Working with Android:

  1. Firstly, we need an USBManager instance initialized with the system service – ‘USB_SERVICE’. This needs to be done in an Activity (preferably main) so that this instance can be passed to the Communication Handler class, which we are going to create next.
  2. Now, we will be working with a class for handling the serial communications with our Android device and the Arduino board. We would pass the USBManager instance to this class wherein work will be done with that to find the USBDevice and USBDeviceConnection

Starting with, we need to search for any attached Arduino devices to the Android device. We create a method for this and use the ‘getDevicesList’ callback to achieve this in the following way:

public void searchForArduinoDevice(Context context) {
        HashMap usbDevices = usbManager.getDeviceList();

        if (!usbDevices.isEmpty()) {
            boolean keep = true;
            for (Object object : usbDevices.entrySet()) {
                Map.Entry<String, UsbDevice> entry = (Map.Entry<String, UsbDevice>) object;
                device = entry.getValue();

                int deviceVID = device.getVendorId();
                if (deviceVID == ARDUINO_DEVICE_ID) { //Arduino Vendor ID = 0x2341
                    PendingIntent pi = PendingIntent.getBroadcast(context, 0,
                            new Intent(ACTION_USB_PERMISSION), 0);
                    usbManager.requestPermission(device, pi);
                    keep = false;
                } else {
                    connection = null;
                    device = null;
                }
                if (!keep)
                    break;
            }
        }
    }

c. Now, in the Activity where we will be testing or intend to work with the serial connection, we check for the usb permission in a broadcast receiver which is registered in the onCreate method of the activity along with an Intent Filter. The Intent filter has the usp permission as an action added to it.

usbCommunicationHandler = USBCommunicationHandler.getInstance(this, NeuroLab.getUsbManager());

        deviceConnector = new DeviceConnector(NeuroLab.getUsbManager());

        IntentFilter intentFilter = new IntentFilter();
 
        intentFilter.addAction(ACTION_USB_PERMISSION);
        registerReceiver(broadcastReceiver, intentFilter);

d. In the onReceive callback of the broadcast receiver, if the usb permission is granted, we initialize the serial connection with a baud rate for our Arduino device. In this initialization method, we get the connection and serial port of the connected Arduino to the Android device with which we can work. The method is implemented in the following way:

public boolean initializeSerialConnection(int baudRate) {
        connection = usbManager.openDevice(device);
        serialPort = UsbSerialDevice.createUsbSerialDevice(device, connection);
        if (serialPort != null) {
            if (serialPort.open()) { 
                serialPort.setBaudRate(baudRate);
                serialPort.setDataBits(UsbSerialInterface.DATA_BITS_8);
                serialPort.setStopBits(UsbSerialInterface.STOP_BITS_1);
                serialPort.setParity(UsbSerialInterface.PARITY_NONE);
                serialPort.setFlowControl(UsbSerialInterface.FLOW_CONTROL_OFF);
            } else {
                Log.d("SERIAL", "PORT NOT OPEN");
                return false;
            }
        } else {
            Log.d("SERIAL", "PORT IS NULL");
            return false;
        }
        setSerialPort(serialPort);
        return true;
    }

e. Now, with this ‘serialPort’ we can read data from the Arduino in the UsbReadCallback callback from the USBSerialInterface. The data is read in the form of array of bytes. This read data can be used to carry out the various functionalities we want to achieve.

With the above steps we can establish a serial connection between Android and Arduino, transmit data from Arduino to Android device for processing.

The whole working source code of the Neurolab Android project can be found here:

https://github.com/fossasia/neurolab-android/

Thanks for taking the time to read this blog. Hope it was able to make some good contributions to your knowledge base.

References

  1. https://github.com/felHR85/UsbSerial
  2. https://www.arduino.cc/reference/en/language/functions/communication/serial/

Tags: FOSSASIA, Neurolab, GSOC19, Open-source, Arduino, Serial terminal

Continue ReadingNeurolab data transfer – Establishing serial communication between Arduino and Android

Workflow of Admin Translations downloads as ZIP

This blog post emphasizes on the feature of translation archiving and download routes. The Open Event Project, popularly known as Eventyay is an event management solution which provides a robust platform to manage events, schedules multi-track sessions and supports many other features.

Prior to the actual implementation of this feature, a blueprint had to be registered in the flask app which handles the archiving and downloads logic for the translations. In addition, as this is only specific for administrators, the route had to be secured with access control. The access is controlled with the help of the is_admin decorator which checks if the user attempting to access the route is an admin. 

from flask import send_file, Blueprint
import shutil
import uuid
import tempfile
import os
from app.api.helpers.permissions import is_admin

admin_blueprint = Blueprint('admin_blueprint', __name__, url_prefix='/v1/admin/content/translations/all')
temp_dir = tempfile.gettempdir()
translations_dir = 'app/translations'

@admin_blueprint.route('/', methods=['GET'])
@is_admin
def download_translations():
    """Admin Translations Downloads"""
    uuid_literal = uuid.uuid4()
    zip_file = "translations{}".format(uuid_literal)
    zip_file_ext = zip_file+'.zip'
    shutil.make_archive(zip_file, "zip", translations_dir)
    shutil.move(zip_file_ext, temp_dir)
    path_to_zip = os.path.join(temp_dir, zip_file_ext)
    from .helpers.tasks import delete_translations
    delete_translations.apply_async(kwargs={'zip_file_path': path_to_zip}, countdown=600)
    return send_file(path_to_zip,  mimetype='application/zip',
                     as_attachment=True,
                     attachment_filename='translations.zip')

                                                         Code Snippet – Translations archiving

We utilize the shutil library to create archives of the specified directory which is the translations directory here. The directory file path which is to be archived must point to the folder with sub-directories of various language translations. We append a unique name generated by the UUID(Universally Unique Identifier) to the generated translations zip file. Also observe that the translations archive is saved to the temp folder. This is because the zip archive needs to be deleted after a certain amount of time. This time window mustn’t be too bounded as there might be multiple admins who might want to access these translations at the same time. Therefore, it is saved in the temp folder for 10 mins.

import Controller from '@ember/controller';
import { action } from '@ember/object';

export default class extends Controller {
  isLoading = false;

  @action
  async translationsDownload() {
    this.set('isLoading', true);
    try {
      const result = this.loader.downloadFile('/admin/content/translations/all/');
      const anchor = document.createElement('a');
      anchor.style.display = 'none';
      anchor.href = URL.createObjectURL(new Blob([result], { type: 'octet/stream' }));
      anchor.download = 'Translations.zip';
      anchor.click();
      this.notify.success(this.l10n.t('Translations Zip generated successfully.'));
    } catch (e) {
      console.warn(e);
      this.notify.error(this.l10n.t('Unexpected error occurred.'));
    }
    this.set('isLoading', false);
  }
}

                                           Code Snippet – Frontend logic for anchor downloads

To make this work on all browsers, the frontend part handles the downloads via an action in the controller. The action translationsDownload is linked to the download button which uses the download method in the loader service to hit the specific route and fetch the resource intuitively. This is stored in a constant result.

After this, an HTML anchor is created explicitly and its attribute values are updated.

The href of the anchor is set to the result of the resource fetched using the loader service with a Blob.

 Then a click action is triggered to download this file. As the loader service returns a Promise, we use an async function as the action to resolve it. In cases of failures, the notify service raises the error related and the frontend triggers this notification.

Resources:

Related work and code repo:

Tags:

Eventyay, FOSSASIA, Flask, Ember.js, Open Event, API

Continue ReadingWorkflow of Admin Translations downloads as ZIP

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 ReadingGas sensor (MQ-135) support In PSLab Android application

Implementing Event Invoice Forms

This blog post elaborates on the recent addition of user billing form in Eventyay which is an open source event management solution which allows users to buy & sell tickets, organize events & promote their brand, developed by FOSSASIA. As this project moves forward with the implementation of event invoices coming up,. In the past few weeks, I have collaborated with fellow developers in planning the integration of event invoice payments and this is a necessary step for the same due to its involvement in order invoice templates. This implementation focuses on event invoices billing ( the calculated amount an event organiser has to pay to the platform for their event’s revenue ).

This form includes basic details like contact details, tax ID, billing location and additional information (if any). The following is a specimen of this form :

Tax Form Implementation

First step of this form creation is to employ the account/billing/payment-info route for serving the relevant model data to the frontend.

// app/routes/account/billing/payment-info.js
import Route from '@ember/routing/route';
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';

export default class extends Route.extend(AuthenticatedRouteMixin) {
titleToken() {
  return this.l10n.t('Payment Info');
}
}

Since the field additions have been done in the user schema in the server side, the corresponding changes have to made in the ember user model as well.

// app/models/user.js
/**
  * Billing Contact Information
  */

billingContactName    : attr('string'),
billingPhone          : attr('string'),
billingCountry        : attr('string'),
company               : attr('string'),
billingAddress        : attr('string'),
billingCity           : attr('string'),
billingZipCode        : attr('string'),
billingTaxInfo        : attr('string'),
billingAdditionalInfo : attr('string'),
billingState          : attr('string'),

This form has a speciality. Instead of using the current user information directly, it uses an intermediate object and employs manipulation in current user record only when the submit button is clicked. This has been implemented in the following way : 

// app/components/user-payment-info-form.js
export default class extends Component.extend(FormMixin) {
didInsertElement() {
  super.didInsertElement(...arguments);
  this.set('userBillingInfo', pick(this.authManager.currentUser, ['billingContactName', 'billingCity', 'billingPhone', 'company', 'billingTaxInfo', 'billingCountry', 'billingState', 'billingAddress', 'billingZipCode', 'billingAdditionalInfo']));
}
@action
submit() {
  this.onValid(async() => {
    this.set('isLoading', true);
    try {
      this.authManager.currentUser.setProperties(this.userBillingInfo);
      await this.authManager.currentUser.save();
      this.notify.success(this.l10n.t('Your billing details has been updated'));
    } catch (error) {
      this.authManager.currentUser.rollbackAttributes();
      this.notify.error(this.l10n.t('An unexpected error occurred'));
    }
    this.set('isLoading', false);
  });
}
}

The usual form validations are employed as expected in this one too and works well in storing the invoice based information.

Resources

Related Work and Code Repository

Continue ReadingImplementing Event Invoice Forms

Play Your Favourite Music Files on your SUSI Smart Speaker

The SUSI smart speaker supports playing local music from any USB device connected to the smart speaker. To play your favourite music directly from files, just put them in a thumb drive and plug it into any one of the four USB ports on the smart speaker. SUSI can either play all songs from the USB device or songs from a specific artist, genre or album.

Working

The first thing that needs to be done is to automount the thumb drive in the smart speaker, for this the usbmount package is used. Further, after the mount is done local skills are created which are then used by the SUSI server to interpret voice commands related to offline music playback.

Breakdown

The code which enables the above functionality is : 

  1. Add dependency – https://github.com/fossasia/susi_installer/blob/1a2950b0eb1f88d4ecbd5b3c348d9b67ac2f4705/install.sh#L287
# usbmount is needed to automount usb drives on susibian(raspbian lite)
if [ $targetSystem = raspi ] ; then
DEBDEPS=”$DEBDEPS hostapd dnsmasq usbmount”
fi


USB mount is added to the dependency list if the installer is running on a Raspberry

  1. Enable offline skills – https://github.com/fossasia/susi_installer/blob/1a2950b0eb1f88d4ecbd5b3c348d9b67ac2f4705/install.sh#L881
mkdir -p $WORKDIR/susi_server_data/generic_skills/media_discovery
touch $WORKDIR/susi_server_data/generic_skills/media_discovery/custom_skill.txt
mkdir -p $WORKDIR/susi_server_data/settings
echo “local.mode = true” > $WORKDIR/susi_server_data/settings/customized_config.properties

Enable the server to work with offline skills stored on the device. The new skills related to offline music playback are stored in /susi_server_data/generic_skills/media_discovery in a file named custom_skill.txt.

  1. Creating on the fly skills – https://github.com/fossasia/susi_installer/blob/1a2950b0eb1f88d4ecbd5b3c348d9b67ac2f4705/install.sh#L758
echo “Preparing USB automount”
# systemd-udevd creates its own filesystem namespace, so mount is done, but it is not visible in the principal namespace.
sudo mkdir /etc/systemd/system/systemd-udevd.service.d/
echo -e “[Service]\nPrivateMounts=no” | sudo tee /etc/systemd/system/systemd-udevd.service.d/udev-service-override.conf

# readonly mount for external USB drives
sudo sed -i -e ‘/^MOUNTOPTIONS/ s/sync/ro/’ /etc/usbmount/usbmount.conf
sudo cp $INSTALLERDIR/raspi/media_daemon/01_create_skill /etc/usbmount/mount.d/
sudo cp $INSTALLERDIR/raspi/media_daemon/01_remove_auto_skill /etc/usbmount/umount.d/


First an override rule is added, which changes `PrivateMounts` rule’s value to `no` in /lib/systemd/system/systemd-udevd.service.  PrivateMounts if set to yes, the processes of this unit will be run in their own private file system (mount) namespace with all mount propagation from the processes towards the host’s main file system namespace turned off. This means any file system mount points established or removed by the unit’s processes will be private to them and not be visible to the host. To learn more about mount namespaces read –
http://man7.org/linux/man-pages/man7/mount_namespaces.7.html 

Next, whenever a device is mounted the 01_create_skill file is executed which contains the following instruction:

python3 /home/pi/SUSI.AI/susi_installer/raspi/media_daemon/auto_skills.py “$UM_MOUNTPOINT”

This calls the auto_skills.py file with the mount point of the storage device.

The auto_skills.py file is used to generate audio skills for the USB drive. It scans for all the files in the USB thumb drive and creates relevant skills.

Whenever the thumb drive is removed it calls out the 01_remove_auto_skill script which has the following instruction –


echo -n > /home/pi/SUSI.AI/susi_server_data/generic_skills/media_discovery/custom_skill.txt


This cleans out the custom_skills.txt file i.e. all the offline skills that were created for music playback are removed and the server no longer responds those skills.

USAGE

Play all music on the USB device

Usage: SUSI, Play Audio

This will play all audio from the USB device connected to the speaker. SUSI Smart speaker currently supports the following audio formats:

  • MP3
  • FLAC
  • OGG
  • WAV

Play All Songs From an Artist

Usage : SUSI, play <artist_name> from USB

Example : SUSI, play Linkin Park from USB

This will play and queue all songs from the given artist if found on the USB device.

Play a Specific Music Genre

Usage : SUSI, play <Genre> from USB

Example : SUSI, play Hard Rock from USB

This will play and queue songs from the USB device that matches the given genre.

Play an Album

Usage : SUSI, play <album_name> from USB

Example : SUSI, play Hybrid Thoery from USB

This will play and queue songs from a specific Album Name.

Note: The above three skills depend on the metadata of the file. The file should have relevant metadata for these skills to work.

Playback Control

Usage : SUSI, <control_keyword>

Example : SUSI, pause or SUSI, resume

Available Music Playback Control keywords

  • Pause : Pause the currently playing music
  • Resume : Resume the currently playing music if paused
  • Restart : Restart the currently playing Music
  • Next : Go to the next song in the current playlist
  • Previous : Plays the previous song in the current playlist
  • Shuffle : Shuffles all songs in the current playlist and play again

Note : Playlist is made for offline Music skills such as play audio or play album from USB.

Tags

SUSI Smart Speaker, SUSI.AI, FOSSASIA, GSoC19

Resources

Continue ReadingPlay Your Favourite Music Files on your SUSI Smart Speaker

Control Your Susi Smart Speaker

The SUSI Smart Speaker is an AI assistant device which runs SUS.AI. To learn to set up your own smart speaker, head up to SUSI Installer. One of the new features of the smart speaker is the ability to control it via a webpage, the smarts speaker now allows the user to control various playback features such play/pause music directly via their mobile phones or laptops which are in the same network. The web page is served via the sound server running locally on the Raspberry Pi. The soundserver provides various methods of the vlcplayer as endpoints. The webpage uses these endpoints to control the smart speaker. Also, an external application such as an android/ios app can use these endpoints(or the webpage) to control the music playback on the device.

Making the Front-end

The front end is served via the flask server on ‘ / ’ endpoint and on the port 7070. Currently, the Front End contains the volume control slider and various buttons to control the audio playback of the device. The responses are sent to the server via javascript. Bootstrap is used for the CSS framework and Fontawesome is used for various icon support. Since the smart speaker should be able to run offline, CDN links for Bootstrap and Fontawesome are not used and the required files are served via the flask server on /static. 

Adding required frameworks:

    <link href=”{{ url_for(‘static’, filename=’bootstrap.min.css’) }}” rel=”stylesheet”>
    <script type=”text/javascript” src=”{{
      url_for(‘static’,filename=’fontawesome.min.js’)
    }}”></script>

Web Page front-end

<div class=”form-signin”>
      <img class=”mb-4″ src=”{{ url_for(‘static’,       filename=’SUSI.AI_Icon_2017a.svg’) }}” alt=”” width=”256″       height=”256″>      {SUSI.AI Icon}
      <h1 class=”h3 mb-3 font-weight-normal”>Smart Speaker Control</h1>
        <div class=”form-group”>
          <fieldset class=”the-fieldset”>
              <legend class=”text-left w-auto”>Volume Control</legend>
              <span class=”font-weight-bold”>0</span>
              <i class=”fas fa-volume-down”></i>
              <input id=”vol-control” class=”slider” type=”range” min=”0″                  max=”100″ value=”100″ step=”1″ oninput=”SetVolume(this.value)”               onchange=”SetVolume(this.value)”></input>              {Volume Control Slider}
              <i class=”fas fa-volume-up”></i>
              <span class=”font-weight-bold”>100</span>
          </fieldset>
          <fieldset class=”the-fieldset”>
              <legend class=”text-left w-auto”>Playback Control</legend>
          <button onclick=”control(‘pause’)”                   class=”btn btn-outline-primary m-2″>
          Pause
          <i class=”fas fa-pause”></i>
          </button>                  {pause control button}
            {similar to the pause button other required buttons are added}
          </fieldset>
          <button onclick=”window.location.href = ‘/set_password’;”            class=”btn btn-warning m-2″>
            Set or Change Password
            <i class=”fa fa-key”></i>
          </button>
        </div>
    </div>

Sending Response to Server

Since this is a control webpage, on sending of a response, the webpage should not reload. To accomplish this all the buttons point to a javascript function which then sends out an HTTP POST request to the server. For this purpose XMLHttpRequest Object is used. The XMLHttpRequest object is used to exchange data with a web server behind the scenes. 


Here the SetVolume function is used to send a request to the /volume endpoint which is used to control the volume of the device. The control function is used to send a post request to audio control endpoints such as /pause /stop /shuffle etc.

      function control(action){
        console.log(action)
        var http = new XMLHttpRequest();
        var url = ‘/’+action;
        http.open(‘POST’, url, true);
        http.send();
      }
      function SetVolume(val){
        console.log(val)
        var http = new XMLHttpRequest();
        var url = ‘/volume?val=’+val;
        http.open(‘POST’, url, true);
        http.send();
      }

Features

The endpoints on the server provide the different audio control features via the vlc player. The endpoints used are listed below –

Play

The play functionality currently is only used directly via the busy state. It currently supports playback via youtube URL or MRLs. 

To play using an MRL(Media Resource Locator) the request URL should have an argument called MRL with the needed MRL value. This also supports multiple semicolon ‘ ; ‘ separated MRLs in a single request. 

Example Request URL: http://127.0.0.1:7070/play?mrl=/home/user/Desktop/song1.mp3;/home/user/Desktop/song2.mp3

@app.route(‘/play’, methods=[‘POST’, ‘PUT’])
def play_route():
    if ‘ytb’ in request.args:
        vlcplayer.playytb(request.args.get(‘ytb’))
        return do_return(‘Ok’, 200)
    elif ‘mrl’ in request.args:
        vlcplayer.play(request.args.get(‘mrl’))
        return do_return(‘Ok’, 200)
    else:
        return do_return(‘Unknown play mode’, 400)

Stop, Next, Previous, Pause and Resume

Stop, next and previous use the inbuilt methods of the MediaListPlayer class and are implemented in the same way. The request type must be POST and the request URL doesn’t require any arguments.

@app.route(‘/stop’, methods=[‘POST’, ‘PUT’])
def stop_route():
    vlcplayer.stop()
    return do_return(‘Ok’, 200)

Pause and Resume are also implemented in the same way but both of these use the same method pause of the MediaListPlayer class as that method acts as a toggle.

Shuffle

The shuffle endpoint shuffles the currently playing song list. It uses the shuffle method of the random library to shuffle the list containing MRLs of all the songs and then initiates a new MediaListPlayer object for playback. The Request URL doesn’t need any arguments.

    def shuffle(self):
        if self.is_playing():
            self.list_player.stop()
            random.shuffle(self.mrl)
            media_list = self.instance.media_list_new(self.mrl)
            self.list_player.set_media_list(media_list)
            self.list_player.play()
            self.softvolume(100, self.player)

Restart

The restart endpoint is used to restart the currently playing audio. It does so by going back in the playlist and playing the current audio again. The Request URL doesn’t need any arguments.

@app.route(‘/restart’, methods=[‘POST’, ‘PUT’])
def restart_route():
    vlcplayer.restart()
    return do_return(‘Ok’, 200)

Volume

The Volume endpoint is used to set the volume of the device, The volume control slider uses this endpoint. A single argument val is needed in the URL of the POST request. Val can have a value ranging from 0 to 100, where 0 means mute and 100 means full volume.

@app.route(‘/volume’, methods=[‘POST’, ‘PUT’])
def volume_route():
    try:
        vlcplayer.volume(request.args.get(‘val’))
        return do_return(‘Ok’, 200)
    except Exception as e:
        logger.error(e)
        return do_return(‘Volume adjustment error’ + e, 400)

Resources

Javascript XML HTTP Requests – https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest

Flask HTML Templates –
https://pythonhow.com/html-templates-in-flask/

Tags

SUSI Smart Speaker, SUSI.AI, FOSSASIA, GSoC19

Continue ReadingControl Your Susi Smart Speaker