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.
Files changed (58) hide show
  1. fastled/__init__.py +51 -192
  2. fastled/__main__.py +14 -0
  3. fastled/__version__.py +6 -0
  4. fastled/app.py +124 -27
  5. fastled/args.py +124 -0
  6. fastled/assets/localhost-key.pem +28 -0
  7. fastled/assets/localhost.pem +27 -0
  8. fastled/cli.py +10 -2
  9. fastled/cli_test.py +21 -0
  10. fastled/cli_test_interactive.py +21 -0
  11. fastled/client_server.py +334 -55
  12. fastled/compile_server.py +12 -1
  13. fastled/compile_server_impl.py +115 -42
  14. fastled/docker_manager.py +392 -69
  15. fastled/emoji_util.py +27 -0
  16. fastled/filewatcher.py +100 -8
  17. fastled/find_good_connection.py +105 -0
  18. fastled/header_dump.py +63 -0
  19. fastled/install/__init__.py +1 -0
  20. fastled/install/examples_manager.py +62 -0
  21. fastled/install/extension_manager.py +113 -0
  22. fastled/install/main.py +156 -0
  23. fastled/install/project_detection.py +167 -0
  24. fastled/install/test_install.py +373 -0
  25. fastled/install/vscode_config.py +344 -0
  26. fastled/interruptible_http.py +148 -0
  27. fastled/keyboard.py +1 -0
  28. fastled/keyz.py +84 -0
  29. fastled/live_client.py +26 -1
  30. fastled/open_browser.py +133 -89
  31. fastled/parse_args.py +219 -15
  32. fastled/playwright/chrome_extension_downloader.py +207 -0
  33. fastled/playwright/playwright_browser.py +773 -0
  34. fastled/playwright/resize_tracking.py +127 -0
  35. fastled/print_filter.py +52 -0
  36. fastled/project_init.py +20 -13
  37. fastled/select_sketch_directory.py +142 -17
  38. fastled/server_flask.py +487 -0
  39. fastled/server_start.py +21 -0
  40. fastled/settings.py +53 -4
  41. fastled/site/build.py +2 -10
  42. fastled/site/examples.py +10 -0
  43. fastled/sketch.py +129 -7
  44. fastled/string_diff.py +218 -9
  45. fastled/test/examples.py +7 -5
  46. fastled/types.py +22 -2
  47. fastled/util.py +78 -0
  48. fastled/version.py +41 -0
  49. fastled/web_compile.py +401 -218
  50. fastled/zip_files.py +76 -0
  51. {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/METADATA +533 -382
  52. fastled-1.4.50.dist-info/RECORD +60 -0
  53. {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/WHEEL +1 -1
  54. fastled/open_browser2.py +0 -111
  55. fastled-1.2.33.dist-info/RECORD +0 -33
  56. {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/entry_points.txt +0 -0
  57. {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info/licenses}/LICENSE +0 -0
  58. {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.filewatcher import FileWatcherProcess
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 open_browser_process
14
- from fastled.settings import DEFAULT_URL
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, host=host, build_mode=build_mode, profile=profile
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
- print(web_result.stdout)
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 Time: {diff:.2f}\n output: {output_dir}\n hash: {hash_value}\n zip size: {len(web_result.zip_bytes)} bytes"
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
- # Extract zip contents to fastled_js directory
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, args_web: str | bool, localhost: 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
- urls.append(f"{addr}:{SERVER_PORT}")
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(auto_updates=auto_update)
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 RuntimeError:
164
- print("Failed to start local compile server, using web compiler instead.")
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
- browser_proc: Process | None = None
216
- if open_web_browser:
217
- browser_proc = open_browser_process(directory / "fastled_js")
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
- print("\nCompilation successful.")
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 browser_proc:
227
- browser_proc.kill()
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
- sketch_filewatcher = FileWatcherProcess(directory, excluded_patterns=["fastled_js"])
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 = sketch_filewatcher.get_all_changes()
455
+ changed_files = debounced_sketch_watcher.get_all_changes()
246
456
  if changed_files:
247
- print(f"\nChanges detected in {changed_files}")
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 = sketch_filewatcher.get_all_changes()
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
- sketch_filewatcher.stop()
590
+ debounced_sketch_watcher.stop()
331
591
  if compile_server:
332
592
  compile_server.stop()
333
- if browser_proc:
334
- browser_proc.kill()
593
+ if http_proc:
594
+ http_proc.kill()
335
595
 
336
596
 
337
- def run_client_server(args: argparse.Namespace) -> int:
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(args.directory)
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(auto_update, web, localhost)
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
- print(f"Error: {e}")
384
- if compile_server:
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
- return self.impl.running
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)