1- """
2- ESPOS Updater con pywebview
3- Requisitos:
4- pip install pywebview[mshtml] requests pyserial
5- """
1+ # ESPOS Updater con pywebview
2+ # Requisitos:
3+ # pip install pywebview[mshtml] requests pyserial
4+
65import os
76import threading
87import time
9- import subprocess
108import requests
119import serial .tools .list_ports
1210import webview
1311import json
1412
15- # Querido lector de código, cambia estos valores según tengas configurado tu propio repositorio
13+ # Configuración del repositorio
1614GITHUB_OWNER = 'espos-project'
17- GITHUB_REPO = 'espos'
18- BIN_PATH = 'latest.bin'
19- ESP_URL = 'http://192.168.4.1/upload'
20- BAUD_RATE = 9600
15+ GITHUB_REPO = 'espos'
16+ LATEST_BIN = 'latest.bin'
17+ ESP_URL = 'http://192.168.4.1/upload'
18+ BAUD_RATE = 9600
19+ # Usar la raíz del servidor como comprobación de estado
20+ STATUS_PATH = 'http://192.168.4.1/'
2121
2222class Api :
23- __pywebview_rpc__ = ['start_update' ]
23+ __pywebview_rpc__ = ['start_update' , 'select_custom_bin' ]
2424 def __dir__ (self ):
2525 return self .__pywebview_rpc__
2626
2727 def __init__ (self ):
2828 self .window = None
29- self .port = None
29+ self .port = None
30+ self .custom_bin = None
3031
3132 def init_ports (self ):
32- ports = [p .device for p in serial .tools .list_ports .comports ()]
33+ ports = [p .device for p in serial .tools .list_ports .comports ()]
3334 options = '' .join (f"<option value='{ p } '>{ p } </option>" for p in ports )
3435 opts_json = json .dumps (options )
3536 self .window .evaluate_js (
3637 f"document.getElementById('comports').innerHTML = { opts_json } ;"
3738 )
3839
40+ def select_custom_bin (self ):
41+ try :
42+ paths = webview .create_file_dialog (
43+ self .window , webview .OPEN_DIALOG ,
44+ file_types = ('Binary files (*.bin)' ,),
45+ allow_multiple = False
46+ )
47+ if paths :
48+ path = paths [0 ]
49+ else :
50+ return
51+ except AttributeError :
52+ try :
53+ import tkinter as tk
54+ from tkinter import filedialog
55+ root = tk .Tk ()
56+ root .withdraw ()
57+ path = filedialog .askopenfilename (
58+ filetypes = [('Binarios' , '*.bin' )]
59+ )
60+ root .destroy ()
61+ if not path :
62+ return
63+ except Exception as e :
64+ return self ._alert (f"Error al abrir diálogo: { e } " )
65+ self .custom_bin = path
66+ name = os .path .basename (self .custom_bin )
67+ name_json = json .dumps (name )
68+ self .window .evaluate_js (
69+ f"document.getElementById('customBinLabel').innerText = { name_json } ;"
70+ )
71+
3972 def start_update (self , port ):
4073 self .port = port
4174 self .window .evaluate_js (
@@ -44,6 +77,7 @@ def start_update(self, port):
4477 threading .Thread (target = self ._workflow , daemon = True ).start ()
4578
4679 def _workflow (self ):
80+ # 1) Serial
4781 self ._log (f"🔌 Abriendo { self .port } @{ BAUD_RATE } ..." )
4882 try :
4983 import serial
@@ -53,42 +87,57 @@ def _workflow(self):
5387 except Exception as e :
5488 return self ._alert (f"Error serial: { e } " )
5589
56- self ._log ("🔍 Descargando .bin de la última release..." )
57- try :
58- api_url = f'https://api.github.com/repos/{ GITHUB_OWNER } /{ GITHUB_REPO } /releases/latest'
59- r = requests .get (api_url , timeout = 10 ); r .raise_for_status ()
60- asset = next ((a for a in r .json ().get ('assets' , []) if a ['name' ].endswith ('.bin' )), None )
61- if not asset :
62- return self ._alert ('No se encontró .bin en la última release.' )
63- dl = asset ['browser_download_url' ]
64- r2 = requests .get (dl , stream = True , timeout = 20 ); r2 .raise_for_status ()
65- with open (BIN_PATH , 'wb' ) as f :
66- for chunk in r2 .iter_content (1024 ):
67- f .write (chunk )
68- self ._log ("✅ Descarga completada." )
69- except Exception as e :
70- return self ._alert (f"Error descarga: { e } " )
90+ # 2) Obtener .bin
91+ if self .custom_bin :
92+ bin_path = self .custom_bin
93+ self ._log (f"📦 Usando bin personalizado: { os .path .basename (bin_path )} " )
94+ else :
95+ bin_path = LATEST_BIN
96+ self ._log ("🔍 Descargando .bin de la última release..." )
97+ try :
98+ api_url = f'https://api.github.com/repos/{ GITHUB_OWNER } /{ GITHUB_REPO } /releases/latest'
99+ r = requests .get (api_url , timeout = 10 )
100+ r .raise_for_status ()
101+ asset = next ((a for a in r .json ().get ('assets' , []) if a ['name' ].endswith ('.bin' )), None )
102+ if not asset :
103+ return self ._alert ('No se encontró .bin en la última release.' )
104+ dl = asset ['browser_download_url' ]
105+ r2 = requests .get (dl , stream = True , timeout = 20 )
106+ r2 .raise_for_status ()
107+ with open (bin_path , 'wb' ) as f :
108+ for chunk in r2 .iter_content (1024 ):
109+ f .write (chunk )
110+ self ._log ("✅ Descarga completada." )
111+ except Exception as e :
112+ return self ._alert (f"Error descarga: { e } " )
113+
114+ # 3) Espera AP (sin ping, usando HTTP polling)
71115 self .window .evaluate_js (
72116 "document.getElementById('waitModal').style.display='flex';"
73117 )
74- while subprocess .run (
75- ['ping' ,'-n' ,'1' ,'-w' ,'1000' ,'192.168.4.1' ],
76- stdout = subprocess .DEVNULL
77- ).returncode :
118+ self ._log ("⌛ Esperando que el ESP32 entre en modo DFU..." )
119+ # Intentar conectar a la raíz cada segundo
120+ while True :
121+ try :
122+ resp = requests .get (STATUS_PATH , timeout = 1 )
123+ if resp .status_code == 200 :
124+ break
125+ except requests .RequestException :
126+ pass
78127 time .sleep (1 )
79128 self .window .evaluate_js (
80129 "document.getElementById('waitModal').style.display='none';"
81130 )
82131 self ._log ("🔗 AP detectado. Subiendo firmware..." )
83- self ._log ("⚠️ NO DESENCHUFES TU ESP32. PODRÍA QUEDAR INUTILIZABLE ⚠️" )
132+ self ._log ("⚠️ NO DESCONECTES TU ESP32. PODRÍA QUEDAR INUTILIZABLE ⚠️" )
84133
85134 # 4) Upload
86135 try :
87- with open (BIN_PATH , 'rb' ) as f :
88- files = {'update' : (os .path .basename (BIN_PATH ), f , 'application/octet-stream' )}
136+ with open (bin_path , 'rb' ) as f :
137+ files = {'update' : (os .path .basename (bin_path ), f , 'application/octet-stream' )}
89138 r3 = requests .post (ESP_URL , files = files , timeout = 30 )
90139 if r3 .status_code == 200 :
91- self ._log ("🎉 Firmware enviado. ESP reiniciará." )
140+ self ._log ("🎉 ESPOS ha sido actualizado a la última versión. El ESP32 se reiniciará." )
92141 else :
93142 self ._alert (f"Subida fallida: { r3 .status_code } " )
94143 except Exception as e :
@@ -97,14 +146,15 @@ def _workflow(self):
97146 def _log (self , msg ):
98147 safe = msg .replace ('`' , '' )
99148 message_json = json .dumps ("\n " + safe )
100- js = f"document.getElementById('status').innerText += { message_json } ;"
101- self .window .evaluate_js (js )
149+ self .window .evaluate_js (
150+ f"document.getElementById('status').innerText += { message_json } ;"
151+ )
102152
103153 def _alert (self , text ):
104154 alert_json = json .dumps (text )
105155 self .window .evaluate_js (f"alert({ alert_json } );" )
106156
107-
157+ # HTML con opción para bin personalizado + footer
108158html = '''<!DOCTYPE html>
109159<html lang="es">
110160<head><meta charset="UTF-8"><title>ESPOS Updater</title>
@@ -115,35 +165,37 @@ def _alert(self, text):
115165button{background:none;border:2px solid #0f0;color:#0f0;padding:10px;cursor:pointer;margin:10px 0}
116166.modal{position:fixed;inset:0;background:rgba(0,0,0,0.9);display:flex;align-items:center;justify-content:center}
117167.modalContent{background:#111;border:2px solid #0f0;padding:20px;text-align:center;width:300px}
118- select{width:100%;padding:5px;margin:10px 0;background:#000;color:#0f0;border:1px solid #0f0}
168+ select, input[type=file] {width:100%;padding:5px;margin:10px 0;background:#000;color:#0f0;border:1px solid #0f0}
119169#footer{position:fixed;bottom:5px;right:10px;opacity:0.2;font-size:12px;pointer-events:none}
120170</style></head><body>
121171<h2>ESPOS Updater</h2>
122- <div id="status">> listo...</div>
123- <!-- Muestra modal de selección, sin iniciar actualización -->
172+ <div id="status">> listo...</div>
124173<button onclick="document.getElementById('portModal').style.display='flex';">
125174 ⚡ Actualizar ESP
126175</button>
127-
128176<div id="portModal" class="modal" style="display:none">
129177 <div class="modalContent">
178+ <h2>Actualizar con la última versión de ESPOS</h2>
130179 <h3>Selecciona puerto COM</h3>
131180 <select id="comports"></select>
181+ <h3>O carga tu .bin personalizado</h3>
182+ <button onclick="window.pywebview.api.select_custom_bin()">
183+ 📁 Elegir .bin
184+ </button>
185+ <div id="customBinLabel" style="margin-bottom:10px;">(Ningún bin seleccionado)</div>
132186 <button onclick="window.pywebview.api.start_update(document.getElementById('comports').value)">
133- Conectar
187+ Conectar y subir
134188 </button>
135189 </div>
136190</div>
137-
138191<div id="waitModal" class="modal" style="display:none">
139192 <div class="modalContent">
140193 <h3>▶ Conecta este dispositivo al punto de acceso DFU de tu ESP32</h3>
141194 <small style="color:#0a0;">(Se ocultará al detectar)</small>
142195 </div>
143196</div>
144-
145197<div id="footer">
146- ESPOS Updater – v1.0 : https://github.com/espos-project/espos-updater
198+ ESPOS Updater – v1.1 : https://github.com/espos-project/espos-updater
147199</div>
148200</body>
149201</html>'''
0 commit comments