77import re
88import pkg_resources
99from microweb .uploader import upload_file , create_directory , verify_files
10+ from microweb .dotenv import load_dotenv , get_env
1011
1112# ANSI color codes for enhanced terminal output
1213COLORS = {
@@ -227,19 +228,22 @@ def remove_boot_py(port):
227228@click .group ()
228229def cli ():
229230 pass
230-
231+
231232@cli .command ()
232233@click .option ('--port' , default = None , help = 'Serial port, e.g., COM10' )
234+ @click .option ('--baud' , default = 460800 , help = 'Baud rate for flashing (default: 460800)' )
233235@click .option ('--erase' , is_flag = True , help = 'Erase all flash before writing firmware' )
234236@click .option ('--esp8266' , is_flag = True , help = 'Flash ESP8266 firmware instead of ESP32' )
235237@click .option ('--firmware' , type = click .Path (exists = True ), help = 'Custom firmware .bin file to flash' )
236- def flash (port , erase , esp8266 , firmware ):
237- """Flash MicroPython and MicroWeb to the ESP32 or ESP8266."""
238+ @click .option ('--full-flash' , is_flag = True , help = 'Use full flash mode (offset 0) instead of 0x1000' )
239+ def flash (port , baud , erase , esp8266 , firmware , full_flash ):
240+ """Flash MicroPython and MicroWeb to the ESP32, ESP8266, or other ESP boards."""
241+ port = port
238242 if not port :
239243 ports = [p .device for p in serial .tools .list_ports .comports ()]
240244 port = ports [0 ] if ports else None
241245 if not port :
242- print_colored ("No ESP device found. Specify --port, e.g., --port COM10 ." , color = 'red' )
246+ print_colored ("No ESP device found. Specify --port or set PORT in .env file ." , color = 'red' )
243247 return
244248
245249 chip_name = "ESP8266" if esp8266 else "ESP32"
@@ -257,7 +261,7 @@ def flash(port, erase, esp8266, firmware):
257261 print_colored ("Erase cancelled." , color = 'yellow' )
258262 return
259263 print_colored (f"Erasing all flash on { port } ({ chip_name } )..." , color = 'yellow' )
260- esptool .main (['--port' , port , 'erase_flash' ])
264+ esptool .main (['--port' , port , '--baud' , str ( baud ), ' erase_flash' ])
261265
262266 try :
263267 print_colored (f"Checking for MicroPython on { port } ..." , color = 'blue' )
@@ -267,17 +271,19 @@ def flash(port, erase, esp8266, firmware):
267271 if not os .path .exists (firmware_path ):
268272 print_colored (f"Error: Firmware file not found at { firmware_path } ." , color = 'red' )
269273 return
270- print_colored (f"Flashing { chip_name } firmware on { port } ..." , color = 'blue' )
271- esptool .main (['--port' , port , 'write_flash' , '-z' , '0x1000' , firmware_path ])
274+ flash_offset = '0x0' if full_flash else '0x1000'
275+ print_colored (f"Flashing { chip_name } firmware at offset { flash_offset } on { port } ..." , color = 'blue' )
276+ esptool .main (['--port' , port , '--baud' , str (baud ), 'write_flash' , '-z' , flash_offset , firmware_path ])
272277
273278 print_colored ("Uploading core files..." , color = 'blue' )
274279 core_files = [
275280 ('firmware/boot.py' , 'boot.py' ),
276281 ('microweb.py' , 'microweb.py' ),
277282 ('wifi.py' , 'wifi.py' ),
283+ ('dotenv.py' , 'dotenv.py' ),
278284 ]
279285 for src , dest in core_files :
280- src_path = pkg_resources .resource_filename ('microweb' , src )
286+ src_path = pkg_resources .resource_filename ('microweb' , src ) if src . startswith ( 'firmware/' ) else os . path . join ( os . path . dirname ( __file__ ), src )
281287 print_colored (f"Uploading { dest } from { src_path } ..." , color = 'cyan' )
282288 if not os .path .exists (src_path ):
283289 print_colored (f"Error: Source file { src_path } not found." , color = 'red' )
@@ -297,14 +303,17 @@ def flash(port, erase, esp8266, firmware):
297303@click .argument ('file' )
298304@click .option ('--port' , default = None , help = 'Serial port, e.g., COM10' )
299305@click .option ('--check-only' , is_flag = True , help = 'Only check static files, don\' t upload' )
300- @click .option ('--static' , default = 'static' , help = 'Local static files folder path' )
306+ @click .option ('--static' , default = None , help = 'Local static files folder path' )
301307@click .option ('--force' , is_flag = True , help = 'Force upload all files regardless of changes' )
302308@click .option ('--no-stop' , is_flag = True , help = 'Do not reset ESP32 before running app' )
303309@click .option ('--timeout' , default = 3600 , show_default = True , help = 'Timeout seconds for running app' )
304310@click .option ('--add-boot' , is_flag = True , help = 'Add boot.py that imports the app to run it on boot' )
305311@click .option ('--remove-boot' , is_flag = True , help = 'Remove boot.py from the ESP32' )
306312def run (file , port , check_only , static , force , no_stop , timeout , add_boot , remove_boot ):
307313 """Upload and execute a file on the ESP32 (only uploads changed files)."""
314+ env_vars = load_dotenv ()
315+ port = port or get_env ('PORT' , default = None , env_vars = env_vars )
316+ static = static or get_env ('STATIC_DIR' , default = 'static' , env_vars = env_vars )
308317 if not file .endswith ('.py' ):
309318 print_colored ("Error: File must have a .py extension." , color = 'red' )
310319 return
@@ -382,6 +391,14 @@ def run(file, port, check_only, static, force, no_stop, timeout, add_boot, remov
382391 print_colored (f" { lib } (NOT FOUND)" , color = 'red' )
383392 print_colored ("\n Please create these library/model files or update your app.py file." , color = 'yellow' )
384393 return
394+ # Check for load_dotenv import to determine if .env file should be uploaded
395+ dotenv_pattern = r'^\s*(?:import\s+dotenv|from\s+dotenv\s+import\s+.*)\s*$'
396+ uses_dotenv = False
397+ for line in content .split ('\n ' ):
398+ if re .match (dotenv_pattern , line , re .MULTILINE ):
399+ uses_dotenv = True
400+ print_colored ("Detected dotenv import in app.py, checking for .env file..." , color = 'cyan' )
401+ break
385402 except Exception as e :
386403 print_colored (f"Error analyzing library/model files in { file } : { e } " , color = 'red' )
387404 return
@@ -392,7 +409,7 @@ def run(file, port, check_only, static, force, no_stop, timeout, add_boot, remov
392409 ports = [p .device for p in serial .tools .list_ports .comports ()]
393410 port = ports [0 ] if ports else None
394411 if not port :
395- print_colored ("No ESP32 found. Specify --port, e.g., --port COM10 ." , color = 'red' )
412+ print_colored ("No ESP32 found. Specify --port or set PORT in .env file ." , color = 'red' )
396413 return
397414 if remove_boot :
398415 remove_boot_py (port )
@@ -424,7 +441,7 @@ def run(file, port, check_only, static, force, no_stop, timeout, add_boot, remov
424441 else :
425442 files_skipped .append ((remote_name , reason ))
426443 else :
427- print_colored (f"farning Fajling : Template file { template_file } not found locally, skipping upload." , color = 'yellow' )
444+ print_colored (f"Warning : Template file { template_file } not found locally, skipping upload." , color = 'yellow' )
428445 static_uploads = []
429446 if existing_files :
430447 for url_path , file_full_path in existing_files :
@@ -446,6 +463,18 @@ def run(file, port, check_only, static, force, no_stop, timeout, add_boot, remov
446463 lib_uploads .append ((lib_file , filename , relative_path , reason ))
447464 else :
448465 files_skipped .append ((remote_name , reason ))
466+ env_upload = []
467+ if uses_dotenv :
468+ env_file = os .path .join (os .path .dirname (file ), '.env' )
469+ if os .path .exists (env_file ):
470+ should_upload , reason = should_upload_file (env_file , '.env' , remote_files )
471+ if force or should_upload :
472+ env_upload .append ((env_file , '.env' , reason ))
473+ else :
474+ files_skipped .append (('.env' , reason ))
475+ else :
476+ print_colored ("Warning: .env file not found in project directory, skipping upload." , color = 'yellow' )
477+
449478 total_uploads = len (files_to_upload ) + len (template_uploads ) + len (static_uploads ) + len (lib_uploads )
450479 if files_skipped :
451480 print_colored (f"\n 📋 Files skipped ({ len (files_skipped )} ):" , color = 'yellow' )
@@ -490,6 +519,10 @@ def run(file, port, check_only, static, force, no_stop, timeout, add_boot, remov
490519 print_colored (f"⬆️ Uploading library/model file: { relative_path } ..." , color = 'cyan' )
491520 upload_file (lib_file , port , destination = relative_path )
492521 upload_count += 1
522+ for env_file_path , remote_name , reason in env_upload :
523+ print_colored (f"⬆️ Uploading environment file: { remote_name } ..." , color = 'cyan' )
524+ upload_file (env_file_path , port , destination = remote_name )
525+ upload_count += 1
493526 if add_boot :
494527 upload_boot_py (port , module_name )
495528 if not no_stop :
@@ -501,32 +534,32 @@ def run(file, port, check_only, static, force, no_stop, timeout, add_boot, remov
501534 cmd = ['mpremote' , 'connect' , port , 'exec' , f'import { module_name } ; { module_name } .app.run()' ]
502535 try :
503536 print_colored (f"\n ✅ { file } is running on ESP32" , color = 'green' )
504- ssid = None
505- password = None
537+ ssid = get_env ( 'SSID' , default = None , env_vars = env_vars )
538+ password = get_env ( 'PASSWORD' , default = None , env_vars = env_vars )
506539 try :
507540 with open (file , 'r' , encoding = 'utf-8' ) as f :
508541 content = f .read ()
509542 ap_match = re .search (
510- r'MicroWeb\s*\(\s*.*ocier* ap\s*=\s*{[^}]*["\']ssid["\']\s*:\s*["\']([^"\']+)["\']\s*,\s*["\']password["\']\s*:\s*["\']([^"\']+)["\']' ,
543+ r'MicroWeb\s*\(\s*.*ap\s*=\s*{[^}]*["\']ssid["\']\s*:\s*["\']([^"\']+)["\']\s*,\s*["\']password["\']\s*:\s*["\']([^"\']+)["\']' ,
511544 content
512545 )
513546 if ap_match :
514- ssid = ap_match .group (1 )
515- password = ap_match .group (2 )
547+ ssid = ssid or ap_match .group (1 )
548+ password = password or ap_match .group (2 )
516549 else :
517550 ap_match = re .search (
518551 r'MicroWeb\s*\([^)]*ap\s*=\s*{\s*["\']ssid["\']\s*:\s*["\']([^"\']+)["\']\s*,\s*["\']password["\']\s*:\s*["\']([^"\']+)["\']' ,
519552 content , re .DOTALL
520553 )
521554 if ap_match :
522- ssid = ap_match .group (1 )
523- password = ap_match .group (2 )
555+ ssid = ssid or ap_match .group (1 )
556+ password = password or ap_match .group (2 )
524557 except Exception :
525558 pass
526559 if ssid and password :
527560 print_colored (f"📶 Connect to SSID: { ssid } , Password: { password } " , color = 'cyan' )
528561 else :
529- print_colored (" ⚠️ No Wi-Fi access point configured in app.py. Using default IP." , color = 'yellow' )
562+ print_colored (" ⚠️ No Wi-Fi access point configured in app.py or .env . Using default IP." , color = 'yellow' )
530563 try :
531564 ip_line = f"import { module_name } ; print({ module_name } .app.get_ip())"
532565 result = subprocess .run (['mpremote' , 'connect' , port , 'exec' , ip_line ],
@@ -553,21 +586,17 @@ def run(file, port, check_only, static, force, no_stop, timeout, add_boot, remov
553586 print_colored (f"❌ Unexpected error running { file } : { e } " , color = 'red' )
554587 else :
555588 print_colored (f"⚠️ boot.py uploaded, app will run automatically on boot. Not running app.run() now." , color = 'yellow' )
556-
557-
589+
558590 except Exception as e :
559591 print_colored (f"❌ Error: { e } " , color = 'red' )
560592
561593
562-
563-
564-
565594@cli .command ()
566595@click .option ('--port' , default = None , help = 'Serial port, e.g., COM10' )
567596@click .option ('--remove' , 'remove_everything' , is_flag = True , help = 'Actually remove all files in the ESP32 home directory' )
568597def remove (port , remove_everything ):
569598 """Remove all files in the ESP32 home directory (requires --remove flag to actually delete files)."""
570- boot_files = ["boot.py" ,"microweb.py" ,"wifi.py" ]
599+ boot_files = ["boot.py" ,"microweb.py" ,"wifi.py" , "dotenv.py" ]
571600 if not port :
572601 ports = [p .device for p in serial .tools .list_ports .comports ()]
573602 port = ports [0 ] if ports else None
0 commit comments