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/web_compile.py CHANGED
@@ -1,34 +1,23 @@
1
- import _thread
2
1
  import io
3
- import json
4
2
  import os
5
3
  import shutil
6
4
  import tempfile
5
+ import time
7
6
  import zipfile
8
- from concurrent.futures import Future, ThreadPoolExecutor, as_completed
9
- from dataclasses import dataclass
10
7
  from pathlib import Path
11
8
 
12
9
  import httpx
13
10
 
14
- from fastled.settings import SERVER_PORT
15
- from fastled.sketch import get_sketch_files
11
+ from fastled.emoji_util import EMO, safe_print
12
+ from fastled.find_good_connection import find_good_connection
13
+ from fastled.interruptible_http import make_interruptible_post_request
14
+ from fastled.settings import AUTH_TOKEN, SERVER_PORT
16
15
  from fastled.types import BuildMode, CompileResult
17
- from fastled.util import hash_file
16
+ from fastled.zip_files import ZipResult, zip_files
18
17
 
19
18
  DEFAULT_HOST = "https://fastled.onrender.com"
20
19
  ENDPOINT_COMPILED_WASM = "compile/wasm"
21
- _TIMEOUT = 60 * 4 # 2 mins timeout
22
- _AUTH_TOKEN = "oBOT5jbsO4ztgrpNsQwlmFLIKB"
23
- ENABLE_EMBEDDED_DATA = True
24
- _EXECUTOR = ThreadPoolExecutor(max_workers=8)
25
-
26
-
27
- @dataclass
28
- class ConnectionResult:
29
- host: str
30
- success: bool
31
- ipv4: bool
20
+ _TIMEOUT = 60 * 4 # 4 mins timeout
32
21
 
33
22
 
34
23
  def _sanitize_host(host: str) -> str:
@@ -41,116 +30,272 @@ def _sanitize_host(host: str) -> str:
41
30
  return host if host.startswith("http://") else f"http://{host}"
42
31
 
43
32
 
44
- def _test_connection(host: str, use_ipv4: bool) -> ConnectionResult:
45
- # Function static cache
46
- host = _sanitize_host(host)
47
- transport = httpx.HTTPTransport(local_address="0.0.0.0") if use_ipv4 else None
48
- try:
49
- with httpx.Client(
50
- timeout=_TIMEOUT,
51
- transport=transport,
52
- ) as test_client:
53
- test_response = test_client.get(
54
- f"{host}/healthz", timeout=3, follow_redirects=True
55
- )
56
- result = ConnectionResult(host, test_response.status_code == 200, use_ipv4)
57
- except KeyboardInterrupt:
58
- _thread.interrupt_main()
33
+ def _check_embedded_http_status(response_content: bytes) -> tuple[bool, int | None]:
34
+ """
35
+ Check if the response content has an embedded HTTP status at the end.
59
36
 
60
- except TimeoutError:
61
- result = ConnectionResult(host, False, use_ipv4)
37
+ Returns:
38
+ tuple: (has_embedded_status, status_code)
39
+ has_embedded_status is True if an embedded status was found
40
+ status_code is the embedded status code or None if not found
41
+ """
42
+ try:
43
+ # Convert bytes to string for parsing
44
+ content_str = response_content.decode("utf-8", errors="ignore")
45
+
46
+ # Look for HTTP_STATUS: at the end of the content
47
+ lines = content_str.strip().split("\n")
48
+ if lines:
49
+ last_line = lines[-1].strip()
50
+ if "HTTP_STATUS:" in last_line:
51
+ try:
52
+ right = last_line.split("HTTP_STATUS:")[-1].strip()
53
+ status_code = int(right)
54
+ return True, status_code
55
+ except (ValueError, IndexError):
56
+ # Malformed status line
57
+ return True, None
58
+
59
+ return False, None
62
60
  except Exception:
63
- result = ConnectionResult(host, False, use_ipv4)
64
- return result
61
+ # If we can't parse the content, assume no embedded status
62
+ return False, None
63
+
64
+
65
+ def _banner(msg: str) -> str:
66
+ """
67
+ Create a banner for the given message.
68
+ Example:
69
+ msg = "Hello, World!"
70
+ print -> "#################"
71
+ "# Hello, World! #"
72
+ "#################"
73
+ """
74
+ lines = msg.split("\n")
75
+ # Find the width of the widest line
76
+ max_width = max(len(line) for line in lines)
77
+ width = max_width + 4 # Add 4 for "# " and " #"
78
+
79
+ # Create the top border
80
+ banner = "\n" + "#" * width + "\n"
81
+
82
+ # Add each line with proper padding
83
+ for line in lines:
84
+ padding = max_width - len(line)
85
+ banner += f"# {line}{' ' * padding} #\n"
86
+
87
+ # Add the bottom border
88
+ banner += "#" * width + "\n"
89
+ return f"\n{banner}\n"
90
+
91
+
92
+ def _print_banner(msg: str) -> None:
93
+ safe_print(_banner(msg))
94
+
95
+
96
+ def _compile_libfastled(
97
+ host: str,
98
+ auth_token: str,
99
+ build_mode: BuildMode,
100
+ ) -> httpx.Response:
101
+ """Compile the FastLED library separately."""
102
+ host = _sanitize_host(host)
103
+ urls = [host]
104
+ domain = host.split("://")[-1]
105
+ if ":" not in domain:
106
+ urls.append(f"{host}:{SERVER_PORT}")
107
+
108
+ connection_result = find_good_connection(urls)
109
+ if connection_result is None:
110
+ raise ConnectionError(
111
+ "Connection failed to all endpoints for libfastled compilation"
112
+ )
65
113
 
114
+ ipv4_stmt = "IPv4" if connection_result.ipv4 else "IPv6"
115
+ transport = (
116
+ httpx.HTTPTransport(local_address="0.0.0.0") if connection_result.ipv4 else None
117
+ )
118
+
119
+ headers = {
120
+ "accept": "application/json",
121
+ "authorization": auth_token,
122
+ "build": build_mode.value.lower(),
123
+ }
124
+
125
+ url = f"{connection_result.host}/compile/libfastled"
126
+ print(f"Compiling libfastled on {url} via {ipv4_stmt}")
127
+
128
+ # Use interruptible HTTP request
129
+ response = make_interruptible_post_request(
130
+ url=url,
131
+ files={}, # No files for libfastled compilation
132
+ headers=headers,
133
+ transport=transport,
134
+ timeout=_TIMEOUT * 2, # Give more time for library compilation
135
+ follow_redirects=False,
136
+ )
137
+
138
+ return response
139
+
140
+
141
+ def _send_compile_request(
142
+ host: str,
143
+ zip_bytes: bytes,
144
+ auth_token: str,
145
+ build_mode: BuildMode,
146
+ profile: bool,
147
+ no_platformio: bool,
148
+ ) -> httpx.Response:
149
+ """Send the compile request to the server and return the response."""
150
+ host = _sanitize_host(host)
151
+ urls = [host]
152
+ domain = host.split("://")[-1]
153
+ if ":" not in domain:
154
+ urls.append(f"{host}:{SERVER_PORT}")
66
155
 
67
- def _file_info(file_path: Path) -> str:
68
- hash_txt = hash_file(file_path)
69
- file_size = file_path.stat().st_size
70
- json_str = json.dumps({"hash": hash_txt, "size": file_size})
71
- return json_str
156
+ connection_result = find_good_connection(urls)
157
+ if connection_result is None:
158
+ raise ConnectionError("Connection failed to all endpoints")
72
159
 
160
+ ipv4_stmt = "IPv4" if connection_result.ipv4 else "IPv6"
161
+ transport = (
162
+ httpx.HTTPTransport(local_address="0.0.0.0") if connection_result.ipv4 else None
163
+ )
73
164
 
74
- @dataclass
75
- class ZipResult:
76
- zip_bytes: bytes
77
- zip_embedded_bytes: bytes | None
78
- success: bool
79
- error: str | None
165
+ archive_size = len(zip_bytes)
80
166
 
167
+ headers = {
168
+ "accept": "application/json",
169
+ "authorization": auth_token,
170
+ "build": (
171
+ build_mode.value.lower() if build_mode else BuildMode.QUICK.value.lower()
172
+ ),
173
+ "profile": "true" if profile else "false",
174
+ "no-platformio": "true" if no_platformio else "false",
175
+ "allow-libcompile": "false", # Always false since we handle it manually
176
+ }
177
+
178
+ url = f"{connection_result.host}/{ENDPOINT_COMPILED_WASM}"
179
+ print(f"Compiling sketch on {url} via {ipv4_stmt}. Zip size: {archive_size} bytes")
180
+ files = {"file": ("wasm.zip", zip_bytes, "application/x-zip-compressed")}
181
+
182
+ # Use interruptible HTTP request
183
+ response = make_interruptible_post_request(
184
+ url=url,
185
+ files=files,
186
+ headers=headers,
187
+ transport=transport,
188
+ timeout=_TIMEOUT,
189
+ follow_redirects=True,
190
+ )
191
+
192
+ return response
193
+
194
+
195
+ def _process_compile_response(
196
+ response: httpx.Response,
197
+ zip_result: ZipResult,
198
+ start_time: float,
199
+ zip_time: float,
200
+ libfastled_time: float,
201
+ sketch_time: float,
202
+ ) -> CompileResult:
203
+ """Process the compile response and return the final result."""
204
+ if response.status_code != 200:
205
+ json_response = response.json()
206
+ detail = json_response.get("detail", "Could not compile")
207
+ return CompileResult(
208
+ success=False,
209
+ stdout=detail,
210
+ hash_value=None,
211
+ zip_bytes=b"",
212
+ zip_time=zip_time,
213
+ libfastled_time=libfastled_time,
214
+ sketch_time=sketch_time,
215
+ response_processing_time=0.0, # No response processing in error case
216
+ )
81
217
 
82
- def zip_files(directory: Path, build_mode: BuildMode) -> ZipResult | Exception:
83
- print("Zipping files...")
84
- try:
85
- files = get_sketch_files(directory)
86
- if not files:
87
- raise FileNotFoundError(f"No files found in {directory}")
88
- for f in files:
89
- print(f"Adding file: {f}")
90
- # Create in-memory zip file
91
- has_embedded_zip = False
92
- zip_embedded_buffer = io.BytesIO()
93
- zip_buffer = io.BytesIO()
218
+ print(f"Response status code: {response}")
219
+
220
+ # Time the response processing
221
+ response_processing_start = time.time()
222
+
223
+ # Create a temporary directory to extract the zip
224
+ with tempfile.TemporaryDirectory() as extract_dir:
225
+ extract_path = Path(extract_dir)
226
+
227
+ # Write the response content to a temporary zip file
228
+ temp_zip = extract_path / "response.zip"
229
+ temp_zip.write_bytes(response.content)
230
+
231
+ # Extract the zip
232
+ shutil.unpack_archive(temp_zip, extract_path, "zip")
233
+
234
+ if zip_result.zip_embedded_bytes:
235
+ # extract the embedded bytes, which were not sent to the server
236
+ temp_zip.write_bytes(zip_result.zip_embedded_bytes)
237
+ shutil.unpack_archive(temp_zip, extract_path, "zip")
238
+
239
+ # we don't need the temp zip anymore
240
+ temp_zip.unlink()
241
+
242
+ # Read stdout from out.txt if it exists
243
+ stdout_file = extract_path / "out.txt"
244
+ hash_file = extract_path / "hash.txt"
245
+ stdout = (
246
+ stdout_file.read_text(encoding="utf-8", errors="replace")
247
+ if stdout_file.exists()
248
+ else ""
249
+ )
250
+ hash_value = (
251
+ hash_file.read_text(encoding="utf-8", errors="replace")
252
+ if hash_file.exists()
253
+ else None
254
+ )
255
+
256
+ # now rezip the extracted files since we added the embedded json files
257
+ out_buffer = io.BytesIO()
94
258
  with zipfile.ZipFile(
95
- zip_embedded_buffer, "w", zipfile.ZIP_DEFLATED, compresslevel=9
96
- ) as emebedded_zip_file:
97
- with zipfile.ZipFile(
98
- zip_buffer, "w", zipfile.ZIP_DEFLATED, compresslevel=9
99
- ) as zip_file:
100
- for file_path in files:
101
- relative_path = file_path.relative_to(directory)
102
- achive_path = str(Path("wasm") / relative_path)
103
- if str(relative_path).startswith("data") and ENABLE_EMBEDDED_DATA:
104
- _file_info_str = _file_info(file_path)
105
- zip_file.writestr(
106
- achive_path + ".embedded.json", _file_info_str
107
- )
108
- emebedded_zip_file.write(file_path, relative_path)
109
- has_embedded_zip = True
110
- else:
111
- zip_file.write(file_path, achive_path)
112
- # write build mode into the file as build.txt so that sketches are fingerprinted
113
- # based on the build mode. Otherwise the same sketch with different build modes
114
- # will have the same fingerprint.
115
- zip_file.writestr(
116
- str(Path("wasm") / "build_mode.txt"), build_mode.value
117
- )
118
- result = ZipResult(
119
- zip_bytes=zip_buffer.getvalue(),
120
- zip_embedded_bytes=(
121
- zip_embedded_buffer.getvalue() if has_embedded_zip else None
122
- ),
259
+ out_buffer, "w", zipfile.ZIP_DEFLATED, compresslevel=9
260
+ ) as out_zip:
261
+ for root, _, _files in os.walk(extract_path):
262
+ for file in _files:
263
+ file_path = Path(root) / file
264
+ relative_path = file_path.relative_to(extract_path)
265
+ out_zip.write(file_path, relative_path)
266
+
267
+ response_processing_time = time.time() - response_processing_start
268
+ diff_time = time.time() - start_time
269
+
270
+ # Create detailed timing breakdown
271
+ unaccounted_time = diff_time - (
272
+ zip_time + libfastled_time + sketch_time + response_processing_time
273
+ )
274
+ msg = f"Compilation success, took {diff_time:.2f} seconds\n"
275
+ msg += f" zip creation: {zip_time:.2f}\n"
276
+ if libfastled_time > 0:
277
+ msg += f" libfastled: {libfastled_time:.2f}\n"
278
+ msg += f" sketch compile + link: {sketch_time:.2f}\n"
279
+ msg += f" response processing: {response_processing_time:.2f}\n"
280
+ if unaccounted_time > 0.01: # Only show if significant
281
+ msg += f" other overhead: {unaccounted_time:.2f}"
282
+
283
+ _print_banner(msg)
284
+ return CompileResult(
123
285
  success=True,
124
- error=None,
286
+ stdout=stdout,
287
+ hash_value=hash_value,
288
+ zip_bytes=out_buffer.getvalue(),
289
+ zip_time=zip_time,
290
+ libfastled_time=libfastled_time,
291
+ sketch_time=sketch_time,
292
+ response_processing_time=response_processing_time,
125
293
  )
126
- return result
127
- except Exception as e:
128
- return e
129
-
130
294
 
131
- def find_good_connection(
132
- urls: list[str], filter_out_bad=True, use_ipv6: bool = True
133
- ) -> ConnectionResult | None:
134
- futures: list[Future] = []
135
- for url in urls:
136
295
 
137
- f = _EXECUTOR.submit(_test_connection, url, use_ipv4=True)
138
- futures.append(f)
139
- if use_ipv6 and "localhost" not in url:
140
- f_v6 = _EXECUTOR.submit(_test_connection, url, use_ipv4=False)
141
- futures.append(f_v6)
142
-
143
- try:
144
- # Return first successful result
145
- for future in as_completed(futures):
146
- result: ConnectionResult = future.result()
147
- if result.success or not filter_out_bad:
148
- return result
149
- finally:
150
- # Cancel any remaining futures
151
- for future in futures:
152
- future.cancel()
153
- return None
296
+ def _libcompiled_is_allowed(host: str) -> bool:
297
+ """Check if libcompiled is allowed for the given host."""
298
+ return "localhost" in host or "127.0.0.1" in host or "0.0.0.0" in host
154
299
 
155
300
 
156
301
  def web_compile(
@@ -159,127 +304,165 @@ def web_compile(
159
304
  auth_token: str | None = None,
160
305
  build_mode: BuildMode | None = None,
161
306
  profile: bool = False,
307
+ no_platformio: bool = False,
308
+ allow_libcompile: bool = True,
162
309
  ) -> CompileResult:
310
+ start_time = time.time()
163
311
  if isinstance(directory, str):
164
312
  directory = Path(directory)
165
313
  host = _sanitize_host(host or DEFAULT_HOST)
166
314
  build_mode = build_mode or BuildMode.QUICK
167
- print("Compiling on", host)
168
- auth_token = auth_token or _AUTH_TOKEN
315
+ _print_banner(f"Compiling on {host}")
316
+ auth_token = auth_token or AUTH_TOKEN
169
317
  if not directory.exists():
170
318
  raise FileNotFoundError(f"Directory not found: {directory}")
171
- zip_result = zip_files(directory, build_mode=build_mode)
319
+
320
+ if allow_libcompile and not _libcompiled_is_allowed(host):
321
+ print(
322
+ f"{EMO('🚫', '[ERROR]')} libcompile is not allowed for host {host}, disabling."
323
+ )
324
+ allow_libcompile = False
325
+
326
+ # Time the zip creation
327
+ zip_start_time = time.time()
328
+ zip_result: ZipResult | Exception = zip_files(directory, build_mode=build_mode)
329
+ zip_time = time.time() - zip_start_time
330
+
172
331
  if isinstance(zip_result, Exception):
173
332
  return CompileResult(
174
- success=False, stdout=str(zip_result), hash_value=None, zip_bytes=b""
333
+ success=False,
334
+ stdout=str(zip_result),
335
+ hash_value=None,
336
+ zip_bytes=b"",
337
+ zip_time=zip_time,
338
+ libfastled_time=0.0, # No libfastled compilation in zip error case
339
+ sketch_time=0.0, # No sketch compilation in zip error case
340
+ response_processing_time=0.0, # No response processing in zip error case
175
341
  )
176
342
  zip_bytes = zip_result.zip_bytes
177
- archive_size = len(zip_bytes)
178
343
  print(f"Web compiling on {host}...")
344
+
345
+ # Track timing for each step
346
+ libfastled_time = 0.0
347
+ sketch_time = 0.0
348
+
179
349
  try:
180
- host = _sanitize_host(host)
181
- urls = [host]
182
- domain = host.split("://")[-1]
183
- if ":" not in domain:
184
- urls.append(f"{host}:{SERVER_PORT}")
185
-
186
- connection_result = find_good_connection(urls)
187
- if connection_result is None:
188
- print("Connection failed to all endpoints")
189
- return CompileResult(
190
- success=False,
191
- stdout="Connection failed",
192
- hash_value=None,
193
- zip_bytes=b"",
194
- )
195
-
196
- ipv4_stmt = "IPv4" if connection_result.ipv4 else "IPv6"
197
- transport = (
198
- httpx.HTTPTransport(local_address="0.0.0.0")
199
- if connection_result.ipv4
200
- else None
350
+ # Step 1: Compile libfastled if requested
351
+ if allow_libcompile:
352
+ print("Step 1: Compiling libfastled...")
353
+ libfastled_start_time = time.time()
354
+ try:
355
+ libfastled_response = _compile_libfastled(host, auth_token, build_mode)
356
+
357
+ # Check HTTP response status first
358
+ if libfastled_response.status_code != 200:
359
+ msg = f"Error: libfastled compilation failed with HTTP status {libfastled_response.status_code}"
360
+
361
+ # Error out here, this is a critical error
362
+ stdout = libfastled_response.content
363
+ if stdout is not None:
364
+ stdout = stdout.decode("utf-8", errors="replace") + "\n" + msg
365
+ else:
366
+ stdout = msg
367
+ return CompileResult(
368
+ success=False,
369
+ stdout=stdout,
370
+ hash_value=None,
371
+ zip_bytes=b"",
372
+ zip_time=zip_time,
373
+ libfastled_time=libfastled_time,
374
+ sketch_time=0.0, # No sketch compilation when libfastled fails
375
+ response_processing_time=0.0, # No response processing when libfastled fails
376
+ )
377
+ else:
378
+ # Check for embedded HTTP status in response content
379
+ has_embedded_status, embedded_status = _check_embedded_http_status(
380
+ libfastled_response.content
381
+ )
382
+ if has_embedded_status:
383
+ if embedded_status is not None and embedded_status != 200:
384
+ msg = f"Warning: libfastled compilation failed with embedded status {embedded_status}"
385
+ # this is a critical error
386
+ stdout = libfastled_response.content
387
+ if stdout is not None:
388
+ stdout = (
389
+ stdout.decode("utf-8", errors="replace")
390
+ + "\n"
391
+ + msg
392
+ )
393
+ else:
394
+ stdout = msg
395
+ return CompileResult(
396
+ success=False,
397
+ stdout=stdout,
398
+ hash_value=None,
399
+ zip_bytes=b"",
400
+ zip_time=zip_time,
401
+ libfastled_time=libfastled_time,
402
+ sketch_time=0.0, # No sketch compilation when libfastled fails
403
+ response_processing_time=0.0, # No response processing when libfastled fails
404
+ )
405
+ # Continue with sketch compilation even if libfastled fails
406
+ elif embedded_status is None:
407
+ print(
408
+ "Warning: libfastled compilation returned malformed embedded status"
409
+ )
410
+ # Continue with sketch compilation even if libfastled fails
411
+ else:
412
+ print("✅ libfastled compilation successful")
413
+ else:
414
+ print("✅ libfastled compilation successful")
415
+ libfastled_time = time.time() - libfastled_start_time
416
+ except Exception as e:
417
+ libfastled_time = time.time() - libfastled_start_time
418
+ print(f"Warning: libfastled compilation failed: {e}")
419
+ # Continue with sketch compilation even if libfastled fails
420
+ else:
421
+ print("Step 1 (skipped): Compiling libfastled")
422
+
423
+ # Step 2: Compile the sketch
424
+ print("Step 2: Compiling sketch...")
425
+ sketch_start_time = time.time()
426
+ response = _send_compile_request(
427
+ host,
428
+ zip_bytes,
429
+ auth_token,
430
+ build_mode,
431
+ profile,
432
+ no_platformio,
201
433
  )
202
- with httpx.Client(
203
- transport=transport,
204
- timeout=_TIMEOUT,
205
- ) as client:
206
- headers = {
207
- "accept": "application/json",
208
- "authorization": auth_token,
209
- "build": (
210
- build_mode.value.lower()
211
- if build_mode
212
- else BuildMode.QUICK.value.lower()
213
- ),
214
- "profile": "true" if profile else "false",
215
- }
216
-
217
- url = f"{connection_result.host}/{ENDPOINT_COMPILED_WASM}"
218
- print(f"Compiling on {url} via {ipv4_stmt}. Zip size: {archive_size} bytes")
219
- files = {"file": ("wasm.zip", zip_bytes, "application/x-zip-compressed")}
220
- response = client.post(
221
- url,
222
- follow_redirects=True,
223
- files=files,
224
- headers=headers,
225
- timeout=_TIMEOUT,
226
- )
227
-
228
- if response.status_code != 200:
229
- json_response = response.json()
230
- detail = json_response.get("detail", "Could not compile")
231
- return CompileResult(
232
- success=False, stdout=detail, hash_value=None, zip_bytes=b""
233
- )
234
-
235
- print(f"Response status code: {response}")
236
- # Create a temporary directory to extract the zip
237
- with tempfile.TemporaryDirectory() as extract_dir:
238
- extract_path = Path(extract_dir)
239
-
240
- # Write the response content to a temporary zip file
241
- temp_zip = extract_path / "response.zip"
242
- temp_zip.write_bytes(response.content)
243
-
244
- # Extract the zip
245
- shutil.unpack_archive(temp_zip, extract_path, "zip")
246
-
247
- if zip_result.zip_embedded_bytes:
248
- # extract the embedded bytes, which were not sent to the server
249
- temp_zip.write_bytes(zip_result.zip_embedded_bytes)
250
- shutil.unpack_archive(temp_zip, extract_path, "zip")
251
-
252
- # we don't need the temp zip anymore
253
- temp_zip.unlink()
254
-
255
- # Read stdout from out.txt if it exists
256
- stdout_file = extract_path / "out.txt"
257
- hash_file = extract_path / "hash.txt"
258
- stdout = stdout_file.read_text() if stdout_file.exists() else ""
259
- hash_value = hash_file.read_text() if hash_file.exists() else None
260
-
261
- # now rezip the extracted files since we added the embedded json files
262
- out_buffer = io.BytesIO()
263
- with zipfile.ZipFile(
264
- out_buffer, "w", zipfile.ZIP_DEFLATED, compresslevel=9
265
- ) as out_zip:
266
- for root, _, _files in os.walk(extract_path):
267
- for file in _files:
268
- file_path = Path(root) / file
269
- relative_path = file_path.relative_to(extract_path)
270
- out_zip.write(file_path, relative_path)
271
-
272
- return CompileResult(
273
- success=True,
274
- stdout=stdout,
275
- hash_value=hash_value,
276
- zip_bytes=out_buffer.getvalue(),
277
- )
434
+ sketch_time = time.time() - sketch_start_time
435
+
436
+ return _process_compile_response(
437
+ response, zip_result, start_time, zip_time, libfastled_time, sketch_time
438
+ )
439
+
278
440
  except KeyboardInterrupt:
279
441
  print("Keyboard interrupt")
280
442
  raise
443
+ except (ConnectionError, httpx.RemoteProtocolError, httpx.RequestError) as e:
444
+ # Handle connection and server disconnection issues
445
+ error_msg = f"Server connection error: {e}"
446
+ _print_banner(error_msg)
447
+ return CompileResult(
448
+ success=False,
449
+ stdout=error_msg,
450
+ hash_value=None,
451
+ zip_bytes=b"",
452
+ zip_time=zip_time,
453
+ libfastled_time=libfastled_time,
454
+ sketch_time=sketch_time,
455
+ response_processing_time=0.0,
456
+ )
281
457
  except httpx.HTTPError as e:
282
458
  print(f"Error: {e}")
283
459
  return CompileResult(
284
- success=False, stdout=str(e), hash_value=None, zip_bytes=b""
460
+ success=False,
461
+ stdout=str(e),
462
+ hash_value=None,
463
+ zip_bytes=b"",
464
+ zip_time=zip_time,
465
+ libfastled_time=libfastled_time,
466
+ sketch_time=sketch_time,
467
+ response_processing_time=0.0, # No response processing in HTTP error case
285
468
  )