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…

[pcsh lang=”python” tab_size=”4″ message=”” hl_lines=”” provider=”manual”]

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

[/pcsh]

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

[pcsh lang=”python” tab_size=”4″ message=”” hl_lines=”” provider=”manual”]

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

[/pcsh]

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.

16 thoughts on “SSD1306 with Python

  1. Hello, any idea how to avoid typing sudo, when running python scripts, which uses luma libraries? I already ran sudo usermod -a -G i2c pi, but python without sudo ends up on permissions to dev/i2c-0… :-/

    thank you

    1. I feel your pain. Why we have to put up with this security crap on “educational” machines is beyond me. There is only me using the PIs in here and all outside access is via a VPN. It would make far more sense to me if everything was owned by user PI.

  2. Hi Peter,

    thank you for your comment, I eventually got it working after hours of fiddling 🙂
    Looks like some required Modules were disable by default.

    I’m using the oled to display temperature and CPU usage for all of the workers in my Opi cluster, atm it’s just a placeholder, until my new PSU arrives.

    If i had known earlier how few documentation there is on those chinese clones, i would have built it out of Raspberries instead.

    I also tried using an SSD1331 because i wanted the bar indicators to range from green to red, however i’m getting I/O errors from the luma library.

    I’m really enjoying the content on your blog so far 🙂

    Cheers

    1. Depends on the clones- FriendlyArm boards have some pretty good documentation.

      That display looks excellent !!!

  3. Greetings,

    I’m trying to get an ssd1306 working with my OrangePiOne, to no avail unfortunately.
    I have some experience with this particular oled, however it only ever worked for me on my Raspi3.

    I2cdetect doesnt show anything, even with other devices wired up instead.
    I’m running an Armbian image with legacy kernel.

    Did you ever get it working on an OpiOne?

    1. Hi there

      Well as you can probably see in the various blog entries, I’ve never tested an OrangePiOne – here’s hoping someone who has can answer the question. I can assure you they work on RPI2 and 3 – check out elsewhere on the blog for info on this. This may be a stupid comment – but make sure you have pullup resistors – I’m not entirely sure the Pi3 has them – also – make sure you’re using the right I2c (0 or 1) – different boards I’ve tested use a different I2c channel .. . though if other devices show up that tends to suggest you’re using the right one.

  4. 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?

    1. OLED needs +5v, ground and then you have the two lines for data and clock – which need 2k2 pullups depending on your hardware setup, to +3v3

  5. 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.

Comments are closed.