SSD1306 on the NEO Plus2

You might recall some time ago I wrote about using Python on the Orange Pi Zero etc to run the little SSD1306-based displays.

Despite that being successful I did have a nagging doubt about the LUMA library because later on – when doing some apt-get upgrades I got a segmentation error which I’d originally attributed to using a hard disk with the device. I now think it might be something to do with that library. Well, when my NEO PLUS2 arrived I thought I’d try again..

WELL!!! This version works and also works with the NanoPi AIR AND the NEO2 using the standard Ubuntu image (and has solved my problem with the hard drive on the NEO2)…

I followed the instructions for loading up the Luma code… but missed out the line about installing LUMA as it had caused me problems on NEO2 boards in the past – segmentation errors and now just a point blank refusal to load up.

sudo apt-get install python-dev python-pip libfreetype6-dev libjpeg-dev
sudo -H pip install –upgrade pip
sudo apt-get purge python-pip
#sudo -H pip install –upgrade luma.oled

I replaced that last line with this – which also conveniently loads some other needed stuff like Pillow

sudo pip install –upgrade ssd1306

I’m reliably informed that the LUMA library is later than this – but for me right now – I’ve had no joy with it.

At this point I noted a comment about something being left around and so..

sudo apt-get autoremove

Of course  my original example would fail as the luma libraries were not present – but amazingly all I had to do was replace three lines…

from oled.serial import i2c

from oled.device import ssd1306

from oled.render import canvas

These lines previously referred to the Luma library.  with those changes everything worked for my little SSD1306 display other than the core temperature of the processor showing as being hotter than the sun…  I put a suitable division into the code and I was up and running. I’ll put the full code below as apples now to the NEO PLUS2

I recalled I was using some font material from the LUMA examples – but as it turns out – all I needed was the one font which you’ll see I put in my /home/pi folder under fonts – I just grabbed the lot from the examples folder (NOT to be confused with the LUMA examples.

I ran the code-  did an apt-get update and apt-get upgrade – no problems at all – so this is now part of my standard setup I’m using for the Neo Plus2. The code came from here (thank you) and there are a ton of examples to play with.

My little test program works as…

sudo python

if you want to leave it running and get on with something else… stick an ampersand on the end! You will see a number appear and you can use that with “sudo kill” to kill the process (probably not cleanly).

I have since ALSO tested this exact setup on the NANO AIR – with exactly the same successful results.

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

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

global line1
global line2
global line3
global line4
global line5
global line6
global col1
def do_nothing(obj):
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, height=32)
device.cleanup = do_nothing
myfont = make_font("/home/pi/fonts/ProggyTiny.ttf", 16)
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)
    >>> bytes2human(100001221)
    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/1000))
def mem_usage():
    usage = psutil.virtual_memory()
    return "MEM FREE: %s/%s" \
        % (bytes2human(usage.available), bytes2human(
def disk_usage(dir):
    usage = psutil.disk_usage(dir)
    return "DSK FREE: %s/%s" \
        % (bytes2human(, bytes2human(
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(
    # 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 NEO PLUS2', font=myfont, fill=255)
            draw.text((col1, line3), 'Starting up...', font=myfont, fill=255)
        elif looper==1:
            draw.text((col1, line1), cpu_usage(),  font=myfont, fill=255)
            draw.text((col1, line2), cpu_temperature(),  font=myfont, fill=255)
        elif looper==2:
            draw.text((col1, line1), mem_usage(),  font=myfont, fill=255)
            draw.text((col1, line2), disk_usage('/'),  font=myfont, fill=255)
        elif looper==3:
            draw.text((col1, line1),"%s %s" % (platform.system(),platform.release()), font=myfont, fill=255)
            draw.text((col1, line2), lan_ip(),  font=myfont, fill=255)
            uptime = - datetime.fromtimestamp(psutil.boot_time())
            draw.text((col1, line1),str('%a %b %d %H:%M:%S')), font=myfont, fill=255)
            draw.text((col1, line2),"%s" % str(uptime).split('.')[0], font=myfont, fill=255)
def main():
    global looper
    looper = 0
    while True:
        if looper==0:
if __name__ == "__main__":
    except KeyboardInterrupt:


Update: you CAN install LUMA on a NEO2 which only has 512MB RAM by doing this…

sudo -H pip install –no-cache-dir –upgrade luma.oled
sudo -H pip install –upgrade pip setuptools
git clone
cd luma.examples
sudo -H pip install -e .

An error referring to SDL-config appeared at the end of this… but seemed to have no ill effect..  I took my usual system display example, made sure the font referred to path /home/pi/luma.examples/examples/fonts   – and ran it – no problem at all.  If someone knows the meaning of that SDL stuff – and a fix? Let me know.

I did see this –    and tried the compile there but that griped that it could not guess build type – and stopped! Oh, well.

You should know that LUMA is later than the SSD1306 code.. you were warned if you use the older (simpler) setup…  I have the LUMA code running on a NEO2 but not tested it long enough to verify it won’t cause the old segmentation issue I experienced in the past.

Incidentally for those who don’t use Linux a lot – my little test program runs as “sudo python” –  if you add a space then an ampersand to the end of that – it’ll keep running in the background and return to the prompt to let you do other stuff.

I’ve also tested that latest version  LUMA on a NEO PLUS2 successfully. Between this and the new Node-Red node for handling I2c… things are moving along nicely.

Ultimately I came to the conclusion that an old version of libc.bin seemed to be the problem with the NEO2 as once updated to a later version – Luma installed file… in which case..

sudo apt-get install python-dev python-pip libfreetype6-dev libjpeg-dev
sudo -H pip install –upgrade pip
#sudo apt-get purge python-pip
sudo pip install setuptools
sudo pip install psutil
sudo -H pip install –upgrade luma.oled

At that point I could get my little OLED display to work no problem, turns out the examples were not needed at all.


19 thoughts on “SSD1306 on the NEO Plus2

  1. Hi,

    where are you getting a standard ubuntu image for the neo plus 2? The only image they have on their site is an ubuntu core image.


    1. Ubuntu core – that’s the one I used. Didn’t see the point of fully blown Ubuntu when there is no HDMI output..

      1. how do you get apt-get to work on ubuntu core? I thought you have to use snappy. That’s my main concern with it, I want to install rabbitMQ and a bunch of other stuff and they’re not available as a snappy package


        1. No, apt-get works a treat – indeed if you look at “the script” there are very few differences between Debian and Ubuntucore (if you’re not interested in a graphical environment) which is how we managed to get one script to handle installations of Mosquitto MQTT, Node-Red, MC, Grafana, Sqlite, InfluxDB and a host of other bits and pieces. I’ve never used Snappy (in fact come to think of it, I’ve never heard of it until now).

  2. I have to say, this does NOT work on the NanoPi S2 (latest Ubuntu)- indeed their own setup program won’t let you alter I2c and i2cdetect is seeing the board but marked as UU. Not impressed.

  3. If you want some simple C code to talk to a SSD1306 display without needing root privilege, you can use my project:

    It uses the Linux I2C “file” driver (/dev/i2c-x), so doesn’t need any other libraries installed to work. It includes a sample program to show you how to use it.

    1. Funny you should say that I’ve been talking to a fellow who has a file based solution for GPIO for Node-Red and we just tested it on the NanoPi Air tonight – I’ve learned a lot from that as to how to get to the actual port number etc… I’m all sorted with the SSD1306 on my various devices – using Python to talk to the SSD1306 from Node-Red – but will look at your code now.

      1. I updated my SPI_LCD project ( to use 4 possible methods to communicate over SPI: Linux driver (/dev/spidev), PIGPIO, wiringPi and bcm2835. The PIGPIO driver is by far the fastest since it talks directly to the SPI hardware. The nice thing about the /dev/spidev interface is that it works on all boards which implement that interface (E.g. NanoPi). I added support for translating GPIO numbers from the NanoPi 2 board to allow easy access to GPIO for simple input/output. If you look in the NanoPi github (matrix project), you can see the lookup tables for their supported boards that translates the header pin numbers into GPIO numbers suitable for the /sys/class/gpio driver.

    2. I do note that you say it has only been tested on RPI3 – the RPIs don’t concern me as there is plenty of support – I hope I’ll have time to test your code on some of the many NON-PI boards which really do need that extra support.

    3. Wow that looks a lot easier than I was expecting – I’d like to try that in Python or something else interactive – so – are you saying you can do that file I2c writing – which looks straightforward, WITHOUT being root?

      1. I’m not an authority on Linux, so it may be how the system is configured which determines if SPI/I2C can operate as root or user. One trick that you can do is just give your executable root privilege so that you can start it without sudo:

        sudo chown root:root my_program
        sudo chmod 4755 my_program

  4. This conversation brings to mind a section of the Kernighan and Ritchie text I was taught Unix system management from back in the early 80’s. It said that when a child process died it was turned into a zombie and the parent was notified so that it could clean up the remains. How could you not love an opsys that was described so brutally.

    1. I’ve worked on some Unix systems where the -15 doesn’t always work. I find using:
      kill -s SIGHUP n
      kill -s SIGTERM n
      work. But I’ve also worked on systems where the -15 and -9 were all you had. 🙂

      1. Oops, hehe, I think SIGHUP used to be 15 (I see now it’s 1). SIGTERM is 15, SIGHUP is 1. And yes, I’d hup the app then kill the app if that failed. I’ve seen SIGHUP and SIGUSR1 both used for reloading an applications configs.

Comments are closed.