Browse Source

more fonts. more instructions. added images and colors from cccamp23 style guide. text rendering now possible with colors. custom font spacing. images automatically scaled and cropped. fixed qr text and colors. allow exiting test gui with q or esc. better target check.

Thomas Buck 10 months ago
parent
commit
ba91afc6b4

+ 25
- 4
README.md View File

@@ -4,7 +4,7 @@ Render various content to various output devices.
4 4
 
5 5
 ## Quick Start
6 6
 
7
-Just run
7
+Just run:
8 8
 
9 9
     ./manager.py
10 10
 
@@ -12,13 +12,30 @@ and go from there.
12 12
 
13 13
 ## Dependencies
14 14
 
15
-You always need
15
+You always need:
16 16
 
17 17
     pip install pil
18 18
     pip install bdfparser
19 19
     pip install "qrcode[pil]"
20 20
 
21 21
 The rest depends on the output device chosen.
22
+For debugging on your host PC you can use the TestGUI interface with pygame:
23
+
24
+    pip install pygame
25
+
26
+The other currently supported option is using a Raspberry Pi with the [Adafruit RGB Matrix Bonnet](https://shop.pimoroni.com/products/adafruit-rgb-matrix-bonnet-for-raspberry-pi?variant=2257849155594) and a matching [LED Matrix](https://shop.pimoroni.com/products/rgb-led-matrix-panel?variant=35962488650).
27
+The [tutorial](https://learn.adafruit.com/adafruit-rgb-matrix-bonnet-for-raspberry-pi/driving-matrices) suggests using the [Adafruit Raspberry Pi Installer Script for the RGB matrix](https://github.com/adafruit/Raspberry-Pi-Installer-Scripts/blob/339cccfbdd8b503b53186176ff96bead9a13a2f5/rgb-matrix.sh).
28
+This will give you the [hzeller/rpi-rgb-led-matrix](https://github.com/hzeller/rpi-rgb-led-matrix) project which includes the Python bindings used in this project.
29
+
30
+## Adding your own visualizations
31
+
32
+Take a look how others are implemented.
33
+You can chain the different screens together using `Manager` and also check for conditions, as seen in `CheckHTTP`.
34
+This should enable you to quickly create something usable.
35
+
36
+One goal is to run this project on public events.
37
+If this is the case, and you want your own message to appear, simply open up a PR or send an email with a patch.
38
+Because of different versions between host PCs and the Raspbian OS, things may look slightly different, especially regarding stuff like animated GIF viewing.
22 39
 
23 40
 ## Licensing
24 41
 
@@ -31,6 +48,10 @@ This project is licensed as beer-ware:
31 48
     think this stuff is worth it, you can buy me a beer in return.   Thomas Buck
32 49
     ----------------------------------------------------------------------------
33 50
 
34
-The included font from [farsil/ibmfonts](https://github.com/farsil/ibmfonts) is licensed as `CC-BY-SA-4.0`.
51
+The included fonts from [farsil/ibmfonts](https://github.com/farsil/ibmfonts) are licensed as `CC-BY-SA-4.0`.
52
+
53
+The fonts from [cmvnd/fonts](https://github.com/cmvnd/fonts) are licensed as GPLv3.
54
+
55
+The tiny font is from [robey](https://robey.lag.net/2010/01/23/tiny-monospace-font.html) and licensed as CC0.
35 56
 
36
-The included GIFs are from [GifCities](https://gifcities.org/?q=32)
57
+The included GIFs are from [GifCities](https://gifcities.org/?q=32).

+ 18
- 4
camp_small.py View File

@@ -7,6 +7,9 @@
7 7
 # think this stuff is worth it, you can buy me a beer in return.   Thomas Buck
8 8
 # ----------------------------------------------------------------------------
9 9
 
10
+camp_pink = (251, 72, 196)
11
+camp_green = (63, 255, 33)
12
+
10 13
 if __name__ == "__main__":
11 14
     from splash import SplashScreen
12 15
     from draw import ScrollText
@@ -30,18 +33,29 @@ if __name__ == "__main__":
30 33
     success = Manager(t)
31 34
     success.add(ImageScreen(t, "drinka.gif", 0.2, 2, 20.0))
32 35
     success.add(Solid(t, 1.0))
33
-    success.add(QRScreen(t, url, 30.0, "Order:"))
36
+    success.add(QRScreen(t, url, 30.0, "Drinks:", "tom-thumb", (255, 255, 255), (0, 0, 0)))
34 37
     success.add(Solid(t, 1.0))
35 38
 
36 39
     fail = Manager(t)
37 40
     fail.add(ImageScreen(t, "attention.gif", 0.2, 2, 20.0, (0, 0, 0)))
38 41
     fail.add(ScrollText(t, "The UbaBot Cocktail machine is currently closed. Please come back later for more drinks!", "ib8x8u", 2))
39
-    fail.add(Solid(t, 2.0))
42
+    fail.add(Solid(t, 1.0))
40 43
     fail.add(GameOfLife(t, 20, (0, 255, 0), (0, 0, 0), None, 2.0))
41
-    fail.add(Solid(t, 2.0))
44
+    fail.add(Solid(t, 1.0))
42 45
 
43 46
     d = CheckHTTP(url)
44 47
     d.success(success)
45 48
     d.fail(fail)
46 49
 
47
-    t.debug_loop(d.draw)
50
+    m = Manager(t)
51
+    m.add(ScrollText(t, "#CCCAMP23", "lemon", 1, 75, camp_green))
52
+    m.add(Solid(t, 1.0))
53
+    m.add(ImageScreen(t, "Favicon.png", 0, 1, 10.0))
54
+    m.add(Solid(t, 1.0))
55
+    m.add(ScrollText(t, "#CCCAMP23", "lemon", 1, 75, camp_pink))
56
+    m.add(Solid(t, 1.0))
57
+    m.add(d)
58
+    m.add(Solid(t, 1.0))
59
+
60
+    m.restart()
61
+    t.debug_loop(m.draw)

+ 74
- 22
draw.py View File

@@ -1,5 +1,11 @@
1 1
 #!/usr/bin/env python3
2 2
 
3
+# Uses the Python BDF format bitmap font parser:
4
+# https://github.com/tomchen/bdfparser
5
+#
6
+# And the pillow Python Imaging Library:
7
+# https://github.com/python-pillow/Pillow
8
+#
3 9
 # ----------------------------------------------------------------------------
4 10
 # "THE BEER-WARE LICENSE" (Revision 42):
5 11
 # <xythobuz@xythobuz.de> wrote this file.  As long as you retain this notice
@@ -13,8 +19,11 @@ import os
13 19
 import time
14 20
 
15 21
 class DrawText:
16
-    def __init__(self, g):
22
+    def __init__(self, g, fg = (255, 255, 255), bg = (0, 0, 0), c = (0, 255, 0)):
17 23
         self.gui = g
24
+        self.fg = fg
25
+        self.bg = bg
26
+        self.color = c
18 27
 
19 28
         scriptDir = os.path.dirname(os.path.realpath(__file__))
20 29
         fontDir = os.path.join(scriptDir, "fonts")
@@ -25,23 +34,37 @@ class DrawText:
25 34
             if not filename.lower().endswith(".bdf"):
26 35
                 continue
27 36
 
28
-            font = Font(os.path.join(fontDir, filename))
29
-            #print(f"{filename} global size is "
30
-            #    f"{font.headers['fbbx']} x {font.headers['fbby']} (pixel), "
31
-            #    f"it contains {len(font)} glyphs.")
32
-
33
-            # TODO hard-coded per-font offsets
37
+            # TODO hard-coded per-font offsets and spacing adjustment
34 38
             offset = 0
39
+            spacing = 0
35 40
             if filename == "iv18x16u.bdf":
36 41
                 offset = 6
37 42
             elif filename == "ib8x8u.bdf":
38 43
                 offset = 10
44
+            elif filename == "lemon.bdf":
45
+                offset = 8
46
+                spacing = -3
47
+            elif filename == "antidote.bdf":
48
+                offset = 9
49
+                spacing = -1
50
+            elif filename == "uushi.bdf":
51
+                offset = 8
52
+                spacing = -2
53
+            elif filename == "tom-thumb.bdf":
54
+                offset = 12
55
+                spacing = 2
39 56
 
40
-            data = (font, offset, {})
57
+            font = Font(os.path.join(fontDir, filename))
58
+            data = (font, offset, {}, spacing)
41 59
             self.fonts[filename[:-4]] = data
42 60
 
43 61
     def getGlyph(self, c, font):
44
-        f, o, cache = self.fonts[font]
62
+        if not isinstance(font, str):
63
+            # fall-back to first available font
64
+            f, offset, cache, spacing = next(iter(self.fonts))
65
+        else:
66
+            # users font choice
67
+            f, offset, cache, spacing = self.fonts[font]
45 68
 
46 69
         # only render glyphs once, cache resulting image data
47 70
         if not c in cache:
@@ -51,15 +74,19 @@ class DrawText:
51 74
             g = g.replace(1, 2).replace(0, 1).replace(2, 0)
52 75
 
53 76
             # render to pixel data
77
+            bytesdict = {
78
+                0: int(self.fg[2] << 16 | self.fg[1] << 8 | self.fg[0]).to_bytes(4, byteorder = "little"),
79
+                1: int(self.bg[2] << 16 | self.bg[1] << 8 | self.bg[0]).to_bytes(4, byteorder = "little"),
80
+                2: int(self.color[2] << 16 | self.color[1] << 8 | self.color[0]).to_bytes(4, byteorder = "little"),
81
+            }
54 82
             img = Image.frombytes('RGBA',
55 83
                                 (g.width(), g.height()),
56
-                                g.tobytes('RGBA'))
57
-
84
+                                g.tobytes('RGBA', bytesdict))
58 85
             cache[c] = img
59 86
 
60
-        return (cache[c], o)
87
+        return (cache[c], offset, spacing)
61 88
 
62
-    def drawGlyph(self, g, xOff, yOff):
89
+    def drawGlyph(self, g, xOff, yOff, spacing):
63 90
         if xOff >= self.gui.width:
64 91
             return
65 92
 
@@ -72,6 +99,10 @@ class DrawText:
72 99
                 p = g.getpixel((x, y))
73 100
                 self.gui.set_pixel(xTarget, yOff + y, p)
74 101
 
102
+        for x in range(0, spacing):
103
+            for y in range(0, g.height):
104
+                self.gui.set_pixel(xOff + x + g.width, yOff + y, self.bg)
105
+
75 106
     def text(self, s, f, offset = 0, earlyAbort = True, yOff = 0):
76 107
         w = 0
77 108
         for c in s:
@@ -80,17 +111,17 @@ class DrawText:
80 111
                 if xOff >= self.gui.width:
81 112
                     break
82 113
 
83
-            g, y = self.getGlyph(c, f)
84
-            w += g.width
114
+            g, y, spacing = self.getGlyph(c, f)
115
+            w += g.width + spacing
85 116
 
86
-            if xOff >= -10: # some wiggle room so chars dont disappear
87
-                self.drawGlyph(g, xOff, y + yOff)
117
+            if xOff >= -16: # some wiggle room so chars dont disappear
118
+                self.drawGlyph(g, xOff, y + yOff, spacing)
88 119
         return w
89 120
 
90 121
 class ScrollText:
91
-    def __init__(self, g, t, f, i = 1, s = 75):
122
+    def __init__(self, g, t, f, i = 1, s = 75, fg = (255, 255, 255), bg = (0, 0, 0)):
92 123
         self.gui = g
93
-        self.drawer = DrawText(self.gui)
124
+        self.drawer = DrawText(self.gui, fg, bg)
94 125
         self.text = t
95 126
         self.font = f
96 127
         self.iterations = i
@@ -122,6 +153,27 @@ if __name__ == "__main__":
122 153
     import util
123 154
     t = util.getTarget()
124 155
 
125
-    #d = ScrollText(t, "This is a long scrolling text. Is it too fast or maybe too slow?", "iv18x16u")
126
-    d = ScrollText(t, "This is a long scrolling text. Is it too fast or maybe too slow?", "ib8x8u")
127
-    t.debug_loop(d.draw)
156
+    from splash import SplashScreen
157
+    splash = SplashScreen(t)
158
+    t.loop_start()
159
+    splash.draw()
160
+    t.loop_end()
161
+
162
+    from manager import Manager
163
+    m = Manager(t)
164
+
165
+    m.add(ScrollText(t, "tom-thumb Abcdefgh tom-thumb", "tom-thumb",
166
+                     1, 75, (0, 255, 0), (0, 0, 255)))
167
+    m.add(ScrollText(t, "antidote Abcdefgh antidote", "antidote",
168
+                     1, 75, (0, 255, 0), (0, 0, 255)))
169
+    m.add(ScrollText(t, "uushi Abcdefgh uushi", "uushi",
170
+                     1, 75, (0, 255, 0), (0, 0, 255)))
171
+    m.add(ScrollText(t, "lemon Abcdefgh lemon", "lemon",
172
+                     1, 75, (0, 255, 0), (0, 0, 255)))
173
+    m.add(ScrollText(t, "ib8x8u Abcdefgh ib8x8u", "ib8x8u",
174
+                     1, 75, (0, 255, 0), (0, 0, 255)))
175
+    m.add(ScrollText(t, "iv18x16u Abcdefgh iv18x16u", "iv18x16u",
176
+                     1, 75, (0, 255, 0), (0, 0, 255)))
177
+
178
+    m.restart()
179
+    t.debug_loop(m.draw)

+ 2614
- 0
fonts/antidote.bdf
File diff suppressed because it is too large
View File


+ 37441
- 0
fonts/lemon.bdf
File diff suppressed because it is too large
View File


+ 2353
- 0
fonts/tom-thumb.bdf
File diff suppressed because it is too large
View File


+ 14695
- 0
fonts/uushi.bdf
File diff suppressed because it is too large
View File


+ 20
- 1
image.py View File

@@ -1,5 +1,8 @@
1 1
 #!/usr/bin/env python3
2 2
 
3
+# Uses the pillow Python Imaging Library:
4
+# https://github.com/python-pillow/Pillow
5
+#
3 6
 # ----------------------------------------------------------------------------
4 7
 # "THE BEER-WARE LICENSE" (Revision 42):
5 8
 # <xythobuz@xythobuz.de> wrote this file.  As long as you retain this notice
@@ -12,7 +15,7 @@ import time
12 15
 import os
13 16
 
14 17
 class ImageScreen:
15
-    def __init__(self, g, p, t = 0.2, i = 1, to = 20.0, bg = None):
18
+    def __init__(self, g, p, t = 0.2, i = 1, to = 10.0, bg = None):
16 19
         self.gui = g
17 20
         self.time = t
18 21
         self.iterations = i
@@ -22,6 +25,22 @@ class ImageScreen:
22 25
         scriptDir = os.path.dirname(os.path.realpath(__file__))
23 26
         self.path = os.path.join(scriptDir, "images", p)
24 27
         self.image = Image.open(self.path)
28
+
29
+        # for some reason non-animated images don't even have this attribute
30
+        if not hasattr(self.image, "is_animated"):
31
+            self.image.is_animated = False
32
+            self.image.n_frames = 1
33
+
34
+        # automatically crop and scale large images
35
+        if not self.image.is_animated and ((self.image.width > self.gui.width) or (self.image.height > self.gui.height)):
36
+            self.image = self.image.crop(self.image.getbbox())
37
+            self.image = self.image.resize((self.gui.width, self.gui.height),
38
+                                           Image.Resampling.NEAREST)
39
+
40
+            # new image object is also missing these
41
+            self.image.is_animated = False
42
+            self.image.n_frames = 1
43
+
25 44
         print(p, self.image.width, self.image.height, self.image.is_animated, self.image.n_frames)
26 45
 
27 46
         self.xOff = int((self.gui.width - self.image.width) / 2)

BIN
images/Favicon.png View File


BIN
images/Favicon_Accessability.png View File


BIN
images/Favicon_General.png View File


BIN
images/Favicon_Kitchens.png View File


BIN
images/Favicon_OffGrid.png View File


BIN
images/Favicon_Offtopic.png View File


BIN
images/Favicon_Trains.png View File


BIN
images/Favicon_Villages.png View File


+ 6
- 0
pi.py View File

@@ -1,5 +1,11 @@
1 1
 #!/usr/bin/env python3
2 2
 
3
+# Uses the Python bindings for the Raspberry Pi RGB LED Matrix library:
4
+# https://github.com/hzeller/rpi-rgb-led-matrix
5
+#
6
+# And the pillow Python Imaging Library:
7
+# https://github.com/python-pillow/Pillow
8
+#
3 9
 # ----------------------------------------------------------------------------
4 10
 # "THE BEER-WARE LICENSE" (Revision 42):
5 11
 # <xythobuz@xythobuz.de> wrote this file.  As long as you retain this notice

+ 23
- 10
qr.py View File

@@ -1,5 +1,8 @@
1 1
 #!/usr/bin/env python3
2 2
 
3
+# Uses the Python QR code image generator:
4
+# https://github.com/lincolnloop/python-qrcode
5
+#
3 6
 # ----------------------------------------------------------------------------
4 7
 # "THE BEER-WARE LICENSE" (Revision 42):
5 8
 # <xythobuz@xythobuz.de> wrote this file.  As long as you retain this notice
@@ -13,10 +16,11 @@ import util
13 16
 from draw import DrawText
14 17
 
15 18
 class QRScreen:
16
-    def __init__(self, g, d, t = 10.0, h = None, c1 = (0, 0, 0), c2 = (255, 255, 255)):
19
+    def __init__(self, g, d, t = 10.0, h = None, f = None, c1 = (0, 0, 0), c2 = (255, 255, 255)):
17 20
         self.gui = g
18 21
         self.time = t
19 22
         self.heading = h
23
+        self.font = f
20 24
         self.c1 = c1
21 25
         self.c2 = c2
22 26
 
@@ -29,17 +33,26 @@ class QRScreen:
29 33
 
30 34
         if util.isPi():
31 35
             # work-around for weird bug in old qrcode lib?
32
-            self.image = qr.make_image(fill_color = "black", back_color = "white")
33
-            self.c1 = (0, 0, 0)
34
-            self.c2 = (255, 255, 255)
36
+            if (self.c1 == (0, 0, 0)) and (self.c2 == (255, 255, 255)):
37
+                self.image = qr.make_image(fill_color = "black", back_color = "white")
38
+                self.c1 = (0, 0, 0)
39
+                self.c2 = (255, 255, 255)
40
+            elif (self.c1 == (255, 255, 255)) and (self.c2 == (0, 0, 0)):
41
+                self.image = qr.make_image(fill_color = "white", back_color = "black")
42
+                self.c1 = (255, 255, 255)
43
+                self.c2 = (0, 0, 0)
44
+            else:
45
+                raise RuntimeError("QR colors other than black/white not supported on Pi")
35 46
         else:
36 47
             self.image = qr.make_image(fill_color = self.c1, back_color = self.c2)
37 48
 
38 49
         if self.heading != None:
39
-            self.text = DrawText(self.gui)
50
+            self.text = DrawText(self.gui, self.c1, self.c2)
51
+            self.yOff = self.gui.height - self.image.height
52
+        else:
53
+            self.yOff = int((self.gui.height - self.image.height) / 2)
40 54
 
41 55
         self.xOff = int((self.gui.width - self.image.width) / 2)
42
-        self.yOff = int((self.gui.height - self.image.height) / 2)
43 56
 
44 57
         self.restart()
45 58
 
@@ -63,6 +76,9 @@ class QRScreen:
63 76
                 for x in range(0, self.gui.width - self.image.width - self.xOff):
64 77
                     self.gui.set_pixel(x + self.xOff + self.image.width, y + self.yOff, self.c2)
65 78
 
79
+        if self.heading != None:
80
+            self.text.text(self.heading, self.font, 0, True, -10)
81
+
66 82
         for x in range(0, self.image.width):
67 83
             for y in range(0, self.image.height):
68 84
                 v = self.image.getpixel((x, y))
@@ -70,12 +86,9 @@ class QRScreen:
70 86
                     v = (v, v, v)
71 87
                 self.gui.set_pixel(x + self.xOff, y + self.yOff, v)
72 88
 
73
-        if self.heading != None:
74
-            self.text.text(self.heading, "ib8x8u", 0, True, -10)
75
-
76 89
 if __name__ == "__main__":
77 90
     import util
78 91
     t = util.getTarget()
79 92
 
80
-    d = QRScreen(t, "Hello World", 10.0, "Test")
93
+    d = QRScreen(t, "Hello World", 10.0, "Drinks:", "tom-thumb", (255, 255, 255), (0, 0, 0))
81 94
     t.debug_loop(d.draw)

+ 6
- 0
test.py View File

@@ -1,5 +1,8 @@
1 1
 #!/usr/bin/env python3
2 2
 
3
+# Uses the pygame SDL wrapper:
4
+# https://github.com/pygame/pygame
5
+#
3 6
 # ----------------------------------------------------------------------------
4 7
 # "THE BEER-WARE LICENSE" (Revision 42):
5 8
 # <xythobuz@xythobuz.de> wrote this file.  As long as you retain this notice
@@ -27,6 +30,9 @@ class TestGUI:
27 30
         for event in pygame.event.get():
28 31
             if event.type == pygame.QUIT:
29 32
                 return True
33
+            elif event.type == pygame.KEYUP:
34
+                if (event.key == pygame.K_q) or (event.key == pygame.K_ESCAPE):
35
+                    return True
30 36
 
31 37
         self.screen.fill("black")
32 38
         return False

+ 26
- 8
util.py View File

@@ -7,17 +7,35 @@
7 7
 # think this stuff is worth it, you can buy me a beer in return.   Thomas Buck
8 8
 # ----------------------------------------------------------------------------
9 9
 
10
-import platform
10
+targetIsPi = None
11 11
 
12 12
 def isPi():
13
-    return platform.machine() == "armv7l"
13
+    global targetIsPi
14
+
15
+    if targetIsPi == None:
16
+        getTarget()
17
+    return targetIsPi
14 18
 
15 19
 def getTarget():
16
-    t = None
17
-    if isPi():
20
+    global targetIsPi
21
+
22
+    target = None
23
+    try:
18 24
         from pi import PiMatrix
19
-        t = PiMatrix()
20
-    else:
25
+        target = PiMatrix()
26
+
27
+        if targetIsPi == None:
28
+            # only print once
29
+            print("Raspberry Pi Adafruit RGB LED Matrix detected")
30
+
31
+        targetIsPi = True
32
+    except ModuleNotFoundError:
21 33
         from test import TestGUI
22
-        t = TestGUI()
23
-    return t
34
+        target = TestGUI()
35
+
36
+        if targetIsPi == None:
37
+            # only print once
38
+            print("Falling back to GUI debug interface")
39
+
40
+        targetIsPi = False
41
+    return target

Loading…
Cancel
Save