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