Skip to content

Commit 9f24d2f

Browse files
Update espos-updater.py
1 parent 004cff4 commit 9f24d2f

File tree

1 file changed

+100
-48
lines changed

1 file changed

+100
-48
lines changed

espos-updater.py

Lines changed: 100 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,74 @@
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+
65
import os
76
import threading
87
import time
9-
import subprocess
108
import requests
119
import serial.tools.list_ports
1210
import webview
1311
import json
1412

15-
# Querido lector de código, cambia estos valores según tengas configurado tu propio repositorio
13+
# Configuración del repositorio
1614
GITHUB_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

2222
class 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
108158
html = '''<!DOCTYPE html>
109159
<html lang="es">
110160
<head><meta charset="UTF-8"><title>ESPOS Updater</title>
@@ -115,35 +165,37 @@ def _alert(self, text):
115165
button{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">&gt; 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

Comments
 (0)