I have a few old Raspberry Pis lying around, including a Model B Rev 1 running Raspberry Pi OS Buster lite. I use it to serve up my simple MUD game. It has no wi-fi so it’s connected by ethernet to my router which also gives it power over USB. I really must write that up some time.
Anyway, I have a whole bunch of cheap, tiny 1306 i2c OLED 128×64 pixel displays so I thought it might be fun to see if I could use one as a little shell display so I could do some basic admin on the headless Pi locally, just with a USB keyboard.
It took way longer than I expected, especially given I found someone had already done this, but it works. Here’s how I did it. Please bear in mind… my Pi is very old, I’m using Buster Lite as my OS, my OLED display’s address is 0x3C, it’s set to auto login to a command prompt, no GUI – your mileage may well vary!
It’s based on this project: https://github.com/satoshinm/oledterm – however, that was written in Python 2, uses a newer Pi, uses an spi not i2c interface, and because the OLED display libraries now only work in Python 3, I couldn’t get it to work. Issue #4 on that repo was the key to solving this, but I thought I’d summarise how I got this to work.
First, I connected the display. GND on the display to GND on the Pi, VCC to +3.3v on the Pi, SDA to Raspberry Pi pin 3, SCL to Pi pin 5 – remember this is an old Raspberry Pi original model B!
I installed git and downloaded oledterm:
git clone https://github.com/satoshinm/oledterm
This wouldn’t run for various reasons – not least because I needed to install luma.core to drive the OLED display, and I needed to install pip to install that:
sudo apt install python3-pip
sudo -H pip3 install --upgrade luma.oled
Then I copied the Python 3 version of oledterm from here and saved it as a file called oledterm3.py
I then edited /etc/rc.local to add this:
sudo python3 /home/pi/oledterm/oledterm3.py --display ssd1306 --interface i2c --i2c-port 0 &
exit 0
I also edited go.sh the same way. Let me explain the options in more detail. My display type is set to ssd1306, this is a very common kind of small OLED display. If my display’s i2c address were not 0x3c, I’d have needed to add an option to change that here. I specify the interface as i2c, rather than SPI as used in oledterm, and because I have a very old Pi I need to specify the i2c port as 0. With a newer Pi you could probably omit –i2c-port, or set it to 1.
I then unplugged the HDMI display, and rebooted – and lo! I could just about see a tiny shell and use my USB keyboard to type instructions! I could even edit text in nano – just about! Who needs more than 31 columns and 9 rows of text, anyway!?
If you like this, you may also like my adventures with using OLED displays in Arduino-based TinyBASIC computers, a micro:bit pulse oximeter, air quality sensor, or playing ArduBoy games on a BBC micro:bit.
Python 3 version of oledterm by Krizzel87
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# based on:
# Copyright (c) 2014-17 Richard Hull and contributors
# See LICENSE.rst for details.
# PYTHON_ARGCOMPLETE_OK
import os
import time
import sys
import subprocess
from luma.core import cmdline
from luma.core.virtual import terminal
from PIL import ImageFont
VIRTUAL_TERMINAL_DEVICE = "/dev/vcsa"
ROWS = 9
COLS = 31
# based on demo_opts.py
from luma.core import cmdline, error
def get_device(actual_args=None):
"""
Create device from command-line arguments and return it.
"""
if actual_args is None:
actual_args = sys.argv[1:]
parser = cmdline.create_parser(description='luma.examples arguments')
args = parser.parse_args(actual_args)
if args.config:
# load config from file
config = cmdline.load_config(args.config)
args = parser.parse_args(config + actual_args)
# create device
try:
device = cmdline.create_device(args)
except error.Error as e:
parser.error(e)
#print(display_settings(args))
return device
# based on luma.examples terminal
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)
def main():
if not os.access(VIRTUAL_TERMINAL_DEVICE, os.R_OK):
print(("Unable to access %s, try running as root?" % (VIRTUAL_TERMINAL_DEVICE,)))
raise SystemExit
fontname = "tiny.ttf"
size = 6
font = make_font(fontname, size) if fontname else None
term = terminal(device, font, animate=False)
term.clear()
for i in range(0, ROWS):
term.puts(str(i) * COLS)
term.flush()
#time.sleep(1)
while True:
# Get terminal text; despite man page, `screendump` differs from reading vcs dev
#data = file(VIRTUAL_TERMINAL_DEVICE).read()
data = subprocess.check_output(["screendump"])
#print [data]
# Clear, but don't flush to avoid flashing
#term.clear()
term._cx, term._cy = (0, 0)
#term._canvas.rectangle(term._device.bounding_box, fill=term.bgcolor)
term._canvas.rectangle(term._device.bounding_box, fill="black")
# puts() flushes on newline(), so reimplement it ourselves
#term.puts(data)
for char in data:
if '\r' in chr(char):
term.carriage_return()
elif chr(10) in chr(char):
#term.newline()
# no scroll, no flush
term.carriage_return()
x = 0
term._cy += term._ch
elif '\b' in chr(char):
term.backspace()
x =- 1
elif '\t' in chr(char):
term.tab()
else:
term.putch(chr(char))
term.flush()
time.sleep(0.01)
#print "refresh"
#print data
if __name__ == "__main__":
os.system("stty --file=/dev/console rows %d" % (ROWS,))
os.system("stty --file=/dev/console cols %d" % (COLS,))
try:
device = get_device()
main()
except KeyboardInterrupt:
pass