I bought a cool device called SPIDriver through CrowdSupply. Actually, I bought two – and got them each with a neat little SPI based 132 x 162 colour LCD display.
It’s a very neat device, appearing as a USB serial port to your system. On my LINUX system, it appears as /dev/ttyUSBn. There are several sample programs and libraries available to drive SPIDriver, including for the display.
The display operates with a Sitronix ST7735 controller – here are a couple of different version datasheets: ST7735 ST7735S_v1.1 .
Anyway, I hacked the included Python program st7735s.py to do my own bidding. irig_to_st7735.py takes STDIN text, uses the PIL library to rasterize it into graphics, and paints it into the little display.
Unfortunately, it takes about 1-1/2 seconds to get it transferred onto the display – it is a full graphics display, and it is a SPI interface, after all. I added an option to read_irig to put out 8 lines of text every 2 seconds, which I then piped into the Python program. It worked fine. Not terribly useful, but fine 🙂
#!/usr/bin/env python3
# coding=utf-8
#***************
# Import functions.
#---------------
import array
import getopt
import struct
import sys
import time
from PIL import Image, ImageDraw, ImageFont
from spidriver import SPIDriver
#***************
# Constants.
#---------------
Version = 0
Issue = 3
IssueDate = "2019-05-06"
DefaultDevice = "/dev/ttyUSB0"
LightBlue = (102, 255, 255)
PaleBlue = (102, 204, 255)
Red = (255, 0, 0)
Yellow = (255, 255, 0)
Lime = ( 0, 255, 0)
White = (255, 255, 255)
NOP = 0x00
SWRESET = 0x01
RDDID = 0x04
RDDST = 0x09
SLPIN = 0x10
SLPOUT = 0x11
PTLON = 0x12
NORON = 0x13
INVOFF = 0x20
INVON = 0x21
DISPOFF = 0x28
DISPON = 0x29
CASET = 0x2A
RASET = 0x2B
RAMWR = 0x2C
RAMRD = 0x2E
PTLAR = 0x30
COLMOD = 0x3A
MADCTL = 0x36
FRMCTR1 = 0xB1
FRMCTR2 = 0xB2
FRMCTR3 = 0xB3
INVCTR = 0xB4
DISSET5 = 0xB6
PWCTR1 = 0xC0
PWCTR2 = 0xC1
PWCTR3 = 0xC2
PWCTR4 = 0xC3
PWCTR5 = 0xC4
VMCTR1 = 0xC5
RDID1 = 0xDA
RDID2 = 0xDB
RDID3 = 0xDC
RDID4 = 0xDD
PWCTR6 = 0xFC
GMCTRP1 = 0xE0
GMCTRN1 = 0xE1
DELAY = 0x80
#***************
# Pure Python rgb to 565 encoder for portablity
#---------------
def as565(ProcessedImage):
#print ("ProcessedImage:", ProcessedImage)
OriginalRed, OriginalGreen, OriginalBlue = [list(c.getdata()) for c in ProcessedImage.convert("RGB").split()]
def MultiplyAndShift(ColourValue, ShiftBy):
return ColourValue * (2 ** ShiftBy - 1) // 255
d565 = [(MultiplyAndShift(BlueValue, 5) << 11) | (MultiplyAndShift(GreenValue, 6) << 5) | MultiplyAndShift(RedValue, 5) for (RedValue, GreenValue, BlueValue) in zip(OriginalRed, OriginalGreen, OriginalBlue)]
d565h = array.array('H', d565)
#print ("d565h:", d565h)
d565h.byteswap()
d565s = d565h.tostring()
#print ("d565s:", d565s);
return array.array('B', d565s)
def debug565(OriginalColour):
print ("OriginalColour:", OriginalColour)
OriginalRed, OriginalGreen, OriginalBlue = OriginalColour
def MultiplyAndShift(ColourValue, ShiftBy):
return ColourValue * (2 ** ShiftBy - 1) // 255
d565 = [(MultiplyAndShift(OriginalBlue, 5) << 11) | (MultiplyAndShift(OriginalGreen, 6) << 5) | MultiplyAndShift(OriginalRed, 5)]
d565h = array.array('H', d565)
print ("d565h:", d565h)
d565h.byteswap()
d565s = d565h.tostring()
print ("d565s:", d565s);
return array.array('B', d565s)
#***************
# Class for wrangling with the ST7735 160 x 128 dot display
#---------------
class ST7735:
def __init__(self, sd):
self.sd = sd
self.sd.unsel()
def write(self, a, c):
self.sd.seta(a)
self.sd.sel()
self.sd.write(c)
self.sd.unsel()
def writeCommand(self, cc):
self.write(0, struct.pack("B", cc))
def writeData(self, c):
self.write(1, c)
def writeData1(self, cc):
self.writeData(struct.pack("B", cc))
def cmd(self, cc, args=()):
self.writeCommand(cc)
n = len(args)
if n != 0:
self.writeData(struct.pack(str(n) + "B", *args))
def setAddrWindow(self, x0, y0, x1, y1):
self.writeCommand(CASET) # Column addr set
self.writeData(struct.pack(">HH", x0, x1))
self.writeCommand(RASET) # Row addr set
self.writeData(struct.pack(">HH", y0, y1))
self.writeCommand(RAMWR) # write to RAM
def rect(self, x, y, w, h, color):
self.setAddrWindow(x, y, x + w - 1, y + h - 1)
self.writeData(w * h * struct.pack(">H", color))
def start(self):
self.sd.setb(0)
time.sleep(.001)
self.sd.setb(1)
time.sleep(.001)
self.cmd(SWRESET) # Software reset, 0 args, w/delay
time.sleep(.180)
self.cmd(SLPOUT) # Out of sleep mode, 0 args, w/delay
time.sleep(.180)
commands = [
(FRMCTR1, ( # Frame rate ctrl - normal mode
0x01, 0x2C, 0x2D)), # Rate = fosc/(1x2+40) * (LINE+2C+2D)
(FRMCTR2, ( # Frame rate control - idle mode
0x01, 0x2C, 0x2D)), # Rate = fosc/(1x2+40) * (LINE+2C+2D)
(FRMCTR3, ( # Frame rate ctrl - partial mode
0x01, 0x2C, 0x2D, # Dot inversion mode
0x01, 0x2C, 0x2D)), # Line inversion mode
(PWCTR1, ( # Power control
0xA2,
0x02, # -4.6V
0x84)), # AUTO mode
(PWCTR2, ( # Power control
0xC5,)), # VGH25 = 2.4C VGSEL = -10 VGH = 3 * AVDD
(PWCTR3, ( # Power control
0x0A, # Opamp current small
0x00)), # Boost frequency
(PWCTR4, ( # Power control
0x8A, # BCLK/2, Opamp current small & Medium low
0x2A)),
(PWCTR5, ( # Power control
0x8A, 0xEE)),
(VMCTR1, ( # VCOM control
0x0E,)),
(MADCTL, ( # Memory access control (directions)
0xC8,)), # row addr/col addr, bottom to top refresh
(COLMOD, ( # set color mode
0x05,)), # 16-bit color
(GMCTRP1, ( # Gamma + polarity Correction Characterstics
0x02, 0x1c, 0x07, 0x12,
0x37, 0x32, 0x29, 0x2d,
0x29, 0x25, 0x2B, 0x39,
0x00, 0x01, 0x03, 0x10)),
(GMCTRN1, ( # Gamma - polarity Correction Characterstics
0x03, 0x1d, 0x07, 0x06,
0x2E, 0x2C, 0x29, 0x2D,
0x2E, 0x2E, 0x37, 0x3F,
0x00, 0x00, 0x02, 0x10)),
(NORON, ()), # Normal display on
(DISPON, ()), # Main screen turn on
]
for c, args in commands:
self.cmd(c, args)
def clear(self):
self.rect(0, 0, 128, 160, 0x0000)
def writestrings(self, ListOfLineStrings):
#print ("On entry into writestrings(), ListOfLineStrings: ", ListOfLineStrings)
#print ("Starting writestrings")
BaseWidth = 160
BaseHeight = 128
# make a blank image for the text, initialized to transparent text color
TextImage = Image.new('RGB', (BaseWidth, BaseHeight), ( 16, 16, 16))
TextSize = int(BaseHeight/ 8)
LineColours = (White, Lime, Lime, PaleBlue, LightBlue, LightBlue, Yellow, Red)
#print ("Length of list of LineColours: " + "{0:d}".format(len(LineColours)))
StartColumnOffset = 0
# get a font
#FontToWrite = ImageFont.truetype('Pillow/Tests/fonts/FreeMono.ttf', int(TextSize))
FontToWrite = ImageFont.truetype('Courier_New.ttf', int(TextSize))
# get a drawing context
DrawContext = ImageDraw.Draw(TextImage)
# draw text, full opacity
LineNumber = 1
for LineString in ListOfLineStrings:
#print ("LineString: " + LineString)
if (LineNumber > len(LineColours)):
print ("Past end of colour list, LineNumber = " + "{0:d}".format(LineNumber) + " and length of LineColours list = " + "{0:d}".format(len(LineColours)))
print ("Length of ListOfLineStrings = " + "{0:d}".format(len(ListOfLineStrings)))
print ("ListOfLineStrings: ", ListOfLineStrings )
print ()
LocalColour = White
else:
LocalColour = LineColours[LineNumber-1]
DrawContext.text((StartColumnOffset, (LineNumber-1)*TextSize), LineString, font=FontToWrite, fill=LocalColour)
LineNumber += 1
FinalImage = TextImage
# debug by saving images to disk
#print ("Output 01txt.jpg")
#SaveImage = TextImage.convert('RGB')
#SaveImage.save("01text.jpg")
#print ("Ouput 02base.jpg")
#SaveImage = BaseImage.convert('RGB')
#SaveImage.save("02base.jpg")
#print ("Ouput 03final.jpg")
#SaveImage = FinalImage.convert('RGB')
#SaveImage.save("03final.jpg")
#print ("FinalImage.size")
#print (FinalImage.size)
if FinalImage.size[0] > FinalImage.size[1]:
#print ("Rotating 90 degrees")
FinalImage = FinalImage.transpose(Image.ROTATE_90)
#w = 160 * FinalImage.size[0] // FinalImage.size[1]
#FinalImage = FinalImage.resize((w, 160), Image.ANTIALIAS)
#(w, h) = FinalImage.size
#if w > 128:
#FinalImage = FinalImage.crop((w // 2 - 64, 0, w // 2 + 64, 160))
#elif w < 128:
#c = Image.new("RGB", (128, 160))
#c.paste(FinalImage, (64 - w // 2, 0))
#FinalImage = c
st.setAddrWindow(0, 0, 127, 159)
#st.writeData(as565(FinalImage.convert("RGB")))
st.writeData(as565(FinalImage))
#***************
# Function to provide usage help.
#---------------
def usage():
print ("\nTake in read_irig LCD output and display to ST7735 display attached to SPIDriver, v"+"%1d" % (Version)+"."+"%1d" % (Issue)+" "+IssueDate+" dmw")
print ("\nTypical usage: "+sys.argv[0]+" [option]* ")
print ("\n Options: ")
print (" -o <device> Output device for SPIDriver (default " + DefaultDevice + ")")
print (" -v More verbose")
print ("\n RCS Info:")
print (" Header: /home/dmw/src/ntp/refclock_irig/RCS/irig_to_st7735.py,v 1.4 2019/05/17 03:37:27 dmw Exp")
print ("\n")
#***************
# Main function.
#---------------
if __name__ == '__main__':
#***************
# Get command line options. Error results in help text dump and exit.
#---------------
try:
opts, args = getopt.getopt(sys.argv[1:], "o:v")
except getopt.GetoptError as Error:
# print help information and exit:
print ("\n")
print (Error) # will print something like "option -a not recognized"
print ("\n------------------------------")
usage()
sys.exit(2)
#***************
# Set defaults.
#---------------
UseDevice = DefaultDevice
Verbose = False
#print ("Checking options now")
#***************
# Parse values from command line options. Error results in message and exit.
#---------------
for Option, Argument in opts:
#print ("Checking option: " + Option + ", with argument: " + Argument)
if Option in ("-o"): # Output file name.
UseDevice = Argument
#print ("\nUsing device: " + UseDevice)
elif Option in ("-v"): # Turn on verbosity.
Verbose = True
else:
print ("\nUnknown option \"" + Option + " " + Argument + "\", aborting...")
print ("\n------------------------------")
usage()
sys.exit(2)
if (Verbose):
print("\nLightBlue")
debug565(LightBlue)
print("\nPaleBlue")
debug565(PaleBlue)
print("\nRed")
debug565(Red)
print("\nYellow")
debug565(Yellow)
print("\nLime")
debug565(Lime)
print ("\nWhite")
debug565(White)
print ()
print ("\nUsing device: " + UseDevice)
print ()
#exit(0)
#***************
# Open ST7735 display through SPIDriver, initialize and clear.
#---------------
st = ST7735(SPIDriver(UseDevice))
st.start()
st.clear()
#***************
# Initial message.
#---------------
LineList = [
# 1111111
#1234567890123456
" IRIG Decoder",
"v"+"%1d" % (Version)+"."+"%1d" % (Issue)+" "+IssueDate,
" (c) 2019",
" Dean Weiten",
" Winnipeg, MB",
" (204)-888-1334",
" dmw@weiten.com"]
st.writestrings( LineList )
#***************
# Loop on input.
# Expecting up to 8 lines per frame,
# up to 16 characters per line.
# Colours are fixed sequence.
# An exception (often a ctrl-C break)
# results in clearing and exit message display.
#---------------
try:
LineIndex = 0
LineList = []
for line in sys.stdin:
FindFF = line.find('\f')
if (FindFF>=0):
if (Verbose):
print ("Got FF at " + "{0:d}".format(FindFF))
print( "The line up to the FF is \"" + line[:FindFF] + "\"." )
LineToAppend = line[:FindFF].rstrip("\r\n\f")
if (len(LineToAppend) > 0):
LineList.append(LineToAppend)
st.writestrings( LineList )
LineList = []
LineToAppend = line[FindFF+1:].rstrip("\r\n\f")
if (len(LineToAppend) > 0):
#print ("First line \"" + LineToAppend + "\" has length " + "{0:d}".format(len(LineToAppend)) + ", so will be appended, list is at present: \"", LineList, "\".")
LineList.append(LineToAppend)
else:
LineToAppend = line.rstrip("\r\n\f")
if (len(LineToAppend) > 0):
#print ("Line \"" + LineToAppend + "\" has length " + "{0:d}".format(len(LineToAppend)) + ", so will be appended.")
LineList.append(LineToAppend)
#print ("Length of LineList after append is " + "{0:d}".format(len(LineList)) + ".")
if (Verbose):
print( "The line list is:" )
print(LineList)
#st.writestrings(LoopNumber)
#LoopNumber += 1
finally:
st.clear()
#time.sleep(3)
LineList = [
# 1111111
#1234567890123456
" IRIG Decoder",
"v"+"%1d" % (Version)+"."+"%1d" % (Issue)+" "+IssueDate,
"",
" Exiting"]
st.writestrings( LineList )
time.sleep(4)
st.clear()