RGBMatrixEmulator 0.16.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. RGBMatrixEmulator/__init__.py +5 -0
  2. RGBMatrixEmulator/adapters/__init__.py +42 -0
  3. RGBMatrixEmulator/adapters/base.py +229 -0
  4. RGBMatrixEmulator/adapters/browser_adapter/README.md +62 -0
  5. RGBMatrixEmulator/adapters/browser_adapter/__init__.py +0 -0
  6. RGBMatrixEmulator/adapters/browser_adapter/adapter.py +80 -0
  7. RGBMatrixEmulator/adapters/browser_adapter/fps.py +26 -0
  8. RGBMatrixEmulator/adapters/browser_adapter/request_handlers/__init__.py +14 -0
  9. RGBMatrixEmulator/adapters/browser_adapter/request_handlers/base.py +25 -0
  10. RGBMatrixEmulator/adapters/browser_adapter/request_handlers/image.py +14 -0
  11. RGBMatrixEmulator/adapters/browser_adapter/request_handlers/image_web_socket.py +60 -0
  12. RGBMatrixEmulator/adapters/browser_adapter/request_handlers/main.py +11 -0
  13. RGBMatrixEmulator/adapters/browser_adapter/request_handlers/single_file.py +19 -0
  14. RGBMatrixEmulator/adapters/browser_adapter/server.py +98 -0
  15. RGBMatrixEmulator/adapters/browser_adapter/static/assets/client.js +78 -0
  16. RGBMatrixEmulator/adapters/browser_adapter/static/assets/icon.ico +0 -0
  17. RGBMatrixEmulator/adapters/browser_adapter/static/assets/styles.css +25 -0
  18. RGBMatrixEmulator/adapters/browser_adapter/static/index.html +145 -0
  19. RGBMatrixEmulator/adapters/pi5_adapter/README.md +62 -0
  20. RGBMatrixEmulator/adapters/pi5_adapter/__init__.py +245 -0
  21. RGBMatrixEmulator/adapters/pygame_adapter.py +61 -0
  22. RGBMatrixEmulator/adapters/raw_adapter/README.md +48 -0
  23. RGBMatrixEmulator/adapters/raw_adapter/__init__.py +43 -0
  24. RGBMatrixEmulator/adapters/sixel_adapter.py +91 -0
  25. RGBMatrixEmulator/adapters/terminal_adapter.py +35 -0
  26. RGBMatrixEmulator/adapters/tkinter_adapter.py +79 -0
  27. RGBMatrixEmulator/adapters/turtle_adapter.py +92 -0
  28. RGBMatrixEmulator/cli/__init__.py +42 -0
  29. RGBMatrixEmulator/cli/cli.py +2 -0
  30. RGBMatrixEmulator/cli/command.py +7 -0
  31. RGBMatrixEmulator/cli/config.py +11 -0
  32. RGBMatrixEmulator/emulation/__init__.py +0 -0
  33. RGBMatrixEmulator/emulation/canvas.py +80 -0
  34. RGBMatrixEmulator/emulation/matrix.py +64 -0
  35. RGBMatrixEmulator/emulation/options.py +47 -0
  36. RGBMatrixEmulator/graphics/__init__.py +194 -0
  37. RGBMatrixEmulator/graphics/color.py +40 -0
  38. RGBMatrixEmulator/graphics/font.py +36 -0
  39. RGBMatrixEmulator/icon.png +0 -0
  40. RGBMatrixEmulator/internal/adapter_loader.py +86 -0
  41. RGBMatrixEmulator/internal/emulator_config.py +198 -0
  42. RGBMatrixEmulator/internal/pixel_style.py +20 -0
  43. RGBMatrixEmulator/logger.py +28 -0
  44. RGBMatrixEmulator/version.py +5 -0
  45. rgbmatrixemulator-0.16.0.data/data/RGBMatrixEmulator/client.js +78 -0
  46. rgbmatrixemulator-0.16.0.data/data/RGBMatrixEmulator/icon.ico +0 -0
  47. rgbmatrixemulator-0.16.0.data/data/RGBMatrixEmulator/icon.png +0 -0
  48. rgbmatrixemulator-0.16.0.data/data/RGBMatrixEmulator/index.html +145 -0
  49. rgbmatrixemulator-0.16.0.data/data/RGBMatrixEmulator/styles.css +25 -0
  50. rgbmatrixemulator-0.16.0.data/data/docs/LICENSE +9 -0
  51. rgbmatrixemulator-0.16.0.data/data/docs/README.md +306 -0
  52. rgbmatrixemulator-0.16.0.dist-info/METADATA +336 -0
  53. rgbmatrixemulator-0.16.0.dist-info/RECORD +56 -0
  54. rgbmatrixemulator-0.16.0.dist-info/WHEEL +4 -0
  55. rgbmatrixemulator-0.16.0.dist-info/entry_points.txt +2 -0
  56. rgbmatrixemulator-0.16.0.dist-info/licenses/LICENSE +9 -0
@@ -0,0 +1,5 @@
1
+ from RGBMatrixEmulator.graphics import *
2
+ from RGBMatrixEmulator.emulation.matrix import RGBMatrix
3
+ from RGBMatrixEmulator.emulation.options import RGBMatrixOptions
4
+
5
+ from RGBMatrixEmulator.version import __version__
@@ -0,0 +1,42 @@
1
+ ADAPTERS = {
2
+ "browser": {
3
+ "path": "RGBMatrixEmulator.adapters.browser_adapter.adapter",
4
+ "class": "BrowserAdapter",
5
+ "fallback": True,
6
+ },
7
+ "pygame": {
8
+ "path": "RGBMatrixEmulator.adapters.pygame_adapter",
9
+ "class": "PygameAdapter",
10
+ "fallback": True,
11
+ },
12
+ "sixel": {
13
+ "path": "RGBMatrixEmulator.adapters.sixel_adapter",
14
+ "class": "SixelAdapter",
15
+ "fallback": True,
16
+ },
17
+ "terminal": {
18
+ "path": "RGBMatrixEmulator.adapters.terminal_adapter",
19
+ "class": "TerminalAdapter",
20
+ "fallback": True,
21
+ },
22
+ "tkinter": {
23
+ "path": "RGBMatrixEmulator.adapters.tkinter_adapter",
24
+ "class": "TkinterAdapter",
25
+ "fallback": True,
26
+ },
27
+ "turtle": {
28
+ "path": "RGBMatrixEmulator.adapters.turtle_adapter",
29
+ "class": "TurtleAdapter",
30
+ "fallback": True,
31
+ },
32
+ "raw": {
33
+ "path": "RGBMatrixEmulator.adapters.raw_adapter",
34
+ "class": "RawAdapter",
35
+ "fallback": False,
36
+ },
37
+ "pi5": {
38
+ "path": "RGBMatrixEmulator.adapters.pi5_adapter",
39
+ "class": "Pi5Adapter",
40
+ "fallback": False,
41
+ },
42
+ }
@@ -0,0 +1,229 @@
1
+ import numpy as np
2
+ from pathlib import Path
3
+
4
+ from PIL import Image, ImageDraw
5
+ from RGBMatrixEmulator import version
6
+ from RGBMatrixEmulator.logger import Logger
7
+ from RGBMatrixEmulator.internal.pixel_style import PixelStyle
8
+
9
+
10
+ class BaseAdapter:
11
+ SUPPORTED_PIXEL_STYLES = [PixelStyle.DEFAULT]
12
+ INSTANCE = None
13
+
14
+ DEFAULT_MASK_FN = "_draw_square_mask"
15
+ MASK_FNS = {
16
+ PixelStyle.SQUARE: "_draw_square_mask",
17
+ PixelStyle.CIRCLE: "_draw_circle_mask",
18
+ PixelStyle.REAL: "_draw_real_mask",
19
+ }
20
+
21
+ DEBUG_TEXT_TEMPLATE = (
22
+ "RGBME v{} - {}x{} Matrix | {}x{} Chain | {}px per LED ({}) | {}"
23
+ )
24
+
25
+ ICON_FORMATS = ["PNG", "ICO", "JPEG"]
26
+ ICON_MAX_SIZE = (512, 512)
27
+
28
+ def __init__(self, width, height, options):
29
+ self.width = width
30
+ self.height = height
31
+ self.options = options
32
+ self.__black = Image.new("RGB", self.options.window_size(), "black")
33
+ self.__mask = self.__draw_mask()
34
+ self.loaded = False
35
+
36
+ self.emulator_title = self.options.emulator_title or str(self)
37
+
38
+ self.default_icon_path = (Path(__file__).parent / ".." / "icon.png").resolve()
39
+ self._set_icon_path()
40
+
41
+ @classmethod
42
+ def get_instance(cls, *args, **kwargs):
43
+ if cls.INSTANCE is None:
44
+ instance = cls(*args, **kwargs)
45
+ cls.INSTANCE = instance
46
+
47
+ return cls.INSTANCE
48
+
49
+ # This method is required for the pygame adapter but nothing else, so just skip it if not defined.
50
+ def check_for_quit_event(self):
51
+ pass
52
+
53
+ #############################################################
54
+ # These methods must be implemented by BaseAdapter subclasses
55
+ #############################################################
56
+ def load_emulator_window(self):
57
+ """
58
+ Initialize the external dependency as a graphics display.
59
+
60
+ This method is fired when the emulated canvas is initialized.
61
+ """
62
+ raise NotImplementedError
63
+
64
+ def draw_to_screen(self, _pixels):
65
+ """
66
+ Accepts a 2D array of pixels of size height x width.
67
+
68
+ Implements drawing each pixel to the screen via the external dependency loaded in load_emulator_window.
69
+ """
70
+ raise NotImplementedError
71
+
72
+ #############################################################
73
+ # These methods implement pixel masks (styles)
74
+ #############################################################
75
+ def _get_masked_image(self, pixels):
76
+ image = Image.fromarray(np.array(pixels, dtype=np.uint8), "RGB")
77
+ image = image.resize(self.options.window_size(), Image.NEAREST)
78
+
79
+ return Image.composite(image, self.__black, self.__mask)
80
+
81
+ def __mask_fn(self, pixel_style):
82
+ if pixel_style not in self.MASK_FNS:
83
+ Logger.warning(
84
+ f"Pixel style '{pixel_style.config_name}' mask function not found, defaulting to {self.DEFAULT_MASK_FN}..."
85
+ )
86
+
87
+ return getattr(self, self.MASK_FNS.get(pixel_style, self.DEFAULT_MASK_FN))
88
+
89
+ def __draw_mask(self):
90
+ mask = Image.new("L", self.options.window_size())
91
+
92
+ draw_fn = self.__mask_fn(self.options.pixel_style)
93
+ draw_fn(mask)
94
+
95
+ return mask
96
+
97
+ def _draw_circle_mask(self, mask):
98
+ pixel_size = self.options.pixel_size
99
+ width, height = self.options.window_size()
100
+
101
+ drawer = ImageDraw.Draw(mask)
102
+
103
+ for y in range(0, height, pixel_size):
104
+ for x in range(0, width, pixel_size):
105
+ drawer.ellipse(
106
+ (x, y, x + pixel_size - 1, y + pixel_size - 1),
107
+ fill=255,
108
+ outline=255,
109
+ )
110
+
111
+ def _draw_square_mask(self, mask):
112
+ pixel_size = self.options.pixel_size
113
+ width, height = self.options.window_size()
114
+
115
+ drawer = ImageDraw.Draw(mask)
116
+
117
+ for y in range(0, height, pixel_size):
118
+ for x in range(0, width, pixel_size):
119
+ drawer.rectangle(
120
+ (x, y, x + pixel_size, y + pixel_size),
121
+ fill=255,
122
+ outline=255,
123
+ )
124
+
125
+ def _draw_real_mask(self, mask):
126
+ pixel_size = self.options.pixel_size
127
+ width, height = self.options.window_size()
128
+ pixel_glow = self.options.pixel_glow
129
+
130
+ if pixel_glow == 0:
131
+ # Short circuit to a faster draw routine
132
+ return self._draw_circle_mask(mask)
133
+
134
+ # Create two gradients.
135
+ # The first is the LED with a gradient amount of 1 to antialias the result.
136
+ # The second is the actual glow given the setting.
137
+ gradient = self._gradient_add(
138
+ self._generate_gradient(pixel_size, 1),
139
+ self._generate_gradient(pixel_size, pixel_glow),
140
+ )
141
+
142
+ pixel = Image.fromarray(gradient.astype(np.uint8))
143
+
144
+ # Paste the pixel into the mask at each point.
145
+ for y in range(0, height, pixel_size):
146
+ for x in range(0, width, pixel_size):
147
+ mask.paste(pixel, (x, y), pixel)
148
+
149
+ def _generate_gradient(self, sz, amt):
150
+ """
151
+ Generates a radial gradient of size sz with glow amt.
152
+
153
+ The resulting array is normalized between [0, 255].
154
+ """
155
+ # Calculate our own radial gradient to use in the mask.
156
+ # PIL.Image.radial_gradient() produces subpar results (LEDs look blocky)
157
+ x = np.linspace(0, sz, sz)
158
+ y = np.linspace(0, sz, sz)
159
+
160
+ # Discrete points (x, y) between 0 and pixel_size
161
+ X, Y = np.meshgrid(x, y)
162
+
163
+ # Distance of point to center
164
+ center = sz / 2
165
+ D = np.sqrt((X - center) ** 2 + (Y - center) ** 2)
166
+
167
+ # LED radius
168
+ L = (sz - amt) / 2
169
+ G = amt
170
+
171
+ # Calculate the radial gradient glow, clip it between 0 and 1, scale by 255 (WHITE), and get the additive inverse
172
+ gradient = 255 - np.clip((D - L) / G, 0, 1) * 255
173
+
174
+ return gradient
175
+
176
+ def _gradient_add(self, g1, g2):
177
+ """
178
+ Adds two arrays g1 and g2 with equal weight, and normalizes between [0, 255].
179
+ Assumes the input arrays are normalized between [0, 255].
180
+
181
+ Each pair of points is added with equal weight by first subtracting 128,
182
+ which produces fewer graphical artifacts than summation by average of the two points.
183
+ """
184
+ return np.clip((g1 - 128) + (g2 - 128), 0, 255)
185
+
186
+ def _set_icon_path(self):
187
+ self.icon_path = self.default_icon_path
188
+ custom_path = self.options.icon_path
189
+
190
+ if not custom_path:
191
+ return
192
+
193
+ try:
194
+ icon = Image.open(custom_path)
195
+
196
+ if icon.format not in self.ICON_FORMATS:
197
+ Logger.info(
198
+ f"Custom icon format '{icon.format}' is not in allowed formats ({self.ICON_FORMATS}). Using default icon instead."
199
+ )
200
+ return
201
+
202
+ if (
203
+ icon.width > self.ICON_MAX_SIZE[0]
204
+ or icon.height > self.ICON_MAX_SIZE[1]
205
+ ):
206
+ Logger.info(
207
+ f"Icon of size '{icon.size}' is too large (max size is {self.ICON_MAX_SIZE}). Using default icon instead."
208
+ )
209
+ return
210
+ except Exception as e:
211
+ Logger.exception(
212
+ "Encountered exception while loading custom icon. Using default icon instead."
213
+ )
214
+ Logger.exception(e)
215
+ return
216
+
217
+ self.icon_path = self.options.icon_path
218
+
219
+ def __str__(self):
220
+ return self.DEBUG_TEXT_TEMPLATE.format(
221
+ version.__version__,
222
+ self.options.cols,
223
+ self.options.rows,
224
+ self.options.chain_length,
225
+ self.options.parallel,
226
+ self.options.pixel_size,
227
+ self.options.pixel_style.name,
228
+ self.__class__.__name__,
229
+ )
@@ -0,0 +1,62 @@
1
+ # `browser` Display Adapter
2
+
3
+ The `browser` adapter operates differently than other `RGBMatrixEmulator` display adapters. Under the hood, the adapter is a full webserver and WebSocket wrapper and includes its own JS client that can render an emulated image from a Python script within the browser.
4
+
5
+ ![browser-adapter](../../../assets/browser-adapter.gif)
6
+
7
+ ## Running the `browser` Display Adapter Server
8
+
9
+ For configuration of the display adapter, check the main [README's configuration section](../../../README.md#configuration-options).
10
+
11
+ Startup of the script remains unchanged, just like with other display adapters. Additional command line flags work as usual.
12
+
13
+ After running your startup script, you should get a notice that your server is starting on your configured port. For the above example:
14
+
15
+ ```
16
+ C:\Users\tyler\development\RGBMatrixEmulator\samples>python runtext.py
17
+ Press CTRL-C to stop sample
18
+ RGBME v0.6.0 - 32x32 Matrix | 1x1 Chain | 16px per LED (SQUARE) | BrowserAdapter
19
+ Starting server...
20
+ Server started and ready to accept requests on http://localhost:8888/
21
+ ```
22
+
23
+ Once this is configured, your server is up and running and ready to accept requests on the configured port.
24
+
25
+ ## Viewing the Emulator
26
+
27
+ ### Via Websocket
28
+
29
+ By default, the server is configured to `http://localhost:8888/`. Simply navigate to this in the web browser of your choice and you should see streaming images once loaded.
30
+
31
+ If you've configured a custom port, you can navigate to the correct URL instead: `http://localhost:$PORT_NUMBER/`.
32
+
33
+ Once a browser is connected to the server, you will see the following message(s) in your terminal window:
34
+
35
+ ```
36
+ WebSocket opened from: ::1
37
+ WebSocket opened from: 127.0.0.1
38
+ ```
39
+
40
+ This indicates a successful connection has occurred.
41
+
42
+ ### Via Static Image
43
+
44
+ :warning: **This functionality is experimental!** :warning:
45
+
46
+ The emulator also exposes static images in a format you configure. By default, the server is configured to expose this endpoint at `http://localhost:8888/image`.
47
+
48
+ This can be used to allow applications to poll the webserver for updated images or where a websocket is not applicable.
49
+
50
+ ## Error Handling
51
+
52
+ Exceptions in emulated Python scripts will cause the server to shut down. Fix the errors in the script before attempting to restart.
53
+
54
+ The Javascript client attempts to handle errors from the browser gracefully. If for some reason the socket is unable to fetch the next frame, it will retry the fetch every 6 seconds up to 10 times (for a total of 1 minute of retries). Once the max retry count is exhausted, it will stop retrying as there is likely another issue occurring.
55
+
56
+ You can view the errors via a console in the browser (which can be opened with hotkey `F12` or right click -> "Inspect").
57
+
58
+ ## Known Issues
59
+
60
+ * A socket connection is never re-established when shutting down the server (either via `SIGINT` or unhandled exception) and restarting it before the client hits max retry count
61
+ * Excess blob references do not get cleaned up correctly in some instances, leading to memory leaks in some browsers
62
+ * Main symptom appears to be a hanging browser when closing the emulator tab
File without changes
@@ -0,0 +1,80 @@
1
+ import io, webbrowser
2
+ from pathlib import Path
3
+
4
+ from RGBMatrixEmulator.adapters.base import BaseAdapter
5
+ from RGBMatrixEmulator.internal.pixel_style import PixelStyle
6
+ from RGBMatrixEmulator.adapters.browser_adapter.server import Server
7
+ from RGBMatrixEmulator.logger import Logger
8
+
9
+
10
+ class BrowserAdapter(BaseAdapter):
11
+ SUPPORTED_PIXEL_STYLES = [
12
+ PixelStyle.SQUARE,
13
+ PixelStyle.CIRCLE,
14
+ PixelStyle.REAL,
15
+ ]
16
+ IMAGE_FORMATS = {"bmp": "BMP", "jpeg": "JPEG", "png": "PNG", "webp": "WebP"}
17
+
18
+ def __init__(self, width, height, options):
19
+ super().__init__(width, height, options)
20
+ self.__server = None
21
+ self.image = None
22
+ self.image_ready = False
23
+ self.default_image_format = "JPEG"
24
+
25
+ image_format = options.browser.image_format
26
+ if image_format.lower() in self.IMAGE_FORMATS:
27
+ self.image_format = self.IMAGE_FORMATS[image_format.lower()]
28
+ else:
29
+ Logger.warning(
30
+ "Invalid browser image format '{}', falling back to '{}'".format(
31
+ image_format, self.default_image_format
32
+ )
33
+ )
34
+ self.image_format = self.IMAGE_FORMATS.get(
35
+ self.default_image_format.lower()
36
+ )
37
+
38
+ # Default icon path is browser adapter assets
39
+ self.default_icon_path = str(
40
+ (Path(__file__).parent / "static" / "assets" / "icon.ico").resolve()
41
+ )
42
+ self._set_icon_path()
43
+
44
+ def load_emulator_window(self):
45
+ if self.loaded:
46
+ return
47
+
48
+ Logger.info(self.emulator_title)
49
+
50
+ self.__server = Server(self)
51
+ self.__server.run()
52
+
53
+ self.loaded = True
54
+
55
+ self.__open_browser()
56
+
57
+ def draw_to_screen(self, pixels):
58
+ image = self._get_masked_image(pixels)
59
+ with io.BytesIO() as bytesIO:
60
+ image.save(
61
+ bytesIO,
62
+ self.image_format,
63
+ quality=self.options.browser.quality,
64
+ optimize=True,
65
+ )
66
+ self.image = bytesIO.getvalue()
67
+
68
+ self.image_ready = True
69
+
70
+ def __open_browser(self):
71
+ if self.options.browser.open_immediately:
72
+ try:
73
+ uri = f"http://localhost:{self.options.browser.port}"
74
+ Logger.info(
75
+ f"Browser adapter configured to open immediately, opening new window/tab to {uri}"
76
+ )
77
+ webbrowser.open(uri)
78
+ except Exception as e:
79
+ Logger.exception("Failed to open a browser window")
80
+ Logger.exception(e)
@@ -0,0 +1,26 @@
1
+ import time
2
+ from RGBMatrixEmulator.logger import Logger
3
+
4
+ DEFAULT_UPDATE_RATE = 0.5 # seconds
5
+
6
+
7
+ class FPSMonitor:
8
+ def __init__(self, update_rate=DEFAULT_UPDATE_RATE):
9
+ self.update_rate = update_rate
10
+ self.n_frames = 0
11
+ self.start_time = time.time()
12
+
13
+ def tick(self):
14
+ self.n_frames += 1
15
+ now = time.time()
16
+ elapsed = now - self.start_time
17
+
18
+ if elapsed >= self.update_rate:
19
+ fps = round(self.n_frames / elapsed, 2)
20
+
21
+ Logger.debug(
22
+ f"FPS: {fps} (received {self.n_frames} frames over {round(elapsed, 2)}s)"
23
+ )
24
+
25
+ self.n_frames = 0
26
+ self.start_time = now
@@ -0,0 +1,14 @@
1
+ from RGBMatrixEmulator.adapters.browser_adapter.request_handlers.base import (
2
+ NoCacheRequestHandler,
3
+ NoCacheStaticFileHandler,
4
+ )
5
+ from RGBMatrixEmulator.adapters.browser_adapter.request_handlers.main import MainHandler
6
+ from RGBMatrixEmulator.adapters.browser_adapter.request_handlers.image import (
7
+ ImageHandler,
8
+ )
9
+ from RGBMatrixEmulator.adapters.browser_adapter.request_handlers.image_web_socket import (
10
+ ImageWebSocketHandler,
11
+ )
12
+ from RGBMatrixEmulator.adapters.browser_adapter.request_handlers.single_file import (
13
+ SingleFileHandler,
14
+ )
@@ -0,0 +1,25 @@
1
+ import tornado.web
2
+
3
+
4
+ class NoCacheRequestHandler(tornado.web.RequestHandler):
5
+ """Base handler that adds no-cache headers to all responses."""
6
+
7
+ def set_default_headers(self):
8
+ """Set headers to prevent browser caching."""
9
+ self.set_header(
10
+ "Cache-Control", "no-store, no-cache, must-revalidate, max-age=0"
11
+ )
12
+ self.set_header("Pragma", "no-cache")
13
+ self.set_header("Expires", "0")
14
+
15
+
16
+ class NoCacheStaticFileHandler(tornado.web.StaticFileHandler):
17
+ """Static file handler that adds no-cache headers to all responses."""
18
+
19
+ def set_default_headers(self):
20
+ """Set headers to prevent browser caching."""
21
+ self.set_header(
22
+ "Cache-Control", "no-store, no-cache, must-revalidate, max-age=0"
23
+ )
24
+ self.set_header("Pragma", "no-cache")
25
+ self.set_header("Expires", "0")
@@ -0,0 +1,14 @@
1
+ from RGBMatrixEmulator.adapters.browser_adapter.request_handlers import (
2
+ NoCacheRequestHandler,
3
+ )
4
+
5
+
6
+ class ImageHandler(NoCacheRequestHandler):
7
+ def get(self):
8
+ self.set_header(
9
+ "Content-type", "image/{}".format(self.adapter.image_format.lower())
10
+ )
11
+ self.write(self.adapter.image)
12
+
13
+ def register_adapter(adapter):
14
+ ImageHandler.adapter = adapter
@@ -0,0 +1,60 @@
1
+ import tornado.websocket
2
+
3
+ from RGBMatrixEmulator.logger import Logger
4
+ from RGBMatrixEmulator.adapters.browser_adapter.fps import FPSMonitor
5
+
6
+ FPS_UPDATE_RATE = 10 # seconds
7
+ FPS = FPSMonitor(FPS_UPDATE_RATE)
8
+
9
+
10
+ class ImageWebSocketHandler(tornado.websocket.WebSocketHandler):
11
+ clients = set()
12
+ adapter = None
13
+
14
+ @classmethod
15
+ def broadcast(cls):
16
+ if not ImageWebSocketHandler.adapter.image_ready:
17
+ return
18
+
19
+ if not ImageWebSocketHandler.adapter.image:
20
+ Logger.warning(
21
+ "No image received from {}!".format(
22
+ ImageWebSocketHandler.adapter.__class__.__name__
23
+ )
24
+ )
25
+ return
26
+
27
+ io_loop = tornado.ioloop.IOLoop.current()
28
+
29
+ for client in list(cls.clients):
30
+ io_loop.add_callback(
31
+ client.write_message, ImageWebSocketHandler.adapter.image, binary=True
32
+ )
33
+
34
+ FPS.tick()
35
+
36
+ def check_origin(self, _origin):
37
+ # Allow access from every origin
38
+ return True
39
+
40
+ def open(self):
41
+ ImageWebSocketHandler.clients.add(self)
42
+ Logger.info("WebSocket opened from: " + self.request.remote_ip)
43
+
44
+ def on_message(self, _message):
45
+ if not ImageWebSocketHandler.adapter.image:
46
+ Logger.warning(
47
+ "No image received from {}!".format(
48
+ ImageWebSocketHandler.adapter.__class__.__name__
49
+ )
50
+ )
51
+ return
52
+
53
+ image_bytes = ImageWebSocketHandler.adapter.image
54
+ self.write_message(image_bytes, binary=True)
55
+
56
+ def on_close(self):
57
+ ImageWebSocketHandler.clients.remove(self)
58
+
59
+ def register_adapter(adapter):
60
+ ImageWebSocketHandler.adapter = adapter
@@ -0,0 +1,11 @@
1
+ from RGBMatrixEmulator.adapters.browser_adapter.request_handlers import (
2
+ NoCacheRequestHandler,
3
+ )
4
+
5
+
6
+ class MainHandler(NoCacheRequestHandler):
7
+ def get(self):
8
+ self.render("./../static/index.html", adapter=MainHandler.adapter)
9
+
10
+ def register_adapter(adapter):
11
+ MainHandler.adapter = adapter
@@ -0,0 +1,19 @@
1
+ import os
2
+ from RGBMatrixEmulator.adapters.browser_adapter.request_handlers import (
3
+ NoCacheRequestHandler,
4
+ )
5
+
6
+
7
+ class SingleFileHandler(NoCacheRequestHandler):
8
+ def initialize(self, file_path):
9
+ self.file_path = file_path
10
+
11
+ async def get(self):
12
+ if not os.path.exists(self.file_path):
13
+ self.set_status(404)
14
+ return
15
+
16
+ self.set_header("Content-Type", "image/x-icon")
17
+
18
+ with open(self.file_path, "rb") as f:
19
+ self.write(f.read())