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
|
||||
- tables
|
||||
- attr_list
|
||||
- md_in_html
|
||||
- pymdownx.details
|
||||
- pymdownx.superfences
|
||||
- pymdownx.tasklist:
|
||||
@ -39,6 +40,11 @@ markdown_extensions:
|
||||
|
||||
nav:
|
||||
- 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:
|
||||
- "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
|
||||
@ -48,6 +54,7 @@ nav:
|
||||
- Montaje y arneses:
|
||||
- "Pinout de arneses y conectores": montaje/pinout-arneses.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
|
||||
- Diagnóstico:
|
||||
- "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