diff --git a/backend/app.py b/backend/app.py index 302ebb8..5da0df1 100644 --- a/backend/app.py +++ b/backend/app.py @@ -17,7 +17,10 @@ Rutas: GET /api/acta?tipo=&serie= -> {exists, acta?} POST /api/actas -> crea (409 si ya existe una real) DELETE /api/acta?tipo=&serie= -> borra (solo admin) - GET /api/actas -> listado (ingeniería) + GET /api/actas -> listado (ingeniería); incluye 'componentes' en actas compuestas + +Actas compuestas (integracion, ensamble): traen d["componentes"] = {clave: serie_modulo}. En los tipos +de TIPOS_UNICOS un módulo no puede aparecer en dos actas reales (409). """ import os, re, json, csv, datetime from flask import Flask, request, jsonify, abort @@ -25,6 +28,7 @@ from flask import Flask, request, jsonify, abort app = Flask(__name__) BASE = os.environ.get("ACTAS_DIR", "/srv/projects/drogheda/actas") ADMINS = {u.strip().lower() for u in os.environ.get("ACTAS_ADMINS", "leo").split(",") if u.strip()} +TIPOS_UNICOS = {"ensamble"} # tipos en los que un componente solo puede usarse en un acta real SAFE = re.compile(r"[^A-Za-z0-9_\-]") def slug(s, n=40): return SAFE.sub("", str(s or "").strip())[:n] @@ -35,6 +39,27 @@ def current_user(): def is_admin(): return current_user().lower() in ADMINS +def componentes_en_uso(tipo, serie_actual, componentes): + """Devuelve [(clave, valor, serie_otra)] para componentes ya usados por OTRA acta real del mismo tipo.""" + carpeta = os.path.join(BASE, tipo) + if not os.path.isdir(carpeta) or not isinstance(componentes, dict): + return [] + usados = [] + for fn in os.listdir(carpeta): + if not fn.endswith(".json"): + continue + try: + with open(os.path.join(carpeta, fn), encoding="utf-8") as f: + d = json.load(f) + except Exception: + continue + if d.get("serie") == serie_actual or not isinstance(d.get("componentes"), dict): + continue + for k, v in componentes.items(): + if v and d["componentes"].get(k) == v: + usados.append((k, v, d.get("serie"))) + return usados + def find_latest(tipo, serie): """Ruta del acta real más reciente para (tipo, serie), o None.""" carpeta = os.path.join(BASE, tipo) @@ -56,7 +81,7 @@ def whoami(): @app.get("/api/acta") def get_acta(): - tipo = slug(request.args.get("tipo")); serie = slug(request.args.get("serie"), 20) + tipo = slug(request.args.get("tipo")); serie = slug(request.args.get("serie"), 24) if not tipo or not serie: return jsonify(ok=False, error="faltan tipo/serie"), 400 ruta = find_latest(tipo, serie) @@ -76,7 +101,7 @@ def crear(): if not isinstance(d, dict): return jsonify(ok=False, error="Se esperaba un objeto"), 400 - tipo = slug(d.get("tipo")); serie = slug(d.get("serie"), 20) + tipo = slug(d.get("tipo")); serie = slug(d.get("serie"), 24) tecnico = str(d.get("tecnico", "")).strip() if not tipo or not serie: return jsonify(ok=False, error="Faltan 'tipo' o 'serie'"), 400 @@ -92,6 +117,17 @@ def crear(): return jsonify(ok=False, existe=True, error=f"Ya existe un acta para {serie}. Pide a Leo que la borre para rehacerla."), 409 + # actas compuestas (ensamble): un módulo no puede pertenecer a dos unidades + comps = d.get("componentes") + if isinstance(comps, dict): + comps = {slug(k, 20): slug(v, 20) for k, v in comps.items()} + d["componentes"] = comps + if not es_prueba and tipo in TIPOS_UNICOS: + usados = componentes_en_uso(tipo, serie, comps) + if usados: + det = "; ".join(f"{v} ya está en {s}" for _, v, s in usados) + return jsonify(ok=False, error=f"Módulo ya asignado: {det}. Pide a Leo que revise."), 409 + ts = datetime.datetime.now().replace(microsecond=0) iso = ts.isoformat() d["_recibido"] = iso @@ -121,7 +157,7 @@ def crear(): def borrar_acta(): if not is_admin(): return jsonify(ok=False, error="Solo un administrador puede borrar actas."), 403 - tipo = slug(request.args.get("tipo")); serie = slug(request.args.get("serie"), 20) + tipo = slug(request.args.get("tipo")); serie = slug(request.args.get("serie"), 24) if not tipo or not serie: return jsonify(ok=False, error="faltan tipo/serie"), 400 carpeta = os.path.join(BASE, tipo) @@ -180,9 +216,12 @@ def listar(): d = json.load(f) except Exception: continue - filas.append({"tipo": tipo, "archivo": f"{tipo}/{fn}", - "serie": d.get("serie"), "tecnico": d.get("tecnico"), - "resultado": d.get("resultado"), "recibido": d.get("_recibido")}) + fila = {"tipo": tipo, "archivo": f"{tipo}/{fn}", + "serie": d.get("serie"), "tecnico": d.get("tecnico"), + "resultado": d.get("resultado"), "recibido": d.get("_recibido")} + if isinstance(d.get("componentes"), dict): + fila["componentes"] = d["componentes"] + filas.append(fila) filas.sort(key=lambda r: (r.get("recibido") or ""), reverse=True) return jsonify(ok=True, total=len(filas), actas=filas) diff --git a/docs/actas/index.md b/docs/actas/index.md index c9681c7..1189b62 100644 --- a/docs/actas/index.md +++ b/docs/actas/index.md @@ -1,34 +1,56 @@ # Actas y control de calidad -Formularios que el técnico llena **en pantalla mientras prueba o ensambla** cada -tarjeta. Al terminar se firman (nombre + conformidad) y se envían al servidor, donde -quedan registrados por número de serie. Así queda constancia de que se siguió el -procedimiento en cada unidad. +Formularios que el técnico llena **en pantalla mientras prueba o ensambla**. Al terminar se +firman (nombre + conformidad) y se envían al servidor, donde quedan registrados por número de +serie. Un acta real se envía **una sola vez** por serie; para rehacerla, Leo la borra. + +## Flujo de una unidad HBOX v5 + +``` +EinarBoard V5-NN ──────► Acta de tarjeta ────────┐ +ArduSimple ARD-NNN ────► Acta módulo ArduSimple ─┤ +4G NTRIP Master RTK-NNNN ► Acta módem 4G ─────────┼─► Acta de integración (ArduSimple + 4G) ──► Acta de ensamble + │ (pareja que irá junta) (unidad HBOX5-Exx-Axxx-Rxxxx) + └──────────────────────────────────────────────────┘ +``` + +El acta de ensamble **no deja seleccionar** un módulo que no esté aprobado, que no tenga su +integración, o que ya esté montado en otra unidad. El número de la unidad se calcula solo a partir +de los tres módulos elegidos: `HBOX5-E07-A010-R0006` = EinarBoard V5-07 + ArduSimple ARD-010 + módem 4G RTK-0006. ## Formularios **[▶ Acta de prueba — EinarBoard V5](../formularios/v5.html)** -Lote de 15 tarjetas (V5-01 … V5-15). Procedimiento verificado §1–§6: inspección, -continuidad, rieles 3V3 / 5 V / 24 V, flasheo DFU y POST. +Lote de 15 tarjetas (V5-01 … V5-15; 30 seleccionables). Procedimiento: [Prueba y flasheo v5](../procedimientos/v5-prueba-y-flasheo.md). -**[▶ Acta de prueba — EinarBoard V4](../formularios/v4.html)** — *borrador* -V4-01 … V4-15. Adaptado de V5; la sección de MCU/firmware está marcada como -**pendiente de definir** para V4. Revisar antes de uso oficial. +**[▶ Acta de módulo — simpleRTK2B Pro / ArduSimple (ARD-001 … ARD-030)](../formularios/ardusimple.html)** +Un acta por receptor: visual, fix GNSS, configuración Helgen. Procedimiento: [Módulos e integración §2](../procedimientos/v5-modulos-integracion.md). -**[▶ Checklist de ensamble — HBOX](../formularios/ensamble.html)** — *borrador* -Control de calidad durante el armado: recepción de PCBA, módulos, mecánica, arnés, -etiquetado y energizado final. Ver la [guía de ensamble](../montaje/guia-ensamble-qc.md). +**[▶ Acta de módem — 4G NTRIP Master (RTK-0001 … RTK-0030)](../formularios/modem4g.html)** +Un acta por módem: IMEI/ICCID, configuración, registro en red. Procedimiento: [Módulos e integración §3](../procedimientos/v5-modulos-integracion.md). + +**[▶ Acta de integración — ArduSimple + 4G](../formularios/integracion.html)** +La pareja que irá junta: RTK FIX con correcciones NTRIP. Procedimiento: [Módulos e integración §4](../procedimientos/v5-modulos-integracion.md). + +**[▶ Acta de ensamble — unidad HBOX v5](../formularios/ensamble.html)** +Selecciona los tres módulos aprobados → número de unidad automático → armado, energizado desde 24 V, +batería y respaldo. Procedimiento: [Ensamble de la unidad](../procedimientos/v5-ensamble.md). + +**[▶ Acta de prueba — EinarBoard V4](../formularios/v4.html)** — *borrador*. ## Cómo se usan -1. Abre el formulario de la versión que estás probando y **elige el número de serie**. -2. Ve llenando cada sección conforme avanzas. El formulario **se guarda solo** en el - navegador: si se cierra, al reabrir con el mismo número de serie recuperas lo escrito. -3. Al final marca el **resultado** (Aprobada / Falla), escribe tu **nombre** y confirma - que seguiste el procedimiento. -4. Pulsa **Enviar acta firmada**. Queda registrada en el servidor. +1. Abre el formulario y **elige el número de serie** (o los módulos, en integración y ensamble). +2. Ve llenando cada sección conforme avanzas. El formulario **se guarda solo** en el navegador: si se + cierra, al reabrir con la misma serie recuperas lo escrito. +3. Al final marca el **resultado** (Aprobada / Falla), escribe tu **nombre** y confirma que seguiste + el procedimiento. +4. Pulsa **Enviar acta firmada**. Queda registrada en el servidor y aparece en el + [tablero de estado](../tablero.html). + +La serie **PRUEBA** sirve para practicar: no cuenta y se puede reenviar. !!! note "Para ingeniería" - Los envíos se guardan en el servidor por tipo y número de serie. Índice rápido en - `…/actas//_indice.csv`, o el listado completo en + Los envíos se guardan en el servidor por tipo y número de serie (`…/actas//`). Las actas de + integración y ensamble llevan `componentes` con los módulos elegidos. Listado completo en [`/api/actas`](/api/actas){:target="_blank"}. diff --git a/docs/descargas/SHA256SUMS.txt b/docs/descargas/SHA256SUMS.txt index b84ea21..aa0bd53 100644 --- a/docs/descargas/SHA256SUMS.txt +++ b/docs/descargas/SHA256SUMS.txt @@ -1 +1 @@ -91ca6f87a86686be33ce013110f214a7e94ae7c8909c9eea1dbe846d9b5247f3 hbox-v5-bringup_0.1.1.bin +8491625d494f9ee19a2cc0a7b00d62cc4ba5c08f0ada16572ac163ee10bf1e77 hbox-v5-bringup_0.2.0.bin diff --git a/docs/descargas/hbox-v5-bringup-README.md b/docs/descargas/hbox-v5-bringup-README.md index f447570..9688d9f 100644 --- a/docs/descargas/hbox-v5-bringup-README.md +++ b/docs/descargas/hbox-v5-bringup-README.md @@ -1,6 +1,7 @@ # hbox_v5_bringup — EinarBoard v5 first-flash bring-up firmware Bench-proves a newly assembled v5 board over USB-C **J11** only. Never powers the RTK stack. +Bench mode (0.2.0): `P`+`Y` stack ON/OFF, `n` NMEA monitor (fix 4 = RTK FIX). Checks: HSE 25 MHz + USB CDC · LSE Y4 · I2C: PCF8574 0x20 / SHT40 0x44 / TMP102 0x48 / SC16IS752 0x4D · gauge + RGB LED walk · ADC · digital inputs. Prints PASS/FAIL, then a `STAT` line every 2 s. Console commands: `r` rerun · `g` gauge walk · `i` I2C scan · `h` help. @@ -22,17 +23,21 @@ generic variant defaults to HSI, which is not accurate enough for USB. ## Flash (DFU, J11) Hold **SW2** (BOOT0) → tap **SW1** (RESET) → release SW2 → device `0483:DF11` appears. ```bash -dfu-util -a 0 -s 0x08000000:leave -D release/hbox-v5-bringup_0.1.1.bin +dfu-util -a 0 -s 0x08000000:leave -D release/hbox-v5-bringup_0.2.0.bin ``` -or STM32CubeProgrammer → USB → Connect → Download `release/hbox-v5-bringup_0.1.1.bin` @ `0x08000000`. +or STM32CubeProgrammer → USB → Connect → Download `release/hbox-v5-bringup_0.2.0.bin` @ `0x08000000`. ## Release | File | Version | SHA-256 (first 16) | Size | |---|---|---|---| -| **`release/hbox-v5-bringup_0.1.1.bin`** | **0.1.1 (2026-09-16) — USE THIS** | `91ca6f87a86686be` | 55 104 B | +| **`release/hbox-v5-bringup_0.2.0.bin`** | **0.2.0 (2026-09-17) — USE THIS** | `8491625d494f9ee1` | 64 780 B | +| `release/hbox-v5-bringup_0.1.1.bin` | 0.1.1 — superseded (no bench mode) | `91ca6f87a86686be` | 55 104 B | | `release/hbox-v5-bringup_0.1.0.bin` | 0.1.0 — SUPERSEDED (no float printf, ETH_CS driven) | `1a9a77ae479c8f86` | 41 040 B | ## Changelog +- **0.2.0** (2026-09-17): bench mode for the assembled unit — `P`+`Y` powers the RTK stack (only with 5 V on P2), + `P` again turns it off; `n` streams `$GxGGA` from the simpleRTK2B UART1 (PA3, 115200) for 20 s with fix/sats/HDOP; + `STAT` shows `stack=` and the last GGA. Charge stays disabled, K1 released. Uses the core's `Serial2` (USART2). - **0.1.1** (2026-09-16, audit): build with `rtlib=nanofp` (floats printed blank in 0.1.0); `ETH_CS` now `INPUT_PULLUP` instead of driving HIGH into the unpowered W5500; reset cause reports `PIN` only when `PINRSTF` is set (else `UNKNOWN`). diff --git a/docs/descargas/hbox-v5-bringup_0.1.1.bin b/docs/descargas/hbox-v5-bringup_0.1.1.bin deleted file mode 100644 index 488d01c..0000000 Binary files a/docs/descargas/hbox-v5-bringup_0.1.1.bin and /dev/null differ diff --git a/docs/descargas/hbox-v5-bringup_0.2.0.bin b/docs/descargas/hbox-v5-bringup_0.2.0.bin new file mode 100644 index 0000000..a9df083 Binary files /dev/null and b/docs/descargas/hbox-v5-bringup_0.2.0.bin differ diff --git a/docs/descargas/index.md b/docs/descargas/index.md index efbefaa..0efd661 100644 --- a/docs/descargas/index.md +++ b/docs/descargas/index.md @@ -7,16 +7,19 @@ Archivos que el técnico necesita en el banco. Verificar el SHA-256 antes de fla | Archivo | Versión | SHA-256 | Uso | |---|---|---|---| -| [**hbox-v5-bringup_0.1.1.bin**](hbox-v5-bringup_0.1.1.bin) | 0.1.1 (2026-09-16) | `91ca6f87a86686be…` ([SHA256SUMS](SHA256SUMS.txt)) | Sección 5 del [procedimiento de prueba y flasheo v5](../procedimientos/v5-prueba-y-flasheo.md) | +| [**hbox-v5-bringup_0.2.0.bin**](hbox-v5-bringup_0.2.0.bin) | **0.2.0** (2026-09-17) | `8491625d494f9ee1…` ([SHA256SUMS](SHA256SUMS.txt)) | Sección 5 del [procedimiento de prueba y flasheo v5](../procedimientos/v5-prueba-y-flasheo.md) y sección 5 del [ensamble](../procedimientos/v5-ensamble.md) (modo de banco `P` / `n`) | Flasheo por DFU en **J11** (mantener **SW2**, pulsar y soltar **SW1**, soltar SW2): ```bash -dfu-util -a 0 -s 0x08000000:leave -D hbox-v5-bringup_0.1.1.bin +dfu-util -a 0 -s 0x08000000:leave -D hbox-v5-bringup_0.2.0.bin ``` o con **STM32CubeProgrammer** → USB → Connect → Download del `.bin` en `0x08000000`. +!!! note "Tarjetas ya flasheadas con 0.1.1" + Re-flashear con 0.2.0 por el mismo procedimiento DFU antes del ensamble: 0.1.1 no tiene el modo de banco. + !!! warning "Solo para la tarjeta v5" Este firmware **no** es el de producción: solo prueba la tarjeta (reloj, USB, cristales, I²C, LEDs, ADC) sin encender la etapa RTK ni el cargador. **Nunca flashear `blackpillrtkcode_v3` diff --git a/docs/formularios/acta.css b/docs/formularios/acta.css index 55c442f..dea49e6 100644 --- a/docs/formularios/acta.css +++ b/docs/formularios/acta.css @@ -58,3 +58,15 @@ a.volver{color:var(--naranja);font-size:14px;text-decoration:none} button.limpiar{background:#fff;color:var(--tinta2);border:1px solid var(--linea); border-radius:10px;padding:13px 16px;font-size:15px;cursor:pointer} button.limpiar:hover{border-color:var(--naranja);color:var(--naranja)} + +/* ---- actas compuestas (ensamble / integración): estado de cada módulo ---- */ +.datos{grid-template-columns:repeat(auto-fit,minmax(240px,1fr))} +.datos .serie-calc{grid-column:1 / -1;background:var(--naranja-suave);border:1px solid #ffd8a8;border-radius:10px;padding:10px 12px} +.datos .serie-calc input{font-weight:700;font-size:20px;letter-spacing:.04em;background:#fff} +.badge{display:inline-block;margin-top:6px;font-size:13px;font-weight:700;padding:3px 10px;border-radius:99px;background:#f1efe9;color:var(--tinta2)} +.badge.ok{background:var(--ok-bg);color:var(--ok)} +.badge.falla{background:var(--falla-bg);color:var(--falla)} +.badge.pend{background:#f1efe9;color:var(--tinta2)} +.radios{display:flex;flex-wrap:wrap;gap:8px 16px;margin-top:4px} +.radios .rad{font-weight:500;display:flex;align-items:center;gap:6px;cursor:pointer} +.radios input{width:22px;height:22px;accent-color:var(--naranja)} diff --git a/docs/formularios/acta.js b/docs/formularios/acta.js index 4fa2200..e4e7b54 100644 --- a/docs/formularios/acta.js +++ b/docs/formularios/acta.js @@ -2,13 +2,32 @@ - Un acta real se envía UNA vez: al elegir una serie ya enviada, se carga llena y bloqueada. - Solo el administrador (leo) ve el botón "Borrar acta (reabrir)". - La serie PRUEBA no se bloquea (para practicar). - Tipos de ítem: check | medida | texto | radio | nota · Envío: POST /api/actas */ + Tipos de ítem: check | medida | texto | radio | nota · Envío: POST /api/actas + + Modos de serie: + (a) simple: cfg.prefijo + cfg.series (cantidad) [+ cfg.desde, cfg.pad] → "RTK-005" + (b) compuesta: cfg.componentes = [{clave, etiqueta, prefijo, desde, hasta, pad, tipoActa}] + cfg.serie(sel) → número de serie calculado, p.ej. "HBOX5-E07-R010-M006" + cfg.prerequisitos = [{etiqueta, tipo, serie(sel)}] → otras actas que deben existir y estar OK + cfg.exigirAprobados = true → no se puede enviar si algún componente/prerrequisito no está aprobado + cfg.unicoPorComponente = true → un componente no puede estar en dos actas de este tipo */ (function () { const $ = (t, a, ...k) => { const e = document.createElement(t); if (a) for (const p in a){ if(p==='html')e.innerHTML=a[p]; else if(p==='text')e.textContent=a[p]; else e.setAttribute(p,a[p]); } k.forEach(c=>e.append(c)); return e; }; + const num = s => (String(s || '').match(/(\d+)\s*$/) || [,''])[1]; + const esPrueba = s => String(s || '').toUpperCase().indexOf('PRUEBA') >= 0; + + function opciones(sel, prefijo, desde, hasta, pad) { + sel.append($('option', { value: '', text: '— elegir —' })); + sel.append($('option', { value: prefijo + '-PRUEBA', text: '⚙ PRUEBA (no cuenta)' })); + for (let i = desde; i <= hasta; i++) { + const s = prefijo + '-' + String(i).padStart(pad, '0'); + sel.append($('option', { value: s, text: s })); + } + } function render(cfg) { const root = document.getElementById('acta'); - const key = () => 'acta:' + cfg.tipo + ':' + (serieSel.value || ''); + const compuesta = Array.isArray(cfg.componentes) && cfg.componentes.length > 0; let admin = false; const wrap = $('div', { class: 'wrap' }); @@ -24,25 +43,57 @@ if (cfg.borrador) wrap.append($('div', { class: 'borrador', html: '⚠ BORRADOR — este formulario aún debe revisarse con ingeniería antes de usarlo como registro oficial.' })); + if (cfg.intro) wrap.append($('div', { class: 'nota-item', style: 'margin:0 0 10px', html: cfg.intro })); + // ---------------- cabecera: serie (simple o compuesta) + técnico ---------------- const datos = $('div', { class: 'datos' }); - const serieSel = $('select', { id: 'f_serie' }); - serieSel.append($('option', { value: '', text: '— elegir —' })); - serieSel.append($('option', { value: cfg.prefijo + '-PRUEBA', text: '⚙ PRUEBA (no cuenta)' })); - for (let i = 1; i <= (cfg.series || 15); i++) { - const s = cfg.prefijo + '-' + String(i).padStart(2, '0'); - serieSel.append($('option', { value: s, text: s })); - } + const serieSel = $('select', { id: 'f_serie' }); // modo simple + const serieCalc = $('input', { type: 'text', id: 'f_serie_calc', readonly: 'readonly', placeholder: 'se calcula al elegir los módulos' }); // modo compuesto + const compSel = {}; // clave ->