Control Garage Door Openers using Android – Tinkerforge

- Getting Started
  - Primer
  - Tutorials
  - FAQ
- Hardware
  - Bricks
  - Bricklets
  - Master Extensions
  - Power Supplies
  - Accessories
  - Timeline
- Software
  - Brick Daemon (brickd)
  - Brick Viewer (brickv)
  - API Bindings
  - Firmwares and Plugins
  - Device Identifier
  - Source Code and Bug Tracking
  - Programming Interface
- Starter Kits
  - Weather Station
  - Hardware Hacking
    - Features
    - Description
    - Technical Specifications
    - Resources
    - Required Tools
    - Firmware updating and first tests
    - How it works
    - Hardware Hacking for Beginners
    - Examples
      - Control Mains Switches Remotely
      - Read out Smoke Detectors
      - Control Garage Doors Remotely
        - Hardware Setup
        - Using Android
          - Goals
          - Step 1: Creating the GUI
          - Step 2: Discover Bricks and Bricklets
          - Step 3: Triggering Switches
          - Step 4: More GUI logic
          - Step 5: Error Handling and Reporting
          - Step 6: Persistent Configuration and State
          - Step 7: Everything put together
        - Using Windows Phone
        - Using iOS
      - Is someone at the Door? - Doorbell Notifier
  - Server Room Monitoring
  - Blinkenlights
  - Internet of Things
  - Camera Slider
- Embedded Boards
  - Raspberry Pi
- Specifications
  - Technical Data
  - TCP/IP Protocol
  - Modbus Protocol
  - Wireshark Dissector

# Control Garage Door Openers using Android

For this project we are assuming, that you have the Android SDK set up and that you have a rudimentary understanding of the Java language.

If you are totally new to Java itself you should start here. If you are new to the Tinkerforge API, you should start here.

We are also assuming that you have a remote control connected to an Industrial Quad Relay Bricklet as described here.

The complete Eclipse project can be downloaded here. A demo app based on this project is available in the Google Play Store.

## Goals

In this project we will create a simple Android app that resembles the functionality of the actual remote control.

## Step 1: Creating the GUI

After creating a new "Android Application Project" named "Garage Control" in Eclipse we start with creating the GUI:

The basis is a "Linear Layout (Vertical)". Three text fields allow to enter the host, port and UID of the Industrial Quad Relay Bricklet. For the port a "Number" text field is used, so Android will restrict the content of this text field to numbers. The final two elements are one "Button" to connect and disconnect and another one to trigger the remote control.

Now the GUI layout is finished. To access the GUI components in the Java code we use the `findViewById()` method and store the references to member variables of the `MainActivity` class:

    public class MainActivity extends Activity {
        private EditText host;
        private EditText port;
        private EditText uid;
        private Button connect;
        private Button trigger;

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);

            host = (EditText)findViewById(R.id.host);
            port = (EditText)findViewById(R.id.port);
            uid = (EditText)findViewById(R.id.uid);
            connect = (Button)findViewById(R.id.connect);
            trigger = (Button)findViewById(R.id.trigger);
        }
    }

## Step 2: Discover Bricks and Bricklets

This step is similar to step 1 in the Read out Smoke Detectors using Java project. We apply some changes to make it work in a GUI program and instead of using the `EnumerateListener` to discover the Industrial Quad Relay Bricklet its UID has to be specified. This approach allows to pick the correct Industrial Quad Relay Bricklet even if multiple are connected to the same host at once.

We don't want to call the `connect()` method directly, because it might take a moment and block the GUI during that period of time. Instead `connect()` will be called from an `AsyncTask`, so it will run in the background and the GUI stays responsive:

    public class MainActivity extends Activity {
        // [...]

        private IPConnection ipcon;
        private BrickletIndustrialQuadRelay relay;

        class ConnectAsyncTask extends AsyncTask<Void, Void, Void> {
            private String currentHost;
            private String currentPort;
            private String currentUID;

            @Override
            protected void onPreExecute() {
                currentHost = host.getText().toString();
                currentPort = port.getText().toString();
                currentUID = uid.getText().toString();
            }

            protected Void doInBackground(Void... params) {
                ipcon = new IPConnection();
                relay = new BrickletIndustrialQuadRelay(currentUID, ipcon);

                ipcon.connect(currentHost, Integer.parseInt(currentPort));

                return null;
            }
        }
    }

The `ConnectAsyncTask` is implemented as an inner class. This allows for direct access to the member variables of the `MainActivity` class.

The `onPreExecute()` method is called before `doInBackground()` and stores the host, port and UID configuration from the GUI elements. This is necessary, because the `doInBackground()` method needs this information, but is not allowed to access the GUI elements. Now the `doInBackground()` method can create an `IPConnection` and `BrickletIndustrialQuadRelay` object and call the `connect()` method.

Finally, a `ConnectAsyncTask` should be created and executed when the connect button is clicked. A `OnClickListener` added to the connect button does this:

    public class MainActivity extends Activity {
        // [...]

        class ConnectClickListener implements OnClickListener {
            public void onClick(View v) {
                new ConnectAsyncTask().execute();
            }
        }

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            // [...]

            connect.setOnClickListener(new ConnectClickListener());
        }
    }

Host, port and UID can now be configured and a click on the connect button establishes the connection.

## Step 3: Triggering Switches

The connection is established and the Industrial Quad Relay Bricklet is found but there is no logic yet to trigger the switch on the remote control if the trigger button is clicked.

An `OnClickListener` is added to the trigger button that creates and executes an `AsyncTask` that in turn calls the `setMonoflop()` method of the Industrial Quad Relay Bricklet to trigger the switch on the remote control:

    public class MainActivity extends Activity {
        // [...]

        private BrickletIndustrialQuadRelay relay;

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            // [...]

            trigger.setOnClickListener(new TriggerClickListener());
        }

        class TriggerAsyncTask extends AsyncTask<Void, Void, Void> {
            protected Void doInBackground(Void... params) {
                relay.setMonoflop(1 << 0, 15, 1500);
                return null;
            }
        }

        class TriggerClickListener implements OnClickListener {
            public void onClick(View v) {
                new TriggerAsyncTask().execute();
            }
        }
    }

The call to `setMonoflop(1 << 0, 15, 1500)` closes the first relay for 1.5s then opens it again.

That's it. If we would copy these three steps together in one project, we would have a working app that allows a smart phone to control a garage door opener using its hacked remote control!

We don't have a disconnect button yet and the trigger button can be clicked before the connection is established. We need some more GUI logic!

## Step 4: More GUI logic

There is no button to close the connection again after it got established. The connect button could do this. When the connection is established it should allow to disconnect it again:

    class ConnectAsyncTask extends AsyncTask<Void, Void, Void> {
        // [...]

        @Override
        protected void onPostExecute(Void result) {
            connect.setText("Disconnect");
            connect.setOnClickListener(new DisconnectClickListener());
        }
    }

The `onPostExecute()` method is called after `doInBackground()`. It changes the text on the button and sets a new `OnClickListener` that will closes the connection if the button is clicked:

    class DisconnectClickListener implements OnClickListener {
        public void onClick(View v) {
            new DisconnectAsyncTask().execute();
        }
    }

We don't want to call the `disconnect()` method directly, because it might take a moment and block the GUI during that period of time. Instead `disconnect()` will be called from an `AsyncTask`, so it will run in the background and the GUI stays responsive:

    class DisconnectAsyncTask extends AsyncTask<Void, Void, Void> {
        protected Void doInBackground(Void... params) {
            ipcon.disconnect();
            return null;
        }

        @Override
        protected void onPostExecute(Void result) {
            connect.setText("Connect");
            connect.setOnClickListener(new ConnectClickListener());
        }
    }

Once the connection is closed a new `OnClickListener` is set that will execute `ConnectAsyncTask` to establish the connection if the connect button is clicked:

    class ConnectClickListener implements OnClickListener {
        public void onClick(View v) {
            new ConnectAsyncTask().execute();
        }
    }

Finally, the user should not be able to change the content of the text fields during the time the connection gets established and the trigger button should not be clickable if there is no connection.

The `ConnectAsyncTask` and the `DisconnectAsyncTask` are extended to disable and enable the GUI elements according to the current connection state:

    class ConnectAsyncTask extends AsyncTask<Void, Void, Void> {
        // [...]

        @Override
        protected void onPreExecute() {
            // [...]

            host.setEnabled(false);
            port.setEnabled(false);
            uid.setEnabled(false);
            connect.setEnabled(false);
            trigger.setEnabled(false);
        }

        // [...]

        @Override
        protected void onPostExecute(Void result) {
            // [...]

            connect.setEnabled(true);
            trigger.setEnabled(true);
        }
    }

    class DisconnectAsyncTask extends AsyncTask<Void, Void, Void> {
        // [...]

        @Override
        protected void onPreExecute() {
            connect.setEnabled(false);
            trigger.setEnabled(false);
        }

        // [...]

        @Override
        protected void onPostExecute(Void result) {
            host.setEnabled(true);
            port.setEnabled(true);
            uid.setEnabled(true);
            connect.setEnabled(true);

            // [...]
        }
    }

But the program is not yet robust enough. What happens if can't connect? What happens if there is no Industrial Quad Relay Bricklet with the given UID?

What we need is error handling!

## Step 5: Error Handling and Reporting

We will use similar principals as in step 4 of the Read out Smoke Detectors using Java project, but with some changes to make it work in a GUI program.

We can't just use `System.out.println()` for error reporting because there is no console window in an app. Instead dialog boxes are used.

The `ConnectAsyncTask` has to validate the user input before using it. An `AlertDialog` is used to report possible problems:

    class ConnectAsyncTask extends AsyncTask<Void, Void, ConnectResult> {
        // [...]

        @Override
        protected void onPreExecute() {
            currentHost = host.getText().toString();
            currentPort = port.getText().toString();
            currentUID = uid.getText().toString();

            if (currentHost.length() == 0 || currentPort.length() == 0 || currentUID.length() == 0) {
                AlertDialog.Builder builder = new AlertDialog.Builder(context);
                builder.setMessage("Host/Port/UID cannot be empty");
                builder.create().show();
                cancel(true);
                return;
            }
        }

        // [...]
    }

The `ConnectAsyncTask` also gains a `ProgressDialog` to show the connection process:

    class ConnectAsyncTask extends AsyncTask<Void, Void, ConnectResult> {
        // [...]
        private ProgressDialog progressDialog;

        @Override
        protected void onPreExecute() {
            // [...]

            progressDialog = new ProgressDialog(context);
            progressDialog.setMessage("Connecting to " + currentHost + ":" + currentPort);
            progressDialog.setCancelable(false);
            progressDialog.show();
        }

        // [...]
    }

Then the `doInBackground()` method needs to be able to report its result. But it is not allowed to interact with the GUI, but it can return a value that is then passed to the `onPostExecute()` method. We use this `enum` that represents the three possible outcomes of a connection attempt:

    enum ConnectResult {
        SUCCESS,
        NO_CONNECTION,
        NO_DEVICE
    }

- SUCCESS: The connection got established and an Industrial Quad Relay Bricklet with the given UID was found.
- NO_CONNECTION: The connection could not be established.
- NO_DEVICE: The connection got established but there was no Industrial Quad Relay Bricklet with the given UID.

The `getIdentity()` method is used to check that the device for the given UID really is an Industrial Quad Relay Bricklet. If this is not the case then the connection gets closed:

    protected ConnectResult doInBackground(Void... params) {
        ipcon = new IPConnection();

        try {
            relay = new BrickletIndustrialQuadRelay(currentUID, ipcon);
        } catch(IllegalArgumentException e) {
            return ConnectResult.NO_DEVICE;
        }

        try {
            ipcon.connect(currentHost, currentPort);
        } catch(java.net.UnknownHostException e) {
            return ConnectResult.NO_CONNECTION;
        } catch(IllegalArgumentException e) {
            return ConnectResult.NO_CONNECTION;
        } catch(java.io.IOException e) {
            return ConnectResult.NO_CONNECTION;
        } catch(com.tinkerforge.AlreadyConnectedException e) {
            return ConnectResult.NO_CONNECTION;
        }

        try {
            if (relay.getIdentity().deviceIdentifier != BrickletIndustrialQuadRelay.DEVICE_IDENTIFIER) {
                ipcon.disconnect();
                return ConnectResult.NO_DEVICE;
            }
        } catch (com.tinkerforge.TinkerforgeException e1) {
            try {
                ipcon.disconnect();
            } catch (com.tinkerforge.NotConnectedException e2) {
            }

            return ConnectResult.NO_DEVICE;
        }

        return ConnectResult.SUCCESS;
    }

Now the `onPostExecute()` method has to handle this three outcomes. First the progress dialog is dismissed:

    @Override
    protected void onPostExecute(ConnectResult result) {
        progressDialog.dismiss();

In case the connection attempt was successful the original logic stays the same:

    if (result == ConnectResult.SUCCESS) {
        connect.setText("Disconnect");
        connect.setOnClickListener(new DisconnectClickListener());
        connect.setEnabled(true);
        trigger.setEnabled(true);
    }

In the error case we use an `AlertDialog` and set the error message according to the connection result:

    else {
        AlertDialog.Builder builder = new AlertDialog.Builder(context);

        if (result == ConnectResult.NO_CONNECTION) {
            builder.setMessage("Could not connect to " + currentHost + ":" + currentPort);
        } else { // ConnectResult.NO_DEVICE
            builder.setMessage("Could not find Industrial Quad Relay Bricklet [" + currentUID + "]");
        }

        builder.setCancelable(false);

Then retry and cancel buttons are added with the respective logic to either retry to connect or to cancel the connection attempt:

    builder.setPositiveButton("Retry", new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            dialog.dismiss();
            new ConnectAsyncTask().execute();
        }
    });
    builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            host.setEnabled(true);
            port.setEnabled(true);
            uid.setEnabled(true);
            connect.setText("Connect");
            connect.setOnClickListener(new ConnectClickListener());
            connect.setEnabled(true);
            dialog.dismiss();
        }
    });

Finally, the dialog gets created and shown:

            builder.create().show();
        }
    }

Now the app can connect to an configurable host and port and trigger a button on the remote control of your garage door opener using an Industrial Quad Relay Bricklet.

## Step 6: Persistent Configuration and State

The app doesn't store its configuration yet. Android provides the `SharedPreferences` class to take care of this. In `onCreate()` the configuration is restored:

    protected void onCreate(Bundle savedInstanceState) {
        // [...]

        SharedPreferences settings = getPreferences(0);
        host.setText(settings.getString("host", host.getText().toString()));
        port.setText(settings.getString("port", port.getText().toString()));
        uid.setText(settings.getString("uid", uid.getText().toString()));
    }

In `onStop()` the configuration is then stored again:

    protected void onStop() {
        super.onStop();

        SharedPreferences settings = getPreferences(0);
        SharedPreferences.Editor editor = settings.edit();

        editor.putString("host", host.getText().toString());
        editor.putString("port", port.getText().toString());
        editor.putString("uid", uid.getText().toString());
        editor.commit();
    }

If the orientation is changed Android basically restarts the app for the new orientation. This makes our app loss the connection. Therefore, the state of the connection is stored when `onSaveInstanceState()` is called:

    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);

        outState.putBoolean("connected", ipcon != null &&
                                         ipcon.getConnectionState() == IPConnection.CONNECTION_STATE_CONNECTED);
    }

And is restored when `onCreate()` is called:

    protected void onCreate(Bundle savedInstanceState) {
        // [...]

        if (savedInstanceState != null && savedInstanceState.getBoolean("connected", false)) {
            new ConnectAsyncTask().execute();
        }
    }

Now the configuration and state is stored persistent across a restart of the app.

## Step 7: Everything put together

That's it! We are done with the app for our hacked garage door opener remote control.

Now all of the above put together (Download):

    package com.tinkerforge.garagecontrol;

    import android.os.Bundle;
    import android.app.Activity;
    import android.app.AlertDialog;
    import android.app.ProgressDialog;
    import android.content.Context;
    import android.content.DialogInterface;
    import android.content.SharedPreferences;
    import android.os.AsyncTask;
    import android.view.View;
    import android.view.View.OnClickListener;
    import android.widget.Button;
    import android.widget.EditText;

    import com.tinkerforge.BrickletIndustrialQuadRelay;
    import com.tinkerforge.IPConnection;
    import com.tinkerforge.TinkerforgeException;
    import com.tinkerforge.NotConnectedException;
    import com.tinkerforge.AlreadyConnectedException;

    public class MainActivity extends Activity {
        final Context context = this;
        private IPConnection ipcon = null;
        private BrickletIndustrialQuadRelay relay = null;
        private EditText host;
        private EditText port;
        private EditText uid;
        private Button connect;
        private Button trigger;

        enum ConnectResult {
            SUCCESS,
            NO_CONNECTION,
            NO_DEVICE
        }

        class ConnectAsyncTask extends AsyncTask<Void, Void, ConnectResult> {
            private ProgressDialog progressDialog;
            private String currentHost;
            private String currentPort;
            private String currentUID;

            @Override
            protected void onPreExecute() {
                currentHost = host.getText().toString();
                currentPort = port.getText().toString();
                currentUID = uid.getText().toString();

                if (currentHost.length() == 0 || currentPort.length() == 0 || currentUID.length() == 0) {
                    AlertDialog.Builder builder = new AlertDialog.Builder(context);
                    builder.setMessage("Host/Port/UID cannot be empty");
                    builder.create().show();
                    cancel(true);
                    return;
                }

                host.setEnabled(false);
                port.setEnabled(false);
                uid.setEnabled(false);
                connect.setEnabled(false);
                trigger.setEnabled(false);

                progressDialog = new ProgressDialog(context);
                progressDialog.setMessage("Connecting to " + currentHost + ":" + currentPort);
                progressDialog.setCancelable(false);
                progressDialog.show();
            }

            protected ConnectResult doInBackground(Void... params) {
                ipcon = new IPConnection();

                try {
                    relay = new BrickletIndustrialQuadRelay(currentUID, ipcon);
                } catch(IllegalArgumentException e) {
                    return ConnectResult.NO_DEVICE;
                }

                try {
                    ipcon.connect(currentHost, Integer.parseInt(currentPort));
                } catch(java.net.UnknownHostException e) {
                    return ConnectResult.NO_CONNECTION;
                } catch(IllegalArgumentException e) {
                    return ConnectResult.NO_CONNECTION;
                } catch(java.io.IOException e) {
                    return ConnectResult.NO_CONNECTION;
                } catch(AlreadyConnectedException e) {
                    return ConnectResult.NO_CONNECTION;
                }

                try {
                    if (relay.getIdentity().deviceIdentifier != BrickletIndustrialQuadRelay.DEVICE_IDENTIFIER) {
                        ipcon.disconnect();
                        return ConnectResult.NO_DEVICE;
                    }
                } catch (TinkerforgeException e1) {
                    try {
                        ipcon.disconnect();
                    } catch (NotConnectedException e2) {
                    }

                    return ConnectResult.NO_DEVICE;
                }

                return ConnectResult.SUCCESS;
            }

            @Override
            protected void onPostExecute(ConnectResult result) {
                progressDialog.dismiss();

                if (result == ConnectResult.SUCCESS) {
                    connect.setText("Disconnect");
                    connect.setOnClickListener(new DisconnectClickListener());
                    connect.setEnabled(true);
                    trigger.setEnabled(true);
                } else {
                    AlertDialog.Builder builder = new AlertDialog.Builder(context);

                    if (result == ConnectResult.NO_CONNECTION) {
                        builder.setMessage("Could not connect to " + currentHost + ":" + currentPort);
                    } else { // ConnectResult.NO_DEVICE
                        builder.setMessage("Could not find Industrial Quad Relay Bricklet [" + currentUID + "]");
                    }

                    builder.setCancelable(false);
                    builder.setPositiveButton("Retry", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            dialog.dismiss();
                            new ConnectAsyncTask().execute();
                        }
                    });
                    builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            host.setEnabled(true);
                            port.setEnabled(true);
                            uid.setEnabled(true);
                            connect.setText("Connect");
                            connect.setOnClickListener(new ConnectClickListener());
                            connect.setEnabled(true);
                            dialog.dismiss();
                        }
                    });
                    builder.create().show();
                }
            }
        }

        class DisconnectAsyncTask extends AsyncTask<Void, Void, Boolean> {
            @Override
            protected void onPreExecute() {
                connect.setEnabled(false);
                trigger.setEnabled(false);
            }

            protected Boolean doInBackground(Void... params) {
                try {
                    ipcon.disconnect();
                    return true;
                } catch(TinkerforgeException e) {
                    return false;
                }
            }

            @Override
            protected void onPostExecute(Boolean result) {
                if (result) {
                    host.setEnabled(true);
                    port.setEnabled(true);
                    uid.setEnabled(true);
                    connect.setText("Connect");
                    connect.setOnClickListener(new ConnectClickListener());
                }

                connect.setEnabled(true);
            }
        }

        class TriggerAsyncTask extends AsyncTask<Void, Void, Void> {
            protected Void doInBackground(Void... params) {
                try {
                    relay.setMonoflop(1 << 0, 15, 1500);
                } catch (TinkerforgeException e) {
                }

                return null;
            }
        }

        class ConnectClickListener implements OnClickListener {
            public void onClick(View v) {
                new ConnectAsyncTask().execute();
            }
        }

        class DisconnectClickListener implements OnClickListener {
            public void onClick(View v) {
                new DisconnectAsyncTask().execute();
            }
        }

        class TriggerClickListener implements OnClickListener {
            public void onClick(View v) {
                new TriggerAsyncTask().execute();
            }
        }

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);

            host = (EditText)findViewById(R.id.host);
            port = (EditText)findViewById(R.id.port);
            uid = (EditText)findViewById(R.id.uid);
            connect = (Button)findViewById(R.id.connect);
            trigger = (Button)findViewById(R.id.trigger);

            SharedPreferences settings = getPreferences(0);
            host.setText(settings.getString("host", host.getText().toString()));
            port.setText(settings.getString("port", port.getText().toString()));
            uid.setText(settings.getString("uid", uid.getText().toString()));

            connect.setOnClickListener(new ConnectClickListener());
            trigger.setOnClickListener(new TriggerClickListener());
            trigger.setEnabled(false);

            if (savedInstanceState != null && savedInstanceState.getBoolean("connected", false)) {
                new ConnectAsyncTask().execute();
            }
        }

        @Override
        protected void onSaveInstanceState(Bundle outState) {
            super.onSaveInstanceState(outState);

            boolean connected = false;

            if (ipcon != null) {
                connected = ipcon.getConnectionState() == IPConnection.CONNECTION_STATE_CONNECTED;
            }

            outState.putBoolean("connected", connected);
        }

        @Override
        protected void onStop() {
            super.onStop();

            SharedPreferences settings = getPreferences(0);
            SharedPreferences.Editor editor = settings.edit();

            editor.putString("host", host.getText().toString());
            editor.putString("port", port.getText().toString());
            editor.putString("uid", uid.getText().toString());
            editor.commit();
        }
    }

The content of this page is licensed under Creative Commons Attribution 3.0 Unported License.