Let the (Graphical) Fun Begin!

SPI 128 x 160 display operating with SPIDriver on output from read_irig program

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()

 

Leave a Reply