microweb 0.1.1__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.
microweb/__init__.py ADDED
File without changes
microweb/cli.py ADDED
@@ -0,0 +1,525 @@
1
+ import click
2
+ import serial.tools.list_ports
3
+ import esptool
4
+ import subprocess
5
+ import time
6
+ import os
7
+ import re
8
+ import pkg_resources
9
+ from microweb.uploader import upload_file, create_directory, verify_files
10
+
11
+ # ANSI color codes for enhanced terminal output
12
+ COLORS = {
13
+ 'reset': '\033[0m',
14
+ 'bold': '\033[1m',
15
+ 'red': '\033[91m',
16
+ 'green': '\033[92m',
17
+ 'yellow': '\033[93m',
18
+ 'blue': '\033[94m',
19
+ 'magenta': '\033[95m',
20
+ 'cyan': '\033[96m'
21
+ }
22
+
23
+ STYLES = {
24
+ 'underline': '\033[4m',
25
+ 'blink': '\033[5m',
26
+ }
27
+
28
+ def print_colored(message, color=None, style=None):
29
+ """Print a message with optional color and style."""
30
+ prefix = ''
31
+ if color in COLORS:
32
+ prefix += COLORS[color]
33
+ if style in STYLES:
34
+ prefix += STYLES[style]
35
+ click.echo(f"{prefix}{message}{COLORS['reset']}")
36
+
37
+ def check_micropython(port):
38
+ """Check if MicroPython is responding via mpremote on the given port."""
39
+ try:
40
+ cmd = ['mpremote', 'connect', port, 'eval', 'print("MicroPython detected")']
41
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
42
+ if result.returncode == 0 and "MicroPython detected" in result.stdout:
43
+ print_colored(f"MicroPython detected on {port}", color='green')
44
+ return True
45
+ else:
46
+ print_colored(f"mpremote output:\n{result.stdout.strip()}\n{result.stderr.strip()}", color='yellow')
47
+ return False
48
+ except Exception as e:
49
+ print_colored(f"Error checking MicroPython via mpremote: {e}", color='red')
50
+ return False
51
+
52
+ def get_remote_file_info(port):
53
+ """Get remote file information from ESP32 including sizes."""
54
+ try:
55
+ cmd = ['mpremote', 'connect', port, 'ls']
56
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
57
+ if result.returncode != 0:
58
+ print_colored(f"Error getting remote file list: {result.stderr}", color='red')
59
+ return {}
60
+ file_info = {}
61
+ lines = result.stdout.strip().split('\n')
62
+ for line in lines:
63
+ line = line.strip()
64
+ if not line or line.startswith('ls :') or line == '':
65
+ continue
66
+ parts = line.split()
67
+ if len(parts) >= 2:
68
+ try:
69
+ size = int(parts[0])
70
+ filename = ' '.join(parts[1:])
71
+ file_info[filename] = size
72
+ except ValueError:
73
+ continue
74
+ return file_info
75
+ except Exception as e:
76
+ print_colored(f"Error getting remote file info: {e}", color='red')
77
+ return {}
78
+
79
+ def should_upload_file(local_path, remote_filename, remote_files):
80
+ """Determine if a file should be uploaded based on size comparison."""
81
+ if not os.path.exists(local_path):
82
+ return False, f"Local file {local_path} not found"
83
+ local_size = os.path.getsize(local_path)
84
+ if remote_filename not in remote_files:
85
+ return True, f"New file (local: {local_size} bytes)"
86
+ remote_size = remote_files[remote_filename]
87
+ if local_size != remote_size:
88
+ return True, f"Size changed (local: {local_size} bytes, remote: {remote_size} bytes)"
89
+ return False, f"No change (both: {local_size} bytes)"
90
+
91
+ def analyze_app_static_files(app_file):
92
+ """Analyze the app.py file to find static file and template references."""
93
+ static_files = set()
94
+ template_files = set()
95
+ try:
96
+ app_dir = os.path.dirname(app_file) or '.'
97
+ with open(app_file, 'r', encoding='utf-8') as f:
98
+ content = f.read()
99
+ lines = content.split('\n')
100
+ filtered_lines = []
101
+ in_multiline_string = False
102
+ string_delimiter = None
103
+ for line in lines:
104
+ if line.strip().startswith('#'):
105
+ continue
106
+ if '"""' in line or "'''" in line:
107
+ if not in_multiline_string:
108
+ in_multiline_string = True
109
+ string_delimiter = '"""' if '"""' in line else "'''"
110
+ elif string_delimiter in line:
111
+ in_multiline_string = False
112
+ string_delimiter = None
113
+ continue
114
+ if not in_multiline_string:
115
+ if '#' in line:
116
+ line = line.split('#')[0]
117
+ filtered_lines.append(line)
118
+ filtered_content = '\n'.join(filtered_lines)
119
+ static_pattern = r'app\.add_static\s*\(\s*[\'"]([^\'"]+)[\'"]\s*,\s*[\'"]([^\'"]+)[\'"]\s*\)'
120
+ static_matches = re.findall(static_pattern, filtered_content)
121
+ for url_path, file_path in static_matches:
122
+ if url_path in ['/url', '/path', '/example'] or file_path in ['path', 'file', 'example']:
123
+ print_colored(f"āš ļø Skipping placeholder: app.add_static('{url_path}', '{file_path}')", color='yellow')
124
+ continue
125
+ if len(file_path) > 2 and not file_path.startswith('/'):
126
+ static_files.add((url_path, file_path))
127
+ template_pattern = r'app\.render_template\s*\(\s*[\'"]([^\'"]+)[\'"][^\)]*\)'
128
+ template_matches = re.findall(template_pattern, filtered_content)
129
+ for template in template_matches:
130
+ if template not in ['template', 'example', 'placeholder']:
131
+ template_path = os.path.join(app_dir, template)
132
+ template_files.add(template_path)
133
+ html_static_pattern = r'(?:href|src)\s*=\s*[\'"]([^\'"]+\.(css|js|png|jpg|jpeg|gif|ico|svg|webp))[\'"]'
134
+ html_matches = re.findall(html_static_pattern, filtered_content, re.IGNORECASE)
135
+ for url_path, ext in html_matches:
136
+ if url_path.startswith('/') and not url_path.startswith('//') and 'http' not in url_path:
137
+ guessed_path = url_path.lstrip('/')
138
+ if '.' in guessed_path and len(guessed_path) > 3:
139
+ static_files.add((url_path, guessed_path))
140
+ if template_files:
141
+ print_colored(f"Resolved template file paths:", color='cyan')
142
+ for template in template_files:
143
+ print_colored(f" {template} {'(exists)' if os.path.exists(template) else '(missing)'}", color='cyan')
144
+ return static_files, template_files
145
+ except Exception as e:
146
+ print_colored(f"Error analyzing {app_file}: {e}", color='red')
147
+ return set(), set()
148
+
149
+ def analyze_template_static_files(template_files):
150
+ """Analyze template files to find additional static file references."""
151
+ static_files = set()
152
+ for template_file in template_files:
153
+ if not os.path.exists(template_file):
154
+ print_colored(f"Warning: Template file {template_file} not found", color='yellow')
155
+ continue
156
+ try:
157
+ with open(template_file, 'r', encoding='utf-8') as f:
158
+ content = f.read()
159
+ html_static_pattern = r'(?:href|src)\s*=\s*[\'"]([^\'"]+\.(css|js|png|jpg|jpeg|gif|ico|svg|webp))[\'"]'
160
+ html_matches = re.findall(html_static_pattern, content, re.IGNORECASE)
161
+ for url_path, ext in html_matches:
162
+ if url_path.startswith('/') and not url_path.startswith('//') and 'http' not in url_path:
163
+ guessed_path = url_path.lstrip('/')
164
+ if '.' in guessed_path and len(guessed_path) > 3:
165
+ static_files.add((url_path, guessed_path))
166
+ except Exception as e:
167
+ print_colored(f"Error analyzing template {template_file}: {e}", color='red')
168
+ return static_files
169
+
170
+ def verify_static_files_exist(static_files, static_dir):
171
+ """Verify that all required static files exist locally."""
172
+ missing_files = []
173
+ existing_files = []
174
+ for url_path, file_rel_path in static_files:
175
+ if os.path.isabs(file_rel_path):
176
+ full_path = file_rel_path
177
+ else:
178
+ full_path = os.path.join(static_dir, file_rel_path)
179
+ if os.path.exists(full_path):
180
+ existing_files.append((url_path, full_path))
181
+ else:
182
+ missing_files.append((url_path, full_path))
183
+ return existing_files, missing_files
184
+
185
+ def upload_boot_py(port, module_name):
186
+ """Create and upload boot.py to import the app module."""
187
+ boot_content = f"import {module_name}\n"
188
+ import tempfile
189
+ with tempfile.NamedTemporaryFile('w', delete=False, encoding='utf-8') as tmp:
190
+ tmp.write(boot_content)
191
+ tmp_path = tmp.name
192
+ try:
193
+ print_colored(f"ā¬†ļø Uploading boot.py to import {module_name}...", color='cyan')
194
+ upload_file(tmp_path, port, destination='boot.py')
195
+ finally:
196
+ os.unlink(tmp_path)
197
+
198
+ def remove_boot_py(port):
199
+ """Remove boot.py from the ESP32 filesystem."""
200
+ try:
201
+ print_colored("šŸ—‘ļø Removing boot.py from ESP32...", color='cyan')
202
+ cmd = ['mpremote', 'connect', port, 'exec', "import os; os.remove('boot.py')"]
203
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
204
+ if result.returncode != 0:
205
+ print_colored(f"āš ļø Could not remove boot.py: {result.stderr.strip()}", color='yellow')
206
+ else:
207
+ print_colored("boot.py removed successfully.", color='green')
208
+ except Exception as e:
209
+ print_colored(f"Error removing boot.py: {e}", color='red')
210
+
211
+ @click.group()
212
+ def cli():
213
+ pass
214
+
215
+ @cli.command()
216
+ @click.option('--port', default=None, help='Serial port, e.g., COM10')
217
+ @click.option('--erase', is_flag=True, help='Erase all flash before writing firmware')
218
+ def flash(port, erase):
219
+ """Flash MicroPython and MicroWeb to the ESP32."""
220
+ if not port:
221
+ ports = [p.device for p in serial.tools.list_ports.comports()]
222
+ port = ports[0] if ports else None
223
+ if not port:
224
+ print_colored("No ESP32 found. Specify --port, e.g., --port COM10.", color='red')
225
+ return
226
+ if erase:
227
+ print_colored("You requested --erase. This will erase ALL data on the ESP32!", color='yellow')
228
+ confirm = input("Type 'erase' to continue, or anything else to cancel: ")
229
+ if "erase" not in confirm.lower():
230
+ print_colored("Erase cancelled.", color='yellow')
231
+ return
232
+ print_colored(f"Erasing all flash on {port}...", color='yellow')
233
+ esptool.main(['--port', port, 'erase_flash'])
234
+ try:
235
+ print_colored(f"Checking for MicroPython on {port}...", color='blue')
236
+ if check_micropython(port):
237
+ print_colored(f"MicroPython detected on {port}. Skipping firmware flash.", color='green')
238
+ else:
239
+ firmware_path = pkg_resources.resource_filename('microweb', 'firmware/ESP32_GENERIC-20250415-v1.25.0.bin')
240
+ if not os.path.exists(firmware_path):
241
+ print_colored(f"Error: Firmware file not found at {firmware_path}. Ensure it is included in the package.", color='red')
242
+ return
243
+ print_colored(f"Flashing firmware on {port}...", color='blue')
244
+ esptool.main(['--port', port, 'write_flash', '-z', '0x1000', firmware_path])
245
+ print_colored("Uploading core files...", color='blue')
246
+ core_files = [
247
+ ('firmware/boot.py', 'boot.py'),
248
+ ('microweb.py', 'microweb.py'),
249
+ ('wifi.py', 'wifi.py'),
250
+ ]
251
+ for src, dest in core_files:
252
+ src_path = pkg_resources.resource_filename('microweb', src)
253
+ print_colored(f"Uploading {dest} from {src_path}...", color='cyan')
254
+ if not os.path.exists(src_path):
255
+ print_colored(f"Error: Source file {src_path} not found.", color='red')
256
+ return
257
+ upload_file(src_path, port, destination=dest)
258
+ print_colored("Verifying uploaded files...", color='blue')
259
+ verify_files(port, [dest for _, dest in core_files])
260
+ print_colored("MicroWeb flashed successfully", color='green')
261
+ except Exception as e:
262
+ print_colored(f"Error during flash: {e}", color='red')
263
+
264
+ @cli.command()
265
+ @click.argument('file')
266
+ @click.option('--port', default=None, help='Serial port, e.g., COM10')
267
+ @click.option('--check-only', is_flag=True, help='Only check static files, don\'t upload')
268
+ @click.option('--static', default='static', help='Local static files folder path')
269
+ @click.option('--force', is_flag=True, help='Force upload all files regardless of changes')
270
+ @click.option('--no-stop', is_flag=True, help='Do not reset ESP32 before running app')
271
+ @click.option('--timeout', default=3600, show_default=True, help='Timeout seconds for running app')
272
+ @click.option('--add-boot', is_flag=True, help='Add boot.py that imports the app to run it on boot')
273
+ @click.option('--remove-boot', is_flag=True, help='Remove boot.py from the ESP32')
274
+ def run(file, port, check_only, static, force, no_stop, timeout, add_boot, remove_boot):
275
+ """Upload and execute a file on the ESP32 (only uploads changed files)."""
276
+ if not file.endswith('.py'):
277
+ print_colored("Error: File must have a .py extension.", color='red')
278
+ return
279
+ if not os.path.exists(file):
280
+ print_colored(f"Error: File {file} does not exist.", color='red')
281
+ return
282
+ module_name = os.path.splitext(os.path.basename(file))[0]
283
+ if add_boot and remove_boot:
284
+ print_colored("Error: --add-boot and --remove-boot options cannot be used together.", color='red')
285
+ return
286
+ print_colored(f"Analyzing {file} for static file and template dependencies...", color='blue')
287
+ static_files, template_files = analyze_app_static_files(file)
288
+ # --- Find templates in ./ and ./static ---
289
+ found_templates = set()
290
+ for folder in [os.path.dirname(file), static]:
291
+ if os.path.isdir(folder):
292
+ for entry in os.listdir(folder):
293
+ if entry.endswith('.html') or entry.endswith('.htm'):
294
+ found_templates.add(os.path.join(folder, entry))
295
+ # Add found templates if not already in template_files
296
+ for tfile in found_templates:
297
+ if tfile not in template_files:
298
+ template_files.add(tfile)
299
+ if template_files:
300
+ print_colored(f"Found template files: {', '.join(os.path.basename(t) for t in template_files)}", color='cyan')
301
+ template_static_files = analyze_template_static_files(template_files)
302
+ static_files.update(template_static_files)
303
+ # --- Find static files in ./ and ./static ---
304
+ found_static = set()
305
+ for folder in [os.path.dirname(file), static]:
306
+ if os.path.isdir(folder):
307
+ for entry in os.listdir(folder):
308
+ if entry.endswith(('.css', '.js', '.png', '.jpg', '.jpeg', '.gif', '.ico', '.svg', '.webp')):
309
+ found_static.add(('/' + entry, entry))
310
+ for url_path, file_rel_path in found_static:
311
+ if (url_path, file_rel_path) not in static_files:
312
+ static_files.add((url_path, file_rel_path))
313
+ existing_files = []
314
+ missing_files = []
315
+ if static_files:
316
+ print_colored(f"Found {len(static_files)} static file references:", color='blue')
317
+ for url_path, file_rel_path in static_files:
318
+ print_colored(f" {url_path} -> {file_rel_path}", color='cyan')
319
+ existing_files, missing_files = verify_static_files_exist(static_files, static)
320
+ if missing_files:
321
+ print_colored(f"\nError: Missing {len(missing_files)} static files:", color='red')
322
+ for url_path, file_full_path in missing_files:
323
+ print_colored(f" {url_path} -> {file_full_path} (NOT FOUND)", color='red')
324
+ print_colored("\nPlease create these files or update your app.py file or --static folder.", color='yellow')
325
+ return
326
+ print_colored(f"\nAll {len(existing_files)} static files found locally:", color='green')
327
+ for url_path, file_full_path in existing_files:
328
+ file_size = os.path.getsize(file_full_path)
329
+ print_colored(f" āœ“ {url_path} -> {file_full_path} ({file_size} bytes)", color='green')
330
+ else:
331
+ print_colored("No static files found in app.", color='yellow')
332
+ if check_only:
333
+ print_colored("\nStatic file and template check complete.", color='green')
334
+ return
335
+ if not port:
336
+ ports = [p.device for p in serial.tools.list_ports.comports()]
337
+ port = ports[0] if ports else None
338
+ if not port:
339
+ print_colored("No ESP32 found. Specify --port, e.g., --port COM10.", color='red')
340
+ return
341
+ if not check_micropython(port):
342
+ print_colored(f"MicroPython not detected on ESP32. Please run 'microweb flash --port {port}' first.", color='red')
343
+ return
344
+ if remove_boot:
345
+ remove_boot_py(port)
346
+ return
347
+ try:
348
+ print_colored(f"\nGetting remote file information from {port}...", color='blue')
349
+ remote_files = get_remote_file_info(port)
350
+ print_colored(f"Found {len(remote_files)} files on ESP32:", color='blue')
351
+ for filename, size in remote_files.items():
352
+ print_colored(f" {filename}: {size} bytes", color='cyan')
353
+ files_to_upload = []
354
+ files_skipped = []
355
+ main_filename = os.path.basename(file)
356
+ should_upload, reason = should_upload_file(file, main_filename, remote_files)
357
+ if force or should_upload:
358
+ files_to_upload.append(('main', file, main_filename, reason))
359
+ else:
360
+ files_skipped.append((main_filename, reason))
361
+ template_uploads = []
362
+ for template_file in template_files:
363
+ if os.path.exists(template_file):
364
+ remote_name = os.path.basename(template_file)
365
+ should_upload, reason = should_upload_file(template_file, remote_name, remote_files)
366
+ if force or should_upload:
367
+ template_uploads.append((template_file, remote_name, reason))
368
+ else:
369
+ files_skipped.append((remote_name, reason))
370
+ else:
371
+ print_colored(f"Warning: Template file {template_file} not found locally, skipping upload", color='yellow')
372
+ static_uploads = []
373
+ if existing_files:
374
+ for url_path, file_full_path in existing_files:
375
+ filename = os.path.basename(file_full_path)
376
+ should_upload, reason = should_upload_file(file_full_path, f"static/{filename}", remote_files)
377
+ if force or should_upload:
378
+ static_uploads.append((file_full_path, filename, reason))
379
+ else:
380
+ files_skipped.append((f"static/{filename}", reason))
381
+ total_uploads = len(files_to_upload) + len(template_uploads) + len(static_uploads)
382
+ if files_skipped:
383
+ print_colored(f"\nšŸ“‹ Files skipped ({len(files_skipped)}):", color='yellow')
384
+ for filename, reason in files_skipped:
385
+ print_colored(f" ā­ļø {filename}: {reason}", color='yellow')
386
+ if total_uploads == 0:
387
+ print_colored(f"\nāœ… All files are up to date! No uploads needed.", color='green')
388
+ if not force:
389
+ print_colored("Use --force to upload all files anyway.", color='yellow')
390
+ else:
391
+ print_colored(f"\nšŸ“¤ Files to upload ({total_uploads}):", color='blue')
392
+ for file_type, local_path, remote_name, reason in files_to_upload:
393
+ print_colored(f" šŸ“ {remote_name}: {reason}", color='cyan')
394
+ for template_file, remote_name, reason in template_uploads:
395
+ print_colored(f" šŸ“„ {remote_name}: {reason}", color='cyan')
396
+ for local_path, filename, reason in static_uploads:
397
+ print_colored(f" šŸŽØ static/{filename}: {reason}", color='cyan')
398
+ upload_count = 0
399
+ for file_type, local_path, remote_name, reason in files_to_upload:
400
+ print_colored(f"\nā¬†ļø Uploading {remote_name}...", color='cyan')
401
+ upload_file(local_path, port, destination=remote_name)
402
+ upload_count += 1
403
+ for template_file, remote_name, reason in template_uploads:
404
+ print_colored(f"ā¬†ļø Uploading template: {remote_name}...", color='cyan')
405
+ upload_file(template_file, port, destination=remote_name)
406
+ upload_count += 1
407
+ if static_uploads:
408
+ print_colored("šŸ“ Creating static directory on ESP32...", color='blue')
409
+ create_directory('static', port)
410
+ for file_full_path, filename, reason in static_uploads:
411
+ print_colored(f"ā¬†ļø Uploading static file: static/{filename}...", color='cyan')
412
+ upload_file(file_full_path, port, destination=f"static/{filename}")
413
+ upload_count += 1
414
+ if add_boot:
415
+ upload_boot_py(port, module_name)
416
+ if not no_stop:
417
+ print_colored(f"\nšŸ”„ Resetting ESP32 to ensure clean state...", color='blue')
418
+ subprocess.run(['mpremote', 'connect', port, 'reset'], capture_output=True, text=True, timeout=10)
419
+ time.sleep(2)
420
+ if not add_boot:
421
+ print_colored(f"šŸš€ Starting {module_name}.run() with timeout {timeout} seconds...", color='blue')
422
+ cmd = ['mpremote', 'connect', port, 'exec', f'import {module_name}; {module_name}.app.run()']
423
+ try:
424
+ print_colored(f"\nāœ… {file} is running on ESP32", color='green')
425
+ ssid = None
426
+ password = None
427
+ try:
428
+ with open(file, 'r', encoding='utf-8') as f:
429
+ content = f.read()
430
+ ap_match = re.search(
431
+ r'MicroWeb\s*\(\s*.*ap\s*=\s*{[^}]*["\']ssid["\']\s*:\s*["\']([^"\']+)["\']\s*,\s*["\']password["\']\s*:\s*["\']([^"\']+)["\']',
432
+ content
433
+ )
434
+ if ap_match:
435
+ ssid = ap_match.group(1)
436
+ password = ap_match.group(2)
437
+ except Exception:
438
+ pass
439
+ if ssid and password:
440
+ print_colored(f"šŸ“¶ Connect to SSID: {ssid}, Password: {password}", color='cyan')
441
+ print_colored(f"🌐 Visit: http://192.168.4.1", color='cyan')
442
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
443
+ if result.returncode != 0:
444
+ print_colored(f"āŒ Error running {file}: return code {result.returncode}", color='red')
445
+ print_colored(f"stdout:\n{result.stdout.strip()}\nstderr:\n{result.stderr.strip()}", color='red')
446
+ return
447
+ if upload_count > 0:
448
+ print_colored(f"šŸ“Š Uploaded {upload_count} file(s), skipped {len(files_skipped)} file(s)", color='green')
449
+ else:
450
+ print_colored(f"šŸ“Š No files uploaded, {len(files_skipped)} file(s) were already up to date", color='green')
451
+ except subprocess.TimeoutExpired:
452
+ print_colored(f"āŒ Error: Running {file} timed out after {timeout} seconds.", color='red')
453
+ except Exception as e:
454
+ print_colored(f"āŒ Unexpected error running {file}: {e}", color='red')
455
+ else:
456
+ print_colored(f"āš ļø boot.py uploaded, app will run automatically on boot. Not running app.run() now.", color='yellow')
457
+ except Exception as e:
458
+ print_colored(f"āŒ Error: {e}", color='red')
459
+
460
+ @cli.command()
461
+ @click.option('--port', default=None, help='Serial port, e.g., COM10')
462
+ @click.option('--remove', 'remove_everything', is_flag=True, help='Actually remove all files in the ESP32 home directory')
463
+ def remove(port, remove_everything):
464
+ """Remove all files in the ESP32 home directory (requires --remove flag to actually delete files)."""
465
+ if not port:
466
+ ports = [p.device for p in serial.tools.list_ports.comports()]
467
+ port = ports[0] if ports else None
468
+ if not port:
469
+ print_colored("No ESP32 found. Specify --port, e.g., --port COM10.", color='red')
470
+ return
471
+ if not check_micropython(port):
472
+ print_colored(f"MicroPython not detected on ESP32. Please run 'microweb flash --port {port}' first.", color='red')
473
+ return
474
+ try:
475
+ if remove_everything:
476
+ print_colored("Removing all files in ESP32 home directory...", color='yellow')
477
+ cmd_ls = ['mpremote', 'connect', port, 'ls']
478
+ result = subprocess.run(cmd_ls, capture_output=True, text=True, timeout=10)
479
+ if result.returncode == 0:
480
+ files = []
481
+ for line in result.stdout.strip().split('\n'):
482
+ parts = line.strip().split()
483
+ if len(parts) >= 2:
484
+ filename = ' '.join(parts[1:])
485
+ files.append(filename)
486
+ for filename in files:
487
+ if filename in ('.', '..'):
488
+ continue
489
+ print_colored(f"Removing {filename}...", color='cyan')
490
+ cmd_rm = [
491
+ 'mpremote', 'connect', port, 'exec',
492
+ f"import os; import shutil; "
493
+ f"shutil.rmtree('{filename}') if hasattr(__import__('shutil'), 'rmtree') and os.path.isdir('{filename}') "
494
+ f"else (os.remove('{filename}') if '{filename}' in os.listdir() else None)"
495
+ ]
496
+ subprocess.run(cmd_rm, capture_output=True, text=True, timeout=10)
497
+ print_colored("All files in ESP32 home directory removed.", color='green')
498
+ else:
499
+ print_colored(f"Error listing files: {result.stderr}", color='red')
500
+ else:
501
+ print_colored("Dry run: No files were removed. Use --remove to actually delete all files in the ESP32 home directory.", color='yellow')
502
+ except Exception as e:
503
+ print_colored(f"Error removing files: {e}", color='red')
504
+
505
+ @cli.command()
506
+ def examples():
507
+ """Show example commands for using microweb CLI."""
508
+ print_colored("Example commands for microweb CLI:", color='blue', style='bold')
509
+ print_colored("\n1. Flash MicroPython and MicroWeb to ESP32:", color='cyan')
510
+ print_colored(" microweb flash --port COM10", color='green')
511
+ print_colored("\n2. Upload and run your app.py on ESP32:", color='cyan')
512
+ print_colored(" microweb run app.py --port COM10", color='green')
513
+ print_colored("\n3. Check static/template files without uploading:", color='cyan')
514
+ print_colored(" microweb run app.py --check-only", color='green')
515
+ print_colored("\n4. Remove all files from ESP32 (DANGEROUS):", color='cyan')
516
+ print_colored(" microweb remove --port COM10 --remove", color='green')
517
+ print_colored("\n5. Upload and set app to run on boot:", color='cyan')
518
+ print_colored(" microweb run app.py --port COM10 --add-boot", color='green')
519
+ print_colored("\n6. Remove boot.py from ESP32:", color='cyan')
520
+ print_colored(" microweb run app.py --port COM10 --remove-boot", color='green')
521
+ print_colored("\nReplace COM10 with your actual ESP32 serial port.", color='yellow')
522
+
523
+
524
+ if __name__ == '__main__':
525
+ cli()
@@ -0,0 +1,2 @@
1
+ import gc
2
+ gc.collect()
@@ -0,0 +1 @@
1
+ import app