1
2
3
4
5
6 '''
7 Abstraction of a OLED display based on the SSD1306 chip connected to the I2C bus.
8 '''
9
10 import SSD1306
11 import RPi.GPIO as GPIO
12 import smbus
13
14
15 import os, time
16 from threading import Thread
17 from PIL import Image
18 from PIL import ImageFont
19 from PIL import ImageDraw
20 from threading import RLock
21
23 '''
24 Creates a display instances with given OLED display type (128x32 or 128x64, black & white).
25 with standard font from font file /usr/share/fonts/truetype/freefont/FreeSans.ttf.
26 All public methods (except ctor and blinker methods) are thread-safe (locked by a static RLock.
27 @param imagePath: the path to the PPM image file used as background (must have size 128x32 or 128x64 resp.)
28 @param type: 32 or 64 defining 128x32 or 128x64 bit resolution (default: 64)
29 @param inverse: if True, the background is white and the text is black;
30 otherwise the background is black and the text is white (default)
31 '''
32 _lock = RLock()
33 - def __init__(self, bkImagePath = None, type = 64, inverse = False):
34 bus = smbus.SMBus(1)
35 i2c_address = 0x3C
36 try:
37 bus.read_word_data(i2c_address, 0)
38 except:
39 self._isAvailable = False
40 return
41 self._isAvailable = True
42 if type == 32:
43
44 self.disp = SSD1306.SSD1306_128_32(rst = None, gpio = GPIO)
45 elif type == 64:
46
47 self.disp = SSD1306.SSD1306_128_64(rst = None, gpio = GPIO)
48 else:
49 print "Device type", type, "not supported"
50 self.inverse = inverse
51 self.bkImagePath = bkImagePath
52 self.type = type
53 self.blinkerThread = None
54
55
56 self.disp.begin()
57
58
59 self.width = self.disp.width
60 self.height = self.disp.height
61
62
63 self.disp.clear()
64 self.disp.display()
65
66
67 self.image = Image.new('1', (self.width, self.height))
68
69
70
71 self.fontSize = 10
72
73 self.ttfFileStandard = "/home/pi/Fonts/OpenSans-Semibold.ttf"
74 self.ttfFile = self.ttfFileStandard
75 self.font = ImageFont.truetype(self.ttfFile, 10)
76
77
78 self.draw = ImageDraw.Draw(self.image)
79
80
81 self.disp.set_contrast(255)
82
83
84 self.textBuf = {}
85 self.cursor = [0, 0]
86 self.scroll = False
87
88 - def dim(self, enable):
89 '''
90 Enables/disables dimming the display.
91 @param enable: if True, the display is slightly dimmed;
92 otherwise it is set to full contrast
93 '''
94 OLED1306._lock.acquire()
95 if enable:
96 self.disp.set_contrast(0)
97 else:
98 self.disp.set_contrast(255)
99 OLED1306._lock.release()
100
102 '''
103 Defines a background image to be displayed with next setText() or show().
104 '''
105 self.bkImagePath = bkImagePath
106
107 - def setFont(self, ttfFile, fontSize = 10):
108 '''
109 Sets a new font defined by the given TTF font file.
110 @ttfFile: the path to the font file (only TTF fonts supported)
111 @fontSize: the font size (default: 10)
112 '''
113 OLED1306._lock.acquire()
114 self.ttfFile = ttfFile
115 self.ttfFileStandard = ttfFile
116 self.fontSize = fontSize
117 self.font = ImageFont.truetype(ttfFile, fontSize)
118 OLED1306._lock.release()
119
121 '''
122 Sets a new font size of current font.
123 @fontSize: the new font size
124 '''
125 OLED1306._lock.acquire()
126 self.font = ImageFont.truetype(self.ttfFile, fontSize)
127 self.fontSize = fontSize
128 OLED1306._lock.release()
129
131 '''
132 Erases the display and clears the text buffer.
133 '''
134 OLED1306._lock.acquire()
135 if self.inverse:
136 self.draw.rectangle((0, 0, self.width, self.height), outline = 0, fill = 255)
137 else:
138 self.draw.rectangle((0, 0, self.width, self.height), outline = 0, fill = 0)
139 self.disp.image(self.image)
140 self.disp.display()
141 self.textBuf = {}
142 self.cursor = [0, 0]
143 self.scroll = False
144 OLED1306._lock.release()
145
147 '''
148 Erases the display without clearing the text buffer.
149 '''
150 OLED1306._lock.acquire()
151 if self.inverse:
152 self.draw.rectangle((0, 0, self.width, self.height), outline = 0, fill = 255)
153 else:
154 self.draw.rectangle((0, 0, self.width, self.height), outline = 0, fill = 0)
155 self.disp.image(self.image)
156 self.disp.display()
157 OLED1306._lock.release()
158
159 - def setText(self, text, lineNum = 0, fontSize = None, indent = 0):
160 '''
161 Displays text at given line left adjusted.
162 The old text of this line is erased, other text is not modified
163 The line distance is defined by the font size (text height + 1).
164 If no text is attributed to a line, the line is considered to consist of a single space
165 character with the font size of the preceeding line.
166 The position of the text cursor is not modified.
167 Text separated by \n is considered as a multiline text. In this case lineNum is the line number of the
168 first line.
169 @param text: the text to display. If emtpy, text with a single space character is assumed.
170 @param lineNum: the line number where to display the text (default: 0)
171 @param fontSize: the size of the font (default: None, set to current font size)
172 @indent: the line indent in pixels (default: 0)
173 '''
174 OLED1306._lock.acquire()
175 if "\n" not in text:
176 self._setLine(text, lineNum, fontSize, indent)
177 else:
178 lines = text.split("\n")
179 nb = lineNum
180 for line in lines:
181 self._setLine(line, nb, fontSize, indent)
182 nb += 1
183 OLED1306._lock.release()
184
185 - def _setLine(self, text, lineNum, fontSize, indent):
186 if text == "":
187 text = " "
188 if fontSize == None:
189 fontSize = self.fontSize
190 self.textBuf[lineNum] = (text, fontSize, indent)
191 self.repaint()
192
194 '''
195 Returns the current font size.
196 @return: the font size
197 '''
198 return self.fontSize
199
201 '''
202 Returns the height of one line.
203 @return: line spacing in pixels
204 '''
205 OLED1306._lock.acquire()
206 charWidthDummy, charHeight = self.draw.textsize('I', font = self.font)
207 OLED1306._lock.release()
208 return charHeight
209
211 '''
212 Repaints the screen (background image and text buffer).
213 '''
214 OLED1306._lock.acquire()
215 if len(self.textBuf) == 0:
216 maxNum = 0
217 else:
218 maxNum = max(self.textBuf.keys())
219
220 if self.inverse:
221 self.draw.rectangle((0, 0, self.width, self.height), outline = 0, fill = 255)
222 else:
223 self.draw.rectangle((0, 0, self.width, self.height), outline = 0, fill = 0)
224
225 if self.bkImagePath != None:
226 picture = Image.open(self.bkImagePath).convert('1')
227 self.image.paste(picture)
228 y = 0
229 lastFontSize = 0
230 for lineNb in range(maxNum + 1):
231 try:
232 text = self.textBuf[lineNb][0]
233 fontSize = self.textBuf[lineNb][1]
234 x = self.textBuf[lineNb][2]
235 except:
236 text = " "
237 fontSize = 10
238 x = 0
239 self.font = ImageFont.truetype(self.ttfFile, fontSize)
240 charWidthDummy, charHeight = self.draw.textsize('A', font = self.font)
241 for c in text:
242 charWidth, charHeightDummy = self.draw.textsize(c, font = self.font)
243 if self.inverse:
244 self.draw.text((x, y), c, font = self.font, fill = 0)
245 else:
246 self.draw.text((x, y), c, font = self.font, fill = 255)
247 x += charWidth
248 y += charHeight
249
250
251 self.disp.image(self.image)
252 self.disp.display()
253 OLED1306._lock.release()
254
256 '''
257 Shows the image (1 pixel monochrome) with given filename (must have size 128x32 for display type 32
258 or 128x64 for display type 64).
259 @param imagePath: the path to the PPM image file
260 '''
261 OLED1306._lock.acquire()
262 picture = Image.open(imagePath).convert('1')
263 self.disp.image(picture)
264 self.disp.display()
265 OLED1306._lock.release()
266
268 '''
269 Appends text at current cursor position and scrolls, if necessary.
270 Sets the cursor at the beginning of next line.
271 @param text: the text to display
272 '''
273 OLED1306._lock.acquire()
274 charWidthDummy, charHeight = self.draw.textsize('I', font = self.font)
275 nbLines = int(self.type / (charHeight))
276
277 if not self.scroll:
278 self.setText(text, self.cursor[1])
279 self.cursor[1] += 1
280 if self.cursor[1] == nbLines:
281 self.scroll = True
282 else:
283 for i in range(nbLines - 1):
284 self.textBuf[i] = self.textBuf[i + 1]
285 self.setText(text, nbLines - 1)
286 OLED1306._lock.release()
287
289 '''
290 Sets the current font size to a maximum to show the given number of lines.
291 @param: the number of lines to display
292 '''
293 OLED1306._lock.acquire()
294 fontSize = int(self.type / nbLines) + 1
295 self.setFontSize(fontSize)
296 OLED1306._lock.release()
297
299 '''
300 @param inverse: if True, the background is white and the text is black;
301 otherwise the background is black and the text is white (default)
302 '''
303 OLED1306._lock.acquire()
304 self.inverse = inverse
305 self.repaint()
306 OLED1306._lock.release()
307
308 - def startBlinker(self, count = 3, offTime = 1000, onTime = 1000, blocking = False):
309 '''
310 Blicks the entire screen for given number of times (off-on periods).
311 @param count: the number of blinking (default: 3)
312 @param offTime: the time the display is erased (in ms, default: 1000)
313 @param onTime: the time the display is shown (in ms, default: 1000)
314 @param blocking: if True, the function blocks until the blinking is finished; otherwise
315 it returns immediately
316 '''
317 if self.blinkerThread != None:
318 self.stopBlinker()
319 self.blinkerThread = BlinkerThread(self, count, offTime, onTime)
320 if blocking:
321 while self.isBlinking():
322 continue
323
325 '''
326 Stops a running blinker.
327 The method blocks until the blinker thread is finished and isBlinkerAlive() returns False.
328 '''
329 if self.blinkerThread != None:
330 self.blinkerThread.stop()
331 self.blinkerThread = None
332
334 '''
335 @return: True, if the blinker is displaying; otherwise False
336 '''
337 time.sleep(0.001)
338 if self.blinkerThread == None:
339 return False
340 return self.blinkerThread.isAlive
341
343 '''
344 Returns True, if the device is detected on the I2C bus;
345 otherwise returns False
346 '''
347 return self._isAvailable
348
349
351 - def __init__(self, display, count, offTime, onTime):
352 Thread.__init__(self)
353 self.display = display
354 self.offTime = offTime
355 self.onTime = onTime
356 self.count = count
357 self.isRunning = False
358 self.isAlive = True
359 self.start()
360 while not self.isRunning:
361 time.sleep(0.1)
362
364 nb = 0
365 time.sleep(1)
366 self.isRunning = True
367 while self.isRunning:
368 self.display.erase()
369 startTime = time.time()
370 while time.time() - startTime < self.offTime / 1000 and self.isRunning:
371 time.sleep(0.001)
372 if not self.isRunning:
373 break
374 nb += 1
375 self.display.repaint()
376 startTime = time.time()
377 while time.time() - startTime < self.onTime / 1000 and self.isRunning:
378 time.sleep(0.001)
379 if not self.isRunning:
380 break
381 if nb == self.count:
382 self.isRunning = False
383 self.isAlive = False
384
386 self.isRunning = False
387