actas: modulos ArduSimple (ARD) y modem 4G (RTK-NNNN), integracion y ensamble con serie automatica HBOX5-Exx-Axxx-Rxxxx; procedimientos; fw bring-up 0.2.0

This commit is contained in:
Leopoldo Rodríguez 2026-09-17 10:43:39 -06:00
parent f637d7813e
commit 1a31352bda
21 changed files with 966 additions and 156 deletions

View File

@ -17,7 +17,10 @@ Rutas:
GET /api/acta?tipo=&serie= -> {exists, acta?}
POST /api/actas -> crea (409 si ya existe una real)
DELETE /api/acta?tipo=&serie= -> borra (solo admin)
GET /api/actas -> listado (ingeniería)
GET /api/actas -> listado (ingeniería); incluye 'componentes' en actas compuestas
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
from flask import Flask, request, jsonify, abort
@ -25,6 +28,7 @@ from flask import Flask, request, jsonify, abort
app = Flask(__name__)
BASE = os.environ.get("ACTAS_DIR", "/srv/projects/drogheda/actas")
ADMINS = {u.strip().lower() for u in os.environ.get("ACTAS_ADMINS", "leo").split(",") if u.strip()}
TIPOS_UNICOS = {"ensamble"} # tipos en los que un componente solo puede usarse en un acta real
SAFE = re.compile(r"[^A-Za-z0-9_\-]")
def slug(s, n=40): return SAFE.sub("", str(s or "").strip())[:n]
@ -35,6 +39,27 @@ def current_user():
def is_admin():
return current_user().lower() in ADMINS
def componentes_en_uso(tipo, serie_actual, componentes):
"""Devuelve [(clave, valor, serie_otra)] para componentes ya usados por OTRA acta real del mismo tipo."""
carpeta = os.path.join(BASE, tipo)
if not os.path.isdir(carpeta) or not isinstance(componentes, dict):
return []
usados = []
for fn in os.listdir(carpeta):
if not fn.endswith(".json"):
continue
try:
with open(os.path.join(carpeta, fn), encoding="utf-8") as f:
d = json.load(f)
except Exception:
continue
if d.get("serie") == serie_actual or not isinstance(d.get("componentes"), dict):
continue
for k, v in componentes.items():
if v and d["componentes"].get(k) == v:
usados.append((k, v, d.get("serie")))
return usados
def find_latest(tipo, serie):
"""Ruta del acta real más reciente para (tipo, serie), o None."""
carpeta = os.path.join(BASE, tipo)
@ -56,7 +81,7 @@ def whoami():
@app.get("/api/acta")
def get_acta():
tipo = slug(request.args.get("tipo")); serie = slug(request.args.get("serie"), 20)
tipo = slug(request.args.get("tipo")); serie = slug(request.args.get("serie"), 24)
if not tipo or not serie:
return jsonify(ok=False, error="faltan tipo/serie"), 400
ruta = find_latest(tipo, serie)
@ -76,7 +101,7 @@ def crear():
if not isinstance(d, dict):
return jsonify(ok=False, error="Se esperaba un objeto"), 400
tipo = slug(d.get("tipo")); serie = slug(d.get("serie"), 20)
tipo = slug(d.get("tipo")); serie = slug(d.get("serie"), 24)
tecnico = str(d.get("tecnico", "")).strip()
if not tipo or not serie:
return jsonify(ok=False, error="Faltan 'tipo' o 'serie'"), 400
@ -92,6 +117,17 @@ def crear():
return jsonify(ok=False, existe=True,
error=f"Ya existe un acta para {serie}. Pide a Leo que la borre para rehacerla."), 409
# actas compuestas (ensamble): un módulo no puede pertenecer a dos unidades
comps = d.get("componentes")
if isinstance(comps, dict):
comps = {slug(k, 20): slug(v, 20) for k, v in comps.items()}
d["componentes"] = comps
if not es_prueba and tipo in TIPOS_UNICOS:
usados = componentes_en_uso(tipo, serie, comps)
if usados:
det = "; ".join(f"{v} ya está en {s}" for _, v, s in usados)
return jsonify(ok=False, error=f"Módulo ya asignado: {det}. Pide a Leo que revise."), 409
ts = datetime.datetime.now().replace(microsecond=0)
iso = ts.isoformat()
d["_recibido"] = iso
@ -121,7 +157,7 @@ def crear():
def borrar_acta():
if not is_admin():
return jsonify(ok=False, error="Solo un administrador puede borrar actas."), 403
tipo = slug(request.args.get("tipo")); serie = slug(request.args.get("serie"), 20)
tipo = slug(request.args.get("tipo")); serie = slug(request.args.get("serie"), 24)
if not tipo or not serie:
return jsonify(ok=False, error="faltan tipo/serie"), 400
carpeta = os.path.join(BASE, tipo)
@ -180,9 +216,12 @@ def listar():
d = json.load(f)
except Exception:
continue
filas.append({"tipo": tipo, "archivo": f"{tipo}/{fn}",
"serie": d.get("serie"), "tecnico": d.get("tecnico"),
"resultado": d.get("resultado"), "recibido": d.get("_recibido")})
fila = {"tipo": tipo, "archivo": f"{tipo}/{fn}",
"serie": d.get("serie"), "tecnico": d.get("tecnico"),
"resultado": d.get("resultado"), "recibido": d.get("_recibido")}
if isinstance(d.get("componentes"), dict):
fila["componentes"] = d["componentes"]
filas.append(fila)
filas.sort(key=lambda r: (r.get("recibido") or ""), reverse=True)
return jsonify(ok=True, total=len(filas), actas=filas)

View File

@ -1,34 +1,56 @@
# Actas y control de calidad
Formularios que el técnico llena **en pantalla mientras prueba o ensambla** cada
tarjeta. Al terminar se firman (nombre + conformidad) y se envían al servidor, donde
quedan registrados por número de serie. Así queda constancia de que se siguió el
procedimiento en cada unidad.
Formularios que el técnico llena **en pantalla mientras prueba o ensambla**. Al terminar se
firman (nombre + conformidad) y se envían al servidor, donde quedan registrados por número de
serie. Un acta real se envía **una sola vez** por serie; para rehacerla, Leo la borra.
## Flujo de una unidad HBOX v5
```
EinarBoard V5-NN ──────► Acta de tarjeta ────────┐
ArduSimple ARD-NNN ────► Acta módulo ArduSimple ─┤
4G NTRIP Master RTK-NNNN ► Acta módem 4G ─────────┼─► Acta de integración (ArduSimple + 4G) ──► Acta de ensamble
│ (pareja que irá junta) (unidad HBOX5-Exx-Axxx-Rxxxx)
└──────────────────────────────────────────────────┘
```
El acta de ensamble **no deja seleccionar** un módulo que no esté aprobado, que no tenga su
integración, o que ya esté montado en otra unidad. El número de la unidad se calcula solo a partir
de los tres módulos elegidos: `HBOX5-E07-A010-R0006` = EinarBoard V5-07 + ArduSimple ARD-010 + módem 4G RTK-0006.
## Formularios
**[▶ Acta de prueba — EinarBoard V5](../formularios/v5.html)**
Lote de 15 tarjetas (V5-01 … V5-15). Procedimiento verificado §1–§6: inspección,
continuidad, rieles 3V3 / 5 V / 24 V, flasheo DFU y POST.
Lote de 15 tarjetas (V5-01 … V5-15; 30 seleccionables). Procedimiento: [Prueba y flasheo v5](../procedimientos/v5-prueba-y-flasheo.md).
**[▶ Acta de prueba — EinarBoard V4](../formularios/v4.html)** — *borrador*
V4-01 … V4-15. Adaptado de V5; la sección de MCU/firmware está marcada como
**pendiente de definir** para V4. Revisar antes de uso oficial.
**[▶ Acta de módulo — simpleRTK2B Pro / ArduSimple (ARD-001 … ARD-030)](../formularios/ardusimple.html)**
Un acta por receptor: visual, fix GNSS, configuración Helgen. Procedimiento: [Módulos e integración §2](../procedimientos/v5-modulos-integracion.md).
**[▶ Checklist de ensamble — HBOX](../formularios/ensamble.html)** — *borrador*
Control de calidad durante el armado: recepción de PCBA, módulos, mecánica, arnés,
etiquetado y energizado final. Ver la [guía de ensamble](../montaje/guia-ensamble-qc.md).
**[▶ Acta de módem — 4G NTRIP Master (RTK-0001 … RTK-0030)](../formularios/modem4g.html)**
Un acta por módem: IMEI/ICCID, configuración, registro en red. Procedimiento: [Módulos e integración §3](../procedimientos/v5-modulos-integracion.md).
**[▶ Acta de integración — ArduSimple + 4G](../formularios/integracion.html)**
La pareja que irá junta: RTK FIX con correcciones NTRIP. Procedimiento: [Módulos e integración §4](../procedimientos/v5-modulos-integracion.md).
**[▶ Acta de ensamble — unidad HBOX v5](../formularios/ensamble.html)**
Selecciona los tres módulos aprobados → número de unidad automático → armado, energizado desde 24 V,
batería y respaldo. Procedimiento: [Ensamble de la unidad](../procedimientos/v5-ensamble.md).
**[▶ Acta de prueba — EinarBoard V4](../formularios/v4.html)** — *borrador*.
## Cómo se usan
1. Abre el formulario de la versión que estás probando y **elige el número de serie**.
2. Ve llenando cada sección conforme avanzas. El formulario **se guarda solo** en el
navegador: si se cierra, al reabrir con el mismo número de serie recuperas lo escrito.
3. Al final marca el **resultado** (Aprobada / Falla), escribe tu **nombre** y confirma
que seguiste el procedimiento.
4. Pulsa **Enviar acta firmada**. Queda registrada en el servidor.
1. Abre el formulario y **elige el número de serie** (o los módulos, en integración y ensamble).
2. Ve llenando cada sección conforme avanzas. El formulario **se guarda solo** en el navegador: si se
cierra, al reabrir con la misma serie recuperas lo escrito.
3. Al final marca el **resultado** (Aprobada / Falla), escribe tu **nombre** y confirma que seguiste
el procedimiento.
4. Pulsa **Enviar acta firmada**. Queda registrada en el servidor y aparece en el
[tablero de estado](../tablero.html).
La serie **PRUEBA** sirve para practicar: no cuenta y se puede reenviar.
!!! note "Para ingeniería"
Los envíos se guardan en el servidor por tipo y número de serie. Índice rápido en
`…/actas/<tipo>/_indice.csv`, o el listado completo en
Los envíos se guardan en el servidor por tipo y número de serie (`…/actas/<tipo>/`). Las actas de
integración y ensamble llevan `componentes` con los módulos elegidos. Listado completo en
[`/api/actas`](/api/actas){:target="_blank"}.

View File

@ -1 +1 @@
91ca6f87a86686be33ce013110f214a7e94ae7c8909c9eea1dbe846d9b5247f3 hbox-v5-bringup_0.1.1.bin
8491625d494f9ee19a2cc0a7b00d62cc4ba5c08f0ada16572ac163ee10bf1e77 hbox-v5-bringup_0.2.0.bin

View File

@ -1,6 +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.
Bench mode (0.2.0): `P`+`Y` stack ON/OFF, `n` NMEA monitor (fix 4 = RTK FIX).
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.
@ -22,17 +23,21 @@ 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.1.1.bin
dfu-util -a 0 -s 0x08000000:leave -D release/hbox-v5-bringup_0.2.0.bin
```
or STM32CubeProgrammer → USB → Connect → Download `release/hbox-v5-bringup_0.1.1.bin` @ `0x08000000`.
or STM32CubeProgrammer → USB → Connect → Download `release/hbox-v5-bringup_0.2.0.bin` @ `0x08000000`.
## Release
| File | Version | SHA-256 (first 16) | Size |
|---|---|---|---|
| **`release/hbox-v5-bringup_0.1.1.bin`** | **0.1.1 (2026-09-16) — USE THIS** | `91ca6f87a86686be` | 55 104 B |
| **`release/hbox-v5-bringup_0.2.0.bin`** | **0.2.0 (2026-09-17) — USE THIS** | `8491625d494f9ee1` | 64 780 B |
| `release/hbox-v5-bringup_0.1.1.bin` | 0.1.1 — superseded (no bench mode) | `91ca6f87a86686be` | 55 104 B |
| `release/hbox-v5-bringup_0.1.0.bin` | 0.1.0 — SUPERSEDED (no float printf, ETH_CS driven) | `1a9a77ae479c8f86` | 41 040 B |
## Changelog
- **0.2.0** (2026-09-17): bench mode for the assembled unit — `P`+`Y` powers the RTK stack (only with 5 V on P2),
`P` again turns it off; `n` streams `$GxGGA` from the simpleRTK2B UART1 (PA3, 115200) for 20 s with fix/sats/HDOP;
`STAT` shows `stack=` and the last GGA. Charge stays disabled, K1 released. Uses the core's `Serial2` (USART2).
- **0.1.1** (2026-09-16, audit): build with `rtlib=nanofp` (floats printed blank in 0.1.0); `ETH_CS` now
`INPUT_PULLUP` instead of driving HIGH into the unpowered W5500; reset cause reports `PIN` only when
`PINRSTF` is set (else `UNKNOWN`).

Binary file not shown.

View File

@ -7,16 +7,19 @@ 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.1.1.bin**](hbox-v5-bringup_0.1.1.bin) | 0.1.1 (2026-09-16) | `91ca6f87a86686be…` ([SHA256SUMS](SHA256SUMS.txt)) | Sección 5 del [procedimiento de prueba y flasheo v5](../procedimientos/v5-prueba-y-flasheo.md) |
| [**hbox-v5-bringup_0.2.0.bin**](hbox-v5-bringup_0.2.0.bin) | **0.2.0** (2026-09-17) | `8491625d494f9ee1…` ([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.1.1.bin
dfu-util -a 0 -s 0x08000000:leave -D hbox-v5-bringup_0.2.0.bin
```
o con **STM32CubeProgrammer** → USB → Connect → Download del `.bin` en `0x08000000`.
!!! note "Tarjetas ya flasheadas con 0.1.1"
Re-flashear con 0.2.0 por el mismo procedimiento DFU antes del ensamble: 0.1.1 no tiene el modo de banco.
!!! warning "Solo para la tarjeta v5"
Este firmware **no** es el de producción: solo prueba la tarjeta (reloj, USB, cristales, I²C,
LEDs, ADC) sin encender la etapa RTK ni el cargador. **Nunca flashear `blackpillrtkcode_v3`

View File

@ -58,3 +58,15 @@ a.volver{color:var(--naranja);font-size:14px;text-decoration:none}
button.limpiar{background:#fff;color:var(--tinta2);border:1px solid var(--linea);
border-radius:10px;padding:13px 16px;font-size:15px;cursor:pointer}
button.limpiar:hover{border-color:var(--naranja);color:var(--naranja)}
/* ---- actas compuestas (ensamble / integración): estado de cada módulo ---- */
.datos{grid-template-columns:repeat(auto-fit,minmax(240px,1fr))}
.datos .serie-calc{grid-column:1 / -1;background:var(--naranja-suave);border:1px solid #ffd8a8;border-radius:10px;padding:10px 12px}
.datos .serie-calc input{font-weight:700;font-size:20px;letter-spacing:.04em;background:#fff}
.badge{display:inline-block;margin-top:6px;font-size:13px;font-weight:700;padding:3px 10px;border-radius:99px;background:#f1efe9;color:var(--tinta2)}
.badge.ok{background:var(--ok-bg);color:var(--ok)}
.badge.falla{background:var(--falla-bg);color:var(--falla)}
.badge.pend{background:#f1efe9;color:var(--tinta2)}
.radios{display:flex;flex-wrap:wrap;gap:8px 16px;margin-top:4px}
.radios .rad{font-weight:500;display:flex;align-items:center;gap:6px;cursor:pointer}
.radios input{width:22px;height:22px;accent-color:var(--naranja)}

View File

@ -2,13 +2,32 @@
- Un acta real se envía UNA vez: al elegir una serie ya enviada, se carga llena y bloqueada.
- Solo el administrador (leo) ve el botón "Borrar acta (reabrir)".
- La serie PRUEBA no se bloquea (para practicar).
Tipos de ítem: check | medida | texto | radio | nota · Envío: POST /api/actas */
Tipos de ítem: check | medida | texto | radio | nota · Envío: POST /api/actas
Modos de serie:
(a) simple: cfg.prefijo + cfg.series (cantidad) [+ cfg.desde, cfg.pad] → "RTK-005"
(b) compuesta: cfg.componentes = [{clave, etiqueta, prefijo, desde, hasta, pad, tipoActa}]
cfg.serie(sel) → número de serie calculado, p.ej. "HBOX5-E07-R010-M006"
cfg.prerequisitos = [{etiqueta, tipo, serie(sel)}] → otras actas que deben existir y estar OK
cfg.exigirAprobados = true → no se puede enviar si algún componente/prerrequisito no está aprobado
cfg.unicoPorComponente = true → un componente no puede estar en dos actas de este tipo */
(function () {
const $ = (t, a, ...k) => { const e = document.createElement(t); if (a) for (const p in a){ if(p==='html')e.innerHTML=a[p]; else if(p==='text')e.textContent=a[p]; else e.setAttribute(p,a[p]); } k.forEach(c=>e.append(c)); return e; };
const num = s => (String(s || '').match(/(\d+)\s*$/) || [,''])[1];
const esPrueba = s => String(s || '').toUpperCase().indexOf('PRUEBA') >= 0;
function opciones(sel, prefijo, desde, hasta, pad) {
sel.append($('option', { value: '', text: '— elegir —' }));
sel.append($('option', { value: prefijo + '-PRUEBA', text: '⚙ PRUEBA (no cuenta)' }));
for (let i = desde; i <= hasta; i++) {
const s = prefijo + '-' + String(i).padStart(pad, '0');
sel.append($('option', { value: s, text: s }));
}
}
function render(cfg) {
const root = document.getElementById('acta');
const key = () => 'acta:' + cfg.tipo + ':' + (serieSel.value || '');
const compuesta = Array.isArray(cfg.componentes) && cfg.componentes.length > 0;
let admin = false;
const wrap = $('div', { class: 'wrap' });
@ -24,25 +43,57 @@
if (cfg.borrador)
wrap.append($('div', { class: 'borrador', html: '⚠ <b>BORRADOR</b> — este formulario aún debe revisarse con ingeniería antes de usarlo como registro oficial.' }));
if (cfg.intro) wrap.append($('div', { class: 'nota-item', style: 'margin:0 0 10px', html: cfg.intro }));
// ---------------- cabecera: serie (simple o compuesta) + técnico ----------------
const datos = $('div', { class: 'datos' });
const serieSel = $('select', { id: 'f_serie' });
serieSel.append($('option', { value: '', text: '— elegir —' }));
serieSel.append($('option', { value: cfg.prefijo + '-PRUEBA', text: '⚙ PRUEBA (no cuenta)' }));
for (let i = 1; i <= (cfg.series || 15); i++) {
const s = cfg.prefijo + '-' + String(i).padStart(2, '0');
serieSel.append($('option', { value: s, text: s }));
}
const serieSel = $('select', { id: 'f_serie' }); // modo simple
const serieCalc = $('input', { type: 'text', id: 'f_serie_calc', readonly: 'readonly', placeholder: 'se calcula al elegir los módulos' }); // modo compuesto
const compSel = {}; // clave -> <select>
const compBadge = {}; // clave -> <span> estado
const preBadge = []; // prerrequisitos -> <span>
const tecnico = $('input', { type: 'text', id: 'f_tecnico', placeholder: 'Nombre y apellido' });
const dS = $('div'); dS.append($('label', { text: 'Número de serie de la tarjeta' }), serieSel);
const dT = $('div'); dT.append($('label', { text: 'Técnico' }), tecnico);
datos.append(dS, dT);
if (!compuesta) {
opciones(serieSel, cfg.prefijo, cfg.desde || 1, (cfg.desde || 1) + (cfg.series || 15) - 1, cfg.pad || 2);
const dS = $('div'); dS.append($('label', { text: cfg.etiquetaSerie || 'Número de serie' }), serieSel);
const dT = $('div'); dT.append($('label', { text: 'Técnico' }), tecnico);
datos.append(dS, dT);
} else {
cfg.componentes.forEach(c => {
const sel = $('select', { id: 'f_comp_' + c.clave, 'data-comp': c.clave });
opciones(sel, c.prefijo, c.desde || 1, c.hasta || ((c.desde || 1) + (c.series || 15) - 1), c.pad || 2);
const badge = $('span', { class: 'badge pend', text: '—' });
const d = $('div'); d.append($('label', { text: c.etiqueta }), sel, badge);
datos.append(d); compSel[c.clave] = sel; compBadge[c.clave] = badge;
});
const dSer = $('div', { class: 'serie-calc' }); dSer.append($('label', { text: cfg.etiquetaSerie || 'Número de serie de la unidad (automático)' }), serieCalc);
(cfg.prerequisitos || []).forEach(p => {
const b = $('span', { class: 'badge pend', text: '—' });
const d = $('div'); d.append($('label', { text: p.etiqueta }), b); dSer.append(d); preBadge.push(b);
});
datos.append(dSer);
const dT = $('div'); dT.append($('label', { text: 'Técnico' }), tecnico);
datos.append(dT);
}
const cab = $('section', { class: 'paso' }); cab.append(datos);
wrap.append(cab);
const banner = $('div', { class: 'msg', id: 'f_banner' });
wrap.append(banner);
// selección actual de componentes -> {clave: valor}
function seleccion() { const s = {}; for (const k in compSel) s[k] = compSel[k].value; return s; }
function serieActual() {
if (!compuesta) return serieSel.value;
const s = seleccion();
if (Object.values(s).some(v => !v)) return '';
if (Object.values(s).some(esPrueba)) return (cfg.prefijoPrueba || 'PRUEBA') + '-PRUEBA';
return cfg.serie(s);
}
const key = () => 'acta:' + cfg.tipo + ':' + (serieActual() || '');
// ---------------- secciones ----------------
cfg.secciones.forEach((sec, si) => {
const s = $('section', { class: 'paso' });
s.append($('h2', { text: sec.titulo }));
@ -60,11 +111,20 @@
box.classList.add('medida');
box.append($('span', { class: 'txt', text: it.txt }));
if (it.esperado) box.append($('span', { class: 'esp', html: 'Esperado: ' + it.esperado }));
box.append($('input', { type: 'text', inputmode: 'decimal', 'data-id': id, 'data-lbl': it.txt, placeholder: 'medido' }));
box.append($('input', { type: 'text', inputmode: it.modo || 'decimal', 'data-id': id, 'data-lbl': it.txt, placeholder: it.ph || 'medido' }));
if (it.u) box.append($('span', { class: 'u', text: it.u }));
} else if (it.tipo === 'texto') {
box.append($('label', { text: it.txt }));
box.append($('textarea', { rows: '2', 'data-id': id, 'data-lbl': it.txt, placeholder: it.ph || '' }));
box.append($('textarea', { rows: it.rows || '2', 'data-id': id, 'data-lbl': it.txt, placeholder: it.ph || '' }));
} else if (it.tipo === 'radio') {
box.append($('label', { text: it.txt }));
const g = $('div', { class: 'radios' });
(it.opciones || []).forEach(o => {
const l = $('label', { class: 'rad' });
l.append($('input', { type: 'radio', name: 'r_' + id, value: o, 'data-id': id, 'data-lbl': it.txt }), document.createTextNode(' ' + o));
g.append(l);
});
box.append(g);
} else if (it.tipo === 'nota') {
box.append($('div', { class: 'nota-item', html: it.txt }));
}
@ -73,11 +133,12 @@
wrap.append(s);
});
// ---------------- firma ----------------
const firma = $('section', { class: 'paso firma' });
firma.append($('h2', { text: 'Cierre y firma' }));
firma.append($('p', { class: 'sub', text: 'Al enviar, quedas registrado como responsable de esta prueba.' }));
const res = $('div', { class: 'resultado' });
const rOK = $('label', { class: 'ok' }); rOK.append($('input', { type: 'radio', name: 'resultado', value: 'OK' }), document.createTextNode('APROBADA'));
const rOK = $('label', { class: 'ok' }); rOK.append($('input', { type: 'radio', name: 'resultado', value: 'OK' }), document.createTextNode(cfg.textoOK || 'APROBADA'));
const rNO = $('label', { class: 'falla' }); rNO.append($('input', { type: 'radio', name: 'resultado', value: 'FALLA' }), document.createTextNode('FALLA'));
res.append(rOK, rNO);
firma.append($('label', { text: 'Resultado final' }), res);
@ -100,7 +161,7 @@
barra.append(estado, grp);
document.body.append(barra);
// ---- helpers de estado ----
// ---------------- helpers de estado ----------------
const conforme = () => document.getElementById('f_conforme');
function fields() { return wrap.querySelectorAll('[data-id]'); }
function setEnabled(on) {
@ -109,7 +170,7 @@
wrap.querySelectorAll('input[name=resultado]').forEach(el => el.disabled = !on);
}
function blankFields() {
fields().forEach(el => { if (el.type === 'checkbox') el.checked = false; else el.value = ''; });
fields().forEach(el => { if (el.type === 'checkbox' || el.type === 'radio') el.checked = false; else el.value = ''; });
const rr = wrap.querySelector('input[name=resultado]:checked'); if (rr) rr.checked = false;
conforme().checked = false; tecnico.value = '';
}
@ -118,8 +179,13 @@
conforme().checked = !!a.conforme;
if (a.resultado) { const el = wrap.querySelector(`input[name=resultado][value="${a.resultado}"]`); if (el) el.checked = true; }
for (const k in (a.respuestas || {})) {
const el = wrap.querySelector(`[data-id="${k}"]`); if (!el) continue;
if (el.type === 'checkbox') el.checked = !!a.respuestas[k].val; else el.value = a.respuestas[k].val || '';
const v = a.respuestas[k].val;
const els = wrap.querySelectorAll(`[data-id="${k}"]`); if (!els.length) continue;
els.forEach(el => {
if (el.type === 'checkbox') el.checked = !!v;
else if (el.type === 'radio') el.checked = (el.value === v);
else el.value = v || '';
});
}
}
function showEditable() {
@ -140,17 +206,77 @@
estado.textContent = 'Bloqueada';
}
// ---- autoguardado local (solo para actas nuevas, no enviadas) ----
// ---------------- verificación de componentes / prerrequisitos (modo compuesto) ----------------
let verif = {}; // clave -> 'OK' | 'FALLA' | 'SIN_ACTA' | 'PRUEBA' | 'EN_USO'
let usados = null; // { clave: { valor: serieUnidad } } — componentes ya asignados en otras actas de este tipo
function setBadge(el, st, extra) {
const map = { OK: ['ok', '✓ Aprobado'], FALLA: ['falla', '✕ FALLA'], SIN_ACTA: ['falla', '✕ Sin acta'],
PRUEBA: ['pend', '⚙ prueba'], EN_USO: ['falla', '⚠ ya en ' + (extra || 'otra unidad')], CONSULTANDO: ['pend', '…'] };
const m = map[st] || ['pend', '—'];
el.className = 'badge ' + m[0]; el.textContent = m[1];
}
async function estadoActa(tipo, serie) {
if (!serie) return 'SIN_ACTA';
if (esPrueba(serie)) return 'PRUEBA';
try {
const r = await fetch(`/api/acta?tipo=${encodeURIComponent(tipo)}&serie=${encodeURIComponent(serie)}`);
const j = await r.json();
if (!j.exists) return 'SIN_ACTA';
return j.acta && j.acta.resultado === 'FALLA' ? 'FALLA' : 'OK';
} catch (e) { return 'SIN_ACTA'; }
}
async function cargarUsados() {
if (!cfg.unicoPorComponente) { usados = {}; return; }
usados = {};
try {
const r = await fetch('/api/actas', { cache: 'no-store' }); const j = await r.json();
(j.actas || []).filter(a => a.tipo === cfg.tipo && a.componentes && !esPrueba(a.serie)).forEach(a => {
for (const k in a.componentes) { usados[k] = usados[k] || {}; usados[k][a.componentes[k]] = a.serie; }
});
} catch (e) {}
}
async function verificar() {
verif = {};
const s = seleccion();
const tareas = cfg.componentes.map(async c => {
const v = s[c.clave]; const b = compBadge[c.clave];
if (!v) { setBadge(b, null); verif[c.clave] = 'SIN_SELECCION'; return; }
setBadge(b, 'CONSULTANDO');
let st = await estadoActa(c.tipoActa, v);
if (st === 'OK' && usados && usados[c.clave] && usados[c.clave][v] && usados[c.clave][v] !== serieActual()) {
st = 'EN_USO'; setBadge(b, st, usados[c.clave][v]); verif[c.clave] = st; return;
}
setBadge(b, st); verif[c.clave] = st;
});
const pre = (cfg.prerequisitos || []).map(async (p, i) => {
const b = preBadge[i];
if (Object.values(s).some(v => !v)) { setBadge(b, null); verif['pre_' + i] = 'SIN_SELECCION'; return; }
setBadge(b, 'CONSULTANDO');
const st = await estadoActa(p.tipo, Object.values(s).some(esPrueba) ? 'PRUEBA' : p.serie(s));
setBadge(b, st); verif['pre_' + i] = st;
});
await Promise.all(tareas.concat(pre));
}
function todoAprobado() {
return Object.values(verif).every(v => v === 'OK' || v === 'PRUEBA');
}
// ---------------- autoguardado local ----------------
let t, locked = false;
function snapshot() {
const r = {};
fields().forEach(el => { r[el.dataset.id] = { lbl: el.dataset.lbl, val: el.type === 'checkbox' ? el.checked : el.value }; });
fields().forEach(el => {
if (el.type === 'radio') { if (el.checked) r[el.dataset.id] = { lbl: el.dataset.lbl, val: el.value }; else if (!r[el.dataset.id]) r[el.dataset.id] = { lbl: el.dataset.lbl, val: '' }; }
else r[el.dataset.id] = { lbl: el.dataset.lbl, val: el.type === 'checkbox' ? el.checked : el.value };
});
const rr = wrap.querySelector('input[name=resultado]:checked');
return { tipo: cfg.tipo, serie: serieSel.value, tecnico: tecnico.value,
const d = { tipo: cfg.tipo, serie: serieActual(), tecnico: tecnico.value,
conforme: conforme().checked, resultado: rr ? rr.value : '', respuestas: r };
if (compuesta) { d.componentes = seleccion(); d.verificacion = Object.assign({}, verif); }
return d;
}
function guardarLocal() {
if (locked || !serieSel.value) return;
if (locked || !serieActual()) return;
clearTimeout(t);
t = setTimeout(() => {
try { localStorage.setItem(key(), JSON.stringify(snapshot())); } catch (e) {}
@ -163,23 +289,31 @@
else estado.textContent = 'Nueva acta';
}
// ---- cargar una serie: consulta el servidor y bloquea si ya existe ----
// ---------------- cargar una serie ----------------
async function loadSerie() {
msg.className = 'msg'; locked = false;
const s = serieSel.value;
const s = serieActual();
if (compuesta) serieCalc.value = s || '';
blankFields(); showEditable();
if (!s) { estado.textContent = 'Elige el número de serie'; return; }
if (s.toUpperCase().indexOf('PRUEBA') >= 0) { cargarLocal(); estado.textContent = 'Modo prueba (no cuenta)'; return; }
if (compuesta) { if (usados === null) await cargarUsados(); await verificar(); }
if (!s) { estado.textContent = compuesta ? 'Elige los tres módulos' : 'Elige el número de serie'; return; }
if (esPrueba(s)) { cargarLocal(); estado.textContent = 'Modo prueba (no cuenta)'; return; }
estado.textContent = 'Consultando…';
let j = null;
try { const r = await fetch(`/api/acta?tipo=${encodeURIComponent(cfg.tipo)}&serie=${encodeURIComponent(s)}`); j = await r.json(); } catch (e) {}
if (j && j.exists) { locked = true; fillFrom(j.acta); showLocked(j.acta); }
else { cargarLocal(); }
else {
cargarLocal();
if (compuesta && cfg.exigirAprobados && !todoAprobado()) {
banner.className = 'msg err';
banner.innerHTML = '⛔ <b>No se puede ensamblar con esta selección.</b> Todos los módulos deben estar <b>aprobados</b> en su acta y no estar ya asignados a otra unidad. Revisa los indicadores de arriba.';
}
}
}
wrap.addEventListener('input', guardarLocal);
serieSel.addEventListener('change', loadSerie);
Object.values(compSel).forEach(sel => sel.addEventListener('change', loadSerie));
// ---- limpiar (solo borrador local; no aplica a enviadas) ----
limpiar.addEventListener('click', () => {
if (locked) return;
if (!confirm('¿Limpiar este formulario y empezar de nuevo?')) return;
@ -187,27 +321,28 @@
blankFields(); msg.className = 'msg'; estado.textContent = 'Formulario limpio'; window.scrollTo(0, 0);
});
// ---- borrar / reabrir (solo admin) ----
borrar.addEventListener('click', async () => {
if (!admin) return;
if (!confirm('¿Borrar el acta de ' + serieSel.value + '? Quedará disponible para rehacerse.')) return;
if (!confirm('¿Borrar el acta de ' + serieActual() + '? Quedará disponible para rehacerse.')) return;
borrar.disabled = true;
try {
const r = await fetch(`/api/acta?tipo=${encodeURIComponent(cfg.tipo)}&serie=${encodeURIComponent(serieSel.value)}`, { method: 'DELETE' });
const r = await fetch(`/api/acta?tipo=${encodeURIComponent(cfg.tipo)}&serie=${encodeURIComponent(serieActual())}`, { method: 'DELETE' });
const j = await r.json();
if (!r.ok || !j.ok) throw new Error(j.error || ('HTTP ' + r.status));
locked = false; blankFields(); showEditable();
locked = false; blankFields(); showEditable(); usados = null;
msg.className = 'msg ok'; msg.textContent = 'Acta borrada. Ya puede rehacerse.';
estado.textContent = 'Reabierta';
} catch (e) { msg.className = 'msg err'; msg.textContent = 'No se pudo borrar: ' + e.message; }
borrar.disabled = false;
});
// ---- envío ----
// ---------------- envío ----------------
btn.addEventListener('click', async () => {
if (locked) return;
const d = snapshot();
if (!d.serie) return fail('Elige el número de serie de la tarjeta.');
if (!d.serie) return fail(compuesta ? 'Elige los tres módulos.' : 'Elige el número de serie.');
if (compuesta && cfg.exigirAprobados && !esPrueba(d.serie) && !todoAprobado())
return fail('Hay módulos sin aprobar, sin acta o ya asignados a otra unidad. No se puede enviar.');
if (!d.tecnico.trim()) return fail('Escribe tu nombre.');
if (!d.resultado) return fail('Marca el resultado final (Aprobada o Falla).');
if (!d.conforme) return fail('Debes confirmar que seguiste el procedimiento.');
@ -215,9 +350,9 @@
try {
const r = await fetch('/api/actas', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(d) });
const j = await r.json();
if (r.status === 409) { // ya existía: recargar bloqueada
if (r.status === 409) {
try { localStorage.removeItem(key()); } catch (e) {}
await loadSerie();
usados = null; await loadSerie();
msg.className = 'msg err'; msg.textContent = j.error || 'Ya existe un acta para esta serie.';
btn.disabled = false; btn.textContent = 'Enviar acta firmada';
return;
@ -225,7 +360,8 @@
if (!r.ok || !j.ok) throw new Error(j.error || ('HTTP ' + r.status));
try { localStorage.removeItem(key()); } catch (e) {}
msg.className = 'msg ok';
msg.innerHTML = '✓ Acta enviada y registrada como <b>' + d.serie + '</b>.';
msg.innerHTML = '✓ Acta enviada y registrada como <b>' + d.serie + '</b>.'
+ (compuesta && !esPrueba(d.serie) ? '<br>Etiqueta de la unidad: <b style="font-size:18px;letter-spacing:.04em">' + d.serie + '</b>' : '');
if (!j.prueba) { locked = true; showLocked({ tecnico: d.tecnico, resultado: d.resultado, _recibido: new Date().toISOString() }); }
else { btn.disabled = false; btn.textContent = 'Enviar acta firmada'; estado.textContent = 'Prueba enviada'; }
window.scrollTo(0, document.body.scrollHeight);
@ -236,11 +372,10 @@
});
function fail(m) { msg.className = 'msg err'; msg.textContent = m; window.scrollTo(0, document.body.scrollHeight); }
// ---- init: saber si soy admin, luego cargar la serie elegida ----
(async () => {
try { const r = await fetch('/api/whoami'); const j = await r.json(); admin = !!j.admin; } catch (e) {}
loadSerie();
})();
}
window.Acta = { render };
window.Acta = { render, num };
})();

View File

@ -0,0 +1,52 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Acta de módulo — simpleRTK2B Pro (RTK)</title>
<link rel="stylesheet" href="acta.css">
</head>
<body>
<div id="acta"></div>
<script src="acta.js"></script>
<script>
Acta.render({
tipo: 'modulo-ard',
prefijo: 'ARD', desde: 1, series: 30, pad: 3, // ARD-001 … ARD-030
etiquetaSerie: 'Número de serie del módulo (etiqueta ARD-NNN)',
titulo: 'Acta de módulo — simpleRTK2B Pro (ZED-F9P)',
subtitulo: 'Un acta por receptor. Procedimiento: "Módulos e integración" §2. Sin EinarBoard.',
secciones: [
{ titulo: '2a · Inspección visual (sin energía)',
items: [
{ tipo:'medida', txt:'Etiqueta de fábrica ArduSimple (código impreso en el módulo)', modo:'text', ph:'p.ej. SRTK2B-…' },
{ tipo:'check', txt:'Etiqueta <b>ARD-NNN</b> puesta y legible, sin tapar SMA ni zócalo XBee.' },
{ tipo:'check', txt:'Pines del zócalo <b>XBee</b> (2 × 10) rectos; conector <b>SMA</b> firme.' },
{ tipo:'check', txt:'Tiras tipo Arduino (las que entran en MOD1) rectas y completas.' },
] },
{ titulo: '2b · Energizado y fix GNSS',
sub: 'Antena GNSS al SMA con vista al cielo. USB-C en POWER+GPS. u-center conectado.',
items: [
{ tipo:'check', txt:'LED <b>POWER</b> encendido; la laptop ve un puerto serie u-blox.' },
{ tipo:'medida', txt:'Versión de firmware del ZED-F9P (UBX-MON-VER)', modo:'text', ph:'p.ej. HPG 1.32' },
{ tipo:'medida', txt:'Tiempo hasta fix 3D', esperado:'< 2 min a cielo abierto', u:'min' },
{ tipo:'medida', txt:'Satélites en uso con fix 3D', esperado:'≥ 10', u:'sat' },
{ tipo:'check', txt:'LED <b>GPS FIX</b> parpadea con el fix.' },
] },
{ titulo: '2c · Configuración de Helgen',
sub: 'u-center → Tools → Receiver Configuration → Load → Transfer file → GNSS. Luego UBX-CFG-CFG → Save.',
items: [
{ tipo:'medida', txt:'Nombre del archivo de configuración cargado', modo:'text', ph:'p.ej. HBOX_F9P_115200_v1.txt' },
{ tipo:'check', txt:'Configuración transferida sin errores y <b>guardada</b> (CFG-CFG Save).' },
{ tipo:'check', txt:'Tras desconectar/reconectar USB, conecta a <b>115200</b> y sigue mostrando fix y <code>$GNGGA</code>.' },
{ tipo:'texto', txt:'Observaciones', ph:'ninguna / describir' },
] },
{ titulo: 'Cierre',
items: [
{ tipo:'check', txt:'Módulo en bolsa antiestática con su etiqueta ARD-NNN.' },
] },
]
});
</script>
</body>
</html>

View File

@ -3,78 +3,84 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Checklist de ensamble — HBOX (borrador)</title>
<title>Acta de ensamble — unidad HBOX v5</title>
<link rel="stylesheet" href="acta.css">
</head>
<body>
<div id="acta"></div>
<script src="acta.js"></script>
<script>
/* BORRADOR — checklist de control de calidad durante el ensamble del HBOX.
Basado en el BOM de ensamble y el pinout de arneses. Ingeniería debe validar
los pasos antes de usarlo como registro oficial. */
Acta.render({
tipo: 'ensamble',
prefijo: 'HBOX',
series: 15,
borrador: true,
titulo: 'Control de calidad de ensamble — HBOX',
subtitulo: 'Llena mientras armas la unidad. Un acta por equipo (número de serie).',
titulo: 'Acta de ensamble — unidad HBOX v5',
subtitulo: 'Una unidad a la vez. Procedimiento: "Ensamble de la unidad y energizado final".',
intro: '<b>Primero el acta, luego el desarmador.</b> Selecciona los tres módulos: el sitio verifica que cada uno esté <b>aprobado</b>, que la pareja ArduSimple+4G tenga su <b>integración</b> y que ninguno esté ya en otra unidad. El número de serie de la unidad se calcula solo.',
componentes: [
{ clave:'einar', etiqueta:'EinarBoard v5', prefijo:'V5', desde:1, hasta:30, pad:2, tipoActa:'prueba-v5' },
{ clave:'ard', etiqueta:'simpleRTK2B Pro (ArduSimple)', prefijo:'ARD', desde:1, hasta:30, pad:3, tipoActa:'modulo-ard' },
{ clave:'modem', etiqueta:'4G NTRIP Master', prefijo:'RTK', desde:1, hasta:30, pad:4, tipoActa:'modulo-4g' },
],
serie: s => 'HBOX5-E' + Acta.num(s.einar) + '-A' + Acta.num(s.ard) + '-R' + Acta.num(s.modem),
prefijoPrueba: 'HBOX5',
prerequisitos: [
{ etiqueta:'Integración ArduSimple + 4G', tipo:'integracion', serie: s => 'INT-A' + Acta.num(s.ard) + '-R' + Acta.num(s.modem) },
],
exigirAprobados: true,
unicoPorComponente: true,
textoOK: 'APROBADA — UNIDAD LISTA',
secciones: [
{ titulo: '1 · Recepción de la PCBA',
sub: 'Revisión de la tarjeta ensamblada que llega de JLCPCB, antes de montar nada.',
{ titulo: '2 · Preparación de la tarjeta (sin energía)',
items: [
{ tipo:'check', txt:'La tarjeta corresponde al modelo/revisión correcto (serigrafía).' },
{ tipo:'check', txt:'Sin componentes faltantes, torcidos, ni con soldadura fría (inspección visual con lupa).' },
{ tipo:'check', txt:'Sin puentes de soldadura, sobre todo en los ICs de paso fino.' },
{ tipo:'check', txt:'Conectores y zócalos rectos, sin pines doblados.' },
{ tipo:'texto', txt:'Defectos encontrados en la PCBA', ph:'ninguno / describir' },
{ tipo:'check', txt:'La EinarBoard física es la seleccionada arriba (etiqueta cerca de MH4).' },
{ tipo:'radio', txt:'JP2 (F2)', opciones:['1 = Li-ion','0 = LiFePO4'] },
{ tipo:'radio', txt:'JP3 (F1)', opciones:['0','1'] },
{ tipo:'radio', txt:'JP4 (F0)', opciones:['1','0'] },
{ tipo:'check', txt:'Posición esperada para el pack Li-ion 4.00 V: <b>JP2 = 1 · JP3 = 0 · JP4 = 1</b>. JP1 (0R) presente.' },
{ tipo:'check', txt:'P11 sin jumper (el interruptor de panel va ahí). Tarjeta fijada con separadores, sin flexión.' },
] },
{ titulo: '2 · Soldadura manual (THT y conectores)',
sub: 'Partes que se sueldan a mano después de la PCBA (si aplica en este lote).',
{ titulo: '3 · Módulos enchufables (sin energía)',
items: [
{ tipo:'check', txt:'Terminal de potencia <b>P1</b> (Phoenix/GX16) firme y con soldadura completa.' },
{ tipo:'check', txt:'Conectores JST (<b>P2, P4, P6, P8, P10, P11, P12</b>…) soldados y sin frío.' },
{ tipo:'check', txt:'Relevador <b>K1</b>, botón <b>SW1</b> y electrolíticos radiales bien asentados y en polaridad correcta.' },
{ tipo:'check', txt:'Zócalos de módulos (J1/J2, J7/J8, MOD1) soldados a ras y rectos.' },
{ tipo:'check', txt:'simpleRTK2B Pro en <b>MOD1</b>: pin "5V" alineado, cuatro tiras a fondo, ninguna clavija fuera.' },
{ tipo:'check', txt:'4G NTRIP Master en <b>J7/J8</b> de la EinarBoard (no en el simpleRTK2B), silueta coincide con la serigrafía.' },
{ tipo:'check', txt:'SIM presente. Nada roza la tapa ni componentes altos (K1, C10, L6).' },
] },
{ titulo: '3 · Módulos enchufables',
sub: 'Se instalan sobre la tarjeta ya probada. Verifica orientación antes de energizar.',
{ titulo: '4 · Panel y arnés interno (sin energía)',
items: [
{ tipo:'check', txt:'MCU (STM32) correctamente asentado y en la orientación correcta.' },
{ tipo:'check', txt:'simpleRTK2B Pro insertado en <b>MOD1</b> sin forzar pines.' },
{ tipo:'check', txt:'Módulo 4G insertado en su zócalo (2 mm) en la orientación correcta.' },
{ tipo:'check', txt:'Ninguna antena/módulo obstruye el cierre del gabinete.' },
{ tipo:'check', txt:'<b>P1</b> ← GX16: 1 = +24 V, 2 = GND, 3 = ignición, 4 = GND. Polaridad verificada con multímetro.' },
{ tipo:'medida', txt:'Salida de la SD-15B-05 ajustada en banco', esperado:'5.00 V', u:'V' },
{ tipo:'check', txt:'<b>P2</b> ← SD-15B (pin 1 = +5 V). Entrada de la SD-15B al +24 V/GND del GX16.' },
{ tipo:'check', txt:'<b>P6</b> ventilador (flujo hacia afuera) · <b>P8</b> LED panel · <b>P10</b> reset · <b>P11</b> interruptor de corte · <b>P12</b> NTC punto caliente.' },
{ tipo:'check', txt:'Antena GNSS: bulkhead SMA → SMA del simpleRTK2B, con fijador de rosca.' },
{ tipo:'check', txt:'2 antenas LTE: bulkheads SMA → u.FL del módulo 4G (clic), sin dobleces cerrados.' },
{ tipo:'check', txt:'<b>P4</b> (batería) todavía <b>NO</b> conectado. Cables sujetos; nada expuesto cerca de 24 V.' },
] },
{ titulo: '4 · Mecánica y gabinete',
{ titulo: '5 · Energizado desde 24 V (sin batería)',
sub: 'Fuente 24.0 V, límite 1.0 A al GX16. Interruptor de corte ON. USB en J11, terminal 115200.',
items: [
{ tipo:'check', txt:'Tarjeta fijada con tornillos/separadores M2.5/M3 en los barrenos correctos, sin flexión.' },
{ tipo:'check', txt:'Ventilador montado y con flujo en la dirección correcta.' },
{ tipo:'check', txt:'Conectores de panel (GX16, RJ45, antenas) alineados con sus recortes del gabinete.' },
{ tipo:'check', txt:'Sin cables pellizcados; el gabinete cierra sin forzar.' },
{ tipo:'medida', txt:'Corriente con la etapa RTK apagada', esperado:'< 150 mA', u:'mA' },
{ tipo:'check', txt:'D18 verde encendido · D17 azul y LED de panel parpadean · ventilador gira.' },
{ tipo:'check', txt:'Consola: encabezado <code>hbox-v5-bringup</code> con <b>12 PASS</b>.' },
{ tipo:'medida', txt:'STAT v5', esperado:'4.8 – 5.1 V', u:'V' },
{ tipo:'medida', txt:'STAT tmp102 / sht40', modo:'text', ph:'p.ej. 31.2 / 30.8 °C' },
{ tipo:'check', txt:'Comando <b>P</b> + <b>Y</b> → <code>STACK ON</code>. simpleRTK2B POWER encendido; 4G se registra (< 2 min).' },
{ tipo:'medida', txt:'Corriente con la etapa RTK encendida', esperado:'250 – 600 mA', u:'mA' },
{ tipo:'medida', txt:'Comando <b>n</b>: tiempo hasta <code>fix=4</code> (RTK FIX)', esperado:'< 10 min con antena al cielo', u:'min' },
{ tipo:'medida', txt:'Satélites reportados en <code>n</code>', esperado:'≥ 10', u:'sat' },
{ tipo:'check', txt:'Comando <b>P</b> de nuevo → <code>STACK OFF</code>; corriente vuelve a < 150 mA.' },
] },
{ titulo: '5 · Arnés y cableado',
sub: 'Verifica cada conector contra el pinout de arneses.',
{ titulo: '6 · Batería y respaldo',
sub: 'Etapa RTK apagada. Pack en P4 (1 = BAT+, 2 = GND, 3 = NTC).',
items: [
{ tipo:'check', txt:'<b>P1</b> alimentación de máquina: pin 1 = 24V, pin 2 = GND, pin 3 = ignición.' },
{ tipo:'check', txt:'<b>P2</b> entrada 5 V (Mean Well) en polaridad correcta.' },
{ tipo:'check', txt:'<b>P4</b> batería 1S4P + NTC conectado (pin 1 = BAT+, 2 = GND, 3 = NTC).' },
{ tipo:'check', txt:'<b>P6</b> ventilador y demás JST insertados a fondo (clic), cada uno en su lugar.' },
{ tipo:'check', txt:'Sin conectores intercambiados; colores/etiquetas del arnés coinciden.' },
{ tipo:'texto', txt:'Observaciones del arnés', ph:'ninguno / describir' },
{ tipo:'medida', txt:'Voltaje del pack antes de conectar (BAT+ a GND)', esperado:'3.0 – 4.2 V', u:'V' },
{ tipo:'medida', txt:'STAT vbat con el pack conectado', esperado:'≈ voltaje del pack', u:'V' },
{ tipo:'check', txt:'<b>Respaldo:</b> al cortar 24 V la consola y los LEDs <b>siguen vivos</b>, sin reinicio (el encabezado no se reimprime). STAT v5 = 0.0.' },
{ tipo:'check', txt:'Al regresar 24 V, STAT v5 ≈ 5.0 sin reinicio.' },
{ tipo:'texto', txt:'Observaciones (reinicios, calentamiento, corriente fuera de rango)', ph:'ninguna / describir' },
] },
{ titulo: '6 · Etiquetado e identidad',
{ titulo: '7 · Cierre y etiquetado',
items: [
{ tipo:'check', txt:'Número de serie del equipo colocado de forma visible y legible.' },
{ tipo:'check', txt:'Etiqueta no tapa serigrafía, tornillos ni ventilación.' },
] },
{ titulo: '7 · Energizado final',
sub: 'Primera energización del equipo ya armado (con la tarjeta previamente aprobada en su acta de prueba).',
items: [
{ tipo:'check', txt:'Enciende sin olor, humo ni calentamiento anormal.' },
{ tipo:'check', txt:'LED(s) de estado responden como se espera.' },
{ tipo:'check', txt:'Ventilador gira.' },
{ tipo:'texto', txt:'Número de serie de la tarjeta (acta de prueba) usada en este equipo', ph:'p.ej. V5-07' },
{ tipo:'check', txt:'Etiqueta <b>exterior</b> con el número de la unidad junto al GX16; etiqueta <b>interior</b> en la tapa.' },
{ tipo:'check', txt:'Interruptor de corte en <b>OFF</b> para almacenar. Tapa cerrada con sello, tornillos en cruz, prensaestopas apretados.' },
] },
]
});

View File

@ -0,0 +1,52 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Acta de integración — RTK + 4G</title>
<link rel="stylesheet" href="acta.css">
</head>
<body>
<div id="acta"></div>
<script src="acta.js"></script>
<script>
Acta.render({
tipo: 'integracion',
titulo: 'Acta de integración — simpleRTK2B Pro + 4G NTRIP Master',
subtitulo: 'La pareja que irá junta en una unidad. Procedimiento: "Módulos e integración" §4.',
intro: 'Elige los dos módulos. Ambos deben estar <b>aprobados</b> en su propia acta; el sitio lo verifica al seleccionarlos.',
componentes: [
{ clave:'ard', etiqueta:'simpleRTK2B Pro (ArduSimple)', prefijo:'ARD', desde:1, hasta:30, pad:3, tipoActa:'modulo-ard' },
{ clave:'modem', etiqueta:'4G NTRIP Master', prefijo:'RTK', desde:1, hasta:30, pad:4, tipoActa:'modulo-4g' },
],
serie: s => 'INT-A' + Acta.num(s.ard) + '-R' + Acta.num(s.modem),
prefijoPrueba: 'INT',
etiquetaSerie: 'Identificador de la pareja (automático)',
exigirAprobados: true,
secciones: [
{ titulo: '4 · Montaje',
sub: '4G sobre el zócalo XBee del simpleRTK2B, 2 antenas LTE, antena GNSS al cielo, USB POWER+GPS. u-center a 115200.',
items: [
{ tipo:'check', txt:'Montaje completo <b>sin energía</b>; antenas conectadas antes de energizar.' },
{ tipo:'check', txt:'Fix <b>3D</b> en u-center.' },
{ tipo:'check', txt:'LED de red del 4G registrado; LED <b>NO RTK</b> del simpleRTK2B empieza a <b>parpadear</b> (llegan correcciones).' },
] },
{ titulo: '4 · RTK FIX',
items: [
{ tipo:'medida', txt:'Tiempo desde encendido hasta RTK FIX (LED NO RTK apagado / Fix Mode FIXED)', esperado:'< 10 min', u:'min' },
{ tipo:'medida', txt:'Satélites en uso', esperado:'≥ 10', u:'sat' },
{ tipo:'medida', txt:'Precisión estimada hAcc en FIX', esperado:'< 0.05 m', u:'m' },
{ tipo:'check', txt:'Se mantiene <b>FIXED 5 min</b> sin caer a FLOAT (antena quieta).' },
{ tipo:'radio', txt:'Confirmación en la plataforma Helgen (estación AS+K reportando)', opciones:['Confirmado por ingeniería','Pendiente de confirmar','No aparece'] },
{ tipo:'medida', txt:'Hora de la confirmación / a quién se avisó', modo:'text', ph:'p.ej. 12:40, Leo' },
{ tipo:'texto', txt:'Observaciones (FLOAT sin FIX, caídas, señal…)', ph:'ninguna / describir' },
] },
{ titulo: '4b · Cierre',
items: [
{ tipo:'check', txt:'Módulos separados con cuidado; cada uno en su bolsa. En la bolsa del ArduSimple anotada la pareja RTK-NNNN.' },
] },
]
});
</script>
</body>
</html>

View File

@ -0,0 +1,52 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Acta de módulo — 4G NTRIP Master</title>
<link rel="stylesheet" href="acta.css">
</head>
<body>
<div id="acta"></div>
<script src="acta.js"></script>
<script>
Acta.render({
tipo: 'modulo-4g',
prefijo: 'RTK', desde: 1, series: 30, pad: 4, // RTK-0001 … RTK-0030 (etiqueta del módem 4G)
etiquetaSerie: 'Número de serie del módem (etiqueta RTK-NNNN)',
titulo: 'Acta de módulo — 4G NTRIP Master',
subtitulo: 'Un acta por módem. Procedimiento: "Módulos e integración" §3. Antenas LTE conectadas ANTES de energizar.',
secciones: [
{ titulo: '3a · Inspección visual (sin energía)',
items: [
{ tipo:'medida', txt:'IMEI (etiqueta del módem)', modo:'numeric', ph:'15 dígitos' },
{ tipo:'medida', txt:'ICCID de la SIM Telcel', modo:'numeric', ph:'19–20 dígitos' },
{ tipo:'check', txt:'Etiqueta <b>RTK-NNNN</b> puesta; SIM insertada con clic en la dirección correcta.' },
{ tipo:'check', txt:'Pines XBee (2 × 10, 2 mm) rectos; conectores <b>u.FL</b> sin daño.' },
] },
{ titulo: '3b · Montaje sobre el simpleRTK2B y configuración',
sub: 'Sin energía: módulo en el zócalo XBee del simpleRTK2B. 2 antenas LTE + antena GNSS. USB POWER+GPS y USB XBEE conectados. Terminal en el puerto XBEE a 115200.',
items: [
{ tipo:'medida', txt:'simpleRTK2B usado como base para esta prueba', modo:'text', ph:'ARD-NNN' },
{ tipo:'check', txt:'Archivo <code>4G_NTRIP_HelgenSRV-NMEA.local.txt</code> enviado línea por línea; <b>ninguna</b> línea con <code>ERROR</code>.' },
{ tipo:'medida', txt:'ID de estación programado (<code>$$$AS+K</code>)', modo:'text', ph:'según ingeniería' },
{ tipo:'check', txt:'Configuración verificada al releer (el módulo responde con los valores enviados).' },
] },
{ titulo: '3c · Registro en la red',
sub: 'Esperar hasta 2 min con las antenas LTE conectadas.',
items: [
{ tipo:'check', txt:'LED de red del módulo pasa de parpadeo rápido a <b>lento o fijo</b> (registrado).' },
{ tipo:'medida', txt:'Operador reportado', modo:'text', esperado:'Telcel', ph:'Telcel' },
{ tipo:'medida', txt:'Señal (RSSI/CSQ) si el módulo la reporta', modo:'text', ph:'p.ej. 18/31 o −75 dBm' },
{ tipo:'medida', txt:'Tiempo hasta registrarse', esperado:'< 2 min', u:'min' },
{ tipo:'texto', txt:'Observaciones', ph:'ninguna / describir' },
] },
{ titulo: 'Cierre',
items: [
{ tipo:'check', txt:'Módulo separado del simpleRTK2B sin doblar pines; en bolsa con su etiqueta RTK-NNNN.' },
] },
]
});
</script>
</body>
</html>

View File

@ -13,7 +13,7 @@
Acta.render({
tipo: 'prueba-v5',
prefijo: 'V5',
series: 15,
series: 30, // 15 en el lote actual; 30 seleccionables
titulo: 'Acta de prueba y flasheo — EinarBoard V5',
subtitulo: 'Lote 15 tarjetas (JLC 22-Ago). Llena cada sección durante la prueba.',
secciones: [
@ -65,7 +65,7 @@ Acta.render({
{ tipo:'nota', txt:'Si algún valor está fuera de rango: marca FALLA y no flashees la tarjeta.' },
] },
{ titulo: '5 · Flasheo por USB DFU (J11)',
sub: 'Solo tarjetas que pasaron 1–4. Firmware: hbox-v5-bringup_0.1.0.bin @ 0x08000000.',
sub: 'Solo tarjetas que pasaron 1–4. Firmware: hbox-v5-bringup_0.1.1.bin @ 0x08000000.',
items: [
{ tipo:'check', txt:'DFU: mantener <b>SW2 (BOOT0)</b>, presionar y soltar <b>SW1 (RESET)</b>, soltar SW2 → aparece "STM32 BOOTLOADER" (0483:DF11).' },
{ tipo:'check', txt:'Flasheado con STM32CubeProgrammer (o dfu-util): "download complete" + "verify successful".' },
@ -76,7 +76,7 @@ Acta.render({
{ tipo:'check', txt:'<b>D17</b> (LED azul) parpadea a 1 Hz.' },
{ tipo:'check', txt:'<b>DB1 → DB4</b> se encienden en secuencia una vez.' },
{ tipo:'check', txt:'Aparece un puerto serie nuevo en la laptop.' },
{ tipo:'check', txt:'Encabezado <code>hbox-v5-bringup 0.1.0</code> y al final <code>&gt;&gt;&gt; BOARD OK</code>.' },
{ tipo:'check', txt:'Encabezado <code>hbox-v5-bringup 0.1.1</code> y al final <code>&gt;&gt;&gt; BOARD OK</code>.' },
{ tipo:'texto', txt:'Reporte / observaciones (anota cualquier línea [FAIL] completa)', ph:'---- RESULT: 12 PASS / 0 FAIL ----' },
] },
{ titulo: '6 · Cierre',

View File

@ -35,7 +35,8 @@ const qs = new URLSearchParams(location.search);
const tipo = qs.get('tipo') || '';
const serie = qs.get('serie') || '';
const cont = document.getElementById('cont');
const TITULOS = { 'prueba-v5':'Acta de prueba V5', 'prueba-v4':'Acta de prueba V4', 'ensamble':'Checklist de ensamble' };
const TITULOS = { 'prueba-v5':'Acta de prueba V5', 'prueba-v4':'Acta de prueba V4', 'modulo-ard':'Acta de módulo ArduSimple', 'modulo-4g':'Acta de módulo 4G', 'integracion':'Acta de integración RTK + 4G', 'ensamble':'Acta de ensamble — unidad HBOX' };
const COMP = { einar:'EinarBoard', ard:'simpleRTK2B Pro (ArduSimple)', modem:'4G NTRIP Master' };
function esc(s){ const d=document.createElement('div'); d.textContent=s==null?'':String(s); return d.innerHTML; }
function fmt(iso){ return iso? iso.replace('T',' ').slice(0,16):''; }
@ -50,6 +51,9 @@ function render(a){
+ '<div class="c"><div class="l">Fecha</div><div class="v">'+esc(fmt(a._recibido))+'</div></div>'
+ '<div class="c"><div class="l">Usuario</div><div class="v">'+esc(a._usuario||'—')+'</div></div>'
+ '</div>';
if(a.componentes){
h += '<div class="hdr">' + Object.keys(a.componentes).map(k=>'<div class="c"><div class="l">'+esc(COMP[k]||k)+'</div><div class="v">'+esc(a.componentes[k])+'</div></div>').join('') + '</div>';
}
// agrupar respuestas por sección (prefijo sN_)
const R = a.respuestas || {};
const keys = Object.keys(R).sort((x,y)=>{

View File

@ -0,0 +1,176 @@
---
title: HBOX v5 — Procedimiento de ensamble de la unidad y energizado final
tags: [project-drogheda, hardware, hbox, v5, ensamble, checklist, procedimiento]
project: Project Drogheda
author: Helgen Technologies
date: 2026-09-17
version: 1.0
status: ACTIVO — una unidad a la vez; se registra en el acta "Ensamble"
audience: técnicos de banco
---
# HBOX v5 — Ensamble de la unidad y energizado final
**Entrada:** tres módulos **ya aprobados** en sus actas — una EinarBoard **V5-NN** (acta de prueba y
flasheo), un simpleRTK2B Pro **ARD-NNN** y un módem 4G NTRIP Master **RTK-NNNN** que pasaron juntos la
**integración**. **Salida:** una unidad HBOX cerrada, etiquetada **HBOX5-Exx-Axxx-Rxxxx**, encendida y
reportando.
**Duración estimada:** 60–90 min por unidad.
> ⛔ **REGLA 1 — Primero el acta, luego el desarmador.** Abrir el acta de ensamble, seleccionar los
> tres módulos y esperar a que el sitio muestre los tres en **verde (Aprobado)** y la pareja ArduSimple+4G
> con **integración OK**. Si alguno sale en rojo o "sin acta", **no se ensambla** con ese módulo.
>
> ⛔ **REGLA 2 — Batería al final.** El pack 1S4P se conecta en P4 solo en la sección 6, con la unidad
> ya energizada desde 24 V y con el NTC del pack en P4-3.
>
> ⛔ **REGLA 3 — Todo módulo se inserta sin energía**, mirando la orientación dos veces. Un XBee al
> revés o un simpleRTK2B corrido un pin se destruye al encender.
---
## 0. El número de serie de la unidad
El acta lo compone automáticamente al seleccionar los módulos:
```
HBOX5 - E07 - A010 - R0006
│ │ │ └── módem 4G NTRIP Master RTK-0006
│ │ └────────── simpleRTK2B Pro (ArduSimple) ARD-010
│ └──────────────── EinarBoard V5-07
└──────────────────────── producto HBOX, tarjeta v5
```
Ese texto va en la **etiqueta exterior** del HBOX (panel, junto al GX16) y en la **etiqueta interior**
(cara interna de la tapa). Un módulo solo puede pertenecer a **una** unidad: el sitio rechaza el acta
si el módulo ya está asignado.
---
## 1. Material y herramientas
| Í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.2.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 |
| Ventilador 5 V 2 hilos (P6), LED de panel (P8), botón de reset (P10), interruptor de corte (P11), NTC de punto caliente (P12) | arnés interno |
| Pigtails de antena: GNSS SMA-hembra bulkhead → SMA del simpleRTK2B; 2 × LTE SMA bulkhead → u.FL del módulo 4G | |
| Separadores M2.5/M3, tornillería, bridas, fijador de rosca para SMA | |
| Fuente de banco 24 V / 1 A con límite de corriente, multímetro, laptop con terminal serie | |
| Etiquetas: exterior e interior con el número de unidad | impresas desde el acta |
---
## 2. Preparación de la tarjeta (sin energía)
- [ ] EinarBoard **V5-NN** corresponde a la seleccionada en el acta (leer la etiqueta cerca de MH4).
- [ ] Jumpers del cargador en la posición del pack Li-ion 4.00 V: **JP2 (F2) = 1 · JP3 (F1) = 0 · JP4 (F0) = 1**.
Anotar en el acta la posición de cada uno. *(Si el pack cambia a LiFePO4, ingeniería indicará JP2 = 0.)*
- [ ] Jumper **JP1** (0R, alimentación del ventilador) presente.
- [ ] **P11** (corte) **abierto** — sin jumper; el interruptor de panel se conecta ahí en la sección 4.
- [ ] Tarjeta fijada en la platina/gabinete con separadores en **MH1–MH6** (y MH7–MH12 según el diseño) sin flexionar la tarjeta; tornillos con apriete suave.
## 3. Módulos enchufables (sin energía)
- [ ] **simpleRTK2B Pro ARD-NNN** en **MOD1**: alinear el pin **"5V"** del zócalo UNO con el pin 5V del
módulo; las cuatro tiras entran a fondo y parejas; **ninguna** clavija queda fuera del zócalo.
- [ ] **Módem 4G NTRIP Master RTK-NNNN** en **J7/J8** (zócalo XBee de 2 mm de la EinarBoard, **no** en el
simpleRTK2B): la silueta del módulo coincide con la serigrafía; pin 1 (VCC) del lado marcado.
- [ ] SIM presente en el módulo 4G (ya verificada en su acta).
- [ ] Nada toca la tapa ni los separadores; los módulos no rozan componentes altos (K1, C10, L6).
## 4. Panel y arnés interno (sin energía)
Conectar contra el pinout; cada JST hace **clic**.
| Conector | Qué se conecta | Verificar |
|---|---|---|
| **P1** (GX16-5 del panel) | pin 1 = +24 V · pin 2 = GND · pin 3 = ignición · pin 4 = GND | polaridad con multímetro **antes** de conectar al panel |
| **SD-15B-05** | entrada: +24 V/GND del GX16 (empalme en Y) · salida +5 V/GND → **P2** (pin 1 = +5 V) | ajustar salida a **5.0 V** en banco antes de montar |
| **P6** | ventilador (pin 1 = +5V_FAN, pin 2 = retorno) | dirección de flujo: aire **hacia afuera** por la rejilla |
| **P8** | LED de panel (pin 1 = ánodo, pin 2 = cátodo) | LED asoma por el panel |
| **P10** | botón de reset externo | normalmente abierto |
| **P11** | interruptor de corte del panel (2 hilos) | en posición **ON** cierra pin 1–2 |
| **P12** | NTC de punto caliente pegado al módulo 4G o al disipador | con cinta térmica |
| **P4** | **NO conectar todavía** (batería, sección 6) | — |
| Antena **GNSS** | bulkhead SMA del panel → SMA del simpleRTK2B | fijador de rosca, apriete a mano + ¼ vuelta |
| Antenas **LTE (2)** | bulkheads SMA del panel → u.FL del módulo 4G | los u.FL hacen clic; ruta del cable sin dobleces < 10 mm |
| **J10** RJ45 / **P15** RS-485 / **P16** RS-232 | solo si el panel de esta unidad los lleva | tapas ciegas en los que no |
- [ ] Ningún cable pasa sobre el módulo 4G ni sobre la antena; cables sujetos con bridas al chasis.
- [ ] Sin conductores expuestos cerca de 24 V (P1, SD-15B).
## 5. Energizado desde 24 V (unidad abierta, sin batería)
1. Fuente de banco **24.0 V, límite 1.0 A**, salida apagada, conectada al **GX16** del panel (pin 1 = +, pin 2 = −).
Interruptor de corte (P11) en **ON**. USB-C en **J11** conectado a la laptop, terminal serie a 115200.
2. Encender la salida. En los primeros 10 s:
| Punto | Esperado |
|---|---|
| Corriente de la fuente | **< 150 mA** (etapa RTK aún apagada) |
| **D18** verde | Encendido |
| **D17** azul + LED de panel | Parpadean (firmware de bring-up corriendo) |
| Consola | Encabezado `hbox-v5-bringup`, reporte con **12 PASS**, luego `STAT … v5=4.9–5.1` |
| Ventilador | **Gira** (con 24 V presente el buck U2 se enciende solo) |
3. Anotar en el acta: `v5` (debe ser 4.8–5.1 V), `tmp102`, `sht40`.
4. **Encender la etapa RTK desde la consola:** teclear **`P`** y confirmar con **`Y`**. El firmware sube
`PWR_CTRL` y reporta `STACK ON`. En los siguientes 60 s:
| Punto | Esperado |
|---|---|
| Corriente de la fuente | sube a **250–600 mA** (pico corto al registrar el 4G) |
| simpleRTK2B: LED POWER | Encendido |
| 4G: LED de red | Buscando → registrado (< 2 min) |
| Consola, comando **`n`** | Muestra las tramas `$GNGGA` de PA3: **`fix=1`** (GPS) al minuto, **`fix=4`** (RTK FIX) en < 10 min con antena al cielo; `sats ≥ 10` |
5. Anotar en el acta el **tiempo hasta `fix=4`** y el número de satélites.
6. Teclear **`P`** de nuevo para **apagar** la etapa RTK. La corriente debe volver a < 150 mA.
> Si `n` no muestra tramas: el simpleRTK2B no está a 115200 en UART1 (volver al acta del módulo RTK:
> configuración Helgen) o MOD1 está corrido un pin. **Apagar antes de tocar nada.**
## 6. Batería y respaldo
1. Con la unidad encendida desde 24 V y la etapa RTK **apagada**: conectar el pack en **P4**
(pin 1 = BAT+, pin 2 = GND, pin 3 = NTC). Medir antes con multímetro: **3.0–4.2 V** entre BAT+ y GND.
2. En la consola, `STAT` debe mostrar **`vbat`** con el voltaje del pack y **`chrg`**: `0` = cargando
(el firmware de bring-up mantiene la carga **deshabilitada**, así que `chrg=1` es lo esperado aquí).
3. **Prueba de respaldo:** apagar la salida de la fuente de 24 V.
| Punto | Esperado |
|---|---|
| D18, D17 y consola | **Siguen vivos** (el LTC4040 conmuta a batería sin reinicio: el encabezado **no** se vuelve a imprimir) |
| `STAT` | `v5=0.0`, `vbat` con el voltaje del pack |
| Ventilador | Se detiene (no hay 24 V) — correcto |
4. Volver a encender 24 V: `STAT` vuelve a `v5≈5.0`. Sin reinicio.
5. Anotar: voltaje del pack, "respaldo OK sin reinicio".
## 7. Cierre, etiquetado y registro
- [ ] Etiqueta **exterior** `HBOX5-Exx-Axxx-Rxxxx` en el panel, junto al GX16. Etiqueta **interior** en la tapa.
- [ ] Interruptor de corte en **OFF** para almacenar (evita descargar la batería). Anotarlo en la etiqueta interior.
- [ ] Tapa cerrada con su sello; tornillos en cruz; prensaestopas apretados.
- [ ] Acta de ensamble enviada con resultado **APROBADA**.
---
## Anexo A — Notas de ingeniería
- **Firmware:** el bring-up **0.2.0** añade el modo de banco (`P` = stack ON/OFF con confirmación,
`n` = monitor NMEA de PA3, 20 s). Mantiene la carga deshabilitada (`CHRG_EN` alto) y K1 liberado.
La unidad ensamblada queda con este firmware hasta que exista el firmware de producción v5
(rediseño de gestión de batería); se re-flashea por J11 sin abrir el HBOX si J11 queda accesible —
**decidir si J11 asoma por el panel o se deja interno.**
- **Corriente en 24 V:** valores orientativos; los reales se fijan con las primeras tres unidades y
se actualizan aquí (bring-up 0.2.0 imprime `v5` pero no mide corriente).
- **Respaldo sin reinicio** es la prueba del LTC4040 + L6/SW_BAT en la tarjeta real (el EMC audit
marcó `/SW_BAT` 0.3 mm como crítico): si alguna unidad reinicia al cortar 24 V, anotar `vbat` y
temperatura — es evidencia para el respin.
- **Modo B / relevador K1:** no se prueba aquí; K1 queda liberado (camino directo F9P ↔ módem).

View File

@ -0,0 +1,211 @@
---
title: HBOX v5 — Prueba de módulos (simpleRTK2B Pro y 4G NTRIP Master) e integración RTK antes del ensamble
tags: [project-drogheda, hardware, hbox, v5, ardusimple, 4g, ntrip, testing, procedimiento]
project: Project Drogheda
author: Helgen Technologies
date: 2026-09-17
version: 1.0
status: ACTIVO — se aplica a cada módulo antes de asignarlo a una unidad
audience: técnicos de banco
---
# HBOX v5 — Prueba de módulos e integración RTK (antes del ensamble)
**Objetivo:** que ningún módulo llegue al ensamble sin haber demostrado que funciona **solo** y **en
pareja**: cada simpleRTK2B Pro obtiene fix GNSS, cada 4G NTRIP Master se registra en la red, y la
pareja RTK + 4G alcanza **RTK FIX** con correcciones NTRIP. Todo queda registrado en las actas
del sitio (`https://drogheda.helgen.online` → Actas).
**Duración estimada:** módulo RTK 10 min · módulo 4G 10 min · integración 15–30 min (depende del cielo).
> ⛔ **REGLA 1 — Un acta por módulo, con su número de serie.** No se prueba "un lote"; se prueba
> RTK-010, 4G-006, uno por uno.
>
> ⛔ **REGLA 2 — Sin EinarBoard en estas pruebas.** El simpleRTK2B se alimenta por su propio USB y el
> módulo 4G va en el zócalo XBee del simpleRTK2B. La tarjeta EinarBoard entra hasta el ensamble.
>
> ⛔ **REGLA 3 — Antenas siempre conectadas antes de energizar** el módulo 4G y el receptor GNSS.
> Un transmisor LTE sin antena se puede dañar.
---
## 0. Identidad de cada pieza (números de serie)
| Pieza | Etiqueta | Rango | Dónde va la etiqueta |
|---|---|---|---|
| EinarBoard v5 | **V5-01 … V5-15** (30 seleccionables) | lote JLC 22-Ago | esquina cerca de MH4 (ya puesta en la prueba de tarjeta) |
| simpleRTK2B Pro (ArduSimple) | **ARD-001 … ARD-030** | consecutivo de 3 dígitos | sobre el blindaje del ZED-F9P, sin tapar el conector SMA ni el zócalo XBee |
| 4G NTRIP Master (módem) | **RTK-0001 … RTK-0030** | consecutivo de 4 dígitos (serie hasta RTK-1000) | sobre el blindaje del módem, sin tapar los conectores u.FL de antena |
| Unidad HBOX (ensamblada) | **HBOX5-E07-A010-R0006** | se calcula sola en el acta de ensamble | panel exterior del HBOX |
El número de la unidad **se arma con los tres módulos que lleva dentro**: `E07` = EinarBoard V5-07,
`A010` = ArduSimple ARD-010, `R0006` = módem 4G RTK-0006. **Ojo:** la etiqueta "RTK-" es la del **módem 4G**, no la del receptor GNSS. No hace falta que los números coincidan entre sí: el técnico
**selecciona** en el acta de ensamble qué módulo (ya aprobado) va en qué unidad, y el sitio compone el
número. Así, con solo leer la etiqueta del HBOX se sabe qué hay dentro.
Anota también en cada acta la **etiqueta de fábrica** del módulo (el código que trae impreso
ArduSimple) y, en el 4G, el **IMEI** y el **ICCID de la SIM**. Es lo que después permite rastrear un
módulo en la plataforma.
---
## 1. Material
| Cant. | Ítem |
|---|---|
| 1 | Laptop con **u-center** (u-blox, Windows) o **u-center 2** |
| 2 | Cables USB-C **con datos** (el simpleRTK2B Pro tiene dos puertos USB: **POWER+GPS** y **XBEE**) |
| 1 | Antena GNSS multibanda (SMA) con vista al cielo (ventana con cielo abierto o exterior) |
| 2 | Antenas LTE (u.FL → SMA pigtail según el módulo) |
| 1 | SIM Telcel activa (nano-SIM) por módulo 4G |
| 1 | Archivo de configuración del **ZED-F9P** de Helgen (u-center `.txt`) — lo entrega ingeniería |
| 1 | Archivo de configuración del **4G NTRIP Master** con credenciales reales (`4G_NTRIP_HelgenSRV-NMEA.local.txt`) — lo entrega ingeniería, **no** se copia a otras máquinas |
| — | Etiquetas ARD-NNN (ArduSimple) y RTK-NNNN (módem 4G), marcador, bolsas antiestáticas |
---
## 2. Módulo simpleRTK2B Pro (ArduSimple) — acta **"Módulo ArduSimple"**
### 2a. Inspección visual (sin energía)
- [ ] Etiqueta **ARD-NNN** puesta y legible; anotar también la etiqueta de fábrica.
- [ ] Pines del zócalo **XBee** (2 hileras de 10) rectos; conector **SMA** firme; sin componentes golpeados.
- [ ] Pines de la tira tipo Arduino (los que entran en MOD1 de la EinarBoard) rectos y completos.
### 2b. Energizado y fix GNSS
1. Conectar la **antena GNSS** al SMA. Colocarla con vista al cielo.
2. Conectar el USB-C **POWER+GPS** a la laptop.
3. Verificar:
| Punto | Esperado |
|---|---|
| LED **POWER** | Encendido |
| Laptop | Aparece un puerto serie (u-blox GNSS receiver) |
| u-center → Receiver → Connect | Conecta; **Fix Mode** pasa de *No Fix* a **3D** en < 2 min a cielo abierto |
| Satélites en uso | **≥ 10** (cielo abierto) |
| LED **GPS FIX** | Parpadea cuando hay fix |
4. Anotar en el acta: **versión de firmware** del ZED-F9P (u-center → View → Messages → UBX-MON-VER),
satélites en uso, y el tiempo hasta el primer fix.
### 2c. Cargar la configuración de Helgen
1. u-center → **Tools → Receiver Configuration** → *Load configuration* → archivo F9P de Helgen →
**Transfer file → GNSS**.
2. Guardar en el receptor: **UBX-CFG-CFG → Save current configuration** (todas las memorias) → Send.
3. Desconectar y reconectar el USB. Volver a conectar en u-center a la **velocidad configurada por Helgen
(115200)**. Debe seguir mostrando fix y mensajes `$GNGGA`.
- [ ] Configuración cargada y guardada; sobrevive al reinicio.
> Si el módulo no da fix en 5 min a cielo abierto, probar con otra antena antes de marcarlo FALLA.
---
## 3. Módem 4G NTRIP Master — acta **"Módem 4G"**
### 3a. Inspección visual (sin energía)
- [ ] Etiqueta **RTK-NNNN** puesta; anotar **IMEI** (etiqueta del módem) e **ICCID** (tarjeta SIM).
- [ ] **SIM insertada** en la dirección correcta, con clic.
- [ ] Pines XBee (2 × 10, paso 2 mm) rectos; conectores **u.FL** de antena sin daño.
### 3b. Montaje sobre el simpleRTK2B y configuración
1. **Sin energía**, insertar el módulo 4G en el zócalo **XBee del simpleRTK2B Pro** (el módulo queda
sobre el receptor; el corte de la silueta indica la orientación — nunca forzar).
2. Conectar las **2 antenas LTE** a los u.FL del módulo.
3. Conectar la antena GNSS. Conectar el USB **POWER+GPS** (alimenta todo) **y** el USB **XBEE** (puerto de
configuración del módem).
4. En la laptop abrir un terminal serie en el puerto **XBEE** a **115200 baud** y enviar, línea por línea,
el archivo `4G_NTRIP_HelgenSRV-NMEA.local.txt` (solo las líneas `$$$AS+…`). Cada línea debe contestar
`OK` (o el eco de ArduSimple).
5. Al terminar: `$$$AS+I=1111` para entrar a config y **`$$$AS+K=…`** debe llevar el **ID de estación**
que indique ingeniería para este módulo (se anota en el acta).
- [ ] Configuración enviada completa, sin líneas con `ERROR`.
- [ ] ID de estación (`AS+K`) anotado en el acta.
### 3c. Registro en la red
Esperar hasta 2 min con las antenas LTE conectadas:
| Punto | Esperado |
|---|---|
| LED de **red / 4G** del módulo | Pasa de parpadeo rápido (buscando) a **parpadeo lento o fijo** (registrado) |
| Consulta de estado por el puerto XBEE (comando de estado de ArduSimple) | Reporta operador **Telcel** y señal (RSSI) |
- [ ] Registrado en la red Telcel. Anotar RSSI si el módulo lo reporta.
> Sin registro en 3 min: revisar que la SIM esté activa (probar la SIM en otro módulo aprobado). Si
> la SIM funciona en otro módulo y este no registra → **FALLA (4G)**.
---
## 4. Integración ArduSimple + 4G — acta **"Integración"**
Se hace con la **pareja** que después irá junta en una unidad (ARD-NNN + RTK-NNNN). El acta registra
los dos números; el mismo par se seleccionará en el ensamble.
Montaje: el mismo del punto 3b (4G sobre el simpleRTK2B, dos antenas LTE, antena GNSS al cielo, USB
POWER+GPS conectado; el USB XBEE puede quedar conectado para ver mensajes).
1. u-center conectado al puerto **POWER+GPS** (115200). Esperar fix **3D**.
2. Observar el LED **NO RTK** del simpleRTK2B: encendido = sin correcciones; **parpadeando = RTK FLOAT**;
**apagado = RTK FIX**.
3. En u-center: **Fix Mode** debe llegar a **3D/DGNSS/FIXED** (RTK fixed). Tiempo típico a cielo abierto:
1–5 min tras el registro 4G.
| Punto | Esperado | Anotar |
|---|---|---|
| Tiempo desde encendido hasta **RTK FIX** | < 10 min | minutos |
| Fix Mode en u-center | 3D/DGNSS/**FIXED** | — |
| Satélites en uso | ≥ 10 | número |
| Precisión estimada (u-center, hAcc) | < 0.05 m en FIX | metros |
| Estabilidad | Se mantiene FIXED **5 min** sin caer a FLOAT (con la antena quieta) | sí/no |
| Plataforma Helgen | Ingeniería confirma que la estación (ID `AS+K`) aparece reportando en el servidor | sí/no |
- [ ] RTK FIX alcanzado y estable 5 min.
- [ ] Confirmado en plataforma (o anotado "pendiente de confirmar" con hora).
> **Si llega a FLOAT pero nunca a FIX:** cielo insuficiente (probar exterior) o punto de montaje
> incorrecto; no es falla del módulo si el módulo 4G ya registró y hay correcciones (LED NO RTK
> parpadea). Si **nunca sale de "encendido"** (sin correcciones): revisar credenciales NTRIP con
> ingeniería antes de marcar FALLA.
### 4b. Cierre
- [ ] Apagar (desconectar USB). Separar el módulo 4G del simpleRTK2B **con cuidado, sin doblar pines**.
- [ ] Guardar cada módulo en su bolsa con su etiqueta. Anotar en la bolsa del ArduSimple: "pareja: RTK-NNNN".
- [ ] Enviar el acta de integración con los dos números.
---
## 5. Hoja rápida de estados (para el técnico)
| Módulo | Aprobado cuando… |
|---|---|
| ARD-NNN | fix 3D, ≥10 sat, configuración Helgen cargada y guardada |
| RTK-NNNN (módem) | SIM registrada en Telcel, configuración enviada sin errores, `AS+K` anotado |
| Pareja ArduSimple + 4G | RTK FIX estable 5 min con correcciones por 4G |
Un módulo con **FALLA** no se asigna a ninguna unidad: el acta de ensamble no lo dejará seleccionar.
---
## Anexo A — Notas de ingeniería
- En la **unidad v5** el módulo 4G **no** va sobre el simpleRTK2B: va en el zócalo **J7/J8** de la
EinarBoard (mismo footprint XBee). El enlace F9P ↔ módem pasa por el relevador **K1**; con K1
liberado (estado por defecto del firmware, `BYP_CTRL` bajo) el camino es directo. Por eso la
prueba de integración se hace con el módulo montado en el simpleRTK2B (misma electrónica, sin
depender del firmware de la EinarBoard).
- Configuración F9P: UART1 a **115200** (es lo que lee la EinarBoard en PA3/PA2) y UART2 a la
velocidad del módem (`AS+B=115200`). El archivo `.txt` del F9P **no está en el repo** — hay que
añadirlo a `04-Source-Code/rtk-config/` (sin secretos) para que este procedimiento sea reproducible.
- El ID de estación `AS+K` es la identidad de la unidad en el servidor secundario (`AS+r/AS+h`).
Decisión pendiente: `AS+K` = número del módem 4G (R0006) o número de unidad. Mientras se decide,
el acta lo registra como texto libre.
- Las credenciales (APN, NTRIP) viven solo en `…local.txt` en la laptop de banco. Nunca en git ni en
el sitio.

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.1.1.bin`** — descarga: **https://drogheda.helgen.online/descargas/** (repo: `04-Source-Code/firmware/hbox_v5_bringup/release/`, SHA-256 inicia `91ca6f87`) |
| 1 | Archivo de firmware: **`hbox-v5-bringup_0.2.0.bin`** — descarga: **https://drogheda.helgen.online/descargas/** (repo: `04-Source-Code/firmware/hbox_v5_bringup/release/`, SHA-256 inicia `8491625d`) |
| 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.1.1.bin`.
4. **File path** → seleccionar `hbox-v5-bringup_0.2.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.1.1.bin
dfu-util -a 0 -s 0x08000000:leave -D hbox-v5-bringup_0.2.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.1.1.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.1.1`, luego líneas `[PASS]` / `[FAIL]`, y al final **`>>> BOARD OK`** |
| Terminal serie a **115200 baud** | Encabezado `hbox-v5-bringup 0.2.0`, 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

@ -68,9 +68,14 @@ td{padding:7px 8px 7px 0;border-bottom:1px solid var(--linea)}
</div>
<script>
const STREAMS = [
{ tipo:'prueba-v5', prefijo:'V5', n:15, titulo:'Prueba de tarjeta — V5', form:'formularios/v5.html' },
{ tipo:'prueba-v4', prefijo:'V4', n:15, titulo:'Prueba de tarjeta — V4', form:'formularios/v4.html' },
{ tipo:'ensamble', prefijo:'HBOX', n:15, titulo:'Ensamble de equipo — HBOX', form:'formularios/ensamble.html', assembly:true },
{ tipo:'prueba-v5', prefijo:'V5', desde:1, n:30, pad:2, titulo:'Tarjetas EinarBoard — V5', form:'formularios/v5.html' },
{ tipo:'modulo-ard', prefijo:'ARD', desde:1, n:30, pad:3, titulo:'Módulos simpleRTK2B Pro — ARD', form:'formularios/ardusimple.html', usoClave:'ard' },
{ tipo:'modulo-4g', prefijo:'RTK', desde:1, n:30, pad:4, titulo:'Módems 4G NTRIP Master — RTK', form:'formularios/modem4g.html', usoClave:'modem' },
{ tipo:'prueba-v4', prefijo:'V4', desde:1, n:15, pad:2, titulo:'Tarjetas EinarBoard — V4 (borrador)', form:'formularios/v4.html' },
];
const TABLAS = [
{ tipo:'integracion', titulo:'Integración ArduSimple + 4G (parejas)', form:'formularios/integracion.html', cols:[['ard','ArduSimple'],['modem','4G (RTK-)']] },
{ tipo:'ensamble', titulo:'Unidades ensambladas — HBOX v5', form:'formularios/ensamble.html', cols:[['einar','EinarBoard'],['ard','ArduSimple'],['modem','4G (RTK-)']], unidad:true },
];
const ICON = { apro:'✓', falla:'✕', pend:'·', ens:'▣' };
const cont = document.getElementById('cont');
@ -96,13 +101,15 @@ function bloque(sc, actas){
actas.filter(a=>a.tipo===sc.tipo).forEach(a=>{ map[a.serie]=a; });
let apro=0, falla=0, pend=0, ens=0;
const cells=[];
for(let i=1;i<=sc.n;i++){
const serie = sc.prefijo+'-'+String(i).padStart(2,'0');
const desde = sc.desde||1, pad = sc.pad||2;
for(let i=desde;i<desde+sc.n;i++){
const serie = sc.prefijo+'-'+String(i).padStart(pad,'0');
const a = map[serie];
let estado='pend', tecnico='';
if(a){ tecnico=a.tecnico||'';
if(sc.assembly){ estado = a.resultado==='FALLA' ? 'falla' : 'ens'; }
else { estado = a.resultado==='FALLA' ? 'falla' : 'apro'; }
estado = a.resultado==='FALLA' ? 'falla' : 'apro';
// módulo aprobado y ya montado en una unidad → 'ens' (ensamblado)
if(estado==='apro' && sc.usoClave && enUnidad[sc.usoClave] && enUnidad[sc.usoClave][serie]){ estado='ens'; tecnico='en '+enUnidad[sc.usoClave][serie]; }
}
if(estado==='apro')apro++; else if(estado==='falla')falla++; else if(estado==='ens')ens++; else pend++;
cells.push(celda({serie,estado,tecnico,recibido:a?a.recibido:''}, sc));
@ -110,12 +117,11 @@ function bloque(sc, actas){
const hechas = sc.n - pend;
const sec = document.createElement('section'); sec.className='bloque';
sec.innerHTML = '<h2>'+sc.titulo+'</h2>'
+ '<p class="resumen">'+hechas+' de '+sc.n+' registradas'
+ (sc.assembly ? ' · '+ens+' ensambladas · '+falla+' con falla'
: ' · '+apro+' aprobadas · '+falla+' con falla') + ' · '+pend+' pendientes</p>';
+ '<p class="resumen">'+hechas+' de '+sc.n+' registradas · '+apro+' aprobadas'
+ (ens ? ' · '+ens+' ya en unidad' : '') + ' · '+falla+' con falla · '+pend+' pendientes</p>';
const barra = document.createElement('div'); barra.className='barra';
const seg=(c,n)=>{ if(!n)return; const s=document.createElement('span'); s.className=c; s.style.flex=n; barra.append(s); };
if(sc.assembly){ seg('e',ens); seg('f',falla); } else { seg('a',apro); seg('f',falla); }
seg('a',apro); seg('e',ens); seg('f',falla);
const rest=document.createElement('span'); rest.style.flex=pend; barra.append(rest);
sec.append(barra);
const grid=document.createElement('div'); grid.className='grid'; cells.forEach(c=>grid.append(c));
@ -123,27 +129,55 @@ function bloque(sc, actas){
return sec;
}
let enUnidad = {}; // clave -> { serieModulo: serieUnidad }
function tabla(tc, actas){
const rows = actas.filter(a=>a.tipo===tc.tipo);
const sec=document.createElement('section'); sec.className='bloque';
const ok = rows.filter(a=>a.resultado!=='FALLA').length, bad = rows.length-ok;
sec.innerHTML='<h2>'+tc.titulo+'</h2><p class="resumen">'+rows.length+' registradas · '+ok+' aprobadas · '+bad+' con falla · <a href="'+tc.form+'">+ nueva acta</a></p>';
const t=document.createElement('table');
t.innerHTML='<tr><th>'+(tc.unidad?'Unidad':'Pareja')+'</th>'+tc.cols.map(c=>'<th>'+c[1]+'</th>').join('')+'<th>Técnico</th><th>Resultado</th><th>Fecha</th></tr>';
rows.forEach(a=>{
const tr=document.createElement('tr');
const p = a.resultado==='FALLA'?'<span class="pill falla">FALLA</span>':'<span class="pill apro">OK</span>';
const link='formularios/ver.html?tipo='+encodeURIComponent(tc.tipo)+'&serie='+encodeURIComponent(a.serie||'');
tr.innerHTML='<td><a href="'+link+'"><b>'+(a.serie||'')+'</b></a></td>'
+ tc.cols.map(c=>'<td>'+((a.componentes||{})[c[0]]||'—')+'</td>').join('')
+ '<td>'+(a.tecnico||'')+'</td><td>'+p+'</td><td>'+fmt(a.recibido)+'</td>';
t.append(tr);
});
sec.append(t); if(!rows.length) sec.innerHTML+='<p class="resumen">Aún no hay registros.</p>';
return sec;
}
function render(actas){
cont.innerHTML='';
// módulos ya montados en unidades (actas de ensamble reales)
enUnidad = {};
actas.filter(a=>a.tipo==='ensamble' && a.componentes && a.resultado!=='FALLA').forEach(a=>{
for(const k in a.componentes){ enUnidad[k]=enUnidad[k]||{}; enUnidad[k][a.componentes[k]]=a.serie; }
});
// KPIs
const pruebas = actas.filter(a=>a.tipo==='prueba-v5'||a.tipo==='prueba-v4');
const v5 = actas.filter(a=>a.tipo==='prueba-v5');
const rtk = actas.filter(a=>a.tipo==='modulo-ard'), g4 = actas.filter(a=>a.tipo==='modulo-4g');
const integ = actas.filter(a=>a.tipo==='integracion');
const ens = actas.filter(a=>a.tipo==='ensamble');
const aprob = pruebas.filter(a=>a.resultado!=='FALLA').length;
const fallas = pruebas.filter(a=>a.resultado==='FALLA').length;
const okN = l=>l.filter(a=>a.resultado!=='FALLA').length;
const kpis=document.createElement('div'); kpis.className='kpis';
const kpi=(n,l)=>{ const d=document.createElement('div'); d.className='kpi'; d.innerHTML='<div class="n">'+n+'</div><div class="l">'+l+'</div>'; return d; };
kpis.append(kpi(pruebas.length+' / 30','Tarjetas probadas'));
kpis.append(kpi(aprob,'Aprobadas'));
kpis.append(kpi(fallas,'Con falla'));
kpis.append(kpi(ens.length+' / 15','Equipos ensamblados'));
kpis.append(kpi(okN(v5),'EinarBoard V5 aprobadas'));
kpis.append(kpi(okN(rtk),'ArduSimple aprobados'));
kpis.append(kpi(okN(g4),'Módems 4G aprobados'));
kpis.append(kpi(okN(integ),'Parejas integradas'));
kpis.append(kpi(okN(ens),'Unidades HBOX listas'));
cont.append(kpis);
// streams
// unidades e integración primero, luego inventario de módulos
TABLAS.slice().reverse().forEach(tc=>cont.append(tabla(tc,actas)));
STREAMS.forEach(sc=>cont.append(bloque(sc,actas)));
// leyenda
const ley=document.createElement('div'); ley.className='leyenda';
ley.innerHTML='<span><b style="color:var(--apro)">✓</b> Aprobada</span>'
+'<span><b style="color:var(--falla)">✕</b> Falla</span>'
+'<span><b style="color:var(--ens)">▣</b> Ensamblada</span>'
+'<span><b style="color:var(--ens)">▣</b> Ya montado en una unidad</span>'
+'<span><b>·</b> Pendiente</span>'
+'<span>· Toca una tarjeta para ver o llenar su acta.</span>';
cont.append(ley);

View File

@ -43,11 +43,16 @@ nav:
- "Tablero de estado": tablero.html
- Actas y control de calidad:
- "Índice de actas": actas/index.md
- "Acta de prueba V5": formularios/v5.html
- "Acta de prueba V5 (tarjeta)": formularios/v5.html
- "Acta de módulo ArduSimple (simpleRTK2B, ARD-NNN)": formularios/ardusimple.html
- "Acta de módem 4G (NTRIP Master, RTK-NNNN)": formularios/modem4g.html
- "Acta de integración ArduSimple + 4G": formularios/integracion.html
- "Acta de ensamble (unidad HBOX v5)": formularios/ensamble.html
- "Acta de prueba V4 (borrador)": formularios/v4.html
- "Checklist de ensamble (borrador)": formularios/ensamble.html
- Procedimientos de banco:
- "EinarBoard v5 — Prueba y flasheo (lote 15 pzas)": procedimientos/v5-prueba-y-flasheo.md
- "HBOX v5 — Módulos RTK/4G e integración (antes del ensamble)": procedimientos/v5-modulos-integracion.md
- "HBOX v5 — Ensamble de la unidad y energizado final": procedimientos/v5-ensamble.md
- "EinarBoard 1.0 — Bring-up runbook": procedimientos/einarboard-1.0-bringup.md
- "FW v3.0.3 — Prueba de banco (política de ventilador)": procedimientos/fan-policy-bench-test.md
- "Histórico — Proto bench v3": procedimientos/historico-proto-bench-v3.md

View File

@ -8,6 +8,8 @@ SRC="${DROGHEDA_ROOT:-..}" # hbox-docs vive como submódulo directo de Droghed
declare -a MAP=(
"06-Testing-Validation/2026-09-15_Drogheda_EinarBoard-v5_Procedimiento-Prueba-y-Flasheo_ES_v1.0.md|docs/procedimientos/v5-prueba-y-flasheo.md"
"06-Testing-Validation/2026-09-17_Drogheda_HBOX-v5_Modulos-e-Integracion_ES_v1.0.md|docs/procedimientos/v5-modulos-integracion.md"
"06-Testing-Validation/2026-09-17_Drogheda_HBOX-v5_Procedimiento-Ensamble_ES_v1.0.md|docs/procedimientos/v5-ensamble.md"
"06-Testing-Validation/2026-07-08_Drogheda_EinarBoard-1.0_BringUp-Runbook_v1.0.md|docs/procedimientos/einarboard-1.0-bringup.md"
"06-Testing-Validation/2026-08-18_Drogheda_FW-v3.0.3_Fan-Policy_Bench-Test_v1.0.md|docs/procedimientos/fan-policy-bench-test.md"
"03-Architecture-Hardware/watchdog-proto-bench-bringup-v3.md|docs/procedimientos/historico-proto-bench-v3.md"
@ -19,7 +21,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.1.1.bin|docs/descargas/hbox-v5-bringup_0.1.1.bin"
"04-Source-Code/firmware/hbox_v5_bringup/release/hbox-v5-bringup_0.2.0.bin|docs/descargas/hbox-v5-bringup_0.2.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"
)