RGBMatrixEmulator 0.11.4__py2.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.
- RGBMatrixEmulator/__init__.py +5 -0
- RGBMatrixEmulator/adapters/__init__.py +68 -0
- RGBMatrixEmulator/adapters/base.py +111 -0
- RGBMatrixEmulator/adapters/browser_adapter/README.md +89 -0
- RGBMatrixEmulator/adapters/browser_adapter/__init__.py +0 -0
- RGBMatrixEmulator/adapters/browser_adapter/adapter.py +53 -0
- RGBMatrixEmulator/adapters/browser_adapter/request_handlers/__init__.py +3 -0
- RGBMatrixEmulator/adapters/browser_adapter/request_handlers/image.py +10 -0
- RGBMatrixEmulator/adapters/browser_adapter/request_handlers/image_web_socket.py +30 -0
- RGBMatrixEmulator/adapters/browser_adapter/request_handlers/main.py +9 -0
- RGBMatrixEmulator/adapters/browser_adapter/server.py +73 -0
- RGBMatrixEmulator/adapters/browser_adapter/static/assets/client.js +91 -0
- RGBMatrixEmulator/adapters/browser_adapter/static/assets/icon.ico +0 -0
- RGBMatrixEmulator/adapters/browser_adapter/static/assets/styles.css +25 -0
- RGBMatrixEmulator/adapters/browser_adapter/static/index.html +144 -0
- RGBMatrixEmulator/adapters/pygame_adapter.py +71 -0
- RGBMatrixEmulator/adapters/sixel_adapter.py +72 -0
- RGBMatrixEmulator/adapters/terminal_adapter.py +36 -0
- RGBMatrixEmulator/adapters/tkinter_adapter.py +83 -0
- RGBMatrixEmulator/adapters/turtle_adapter.py +95 -0
- RGBMatrixEmulator/emulators/__init__.py +0 -0
- RGBMatrixEmulator/emulators/canvas.py +39 -0
- RGBMatrixEmulator/emulators/matrix.py +50 -0
- RGBMatrixEmulator/emulators/options.py +188 -0
- RGBMatrixEmulator/graphics/__init__.py +155 -0
- RGBMatrixEmulator/graphics/color.py +36 -0
- RGBMatrixEmulator/graphics/font.py +34 -0
- RGBMatrixEmulator/icon.ico +0 -0
- RGBMatrixEmulator/icon.png +0 -0
- RGBMatrixEmulator/logger.py +29 -0
- RGBMatrixEmulator/version.py +5 -0
- rgbmatrixemulator-0.11.4.data/data/RGBMatrixEmulator/client.js +91 -0
- rgbmatrixemulator-0.11.4.data/data/RGBMatrixEmulator/icon.ico +0 -0
- rgbmatrixemulator-0.11.4.data/data/RGBMatrixEmulator/icon.png +0 -0
- rgbmatrixemulator-0.11.4.data/data/RGBMatrixEmulator/index.html +144 -0
- rgbmatrixemulator-0.11.4.data/data/RGBMatrixEmulator/styles.css +25 -0
- rgbmatrixemulator-0.11.4.data/data/docs/LICENSE +9 -0
- rgbmatrixemulator-0.11.4.data/data/docs/README.md +160 -0
- rgbmatrixemulator-0.11.4.dist-info/METADATA +186 -0
- rgbmatrixemulator-0.11.4.dist-info/RECORD +43 -0
- rgbmatrixemulator-0.11.4.dist-info/WHEEL +5 -0
- rgbmatrixemulator-0.11.4.dist-info/licenses/LICENSE +9 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import json, os, pprint, sys
|
|
2
|
+
|
|
3
|
+
from RGBMatrixEmulator.adapters import ADAPTER_TYPES
|
|
4
|
+
from RGBMatrixEmulator.logger import Logger
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class RGBMatrixOptions:
|
|
8
|
+
def __init__(self):
|
|
9
|
+
self.hardware_mapping = 'EMULATED'
|
|
10
|
+
self.rows = 32
|
|
11
|
+
self.cols = 32
|
|
12
|
+
self.chain_length = 1
|
|
13
|
+
self.parallel = 1
|
|
14
|
+
self.row_address_type = 0
|
|
15
|
+
self.multiplexing = 0
|
|
16
|
+
self.pwm_bits = 0
|
|
17
|
+
self.brightness = 100
|
|
18
|
+
self.pwm_lsb_nanoseconds = 130
|
|
19
|
+
self.led_rgb_sequence = 'RGB-EMULATED'
|
|
20
|
+
self.show_refresh_rate = 0
|
|
21
|
+
self.gpio_slowdown = None
|
|
22
|
+
self.disable_hardware_pulsing = False
|
|
23
|
+
|
|
24
|
+
emulator_config = RGBMatrixEmulatorConfig()
|
|
25
|
+
|
|
26
|
+
if emulator_config.display_adapter.lower() in ADAPTER_TYPES:
|
|
27
|
+
self.display_adapter = ADAPTER_TYPES[emulator_config.display_adapter.lower()]
|
|
28
|
+
elif len(ADAPTER_TYPES.keys()) > 0:
|
|
29
|
+
adapter_types = ', '.join('"{}"'.format(key) for key in ADAPTER_TYPES.keys())
|
|
30
|
+
|
|
31
|
+
# Try to set it to the emulator default, but if it failed to load, pick the first one that did.
|
|
32
|
+
if emulator_config.DEFAULT_CONFIG.get('display_adapter') in ADAPTER_TYPES:
|
|
33
|
+
default_adapter = emulator_config.DEFAULT_CONFIG.get('display_adapter')
|
|
34
|
+
else:
|
|
35
|
+
default_adapter = list(ADAPTER_TYPES.keys())[0]
|
|
36
|
+
|
|
37
|
+
Logger.warning('"{}" display adapter option not recognized. Valid adapters are {}. Defaulting to "{}"...'.format(
|
|
38
|
+
emulator_config.display_adapter,
|
|
39
|
+
adapter_types,
|
|
40
|
+
default_adapter
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
self.display_adapter = ADAPTER_TYPES[default_adapter]
|
|
44
|
+
else:
|
|
45
|
+
Logger.critical("Failed to find a valid display adapter to load! Check that you have installed dependencies required for your configured adapter.")
|
|
46
|
+
|
|
47
|
+
sys.exit(1)
|
|
48
|
+
|
|
49
|
+
self.pixel_style = emulator_config.DEFAULT_CONFIG.get('pixel_style')
|
|
50
|
+
config_pixel_style = emulator_config.pixel_style.lower()
|
|
51
|
+
|
|
52
|
+
if config_pixel_style in emulator_config.VALID_PIXEL_STYLES:
|
|
53
|
+
if config_pixel_style != self.pixel_style:
|
|
54
|
+
if self.display_adapter.SUPPORTS_ALTERNATE_PIXEL_STYLE:
|
|
55
|
+
self.pixel_style = emulator_config.pixel_style
|
|
56
|
+
else:
|
|
57
|
+
Logger.warning('"{}" pixel style option is not supported by adapter "{}". Defaulting to "square"...'.format(config_pixel_style, emulator_config.display_adapter.lower()))
|
|
58
|
+
else:
|
|
59
|
+
Logger.warning('"{}" pixel style option not recognized. Valid options are "square", "circle". Defaulting to "square"...'.format(config_pixel_style))
|
|
60
|
+
|
|
61
|
+
self.pixel_size = emulator_config.pixel_size
|
|
62
|
+
self.pixel_outline = emulator_config.DEFAULT_CONFIG['pixel_outline']
|
|
63
|
+
self.pixel_outline = emulator_config.pixel_outline
|
|
64
|
+
self.browser = emulator_config.browser
|
|
65
|
+
|
|
66
|
+
if emulator_config.suppress_font_warnings:
|
|
67
|
+
import bdfparser
|
|
68
|
+
|
|
69
|
+
bdfparser.warnings.simplefilter("ignore")
|
|
70
|
+
|
|
71
|
+
def window_size(self):
|
|
72
|
+
return (self.cols * self.pixel_size * self.chain_length, self.rows * self.pixel_size * self.parallel)
|
|
73
|
+
|
|
74
|
+
def window_size_str(self, pixel_text=""):
|
|
75
|
+
width, height = self.window_size()
|
|
76
|
+
|
|
77
|
+
return f"{width} x {height} {pixel_text}"
|
|
78
|
+
|
|
79
|
+
class RGBMatrixEmulatorConfig:
|
|
80
|
+
|
|
81
|
+
__CONFIG_PATH = 'emulator_config.json'
|
|
82
|
+
|
|
83
|
+
VALID_PIXEL_STYLES = ['square', 'circle']
|
|
84
|
+
DEFAULT_CONFIG = {
|
|
85
|
+
'pixel_outline': 0,
|
|
86
|
+
'pixel_size': 16,
|
|
87
|
+
'pixel_style': 'square',
|
|
88
|
+
'display_adapter': 'browser',
|
|
89
|
+
'suppress_font_warnings': False,
|
|
90
|
+
'suppress_adapter_load_errors': False,
|
|
91
|
+
'browser': {
|
|
92
|
+
'_comment': 'For use with the browser adapter only.',
|
|
93
|
+
'port': 8888,
|
|
94
|
+
'target_fps': 24,
|
|
95
|
+
'fps_display': False,
|
|
96
|
+
'quality': 70,
|
|
97
|
+
'image_border': True,
|
|
98
|
+
'debug_text': False,
|
|
99
|
+
'image_format': 'JPEG'
|
|
100
|
+
},
|
|
101
|
+
"log_level": "info"
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
def __init__(self):
|
|
105
|
+
self.config = self.__load_config()
|
|
106
|
+
self.default_config = self.DEFAULT_CONFIG
|
|
107
|
+
|
|
108
|
+
RGBMatrixEmulatorConfig.Utils.set_attributes(self)
|
|
109
|
+
|
|
110
|
+
def __load_config(self):
|
|
111
|
+
if os.path.exists(self.__CONFIG_PATH):
|
|
112
|
+
with open(self.__CONFIG_PATH) as f:
|
|
113
|
+
config = json.load(f)
|
|
114
|
+
|
|
115
|
+
return config
|
|
116
|
+
|
|
117
|
+
with open(self.__CONFIG_PATH, 'w') as f:
|
|
118
|
+
json.dump(self.DEFAULT_CONFIG, f, indent=4)
|
|
119
|
+
|
|
120
|
+
return self.DEFAULT_CONFIG
|
|
121
|
+
|
|
122
|
+
def __str__(self):
|
|
123
|
+
return RGBMatrixEmulatorConfig.Utils.to_str(self)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class ChildConfig:
|
|
127
|
+
def __init__(self, config, default_config):
|
|
128
|
+
self.config = config
|
|
129
|
+
self.default_config = default_config
|
|
130
|
+
|
|
131
|
+
RGBMatrixEmulatorConfig.Utils.set_attributes(self)
|
|
132
|
+
|
|
133
|
+
def __str__(self):
|
|
134
|
+
return RGBMatrixEmulatorConfig.Utils.to_str(self)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class Utils:
|
|
138
|
+
def to_str(obj):
|
|
139
|
+
'''
|
|
140
|
+
Pretty prints the config object from dict.
|
|
141
|
+
'''
|
|
142
|
+
printer = pprint.PrettyPrinter(sort_dicts=False)
|
|
143
|
+
return "\n".join([obj.__repr__(), printer.pformat(RGBMatrixEmulatorConfig.Utils.to_dict(obj))])
|
|
144
|
+
|
|
145
|
+
def to_dict(obj):
|
|
146
|
+
'''
|
|
147
|
+
Recursively recreates the config dict from child config objects.
|
|
148
|
+
'''
|
|
149
|
+
config = {}
|
|
150
|
+
for key in obj.__dict__.keys():
|
|
151
|
+
if key in ['config', 'default_config'] or key[0] == '_':
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
value = obj.__dict__.get(key)
|
|
155
|
+
|
|
156
|
+
if isinstance(value, RGBMatrixEmulatorConfig.ChildConfig):
|
|
157
|
+
value = RGBMatrixEmulatorConfig.Utils.to_dict(value)
|
|
158
|
+
|
|
159
|
+
config[key] = value
|
|
160
|
+
|
|
161
|
+
return config
|
|
162
|
+
|
|
163
|
+
def set_attributes(obj):
|
|
164
|
+
'''
|
|
165
|
+
Dynamically set attributes loaded into config and default config variables.
|
|
166
|
+
|
|
167
|
+
Numbers, strings, and arrays are stored natively. Nested dicts are parsed into RGBMatrixEmulatorChildConfig objects recursively.
|
|
168
|
+
'''
|
|
169
|
+
for key in obj.default_config.keys():
|
|
170
|
+
if key in obj.config:
|
|
171
|
+
value = obj.config.get(key)
|
|
172
|
+
default = obj.default_config.get(key)
|
|
173
|
+
else:
|
|
174
|
+
value = obj.default_config.get(key)
|
|
175
|
+
default = value
|
|
176
|
+
|
|
177
|
+
Logger.warning("Emulator config is missing key '{}', falling back to default '{}'. Consider adding this to your emulator config file.".format(key, value))
|
|
178
|
+
|
|
179
|
+
RGBMatrixEmulatorConfig.Utils.set_attribute(obj, key, value, default)
|
|
180
|
+
|
|
181
|
+
def set_attribute(obj, key, value, default):
|
|
182
|
+
'''
|
|
183
|
+
Store the value as an attribute or delegate to the RGBMatrixEmulatorChildConfig to parse into a new node.
|
|
184
|
+
'''
|
|
185
|
+
if isinstance(value, dict):
|
|
186
|
+
obj.__setattr__(key, RGBMatrixEmulatorConfig.ChildConfig(value, default))
|
|
187
|
+
else:
|
|
188
|
+
obj.__setattr__(key, value)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
from RGBMatrixEmulator.graphics.color import Color
|
|
2
|
+
from RGBMatrixEmulator.graphics.font import Font
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def DrawText(canvas, font, x, y, color, text):
|
|
6
|
+
# Early return for empty string prevents bugs in bdfparser library
|
|
7
|
+
# and makes good sense anyway
|
|
8
|
+
if len(text) == 0:
|
|
9
|
+
return
|
|
10
|
+
|
|
11
|
+
# Support multiple spacings based on device width
|
|
12
|
+
character_widths = [__actual_width(font, letter) for letter in text]
|
|
13
|
+
first_char_width = character_widths[0]
|
|
14
|
+
max_char_width = max(character_widths)
|
|
15
|
+
total_width = sum(character_widths)
|
|
16
|
+
|
|
17
|
+
# Offscreen to the left, adjust by first character width
|
|
18
|
+
if x < 0:
|
|
19
|
+
adjustment = abs(x + first_char_width) // first_char_width
|
|
20
|
+
text = text[adjustment:]
|
|
21
|
+
if adjustment:
|
|
22
|
+
x += first_char_width * adjustment
|
|
23
|
+
|
|
24
|
+
# Offscreen to the right, rough adjustment by max width
|
|
25
|
+
if (total_width + x) > canvas.width:
|
|
26
|
+
text = text[: ((canvas.width + 1) // max_char_width) + 2]
|
|
27
|
+
|
|
28
|
+
# Draw the text!
|
|
29
|
+
if len(text) != 0:
|
|
30
|
+
# Ensure text doesn't get drawn as multiple lines
|
|
31
|
+
linelimit = len(text) * (font.headers['fbbx'] + 1)
|
|
32
|
+
|
|
33
|
+
text_map = font.bdf_font.draw(text, linelimit, missing=font.default_character).todata(2)
|
|
34
|
+
font_y_offset = -(font.headers['fbby'] + font.headers['fbbyoff'])
|
|
35
|
+
|
|
36
|
+
for y2, row in enumerate(text_map):
|
|
37
|
+
for x2, value in enumerate(row):
|
|
38
|
+
if value == 1:
|
|
39
|
+
if isinstance(color, tuple):
|
|
40
|
+
canvas.SetPixel(x + x2, y + y2 + font_y_offset, *color)
|
|
41
|
+
else:
|
|
42
|
+
canvas.SetPixel(x + x2, y + y2 + font_y_offset, color.red, color.green, color.blue)
|
|
43
|
+
|
|
44
|
+
return total_width
|
|
45
|
+
|
|
46
|
+
def DrawLine(canvas, x1, y1, x2, y2, color):
|
|
47
|
+
int_points = __coerce_int(x1, y1, x2, y2)
|
|
48
|
+
rows, cols = __line(*int_points)
|
|
49
|
+
|
|
50
|
+
for point in zip(rows, cols):
|
|
51
|
+
if isinstance(color, tuple):
|
|
52
|
+
canvas.SetPixel(*point, *color)
|
|
53
|
+
else:
|
|
54
|
+
canvas.SetPixel(*point, color.red, color.green, color.blue)
|
|
55
|
+
|
|
56
|
+
def DrawCircle(canvas, x, y, r, color):
|
|
57
|
+
int_points = __coerce_int(x, y)
|
|
58
|
+
rows, cols = __circle_perimeter(*int_points, r)
|
|
59
|
+
|
|
60
|
+
for point in zip(rows, cols):
|
|
61
|
+
if isinstance(color, tuple):
|
|
62
|
+
canvas.SetPixel(*point, *color)
|
|
63
|
+
else:
|
|
64
|
+
canvas.SetPixel(*point, color.red, color.green, color.blue)
|
|
65
|
+
|
|
66
|
+
def __actual_width(font, letter):
|
|
67
|
+
'''
|
|
68
|
+
Returns the actual width of the letter in the font. If the font doesn't contain a glyph for this letter, it falls back to
|
|
69
|
+
the width of the default character (?) to prevent division by 0.
|
|
70
|
+
'''
|
|
71
|
+
width = font.CharacterWidth(ord(letter))
|
|
72
|
+
|
|
73
|
+
if width > 0:
|
|
74
|
+
return width
|
|
75
|
+
|
|
76
|
+
return font.CharacterWidth(font.default_character.cp())
|
|
77
|
+
|
|
78
|
+
def __coerce_int(*values):
|
|
79
|
+
return [int(value) for value in values]
|
|
80
|
+
|
|
81
|
+
def __line(x1, y1, x2, y2):
|
|
82
|
+
'''
|
|
83
|
+
Line drawing algorithm
|
|
84
|
+
|
|
85
|
+
Extracted from scikit-image:
|
|
86
|
+
https://github.com/scikit-image/scikit-image/blob/00177e14097237ef20ed3141ed454bc81b308f82/skimage/draw/_draw.pyx#L44
|
|
87
|
+
'''
|
|
88
|
+
steep = 0
|
|
89
|
+
r = x1
|
|
90
|
+
c = y1
|
|
91
|
+
dr = abs(x2 - x1)
|
|
92
|
+
dc = abs(y2 - y1)
|
|
93
|
+
|
|
94
|
+
rr = [0] * (max(dc, dr) + 1)
|
|
95
|
+
cc = [0] * (max(dc, dr) + 1)
|
|
96
|
+
|
|
97
|
+
if (y2 - c) > 0:
|
|
98
|
+
sc = 1
|
|
99
|
+
else:
|
|
100
|
+
sc = -1
|
|
101
|
+
if (x2 - r) > 0:
|
|
102
|
+
sr = 1
|
|
103
|
+
else:
|
|
104
|
+
sr = -1
|
|
105
|
+
if dr > dc:
|
|
106
|
+
steep = 1
|
|
107
|
+
c, r = r, c
|
|
108
|
+
dc, dr = dr, dc
|
|
109
|
+
sc, sr = sr, sc
|
|
110
|
+
d = (2 * dr) - dc
|
|
111
|
+
|
|
112
|
+
for i in range(dc):
|
|
113
|
+
if steep:
|
|
114
|
+
rr[i] = c
|
|
115
|
+
cc[i] = r
|
|
116
|
+
else:
|
|
117
|
+
rr[i] = r
|
|
118
|
+
cc[i] = c
|
|
119
|
+
while d >= 0:
|
|
120
|
+
r = r + sr
|
|
121
|
+
d = d - (2 * dc)
|
|
122
|
+
c = c + sc
|
|
123
|
+
d = d + (2 * dr)
|
|
124
|
+
|
|
125
|
+
rr[dc] = x2
|
|
126
|
+
cc[dc] = y2
|
|
127
|
+
|
|
128
|
+
return (rr, cc)
|
|
129
|
+
|
|
130
|
+
def __circle_perimeter(x, y, radius):
|
|
131
|
+
'''
|
|
132
|
+
Bresenham circle algorithm
|
|
133
|
+
|
|
134
|
+
Extracted from scikit-image
|
|
135
|
+
https://github.com/scikit-image/scikit-image/blob/00177e14097237ef20ed3141ed454bc81b308f82/skimage/draw/_draw.pyx#L248
|
|
136
|
+
'''
|
|
137
|
+
rr = list()
|
|
138
|
+
cc = list()
|
|
139
|
+
|
|
140
|
+
c = 0
|
|
141
|
+
r = radius
|
|
142
|
+
d = 3 - 2 * radius
|
|
143
|
+
|
|
144
|
+
while r >= c:
|
|
145
|
+
rr.extend([_ + x for _ in [r, -r, r, -r, c, -c, c, -c]])
|
|
146
|
+
cc.extend([_ + y for _ in [c, c, -c, -c, r, r, -r, -r]])
|
|
147
|
+
|
|
148
|
+
if d < 0:
|
|
149
|
+
d += 4 * c + 6
|
|
150
|
+
else:
|
|
151
|
+
d += 4 * (c - r) + 10
|
|
152
|
+
r -= 1
|
|
153
|
+
c += 1
|
|
154
|
+
|
|
155
|
+
return (rr, cc)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
class Color:
|
|
2
|
+
def __init__(self, r = 0, g = 0, b = 0):
|
|
3
|
+
self.red = r
|
|
4
|
+
self.green = g
|
|
5
|
+
self.blue = b
|
|
6
|
+
|
|
7
|
+
@classmethod
|
|
8
|
+
def adjust_brightness(cls, pixel, alpha, to_int = False):
|
|
9
|
+
p = tuple(channel * alpha for channel in pixel)
|
|
10
|
+
|
|
11
|
+
if to_int:
|
|
12
|
+
p = tuple(int(channel) for channel in pixel)
|
|
13
|
+
|
|
14
|
+
return p
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def to_hex(cls, pixel):
|
|
18
|
+
return "#%02x%02x%02x" % pixel
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def BLACK(cls):
|
|
22
|
+
return (0, 0, 0)
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def RED(cls):
|
|
26
|
+
return (255, 0, 0)
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def GREEN(cls):
|
|
30
|
+
return (0, 255, 0)
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def BLUE(cls):
|
|
34
|
+
return (0, 0, 255)
|
|
35
|
+
|
|
36
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import bdfparser
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Font:
|
|
5
|
+
def __init__(self):
|
|
6
|
+
self.bdf_font = None
|
|
7
|
+
self.headers = {}
|
|
8
|
+
self.spacing = {}
|
|
9
|
+
|
|
10
|
+
def LoadFont(self, path):
|
|
11
|
+
self.bdf_font = bdfparser.Font(path)
|
|
12
|
+
self.headers = self.bdf_font.headers
|
|
13
|
+
self.props = self.bdf_font.props
|
|
14
|
+
|
|
15
|
+
# All rpi-rgb-led-matrix fonts have a character at 0xFFFD to represent a missing character
|
|
16
|
+
# Cache this for use later so we don't have to constantly look it up
|
|
17
|
+
self.default_character = self.bdf_font.glyphbycp(0xFFFD)
|
|
18
|
+
|
|
19
|
+
def CharacterWidth(self, char):
|
|
20
|
+
# Missing glyphs return 0 width in rpi-rgb-led-matrix
|
|
21
|
+
if self.bdf_font == None or not self.bdf_font.glyphbycp(char):
|
|
22
|
+
return 0
|
|
23
|
+
|
|
24
|
+
return self.bdf_font.glyphbycp(char).meta['dwx0']
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def height(self):
|
|
28
|
+
if self.bdf_font is None: return -1
|
|
29
|
+
return self.headers['fbby']
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def baseline(self):
|
|
33
|
+
if self.bdf_font is None: return 0
|
|
34
|
+
return self.headers['fbby'] + self.headers['fbbyoff']
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import json, logging
|
|
2
|
+
|
|
3
|
+
# Try to load the config from file. (Default: INFO)
|
|
4
|
+
try:
|
|
5
|
+
with open("emulator_config.json") as config_file:
|
|
6
|
+
log_level_name = json.load(config_file).get("log_level", 'INFO').upper()
|
|
7
|
+
log_level = getattr(logging, log_level_name)
|
|
8
|
+
except:
|
|
9
|
+
log_level = logging.INFO
|
|
10
|
+
|
|
11
|
+
# Create a Logger
|
|
12
|
+
Logger = logging.getLogger('RGBME')
|
|
13
|
+
Logger.setLevel(log_level)
|
|
14
|
+
|
|
15
|
+
# Create console handler and set the log level
|
|
16
|
+
ch = logging.StreamHandler()
|
|
17
|
+
ch.setLevel(log_level)
|
|
18
|
+
|
|
19
|
+
# Create formatter
|
|
20
|
+
formatter = logging.Formatter(
|
|
21
|
+
'[%(asctime)s] [%(name)s] [%(levelname)s]: %(message)s',
|
|
22
|
+
datefmt = '%Y-%m-%d %H:%M:%S'
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# Add formatter to console handler
|
|
26
|
+
ch.setFormatter(formatter)
|
|
27
|
+
|
|
28
|
+
# Add console handler to Logger
|
|
29
|
+
Logger.addHandler(ch)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
function init() {
|
|
2
|
+
const WS_RETRY_DELAY = 2000;
|
|
3
|
+
const FPS_DEFAULT = 24;
|
|
4
|
+
|
|
5
|
+
let img = document.getElementById("liveImg");
|
|
6
|
+
let fpsText = document.getElementById("fps");
|
|
7
|
+
let fpsTarget = parseInt(document.getElementById("targetFps").value) || FPS_DEFAULT;
|
|
8
|
+
|
|
9
|
+
let requestStartTime = performance.now();
|
|
10
|
+
let startTime = performance.now();
|
|
11
|
+
let time = 0;
|
|
12
|
+
let requestTime = 0;
|
|
13
|
+
let timeSmoothing = 0.9; // larger=more smoothing
|
|
14
|
+
let requestTimeSmoothing = 0.2; // larger=more smoothing
|
|
15
|
+
let targetTime = 1000 / fpsTarget;
|
|
16
|
+
|
|
17
|
+
let socket = generateSocket();
|
|
18
|
+
|
|
19
|
+
function requestImage() {
|
|
20
|
+
requestStartTime = performance.now();
|
|
21
|
+
socket.send('more');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function generateSocket() {
|
|
25
|
+
let path = location.pathname;
|
|
26
|
+
|
|
27
|
+
if (path.endsWith("index.html")) {
|
|
28
|
+
path = path.substring(0, path.length - "index.html".length);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if(!path.endsWith("/")) {
|
|
32
|
+
path = path + "/";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let wsProtocol = (location.protocol === "https:") ? "wss://" : "ws://";
|
|
36
|
+
let ws = new WebSocket(wsProtocol + location.host + path + "websocket");
|
|
37
|
+
|
|
38
|
+
ws.binaryType = 'arraybuffer';
|
|
39
|
+
|
|
40
|
+
ws.onopen = function() {
|
|
41
|
+
console.log("RGBME WebSocket connection established!");
|
|
42
|
+
startTime = performance.now();
|
|
43
|
+
requestImage();
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
ws.onclose = function() {
|
|
47
|
+
// Handle retries by recreating the connection to websocket.
|
|
48
|
+
console.warn(`RGBME WebSocket connection lost. Retrying in ${WS_RETRY_DELAY / 1000}s.`)
|
|
49
|
+
setTimeout(function() {
|
|
50
|
+
// We generate socket with a timeout to make sure server has time to recover.
|
|
51
|
+
socket = generateSocket();
|
|
52
|
+
}, WS_RETRY_DELAY);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
ws.onerror = function() {
|
|
56
|
+
ws.close();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
ws.onmessage = function(evt) {
|
|
60
|
+
let arrayBuffer = evt.data;
|
|
61
|
+
let blob = new Blob([new Uint8Array(arrayBuffer)], {type: "image/jpeg"});
|
|
62
|
+
let old_img = img.src.slice()
|
|
63
|
+
img.src = window.URL.createObjectURL(blob);
|
|
64
|
+
window.URL.revokeObjectURL(old_img);
|
|
65
|
+
|
|
66
|
+
let endTime = performance.now();
|
|
67
|
+
let currentTime = endTime - startTime;
|
|
68
|
+
// smooth with moving average
|
|
69
|
+
time = (time * timeSmoothing) + (currentTime * (1.0 - timeSmoothing));
|
|
70
|
+
startTime = endTime;
|
|
71
|
+
let fps = Math.round(1000 / time);
|
|
72
|
+
|
|
73
|
+
if (fpsText) {
|
|
74
|
+
fpsText.textContent = fps;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let currentRequestTime = performance.now() - requestStartTime;
|
|
78
|
+
// smooth with moving average
|
|
79
|
+
requestTime = (requestTime * requestTimeSmoothing) + (currentRequestTime * (1.0 - requestTimeSmoothing));
|
|
80
|
+
let timeout = Math.max(0, targetTime - requestTime);
|
|
81
|
+
|
|
82
|
+
setTimeout(requestImage, timeout);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return ws;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
console.log(`TARGET FPS: ${fpsTarget}`);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
init();
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
<html>
|
|
2
|
+
<head>
|
|
3
|
+
<title>{{ adapter.emulator_details_text() }}</title>
|
|
4
|
+
<link rel="shortcut icon" href="assets/icon.ico">
|
|
5
|
+
<link rel="stylesheet" href="assets/styles.css">
|
|
6
|
+
</head>
|
|
7
|
+
<body>
|
|
8
|
+
<img id="liveImg" class={{ "" if adapter.options.browser.image_border else "no-border" }} />
|
|
9
|
+
|
|
10
|
+
{% if adapter.options.browser.fps_display %}
|
|
11
|
+
<div id="fpsDisplay">
|
|
12
|
+
Frames per second: <span id="fps">0</span>
|
|
13
|
+
</div>
|
|
14
|
+
{% end %}
|
|
15
|
+
|
|
16
|
+
{% if adapter.options.browser.debug_text %}
|
|
17
|
+
<div id="emulatorDetails">
|
|
18
|
+
<h2>{{ adapter.emulator_details_text() }}</h2>
|
|
19
|
+
|
|
20
|
+
<div class="emulatorDetailsTableRow">
|
|
21
|
+
<div id="matrixDetails" class="tableContainer">
|
|
22
|
+
<h3>Matrix Details:</h3>
|
|
23
|
+
|
|
24
|
+
<table>
|
|
25
|
+
<tbody>
|
|
26
|
+
<tr>
|
|
27
|
+
<td>
|
|
28
|
+
Matrix Width:
|
|
29
|
+
</td>
|
|
30
|
+
<td>
|
|
31
|
+
{{ adapter.options.cols }}
|
|
32
|
+
</td>
|
|
33
|
+
</tr>
|
|
34
|
+
<tr>
|
|
35
|
+
<td>
|
|
36
|
+
Matrix Height:
|
|
37
|
+
</td>
|
|
38
|
+
<td>
|
|
39
|
+
{{ adapter.options.rows }}
|
|
40
|
+
</td>
|
|
41
|
+
</td>
|
|
42
|
+
</tr>
|
|
43
|
+
<tr>
|
|
44
|
+
<td>
|
|
45
|
+
Image Size:
|
|
46
|
+
</td>
|
|
47
|
+
<td>
|
|
48
|
+
{{ adapter.options.window_size_str(pixel_text="px") }}
|
|
49
|
+
</td>
|
|
50
|
+
</tr>
|
|
51
|
+
<tr>
|
|
52
|
+
<td>
|
|
53
|
+
Chain Length:
|
|
54
|
+
</td>
|
|
55
|
+
<td>
|
|
56
|
+
{{ adapter.options.chain_length }}
|
|
57
|
+
</td>
|
|
58
|
+
</tr>
|
|
59
|
+
<tr>
|
|
60
|
+
<td>
|
|
61
|
+
Parallel Chains:
|
|
62
|
+
</td>
|
|
63
|
+
<td>
|
|
64
|
+
{{ adapter.options.parallel }}
|
|
65
|
+
</td>
|
|
66
|
+
</tr>
|
|
67
|
+
<tr>
|
|
68
|
+
<td>
|
|
69
|
+
Pixel Size:
|
|
70
|
+
</td>
|
|
71
|
+
<td>
|
|
72
|
+
{{ adapter.options.pixel_size }}
|
|
73
|
+
</td>
|
|
74
|
+
</tr>
|
|
75
|
+
<tr>
|
|
76
|
+
<td>
|
|
77
|
+
Pixel Style:
|
|
78
|
+
</td>
|
|
79
|
+
<td>
|
|
80
|
+
{{ adapter.options.pixel_style }}
|
|
81
|
+
</td>
|
|
82
|
+
</tr>
|
|
83
|
+
<tr>
|
|
84
|
+
<td>
|
|
85
|
+
Adapter:
|
|
86
|
+
</td>
|
|
87
|
+
<td>
|
|
88
|
+
{{ adapter.__class__.__name__ }}
|
|
89
|
+
</td>
|
|
90
|
+
</tr>
|
|
91
|
+
</tbody>
|
|
92
|
+
</table>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<div id="browserDetails" class="tableContainer">
|
|
96
|
+
<h3>Browser Details:</h3>
|
|
97
|
+
|
|
98
|
+
<table>
|
|
99
|
+
<tbody>
|
|
100
|
+
<tr>
|
|
101
|
+
<td>
|
|
102
|
+
Port:
|
|
103
|
+
</td>
|
|
104
|
+
<td>
|
|
105
|
+
{{ adapter.options.browser.port }}
|
|
106
|
+
</td>
|
|
107
|
+
</tr>
|
|
108
|
+
<tr>
|
|
109
|
+
<td>
|
|
110
|
+
Target FPS:
|
|
111
|
+
</td>
|
|
112
|
+
<td>
|
|
113
|
+
{{ adapter.options.browser.target_fps }}
|
|
114
|
+
</td>
|
|
115
|
+
</td>
|
|
116
|
+
</tr>
|
|
117
|
+
<tr>
|
|
118
|
+
<td>
|
|
119
|
+
FPS Display Enabled:
|
|
120
|
+
</td>
|
|
121
|
+
<td>
|
|
122
|
+
{{ adapter.options.browser.fps_display }}
|
|
123
|
+
</td>
|
|
124
|
+
</tr>
|
|
125
|
+
<tr>
|
|
126
|
+
<td>
|
|
127
|
+
Image Quality:
|
|
128
|
+
</td>
|
|
129
|
+
<td>
|
|
130
|
+
{{ adapter.options.browser.quality }}
|
|
131
|
+
</td>
|
|
132
|
+
</tr>
|
|
133
|
+
</tbody>
|
|
134
|
+
</table>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
{% end %}
|
|
139
|
+
|
|
140
|
+
<input id="targetFps" type="hidden" value={{ adapter.options.browser.target_fps }} />
|
|
141
|
+
|
|
142
|
+
<script type="text/javascript" src="assets/client.js"></script>
|
|
143
|
+
</body>
|
|
144
|
+
</html>
|