pagina Mi cuenta: cambio y restablecimiento de contrasena

This commit is contained in:
Leopoldo Rodríguez 2026-09-18 11:46:48 -06:00
parent 86af91fe4b
commit caee07fde9
14 changed files with 400 additions and 27 deletions

View File

@ -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 |

View File

@ -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

View File

@ -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
View 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>

View File

@ -1 +1 @@
a8de74b3d7768e9d5656ae0bff05475445c0cd2599ecfdc4ab081e517ed3d6ef hbox-v5-bringup_0.4.0.bin
98656ddcc4e209e291d554341318c74d3f05713ce7198c7f6dc2d816eb3c6c3f hbox-v5-bringup_0.5.0.bin

View File

@ -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) ·

View File

@ -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.

View File

@ -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
---

View File

@ -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 |

View File

@ -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 |

View File

@ -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).

View File

@ -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

View File

@ -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"
)