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. It 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:
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 (email@example.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 y = coordinates 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):
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)…