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,68 @@
|
|
|
1
|
+
import importlib, json
|
|
2
|
+
|
|
3
|
+
from RGBMatrixEmulator.logger import Logger
|
|
4
|
+
|
|
5
|
+
adapters = [
|
|
6
|
+
{
|
|
7
|
+
'path': 'RGBMatrixEmulator.adapters.browser_adapter.adapter',
|
|
8
|
+
'class': 'BrowserAdapter',
|
|
9
|
+
'type': 'browser'
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
'path': 'RGBMatrixEmulator.adapters.pygame_adapter',
|
|
13
|
+
'class': 'PygameAdapter',
|
|
14
|
+
'type': 'pygame'
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
'path': 'RGBMatrixEmulator.adapters.sixel_adapter',
|
|
18
|
+
'class': 'SixelAdapter',
|
|
19
|
+
'type': 'sixel'
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
'path': 'RGBMatrixEmulator.adapters.terminal_adapter',
|
|
23
|
+
'class': 'TerminalAdapter',
|
|
24
|
+
'type': 'terminal'
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
'path': 'RGBMatrixEmulator.adapters.tkinter_adapter',
|
|
28
|
+
'class': 'TkinterAdapter',
|
|
29
|
+
'type': 'tkinter'
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
'path': 'RGBMatrixEmulator.adapters.turtle_adapter',
|
|
33
|
+
'class': 'TurtleAdapter',
|
|
34
|
+
'type': 'turtle'
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
ADAPTER_TYPES = {}
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
with open("emulator_config.json") as config_file:
|
|
42
|
+
suppress_adapter_load_errors = json.load(config_file).get("suppress_adapter_load_errors", False)
|
|
43
|
+
except:
|
|
44
|
+
suppress_adapter_load_errors = False
|
|
45
|
+
|
|
46
|
+
for adapter in adapters:
|
|
47
|
+
package_path = adapter.get('path')
|
|
48
|
+
adapter_class = adapter.get('class')
|
|
49
|
+
adapter_name = adapter.get('type')
|
|
50
|
+
try:
|
|
51
|
+
package = importlib.import_module(package_path)
|
|
52
|
+
adapter = getattr(package, adapter_class)
|
|
53
|
+
|
|
54
|
+
ADAPTER_TYPES[adapter_name] = adapter
|
|
55
|
+
except Exception as ex:
|
|
56
|
+
if suppress_adapter_load_errors: continue
|
|
57
|
+
|
|
58
|
+
Logger.exception(f'''
|
|
59
|
+
Failed to load {adapter_class} for "{adapter_name}" display adapter!
|
|
60
|
+
|
|
61
|
+
If this is not your configured display adapter, the emulator will continue to load.
|
|
62
|
+
|
|
63
|
+
You can suppress this error in the `emulator_config.json` by adding:
|
|
64
|
+
|
|
65
|
+
"suppress_adapter_load_errors": true
|
|
66
|
+
|
|
67
|
+
'''
|
|
68
|
+
)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from PIL import Image, ImageDraw, ImageEnhance
|
|
4
|
+
from RGBMatrixEmulator import version
|
|
5
|
+
from RGBMatrixEmulator.graphics import Color
|
|
6
|
+
|
|
7
|
+
def draw_circle_mask(drawer, x, y, pixel_size, color):
|
|
8
|
+
drawer.ellipse(
|
|
9
|
+
(x, y, x + pixel_size - 1, y + pixel_size - 1),
|
|
10
|
+
fill=color,
|
|
11
|
+
outline=color,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
def draw_square_mask(drawer, x, y, pixel_size, color):
|
|
15
|
+
drawer.rectangle(
|
|
16
|
+
(x, y, x + pixel_size, y + pixel_size),
|
|
17
|
+
fill=color,
|
|
18
|
+
outline=color,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
class BaseAdapter:
|
|
22
|
+
|
|
23
|
+
SUPPORTS_ALTERNATE_PIXEL_STYLE = False
|
|
24
|
+
INSTANCE = None
|
|
25
|
+
|
|
26
|
+
def __init__(self, width, height, options):
|
|
27
|
+
self.width = width
|
|
28
|
+
self.height = height
|
|
29
|
+
self.options = options
|
|
30
|
+
self.__black = Image.new("RGB", self.options.window_size(), "black")
|
|
31
|
+
self.__mask = self.__draw_mask()
|
|
32
|
+
self.loaded = False
|
|
33
|
+
|
|
34
|
+
def __draw_mask(self):
|
|
35
|
+
mask = Image.new("L", self.options.window_size())
|
|
36
|
+
drawer = ImageDraw.Draw(mask)
|
|
37
|
+
pixel_size = self.options.pixel_size
|
|
38
|
+
width, height = self.options.window_size()
|
|
39
|
+
color = int((self.options.brightness * 255) / 100)
|
|
40
|
+
draw_mask_shape = (draw_circle_mask
|
|
41
|
+
if self.options.pixel_style == "circle" else draw_square_mask)
|
|
42
|
+
for y in range(0, height, pixel_size):
|
|
43
|
+
for x in range(0, width, pixel_size):
|
|
44
|
+
draw_mask_shape(drawer, x, y, pixel_size, color)
|
|
45
|
+
|
|
46
|
+
return mask
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def get_instance(cls, *args, **kwargs):
|
|
50
|
+
if cls.INSTANCE is None:
|
|
51
|
+
instance = cls(*args, **kwargs)
|
|
52
|
+
cls.INSTANCE = instance
|
|
53
|
+
|
|
54
|
+
return cls.INSTANCE
|
|
55
|
+
|
|
56
|
+
def adjust_pixel_brightness(self, pixel, to_int = True):
|
|
57
|
+
alpha = self.options.brightness / 100.0
|
|
58
|
+
return Color.adjust_brightness(pixel, alpha, to_int = to_int)
|
|
59
|
+
|
|
60
|
+
def pixel_out_of_bounds(self, x, y):
|
|
61
|
+
if x < 0 or x >= self.width:
|
|
62
|
+
return True
|
|
63
|
+
|
|
64
|
+
if y < 0 or y >= self.height:
|
|
65
|
+
return True
|
|
66
|
+
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
def _get_masked_image(self, pixels):
|
|
70
|
+
image = Image.fromarray(np.array(pixels, dtype=np.uint8), "RGB")
|
|
71
|
+
image = image.resize(self.options.window_size(), Image.NEAREST)
|
|
72
|
+
enhancer = ImageEnhance.Brightness(image)
|
|
73
|
+
image = enhancer.enhance(self.options.brightness / 100.0)
|
|
74
|
+
|
|
75
|
+
return Image.composite(image, self.__black, self.__mask)
|
|
76
|
+
|
|
77
|
+
def emulator_details_text(self):
|
|
78
|
+
details_text = 'RGBME v{} - {}x{} Matrix | {}x{} Chain | {}px per LED ({}) | {}'
|
|
79
|
+
|
|
80
|
+
return details_text.format(version.__version__,
|
|
81
|
+
self.options.cols,
|
|
82
|
+
self.options.rows,
|
|
83
|
+
self.options.chain_length,
|
|
84
|
+
self.options.parallel,
|
|
85
|
+
self.options.pixel_size,
|
|
86
|
+
self.options.pixel_style.upper(),
|
|
87
|
+
self.__class__.__name__)
|
|
88
|
+
|
|
89
|
+
# This method is required for the pygame adapter but nothing else, so just skip it if not defined.
|
|
90
|
+
def check_for_quit_event(self):
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
#############################################################
|
|
94
|
+
# These methods must be implemented by BaseAdapter subclasses
|
|
95
|
+
#############################################################
|
|
96
|
+
def load_emulator_window(self):
|
|
97
|
+
'''
|
|
98
|
+
Initialize the external dependency as a graphics display.
|
|
99
|
+
|
|
100
|
+
This method is fired when the emulated canvas is initialized.
|
|
101
|
+
'''
|
|
102
|
+
raise NotImplementedError
|
|
103
|
+
|
|
104
|
+
def draw_to_screen(self, _pixels):
|
|
105
|
+
'''
|
|
106
|
+
Accepts a 2D array of pixels of size height x width.
|
|
107
|
+
|
|
108
|
+
Implements drawing each pixel to the screen via the external dependency loaded in load_emulator_window.
|
|
109
|
+
Before drawing, use adjust_pixel_brightness() on each pixel if your display adapter supports it.
|
|
110
|
+
'''
|
|
111
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,89 @@
|
|
|
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
|
+

|
|
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
|
+
#### Tydbit Support
|
|
51
|
+
|
|
52
|
+
Tydbit requires `WebP` images to be exposed over a static HTTP endpoint. You can use the browser adapter's static image endpoint to provide cross-functional compatibility to Tydbit boards.
|
|
53
|
+
|
|
54
|
+
An example configuration that works for Tydbit matrices is provided:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"pixel_outline": 0,
|
|
59
|
+
"pixel_size": 1,
|
|
60
|
+
"pixel_style": "square",
|
|
61
|
+
"display_adapter": "browser",
|
|
62
|
+
"suppress_font_warnings": false,
|
|
63
|
+
"suppress_adapter_load_errors": false,
|
|
64
|
+
"browser": {
|
|
65
|
+
"port": 8888,
|
|
66
|
+
"target_fps": 24,
|
|
67
|
+
"fps_display": false,
|
|
68
|
+
"quality": 70,
|
|
69
|
+
"image_border": true,
|
|
70
|
+
"debug_text": false,
|
|
71
|
+
"image_format": "WebP"
|
|
72
|
+
},
|
|
73
|
+
"log_level": "info"
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Error Handling
|
|
78
|
+
|
|
79
|
+
Exceptions in emulated Python scripts will cause the server to shut down. Fix the errors in the script before attempting to restart.
|
|
80
|
+
|
|
81
|
+
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.
|
|
82
|
+
|
|
83
|
+
You can view the errors via a console in the browser (which can be opened with hotkey `F12` or right click -> "Inspect").
|
|
84
|
+
|
|
85
|
+
## Known Issues
|
|
86
|
+
|
|
87
|
+
* 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
|
|
88
|
+
* Excess blob references do not get cleaned up correctly in some instances, leading to memory leaks in some browsers
|
|
89
|
+
* Main symptom appears to be a hanging browser when closing the emulator tab
|
|
File without changes
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import io
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from PIL import Image, ImageDraw
|
|
6
|
+
from RGBMatrixEmulator.graphics import Color
|
|
7
|
+
from RGBMatrixEmulator.adapters.base import BaseAdapter
|
|
8
|
+
from RGBMatrixEmulator.adapters.browser_adapter.server import Server
|
|
9
|
+
from RGBMatrixEmulator.logger import Logger
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BrowserAdapter(BaseAdapter):
|
|
13
|
+
|
|
14
|
+
SUPPORTS_ALTERNATE_PIXEL_STYLE = True
|
|
15
|
+
IMAGE_FORMATS = {
|
|
16
|
+
'bmp': 'BMP',
|
|
17
|
+
'jpeg': 'JPEG',
|
|
18
|
+
'png': 'PNG',
|
|
19
|
+
'webp': 'WebP'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
def __init__(self, width, height, options):
|
|
23
|
+
super().__init__(width, height, options)
|
|
24
|
+
self.__server = None
|
|
25
|
+
self.image = None
|
|
26
|
+
self.default_image_format = "JPEG"
|
|
27
|
+
|
|
28
|
+
image_format = options.browser.image_format
|
|
29
|
+
if image_format.lower() in self.IMAGE_FORMATS:
|
|
30
|
+
self.image_format = self.IMAGE_FORMATS[image_format.lower()]
|
|
31
|
+
else:
|
|
32
|
+
Logger.warning("Invalid browser image format '{}', falling back to '{}'".format(image_format, self.default_image_format))
|
|
33
|
+
self.image_format = self.IMAGE_FORMATS.get(self.default_image_format.lower())
|
|
34
|
+
|
|
35
|
+
def load_emulator_window(self):
|
|
36
|
+
if self.loaded:
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
Logger.info(self.emulator_details_text())
|
|
40
|
+
|
|
41
|
+
self.__server = Server(self)
|
|
42
|
+
self.__server.run()
|
|
43
|
+
|
|
44
|
+
self.loaded = True
|
|
45
|
+
|
|
46
|
+
def draw_to_screen(self, pixels):
|
|
47
|
+
image = self._get_masked_image(pixels)
|
|
48
|
+
with io.BytesIO() as bytesIO:
|
|
49
|
+
image.save(
|
|
50
|
+
bytesIO, self.image_format, quality=self.options.browser.quality, optimize=True
|
|
51
|
+
)
|
|
52
|
+
self.image = bytesIO.getvalue()
|
|
53
|
+
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
from RGBMatrixEmulator.adapters.browser_adapter.request_handlers.main import MainHandler
|
|
2
|
+
from RGBMatrixEmulator.adapters.browser_adapter.request_handlers.image import ImageHandler
|
|
3
|
+
from RGBMatrixEmulator.adapters.browser_adapter.request_handlers.image_web_socket import ImageWebSocketHandler
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import tornado.web
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ImageHandler(tornado.web.RequestHandler):
|
|
5
|
+
def get(self):
|
|
6
|
+
self.set_header("Content-type", "image/{}".format(self.adapter.image_format.lower()))
|
|
7
|
+
self.write(self.adapter.image)
|
|
8
|
+
|
|
9
|
+
def register_adapter(adapter):
|
|
10
|
+
ImageHandler.adapter = adapter
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import tornado.websocket
|
|
2
|
+
|
|
3
|
+
from RGBMatrixEmulator.logger import Logger
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ImageWebSocketHandler(tornado.websocket.WebSocketHandler):
|
|
7
|
+
clients = set()
|
|
8
|
+
adapter = None
|
|
9
|
+
|
|
10
|
+
def check_origin(self, _origin):
|
|
11
|
+
# Allow access from every origin
|
|
12
|
+
return True
|
|
13
|
+
|
|
14
|
+
def open(self):
|
|
15
|
+
ImageWebSocketHandler.clients.add(self)
|
|
16
|
+
Logger.info("WebSocket opened from: " + self.request.remote_ip)
|
|
17
|
+
|
|
18
|
+
def on_message(self, _message):
|
|
19
|
+
if not ImageWebSocketHandler.adapter.image:
|
|
20
|
+
Logger.warning("No image received from {}!".format(ImageWebSocketHandler.adapter.__class__.__name__))
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
image_bytes = ImageWebSocketHandler.adapter.image
|
|
24
|
+
self.write_message(image_bytes, binary=True)
|
|
25
|
+
|
|
26
|
+
def on_close(self):
|
|
27
|
+
ImageWebSocketHandler.clients.remove(self)
|
|
28
|
+
|
|
29
|
+
def register_adapter(adapter):
|
|
30
|
+
ImageWebSocketHandler.adapter = adapter
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import signal
|
|
3
|
+
import sys
|
|
4
|
+
import threading
|
|
5
|
+
import tornado.web
|
|
6
|
+
import tornado.ioloop
|
|
7
|
+
|
|
8
|
+
from os import path
|
|
9
|
+
from tornado.platform.asyncio import AnyThreadEventLoopPolicy
|
|
10
|
+
|
|
11
|
+
from RGBMatrixEmulator.adapters.browser_adapter.request_handlers import *
|
|
12
|
+
from RGBMatrixEmulator.logger import Logger
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
asyncio.set_event_loop_policy(AnyThreadEventLoopPolicy())
|
|
16
|
+
|
|
17
|
+
class Server:
|
|
18
|
+
|
|
19
|
+
instance = None
|
|
20
|
+
|
|
21
|
+
# Singleton class for the server.
|
|
22
|
+
# No more than one webserver can be running at a given time.
|
|
23
|
+
class __Singleton:
|
|
24
|
+
|
|
25
|
+
def __init__(self, adapter):
|
|
26
|
+
self.adapter = adapter
|
|
27
|
+
self.io_loop = None
|
|
28
|
+
self.listening = False
|
|
29
|
+
|
|
30
|
+
MainHandler.register_adapter(self.adapter)
|
|
31
|
+
ImageWebSocketHandler.register_adapter(self.adapter)
|
|
32
|
+
ImageHandler.register_adapter(self.adapter)
|
|
33
|
+
|
|
34
|
+
script_path = path.dirname(path.realpath(__file__))
|
|
35
|
+
asset_path = path.normpath(script_path + '/static/assets/')
|
|
36
|
+
|
|
37
|
+
self.app = tornado.web.Application([
|
|
38
|
+
(r"/websocket", ImageWebSocketHandler),
|
|
39
|
+
(r"/image", ImageHandler),
|
|
40
|
+
(r"/", MainHandler),
|
|
41
|
+
(r"/assets/(.*)", tornado.web.StaticFileHandler, { 'path': asset_path, 'default_filename': 'client.js' })
|
|
42
|
+
])
|
|
43
|
+
|
|
44
|
+
def __init__(self, adapter):
|
|
45
|
+
if not Server.instance:
|
|
46
|
+
Server.instance = Server.__Singleton(adapter)
|
|
47
|
+
|
|
48
|
+
def run(self):
|
|
49
|
+
if not self.instance.listening:
|
|
50
|
+
Logger.info("Starting server...")
|
|
51
|
+
|
|
52
|
+
self.instance.listening = True
|
|
53
|
+
self.instance.app.listen(self.instance.adapter.options.browser.port)
|
|
54
|
+
self.instance.io_loop = tornado.ioloop.IOLoop.current()
|
|
55
|
+
thread = threading.Thread(target=self.instance.io_loop.start, name="RGBMEServerThread", daemon=True)
|
|
56
|
+
self.__initialize_interrupts()
|
|
57
|
+
thread.start()
|
|
58
|
+
|
|
59
|
+
Logger.info("Server started and ready to accept requests on http://localhost:" + str(self.instance.adapter.options.browser.port) + "/")
|
|
60
|
+
|
|
61
|
+
def __initialize_interrupts(self):
|
|
62
|
+
'''
|
|
63
|
+
Add custom signal handling to ensure webserver thread exits appropriately.
|
|
64
|
+
|
|
65
|
+
Not thread-safe, signal handling must happen on the main thread.
|
|
66
|
+
'''
|
|
67
|
+
if threading.current_thread() is threading.main_thread():
|
|
68
|
+
signal.signal(signal.SIGINT , self.__kill)
|
|
69
|
+
signal.signal(signal.SIGTERM, self.__kill)
|
|
70
|
+
|
|
71
|
+
def __kill(self, *_args):
|
|
72
|
+
self.instance.io_loop.add_callback(self.instance.io_loop.stop)
|
|
73
|
+
sys.exit()
|
|
@@ -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
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
body {
|
|
2
|
+
background-color: black;
|
|
3
|
+
color: white;
|
|
4
|
+
font-family: monospace;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
img#liveImg {
|
|
8
|
+
border: 1px solid gray;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
img#liveImg.no-border {
|
|
12
|
+
border: none;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.emulatorDetailsTableRow {
|
|
16
|
+
display: flex;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.tableContainer {
|
|
20
|
+
margin-right: 20px;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
td {
|
|
24
|
+
padding-right: 10px;
|
|
25
|
+
}
|