December 1, 2016 · Python Arduino Hardware

Tiny Dashboard: A Short Python + Arduino Project

I built a mini Arduino project that prints out my PC's CPU and memory utilization stats on an LCD display. I used a Python client to send content into my Arduino board over USB.

Here it is in action:

I - The Components

The Components

  1. Arduino Uno
  2. LCD Screen (I'm using an LCM1602C module)
  3. USB cable
  4. A breadboard
  5. Jump wires
  6. 220-ohm resistor
  7. (Optional) 10k potentiometer - to control screen contrast

The Arduino Starter Kit (Recommendation)

All of the components I used came from this Arduino Starter Kit I purchased to ease myself into electronics. Getting started with electronics was a bit of a hassle for me. I had no idea what components to buy, or what I needed an Arduino for, so buying this kit was like buying a bucket of Lego pieces for me to play with.

It even came with a book of sample projects that can be done with the included components!

II - Setting up the Hardware

Our layout

The LCD module has 16 pins in total. From left to right, with the LCD screen upright and facing towards us, here's how we hook it up to the Arduino:

  • Pin 1: To GND
  • Pin 2: VCC (+5V)
  • Pin 3: Either to the 3rd pin of a potentiometer or GND, if you don't want adjustable contrast
  • Pin 4: Arduino, digital pin 12
  • Pin 5: GND
  • Pin 6: Arduino, digital pin 11
  • Pin 7-10: None
  • Pin 11: Arduino, pin 5
  • Pin 12: Arduino, pin 4
  • Pin 13: Arduino, pin 3
  • Pin 14: Arduino, pin 2
  • Pin 15: 220-ohm resistor, connected to VCC (+5V)
  • Pin 16: GND

Here's some more reference photos:

III - The Arduino Code

The complete code is already hosted on my Github page. On this post, however, I'll try to explain what is happening on each chunk.

Here it is, for those of us who are too lazy to go to Github:

#include <LiquidCrystal.h>

LiquidCrystal lcd(12, 11, 5, 4, 3, 2);

String line1 = "";
String line2 = "";

void printSerialLine(int line, String content){
  // Clear line
  lcd.setCursor(0, line);
  lcd.print("                ");

  // Display contents
  lcd.setCursor(0, line);
  lcd.print(content);
}

  
void setup(){
  Serial.begin(9600);
  
  // LCD Setup
  lcd.begin(16, 2);
  printSerialLine(0, "Waiting for");
  printSerialLine(1, "connection...");
}

void loop(){

  while(Serial.available()==0);
  
  // Discard everything until this marker
  Serial.readStringUntil('Clujlusjarr7');

  line1 = Serial.readStringUntil('\n');
  line2 = Serial.readStringUntil('\n');
  printSerialLine(0, line1);
  printSerialLine(1, line2);
}

Code Breakdown

First, we import and initialize an object on Arduino's built in LiquidCrystal library meant for controlling LCD screens like the one we have.

#include <LiquidCrystal.h>

LiquidCrystal lcd(12, 11, 5, 4, 3, 2);

Then, we initialize our strings that would contain each line of our message.

String line1 = "";
String line2 = "";

Next, I made a function that would use the LiquidCrystal library to clear the current content of a line and display the new content right after.

void printSerialLine(int line, String content){
    // Clear line
    lcd.setCursor(0, line);
    lcd.print("                ");

    // Display contents
    lcd.setCursor(0, line);
    lcd.print(content);
}

The setup sequence done in setup() is pretty straightforward. We open the serial stream, initialize the LCD display, and then print out "Waiting for connection..." -- if anything, the last part is for us to know that our hardware setup works.

void setup(){
    Serial.begin(9600);
  
    // LCD Setup
    lcd.begin(16, 2);
    printSerialLine(0, "Waiting for");
    printSerialLine(1, "connection...");
}

Here, I suggest you compile and load the script we've done so far onto your Arduino, fingers crossed, and check. It should display something like this:

Okay! If that went well, we proceed to building our main loop.

void loop(){

On each iteration, we start with waiting for any input from our serial port.

    while(Serial.available()==0);

To avoid garbage being displayed on the screen by unintentional connections to our serial port, we use a key (in our case, Clujlusjarr7) to tell our Arduino that the next 2 lines should be displayed on the screen.

The readStringUntil function from the Serial library is perfect for this, as it will discard any input from the serial buffer until it sees a specific string.

    Serial.readStringUntil('Clujlusjarr7');

When the script finally continues, meaning the readStringUntil function finally saw our token, we read the next two lines, assuming each line ends with a \n character. We then use our printSerialLine function to print both lines onto the screen.

    line1 = Serial.readStringUntil('\n');
    line2 = Serial.readStringUntil('\n');
    printSerialLine(0, line1);
    printSerialLine(1, line2);
}

There you have it! By now, we should have working Arduino code that displays 2 lines sent through USB.

IV - The Python Client

Next, I'll talk about the Python client we run on the PC. This component is responsible for reading our PC's system status every second and sending data to our Arduino via USB.

Requirements

Using the Installer

If you are feeling lazy and just want to get the thing to work, here's how you can install the client:

  1. Download the source code here.
  2. Extract the zip file.
  3. In your command line/terminal, go to the extracted directory.
  4. Run python setup.py install
  5. To run the client, type: pc-stats

Plug in your Arduino, the script should automatically attempt to connect as soon as it sees it!

Checking out the code

For those of you who are feeling adventurous and those who would like to get their hands dirty on the Python code, this section is for you. Much like our Arduino code, our Python client can also be found on Github.

Most of the important stuff can be found inside pc_stats/pc_stats.py -- we'll focus on this part.

import re
import time
import serial
from serial.tools import list_ports
import psutil
import click


START_MARKER = 'Clujlusjarr7'
RETRY_INTERVAL = 5


def displayToHardware(s, line1, line2=""):
    line1_str = "%s".ljust(16, ' ') % line1
    line2_str = "%s".ljust(16, ' ') % line2

    s.write(START_MARKER)
    s.write(line1_str + '\n')
    s.write(line2_str + '\n')


def attempt_connection():
    serial_port = None
    ports = list_ports.comports()
    r = re.compile(r'^Arduino*')

    # Guess serial port
    for port in ports:
        if r.match(port.description) is not None:
            print "\nDevice found (%s)! %s" % (port.description, port.device)
            serial_port = port.device

    if serial_port is None:
        raise "Cannot find a device"
    else:
        loop(serial_port)


def loop(serial_port):
    print "Connecting to %s..." % serial_port
    s = serial.Serial(serial_port)

    while(True):
        # CPU Percentage
        cpu_pct = psutil.cpu_percent(interval=1, percpu=False)

        # Memory Percentage
        mem = psutil.virtual_memory()
        mem_pct = mem.percent

        displayToHardware(s, "CPU: %s%%" % cpu_pct, "MEM: %s%%" % mem_pct)
        time.sleep(2)


@click.command()
def cli():
    while True:
        try:
            attempt_connection()
        except KeyboardInterrupt:
            print "\nBye!"
            break
        except:
            print "Connection failed! Retry in %s seconds..." % RETRY_INTERVAL
            time.sleep(RETRY_INTERVAL)


if __name__ == '__main__':
    cli()

Here's a short summary of what this script does:

  1. Automatically looks for and connects to an Arduino device.
  2. Checks and updates CPU and Memory usage statistics every 2 seconds.

Now, for the details.

First, we initialize all libraries we'll use within this script:

import re  
import time  
import serial  
from serial.tools import list_ports  
import psutil  
import click

Important things to note here:

  • serial is a library we use to communicate via USB
  • psutil is the library we use to fetch CPU and Memory usage details.
  • Click is a library I use for command line parameters. (This part is actually unnecessary at this point since we do not have parameters yet, but I set it up for future plans).

Okay, now we have some configuration variables:

START_MARKER = 'Clujlusjarr7'  
RETRY_INTERVAL = 5

Remember the token we specified as a start marker for our messages in the Arduino code? It should match the one we specify in START_MARKER.

RETRY_INTERVAL will control how often we try to look for and connect to an Arduino device.

This next chunk of code is our entrypoint:

@click.command()
def cli():
    while True:
        try:
            attempt_connection()
        except KeyboardInterrupt:
            print "\nBye!"
            break
        except:
            print "Connection failed! Retry in %s seconds..." % RETRY_INTERVAL
            time.sleep(RETRY_INTERVAL)


if __name__ == '__main__':
    cli()

It basically tries to connect to an Arduino, catches any error encountered, then tries to connect again after our RETRY_INTERVAL.

Next, lets take a look at the setup/attempt-connection chunk:

def attempt_connection():
    serial_port = None
    ports = list_ports.comports()
    r = re.compile(r'^Arduino*')

    # Guess serial port
    for port in ports:
        if r.match(port.description) is not None:
            print "\nDevice found (%s)! %s" % (port.description, port.device)
            serial_port = port.device

    if serial_port is None:
        raise "Cannot find a device"
    else:
        loop(serial_port)

It uses the list_ports function from the serial library to list out the names of connected devices. From those devices, try to get one with the name that starts with "Arduino" -- this part can be improved to look for a specific one.

If a valid port is found, we now trigger the main loop with loop(serial_port).

Let's check out loop():

def loop(serial_port):  
    print "Connecting to %s..." % serial_port
    s = serial.Serial(serial_port)

    while(True):
        # CPU Percentage
        cpu_pct = psutil.cpu_percent(interval=1, percpu=False)

        # Memory Percentage
        mem = psutil.virtual_memory()
        mem_pct = mem.percent

        displayToHardware(s, "CPU: %s%%" % cpu_pct, "MEM: %s%%" % mem_pct)
        time.sleep(2)

Here, we can see that we connect to the specified port using the Serial library

We enter the main loop, where we use the psutil library to fetch CPU and memory utilization percentages. We then build the lines we want to display and call our displayToHardware() function to send these lines to our Arduino.

Finally, let's look at how we send messages to the Arduino with the displayToHardware() function:

def displayToHardware(s, line1, line2=""):
    // Make sure the lines are 16 characters wide
    line1_str = "%s".ljust(16, ' ') % line1
    line2_str = "%s".ljust(16, ' ') % line2

    // Send messages via serial
    s.write(START_MARKER)
    s.write(line1_str + '\n')
    s.write(line2_str + '\n')

It is pretty straightforward. We make the two strings exactly 16 characters in length, which is the row length of the LCD display I am using. Then we send the start marker, to tell the Arduino to display the next two lines. Then we write the two lines.

We can run this code: python pc_stats.py. And watch our LED dashboard come to life!

V - Conclusion

That's it! We have successfully built a dashboard and client that would display CPU and Memory utilization on an LCD Display via a USB

We can stop here, although building a case for it would be nice. Maybe next time?