Skip to content

Commit dc4f62a

Browse files
committed
Update
1 parent 4627678 commit dc4f62a

6 files changed

Lines changed: 169 additions & 33 deletions

File tree

README.md

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -203,10 +203,45 @@ Flash MicroPython firmware and MicroWeb to your device:
203203
microweb flash --port COM10
204204
```
205205

206-
#### Options:
207-
- `--erase`: Erase the entire flash memory before flashing firmware.
208-
- `--esp8266`: Flash ESP8266 firmware instead of the default ESP32.
209-
- `--firmware firmware.bin`: Use a custom `.bin` firmware file, overriding the default firmware for ESP32 or ESP8266.
206+
#### ⚙️ Options:
207+
208+
* `--port COMx`
209+
Specify the serial port to which your ESP device is connected.
210+
Example: `--port COM5` (Windows), `--port /dev/ttyUSB0` (Linux/macOS)
211+
212+
* `--erase`
213+
Erase the entire flash memory before writing firmware. **Warning:** This will remove all existing data.
214+
215+
* `--esp8266`
216+
Flash ESP8266 firmware instead of the default ESP32 firmware.
217+
218+
* `--firmware firmware.bin`
219+
Use a custom `.bin` firmware file. Overrides the default firmware path.
220+
221+
* `--baud 460800`
222+
Set a custom baud rate for flashing. Defaults to `460800`, but can be changed to `115200`, `921600`, etc.
223+
224+
* `--full-flash`
225+
Flash the firmware to address `0x0` (useful for full `.bin` images with bootloader). Default flashing address is `0x1000`.
226+
227+
---
228+
229+
### 📝 Examples:
230+
231+
```bash
232+
# Flash default ESP32 firmware and MicroWeb files
233+
microweb flash --port COM10
234+
235+
# Flash custom firmware with erase
236+
microweb flash --port COM10 --erase --firmware ./firmware/ESP32-C3-20250722-v1.25.1.bin
237+
238+
# Flash at 921600 baud and use full-flash mode (offset 0x0)
239+
microweb flash --port COM10 --baud 921600 --full-flash --firmware ./firmware/full_image.bin
240+
241+
# Flash ESP8266 device with default firmware
242+
microweb flash --port COM10 --esp8266
243+
```
244+
210245

211246
### Running a Custom Application
212247
Upload and run a MicroPython script:
@@ -297,6 +332,13 @@ Explore how MicroWeb is used in practical applications with minimal setup!
297332
```python
298333
from microweb import MicroWeb, Response
299334
335+
#from dotenv import load_dotenv , get_env
336+
337+
# env_vars = load_dotenv()
338+
# ssid = get_env('SSID', 'MyESP32', env_vars)
339+
# password = get_env('PASSWORD', 'mypassword', env_vars)
340+
# db_uri = get_env('DB_URI', None, env_vars)
341+
300342
app = MicroWeb(debug=True, ap={'ssid': 'MyWiFi', 'password': 'MyPassword'})
301343
302344
# app = MicroWeb(

microweb/cli.py

Lines changed: 54 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import re
88
import pkg_resources
99
from 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
1213
COLORS = {
@@ -227,19 +228,22 @@ def remove_boot_py(port):
227228
@click.group()
228229
def 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')
306312
def 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("\nPlease 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')
568597
def 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

microweb/dotenv.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
try:
2+
import re
3+
except ImportError:
4+
import ure as re
5+
6+
# Custom dotenv functionality for MicroPython
7+
def load_dotenv():
8+
try:
9+
env_vars = {}
10+
with open('.env', 'r') as f:
11+
for line in f:
12+
line = line.strip()
13+
# Skip empty lines or comments
14+
if not line or line.startswith('#'):
15+
continue
16+
# Split the line into key and value
17+
parts = line.split('=', 1)
18+
if len(parts) == 2:
19+
key, value = parts
20+
env_vars[key.strip()] = value.strip()
21+
else:
22+
print(f"Skipping malformed .env line: {line}")
23+
return env_vars
24+
except Exception as e:
25+
print(f"Failed to load .env file: {str(e)}")
26+
return {}
27+
28+
def get_env(key, default=None, env_vars=None):
29+
return env_vars.get(key, default) if env_vars else default
30+
31+
32+

microweb/microweb.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,21 @@ def decorator(func):
253253
return func
254254
return decorator
255255

256+
def url_encode(self, s):
257+
"""Encode a string for safe use in URLs."""
258+
safe = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~'
259+
result = ''
260+
for char in s:
261+
if char in safe:
262+
result += char
263+
else:
264+
hex_val = hex(ord(char))[2:].upper()
265+
# Ensure two-digit hex by adding leading zero if needed
266+
if len(hex_val) == 1:
267+
hex_val = '0' + hex_val
268+
result += '%' + hex_val
269+
return result
270+
256271
def add_static(self, path, file_path):
257272
self.static_files[path] = file_path
258273

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# The name of the package as it will appear on PyPI
55
name="microweb",
66
# Updated version to reflect new features (template engine with for loops, etc.)
7-
version="0.2.0",
7+
version="0.2.1",
88
# Automatically find all packages and subpackages
99
packages=find_packages(),
1010
# Include non-code files specified in MANIFEST.in or package_data

tests/2/app.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,30 @@
1313
1414
"""
1515

16+
17+
1618
from microweb import MicroWeb, Response
1719

20+
#from dotenv import load_dotenv , get_env
21+
22+
# env_vars = load_dotenv()
23+
# ssid = get_env('SSID', 'MyESP32', env_vars)
24+
# password = get_env('PASSWORD', 'mypassword', env_vars)
25+
# db_uri = get_env('DB_URI', None, env_vars)
26+
1827
app = MicroWeb(debug=True, ap={'ssid': 'MyWiFi', 'password': 'MyPassword'})
1928

20-
@app.route("/")
29+
# app = MicroWeb(
30+
# ap={"ssid": "Dialog 4G 0F8", "password": "youpassword"}, # Change to your router
31+
# debug=True,
32+
# mode="wifi" # Connect as client to your router
33+
# )
34+
35+
# Uncomment to stop Wi-Fi access point
36+
# app.stop_wifi() # Uncomment to stop Wi-Fi access point
37+
## app.start_wifi() # Uncomment to start Wi-Fi access point after stop
38+
39+
@app.route("/")
2140
def home(request):
2241
return Response("Hello from MicroWeb!", content_type="text/plain")
2342

@@ -40,6 +59,5 @@ def headers_example(request):
4059
resp.headers["X-Custom-Header"] = "Value"
4160
return resp
4261

43-
app.run()
44-
4562

63+
app.run()

0 commit comments

Comments
 (0)