pagina Mi cuenta: cambio y restablecimiento de contrasena
This commit is contained in:
parent
86af91fe4b
commit
caee07fde9
@ -98,6 +98,42 @@ sudo cut -d: -f1 /etc/nginx/hbox-docs.htpasswd # lista de usuarios
|
||||
Administradores (quién puede borrar o reabrir actas) = variable `ACTAS_ADMINS` del servicio,
|
||||
hoy `leo`. Alan y Daniel quedan como técnicos: llenan y envían, no borran.
|
||||
|
||||
### 7 · Permisos para que los técnicos cambien su contraseña (una sola vez)
|
||||
|
||||
La página **Mi cuenta** (`/cuenta.html`) deja que cada quien cambie su propia contraseña, y que el
|
||||
administrador restablezca la de otro. Para eso el servicio tiene que poder **escribir** el archivo de
|
||||
usuarios que nginx solo **lee**:
|
||||
|
||||
```bash
|
||||
# quién es el usuario de nginx (normalmente www-data)
|
||||
grep -E "^\s*user" /etc/nginx/nginx.conf
|
||||
|
||||
sudo chown leo:www-data /etc/nginx/hbox-docs.htpasswd
|
||||
sudo chmod 640 /etc/nginx/hbox-docs.htpasswd
|
||||
sudo systemctl restart actas-hbox
|
||||
```
|
||||
|
||||
Así: **leo** (el servicio) escribe, **www-data** (nginx) lee, y nadie más puede ni leerlo.
|
||||
|
||||
Comprueba que las contraseñas estén en el formato que el servicio sabe verificar:
|
||||
|
||||
```bash
|
||||
sudo cut -d: -f2 /etc/nginx/hbox-docs.htpasswd | cut -c1-6
|
||||
```
|
||||
|
||||
Todas deben decir `$apr1$`. Si alguna sale distinta (`$2y$` es bcrypt), esa persona no va a poder
|
||||
cambiar la suya sola: restablécesela desde la propia página, que la vuelve a guardar en `$apr1$`.
|
||||
|
||||
!!! note "Por qué no se usa el comando `htpasswd` desde el servicio"
|
||||
Pasar una contraseña como argumento (`htpasswd -b … clave`) la deja visible en `ps` para
|
||||
cualquiera con sesión en el servidor. El hash `$apr1$` se calcula dentro del proceso; está
|
||||
verificado carácter por carácter contra la salida de `htpasswd`, incluyendo acentos y espacios.
|
||||
|
||||
!!! tip "Al cambiar la contraseña, la sesión no se cae"
|
||||
HTTP Basic se guarda en el navegador: quien cambie la suya sigue navegando con la anterior hasta
|
||||
que cierre sesión o cierre el navegador. La página lo explica y ofrece la liga de **Salir** para
|
||||
probarla de inmediato. No es un error.
|
||||
|
||||
---
|
||||
|
||||
## B · Verificación después de desplegar
|
||||
@ -158,3 +194,5 @@ rsync -az leo@git.helgen.online:/srv/projects/drogheda/adjuntos/ ~/respaldo-foto
|
||||
| Las fotos se ven rotas en el tablero | falta la carpeta `adjuntos` o su permiso | paso 3 |
|
||||
| `mkdocs build --strict` falla | un enlace del `nav` apunta a un archivo que no existe | revisa `mkdocs.yml` |
|
||||
| El técnico no puede entrar | usuario no creado, o se usó `htpasswd -c` y borró a los demás | paso 5 |
|
||||
| «El servicio no tiene permiso de escribir el archivo de usuarios» | falta el `chown`/`chmod` del htpasswd | paso 7 |
|
||||
| «Tu contraseña está guardada en un formato que no puedo verificar» | esa entrada es bcrypt, no `$apr1$` | restablecerla desde `/cuenta.html` como administrador |
|
||||
|
||||
@ -12,6 +12,8 @@ Environment=ADJUNTO_MAX_MB=8
|
||||
Environment=ARCHIVO_MAX_MB=20
|
||||
Environment=ACTAS_REPETIBLES=diagnostico-campo,instalacion-campo
|
||||
Environment=ACTAS_ADMINS=leo
|
||||
Environment=HTPASSWD_FILE=/etc/nginx/hbox-docs.htpasswd
|
||||
Environment=PASS_MIN=8
|
||||
Environment=PORT=8081
|
||||
ExecStart=/srv/projects/drogheda/hbox-docs/backend/venv/bin/gunicorn -w 2 -b 127.0.0.1:8081 app:app
|
||||
Restart=on-failure
|
||||
|
||||
128
backend/app.py
128
backend/app.py
@ -26,7 +26,7 @@ Rutas:
|
||||
Actas compuestas (integracion, ensamble): traen d["componentes"] = {clave: serie_modulo}. En los tipos
|
||||
de TIPOS_UNICOS un módulo no puede aparecer en dos actas reales (409).
|
||||
"""
|
||||
import os, re, json, csv, datetime, uuid
|
||||
import os, re, json, csv, datetime, uuid, hashlib
|
||||
from flask import Flask, request, jsonify, abort, send_file
|
||||
|
||||
app = Flask(__name__)
|
||||
@ -435,5 +435,131 @@ def _muy_grande(e):
|
||||
f"logs: máximo {LOG_MAX_MB} MB)."), 413
|
||||
|
||||
|
||||
# ------------------------------------------------- contraseñas (HTTP Basic de nginx)
|
||||
# El archivo de usuarios de nginx se escribe aquí mismo, sin invocar `htpasswd`:
|
||||
# pasar una contraseña como argumento de un proceso la deja visible en `ps`.
|
||||
HTPASSWD = os.environ.get("HTPASSWD_FILE", "/etc/nginx/hbox-docs.htpasswd")
|
||||
PASS_MIN = int(os.environ.get("PASS_MIN", "8"))
|
||||
ITOA64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
|
||||
|
||||
def _to64(v, n):
|
||||
return "".join(ITOA64[(v >> (6 * i)) & 0x3F] for i in range(n))
|
||||
|
||||
|
||||
def apr1(password, salt):
|
||||
"""Hash Apache MD5 ($apr1$): lo que escribe htpasswd por defecto y lo que lee nginx."""
|
||||
pw = password.encode("utf-8")
|
||||
sl = salt.encode("ascii")
|
||||
ctx = hashlib.md5(pw + b"$apr1$" + sl)
|
||||
final = hashlib.md5(pw + sl + pw).digest()
|
||||
i = len(pw)
|
||||
while i > 0:
|
||||
ctx.update(final[:16] if i > 16 else final[:i]); i -= 16
|
||||
i = len(pw)
|
||||
while i:
|
||||
ctx.update(b"\0" if (i & 1) else pw[:1]); i >>= 1
|
||||
final = ctx.digest()
|
||||
for i in range(1000):
|
||||
c = hashlib.md5()
|
||||
c.update(pw if (i & 1) else final)
|
||||
if i % 3: c.update(sl)
|
||||
if i % 7: c.update(pw)
|
||||
c.update(final if (i & 1) else pw)
|
||||
final = c.digest()
|
||||
out = "".join(_to64((final[a] << 16) | (final[b] << 8) | final[c], 4)
|
||||
for a, b, c in ((0, 6, 12), (1, 7, 13), (2, 8, 14), (3, 9, 15), (4, 10, 5)))
|
||||
return "$apr1$" + salt + "$" + out + _to64(final[11], 2)
|
||||
|
||||
|
||||
def _leer_htpasswd():
|
||||
if not os.path.isfile(HTPASSWD):
|
||||
return None
|
||||
with open(HTPASSWD, encoding="utf-8") as f:
|
||||
return f.read().splitlines()
|
||||
|
||||
|
||||
def _escribir_htpasswd(lineas):
|
||||
"""Reescribe el archivo de forma atómica, conservando permisos y dueño."""
|
||||
carpeta = os.path.dirname(HTPASSWD) or "."
|
||||
st = os.stat(HTPASSWD)
|
||||
tmp = os.path.join(carpeta, ".htpasswd.tmp-" + uuid.uuid4().hex[:8])
|
||||
with open(tmp, "w", encoding="utf-8") as f:
|
||||
f.write("\n".join(lineas) + "\n")
|
||||
os.chmod(tmp, st.st_mode & 0o7777)
|
||||
try:
|
||||
os.chown(tmp, st.st_uid, st.st_gid)
|
||||
except PermissionError:
|
||||
pass
|
||||
os.replace(tmp, HTPASSWD)
|
||||
|
||||
|
||||
@app.get("/api/usuarios")
|
||||
def usuarios():
|
||||
"""Lista de usuarios (solo administrador), para el restablecimiento."""
|
||||
if not is_admin():
|
||||
return jsonify(ok=False, error="Solo un administrador puede ver la lista."), 403
|
||||
lineas = _leer_htpasswd()
|
||||
if lineas is None:
|
||||
return jsonify(ok=False, error="No encuentro el archivo de usuarios."), 500
|
||||
return jsonify(ok=True, usuarios=sorted(l.split(":", 1)[0] for l in lineas if ":" in l))
|
||||
|
||||
|
||||
@app.post("/api/password")
|
||||
def cambiar_password():
|
||||
yo = current_user()
|
||||
if not yo:
|
||||
return jsonify(ok=False, error="No puedo identificarte. Vuelve a iniciar sesión."), 403
|
||||
try:
|
||||
d = request.get_json(force=True) or {}
|
||||
except Exception:
|
||||
return jsonify(ok=False, error="JSON inválido"), 400
|
||||
|
||||
destino = str(d.get("usuario") or yo).strip()
|
||||
actual = str(d.get("actual") or "")
|
||||
nueva = str(d.get("nueva") or "")
|
||||
confirma = str(d.get("confirma") or "")
|
||||
es_reset = (destino.lower() != yo.lower())
|
||||
|
||||
if es_reset and not is_admin():
|
||||
return jsonify(ok=False, error="Solo puedes cambiar tu propia contraseña."), 403
|
||||
if len(nueva) < PASS_MIN:
|
||||
return jsonify(ok=False, error=f"La nueva contraseña debe tener al menos {PASS_MIN} caracteres."), 400
|
||||
if nueva != confirma:
|
||||
return jsonify(ok=False, error="Las dos contraseñas nuevas no coinciden."), 400
|
||||
if not es_reset and nueva == actual:
|
||||
return jsonify(ok=False, error="La nueva contraseña es igual a la actual."), 400
|
||||
|
||||
lineas = _leer_htpasswd()
|
||||
if lineas is None:
|
||||
return jsonify(ok=False, error="No encuentro el archivo de usuarios en el servidor."), 500
|
||||
|
||||
idx = next((i for i, l in enumerate(lineas)
|
||||
if ":" in l and l.split(":", 1)[0] == destino), None)
|
||||
if idx is None:
|
||||
return jsonify(ok=False, error=f"El usuario {destino} no existe."), 404
|
||||
|
||||
guardado = lineas[idx].split(":", 1)[1]
|
||||
if not es_reset:
|
||||
partes = guardado.split("$")
|
||||
if not guardado.startswith("$apr1$") or len(partes) < 4:
|
||||
return jsonify(ok=False, error="Tu contraseña está guardada en un formato que este "
|
||||
"servicio no puede verificar. Pide a Leo que la restablezca."), 400
|
||||
if apr1(actual, partes[2]) != guardado:
|
||||
return jsonify(ok=False, error="La contraseña actual no es correcta."), 403
|
||||
|
||||
salt = "".join(ITOA64[b % 64] for b in os.urandom(8))
|
||||
lineas[idx] = destino + ":" + apr1(nueva, salt)
|
||||
try:
|
||||
_escribir_htpasswd(lineas)
|
||||
except PermissionError:
|
||||
return jsonify(ok=False, error="El servicio no tiene permiso de escribir el archivo de "
|
||||
"usuarios. Avisa a Leo (permisos de hbox-docs.htpasswd)."), 500
|
||||
except OSError as e:
|
||||
return jsonify(ok=False, error="No se pudo guardar: " + str(e)), 500
|
||||
|
||||
return jsonify(ok=True, usuario=destino, reset=es_reset)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="127.0.0.1", port=int(os.environ.get("PORT", "8081")))
|
||||
|
||||
183
docs/cuenta.html
Normal file
183
docs/cuenta.html
Normal file
@ -0,0 +1,183 @@
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Mi cuenta — HBOX</title>
|
||||
<link rel="stylesheet" href="formularios/acta.css">
|
||||
<style>
|
||||
.campo{margin:12px 0}
|
||||
.quien{background:var(--naranja-suave);border:1px solid #ffd8a8;border-radius:10px;
|
||||
padding:12px 14px;margin-bottom:14px;font-size:15px}
|
||||
.quien b{font-size:17px}
|
||||
.fuerza{height:6px;border-radius:3px;background:var(--linea);margin-top:6px;overflow:hidden}
|
||||
.fuerza i{display:block;height:100%;width:0;transition:width .2s,background .2s}
|
||||
.pista{font-size:12.5px;color:var(--tinta2);margin-top:5px}
|
||||
.admin{border:1px solid var(--linea);border-left:4px solid var(--naranja)}
|
||||
button.enviar{width:100%;margin-top:6px}
|
||||
.ver{font-size:13px;color:var(--naranja);cursor:pointer;user-select:none;margin-top:6px;display:inline-block}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header class="top">
|
||||
<h1>Mi cuenta</h1>
|
||||
<p>Aquí cambias tu contraseña de acceso al sitio.</p>
|
||||
</header>
|
||||
<div style="display:flex;justify-content:space-between;margin:2px 0 10px">
|
||||
<a class="volver" href="./">← Documentación</a>
|
||||
<a class="volver" href="salir.html">Cerrar sesión</a>
|
||||
</div>
|
||||
|
||||
<div class="quien" id="quien">Consultando quién eres…</div>
|
||||
|
||||
<section class="paso">
|
||||
<h2>Cambiar mi contraseña</h2>
|
||||
<p class="sub">Elige algo que recuerdes sin apuntarlo. Mínimo 8 caracteres.</p>
|
||||
<div class="campo">
|
||||
<label for="actual">Contraseña actual</label>
|
||||
<input type="password" id="actual" autocomplete="current-password">
|
||||
</div>
|
||||
<div class="campo">
|
||||
<label for="nueva">Contraseña nueva</label>
|
||||
<input type="password" id="nueva" autocomplete="new-password">
|
||||
<div class="fuerza"><i id="barra"></i></div>
|
||||
<div class="pista" id="pista">Mientras más larga, mejor. Una frase con espacios funciona y se recuerda.</div>
|
||||
</div>
|
||||
<div class="campo">
|
||||
<label for="confirma">Repite la contraseña nueva</label>
|
||||
<input type="password" id="confirma" autocomplete="new-password">
|
||||
<span class="ver" id="ver">👁 Ver lo que escribí</span>
|
||||
</div>
|
||||
<button class="enviar" id="guardar">Guardar contraseña nueva</button>
|
||||
<div class="msg" id="msg"></div>
|
||||
</section>
|
||||
|
||||
<section class="paso admin" id="bloqueAdmin" style="display:none">
|
||||
<h2>Restablecer la contraseña de alguien más</h2>
|
||||
<p class="sub">Solo administración. Úsalo cuando un técnico olvidó la suya: no necesitas la anterior.</p>
|
||||
<div class="campo">
|
||||
<label for="quienReset">Usuario</label>
|
||||
<select id="quienReset"></select>
|
||||
</div>
|
||||
<div class="campo">
|
||||
<label for="nuevaR">Contraseña nueva para esa persona</label>
|
||||
<input type="text" id="nuevaR" autocomplete="off" placeholder="se la pasas en persona o por mensaje directo">
|
||||
</div>
|
||||
<button class="enviar" id="resetear">Restablecer</button>
|
||||
<div class="msg" id="msgR"></div>
|
||||
</section>
|
||||
|
||||
<section class="paso">
|
||||
<h2>Si olvidaste tu contraseña</h2>
|
||||
<p class="sub" style="margin:0">
|
||||
No hay recuperación por correo: pídele a Leo que te la restablezca desde esta misma página.
|
||||
Es cosa de un minuto.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const $ = id => document.getElementById(id);
|
||||
let yo = '';
|
||||
|
||||
function aviso(el, texto, bien) {
|
||||
el.className = 'msg ' + (bien ? 'ok' : 'err');
|
||||
el.innerHTML = texto;
|
||||
el.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
|
||||
// --- quién soy + si soy admin ---
|
||||
(async () => {
|
||||
try {
|
||||
const j = await (await fetch('/api/whoami')).json();
|
||||
yo = j.user || '';
|
||||
$('quien').innerHTML = yo
|
||||
? 'Entraste como <b>' + yo + '</b>' + (j.admin ? ' · administrador' : '')
|
||||
: '⚠ No puedo identificarte. Vuelve a entrar al sitio y recarga esta página.';
|
||||
if (j.admin) {
|
||||
$('bloqueAdmin').style.display = '';
|
||||
try {
|
||||
const u = await (await fetch('/api/usuarios')).json();
|
||||
(u.usuarios || []).forEach(n => {
|
||||
const o = document.createElement('option'); o.value = n; o.textContent = n;
|
||||
if (n === yo) o.textContent = n + ' (tú)';
|
||||
$('quienReset').append(o);
|
||||
});
|
||||
} catch (e) {}
|
||||
}
|
||||
} catch (e) {
|
||||
$('quien').textContent = 'No pude contactar al servidor.';
|
||||
}
|
||||
})();
|
||||
|
||||
// --- medidor simple: la longitud es lo que más cuenta ---
|
||||
$('nueva').addEventListener('input', () => {
|
||||
const v = $('nueva').value;
|
||||
let p = Math.min(100, v.length * 8);
|
||||
if (/[^a-zA-Z0-9]/.test(v)) p += 10;
|
||||
if (/[0-9]/.test(v) && /[a-zA-Z]/.test(v)) p += 10;
|
||||
p = Math.min(100, p);
|
||||
const b = $('barra');
|
||||
b.style.width = p + '%';
|
||||
b.style.background = p < 40 ? 'var(--falla)' : p < 70 ? 'var(--aviso)' : 'var(--ok)';
|
||||
$('pista').textContent = v.length < 8
|
||||
? 'Te faltan ' + (8 - v.length) + ' caracteres.'
|
||||
: p < 70 ? 'Aceptable. Si la alargas un poco queda mucho mejor.'
|
||||
: 'Buena contraseña.';
|
||||
});
|
||||
|
||||
$('ver').addEventListener('click', () => {
|
||||
const t = $('nueva').type === 'password' ? 'text' : 'password';
|
||||
$('nueva').type = t; $('confirma').type = t; $('actual').type = t;
|
||||
$('ver').textContent = t === 'text' ? '🙈 Ocultar' : '👁 Ver lo que escribí';
|
||||
});
|
||||
|
||||
const AVISO_SESION =
|
||||
'<br><br>Tu navegador todavía recuerda la contraseña anterior, así que puedes seguir '
|
||||
+ 'navegando sin problema. <b>La nueva la vas a necesitar la próxima vez que entres.</b> '
|
||||
+ 'Si quieres probarla ahora, usa <a href="salir.html">Cerrar sesión</a>.';
|
||||
|
||||
$('guardar').addEventListener('click', async () => {
|
||||
const b = $('guardar');
|
||||
b.disabled = true; b.textContent = 'Guardando…';
|
||||
try {
|
||||
const r = await fetch('/api/password', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ actual: $('actual').value, nueva: $('nueva').value, confirma: $('confirma').value })
|
||||
});
|
||||
const j = await r.json();
|
||||
if (!r.ok || !j.ok) throw new Error(j.error || ('HTTP ' + r.status));
|
||||
aviso($('msg'), '✓ <b>Contraseña cambiada.</b>' + AVISO_SESION, true);
|
||||
$('actual').value = $('nueva').value = $('confirma').value = '';
|
||||
$('barra').style.width = '0';
|
||||
} catch (e) {
|
||||
aviso($('msg'), e.message, false);
|
||||
}
|
||||
b.disabled = false; b.textContent = 'Guardar contraseña nueva';
|
||||
});
|
||||
|
||||
$('resetear').addEventListener('click', async () => {
|
||||
const u = $('quienReset').value, nueva = $('nuevaR').value;
|
||||
if (!u) return aviso($('msgR'), 'Elige a quién le vas a restablecer la contraseña.', false);
|
||||
if (!confirm('¿Restablecer la contraseña de ' + u + '?')) return;
|
||||
const b = $('resetear'); b.disabled = true; b.textContent = 'Restableciendo…';
|
||||
try {
|
||||
const r = await fetch('/api/password', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ usuario: u, nueva: nueva, confirma: nueva })
|
||||
});
|
||||
const j = await r.json();
|
||||
if (!r.ok || !j.ok) throw new Error(j.error || ('HTTP ' + r.status));
|
||||
aviso($('msgR'), '✓ Contraseña de <b>' + u + '</b> restablecida. Pásasela en persona o por '
|
||||
+ 'mensaje directo, y dile que la cambie aquí mismo cuando entre.'
|
||||
+ (u === yo ? AVISO_SESION : ''), true);
|
||||
$('nuevaR').value = '';
|
||||
} catch (e) {
|
||||
aviso($('msgR'), e.message, false);
|
||||
}
|
||||
b.disabled = false; b.textContent = 'Restablecer';
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1 +1 @@
|
||||
a8de74b3d7768e9d5656ae0bff05475445c0cd2599ecfdc4ab081e517ed3d6ef hbox-v5-bringup_0.4.0.bin
|
||||
98656ddcc4e209e291d554341318c74d3f05713ce7198c7f6dc2d816eb3c6c3f hbox-v5-bringup_0.5.0.bin
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# hbox_v5_bringup — EinarBoard v5 first-flash bring-up firmware
|
||||
|
||||
Bench-proves a newly assembled v5 board over USB-C **J11** only. Never powers the RTK stack.
|
||||
0.4.0 field-minimum: AUTO by default (stack self-powers with 5 V on P2, kept on through 5 V loss), watchdog, fan, link recovery, LEDs; charge OFF. Bench mode: `P`+`Y` stack ON/OFF, `n` NMEA monitor (fix 4 = RTK FIX). Console (0.2.1): CR+LF lines, `EVT` on every state change, named `STAT` every 5 s, `$PHLG` every 10 s in the fleet v3.x layout (server-parseable).
|
||||
0.5.0: Modo B in-band $PHLG (server Salud column) on top of the 0.4.0 field-minimum: AUTO by default (stack self-powers with 5 V on P2, kept on through 5 V loss), watchdog, fan, link recovery, LEDs; charge OFF. Bench mode: `P`+`Y` stack ON/OFF, `n` NMEA monitor (fix 4 = RTK FIX). Console (0.2.1): CR+LF lines, `EVT` on every state change, named `STAT` every 5 s, `$PHLG` every 10 s in the fleet v3.x layout (server-parseable).
|
||||
Checks: HSE 25 MHz + USB CDC · LSE Y4 · I2C: PCF8574 0x20 / SHT40 0x44 / TMP102 0x48 /
|
||||
SC16IS752 0x4D · gauge + RGB LED walk · ADC · digital inputs. Prints PASS/FAIL, then a
|
||||
`STAT` line every 2 s. Console commands: `r` rerun · `g` gauge walk · `i` I2C scan · `h` help.
|
||||
@ -23,14 +23,15 @@ generic variant defaults to HSI, which is not accurate enough for USB.
|
||||
## Flash (DFU, J11)
|
||||
Hold **SW2** (BOOT0) → tap **SW1** (RESET) → release SW2 → device `0483:DF11` appears.
|
||||
```bash
|
||||
dfu-util -a 0 -s 0x08000000:leave -D release/hbox-v5-bringup_0.4.0.bin
|
||||
dfu-util -a 0 -s 0x08000000:leave -D release/hbox-v5-bringup_0.5.0.bin
|
||||
```
|
||||
or STM32CubeProgrammer → USB → Connect → Download `release/hbox-v5-bringup_0.4.0.bin` @ `0x08000000`.
|
||||
or STM32CubeProgrammer → USB → Connect → Download `release/hbox-v5-bringup_0.5.0.bin` @ `0x08000000`.
|
||||
|
||||
## Release
|
||||
| File | Version | SHA-256 (first 16) | Size |
|
||||
|---|---|---|---|
|
||||
| **`release/hbox-v5-bringup_0.4.0.bin`** | **0.4.0 (2026-09-17) — USE THIS (field-minimum, AUTO by default)** | `a8de74b3d7768e9d` | 75 524 B |
|
||||
| **`release/hbox-v5-bringup_0.5.0.bin`** | **0.5.0 (2026-09-18) — USE THIS (field-minimum + Modo B telemetry)** | `98656ddcc4e209e2` | 77 312 B |
|
||||
| `release/superseded/hbox-v5-bringup_0.4.0.bin` | 0.4.0 — superseded (no in-band telemetry) | `a8de74b3d7768e9d` | 75 524 B |
|
||||
| `release/superseded/hbox-v5-bringup_0.3.0.bin` | 0.3.0 — superseded (AUTO opt-in, no watchdog/fan/recovery) | `d4608c739cef2661` | 72 860 B |
|
||||
| `release/superseded/hbox-v5-bringup_0.2.1.bin` | 0.2.1 — superseded (no AUTO mode; stack OFF on 5 V loss) | `78b34f4603550c05` | 70 084 B |
|
||||
| `release/superseded/hbox-v5-bringup_0.2.0.bin` | 0.2.0 — superseded (LF-only console, no EVT/$PHLG) | `8491625d494f9ee1` | 64 780 B |
|
||||
@ -38,6 +39,14 @@ or STM32CubeProgrammer → USB → Connect → Download `release/hbox-v5-bringup
|
||||
| `release/hbox-v5-bringup_0.1.0.bin` | 0.1.0 — SUPERSEDED (no float printf, ETH_CS driven) | `1a9a77ae479c8f86` | 41 040 B |
|
||||
|
||||
## Changelog
|
||||
- **0.5.0** (2026-09-18) — **Modo B: `$PHLG` in band.** Once the stack is on and the F9P has been talking for 10 s, USART1
|
||||
(PA10 tap / PA9) is started **before** K1 (PB5) is energised; the F9P TX2 stream then flows F9P → PA10 → MCU → PA9 → K1 NO → modem DIN
|
||||
byte for byte, and a `$PHLG` (same 24 fields) is injected every 30 s only after > 8 ms of line idle (a GGA is never split).
|
||||
Corrections modem → F9P stay on their direct wire. Fail-safe is hardware: PB5 low (reset, IWDG, any stack-off) = K1 NC = direct path.
|
||||
Prerequisites shipped with it: **256-byte UART rings** (`build_opt.h`, `static_assert`ed at compile), **non-blocking SHT40**
|
||||
(two-phase kick/poll), console STAT/`$PHLG` deferred to line-idle gaps, K1 released before `EEPROM.write` (flash erase stalls the core)
|
||||
and before the `r` self-tests. `B` toggles Modo B (default allowed). `STAT` adds `modoB= relay= phlg_aire=`.
|
||||
Server: no change needed — `retranslator._parse_phlg` already accepts the sentence; units get the **Salud** column.
|
||||
- **0.4.0** (2026-09-17) — **field-minimum ("-fm")**: what a unit needs to be left on a machine for days **without the production
|
||||
charge governor**. **AUTO is now the default** (only the flash byte `0x5A` means MANUAL; a virgin board powers the stack by itself).
|
||||
IWDG 16 s watchdog · fan policy = v3.0.3 (vehicle 5 V+ign → 100 %; battery → thermostat 48/45 C on the hottest sensor) ·
|
||||
|
||||
Binary file not shown.
@ -7,30 +7,34 @@ Archivos que el técnico necesita en el banco. Verificar el SHA-256 antes de fla
|
||||
|
||||
| Archivo | Versión | SHA-256 | Uso |
|
||||
|---|---|---|---|
|
||||
| [**hbox-v5-bringup_0.4.0.bin**](hbox-v5-bringup_0.4.0.bin) | **0.4.0** (2026-09-17) | `a8de74b3d7768e9d…` ([SHA256SUMS](SHA256SUMS.txt)) | Sección 5 del [procedimiento de prueba y flasheo v5](../procedimientos/v5-prueba-y-flasheo.md) y sección 5 del [ensamble](../procedimientos/v5-ensamble.md) (modo de banco `P` / `n`) |
|
||||
| [**hbox-v5-bringup_0.5.0.bin**](hbox-v5-bringup_0.5.0.bin) | **0.5.0** (2026-09-18) | `98656ddcc4e209e2…` ([SHA256SUMS](SHA256SUMS.txt)) | Sección 5 del [procedimiento de prueba y flasheo v5](../procedimientos/v5-prueba-y-flasheo.md) y sección 5 del [ensamble](../procedimientos/v5-ensamble.md) (modo de banco `P` / `n`) |
|
||||
|
||||
Flasheo por DFU en **J11** (mantener **SW2**, pulsar y soltar **SW1**, soltar SW2):
|
||||
|
||||
```bash
|
||||
dfu-util -a 0 -s 0x08000000:leave -D hbox-v5-bringup_0.4.0.bin
|
||||
dfu-util -a 0 -s 0x08000000:leave -D hbox-v5-bringup_0.5.0.bin
|
||||
```
|
||||
|
||||
o con **STM32CubeProgrammer** → USB → Connect → Download del `.bin` en `0x08000000`.
|
||||
|
||||
!!! note "Tarjetas ya flasheadas con 0.1.1 / 0.2.0 / 0.2.1 / 0.3.0"
|
||||
Re-flashear con 0.4.0 por el mismo procedimiento DFU. **La primera línea de la consola tras reset dice la versión**
|
||||
(`hbox-v5-bringup 0.4.0 built …`): si la línea `STAT` no termina en `stack=…`, la tarjeta sigue con 0.1.1.
|
||||
!!! note "Tarjetas ya flasheadas con cualquier versión anterior"
|
||||
Re-flashear con 0.5.0 por el mismo procedimiento DFU. **La primera línea de la consola tras reset dice la versión**
|
||||
(`hbox-v5-bringup 0.5.0 built …`): si la línea `STAT` no termina en `stack=…`, la tarjeta sigue con 0.1.1.
|
||||
|
||||
!!! tip "Consola 0.4.0"
|
||||
!!! tip "Consola 0.5.0"
|
||||
Líneas `EVT` en cada cambio (stack, ignición, 5 V, cargador, NMEA, fix), `STAT` con nombres cada 5 s y `$PHLG` cada 10 s
|
||||
(misma trama de salud que lee el servidor). Teclear `l` para la leyenda y `v` para los puntos de medición de la etapa RTK.
|
||||
Para encender la etapa RTK: `P` y luego `Y` (requiere 5 V en P2).
|
||||
|
||||
!!! warning "0.4.0 = firmware de campo mínimo — AUTO de fábrica"
|
||||
!!! warning "0.5.0 = firmware de campo mínimo + telemetría en banda — AUTO de fábrica"
|
||||
Desde 0.4.0 el modo AUTO es el **de fábrica**: la etapa RTK se enciende sola 3 s después de ver 5 V en P2 — no hace falta consola
|
||||
ni teclear `A`. Incluye watchdog 16 s, ventilador (política v3.0.3), recuperación de enlace (ciclo de etapa si el F9P deja de
|
||||
hablar 60 s; LIMP tras 3 ciclos), LEDs de carga y color de estado, corte térmico 60 °C y corte de batería 3.40 V.
|
||||
**El cargador sigue APAGADO: el paquete es de un solo uso hasta el firmware de producción.** Anotarlo en el acta de la unidad. Si se pierde el 5 V, la etapa **sigue encendida desde la batería**
|
||||
**El cargador sigue APAGADO: el paquete es de un solo uso hasta el firmware de producción.** Anotarlo en el acta de la unidad.
|
||||
|
||||
**0.5.0 — Modo B:** 10 s después de que el F9P empieza a hablar, el MCU conmuta K1 y la trama `$PHLG` viaja al servidor
|
||||
cada 30 s junto con el NMEA (columna **Salud** del monitor). En consola: `STAT … modoB=ON relay=… phlg_aire=…` (debe subir de
|
||||
1 en 1 cada 30 s). Si el MCU falla, K1 regresa solo al paso directo F9P→módem. `B` lo bloquea/permite para pruebas. Si se pierde el 5 V, la etapa **sigue encendida desde la batería**
|
||||
y la consola registra `EVT BACKUP inicio` / `BACKUP fin` con duración y tramas NMEA: esa es la prueba de respaldo.
|
||||
Se apaga sola si vbat < 3.40 V o temperatura > 50 °C. `A` de nuevo regresa a MANUAL. El cargador sigue apagado en todos los modos.
|
||||
|
||||
|
||||
@ -109,6 +109,14 @@ Sitio interno de Helgen Technologies. **Busca abajo lo que vas a hacer hoy.**
|
||||
|
||||
[Índice de actas](actas/index.md) · [Tablero de producción](tablero.html)
|
||||
|
||||
- ### 👤 Mi cuenta
|
||||
|
||||
---
|
||||
|
||||
Cambiar tu contraseña del sitio. Si la olvidaste, Leo te la restablece desde ahí mismo.
|
||||
|
||||
[Cambiar mi contraseña](cuenta.html)
|
||||
|
||||
- ### 🌿 Git y archivos de diseño
|
||||
|
||||
---
|
||||
|
||||
@ -53,7 +53,7 @@ si el módulo ya está asignado.
|
||||
| Ítem | Notas |
|
||||
|---|---|
|
||||
| Gabinete HBOX con panel perforado (GX16-5, 3 × SMA, RJ45, prensaestopas) | según el diseño mecánico vigente |
|
||||
| EinarBoard **V5-NN** aprobada, con firmware de bring-up **0.4.0 o superior** | ver acta de tarjeta |
|
||||
| EinarBoard **V5-NN** aprobada, con firmware de bring-up **0.5.0 o superior** | ver acta de tarjeta |
|
||||
| simpleRTK2B Pro **ARD-NNN** y módem 4G NTRIP Master **RTK-NNNN** aprobados e integrados | con sus bolsas |
|
||||
| Fuente **Mean Well SD-15B-05** (24 V → 5 V) | va dentro del HBOX |
|
||||
| Pack de batería **1S4P** con NTC (3 hilos → P4) | conectar al final |
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
# HBOX v5 — Prueba final de unidad ensamblada con firmware 0.4.0 (field-minimum)
|
||||
# HBOX v5 — Prueba final de unidad ensamblada con firmware 0.5.0 (field-minimum + telemetría)
|
||||
|
||||
**Alcance:** unidad completa (EinarBoard V5 + ArduSimple + módem 4G + batería + antenas) dentro del HBOX, antes de embarcar.
|
||||
**Firmware:** `hbox-v5-bringup_0.4.0.bin` (SHA-256 `a8de74b3…`) — descarga en <https://drogheda.helgen.online/descargas/>.
|
||||
**Firmware:** `hbox-v5-bringup_0.5.0.bin` (SHA-256 `98656ddc…`) — descarga en <https://drogheda.helgen.online/descargas/>.
|
||||
**Duración:** ~45 min por unidad (P1–P6). Registrar todo en el acta de ensamble (`HBOX5-Exx-Axxx-Rxxxx`).
|
||||
|
||||
!!! warning "Lo que 0.4.0 NO hace"
|
||||
!!! warning "Lo que 0.5.0 NO hace"
|
||||
**No carga la batería** (cargador apagado). El paquete es de un solo uso: sostiene la unidad cuando se va el 5 V del vehículo
|
||||
hasta 3.40 V y después la unidad solo funciona con alimentación del vehículo. Las unidades regresan a laptop (DFU en J11, 2 min)
|
||||
cuando salga el firmware de producción. Anotar en el acta: *«fw 0.4.0-fm, cargador OFF»*.
|
||||
cuando salga el firmware de producción. Anotar en el acta: *«fw 0.5.0-fm, cargador OFF»*.
|
||||
|
||||
## 0. Preparación
|
||||
- Consola: cable USB-C en **J11**, PuTTY/CoolTerm **115200**. Cada línea es `STAT`, `EVT` o `$PHLG`; teclear `l` para la leyenda.
|
||||
@ -20,7 +20,7 @@ Energizar 24 V (con ignición) y **no tocar la consola**.
|
||||
|
||||
| Esperado | Si falla |
|
||||
|---|---|
|
||||
| Banner `hbox-v5-bringup 0.4.0` · `modo=AUTO` · `inputs at boot: v5=5.2–5.4V(ok) ign=ON` | otra versión → re-flashear; `ign=OFF` → arnés pin 3 / divisor 22k-10k |
|
||||
| Banner `hbox-v5-bringup 0.5.0` · `modo=AUTO` · `inputs at boot: v5=5.2–5.4V(ok) ign=ON` | otra versión → re-flashear; `ign=OFF` → arnés pin 3 / divisor 22k-10k |
|
||||
| `---- RESULT: 12 PASS / 0 FAIL ----` · `>>> BOARD OK` | cualquier `[FAIL]` → tarjeta a revisión, no embarcar |
|
||||
| `EVT … STACK ON reason=auto` **sin teclear nada** (≈10 s tras el banner) | no enciende → `STAT` muestra `v5=` bajo 4.5 o `modo=MAN` (teclear `A`) |
|
||||
| `EVT … NMEA primera trama` < 3 s después · `EVT FAN ON (vehiculo: 5V+ign)` | sin NMEA → ArduSimple no arranca: medir SW_5V (MOD1 pin 5) ≈ 5 V |
|
||||
@ -33,7 +33,9 @@ Dejar la unidad 10 min encendida con el módem registrándose (LEDs del módem /
|
||||
|---|---|
|
||||
| `STAT … v5=5.2x–5.3x(ok)` **todo el tiempo**; nunca `(BAJO)` ni `EVT V5 BAJO` | `BAJO` → el riel cae con la ráfaga LTE: SD-15B ajustado a ~5.3 V en P2 (memo 24-Jul, PFI LTC4040) |
|
||||
| `tbat`/`tair`/`tmcu` estables (< 45 °C en banco) · `i2cerr=0` | `i2cerr` subiendo → bus I²C / soldadura U3/U4/U13 |
|
||||
| `$PHLG,0.4.0-fm,RUN,…` cada 10 s, `rec=0`, sin `LIMP` | `rec` > 0 sin causa → NMEA intermitente: revisar MOD1 / UART1 F9P |
|
||||
| `$PHLG,0.5.0-fm,RUN,…` cada 10 s, `rec=0`, sin `LIMP` | `rec` > 0 sin causa → NMEA intermitente: revisar MOD1 / UART1 F9P |
|
||||
| **Modo B:** ≈10 s tras la primera NMEA, `EVT MODOB ON`; luego `STAT … modoB=ON relay=` (sube cientos/s) `phlg_aire=` (+1 cada 30 s) | `modoB=off` permanente → sin NMEA en PA3 o `B` bloqueado; `relay=0` con `modoB=ON` → K1 / PA10 |
|
||||
| **Servidor:** en ≤ 2 min la unidad `H00xx` muestra la columna **Salud** (fw `0.5.0-fm`, vbat, temperaturas) y sigue reportando posición 1 Hz | posición sí pero sin Salud → el módem no reenvía la `$PHLG`: revisar `$$$AS+` (modo transparente) |
|
||||
|
||||
## P3 — Respaldo de batería (6 min) — **la prueba que nunca se había hecho en v5**
|
||||
Con la etapa ON: **desconectar el 24 V** (se va el 5 V del SD-15B). Esperar 5 min. Reconectar.
|
||||
@ -63,7 +65,7 @@ Con la etapa ON y > 90 s de encendida: **retirar el ArduSimple del socket** (o s
|
||||
## P6 — Apagado y acta (2 min)
|
||||
- Quitar ignición: `EVT IGN OFF`, `EVT FAN OFF (bateria: termostato)`; la etapa sigue ON mientras haya 5 V (el SD-15B sigue con 24 V).
|
||||
- Quitar 24 V: si la batería está, entra `BACKUP` (P3); dejarla hasta `SLEEP` **no** es necesario — reconectar y apagar con `P`.
|
||||
- Guardar el log. En el acta: versión (`0.4.0-fm`), `uid=` del banner, resultado P1–P5, `vbat` final, y la nota *cargador OFF*.
|
||||
- Guardar el log. En el acta: versión (`0.5.0-fm`), `uid=` del banner, resultado P1–P5, `vbat` final, y la nota *cargador OFF*.
|
||||
|
||||
## Lectura rápida del LED RGB (para el técnico de campo)
|
||||
| Color | Significado |
|
||||
|
||||
@ -37,7 +37,7 @@ y dejarla flasheada con el firmware de arranque entregado por ingeniería.
|
||||
| 1 | Fuente de banco 0–30 V con límite de corriente ajustable |
|
||||
| 1 | Cable USB-C ↔ USB-C (o USB-A ↔ USB-C) **con datos** (no solo carga) |
|
||||
| 1 | Laptop con **STM32CubeProgrammer** instalado (o `dfu-util` en terminal) |
|
||||
| 1 | Archivo de firmware: **`hbox-v5-bringup_0.4.0.bin`** — descarga: **https://drogheda.helgen.online/descargas/** (repo: `04-Source-Code/firmware/hbox_v5_bringup/release/`, SHA-256 inicia `a8de74b3`) |
|
||||
| 1 | Archivo de firmware: **`hbox-v5-bringup_0.5.0.bin`** — descarga: **https://drogheda.helgen.online/descargas/** (repo: `04-Source-Code/firmware/hbox_v5_bringup/release/`, SHA-256 inicia `98656ddc`) |
|
||||
| 1 | Jumper 2.54 mm para P11 (interruptor de corte) |
|
||||
| 1 | Lupa o lámpara con aumento |
|
||||
| — | Hoja de registro (sección 7) |
|
||||
@ -183,7 +183,7 @@ Solo para tarjetas que pasaron las secciones 1 a 4.
|
||||
2. Arriba a la derecha, en el selector de conexión elegir **USB**. Pulsar el botón de refrescar;
|
||||
debe aparecer `USB1`. Pulsar **Connect**.
|
||||
3. Ir a la pestaña **Erasing & Programming** (icono de flecha hacia abajo, lado izquierdo).
|
||||
4. **File path** → seleccionar `hbox-v5-bringup_0.4.0.bin`.
|
||||
4. **File path** → seleccionar `hbox-v5-bringup_0.5.0.bin`.
|
||||
5. **Start address** = `0x08000000`. Marcar **Verify programming** y **Run after programming**.
|
||||
6. Pulsar **Start Programming**. Esperar "File download complete" y "Verify… successful".
|
||||
7. Pulsar **Disconnect**.
|
||||
@ -192,7 +192,7 @@ Solo para tarjetas que pasaron las secciones 1 a 4.
|
||||
|
||||
```bash
|
||||
dfu-util -l # debe listar [0483:df11]
|
||||
dfu-util -a 0 -s 0x08000000:leave -D hbox-v5-bringup_0.4.0.bin
|
||||
dfu-util -a 0 -s 0x08000000:leave -D hbox-v5-bringup_0.5.0.bin
|
||||
```
|
||||
|
||||
### 5d. Verificación después del flasheo
|
||||
@ -206,7 +206,7 @@ dfu-util -a 0 -s 0x08000000:leave -D hbox-v5-bringup_0.4.0.bin
|
||||
| **D17** (LED azul) + LED de panel en P8 (si hay) | **Parpadean** a 1 Hz (MCU ejecutando el firmware) |
|
||||
| **DB1 → DB4** (LEDs del indicador de batería) | Se encienden **en secuencia** una vez, luego se apagan; después R, G, B en P14 si hay LED RGB conectado |
|
||||
| Laptop | Aparece un **puerto serie** nuevo ("STMicroelectronics Virtual COM Port" / `usbmodem…`) |
|
||||
| Terminal serie a **115200 baud** | Encabezado `hbox-v5-bringup 0.4.0` (cualquier otra version: re-flashear), luego líneas `[PASS]` / `[FAIL]`, y al final **`>>> BOARD OK`** |
|
||||
| Terminal serie a **115200 baud** | Encabezado `hbox-v5-bringup 0.5.0` (cualquier otra version: re-flashear), luego líneas `[PASS]` / `[FAIL]`, y al final **`>>> BOARD OK`** |
|
||||
|
||||
3. Si el terminal se abrió tarde y no se vio el reporte: teclear **`r`** en el terminal → repite
|
||||
todas las pruebas (y vuelve a encender los LEDs en secuencia).
|
||||
|
||||
@ -88,5 +88,6 @@ nav:
|
||||
- "Histórico — Proto bench v3": procedimientos/historico-proto-bench-v3.md
|
||||
- "Histórico — Protoboard": procedimientos/historico-protoboard.md
|
||||
|
||||
- "👤 Mi cuenta (cambiar contraseña)": cuenta.html
|
||||
- "⚙ Administración (Leo)": admin.html
|
||||
- "🚪 Salir": salir.html
|
||||
|
||||
@ -22,7 +22,7 @@ declare -a MAP=(
|
||||
"03-Architecture-Hardware/kicad/docs/GIT-GUIDE_Colaboradores-Externos_ES.md|docs/git/guia-git-es.md"
|
||||
"03-Architecture-Hardware/kicad/docs/GIT-GUIDE_External-Collaborators_EN.md|docs/git/git-guide-en.md"
|
||||
# Descargas para técnicos (firmware de bring-up v5)
|
||||
"04-Source-Code/firmware/hbox_v5_bringup/release/hbox-v5-bringup_0.4.0.bin|docs/descargas/hbox-v5-bringup_0.4.0.bin"
|
||||
"04-Source-Code/firmware/hbox_v5_bringup/release/hbox-v5-bringup_0.5.0.bin|docs/descargas/hbox-v5-bringup_0.5.0.bin"
|
||||
"04-Source-Code/firmware/hbox_v5_bringup/release/SHA256SUMS|docs/descargas/SHA256SUMS.txt"
|
||||
"04-Source-Code/firmware/hbox_v5_bringup/README.md|docs/descargas/hbox-v5-bringup-README.md"
|
||||
)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user