Ok, I came to terms with the fact that I am out of time to put into this project, so I am done with it. This is an LCD driver program for the NetMedia 2×16 LCD, which I ‘won’ at the PAREx robotics competition. Instead of going for a low-level API to expose the hardware directly, I decided to experiment with a more task oriented approach. Additionally, I tried to separate the low level stuff from the functional bits, to make it easier to port to a different LCD controller.
I figured that the main thing I wanted to do with the LCD was to make a status display that would update periodically. To do this with the API, you simply make a function that returns the text you want to display (for instance, the time), and register it as a callback function with a specific frequency. Then, the driver spawns a separate thread that acts as a timer, calling the user-provided function every time period and then updating the display with that data. It sounds complicated but I think the user code comes out to be fairly elegant. As a bonus, I also built an animation class using the devices custom characters. Here is a demo video:
Source and demo program here or after the break:
Demo program (test.py):
#!/usr/bin/env python """ Demonstration program for the lcd class """ from time import strftime, sleep from lcd import * # Initialize the display display = Lcd('/dev/ttyUSB0') # Demonstrate use of the Animation feature mouth = [[0,0,0,0,0,0,31,31], [0,0,0,0,0,31,0,31]] eye = [[0,0,4,10,4,0,0,0], [0,4,10,17,10,4,0,0], [4,10,17,0,17,10,4,0], [10,17,0,4,0,17,10,0]] display.registerAnimation(eye, .2, 3,0,13, 'left_eye') display.registerAnimation(mouth, .5, 4,0,14, 'mouth') display.write(chr(3),row=0,col=15) display.startAnimation('left_eye') display.startAnimation('mouth') # Demonstrate use of the buildTransition helper function slide_anim = buildTransition(lcdNUMBERS) display.registerAnimation(slide_anim, .3, 0,0,0, 'scrolling_digits') display.startAnimation('scrolling_digits') # Demonstrate use of the callback feature def time_display(): return strftime('%H:%M:%S') def date_display(): return strftime('%a, %b %d %Y') display.registerCallback(time_display, 1, 0,4, 'time') display.startCallback('time') display.registerCallback(date_display, 360, 1,0, 'date') display.startCallback('date') # Other program code would go here. Note that the main thread doesn't have to # do anything else to keep the lcd updating itself! while(1): sleep(1) |
Driver (lcd.py):
""" Version: .1 Date: Dec. 5th, 2007 License: Public Domain Description: This library implements a fairly reasonable interface to the NetMedia 2x16 Serial LCD Display Module, which I happen to have acquired. It tries to implement some decently complex behavior using threading. Device-specific code is limited to the _ functions, which should be able to be modified for the specific LCD without changing the front-end interface. I refuse to separate the device driver code completely unless someone gives me a model from another vendor as a bribe :-). There are two more documented functions that this LCD supports, 'set geometry' and 'tab size'. Their usefullness is debatable so they have not been included in the library. The most interesting bits, the Animation and Callback features, still need some rework. In particular, it seems that the stop functions do not function correctly. This will come as I have time and a need for them. This library requires the PySerial library. """ import serial import threading from time import sleep ################################################################################ # These are some helpful defines for the board. # # Note that they are the same as the standard ASCII definitions. ################################################################################ lcdCHR_BACKSPACE = chr(8) lcdCHR_HORIZTAB = chr(9) lcdCHR_LF = chr(10) lcdCHR_VERTTAB = chr(11) lcdCHR_CR = chr(13) ################################################################################ # Some handy (8x5) custom character definitions ################################################################################ """ Set of horizontal bars that can be used to display a meter """ lcdMETER = [[0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,31], [0,0,0,0,0,0,31,31], [0,0,0,0,0,31,31,31], [0,0,0,0,31,31,31,31], [0,0,0,31,31,31,31,31], [0,0,31,31,31,31,31,31], [0,31,31,31,31,31,31,31], [31,31,31,31,31,31,31,31]] """ Array of numbers. Use these to make animations involving digits """ lcdNUMBERS = [[14,17,19,21,25,17,14,0], [4,12,4,4,4,4,14,0], [14,17,1,2,4,8,31,0], [31,2,4,2,1,17,14,0], [2,6,10,18,31,2,2,0], [31,16,30,1,1,17,14,0], [6,8,16,30,17,17,14,0], [31,1,2,4,8,8,8,0], [14,17,17,14,17,17,14,0], [14,17,17,15,1,2,12,0]] ################################################################################ # AnimationThread class # TODO: rework this to something more sensible, add runonce method ################################################################################ class AnimationThread(threading.Thread): """ Build an animation based on an array of custom characters, that updates at a specific interval. The intended purpose of this is to make simple animations. """ def __init__ (self, display, animation, time, address, row, col): self.display = display self.time = time self.animation = animation self.index = 0 self.length = len(animation) self.address = address self.row = row self.col = col threading.Thread.__init__(self) def run(self): self.display.write(chr(self.address), row=self.row, col=self.col) while(1): self.display.assignCustomCharacter(self.animation[self.index], self.address) self.index = (self.index + 1) % self.length sleep(self.time) ################################################################################ # CallbackThread class # TODO: rework this to something more sensible, add runonce method ################################################################################ class CallbackThread(threading.Thread): """ Set up a periodic call to a function that returns text. The intended use for this is to make it easy to monitor data that is constantly changing. """ def __init__ (self, display, callback, time, row, col): self.display = display self.time = time self.callback = callback self.row = row self.col = col threading.Thread.__init__(self) def run(self): while(1): self.display.write(self.callback(),row=self.row,col=self.col) sleep(self.time) ################################################################################ # buildTransition helper function ################################################################################ def buildTransition(characters, transition="vertical", charheight=8, charwidth=5): """ Build a custom character animation by transitioning from the first character to the second. The parameters charheight and charwidth are optional. Keyword Arguments characters -- A list of custom caracters to build a transition animation out of. For example, a slot machine effect could be created with this effect, or a waterfall. transition -- Type of transition to perform. Currently, only "vertical" is supported. Additions to this are welcome. charheight -- Height of a character (pixels) charwidth -- Width of a character (pixels) """ animation = [] for n in range(0, len(characters)-1): character1 = characters[n] character2 = characters[n+1] if(transition=="vertical"): for i in range(0, charheight): # outer loop, make each character tempchar = [] for j in range(0, charheight): # inner loop, make line of character if(j <= i): tempchar.append(character2[charheight-1-i+j]) else: tempchar.append(character1[j-i-1]) animation.append(tempchar) else: # not implemented return return animation ################################################################################ # Lcd Class. ################################################################################ class Lcd(): """ Class to control an LCD display The display will be intialized when the class is started. Required arguments: port -- Serial port to attach to (for example, /dev/ttyS0) Optional arguments: backlight -- Power level of the backlight (in %). 0 corresponds to off. contrast -- Contrast setting. baud -- Baud rate that the device talks at. The NetMedia is hardwired to either 9600 or 2400 rows -- Number of rows that the display has cols -- Number of colums that the display has charheight -- Height of a character (pixels) charwidth -- Width of a character (pixels) """ def __init__(self, port, baud=9600, backlight=50, contrast=30, rows=2, cols=16, charheight = 8, charwidth=5): # Anything that uses _serial should first acquire _writelock self._serial = serial.Serial(port, baud) self._writelock = threading.Semaphore() self._backlight = backlight self._contrast = contrast # These are really functions of the specific LCD being used, so they # should be moved to a device-specific class if that is separated from # this interface. self._rows = rows self._cols = cols self._charheight = charheight self._charwidth = charheight # These are dictionaries that contain lists of the current animations # and callbacks self._animations = {} self._callbacks = {} # Reset the display. This has the side effect of clearing the screen # and setting the backlight and contrast values to their specified # values. self.reset() ############################################################################### # 'Public' functions. These can be called at any time. Note that they do not # affect the hardware directly, but through the back-end functions. This is # to facilitate easy porting of this library to other LCD hardware. ############################################################################### def write(self, data, row=-1, col=-1): """ Function to write characters to the display. The parameters x and y are optional. They should either both be used or both not be used. If they are not specified, or the position they specify is out of range, they will both be ignored. Keyword Arguments data -- Data to write to the display row -- Row to write data to col -- Column to write data to """ self._writelock.acquire() if(self._isValidPosition(row,col)): self._setPosition(row,col) self._serial.write(data) self._writelock.release() def reset(self): """ Reset the device, clear the screen, and set the backlight and contrast settings to their default settings. """ self._writelock.acquire() self._reset() self._setBacklight(self._backlight) self._setContrast(self._contrast) self._clearScreen() self._writelock.release() def clearScreen(self): """ Clear the LCD screen. This has the side effect of moving the cursor position to 0,0 """ self._writelock.acquire() self._clearScreen() self._writelock.release() def setPosition(self, row, col): """ Set the write cursor to the given position. A better way of acheiving this is to specify the x and y arguments to write(). Keyword Arguments row -- Row to write data to col -- Column to write data to """ self._writelock.acquire() self._setPosition(row, col) self._writelock.release() def setBacklight(self, percent): """ Set the power level of the backlight to the given percentage. Also, store the given backlight setting so it can be restored during a call to reset(). Keyword Arguments percent -- Percentage of power that the backlight should be set to. Zero corresponds to turning the backlight off. """ self._backlight = percent self._writelock.acquire() self._setBacklight(percent) self._writelock.release() def setContrast(self, percent): """ Set the contrast level of the LCD. Also, store the given contrast setting so it can be restored during a call to reset(). Keyword Arguments percent -- Contrast level, in percent. 100 is maximum contrast. """ self._contrast = contrast self._writelock.acquire() self._setContrast(percent) self._writelock.release() def assignCustomCharacter(self, character, address): """ Place a custom character in one of the character slots. The character should be an array of bytes representing the character bitmap. Keyword Arguments character -- The custom character to use write. This should be a character array representing the """ self._writelock.acquire() self._assignCustomCharacter(character, address) self._writelock.release() def registerAnimation(self, animation, time, address, col, row, name): """ TODO: rework this feature """ # first, check if the name is new if(name in self._animations): return else: # make a new animation thread newanimation = AnimationThread(self, animation, time, address, col, row) # make it a daemon so that it won't stall the program at exit newanimation.setDaemon(1) # and add it to the list of animations self._animations[name] = newanimation def startAnimation(self, name): """ TODO: rework this feature """ if(name in self._animations): self._animations[name].start() def stopAnimation(self, name): """ TODO: rework this feature """ if(name in self._animations): # do we need to make sure semaphore is released first? self._animations[name].stop() def unregisterAnimation(self, name): """ TODO: rework this feature """ if(name in self._animations): # do we need to make sure semaphore is released first? stop_animation(name) self._animations.pop(name) def registerCallback(self, callback, time, row, col, name): """ TODO: rework this feature """ if(name in self._callbacks): return else: newcallback = CallbackThread(self, callback, time, row, col) newcallback.setDaemon(1) self._callbacks[name] = newcallback def startCallback(self, name): """ TODO: rework this feature """ if(name in self._callbacks): self._callbacks[name].start() def stopCallback(self, name): """ TODO: rework this feature """ if(name in self._callbacks): # do we need to make sure semaphore is released first? self._callback[name].stop() def unregisterCallback(self, name): """ TODO: rework this feature """ if(name in self._callbacks): # do we need to make sure semaphore is released first? stop_callback(name) self._callbacks.pop(name) ############################################################################### # Back-End functions. This is where the actual hardware interaction happens. # These are not intended to be called directly by the user. ############################################################################### def _reset(self): self._serial.write(chr(14)) sleep(1) # wait for device to come around def _clearScreen(self): self._serial.write(chr(12)) def _setPosition(self, row, col): self._serial.write(chr(17) + chr(row) + chr(col)) def _setBacklight(self, percent): self._serial.write(chr(20)+chr(int(percent*255/100))) def _setContrast(self, percent): self._serial.write(chr(19)+chr(int(percent*255/100))) def _assignCustomCharacter(self,character, address): # address is 0 to 7. This table defines the RAM locations for each # custom character location = [64, 72, 80, 88, 96, 104, 112, 120] # character is an array of eight integers self._serial.write(chr(21) + chr(location[address])) for i in range(0,8): self._serial.write(chr(23) + chr(character[i])) def _isValidPosition(self, row, col): if(col >=0 and col< self._cols and row>=0 and row< self._rows): return True return False |
Pingback: 10 minute project: Traffic counter for router | C i b o M a h t o . c o m