actas: formularios de prueba V5/V4 + checklist de ensamble + backend
- backend Flask (backend/): recibe formularios firmados, guarda JSON por numero de serie en /srv/projects/drogheda/actas, indice CSV, GET /api/actas + systemd unit + README de instalacion (nginx /api proxy) - formularios (docs/formularios/): acta.css/acta.js (motor con autoguardado y envio), V5 fiel al procedimiento verificado, V4 borrador adaptado, checklist de ensamble borrador; firma = nombre + conformidad - docs/actas/index.md + guia-ensamble-qc.md; nav actualizada - probado end-to-end (headless): envio + validacion + guardado por serie Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJ4eAB6zjQT49RVwpgfKdt
This commit is contained in:
parent
a7495bfbe6
commit
57f67332eb
63
backend/README.md
Normal file
63
backend/README.md
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
# Backend de actas — configuración en el servidor (una sola vez)
|
||||||
|
|
||||||
|
Servicio Flask que recibe los formularios firmados de los técnicos y los guarda como
|
||||||
|
JSON por número de serie en `/srv/projects/drogheda/actas/`. Corre detrás de nginx en
|
||||||
|
`127.0.0.1:8081`; nginx expone `/api/` en el mismo dominio, con la misma contraseña.
|
||||||
|
|
||||||
|
## 1. Instalar (en el servidor, como leo)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /srv/projects/drogheda/hbox-docs/backend
|
||||||
|
python3 -m venv venv
|
||||||
|
./venv/bin/pip install -r requirements.txt
|
||||||
|
|
||||||
|
# carpeta donde se guardan las actas
|
||||||
|
mkdir -p /srv/projects/drogheda/actas
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Servicio systemd (arranque automático)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo cp actas-hbox.service /etc/systemd/system/actas-hbox.service
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable --now actas-hbox
|
||||||
|
systemctl status actas-hbox # debe decir "active (running)"
|
||||||
|
curl -s http://127.0.0.1:8081/api/salud # {"ok":true,...}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. nginx — exponer /api/ en el mismo dominio
|
||||||
|
|
||||||
|
Dentro del bloque `server { ... }` de `drogheda.helgen.online` (el mismo archivo
|
||||||
|
`/etc/nginx/sites-enabled/drogheda.helgen.online.conf`, en la parte de `listen 443`),
|
||||||
|
agrega **antes** del `location / {`:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://127.0.0.1:8081;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
# hereda el auth_basic del server; si quieres, puedes repetirlo aquí.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Luego:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo nginx -t && sudo systemctl reload nginx
|
||||||
|
curl -u leo:TU_PASS https://drogheda.helgen.online/api/salud # {"ok":true,...}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Ver las actas recibidas
|
||||||
|
|
||||||
|
- Índice rápido por tipo: `/srv/projects/drogheda/actas/<tipo>/_indice.csv` (ábrelo en Excel).
|
||||||
|
- Listado JSON: `https://drogheda.helgen.online/api/actas`
|
||||||
|
- Un acta concreta: `https://drogheda.helgen.online/api/actas/prueba-v5/V5-07_....json`
|
||||||
|
- Respaldo: basta con copiar la carpeta `/srv/projects/drogheda/actas/`.
|
||||||
|
|
||||||
|
## Actualizar el backend
|
||||||
|
|
||||||
|
Tras un `git pull` que cambie `app.py`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl restart actas-hbox
|
||||||
|
```
|
||||||
16
backend/actas-hbox.service
Normal file
16
backend/actas-hbox.service
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Actas HBOX — recepcion de formularios de tecnicos
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=leo
|
||||||
|
Group=leo
|
||||||
|
WorkingDirectory=/srv/projects/drogheda/hbox-docs/backend
|
||||||
|
Environment=ACTAS_DIR=/srv/projects/drogheda/actas
|
||||||
|
Environment=PORT=8081
|
||||||
|
ExecStart=/srv/projects/drogheda/hbox-docs/backend/venv/bin/gunicorn -w 2 -b 127.0.0.1:8081 app:app
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=3
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
108
backend/app.py
Normal file
108
backend/app.py
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Actas HBOX — servicio mínimo para recibir formularios firmados de los técnicos.
|
||||||
|
|
||||||
|
Guarda cada envío como un JSON por número de serie, y mantiene un índice CSV por tipo
|
||||||
|
de acta. Sin base de datos: un archivo por envío, fácil de respaldar y de leer.
|
||||||
|
|
||||||
|
Rutas:
|
||||||
|
POST /api/actas -> recibe un acta (JSON), la guarda, devuelve {ok, archivo}
|
||||||
|
GET /api/actas -> lista los envíos (para ingeniería)
|
||||||
|
GET /api/actas/<tipo>/<archivo> -> devuelve un acta concreta
|
||||||
|
GET /api/salud -> healthcheck
|
||||||
|
|
||||||
|
Variables de entorno:
|
||||||
|
ACTAS_DIR carpeta donde se guardan las actas (default /srv/projects/drogheda/actas)
|
||||||
|
"""
|
||||||
|
import os, re, json, csv, datetime
|
||||||
|
from flask import Flask, request, jsonify, abort
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
BASE = os.environ.get("ACTAS_DIR", "/srv/projects/drogheda/actas")
|
||||||
|
|
||||||
|
SAFE = re.compile(r"[^A-Za-z0-9_\-]")
|
||||||
|
|
||||||
|
def slug(s, n=40):
|
||||||
|
return SAFE.sub("", str(s or "").strip())[:n]
|
||||||
|
|
||||||
|
@app.get("/api/salud")
|
||||||
|
def salud():
|
||||||
|
return jsonify(ok=True, servicio="actas-hbox")
|
||||||
|
|
||||||
|
@app.post("/api/actas")
|
||||||
|
def crear():
|
||||||
|
try:
|
||||||
|
d = request.get_json(force=True)
|
||||||
|
except Exception:
|
||||||
|
return jsonify(ok=False, error="JSON inválido"), 400
|
||||||
|
if not isinstance(d, dict):
|
||||||
|
return jsonify(ok=False, error="Se esperaba un objeto"), 400
|
||||||
|
|
||||||
|
tipo = slug(d.get("tipo")) # p.ej. prueba-v5, prueba-v4, ensamble
|
||||||
|
serie = slug(d.get("serie"), 20) # p.ej. V5-07
|
||||||
|
tecnico = str(d.get("tecnico", "")).strip()
|
||||||
|
if not tipo or not serie:
|
||||||
|
return jsonify(ok=False, error="Faltan 'tipo' o 'serie'"), 400
|
||||||
|
if not tecnico:
|
||||||
|
return jsonify(ok=False, error="Falta el nombre del técnico"), 400
|
||||||
|
if not d.get("conforme"):
|
||||||
|
return jsonify(ok=False, error="Falta la conformidad del técnico"), 400
|
||||||
|
|
||||||
|
ts = datetime.datetime.now().replace(microsecond=0)
|
||||||
|
iso = ts.isoformat()
|
||||||
|
d["_recibido"] = iso
|
||||||
|
d["_ip"] = request.headers.get("X-Forwarded-For", request.remote_addr)
|
||||||
|
|
||||||
|
carpeta = os.path.join(BASE, tipo)
|
||||||
|
os.makedirs(carpeta, exist_ok=True)
|
||||||
|
archivo = f"{serie}_{iso.replace(':', '-')}.json"
|
||||||
|
ruta = os.path.join(carpeta, archivo)
|
||||||
|
with open(ruta, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(d, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
# índice CSV acumulado por tipo (fácil de abrir en Excel)
|
||||||
|
idx = os.path.join(carpeta, "_indice.csv")
|
||||||
|
nuevo = not os.path.exists(idx)
|
||||||
|
with open(idx, "a", newline="", encoding="utf-8") as f:
|
||||||
|
w = csv.writer(f)
|
||||||
|
if nuevo:
|
||||||
|
w.writerow(["recibido", "serie", "tecnico", "resultado", "archivo"])
|
||||||
|
w.writerow([iso, serie, tecnico, d.get("resultado", ""), archivo])
|
||||||
|
|
||||||
|
return jsonify(ok=True, archivo=f"{tipo}/{archivo}")
|
||||||
|
|
||||||
|
@app.get("/api/actas")
|
||||||
|
def listar():
|
||||||
|
filas = []
|
||||||
|
if os.path.isdir(BASE):
|
||||||
|
for tipo in sorted(os.listdir(BASE)):
|
||||||
|
carpeta = os.path.join(BASE, tipo)
|
||||||
|
if not os.path.isdir(carpeta):
|
||||||
|
continue
|
||||||
|
for fn in sorted(os.listdir(carpeta)):
|
||||||
|
if fn.endswith(".json"):
|
||||||
|
try:
|
||||||
|
with open(os.path.join(carpeta, fn), encoding="utf-8") as f:
|
||||||
|
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"),
|
||||||
|
})
|
||||||
|
filas.sort(key=lambda r: (r.get("recibido") or ""), reverse=True)
|
||||||
|
return jsonify(ok=True, total=len(filas), actas=filas)
|
||||||
|
|
||||||
|
@app.get("/api/actas/<tipo>/<archivo>")
|
||||||
|
def leer(tipo, archivo):
|
||||||
|
tipo = slug(tipo); archivo = slug(archivo, 120)
|
||||||
|
if not archivo.endswith("json"):
|
||||||
|
archivo += ".json" if not archivo.endswith(".json") else ""
|
||||||
|
ruta = os.path.join(BASE, tipo, archivo)
|
||||||
|
if not os.path.isfile(ruta):
|
||||||
|
abort(404)
|
||||||
|
with open(ruta, encoding="utf-8") as f:
|
||||||
|
return app.response_class(f.read(), mimetype="application/json")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(host="127.0.0.1", port=int(os.environ.get("PORT", "8081")))
|
||||||
2
backend/requirements.txt
Normal file
2
backend/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
flask>=3.0
|
||||||
|
gunicorn>=21.2
|
||||||
34
docs/actas/index.md
Normal file
34
docs/actas/index.md
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
**[▶ 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.
|
||||||
|
|
||||||
|
**[▶ 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.
|
||||||
|
|
||||||
|
**[▶ 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).
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
!!! note "Para ingeniería"
|
||||||
|
Los envíos se guardan en el servidor por tipo y número de serie. Índice rápido en
|
||||||
|
`…/actas/<tipo>/_indice.csv`, o el listado completo en
|
||||||
|
[`/api/actas`](/api/actas){:target="_blank"}.
|
||||||
57
docs/formularios/acta.css
Normal file
57
docs/formularios/acta.css
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
/* Estilo compartido de las actas HBOX — pensado para tablet/laptop en el banco */
|
||||||
|
:root{
|
||||||
|
--naranja:#e8590c; --naranja-suave:#fff1e8; --tinta:#1d1c19; --tinta2:#5a564d;
|
||||||
|
--linea:#e2ddd2; --papel:#faf8f4; --tarjeta:#ffffff; --ok:#2f9e44; --falla:#e03131;
|
||||||
|
--ok-bg:#ebfbee; --falla-bg:#fff5f5; --aviso:#f08c00; --aviso-bg:#fff9db;
|
||||||
|
}
|
||||||
|
*{box-sizing:border-box}
|
||||||
|
body{margin:0;background:var(--papel);color:var(--tinta);
|
||||||
|
font:16px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;-webkit-text-size-adjust:100%}
|
||||||
|
.wrap{max-width:820px;margin:0 auto;padding:16px 16px 120px}
|
||||||
|
header.top{background:var(--tinta);color:#fff;padding:16px;border-radius:12px;margin-bottom:16px}
|
||||||
|
header.top h1{margin:0;font-size:20px}
|
||||||
|
header.top p{margin:4px 0 0;color:#d7d2c8;font-size:14px}
|
||||||
|
.borrador{background:var(--aviso-bg);border:1px solid var(--aviso);color:#a5620a;
|
||||||
|
padding:10px 14px;border-radius:10px;margin-bottom:16px;font-size:14px}
|
||||||
|
.datos{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:8px}
|
||||||
|
@media(max-width:560px){.datos{grid-template-columns:1fr}}
|
||||||
|
label{display:block;font-weight:600;font-size:14px;margin-bottom:4px}
|
||||||
|
input[type=text],input[type=number],select,textarea{
|
||||||
|
width:100%;padding:11px 12px;border:1px solid var(--linea);border-radius:9px;
|
||||||
|
font-size:16px;background:#fff;color:var(--tinta)}
|
||||||
|
input:focus,select:focus,textarea:focus{outline:2px solid var(--naranja);border-color:var(--naranja)}
|
||||||
|
section.paso{background:var(--tarjeta);border:1px solid var(--linea);border-radius:12px;
|
||||||
|
padding:14px 16px;margin:14px 0}
|
||||||
|
section.paso h2{margin:0 0 4px;font-size:17px;color:var(--naranja)}
|
||||||
|
section.paso .sub{color:var(--tinta2);font-size:13.5px;margin:0 0 10px}
|
||||||
|
.item{padding:10px 0;border-top:1px solid var(--linea)}
|
||||||
|
.item:first-of-type{border-top:0}
|
||||||
|
.chk{display:flex;gap:12px;align-items:flex-start;cursor:pointer}
|
||||||
|
.chk input{width:26px;height:26px;margin:1px 0 0;flex:0 0 auto;accent-color:var(--naranja)}
|
||||||
|
.chk .txt{flex:1}
|
||||||
|
.medida{display:flex;flex-wrap:wrap;gap:8px 12px;align-items:center}
|
||||||
|
.medida .txt{flex:1 1 100%;font-weight:600}
|
||||||
|
.medida .esp{flex:1 1 100%;color:var(--tinta2);font-size:13px;margin-top:-2px}
|
||||||
|
.medida input{max-width:150px}
|
||||||
|
.medida .u{color:var(--tinta2)}
|
||||||
|
.nota-item{color:var(--tinta2);font-size:14px}
|
||||||
|
.aviso{background:var(--aviso-bg);border-left:3px solid var(--aviso);padding:8px 12px;
|
||||||
|
border-radius:0 8px 8px 0;font-size:13.5px;margin:6px 0}
|
||||||
|
.firma{background:var(--naranja-suave);border:1px solid #ffd8a8}
|
||||||
|
.resultado{display:flex;gap:10px;margin-top:6px}
|
||||||
|
.resultado label{flex:1;border:2px solid var(--linea);border-radius:10px;padding:12px;
|
||||||
|
text-align:center;cursor:pointer;font-weight:700;margin:0}
|
||||||
|
.resultado input{position:absolute;opacity:0}
|
||||||
|
.resultado label:has(input:checked).ok{border-color:var(--ok);background:var(--ok-bg);color:var(--ok)}
|
||||||
|
.resultado label:has(input:checked).falla{border-color:var(--falla);background:var(--falla-bg);color:var(--falla)}
|
||||||
|
.barra{position:fixed;left:0;right:0;bottom:0;background:#fff;border-top:1px solid var(--linea);
|
||||||
|
padding:12px 16px;display:flex;gap:12px;align-items:center;justify-content:space-between}
|
||||||
|
.barra .estado{font-size:13px;color:var(--tinta2)}
|
||||||
|
button.enviar{background:var(--naranja);color:#fff;border:0;border-radius:10px;
|
||||||
|
padding:13px 22px;font-size:16px;font-weight:700;cursor:pointer}
|
||||||
|
button.enviar:disabled{opacity:.5;cursor:not-allowed}
|
||||||
|
.msg{padding:12px 14px;border-radius:10px;margin-top:12px;font-weight:600;display:none}
|
||||||
|
.msg.ok{display:block;background:var(--ok-bg);color:var(--ok);border:1px solid var(--ok)}
|
||||||
|
.msg.err{display:block;background:var(--falla-bg);color:var(--falla);border:1px solid var(--falla)}
|
||||||
|
.enviada{opacity:.6;pointer-events:none}
|
||||||
|
a.volver{color:var(--naranja);font-size:14px;text-decoration:none}
|
||||||
156
docs/formularios/acta.js
Normal file
156
docs/formularios/acta.js
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
/* Motor de actas HBOX — renderiza el formulario desde una config y lo envía al servidor.
|
||||||
|
Uso: <script src="acta.js"></script> luego Acta.render(CONFIG)
|
||||||
|
Tipos de ítem: check | medida | texto | radio | nota
|
||||||
|
Autoguardado en localStorage por (tipo + serie). Envío: POST /api/actas */
|
||||||
|
(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; };
|
||||||
|
|
||||||
|
function render(cfg) {
|
||||||
|
const root = document.getElementById('acta');
|
||||||
|
const key = () => 'acta:' + cfg.tipo + ':' + (serieSel.value || '');
|
||||||
|
const wrap = $('div', { class: 'wrap' });
|
||||||
|
|
||||||
|
const head = $('header', { class: 'top' });
|
||||||
|
head.append($('h1', { text: cfg.titulo }));
|
||||||
|
head.append($('p', { text: cfg.subtitulo || 'Helgen Technologies · llena mientras pruebas la tarjeta' }));
|
||||||
|
wrap.append(head);
|
||||||
|
wrap.append($('a', { class: 'volver', href: '../actas/' , text: '← Volver a Actas' }));
|
||||||
|
|
||||||
|
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.' }));
|
||||||
|
|
||||||
|
// datos de cabecera
|
||||||
|
const datos = $('div', { class: 'datos' });
|
||||||
|
const serieSel = $('select', { id: 'f_serie' });
|
||||||
|
serieSel.append($('option', { value: '', text: '— elegir —' }));
|
||||||
|
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 tecnico = $('input', { type: 'text', id: 'f_tecnico', placeholder: 'Nombre y apellido' });
|
||||||
|
const dS = $('div'); dS.append($('label', { text: 'Número de serie de la tarjeta' }), serieSel);
|
||||||
|
const dT = $('div'); dT.append($('label', { text: 'Técnico' }), tecnico);
|
||||||
|
datos.append(dS, dT);
|
||||||
|
const cab = $('section', { class: 'paso' }); cab.append(datos);
|
||||||
|
wrap.append(cab);
|
||||||
|
|
||||||
|
// secciones del procedimiento
|
||||||
|
cfg.secciones.forEach((sec, si) => {
|
||||||
|
const s = $('section', { class: 'paso' });
|
||||||
|
s.append($('h2', { text: sec.titulo }));
|
||||||
|
if (sec.sub) s.append($('p', { class: 'sub', text: sec.sub }));
|
||||||
|
(sec.items || []).forEach((it, ii) => {
|
||||||
|
const id = `s${si}_${ii}`;
|
||||||
|
const box = $('div', { class: 'item' });
|
||||||
|
if (it.aviso) box.append($('div', { class: 'aviso', html: it.aviso }));
|
||||||
|
if (it.tipo === 'check') {
|
||||||
|
const lab = $('label', { class: 'chk' });
|
||||||
|
lab.append($('input', { type: 'checkbox', 'data-id': id, 'data-lbl': it.txt }));
|
||||||
|
lab.append($('span', { class: 'txt', html: it.txt }));
|
||||||
|
box.append(lab);
|
||||||
|
} else if (it.tipo === 'medida') {
|
||||||
|
box.classList.add('medida');
|
||||||
|
box.append($('span', { class: 'txt', text: it.txt }));
|
||||||
|
if (it.esperado) box.append($('span', { class: 'esp', html: 'Esperado: ' + it.esperado }));
|
||||||
|
box.append($('input', { type: 'text', inputmode: 'decimal', 'data-id': id, 'data-lbl': it.txt, placeholder: 'medido' }));
|
||||||
|
if (it.u) box.append($('span', { class: 'u', text: it.u }));
|
||||||
|
} else if (it.tipo === 'texto') {
|
||||||
|
box.append($('label', { text: it.txt }));
|
||||||
|
box.append($('textarea', { rows: '2', 'data-id': id, 'data-lbl': it.txt, placeholder: it.ph || '' }));
|
||||||
|
} else if (it.tipo === 'nota') {
|
||||||
|
box.append($('div', { class: 'nota-item', html: it.txt }));
|
||||||
|
}
|
||||||
|
s.append(box);
|
||||||
|
});
|
||||||
|
wrap.append(s);
|
||||||
|
});
|
||||||
|
|
||||||
|
// firma / conformidad
|
||||||
|
const firma = $('section', { class: 'paso firma' });
|
||||||
|
firma.append($('h2', { text: 'Cierre y firma' }));
|
||||||
|
firma.append($('p', { class: 'sub', text: 'Al enviar, quedas registrado como responsable de esta prueba.' }));
|
||||||
|
const res = $('div', { class: 'resultado' });
|
||||||
|
const rOK = $('label', { class: 'ok' }); rOK.append($('input', { type: 'radio', name: 'resultado', value: 'OK' }), document.createTextNode('APROBADA'));
|
||||||
|
const rNO = $('label', { class: 'falla' }); rNO.append($('input', { type: 'radio', name: 'resultado', value: 'FALLA' }), document.createTextNode('FALLA'));
|
||||||
|
res.append(rOK, rNO);
|
||||||
|
firma.append($('label', { text: 'Resultado final' }), res);
|
||||||
|
const conf = $('label', { class: 'chk', style: 'margin-top:14px' });
|
||||||
|
conf.append($('input', { type: 'checkbox', id: 'f_conforme' }));
|
||||||
|
conf.append($('span', { class: 'txt', html: 'Confirmo que <b>seguí el procedimiento completo</b> y que los datos anotados son verídicos.' }));
|
||||||
|
firma.append(conf);
|
||||||
|
wrap.append(firma);
|
||||||
|
|
||||||
|
const msg = $('div', { class: 'msg', id: 'f_msg' });
|
||||||
|
wrap.append(msg);
|
||||||
|
root.append(wrap);
|
||||||
|
|
||||||
|
// barra fija de envío
|
||||||
|
const barra = $('div', { class: 'barra' });
|
||||||
|
const estado = $('span', { class: 'estado', id: 'f_estado', text: 'Sin guardar' });
|
||||||
|
const btn = $('button', { class: 'enviar', id: 'f_enviar', text: 'Enviar acta firmada' });
|
||||||
|
barra.append(estado, btn);
|
||||||
|
document.body.append(barra);
|
||||||
|
|
||||||
|
// ---- autoguardado ----
|
||||||
|
function snapshot() {
|
||||||
|
const r = {};
|
||||||
|
wrap.querySelectorAll('[data-id]').forEach(el => {
|
||||||
|
r[el.dataset.id] = { lbl: el.dataset.lbl, val: el.type === 'checkbox' ? el.checked : el.value };
|
||||||
|
});
|
||||||
|
const rr = wrap.querySelector('input[name=resultado]:checked');
|
||||||
|
return { tipo: cfg.tipo, serie: serieSel.value, tecnico: tecnico.value,
|
||||||
|
conforme: document.getElementById('f_conforme').checked,
|
||||||
|
resultado: rr ? rr.value : '', respuestas: r };
|
||||||
|
}
|
||||||
|
let t;
|
||||||
|
function guardar() {
|
||||||
|
if (!serieSel.value) { estado.textContent = 'Elige el número de serie'; return; }
|
||||||
|
clearTimeout(t);
|
||||||
|
t = setTimeout(() => {
|
||||||
|
try { localStorage.setItem(key(), JSON.stringify(snapshot())); } catch (e) {}
|
||||||
|
estado.textContent = 'Guardado local ' + new Date().toLocaleTimeString('es-MX', { hour: '2-digit', minute: '2-digit' });
|
||||||
|
}, 400);
|
||||||
|
}
|
||||||
|
function cargar() {
|
||||||
|
let d; try { d = JSON.parse(localStorage.getItem(key()) || 'null'); } catch (e) {}
|
||||||
|
if (!d) { estado.textContent = 'Nueva acta'; return; }
|
||||||
|
tecnico.value = d.tecnico || '';
|
||||||
|
document.getElementById('f_conforme').checked = !!d.conforme;
|
||||||
|
if (d.resultado) { const el = wrap.querySelector(`input[name=resultado][value="${d.resultado}"]`); if (el) el.checked = true; }
|
||||||
|
for (const k in (d.respuestas || {})) {
|
||||||
|
const el = wrap.querySelector(`[data-id="${k}"]`); if (!el) continue;
|
||||||
|
if (el.type === 'checkbox') el.checked = !!d.respuestas[k].val; else el.value = d.respuestas[k].val || '';
|
||||||
|
}
|
||||||
|
estado.textContent = 'Borrador recuperado';
|
||||||
|
}
|
||||||
|
wrap.addEventListener('input', guardar);
|
||||||
|
serieSel.addEventListener('change', cargar);
|
||||||
|
|
||||||
|
// ---- envío ----
|
||||||
|
btn.addEventListener('click', async () => {
|
||||||
|
const d = snapshot();
|
||||||
|
if (!d.serie) return fail('Elige el número de serie de la tarjeta.');
|
||||||
|
if (!d.tecnico.trim()) return fail('Escribe tu nombre.');
|
||||||
|
if (!d.resultado) return fail('Marca el resultado final (Aprobada o Falla).');
|
||||||
|
if (!d.conforme) return fail('Debes confirmar que seguiste el procedimiento.');
|
||||||
|
btn.disabled = true; btn.textContent = 'Enviando…';
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/actas', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(d) });
|
||||||
|
const j = await r.json();
|
||||||
|
if (!r.ok || !j.ok) throw new Error(j.error || ('HTTP ' + r.status));
|
||||||
|
msg.className = 'msg ok';
|
||||||
|
msg.innerHTML = '✓ Acta enviada y registrada como <b>' + d.serie + '</b>. Puedes cerrar esta página.';
|
||||||
|
try { localStorage.removeItem(key()); } catch (e) {}
|
||||||
|
wrap.classList.add('enviada'); estado.textContent = 'Enviada';
|
||||||
|
btn.textContent = 'Enviada ✓'; window.scrollTo(0, document.body.scrollHeight);
|
||||||
|
} catch (e) {
|
||||||
|
fail('No se pudo enviar: ' + e.message + '. Revisa tu conexión e inténtalo otra vez (tu borrador está guardado).');
|
||||||
|
btn.disabled = false; btn.textContent = 'Enviar acta firmada';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
function fail(m) { msg.className = 'msg err'; msg.textContent = m; window.scrollTo(0, document.body.scrollHeight); }
|
||||||
|
|
||||||
|
cargar();
|
||||||
|
}
|
||||||
|
window.Acta = { render };
|
||||||
|
})();
|
||||||
83
docs/formularios/ensamble.html
Normal file
83
docs/formularios/ensamble.html
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Checklist de ensamble — HBOX (borrador)</title>
|
||||||
|
<link rel="stylesheet" href="acta.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="acta"></div>
|
||||||
|
<script src="acta.js"></script>
|
||||||
|
<script>
|
||||||
|
/* BORRADOR — checklist de control de calidad durante el ensamble del HBOX.
|
||||||
|
Basado en el BOM de ensamble y el pinout de arneses. Ingeniería debe validar
|
||||||
|
los pasos antes de usarlo como registro oficial. */
|
||||||
|
Acta.render({
|
||||||
|
tipo: 'ensamble',
|
||||||
|
prefijo: 'HBOX',
|
||||||
|
series: 15,
|
||||||
|
borrador: true,
|
||||||
|
titulo: 'Control de calidad de ensamble — HBOX',
|
||||||
|
subtitulo: 'Llena mientras armas la unidad. Un acta por equipo (número de serie).',
|
||||||
|
secciones: [
|
||||||
|
{ titulo: '1 · Recepción de la PCBA',
|
||||||
|
sub: 'Revisión de la tarjeta ensamblada que llega de JLCPCB, antes de montar nada.',
|
||||||
|
items: [
|
||||||
|
{ tipo:'check', txt:'La tarjeta corresponde al modelo/revisión correcto (serigrafía).' },
|
||||||
|
{ tipo:'check', txt:'Sin componentes faltantes, torcidos, ni con soldadura fría (inspección visual con lupa).' },
|
||||||
|
{ tipo:'check', txt:'Sin puentes de soldadura, sobre todo en los ICs de paso fino.' },
|
||||||
|
{ tipo:'check', txt:'Conectores y zócalos rectos, sin pines doblados.' },
|
||||||
|
{ tipo:'texto', txt:'Defectos encontrados en la PCBA', ph:'ninguno / describir' },
|
||||||
|
] },
|
||||||
|
{ titulo: '2 · Soldadura manual (THT y conectores)',
|
||||||
|
sub: 'Partes que se sueldan a mano después de la PCBA (si aplica en este lote).',
|
||||||
|
items: [
|
||||||
|
{ tipo:'check', txt:'Terminal de potencia <b>P1</b> (Phoenix/GX16) firme y con soldadura completa.' },
|
||||||
|
{ tipo:'check', txt:'Conectores JST (<b>P2, P4, P6, P8, P10, P11, P12</b>…) soldados y sin frío.' },
|
||||||
|
{ tipo:'check', txt:'Relevador <b>K1</b>, botón <b>SW1</b> y electrolíticos radiales bien asentados y en polaridad correcta.' },
|
||||||
|
{ tipo:'check', txt:'Zócalos de módulos (J1/J2, J7/J8, MOD1) soldados a ras y rectos.' },
|
||||||
|
] },
|
||||||
|
{ titulo: '3 · Módulos enchufables',
|
||||||
|
sub: 'Se instalan sobre la tarjeta ya probada. Verifica orientación antes de energizar.',
|
||||||
|
items: [
|
||||||
|
{ tipo:'check', txt:'MCU (STM32) correctamente asentado y en la orientación correcta.' },
|
||||||
|
{ tipo:'check', txt:'simpleRTK2B Pro insertado en <b>MOD1</b> sin forzar pines.' },
|
||||||
|
{ tipo:'check', txt:'Módulo 4G insertado en su zócalo (2 mm) en la orientación correcta.' },
|
||||||
|
{ tipo:'check', txt:'Ninguna antena/módulo obstruye el cierre del gabinete.' },
|
||||||
|
] },
|
||||||
|
{ titulo: '4 · Mecánica y gabinete',
|
||||||
|
items: [
|
||||||
|
{ tipo:'check', txt:'Tarjeta fijada con tornillos/separadores M2.5/M3 en los barrenos correctos, sin flexión.' },
|
||||||
|
{ tipo:'check', txt:'Ventilador montado y con flujo en la dirección correcta.' },
|
||||||
|
{ tipo:'check', txt:'Conectores de panel (GX16, RJ45, antenas) alineados con sus recortes del gabinete.' },
|
||||||
|
{ tipo:'check', txt:'Sin cables pellizcados; el gabinete cierra sin forzar.' },
|
||||||
|
] },
|
||||||
|
{ titulo: '5 · Arnés y cableado',
|
||||||
|
sub: 'Verifica cada conector contra el pinout de arneses.',
|
||||||
|
items: [
|
||||||
|
{ tipo:'check', txt:'<b>P1</b> alimentación de máquina: pin 1 = 24V, pin 2 = GND, pin 3 = ignición.' },
|
||||||
|
{ tipo:'check', txt:'<b>P2</b> entrada 5 V (Mean Well) en polaridad correcta.' },
|
||||||
|
{ tipo:'check', txt:'<b>P4</b> batería 1S4P + NTC conectado (pin 1 = BAT+, 2 = GND, 3 = NTC).' },
|
||||||
|
{ tipo:'check', txt:'<b>P6</b> ventilador y demás JST insertados a fondo (clic), cada uno en su lugar.' },
|
||||||
|
{ tipo:'check', txt:'Sin conectores intercambiados; colores/etiquetas del arnés coinciden.' },
|
||||||
|
{ tipo:'texto', txt:'Observaciones del arnés', ph:'ninguno / describir' },
|
||||||
|
] },
|
||||||
|
{ titulo: '6 · Etiquetado e identidad',
|
||||||
|
items: [
|
||||||
|
{ tipo:'check', txt:'Número de serie del equipo colocado de forma visible y legible.' },
|
||||||
|
{ tipo:'check', txt:'Etiqueta no tapa serigrafía, tornillos ni ventilación.' },
|
||||||
|
] },
|
||||||
|
{ titulo: '7 · Energizado final',
|
||||||
|
sub: 'Primera energización del equipo ya armado (con la tarjeta previamente aprobada en su acta de prueba).',
|
||||||
|
items: [
|
||||||
|
{ tipo:'check', txt:'Enciende sin olor, humo ni calentamiento anormal.' },
|
||||||
|
{ tipo:'check', txt:'LED(s) de estado responden como se espera.' },
|
||||||
|
{ tipo:'check', txt:'Ventilador gira.' },
|
||||||
|
{ tipo:'texto', txt:'Número de serie de la tarjeta (acta de prueba) usada en este equipo', ph:'p.ej. V5-07' },
|
||||||
|
] },
|
||||||
|
]
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
85
docs/formularios/v4.html
Normal file
85
docs/formularios/v4.html
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Acta de prueba — EinarBoard V4 (borrador)</title>
|
||||||
|
<link rel="stylesheet" href="acta.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="acta"></div>
|
||||||
|
<script src="acta.js"></script>
|
||||||
|
<script>
|
||||||
|
/* BORRADOR V4 — adaptado del procedimiento verificado de V5.
|
||||||
|
V4 usa módulos enchufables (Blackpill en J1/J2, 4G XBee en J7/J8, simpleRTK2B en MOD1)
|
||||||
|
en lugar del MCU y Ethernet on-board de V5. Ingeniería debe confirmar los puntos
|
||||||
|
marcados "PENDIENTE V4" antes de usarlo como registro oficial. */
|
||||||
|
Acta.render({
|
||||||
|
tipo: 'prueba-v4',
|
||||||
|
prefijo: 'V4',
|
||||||
|
series: 15,
|
||||||
|
borrador: true,
|
||||||
|
titulo: 'Acta de prueba — EinarBoard V4',
|
||||||
|
subtitulo: 'Borrador adaptado de V5 — revisar con ingeniería antes de uso oficial.',
|
||||||
|
secciones: [
|
||||||
|
{ titulo: '1 · Inspección visual (sin energía)',
|
||||||
|
sub: 'Con lupa. Marca cada casilla solo cuando lo verifiques.',
|
||||||
|
items: [
|
||||||
|
{ tipo:'check', txt:'Sin puentes de soldadura entre pines de <b>U6</b> (LTC4040), <b>U2</b>/<b>U7</b> (bucks MP1584), <b>U3</b>/<b>U4</b> (sensores).' },
|
||||||
|
{ tipo:'check', txt:'Zócalos de módulos limpios y sin pines doblados: <b>J1/J2</b> (Blackpill), <b>J7/J8</b> (4G), <b>MOD1</b> (simpleRTK2B).' },
|
||||||
|
{ tipo:'check', txt:'Botón <b>SW1</b> (RESET) presente y con "clic".' },
|
||||||
|
{ tipo:'check', txt:'Jumpers del cargador presentes: <b>JP2, JP3, JP4</b> (no tocar su posición).', aviso:'PENDIENTE V4: confirmar cristales y refs exactas con ingeniería.' },
|
||||||
|
{ tipo:'check', txt:'Sin componentes faltantes ni marcas de quemado.' },
|
||||||
|
] },
|
||||||
|
{ titulo: '2 · Continuidad — cortos a tierra (sin energía)',
|
||||||
|
sub: 'Multímetro en ohms, punta negra en GND. Espera a que la lectura se estabilice.',
|
||||||
|
items: [
|
||||||
|
{ tipo:'medida', txt:'Riel 3V3 (TP 3V3)', esperado:'> 1 kΩ (sube lento)', u:'Ω' },
|
||||||
|
{ tipo:'medida', txt:'P2 pin 1 — VEH5V (entrada 5 V)', esperado:'> 1 kΩ', u:'Ω' },
|
||||||
|
{ tipo:'medida', txt:'P11 pin 1 — MAIN5V', esperado:'> 1 kΩ', u:'Ω' },
|
||||||
|
{ tipo:'medida', txt:'P1 pin 1 — 24V_IN', esperado:'> 10 kΩ', u:'Ω' },
|
||||||
|
{ tipo:'medida', txt:'MOD1 pin "5V" — SW_5V (etapa RTK)', esperado:'> 1 kΩ', u:'Ω',
|
||||||
|
aviso:'Una lectura fija de 0–20 Ω = corto = FALLA.' },
|
||||||
|
] },
|
||||||
|
{ titulo: '3 · Riel 3.3 V',
|
||||||
|
sub: 'Energiza según el método de V4 (PENDIENTE V4: confirmar si hay USB DFU o se alimenta por P2).',
|
||||||
|
items: [
|
||||||
|
{ tipo:'medida', txt:'Riel 3V3 respecto a GND', esperado:'3.20 – 3.40 V', u:'V' },
|
||||||
|
{ tipo:'check', txt:'LED de encendido presente.' },
|
||||||
|
{ tipo:'check', txt:'Regulador 3V3 (U17 AMS1117) tibio, no quema.' },
|
||||||
|
] },
|
||||||
|
{ titulo: '4a · Riel 5 V con fuente de banco (entrada P2)',
|
||||||
|
sub: 'Fuente 5.0 V, límite 500 mA. Jumper en P11. Sin módulos insertados.',
|
||||||
|
items: [
|
||||||
|
{ tipo:'medida', txt:'Corriente de la fuente', esperado:'< 300 mA', u:'mA' },
|
||||||
|
{ tipo:'medida', txt:'P11 pin 1 — MAIN5V', esperado:'4.8 – 5.1 V', u:'V' },
|
||||||
|
{ tipo:'medida', txt:'P11 pin 2 — SYS5V (con jumper)', esperado:'4.8 – 5.1 V', u:'V' },
|
||||||
|
{ tipo:'medida', txt:'Riel 3V3', esperado:'3.20 – 3.40 V', u:'V' },
|
||||||
|
{ tipo:'medida', txt:'MOD1 pin "5V" — SW_5V', esperado:'0 V (obligatorio)', u:'V',
|
||||||
|
aviso:'Si hay 5 V aquí → FALLA (la etapa RTK debe arrancar apagada).' },
|
||||||
|
{ tipo:'check', txt:'<b>U6</b> tibio, no quema.' },
|
||||||
|
] },
|
||||||
|
{ titulo: '4b · Riel 24 V → buck del ventilador (entrada P1)',
|
||||||
|
sub: 'Fuente 24.0 V, límite 200 mA. Pin 3 (ignición) sin conectar.',
|
||||||
|
items: [
|
||||||
|
{ tipo:'medida', txt:'Corriente de la fuente', esperado:'< 50 mA', u:'mA' },
|
||||||
|
{ tipo:'medida', txt:'P6 pin 1 — 5V_FAN (contra GND)', esperado:'4.9 – 5.1 V', u:'V' },
|
||||||
|
{ tipo:'check', txt:'<b>U2</b> (buck) frío o tibio.' },
|
||||||
|
] },
|
||||||
|
{ titulo: '5 · Módulos y firmware (PENDIENTE V4)',
|
||||||
|
sub: 'V4 usa MCU Blackpill enchufable. El flujo de flasheo difiere de V5 — ingeniería debe definirlo.',
|
||||||
|
items: [
|
||||||
|
{ tipo:'nota', txt:'⚠ <b>PENDIENTE V4:</b> definir el método de flasheo del Blackpill y el firmware de arranque para V4. Mientras tanto, anota abajo lo que se hizo.' },
|
||||||
|
{ tipo:'check', txt:'Blackpill insertado correctamente en J1/J2 (orientación verificada).' },
|
||||||
|
{ tipo:'texto', txt:'Firmware cargado / observaciones', ph:'versión, método, resultado del arranque…' },
|
||||||
|
] },
|
||||||
|
{ titulo: '6 · Cierre',
|
||||||
|
items: [
|
||||||
|
{ tipo:'check', txt:'Tarjeta etiquetada con su número de serie y fecha.' },
|
||||||
|
{ tipo:'check', txt:'Guardada en bolsa antiestática (las FALLA aparte con la nota del punto que falló).' },
|
||||||
|
] },
|
||||||
|
]
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
91
docs/formularios/v5.html
Normal file
91
docs/formularios/v5.html
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Acta de prueba — EinarBoard V5</title>
|
||||||
|
<link rel="stylesheet" href="acta.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="acta"></div>
|
||||||
|
<script src="acta.js"></script>
|
||||||
|
<script>
|
||||||
|
Acta.render({
|
||||||
|
tipo: 'prueba-v5',
|
||||||
|
prefijo: 'V5',
|
||||||
|
series: 15,
|
||||||
|
titulo: 'Acta de prueba y flasheo — EinarBoard V5',
|
||||||
|
subtitulo: 'Lote 15 tarjetas (JLC 22-Ago). Llena cada sección durante la prueba.',
|
||||||
|
secciones: [
|
||||||
|
{ titulo: '1 · Inspección visual (sin energía)',
|
||||||
|
sub: 'Con lupa. Marca cada casilla solo cuando lo verifiques.',
|
||||||
|
items: [
|
||||||
|
{ tipo:'check', txt:'Sin puentes de soldadura entre pines de <b>U16</b> (MCU), <b>U11</b> (W5500), <b>U10</b> (SC16IS752), <b>U6</b> (LTC4040).' },
|
||||||
|
{ tipo:'check', txt:'<b>Y2</b> dice <code>3.6864</code> (o <code>36.864</code>). Y1 y Y3 = <code>25.000</code>, Y4 = 32.768 kHz.', aviso:'Si Y2 dice <code>1.8432</code> → marca y avisa a ingeniería.' },
|
||||||
|
{ tipo:'check', txt:'Botones <b>SW1</b> y <b>SW2</b> presentes y con "clic".' },
|
||||||
|
{ tipo:'check', txt:'Conectores <b>J11</b>, <b>J9</b>, <b>J10 (RJ45)</b> bien asentados, sin pines doblados.' },
|
||||||
|
{ tipo:'check', txt:'Jumpers del cargador presentes: <b>JP2, JP3, JP4</b> (no tocar su posición).' },
|
||||||
|
{ tipo:'check', txt:'Sin componentes faltantes ni marcas de quemado.' },
|
||||||
|
] },
|
||||||
|
{ titulo: '2 · Continuidad — cortos a tierra (sin energía)',
|
||||||
|
sub: 'Multímetro en ohms, punta negra en TP6 (GND). Espera a que la lectura se estabilice.',
|
||||||
|
items: [
|
||||||
|
{ tipo:'medida', txt:'TP7 — riel 3V3', esperado:'> 1 kΩ (sube lento)', u:'Ω' },
|
||||||
|
{ tipo:'medida', txt:'P2 pin 1 — VEH5V (entrada 5 V)', esperado:'> 1 kΩ', u:'Ω' },
|
||||||
|
{ tipo:'medida', txt:'P11 pin 1 — MAIN5V', esperado:'> 1 kΩ', u:'Ω' },
|
||||||
|
{ tipo:'medida', txt:'P1 pin 1 — 24V_IN', esperado:'> 10 kΩ', u:'Ω' },
|
||||||
|
{ tipo:'medida', txt:'MOD1 pin "5V" — SW_5V (etapa RTK)', esperado:'> 1 kΩ', u:'Ω',
|
||||||
|
aviso:'Una lectura fija de 0–20 Ω = corto = FALLA. Unos cientos de Ω que suben es normal (capacitores cargando).' },
|
||||||
|
] },
|
||||||
|
{ titulo: '3 · Riel 3.3 V por USB (J11)',
|
||||||
|
sub: 'Conecta el USB-C en J11 (junto a los botones). Solo energiza el MCU.',
|
||||||
|
items: [
|
||||||
|
{ tipo:'check', txt:'<b>D18</b> (LED verde PWR) <b>encendido</b>.' },
|
||||||
|
{ tipo:'medida', txt:'TP7 respecto a TP6', esperado:'3.20 – 3.40 V', u:'V' },
|
||||||
|
{ tipo:'check', txt:'<b>U17</b> (AMS1117) tibio tras 1 min (si quema → FALLA).' },
|
||||||
|
{ tipo:'nota', txt:'Deja el USB conectado para las siguientes secciones.' },
|
||||||
|
] },
|
||||||
|
{ titulo: '4a · Riel 5 V con fuente de banco (entrada P2)',
|
||||||
|
sub: 'Fuente 5.0 V, límite 500 mA. Coloca el jumper en P11. MCU virgen (sin firmware).',
|
||||||
|
items: [
|
||||||
|
{ tipo:'medida', txt:'Corriente de la fuente', esperado:'< 300 mA (típ. 100–250)', u:'mA' },
|
||||||
|
{ tipo:'medida', txt:'P11 pin 1 — MAIN5V', esperado:'4.8 – 5.1 V', u:'V' },
|
||||||
|
{ tipo:'medida', txt:'P11 pin 2 — SYS5V (con jumper)', esperado:'4.8 – 5.1 V', u:'V' },
|
||||||
|
{ tipo:'medida', txt:'TP7 — 3V3', esperado:'3.20 – 3.40 V', u:'V' },
|
||||||
|
{ tipo:'medida', txt:'MOD1 pin "5V" — SW_5V', esperado:'0 V (obligatorio)', u:'V',
|
||||||
|
aviso:'Si hay 5 V aquí → FALLA (la etapa RTK no arranca apagada).' },
|
||||||
|
{ tipo:'check', txt:'<b>U6</b> y <b>U12</b> tibios, no queman.' },
|
||||||
|
] },
|
||||||
|
{ titulo: '4b · Riel 24 V → buck del ventilador (entrada P1)',
|
||||||
|
sub: 'Fuente 24.0 V, límite 200 mA. Pin 3 (ignición) sin conectar.',
|
||||||
|
items: [
|
||||||
|
{ tipo:'medida', txt:'Corriente de la fuente', esperado:'< 50 mA (sin ventilador)', u:'mA' },
|
||||||
|
{ tipo:'medida', txt:'P6 pin 1 — 5V_FAN (contra TP6)', esperado:'4.9 – 5.1 V', u:'V' },
|
||||||
|
{ tipo:'check', txt:'<b>U2</b> (buck) frío o tibio.' },
|
||||||
|
{ tipo:'nota', txt:'Si algún valor está fuera de rango: marca FALLA y no flashees la tarjeta.' },
|
||||||
|
] },
|
||||||
|
{ titulo: '5 · Flasheo por USB DFU (J11)',
|
||||||
|
sub: 'Solo tarjetas que pasaron 1–4. Firmware: hbox-v5-bringup_0.1.0.bin @ 0x08000000.',
|
||||||
|
items: [
|
||||||
|
{ tipo:'check', txt:'DFU: mantener <b>SW2 (BOOT0)</b>, presionar y soltar <b>SW1 (RESET)</b>, soltar SW2 → aparece "STM32 BOOTLOADER" (0483:DF11).' },
|
||||||
|
{ tipo:'check', txt:'Flasheado con STM32CubeProgrammer (o dfu-util): "download complete" + "verify successful".' },
|
||||||
|
] },
|
||||||
|
{ titulo: '5d · Verificación después del flasheo',
|
||||||
|
sub: 'Abre un terminal serie a 115200 baud y pulsa SW1 (RESET).',
|
||||||
|
items: [
|
||||||
|
{ tipo:'check', txt:'<b>D17</b> (LED azul) parpadea a 1 Hz.' },
|
||||||
|
{ tipo:'check', txt:'<b>DB1 → DB4</b> se encienden en secuencia una vez.' },
|
||||||
|
{ tipo:'check', txt:'Aparece un puerto serie nuevo en la laptop.' },
|
||||||
|
{ tipo:'check', txt:'Encabezado <code>hbox-v5-bringup 0.1.0</code> y al final <code>>>> BOARD OK</code>.' },
|
||||||
|
{ tipo:'texto', txt:'Reporte / observaciones (anota cualquier línea [FAIL] completa)', ph:'---- RESULT: 12 PASS / 0 FAIL ----' },
|
||||||
|
] },
|
||||||
|
{ titulo: '6 · Cierre',
|
||||||
|
items: [
|
||||||
|
{ tipo:'check', txt:'Tarjeta etiquetada con su número de serie y fecha (cerca de MH4).' },
|
||||||
|
{ tipo:'check', txt:'Guardada en bolsa antiestática (las FALLA en bolsa aparte con la nota del punto que falló).' },
|
||||||
|
] },
|
||||||
|
]
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
52
docs/montaje/guia-ensamble-qc.md
Normal file
52
docs/montaje/guia-ensamble-qc.md
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
# Guía de ensamble y control de calidad — HBOX
|
||||||
|
|
||||||
|
!!! warning "Borrador para revisión"
|
||||||
|
Esta guía es un **borrador** basado en el BOM de ensamble y el pinout de arneses.
|
||||||
|
Ingeniería debe validarla antes de adoptarla como procedimiento oficial. El
|
||||||
|
**checklist interactivo** que la acompaña está en
|
||||||
|
[Actas → Checklist de ensamble](../actas/index.md).
|
||||||
|
|
||||||
|
El objetivo es que cada HBOX se arme siempre igual y quede un registro firmado por
|
||||||
|
unidad. El ensamble parte de una **PCBA ya probada y aprobada** en su acta de prueba
|
||||||
|
(V4 o V5); esta guía cubre lo que ocurre *después* de esa prueba: montaje de módulos,
|
||||||
|
mecánica, arnés y energizado final.
|
||||||
|
|
||||||
|
## Orden de trabajo
|
||||||
|
|
||||||
|
1. **Recepción de la PCBA.** Inspección visual de la tarjeta que llega de JLCPCB:
|
||||||
|
componentes completos, sin soldadura fría ni puentes, conectores rectos.
|
||||||
|
2. **Soldadura manual** (si el lote la requiere): terminal de potencia P1, conectores
|
||||||
|
JST, relevador K1, botón SW1, electrolíticos radiales y zócalos de módulos. Cuidar
|
||||||
|
polaridad de diodos y electrolíticos.
|
||||||
|
3. **Prueba eléctrica de la tarjeta** → se registra en su acta de prueba
|
||||||
|
([V5](../formularios/v5.html) / [V4](../formularios/v4.html)). **No se montan módulos
|
||||||
|
hasta que la tarjeta pase.**
|
||||||
|
4. **Módulos enchufables** sobre la tarjeta aprobada: MCU, simpleRTK2B en MOD1, módulo
|
||||||
|
4G. Verificar orientación **antes** de energizar.
|
||||||
|
5. **Mecánica:** fijar la tarjeta con separadores en sus barrenos, montar ventilador
|
||||||
|
(dirección de flujo), alinear conectores de panel con el gabinete.
|
||||||
|
6. **Arnés:** conectar cada JST contra el pinout, sin intercambiar conectores.
|
||||||
|
7. **Etiquetado:** número de serie del equipo, visible y legible.
|
||||||
|
8. **Energizado final** del equipo armado y registro del acta de ensamble.
|
||||||
|
|
||||||
|
## Referencia de conectores (arnés)
|
||||||
|
|
||||||
|
| Conector | Función | Puntos clave |
|
||||||
|
|---|---|---|
|
||||||
|
| **P1** GX16-5 | Alimentación de máquina + ignición | 1 = 24V ⚡, 3 = ignición, 2 = GND |
|
||||||
|
| **P2** | Entrada 5 V (Mean Well SD-15B) | 1 = VEH5V ⚡, 2 = GND |
|
||||||
|
| **P4** | Batería 1S4P + NTC | 1 = BAT+ ⚡, 3 = NTC, 2 = GND |
|
||||||
|
| **P6** | Ventilador | 1 = 5V_FAN ⚡, 2 = retorno, 4 = tacómetro |
|
||||||
|
| **P11** | Corte de energía (SYS→MAIN 5 V) | 1 = MAIN5V ⚡, 2 = SYS5V ⚡ |
|
||||||
|
| **MOD1** | Zócalo simpleRTK2B Pro | pin "5V" debe estar en 0 V con la tarjeta virgen |
|
||||||
|
|
||||||
|
*(Pinout completo en [Pinout de arneses](pinout-arneses.md). La guía de ensamble
|
||||||
|
detallada del gabinete es el [PDF 0007-1000](assembly-guide.md).)*
|
||||||
|
|
||||||
|
## Reglas de calidad
|
||||||
|
|
||||||
|
- Cada unidad lleva **dos actas**: la de **prueba de la tarjeta** y la de **ensamble**.
|
||||||
|
La de ensamble anota el número de serie de la tarjeta usada, para trazabilidad.
|
||||||
|
- Una tarjeta con FALLA **no se ensambla**; va a la bolsa de rechazos con su nota.
|
||||||
|
- Cualquier diferencia entre lo que ves y lo que dice el documento: **detente y
|
||||||
|
pregunta** a ingeniería antes de continuar.
|
||||||
@ -30,6 +30,7 @@ markdown_extensions:
|
|||||||
- admonition
|
- admonition
|
||||||
- tables
|
- tables
|
||||||
- attr_list
|
- attr_list
|
||||||
|
- md_in_html
|
||||||
- pymdownx.details
|
- pymdownx.details
|
||||||
- pymdownx.superfences
|
- pymdownx.superfences
|
||||||
- pymdownx.tasklist:
|
- pymdownx.tasklist:
|
||||||
@ -39,6 +40,11 @@ markdown_extensions:
|
|||||||
|
|
||||||
nav:
|
nav:
|
||||||
- Inicio: index.md
|
- Inicio: index.md
|
||||||
|
- Actas y control de calidad:
|
||||||
|
- "Índice de actas": actas/index.md
|
||||||
|
- "Acta de prueba V5": formularios/v5.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
|
||||||
- "EinarBoard 1.0 — Bring-up runbook": procedimientos/einarboard-1.0-bringup.md
|
- "EinarBoard 1.0 — Bring-up runbook": procedimientos/einarboard-1.0-bringup.md
|
||||||
@ -48,6 +54,7 @@ nav:
|
|||||||
- Montaje y arneses:
|
- Montaje y arneses:
|
||||||
- "Pinout de arneses y conectores": montaje/pinout-arneses.md
|
- "Pinout de arneses y conectores": montaje/pinout-arneses.md
|
||||||
- "BOM dividido para ensamble": montaje/bom-split.md
|
- "BOM dividido para ensamble": montaje/bom-split.md
|
||||||
|
- "Guía de ensamble y control de calidad": montaje/guia-ensamble-qc.md
|
||||||
- "Guía de ensamble (PDF)": montaje/assembly-guide.md
|
- "Guía de ensamble (PDF)": montaje/assembly-guide.md
|
||||||
- Diagnóstico:
|
- Diagnóstico:
|
||||||
- "Protocolo de diagnóstico remoto": diagnostico/protocolo-diagnostico-remoto.md
|
- "Protocolo de diagnóstico remoto": diagnostico/protocolo-diagnostico-remoto.md
|
||||||
|
|||||||
0
scripts/deploy.sh
Normal file → Executable file
0
scripts/deploy.sh
Normal file → Executable file
Loading…
Reference in New Issue
Block a user