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.
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.
Here's what Java program will do while interacting with the Firmata code running on the Arduino's ATMEGA328 processor:
- First, the Java program tells the Arduino's ATMEGA chip to configure the I2C connection. (0x78)
- Second, the Java program will send, over USB, a command to tell the ATMEGA to request the ID from the sensor.
- Then, the ATMEGA238 processor will send the command "0xD0" to address "0x77" on the I2C bus.
- Then, the Java program will send a Firmata message, over USB, to ask that the ATMEGA processor to wait for a response.
- 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:
The listener for the I2C is coded as follows:
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.
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 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):
Within Java, we executed this using the tell() method, as follows, where BMP280_ID_ASK is a constant set to 0xD0:
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 sensor then detects that the ATMEGA has given up control of the I2C bus and then responds by saying that its ID is 0x58:
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:
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
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:
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:
- List of relevant codes used by FIrmata4j: FirmataToken.java (on GitHub)
- Firmata Feature Registry: https://github.com/firmata/protocol/blob/master/feature-registry.md
- Firmata I2C messages: https://github.com/firmata/protocol/blob/master/i2c.md
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.