One of the joys of Android development is that; whatever your idea, there is likely to be a library available to help you out. Most new projects begin with the addition of a few favourites.  If you are going to be consuming any web APIs then high on your list should be retrofit. In Android Things a few of the familiar Android APIs are not available. This may affect you if you are porting some existing Android code. It may also impact your choice of library and you’ll need to check that it doesn’t use a missing API.

In this example we’ll be continuing development on the LCD driver. We’ll be adding retrofit to ping the bus company for bus arrival times at a couple of local bus stops. It’s about time we got to the ‘Internet’ part of IoT 🙂

Here in Wellington some of our bus stops have live displays showing the times of the next few buses. Whenever I get the looser cruiser I check an app to see when I should leave the house,  which is like four or five clicks/swipes etc on my phone. Ain’t nobody got time for that. I need that information displayed in my house somewhere.

 

Two displays, one local stop each.

There is an unofficial API that can be accessed to get the live timing of busses in the system. In this project I want to have more than one LCD display. This means I’ll need to modify the LCD driver to add the ability to specify data and control masks and pin descriptions. These are hard coded values bit I’ll need to make them variables. Passing them in to the constructor is error prone. Getting the right order of arguments sounds like something I’m going to screw up. We’ll use a builder instead.

Changes to the LCD Driver

If you have the code cloned from the previous example, switch to the things-driver-0.1.0 tag. Have a look at the differences here. What I’ve done is to add a builder, this allows me to specify each of the PCF8574 pin functions at construction time. I no longer have to change hard coded values to support different pin configurations.

I2cSerialCharLcd.I2cSerialCharLcdBuilder builder = I2cSerialCharLcd.builder(16, 4);
    builder.rs(0).rw(1).e(2).bl(3).data(4, 5, 6, 7).address(7);
    I2cSerialCharLcd lcd = builder.build();

The first thing we do is initialise an I2cSerialCharLcd.builder. Pass it the LCD’s width and height. Then we specify the pin mapping, the numbers map to port pin names, i,e. 0 is P0 on the PCF8574. You can use this to setup your PCF8574 to LCD mapping. Specify your module’s address with, strangely enough, the address() builder method. This takes the value of your A0 – A2 pins.

Once you have a reference to the lcd, you can connect to it and write as before. One small change to the print() method means that you now pass it the line number as the first argument and not the DDRAM address.

Setting up Retrofit

Setting up retrofit is already extensively documented. The reference I usually go to is CodePath. I took an example of the MetLink ‘API’ from here and ran the response through jsonschema2pojo to generate the model. It’s too easy. The result is upcoming bus arrivals at a couple of local stops, the first column is the route number, the second column is the destination stop, the third is the number of minutes to the bus arrival.

The  Source

Here’s the source for the main activity.

/*
 * Copyright 2016 Dave McKelvie <www.android.geek.nz>.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package nz.geek.android.tingting;

import android.app.Activity;
import android.os.Handler;
import android.os.Bundle;

import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import nz.geek.android.things.drivers.lcd.I2cSerialCharLcd;
import nz.geek.android.tingting.model.Service;
import nz.geek.android.tingting.model.StopDepartures;
import nz.geek.android.tingting.network.MetlinkApi;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

public class MainActivity extends Activity {

  private static final String TAG = "TingTing";
  private static final String STOP_1 = "7120";
  private static final String STOP_2 = "7018";

  Map<String, I2cSerialCharLcd> stopMap;
  Handler handler = new Handler();
  UpdateStopRunner updateStopRunner;
  MetlinkApi metlinkApi;

  public static final String BASE_URL = "https://www.metlink.org.nz/api/v1/";

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    createLcds();
    updateStopRunner = new UpdateStopRunner(stopMap);
    Retrofit retrofit = new Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build();
    metlinkApi = retrofit.create(MetlinkApi.class);
    handler.postDelayed(updateStopRunner, 2000);

  }

  @Override
  public void onDestroy() {
    super.onDestroy();
    handler.removeCallbacks(updateStopRunner);
    for (I2cSerialCharLcd lcd : stopMap.values()) {
      if (lcd != null) {
        lcd.disconnect();
      }
    }
  }

  private void createLcds() {
    stopMap = new HashMap<>();
    I2cSerialCharLcd.I2cSerialCharLcdBuilder builder = I2cSerialCharLcd.builder(16, 4);
    builder.rs(0).rw(1).e(2).bl(3).data(4, 5, 6, 7).address(7);
    stopMap.put(STOP_1, builder.build());
    stopMap.get(STOP_1).connect();

    builder = I2cSerialCharLcd.builder(20, 4);
    builder.rs(0).rw(1).e(2).bl(3).data(4, 5, 6, 7).address(6);
    stopMap.put(STOP_2, builder.build());
    stopMap.get(STOP_2).connect();
  }

  /**
   * A native method that is implemented by the 'native-lib' native library,
   * which is packaged with this application.
   */
  public native String stringFromJNI();

  // Used to load the 'native-lib' library on application startup.
  static {
      System.loadLibrary("native-lib");
  }

  private final class UpdateStopRunner implements Runnable {
    private final Map<String, I2cSerialCharLcd> stopMap;

    public UpdateStopRunner(Map<String, I2cSerialCharLcd> stopMap) {
      this.stopMap = stopMap;
    }

    private void updateDisplay(I2cSerialCharLcd lcd, List<Service> services) {
      lcd.clearDisplay();
      for (int i = 0; i < lcd.getHeight(); i++) {
        if (services.size() < i) break;
        Service service = services.get(i);
        lcd.print(i + 1, String.format(Locale.UK, "%3s %-" + (lcd.getWidth() - 8) + "s %3d",
                service.getServiceID(),
                service.getDestinationStopName().substring(0, Math.min((lcd.getWidth() - 8),
                service.getDestinationStopName().length())), service.getDisplayDepartureSeconds() / 60));
      }
    }

    void getDeparturesForStop(String stop) {
      Call<StopDepartures> call = metlinkApi.getStopDepartures(String.valueOf(stop));
      call.enqueue(new Callback<StopDepartures>() {
        @Override
        public void onResponse(Call<StopDepartures> call, Response<StopDepartures> response) {
          int statusCode = response.code();
          if (statusCode == 200) {
            StopDepartures departures = response.body();
            List<Service> services = departures.getServices();
            updateDisplay(stopMap.get(departures.getStop().getSms()), services);
          }
        }

        @Override
        public void onFailure(Call<StopDepartures> call, Throwable t) {
          // Log error here since request failed
        }
      });

    }

    @Override
    public void run() {
      getDeparturesForStop(STOP_1);
      getDeparturesForStop(STOP_2);
      handler.postDelayed(this, 30000);
    }
  }
}