SSD1306 with Python

ssd1306Following on from earlier articles – this is as much a collection of notes than anything else – and there’s a demo video in here of the SD1306 using the Luma library on the Orange Pi Zero.   After months of thinking the only SBC I’d get working with I2c was the Raspberry Pi it transpires that I was miles out and more and more boards are succumbing to cheap display nirvana.

In a previous blog entry I covered the FriendlyArm NEO 2 – today I’m working on the Orange Pi Zero.  That took a little more doing…

So having installed DIETPI on the Orange Pi Zero Zero to at LAST seeing it working utterly reliably (regular viewers may know that the Pi Zero WIFI is not the best) I installed my script – and included WiringOP for the H3.  I hooked an SSD1306 to ground, 5v, SDA and SCK  and the first thing was to check that the board recognised the I2c device on port 3C.

sudo gpio i2cd

Nothing – i2cdetect was missing.

sudo apt-get install i2c-tools

That sorted that and indeed I could see the device was present DESPITE the relevant boot file suggesting I2c was turned off.

Good start – so I loaded up the relevant library… simple instructions are on the INSTALLATION page.

I then grabbed the EXAMPLES…   no point in re-inventing the wheel.  Of course running the examples proved nothing – because like the NEO, the I2c on the Pi Zero does not run on “port 1” but port 0 – so you have to suffix commands with –i2c-port 0

Here’s a working example:

sudo python luma.examples/examples/font_awesome.py --i2c-port 0

With that little bit at the end  - the I2c display works perfectly (don’t forget pullup resistors – I used 2k2).

Ah, but – whenever the demos ended, the display went blank – apparently this is default behaviour – so here is an example I put together and in RED you will see the bits I added to STOP this happening. Fairly self-explanatory once it is sitting in front of you. Took me ages to find out how.

from luma.core.interface.serial import i2c
from luma.core.render import canvas
from luma.oled.device import ssd1306

def do_nothing(obj):
pass

serial = i2c(port=0, address=0x3C)
device = ssd1306(serial)
device.cleanup = do_nothing

with canvas(device) as draw:
draw.rectangle(device.bounding_box, outline="white", fill="black")
draw.text((30, 40), "Hello World", fill="white")

The above works and as you can see there are only 2 lines of code other than setup! It doesn’t get any easier.

Now – this is IMPORTANT – if you purchased the little 32-line displays, you may notice that although the use the same chip – the output looks somewhat SQUASHED. I went all over the forums trying to find out how to get around this and eventually ended up looking at the source code in “device.py” – I noticed there was an automatic initialisation including height and width when using this line..

device = ssd1306(serial)

WELL, being one to experiment I tried this variation to see if I could force that 64 value to 32 and to see if it would make any difference.

device = ssd1306(serial, height=32)

Problem solved, the light blue 32-line display looks LOVELY!!!! I can now squeeze 3 lines of text and a nice border into it!

If you go back over my previous blog entry about putting useful info onto the OLED display – it covers this pretty well so expanding the above to do what you want should not be an issue.

On the Orange Pi Zero using DietPi, in order to put my own login information, I had to comment out in root/.bashrc the login line.

I’ve also made a change to my start-up screen (see previous blog) as it turned out that not all devices produce the IP address from the way I was doing it. Here’s a new version.

def lan_ip():
#f = os.popen('ifconfig eth0 | grep "inet\ addr" | cut -c 21-33')
f = os.popen("ip route get 1 | awk '{print $NF;exit}'")
ip = str(f.read())
# strip out trailing chars for cleaner output
return "%s" % ip.rstrip('\r\n').rstrip(' ')
I also found on the Pi Zero that the temperature display showed zero – as noted by one of our readers – I took out the divide by 1000 and it works a treat.

And before anyone says anything – yes, I know compiled C is faster and better etc., but the interpreter approach – IF you have a fast enough computer, certainly makes experimenting more interesting.   I’ve been thinking that I could put up messages on to the OLED from time to time and the overhead for each message really is not a lot.

 

Alternatively to put up messages regularly – one could use CHRON jobs – which I understand but never understood how easy they were to use until I stumbled on THIS site.

If you are interested in other display for the Orange Pi Zero, check out this fellow’s video.

If you’ve read my previous blog entries on the Orange Pi Zero – I should take this opportunity to say that up to now over a period of days, the WIFI continues to operate perfectly. This particular setup using DietPi seems to be good. The board does get warm however and I’m looking for a suitable heatsink.

Here is the startup code I'm using right now (not guaranteed to last) on the Orange Pi Zero at startup and in my CLS command...

import time
import os
import psutil
import platform
from datetime import datetime

def get_ip_address(ifname):
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    return socket.inet_ntoa(fcntl.ioctl(
        s.fileno(),
        0x8915,  # SIOCGIFADDR
        struct.pack('256s', ifname[:15])
    )[20:24])
 
byteunits = ('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB')
def filesizeformat(value):
    exponent = int(log(value, 1024))
    return "%.1f %s" % (float(value) / pow(1024, exponent), byteunits[exponent])
 
def bytes2human(n):
    """
    >>> bytes2human(10000)
    '9K'
    >>> bytes2human(100001221)
    '95M'
    """
    symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y')
    prefix = {}
    for i, s in enumerate(symbols):
        prefix[s] = 1 << (i + 1) * 10
    for s in reversed(symbols):
        if n >= prefix[s]:
            value = int(float(n) / prefix[s])
            return '%s%s' % (value, s)
    return "%sB" % n
  
def cpu_usage():
    # load average, uptime
    av1, av2, av3 = os.getloadavg()
    return "%.1f %.1f %.1f" \
        % (av1, av2, av3)
 
def cpu_temperature():
    tempC = ((int(open('/sys/class/thermal/thermal_zone0/temp').read())))
    return "%sc" \
        % (str(tempC))
 
def mem_usage():
    usage = psutil.virtual_memory()
    return "%s/%s" \
        % (bytes2human(usage.available), bytes2human(usage.total))
  
def disk_usage(dir):
    usage = psutil.disk_usage(dir)
    return " %s/%s" \
        % (bytes2human(usage.total-usage.used), bytes2human(usage.total))
  
def network(iface):
    stat = psutil.net_io_counters(pernic=True)[iface]
    return "%s: Tx%s, Rx%s" % \
           (iface, bytes2human(stat.bytes_sent), bytes2human(stat.bytes_recv))
  
def lan_ip():
    #f = os.popen('ifconfig eth0 | grep "inet\ addr" | cut -c 21-33')
    f = os.popen("ip route get 1 | awk '{print $NF;exit}'")
    ip = str(f.read())
    # strip out trailing chars for cleaner output
    return "%s" % ip.rstrip('\r\n').rstrip(' ')
	

Normal = '\033[0m'

# High Intensity
IGreen='\033[0;92m'       # Green
IYellow='\033[0;93m'      # Yellow
IBlue='\033[0;94m'        # Blue
ICyan='\033[0;96m'        # Cyan
IWhite='\033[0;97m'       # White

# Bold High Intensity
BIRed='\033[1;91m'        # Red
BIGreen='\033[1;92m'      # Green
BIYellow='\033[1;93m'     # Yellow
BIPurple='\033[1;95m'     # Purple
BIMagenta='\033[1;95m'    # Purple
BICyan='\033[1;96m'       # Cyan
BIWhite='\033[1;97m'      # White

uptime = datetime.now() - datetime.fromtimestamp(psutil.boot_time())

os.system('cls' if os.name == 'nt' else 'clear')
print IBlue + "____________________________________________________________________________\n" + Normal
print " Platform: " + BIPurple + "%s" % (platform.platform()) + Normal 
print " CPU Usage: " + BIRed + cpu_usage() + Normal + "\t\tCPU Temperature: " + BIRed + cpu_temperature() + Normal
print " Memory Free: " + BIRed + mem_usage() + Normal + "\t\tDisk Free: " + BIRed + disk_usage('/') + Normal
print " IP Address: " + BIRed + lan_ip() + Normal + "\tUptime: " + BIRed + "%s" % str(uptime).split('.')[0]  + Normal
bum=psutil.cpu_freq(0)
#print " Current CPU speed: " + BIRed + "%d" % int(bum.current) + "Mhz" + Normal + "\tmin: " + BIRed + "%d" % int(bum.min) + "Mhz" + Normal + " max: " + BIRed + "%d" % int(bum.max) + "Mhz" + Normal
#print " Time/Date: " + IGreen + str(datetime.now().strftime('%a %b %d at %H:%M:%S')) + Normal
print " Current CPU speed: " + BIRed + "%d" % int(bum.current) + "Mhz" + Normal + "\t" + IGreen + str(datetime.now().strftime('%a %b %d at %H:%M:%S')) + Normal
print IBlue + "____________________________________________________________________________\n" + Normal

and here's the code I use in the OLED for the same board..

import os
import psutil
import platform
from luma.core.interface.serial import i2c
from luma.core.render import canvas
from luma.oled.device import ssd1306
from PIL import ImageFont
import time
from datetime import datetime

global width
width=128
global height
height=64

global line1
line1=2
global line2
line2=11
global line3
line3=20
global line4
line4=29
global line5
line5=38
global line6
line6=47
global col1
col1=4


def do_nothing(obj):
    pass

def make_font(name, size):
    font_path = os.path.abspath(os.path.join(
        os.path.dirname(__file__), 'fonts', name))
    return ImageFont.truetype(font_path, size)


# rev.1 users set port=0
# substitute spi(device=0, port=0) below if using that interface
serial = i2c(port=0, address=0x3C)
device = ssd1306(serial)
device.cleanup = do_nothing

font10 = make_font("/home/pi/luma.examples/examples/fonts/ProggyTiny.ttf", 16)

#with canvas(device) as draw:
#    draw.rectangle(device.bounding_box, outline="white", fill="black")
#    draw.text((4, 6), "Hello World", font=font, fill="white")

 
byteunits = ('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB')
def filesizeformat(value):
    exponent = int(log(value, 1024))
    return "%.1f %s" % (float(value) / pow(1024, exponent), byteunits[exponent])
 
def bytes2human(n):
    """
    >>> bytes2human(10000)
    '9K'
    >>> bytes2human(100001221)
    '95M'
    """
    symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y')
    prefix = {}
    for i, s in enumerate(symbols):
        prefix[s] = 1 << (i + 1) * 10
    for s in reversed(symbols):
        if n >= prefix[s]:
            value = int(float(n) / prefix[s])
            return '%s%s' % (value, s)
    return "%sB" % n
  
def cpu_usage():
    # load average, uptime
    av1, av2, av3 = os.getloadavg()
    tempC = ((int(open('/sys/class/thermal/thermal_zone0/temp').read()) / 1000))
    return "LOAD: %.1f %.1f %.1f" \
        % (av1, av2, av3)
 
def cpu_temperature():
    # load average, uptime
    av1, av2, av3 = os.getloadavg()
    tempC = ((int(open('/sys/class/thermal/thermal_zone0/temp').read())))
    return "CPU TEMP: %sc" \
        % (str(tempC))
 
def mem_usage():
    usage = psutil.virtual_memory()
    return "MEM FREE: %s/%s" \
        % (bytes2human(usage.available), bytes2human(usage.total))
  
def disk_usage(dir):
    usage = psutil.disk_usage(dir)
    return "DSK FREE: %s/%s" \
        % (bytes2human(usage.total-usage.used), bytes2human(usage.total))
  
def network(iface):
    stat = psutil.net_io_counters(pernic=True)[iface]
    return "NET: %s: Tx%s, Rx%s" % \
           (iface, bytes2human(stat.bytes_sent), bytes2human(stat.bytes_recv))
  
def lan_ip():
    #f = os.popen('ifconfig eth0 | grep "inet\ addr" | cut -c 21-33')
    f = os.popen("ip route get 1 | awk '{print $NF;exit}'")
    ip = str(f.read())
    # strip out trailing chars for cleaner output
    return "IP: %s" % ip.rstrip('\r\n').rstrip(' ')

def stats():
    global looper
    with canvas(device) as draw:
        draw.rectangle((0,0,127,31), outline="white", fill="black")
        if looper==0:
            draw.text((col1, line1), 'WELCOME TO OP ZERO', font=font10, fill=255)
            draw.text((col1, line3), 'Starting up...', font=font10, fill=255)
            looper=1
        elif looper==1:
            draw.text((col1, line1), cpu_usage(),  font=font10, fill=255)
            draw.text((col1, line2), cpu_temperature(),  font=font10, fill=255)
            looper=2
        elif looper==2:
            draw.text((col1, line1), mem_usage(),  font=font10, fill=255)
            draw.text((col1, line2), disk_usage('/'),  font=font10, fill=255)
            looper=3       
        elif looper==3:
            draw.text((col1, line1),"%s %s" % (platform.system(),platform.release()), font=font10, fill=255)
            draw.text((col1, line2), lan_ip(),  font=font10, fill=255)
            looper=4
        else:
            uptime = datetime.now() - datetime.fromtimestamp(psutil.boot_time())
            draw.text((col1, line1),str(datetime.now().strftime('%a %b %d %H:%M:%S')), font=font10, fill=255)
            draw.text((col1, line2),"%s" % str(uptime).split('.')[0], font=font10, fill=255)
            looper=1
        #oled.drawImage(image)
    
def main():
    global looper
    looper = 0
    while True:
        stats()
        if looper==0:
            time.sleep(10)
        else:
            time.sleep(5)
  
  
if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        pass

By a change of fonts I'm managing to get crystal-clear up to 6 lines on the 128*64 display including a white border. I'm waiting for the 128*32 displays to turn up as they'll fit better in my NAS case and that then should give me 3 lines plus border. It helps if you have good eyesight with these little boards.

Facebooktwittergoogle_pluspinterestlinkedin

5 thoughts on “SSD1306 with Python

  1. Could you provide the link of the mentioned "start-up screen (see previous blog)" and the script to produce the OrangePI Zero sytem information

    Thks

    1. The blog is now updated. I've provided the code I'm using for both the startup screen (and CLS command) and for the OLED. Don't expect support on this as by the time you read this, it may have changed - I'm still learning... but it's pretty good right now.

  2. Hi There,

    Great work you got here and thanks for sharing, I am also trying to setup my tiny LCD, Can you please also provide the wiring diagram for this?

Leave a Reply

Your email address will not be published. Required fields are marked *