Skip to main content Skip to local navigation

Firmata Example : I2C Sensor + Java & Firmata4j

Sketch of the Grove beginner kit for Arduino showing USB connection, the converter chip, the processor (ATMEGA328) and the sensor (BMP280)
Grove Beginner Kit, with USB connection to PC, as well as the ATMEGA328 and I2C connection to the BMP280 sensor.

A lot of Arduino projects are meant to always be attached to a PC, transferring data back and forth between the Arduino and the PC. That's why Firmata, a program that sits on the Arduino and allows a program on your PC to remotely read from sensors and buttons and transmit to things like speakers and displays, was developed.

The Firmata approach to dealing with the Arduino allows you to focus your programming efforts on the PC side of things, whether you like programming in Java, Python, or any other language. A similar, but distinct, approach is taken by MATLAB.

Unfortunately, a lot of interesting peripherals are I2C-based and that makes them a little complicated to deal with. That complexity could be overcome with some good examples. Unfortunately, there aren't any good examples of setting up an I2C sensor in Firmata that work with Firmata4J. The best we have is the OLED SSD1306 example, but it's really one way only: you can only send the OLED commands -- you can't receive data from it.

BMP280 sensor
BMP280 sensor (look for the VCC and SDA labels, go right, over the little beige capacitor and find the little grey rectangle with the black dot on top. That's the actual sensor)

What we need is a simple, public example of an Arduino board, equipped with Firmata, talking to and receiving data from a typical I2C sensor. The sensor needs to get recognized by the Arduino board via a Java command that gets interpreted by the Firmata firmware on the Arduino. The Grove Beginner Kit for Arduino is a good example of this because it is both Arduino-compatible, has a compatible OLED, and has a commonly-used I2C sensor, the Bosch BMP280.

Here, we'll use that BMP280 sensor, an inexpensive air pressure sensor, labelled with "IIC" in the image to the left. All this example does is find the sensor and asks it for its ID. In this case the sensor, the BMP280, has an address on the I2C bus of 0x77. To ask the sensor for its ID, we need to send it the command 0xD0. If all goes well, it should respond with 0x58.

print out of source code on table, with highlighters, pens and coffee.
Print out of some of the key firmata4j source code. This was used to determine what elements needed to be included in this I2C sensor demonstration.

Here's what Java program will do while interacting with the Firmata code running on the Arduino's ATMEGA328 processor:

  1. First, the Java program tells the Arduino's ATMEGA chip to configure the I2C connection. (0x78)
  2. Second, the Java program will send, over USB, a command to tell the ATMEGA to request the ID from the sensor.
  3. Then, the ATMEGA238 processor will send the command "0xD0" to address "0x77" on the I2C bus.
  4. Then, the Java program will send a Firmata message, over USB, to ask that the ATMEGA processor to wait for a response.
  5. The ATMEGA processor will receive the response from the sensor and then transmit it, over USB, back to the Java program.

In the following you can see how commands send from our Java program get converted into UART and I2C messages that are read by a Saleae logic analyzer that I've connected to the board. While not a standard piece of equipment for a student, this is absolutely a critical tool when developing I2C-capable programs. If you're thinking of doing I2C work, consider getting a logic analyzer, whether it's a sub-$100 unit or a mid-range unit like mine.

The core of the Java program is found in the main method. It's responsible for setting up the I2C mode on the Arduino, then setting up the listener and then initializing the BMP280 connection on the I2C:

        // Set up the BMP280 sensor & a listener for I2C events.
        I2CDevice firmataToI2CPressureSensor =
                groveArduinoBoard.getI2CDevice(Bmp280Token.BMP280_ADDR);                        // Sensor as I2C Object.
        SensorI2CListener  myI2CListener  = new SensorI2CListener(firmataToI2CPressureSensor);  // Listener setup.
        firmataToI2CPressureSensor.subscribe(myI2CListener);                                    // Subscribe to listener
        Bmp280 groveBmp280Sensor = new Bmp280(firmataToI2CPressureSensor, myI2CListener);       // BMP280 specifics

        // Initialize the sensor.  Ask it for its ID.
        groveBmp280Sensor.init();  // init() method is like the SSD1306 Method.
Core portion of the code to run the sensor via firmata4j.

The listener for the I2C is coded as follows:

public class SensorI2CListener implements I2CListener{

    private final I2CDevice theI2CDevice;
    SensorI2CListener(I2CDevice theI2CDevice){
        this.theI2CDevice = theI2CDevice;
    }
    @Override
    public void onReceive(I2CEvent theI2CEvent) {

        // Print out the event details (address, data, etc.)
        System.out.println("Listener reports the following information: ");
        System.out.println("The event:" + theI2CEvent + ". Note: data is in base 10.");
        System.out.println("the Device address: " +theI2CEvent.getDevice());
        System.out.println("Event data array: " + Arrays.toString(theI2CEvent.getData()) + " in base 10.");
        /* convert the byte array and print it */
        String hexString = new BigInteger(1,theI2CEvent.getData()).toString(16);
        System.out.println("Event data array: 0x" + hexString);
        /* again, but compact */
        System.out.println("Event data array: 0x" + new BigInteger(1,theI2CEvent.getData()).toString(16));


    }
}
I2C listener class.

UART Message 1: Configure the I2C

This message is sent by Firmata4j to tell the ATMEGA to set up the I2C feature on the ATMEGA processor.

The first UART command to the ATMEGA processor. It's a "SysEx" command and contains the value 0x78, which says that the I2C bus should be initialized.

No visible change occurs on the I2C bus at this point. The changes are happening internal to the ATMEGA processor.

UART Message 2: Request the Sensor ID

This message is sent by Firmata4j to tell the ATMEGA that we want the sensor's ID value. Here, we use the optional Firmata command, 0x76, to get the ATMEGA to communicate with the BMP280 at address 0x77 that we want its identification value.

The second UART command to the ATMEGA processor. It's a "SysEx" command and contains the value 0x76, which says that the sensor at address 0x77 should report its identification code. The message for the ATMEGA is encoded in the 0x00, 0x50 and 0x01.

The ATMEGA responds by sending the BMP280 sensor a command on the I2C bus. This message contains both the address of the sensor (0x77) and the command (0xD0):

I2C message
The first I2C message, with two bytes: an address byte (0x77) and the command (0xD0) for requesting the sensor's ID. Note that the "ACK" messages are included because the sensor acknowledges receipt of each of the two components of the message.

Within Java, we executed this using the tell() method, as follows, where BMP280_ID_ASK is a constant set to 0xD0:

        // 1. Tell the chip.
        try{
            theBMPdevice.tell(Bmp280Token.BMP280_ID_ASK);
            System.out.println("I2C telling...");
        }        catch (IOException e){
            throw new RuntimeException(e);
        }
This is the Java code used to tell the I2C sensor that we want its ID value.

UART Message 3: Request that we listen to the Sensor's Response

This message is sent by Firmata4j to tell the ATMEGA that we want it to listen for a response from the BMP280 sensor. The difference between the previous message and this one are the three bytes, 0x08, 0x01 and 0x00.

The third UART command to the ATMEGA processor. It's a "SysEx" command and contains the value 0x76, which says that the sensor at address 0x77 should report its identification code. The message for the ATMEGA is encoded in the 0x08, 0x01 and 0x00.

The sensor then detects that the ATMEGA has given up control of the I2C bus and then responds by saying that its ID is 0x58:

The sensor responds on the I2C bus with its ID code, 0x58. The first byte is acknowledged (ACK) but the second one is not (NAK).

Now that the sensor has responded, the ATMEGA can send this value, 0x58, back to the PC by encoding the value in a Firmata message.

Within Java this was achieved using the ask() method, which explicitly only asks for a single returned byte (that's the 1) and engages the listener, myI2CListener:

        // 3. listen.  (ask)
        try {
            theBMPdevice.ask((byte)1, myI2CListener);
            System.out.println("I2C asking...");

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
Here we use the ask() method to ensure that the listener is ready to receive the BMP280 response.

UART Message 4: Response from the Arduino back to the PC with Sensor ID Code

This message is sent by the ATMEGA chip on the Arduino via its UART port. It has obtained the sensor ID code and is now sending it back to the PC via the USB cable. Like the other Firmata messages, it's formatted as a SysEx command

Fourth UART message, from the Arduino, back to the PC. It states that this is an I2C reply from the BMP280 sensor and that it contains the value 0x58, which corresponds to the ID of the sensor.

From within the Java program we can print out the Firmata message contents. Note that the standard method for doing this, , will print out the data in base 10. So, while we're expecting 0x58, it will print out as 88. Not a big deal, just important to keep in mind. It is, possible, to convert it back to hex for displaying to the user:

Displaying the Firmata message data from a Java program using the Firmata4j library. Here, we can see that the address of the BMP280 sensor is 0x77 and the value returned from the sensor is 88 (in base 10) or 0x58 (in hex, or base 16)

Most of what is displayed above is found within the onReceive() method found within the SensorI2CListener class that I wrote. The program, as I wrote it, is found below. It borrows heavily from the SSD1306 example in the firmata4j GitHub page.

Bmp280 class

/* model this after the SSD1306 class by Oleg Kurbatov */

import org.firmata4j.I2CDevice;
import org.firmata4j.I2CEvent;
import org.firmata4j.I2CListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.Arrays;

import static java.lang.Integer.toHexString;

public class Bmp280 {
    private final I2CDevice theBMPdevice;
    private final SensorI2CListener myI2CListener;
    private static final Logger LOGGER = LoggerFactory.getLogger(Bmp280.class);

    /* constructor 1 */
    public Bmp280(I2CDevice theBMPdevice, SensorI2CListener myI2CListener) {
        this.theBMPdevice = theBMPdevice;
        this.myI2CListener = myI2CListener;
    }

    /* Initialize the Bmp280 sensor by asking for its ID. */
    public void init() throws InterruptedException {


        // 1. Tell the chip.
        try{
            theBMPdevice.tell(Bmp280Token.BMP280_ID_ASK);
            System.out.println("I2C telling...");
        }        catch (IOException e){
            throw new RuntimeException(e);
        }

        // 2. Pause briefly.
        Thread.sleep(1);

        // 3. listen.  (ask)
        try {
            theBMPdevice.ask((byte)1, myI2CListener);
            System.out.println("I2C asking...");

        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        Thread.sleep(20);
        System.out.println("The BMP280 address: (after asking...): 0x" + toHexString(theBMPdevice.getAddress()));

    }


     /* basic I2C command method.
     *  Copied from SSD1306.java */
    private void command (byte... commandBytes){
        try{
            for(int i = 0; i < commandBytes.length; i += 2){
                theBMPdevice.tell(Arrays.copyOfRange(commandBytes,i,i+2));
            }
        }
        catch (IOException e){
            throw new RuntimeException(e);
        }
    }
}

Bmp280MessageFactory class

Not used here... use later.

/*
  Complex messages for the BMP280 pressure sensor.

  Based on SSD1306MessageFactory by Oleg Kurbatov.

 */
//import static Bmp280Token;
public class Bmp280MessageFactory {
  public static final byte[] BMP280_EXAMPLE_COMMAND_ARRAY_1 = {(byte)0xA0, (byte)0xFF, (byte)0x01}; /* actually means nothing */
    public static final byte[] BMP280_EXAMPLE_COMMAND_ARRAY_2 = {Bmp280Token.BMP280_ID_ASK}; /* actually means nothing */


}

BmpToken interface


/* BMP280 constants for I2C communicition.
 *
 * Based on approach taken by Oleg Kurbatov and the SSD1306Token interface file.
 */
public interface Bmp280Token {
     final byte BMP280_ADDR = (byte) 0x77;
     final byte BMP280_ID_ASK = (byte) 0xD0;
     final byte BMP280_RESET = (byte) 0xE0;
     final byte BMP280_STATUS = (byte) 0xF3;
     final byte BMP280_CTRL_MEAS = (byte) 0xF4;
     final byte BMP280_CONFIG = (byte) 0xF5;

     /* the chip should respond with this ID value when asked */
     final byte BMP280_ID_DESIRED_RESPONSE = (byte) 0x58;
 }

Main class


import org.firmata4j.I2CDevice;
import org.firmata4j.firmata.FirmataDevice;

import java.io.IOException;

public class MainClass {

    static String USBPORT = "/dev/cu.usbserial-0001"; // TO-DO : modify based on your computer setup.

    public static void main(String[] args) throws IOException, InterruptedException {
        var myUSBPort = USBPORT;

        var groveArduinoBoard = new FirmataDevice(myUSBPort);
        groveArduinoBoard.start();
        groveArduinoBoard.ensureInitializationIsDone();

        // Set up the BMP280 sensor & a listener for I2C events.
        I2CDevice firmataToI2CPressureSensor =
                groveArduinoBoard.getI2CDevice(Bmp280Token.BMP280_ADDR);                        // Sensor as I2C Object.
        SensorI2CListener  myI2CListener  = new SensorI2CListener(firmataToI2CPressureSensor);  // Listener setup.
        firmataToI2CPressureSensor.subscribe(myI2CListener);                                    // Subscribe to listener
        Bmp280 groveBmp280Sensor = new Bmp280(firmataToI2CPressureSensor, myI2CListener);       // BMP280 specifics

        // Initialize the sensor.  Ask it for its ID.
        groveBmp280Sensor.init();  // init() method is like the SSD1306 Method.

        // Shut off connection to the board.
        groveArduinoBoard.stop();
    }
}

SensorI2CListener class

import org.firmata4j.I2CDevice;
import org.firmata4j.I2CEvent;
import org.firmata4j.I2CListener;

import java.math.BigInteger;
import java.util.Arrays;

public class SensorI2CListener implements I2CListener{

    private final I2CDevice theI2CDevice;
    SensorI2CListener(I2CDevice theI2CDevice){
        this.theI2CDevice = theI2CDevice;
    }
    @Override
    public void onReceive(I2CEvent theI2CEvent) {

        // Print out the event details (address, data, etc.)
        System.out.println("Listener reports the following information: ");
        System.out.println("The event:" + theI2CEvent + ". Note: data is in base 10.");
        System.out.println("the Device address: " +theI2CEvent.getDevice());
        System.out.println("Event data array: " + Arrays.toString(theI2CEvent.getData()) + " in base 10.");
        /* convert the byte array and print it */
        String hexString = new BigInteger(1,theI2CEvent.getData()).toString(16);
        System.out.println("Event data array: 0x" + hexString);
        /* again, but compact */
        System.out.println("Event data array: 0x" + new BigInteger(1,theI2CEvent.getData()).toString(16));


    }
}

Dependencies

This code is dependant on the Firmata4j library which, in turn, requires JSSC, jSerialComm and SLF4J. All of these are packaged into a single JAR file located on GitHub. The JAR is based on the 2.3.9 release of Firmata4j, which is available on GitHub but not on Maven.

Conclusion

Still lots of work to do. The code is pretty rough and could stand some cleaning up. Plus, this only captures a single byte from the sensor. That's not enough. But it's enough to get started.


Firmata Codes

There are plenty of different codes to keep track of in Firmata. It's important to note that they are, for the most part, based on the MIDI 1.0 protocol. Here are some useful links:


a pen

James Andrew Smith is a Professional Engineer and Associate Professor in the Electrical Engineering and Computer Science Department of York University's Lassonde School, with degrees in Electrical and Mechanical Engineering from the University of Alberta and McGill University.  Previously a program director in biomedical engineering, his research background spans robotics, locomotion, human birth and engineering education. While on sabbatical in 2018-19 with his wife and kids he lived in Strasbourg, France and he taught at the INSA Strasbourg and Hochschule Karlsruhe and wrote about his personal and professional perspectives.  James is a proponent of using social media to advocate for justice, equity, diversity and inclusion as well as evidence-based applications of research in the public sphere. You can find him on Twitter. Originally from Québec City, he now lives in Toronto, Canada.