This post will explain how you can use the I2C protocol to output from your AndroidThings application. We’ll discuss the Raspberry Pi Rainbow hat and its actuators, in this case the 4-digit Alphanumeric Character Display which we’ll use to count down from 1000.
Hardware Prerequisite:
We have the Rainbow Hat connected to the top of our raspberry pi.
Software Prerequisite:
An AndroidThings project already set up in Android Studio, if you aren’t sure how to get your project to this point you can view the ‘first Android Things app’ blog for instructions.
Let’s get started.
First we create an instance of PeripheralManagerService. This is the AndroidThings SDK class that allows us to open connections to different board pins using the different available protocols.
PeripheralManagerService service = new PeripheralManagerService();
Using this instance, we want to open an I2C connection to our alphanumeric display, the open method takes two parameters, first is the pin address and the second is the individual I2C peripheral address.
public class MainActivity extends Activity { private static final String I2C_ADDRESS = "I2C1"; private static final int HT16k33_SEGMENT_DISPLAY_SLAVE = 0x70; private I2cDevice bus; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); PeripheralManagerService service = new PeripheralManagerService(); try { bus = service.openI2cDevice(I2C_ADDRESS, HT16k33_SEGMENT_DISPLAY_SLAVE); } catch (IOException e) { throw new IllegalStateException(I2C_ADDRESS + " bus slave " + HT16k33_SEGMENT_DISPLAY_SLAVE + " connection cannot be opened.", e); } } }
Since we opened the connection to our pin in the onCreate method, we should use the symmetrically matching pair from the Android Lifecycle to close the connection.
@Override protected void onDestroy() { try { bus.close(); } catch (IOException e) { Log.e("TUT", I2C_ADDRESS + " bus slave " + HT16k33_SEGMENT_DISPLAY_SLAVE + "connection cannot be closed, you may experience errors on next launch.", e); } super.onDestroy(); }
Let’s create a handler that we can post runnables to, these runnables will countdown from a set number at a fixed rate. We create the handler using our own HandlerThread Looper, meaning we post messages on our own non-ui thread. We are counting down from a 1000 and it could take some time so we don’t want to do this on the MainThread.
HandlerThread handlerThread = new HandlerThread("PeripheralThread"); handlerThread.start(); handler = new Handler(handlerThread.getLooper());
We have a field to declare the number we are counting down from, in this case 1000, and we have a variable which will start at this number but be mutable so we can decrement it every time we post a new runnable to our handler. These look like this.
private static final int COUNTDOWN_FROM = 1000; private int count = COUNTDOWN_FROM;
Now lets create a runnable to post to the handler. This runnable will decrement the count. We post this same runnable again with a delay of 100ms to create a loop. This means we will decrement 10 times a second, so it should be quite fast counting down. When we reach 0 we want to stop posting more messages and return early.
private final Runnable writeDisplay = new Runnable() { @Override public void run() { if (count < 0) { return; } Log.d("TUT", "display " + count); count--; handler.postDelayed(this, TimeUnit.MILLISECONDS.toMillis(100)); } };
Note: All the specific information about this alphanumeric display that we are about to discuss, if you are looking for it as a reference, is in the datasheet here.
The datasheet explains, when the system is first turned on it is in a standby state, with the system being off and the display being off. If we exit standby mode we turn on the system.
@Override protected void onStart() { super.onStart(); try { bus.write(new byte[]{(byte) (0x20 | 0b00000001)}, 1); } catch (IOException e) { throw new IllegalStateException("Cannot turn on peripheral (exit standby)", e); } }
Now the system is out of standby we also need to turn the LED display on. We can turn the system on using this OR bit manipulation in Java.
@Override protected void onStart() { super.onStart(); ... other code try { bus.write(new byte[]{(byte) (0x80 | 0b00000001)}, 1); // bus.write(new byte[]{(byte) (0x80 | 0b00000111)}, 1); // if you want blinking } catch (IOException e) { throw new IllegalStateException("Cannot turn on the LED display", e); } }
The LED segments for the display have their own dataformat for encoding characters, this can be looked up online. For simplicity I’ve recreated just the 0-9 byte formats here as that is all we will need.
private static final Map<Character, Short> ENCODED_DIGITS = new HashMap<>(); static { ENCODED_DIGITS.put('0', (short) 0b00001100_00111111); ENCODED_DIGITS.put('1', (short) 0b00000000_00000110); ENCODED_DIGITS.put('2', (short) 0b00000000_11011011); ENCODED_DIGITS.put('3', (short) 0b00000000_10001111); ENCODED_DIGITS.put('4', (short) 0b00000000_11100110); ENCODED_DIGITS.put('5', (short) 0b00100000_01101001); ENCODED_DIGITS.put('6', (short) 0b00000000_11111101); ENCODED_DIGITS.put('7', (short) 0b00000000_00000111); ENCODED_DIGITS.put('8', (short) 0b00000000_11111111); ENCODED_DIGITS.put('9', (short) 0b00000000_11101111); }
To lookup our count digit in the map, we need to convert it to chars. We do that with this code. Notice we are padding out the characters with leading 0’s, this gives a nice left to right formatting and stops ghost digits, for example a 1 being left written in the first segment when we move to 999.
private char[] convertToChars(int count) { return padWithZeros(count).toCharArray(); } private String padWithZeros(int count) { String countAsString = String.valueOf(count); int length = countAsString.length(); if (length == 1) { return "000" + countAsString; } if (length == 2) { return "00" + countAsString; } if (length == 3) { return "0" + countAsString; } if (length == 4) { return countAsString; } throw new IllegalStateException(count + " cannot be padded. We only coded to handle 4 digits."); }
Now that we have our number we are counting down from as an array of characters, and we have the map to lookup the byte value for these digits. We need to know where to write the byte encoded digits to. The datasheet explains about display memory. Each row output corresponds to one of our segments in our four segment display. Therefore we can use the hex values, 0, 2, 4 and 6 to address our segments left to right.
bus.writeRegWord(0x0, ENCODED_DIGITS.get(digits[0])); bus.writeRegWord(0x2, ENCODED_DIGITS.get(digits[1])); bus.writeRegWord(0x4, ENCODED_DIGITS.get(digits[2])); bus.writeRegWord(0x6, ENCODED_DIGITS.get(digits[3]));
The whole code for the runnable looks like this:
private final Runnable writeDisplay = new Runnable() { @Override public void run() { if (count < 0) { return; } Log.d("TUT", "display " + count); char[] digits = convertToChars(count); try { bus.writeRegWord(0x0, ENCODED_DIGITS.get(digits[0])); bus.writeRegWord(0x2, ENCODED_DIGITS.get(digits[1])); bus.writeRegWord(0x4, ENCODED_DIGITS.get(digits[2])); bus.writeRegWord(0x6, ENCODED_DIGITS.get(digits[3])); } catch (IOException e) { throw new IllegalStateException("Cannot write " + count + " to peripheral.", e); } count--; handler.postDelayed(this, TimeUnit.MILLISECONDS.toMillis(100)); } private char[] convertToChars(int count) { return padWithZeros(count).toCharArray(); } private String padWithZeros(int count) { String countAsString = String.valueOf(count); int length = countAsString.length(); if (length == 1) { return "000" + countAsString; } if (length == 2) { return "00" + countAsString; } if (length == 3) { return "0" + countAsString; } if (length == 4) { return countAsString; } throw new IllegalStateException(count + " cannot be padded. We only coded to handle 4 digits."); } };
We hook up the runnable to run when the app starts. Remember the runnable repeats itself by posting again every time it gets to the end. And for best practice always remember to remove callbacks using the Android lifecycle symmetrically matching method which in this case is onStop like this.
@Override protected void onStart() { super.onStart(); … other code handler.post(writeDisplay); } @Override protected void onStop() { handler.removeCallbacks(writeDisplay); super.onStop(); }
That’s it!
You now understand how the Android Things I 2 C output communication with peripherals works. You can interface with a alphanumeric character display writing commands to control its settings such as brightness and display characters. This knowledge also transfers to coding other I2C peripherals such as fingerprint readers, motor drivers or other digital displays.
The source is on GitHub and all the code is available here.
If you are more of an audible learning, or prefer video content. You can check out my Android Things introductory video course on Caster.io.