fastled 1.1.0__tar.gz → 1.1.2__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.
- {fastled-1.1.0 → fastled-1.1.2}/PKG-INFO +3 -2
- {fastled-1.1.0 → fastled-1.1.2}/README.md +2 -1
- {fastled-1.1.0 → fastled-1.1.2}/examples/wasm/wasm.ino +5 -9
- {fastled-1.1.0 → fastled-1.1.2}/pyproject.toml +1 -1
- {fastled-1.1.0 → fastled-1.1.2}/src/fastled/app.py +73 -6
- {fastled-1.1.0 → fastled-1.1.2}/src/fastled/compile_server.py +94 -41
- {fastled-1.1.0 → fastled-1.1.2}/src/fastled/docker_manager.py +8 -6
- {fastled-1.1.0 → fastled-1.1.2}/src/fastled/web_compile.py +220 -173
- {fastled-1.1.0 → fastled-1.1.2}/src/fastled.egg-info/PKG-INFO +3 -2
- {fastled-1.1.0 → fastled-1.1.2}/test +1 -1
- {fastled-1.1.0 → fastled-1.1.2}/tests/test_compile_server.py +1 -1
- {fastled-1.1.0 → fastled-1.1.2}/tests/test_webcompile.py +1 -1
- {fastled-1.1.0 → fastled-1.1.2}/.aiderignore +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/.github/workflows/build_multi_docker_image.yml +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/.github/workflows/lint.yml +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/.github/workflows/test_macos.yml +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/.github/workflows/test_ubuntu.yml +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/.github/workflows/test_win.yml +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/.gitignore +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/.pylintrc +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/.vscode/launch.json +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/.vscode/settings.json +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/.vscode/tasks.json +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/LICENSE +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/MANIFEST.in +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/clean +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/docs/fastled.js +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/docs/fastled.wasm +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/docs/index.css +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/docs/index.html +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/docs/index.js +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/examples/Blink/Blink.ino +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/examples/Chromancer/Chromancer.ino +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/examples/Chromancer/detail.h +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/examples/Chromancer/gary_woos_wled_port/gary_woos_wled_ledmap.h +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/examples/Chromancer/gary_woos_wled_port/presets.json +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/examples/Chromancer/gary_woos_wled_port/presets.min.json +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/examples/Chromancer/gen.py +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/examples/Chromancer/mapping.h +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/examples/Chromancer/net.h +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/examples/Chromancer/output.json +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/examples/Chromancer/ripple.h +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/examples/Chromancer/screenmap.json.h +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/examples/ColorPalette/ColorPalette.ino +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/examples/ColorTemperature/ColorTemperature.ino +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/examples/Cylon/Cylon.ino +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/examples/DemoReel100/DemoReel100.ino +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/examples/Esp32Rmt51/Esp32Rmt51.ino +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/examples/EspI2SDemo/EspI2SDemo.ino +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/examples/Fire2012/Fire2012.ino +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/examples/Fire2012WithPalette/Fire2012WithPalette.ino +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/examples/FirstLight/FirstLight.ino +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/examples/FxEngine/FxEngine.ino +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/examples/Noise/Noise.ino +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/examples/NoisePlayground/NoisePlayground.ino +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/examples/NoisePlusPalette/NoisePlusPalette.ino +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/examples/OctoWS2811/OctoWS2811.ino +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/examples/Pacifica/Pacifica.ino +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/examples/Pride2015/Pride2015.ino +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/examples/SdCard/SdCard.ino +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/examples/TwinkleFox/TwinkleFox.ino +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/examples/Video/Gfx2Video/Gfx2Video.ino +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/examples/WasmScreenCoords/WasmScreenCoords.ino +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/examples/Water/Water.ino +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/examples/XYMatrix/XYMatrix.ino +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/install +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/lint +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/requirements.testing.txt +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/setup.cfg +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/setup.py +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/src/fastled/__init__.py +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/src/fastled/assets/example.txt +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/src/fastled/build_mode.py +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/src/fastled/check_cpp_syntax.py +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/src/fastled/cli.py +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/src/fastled/filewatcher.py +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/src/fastled/open_browser.py +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/src/fastled/paths.py +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/src/fastled.egg-info/SOURCES.txt +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/src/fastled.egg-info/dependency_links.txt +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/src/fastled.egg-info/entry_points.txt +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/src/fastled.egg-info/requires.txt +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/src/fastled.egg-info/top_level.txt +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/tests/test_bad_ino.py +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/tests/test_cli.py +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/tests/test_filechanger.py +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/tests/test_ino/bad/bad.ino +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/tests/test_ino/wasm/wasm.ino +0 -0
- {fastled-1.1.0 → fastled-1.1.2}/upload_package.sh +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: fastled
|
3
|
-
Version: 1.1.
|
3
|
+
Version: 1.1.2
|
4
4
|
Summary: FastLED Wasm Compiler
|
5
5
|
Home-page: https://github.com/zackees/fastled-wasm
|
6
6
|
Maintainer: Zachary Vorhies
|
@@ -28,7 +28,6 @@ Compiles an Arduino/Platformio sketch into a wasm binary that can be run directl
|
|
28
28
|
[](https://github.com/zackees/fastled-wasm/actions/workflows/test_win.yml)
|
29
29
|
|
30
30
|
|
31
|
-
|
32
31
|
# About
|
33
32
|
|
34
33
|
This python app will compile your FastLED style sketches into html/js/wasm output that runs directly in the browser.
|
@@ -92,6 +91,8 @@ provide shims for most of the common api points.
|
|
92
91
|
|
93
92
|
# Revisions
|
94
93
|
|
94
|
+
* 1.1.2 - `--server` will now volume map fastled src directory if it detects this. This was also implemented on the docker side.
|
95
|
+
* 1.1.1 - `--interactive` is now supported to debug the container. Volume maps and better compatibilty with ipv4/v6 by concurrent connection finding.
|
95
96
|
* 1.1.0 - Use `fastled` as the command for the wasm compiler.
|
96
97
|
* 1.0.17 - Pulls updates when necessary. Removed dependency on keyring.
|
97
98
|
* 1.0.16 - `fastled-wasm` package name has been changed to `fled`
|
@@ -9,7 +9,6 @@ Compiles an Arduino/Platformio sketch into a wasm binary that can be run directl
|
|
9
9
|
[](https://github.com/zackees/fastled-wasm/actions/workflows/test_win.yml)
|
10
10
|
|
11
11
|
|
12
|
-
|
13
12
|
# About
|
14
13
|
|
15
14
|
This python app will compile your FastLED style sketches into html/js/wasm output that runs directly in the browser.
|
@@ -73,6 +72,8 @@ provide shims for most of the common api points.
|
|
73
72
|
|
74
73
|
# Revisions
|
75
74
|
|
75
|
+
* 1.1.2 - `--server` will now volume map fastled src directory if it detects this. This was also implemented on the docker side.
|
76
|
+
* 1.1.1 - `--interactive` is now supported to debug the container. Volume maps and better compatibilty with ipv4/v6 by concurrent connection finding.
|
76
77
|
* 1.1.0 - Use `fastled` as the command for the wasm compiler.
|
77
78
|
* 1.0.17 - Pulls updates when necessary. Removed dependency on keyring.
|
78
79
|
* 1.0.16 - `fastled-wasm` package name has been changed to `fled`
|
@@ -14,10 +14,9 @@
|
|
14
14
|
#include "fx/fx_engine.h"
|
15
15
|
|
16
16
|
#include "fx/2d/animartrix.hpp"
|
17
|
-
#include "platforms/wasm/js.h"
|
18
|
-
|
19
17
|
#include "ui.h"
|
20
18
|
|
19
|
+
|
21
20
|
#define LED_PIN 3
|
22
21
|
#define BRIGHTNESS 96
|
23
22
|
#define COLOR_ORDER GRB
|
@@ -69,6 +68,9 @@ CRGB leds[NUM_LEDS];
|
|
69
68
|
XYMap xyMap = XYMap::constructRectangularGrid(MATRIX_WIDTH, MATRIX_HEIGHT);
|
70
69
|
NoisePalette noisePalette = NoisePalette(xyMap);
|
71
70
|
|
71
|
+
Title title("FastLED Wasm Demo");
|
72
|
+
Description description("This example combines two features of FastLED to produce a remarkable range of effects from a relatively small amount of code. This example combines FastLED's color palette lookup functions with FastLED's Perlin noise generator, and the combination is extremely powerful.");
|
73
|
+
|
72
74
|
Slider brightness("Brightness", 255, 0, 255);
|
73
75
|
Checkbox isOff("Off", false);
|
74
76
|
Slider speed("Noise - Speed", 15, 1, 50);
|
@@ -77,8 +79,7 @@ Slider changePalletTime("Noise - Time until next random Palette", 5, 1, 100);
|
|
77
79
|
Slider scale( "Noise - Scale", 20, 1, 100);
|
78
80
|
Button changePalette("Noise - Next Palette");
|
79
81
|
Button changeFx("Switch between Noise & Animartrix");
|
80
|
-
NumberField fxIndex("Animartrix -
|
81
|
-
|
82
|
+
NumberField fxIndex("Animartrix - iex", 0, 0, NUM_ANIMATIONS);
|
82
83
|
|
83
84
|
Animartrix animartrix(xyMap, POLAR_WAVES);
|
84
85
|
FxEngine fxEngine(NUM_LEDS);
|
@@ -119,11 +120,6 @@ void loop() {
|
|
119
120
|
animartrix.fxSet(fxIndex);
|
120
121
|
}
|
121
122
|
|
122
|
-
EVERY_N_MILLISECONDS(1000) {
|
123
|
-
printf("fastled running\r\n");
|
124
|
-
printf("Numberfield: %f\r\n", fxIndex.value());
|
125
|
-
}
|
126
|
-
|
127
123
|
|
128
124
|
fxEngine.draw(millis(), leds);
|
129
125
|
FastLED.show();
|
@@ -16,7 +16,7 @@ from dataclasses import dataclass
|
|
16
16
|
from pathlib import Path
|
17
17
|
|
18
18
|
from fastled.build_mode import BuildMode, get_build_mode
|
19
|
-
from fastled.compile_server import CompileServer
|
19
|
+
from fastled.compile_server import CompileServer, looks_like_fastled_repo
|
20
20
|
from fastled.docker_manager import DockerManager
|
21
21
|
from fastled.filewatcher import FileChangedNotifier
|
22
22
|
from fastled.open_browser import open_browser_thread
|
@@ -81,6 +81,12 @@ def parse_args() -> argparse.Namespace:
|
|
81
81
|
nargs="+",
|
82
82
|
help="Additional patterns to exclude from file watching (Not available with --web)",
|
83
83
|
)
|
84
|
+
parser.add_argument(
|
85
|
+
"-i",
|
86
|
+
"--interactive",
|
87
|
+
action="store_true",
|
88
|
+
help="Run in interactive mode (Not available with --web)",
|
89
|
+
)
|
84
90
|
parser.add_argument(
|
85
91
|
"--profile",
|
86
92
|
action="store_true",
|
@@ -97,6 +103,11 @@ def parse_args() -> argparse.Namespace:
|
|
97
103
|
build_mode.add_argument(
|
98
104
|
"--release", action="store_true", help="Build in release mode"
|
99
105
|
)
|
106
|
+
build_mode.add_argument(
|
107
|
+
"--server",
|
108
|
+
action="store_true",
|
109
|
+
help="Run the server in the current directory, volume mapping fastled if we are in the repo",
|
110
|
+
)
|
100
111
|
|
101
112
|
build_mode.add_argument(
|
102
113
|
"--force-compile",
|
@@ -200,7 +211,24 @@ def _try_start_server_or_get_url(args: argparse.Namespace) -> str | CompileServe
|
|
200
211
|
return DEFAULT_URL
|
201
212
|
|
202
213
|
|
214
|
+
def _lots_and_lots_of_files(directory: Path) -> bool:
|
215
|
+
count = 0
|
216
|
+
for root, dirs, files in os.walk(directory):
|
217
|
+
count += len(files)
|
218
|
+
if count > 100:
|
219
|
+
return True
|
220
|
+
return False
|
221
|
+
|
222
|
+
|
203
223
|
def _looks_like_sketch_directory(directory: Path) -> bool:
|
224
|
+
if looks_like_fastled_repo(directory):
|
225
|
+
print("Directory looks like the FastLED repo")
|
226
|
+
return False
|
227
|
+
|
228
|
+
if _lots_and_lots_of_files(directory):
|
229
|
+
print("Too many files in the directory, bailing out")
|
230
|
+
return False
|
231
|
+
|
204
232
|
# walk the path and if there are over 30 files, return False
|
205
233
|
# at the root of the directory there should either be an ino file or a src directory
|
206
234
|
# or some cpp files
|
@@ -217,11 +245,10 @@ def _looks_like_sketch_directory(directory: Path) -> bool:
|
|
217
245
|
return False
|
218
246
|
|
219
247
|
|
220
|
-
def
|
221
|
-
|
248
|
+
def run_client(args: argparse.Namespace) -> int:
|
249
|
+
compile_server: CompileServer | None = None
|
222
250
|
open_web_browser = not args.just_compile
|
223
251
|
profile = args.profile
|
224
|
-
|
225
252
|
if not args.force_compile and not _looks_like_sketch_directory(
|
226
253
|
Path(args.directory)
|
227
254
|
):
|
@@ -237,16 +264,16 @@ def main() -> int:
|
|
237
264
|
)
|
238
265
|
args.web = True
|
239
266
|
|
240
|
-
compile_server: CompileServer | None = None
|
241
267
|
url: str
|
242
|
-
|
243
268
|
try:
|
244
269
|
try:
|
245
270
|
url_or_server: str | CompileServer = _try_start_server_or_get_url(args)
|
246
271
|
if isinstance(url_or_server, str):
|
272
|
+
print(f"Found URL: {url_or_server}")
|
247
273
|
url = url_or_server
|
248
274
|
else:
|
249
275
|
compile_server = url_or_server
|
276
|
+
print(f"Server started at {compile_server.url()}")
|
250
277
|
url = compile_server.url()
|
251
278
|
except KeyboardInterrupt:
|
252
279
|
print("\nExiting from first try...")
|
@@ -343,9 +370,49 @@ def main() -> int:
|
|
343
370
|
browser_proc.kill()
|
344
371
|
|
345
372
|
|
373
|
+
def run_server(args: argparse.Namespace) -> int:
|
374
|
+
interactive = args.interactive
|
375
|
+
compile_server = CompileServer(
|
376
|
+
disable_auto_clean=args.no_auto_clean, interactive=interactive
|
377
|
+
)
|
378
|
+
print(f"Server started at {compile_server.url()}")
|
379
|
+
compile_server.start()
|
380
|
+
compile_server.wait_for_startup()
|
381
|
+
try:
|
382
|
+
while True:
|
383
|
+
if not compile_server.proceess_running():
|
384
|
+
print("Server process is not running. Exiting...")
|
385
|
+
return 1
|
386
|
+
time.sleep(1)
|
387
|
+
except KeyboardInterrupt:
|
388
|
+
print("\nExiting from server...")
|
389
|
+
return 1
|
390
|
+
finally:
|
391
|
+
compile_server.stop()
|
392
|
+
return 0
|
393
|
+
|
394
|
+
|
395
|
+
def main() -> int:
|
396
|
+
args = parse_args()
|
397
|
+
target_dir = Path(args.directory)
|
398
|
+
cwd_is_target_dir = target_dir == Path(os.getcwd())
|
399
|
+
force_server = cwd_is_target_dir and looks_like_fastled_repo(target_dir)
|
400
|
+
auto_server = (args.server or args.interactive or cwd_is_target_dir) and (
|
401
|
+
not args.web and not args.just_compile
|
402
|
+
)
|
403
|
+
if auto_server or force_server:
|
404
|
+
print("Running in server only mode.")
|
405
|
+
return run_server(args)
|
406
|
+
else:
|
407
|
+
print("Running in client/server mode.")
|
408
|
+
return run_client(args)
|
409
|
+
|
410
|
+
|
346
411
|
if __name__ == "__main__":
|
347
412
|
try:
|
348
413
|
sys.argv.append("examples/wasm")
|
414
|
+
sys.argv.append("-w")
|
415
|
+
sys.argv.append("localhost")
|
349
416
|
sys.exit(main())
|
350
417
|
except KeyboardInterrupt:
|
351
418
|
print("\nExiting from main...")
|
@@ -2,6 +2,7 @@ import socket
|
|
2
2
|
import subprocess
|
3
3
|
import threading
|
4
4
|
import time
|
5
|
+
from pathlib import Path
|
5
6
|
from typing import Optional
|
6
7
|
|
7
8
|
import httpx
|
@@ -10,10 +11,10 @@ from fastled.docker_manager import DockerManager
|
|
10
11
|
|
11
12
|
_DEFAULT_CONTAINER_NAME = "fastled-wasm-compiler"
|
12
13
|
|
13
|
-
|
14
|
+
SERVER_PORT = 9021
|
14
15
|
|
15
16
|
|
16
|
-
def
|
17
|
+
def find_available_port(start_port: int = SERVER_PORT) -> int:
|
17
18
|
"""Find an available port starting from the given port."""
|
18
19
|
port = start_port
|
19
20
|
end_port = start_port + 1000
|
@@ -26,16 +27,38 @@ def _find_available_port(start_port: int = _DEFAULT_START_PORT) -> int:
|
|
26
27
|
raise RuntimeError("No available ports found")
|
27
28
|
|
28
29
|
|
30
|
+
def looks_like_fastled_repo(directory: Path) -> bool:
|
31
|
+
libprops = directory / "library.properties"
|
32
|
+
if not libprops.exists():
|
33
|
+
return False
|
34
|
+
txt = libprops.read_text()
|
35
|
+
return "FastLED" in txt
|
36
|
+
|
37
|
+
|
29
38
|
class CompileServer:
|
30
39
|
def __init__(
|
31
|
-
self,
|
40
|
+
self,
|
41
|
+
container_name=_DEFAULT_CONTAINER_NAME,
|
42
|
+
disable_auto_clean: bool = False,
|
43
|
+
interactive: bool = False,
|
32
44
|
) -> None:
|
45
|
+
|
46
|
+
cwd = Path(".").resolve()
|
47
|
+
fastled_src_dir: Path | None = None
|
48
|
+
if looks_like_fastled_repo(cwd):
|
49
|
+
print(
|
50
|
+
"Looks like a FastLED repo, using it as the source directory and mapping it into the server."
|
51
|
+
)
|
52
|
+
fastled_src_dir = cwd / "src"
|
53
|
+
|
33
54
|
self.container_name = container_name
|
34
55
|
self.disable_auto_clean = disable_auto_clean
|
35
56
|
self.docker = DockerManager(container_name=container_name)
|
36
57
|
self.running = False
|
37
58
|
self.thread: Optional[threading.Thread] = None
|
38
59
|
self.running_process: subprocess.Popen | None = None
|
60
|
+
self.fastled_src_dir: Path | None = fastled_src_dir
|
61
|
+
self.interactive = interactive
|
39
62
|
self._port = self.start()
|
40
63
|
|
41
64
|
def port(self) -> int:
|
@@ -54,7 +77,9 @@ class CompileServer:
|
|
54
77
|
# use httpx to ping the server
|
55
78
|
# if successful, return True
|
56
79
|
try:
|
57
|
-
response = httpx.get(
|
80
|
+
response = httpx.get(
|
81
|
+
f"http://localhost:{self._port}", follow_redirects=True
|
82
|
+
)
|
58
83
|
if response.status_code < 400:
|
59
84
|
return True
|
60
85
|
except KeyboardInterrupt:
|
@@ -98,7 +123,7 @@ class CompileServer:
|
|
98
123
|
except Exception as e:
|
99
124
|
print(f"Warning: Failed to remove existing container: {e}")
|
100
125
|
|
101
|
-
print("Ensuring Docker image exists")
|
126
|
+
print("Ensuring Docker image exists at latest version")
|
102
127
|
if not self.docker.ensure_image_exists():
|
103
128
|
print("Failed to ensure Docker image exists.")
|
104
129
|
raise RuntimeError("Failed to ensure Docker image exists")
|
@@ -109,14 +134,31 @@ class CompileServer:
|
|
109
134
|
# subprocess.run(["docker", "rmi", "fastled-wasm"], capture_output=True)
|
110
135
|
# print("All clean")
|
111
136
|
|
112
|
-
port =
|
137
|
+
port = find_available_port()
|
138
|
+
print(f"Found an available port: {port}")
|
113
139
|
# server_command = ["python", "/js/run.py", "server", "--allow-shutdown"]
|
114
|
-
|
140
|
+
if self.interactive:
|
141
|
+
server_command = ["/bin/bash"]
|
142
|
+
else:
|
143
|
+
server_command = ["python", "/js/run.py", "server"]
|
115
144
|
if self.disable_auto_clean:
|
116
145
|
server_command.append("--disable-auto-clean")
|
117
146
|
print(f"Started Docker container with command: {server_command}")
|
118
147
|
ports = {port: 80}
|
119
|
-
|
148
|
+
volumes = None
|
149
|
+
if self.fastled_src_dir:
|
150
|
+
print(
|
151
|
+
f"Mounting FastLED source directory {self.fastled_src_dir} into container /js/fastled/src"
|
152
|
+
)
|
153
|
+
volumes = {
|
154
|
+
str(self.fastled_src_dir): {"bind": "/js/fastled/src", "mode": "rw"}
|
155
|
+
}
|
156
|
+
# no auto-update because the source directory is mapped in.
|
157
|
+
server_command.append("--no-auto-update")
|
158
|
+
self.running_process = self.docker.run_container(
|
159
|
+
server_command, ports=ports, volumes=volumes
|
160
|
+
)
|
161
|
+
print("Compile server starting")
|
120
162
|
time.sleep(3)
|
121
163
|
if self.running_process.poll() is not None:
|
122
164
|
print("Server failed to start")
|
@@ -124,38 +166,47 @@ class CompileServer:
|
|
124
166
|
raise RuntimeError("Server failed to start")
|
125
167
|
self.thread = threading.Thread(target=self._server_loop, daemon=True)
|
126
168
|
self.thread.start()
|
127
|
-
|
169
|
+
|
128
170
|
return port
|
129
171
|
|
172
|
+
def proceess_running(self) -> bool:
|
173
|
+
if self.running_process is None:
|
174
|
+
return False
|
175
|
+
return self.running_process.poll() is None
|
176
|
+
|
130
177
|
def stop(self) -> None:
|
131
178
|
print(f"Stopping server on port {self._port}")
|
132
179
|
# attempt to send a shutdown signal to the server
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
180
|
+
try:
|
181
|
+
httpx.get(f"http://localhost:{self._port}/shutdown", timeout=2)
|
182
|
+
# except Exception:
|
183
|
+
except Exception as e:
|
184
|
+
print(f"Failed to send shutdown signal: {e}")
|
185
|
+
pass
|
186
|
+
try:
|
187
|
+
# Stop the Docker container
|
188
|
+
cp: subprocess.CompletedProcess
|
189
|
+
cp = subprocess.run(
|
190
|
+
["docker", "stop", self.container_name],
|
191
|
+
capture_output=True,
|
192
|
+
text=True,
|
193
|
+
check=True,
|
194
|
+
)
|
195
|
+
if cp.returncode != 0:
|
196
|
+
print(f"Failed to stop Docker container: {cp.stderr}")
|
197
|
+
|
198
|
+
cp = subprocess.run(
|
199
|
+
["docker", "rm", self.container_name],
|
200
|
+
capture_output=True,
|
201
|
+
text=True,
|
202
|
+
check=True,
|
203
|
+
)
|
204
|
+
if cp.returncode != 0:
|
205
|
+
print(f"Failed to remove Docker container: {cp.stderr}")
|
155
206
|
|
156
|
-
|
157
|
-
|
158
|
-
|
207
|
+
# Close the stdout pipe
|
208
|
+
if self.running_process and self.running_process.stdout:
|
209
|
+
self.running_process.stdout.close()
|
159
210
|
|
160
211
|
# Wait for the process to fully terminate with a timeout
|
161
212
|
self.running_process.wait(timeout=10)
|
@@ -167,17 +218,19 @@ class CompileServer:
|
|
167
218
|
f"Server stopped with return code {self.running_process.returncode}"
|
168
219
|
)
|
169
220
|
|
170
|
-
|
171
|
-
|
221
|
+
except subprocess.TimeoutExpired:
|
222
|
+
# Force kill if it doesn't stop gracefully
|
223
|
+
if self.running_process:
|
172
224
|
self.running_process.kill()
|
173
225
|
self.running_process.wait()
|
174
|
-
|
226
|
+
except KeyboardInterrupt:
|
227
|
+
if self.running_process:
|
175
228
|
self.running_process.kill()
|
176
229
|
self.running_process.wait()
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
230
|
+
except Exception as e:
|
231
|
+
print(f"Error stopping Docker container: {e}")
|
232
|
+
finally:
|
233
|
+
self.running_process = None
|
181
234
|
# Signal the server thread to stop
|
182
235
|
self.running = False
|
183
236
|
if self.thread:
|
@@ -210,15 +210,15 @@ class DockerManager:
|
|
210
210
|
def run_container(
|
211
211
|
self,
|
212
212
|
cmd: list[str],
|
213
|
-
volumes: dict[str, str] | None = None,
|
213
|
+
volumes: dict[str, dict[str, str]] | None = None,
|
214
214
|
ports: dict[int, int] | None = None,
|
215
215
|
) -> subprocess.Popen:
|
216
216
|
"""Run the Docker container with the specified volume.
|
217
217
|
|
218
218
|
Args:
|
219
|
-
|
220
|
-
|
221
|
-
|
219
|
+
cmd: Command to run in the container
|
220
|
+
volumes: Dict mapping host paths to dicts with 'bind' and 'mode' keys
|
221
|
+
ports: Dict mapping host ports to container ports
|
222
222
|
"""
|
223
223
|
volumes = volumes or {}
|
224
224
|
ports = ports or {}
|
@@ -237,8 +237,10 @@ class DockerManager:
|
|
237
237
|
for host_port, container_port in ports.items():
|
238
238
|
docker_command.extend(["-p", f"{host_port}:{container_port}"])
|
239
239
|
if volumes:
|
240
|
-
for host_path,
|
241
|
-
docker_command.extend(
|
240
|
+
for host_path, mount_spec in volumes.items():
|
241
|
+
docker_command.extend(
|
242
|
+
["-v", f"{host_path}:{mount_spec['bind']}:{mount_spec['mode']}"]
|
243
|
+
)
|
242
244
|
|
243
245
|
docker_command.extend(
|
244
246
|
[
|
@@ -1,173 +1,220 @@
|
|
1
|
-
import shutil
|
2
|
-
import tempfile
|
3
|
-
from
|
4
|
-
from
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
return
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
1
|
+
import shutil
|
2
|
+
import tempfile
|
3
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
4
|
+
from dataclasses import dataclass
|
5
|
+
from pathlib import Path
|
6
|
+
|
7
|
+
import httpx
|
8
|
+
|
9
|
+
from fastled.build_mode import BuildMode
|
10
|
+
from fastled.compile_server import SERVER_PORT
|
11
|
+
|
12
|
+
DEFAULT_HOST = "https://fastled.onrender.com"
|
13
|
+
ENDPOINT_COMPILED_WASM = "compile/wasm"
|
14
|
+
_TIMEOUT = 60 * 4 # 2 mins timeout
|
15
|
+
_AUTH_TOKEN = "oBOT5jbsO4ztgrpNsQwlmFLIKB"
|
16
|
+
|
17
|
+
|
18
|
+
@dataclass
|
19
|
+
class TestConnectionResult:
|
20
|
+
host: str
|
21
|
+
success: bool
|
22
|
+
ipv4: bool
|
23
|
+
|
24
|
+
|
25
|
+
@dataclass
|
26
|
+
class WebCompileResult:
|
27
|
+
success: bool
|
28
|
+
stdout: str
|
29
|
+
hash_value: str | None
|
30
|
+
zip_bytes: bytes
|
31
|
+
|
32
|
+
def __bool__(self) -> bool:
|
33
|
+
return self.success
|
34
|
+
|
35
|
+
|
36
|
+
def _sanitize_host(host: str) -> str:
|
37
|
+
if host.startswith("http"):
|
38
|
+
return host
|
39
|
+
is_local_host = "localhost" in host or "127.0.0.1" in host or "0.0.0.0" in host
|
40
|
+
use_https = not is_local_host
|
41
|
+
if use_https:
|
42
|
+
return host if host.startswith("https://") else f"https://{host}"
|
43
|
+
return host if host.startswith("http://") else f"http://{host}"
|
44
|
+
|
45
|
+
|
46
|
+
_CONNECTION_ERROR_MAP: dict[str, TestConnectionResult] = {}
|
47
|
+
|
48
|
+
|
49
|
+
def _test_connection(host: str, use_ipv4: bool) -> TestConnectionResult:
|
50
|
+
key = f"{host}-{use_ipv4}"
|
51
|
+
maybe_result: TestConnectionResult | None = _CONNECTION_ERROR_MAP.get(key)
|
52
|
+
if maybe_result is not None:
|
53
|
+
return maybe_result
|
54
|
+
transport = httpx.HTTPTransport(local_address="0.0.0.0") if use_ipv4 else None
|
55
|
+
try:
|
56
|
+
with httpx.Client(
|
57
|
+
timeout=_TIMEOUT,
|
58
|
+
transport=transport,
|
59
|
+
) as test_client:
|
60
|
+
test_response = test_client.get(
|
61
|
+
f"{host}/healthz", timeout=_TIMEOUT, follow_redirects=True
|
62
|
+
)
|
63
|
+
result = TestConnectionResult(
|
64
|
+
host, test_response.status_code == 200, use_ipv4
|
65
|
+
)
|
66
|
+
_CONNECTION_ERROR_MAP[key] = result
|
67
|
+
except Exception:
|
68
|
+
result = TestConnectionResult(host, False, use_ipv4)
|
69
|
+
_CONNECTION_ERROR_MAP[key] = result
|
70
|
+
return result
|
71
|
+
|
72
|
+
|
73
|
+
def web_compile(
|
74
|
+
directory: Path,
|
75
|
+
host: str | None = None,
|
76
|
+
auth_token: str | None = None,
|
77
|
+
build_mode: BuildMode | None = None,
|
78
|
+
profile: bool = False,
|
79
|
+
) -> WebCompileResult:
|
80
|
+
host = _sanitize_host(host or DEFAULT_HOST)
|
81
|
+
print("Compiling on", host)
|
82
|
+
auth_token = auth_token or _AUTH_TOKEN
|
83
|
+
# zip up the files
|
84
|
+
print("Zipping files...")
|
85
|
+
|
86
|
+
# Create a temporary zip file
|
87
|
+
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp_zip:
|
88
|
+
# Create temporary directory for organizing files
|
89
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
90
|
+
# Create wasm subdirectory
|
91
|
+
wasm_dir = Path(temp_dir) / "wasm"
|
92
|
+
|
93
|
+
# Copy all files from source to wasm subdirectory, excluding fastled_js
|
94
|
+
def ignore_fastled_js(dir: str, files: list[str]) -> list[str]:
|
95
|
+
if "fastled_js" in dir:
|
96
|
+
return files
|
97
|
+
if dir.startswith("."):
|
98
|
+
return files
|
99
|
+
return []
|
100
|
+
|
101
|
+
shutil.copytree(directory, wasm_dir, ignore=ignore_fastled_js)
|
102
|
+
# Create zip archive from the temp directory
|
103
|
+
shutil.make_archive(tmp_zip.name[:-4], "zip", temp_dir)
|
104
|
+
|
105
|
+
print(f"Web compiling on {host}...")
|
106
|
+
|
107
|
+
try:
|
108
|
+
with open(tmp_zip.name, "rb") as zip_file:
|
109
|
+
files = {"file": ("wasm.zip", zip_file, "application/x-zip-compressed")}
|
110
|
+
urls = [host]
|
111
|
+
domain = host.split("://")[-1]
|
112
|
+
if ":" not in domain:
|
113
|
+
urls.append(f"{host}:{SERVER_PORT}")
|
114
|
+
test_connection_result: TestConnectionResult | None = None
|
115
|
+
|
116
|
+
with ThreadPoolExecutor(max_workers=len(urls)) as executor:
|
117
|
+
futures: list = []
|
118
|
+
ip_versions = [True, False] if "localhost" not in host else [True]
|
119
|
+
for ipv4 in ip_versions:
|
120
|
+
for url in urls:
|
121
|
+
f = executor.submit(_test_connection, url, ipv4)
|
122
|
+
futures.append(f)
|
123
|
+
|
124
|
+
succeeded = False
|
125
|
+
for future in as_completed(futures):
|
126
|
+
result: TestConnectionResult = future.result()
|
127
|
+
|
128
|
+
if result.success:
|
129
|
+
print(f"Connection successful to {result.host}")
|
130
|
+
succeeded = True
|
131
|
+
# host = test_url
|
132
|
+
test_connection_result = result
|
133
|
+
break
|
134
|
+
else:
|
135
|
+
print(f"Ignoring {result.host} due to connection failure")
|
136
|
+
|
137
|
+
if not succeeded:
|
138
|
+
print("Connection failed to all endpoints")
|
139
|
+
return WebCompileResult(
|
140
|
+
success=False,
|
141
|
+
stdout="Connection failed",
|
142
|
+
hash_value=None,
|
143
|
+
zip_bytes=b"",
|
144
|
+
)
|
145
|
+
assert test_connection_result is not None
|
146
|
+
ipv4_stmt = "IPv4" if test_connection_result.ipv4 else "IPv6"
|
147
|
+
transport = (
|
148
|
+
httpx.HTTPTransport(local_address="0.0.0.0")
|
149
|
+
if test_connection_result.ipv4
|
150
|
+
else None
|
151
|
+
)
|
152
|
+
with httpx.Client(
|
153
|
+
transport=transport,
|
154
|
+
timeout=_TIMEOUT,
|
155
|
+
) as client:
|
156
|
+
headers = {
|
157
|
+
"accept": "application/json",
|
158
|
+
"authorization": auth_token,
|
159
|
+
"build": (
|
160
|
+
build_mode.value.lower()
|
161
|
+
if build_mode
|
162
|
+
else BuildMode.QUICK.value.lower()
|
163
|
+
),
|
164
|
+
"profile": "true" if profile else "false",
|
165
|
+
}
|
166
|
+
|
167
|
+
url = f"{test_connection_result.host}/{ENDPOINT_COMPILED_WASM}"
|
168
|
+
print(f"Compiling on {url} via {ipv4_stmt}")
|
169
|
+
response = client.post(
|
170
|
+
url,
|
171
|
+
follow_redirects=True,
|
172
|
+
files=files,
|
173
|
+
headers=headers,
|
174
|
+
timeout=_TIMEOUT,
|
175
|
+
)
|
176
|
+
|
177
|
+
if response.status_code != 200:
|
178
|
+
json_response = response.json()
|
179
|
+
detail = json_response.get("detail", "Could not compile")
|
180
|
+
return WebCompileResult(
|
181
|
+
success=False, stdout=detail, hash_value=None, zip_bytes=b""
|
182
|
+
)
|
183
|
+
|
184
|
+
print(f"Response status code: {response}")
|
185
|
+
# Create a temporary directory to extract the zip
|
186
|
+
with tempfile.TemporaryDirectory() as extract_dir:
|
187
|
+
extract_path = Path(extract_dir)
|
188
|
+
|
189
|
+
# Write the response content to a temporary zip file
|
190
|
+
temp_zip = extract_path / "response.zip"
|
191
|
+
temp_zip.write_bytes(response.content)
|
192
|
+
|
193
|
+
# Extract the zip
|
194
|
+
shutil.unpack_archive(temp_zip, extract_path, "zip")
|
195
|
+
|
196
|
+
# Read stdout from out.txt if it exists
|
197
|
+
stdout_file = extract_path / "out.txt"
|
198
|
+
hash_file = extract_path / "hash.txt"
|
199
|
+
stdout = stdout_file.read_text() if stdout_file.exists() else ""
|
200
|
+
hash_value = hash_file.read_text() if hash_file.exists() else None
|
201
|
+
|
202
|
+
return WebCompileResult(
|
203
|
+
success=True,
|
204
|
+
stdout=stdout,
|
205
|
+
hash_value=hash_value,
|
206
|
+
zip_bytes=response.content,
|
207
|
+
)
|
208
|
+
except KeyboardInterrupt:
|
209
|
+
print("Keyboard interrupt")
|
210
|
+
raise
|
211
|
+
except httpx.HTTPError as e:
|
212
|
+
print(f"Error: {e}")
|
213
|
+
return WebCompileResult(
|
214
|
+
success=False, stdout=str(e), hash_value=None, zip_bytes=b""
|
215
|
+
)
|
216
|
+
finally:
|
217
|
+
try:
|
218
|
+
Path(tmp_zip.name).unlink()
|
219
|
+
except PermissionError:
|
220
|
+
print("Warning: Could not delete temporary zip file")
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: fastled
|
3
|
-
Version: 1.1.
|
3
|
+
Version: 1.1.2
|
4
4
|
Summary: FastLED Wasm Compiler
|
5
5
|
Home-page: https://github.com/zackees/fastled-wasm
|
6
6
|
Maintainer: Zachary Vorhies
|
@@ -28,7 +28,6 @@ Compiles an Arduino/Platformio sketch into a wasm binary that can be run directl
|
|
28
28
|
[](https://github.com/zackees/fastled-wasm/actions/workflows/test_win.yml)
|
29
29
|
|
30
30
|
|
31
|
-
|
32
31
|
# About
|
33
32
|
|
34
33
|
This python app will compile your FastLED style sketches into html/js/wasm output that runs directly in the browser.
|
@@ -92,6 +91,8 @@ provide shims for most of the common api points.
|
|
92
91
|
|
93
92
|
# Revisions
|
94
93
|
|
94
|
+
* 1.1.2 - `--server` will now volume map fastled src directory if it detects this. This was also implemented on the docker side.
|
95
|
+
* 1.1.1 - `--interactive` is now supported to debug the container. Volume maps and better compatibilty with ipv4/v6 by concurrent connection finding.
|
95
96
|
* 1.1.0 - Use `fastled` as the command for the wasm compiler.
|
96
97
|
* 1.0.17 - Pulls updates when necessary. Removed dependency on keyring.
|
97
98
|
* 1.0.16 - `fastled-wasm` package name has been changed to `fled`
|
@@ -36,7 +36,7 @@ class WebCompilerTester(unittest.TestCase):
|
|
36
36
|
# Verify server stopped
|
37
37
|
self.assertFalse(server.running, "Server did not stop")
|
38
38
|
self.assertIsNone(server.running_process, "Server process not cleared")
|
39
|
-
self.assertTrue(result.success, "Compilation failed")
|
39
|
+
self.assertTrue(result.success, f"Compilation failed: {result.stdout}")
|
40
40
|
|
41
41
|
|
42
42
|
if __name__ == "__main__":
|
@@ -23,7 +23,7 @@ class WebCompileTester(unittest.TestCase):
|
|
23
23
|
print(f"Time taken: {diff:.2f} seconds")
|
24
24
|
|
25
25
|
# Verify we got a successful result
|
26
|
-
self.assertTrue(result.success)
|
26
|
+
self.assertTrue(result.success, f"Compilation failed: {result.stdout}")
|
27
27
|
|
28
28
|
# Verify we got actual WASM data back
|
29
29
|
self.assertTrue(len(result.zip_bytes) > 0)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{fastled-1.1.0 → fastled-1.1.2}/examples/Chromancer/gary_woos_wled_port/gary_woos_wled_ledmap.h
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|