RGBMatrixEmulator 0.16.4__tar.gz → 0.17.0__tar.gz

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 (57) hide show
  1. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/PKG-INFO +52 -4
  2. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/README.md +51 -3
  3. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/adapters/base.py +26 -8
  4. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/adapters/browser_adapter/static/index.html +5 -6
  5. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/adapters/pi5_adapter/__init__.py +5 -0
  6. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/adapters/pygame_adapter.py +2 -2
  7. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/adapters/tkinter_adapter.py +4 -4
  8. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/adapters/turtle_adapter.py +1 -1
  9. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/emulation/canvas.py +5 -4
  10. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/emulation/matrix.py +1 -2
  11. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/emulation/options.py +10 -8
  12. rgbmatrixemulator-0.17.0/RGBMatrixEmulator/internal/screen.py +44 -0
  13. rgbmatrixemulator-0.17.0/RGBMatrixEmulator/pixel_mappers/__init__.py +104 -0
  14. rgbmatrixemulator-0.17.0/RGBMatrixEmulator/pixel_mappers/identity.py +15 -0
  15. rgbmatrixemulator-0.17.0/RGBMatrixEmulator/pixel_mappers/mirror.py +25 -0
  16. rgbmatrixemulator-0.17.0/RGBMatrixEmulator/pixel_mappers/rotate.py +35 -0
  17. rgbmatrixemulator-0.17.0/RGBMatrixEmulator/pixel_mappers/stack_to_row.py +25 -0
  18. rgbmatrixemulator-0.17.0/RGBMatrixEmulator/pixel_mappers/umapper.py +27 -0
  19. rgbmatrixemulator-0.17.0/RGBMatrixEmulator/pixel_mappers/vmapper.py +28 -0
  20. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/version.py +1 -1
  21. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/.gitignore +0 -0
  22. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/LICENSE +0 -0
  23. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/__init__.py +0 -0
  24. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/adapters/__init__.py +0 -0
  25. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/adapters/browser_adapter/README.md +0 -0
  26. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/adapters/browser_adapter/__init__.py +0 -0
  27. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/adapters/browser_adapter/adapter.py +0 -0
  28. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/adapters/browser_adapter/fps.py +0 -0
  29. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/adapters/browser_adapter/request_handlers/__init__.py +0 -0
  30. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/adapters/browser_adapter/request_handlers/base.py +0 -0
  31. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/adapters/browser_adapter/request_handlers/image.py +0 -0
  32. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/adapters/browser_adapter/request_handlers/image_web_socket.py +0 -0
  33. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/adapters/browser_adapter/request_handlers/main.py +0 -0
  34. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/adapters/browser_adapter/request_handlers/single_file.py +0 -0
  35. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/adapters/browser_adapter/server.py +0 -0
  36. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/adapters/browser_adapter/static/assets/client.js +0 -0
  37. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/adapters/browser_adapter/static/assets/icon.ico +0 -0
  38. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/adapters/browser_adapter/static/assets/styles.css +0 -0
  39. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/adapters/pi5_adapter/README.md +0 -0
  40. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/adapters/raw_adapter/README.md +0 -0
  41. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/adapters/raw_adapter/__init__.py +0 -0
  42. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/adapters/sixel_adapter.py +0 -0
  43. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/adapters/terminal_adapter.py +0 -0
  44. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/cli/__init__.py +0 -0
  45. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/cli/command.py +0 -0
  46. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/cli/config.py +0 -0
  47. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/emulation/__init__.py +0 -0
  48. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/graphics/__init__.py +0 -0
  49. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/graphics/color.py +0 -0
  50. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/graphics/font.py +0 -0
  51. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/icon.png +0 -0
  52. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/internal/adapter_loader.py +0 -0
  53. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/internal/emulator_config.py +0 -0
  54. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/internal/pixel_style.py +0 -0
  55. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/logger.py +0 -0
  56. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/RGBMatrixEmulator/py.typed +0 -0
  57. {rgbmatrixemulator-0.16.4 → rgbmatrixemulator-0.17.0}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: RGBMatrixEmulator
3
- Version: 0.16.4
3
+ Version: 0.17.0
4
4
  Summary: A PC emulator for Raspberry Pi LED matrices driven by rpi-rgb-led-matrix
5
5
  Project-URL: Homepage, https://github.com/ty-porter/RGBMatrixEmulator
6
6
  Author-email: Tyler Porter <tyler.b.porter@gmail.com>
@@ -99,6 +99,53 @@ After this, most of the existing command line arguments from the `rpi-rgb-led-ma
99
99
 
100
100
  Startup of the existing script will be unchanged.
101
101
 
102
+ ## Multi-Panel Emulation
103
+
104
+ Emulated multi-panel chaining is supported via `rpi-rgb-led-matrix` CLI flags such as `--led-chain`, `--led-parallel`, `--led-pixel-mappers`. RGBME makes the assumption that the desired orientation for a multi-panel arrangement is a rectangular, upright array of panels.
105
+
106
+ ### `--led-chain` / `--led-parallel`
107
+
108
+ Single-panel emulation creates an emulated matrix size of `--led-cols` (width) x `--led-rows` (height). For instance, the following command creates a 64x32 emulated matrix.
109
+
110
+ ```sh
111
+ python main.py --led-cols 64 --led-rows 32
112
+ ```
113
+
114
+ `--led-chain` can be thought of as the number of panels chained horizontally, and `--led-parallel` is the number of parallel chains. This also assumes the chains contain the same number of panels. Thus, these flags create a `--led-cols` x `--led-chain` wide by `--led-rows` x `--led-parallel` high. The following example creates a 128x64 emulated matrix.
115
+
116
+ ```sh
117
+ python main.py --led-cols 64 --led-rows 32 --led-chain 2 --led-parallel 2
118
+ ```
119
+
120
+ ### `--led-pixel-mapper`
121
+
122
+ > [!TIP]
123
+ > Contributions welcome to support more complex mapper behavior!
124
+
125
+ RGBME assumes that the user wants to simulate a physical arrangement of panels such that the image appears continuous and upright. This differs a bit from how `rpi-rgb-led-matrix` treats chains and pixel mappers, which describe the wiring arrangement of the panels and the physical arrangement of panels is not specified.
126
+
127
+ RGBME does **NOT** support pixel mapper chaining:
128
+
129
+ ```sh
130
+ # More than one pixel mapper, V-mapper:Z chained into Rotate:90
131
+ # RGBME will use the first detected mapper.
132
+ python main.py --led-chain 2 --led-parallel 2 --led-pixel-mapper "V-mapper:Z;Rotate:90"
133
+
134
+ # Valid
135
+ python main.py --led-chain 2 --led-parallel 2 --led-pixel-mapper "V-mapper:Z"
136
+ python main.py --led-chain 2 --led-parallel 2 --led-pixel-mapper "Rotate:90"
137
+ ```
138
+
139
+ Instead, RGBME can use a single pixel mapper to alter the emulated dimensions or perform affine transformations over the entire emulated matrix.
140
+
141
+ | Mapper | Parameter | Description |
142
+ | --- | --- | --- |
143
+ | `V-mapper` | `Z` (optional) for zig-zag wiring | Folds a horizontal chain into vertical panel stacks. Changes the dimensions to `(W·parallel/chain) × (H·chain/parallel)`. |
144
+ | `U-mapper` | | Folds a long chain back on itself into a U, halving the width and doubling the height (`W/2 × H·2`). Requires an even chain of at least 2 panels. |
145
+ | `StackToRow` | | Lays parallel chains end-to-end into one wide horizontal row, reshaping the dimensions to `(W·parallel) × (H/parallel)`. |
146
+ | `Rotate` | Angle in degrees, a multiple of 90 (e.g. `90`) | Rotates the displayed image by the given angle. At 90° or 270° the width and height are swapped. |
147
+ | `Mirror` | `H` (default) or `V` | Mirrors the display left/right (`H`) or top/bottom (`V`). Preserves dimensions. |
148
+
102
149
  ## Customization
103
150
 
104
151
  ### Generating a Configuration File
@@ -228,10 +275,10 @@ Please see the [README for the `browser` display adapter](RGBMatrixEmulator/adap
228
275
 
229
276
  ### Raspberry Pi 5 Adapter
230
277
 
231
- > [!NOTE]
232
- > This adapter is currently experimental.
278
+ > [!WARNING]
279
+ > This adapter is deprecated. It will be removed no later than July 1, 2026. Please use native `rpi-rgb-led-matrix` Raspberry Pi 5 support.
233
280
 
234
- RGBME can be used to "bridge" scripts written for `rpi-rgb-led-matrix` to Raspberry Pi model 5 through [Adafruit Blinka](https://github.com/adafruit/Adafruit_Blinka) and the [Adafruit-Blinka-Raspberry-Pi5-Piomatter](https://github.com/adafruit/Adafruit_Blinka_Raspberry_Pi5_Piomatter) libraries. As of Jan 1, 2026 `rpi-rgb-led-matrix` does not support Raspberry Pi 5 natively.
281
+ RGBME can be used to "bridge" scripts written for `rpi-rgb-led-matrix` to Raspberry Pi model 5 through [Adafruit Blinka](https://github.com/adafruit/Adafruit_Blinka) and the [Adafruit-Blinka-Raspberry-Pi5-Piomatter](https://github.com/adafruit/Adafruit_Blinka_Raspberry_Pi5_Piomatter) libraries.
235
282
 
236
283
  Please see the [README for the `pi5` display adapter](RGBMatrixEmulator/adapters/pi5_adapter/README.md) for further information regarding its configuration and usage.
237
284
 
@@ -317,6 +364,7 @@ See [Samples README](samples/README.md) for more information about running examp
317
364
  ```
318
365
  </details>
319
366
  - Drawing large strings is slow, partly because of the `linelimit` parameter in the BDF font parser this emulator uses to prevent multiline text from being rendered unintentionally.
367
+ - `--led-pixel-mapper` emulation differs from `rpi-rgb-led-matrix`. This is due to the assumption made by RGBME for an upright, rectangular emulated matrix. See [Multi-Panel Emulation](#multi-panel-emulation) for details.
320
368
 
321
369
  ## Contributing
322
370
 
@@ -67,6 +67,53 @@ After this, most of the existing command line arguments from the `rpi-rgb-led-ma
67
67
 
68
68
  Startup of the existing script will be unchanged.
69
69
 
70
+ ## Multi-Panel Emulation
71
+
72
+ Emulated multi-panel chaining is supported via `rpi-rgb-led-matrix` CLI flags such as `--led-chain`, `--led-parallel`, `--led-pixel-mappers`. RGBME makes the assumption that the desired orientation for a multi-panel arrangement is a rectangular, upright array of panels.
73
+
74
+ ### `--led-chain` / `--led-parallel`
75
+
76
+ Single-panel emulation creates an emulated matrix size of `--led-cols` (width) x `--led-rows` (height). For instance, the following command creates a 64x32 emulated matrix.
77
+
78
+ ```sh
79
+ python main.py --led-cols 64 --led-rows 32
80
+ ```
81
+
82
+ `--led-chain` can be thought of as the number of panels chained horizontally, and `--led-parallel` is the number of parallel chains. This also assumes the chains contain the same number of panels. Thus, these flags create a `--led-cols` x `--led-chain` wide by `--led-rows` x `--led-parallel` high. The following example creates a 128x64 emulated matrix.
83
+
84
+ ```sh
85
+ python main.py --led-cols 64 --led-rows 32 --led-chain 2 --led-parallel 2
86
+ ```
87
+
88
+ ### `--led-pixel-mapper`
89
+
90
+ > [!TIP]
91
+ > Contributions welcome to support more complex mapper behavior!
92
+
93
+ RGBME assumes that the user wants to simulate a physical arrangement of panels such that the image appears continuous and upright. This differs a bit from how `rpi-rgb-led-matrix` treats chains and pixel mappers, which describe the wiring arrangement of the panels and the physical arrangement of panels is not specified.
94
+
95
+ RGBME does **NOT** support pixel mapper chaining:
96
+
97
+ ```sh
98
+ # More than one pixel mapper, V-mapper:Z chained into Rotate:90
99
+ # RGBME will use the first detected mapper.
100
+ python main.py --led-chain 2 --led-parallel 2 --led-pixel-mapper "V-mapper:Z;Rotate:90"
101
+
102
+ # Valid
103
+ python main.py --led-chain 2 --led-parallel 2 --led-pixel-mapper "V-mapper:Z"
104
+ python main.py --led-chain 2 --led-parallel 2 --led-pixel-mapper "Rotate:90"
105
+ ```
106
+
107
+ Instead, RGBME can use a single pixel mapper to alter the emulated dimensions or perform affine transformations over the entire emulated matrix.
108
+
109
+ | Mapper | Parameter | Description |
110
+ | --- | --- | --- |
111
+ | `V-mapper` | `Z` (optional) for zig-zag wiring | Folds a horizontal chain into vertical panel stacks. Changes the dimensions to `(W·parallel/chain) × (H·chain/parallel)`. |
112
+ | `U-mapper` | | Folds a long chain back on itself into a U, halving the width and doubling the height (`W/2 × H·2`). Requires an even chain of at least 2 panels. |
113
+ | `StackToRow` | | Lays parallel chains end-to-end into one wide horizontal row, reshaping the dimensions to `(W·parallel) × (H/parallel)`. |
114
+ | `Rotate` | Angle in degrees, a multiple of 90 (e.g. `90`) | Rotates the displayed image by the given angle. At 90° or 270° the width and height are swapped. |
115
+ | `Mirror` | `H` (default) or `V` | Mirrors the display left/right (`H`) or top/bottom (`V`). Preserves dimensions. |
116
+
70
117
  ## Customization
71
118
 
72
119
  ### Generating a Configuration File
@@ -196,10 +243,10 @@ Please see the [README for the `browser` display adapter](RGBMatrixEmulator/adap
196
243
 
197
244
  ### Raspberry Pi 5 Adapter
198
245
 
199
- > [!NOTE]
200
- > This adapter is currently experimental.
246
+ > [!WARNING]
247
+ > This adapter is deprecated. It will be removed no later than July 1, 2026. Please use native `rpi-rgb-led-matrix` Raspberry Pi 5 support.
201
248
 
202
- RGBME can be used to "bridge" scripts written for `rpi-rgb-led-matrix` to Raspberry Pi model 5 through [Adafruit Blinka](https://github.com/adafruit/Adafruit_Blinka) and the [Adafruit-Blinka-Raspberry-Pi5-Piomatter](https://github.com/adafruit/Adafruit_Blinka_Raspberry_Pi5_Piomatter) libraries. As of Jan 1, 2026 `rpi-rgb-led-matrix` does not support Raspberry Pi 5 natively.
249
+ RGBME can be used to "bridge" scripts written for `rpi-rgb-led-matrix` to Raspberry Pi model 5 through [Adafruit Blinka](https://github.com/adafruit/Adafruit_Blinka) and the [Adafruit-Blinka-Raspberry-Pi5-Piomatter](https://github.com/adafruit/Adafruit_Blinka_Raspberry_Pi5_Piomatter) libraries.
203
250
 
204
251
  Please see the [README for the `pi5` display adapter](RGBMatrixEmulator/adapters/pi5_adapter/README.md) for further information regarding its configuration and usage.
205
252
 
@@ -285,6 +332,7 @@ See [Samples README](samples/README.md) for more information about running examp
285
332
  ```
286
333
  </details>
287
334
  - Drawing large strings is slow, partly because of the `linelimit` parameter in the BDF font parser this emulator uses to prevent multiline text from being rendered unintentionally.
335
+ - `--led-pixel-mapper` emulation differs from `rpi-rgb-led-matrix`. This is due to the assumption made by RGBME for an upright, rectangular emulated matrix. See [Multi-Panel Emulation](#multi-panel-emulation) for details.
288
336
 
289
337
  ## Contributing
290
338
 
@@ -29,7 +29,7 @@ class BaseAdapter:
29
29
  self.width = width
30
30
  self.height = height
31
31
  self.options = options
32
- self.__black = Image.new("RGB", self.options.window_size(), "black")
32
+ self.__black = Image.new("RGB", self.scaled_screen_size, "black")
33
33
  self.__mask = self.__draw_mask()
34
34
  self.loaded = False
35
35
 
@@ -38,6 +38,24 @@ class BaseAdapter:
38
38
  self.default_icon_path = (Path(__file__).parent / ".." / "icon.png").resolve()
39
39
  self._set_icon_path()
40
40
 
41
+ @property
42
+ def scaled_screen_size(self):
43
+ """On-screen size in pixels: the screen (LED grid) scaled by pixel_size."""
44
+ pixel_size = self.options.pixel_size
45
+
46
+ return (self.width * pixel_size, self.height * pixel_size)
47
+
48
+ def scaled_screen_size_str(self, pixel_text: str = ""):
49
+ width, height = self.scaled_screen_size
50
+
51
+ return "".join([f"{width} x {height}", pixel_text])
52
+
53
+ def screen_size_str(self, pixel_text: str = ""):
54
+ return "".join([f"{self.width} x {self.height}", pixel_text])
55
+
56
+ def panel_size_str(self, pixel_text: str = ""):
57
+ return "".join([f"{self.options.cols} x {self.options.rows}", pixel_text])
58
+
41
59
  @classmethod
42
60
  def get_instance(cls, *args, **kwargs):
43
61
  if cls.INSTANCE is None:
@@ -74,7 +92,7 @@ class BaseAdapter:
74
92
  #############################################################
75
93
  def _get_masked_image(self, pixels):
76
94
  image = Image.fromarray(np.array(pixels, dtype=np.uint8), "RGB")
77
- image = image.resize(self.options.window_size(), Image.NEAREST)
95
+ image = image.resize(self.scaled_screen_size, Image.NEAREST)
78
96
 
79
97
  return Image.composite(image, self.__black, self.__mask)
80
98
 
@@ -87,7 +105,7 @@ class BaseAdapter:
87
105
  return getattr(self, self.MASK_FNS.get(pixel_style, self.DEFAULT_MASK_FN))
88
106
 
89
107
  def __draw_mask(self):
90
- mask = Image.new("L", self.options.window_size())
108
+ mask = Image.new("L", self.scaled_screen_size)
91
109
 
92
110
  draw_fn = self.__mask_fn(self.options.pixel_style)
93
111
  draw_fn(mask)
@@ -96,7 +114,7 @@ class BaseAdapter:
96
114
 
97
115
  def _draw_circle_mask(self, mask):
98
116
  pixel_size = self.options.pixel_size
99
- width, height = self.options.window_size()
117
+ width, height = self.scaled_screen_size
100
118
 
101
119
  drawer = ImageDraw.Draw(mask)
102
120
 
@@ -110,7 +128,7 @@ class BaseAdapter:
110
128
 
111
129
  def _draw_square_mask(self, mask):
112
130
  pixel_size = self.options.pixel_size
113
- width, height = self.options.window_size()
131
+ width, height = self.scaled_screen_size
114
132
 
115
133
  drawer = ImageDraw.Draw(mask)
116
134
 
@@ -124,7 +142,7 @@ class BaseAdapter:
124
142
 
125
143
  def _draw_real_mask(self, mask):
126
144
  pixel_size = self.options.pixel_size
127
- width, height = self.options.window_size()
145
+ width, height = self.scaled_screen_size
128
146
  pixel_glow = self.options.pixel_glow
129
147
 
130
148
  if pixel_glow == 0:
@@ -219,8 +237,8 @@ class BaseAdapter:
219
237
  def __str__(self):
220
238
  return self.DEBUG_TEXT_TEMPLATE.format(
221
239
  version.__version__,
222
- self.options.cols,
223
- self.options.rows,
240
+ self.width,
241
+ self.height,
224
242
  self.options.chain_length,
225
243
  self.options.parallel,
226
244
  self.options.pixel_size,
@@ -26,27 +26,26 @@
26
26
  <tbody>
27
27
  <tr>
28
28
  <td>
29
- Matrix Width:
29
+ Panel Size:
30
30
  </td>
31
31
  <td>
32
- {{ adapter.options.cols }}
32
+ {{ adapter.panel_size_str(pixel_text="px") }}
33
33
  </td>
34
34
  </tr>
35
35
  <tr>
36
36
  <td>
37
- Matrix Height:
37
+ Overall Size:
38
38
  </td>
39
39
  <td>
40
- {{ adapter.options.rows }}
40
+ {{ adapter.screen_size_str(pixel_text="px") }}
41
41
  </td>
42
- </td>
43
42
  </tr>
44
43
  <tr>
45
44
  <td>
46
45
  Image Size:
47
46
  </td>
48
47
  <td>
49
- {{ adapter.options.window_size_str(pixel_text="px") }}
48
+ {{ adapter.scaled_screen_size_str(pixel_text="px") }}
50
49
  </td>
51
50
  </tr>
52
51
  <tr>
@@ -13,6 +13,11 @@ except ImportError:
13
13
 
14
14
  class Pi5Adapter(BaseAdapter):
15
15
  def __init__(self, width, height, options):
16
+ Logger.warning(
17
+ "The Pi5 adapter is deprecated. rpi-rgb-led-matrix now provides Raspberry Pi 5 support natively. "
18
+ "This adapter will be removed no later than July 1, 2026.",
19
+ )
20
+
16
21
  super().__init__(width, height, options)
17
22
  self.matrix = None
18
23
  self.framebuffer = None
@@ -31,7 +31,7 @@ class PygameAdapter(BaseAdapter):
31
31
  return
32
32
 
33
33
  Logger.info(self.emulator_title)
34
- self.__surface = pygame.display.set_mode(self.options.window_size())
34
+ self.__surface = pygame.display.set_mode(self.scaled_screen_size)
35
35
  pygame.init()
36
36
 
37
37
  self.__set_emulator_icon()
@@ -42,7 +42,7 @@ class PygameAdapter(BaseAdapter):
42
42
  def draw_to_screen(self, pixels):
43
43
  image = self._get_masked_image(pixels)
44
44
  pygame_surface = pygame.image.fromstring(
45
- image.tobytes(), self.options.window_size(), "RGB"
45
+ image.tobytes(), self.scaled_screen_size, "RGB"
46
46
  )
47
47
  self.__surface.blit(pygame_surface, (0, 0))
48
48
 
@@ -24,12 +24,12 @@ class TkinterAdapter(BaseAdapter):
24
24
  self.__set_emulator_icon()
25
25
  self.__root.title(self.emulator_title)
26
26
 
27
- window_size = self.options.window_size()
28
- self.__root.geometry("{}x{}".format(*window_size))
27
+ scaled_screen_size = self.scaled_screen_size
28
+ self.__root.geometry("{}x{}".format(*scaled_screen_size))
29
29
  self.__canvas = tkinter.Canvas(
30
30
  self.__root,
31
- width=window_size[0],
32
- height=window_size[1],
31
+ width=scaled_screen_size[0],
32
+ height=scaled_screen_size[1],
33
33
  bd=0,
34
34
  highlightthickness=0,
35
35
  bg="black",
@@ -29,7 +29,7 @@ class TurtleAdapter(BaseAdapter):
29
29
  return
30
30
 
31
31
  Logger.info(self.emulator_title)
32
- turtle.setup(*self.options.window_size())
32
+ turtle.setup(*self.scaled_screen_size)
33
33
  turtle.title(self.emulator_title)
34
34
  self.__pen = turtle.Turtle(visible=False)
35
35
  self.__screen = self.__pen.getscreen()
@@ -8,15 +8,16 @@ from RGBMatrixEmulator.emulation.options import RGBMatrixOptions
8
8
  class Canvas:
9
9
  def __init__(self, options: RGBMatrixOptions) -> None:
10
10
  self.options = options
11
+ self.__screen = options.screen
11
12
 
12
- self.width = options.cols * options.chain_length
13
- self.height = options.rows * options.parallel
13
+ self.width, self.height = self.__screen.pixel_buffer_size
14
14
 
15
15
  # 3D numpy array -- rows (H), columns (W), 3-tuple RGB
16
16
  self.__pdims = (self.height, self.width, 3)
17
17
 
18
+ screen_w, screen_h = self.__screen.screen_size
18
19
  self.display_adapter = options.display_adapter.get_instance(
19
- self.width, self.height, options
20
+ screen_w, screen_h, options
20
21
  )
21
22
 
22
23
  self.Clear()
@@ -88,7 +89,7 @@ class Canvas:
88
89
 
89
90
  # These are delegated to the display adapter to handle specific implementation.
90
91
  def draw_to_screen(self) -> None:
91
- self.display_adapter.draw_to_screen(self.__pixels)
92
+ self.display_adapter.draw_to_screen(self.__screen.render(self.__pixels))
92
93
 
93
94
  def check_for_quit_event(self) -> None:
94
95
  self.display_adapter.check_for_quit_event()
@@ -8,8 +8,7 @@ class RGBMatrix:
8
8
  def __init__(self, options: RGBMatrixOptions = RGBMatrixOptions()) -> None:
9
9
  self.options = options
10
10
 
11
- self.width = options.cols * options.chain_length
12
- self.height = options.rows * options.parallel
11
+ self.width, self.height = options.screen.pixel_buffer_size
13
12
 
14
13
  def CreateFrameCanvas(self) -> Canvas:
15
14
  self.canvas = Canvas(options=self.options)
@@ -1,4 +1,5 @@
1
1
  from RGBMatrixEmulator.internal.emulator_config import RGBMatrixEmulatorConfig
2
+ from RGBMatrixEmulator.internal.screen import Screen
2
3
 
3
4
 
4
5
  class RGBMatrixOptions:
@@ -14,6 +15,7 @@ class RGBMatrixOptions:
14
15
  self.brightness = 100
15
16
  self.pwm_lsb_nanoseconds = 130
16
17
  self.led_rgb_sequence = "RGB-EMULATED"
18
+ self.pixel_mapper_config = ""
17
19
  self.show_refresh_rate = 0
18
20
  self.gpio_slowdown = None
19
21
  self.disable_hardware_pulsing = False
@@ -35,13 +37,13 @@ class RGBMatrixOptions:
35
37
  # Pi5 Adapter
36
38
  self.pi5 = emulator_config.pi5
37
39
 
38
- def window_size(self) -> tuple[int, int]:
39
- return (
40
- self.cols * self.pixel_size * self.chain_length,
41
- self.rows * self.pixel_size * self.parallel,
42
- )
40
+ @property
41
+ def screen(self) -> Screen:
42
+ """The emulated screen model (mapper geometry + render).
43
43
 
44
- def window_size_str(self, pixel_text: str = "") -> str:
45
- width, height = self.window_size()
44
+ Built lazily and cached, so it snapshots the configured options rather
45
+ than the defaults present at construction time."""
46
+ if not hasattr(self, "_screen"):
47
+ self._screen = Screen(self)
46
48
 
47
- return f"{width} x {height} {pixel_text}"
49
+ return self._screen
@@ -0,0 +1,44 @@
1
+ import numpy as np
2
+
3
+ from RGBMatrixEmulator.pixel_mappers import get_pixel_mapper
4
+
5
+
6
+ class Screen:
7
+ """
8
+ The emulated screen: how a program's drawn canvas is mapped into the
9
+ displayed window.
10
+
11
+ It resolves the configured pixel mapper once and snapshots the resulting
12
+ geometry. The pipeline is:
13
+
14
+ pixel_buffer_size --(LUT)--> screen_size --(x pixel_size)--> scaled
15
+
16
+ Both sizes here are in LED units; scaling to on-screen pixels by pixel_size
17
+ is a rendering concern owned by the display adapter.
18
+
19
+ pixel_buffer_size -- what the program draws into (SetPixel/SetImage),
20
+ and the size reported as RGBMatrix.width/height
21
+ screen_size -- what the screen shows after the mapper's LUT; a
22
+ content transform may change the dimensions (e.g. a 90
23
+ degree rotation), so this can differ from the buffer
24
+ """
25
+
26
+ def __init__(self, options) -> None:
27
+ base_w = options.cols * options.chain_length
28
+ base_h = options.rows * options.parallel
29
+
30
+ mapper = get_pixel_mapper(
31
+ options.pixel_mapper_config, options.chain_length, options.parallel
32
+ )
33
+
34
+ self.pixel_buffer_size = mapper.get_size_mapping(base_w, base_h)
35
+ self._lut, self.screen_size = mapper.build_lut(*self.pixel_buffer_size)
36
+
37
+ def render(self, pixels: np.ndarray) -> np.ndarray:
38
+ """Map a pixel buffer to screen pixels via the mapper's LUT."""
39
+ if self._lut is None:
40
+ return pixels
41
+
42
+ vy_lut, vx_lut = self._lut
43
+
44
+ return pixels[vy_lut, vx_lut]
@@ -0,0 +1,104 @@
1
+ import abc
2
+
3
+ import numpy as np
4
+
5
+ """A gather lookup table: a pair of integer index arrays (vy_lut, vx_lut), each shaped
6
+ like the screen, holding the visible coordinate that feeds each screen pixel.
7
+ Apply with `screen_pixels = visible_pixels[vy_lut, vx_lut]`."""
8
+ LUT = tuple[np.ndarray, np.ndarray]
9
+
10
+
11
+ class PixelMapper(abc.ABC):
12
+ """
13
+ A pixel mapper, modeled on rpi-rgb-led-matrix's PixelMapper.
14
+
15
+ The emulator assumes panels are physically arranged as an upright,
16
+ rectangular matrix.
17
+
18
+ A pixel mapper may:
19
+ - resize the canvas relative to the base panel grid (V, U, StackToRow)
20
+ - transform the content the viewer sees within that size (Mirror, Rotate)
21
+
22
+ The emulator never drives real LEDs. Mappers describe where pixels
23
+ end up on screen and not physical panel arrangement.
24
+ """
25
+
26
+ @abc.abstractmethod
27
+ def get_size_mapping(self, base_w: int, base_h: int) -> tuple[int, int]:
28
+ """Return the screen (W, H) produced from the base panel grid size."""
29
+
30
+ @abc.abstractmethod
31
+ def map_visible_to_screen(
32
+ self, draw_w: int, draw_h: int, vx: np.ndarray, vy: np.ndarray
33
+ ) -> tuple[np.ndarray, np.ndarray]:
34
+ """
35
+ Map drawn coordinates to displayed-screen coordinates, elementwise.
36
+
37
+ `vx` / `vy` are integer arrays of matching shape holding coordinates
38
+ in the drawn pixel buffer (the `get_size_mapping` size the user draws
39
+ into). The return is a pair of same-shape arrays holding screen
40
+ coordinates. `draw_w` / `draw_h` are the drawn-canvas dimensions.
41
+
42
+ Arrangement mappers that only resize return their inputs unchanged. A
43
+ content transform may change the dimensions (e.g. a 90 degree rotation).
44
+ """
45
+
46
+ def build_lut(self, draw_w: int, draw_h: int) -> tuple[LUT | None, tuple[int, int]]:
47
+ """
48
+ Compile this mapper's coordinate transform over a pixel buffer of
49
+ `draw_w` x `draw_h` into a LUT plus the resulting displayed (W, H).
50
+ """
51
+ vy_grid, vx_grid = np.indices((draw_h, draw_w))
52
+ sx, sy = self.map_visible_to_screen(draw_w, draw_h, vx_grid, vy_grid)
53
+
54
+ # Identity content transform -> no remapping needed
55
+ if np.array_equal(sx, vx_grid) and np.array_equal(sy, vy_grid):
56
+ return None, (draw_w, draw_h)
57
+
58
+ display_w = int(sx.max()) + 1
59
+ display_h = int(sy.max()) + 1
60
+
61
+ vx_lut = np.empty((display_h, display_w), dtype=np.intp)
62
+ vy_lut = np.empty((display_h, display_w), dtype=np.intp)
63
+ vx_lut[sy, sx] = vx_grid
64
+ vy_lut[sy, sx] = vy_grid
65
+
66
+ return (vy_lut, vx_lut), (display_w, display_h)
67
+
68
+
69
+ def get_pixel_mapper(
70
+ config: str, chain_length: int = 1, parallel: int = 1
71
+ ) -> PixelMapper:
72
+ """
73
+ Build a single pixel mapper from a config string ("Name" or "Name:param").
74
+
75
+ Composition (semicolon-separated chains) is not handled yet; only the first
76
+ mapper in the string is used.
77
+ """
78
+ from RGBMatrixEmulator.pixel_mappers.identity import IdentityMapper
79
+ from RGBMatrixEmulator.pixel_mappers.mirror import MirrorMapper
80
+ from RGBMatrixEmulator.pixel_mappers.rotate import RotateMapper
81
+ from RGBMatrixEmulator.pixel_mappers.stack_to_row import StackToRowMapper
82
+ from RGBMatrixEmulator.pixel_mappers.umapper import UMapper
83
+ from RGBMatrixEmulator.pixel_mappers.vmapper import VMapper
84
+
85
+ if not config:
86
+ return IdentityMapper()
87
+
88
+ spec = config.split(";")[0].strip()
89
+ name, _, param = spec.partition(":")
90
+ key = name.lower().replace("-", "")
91
+ param = param.strip()
92
+
93
+ if key == "mirror":
94
+ return MirrorMapper(horizontal=(param.upper() != "V"))
95
+ if key == "rotate":
96
+ return RotateMapper(angle=int(param) if param else 0)
97
+ if key == "vmapper":
98
+ return VMapper(chain_length, parallel, z=(param.upper() == "Z"))
99
+ if key == "umapper":
100
+ return UMapper(chain_length, parallel)
101
+ if key == "stacktorow":
102
+ return StackToRowMapper(chain_length, parallel)
103
+
104
+ return IdentityMapper()
@@ -0,0 +1,15 @@
1
+ import numpy as np
2
+
3
+ from RGBMatrixEmulator.pixel_mappers import PixelMapper
4
+
5
+
6
+ class IdentityMapper(PixelMapper):
7
+ """No-op mapper. The drawn canvas and the displayed screen are identical."""
8
+
9
+ def get_size_mapping(self, base_w: int, base_h: int) -> tuple[int, int]:
10
+ return (base_w, base_h)
11
+
12
+ def map_visible_to_screen(
13
+ self, screen_w: int, screen_h: int, vx: np.ndarray, vy: np.ndarray
14
+ ) -> tuple[np.ndarray, np.ndarray]:
15
+ return (vx, vy)
@@ -0,0 +1,25 @@
1
+ import numpy as np
2
+
3
+ from RGBMatrixEmulator.pixel_mappers import PixelMapper
4
+
5
+
6
+ class MirrorMapper(PixelMapper):
7
+ """
8
+ Mirror the display horizontally (default) or vertically.
9
+
10
+ Vectorized port of rpi-rgb-led-matrix's MirrorPixelMapper ("Mirror").
11
+ Parameter "H" mirrors left/right, "V" mirrors top/bottom. Preserves dimensions.
12
+ """
13
+
14
+ def __init__(self, horizontal: bool = True):
15
+ self.horizontal = horizontal
16
+
17
+ def get_size_mapping(self, base_w: int, base_h: int) -> tuple[int, int]:
18
+ return (base_w, base_h)
19
+
20
+ def map_visible_to_screen(
21
+ self, screen_w: int, screen_h: int, vx: np.ndarray, vy: np.ndarray
22
+ ) -> tuple[np.ndarray, np.ndarray]:
23
+ if self.horizontal:
24
+ return (screen_w - 1 - vx, vy)
25
+ return (vx, screen_h - 1 - vy)
@@ -0,0 +1,35 @@
1
+ import numpy as np
2
+
3
+ from RGBMatrixEmulator.pixel_mappers import PixelMapper
4
+
5
+
6
+ class RotateMapper(PixelMapper):
7
+ """
8
+ Rotate the display by a multiple of 90 degrees.
9
+
10
+ Based on rpi-rgb-led-matrix's RotatePixelMapper ("Rotate"). With panels in a
11
+ normal upright arrangement the viewer sees the rotated image. At 90/270 degrees
12
+ the rotation also swaps the dimensions, so the drawn canvas and the displayed
13
+ screen have transposed dimensions.
14
+ """
15
+
16
+ def __init__(self, angle: int = 0):
17
+ if angle % 90 != 0:
18
+ raise ValueError("Rotate angle must be a multiple of 90 degrees")
19
+ self.angle = (angle + 360) % 360
20
+
21
+ def get_size_mapping(self, base_w: int, base_h: int) -> tuple[int, int]:
22
+ if self.angle % 180 == 0:
23
+ return (base_w, base_h)
24
+ return (base_h, base_w)
25
+
26
+ def map_visible_to_screen(
27
+ self, draw_w: int, draw_h: int, vx: np.ndarray, vy: np.ndarray
28
+ ) -> tuple[np.ndarray, np.ndarray]:
29
+ if self.angle == 90:
30
+ return (draw_h - 1 - vy, vx)
31
+ if self.angle == 180:
32
+ return (draw_w - 1 - vx, draw_h - 1 - vy)
33
+ if self.angle == 270:
34
+ return (vy, draw_w - 1 - vx)
35
+ return (vx, vy)
@@ -0,0 +1,25 @@
1
+ import numpy as np
2
+
3
+ from RGBMatrixEmulator.pixel_mappers import PixelMapper
4
+
5
+
6
+ class StackToRowMapper(PixelMapper):
7
+ """
8
+ Lay parallel chains end-to-end as one wide horizontal row.
9
+
10
+ Based on rpi-rgb-led-matrix's StackToRowMapper ("StackToRow"). This changes
11
+ the dimensions of the drawn image but the emulator does not model physical
12
+ layout, so it is otherwise unchanged.
13
+ """
14
+
15
+ def __init__(self, chain_length: int = 1, parallel: int = 1):
16
+ self.chain_length = chain_length
17
+ self.bands = max(parallel, 1)
18
+
19
+ def get_size_mapping(self, base_w: int, base_h: int) -> tuple[int, int]:
20
+ return (base_w * self.bands, base_h // self.bands)
21
+
22
+ def map_visible_to_screen(
23
+ self, screen_w: int, screen_h: int, vx: np.ndarray, vy: np.ndarray
24
+ ) -> tuple[np.ndarray, np.ndarray]:
25
+ return (vx, vy)
@@ -0,0 +1,27 @@
1
+ import numpy as np
2
+
3
+ from RGBMatrixEmulator.pixel_mappers import PixelMapper
4
+
5
+
6
+ class UMapper(PixelMapper):
7
+ """
8
+ U-arrangement mapper: a long chain folded back on itself into a U.
9
+
10
+ Based on rpi-rgb-led-matrix's UArrangementMapper ("U-mapper"). This changes
11
+ the dimensions of the drawn image but the emulator does not model physical
12
+ layout, so it is otherwise unchanged.
13
+
14
+ Requires an even chain of at least 2 panels.
15
+ """
16
+
17
+ def __init__(self, chain_length: int = 1, parallel: int = 1):
18
+ self.chain_length = chain_length
19
+ self.parallel = parallel
20
+
21
+ def get_size_mapping(self, base_w: int, base_h: int) -> tuple[int, int]:
22
+ return (base_w // 2, base_h * 2)
23
+
24
+ def map_visible_to_screen(
25
+ self, screen_w: int, screen_h: int, vx: np.ndarray, vy: np.ndarray
26
+ ) -> tuple[np.ndarray, np.ndarray]:
27
+ return (vx, vy)
@@ -0,0 +1,28 @@
1
+ import numpy as np
2
+
3
+ from RGBMatrixEmulator.pixel_mappers import PixelMapper
4
+
5
+
6
+ class VMapper(PixelMapper):
7
+ """
8
+ Vertical mapper: a horizontal chain folded into vertical panel stacks.
9
+
10
+ Based on rpi-rgb-led-matrix's VerticalMapper ("V-mapper"). This changes
11
+ the dimensions of the drawn image but the emulator does not model physical
12
+ layout, so it is otherwise unchanged.
13
+ """
14
+
15
+ def __init__(self, chain_length: int = 1, parallel: int = 1, z: bool = False):
16
+ self.chain_length = chain_length
17
+ self.parallel = parallel
18
+ self.z = z
19
+
20
+ def get_size_mapping(self, base_w: int, base_h: int) -> tuple[int, int]:
21
+ screen_w = base_w * self.parallel // self.chain_length
22
+ screen_h = base_h * self.chain_length // self.parallel
23
+ return (screen_w, screen_h)
24
+
25
+ def map_visible_to_screen(
26
+ self, screen_w: int, screen_h: int, vx: np.ndarray, vy: np.ndarray
27
+ ) -> tuple[np.ndarray, np.ndarray]:
28
+ return (vx, vy)
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env python
2
2
 
3
3
  # package version
4
- __version__ = "0.16.4"
4
+ __version__ = "0.17.0"
5
5
  """Installed version of RGBMatrixEmulator."""