Saturday, February 28, 2015

Setting up a Bonjour (Zeroconf) service name for your Raspberry Pi and accessing it from an Android App

I have written an Android app that communicates to my Raspberry Pi over SSH (using JSch) but the issue is that in the App the IP address of my Raspberry Pi has been hardcoded at 192.168.1.109 - ideally it would be good to be able to give it a local name, something like garagedoor.local - something that won't change when the DHCP assigns a different IP address.

It turns out that the way to do this isn't difficult on the Raspberry Pi and most of work is in changes to the Android App needed to make it happen. I've numbered the steps 1-4.

1. Configuration of your Raspberry Pi.

# Get superuser privileges
sudo su

# Change the host name of your Raspberry Pi from "raspberrypi" to "garagedoor"
# Reference: http://www.howtogeek.com/167195/how-to-change-your-raspberry-pi-or-other-linux-devices-hostname/

sudo nano /etc/hosts            # <== Replace raspberrypi with garagedoor
sudo nano /etc/hostname         # <== Replace raspberrypi with garagedoor

sudo /etc/init.d/hostname.sh    # Commit the changes.
sudo reboot                     # Reboot

# Install the mDNS implementation - avahi
# Reference: http://www.howtogeek.com/167190/how-and-why-to-assign-the-.local-domain-to-your-raspberry-pi/
# Get superuser privileges
sudo su

# Update the package sources
apt-get update

# Ugrade the packages
apt-get upgrade

# Install the mDNS implementation - avahi
sudo apt-get install avahi-daemon



That's the end of the changes needed on your Raspberry Pi.

2. Test on either Windows, Linux or Mac OSX that the Raspberry Pi is visible on the network with its new name of "garagedoor.local".

On Windows, you will need to have Bonjour Networking Services installed, which comes bundled with iTunes.
Run a cmd.exe prompt and type the command ping garagedoor.local -- you should get a response from your Raspberry Pi.

On Mac OSX, Bonjour Networking Services are installed by default.
In a bash terminal window, type the command ping garagedoor.local -- you should get a response from your Raspberry Pi.

On Linux, first ensure you have installed the avahi-daemon package.
In a bash terminal window, type the command ping garagedoor.local -- you should get a response from your Raspberry Pi.

3. Using NSD (Network Service Discovery) on Android to resolve the Raspberry Pi's service name.

Browsers and terminal programs on Android won't be able to resolve the garagedoor.local since to do this requires use of either the official Network Service Discovery library (supported by API Level 16 up) or the open source Java Zeroconf library named jMDNS.

In my case, I am using a custom app I wrote to communicate to the Raspberry Pi, so I have decided just to use the Network Service Discovery library.

4. Source code changes needed in the Android App to support NSD (Network Service Discovery)

Notes:
 (1) Below is just how I decided to implement it, you may want to separate the NSD related code out of the Activity into its own class, but for me that wasn't a problem.
 (2) I probably need to do something related to NSD in the Application lifecycle events of onPause(), onResume(), onDestroy() and onTeardown(). I haven't done that here yet.


4.A. Add some members to the Activity

I added this to my MainActivity but my app is small and so that's all I needed to do.

// Network Service Discovery related members
// This allows the app to discover the garagedoor.local
// "service" on the local network.
// Reference: http://developer.android.com/training/connect-devices-wirelessly/nsd.html
private NsdManager mNsdManager;
private NsdManager.DiscoveryListener mDiscoveryListener;
private NsdManager.ResolveListener mResolveListener;
private NsdServiceInfo mServiceInfo;
public String mRPiAddress;

// The NSD service type that the RPi exposes.
private static final String SERVICE_TYPE = "_workstation._tcp.";

   
4.B. Add some init code to the bottom of the Activity's onCreate() method.

mRPiAddress = "";
mNsdManager = (NsdManager)(getApplicationContext().getSystemService(Context.NSD_SERVICE));

initializeResolveListener();
initializeDiscoveryListener();
mNsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, mDiscoveryListener);

   
4.C. Add the following two new methods to your Activity.

 private void initializeDiscoveryListener() {

     // Instantiate a new DiscoveryListener
     mDiscoveryListener = new NsdManager.DiscoveryListener() {

         //  Called as soon as service discovery begins.
         @Override
         public void onDiscoveryStarted(String regType) {
         }

         @Override
         public void onServiceFound(NsdServiceInfo service) {
             // A service was found!  Do something with it.
             String name = service.getServiceName();
             String type = service.getServiceType();
             Log.d("NSD", "Service Name=" + name);
             Log.d("NSD", "Service Type=" + type);
             if (type.equals(SERVICE_TYPE) && name.contains("garagedoor")) {
                 Log.d("NSD", "Service Found @ '" + name + "'");
                 mNsdManager.resolveService(service, mResolveListener);
             }
         }

         @Override
         public void onServiceLost(NsdServiceInfo service) {
             // When the network service is no longer available.
             // Internal bookkeeping code goes here.
         }

         @Override
         public void onDiscoveryStopped(String serviceType) {
         }

         @Override
         public void onStartDiscoveryFailed(String serviceType, int errorCode) {
             mNsdManager.stopServiceDiscovery(this);
         }

         @Override
         public void onStopDiscoveryFailed(String serviceType, int errorCode) {
             mNsdManager.stopServiceDiscovery(this);
         }
     };
 }

 private void initializeResolveListener() {
     mResolveListener = new NsdManager.ResolveListener() {

         @Override
         public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
             // Called when the resolve fails.  Use the error code to debug.
             Log.e("NSD", "Resolve failed" + errorCode);
         }

         @Override
         public void onServiceResolved(NsdServiceInfo serviceInfo) {
             mServiceInfo = serviceInfo;

             // Port is being returned as 9. Not needed.
             //int port = mServiceInfo.getPort();

             InetAddress host = mServiceInfo.getHost();
             String address = host.getHostAddress();
             Log.d("NSD", "Resolved address = " + address);
             mRPiAddress = address;
         }
     };
 }


4.D. Make use of the mRPiAddress member where previously you used a hardcoded IP address.

Check that its not empty before using it. If its empty, it means the RPi's name couldn't be resolved.


Follow @dodgy_coder

Subscribe to posts via RSS




9 comments:

  1. Hey there,

    Great article! I'm pretty much doing your setup the only difference is I'm doing this using an iPhone :-)

    Anyhow, I'm having some trouble setting up zeroconf on my banana pi running raspbian - I'm hoping you can clarify a couple of things:
    - I suppose from your article you don't have a dhcp server running that is assigning a network IP address - can you confirm?

    My issue:
    - if I have my pi hardwired to my network my service is running and my iPhone app can see the pi using something like pi.local (I have updated the host file)
    - if I unplug the RJ45 my service is advertised (I can see it from my iPhone app) but the domain resolution fails

    I have a feeling my network conf file is not correct for wlan0 but can you confirm you got this to work with using dhcp and let me know if I missed anything obvious?

    Thanks in advance for any help/advice you may have,

    X
    fyi my /etc/network/interfaces :
    auto lo

    iface lo inet loopback
    iface eth0 inet dhcp

    #auto wlan0
    #allow-hotplug wlan0
    #iface wlan0 inet manual

    allow-hotplug wlan0
    auto wlan0
    iface wlan0 inte ipv4ll

    iface default inet dhcp

    ReplyDelete
    Replies
    1. Hi Xav, thanks for the feedback! I'll try and help. To confirm I originally hardcoded my RPi IP address (i.e. used a static IP) before setting up Zeroconf, but after, yes, I reverted back to using DHCP to get an IP address for the RPi and it worked fine resolving the name on Windows and Android. I was always running on WiFi (wlan0) because my RPi is located far from my home's ADSL modem/router. I will dig out my /etc/network/interfaces file for you also, if that helps. Just give me a few hours to get around to it since I'm at work right now.

      Delete
    2. that would be great because right now I'm stuck and I have no clue what I'm missing. I'm starting to wonder if this is supposed to work when the wifi needs a pwd (maybe I need to specify ssid and psk?) this would kinda make this not very useful but anyhow, lmk what your setup is... thanks!

      Delete
    3. I have a feeling though you may be using a static or dhcp address - from your post it seems you're not using avahi-autoipd...anyhow I'll wait now for your reply :-)

      Delete
    4. Hi Xav,

      I went back to look at the notes I made when setting up the RPi. It seems I never used a static IP ... I was always using DHCP with WiFi.

      Also I am running avahi-daemon, which I installed with this command ...

      sudo apt-get install avahi-daemon

      Here's the full set of notes I made regarding setting up the WiFi, hopefully this helps ...

      ;
      ; Installing the Edimax USB WiFi dongle on the RPi
      ; Ref: "Raspberry Pi - Installing the Edimax EW-7811Un USB WiFi Adapter (WiFiPi)"
      ; http://www.savagehomeautomation.com/projects/raspberry-pi-installing-the-edimax-ew-7811un-usb-wifi-adapte.html

      ; Boot the RPi with the only the dongle and the keyboard attached
      ; Check the device is detected
      lsusb # Ensure present: Edimax Technology Co., Ltd EW-7811Un 802.11n Wireless Adapter [Realtek RTL8188CUS]

      ; list the loaded kernel modules
      lsmod # Ensure present: 8192cu

      ; Check the WiFi config
      iwconfig # Ensure present: wlan0

      ; Configure the WiFi as shown on the page above
      sudo nano /etc/network/interfaces

      ; CONTENTS SET TO --->
      auto lo

      iface lo inet loopback
      iface eth0 inet dhcp

      auto wlan0
      allow-hotplug wlan0
      iface wlan0 inet manual
      wpa-roam /etc/wpa_supplicant/wpa_supplicant.conf
      iface default inet dhcp
      <----

      ; Additional ref: http://www.raspberrypi.org/phpBB3/viewtopic.php?t=50312&p=391044
      sudo nano /etc/wpa_supplicant/wpa_supplicant.conf

      ; CONTENTS SET TO --->
      ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
      update_config=1

      network={
      ssid="YOURSSIDHERE"
      proto=RSN
      key_mgmt=WPA-PSK
      pairwise=CCMP TKIP
      group=CCMP TKIP
      psk="PASSWORDHERE"
      }
      <----

      ; restart the RPi
      sudo shutdown -r now

      ; pull out the ethernet cable
      ; check the status and IP address of the WLAN
      ifconfig wlan0

      ; check the status and IP address of the LAN
      ifconfig eth0

      Delete
    5. Another small comment - you are correct, I am not running avahi-autoipd.

      Delete
  2. This comment has been removed by the author.

    ReplyDelete
  3. Thank you for such a nice post , I have been trying to find solution for same problem , did not know it has to be _workstation._tcp. ...Thank you so much

    ReplyDelete
  4. Thank you for such a nice post , I have been trying to find solution for same problem , did not know it has to be _workstation._tcp. ...Thank you so much

    ReplyDelete