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?} GET /api/acta?tipo=&serie= -> {exists, acta?}
POST /api/actas -> crea (409 si ya existe una real) POST /api/actas -> crea (409 si ya existe una real)
DELETE /api/acta?tipo=&serie= -> borra (solo admin) 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 import os, re, json, csv, datetime
from flask import Flask, request, jsonify, abort from flask import Flask, request, jsonify, abort
@ -25,6 +28,7 @@ from flask import Flask, request, jsonify, abort
app = Flask(__name__) app = Flask(__name__)
BASE = os.environ.get("ACTAS_DIR", "/srv/projects/drogheda/actas") 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()} 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_\-]") SAFE = re.compile(r"[^A-Za-z0-9_\-]")
def slug(s, n=40): return SAFE.sub("", str(s or "").strip())[:n] def slug(s, n=40): return SAFE.sub("", str(s or "").strip())[:n]
@ -35,6 +39,27 @@ def current_user():
def is_admin(): def is_admin():
return current_user().lower() in ADMINS 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): def find_latest(tipo, serie):
"""Ruta del acta real más reciente para (tipo, serie), o None.""" """Ruta del acta real más reciente para (tipo, serie), o None."""
carpeta = os.path.join(BASE, tipo) carpeta = os.path.join(BASE, tipo)
@ -56,7 +81,7 @@ def whoami():
@app.get("/api/acta") @app.get("/api/acta")
def get_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: if not tipo or not serie:
return jsonify(ok=False, error="faltan tipo/serie"), 400 return jsonify(ok=False, error="faltan tipo/serie"), 400
ruta = find_latest(tipo, serie) ruta = find_latest(tipo, serie)
@ -76,7 +101,7 @@ def crear():
if not isinstance(d, dict): if not isinstance(d, dict):
return jsonify(ok=False, error="Se esperaba un objeto"), 400 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() tecnico = str(d.get("tecnico", "")).strip()
if not tipo or not serie: if not tipo or not serie:
return jsonify(ok=False, error="Faltan 'tipo' o 'serie'"), 400 return jsonify(ok=False, error="Faltan 'tipo' o 'serie'"), 400
@ -92,6 +117,17 @@ def crear():
return jsonify(ok=False, existe=True, return jsonify(ok=False, existe=True,
error=f"Ya existe un acta para {serie}. Pide a Leo que la borre para rehacerla."), 409 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) ts = datetime.datetime.now().replace(microsecond=0)
iso = ts.isoformat() iso = ts.isoformat()
d["_recibido"] = iso d["_recibido"] = iso
@ -121,7 +157,7 @@ def crear():
def borrar_acta(): def borrar_acta():
if not is_admin(): if not is_admin():
return jsonify(ok=False, error="Solo un administrador puede borrar actas."), 403 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: if not tipo or not serie:
return jsonify(ok=False, error="faltan tipo/serie"), 400 return jsonify(ok=False, error="faltan tipo/serie"), 400
carpeta = os.path.join(BASE, tipo) carpeta = os.path.join(BASE, tipo)
@ -180,9 +216,12 @@ def listar():
d = json.load(f) d = json.load(f)
except Exception: except Exception:
continue continue
filas.append({"tipo": tipo, "archivo": f"{tipo}/{fn}", fila = {"tipo": tipo, "archivo": f"{tipo}/{fn}",
"serie": d.get("serie"), "tecnico": d.get("tecnico"), "serie": d.get("serie"), "tecnico": d.get("tecnico"),
"resultado": d.get("resultado"), "recibido": d.get("_recibido")}) "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) filas.sort(key=lambda r: (r.get("recibido") or ""), reverse=True)
return jsonify(ok=True, total=len(filas), actas=filas) return jsonify(ok=True, total=len(filas), actas=filas)

View File

@ -1,34 +1,56 @@
# Actas y control de calidad # Actas y control de calidad
Formularios que el técnico llena **en pantalla mientras prueba o ensambla** cada Formularios que el técnico llena **en pantalla mientras prueba o ensambla**. Al terminar se
tarjeta. Al terminar se firman (nombre + conformidad) y se envían al servidor, donde firman (nombre + conformidad) y se envían al servidor, donde quedan registrados por número de
quedan registrados por número de serie. Así queda constancia de que se siguió el serie. Un acta real se envía **una sola vez** por serie; para rehacerla, Leo la borra.
procedimiento en cada unidad.
## 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 ## Formularios
**[▶ Acta de prueba — EinarBoard V5](../formularios/v5.html)** **[▶ Acta de prueba — EinarBoard V5](../formularios/v5.html)**
Lote de 15 tarjetas (V5-01 … V5-15). Procedimiento verificado §1–§6: inspección, Lote de 15 tarjetas (V5-01 … V5-15; 30 seleccionables). Procedimiento: [Prueba y flasheo v5](../procedimientos/v5-prueba-y-flasheo.md).
continuidad, rieles 3V3 / 5 V / 24 V, flasheo DFU y POST.
**[▶ Acta de prueba — EinarBoard V4](../formularios/v4.html)** — *borrador* **[▶ Acta de módulo — simpleRTK2B Pro / ArduSimple (ARD-001 … ARD-030)](../formularios/ardusimple.html)**
V4-01 … V4-15. Adaptado de V5; la sección de MCU/firmware está marcada como Un acta por receptor: visual, fix GNSS, configuración Helgen. Procedimiento: [Módulos e integración §2](../procedimientos/v5-modulos-integracion.md).
**pendiente de definir** para V4. Revisar antes de uso oficial.
**[▶ Checklist de ensamble — HBOX](../formularios/ensamble.html)** — *borrador* **[▶ Acta de módem — 4G NTRIP Master (RTK-0001 … RTK-0030)](../formularios/modem4g.html)**
Control de calidad durante el armado: recepción de PCBA, módulos, mecánica, arnés, Un acta por módem: IMEI/ICCID, configuración, registro en red. Procedimiento: [Módulos e integración §3](../procedimientos/v5-modulos-integracion.md).
etiquetado y energizado final. Ver la [guía de ensamble](../montaje/guia-ensamble-qc.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 ## Cómo se usan
1. Abre el formulario de la versión que estás probando y **elige el número de serie**. 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 2. Ve llenando cada sección conforme avanzas. El formulario **se guarda solo** en el navegador: si se
navegador: si se cierra, al reabrir con el mismo número de serie recuperas lo escrito. cierra, al reabrir con la misma serie recuperas lo escrito.
3. Al final marca el **resultado** (Aprobada / Falla), escribe tu **nombre** y confirma 3. Al final marca el **resultado** (Aprobada / Falla), escribe tu **nombre** y confirma que seguiste
que seguiste el procedimiento. el procedimiento.
4. Pulsa **Enviar acta firmada**. Queda registrada en el servidor. 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" !!! note "Para ingeniería"
Los envíos se guardan en el servidor por tipo y número de serie. Índice rápido en Los envíos se guardan en el servidor por tipo y número de serie (`…/actas/<tipo>/`). Las actas de
`…/actas/<tipo>/_indice.csv`, o el listado completo en integración y ensamble llevan `componentes` con los módulos elegidos. Listado completo en
[`/api/actas`](/api/actas){:target="_blank"}. [`/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 # 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-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 / 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 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. `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) ## Flash (DFU, J11)
Hold **SW2** (BOOT0) → tap **SW1** (RESET) → release SW2 → device `0483:DF11` appears. Hold **SW2** (BOOT0) → tap **SW1** (RESET) → release SW2 → device `0483:DF11` appears.
```bash ```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 ## Release
| File | Version | SHA-256 (first 16) | Size | | 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 | | `release/hbox-v5-bringup_0.1.0.bin` | 0.1.0 — SUPERSEDED (no float printf, ETH_CS driven) | `1a9a77ae479c8f86` | 41 040 B |
## Changelog ## 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 - **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 `INPUT_PULLUP` instead of driving HIGH into the unpowered W5500; reset cause reports `PIN` only when
`PINRSTF` is set (else `UNKNOWN`). `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 | | 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): Flasheo por DFU en **J11** (mantener **SW2**, pulsar y soltar **SW1**, soltar SW2):
```bash ```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`. 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" !!! warning "Solo para la tarjeta v5"
Este firmware **no** es el de producción: solo prueba la tarjeta (reloj, USB, cristales, I²C, 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` 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); button.limpiar{background:#fff;color:var(--tinta2);border:1px solid var(--linea);
border-radius:10px;padding:13px 16px;font-size:15px;cursor:pointer} border-radius:10px;padding:13px 16px;font-size:15px;cursor:pointer}
button.limpiar:hover{border-color:var(--naranja);color:var(--naranja)} 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. - 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)". - Solo el administrador (leo) ve el botón "Borrar acta (reabrir)".
- La serie PRUEBA no se bloquea (para practicar). - 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 () { (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 $ = (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) { function render(cfg) {
const root = document.getElementById('acta'); 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; let admin = false;
const wrap = $('div', { class: 'wrap' }); const wrap = $('div', { class: 'wrap' });
@ -24,25 +43,57 @@
if (cfg.borrador) 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.' })); 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 datos = $('div', { class: 'datos' });
const serieSel = $('select', { id: 'f_serie' }); const serieSel = $('select', { id: 'f_serie' }); // modo simple
serieSel.append($('option', { value: '', text: '— elegir —' })); const serieCalc = $('input', { type: 'text', id: 'f_serie_calc', readonly: 'readonly', placeholder: 'se calcula al elegir los módulos' }); // modo compuesto
serieSel.append($('option', { value: cfg.prefijo + '-PRUEBA', text: '⚙ PRUEBA (no cuenta)' })); const compSel = {}; // clave -> <select>
for (let i = 1; i <= (cfg.series || 15); i++) { const compBadge = {}; // clave -> <span> estado
const s = cfg.prefijo + '-' + String(i).padStart(2, '0'); const preBadge = []; // prerrequisitos -> <span>
serieSel.append($('option', { value: s, text: s }));
}
const tecnico = $('input', { type: 'text', id: 'f_tecnico', placeholder: 'Nombre y apellido' }); 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); if (!compuesta) {
datos.append(dS, dT); 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); const cab = $('section', { class: 'paso' }); cab.append(datos);
wrap.append(cab); wrap.append(cab);
const banner = $('div', { class: 'msg', id: 'f_banner' }); const banner = $('div', { class: 'msg', id: 'f_banner' });
wrap.append(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) => { cfg.secciones.forEach((sec, si) => {
const s = $('section', { class: 'paso' }); const s = $('section', { class: 'paso' });
s.append($('h2', { text: sec.titulo })); s.append($('h2', { text: sec.titulo }));
@ -60,11 +111,20 @@
box.classList.add('medida'); box.classList.add('medida');
box.append($('span', { class: 'txt', text: it.txt })); box.append($('span', { class: 'txt', text: it.txt }));
if (it.esperado) box.append($('span', { class: 'esp', html: 'Esperado: ' + it.esperado })); 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 })); if (it.u) box.append($('span', { class: 'u', text: it.u }));
} else if (it.tipo === 'texto') { } else if (it.tipo === 'texto') {
box.append($('label', { text: it.txt })); 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') { } else if (it.tipo === 'nota') {
box.append($('div', { class: 'nota-item', html: it.txt })); box.append($('div', { class: 'nota-item', html: it.txt }));
} }
@ -73,11 +133,12 @@
wrap.append(s); wrap.append(s);
}); });
// ---------------- firma ----------------
const firma = $('section', { class: 'paso firma' }); const firma = $('section', { class: 'paso firma' });
firma.append($('h2', { text: 'Cierre y firma' })); firma.append($('h2', { text: 'Cierre y firma' }));
firma.append($('p', { class: 'sub', text: 'Al enviar, quedas registrado como responsable de esta prueba.' })); firma.append($('p', { class: 'sub', text: 'Al enviar, quedas registrado como responsable de esta prueba.' }));
const res = $('div', { class: 'resultado' }); 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')); const rNO = $('label', { class: 'falla' }); rNO.append($('input', { type: 'radio', name: 'resultado', value: 'FALLA' }), document.createTextNode('FALLA'));
res.append(rOK, rNO); res.append(rOK, rNO);
firma.append($('label', { text: 'Resultado final' }), res); firma.append($('label', { text: 'Resultado final' }), res);
@ -100,7 +161,7 @@
barra.append(estado, grp); barra.append(estado, grp);
document.body.append(barra); document.body.append(barra);
// ---- helpers de estado ---- // ---------------- helpers de estado ----------------
const conforme = () => document.getElementById('f_conforme'); const conforme = () => document.getElementById('f_conforme');
function fields() { return wrap.querySelectorAll('[data-id]'); } function fields() { return wrap.querySelectorAll('[data-id]'); }
function setEnabled(on) { function setEnabled(on) {
@ -109,7 +170,7 @@
wrap.querySelectorAll('input[name=resultado]').forEach(el => el.disabled = !on); wrap.querySelectorAll('input[name=resultado]').forEach(el => el.disabled = !on);
} }
function blankFields() { 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; const rr = wrap.querySelector('input[name=resultado]:checked'); if (rr) rr.checked = false;
conforme().checked = false; tecnico.value = ''; conforme().checked = false; tecnico.value = '';
} }
@ -118,8 +179,13 @@
conforme().checked = !!a.conforme; conforme().checked = !!a.conforme;
if (a.resultado) { const el = wrap.querySelector(`input[name=resultado][value="${a.resultado}"]`); if (el) el.checked = true; } if (a.resultado) { const el = wrap.querySelector(`input[name=resultado][value="${a.resultado}"]`); if (el) el.checked = true; }
for (const k in (a.respuestas || {})) { for (const k in (a.respuestas || {})) {
const el = wrap.querySelector(`[data-id="${k}"]`); if (!el) continue; const v = a.respuestas[k].val;
if (el.type === 'checkbox') el.checked = !!a.respuestas[k].val; else el.value = 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() { function showEditable() {
@ -140,17 +206,77 @@
estado.textContent = 'Bloqueada'; 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; let t, locked = false;
function snapshot() { function snapshot() {
const r = {}; 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'); 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 }; conforme: conforme().checked, resultado: rr ? rr.value : '', respuestas: r };
if (compuesta) { d.componentes = seleccion(); d.verificacion = Object.assign({}, verif); }
return d;
} }
function guardarLocal() { function guardarLocal() {
if (locked || !serieSel.value) return; if (locked || !serieActual()) return;
clearTimeout(t); clearTimeout(t);
t = setTimeout(() => { t = setTimeout(() => {
try { localStorage.setItem(key(), JSON.stringify(snapshot())); } catch (e) {} try { localStorage.setItem(key(), JSON.stringify(snapshot())); } catch (e) {}
@ -163,23 +289,31 @@
else estado.textContent = 'Nueva acta'; else estado.textContent = 'Nueva acta';
} }
// ---- cargar una serie: consulta el servidor y bloquea si ya existe ---- // ---------------- cargar una serie ----------------
async function loadSerie() { async function loadSerie() {
msg.className = 'msg'; locked = false; msg.className = 'msg'; locked = false;
const s = serieSel.value; const s = serieActual();
if (compuesta) serieCalc.value = s || '';
blankFields(); showEditable(); blankFields(); showEditable();
if (!s) { estado.textContent = 'Elige el número de serie'; return; } if (compuesta) { if (usados === null) await cargarUsados(); await verificar(); }
if (s.toUpperCase().indexOf('PRUEBA') >= 0) { cargarLocal(); estado.textContent = 'Modo prueba (no cuenta)'; return; } 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…'; estado.textContent = 'Consultando…';
let j = null; let j = null;
try { const r = await fetch(`/api/acta?tipo=${encodeURIComponent(cfg.tipo)}&serie=${encodeURIComponent(s)}`); j = await r.json(); } catch (e) {} 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); } 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); wrap.addEventListener('input', guardarLocal);
serieSel.addEventListener('change', loadSerie); serieSel.addEventListener('change', loadSerie);
Object.values(compSel).forEach(sel => sel.addEventListener('change', loadSerie));
// ---- limpiar (solo borrador local; no aplica a enviadas) ----
limpiar.addEventListener('click', () => { limpiar.addEventListener('click', () => {
if (locked) return; if (locked) return;
if (!confirm('¿Limpiar este formulario y empezar de nuevo?')) 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); blankFields(); msg.className = 'msg'; estado.textContent = 'Formulario limpio'; window.scrollTo(0, 0);
}); });
// ---- borrar / reabrir (solo admin) ----
borrar.addEventListener('click', async () => { borrar.addEventListener('click', async () => {
if (!admin) return; 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; borrar.disabled = true;
try { 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(); const j = await r.json();
if (!r.ok || !j.ok) throw new Error(j.error || ('HTTP ' + r.status)); 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.'; msg.className = 'msg ok'; msg.textContent = 'Acta borrada. Ya puede rehacerse.';
estado.textContent = 'Reabierta'; estado.textContent = 'Reabierta';
} catch (e) { msg.className = 'msg err'; msg.textContent = 'No se pudo borrar: ' + e.message; } } catch (e) { msg.className = 'msg err'; msg.textContent = 'No se pudo borrar: ' + e.message; }
borrar.disabled = false; borrar.disabled = false;
}); });
// ---- envío ---- // ---------------- envío ----------------
btn.addEventListener('click', async () => { btn.addEventListener('click', async () => {
if (locked) return; if (locked) return;
const d = snapshot(); 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.tecnico.trim()) return fail('Escribe tu nombre.');
if (!d.resultado) return fail('Marca el resultado final (Aprobada o Falla).'); if (!d.resultado) return fail('Marca el resultado final (Aprobada o Falla).');
if (!d.conforme) return fail('Debes confirmar que seguiste el procedimiento.'); if (!d.conforme) return fail('Debes confirmar que seguiste el procedimiento.');
@ -215,9 +350,9 @@
try { try {
const r = await fetch('/api/actas', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(d) }); const r = await fetch('/api/actas', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(d) });
const j = await r.json(); const j = await r.json();
if (r.status === 409) { // ya existía: recargar bloqueada if (r.status === 409) {
try { localStorage.removeItem(key()); } catch (e) {} 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.'; msg.className = 'msg err'; msg.textContent = j.error || 'Ya existe un acta para esta serie.';
btn.disabled = false; btn.textContent = 'Enviar acta firmada'; btn.disabled = false; btn.textContent = 'Enviar acta firmada';
return; return;
@ -225,7 +360,8 @@
if (!r.ok || !j.ok) throw new Error(j.error || ('HTTP ' + r.status)); if (!r.ok || !j.ok) throw new Error(j.error || ('HTTP ' + r.status));
try { localStorage.removeItem(key()); } catch (e) {} try { localStorage.removeItem(key()); } catch (e) {}
msg.className = 'msg ok'; 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() }); } 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'; } else { btn.disabled = false; btn.textContent = 'Enviar acta firmada'; estado.textContent = 'Prueba enviada'; }
window.scrollTo(0, document.body.scrollHeight); 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); } 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 () => { (async () => {
try { const r = await fetch('/api/whoami'); const j = await r.json(); admin = !!j.admin; } catch (e) {} try { const r = await fetch('/api/whoami'); const j = await r.json(); admin = !!j.admin; } catch (e) {}
loadSerie(); 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> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <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"> <link rel="stylesheet" href="acta.css">
</head> </head>
<body> <body>
<div id="acta"></div> <div id="acta"></div>
<script src="acta.js"></script> <script src="acta.js"></script>
<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({ Acta.render({
tipo: 'ensamble', tipo: 'ensamble',
prefijo: 'HBOX', titulo: 'Acta de ensamble — unidad HBOX v5',
series: 15, subtitulo: 'Una unidad a la vez. Procedimiento: "Ensamble de la unidad y energizado final".',
borrador: true, 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.',
titulo: 'Control de calidad de ensamble — HBOX', componentes: [
subtitulo: 'Llena mientras armas la unidad. Un acta por equipo (número de serie).', { 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: [ secciones: [
{ titulo: '1 · Recepción de la PCBA', { titulo: '2 · Preparación de la tarjeta (sin energía)',
sub: 'Revisión de la tarjeta ensamblada que llega de JLCPCB, antes de montar nada.',
items: [ items: [
{ tipo:'check', txt:'La tarjeta corresponde al modelo/revisión correcto (serigrafía).' }, { tipo:'check', txt:'La EinarBoard física es la seleccionada arriba (etiqueta cerca de MH4).' },
{ tipo:'check', txt:'Sin componentes faltantes, torcidos, ni con soldadura fría (inspección visual con lupa).' }, { tipo:'radio', txt:'JP2 (F2)', opciones:['1 = Li-ion','0 = LiFePO4'] },
{ tipo:'check', txt:'Sin puentes de soldadura, sobre todo en los ICs de paso fino.' }, { tipo:'radio', txt:'JP3 (F1)', opciones:['0','1'] },
{ tipo:'check', txt:'Conectores y zócalos rectos, sin pines doblados.' }, { tipo:'radio', txt:'JP4 (F0)', opciones:['1','0'] },
{ tipo:'texto', txt:'Defectos encontrados en la PCBA', ph:'ninguno / describir' }, { 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)', { titulo: '3 · Módulos enchufables (sin energía)',
sub: 'Partes que se sueldan a mano después de la PCBA (si aplica en este lote).',
items: [ items: [
{ tipo:'check', txt:'Terminal de potencia <b>P1</b> (Phoenix/GX16) firme y con soldadura completa.' }, { tipo:'check', txt:'simpleRTK2B Pro en <b>MOD1</b>: pin "5V" alineado, cuatro tiras a fondo, ninguna clavija fuera.' },
{ tipo:'check', txt:'Conectores JST (<b>P2, P4, P6, P8, P10, P11, P12</b>…) soldados y sin frío.' }, { 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:'Relevador <b>K1</b>, botón <b>SW1</b> y electrolíticos radiales bien asentados y en polaridad correcta.' }, { tipo:'check', txt:'SIM presente. Nada roza la tapa ni componentes altos (K1, C10, L6).' },
{ tipo:'check', txt:'Zócalos de módulos (J1/J2, J7/J8, MOD1) soldados a ras y rectos.' },
] }, ] },
{ titulo: '3 · Módulos enchufables', { titulo: '4 · Panel y arnés interno (sin energía)',
sub: 'Se instalan sobre la tarjeta ya probada. Verifica orientación antes de energizar.',
items: [ items: [
{ tipo:'check', txt:'MCU (STM32) correctamente asentado y en la orientación correcta.' }, { tipo:'check', txt:'<b>P1</b> ← GX16: 1 = +24 V, 2 = GND, 3 = ignición, 4 = GND. Polaridad verificada con multímetro.' },
{ tipo:'check', txt:'simpleRTK2B Pro insertado en <b>MOD1</b> sin forzar pines.' }, { tipo:'medida', txt:'Salida de la SD-15B-05 ajustada en banco', esperado:'5.00 V', u:'V' },
{ tipo:'check', txt:'Módulo 4G insertado en su zócalo (2 mm) en la orientación correcta.' }, { 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:'Ninguna antena/módulo obstruye el cierre del gabinete.' }, { 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: [ items: [
{ tipo:'check', txt:'Tarjeta fijada con tornillos/separadores M2.5/M3 en los barrenos correctos, sin flexión.' }, { tipo:'medida', txt:'Corriente con la etapa RTK apagada', esperado:'< 150 mA', u:'mA' },
{ tipo:'check', txt:'Ventilador montado y con flujo en la dirección correcta.' }, { tipo:'check', txt:'D18 verde encendido · D17 azul y LED de panel parpadean · ventilador gira.' },
{ tipo:'check', txt:'Conectores de panel (GX16, RJ45, antenas) alineados con sus recortes del gabinete.' }, { tipo:'check', txt:'Consola: encabezado <code>hbox-v5-bringup</code> con <b>12 PASS</b>.' },
{ tipo:'check', txt:'Sin cables pellizcados; el gabinete cierra sin forzar.' }, { 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', { titulo: '6 · Batería y respaldo',
sub: 'Verifica cada conector contra el pinout de arneses.', sub: 'Etapa RTK apagada. Pack en P4 (1 = BAT+, 2 = GND, 3 = NTC).',
items: [ items: [
{ tipo:'check', txt:'<b>P1</b> alimentación de máquina: pin 1 = 24V, pin 2 = GND, pin 3 = ignición.' }, { tipo:'medida', txt:'Voltaje del pack antes de conectar (BAT+ a GND)', esperado:'3.0 – 4.2 V', u:'V' },
{ tipo:'check', txt:'<b>P2</b> entrada 5 V (Mean Well) en polaridad correcta.' }, { tipo:'medida', txt:'STAT vbat con el pack conectado', esperado:'≈ voltaje del pack', u:'V' },
{ tipo:'check', txt:'<b>P4</b> batería 1S4P + NTC conectado (pin 1 = BAT+, 2 = GND, 3 = NTC).' }, { 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:'<b>P6</b> ventilador y demás JST insertados a fondo (clic), cada uno en su lugar.' }, { tipo:'check', txt:'Al regresar 24 V, STAT v5 ≈ 5.0 sin reinicio.' },
{ tipo:'check', txt:'Sin conectores intercambiados; colores/etiquetas del arnés coinciden.' }, { tipo:'texto', txt:'Observaciones (reinicios, calentamiento, corriente fuera de rango)', ph:'ninguna / describir' },
{ tipo:'texto', txt:'Observaciones del arnés', ph:'ninguno / describir' },
] }, ] },
{ titulo: '6 · Etiquetado e identidad', { titulo: '7 · Cierre y etiquetado',
items: [ items: [
{ tipo:'check', txt:'Número de serie del equipo colocado de forma visible y legible.' }, { 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:'Etiqueta no tapa serigrafía, tornillos ni ventilación.' }, { tipo:'check', txt:'Interruptor de corte en <b>OFF</b> para almacenar. Tapa cerrada con sello, tornillos en cruz, prensaestopas apretados.' },
] },
{ 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' },
] }, ] },
] ]
}); });

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({ Acta.render({
tipo: 'prueba-v5', tipo: 'prueba-v5',
prefijo: 'V5', prefijo: 'V5',
series: 15, series: 30, // 15 en el lote actual; 30 seleccionables
titulo: 'Acta de prueba y flasheo — EinarBoard V5', titulo: 'Acta de prueba y flasheo — EinarBoard V5',
subtitulo: 'Lote 15 tarjetas (JLC 22-Ago). Llena cada sección durante la prueba.', subtitulo: 'Lote 15 tarjetas (JLC 22-Ago). Llena cada sección durante la prueba.',
secciones: [ 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.' }, { 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)', { 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: [ 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:'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".' }, { 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>D17</b> (LED azul) parpadea a 1 Hz.' },
{ tipo:'check', txt:'<b>DB1 → DB4</b> se encienden en secuencia una vez.' }, { 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:'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 ----' }, { tipo:'texto', txt:'Reporte / observaciones (anota cualquier línea [FAIL] completa)', ph:'---- RESULT: 12 PASS / 0 FAIL ----' },
] }, ] },
{ titulo: '6 · Cierre', { titulo: '6 · Cierre',

View File

@ -35,7 +35,8 @@ const qs = new URLSearchParams(location.search);
const tipo = qs.get('tipo') || ''; const tipo = qs.get('tipo') || '';
const serie = qs.get('serie') || ''; const serie = qs.get('serie') || '';
const cont = document.getElementById('cont'); 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 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):''; } 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">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 class="c"><div class="l">Usuario</div><div class="v">'+esc(a._usuario||'—')+'</div></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_) // agrupar respuestas por sección (prefijo sN_)
const R = a.respuestas || {}; const R = a.respuestas || {};
const keys = Object.keys(R).sort((x,y)=>{ 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 | 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 | 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 | 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 | Jumper 2.54 mm para P11 (interruptor de corte) |
| 1 | Lupa o lámpara con aumento | | 1 | Lupa o lámpara con aumento |
| — | Hoja de registro (sección 7) | | — | 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; 2. Arriba a la derecha, en el selector de conexión elegir **USB**. Pulsar el botón de refrescar;
debe aparecer `USB1`. Pulsar **Connect**. debe aparecer `USB1`. Pulsar **Connect**.
3. Ir a la pestaña **Erasing & Programming** (icono de flecha hacia abajo, lado izquierdo). 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**. 5. **Start address** = `0x08000000`. Marcar **Verify programming** y **Run after programming**.
6. Pulsar **Start Programming**. Esperar "File download complete" y "Verify… successful". 6. Pulsar **Start Programming**. Esperar "File download complete" y "Verify… successful".
7. Pulsar **Disconnect**. 7. Pulsar **Disconnect**.
@ -192,7 +192,7 @@ Solo para tarjetas que pasaron las secciones 1 a 4.
```bash ```bash
dfu-util -l # debe listar [0483:df11] 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 ### 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) | | **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 | | **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…`) | | 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 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). 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> </div>
<script> <script>
const STREAMS = [ const STREAMS = [
{ tipo:'prueba-v5', prefijo:'V5', n:15, titulo:'Prueba de tarjeta — V5', form:'formularios/v5.html' }, { tipo:'prueba-v5', prefijo:'V5', desde:1, n:30, pad:2, titulo:'Tarjetas EinarBoard — V5', form:'formularios/v5.html' },
{ tipo:'prueba-v4', prefijo:'V4', n:15, titulo:'Prueba de tarjeta — V4', form:'formularios/v4.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:'ensamble', prefijo:'HBOX', n:15, titulo:'Ensamble de equipo — HBOX', form:'formularios/ensamble.html', assembly:true }, { 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 ICON = { apro:'✓', falla:'✕', pend:'·', ens:'▣' };
const cont = document.getElementById('cont'); 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; }); actas.filter(a=>a.tipo===sc.tipo).forEach(a=>{ map[a.serie]=a; });
let apro=0, falla=0, pend=0, ens=0; let apro=0, falla=0, pend=0, ens=0;
const cells=[]; const cells=[];
for(let i=1;i<=sc.n;i++){ const desde = sc.desde||1, pad = sc.pad||2;
const serie = sc.prefijo+'-'+String(i).padStart(2,'0'); for(let i=desde;i<desde+sc.n;i++){
const serie = sc.prefijo+'-'+String(i).padStart(pad,'0');
const a = map[serie]; const a = map[serie];
let estado='pend', tecnico=''; let estado='pend', tecnico='';
if(a){ tecnico=a.tecnico||''; if(a){ tecnico=a.tecnico||'';
if(sc.assembly){ estado = a.resultado==='FALLA' ? 'falla' : 'ens'; } estado = a.resultado==='FALLA' ? 'falla' : 'apro';
else { 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++; 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)); 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 hechas = sc.n - pend;
const sec = document.createElement('section'); sec.className='bloque'; const sec = document.createElement('section'); sec.className='bloque';
sec.innerHTML = '<h2>'+sc.titulo+'</h2>' sec.innerHTML = '<h2>'+sc.titulo+'</h2>'
+ '<p class="resumen">'+hechas+' de '+sc.n+' registradas' + '<p class="resumen">'+hechas+' de '+sc.n+' registradas · '+apro+' aprobadas'
+ (sc.assembly ? ' · '+ens+' ensambladas · '+falla+' con falla' + (ens ? ' · '+ens+' ya en unidad' : '') + ' · '+falla+' con falla · '+pend+' pendientes</p>';
: ' · '+apro+' aprobadas · '+falla+' con falla') + ' · '+pend+' pendientes</p>';
const barra = document.createElement('div'); barra.className='barra'; 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); }; 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); const rest=document.createElement('span'); rest.style.flex=pend; barra.append(rest);
sec.append(barra); sec.append(barra);
const grid=document.createElement('div'); grid.className='grid'; cells.forEach(c=>grid.append(c)); const grid=document.createElement('div'); grid.className='grid'; cells.forEach(c=>grid.append(c));
@ -123,27 +129,55 @@ function bloque(sc, actas){
return sec; 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){ function render(actas){
cont.innerHTML=''; 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 // 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 ens = actas.filter(a=>a.tipo==='ensamble');
const aprob = pruebas.filter(a=>a.resultado!=='FALLA').length; const okN = l=>l.filter(a=>a.resultado!=='FALLA').length;
const fallas = pruebas.filter(a=>a.resultado==='FALLA').length;
const kpis=document.createElement('div'); kpis.className='kpis'; 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; }; 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(okN(v5),'EinarBoard V5 aprobadas'));
kpis.append(kpi(aprob,'Aprobadas')); kpis.append(kpi(okN(rtk),'ArduSimple aprobados'));
kpis.append(kpi(fallas,'Con falla')); kpis.append(kpi(okN(g4),'Módems 4G aprobados'));
kpis.append(kpi(ens.length+' / 15','Equipos ensamblados')); kpis.append(kpi(okN(integ),'Parejas integradas'));
kpis.append(kpi(okN(ens),'Unidades HBOX listas'));
cont.append(kpis); 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))); STREAMS.forEach(sc=>cont.append(bloque(sc,actas)));
// leyenda // leyenda
const ley=document.createElement('div'); ley.className='leyenda'; const ley=document.createElement('div'); ley.className='leyenda';
ley.innerHTML='<span><b style="color:var(--apro)">✓</b> Aprobada</span>' 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(--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><b>·</b> Pendiente</span>'
+'<span>· Toca una tarjeta para ver o llenar su acta.</span>'; +'<span>· Toca una tarjeta para ver o llenar su acta.</span>';
cont.append(ley); cont.append(ley);

View File

@ -43,11 +43,16 @@ nav:
- "Tablero de estado": tablero.html - "Tablero de estado": tablero.html
- Actas y control de calidad: - Actas y control de calidad:
- "Índice de actas": actas/index.md - "Í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 - "Acta de prueba V4 (borrador)": formularios/v4.html
- "Checklist de ensamble (borrador)": formularios/ensamble.html
- Procedimientos de banco: - Procedimientos de banco:
- "EinarBoard v5 — Prueba y flasheo (lote 15 pzas)": procedimientos/v5-prueba-y-flasheo.md - "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 - "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 - "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 - "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=( 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-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-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" "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" "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_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" "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) # 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/release/SHA256SUMS|docs/descargas/SHA256SUMS.txt"
"04-Source-Code/firmware/hbox_v5_bringup/README.md|docs/descargas/hbox-v5-bringup-README.md" "04-Source-Code/firmware/hbox_v5_bringup/README.md|docs/descargas/hbox-v5-bringup-README.md"
) )