As members of the maker community, we are always looking for creative ways to use hardware and software. This time, Patrick Lima and I decided we wanted to expand the Raspberry Pi's ports using an Arduino board, so we could access more functionality and ports and add a layer of protection to the device. There are a lot of ways to use this setup, such as building a solar panel that follows the sun, a home weather station, joystick interaction, and more.
We decided to start by building a dashboard that allows the following serial port interactions:
- Control three LEDs to turn them on and off
- Control three LEDs to adjust their light intensity
- Identify which ports are being used
- Show input movements on a joystick
- Measure temperature
We also want to show all the interactions between ports, hardware, and sensors in a nice user interface (UI) like this:
You can use the concepts in this article to build many different projects that use many different components. Your imagination is the limit!
1. Get started
The first step is to expand the Raspberry Pi's ports to also use Arduino ports. This is possible using Linux ARM's native serial communication implementation that enables you to use an Arduino's digital, analogical, and Pulse Width Modulation (PWM) ports to run an application on the Raspberry Pi.
This project uses TotalCross, an open source software development kit for building UIs for embedded devices, to execute external applications through the terminal and use the native serial communication. There are two classes you can use to achieve this: Runtime.exec and PortConnector. They represent different ways to execute these actions, so we will show how to use both in this tutorial, and you can decide which way is best for you.
To start this project, you need:
- 1 Raspberry Pi 3
- 1 Arduino Uno
- 3 LEDs
- 2 resistors between 1K and 2.2K ohms
- 1 push button
- 1 potentiometer between 1K and 50K ohms
- 1 protoboard (aka breadboard)
- Jumpers
2. Set up the Arduino
Create a communication protocol to receive messages, process them, execute the request, and send a response between the Raspberry Pi and the Arduino. This is done on the Arduino.
2.1 Define the message format
Every message received will have the following format:
- Indication of the function called
- Port used
- A char separator, if needed
- A value to be sent, if needed
- Indication of the message's end
The following table presents the list of characters with their respective functions, example values, and descriptions of the example. The choice of characters used in this example is arbitrary and can be changed anytime.
Characters | Function | Example | Description of the example |
---|---|---|---|
* | End of the instruction | - | - |
, | Separator | - | - |
# | Set mode | #8,0* | Pin 8 input mode |
< | Set digital value | <1,0* | Set pin 1 low |
> | Get digital value | >13* | Get value pin 13 |
+ | Get PWM value | +6,250* | Set pin 6 value 250 |
- | Get analogic value | -14* | Get value pin A0 |
2.2 Source code
The following source code implements the communication protocol specified above. It must be sent to the Arduino, so it can interpret and execute messages' commands:
void setup() {
Serial.begin(9600);
Serial.println("Connected");
Serial.println("Waiting command...");
}
void loop() {
String text="";
char character;
String pin="";
String value="0";
char separator='.';
char inst='.';
while(Serial.available()){ // verify RX is getting data
delay(10);
character= Serial.read();
if(character=='*'){
action(inst,pin,value);
break;
}
else {
text.concat(character);}
if(character==',') {
separator=character;
if(inst=='.'){
inst = character;}
else if(separator!=',' && character!=inst ){
pin.concat(character);}
else if (character!=separator && character!=inst ){
value.concat(character);}
}
}
void action(char instruction, String pin, String value){
if (instruction=='#'){//pinMode
pinMode(pin.toInt(),value.toInt());
}
if (instruction=='<'){//digitalWrite
digitalWrite(pin.toInt(),value.toInt());
}
if (instruction=='>'){ //digitalRead
String aux= pin+':'+String(digitalRead(pin.toInt()));
Serial.println(aux);
}
if (instruction=='+'){ // analogWrite = PWM
analogWrite(pin.toInt(),value.toInt());
}
if (instruction=='-'){ // analogRead
String aux= pin+':'+String(analogRead(pin.toInt()));
Serial.println(aux);
}
}
2.3 Build the electronics
Define what you need to test to check communication with the Arduino and ensure the inputs and outputs are responding as expected:
- LEDs are connected with positive logic. Connect to the GND pin through a resistor and activate it with the digital port I/O 2 and PWM 3.
- The button has a pull-down resistor connected to the digital port I/O 4, which sends a signal of 0 if not pressed and 1 if pressed.
- The potentiometer is connected with the central pin to the analog input A0 with one of the side pins on the positive and the other on the negative.
2.4 Test communications
Send the code in section 2.2 to the Arduino. Open the serial monitor and check the communication protocol by sending the commands below:
#2,1*<2,1*>2*
#3,1*+3,10*
#4,0*>4*
#14,0*-14*
This should be the result in the serial monitor:
One LED on the device should be on at maximum intensity and the other at a lower intensity.
Pressing the button and changing the position of the potentiometer when sending reading commands will display different values. For example, turn the potentiometer to the positive side and press the button. With the button still pressed, send the commands:
>4*
-14*
Two lines should appear:
3. Set up the Raspberry Pi
Use a Raspberry Pi to access the serial port via the terminal using the cat
command to read the entries and the echo
command to send the message.
3.1 Do a serial test
Connect the Arduino to one of the USB ports on the Raspberry Pi, open the terminal, and execute this command:
cat /dev/ttyUSB0 9600
This will initiate the connection with the Arduino and display what is returned to the serial.
To test sending commands, open a new terminal window (keeping the previous one open), and send this command:
echo "command" > /dev/ttyUSB0 9600
You can send the same commands used in section 2.4.
You should see feedback in the first terminal along with the same result you got in section 2.4:
4. Create the graphical user interface
The UI for this project will be simple, as the objective is just to show the ports expansion using the serial. Another article will use TotalCross to create a high-quality GUI for this project and start the application backend (working with sensors), as shown in the dashboard image at the top of this article.
This first part uses two UI components: a Listbox and an Edit. These build a connection between the Raspberry Pi and the Arduino and test that everything is working as expected.
Simulate the terminal where you put the commands and watch for answers:
- Edit is used to send messages. Place it at the bottom with a FILL width that extends the component to the entire width of the screen.
- Listbox is used to show results, e.g., in the terminal. Add it at the TOP position, starting at the LEFT side, with a width equal to Edit and a FIT height to vertically occupy all space not filled by Edit.
package com.totalcross.sample.serial;
import totalcross.sys.Settings;
import totalcross.ui.Edit;
import totalcross.ui.ListBox;
import totalcross.ui.MainWindow;
import totalcross.ui.gfx.Color;
public class SerialSample extends MainWindow {
ListBox Output;
Edit Input;
public SerialSample() {
setUIStyle(Settings.MATERIAL_UI);
}
@Override
public void initUI() {
Input = new Edit();
add(Input, LEFT, BOTTOM, FILL, PREFERRED);
Output = new ListBox();
Output.setBackForeColors(Color.BLACK, Color.WHITE);
add(Output, LEFT, TOP, FILL, FIT);
}
}
It should look like this:
5. Set up serial communication
As stated above, there are two ways to set up serial communication: Runtime.exec and PortConnector.
5.1 Option 1: Use Runtime.exec
The java.lang.Runtime
class allows the application to create a connection interface with the environment where it is running. It allows the program to use the Raspberry Pi's native serial communication.
Use the same commands you used in section 3.1, but now use the Edit component on the UI to send the commands to the device.
Read the serial
The application must constantly read the serial, and if a value is returned, add it to the Listbox using threads. Threads are a great way to work with processes in the background without blocking user interaction.
The following code creates a new process on this thread that executes the cat
command, tests the serial, and starts an infinite loop to check if something new is received. If something is received, the value is added to the next line of the Listbox component. This process will continue to run as long as the application is running:
new Thread () {
@Override
public void run() {
try {
Process Runexec2 = Runtime.getRuntime().exec("cat /dev/ttyUSB0 9600\n");
LineReader lineReader = new LineReader(Stream.asStream(Runexec2.getInputStream()));
String input;
while (true) {
if ((input = lineReader.readLine()) != null) {
Output.add(input);
Output.selectLast();
Output.repaintNow();
}
}
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
}.start();
}
Send commands
Sending commands is a simpler process. It happens whenever you press Enter on the Edit component.
To forward the commands to the device, as shown in section 3.1, you must instantiate a new terminal. For that, the Runtime class must execute a sh
command on Linux:
try{
Runexec = Runtime.getRuntime().exec("sh").getOutputStream() }catch (IOException ioe) {
ioe.printStackTrace();
}
After the user writes the command in Edit and presses Enter, the application triggers an event that executes the echo
command with the value indicated in Edit:
Input.addKeyListener(new KeyListener() {
@Override
public void specialkeyPressed(KeyEvent e) {
if (e.key == SpecialKeys.ENTER) {
String s = Input.getText();
Input.clear();
try {
Runexec.write(("echo \"" + s + "\" > /dev/ttyUSB0 9600\n").getBytes());
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
}
@Override
public void keyPressed(KeyEvent e) {} //auto-generate code
@Override
public void actionkeyPressed(KeyEvent e) {} //auto-generate code
});
Run the application on the Raspberry Pi with the Arduino connected and send the commands for testing. The result should be:
Runtime.exec source code
Following is the source code with all parts explained. It includes the thread that will read the serial on line 31 and the KeyListener
that will send the commands on line 55:
package com.totalcross.sample.serial;
import totalcross.ui.MainWindow;
import totalcross.ui.event.KeyEvent;
import totalcross.ui.event.KeyListener;
import totalcross.ui.gfx.Color;
import totalcross.ui.Edit;
import totalcross.ui.ListBox;
import java.io.IOException;
import java.io.OutputStream;
import totalcross.io.LineReader;
import totalcross.io.Stream;
import totalcross.sys.Settings;
import totalcross.sys.SpecialKeys;
public class SerialSample extends MainWindow {
OutputStream Runexec;
ListBox Output;
public SerialSample() {
setUIStyle(Settings.MATERIAL_UI);
}
@Override
public void initUI() {
Edit Input = new Edit();
add(Input, LEFT, BOTTOM, FILL, PREFERRED);
Output = new ListBox();
Output.setBackForeColors(Color.BLACK, Color.WHITE);
add(Output, LEFT, TOP, FILL, FIT);
new Thread() {
@Override
public void run() {
try {
Process Runexec2 = Runtime.getRuntime().exec("cat /dev/ttyUSB0 9600\n");
LineReader lineReader = new
LineReader(Stream.asStream(Runexec2.getInputStream()));
String input;
while (true) {
if ((input = lineReader.readLine()) != null) {
Output.add(input);
Output.selectLast();
Output.repaintNow();
}
}
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
}.start();
try {
Runexec = Runtime.getRuntime().exec("sh").getOutputStream();
} catch (IOException ioe) {
ioe.printStackTrace();
}
Input.addKeyListener(new KeyListener() {
@Override
public void specialkeyPressed(KeyEvent e) {
if (e.key == SpecialKeys.ENTER) {
String s = Input.getText();
Input.clear();
try {
Runexec.write(("echo \"" + s + "\" > /dev/ttyUSB0 9600\n").getBytes());
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
}
@Override
public void keyPressed(KeyEvent e) {
}
@Override
public void actionkeyPressed(KeyEvent e) {
}
});
}
}
5.2 Option 2: Use PortConnector
PortConnector is specifically for working with serial communication. If you want to follow the original example, you can skip this section, as the intention here is to show another, easier way to work with serial.
Change the original source code to work with PortConnector:
package com.totalcross.sample.serial;
import totalcross.io.LineReader;
import totalcross.io.device.PortConnector;
import totalcross.sys.Settings;
import totalcross.sys.SpecialKeys;
import totalcross.ui.Edit;
import totalcross.ui.ListBox;
import totalcross.ui.MainWindow;
import totalcross.ui.event.KeyEvent;
import totalcross.ui.event.KeyListener;
import totalcross.ui.gfx.Color;
public class SerialSample extends MainWindow {
PortConnector pc;
ListBox Output;
public SerialSample() {
setUIStyle(Settings.MATERIAL_UI);
}
@Override
public void initUI() {
Edit Input = new Edit();
add(Input, LEFT, BOTTOM, FILL, PREFERRED);
Output = new ListBox();
Output.setBackForeColors(Color.BLACK, Color.WHITE);
add(Output, LEFT, TOP, FILL, FIT);
new Thread() {
@Override
public void run() {
try {
pc = new PortConnector(PortConnector.USB, 9600);
LineReader lineReader = new LineReader(pc);
String input;
while (true) {
if ((input = lineReader.readLine()) != null) {
Output.add(input);
Output.selectLast();
Output.repaintNow();
}
}
} catch (totalcross.io.IOException ioe) {
ioe.printStackTrace();
}
}
}.start();
Input.addKeyListener(new KeyListener() {
@Override
public void specialkeyPressed(KeyEvent e) {
if (e.key == SpecialKeys.ENTER) {
String s = Input.getText();
Input.clear();
try {
pc.writeBytes(s);
} catch (totalcross.io.IOException ioe) {
ioe.printStackTrace();
}
}
}
@Override
public void keyPressed(KeyEvent e) {
}
@Override
public void actionkeyPressed(KeyEvent e) {
}
});
}
}
You can find all the code in the project's repository.
6. Next steps
This article shows how to use Raspberry Pi serial ports with Java by using either the Runtime or PortConnector classes. You can also call external files in other languages and create countless other projects—like a water quality monitoring system for an aquarium with temperature measurement via the analog inputs, or a chicken brooder with temperature and humidity regulation and a servo motor to rotate the eggs.
A future article will use the PortConnector implementation (because it is focused on serial connection) to finish the communications with all sensors. It will also add a digital input and complete the UI.
Here are some references for more reading:
- Get started with TotalCross
- TotalCross PortConnector class
- Running C++ applications with TotalCross
- VSCode TotalCross Project Extension plugin
After you connect your Arduino and Raspberry Pi, please leave comments below with your results. We'd love to read them!
1 Comment