# Modified from https://github.com/ayushsharma82/ElegantOTA # # This is called during the PlatformIO upload process, when the target is 'upload'. # Use the file upload_cli.py for manual uploads outside PIO. # # To use create a pio_local.ini file in the project root and add the following: # [env] # upload_protocol = custom # custom_emsesp_ip = ems-esp.local # custom_username = admin # custom_password = admin # # and # extra_scripts = scripts/upload.py # import requests import hashlib from urllib.parse import urlparse import time import os Import("env") try: from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor from tqdm import tqdm from termcolor import cprint except ImportError: env.Execute("$PYTHONEXE -m pip install requests_toolbelt") env.Execute("$PYTHONEXE -m pip install tqdm") env.Execute("$PYTHONEXE -m pip install termcolor") from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor from tqdm import tqdm from termcolor import cprint def print_success(x): cprint(x, 'green') def print_fail(x): cprint(f'Error: {x}', 'red') def build_headers(host_ip, emsesp_url, content_type='application/json', access_token=None, extra_headers=None): """Build common HTTP headers with optional overrides.""" headers = { 'Host': host_ip, 'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/118.0', 'Accept': '*/*', 'Accept-Language': 'en-US', 'Accept-Encoding': 'gzip, deflate', 'Referer': emsesp_url, 'Content-Type': content_type, 'Connection': 'keep-alive' } if access_token: headers['Authorization'] = f'Bearer {access_token}' if extra_headers: headers.update(extra_headers) return headers def on_upload(source, target, env): # make sure we have set the upload_protocol to custom if env.get('UPLOAD_PROTOCOL') != 'custom': print_fail( "Please set upload_protocol = custom in your pio_local.ini file when using upload.py") return # first check authentication try: username = env.GetProjectOption('custom_username') password = env.GetProjectOption('custom_password') emsesp_ip = env.GetProjectOption('custom_emsesp_ip') except Exception as e: print_fail(f'Missing settings. Add these to your pio_local.ini file:\n\ncustom_username=username\ncustom_password=password\ncustom_emsesp_ip=ems-esp.local\n') return emsesp_url = f"http://{emsesp_ip}" parsed_url = urlparse(emsesp_url) host_ip = parsed_url.netloc signon_url = f"{emsesp_url}/rest/signIn" signon_headers = build_headers(host_ip, emsesp_url) username_password = { "username": username, "password": password } response = requests.post( signon_url, json=username_password, headers=signon_headers) if response.status_code != 200: print_fail("Authentication with EMS-ESP failed (code " + str(response.status_code) + ")") return print_success("Authentication with EMS-ESP successful") access_token = response.json().get('access_token') # start the upload firmware_path = str(source[0]) with open(firmware_path, 'rb') as firmware: md5 = hashlib.md5(firmware.read()).hexdigest() firmware.seek(0) encoder = MultipartEncoder(fields={ 'MD5': md5, 'file': (firmware_path, firmware, 'application/octet-stream')} ) bar = tqdm(desc='Upload Progress', total=encoder.len, dynamic_ncols=True, unit='B', unit_scale=True, unit_divisor=1024 ) monitor = MultipartEncoderMonitor( encoder, lambda monitor: bar.update(monitor.bytes_read - bar.n)) post_headers = build_headers( host_ip, emsesp_url, content_type=monitor.content_type, access_token=access_token, extra_headers={ 'Content-Length': str(monitor.len), 'Origin': emsesp_url } ) upload_url = f"{emsesp_url}/rest/uploadFile" response = requests.post( upload_url, data=monitor, headers=post_headers) bar.close() time.sleep(0.1) print() if response.status_code != 200: print_fail(f"Upload failed (code {response.status_code}).") else: print_success("Upload successful. Rebooting device.") restart_headers = build_headers( host_ip, emsesp_url, access_token=access_token) restart_url = f"{emsesp_url}/api/system/restart" response = requests.get(restart_url, headers=restart_headers) if response.status_code != 200: print_fail(f"Restart failed (code {response.status_code})") print() if env.get('UPLOAD_PROTOCOL') == 'custom': env.Replace(UPLOADCMD=on_upload)