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 +0 -0
- microweb/cli.py +525 -0
- microweb/firmware/ESP32_GENERIC-20250415-v1.25.0.bin +0 -0
- microweb/firmware/boot.py +2 -0
- microweb/firmware/main.py +1 -0
- microweb/microweb.py +332 -0
- microweb/uploader.py +79 -0
- microweb/wifi.py +38 -0
- microweb-0.1.1.dist-info/METADATA +363 -0
- microweb-0.1.1.dist-info/RECORD +13 -0
- microweb-0.1.1.dist-info/WHEEL +5 -0
- microweb-0.1.1.dist-info/entry_points.txt +2 -0
- microweb-0.1.1.dist-info/top_level.txt +1 -0
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()
|
Binary file
|
@@ -0,0 +1 @@
|
|
1
|
+
import app
|