fastled 1.4.2__tar.gz → 1.4.4__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 (132) hide show
  1. {fastled-1.4.2 → fastled-1.4.4}/PKG-INFO +1 -1
  2. fastled-1.4.4/requirements.docker.txt +1 -0
  3. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/__init__.py +11 -0
  4. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/__version__.py +1 -1
  5. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/client_server.py +23 -2
  6. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/compile_server.py +2 -0
  7. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/compile_server_impl.py +13 -0
  8. fastled-1.4.4/src/fastled/find_good_connection.py +105 -0
  9. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/playwright_browser.py +77 -2
  10. fastled-1.4.4/src/fastled/web_compile.py +309 -0
  11. fastled-1.4.4/src/fastled/zip_files.py +76 -0
  12. {fastled-1.4.2 → fastled-1.4.4}/src/fastled.egg-info/PKG-INFO +1 -1
  13. {fastled-1.4.2 → fastled-1.4.4}/src/fastled.egg-info/SOURCES.txt +3 -0
  14. fastled-1.4.4/tests/integration/test_libcompile.py +294 -0
  15. fastled-1.4.2/requirements.docker.txt +0 -1
  16. fastled-1.4.2/src/fastled/web_compile.py +0 -335
  17. {fastled-1.4.2 → fastled-1.4.4}/.aiderignore +0 -0
  18. {fastled-1.4.2 → fastled-1.4.4}/.cursorrules +0 -0
  19. {fastled-1.4.2 → fastled-1.4.4}/.dockerignore +0 -0
  20. {fastled-1.4.2 → fastled-1.4.4}/.github/workflows/build_multi_docker_image.yml +0 -0
  21. {fastled-1.4.2 → fastled-1.4.4}/.github/workflows/build_webpage.yml +0 -0
  22. {fastled-1.4.2 → fastled-1.4.4}/.github/workflows/lint.yml +0 -0
  23. {fastled-1.4.2 → fastled-1.4.4}/.github/workflows/publish_release.yml +0 -0
  24. {fastled-1.4.2 → fastled-1.4.4}/.github/workflows/template_build_docker_image.yml +0 -0
  25. {fastled-1.4.2 → fastled-1.4.4}/.github/workflows/test_build_exe.yml +0 -0
  26. {fastled-1.4.2 → fastled-1.4.4}/.github/workflows/test_macos.yml +0 -0
  27. {fastled-1.4.2 → fastled-1.4.4}/.github/workflows/test_ubuntu.yml +0 -0
  28. {fastled-1.4.2 → fastled-1.4.4}/.github/workflows/test_win.yml +0 -0
  29. {fastled-1.4.2 → fastled-1.4.4}/.gitignore +0 -0
  30. {fastled-1.4.2 → fastled-1.4.4}/.pylintrc +0 -0
  31. {fastled-1.4.2 → fastled-1.4.4}/.vscode/launch.json +0 -0
  32. {fastled-1.4.2 → fastled-1.4.4}/.vscode/settings.json +0 -0
  33. {fastled-1.4.2 → fastled-1.4.4}/.vscode/tasks.json +0 -0
  34. {fastled-1.4.2 → fastled-1.4.4}/DEBUGGER.md +0 -0
  35. {fastled-1.4.2 → fastled-1.4.4}/Dockerfile +0 -0
  36. {fastled-1.4.2 → fastled-1.4.4}/FAQ.md +0 -0
  37. {fastled-1.4.2 → fastled-1.4.4}/LICENSE +0 -0
  38. {fastled-1.4.2 → fastled-1.4.4}/MANIFEST.in +0 -0
  39. {fastled-1.4.2 → fastled-1.4.4}/README.md +0 -0
  40. {fastled-1.4.2 → fastled-1.4.4}/RELEASE.md +0 -0
  41. {fastled-1.4.2 → fastled-1.4.4}/TODO.md +0 -0
  42. {fastled-1.4.2 → fastled-1.4.4}/build_exe.py +0 -0
  43. {fastled-1.4.2 → fastled-1.4.4}/build_local_docker.py +0 -0
  44. {fastled-1.4.2 → fastled-1.4.4}/build_site.py +0 -0
  45. {fastled-1.4.2 → fastled-1.4.4}/clean +0 -0
  46. {fastled-1.4.2 → fastled-1.4.4}/compiler/debug.sh +0 -0
  47. {fastled-1.4.2 → fastled-1.4.4}/compiler/run.py +0 -0
  48. {fastled-1.4.2 → fastled-1.4.4}/demo/100dots.html +0 -0
  49. {fastled-1.4.2 → fastled-1.4.4}/demo/demo_threejs.html +0 -0
  50. {fastled-1.4.2 → fastled-1.4.4}/demo/micdemo.html +0 -0
  51. {fastled-1.4.2 → fastled-1.4.4}/demo/mp3upload.html +0 -0
  52. {fastled-1.4.2 → fastled-1.4.4}/demo/webgl_postprocessing_unreal_bloom.html +0 -0
  53. {fastled-1.4.2 → fastled-1.4.4}/docker-compose.yml +0 -0
  54. {fastled-1.4.2 → fastled-1.4.4}/entrypoint.sh +0 -0
  55. {fastled-1.4.2 → fastled-1.4.4}/install +0 -0
  56. {fastled-1.4.2 → fastled-1.4.4}/install_linux.sh +0 -0
  57. {fastled-1.4.2 → fastled-1.4.4}/lint +0 -0
  58. {fastled-1.4.2 → fastled-1.4.4}/pyproject.toml +0 -0
  59. {fastled-1.4.2 → fastled-1.4.4}/requirements.testing.txt +0 -0
  60. {fastled-1.4.2 → fastled-1.4.4}/setup.cfg +0 -0
  61. {fastled-1.4.2 → fastled-1.4.4}/setup.py +0 -0
  62. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/__main__.py +0 -0
  63. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/app.py +0 -0
  64. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/args.py +0 -0
  65. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/assets/example.txt +0 -0
  66. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/assets/localhost-key.pem +0 -0
  67. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/assets/localhost.pem +0 -0
  68. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/cli.py +0 -0
  69. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/cli_test.py +0 -0
  70. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/cli_test_interactive.py +0 -0
  71. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/docker_manager.py +0 -0
  72. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/filewatcher.py +0 -0
  73. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/keyboard.py +0 -0
  74. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/keyz.py +0 -0
  75. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/live_client.py +0 -0
  76. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/open_browser.py +0 -0
  77. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/parse_args.py +0 -0
  78. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/paths.py +0 -0
  79. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/print_filter.py +0 -0
  80. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/project_init.py +0 -0
  81. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/select_sketch_directory.py +0 -0
  82. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/server_flask.py +0 -0
  83. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/server_start.py +0 -0
  84. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/settings.py +0 -0
  85. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/site/build.py +0 -0
  86. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/site/examples.py +0 -0
  87. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/sketch.py +0 -0
  88. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/spinner.py +0 -0
  89. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/string_diff.py +0 -0
  90. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/test/can_run_local_docker_tests.py +0 -0
  91. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/test/examples.py +0 -0
  92. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/types.py +0 -0
  93. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/util.py +0 -0
  94. {fastled-1.4.2 → fastled-1.4.4}/src/fastled/version.py +0 -0
  95. {fastled-1.4.2 → fastled-1.4.4}/src/fastled.egg-info/dependency_links.txt +0 -0
  96. {fastled-1.4.2 → fastled-1.4.4}/src/fastled.egg-info/entry_points.txt +0 -0
  97. {fastled-1.4.2 → fastled-1.4.4}/src/fastled.egg-info/requires.txt +0 -0
  98. {fastled-1.4.2 → fastled-1.4.4}/src/fastled.egg-info/top_level.txt +0 -0
  99. {fastled-1.4.2 → fastled-1.4.4}/test +0 -0
  100. {fastled-1.4.2 → fastled-1.4.4}/tests/integration/test_build_examples.py +0 -0
  101. {fastled-1.4.2 → fastled-1.4.4}/tests/integration/test_examples.py +0 -0
  102. {fastled-1.4.2 → fastled-1.4.4}/tests/integration/test_playwright_integration.py +0 -0
  103. {fastled-1.4.2 → fastled-1.4.4}/tests/unit/html/index.html +0 -0
  104. {fastled-1.4.2 → fastled-1.4.4}/tests/unit/test_api.py +0 -0
  105. {fastled-1.4.2 → fastled-1.4.4}/tests/unit/test_bad_ino.py +0 -0
  106. {fastled-1.4.2 → fastled-1.4.4}/tests/unit/test_banner_string.py +0 -0
  107. {fastled-1.4.2 → fastled-1.4.4}/tests/unit/test_cli.py +0 -0
  108. {fastled-1.4.2 → fastled-1.4.4}/tests/unit/test_cli_no_platformio.py +0 -0
  109. {fastled-1.4.2 → fastled-1.4.4}/tests/unit/test_compile_server.py +0 -0
  110. {fastled-1.4.2 → fastled-1.4.4}/tests/unit/test_debug_fetch_source_files.py +0 -0
  111. {fastled-1.4.2 → fastled-1.4.4}/tests/unit/test_docker_linux_on_windows.py +0 -0
  112. {fastled-1.4.2 → fastled-1.4.4}/tests/unit/test_embedded_data.py +0 -0
  113. {fastled-1.4.2 → fastled-1.4.4}/tests/unit/test_filechanger.py +0 -0
  114. {fastled-1.4.2 → fastled-1.4.4}/tests/unit/test_flask_headers.py +0 -0
  115. {fastled-1.4.2 → fastled-1.4.4}/tests/unit/test_http_server.py +0 -0
  116. {fastled-1.4.2 → fastled-1.4.4}/tests/unit/test_ino/bad/bad.ino +0 -0
  117. {fastled-1.4.2 → fastled-1.4.4}/tests/unit/test_ino/bad_platformio/bad_platformio.ino +0 -0
  118. {fastled-1.4.2 → fastled-1.4.4}/tests/unit/test_ino/bad_platformio/platformio.ini +0 -0
  119. {fastled-1.4.2 → fastled-1.4.4}/tests/unit/test_ino/embedded/data/bigdata.dat +0 -0
  120. {fastled-1.4.2 → fastled-1.4.4}/tests/unit/test_ino/embedded/wasm.ino +0 -0
  121. {fastled-1.4.2 → fastled-1.4.4}/tests/unit/test_ino/wasm/wasm.ino +0 -0
  122. {fastled-1.4.2 → fastled-1.4.4}/tests/unit/test_manual_api_invocation.py +0 -0
  123. {fastled-1.4.2 → fastled-1.4.4}/tests/unit/test_no_platformio_compile.py +0 -0
  124. {fastled-1.4.2 → fastled-1.4.4}/tests/unit/test_project_init.py +0 -0
  125. {fastled-1.4.2 → fastled-1.4.4}/tests/unit/test_server_and_client_seperatly.py +0 -0
  126. {fastled-1.4.2 → fastled-1.4.4}/tests/unit/test_session_compile.py +0 -0
  127. {fastled-1.4.2 → fastled-1.4.4}/tests/unit/test_string_diff.py +0 -0
  128. {fastled-1.4.2 → fastled-1.4.4}/tests/unit/test_string_diff_comprehensive.py +0 -0
  129. {fastled-1.4.2 → fastled-1.4.4}/tests/unit/test_version.py +0 -0
  130. {fastled-1.4.2 → fastled-1.4.4}/tests/unit/test_webcompile.py +0 -0
  131. {fastled-1.4.2 → fastled-1.4.4}/upload_package.sh +0 -0
  132. {fastled-1.4.2 → fastled-1.4.4}/vscode-plugin/readme +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastled
3
- Version: 1.4.2
3
+ Version: 1.4.4
4
4
  Summary: FastLED Wasm Compiler
5
5
  Home-page: https://github.com/zackees/fastled-wasm
6
6
  Maintainer: Zachary Vorhies
@@ -0,0 +1 @@
1
+ fastled-wasm-server>=1.1.17
@@ -41,18 +41,29 @@ class Api:
41
41
  profile: bool = False, # When true then profile information will be enabled and included in the zip.
42
42
  no_platformio: bool = False,
43
43
  ) -> CompileResult:
44
+ from fastled.sketch import looks_like_fastled_repo
44
45
  from fastled.web_compile import web_compile
45
46
 
46
47
  if isinstance(host, CompileServer):
47
48
  host = host.url()
48
49
  if isinstance(directory, str):
49
50
  directory = Path(directory)
51
+
52
+ # Guard: libfastled compilation requires volume source mapping
53
+ # Only allow libcompile if we're in a FastLED repository
54
+ allow_libcompile = looks_like_fastled_repo(Path(".").resolve())
55
+ if not allow_libcompile:
56
+ print(
57
+ "⚠️ libfastled compilation disabled: not running in FastLED repository"
58
+ )
59
+
50
60
  out: CompileResult = web_compile(
51
61
  directory,
52
62
  host,
53
63
  build_mode=build_mode,
54
64
  profile=profile,
55
65
  no_platformio=no_platformio,
66
+ allow_libcompile=allow_libcompile,
56
67
  )
57
68
  return out
58
69
 
@@ -1,6 +1,6 @@
1
1
  # IMPORTANT! There's a bug in github which will REJECT any version update
2
2
  # that has any other change in the repo. Please bump the version as the
3
3
  # ONLY change in a commit, or else the pypi update and the release will fail.
4
- __version__ = "1.4.2"
4
+ __version__ = "1.4.4"
5
5
 
6
6
  __version_url_latest__ = "https://raw.githubusercontent.com/zackees/fastled-wasm/refs/heads/main/src/fastled/__version__.py"
@@ -11,6 +11,7 @@ from pathlib import Path
11
11
  from fastled.compile_server import CompileServer
12
12
  from fastled.docker_manager import DockerManager
13
13
  from fastled.filewatcher import DebouncedFileWatcherProcess, FileWatcherProcess
14
+ from fastled.find_good_connection import ConnectionResult
14
15
  from fastled.keyboard import SpaceBarWatcher
15
16
  from fastled.open_browser import spawn_http_server
16
17
  from fastled.parse_args import Args
@@ -19,7 +20,6 @@ from fastled.sketch import looks_like_sketch_directory
19
20
  from fastled.types import BuildMode, CompileResult, CompileServerError
20
21
  from fastled.web_compile import (
21
22
  SERVER_PORT,
22
- ConnectionResult,
23
23
  find_good_connection,
24
24
  web_compile,
25
25
  )
@@ -101,9 +101,17 @@ def _run_web_compiler(
101
101
  profile: bool,
102
102
  last_hash_value: str | None,
103
103
  no_platformio: bool = False,
104
+ allow_libcompile: bool = False,
104
105
  ) -> CompileResult:
106
+ # Remove the import and libcompile detection logic from here
107
+ # since it will now be passed as a parameter
105
108
  input_dir = Path(directory)
106
109
  output_dir = input_dir / "fastled_js"
110
+
111
+ # Guard: libfastled compilation requires volume source mapping
112
+ if not allow_libcompile:
113
+ print("⚠️ libfastled compilation disabled: not running in FastLED repository")
114
+
107
115
  start = time.time()
108
116
  web_result = web_compile(
109
117
  directory=input_dir,
@@ -111,6 +119,7 @@ def _run_web_compiler(
111
119
  build_mode=build_mode,
112
120
  profile=profile,
113
121
  no_platformio=no_platformio,
122
+ allow_libcompile=allow_libcompile,
114
123
  )
115
124
  diff = time.time() - start
116
125
  if not web_result.success:
@@ -310,6 +319,11 @@ def run_client(
310
319
  # Assume default port for www
311
320
  port = 80
312
321
 
322
+ # Auto-detect libcompile capability on first call
323
+ from fastled.sketch import looks_like_fastled_repo
324
+
325
+ allow_libcompile = looks_like_fastled_repo(Path(".").resolve())
326
+
313
327
  try:
314
328
 
315
329
  def compile_function(
@@ -318,6 +332,7 @@ def run_client(
318
332
  profile: bool = profile,
319
333
  last_hash_value: str | None = None,
320
334
  no_platformio: bool = no_platformio,
335
+ allow_libcompile: bool = allow_libcompile,
321
336
  ) -> CompileResult:
322
337
  TEST_BEFORE_COMPILE(url)
323
338
  return _run_web_compiler(
@@ -327,6 +342,7 @@ def run_client(
327
342
  profile=profile,
328
343
  last_hash_value=last_hash_value,
329
344
  no_platformio=no_platformio,
345
+ allow_libcompile=allow_libcompile,
330
346
  )
331
347
 
332
348
  result: CompileResult = compile_function(last_hash_value=None)
@@ -454,6 +470,10 @@ def run_client(
454
470
  if changed_files:
455
471
  print(f"\nChanges detected in FastLED source code: {changed_files}")
456
472
  print("Press space bar to trigger compile.")
473
+
474
+ # Re-evaluate libcompile capability when source code changes
475
+ allow_libcompile = True
476
+
457
477
  while True:
458
478
  space_bar_pressed = SpaceBarWatcher.watch_space_bar_pressed(
459
479
  timeout=1.0
@@ -476,8 +496,9 @@ def run_client(
476
496
  f"Changes detected in {','.join(sketch_files_changed)}, triggering recompile..."
477
497
  )
478
498
  last_compiled_result = compile_function(
479
- last_hash_value=None
499
+ last_hash_value=None, allow_libcompile=allow_libcompile
480
500
  )
501
+ allow_libcompile = False
481
502
  print("Finished recompile.")
482
503
  # Drain the space bar queue
483
504
  SpaceBarWatcher.watch_space_bar_pressed()
@@ -16,6 +16,7 @@ class CompileServer:
16
16
  platform: Platform = Platform.WASM,
17
17
  remove_previous: bool = False,
18
18
  no_platformio: bool = False,
19
+ allow_libcompile: bool = True,
19
20
  ) -> None:
20
21
  from fastled.compile_server_impl import ( # avoid circular import
21
22
  CompileServerImpl,
@@ -31,6 +32,7 @@ class CompileServer:
31
32
  auto_start=auto_start,
32
33
  remove_previous=remove_previous,
33
34
  no_platformio=no_platformio,
35
+ allow_libcompile=allow_libcompile,
34
36
  )
35
37
 
36
38
  # May throw CompileServerError if server could not be started.
@@ -51,6 +51,7 @@ class CompileServerImpl:
51
51
  container_name: str | None = None,
52
52
  remove_previous: bool = False,
53
53
  no_platformio: bool = False,
54
+ allow_libcompile: bool = True,
54
55
  ) -> None:
55
56
  container_name = container_name or DEFAULT_CONTAINER_NAME
56
57
  if interactive and not mapped_dir:
@@ -70,6 +71,17 @@ class CompileServerImpl:
70
71
  self.auto_updates = auto_updates
71
72
  self.remove_previous = remove_previous
72
73
  self.no_platformio = no_platformio
74
+
75
+ # Guard: libfastled compilation requires volume source mapping
76
+ # If we don't have fastled_src_dir (not in FastLED repo), disable libcompile
77
+ if allow_libcompile and self.fastled_src_dir is None:
78
+ print(
79
+ "⚠️ libfastled compilation disabled: volume source mapping not available"
80
+ )
81
+ print(" (not running in FastLED repository)")
82
+ allow_libcompile = False
83
+
84
+ self.allow_libcompile = allow_libcompile
73
85
  self._port = 0 # 0 until compile server is started
74
86
  if auto_start:
75
87
  self.start()
@@ -112,6 +124,7 @@ class CompileServerImpl:
112
124
  build_mode=build_mode,
113
125
  profile=profile,
114
126
  no_platformio=self.no_platformio,
127
+ allow_libcompile=self.allow_libcompile,
115
128
  )
116
129
  return out
117
130
 
@@ -0,0 +1,105 @@
1
+ import _thread
2
+ import time
3
+ from concurrent.futures import Future, ThreadPoolExecutor, as_completed
4
+ from dataclasses import dataclass
5
+ from typing import Dict, Tuple
6
+
7
+ import httpx
8
+
9
+ _TIMEOUT = 30.0
10
+
11
+ _EXECUTOR = ThreadPoolExecutor(max_workers=8)
12
+
13
+ # In-memory cache for connection results
14
+ # Key: (tuple of urls, filter_out_bad, use_ipv6)
15
+ # Value: (ConnectionResult | None, timestamp)
16
+ _CONNECTION_CACHE: Dict[
17
+ Tuple[tuple, bool, bool], Tuple["ConnectionResult | None", float]
18
+ ] = {}
19
+ _CACHE_TTL = 60.0 * 60.0 # Cache results for 1 hour
20
+
21
+
22
+ @dataclass
23
+ class ConnectionResult:
24
+ host: str
25
+ success: bool
26
+ ipv4: bool
27
+
28
+
29
+ def _sanitize_host(host: str) -> str:
30
+ if host.startswith("http"):
31
+ return host
32
+ is_local_host = "localhost" in host or "127.0.0.1" in host or "0.0.0.0" in host
33
+ use_https = not is_local_host
34
+ if use_https:
35
+ return host if host.startswith("https://") else f"https://{host}"
36
+ return host if host.startswith("http://") else f"http://{host}"
37
+
38
+
39
+ def _test_connection(host: str, use_ipv4: bool) -> ConnectionResult:
40
+ # Function static cache
41
+ host = _sanitize_host(host)
42
+ transport = httpx.HTTPTransport(local_address="0.0.0.0") if use_ipv4 else None
43
+ result: ConnectionResult | None = None
44
+ try:
45
+ with httpx.Client(
46
+ timeout=_TIMEOUT,
47
+ transport=transport,
48
+ ) as test_client:
49
+ test_response = test_client.get(
50
+ f"{host}/healthz", timeout=3, follow_redirects=True
51
+ )
52
+ result = ConnectionResult(host, test_response.status_code == 200, use_ipv4)
53
+ except KeyboardInterrupt:
54
+ _thread.interrupt_main()
55
+ result = ConnectionResult(host, False, use_ipv4)
56
+ except TimeoutError:
57
+ result = ConnectionResult(host, False, use_ipv4)
58
+ except Exception:
59
+ result = ConnectionResult(host, False, use_ipv4)
60
+ return result
61
+
62
+
63
+ def find_good_connection(
64
+ urls: list[str], filter_out_bad=True, use_ipv6: bool = True
65
+ ) -> ConnectionResult | None:
66
+ # Create cache key from parameters
67
+ cache_key = (tuple(sorted(urls)), filter_out_bad, use_ipv6)
68
+ current_time = time.time()
69
+
70
+ # Check if we have a cached result
71
+ if cache_key in _CONNECTION_CACHE:
72
+ cached_result, cached_time = _CONNECTION_CACHE[cache_key]
73
+ if current_time - cached_time < _CACHE_TTL:
74
+ return cached_result
75
+ else:
76
+ # Remove expired cache entry
77
+ del _CONNECTION_CACHE[cache_key]
78
+
79
+ # No valid cache entry, perform the actual connection test
80
+ futures: list[Future] = []
81
+ for url in urls:
82
+
83
+ f = _EXECUTOR.submit(_test_connection, url, use_ipv4=True)
84
+ futures.append(f)
85
+ if use_ipv6 and "localhost" not in url:
86
+ f_v6 = _EXECUTOR.submit(_test_connection, url, use_ipv4=False)
87
+ futures.append(f_v6)
88
+
89
+ result = None
90
+ try:
91
+ # Return first successful result
92
+ for future in as_completed(futures):
93
+ connection_result: ConnectionResult = future.result()
94
+ if connection_result.success or not filter_out_bad:
95
+ result = connection_result
96
+ break
97
+ finally:
98
+ # Cancel any remaining futures
99
+ for future in futures:
100
+ future.cancel()
101
+
102
+ # Cache the result (even if None)
103
+ _CONNECTION_CACHE[cache_key] = (result, current_time)
104
+
105
+ return result
@@ -79,8 +79,83 @@ class PlaywrightBrowser:
79
79
  )
80
80
 
81
81
  if self.page is None and self.browser is not None:
82
- # Create a new browser context and page
83
- context = await self.browser.new_context()
82
+ # Detect system device scale factor to match normal browser behavior
83
+ import platform
84
+
85
+ device_scale_factor = 1.0
86
+
87
+ # Try to detect system display scaling
88
+ try:
89
+ if platform.system() == "Windows":
90
+ # On Windows, try to get the DPI scaling factor
91
+ import ctypes
92
+
93
+ try:
94
+ # Get DPI awareness and scale factor
95
+ user32 = ctypes.windll.user32
96
+ user32.SetProcessDPIAware()
97
+ dc = user32.GetDC(0)
98
+ dpi = ctypes.windll.gdi32.GetDeviceCaps(dc, 88) # LOGPIXELSX
99
+ user32.ReleaseDC(0, dc)
100
+ device_scale_factor = dpi / 96.0 # 96 DPI is 100% scaling
101
+ except Exception:
102
+ # Fallback: try alternative method
103
+ try:
104
+ import tkinter as tk
105
+
106
+ root = tk.Tk()
107
+ device_scale_factor = root.winfo_fpixels("1i") / 96.0
108
+ root.destroy()
109
+ except Exception:
110
+ pass
111
+ elif platform.system() == "Darwin": # macOS
112
+ # On macOS, try to get the display scaling
113
+ try:
114
+ import subprocess
115
+
116
+ result = subprocess.run(
117
+ ["system_profiler", "SPDisplaysDataType"],
118
+ capture_output=True,
119
+ text=True,
120
+ timeout=5,
121
+ )
122
+ if "Retina" in result.stdout or "2x" in result.stdout:
123
+ device_scale_factor = 2.0
124
+ elif "3x" in result.stdout:
125
+ device_scale_factor = 3.0
126
+ except Exception:
127
+ pass
128
+ elif platform.system() == "Linux":
129
+ # On Linux, try to get display scaling from environment or system
130
+ try:
131
+ import os
132
+
133
+ # Try GDK scaling first
134
+ gdk_scale = os.environ.get("GDK_SCALE")
135
+ if gdk_scale:
136
+ device_scale_factor = float(gdk_scale)
137
+ else:
138
+ # Try QT scaling
139
+ qt_scale = os.environ.get("QT_SCALE_FACTOR")
140
+ if qt_scale:
141
+ device_scale_factor = float(qt_scale)
142
+ except Exception:
143
+ pass
144
+ except Exception:
145
+ # If all detection methods fail, default to 1.0
146
+ device_scale_factor = 1.0
147
+
148
+ # Ensure device scale factor is reasonable (between 0.5 and 4.0)
149
+ device_scale_factor = max(0.5, min(4.0, device_scale_factor))
150
+
151
+ print(f"[PYTHON] Detected device scale factor: {device_scale_factor}")
152
+
153
+ # Create a new browser context with proper device scale factor
154
+ context = await self.browser.new_context(
155
+ device_scale_factor=device_scale_factor,
156
+ # Also ensure viewport scaling is handled properly
157
+ viewport={"width": 1280, "height": 720} if not self.headless else None,
158
+ )
84
159
  self.page = await context.new_page()
85
160
 
86
161
  async def open_url(self, url: str) -> None:
@@ -0,0 +1,309 @@
1
+ import io
2
+ import os
3
+ import shutil
4
+ import tempfile
5
+ import time
6
+ import zipfile
7
+ from pathlib import Path
8
+
9
+ import httpx
10
+
11
+ from fastled.find_good_connection import find_good_connection
12
+ from fastled.settings import SERVER_PORT
13
+ from fastled.types import BuildMode, CompileResult
14
+ from fastled.zip_files import ZipResult, zip_files
15
+
16
+ DEFAULT_HOST = "https://fastled.onrender.com"
17
+ ENDPOINT_COMPILED_WASM = "compile/wasm"
18
+ _TIMEOUT = 60 * 4 # 2 mins timeout
19
+ _AUTH_TOKEN = "oBOT5jbsO4ztgrpNsQwlmFLIKB"
20
+
21
+
22
+ def _sanitize_host(host: str) -> str:
23
+ if host.startswith("http"):
24
+ return host
25
+ is_local_host = "localhost" in host or "127.0.0.1" in host or "0.0.0.0" in host
26
+ use_https = not is_local_host
27
+ if use_https:
28
+ return host if host.startswith("https://") else f"https://{host}"
29
+ return host if host.startswith("http://") else f"http://{host}"
30
+
31
+
32
+ def _banner(msg: str) -> str:
33
+ """
34
+ Create a banner for the given message.
35
+ Example:
36
+ msg = "Hello, World!"
37
+ print -> "#################"
38
+ "# Hello, World! #"
39
+ "#################"
40
+ """
41
+ lines = msg.split("\n")
42
+ # Find the width of the widest line
43
+ max_width = max(len(line) for line in lines)
44
+ width = max_width + 4 # Add 4 for "# " and " #"
45
+
46
+ # Create the top border
47
+ banner = "\n" + "#" * width + "\n"
48
+
49
+ # Add each line with proper padding
50
+ for line in lines:
51
+ padding = max_width - len(line)
52
+ banner += f"# {line}{' ' * padding} #\n"
53
+
54
+ # Add the bottom border
55
+ banner += "#" * width + "\n"
56
+ return f"\n{banner}\n"
57
+
58
+
59
+ def _print_banner(msg: str) -> None:
60
+ print(_banner(msg))
61
+
62
+
63
+ def _compile_libfastled(
64
+ host: str,
65
+ auth_token: str,
66
+ build_mode: BuildMode,
67
+ ) -> httpx.Response:
68
+ """Compile the FastLED library separately."""
69
+ host = _sanitize_host(host)
70
+ urls = [host]
71
+ domain = host.split("://")[-1]
72
+ if ":" not in domain:
73
+ urls.append(f"{host}:{SERVER_PORT}")
74
+
75
+ connection_result = find_good_connection(urls)
76
+ if connection_result is None:
77
+ raise ConnectionError(
78
+ "Connection failed to all endpoints for libfastled compilation"
79
+ )
80
+
81
+ ipv4_stmt = "IPv4" if connection_result.ipv4 else "IPv6"
82
+ transport = (
83
+ httpx.HTTPTransport(local_address="0.0.0.0") if connection_result.ipv4 else None
84
+ )
85
+
86
+ with httpx.Client(
87
+ transport=transport,
88
+ timeout=_TIMEOUT * 2, # Give more time for library compilation
89
+ ) as client:
90
+ headers = {
91
+ "accept": "application/json",
92
+ "authorization": auth_token,
93
+ "build": build_mode.value.lower(),
94
+ }
95
+
96
+ url = f"{connection_result.host}/compile/libfastled"
97
+ print(f"Compiling libfastled on {url} via {ipv4_stmt}")
98
+ response = client.post(
99
+ url,
100
+ headers=headers,
101
+ timeout=_TIMEOUT * 2,
102
+ )
103
+
104
+ return response
105
+
106
+
107
+ def _send_compile_request(
108
+ host: str,
109
+ zip_bytes: bytes,
110
+ auth_token: str,
111
+ build_mode: BuildMode,
112
+ profile: bool,
113
+ no_platformio: bool,
114
+ allow_libcompile: bool,
115
+ ) -> httpx.Response:
116
+ """Send the compile request to the server and return the response."""
117
+ host = _sanitize_host(host)
118
+ urls = [host]
119
+ domain = host.split("://")[-1]
120
+ if ":" not in domain:
121
+ urls.append(f"{host}:{SERVER_PORT}")
122
+
123
+ connection_result = find_good_connection(urls)
124
+ if connection_result is None:
125
+ raise ConnectionError("Connection failed to all endpoints")
126
+
127
+ ipv4_stmt = "IPv4" if connection_result.ipv4 else "IPv6"
128
+ transport = (
129
+ httpx.HTTPTransport(local_address="0.0.0.0") if connection_result.ipv4 else None
130
+ )
131
+
132
+ archive_size = len(zip_bytes)
133
+
134
+ with httpx.Client(
135
+ transport=transport,
136
+ timeout=_TIMEOUT,
137
+ ) as client:
138
+ headers = {
139
+ "accept": "application/json",
140
+ "authorization": auth_token,
141
+ "build": (
142
+ build_mode.value.lower()
143
+ if build_mode
144
+ else BuildMode.QUICK.value.lower()
145
+ ),
146
+ "profile": "true" if profile else "false",
147
+ "no-platformio": "true" if no_platformio else "false",
148
+ "allow-libcompile": "false", # Always false since we handle it manually
149
+ }
150
+
151
+ url = f"{connection_result.host}/{ENDPOINT_COMPILED_WASM}"
152
+ print(
153
+ f"Compiling sketch on {url} via {ipv4_stmt}. Zip size: {archive_size} bytes"
154
+ )
155
+ files = {"file": ("wasm.zip", zip_bytes, "application/x-zip-compressed")}
156
+ response = client.post(
157
+ url,
158
+ follow_redirects=True,
159
+ files=files,
160
+ headers=headers,
161
+ timeout=_TIMEOUT,
162
+ )
163
+
164
+ return response
165
+
166
+
167
+ def _process_compile_response(
168
+ response: httpx.Response,
169
+ zip_result: ZipResult,
170
+ start_time: float,
171
+ ) -> CompileResult:
172
+ """Process the compile response and return the final result."""
173
+ if response.status_code != 200:
174
+ json_response = response.json()
175
+ detail = json_response.get("detail", "Could not compile")
176
+ return CompileResult(
177
+ success=False, stdout=detail, hash_value=None, zip_bytes=b""
178
+ )
179
+
180
+ print(f"Response status code: {response}")
181
+ # Create a temporary directory to extract the zip
182
+ with tempfile.TemporaryDirectory() as extract_dir:
183
+ extract_path = Path(extract_dir)
184
+
185
+ # Write the response content to a temporary zip file
186
+ temp_zip = extract_path / "response.zip"
187
+ temp_zip.write_bytes(response.content)
188
+
189
+ # Extract the zip
190
+ shutil.unpack_archive(temp_zip, extract_path, "zip")
191
+
192
+ if zip_result.zip_embedded_bytes:
193
+ # extract the embedded bytes, which were not sent to the server
194
+ temp_zip.write_bytes(zip_result.zip_embedded_bytes)
195
+ shutil.unpack_archive(temp_zip, extract_path, "zip")
196
+
197
+ # we don't need the temp zip anymore
198
+ temp_zip.unlink()
199
+
200
+ # Read stdout from out.txt if it exists
201
+ stdout_file = extract_path / "out.txt"
202
+ hash_file = extract_path / "hash.txt"
203
+ stdout = (
204
+ stdout_file.read_text(encoding="utf-8", errors="replace")
205
+ if stdout_file.exists()
206
+ else ""
207
+ )
208
+ hash_value = (
209
+ hash_file.read_text(encoding="utf-8", errors="replace")
210
+ if hash_file.exists()
211
+ else None
212
+ )
213
+
214
+ # now rezip the extracted files since we added the embedded json files
215
+ out_buffer = io.BytesIO()
216
+ with zipfile.ZipFile(
217
+ out_buffer, "w", zipfile.ZIP_DEFLATED, compresslevel=9
218
+ ) as out_zip:
219
+ for root, _, _files in os.walk(extract_path):
220
+ for file in _files:
221
+ file_path = Path(root) / file
222
+ relative_path = file_path.relative_to(extract_path)
223
+ out_zip.write(file_path, relative_path)
224
+
225
+ diff_time = time.time() - start_time
226
+ msg = f"Compilation success, took {diff_time:.2f} seconds"
227
+ _print_banner(msg)
228
+ return CompileResult(
229
+ success=True,
230
+ stdout=stdout,
231
+ hash_value=hash_value,
232
+ zip_bytes=out_buffer.getvalue(),
233
+ )
234
+
235
+
236
+ def web_compile(
237
+ directory: Path | str,
238
+ host: str | None = None,
239
+ auth_token: str | None = None,
240
+ build_mode: BuildMode | None = None,
241
+ profile: bool = False,
242
+ no_platformio: bool = False,
243
+ allow_libcompile: bool = True,
244
+ ) -> CompileResult:
245
+ start_time = time.time()
246
+ if isinstance(directory, str):
247
+ directory = Path(directory)
248
+ host = _sanitize_host(host or DEFAULT_HOST)
249
+ build_mode = build_mode or BuildMode.QUICK
250
+ _print_banner(f"Compiling on {host}")
251
+ auth_token = auth_token or _AUTH_TOKEN
252
+ if not directory.exists():
253
+ raise FileNotFoundError(f"Directory not found: {directory}")
254
+ zip_result: ZipResult | Exception = zip_files(directory, build_mode=build_mode)
255
+ if isinstance(zip_result, Exception):
256
+ return CompileResult(
257
+ success=False, stdout=str(zip_result), hash_value=None, zip_bytes=b""
258
+ )
259
+ zip_bytes = zip_result.zip_bytes
260
+ print(f"Web compiling on {host}...")
261
+ try:
262
+ # Step 1: Compile libfastled if requested
263
+ if allow_libcompile:
264
+ print("Step 1: Compiling libfastled...")
265
+ try:
266
+ libfastled_response = _compile_libfastled(host, auth_token, build_mode)
267
+ if libfastled_response.status_code != 200:
268
+ print(
269
+ f"Warning: libfastled compilation failed with status {libfastled_response.status_code}"
270
+ )
271
+ # Continue with sketch compilation even if libfastled fails
272
+ else:
273
+ print("✅ libfastled compilation successful")
274
+ except Exception as e:
275
+ print(f"Warning: libfastled compilation failed: {e}")
276
+ # Continue with sketch compilation even if libfastled fails
277
+ else:
278
+ print("Step 1 (skipped): Compiling libfastled")
279
+
280
+ # Step 2: Compile the sketch
281
+ print("Step 2: Compiling sketch...")
282
+ response = _send_compile_request(
283
+ host,
284
+ zip_bytes,
285
+ auth_token,
286
+ build_mode,
287
+ profile,
288
+ no_platformio,
289
+ False, # allow_libcompile is always False since we handle it manually
290
+ )
291
+
292
+ return _process_compile_response(response, zip_result, start_time)
293
+
294
+ except ConnectionError as e:
295
+ _print_banner(str(e))
296
+ return CompileResult(
297
+ success=False,
298
+ stdout=str(e),
299
+ hash_value=None,
300
+ zip_bytes=b"",
301
+ )
302
+ except KeyboardInterrupt:
303
+ print("Keyboard interrupt")
304
+ raise
305
+ except httpx.HTTPError as e:
306
+ print(f"Error: {e}")
307
+ return CompileResult(
308
+ success=False, stdout=str(e), hash_value=None, zip_bytes=b""
309
+ )