fastled 1.2.33__py3-none-any.whl → 1.4.50__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastled/__init__.py +51 -192
- fastled/__main__.py +14 -0
- fastled/__version__.py +6 -0
- fastled/app.py +124 -27
- fastled/args.py +124 -0
- fastled/assets/localhost-key.pem +28 -0
- fastled/assets/localhost.pem +27 -0
- fastled/cli.py +10 -2
- fastled/cli_test.py +21 -0
- fastled/cli_test_interactive.py +21 -0
- fastled/client_server.py +334 -55
- fastled/compile_server.py +12 -1
- fastled/compile_server_impl.py +115 -42
- fastled/docker_manager.py +392 -69
- fastled/emoji_util.py +27 -0
- fastled/filewatcher.py +100 -8
- fastled/find_good_connection.py +105 -0
- fastled/header_dump.py +63 -0
- fastled/install/__init__.py +1 -0
- fastled/install/examples_manager.py +62 -0
- fastled/install/extension_manager.py +113 -0
- fastled/install/main.py +156 -0
- fastled/install/project_detection.py +167 -0
- fastled/install/test_install.py +373 -0
- fastled/install/vscode_config.py +344 -0
- fastled/interruptible_http.py +148 -0
- fastled/keyboard.py +1 -0
- fastled/keyz.py +84 -0
- fastled/live_client.py +26 -1
- fastled/open_browser.py +133 -89
- fastled/parse_args.py +219 -15
- fastled/playwright/chrome_extension_downloader.py +207 -0
- fastled/playwright/playwright_browser.py +773 -0
- fastled/playwright/resize_tracking.py +127 -0
- fastled/print_filter.py +52 -0
- fastled/project_init.py +20 -13
- fastled/select_sketch_directory.py +142 -17
- fastled/server_flask.py +487 -0
- fastled/server_start.py +21 -0
- fastled/settings.py +53 -4
- fastled/site/build.py +2 -10
- fastled/site/examples.py +10 -0
- fastled/sketch.py +129 -7
- fastled/string_diff.py +218 -9
- fastled/test/examples.py +7 -5
- fastled/types.py +22 -2
- fastled/util.py +78 -0
- fastled/version.py +41 -0
- fastled/web_compile.py +401 -218
- fastled/zip_files.py +76 -0
- {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/METADATA +533 -382
- fastled-1.4.50.dist-info/RECORD +60 -0
- {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/WHEEL +1 -1
- fastled/open_browser2.py +0 -111
- fastled-1.2.33.dist-info/RECORD +0 -33
- {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/entry_points.txt +0 -0
- {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info/licenses}/LICENSE +0 -0
- {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/top_level.txt +0 -0
fastled/client_server.py
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
|
-
import argparse
|
|
2
1
|
import shutil
|
|
2
|
+
import sys
|
|
3
3
|
import tempfile
|
|
4
4
|
import threading
|
|
5
5
|
import time
|
|
6
|
+
import traceback
|
|
7
|
+
import warnings
|
|
6
8
|
from multiprocessing import Process
|
|
7
9
|
from pathlib import Path
|
|
8
10
|
|
|
9
11
|
from fastled.compile_server import CompileServer
|
|
10
12
|
from fastled.docker_manager import DockerManager
|
|
11
|
-
from fastled.
|
|
13
|
+
from fastled.emoji_util import EMO
|
|
14
|
+
from fastled.filewatcher import DebouncedFileWatcherProcess, FileWatcherProcess
|
|
15
|
+
from fastled.find_good_connection import ConnectionResult
|
|
12
16
|
from fastled.keyboard import SpaceBarWatcher
|
|
13
|
-
from fastled.open_browser import
|
|
14
|
-
from fastled.
|
|
17
|
+
from fastled.open_browser import spawn_http_server
|
|
18
|
+
from fastled.parse_args import Args
|
|
19
|
+
from fastled.settings import DEFAULT_URL, IMAGE_NAME
|
|
15
20
|
from fastled.sketch import looks_like_sketch_directory
|
|
16
21
|
from fastled.types import BuildMode, CompileResult, CompileServerError
|
|
17
22
|
from fastled.web_compile import (
|
|
18
23
|
SERVER_PORT,
|
|
19
|
-
ConnectionResult,
|
|
20
24
|
find_good_connection,
|
|
21
25
|
web_compile,
|
|
22
26
|
)
|
|
@@ -62,38 +66,116 @@ def TEST_BEFORE_COMPILE(url) -> None:
|
|
|
62
66
|
pass
|
|
63
67
|
|
|
64
68
|
|
|
69
|
+
def _chunked_print(stdout: str) -> None:
|
|
70
|
+
lines = stdout.splitlines()
|
|
71
|
+
for line in lines:
|
|
72
|
+
try:
|
|
73
|
+
print(line)
|
|
74
|
+
except UnicodeEncodeError:
|
|
75
|
+
# On Windows, the console may not support Unicode characters
|
|
76
|
+
# Try to encode the line with the console's encoding and replace problematic characters
|
|
77
|
+
try:
|
|
78
|
+
console_encoding = sys.stdout.encoding or "utf-8"
|
|
79
|
+
encoded_line = line.encode(console_encoding, errors="replace").decode(
|
|
80
|
+
console_encoding
|
|
81
|
+
)
|
|
82
|
+
print(encoded_line)
|
|
83
|
+
except Exception:
|
|
84
|
+
# If all else fails, just print the line without problematic characters
|
|
85
|
+
print(line.encode("ascii", errors="replace").decode("ascii"))
|
|
86
|
+
|
|
87
|
+
|
|
65
88
|
def _run_web_compiler(
|
|
66
89
|
directory: Path,
|
|
67
90
|
host: str,
|
|
68
91
|
build_mode: BuildMode,
|
|
69
92
|
profile: bool,
|
|
70
93
|
last_hash_value: str | None,
|
|
94
|
+
no_platformio: bool = False,
|
|
95
|
+
allow_libcompile: bool = False,
|
|
71
96
|
) -> CompileResult:
|
|
97
|
+
# Remove the import and libcompile detection logic from here
|
|
98
|
+
# since it will now be passed as a parameter
|
|
72
99
|
input_dir = Path(directory)
|
|
73
100
|
output_dir = input_dir / "fastled_js"
|
|
101
|
+
|
|
102
|
+
# Guard: libfastled compilation requires volume source mapping
|
|
103
|
+
if not allow_libcompile:
|
|
104
|
+
print(f"{EMO('⚠️', 'WARNING:')} libfastled compilation disabled.")
|
|
105
|
+
|
|
74
106
|
start = time.time()
|
|
75
107
|
web_result = web_compile(
|
|
76
|
-
directory=input_dir,
|
|
108
|
+
directory=input_dir,
|
|
109
|
+
host=host,
|
|
110
|
+
build_mode=build_mode,
|
|
111
|
+
profile=profile,
|
|
112
|
+
no_platformio=no_platformio,
|
|
113
|
+
allow_libcompile=allow_libcompile,
|
|
77
114
|
)
|
|
78
115
|
diff = time.time() - start
|
|
79
116
|
if not web_result.success:
|
|
80
117
|
print("\nWeb compilation failed:")
|
|
81
118
|
print(f"Time taken: {diff:.2f} seconds")
|
|
82
|
-
|
|
119
|
+
_chunked_print(web_result.stdout)
|
|
83
120
|
# Create error page
|
|
84
121
|
output_dir.mkdir(exist_ok=True)
|
|
85
122
|
error_html = _create_error_html(web_result.stdout)
|
|
86
123
|
(output_dir / "index.html").write_text(error_html, encoding="utf-8")
|
|
87
124
|
return web_result
|
|
88
125
|
|
|
126
|
+
# Extract zip contents to fastled_js directory
|
|
127
|
+
extraction_start_time = time.time()
|
|
128
|
+
output_dir.mkdir(exist_ok=True)
|
|
129
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
130
|
+
temp_path = Path(temp_dir)
|
|
131
|
+
temp_zip = temp_path / "result.zip"
|
|
132
|
+
temp_zip.write_bytes(web_result.zip_bytes)
|
|
133
|
+
|
|
134
|
+
# Clear existing contents
|
|
135
|
+
shutil.rmtree(output_dir, ignore_errors=True)
|
|
136
|
+
output_dir.mkdir(exist_ok=True)
|
|
137
|
+
|
|
138
|
+
# Extract zip contents
|
|
139
|
+
shutil.unpack_archive(temp_zip, output_dir, "zip")
|
|
140
|
+
extraction_time = time.time() - extraction_start_time
|
|
141
|
+
|
|
89
142
|
def print_results() -> None:
|
|
90
143
|
hash_value = (
|
|
91
144
|
web_result.hash_value
|
|
92
145
|
if web_result.hash_value is not None
|
|
93
146
|
else "NO HASH VALUE"
|
|
94
147
|
)
|
|
148
|
+
|
|
149
|
+
# Build timing breakdown
|
|
150
|
+
timing_breakdown = f" Time: {diff:.2f} (seconds)"
|
|
151
|
+
if hasattr(web_result, "zip_time"):
|
|
152
|
+
timing_breakdown += f"\n zip creation: {web_result.zip_time:.2f}"
|
|
153
|
+
if web_result.libfastled_time > 0:
|
|
154
|
+
timing_breakdown += (
|
|
155
|
+
f"\n libfastled: {web_result.libfastled_time:.2f}"
|
|
156
|
+
)
|
|
157
|
+
timing_breakdown += (
|
|
158
|
+
f"\n sketch compile + link: {web_result.sketch_time:.2f}"
|
|
159
|
+
)
|
|
160
|
+
if hasattr(web_result, "response_processing_time"):
|
|
161
|
+
timing_breakdown += f"\n response processing: {web_result.response_processing_time:.2f}"
|
|
162
|
+
|
|
163
|
+
# Calculate any unaccounted time
|
|
164
|
+
accounted_time = (
|
|
165
|
+
web_result.zip_time
|
|
166
|
+
+ web_result.libfastled_time
|
|
167
|
+
+ web_result.sketch_time
|
|
168
|
+
+ web_result.response_processing_time
|
|
169
|
+
+ extraction_time
|
|
170
|
+
)
|
|
171
|
+
unaccounted_time = diff - accounted_time
|
|
172
|
+
if extraction_time > 0.01:
|
|
173
|
+
timing_breakdown += f"\n extraction: {extraction_time:.2f}"
|
|
174
|
+
if unaccounted_time > 0.01:
|
|
175
|
+
timing_breakdown += f"\n other overhead: {unaccounted_time:.2f}"
|
|
176
|
+
|
|
95
177
|
print(
|
|
96
|
-
f"\nWeb compilation successful\n
|
|
178
|
+
f"\nWeb compilation successful\n{timing_breakdown}\n output: {output_dir}\n hash: {hash_value}\n zip size: {len(web_result.zip_bytes)} bytes"
|
|
97
179
|
)
|
|
98
180
|
|
|
99
181
|
# now check to see if the hash value is the same as the last hash value
|
|
@@ -102,27 +184,17 @@ def _run_web_compiler(
|
|
|
102
184
|
print_results()
|
|
103
185
|
return web_result
|
|
104
186
|
|
|
105
|
-
|
|
106
|
-
output_dir.mkdir(exist_ok=True)
|
|
107
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
|
108
|
-
temp_path = Path(temp_dir)
|
|
109
|
-
temp_zip = temp_path / "result.zip"
|
|
110
|
-
temp_zip.write_bytes(web_result.zip_bytes)
|
|
111
|
-
|
|
112
|
-
# Clear existing contents
|
|
113
|
-
shutil.rmtree(output_dir, ignore_errors=True)
|
|
114
|
-
output_dir.mkdir(exist_ok=True)
|
|
115
|
-
|
|
116
|
-
# Extract zip contents
|
|
117
|
-
shutil.unpack_archive(temp_zip, output_dir, "zip")
|
|
118
|
-
|
|
119
|
-
print(web_result.stdout)
|
|
187
|
+
_chunked_print(web_result.stdout)
|
|
120
188
|
print_results()
|
|
121
189
|
return web_result
|
|
122
190
|
|
|
123
191
|
|
|
124
192
|
def _try_start_server_or_get_url(
|
|
125
|
-
auto_update: bool,
|
|
193
|
+
auto_update: bool,
|
|
194
|
+
args_web: str | bool,
|
|
195
|
+
localhost: bool,
|
|
196
|
+
clear: bool,
|
|
197
|
+
no_platformio: bool = False,
|
|
126
198
|
) -> tuple[str, CompileServer | None]:
|
|
127
199
|
is_local_host = localhost or (
|
|
128
200
|
isinstance(args_web, str)
|
|
@@ -132,9 +204,9 @@ def _try_start_server_or_get_url(
|
|
|
132
204
|
local_host_needs_server = False
|
|
133
205
|
if is_local_host:
|
|
134
206
|
addr = "localhost" if localhost or not isinstance(args_web, str) else args_web
|
|
135
|
-
urls = [addr]
|
|
136
207
|
if ":" not in addr:
|
|
137
|
-
|
|
208
|
+
addr = f"{addr}:{SERVER_PORT}"
|
|
209
|
+
urls = [addr]
|
|
138
210
|
|
|
139
211
|
result: ConnectionResult | None = find_good_connection(urls)
|
|
140
212
|
if result is not None:
|
|
@@ -152,7 +224,11 @@ def _try_start_server_or_get_url(
|
|
|
152
224
|
else:
|
|
153
225
|
try:
|
|
154
226
|
print("No local server found, starting one...")
|
|
155
|
-
compile_server = CompileServer(
|
|
227
|
+
compile_server = CompileServer(
|
|
228
|
+
auto_updates=auto_update,
|
|
229
|
+
remove_previous=clear,
|
|
230
|
+
no_platformio=no_platformio,
|
|
231
|
+
)
|
|
156
232
|
print("Waiting for the local compiler to start...")
|
|
157
233
|
if not compile_server.ping():
|
|
158
234
|
print("Failed to start local compiler.")
|
|
@@ -160,11 +236,83 @@ def _try_start_server_or_get_url(
|
|
|
160
236
|
return (compile_server.url(), compile_server)
|
|
161
237
|
except KeyboardInterrupt:
|
|
162
238
|
raise
|
|
163
|
-
except
|
|
164
|
-
|
|
239
|
+
except Exception as e:
|
|
240
|
+
warnings.warn(
|
|
241
|
+
f"Failed to start local compile server because of {e}, using web compiler instead."
|
|
242
|
+
)
|
|
165
243
|
return (DEFAULT_URL, None)
|
|
166
244
|
|
|
167
245
|
|
|
246
|
+
def _try_make_compile_server(
|
|
247
|
+
clear: bool = False, no_platformio: bool = False
|
|
248
|
+
) -> CompileServer | None:
|
|
249
|
+
if not DockerManager.is_docker_installed():
|
|
250
|
+
return None
|
|
251
|
+
try:
|
|
252
|
+
print(
|
|
253
|
+
"\nNo host specified, but Docker is installed, attempting to start a compile server using Docker."
|
|
254
|
+
)
|
|
255
|
+
from fastled.util import find_free_port
|
|
256
|
+
|
|
257
|
+
free_port = find_free_port(start_port=9723, end_port=9743)
|
|
258
|
+
if free_port is None:
|
|
259
|
+
return None
|
|
260
|
+
compile_server = CompileServer(
|
|
261
|
+
auto_updates=False, remove_previous=clear, no_platformio=no_platformio
|
|
262
|
+
)
|
|
263
|
+
print("Waiting for the local compiler to start...")
|
|
264
|
+
if not compile_server.ping():
|
|
265
|
+
print("Failed to start local compiler.")
|
|
266
|
+
raise CompileServerError("Failed to start local compiler.")
|
|
267
|
+
return compile_server
|
|
268
|
+
except KeyboardInterrupt:
|
|
269
|
+
import _thread
|
|
270
|
+
|
|
271
|
+
_thread.interrupt_main()
|
|
272
|
+
raise
|
|
273
|
+
except Exception as e:
|
|
274
|
+
warnings.warn(f"Error starting local compile server: {e}")
|
|
275
|
+
return None
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _background_update_docker_image() -> None:
|
|
279
|
+
"""Perform docker image update in the background silently."""
|
|
280
|
+
try:
|
|
281
|
+
# Only attempt update if Docker is installed and running
|
|
282
|
+
if not DockerManager.is_docker_installed():
|
|
283
|
+
return
|
|
284
|
+
|
|
285
|
+
docker_manager = DockerManager()
|
|
286
|
+
docker_running, _ = docker_manager.is_running()
|
|
287
|
+
if not docker_running:
|
|
288
|
+
return
|
|
289
|
+
|
|
290
|
+
# Silently update the docker image
|
|
291
|
+
docker_manager.validate_or_download_image(
|
|
292
|
+
image_name=IMAGE_NAME, tag="latest", upgrade=True
|
|
293
|
+
)
|
|
294
|
+
except KeyboardInterrupt:
|
|
295
|
+
import _thread
|
|
296
|
+
|
|
297
|
+
_thread.interrupt_main()
|
|
298
|
+
except Exception as e:
|
|
299
|
+
# Log warning but don't disrupt user experience
|
|
300
|
+
import warnings
|
|
301
|
+
|
|
302
|
+
warnings.warn(f"Background docker image update failed: {e}")
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _is_local_host(url: str) -> bool:
|
|
306
|
+
return (
|
|
307
|
+
url.startswith("http://localhost")
|
|
308
|
+
or url.startswith("http://127.0.0.1")
|
|
309
|
+
or url.startswith("http://0.0.0.0")
|
|
310
|
+
or url.startswith("http://[::]")
|
|
311
|
+
or url.startswith("http://[::1]")
|
|
312
|
+
or url.startswith("http://[::ffff:127.0.0.1]")
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
168
316
|
def run_client(
|
|
169
317
|
directory: Path,
|
|
170
318
|
host: str | CompileServer | None,
|
|
@@ -173,14 +321,32 @@ def run_client(
|
|
|
173
321
|
build_mode: BuildMode = BuildMode.QUICK,
|
|
174
322
|
profile: bool = False,
|
|
175
323
|
shutdown: threading.Event | None = None,
|
|
324
|
+
http_port: (
|
|
325
|
+
int | None
|
|
326
|
+
) = None, # None means auto select a free port, http_port < 0 means no server.
|
|
327
|
+
clear: bool = False,
|
|
328
|
+
no_platformio: bool = False,
|
|
329
|
+
app: bool = False, # Use app-like browser experience
|
|
330
|
+
background_update: bool = False,
|
|
331
|
+
enable_https: bool = True, # Enable HTTPS for the local server
|
|
176
332
|
) -> int:
|
|
333
|
+
has_checked_newer_version_yet = False
|
|
334
|
+
compile_server: CompileServer | None = None
|
|
335
|
+
|
|
336
|
+
if host is None:
|
|
337
|
+
# attempt to start a compile server if docker is installed.
|
|
338
|
+
compile_server = _try_make_compile_server(
|
|
339
|
+
clear=clear, no_platformio=no_platformio
|
|
340
|
+
)
|
|
341
|
+
if compile_server is None:
|
|
342
|
+
host = DEFAULT_URL
|
|
343
|
+
elif isinstance(host, CompileServer):
|
|
344
|
+
# if the host is a compile server, use that
|
|
345
|
+
compile_server = host
|
|
177
346
|
|
|
178
|
-
compile_server: CompileServer | None = (
|
|
179
|
-
host if isinstance(host, CompileServer) else None
|
|
180
|
-
)
|
|
181
347
|
shutdown = shutdown or threading.Event()
|
|
182
348
|
|
|
183
|
-
def get_url() -> str:
|
|
349
|
+
def get_url(host=host, compile_server=compile_server) -> str:
|
|
184
350
|
if compile_server is not None:
|
|
185
351
|
return compile_server.url()
|
|
186
352
|
if isinstance(host, str):
|
|
@@ -188,6 +354,26 @@ def run_client(
|
|
|
188
354
|
return DEFAULT_URL
|
|
189
355
|
|
|
190
356
|
url = get_url()
|
|
357
|
+
is_local_host = _is_local_host(url)
|
|
358
|
+
# parse out the port from the url
|
|
359
|
+
# use a standard host address parser to grab it
|
|
360
|
+
import urllib.parse
|
|
361
|
+
|
|
362
|
+
parsed_url = urllib.parse.urlparse(url)
|
|
363
|
+
if parsed_url.port is not None:
|
|
364
|
+
port = parsed_url.port
|
|
365
|
+
else:
|
|
366
|
+
if is_local_host:
|
|
367
|
+
raise ValueError(
|
|
368
|
+
"Cannot use local host without a port. Please specify a port."
|
|
369
|
+
)
|
|
370
|
+
# Assume default port for www
|
|
371
|
+
port = 80
|
|
372
|
+
|
|
373
|
+
# Auto-detect libcompile capability on first call
|
|
374
|
+
from fastled.sketch import looks_like_fastled_repo
|
|
375
|
+
|
|
376
|
+
allow_libcompile = is_local_host and looks_like_fastled_repo(Path(".").resolve())
|
|
191
377
|
|
|
192
378
|
try:
|
|
193
379
|
|
|
@@ -196,6 +382,8 @@ def run_client(
|
|
|
196
382
|
build_mode: BuildMode = build_mode,
|
|
197
383
|
profile: bool = profile,
|
|
198
384
|
last_hash_value: str | None = None,
|
|
385
|
+
no_platformio: bool = no_platformio,
|
|
386
|
+
allow_libcompile: bool = allow_libcompile,
|
|
199
387
|
) -> CompileResult:
|
|
200
388
|
TEST_BEFORE_COMPILE(url)
|
|
201
389
|
return _run_web_compiler(
|
|
@@ -204,6 +392,8 @@ def run_client(
|
|
|
204
392
|
build_mode=build_mode,
|
|
205
393
|
profile=profile,
|
|
206
394
|
last_hash_value=last_hash_value,
|
|
395
|
+
no_platformio=no_platformio,
|
|
396
|
+
allow_libcompile=allow_libcompile,
|
|
207
397
|
)
|
|
208
398
|
|
|
209
399
|
result: CompileResult = compile_function(last_hash_value=None)
|
|
@@ -212,39 +402,67 @@ def run_client(
|
|
|
212
402
|
if not result.success:
|
|
213
403
|
print("\nCompilation failed.")
|
|
214
404
|
|
|
215
|
-
|
|
216
|
-
if open_web_browser:
|
|
217
|
-
|
|
405
|
+
use_http_server = http_port is None or http_port >= 0
|
|
406
|
+
if not use_http_server and open_web_browser:
|
|
407
|
+
warnings.warn(
|
|
408
|
+
f"Warning: --http-port={http_port} specified but open_web_browser is False, ignoring --http-port."
|
|
409
|
+
)
|
|
410
|
+
use_http_server = False
|
|
411
|
+
|
|
412
|
+
http_proc: Process | None = None
|
|
413
|
+
if use_http_server:
|
|
414
|
+
http_proc = spawn_http_server(
|
|
415
|
+
directory / "fastled_js",
|
|
416
|
+
port=http_port,
|
|
417
|
+
compile_server_port=port,
|
|
418
|
+
open_browser=open_web_browser,
|
|
419
|
+
app=app,
|
|
420
|
+
enable_https=enable_https,
|
|
421
|
+
)
|
|
218
422
|
else:
|
|
219
|
-
|
|
423
|
+
if result.success:
|
|
424
|
+
print("\nCompilation successful.")
|
|
425
|
+
else:
|
|
426
|
+
print("\nCompilation failed.")
|
|
220
427
|
if compile_server:
|
|
221
428
|
print("Shutting down compile server...")
|
|
222
429
|
compile_server.stop()
|
|
223
|
-
return 0
|
|
430
|
+
return 0 if result.success else 1
|
|
224
431
|
|
|
225
432
|
if not keep_running or shutdown.is_set():
|
|
226
|
-
if
|
|
227
|
-
|
|
433
|
+
if http_proc:
|
|
434
|
+
http_proc.kill()
|
|
228
435
|
return 0 if result.success else 1
|
|
229
436
|
except KeyboardInterrupt:
|
|
230
437
|
print("\nExiting from main")
|
|
231
438
|
return 1
|
|
232
439
|
|
|
233
|
-
|
|
440
|
+
excluded_patterns = ["fastled_js"]
|
|
441
|
+
debounced_sketch_watcher = DebouncedFileWatcherProcess(
|
|
442
|
+
FileWatcherProcess(directory, excluded_patterns=excluded_patterns),
|
|
443
|
+
)
|
|
234
444
|
|
|
235
445
|
source_code_watcher: FileWatcherProcess | None = None
|
|
236
446
|
if compile_server and compile_server.using_fastled_src_dir_volume():
|
|
237
447
|
assert compile_server.fastled_src_dir is not None
|
|
238
448
|
source_code_watcher = FileWatcherProcess(
|
|
239
|
-
compile_server.fastled_src_dir, excluded_patterns=
|
|
449
|
+
compile_server.fastled_src_dir, excluded_patterns=excluded_patterns
|
|
240
450
|
)
|
|
241
451
|
|
|
242
452
|
def trigger_rebuild_if_sketch_changed(
|
|
243
453
|
last_compiled_result: CompileResult,
|
|
244
454
|
) -> tuple[bool, CompileResult]:
|
|
245
|
-
changed_files =
|
|
455
|
+
changed_files = debounced_sketch_watcher.get_all_changes()
|
|
246
456
|
if changed_files:
|
|
247
|
-
|
|
457
|
+
# Filter out any fastled_js changes that slipped through
|
|
458
|
+
sketch_changes = [
|
|
459
|
+
f for f in changed_files if "fastled_js" not in Path(f).parts
|
|
460
|
+
]
|
|
461
|
+
if not sketch_changes:
|
|
462
|
+
# All changes were in fastled_js, ignore them
|
|
463
|
+
return False, last_compiled_result
|
|
464
|
+
print(f"\nChanges detected in {sketch_changes}")
|
|
465
|
+
print("Compiling...")
|
|
248
466
|
last_hash_value = last_compiled_result.hash_value
|
|
249
467
|
out = compile_function(last_hash_value=last_hash_value)
|
|
250
468
|
if not out.success:
|
|
@@ -265,6 +483,40 @@ def run_client(
|
|
|
265
483
|
if shutdown.is_set():
|
|
266
484
|
print("\nStopping watch mode...")
|
|
267
485
|
return 0
|
|
486
|
+
|
|
487
|
+
# Check for newer Docker image version after first successful compilation
|
|
488
|
+
if (
|
|
489
|
+
not has_checked_newer_version_yet
|
|
490
|
+
and last_compiled_result.success
|
|
491
|
+
and is_local_host
|
|
492
|
+
):
|
|
493
|
+
has_checked_newer_version_yet = True
|
|
494
|
+
try:
|
|
495
|
+
|
|
496
|
+
docker_manager = DockerManager()
|
|
497
|
+
has_update, message = docker_manager.has_newer_version(
|
|
498
|
+
image_name=IMAGE_NAME, tag="latest"
|
|
499
|
+
)
|
|
500
|
+
if has_update:
|
|
501
|
+
print(f"\n🔄 {message}")
|
|
502
|
+
if background_update:
|
|
503
|
+
# Start background update in a separate thread
|
|
504
|
+
update_thread = threading.Thread(
|
|
505
|
+
target=_background_update_docker_image, daemon=True
|
|
506
|
+
)
|
|
507
|
+
update_thread.start()
|
|
508
|
+
background_update = False
|
|
509
|
+
else:
|
|
510
|
+
print(
|
|
511
|
+
"Run with `fastled -u` to update the docker image to the latest version."
|
|
512
|
+
)
|
|
513
|
+
print(
|
|
514
|
+
"Or use `--background-update` to update automatically in the background after compilation."
|
|
515
|
+
)
|
|
516
|
+
except Exception as e:
|
|
517
|
+
# Don't let Docker check failures interrupt the main flow
|
|
518
|
+
warnings.warn(f"Failed to check for Docker image updates: {e}")
|
|
519
|
+
|
|
268
520
|
if SpaceBarWatcher.watch_space_bar_pressed(timeout=1.0):
|
|
269
521
|
print("Compiling...")
|
|
270
522
|
last_compiled_result = compile_function(last_hash_value=None)
|
|
@@ -292,17 +544,22 @@ def run_client(
|
|
|
292
544
|
if changed_files:
|
|
293
545
|
print(f"\nChanges detected in FastLED source code: {changed_files}")
|
|
294
546
|
print("Press space bar to trigger compile.")
|
|
547
|
+
|
|
295
548
|
while True:
|
|
296
549
|
space_bar_pressed = SpaceBarWatcher.watch_space_bar_pressed(
|
|
297
550
|
timeout=1.0
|
|
298
551
|
)
|
|
299
552
|
file_changes = source_code_watcher.get_all_changes()
|
|
300
|
-
sketch_files_changed =
|
|
553
|
+
sketch_files_changed = (
|
|
554
|
+
debounced_sketch_watcher.get_all_changes()
|
|
555
|
+
)
|
|
301
556
|
|
|
302
557
|
if file_changes:
|
|
303
558
|
print(
|
|
304
559
|
f"Changes detected in {file_changes}\nHit the space bar to trigger compile."
|
|
305
560
|
)
|
|
561
|
+
# Re-evaluate libcompile capability when source code changes
|
|
562
|
+
allow_libcompile = True
|
|
306
563
|
|
|
307
564
|
if space_bar_pressed or sketch_files_changed:
|
|
308
565
|
if space_bar_pressed:
|
|
@@ -312,7 +569,10 @@ def run_client(
|
|
|
312
569
|
f"Changes detected in {','.join(sketch_files_changed)}, triggering recompile..."
|
|
313
570
|
)
|
|
314
571
|
last_compiled_result = compile_function(
|
|
315
|
-
last_hash_value=None
|
|
572
|
+
last_hash_value=None, allow_libcompile=allow_libcompile
|
|
573
|
+
)
|
|
574
|
+
allow_libcompile = (
|
|
575
|
+
allow_libcompile and not last_compiled_result.success
|
|
316
576
|
)
|
|
317
577
|
print("Finished recompile.")
|
|
318
578
|
# Drain the space bar queue
|
|
@@ -327,24 +587,27 @@ def run_client(
|
|
|
327
587
|
print(f"Error: {e}")
|
|
328
588
|
return 1
|
|
329
589
|
finally:
|
|
330
|
-
|
|
590
|
+
debounced_sketch_watcher.stop()
|
|
331
591
|
if compile_server:
|
|
332
592
|
compile_server.stop()
|
|
333
|
-
if
|
|
334
|
-
|
|
593
|
+
if http_proc:
|
|
594
|
+
http_proc.kill()
|
|
335
595
|
|
|
336
596
|
|
|
337
|
-
def run_client_server(args:
|
|
597
|
+
def run_client_server(args: Args) -> int:
|
|
338
598
|
profile = bool(args.profile)
|
|
339
599
|
web: str | bool = args.web if isinstance(args.web, str) else bool(args.web)
|
|
340
600
|
auto_update = bool(args.auto_update)
|
|
601
|
+
background_update = bool(args.background_update)
|
|
341
602
|
localhost = bool(args.localhost)
|
|
342
|
-
directory = Path(
|
|
603
|
+
directory = args.directory if args.directory else Path(".")
|
|
343
604
|
just_compile = bool(args.just_compile)
|
|
344
605
|
interactive = bool(args.interactive)
|
|
345
606
|
force_compile = bool(args.force_compile)
|
|
346
607
|
open_web_browser = not just_compile and not interactive
|
|
347
608
|
build_mode: BuildMode = BuildMode.from_args(args)
|
|
609
|
+
no_platformio = bool(args.no_platformio)
|
|
610
|
+
app = bool(args.app)
|
|
348
611
|
|
|
349
612
|
if not force_compile and not looks_like_sketch_directory(directory):
|
|
350
613
|
# if there is only one directory in the sketch directory, use that
|
|
@@ -373,15 +636,27 @@ def run_client_server(args: argparse.Namespace) -> int:
|
|
|
373
636
|
url: str
|
|
374
637
|
compile_server: CompileServer | None = None
|
|
375
638
|
try:
|
|
376
|
-
url, compile_server = _try_start_server_or_get_url(
|
|
639
|
+
url, compile_server = _try_start_server_or_get_url(
|
|
640
|
+
auto_update, web, localhost, args.clear, no_platformio
|
|
641
|
+
)
|
|
377
642
|
except KeyboardInterrupt:
|
|
378
643
|
print("\nExiting from first try...")
|
|
379
|
-
if compile_server:
|
|
644
|
+
if compile_server is not None:
|
|
380
645
|
compile_server.stop()
|
|
381
646
|
return 1
|
|
382
647
|
except Exception as e:
|
|
383
|
-
|
|
384
|
-
|
|
648
|
+
stack_trace = e.__traceback__
|
|
649
|
+
stack_trace_list: list[str] | None = (
|
|
650
|
+
traceback.format_exception(type(e), e, stack_trace) if stack_trace else None
|
|
651
|
+
)
|
|
652
|
+
stack_trace_str = ""
|
|
653
|
+
if stack_trace_list is not None:
|
|
654
|
+
stack_trace_str = "".join(stack_trace_list) if stack_trace_list else ""
|
|
655
|
+
if stack_trace:
|
|
656
|
+
print(f"Error in starting compile server: {e}\n{stack_trace_str}")
|
|
657
|
+
else:
|
|
658
|
+
print(f"Error: {e}")
|
|
659
|
+
if compile_server is not None:
|
|
385
660
|
compile_server.stop()
|
|
386
661
|
return 1
|
|
387
662
|
|
|
@@ -393,6 +668,10 @@ def run_client_server(args: argparse.Namespace) -> int:
|
|
|
393
668
|
keep_running=not just_compile,
|
|
394
669
|
build_mode=build_mode,
|
|
395
670
|
profile=profile,
|
|
671
|
+
clear=args.clear,
|
|
672
|
+
no_platformio=no_platformio,
|
|
673
|
+
app=app,
|
|
674
|
+
background_update=background_update,
|
|
396
675
|
)
|
|
397
676
|
except KeyboardInterrupt:
|
|
398
677
|
return 1
|
fastled/compile_server.py
CHANGED
|
@@ -15,6 +15,8 @@ class CompileServer:
|
|
|
15
15
|
container_name: str | None = None,
|
|
16
16
|
platform: Platform = Platform.WASM,
|
|
17
17
|
remove_previous: bool = False,
|
|
18
|
+
no_platformio: bool = False,
|
|
19
|
+
allow_libcompile: bool = True,
|
|
18
20
|
) -> None:
|
|
19
21
|
from fastled.compile_server_impl import ( # avoid circular import
|
|
20
22
|
CompileServerImpl,
|
|
@@ -29,6 +31,8 @@ class CompileServer:
|
|
|
29
31
|
mapped_dir=mapped_dir,
|
|
30
32
|
auto_start=auto_start,
|
|
31
33
|
remove_previous=remove_previous,
|
|
34
|
+
no_platformio=no_platformio,
|
|
35
|
+
allow_libcompile=allow_libcompile,
|
|
32
36
|
)
|
|
33
37
|
|
|
34
38
|
# May throw CompileServerError if server could not be started.
|
|
@@ -59,7 +63,8 @@ class CompileServer:
|
|
|
59
63
|
|
|
60
64
|
@property
|
|
61
65
|
def running(self) -> bool:
|
|
62
|
-
|
|
66
|
+
ok, _ = self.impl.running
|
|
67
|
+
return ok
|
|
63
68
|
|
|
64
69
|
@property
|
|
65
70
|
def fastled_src_dir(self) -> Path | None:
|
|
@@ -96,3 +101,9 @@ class CompileServer:
|
|
|
96
101
|
|
|
97
102
|
def process_running(self) -> bool:
|
|
98
103
|
return self.impl.process_running()
|
|
104
|
+
|
|
105
|
+
def get_emsdk_headers(self, filepath: Path) -> None:
|
|
106
|
+
"""Get EMSDK headers ZIP data from the server and save to filepath."""
|
|
107
|
+
if not str(filepath).endswith(".zip"):
|
|
108
|
+
raise ValueError("Filepath must end with .zip")
|
|
109
|
+
return self.impl.get_emsdk_headers(filepath)
|