fastled 1.4.3__tar.gz → 1.4.5__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.3 → fastled-1.4.5}/PKG-INFO +2 -2
  2. {fastled-1.4.3 → fastled-1.4.5}/compiler/run.py +2 -1
  3. {fastled-1.4.3 → fastled-1.4.5}/install +1 -1
  4. {fastled-1.4.3 → fastled-1.4.5}/pyproject.toml +1 -8
  5. fastled-1.4.5/requirements.docker.txt +1 -0
  6. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/__init__.py +12 -1
  7. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/__version__.py +1 -1
  8. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/client_server.py +23 -2
  9. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/compile_server.py +2 -0
  10. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/compile_server_impl.py +13 -0
  11. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/filewatcher.py +5 -4
  12. fastled-1.4.5/src/fastled/find_good_connection.py +105 -0
  13. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/playwright_browser.py +92 -22
  14. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/site/build.py +5 -2
  15. fastled-1.4.5/src/fastled/web_compile.py +309 -0
  16. fastled-1.4.5/src/fastled/zip_files.py +76 -0
  17. {fastled-1.4.3 → fastled-1.4.5}/src/fastled.egg-info/PKG-INFO +2 -2
  18. {fastled-1.4.3 → fastled-1.4.5}/src/fastled.egg-info/SOURCES.txt +3 -0
  19. {fastled-1.4.3 → fastled-1.4.5}/src/fastled.egg-info/requires.txt +1 -1
  20. fastled-1.4.5/tests/integration/test_libcompile.py +294 -0
  21. fastled-1.4.3/requirements.docker.txt +0 -1
  22. fastled-1.4.3/src/fastled/web_compile.py +0 -335
  23. {fastled-1.4.3 → fastled-1.4.5}/.aiderignore +0 -0
  24. {fastled-1.4.3 → fastled-1.4.5}/.cursorrules +0 -0
  25. {fastled-1.4.3 → fastled-1.4.5}/.dockerignore +0 -0
  26. {fastled-1.4.3 → fastled-1.4.5}/.github/workflows/build_multi_docker_image.yml +0 -0
  27. {fastled-1.4.3 → fastled-1.4.5}/.github/workflows/build_webpage.yml +0 -0
  28. {fastled-1.4.3 → fastled-1.4.5}/.github/workflows/lint.yml +0 -0
  29. {fastled-1.4.3 → fastled-1.4.5}/.github/workflows/publish_release.yml +0 -0
  30. {fastled-1.4.3 → fastled-1.4.5}/.github/workflows/template_build_docker_image.yml +0 -0
  31. {fastled-1.4.3 → fastled-1.4.5}/.github/workflows/test_build_exe.yml +0 -0
  32. {fastled-1.4.3 → fastled-1.4.5}/.github/workflows/test_macos.yml +0 -0
  33. {fastled-1.4.3 → fastled-1.4.5}/.github/workflows/test_ubuntu.yml +0 -0
  34. {fastled-1.4.3 → fastled-1.4.5}/.github/workflows/test_win.yml +0 -0
  35. {fastled-1.4.3 → fastled-1.4.5}/.gitignore +0 -0
  36. {fastled-1.4.3 → fastled-1.4.5}/.pylintrc +0 -0
  37. {fastled-1.4.3 → fastled-1.4.5}/.vscode/launch.json +0 -0
  38. {fastled-1.4.3 → fastled-1.4.5}/.vscode/settings.json +0 -0
  39. {fastled-1.4.3 → fastled-1.4.5}/.vscode/tasks.json +0 -0
  40. {fastled-1.4.3 → fastled-1.4.5}/DEBUGGER.md +0 -0
  41. {fastled-1.4.3 → fastled-1.4.5}/Dockerfile +0 -0
  42. {fastled-1.4.3 → fastled-1.4.5}/FAQ.md +0 -0
  43. {fastled-1.4.3 → fastled-1.4.5}/LICENSE +0 -0
  44. {fastled-1.4.3 → fastled-1.4.5}/MANIFEST.in +0 -0
  45. {fastled-1.4.3 → fastled-1.4.5}/README.md +0 -0
  46. {fastled-1.4.3 → fastled-1.4.5}/RELEASE.md +0 -0
  47. {fastled-1.4.3 → fastled-1.4.5}/TODO.md +0 -0
  48. {fastled-1.4.3 → fastled-1.4.5}/build_exe.py +0 -0
  49. {fastled-1.4.3 → fastled-1.4.5}/build_local_docker.py +0 -0
  50. {fastled-1.4.3 → fastled-1.4.5}/build_site.py +0 -0
  51. {fastled-1.4.3 → fastled-1.4.5}/clean +0 -0
  52. {fastled-1.4.3 → fastled-1.4.5}/compiler/debug.sh +0 -0
  53. {fastled-1.4.3 → fastled-1.4.5}/demo/100dots.html +0 -0
  54. {fastled-1.4.3 → fastled-1.4.5}/demo/demo_threejs.html +0 -0
  55. {fastled-1.4.3 → fastled-1.4.5}/demo/micdemo.html +0 -0
  56. {fastled-1.4.3 → fastled-1.4.5}/demo/mp3upload.html +0 -0
  57. {fastled-1.4.3 → fastled-1.4.5}/demo/webgl_postprocessing_unreal_bloom.html +0 -0
  58. {fastled-1.4.3 → fastled-1.4.5}/docker-compose.yml +0 -0
  59. {fastled-1.4.3 → fastled-1.4.5}/entrypoint.sh +0 -0
  60. {fastled-1.4.3 → fastled-1.4.5}/install_linux.sh +0 -0
  61. {fastled-1.4.3 → fastled-1.4.5}/lint +0 -0
  62. {fastled-1.4.3 → fastled-1.4.5}/requirements.testing.txt +0 -0
  63. {fastled-1.4.3 → fastled-1.4.5}/setup.cfg +0 -0
  64. {fastled-1.4.3 → fastled-1.4.5}/setup.py +0 -0
  65. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/__main__.py +0 -0
  66. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/app.py +0 -0
  67. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/args.py +0 -0
  68. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/assets/example.txt +0 -0
  69. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/assets/localhost-key.pem +0 -0
  70. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/assets/localhost.pem +0 -0
  71. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/cli.py +0 -0
  72. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/cli_test.py +0 -0
  73. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/cli_test_interactive.py +0 -0
  74. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/docker_manager.py +0 -0
  75. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/keyboard.py +0 -0
  76. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/keyz.py +0 -0
  77. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/live_client.py +0 -0
  78. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/open_browser.py +0 -0
  79. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/parse_args.py +0 -0
  80. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/paths.py +0 -0
  81. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/print_filter.py +0 -0
  82. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/project_init.py +0 -0
  83. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/select_sketch_directory.py +0 -0
  84. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/server_flask.py +0 -0
  85. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/server_start.py +0 -0
  86. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/settings.py +0 -0
  87. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/site/examples.py +0 -0
  88. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/sketch.py +0 -0
  89. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/spinner.py +0 -0
  90. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/string_diff.py +0 -0
  91. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/test/can_run_local_docker_tests.py +0 -0
  92. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/test/examples.py +0 -0
  93. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/types.py +0 -0
  94. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/util.py +0 -0
  95. {fastled-1.4.3 → fastled-1.4.5}/src/fastled/version.py +0 -0
  96. {fastled-1.4.3 → fastled-1.4.5}/src/fastled.egg-info/dependency_links.txt +0 -0
  97. {fastled-1.4.3 → fastled-1.4.5}/src/fastled.egg-info/entry_points.txt +0 -0
  98. {fastled-1.4.3 → fastled-1.4.5}/src/fastled.egg-info/top_level.txt +0 -0
  99. {fastled-1.4.3 → fastled-1.4.5}/test +0 -0
  100. {fastled-1.4.3 → fastled-1.4.5}/tests/integration/test_build_examples.py +0 -0
  101. {fastled-1.4.3 → fastled-1.4.5}/tests/integration/test_examples.py +0 -0
  102. {fastled-1.4.3 → fastled-1.4.5}/tests/integration/test_playwright_integration.py +0 -0
  103. {fastled-1.4.3 → fastled-1.4.5}/tests/unit/html/index.html +0 -0
  104. {fastled-1.4.3 → fastled-1.4.5}/tests/unit/test_api.py +0 -0
  105. {fastled-1.4.3 → fastled-1.4.5}/tests/unit/test_bad_ino.py +0 -0
  106. {fastled-1.4.3 → fastled-1.4.5}/tests/unit/test_banner_string.py +0 -0
  107. {fastled-1.4.3 → fastled-1.4.5}/tests/unit/test_cli.py +0 -0
  108. {fastled-1.4.3 → fastled-1.4.5}/tests/unit/test_cli_no_platformio.py +0 -0
  109. {fastled-1.4.3 → fastled-1.4.5}/tests/unit/test_compile_server.py +0 -0
  110. {fastled-1.4.3 → fastled-1.4.5}/tests/unit/test_debug_fetch_source_files.py +0 -0
  111. {fastled-1.4.3 → fastled-1.4.5}/tests/unit/test_docker_linux_on_windows.py +0 -0
  112. {fastled-1.4.3 → fastled-1.4.5}/tests/unit/test_embedded_data.py +0 -0
  113. {fastled-1.4.3 → fastled-1.4.5}/tests/unit/test_filechanger.py +0 -0
  114. {fastled-1.4.3 → fastled-1.4.5}/tests/unit/test_flask_headers.py +0 -0
  115. {fastled-1.4.3 → fastled-1.4.5}/tests/unit/test_http_server.py +0 -0
  116. {fastled-1.4.3 → fastled-1.4.5}/tests/unit/test_ino/bad/bad.ino +0 -0
  117. {fastled-1.4.3 → fastled-1.4.5}/tests/unit/test_ino/bad_platformio/bad_platformio.ino +0 -0
  118. {fastled-1.4.3 → fastled-1.4.5}/tests/unit/test_ino/bad_platformio/platformio.ini +0 -0
  119. {fastled-1.4.3 → fastled-1.4.5}/tests/unit/test_ino/embedded/data/bigdata.dat +0 -0
  120. {fastled-1.4.3 → fastled-1.4.5}/tests/unit/test_ino/embedded/wasm.ino +0 -0
  121. {fastled-1.4.3 → fastled-1.4.5}/tests/unit/test_ino/wasm/wasm.ino +0 -0
  122. {fastled-1.4.3 → fastled-1.4.5}/tests/unit/test_manual_api_invocation.py +0 -0
  123. {fastled-1.4.3 → fastled-1.4.5}/tests/unit/test_no_platformio_compile.py +0 -0
  124. {fastled-1.4.3 → fastled-1.4.5}/tests/unit/test_project_init.py +0 -0
  125. {fastled-1.4.3 → fastled-1.4.5}/tests/unit/test_server_and_client_seperatly.py +0 -0
  126. {fastled-1.4.3 → fastled-1.4.5}/tests/unit/test_session_compile.py +0 -0
  127. {fastled-1.4.3 → fastled-1.4.5}/tests/unit/test_string_diff.py +0 -0
  128. {fastled-1.4.3 → fastled-1.4.5}/tests/unit/test_string_diff_comprehensive.py +0 -0
  129. {fastled-1.4.3 → fastled-1.4.5}/tests/unit/test_version.py +0 -0
  130. {fastled-1.4.3 → fastled-1.4.5}/tests/unit/test_webcompile.py +0 -0
  131. {fastled-1.4.3 → fastled-1.4.5}/upload_package.sh +0 -0
  132. {fastled-1.4.3 → fastled-1.4.5}/vscode-plugin/readme +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastled
3
- Version: 1.4.3
3
+ Version: 1.4.5
4
4
  Summary: FastLED Wasm Compiler
5
5
  Home-page: https://github.com/zackees/fastled-wasm
6
6
  Maintainer: Zachary Vorhies
@@ -10,7 +10,7 @@ Classifier: Programming Language :: Python :: 3
10
10
  Requires-Python: >=3.10
11
11
  Description-Content-Type: text/markdown
12
12
  License-File: LICENSE
13
- Requires-Dist: fastled-wasm-server>=1.1.0
13
+ Requires-Dist: httpx>=0.28.1
14
14
  Requires-Dist: watchdog>=6.0.0
15
15
  Requires-Dist: docker>=7.1.0
16
16
  Requires-Dist: filelock>=3.16.1
@@ -4,6 +4,7 @@ import subprocess
4
4
  import sys
5
5
  import warnings
6
6
  from pathlib import Path
7
+ from typing import Tuple
7
8
 
8
9
  from fastled_wasm_compiler import Compiler
9
10
  from fastled_wasm_compiler.paths import VOLUME_MAPPED_SRC
@@ -15,7 +16,7 @@ _CHOICES = ["compile", "server"]
15
16
  HERE = Path(__file__).parent
16
17
 
17
18
 
18
- def _parse_args() -> tuple[argparse.Namespace, list[str]]:
19
+ def _parse_args() -> Tuple[argparse.Namespace, list[str]]:
19
20
  parser = argparse.ArgumentParser(
20
21
  description="Run compile.py with additional arguments"
21
22
  )
@@ -17,7 +17,7 @@ if ! command -v uv &> /dev/null; then
17
17
  fi
18
18
 
19
19
  uv venv --python 3.11 --seed
20
- uv pip install -e .[full] --refresh
20
+ uv pip install -e .[full]
21
21
 
22
22
  uv run pip install -r requirements.testing.txt
23
23
  uv run pip install -r requirements.docker.txt
@@ -11,7 +11,7 @@ keywords = ["template-python-cmd"]
11
11
  license = { text = "BSD 3-Clause License" }
12
12
  classifiers = ["Programming Language :: Python :: 3"]
13
13
  dependencies = [
14
- "fastled-wasm-server>=1.1.0", # Just use the Client Code
14
+ "httpx>=0.28.1",
15
15
  "watchdog>=6.0.0",
16
16
  ########## Begin Docker Manager Dependencies
17
17
  "docker>=7.1.0",
@@ -43,13 +43,6 @@ version = { attr = "fastled.__version__" }
43
43
  [tool.ruff]
44
44
  line-length = 200
45
45
 
46
- [tool.ruff.lint]
47
- select = ["F", "E", "W", "UP", "PYI"]
48
-
49
- [tool.ruff.lint.per-file-ignores]
50
- "__init__.py" = ["F401"]
51
- "src/fastled/site/build.py" = ["W291", "W293"] # Ignore whitespace in multi-line strings
52
-
53
46
  [tool.pylint."MESSAGES CONTROL"]
54
47
  good-names = [
55
48
  "c",
@@ -0,0 +1 @@
1
+ fastled-wasm-server>=1.1.17
@@ -1,9 +1,9 @@
1
1
  """FastLED Wasm Compiler package."""
2
2
 
3
- from collections.abc import Generator
4
3
  from contextlib import contextmanager
5
4
  from multiprocessing import Process
6
5
  from pathlib import Path
6
+ from typing import Generator
7
7
 
8
8
  from .__version__ import __version__
9
9
  from .compile_server import CompileServer
@@ -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.3"
4
+ __version__ = "1.4.5"
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
 
@@ -10,6 +10,7 @@ from contextlib import redirect_stdout
10
10
  from multiprocessing import Process, Queue
11
11
  from pathlib import Path
12
12
  from queue import Empty
13
+ from typing import Dict, Set
13
14
 
14
15
  from watchdog.events import FileSystemEvent, FileSystemEventHandler
15
16
  from watchdog.observers import Observer
@@ -34,8 +35,8 @@ class MyEventHandler(FileSystemEventHandler):
34
35
  def __init__(
35
36
  self,
36
37
  change_queue: queue.Queue,
37
- excluded_patterns: set[str],
38
- file_hashes: dict[str, str],
38
+ excluded_patterns: Set[str],
39
+ file_hashes: Dict[str, str],
39
40
  ) -> None:
40
41
  super().__init__()
41
42
  self.change_queue = change_queue
@@ -93,8 +94,8 @@ class FileChangedNotifier(threading.Thread):
93
94
  )
94
95
  self.stopped = False
95
96
  self.change_queue: queue.Queue = queue.Queue()
96
- self.last_notification: dict[str, float] = {}
97
- self.file_hashes: dict[str, str] = {}
97
+ self.last_notification: Dict[str, float] = {}
98
+ self.file_hashes: Dict[str, str] = {}
98
99
  self.debounce_seconds = debounce_seconds
99
100
 
100
101
  def stop(self) -> None:
@@ -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
@@ -44,7 +44,7 @@ class PlaywrightBrowser:
44
44
  Args:
45
45
  headless: Whether to run the browser in headless mode
46
46
  """
47
- if not PLAYWRIGHT_AVAILABLE:
47
+ if not is_playwright_available():
48
48
  raise ImportError(
49
49
  "Playwright is not installed. Install with: pip install fastled[full]"
50
50
  )
@@ -56,31 +56,92 @@ class PlaywrightBrowser:
56
56
  self.playwright: Any = None
57
57
  self._should_exit = asyncio.Event()
58
58
 
59
+ def _detect_device_scale_factor(self) -> float | None:
60
+ """Detect the system's device scale factor for natural browser behavior.
61
+
62
+ Returns:
63
+ The detected device scale factor, or None if detection fails or
64
+ the value is outside reasonable bounds (0.5-4.0).
65
+ """
66
+ try:
67
+ import platform
68
+
69
+ if platform.system() == "Windows":
70
+ import winreg
71
+
72
+ try:
73
+ key = winreg.OpenKey(
74
+ winreg.HKEY_CURRENT_USER, r"Control Panel\Desktop\WindowMetrics"
75
+ )
76
+ dpi, _ = winreg.QueryValueEx(key, "AppliedDPI")
77
+ winreg.CloseKey(key)
78
+ device_scale_factor = dpi / 96.0
79
+
80
+ # Validate the scale factor is within reasonable bounds
81
+ if 0.5 <= device_scale_factor <= 4.0:
82
+ print(
83
+ f"[PYTHON] Detected Windows DPI scaling: {device_scale_factor:.2f}"
84
+ )
85
+ return device_scale_factor
86
+ else:
87
+ print(
88
+ f"[PYTHON] Detected scale factor {device_scale_factor:.2f} outside reasonable bounds"
89
+ )
90
+ return None
91
+
92
+ except (OSError, FileNotFoundError):
93
+ print(
94
+ "[PYTHON] Could not detect Windows DPI, using browser default"
95
+ )
96
+ return None
97
+ else:
98
+ # Future: Add support for other platforms (macOS, Linux) here
99
+ print(
100
+ f"[PYTHON] Device scale detection not implemented for {platform.system()}"
101
+ )
102
+ return None
103
+
104
+ except Exception as e:
105
+ print(f"[PYTHON] DPI detection failed: {e}")
106
+ return None
107
+
59
108
  async def start(self) -> None:
60
109
  """Start the Playwright browser."""
61
- if self.playwright is None and async_playwright is not None:
62
- self.playwright = await async_playwright().start()
110
+ if self.browser is None:
111
+ if is_playwright_available():
112
+ from playwright.async_api import async_playwright
63
113
 
64
- if self.browser is None and self.playwright is not None:
65
- # Try Chrome first, then Firefox, then WebKit
66
- try:
67
- self.browser = await self.playwright.chromium.launch(
114
+ self.playwright = async_playwright()
115
+ playwright = await self.playwright.start()
116
+ self.browser = await playwright.chromium.launch(
68
117
  headless=self.headless,
69
- args=["--disable-web-security", "--allow-running-insecure-content"],
118
+ args=[
119
+ "--disable-dev-shm-usage",
120
+ "--disable-web-security",
121
+ "--allow-running-insecure-content",
122
+ ],
70
123
  )
71
- except Exception:
72
- try:
73
- self.browser = await self.playwright.firefox.launch(
74
- headless=self.headless
75
- )
76
- except Exception:
77
- self.browser = await self.playwright.webkit.launch(
78
- headless=self.headless
79
- )
124
+ else:
125
+ raise RuntimeError("Playwright is not available")
80
126
 
81
127
  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()
128
+ # Detect system device scale factor for natural browser behavior
129
+ device_scale_factor = self._detect_device_scale_factor()
130
+
131
+ # Create browser context with detected or default device scale factor
132
+ if device_scale_factor:
133
+ context = await self.browser.new_context(
134
+ device_scale_factor=device_scale_factor
135
+ )
136
+ print(
137
+ f"[PYTHON] Created browser context with device scale factor: {device_scale_factor:.2f}"
138
+ )
139
+ else:
140
+ context = await self.browser.new_context()
141
+ print(
142
+ "[PYTHON] Created browser context with default device scale factor"
143
+ )
144
+
84
145
  self.page = await context.new_page()
85
146
 
86
147
  async def open_url(self, url: str) -> None:
@@ -99,6 +160,15 @@ class PlaywrightBrowser:
99
160
  # Wait for the page to load
100
161
  await self.page.wait_for_load_state("networkidle")
101
162
 
163
+ # Verify device scale factor is working correctly
164
+ try:
165
+ device_pixel_ratio = await self.page.evaluate("window.devicePixelRatio")
166
+ print(
167
+ f"[PYTHON] Verified browser device pixel ratio: {device_pixel_ratio}"
168
+ )
169
+ except Exception as e:
170
+ print(f"[PYTHON] Could not verify device pixel ratio: {e}")
171
+
102
172
  # Set up auto-resizing functionality if enabled
103
173
  if self.auto_resize:
104
174
  await self._setup_auto_resize()
@@ -268,7 +338,7 @@ def run_playwright_browser(url: str, headless: bool = False) -> None:
268
338
  url: The URL to open
269
339
  headless: Whether to run in headless mode
270
340
  """
271
- if not PLAYWRIGHT_AVAILABLE:
341
+ if not is_playwright_available():
272
342
  warnings.warn(
273
343
  "Playwright is not installed. Install with: pip install fastled[full]. "
274
344
  "Falling back to default browser."
@@ -342,7 +412,7 @@ class PlaywrightBrowserProxy:
342
412
  url: The URL to open
343
413
  headless: Whether to run in headless mode
344
414
  """
345
- if not PLAYWRIGHT_AVAILABLE:
415
+ if not is_playwright_available():
346
416
  warnings.warn(
347
417
  "Playwright is not installed. Install with: pip install fastled[full]. "
348
418
  "Falling back to default browser."
@@ -430,7 +500,7 @@ def run_playwright_browser_persistent(url: str, headless: bool = False) -> None:
430
500
  url: The URL to open
431
501
  headless: Whether to run in headless mode
432
502
  """
433
- if not PLAYWRIGHT_AVAILABLE:
503
+ if not is_playwright_available():
434
504
  return
435
505
 
436
506
  async def main():
@@ -265,6 +265,7 @@ INDEX_TEMPLATE = """<!DOCTYPE html>
265
265
  checkmark.style.color = '#E0E0E0';
266
266
  link.appendChild(checkmark);
267
267
  }});
268
+
268
269
  // Now load first example and show its checkmark
269
270
  if (links.length > 0) {{
270
271
  // Try to find SdCard example first
@@ -273,6 +274,7 @@ INDEX_TEMPLATE = """<!DOCTYPE html>
273
274
  startLink.classList.add('active');
274
275
  startLink.querySelector('.fa-check').style.display = 'inline-block';
275
276
  }}
277
+
276
278
  // Add click handlers
277
279
  links.forEach(link => {{
278
280
  link.addEventListener('click', function(e) {{
@@ -311,10 +313,11 @@ INDEX_TEMPLATE = """<!DOCTYPE html>
311
313
  showNav();
312
314
  }}
313
315
  }});
316
+
314
317
  // Close menu when clicking anywhere in the document
315
318
  document.addEventListener('click', (e) => {{
316
- if (navPane.classList.contains('visible') &&
317
- !navPane.contains(e.target) &&
319
+ if (navPane.classList.contains('visible') &&
320
+ !navPane.contains(e.target) &&
318
321
  !navTrigger.contains(e.target)) {{
319
322
  hideNav();
320
323
  }}