A Python interface to a large-format pen plotter
Code Walkthrough
Lines 1-5 of the program (Listing 1) handle imports [5]. import
is Python's command to bring in external libraries. Many libraries are included with Python, but they can also be purchased, written, or downloaded.
Listing 1
Imports
001 import pygame 002 import serial 003 import curses 004 import os 005 import time 006
Line 1 imports the pygame
library, which is a low-level graphics library that also includes functions for sound playback, user input from a variety of devices, and other modules designed for game creation. The plotter program uses Pygame to draw previews of HPGL files before plotting them.
The serial
library imported on line 2 provides the functions to talk to serial ports. On a Raspberry Pi, this is GPIO pin 14 (transmit) and GPIO pin 15 (receive) or a USB serial dongle. The GPIO port only operates at 3.3V, which is great for electronics projects but difficult for traditional serial equipment. Thus, I use a serial dongle to connect to the plotter.
Line 3 imports the curses
library, which provides functions to create a text interface in a terminal or console. This library is also capable of creating interfaces on traditional serial terminals, but this capability is unrelated to the serial communication in use in this program.
The os
library (line 4) provides functions relating to the current operating system running the Python interpreter. My program uses the OS library to compile file paths with Linux-appropriate directory separators and to poll the filesystem for directory listings. The time library (line 5) provides delay and timing functions used by the nullPlotter
class to simulate a physical plotter.
The plotMenu Class
The plotMenu
class (Listing 2; lines 7-222) is the main class of the program and provides the user interface. plotMenu
also initiates all of the other classes as needed. In a Python class, __init__
is run each time a class is initialized. Here, I use it to initialize the curses display and general variables for the program.
Listing 2
plotMenu Class
007 class plotMenu: 008 def __init__ ( self ): 009 self.screen = curses.initscr() 010 curses.noecho() 011 curses.cbreak() 012 self.screen.keypad ( True ) 013 screenDimensions = self.screen.getmaxyx() 014 self.maxX = screenDimensions [ 1 ] 015 self.maxY = screenDimensions [ 0 ] 016 017 self.hpglFiles = list() 018 self.fileView = None 019 self.fileOffset = 0 020 self.paper = None 021 022 def menu ( self ): 023 while 1: 024 self.screen.clear() 025 self.screen.addstr ( 1 , 1 , "Files Menu" ) 026 self.screen.addstr ( 3 , 1 , "1) Open a File" ) 027 self.screen.addstr ( 4 , 1 , "2) Shift to center" ) 028 self.screen.addstr ( 5 , 1 , "3) Flip Horizontal" ) 029 self.screen.addstr ( 6 , 1 , "4) Flip Vertical" ) 030 self.screen.addstr ( 7 , 1 , "5) Preview file" ) 031 032 self.screen.addstr ( 1 , 20 , "Plotter Menu" ) 033 self.screen.addstr ( 3 , 20 , "C) connect to plotter" ) 034 self.screen.addstr ( 4 , 20 , "P) plot current fileset" ) 035 036 self.screen.addstr ( 1 , 50 , "Paper Information" ) 037 if self.paper != None: 038 self.screen.addstr ( 3 , 50 , " X Y " ) 039 self.screen.addstr ( 4 , 50 , "Min {0:8d} {1:8d}".format \ ( self.paper [ 0 ] , self.paper [ 1 ] ) ) 040 self.screen.addstr ( 5 , 50 , "Max {0:8d} {1:8d}".format \ ( self.paper [ 2 ] , self.paper [ 3 ] ) ) 041 else: 042 self.screen.addstr ( 3 , 50 , "(Plotter Not Connected)" ) 043 044 if self.maxY % 2 == 0: y = self.maxY / 2 045 else: y = ( self.maxY - 1 ) / 2 046 047 if self.fileView != None: 048 for i in range ( self.fileOffset , self.fileOffset + \ self.maxY - 3 ): 049 self.screen.addstr ( i - self.fileOffset + 3 , 90 , \ "{0:4d}: {1:s}".format ( i , \ self.hpglFiles [ self.fileView ].\ commands [ i ] ) ) 050 051 self.screen.addstr ( y , 1 , "Filename" ) 052 self.screen.addstr ( y , 20 , "Commands" ) 053 self.screen.addstr ( y , 30 , "Pen" ) 054 self.screen.addstr ( y , 50 , "Min X" ) 055 self.screen.addstr ( y , 60 , "Min Y" ) 056 self.screen.addstr ( y , 70 , "Max X" ) 057 self.screen.addstr ( y , 80 , "Max Y" ) 058 059 y += 1 060 for index , hpgl in enumerate ( self.hpglFiles ): 061 if index == self.fileView: self.screen.addstr ( y , 0 , ">" ) 062 else: self.screen.addstr ( y , 0 , " " ) 063 064 self.screen.addstr ( y , 1 , os.path.basename \ ( hpgl.filename ) ) 065 self.screen.addstr ( y , 20 , str ( hpgl.commandLength() ) ) 066 self.screen.addstr ( y , 30 , str ( hpgl.pen ) ) 067 self.screen.addstr ( y , 50 , str ( hpgl.minX ) ) 068 self.screen.addstr ( y , 60 , str ( hpgl.minY ) ) 069 self.screen.addstr ( y , 70 , str ( hpgl.maxX ) ) 070 self.screen.addstr ( y , 80 , str ( hpgl.maxY ) ) 071 y += 1 072 073 self.screen.refresh() 074 075 key = self.screen.getch() 076 if key == ord ( "1" ): 077 selection = self.fileBrowser() 078 if selection != None: 079 newFile = HPGL() 080 newFile.openFile ( selection ) 081 self.hpglFiles.append ( newFile ) 082 self.fileView = len ( self.hpglFiles ) - 1 083 elif key == ord ( "2" ): 084 x , y = self.hpglFiles [ self.fileView ].shiftToCenter() 085 self.screen.addstr ( 35 , 5 , str ( x ) + "," + str ( y ) ) 086 elif key == ord ( "3" ): 087 self.screen.addstr ( self.maxY - 1 , 1 , "Working..." ) 088 self.screen.refresh() 089 self.hpglFiles [ self.fileView ].flipHorizontal() 090 self.screen.addstr ( self.maxY - 1 , 1 , " " ) 091 self.screen.refresh() 092 elif key == ord ( "4" ): 093 self.screen.addstr ( self.maxY - 1 , 1 , "Working..." ) 094 self.screen.refresh() 095 self.hpglFiles [ self.fileView ].flipVertical() 096 self.screen.addstr ( self.maxY - 1 , 1 , " " ) 097 self.screen.refresh() 098 elif key == ord ( "5" ): 099 self.preview() 100 elif key == curses.KEY_PPAGE: 101 self.fileOffset -= self.maxY - 3 102 elif key == curses.KEY_NPAGE: 103 self.fileOffset += self.maxY - 3 104 elif key == curses.KEY_DOWN: 105 self.fileView += 1 106 if self.fileView > len ( self.hpglFiles ) - \ 1:self.fileView = len ( self.hpglFiles ) - 1 107 self.fileOffset = 0 108 elif key == curses.KEY_UP: 109 self.fileView -= 1 110 if self.fileView < 0: self.fileView = 0 111 self.fileOffset = 0 112 elif key == ord ( "c" ): 113 #self.draftPro = plotter ( "/dev/ttyUSB0" ) 114 self.draftPro = nullPlotter ( "" ) 115 self.paper = self.draftPro.getPaper() 116 elif key == ord ( "p" ): 117 self.plot() 118 119 120 def fileBrowser ( self ): 121 path = "../plotter" 122 y = 1 123 124 selectedX = 1 125 selectedY = 1 126 127 while 1: 128 screenDirectories = dict() 129 screenFiles = dict() 130 131 dirOffset = 0 132 fileOffset = 0 133 134 self.screen.clear() 135 136 for ( dirpath , dirnames , filenames ) in os.walk ( path ): 137 y = 1 138 x = 0 139 for directory in dirnames [ dirOffset:(dirOffset + self.max\ Y - 3 ) ]: 140 if ( selectedX * 30 ) == x and selectedY == y: \ self.screen.addstr ( y , 1 , directory , \ curses.A_REVERSE ) 141 else: self.screen.addstr ( y , 1 , directory ) 142 screenDirectories [ ( y , x ) ] = directory 143 y += 1 144 145 y = 1 146 x = 30 147 148 for filename in filenames [ fileOffset:\ (fileOffset + self.maxY - 3 ) ]: 149 if "hpgl" not in filename: continue 150 if ( selectedX * 30 ) == x and selectedY == y: \ self.screen.addstr ( y , x , filename , curses.A_REVERSE ) 151 else: self.screen.addstr ( y , x , filename ) 152 y += 1 153 154 screenFiles [ ( y , x ) ] = os.path.join ( dirpath , filename ) 155 if y == self.maxY - 5: 156 y = 1 157 x += 30 158 159 self.screen.refresh() 160 break 161 162 key = self.screen.getch() 163 if key == 13 or key == 10: 164 if ( selectedY + 1 , selectedX * 30 ) in screenFiles: 165 return screenFiles [ ( selectedY + 1 , selectedX * 30 ) ] 166 elif ( selectedY , selectedX * 30 ) in screenDirectories: 167 path += "/" + screenDirectories [ ( selectedY , selectedX * 30 ) ] 168 selectedX = 1 169 selectedY = 1 170 elif key == curses.KEY_RIGHT: 171 selectedX += 1 172 elif key == curses.KEY_LEFT: 173 selectedX -= 1 174 elif key == curses.KEY_DOWN: 175 selectedY += 1 176 elif key == curses.KEY_UP: 177 selectedY -= 1 178 elif key == ord ( "u" ): 179 path += "/.." 180 selectedX = 1 181 selectedY = 1 182 self.screen.addstr ( self.maxY - 1 , 1 , str ( selectedX ) + "," \ + str ( selectedY ) ) 183 self.screen.refresh() 184 185 def preview ( self ): 186 pygame.display.init() 187 screen = pygame.display.set_mode ( ( 1680 , 1050 ) ) 188 self.hpglFiles [ self.fileView ].visual ( screen ) 189 pygame.display.flip() 190 191 def plot ( self ): 192 for fileIndex , hpgl in enumerate ( self.hpglFiles ): 193 for index , cmd in enumerate ( hpgl.cmdPoints ): 194 if cmd.cmd == "PU" or cmd.cmd == "PD": 195 self.draftPro.port.write ( cmd.cmd + str ( cmd.x ) + "," + \ str ( cmd.y ) + ";" ) 196 else: 197 self.draftPro.port.write ( cmd.cmd + ";" ) 198 199 for i in range ( index , index + self.maxY - 3 ): 200 if i < len ( self.hpglFiles [ fileIndex ].commands ): 201 if i == index: 202 self.screen.addstr ( i - index + 3 , 90 , "{0:4d}: \ {1:s}".format ( i , self.\ hpglFiles [ fileIndex ].commands \ [ i ] ) , curses.A_REVERSE ) 203 else: 204 self.screen.addstr ( i - index + 3 , 90 , "{0:4d}: \ {1:s}".format ( i , self.\ hpglFiles [ fileIndex ].commands \ [ i ] ) ) 205 206 207 buffer = self.draftPro.getBuffer() 208 percent = float ( buffer ) / 1024.0 209 chars = int ( ( self.maxX - 3 ) * percent ) 210 self.screen.addstr ( self.maxY - 1 , 0 , "#" * chars ) 211 self.screen.clrtoeol() 212 self.screen.refresh() 213 214 while buffer < 64: 215 buffer = self.draftPro.getBuffer() 216 percent = float ( buffer ) / 1024 217 chars = int ( ( self.maxX - 3 ) * percent ) 218 self.screen.addstr ( self.maxY - 1 , 0 , "#" * chars ) 219 self.screen.clrtoeol() 220 self.screen.refresh() 221 222 time.sleep ( .5 ) 223
Line 9 shows how to start a curses program: curses.initiscr()
creates the curses display, which I store in self.screen
. Because I use self
to store it, the screen will be accessible to all functions inside the plotMenu
class.
Lines 10-12 set curses modes and configuration options. curses.noecho()
instructs curses only to display characters when they are explicitly drawn. Keypresses used to navigate a menu or make a selection will not be drawn to the screen. curses.cbreak
disables line buffering from the console, allowing curses to receive each keystroke as it occurs. The special characters interrupt, quit, suspend, and flow control are not caught by curses in this mode and retain their normal function.
The self.screen.keypad (True)
command asks curses to interpret "special" (non-ASCII) keystrokes.
The menu
function – lines 22-88 – is essentially the main loop of the program. It takes care of drawing the display and handling user input. The entire function is an infinite loop (line 23) that is called as the entry point to the class.
Line 24 uses the curses function clear
to clear the screen, and lines 25-36 draw the files and plotter menus to the screen. Line 37 checks whether self.paper
has any information about the paper currently loaded in the plotter. If so, lines 38-40 draw the paper dimensions to the screen. (Note the use of the inline string function format
to align the variables in the string.) Otherwise, line 42 states that there is no information about the paper yet.
Starting at line 44, the program prepares to draw the list of files currently loaded and determines the coordinates of a place halfway down the screen.
The self.fileView
on line 47 is the index of the currently selected HPGL file. If it is set, then lines 48 and 49 draw the number of lines that will fit on the screen, with paging tracked by self.fileOffset
. Lines 51-57 draw the labels for the file list. Line 59 increments the y
(screen row) counter.
Line 60 sets up a loop for all of the currently loaded HPGL files. Finally, lines 64-70 draw the line specified in the file table, and line 71 increments y
for the next round of the loop.
Now that all of the drawing is complete, line 73 forces a refresh of the screen. Curses caches all of the screen data in internal structures and does all of the updates at once.
Line 75 uses self.screen.getch()
to get a keypress. This is blocking by default but that behavior can be easily changed. Curses supports full blocking, no blocking (getch()
returns -1 immediately if no key is currently pressed), or "delayed" mode, wherein a getch()
times out after a specified interval and returns -1 or returns immediately with the value of a pressed key. The possible keypress options refer to integers (1, 2, 3, 4, 5) that correspond to specific actions, such as opening a file, shifting the current file to the center of the screen, flipping the current image, or drawing a preview plot on the screen.
Buy this article as PDF
(incl. VAT)