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) <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            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("rnf")
                if  (len(LineToAppend) > 0):
                    LineList.append(LineToAppend)
                st.writestrings( LineList )
                LineList = []
                LineToAppend = line[FindFF+1:].rstrip("rnf")
                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("rnf")
                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