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.
Files changed (89) hide show
  1. {fastled-1.1.0 → fastled-1.1.2}/PKG-INFO +3 -2
  2. {fastled-1.1.0 → fastled-1.1.2}/README.md +2 -1
  3. {fastled-1.1.0 → fastled-1.1.2}/examples/wasm/wasm.ino +5 -9
  4. {fastled-1.1.0 → fastled-1.1.2}/pyproject.toml +1 -1
  5. {fastled-1.1.0 → fastled-1.1.2}/src/fastled/app.py +73 -6
  6. {fastled-1.1.0 → fastled-1.1.2}/src/fastled/compile_server.py +94 -41
  7. {fastled-1.1.0 → fastled-1.1.2}/src/fastled/docker_manager.py +8 -6
  8. {fastled-1.1.0 → fastled-1.1.2}/src/fastled/web_compile.py +220 -173
  9. {fastled-1.1.0 → fastled-1.1.2}/src/fastled.egg-info/PKG-INFO +3 -2
  10. {fastled-1.1.0 → fastled-1.1.2}/test +1 -1
  11. {fastled-1.1.0 → fastled-1.1.2}/tests/test_compile_server.py +1 -1
  12. {fastled-1.1.0 → fastled-1.1.2}/tests/test_webcompile.py +1 -1
  13. {fastled-1.1.0 → fastled-1.1.2}/.aiderignore +0 -0
  14. {fastled-1.1.0 → fastled-1.1.2}/.github/workflows/build_multi_docker_image.yml +0 -0
  15. {fastled-1.1.0 → fastled-1.1.2}/.github/workflows/lint.yml +0 -0
  16. {fastled-1.1.0 → fastled-1.1.2}/.github/workflows/test_macos.yml +0 -0
  17. {fastled-1.1.0 → fastled-1.1.2}/.github/workflows/test_ubuntu.yml +0 -0
  18. {fastled-1.1.0 → fastled-1.1.2}/.github/workflows/test_win.yml +0 -0
  19. {fastled-1.1.0 → fastled-1.1.2}/.gitignore +0 -0
  20. {fastled-1.1.0 → fastled-1.1.2}/.pylintrc +0 -0
  21. {fastled-1.1.0 → fastled-1.1.2}/.vscode/launch.json +0 -0
  22. {fastled-1.1.0 → fastled-1.1.2}/.vscode/settings.json +0 -0
  23. {fastled-1.1.0 → fastled-1.1.2}/.vscode/tasks.json +0 -0
  24. {fastled-1.1.0 → fastled-1.1.2}/LICENSE +0 -0
  25. {fastled-1.1.0 → fastled-1.1.2}/MANIFEST.in +0 -0
  26. {fastled-1.1.0 → fastled-1.1.2}/clean +0 -0
  27. {fastled-1.1.0 → fastled-1.1.2}/docs/fastled.js +0 -0
  28. {fastled-1.1.0 → fastled-1.1.2}/docs/fastled.wasm +0 -0
  29. {fastled-1.1.0 → fastled-1.1.2}/docs/index.css +0 -0
  30. {fastled-1.1.0 → fastled-1.1.2}/docs/index.html +0 -0
  31. {fastled-1.1.0 → fastled-1.1.2}/docs/index.js +0 -0
  32. {fastled-1.1.0 → fastled-1.1.2}/examples/Blink/Blink.ino +0 -0
  33. {fastled-1.1.0 → fastled-1.1.2}/examples/Chromancer/Chromancer.ino +0 -0
  34. {fastled-1.1.0 → fastled-1.1.2}/examples/Chromancer/detail.h +0 -0
  35. {fastled-1.1.0 → fastled-1.1.2}/examples/Chromancer/gary_woos_wled_port/gary_woos_wled_ledmap.h +0 -0
  36. {fastled-1.1.0 → fastled-1.1.2}/examples/Chromancer/gary_woos_wled_port/presets.json +0 -0
  37. {fastled-1.1.0 → fastled-1.1.2}/examples/Chromancer/gary_woos_wled_port/presets.min.json +0 -0
  38. {fastled-1.1.0 → fastled-1.1.2}/examples/Chromancer/gen.py +0 -0
  39. {fastled-1.1.0 → fastled-1.1.2}/examples/Chromancer/mapping.h +0 -0
  40. {fastled-1.1.0 → fastled-1.1.2}/examples/Chromancer/net.h +0 -0
  41. {fastled-1.1.0 → fastled-1.1.2}/examples/Chromancer/output.json +0 -0
  42. {fastled-1.1.0 → fastled-1.1.2}/examples/Chromancer/ripple.h +0 -0
  43. {fastled-1.1.0 → fastled-1.1.2}/examples/Chromancer/screenmap.json.h +0 -0
  44. {fastled-1.1.0 → fastled-1.1.2}/examples/ColorPalette/ColorPalette.ino +0 -0
  45. {fastled-1.1.0 → fastled-1.1.2}/examples/ColorTemperature/ColorTemperature.ino +0 -0
  46. {fastled-1.1.0 → fastled-1.1.2}/examples/Cylon/Cylon.ino +0 -0
  47. {fastled-1.1.0 → fastled-1.1.2}/examples/DemoReel100/DemoReel100.ino +0 -0
  48. {fastled-1.1.0 → fastled-1.1.2}/examples/Esp32Rmt51/Esp32Rmt51.ino +0 -0
  49. {fastled-1.1.0 → fastled-1.1.2}/examples/EspI2SDemo/EspI2SDemo.ino +0 -0
  50. {fastled-1.1.0 → fastled-1.1.2}/examples/Fire2012/Fire2012.ino +0 -0
  51. {fastled-1.1.0 → fastled-1.1.2}/examples/Fire2012WithPalette/Fire2012WithPalette.ino +0 -0
  52. {fastled-1.1.0 → fastled-1.1.2}/examples/FirstLight/FirstLight.ino +0 -0
  53. {fastled-1.1.0 → fastled-1.1.2}/examples/FxEngine/FxEngine.ino +0 -0
  54. {fastled-1.1.0 → fastled-1.1.2}/examples/Noise/Noise.ino +0 -0
  55. {fastled-1.1.0 → fastled-1.1.2}/examples/NoisePlayground/NoisePlayground.ino +0 -0
  56. {fastled-1.1.0 → fastled-1.1.2}/examples/NoisePlusPalette/NoisePlusPalette.ino +0 -0
  57. {fastled-1.1.0 → fastled-1.1.2}/examples/OctoWS2811/OctoWS2811.ino +0 -0
  58. {fastled-1.1.0 → fastled-1.1.2}/examples/Pacifica/Pacifica.ino +0 -0
  59. {fastled-1.1.0 → fastled-1.1.2}/examples/Pride2015/Pride2015.ino +0 -0
  60. {fastled-1.1.0 → fastled-1.1.2}/examples/SdCard/SdCard.ino +0 -0
  61. {fastled-1.1.0 → fastled-1.1.2}/examples/TwinkleFox/TwinkleFox.ino +0 -0
  62. {fastled-1.1.0 → fastled-1.1.2}/examples/Video/Gfx2Video/Gfx2Video.ino +0 -0
  63. {fastled-1.1.0 → fastled-1.1.2}/examples/WasmScreenCoords/WasmScreenCoords.ino +0 -0
  64. {fastled-1.1.0 → fastled-1.1.2}/examples/Water/Water.ino +0 -0
  65. {fastled-1.1.0 → fastled-1.1.2}/examples/XYMatrix/XYMatrix.ino +0 -0
  66. {fastled-1.1.0 → fastled-1.1.2}/install +0 -0
  67. {fastled-1.1.0 → fastled-1.1.2}/lint +0 -0
  68. {fastled-1.1.0 → fastled-1.1.2}/requirements.testing.txt +0 -0
  69. {fastled-1.1.0 → fastled-1.1.2}/setup.cfg +0 -0
  70. {fastled-1.1.0 → fastled-1.1.2}/setup.py +0 -0
  71. {fastled-1.1.0 → fastled-1.1.2}/src/fastled/__init__.py +0 -0
  72. {fastled-1.1.0 → fastled-1.1.2}/src/fastled/assets/example.txt +0 -0
  73. {fastled-1.1.0 → fastled-1.1.2}/src/fastled/build_mode.py +0 -0
  74. {fastled-1.1.0 → fastled-1.1.2}/src/fastled/check_cpp_syntax.py +0 -0
  75. {fastled-1.1.0 → fastled-1.1.2}/src/fastled/cli.py +0 -0
  76. {fastled-1.1.0 → fastled-1.1.2}/src/fastled/filewatcher.py +0 -0
  77. {fastled-1.1.0 → fastled-1.1.2}/src/fastled/open_browser.py +0 -0
  78. {fastled-1.1.0 → fastled-1.1.2}/src/fastled/paths.py +0 -0
  79. {fastled-1.1.0 → fastled-1.1.2}/src/fastled.egg-info/SOURCES.txt +0 -0
  80. {fastled-1.1.0 → fastled-1.1.2}/src/fastled.egg-info/dependency_links.txt +0 -0
  81. {fastled-1.1.0 → fastled-1.1.2}/src/fastled.egg-info/entry_points.txt +0 -0
  82. {fastled-1.1.0 → fastled-1.1.2}/src/fastled.egg-info/requires.txt +0 -0
  83. {fastled-1.1.0 → fastled-1.1.2}/src/fastled.egg-info/top_level.txt +0 -0
  84. {fastled-1.1.0 → fastled-1.1.2}/tests/test_bad_ino.py +0 -0
  85. {fastled-1.1.0 → fastled-1.1.2}/tests/test_cli.py +0 -0
  86. {fastled-1.1.0 → fastled-1.1.2}/tests/test_filechanger.py +0 -0
  87. {fastled-1.1.0 → fastled-1.1.2}/tests/test_ino/bad/bad.ino +0 -0
  88. {fastled-1.1.0 → fastled-1.1.2}/tests/test_ino/wasm/wasm.ino +0 -0
  89. {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.0
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
  [![Win_Tests](https://github.com/zackees/fastled-wasm/actions/workflows/test_win.yml/badge.svg)](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
  [![Win_Tests](https://github.com/zackees/fastled-wasm/actions/workflows/test_win.yml/badge.svg)](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 - index", 0, 0, NUM_ANIMATIONS);
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();
@@ -19,7 +19,7 @@ dependencies = [
19
19
  "filelock",
20
20
  ]
21
21
  # Change this with the version number bump.
22
- version = "1.1.0"
22
+ version = "1.1.2"
23
23
 
24
24
  [tool.setuptools]
25
25
  package-dir = {"" = "src"}
@@ -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 main() -> int:
221
- args = parse_args()
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
- _DEFAULT_START_PORT = 9021
14
+ SERVER_PORT = 9021
14
15
 
15
16
 
16
- def _find_available_port(start_port: int = _DEFAULT_START_PORT) -> int:
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, container_name=_DEFAULT_CONTAINER_NAME, disable_auto_clean: bool = False
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(f"http://localhost:{self._port}")
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 = _find_available_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
- server_command = ["python", "/js/run.py", "server"]
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
- self.running_process = self.docker.run_container(server_command, ports=ports)
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
- print("Compile server started")
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
- # httpx.get(f"http://localhost:{self._port}/shutdown", timeout=2)
134
- if self.running_process:
135
- try:
136
- # Stop the Docker container
137
- cp: subprocess.CompletedProcess
138
- cp = subprocess.run(
139
- ["docker", "stop", self.container_name],
140
- capture_output=True,
141
- text=True,
142
- check=True,
143
- )
144
- if cp.returncode != 0:
145
- print(f"Failed to stop Docker container: {cp.stderr}")
146
-
147
- cp = subprocess.run(
148
- ["docker", "rm", self.container_name],
149
- capture_output=True,
150
- text=True,
151
- check=True,
152
- )
153
- if cp.returncode != 0:
154
- print(f"Failed to remove Docker container: {cp.stderr}")
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
- # Close the stdout pipe
157
- if self.running_process.stdout:
158
- self.running_process.stdout.close()
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
- except subprocess.TimeoutExpired:
171
- # Force kill if it doesn't stop gracefully
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
- except KeyboardInterrupt:
226
+ except KeyboardInterrupt:
227
+ if self.running_process:
175
228
  self.running_process.kill()
176
229
  self.running_process.wait()
177
- except Exception as e:
178
- print(f"Error stopping Docker container: {e}")
179
- finally:
180
- self.running_process = None
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
- volume_path: Path to the volume to mount
220
- base_name: Base name for the mounted volume
221
- build_mode: Build mode (DEBUG, QUICK, or RELEASE)
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, container_path in volumes.items():
241
- docker_command.extend(["-v", f"{host_path}:{container_path}"])
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 dataclasses import dataclass
4
- from pathlib import Path
5
-
6
- import httpx
7
-
8
- from fastled.build_mode import BuildMode
9
-
10
- DEFAULT_HOST = "https://fastled.onrender.com"
11
- ENDPOINT_COMPILED_WASM = "compile/wasm"
12
- _TIMEOUT = 60 * 4 # 2 mins timeout
13
- _AUTH_TOKEN = "oBOT5jbsO4ztgrpNsQwlmFLIKB"
14
-
15
-
16
- @dataclass
17
- class WebCompileResult:
18
- success: bool
19
- stdout: str
20
- hash_value: str | None
21
- zip_bytes: bytes
22
-
23
- def __bool__(self) -> bool:
24
- return self.success
25
-
26
-
27
- def _sanitize_host(host: str) -> str:
28
- if host.startswith("http"):
29
- return host
30
- is_local_host = "localhost" in host or "127.0.0.1" in host or "0.0.0.0" in host
31
- use_https = not is_local_host
32
- if use_https:
33
- return host if host.startswith("https://") else f"https://{host}"
34
- return host if host.startswith("http://") else f"http://{host}"
35
-
36
-
37
- _CONNECTION_ERROR_MAP: dict[str, bool] = {}
38
-
39
-
40
- def web_compile(
41
- directory: Path,
42
- host: str | None = None,
43
- auth_token: str | None = None,
44
- build_mode: BuildMode | None = None,
45
- profile: bool = False,
46
- ) -> WebCompileResult:
47
- host = _sanitize_host(host or DEFAULT_HOST)
48
- auth_token = auth_token or _AUTH_TOKEN
49
- # zip up the files
50
- print("Zipping files...")
51
-
52
- # Create a temporary zip file
53
- with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp_zip:
54
- # Create temporary directory for organizing files
55
- with tempfile.TemporaryDirectory() as temp_dir:
56
- # Create wasm subdirectory
57
- wasm_dir = Path(temp_dir) / "wasm"
58
-
59
- # Copy all files from source to wasm subdirectory, excluding fastled_js
60
- def ignore_fastled_js(dir: str, files: list[str]) -> list[str]:
61
- if "fastled_js" in dir:
62
- return files
63
- if dir.startswith("."):
64
- return files
65
- return []
66
-
67
- shutil.copytree(directory, wasm_dir, ignore=ignore_fastled_js)
68
- # Create zip archive from the temp directory
69
- shutil.make_archive(tmp_zip.name[:-4], "zip", temp_dir)
70
-
71
- print(f"Web compiling on {host}...")
72
-
73
- try:
74
- with open(tmp_zip.name, "rb") as zip_file:
75
- files = {"file": ("wasm.zip", zip_file, "application/x-zip-compressed")}
76
-
77
- tested = host in _CONNECTION_ERROR_MAP
78
- if not tested:
79
- test_url = f"{host}/healthz"
80
- print(f"Testing connection to {test_url}")
81
- timeout = 10
82
- with httpx.Client(
83
- transport=httpx.HTTPTransport(local_address="0.0.0.0"),
84
- timeout=timeout,
85
- ) as test_client:
86
- test_response = test_client.get(test_url, timeout=timeout)
87
- if test_response.status_code != 200:
88
- print(f"Connection to {test_url} failed")
89
- _CONNECTION_ERROR_MAP[host] = True
90
- return WebCompileResult(
91
- success=False,
92
- stdout="Connection failed",
93
- hash_value=None,
94
- zip_bytes=b"",
95
- )
96
- _CONNECTION_ERROR_MAP[host] = False
97
-
98
- ok = not _CONNECTION_ERROR_MAP[host]
99
- if not ok:
100
- return WebCompileResult(
101
- success=False,
102
- stdout="Connection failed",
103
- hash_value=None,
104
- zip_bytes=b"",
105
- )
106
- print(f"Connection to {host} successful")
107
- with httpx.Client(
108
- transport=httpx.HTTPTransport(local_address="0.0.0.0"), # forces IPv4
109
- timeout=_TIMEOUT,
110
- ) as client:
111
- url = f"{host}/{ENDPOINT_COMPILED_WASM}"
112
- headers = {
113
- "accept": "application/json",
114
- "authorization": auth_token,
115
- "build": (
116
- build_mode.value.lower()
117
- if build_mode
118
- else BuildMode.QUICK.value.lower()
119
- ),
120
- "profile": "true" if profile else "false",
121
- }
122
- print(f"Compiling on {url}")
123
- response = client.post(
124
- url,
125
- files=files,
126
- headers=headers,
127
- timeout=_TIMEOUT,
128
- )
129
-
130
- if response.status_code != 200:
131
- json_response = response.json()
132
- detail = json_response.get("detail", "Could not compile")
133
- return WebCompileResult(
134
- success=False, stdout=detail, hash_value=None, zip_bytes=b""
135
- )
136
-
137
- print(f"Response status code: {response}")
138
- # Create a temporary directory to extract the zip
139
- with tempfile.TemporaryDirectory() as extract_dir:
140
- extract_path = Path(extract_dir)
141
-
142
- # Write the response content to a temporary zip file
143
- temp_zip = extract_path / "response.zip"
144
- temp_zip.write_bytes(response.content)
145
-
146
- # Extract the zip
147
- shutil.unpack_archive(temp_zip, extract_path, "zip")
148
-
149
- # Read stdout from out.txt if it exists
150
- stdout_file = extract_path / "out.txt"
151
- hash_file = extract_path / "hash.txt"
152
- stdout = stdout_file.read_text() if stdout_file.exists() else ""
153
- hash_value = hash_file.read_text() if hash_file.exists() else None
154
-
155
- return WebCompileResult(
156
- success=True,
157
- stdout=stdout,
158
- hash_value=hash_value,
159
- zip_bytes=response.content,
160
- )
161
- except KeyboardInterrupt:
162
- print("Keyboard interrupt")
163
- raise
164
- except httpx.HTTPError as e:
165
- print(f"Error: {e}")
166
- return WebCompileResult(
167
- success=False, stdout=str(e), hash_value=None, zip_bytes=b""
168
- )
169
- finally:
170
- try:
171
- Path(tmp_zip.name).unlink()
172
- except PermissionError:
173
- print("Warning: Could not delete temporary zip file")
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.0
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
  [![Win_Tests](https://github.com/zackees/fastled-wasm/actions/workflows/test_win.yml/badge.svg)](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`
@@ -4,6 +4,6 @@ set -e
4
4
 
5
5
  # Can't run in parralel because of the shared docker
6
6
  # instance.
7
- uv run pytest tests -v
7
+ uv run pytest tests -v "$@"
8
8
  # uv run pytest -n auto tests -v
9
9
 
@@ -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
File without changes
File without changes