MyRaspberryAndMe

Tinkering with Raspberry (and other things)

Pi-Hicle part 1 – Touch Screen support

3 Comments

So here’s the first real part of the “Pi-Hicle” series. As the final goal is to control my vehicle from a touch screen, getting that to work is the first task.

The touch screen I have lying around is a 4DSystems μLCD-32PT type. ulcd_backIt is controlled via a serial port and that means: 4 cables only.

Have a look at the dimensions. This thing is a 3.2″ LCD and the control board is not bigger than the screen. The processor on this display supports a lot of commands for graphics primitives, like drawing lines, circles etc. You can even select from 4 different font sizes.

It is equipped with a micro SD-card reader from which it can display graphics files and even play wav-sounds. My display is the “SGC” type, that means “Serial Graphics Client”. So the display only serves as a client, meaning that all calculations need to be done by the controlling program. For real-time updating of complex graphics or such things this may be a major drawback, but I won’t need that.

Controlling the display

As the display itself is more or less “dumb”, let alone being a “legacy product”, there are no libraries around which I could have used. Some time ago I found a CPP library for an Arduino but it seems that this library (supporting my old display) is no more. So I started from scratch with only the technical manual and the description of all supported functions and began coding.

My python class does not support the complete command set, I concentrated on the things I am going to need. And I do not need low-level support for reading/writing the SD-card slot.

The code is rather lengthy, so here are the function definitions for my library:

  • def __init__(self, port)
  • def reopenPort(self, baud=9600)
  • def closePort(self)
  • def nakAck(self)
  • def readResponse(self, numBytes)
  • def get565Color(self, red, green, blue)
  • def getRGBColor(self, color)
  • def autoBaud(self)
  • def setBaudRate(self, baudrate)
  • def getVersionInfo(self, osd=False)
  • def replaceBackgroundColor(self, red, green, blue)
  • def clearScreen(self)
  • def setBacklight(self, light)
  • def setDisplay(self, onoff)
  • def setContrast(self, contrast)
  • def displayPower(self, power)
  • def setOrientation(self, orientation)
  • def setTouchControl(self, touch)
  • def resetActiveRegion(self)
  • def drawCircle(self, x, y, r, red, green, blue)
  • def drawTriangle(self, x1,y1,x2,y2,x3,y3, red, green, blue)
  • def setBackgroundColor(self, red, green, blue)
  • def drawLine(self, x1,y1,x2,y2,red, green, blue)
  • def drawRectangle(self, x1,y1,x2,y2,red, green, blue)
  • def drawEllipse(self, x,y,rx,ry,red, green, blue)
  • def drawPixel(self, x,y,red, green, blue)
  • def readPixel(self, x, y)
  • def setPenSize(self, size)
  • def setFont(self, font)
  • def setOpacity(self, opacity=False)
  • def drawCharacter(self, col, row, char, red, green, blue)
  • def gDrawCharacter(self, xpos, ypos, char, red, green, blue, width, height)
  • def drawString(self, col, row, font,red, green, blue, text)
  • def gDrawString(self, xpos, ypos, font,red, green, blue, width, height, text)
  • def getTouch(self, mode)
  • def setTouchRegion(self, x1, y1, x2, y2)
  • def displayImage(self, filename, xmsb, xlsb, ymsb, ylsb, p1, p2, p3, p4)
  • def playSound(self, fname, option)

I will publish the sources on Github once I am finished. The code there will be well-documented (and that is the reason why it takes some more time…)

So now I am able to draw things on the display, but without feedback if/what/where the user touches the screen this is useless. Next up:

Feedback from the display

Usually graphical user interfaces (GUI) are event driven. So if the user presses the “OK”-button on screen, this button will raise an event. Every other object may register as a listener and thus will get notified from the button itself if it was clicked. My display can’t do this. It does not even raise an event, so I need to frequently ask the display if there has been a “TouchEvent” and then handle it with my own code.

Another important thing is that the display only supports “primitive” graphic operations. There is a command to draw a button, but that button will not send any events. It does not even look too good compared with the user interfaces we are used to. But the display has an interesting command I am using to somewhat overvome these limitations: it can display an image saved on the sd-card. So I am going to draw the user interface, show it on screen and act based on the region clicked.

This is the “main control” GUI I designed:

doc_main_keypad

The “GUI” is strictly grid based. Every grid cell has the size of the “OUT” button, if a button spans over rows/columns (like the “GO” button), I simply define the same “event” for cells it uses.

My “KeyPad” class is actually really simple. From the maximum dimensions and the grid size the number of rows and columns is calculated and a two-dimensional list is constructed. This list needs to be filled with the “event” that corresponds with the cell. And exactly this “event” is returned from the KeyPad class. Take a look at the actual source code:

#!/usr/bin/python

"""
      Define a "KeyPad" with a rectangular layout

      This class is meant to be used with the "PicasoSGC" class. The dimensions
      and grid width and height are used to determine an "event" that can be
      called for given coordinates.
      This supports simple grid layouts only. For combined rows or columns
      simply assign the same eventname.

      Usually a graphic is displayed on the display and the controlling program
      gets the touch coordinates. It then queries this class, which "event"
      correspond with the touch coordinates
      If no event is defined for a given cell, False is returned as the event.

      Copyright (C) 2013  Thomas Henkel (mypiandme@gmail.com)

      This program is free software: you can redistribute it and/or modify
      it under the terms of the GNU General Public License as published by
      the Free Software Foundation, either version 3 of the License, or
      any later version.

      This program is distributed in the hope that it will be useful,
      but WITHOUT ANY WARRANTY; without even the implied warranty of
      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      GNU General Public License for more details.

      You should have received a copy of the GNU General Public License
      along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""

class KeyPad:

    _matrix = 0
    _gridX  = 0
    _gridY  = 0
    _maxX   = 0
    _maxY   = 0
    _cols   = 0
    _rows   = 0
    """
        adding graphics via 4DSystems Graphics Composer (that is the only way)
        results in one file (with ending .Gci) and all images
        identified by an 4-byte offset
        GraphicsComposer creates a .txt file with the offset values
    """
    _imageName = ""
    _p1     = 0     # offset xx000000
    _p2     = 0     # offset 00xx0000
    _p3     = 0     # offset 0000xx00
    _p4     = 0     # offset 000000xx

    """
        initialize KeyPad
        From pixel size and gridisze the number of columns and rows
        is calculated. Row/Col numbering starts at 0
        The matrix is pre-filled with False
        Parameters:
            gridx, gridy    - width of grid in pixels
            maxx, maxy      - screen size in pixels
    """
    def __init__(self, gridx, gridy, maxx, maxy, imageName, offset):
        self._gridX = int(gridx)
        self._gridY = int(gridy)
        self._maxX  = int(maxx)
        self._maxY  = int(maxy)
        self._imageName = imageName
        self._p1 = (offset>>24)&0xFF
        self._p2 = (offset>>16)&0xFF
        self._p3 = (offset>>8)&0xFF
        self._p4 = (offset)&0xFF

        #calculate number of rows and cols
        self._cols = self._maxX/self._gridX
        self._rows = self._maxY/self._gridY

        # make 2D array [cols][rows]
        self._matrix = [[False for x in xrange(self._rows)] for x in xrange(self._cols)]

    """
        fill the matrix with eventnames
        According to the screen design event names can be assigned to
        each row/col pair
        Parameters:
            col, row    column and row index
            eventname   the value for the event at this coordinates
    """
    def setEvent(self, col, row, eventname):
        # check dimensions
        if col not in range(0,self._cols):
            return False
        if row not in range(self._rows):
            return False
        self._matrix[col][row] = eventname
        return eventname

    """
        return an event for given coordinates of the touch event
        From the coordinate tuple (x,y) the row and column are calculated. If
        the division has no residue (a%b==0) the the touch coordinates are
        exactly between two columns or rows thus False is returned, as this event
        can not be identified
        Parameters:
            coordinates     as tuple (x,y)
        Return Value:
            the name of the event triggered
    """
    def getEvent(self, coordinates):
        # expect coordinates to be tuple
        if len(coordinates) != 2:
            return False
        # col/row calculation is simple division
        # if residue is 0, ignore event as it is on border between two cols or rows
        x = coordinates[0]
        y = coordinates[1]
        col  = x/self._gridX
        rcol = x%self._gridX
        row  = y/self._gridY
        rrow = y%self._gridY

        if rrow*rcol == 0:
            return False

        return self._matrix[col][row]

    def getDimensions(self):
        return(self._maxX, self._maxY)

    def getImageName(self):
        return self._imageName

    def getOffset(self):
        return(self._p1,self._p2,self._p3,self._p4)

Pretty simple. And I am not going to use threads (yet). While prototyping I realized that at the moment things are fast enough, so no need for adding complexity like synchronization, locking, and all the other stuff.

Now how does that actually look in code? Compare the following code fragment to the “GUI” image above and you will see that initializing the keypad is straightforward:

    def initKeyPadHandler(self):
        self._prgPad = kp(60, 45, 240, 320, self._keypadGraph, 0x00000000)
        self._prgPad.setEvent(1,0,'FORWARD')
        self._prgPad.setEvent(3,0,'CLR')
        self._prgPad.setEvent(0,1,'LEFT')
        self._prgPad.setEvent(1,1,'HOLD')
        self._prgPad.setEvent(2,1,'RIGHT')
        self._prgPad.setEvent(3,1,'FIRE')
        self._prgPad.setEvent(1,2,'BACK')
        self._prgPad.setEvent(3,2,'CLS')

And this is the piece of code that is handling the “events”. The display is asked, if a “touch release” has happened. If yes, it calls for the coordinates of the touch event and passes them to the getEvent() function of the KeyPad class. The getEvent() method calculates the row/col of the touch event and returns the corresponding event. The calling code then has to handle everything.

        while not progDone:
            # check user inputs and interpret them
            status = self._screen.getTouch('status')
            if status == 2:
                # release detected, get coordinates
                #print display.getTouch('coord')
                event = self._prgPad.getEvent(self._screen.getTouch('coord'))
                if event == 'OUT':
                    # done here, return
                    progDone = True
                    continue
                elif event == 'CLR':
                    # clear memory
                    progDone = False
                    cmdStarted = False
                    cmdNumcount = 0
                    del self._cmdBuffer[:]
                    self._shortBuffer = ""
                    del self._path[:]
                    continue
                elif event == 'TEST':

Here’s a picture what it will look like (with an older GUI version):

P1010318

Now would be the right time for a demonstration to prove that this actually works. Unfortunately that has to wait. First: I do not have a decent camera for making short videos (tried with my mobile but the image is not steady) and second: I can’t upload videos. So I am planning on creating a YouTube channel for my blog.
Update: Here’s a video what it looks like at this early stage:

The next part will be about recreating that BigTrak feeling, aka programming the logic and the on-screen simulation (attention: maths content)…

Advertisements